diff --git a/.gitignore b/.gitignore index b81ecaff7..6e9f5ef33 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ hs_err_pid* *.iml +js_npm/evomaster-client-js /temp/ /dist.zip @@ -257,6 +258,24 @@ dotnet_3/em/embedded/rest/ScsDriver/generated-tests/ /jdk_11_gradle/em/external/rest/reservations-api/build/ /jdk_17_gradle/.gradle/ /jdk_8_maven/em/embedded/graphql/spring-petclinic-graphql/target/ +/jdk_17_gradle/cs/rest/bibliothek/build/ +/jdk_17_gradle/em/external/rest/bibliothek/build +/jdk_17_maven/cs/grpc/signal-registration/target/ +jdk_11_maven/cs/rest/pay-publicapi/target/ +jdk_11_maven/em/embedded/rest/pay-publicapi/target/ +jdk_11_maven/em/external/rest/ind1/target/ +jdk_17_maven/cs/rest/signal-server/event-logger/target/ +jdk_17_maven/cs/rest/signal-server/websocket-resources/target/ +jdk_17_maven/cs/rest/signal-server/integration-tests/target/ +jdk_17_maven/cs/rest/signal-server/service/target/ +jdk_17_maven/cs/rest/signal-server/api-doc/target/ +jdk_17_maven/em/embedded/rest/signal-server/target/ +jdk_17_maven/cs/rest/familie-tilbake/target/ +jdk_17_maven/em/embedded/rest/familie-tilbake/target/ +jdk_17_maven/cs/rest/familie-ba-sak/target/ +jdk_17_maven/cs/rest/tiltaksgjennomforing-api/target/ +jdk_17_maven/em/embedded/rest/familie-ba-sak/target/ + /jdk_8_maven/em/embedded/grpc/ncs/target/ /jdk_8_maven/em/embedded/grpc/scs/target/ /jdk_8_maven/em/external/grpc/ncs/target/ diff --git a/README.md b/README.md index 77bc7590e..5ae550a23 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,9 @@ [EvoMaster](http://evomaster.org) Benchmark (EMB): a set of web/enterprise applications for scientific research in Software Engineering. -We collected several different systems, in different programming languages, like -Java, Kotlin, JavaScript and C#. +We collected several different systems running on the JVM, in different programming languages such as Java and Kotlin. In this documentation, we will refer to these projects as System Under Test (SUT). -Currently, the SUTs are either _REST_ or _GraphQL_ APIs. +Currently, the SUTs are either _REST_, _GraphQL_ or _RPC_ APIs. For each SUT, we implemented _driver_ classes, which can programmatically _start_, _stop_ and _reset_ the state of SUT (e.g., data in SQL databases). As well as enable setting up different properties in a _uniform_ way, like choosing TCP port numbers for the HTTP servers. @@ -18,7 +17,7 @@ If a SUT uses any external services (e.g., a SQL database), these will be automa This collection of SUTs was originally assembled for easing experimentation with the fuzzer called [EvoMaster](http://evomaster.org). -However, finding this type of applications is not trivial among open-source projects. +However, finding this type of application is not trivial among open-source projects. Furthermore, it is not simple to sort out all the technical details on how to set these applications up and start them in a simple, uniform approach. Therefore, this repository provides the important contribution of providing all these necessary scripts for researchers that need this kind of case study. @@ -72,6 +71,10 @@ More details (e.g., #LOCs and used databases) on these APIs can be found [in thi ### REST: Java/Kotlin +* Familie Ba Sak (MIT), [jdk_17_maven/cs/rest/familie-ba-sak](jdk_17_maven/cs/rest/familie-ba-sak), from [https://github.com/navikt/familie-ba-sak](https://github.com/navikt/familie-ba-sak) + +* Payments Public API (MIT), [jdk_11_maven/cs/rest/pay-publicapi](jdk_11_maven/cs/rest/pay-publicapi), from [https://github.com/alphagov/pay-publicapi](https://github.com/alphagov/pay-publicapi) + * Session Service (not-known license), [jdk_8_maven/cs/rest/original/session-service](jdk_8_maven/cs/rest/original/session-service), from [https://github.com/cBioPortal/session-service](https://github.com/cBioPortal/session-service) * Bibliothek (MIT), [jdk_17_gradle/cs/rest/bibliothek](jdk_17_gradle/cs/rest/bibliothek), from [https://github.com/PaperMC/bibliothek](https://github.com/PaperMC/bibliothek) @@ -189,11 +192,45 @@ There are 2 main use cases for EMB: * Run experiments with other tools Everything can be setup by running the script `scripts/dist.py`. -Note that you will need installed at least JDK 8, JDK 11, NPM and .NET 3.x, as well as Docker. -Also, you will need to setup environment variables like `JAVA_HOME_8` and `JAVA_HOME_11`. +Note that you will need installed at least Maven, Gradle, JDK 8, JDK 11, JDK 17, NPM, as well as Docker. +Also, you will need to setup environment variables like `JAVA_HOME_8`, `JAVA_HOME_11` and `JAVA_HOME_17`. The script will issue error messages if any prerequisite is missing. Once the script is completed, all the SUTs will be available under the `dist` folder, and a `dist.zip` will be created as well (if `scripts/dist.py` is run with `True` as input). +Regarding Maven, most-third party dependencies are automatically downloaded from Maven Central. +However, some dependencies are from GitHub, which unfortunately require authentication to be able to download such dependencies. +Unfortunately, they have [no intention](https://github.com/orgs/community/discussions/26634) to fix this huge usability issue :( +In your home folder, you need to create a configuration file for Maven, in particular `.m2/settings.xml`, with the following configurations: + +``` + + + + + github + + YOURUSERNAME + ??? + + + + + Authorization + Bearer ??? + + + + + + +``` +Which configuration to use depends on the version of Maven (it was changed in version 3.9.0). +In latest versions of Maven, you need to create an authorization token in GitHub (see more info directly on [GitHub documentation pages](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry)), and put it instead of `???`. + + + [//]: # (There is also a Docker file to run `dist.py`, named `build.dockerfile`.) [//]: # (It can be built with:) @@ -210,20 +247,14 @@ Once the script is completed, all the SUTs will be available under the `dist` fo -Note that here the drivers will be built as well besides the SUTs, and the SUT themselves will also have an instrumented version (for white-box testing heuristics) for _EvoMaster_ (this is for JavaScript and .NET, whereas instrumentation for JVM is done at runtime, via an attached JavaAgent). - In the built `dist` folder, the files will be organized as follows: - -* For JVM: `-sut.jar` will be the non-instrumented SUTs, whereas their executable drivers will be called `-evomaster-runner.jar`. + `-sut.jar` will be the non-instrumented SUTs, whereas their executable drivers will be called `-evomaster-runner.jar`. Instrumentation can be done at runtime by attaching the `evomaster-agent.jar` JavaAgent. If you are running experiments with EvoMaster, this will be automatically attached when running experiments with `exp.py` (available in the EvoMaster's repository). Or it can be attached manually with JVM option `-Devomaster.instrumentation.jar.path=evomaster-agent.jar` when starting the driver. -* For NodeJS: under the folder `` (for each NodeJS SUT), the SUT is available under `src`, whereas the instrumented version is under `instrumented`. If the SUT is written in TypeScript, then the compiled version will be under `build`. -* For .NET: currently only the instrumented version is available (WORK IN PROGRESS) For running experiments with EvoMaster, you can also "start" each driver directly from an IDE (e.g., IntelliJ). Each of these drivers has a "main" method that is running a REST API (binding on default port 40100), where each operation (like start/stop/reset the SUT) can be called via an HTTP message by EvoMaster. -For JavaScript, you need to use the files `em-main.js` under the `instrumented/em` folders. @@ -237,18 +268,12 @@ Each folder represents a set of SUTs (and drivers) that can be built using the s For example, the folder `jdk_8_maven` contains all the SUTs that need JDK 8 and are built with Maven. On the other hand, the SUTs in the folder `jdk_11_gradle` require JDK 11 and Gradle. -For JVM and .NET, each module has 2 submodules, called `cs` (short for "Case Study") and `em` (short for "EvoMaster"). +For thr JVM, each module has 2 submodules, called `cs` (short for "Case Study") and `em` (short for "EvoMaster"). `cs` contains all the source code of the different SUTs, whereas `em` contains all the drivers. Note: building a top-module will build as well all of its internal submodules. -Regarding JavaScript, unfortunately NodeJS does not have a good handling of multi-module projects. -Each SUT has to be built separately. -However, for each SUT, we put its source code under a folder called `src`, whereas all the code related to the drivers is under `em`. -Currently, both NodeJS `14` and `16` should work on these SUTs. - -The driver classes for Java and .NET are called `EmbeddedEvoMasterController`. -For JavaScript, they are in a script file called `app-driver.js`. -Note that Java also a different kind of driver called `ExternalEvoMasterController`. +The driver classes for Java are called `EmbeddedEvoMasterController`. +Note that Java also has a different kind of driver called `ExternalEvoMasterController`. The difference is that in External the SUT is started on a separated process, and not running in the same JVM of the driver itself. @@ -292,13 +317,3 @@ Branch *develop* is using the most recent SNAPSHOT version of _EvoMaster_. As that is not published online, you need to clone its repository, and build it locally (see its documentation on how to do it). -To handle JavaScript, unfortunately there is the need for some manual settings. -However, it needs to be done just once. - -You need to create _symbolic_ link inside `EMB\js_npm` that points to the `evomaster-client-js` folder in _EvoMaster_. -How to do this, depends on the Operating System. -Note that in the following, `` should be replaced with the actual real paths of where you cloned the _EvoMaster_ and _EMB_ repositories. - -Windows: `mklink /D C:\\EMB\js_npm\evomaster-client-js C:\\EvoMaster\client-js\evomaster-client-js` - -Mac: `ln -s //EvoMaster/client-js/evomaster-client-js //EMB/js_npm/evomaster-client-js` \ No newline at end of file diff --git a/jdk_11_maven/cs/rest/pay-publicapi/.secrets.baseline b/jdk_11_maven/cs/rest/pay-publicapi/.secrets.baseline new file mode 100644 index 000000000..eb68136e4 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/.secrets.baseline @@ -0,0 +1,196 @@ +{ + "version": "1.4.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": { + "openapi/publicapi_spec.json": [ + { + "type": "Base64 High Entropy String", + "filename": "openapi/publicapi_spec.json", + "hashed_secret": "0ca33fee4444c18265ffce030b9e327b54f05ae0", + "is_verified": false, + "line_number": 602 + } + ], + "src/main/java/uk/gov/pay/api/model/CreateCardPaymentRequest.java": [ + { + "type": "Base64 High Entropy String", + "filename": "src/main/java/uk/gov/pay/api/model/CreateCardPaymentRequest.java", + "hashed_secret": "0ca33fee4444c18265ffce030b9e327b54f05ae0", + "is_verified": false, + "line_number": 202 + } + ], + "src/main/java/uk/gov/pay/api/resources/PaymentsResource.java": [ + { + "type": "Base64 High Entropy String", + "filename": "src/main/java/uk/gov/pay/api/resources/PaymentsResource.java", + "hashed_secret": "0ca33fee4444c18265ffce030b9e327b54f05ae0", + "is_verified": false, + "line_number": 241 + } + ], + "src/test/java/uk/gov/pay/api/filter/AuthorizationValidationFilterTest.java": [ + { + "type": "Secret Keyword", + "filename": "src/test/java/uk/gov/pay/api/filter/AuthorizationValidationFilterTest.java", + "hashed_secret": "70abceeb20d82fc2d55e8934d1ad05ad17609752", + "is_verified": false, + "line_number": 36 + }, + { + "type": "Secret Keyword", + "filename": "src/test/java/uk/gov/pay/api/filter/AuthorizationValidationFilterTest.java", + "hashed_secret": "a0936a38d2c31ad225d670f529a82319fc5bb915", + "is_verified": false, + "line_number": 87 + } + ], + "src/test/resources/config/empty-elevated-accounts-test-config.yaml": [ + { + "type": "Secret Keyword", + "filename": "src/test/resources/config/empty-elevated-accounts-test-config.yaml", + "hashed_secret": "3d4478f77d368235803ceb52bbd45b7240e6af62", + "is_verified": false, + "line_number": 48 + } + ], + "src/test/resources/config/test-config.yaml": [ + { + "type": "Secret Keyword", + "filename": "src/test/resources/config/test-config.yaml", + "hashed_secret": "3d4478f77d368235803ceb52bbd45b7240e6af62", + "is_verified": false, + "line_number": 50 + } + ], + "src/test/resources/pacts/publicapi-connector-get-payment-refund.json": [ + { + "type": "Base64 High Entropy String", + "filename": "src/test/resources/pacts/publicapi-connector-get-payment-refund.json", + "hashed_secret": "4c39a6a28507c3d7ea6de26da0bd1d27cff4a4af", + "is_verified": false, + "line_number": 25 + } + ], + "src/test/resources/pacts/publicapi-ledger-get-one-agreement.json": [ + { + "type": "Base64 High Entropy String", + "filename": "src/test/resources/pacts/publicapi-ledger-get-one-agreement.json", + "hashed_secret": "2d893b1b122fa0a884e02bb0a5b20764a80ef6e4", + "is_verified": false, + "line_number": 22 + } + ] + }, + "generated_at": "2023-09-06T14:26:21Z" +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/CONTRIBUTING.md b/jdk_11_maven/cs/rest/pay-publicapi/CONTRIBUTING.md new file mode 100644 index 000000000..2102a4b78 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/CONTRIBUTING.md @@ -0,0 +1,11 @@ +# GOV.UK Pay contributing guide + +This guide covers the basics of how to contribute to the GOV.UK Pay project. + +## Pull requests +The team's pull request checklist can be found [here](https://github.com/alphagov/pay-team-manual/blob/master/docs/development-processes/pull-request-checklist.md) + +## Contributions from beyond the team +If you have an idea to share or a feature to request to raise please contact the GOV.UK Pay team govuk-pay-support@digital.cabinet-office.gov.uk. + +If this is a security issue please do not submit a pull request or raise a GitHub issue, instead, please read the disclosure process [here](/README.md#responsible-disclosure). diff --git a/jdk_11_maven/cs/rest/pay-publicapi/Dockerfile b/jdk_11_maven/cs/rest/pay-publicapi/Dockerfile new file mode 100644 index 000000000..8d96a6acc --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/Dockerfile @@ -0,0 +1,28 @@ +FROM eclipse-temurin:11-jre-alpine@sha256:77f7c2509dba2f346bf042e424d395b16b0c432ee1eabf0cbcc17922dc900c73 + +RUN ["apk", "--no-cache", "upgrade"] + +ARG DNS_TTL=15 + +# Default to UTF-8 file.encoding +ENV LANG C.UTF-8 + +RUN echo networkaddress.cache.ttl=$DNS_TTL >> "$JAVA_HOME/conf/security/java.security" + +RUN ["apk", "add", "--no-cache", "bash", "tini"] + +ENV PORT 8080 +ENV ADMIN_PORT 8081 + +EXPOSE 8080 +EXPOSE 8081 + +WORKDIR /app + +ADD docker-startup.sh /app/docker-startup.sh +ADD target/*.yaml /app/ +ADD target/pay-*-allinone.jar /app/ + +ENTRYPOINT ["tini", "-e", "143", "--"] + +CMD ["bash", "./docker-startup.sh"] diff --git a/jdk_11_maven/cs/rest/pay-publicapi/LICENSE b/jdk_11_maven/cs/rest/pay-publicapi/LICENSE new file mode 100644 index 000000000..f8436059d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Crown Copyright (Government Digital Service) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/jdk_11_maven/cs/rest/pay-publicapi/README.md b/jdk_11_maven/cs/rest/pay-publicapi/README.md new file mode 100644 index 000000000..66a297933 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/README.md @@ -0,0 +1,59 @@ +# pay-publicapi + +GOV.UK Pay Public API service in Java (Dropwizard) + +## General configuration + +Configuration of the application is performed via environment variables, some of which are mandatory. + +| Variable | Required? | Default | Description | +| --------------------------- | --------- | -------------- | ---------------------------------------------------------------------------------------------------------- | +| `ADMIN_PORT` | No | 8081 | The port number to listen for Dropwizard admin requests on. | +| `ALLOW_HTTP_FOR_RETURN_URL` | No | false | Whether to allow service return URLs to be non-HTTPS | +| `CONNECTOR_URL` | Yes | N/A | The URL to the [connector](https://github.com/alphagov/pay-connector) service | +| `DISABLE_INTERNAL_HTTPS` | No | false | Disable secure connection for calls to internal APIs | +| `PORT` | No | 8080 | The port number to listen for requests on. | +| `PUBLICAPI_BASE` | Yes | N/A | The base URL clients can use to reach the API. e.g. http://api.example.org:1234/ | +| `PUBLIC_AUTH_URL` | Yes | N/A | The URL to the [publicauth](https://github.com/alphagov/pay-publicauth) service | +| `REDIS_URL` | No | localhost:6379 | The location of the redis endpoint to store rate-limiter information in | +| `TOKEN_API_HMAC_SECRET` | Yes | N/A | Hmac secret to be used to validate that the given token is genuine (Api Key = Token + Hmac (Token, Secret) | + +## Rate limiting + +The application will rate-limit incoming API requests, recording the current +rate limit state in Redis (see `REDIS_URL` above). The rate-limiting behaviour +can be tuned via the following environment variables which all have default +values: + +| Variable | Default | Description | +| ---------------------------------- | ------------ | ------------------------------------------ | +| `RATE_LIMITER_VALUE` | Default 75 | Number of non-`POST` requests allowed per `RATE_LIMITER_PER_MILLIS` milliseconds | +| `RATE_LIMITER_VALUE_POST` | Default 15 | Number of `POST` requests allowed per `RATE_LIMITER_PER_MILLIS` milliseconds | +| `RATE_LIMITER_ELEVATED_ACCOUNTS` | N/A | Comma-separated list of accounts to which `..._ELEVATED_...` limits apply (example: `1,2,3`) | +| `RATE_LIMITER_ELEVATED_VALUE_GET` | Default 100 | Number of non-`POST` requests allowed per `RATE_LIMITER_PER_MILLIS` milliseconds (for `RATE_LIMITER_ELEVATED_ACCOUNTS`) | +| `RATE_LIMITER_ELEVATED_VALUE_POST` | Default 40 | Number of `POST` requests allowed per `RATE_LIMITER_PER_MILLIS` milliseconds (for `RATE_LIMITER_ELEVATED_ACCOUNTS`) | +| `RATE_LIMITER_VALUE_PER_NODE` | Default 25 | Number of non-`POST` requests allowed per `RATE_LIMITER_PER_MILLIS` milliseconds for a given client | +| `RATE_LIMITER_VALUE_PER_NODE_POST` | Default 5 | Number of `POST` requests allowed per `RATE_LIMITER_PER_MILLIS` milliseconds for a given client | +| `RATE_LIMITER_PER_MILLIS` | Default 1000 | Rate limiter time window | +| `RATE_LIMITER_LOW_TRAFFIC_ACCOUNTS` | N/A | Comma-separated list of accounts to which `..._LOW_TRAFFIC_...` limits apply (example: `5,6,7`) | +| `RATE_LIMITER_LOW_TRAFFIC_VALUE_GET` | Default 4500 | Number of non-`POST` requests allowed per `RATE_LIMITER_LOW_TRAFFIC_PER_MILLIS` in milliseconds for a given account (for `RATE_LIMITER_LOW_TRAFFIC_ACCOUNTS`) | +| `RATE_LIMITER_LOW_TRAFFIC_VALUE_POST`| Default 1 | Number of `POST` requests allowed per `RATE_LIMITER_LOW_TRAFFIC_PER_MILLIS` in milliseconds (for `RATE_LIMITER_LOW_TRAFFIC_ACCOUNTS`) | +| `RATE_LIMITER_LOW_TRAFFIC_PER_MILLIS`| Default 60000| rate limit internal per `RATE_LIMITER_LOW_TRAFFIC_PER_MILLIS` (in milliseconds) for `RATE_LIMITER_LOW_TRAFFIC_ACCOUNTS` | + +## API specification + +Read our [developer documentation](https://docs.payments.service.gov.uk/#gov-uk-pay-documentation) for guidance on using the API. + +For more detailed information you can use our [OpenAPI specifiation](https://github.com/alphagov/pay-publicapi/blob/master/openapi/publicapi_spec.json) + +## Dependencies + +- https://www.mock-server.com/ is used for mocking dependent services + +## Licence + +[MIT License](LICENSE) + +## Vulnerability Disclosure + +GOV.UK Pay aims to stay secure for everyone. If you are a security researcher and have discovered a security vulnerability in this code, we appreciate your help in disclosing it to us in a responsible manner. Please refer to our [vulnerability disclosure policy](https://www.gov.uk/help/report-vulnerability) and our [security.txt](https://vdp.cabinetoffice.gov.uk/.well-known/security.txt) file for details. diff --git a/jdk_11_maven/cs/rest/pay-publicapi/build-local.sh b/jdk_11_maven/cs/rest/pay-publicapi/build-local.sh new file mode 100644 index 000000000..bef544d15 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/build-local.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +cd "$(dirname "$0")" + +mvn -DskipITs clean verify +if [ "$(uname -m)" == "arm64" ]; then + docker build -t governmentdigitalservice/pay-publicapi:local -f m1/arm64.Dockerfile . +else + docker build -t governmentdigitalservice/pay-publicapi:local . +fi diff --git a/jdk_11_maven/cs/rest/pay-publicapi/docker-startup.sh b/jdk_11_maven/cs/rest/pay-publicapi/docker-startup.sh new file mode 100644 index 000000000..ddb7914b7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/docker-startup.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -eu + +exec java ${JAVA_OPTS} -jar *-allinone.jar server *.yaml diff --git a/jdk_11_maven/cs/rest/pay-publicapi/docs/arch/adr-001-use-lettuce-core-lib-instead-of-dropwizard-redis.md b/jdk_11_maven/cs/rest/pay-publicapi/docs/arch/adr-001-use-lettuce-core-lib-instead-of-dropwizard-redis.md new file mode 100644 index 000000000..8c38a7466 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/docs/arch/adr-001-use-lettuce-core-lib-instead-of-dropwizard-redis.md @@ -0,0 +1,44 @@ +# ADR 001 - Use lettuce-core library instead of dropwizard redis + +## Context + +We were using an [unsupported dropwizard-redis](https://github.com/benjamin-bader/droptools/tree/master/dropwizard-redis) bundle. Dropwizard now offer a managed bundle which provides a managed redis. This bundle uses `lettuce-core` instead of `jedis`. + +[We wanted to upgrade to this new library](https://payments-platform.atlassian.net/browse/PP-6343). + +The advantages of using this library are: + +* Configuration +* Client lifecycle management +* Client health checks +* Dropwizard Metrics integration + +However, the dropwizard-redis library's io.dropwizard.redis.RedisClientFactory class +[expects](https://github.com/dropwizard/dropwizard-redis/blob/master/src/main/java/io/dropwizard/redis/RedisClientFactory.java#L54) +to make a connection with Redis on application startup. If a connection cannot be made an exception will be thrown +which causes the application to fail to start up. + +This has some unintended consequences: + +* Mandating a connection on startup causes many integration tests to fail. Making a redis connection available for every relevant test is a fairly big change. +* Mandating a connection might affect running Publicapi in dev/local environments. + +In order to work around this issue we could either: + +1. adapt the dropwizard-redis library so that it doesn't crash if the redis connection is not available +2. use lettuce-core directly + +| Option | Pros | Cons | +|---------|---------|---------| +| adapt | use common component; metrics instrumentation | more effort than justified | +| use lettuce-core directly | simpler; not hard; we don't need the extra features of dropwizard-redis; we don't need healthchecks because redis is optional | | + +On balance we think there are no advantages to us in using dropwizard-redis so we'll just use the lettuce-core library directly. + +## Decision + +We will use the [lettuce-core](https://github.com/lettuce-io/lettuce-core) directly. + +## Status + +Accepted diff --git a/jdk_11_maven/cs/rest/pay-publicapi/env.sh b/jdk_11_maven/cs/rest/pay-publicapi/env.sh new file mode 100644 index 000000000..34f376cee --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/env.sh @@ -0,0 +1,10 @@ +#!/bin/bash +ENV_FILE="$WORKSPACE/pay-scripts/services/publicapi.env" +if [ -f $ENV_FILE ] +then + set -a + source $ENV_FILE + set +a +fi + +eval "$@" diff --git a/jdk_11_maven/cs/rest/pay-publicapi/m1/arm64.Dockerfile b/jdk_11_maven/cs/rest/pay-publicapi/m1/arm64.Dockerfile new file mode 100644 index 000000000..d72962758 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/m1/arm64.Dockerfile @@ -0,0 +1,28 @@ +FROM eclipse-temurin:11-jre@sha256:e12585f95f18fba9841aa3c12e6e4261f74b608cbc27b0fab61ac267b57de8f6 + +RUN ["apt-get", "update"] + +ARG DNS_TTL=15 + +# Default to UTF-8 file.encoding +ENV LANG C.UTF-8 + +RUN echo networkaddress.cache.ttl=$DNS_TTL >> "$JAVA_HOME/conf/security/java.security" + +RUN ["apt-get", "install", "-y", "bash", "tini"] + +ENV PORT 8080 +ENV ADMIN_PORT 8081 + +EXPOSE 8080 +EXPOSE 8081 + +WORKDIR /app + +COPY docker-startup.sh /app/docker-startup.sh +COPY target/*.yaml /app/ +COPY target/pay-*-allinone.jar /app/ + +ENTRYPOINT ["tini", "-e", "143", "--"] + +CMD ["bash", "./docker-startup.sh"] diff --git a/jdk_11_maven/cs/rest/pay-publicapi/openapi/publicapi_spec.json b/jdk_11_maven/cs/rest/pay-publicapi/openapi/publicapi_spec.json new file mode 100644 index 000000000..fd8830b6a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/openapi/publicapi_spec.json @@ -0,0 +1,2774 @@ +{ + "openapi" : "3.0.1", + "info" : { + "description" : "The GOV.UK Pay REST API. Read [our documentation](https://docs.payments.service.gov.uk/) for more details.", + "title" : "GOV.UK Pay API", + "version" : "1.0.3" + }, + "servers" : [ { + "url" : "https://publicapi.payments.service.gov.uk" + } ], + "tags" : [ { + "name" : "Agreements" + }, { + "name" : "Card payments" + }, { + "name" : "Refunding card payments" + } ], + "paths" : { + "/v1/agreements" : { + "get" : { + "description" : "You can use this endpoint to search for recurring payments agreements. The agreements are sorted by date, with the most recently-created agreements appearing first.", + "operationId" : "Search agreements", + "parameters" : [ { + "description" : "Returns agreements with a `reference` that exactly matches the value you sent. This parameter is not case sensitive. A `reference` was associated with the agreement when that agreement was created.", + "example" : "CT-22-23-0001", + "in" : "query", + "name" : "reference", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns agreements in a matching `status`. `status` reflects where an agreement is in its lifecycle. You can [read more about the meanings of the different agreement status values](https://docs.payments.service.gov.uk/recurring_payments/#understanding-agreement-status).", + "in" : "query", + "name" : "status", + "schema" : { + "type" : "string", + "enum" : [ "created", "active", "cancelled", "inactive" ] + } + }, { + "description" : "Returns a specific page of results. Defaults to `1`. You can [read about search pagination](https://docs.payments.service.gov.uk/api_reference/#pagination)", + "example" : 1, + "in" : "query", + "name" : "page", + "schema" : { + "type" : "string" + } + }, { + "description" : "The number of agreements returned per results page. Defaults to `500`. Maximum value is `500`. You can [read about search pagination](https://docs.payments.service.gov.uk/api_reference/#pagination)", + "example" : 50, + "in" : "query", + "name" : "display_size", + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/AgreementSearchResults" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Your request failed. Check the `code` and `description` in the response to find out why your request failed." + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Search agreements for recurring payments", + "tags" : [ "Agreements" ] + }, + "post" : { + "description" : "You can use this endpoint to create a new agreement.", + "operationId" : "Create an agreement", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CreateAgreementRequest" + } + } + }, + "description" : "requestPayload", + "required" : true + }, + "responses" : { + "201" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Agreement" + } + } + }, + "description" : "Created" + }, + "400" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Bad request" + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Your request failed. Check the `code` and `description` in the response to find out why your request failed." + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Create an agreement for recurring payments", + "tags" : [ "Agreements" ] + } + }, + "/v1/agreements/{agreementId}" : { + "get" : { + "description" : "You can use this endpoint to get information about a single recurring payments agreement.", + "operationId" : "Get an agreement", + "parameters" : [ { + "description" : "Returns the agreement with the matching `agreement_id`. GOV.UK Pay generated an `agreement_id` when you created the agreement.", + "example" : "cgc1ocvh0pt9fqs0ma67r42l58", + "in" : "path", + "name" : "agreementId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Agreement" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Get information about a single agreement for recurring payments", + "tags" : [ "Agreements" ] + } + }, + "/v1/agreements/{agreementId}/cancel" : { + "post" : { + "description" : "You can use this endpoint to cancel a recurring payments agreement in the `active` status.", + "operationId" : "Cancel an agreement", + "parameters" : [ { + "description" : "The `agreement_id` of the agreement you are cancelling", + "example" : "cgc1ocvh0pt9fqs0ma67r42l58", + "in" : "path", + "name" : "agreementId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "No Content" + }, + "400" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Cancellation of agreement failed" + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Cancel an agreement for recurring payments", + "tags" : [ "Agreements" ] + } + }, + "/v1/auth" : { + "post" : { + "description" : "You can use this endpoint to [authorise payments](https://docs.payments.service.gov.uk/moto_payments/moto_send_card_details_api/) you have created with `authorisation_mode` set to `moto_api`.", + "operationId" : "Authorise a MOTO payment", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/AuthorisationRequest" + } + } + }, + "required" : true + }, + "responses" : { + "204" : { + "description" : "Your authorisation request was successful." + }, + "400" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Your request is invalid. Check the `code` and `description` in the response to find out why your request failed." + }, + "402" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "The `card_number` you sent is not a valid card number or you chose not to accept this card type. Check the `code` and `description` fields in the response to find out why your request failed." + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "A value you sent is invalid or missing. Check the `code` and `description` in the response to find out why your request failed." + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "There is something wrong with GOV.UK Pay. If there are no issues on our status page (https://payments.statuspage.io), you can contact us with your error code and we'll investigate." + } + }, + "summary" : "Send card details to authorise a MOTO payment", + "tags" : [ "Authorise card payments" ] + } + }, + "/v1/disputes" : { + "get" : { + "description" : "You can use this endpoint to search disputes. A dispute is when [a paying user challenges a completed payment through their bank](https://docs.payments.service.gov.uk/disputes/).", + "operationId" : "Search disputes", + "parameters" : [ { + "description" : "Returns disputes raised on or after the `from_date`. Date and time must be coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", + "example" : "2015-08-13T12:35:00Z", + "in" : "query", + "name" : "from_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns disputes raised before the `to_date`. Date and time must be coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", + "example" : "2015-08-13T12:35:00Z", + "in" : "query", + "name" : "to_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns disputes settled on or after the `from_settled_date`. Date must be in ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. Disputes are settled when your payment service provider takes the disputed amount from a payout to your bank account.", + "in" : "query", + "name" : "from_settled_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns disputes settled before the `to_settled_date`. Date must be in ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. Disputes are settled when your payment service provider takes the disputed amount from a payout to your bank account.", + "in" : "query", + "name" : "to_settled_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns disputes with a matching `status`. `status` reflects what stage of the dispute process a dispute is at. You can [read more about the meanings of the different status values](https://docs.payments.service.gov.uk/disputes/#dispute-status)", + "example" : "won", + "in" : "query", + "name" : "status", + "schema" : { + "type" : "string", + "enum" : [ "needs_response", "under_review", "lost", "won" ] + } + }, { + "description" : "Returns a specific page of results. Defaults to `1`.", + "in" : "query", + "name" : "page", + "schema" : { + "type" : "string" + } + }, { + "description" : "The number of disputes returned per results page. Defaults to `500`. Maximum value is `500`.", + "in" : "query", + "name" : "display_size", + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/DisputesSearchResults" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Invalid parameters: from_date, to_date, from_settled_date, to_settled_date, status, display_size. See Public API documentation for the correct data formats" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Search disputes", + "tags" : [ "Disputes" ] + } + }, + "/v1/payments" : { + "get" : { + "description" : "You can use this endpoint to [search for payments you’ve previously created](https://docs.payments.service.gov.uk/reporting/#search-payments/). Payments are sorted by date, with the most recently-created payment appearing first.", + "operationId" : "Search payments", + "parameters" : [ { + "description" : "Returns payments with `reference` values exactly matching your specified value.", + "in" : "query", + "name" : "reference", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments with matching `email` values. You can send full or partial email addresses. `email` is the paying user’s email address.", + "in" : "query", + "name" : "email", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments in a matching `state`. `state` reflects where a payment is in the [payment status lifecycle](https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle).", + "example" : "success", + "in" : "query", + "name" : "state", + "schema" : { + "type" : "string", + "enum" : [ "created", "started", "submitted", "success", "failed", "cancelled", "error" ] + } + }, { + "description" : "Returns payments paid with a particular card brand.", + "in" : "query", + "name" : "card_brand", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments created on or after the `from_date`. Date and time must be coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", + "example" : "2015-08-13T12:35:00Z", + "in" : "query", + "name" : "from_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments created before the `to_date`. Date and time must be coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", + "example" : "2015-08-13T12:35:00Z", + "in" : "query", + "name" : "to_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns a [specific page of results](https://docs.payments.service.gov.uk/api_reference/#pagination). Defaults to `1`.", + "in" : "query", + "name" : "page", + "schema" : { + "type" : "string" + } + }, { + "description" : "The number of payments returned [per results page](https://docs.payments.service.gov.uk/api_reference/#pagination). Defaults to `500`. Maximum value is `500`.", + "in" : "query", + "name" : "display_size", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments paid with cards under this cardholder name.", + "in" : "query", + "name" : "cardholder_name", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments paid by cards beginning with the `first_digits_card_number` value. `first_digits_card_number` value must be 6 digits.", + "in" : "query", + "name" : "first_digits_card_number", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments paid by cards ending with the `last_digits_card_number` value. `last_digits_card_number` value must be 4 digits.", + "in" : "query", + "name" : "last_digits_card_number", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments settled on or after the `from_settled_date` value. You can only search by settled date if your payment service provider is Stripe. Date must be in ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. Payments are settled when your payment service provider sends funds to your bank account.", + "in" : "query", + "name" : "from_settled_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments settled before the `to_settled_date` value. You can only search by settled date if your payment service provider is Stripe. Date must be in ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. Payments are settled when your payment service provider sends funds to your bank account.", + "in" : "query", + "name" : "to_settled_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments that were authorised using the agreement with this `agreement_id`. Must be an exact match.", + "example" : "abcefghjklmnopqr1234567890", + "in" : "query", + "name" : "agreement_id", + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PaymentSearchResults" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Invalid parameters: from_date, to_date, status, display_size. See Public API documentation for the correct data formats" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Search payments", + "tags" : [ "Card payments" ] + }, + "post" : { + "description" : "You can use this endpoint to [create a new payment](https://docs.payments.service.gov.uk/making_payments/).", + "operationId" : "Create a payment", + "parameters" : [ { + "in" : "header", + "name" : "Idempotency-Key", + "schema" : { + "type" : "string", + "pattern" : "^$|^[a-zA-Z0-9-]+$" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CreateCardPaymentRequest" + } + } + }, + "description" : "requestPayload", + "required" : true + }, + "responses" : { + "201" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CreatePaymentResult" + } + } + }, + "description" : "Created" + }, + "400" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Bad request" + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Your request failed. Check the `code` and `description` in the response to find out why your request failed." + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Create a payment", + "tags" : [ "Card payments" ] + } + }, + "/v1/payments/{paymentId}" : { + "get" : { + "description" : "You can use this endpoint to [get details about a single payment you’ve previously created](https://docs.payments.service.gov.uk/reporting/#get-information-about-a-single-payment).", + "operationId" : "Get a payment", + "parameters" : [ { + "description" : "Returns the payment with the matching `payment_id`.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PaymentWithAllLinks" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Get information about a single payment", + "tags" : [ "Card payments" ] + } + }, + "/v1/payments/{paymentId}/cancel" : { + "post" : { + "description" : "You can use this endpoint [to cancel an unfinished payment](https://docs.payments.service.gov.uk/making_payments/#cancel-a-payment-that-s-in-progress).", + "operationId" : "Cancel a payment", + "parameters" : [ { + "description" : "The `payment_id` of the payment you’re cancelling.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "No Content" + }, + "400" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Cancellation of payment failed" + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "409" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Conflict" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Cancel payment", + "tags" : [ "Card payments" ] + } + }, + "/v1/payments/{paymentId}/capture" : { + "post" : { + "description" : "You can use this endpoint to [take (‘capture’) a delayed payment from the paying user’s bank account](https://docs.payments.service.gov.uk/delayed_capture/).", + "operationId" : "Capture a payment", + "parameters" : [ { + "description" : "The `payment_id` of the payment you’re capturing.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "No Content" + }, + "400" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Capture of payment failed" + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "409" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Conflict" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Take a delayed payment", + "tags" : [ "Card payments" ] + } + }, + "/v1/payments/{paymentId}/events" : { + "get" : { + "description" : "You can use this endpoint to [get a list of a payment’s events](https://docs.payments.service.gov.uk/reporting/#get-a-payment-s-events). A payment event is when a payment’s `state` changes, such as when the payment is created, or when the paying user submits their details.", + "operationId" : "Get events for a payment", + "parameters" : [ { + "description" : "Payment identifier", + "example" : "hu20sqlact5260q2nanm0q8u93", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PaymentEvents" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Get a payment's events", + "tags" : [ "Card payments" ] + } + }, + "/v1/payments/{paymentId}/refunds" : { + "get" : { + "description" : "You can use this endpoint to [get a list of refunds for a payment](https://docs.payments.service.gov.uk/refunding_payments/#get-all-refunds-for-a-single-payment).", + "operationId" : "Get all refunds for a payment", + "parameters" : [ { + "description" : "The unique `payment_id` of the payment you want a list of refunds for.", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RefundForSearchResult" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Get information about a payment’s refunds", + "tags" : [ "Refunding card payments" ] + }, + "post" : { + "description" : "You can use this endpoint to [fully or partially refund a payment](https://docs.payments.service.gov.uk/refunding_payments).", + "operationId" : "Submit a refund for a payment", + "parameters" : [ { + "description" : "The unique `payment_id` of the payment you want to refund.", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PaymentRefundRequest" + } + } + }, + "description" : "requestPayload", + "required" : true + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Refund" + } + } + }, + "description" : "successful operation" + }, + "202" : { + "description" : "ACCEPTED" + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "412" : { + "description" : "Refund amount available mismatch" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Refund a payment", + "tags" : [ "Refunding card payments" ] + } + }, + "/v1/payments/{paymentId}/refunds/{refundId}" : { + "get" : { + "description" : "You can use this endpoint to [get details about an individual refund](https://docs.payments.service.gov.uk/refunding_payments/#checking-the-status-of-a-refund).", + "operationId" : "Get a payment refund", + "parameters" : [ { + "description" : "The unique `payment_id` of the payment you want to view a refund of.", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "description" : "The unique `refund_id` of the refund you want to view. If one payment has multiple refunds, each refund has a different `refund_id`.", + "in" : "path", + "name" : "refundId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Refund" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Check the status of a refund", + "tags" : [ "Refunding card payments" ] + } + }, + "/v1/refunds" : { + "get" : { + "description" : "You can use this endpoint to [search refunds you’ve previously created](https://docs.payments.service.gov.uk/refunding_payments/#searching-refunds). The refunds are sorted by date, with the most recently created refunds appearing first.", + "operationId" : "Search refunds", + "parameters" : [ { + "description" : "Returns refunds created on or after the `from_date`. Date and time must use Coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", + "example" : "2015-08-13T12:35:00Z", + "in" : "query", + "name" : "from_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns refunds created before the `to_date`. Date and time must use Coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", + "example" : "2015-08-13T12:35:00Z", + "in" : "query", + "name" : "to_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns refunds settled on or after the `from_settled_date` value. You can only use `from_settled_date` if your payment service provider is Stripe. Date must use ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. Refunds are settled when Stripe takes the refund from your account balance.", + "example" : "2022-08-13", + "in" : "query", + "name" : "from_settled_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns refunds settled before the `to_settled_date` value. You can only use `to_settled_date` if your payment service provider is Stripe. Date must use ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. Refunds are settled when Stripe takes the refund from your account balance.", + "example" : "2022-08-13", + "in" : "query", + "name" : "to_settled_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns a [specific page of results](https://docs.payments.service.gov.uk/api_reference/#pagination). Defaults to `1`.", + "in" : "query", + "name" : "page", + "schema" : { + "type" : "string" + } + }, { + "description" : "The number of refunds returned [per results page](https://docs.payments.service.gov.uk/api_reference/#pagination). Defaults to `500`. Maximum value is `500`.", + "in" : "query", + "name" : "display_size", + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RefundSearchResults" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Invalid parameters. See Public API documentation for the correct data formats" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Search refunds", + "tags" : [ "Refunding card payments" ] + } + } + }, + "components" : { + "schemas" : { + "Address" : { + "type" : "object", + "description" : "A structure representing the billing address of a card", + "properties" : { + "city" : { + "type" : "string", + "description" : "The paying user's city.", + "example" : "address city", + "maxLength" : 255, + "minLength" : 0 + }, + "country" : { + "type" : "string", + "description" : "The paying user’s country, displayed as a 2-character ISO-3166-1-alpha-2 code.", + "example" : "GB" + }, + "line1" : { + "type" : "string", + "description" : "The first line of the paying user’s address.", + "example" : "address line 1", + "maxLength" : 255, + "minLength" : 0 + }, + "line2" : { + "type" : "string", + "description" : "The second line of the paying user’s address.", + "example" : "address line 2", + "maxLength" : 255, + "minLength" : 0 + }, + "postcode" : { + "type" : "string", + "description" : "The paying user's postcode.", + "example" : "AB1 2CD", + "maxLength" : 25, + "minLength" : 0 + } + } + }, + "Agreement" : { + "type" : "object", + "description" : "Contains information about a user's agreement for recurring payments. An agreement represents an understanding between you and your paying user that you'll use their card to make ongoing payments for a service.", + "properties" : { + "agreement_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this agreement when you created it.", + "example" : "cgc1ocvh0pt9fqs0ma67r42l58" + }, + "cancelled_date" : { + "type" : "string", + "description" : "The date and time this agreement was cancelled. This value uses Coordinated Universal Time (UTC) and ISO 8601 format – `YYYY-MM-DDThh:mm:ss.sssZ`.", + "example" : "2022-07-08T14:33:00.000Z" + }, + "created_date" : { + "type" : "string", + "description" : "The date and time you created this agreement. This value uses Coordinated Universal Time (UTC) and ISO 8601 format – `YYYY-MM-DDThh:mm:ss.sssZ`.", + "example" : "2022-07-08T14:33:00.000Z" + }, + "description" : { + "type" : "string", + "description" : "The description you sent when creating this agreement.", + "example" : "Dorset Council 2022/23 council tax subscription." + }, + "payment_instrument" : { + "$ref" : "#/components/schemas/PaymentInstrument" + }, + "reference" : { + "type" : "string", + "description" : "The reference you sent when creating this agreement.", + "example" : "CT-22-23-0001" + }, + "status" : { + "type" : "string", + "description" : "The status of this agreement. You can [read more about the meanings of each agreement status.](https://docs.payments.service.gov.uk/recurring_payments/#understanding-agreement-status)", + "enum" : [ "created", "active", "cancelled", "inactive" ] + }, + "user_identifier" : { + "type" : "string", + "description" : "The identifier you sent when creating this agreement. `user_identifier` helps you identify users in your records.", + "example" : "user-3fb81107-76b7-4910" + } + } + }, + "AgreementSearchResults" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/SearchNavigationLinks" + }, + "count" : { + "type" : "integer", + "format" : "int32", + "description" : "Number of agreements on the current page of search results.", + "example" : 20 + }, + "page" : { + "type" : "integer", + "format" : "int32", + "description" : "The [page of agreements you’re viewing](https://docs.payments.service.gov.uk/api_reference/#pagination). To view other pages, make this request again using the `page` parameter.", + "example" : 1 + }, + "results" : { + "type" : "array", + "description" : "Contains agreements matching your search criteria.", + "items" : { + "$ref" : "#/components/schemas/Agreement" + } + }, + "total" : { + "type" : "integer", + "format" : "int32", + "description" : "Total number of agreements matching your search criteria.", + "example" : 100 + } + } + }, + "AuthorisationRequest" : { + "type" : "object", + "description" : "Contains the user's payment information. This information will be sent to the payment service provider to authorise the payment.", + "properties" : { + "card_number" : { + "type" : "string", + "description" : "The full card number from the paying user's card.", + "example" : "4242424242424242", + "maxLength" : 19, + "minLength" : 12 + }, + "cardholder_name" : { + "type" : "string", + "description" : "The name on the paying user's card.", + "example" : "J. Citizen", + "maxLength" : 255, + "minLength" : 0 + }, + "cvc" : { + "type" : "string", + "description" : "The card verification code (CVC) or card verification value (CVV) on the paying user's card.", + "example" : "123", + "maxLength" : 4, + "minLength" : 3 + }, + "expiry_date" : { + "type" : "string", + "description" : "The expiry date of the paying user's card. This value must be in `MM/YY` format.", + "example" : "09/22", + "maxLength" : 5, + "minLength" : 5 + }, + "one_time_token" : { + "type" : "string", + "description" : "This single use token authorises your request and matches it to a payment. GOV.UK Pay generated the `one_time_token` when the payment was created.", + "example" : "12345-edsfr-6789-gtyu" + } + }, + "required" : [ "card_number", "cardholder_name", "cvc", "expiry_date", "one_time_token" ] + }, + "AuthorisationSummary" : { + "type" : "object", + "description" : "Object containing information about the authentication of the payment.", + "properties" : { + "three_d_secure" : { + "$ref" : "#/components/schemas/ThreeDSecure" + } + } + }, + "CardDetails" : { + "type" : "object", + "description" : "A structure representing the payment card", + "properties" : { + "billing_address" : { + "$ref" : "#/components/schemas/Address" + }, + "card_brand" : { + "type" : "string", + "description" : "The brand of card the user paid with.", + "example" : "Visa", + "readOnly" : true + }, + "card_type" : { + "type" : "string", + "description" : "The type of card the user paid with.`null` means your user paid with Google Pay or we did not recognise which type of card they paid with.", + "enum" : [ "debit", "credit", "null" ], + "example" : "debit", + "readOnly" : true + }, + "cardholder_name" : { + "type" : "string", + "example" : "Mr. Card holder" + }, + "expiry_date" : { + "type" : "string", + "description" : "The expiry date of the card the user paid with in `MM/YY` format.", + "example" : "04/24", + "readOnly" : true + }, + "first_digits_card_number" : { + "type" : "string", + "example" : "123456", + "readOnly" : true + }, + "last_digits_card_number" : { + "type" : "string", + "example" : "1234", + "readOnly" : true + }, + "wallet_type" : { + "type" : "string", + "description" : "The digital wallet type that the user paid with", + "enum" : [ "Apple Pay", "Google Pay" ], + "example" : "Apple Pay" + } + } + }, + "CardDetailsFromResponse" : { + "type" : "object", + "properties" : { + "billing_address" : { + "$ref" : "#/components/schemas/Address" + }, + "card_brand" : { + "type" : "string" + }, + "card_type" : { + "type" : "string" + }, + "cardholder_name" : { + "type" : "string" + }, + "expiry_date" : { + "type" : "string" + }, + "first_digits_card_number" : { + "type" : "string" + }, + "last_digits_card_number" : { + "type" : "string" + } + } + }, + "CreateAgreementRequest" : { + "type" : "object", + "description" : "The Agreement Request Payload", + "properties" : { + "description" : { + "type" : "string", + "description" : "A human-readable description of the purpose of the agreement for recurring payments. We’ll show the description to your user when they make their first payment to activate this agreement. Limited to 255 characters.", + "example" : "Dorset Council 2022/23 council tax subscription.", + "maxLength" : 255, + "minLength" : 1 + }, + "reference" : { + "type" : "string", + "description" : "Associate a reference with this agreement to help you identify it. Limited to 255 characters.", + "example" : "CT-22-23-0001", + "maxLength" : 255, + "minLength" : 1 + }, + "user_identifier" : { + "type" : "string", + "description" : "Associate an identifier with the user who will enter into this agreement with your service.user_identifier is not unique – multiple agreements can have identical user_identifier values.You should not include personal data in user_identifier.", + "example" : "user-3fb81107-76b7-4910", + "maxLength" : 255, + "minLength" : 1 + } + } + }, + "CreateCardPaymentRequest" : { + "type" : "object", + "description" : "The create payment request body", + "properties" : { + "agreement_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with a recurring payments agreement. Including `agreement_id` in your request tells the API to take this payment using the card details that are associated with this agreement. `agreement_id` must match an active agreement ID. You must set `authorisation_mode` to `agreement` for the API to accept `agreement_id`.", + "example" : "abcefghjklmnopqr1234567890", + "maxLength" : 26, + "minLength" : 26 + }, + "amount" : { + "type" : "integer", + "format" : "int32", + "description" : "Sets the amount the user will pay, in pence.", + "example" : 12000, + "maximum" : 10000000, + "minimum" : 0 + }, + "authorisation_mode" : { + "type" : "string", + "description" : "Sets how you intend to authorise the payment. Defaults to `web`. Payments created with `web` mode follow the [standard GOV.UK Pay payment journey](https://docs.payments.service.gov.uk/payment_flow/). Paying users visit the `next_url` in the response to complete their payment. Payments created with `agreement` mode are authorised with an agreement for recurring payments. If you create an `agreement` payment, you must also send an active `agreement_id`. You must not send `return_url`, `email`, or `prefilled_cardholder_details` or your request will fail. Payments created with `moto_api` mode return an `auth_url_post` object and a `one_time_token`. You can use `auth_url_post` and `one_time_token` to send the paying user’s card details through the API and complete the payment. If you create a `moto_api` payment, do not send a `return_url` in your request.", + "enum" : [ "web", "agreement", "moto_api" ] + }, + "delayed_capture" : { + "type" : "boolean", + "description" : "You can use this parameter to [delay taking a payment from the paying user’s bank account](https://docs.payments.service.gov.uk/delayed_capture/#delay-taking-a-payment). For example, you might want to do your own anti-fraud checks on payments, or check that users are eligible for your service. Defaults to `false`.", + "example" : false + }, + "description" : { + "type" : "string", + "description" : "A human-readable description of the payment you’re creating. Paying users see this description on the payment pages. Service staff see the description in the GOV.UK Pay admin tool", + "example" : "New passport application", + "maxLength" : 255, + "minLength" : 0 + }, + "email" : { + "type" : "string", + "description" : "email", + "example" : "Joe.Bogs@example.org" + }, + "language" : { + "type" : "string", + "description" : "[Sets the language of the user’s payment page](https://docs.payments.service.gov.uk/optional_features/welsh_language) with an ISO-6391 Alpha-2 code of a supported language.", + "enum" : [ "en", "cy" ], + "example" : "en" + }, + "metadata" : { + "$ref" : "#/components/schemas/ExternalMetadata" + }, + "moto" : { + "type" : "boolean", + "description" : "You can use this parameter to [designate a payment as a Mail Order / Telephone Order (MOTO) payment](https://docs.payments.service.gov.uk/moto_payments).", + "example" : false + }, + "prefilled_cardholder_details" : { + "$ref" : "#/components/schemas/PrefilledCardholderDetails" + }, + "reference" : { + "type" : "string", + "description" : "Associate a reference with this payment. `reference` is not unique - multiple payments can have identical `reference` values.", + "example" : "12345", + "maxLength" : 255, + "minLength" : 0 + }, + "return_url" : { + "type" : "string", + "description" : "The URL [the paying user is directed to after their payment journey on GOV.UK Pay ends](https://docs.payments.service.gov.uk/making_payments/#choose-the-return-url-and-match-your-users-to-payments).", + "example" : "https://service-name.gov.uk/transactions/12345", + "maxLength" : 2000, + "minLength" : 0 + }, + "set_up_agreement" : { + "type" : "string", + "description" : "Use this parameter to set up an existing agreement for recurring payments. The `set_up_agreement` value you send must be a valid `agreement_id`.", + "example" : "abcefghjklmnopqr1234567890", + "maxLength" : 26, + "minLength" : 26 + } + }, + "required" : [ "amount", "description", "reference", "return_url" ] + }, + "CreatePaymentResult" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/PaymentLinks" + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The amount, in pence, the user has paid or will pay. `amount` will match the value you sent in the request body.", + "example" : 1200 + }, + "card_details" : { + "$ref" : "#/components/schemas/CardDetailsFromResponse" + }, + "created_date" : { + "type" : "string", + "description" : "The date you created the payment.", + "example" : "2016-01-21T17:15:00.000Z" + }, + "delayed_capture" : { + "type" : "boolean", + "description" : "`delayed_capture` is `true` if you’re controlling [when GOV.UK Pay takes (‘captures’) the payment from the paying user’s bank account](https://docs.payments.service.gov.uk/delayed_capture).", + "example" : false, + "readOnly" : true + }, + "description" : { + "type" : "string", + "description" : "The description you sent in the request body when creating this payment.", + "example" : "New passport application" + }, + "email" : { + "type" : "string", + "description" : "The paying user’s email address. The paying user’s email field will be prefilled with this value when they make their payment. `email` does not appear if you did not include it in the request body.", + "example" : "citizen@example.org" + }, + "language" : { + "type" : "string", + "description" : "The language of the user’s payment page.", + "enum" : [ "en", "cy" ], + "example" : "en" + }, + "metadata" : { + "$ref" : "#/components/schemas/ExternalMetadata" + }, + "moto" : { + "type" : "boolean", + "description" : "Indicates if this payment is a [Mail Order / Telephone Order (MOTO) payment](https://docs.payments.service.gov.uk/moto_payments).", + "example" : false + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93" + }, + "payment_provider" : { + "type" : "string", + "example" : "worldpay" + }, + "provider_id" : { + "type" : "string", + "description" : "The reference number your payment service provider associated with the payment.", + "example" : "null" + }, + "reference" : { + "type" : "string", + "description" : "The reference number you associated with this payment.", + "example" : "12345" + }, + "refund_summary" : { + "$ref" : "#/components/schemas/RefundSummary" + }, + "return_url" : { + "type" : "string", + "description" : "The URL you direct the paying user to after their payment journey on GOV.UK Pay ends.", + "example" : "https://service-name.gov.uk/transactions/12345" + }, + "settlement_summary" : { + "$ref" : "#/components/schemas/PaymentSettlementSummary" + }, + "state" : { + "$ref" : "#/components/schemas/PaymentState" + } + } + }, + "DisputeDetailForSearch" : { + "type" : "object", + "description" : "Contains disputes matching your search criteria.", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/DisputeLinksForSearch" + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The disputed amount in pence.", + "example" : 1200, + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "description" : "The date and time the user's bank told GOV.UK Pay about this dispute.", + "example" : "2022-07-28T16:43:00.000Z", + "readOnly" : true + }, + "dispute_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this dispute when the paying user disputed the payment.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "evidence_due_date" : { + "type" : "string", + "description" : "The deadline for submitting your supporting evidence. This value uses Coordinated Universal Time (UTC) and ISO 8601 format", + "example" : "2022-07-28T16:43:00.000Z", + "readOnly" : true + }, + "fee" : { + "type" : "integer", + "format" : "int64", + "description" : "The payment service provider’s dispute fee, in pence.", + "example" : 1200, + "readOnly" : true + }, + "net_amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The amount, in pence, your payment service provider will take for a lost dispute. 'net_amount' is deducted from your payout after you lose the dispute. For example, a 'net_amount' of '-1500' means your PSP will take £15.00 from your next payout into your bank account. 'net_amount' is always a negative value. 'net_amount' only appears if you lose the dispute.", + "example" : -2400, + "readOnly" : true + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "reason" : { + "type" : "string", + "description" : "The reason the paying user gave for disputing this payment. Possible values are: 'credit_not_processed', 'duplicate', 'fraudulent', 'general', 'product_not_received', 'product_unacceptable', 'unrecognised', 'subscription_cancelled', >'other'", + "example" : "fraudulent", + "readOnly" : true + }, + "settlement_summary" : { + "$ref" : "#/components/schemas/SettlementSummary" + }, + "status" : { + "type" : "string", + "description" : "The current status of the dispute. Possible values are: 'needs_response', 'won', 'lost', 'under_review'", + "example" : "under_review", + "readOnly" : true + } + } + }, + "DisputeLinksForSearch" : { + "type" : "object", + "description" : "links for search dispute resource", + "properties" : { + "payment" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "DisputesSearchResults" : { + "type" : "object", + "properties" : { + "count" : { + "type" : "integer", + "format" : "int32", + "description" : "Number of disputes on the current page of search results.", + "example" : 20 + }, + "links" : { + "$ref" : "#/components/schemas/SearchNavigationLinks" + }, + "page" : { + "type" : "integer", + "format" : "int32", + "description" : "The page of results you’re viewing. To view other pages, make this request again using the 'page' parameter.", + "example" : 1 + }, + "results" : { + "type" : "array", + "description" : "Contains disputes matching your search criteria.", + "items" : { + "$ref" : "#/components/schemas/DisputeDetailForSearch" + } + }, + "total" : { + "type" : "integer", + "format" : "int32", + "description" : "Number of total disputes matching your search criteria.", + "example" : 100 + } + } + }, + "EmbeddedRefunds" : { + "type" : "object", + "properties" : { + "refunds" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/Refund" + } + } + } + }, + "ErrorResponse" : { + "type" : "object", + "description" : "An error response", + "properties" : { + "code" : { + "type" : "string", + "description" : "A GOV.UK Pay API error code. You can [find out more about this code in our documentation](https://docs.payments.service.gov.uk/api_reference/#gov-uk-pay-api-error-codes).", + "example" : "P0900" + }, + "description" : { + "type" : "string", + "description" : "Additional details about the error", + "example" : "Too many requests" + } + } + }, + "ExternalMetadata" : { + "type" : "object", + "example" : "{\"property1\": \"value1\", \"property2\": \"value2\"}\"", + "properties" : { + "metadata" : { + "type" : "object", + "additionalProperties" : { + "type" : "object" + } + } + } + }, + "Link" : { + "type" : "object", + "description" : "A link related to a payment", + "properties" : { + "href" : { + "type" : "string", + "description" : "A URL that lets you perform additional actions to this payment when combined with the associated `method`.", + "example" : "https://an.example.link/from/payment/platform", + "readOnly" : true + }, + "method" : { + "type" : "string", + "description" : "An API method that lets you perform additional actions to this paymentwhen combined with the associated `href`.", + "example" : "GET", + "readOnly" : true + } + } + }, + "PaymentDetailForSearch" : { + "type" : "object", + "description" : "Contains payments matching your search criteria.", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/PaymentLinksForSearch" + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The description assigned to the payment when it was created.", + "example" : 1200 + }, + "authorisation_mode" : { + "type" : "string", + "description" : "How the payment will be authorised. Payments created in `web` mode require the paying user to visit the `next_url` to complete the payment.", + "enum" : [ "web", "moto_api", "external" ] + }, + "authorisation_summary" : { + "$ref" : "#/components/schemas/AuthorisationSummary" + }, + "card_brand" : { + "type" : "string", + "deprecated" : true, + "description" : "This attribute is deprecated. Please use `card_details.card_brand` instead.", + "example" : "Visa", + "readOnly" : true + }, + "card_details" : { + "$ref" : "#/components/schemas/CardDetails" + }, + "corporate_card_surcharge" : { + "type" : "integer", + "format" : "int64", + "description" : "The [corporate card surcharge](https://docs.payments.service.gov.uk/corporate_card_surcharges/#add-corporate-card-fees) amount in pence.", + "example" : 250, + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "example" : "2016-01-21T17:15:00.000Z", + "readOnly" : true + }, + "delayed_capture" : { + "type" : "boolean", + "description" : "`delayed_capture` is `true` if you’re [controlling how long it takes GOV.UK Pay to take (‘capture’) a payment](https://docs.payments.service.gov.uk/delayed_capture).", + "example" : false, + "readOnly" : true + }, + "description" : { + "type" : "string", + "description" : "The description assigned to the payment when it was created.", + "example" : "Your Service Description" + }, + "email" : { + "type" : "string", + "example" : "The paying user’s email address." + }, + "fee" : { + "type" : "integer", + "format" : "int64", + "description" : "The [payment service provider’s (PSP) transaction fee](https://docs.payments.service.gov.uk/reporting/#psp-fees), in pence. `fee` only appears when we have taken (‘captured’) the payment from the user or if their payment fails after they submitted their card details. `fee` will not appear if your PSP is Worldpay or you are using an API key from a test service.", + "example" : 5, + "readOnly" : true + }, + "language" : { + "type" : "string", + "description" : "The ISO-6391 Alpha-2 code of the [language of the user's payment page](https://docs.payments.service.gov.uk/optional_features/welsh_language).", + "enum" : [ "en", "cy" ], + "example" : "en" + }, + "metadata" : { + "$ref" : "#/components/schemas/ExternalMetadata" + }, + "moto" : { + "type" : "boolean", + "description" : "Indicates if this payment is a [Mail Order / Telephone Order (MOTO) payment](https://docs.payments.service.gov.uk/moto_payments).", + "example" : false + }, + "net_amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The amount, in pence, that will be paid into your bank account after your payment service provider takes the `fee`.", + "example" : 1195, + "readOnly" : true + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "payment_provider" : { + "type" : "string", + "description" : "The payment service provider that processed this payment.", + "example" : "worldpay", + "readOnly" : true + }, + "provider_id" : { + "type" : "string", + "description" : "The unique ID your payment service provider generated for this payment. This is not the same as the `payment_id`.", + "example" : "reference-from-payment-gateway", + "readOnly" : true + }, + "reference" : { + "type" : "string", + "description" : "The reference associated with the payment when it was created. `reference` is not unique - multiple payments can have the same `reference` value.", + "example" : "your-reference" + }, + "refund_summary" : { + "$ref" : "#/components/schemas/RefundSummary" + }, + "return_url" : { + "type" : "string", + "description" : "The URL you direct the paying user to after their payment journey on GOV.UK Pay ends.", + "example" : "http://your.service.domain/your-reference", + "readOnly" : true + }, + "settlement_summary" : { + "$ref" : "#/components/schemas/PaymentSettlementSummary" + }, + "state" : { + "$ref" : "#/components/schemas/PaymentState" + }, + "total_amount" : { + "type" : "integer", + "format" : "int64", + "description" : "Amount your user paid in pence, including corporate card fees. `total_amount` only appears if you [added a corporate card surcharge to the payment](https://docs.payments.service.gov.uk/corporate_card_surcharges/#add-corporate-card-fees).", + "example" : 1450, + "readOnly" : true + } + } + }, + "PaymentEvent" : { + "type" : "object", + "description" : "A List of Payment Events information", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/PaymentEventLink" + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "state" : { + "$ref" : "#/components/schemas/PaymentState" + }, + "updated" : { + "type" : "string", + "description" : "When this payment’s state changed. This value uses Coordinated Universal Time (UTC) and ISO-8601 format - `YYYY-MM-DDThh:mm:ss.SSSZ`.", + "example" : "2017-01-10T16:44:48.646Z", + "readOnly" : true + } + } + }, + "PaymentEventLink" : { + "type" : "object", + "description" : "Resource link for a payment of a payment event", + "properties" : { + "payment_url" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "PaymentEvents" : { + "type" : "object", + "description" : "A List of Payment Events information", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/PaymentLinksForEvents" + }, + "events" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PaymentEvent" + } + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + } + } + }, + "PaymentInstrument" : { + "type" : "object", + "properties" : { + "CardDetails" : { + "$ref" : "#/components/schemas/CardDetailsFromResponse" + }, + "created_date" : { + "type" : "string", + "description" : "The date and time you created this payment instrument. This value uses Coordinated Universal Time (UTC) and ISO 8601 format – `YYYY-MM-DDThh:mm:ss.sssZ`.", + "example" : "2022-07-08T14:33:00.000Z" + }, + "type" : { + "type" : "string", + "description" : "The type of payment instrument.", + "enum" : [ "card" ] + } + } + }, + "PaymentLinks" : { + "type" : "object", + "description" : "links for payment", + "properties" : { + "auth_url_post" : { + "$ref" : "#/components/schemas/PostLink" + }, + "cancel" : { + "$ref" : "#/components/schemas/PostLink" + }, + "capture" : { + "$ref" : "#/components/schemas/PostLink" + }, + "events" : { + "$ref" : "#/components/schemas/Link" + }, + "next_url" : { + "$ref" : "#/components/schemas/Link" + }, + "next_url_post" : { + "$ref" : "#/components/schemas/PostLink" + }, + "refunds" : { + "$ref" : "#/components/schemas/Link" + }, + "self" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "PaymentLinksForEvents" : { + "type" : "object", + "description" : "links for events resource", + "properties" : { + "self" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "PaymentLinksForSearch" : { + "type" : "object", + "description" : "links for search payment resource", + "properties" : { + "cancel" : { + "$ref" : "#/components/schemas/PostLink" + }, + "capture" : { + "$ref" : "#/components/schemas/PostLink" + }, + "events" : { + "$ref" : "#/components/schemas/Link" + }, + "refunds" : { + "$ref" : "#/components/schemas/Link" + }, + "self" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "PaymentRefundRequest" : { + "type" : "object", + "description" : "The Payment Refund Request Payload", + "properties" : { + "amount" : { + "type" : "integer", + "format" : "int32", + "description" : "The amount you want to [refund to your user](https://docs.payments.service.gov.uk/refunding_payments/) in pence.", + "example" : 150000, + "maximum" : 10000000, + "minimum" : 1 + }, + "refund_amount_available" : { + "type" : "integer", + "format" : "int32", + "description" : "Amount in pence. Total amount still available before issuing the refund", + "example" : 200000, + "maximum" : 10000000, + "minimum" : 1 + } + }, + "required" : [ "amount" ] + }, + "PaymentSearchResults" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/SearchNavigationLinks" + }, + "count" : { + "type" : "integer", + "format" : "int32", + "description" : "Number of payments on the current page of search results.", + "example" : 20 + }, + "page" : { + "type" : "integer", + "format" : "int32", + "description" : "The [page of results you’re viewing](https://docs.payments.service.gov.uk/api_reference/#pagination). To view other pages, make this request again using the `page` parameter.", + "example" : 1 + }, + "results" : { + "type" : "array", + "description" : "Contains payments matching your search criteria.", + "items" : { + "$ref" : "#/components/schemas/PaymentDetailForSearch" + } + }, + "total" : { + "type" : "integer", + "format" : "int32", + "description" : "Total number of payments matching your search criteria.", + "example" : 100 + } + } + }, + "PaymentSettlementSummary" : { + "type" : "object", + "description" : "A structure representing information about a settlement", + "properties" : { + "capture_submit_time" : { + "type" : "string", + "description" : "The date and time GOV.UK Pay asked your payment service provider to take the payment from your user’s account. This value uses Coordinated Universal Time (UTC) and ISO 8601 format - `YYYY-MM-DDThh:mm:ss.SSSZ`", + "example" : "2016-01-21T17:15:00.000Z", + "readOnly" : true + }, + "captured_date" : { + "type" : "string", + "description" : "The date your payment service provider took the payment from your user. This value uses ISO 8601 format - `YYYY-MM-DD`", + "example" : "2016-01-21", + "readOnly" : true + }, + "settled_date" : { + "type" : "string", + "description" : "The date that the transaction was paid into the service's account.", + "example" : "2016-01-21", + "readOnly" : true + } + } + }, + "PaymentState" : { + "type" : "object", + "description" : "A structure representing the current state of the payment in its lifecycle.", + "properties" : { + "can_retry" : { + "type" : "boolean", + "description" : "If `can_retry` is `true`, you can use this agreement to try to take another recurring payment. If `can_retry` is `false`, you cannot take another recurring payment with this agreement. `can_retry` only appears on failed payments that were attempted using an agreement for recurring payments.", + "nullable" : true, + "readOnly" : true + }, + "code" : { + "type" : "string", + "description" : "An [API error code](https://docs.payments.service.gov.uk/api_reference/#gov-uk-pay-api-error-codes)that explains why the payment failed. `code` only appears if the payment failed.", + "example" : "P010", + "readOnly" : true + }, + "finished" : { + "type" : "boolean", + "description" : "Indicates whether a payment journey is finished.", + "readOnly" : true + }, + "message" : { + "type" : "string", + "description" : "A description of what went wrong with this payment. `message` only appears if the payment failed.", + "example" : "User cancelled the payment", + "readOnly" : true + }, + "status" : { + "type" : "string", + "description" : "Where the payment is in [the payment status lifecycle](https://docs.payments.service.gov.uk/api_reference/#payment-status-meanings).", + "example" : "created", + "readOnly" : true + } + } + }, + "PaymentWithAllLinks" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/PaymentLinks" + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The description assigned to the payment when it was created.", + "example" : 1200 + }, + "authorisation_mode" : { + "type" : "string", + "description" : "How the payment will be authorised. Payments created in `web` mode require the paying user to visit the `next_url` to complete the payment.", + "enum" : [ "web", "moto_api", "external" ] + }, + "authorisation_summary" : { + "$ref" : "#/components/schemas/AuthorisationSummary" + }, + "card_brand" : { + "type" : "string", + "deprecated" : true, + "description" : "This attribute is deprecated. Please use `card_details.card_brand` instead.", + "example" : "Visa", + "readOnly" : true + }, + "card_details" : { + "$ref" : "#/components/schemas/CardDetails" + }, + "corporate_card_surcharge" : { + "type" : "integer", + "format" : "int64", + "description" : "The [corporate card surcharge](https://docs.payments.service.gov.uk/corporate_card_surcharges/#add-corporate-card-fees) amount in pence.", + "example" : 250, + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "example" : "2016-01-21T17:15:00.000Z", + "readOnly" : true + }, + "delayed_capture" : { + "type" : "boolean", + "description" : "`delayed_capture` is `true` if you’re [controlling how long it takes GOV.UK Pay to take (‘capture’) a payment](https://docs.payments.service.gov.uk/delayed_capture).", + "example" : false, + "readOnly" : true + }, + "description" : { + "type" : "string", + "description" : "The description assigned to the payment when it was created.", + "example" : "Your Service Description" + }, + "email" : { + "type" : "string", + "example" : "The paying user’s email address." + }, + "fee" : { + "type" : "integer", + "format" : "int64", + "description" : "The [payment service provider’s (PSP) transaction fee](https://docs.payments.service.gov.uk/reporting/#psp-fees), in pence. `fee` only appears when we have taken (‘captured’) the payment from the user or if their payment fails after they submitted their card details. `fee` will not appear if your PSP is Worldpay or you are using an API key from a test service.", + "example" : 5, + "readOnly" : true + }, + "language" : { + "type" : "string", + "description" : "The ISO-6391 Alpha-2 code of the [language of the user's payment page](https://docs.payments.service.gov.uk/optional_features/welsh_language).", + "enum" : [ "en", "cy" ], + "example" : "en" + }, + "metadata" : { + "$ref" : "#/components/schemas/ExternalMetadata" + }, + "moto" : { + "type" : "boolean", + "description" : "Indicates if this payment is a [Mail Order / Telephone Order (MOTO) payment](https://docs.payments.service.gov.uk/moto_payments).", + "example" : false + }, + "net_amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The amount, in pence, that will be paid into your bank account after your payment service provider takes the `fee`.", + "example" : 1195, + "readOnly" : true + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "payment_provider" : { + "type" : "string", + "description" : "The payment service provider that processed this payment.", + "example" : "worldpay", + "readOnly" : true + }, + "provider_id" : { + "type" : "string", + "description" : "The unique ID your payment service provider generated for this payment. This is not the same as the `payment_id`.", + "example" : "reference-from-payment-gateway", + "readOnly" : true + }, + "reference" : { + "type" : "string", + "description" : "The reference associated with the payment when it was created. `reference` is not unique - multiple payments can have the same `reference` value.", + "example" : "your-reference" + }, + "refund_summary" : { + "$ref" : "#/components/schemas/RefundSummary" + }, + "return_url" : { + "type" : "string", + "description" : "The URL you direct the paying user to after their payment journey on GOV.UK Pay ends.", + "example" : "http://your.service.domain/your-reference", + "readOnly" : true + }, + "settlement_summary" : { + "$ref" : "#/components/schemas/PaymentSettlementSummary" + }, + "state" : { + "$ref" : "#/components/schemas/PaymentState" + }, + "total_amount" : { + "type" : "integer", + "format" : "int64", + "description" : "Amount your user paid in pence, including corporate card fees. `total_amount` only appears if you [added a corporate card surcharge to the payment](https://docs.payments.service.gov.uk/corporate_card_surcharges/#add-corporate-card-fees).", + "example" : 1450, + "readOnly" : true + } + } + }, + "PostLink" : { + "type" : "object", + "description" : "A POST link related to a payment", + "properties" : { + "href" : { + "type" : "string", + "description" : "A URL that lets you perform additional actions to this payment when combined with the associated `method`.", + "example" : "https://an.example.link/from/payment/platform", + "readOnly" : true + }, + "method" : { + "type" : "string", + "example" : "POST", + "readOnly" : true + }, + "params" : { + "type" : "object", + "additionalProperties" : { + "type" : "object", + "example" : { + "description" : "This is a value for a parameter called description" + } + }, + "example" : { + "description" : "This is a value for a parameter called description" + } + }, + "type" : { + "type" : "string", + "example" : "application/x-www-form-urlencoded" + } + } + }, + "PrefilledCardholderDetails" : { + "type" : "object", + "description" : "prefilled_cardholder_details", + "properties" : { + "billing_address" : { + "$ref" : "#/components/schemas/Address" + }, + "cardholder_name" : { + "type" : "string", + "description" : "The cardholder name you prefilled when you created this payment.", + "example" : "J. Bogs", + "maxLength" : 255, + "minLength" : 0 + } + } + }, + "Refund" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/RefundLinksForSearch" + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The amount refunded to the user in pence.", + "example" : 120, + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "description" : "The date and time you created this refund. This value uses Coordinated Universal Time (UTC) and ISO 8601 format - `YYYY-MM-DDThh:mm:ss.SSSZ`.", + "example" : "2017-01-10T16:52:07.855Z", + "readOnly" : true + }, + "refund_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this refund when you created it.", + "example" : "act4c33g40j3edfmi8jknab84x", + "readOnly" : true + }, + "settlement_summary" : { + "$ref" : "#/components/schemas/RefundSettlementSummary" + }, + "status" : { + "type" : "string", + "description" : "The [status of the refund](https://docs.payments.service.gov.uk/refunding_payments/#checking-the-status-of-a-refund-status).", + "enum" : [ "submitted", "success", "error" ], + "example" : "success", + "readOnly" : true + } + } + }, + "RefundDetailForSearch" : { + "type" : "object", + "description" : "Contains the refunds matching your search criteria.", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/RefundLinksForSearch" + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The amount refunded to the user in pence.", + "example" : 120, + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "description" : "The date and time you created this refund. This value uses Coordinated Universal Time (UTC) and ISO 8601 format - `YYYY-MM-DDThh:mm:ss.SSSZ`.", + "example" : "2017-01-10T16:52:07.855Z", + "readOnly" : true + }, + "refund_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this refund when you created it.", + "example" : "act4c33g40j3edfmi8jknab84x", + "readOnly" : true + }, + "settlement_summary" : { + "$ref" : "#/components/schemas/RefundSettlementSummary" + }, + "status" : { + "type" : "string", + "description" : "The [status of the refund](https://docs.payments.service.gov.uk/refunding_payments/#checking-the-status-of-a-refund-status).", + "enum" : [ "submitted", "success", "error" ], + "example" : "success", + "readOnly" : true + } + } + }, + "RefundForSearchResult" : { + "type" : "object", + "properties" : { + "_embedded" : { + "$ref" : "#/components/schemas/EmbeddedRefunds" + }, + "_links" : { + "$ref" : "#/components/schemas/RefundLinksForSearch" + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93" + } + } + }, + "RefundLinksForSearch" : { + "type" : "object", + "description" : "links for search refunds resource", + "properties" : { + "payment" : { + "$ref" : "#/components/schemas/Link" + }, + "self" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "RefundSearchResults" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/SearchNavigationLinks" + }, + "count" : { + "type" : "integer", + "format" : "int32", + "description" : "Number of refunds on the current page of search results.", + "example" : 20 + }, + "page" : { + "type" : "integer", + "format" : "int32", + "description" : "The [page of results](payments.service.gov.uk/api_reference/#pagination) you’re viewing. To view other pages, make this request again using the `page` parameter.", + "example" : 1 + }, + "results" : { + "type" : "array", + "description" : "Contains the refunds matching your search criteria.", + "items" : { + "$ref" : "#/components/schemas/RefundDetailForSearch" + } + }, + "total" : { + "type" : "integer", + "format" : "int32", + "description" : "Number of refunds matching your search criteria.", + "example" : 100 + } + } + }, + "RefundSettlementSummary" : { + "type" : "object", + "description" : "A structure representing information about a settlement for refunds", + "properties" : { + "settled_date" : { + "type" : "string", + "description" : "The date Stripe took the refund from a payout to your bank account. `settled_date` only appears if Stripe has taken the refund. This value uses Coordinated Universal Time (UTC) and ISO 8601 format - `YYYY-MM-DD`.", + "example" : "2016-01-21", + "readOnly" : true + } + }, + "readOnly" : true + }, + "RefundSummary" : { + "type" : "object", + "description" : "A structure representing the refunds availability", + "properties" : { + "amount_available" : { + "type" : "integer", + "format" : "int64", + "description" : "How much you can refund to the user, in pence.", + "example" : 100, + "readOnly" : true + }, + "amount_submitted" : { + "type" : "integer", + "format" : "int64", + "description" : "How much you’ve already refunded to the user, in pence.", + "readOnly" : true + }, + "status" : { + "type" : "string", + "description" : "Whether you can [refund the payment](https://docs.payments.service.gov.uk/refunding_payments/#checking-the-status-of-a-refund-status).", + "example" : "available" + } + } + }, + "RefundsResponse" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/RefundLinksForSearch" + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93" + } + } + }, + "RequestError" : { + "type" : "object", + "description" : "A Request Error response", + "properties" : { + "code" : { + "type" : "string", + "description" : "An [API error code](https://docs.payments.service.gov.uk/api_reference/#gov-uk-pay-api-error-codes)that explains why the payment failed.

`code` only appears if the payment failed.", + "example" : "P0102" + }, + "description" : { + "type" : "string", + "description" : "Additional details about the error.", + "example" : "Invalid attribute value: amount. Must be less than or equal to 10000000" + }, + "field" : { + "type" : "string", + "description" : "The parameter in your request that's causing the error.", + "example" : "amount" + }, + "header" : { + "type" : "string", + "description" : "The header in your request that's causing the error.", + "example" : "Idempotency-Key" + } + } + }, + "SearchNavigationLinks" : { + "type" : "object", + "description" : "Links to navigate through pages of your search.", + "properties" : { + "first_page" : { + "$ref" : "#/components/schemas/Link" + }, + "last_page" : { + "$ref" : "#/components/schemas/Link" + }, + "next_page" : { + "$ref" : "#/components/schemas/Link" + }, + "prev_page" : { + "$ref" : "#/components/schemas/Link" + }, + "self" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "SettlementSummary" : { + "type" : "object", + "description" : "Contains information about when a lost dispute was settled. A dispute is settled when your payment service provider takes it from a payout to your bank account. 'settlement_summary' only appears if you lost the dispute.", + "properties" : { + "settled_date" : { + "type" : "string", + "description" : "The date your payment service provider took the disputed payment and dispute fee from a payout to your bank account. This value appears in ISO 8601 format - `YYYY-MM-DD`. `settled_date` only appears if you lost the dispute.", + "example" : "2022-07-28", + "readOnly" : true + } + } + }, + "ThreeDSecure" : { + "type" : "object", + "description" : "Object containing information about the 3D Secure authentication of the payment.", + "properties" : { + "required" : { + "type" : "boolean", + "description" : "Indicates if this payment was authorised with 3D Secure authentication. `required` is `true` if the payment required 3D Secure authentication." + } + } + } + }, + "securitySchemes" : { + "BearerAuth" : { + "description" : "GOV.UK Pay authenticates API calls with [OAuth2 HTTP bearer tokens](http://tools.ietf.org/html/rfc6750). You need to use an `\"Authorization\"` HTTP header to provide your API key, with a `\"Bearer\"` prefix. For example: `Authorization: Bearer {YOUR_API_KEY_HERE}`", + "scheme" : "bearer", + "type" : "http" + } + } + } +} \ No newline at end of file diff --git a/jdk_11_maven/cs/rest/pay-publicapi/pom.xml b/jdk_11_maven/cs/rest/pay-publicapi/pom.xml new file mode 100644 index 000000000..781002346 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/pom.xml @@ -0,0 +1,561 @@ + + 4.0.0 + uk.gov.pay + 0.1-SNAPSHOT + pay-publicapi + + + UTF-8 + 2.1.7 + 5.1.0 + 32.1.2-jre + 2.35.1 + 2.2 + 2.15.2 + 1.2.11 + 1.0.20230920091457 + 5.10.0 + 3.6.15 + 0.16.0 + 2.2.15 + + + + + + + + + + + + + + + + + + io.dropwizard + dropwizard-dependencies + 2.1.7 + pom + + + + + + + io.prometheus + simpleclient + ${prometheus.version} + + + io.prometheus + simpleclient_servlet + ${prometheus.version} + + + io.prometheus + simpleclient_dropwizard + ${prometheus.version} + + + uk.gov.service.payments + model + ${pay-java-commons.version} + + + uk.gov.service.payments + utils + ${pay-java-commons.version} + + + uk.gov.service.payments + validation + ${pay-java-commons.version} + + + uk.gov.service.payments + logging + ${pay-java-commons.version} + + + io.dropwizard + dropwizard-core + ${dropwizard.version} + + + io.dropwizard + dropwizard-auth + ${dropwizard.version} + + + io.dropwizard + dropwizard-client + ${dropwizard.version} + + + io.dropwizard + dropwizard-json-logging + ${dropwizard.version} + + + com.google.inject + guice + ${guice.version} + + + commons-validator + commons-validator + 1.7 + + + commons-collections + commons-collections + + + + + commons-codec + commons-codec + 1.16.0 + + + ch.qos.logback + logback-classic + ${logback.version} + + + ch.qos.logback + logback-access + ${logback.version} + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + commons-collections + commons-collections + 3.2.2 + + + com.google.guava + guava + ${guava.version} + + + org.dhatim + dropwizard-sentry + 2.1.2-4 + + + org.json + json + 20230618 + + + + uk.gov.service.payments + testing + ${pay-java-commons.version} + test + + + org.testcontainers + testcontainers + 1.19.0 + test + + + io.dropwizard + dropwizard-testing + ${dropwizard.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit5.version} + test + + + org.junit.jupiter + junit-jupiter-api + ${junit5.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit5.version} + test + + + org.junit.vintage + junit-vintage-engine + ${junit5.version} + test + + + org.mockito + mockito-junit-jupiter + 4.8.0 + test + + + io.rest-assured + rest-assured + 5.3.1 + test + + + com.jayway.jsonpath + json-path-assert + 2.8.0 + test + + + org.hamcrest + hamcrest-core + ${hamcrest.version} + test + + + org.hamcrest + hamcrest-library + ${hamcrest.version} + test + + + com.github.tomakehurst + wiremock-jre8 + ${wiremock.version} + + + org.mortbay.jetty + servlet-api + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-servlet + + + org.eclipse.jetty + jetty-servlets + + + org.eclipse.jetty + jetty-webapp + + + test + + + au.com.dius + pact-jvm-consumer-junit_2.12 + ${pact.version} + + + org.json + json + + + test + + + + io.swagger.core.v3 + swagger-annotations + ${swagger.lib.version} + compile + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-guava + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${jackson.version} + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-base + ${jackson.version} + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + ${jackson.version} + + + com.fasterxml.jackson.module + jackson-module-afterburner + ${jackson.version} + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + ${jackson.version} + + + com.google.code.gson + gson + 2.10.1 + + + org.exparity + hamcrest-date + 2.0.8 + test + + + pl.pragmatists + JUnitParams + 1.1.1 + test + + + black.door + hate + v1r4t5 + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + + + io.lettuce + lettuce-core + 6.2.6.RELEASE + + + javax.xml.bind + jaxb-api + 2.3.1 + compile + + + + + io.dropwizard + dropwizard-assets + 2.0.25 + compile + + + + + + + au.com.dius + pact-jvm-provider-maven_2.12 + ${pact.version} + + target/pacts + ${PACT_BROKER_URL} + ${PACT_BROKER_USERNAME} + ${PACT_BROKER_PASSWORD} + ${PACT_CONSUMER_VERSION} + + ${PACT_CONSUMER_TAG} + + true + + + + maven-compiler-plugin + 3.11.0 + + 11 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + true + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.6.0 + + + analyze + + analyze-only + + + false + + + + copy-dependencies + package + + copy-dependencies + + + true + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + copy-resources + validate + + copy-resources + + + ${basedir}/target + + + src/main/resources/config + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + pay-publicapi-sut + + + + uk.gov.pay.api.app.PublicApi + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.1.2 + + + failsafe-integration-tests + integration-test + + integration-test + verify + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + uk.gov.pay.api.app.PublicApi + + server + src/main/resources/config/config.yaml + + + + + io.swagger.core.v3 + swagger-maven-plugin + ${swagger.lib.version} + + openapi + publicapi_spec + JSON + true + ${basedir}/src/main/resources/swagger-config.yaml + + + + + compile + + resolve + + + + + + + diff --git a/jdk_11_maven/cs/rest/pay-publicapi/ruleset.xml b/jdk_11_maven/cs/rest/pay-publicapi/ruleset.xml new file mode 100644 index 000000000..e2a2172f8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/ruleset.xml @@ -0,0 +1,125 @@ + + + GOV.UK Pay PMD rules, primarily for use by Codacy + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/Agreement.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/Agreement.java new file mode 100644 index 000000000..0452f0373 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/Agreement.java @@ -0,0 +1,140 @@ +package uk.gov.pay.api.agreement.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.CardDetailsFromResponse; + +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Schema(description = "Contains information about a user's agreement for recurring payments. " + + "An agreement represents an understanding between you and your paying user that you'll use their card to make ongoing payments for a service.") +public class Agreement { + private String externalId; + private String reference; + private String description; + private String status; + private String createdDate; + private PaymentInstrument paymentInstrument; + private String userIdentifier; + private String cancelledDate; + + @JsonProperty("agreement_id") + @Schema(description = "The unique ID GOV.UK Pay automatically associated with this agreement when you created it.", + example = "cgc1ocvh0pt9fqs0ma67r42l58") + public String getExternalId() { + return externalId; + } + + @Schema(description = "The reference you sent when creating this agreement.", + example = "CT-22-23-0001") + public String getReference() { + return reference; + } + + @Schema(description = "The description you sent when creating this agreement.", + example = "Dorset Council 2022/23 council tax subscription.") + public String getDescription() { + return description; + } + + @Schema(description = "The status of this agreement. " + + "You can [read more about the meanings of each agreement status.](https://docs.payments.service.gov.uk/recurring_payments/#understanding-agreement-status)", + allowableValues = {"created", "active", "cancelled", "inactive"}) + public String getStatus() { + return Optional.ofNullable(status).map(String::toLowerCase).orElse(null); + } + + @Schema(description = "The date and time you created this agreement. " + + "This value uses Coordinated Universal Time (UTC) and ISO 8601 format – `YYYY-MM-DDThh:mm:ss.sssZ`.", + example = "2022-07-08T14:33:00.000Z") + public String getCreatedDate() { + return createdDate; + } + + @Schema(description = "The identifier you sent when creating this agreement. " + + "`user_identifier` helps you identify users in your records.", + example = "user-3fb81107-76b7-4910") + public String getUserIdentifier() { + return userIdentifier; + } + + @Schema(description = "The date and time this agreement was cancelled. " + + "This value uses Coordinated Universal Time (UTC) and ISO 8601 format – `YYYY-MM-DDThh:mm:ss.sssZ`.", + example = "2022-07-08T14:33:00.000Z") + public String getCancelledDate() { + return cancelledDate; + } + + public PaymentInstrument getPaymentInstrument() { + return paymentInstrument; + } + + public Agreement(String externalId, String reference, String description, String status, String createdDate, + PaymentInstrument paymentInstrument, String userIdentifier, String cancelledDate) { + this.externalId = externalId; + this.reference = reference; + this.description = description; + this.status = status; + this.createdDate = createdDate; + this.paymentInstrument = paymentInstrument; + this.userIdentifier = userIdentifier; + this.cancelledDate = cancelledDate; + } + + public static Agreement from(AgreementLedgerResponse agreementLedgerResponse) { + return new Agreement( + agreementLedgerResponse.getExternalId(), + agreementLedgerResponse.getReference(), + agreementLedgerResponse.getDescription(), + agreementLedgerResponse.getStatus(), + agreementLedgerResponse.getCreatedDate(), + Optional.ofNullable(agreementLedgerResponse.getPaymentInstrument()).map(PaymentInstrument::from).orElse(null), + agreementLedgerResponse.getUserIdentifier(), + agreementLedgerResponse.getCancelledDate()); + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class PaymentInstrument { + private final CardDetailsFromResponse cardDetails; + private final String createdDate; + private final String type; + + public PaymentInstrument(CardDetailsFromResponse cardDetails, String createdDate, String type) { + this.cardDetails = cardDetails; + this.createdDate = createdDate; + this.type = type; + } + + public static PaymentInstrument from(AgreementLedgerResponse.PaymentInstrumentLedgerResponse paymentInstrumentLedgerResponse) { + return new PaymentInstrument(paymentInstrumentLedgerResponse.getCardDetails(), paymentInstrumentLedgerResponse.getCreatedDate(), paymentInstrumentLedgerResponse.getType()); + } + + + @Schema(name = "CardDetails") + public CardDetailsFromResponse getCardDetails() { + return cardDetails; + } + + @Schema(description = "The date and time you created this payment instrument. " + + "This value uses Coordinated Universal Time (UTC) and ISO 8601 format – `YYYY-MM-DDThh:mm:ss.sssZ`.", + example = "2022-07-08T14:33:00.000Z") + public String getCreatedDate() { + return createdDate; + } + + @Schema(description = "The type of payment instrument.", + allowableValues = {"card"}) + public String getType() { + return Optional.ofNullable(type).map(String::toLowerCase).orElse(null); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/AgreementCreatedResponse.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/AgreementCreatedResponse.java new file mode 100644 index 000000000..e1ab914f0 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/AgreementCreatedResponse.java @@ -0,0 +1,33 @@ +package uk.gov.pay.api.agreement.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class AgreementCreatedResponse { + + @JsonProperty("agreement_id") + private String agreementId; + + public AgreementCreatedResponse() { + } + + public AgreementCreatedResponse(String agreementId){ + this.agreementId = agreementId; + } + + public String getAgreementId() { + return agreementId; + } + + @Override + public String toString() { + return "AgreementCreatedResponse{" + + "agreementId='" + agreementId +'}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/AgreementLedgerResponse.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/AgreementLedgerResponse.java new file mode 100644 index 000000000..9ba7488c7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/AgreementLedgerResponse.java @@ -0,0 +1,146 @@ +package uk.gov.pay.api.agreement.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import uk.gov.pay.api.model.CardDetailsFromResponse; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class AgreementLedgerResponse { + private String externalId; + private String serviceId; + private String reference; + private String description; + private String status; + private String createdDate; + private PaymentInstrumentLedgerResponse paymentInstrument; + private String userIdentifier; + private String cancelledDate; + + @JsonProperty("external_id") + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + @JsonProperty("id") + public String getExternalId() { + return externalId; + } + + public String getServiceId() { + return serviceId; + } + + public String getReference() { + return reference; + } + + public String getDescription() { + return description; + } + + public String getStatus() { + return status; + } + + public String getCreatedDate() { + return createdDate; + } + + public PaymentInstrumentLedgerResponse getPaymentInstrument() { + return paymentInstrument; + } + + public String getUserIdentifier() { + return userIdentifier; + } + + public String getCancelledDate() { + return cancelledDate; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class PaymentInstrumentLedgerResponse { + + private String externalId; + private String agreementExternalId; + private CardDetailsFromResponse cardDetails; + private String createdDate; + private String type; + + public PaymentInstrumentLedgerResponse() { + // Janet Jackson + } + + private PaymentInstrumentLedgerResponse(Builder builder) { + this.externalId = builder.externalId; + this.agreementExternalId = builder.agreementExternalId; + this.cardDetails = builder.cardDetails; + this.createdDate = builder.createdDate; + this.type = builder.type; + } + + public String getExternalId() { + return externalId; + } + + public String getAgreementExternalId() { + return agreementExternalId; + } + + public CardDetailsFromResponse getCardDetails() { + return cardDetails; + } + + public String getCreatedDate() { + return createdDate; + } + + public String getType() { + return type; + } + + public static class Builder { + private String externalId; + private String agreementExternalId; + private CardDetailsFromResponse cardDetails; + private String createdDate; + private String type; + + public PaymentInstrumentLedgerResponse build() { + return new PaymentInstrumentLedgerResponse(this); + } + + public Builder withExternalId(String externalId) { + this.externalId = externalId; + return this; + } + + public Builder withAgreementExternalId(String agreementExternalId) { + this.agreementExternalId = agreementExternalId; + return this; + } + + public Builder withCardDetails(CardDetailsFromResponse cardDetails) { + this.cardDetails = cardDetails; + return this; + } + + public Builder withCreatedDate(String createdDate) { + this.createdDate = createdDate; + return this; + } + + public Builder withType(String type) { + this.type = type; + return this; + } + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/AgreementSearchResults.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/AgreementSearchResults.java new file mode 100644 index 000000000..e58161615 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/AgreementSearchResults.java @@ -0,0 +1,61 @@ +package uk.gov.pay.api.agreement.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.links.SearchNavigationLinks; +import uk.gov.pay.api.model.search.SearchPagination; + +import java.util.List; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class AgreementSearchResults implements SearchPagination { + + @Schema(name = "total", example = "100", description = "Total number of agreements matching your search criteria.") + private int total; + @Schema(name = "count", example = "20", description = "Number of agreements on the current page of search results.") + private int count; + @Schema(name = "page", example = "1", + description = "The [page of agreements you’re viewing]" + + "(https://docs.payments.service.gov.uk/api_reference/#pagination). " + + "To view other pages, make this request again using the `page` parameter.") + private int page; + private List results; + @JsonProperty("_links") + @Schema(name = "_links") + SearchNavigationLinks links; + + public AgreementSearchResults(int total, int count, int page, List results, SearchNavigationLinks links) { + this.total = total; + this.count = count; + this.page = page; + this.results = results; + this.links = links; + } + + @Override + public int getTotal() { + return total; + } + + @Override + public int getCount() { + return count; + } + + @Override + public int getPage() { + return page; + } + + @Schema(name = "results", description = "Contains agreements matching your search criteria.") + public List getResults() { + return results; + } + + @Override + public SearchNavigationLinks getLinks() { + return links; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/CreateAgreementRequest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/CreateAgreementRequest.java new file mode 100644 index 000000000..19b219dae --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/model/CreateAgreementRequest.java @@ -0,0 +1,87 @@ +package uk.gov.pay.api.agreement.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.CreateAgreementRequestBuilder; +import uk.gov.pay.api.utils.JsonStringBuilder; +import javax.validation.constraints.Size; +import java.util.Objects; + +@Schema(description = "The Agreement Request Payload") +@JsonIgnoreProperties(ignoreUnknown = true) +public class CreateAgreementRequest { + + public static final String USER_IDENTIFIER_FIELD = "user_identifier"; + + public static final int MIN_LENGTH = 1; + public static final int MAX_LENGTH = 255; + + @JsonProperty("reference") + @Size(min= MIN_LENGTH, max = MAX_LENGTH, message = "Must be less than or equal to {max} characters length") + private String reference; + + @JsonProperty("description") + @Size(min= MIN_LENGTH, max = MAX_LENGTH, message = "Must be less than or equal to {max} characters length") + private String description; + + @JsonProperty(USER_IDENTIFIER_FIELD) + @Size(min= MIN_LENGTH, max = MAX_LENGTH, message = "Must be less than or equal to {max} characters length") + private String userIdentifier; + + public CreateAgreementRequest() { + // for Jackson + } + + public CreateAgreementRequest(CreateAgreementRequestBuilder builder) { + this.reference = builder.getReference(); + this.description = builder.getDescription(); + this.userIdentifier = builder.getUserIdentifier(); + } + + @Schema(description = "Associate a reference with this agreement to help you identify it. Limited to 255 characters.", + example = "CT-22-23-0001") + public String getReference() { + return reference; + } + + @Schema(description = "A human-readable description of the purpose of the agreement for recurring payments. " + + "We’ll show the description to your user when they make their first payment to activate this agreement. " + + "Limited to 255 characters.", + example = "Dorset Council 2022/23 council tax subscription.") + public String getDescription() { + return description; + } + + @Schema(description = "Associate an identifier with the user who will enter into this agreement with your service." + + "user_identifier is not unique – multiple agreements can have identical user_identifier values." + + "You should not include personal data in user_identifier.", + example = "user-3fb81107-76b7-4910") + public String getUserIdentifier() { + return userIdentifier; + } + + public String toConnectorPayload() { + var stringBuilder = new JsonStringBuilder() + .add("reference", this.getReference()) + .add("description", this.getDescription()); + + if (this.getUserIdentifier() != null) { + stringBuilder.add("user_identifier", this.getUserIdentifier()); + } + return stringBuilder.build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CreateAgreementRequest that = (CreateAgreementRequest) o; + return Objects.equals(reference, that.reference) && Objects.equals(description, that.description) && Objects.equals(userIdentifier, that.userIdentifier); + } + + @Override + public int hashCode() { + return Objects.hash(reference, description, userIdentifier); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/resource/AgreementsApiResource.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/resource/AgreementsApiResource.java new file mode 100644 index 000000000..958bbce99 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/resource/AgreementsApiResource.java @@ -0,0 +1,189 @@ +package uk.gov.pay.api.agreement.resource; + +import com.codahale.metrics.annotation.Timed; +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.agreement.model.Agreement; +import uk.gov.pay.api.agreement.model.AgreementSearchResults; +import uk.gov.pay.api.agreement.model.CreateAgreementRequest; +import uk.gov.pay.api.agreement.service.AgreementsService; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.ledger.model.AgreementSearchParams; +import uk.gov.pay.api.model.RequestError; +import uk.gov.pay.api.resources.error.ApiErrorResponse; + +import javax.inject.Inject; +import javax.validation.Valid; +import javax.ws.rs.BeanParam; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.apache.http.HttpStatus.SC_CREATED; +import static org.apache.http.HttpStatus.SC_NO_CONTENT; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_200_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_201_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_400_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_401_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_404_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_422_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_429_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_500_DESCRIPTION; + +@Path("/") +@Tag(name = "Agreements") +@Produces({"application/json"}) +public class AgreementsApiResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(AgreementsApiResource.class); + + private final AgreementsService agreementsService; + + @Inject + public AgreementsApiResource(AgreementsService agreementsService) { + this.agreementsService = agreementsService; + } + + @POST + @Path("/v1/agreements") + @Produces(APPLICATION_JSON) + @Consumes(APPLICATION_JSON) + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Create an agreement", + summary = "Create an agreement for recurring payments", + description = "You can use this endpoint to create a new agreement.", + responses = { + @ApiResponse(responseCode = "201", description = RESPONSE_201_DESCRIPTION, + content = @Content(schema = @Schema(implementation = Agreement.class))), + @ApiResponse(responseCode = "400", description = RESPONSE_400_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "401", + description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "422", + description = RESPONSE_422_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public Response createAgreement( + @Parameter(hidden = true) @Auth Account account, + @Parameter(required = true, description = "requestPayload") + @Valid CreateAgreementRequest createAgreementRequest) + { + LOGGER.info("Creating new agreement for reference {} and gateway accountID {}", + createAgreementRequest.getReference(), account.getAccountId()); + var agreementCreatedResponse = agreementsService.createAgreement(account, createAgreementRequest); + var agreementLedgerResponse = agreementsService.getAgreement(account, agreementCreatedResponse.getAgreementId()); + + LOGGER.info("Agreement returned (created): [ {} ]", agreementCreatedResponse); + return Response.status(SC_CREATED).entity(Agreement.from(agreementLedgerResponse)).build(); + } + + @GET + @Path("/v1/agreements/{agreementId}") + @Produces(APPLICATION_JSON) + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Get an agreement", + summary = "Get information about a single agreement for recurring payments", + description = "You can use this endpoint to get information about a single recurring payments agreement.", + responses = { + @ApiResponse(responseCode = "200", description = RESPONSE_200_DESCRIPTION, + content = @Content(schema = @Schema(implementation = Agreement.class))), + @ApiResponse(responseCode = "401", + description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "404", description = RESPONSE_404_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public Agreement getAgreement( + @Parameter(hidden = true) @Auth Account account, + @PathParam("agreementId") + @Parameter(name = "agreementId", + description = "Returns the agreement with the matching `agreement_id`. " + + "GOV.UK Pay generated an `agreement_id` when you created the agreement.", + example = "cgc1ocvh0pt9fqs0ma67r42l58") String agreementId) + { + LOGGER.info("Get agreement {} request", agreementId); + var agreementLedgerResponse = agreementsService.getAgreement(account, agreementId); + return Agreement.from(agreementLedgerResponse); + } + + @GET + @Timed + @Path("/v1/agreements") + @Produces(APPLICATION_JSON) + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Search agreements", + summary = "Search agreements for recurring payments", + description = "You can use this endpoint to search for recurring payments agreements. " + + "The agreements are sorted by date, with the most recently-created agreements appearing first.", + responses = { + @ApiResponse(responseCode = "200", description = RESPONSE_200_DESCRIPTION, + content = @Content(schema = @Schema(implementation = AgreementSearchResults.class))), + @ApiResponse(responseCode = "401", + description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "404", description = RESPONSE_404_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "422", + description = RESPONSE_422_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public AgreementSearchResults getAgreements(@Parameter(hidden = true) @Auth Account account, @BeanParam AgreementSearchParams searchParams) { + return agreementsService.searchAgreements(account, searchParams); + } + + @POST + @Path("/v1/agreements/{agreementId}/cancel") + @Produces(APPLICATION_JSON) + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Cancel an agreement", + summary = "Cancel an agreement for recurring payments", + description = "You can use this endpoint to cancel a recurring payments agreement in the `active` status.", + responses = { + @ApiResponse(responseCode = "204", description = "No Content"), + @ApiResponse(responseCode = "400", description = "Cancellation of agreement failed", + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "401", + description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "404", description = RESPONSE_404_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public Response cancelAgreement(@Parameter(hidden = true) @Auth Account account, @PathParam("agreementId") @Parameter(name = "agreementId", + description = "The `agreement_id` of the agreement you are cancelling", + example = "cgc1ocvh0pt9fqs0ma67r42l58") String agreementId) + { + agreementsService.cancelAgreement(account, agreementId); + return Response.status(SC_NO_CONTENT).build(); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/service/AgreementsService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/service/AgreementsService.java new file mode 100644 index 000000000..89a397b90 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/agreement/service/AgreementsService.java @@ -0,0 +1,61 @@ +package uk.gov.pay.api.agreement.service; + +import org.apache.http.HttpStatus; +import uk.gov.pay.api.agreement.model.Agreement; +import uk.gov.pay.api.agreement.model.AgreementCreatedResponse; +import uk.gov.pay.api.agreement.model.AgreementLedgerResponse; +import uk.gov.pay.api.agreement.model.AgreementSearchResults; +import uk.gov.pay.api.agreement.model.CreateAgreementRequest; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.CancelAgreementException; +import uk.gov.pay.api.exception.CreateAgreementException; +import uk.gov.pay.api.ledger.model.AgreementSearchParams; +import uk.gov.pay.api.ledger.model.SearchResults; +import uk.gov.pay.api.model.search.PaginationDecorator; +import uk.gov.pay.api.service.ConnectorService; +import uk.gov.pay.api.service.LedgerService; + +import javax.inject.Inject; +import javax.ws.rs.core.Response; +import java.util.stream.Collectors; + +public class AgreementsService { + private static final String AGREEMENTS_PATH = "/v1/agreements"; + private final ConnectorService connectorService; + private final LedgerService ledgerService; + private final PaginationDecorator paginationDecorator; + + @Inject + public AgreementsService(ConnectorService connectorService, LedgerService ledgerService, PaginationDecorator paginationDecorator) { + this.connectorService = connectorService; + this.ledgerService = ledgerService; + this.paginationDecorator = paginationDecorator; + } + + public AgreementCreatedResponse createAgreement(Account account, CreateAgreementRequest createAgreementRequest) { + return connectorService.createAgreement(account, createAgreementRequest); + } + + public Response cancelAgreement(Account account, String agreementId) { + connectorService.cancelAgreement(account, agreementId); + + return Response.noContent().build(); + } + + public AgreementLedgerResponse getAgreement(Account account, String agreementId) { + return ledgerService.getAgreement(account, agreementId); + } + + public AgreementSearchResults searchAgreements(Account account, AgreementSearchParams params) { + SearchResults ledgerResponse = ledgerService.searchAgreements(account, params); + return processLedgerResponse(ledgerResponse); + } + + private AgreementSearchResults processLedgerResponse(SearchResults searchResults) { + return new AgreementSearchResults(searchResults.getTotal(), + searchResults.getCount(), + searchResults.getPage(), + searchResults.getResults().stream().map(Agreement::from).collect(Collectors.toUnmodifiableList()), + paginationDecorator.transformLinksToPublicApiUri(searchResults.getLinks(), AGREEMENTS_PATH)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/PublicApi.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/PublicApi.java new file mode 100644 index 000000000..c87a5f537 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/PublicApi.java @@ -0,0 +1,220 @@ +package uk.gov.pay.api.app; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import io.dropwizard.Application; +import io.dropwizard.assets.AssetsBundle; +import io.dropwizard.auth.AuthDynamicFeature; +import io.dropwizard.auth.AuthValueFactoryProvider; +import io.dropwizard.auth.CachingAuthenticator; +import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter; +import io.dropwizard.configuration.EnvironmentVariableSubstitutor; +import io.dropwizard.configuration.SubstitutingSourceProvider; +import io.dropwizard.jersey.setup.JerseyEnvironment; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.dropwizard.DropwizardExports; +import io.prometheus.client.exporter.MetricsServlet; +import org.eclipse.jetty.server.AbstractNetworkConnector; +import org.eclipse.jetty.server.Server; +import org.glassfish.jersey.CommonProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.agreement.resource.AgreementsApiResource; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.PublicApiModule; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.auth.AccountAuthenticator; +import uk.gov.pay.api.exception.mapper.AgreementValidationExceptionMapper; +import uk.gov.pay.api.exception.mapper.AuthorisationRequestExceptionMapper; +import uk.gov.pay.api.exception.mapper.BadRefundsRequestExceptionMapper; +import uk.gov.pay.api.exception.mapper.BadRequestExceptionMapper; +import uk.gov.pay.api.exception.mapper.CancelAgreementExceptionMapper; +import uk.gov.pay.api.exception.mapper.CancelChargeExceptionMapper; +import uk.gov.pay.api.exception.mapper.CaptureChargeExceptionMapper; +import uk.gov.pay.api.exception.mapper.CreateAgreementExceptionMapper; +import uk.gov.pay.api.exception.mapper.CreateChargeExceptionMapper; +import uk.gov.pay.api.exception.mapper.CreateRefundExceptionMapper; +import uk.gov.pay.api.exception.mapper.DisputeValidationExceptionMapper; +import uk.gov.pay.api.exception.mapper.GetAgreementExceptionMapper; +import uk.gov.pay.api.exception.mapper.GetChargeExceptionMapper; +import uk.gov.pay.api.exception.mapper.GetEventsExceptionMapper; +import uk.gov.pay.api.exception.mapper.GetRefundExceptionMapper; +import uk.gov.pay.api.exception.mapper.GetRefundsExceptionMapper; +import uk.gov.pay.api.exception.mapper.InternalServerExceptionMapper; +import uk.gov.pay.api.exception.mapper.JsonProcessingExceptionMapper; +import uk.gov.pay.api.exception.mapper.PaymentValidationExceptionMapper; +import uk.gov.pay.api.exception.mapper.RefundsValidationExceptionMapper; +import uk.gov.pay.api.exception.mapper.SearchAgreementsExceptionMapper; +import uk.gov.pay.api.exception.mapper.SearchChargesExceptionMapper; +import uk.gov.pay.api.exception.mapper.SearchDisputesExceptionMapper; +import uk.gov.pay.api.exception.mapper.SearchRefundsExceptionMapper; +import uk.gov.pay.api.exception.mapper.ViolationExceptionMapper; +import uk.gov.pay.api.filter.AuthorizationValidationFilter; +import uk.gov.pay.api.filter.ClearMdcValuesFilter; +import uk.gov.pay.api.filter.LoggingMDCRequestFilter; +import uk.gov.pay.api.filter.RateLimiterFilter; +import uk.gov.pay.api.healthcheck.Ping; +import uk.gov.pay.api.ledger.resource.TransactionsResource; +import uk.gov.pay.api.managed.RedisClientManager; +import uk.gov.pay.api.resources.AuthorisationResource; +import uk.gov.pay.api.resources.HealthCheckResource; +import uk.gov.pay.api.resources.PaymentRefundsResource; +import uk.gov.pay.api.resources.PaymentsResource; +import uk.gov.pay.api.resources.RequestDeniedResource; +import uk.gov.pay.api.resources.SearchDisputesResource; +import uk.gov.pay.api.resources.SearchRefundsResource; +import uk.gov.pay.api.resources.SecuritytxtResource; +import uk.gov.pay.api.resources.telephone.TelephonePaymentNotificationResource; +import uk.gov.pay.api.validation.InjectingValidationFeature; +import uk.gov.service.payments.logging.GovUkPayDropwizardRequestJsonLogLayoutFactory; +import uk.gov.service.payments.logging.LoggingFilter; +import uk.gov.service.payments.logging.LogstashConsoleAppenderFactory; + +import javax.net.ssl.HttpsURLConnection; +import javax.servlet.FilterRegistration; + +import static java.util.EnumSet.of; +import static javax.servlet.DispatcherType.REQUEST; + +public class PublicApi extends Application { + + private static final Logger logger = LoggerFactory.getLogger(PublicApi.class); + + private static final String SERVICE_METRICS_NODE = "publicapi"; + + /** + * Added to check the status of the SUT, since there is no API + * available to access this information. + */ + private Server jettyServer; + + public int getJettyPort() { + return ((AbstractNetworkConnector)jettyServer.getConnectors()[0]).getLocalPort(); + } + + public Server getJettyServer() { + return jettyServer; + } + + @Override + public void initialize(Bootstrap bootstrap) { + // Added to server Swagger JSON as static file + bootstrap.addBundle(new AssetsBundle("/assets/", "/assets/")); + + bootstrap.setConfigurationSourceProvider( + new SubstitutingSourceProvider( + bootstrap.getConfigurationSourceProvider(), + new EnvironmentVariableSubstitutor(false) + ) + ); + bootstrap.getObjectMapper().getSubtypeResolver().registerSubtypes(LogstashConsoleAppenderFactory.class); + bootstrap.getObjectMapper().getSubtypeResolver().registerSubtypes(GovUkPayDropwizardRequestJsonLogLayoutFactory.class); + } + + @Override + public void run(PublicApiConfig configuration, Environment environment) { + initialiseSSLSocketFactory(); + + final Injector injector = Guice.createInjector(new PublicApiModule(configuration, environment)); + + environment.healthChecks().register("ping", new Ping()); + + environment.jersey().register(injector.getInstance(HealthCheckResource.class)); + environment.jersey().register(injector.getInstance(PaymentsResource.class)); + environment.jersey().register(injector.getInstance(AgreementsApiResource.class)); + environment.jersey().register(injector.getInstance(PaymentRefundsResource.class)); + environment.jersey().register(injector.getInstance(RequestDeniedResource.class)); + environment.jersey().register(injector.getInstance(SearchRefundsResource.class)); + environment.jersey().register(injector.getInstance(TransactionsResource.class)); + environment.jersey().register(injector.getInstance(TelephonePaymentNotificationResource.class)); + environment.jersey().register(new InjectingValidationFeature(injector)); + environment.jersey().register(injector.getInstance(SecuritytxtResource.class)); + environment.jersey().register(injector.getInstance(AuthorisationResource.class)); + environment.jersey().register(injector.getInstance(SearchDisputesResource.class)); + + environment.jersey().register(injector.getInstance(RateLimiterFilter.class)); + environment.jersey().register(injector.getInstance(LoggingMDCRequestFilter.class)); + + environment.servlets().addFilter("ClearMdcValuesFilter", injector.getInstance(ClearMdcValuesFilter.class)) + .addMappingForUrlPatterns(of(REQUEST), true, "/v1/*"); + + environment.servlets().addFilter("LoggingFilter", injector.getInstance(LoggingFilter.class)) + .addMappingForUrlPatterns(of(REQUEST), true, "/v1/*"); + + FilterRegistration.Dynamic authorizationValidationFilter = environment.servlets().addFilter("AuthorizationValidationFilter", injector.getInstance(AuthorizationValidationFilter.class)); + authorizationValidationFilter.setInitParameter("excludedUrls", "/v1/auth"); + authorizationValidationFilter.addMappingForUrlPatterns(of(REQUEST), true, "/v1/*"); + + /* + Turn off 'FilteringJacksonJaxbJsonProvider' which overrides dropwizard JacksonMessageBodyProvider. + Fails on Integration tests if not disabled. + - https://github.com/dropwizard/dropwizard/issues/1341 + */ + environment.jersey().property(CommonProperties.FEATURE_AUTO_DISCOVERY_DISABLE, Boolean.TRUE); + + CachingAuthenticator cachingAuthenticator = new CachingAuthenticator<>( + environment.metrics(), + injector.getInstance(AccountAuthenticator.class), + configuration.getAuthenticationCachePolicy()); + + environment.jersey().register(new AuthDynamicFeature( + new OAuthCredentialAuthFilter.Builder() + .setAuthenticator(cachingAuthenticator) + .setPrefix("Bearer") + .buildAuthFilter())); + environment.jersey().register(new AuthValueFactoryProvider.Binder<>(Account.class)); + + attachExceptionMappersTo(environment.jersey()); + + CollectorRegistry collectorRegistry = CollectorRegistry.defaultRegistry; + collectorRegistry.register(new DropwizardExports(environment.metrics())); + environment.admin().addServlet("prometheusMetrics", new MetricsServlet(collectorRegistry)).addMapping("/metrics"); + + environment.lifecycle().manage(injector.getInstance(RedisClientManager.class)); + + environment.lifecycle().addServerLifecycleListener(server -> jettyServer = server); + } + + /** + * Adding a call to initialise SSL socket factory at startup until we find a resolution for the following jersey client bug (JERSEY-3124). + * + * @see https://jersey.github.io/release-notes/2.24.html + */ + private void initialiseSSLSocketFactory() { + HttpsURLConnection.getDefaultSSLSocketFactory(); + } + + private void attachExceptionMappersTo(JerseyEnvironment jersey) { + jersey.register(ViolationExceptionMapper.class); + jersey.register(CreateChargeExceptionMapper.class); + jersey.register(GetChargeExceptionMapper.class); + jersey.register(GetEventsExceptionMapper.class); + jersey.register(SearchChargesExceptionMapper.class); + jersey.register(SearchRefundsExceptionMapper.class); + jersey.register(CancelChargeExceptionMapper.class); + jersey.register(PaymentValidationExceptionMapper.class); + jersey.register(CreateAgreementExceptionMapper.class); + jersey.register(SearchAgreementsExceptionMapper.class); + jersey.register(GetAgreementExceptionMapper.class); + jersey.register(CancelAgreementExceptionMapper.class); + jersey.register(AgreementValidationExceptionMapper.class); + jersey.register(RefundsValidationExceptionMapper.class); + jersey.register(BadRefundsRequestExceptionMapper.class); + jersey.register(BadRequestExceptionMapper.class); + jersey.register(CreateRefundExceptionMapper.class); + jersey.register(GetRefundExceptionMapper.class); + jersey.register(GetRefundsExceptionMapper.class); + jersey.register(CaptureChargeExceptionMapper.class); + jersey.register(JsonProcessingExceptionMapper.class); + jersey.register(AuthorisationRequestExceptionMapper.class); + jersey.register(InternalServerExceptionMapper.class); + jersey.register(DisputeValidationExceptionMapper.class); + jersey.register(SearchDisputesExceptionMapper.class); + } + + public static void main(String[] args) throws Exception { + new PublicApi().run(args); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/RestClientFactory.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/RestClientFactory.java new file mode 100644 index 000000000..b05b771ed --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/RestClientFactory.java @@ -0,0 +1,38 @@ +package uk.gov.pay.api.app; + +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.service.payments.logging.RestClientLoggingFilter; + +import javax.net.ssl.SSLContext; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import static java.lang.String.format; + +public class RestClientFactory { + private static final String TLSV1_2 = "TLSv1.2"; + + public static Client buildClient(RestClientConfig clientConfig) { + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + + if (!clientConfig.isDisabledSecureConnection()) { + try { + SSLContext sslContext = SSLContext.getInstance(TLSV1_2); + sslContext.init(null, null, null); + clientBuilder = clientBuilder.sslContext(sslContext); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException(format("Unable to find an SSL context for %s", TLSV1_2), e); + } + } + + Client client = clientBuilder.build(); + client.register(RestClientLoggingFilter.class); + + return client; + } + + private RestClientFactory() { + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/PublicApiConfig.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/PublicApiConfig.java new file mode 100644 index 000000000..cd8b17b29 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/PublicApiConfig.java @@ -0,0 +1,94 @@ +package uk.gov.pay.api.app.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.benmanes.caffeine.cache.CaffeineSpec; +import io.dropwizard.Configuration; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.net.URI; +import java.util.Optional; + +public class PublicApiConfig extends Configuration { + + @NotNull + private String baseUrl; + + @NotNull + private String connectorUrl; + + @NotNull + private String ledgerUrl; + + @NotNull + private String publicAuthUrl; + + @NotNull + private Boolean allowHttpForReturnUrl; + + private String apiKeyHmacSecret; + + @NotNull + private CaffeineSpec authenticationCachePolicy; + + @Valid + @NotNull + @JsonProperty("jerseyClientConfig") + private RestClientConfig restClientConfig; + + @NotNull + @JsonProperty + private RedisConfiguration redis; + + @Valid + @NotNull + @JsonProperty("rateLimiter") + private RateLimiterConfig rateLimiterConfig; + + @JsonProperty("ecsContainerMetadataUriV4") + private URI ecsContainerMetadataUriV4; + + public String getBaseUrl() { + return baseUrl; + } + + public String getConnectorUrl() { + return connectorUrl; + } + + public String getLedgerUrl() { + return ledgerUrl; + } + + public String getPublicAuthUrl() { + return publicAuthUrl; + } + + public Boolean getAllowHttpForReturnUrl() { + return allowHttpForReturnUrl; + } + + public String getApiKeyHmacSecret() { + return apiKeyHmacSecret; + } + + public RestClientConfig getRestClientConfig() { + return restClientConfig; + } + + public RateLimiterConfig getRateLimiterConfig() { + return rateLimiterConfig; + } + + public CaffeineSpec getAuthenticationCachePolicy() { + return authenticationCachePolicy; + } + + public RedisConfiguration getRedisConfiguration() { + return redis; + } + + public Optional getEcsContainerMetadataUriV4() { + return Optional.ofNullable(ecsContainerMetadataUriV4); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/PublicApiModule.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/PublicApiModule.java new file mode 100644 index 000000000..284366986 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/PublicApiModule.java @@ -0,0 +1,96 @@ +package uk.gov.pay.api.app.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import io.dropwizard.setup.Environment; +import io.lettuce.core.ClientOptions; +import io.lettuce.core.RedisClient; +import io.lettuce.core.SocketOptions; +import uk.gov.pay.api.agreement.model.CreateAgreementRequest; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.json.CreateAgreementRequestDeserializer; +import uk.gov.pay.api.json.CreateCardPaymentRequestDeserializer; +import uk.gov.pay.api.json.CreatePaymentRefundRequestDeserializer; +import uk.gov.pay.api.json.StringDeserializer; +import uk.gov.pay.api.model.CreateCardPaymentRequest; +import uk.gov.pay.api.model.CreatePaymentRefundRequest; +import uk.gov.pay.api.validation.PaymentRefundRequestValidator; +import uk.gov.pay.api.validation.URLValidator; + +import javax.ws.rs.client.Client; +import java.time.Duration; + +import static uk.gov.pay.api.validation.URLValidator.urlValidatorValueOf; + +public class PublicApiModule extends AbstractModule { + + private final PublicApiConfig configuration; + private final Environment environment; + + public PublicApiModule(final PublicApiConfig configuration, final Environment environment) { + this.configuration = configuration; + this.environment = environment; + } + + @Override + protected void configure() { + bind(PublicApiConfig.class).toInstance(configuration); + bind(Environment.class).toInstance(environment); + bind(URLValidator.class).toInstance(urlValidatorValueOf(configuration.getAllowHttpForReturnUrl())); + } + + @Provides + @Singleton + public Client provideClient() { + return RestClientFactory.buildClient(configuration.getRestClientConfig()); + } + + @Provides + @Singleton + public ObjectMapper provideObjectMapper() { + ObjectMapper objectMapper = environment.getObjectMapper(); + CreateAgreementRequestDeserializer agreementRequestDeserializer = new CreateAgreementRequestDeserializer(); + CreateCardPaymentRequestDeserializer cardPaymentRequestDeserializer = new CreateCardPaymentRequestDeserializer(); + CreatePaymentRefundRequestDeserializer paymentRefundRequestDeserializer = new CreatePaymentRefundRequestDeserializer(new PaymentRefundRequestValidator()); + StringDeserializer stringDeserializer = new StringDeserializer(); + + SimpleModule publicApiDeserializationModule = new SimpleModule("publicApiDeserializationModule"); + publicApiDeserializationModule.addDeserializer(CreateAgreementRequest.class, agreementRequestDeserializer); + publicApiDeserializationModule.addDeserializer(CreateCardPaymentRequest.class, cardPaymentRequestDeserializer); + publicApiDeserializationModule.addDeserializer(CreatePaymentRefundRequest.class, paymentRefundRequestDeserializer); + publicApiDeserializationModule.addDeserializer(String.class, stringDeserializer); + + objectMapper.configure(DeserializationFeature.ACCEPT_FLOAT_AS_INT, false); + objectMapper.registerModule(publicApiDeserializationModule); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + return objectMapper; + } + + @Provides + public RateLimiterConfig getRateLimiterConfig() { + return configuration.getRateLimiterConfig(); + } + + @Provides + @Singleton + public RedisClient getRedisClient() { + RedisClient client = RedisClient.create(configuration.getRedisConfiguration().getUrl()); + SocketOptions socketOptions = SocketOptions + .builder() + .connectTimeout(Duration.ofMillis(configuration.getRedisConfiguration().getConnectTimeout())) + .build(); + ClientOptions clientOptions = ClientOptions + .builder() + .socketOptions(socketOptions) + .build(); + client.setOptions(clientOptions); + client.setDefaultTimeout(Duration.ofMillis(configuration.getRedisConfiguration().getCommandTimeout())); + return client; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/RateLimiterConfig.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/RateLimiterConfig.java new file mode 100644 index 000000000..0a8c7d063 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/RateLimiterConfig.java @@ -0,0 +1,102 @@ +package uk.gov.pay.api.app.config; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.dropwizard.Configuration; + +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class RateLimiterConfig extends Configuration { + + @Min(1) + private int noOfReq; + + @Min(1) + private int noOfReqForPost; + + @Min(1) + private int noOfReqForElevatedAccounts; + + @Min(1) + private int noOfPostReqForElevatedAccounts; + + @Min(1) + private int noOfReqPerNode; + + @Min(1) + private int noOfReqForPostPerNode; + + @Valid + @JsonDeserialize(converter = StringToListConverter.class) + private List elevatedAccounts; + + @Min(500) + @Max(60000) + private int perMillis; + + @Valid + @JsonDeserialize(converter = StringToListConverter.class) + private List lowTrafficAccounts; + + @Min(1) + private int noOfReqForLowTrafficAccounts; + + @Min(1) + private int noOfPostReqForLowTrafficAccounts; + + @Min(1000) + @Max(3_599_999) + private int intervalInMillisForLowTrafficAccounts; + + public int getNoOfReq() { + return noOfReq; + } + + public int getPerMillis() { + return perMillis; + } + + public int getNoOfReqForPost() { + return noOfReqForPost; + } + + public int getNoOfReqPerNode() { + return noOfReqPerNode; + } + + public int getNoOfReqForPostPerNode() { + return noOfReqForPostPerNode; + } + + public int getNoOfReqForElevatedAccounts() { + return noOfReqForElevatedAccounts; + } + + public int getNoOfPostReqForElevatedAccounts() { + return noOfPostReqForElevatedAccounts; + } + + public List getElevatedAccounts() { + return Optional.ofNullable(elevatedAccounts).orElse(Collections.emptyList()); + } + + public List getLowTrafficAccounts() { + return Optional.ofNullable(lowTrafficAccounts).orElse(Collections.emptyList()); + } + + public int getNoOfReqForLowTrafficAccounts() { + return noOfReqForLowTrafficAccounts; + } + + public int getNoOfPostReqForLowTrafficAccounts() { + return noOfPostReqForLowTrafficAccounts; + } + + public int getIntervalInMillisForLowTrafficAccounts() { + return intervalInMillisForLowTrafficAccounts; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/RedisConfiguration.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/RedisConfiguration.java new file mode 100644 index 000000000..899e28c9a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/RedisConfiguration.java @@ -0,0 +1,41 @@ +package uk.gov.pay.api.app.config; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import io.dropwizard.util.Duration; + +import static java.lang.String.format; + +public class RedisConfiguration { + + @Valid + @NotNull + @JsonProperty("endpoint") + private String endpoint; + + @Valid + @JsonProperty("ssl") + private boolean ssl; + + @Valid + @JsonProperty("commandTimeout") + private Duration commandTimeout; + + @Valid + @JsonProperty("connectTimeout") + private Duration connectTimeout; + + public String getUrl() { + return format("%s://%s", ssl ? "rediss" : "redis", endpoint); + } + + public Long getCommandTimeout() { + return commandTimeout.toMilliseconds(); + } + + public Long getConnectTimeout() { + return connectTimeout.toMilliseconds(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/RestClientConfig.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/RestClientConfig.java new file mode 100644 index 000000000..146015216 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/RestClientConfig.java @@ -0,0 +1,20 @@ +package uk.gov.pay.api.app.config; + +import io.dropwizard.Configuration; + +public class RestClientConfig extends Configuration { + + private String disabledSecureConnection = "false"; + + public RestClientConfig() { + } + + public RestClientConfig(boolean disabledSecureConnection) { + this.disabledSecureConnection = Boolean.valueOf(disabledSecureConnection).toString(); + } + + public Boolean isDisabledSecureConnection() { + return "true".equals(disabledSecureConnection); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/StringToListConverter.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/StringToListConverter.java new file mode 100644 index 000000000..48f091715 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/app/config/StringToListConverter.java @@ -0,0 +1,24 @@ +package uk.gov.pay.api.app.config; + +import com.fasterxml.jackson.databind.util.StdConverter; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class StringToListConverter extends StdConverter> { + + @Override + public List convert(String value) { + if (StringUtils.isBlank(value)) { + return Collections.emptyList(); + } + + return Arrays.stream(value.split(",")) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .collect(Collectors.toList()); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/auth/Account.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/auth/Account.java new file mode 100644 index 000000000..29a988c6f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/auth/Account.java @@ -0,0 +1,35 @@ +package uk.gov.pay.api.auth; + +import uk.gov.pay.api.model.TokenPaymentType; + +import java.security.Principal; + +public class Account implements Principal { + + private final String accountId; + private final TokenPaymentType paymentType; + private final String tokenLink; + + public Account(String accountId, TokenPaymentType paymentType, String tokenLink) { + this.accountId = accountId; + this.paymentType = paymentType; + this.tokenLink = tokenLink; + } + + @Override + public String getName() { + return getAccountId(); + } + + public String getTokenLink() { + return tokenLink; + } + + public String getAccountId() { + return accountId; + } + + public TokenPaymentType getPaymentType() { + return paymentType; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/auth/AccountAuthenticator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/auth/AccountAuthenticator.java new file mode 100644 index 000000000..e3163aeb9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/auth/AccountAuthenticator.java @@ -0,0 +1,66 @@ +package uk.gov.pay.api.auth; + +import com.fasterxml.jackson.databind.JsonNode; +import io.dropwizard.auth.Authenticator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.model.publicauth.AuthResponse; +import uk.gov.service.payments.commons.model.ErrorIdentifier; + +import javax.inject.Inject; +import javax.ws.rs.ServiceUnavailableException; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Optional; + +import static java.lang.String.format; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static javax.ws.rs.core.Response.Status.OK; +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; +import static net.logstash.logback.argument.StructuredArguments.kv; + +public class AccountAuthenticator implements Authenticator { + private static Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class); + + private final Client client; + private final String publicAuthUrl; + + @Inject + public AccountAuthenticator(Client client, PublicApiConfig configuration) { + this.client = client; + this.publicAuthUrl = configuration.getPublicAuthUrl(); + } + + @Override + public Optional authenticate(String bearerToken) { + + Response response = client.target(publicAuthUrl).request() + .header(AUTHORIZATION, "Bearer " + bearerToken) + .accept(MediaType.APPLICATION_JSON) + .get(); + + if (response.getStatus() == OK.getStatusCode()) { + AuthResponse authResponse = response.readEntity(AuthResponse.class); + logger.info(format("Successfully authenticated using API key with token_link %s", authResponse.getTokenLink()), + kv("token_link", authResponse.getTokenLink())); + return Optional.of(new Account(authResponse.getAccountId(), authResponse.getTokenType(), authResponse.getTokenLink())); + } else if (response.getStatus() == UNAUTHORIZED.getStatusCode()) { + JsonNode unauthorisedResponse = response.readEntity(JsonNode.class); + ErrorIdentifier errorIdentifier = ErrorIdentifier.valueOf(unauthorisedResponse.get("error_identifier").asText()); + if (errorIdentifier == ErrorIdentifier.AUTH_TOKEN_REVOKED) { + String tokenLink = unauthorisedResponse.get("token_link").asText(); + logger.warn(format("Attempt to authenticate using revoked API key with token_link %s", tokenLink), kv("token_link", tokenLink)); + } else { + logger.warn("Attempt to authenticate using invalid API key with valid checksum"); + } + response.close(); + return Optional.empty(); + } else { + response.close(); + logger.warn("Unexpected status code " + response.getStatus() + " from auth."); + throw new ServiceUnavailableException(); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/common/ResponseConstants.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/common/ResponseConstants.java new file mode 100644 index 000000000..50b78298e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/common/ResponseConstants.java @@ -0,0 +1,15 @@ +package uk.gov.pay.api.common; + +public final class ResponseConstants { + + public static final String RESPONSE_200_DESCRIPTION = "OK - your request was successful."; + public static final String RESPONSE_201_DESCRIPTION = "Created"; + public static final String RESPONSE_400_DESCRIPTION = "Bad request"; + public static final String RESPONSE_401_DESCRIPTION = "Your API key is missing or invalid. " + + "Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)"; + public static final String RESPONSE_404_DESCRIPTION = "Not found"; + public static final String RESPONSE_409_DESCRIPTION = "Conflict"; + public static final String RESPONSE_422_DESCRIPTION = "Your request failed. Check the `code` and `description` in the response to find out why your request failed."; + public static final String RESPONSE_429_DESCRIPTION = "Too many requests"; + public static final String RESPONSE_500_DESCRIPTION = "Downstream system error"; +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/common/SearchConstants.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/common/SearchConstants.java new file mode 100644 index 000000000..3b5eac3d5 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/common/SearchConstants.java @@ -0,0 +1,21 @@ +package uk.gov.pay.api.common; + +public class SearchConstants { + + public static final String GATEWAY_ACCOUNT_ID = "account_id"; + public static final String REFERENCE_KEY = "reference"; + public static final String EMAIL_KEY = "email"; + public static final String STATE_KEY = "state"; + public static final String STATUS_KEY = "status"; + public static final String CARD_BRAND_KEY = "card_brand"; + public static final String FIRST_DIGITS_CARD_NUMBER_KEY = "first_digits_card_number"; + public static final String LAST_DIGITS_CARD_NUMBER_KEY = "last_digits_card_number"; + public static final String CARDHOLDER_NAME_KEY = "cardholder_name"; + public static final String FROM_DATE_KEY = "from_date"; + public static final String TO_DATE_KEY = "to_date"; + public static final String PAGE = "page"; + public static final String DISPLAY_SIZE = "display_size"; + public static final String FROM_SETTLED_DATE = "from_settled_date"; + public static final String TO_SETTLED_DATE = "to_settled_date"; + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/AgreementValidationException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/AgreementValidationException.java new file mode 100644 index 000000000..36f45379b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/AgreementValidationException.java @@ -0,0 +1,24 @@ +package uk.gov.pay.api.exception; + +import uk.gov.pay.api.model.RequestError; + +public class AgreementValidationException extends RuntimeException { + + private RequestError requestError; + + public AgreementValidationException(RequestError requestError) { + this.requestError = requestError; + } + + public RequestError getRequestError() { + return requestError; + } + + @Override + public String toString() { + return "AgreementValidationException{" + + "requestError=" + requestError + + '}'; + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/AuthorisationRequestException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/AuthorisationRequestException.java new file mode 100644 index 000000000..0db1bb567 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/AuthorisationRequestException.java @@ -0,0 +1,12 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class AuthorisationRequestException extends ConnectorResponseErrorException{ + private Response response; + + public AuthorisationRequestException(Response response) { + super(response); + this.response = response; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/BadRefundsRequestException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/BadRefundsRequestException.java new file mode 100644 index 000000000..eb512204a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/BadRefundsRequestException.java @@ -0,0 +1,23 @@ +package uk.gov.pay.api.exception; + +import uk.gov.pay.api.model.RequestError; + +public class BadRefundsRequestException extends RuntimeException { + + private RequestError refundError; + + public BadRefundsRequestException(RequestError refundError) { + this.refundError = refundError; + } + + public RequestError getRequestError() { + return refundError; + } + + @Override + public String toString() { + return "BadRefundsRequestException{" + + "refundError=" + refundError + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/BadRequestException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/BadRequestException.java new file mode 100644 index 000000000..86b093cb8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/BadRequestException.java @@ -0,0 +1,23 @@ +package uk.gov.pay.api.exception; + +import uk.gov.pay.api.model.RequestError; + +public class BadRequestException extends RuntimeException { + + private RequestError requestError; + + public BadRequestException(RequestError requestError) { + this.requestError = requestError; + } + + public RequestError getRequestError() { + return requestError; + } + + @Override + public String toString() { + return "BadRequestException{" + + "requestError=" + requestError + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CancelAgreementException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CancelAgreementException.java new file mode 100644 index 000000000..2d0d3ed0e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CancelAgreementException.java @@ -0,0 +1,11 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class CancelAgreementException extends ConnectorResponseErrorException { + + public CancelAgreementException(Response response) { + super(response); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CancelChargeException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CancelChargeException.java new file mode 100644 index 000000000..0169affbf --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CancelChargeException.java @@ -0,0 +1,10 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class CancelChargeException extends ConnectorResponseErrorException { + + public CancelChargeException(Response response) { + super(response); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CaptureChargeException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CaptureChargeException.java new file mode 100644 index 000000000..5e2762b08 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CaptureChargeException.java @@ -0,0 +1,11 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class CaptureChargeException extends ConnectorResponseErrorException { + + public CaptureChargeException(Response response) { + super(response); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/ConnectorResponseErrorException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/ConnectorResponseErrorException.java new file mode 100644 index 000000000..cd06afec9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/ConnectorResponseErrorException.java @@ -0,0 +1,132 @@ +package uk.gov.pay.api.exception; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.service.payments.commons.model.ErrorIdentifier; + +import javax.ws.rs.core.Response; +import java.util.List; + +public class ConnectorResponseErrorException extends RuntimeException { + + private static final Logger LOGGER = LoggerFactory.getLogger(ConnectorResponseErrorException.class); + + private ConnectorErrorResponse error; + private int status; + + public ConnectorResponseErrorException(Response response) { + super(response.toString()); + this.status = response.getStatus(); + this.error = readError(response); + response.close(); + } + + ConnectorResponseErrorException(ConnectorResponseErrorException exception) { + super(exception); + this.status = exception.status; + this.error = exception.error; + } + + ConnectorResponseErrorException(Throwable cause) { + super(cause); + } + + public int getErrorStatus() { + return status; + } + + public ErrorIdentifier getErrorIdentifier() { + return error == null ? ErrorIdentifier.GENERIC : error.errorIdentifier; + } + + public String getReason() { + if (error != null) { + return error.getReason(); + } + return null; + } + + public boolean hasReason() { + return this.error != null && this.error.getReason() != null; + } + + private ConnectorErrorResponse readError(Response response) { + ConnectorErrorResponse connectorError = null; + try { + connectorError = response.readEntity(ConnectorErrorResponse.class); + } catch (Exception exception) { + LOGGER.debug("Could not read error response from connector", exception); + } + return connectorError; + } + + private String getResponseBody() { + if (error != null) { + return error.toString(); + } + return null; + } + + @Override + public String getMessage() { + String body = getResponseBody(); + if (body != null) { + return super.getMessage() + " and body " + body; + } + return super.getMessage(); + } + + public String getConnectorErrorMessage() { + return error.message.stream().findFirst() + .orElseThrow(() -> new InternalServerException("Error deserializing connector error message")); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ConnectorErrorResponse { + + @JsonProperty("error_identifier") + private ErrorIdentifier errorIdentifier; + + private String reason; + + private List message; + + // Needed for Jackson deserialization from Responses + public ConnectorErrorResponse() { + + } + + public ConnectorErrorResponse(ErrorIdentifier errorIdentifier, List message) { + this(errorIdentifier, null, message); + } + + public ConnectorErrorResponse(ErrorIdentifier errorIdentifier, String reason, List message) { + this.errorIdentifier = errorIdentifier; + this.reason = reason; + this.message = message; + } + + public ErrorIdentifier getErrorIdentifier() { + return errorIdentifier; + } + + public String getReason() { + return reason; + } + + public List getMessage() { + return message; + } + + @Override + public String toString() { + return "ConnectorErrorResponse{" + + "error_identifier='" + errorIdentifier + '\'' + + ", reason='" + reason + '\'' + + ", message='" + message + '\'' + + '}'; + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CreateAgreementException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CreateAgreementException.java new file mode 100644 index 000000000..b158604b3 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CreateAgreementException.java @@ -0,0 +1,8 @@ +package uk.gov.pay.api.exception; +import javax.ws.rs.core.Response; + +public class CreateAgreementException extends ConnectorResponseErrorException { + public CreateAgreementException(Response response) { + super(response); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CreateChargeException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CreateChargeException.java new file mode 100644 index 000000000..615cf0e34 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CreateChargeException.java @@ -0,0 +1,10 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class CreateChargeException extends ConnectorResponseErrorException { + + public CreateChargeException(Response response) { + super(response); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CreateRefundException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CreateRefundException.java new file mode 100644 index 000000000..5e59aaf03 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/CreateRefundException.java @@ -0,0 +1,10 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class CreateRefundException extends ConnectorResponseErrorException { + + public CreateRefundException(Response response) { + super(response); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/DisputesValidationException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/DisputesValidationException.java new file mode 100644 index 000000000..5c793ec5e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/DisputesValidationException.java @@ -0,0 +1,23 @@ +package uk.gov.pay.api.exception; + +import uk.gov.pay.api.model.RequestError; + +public class DisputesValidationException extends RuntimeException { + + private RequestError requestError; + + public DisputesValidationException(RequestError requestError) { + this.requestError = requestError; + } + + public RequestError getRequestError() { + return requestError; + } + + @Override + public String toString() { + return "DisputesValidationException{" + + "disputeError=" + requestError + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetAgreementException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetAgreementException.java new file mode 100644 index 000000000..4554dca60 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetAgreementException.java @@ -0,0 +1,10 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class GetAgreementException extends ConnectorResponseErrorException { + + public GetAgreementException(Response response) { + super(response); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetChargeException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetChargeException.java new file mode 100644 index 000000000..e6cfa7c2c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetChargeException.java @@ -0,0 +1,10 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class GetChargeException extends ConnectorResponseErrorException { + + public GetChargeException(Response response) { + super(response); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetEventsException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetEventsException.java new file mode 100644 index 000000000..e02f347bc --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetEventsException.java @@ -0,0 +1,10 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class GetEventsException extends ConnectorResponseErrorException { + + public GetEventsException(Response response) { + super(response); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetRefundException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetRefundException.java new file mode 100644 index 000000000..7ad336ca2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetRefundException.java @@ -0,0 +1,13 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class GetRefundException extends ConnectorResponseErrorException { + public GetRefundException(Response response) { + super(response); + } + + public GetRefundException(GetTransactionException exception) { + super(exception); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetRefundsException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetRefundsException.java new file mode 100644 index 000000000..4390f1058 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetRefundsException.java @@ -0,0 +1,9 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class GetRefundsException extends ConnectorResponseErrorException { + public GetRefundsException(Response response) { + super(response); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetTransactionException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetTransactionException.java new file mode 100644 index 000000000..6bffc2518 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/GetTransactionException.java @@ -0,0 +1,10 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class GetTransactionException extends ConnectorResponseErrorException { + + public GetTransactionException(Response response) { + super(response); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/InternalServerException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/InternalServerException.java new file mode 100644 index 000000000..00d01205c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/InternalServerException.java @@ -0,0 +1,8 @@ +package uk.gov.pay.api.exception; + +public class InternalServerException extends RuntimeException{ + + public InternalServerException(String message) { + super(message); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/PaymentValidationException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/PaymentValidationException.java new file mode 100644 index 000000000..716b8c63b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/PaymentValidationException.java @@ -0,0 +1,23 @@ +package uk.gov.pay.api.exception; + +import uk.gov.pay.api.model.RequestError; + +public class PaymentValidationException extends RuntimeException { + + private RequestError requestError; + + public PaymentValidationException(RequestError requestError) { + this.requestError = requestError; + } + + public RequestError getRequestError() { + return requestError; + } + + @Override + public String toString() { + return "PaymentValidationException{" + + "requestError=" + requestError + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/RefundsValidationException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/RefundsValidationException.java new file mode 100644 index 000000000..a420d7f2d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/RefundsValidationException.java @@ -0,0 +1,23 @@ +package uk.gov.pay.api.exception; + +import uk.gov.pay.api.model.RequestError; + +public class RefundsValidationException extends RuntimeException { + + private RequestError requestError; + + public RefundsValidationException(RequestError requestError) { + this.requestError = requestError; + } + + public RequestError getRequestError() { + return requestError; + } + + @Override + public String toString() { + return "RefundsValidationException{" + + "refundError=" + requestError + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchAgreementsException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchAgreementsException.java new file mode 100644 index 000000000..fa9f92c04 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchAgreementsException.java @@ -0,0 +1,14 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class SearchAgreementsException extends ConnectorResponseErrorException { + + public SearchAgreementsException(Response response) { + super(response); + } + + public SearchAgreementsException(Throwable cause) { + super(cause); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchDisputesException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchDisputesException.java new file mode 100644 index 000000000..f04eafa47 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchDisputesException.java @@ -0,0 +1,14 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class SearchDisputesException extends ConnectorResponseErrorException { + + public SearchDisputesException(Response response) { + super(response); + } + + public SearchDisputesException(Throwable cause) { + super(cause); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchPaymentsException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchPaymentsException.java new file mode 100644 index 000000000..0850f4071 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchPaymentsException.java @@ -0,0 +1,14 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class SearchPaymentsException extends ConnectorResponseErrorException { + + public SearchPaymentsException(Response response) { + super(response); + } + + public SearchPaymentsException(Throwable cause) { + super(cause); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchRefundsException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchRefundsException.java new file mode 100644 index 000000000..ad61ca000 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchRefundsException.java @@ -0,0 +1,14 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class SearchRefundsException extends ConnectorResponseErrorException { + + public SearchRefundsException(Response response) { + super(response); + } + + public SearchRefundsException(Throwable cause) { + super(cause); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchTransactionsException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchTransactionsException.java new file mode 100644 index 000000000..69ed55308 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/SearchTransactionsException.java @@ -0,0 +1,14 @@ +package uk.gov.pay.api.exception; + +import javax.ws.rs.core.Response; + +public class SearchTransactionsException extends ConnectorResponseErrorException { + + public SearchTransactionsException(Response response) { + super(response); + } + + public SearchTransactionsException(Throwable cause) { + super(cause); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/AgreementValidationExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/AgreementValidationExceptionMapper.java new file mode 100644 index 000000000..469aac81d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/AgreementValidationExceptionMapper.java @@ -0,0 +1,25 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.AgreementValidationException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +public class AgreementValidationExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(AgreementValidationException.class); + + @Override + public Response toResponse(AgreementValidationException exception) { + RequestError requestError = exception.getRequestError(); + LOGGER.debug("Agreement Validation exception {}", requestError); + + return Response.status(422) + .entity(requestError) + .build(); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/AuthorisationRequestExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/AuthorisationRequestExceptionMapper.java new file mode 100644 index 000000000..ce433e317 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/AuthorisationRequestExceptionMapper.java @@ -0,0 +1,68 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.AuthorisationRequestException; +import uk.gov.pay.api.model.RequestError; +import uk.gov.service.payments.commons.model.ErrorIdentifier; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.PAYMENT_REQUIRED; +import static org.apache.http.HttpStatus.SC_UNPROCESSABLE_ENTITY; +import static uk.gov.pay.api.model.RequestError.Code.AUTHORISATION_CARD_NUMBER_REJECTED_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.AUTHORISATION_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.AUTHORISATION_ONE_TIME_TOKEN_ALREADY_USED_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.AUTHORISATION_ONE_TIME_TOKEN_INVALID_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.AUTHORISATION_REJECTED_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.GENERIC_VALIDATION_EXCEPTION_MESSAGE_FROM_CONNECTOR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class AuthorisationRequestExceptionMapper implements ExceptionMapper { + private static final Logger LOGGER = LoggerFactory.getLogger(AuthorisationRequestExceptionMapper.class); + @Override + public Response toResponse(AuthorisationRequestException exception) { + int errorStatus; + ErrorIdentifier errorIdentifier = exception.getErrorIdentifier(); + RequestError requestError; + switch (errorIdentifier) { + case CARD_NUMBER_REJECTED: + errorStatus = PAYMENT_REQUIRED.getStatusCode(); + requestError = aRequestError(AUTHORISATION_CARD_NUMBER_REJECTED_ERROR, exception.getConnectorErrorMessage()); + break; + case AUTHORISATION_REJECTED: + errorStatus = PAYMENT_REQUIRED.getStatusCode(); + requestError = aRequestError(AUTHORISATION_REJECTED_ERROR, exception.getConnectorErrorMessage()); + break; + case AUTHORISATION_ERROR: + case AUTHORISATION_TIMEOUT: + errorStatus = INTERNAL_SERVER_ERROR.getStatusCode(); + requestError = aRequestError(AUTHORISATION_ERROR, "There was an error authorising the payment"); + break; + case ONE_TIME_TOKEN_ALREADY_USED: + errorStatus = BAD_REQUEST.getStatusCode(); + requestError = aRequestError(AUTHORISATION_ONE_TIME_TOKEN_ALREADY_USED_ERROR, exception.getConnectorErrorMessage()); + break; + case ONE_TIME_TOKEN_INVALID: + errorStatus = BAD_REQUEST.getStatusCode(); + requestError = aRequestError(AUTHORISATION_ONE_TIME_TOKEN_INVALID_ERROR, exception.getConnectorErrorMessage()); + break; + case INVALID_ATTRIBUTE_VALUE: + errorStatus = SC_UNPROCESSABLE_ENTITY; + requestError = aRequestError(GENERIC_VALIDATION_EXCEPTION_MESSAGE_FROM_CONNECTOR, exception.getConnectorErrorMessage()); + break; + default: + LOGGER.error("Connector invalid response was {}.\n Returning http status {}", exception.getConnectorErrorMessage(), INTERNAL_SERVER_ERROR); + errorStatus = INTERNAL_SERVER_ERROR.getStatusCode(); + requestError = aRequestError(CREATE_PAYMENT_CONNECTOR_ERROR, exception.getConnectorErrorMessage()); + } + return Response + .status(errorStatus) + .entity(requestError) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/BadRefundsRequestExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/BadRefundsRequestExceptionMapper.java new file mode 100644 index 000000000..15d7980e8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/BadRefundsRequestExceptionMapper.java @@ -0,0 +1,26 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.BadRefundsRequestException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; + +public class BadRefundsRequestExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(BadRefundsRequestExceptionMapper.class); + + @Override + public Response toResponse(BadRefundsRequestException exception) { + + RequestError requestError = exception.getRequestError(); + LOGGER.debug("Bad Refunds Request exception {}", requestError); + + return Response.status(Status.BAD_REQUEST) + .entity(exception.getRequestError()) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/BadRequestExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/BadRequestExceptionMapper.java new file mode 100644 index 000000000..c24ecf774 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/BadRequestExceptionMapper.java @@ -0,0 +1,26 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.BadRequestException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; + +public class BadRequestExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(BadRequestExceptionMapper.class); + + @Override + public Response toResponse(BadRequestException exception) { + + RequestError requestError = exception.getRequestError(); + LOGGER.debug("Bad Request exception {}", requestError); + + return Response.status(Status.BAD_REQUEST) + .entity(exception.getRequestError()) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CancelAgreementExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CancelAgreementExceptionMapper.java new file mode 100644 index 000000000..c2efac869 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CancelAgreementExceptionMapper.java @@ -0,0 +1,49 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.CancelAgreementException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.Code.CANCEL_AGREEMENT_CONNECTOR_BAD_REQUEST_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CANCEL_AGREEMENT_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CANCEL_AGREEMENT_NOT_FOUND_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class CancelAgreementExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(CancelAgreementExceptionMapper.class); + + @Override + public Response toResponse(CancelAgreementException exception) { + int errorStatus = exception.getErrorStatus(); + RequestError requestError; + Response.Status status; + + if (errorStatus == NOT_FOUND.getStatusCode()) { + requestError = aRequestError(CANCEL_AGREEMENT_NOT_FOUND_ERROR); + status = NOT_FOUND; + } else if (errorStatus == BAD_REQUEST.getStatusCode()) { + requestError = aRequestError(CANCEL_AGREEMENT_CONNECTOR_BAD_REQUEST_ERROR); + status = BAD_REQUEST; + } else { + requestError = aRequestError(CANCEL_AGREEMENT_CONNECTOR_ERROR); + status = INTERNAL_SERVER_ERROR; + } + + if (status == INTERNAL_SERVER_ERROR) { + LOGGER.error("Connector invalid response was {}.\n Returning http status {} with error body {}", exception.getMessage(), status, requestError); + } + return Response + .status(status) + .entity(requestError) + .build(); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CancelChargeExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CancelChargeExceptionMapper.java new file mode 100644 index 000000000..23cc1f7a2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CancelChargeExceptionMapper.java @@ -0,0 +1,54 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.CancelChargeException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static javax.ws.rs.core.Response.Status.CONFLICT; +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.Code.CANCEL_PAYMENT_CONNECTOR_BAD_REQUEST_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CANCEL_PAYMENT_CONNECTOR_CONFLICT_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CANCEL_PAYMENT_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CANCEL_PAYMENT_NOT_FOUND_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class CancelChargeExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(CancelChargeExceptionMapper.class); + + @Override + public Response toResponse(CancelChargeException exception) { + + int errorStatus = exception.getErrorStatus(); + RequestError requestError; + Response.Status status; + + if (errorStatus == NOT_FOUND.getStatusCode()) { + requestError = aRequestError(CANCEL_PAYMENT_NOT_FOUND_ERROR); + status = NOT_FOUND; + } else if (errorStatus == BAD_REQUEST.getStatusCode()) { + requestError = aRequestError(CANCEL_PAYMENT_CONNECTOR_BAD_REQUEST_ERROR); + status = BAD_REQUEST; + } else if (errorStatus == CONFLICT.getStatusCode()) { + requestError = aRequestError(CANCEL_PAYMENT_CONNECTOR_CONFLICT_ERROR); + status = CONFLICT; + } else { + requestError = aRequestError(CANCEL_PAYMENT_CONNECTOR_ERROR); + status = INTERNAL_SERVER_ERROR; + } + + if (status == INTERNAL_SERVER_ERROR) { + LOGGER.error("Connector invalid response was {}.\n Returning http status {} with error body {}", exception.getMessage(), status, requestError); + } + return Response + .status(status) + .entity(requestError) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CaptureChargeExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CaptureChargeExceptionMapper.java new file mode 100644 index 000000000..8393158a7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CaptureChargeExceptionMapper.java @@ -0,0 +1,54 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.CaptureChargeException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static javax.ws.rs.core.Response.Status.CONFLICT; +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.Code.CAPTURE_PAYMENT_CONNECTOR_BAD_REQUEST_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CAPTURE_PAYMENT_CONNECTOR_CONFLICT_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CAPTURE_PAYMENT_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CAPTURE_PAYMENT_NOT_FOUND_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class CaptureChargeExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(CaptureChargeExceptionMapper.class); + + @Override + public Response toResponse(CaptureChargeException exception) { + int errorStatus = exception.getErrorStatus(); + RequestError requestError; + Response.Status status; + + if (errorStatus == NOT_FOUND.getStatusCode()) { + requestError = aRequestError(CAPTURE_PAYMENT_NOT_FOUND_ERROR); + status = NOT_FOUND; + } else if (errorStatus == BAD_REQUEST.getStatusCode()) { + requestError = aRequestError(CAPTURE_PAYMENT_CONNECTOR_BAD_REQUEST_ERROR); + status = BAD_REQUEST; + } else if (errorStatus == CONFLICT.getStatusCode()) { + requestError = aRequestError(CAPTURE_PAYMENT_CONNECTOR_CONFLICT_ERROR); + status = CONFLICT; + } else { + requestError = aRequestError(CAPTURE_PAYMENT_CONNECTOR_ERROR); + status = INTERNAL_SERVER_ERROR; + } + + if (status == INTERNAL_SERVER_ERROR) { + LOGGER.error("Connector invalid response was {}.\n Returning http status {} with error body {}", exception.getMessage(), status, requestError); + } + return Response + .status(status) + .entity(requestError) + .build(); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CreateAgreementExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CreateAgreementExceptionMapper.java new file mode 100644 index 000000000..e2a7568af --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CreateAgreementExceptionMapper.java @@ -0,0 +1,40 @@ +package uk.gov.pay.api.exception.mapper; + +import org.eclipse.jetty.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.CreateAgreementException; +import uk.gov.pay.api.model.RequestError; +import uk.gov.service.payments.commons.model.ErrorIdentifier; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_AGREEMENT_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class CreateAgreementExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(CreateAgreementExceptionMapper.class); + + @Override + public Response toResponse(CreateAgreementException exception) { + RequestError requestError; + int statusCode = HttpStatus.INTERNAL_SERVER_ERROR_500; + + if (exception.getErrorIdentifier() == ErrorIdentifier.RECURRING_CARD_PAYMENTS_NOT_ALLOWED) { + statusCode = HttpStatus.UNPROCESSABLE_ENTITY_422; + requestError = aRequestError(RequestError.Code.RECURRING_CARD_PAYMENTS_NOT_ALLOWED_ERROR); + } + else { + requestError = aRequestError(CREATE_AGREEMENT_CONNECTOR_ERROR); + LOGGER.info("Connector invalid response was {}.\n Returning http status {} with error body {}", + exception.getMessage(), INTERNAL_SERVER_ERROR, requestError); + } + + return Response.status(statusCode).entity(requestError).build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CreateChargeExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CreateChargeExceptionMapper.java new file mode 100644 index 000000000..f20bf47e3 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CreateChargeExceptionMapper.java @@ -0,0 +1,125 @@ +package uk.gov.pay.api.exception.mapper; + +import org.eclipse.jetty.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.CreateChargeException; +import uk.gov.pay.api.model.RequestError; +import uk.gov.service.payments.commons.model.ErrorIdentifier; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.SET_UP_AGREEMENT_FIELD_NAME; +import static uk.gov.pay.api.model.RequestError.Code.ACCOUNT_DISABLED; +import static uk.gov.pay.api.model.RequestError.Code.ACCOUNT_NOT_LINKED_WITH_PSP; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_ACCOUNT_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_AGREEMENT_ID_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_AUTHORISATION_API_NOT_ENABLED; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_CARD_NUMBER_IN_PAYMENT_LINK_REFERENCE_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_IDEMPOTENCY_KEY_ALREADY_USED; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_MOTO_NOT_ENABLED; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_UNEXPECTED_FIELD_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_VALIDATION_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.GENERIC_MISSING_FIELD_ERROR_MESSAGE_FROM_CONNECTOR; +import static uk.gov.pay.api.model.RequestError.Code.GENERIC_UNEXPECTED_FIELD_ERROR_MESSAGE_FROM_CONNECTOR; +import static uk.gov.pay.api.model.RequestError.Code.GENERIC_VALIDATION_EXCEPTION_MESSAGE_FROM_CONNECTOR; +import static uk.gov.pay.api.model.RequestError.Code.RECURRING_CARD_PAYMENTS_NOT_ALLOWED_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.RESOURCE_ACCESS_FORBIDDEN; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class CreateChargeExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(CreateChargeExceptionMapper.class); + + @Override + public Response toResponse(CreateChargeException exception) { + + RequestError requestError; + int statusCode = HttpStatus.INTERNAL_SERVER_ERROR_500; + + if (exception.getErrorStatus() == NOT_FOUND.getStatusCode()) { + if (exception.getErrorIdentifier() == ErrorIdentifier.AGREEMENT_NOT_FOUND) { + statusCode = HttpStatus.BAD_REQUEST_400; + requestError = aRequestError("set_up_agreement", CREATE_PAYMENT_AGREEMENT_ID_ERROR); + } else { + requestError = aRequestError(CREATE_PAYMENT_ACCOUNT_ERROR); + } + } else { + ErrorIdentifier errorIdentifier = exception.getErrorIdentifier(); + switch (errorIdentifier) { + case ZERO_AMOUNT_NOT_ALLOWED: + statusCode = HttpStatus.UNPROCESSABLE_ENTITY_422; + requestError = aRequestError("amount", CREATE_PAYMENT_VALIDATION_ERROR, + "Must be greater than or equal to 1"); + break; + case MOTO_NOT_ALLOWED: + statusCode = HttpStatus.UNPROCESSABLE_ENTITY_422; + requestError = aRequestError(CREATE_PAYMENT_MOTO_NOT_ENABLED); + break; + case ACCOUNT_DISABLED: + statusCode = HttpStatus.FORBIDDEN_403; + requestError = aRequestError(ACCOUNT_DISABLED); + break; + case TELEPHONE_PAYMENT_NOTIFICATIONS_NOT_ALLOWED: + statusCode = HttpStatus.FORBIDDEN_403; + requestError = aRequestError(RESOURCE_ACCESS_FORBIDDEN); + break; + case ACCOUNT_NOT_LINKED_WITH_PSP: + statusCode = HttpStatus.FORBIDDEN_403; + requestError = aRequestError(ACCOUNT_NOT_LINKED_WITH_PSP); + break; + case AUTHORISATION_API_NOT_ALLOWED: + statusCode = HttpStatus.UNPROCESSABLE_ENTITY_422; + requestError = aRequestError(CREATE_PAYMENT_AUTHORISATION_API_NOT_ENABLED); + break; + case AGREEMENT_NOT_FOUND: + statusCode = HttpStatus.BAD_REQUEST_400; + requestError = aRequestError("agreement_id", CREATE_PAYMENT_VALIDATION_ERROR, "Agreement does not exist"); + break; + case AGREEMENT_NOT_ACTIVE: + statusCode = HttpStatus.BAD_REQUEST_400; + requestError = aRequestError("agreement_id", CREATE_PAYMENT_VALIDATION_ERROR, "Agreement must be active"); + break; + case MISSING_MANDATORY_ATTRIBUTE: + statusCode = HttpStatus.BAD_REQUEST_400; + requestError = aRequestError(GENERIC_MISSING_FIELD_ERROR_MESSAGE_FROM_CONNECTOR, exception.getConnectorErrorMessage()); + break; + case UNEXPECTED_ATTRIBUTE: + statusCode = HttpStatus.BAD_REQUEST_400; + requestError = aRequestError(GENERIC_UNEXPECTED_FIELD_ERROR_MESSAGE_FROM_CONNECTOR, exception.getConnectorErrorMessage()); + break; + case INCORRECT_AUTHORISATION_MODE_FOR_SAVE_PAYMENT_INSTRUMENT_TO_AGREEMENT: + statusCode = HttpStatus.BAD_REQUEST_400; + requestError = aRequestError(CREATE_PAYMENT_UNEXPECTED_FIELD_ERROR, SET_UP_AGREEMENT_FIELD_NAME); + break; + case INVALID_ATTRIBUTE_VALUE: + statusCode = HttpStatus.UNPROCESSABLE_ENTITY_422; + requestError = aRequestError(GENERIC_VALIDATION_EXCEPTION_MESSAGE_FROM_CONNECTOR, exception.getConnectorErrorMessage()); + break; + case RECURRING_CARD_PAYMENTS_NOT_ALLOWED: + statusCode = HttpStatus.UNPROCESSABLE_ENTITY_422; + requestError = aRequestError(RECURRING_CARD_PAYMENTS_NOT_ALLOWED_ERROR); + break; + case IDEMPOTENCY_KEY_USED: + statusCode = HttpStatus.CONFLICT_409; + requestError = aRequestError(CREATE_PAYMENT_IDEMPOTENCY_KEY_ALREADY_USED); + break; + case CARD_NUMBER_IN_PAYMENT_LINK_REFERENCE_REJECTED: + statusCode = HttpStatus.BAD_REQUEST_400; + requestError = aRequestError(CREATE_PAYMENT_CARD_NUMBER_IN_PAYMENT_LINK_REFERENCE_ERROR, exception.getConnectorErrorMessage()); + break; + default: + requestError = aRequestError(CREATE_PAYMENT_CONNECTOR_ERROR); + LOGGER.info("Connector invalid response was {}.\n Returning http status {} with error body {}", + exception.getMessage(), INTERNAL_SERVER_ERROR, requestError); + + } + } + + return Response.status(statusCode).entity(requestError).build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CreateRefundExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CreateRefundExceptionMapper.java new file mode 100644 index 000000000..2d6a65042 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/CreateRefundExceptionMapper.java @@ -0,0 +1,85 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.CreateRefundException; +import uk.gov.pay.api.model.RequestError; +import uk.gov.service.payments.commons.model.ErrorIdentifier; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static javax.ws.rs.core.Response.Status.FORBIDDEN; +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED; +import static uk.gov.pay.api.model.RequestError.Code.ACCOUNT_DISABLED; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_REFUND_AMOUNT_AVAILABLE_MISMATCH; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_REFUND_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_REFUND_NOT_AVAILABLE; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_REFUND_NOT_AVAILABLE_DUE_TO_DISPUTE; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_REFUND_NOT_FOUND_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class CreateRefundExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(CreateRefundExceptionMapper.class); + + @Override + public Response toResponse(CreateRefundException exception) { + + RequestError requestError; + Response.Status status; + + if (exception.getErrorStatus() == NOT_FOUND.getStatusCode()) { + requestError = aRequestError(CREATE_PAYMENT_REFUND_NOT_FOUND_ERROR); + status = NOT_FOUND; + + } + else { + switch (exception.getErrorIdentifier()) { + case ACCOUNT_DISABLED: { + requestError = aRequestError(ACCOUNT_DISABLED); + status = FORBIDDEN; + break; + } + case REFUND_NOT_AVAILABLE: { + if (exception.hasReason()) { + requestError = aRequestError(CREATE_PAYMENT_REFUND_NOT_AVAILABLE, exception.getReason()); + status = BAD_REQUEST; + } + else { + LOGGER.error("Connector response for REFUND_NOT_AVAILABLE is missing the 'reason' field"); + requestError = aRequestError(CREATE_PAYMENT_REFUND_CONNECTOR_ERROR); + status = INTERNAL_SERVER_ERROR; + } + break; + } + case REFUND_NOT_AVAILABLE_DUE_TO_DISPUTE: { + requestError = aRequestError(CREATE_PAYMENT_REFUND_NOT_AVAILABLE_DUE_TO_DISPUTE); + status = BAD_REQUEST; + break; + } + case REFUND_AMOUNT_AVAILABLE_MISMATCH: { + requestError = aRequestError(CREATE_PAYMENT_REFUND_AMOUNT_AVAILABLE_MISMATCH); + status = PRECONDITION_FAILED; + break; + } + default: { + requestError = aRequestError(CREATE_PAYMENT_REFUND_CONNECTOR_ERROR); + status = INTERNAL_SERVER_ERROR; + } + } + } + + if (status == INTERNAL_SERVER_ERROR) { + LOGGER.info("Connector invalid response was {}.\n Returning http status {} with error body {}", exception.getMessage(), status, requestError); + } + + return Response + .status(status) + .entity(requestError) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/DisputeValidationExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/DisputeValidationExceptionMapper.java new file mode 100644 index 000000000..36896b91a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/DisputeValidationExceptionMapper.java @@ -0,0 +1,25 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.DisputesValidationException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +public class DisputeValidationExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(DisputeValidationExceptionMapper.class); + + @Override + public Response toResponse(DisputesValidationException exception) { + + RequestError requestError = exception.getRequestError(); + LOGGER.debug("Dispute Validation exception {}", requestError); + + return Response.status(422) + .entity(requestError) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetAgreementExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetAgreementExceptionMapper.java new file mode 100644 index 000000000..5daeff018 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetAgreementExceptionMapper.java @@ -0,0 +1,41 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.GetAgreementException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.Code.GET_AGREEMENT_LEDGER_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.GET_AGREEMENT_NOT_FOUND_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class GetAgreementExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(GetAgreementExceptionMapper.class); + + @Override + public Response toResponse(GetAgreementException exception) { + + RequestError requestError; + Response.Status status; + + if (exception.getErrorStatus() == NOT_FOUND.getStatusCode()) { + requestError = aRequestError(GET_AGREEMENT_NOT_FOUND_ERROR); + status = NOT_FOUND; + } else { + requestError = aRequestError(GET_AGREEMENT_LEDGER_ERROR); + status = INTERNAL_SERVER_ERROR; + LOGGER.error("Ledger invalid response was {}.\n Returning http status {} with error body {}", exception.getMessage(), status, requestError); + } + + return Response + .status(status) + .entity(requestError) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetChargeExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetChargeExceptionMapper.java new file mode 100644 index 000000000..63c661a78 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetChargeExceptionMapper.java @@ -0,0 +1,41 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.GetChargeException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.Code.GET_PAYMENT_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.GET_PAYMENT_NOT_FOUND_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class GetChargeExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(GetChargeExceptionMapper.class); + + @Override + public Response toResponse(GetChargeException exception) { + + RequestError requestError; + Response.Status status; + + if (exception.getErrorStatus() == NOT_FOUND.getStatusCode()) { + requestError = aRequestError(GET_PAYMENT_NOT_FOUND_ERROR); + status = NOT_FOUND; + } else { + requestError = aRequestError(GET_PAYMENT_CONNECTOR_ERROR); + status = INTERNAL_SERVER_ERROR; + LOGGER.error("Connector invalid response was {}.\n Returning http status {} with error body {}", exception.getMessage(), status, requestError); + } + + return Response + .status(status) + .entity(requestError) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetEventsExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetEventsExceptionMapper.java new file mode 100644 index 000000000..2f5ce40f7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetEventsExceptionMapper.java @@ -0,0 +1,41 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.GetEventsException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.Code.GET_PAYMENT_EVENTS_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.GET_PAYMENT_EVENTS_NOT_FOUND_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class GetEventsExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(GetEventsExceptionMapper.class); + + @Override + public Response toResponse(GetEventsException exception) { + + RequestError requestError; + Response.Status status; + + if (exception.getErrorStatus() == NOT_FOUND.getStatusCode()) { + requestError = aRequestError(GET_PAYMENT_EVENTS_NOT_FOUND_ERROR); + status = NOT_FOUND; + } else { + requestError = aRequestError(GET_PAYMENT_EVENTS_CONNECTOR_ERROR); + status = INTERNAL_SERVER_ERROR; + LOGGER.error("Connector invalid response was {}.\n Returning http status {} with error body {}", exception.getMessage(), status, requestError); + } + + return Response + .status(status) + .entity(requestError) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetRefundExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetRefundExceptionMapper.java new file mode 100644 index 000000000..8ffadae43 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetRefundExceptionMapper.java @@ -0,0 +1,41 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.GetRefundException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.Code.GET_PAYMENT_REFUND_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.GET_PAYMENT_REFUND_NOT_FOUND_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class GetRefundExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(GetRefundExceptionMapper.class); + + @Override + public Response toResponse(GetRefundException exception) { + + RequestError requestError; + Response.Status status; + + if (exception.getErrorStatus() == NOT_FOUND.getStatusCode()) { + requestError = aRequestError(GET_PAYMENT_REFUND_NOT_FOUND_ERROR); + status = NOT_FOUND; + } else { + requestError = aRequestError(GET_PAYMENT_REFUND_CONNECTOR_ERROR); + status = INTERNAL_SERVER_ERROR; + LOGGER.error("Connector invalid response was {}.\n Returning http status {} with error body {}", exception.getMessage(), status, requestError); + } + + return Response + .status(status) + .entity(requestError) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetRefundsExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetRefundsExceptionMapper.java new file mode 100644 index 000000000..58a2e9642 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/GetRefundsExceptionMapper.java @@ -0,0 +1,41 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.GetRefundsException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.Code.GET_PAYMENT_REFUNDS_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.GET_PAYMENT_REFUNDS_NOT_FOUND_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class GetRefundsExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(GetRefundsExceptionMapper.class); + + @Override + public Response toResponse(GetRefundsException exception) { + + RequestError requestError; + Response.Status status; + + if (exception.getErrorStatus() == NOT_FOUND.getStatusCode()) { + requestError = aRequestError(GET_PAYMENT_REFUNDS_NOT_FOUND_ERROR); + status = NOT_FOUND; + } else { + requestError = aRequestError(GET_PAYMENT_REFUNDS_CONNECTOR_ERROR); + status = INTERNAL_SERVER_ERROR; + LOGGER.error("Connector invalid response was {}.\n Returning http status {} with error body {}", exception.getMessage(), status, requestError); + } + + return Response + .status(status) + .entity(requestError) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/InternalServerExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/InternalServerExceptionMapper.java new file mode 100644 index 000000000..ae8a3210d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/InternalServerExceptionMapper.java @@ -0,0 +1,25 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.InternalServerException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class InternalServerExceptionMapper implements ExceptionMapper { + private static final Logger logger = LoggerFactory.getLogger(InternalServerExceptionMapper.class); + @Override + public Response toResponse(InternalServerException exception) { + logger.error(exception.getMessage()); + RequestError error = aRequestError(CREATE_PAYMENT_CONNECTOR_ERROR); + return Response + .status(500) + .entity(error) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/JsonProcessingExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/JsonProcessingExceptionMapper.java new file mode 100644 index 000000000..3467cc8e2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/JsonProcessingExceptionMapper.java @@ -0,0 +1,48 @@ +package uk.gov.pay.api.exception.mapper; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import io.dropwizard.jersey.errors.LoggingExceptionMapper; +import org.apache.http.HttpStatus; +import uk.gov.pay.api.exception.AgreementValidationException; +import uk.gov.pay.api.exception.PaymentValidationException; +import uk.gov.pay.api.model.RequestError; + +import javax.annotation.Priority; +import javax.ws.rs.core.Response; + +import static uk.gov.pay.api.model.RequestError.aRequestError; + +@Priority(1) +public class JsonProcessingExceptionMapper extends LoggingExceptionMapper { + + @Override + public Response toResponse(JsonProcessingException exception) { + if (exception.getCause() instanceof PaymentValidationException) { + RequestError requestError = ((PaymentValidationException) exception.getCause()).getRequestError(); + return Response.status(HttpStatus.SC_UNPROCESSABLE_ENTITY).entity(requestError).build(); + } else if (exception.getCause() instanceof AgreementValidationException) { + RequestError requestError = ((AgreementValidationException) exception.getCause()).getRequestError(); + return Response.status(HttpStatus.SC_UNPROCESSABLE_ENTITY).entity(requestError).build(); + } else if (exception instanceof MismatchedInputException) { + MismatchedInputException mismatchedInputException = (MismatchedInputException) exception; + String typeName = isNumeric(mismatchedInputException.getTargetType()) ? + "numeric" : + mismatchedInputException.getTargetType().getSimpleName(); + + String message = String.format("Must be a valid %s format", typeName); + var requestError = aRequestError(mismatchedInputException.getPath().get(0).getFieldName(), + RequestError.Code.CREATE_PAYMENT_VALIDATION_ERROR, + message); + + return Response.status(HttpStatus.SC_UNPROCESSABLE_ENTITY).entity(requestError).build(); + } else { + return super.toResponse(exception); + } + } + + private boolean isNumeric(Class type) { + return type == int.class || type == long.class || type == double.class || Number.class.isAssignableFrom(type); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/PaymentValidationExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/PaymentValidationExceptionMapper.java new file mode 100644 index 000000000..9dc292bac --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/PaymentValidationExceptionMapper.java @@ -0,0 +1,25 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.PaymentValidationException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +public class PaymentValidationExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(PaymentValidationExceptionMapper.class); + + @Override + public Response toResponse(PaymentValidationException exception) { + + RequestError requestError = exception.getRequestError(); + LOGGER.debug("Payment Validation exception {}", requestError); + + return Response.status(422) + .entity(requestError) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/RefundsValidationExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/RefundsValidationExceptionMapper.java new file mode 100644 index 000000000..c7326786c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/RefundsValidationExceptionMapper.java @@ -0,0 +1,25 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.RefundsValidationException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +public class RefundsValidationExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(RefundsValidationExceptionMapper.class); + + @Override + public Response toResponse(RefundsValidationException exception) { + + RequestError requestError = exception.getRequestError(); + LOGGER.debug("Refunds Validation exception {}", requestError); + + return Response.status(422) + .entity(requestError) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/SearchAgreementsExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/SearchAgreementsExceptionMapper.java new file mode 100644 index 000000000..f9674a6b0 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/SearchAgreementsExceptionMapper.java @@ -0,0 +1,38 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.SearchAgreementsException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.Code.SEARCH_AGREEMENTS_LEDGER_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.SEARCH_AGREEMENTS_NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class SearchAgreementsExceptionMapper implements ExceptionMapper { + private static final Logger LOGGER = LoggerFactory.getLogger(SearchAgreementsExceptionMapper.class); + + @Override + public Response toResponse(SearchAgreementsException exception) { + if (exception.getErrorStatus() == NOT_FOUND.getStatusCode()) { + return Response + .status(NOT_FOUND) + .entity(aRequestError(SEARCH_AGREEMENTS_NOT_FOUND)) + .build(); + } else { + RequestError requestError = aRequestError(SEARCH_AGREEMENTS_LEDGER_ERROR); + final Response.Status status = INTERNAL_SERVER_ERROR; + LOGGER.error("Ledger response was {}.\n Returning http status {} with error body {}", exception.getMessage(), status, requestError); + return Response + .status(status) + .entity(requestError) + .build(); + } + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/SearchChargesExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/SearchChargesExceptionMapper.java new file mode 100644 index 000000000..fc9579e31 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/SearchChargesExceptionMapper.java @@ -0,0 +1,39 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.SearchPaymentsException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.Code.SEARCH_PAYMENTS_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.SEARCH_PAYMENTS_NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class SearchChargesExceptionMapper implements ExceptionMapper { + private static final Logger LOGGER = LoggerFactory.getLogger(SearchChargesExceptionMapper.class); + + @Override + public Response toResponse(SearchPaymentsException exception) { + if (exception.getErrorStatus() == NOT_FOUND.getStatusCode()) { + return Response + .status(NOT_FOUND) + .entity(aRequestError(SEARCH_PAYMENTS_NOT_FOUND)) + .build(); + } + else { + RequestError requestError = aRequestError(SEARCH_PAYMENTS_CONNECTOR_ERROR); + final Response.Status status = INTERNAL_SERVER_ERROR; + LOGGER.error("Connector response was {}.\n Returning http status {} with error body {}", exception.getMessage(), status, requestError); + return Response + .status(status) + .entity(requestError) + .build(); + } + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/SearchDisputesExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/SearchDisputesExceptionMapper.java new file mode 100644 index 000000000..7ed4bd75f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/SearchDisputesExceptionMapper.java @@ -0,0 +1,36 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.SearchDisputesException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.Code.GET_DISPUTE_LEDGER_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.SEARCH_DISPUTES_NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class SearchDisputesExceptionMapper implements ExceptionMapper { + private static final Logger LOGGER = LoggerFactory.getLogger(SearchDisputesExceptionMapper.class); + + @Override + public Response toResponse(SearchDisputesException exception) { + if (exception.getErrorStatus() == NOT_FOUND.getStatusCode()) { + return buildResponse(exception, SEARCH_DISPUTES_NOT_FOUND, NOT_FOUND); + } + return buildResponse(exception, GET_DISPUTE_LEDGER_ERROR, INTERNAL_SERVER_ERROR); + } + + private Response buildResponse(SearchDisputesException exception, RequestError.Code connectorError, Response.Status status) { + RequestError requestError = aRequestError(connectorError); + LOGGER.info("Ledger response was {}.\n Returning http status {} with error body {}", exception.getMessage(), status, requestError); + return Response + .status(status) + .entity(requestError) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/SearchRefundsExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/SearchRefundsExceptionMapper.java new file mode 100644 index 000000000..211b9de18 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/SearchRefundsExceptionMapper.java @@ -0,0 +1,36 @@ +package uk.gov.pay.api.exception.mapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.exception.SearchRefundsException; +import uk.gov.pay.api.model.RequestError; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.Code.SEARCH_REFUNDS_CONNECTOR_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.SEARCH_REFUNDS_NOT_FOUND; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class SearchRefundsExceptionMapper implements ExceptionMapper { + private static final Logger LOGGER = LoggerFactory.getLogger(SearchRefundsExceptionMapper.class); + + @Override + public Response toResponse(SearchRefundsException exception) { + if (exception.getErrorStatus() == NOT_FOUND.getStatusCode()) { + return buildResponse(exception, SEARCH_REFUNDS_NOT_FOUND, NOT_FOUND); + } + return buildResponse(exception, SEARCH_REFUNDS_CONNECTOR_ERROR, INTERNAL_SERVER_ERROR); + } + + private Response buildResponse(SearchRefundsException exception, RequestError.Code connectorError, Response.Status status) { + RequestError refundError = aRequestError(connectorError); + LOGGER.info("Connector response was {}.\n Returning http status {} with error body {}", exception.getMessage(), status, refundError); + return Response + .status(status) + .entity(refundError) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/ViolationExceptionMapper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/ViolationExceptionMapper.java new file mode 100644 index 000000000..90c894b9e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/exception/mapper/ViolationExceptionMapper.java @@ -0,0 +1,80 @@ +package uk.gov.pay.api.exception.mapper; + +import com.google.common.base.CaseFormat; +import io.dropwizard.jersey.validation.JerseyViolationException; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.validator.constraints.Length; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.model.RequestError; + +import javax.annotation.Priority; +import javax.validation.ConstraintViolation; +import javax.validation.Path; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_HEADER_VALIDATION_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_MISSING_FIELD_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_VALIDATION_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; +import static uk.gov.pay.api.model.RequestError.aHeaderRequestError; + +@Priority(1) +public class ViolationExceptionMapper implements ExceptionMapper { + private static final Logger LOGGER = LoggerFactory.getLogger(ViolationExceptionMapper.class); + private static final Pattern HEADER_VIOLATION_EXCEPTION_MESSAGE = Pattern.compile("Header \\[(.*)\\] .*"); + + @Override + public Response toResponse(JerseyViolationException exception) { + LOGGER.info(exception.getMessage()); + ConstraintViolation firstException = exception.getConstraintViolations().iterator().next(); + + RequestError requestError; + Matcher matcher = HEADER_VIOLATION_EXCEPTION_MESSAGE.matcher(firstException.getMessage()); + if (matcher.matches() && isLengthViolationOrPatternViolation(firstException)) { + String header = matcher.group(1); + requestError = aHeaderRequestError(header, CREATE_PAYMENT_HEADER_VALIDATION_ERROR, firstException.getMessage()); + } else { + requestError = getFieldNameRequestError(firstException); + } + + return Response.status(422) + .entity(requestError) + .build(); + } + + private static boolean isLengthViolationOrPatternViolation(ConstraintViolation firstException) { + return firstException.getConstraintDescriptor() != null && + firstException.getConstraintDescriptor().getAnnotation() != null && + (firstException.getConstraintDescriptor().getAnnotation().annotationType() == Length.class + || firstException.getConstraintDescriptor().getAnnotation().annotationType() == javax.validation.constraints.Pattern.class); + } + + private RequestError getFieldNameRequestError(ConstraintViolation firstException) { + String fieldName = getApiFieldName(firstException.getPropertyPath()); + if (firstException.getConstraintDescriptor() != null && + firstException.getConstraintDescriptor().getAnnotation() != null && + (firstException.getConstraintDescriptor().getAnnotation().annotationType() == NotBlank.class || + firstException.getConstraintDescriptor().getAnnotation().annotationType() == NotEmpty.class || + firstException.getConstraintDescriptor().getAnnotation().annotationType() == NotNull.class)) { + return aRequestError(fieldName, CREATE_PAYMENT_MISSING_FIELD_ERROR); + } + return aRequestError(fieldName, CREATE_PAYMENT_VALIDATION_ERROR, StringUtils.capitalize(firstException.getMessage())); + } + + private String getApiFieldName(Path path) { + return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, getFieldNameFromPath(path)); + } + + private String getFieldNameFromPath(Path path) { + String[] pathParts = path.toString().split("\\."); + return pathParts[pathParts.length - 1]; + } +} + diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/AuthorizationValidationFilter.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/AuthorizationValidationFilter.java new file mode 100644 index 000000000..733afa446 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/AuthorizationValidationFilter.java @@ -0,0 +1,105 @@ +package uk.gov.pay.api.filter; + +import com.google.common.io.BaseEncoding; +import org.apache.commons.codec.digest.HmacAlgorithms; +import org.apache.commons.codec.digest.HmacUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; + +import javax.inject.Inject; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Optional; + +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; +import static net.logstash.logback.argument.StructuredArguments.kv; +import static uk.gov.service.payments.logging.LoggingKeys.REMOTE_ADDRESS; + +public class AuthorizationValidationFilter implements Filter { + + private static final Logger logger = LoggerFactory.getLogger(AuthorizationValidationFilter.class); + + private static final int HMAC_SHA1_LENGTH = 32; + private static final String BEARER_PREFIX = "Bearer "; + + private static final String[] EXCLUDED_URLS = { + "/v1/auth" + }; + + private String apiKeyHmacSecret; + + @Inject + public AuthorizationValidationFilter(PublicApiConfig configuration) { + this.apiKeyHmacSecret = configuration.getApiKeyHmacSecret(); + } + + @Override + public void init(FilterConfig filterConfig) { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + String path = ((HttpServletRequest) request).getRequestURI(); + if (Arrays.stream(EXCLUDED_URLS).anyMatch(path::startsWith)) { + chain.doFilter(request, response); + return; + } + + final String authorization = ((HttpServletRequest) request).getHeader("Authorization"); + String clientAddress = Optional.ofNullable(((HttpServletRequest) request).getHeader("X-Forwarded-For")) + .map(forwarded -> forwarded.split(",")[0]) + .orElse(null); + + if (isValidAuthorizationHeader(authorization, clientAddress)) { + chain.doFilter(request, response); + } else { + ((HttpServletResponse) response).sendError(UNAUTHORIZED.getStatusCode(), UNAUTHORIZED.getReasonPhrase()); + } + } + + @Override + public void destroy() { + } + + private boolean isValidAuthorizationHeader(String authorization, String clientAddress) { + return authorization != null + && authorization.startsWith(BEARER_PREFIX) + && isValidTokenIntegrity(authorization.substring(BEARER_PREFIX.length()), clientAddress); + } + + private boolean isValidTokenIntegrity(String apiKey, String clientAddress) { + boolean isValid = false; + if (apiKey.length() >= HMAC_SHA1_LENGTH + 1) { + int initHmacIndex = apiKey.length() - HMAC_SHA1_LENGTH; + String hmacFromApiKey = apiKey.substring(initHmacIndex); + String tokenFromApiKey = apiKey.substring(0, initHmacIndex); + isValid = tokenMatchesHmac(tokenFromApiKey, hmacFromApiKey); + } + + if (!isValid) { + logger.warn("Attempt to authenticate using an API key with an invalid checksum", + kv(REMOTE_ADDRESS, clientAddress)); + } + + return isValid; + } + + private boolean tokenMatchesHmac(String token, String currentHmac) { + final String hmacCalculatedFromToken = BaseEncoding.base32Hex() + .lowerCase().omitPadding() + .encode(new HmacUtils(HmacAlgorithms.HMAC_SHA_1, apiKeyHmacSecret).hmac(token)); + return MessageDigest.isEqual(hmacCalculatedFromToken.getBytes(StandardCharsets.UTF_8), currentHmac.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ClearMdcValuesFilter.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ClearMdcValuesFilter.java new file mode 100644 index 000000000..2b4d87932 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ClearMdcValuesFilter.java @@ -0,0 +1,36 @@ +package uk.gov.pay.api.filter; + +import org.slf4j.MDC; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; + +/** + * This filter will remove MDC values at the end of a request to clear them from the thread + * before it is re-used. This filter should be registered first so that the `finally` block is + * called after all other application logic has finished. + */ +public class ClearMdcValuesFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) { } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + MDC.clear(); + try { + chain.doFilter(request, response); + } finally { + MDC.clear(); + } + } + + @Override + public void destroy() {} + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/LoggingMDCRequestFilter.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/LoggingMDCRequestFilter.java new file mode 100644 index 000000000..c3467f576 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/LoggingMDCRequestFilter.java @@ -0,0 +1,56 @@ +package uk.gov.pay.api.filter; + +import org.slf4j.MDC; +import uk.gov.pay.api.auth.Account; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.ext.Provider; +import java.security.Principal; +import java.util.Optional; + +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static uk.gov.service.payments.logging.LoggingKeys.AGREEMENT_EXTERNAL_ID; +import static uk.gov.service.payments.logging.LoggingKeys.GATEWAY_ACCOUNT_ID; +import static uk.gov.service.payments.logging.LoggingKeys.PAYMENT_EXTERNAL_ID; +import static uk.gov.service.payments.logging.LoggingKeys.REFUND_EXTERNAL_ID; +import static uk.gov.service.payments.logging.LoggingKeys.REMOTE_ADDRESS; + +@Provider +@Priority(Priorities.USER) +public class LoggingMDCRequestFilter implements ContainerRequestFilter { + + @Override + public void filter(ContainerRequestContext requestContext) { + Optional mayBeAccount = getAccount(requestContext); + MDC.put(GATEWAY_ACCOUNT_ID, mayBeAccount.map(Account::getName).orElse(EMPTY)); + mayBeAccount.ifPresent(account -> MDC.put("token_link", account.getTokenLink())); + + String clientAddress = getClientAddress(requestContext); + MDC.put(REMOTE_ADDRESS, clientAddress); + + getPathParameterFromRequest("paymentId", requestContext) + .ifPresent(paymentId -> MDC.put(PAYMENT_EXTERNAL_ID, paymentId)); + getPathParameterFromRequest("refundId", requestContext) + .ifPresent(refundId -> MDC.put(REFUND_EXTERNAL_ID, refundId)); + getPathParameterFromRequest("agreementId", requestContext) + .ifPresent(agreementId -> MDC.put(AGREEMENT_EXTERNAL_ID, agreementId)); + } + + private String getClientAddress(ContainerRequestContext requestContext) { + return Optional.ofNullable(requestContext.getHeaderString("X-Forwarded-For")) + .map(forwarded -> forwarded.split(",")[0]) + .orElse(null); + } + + private Optional getPathParameterFromRequest(String parameterName, ContainerRequestContext requestContext) { + return Optional.ofNullable(requestContext.getUriInfo().getPathParameters().getFirst(parameterName)); + } + + private Optional getAccount(ContainerRequestContext requestContext) { + Optional userPrincipal = Optional.ofNullable(requestContext.getSecurityContext().getUserPrincipal()); + return userPrincipal.map(principal -> (Account) principal); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/RateLimiterFilter.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/RateLimiterFilter.java new file mode 100644 index 000000000..e8222ade8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/RateLimiterFilter.java @@ -0,0 +1,86 @@ +package uk.gov.pay.api.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.filter.ratelimit.RateLimitException; +import uk.gov.pay.api.filter.ratelimit.RateLimiter; +import uk.gov.pay.api.resources.error.ApiErrorResponse.Code; + +import javax.annotation.Priority; +import javax.inject.Inject; +import javax.ws.rs.Priorities; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Variant; +import javax.ws.rs.ext.Provider; +import java.io.IOException; +import java.util.Optional; + +import static uk.gov.pay.api.resources.error.ApiErrorResponse.anApiErrorResponse; + +/** + * Allow only a certain number of requests from the same source (given by the Authorization Header) + * within the given time configured in the RateLimiter. See {@link RateLimiter} + *

+ * 429 Too Many Requests will be returned when rate limit is reached. + */ +@Provider +@Priority(Priorities.USER + 1000) +public class RateLimiterFilter implements ContainerRequestFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(RateLimiterFilter.class); + private static final int TOO_MANY_REQUESTS_STATUS_CODE = 429; + private static final String UTF8_CHARACTER_ENCODING = "utf-8"; + + private final RateLimiter rateLimiter; + private ObjectMapper objectMapper; + + /** + * @param rateLimiter Limiter in number of requests per given time coming from the same source (Authorization) + */ + @Inject + public RateLimiterFilter(RateLimiter rateLimiter, ObjectMapper objectMapper) { + this.rateLimiter = rateLimiter; + this.objectMapper = objectMapper; + } + + private void setTooManyRequestsError() throws IOException { + String errorResponse = objectMapper.writeValueAsString(anApiErrorResponse(Code.TOO_MANY_REQUESTS_ERROR)); + Response.ResponseBuilder builder = Response.status(TOO_MANY_REQUESTS_STATUS_CODE) + .entity(errorResponse) + .encoding(UTF8_CHARACTER_ENCODING) + .variant(new Variant(MediaType.APPLICATION_JSON_TYPE, "", UTF8_CHARACTER_ENCODING)); + + throw new WebApplicationException(builder.build()); + } + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + if ("healthcheck".equals(requestContext.getUriInfo().getPath())) { + return; + } + + String accountId = getAccountId(requestContext); + RateLimiterKey key = RateLimiterKey.from(requestContext, accountId); + try { + rateLimiter.checkRateOf(accountId, key); + } catch (RateLimitException e) { + LOGGER.info("Rate limit reached for current service [account - {}, method - {}]. Sending response '429 Too Many Requests'", + accountId, key.getMethod()); + setTooManyRequestsError(); + } + } + + private String getAccountId(ContainerRequestContext requestContext) { + Account account = (Account) requestContext.getSecurityContext().getUserPrincipal(); + return Optional.ofNullable(account) + .map(Account::getAccountId) + .orElse(StringUtils.EMPTY); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/RateLimiterKey.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/RateLimiterKey.java new file mode 100644 index 000000000..3a86b87d8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/RateLimiterKey.java @@ -0,0 +1,46 @@ +package uk.gov.pay.api.filter; + +import uk.gov.pay.api.utils.PathHelper; + +import javax.ws.rs.container.ContainerRequestContext; + +public class RateLimiterKey { + + private String method; + private String key; + private String keyType; + + public RateLimiterKey(String key, String keyType, String method) { + this.key = key; + this.keyType = keyType; + this.method = method; + } + + public static RateLimiterKey from(ContainerRequestContext requestContext, String accountId) { + final String method = requestContext.getMethod(); + + StringBuilder builder = new StringBuilder(method); + + final String pathType = PathHelper.getPathType(requestContext.getUriInfo().getPath(), method); + if (!pathType.isBlank()) { + builder.append("-").append(pathType); + } + + final String keyType = builder.toString(); + builder.append("-").append(accountId); + + return new RateLimiterKey(builder.toString(), keyType, method); + } + + public String getKey() { + return key; + } + + public String getKeyType() { + return keyType; + } + + public String getMethod() { + return method; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/LocalRateLimiter.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/LocalRateLimiter.java new file mode 100644 index 000000000..da7c055e2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/LocalRateLimiter.java @@ -0,0 +1,64 @@ +package uk.gov.pay.api.filter.ratelimit; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.app.config.RateLimiterConfig; +import uk.gov.pay.api.filter.RateLimiterKey; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.HttpMethod; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +@Singleton +public class LocalRateLimiter { + + private static final Logger LOGGER = LoggerFactory.getLogger(LocalRateLimiter.class); + + private final int noOfReqPerNode; + private final int noOfReqForPostPerNode; + private final int perMillis; + + private final Cache cache; + + @Inject + public LocalRateLimiter(RateLimiterConfig rateLimiterConfig) { + this.noOfReqPerNode = rateLimiterConfig.getNoOfReqPerNode(); + this.noOfReqForPostPerNode = rateLimiterConfig.getNoOfReqForPostPerNode(); + this.perMillis = rateLimiterConfig.getPerMillis(); + this.cache = CacheBuilder.newBuilder() + .expireAfterAccess(perMillis, TimeUnit.MILLISECONDS) + .build(); + } + + void checkRateOf(String accountId, RateLimiterKey rateLimiterKey) throws RateLimitException { + + RateLimit rateLimit = null; + try { + rateLimit = cache.get(rateLimiterKey.getKey(), () -> new RateLimit(getNoOfRequestsForMethod(rateLimiterKey.getMethod()), perMillis)); + rateLimit.updateAllowance(); + } catch (ExecutionException e) { + //ExecutionException is thrown when the valueLoader (cache.get()) throws a checked exception. + //We just create a new instance (RateLimit) so no exceptions will be thrown, this should never happen. + LOGGER.error("Unexpected error creating a Rate Limiter object in cache", e); + } catch (RateLimitException e) { + LOGGER.info(String.format("LocalRateLimiter - Rate limit exceeded for account [%s] and method [%s] - count: %d, rate allowed: %d", + accountId, + rateLimiterKey.getMethod(), + rateLimit.getRequestCount(), + rateLimit.getNoOfReq())); + + throw e; + } + } + + private int getNoOfRequestsForMethod(String method) { + if (HttpMethod.POST.equals(method)) { + return noOfReqForPostPerNode; + } + return noOfReqPerNode; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RateLimit.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RateLimit.java new file mode 100644 index 000000000..887b06fa2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RateLimit.java @@ -0,0 +1,48 @@ +package uk.gov.pay.api.filter.ratelimit; + +import java.time.Duration; +import java.time.Instant; + +import static java.time.temporal.ChronoUnit.MILLIS; + +final class RateLimit { + + private final int noOfReq; + private final int perMillis; + + private Instant created; + private int requestCount; + + RateLimit(int noOfReq, int perMillis) { + this.noOfReq = noOfReq; + this.perMillis = perMillis; + this.requestCount = 0; + this.created = Instant.now().truncatedTo(MILLIS); + } + + /** + * This block needs to be synchronous. Each RateLimit object will be shared between requests + * from the same source (Service), so is not shared across all the requests. + * + * @throws RateLimitException + */ + synchronized void updateAllowance() throws RateLimitException { + requestCount += 1; + Instant now = Instant.now().truncatedTo(MILLIS); + if (Duration.between(created, now).toMillis() >= perMillis) { + requestCount = 1; + created = now; + } + if (requestCount > noOfReq) { + throw new RateLimitException(); + } + } + + public int getNoOfReq() { + return noOfReq; + } + + public int getRequestCount() { + return requestCount; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RateLimitException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RateLimitException.java new file mode 100644 index 000000000..d3bf3a245 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RateLimitException.java @@ -0,0 +1,4 @@ +package uk.gov.pay.api.filter.ratelimit; + +public class RateLimitException extends Exception { +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RateLimitManager.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RateLimitManager.java new file mode 100644 index 000000000..22c31e85c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RateLimitManager.java @@ -0,0 +1,46 @@ +package uk.gov.pay.api.filter.ratelimit; + +import uk.gov.pay.api.app.config.RateLimiterConfig; +import uk.gov.pay.api.filter.RateLimiterKey; + +import javax.ws.rs.HttpMethod; + +public class RateLimitManager { + + private RateLimiterConfig configuration; + + public RateLimitManager(RateLimiterConfig config) { + configuration = config; + } + + public int getAllowedNumberOfRequests(RateLimiterKey rateLimiterKey, String account) { + if (configuration.getElevatedAccounts().contains(account)) { + if (HttpMethod.POST.equals(rateLimiterKey.getMethod())) { + return configuration.getNoOfPostReqForElevatedAccounts(); + } + + return configuration.getNoOfReqForElevatedAccounts(); + } + + if (configuration.getLowTrafficAccounts().contains(account)) { + if (HttpMethod.POST.equals(rateLimiterKey.getMethod())) { + return configuration.getNoOfPostReqForLowTrafficAccounts(); + } + return configuration.getNoOfReqForLowTrafficAccounts(); + } + + if (HttpMethod.POST.equals(rateLimiterKey.getMethod())) { + return configuration.getNoOfReqForPost(); + } + + return configuration.getNoOfReq(); + } + + public int getRateLimitInterval(String account) { + if (configuration.getLowTrafficAccounts().contains(account)) { + return configuration.getIntervalInMillisForLowTrafficAccounts(); + } + + return configuration.getPerMillis(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RateLimiter.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RateLimiter.java new file mode 100644 index 000000000..e6af13c2e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RateLimiter.java @@ -0,0 +1,32 @@ +package uk.gov.pay.api.filter.ratelimit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.filter.RateLimiterKey; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class RateLimiter { + + private static final Logger LOGGER = LoggerFactory.getLogger(RateLimiter.class); + + private final LocalRateLimiter localRateLimiter; + private final RedisRateLimiter redisRateLimiter; + + @Inject + public RateLimiter(LocalRateLimiter localRateLimiter, RedisRateLimiter redisRateLimiter) { + this.localRateLimiter = localRateLimiter; + this.redisRateLimiter = redisRateLimiter; + } + + public void checkRateOf(String accountId, RateLimiterKey key) throws RateLimitException { + try { + redisRateLimiter.checkRateOf(accountId, key); + } catch (RedisException e) { + LOGGER.warn("Exception occurred checking rate limits using RedisRateLimiter, falling back to LocalRateLimiter"); + localRateLimiter.checkRateOf(accountId, key); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RedisException.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RedisException.java new file mode 100644 index 000000000..06d7d72e4 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RedisException.java @@ -0,0 +1,4 @@ +package uk.gov.pay.api.filter.ratelimit; + +public class RedisException extends Exception { +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RedisRateLimiter.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RedisRateLimiter.java new file mode 100644 index 000000000..cb7a99aae --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/filter/ratelimit/RedisRateLimiter.java @@ -0,0 +1,101 @@ +package uk.gov.pay.api.filter.ratelimit; + +import com.codahale.metrics.MetricRegistry; +import com.google.inject.Inject; +import com.google.inject.OutOfScopeException; +import io.dropwizard.setup.Environment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.app.config.RateLimiterConfig; +import uk.gov.pay.api.filter.RateLimiterKey; +import uk.gov.pay.api.managed.RedisClientManager; + +import javax.inject.Singleton; +import java.time.LocalDateTime; +import java.time.temporal.ChronoField; +import java.util.concurrent.Callable; + +@Singleton +public class RedisRateLimiter { + private static final Logger LOGGER = LoggerFactory.getLogger(RedisRateLimiter.class); + private final MetricRegistry metricsRegistry; + + private RateLimitManager rateLimitManager; + private RedisClientManager redisClientManager; + + @Inject + public RedisRateLimiter(RateLimiterConfig rateLimiterConfig, RedisClientManager redisClientManager, Environment environment) { + this.rateLimitManager = new RateLimitManager(rateLimiterConfig); + this.redisClientManager = redisClientManager; + this.metricsRegistry = environment.metrics(); + } + + /** + * @throws RateLimitException + */ + void checkRateOf(String accountId, RateLimiterKey key) + throws RedisException, RateLimitException { + + Long count; + + try { + int rateLimitInterval = rateLimitManager.getRateLimitInterval(accountId); + count = updateAllowance(key.getKey(), rateLimitInterval); + } catch (Exception e) { + LOGGER.info("Failed to update allowance. Cause of error: " + e.getMessage()); + // Exception possible if redis is unavailable or perMillis is too high + throw new RedisException(); + } + + if (count != null) { + int allowedNumberOfRequests = rateLimitManager.getAllowedNumberOfRequests(key, accountId); + if (count > allowedNumberOfRequests) { + LOGGER.info(String.format("RedisRateLimiter - Rate limit exceeded for account [%s] and method [%s] - count: %d, rate allowed: %d", + accountId, key.getKeyType(), count, allowedNumberOfRequests)); + throw new RateLimitException(); + } + } + } + + private Long updateAllowance(String key, int rateLimitInterval) throws Exception { + String derivedKey = getKeyForWindow(key, rateLimitInterval); + Long count = time("redis.incr", () -> redisClientManager.getRedisConnection().sync().incr(derivedKey)); + time("redis.expire", () -> redisClientManager.getRedisConnection().sync().expire(derivedKey, rateLimitInterval / 1000)); + return count; + } + + private T time(String metricName, Callable callable) throws Exception { + return this.metricsRegistry.timer(metricName).time(callable); + } + + /** + * Derives Key (Service Key + Window) to use in Redis for noOfReq limiting. + * Recommended to use perMillis to lowest granularity. i.e, to seconds. 1000, 2000 + *

+ * Depends on perMillis + *

+ * - perMillis < 1000 : Window considered for milliseconds + * - perMillis >=1000 && <60000 : Window considered for seconds(s) + * - perMillis >=60000 && <3600000 : Window considered for minute(s) + * + * @return new key based on perMillis (works for second/minute/hour windows only) + */ + private String getKeyForWindow(String key, int rateLimitInterval) throws OutOfScopeException { + + LocalDateTime now = LocalDateTime.now(); + + int window; + + if (rateLimitInterval >= 1 && rateLimitInterval < 1000) { + window = (now.get(ChronoField.MILLI_OF_DAY) / rateLimitInterval) + 1; + } else if (rateLimitInterval >= 1000 && rateLimitInterval < 60000) { + window = now.get(ChronoField.SECOND_OF_MINUTE) / (rateLimitInterval / 1000); + } else if (rateLimitInterval >= 60000 && rateLimitInterval < 3600000) { + window = now.get(ChronoField.MINUTE_OF_HOUR) / (rateLimitInterval / 1000); + } else { + throw new OutOfScopeException("Rate limit interval specified is not currently supported"); + } + + return key + window; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/healthcheck/Ping.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/healthcheck/Ping.java new file mode 100644 index 000000000..2bbfdc8e9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/healthcheck/Ping.java @@ -0,0 +1,11 @@ +package uk.gov.pay.api.healthcheck; + +import com.codahale.metrics.health.HealthCheck; + +public class Ping extends HealthCheck { + + @Override + protected Result check() { + return Result.healthy(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/CreateAgreementRequestDeserializer.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/CreateAgreementRequestDeserializer.java new file mode 100644 index 000000000..32b677d65 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/CreateAgreementRequestDeserializer.java @@ -0,0 +1,31 @@ +package uk.gov.pay.api.json; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import uk.gov.pay.api.agreement.model.CreateAgreementRequest; +import uk.gov.pay.api.exception.BadRequestException; + +import java.io.IOException; + +import static uk.gov.pay.api.json.RequestJsonParser.parseAgreementRequest; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_AGREEMENT_PARSING_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class CreateAgreementRequestDeserializer extends StdDeserializer { + + public CreateAgreementRequestDeserializer() { + super(CreateAgreementRequest.class); + } + + @Override + public CreateAgreementRequest deserialize(JsonParser parser, DeserializationContext context) { + try { + JsonNode json = parser.readValueAsTree(); + return parseAgreementRequest(json); + } catch (IOException e) { + throw new BadRequestException(aRequestError(CREATE_AGREEMENT_PARSING_ERROR)); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/CreateCardPaymentRequestDeserializer.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/CreateCardPaymentRequestDeserializer.java new file mode 100644 index 000000000..e004d865c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/CreateCardPaymentRequestDeserializer.java @@ -0,0 +1,31 @@ +package uk.gov.pay.api.json; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import uk.gov.pay.api.exception.BadRequestException; +import uk.gov.pay.api.model.CreateCardPaymentRequest; + +import java.io.IOException; + +import static uk.gov.pay.api.json.RequestJsonParser.parsePaymentRequest; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_PARSING_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class CreateCardPaymentRequestDeserializer extends StdDeserializer { + + public CreateCardPaymentRequestDeserializer() { + super(CreateCardPaymentRequest.class); + } + + @Override + public CreateCardPaymentRequest deserialize(JsonParser parser, DeserializationContext context) { + try { + JsonNode json = parser.readValueAsTree(); + return parsePaymentRequest(json); + } catch (IOException e) { + throw new BadRequestException(aRequestError(CREATE_PAYMENT_PARSING_ERROR)); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/CreatePaymentRefundRequestDeserializer.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/CreatePaymentRefundRequestDeserializer.java new file mode 100644 index 000000000..8a0236da4 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/CreatePaymentRefundRequestDeserializer.java @@ -0,0 +1,39 @@ +package uk.gov.pay.api.json; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import uk.gov.pay.api.exception.BadRequestException; +import uk.gov.pay.api.model.CreatePaymentRefundRequest; +import uk.gov.pay.api.validation.PaymentRefundRequestValidator; + +import java.io.IOException; + +import static uk.gov.pay.api.json.RequestJsonParser.parseRefundRequest; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_REFUND_PARSING_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class CreatePaymentRefundRequestDeserializer extends StdDeserializer { + + private PaymentRefundRequestValidator validator; + + public CreatePaymentRefundRequestDeserializer(PaymentRefundRequestValidator validator) { + super(CreatePaymentRefundRequest.class); + this.validator = validator; + } + + @Override + public CreatePaymentRefundRequest deserialize(JsonParser parser, DeserializationContext context) { + + CreatePaymentRefundRequest paymentRefundRequest; + + try { + paymentRefundRequest = parseRefundRequest(parser.readValueAsTree()); + } catch (IOException e) { + throw new BadRequestException(aRequestError(CREATE_PAYMENT_REFUND_PARSING_ERROR)); + } + + validator.validate(paymentRefundRequest); + return paymentRefundRequest; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/RequestJsonParser.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/RequestJsonParser.java new file mode 100644 index 000000000..af0149484 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/RequestJsonParser.java @@ -0,0 +1,382 @@ +package uk.gov.pay.api.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import uk.gov.pay.api.agreement.model.CreateAgreementRequest; +import uk.gov.pay.api.exception.BadRequestException; +import uk.gov.pay.api.exception.PaymentValidationException; +import uk.gov.pay.api.model.CreateAgreementRequestBuilder; +import uk.gov.pay.api.model.CreateCardPaymentRequest; +import uk.gov.pay.api.model.CreateCardPaymentRequestBuilder; +import uk.gov.pay.api.model.CreatePaymentRefundRequest; +import uk.gov.pay.api.model.RequestError; +import uk.gov.pay.api.model.RequestError.Code; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.Source; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.model.charge.ExternalMetadata; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.http.HttpStatus.SC_UNPROCESSABLE_ENTITY; +import static uk.gov.pay.api.agreement.model.CreateAgreementRequest.USER_IDENTIFIER_FIELD; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.AGREEMENT_ID_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.AMOUNT_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.AUTHORISATION_MODE; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.DELAYED_CAPTURE_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.DESCRIPTION_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.EMAIL_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.INTERNAL; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.LANGUAGE_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.METADATA; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.MOTO_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.PREFILLED_ADDRESS_CITY_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.PREFILLED_ADDRESS_COUNTRY_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.PREFILLED_ADDRESS_LINE1_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.PREFILLED_ADDRESS_LINE2_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.PREFILLED_ADDRESS_POSTCODE_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.PREFILLED_BILLING_ADDRESS_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.PREFILLED_CARDHOLDER_DETAILS_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.PREFILLED_CARDHOLDER_NAME_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.REFERENCE_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.RETURN_URL_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.SET_UP_AGREEMENT_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.SOURCE_FIELD_NAME; +import static uk.gov.pay.api.model.CreatePaymentRefundRequest.REFUND_AMOUNT_AVAILABLE; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_AGREEMENT_MISSING_FIELD_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_AGREEMENT_VALIDATION_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_MISSING_FIELD_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_REFUND_MISSING_FIELD_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_REFUND_VALIDATION_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_UNEXPECTED_FIELD_ERROR; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_VALIDATION_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; +import static uk.gov.service.payments.commons.model.Source.CARD_AGENT_INITIATED_MOTO; +import static uk.gov.service.payments.commons.model.Source.CARD_API; +import static uk.gov.service.payments.commons.model.Source.CARD_PAYMENT_LINK; + +class RequestJsonParser { + + private static final Set ALLOWED_SOURCES = EnumSet.of(CARD_PAYMENT_LINK, CARD_AGENT_INITIATED_MOTO); + public static final Set ALLOWED_AUTHORISATION_MODES = EnumSet.of(AuthorisationMode.WEB, AuthorisationMode.MOTO_API, AuthorisationMode.AGREEMENT); + + private static ObjectMapper objectMapper = new ObjectMapper(); + private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + static CreatePaymentRefundRequest parseRefundRequest(JsonNode rootNode) { + Integer amount = validateAndGetAmount(rootNode, CREATE_PAYMENT_REFUND_VALIDATION_ERROR, CREATE_PAYMENT_REFUND_MISSING_FIELD_ERROR); + Integer refundAmountAvailable = rootNode.get(REFUND_AMOUNT_AVAILABLE) == null ? null : rootNode.get(REFUND_AMOUNT_AVAILABLE).asInt(); + return new CreatePaymentRefundRequest(amount, refundAmountAvailable); + } + + static CreateCardPaymentRequest parsePaymentRequest(JsonNode paymentRequest) { + + var builder = CreateCardPaymentRequestBuilder.builder() + .amount(validateAndGetAmount(paymentRequest, CREATE_PAYMENT_VALIDATION_ERROR, CREATE_PAYMENT_MISSING_FIELD_ERROR)) + .reference(validateAndGetPaymentReference(paymentRequest)) + .description(validateAndGetPaymentDescription(paymentRequest)); + + if (paymentRequest.has(RETURN_URL_FIELD_NAME)) { + String returnUrl = validateSkipNullValueAndGetString(paymentRequest.get(RETURN_URL_FIELD_NAME), + aRequestError(RETURN_URL_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, "Must be a valid URL format")); + builder.returnUrl(returnUrl); + } + + if (paymentRequest.has(MOTO_FIELD_NAME)) { + builder.moto(validateAndGetMoto(paymentRequest)); + } + + if(paymentRequest.has(SET_UP_AGREEMENT_FIELD_NAME)) { + builder.setUpAgreement(validateAndGetSetUpAgreement(paymentRequest)); + } + + AuthorisationMode authorisationMode = null; + if (paymentRequest.has(AUTHORISATION_MODE)) { + authorisationMode = validateAndGetAuthorisationMode(paymentRequest); + builder.authorisationMode(authorisationMode); + } + + if (paymentRequest.has(AGREEMENT_ID_FIELD_NAME)) { + if (AuthorisationMode.AGREEMENT == authorisationMode) { + builder.agreementId(validateAndGetString( + paymentRequest.get(AGREEMENT_ID_FIELD_NAME), + aRequestError(AGREEMENT_ID_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, "Must be a valid string format"), + aRequestError(AGREEMENT_ID_FIELD_NAME, CREATE_PAYMENT_MISSING_FIELD_ERROR))); + } else { + throw new BadRequestException(aRequestError(CREATE_PAYMENT_UNEXPECTED_FIELD_ERROR, AGREEMENT_ID_FIELD_NAME)); + } + } + + if (paymentRequest.has(LANGUAGE_FIELD_NAME)) { + builder.language(validateAndGetLanguage(paymentRequest)); + } + + if (paymentRequest.has(DELAYED_CAPTURE_FIELD_NAME)) { + builder.delayedCapture(validateAndGetDelayedCapture(paymentRequest)); + } + + if (paymentRequest.has(EMAIL_FIELD_NAME)) { + String email = validateSkipNullValueAndGetString(paymentRequest.get(EMAIL_FIELD_NAME), + aRequestError(EMAIL_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, "Field must be a string")); + builder.email(email); + } + + if (paymentRequest.has(PREFILLED_CARDHOLDER_DETAILS_FIELD_NAME)) { + JsonNode prefilledNode = paymentRequest.get(PREFILLED_CARDHOLDER_DETAILS_FIELD_NAME); + validatePrefilledCardholderDetails(prefilledNode, builder); + } + + if (paymentRequest.has(METADATA)) { + builder.metadata(validateAndGetMetadata(paymentRequest)); + } + + builder.source(validateAndGetSource(paymentRequest)); + + return builder.build(); + } + + static CreateAgreementRequest parseAgreementRequest(JsonNode agreementRequest) { + var builder = CreateAgreementRequestBuilder.builder() + .reference(validateAndGetAgreementReference(agreementRequest)) + .description(validateAndGetAgreementDescription(agreementRequest)); + + if (agreementRequest.has(USER_IDENTIFIER_FIELD)) { + String userIdentifier = validateSkipNullValueAndGetString(agreementRequest.get(USER_IDENTIFIER_FIELD), + aRequestError(USER_IDENTIFIER_FIELD, CREATE_AGREEMENT_VALIDATION_ERROR, "Field must be a string")); + builder.userIdentifier(userIdentifier); + } + + return builder.build(); + } + + private static SupportedLanguage validateAndGetLanguage(JsonNode paymentRequest) { + String errorMessage = "Must be \"en\" or \"cy\""; + RequestError requestError = aRequestError(LANGUAGE_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, errorMessage); + String language = validateAndGetString(paymentRequest.get(LANGUAGE_FIELD_NAME), requestError, requestError); + try { + return SupportedLanguage.fromIso639AlphaTwoCode(language); + } catch (IllegalArgumentException e) { + throw new WebApplicationException(Response.status(SC_UNPROCESSABLE_ENTITY).entity(requestError).build()); + } + } + + private static AuthorisationMode validateAndGetAuthorisationMode(JsonNode paymentRequest) { + String errorMessage = "Must be one of " + ALLOWED_AUTHORISATION_MODES.stream() + .map(AuthorisationMode::getName) + .collect(Collectors.joining(", ")); + RequestError requestError = aRequestError(AUTHORISATION_MODE, CREATE_PAYMENT_VALIDATION_ERROR, errorMessage); + String value = validateAndGetString(paymentRequest.get(AUTHORISATION_MODE), requestError, requestError); + + try { + AuthorisationMode authorisationMode = AuthorisationMode.of(value); + if (ALLOWED_AUTHORISATION_MODES.contains(authorisationMode)) { + return authorisationMode; + } else { + throw new PaymentValidationException(requestError); + } + } catch (IllegalArgumentException e) { + throw new PaymentValidationException(requestError); + } + } + + private static String validateAndGetPaymentDescription(JsonNode paymentRequest) { + return validateAndGetDescription(paymentRequest, CREATE_PAYMENT_VALIDATION_ERROR, CREATE_PAYMENT_MISSING_FIELD_ERROR); + } + + private static String validateAndGetAgreementDescription(JsonNode paymentRequest) { + return validateAndGetDescription(paymentRequest, CREATE_AGREEMENT_VALIDATION_ERROR, CREATE_AGREEMENT_MISSING_FIELD_ERROR); + } + + private static String validateAndGetDescription(JsonNode request, RequestError.Code validationError, RequestError.Code missingFieldError) { + return validateAndGetString( + request.get(DESCRIPTION_FIELD_NAME), + aRequestError(DESCRIPTION_FIELD_NAME, validationError, "Must be a valid string format"), + aRequestError(DESCRIPTION_FIELD_NAME, missingFieldError)); + } + + private static String validateAndGetPaymentReference(JsonNode paymentRequest) { + return validateAndGetReference(paymentRequest, CREATE_PAYMENT_VALIDATION_ERROR, CREATE_PAYMENT_MISSING_FIELD_ERROR); + } + + private static String validateAndGetAgreementReference(JsonNode agreementRequest) { + return validateAndGetReference(agreementRequest, CREATE_AGREEMENT_VALIDATION_ERROR, CREATE_AGREEMENT_MISSING_FIELD_ERROR); + } + + private static String validateAndGetReference(JsonNode request, RequestError.Code validationError, RequestError.Code missingFieldError) { + return validateAndGetString( + request.get(REFERENCE_FIELD_NAME), + aRequestError(REFERENCE_FIELD_NAME, validationError, "Must be a valid string format"), + aRequestError(REFERENCE_FIELD_NAME, missingFieldError)); + } + + private static String validateAndGetString(JsonNode jsonNode, RequestError validationError, RequestError missingError) { + String value = validateAndGetValue(jsonNode, validationError, missingError, JsonNode::isTextual, JsonNode::asText); + check(isNotBlank(value), missingError); + return value; + } + + private static Boolean validateAndGetDelayedCapture(JsonNode paymentRequest) { + return validateAndGetValue( + paymentRequest.get(DELAYED_CAPTURE_FIELD_NAME), + aRequestError(DELAYED_CAPTURE_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, "Must be true or false"), + aRequestError(DELAYED_CAPTURE_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, "Must be true or false"), + JsonNode::isBoolean, + JsonNode::booleanValue); + } + + private static Boolean validateAndGetMoto(JsonNode paymentRequest) { + return validateAndGetValue( + paymentRequest.get(MOTO_FIELD_NAME), + aRequestError(MOTO_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, "Must be true or false"), + aRequestError(MOTO_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, "Must be true or false"), + JsonNode::isBoolean, + JsonNode::booleanValue); + } + + private static String validateAndGetSetUpAgreement(JsonNode paymentRequest) { + return paymentRequest.get(SET_UP_AGREEMENT_FIELD_NAME).textValue(); + } + + private static Integer validateAndGetAmount(JsonNode paymentRequest, Code validationError, Code missingError) { + return validateAndGetValue( + paymentRequest.get(AMOUNT_FIELD_NAME), + aRequestError(AMOUNT_FIELD_NAME, validationError, "Must be a valid numeric format"), + aRequestError(AMOUNT_FIELD_NAME, missingError), + JsonNode::isInt, + JsonNode::intValue); + } + + private static ExternalMetadata validateAndGetMetadata(JsonNode paymentRequest) { + Map metadataMap; + try { + metadataMap = objectMapper.convertValue(paymentRequest.get("metadata"), Map.class); + } catch (IllegalArgumentException e) { + RequestError requestError = aRequestError(METADATA, CREATE_PAYMENT_VALIDATION_ERROR, + "Must be an object of JSON key-value pairs"); + throw new WebApplicationException(Response.status(SC_UNPROCESSABLE_ENTITY).entity(requestError).build()); + } + + if (metadataMap == null) { + RequestError requestError = aRequestError(METADATA, CREATE_PAYMENT_VALIDATION_ERROR, + "Value must not be null"); + throw new WebApplicationException(Response.status(SC_UNPROCESSABLE_ENTITY).entity(requestError).build()); + } + + ExternalMetadata metadata = new ExternalMetadata(metadataMap); + Set> violations = validator.validate(metadata); + if (violations.size() > 0) { + String message = violations.stream() + .map(ConstraintViolation::getMessage) + .map(msg -> msg.replace("Field [metadata] ", "")) + .map(StringUtils::capitalize) + .collect(Collectors.joining(". ")); + RequestError requestError = aRequestError(METADATA, CREATE_PAYMENT_VALIDATION_ERROR, message); + throw new WebApplicationException(Response.status(SC_UNPROCESSABLE_ENTITY).entity(requestError).build()); + } + return metadata; + } + + private static void validatePrefilledCardholderDetails(JsonNode prefilledNode, CreateCardPaymentRequestBuilder builder) { + if (prefilledNode.has(PREFILLED_CARDHOLDER_NAME_FIELD_NAME)) { + String cardHolderName = validateSkipNullValueAndGetString(prefilledNode.get(PREFILLED_CARDHOLDER_NAME_FIELD_NAME), + aRequestError(PREFILLED_CARDHOLDER_NAME_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, "Field must be a string")); + builder.cardholderName(cardHolderName); + } + if (prefilledNode.has(PREFILLED_BILLING_ADDRESS_FIELD_NAME)) { + JsonNode addressNode = prefilledNode.get(PREFILLED_BILLING_ADDRESS_FIELD_NAME); + if (addressNode.has(PREFILLED_ADDRESS_LINE1_FIELD_NAME)) { + String addressLine1 = validateSkipNullValueAndGetString(addressNode.get(PREFILLED_ADDRESS_LINE1_FIELD_NAME), + aRequestError(PREFILLED_ADDRESS_LINE1_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, "Field must be a string")); + builder.addressLine1(addressLine1); + } + if (addressNode.has(PREFILLED_ADDRESS_LINE2_FIELD_NAME)) { + String addressLine1 = validateSkipNullValueAndGetString(addressNode.get(PREFILLED_ADDRESS_LINE2_FIELD_NAME), + aRequestError(PREFILLED_ADDRESS_LINE2_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, "Field must be a string")); + builder.addressLine2(addressLine1); + } + if (addressNode.has(PREFILLED_ADDRESS_CITY_FIELD_NAME)) { + String addressCity = validateSkipNullValueAndGetString(addressNode.get(PREFILLED_ADDRESS_CITY_FIELD_NAME), + aRequestError(PREFILLED_ADDRESS_CITY_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, "Field must be a string")); + builder.city(addressCity); + } + if (addressNode.has(PREFILLED_ADDRESS_POSTCODE_FIELD_NAME)) { + String addressPostcode = validateSkipNullValueAndGetString(addressNode.get(PREFILLED_ADDRESS_POSTCODE_FIELD_NAME), + aRequestError(PREFILLED_ADDRESS_POSTCODE_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, "Field must be a string")); + builder.postcode(addressPostcode); + } + if (addressNode.has(PREFILLED_ADDRESS_COUNTRY_FIELD_NAME)) { + String countryCode = validateSkipNullValueAndGetString(addressNode.get(PREFILLED_ADDRESS_COUNTRY_FIELD_NAME), + aRequestError(PREFILLED_ADDRESS_COUNTRY_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, "Field must be a string")); + builder.country(countryCode); + } + } + } + + private static Source validateAndGetSource(JsonNode paymentRequest) { + if (paymentRequest.has(INTERNAL)) { + JsonNode internalNode = paymentRequest.get(INTERNAL); + + if (internalNode.has(SOURCE_FIELD_NAME)) { + String errorMessage = "Accepted values are only CARD_PAYMENT_LINK, CARD_AGENT_INITIATED_MOTO"; + RequestError requestError = aRequestError(SOURCE_FIELD_NAME, CREATE_PAYMENT_VALIDATION_ERROR, errorMessage); + String sourceString = validateSkipNullValueAndGetString(internalNode.get(SOURCE_FIELD_NAME), requestError); + + try { + Source source = Source.valueOf(sourceString); + if (ALLOWED_SOURCES.contains(source)) { + return source; + } + throw new BadRequestException(requestError); + } catch (IllegalArgumentException e) { + throw new WebApplicationException(Response.status(SC_UNPROCESSABLE_ENTITY).entity(requestError).build()); + } + } + } + + return CARD_API; + } + + private static T validateAndGetValue(JsonNode jsonNode, + RequestError validationError, + RequestError missingError, + Function isExpectedType, + Function valueFromJsonNode) { + if (jsonNode != null && !jsonNode.isNull()) { + check(isExpectedType.apply(jsonNode), validationError); + return valueFromJsonNode.apply(jsonNode); + } + throw new BadRequestException(missingError); + } + + private static String validateSkipNullValueAndGetString(JsonNode jsonNode, RequestError validationError) { + return validateSkipNullAndGetValue(jsonNode, validationError, JsonNode::isTextual, JsonNode::asText); + } + + private static T validateSkipNullAndGetValue(JsonNode jsonNode, + RequestError validationError, + Function isExpectedType, + Function valueFromJsonNode) { + if (jsonNode == null || jsonNode.isNull()) { + return null; + } + check(isExpectedType.apply(jsonNode), validationError); + return valueFromJsonNode.apply(jsonNode); + } + + private static void check(boolean condition, RequestError error) { + if (!condition) { + throw new BadRequestException(error); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/StringDeserializer.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/StringDeserializer.java new file mode 100644 index 000000000..462a4ce2b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/json/StringDeserializer.java @@ -0,0 +1,28 @@ +package uk.gov.pay.api.json; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import uk.gov.pay.api.exception.PaymentValidationException; + +import java.io.IOException; + +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_VALIDATION_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class StringDeserializer extends StdDeserializer { + + public StringDeserializer() { + super(String.class); + } + + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.hasToken(JsonToken.VALUE_STRING)) { + return p.getText(); + } + var requestError = aRequestError(p.getCurrentName(), CREATE_PAYMENT_VALIDATION_ERROR, "Must be of type String"); + throw new PaymentValidationException(requestError); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/model/AgreementSearchParams.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/model/AgreementSearchParams.java new file mode 100644 index 000000000..219db71a7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/model/AgreementSearchParams.java @@ -0,0 +1,97 @@ +package uk.gov.pay.api.ledger.model; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.ws.rs.QueryParam; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static uk.gov.pay.api.common.SearchConstants.DISPLAY_SIZE; +import static uk.gov.pay.api.common.SearchConstants.PAGE; +import static uk.gov.pay.api.common.SearchConstants.REFERENCE_KEY; +import static uk.gov.pay.api.common.SearchConstants.STATUS_KEY; + +public class AgreementSearchParams { + @QueryParam("reference") + @Parameter(name = "reference", description = "Returns agreements with a `reference` that exactly matches the value you sent. " + + "This parameter is not case sensitive. " + + "A `reference` was associated with the agreement when that agreement was created.", + example = "CT-22-23-0001") + private String reference; + + @QueryParam("status") + @Parameter(name = "status", description = "Returns agreements in a matching `status`. " + + "`status` reflects where an agreement is in its lifecycle. " + + "You can [read more about the meanings of the different agreement status values]" + + "(https://docs.payments.service.gov.uk/recurring_payments/#understanding-agreement-status).", + schema = @Schema(allowableValues = {"created", "active", "cancelled", "inactive"})) + private String status; + + @QueryParam("page") + @Parameter(name = "page", + description = "Returns a specific page of results. Defaults to `1`. " + + "You can [read about search pagination](https://docs.payments.service.gov.uk/api_reference/#pagination)", + example = "1") + private String pageNumber; + + @QueryParam("display_size") + @Parameter(name = "display_size", + description = "The number of agreements returned per results page. Defaults to `500`. " + + "Maximum value is `500`. You can [read about search pagination](https://docs.payments.service.gov.uk/api_reference/#pagination)", + example = "50") + private String displaySize; + + public AgreementSearchParams() { + // Framework gubbins + } + + public AgreementSearchParams(String reference, String status, String pageNumber, String displaySize) { + this.reference = reference; + this.status = status; + this.pageNumber = pageNumber; + this.displaySize = displaySize; + } + + public String getReference() { + return reference; + } + + public void setReference(String reference) { + this.reference = reference; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getPageNumber() { + return pageNumber; + } + + public void setPageNumber(String pageNumber) { + this.pageNumber = pageNumber; + } + + public String getDisplaySize() { + return displaySize; + } + + public void setDisplaySize(String displaySize) { + this.displaySize = displaySize; + } + + public Map getQueryMap() { + var queryParams = new HashMap(); + Optional.ofNullable(reference).ifPresent(reference -> queryParams.put(REFERENCE_KEY, reference)); + Optional.ofNullable(status).ifPresent(status -> queryParams.put(STATUS_KEY, status)); + Optional.ofNullable(pageNumber).ifPresent(pageNumber -> queryParams.put(PAGE, pageNumber)); + Optional.ofNullable(displaySize).ifPresent(displaySize -> queryParams.put(DISPLAY_SIZE, displaySize)); + return Map.copyOf(queryParams); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/model/SearchResults.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/model/SearchResults.java new file mode 100644 index 000000000..487bb18a7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/model/SearchResults.java @@ -0,0 +1,58 @@ +package uk.gov.pay.api.ledger.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import uk.gov.pay.api.model.links.SearchNavigationLinks; +import uk.gov.pay.api.model.search.SearchPagination; + +import java.util.List; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class SearchResults implements SearchPagination { + + private int total; + private int count; + private int page; + private List results; + SearchNavigationLinks links; + + public SearchResults() { + } + + public SearchResults(int total, int count, int page, List results, + SearchNavigationLinks links) { + this.total = total; + this.count = count; + this.page = page; + this.results = results; + this.links = links; + } + + @Override + public int getTotal() { + return total; + } + + @Override + public int getCount() { + return count; + } + + @Override + + public int getPage() { + return page; + } + + @JsonProperty("results") + public List getResults() { + return results; + } + + @Override + @JsonProperty("_links") + public SearchNavigationLinks getLinks() { + return links; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/model/TransactionSearchParams.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/model/TransactionSearchParams.java new file mode 100644 index 000000000..d8fac11ac --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/model/TransactionSearchParams.java @@ -0,0 +1,127 @@ +package uk.gov.pay.api.ledger.model; + +import javax.ws.rs.QueryParam; +import java.util.HashMap; +import java.util.Map; + +import static uk.gov.pay.api.common.SearchConstants.CARDHOLDER_NAME_KEY; +import static uk.gov.pay.api.common.SearchConstants.CARD_BRAND_KEY; +import static uk.gov.pay.api.common.SearchConstants.DISPLAY_SIZE; +import static uk.gov.pay.api.common.SearchConstants.EMAIL_KEY; +import static uk.gov.pay.api.common.SearchConstants.FIRST_DIGITS_CARD_NUMBER_KEY; +import static uk.gov.pay.api.common.SearchConstants.FROM_DATE_KEY; +import static uk.gov.pay.api.common.SearchConstants.FROM_SETTLED_DATE; +import static uk.gov.pay.api.common.SearchConstants.GATEWAY_ACCOUNT_ID; +import static uk.gov.pay.api.common.SearchConstants.LAST_DIGITS_CARD_NUMBER_KEY; +import static uk.gov.pay.api.common.SearchConstants.PAGE; +import static uk.gov.pay.api.common.SearchConstants.REFERENCE_KEY; +import static uk.gov.pay.api.common.SearchConstants.STATE_KEY; +import static uk.gov.pay.api.common.SearchConstants.TO_DATE_KEY; +import static uk.gov.pay.api.common.SearchConstants.TO_SETTLED_DATE; + +public class TransactionSearchParams { + + private String accountId; + @QueryParam("reference") + private String reference; + @QueryParam("email") + private String email; + @QueryParam("state") + private String state; + @QueryParam("card_brand") + private String cardBrand; + @QueryParam("from_date") + private String fromDate; + @QueryParam("to_date") + private String toDate; + @QueryParam("page") + private String pageNumber; + @QueryParam("display_size") + private String displaySize; + @QueryParam("cardholder_name") + private String cardHolderName; + @QueryParam("first_digits_card_number") + private String firstDigitsCardNumber; + @QueryParam("last_digits_card_number") + private String lastDigitsCardNumber; + @QueryParam("from_settled_date") + private String fromSettledDate; + @QueryParam("to_settled_date") + private String toSettledDate; + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public String getReference() { + return reference; + } + + public String getEmail() { + return email; + } + + public String getState() { + return state; + } + + public String getCardBrand() { + return cardBrand; + } + + public String getFromDate() { + return fromDate; + } + + public String getToDate() { + return toDate; + } + + public String getPageNumber() { + return pageNumber; + } + + public String getDisplaySize() { + return displaySize; + } + + public String getCardHolderName() { + return cardHolderName; + } + + public String getFirstDigitsCardNumber() { + return firstDigitsCardNumber; + } + + public String getLastDigitsCardNumber() { + return lastDigitsCardNumber; + } + + public String getFromSettledDate() { + return fromSettledDate; + } + + public String getToSettledDate() { + return toSettledDate; + } + + public Map getQueryMap() { + Map queryParams = new HashMap<>(); + queryParams.put(GATEWAY_ACCOUNT_ID, accountId); + queryParams.put(REFERENCE_KEY, reference); + queryParams.put(EMAIL_KEY, email); + queryParams.put(STATE_KEY, state); + queryParams.put(CARD_BRAND_KEY, cardBrand); + queryParams.put(CARDHOLDER_NAME_KEY, cardHolderName); + queryParams.put(FIRST_DIGITS_CARD_NUMBER_KEY, firstDigitsCardNumber); + queryParams.put(LAST_DIGITS_CARD_NUMBER_KEY, lastDigitsCardNumber); + queryParams.put(FROM_DATE_KEY, fromDate); + queryParams.put(TO_DATE_KEY, toDate); + queryParams.put(PAGE, pageNumber); + queryParams.put(DISPLAY_SIZE, displaySize); + queryParams.put(FROM_SETTLED_DATE, fromSettledDate); + queryParams.put(TO_SETTLED_DATE, toSettledDate); + + return queryParams; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/resource/TransactionsResource.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/resource/TransactionsResource.java new file mode 100644 index 000000000..51aea9032 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/resource/TransactionsResource.java @@ -0,0 +1,37 @@ +package uk.gov.pay.api.ledger.resource; + +import com.codahale.metrics.annotation.Timed; +import io.dropwizard.auth.Auth; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.ledger.model.SearchResults; +import uk.gov.pay.api.ledger.model.TransactionSearchParams; +import uk.gov.pay.api.ledger.service.TransactionSearchService; +import uk.gov.pay.api.model.search.card.PaymentForSearchResult; + +import javax.inject.Inject; +import javax.ws.rs.BeanParam; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +@Path("/") +@Produces({"application/json"}) +public class TransactionsResource { + + private final TransactionSearchService transactionSearchService; + + @Inject + public TransactionsResource(TransactionSearchService transactionSearchService) { + this.transactionSearchService = transactionSearchService; + } + + @GET + @Timed + @Path("/v1/transactions") + @Produces(APPLICATION_JSON) + public SearchResults getTransactions(@Auth Account account, @BeanParam TransactionSearchParams searchParams) { + return transactionSearchService.doSearch(account, searchParams); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/service/LedgerUriGenerator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/service/LedgerUriGenerator.java new file mode 100644 index 000000000..d68e7f168 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/service/LedgerUriGenerator.java @@ -0,0 +1,79 @@ +package uk.gov.pay.api.ledger.service; + +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.auth.Account; + +import javax.inject.Inject; +import javax.ws.rs.core.UriBuilder; +import java.util.Map; + +import static java.lang.String.format; + +public class LedgerUriGenerator { + private final PublicApiConfig configuration; + + @Inject + public LedgerUriGenerator(PublicApiConfig configuration) { + this.configuration = configuration; + } + + public String transactionsURIWithParams(Map queryParams) { + queryParams.put("status_version", "1"); + return buildLedgerUri("/v1/transaction", queryParams); + } + + private String buildLedgerUri(String path, Map params) { + var ledgerUrl = configuration.getLedgerUrl(); + UriBuilder builder = UriBuilder.fromPath(ledgerUrl).path(path); + params.entrySet().stream() + .filter(k -> k.getValue() != null) + .forEach(k -> builder.queryParam(k.getKey(), k.getValue())); + return builder.toString(); + } + + public String transactionURI(Account gatewayAccountId, String paymentId, String transactionType) { + String path = format("/v1/transaction/%s", paymentId); + return buildLedgerUri(path, Map.of( + "account_id", gatewayAccountId.getAccountId(), + "transaction_type", transactionType, + "status_version", "1" + )); + } + + public String transactionURI(Account gatewayAccountId, String refundId, String transactionType, String paymentId) { + String path = format("/v1/transaction/%s", refundId); + return buildLedgerUri(path, Map.of( + "account_id", gatewayAccountId.getAccountId(), + "transaction_type", transactionType, + "parent_external_id", paymentId, + "status_version", "1") + ); + } + + public String transactionEventsURI(Account gatewayAccountId, String paymentId) { + String path = format("/v1/transaction/%s/event", paymentId); + return buildLedgerUri(path, Map.of( + "gateway_account_id", gatewayAccountId.getAccountId(), + "status_version", "1") + ); + } + + public String transactionsForTransactionURI(String gatewayAccountId, String paymentId, String transactionType) { + String path = format("/v1/transaction/%s/transaction", paymentId); + return buildLedgerUri(path, + Map.of("gateway_account_id", gatewayAccountId, + "transaction_type", transactionType) + ); + } + + public String agreementURI(Account account, String agreementId) { + String path = format("/v1/agreement/%s", agreementId); + return buildLedgerUri(path, Map.of( + "account_id", account.getAccountId() + )); + } + + public String agreementsURIWithParams(Map queryParams) { + return buildLedgerUri("/v1/agreement", queryParams); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/service/TransactionSearchService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/service/TransactionSearchService.java new file mode 100644 index 000000000..6c2ed40d4 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/ledger/service/TransactionSearchService.java @@ -0,0 +1,169 @@ +package uk.gov.pay.api.ledger.service; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.BadRequestException; +import uk.gov.pay.api.exception.SearchPaymentsException; +import uk.gov.pay.api.exception.SearchTransactionsException; +import uk.gov.pay.api.ledger.model.SearchResults; +import uk.gov.pay.api.ledger.model.TransactionSearchParams; +import uk.gov.pay.api.model.RequestError; +import uk.gov.pay.api.model.TransactionResponse; +import uk.gov.pay.api.model.links.Link; +import uk.gov.pay.api.model.links.SearchNavigationLinks; +import uk.gov.pay.api.model.search.card.PaymentForSearchResult; +import uk.gov.pay.api.model.search.card.PaymentSearchResponse; +import uk.gov.pay.api.service.PaymentUriGenerator; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.util.stream.Collectors.toList; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.http.HttpStatus.SC_OK; +import static uk.gov.pay.api.common.SearchConstants.CARDHOLDER_NAME_KEY; +import static uk.gov.pay.api.common.SearchConstants.CARD_BRAND_KEY; +import static uk.gov.pay.api.common.SearchConstants.DISPLAY_SIZE; +import static uk.gov.pay.api.common.SearchConstants.EMAIL_KEY; +import static uk.gov.pay.api.common.SearchConstants.FIRST_DIGITS_CARD_NUMBER_KEY; +import static uk.gov.pay.api.common.SearchConstants.FROM_DATE_KEY; +import static uk.gov.pay.api.common.SearchConstants.LAST_DIGITS_CARD_NUMBER_KEY; +import static uk.gov.pay.api.common.SearchConstants.PAGE; +import static uk.gov.pay.api.common.SearchConstants.REFERENCE_KEY; +import static uk.gov.pay.api.common.SearchConstants.STATE_KEY; +import static uk.gov.pay.api.common.SearchConstants.TO_DATE_KEY; +import static uk.gov.pay.api.validation.PaymentSearchValidator.validateSearchParameters; + +public class TransactionSearchService { + + private final String baseUrl; + private final PaymentUriGenerator paymentApiUriGenerator; + private final Client client; + private final LedgerUriGenerator ledgerUriGenerator; + private final Logger LOGGER = LoggerFactory.getLogger(TransactionSearchService.class); + + @Inject + public TransactionSearchService(Client client, + PublicApiConfig configuration, + LedgerUriGenerator ledgerUriGenerator, + PaymentUriGenerator paymentApiUriGenerator) { + this.client = client; + this.ledgerUriGenerator = ledgerUriGenerator; + this.paymentApiUriGenerator = paymentApiUriGenerator; + this.baseUrl = configuration.getBaseUrl(); + } + + public SearchResults doSearch(Account account, TransactionSearchParams searchParams) { + validateSearchParameters(searchParams.getState(), searchParams.getReference(), + searchParams.getEmail(), searchParams.getCardBrand(), searchParams.getFromDate(), + searchParams.getToDate(), searchParams.getPageNumber(), searchParams.getDisplaySize(), + searchParams.getFirstDigitsCardNumber(), searchParams.getLastDigitsCardNumber(), + searchParams.getFromSettledDate(), searchParams.getToSettledDate()); + validateSupportedSearchParams(searchParams.getQueryMap()); + + searchParams.setAccountId(account.getAccountId()); + String url = ledgerUriGenerator.transactionsURIWithParams(searchParams.getQueryMap()); + Response ledgerResponse = client + .target(url) + .request() + .header(HttpHeaders.ACCEPT, APPLICATION_JSON) + .get(); + LOGGER.info("response from ledger for transaction search: {}", ledgerResponse); + if (ledgerResponse.getStatus() == SC_OK) { + return processResponse(ledgerResponse); + } + throw new SearchTransactionsException(ledgerResponse); + } + + private SearchResults processResponse(Response connectorResponse) { + PaymentSearchResponse response; + try { + response = connectorResponse.readEntity(new GenericType<>() { + }); + } catch (ProcessingException ex) { + throw new SearchTransactionsException(ex); + } + + List chargeFromResponses = response.getPayments() + .stream() + .map(this::getPaymentForSearchResult) + .collect(toList()); + + return new SearchResults<>( + response.getTotal(), + response.getCount(), + response.getPage(), + chargeFromResponses, + transformLinks(response.getLinks()) + ); + } + + private PaymentForSearchResult getPaymentForSearchResult(TransactionResponse transactionResponse) { + return PaymentForSearchResult.valueOf( + transactionResponse, + paymentApiUriGenerator.getPaymentURI(baseUrl, transactionResponse.getTransactionId()), + paymentApiUriGenerator.getPaymentEventsURI(baseUrl, transactionResponse.getTransactionId()), + paymentApiUriGenerator.getPaymentCancelURI(baseUrl, transactionResponse.getTransactionId()), + paymentApiUriGenerator.getPaymentRefundsURI(baseUrl, transactionResponse.getTransactionId()), + paymentApiUriGenerator.getPaymentCaptureURI(baseUrl, transactionResponse.getTransactionId())); + } + + private SearchNavigationLinks transformLinks(SearchNavigationLinks links) { + final String path = "/v1/transactions"; + + try { + return new SearchNavigationLinks() + .withSelfLink(transformIntoPublicUri(baseUrl, links.getSelf(), path)) + .withFirstLink(transformIntoPublicUri(baseUrl, links.getFirstPage(), path)) + .withLastLink(transformIntoPublicUri(baseUrl, links.getLastPage(), path)) + .withPrevLink(transformIntoPublicUri(baseUrl, links.getPrevPage(), path)) + .withNextLink(transformIntoPublicUri(baseUrl, links.getNextPage(), path)); + } catch (URISyntaxException ex) { + throw new SearchPaymentsException(ex); + } + } + + private String transformIntoPublicUri(String baseUrl, Link link, String path) throws URISyntaxException { + if (link == null) + return null; + + return UriBuilder.fromUri(baseUrl) + .path(path) + .replaceQuery(new URI(link.getHref()).getQuery()) + .replaceQueryParam("account_id", (Object[]) null) + .build() + .toString(); + } + + private void validateSupportedSearchParams(Map queryParams) { + queryParams.entrySet().stream() + .filter(this::isUnsupportedParamWithNonBlankValue) + .findFirst() + .ifPresent(invalidParam -> { + throw new BadRequestException(RequestError + .aRequestError(RequestError.Code.SEARCH_PAYMENTS_VALIDATION_ERROR, invalidParam.getKey())); + }); + } + + private boolean isUnsupportedParamWithNonBlankValue(Map.Entry queryParam) { + return !getSupportedSearchParams().contains(queryParam.getKey()) && isNotBlank(queryParam.getValue()); + } + + private Set getSupportedSearchParams() { + return ImmutableSet.of(REFERENCE_KEY, EMAIL_KEY, STATE_KEY, CARD_BRAND_KEY, CARDHOLDER_NAME_KEY, FIRST_DIGITS_CARD_NUMBER_KEY, LAST_DIGITS_CARD_NUMBER_KEY, FROM_DATE_KEY, TO_DATE_KEY, PAGE, DISPLAY_SIZE); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/managed/RedisClientManager.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/managed/RedisClientManager.java new file mode 100644 index 000000000..31b7892f2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/managed/RedisClientManager.java @@ -0,0 +1,37 @@ +package uk.gov.pay.api.managed; + +import io.dropwizard.lifecycle.Managed; +import io.lettuce.core.RedisClient; +import io.lettuce.core.api.StatefulRedisConnection; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class RedisClientManager implements Managed { + private RedisClient redisClient; + private StatefulRedisConnection statefulRedisConnection; + + @Inject + public RedisClientManager(RedisClient redisClient) { + this.redisClient = redisClient; + } + + public StatefulRedisConnection getRedisConnection() { + if (statefulRedisConnection == null) { + statefulRedisConnection = redisClient.connect(); + } + return statefulRedisConnection; + } + + @Override + public void start() throws Exception {} + + @Override + public void stop() throws Exception { + if (statefulRedisConnection != null) { + statefulRedisConnection.close(); + } + redisClient.shutdown(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/Address.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/Address.java new file mode 100644 index 000000000..5e1dc0c08 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/Address.java @@ -0,0 +1,85 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.validation.constraints.Size; +import java.util.Objects; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(NON_NULL) +@Schema(name = "Address", description = "A structure representing the billing address of a card") +public class Address { + + @Size(max = 255, message = "Must be less than or equal to {max} characters length") + private String line1; + + @Size(max = 255, message = "Must be less than or equal to {max} characters length") + private String line2; + + @Size(max = 25, message = "Must be less than or equal to {max} characters length") + private String postcode; + + @Size(max = 255, message = "Must be less than or equal to {max} characters length") + private String city; + + private String country; + + public Address(@JsonProperty("line1") String line1, + @JsonProperty("line2") String line2, + @JsonProperty("postcode") String postcode, + @JsonProperty("city") String city, + @JsonProperty("country") String country) { + this.line1 = line1; + this.line2 = line2; + this.postcode = postcode; + this.city = city; + this.country = country; + } + + @Schema(example = "address line 1", description = "The first line of the paying user’s address.") + public String getLine1() { + return line1; + } + + @Schema(example = "address line 2", description = "The second line of the paying user’s address.") + public String getLine2() { + return line2; + } + + @Schema(example = "AB1 2CD", description = "The paying user's postcode.") + public String getPostcode() { + return postcode; + } + + @Schema(example = "address city", description="The paying user's city.") + public String getCity() { + return city; + } + + @Schema(example = "GB", description = "The paying user’s country, displayed as a 2-character ISO-3166-1-alpha-2 code.") + public String getCountry() { + return country; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Address address = (Address) o; + return Objects.equals(line1, address.line1) && + Objects.equals(line2, address.line2) && + Objects.equals(postcode, address.postcode) && + Objects.equals(city, address.city) && + Objects.equals(country, address.country); + } + + @Override + public int hashCode() { + return Objects.hash(line1, line2, postcode, city, country); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/AuthorisationRequest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/AuthorisationRequest.java new file mode 100644 index 000000000..2fb706ec7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/AuthorisationRequest.java @@ -0,0 +1,66 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +@Schema(name = "AuthorisationRequest", description = "Contains the user's payment information. This information will be sent to the payment service provider to authorise the payment.") +public class AuthorisationRequest { + @JsonProperty("one_time_token") + @NotBlank + private String oneTimeToken; + @JsonProperty("card_number") + @NotNull + @Size(min = 12, max = 19, message = "Must be between 12 and 19 characters long") + private String cardNumber; + @JsonProperty("cvc") + @NotNull + @Size(min = 3, max = 4, message = "Must be between 3 and 4 characters long") + private String cvc; + @JsonProperty("expiry_date") + @NotNull + @Size(min = 5, max = 5, message = "Must be a valid date with the format MM/YY") + private String expiryDate; + @JsonProperty("cardholder_name") + @NotBlank + @Size(max = 255, message = "Must be less than or equal to {max} characters long") + private String cardholderName; + + public AuthorisationRequest() { + } + + public AuthorisationRequest(String oneTimeToken, String cardNumber, String cvc, String expiryDate, String cardholderName) { + this.oneTimeToken = oneTimeToken; + this.cardNumber = cardNumber; + this.cvc = cvc; + this.expiryDate = expiryDate; + this.cardholderName = cardholderName; + } + + @Schema(description = "This single use token authorises your request and matches it to a payment. GOV.UK Pay generated the `one_time_token` when the payment was created.", required = true, example = "12345-edsfr-6789-gtyu") + public String getOneTimeToken() { + return oneTimeToken; + } + + @Schema(description = "The full card number from the paying user's card.", required = true, minLength = 12, maxLength = 19, example = "4242424242424242") + public String getCardNumber() { + return cardNumber; + } + + @Schema(description = "The card verification code (CVC) or card verification value (CVV) on the paying user's card.", required = true, minLength = 3, maxLength = 4, example = "123") + public String getCvc() { + return cvc; + } + + @Schema(description = "The expiry date of the paying user's card. This value must be in `MM/YY` format.", required = true, minLength = 5, maxLength = 5, example = "09/22") + public String getExpiryDate() { + return expiryDate; + } + + @Schema(description = "The name on the paying user's card.", required = true, maxLength = 255, example = "J. Citizen") + public String getCardholderName() { + return cardholderName; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/AuthorisationSummary.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/AuthorisationSummary.java new file mode 100644 index 000000000..4a317e181 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/AuthorisationSummary.java @@ -0,0 +1,26 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "AuthorisationSummary", description = "Object containing information about the authentication of the payment.") +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AuthorisationSummary { + + @JsonProperty("three_d_secure") + private ThreeDSecure threeDSecure; + + public AuthorisationSummary() { + } + + public AuthorisationSummary(ThreeDSecure threeDSecure) { + this.threeDSecure = threeDSecure; + } + + public ThreeDSecure getThreeDSecure() { + return threeDSecure; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CardDetails.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CardDetails.java new file mode 100644 index 000000000..961f9c359 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CardDetails.java @@ -0,0 +1,118 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.Optional; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.ALWAYS; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +@JsonInclude(ALWAYS) +@Schema(name = "CardDetails", description = "A structure representing the payment card") +@JsonIgnoreProperties(ignoreUnknown = true) +public class CardDetails { + + @JsonProperty("last_digits_card_number") + private final String lastDigitsCardNumber; + @JsonProperty("first_digits_card_number") + private final String firstDigitsCardNumber; + @JsonProperty("cardholder_name") + private final String cardHolderName; + @JsonProperty("expiry_date") + private final String expiryDate; + @JsonProperty("billing_address") + private final Address billingAddress; + @JsonProperty("card_brand") + private final String cardBrand; + @JsonProperty("card_type") + private final String cardType; + @JsonProperty("wallet_type") + private String walletType; + + public CardDetails(String lastDigitsCardNumber, + String firstDigitsCardNumber, + String cardHolderName, + String expiryDate, + Address billingAddress, + String cardBrand, + String cardType, + String walletType + ) { + this.lastDigitsCardNumber = lastDigitsCardNumber; + this.firstDigitsCardNumber = firstDigitsCardNumber; + this.cardHolderName = cardHolderName; + this.expiryDate = expiryDate; + this.billingAddress = billingAddress; + this.cardBrand = cardBrand; + this.cardType = cardType; + this.walletType = walletType; + } + + public static CardDetails from(CardDetailsFromResponse cardDetailsFromResponse, String walletType) { + if (cardDetailsFromResponse == null) { + return null; + } else { + return new CardDetails( + cardDetailsFromResponse.getLastDigitsCardNumber(), + cardDetailsFromResponse.getFirstDigitsCardNumber(), + cardDetailsFromResponse.getCardHolderName(), + cardDetailsFromResponse.getExpiryDate(), + cardDetailsFromResponse.getBillingAddress().orElse(null), + cardDetailsFromResponse.getCardBrand(), + cardDetailsFromResponse.getCardType(), + walletType + ); + } + + } + + + @Schema(example = "1234", accessMode = READ_ONLY) + public String getLastDigitsCardNumber() { + return lastDigitsCardNumber; + } + + @Schema(example = "123456", accessMode = READ_ONLY) + public String getFirstDigitsCardNumber() { + return firstDigitsCardNumber; + } + + @Schema(example = "Mr. Card holder") + public String getCardHolderName() { + return cardHolderName; + } + + @Schema(description = "The expiry date of the card the user paid with in `MM/YY` format.", example = "04/24", accessMode = READ_ONLY) + public String getExpiryDate() { + return expiryDate; + } + + public Optional

getBillingAddress() { + return Optional.ofNullable(billingAddress); + } + + @Schema(example = "Visa", description = "The brand of card the user paid with.", accessMode = READ_ONLY) + public String getCardBrand() { + return cardBrand; + } + + @Schema(description = "The type of card the user paid with." + + "`null` means your user paid with Google Pay or " + + "we did not recognise which type of card they paid with.", + allowableValues = {"debit", "credit", "null"}, example = "debit", accessMode = READ_ONLY) + public String getCardType() { + return cardType; + } + + public void setWalletType(String walletType) { + this.walletType = walletType; + } + + @Schema(example = "Apple Pay", description = "The digital wallet type that the user paid with", allowableValues = {"Apple Pay", "Google Pay"}) + public Optional getWalletType() { + return Optional.ofNullable(walletType); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CardDetailsFromResponse.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CardDetailsFromResponse.java new file mode 100644 index 000000000..82d0e7e5b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CardDetailsFromResponse.java @@ -0,0 +1,80 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Optional; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.ALWAYS; + +@JsonInclude(ALWAYS) +@JsonIgnoreProperties(ignoreUnknown = true) +public class CardDetailsFromResponse { + + @JsonProperty("last_digits_card_number") + private final String lastDigitsCardNumber; + + @JsonProperty("first_digits_card_number") + private final String firstDigitsCardNumber; + + @JsonProperty("cardholder_name") + private final String cardHolderName; + + @JsonProperty("expiry_date") + private final String expiryDate; + + @JsonProperty("billing_address") + private final Address billingAddress; + + @JsonProperty("card_brand") + private final String cardBrand; + + @JsonProperty("card_type") + private final String cardType; + + public CardDetailsFromResponse(@JsonProperty("last_digits_card_number") String lastDigitsCardNumber, + @JsonProperty("first_digits_card_number") String firstDigitsCardNumber, + @JsonProperty("cardholder_name") String cardHolderName, + @JsonProperty("expiry_date") String expiryDate, + @JsonProperty("billing_address") Address billingAddress, + @JsonProperty("card_brand") String cardBrand, + @JsonProperty("card_type") String cardType) { + this.lastDigitsCardNumber = lastDigitsCardNumber; + this.firstDigitsCardNumber = firstDigitsCardNumber; + this.cardHolderName = cardHolderName; + this.expiryDate = expiryDate; + this.billingAddress = billingAddress; + this.cardBrand = cardBrand; + this.cardType = cardType; + } + + public String getLastDigitsCardNumber() { + return lastDigitsCardNumber; + } + + public String getFirstDigitsCardNumber() { + return firstDigitsCardNumber; + } + + public String getCardHolderName() { + return cardHolderName; + } + + public String getExpiryDate() { + return expiryDate; + } + + public Optional
getBillingAddress() { + return Optional.ofNullable(billingAddress); + } + + public String getCardBrand() { + return cardBrand; + } + + + public String getCardType() { + return cardType; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CardPayment.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CardPayment.java new file mode 100644 index 000000000..194b49355 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CardPayment.java @@ -0,0 +1,311 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.service.payments.commons.api.json.ExternalMetadataSerialiser; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.model.charge.ExternalMetadata; + +import java.util.Optional; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +@JsonInclude(value = JsonInclude.Include.NON_EMPTY) +@Schema(name = "CardPayment") +public class CardPayment { + + public static final String LINKS_JSON_ATTRIBUTE = "_links"; + + @JsonProperty("payment_id") + protected String paymentId; + + @JsonProperty("payment_provider") + protected String paymentProvider; + + protected long amount; + + protected String description; + + protected String reference; + + @JsonProperty("created_date") + protected String createdDate; + @JsonProperty("refund_summary") + private RefundSummary refundSummary; + + @JsonProperty("settlement_summary") + private PaymentSettlementSummary settlementSummary; + + @JsonProperty("card_details") + private CardDetails cardDetails; + + @JsonSerialize(using = ToStringSerializer.class) + private SupportedLanguage language; + + @JsonProperty("delayed_capture") + private boolean delayedCapture; + + @JsonProperty("moto") + private boolean moto; + + @JsonProperty("corporate_card_surcharge") + private Long corporateCardSurcharge; + + @JsonProperty("total_amount") + private Long totalAmount; + + @JsonProperty("fee") + private Long fee; + + @JsonProperty("net_amount") + private Long netAmount; + + @JsonProperty("provider_id") + @Schema(example = "reference-from-payment-gateway", + description = "The unique ID your payment service provider generated for this payment. " + + "This is not the same as the `payment_id`.", + accessMode = READ_ONLY) + private String providerId; + + @JsonSerialize(using = ExternalMetadataSerialiser.class) + @Schema(name = "metadata", example = "{\"property1\": \"value1\", \"property2\": \"value2\"}\"") + private ExternalMetadata metadata; + + @JsonProperty("return_url") + protected String returnUrl; + + protected String email; + + protected PaymentState state; + + //Used by Swagger to document the right model in the PaymentsResource + @JsonIgnore + protected String paymentType; + + @JsonProperty("authorisation_summary") + private AuthorisationSummary authorisationSummary; + + @JsonProperty("agreement_id") + private String agreementId; + + @JsonProperty("authorisation_mode") + private AuthorisationMode authorisationMode; + + public CardPayment(String chargeId, long amount, PaymentState state, String returnUrl, String description, + String reference, String email, String paymentProvider, String createdDate, + RefundSummary refundSummary, PaymentSettlementSummary settlementSummary, CardDetails cardDetails, + SupportedLanguage language, boolean delayedCapture, boolean moto, Long corporateCardSurcharge, Long totalAmount, + String providerId, ExternalMetadata metadata, Long fee, Long netAmount, AuthorisationSummary authorisationSummary, String agreementId, + AuthorisationMode authorisationMode) { + this.paymentId = chargeId; + this.amount = amount; + this.description = description; + this.reference = reference; + this.paymentProvider = paymentProvider; + this.createdDate = createdDate; + this.state = state; + this.refundSummary = refundSummary; + this.settlementSummary = settlementSummary; + this.cardDetails = cardDetails; + this.providerId = providerId; + this.metadata = metadata; + this.paymentType = TokenPaymentType.CARD.getFriendlyName(); + this.language = language; + this.delayedCapture = delayedCapture; + this.moto = moto; + this.corporateCardSurcharge = corporateCardSurcharge; + this.totalAmount = totalAmount; + this.fee = fee; + this.netAmount = netAmount; + this.email = email; + this.returnUrl = returnUrl; + this.authorisationSummary = authorisationSummary; + this.agreementId = agreementId; + this.authorisationMode = authorisationMode; + } + + /** + * card brand is no longer a top level charge property. It is now at `card_details.card_brand` attribute + * We still need to support `v1` clients with a top level card brand attribute to keep support their integrations. + * + * @return + */ + @Schema(description = "This attribute is deprecated. Please use `card_details.card_brand` instead.", + example = "Visa", accessMode = READ_ONLY, deprecated = true) + @JsonProperty("card_brand") + @Deprecated + public String getCardBrand() { + return cardDetails != null ? cardDetails.getCardBrand() : null; + } + + public ExternalMetadata getMetadata() { + return metadata; + } + + public Optional getRefundSummary() { + return Optional.ofNullable(refundSummary); + } + + public Optional getSettlementSummary() { + return Optional.ofNullable(settlementSummary); + } + + public Optional getCardDetails() { + return Optional.ofNullable(cardDetails); + } + + @Schema(name = "language", example = "en", + description = "The ISO-6391 Alpha-2 code of the [language of the user's payment page]" + + "(https://docs.payments.service.gov.uk/optional_features/welsh_language).") + public SupportedLanguage getLanguage() { + return language; + } + + @Schema(description = "`delayed_capture` is `true` if you’re " + + "[controlling how long it takes GOV.UK Pay to take (‘capture’) a payment]" + + "(https://docs.payments.service.gov.uk/delayed_capture).", + example = "false", accessMode = READ_ONLY) + public boolean getDelayedCapture() { + return delayedCapture; + } + + @Schema(description = "Indicates if this payment is a [Mail Order / Telephone Order (MOTO) payment]" + + "(https://docs.payments.service.gov.uk/moto_payments).", example = "false") + public boolean getMoto() { return moto; } + + @Schema(example = "250", description = "The [corporate card surcharge]" + + "(https://docs.payments.service.gov.uk/corporate_card_surcharges/#add-corporate-card-fees) " + + "amount in pence.", + accessMode = READ_ONLY) + public Optional getCorporateCardSurcharge() { + return Optional.ofNullable(corporateCardSurcharge); + } + + @Schema(example = "5", description = "The [payment service provider’s (PSP) transaction fee]" + + "(https://docs.payments.service.gov.uk/reporting/#psp-fees), in pence. " + + "`fee` only appears when we have taken (‘captured’) the payment from the user " + + "or if their payment fails after they submitted their card details. " + + "`fee` will not appear if your PSP is Worldpay or you are using an API key from a test service.", + accessMode = READ_ONLY) + public Optional getFee() { + return Optional.ofNullable(fee); + } + + @Schema(example = "1195", + description = "The amount, in pence, that will be paid into your bank account " + + "after your payment service provider takes the `fee`.", + accessMode = READ_ONLY) + public Optional getNetAmount() { + return Optional.ofNullable(netAmount); + } + + @Schema(example = "1450", + description = "Amount your user paid in pence, including corporate card fees. " + + "`total_amount` only appears if you [added a corporate card surcharge to the payment]" + + "(https://docs.payments.service.gov.uk/corporate_card_surcharges/#add-corporate-card-fees).", + accessMode = READ_ONLY) + public Optional getTotalAmount() { + return Optional.ofNullable(totalAmount); + } + + public String getProviderId() { + return providerId; + } + + @Schema(example = "http://your.service.domain/your-reference", + description = "The URL you direct the paying user to after their payment journey on GOV.UK Pay ends.", + accessMode = READ_ONLY) + public Optional getReturnUrl() { + return Optional.ofNullable(returnUrl); + } + + @Schema(example = "The paying user’s email address.") + public Optional getEmail() { + return Optional.ofNullable(email); + } + + public PaymentState getState() { + return state; + } + + @Schema(description = "Object containing information about the authentication of the payment.") + public AuthorisationSummary getAuthorisationSummary() { + return authorisationSummary; + } + + @Schema(hidden = true) + public String getAgreementId() { + return agreementId; + } + + @Schema(type = "String", description = "How the payment will be authorised. Payments created in `web` mode require the paying user to visit the `next_url` to complete the payment.", + allowableValues = {"web", "moto_api", "external"}) + public AuthorisationMode getAuthorisationMode() { + return authorisationMode; + } + + @Schema(example = "2016-01-21T17:15:00.000Z", accessMode = READ_ONLY) + public String getCreatedDate() { + return createdDate; + } + + @Schema(example = "hu20sqlact5260q2nanm0q8u93", + description = "The unique ID GOV.UK Pay automatically associated " + + "with this payment when you created it.", + accessMode = READ_ONLY) + public String getPaymentId() { + return paymentId; + } + + @Schema(example = "1200", description = "The description assigned to the payment when it was created.") + public long getAmount() { + return amount; + } + + @Schema(example = "Your Service Description", description = "The description assigned to the payment when it was created.") + public String getDescription() { + return description; + } + + @Schema(example = "your-reference", + description = "The reference associated with the payment when it was created. " + + "`reference` is not unique - multiple payments can have the same `reference` value.") + public String getReference() { + return reference; + } + + @Schema(example = "worldpay", + description = "The payment service provider that processed this payment.", + accessMode = READ_ONLY) + public String getPaymentProvider() { + return paymentProvider; + } + + @Override + public String toString() { + // Don't include: + // description - some services include PII + // reference - can come from user input for payment links, in the past they have mistakenly entered card numbers + // return url - services can include identifiers that are incorrectly flagged as PII or card numbers + return "Card Payment{" + + "paymentId='" + paymentId + '\'' + + ", paymentProvider='" + paymentProvider + '\'' + + ", cardBrandLabel='" + getCardBrand() + '\'' + + ", amount=" + amount + + ", fee=" + fee + + ", netAmount=" + netAmount + + ", corporateCardSurcharge='" + corporateCardSurcharge + '\'' + + ", state='" + state + '\'' + + ", language='" + language.toString() + '\'' + + ", delayedCapture=" + delayedCapture + + ", moto=" + moto + + ", createdDate='" + createdDate + '\'' + + ", agreementId='" + agreementId + '\'' + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/Charge.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/Charge.java new file mode 100644 index 000000000..2ab0a340f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/Charge.java @@ -0,0 +1,262 @@ +package uk.gov.pay.api.model; + +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.model.charge.ExternalMetadata; + +import java.util.List; +import java.util.Optional; + +public class Charge { + + private String chargeId; + + private String returnUrl; + + private String paymentProvider; + + private List links; + + private RefundSummary refundSummary; + + private PaymentSettlementSummary settlementSummary; + + private CardDetails cardDetails; + + private Long amount; + + private PaymentState state; + + private String description; + + private String reference; + + private String email; + + private SupportedLanguage language; + + private boolean delayedCapture; + + private boolean moto; + + private String agreementId; + + private Long corporateCardSurcharge; + + private Long totalAmount; + + private Long fee; + + private Long netAmount; + + private String createdDate; + + private String gatewayTransactionId; + + private ExternalMetadata metadata; + + private AuthorisationSummary authorisationSummary; + + private AuthorisationMode authorisationMode; + + public Charge(String chargeId, Long amount, PaymentState state, String returnUrl, String description, + String reference, String email, String paymentProvider, String createdDate, + SupportedLanguage language, boolean delayedCapture, boolean moto, RefundSummary refundSummary, + PaymentSettlementSummary settlementSummary, CardDetails cardDetails, + List links, Long corporateCardSurcharge, Long totalAmount, + String gatewayTransactionId, ExternalMetadata metadata, Long fee, Long netAmount, + AuthorisationSummary authorisationSummary, String agreementId, AuthorisationMode authorisationMode) { + this.chargeId = chargeId; + this.amount = amount; + this.state = state; + this.returnUrl = returnUrl; + this.description = description; + this.reference = reference; + this.email = email; + this.paymentProvider = paymentProvider; + this.createdDate = createdDate; + this.language = language; + this.delayedCapture = delayedCapture; + this.moto = moto; + this.refundSummary = refundSummary; + this.settlementSummary = settlementSummary; + this.cardDetails = cardDetails; + this.links = links; + this.corporateCardSurcharge = corporateCardSurcharge; + this.totalAmount = totalAmount; + this.gatewayTransactionId = gatewayTransactionId; + this.metadata = metadata; + this.fee = fee; + this.netAmount = netAmount; + this.authorisationSummary = authorisationSummary; + this.agreementId = agreementId; + this.authorisationMode = authorisationMode; + } + + public static Charge from(ChargeFromResponse chargeFromResponse) { + return new Charge( + chargeFromResponse.getChargeId(), + chargeFromResponse.getAmount(), + chargeFromResponse.getState(), + chargeFromResponse.getReturnUrl(), + chargeFromResponse.getDescription(), + chargeFromResponse.getReference(), + chargeFromResponse.getEmail(), + chargeFromResponse.getPaymentProvider(), + chargeFromResponse.getCreatedDate(), + chargeFromResponse.getLanguage(), + chargeFromResponse.getDelayedCapture(), + chargeFromResponse.isMoto(), + chargeFromResponse.getRefundSummary(), + chargeFromResponse.getSettlementSummary(), + chargeFromResponse.getWalletType() + .map(wallet -> CardDetails.from(chargeFromResponse.getCardDetailsFromResponse(), wallet.getTitleCase())) + .orElse(CardDetails.from(chargeFromResponse.getCardDetailsFromResponse(), null)), + chargeFromResponse.getLinks(), + chargeFromResponse.getCorporateCardSurcharge(), + chargeFromResponse.getTotalAmount(), + chargeFromResponse.getGatewayTransactionId(), + chargeFromResponse.getMetadata().orElse(null), + chargeFromResponse.getFee(), + chargeFromResponse.getNetAmount(), + chargeFromResponse.getAuthorisationSummary(), + chargeFromResponse.getAgreementId(), + chargeFromResponse.getAuthorisationMode() + ); + } + + public static Charge from(TransactionResponse transactionResponse) { + return new Charge( + transactionResponse.getTransactionId(), + transactionResponse.getAmount(), + transactionResponse.getState(), + transactionResponse.getReturnUrl(), + transactionResponse.getDescription(), + transactionResponse.getReference(), + transactionResponse.getEmail(), + transactionResponse.getPaymentProvider(), + transactionResponse.getCreatedDate(), + transactionResponse.getLanguage(), + transactionResponse.getDelayedCapture(), + transactionResponse.isMoto(), + transactionResponse.getRefundSummary(), + transactionResponse.getSettlementSummary(), + transactionResponse.getWalletType() + .map(wallet -> CardDetails.from(transactionResponse.getCardDetailsFromResponse(), wallet.getTitleCase())) + .orElse(CardDetails.from(transactionResponse.getCardDetailsFromResponse(), null)), + transactionResponse.getLinks(), + transactionResponse.getCorporateCardSurcharge(), + transactionResponse.getTotalAmount(), + transactionResponse.getGatewayTransactionId(), + transactionResponse.getMetadata().orElse(null), + transactionResponse.getFee(), + transactionResponse.getNetAmount(), + transactionResponse.getAuthorisationSummary(), + null, + transactionResponse.getAuthorisationMode()); + } + + public Optional getMetadata() { + return Optional.ofNullable(metadata); + } + + public String getChargeId() { + return chargeId; + } + + public Long getAmount() { + return amount; + } + + public PaymentState getState() { + return state; + } + + public String getReturnUrl() { + return returnUrl; + } + + public String getDescription() { + return description; + } + + public String getReference() { + return reference; + } + + public String getEmail() { + return email; + } + + public SupportedLanguage getLanguage() { + return language; + } + + public boolean getDelayedCapture() { + return delayedCapture; + } + + public boolean isMoto() { + return moto; + } + + public Long getCorporateCardSurcharge() { + return corporateCardSurcharge; + } + + public Long getTotalAmount() { + return totalAmount; + } + + public Long getFee() { + return fee; + } + + public Long getNetAmount() { + return netAmount; + } + + public String getPaymentProvider() { + return paymentProvider; + } + + public String getCardBrand() { + return cardDetails != null ? cardDetails.getCardBrand() : ""; + } + + public String getCreatedDate() { + return createdDate; + } + + public List getLinks() { + return links; + } + + public RefundSummary getRefundSummary() { + return refundSummary; + } + + public PaymentSettlementSummary getSettlementSummary() { + return settlementSummary; + } + + public CardDetails getCardDetails() { + return cardDetails; + } + + public String getGatewayTransactionId() { + return gatewayTransactionId; + } + + public AuthorisationSummary getAuthorisationSummary() { + return authorisationSummary; + } + + public String getAgreementId() { + return agreementId; + } + + public AuthorisationMode getAuthorisationMode() { + return authorisationMode; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ChargeFromResponse.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ChargeFromResponse.java new file mode 100644 index 000000000..baa959f39 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ChargeFromResponse.java @@ -0,0 +1,235 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import uk.gov.pay.api.model.telephone.PaymentOutcome; +import uk.gov.pay.api.utils.CustomSupportedLanguageDeserializer; +import uk.gov.pay.api.utils.WalletDeserializer; +import uk.gov.service.payments.commons.api.json.ExternalMetadataDeserialiser; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.model.charge.ExternalMetadata; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ChargeFromResponse { + + private String chargeId; + + private String returnUrl; + + private String paymentProvider; + + private List links = new ArrayList<>(); + + private RefundSummary refundSummary; + + private PaymentSettlementSummary settlementSummary; + + @JsonProperty("card_details") + private CardDetailsFromResponse cardDetails; + + private Long amount; + + private PaymentState state; + + private String description; + + private String reference; + + private String email; + + private String telephoneNumber; + + @JsonProperty("agreement_id") + private String agreementId; + + @JsonDeserialize(using = CustomSupportedLanguageDeserializer.class) + private SupportedLanguage language; + + private boolean delayedCapture; + + private boolean moto; + + private Long corporateCardSurcharge; + + private Long totalAmount; + + private Long fee; + + private Long netAmount; + + private String createdDate; + + private String authorisedDate; + + private String processorId; + + private String providerId; + + private String authCode; + + private PaymentOutcome paymentOutcome; + + private String gatewayTransactionId; + + @JsonDeserialize(using = ExternalMetadataDeserialiser.class) + private ExternalMetadata metadata; + + private AuthorisationSummary authorisationSummary; + + private AuthorisationMode authorisationMode; + + // wallet_type is a top level charge property but is returned as part of the card_details object + @JsonProperty("wallet_type") + @JsonDeserialize(using = WalletDeserializer.class) + private Wallet walletType; + + public Optional getMetadata() { + return Optional.ofNullable(metadata); + } + + public String getChargeId() { + return chargeId; + } + + public Long getAmount() { + return amount; + } + + public PaymentState getState() { + return state; + } + + public String getReturnUrl() { + return returnUrl; + } + + public String getDescription() { + return description; + } + + public String getReference() { + return reference; + } + + public String getEmail() { + return email; + } + + public String getTelephoneNumber() { + return telephoneNumber; + } + + public SupportedLanguage getLanguage() { + return language; + } + + public boolean getDelayedCapture() { + return delayedCapture; + } + + public boolean isMoto() { + return moto; + } + + public String getAgreementId() { + return agreementId; + } + + public Long getCorporateCardSurcharge() { + return corporateCardSurcharge; + } + + public Long getTotalAmount() { + return totalAmount; + } + + public Long getFee() { + return fee; + } + + public Long getNetAmount() { + return netAmount; + } + + public String getPaymentProvider() { + return paymentProvider; + } + + /** + * card brand is no longer a top level charge property. It is now at `card_details.card_brand` attribute + * We still need to support `v1` clients with a top level card brand attribute to keep support their integrations. + * + * @return + */ + @JsonProperty("card_brand") + public String getCardBrand() { + return cardDetails != null ? cardDetails.getCardBrand() : ""; + } + + public String getCreatedDate() { + return createdDate; + } + + public String getAuthorisedDate() { + return authorisedDate; + } + + public String getProcessorId() { + return processorId; + } + + public String getProviderId() { + return providerId; + } + + public String getAuthCode() { + return authCode; + } + + public PaymentOutcome getPaymentOutcome() { + return paymentOutcome; + } + + public List getLinks() { + return links; + } + + public RefundSummary getRefundSummary() { + return refundSummary; + } + + public PaymentSettlementSummary getSettlementSummary() { + return settlementSummary; + } + + public CardDetailsFromResponse getCardDetailsFromResponse() { + return cardDetails; + } + + public String getGatewayTransactionId() { + return gatewayTransactionId; + } + + public AuthorisationSummary getAuthorisationSummary() { + return authorisationSummary; + }; + + public AuthorisationMode getAuthorisationMode() { + return authorisationMode; + } + + public Optional getWalletType() { + return Optional.ofNullable(walletType); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreateAgreementRequestBuilder.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreateAgreementRequestBuilder.java new file mode 100644 index 000000000..23a059785 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreateAgreementRequestBuilder.java @@ -0,0 +1,43 @@ +package uk.gov.pay.api.model; + +import uk.gov.pay.api.agreement.model.CreateAgreementRequest; + +public class CreateAgreementRequestBuilder { + private String reference; + private String description; + private String userIdentifier; + public static CreateAgreementRequestBuilder builder() { + return new CreateAgreementRequestBuilder(); + } + + public CreateAgreementRequest build() { + return new CreateAgreementRequest(this); + } + + public CreateAgreementRequestBuilder reference(String reference) { + this.reference = reference; + return this; + } + + public CreateAgreementRequestBuilder description(String description) { + this.description = description; + return this; + } + + public CreateAgreementRequestBuilder userIdentifier(String userIdentifier) { + this.userIdentifier = userIdentifier; + return this; + } + + public String getReference() { + return reference; + } + + public String getDescription() { + return description; + } + + public String getUserIdentifier() { + return userIdentifier; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreateCardPaymentRequest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreateCardPaymentRequest.java new file mode 100644 index 000000000..540d2d801 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreateCardPaymentRequest.java @@ -0,0 +1,287 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.hibernate.validator.constraints.Length; +import uk.gov.pay.api.utils.JsonStringBuilder; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.model.charge.ExternalMetadata; + +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.Size; +import java.util.Optional; +import java.util.StringJoiner; + +@Schema(description = "The create payment request body") +public class CreateCardPaymentRequest { + + public static final int EMAIL_MAX_LENGTH = 254; + public static final String AMOUNT_FIELD_NAME = "amount"; + public static final String REFERENCE_FIELD_NAME = "reference"; + public static final String DESCRIPTION_FIELD_NAME = "description"; + public static final String LANGUAGE_FIELD_NAME = "language"; + public static final String EMAIL_FIELD_NAME = "email"; + public static final int REFERENCE_MAX_LENGTH = 255; + public static final int AMOUNT_MAX_VALUE = 10000000; + public static final int AMOUNT_MIN_VALUE = 0; + public static final int DESCRIPTION_MAX_LENGTH = 255; + public static final String RETURN_URL_FIELD_NAME = "return_url"; + public static final int URL_MAX_LENGTH = 2000; + public static final String PREFILLED_CARDHOLDER_DETAILS_FIELD_NAME = "prefilled_cardholder_details"; + public static final String PREFILLED_CARDHOLDER_NAME_FIELD_NAME = "cardholder_name"; + public static final String PREFILLED_BILLING_ADDRESS_FIELD_NAME = "billing_address"; + public static final String PREFILLED_ADDRESS_LINE1_FIELD_NAME = "line1"; + public static final String PREFILLED_ADDRESS_LINE2_FIELD_NAME = "line2"; + public static final String PREFILLED_ADDRESS_CITY_FIELD_NAME = "city"; + public static final String PREFILLED_ADDRESS_POSTCODE_FIELD_NAME = "postcode"; + public static final String PREFILLED_ADDRESS_COUNTRY_FIELD_NAME = "country"; + public static final String DELAYED_CAPTURE_FIELD_NAME = "delayed_capture"; + public static final String MOTO_FIELD_NAME = "moto"; + public static final String SET_UP_AGREEMENT_FIELD_NAME = "set_up_agreement"; + public static final String AGREEMENT_ID_FIELD_NAME = "agreement_id"; + public static final String SOURCE_FIELD_NAME = "source"; + public static final String METADATA = "metadata"; + public static final String INTERNAL = "internal"; + public static final String AUTHORISATION_MODE = "authorisation_mode"; + private static final String PREFILLED_CARDHOLDER_DETAILS = "prefilled_cardholder_details"; + private static final String BILLING_ADDRESS = "billing_address"; + + // Even though the minimum is 0, this is only allowed for accounts this is enabled for and is a hidden feature + // so the validation error message will always state that the minimum is 1 for consistency. + @JsonProperty("amount") + @Min(value = AMOUNT_MIN_VALUE, message = "Must be greater than or equal to 1") + @Max(value = AMOUNT_MAX_VALUE, message = "Must be less than or equal to {value}") + private int amount; + + @JsonProperty("reference") + @Size(max = REFERENCE_MAX_LENGTH, message = "Must be less than or equal to {max} characters length") + private String reference; + + @JsonProperty("description") + @Size(max = DESCRIPTION_MAX_LENGTH, message = "Must be less than or equal to {max} characters length") + private String description; + + @JsonProperty("language") + private SupportedLanguage language; + + @JsonProperty("email") + @Length(max = EMAIL_MAX_LENGTH, message = "Must be less than or equal to {max} characters length") + private String email; + + @Size(max = URL_MAX_LENGTH, message = "Must be less than or equal to {max} characters length") + @JsonProperty("return_url") + private final String returnUrl; + + private final Boolean delayedCapture; + + private final Boolean moto; + + @Schema(name = "metadata", example = "{\"property1\": \"value1\", \"property2\": \"value2\"}\"") + private final ExternalMetadata metadata; + + private final Internal internal; + + @JsonProperty("set_up_agreement") + @Size(min=26, max=26, message = "Field [set_up_agreement] length must be 26") + private String setUpAgreement; + + @JsonProperty("agreement_id") + @Size(min=26, max=26, message = "Field [agreement_id] length must be 26") + private String agreementId; + + @Valid + private final PrefilledCardholderDetails prefilledCardholderDetails; + + private final AuthorisationMode authorisationMode; + + public CreateCardPaymentRequest(CreateCardPaymentRequestBuilder builder) { + this.amount = builder.getAmount(); + this.reference = builder.getReference(); + this.description = builder.getDescription(); + this.language = builder.getLanguage(); + this.email = builder.getEmail(); + this.returnUrl = builder.getReturnUrl(); + this.delayedCapture = builder.getDelayedCapture(); + this.moto = builder.isMoto(); + this.metadata = builder.getMetadata(); + this.prefilledCardholderDetails = builder.getPrefilledCardholderDetails(); + this.internal = builder.getInternal(); + this.setUpAgreement = builder.getSetUpAgreement(); + this.authorisationMode = builder.getAuthorisationMode(); + this.agreementId = builder.getAgreementId(); + } + + @Schema(description = "Sets the amount the user will pay, in pence.", required = true, minimum = "1", maximum = "10000000", example = "12000") + public int getAmount() { + return amount; + } + + @Schema(description = "Associate a reference with this payment. " + + "`reference` is not unique - multiple payments can have identical `reference` values.", + required = true, example = "12345") + public String getReference() { + return reference; + } + + @Schema(description = "A human-readable description of the payment you’re creating. " + + "Paying users see this description on the payment pages. " + + "Service staff see the description in the GOV.UK Pay admin tool", + required = true, example = "New passport application") + public String getDescription() { + return description; + } + + @Schema(description = "[Sets the language of the user’s payment page]" + + "(https://docs.payments.service.gov.uk/optional_features/welsh_language) " + + "with an ISO-6391 Alpha-2 code of a supported language.", example = "en") + @JsonProperty(LANGUAGE_FIELD_NAME) + public Optional getLanguage() { + return Optional.ofNullable(language); + } + + @Schema(name = "email", example = "Joe.Bogs@example.org", description = "email") + @JsonProperty(EMAIL_FIELD_NAME) + public Optional getEmail() { + return Optional.ofNullable(email); + } + + @Schema(description = "The URL [the paying user is directed to after their payment journey on GOV.UK Pay ends]" + + "(https://docs.payments.service.gov.uk/making_payments/#choose-the-return-url-and-match-your-users-to-payments).", + required = true, example = "https://service-name.gov.uk/transactions/12345") + public String getReturnUrl() { + return returnUrl; + } + + @Schema(description = "prefilled_cardholder_details") + @JsonProperty(CreateCardPaymentRequest.PREFILLED_CARDHOLDER_DETAILS_FIELD_NAME) + public Optional getPrefilledCardholderDetails() { + return Optional.ofNullable(prefilledCardholderDetails); + } + + @Schema(description = "You can use this parameter to " + + "[delay taking a payment from the paying user’s bank account]" + + "(https://docs.payments.service.gov.uk/delayed_capture/#delay-taking-a-payment). " + + "For example, you might want to do your own anti-fraud checks on payments, " + + "or check that users are eligible for your service. Defaults to `false`.", + example = "false") + @JsonProperty(DELAYED_CAPTURE_FIELD_NAME) + public Optional getDelayedCapture() { + return Optional.ofNullable(delayedCapture); + } + + @JsonProperty(MOTO_FIELD_NAME) + @Schema(description = "You can use this parameter to " + + "[designate a payment as a Mail Order / Telephone Order (MOTO) payment]" + + "(https://docs.payments.service.gov.uk/moto_payments).", example = "false") + public Optional getMoto() { + return Optional.ofNullable(moto); + } + + @JsonProperty("metadata") + @Schema(description = "Additional metadata - up to 10 name/value pairs - on the payment. " + + "Each key must be between 1 and 30 characters long. " + + "The value, if a string, must be no greater than 50 characters long. " + + "Other permissible value types: boolean, number.", + example = "{\"ledger_code\":\"123\", \"reconciled\": true}") + public Optional getMetadata() { + return Optional.ofNullable(metadata); + } + + @JsonProperty("internal") + @Schema(hidden = true) + public Optional getInternal() { + return Optional.ofNullable(internal); + } + + @JsonProperty("set_up_agreement") + @Schema(description = "Use this parameter to set up an existing agreement for recurring payments. " + + "The `set_up_agreement` value you send must be a valid `agreement_id`.", + required = false, example = "abcefghjklmnopqr1234567890") + public Optional getSetUpAgreement() { + return Optional.ofNullable(setUpAgreement); + } + + @JsonProperty("agreement_id") + @Schema(description = "The unique ID GOV.UK Pay automatically associated with a recurring payments agreement. " + + "Including `agreement_id` in your request tells the API to take this payment using the card details that are associated with this agreement. " + + "`agreement_id` must match an active agreement ID. " + + "You must set `authorisation_mode` to `agreement` for the API to accept `agreement_id`.", + required = false, example = "abcefghjklmnopqr1234567890") + public Optional getAgreementId() { + return Optional.ofNullable(agreementId); + } + + @JsonProperty("authorisation_mode") + @Schema(description = "Sets how you intend to authorise the payment. Defaults to `web`. " + + "Payments created with `web` mode follow the [standard GOV.UK Pay payment journey](https://docs.payments.service.gov.uk/payment_flow/). " + + "Paying users visit the `next_url` in the response to complete their payment. " + + "Payments created with `agreement` mode are authorised with an agreement for recurring payments. " + + "If you create an `agreement` payment, you must also send an active `agreement_id`. " + + "You must not send `return_url`, `email`, or `prefilled_cardholder_details` or your request will fail. " + + "Payments created with `moto_api` mode return an `auth_url_post` object and a `one_time_token`. " + + "You can use `auth_url_post` and `one_time_token` to send the paying user’s card details through the API and complete the payment. " + + "If you create a `moto_api` payment, do not send a `return_url` in your request.", + type = "String", allowableValues = {"web", "agreement", "moto_api"}) + public Optional getAuthorisationMode() { + return Optional.ofNullable(authorisationMode); + } + + public String toConnectorPayload() { + JsonStringBuilder request = new JsonStringBuilder() + .add("amount", this.getAmount()) + .add("reference", this.getReference()) + .add("description", this.getDescription()) + .add("return_url", this.getReturnUrl()); + getLanguage().ifPresent(language -> request.add("language", language.toString())); + getDelayedCapture().ifPresent(delayedCapture -> request.add("delayed_capture", delayedCapture)); + getMoto().ifPresent(moto -> request.add("moto", moto)); + getMetadata().ifPresent(metadata -> request.add("metadata", metadata.getMetadata())); + getEmail().ifPresent(email -> request.add("email", email)); + getInternal().flatMap(Internal::getSource).ifPresent(source -> request.add("source", source)); + getAuthorisationMode().ifPresent(authorisationMode -> request.add("authorisation_mode", authorisationMode.getName())); + getAgreementId().ifPresent(agreementId -> request.add("agreement_id", agreementId)); + getSetUpAgreement().ifPresent(setUpAgreement -> { + request.add("agreement_id", setUpAgreement); + request.add("save_payment_instrument_to_agreement", true); + }); + + getPrefilledCardholderDetails().ifPresent(prefilledDetails -> { + prefilledDetails.getCardholderName().ifPresent(name -> request.addToMap(PREFILLED_CARDHOLDER_DETAILS, "cardholder_name", name)); + prefilledDetails.getBillingAddress().ifPresent(address -> { + request.addToNestedMap("line1", address.getLine1(), PREFILLED_CARDHOLDER_DETAILS, BILLING_ADDRESS); + request.addToNestedMap("line2", address.getLine2(), PREFILLED_CARDHOLDER_DETAILS, BILLING_ADDRESS); + request.addToNestedMap("postcode", address.getPostcode(), PREFILLED_CARDHOLDER_DETAILS, BILLING_ADDRESS); + request.addToNestedMap("city", address.getCity(), PREFILLED_CARDHOLDER_DETAILS, BILLING_ADDRESS); + request.addToNestedMap("country", address.getCountry(), PREFILLED_CARDHOLDER_DETAILS, BILLING_ADDRESS); + }); + }); + + return request.build(); + } + + /** + * This looks JSONesque but is not identical to the received request + */ + @Override + public String toString() { + // Don't include: + // description - some services include PII + // reference - can come from user input for payment links, in the past they have mistakenly entered card numbers + StringJoiner joiner = new StringJoiner(", ", "{", "}"); + joiner.add("amount: " + getAmount()); + joiner.add("return_url: " + returnUrl); + getInternal().flatMap(Internal::getSource).ifPresent(source -> joiner.add("source: " + source)); + getLanguage().ifPresent(value -> joiner.add("language: " + value)); + getDelayedCapture().ifPresent(value -> joiner.add("delayed_capture: " + value)); + getMoto().ifPresent(value -> joiner.add("moto: " + value)); + getMetadata().ifPresent(value -> joiner.add("metadata: " + value)); + getAuthorisationMode().ifPresent(authorisationMode -> joiner.add("authorisation_mode: " + authorisationMode)); + getSetUpAgreement().ifPresent(setUpAgreement -> joiner.add("set_up_agreement: " + setUpAgreement)); + getAgreementId().ifPresent(agreementId -> joiner.add("agreement_id: " + agreementId)); + + return joiner.toString(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreateCardPaymentRequestBuilder.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreateCardPaymentRequestBuilder.java new file mode 100644 index 000000000..5aafab9ed --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreateCardPaymentRequestBuilder.java @@ -0,0 +1,229 @@ +package uk.gov.pay.api.model; + +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.Source; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.model.charge.ExternalMetadata; + +public class CreateCardPaymentRequestBuilder { + private ExternalMetadata metadata; + private int amount; + private String returnUrl; + private String reference; + private String description; + private SupportedLanguage language; + private Boolean delayedCapture; + private Boolean moto; + private String email; + private String cardholderName; + private String addressLine1; + private String addressLine2; + private String city; + private String postcode; + private String country; + private PrefilledCardholderDetails prefilledCardholderDetails; + private Source source; + private Internal internal; + private String setUpAgreement; + private AuthorisationMode authorisationMode; + private String agreementId; + + public static CreateCardPaymentRequestBuilder builder() { + return new CreateCardPaymentRequestBuilder(); + } + + public CreateCardPaymentRequest build() { + return new CreateCardPaymentRequest(this); + } + + public CreateCardPaymentRequestBuilder amount(int amount) { + this.amount = amount; + return this; + } + + public CreateCardPaymentRequestBuilder returnUrl(String returnUrl) { + this.returnUrl = returnUrl; + return this; + } + + public CreateCardPaymentRequestBuilder reference(String reference) { + this.reference = reference; + return this; + } + + public CreateCardPaymentRequestBuilder description(String description) { + this.description = description; + return this; + } + + public CreateCardPaymentRequestBuilder language(SupportedLanguage language) { + this.language = language; + return this; + } + + public CreateCardPaymentRequestBuilder delayedCapture(Boolean delayedCapture) { + this.delayedCapture = delayedCapture; + return this; + } + + public CreateCardPaymentRequestBuilder moto(Boolean moto) { + this.moto = moto; + return this; + } + + public CreateCardPaymentRequestBuilder metadata(ExternalMetadata metadata) { + this.metadata = metadata; + return this; + } + + public CreateCardPaymentRequestBuilder email(String email) { + this.email = email; + return this; + } + + public CreateCardPaymentRequestBuilder cardholderName(String cardHolderName) { + this.cardholderName = cardHolderName; + return this; + } + + public CreateCardPaymentRequestBuilder addressLine1(String addressLine1) { + this.addressLine1 = addressLine1; + return this; + } + + public CreateCardPaymentRequestBuilder addressLine2(String addressLine2) { + this.addressLine2 = addressLine2; + return this; + } + + public CreateCardPaymentRequestBuilder city(String city) { + this.city = city; + return this; + } + + public CreateCardPaymentRequestBuilder postcode(String postcode) { + this.postcode = postcode; + return this; + } + + public CreateCardPaymentRequestBuilder country(String country) { + this.country = country; + return this; + } + + public CreateCardPaymentRequestBuilder source(Source source) { + this.source = source; + return this; + } + + public CreateCardPaymentRequestBuilder authorisationMode(AuthorisationMode authorisationMode) { + this.authorisationMode = authorisationMode; + return this; + } + + public CreateCardPaymentRequestBuilder agreementId(String agreementId) { + this.agreementId = agreementId; + return this; + } + + public PrefilledCardholderDetails getPrefilledCardholderDetails() { + if (cardholderName != null) { + this.prefilledCardholderDetails = new PrefilledCardholderDetails(); + this.prefilledCardholderDetails.setCardholderName(cardholderName); + } + if (addressLine1 != null || addressLine2 != null || + postcode != null || city != null || country != null) { + if (this.prefilledCardholderDetails == null) { + this.prefilledCardholderDetails = new PrefilledCardholderDetails(); + } + this.prefilledCardholderDetails.setAddress(addressLine1, addressLine2, postcode, city, country); + } + return prefilledCardholderDetails; + } + + public ExternalMetadata getMetadata() { + return metadata; + } + + public Boolean isMoto() { + return moto; + } + + public int getAmount() { + return amount; + } + + public String getReturnUrl() { + return returnUrl; + } + + public String getReference() { + return reference; + } + + public String getDescription() { + return description; + } + + public SupportedLanguage getLanguage() { + return language; + } + + public Boolean getDelayedCapture() { + return delayedCapture; + } + + public String getEmail() { + return email; + } + + public String getCardholderName() { + return cardholderName; + } + + public String getAddressLine1() { + return addressLine1; + } + + public String getAddressLine2() { + return addressLine2; + } + + public String getCity() { + return city; + } + + public String getPostcode() { + return postcode; + } + + public String getCountry() { + return country; + } + + public Internal getInternal() { + if (source != null) { + this.internal = new Internal(); + this.internal.setSource(source); + } + + return internal; + } + + public void setUpAgreement(String setUpAgreement) { + this.setUpAgreement = setUpAgreement; + + } + + public AuthorisationMode getAuthorisationMode() { + return authorisationMode; + } + + public String getSetUpAgreement() { + return setUpAgreement; + } + + public String getAgreementId() { + return agreementId; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreatePaymentRefundRequest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreatePaymentRefundRequest.java new file mode 100644 index 000000000..d71d2fb6c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreatePaymentRefundRequest.java @@ -0,0 +1,51 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.Optional; + +@Schema(name = "PaymentRefundRequest", description = "The Payment Refund Request Payload") +public class CreatePaymentRefundRequest { + + public static final String REFUND_AMOUNT_AVAILABLE = "refund_amount_available"; + public static final int REFUND_MIN_VALUE = 1; + + @Schema(description = "The amount you want to [refund to your user]" + + "(https://docs.payments.service.gov.uk/refunding_payments/) in pence.", required = true, + example = "150000", minimum = "1", maximum = "10000000") + private int amount; + @JsonProperty("refund_amount_available") + @Schema(description = "Amount in pence. Total amount still available before issuing the refund", required = false, + example = "200000", minimum = "1", maximum = "10000000") + private Integer refundAmountAvailable; + + public CreatePaymentRefundRequest() { + } + + public CreatePaymentRefundRequest(int amount, Integer refundAmountAvailable) { + this.amount = amount; + this.refundAmountAvailable = refundAmountAvailable; + } + + public int getAmount() { + return amount; + } + + /** + * This field should be made compulsory at a later stage + * + * @return + */ + public Optional getRefundAmountAvailable() { + return Optional.ofNullable(refundAmountAvailable); + } + + @Override + public String toString() { + return "CreatePaymentRefundRequest{" + + "amount=" + amount + + ", refundAmountAvailable=" + refundAmountAvailable + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreatePaymentResult.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreatePaymentResult.java new file mode 100644 index 000000000..0e7d1bfc2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreatePaymentResult.java @@ -0,0 +1,97 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.links.PaymentLinks; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.model.charge.ExternalMetadata; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static uk.gov.pay.api.model.CardPayment.LINKS_JSON_ATTRIBUTE; + +/** + * Defines swagger specs for create payment + */ +public class CreatePaymentResult { + + @JsonProperty + @Schema(name = "amount", description = "The amount, in pence, the user has paid or will pay. " + + "`amount` will match the value you sent in the request body.", example = "1200") + private long amount; + + @JsonProperty + private PaymentState state; + + @JsonProperty + @Schema(description = "The description you sent in the request body when creating this payment.", example = "New passport application") + private String description; + + @JsonProperty + @Schema(description = "The reference number you associated with this payment.", example = "12345") + private String reference; + + @JsonProperty + @Schema(name = "language", description = "The language of the user’s payment page.", example = "en") + private SupportedLanguage language; + + @JsonProperty + @Schema(name = "payment_id", description = "The unique ID GOV.UK Pay automatically associated " + + "with this payment when you created it.", example = "hu20sqlact5260q2nanm0q8u93") + private String paymentId; + + @JsonProperty + @Schema(name = "payment_provider", example = "worldpay") + private String paymentProvider; + + @JsonProperty + @Schema(name = "return_url", description = "The URL you direct the paying user to " + + "after their payment journey on GOV.UK Pay ends.", + example = "https://service-name.gov.uk/transactions/12345") + private String returnUrl; + + @JsonProperty + @Schema(name = "created_date", description = "The date you created the payment.", example = "2016-01-21T17:15:00.000Z") + private String createdDate; + + @JsonProperty + @Schema(name = "delayed_capture", description = "`delayed_capture` is `true` if you’re controlling " + + "[when GOV.UK Pay takes (‘captures’) the payment from the paying user’s bank account]" + + "(https://docs.payments.service.gov.uk/delayed_capture).", example = "false", accessMode = READ_ONLY) + private boolean delayedCapture; + + @JsonProperty + @Schema(description = "Indicates if this payment is a " + + "[Mail Order / Telephone Order (MOTO) payment]" + + "(https://docs.payments.service.gov.uk/moto_payments).", example = "false") + private boolean moto; + + @JsonProperty("refund_summary") + private RefundSummary refundSummary; + + @JsonProperty("settlement_summary") + private PaymentSettlementSummary settlementSummary; + + @JsonProperty + @Schema(name = LINKS_JSON_ATTRIBUTE, description = "API endpoints related to the payment.") + private PaymentLinks links; + + @JsonProperty + @Schema(name = "provider_id", description = "The reference number your " + + "payment service provider associated with the payment.", example = "null") + private String providerId; + + @JsonProperty + @Schema(name = "metadata", description = "[Custom metadata](https://docs.payments.service.gov.uk/optional_features/custom_metadata/) you added to the payment.") + private ExternalMetadata metadata; + + @JsonProperty + @Schema(name = "email", description = "The paying user’s email address. " + + "The paying user’s email field will be prefilled with this value when they make their payment. " + + "`email` does not appear if you did not include it in the request body.", + example = "citizen@example.org") + private String email; + + @JsonProperty(value = "card_details") + @Schema(name = "card_details") + private CardDetailsFromResponse cardDetails; +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreatedPaymentWithAllLinks.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreatedPaymentWithAllLinks.java new file mode 100644 index 000000000..519c248ba --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/CreatedPaymentWithAllLinks.java @@ -0,0 +1,31 @@ +package uk.gov.pay.api.model; + +import uk.gov.pay.api.model.links.PaymentWithAllLinks; + +public class CreatedPaymentWithAllLinks { + + public enum WhenCreated { + BRAND_NEW, + EXISTING + } + + private final PaymentWithAllLinks payment; + private final WhenCreated whenCreated; + + private CreatedPaymentWithAllLinks(PaymentWithAllLinks payment, WhenCreated whenCreated) { + this.payment = payment; + this.whenCreated = whenCreated; + } + + public static CreatedPaymentWithAllLinks of(PaymentWithAllLinks payment, WhenCreated whenCreated) { + return new CreatedPaymentWithAllLinks(payment, whenCreated); + } + + public PaymentWithAllLinks getPayment() { + return payment; + } + + public WhenCreated getWhenCreated() { + return whenCreated; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/Internal.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/Internal.java new file mode 100644 index 000000000..73c1fb75f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/Internal.java @@ -0,0 +1,22 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import uk.gov.service.payments.commons.model.Source; + +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Internal { + + @JsonProperty("source") + private Source source; + + public Optional getSource() { + return Optional.ofNullable(source); + } + + public void setSource(Source source) { + this.source = source; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentConnectorResponseLink.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentConnectorResponseLink.java new file mode 100644 index 000000000..c9d2dcd0d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentConnectorResponseLink.java @@ -0,0 +1,69 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Map; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PaymentConnectorResponseLink { + + private String rel; + private String href; + private String method; + private String type; + private Map params; + + // required for Jackson + private PaymentConnectorResponseLink() { + } + + public PaymentConnectorResponseLink(String rel, + String href, + String method, + String type, + Map params) { + this.rel = rel; + this.href = href; + this.method = method; + this.type = type; + this.params = params; + } + + public String getRel() { + return rel; + } + + public String getHref() { + return href; + } + + public String getMethod() { + return method; + } + + public String getType() { + return type; + } + + public Map getParams() { + return params; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PaymentConnectorResponseLink that = (PaymentConnectorResponseLink) o; + return Objects.equals(rel, that.rel) && + Objects.equals(href, that.href) && + Objects.equals(method, that.method) && + Objects.equals(type, that.type) && + Objects.equals(params, that.params); + } + + @Override + public int hashCode() { + return Objects.hash(rel, href, method, type, params); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentEvent.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentEvent.java new file mode 100644 index 000000000..4b54791b6 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentEvent.java @@ -0,0 +1,32 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PaymentEvent { + + @JsonProperty("state") + private PaymentState state; + + @JsonProperty("updated") + private String updated; + + public PaymentEvent() {} + + public PaymentState getState() { + return state; + } + + public String getUpdated() { + return updated; + } + + @Override + public String toString() { + return "PaymentEvent{" + + "state='" + state + '\'' + + ", updated=" + updated + + "}"; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentEventResponse.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentEventResponse.java new file mode 100644 index 000000000..553b25b41 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentEventResponse.java @@ -0,0 +1,72 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.links.PaymentEventLink; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +@Schema(name = "PaymentEvent", description = "A List of Payment Events information") +@JsonIgnoreProperties(ignoreUnknown = true) +public class PaymentEventResponse { + @JsonProperty("payment_id") + private String paymentId; + + @JsonProperty("state") + private PaymentState state; + + @JsonProperty("updated") + private String updated; + + @JsonProperty("_links") + private PaymentEventLink paymentLink; + + public static PaymentEventResponse from(PaymentEvent paymentEvent, String paymentId, String paymentLink) { + return new PaymentEventResponse(paymentId, paymentEvent.getState(), paymentEvent.getUpdated(), paymentLink); + } + + public static PaymentEventResponse from(TransactionEvent event, String paymentId, String paymentLink) { + return new PaymentEventResponse(paymentId, event.getState(), event.getTimestamp(), paymentLink); + } + + private PaymentEventResponse(String paymentId, PaymentState state, String updated, String paymentLink) { + this.paymentId = paymentId; + this.state = state; + this.updated = updated; + this.paymentLink = new PaymentEventLink(paymentLink); + } + + @Schema(example = "hu20sqlact5260q2nanm0q8u93", + description = "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + accessMode = READ_ONLY) + public String getPaymentId() { + return paymentId; + } + + @Schema(description = "state") + public PaymentState getState() { + return state; + } + + @Schema(description = "When this payment’s state changed. " + + "This value uses Coordinated Universal Time (UTC) and ISO-8601 format - `YYYY-MM-DDThh:mm:ss.SSSZ`.", + example = "2017-01-10T16:44:48.646Z", accessMode = READ_ONLY) + public String getUpdated() { + return updated; + } + + public PaymentEventLink getPaymentLink() { + return paymentLink; + } + + @Override + public String toString() { + return "PaymentEvent{" + + "paymentId='" + paymentId + '\'' + + ", state='" + state + '\'' + + ", updated=" + updated + + ", paymentLink=" + paymentLink + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentEvents.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentEvents.java new file mode 100644 index 000000000..c77382134 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentEvents.java @@ -0,0 +1,32 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PaymentEvents { + @JsonProperty("charge_id") + private String chargeId; + + private List events; + + public PaymentEvents() {} + + public String getChargeId() { + return chargeId; + } + + public List getEvents() { + return events; + } + + @Override + public String toString() { + return "PaymentEvents{" + + "chargeId='" + chargeId + '\'' + + ", events=" + events + + "}"; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentEventsResponse.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentEventsResponse.java new file mode 100644 index 000000000..9ba89d870 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentEventsResponse.java @@ -0,0 +1,70 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.links.PaymentLinksForEvents; + +import java.net.URI; +import java.util.List; +import java.util.stream.Collectors; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +@Schema(name = "PaymentEvents", description = "A List of Payment Events information") +public class PaymentEventsResponse { + @JsonProperty("payment_id") + private final String paymentId; + + private final List events; + + @JsonProperty("_links") + private PaymentLinksForEvents links; + + private PaymentEventsResponse(String paymentId, List events, PaymentLinksForEvents links) { + this.paymentId = paymentId; + this.events = events; + this.links = links; + } + + public static PaymentEventsResponse from(PaymentEvents paymentEvents, URI paymentEventsLink, URI eventsLink) { + List events = paymentEvents.getEvents().stream() + .map(paymentEvent -> PaymentEventResponse.from(paymentEvent, paymentEvents.getChargeId(), paymentEventsLink.toString())) + .collect(Collectors.toList()); + PaymentLinksForEvents paymentLinksForEvents = new PaymentLinksForEvents(); + paymentLinksForEvents.addSelf(eventsLink.toString()); + return new PaymentEventsResponse(paymentEvents.getChargeId(), events, paymentLinksForEvents); + } + + public static PaymentEventsResponse from(TransactionEvents transactionEvents, URI paymentEventsLink, URI eventsLink) { + List events = transactionEvents.getEvents().stream() + .map(paymentEvent -> PaymentEventResponse.from(paymentEvent, transactionEvents.getTransactionId(), paymentEventsLink.toString())) + .collect(Collectors.toList()); + PaymentLinksForEvents paymentLinksForEvents = new PaymentLinksForEvents(); + paymentLinksForEvents.addSelf(eventsLink.toString()); + return new PaymentEventsResponse(transactionEvents.getTransactionId(), events, paymentLinksForEvents); + } + + @Schema(example = "hu20sqlact5260q2nanm0q8u93", + description = "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + accessMode = READ_ONLY) + public String getPaymentId() { + return paymentId; + } + + public List getEvents() { + return events; + } + + public PaymentLinksForEvents getLinks() { + return links; + } + + @Override + public String toString() { + return "PaymentEvents{" + + "paymentId='" + paymentId + '\'' + + ", events=" + events + + ", links=" + links + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentSettlementSummary.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentSettlementSummary.java new file mode 100644 index 000000000..f74fe65be --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentSettlementSummary.java @@ -0,0 +1,52 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@Schema(name = "PaymentSettlementSummary", description = "A structure representing information about a settlement") +public class PaymentSettlementSummary { + + @JsonProperty("capture_submit_time") + private String captureSubmitTime; + + @JsonProperty("captured_date") + private String capturedDate; + + @JsonProperty("settled_date") + private String settledDate; + + public PaymentSettlementSummary() {} + + public PaymentSettlementSummary(String captureSubmitTime, String capturedDate, String settledDate) { + this.captureSubmitTime = captureSubmitTime; + this.capturedDate = capturedDate; + this.settledDate = settledDate; + } + + @Schema(description = "The date and time GOV.UK Pay asked your payment service provider " + + "to take the payment from your user’s account. " + + "This value uses Coordinated Universal Time (UTC) and ISO 8601 format - `YYYY-MM-DDThh:mm:ss.SSSZ`", + example = "2016-01-21T17:15:00.000Z", accessMode = READ_ONLY) + public String getCaptureSubmitTime() { + return captureSubmitTime; + } + + @Schema(description = "The date your payment service provider took the payment from your user. " + + "This value uses ISO 8601 format - `YYYY-MM-DD`", + example = "2016-01-21", accessMode = READ_ONLY) + public String getCapturedDate() { + return capturedDate; + } + + @Schema(description = "The date that the transaction was paid into the service's account.", example = "2016-01-21", + accessMode = READ_ONLY) + public String getSettledDate() { + return settledDate; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentState.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentState.java new file mode 100644 index 000000000..94ee25ad9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PaymentState.java @@ -0,0 +1,122 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.Objects; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@Schema(name = "PaymentState", description = "A structure representing the current state of the payment in its lifecycle.") +public class PaymentState { + @JsonProperty("status") + private String status; + + @JsonProperty("finished") + private boolean finished; + + @JsonProperty("message") + private String message; + + @JsonProperty("code") + private String code; + + @JsonProperty("can_retry") + private Boolean canRetry; + + + public static PaymentState createPaymentState(JsonNode node) { + return new PaymentState( + node.get("status").asText(), + node.get("finished").asBoolean(), + node.has("message") ? node.get("message").asText() : null, + node.has("code") ? node.get("code").asText() : null, + node.has("can_retry") ? node.get("can_retry").asBoolean() : null + ); + } + + public PaymentState() { + } + + public PaymentState(String status, boolean finished) { + this(status, finished, null, null); + } + + public PaymentState(String status, boolean finished, String message, String code) { + this(status, finished, message, code, null); + } + + public PaymentState(String status, boolean finished, String message, String code, Boolean canRetry) { + this.status = status; + this.finished = finished; + this.message = message; + this.code = code; + this.canRetry = canRetry; + } + + @Schema(description = "Where the payment is in [the payment status lifecycle]" + + "(https://docs.payments.service.gov.uk/api_reference/#payment-status-meanings).", + example = "created", accessMode = READ_ONLY) + public String getStatus() { + return status; + } + + @Schema(description = "Indicates whether a payment journey is finished.", accessMode = READ_ONLY) + public boolean isFinished() { + return finished; + } + + @Schema(description = "A description of what went wrong with this payment. `message` only appears if the payment failed.", + example = "User cancelled the payment", accessMode = READ_ONLY) + public String getMessage() { + return message; + } + + @Schema(description = "An [API error code](https://docs.payments.service.gov.uk/api_reference/#gov-uk-pay-api-error-codes)" + + "that explains why the payment failed. `code` only appears if the payment failed.", example = "P010", + accessMode = READ_ONLY) + public String getCode() { + return code; + } + + @Schema(description = "If `can_retry` is `true`, you can use this agreement to try to take another recurring payment. " + + "If `can_retry` is `false`, you cannot take another recurring payment with this agreement. " + + "`can_retry` only appears on failed payments that were attempted using an agreement for recurring payments.", + nullable = true, accessMode = READ_ONLY) + public Boolean getCanRetry() { + return canRetry; + } + + @Override + public String toString() { + return "PaymentState{" + + "status='" + status + '\'' + + ", finished='" + finished + '\'' + + ", message=" + message + + ", code=" + code + + (canRetry != null ? ", canRetry=" +canRetry : "") + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PaymentState that = (PaymentState) o; + return finished == that.finished && + Objects.equals(status, that.status) && + Objects.equals(message, that.message) && + Objects.equals(code, that.code) && + Objects.equals(canRetry, that.canRetry); + } + + @Override + public int hashCode() { + return Objects.hash(status, finished, message, code, canRetry); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PrefilledCardholderDetails.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PrefilledCardholderDetails.java new file mode 100644 index 000000000..47449dd5a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/PrefilledCardholderDetails.java @@ -0,0 +1,56 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.validation.Valid; +import javax.validation.constraints.Size; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PrefilledCardholderDetails { + + @JsonProperty("cardholder_name") + @Schema(name = "cardholder_name", + description = "The cardholder name you prefilled when you created this payment.", + example = "J. Bogs") + @Size(max = 255, message = "Must be less than or equal to {max} characters length") + private String cardholderName; + + @Schema(name = "billing_address", description = "prefilled billing address") + @JsonProperty("billing_address") + @Valid + private Address billingAddress; + + public Optional
getBillingAddress() { + return Optional.ofNullable(billingAddress); + } + + public Optional getCardholderName() { + return Optional.ofNullable(cardholderName); + } + + public void setCardholderName(String cardholderName) { + this.cardholderName = cardholderName; + } + + public void setAddress(String addressLine1, String addressLine2, String postcode, String city, String country) { + this.billingAddress = new Address(addressLine1, addressLine2, postcode, city, country); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PrefilledCardholderDetails that = (PrefilledCardholderDetails) o; + return Objects.equals(cardholderName, that.cardholderName) && + Objects.equals(billingAddress, that.billingAddress); + } + + @Override + public int hashCode() { + return Objects.hash(cardholderName, billingAddress); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundFromConnector.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundFromConnector.java new file mode 100644 index 000000000..874b6a2ab --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundFromConnector.java @@ -0,0 +1,43 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class RefundFromConnector { + + @JsonProperty(value = "refund_id") + private String refundId; + + @JsonProperty(value = "created_date") + private String createdDate; + + private Long amount; + private String status; + + public String getRefundId() { + return refundId; + } + + public Long getAmount() { + return amount; + } + + public String getStatus() { + return status; + } + + public String getCreatedDate() { + return createdDate; + } + + @Override + public String toString() { + return "RefundFromConnector{" + + "refundId='" + refundId + '\'' + + ", createdDate='" + createdDate + '\'' + + ", amount=" + amount + + ", status='" + status + '\'' + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundResponse.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundResponse.java new file mode 100644 index 000000000..3c1520341 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundResponse.java @@ -0,0 +1,125 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.links.RefundLinksForSearch; +import uk.gov.pay.api.model.ledger.RefundTransactionFromLedger; + +import javax.ws.rs.core.UriBuilder; +import java.net.URI; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Schema(name = "Refund") +public class RefundResponse { + + @Schema(example = "act4c33g40j3edfmi8jknab84x", + description = "The unique ID GOV.UK Pay automatically associated with this refund when you created it.", + accessMode = READ_ONLY) + private String refundId; + @Schema(example = "2017-01-10T16:52:07.855Z", + description = "The date and time you created this refund. " + + "This value uses Coordinated Universal Time (UTC) and ISO 8601 format - `YYYY-MM-DDThh:mm:ss.SSSZ`", + accessMode = READ_ONLY) + private String createdDate; + @Schema(example = "120", description = "The amount refunded to the user in pence.", accessMode = READ_ONLY) + private Long amount; + @JsonProperty("_links") + private RefundLinksForSearch links; + @Schema(example = "success", + description = "The [status of the refund](https://docs.payments.service.gov.uk/refunding_payments/#checking-the-status-of-a-refund-status).", + allowableValues = {"submitted", "success", "error"}, accessMode = READ_ONLY) + private String status; + @Schema(accessMode = READ_ONLY) + private RefundSettlementSummary settlementSummary; + + private RefundResponse(RefundFromConnector refund, URI selfLink, URI paymentLink) { + this.refundId = refund.getRefundId(); + this.amount = refund.getAmount(); + this.status = refund.getStatus(); + this.createdDate = refund.getCreatedDate(); + this.links = new RefundLinksForSearch(); + this.settlementSummary = new RefundSettlementSummary(); + + links.addSelf(selfLink.toString()); + links.addPayment(paymentLink.toString()); + } + + private RefundResponse(RefundTransactionFromLedger refund, URI selfLink, URI paymentLink) { + this.refundId = refund.getTransactionId(); + this.amount = refund.getAmount(); + this.status = refund.getState().getStatus(); + this.createdDate = refund.getCreatedDate(); + this.settlementSummary = refund.getSettlementSummary(); + this.links = new RefundLinksForSearch(); + + links.addSelf(selfLink.toString()); + links.addPayment(paymentLink.toString()); + } + + private RefundResponse(String refundId, Long amount, String status, + String createdDate, URI selfLink, URI paymentLink) { + this.refundId = refundId; + this.amount = amount; + this.status = status; + this.createdDate = createdDate; + this.links = new RefundLinksForSearch(); + + links.addSelf(selfLink.toString()); + links.addPayment(paymentLink.toString()); + } + + public static RefundResponse from(RefundFromConnector refund, URI selfLink, URI paymentLink) { + return new RefundResponse(refund, selfLink, paymentLink); + } + + public static RefundResponse from(RefundTransactionFromLedger refund, URI selfLink, URI paymentLink) { + return new RefundResponse(refund, selfLink, paymentLink); + } + + //todo: remove after full refactoring of PaymentRefundsResource (to use service layer) + public static RefundResponse valueOf(RefundFromConnector refundEntity, String paymentId, String baseUrl) { + URI selfLink = UriBuilder.fromUri(baseUrl) + .path("/v1/payments/{paymentId}/refunds/{refundId}") + .build(paymentId, refundEntity.getRefundId()); + + URI paymentLink = UriBuilder.fromUri(baseUrl) + .path("/v1/payments/{paymentId}") + .build(paymentId); + + return new RefundResponse( + refundEntity.getRefundId(), + refundEntity.getAmount(), + refundEntity.getStatus(), + refundEntity.getCreatedDate(), + selfLink, + paymentLink); + } + + public String getRefundId() { + return refundId; + } + + public Long getAmount() { + return amount; + } + + public String getStatus() { + return status; + } + + public String getCreatedDate() { + return createdDate; + } + + public RefundLinksForSearch getLinks() { + return links; + } + + public RefundSettlementSummary getSettlementSummary() { + return settlementSummary; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundSettlementSummary.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundSettlementSummary.java new file mode 100644 index 000000000..475636f39 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundSettlementSummary.java @@ -0,0 +1,32 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@Schema(name = "RefundSettlementSummary", description = "A structure representing information about a settlement for refunds") +public class RefundSettlementSummary { + + @JsonProperty("settled_date") + private String settledDate; + + public RefundSettlementSummary() {} + + public RefundSettlementSummary(String settledDate) { + this.settledDate = settledDate; + } + + @Schema(description = "The date Stripe took the refund from a payout to your bank account. " + + "`settled_date` only appears if Stripe has taken the refund. " + + "This value uses Coordinated Universal Time (UTC) and ISO 8601 format - `YYYY-MM-DD`.", + example = "2016-01-21", + accessMode = READ_ONLY) + public String getSettledDate() { + return settledDate; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundSummary.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundSummary.java new file mode 100644 index 000000000..4e34ef088 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundSummary.java @@ -0,0 +1,45 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +@Schema(name = "RefundSummary", description = "A structure representing the refunds availability") +@JsonIgnoreProperties(ignoreUnknown = true) +public class RefundSummary { + + private String status; + + @JsonProperty("amount_available") + private long amountAvailable; + + @JsonProperty("amount_submitted") + private long amountSubmitted; + + public RefundSummary() {} + + public RefundSummary(String status, long amountAvailable, long amountSubmitted) { + this.status = status; + this.amountAvailable = amountAvailable; + this.amountSubmitted = amountSubmitted; + } + + @Schema(description = "Whether you can [refund the payment]" + + "(https://docs.payments.service.gov.uk/refunding_payments/#checking-the-status-of-a-refund-status).", + example = "available") + public String getStatus() { + return status; + } + + @Schema(description = "How much you can refund to the user, in pence.", example = "100", accessMode = READ_ONLY) + public long getAmountAvailable() { + return amountAvailable; + } + + @Schema(description = "How much you’ve already refunded to the user, in pence.", accessMode = READ_ONLY) + public long getAmountSubmitted() { + return amountSubmitted; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundsFromConnector.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundsFromConnector.java new file mode 100644 index 000000000..63c56b474 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundsFromConnector.java @@ -0,0 +1,53 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class RefundsFromConnector { + + public class Embedded { + private List refunds; + + public Embedded() { + } + + public List getRefunds() { + return refunds; + } + + @Override + public String toString() { + return "Embedded{" + + "refunds=" + refunds + + '}'; + } + } + + @JsonProperty(value = "payment_id") + private String paymentId; + + @JsonProperty(value = "_embedded") + private Embedded embedded; + + public RefundsFromConnector() { + } + + public String getPaymentId() { + return paymentId; + } + + public Embedded getEmbedded() { + return embedded; + } + + @Override + public String toString() { + return "RefundsFromConnector{" + + "paymentId='" + paymentId + '\'' + + ", " + embedded + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundsResponse.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundsResponse.java new file mode 100644 index 000000000..7fc863f28 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RefundsResponse.java @@ -0,0 +1,69 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.links.RefundLinksForSearch; + +import java.util.List; + +public class RefundsResponse { + + @JsonProperty("payment_id") + @Schema(example = "hu20sqlact5260q2nanm0q8u93", description = "The unique ID GOV.UK Pay associated with this payment when you created it.") + private String paymentId; + @JsonProperty("_links") + private RefundLinksForSearch links; + @JsonProperty("_embedded") + @Schema(name = "_embedded") + private EmbeddedRefunds embedded; + + private RefundsResponse(String paymentId, List refundsForPayment, String selfLink, String paymentLink) { + this.paymentId = paymentId; + + embedded = new EmbeddedRefunds(); + embedded.refunds = refundsForPayment; + + this.links = new RefundLinksForSearch(); + this.links.addPayment(paymentLink); + this.links.addSelf(selfLink); + } + + public static RefundsResponse from(String paymentId, + List refundsForPayment, + String selfLink, + String paymentLink) { + return new RefundsResponse(paymentId, refundsForPayment, selfLink, paymentLink); + } + + public String getPaymentId() { + return paymentId; + } + + public RefundLinksForSearch getLinks() { + return links; + } + + @Schema(hidden = true) + public EmbeddedRefunds getEmbedded() { + return embedded; + } + + @Schema(hidden = true) + public class EmbeddedRefunds { + private List refunds; + + public EmbeddedRefunds() { + } + + public List getRefunds() { + return refunds; + } + + @Override + public String toString() { + return "Embedded{" + + "refunds=" + refunds + + '}'; + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RequestError.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RequestError.java new file mode 100644 index 000000000..510f48ba5 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/RequestError.java @@ -0,0 +1,185 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; +import static com.google.common.collect.ObjectArrays.concat; +import static java.lang.String.format; + +@Schema(name = "RequestError", description = "A Request Error response") +@JsonInclude(NON_NULL) +public class RequestError { + + public enum Code { + + CREATE_PAYMENT_ACCOUNT_ERROR("P0199", "There is an error with this account. Contact support with your error code - https://www.payments.service.gov.uk/support/ ."), + CREATE_PAYMENT_CONNECTOR_ERROR("P0198", "Downstream system error"), + CREATE_PAYMENT_PARSING_ERROR("P0197", "Unable to parse JSON"), + CREATE_PAYMENT_MOTO_NOT_ENABLED("P0196", "MOTO payments are not enabled for this account. Please contact support if you would like to process MOTO payments - https://www.payments.service.gov.uk/support/ ."), + CREATE_PAYMENT_AUTHORISATION_API_NOT_ENABLED("P0195","Using authorisation_mode of moto_api is not allowed for this account"), + CREATE_PAYMENT_AGREEMENT_ID_ERROR("P0103", "Invalid attribute value: set_up_agreement. Agreement ID does not exist"), + CREATE_PAYMENT_CARD_NUMBER_IN_PAYMENT_LINK_REFERENCE_ERROR("P0105", "%s"), + + GENERIC_MISSING_FIELD_ERROR_MESSAGE_FROM_CONNECTOR("P0101", "%s"), + GENERIC_VALIDATION_EXCEPTION_MESSAGE_FROM_CONNECTOR("P0102", "%s"), + GENERIC_UNEXPECTED_FIELD_ERROR_MESSAGE_FROM_CONNECTOR("P0104", "%s"), + + CREATE_PAYMENT_MISSING_FIELD_ERROR("P0101", "Missing mandatory attribute: %s"), + CREATE_PAYMENT_UNEXPECTED_FIELD_ERROR("P0104", "Unexpected attribute: %s"), + CREATE_PAYMENT_VALIDATION_ERROR("P0102", "Invalid attribute value: %s. %s"), + CREATE_PAYMENT_HEADER_VALIDATION_ERROR("P0102", "%s"), + CREATE_PAYMENT_IDEMPOTENCY_KEY_ALREADY_USED("P0191", "The `Idempotency-Key` you sent in the request header has already been used to create a payment."), + + GET_PAYMENT_NOT_FOUND_ERROR("P0200", "Not found"), + GET_PAYMENT_CONNECTOR_ERROR("P0298", "Downstream system error"), + + GET_PAYMENT_EVENTS_NOT_FOUND_ERROR("P0300", "Not found"), + GET_PAYMENT_EVENTS_CONNECTOR_ERROR("P0398", "Downstream system error"), + + SEARCH_PAYMENTS_VALIDATION_ERROR("P0401", "Invalid parameters: %s. See Public API documentation for the correct data formats"), + SEARCH_PAYMENTS_NOT_FOUND("P0402", "Page not found"), + SEARCH_PAYMENTS_CONNECTOR_ERROR("P0498", "Downstream system error"), + + SEARCH_AGREEMENTS_VALIDATION_ERROR("P2401", "Invalid parameters: %s. See Public API documentation for the correct data formats"), + SEARCH_AGREEMENTS_NOT_FOUND("P2402", "Page not found"), + SEARCH_AGREEMENTS_LEDGER_ERROR("P2498", "Downstream system error"), + + CANCEL_PAYMENT_NOT_FOUND_ERROR("P0500", "Not found"), + CANCEL_PAYMENT_CONNECTOR_BAD_REQUEST_ERROR("P0501", "Cancellation of payment failed"), + CANCEL_PAYMENT_CONNECTOR_CONFLICT_ERROR("P0502", "Cancellation of payment failed"), + CANCEL_PAYMENT_CONNECTOR_ERROR("P0598", "Downstream system error"), + + CAPTURE_PAYMENT_NOT_FOUND_ERROR("P1000", "Not found"), + CAPTURE_PAYMENT_CONNECTOR_BAD_REQUEST_ERROR("P1001", "Capture of payment failed"), + CAPTURE_PAYMENT_CONNECTOR_CONFLICT_ERROR("P1003", "Payment cannot be captured"), + CAPTURE_PAYMENT_CONNECTOR_ERROR("P1098", "Downstream system error"), + + CREATE_PAYMENT_REFUND_CONNECTOR_ERROR("P0698", "Downstream system error"), + CREATE_PAYMENT_REFUND_PARSING_ERROR("P0697", "Unable to parse JSON"), + CREATE_PAYMENT_REFUND_NOT_FOUND_ERROR("P0600", "Not found"), + CREATE_PAYMENT_REFUND_MISSING_FIELD_ERROR("P0601", "Missing mandatory attribute: %s"), + CREATE_PAYMENT_REFUND_VALIDATION_ERROR("P0602", "Invalid attribute value: %s. %s"), + CREATE_PAYMENT_REFUND_NOT_AVAILABLE("P0603", "The payment is not available for refund. Payment refund status: %s"), + CREATE_PAYMENT_REFUND_NOT_AVAILABLE_DUE_TO_DISPUTE("P0603", "The payment is disputed and cannot be refunded"), + CREATE_PAYMENT_REFUND_AMOUNT_AVAILABLE_MISMATCH("P0604", "Refund amount available mismatch."), + + GET_PAYMENT_REFUND_NOT_FOUND_ERROR("P0700", "Not found"), + GET_PAYMENT_REFUND_CONNECTOR_ERROR("P0798", "Downstream system error"), + + GET_PAYMENT_REFUNDS_NOT_FOUND_ERROR("P0800", "Not found"), + GET_PAYMENT_REFUNDS_CONNECTOR_ERROR("P0898", "Downstream system error"), + + TOO_MANY_REQUESTS_ERROR("P0900", "Too many requests"), + REQUEST_DENIED_ERROR("P0920", "Request blocked by security rules. Please consult API documentation for more information."), + RESOURCE_ACCESS_FORBIDDEN("P0930", "Access to this resource is not enabled for this account. Contact support with your error code - https://www.payments.service.gov.uk/support/ ."), + ACCOUNT_NOT_LINKED_WITH_PSP("P0940", "Account is not fully configured. Please refer to documentation to setup your account or contact support with your error code - https://www.payments.service.gov.uk/support/ ."), + ACCOUNT_DISABLED("P0941", "GOV.UK Pay has disabled payment and refund creation on this account. Contact support with your error code - https://www.payments.service.gov.uk/support/ ."), + RECURRING_CARD_PAYMENTS_NOT_ALLOWED_ERROR("P0942", "Recurring card payments are currently disabled for this service. Contact support with your error code - https://www.payments.service.gov.uk/support/"), + + SEARCH_REFUNDS_VALIDATION_ERROR("P1101", "Invalid parameters: %s. See Public API documentation for the correct data formats"), + SEARCH_REFUNDS_NOT_FOUND("P1100", "Page not found"), + SEARCH_REFUNDS_CONNECTOR_ERROR("P1898", "Downstream system error"), + + AUTHORISATION_CARD_NUMBER_REJECTED_ERROR("P0010", "%s"), + AUTHORISATION_REJECTED_ERROR("P0010", "%s"), + AUTHORISATION_ERROR("P0050", "%s"), + AUTHORISATION_TIMEOUT_ERROR("P0050", "%s"), + AUTHORISATION_ONE_TIME_TOKEN_ALREADY_USED_ERROR("P1212", "%s"), + AUTHORISATION_ONE_TIME_TOKEN_INVALID_ERROR("P1211", "%s"), + + CREATE_AGREEMENT_CONNECTOR_ERROR("P2198", "Downstream system error"), + CREATE_AGREEMENT_PARSING_ERROR("P2197", "Unable to parse JSON"), + + CREATE_AGREEMENT_MISSING_FIELD_ERROR("P2101", "Missing mandatory attribute: %s"), + CREATE_AGREEMENT_VALIDATION_ERROR("P2102", "Invalid attribute value: %s. %s"), + GET_AGREEMENT_NOT_FOUND_ERROR("P2200", "Not found"), + GET_AGREEMENT_LEDGER_ERROR("P2298", "Downstream system error"), + + CANCEL_AGREEMENT_NOT_FOUND_ERROR("P2500", "Not found"), + CANCEL_AGREEMENT_CONNECTOR_BAD_REQUEST_ERROR("P2501", "Cancellation of agreement failed"), + CANCEL_AGREEMENT_CONNECTOR_ERROR("P2598", "Downstream system error"), + + SEARCH_DISPUTES_VALIDATION_ERROR("P0401", "Invalid parameters: %s. See Public API documentation for the correct data formats"), + GET_DISPUTE_LEDGER_ERROR("P0498", "Downstream system error"), + SEARCH_DISPUTES_NOT_FOUND("P0402", "Page not found"); + + private String value; + private String format; + + Code(String value, String format) { + this.value = value; + this.format = format; + } + + public String value() { + return value; + } + + public String getFormat() { + return format; + } + } + + private String field; + private String header; + private final Code code; + private final String description; + + public static RequestError aRequestError(Code code, Object... parameters) { + return new RequestError(code, parameters); + } + + public static RequestError aRequestError(String fieldName, Code code, Object... parameters) { + return new RequestError(null, fieldName, code, parameters); + } + + public static RequestError aHeaderRequestError(String header, Code code, Object... parameters) { + return new RequestError(header, null, code, parameters); + } + + private RequestError(Code code, Object... parameters) { + this.code = code; + this.description = format(code.getFormat(), parameters); + } + + private RequestError(String header, String fieldName, Code code, Object... parameters) { + this.header = header; + this.field = fieldName; + this.code = code; + this.description = format(code.getFormat(), fieldName != null ? concat(fieldName, parameters) : parameters); + } + + @Schema(example = "amount", description = "The parameter in your request that's causing the error.") + public String getField() { + return field; + } + + @Schema(example = "Idempotency-Key", description = "The header in your request that's causing the error.") + public String getHeader() { + return header; + } + + @Schema(example = "P0102", + description = "An [API error code](https://docs.payments.service.gov.uk/api_reference/#gov-uk-pay-api-error-codes)" + + "that explains why the payment failed.

`code` only appears if the payment failed.") + public String getCode() { + return code.value(); + } + + @Schema(example = "Invalid attribute value: amount. Must be less than or equal to 10000000", + description = "Additional details about the error.") + public String getDescription() { + return description; + } + + @Override + public String toString() { + return "RequestError{" + + "field=" + field + + ", code=" + code.value() + + ", name=" + code + + ", description='" + description + '\'' + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ThreeDSecure.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ThreeDSecure.java new file mode 100644 index 000000000..a14df8afb --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ThreeDSecure.java @@ -0,0 +1,27 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "ThreeDSecure", description = "Object containing information about the 3D Secure authentication of the payment.") +@JsonIgnoreProperties(ignoreUnknown = true) +public class ThreeDSecure { + + @JsonProperty("required") + @Schema(name = "required", description = "Indicates if this payment was authorised with 3D Secure authentication. " + + "`required` is `true` if the payment required 3D Secure authentication.") + private boolean required; + + public ThreeDSecure() { + } + + public ThreeDSecure(boolean required) { + this.required = required; + } + + public boolean isRequired() { + return required; + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/TokenPaymentType.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/TokenPaymentType.java new file mode 100644 index 000000000..c0b9f309c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/TokenPaymentType.java @@ -0,0 +1,16 @@ +package uk.gov.pay.api.model; + +//to be shared between Public Auth and Public Api +public enum TokenPaymentType { + CARD("Card Payment"); + + private String friendlyName; + + TokenPaymentType(String friendlyName) { + this.friendlyName = friendlyName; + } + + public String getFriendlyName() { + return this.friendlyName; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/TransactionEvent.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/TransactionEvent.java new file mode 100644 index 000000000..3b13b27ee --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/TransactionEvent.java @@ -0,0 +1,20 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class TransactionEvent { + + private PaymentState state; + private String timestamp; + + public TransactionEvent() {} + + public PaymentState getState() { + return state; + } + + public String getTimestamp() { + return timestamp; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/TransactionEvents.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/TransactionEvents.java new file mode 100644 index 000000000..8e39ad941 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/TransactionEvents.java @@ -0,0 +1,25 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class TransactionEvents { + + private String transactionId; + private List events; + + public TransactionEvents() {} + + public String getTransactionId() { + return transactionId; + } + + public List getEvents() { + return events; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/TransactionResponse.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/TransactionResponse.java new file mode 100644 index 000000000..db36fa48d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/TransactionResponse.java @@ -0,0 +1,202 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import uk.gov.pay.api.utils.CustomSupportedLanguageDeserializer; +import uk.gov.pay.api.utils.WalletDeserializer; +import uk.gov.service.payments.commons.api.json.ExternalMetadataDeserialiser; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.model.charge.ExternalMetadata; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class TransactionResponse { + + @JsonProperty("transaction_id") + private String transactionId; + + @JsonProperty("return_url") + private String returnUrl; + + @JsonProperty("payment_provider") + private String paymentProvider; + + @JsonProperty("links") + private List links = new ArrayList<>(); + + @JsonProperty(value = "refund_summary") + private RefundSummary refundSummary; + + @JsonProperty(value = "settlement_summary") + private PaymentSettlementSummary settlementSummary; + + @JsonProperty(value = "card_details") + private CardDetailsFromResponse cardDetails; + + private Long amount; + + private PaymentState state; + + private String description; + + private String reference; + + private String email; + + @JsonDeserialize(using = CustomSupportedLanguageDeserializer.class) + private SupportedLanguage language; + + @JsonProperty(value = "delayed_capture") + private boolean delayedCapture; + + private boolean moto; + + @JsonProperty("corporate_card_surcharge") + private Long corporateCardSurcharge; + + @JsonProperty("total_amount") + private Long totalAmount; + + @JsonProperty("fee") + private Long fee; + + @JsonProperty("net_amount") + private Long netAmount; + + @JsonProperty(value = "created_date") + private String createdDate; + + @JsonProperty(value = "gateway_transaction_id") + private String gatewayTransactionId; + + @JsonDeserialize(using = ExternalMetadataDeserialiser.class) + private ExternalMetadata metadata; + + @JsonProperty("authorisation_summary") + private AuthorisationSummary authorisationSummary; + + @JsonProperty("authorisation_mode") + private AuthorisationMode authorisationMode; + + @JsonProperty("wallet_type") + @JsonDeserialize(using = WalletDeserializer.class) + private Wallet walletType; + + public Optional getMetadata() { + return Optional.ofNullable(metadata); + } + + public Long getAmount() { + return amount; + } + + public PaymentState getState() { + return state; + } + + public String getReturnUrl() { + return returnUrl; + } + + public String getDescription() { + return description; + } + + public String getReference() { + return reference; + } + + public String getEmail() { + return email; + } + + public SupportedLanguage getLanguage() { + return language; + } + + public boolean getDelayedCapture() { + return delayedCapture; + } + + public boolean isMoto() { + return moto; + } + + public Long getCorporateCardSurcharge() { + return corporateCardSurcharge; + } + + public Long getTotalAmount() { + return totalAmount; + } + + public Long getFee() { + return fee; + } + + public Long getNetAmount() { + return netAmount; + } + + public String getPaymentProvider() { + return paymentProvider; + } + + /** + * card brand is no longer a top level charge property. It is now at `card_details.card_brand` attribute + * We still need to support `v1` clients with a top level card brand attribute to keep support their integrations. + * + * @return + */ + @JsonProperty("card_brand") + public String getCardBrand() { + return cardDetails != null ? cardDetails.getCardBrand() : ""; + } + + public String getCreatedDate() { + return createdDate; + } + + public List getLinks() { + return links; + } + + public RefundSummary getRefundSummary() { + return refundSummary; + } + + public PaymentSettlementSummary getSettlementSummary() { + return settlementSummary; + } + + public CardDetailsFromResponse getCardDetailsFromResponse() { + return cardDetails; + } + + public String getGatewayTransactionId() { + return gatewayTransactionId; + } + + public String getTransactionId() { + return transactionId; + } + + public AuthorisationSummary getAuthorisationSummary() { + return authorisationSummary; + } + + public AuthorisationMode getAuthorisationMode() { + return authorisationMode; + } + + public Optional getWalletType() { + return Optional.ofNullable(walletType); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/Wallet.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/Wallet.java new file mode 100644 index 000000000..c708be054 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/Wallet.java @@ -0,0 +1,17 @@ +package uk.gov.pay.api.model; + +public enum Wallet { + + APPLE_PAY("Apple Pay"), + GOOGLE_PAY("Google Pay"); + + private final String titleCase; + + Wallet(String titleCase) { + this.titleCase = titleCase; + } + + public String getTitleCase() { + return titleCase; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/DisputeSettlementSummary.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/DisputeSettlementSummary.java new file mode 100644 index 000000000..c0c3ecf7f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/DisputeSettlementSummary.java @@ -0,0 +1,33 @@ +package uk.gov.pay.api.model.ledger; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@Schema(name = "SettlementSummary", description = "Contains information about when a lost dispute was settled. A dispute is settled when your payment service provider takes it from a payout to your bank account. 'settlement_summary' only appears if you lost the dispute.") +public class DisputeSettlementSummary { + + @JsonProperty("settled_date") + private String settledDate; + + public DisputeSettlementSummary() {} + + public DisputeSettlementSummary(String settledDate) { + this.settledDate = settledDate; + } + + @Schema(description = "The date your payment service provider took the disputed payment " + + "and dispute fee from a payout to your bank account. " + + "This value appears in ISO 8601 format - `YYYY-MM-DD`. " + + "`settled_date` only appears if you lost the dispute.", + example = "2022-07-28", + accessMode = READ_ONLY) + public String getSettledDate() { + return settledDate; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/DisputeTransactionFromLedger.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/DisputeTransactionFromLedger.java new file mode 100644 index 000000000..eb502d612 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/DisputeTransactionFromLedger.java @@ -0,0 +1,60 @@ +package uk.gov.pay.api.model.ledger; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public class DisputeTransactionFromLedger { + private Long amount; + private String createdDate; + private String transactionId; + private String evidenceDueDate; + private Long fee; + private Long netAmount; + private String parentTransactionId; + private String reason; + private DisputeSettlementSummary settlementSummary; + private TransactionState state; + + public Long getAmount() { + return amount; + } + + public String getTransactionId() { + return transactionId; + } + + public TransactionState getState() { + return state; + } + + public Long getNetAmount() { + return netAmount; + } + + public Long getFee() { + return fee; + } + + public String getReason() { + return reason; + } + + public DisputeSettlementSummary getSettlementSummary() { + return settlementSummary; + } + + public String getParentTransactionId() { + return parentTransactionId; + } + + public String getCreatedDate() { + return createdDate; + } + + public String getEvidenceDueDate() { + return evidenceDueDate; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/RefundTransactionFromLedger.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/RefundTransactionFromLedger.java new file mode 100644 index 000000000..7b5178e57 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/RefundTransactionFromLedger.java @@ -0,0 +1,56 @@ +package uk.gov.pay.api.model.ledger; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import uk.gov.pay.api.model.RefundSettlementSummary; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public class RefundTransactionFromLedger { + + Long amount; + String description; + String reference; + String createdDate; + String refundedBy; + String transactionId; + String parentTransactionId; + TransactionState state; + RefundSettlementSummary settlementSummary; + public Long getAmount() { + return amount; + } + + public String getDescription() { + return description; + } + + public String getReference() { + return reference; + } + + public String getCreatedDate() { + return createdDate; + } + + public String getRefundedBy() { + return refundedBy; + } + + public String getTransactionId() { + return transactionId; + } + + public String getParentTransactionId() { + return parentTransactionId; + } + + public TransactionState getState() { + return state; + } + + public RefundSettlementSummary getSettlementSummary() { + return settlementSummary; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/RefundsFromLedger.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/RefundsFromLedger.java new file mode 100644 index 000000000..09bcfcc0f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/RefundsFromLedger.java @@ -0,0 +1,23 @@ +package uk.gov.pay.api.model.ledger; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import java.util.List; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public class RefundsFromLedger { + + String parentTransactionId; + List transactions; + + public String getParentTransactionId() { + return parentTransactionId; + } + + public List getTransactions() { + return transactions; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/SearchDisputesResponseFromLedger.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/SearchDisputesResponseFromLedger.java new file mode 100644 index 000000000..933e31ebb --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/SearchDisputesResponseFromLedger.java @@ -0,0 +1,49 @@ +package uk.gov.pay.api.model.ledger; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import uk.gov.pay.api.model.links.SearchNavigationLinks; +import uk.gov.pay.api.model.search.SearchPagination; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class SearchDisputesResponseFromLedger implements SearchPagination { + + @JsonProperty("total") + private int total; + + @JsonProperty("count") + private int count; + + @JsonProperty("page") + private int page; + + @JsonProperty("results") + private List disputes; + + @JsonProperty("_links") + private SearchNavigationLinks links = new SearchNavigationLinks(); + + public int getTotal() { + return total; + } + + public int getCount() { + return count; + } + + public int getPage() { + return page; + } + + public SearchNavigationLinks getLinks() { + return links; + } + + public List getDisputes() { + return disputes; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/SearchRefundsResponseFromLedger.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/SearchRefundsResponseFromLedger.java new file mode 100644 index 000000000..bf9b101af --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/SearchRefundsResponseFromLedger.java @@ -0,0 +1,49 @@ +package uk.gov.pay.api.model.ledger; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import uk.gov.pay.api.model.links.SearchNavigationLinks; +import uk.gov.pay.api.model.search.SearchPagination; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class SearchRefundsResponseFromLedger implements SearchPagination { + + @JsonProperty("total") + private int total; + + @JsonProperty("count") + private int count; + + @JsonProperty("page") + private int page; + + @JsonProperty("results") + private List refunds; + + @JsonProperty("_links") + private SearchNavigationLinks links = new SearchNavigationLinks(); + + public List getRefunds() { + return refunds; + } + + public int getTotal() { + return total; + } + + public int getCount() { + return count; + } + + public int getPage() { + return page; + } + + public SearchNavigationLinks getLinks() { + return links; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/TransactionState.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/TransactionState.java new file mode 100644 index 000000000..4cf21f326 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/ledger/TransactionState.java @@ -0,0 +1,39 @@ +package uk.gov.pay.api.model.ledger; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public class TransactionState { + + private String status; + private boolean finished; + private String message; + private String code; + + public String getStatus() { + return status; + } + + public boolean isFinished() { + return finished; + } + + public String getMessage() { + return message; + } + + public String getCode() { + return code; + } + + public TransactionState() { + } + + public TransactionState(String status, boolean finished) { + this.status = status; + this.finished = finished; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/DisputeLinksForSearch.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/DisputeLinksForSearch.java new file mode 100644 index 000000000..7d6742f65 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/DisputeLinksForSearch.java @@ -0,0 +1,21 @@ +package uk.gov.pay.api.model.links; + +import io.swagger.v3.oas.annotations.media.Schema; + +import static javax.ws.rs.HttpMethod.GET; + +@Schema(name = "DisputeLinksForSearch", description = "links for search dispute resource") +public class DisputeLinksForSearch { + + private static final String PAYMENT = "payment"; + + private Link payment; + + public Link getPayment() { + return payment; + } + + public void addPayment(String href) { + this.payment = new Link(href, GET); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/Link.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/Link.java new file mode 100644 index 000000000..f20c985cb --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/Link.java @@ -0,0 +1,69 @@ +package uk.gov.pay.api.model.links; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.Objects; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +@Schema(name = "Link", description = "A link related to a payment") +@JsonInclude(Include.NON_NULL) +public class Link { + + @JsonProperty(value = "href") + @Schema(example = "https://an.example.link/from/payment/platform", + description = "A URL that lets you perform additional actions to this payment " + + "when combined with the associated `method`.", + accessMode = READ_ONLY) + private String href; + @JsonProperty(value = "method") + @Schema(example = "GET", + description = "An API method that lets you perform additional actions to this payment" + + "when combined with the associated `href`.", + accessMode = READ_ONLY) + private String method; + + public Link(String href, String method) { + this.href = href; + this.method = method; + } + + public Link(String href) { + this.href = href; + } + + public Link() {} + + public String getHref() { + return href; + } + + public String getMethod() { + return method; + } + + @Override + public String toString() { + return "Link{" + + "href='" + href + '\'' + + ", method='" + method + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Link link = (Link) o; + return Objects.equals(href, link.href) && + Objects.equals(method, link.method); + } + + @Override + public int hashCode() { + return Objects.hash(href, method); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentEventLink.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentEventLink.java new file mode 100644 index 000000000..484a619eb --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentEventLink.java @@ -0,0 +1,35 @@ +package uk.gov.pay.api.model.links; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import static javax.ws.rs.HttpMethod.GET; + +@Schema(name = "PaymentEventLink", description = "Resource link for a payment of a payment event") +public class PaymentEventLink { + + public static final String PAYMENT_LINK = "payment_url"; + + @JsonProperty(value = PAYMENT_LINK) + private Link paymentLink; + + + public PaymentEventLink(String href) { + this.paymentLink = new Link(href, GET); + } + + @Schema(name = PAYMENT_LINK) + public Link getPaymentLink() { + return paymentLink; + } + + + @Override + public String toString() { + return "PaymentEventLink{" + + "paymentLink=" + paymentLink + + '}'; + } + + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentLinks.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentLinks.java new file mode 100644 index 000000000..38f7f9dac --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentLinks.java @@ -0,0 +1,136 @@ +package uk.gov.pay.api.model.links; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.PaymentConnectorResponseLink; +import uk.gov.pay.api.service.PublicApiUriGenerator; + +import java.net.URI; +import java.util.List; + +import static javax.ws.rs.HttpMethod.GET; +import static javax.ws.rs.HttpMethod.POST; + +@Schema(name = "PaymentLinks", description = "links for payment") +public class PaymentLinks { + + private static final String SELF_FIELD = "self"; + private static final String NEXT_URL_FIELD = "next_url"; + private static final String NEXT_URL_POST_FIELD = "next_url_post"; + private static final String AUTH_URL_POST_FIELD = "auth_url_post"; + private static final String EVENTS_FIELD = "events"; + private static final String CANCEL_FIELD = "cancel"; + private static final String REFUNDS_FIELD = "refunds"; + private static final String CAPTURE_FIELD = "capture"; + + @JsonProperty(value = SELF_FIELD) + private Link self; + + @JsonProperty(NEXT_URL_FIELD) + private Link nextUrl; + + @JsonProperty(NEXT_URL_POST_FIELD) + private PostLink nextUrlPost; + + @JsonProperty(AUTH_URL_POST_FIELD) + private PostLink authUrlPost; + + @JsonProperty(value = EVENTS_FIELD) + private Link events; + + @JsonProperty(value = REFUNDS_FIELD) + private Link refunds; + + @JsonProperty(value = CANCEL_FIELD) + private PostLink cancel; + + @JsonProperty(value = CAPTURE_FIELD) + private PostLink capture; + + public Link getSelf() { + return self; + } + + public Link getNextUrl() { + return nextUrl; + } + + public PostLink getNextUrlPost() { + return nextUrlPost; + } + + public PostLink getAuthUrlPost() { + return authUrlPost; + } + + public Link getEvents() { + return events; + } + + public Link getRefunds() { + return refunds; + } + + public PostLink getCancel() { + return cancel; + } + + public PostLink getCapture() { + return capture; + } + + public void addKnownLinksValueOf(List chargeLinks, URI paymentAuthorisationUri) { + addNextUrlIfPresent(chargeLinks); + addNextUrlPostIfPresent(chargeLinks); + addAuthUrlPostIfPresent(chargeLinks, paymentAuthorisationUri); + addCaptureUrlIfPresent(chargeLinks); + } + + public void addSelf(String href) { + this.self = new Link(href, GET); + } + + public void addEvents(String href) { + this.events = new Link(href, GET); + } + + public void addRefunds(String href) { + this.refunds = new Link(href, GET); + } + + public void addCancel(String href) { + this.cancel = new PostLink(href, POST); + } + + public void addCapture(String href) { + this.capture = new PostLink(href, POST); + } + + private void addAuthUrlPostIfPresent(List chargeLinks, URI paymentAuthorisationUri) { + chargeLinks.stream() + .filter(links -> AUTH_URL_POST_FIELD.equals(links.getRel())) + .findFirst() + .ifPresent(links -> this.authUrlPost = new PostLink(paymentAuthorisationUri.toString(), links.getMethod(), links.getType(), links.getParams())); + } + + private void addNextUrlPostIfPresent(List chargeLinks) { + chargeLinks.stream() + .filter(chargeLink -> NEXT_URL_POST_FIELD.equals(chargeLink.getRel())) + .findFirst() + .ifPresent(chargeLink -> this.nextUrlPost = new PostLink(chargeLink.getHref(), chargeLink.getMethod(), chargeLink.getType(), chargeLink.getParams())); + } + + private void addNextUrlIfPresent(List chargeLinks) { + chargeLinks.stream() + .filter(chargeLink -> NEXT_URL_FIELD.equals(chargeLink.getRel())) + .findFirst() + .ifPresent(chargeLink -> this.nextUrl = new Link(chargeLink.getHref(), chargeLink.getMethod())); + } + + private void addCaptureUrlIfPresent(List chargeLinks) { + chargeLinks.stream() + .filter(chargeLink -> CAPTURE_FIELD.equals(chargeLink.getRel())) + .findFirst() + .ifPresent(chargeLink -> this.capture = new PostLink(chargeLink.getHref(), chargeLink.getMethod())); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentLinksForEvents.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentLinksForEvents.java new file mode 100644 index 000000000..f67395ace --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentLinksForEvents.java @@ -0,0 +1,25 @@ +package uk.gov.pay.api.model.links; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import static javax.ws.rs.HttpMethod.GET; + +@Schema(name = "PaymentLinksForEvents", description = "links for events resource") +public class PaymentLinksForEvents { + + public static final String SELF = "self"; + + @JsonProperty(value = SELF) + private Link self; + + @Schema(description = SELF) + public Link getSelf() { + return self; + } + + public void addSelf(String href) { + this.self = new Link(href, GET); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentLinksForSearch.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentLinksForSearch.java new file mode 100644 index 000000000..94d2fdfeb --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentLinksForSearch.java @@ -0,0 +1,73 @@ +package uk.gov.pay.api.model.links; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import static javax.ws.rs.HttpMethod.GET; +import static javax.ws.rs.HttpMethod.POST; + +@Schema(name = "PaymentLinksForSearch", description = "links for search payment resource") +public class PaymentLinksForSearch { + + private static final String SELF = "self"; + private static final String EVENTS = "events"; + private static final String CANCEL = "cancel"; + private static final String REFUNDS = "refunds"; + private static final String CAPTURE = "capture"; + + @JsonProperty(value = SELF) + private Link self; + + @JsonProperty(value = CANCEL) + private PostLink cancel; + + @JsonProperty(value = EVENTS) + private Link events; + + @JsonProperty(value = REFUNDS) + private Link refunds; + + @JsonProperty(value = CAPTURE) + private PostLink capture; + + public Link getSelf() { + return self; + } + + public PostLink getCancel() { + return cancel; + } + + public Link getEvents() { + return events; + } + + public Link getRefunds() { + return refunds; + } + + @Schema(name = CAPTURE, implementation = PostLink.class) + public Link getCapture() { + return capture; + } + + public void addSelf(String href) { + this.self = new Link(href, GET); + } + + public void addEvents(String href) { + this.events = new Link(href, GET); + } + + public void addCancel(String href) { + this.cancel = new PostLink(href, POST); + } + + public void addRefunds(String href) { + this.refunds = new Link(href, GET); + } + + public void addCapture(String href) { + this.capture = new PostLink(href, POST); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentWithAllLinks.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentWithAllLinks.java new file mode 100644 index 000000000..98f39ae0b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PaymentWithAllLinks.java @@ -0,0 +1,301 @@ +package uk.gov.pay.api.model.links; + +import com.fasterxml.jackson.annotation.JsonProperty; +import uk.gov.pay.api.model.AuthorisationSummary; +import uk.gov.pay.api.model.CardDetails; +import uk.gov.pay.api.model.CardPayment; +import uk.gov.pay.api.model.Charge; +import uk.gov.pay.api.model.PaymentConnectorResponseLink; +import uk.gov.pay.api.model.PaymentSettlementSummary; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.RefundSummary; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.model.charge.ExternalMetadata; + +import java.net.URI; +import java.util.List; + +public class PaymentWithAllLinks extends CardPayment { + + @JsonProperty(CardPayment.LINKS_JSON_ATTRIBUTE) + private PaymentLinks links = new PaymentLinks(); + + public PaymentLinks getLinks() { + return links; + } + + private PaymentWithAllLinks(String chargeId, long amount, PaymentState state, String returnUrl, String description, + String reference, String email, String paymentProvider, String createdDate, SupportedLanguage language, + boolean delayedCapture, boolean moto, RefundSummary refundSummary, PaymentSettlementSummary settlementSummary, CardDetails cardDetails, + List paymentConnectorResponseLinks, URI selfLink, URI paymentEventsUri, URI paymentCancelUri, + URI paymentRefundsUri, URI paymentCaptureUri, URI paymentAuthorisationUri, Long corporateCardSurcharge, Long totalAmount, String providerId, ExternalMetadata metadata, + Long fee, Long netAmount, AuthorisationSummary authorisationSummary, String agreementId, AuthorisationMode authorisationMode) { + super(chargeId, amount, state, returnUrl, description, reference, email, paymentProvider, createdDate, + refundSummary, settlementSummary, cardDetails, language, delayedCapture, moto, corporateCardSurcharge, totalAmount, + providerId, metadata, fee, netAmount, authorisationSummary, agreementId, authorisationMode); + this.links.addSelf(selfLink.toString()); + this.links.addKnownLinksValueOf(paymentConnectorResponseLinks, paymentAuthorisationUri); + this.links.addEvents(paymentEventsUri.toString()); + this.links.addRefunds(paymentRefundsUri.toString()); + + if (!state.isFinished() && authorisationMode != AuthorisationMode.AGREEMENT) { + this.links.addCancel(paymentCancelUri.toString()); + } + + if (paymentConnectorResponseLinks.stream().anyMatch(link -> "capture".equals(link.getRel()))) { + this.links.addCapture(paymentCaptureUri.toString()); + } + } + + public static PaymentWithAllLinks valueOf(Charge paymentConnector, + URI selfLink, + URI paymentEventsUri, + URI paymentCancelUri, + URI paymentRefundsUri, + URI paymentsCaptureUri, + URI paymentAuthorisationUri) { + return new PaymentWithAllLinksBuilder() + .withChargeId(paymentConnector.getChargeId()) + .withAmount(paymentConnector.getAmount()) + .withState(paymentConnector.getState()) + .withReturnUrl(paymentConnector.getReturnUrl()) + .withDescription(paymentConnector.getDescription()) + .withReference(paymentConnector.getReference()) + .withEmail(paymentConnector.getEmail()) + .withPaymentProvider(paymentConnector.getPaymentProvider()) + .withCreatedDate(paymentConnector.getCreatedDate()) + .withLanguage(paymentConnector.getLanguage()) + .withDelayedCapture(paymentConnector.getDelayedCapture()) + .withMoto(paymentConnector.isMoto()) + .withRefundSummary(paymentConnector.getRefundSummary()) + .withSettlementSummary(paymentConnector.getSettlementSummary()) + .withCardDetails(paymentConnector.getCardDetails()) + .withPaymentConnectorResponseLinks(paymentConnector.getLinks()) + .withSelfLink(selfLink) + .withPaymentEventsUri(paymentEventsUri) + .withPaymentCancelUri(paymentCancelUri) + .withPaymentRefundsUri(paymentRefundsUri) + .withPaymentCaptureUri(paymentsCaptureUri) + .withPaymentAuthorisationUri(paymentAuthorisationUri) + .withCorporateCardSurcharge(paymentConnector.getCorporateCardSurcharge()) + .withTotalAmount(paymentConnector.getTotalAmount()) + .withProviderId(paymentConnector.getGatewayTransactionId()) + .withMetadata(paymentConnector.getMetadata().orElse(null)) + .withFee(paymentConnector.getFee()) + .withNetAmount(paymentConnector.getNetAmount()) + .withAuthorisationSummary(paymentConnector.getAuthorisationSummary()) + .withAgreementId(paymentConnector.getAgreementId()) + .withAuthorisationMode(paymentConnector.getAuthorisationMode()) + .build(); + } + + public static PaymentWithAllLinks getPaymentWithLinks( + Charge paymentConnector, + URI selfLink, + URI paymentEventsUri, + URI paymentCancelUri, + URI paymentRefundsUri, + URI paymentsCaptureUri, + URI paymentAuthorisationUri) { + + return PaymentWithAllLinks.valueOf(paymentConnector, selfLink, paymentEventsUri, paymentCancelUri, paymentRefundsUri, paymentsCaptureUri, paymentAuthorisationUri); + } + + public static class PaymentWithAllLinksBuilder { + private String chargeId; + private long amount; + private PaymentState state; + private String returnUrl; + private String description; + private String reference; + private String email; + private String paymentProvider; + private String createdDate; + private SupportedLanguage language; + private boolean delayedCapture; + private boolean moto; + private RefundSummary refundSummary; + private PaymentSettlementSummary settlementSummary; + private CardDetails cardDetails; + private List paymentConnectorResponseLinks; + private URI selfLink; + private URI paymentEventsUri; + private URI paymentCancelUri; + private URI paymentRefundsUri; + private URI paymentCaptureUri; + private URI paymentAuthorisationUri; + private Long corporateCardSurcharge; + private Long totalAmount; + private String providerId; + private ExternalMetadata metadata; + private Long fee; + private Long netAmount; + private AuthorisationSummary authorisationSummary; + private String agreementId; + private AuthorisationMode authorisationMode; + + public PaymentWithAllLinksBuilder withChargeId(String chargeId) { + this.chargeId = chargeId; + return this; + } + + public PaymentWithAllLinksBuilder withAmount(long amount) { + this.amount = amount; + return this; + } + + public PaymentWithAllLinksBuilder withState(PaymentState state) { + this.state = state; + return this; + } + + public PaymentWithAllLinksBuilder withReturnUrl(String returnUrl) { + this.returnUrl = returnUrl; + return this; + } + + public PaymentWithAllLinksBuilder withDescription(String description) { + this.description = description; + return this; + } + + public PaymentWithAllLinksBuilder withReference(String reference) { + this.reference = reference; + return this; + } + + public PaymentWithAllLinksBuilder withEmail(String email) { + this.email = email; + return this; + } + + public PaymentWithAllLinksBuilder withPaymentProvider(String paymentProvider) { + this.paymentProvider = paymentProvider; + return this; + } + + public PaymentWithAllLinksBuilder withCreatedDate(String createdDate) { + this.createdDate = createdDate; + return this; + } + + public PaymentWithAllLinksBuilder withLanguage(SupportedLanguage language) { + this.language = language; + return this; + } + + public PaymentWithAllLinksBuilder withDelayedCapture(boolean delayedCapture) { + this.delayedCapture = delayedCapture; + return this; + } + + public PaymentWithAllLinksBuilder withMoto(boolean moto) { + this.moto = moto; + return this; + } + + public PaymentWithAllLinksBuilder withRefundSummary(RefundSummary refundSummary) { + this.refundSummary = refundSummary; + return this; + } + + public PaymentWithAllLinksBuilder withSettlementSummary(PaymentSettlementSummary settlementSummary) { + this.settlementSummary = settlementSummary; + return this; + } + + public PaymentWithAllLinksBuilder withCardDetails(CardDetails cardDetails) { + this.cardDetails = cardDetails; + return this; + } + + public PaymentWithAllLinksBuilder withPaymentConnectorResponseLinks(List paymentConnectorResponseLinks) { + this.paymentConnectorResponseLinks = paymentConnectorResponseLinks; + return this; + } + + public PaymentWithAllLinksBuilder withSelfLink(URI selfLink) { + this.selfLink = selfLink; + return this; + } + + public PaymentWithAllLinksBuilder withPaymentEventsUri(URI paymentEventsUri) { + this.paymentEventsUri = paymentEventsUri; + return this; + } + + public PaymentWithAllLinksBuilder withPaymentCancelUri(URI paymentCancelUri) { + this.paymentCancelUri = paymentCancelUri; + return this; + } + + public PaymentWithAllLinksBuilder withPaymentRefundsUri(URI paymentRefundsUri) { + this.paymentRefundsUri = paymentRefundsUri; + return this; + } + + public PaymentWithAllLinksBuilder withPaymentCaptureUri(URI paymentCaptureUri) { + this.paymentCaptureUri = paymentCaptureUri; + return this; + } + + public PaymentWithAllLinksBuilder withPaymentAuthorisationUri(URI paymentAuthorisationUri) { + this.paymentAuthorisationUri = paymentAuthorisationUri; + return this; + } + + public PaymentWithAllLinksBuilder withCorporateCardSurcharge(Long corporateCardSurcharge) { + this.corporateCardSurcharge = corporateCardSurcharge; + return this; + } + + public PaymentWithAllLinksBuilder withTotalAmount(Long totalAmount) { + this.totalAmount = totalAmount; + return this; + } + + public PaymentWithAllLinksBuilder withProviderId(String providerId) { + this.providerId = providerId; + return this; + } + + public PaymentWithAllLinksBuilder withMetadata(ExternalMetadata metadata) { + this.metadata = metadata; + return this; + } + + public PaymentWithAllLinksBuilder withFee(Long fee) { + this.fee = fee; + return this; + } + + public PaymentWithAllLinksBuilder withNetAmount(Long netAmount) { + this.netAmount = netAmount; + return this; + } + + public PaymentWithAllLinksBuilder withAuthorisationSummary(AuthorisationSummary authorisationSummary) { + this.authorisationSummary = authorisationSummary; + return this; + } + + public PaymentWithAllLinksBuilder withAgreementId(String agreementId) { + this.agreementId = agreementId; + return this; + } + + public PaymentWithAllLinksBuilder withAuthorisationMode(AuthorisationMode authorisationMode) { + this.authorisationMode = authorisationMode; + return this; + } + + public PaymentWithAllLinks build() { + return new PaymentWithAllLinks(chargeId, amount, state, returnUrl, description, reference, email, + paymentProvider, createdDate, language, delayedCapture, moto, refundSummary, settlementSummary, + cardDetails, paymentConnectorResponseLinks, selfLink, paymentEventsUri, paymentCancelUri, + paymentRefundsUri, paymentCaptureUri, paymentAuthorisationUri, corporateCardSurcharge, totalAmount, + providerId, metadata, fee, netAmount, authorisationSummary, agreementId, authorisationMode); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PostLink.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PostLink.java new file mode 100644 index 000000000..a6abd38ef --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/PostLink.java @@ -0,0 +1,68 @@ +package uk.gov.pay.api.model.links; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.Map; +import java.util.Objects; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +@Schema(name = "PostLink", description = "A POST link related to a payment") +@JsonInclude(Include.NON_NULL) +public class PostLink extends Link { + + private String type; + private Map params; + + public PostLink(String href, String method, String type, Map params) { + super(href, method); + this.type = type; + this.params = params; + } + + public PostLink(String href, String method) { + super(href, method); + } + + @Schema(example = "POST", accessMode = READ_ONLY) + public String getMethod() { + return super.getMethod(); + } + + @Schema(example = "application/x-www-form-urlencoded") + public String getType() { + return type; + } + + @Schema(example = "{\"description\": \"This is a value for a parameter called description\"}") + public Map getParams() { + return params; + } + + @Override + public String toString() { + return "Link{" + + "href='" + getHref() + '\'' + + ", method='" + getMethod() + '\'' + + ", type='" + type + '\'' + + ", params=" + params + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + PostLink postLink = (PostLink) o; + return Objects.equals(type, postLink.type) && + Objects.equals(params, postLink.params); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), type, params); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/RefundLinksForSearch.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/RefundLinksForSearch.java new file mode 100644 index 000000000..592261706 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/RefundLinksForSearch.java @@ -0,0 +1,36 @@ +package uk.gov.pay.api.model.links; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import static javax.ws.rs.HttpMethod.GET; + +@Schema(name = "RefundLinksForSearch", description = "links for search refunds resource") +public class RefundLinksForSearch { + + private static final String SELF = "self"; + private static final String PAYMENT = "payment"; + + private Link self; + private Link payment; + + @Schema(description = "self") + @JsonProperty(value = SELF) + public Link getSelf() { + return self; + } + + @Schema(description = "payment") + @JsonProperty(value = PAYMENT) + public Link getPayment() { + return payment; + } + + public void addSelf(String href) { + this.self = new Link(href, GET); + } + + public void addPayment(String href) { + this.payment = new Link(href, GET); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/SearchNavigationLinks.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/SearchNavigationLinks.java new file mode 100644 index 000000000..31b68c043 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/links/SearchNavigationLinks.java @@ -0,0 +1,69 @@ +package uk.gov.pay.api.model.links; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "SearchNavigationLinks", description = "Links to navigate through pages of your search.") +public class SearchNavigationLinks { + + @JsonProperty(value = "self") + private Link self; + + @JsonProperty(value = "first_page") + private Link firstPage; + + @JsonProperty(value = "last_page") + private Link lastPage; + + @JsonProperty(value = "prev_page") + private Link prevPage; + + @JsonProperty(value = "next_page") + private Link nextPage; + + @Schema(description = "Use this URL ('href') to run the same search again.") + public Link getSelf() { + return self; + } + + @Schema(description = "Use this URL ('href') to get the first page of results.") + public Link getFirstPage() { + return firstPage; + } + + @Schema(description = "Use this URL ('href') to get the last page of results.") + public Link getLastPage() { + return lastPage; + } + + @Schema(description = "Use this URL ('href') to get the previous page of results.") + public Link getPrevPage() { + return prevPage; + } + + @Schema(description = "Use this URL ('href') to get the next page of results.") + public Link getNextPage() { + return nextPage; + } + + public SearchNavigationLinks withSelfLink(String href) { + this.self = new Link(href); + return this; + } + public SearchNavigationLinks withPrevLink(String href) { + this.prevPage = new Link(href); + return this; + } + public SearchNavigationLinks withNextLink(String href) { + this.nextPage = new Link(href); + return this; + } + public SearchNavigationLinks withFirstLink(String href) { + this.firstPage = new Link(href); + return this; + } + public SearchNavigationLinks withLastLink(String href) { + this.lastPage = new Link(href); + return this; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/publicauth/AuthResponse.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/publicauth/AuthResponse.java new file mode 100644 index 000000000..09eef8cfb --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/publicauth/AuthResponse.java @@ -0,0 +1,36 @@ +package uk.gov.pay.api.model.publicauth; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import uk.gov.pay.api.model.TokenPaymentType; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public class AuthResponse { + + private String accountId; + private String tokenLink; + private TokenPaymentType tokenType; + + public AuthResponse() { + } + + public AuthResponse(String accountId, String tokenLink, TokenPaymentType tokenType) { + this.accountId = accountId; + this.tokenLink = tokenLink; + this.tokenType = tokenType; + } + + public String getAccountId() { + return accountId; + } + + public String getTokenLink() { + return tokenLink; + } + + public TokenPaymentType getTokenType() { + return tokenType; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/PaginationDecorator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/PaginationDecorator.java new file mode 100644 index 000000000..68c54c650 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/PaginationDecorator.java @@ -0,0 +1,105 @@ +package uk.gov.pay.api.model.search; + +import black.door.hate.HalRepresentation; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.exception.SearchPaymentsException; +import uk.gov.pay.api.model.links.Link; +import uk.gov.pay.api.model.links.SearchNavigationLinks; + +import javax.inject.Inject; +import javax.ws.rs.core.UriBuilder; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Optional; + +public class PaginationDecorator { + + private final String baseUrl; + private final List queryParametersToBeExcluded + = List.of("account_id", "gateway_account_id", "transaction_type", "status_version"); + + @Inject + public PaginationDecorator(PublicApiConfig config) { + baseUrl = config.getBaseUrl(); + } + + public HalRepresentation.HalRepresentationBuilder decoratePagination(HalRepresentation.HalRepresentationBuilder halRepresentationBuilder, + SearchPagination pagination, String path) { + + HalRepresentation.HalRepresentationBuilder builder = addPaginationProperties(halRepresentationBuilder, pagination); + SearchNavigationLinks links = pagination.getLinks(); + try { + addLink(builder, "self", transformIntoPublicUri(baseUrl, links.getSelf(), path)); + addLink(builder, "first_page", transformIntoPublicUri(baseUrl, links.getFirstPage(), path)); + addLink(builder, "last_page", transformIntoPublicUri(baseUrl, links.getLastPage(), path)); + addLink(builder, "prev_page", transformIntoPublicUri(baseUrl, links.getPrevPage(), path)); + addLink(builder, "next_page", transformIntoPublicUri(baseUrl, links.getNextPage(), path)); + } catch (URISyntaxException ex) { + throw new SearchPaymentsException(ex); + } + return builder; + } + + public SearchNavigationLinks transformLinksToPublicApiUri( + SearchNavigationLinks links, String path) { + try { + return links.withSelfLink(transformIntoPublicUriAsString(baseUrl, links.getSelf(), path)) + .withFirstLink(transformIntoPublicUriAsString(baseUrl, links.getFirstPage(), path)) + .withLastLink(transformIntoPublicUriAsString(baseUrl, links.getLastPage(), path)) + .withPrevLink(transformIntoPublicUriAsString(baseUrl, links.getPrevPage(), path)) + .withNextLink(transformIntoPublicUriAsString(baseUrl, links.getNextPage(), path)); + } catch (URISyntaxException ex) { + throw new SearchPaymentsException(ex); + } + } + + private HalRepresentation.HalRepresentationBuilder addPaginationProperties(HalRepresentation.HalRepresentationBuilder halRepresentationBuilder, + SearchPagination pagination) { + halRepresentationBuilder + .addProperty("count", pagination.getCount()) + .addProperty("total", pagination.getTotal()) + .addProperty("page", pagination.getPage()); + return halRepresentationBuilder; + } + + private void addLink(HalRepresentation.HalRepresentationBuilder halRepresentationBuilder, String name, URI uri) { + if (uri != null) { + halRepresentationBuilder.addLink(name, uri); + } + } + + private String transformIntoPublicUriAsString(String baseUrl, Link link, String path) throws URISyntaxException { + UriBuilder uriBuilder = getUriBuilder(baseUrl, link, path); + + return Optional.ofNullable(uriBuilder) + .map(builder -> { + // breaks the order of query parameters + queryParametersToBeExcluded.forEach(queryParam -> + uriBuilder.replaceQueryParam(queryParam, (Object[]) null)); + return builder.build().toString(); + }) + .orElse(null); + } + + private URI transformIntoPublicUri(String baseUrl, Link link, String path) throws URISyntaxException { + UriBuilder uriBuilder = getUriBuilder(baseUrl, link, path); + return Optional.ofNullable(uriBuilder) + .map(builder -> { + // breaks the order of query parameters + queryParametersToBeExcluded.forEach(queryParam -> + uriBuilder.replaceQueryParam(queryParam, (Object[]) null)); + return builder.build(); + }) + .orElse(null); + } + + private UriBuilder getUriBuilder(String baseUrl, Link link, String path) throws URISyntaxException { + if (link == null) + return null; + + return UriBuilder.fromUri(baseUrl) + .path(path) + .replaceQuery(new URI(link.getHref()).getQuery()); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/SearchPagination.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/SearchPagination.java new file mode 100644 index 000000000..f3388fbb3 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/SearchPagination.java @@ -0,0 +1,14 @@ +package uk.gov.pay.api.model.search; + + +import uk.gov.pay.api.model.links.SearchNavigationLinks; + +public interface SearchPagination { + int getCount(); + + int getTotal(); + + int getPage(); + + SearchNavigationLinks getLinks(); +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/PaymentForSearchResult.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/PaymentForSearchResult.java new file mode 100644 index 000000000..38a98aa59 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/PaymentForSearchResult.java @@ -0,0 +1,95 @@ +package uk.gov.pay.api.model.search.card; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.AuthorisationSummary; +import uk.gov.pay.api.model.CardDetails; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.CardPayment; +import uk.gov.pay.api.model.PaymentConnectorResponseLink; +import uk.gov.pay.api.model.PaymentSettlementSummary; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.RefundSummary; +import uk.gov.pay.api.model.TransactionResponse; +import uk.gov.pay.api.model.links.PaymentLinksForSearch; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.model.charge.ExternalMetadata; + +import java.net.URI; +import java.util.List; + +@Schema(name = "PaymentDetailForSearch") +public class PaymentForSearchResult extends CardPayment { + + @JsonProperty(LINKS_JSON_ATTRIBUTE) + private PaymentLinksForSearch links = new PaymentLinksForSearch(); + + public PaymentForSearchResult(String chargeId, long amount, PaymentState state, String returnUrl, String description, + String reference, String email, String paymentProvider, String createdDate, SupportedLanguage language, + boolean delayedCapture, boolean moto, RefundSummary refundSummary, PaymentSettlementSummary settlementSummary, CardDetails cardDetails, + List links, URI selfLink, URI paymentEventsLink, URI paymentCancelLink, URI paymentRefundsLink, URI paymentCaptureUri, + Long corporateCardSurcharge, Long totalAmount, String providerId, ExternalMetadata externalMetadata, + Long fee, Long netAmount, AuthorisationSummary authorisationSummary, AuthorisationMode authorisationMode) { + + super(chargeId, amount, state, returnUrl, description, reference, email, paymentProvider, + createdDate, refundSummary, settlementSummary, cardDetails, language, delayedCapture, moto, corporateCardSurcharge, totalAmount, providerId, externalMetadata, + fee, netAmount, authorisationSummary, null, authorisationMode); + this.links.addSelf(selfLink.toString()); + this.links.addEvents(paymentEventsLink.toString()); + this.links.addRefunds(paymentRefundsLink.toString()); + + if (!state.isFinished() && authorisationMode != AuthorisationMode.AGREEMENT) { + this.links.addCancel(paymentCancelLink.toString()); + } + if (links.stream().anyMatch(link -> "capture".equals(link.getRel()))) { + this.links.addCapture(paymentCaptureUri.toString()); + } + } + + public static PaymentForSearchResult valueOf( + TransactionResponse paymentResult, + URI selfLink, + URI paymentEventsLink, + URI paymentCancelLink, + URI paymentRefundsLink, + URI paymentCaptureUri) { + + return new PaymentForSearchResult( + paymentResult.getTransactionId(), + paymentResult.getAmount(), + paymentResult.getState(), + paymentResult.getReturnUrl(), + paymentResult.getDescription(), + paymentResult.getReference(), + paymentResult.getEmail(), + paymentResult.getPaymentProvider(), + paymentResult.getCreatedDate(), + paymentResult.getLanguage(), + paymentResult.getDelayedCapture(), + paymentResult.isMoto(), + paymentResult.getRefundSummary(), + paymentResult.getSettlementSummary(), + paymentResult.getWalletType() + .map(wallet -> CardDetails.from(paymentResult.getCardDetailsFromResponse(), wallet.getTitleCase())) + .orElse(CardDetails.from(paymentResult.getCardDetailsFromResponse(), null)), + paymentResult.getLinks(), + selfLink, + paymentEventsLink, + paymentCancelLink, + paymentRefundsLink, + paymentCaptureUri, + paymentResult.getCorporateCardSurcharge(), + paymentResult.getTotalAmount(), + paymentResult.getGatewayTransactionId(), + paymentResult.getMetadata().orElse(null), + paymentResult.getFee(), + paymentResult.getNetAmount(), + paymentResult.getAuthorisationSummary(), + paymentResult.getAuthorisationMode()); + } + + public PaymentLinksForSearch getLinks() { + return links; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/PaymentSearchResponse.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/PaymentSearchResponse.java new file mode 100644 index 000000000..74df5d7d5 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/PaymentSearchResponse.java @@ -0,0 +1,47 @@ +package uk.gov.pay.api.model.search.card; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import uk.gov.pay.api.model.links.SearchNavigationLinks; +import uk.gov.pay.api.model.search.SearchPagination; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PaymentSearchResponse implements SearchPagination { + + @JsonProperty("total") + private int total; + + @JsonProperty("count") + private int count; + + @JsonProperty("page") + private int page; + + @JsonProperty("results") + private List payments; + + @JsonProperty("_links") + private SearchNavigationLinks links = new SearchNavigationLinks(); + + public List getPayments() { + return payments; + } + @Override + public int getTotal() { + return total; + } + @Override + public int getCount() { + return count; + } + @Override + public int getPage() { + return page; + } + + public SearchNavigationLinks getLinks() { + return links; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/PaymentSearchResults.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/PaymentSearchResults.java new file mode 100644 index 000000000..ef8b7ef08 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/PaymentSearchResults.java @@ -0,0 +1,52 @@ +package uk.gov.pay.api.model.search.card; + +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.links.SearchNavigationLinks; +import uk.gov.pay.api.model.search.SearchPagination; + +import java.util.List; + +/** + * Used to define swagger specs for search results. + */ +public class PaymentSearchResults implements SearchPagination { + + @Schema(name = "total", example = "100", description = "Total number of payments matching your search criteria.") + private int total; + @Schema(name = "count", example = "20", description = "Number of payments on the current page of search results.") + private int count; + @Schema(name = "page", example = "1", + description = "The [page of results you’re viewing]" + + "(https://docs.payments.service.gov.uk/api_reference/#pagination). " + + "To view other pages, make this request again using the `page` parameter.") + private int page; + private List results; + @Schema(name = "_links") + SearchNavigationLinks links; + + @Override + public int getTotal() { + return total; + } + + @Override + public int getCount() { + return count; + } + + @Override + + public int getPage() { + return page; + } + + @Schema(name = "results", description = "Contains payments matching your search criteria.") + public List getPayments() { + return results; + } + + @Override + public SearchNavigationLinks getLinks() { + return links; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/RefundForSearchRefundsResult.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/RefundForSearchRefundsResult.java new file mode 100644 index 000000000..b76a18afc --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/RefundForSearchRefundsResult.java @@ -0,0 +1,132 @@ +package uk.gov.pay.api.model.search.card; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.RefundSettlementSummary; +import uk.gov.pay.api.model.ledger.RefundTransactionFromLedger; +import uk.gov.pay.api.model.links.RefundLinksForSearch; + +import java.net.URI; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; + +@JsonIgnoreProperties(ignoreUnknown = true) +@Schema(name = "RefundDetailForSearch") +public class RefundForSearchRefundsResult { + + @JsonProperty("refund_id") + @Schema(example = "act4c33g40j3edfmi8jknab84x", + description = "The unique ID GOV.UK Pay automatically associated with this refund when you created it.", + accessMode = READ_ONLY) + private String refundId; + + @JsonProperty("created_date") + @Schema(example = "2017-01-10T16:52:07.855Z", + description = "The date and time you created this refund. " + + "This value uses Coordinated Universal Time (UTC) and ISO 8601 format - `YYYY-MM-DDThh:mm:ss.SSSZ`.", + accessMode = READ_ONLY) + private String createdDate; + + private String chargeId; + + private Long amount; + + private RefundLinksForSearch links = new RefundLinksForSearch(); + + @JsonProperty("status") + @Schema(example = "success", + description = "The [status of the refund]" + + "(https://docs.payments.service.gov.uk/refunding_payments/#checking-the-status-of-a-refund-status).", + allowableValues = {"submitted", "success", "error"}, accessMode = READ_ONLY) + private String status; + + @Schema(accessMode = READ_ONLY) + private RefundSettlementSummary settlementSummary; + + public RefundForSearchRefundsResult() { + } + + public RefundForSearchRefundsResult(String refundId, String createdDate, String status, + String chargeId, Long amount, URI paymentURI, URI refundsURI, + RefundSettlementSummary settlementSummary) { + this.refundId = refundId; + this.createdDate = createdDate; + this.status = status; + this.chargeId = chargeId; + this.amount = amount; + this.links.addSelf(refundsURI.toString()); + this.links.addPayment(paymentURI.toString()); + this.settlementSummary = settlementSummary; + } + + public String getRefundId() { + return refundId; + } + + public String getCreatedDate() { + return createdDate; + } + + public String getStatus() { + return status; + } + + @JsonProperty("payment_id") + @Schema(hidden = true) + public String getChargeId() { + return chargeId; + } + + @JsonProperty("charge_id") + @Schema(hidden = true) + public void setChargeId(String chargeId) { + this.chargeId = chargeId; + } + + @JsonProperty("amount_submitted") + @Schema(hidden = true) + public void setAmount(Long amount) { + this.amount = amount; + } + + @JsonProperty("amount") + @Schema(example = "120", description = "The amount refunded to the user in pence.", accessMode = READ_ONLY) + public Long getAmount() { + return amount; + } + + @JsonProperty("_links") + public RefundLinksForSearch getLinks() { + return links; + } + + @JsonProperty("settlement_summary") + @Schema(accessMode = READ_ONLY) + public RefundSettlementSummary getSettlementSummary() { + return settlementSummary; + } + + public static RefundForSearchRefundsResult valueOf(RefundTransactionFromLedger refundResult, URI paymentURI, URI refundsURI) { + return new RefundForSearchRefundsResult( + refundResult.getTransactionId(), + refundResult.getCreatedDate(), + refundResult.getState().getStatus(), + refundResult.getParentTransactionId(), + refundResult.getAmount(), + paymentURI, + refundsURI, + refundResult.getSettlementSummary()); + } + + @Override + public String toString() { + return "RefundForSearchRefundsResult{" + + "refundId='" + refundId + '\'' + + ", createdDate='" + createdDate + '\'' + + ", status='" + status + '\'' + + ", amount=" + amount + '\'' + + ", links=" + links + '\'' + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/RefundForSearchResult.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/RefundForSearchResult.java new file mode 100644 index 000000000..2a6d2865c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/RefundForSearchResult.java @@ -0,0 +1,39 @@ +package uk.gov.pay.api.model.search.card; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.links.RefundLinksForSearch; + +import java.util.List; + +public class RefundForSearchResult { + + @Schema(name = "payment_id", example = "hu20sqlact5260q2nanm0q8u93", description = "The unique ID GOV.UK Pay associated with this payment when you created it.") + private String paymentId; + @Schema(name = "_links") + private RefundLinksForSearch links; + @JsonProperty(value = "_embedded") + @Schema(name = "_embedded") + private Embedded embedded; + + @Schema(name = "EmbeddedRefunds") + public class Embedded { + private List refunds; + + public List getRefunds() { + return refunds; + } + } + + public String getPaymentId() { + return paymentId; + } + + public RefundLinksForSearch getLinks() { + return links; + } + + public Embedded getEmbedded() { + return embedded; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/RefundResult.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/RefundResult.java new file mode 100644 index 000000000..323b78f74 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/RefundResult.java @@ -0,0 +1,22 @@ +package uk.gov.pay.api.model.search.card; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.links.RefundLinksForSearch; + +@Schema(name = "Refund") +public class RefundResult { + + @JsonUnwrapped + private RefundForSearchRefundsResult refunds; + @Schema(name = "_links") + private RefundLinksForSearch links; + + public RefundForSearchRefundsResult getRefunds() { + return refunds; + } + + public RefundLinksForSearch getLinks() { + return links; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/SearchRefundsResults.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/SearchRefundsResults.java new file mode 100644 index 000000000..88b8e71c7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/card/SearchRefundsResults.java @@ -0,0 +1,60 @@ +package uk.gov.pay.api.model.search.card; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.links.SearchNavigationLinks; +import uk.gov.pay.api.model.search.SearchPagination; + +import java.util.List; + +@Schema(name = "RefundSearchResults") +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class SearchRefundsResults implements SearchPagination { + + @Schema(example = "100", description = "Number of refunds matching your search criteria.") + private int total; + @Schema(example = "20", description = "Number of refunds on the current page of search results.") + private int count; + @Schema(example = "1", description = "The [page of results](payments.service.gov.uk/api_reference/#pagination) you’re viewing. To view other pages, make this request again using the `page` parameter.") + private int page; + + @Schema(description = "Contains the refunds matching your search criteria.") + private List results; + @JsonProperty("_links") + private SearchNavigationLinks links; + + public SearchRefundsResults(int total, int count, int page, List results, + SearchNavigationLinks links) { + this.total = total; + this.count = count; + this.page = page; + this.results = results; + this.links = links; + } + + @Override + public int getTotal() { + return total; + } + + @Override + public int getCount() { + return count; + } + + @Override + public int getPage() { + return page; + } + + public List getResults() { + return results; + } + + @Override + public SearchNavigationLinks getLinks() { + return links; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/dispute/DisputeForSearchResult.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/dispute/DisputeForSearchResult.java new file mode 100644 index 000000000..2b1a9b385 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/dispute/DisputeForSearchResult.java @@ -0,0 +1,111 @@ +package uk.gov.pay.api.model.search.dispute; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.ledger.DisputeSettlementSummary; +import uk.gov.pay.api.model.ledger.DisputeTransactionFromLedger; +import uk.gov.pay.api.model.links.DisputeLinksForSearch; + +import java.net.URI; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static uk.gov.pay.api.model.CardPayment.LINKS_JSON_ATTRIBUTE; + +@Schema(name = "DisputeDetailForSearch") +public class DisputeForSearchResult { + private Long amount; + @JsonProperty("created_date") + private String createdDate; + @JsonProperty("dispute_id") + private String disputeId; + @JsonProperty("evidence_due_date") + private String evidenceDueDate; + private Long fee; + @JsonProperty("net_amount") + private Long netAmount; + @JsonProperty("payment_id") + private String paymentId; + private String reason; + @JsonProperty("settlement_summary") + private DisputeSettlementSummary settlementSummary; + private String status; + @JsonProperty(LINKS_JSON_ATTRIBUTE) + private DisputeLinksForSearch links = new DisputeLinksForSearch(); + + public DisputeForSearchResult(Long amount, String createdDate, String disputeId, String evidenceDueDate, + Long fee, Long netAmount, String paymentId, String reason, + DisputeSettlementSummary settlementSummary, String status, URI paymentURI) { + this.amount = amount; + this.createdDate = createdDate; + this.disputeId = disputeId; + this.evidenceDueDate = evidenceDueDate; + this.fee = fee; + this.netAmount = netAmount; + this.paymentId = paymentId; + this.reason = reason; + this.settlementSummary = settlementSummary; + this.status = status; + this.links.addPayment(paymentURI.toString()); + } + + public static DisputeForSearchResult valueOf(DisputeTransactionFromLedger fromLedger, URI paymentUri) { + return new DisputeForSearchResult(fromLedger.getAmount(), fromLedger.getCreatedDate(), fromLedger.getTransactionId(), + fromLedger.getEvidenceDueDate(), fromLedger.getFee(), fromLedger.getNetAmount(), fromLedger.getParentTransactionId(), + fromLedger.getReason(), fromLedger.getSettlementSummary(), fromLedger.getState().getStatus(), + paymentUri); + } + + @Schema(example = "1200", description = "The disputed amount in pence.", accessMode = READ_ONLY) + public Long getAmount() { + return amount; + } + + @Schema(example = "2022-07-28T16:43:00.000Z", description = "The date and time the user's bank told GOV.UK Pay about this dispute.", accessMode = READ_ONLY) + public String getCreatedDate() { + return createdDate; + } + + @Schema(example = "hu20sqlact5260q2nanm0q8u93", description = "The unique ID GOV.UK Pay automatically associated with this dispute when the paying user disputed the payment.", accessMode = READ_ONLY) + public String getDisputeId() { + return disputeId; + } + + @Schema(example = "2022-07-28T16:43:00.000Z", description = "The deadline for submitting your supporting evidence. This value uses Coordinated Universal Time (UTC) and ISO 8601 format", accessMode = READ_ONLY) + public String getEvidenceDueDate() { + return evidenceDueDate; + } + + @Schema(example = "1200", description = "The payment service provider’s dispute fee, in pence.", accessMode = READ_ONLY) + public Long getFee() { + return fee; + } + + @Schema(example = "-2400", description = "The amount, in pence, your payment service provider will take for a lost dispute. 'net_amount' is deducted from your payout after you lose the dispute. For example, a 'net_amount' of '-1500' means your PSP will take £15.00 from your next payout into your bank account. 'net_amount' is always a negative value. 'net_amount' only appears if you lose the dispute.", accessMode = READ_ONLY) + public Long getNetAmount() { + return netAmount; + } + + @Schema(example = "hu20sqlact5260q2nanm0q8u93", description = "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", accessMode = READ_ONLY) + public String getPaymentId() { + return paymentId; + } + + @Schema(example = "fraudulent", description = "The reason the paying user gave for disputing this payment. Possible values are: 'credit_not_processed', 'duplicate', 'fraudulent', 'general', 'product_not_received', 'product_unacceptable', 'unrecognised', 'subscription_cancelled', >'other'", accessMode = READ_ONLY) + public String getReason() { + return reason; + } + + public DisputeSettlementSummary getSettlementSummary() { + return settlementSummary; + } + + @Schema(example = "under_review", description = "The current status of the dispute. Possible values are: 'needs_response', 'won', 'lost', 'under_review'", accessMode = READ_ONLY) + public String getStatus() { + return status; + } + + @Schema(description = "Contains an API method and endpoint to get information about this payment. A 'GET' request ('method') to this endpoint ('href') returns information about this payment.") + public DisputeLinksForSearch getLinks() { + return links; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/dispute/DisputesSearchResults.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/dispute/DisputesSearchResults.java new file mode 100644 index 000000000..5b2157cea --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/dispute/DisputesSearchResults.java @@ -0,0 +1,55 @@ +package uk.gov.pay.api.model.search.dispute; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.v3.oas.annotations.media.Schema; +import uk.gov.pay.api.model.links.SearchNavigationLinks; +import uk.gov.pay.api.model.search.SearchPagination; + +import java.util.List; +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class DisputesSearchResults implements SearchPagination { + @Schema(name = "total", example = "100", description = "Number of total disputes matching your search criteria.") + private int total; + @Schema(name = "count", example = "20", description = "Number of disputes on the current page of search results.") + private int count; + @Schema(name = "page", example = "1", description = "The page of results you’re viewing. To view other pages, make this request again using the 'page' parameter.") + private int page; + @Schema(description = "Contains disputes matching your search criteria.") + private List results; + @Schema(name = "links", description = "Contains links you can use to move between the pages of this search.") + SearchNavigationLinks links; + + public DisputesSearchResults(int total, int count, int page, List results, + SearchNavigationLinks links) { + this.total = total; + this.count = count; + this.page = page; + this.results = results; + this.links = links; + } + + @Override + public int getCount() { + return count; + } + + @Override + public int getTotal() { + return total; + } + + @Override + public int getPage() { + return page; + } + + @Override + public SearchNavigationLinks getLinks() { + return links; + } + + public List getResults() { + return results; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/links/DDPaymentLinksForSearch.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/links/DDPaymentLinksForSearch.java new file mode 100644 index 000000000..8a91e61e8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/search/links/DDPaymentLinksForSearch.java @@ -0,0 +1,13 @@ +package uk.gov.pay.api.model.search.links; + +import uk.gov.pay.api.model.links.Link; + +import static javax.ws.rs.HttpMethod.GET; + +public class DDPaymentLinksForSearch { + private Link self; + + public void addSelf(String href) { this.self = new Link(href, GET); } + + public Link getSelf() { return self; } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/telephone/CreateTelephonePaymentRequest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/telephone/CreateTelephonePaymentRequest.java new file mode 100644 index 000000000..3a6eb6b52 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/telephone/CreateTelephonePaymentRequest.java @@ -0,0 +1,336 @@ +package uk.gov.pay.api.model.telephone; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import uk.gov.pay.api.utils.JsonStringBuilder; +import uk.gov.pay.api.validation.ValidCardExpiryDate; +import uk.gov.pay.api.validation.ValidCardFirstSixDigits; +import uk.gov.pay.api.validation.ValidCardLastFourDigits; +import uk.gov.pay.api.validation.ValidCardType; +import uk.gov.pay.api.validation.ValidZonedDateTime; + +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.Optional; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class CreateTelephonePaymentRequest { + + public static final int REFERENCE_MAX_LENGTH = 255; + public static final int AMOUNT_MAX_VALUE = 10000000; + public static final int AMOUNT_MIN_VALUE = 0; + public static final int DESCRIPTION_MAX_LENGTH = 255; + + @Min(value = AMOUNT_MIN_VALUE, message = "Must be greater than or equal to 1") + @Max(value = AMOUNT_MAX_VALUE, message = "Must be less than or equal to {value}") + @NotNull(message = "Field [amount] cannot be null") + private int amount; + + @Size(max = REFERENCE_MAX_LENGTH, message = "Must be less than or equal to {max} characters length") + @NotNull(message = "Field [reference] cannot be null") + private String reference; + + @Size(max = DESCRIPTION_MAX_LENGTH, message = "Must be less than or equal to {max} characters length") + @NotNull(message = "Field [description] cannot be null") + private String description; + + @JsonProperty("created_date") + @ValidZonedDateTime(message = "Field [created_date] must be a valid ISO-8601 time and date format") + private String createdDate; + + @JsonProperty("authorised_date") + @ValidZonedDateTime(message = "Field [authorised_date] must be a valid ISO-8601 time and date format") + private String authorisedDate; + + @NotNull(message = "Field [processor_id] cannot be null") + private String processorId; + + @NotNull(message = "Field [provider_id] cannot be null") + private String providerId; + + @JsonProperty("auth_code") + private String authCode; + + @Valid + @NotNull(message = "Field [payment_outcome] cannot be null") + private PaymentOutcome paymentOutcome; + + @ValidCardType(message = "Field [card_type] must be either master-card, visa, maestro, diners-club, american-express or jcb") + private String cardType; + + @JsonProperty("name_on_card") + private String nameOnCard; + + @JsonProperty("email_address") + private String emailAddress; + + @ValidCardExpiryDate(message = "Field [card_expiry] must have valid MM/YY") + private String cardExpiry; + + @ValidCardLastFourDigits(message = "Field [last_four_digits] must be exactly 4 digits") + private String lastFourDigits; + + @ValidCardFirstSixDigits(message = "Field [first_six_digits] must be exactly 6 digits") + private String firstSixDigits; + + @JsonProperty("telephone_number") + private String telephoneNumber; + + public String toConnectorPayload() { + JsonStringBuilder request = new JsonStringBuilder() + .add("amount", this.getAmount()) + .add("reference", this.getReference()) + .add("description", this.getDescription()) + .add("processor_id", this.getProcessorId()) + .add("provider_id", this.getProviderId()) + .addToMap("payment_outcome", "status", this.getPaymentOutcome().getStatus()); + this.getPaymentOutcome().getCode().ifPresent(code -> request.addToMap("payment_outcome", "code", code)); + this.getPaymentOutcome().getSupplemental().ifPresent(supplemental -> { + supplemental.getErrorCode().ifPresent(errorCode -> request.addToNestedMap("error_code", errorCode, "payment_outcome", "supplemental")); + supplemental.getErrorMessage().ifPresent(errorMessage -> request.addToNestedMap("error_message", errorMessage, "payment_outcome", "supplemental")); + } + ); + + this.getCardExpiry().ifPresent(cardExpiry -> request.add("card_expiry", cardExpiry)); + this.getCreatedDate().ifPresent(createdDate -> request.add("created_date", createdDate)); + this.getAuthorisedDate().ifPresent(authorisedDate -> request.add("authorised_date", authorisedDate)); + this.getAuthCode().ifPresent(authCode -> request.add("auth_code", authCode)); + this.getNameOnCard().ifPresent(nameOnCard -> request.add("name_on_card", nameOnCard)); + this.getEmailAddress().ifPresent(emailAddress -> request.add("email_address", emailAddress)); + this.getTelephoneNumber().ifPresent(telephoneNumber -> request.add("telephone_number", telephoneNumber)); + this.getCardType().ifPresent(cardType -> request.add("card_type", cardType)); + this.getLastFourDigits().ifPresent(lastFourDigits -> request.add("last_four_digits", lastFourDigits)); + this.getFirstSixDigits().ifPresent(firstSixDigits -> request.add("first_six_digits", firstSixDigits)); + + return request.build(); + } + + public CreateTelephonePaymentRequest() { + // To enable Jackson serialisation we need a default constructor + } + + public CreateTelephonePaymentRequest(int amount, String reference, String description, String createdDate, String authorisedDate, String processorId, String providerId, String authCode, PaymentOutcome paymentOutcome, String cardType, String nameOnCard, String emailAddress, String cardExpiry, String lastFourDigits, String firstSixDigits, String telephoneNumber) { + // For testing deserialization + this.amount = amount; + this.reference = reference; + this.description = description; + this.createdDate = createdDate; + this.authorisedDate = authorisedDate; + this.processorId = processorId; + this.providerId = providerId; + this.authCode = authCode; + this.paymentOutcome = paymentOutcome; + this.cardType = cardType; + this.nameOnCard = nameOnCard; + this.emailAddress = emailAddress; + this.cardExpiry = cardExpiry; + this.lastFourDigits = lastFourDigits; + this.firstSixDigits = firstSixDigits; + this.telephoneNumber = telephoneNumber; + } + + public CreateTelephonePaymentRequest(Builder builder) { + this.amount = builder.amount; + this.reference = builder.reference; + this.description = builder.description; + this.createdDate = builder.createdDate; + this.authorisedDate = builder.authorisedDate; + this.processorId = builder.processorId; + this.providerId = builder.providerId; + this.authCode = builder.authCode; + this.paymentOutcome = builder.paymentOutcome; + this.cardType = builder.cardType; + this.nameOnCard = builder.nameOnCard; + this.emailAddress = builder.emailAddress; + this.cardExpiry = builder.cardExpiry; + this.lastFourDigits = builder.lastFourDigits; + this.firstSixDigits = builder.firstSixDigits; + this.telephoneNumber = builder.telephoneNumber; + } + + public int getAmount() { + return amount; + } + + public String getReference() { + return reference; + } + + public String getDescription() { + return description; + } + + public Optional getCreatedDate() { + return Optional.ofNullable(createdDate); + } + + public Optional getAuthorisedDate() { + return Optional.ofNullable(authorisedDate); + } + + public String getProcessorId() { + return processorId; + } + + public String getProviderId() { + return providerId; + } + + public Optional getAuthCode() { + return Optional.ofNullable(authCode); + } + + public PaymentOutcome getPaymentOutcome() { + return paymentOutcome; + } + + public Optional getCardType() { + return Optional.ofNullable(cardType); + } + + public Optional getNameOnCard() { + return Optional.ofNullable(nameOnCard); + } + + public Optional getEmailAddress() { + return Optional.ofNullable(emailAddress); + } + + public Optional getCardExpiry() { + return Optional.ofNullable(cardExpiry); + } + + public Optional getLastFourDigits() { + return Optional.ofNullable(lastFourDigits); + } + + public Optional getFirstSixDigits() { + return Optional.ofNullable(firstSixDigits); + } + + public Optional getTelephoneNumber() { + return Optional.ofNullable(telephoneNumber); + } + + public static class Builder { + private int amount; + + private String reference; + + private String description; + + private String createdDate; + + private String authorisedDate; + + private String processorId; + + private String providerId; + + private String authCode; + + private PaymentOutcome paymentOutcome; + + private String cardType; + + private String nameOnCard; + + private String emailAddress; + + private String cardExpiry; + + private String lastFourDigits; + + private String firstSixDigits; + + private String telephoneNumber; + + public Builder withAmount(int amount) { + this.amount = amount; + return this; + } + + public Builder withReference(String reference) { + this.reference = reference; + return this; + } + + public Builder withDescription(String description) { + this.description = description; + return this; + } + + public Builder withCreatedDate(String createdDate) { + this.createdDate = createdDate; + return this; + } + + public Builder withAuthorisedDate(String authorisedDate) { + this.authorisedDate = authorisedDate; + return this; + } + + public Builder withProcessorId(String processorId) { + this.processorId = processorId; + return this; + } + + public Builder withProviderId(String providerId) { + this.providerId = providerId; + return this; + } + + public Builder withAuthCode(String authCode) { + this.authCode = authCode; + return this; + } + + public Builder withPaymentOutcome(PaymentOutcome paymentOutcome) { + this.paymentOutcome = paymentOutcome; + return this; + } + + public Builder withCardType(String cardType) { + this.cardType = cardType; + return this; + } + + public Builder withNameOnCard(String nameOnCard) { + this.nameOnCard = nameOnCard; + return this; + } + + public Builder withEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + return this; + } + + public Builder withCardExpiry(String cardExpiry) { + this.cardExpiry = cardExpiry; + return this; + } + + public Builder withLastFourDigits(String lastFourDigits) { + this.lastFourDigits = lastFourDigits; + return this; + } + + public Builder withFirstSixDigits(String firstSixDigits) { + this.firstSixDigits = firstSixDigits; + return this; + } + + public Builder withTelephoneNumber(String telephoneNumber) { + this.telephoneNumber = telephoneNumber; + return this; + } + + public CreateTelephonePaymentRequest build() { + return new CreateTelephonePaymentRequest(this); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/telephone/PaymentOutcome.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/telephone/PaymentOutcome.java new file mode 100644 index 000000000..5df843c16 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/telephone/PaymentOutcome.java @@ -0,0 +1,50 @@ +package uk.gov.pay.api.model.telephone; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import uk.gov.pay.api.validation.ValidPaymentOutcome; + +import java.util.Optional; + +@ValidPaymentOutcome(message = "Field [payment_outcome] must include a valid status and error code") +public class PaymentOutcome { + + @JsonProperty("status") + private String status; + + @JsonProperty("code") + private String code; + + @JsonProperty("supplemental") + @JsonInclude(JsonInclude.Include.NON_ABSENT) + private Supplemental supplemental; + + public PaymentOutcome() { + } + + public PaymentOutcome(String status) { + this.status = status; + } + + public PaymentOutcome(String status, String code, Supplemental supplemental) { + // For testing deserialization + this.status = status; + this.code = code; + this.supplemental = supplemental; + } + + public String getStatus() { + return status; + } + + @JsonIgnore + public Optional getCode() { + return Optional.ofNullable(code); + } + + @JsonIgnore + public Optional getSupplemental() { + return Optional.ofNullable(supplemental); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/telephone/Supplemental.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/telephone/Supplemental.java new file mode 100644 index 000000000..1552bb62b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/telephone/Supplemental.java @@ -0,0 +1,37 @@ +package uk.gov.pay.api.model.telephone; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import java.util.Optional; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class Supplemental { + + @JsonProperty("error_code") + private String errorCode; + + @JsonProperty("error_message") + private String errorMessage; + + public Supplemental() { + } + + public Supplemental(String errorCode, String errorMessage) { + // For testing deserialization + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } + + @JsonIgnore + public Optional getErrorCode() { + return Optional.ofNullable(errorCode); + } + + @JsonIgnore + public Optional getErrorMessage() { + return Optional.ofNullable(errorMessage); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/telephone/TelephonePaymentResponse.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/telephone/TelephonePaymentResponse.java new file mode 100644 index 000000000..32d850d1d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/model/telephone/TelephonePaymentResponse.java @@ -0,0 +1,334 @@ +package uk.gov.pay.api.model.telephone; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import uk.gov.pay.api.model.ChargeFromResponse; +import uk.gov.pay.api.model.PaymentState; + +import java.util.Optional; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class TelephonePaymentResponse { + + private Long amount; + + private String reference; + + private String description; + + @JsonProperty("created_date") + private String createdDate; + + @JsonProperty("authorised_date") + private String authorisedDate; + + private String processorId; + + private String providerId; + + @JsonProperty("auth_code") + private String authCode; + + private PaymentOutcome paymentOutcome; + + private String cardType; + + @JsonProperty("name_on_card") + private String nameOnCard; + + @JsonProperty("email_address") + private String emailAddress; + + private String cardExpiry; + + private String lastFourDigits; + + private String firstSixDigits; + + @JsonProperty("telephone_number") + private String telephoneNumber; + + private String paymentId; + + private PaymentState state; + + public TelephonePaymentResponse() { + // For Jackson + } + + public TelephonePaymentResponse(Builder builder) { + this.amount = builder.amount; + this.reference = builder.reference; + this.description = builder.description; + this.createdDate = builder.createdDate; + this.authorisedDate = builder.authorisedDate; + this.processorId = builder.processorId; + this.providerId = builder.providerId; + this.authCode = builder.authCode; + this.paymentOutcome = builder.paymentOutcome; + this.cardType = builder.cardType; + this.nameOnCard = builder.nameOnCard; + this.emailAddress = builder.emailAddress; + this.cardExpiry = builder.cardExpiry; + this.lastFourDigits = builder.lastFourDigits; + this.firstSixDigits = builder.firstSixDigits; + this.telephoneNumber = builder.telephoneNumber; + this.paymentId = builder.paymentId; + this.state = builder.state; + } + + public TelephonePaymentResponse(Long amount, + String reference, + String description, + String createdDate, + String authorisedDate, + String processorId, + String providerId, + String authCode, + PaymentOutcome paymentOutcome, + String cardType, + String nameOnCard, + String emailAddress, + String cardExpiry, + String lastFourDigits, + String firstSixDigits, + String telephoneNumber, + String paymentId, + PaymentState state) { + // For testing serialization + this.amount = amount; + this.reference = reference; + this.description = description; + this.createdDate = createdDate; + this.authorisedDate = authorisedDate; + this.processorId = processorId; + this.providerId = providerId; + this.authCode = authCode; + this.paymentOutcome = paymentOutcome; + this.cardType = cardType; + this.nameOnCard = nameOnCard; + this.emailAddress = emailAddress; + this.cardExpiry = cardExpiry; + this.lastFourDigits = lastFourDigits; + this.firstSixDigits = firstSixDigits; + this.telephoneNumber = telephoneNumber; + this.paymentId = paymentId; + this.state = state; + } + + public Long getAmount() { + return amount; + } + + public String getReference() { + return reference; + } + + public String getDescription() { + return description; + } + + @JsonIgnore + public Optional getCreatedDate() { + return Optional.ofNullable(createdDate); + } + + @JsonIgnore + public Optional getAuthorisedDate() { + return Optional.ofNullable(authorisedDate); + } + + public String getProcessorId() { + return processorId; + } + + public String getProviderId() { + return providerId; + } + + @JsonIgnore + public Optional getAuthCode() { + return Optional.ofNullable(authCode); + } + + public PaymentOutcome getPaymentOutcome() { + return paymentOutcome; + } + + public String getCardType() { + return cardType; + } + + @JsonIgnore + public Optional getNameOnCard() { + return Optional.ofNullable(nameOnCard); + } + + @JsonIgnore + public Optional getEmailAddress() { + return Optional.ofNullable(emailAddress); + } + + public String getCardExpiry() { + return cardExpiry; + } + + public String getLastFourDigits() { + return lastFourDigits; + } + + public String getFirstSixDigits() { + return firstSixDigits; + } + + @JsonIgnore + public Optional getTelephoneNumber() { + return Optional.ofNullable(telephoneNumber); + } + + public String getPaymentId() { return paymentId; } + + public PaymentState getState() { + return state; + } + + public static TelephonePaymentResponse from(ChargeFromResponse chargeFromResponse) { + return new TelephonePaymentResponse.Builder() + .withAmount(chargeFromResponse.getAmount()) + .withDescription(chargeFromResponse.getDescription()) + .withReference(chargeFromResponse.getReference()) + .withCreatedDate(chargeFromResponse.getCreatedDate()) + .withAuthorisedDate(chargeFromResponse.getAuthorisedDate()) + .withProcessorId(chargeFromResponse.getProcessorId()) + .withProviderId(chargeFromResponse.getProviderId()) + .withAuthCode(chargeFromResponse.getAuthCode()) + .withPaymentOutcome(chargeFromResponse.getPaymentOutcome()) + .withCardType(chargeFromResponse.getCardBrand()) + .withEmailAddress(chargeFromResponse.getEmail()) + .withNameOnCard(chargeFromResponse.getCardDetailsFromResponse() != null ? chargeFromResponse.getCardDetailsFromResponse().getCardHolderName() : null) + .withCardExpiry(chargeFromResponse.getCardDetailsFromResponse() != null ? chargeFromResponse.getCardDetailsFromResponse().getExpiryDate() : null) + .withLastFourDigits(chargeFromResponse.getCardDetailsFromResponse() != null ? chargeFromResponse.getCardDetailsFromResponse().getLastDigitsCardNumber() : null) + .withFirstSixDigits(chargeFromResponse.getCardDetailsFromResponse() != null ? chargeFromResponse.getCardDetailsFromResponse().getFirstDigitsCardNumber() : null) + .withTelephoneNumber(chargeFromResponse.getTelephoneNumber()) + .withPaymentId(chargeFromResponse.getChargeId()) + .withState(chargeFromResponse.getState()) + .build(); + } + + public static class Builder { + private Long amount; + private String reference; + private String description; + private String createdDate; + private String authorisedDate; + private String processorId; + private String providerId; + private String authCode; + private PaymentOutcome paymentOutcome; + private String cardType; + private String nameOnCard; + private String emailAddress; + private String cardExpiry; + private String lastFourDigits; + private String firstSixDigits; + private String telephoneNumber; + private String paymentId; + private PaymentState state; + + public Builder withAmount(Long amount) { + this.amount = amount; + return this; + } + + public Builder withReference(String reference) { + this.reference = reference; + return this; + } + + public Builder withDescription(String description) { + this.description = description; + return this; + } + + public Builder withCreatedDate(String createdDate) { + this.createdDate = createdDate; + return this; + } + + public Builder withAuthorisedDate(String authorisedDate) { + this.authorisedDate = authorisedDate; + return this; + } + + public Builder withProcessorId(String processorId) { + this.processorId = processorId; + return this; + } + + public Builder withProviderId(String providerId) { + this.providerId = providerId; + return this; + } + + public Builder withAuthCode(String authCode) { + this.authCode = authCode; + return this; + } + + public Builder withPaymentOutcome(PaymentOutcome paymentOutcome) { + this.paymentOutcome = paymentOutcome; + return this; + } + + public Builder withCardType(String cardType) { + this.cardType = cardType; + return this; + } + + public Builder withNameOnCard(String nameOnCard) { + this.nameOnCard = nameOnCard; + return this; + } + + public Builder withEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + return this; + } + + public Builder withCardExpiry(String cardExpiry) { + this.cardExpiry = cardExpiry; + return this; + } + + public Builder withLastFourDigits(String lastFourDigits) { + this.lastFourDigits = lastFourDigits; + return this; + } + + public Builder withFirstSixDigits(String firstSixDigits) { + this.firstSixDigits = firstSixDigits; + return this; + } + + public Builder withTelephoneNumber(String telephoneNumber) { + this.telephoneNumber = telephoneNumber; + return this; + } + + public Builder withPaymentId(String paymentId) { + this.paymentId = paymentId; + return this; + } + + public Builder withState(PaymentState state) { + this.state = state; + return this; + } + + public TelephonePaymentResponse build () { + return new TelephonePaymentResponse(this); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/AuthorisationResource.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/AuthorisationResource.java new file mode 100644 index 000000000..c9e442fb8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/AuthorisationResource.java @@ -0,0 +1,65 @@ +package uk.gov.pay.api.resources; + +import com.codahale.metrics.annotation.Timed; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.model.AuthorisationRequest; +import uk.gov.pay.api.model.RequestError; +import uk.gov.pay.api.resources.error.ApiErrorResponse; +import uk.gov.pay.api.service.AuthorisationService; + +import javax.inject.Inject; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +@Path("/") +@Produces({"application/json"}) +@Tag(name = "Authorise card payments") +public class AuthorisationResource { + + private static final Logger logger = LoggerFactory.getLogger(AuthorisationResource.class); + private final AuthorisationService authorisationService; + + @Inject + public AuthorisationResource(AuthorisationService authorisationService) { + this.authorisationService = authorisationService; + } + + @POST + @Timed + @Path("/v1/auth") + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + @Operation(operationId = "Authorise a MOTO payment", + summary = "Send card details to authorise a MOTO payment", + description = "You can use this endpoint to [authorise payments](https://docs.payments.service.gov.uk/moto_payments/moto_send_card_details_api/) you have created with `authorisation_mode` set to `moto_api`.", + responses = { + @ApiResponse(responseCode = "204", description = "Your authorisation request was successful."), + @ApiResponse(responseCode = "400", description = "Your request is invalid. Check the `code` and `description` in the response to find out why your request failed.", + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "402", + description = "The `card_number` you sent is not a valid card number or you chose not to accept this card type. Check the `code` and `description` fields in the response to find out why your request failed.", + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "422", description = "A value you sent is invalid or missing. Check the `code` and `description` in the response to find out why your request failed.", + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "There is something wrong with GOV.UK Pay. If there are no issues on our status page (https://payments.statuspage.io), you can contact us with your error code and we'll investigate.", + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public Response authorisePayment(@Parameter(required = true) + @Valid AuthorisationRequest authorisationRequest) { + return authorisationService.authoriseRequest(authorisationRequest); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/GetOnePaymentStrategy.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/GetOnePaymentStrategy.java new file mode 100644 index 000000000..84ef9c7fc --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/GetOnePaymentStrategy.java @@ -0,0 +1,34 @@ +package uk.gov.pay.api.resources; + +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.links.PaymentWithAllLinks; +import uk.gov.pay.api.service.GetPaymentService; + +public class GetOnePaymentStrategy extends LedgerOrConnectorStrategyTemplate { + + private final Account account; + private final String paymentId; + private final GetPaymentService getPaymentService; + + public GetOnePaymentStrategy(String strategy, Account account, String paymentId, GetPaymentService getPaymentService) { + super(strategy); + this.account = account; + this.paymentId = paymentId; + this.getPaymentService = getPaymentService; + } + + @Override + protected PaymentWithAllLinks executeLedgerOnlyStrategy() { + return getPaymentService.getLedgerTransaction(account, paymentId); + } + + @Override + protected PaymentWithAllLinks executeDefaultStrategy() { + return getPaymentService.getPayment(account, paymentId); + } + + @Override + protected PaymentWithAllLinks executeConnectorOnlyStrategy() { + return getPaymentService.getConnectorCharge(account, paymentId); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/GetPaymentEventsStrategy.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/GetPaymentEventsStrategy.java new file mode 100644 index 000000000..d6945e362 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/GetPaymentEventsStrategy.java @@ -0,0 +1,34 @@ +package uk.gov.pay.api.resources; + +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.PaymentEventsResponse; +import uk.gov.pay.api.service.GetPaymentEventsService; + +public class GetPaymentEventsStrategy extends LedgerOrConnectorStrategyTemplate { + private final Account account; + private final String paymentId; + private final GetPaymentEventsService getPaymentEventsService; + + public GetPaymentEventsStrategy(String strategyName, Account account, String paymentId, + GetPaymentEventsService getPaymentEventsService) { + super(strategyName); + this.account = account; + this.paymentId = paymentId; + this.getPaymentEventsService = getPaymentEventsService; + } + + @Override + protected PaymentEventsResponse executeLedgerOnlyStrategy() { + return getPaymentEventsService.getPaymentEventsFromLedger(account, paymentId); + } + + @Override + protected PaymentEventsResponse executeDefaultStrategy() { + return getPaymentEventsService.getPaymentEvents(account, paymentId); + } + + @Override + protected PaymentEventsResponse executeConnectorOnlyStrategy() { + return getPaymentEventsService.getPaymentEventsFromConnector(account, paymentId); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/GetPaymentRefundStrategy.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/GetPaymentRefundStrategy.java new file mode 100644 index 000000000..50bc5028f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/GetPaymentRefundStrategy.java @@ -0,0 +1,37 @@ +package uk.gov.pay.api.resources; + +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.RefundResponse; +import uk.gov.pay.api.service.GetPaymentRefundService; + +public class GetPaymentRefundStrategy extends LedgerOrConnectorStrategyTemplate { + + private final Account account; + private final String paymentId; + private final String refundId; + private final GetPaymentRefundService getPaymentRefundsService; + + public GetPaymentRefundStrategy(String strategy, Account account, String paymentId, String refundId, + GetPaymentRefundService getPaymentRefundsService) { + super(strategy); + this.account = account; + this.paymentId = paymentId; + this.refundId = refundId; + this.getPaymentRefundsService = getPaymentRefundsService; + } + + @Override + protected RefundResponse executeLedgerOnlyStrategy() { + return getPaymentRefundsService.getLedgerPaymentRefund(account, paymentId, refundId); + } + + @Override + protected RefundResponse executeDefaultStrategy() { + return getPaymentRefundsService.getPaymentRefund(account, paymentId, refundId); + } + + @Override + protected RefundResponse executeConnectorOnlyStrategy() { + return getPaymentRefundsService.getConnectorPaymentRefund(account, paymentId, refundId); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/GetPaymentRefundsStrategy.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/GetPaymentRefundsStrategy.java new file mode 100644 index 000000000..9504c840e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/GetPaymentRefundsStrategy.java @@ -0,0 +1,35 @@ +package uk.gov.pay.api.resources; + +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.RefundsResponse; +import uk.gov.pay.api.service.GetPaymentRefundsService; + +public class GetPaymentRefundsStrategy extends LedgerOrConnectorStrategyTemplate { + + private final Account account; + private final String paymentId; + private final GetPaymentRefundsService getPaymentRefundsService; + + public GetPaymentRefundsStrategy(String strategy, Account account, String paymentId, + GetPaymentRefundsService getPaymentRefundsService) { + super(strategy); + this.account = account; + this.paymentId = paymentId; + this.getPaymentRefundsService = getPaymentRefundsService; + } + + @Override + protected RefundsResponse executeLedgerOnlyStrategy() { + return getPaymentRefundsService.getLedgerTransactionTransactions(account, paymentId); + } + + @Override + protected RefundsResponse executeDefaultStrategy() { + return executeLedgerOnlyStrategy(); + } + + @Override + protected RefundsResponse executeConnectorOnlyStrategy() { + return getPaymentRefundsService.getConnectorPaymentRefunds(account, paymentId); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/HealthCheckResource.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/HealthCheckResource.java new file mode 100644 index 000000000..53d213073 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/HealthCheckResource.java @@ -0,0 +1,59 @@ +package uk.gov.pay.api.resources; + +import com.codahale.metrics.annotation.Timed; +import com.codahale.metrics.health.HealthCheck; +import com.google.common.collect.ImmutableMap; +import io.dropwizard.setup.Environment; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; +import java.util.SortedMap; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static javax.ws.rs.core.Response.status; + +@Path("/") +public class HealthCheckResource { + public static final String HEALTHCHECK = "healthcheck"; + public static final String HEALTHY = "healthy"; + + Environment environment; + + @Inject + public HealthCheckResource(Environment environment) { + this.environment = environment; + } + + @GET + @Timed + @Path(HEALTHCHECK) + @Produces(APPLICATION_JSON) + public Response healthCheck() { + SortedMap results = environment.healthChecks().runHealthChecks(); + + Map> response = getResponse(results); + + boolean healthy = results.size() == results.values() + .stream() + .filter(HealthCheck.Result::isHealthy) + .count(); + + if(healthy) { + return Response.ok().entity(response).build(); + } + return status(503).entity(response).build(); + } + + private Map> getResponse(SortedMap results) { + Map> response = new HashMap<>(); + for (SortedMap.Entry entry : results.entrySet() ) { + response.put(entry.getKey(), ImmutableMap.of(HEALTHY, entry.getValue().isHealthy())); + } + return response; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/LedgerOrConnectorStrategyTemplate.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/LedgerOrConnectorStrategyTemplate.java new file mode 100644 index 000000000..29d178fa4 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/LedgerOrConnectorStrategyTemplate.java @@ -0,0 +1,46 @@ +package uk.gov.pay.api.resources; + +import com.google.common.collect.ImmutableList; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public abstract class LedgerOrConnectorStrategyTemplate { + + private static final Logger logger = LoggerFactory.getLogger(LedgerOrConnectorStrategyTemplate.class); + private String strategy; + private List VALID_STRATEGIES = ImmutableList.of("ledger-only", "connector-only"); + + public LedgerOrConnectorStrategyTemplate(String strategy) { + this.strategy = strategy; + } + + private void validate() { + if (!StringUtils.isBlank(strategy) && !VALID_STRATEGIES.contains(strategy)) { + logger.warn("Not valid strategy (valid values are \"ledger-only\", \"connector-only\" or empty); using the default strategy"); + strategy = null; + } + } + + public T validateAndExecute() { + validate(); + + if ("connector-only".equals(strategy)) { + return executeConnectorOnlyStrategy(); + } else if ("ledger-only".equals(strategy)) { + return executeLedgerOnlyStrategy(); + } + + return executeDefaultStrategy(); + } + + protected abstract T executeLedgerOnlyStrategy(); + + protected abstract T executeDefaultStrategy(); + + protected abstract T executeConnectorOnlyStrategy(); +} + + diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/PaymentRefundsResource.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/PaymentRefundsResource.java new file mode 100644 index 000000000..5fb8f489f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/PaymentRefundsResource.java @@ -0,0 +1,175 @@ +package uk.gov.pay.api.resources; + +import com.codahale.metrics.annotation.Timed; +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.CreatePaymentRefundRequest; +import uk.gov.pay.api.model.RefundResponse; +import uk.gov.pay.api.model.RefundsResponse; +import uk.gov.pay.api.model.RequestError; +import uk.gov.pay.api.model.search.card.RefundForSearchResult; +import uk.gov.pay.api.model.search.card.RefundResult; +import uk.gov.pay.api.resources.error.ApiErrorResponse; +import uk.gov.pay.api.service.ConnectorService; +import uk.gov.pay.api.service.CreateRefundService; +import uk.gov.pay.api.service.GetPaymentRefundService; +import uk.gov.pay.api.service.GetPaymentRefundsService; +import uk.gov.pay.api.service.GetPaymentService; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_200_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_401_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_404_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_429_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_500_DESCRIPTION; + +@Path("/v1/payments/{paymentId}/refunds") +@Tag(name = "Refunding card payments") +@Produces({"application/json"}) +public class PaymentRefundsResource { + private static final Logger logger = LoggerFactory.getLogger(PaymentRefundsResource.class); + + private final GetPaymentRefundsService getPaymentRefundsService; + private final GetPaymentRefundService getPaymentRefundService; + private final CreateRefundService createRefundService; + + @Inject + public PaymentRefundsResource(PublicApiConfig configuration, + GetPaymentRefundsService getPaymentRefundsService, + GetPaymentRefundService getPaymentRefundService, + ConnectorService connectorService, + GetPaymentService getPaymentService, + CreateRefundService createRefundService) { + this.getPaymentRefundsService = getPaymentRefundsService; + this.getPaymentRefundService = getPaymentRefundService; + this.createRefundService = createRefundService; + } + + @GET + @Timed + @Produces(APPLICATION_JSON) + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Get all refunds for a payment", + summary = "Get information about a payment’s refunds", + description = "You can use this endpoint to [get a list of refunds for a payment]" + + "(https://docs.payments.service.gov.uk/refunding_payments/#get-all-refunds-for-a-single-payment).", + responses = { + @ApiResponse(responseCode = "200", description = RESPONSE_200_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RefundForSearchResult.class))), + @ApiResponse(responseCode = "401", description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "404", description = RESPONSE_404_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public RefundsResponse getRefunds(@Parameter(hidden = true) @Auth Account account, + @PathParam("paymentId") @Parameter(name = "paymentId", + description = "The unique `payment_id` of the payment you want a list of refunds for.") String paymentId, + @Parameter(hidden = true) @HeaderParam("X-Ledger") String strategyName) { + + logger.info("Get refunds for payment request - paymentId={} using strategy={}", paymentId, strategyName); + + GetPaymentRefundsStrategy strategy = new GetPaymentRefundsStrategy(strategyName, account, paymentId, getPaymentRefundsService); + RefundsResponse refundsResponse = strategy.validateAndExecute(); + + logger.debug("refund returned - [ {} ]", refundsResponse); + return refundsResponse; + } + + @GET + @Timed + @Path("/{refundId}") + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Get a payment refund", + summary = "Check the status of a refund", + description = "You can use this endpoint to [get details about an individual refund]" + + "(https://docs.payments.service.gov.uk/refunding_payments/#checking-the-status-of-a-refund).", + responses = { + @ApiResponse(responseCode = "200", description = RESPONSE_200_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RefundResult.class))), + @ApiResponse(responseCode = "401", description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "404", description = RESPONSE_404_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + @Produces(APPLICATION_JSON) + public RefundResponse getRefundById(@Parameter(hidden = true) + @Auth Account account, + @PathParam("paymentId") @Parameter(name = "paymentId", + description = "The unique `payment_id` of the payment you want to view a refund of.") String paymentId, + @PathParam("refundId") @Parameter(name = "refundId", + description = "The unique `refund_id` of the refund you want to view. " + + "If one payment has multiple refunds, each refund has a different `refund_id`.") String refundId, + @Parameter(hidden = true) @HeaderParam("X-Ledger") String strategyName) { + + logger.info("Payment refund request - paymentId={}, refundId={}", paymentId, refundId); + + var strategy = new GetPaymentRefundStrategy(strategyName, account, paymentId, refundId, getPaymentRefundService); + RefundResponse refundResponse = strategy.validateAndExecute(); + + logger.info("refund returned - [ {} ]", refundResponse); + + return refundResponse; + } + + @POST + @Timed + @Produces(APPLICATION_JSON) + @Consumes(APPLICATION_JSON) + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Submit a refund for a payment", + summary = "Refund a payment", + description = "You can use this endpoint to [fully or partially refund a payment]" + + "(https://docs.payments.service.gov.uk/refunding_payments).", + responses = { + @ApiResponse(responseCode = "200", description = "successful operation", + content = @Content(schema = @Schema(implementation = RefundResult.class))), + @ApiResponse(responseCode = "202", description = "ACCEPTED"), + @ApiResponse(responseCode = "401", description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "404", description = RESPONSE_404_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "412", description = "Refund amount available mismatch"), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public Response submitRefund(@Parameter(hidden = true) @Auth Account account, + @PathParam("paymentId") @Parameter(name = "paymentId", + description = "The unique `payment_id` of the payment you want to refund.") String paymentId, + @Parameter(required = true, description = "requestPayload") + CreatePaymentRefundRequest requestPayload) { + + logger.info("Create a refund for payment request - paymentId={}", paymentId); + RefundResponse refundResponse = createRefundService.createRefund(account, paymentId, requestPayload); + return Response.accepted(refundResponse).build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/PaymentsResource.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/PaymentsResource.java new file mode 100644 index 000000000..4d51854ef --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/PaymentsResource.java @@ -0,0 +1,407 @@ +package uk.gov.pay.api.resources; + +import com.codahale.metrics.annotation.Timed; +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.apache.http.HttpStatus; +import org.hibernate.validator.constraints.Length; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.CaptureChargeException; +import uk.gov.pay.api.model.CreateCardPaymentRequest; +import uk.gov.pay.api.model.CreatePaymentResult; +import uk.gov.pay.api.model.CreatedPaymentWithAllLinks; +import uk.gov.pay.api.model.PaymentEventsResponse; +import uk.gov.pay.api.model.RequestError; +import uk.gov.pay.api.model.links.PaymentWithAllLinks; +import uk.gov.pay.api.model.search.card.PaymentSearchResults; +import uk.gov.pay.api.resources.error.ApiErrorResponse; +import uk.gov.pay.api.service.CancelPaymentService; +import uk.gov.pay.api.service.CapturePaymentService; +import uk.gov.pay.api.service.CreatePaymentService; +import uk.gov.pay.api.service.GetPaymentEventsService; +import uk.gov.pay.api.service.GetPaymentService; +import uk.gov.pay.api.service.PaymentSearchParams; +import uk.gov.pay.api.service.PaymentSearchService; +import uk.gov.pay.api.service.PublicApiUriGenerator; + +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.validation.Valid; +import javax.validation.constraints.Pattern; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import static java.lang.String.format; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.apache.http.HttpHeaders.CACHE_CONTROL; +import static org.apache.http.HttpHeaders.PRAGMA; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_200_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_400_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_401_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_404_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_409_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_422_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_429_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_500_DESCRIPTION; + +@Path("/") +@Tag(name = "Card payments") +@Produces({"application/json"}) +public class PaymentsResource { + + private static final Logger logger = LoggerFactory.getLogger(PaymentsResource.class); + + private final CreatePaymentService createPaymentService; + private final PublicApiUriGenerator publicApiUriGenerator; + private final PaymentSearchService paymentSearchService; + private final GetPaymentService getPaymentService; + private final CapturePaymentService capturePaymentService; + private final CancelPaymentService cancelPaymentService; + private final GetPaymentEventsService getPaymentEventsService; + + @Inject + public PaymentsResource(CreatePaymentService createPaymentService, + PaymentSearchService paymentSearchService, + PublicApiUriGenerator publicApiUriGenerator, + GetPaymentService getPaymentService, + CapturePaymentService capturePaymentService, + CancelPaymentService cancelPaymentService, + GetPaymentEventsService getPaymentEventsService) { + this.createPaymentService = createPaymentService; + this.publicApiUriGenerator = publicApiUriGenerator; + this.paymentSearchService = paymentSearchService; + this.getPaymentService = getPaymentService; + this.capturePaymentService = capturePaymentService; + this.cancelPaymentService = cancelPaymentService; + this.getPaymentEventsService = getPaymentEventsService; + } + + @GET + @Timed + @Path("/v1/payments/{paymentId}") + @Produces(APPLICATION_JSON) + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Get a payment", + summary = "Get information about a single payment", + description = "You can use this endpoint to [get details about a single payment you’ve previously created]" + + "(https://docs.payments.service.gov.uk/reporting/#get-information-about-a-single-payment).", + responses = { + @ApiResponse(responseCode = "200", description = RESPONSE_200_DESCRIPTION, + content = @Content(schema = @Schema(implementation = PaymentWithAllLinks.class))), + @ApiResponse(responseCode = "401", + description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "404", description = RESPONSE_404_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public Response getPayment(@Parameter(hidden = true) @Auth Account account, + @PathParam("paymentId") + @Parameter(name = "paymentId", description = "Returns the payment with the matching `payment_id`.", example = "hu20sqlact5260q2nanm0q8u93") + String paymentId, + @Parameter(hidden = true) @HeaderParam("X-Ledger") String strategyName) { + + logger.info("Payment request - paymentId={}", paymentId); + + var strategy = new GetOnePaymentStrategy(strategyName, account, paymentId, getPaymentService); + PaymentWithAllLinks payment = strategy.validateAndExecute(); + + logger.info("Payment returned - [ {} ]", payment); + return Response.ok(payment) + .header(PRAGMA, "no-cache") + .header(CACHE_CONTROL, "no-store") + .build(); + } + + @GET + @Timed + @Path("/v1/payments/{paymentId}/events") + @Produces(APPLICATION_JSON) + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Get events for a payment", + summary = "Get a payment's events", + description = "You can use this endpoint to " + + "[get a list of a payment’s events](https://docs.payments.service.gov.uk/reporting/#get-a-payment-s-events). " + + "A payment event is when a payment’s `state` changes, such as when the payment is created, or when the paying user submits their details.", + responses = { + @ApiResponse(responseCode = "200", description = RESPONSE_200_DESCRIPTION, + content = @Content(schema = @Schema(implementation = PaymentEventsResponse.class))), + @ApiResponse(responseCode = "401", + description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "404", description = RESPONSE_404_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public PaymentEventsResponse getPaymentEvents(@Parameter(hidden = true) @Auth Account account, + @PathParam("paymentId") + @Parameter(name = "paymentId", description = "Payment identifier", example = "hu20sqlact5260q2nanm0q8u93") + String paymentId, + @Parameter(hidden = true) @HeaderParam("X-Ledger") String strategyName) { + + logger.info("Payment events request - payment_id={}", paymentId); + + var strategy = new GetPaymentEventsStrategy(strategyName, account, paymentId, getPaymentEventsService); + PaymentEventsResponse paymentEventsResponse = strategy.validateAndExecute(); + + logger.info("Payment events returned - [ {} ]", paymentEventsResponse); + + return paymentEventsResponse; + } + + @GET + @Timed + @Path("/v1/payments") + @Produces(APPLICATION_JSON) + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Search payments", + summary = "Search payments", + description = "You can use this endpoint to [search for payments you’ve previously created](https://docs.payments.service.gov.uk/reporting/#search-payments/). " + + "Payments are sorted by date, with the most recently-created payment appearing first.", + responses = { + @ApiResponse(responseCode = "200", description = RESPONSE_200_DESCRIPTION, + content = @Content(schema = @Schema(implementation = PaymentSearchResults.class))), + @ApiResponse(responseCode = "401", + description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "422", + description = "Invalid parameters: from_date, to_date, status, display_size. See Public API documentation for the correct data formats", + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public Response searchPayments(@Parameter(hidden = true) + @Auth Account account, + @Parameter(description = "Returns payments with `reference` values exactly matching your specified value.") + @QueryParam("reference") String reference, + @Parameter(description = "Returns payments with matching `email` values. You can send full or partial email addresses. " + + "`email` is the paying user’s email address.") + @QueryParam("email") String email, + @Parameter(description = "Returns payments in a matching `state`. `state` reflects where a payment is in the " + + "[payment status lifecycle](https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle).", example = "success", + schema = @Schema(allowableValues = {"created", "started", "submitted", "success", "failed", "cancelled", "error"})) + @QueryParam("state") String state, + @Parameter(description = "Returns payments paid with a particular card brand.") + @QueryParam("card_brand") String cardBrand, + @Parameter(description = "Returns payments created on or after the `from_date`. " + + "Date and time must be coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", example = "2015-08-13T12:35:00Z") + @QueryParam("from_date") String fromDate, + @Parameter(description = "Returns payments created before the `to_date`. " + + "Date and time must be coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", example = "2015-08-13T12:35:00Z") + @QueryParam("to_date") String toDate, + @Parameter(description = "Returns a [specific page of results](https://docs.payments.service.gov.uk/api_reference/#pagination). Defaults to `1`.") + @QueryParam("page") String pageNumber, + @Parameter(description = "The number of payments returned " + + "[per results page](https://docs.payments.service.gov.uk/api_reference/#pagination). Defaults to `500`. Maximum value is `500`.") + @QueryParam("display_size") String displaySize, + @Parameter(description = "Returns payments paid with cards under this cardholder name.") + @QueryParam("cardholder_name") String cardHolderName, + @Parameter(description = "Returns payments paid by cards beginning with the `first_digits_card_number` value. " + + "`first_digits_card_number` value must be 6 digits.") + @QueryParam("first_digits_card_number") String firstDigitsCardNumber, + @Parameter(description = "Returns payments paid by cards ending with the `last_digits_card_number` value. " + + "`last_digits_card_number` value must be 4 digits.", hidden = false) + @QueryParam("last_digits_card_number") String lastDigitsCardNumber, + @Parameter(description = "Returns payments settled on or after the `from_settled_date` value. " + + "You can only search by settled date if your payment service provider is Stripe. " + + "Date must be in ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. " + + "Payments are settled when your payment service provider sends funds to your bank account.") + @QueryParam("from_settled_date") String fromSettledDate, + @Parameter(description = "Returns payments settled before the `to_settled_date` value. " + + "You can only search by settled date if your payment service provider is Stripe. " + + "Date must be in ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. " + + "Payments are settled when your payment service provider sends funds to your bank account.") + @QueryParam("to_settled_date") String toSettledDate, + @Parameter(description = "Returns payments that were authorised using the agreement with this `agreement_id`. " + + "Must be an exact match.", example = "abcefghjklmnopqr1234567890") + @QueryParam("agreement_id") String agreementId, + @Context UriInfo uriInfo) { + + logger.info("Payments search request - [ {} ]", + format("reference:%s, email: %s, status: %s, card_brand %s, fromDate: %s, toDate: %s, page: %s, " + + "display_size: %s, cardholder_name: %s, first_digits_card_number: %s, " + + "last_digits_card_number: %s, from_settled_date: %s, to_settled_date: %s, agreement_id: %s", + reference, email, state, cardBrand, fromDate, toDate, pageNumber, displaySize, + cardHolderName, firstDigitsCardNumber, lastDigitsCardNumber, fromSettledDate, toSettledDate, agreementId)); + + var paymentSearchParams = new PaymentSearchParams.Builder() + .withReference(reference) + .withEmail(email) + .withState(state) + .withCardBrand(cardBrand) + .withFromDate(fromDate) + .withToDate(toDate) + .withPageNumber(pageNumber) + .withDisplaySize(displaySize) + .withCardHolderName(cardHolderName) + .withFirstDigitsCardNumber(firstDigitsCardNumber) + .withLastDigitsCardNumber(lastDigitsCardNumber) + .withFromSettledDate(fromSettledDate) + .withToSettledDate(toSettledDate) + .withAgreementId(agreementId) + .build(); + + return paymentSearchService.searchLedgerPayments(account, paymentSearchParams); + } + + @POST + @Timed + @Path("/v1/payments") + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Create a payment", + summary = "Create a payment", + description = "You can use this endpoint to [create a new payment](https://docs.payments.service.gov.uk/making_payments/).", + responses = { + @ApiResponse(responseCode = "201", description = "Created", + content = @Content(schema = @Schema(implementation = CreatePaymentResult.class))), + @ApiResponse(responseCode = "400", description = RESPONSE_400_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "401", + description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "422", + description = RESPONSE_422_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public Response createNewPayment(@Parameter(hidden = true) @Auth Account account, + @Parameter(required = true, description = "requestPayload") + @Valid CreateCardPaymentRequest createCardPaymentRequest, + @Nullable + @Length(min = 1, max = 255, message = "Header [Idempotency-Key] can have a size between 1 and 255") + @Pattern(regexp = "^$|^[a-zA-Z0-9-]+$", message = "Header [Idempotency-Key] can only contain alphanumeric characters and hyphens") + @HeaderParam("Idempotency-Key") + String idempotencyKey) { + logger.info("Payment create request parsed to {}", createCardPaymentRequest); + + CreatedPaymentWithAllLinks createdPayment = createPaymentService.create(account, createCardPaymentRequest, idempotencyKey); + + PaymentWithAllLinks paymentWithAllLinks = createdPayment.getPayment(); + Response.ResponseBuilder response; + + switch (createdPayment.getWhenCreated()) { + case BRAND_NEW: + response = Response + .created(publicApiUriGenerator.getPaymentURI(paymentWithAllLinks.getPaymentId())); + break; + case EXISTING: + response = Response.ok(); + break; + default: + throw new IllegalArgumentException(format("Unrecognised WhenCreated enum: %s", createdPayment.getWhenCreated())); + } + + response.entity(paymentWithAllLinks) + .header(PRAGMA, "no-cache") + .header(CACHE_CONTROL, "no-store"); + + logger.info("Payment returned (created): [ {} ]", paymentWithAllLinks); + return response.build(); + } + + @POST + @Timed + @Path("/v1/payments/{paymentId}/cancel") + @Produces(APPLICATION_JSON) + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Cancel a payment", + summary = "Cancel payment", + description = "You can use this endpoint [to cancel an unfinished payment]" + + "(https://docs.payments.service.gov.uk/making_payments/#cancel-a-payment-that-s-in-progress).", + responses = { + @ApiResponse(responseCode = "204", description = "No Content"), + @ApiResponse(responseCode = "400", description = "Cancellation of payment failed", + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "401", + description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "404", description = RESPONSE_404_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "409", description = RESPONSE_409_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public Response cancelPayment(@Parameter(hidden = true) @Auth Account account, + @PathParam("paymentId") + @Parameter(name = "paymentId", description = "The `payment_id` of the payment you’re cancelling.", example = "hu20sqlact5260q2nanm0q8u93") + String paymentId) { + + logger.info("Payment cancel request - payment_id=[{}]", paymentId); + + return cancelPaymentService.cancel(account, paymentId); + } + + @POST + @Timed + @Path("/v1/payments/{paymentId}/capture") + @Produces(APPLICATION_JSON) + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Capture a payment", + summary = "Take a delayed payment", + description = "You can use this endpoint to [take (‘capture’) a delayed payment from the paying user’s bank account]" + + "(https://docs.payments.service.gov.uk/delayed_capture/).", + responses = { + @ApiResponse(responseCode = "204", description = "No Content"), + @ApiResponse(responseCode = "400", description = "Capture of payment failed", + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "401", + description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "404", description = RESPONSE_404_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "409", description = RESPONSE_409_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public Response capturePayment(@Parameter(hidden = true) @Auth Account account, + @PathParam("paymentId") + @Parameter(name = "paymentId", description = "The `payment_id` of the payment you’re capturing.", example = "hu20sqlact5260q2nanm0q8u93") + String paymentId) { + logger.info("Payment capture request - payment_id=[{}]", paymentId); + + Response connectorResponse = capturePaymentService.capture(account, paymentId); + + if (connectorResponse.getStatus() == HttpStatus.SC_NO_CONTENT) { + connectorResponse.close(); + return Response.noContent().build(); + } + + throw new CaptureChargeException(connectorResponse); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/RequestDeniedResource.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/RequestDeniedResource.java new file mode 100644 index 000000000..b6926c771 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/RequestDeniedResource.java @@ -0,0 +1,62 @@ +package uk.gov.pay.api.resources; + +import com.codahale.metrics.annotation.Timed; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static uk.gov.pay.api.model.RequestError.Code.REQUEST_DENIED_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +@Path("/") +public class RequestDeniedResource { + + private static final Logger logger = LoggerFactory.getLogger(RequestDeniedResource.class); + + @GET + @Timed + @Path("request-denied") + @Produces(APPLICATION_JSON) + public Response requestDeniedGet(@HeaderParam("x-naxsi_sig") String naxsiViolatedRules) { + return requestDenied(naxsiViolatedRules); + } + + @POST + @Timed + @Path("request-denied") + @Produces(APPLICATION_JSON) + public Response requestDeniedPost(@HeaderParam("x-naxsi_sig") String naxsiViolatedRules) { + return requestDenied(naxsiViolatedRules); + } + + @PUT + @Timed + @Path("request-denied") + @Produces(APPLICATION_JSON) + public Response requestDeniedPut(@HeaderParam("x-naxsi_sig") String naxsiViolatedRules) { + return requestDenied(naxsiViolatedRules); + } + + @DELETE + @Timed + @Path("request-denied") + @Produces(APPLICATION_JSON) + public Response requestDeniedDelete(@HeaderParam("x-naxsi_sig") String naxsiViolatedRules) { + return requestDenied(naxsiViolatedRules); + } + + private Response requestDenied(@HeaderParam("x-naxsi_sig") String naxsiViolatedRules) { + logger.info("Naxsi rules violated - [ {} ]", naxsiViolatedRules); + return Response.status(BAD_REQUEST).entity(aRequestError(REQUEST_DENIED_ERROR)).build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/SearchDisputesResource.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/SearchDisputesResource.java new file mode 100644 index 000000000..c82b5336c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/SearchDisputesResource.java @@ -0,0 +1,113 @@ +package uk.gov.pay.api.resources; + +import com.codahale.metrics.annotation.Timed; +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.RequestError; +import uk.gov.pay.api.model.search.dispute.DisputesSearchResults; +import uk.gov.pay.api.resources.error.ApiErrorResponse; +import uk.gov.pay.api.service.DisputesSearchParams; +import uk.gov.pay.api.service.SearchDisputesService; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; + +import static java.lang.String.format; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_200_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_401_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_429_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_500_DESCRIPTION; +import static uk.gov.pay.api.validation.DisputeSearchValidator.validateDisputeParameters; + +@Path("/") +@Tag(name = "Disputes") +@Produces({"application/json"}) +public class SearchDisputesResource { + private static final Logger logger = LoggerFactory.getLogger(SearchDisputesResource.class); + private final SearchDisputesService searchDisputesService; + + @Inject + public SearchDisputesResource(SearchDisputesService searchDisputesService) { + this.searchDisputesService = searchDisputesService; + } + + @GET + @Timed + @Path("/v1/disputes") + @Produces(APPLICATION_JSON) + @Operation(security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Search disputes", + summary = "Search disputes", + description = "You can use this endpoint to search disputes. " + + "A dispute is when [a paying user challenges a completed payment through their bank](https://docs.payments.service.gov.uk/disputes/).", + responses = { + @ApiResponse(responseCode = "200", description = RESPONSE_200_DESCRIPTION, + content = @Content(schema = @Schema(implementation = DisputesSearchResults.class))), + @ApiResponse(responseCode = "401", + description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "422", + description = "Invalid parameters: from_date, to_date, from_settled_date, to_settled_date, status, display_size. See Public API documentation for the correct data formats", + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "429", description = RESPONSE_429_DESCRIPTION, + content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))) + } + ) + public DisputesSearchResults searchDisputes(@Parameter(hidden = true) + @Auth Account account, + @Parameter(description = "Returns disputes raised on or after the `from_date`. " + + "Date and time must be coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", example = "2015-08-13T12:35:00Z") + @QueryParam("from_date") String fromDate, + @Parameter(description = "Returns disputes raised before the `to_date`. " + + "Date and time must be coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", example = "2015-08-13T12:35:00Z") + @QueryParam("to_date") String toDate, + @Parameter(description = "Returns disputes settled on or after the `from_settled_date`. " + + "Date must be in ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. " + + "Disputes are settled when your payment service provider takes the disputed amount from a payout to your bank account.") + @QueryParam("from_settled_date") String fromSettledDate, + @Parameter(description = "Returns disputes settled before the `to_settled_date`. " + + "Date must be in ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. " + + "Disputes are settled when your payment service provider takes the disputed amount from a payout to your bank account.") + @QueryParam("to_settled_date") String toSettledDate, + @Parameter(description = "Returns disputes with a matching `status`. `status` reflects what stage of the dispute process a dispute is at. " + + "You can [read more about the meanings of the different status values](https://docs.payments.service.gov.uk/disputes/#dispute-status)", example = "won", + schema = @Schema(allowableValues = {"needs_response", "under_review", "lost", "won"})) + @QueryParam("status") String status, + @Parameter(description = "Returns a specific page of results. Defaults to `1`.") + @QueryParam("page") String pageNumber, + @Parameter(description = "The number of disputes returned per results page. Defaults to `500`. Maximum value is `500`.") + @QueryParam("display_size") String displaySize) { + logger.info("Disputes search request - [ {} ]", + format("from_date: %s, to_date: %s, from_settled_date: %s, to_settled_date: %s, " + + "status: s, page: %s, display_size: %s", + fromDate, toDate, fromSettledDate, toSettledDate, status, pageNumber, displaySize)); + + DisputesSearchParams params = new DisputesSearchParams.Builder() + .withFromDate(fromDate) + .withToDate(toDate) + .withFromSettledDate(fromSettledDate) + .withToSettledDate(toSettledDate) + .withStatus(status) + .withPage(pageNumber) + .withDisplaySize(displaySize) + .build(); + + validateDisputeParameters(params); + + return searchDisputesService.searchDisputes(account, params); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/SearchRefundsResource.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/SearchRefundsResource.java new file mode 100644 index 000000000..d2657980b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/SearchRefundsResource.java @@ -0,0 +1,102 @@ +package uk.gov.pay.api.resources; + +import com.codahale.metrics.annotation.Timed; +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.RequestError; +import uk.gov.pay.api.model.search.card.SearchRefundsResults; +import uk.gov.pay.api.service.RefundsParams; +import uk.gov.pay.api.service.SearchRefundsService; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; + +import static java.lang.String.format; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_200_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_401_DESCRIPTION; +import static uk.gov.pay.api.common.ResponseConstants.RESPONSE_500_DESCRIPTION; + +@Path("/") +@Tag(name = "Refunding card payments") +@Produces({"application/json"}) +public class SearchRefundsResource { + + private static final Logger logger = LoggerFactory.getLogger(SearchRefundsResource.class); + + private final SearchRefundsService searchRefundsService; + + @Inject + public SearchRefundsResource(SearchRefundsService searchRefundsService, PublicApiConfig configuration) { + this.searchRefundsService = searchRefundsService; + } + + @GET + @Timed + @Path("/v1/refunds") + @Produces(APPLICATION_JSON) + @Operation( + security = {@SecurityRequirement(name = "BearerAuth")}, + operationId = "Search refunds", + summary = "Search refunds", + description = "You can use this endpoint to [search refunds you’ve previously created]" + + "(https://docs.payments.service.gov.uk/refunding_payments/#searching-refunds). " + + "The refunds are sorted by date, with the most recently created refunds appearing first.", + responses = { + @ApiResponse(responseCode = "200", description = RESPONSE_200_DESCRIPTION, content = @Content(schema = @Schema(implementation = SearchRefundsResults.class))), + @ApiResponse(responseCode = "401", description = RESPONSE_401_DESCRIPTION), + @ApiResponse(responseCode = "422", description = "Invalid parameters. See Public API documentation for the correct data formats", + content = @Content(schema = @Schema(implementation = RequestError.class))), + @ApiResponse(responseCode = "500", description = RESPONSE_500_DESCRIPTION, + content = @Content(schema = @Schema(implementation = RequestError.class))), + } + ) + public SearchRefundsResults searchRefunds(@Parameter(hidden = true) + @Auth Account account, + @Parameter(description = "Returns refunds created on or after the `from_date`. " + + "Date and time must use Coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", example = "2015-08-13T12:35:00Z") + @QueryParam("from_date") String fromDate, + @Parameter(description = "Returns refunds created before the `to_date`. " + + "Date and time must use Coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", example = "2015-08-13T12:35:00Z") + @QueryParam("to_date") String toDate, + @Parameter(description = "Returns refunds settled on or after the `from_settled_date` value. " + + "You can only use `from_settled_date` if your payment service provider is Stripe. " + + "Date must use ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. " + + "Refunds are settled when Stripe takes the refund from your account balance.", example="2022-08-13") + @QueryParam("from_settled_date") String fromSettledDate, + @Parameter(description = "Returns refunds settled before the `to_settled_date` value. " + + "You can only use `to_settled_date` if your payment service provider is Stripe. " + + "Date must use ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. " + + "Refunds are settled when Stripe takes the refund from your account balance.", example="2022-08-13") + @QueryParam("to_settled_date") String toSettledDate, + @Parameter(description = "Returns a [specific page of results](https://docs.payments.service.gov.uk/api_reference/#pagination). " + + "Defaults to `1`.") + @QueryParam("page") String pageNumber, + @Parameter(description = "The number of refunds returned [per results page](https://docs.payments.service.gov.uk/api_reference/#pagination). " + + "Defaults to `500`. Maximum value is `500`.", hidden = false) + @QueryParam("display_size") String displaySize) { + + logger.info("Refunds search request - [ {} ]", + format("from_date: %s, to_date: %s, page: %s, display_size: %s," + + "from_settled_date: %s, to_settled_date: %s", + fromDate, toDate, pageNumber, displaySize, fromSettledDate, toSettledDate)); + + RefundsParams refundsParams = new RefundsParams(fromDate, toDate, pageNumber, displaySize, + fromSettledDate, toSettledDate); + + return searchRefundsService.searchLedgerRefunds(account, refundsParams); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/SecuritytxtResource.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/SecuritytxtResource.java new file mode 100644 index 000000000..cee22613a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/SecuritytxtResource.java @@ -0,0 +1,37 @@ +package uk.gov.pay.api.resources; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.Date; + +@Path("/") +public class SecuritytxtResource { + + // https://gds-way.cloudapps.digital/standards/vulnerability-disclosure.html + private static final URI CABINET_OFFICE_SECURITY_TXT = URI.create("https://vdp.cabinetoffice.gov.uk/.well-known/security.txt"); + + @GET + @Path("/.well-known/security.txt") + public Response redirectFromWellKnownSecuritytxt() { + return redirectToCabinetOfficeSecuritytxt(); + } + + @GET + @Path("/security.txt") + public Response redirectFromSecuritytxt() { + return redirectToCabinetOfficeSecuritytxt(); + } + + private static Response redirectToCabinetOfficeSecuritytxt() { + return Response + .status(Response.Status.FOUND) + .location(CABINET_OFFICE_SECURITY_TXT) + .cacheControl(CacheControl.valueOf("no-cache")) + .expires(new Date()) + .build(); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/error/ApiErrorResponse.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/error/ApiErrorResponse.java new file mode 100644 index 000000000..993a2d163 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/error/ApiErrorResponse.java @@ -0,0 +1,66 @@ +package uk.gov.pay.api.resources.error; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; +import static java.lang.String.format; + +@Schema(name = "ErrorResponse", description = "An error response") +@JsonInclude(NON_NULL) +public class ApiErrorResponse { + + public enum Code { + + TOO_MANY_REQUESTS_ERROR("P0900", "Too many requests"); + + private String value; + private String format; + + Code(String value, String format) { + this.value = value; + this.format = format; + } + + public String value() { + return value; + } + + public String getFormat() { + return format; + } + } + + private final Code code; + private final String description; + + public static ApiErrorResponse anApiErrorResponse(Code code, Object... parameters) { + return new ApiErrorResponse(code, parameters); + } + + private ApiErrorResponse(Code code, Object... parameters) { + this.code = code; + this.description = format(code.getFormat(), parameters); + } + + @Schema(example = "P0900", description = "A GOV.UK Pay API error code. " + + "You can [find out more about this code in our documentation]" + + "(https://docs.payments.service.gov.uk/api_reference/#gov-uk-pay-api-error-codes).") + public String getCode() { + return code.value(); + } + + @Schema(example = "Too many requests", description = "Additional details about the error") + public String getDescription() { + return description; + } + + @Override + public String toString() { + return "ApiErrorResponse{" + + "code=" + code.value() + + ", name=" + code + + ", description='" + description + '\'' + + '}'; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/telephone/TelephonePaymentNotificationResource.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/telephone/TelephonePaymentNotificationResource.java new file mode 100644 index 000000000..61e29a54f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/resources/telephone/TelephonePaymentNotificationResource.java @@ -0,0 +1,49 @@ +package uk.gov.pay.api.resources.telephone; + +import com.codahale.metrics.annotation.Timed; +import io.dropwizard.auth.Auth; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.telephone.CreateTelephonePaymentRequest; +import uk.gov.pay.api.model.telephone.TelephonePaymentResponse; +import uk.gov.pay.api.service.telephone.CreateTelephonePaymentService; + +import javax.inject.Inject; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +@Path("/") +public class TelephonePaymentNotificationResource { + + private static final Logger logger = LoggerFactory.getLogger(TelephonePaymentNotificationResource.class); + + private final CreateTelephonePaymentService createTelephonePaymentService; + + @Inject + public TelephonePaymentNotificationResource(CreateTelephonePaymentService createTelephonePaymentService) { + this.createTelephonePaymentService = createTelephonePaymentService; + } + + + @POST + @Timed + @Path("/v1/payment_notification") + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + public Response newPayment(@Auth Account account, @Valid CreateTelephonePaymentRequest createTelephonePaymentRequest) { + Pair responseAndStatusCode = createTelephonePaymentService.create(account, createTelephonePaymentRequest); + var response = responseAndStatusCode.getLeft(); + var statusCode = responseAndStatusCode.getRight(); + + return Response.status(statusCode).entity(response).build(); + } +} + diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/AuthorisationService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/AuthorisationService.java new file mode 100644 index 000000000..7329093ae --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/AuthorisationService.java @@ -0,0 +1,36 @@ +package uk.gov.pay.api.service; + +import uk.gov.pay.api.exception.AuthorisationRequestException; +import uk.gov.pay.api.model.AuthorisationRequest; + +import javax.inject.Inject; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Response; + +import static org.apache.http.HttpStatus.SC_NO_CONTENT; + +public class AuthorisationService { + + private final Client client; + private final ConnectorUriGenerator connectorUriGenerator; + + @Inject + public AuthorisationService(Client client, ConnectorUriGenerator connectorUriGenerator) { + this.client = client; + this.connectorUriGenerator = connectorUriGenerator; + } + + public Response authoriseRequest(AuthorisationRequest authorisationRequest) { + Response response = client + .target(connectorUriGenerator.authorisationURI()) + .request() + .post(Entity.json(authorisationRequest)); + + if (response.getStatus() != SC_NO_CONTENT) { + throw new AuthorisationRequestException(response); + } + + return Response.noContent().build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/CancelPaymentService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/CancelPaymentService.java new file mode 100644 index 000000000..39227ef19 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/CancelPaymentService.java @@ -0,0 +1,36 @@ +package uk.gov.pay.api.service; + +import org.apache.http.HttpStatus; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.CancelChargeException; + +import javax.inject.Inject; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Response; + +public class CancelPaymentService { + + private final Client client; + private final ConnectorUriGenerator connectorUriGenerator; + + @Inject + public CancelPaymentService(Client client, ConnectorUriGenerator connectorUriGenerator) { + this.client = client; + this.connectorUriGenerator = connectorUriGenerator; + } + + public Response cancel(Account account, String chargeId) { + Response connectorResponse = client + .target(connectorUriGenerator.cancelURI(account, chargeId)) + .request() + .post(null); + + if (connectorResponse.getStatus() == HttpStatus.SC_NO_CONTENT) { + connectorResponse.close(); + return Response.noContent().build(); + } + + throw new CancelChargeException(connectorResponse); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/CapturePaymentService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/CapturePaymentService.java new file mode 100644 index 000000000..0a9abd3d1 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/CapturePaymentService.java @@ -0,0 +1,28 @@ +package uk.gov.pay.api.service; + +import uk.gov.pay.api.auth.Account; + +import javax.inject.Inject; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Response; + +public class CapturePaymentService { + + private final Client client; + private final ConnectorUriGenerator connectorUriGenerator; + + @Inject + public CapturePaymentService(Client client, ConnectorUriGenerator connectorUriGenerator) { + this.client = client; + this.connectorUriGenerator = connectorUriGenerator; + } + + public Response capture(Account account, String chargeId) { + return client + .target(connectorUriGenerator.captureURI(account, chargeId)) + .request() + .post(Entity.json("{}")); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/ConnectorService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/ConnectorService.java new file mode 100644 index 000000000..d4016df76 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/ConnectorService.java @@ -0,0 +1,120 @@ +package uk.gov.pay.api.service; + +import org.apache.http.HttpStatus; +import uk.gov.pay.api.agreement.model.AgreementCreatedResponse; +import uk.gov.pay.api.agreement.model.CreateAgreementRequest; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.CancelAgreementException; +import uk.gov.pay.api.exception.CreateAgreementException; +import uk.gov.pay.api.exception.GetChargeException; +import uk.gov.pay.api.exception.GetEventsException; +import uk.gov.pay.api.exception.GetRefundException; +import uk.gov.pay.api.exception.GetRefundsException; +import uk.gov.pay.api.model.Charge; +import uk.gov.pay.api.model.ChargeFromResponse; +import uk.gov.pay.api.model.PaymentEvents; +import uk.gov.pay.api.model.RefundFromConnector; +import uk.gov.pay.api.model.RefundsFromConnector; + +import javax.inject.Inject; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import static javax.ws.rs.client.Entity.json; +import static org.apache.http.HttpStatus.SC_OK; + +public class ConnectorService { + private final Client client; + private final ConnectorUriGenerator connectorUriGenerator; + + @Inject + public ConnectorService(Client client, ConnectorUriGenerator connectorUriGenerator) { + this.client = client; + this.connectorUriGenerator = connectorUriGenerator; + } + + public Charge getCharge(Account account, String paymentId) { + Response response = client + .target(connectorUriGenerator.chargeURI(account, paymentId)) + .request() + .get(); + + if (response.getStatus() == SC_OK) { + ChargeFromResponse chargeFromResponse = response.readEntity(ChargeFromResponse.class); + return Charge.from(chargeFromResponse); + } + + throw new GetChargeException(response); + } + + public PaymentEvents getChargeEvents(Account account, String paymentId) { + Response connectorResponse = client + .target(connectorUriGenerator.chargeEventsURI(account, paymentId)) + .request() + .accept(MediaType.APPLICATION_JSON) + .get(); + + if (connectorResponse.getStatus() == SC_OK) { + return connectorResponse.readEntity(PaymentEvents.class); + } + + throw new GetEventsException(connectorResponse); + } + + public RefundsFromConnector getPaymentRefunds(String accountId, String paymentId) { + Response connectorResponse = client + .target(connectorUriGenerator.refundsForPaymentURI(accountId, paymentId)) + .request() + .get(); + + if (connectorResponse.getStatus() == SC_OK) { + return connectorResponse.readEntity(RefundsFromConnector.class); + } + + throw new GetRefundsException(connectorResponse); + } + + public RefundFromConnector getPaymentRefund(String accountId, String paymentId, String refundId) { + Response connectorResponse = client + .target(connectorUriGenerator.refundForPaymentURI(accountId, paymentId, refundId)) + .request() + .accept(MediaType.APPLICATION_JSON) + .get(); + + if (connectorResponse.getStatus() == SC_OK) { + return connectorResponse.readEntity(RefundFromConnector.class); + } + + throw new GetRefundException(connectorResponse); + } + + public AgreementCreatedResponse createAgreement(Account account, CreateAgreementRequest createAgreementRequest) { + Response response = client + .target(connectorUriGenerator.getAgreementURI(account)) + .request() + .accept(MediaType.APPLICATION_JSON) + .post(buildCreateAgreementRequestPayload(createAgreementRequest)); + if (response.getStatus() != HttpStatus.SC_CREATED) { + throw new CreateAgreementException(response); + } + return response.readEntity(AgreementCreatedResponse.class); + } + + public void cancelAgreement(Account account, String agreementId) { + Response response = client + .target(connectorUriGenerator.cancelAgreementURI(account, agreementId)) + .request() + .post(null); + if (response.getStatus() != HttpStatus.SC_NO_CONTENT) { + throw new CancelAgreementException(response); + } + + response.close(); + } + + private Entity buildCreateAgreementRequestPayload(CreateAgreementRequest requestPayload) { + return json(requestPayload.toConnectorPayload()); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/ConnectorUriGenerator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/ConnectorUriGenerator.java new file mode 100644 index 000000000..c6d774016 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/ConnectorUriGenerator.java @@ -0,0 +1,85 @@ +package uk.gov.pay.api.service; + +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.auth.Account; + +import javax.inject.Inject; +import javax.ws.rs.core.UriBuilder; +import java.util.Collections; +import java.util.Map; + +import static java.lang.String.format; + +public class ConnectorUriGenerator { + private final PublicApiConfig configuration; + + @Inject + public ConnectorUriGenerator(PublicApiConfig configuration) { + this.configuration = configuration; + } + + String chargesURI(Account account) { + return buildConnectorUri(format("/v1/api/accounts/%s/charges", account.getAccountId())); + } + + String chargeURI(Account account, String chargeId) { + String path = format("/v1/api/accounts/%s/charges/%s", account.getAccountId(), chargeId); //TODO rename to /payments instead /charges + return buildConnectorUri(path); + } + + public String chargeEventsURI(Account account, String paymentId) { + String path = format("/v1/api/accounts/%s/charges/%s/events", account.getAccountId(), paymentId); + return buildConnectorUri(path); + } + + String cancelURI(Account account, String paymentId) { + String path = format("/v1/api/accounts/%s/charges/%s/cancel", account.getAccountId(), paymentId); + return buildConnectorUri(path, Collections.emptyMap()); + } + + public String telephoneChargesURI(Account account) { + return buildConnectorUri(format("/v1/api/accounts/%s/telephone-charges", account.getAccountId())); + } + + String captureURI(Account account, String chargeId) { + String path = format("/v1/api/accounts/%s/charges/%s/capture", account.getAccountId(), chargeId); + return buildConnectorUri(path); + } + + String refundsForPaymentURI(String accountId, String chargeId) { + String path = format("/v1/api/accounts/%s/charges/%s/refunds", accountId, chargeId); + return buildConnectorUri(path); + } + + String refundForPaymentURI(String accountId, String chargeId, String refundId) { + String path = format("/v1/api/accounts/%s/charges/%s/refunds/%s", accountId, chargeId, refundId); + return buildConnectorUri(path); + } + + String authorisationURI() { + String path = "/v1/api/charges/authorise"; + return buildConnectorUri(path); + } + + public String getAgreementURI(Account account) { + String path = format("/v1/api/accounts/%s/agreements", account.getAccountId()); + return buildConnectorUri(path); + } + + public String cancelAgreementURI(Account account, String agreementId) { + String path = format("/v1/api/accounts/%s/agreements/%s/cancel", account.getAccountId(), agreementId); + return buildConnectorUri(path, Collections.emptyMap()); + } + + private String buildConnectorUri(String path) { + return buildConnectorUri(path, Collections.emptyMap()); + } + + private String buildConnectorUri(String path, Map params) { + UriBuilder builder = UriBuilder.fromPath(configuration.getConnectorUrl()).path(path); + params.entrySet().stream() + .filter(k -> k.getValue() != null) + .forEach(k -> builder.queryParam(k.getKey(), k.getValue())); + return builder.toString(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/CreatePaymentService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/CreatePaymentService.java new file mode 100644 index 000000000..2f4bda804 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/CreatePaymentService.java @@ -0,0 +1,91 @@ +package uk.gov.pay.api.service; + +import org.apache.http.HttpStatus; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.CreateChargeException; +import uk.gov.pay.api.model.Charge; +import uk.gov.pay.api.model.ChargeFromResponse; +import uk.gov.pay.api.model.CreateCardPaymentRequest; +import uk.gov.pay.api.model.CreatedPaymentWithAllLinks; +import uk.gov.pay.api.model.links.PaymentWithAllLinks; + +import javax.inject.Inject; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import java.util.Optional; + +import static javax.ws.rs.client.Entity.json; +import static uk.gov.pay.api.model.CreatedPaymentWithAllLinks.WhenCreated.BRAND_NEW; +import static uk.gov.pay.api.model.CreatedPaymentWithAllLinks.WhenCreated.EXISTING; + +public class CreatePaymentService { + + private final Client client; + private final PublicApiUriGenerator publicApiUriGenerator; + private final ConnectorUriGenerator connectorUriGenerator; + + @Inject + public CreatePaymentService(Client client, PublicApiUriGenerator publicApiUriGenerator, ConnectorUriGenerator connectorUriGenerator) { + this.client = client; + this.publicApiUriGenerator = publicApiUriGenerator; + this.connectorUriGenerator = connectorUriGenerator; + } + + public CreatedPaymentWithAllLinks create(Account account, CreateCardPaymentRequest createCardPaymentRequest, String idempotencyKey) { + Response connectorResponse = createCharge(account, createCardPaymentRequest, idempotencyKey); + + if (connectorCreatedNewPayment(connectorResponse)) { + ChargeFromResponse chargeFromResponse = connectorResponse.readEntity(ChargeFromResponse.class); + return CreatedPaymentWithAllLinks.of(buildResponseModel(Charge.from(chargeFromResponse)), BRAND_NEW); + } + + if (connectorReturnedExistingPayment(connectorResponse)) { + ChargeFromResponse chargeFromResponse = connectorResponse.readEntity(ChargeFromResponse.class); + return CreatedPaymentWithAllLinks.of(buildResponseModel(Charge.from(chargeFromResponse)), EXISTING); + } + + throw new CreateChargeException(connectorResponse); + } + private PaymentWithAllLinks buildResponseModel(Charge chargeFromResponse) { + return PaymentWithAllLinks.getPaymentWithLinks( + chargeFromResponse, + publicApiUriGenerator.getPaymentURI(chargeFromResponse.getChargeId()), + publicApiUriGenerator.getPaymentEventsURI(chargeFromResponse.getChargeId()), + publicApiUriGenerator.getPaymentCancelURI(chargeFromResponse.getChargeId()), + publicApiUriGenerator.getPaymentRefundsURI(chargeFromResponse.getChargeId()), + publicApiUriGenerator.getPaymentCaptureURI(chargeFromResponse.getChargeId()), + publicApiUriGenerator.getPaymentAuthorisationURI()); + } + + private boolean connectorCreatedNewPayment(Response connectorResponse) { + return connectorResponse.getStatus() == HttpStatus.SC_CREATED; + } + + private boolean connectorReturnedExistingPayment(Response connectorResponse) { + return connectorResponse.getStatus() == HttpStatus.SC_OK; + } + + + private Response createCharge(Account account, CreateCardPaymentRequest createCardPaymentRequest, String idempotencyKey) { + MultivaluedMap headers = new MultivaluedHashMap<>(); + + Optional.ofNullable(idempotencyKey) + .ifPresent(key -> headers.add("Idempotency-Key", idempotencyKey)); + + return client + .target(connectorUriGenerator.chargesURI(account)) + .request() + .headers(headers) + .accept(MediaType.APPLICATION_JSON) + .post(buildChargeRequestPayload(createCardPaymentRequest)); + } + + private Entity buildChargeRequestPayload(CreateCardPaymentRequest requestPayload) { + return json(requestPayload.toConnectorPayload()); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/CreateRefundService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/CreateRefundService.java new file mode 100644 index 000000000..3983e91ac --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/CreateRefundService.java @@ -0,0 +1,83 @@ +package uk.gov.pay.api.service; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.GsonBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.CreateRefundException; +import uk.gov.pay.api.model.CardPayment; +import uk.gov.pay.api.model.CreatePaymentRefundRequest; +import uk.gov.pay.api.model.RefundFromConnector; +import uk.gov.pay.api.model.RefundResponse; +import uk.gov.pay.api.model.RefundSummary; +import uk.gov.pay.api.resources.GetOnePaymentStrategy; + +import javax.inject.Inject; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Response; +import java.util.Optional; + +import static java.lang.String.format; +import static javax.ws.rs.client.Entity.json; +import static javax.ws.rs.core.Response.Status.ACCEPTED; +import static javax.ws.rs.core.UriBuilder.fromPath; + +public class CreateRefundService { + + private static final Logger logger = LoggerFactory.getLogger(CreateRefundService.class); + + private final GetPaymentService getPaymentService; + private final Client client; + private final String connectorUrl; + private final String baseUrl; + + @Inject + public CreateRefundService(GetPaymentService getPaymentService, + Client client, + PublicApiConfig configuration) { + this.getPaymentService = getPaymentService; + this.client = client; + this.baseUrl = configuration.getBaseUrl(); + this.connectorUrl = configuration.getConnectorUrl(); + } + + public RefundResponse createRefund(Account account, String paymentId, CreatePaymentRefundRequest createPaymentRefundRequest) { + var strategy = new GetOnePaymentStrategy("", account, paymentId, getPaymentService); + + Integer refundAmountAvailable = createPaymentRefundRequest.getRefundAmountAvailable() + .orElseGet(() -> getRefundAmountAvailableFromPayment(strategy)); + + ImmutableMap payloadMap = ImmutableMap.of( + "amount", createPaymentRefundRequest.getAmount(), + "refund_amount_available", refundAmountAvailable); + String connectorPayload = new GsonBuilder().create().toJson(payloadMap); + + Response connectorResponse = client + .target(getConnectorUrl(format("/v1/api/accounts/%s/charges/%s/refunds", account.getAccountId(), paymentId))) + .request() + .post(json(connectorPayload)); + + if (connectorResponse.getStatus() != ACCEPTED.getStatusCode()) { + throw new CreateRefundException(connectorResponse); + } + + RefundFromConnector refundFromConnector = connectorResponse.readEntity(RefundFromConnector.class); + logger.debug("created refund returned - [ {} ]", refundFromConnector); + return RefundResponse.valueOf(refundFromConnector, paymentId, baseUrl); + } + + private int getRefundAmountAvailableFromPayment(GetOnePaymentStrategy strategy) { + return Optional.of((CardPayment) strategy.validateAndExecute()) + .map(p -> p.getRefundSummary() + .map(RefundSummary::getAmountAvailable) + .orElse(0L)) + .map(Long::intValue) + .orElse(0); + } + + private String getConnectorUrl(String urlPath) { + return fromPath(connectorUrl).path(urlPath).toString(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/DisputesSearchParams.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/DisputesSearchParams.java new file mode 100644 index 000000000..03e7c2362 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/DisputesSearchParams.java @@ -0,0 +1,129 @@ +package uk.gov.pay.api.service; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +public class DisputesSearchParams { + + private static final String PAGE = "page"; + private static final String DISPLAY_SIZE = "display_size"; + private static final String DEFAULT_PAGE = "1"; + private static final String DEFAULT_DISPLAY_SIZE = "500"; + private static final String FROM_DATE = "from_date"; + private static final String TO_DATE = "to_date"; + private static final String FROM_SETTLED_DATE = "from_settled_date"; + private static final String TO_SETTLED_DATE = "to_settled_date"; + private static final String STATUS = "status"; + + private String fromDate; + private String toDate; + private String page; + private String displaySize; + private String fromSettledDate; + private String toSettledDate; + private String state; + + private DisputesSearchParams(Builder builder) { + this.fromDate = builder.fromDate; + this.toDate = builder.toDate; + this.page = builder.page; + this.displaySize = builder.displaySize; + this.fromSettledDate = builder.fromSettledDate; + this.toSettledDate = builder.toSettledDate; + this.state = builder.status; + } + + public Map getParamsAsMap() { + Map params = new LinkedHashMap<>(); + params.put(FROM_DATE, fromDate); + params.put(TO_DATE, toDate); + params.put(PAGE, Optional.ofNullable(page).orElse(DEFAULT_PAGE)); + params.put(DISPLAY_SIZE, Optional.ofNullable(displaySize).orElse(DEFAULT_DISPLAY_SIZE)); + params.put(FROM_SETTLED_DATE, fromSettledDate); + params.put(TO_SETTLED_DATE, toSettledDate); + params.put(STATUS, state); + + return params; + } + + public String getPage() { + return page; + } + + public String getDisplaySize() { + return displaySize; + } + + public String getFromDate() { + return fromDate; + } + + public String getToDate() { + return toDate; + } + + public String getFromSettledDate() { + return fromSettledDate; + } + + public String getToSettledDate() { + return toSettledDate; + } + + public String getState() { + return state; + } + + public static class Builder { + private String fromDate; + private String toDate; + private String page; + private String displaySize; + private String fromSettledDate; + private String toSettledDate; + private String status; + + public Builder() { + } + + public DisputesSearchParams build() { + return new DisputesSearchParams(this); + } + + public Builder withFromDate(String fromDate) { + this.fromDate = fromDate; + return this; + } + + public Builder withToDate(String toDate) { + this.toDate = toDate; + return this; + } + + public Builder withPage(String page) { + this.page = page; + return this; + } + + public Builder withDisplaySize(String displaySize) { + this.displaySize = displaySize; + return this; + } + + public Builder withFromSettledDate(String fromSettledDate) { + this.fromSettledDate = fromSettledDate; + return this; + } + + public Builder withToSettledDate(String toSettledDate) { + this.toSettledDate = toSettledDate; + return this; + } + + public Builder withStatus(String status) { + this.status = status; + return this; + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/GetPaymentEventsService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/GetPaymentEventsService.java new file mode 100644 index 000000000..65e4ae62f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/GetPaymentEventsService.java @@ -0,0 +1,52 @@ +package uk.gov.pay.api.service; + +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.GetEventsException; +import uk.gov.pay.api.model.PaymentEvents; +import uk.gov.pay.api.model.PaymentEventsResponse; +import uk.gov.pay.api.model.TransactionEvents; + +import javax.inject.Inject; +import java.net.URI; + +public class GetPaymentEventsService { + + private final PublicApiUriGenerator publicApiUriGenerator; + private ConnectorService connectorService; + private LedgerService ledgerService; + + @Inject + public GetPaymentEventsService(PublicApiUriGenerator publicApiUriGenerator, + ConnectorService connectorService, + LedgerService ledgerService) { + this.publicApiUriGenerator = publicApiUriGenerator; + this.connectorService = connectorService; + this.ledgerService = ledgerService; + } + + public PaymentEventsResponse getPaymentEventsFromConnector(Account account, String paymentId) { + PaymentEvents chargeEvents = connectorService.getChargeEvents(account, paymentId); + + URI paymentEventsLink = publicApiUriGenerator.getPaymentEventsURI(paymentId); + URI paymentLink = publicApiUriGenerator.getPaymentURI(paymentId); + + return PaymentEventsResponse.from(chargeEvents, paymentLink, paymentEventsLink); + } + + public PaymentEventsResponse getPaymentEventsFromLedger(Account account, String paymentId) { + TransactionEvents transactionEvents = ledgerService.getTransactionEvents(account, paymentId); + + URI paymentEventsLink = publicApiUriGenerator.getPaymentEventsURI(paymentId); + URI paymentLink = publicApiUriGenerator.getPaymentURI(paymentId); + + return PaymentEventsResponse.from(transactionEvents, paymentLink, paymentEventsLink); + } + + public PaymentEventsResponse getPaymentEvents(Account account, String paymentId) { + try { + return getPaymentEventsFromConnector(account, paymentId); + } catch (GetEventsException ex) { + return getPaymentEventsFromLedger(account, paymentId); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/GetPaymentRefundService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/GetPaymentRefundService.java new file mode 100644 index 000000000..f7deda062 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/GetPaymentRefundService.java @@ -0,0 +1,58 @@ +package uk.gov.pay.api.service; + +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.GetRefundException; +import uk.gov.pay.api.exception.GetTransactionException; +import uk.gov.pay.api.model.RefundFromConnector; +import uk.gov.pay.api.model.RefundResponse; +import uk.gov.pay.api.model.ledger.RefundTransactionFromLedger; + +import javax.inject.Inject; + +public class GetPaymentRefundService { + + private final ConnectorService connectorService; + private final LedgerService ledgerService; + private PublicApiUriGenerator publicApiUriGenerator; + + @Inject + public GetPaymentRefundService(ConnectorService connectorService, + LedgerService ledgerService, + PublicApiUriGenerator publicApiUriGenerator) { + this.connectorService = connectorService; + this.ledgerService = ledgerService; + this.publicApiUriGenerator = publicApiUriGenerator; + } + + public RefundResponse getConnectorPaymentRefund(Account account, String paymentId, String refundId) { + RefundFromConnector refundFromConnector = connectorService.getPaymentRefund(account.getAccountId(), paymentId, refundId); + + return RefundResponse.from( + refundFromConnector, + publicApiUriGenerator.getRefundsURI(paymentId, refundId), + publicApiUriGenerator.getPaymentURI(paymentId) + ); + } + + public RefundResponse getLedgerPaymentRefund(Account account, String paymentId, String refundId) { + try { + RefundTransactionFromLedger refund = ledgerService.getRefundTransaction(account, refundId, paymentId); + + return RefundResponse.from( + refund, + publicApiUriGenerator.getRefundsURI(paymentId, refundId), + publicApiUriGenerator.getPaymentURI(paymentId) + ); + } catch (GetTransactionException exception) { + throw new GetRefundException(exception); + } + } + + public RefundResponse getPaymentRefund(Account account, String paymentId, String refundId) { + try { + return getConnectorPaymentRefund(account, paymentId, refundId); + } catch (GetRefundException ex) { + return getLedgerPaymentRefund(account, paymentId, refundId); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/GetPaymentRefundsService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/GetPaymentRefundsService.java new file mode 100644 index 000000000..74918d862 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/GetPaymentRefundsService.java @@ -0,0 +1,76 @@ +package uk.gov.pay.api.service; + +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.RefundResponse; +import uk.gov.pay.api.model.RefundsFromConnector; +import uk.gov.pay.api.model.RefundsResponse; +import uk.gov.pay.api.model.ledger.RefundsFromLedger; + +import javax.inject.Inject; +import java.util.List; +import java.util.stream.Collectors; + +public class GetPaymentRefundsService { + + private final ConnectorService connectorService; + private final LedgerService ledgerService; + private PublicApiUriGenerator publicApiUriGenerator; + + @Inject + public GetPaymentRefundsService(ConnectorService connectorService, + LedgerService ledgerService, + PublicApiUriGenerator publicApiUriGenerator) { + this.connectorService = connectorService; + this.ledgerService = ledgerService; + this.publicApiUriGenerator = publicApiUriGenerator; + } + + public RefundsResponse getConnectorPaymentRefunds(Account account, String paymentId) { + RefundsFromConnector refundsFromConnector = connectorService.getPaymentRefunds(account.getAccountId(), paymentId); + List refundResponses = processRefunds(paymentId, refundsFromConnector); + + return getRefundsResponse(paymentId, refundResponses); + } + + public RefundsResponse getLedgerTransactionTransactions(Account account, String paymentId) { + RefundsFromLedger refundsFromLedger = ledgerService.getPaymentRefunds(account.getAccountId(), paymentId); + List refundResponses = processRefunds(paymentId, refundsFromLedger); + + return getRefundsResponse(paymentId, refundResponses); + } + + private List processRefunds(String paymentId, RefundsFromLedger refundsFromLedger) { + return refundsFromLedger.getTransactions() + .stream() + .map(refundTransactionFromLedger -> + RefundResponse.from(refundTransactionFromLedger, + publicApiUriGenerator.getRefundsURI(paymentId, + refundTransactionFromLedger.getTransactionId()), + publicApiUriGenerator.getPaymentURI(paymentId) + ) + ) + .collect(Collectors.toList()); + } + + private List processRefunds(String paymentId, RefundsFromConnector refundsFromConnector) { + return refundsFromConnector + .getEmbedded() + .getRefunds() + .stream() + .map(refundFromConnector -> + RefundResponse.from(refundFromConnector, + publicApiUriGenerator.getRefundsURI(paymentId, + refundFromConnector.getRefundId()), + publicApiUriGenerator.getPaymentURI(paymentId) + ) + ) + .collect(Collectors.toList()); + } + + private RefundsResponse getRefundsResponse(String paymentId, List refunds) { + return RefundsResponse.from(paymentId, refunds, + publicApiUriGenerator.getPaymentRefundsURI(paymentId).toString(), + publicApiUriGenerator.getPaymentURI(paymentId).toString() + ); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/GetPaymentService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/GetPaymentService.java new file mode 100644 index 000000000..d92df3d55 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/GetPaymentService.java @@ -0,0 +1,57 @@ +package uk.gov.pay.api.service; + +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.GetChargeException; +import uk.gov.pay.api.model.Charge; +import uk.gov.pay.api.model.links.PaymentWithAllLinks; + +import javax.inject.Inject; +import java.net.URI; + +public class GetPaymentService { + + private final PublicApiUriGenerator publicApiUriGenerator; + private final ConnectorService connectorService; + private final LedgerService ledgerService; + + @Inject + public GetPaymentService(PublicApiUriGenerator publicApiUriGenerator, + ConnectorService connectorService, LedgerService ledgerService) { + this.publicApiUriGenerator = publicApiUriGenerator; + this.connectorService = connectorService; + this.ledgerService = ledgerService; + } + + public PaymentWithAllLinks getConnectorCharge(Account account, String paymentId) { + Charge charge = connectorService.getCharge(account, paymentId); + + return getPaymentWithAllLinks(charge); + } + + public PaymentWithAllLinks getLedgerTransaction(Account account, String paymentId) { + Charge charge = ledgerService.getPaymentTransaction(account, paymentId); + + return getPaymentWithAllLinks(charge); + } + + public PaymentWithAllLinks getPayment(Account account, String paymentId) { + try { + return getConnectorCharge(account, paymentId); + } catch (GetChargeException ex) { + return getLedgerTransaction(account, paymentId); + } + } + + private PaymentWithAllLinks getPaymentWithAllLinks(Charge chargeFromResponse) { + URI paymentURI = publicApiUriGenerator.getPaymentURI(chargeFromResponse.getChargeId()); + + return PaymentWithAllLinks.getPaymentWithLinks( + chargeFromResponse, + paymentURI, + publicApiUriGenerator.getPaymentEventsURI(chargeFromResponse.getChargeId()), + publicApiUriGenerator.getPaymentCancelURI(chargeFromResponse.getChargeId()), + publicApiUriGenerator.getPaymentRefundsURI(chargeFromResponse.getChargeId()), + publicApiUriGenerator.getPaymentCaptureURI(chargeFromResponse.getChargeId()), + publicApiUriGenerator.getPaymentAuthorisationURI()); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/LedgerService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/LedgerService.java new file mode 100644 index 000000000..1ae1b0bc5 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/LedgerService.java @@ -0,0 +1,225 @@ +package uk.gov.pay.api.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.agreement.model.AgreementLedgerResponse; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.GetAgreementException; +import uk.gov.pay.api.exception.GetChargeException; +import uk.gov.pay.api.exception.GetEventsException; +import uk.gov.pay.api.exception.GetRefundsException; +import uk.gov.pay.api.exception.GetTransactionException; +import uk.gov.pay.api.exception.SearchAgreementsException; +import uk.gov.pay.api.exception.SearchDisputesException; +import uk.gov.pay.api.exception.SearchPaymentsException; +import uk.gov.pay.api.exception.SearchRefundsException; +import uk.gov.pay.api.ledger.model.AgreementSearchParams; +import uk.gov.pay.api.ledger.model.SearchResults; +import uk.gov.pay.api.ledger.service.LedgerUriGenerator; +import uk.gov.pay.api.model.Charge; +import uk.gov.pay.api.model.TransactionEvents; +import uk.gov.pay.api.model.TransactionResponse; +import uk.gov.pay.api.model.ledger.RefundTransactionFromLedger; +import uk.gov.pay.api.model.ledger.RefundsFromLedger; +import uk.gov.pay.api.model.ledger.SearchDisputesResponseFromLedger; +import uk.gov.pay.api.model.ledger.SearchRefundsResponseFromLedger; +import uk.gov.pay.api.model.search.card.PaymentSearchResponse; +import uk.gov.pay.api.validation.AgreementSearchValidator; + +import javax.inject.Inject; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.apache.http.HttpStatus.SC_OK; +import static uk.gov.pay.api.common.SearchConstants.GATEWAY_ACCOUNT_ID; + +public class LedgerService { + + private static final Logger LOGGER = LoggerFactory.getLogger(LedgerService.class); + + private static final String PARAM_ACCOUNT_ID = "account_id"; + private static final String PARAM_TRANSACTION_TYPE = "transaction_type"; + public static final String PARAM_EXACT_REFERENCE_MATCH = "exact_reference_match"; + private static final String PAYMENT_TRANSACTION_TYPE = "PAYMENT"; + private static final String REFUND_TRANSACTION_TYPE = "REFUND"; + private static final String DISPUTE_TRANSACTION_TYPE = "DISPUTE"; + + private final Client client; + private final LedgerUriGenerator ledgerUriGenerator; + + @Inject + public LedgerService(Client client, LedgerUriGenerator ledgerUriGenerator) { + this.client = client; + this.ledgerUriGenerator = ledgerUriGenerator; + } + + public Charge getPaymentTransaction(Account account, String paymentId) { + Response response = client + .target(ledgerUriGenerator.transactionURI(account, paymentId, PAYMENT_TRANSACTION_TYPE)) + .request() + .get(); + + if (response.getStatus() == SC_OK) { + TransactionResponse transactionResponse = response.readEntity(TransactionResponse.class); + return Charge.from(transactionResponse); + } + + throw new GetChargeException(response); + } + + public RefundTransactionFromLedger getRefundTransaction(Account account, String transactionId, String parentExternalId) { + Response response = client + .target(ledgerUriGenerator.transactionURI(account, transactionId, REFUND_TRANSACTION_TYPE, parentExternalId)) + .request() + .get(); + + if (response.getStatus() == SC_OK) { + return response.readEntity(RefundTransactionFromLedger.class); + } + + throw new GetTransactionException(response); + } + + public TransactionEvents getTransactionEvents(Account account, String paymentId) { + Response response = client + .target(ledgerUriGenerator.transactionEventsURI(account, paymentId)) + .request() + .get(); + + if (response.getStatus() == SC_OK) { + return response.readEntity(TransactionEvents.class); + } + throw new GetEventsException(response); + } + + public RefundsFromLedger getPaymentRefunds(String accountId, String paymentId) { + Response response = client + .target(ledgerUriGenerator.transactionsForTransactionURI(accountId, paymentId, REFUND_TRANSACTION_TYPE)) + .request() + .get(); + + if (response.getStatus() == SC_OK) { + return response.readEntity(RefundsFromLedger.class); + } + + throw new GetRefundsException(response); + } + + public SearchRefundsResponseFromLedger searchRefunds(Account account, Map paramsAsMap) { + + paramsAsMap.put(PARAM_ACCOUNT_ID, account.getAccountId()); + paramsAsMap.put(PARAM_TRANSACTION_TYPE, REFUND_TRANSACTION_TYPE); + + Response response = client + .target(ledgerUriGenerator.transactionsURIWithParams(paramsAsMap)) + .request() + .accept(MediaType.APPLICATION_JSON_TYPE) + .get(); + + if (response.getStatus() == SC_OK) { + try { + return response.readEntity(SearchRefundsResponseFromLedger.class); + } catch (ProcessingException exception) { + throw new SearchRefundsException(exception); + } + } + + throw new SearchRefundsException(response); + } + + public SearchDisputesResponseFromLedger searchDisputes(Account account, Map paramsAsMap) { + paramsAsMap.put(PARAM_ACCOUNT_ID, account.getAccountId()); + paramsAsMap.put(PARAM_TRANSACTION_TYPE, DISPUTE_TRANSACTION_TYPE); + + if (paramsAsMap.containsKey("status")) { + paramsAsMap.put("state", paramsAsMap.remove("status")); + } + + Response response = client + .target(ledgerUriGenerator.transactionsURIWithParams(paramsAsMap)) + .request() + .accept(MediaType.APPLICATION_JSON_TYPE) + .get(); + + if (response.getStatus() == SC_OK) { + try { + return response.readEntity(SearchDisputesResponseFromLedger.class); + } catch (ProcessingException exception) { + throw new SearchDisputesException(exception); + } + } + + throw new SearchDisputesException(response); + } + + public PaymentSearchResponse searchPayments(Account account, Map paramsAsMap) { + + paramsAsMap.put(PARAM_ACCOUNT_ID, account.getAccountId()); + paramsAsMap.put(PARAM_TRANSACTION_TYPE, PAYMENT_TRANSACTION_TYPE); + paramsAsMap.put(PARAM_EXACT_REFERENCE_MATCH, "true"); + + Response response = client + .target(ledgerUriGenerator.transactionsURIWithParams(paramsAsMap)) + .request() + .accept(MediaType.APPLICATION_JSON_TYPE) + .get(); + + if (response.getStatus() == SC_OK) { + try { + return response.readEntity(new GenericType<>() { + }); + } catch (ProcessingException ex) { + throw new SearchPaymentsException(ex); + } + } + + throw new SearchPaymentsException(response); + } + + public AgreementLedgerResponse getAgreement(Account account, String agreementId) { + Response response = client + .target(ledgerUriGenerator.agreementURI(account, agreementId)) + .request() + .header("X-Consistent", true) + .get(); + + if (response.getStatus() == SC_OK) { + return response.readEntity(AgreementLedgerResponse.class); + } + + throw new GetAgreementException(response); + } + + public SearchResults searchAgreements(Account account, AgreementSearchParams searchParams) { + AgreementSearchValidator.validateSearchParameters(searchParams); + + var params = new HashMap<>(searchParams.getQueryMap()); + params.put(GATEWAY_ACCOUNT_ID, account.getAccountId()); + params.put(PARAM_EXACT_REFERENCE_MATCH, "true"); + + String url = ledgerUriGenerator.agreementsURIWithParams(params); + Response ledgerResponse = client + .target(url) + .request() + .header(HttpHeaders.ACCEPT, APPLICATION_JSON) + .get(); + LOGGER.info("response from ledger for agreement search: {}", ledgerResponse); + + if (ledgerResponse.getStatus() == SC_OK) { + try { + return ledgerResponse.readEntity(new GenericType<>() { + }); + } catch (ProcessingException ex) { + throw new SearchAgreementsException(ex); + } + } + throw new SearchAgreementsException(ledgerResponse); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/PaymentSearchParams.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/PaymentSearchParams.java new file mode 100644 index 000000000..89bd588ff --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/PaymentSearchParams.java @@ -0,0 +1,227 @@ +package uk.gov.pay.api.service; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +public class PaymentSearchParams { + + public static final String REFERENCE_KEY = "reference"; + public static final String EMAIL_KEY = "email"; + public static final String STATE_KEY = "state"; + public static final String CARD_BRAND_KEY = "card_brand"; + public static final String FIRST_DIGITS_CARD_NUMBER_KEY = "first_digits_card_number"; + public static final String LAST_DIGITS_CARD_NUMBER_KEY = "last_digits_card_number"; + public static final String CARDHOLDER_NAME_KEY = "cardholder_name"; + public static final String FROM_DATE_KEY = "from_date"; + public static final String TO_DATE_KEY = "to_date"; + public static final String PAGE = "page"; + public static final String DISPLAY_SIZE = "display_size"; + public static final String FROM_SETTLED_DATE = "from_settled_date"; + public static final String TO_SETTLED_DATE = "to_settled_date"; + public static final String AGREEMENT_ID = "agreement_id"; + + private String reference; + private String email; + private String state; + private String cardBrand; + private String fromDate; + private String toDate; + private String pageNumber; + private String displaySize; + private String cardHolderName; + private String firstDigitsCardNumber; + private String lastDigitsCardNumber; + private String fromSettledDate; + private String toSettledDate; + private String agreementId; + + public PaymentSearchParams(Builder builder) { + this.reference = builder.reference; + this.email = builder.email; + this.state = builder.state; + this.cardBrand = builder.cardBrand; + this.fromDate = builder.fromDate; + this.toDate = builder.toDate; + this.pageNumber = builder.pageNumber; + this.displaySize = builder.displaySize; + this.cardHolderName = builder.cardHolderName; + this.firstDigitsCardNumber = builder.firstDigitsCardNumber; + this.lastDigitsCardNumber = builder.lastDigitsCardNumber; + this.fromSettledDate = builder.fromSettledDate; + this.toSettledDate = builder.toSettledDate; + this.agreementId = builder.agreementId; + } + + public Map getParamsAsMap() { + Map params = new LinkedHashMap<>(); + params.put(REFERENCE_KEY, reference); + params.put(EMAIL_KEY, email); + params.put(STATE_KEY, state); + params.put(CARD_BRAND_KEY, cardBrand); + params.put(CARDHOLDER_NAME_KEY, cardHolderName); + params.put(FIRST_DIGITS_CARD_NUMBER_KEY, firstDigitsCardNumber); + params.put(LAST_DIGITS_CARD_NUMBER_KEY, lastDigitsCardNumber); + params.put(FROM_DATE_KEY, fromDate); + params.put(TO_DATE_KEY, toDate); + params.put(PAGE, pageNumber); + params.put(DISPLAY_SIZE, displaySize); + params.put(FROM_SETTLED_DATE, fromSettledDate); + params.put(TO_SETTLED_DATE, toSettledDate); + params.put(AGREEMENT_ID, agreementId); + + return params; + } + + public String getState() { + return state; + } + + public String getReference() { + return reference; + } + + public String getEmail() { + return email; + } + + public String getCardBrand() { + return cardBrand; + } + + public String getFromDate() { + return fromDate; + } + + public String getToDate() { + return toDate; + } + + public String getPageNumber() { + return pageNumber; + } + + public String getDisplaySize() { + return displaySize; + } + + public String getFirstDigitsCardNumber() { + return firstDigitsCardNumber; + } + + public String getLastDigitsCardNumber() { + return lastDigitsCardNumber; + } + + public String getFromSettledDate() { + return fromSettledDate; + } + + public String getToSettledDate() { + return toSettledDate; + } + + public String getAgreementId() { + return agreementId; + } + + public static class Builder { + private String reference; + private String email; + private String state; + private String cardBrand; + private String fromDate; + private String toDate; + private String pageNumber; + private String displaySize; + private String cardHolderName; + private String firstDigitsCardNumber; + private String lastDigitsCardNumber; + private String fromSettledDate; + private String toSettledDate; + private String agreementId; + + public Builder() { + } + + public PaymentSearchParams build() { + return new PaymentSearchParams(this); + } + + public Builder withReference(String reference) { + this.reference = reference; + return this; + } + + public Builder withEmail(String email) { + this.email = email; + return this; + } + + public Builder withState(String state) { + this.state = state; + return this; + } + + public Builder withCardBrand(String cardBrand) { + this.cardBrand = cardBrand; + + if (isNotBlank(cardBrand)) { + this.cardBrand = cardBrand.toLowerCase(); + } + + return this; + } + + public Builder withFromDate(String fromDate) { + this.fromDate = fromDate; + return this; + } + + public Builder withToDate(String toDate) { + this.toDate = toDate; + return this; + } + + public Builder withPageNumber(String pageNumber) { + this.pageNumber = pageNumber; + return this; + } + + public Builder withDisplaySize(String displaySize) { + this.displaySize = displaySize; + return this; + } + + public Builder withCardHolderName(String cardHolderName) { + this.cardHolderName = cardHolderName; + return this; + } + + public Builder withFirstDigitsCardNumber(String firstDigitsCardNumber) { + this.firstDigitsCardNumber = firstDigitsCardNumber; + return this; + } + + public Builder withLastDigitsCardNumber(String lastDigitsCardNumber) { + this.lastDigitsCardNumber = lastDigitsCardNumber; + return this; + } + + public Builder withFromSettledDate(String fromSettledDate) { + this.fromSettledDate = fromSettledDate; + return this; + } + + public Builder withToSettledDate(String toSettledDate) { + this.toSettledDate = toSettledDate; + return this; + } + + public Builder withAgreementId(String agreementId) { + this.agreementId = agreementId; + return this; + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/PaymentSearchService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/PaymentSearchService.java new file mode 100644 index 000000000..d12ba562b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/PaymentSearchService.java @@ -0,0 +1,69 @@ +package uk.gov.pay.api.service; + +import black.door.hate.HalRepresentation; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.TransactionResponse; +import uk.gov.pay.api.model.search.PaginationDecorator; +import uk.gov.pay.api.model.search.card.PaymentForSearchResult; +import uk.gov.pay.api.model.search.card.PaymentSearchResponse; + +import javax.inject.Inject; +import javax.ws.rs.core.Response; +import java.util.List; + +import static java.util.stream.Collectors.toList; +import static org.apache.http.HttpHeaders.CACHE_CONTROL; +import static org.apache.http.HttpHeaders.PRAGMA; +import static uk.gov.pay.api.validation.PaymentSearchValidator.validateSearchParameters; + +public class PaymentSearchService { + + private static final String PAYMENTS_PATH = "/v1/payments"; + + private final PublicApiUriGenerator publicApiUriGenerator; + private final PaginationDecorator paginationDecorator; + private LedgerService ledgerService; + + @Inject + public PaymentSearchService(PublicApiUriGenerator publicApiUriGenerator, + PaginationDecorator paginationDecorator, + LedgerService ledgerService) { + this.publicApiUriGenerator = publicApiUriGenerator; + this.paginationDecorator = paginationDecorator; + this.ledgerService = ledgerService; + } + + public Response searchLedgerPayments(Account account, PaymentSearchParams searchParams) { + validateSearchParameters(searchParams); + + PaymentSearchResponse paymentSearchResponse = + ledgerService.searchPayments(account, searchParams.getParamsAsMap()); + return processLedgerResponse(paymentSearchResponse); + } + + private Response processLedgerResponse(PaymentSearchResponse paymentSearchResponse) { + List chargeFromResponses = paymentSearchResponse.getPayments() + .stream() + .map(t -> PaymentForSearchResult.valueOf( + t, + publicApiUriGenerator.getPaymentURI(t.getTransactionId()), + publicApiUriGenerator.getPaymentEventsURI(t.getTransactionId()), + publicApiUriGenerator.getPaymentCancelURI(t.getTransactionId()), + publicApiUriGenerator.getPaymentRefundsURI(t.getTransactionId()), + publicApiUriGenerator.getPaymentCaptureURI(t.getTransactionId()))) + .collect(toList()); + + HalRepresentation.HalRepresentationBuilder halRepresentation = HalRepresentation + .builder() + .addProperty("results", chargeFromResponses); + + return Response.ok() + .header(PRAGMA, "no-cache") + .header(CACHE_CONTROL, "no-store") + .entity(paginationDecorator + .decoratePagination(halRepresentation, paymentSearchResponse, PAYMENTS_PATH) + .build() + .toString()) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/PaymentUriGenerator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/PaymentUriGenerator.java new file mode 100644 index 000000000..01cf75dca --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/PaymentUriGenerator.java @@ -0,0 +1,37 @@ +package uk.gov.pay.api.service; + +import javax.ws.rs.core.UriBuilder; +import java.net.URI; + +public class PaymentUriGenerator { + public URI getPaymentURI(String baseUrl, String chargeId) { + return UriBuilder.fromUri(baseUrl) + .path("/v1/payments/{paymentId}") + .build(chargeId); + } + + public URI getPaymentEventsURI(String baseUrl, String chargeId) { + return UriBuilder.fromUri(baseUrl) + .path("/v1/payments/{paymentId}/events") + .build(chargeId); + } + + public URI getPaymentCancelURI(String baseUrl, String chargeId) { + return UriBuilder.fromUri(baseUrl) + .path("/v1/payments/{paymentId}/cancel") + .build(chargeId); + } + + public URI getPaymentRefundsURI(String baseUrl, String chargeId) { + return UriBuilder.fromUri(baseUrl) + .path("/v1/payments/{paymentId}/refunds") + .build(chargeId); + } + + public URI getPaymentCaptureURI(String baseUrl, String chargeId) { + return UriBuilder.fromUri(baseUrl) + .path("/v1/payments/{paymentId}/capture") + .build(chargeId); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/PublicApiUriGenerator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/PublicApiUriGenerator.java new file mode 100644 index 000000000..766aa950b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/PublicApiUriGenerator.java @@ -0,0 +1,69 @@ +package uk.gov.pay.api.service; + +import uk.gov.pay.api.app.config.PublicApiConfig; + +import javax.inject.Inject; +import javax.ws.rs.core.UriBuilder; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; + +public class PublicApiUriGenerator { + + private final String baseUrl; + + @Inject + public PublicApiUriGenerator(PublicApiConfig configuration) { + this.baseUrl = configuration.getBaseUrl(); + } + + public URI getPaymentURI(String chargeId) { + return UriBuilder.fromUri(baseUrl) + .path("/v1/payments/{paymentId}") + .build(chargeId); + } + public URI getRefundsURI(String chargeId, String refundId) { + return UriBuilder.fromUri(baseUrl) + .path("/v1/payments/{paymentId}/refunds/{refunds}") + .build(chargeId, refundId); + } + + public URI getPaymentEventsURI(String chargeId) { + return UriBuilder.fromUri(baseUrl) + .path("/v1/payments/{paymentId}/events") + .build(chargeId); + } + + public URI getPaymentCancelURI(String chargeId) { + return UriBuilder.fromUri(baseUrl) + .path("/v1/payments/{paymentId}/cancel") + .build(chargeId); + } + + public URI getPaymentRefundsURI(String chargeId) { + return UriBuilder.fromUri(baseUrl) + .path("/v1/payments/{paymentId}/refunds") + .build(chargeId); + } + + public URI getPaymentCaptureURI(String chargeId) { + return UriBuilder.fromUri(baseUrl) + .path("/v1/payments/{paymentId}/capture") + .build(chargeId); + } + + public URI getPaymentAuthorisationURI(){ + return UriBuilder.fromUri(baseUrl) + .path("/v1/auth") + .build(); + } + + public String convertHostToPublicAPI(String link) { + URI originalUri = UriBuilder.fromUri(link).build(); + URI newUri = UriBuilder.fromUri(baseUrl) + .path(originalUri.getPath()) + .replaceQuery(originalUri.getQuery()) + .build(); + return URLDecoder.decode(newUri.toString(), StandardCharsets.UTF_8); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/RefundsParams.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/RefundsParams.java new file mode 100644 index 000000000..f39e093a2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/RefundsParams.java @@ -0,0 +1,70 @@ +package uk.gov.pay.api.service; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +public class RefundsParams { + + private static final String PAGE = "page"; + private static final String DISPLAY_SIZE = "display_size"; + private static final String DEFAULT_PAGE = "1"; + private static final String DEFAULT_DISPLAY_SIZE = "500"; + private static final String FROM_DATE = "from_date"; + private static final String TO_DATE = "to_date"; + private static final String FROM_SETTLED_DATE = "from_settled_date"; + private static final String TO_SETTLED_DATE = "to_settled_date"; + + private String fromDate; + private String toDate; + private String page; + private String displaySize; + private String fromSettledDate; + private String toSettledDate; + + public RefundsParams(String fromDate, String toDate, String page, + String displaySize, String fromSettledDate, String toSettledDate) { + this.fromDate = fromDate; + this.toDate = toDate; + this.page = page; + this.displaySize = displaySize; + this.fromSettledDate = fromSettledDate; + this.toSettledDate = toSettledDate; + } + + public Map getParamsAsMap() { + Map params = new LinkedHashMap<>(); + params.put(FROM_DATE, fromDate); + params.put(TO_DATE, toDate); + params.put(PAGE, Optional.ofNullable(page).orElse(DEFAULT_PAGE)); + params.put(DISPLAY_SIZE, Optional.ofNullable(displaySize).orElse(DEFAULT_DISPLAY_SIZE)); + params.put(FROM_SETTLED_DATE, fromSettledDate); + params.put(TO_SETTLED_DATE, toSettledDate); + + return params; + } + + public String getPage() { + return page; + } + + public String getDisplaySize() { + return displaySize; + } + + public String getFromDate() { + return fromDate; + } + + public String getToDate() { + return toDate; + } + + public String getFromSettledDate() { + return fromSettledDate; + } + + public String getToSettledDate() { + return toSettledDate; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/SearchDisputesService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/SearchDisputesService.java new file mode 100644 index 000000000..7cd896153 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/SearchDisputesService.java @@ -0,0 +1,65 @@ +package uk.gov.pay.api.service; + +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.ledger.SearchDisputesResponseFromLedger; +import uk.gov.pay.api.model.search.PaginationDecorator; +import uk.gov.pay.api.model.search.dispute.DisputeForSearchResult; +import uk.gov.pay.api.model.search.dispute.DisputesSearchResults; + +import javax.inject.Inject; +import java.util.List; +import java.util.stream.Collectors; + +public class SearchDisputesService { + private static final String DISPUTES_PATH = "/v1/disputes"; + private final LedgerService ledgerService; + private final PublicApiUriGenerator publicApiUriGenerator; + private final PaginationDecorator paginationDecorator; + + @Inject + public SearchDisputesService(LedgerService ledgerService, + PublicApiUriGenerator publicApiUriGenerator, + PaginationDecorator paginationDecorator) { + + this.ledgerService = ledgerService; + this.publicApiUriGenerator = publicApiUriGenerator; + this.paginationDecorator = paginationDecorator; + } + + public DisputesSearchResults searchDisputes(Account account, DisputesSearchParams params) { + SearchDisputesResponseFromLedger disputesFromLedger = ledgerService.searchDisputes(account, params.getParamsAsMap()); + return processLedgerResponse(disputesFromLedger); + } + + private DisputesSearchResults processLedgerResponse(SearchDisputesResponseFromLedger searchResponse) { + List results = searchResponse.getDisputes() + .stream() + .map(dispute -> DisputeForSearchResult.valueOf(dispute, + publicApiUriGenerator.getPaymentURI(dispute.getParentTransactionId()))) + .collect(Collectors.toList()); + + reWriteSearchLinks(searchResponse); + + return new DisputesSearchResults(searchResponse.getTotal(), searchResponse.getCount(), searchResponse.getPage(), + results, paginationDecorator.transformLinksToPublicApiUri(searchResponse.getLinks(), DISPUTES_PATH)); + } + + private void reWriteSearchLinks(SearchDisputesResponseFromLedger searchResponse) { + var links = searchResponse.getLinks(); + if (links.getSelf() != null) { + links.withSelfLink(links.getSelf().getHref().replace("state", "status")); + } + if (links.getFirstPage() != null) { + links.withFirstLink(links.getFirstPage().getHref().replaceAll("state", "status")); + } + if (links.getLastPage() != null) { + links.withLastLink(links.getLastPage().getHref().replaceAll("state", "status")); + } + if (links.getPrevPage() != null) { + links.withPrevLink(links.getPrevPage().getHref().replaceAll("state", "status")); + } + if (links.getLastPage() != null) { + links.withLastLink(links.getLastPage().getHref().replaceAll("state", "status")); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/SearchRefundsService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/SearchRefundsService.java new file mode 100644 index 000000000..d2bd1548c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/SearchRefundsService.java @@ -0,0 +1,58 @@ +package uk.gov.pay.api.service; + +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.ledger.SearchRefundsResponseFromLedger; +import uk.gov.pay.api.model.search.PaginationDecorator; +import uk.gov.pay.api.model.search.card.RefundForSearchRefundsResult; +import uk.gov.pay.api.model.search.card.SearchRefundsResults; + +import javax.inject.Inject; +import java.util.List; +import java.util.stream.Collectors; + +import static uk.gov.pay.api.validation.RefundSearchValidator.validateSearchParameters; + +public class SearchRefundsService { + + private static final String REFUNDS_PATH = "/v1/refunds"; + private final PaginationDecorator paginationDecorator; + private LedgerService ledgerService; + private PublicApiUriGenerator publicApiUriGenerator; + + @Inject + public SearchRefundsService(LedgerService ledgerService, + PublicApiUriGenerator publicApiUriGenerator, + PaginationDecorator paginationDecorator) { + + this.ledgerService = ledgerService; + this.publicApiUriGenerator = publicApiUriGenerator; + this.paginationDecorator = paginationDecorator; + } + + public SearchRefundsResults searchLedgerRefunds(Account account, RefundsParams params) { + validateSearchParameters(params); + SearchRefundsResponseFromLedger refunds + = ledgerService.searchRefunds(account, params.getParamsAsMap()); + return processLedgerResponse(refunds); + } + + private SearchRefundsResults processLedgerResponse(SearchRefundsResponseFromLedger searchResponse) { + List results = searchResponse.getRefunds() + .stream() + .map(refund -> RefundForSearchRefundsResult.valueOf( + refund, + publicApiUriGenerator.getPaymentURI(refund.getParentTransactionId()), + publicApiUriGenerator.getRefundsURI(refund.getParentTransactionId(), + refund.getTransactionId())) + ) + .collect(Collectors.toList()); + + return new SearchRefundsResults( + searchResponse.getTotal(), + searchResponse.getCount(), + searchResponse.getPage(), + results, + paginationDecorator.transformLinksToPublicApiUri(searchResponse.getLinks(), REFUNDS_PATH) + ); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/telephone/CreateTelephonePaymentService.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/telephone/CreateTelephonePaymentService.java new file mode 100644 index 000000000..d621e3bee --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/service/telephone/CreateTelephonePaymentService.java @@ -0,0 +1,59 @@ +package uk.gov.pay.api.service.telephone; + + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.http.HttpStatus; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.CreateChargeException; +import uk.gov.pay.api.model.ChargeFromResponse; +import uk.gov.pay.api.model.telephone.CreateTelephonePaymentRequest; +import uk.gov.pay.api.model.telephone.TelephonePaymentResponse; +import uk.gov.pay.api.service.ConnectorUriGenerator; + +import javax.inject.Inject; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import static javax.ws.rs.client.Entity.json; + +public class CreateTelephonePaymentService { + + private final Client client; + private final ConnectorUriGenerator connectorUriGenerator; + + @Inject + public CreateTelephonePaymentService(Client client, ConnectorUriGenerator connectorUriGenerator) { + this.client = client; + this.connectorUriGenerator = connectorUriGenerator; + } + + public Pair create(Account account, CreateTelephonePaymentRequest createTelephonePaymentRequest) { + Response connectorResponse = createTelephoneCharge(account, createTelephonePaymentRequest); + + if (!createdSuccessfully(connectorResponse)) { + throw new CreateChargeException(connectorResponse); + } + + ChargeFromResponse chargeFromResponse = connectorResponse.readEntity(ChargeFromResponse.class); + TelephonePaymentResponse telephonePaymentResponse = TelephonePaymentResponse.from(chargeFromResponse); + return Pair.of(telephonePaymentResponse, connectorResponse.getStatus()); + } + + private boolean createdSuccessfully(Response connectorResponse) { + return connectorResponse.getStatus() == HttpStatus.SC_CREATED || connectorResponse.getStatus() == HttpStatus.SC_OK; + } + + private Response createTelephoneCharge(Account account, CreateTelephonePaymentRequest createTelephonePaymentRequest) { + return client + .target(connectorUriGenerator.telephoneChargesURI(account)) + .request() + .accept(MediaType.APPLICATION_JSON) + .post(buildTelephoneChargeRequestPayload(createTelephonePaymentRequest)); + } + + private Entity buildTelephoneChargeRequestPayload(CreateTelephonePaymentRequest requestPayload) { + return json(requestPayload.toConnectorPayload()); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/utils/CustomSupportedLanguageDeserializer.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/utils/CustomSupportedLanguageDeserializer.java new file mode 100644 index 000000000..4f565f0a7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/utils/CustomSupportedLanguageDeserializer.java @@ -0,0 +1,24 @@ +package uk.gov.pay.api.utils; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import uk.gov.service.payments.commons.model.SupportedLanguage; + +import java.io.IOException; + +public class CustomSupportedLanguageDeserializer extends StdDeserializer { + + public CustomSupportedLanguageDeserializer() { + this(null); + } + + public CustomSupportedLanguageDeserializer(Class vc) { + super(vc); + } + + @Override + public SupportedLanguage deserialize(JsonParser jsonparser, DeserializationContext context) throws IOException { + return SupportedLanguage.fromIso639AlphaTwoCode(jsonparser.getText()); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/utils/JsonStringBuilder.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/utils/JsonStringBuilder.java new file mode 100644 index 000000000..c37c7ad5c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/utils/JsonStringBuilder.java @@ -0,0 +1,104 @@ +package uk.gov.pay.api.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializationFeature; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class JsonStringBuilder { + private static final ObjectWriter WRITER = new ObjectMapper() + .writer(); + + private String root; + private Map map; + + private boolean prettyPrint; + + public JsonStringBuilder() { + map = new LinkedHashMap<>(); + prettyPrint = true; + } + + public JsonStringBuilder noPrettyPrint() { + prettyPrint = false; + return this; + } + + public JsonStringBuilder addRoot(String root) { + this.root = root; + return this; + } + + public JsonStringBuilder add(String key, Object value) { + if (value != null) { + map.put(key, value); + } + return this; + } + + public JsonStringBuilder addToMap(String mapKey, String key, Object value) { + Map nestedMap = ensureNestedMap(mapKey); + nestedMap.put(key, value); + return this; + } + + public JsonStringBuilder addToMap(String mapKey) { + ensureNestedMap(mapKey); + return this; + } + + public JsonStringBuilder addToNestedMap(String key, Object value, String... mapKeys) { + Map localMap = map; + for (String mapKey : mapKeys) { + localMap = ensureNestedMap(localMap, mapKey); + } + localMap.put(key, value); + return this; + } + + public String build() { + ObjectWriter writer = WRITER; + if (prettyPrint) { + writer = writer.withDefaultPrettyPrinter(); + } + + if (root != null) { + writer = writer + .with(SerializationFeature.WRAP_ROOT_VALUE) + .withRootName(root); + } + + return asJsonString(writer, map); + } + + private String asJsonString(ObjectWriter writer, Object object) { + try { + return writer.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Error processing json object to string", e); + } + } + + @SuppressWarnings("unchecked") + private Map ensureNestedMap(String mapKey) { + Map nestedMap = (Map) map.get(mapKey); + if (nestedMap == null) { + nestedMap = new LinkedHashMap<>(); + map.put(mapKey, nestedMap); + } + return nestedMap; + } + + @SuppressWarnings("unchecked") + private Map ensureNestedMap(Map localMap, String mapKey) { + Map nestedMap = (Map) localMap.get(mapKey); + if (nestedMap == null) { + nestedMap = new LinkedHashMap<>(); + localMap.put(mapKey, nestedMap); + } + return nestedMap; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/utils/PathHelper.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/utils/PathHelper.java new file mode 100644 index 000000000..3528b1c56 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/utils/PathHelper.java @@ -0,0 +1,20 @@ +package uk.gov.pay.api.utils; + +import org.apache.commons.lang3.StringUtils; + +public class PathHelper { + + public static String getPathType(String pathValue, String method) { + String path = StringUtils.removeEnd(pathValue, "/"); + + if (path.endsWith("/capture")) { + return "capture_payment"; + } + + if (path.endsWith("/payments") && method.equals("POST")) { + return "create_payment"; + } + + return ""; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/utils/WalletDeserializer.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/utils/WalletDeserializer.java new file mode 100644 index 000000000..f68e174db --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/utils/WalletDeserializer.java @@ -0,0 +1,29 @@ +package uk.gov.pay.api.utils; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.model.Wallet; + +import java.io.IOException; + +import static java.lang.String.format; + +public class WalletDeserializer extends JsonDeserializer { + + private static final Logger logger = LoggerFactory.getLogger(WalletDeserializer.class); + + @Override + public Wallet deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + String value = jsonParser.getText().toUpperCase(); + try { + return Wallet.valueOf(value); + } catch (IllegalArgumentException e) { + logger.error(format("Value [%s] matches no known wallet types", value)); + return null; + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/AgreementSearchValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/AgreementSearchValidator.java new file mode 100644 index 000000000..a9c1ecf8f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/AgreementSearchValidator.java @@ -0,0 +1,67 @@ +package uk.gov.pay.api.validation; + +import uk.gov.pay.api.exception.AgreementValidationException; +import uk.gov.pay.api.exception.BadRequestException; +import uk.gov.pay.api.ledger.model.AgreementSearchParams; +import uk.gov.pay.api.model.RequestError; +import uk.gov.service.payments.commons.model.agreement.AgreementStatus; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import static java.util.Locale.ENGLISH; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.join; +import static org.eclipse.jetty.util.StringUtil.isBlank; +import static uk.gov.pay.api.common.SearchConstants.DISPLAY_SIZE; +import static uk.gov.pay.api.common.SearchConstants.PAGE; +import static uk.gov.pay.api.common.SearchConstants.REFERENCE_KEY; +import static uk.gov.pay.api.common.SearchConstants.STATUS_KEY; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.REFERENCE_MAX_LENGTH; +import static uk.gov.pay.api.model.RequestError.Code.SEARCH_AGREEMENTS_VALIDATION_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; +import static uk.gov.pay.api.validation.MaxLengthValidator.isInvalid; +import static uk.gov.pay.api.validation.SearchValidator.validateDisplaySizeIfNotNull; +import static uk.gov.pay.api.validation.SearchValidator.validatePageIfNotNull; + +public class AgreementSearchValidator { + + private static final Set SUPPORTED_SEARCH_PARAMS = Set.of(REFERENCE_KEY, STATUS_KEY, PAGE, DISPLAY_SIZE); + + public static void validateSearchParameters(AgreementSearchParams searchParams) { + List validationErrors = new LinkedList<>(); + try { + validateStatus(searchParams.getStatus(), validationErrors); + validateReference(searchParams.getReference(), validationErrors); + validatePageIfNotNull(searchParams.getPageNumber(), validationErrors); + validateDisplaySizeIfNotNull(searchParams.getDisplaySize(), validationErrors); + } catch (Exception e) { + throw new AgreementValidationException(aRequestError(SEARCH_AGREEMENTS_VALIDATION_ERROR, join(validationErrors, ", "), e.getMessage())); + } + if (!validationErrors.isEmpty()) { + throw new AgreementValidationException(aRequestError(SEARCH_AGREEMENTS_VALIDATION_ERROR, join(validationErrors, ", "))); + } + + searchParams.getQueryMap().entrySet().stream() + .filter(queryParam -> !SUPPORTED_SEARCH_PARAMS.contains(queryParam.getKey()) && isNotBlank(queryParam.getValue())) + .findFirst() + .ifPresent(invalidParam -> { + throw new BadRequestException(RequestError.aRequestError(SEARCH_AGREEMENTS_VALIDATION_ERROR, invalidParam.getKey())); + }); + } + + private static void validateReference(String reference, List validationErrors) { + if (isInvalid(reference, REFERENCE_MAX_LENGTH)) { + validationErrors.add("reference"); + } + } + + private static void validateStatus(String status, List validationErrors) { + if (!(isBlank(status) || Arrays.stream(AgreementStatus.values()).anyMatch(validStatus -> validStatus.name().toLowerCase(ENGLISH).equals(status)))) { + validationErrors.add("status"); + } + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/CardExpiryValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/CardExpiryValidator.java new file mode 100644 index 000000000..73ded15c7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/CardExpiryValidator.java @@ -0,0 +1,15 @@ +package uk.gov.pay.api.validation; + +import uk.gov.service.payments.commons.model.CardExpiryDate; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class CardExpiryValidator implements ConstraintValidator { + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return value == null || CardExpiryDate.CARD_EXPIRY_DATE_PATTERN.matcher(value).matches(); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/CardFirstSixDigitsValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/CardFirstSixDigitsValidator.java new file mode 100644 index 000000000..0e917a6b2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/CardFirstSixDigitsValidator.java @@ -0,0 +1,22 @@ +package uk.gov.pay.api.validation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +public class CardFirstSixDigitsValidator implements ConstraintValidator { + + private Pattern pattern = Pattern.compile("\\d{6}"); + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + + if (value == null) { + return true; + } + + return pattern.matcher(value).matches(); + + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/CardLastFourDigitsValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/CardLastFourDigitsValidator.java new file mode 100644 index 000000000..2703f6531 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/CardLastFourDigitsValidator.java @@ -0,0 +1,21 @@ +package uk.gov.pay.api.validation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +public class CardLastFourDigitsValidator implements ConstraintValidator { + + private Pattern pattern = Pattern.compile("\\d{4}"); + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + + if (value == null) { + return true; + } + + return pattern.matcher(value).matches(); + + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/CardTypeValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/CardTypeValidator.java new file mode 100644 index 000000000..ec8e3c2a8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/CardTypeValidator.java @@ -0,0 +1,30 @@ +package uk.gov.pay.api.validation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.HashSet; + +public class CardTypeValidator implements ConstraintValidator { + + private static final HashSet CARD_TYPES = new HashSet<>(); + + static { + CARD_TYPES.add("master-card"); + CARD_TYPES.add("visa"); + CARD_TYPES.add("maestro"); + CARD_TYPES.add("diners-club"); + CARD_TYPES.add("american-express"); + CARD_TYPES.add("jcb"); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + + if (value == null) { + return true; + } + + return CARD_TYPES.contains(value); + + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/DisputeSearchValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/DisputeSearchValidator.java new file mode 100644 index 000000000..02fb0dc2d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/DisputeSearchValidator.java @@ -0,0 +1,61 @@ +package uk.gov.pay.api.validation; + +import com.google.common.collect.ImmutableList; +import uk.gov.pay.api.exception.DisputesValidationException; +import uk.gov.pay.api.service.DisputesSearchParams; + +import java.util.LinkedList; +import java.util.List; + +import static org.apache.commons.lang3.StringUtils.join; +import static org.eclipse.jetty.util.StringUtil.isBlank; +import static uk.gov.pay.api.model.RequestError.Code.SEARCH_DISPUTES_VALIDATION_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; +import static uk.gov.pay.api.validation.SearchValidator.validateDisplaySizeIfNotNull; +import static uk.gov.pay.api.validation.SearchValidator.validateFromDate; +import static uk.gov.pay.api.validation.SearchValidator.validateFromSettledDate; +import static uk.gov.pay.api.validation.SearchValidator.validatePageIfNotNull; +import static uk.gov.pay.api.validation.SearchValidator.validateToDate; +import static uk.gov.pay.api.validation.SearchValidator.validateToSettledDate; + + +public class DisputeSearchValidator { + private static final List VALID_DISPUTE_STATES = ImmutableList.of("needs_response", "under_review", + "lost", "won"); + + public static void validateDisputeParameters(DisputesSearchParams params) { + String pageNumber = params.getPage(); + String displaySize = params.getDisplaySize(); + String fromSettledDate = params.getFromSettledDate(); + String toSettledDate = params.getToSettledDate(); + String fromDate = params.getFromDate(); + String toDate = params.getToDate(); + String state = params.getState(); + + List validationErrors = new LinkedList<>(); + try { + validateFromDate(fromDate, validationErrors); + validateToDate(toDate, validationErrors); + validateFromSettledDate(fromSettledDate, validationErrors); + validateToSettledDate(toSettledDate, validationErrors); + validatePageIfNotNull(pageNumber, validationErrors); + validateDisplaySizeIfNotNull(displaySize, validationErrors); + validateState(state, validationErrors); + } catch (Exception e) { + throw new DisputesValidationException(aRequestError(SEARCH_DISPUTES_VALIDATION_ERROR, join(validationErrors, ", "), e.getMessage())); + } + if (!validationErrors.isEmpty()) { + throw new DisputesValidationException(aRequestError(SEARCH_DISPUTES_VALIDATION_ERROR, join(validationErrors, ", "))); + } + } + + private static void validateState(String state, List validationErrors) { + if (!validateState(state)) { + validationErrors.add("state"); + } + } + + private static boolean validateState(String state) { + return isBlank(state) || VALID_DISPUTE_STATES.contains(state); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ExactLengthOrEmpty.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ExactLengthOrEmpty.java new file mode 100644 index 000000000..8c05800e8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ExactLengthOrEmpty.java @@ -0,0 +1,29 @@ +package uk.gov.pay.api.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE }) +@Retention(RUNTIME) +@Constraint(validatedBy = ExactLengthOrEmptyValidator.class) +@Documented +public @interface ExactLengthOrEmpty { + + String message() default "Must be exactly {length} characters length"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + int length(); +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ExactLengthOrEmptyValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ExactLengthOrEmptyValidator.java new file mode 100644 index 000000000..59de8ff2a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ExactLengthOrEmptyValidator.java @@ -0,0 +1,25 @@ +package uk.gov.pay.api.validation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +class ExactLengthOrEmptyValidator implements ConstraintValidator { + + private int length; + + static boolean isValid(String value, int length) { + return isBlank(value) || value.length() == length; + } + + @Override + public void initialize(ExactLengthOrEmpty constraintAnnotation) { + this.length = constraintAnnotation.length(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return isValid(value, length); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/InjectingConfiguredValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/InjectingConfiguredValidator.java new file mode 100644 index 000000000..4eb2d3c64 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/InjectingConfiguredValidator.java @@ -0,0 +1,14 @@ +package uk.gov.pay.api.validation; + +import io.dropwizard.jersey.validation.DropwizardConfiguredValidator; + +import javax.inject.Inject; +import javax.validation.Validator; + +public class InjectingConfiguredValidator extends DropwizardConfiguredValidator { + + @Inject + public InjectingConfiguredValidator(Validator validator) { + super(validator); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/InjectingConstraintValidatorFactory.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/InjectingConstraintValidatorFactory.java new file mode 100644 index 000000000..88a181705 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/InjectingConstraintValidatorFactory.java @@ -0,0 +1,26 @@ +package uk.gov.pay.api.validation; + +import com.google.inject.Injector; + +import javax.inject.Singleton; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorFactory; + +@Singleton +public class InjectingConstraintValidatorFactory implements ConstraintValidatorFactory { + + private Injector injector; + + public InjectingConstraintValidatorFactory(Injector injector) { + this.injector = injector; + } + + @Override + public > T getInstance(Class key) { + return injector.getInstance(key); + } + + @Override + public void releaseInstance(ConstraintValidator instance) { + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/InjectingValidationFeature.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/InjectingValidationFeature.java new file mode 100644 index 000000000..26e4a001e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/InjectingValidationFeature.java @@ -0,0 +1,32 @@ +package uk.gov.pay.api.validation; + +import com.google.inject.Injector; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.internal.inject.ConfiguredValidator; + +import javax.inject.Singleton; +import javax.validation.ConstraintValidatorFactory; +import javax.validation.Validator; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; + +public class InjectingValidationFeature implements Feature { + private Injector injector; + + public InjectingValidationFeature(Injector injector) { + this.injector = injector; + } + + @Override + public boolean configure(FeatureContext context) { + context.register(new AbstractBinder() { + @Override + protected void configure() { + bindFactory(ValidatorFactory.class).to(Validator.class).in(Singleton.class); + bind(InjectingConfiguredValidator.class).to(ConfiguredValidator.class).in(Singleton.class); + bind(new InjectingConstraintValidatorFactory(injector)).to(ConstraintValidatorFactory.class); + } + }); + return true; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/MaxLengthValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/MaxLengthValidator.java new file mode 100644 index 000000000..0c4b9b5b4 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/MaxLengthValidator.java @@ -0,0 +1,9 @@ +package uk.gov.pay.api.validation; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +class MaxLengthValidator { + static boolean isInvalid(String value, int maxLength) { + return isNotBlank(value) && value.length() > maxLength; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/NumericValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/NumericValidator.java new file mode 100644 index 000000000..7a5ba0aab --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/NumericValidator.java @@ -0,0 +1,17 @@ +package uk.gov.pay.api.validation; + +import java.util.regex.Pattern; + +import static org.eclipse.jetty.util.StringUtil.isBlank; + +class NumericValidator { + private static final Pattern ALL_DIGITS = Pattern.compile("[0-9]+"); + + static boolean isValidOrNull(String value) { + return isBlank(value) || ALL_DIGITS.matcher(value).matches(); + } + + static boolean isValid(String value) { + return ALL_DIGITS.matcher(value).matches(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/PaymentOutcomeValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/PaymentOutcomeValidator.java new file mode 100644 index 000000000..ed33d6454 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/PaymentOutcomeValidator.java @@ -0,0 +1,32 @@ +package uk.gov.pay.api.validation; + +import uk.gov.pay.api.model.telephone.PaymentOutcome; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.HashSet; + +public class PaymentOutcomeValidator implements ConstraintValidator { + + private final static HashSet ERROR_CODES = new HashSet<>(); + + static { + ERROR_CODES.add("P0010"); + ERROR_CODES.add("P0030"); + ERROR_CODES.add("P0050"); + } + + @Override + public boolean isValid(PaymentOutcome paymentOutcome, ConstraintValidatorContext context) { + + if(paymentOutcome == null) { + return true; + } + + if("success".equals(paymentOutcome.getStatus()) && paymentOutcome.getCode().isEmpty()) { + return true; + } + + return "failed".equals(paymentOutcome.getStatus()) && (ERROR_CODES.contains(paymentOutcome.getCode().orElse(null))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/PaymentRefundRequestValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/PaymentRefundRequestValidator.java new file mode 100644 index 000000000..70c44ba6a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/PaymentRefundRequestValidator.java @@ -0,0 +1,33 @@ +package uk.gov.pay.api.validation; + +import uk.gov.pay.api.exception.PaymentValidationException; +import uk.gov.pay.api.model.CreatePaymentRefundRequest; +import uk.gov.pay.api.model.RequestError; + +import static java.lang.String.format; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.AMOUNT_FIELD_NAME; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.AMOUNT_MAX_VALUE; +import static uk.gov.pay.api.model.CreatePaymentRefundRequest.REFUND_MIN_VALUE; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_REFUND_VALIDATION_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class PaymentRefundRequestValidator { + + public void validate(CreatePaymentRefundRequest paymentRefundRequest) { + validateAmount(paymentRefundRequest.getAmount()); + } + + private void validateAmount(int amount) { + validate(amount >= REFUND_MIN_VALUE, + aRequestError(AMOUNT_FIELD_NAME, CREATE_PAYMENT_REFUND_VALIDATION_ERROR, format("Must be greater than or equal to %d", REFUND_MIN_VALUE))); + + validate(amount <= AMOUNT_MAX_VALUE, + aRequestError(AMOUNT_FIELD_NAME, CREATE_PAYMENT_REFUND_VALIDATION_ERROR, format("Must be less than or equal to %d", AMOUNT_MAX_VALUE))); + } + + private static void validate(boolean condition, RequestError error) { + if (!condition) { + throw new PaymentValidationException(error); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/PaymentSearchValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/PaymentSearchValidator.java new file mode 100644 index 000000000..31126720e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/PaymentSearchValidator.java @@ -0,0 +1,117 @@ +package uk.gov.pay.api.validation; + +import uk.gov.pay.api.exception.PaymentValidationException; +import uk.gov.pay.api.service.PaymentSearchParams; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import static org.apache.commons.lang3.StringUtils.join; +import static org.eclipse.jetty.util.StringUtil.isBlank; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.EMAIL_MAX_LENGTH; +import static uk.gov.pay.api.model.CreateCardPaymentRequest.REFERENCE_MAX_LENGTH; +import static uk.gov.pay.api.model.RequestError.Code.SEARCH_PAYMENTS_VALIDATION_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; +import static uk.gov.pay.api.validation.MaxLengthValidator.isInvalid; +import static uk.gov.pay.api.validation.SearchValidator.validateDisplaySizeIfNotNull; +import static uk.gov.pay.api.validation.SearchValidator.validateFromDate; +import static uk.gov.pay.api.validation.SearchValidator.validateFromSettledDate; +import static uk.gov.pay.api.validation.SearchValidator.validatePageIfNotNull; +import static uk.gov.pay.api.validation.SearchValidator.validateToDate; +import static uk.gov.pay.api.validation.SearchValidator.validateToSettledDate; + + +public class PaymentSearchValidator { + private static final int FIRST_DIGITS_CARD_NUMBER_LENGTH = 6; + private static final int LAST_DIGITS_CARD_NUMBER_LENGTH = 4; + + // we should really find a way to not have this anywhere but in the connector... + private static final Set VALID_CARD_PAYMENT_STATES = + new HashSet<>(Arrays.asList("created", "started", "submitted", "success", "failed", "cancelled", "error")); + + public static void validateSearchParameters(PaymentSearchParams searchParams) { + validateSearchParameters(searchParams.getState(), searchParams.getReference(), + searchParams.getEmail(), searchParams.getCardBrand(), searchParams.getFromDate(), + searchParams.getToDate(), searchParams.getPageNumber(), searchParams.getDisplaySize(), + searchParams.getFirstDigitsCardNumber(), + searchParams.getLastDigitsCardNumber(), searchParams.getFromSettledDate(), + searchParams.getToSettledDate()); + } + + public static void validateSearchParameters(String state, + String reference, + String email, + String cardBrand, + String fromDate, + String toDate, + String pageNumber, + String displaySize, + String firstDigitsCardNumber, + String lastDigitsCardNumber, + String fromSettledDate, + String toSettledDate) { + List validationErrors = new LinkedList<>(); + try { + validateState(state, validationErrors); + validateReference(reference, validationErrors); + validateEmail(email, validationErrors); + validateCardBrand(cardBrand, validationErrors); + validateFromDate(fromDate, validationErrors); + validateToDate(toDate, validationErrors); + validatePageIfNotNull(pageNumber, validationErrors); + validateDisplaySizeIfNotNull(displaySize, validationErrors); + validateFirstDigitsCardNumber(firstDigitsCardNumber, validationErrors); + validateLastDigitsCardNumber(lastDigitsCardNumber, validationErrors); + validateFromSettledDate(fromSettledDate, validationErrors); + validateToSettledDate(toSettledDate, validationErrors); + } catch (Exception e) { + throw new PaymentValidationException(aRequestError(SEARCH_PAYMENTS_VALIDATION_ERROR, join(validationErrors, ", "), e.getMessage())); + } + if (!validationErrors.isEmpty()) { + throw new PaymentValidationException(aRequestError(SEARCH_PAYMENTS_VALIDATION_ERROR, join(validationErrors, ", "))); + } + } + + private static void validateFirstDigitsCardNumber(String firstDigitsCardNumber, List validationErrors) { + if (!ExactLengthOrEmptyValidator.isValid(firstDigitsCardNumber, FIRST_DIGITS_CARD_NUMBER_LENGTH) || !NumericValidator.isValidOrNull(firstDigitsCardNumber)) { + validationErrors.add("first_digits_card_number"); + } + } + + private static void validateLastDigitsCardNumber(String lastDigitsCardNumber, List validationErrors) { + if (!ExactLengthOrEmptyValidator.isValid(lastDigitsCardNumber, LAST_DIGITS_CARD_NUMBER_LENGTH) || !NumericValidator.isValidOrNull(lastDigitsCardNumber)) { + validationErrors.add("last_digits_card_number"); + } + } + + private static void validateReference(String reference, List validationErrors) { + if (isInvalid(reference, REFERENCE_MAX_LENGTH)) { + validationErrors.add("reference"); + } + } + + private static void validateEmail(String email, List validationErrors) { + if (isInvalid(email, EMAIL_MAX_LENGTH)) { + validationErrors.add("email"); + } + } + + private static void validateCardBrand(String cardBrand, List validationErrors) { + if (isInvalid(cardBrand, 20)) { + validationErrors.add("card_brand"); + } + } + + private static void validateState(String state, List validationErrors) { + if (!validateState(state)) { + validationErrors.add("state"); + } + } + + private static boolean validateState(String state) { + return isBlank(state) || VALID_CARD_PAYMENT_STATES.contains(state); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/RefundSearchValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/RefundSearchValidator.java new file mode 100644 index 000000000..1d30e8c0f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/RefundSearchValidator.java @@ -0,0 +1,47 @@ +package uk.gov.pay.api.validation; + +import uk.gov.pay.api.exception.RefundsValidationException; +import uk.gov.pay.api.service.RefundsParams; + +import java.util.LinkedList; +import java.util.List; + +import static org.apache.commons.lang3.StringUtils.join; +import static uk.gov.pay.api.model.RequestError.Code.SEARCH_REFUNDS_VALIDATION_ERROR; +import static uk.gov.pay.api.model.RequestError.aRequestError; +import static uk.gov.pay.api.validation.SearchValidator.validateDisplaySizeIfNotNull; +import static uk.gov.pay.api.validation.SearchValidator.validateFromDate; +import static uk.gov.pay.api.validation.SearchValidator.validateFromSettledDate; +import static uk.gov.pay.api.validation.SearchValidator.validatePageIfNotNull; +import static uk.gov.pay.api.validation.SearchValidator.validateToDate; +import static uk.gov.pay.api.validation.SearchValidator.validateToSettledDate; + + +public class RefundSearchValidator { + + public static void validateSearchParameters(RefundsParams params) { + String pageNumber = params.getPage(); + String displaySize = params.getDisplaySize(); + String fromSettledDate = params.getFromSettledDate(); + String toSettledDate = params.getToSettledDate(); + String fromDate = params.getFromDate(); + String toDate = params.getToDate(); + + List validationErrors = new LinkedList<>(); + try { + validateFromDate(fromDate, validationErrors); + validateToDate(toDate, validationErrors); + validateFromSettledDate(fromSettledDate, validationErrors); + validateToSettledDate(toSettledDate, validationErrors); + validatePageIfNotNull(pageNumber, validationErrors); + validateDisplaySizeIfNotNull(displaySize, validationErrors); + } catch (Exception e) { + throw new RefundsValidationException(aRequestError(SEARCH_REFUNDS_VALIDATION_ERROR, join(validationErrors, ", "), e.getMessage())); + } + if (!validationErrors.isEmpty()) { + throw new RefundsValidationException(aRequestError(SEARCH_REFUNDS_VALIDATION_ERROR, join(validationErrors, ", "))); + } + } + + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/SearchValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/SearchValidator.java new file mode 100644 index 000000000..ead69b428 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/SearchValidator.java @@ -0,0 +1,47 @@ +package uk.gov.pay.api.validation; + +import uk.gov.service.payments.commons.validation.DateTimeUtils; +import uk.gov.service.payments.commons.validation.DateValidator; + +import java.util.List; + +import static org.eclipse.jetty.util.StringUtil.isNotBlank; + +class SearchValidator { + + static void validatePageIfNotNull(String pageNumber, List validationErrors) { + if (isNotBlank(pageNumber) && (!NumericValidator.isValid(pageNumber) || Integer.parseInt(pageNumber) < 1)) { + validationErrors.add("page"); + } + } + + static void validateDisplaySizeIfNotNull(String displaySize, List validationErrors) { + if (isNotBlank(displaySize) && (!NumericValidator.isValid(displaySize) || Integer.parseInt(displaySize) < 1 || Integer.parseInt(displaySize) > 500)) { + validationErrors.add("display_size"); + } + } + + static void validateToDate(String toDate, List validationErrors) { + if (!DateValidator.isValid(toDate)) { + validationErrors.add("to_date"); + } + } + + static void validateFromDate(String fromDate, List validationErrors) { + if (!DateValidator.isValid(fromDate)) { + validationErrors.add("from_date"); + } + } + + static void validateFromSettledDate(String fromSettledDate, List validationErrors) { + if (isNotBlank(fromSettledDate) && !DateTimeUtils.fromLocalDateOnlyString(fromSettledDate).isPresent()) { + validationErrors.add("from_settled_date"); + } + } + + static void validateToSettledDate(String toSettledDate, List validationErrors) { + if (isNotBlank(toSettledDate) && !DateTimeUtils.fromLocalDateOnlyString(toSettledDate).isPresent()) { + validationErrors.add("to_settled_date"); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/URLValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/URLValidator.java new file mode 100644 index 000000000..64b55a951 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/URLValidator.java @@ -0,0 +1,46 @@ +package uk.gov.pay.api.validation; + +import org.apache.commons.validator.routines.DomainValidator; +import org.apache.commons.validator.routines.DomainValidator.ArrayType; +import org.apache.commons.validator.routines.UrlValidator; + +import java.util.List; + +import static org.apache.commons.validator.routines.UrlValidator.ALLOW_2_SLASHES; +import static org.apache.commons.validator.routines.UrlValidator.ALLOW_LOCAL_URLS; + +public class URLValidator { + private UrlValidator urlValidator; + + private URLValidator(boolean allowInsecureConnection) { + long options = ALLOW_LOCAL_URLS + ALLOW_2_SLASHES; + urlValidator = new UrlValidator( + schemes(allowInsecureConnection), + null, + options, + domainValidator() + ); + } + + private String[] schemes(boolean allowInsecureConnection) { + if (allowInsecureConnection) { + return new String[]{"http", "https"}; + } else { + return new String[]{"https"}; + } + } + + private DomainValidator domainValidator() { + String[] otherValidTlds = new String[]{"internal", "local"}; + DomainValidator.Item item = new DomainValidator.Item(ArrayType.GENERIC_PLUS, otherValidTlds); + return DomainValidator.getInstance(true, List.of(item)); + } + + public static URLValidator urlValidatorValueOf(boolean allowInsecureConnection) { + return new URLValidator(allowInsecureConnection); + } + + public boolean isValid(String value) { + return urlValidator.isValid(value); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidCardExpiryDate.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidCardExpiryDate.java new file mode 100644 index 000000000..95771fe6b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidCardExpiryDate.java @@ -0,0 +1,27 @@ +package uk.gov.pay.api.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE}) +@Retention(RUNTIME) +@Constraint(validatedBy = CardExpiryValidator.class) +@Documented +public @interface ValidCardExpiryDate { + + String message() default "Must be MM/YY"; + + Class[] groups() default{}; + + Class[] payload() default{}; +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidCardFirstSixDigits.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidCardFirstSixDigits.java new file mode 100644 index 000000000..1f683a209 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidCardFirstSixDigits.java @@ -0,0 +1,27 @@ +package uk.gov.pay.api.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE }) +@Retention(RUNTIME) +@Constraint(validatedBy = CardFirstSixDigitsValidator.class) +@Documented +public @interface ValidCardFirstSixDigits { + + String message() default "Must be exactly 6 digits"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidCardLastFourDigits.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidCardLastFourDigits.java new file mode 100644 index 000000000..d1ca2ed68 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidCardLastFourDigits.java @@ -0,0 +1,27 @@ +package uk.gov.pay.api.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE }) +@Retention(RUNTIME) +@Constraint(validatedBy = CardLastFourDigitsValidator.class) +@Documented +public @interface ValidCardLastFourDigits { + + String message() default "Must be exactly 4 digits"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidCardType.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidCardType.java new file mode 100644 index 000000000..33141469a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidCardType.java @@ -0,0 +1,27 @@ +package uk.gov.pay.api.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE }) +@Retention(RUNTIME) +@Constraint(validatedBy = CardTypeValidator.class) +@Documented +public @interface ValidCardType { + + String message() default "Must be either master-card, visa, maestro, diners-club, american-express or jcb"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidPaymentOutcome.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidPaymentOutcome.java new file mode 100644 index 000000000..0c3b539da --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidPaymentOutcome.java @@ -0,0 +1,28 @@ +package uk.gov.pay.api.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE, TYPE}) +@Retention(RUNTIME) +@Constraint(validatedBy = PaymentOutcomeValidator.class) +@Documented +public @interface ValidPaymentOutcome { + + String message() default "Must include a valid status and error code"; + + Class[] groups() default{}; + + Class[] payload() default{}; +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidZonedDateTime.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidZonedDateTime.java new file mode 100644 index 000000000..43a50773f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidZonedDateTime.java @@ -0,0 +1,24 @@ +package uk.gov.pay.api.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({FIELD}) +@Retention(RUNTIME) +@Constraint(validatedBy = ZoneDateTimeValidator.class) +@Documented +public @interface ValidZonedDateTime { + + String message() default "must be a valid ISO-8601 time and date format"; + + Class[] groups() default{}; + + Class[] payload() default{}; + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidatorFactory.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidatorFactory.java new file mode 100644 index 000000000..e27eba5d0 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ValidatorFactory.java @@ -0,0 +1,28 @@ +package uk.gov.pay.api.validation; + +import org.glassfish.hk2.api.Factory; + +import javax.inject.Inject; +import javax.validation.ConstraintValidatorFactory; +import javax.validation.Validation; +import javax.validation.Validator; + +public class ValidatorFactory implements Factory { + + private final ConstraintValidatorFactory constraintValidatorFactory; + + @Inject + public ValidatorFactory(ConstraintValidatorFactory constraintValidatorFactory) { + this.constraintValidatorFactory = constraintValidatorFactory; + } + + @Override + public Validator provide() { + return Validation.byDefaultProvider().configure().constraintValidatorFactory(constraintValidatorFactory) + .buildValidatorFactory().getValidator(); + } + + @Override + public void dispose(Validator validator) { + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ZoneDateTimeValidator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ZoneDateTimeValidator.java new file mode 100644 index 000000000..ad48391a9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/java/uk/gov/pay/api/validation/ZoneDateTimeValidator.java @@ -0,0 +1,24 @@ +package uk.gov.pay.api.validation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; + +public class ZoneDateTimeValidator implements ConstraintValidator { + + @Override + public boolean isValid(String date, ConstraintValidatorContext context) { + + if (date == null) { + return true; + } + + try { + ZonedDateTime.parse(date); + return true; + } catch (DateTimeParseException e) { + return false; + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/resources/assets/swagger.json b/jdk_11_maven/cs/rest/pay-publicapi/src/main/resources/assets/swagger.json new file mode 100644 index 000000000..9246c8ee7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/resources/assets/swagger.json @@ -0,0 +1,1713 @@ +{ + "swagger" : "2.0", + "info" : { + "description" : "GOV.UK Pay API (This version is no longer maintained. See openapi/publicapi_spec.json for latest API specification)", + "version" : "1.0.3", + "title" : "GOV.UK Pay API" + }, + "host" : "publicapi.payments.service.gov.uk", + "tags" : [ { + "name" : "Card payments" + }, { + "name" : "Refunding card payments" + } ], + "schemes" : [ "https" ], + "paths" : { + "/v1/payments" : { + "get" : { + "tags" : [ "Card payments" ], + "summary" : "Search payments", + "description" : "Search payments by reference, state, 'from' and 'to' date. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Search payments", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "reference", + "in" : "query", + "description" : "Your payment reference to search (exact match, case insensitive)", + "required" : false, + "type" : "string" + }, { + "name" : "email", + "in" : "query", + "description" : "The user email used in the payment to be searched", + "required" : false, + "type" : "string" + }, { + "name" : "state", + "in" : "query", + "description" : "State of payments to be searched. Example=success", + "required" : false, + "type" : "string", + "enum" : [ "created", "started", "submitted", "success", "failed", "cancelled", "error" ] + }, { + "name" : "card_brand", + "in" : "query", + "description" : "Card brand used for payment. Example=master-card", + "required" : false, + "type" : "string" + }, { + "name" : "from_date", + "in" : "query", + "description" : "From date of payments to be searched (this date is inclusive). Example=2015-08-13T12:35:00Z", + "required" : false, + "type" : "string" + }, { + "name" : "to_date", + "in" : "query", + "description" : "To date of payments to be searched (this date is exclusive). Example=2015-08-14T12:35:00Z", + "required" : false, + "type" : "string" + }, { + "name" : "page", + "in" : "query", + "description" : "Page number requested for the search, should be a positive integer (optional, defaults to 1)", + "required" : false, + "type" : "string" + }, { + "name" : "display_size", + "in" : "query", + "description" : "Number of results to be shown per page, should be a positive integer (optional, defaults to 500, max 500)", + "required" : false, + "type" : "string" + }, { + "name" : "cardholder_name", + "in" : "query", + "description" : "Name on card used to make payment", + "required" : false, + "type" : "string" + }, { + "name" : "first_digits_card_number", + "in" : "query", + "description" : "First six digits of the card used to make payment", + "required" : false, + "type" : "string" + }, { + "name" : "last_digits_card_number", + "in" : "query", + "description" : "Last four digits of the card used to make payment", + "required" : false, + "type" : "string" + }, { + "name" : "from_settled_date", + "in" : "query", + "description" : "From settled date of payment to be searched (this date is inclusive). Example=2015-08-13", + "required" : false, + "type" : "string" + }, { + "name" : "to_settled_date", + "in" : "query", + "description" : "To settled date of payment to be searched (this date is inclusive). Example=2015-08-14", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "OK", + "schema" : { + "$ref" : "#/definitions/PaymentSearchResults" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "422" : { + "description" : "Invalid parameters: from_date, to_date, status, display_size. See Public API documentation for the correct data formats", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + }, + "post" : { + "tags" : [ "Card payments" ], + "summary" : "Create new payment", + "description" : "Create a new payment for the account associated to the Authorisation token. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Create a payment", + "consumes" : [ "application/json" ], + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "description" : "requestPayload", + "required" : true, + "schema" : { + "$ref" : "#/definitions/CreateCardPaymentRequest" + } + } ], + "responses" : { + "201" : { + "description" : "Created", + "schema" : { + "$ref" : "#/definitions/CreatePaymentResult" + } + }, + "400" : { + "description" : "Bad request", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "422" : { + "description" : "Invalid attribute value: description. Must be less than or equal to 255 characters length", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/payments/{paymentId}" : { + "get" : { + "tags" : [ "Card payments" ], + "summary" : "Find payment by ID", + "description" : "Return information about the payment The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Get a payment", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "description" : "Payment identifier", + "required" : true, + "type" : "string", + "x-example" : "hu20sqlact5260q2nanm0q8u93" + } ], + "responses" : { + "200" : { + "description" : "OK", + "schema" : { + "$ref" : "#/definitions/GetPaymentResult" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/payments/{paymentId}/cancel" : { + "post" : { + "tags" : [ "Card payments" ], + "summary" : "Cancel payment", + "description" : "Cancel a payment based on the provided payment ID and the Authorisation token. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'. A payment can only be cancelled if it's in a state that isn't finished.", + "operationId" : "Cancel a payment", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "description" : "Payment identifier", + "required" : true, + "type" : "string", + "x-example" : "hu20sqlact5260q2nanm0q8u93" + } ], + "responses" : { + "204" : { + "description" : "No Content" + }, + "400" : { + "description" : "Cancellation of payment failed", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "409" : { + "description" : "Conflict", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/payments/{paymentId}/capture" : { + "post" : { + "tags" : [ "Card payments" ], + "summary" : "Capture payment", + "description" : "Capture a payment based on the provided payment ID and the Authorisation token. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'. A payment can only be captured if it's in 'submitted' state", + "operationId" : "Capture a payment", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "description" : "Payment identifier", + "required" : true, + "type" : "string", + "x-example" : "hu20sqlact5260q2nanm0q8u93" + } ], + "responses" : { + "204" : { + "description" : "No Content" + }, + "400" : { + "description" : "Capture of payment failed", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "409" : { + "description" : "Conflict", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/payments/{paymentId}/events" : { + "get" : { + "tags" : [ "Card payments" ], + "summary" : "Return payment events by ID", + "description" : "Return payment events information about a certain payment The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Get events for a payment", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "description" : "Payment identifier", + "required" : true, + "type" : "string", + "x-example" : "hu20sqlact5260q2nanm0q8u93" + } ], + "responses" : { + "200" : { + "description" : "OK", + "schema" : { + "$ref" : "#/definitions/PaymentEvents" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/payments/{paymentId}/refunds" : { + "get" : { + "tags" : [ "Refunding card payments" ], + "summary" : "Get all refunds for a payment", + "description" : "Return refunds for a payment. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Get all refunds for a payment", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "OK", + "schema" : { + "$ref" : "#/definitions/RefundForSearchResult" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + }, + "post" : { + "tags" : [ "Refunding card payments" ], + "summary" : "Submit a refund for a payment", + "description" : "Return issued refund information. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Submit a refund for a payment", + "consumes" : [ "application/json" ], + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "description" : "paymentId", + "required" : true, + "type" : "string" + }, { + "in" : "body", + "name" : "body", + "description" : "requestPayload", + "required" : true, + "schema" : { + "$ref" : "#/definitions/PaymentRefundRequest" + } + } ], + "responses" : { + "200" : { + "description" : "successful operation", + "schema" : { + "$ref" : "#/definitions/Refund" + } + }, + "202" : { + "description" : "ACCEPTED" + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "412" : { + "description" : "Refund amount available mismatch" + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/payments/{paymentId}/refunds/{refundId}" : { + "get" : { + "tags" : [ "Refunding card payments" ], + "summary" : "Find payment refund by ID", + "description" : "Return payment refund information by Refund ID The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Get a payment refund", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "required" : true, + "type" : "string" + }, { + "name" : "refundId", + "in" : "path", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "OK", + "schema" : { + "$ref" : "#/definitions/Refund" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/refunds" : { + "get" : { + "tags" : [ "Refunding card payments" ], + "summary" : "Search refunds", + "description" : "Search refunds by 'from' and 'to' date. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Search refunds", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "from_date", + "in" : "query", + "description" : "From date of refunds to be searched (this date is inclusive). Example=2015-08-13T12:35:00Z", + "required" : false, + "type" : "string" + }, { + "name" : "to_date", + "in" : "query", + "description" : "To date of refunds to be searched (this date is exclusive). Example=2015-08-14T12:35:00Z", + "required" : false, + "type" : "string" + }, { + "name" : "from_settled_date", + "in" : "query", + "description" : "From settled date of refund to be searched (this date is inclusive). Example=2015-08-13", + "required" : false, + "type" : "string" + }, { + "name" : "to_settled_date", + "in" : "query", + "description" : "To settled date of refund to be searched (this date is inclusive). Example=2015-08-13", + "required" : false, + "type" : "string" + }, { + "name" : "page", + "in" : "query", + "description" : "Page number requested for the search, should be a positive integer (optional, defaults to 1)", + "required" : false, + "type" : "string" + }, { + "name" : "display_size", + "in" : "query", + "description" : "Number of results to be shown per page, should be a positive integer (optional, defaults to 500, max 500)", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "OK", + "schema" : { + "$ref" : "#/definitions/RefundSearchResults" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "422" : { + "description" : "Invalid parameters. See Public API documentation for the correct data formats", + "schema" : { + "$ref" : "#/definitions/RefundError" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/RefundError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + } + }, + "securityDefinitions" : { + "Authorization" : { + "type" : "apiKey", + "name" : "Authorization", + "in" : "header" + } + }, + "definitions" : { + "Address" : { + "type" : "object", + "properties" : { + "line1" : { + "type" : "string", + "example" : "address line 1", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 255 + }, + "line2" : { + "type" : "string", + "example" : "address line 2", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 255 + }, + "postcode" : { + "type" : "string", + "example" : "AB1 2CD", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 25 + }, + "city" : { + "type" : "string", + "example" : "address city", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 255 + }, + "country" : { + "type" : "string", + "example" : "GB", + "readOnly" : true + } + }, + "description" : "A structure representing the billing address of a card" + }, + "CardDetails" : { + "type" : "object", + "properties" : { + "last_digits_card_number" : { + "type" : "string", + "example" : "1234", + "readOnly" : true + }, + "first_digits_card_number" : { + "type" : "string", + "example" : "123456", + "readOnly" : true + }, + "cardholder_name" : { + "type" : "string", + "example" : "Mr. Card holder", + "readOnly" : true + }, + "expiry_date" : { + "type" : "string", + "example" : "04/24", + "description" : "The expiry date of the card in MM/yy format", + "readOnly" : true + }, + "billing_address" : { + "readOnly" : true, + "$ref" : "#/definitions/Address" + }, + "card_brand" : { + "type" : "string", + "example" : "Visa", + "readOnly" : true + }, + "card_type" : { + "type" : "string", + "example" : "debit", + "description" : "The card type, `debit` or `credit` or `null` if not able to determine", + "readOnly" : true, + "enum" : [ "debit", "credit", "null" ] + } + }, + "description" : "A structure representing the payment card" + }, + "CreateCardPaymentRequest" : { + "type" : "object", + "required" : [ "amount", "description", "reference", "return_url" ], + "properties" : { + "amount" : { + "type" : "integer", + "format" : "int32", + "example" : 12000, + "description" : "amount in pence", + "readOnly" : true, + "minimum" : 0, + "maximum" : 10000000 + }, + "reference" : { + "type" : "string", + "example" : "12345", + "description" : "payment reference", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 255 + }, + "description" : { + "type" : "string", + "example" : "New passport application", + "description" : "payment description", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 255 + }, + "language" : { + "type" : "string", + "example" : "en", + "description" : "ISO-639-1 Alpha-2 code of a supported language to use on the payment pages", + "readOnly" : true, + "enum" : [ "en", "cy" ] + }, + "email" : { + "type" : "string", + "example" : "Joe.Bogs@example.org", + "description" : "email", + "readOnly" : true + }, + "return_url" : { + "type" : "string", + "example" : "https://service-name.gov.uk/transactions/12345", + "description" : "service return url", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 2000 + }, + "delayed_capture" : { + "type" : "boolean", + "example" : false, + "description" : "delayed capture flag", + "readOnly" : true + }, + "moto" : { + "type" : "boolean", + "example" : false, + "description" : "Mail Order / Telephone Order (MOTO) payment flag", + "readOnly" : true + }, + "metadata" : { + "type" : "object", + "example" : "{\"ledger_code\":\"123\", \"reconciled\": true}", + "description" : "Additional metadata - up to 10 name/value pairs - on the payment. Each key must be between 1 and 30 characters long. The value, if a string, must be no greater than 50 characters long. Other permissible value types: boolean, number.", + "readOnly" : true, + "additionalProperties" : { + "type" : "object" + } + }, + "prefilled_cardholder_details" : { + "description" : "prefilled_cardholder_details", + "readOnly" : true, + "$ref" : "#/definitions/PrefilledCardholderDetails" + } + }, + "description" : "The Payment Request Payload" + }, + "CreatePaymentResult" : { + "type" : "object", + "properties" : { + "amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1200, + "description" : "The amount in pence." + }, + "state" : { + "$ref" : "#/definitions/PaymentState" + }, + "description" : { + "type" : "string", + "example" : "New passport application", + "description" : "The human-readable description you gave the payment." + }, + "reference" : { + "type" : "string", + "example" : "12345", + "description" : "The reference number you associated with this payment." + }, + "language" : { + "type" : "string", + "example" : "en", + "description" : "Which language your users will see on the payment pages when they make a payment.", + "enum" : [ "en", "cy" ] + }, + "payment_id" : { + "type" : "string", + "example" : "hu20sqlact5260q2nanm0q8u93", + "description" : "The unique identifier of the payment." + }, + "payment_provider" : { + "type" : "string", + "example" : "worldpay" + }, + "return_url" : { + "type" : "string", + "example" : "https://service-name.gov.uk/transactions/12345", + "description" : "An HTTPS URL on your site that your user will be sent back to once they have completed their payment attempt on GOV.UK Pay." + }, + "created_date" : { + "type" : "string", + "example" : "2016-01-21T17:15:00Z", + "description" : "The date you created the payment." + }, + "delayed_capture" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to [delay capturing](https://docs.payments.service.gov.uk/optional_features/delayed_capture/) this payment." + }, + "moto" : { + "type" : "boolean", + "example" : false, + "description" : "Mail Order / Telephone Order (MOTO) payment flag." + }, + "_links" : { + "description" : "API endpoints related to the payment.", + "$ref" : "#/definitions/PaymentLinks" + }, + "provider_id" : { + "type" : "string", + "example" : "null", + "description" : "The reference number the payment gateway associated with the payment." + }, + "metadata" : { + "type" : "object", + "description" : "[Custom metadata](https://docs.payments.service.gov.uk/optional_features/custom_metadata/) you added to the payment.", + "additionalProperties" : { + "type" : "string" + } + }, + "email" : { + "type" : "string", + "example" : "citizen@example.org", + "description" : "The email address of your user." + }, + "refund_summary" : { + "$ref" : "#/definitions/RefundSummary" + }, + "settlement_summary" : { + "$ref" : "#/definitions/PaymentSettlementSummary" + }, + "card_details" : { + "$ref" : "#/definitions/CardDetails" + } + } + }, + "EmbeddedRefunds" : { + "type" : "object", + "properties" : { + "refunds" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/Refund" + } + } + } + }, + "ErrorResponse" : { + "type" : "object", + "properties" : { + "code" : { + "type" : "string", + "example" : "P0900" + }, + "description" : { + "type" : "string", + "example" : "Too many requests" + } + }, + "description" : "An error response" + }, + "GetPaymentResult" : { + "type" : "object", + "properties" : { + "amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1200 + }, + "description" : { + "type" : "string", + "example" : "Your Service Description" + }, + "reference" : { + "type" : "string", + "example" : "your-reference" + }, + "language" : { + "type" : "string", + "example" : "en", + "enum" : [ "en", "cy" ] + }, + "metadata" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "email" : { + "type" : "string", + "example" : "your email" + }, + "state" : { + "$ref" : "#/definitions/PaymentState" + }, + "payment_id" : { + "type" : "string", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "payment_provider" : { + "type" : "string", + "example" : "worldpay", + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "example" : "2016-01-21T17:15:000Z", + "readOnly" : true + }, + "refund_summary" : { + "readOnly" : true, + "$ref" : "#/definitions/RefundSummary" + }, + "settlement_summary" : { + "readOnly" : true, + "$ref" : "#/definitions/PaymentSettlementSummary" + }, + "card_details" : { + "readOnly" : true, + "$ref" : "#/definitions/CardDetails" + }, + "delayed_capture" : { + "type" : "boolean", + "example" : false, + "description" : "delayed capture flag", + "readOnly" : true + }, + "moto" : { + "type" : "boolean", + "example" : false, + "description" : "Mail Order / Telephone Order (MOTO) payment flag", + "readOnly" : true + }, + "corporate_card_surcharge" : { + "type" : "integer", + "format" : "int64", + "example" : 250, + "readOnly" : true + }, + "total_amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1450, + "readOnly" : true + }, + "fee" : { + "type" : "integer", + "format" : "int64", + "example" : 5, + "description" : "processing fee taken by the GOV.UK Pay platform, in pence. Only available depending on payment service provider", + "readOnly" : true + }, + "net_amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1195, + "description" : "amount including all surcharges and less all fees, in pence. Only available depending on payment service provider", + "readOnly" : true + }, + "provider_id" : { + "type" : "string", + "example" : "reference-from-payment-gateway", + "readOnly" : true + }, + "return_url" : { + "type" : "string", + "example" : "http://your.service.domain/your-reference", + "readOnly" : true + }, + "_links" : { + "$ref" : "#/definitions/PaymentLinks" + }, + "card_brand" : { + "type" : "string", + "example" : "Visa", + "description" : "Card Brand", + "readOnly" : true + } + } + }, + "Link" : { + "type" : "object", + "properties" : { + "href" : { + "type" : "string", + "example" : "https://an.example.link/from/payment/platform", + "readOnly" : true + }, + "method" : { + "type" : "string", + "example" : "GET", + "readOnly" : true + } + }, + "description" : "A link related to a payment" + }, + "Payer" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "readOnly" : true + }, + "email" : { + "type" : "string", + "readOnly" : true + } + } + }, + "PaymentDetailForSearch" : { + "type" : "object", + "properties" : { + "amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1200 + }, + "description" : { + "type" : "string", + "example" : "Your Service Description" + }, + "reference" : { + "type" : "string", + "example" : "your-reference" + }, + "language" : { + "type" : "string", + "example" : "en", + "enum" : [ "en", "cy" ] + }, + "metadata" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "email" : { + "type" : "string", + "example" : "your email" + }, + "state" : { + "$ref" : "#/definitions/PaymentState" + }, + "payment_id" : { + "type" : "string", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "payment_provider" : { + "type" : "string", + "example" : "worldpay", + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "example" : "2016-01-21T17:15:000Z", + "readOnly" : true + }, + "refund_summary" : { + "readOnly" : true, + "$ref" : "#/definitions/RefundSummary" + }, + "settlement_summary" : { + "readOnly" : true, + "$ref" : "#/definitions/PaymentSettlementSummary" + }, + "card_details" : { + "readOnly" : true, + "$ref" : "#/definitions/CardDetails" + }, + "delayed_capture" : { + "type" : "boolean", + "example" : false, + "description" : "delayed capture flag", + "readOnly" : true + }, + "moto" : { + "type" : "boolean", + "example" : false, + "description" : "Mail Order / Telephone Order (MOTO) payment flag", + "readOnly" : true + }, + "corporate_card_surcharge" : { + "type" : "integer", + "format" : "int64", + "example" : 250, + "readOnly" : true + }, + "total_amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1450, + "readOnly" : true + }, + "fee" : { + "type" : "integer", + "format" : "int64", + "example" : 5, + "description" : "processing fee taken by the GOV.UK Pay platform, in pence. Only available depending on payment service provider", + "readOnly" : true + }, + "net_amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1195, + "description" : "amount including all surcharges and less all fees, in pence. Only available depending on payment service provider", + "readOnly" : true + }, + "provider_id" : { + "type" : "string", + "example" : "reference-from-payment-gateway", + "readOnly" : true + }, + "return_url" : { + "type" : "string", + "example" : "http://your.service.domain/your-reference", + "readOnly" : true + }, + "_links" : { + "readOnly" : true, + "$ref" : "#/definitions/PaymentLinksForSearch" + }, + "card_brand" : { + "type" : "string", + "example" : "Visa", + "description" : "Card Brand", + "readOnly" : true + } + } + }, + "PaymentError" : { + "type" : "object", + "properties" : { + "field" : { + "type" : "string", + "example" : "amount" + }, + "code" : { + "type" : "string", + "example" : "P0102" + }, + "description" : { + "type" : "string", + "example" : "Invalid attribute value: amount. Must be less than or equal to 10000000" + } + }, + "description" : "A Payment Error response" + }, + "PaymentEvent" : { + "type" : "object", + "properties" : { + "payment_id" : { + "type" : "string", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "state" : { + "description" : "state", + "readOnly" : true, + "$ref" : "#/definitions/PaymentState" + }, + "updated" : { + "type" : "string", + "example" : "2017-01-10T16:44:48.646Z", + "description" : "updated", + "readOnly" : true + }, + "_links" : { + "readOnly" : true, + "$ref" : "#/definitions/PaymentEventLink" + } + }, + "description" : "A List of Payment Events information" + }, + "PaymentEventLink" : { + "type" : "object", + "properties" : { + "payment_url" : { + "description" : "payment_url", + "readOnly" : true, + "$ref" : "#/definitions/Link" + } + }, + "description" : "Resource link for a payment of a payment event" + }, + "PaymentEvents" : { + "type" : "object", + "properties" : { + "events" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/PaymentEvent" + } + }, + "payment_id" : { + "type" : "string", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "_links" : { + "readOnly" : true, + "$ref" : "#/definitions/PaymentLinksForEvents" + } + }, + "description" : "A List of Payment Events information" + }, + "PaymentLinks" : { + "type" : "object", + "properties" : { + "self" : { + "description" : "self", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "next_url" : { + "description" : "next_url", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "next_url_post" : { + "description" : "next_url_post", + "readOnly" : true, + "$ref" : "#/definitions/PostLink" + }, + "events" : { + "description" : "events", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "refunds" : { + "description" : "refunds", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "cancel" : { + "description" : "cancel", + "readOnly" : true, + "$ref" : "#/definitions/PostLink" + }, + "capture" : { + "description" : "capture", + "readOnly" : true, + "$ref" : "#/definitions/PostLink" + } + }, + "description" : "links for payment" + }, + "PaymentLinksForEvents" : { + "type" : "object", + "properties" : { + "self" : { + "description" : "self", + "readOnly" : true, + "$ref" : "#/definitions/Link" + } + }, + "description" : "links for events resource" + }, + "PaymentLinksForSearch" : { + "type" : "object", + "properties" : { + "self" : { + "description" : "self", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "cancel" : { + "description" : "cancel", + "readOnly" : true, + "$ref" : "#/definitions/PostLink" + }, + "events" : { + "description" : "events", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "refunds" : { + "description" : "refunds", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "capture" : { + "description" : "capture", + "readOnly" : true, + "$ref" : "#/definitions/PostLink" + } + }, + "description" : "links for search payment resource" + }, + "PaymentRefundRequest" : { + "type" : "object", + "required" : [ "amount" ], + "properties" : { + "amount" : { + "type" : "integer", + "format" : "int32", + "example" : 150000, + "description" : "Amount in pence. Can't be more than the available amount for refunds", + "minimum" : 1, + "maximum" : 10000000 + }, + "refund_amount_available" : { + "type" : "integer", + "format" : "int32", + "example" : 200000, + "description" : "Amount in pence. Total amount still available before issuing the refund", + "readOnly" : true, + "minimum" : 1, + "maximum" : 10000000 + } + }, + "description" : "The Payment Refund Request Payload" + }, + "PaymentSearchResults" : { + "type" : "object", + "properties" : { + "total" : { + "type" : "integer", + "format" : "int32", + "example" : 100 + }, + "count" : { + "type" : "integer", + "format" : "int32", + "example" : 20 + }, + "page" : { + "type" : "integer", + "format" : "int32", + "example" : 1 + }, + "_links" : { + "$ref" : "#/definitions/SearchNavigationLinks" + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/PaymentDetailForSearch" + } + } + } + }, + "PaymentSettlementSummary" : { + "type" : "object", + "properties" : { + "capture_submit_time" : { + "type" : "string", + "example" : "2016-01-21T17:15:000Z", + "description" : "Date and time capture request has been submitted. May be null if capture request was not immediately acknowledged by payment gateway.", + "readOnly" : true + }, + "captured_date" : { + "type" : "string", + "example" : "2016-01-21", + "description" : "Date of the capture event.", + "readOnly" : true + }, + "settled_date" : { + "type" : "string", + "example" : "2016-01-21", + "description" : "The date that the transaction was paid into the service's account.", + "readOnly" : true + } + }, + "description" : "A structure representing information about a settlement" + }, + "PaymentState" : { + "type" : "object", + "properties" : { + "status" : { + "type" : "string", + "example" : "created", + "description" : "Current progress of the payment in its lifecycle", + "readOnly" : true + }, + "finished" : { + "type" : "boolean", + "description" : "Whether the payment has finished", + "readOnly" : true + }, + "message" : { + "type" : "string", + "example" : "User cancelled the payment", + "description" : "What went wrong with the Payment if it finished with an error - English message", + "readOnly" : true + }, + "code" : { + "type" : "string", + "example" : "P010", + "description" : "What went wrong with the Payment if it finished with an error - error code", + "readOnly" : true + } + }, + "description" : "A structure representing the current state of the payment in its lifecycle." + }, + "PostLink" : { + "type" : "object", + "properties" : { + "type" : { + "type" : "string", + "example" : "application/x-www-form-urlencoded" + }, + "params" : { + "type" : "object", + "example" : "\"description\":\"This is a value for a parameter called description\"", + "additionalProperties" : { + "type" : "object" + } + }, + "href" : { + "type" : "string", + "example" : "https://an.example.link/from/payment/platform", + "readOnly" : true + }, + "method" : { + "type" : "string", + "example" : "POST", + "readOnly" : true + } + }, + "description" : "A POST link related to a payment" + }, + "PrefilledCardholderDetails" : { + "type" : "object", + "properties" : { + "cardholder_name" : { + "type" : "string", + "example" : "J. Bogs", + "description" : "prefilled cardholder name", + "minLength" : 0, + "maxLength" : 255 + }, + "billing_address" : { + "description" : "prefilled billing address", + "readOnly" : true, + "$ref" : "#/definitions/Address" + } + } + }, + "Refund" : { + "type" : "object", + "properties" : { + "refund_id" : { + "type" : "string", + "example" : "act4c33g40j3edfmi8jknab84x", + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "example" : "2017-01-10T16:52:07.855Z", + "readOnly" : true + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "example" : 120, + "readOnly" : true + }, + "_links" : { + "$ref" : "#/definitions/RefundLinksForSearch" + }, + "status" : { + "type" : "string", + "example" : "success", + "readOnly" : true, + "enum" : [ "submitted", "success", "error" ] + }, + "settlement_summary" : { + "readOnly" : true, + "$ref" : "#/definitions/RefundSettlementSummary" + } + } + }, + "RefundDetailForSearch" : { + "type" : "object", + "properties" : { + "refund_id" : { + "type" : "string", + "example" : "act4c33g40j3edfmi8jknab84x", + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "example" : "2017-01-10T16:52:07.855Z", + "readOnly" : true + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "example" : 120, + "readOnly" : true + }, + "_links" : { + "readOnly" : true, + "$ref" : "#/definitions/RefundLinksForSearch" + }, + "status" : { + "type" : "string", + "example" : "success", + "readOnly" : true, + "enum" : [ "submitted", "success", "error" ] + }, + "settlement_summary" : { + "readOnly" : true, + "$ref" : "#/definitions/RefundSettlementSummary" + } + } + }, + "RefundError" : { + "type" : "object", + "properties" : { + "field" : { + "type" : "string", + "example" : "amount_submitted" + }, + "code" : { + "type" : "string", + "example" : "P0102" + }, + "description" : { + "type" : "string", + "example" : "Invalid attribute value: amountSubmitted. Must be less than or equal to 10000000" + } + }, + "description" : "A Refund Error response" + }, + "RefundForSearchResult" : { + "type" : "object", + "properties" : { + "payment_id" : { + "type" : "string", + "example" : "hu20sqlact5260q2nanm0q8u93" + }, + "_links" : { + "$ref" : "#/definitions/RefundLinksForSearch" + }, + "_embedded" : { + "readOnly" : true, + "$ref" : "#/definitions/EmbeddedRefunds" + } + } + }, + "RefundLinksForSearch" : { + "type" : "object", + "properties" : { + "self" : { + "description" : "self", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "payment" : { + "description" : "payment", + "readOnly" : true, + "$ref" : "#/definitions/Link" + } + }, + "description" : "links for search refunds resource" + }, + "RefundSearchResults" : { + "type" : "object", + "properties" : { + "total" : { + "type" : "integer", + "format" : "int32", + "example" : 100 + }, + "count" : { + "type" : "integer", + "format" : "int32", + "example" : 20 + }, + "page" : { + "type" : "integer", + "format" : "int32", + "example" : 1 + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/RefundDetailForSearch" + } + }, + "_links" : { + "readOnly" : true, + "$ref" : "#/definitions/SearchNavigationLinks" + } + } + }, + "RefundSettlementSummary" : { + "type" : "object", + "properties" : { + "settled_date" : { + "type" : "string", + "example" : "2016-01-21", + "description" : "The date that the transaction was refunded from the service's account.", + "readOnly" : true + } + }, + "description" : "A structure representing information about a settlement for refunds" + }, + "RefundSummary" : { + "type" : "object", + "properties" : { + "status" : { + "type" : "string", + "example" : "available", + "description" : "Availability status of the refund" + }, + "amount_available" : { + "type" : "integer", + "format" : "int64", + "example" : 100, + "description" : "Amount available for refund in pence", + "readOnly" : true + }, + "amount_submitted" : { + "type" : "integer", + "format" : "int64", + "description" : "Amount submitted for refunds on this Payment in pence", + "readOnly" : true + } + }, + "description" : "A structure representing the refunds availability" + }, + "SearchNavigationLinks" : { + "type" : "object", + "properties" : { + "self" : { + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "first_page" : { + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "last_page" : { + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "prev_page" : { + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "next_page" : { + "readOnly" : true, + "$ref" : "#/definitions/Link" + } + }, + "description" : "Links to navigate through pages" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/resources/config/config.yaml b/jdk_11_maven/cs/rest/pay-publicapi/src/main/resources/config/config.yaml new file mode 100644 index 000000000..36e1906a4 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/resources/config/config.yaml @@ -0,0 +1,66 @@ +server: + applicationConnectors: + - type: http + port: ${PORT:-8080} + adminConnectors: + - type: http + port: ${ADMIN_PORT:-8081} + requestLog: + appenders: + - type: console + layout: + type: govuk-pay-access-json + additionalFields: + container: "publicapi" + environment: ${ENVIRONMENT} + +logging: + level: INFO + appenders: + - type: logstash-console + threshold: ALL + target: stdout + customFields: + container: "publicapi" + environment: ${ENVIRONMENT} + - type: sentry + threshold: ERROR + dsn: ${SENTRY_DSN:-https://example.com@dummy/1} + environment: ${ENVIRONMENT} + +baseUrl: ${PUBLICAPI_BASE} +connectorUrl: ${CONNECTOR_URL} +publicAuthUrl: ${PUBLIC_AUTH_URL} +ledgerUrl: ${LEDGER_URL} + +jerseyClientConfig: + disabledSecureConnection: ${DISABLE_INTERNAL_HTTPS:-false} + +rateLimiter: # rate = noOfReq per perMillis + noOfReq: ${RATE_LIMITER_VALUE:-75} # for requests except POST and across all publicapi instances. + noOfReqForPost: ${RATE_LIMITER_VALUE_POST:-15} # for POST requests across all publicapi instances. + elevatedAccounts: ${RATE_LIMITER_ELEVATED_ACCOUNTS} + noOfReqForElevatedAccounts: ${RATE_LIMITER_ELEVATED_VALUE_GET:-100} + noOfPostReqForElevatedAccounts: ${RATE_LIMITER_ELEVATED_VALUE_POST:-40} + noOfReqPerNode: ${RATE_LIMITER_VALUE_PER_NODE:-25} # per public api instance, if Redis is unavailable + noOfReqForPostPerNode: ${RATE_LIMITER_VALUE_PER_NODE_POST:-5} # per public api instance, if Redis is unavailable + perMillis: ${RATE_LIMITER_PER_MILLIS:-1000} + lowTrafficAccounts: ${RATE_LIMITER_LOW_TRAFFIC_ACCOUNTS} + noOfReqForLowTrafficAccounts: ${RATE_LIMITER_LOW_TRAFFIC_VALUE_GET:-4500} + noOfPostReqForLowTrafficAccounts: ${RATE_LIMITER_LOW_TRAFFIC_VALUE_POST:-1} + intervalInMillisForLowTrafficAccounts: ${RATE_LIMITER_LOW_TRAFFIC_PER_MILLIS:-60000} + +redis: + endpoint: ${REDIS_URL:-localhost:6379} + ssl: false + commandTimeout: ${REDIS_COMMAND_TIMEOUT:-250ms} + connectTimeout: ${REDIS_CONNECT_TIMEOUT:-500ms} + +allowHttpForReturnUrl: ${ALLOW_HTTP_FOR_RETURN_URL:-false} + +apiKeyHmacSecret: ${TOKEN_API_HMAC_SECRET} + +# Caching authenticator. +authenticationCachePolicy: expireAfterWrite=1m + +ecsContainerMetadataUriV4: ${ECS_CONTAINER_METADATA_URI_V4:-} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/main/resources/swagger-config.yaml b/jdk_11_maven/cs/rest/pay-publicapi/src/main/resources/swagger-config.yaml new file mode 100644 index 000000000..2ca111b09 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/main/resources/swagger-config.yaml @@ -0,0 +1,27 @@ +resourceClasses: + - uk.gov.pay.api.agreement.resource.AgreementsApiResource + - uk.gov.pay.api.resources.AuthorisationResource + - uk.gov.pay.api.resources.PaymentsResource + - uk.gov.pay.api.resources.PaymentRefundsResource + - uk.gov.pay.api.resources.SearchRefundsResource + - uk.gov.pay.api.resources.SearchDisputesResource +readAllResources: false +sortOutput: true +openAPI: + info: + description: 'The GOV.UK Pay REST API. Read [our documentation](https://docs.payments.service.gov.uk/) for more details.' + version: '1.0.3' + title: 'GOV.UK Pay API' + servers: + - url: 'https://publicapi.payments.service.gov.uk' + tags: + - name: Agreements + - name: Card payments + - name: Refunding card payments + + components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + description: "GOV.UK Pay authenticates API calls with [OAuth2 HTTP bearer tokens](http://tools.ietf.org/html/rfc6750). You need to use an `\"Authorization\"` HTTP header to provide your API key, with a `\"Bearer\"` prefix. For example: `Authorization: Bearer {YOUR_API_KEY_HERE}`" diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/app/RestClientFactoryTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/app/RestClientFactoryTest.java new file mode 100644 index 000000000..08a1f2b09 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/app/RestClientFactoryTest.java @@ -0,0 +1,44 @@ +package uk.gov.pay.api.app; + +import org.junit.jupiter.api.Test; +import uk.gov.pay.api.app.config.RestClientConfig; + +import javax.net.ssl.SSLContext; +import javax.ws.rs.client.Client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.core.IsNot.not; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RestClientFactoryTest { + + @Test + public void jerseyClient_shouldUseSSLWhenSecureInternalCommunicationIsOn() { + //given + RestClientConfig clientConfiguration = mock(RestClientConfig.class); + when(clientConfiguration.isDisabledSecureConnection()).thenReturn(false); + + //when + Client client = RestClientFactory.buildClient(clientConfiguration); + + //then + SSLContext sslContext = client.getSslContext(); + assertThat(sslContext.getProtocol(), is("TLSv1.2")); + + } + + @Test + public void jerseyClient_shouldNotUseSSLWhenSecureInternalCommunicationIsOff() { + //given + RestClientConfig clientConfiguration = mock(RestClientConfig.class); + when(clientConfiguration.isDisabledSecureConnection()).thenReturn(true); + + //when + Client client = RestClientFactory.buildClient(clientConfiguration); + + //then + assertThat(client.getSslContext().getProtocol(), is(not("TLSv1.2"))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/app/config/StringToListConverterTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/app/config/StringToListConverterTest.java new file mode 100644 index 000000000..468249943 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/app/config/StringToListConverterTest.java @@ -0,0 +1,38 @@ +package uk.gov.pay.api.app.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class StringToListConverterTest { + + private StringToListConverter converter; + + @BeforeEach + public void setUp() { + converter = new StringToListConverter(); + } + + @ParameterizedTest + @MethodSource("parametersForConvertsStringInputToListOfStrings") + public void convertsStringInputToListOfStrings(String input, List expectedOutput) { + assertThat(converter.convert(input), is(expectedOutput)); + } + + static Object[] parametersForConvertsStringInputToListOfStrings() { + return new Object[]{ + new Object[]{null, Collections.emptyList()}, + new Object[]{"", Collections.emptyList()}, + new Object[]{", , ,", Collections.emptyList()}, + new Object[]{"a", List.of("a")}, + new Object[]{"a, b, b", List.of("a", "b", "b")}, + new Object[]{"a, , b", List.of("a", "b")} + }; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/auth/AccountAuthenticatorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/auth/AccountAuthenticatorTest.java new file mode 100644 index 000000000..afad8a294 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/auth/AccountAuthenticatorTest.java @@ -0,0 +1,147 @@ +package uk.gov.pay.api.auth; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.model.publicauth.AuthResponse; + +import javax.ws.rs.ServiceUnavailableException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static javax.ws.rs.core.Response.Status.OK; +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static uk.gov.pay.api.model.TokenPaymentType.CARD; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AUTH_TOKEN_INVALID; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AUTH_TOKEN_REVOKED; + +@ExtendWith(MockitoExtension.class) +public class AccountAuthenticatorTest { + + private AccountAuthenticator accountAuthenticator; + private ObjectMapper objectMapper = new ObjectMapper(); + + private final String bearerToken = "aaa"; + private final String accountId = "accountId"; + + @Mock + private Client publicAuthMock; + + @Mock + private WebTarget mockTarget; + + @Mock + private Invocation.Builder mockRequest; + + @Mock + private Response mockResponse; + + @Mock + private PublicApiConfig mockConfiguration; + + @Mock + private Appender mockAppender; + + @Captor + ArgumentCaptor loggingEventArgumentCaptor; + + @BeforeEach + public void setup() { + Logger logger = (Logger) LoggerFactory.getLogger(AccountAuthenticator.class); + logger.setLevel(Level.INFO); + logger.addAppender(mockAppender); + + when(mockConfiguration.getPublicAuthUrl()).thenReturn(""); + accountAuthenticator = new AccountAuthenticator(publicAuthMock, mockConfiguration); + when(publicAuthMock.target("")).thenReturn(mockTarget); + when(mockTarget.request()).thenReturn(mockRequest); + when(mockRequest.header(AUTHORIZATION, "Bearer " + bearerToken)).thenReturn(mockRequest); + when(mockRequest.accept(MediaType.APPLICATION_JSON)).thenReturn(mockRequest); + when(mockRequest.get()).thenReturn(mockResponse); + } + + @Test + public void shouldReturnValidAccount() { + AuthResponse authResponse = new AuthResponse(accountId, "a-token-link", CARD); + when(mockResponse.getStatus()).thenReturn(OK.getStatusCode()); + when(mockResponse.readEntity(AuthResponse.class)).thenReturn(authResponse); + Optional maybeAccount = accountAuthenticator.authenticate(bearerToken); + assertThat(maybeAccount.get().getName(), is(accountId)); + assertThat(maybeAccount.get().getAccountId(), is(accountId)); + assertThat(maybeAccount.get().getPaymentType(), is(CARD)); + + verify(mockAppender).doAppend(loggingEventArgumentCaptor.capture()); + List logEvents = loggingEventArgumentCaptor.getAllValues(); + assertThat(logEvents, hasSize(1)); + assertThat(logEvents.get(0).getFormattedMessage(), is("Successfully authenticated using API key with token_link a-token-link")); + } + + @Test + public void shouldNotReturnAccount_ifUnauthorisedDueToTokenRevoked() { + Map responseEntity = ImmutableMap.of( + "error_identifier", AUTH_TOKEN_REVOKED.toString(), + "token_link", "a-token-link" + ); + JsonNode response = objectMapper.valueToTree(responseEntity); + when(mockResponse.getStatus()).thenReturn(UNAUTHORIZED.getStatusCode()); + when(mockResponse.readEntity(JsonNode.class)).thenReturn(response); + Optional maybeAccount = accountAuthenticator.authenticate(bearerToken); + assertThat(maybeAccount.isPresent(), is(false)); + + verify(mockAppender).doAppend(loggingEventArgumentCaptor.capture()); + List logEvents = loggingEventArgumentCaptor.getAllValues(); + assertThat(logEvents, hasSize(1)); + assertThat(logEvents.get(0).getFormattedMessage(), is("Attempt to authenticate using revoked API key with token_link a-token-link")); + } + + @Test + public void shouldNotReturnAccount_ifUnauthorisedDueToInvalidToken() { + Map responseEntity = ImmutableMap.of( + "error_identifier", AUTH_TOKEN_INVALID.toString() + ); + JsonNode response = objectMapper.valueToTree(responseEntity); + when(mockResponse.getStatus()).thenReturn(UNAUTHORIZED.getStatusCode()); + when(mockResponse.readEntity(JsonNode.class)).thenReturn(response); + Optional maybeAccount = accountAuthenticator.authenticate(bearerToken); + assertThat(maybeAccount.isPresent(), is(false)); + + verify(mockAppender).doAppend(loggingEventArgumentCaptor.capture()); + List logEvents = loggingEventArgumentCaptor.getAllValues(); + assertThat(logEvents, hasSize(1)); + assertThat(logEvents.get(0).getFormattedMessage(), is("Attempt to authenticate using invalid API key with valid checksum")); + } + + @Test + public void shouldThrow_ifUnknownResponse() { + when(mockResponse.getStatus()).thenReturn(NOT_FOUND.getStatusCode()); + assertThrows(ServiceUnavailableException.class, () -> accountAuthenticator.authenticate(bearerToken)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/ConnectorResponseErrorExceptionTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/ConnectorResponseErrorExceptionTest.java new file mode 100644 index 000000000..ffcd65358 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/ConnectorResponseErrorExceptionTest.java @@ -0,0 +1,39 @@ +package uk.gov.pay.api.exception; + +import org.junit.jupiter.api.Test; + +import javax.ws.rs.core.Response; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.nullValue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static uk.gov.pay.api.exception.ConnectorResponseErrorException.ConnectorErrorResponse; + +public class ConnectorResponseErrorExceptionTest { + + @Test + public void whenCreated_shouldCallCloseConnection_soWeMakeSureThatTheConnectionIsClosedWhenConnectorRespondsWithAnUnexpectedResponse() { + // It doesn't matter if the connection was closed before, + // but anyway we make sure it is when an exception of this nature is raised + + Response mockResponse = mock(Response.class); + when(mockResponse.getStatus()) + .thenReturn(400); + + ConnectorResponseErrorException exception = new ConnectorResponseErrorException(mockResponse); + + assertThat(exception.getErrorStatus(), is(400)); + assertThat(exception.hasReason(), is(false)); + assertThat(exception.getMessage(), is(mockResponse.toString())); + assertThat(exception.getReason(), is(nullValue())); + + verify(mockResponse).getStatus(); + verify(mockResponse).readEntity(ConnectorErrorResponse.class); + verify(mockResponse).close(); + verifyNoMoreInteractions(mockResponse); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/AuthorisationRequestExceptionMapperTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/AuthorisationRequestExceptionMapperTest.java new file mode 100644 index 000000000..cd9e4d79e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/AuthorisationRequestExceptionMapperTest.java @@ -0,0 +1,65 @@ +package uk.gov.pay.api.exception.mapper; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.exception.AuthorisationRequestException; +import uk.gov.pay.api.exception.ConnectorResponseErrorException; +import uk.gov.pay.api.model.RequestError; +import uk.gov.service.payments.commons.model.ErrorIdentifier; + +import javax.ws.rs.core.Response; + +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.when; +import static uk.gov.pay.api.model.RequestError.Code.CREATE_PAYMENT_CONNECTOR_ERROR; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AUTHORISATION_ERROR; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AUTHORISATION_REJECTED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AUTHORISATION_TIMEOUT; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.CARD_NUMBER_REJECTED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.INVALID_ATTRIBUTE_VALUE; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.ONE_TIME_TOKEN_ALREADY_USED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.ONE_TIME_TOKEN_INVALID; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.TELEPHONE_PAYMENT_NOTIFICATIONS_NOT_ALLOWED; + +@ExtendWith(MockitoExtension.class) +class AuthorisationRequestExceptionMapperTest { + + @Mock + private Response mockResponse; + + private final AuthorisationRequestExceptionMapper mapper = new AuthorisationRequestExceptionMapper(); + + static Object[] parametersForMapping() { + return new Object[]{ + new Object[]{CARD_NUMBER_REJECTED, true, "An error message from connector", 402, "P0010"}, + new Object[]{AUTHORISATION_REJECTED, true, "An error message from connector", 402, "P0010"}, + new Object[]{AUTHORISATION_ERROR, false, "There was an error authorising the payment", 500, "P0050"}, + new Object[]{AUTHORISATION_TIMEOUT, false, "There was an error authorising the payment", 500, "P0050"}, + new Object[]{ONE_TIME_TOKEN_INVALID, true, "An error message from connector", 400, "P1211"}, + new Object[]{ONE_TIME_TOKEN_ALREADY_USED, true, "An error message from connector", 400, "P1212"}, + new Object[]{INVALID_ATTRIBUTE_VALUE, true, "An error message from connector", 422, "P0102"}, + new Object[]{TELEPHONE_PAYMENT_NOTIFICATIONS_NOT_ALLOWED, false, "Downstream system error", 500, "P0198"} + + }; + } + + @ParameterizedTest + @MethodSource("parametersForMapping") + void testMapping(ErrorIdentifier errorIdentifier, boolean messageFromConnector, String expectedDescription, int expectedStatusCode, String errorCode) { + List connectorErrorMessages = messageFromConnector ? List.of(expectedDescription) : List.of("Generic error message"); + when(mockResponse.readEntity(ConnectorResponseErrorException.ConnectorErrorResponse.class)) + .thenReturn(new ConnectorResponseErrorException.ConnectorErrorResponse(errorIdentifier, connectorErrorMessages)); + Response returnedResponse = mapper.toResponse(new AuthorisationRequestException(mockResponse)); + RequestError returnedError = (RequestError) returnedResponse.getEntity(); + assertThat(returnedError.getCode(), is(errorCode)); + assertThat(returnedError.getDescription(), is(expectedDescription)); + assertThat(returnedResponse.getStatus(), is(expectedStatusCode)); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/CancelAgreementExceptionMapperTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/CancelAgreementExceptionMapperTest.java new file mode 100644 index 000000000..ebedfb14a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/CancelAgreementExceptionMapperTest.java @@ -0,0 +1,58 @@ +package uk.gov.pay.api.exception.mapper; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.exception.CancelAgreementException; +import uk.gov.pay.api.exception.ConnectorResponseErrorException.ConnectorErrorResponse; +import uk.gov.pay.api.model.RequestError; +import uk.gov.service.payments.commons.model.ErrorIdentifier; + +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.when; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AGREEMENT_NOT_ACTIVE; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AGREEMENT_NOT_FOUND; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.GENERIC; + +@ExtendWith(MockitoExtension.class) +class CancelAgreementExceptionMapperTest { + + @Mock + private Response mockResponse; + + private final CancelAgreementExceptionMapper mapper = new CancelAgreementExceptionMapper(); + + @ParameterizedTest + @MethodSource + void testExceptionMapping(int statusCodeFromConnector, ErrorIdentifier errorIdentifierFromConnector, + int expectedStatusCode, String expectedErrorCode, String expectedDescription) { + when(mockResponse.getStatus()).thenReturn(statusCodeFromConnector); + when(mockResponse.readEntity(ConnectorErrorResponse.class)).thenReturn(new ConnectorErrorResponse(errorIdentifierFromConnector, List.of())); + + Response returnedResponse = mapper.toResponse(new CancelAgreementException(mockResponse)); + + assertThat(returnedResponse.getStatus(), is(expectedStatusCode)); + + RequestError returnedError = (RequestError) returnedResponse.getEntity(); + assertThat(returnedError.getDescription(), is(expectedDescription)); + assertThat(returnedError.getCode(), is(expectedErrorCode)); + } + + static Stream testExceptionMapping() { + return Stream.of( + arguments(404, AGREEMENT_NOT_FOUND, 404, "P2500", "Not found"), + arguments(400, AGREEMENT_NOT_ACTIVE, 400, "P2501", "Cancellation of agreement failed"), + arguments(500, GENERIC, 500, "P2598", "Downstream system error") + ); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/CreateAgreementExceptionMapperTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/CreateAgreementExceptionMapperTest.java new file mode 100644 index 000000000..ca721620b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/CreateAgreementExceptionMapperTest.java @@ -0,0 +1,53 @@ +package uk.gov.pay.api.exception.mapper; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.exception.ConnectorResponseErrorException.ConnectorErrorResponse; +import uk.gov.pay.api.exception.CreateAgreementException; +import uk.gov.pay.api.model.RequestError; +import uk.gov.service.payments.commons.model.ErrorIdentifier; + +import javax.ws.rs.core.Response; + +import static org.apache.hc.core5.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; +import static org.apache.hc.core5.http.HttpStatus.SC_UNPROCESSABLE_ENTITY; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +@ExtendWith(MockitoExtension.class) +class CreateAgreementExceptionMapperTest { + + @Mock + private Response mockResponse; + private CreateAgreementExceptionMapper mapper = new CreateAgreementExceptionMapper(); + + @Test + void testExceptionMapping() { + when(mockResponse.readEntity(ConnectorErrorResponse.class)) + .thenReturn(new ConnectorErrorResponse(ErrorIdentifier.GENERIC, null, null)); + Response returnedResponse = mapper.toResponse(new CreateAgreementException(mockResponse)); + RequestError returnedError = (RequestError) returnedResponse.getEntity(); + RequestError expectedError = aRequestError(RequestError.Code.CREATE_AGREEMENT_CONNECTOR_ERROR); + assertThat(returnedResponse.getStatus(), is(SC_INTERNAL_SERVER_ERROR)); + assertThat(returnedError.getDescription(), + is(expectedError.getDescription())); + assertThat(returnedError.getCode(), is(expectedError.getCode())); + } + + @Test + void testExceptionMappingForRecurringCardPaymentsNotAllowed() { + when(mockResponse.readEntity(ConnectorErrorResponse.class)) + .thenReturn(new ConnectorErrorResponse(ErrorIdentifier.RECURRING_CARD_PAYMENTS_NOT_ALLOWED, null, null)); + Response returnedResponse = mapper.toResponse(new CreateAgreementException(mockResponse)); + RequestError returnedError = (RequestError) returnedResponse.getEntity(); + RequestError expectedError = aRequestError(RequestError.Code.RECURRING_CARD_PAYMENTS_NOT_ALLOWED_ERROR); + assertThat(returnedResponse.getStatus(), is(SC_UNPROCESSABLE_ENTITY)); + assertThat(returnedError.getDescription(), + is(expectedError.getDescription())); + assertThat(returnedError.getCode(), is(expectedError.getCode())); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/CreateChargeExceptionMapperTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/CreateChargeExceptionMapperTest.java new file mode 100644 index 000000000..03072847a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/CreateChargeExceptionMapperTest.java @@ -0,0 +1,77 @@ +package uk.gov.pay.api.exception.mapper; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.exception.ConnectorResponseErrorException.ConnectorErrorResponse; +import uk.gov.pay.api.exception.CreateChargeException; +import uk.gov.pay.api.model.RequestError; +import uk.gov.service.payments.commons.model.ErrorIdentifier; + +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.when; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.ACCOUNT_DISABLED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.ACCOUNT_NOT_LINKED_WITH_PSP; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AGREEMENT_NOT_ACTIVE; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AGREEMENT_NOT_FOUND; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AUTHORISATION_API_NOT_ALLOWED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.INCORRECT_AUTHORISATION_MODE_FOR_SAVE_PAYMENT_INSTRUMENT_TO_AGREEMENT; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.INVALID_ATTRIBUTE_VALUE; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.MISSING_MANDATORY_ATTRIBUTE; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.MOTO_NOT_ALLOWED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.RECURRING_CARD_PAYMENTS_NOT_ALLOWED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.TELEPHONE_PAYMENT_NOTIFICATIONS_NOT_ALLOWED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.UNEXPECTED_ATTRIBUTE; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.ZERO_AMOUNT_NOT_ALLOWED; + +@ExtendWith(MockitoExtension.class) +class CreateChargeExceptionMapperTest { + + @Mock + private Response mockResponse; + + private final CreateChargeExceptionMapper mapper = new CreateChargeExceptionMapper(); + + @ParameterizedTest + @MethodSource + void testExceptionMapping(ErrorIdentifier errorIdentifier, boolean messageFromConnector, String expectedDescription, int expectedStatusCode, String expectedErrorCode) { + List connectorErrorMessages = messageFromConnector ? List.of(expectedDescription) : List.of(); + when(mockResponse.readEntity(ConnectorErrorResponse.class)) + .thenReturn(new ConnectorErrorResponse(errorIdentifier, connectorErrorMessages)); + + Response returnedResponse = mapper.toResponse(new CreateChargeException(mockResponse)); + RequestError returnedError = (RequestError) returnedResponse.getEntity(); + + assertThat(returnedResponse.getStatus(), is(expectedStatusCode)); + assertThat(returnedError.getDescription(), is(expectedDescription)); + assertThat(returnedError.getCode(), is(expectedErrorCode)); + } + + static Stream testExceptionMapping() { + return Stream.of( + arguments(ZERO_AMOUNT_NOT_ALLOWED, false, "Invalid attribute value: amount. Must be greater than or equal to 1", 422, "P0102"), + arguments(MOTO_NOT_ALLOWED, false, "MOTO payments are not enabled for this account. Please contact support if you would like to process MOTO payments - https://www.payments.service.gov.uk/support/ .", 422, "P0196"), + arguments(ACCOUNT_DISABLED, false, "GOV.UK Pay has disabled payment and refund creation on this account. Contact support with your error code - https://www.payments.service.gov.uk/support/ .", 403, "P0941"), + arguments(TELEPHONE_PAYMENT_NOTIFICATIONS_NOT_ALLOWED, false, "Access to this resource is not enabled for this account. Contact support with your error code - https://www.payments.service.gov.uk/support/ .", 403, "P0930"), + arguments(ACCOUNT_NOT_LINKED_WITH_PSP, false, "Account is not fully configured. Please refer to documentation to setup your account or contact support with your error code - https://www.payments.service.gov.uk/support/ .", 403, "P0940"), + arguments(AUTHORISATION_API_NOT_ALLOWED, false, "Using authorisation_mode of moto_api is not allowed for this account", 422, "P0195"), + arguments(MISSING_MANDATORY_ATTRIBUTE, true, "An error message from connector", 400, "P0101"), + arguments(UNEXPECTED_ATTRIBUTE, true, "An error message from connector", 400, "P0104"), + arguments(INCORRECT_AUTHORISATION_MODE_FOR_SAVE_PAYMENT_INSTRUMENT_TO_AGREEMENT, true, "Unexpected attribute: set_up_agreement", 400, "P0104"), + arguments(AGREEMENT_NOT_FOUND, false, "Invalid attribute value: agreement_id. Agreement does not exist", 400, "P0102"), + arguments(AGREEMENT_NOT_ACTIVE, false, "Invalid attribute value: agreement_id. Agreement must be active", 400, "P0102"), + arguments(INVALID_ATTRIBUTE_VALUE, true, "An error message from connector", 422, "P0102"), + arguments(RECURRING_CARD_PAYMENTS_NOT_ALLOWED, true, "Recurring card payments are currently disabled for this service. Contact support with your error code - https://www.payments.service.gov.uk/support/", 422, "P0942") + ); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/CreateRefundExceptionMapperTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/CreateRefundExceptionMapperTest.java new file mode 100644 index 000000000..2fe9903e4 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/exception/mapper/CreateRefundExceptionMapperTest.java @@ -0,0 +1,59 @@ +package uk.gov.pay.api.exception.mapper; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.exception.ConnectorResponseErrorException; +import uk.gov.pay.api.exception.CreateRefundException; +import uk.gov.pay.api.model.RequestError; +import uk.gov.service.payments.commons.model.ErrorIdentifier; + +import javax.ws.rs.core.Response; + +import java.util.Collections; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.ACCOUNT_DISABLED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.GENERIC; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.REFUND_AMOUNT_AVAILABLE_MISMATCH; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.REFUND_NOT_AVAILABLE; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.REFUND_NOT_AVAILABLE_DUE_TO_DISPUTE; + +@ExtendWith(MockitoExtension.class) +class CreateRefundExceptionMapperTest { + + @Mock + private Response mockResponse; + + private final CreateRefundExceptionMapper mapper = new CreateRefundExceptionMapper(); + + static Object[] parametersForMapping() { + return new Object[] { + new Object[]{ACCOUNT_DISABLED, null, "GOV.UK Pay has disabled payment and refund creation on this account. Contact support with your error code - https://www.payments.service.gov.uk/support/ .", 403, "P0941"}, + new Object[]{REFUND_AMOUNT_AVAILABLE_MISMATCH, null, "Refund amount available mismatch.", 412, "P0604"}, + new Object[]{REFUND_NOT_AVAILABLE, "This is a reason", "The payment is not available for refund. Payment refund status: This is a reason", 400, "P0603"}, + new Object[]{REFUND_NOT_AVAILABLE, null, "Downstream system error", 500, "P0698"}, + new Object[]{REFUND_NOT_AVAILABLE_DUE_TO_DISPUTE, null, "The payment is disputed and cannot be refunded", 400, "P0603"}, + new Object[]{GENERIC, null, "Downstream system error", 500, "P0698"}, + }; + } + + @ParameterizedTest + @MethodSource("parametersForMapping") + void testExceptionMapping(ErrorIdentifier errorIdentifier, String reason, String expectedDescription, int expectedStatusCode, String expectedErrorCode) { + when(mockResponse.readEntity(ConnectorResponseErrorException.ConnectorErrorResponse.class)) + .thenReturn(new ConnectorResponseErrorException.ConnectorErrorResponse(errorIdentifier, reason, Collections.emptyList())); + + Response returnedResponse = mapper.toResponse(new CreateRefundException(mockResponse)); + RequestError returnedError = (RequestError) returnedResponse.getEntity(); + + assertThat(returnedResponse.getStatus(), is(expectedStatusCode)); + assertThat(returnedError.getDescription(), is(expectedDescription)); + assertThat(returnedError.getCode(), is(expectedErrorCode)); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/AuthorizationValidationFilterTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/AuthorizationValidationFilterTest.java new file mode 100644 index 000000000..6a2480aeb --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/AuthorizationValidationFilterTest.java @@ -0,0 +1,145 @@ +package uk.gov.pay.api.filter; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static uk.gov.pay.api.utils.ApiKeyGenerator.apiKeyValueOf; + +@ExtendWith(MockitoExtension.class) +class AuthorizationValidationFilterTest { + + private static final String SECRET_KEY = "mysupersecret"; + private AuthorizationValidationFilter authorizationValidationFilter; + + @Mock + private PublicApiConfig mockConfiguration; + @Mock + private HttpServletRequest mockRequest; + @Mock + private HttpServletResponse mockResponse; + @Mock + private FilterChain mockFilterChain; + @Mock + private Appender mockAppender; + @Captor + ArgumentCaptor loggingEventArgumentCaptor; + + @BeforeEach + void setup() { + Logger logger = (Logger) LoggerFactory.getLogger(AuthorizationValidationFilter.class); + logger.setLevel(Level.WARN); + logger.addAppender(mockAppender); + + when(mockConfiguration.getApiKeyHmacSecret()).thenReturn(SECRET_KEY); + + authorizationValidationFilter = new AuthorizationValidationFilter(mockConfiguration); + } + + @Test + void shouldProcessFilterChain_whenAuthorizationHeaderIsValid() throws Exception { + + String validToken = "asdfghdasd"; + String authorization = "Bearer " + apiKeyValueOf(validToken, SECRET_KEY); + + when(mockRequest.getRequestURI()).thenReturn("/v1/payments"); + when(mockRequest.getHeader("Authorization")).thenReturn(authorization); + + authorizationValidationFilter.doFilter(mockRequest, mockResponse, mockFilterChain); + + verify(mockFilterChain).doFilter(mockRequest, mockResponse); + } + + @Test + void shouldProcessFilterChain_whenUrlIsAuthURLAndNoAuthorisationHeaderPresent() throws Exception { + when(mockRequest.getRequestURI()).thenReturn("/v1/auth"); + authorizationValidationFilter.doFilter(mockRequest, mockResponse, mockFilterChain); + verify(mockFilterChain).doFilter(mockRequest, mockResponse); + } + + @Test + void shouldRejectRequest_with401ResponseError_whenAuthorizationHeaderIsInvalid() throws Exception { + + String invalidApiKey = "asdfghdasdakjshdkjwhdjweghrhjgwerguweurweruhiweuiweriuui"; + String authorization = "Bearer " + invalidApiKey; + + when(mockRequest.getRequestURI()).thenReturn("/v1/payments"); + when(mockRequest.getHeader("Authorization")).thenReturn(authorization); + + authorizationValidationFilter.doFilter(mockRequest, mockResponse, mockFilterChain); + + verifyNoInteractions(mockFilterChain); + verify(mockResponse).sendError(401, "Unauthorized"); + + verify(mockAppender).doAppend(loggingEventArgumentCaptor.capture()); + List logEvents = loggingEventArgumentCaptor.getAllValues(); + assertThat(logEvents, hasSize(1)); + assertThat(logEvents.get(0).getFormattedMessage(), is("Attempt to authenticate using an API key with an invalid checksum")); + } + + @Test + void shouldRejectRequest_with401ResponseError_whenAuthorizationHeaderIsNotPresent() throws Exception { + + when(mockRequest.getRequestURI()).thenReturn("/v1/payments"); + when(mockRequest.getHeader("Authorization")).thenReturn(null); + + authorizationValidationFilter.doFilter(mockRequest, mockResponse, mockFilterChain); + + verifyNoInteractions(mockFilterChain); + verify(mockResponse).sendError(401, "Unauthorized"); + } + + @Test + void shouldRejectRequest_with401ResponseError_whenAuthorizationHeaderHasInvalidFormat() throws Exception { + + String validToken = "asdfghdasd"; + String authorization = "Bearer" + apiKeyValueOf(validToken, SECRET_KEY); + + when(mockRequest.getRequestURI()).thenReturn("/v1/payments"); + when(mockRequest.getHeader("Authorization")).thenReturn(authorization); + + authorizationValidationFilter.doFilter(mockRequest, mockResponse, mockFilterChain); + + verifyNoInteractions(mockFilterChain); + verify(mockResponse).sendError(401, "Unauthorized"); + } + + @Test + void shouldRejectRequest_with401ResponseError_whenAuthorizationHeaderHasNotMinimumLengthExpected() throws Exception { + + String apiKey = RandomStringUtils.randomAlphanumeric(32); + String authorization = "Bearer " + apiKey; + + when(mockRequest.getRequestURI()).thenReturn("/v1/payments"); + when(mockRequest.getHeader("Authorization")).thenReturn(authorization); + + authorizationValidationFilter.doFilter(mockRequest, mockResponse, mockFilterChain); + + verifyNoInteractions(mockFilterChain); + verify(mockResponse).sendError(401, "Unauthorized"); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/RateLimiterFilterTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/RateLimiterFilterTest.java new file mode 100644 index 000000000..5a14f3bc2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/RateLimiterFilterTest.java @@ -0,0 +1,90 @@ +package uk.gov.pay.api.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.filter.ratelimit.RateLimitException; +import uk.gov.pay.api.filter.ratelimit.RateLimiter; +import uk.gov.pay.api.model.TokenPaymentType; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriInfo; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class RateLimiterFilterTest { + + public static final String ACCOUNT_ID = "account-id"; + private RateLimiterFilter rateLimiterFilter; + private RateLimiter rateLimiter; + @Mock + private ContainerRequestContext mockContainerRequestContext; + @Mock + private UriInfo mockUriInfo; + + @BeforeEach + public void setup() { + rateLimiter = mock(RateLimiter.class); + rateLimiterFilter = new RateLimiterFilter(rateLimiter, new ObjectMapper()); + + when(mockContainerRequestContext.getUriInfo()).thenReturn(mockUriInfo); + when(mockUriInfo.getPath()).thenReturn(""); + } + + @Test + public void shouldCheckRateLimitsWhenFilterIsInvoked() throws Exception { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "some-token-link"); + SecurityContext mockSecurityContext = mock(SecurityContext.class); + when(mockSecurityContext.getUserPrincipal()).thenReturn(account); + when(mockContainerRequestContext.getSecurityContext()).thenReturn(mockSecurityContext); + when(mockContainerRequestContext.getMethod()).thenReturn("GET"); + when(mockContainerRequestContext.getMethod()).thenReturn("POST"); + + rateLimiterFilter.filter(mockContainerRequestContext); + + verify(rateLimiter).checkRateOf(eq(ACCOUNT_ID), any()); + } + + @Test + public void shouldSendErrorResponse_whenRateLimitExceeded() throws Exception { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "some-token-link"); + SecurityContext mockSecurityContext = mock(SecurityContext.class); + when(mockSecurityContext.getUserPrincipal()).thenReturn(account); + when(mockContainerRequestContext.getSecurityContext()).thenReturn(mockSecurityContext); + when(mockContainerRequestContext.getMethod()).thenReturn("GET"); + doThrow(RateLimitException.class).when(rateLimiter).checkRateOf(eq("account-id"), any()); + + WebApplicationException webApplicationException = assertThrows(WebApplicationException.class, + () -> rateLimiterFilter.filter(mockContainerRequestContext)); + + Response response = webApplicationException.getResponse(); + assertEquals(429, response.getStatus()); + assertEquals("application/json", response.getHeaderString("Content-Type")); + assertEquals("utf-8", response.getHeaderString("Content-Encoding")); + assertEquals("{\"code\":\"P0900\",\"description\":\"Too many requests\"}", response.getEntity()); + } + + @Test + public void shouldNotCheckRateLimit_on_healthcheck() throws Exception { + when(mockUriInfo.getPath()).thenReturn(("healthcheck")); + rateLimiterFilter.filter(mockContainerRequestContext); + verify(rateLimiter, never()).checkRateOf(anyString(), any()); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/RateLimiterKeyTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/RateLimiterKeyTest.java new file mode 100644 index 000000000..b92a3feff --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/RateLimiterKeyTest.java @@ -0,0 +1,53 @@ +package uk.gov.pay.api.filter; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.UriInfo; +import java.util.stream.Stream; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class RateLimiterKeyTest { + + @Mock + private ContainerRequestContext containerRequestContext; + + @Mock + private UriInfo uriInfo; + + @BeforeEach + public void setUp() { + when(containerRequestContext.getUriInfo()).thenReturn(uriInfo); + } + + static Stream rateLimitParams() { + return Stream.of( + arguments("/v1/payments", "POST", "POST-create_payment", "POST-create_payment-account_id"), + arguments("/v1/payments/paymentId/capture", "POST", "POST-capture_payment", "POST-capture_payment-account_id"), + arguments("/v1/payments/paymentId/cancel", "POST", "POST", "POST-account_id"), + arguments("/v1/payments", "GET", "GET", "GET-account_id") + ); + } + + @ParameterizedTest + @MethodSource("rateLimitParams") + public void returnsRateLimiterKey(String path, String method, String expectedKeyType, String expectedKey) { + when(uriInfo.getPath()).thenReturn(path); + when(containerRequestContext.getMethod()).thenReturn(method); + + var rateLimiterKey = RateLimiterKey.from(containerRequestContext, "account_id"); + assertThat(rateLimiterKey.getKey(), is(expectedKey)); + assertThat(rateLimiterKey.getKeyType(), is(expectedKeyType)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/ratelimit/LocalRateLimiterTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/ratelimit/LocalRateLimiterTest.java new file mode 100644 index 000000000..7e9f8ce83 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/ratelimit/LocalRateLimiterTest.java @@ -0,0 +1,145 @@ +package uk.gov.pay.api.filter.ratelimit; + + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.app.config.RateLimiterConfig; +import uk.gov.pay.api.filter.RateLimiterKey; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class LocalRateLimiterTest { + + private static final String POST = "POST"; + private static final String accountId = "account-id"; + LocalRateLimiter localRateLimiter; + + @Captor + ArgumentCaptor loggingEventArgumentCaptor; + + @Mock + private Appender mockAppender; + + @Mock + private RateLimiterConfig rateLimiterConfig; + + @BeforeEach + public void setup() { + Logger root = (Logger) LoggerFactory.getLogger(LocalRateLimiter.class); + root.setLevel(Level.INFO); + root.addAppender(mockAppender); + } + + @Test + public void rateLimiterSetTo_2CallsPerSecond_shouldAllow2ConsecutiveCallsWithSameKeys() throws Exception { + when(rateLimiterConfig.getNoOfReqPerNode()).thenReturn(2); + when(rateLimiterConfig.getNoOfReqForPostPerNode()).thenReturn(2); + when(rateLimiterConfig.getPerMillis()).thenReturn(1000); + + String key = "key1"; + var rateLimiterKey = new RateLimiterKey(key, "key-type", POST); + localRateLimiter = new LocalRateLimiter(rateLimiterConfig); + + localRateLimiter.checkRateOf(accountId, rateLimiterKey); + localRateLimiter.checkRateOf(accountId, rateLimiterKey); + } + + @Test + public void rateLimiterSetTo_1CallPer300Millis_shouldAFailWhen2ConsecutiveCallsWithSameKeysAreMade() throws RateLimitException { + when(rateLimiterConfig.getNoOfReqPerNode()).thenReturn(1); + when(rateLimiterConfig.getNoOfReqForPostPerNode()).thenReturn(1); + when(rateLimiterConfig.getPerMillis()).thenReturn(300); + + String key = "key2"; + var rateLimiterKey = new RateLimiterKey(key, "key-type", POST); + localRateLimiter = new LocalRateLimiter(rateLimiterConfig); + + localRateLimiter.checkRateOf(accountId, rateLimiterKey); + assertThrows(RateLimitException.class, () -> { + localRateLimiter.checkRateOf(accountId, rateLimiterKey); + verify(mockAppender, times(1)).doAppend(loggingEventArgumentCaptor.capture()); + List loggingEvents = loggingEventArgumentCaptor.getAllValues(); + assertEquals("LocalRateLimiter - Rate limit exceeded for account [account-id] and method [POST] - count: 2, rate allowed: 1", + loggingEvents.get(0).getFormattedMessage()); + }); + } + + @Test + public void rateLimiterSetTo_3CallsPerSecond_shouldAllowMakingOnly3CallsWithSameKey() throws Exception { + when(rateLimiterConfig.getNoOfReqPerNode()).thenReturn(3); + when(rateLimiterConfig.getNoOfReqForPostPerNode()).thenReturn(3); + when(rateLimiterConfig.getPerMillis()).thenReturn(1000); + + String key = "key3"; + var rateLimiterKey = new RateLimiterKey(key, "key-type", POST); + localRateLimiter = new LocalRateLimiter(rateLimiterConfig); + + ExecutorService executor = Executors.newFixedThreadPool(3); + + List> tasks = Arrays.asList( + () -> { + localRateLimiter.checkRateOf(accountId, rateLimiterKey); + return "task1"; + }, + () -> { + localRateLimiter.checkRateOf(accountId, rateLimiterKey); + return "task2"; + }, + () -> { + localRateLimiter.checkRateOf(accountId, rateLimiterKey); + return "task3"; + }, + () -> { + localRateLimiter.checkRateOf(accountId, rateLimiterKey); + return "task4"; + } + ); + + List successfulTasks = executor.invokeAll(tasks) + .stream() + .map(future -> { + try { + return future.get(); + } catch (Exception e) { + assertThat(e, is(instanceOf(ExecutionException.class))); + ExecutionException ex = (ExecutionException) e; + assertThat(ex.getCause(), is(instanceOf(RateLimitException.class))); + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + assertThat(successfulTasks.size(), is(3)); + } + + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/ratelimit/RateLimitManagerTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/ratelimit/RateLimitManagerTest.java new file mode 100644 index 000000000..f82310f35 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/ratelimit/RateLimitManagerTest.java @@ -0,0 +1,108 @@ +package uk.gov.pay.api.filter.ratelimit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.app.config.RateLimiterConfig; +import uk.gov.pay.api.filter.RateLimiterKey; + +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class RateLimitManagerTest { + + @Mock + private RateLimiterConfig rateLimiterConfig; + + private RateLimitManager rateLimitManager; + + @Test + public void returnsNumberOfAllowedRequestsForNoAccount() { + var rateLimiterKey = new RateLimiterKey("path", "key-type", "PUT"); + when(rateLimiterConfig.getNoOfReq()).thenReturn(1); + + rateLimitManager = new RateLimitManager(rateLimiterConfig); + assertThat(rateLimitManager.getAllowedNumberOfRequests(rateLimiterKey, ""), is(1)); + } + + @Test + public void returnsNumberOfAllowedRequestsForPostForAccount1() { + var rateLimiterKey = new RateLimiterKey("path", "key-type", "POST"); + when(rateLimiterConfig.getElevatedAccounts()).thenReturn(List.of("1")); + when(rateLimiterConfig.getNoOfPostReqForElevatedAccounts()).thenReturn(4); + + rateLimitManager = new RateLimitManager(rateLimiterConfig); + assertThat(rateLimitManager.getAllowedNumberOfRequests(rateLimiterKey, "1"), is(4)); + } + + @Test + public void returnsNumberOfAllowedRequestsForGetForAccount1() { + var rateLimiterKey = new RateLimiterKey("path", "key-type", "GET"); + when(rateLimiterConfig.getElevatedAccounts()).thenReturn(List.of("1")); + when(rateLimiterConfig.getNoOfReqForElevatedAccounts()).thenReturn(3); + + rateLimitManager = new RateLimitManager(rateLimiterConfig); + assertThat(rateLimitManager.getAllowedNumberOfRequests(rateLimiterKey, "1"), is(3)); + } + + @Test + public void returnsNumberOfAllowedRequestsForPostForAccount2() { + var rateLimiterKey = new RateLimiterKey("path", "key-type", "POST"); + when(rateLimiterConfig.getNoOfReqForPost()).thenReturn(2); + + rateLimitManager = new RateLimitManager(rateLimiterConfig); + assertThat(rateLimitManager.getAllowedNumberOfRequests(rateLimiterKey, "2"), is(2)); + } + + @Test + public void returnsNumberOfAllowedRequestsForGetForAccount2() { + var rateLimiterKey = new RateLimiterKey("path", "key-type", "GET"); + when(rateLimiterConfig.getNoOfReq()).thenReturn(1); + + rateLimitManager = new RateLimitManager(rateLimiterConfig); + assertThat(rateLimitManager.getAllowedNumberOfRequests(rateLimiterKey, "2"), is(1)); + } + + @Test + public void shouldReturnNumberOfAllowedPostRequestsCorrectlyForLowTrafficAccounts() { + var rateLimiterKey = new RateLimiterKey("path", "key-type", "POST"); + when(rateLimiterConfig.getLowTrafficAccounts()).thenReturn(List.of("10")); + when(rateLimiterConfig.getNoOfPostReqForLowTrafficAccounts()).thenReturn(7); + + rateLimitManager = new RateLimitManager(rateLimiterConfig); + assertThat(rateLimitManager.getAllowedNumberOfRequests(rateLimiterKey, "10"), is(7)); + } + + @Test + public void shouldReturnNumberOfAllowedGetRequestsCorrectlyForLowTrafficAccounts() { + var rateLimiterKey = new RateLimiterKey("path", "key-type", "GET"); + when(rateLimiterConfig.getLowTrafficAccounts()).thenReturn(List.of("10")); + when(rateLimiterConfig.getNoOfReqForLowTrafficAccounts()).thenReturn(100); + + rateLimitManager = new RateLimitManager(rateLimiterConfig); + assertThat(rateLimitManager.getAllowedNumberOfRequests(rateLimiterKey, "10"), is(100)); + } + + @Test + public void shouldReturnRateLimitIntervalCorrectlyForLowTrafficAccounts() { + when(rateLimiterConfig.getLowTrafficAccounts()).thenReturn(List.of("10")); + when(rateLimiterConfig.getIntervalInMillisForLowTrafficAccounts()).thenReturn(54000); + + rateLimitManager = new RateLimitManager(rateLimiterConfig); + assertThat(rateLimitManager.getRateLimitInterval("10"), is(54000)); + } + + @Test + public void shouldReturnRateLimitIntervalCorrectlyForElevatedAndStandardRateLimiting() { + when(rateLimiterConfig.getPerMillis()).thenReturn(10000); + + rateLimitManager = new RateLimitManager(rateLimiterConfig); + assertThat(rateLimitManager.getRateLimitInterval("12345"), is(10000)); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/ratelimit/RateLimiterTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/ratelimit/RateLimiterTest.java new file mode 100644 index 000000000..acb14adb1 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/ratelimit/RateLimiterTest.java @@ -0,0 +1,50 @@ +package uk.gov.pay.api.filter.ratelimit; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.filter.RateLimiterKey; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class RateLimiterTest { + + private static final String POST = "POST"; + private static final String accountId = "account-id"; + @Mock + private LocalRateLimiter localRateLimiter; + @Mock + private RedisRateLimiter redisRateLimiter; + + private RateLimiterKey rateLimiterKey; + private RateLimiter rateLimiter; + + @BeforeEach + public void setup() { + rateLimiterKey = new RateLimiterKey("key2", "key-type", POST); + rateLimiter = new RateLimiter(localRateLimiter, redisRateLimiter); + } + + @Test + public void shouldInvokeRedisRateLimiter_whenRedisDbIsAvaiable() throws Exception { + rateLimiter.checkRateOf(accountId, rateLimiterKey); + rateLimiter.checkRateOf(accountId, rateLimiterKey); + + verify(redisRateLimiter, times(2)).checkRateOf(accountId, rateLimiterKey); + } + + @Test + public void shouldInvokeLocalRateLimiter_whenRedisIsNotAvaiable() throws Exception { + doThrow(new RedisException()).when(redisRateLimiter).checkRateOf(accountId, rateLimiterKey); + + rateLimiter.checkRateOf(accountId, rateLimiterKey); + rateLimiter.checkRateOf(accountId, rateLimiterKey); + + verify(localRateLimiter, times(2)).checkRateOf(accountId, rateLimiterKey); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/ratelimit/RedisRateLimiterTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/ratelimit/RedisRateLimiterTest.java new file mode 100644 index 000000000..b14bf8c2e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/filter/ratelimit/RedisRateLimiterTest.java @@ -0,0 +1,181 @@ +package uk.gov.pay.api.filter.ratelimit; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import io.dropwizard.setup.Environment; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.app.config.RateLimiterConfig; +import uk.gov.pay.api.filter.RateLimiterKey; +import uk.gov.pay.api.managed.RedisClientManager; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class RedisRateLimiterTest { + + private static final String accountId = "account-id"; + private static final Long perSecondTimeToLiveInSeconds = 1L; + private static final Long perMinuteTimeToLiveInSeconds = 60L; + + @Mock + private RedisClientManager redisClientManager; + + @Mock + private StatefulRedisConnection statefulRedisConnection; + + @Mock + private RedisCommands redisCommands; + + @Mock + private RateLimiterKey rateLimiterKey; + + @Mock + private RateLimiterConfig rateLimiterConfig; + + @Mock + private Environment environment; + + @Captor + ArgumentCaptor loggingEventArgumentCaptor; + + private RedisRateLimiter redisRateLimiter; + + @Mock + private Appender mockAppender; + + @Mock + private MetricRegistry metricsRegistry; + + private Timer timer; + + @BeforeEach + public void setup() { + timer = new Timer(); + when(statefulRedisConnection.sync()).thenReturn(redisCommands); + when(redisClientManager.getRedisConnection()).thenReturn(statefulRedisConnection); + when(environment.metrics()).thenReturn(metricsRegistry); + when(metricsRegistry.timer(any())).thenReturn(timer); + + Logger root = (Logger) LoggerFactory.getLogger(RedisRateLimiter.class); + root.setLevel(Level.INFO); + root.addAppender(mockAppender); + } + + @Test + public void rateLimiterSetTo_1CallPerSecond_shouldAllowSingleCall() throws Exception { + when(rateLimiterConfig.getNoOfReq()).thenReturn(1); + when(rateLimiterKey.getKey()).thenReturn("Key1"); + when(rateLimiterConfig.getPerMillis()).thenReturn(1000); + redisRateLimiter = new RedisRateLimiter(rateLimiterConfig, redisClientManager, environment); + + when(redisCommands.incr(anyString())).thenReturn(1L); + when(redisCommands.expire(anyString(), eq(perSecondTimeToLiveInSeconds))).thenReturn(true); + redisRateLimiter.checkRateOf(accountId, rateLimiterKey); + verify(redisCommands).expire(anyString(), eq(perSecondTimeToLiveInSeconds)); + } + + @Test + public void rateLimiterSetTo_2CallsPerSecond_shouldAllow2ConsecutiveCallsWithSameKeys() throws Exception { + when(rateLimiterConfig.getNoOfReq()).thenReturn(2); + when(rateLimiterKey.getKey()).thenReturn("Key2"); + when(rateLimiterConfig.getPerMillis()).thenReturn(1000); + redisRateLimiter = new RedisRateLimiter(rateLimiterConfig, redisClientManager, environment); + + when(redisCommands.incr(anyString())).thenReturn(1L, 2L); + when(redisCommands.expire(anyString(), eq(perSecondTimeToLiveInSeconds))).thenReturn(true); + + redisRateLimiter.checkRateOf(accountId, rateLimiterKey); + redisRateLimiter.checkRateOf(accountId, rateLimiterKey); + verify(redisCommands, times(2)).expire(anyString(), eq(perSecondTimeToLiveInSeconds)); + } + + @Test + public void rateLimiterSetTo_2CallsPerSecond_shouldFailWhen3ConsecutiveCallsWithSameKeysAreMade() throws RedisException, RateLimitException { + when(rateLimiterConfig.getNoOfReq()).thenReturn(2); + when(rateLimiterKey.getKey()).thenReturn("Key3"); + when(rateLimiterKey.getKeyType()).thenReturn("POST"); + when(rateLimiterConfig.getPerMillis()).thenReturn(1000); + redisRateLimiter = new RedisRateLimiter(rateLimiterConfig, redisClientManager, environment); + + when(redisCommands.incr(anyString())).thenReturn(1L, 2L, 3L); + when(redisCommands.expire(anyString(), eq(perSecondTimeToLiveInSeconds))).thenReturn(true); + + redisRateLimiter.checkRateOf(accountId, rateLimiterKey); + redisRateLimiter.checkRateOf(accountId, rateLimiterKey); + + assertThrows(RateLimitException.class, () -> { + redisRateLimiter.checkRateOf(accountId, rateLimiterKey); + verify(redisCommands, times(3)).expire(anyString(), eq(perSecondTimeToLiveInSeconds)); + verify(mockAppender, times(1)).doAppend(loggingEventArgumentCaptor.capture()); + List loggingEvents = loggingEventArgumentCaptor.getAllValues(); + assertEquals("RedisRateLimiter - Rate limit exceeded for account [account-id] and method [POST] - count: 3, rate allowed: 2", + loggingEvents.get(0).getFormattedMessage()); + }); + } + + @Test + public void shouldRateLimitPostRequestsForLowTrafficAccountsCorrectly() throws RedisException, RateLimitException { + when(rateLimiterConfig.getLowTrafficAccounts()).thenReturn(List.of(accountId)); + when(rateLimiterConfig.getNoOfPostReqForLowTrafficAccounts()).thenReturn(3); + when(rateLimiterConfig.getIntervalInMillisForLowTrafficAccounts()).thenReturn(60000); + when(rateLimiterKey.getMethod()).thenReturn("POST"); + when(rateLimiterKey.getKeyType()).thenReturn("POST-capture-account1"); + + redisRateLimiter = new RedisRateLimiter(rateLimiterConfig, redisClientManager, environment); + + when(redisCommands.incr(anyString())).thenReturn(1L, 2L, 3L, 4L); + when(redisCommands.expire(anyString(), eq(perMinuteTimeToLiveInSeconds))).thenReturn(true); + + redisRateLimiter.checkRateOf(accountId, rateLimiterKey); + redisRateLimiter.checkRateOf(accountId, rateLimiterKey); + redisRateLimiter.checkRateOf(accountId, rateLimiterKey); + verify(redisCommands, times(3)).expire(anyString(), eq(perMinuteTimeToLiveInSeconds)); + + assertThrows("Excepted to throw exception when rate limit exceeds", RateLimitException.class, () -> { + redisRateLimiter.checkRateOf(accountId, rateLimiterKey); + }); + } + @Test + public void shouldRateLimitGetRequestsForLowTrafficAccountsCorrectly() throws RedisException, RateLimitException { + when(rateLimiterConfig.getLowTrafficAccounts()).thenReturn(List.of(accountId)); + when(rateLimiterConfig.getNoOfReqForLowTrafficAccounts()).thenReturn(1); + when(rateLimiterConfig.getIntervalInMillisForLowTrafficAccounts()).thenReturn(60000); + when(rateLimiterKey.getMethod()).thenReturn("GET"); + when(rateLimiterKey.getKeyType()).thenReturn("GET-account1"); + + redisRateLimiter = new RedisRateLimiter(rateLimiterConfig, redisClientManager, environment); + + when(redisCommands.incr(anyString())).thenReturn(1L, 2L); + when(redisCommands.expire(anyString(), eq(perMinuteTimeToLiveInSeconds))).thenReturn(true); + + redisRateLimiter.checkRateOf(accountId, rateLimiterKey); + + assertThrows("Excepted to throw exception when rate limit exceeds", RateLimitException.class, () -> { + redisRateLimiter.checkRateOf(accountId, rateLimiterKey); + verify(redisCommands, times(3)).expire(anyString(), eq(perMinuteTimeToLiveInSeconds)); + }); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AgreementsApiResourceCancelIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AgreementsApiResourceCancelIT.java new file mode 100644 index 000000000..af72c8720 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AgreementsApiResourceCancelIT.java @@ -0,0 +1,143 @@ +package uk.gov.pay.api.it; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.restassured.http.ContentType; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; +import uk.gov.pay.api.utils.mocks.LedgerMockClient; + +import static io.restassured.RestAssured.given; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.utils.mocks.AgreementFromLedgerFixture.AgreementFromLedgerFixtureBuilder.anAgreementFromLedgerWithoutPaymentInstrumentFixture; + +public class AgreementsApiResourceCancelIT extends PaymentResourceITestBase { + + private final PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + private final LedgerMockClient ledgerMockClient = new LedgerMockClient(ledgerMock); + private final ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + + private final String agreementId = "an-agreement-id"; + + @Before + public void setUp() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void getAgreementFromLedger() throws JsonProcessingException { + var fixture = anAgreementFromLedgerWithoutPaymentInstrumentFixture() + .withExternalId(agreementId) + .build(); + ledgerMockClient.respondWithAgreement(agreementId, fixture); + + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .get(AGREEMENTS_PATH + agreementId) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("agreement_id", is(agreementId)) + .body("$", not(hasKey("service_id"))) + .body("reference", is(fixture.getReference())) + .body("description", is(fixture.getDescription())) + .body("status", is(fixture.getStatus().toLowerCase())) + .body("created_date", is(fixture.getCreatedDate())) + .body("$", not(hasKey("user_identifier"))) + .body("$", not(hasKey("cancelled_date"))) + .body("$", not(hasKey("cancelled_by_user_email"))); + } + + @Test + public void getCancelledAgreementFromLedger() throws JsonProcessingException { + var fixture = anAgreementFromLedgerWithoutPaymentInstrumentFixture() + .withExternalId(agreementId) + .withStatus("CANCELLED") + .withServiceId("service-id") + .withCancelledDate("2023-06-14T12:40:00.000Z") + .build(); + ledgerMockClient.respondWithAgreement(agreementId, fixture); + + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .get(AGREEMENTS_PATH + agreementId) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("agreement_id", is(agreementId)) + .body("$", not(hasKey("service_id"))) + .body("reference", is(fixture.getReference())) + .body("description", is(fixture.getDescription())) + .body("status", is(fixture.getStatus().toLowerCase())) + .body("created_date", is(fixture.getCreatedDate())) + .body("$", not(hasKey("user_identifier"))) + .body("cancelled_date", is(fixture.getCancelledDate())); + } + + @Test + public void getAgreement_MissingAgreementShouldMapException() { + ledgerMockClient.respondAgreementNotFound(agreementId); + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .get(AGREEMENTS_PATH + agreementId) + .then() + .statusCode(404) + .contentType(ContentType.JSON) + .body("code", is("P2200")) + .body("description", is("Not found")); + } + + @Test + public void cancelAgreement() { + connectorMockClient.respondOk_whenCancelAgreement(agreementId, GATEWAY_ACCOUNT_ID); + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .post(AGREEMENTS_PATH + agreementId + "/cancel") + .then() + .statusCode(204); + } + + @Test + public void cancelAgreementReturnsErrorWhenConnectorRespondsAgreementNotFound() { + connectorMockClient.respondAgreementNotFound_WhenCancelAgreement(agreementId, GATEWAY_ACCOUNT_ID, "error message"); + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .post(AGREEMENTS_PATH + agreementId + "/cancel") + .then() + .statusCode(404) + .contentType(ContentType.JSON) + .body("code", is("P2500")) + .body("description", is("Not found")); + } + + @Test + public void cancelAgreementReturnsErrorWhenConnectorRespondsAgreementNotActive() { + connectorMockClient.respondAgreementNotActive_WhenCancelAgreement(agreementId, GATEWAY_ACCOUNT_ID, "error message"); + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .post(AGREEMENTS_PATH + agreementId + "/cancel") + .then() + .statusCode(400) + .contentType(ContentType.JSON) + .body("code", is("P2501")) + .body("description", is("Cancellation of agreement failed")); + } + + @Test + public void cancelAgreementReturnsErrorWhenConnectorRespondsWithError() { + connectorMockClient.respondError_WhenCancelAgreement(agreementId, GATEWAY_ACCOUNT_ID, "error message"); + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .post(AGREEMENTS_PATH + agreementId + "/cancel") + .then() + .statusCode(500) + .contentType(ContentType.JSON) + .body("code", is("P2598")) + .body("description", is("Downstream system error")); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AgreementsApiResourceCreateIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AgreementsApiResourceCreateIT.java new file mode 100644 index 000000000..13fa53d3f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AgreementsApiResourceCreateIT.java @@ -0,0 +1,227 @@ +package uk.gov.pay.api.it; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.restassured.response.ValidatableResponse; +import org.apache.http.HttpStatus; +import org.junit.Test; +import uk.gov.pay.api.agreement.model.CreateAgreementRequest; +import uk.gov.pay.api.model.CreateAgreementRequestBuilder; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; +import uk.gov.pay.api.utils.mocks.CreateAgreementRequestParams; +import uk.gov.pay.api.utils.mocks.LedgerMockClient; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.apache.commons.lang3.RandomStringUtils.random; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.model.TokenPaymentType.CARD; +import static uk.gov.pay.api.utils.Payloads.agreementPayload; +import static uk.gov.pay.api.utils.mocks.AgreementFromLedgerFixture.AgreementFromLedgerFixtureBuilder.anAgreementFromLedgerWithoutPaymentInstrumentFixture; +import static uk.gov.pay.api.utils.mocks.CreateAgreementRequestParams.CreateAgreementRequestParamsBuilder.aCreateAgreementRequestParams; + +public class AgreementsApiResourceCreateIT extends PaymentResourceITestBase { + + private static final String REFERENCE = "Some reference "; + private static final String DESCRIPTION = "A valid description"; + private static final String USER_IDENTIFIER = "a-valid-user-identifier"; + public static final String VALID_AGREEMENT_ID = "12345678901234567890123456"; + private ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + private LedgerMockClient ledgerMockClient = new LedgerMockClient(ledgerMock); + + @Test + public void shouldCreateAgreement() throws JsonProcessingException { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + CreateAgreementRequestParams createAgreementRequestParams = aCreateAgreementRequestParams() + .withReference(REFERENCE) + .withDescription(DESCRIPTION) + .withUserIdentifier(USER_IDENTIFIER) + .build(); + var agreementFixture = anAgreementFromLedgerWithoutPaymentInstrumentFixture() + .withExternalId(VALID_AGREEMENT_ID) + .withPaymentInstrument(null) + .build(); + connectorMockClient.respondCreated_whenCreateAgreement(GATEWAY_ACCOUNT_ID, createAgreementRequestParams); + ledgerMockClient.respondWithAgreement(VALID_AGREEMENT_ID, agreementFixture); + postAgreementRequest(agreementPayload(createAgreementRequestParams)) + .statusCode(HttpStatus.SC_CREATED) + .contentType(JSON) + .body("agreement_id", is(VALID_AGREEMENT_ID)) + .body("reference", is("valid-reference")) + .body("description", is("An agreement description")); + connectorMockClient.verifyCreateAgreementConnectorRequest(GATEWAY_ACCOUNT_ID, createAgreementRequestParams); + } + + @Test + public void shouldReturn400WhenWhenReferenceIsEmptyString() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + CreateAgreementRequestParams createAgreementRequestParams = aCreateAgreementRequestParams() + .withReference("") + .build(); + postAgreementRequest(agreementPayload(createAgreementRequestParams)) + .statusCode(HttpStatus.SC_BAD_REQUEST) + .contentType(JSON) + .body("field", is("reference")) + .body("code", is("P2101")) + .body("description", is("Missing mandatory attribute: reference")); + } + + @Test + public void shouldReturn400WhenWhenReferenceIsNull() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + CreateAgreementRequest agreementRequest = new CreateAgreementRequest(CreateAgreementRequestBuilder.builder().reference(null).description(DESCRIPTION)); + postAgreementRequest(agreementRequest.toConnectorPayload()) + .statusCode(HttpStatus.SC_BAD_REQUEST) + .contentType(JSON) + .body("field", is("reference")) + .body("code", is("P2101")) + .body("description", is("Missing mandatory attribute: reference")); + } + + @Test + public void shouldReturn400WhenWhenDescriptionIsNull() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + CreateAgreementRequest agreementRequest = new CreateAgreementRequest(CreateAgreementRequestBuilder.builder().reference(REFERENCE).description(null)); + postAgreementRequest(agreementRequest.toConnectorPayload()) + .statusCode(HttpStatus.SC_BAD_REQUEST) + .contentType(JSON) + .body("field", is("description")) + .body("code", is("P2101")) + .body("description", is("Missing mandatory attribute: description")); + } + + @Test + public void shouldReturn400WhenWhenDescriptionIsEmptyString() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + CreateAgreementRequest agreementRequest = new CreateAgreementRequest(CreateAgreementRequestBuilder.builder().reference(REFERENCE).description("")); + postAgreementRequest(agreementRequest.toConnectorPayload()) + .statusCode(HttpStatus.SC_BAD_REQUEST) + .contentType(JSON) + .body("field", is("description")) + .body("code", is("P2101")) + .body("description", is("Missing mandatory attribute: description")); + } + + @Test + public void shouldReturn422WhenWhenDescriptionIsTooLong() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + CreateAgreementRequest agreementRequest = new CreateAgreementRequest(CreateAgreementRequestBuilder.builder().reference(REFERENCE).description(random(256, true, true))); + postAgreementRequest(agreementRequest.toConnectorPayload()) + .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) + .contentType(JSON) + .body("field", is("description")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: description. Must be less than or equal to 255 characters length")); + } + + @Test + public void shouldReturn201WhenWhenOptionalUserIdentifierIsNull() throws JsonProcessingException { + var agreementFixture = anAgreementFromLedgerWithoutPaymentInstrumentFixture() + .withExternalId(VALID_AGREEMENT_ID) + .withPaymentInstrument(null) + .build(); + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + + var params = aCreateAgreementRequestParams() + .withReference(REFERENCE) + .withDescription(DESCRIPTION) + .withUserIdentifier(null) + .build(); + connectorMockClient.respondCreated_whenCreateAgreement(GATEWAY_ACCOUNT_ID, params); + ledgerMockClient.respondWithAgreement(VALID_AGREEMENT_ID, agreementFixture); + postAgreementRequest(agreementPayload(params)) + .statusCode(HttpStatus.SC_CREATED) + .contentType(JSON); + } + + @Test + public void shouldReturn422WhenWhenUserIdentifierIsEmptyString() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + CreateAgreementRequest agreementRequest = new CreateAgreementRequest(CreateAgreementRequestBuilder.builder().reference(REFERENCE).description(DESCRIPTION).userIdentifier("")); + postAgreementRequest(agreementRequest.toConnectorPayload()) + .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) + .contentType(JSON) + .body("field", is("user_identifier")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: user_identifier. Must be less than or equal to 255 characters length")); + } + + @Test + public void shouldReturn422WhenWhenUserIdentifierIsTooLong() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + CreateAgreementRequest agreementRequest = new CreateAgreementRequest(CreateAgreementRequestBuilder.builder().reference(REFERENCE).description(DESCRIPTION).userIdentifier(random(256, true, true))); + postAgreementRequest(agreementRequest.toConnectorPayload()) + .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) + .contentType(JSON) + .body("field", is("user_identifier")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: user_identifier. Must be less than or equal to 255 characters length")); + } + + @Test + public void shouldReturn422WhenWhenReferenceIsTooLong() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + String tooLongReference = random(256, true, true); + CreateAgreementRequestParams createAgreementRequestParams = aCreateAgreementRequestParams() + .withReference(tooLongReference) + .withDescription(DESCRIPTION) + .build(); + + postAgreementRequest(agreementPayload(createAgreementRequestParams)) + .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) + .contentType(JSON) + .body("field", is("reference")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: reference. Must be less than or equal to 255 characters length")); + } + + @Test + public void createPayment_responseWith500_whenConnectorResponseIsAnUnrecognisedError() throws Exception { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + + CreateAgreementRequestParams createAgreementRequestParams = aCreateAgreementRequestParams() + .withReference(REFERENCE) + .withDescription(DESCRIPTION) + .build(); + connectorMockClient.respondBadRequest_whenCreateAgreement(GATEWAY_ACCOUNT_ID, "Downstream system error"); + + postAgreementRequest(agreementPayload(createAgreementRequestParams)) + .statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR) + .contentType(JSON) + .body("code",is("P2198")) + .body("description", is("Downstream system error")); + + connectorMockClient.verifyCreateAgreementConnectorRequest(GATEWAY_ACCOUNT_ID, createAgreementRequestParams); + } + + @Test + public void createAgreement_Returns401_WhenUnauthorised() { + publicAuthMockClient.respondUnauthorised(); + CreateAgreementRequestParams createAgreementRequestParams = aCreateAgreementRequestParams() + .withReference(REFERENCE) + .withDescription(DESCRIPTION) + .build(); + postAgreementRequest(agreementPayload(createAgreementRequestParams)).statusCode(401); + } + + @Test + public void createAgreement_Returns_WhenPublicAuthInaccessible() { + publicAuthMockClient.respondWithError(); + CreateAgreementRequestParams createAgreementRequestParams = aCreateAgreementRequestParams() + .withReference(REFERENCE) + .build(); + postAgreementRequest(agreementPayload(createAgreementRequestParams)).statusCode(503); + } + + protected ValidatableResponse postAgreementRequest(String payload) { + return given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .post("/v1/agreements") + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AgreementsApiResourceGetOneIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AgreementsApiResourceGetOneIT.java new file mode 100644 index 000000000..52491908e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AgreementsApiResourceGetOneIT.java @@ -0,0 +1,90 @@ +package uk.gov.pay.api.it; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.restassured.http.ContentType; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.LedgerMockClient; + +import static io.restassured.RestAssured.given; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.utils.mocks.AgreementFromLedgerFixture.AgreementFromLedgerFixtureBuilder.anAgreementFromLedgerWithoutPaymentInstrumentFixture; + +public class AgreementsApiResourceGetOneIT extends PaymentResourceITestBase { + + private final PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + private final LedgerMockClient ledgerMockClient = new LedgerMockClient(ledgerMock); + private final String agreementId = "an-agreement-id"; + + @Before + public void setUp() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void getAgreement() throws JsonProcessingException { + var fixture = anAgreementFromLedgerWithoutPaymentInstrumentFixture() + .withExternalId(agreementId) + .build(); + ledgerMockClient.respondWithAgreement(agreementId, fixture); + + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .get(AGREEMENTS_PATH + agreementId) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("agreement_id", is(agreementId)) + .body("$", not(hasKey("service_id"))) + .body("reference", is(fixture.getReference())) + .body("description", is(fixture.getDescription())) + .body("status", is(fixture.getStatus().toLowerCase())) + .body("created_date", is(fixture.getCreatedDate())) + .body("$", not(hasKey("user_identifier"))) + .body("$", not(hasKey("cancelled_date"))) + .body("$", not(hasKey("cancelled_by_user_email"))); + } + + @Test + public void getCancelledAgreement() throws JsonProcessingException { + var fixture = anAgreementFromLedgerWithoutPaymentInstrumentFixture() + .withExternalId(agreementId) + .withStatus("CANCELLED") + .withServiceId("service-id") + .withCancelledDate("2023-06-14T12:40:00.000Z") + .build(); + ledgerMockClient.respondWithAgreement(agreementId, fixture); + + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .get(AGREEMENTS_PATH + agreementId) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("agreement_id", is(agreementId)) + .body("$", not(hasKey("service_id"))) + .body("reference", is(fixture.getReference())) + .body("description", is(fixture.getDescription())) + .body("status", is(fixture.getStatus().toLowerCase())) + .body("created_date", is(fixture.getCreatedDate())) + .body("$", not(hasKey("user_identifier"))) + .body("cancelled_date", is(fixture.getCancelledDate())); + } + + @Test + public void getAgreement_MissingAgreementShouldMapException() { + ledgerMockClient.respondAgreementNotFound(agreementId); + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .get(AGREEMENTS_PATH + agreementId) + .then() + .statusCode(404) + .contentType(ContentType.JSON) + .body("code", is("P2200")) + .body("description", is("Not found")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AgreementsApiResourceSearchIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AgreementsApiResourceSearchIT.java new file mode 100644 index 000000000..ee4f3884d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AgreementsApiResourceSearchIT.java @@ -0,0 +1,172 @@ +package uk.gov.pay.api.it; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.restassured.http.ContentType; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; +import uk.gov.pay.api.utils.mocks.LedgerMockClient; + +import static io.restassured.RestAssured.given; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.utils.mocks.AgreementFromLedgerFixture.AgreementFromLedgerFixtureBuilder.anAgreementFromLedgerWithPaymentInstrumentFixture; +import static uk.gov.pay.api.utils.mocks.AgreementFromLedgerFixture.AgreementFromLedgerFixtureBuilder.anAgreementFromLedgerWithoutPaymentInstrumentFixture; + +public class AgreementsApiResourceSearchIT extends PaymentResourceITestBase { + + private final PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + private final LedgerMockClient ledgerMockClient = new LedgerMockClient(ledgerMock); + + @Before + public void setUp() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + + @Test + public void searchAgreementsFromLedger() throws JsonProcessingException { + String agreementId = "an-agreement-id"; + var agreementWithPaymentInstrument = anAgreementFromLedgerWithPaymentInstrumentFixture() + .withExternalId(agreementId) + .withUserIdentifier("a-valid-user-identifier") + .build(); + + var agreementWithoutPaymentInstrument = anAgreementFromLedgerWithoutPaymentInstrumentFixture() + .withExternalId("another-agreement-id") + .withServiceId("service-2") + .withReference("ref-2") + .withDescription("Description 2") + .withStatus("CREATED") + .withCreatedDate("2022-07-27T12:30:00Z") + .build(); + + ledgerMockClient.respondWithSearchAgreements(GATEWAY_ACCOUNT_ID, "created", agreementWithPaymentInstrument, agreementWithoutPaymentInstrument); + + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .basePath(AGREEMENTS_PATH) + .queryParam("status", "created") + .queryParam("page", "3") + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("total", is(9)) + .body("count", is(2)) + .body("page", is(3)) + .body("_links.self.href", containsString("/v1/agreements?page=3")) + .body("_links.first_page.href", containsString("/v1/agreements?page=1")) + .body("_links.last_page.href", containsString("/v1/agreements?page=5")) + .body("_links.prev_page.href", containsString("/v1/agreements?page=2")) + .body("_links.next_page.href", containsString("/v1/agreements?page=4")) + .body("results.size()", is(2)) + .body("results[0].agreement_id", is(agreementWithPaymentInstrument.getExternalId())) + .body("results[0].reference", is(agreementWithPaymentInstrument.getReference())) + .body("results[0].description", is(agreementWithPaymentInstrument.getDescription())) + .body("results[0].user_identifier", is(agreementWithPaymentInstrument.getUserIdentifier())) + .body("results[0].status", is(agreementWithPaymentInstrument.getStatus().toLowerCase())) + .body("results[0].created_date", is(agreementWithPaymentInstrument.getCreatedDate())) + .body("results[0].payment_instrument.type", is(agreementWithPaymentInstrument.getPaymentInstrument().getType())) + .body("results[0].payment_instrument.card_details.card_type", + is(agreementWithPaymentInstrument.getPaymentInstrument().getCardDetails().getCardType())) + .body("results[0].payment_instrument.card_details.card_brand", + is(agreementWithPaymentInstrument.getPaymentInstrument().getCardDetails().getCardBrand())) + .body("results[0].payment_instrument.card_details.cardholder_name", + is(agreementWithPaymentInstrument.getPaymentInstrument().getCardDetails().getCardHolderName())) + .body("results[0].payment_instrument.card_details.billing_address.line1", + is(agreementWithPaymentInstrument.getPaymentInstrument().getCardDetails().getBillingAddress().get().getLine1())) + .body("results[0].payment_instrument.card_details.billing_address.line2", + is(agreementWithPaymentInstrument.getPaymentInstrument().getCardDetails().getBillingAddress().get().getLine2())) + .body("results[0].payment_instrument.card_details.billing_address.city", + is(agreementWithPaymentInstrument.getPaymentInstrument().getCardDetails().getBillingAddress().get().getCity())) + .body("results[0].payment_instrument.card_details.billing_address.postcode", + is(agreementWithPaymentInstrument.getPaymentInstrument().getCardDetails().getBillingAddress().get().getPostcode())) + .body("results[0].payment_instrument.card_details.billing_address.country", + is(agreementWithPaymentInstrument.getPaymentInstrument().getCardDetails().getBillingAddress().get().getCountry())) + .body("results[0].payment_instrument.card_details.expiry_date", + is(agreementWithPaymentInstrument.getPaymentInstrument().getCardDetails().getExpiryDate())) + .body("results[0].payment_instrument.card_details.first_digits_card_number", + is(agreementWithPaymentInstrument.getPaymentInstrument().getCardDetails().getFirstDigitsCardNumber())) + .body("results[0].payment_instrument.card_details.last_digits_card_number", + is(agreementWithPaymentInstrument.getPaymentInstrument().getCardDetails().getLastDigitsCardNumber())) + .body("results[0].payment_instrument.created_date", is(agreementWithPaymentInstrument.getPaymentInstrument().getCreatedDate())) + .body("results[1].agreement_id", is(agreementWithoutPaymentInstrument.getExternalId())) + .body("results[1].reference", is(agreementWithoutPaymentInstrument.getReference())) + .body("results[1].description", is(agreementWithoutPaymentInstrument.getDescription())) + .body("results[1].status", is(agreementWithoutPaymentInstrument.getStatus().toLowerCase())) + .body("results[1].created_date", is(agreementWithoutPaymentInstrument.getCreatedDate())) + .body("results[1]", not(hasKey("payment_instrument"))); + } + + @Test + public void searchCancelledAgreementsFromLedger() throws JsonProcessingException { + String agreementId = "an-agreement-id"; + var agreementWithoutPaymentInstrument = anAgreementFromLedgerWithoutPaymentInstrumentFixture() + .withExternalId("another-agreement-id") + .withServiceId("service-2") + .withReference("ref-2") + .withDescription("Description 2") + .withStatus("CANCELLED") + .withCreatedDate("2022-07-27T12:30:00Z") + .withCancelledDate("2023-06-14T16:52:00Z") + .build(); + + ledgerMockClient.respondWithSearchAgreements(GATEWAY_ACCOUNT_ID, "cancelled", agreementWithoutPaymentInstrument); + + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .basePath(AGREEMENTS_PATH) + .queryParam("status", "cancelled") + .queryParam("page", "3") + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("total", is(9)) + .body("count", is(2)) + .body("page", is(3)) + .body("_links.self.href", containsString("/v1/agreements?page=3")) + .body("_links.first_page.href", containsString("/v1/agreements?page=1")) + .body("_links.last_page.href", containsString("/v1/agreements?page=5")) + .body("_links.prev_page.href", containsString("/v1/agreements?page=2")) + .body("_links.next_page.href", containsString("/v1/agreements?page=4")) + .body("results.size()", is(1)) + .body("results[0].agreement_id", is(agreementWithoutPaymentInstrument.getExternalId())) + .body("results[0].reference", is(agreementWithoutPaymentInstrument.getReference())) + .body("results[0].description", is(agreementWithoutPaymentInstrument.getDescription())) + .body("results[0].status", is(agreementWithoutPaymentInstrument.getStatus().toLowerCase())) + .body("results[0].created_date", is(agreementWithoutPaymentInstrument.getCreatedDate())) + .body("results[0]", not(hasKey("payment_instrument"))); + } + + @Test + public void searchAgreementsReturnsErrorWhenValidationError() { + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .basePath(AGREEMENTS_PATH) + .queryParam("status", "ethereal") + .get() + .then() + .statusCode(422) + .contentType(ContentType.JSON) + .body("code", is("P2401")) + .body("description", is("Invalid parameters: status. See Public API documentation for the correct data formats")); + } + + @Test + public void searchPaymentsAgreements_errorIfLedgerRespondsWith404() { + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .basePath(AGREEMENTS_PATH) + .get() + .then() + .statusCode(404) + .body("code", is("P2402")) + .body("description", is("Page not found")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AuthorisationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AuthorisationIT.java new file mode 100644 index 000000000..4353c930a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/AuthorisationIT.java @@ -0,0 +1,40 @@ +package uk.gov.pay.api.it; + +import org.apache.http.HttpStatus; +import org.junit.Test; +import uk.gov.pay.api.utils.PublicAuthMockClient; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static uk.gov.pay.api.utils.Payloads.aSuccessfulPaymentPayload; + +public class AuthorisationIT extends PaymentResourceITestBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Test + public void shouldRefuseAuthorisationIfTokenIsNotPresent() { + given().port(app.getLocalPort()) + .body(aSuccessfulPaymentPayload()) + .accept(JSON) + .contentType(JSON) + .post("/v1/payments/") + .then() + .statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + @Test + public void shouldReturn500IfPublicAuthReturnsInvalidTokenPaymentType() { + publicAuthMockClient.respondWithInvalidTokenType(API_KEY, "1"); + + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + API_KEY) + .body(aSuccessfulPaymentPayload()) + .accept(JSON) + .contentType(JSON) + .post("/v1/payments/") + .then() + .statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/CachingAuthenticatorIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/CachingAuthenticatorIT.java new file mode 100644 index 000000000..705d72831 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/CachingAuthenticatorIT.java @@ -0,0 +1,114 @@ +package uk.gov.pay.api.it; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.dropwizard.testing.junit.DropwizardAppRule; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import uk.gov.pay.api.app.PublicApi; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.utils.ApiKeyGenerator; +import uk.gov.pay.api.utils.JsonStringBuilder; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static io.dropwizard.testing.ConfigOverride.config; +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static java.lang.String.format; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static uk.gov.pay.api.model.TokenPaymentType.CARD; +import static uk.gov.pay.api.utils.WiremockStubbing.stubPublicAuthV1ApiAuth; +import static uk.gov.service.payments.commons.testing.port.PortFactory.findFreePort; + +public class CachingAuthenticatorIT { + + private String accountId = "123"; + private String bearerToken = ApiKeyGenerator.apiKeyValueOf("TEST_BEARER_TOKEN", "qwer9yuhgf"); + + private int publicAuthRulePort = findFreePort(); + private int connectorRulePort = findFreePort(); + + @Rule + public WireMockRule publicAuthRule = new WireMockRule(publicAuthRulePort); + + @Rule + public WireMockRule connectorRule = new WireMockRule(connectorRulePort); + + @Rule + public DropwizardAppRule app = new DropwizardAppRule<>( + PublicApi.class, + resourceFilePath("config/test-config.yaml"), + config("publicAuthUrl", "http://localhost:" + publicAuthRulePort + "/v1/api/auth"), + config("connectorUrl", "http://localhost:" + connectorRulePort)); + + @Before + public void setup() throws Exception { + String tokenLink = "some-token-link"; + stubPublicAuthV1ApiAuth(publicAuthRule, new Account(accountId, CARD, tokenLink), bearerToken); + setUpMockForConnector(); + } + + @After + public void cleanup() { + publicAuthRule.resetRequests(); + } + + @Test + public void testAuthenticationRequestsAreCached() throws Exception { + makeRequest(); + Thread.sleep(1000); //pause for 1 second as there's a rate limit of 1 request per second + makeRequest(); + + publicAuthRule.verify(1, getRequestedFor(urlEqualTo("/v1/api/auth"))); + } + + @Test + public void testAuthenticationCacheExpires() throws Exception { + makeRequest(); + Thread.sleep(3000); //expireAfterWrite is set to 3seconds in test-config.yaml + makeRequest(); + + publicAuthRule.verify(2, getRequestedFor(urlEqualTo("/v1/api/auth"))); + } + + private void makeRequest() { + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + bearerToken) + .get("/v1/payments/paymentId") + .then() + .statusCode(200) + .contentType(JSON); + } + + private void setUpMockForConnector() { + connectorRule.stubFor(get(urlEqualTo(format("/v1/api/accounts/%s/charges/paymentId", accountId))) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", APPLICATION_JSON) + .withBody(aPayment()))); + } + + private String aPayment() { + JsonStringBuilder jsonStringBuilder = new JsonStringBuilder() + .add("charge_id", "chargeId") + .add("amount", 100) + .add("language", "en") + .add("reference", "ref 12") + .add("state", new PaymentState("created", false, null, null)) + .add("email", "test@example.com") + .add("description", "description") + .add("return_url", "http://example.com") + .add("payment_provider", "sandbox") + .add("card_brand", "VISA") + .add("created_date", "2018-07-25T13:12:00"); + return jsonStringBuilder.build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/HealthCheckResourceIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/HealthCheckResourceIT.java new file mode 100644 index 000000000..1f2146509 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/HealthCheckResourceIT.java @@ -0,0 +1,20 @@ +package uk.gov.pay.api.it; + +import io.restassured.RestAssured; +import org.junit.Test; + +import static org.hamcrest.Matchers.is; +import static uk.gov.pay.api.resources.HealthCheckResource.HEALTHCHECK; + +public class HealthCheckResourceIT extends PaymentResourceITestBase { + + @Test + public void getAccountShouldReturn404IfAccountIdIsUnknown() { + RestAssured.given().port(app.getLocalPort()) + .get(HEALTHCHECK) + .then() + .statusCode(200) + .body("ping.healthy", is(true)) + .body("deadlocks.healthy", is(true)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentRefundsResourceIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentRefundsResourceIT.java new file mode 100644 index 000000000..c33920bbb --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentRefundsResourceIT.java @@ -0,0 +1,387 @@ +package uk.gov.pay.api.it; + +import com.google.gson.GsonBuilder; +import io.restassured.response.Response; +import io.restassured.response.ValidatableResponse; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.it.fixtures.PaymentRefundJsonFixture; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.RefundSummary; +import uk.gov.pay.api.model.ledger.TransactionState; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; +import uk.gov.pay.api.utils.mocks.LedgerMockClient; +import uk.gov.pay.api.utils.mocks.RefundTransactionFromLedgerFixture; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.validation.DateTimeUtils; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static java.lang.String.format; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static javax.ws.rs.core.Response.Status.ACCEPTED; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.utils.Urls.paymentLocationFor; +import static uk.gov.pay.api.utils.mocks.ChargeResponseFromConnector.ChargeResponseFromConnectorBuilder.aCreateOrGetChargeResponseFromConnector; +import static uk.gov.pay.api.utils.mocks.RefundTransactionFromLedgerFixture.RefundTransactionFromLedgerBuilder.aRefundTransactionFromLedgerFixture; +import static uk.gov.service.payments.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION; + +public class PaymentRefundsResourceIT extends PaymentResourceITestBase { + + private static final int AMOUNT = 1000; + private static final int REFUND_AMOUNT_AVAILABLE = 9000; + private static final String CHARGE_ID = "ch_ab2341da231434l"; + private static final String REFUND_ID = "111999"; + private static final ZonedDateTime TIMESTAMP = DateTimeUtils.toUTCZonedDateTime("2016-01-01T12:00:00Z").get(); + private static final String CREATED_DATE = ISO_INSTANT_MILLISECOND_PRECISION.format(TIMESTAMP); + private static final Address BILLING_ADDRESS = new Address("line1", "line2", "NR2 5 6EG", "city", "UK"); + private static final CardDetailsFromResponse CARD_DETAILS = new CardDetailsFromResponse("1234", "123456", "Mr. Payment", "12/19", BILLING_ADDRESS, "Visa", null); + + private ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + private LedgerMockClient ledgerMockClient = new LedgerMockClient(ledgerMock); + + @Before + public void setUp() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void getRefundByIdThroughConnector_shouldGetValidResponse() { + connectorMockClient.respondWithGetRefundById(GATEWAY_ACCOUNT_ID, CHARGE_ID, REFUND_ID, AMOUNT, REFUND_AMOUNT_AVAILABLE, "available", CREATED_DATE); + + assertSingleRefund(getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID)) + .body("$", hasKey("settlement_summary")); + } + + @Test + public void getRefundByIdThroughLedger_shouldGetValidResponse() { + ledgerMockClient.respondWithRefund(REFUND_ID, aRefundTransactionFromLedgerFixture() + .withAmount((long) AMOUNT) + .withState(new TransactionState("available", false)) + .withParentTransactionId(CHARGE_ID) + .withTransactionId(REFUND_ID) + .withCreatedDate(CREATED_DATE) + .build()); + + assertSingleRefund(getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID, "ledger-only")) + .body("$", hasKey("settlement_summary")); + } + + @Test + public void getRefundByIdThroughLedger_shouldGetSettlementSummary() { + ledgerMockClient.respondWithRefund(REFUND_ID, aRefundTransactionFromLedgerFixture() + .withAmount((long) AMOUNT) + .withState(new TransactionState("available", false)) + .withParentTransactionId(CHARGE_ID) + .withTransactionId(REFUND_ID) + .withCreatedDate(CREATED_DATE) + .withSettlementSummary(ISO_INSTANT_MILLISECOND_PRECISION.format(TIMESTAMP)) + .build()); + + ValidatableResponse response = getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID, "ledger-only"); + response.body("settlement_summary.settled_date", is(ISO_INSTANT_MILLISECOND_PRECISION.format(TIMESTAMP))); + } + + private ValidatableResponse assertSingleRefund(ValidatableResponse paymentRefundByIdResponse) { + return paymentRefundByIdResponse + .statusCode(200) + .contentType(JSON) + .body("refund_id", is(REFUND_ID)) + .body("amount", is(AMOUNT)) + .body("status", is("available")) + .body("created_date", is(CREATED_DATE)) + .body("_links.self.href", is(paymentRefundLocationFor(CHARGE_ID, REFUND_ID))) + .body("_links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))) + .body("settlement_summary", not(hasKey("settled_date"))); + } + + @Test + public void getRefundById_shouldGetNonAuthorized_whenPublicAuthRespondsUnauthorised() { + publicAuthMockClient.respondUnauthorised(); + + getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID) + .statusCode(401); + } + + @Test + public void getRefundByIdThroughConnector_shouldReturnNotFound_whenRefundDoesNotExist() { + connectorMockClient.respondRefundNotFound(GATEWAY_ACCOUNT_ID, CHARGE_ID, "unknown-refund-id"); + + getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID) + .statusCode(404) + .contentType(JSON) + .body("code", is("P0700")) + .body("description", is("Not found")); + } + + @Test + public void getRefundByIdThroughLedger_shouldReturnNotFound_whenRefundDoesNotExist() { + ledgerMockClient.respondRefundNotFound("unknown-refund-id"); + + getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID, "ledger-only") + .statusCode(404) + .contentType(JSON) + .body("code", is("P0700")) + .body("description", is("Not found")); + } + + @Test + public void getRefundById_returns500_whenConnectorRespondsWithResponseOtherThan200Or404() { + connectorMockClient.respondRefundWithError(GATEWAY_ACCOUNT_ID, CHARGE_ID, REFUND_ID); + + getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID, CONNECTOR_ONLY_STRATEGY) + .statusCode(500) + .contentType(JSON) + .body("code", is("P0798")) + .body("description", is("Downstream system error")); + } + + @Test + public void getRefundById_returns500_whenLedgerRespondsWithResponseOtherThan200Or404() { + ledgerMockClient.respondRefundWithError(REFUND_ID); + + getPaymentRefundByIdResponse(CHARGE_ID, REFUND_ID, "ledger-only") + .statusCode(500) + .contentType(JSON) + .body("code", is("P0798")) + .body("description", is("Downstream system error")); + } + + @Test + public void getRefundsThroughConnector_shouldGetValidResponse() { + PaymentRefundJsonFixture refund1 = new PaymentRefundJsonFixture(100L, CREATED_DATE, "100", "available", new ArrayList<>()); + PaymentRefundJsonFixture refund2 = new PaymentRefundJsonFixture(300L, CREATED_DATE, "300", "pending", new ArrayList<>()); + + connectorMockClient.respondWithGetAllRefunds(GATEWAY_ACCOUNT_ID, CHARGE_ID, refund1, refund2); + + assertRefundsResponse(getPaymentRefundsResponse(CHARGE_ID, CONNECTOR_ONLY_STRATEGY)); + } + + @Test + public void getRefundsThroughLedger_shouldGetValidResponse() { + RefundTransactionFromLedgerFixture refund1 = aRefundTransactionFromLedgerFixture() + .withAmount(100L) + .withCreatedDate(CREATED_DATE) + .withTransactionId("100") + .withState(new TransactionState("available", false)) + .build(); + + RefundTransactionFromLedgerFixture refund2 = aRefundTransactionFromLedgerFixture() + .withAmount(300L) + .withCreatedDate(CREATED_DATE) + .withTransactionId("300") + .withState(new TransactionState("pending", false)) + .build(); + + ledgerMockClient.respondWithGetAllRefunds(CHARGE_ID, refund1, refund2); + + assertRefundsResponse(getPaymentRefundsResponse(CHARGE_ID, "ledger-only")); + } + + @Test + public void getRefundsThroughLedger_shouldGetSettlementSummary() { + RefundTransactionFromLedgerFixture refund1 = aRefundTransactionFromLedgerFixture() + .withAmount(100L) + .withCreatedDate(CREATED_DATE) + .withTransactionId("100") + .withState(new TransactionState("available", false)) + .withSettlementSummary(CREATED_DATE) + .build(); + + RefundTransactionFromLedgerFixture refund2 = aRefundTransactionFromLedgerFixture() + .withAmount(300L) + .withCreatedDate(CREATED_DATE) + .withTransactionId("300") + .withState(new TransactionState("pending", false)) + .build(); + + ledgerMockClient.respondWithGetAllRefunds(CHARGE_ID, refund1, refund2); + + getPaymentRefundsResponse(CHARGE_ID, "ledger-only") + .statusCode(200) + .contentType(JSON) + .body("_embedded.refunds[0].settlement_summary.settled_date", is(CREATED_DATE)) + .body("_embedded.refunds[1].settlement_summary", not(hasKey("settled_date"))); + + + } + + private void assertRefundsResponse(ValidatableResponse paymentRefundsResponse) { + paymentRefundsResponse + .statusCode(200) + .contentType(JSON) + .body("payment_id", is(CHARGE_ID)) + .body("_links.self.href", is(paymentRefundsLocationFor(CHARGE_ID))) + .body("_links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))) + .body("_embedded.refunds.size()", is(2)) + .body("_embedded.refunds[0].refund_id", is("100")) + .body("_embedded.refunds[0].created_date", is(CREATED_DATE)) + .body("_embedded.refunds[0].amount", is(100)) + .body("_embedded.refunds[0].status", is("available")) + .body("_embedded.refunds[0]._links.size()", is(2)) + .body("_embedded.refunds[0]._links.self.href", is(paymentRefundLocationFor(CHARGE_ID, "100"))) + .body("_embedded.refunds[0]._links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))) + .body("_embedded.refunds[1].refund_id", is("300")) + .body("_embedded.refunds[1].created_date", is(CREATED_DATE)) + .body("_embedded.refunds[1].amount", is(300)) + .body("_embedded.refunds[1].status", is("pending")) + .body("_embedded.refunds[1]._links.size()", is(2)) + .body("_embedded.refunds[1]._links.self.href", is(paymentRefundLocationFor(CHARGE_ID, "300"))) + .body("_embedded.refunds[1]._links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))); + } + + @Test + public void getRefunds_shouldGetValidResponse_whenListReturnedIsEmpty() { + connectorMockClient.respondWithGetAllRefunds(GATEWAY_ACCOUNT_ID, CHARGE_ID); + + assertEmptyRefundsResponse(getPaymentRefundsResponse(CHARGE_ID, CONNECTOR_ONLY_STRATEGY)); + } + + @Test + public void getRefundsThroughLedger_shouldGetValidResponse_whenListReturnedIsEmpty() { + ledgerMockClient.respondWithGetAllRefunds(CHARGE_ID); + + assertEmptyRefundsResponse(getPaymentRefundsResponse(CHARGE_ID, "ledger-only")); + } + + private void assertEmptyRefundsResponse(ValidatableResponse paymentRefundsResponse) { + paymentRefundsResponse + .statusCode(200) + .contentType(JSON) + .body("payment_id", is(CHARGE_ID)) + .body("_links.self.href", is(paymentRefundsLocationFor(CHARGE_ID))) + .body("_links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))) + .body("_embedded.refunds.size()", is(0)); + } + + @Test + public void getRefunds_shouldGetNonAuthorized_whenPublicAuthRespondsUnauthorised() { + publicAuthMockClient.respondUnauthorised(); + + getPaymentRefundsResponse(CHARGE_ID) + .statusCode(401); + } + + @Test + public void createRefund_shouldGetAcceptedResponse() { + String payload = new GsonBuilder().create().toJson(Map.of("amount", AMOUNT, "refund_amount_available", REFUND_AMOUNT_AVAILABLE)); + postRefundRequest(payload); + } + + @Test + public void createRefundWithNoRefundAmountAvailable_shouldGetAcceptedResponse() { + String payload = new GsonBuilder().create().toJson(Map.of("amount", AMOUNT)); + + connectorMockClient.respondWithChargeFound(null, GATEWAY_ACCOUNT_ID, + aCreateOrGetChargeResponseFromConnector() + .withAmount(AMOUNT) + .withChargeId(CHARGE_ID) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(false) + .withRefundSummary(new RefundSummary("available", 9000, 1000)) + .withCardDetails(CARD_DETAILS) + .withGatewayTransactionId("gatewayTransactionId") + .build()); + + postRefundRequest(payload); + } + + @Test + public void createRefundWhenChargeNotFound_shouldReturn404() { + String payload = new GsonBuilder().create().toJson(Map.of("amount", AMOUNT)); + + connectorMockClient.respondChargeNotFound(CHARGE_ID, GATEWAY_ACCOUNT_ID, "Not found"); + + postRefunds(payload) + .then() + .statusCode(NOT_FOUND.getStatusCode()); + } + + @Test + public void createRefundWhenRefundAmountAvailableMismatch_shouldReturn412Response() { + String payload = new GsonBuilder().create().toJson( + Map.of("amount", AMOUNT, "refund_amount_available", REFUND_AMOUNT_AVAILABLE)); + String errorMessage = new GsonBuilder().create().toJson( + Map.of("code", "P0604", "description", "Refund amount available mismatch.")); + connectorMockClient.respondPreconditionFailed_whenCreateRefund(GATEWAY_ACCOUNT_ID, errorMessage, CHARGE_ID); + + postRefunds(payload) + .then() + .statusCode(PRECONDITION_FAILED.getStatusCode()) + .contentType(JSON) + .body("code", is("P0604")) + .body("description", is("Refund amount available mismatch.")); + } + + @Test + public void createRefund_shouldGetNonAuthorized_whenPublicAuthRespondsUnauthorised() { + publicAuthMockClient.respondUnauthorised(); + + postRefunds("{\"amount\": 1000}") + .then() + .statusCode(401); + } + + private void postRefundRequest(String payload) { + String refundStatus = "available"; + connectorMockClient.respondAccepted_whenCreateARefund(AMOUNT, REFUND_AMOUNT_AVAILABLE, GATEWAY_ACCOUNT_ID, CHARGE_ID, REFUND_ID, refundStatus, CREATED_DATE); + + postRefunds(payload) + .then() + .statusCode(ACCEPTED.getStatusCode()) + .contentType(JSON) + .body("refund_id", is(REFUND_ID)) + .body("amount", is(AMOUNT)) + .body("status", is(refundStatus)) + .body("created_date", is(CREATED_DATE)) + .body("_links.self.href", is(paymentRefundLocationFor(CHARGE_ID, REFUND_ID))) + .body("_links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))); + } + + private Response postRefunds(String payload) { + return given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + API_KEY) + .header(CONTENT_TYPE, APPLICATION_JSON) + .body(payload) + .post(format("/v1/payments/%s/refunds", CHARGE_ID)); + } + + private ValidatableResponse getPaymentRefundByIdResponse(String paymentId, String refundId) { + String defaultConnectorStrategy = ""; + return getPaymentRefundByIdResponse(paymentId, refundId, defaultConnectorStrategy); + } + + private ValidatableResponse getPaymentRefundByIdResponse(String paymentId, String refundId, String strategy) { + return given().port(app.getLocalPort()) + .header("X-Ledger", strategy) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .get(format("/v1/payments/%s/refunds/%s", paymentId, refundId)) + .then(); + } + + private ValidatableResponse getPaymentRefundsResponse(String paymentId) { + return getPaymentRefundsResponse(paymentId, "default"); + } + + private ValidatableResponse getPaymentRefundsResponse(String paymentId, String strategy) { + return given().port(app.getLocalPort()) + .header("X-Ledger", strategy) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .get(format("/v1/payments/%s/refunds", paymentId)) + .then(); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentResourceITestBase.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentResourceITestBase.java new file mode 100644 index 000000000..b36203373 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentResourceITestBase.java @@ -0,0 +1,106 @@ +package uk.gov.pay.api.it; + +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.dropwizard.testing.junit.DropwizardAppRule; +import io.restassured.response.ValidatableResponse; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import uk.gov.pay.api.app.PublicApi; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.it.rule.RedisDockerRule; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.utils.ApiKeyGenerator; + +import java.util.Map; + +import static io.dropwizard.testing.ConfigOverride.config; +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static uk.gov.pay.api.utils.Urls.paymentLocationFor; +import static uk.gov.service.payments.commons.testing.port.PortFactory.findFreePort; + +public abstract class PaymentResourceITestBase { + //Must use same secret set in test-config.xml's apiKeyHmacSecret + protected static final String API_KEY = ApiKeyGenerator.apiKeyValueOf("TEST_BEARER_TOKEN", "qwer9yuhgf"); + protected static final String GATEWAY_ACCOUNT_ID = "GATEWAY_ACCOUNT_ID"; + protected static final String PAYMENTS_PATH = "/v1/payments/"; + protected static final String AGREEMENTS_PATH = "/v1/agreements/"; + protected static final String LEDGER_ONLY_STRATEGY = "ledger-only"; + protected static final String CONNECTOR_ONLY_STRATEGY = "connector-only"; + + @ClassRule + public static RedisDockerRule redisDockerRule = new RedisDockerRule(); + + private static final int CONNECTOR_PORT = findFreePort(); + private static final int PUBLIC_AUTH_PORT = findFreePort(); + private static final int LEDGER_PORT = findFreePort(); + private static final Gson GSON = new GsonBuilder().create(); + + @ClassRule + public static WireMockClassRule connectorMock = new WireMockClassRule(CONNECTOR_PORT); + + @ClassRule + public static WireMockClassRule publicAuthMock = new WireMockClassRule(PUBLIC_AUTH_PORT); + + @ClassRule + public static WireMockClassRule ledgerMock = new WireMockClassRule(LEDGER_PORT); + + @Rule + public DropwizardAppRule app = new DropwizardAppRule<>( + PublicApi.class, + resourceFilePath("config/test-config.yaml"), + config("connectorUrl", "http://localhost:" + CONNECTOR_PORT), + config("publicAuthUrl", "http://localhost:" + PUBLIC_AUTH_PORT + "/v1/auth"), + config("ledgerUrl", "http://localhost:" + LEDGER_PORT), + config("redis.endpoint", redisDockerRule.getRedisUrl()) + ); + + PublicApiConfig configuration; + + @Before + public void setup() { + configuration = app.getConfiguration(); + connectorMock.resetAll(); + publicAuthMock.resetAll(); + ledgerMock.resetAll(); + } + + String frontendUrlFor(TokenPaymentType paymentType) { + return "http://frontend_" + paymentType.toString().toLowerCase() + "/charge/"; + } + + String paymentEventsLocationFor(String chargeId) { + return paymentLocationFor(configuration.getBaseUrl(), chargeId) + "/events"; + } + + String paymentRefundsLocationFor(String chargeId) { + return paymentLocationFor(configuration.getBaseUrl(), chargeId) + "/refunds"; + } + + String paymentRefundLocationFor(String chargeId, String refundId) { + return "http://publicapi.url" + PAYMENTS_PATH + chargeId + "/refunds/" + refundId; + } + + String paymentCancelLocationFor(String chargeId) { + return paymentLocationFor(configuration.getBaseUrl(), chargeId) + "/cancel"; + } + + protected ValidatableResponse postPaymentResponse(String payload) { + return given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + API_KEY) + .post(PAYMENTS_PATH) + .then(); + } + + protected static String toJson(Map map) { + return GSON.toJson(map); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCancelIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCancelIT.java new file mode 100644 index 000000000..0da7da282 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCancelIT.java @@ -0,0 +1,96 @@ +package uk.gov.pay.api.it; + +import com.jayway.jsonassert.JsonAssert; +import io.restassured.response.ValidatableResponse; +import org.junit.Test; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; + +import java.io.IOException; +import java.io.InputStream; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.eclipse.jetty.http.HttpStatus.CONFLICT_409; +import static org.eclipse.jetty.http.HttpStatus.LENGTH_REQUIRED_411; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.core.Is.is; + +public class PaymentsResourceCancelIT extends PaymentResourceITestBase { + + private static final String TEST_CHARGE_ID = "ch_ab2341da231434"; + private static final String CANCEL_PAYMENTS_PATH = PAYMENTS_PATH + "%s/cancel"; + + private ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Test + public void cancelPayment_Returns401_WhenUnauthorised() { + publicAuthMockClient.respondUnauthorised(); + postCancelPaymentResponse(TEST_CHARGE_ID).statusCode(401); + } + + @Test + public void successful_whenConnector_AllowsCancellation() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + connectorMockClient.respondOk_whenCancelCharge(TEST_CHARGE_ID, GATEWAY_ACCOUNT_ID); + postCancelPaymentResponse(TEST_CHARGE_ID).statusCode(204); + connectorMockClient.verifyCancelCharge(TEST_CHARGE_ID, GATEWAY_ACCOUNT_ID); + } + + @Test + public void cancelPayment_returns404_whenPaymentNotFound() throws IOException { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + connectorMockClient.respondChargeNotFound_WhenCancelCharge(TEST_CHARGE_ID, GATEWAY_ACCOUNT_ID, "some backend error message"); + + InputStream body = postCancelPaymentResponse(TEST_CHARGE_ID) + .statusCode(404) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0500")) + .assertThat("$.description", is("Not found")); + } + + @Test + public void cancelPayment_returns409_whenConnectorReturns409() throws IOException { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + connectorMockClient.respond_WhenCancelCharge(TEST_CHARGE_ID, GATEWAY_ACCOUNT_ID, "some backend error message", CONFLICT_409); + + InputStream body = postCancelPaymentResponse(TEST_CHARGE_ID) + .statusCode(409) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0502")) + .assertThat("$.description", is("Cancellation of payment failed")); + } + + @Test + public void cancelPayment_returns500_whenConnectorResponseIsUnexpected() throws IOException { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + connectorMockClient.respond_WhenCancelCharge(TEST_CHARGE_ID, GATEWAY_ACCOUNT_ID, "some backend error message", LENGTH_REQUIRED_411); + + InputStream body = postCancelPaymentResponse(TEST_CHARGE_ID) + .statusCode(500) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0598")) + .assertThat("$.description", is("Downstream system error")); + } + + private ValidatableResponse postCancelPaymentResponse(String paymentId) { + return given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + API_KEY) + .post(String.format(CANCEL_PAYMENTS_PATH, paymentId)) + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCancelPactConsumerIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCancelPactConsumerIT.java new file mode 100644 index 000000000..d04514fa9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCancelPactConsumerIT.java @@ -0,0 +1,62 @@ +package uk.gov.pay.api.it; + +import au.com.dius.pact.consumer.PactVerification; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.dropwizard.testing.junit.DropwizardAppRule; +import org.apache.http.HttpStatus; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import uk.gov.pay.api.app.PublicApi; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.utils.ApiKeyGenerator; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import static io.dropwizard.testing.ConfigOverride.config; +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static io.restassured.RestAssured.given; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.Matchers.is; +import static uk.gov.pay.api.model.TokenPaymentType.CARD; +import static uk.gov.pay.api.utils.WiremockStubbing.stubPublicAuthV1ApiAuth; +import static uk.gov.service.payments.commons.testing.port.PortFactory.findFreePort; + +public class PaymentsResourceCancelPactConsumerIT { + + private static final String API_KEY = ApiKeyGenerator.apiKeyValueOf("TEST_BEARER_TOKEN", "qwer9yuhgf"); + + private final int publicAuthPort = findFreePort(); + + @Rule + public WireMockRule publicAuth = new WireMockRule(publicAuthPort); + + @Rule + public PactProviderRule connector = new PactProviderRule("connector", this); + + @Rule + public DropwizardAppRule app = new DropwizardAppRule<>( + PublicApi.class, + resourceFilePath("config/test-config.yaml"), + config("connectorUrl", "http://localhost:" + connector.getConfig().getPort()), + config("publicAuthUrl", "http://localhost:" + publicAuthPort + "/v1/api/auth")); + + @Before + public void setup() throws Exception { + stubPublicAuthV1ApiAuth(publicAuth, new Account("123456", CARD, "a-token-link"), API_KEY); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-cancel-already-canceled-payment"}) + public void cancelAPaymentThatIsAlreadyCanceled() { + given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + API_KEY) + .post(String.format("/v1/payments/%s/cancel", "charge8133029783750964630")) + .then() + .statusCode(HttpStatus.SC_BAD_REQUEST) + .body("code", is("P0501")) + .body("description", is("Cancellation of payment failed")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCaptureIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCaptureIT.java new file mode 100644 index 000000000..ff7ec5e65 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCaptureIT.java @@ -0,0 +1,113 @@ +package uk.gov.pay.api.it; + +import com.jayway.jsonassert.JsonAssert; +import io.restassured.response.ValidatableResponse; +import org.junit.Test; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; + +import java.io.IOException; +import java.io.InputStream; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.eclipse.jetty.http.HttpStatus.CONFLICT_409; +import static org.eclipse.jetty.http.HttpStatus.LENGTH_REQUIRED_411; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.core.Is.is; + +public class PaymentsResourceCaptureIT extends PaymentResourceITestBase { + + private static final String TEST_CHARGE_ID = "ch_e36c168c41a0"; + private static final String CAPTURE_PAYMENTS_PATH = PAYMENTS_PATH + "%s/capture"; + + private ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Test + public void capturePayment_Returns401_WhenUnauthorised() { + publicAuthMockClient.respondUnauthorised(); + postCapturePaymentResponse(TEST_CHARGE_ID).statusCode(401); + } + + @Test + public void successful_whenConnector_AllowsCapture() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + connectorMockClient.respondOk_whenCaptureCharge(TEST_CHARGE_ID, GATEWAY_ACCOUNT_ID); + postCapturePaymentResponse(TEST_CHARGE_ID).statusCode(204); + connectorMockClient.verifyCaptureCharge(TEST_CHARGE_ID, GATEWAY_ACCOUNT_ID); + } + + @Test + public void capturePayment_returns400_whenConnectorRespondsWithA400() throws IOException { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + connectorMockClient.respondBadRequest_WhenCaptureCharge(TEST_CHARGE_ID, GATEWAY_ACCOUNT_ID, "Invalid account Id"); + + InputStream body = postCapturePaymentResponse(TEST_CHARGE_ID) + .statusCode(400) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P1001")) + .assertThat("$.description", is("Capture of payment failed")); + } + + @Test + public void capturePayment_returns404_whenPaymentNotFound() throws IOException { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + connectorMockClient.respondChargeNotFound_WhenCaptureCharge(TEST_CHARGE_ID, GATEWAY_ACCOUNT_ID, "some backend error message"); + + InputStream body = postCapturePaymentResponse(TEST_CHARGE_ID) + .statusCode(404) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P1000")) + .assertThat("$.description", is("Not found")); + } + + @Test + public void capturePayment_returns409_whenConnectorReturns409() throws IOException { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + connectorMockClient.respond_WhenCaptureCharge(TEST_CHARGE_ID, GATEWAY_ACCOUNT_ID, "some backend error message", CONFLICT_409); + + InputStream body = postCapturePaymentResponse(TEST_CHARGE_ID) + .statusCode(409) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P1003")) + .assertThat("$.description", is("Payment cannot be captured")); + } + + @Test + public void capturePayment_returns500_whenConnectorResponseIsUnexpected() throws IOException { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + connectorMockClient.respond_WhenCaptureCharge(TEST_CHARGE_ID, GATEWAY_ACCOUNT_ID, "some backend error message", LENGTH_REQUIRED_411); + + InputStream body = postCapturePaymentResponse(TEST_CHARGE_ID) + .statusCode(500) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P1098")) + .assertThat("$.description", is("Downstream system error")); + } + + private ValidatableResponse postCapturePaymentResponse(String paymentId) { + return given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + API_KEY) + .post(String.format(CAPTURE_PAYMENTS_PATH, paymentId)) + .then(); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCreateIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCreateIT.java new file mode 100644 index 000000000..9d81da016 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCreateIT.java @@ -0,0 +1,881 @@ +package uk.gov.pay.api.it; + +import com.jayway.jsonassert.JsonAssert; +import io.restassured.response.ValidatableResponse; +import org.apache.http.HttpStatus; +import org.json.JSONObject; +import org.junit.Test; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.RefundSummary; +import uk.gov.pay.api.utils.JsonStringBuilder; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; +import uk.gov.pay.api.utils.mocks.CreateChargeRequestParams; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.validation.DateTimeUtils; + +import javax.ws.rs.core.HttpHeaders; +import java.io.IOException; +import java.io.InputStream; +import java.time.ZonedDateTime; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.model.TokenPaymentType.CARD; +import static uk.gov.pay.api.utils.Urls.paymentLocationFor; +import static uk.gov.pay.api.utils.mocks.ChargeResponseFromConnector.ChargeResponseFromConnectorBuilder.aCreateOrGetChargeResponseFromConnector; +import static uk.gov.pay.api.utils.mocks.CreateChargeRequestParams.CreateChargeRequestParamsBuilder.aCreateChargeRequestParams; +import static uk.gov.service.payments.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION; +import static uk.gov.service.payments.commons.model.Source.CARD_PAYMENT_LINK; + +public class PaymentsResourceCreateIT extends PaymentResourceITestBase { + + private static final ZonedDateTime TIMESTAMP = DateTimeUtils.toUTCZonedDateTime("2016-01-01T12:00:00Z").get(); + private static final int AMOUNT = 9999999; + private static final String CHARGE_ID = "ch_ab2341da231434l"; + private static final String CHARGE_TOKEN_ID = "token_1234567asdf"; + private static final PaymentState CREATED = new PaymentState("created", false, null, null); + private static final RefundSummary REFUND_SUMMARY = new RefundSummary("pending", 100L, 50L); + private static final String PAYMENT_PROVIDER = "Sandbox"; + private static final String CARD_BRAND_LABEL = "Mastercard"; + private static final String CARD_TYPE = "credit"; + private static final String RETURN_URL = "https://somewhere.gov.uk/rainbow/1"; + private static final String REFERENCE = "Some reference "; + private static final String DESCRIPTION = "Some description "; + private static final String CREATED_DATE = ISO_INSTANT_MILLISECOND_PRECISION.format(TIMESTAMP); + private static final Address BILLING_ADDRESS = new Address("line1", "line2", "NR2 5 6EG", "city", "UK"); + private static final CardDetailsFromResponse CARD_DETAILS = new CardDetailsFromResponse("1234", "123456", "Mr. Payment", "12/19", BILLING_ADDRESS, CARD_BRAND_LABEL, CARD_TYPE); + public static final String VALID_AGREEMENT_ID = "12345678901234567890123456"; + public static final String TOO_SHORT_AGREEMENT_ID = "1234567890"; + public static final String TOO_LONG_AGREEMENT_ID = "1234567890123456789012345699999"; + private static final String SUCCESS_PAYLOAD = paymentPayload(aCreateChargeRequestParams() + .withAmount(AMOUNT) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL).build()); + private static final String GATEWAY_TRANSACTION_ID = "gateway-tx-123456"; + private ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Test + public void createCardPaymentWithEmptyMetadataDoesNotStoreMetadata() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + + String payload = new JsonStringBuilder() + .add("amount", 100) + .add("reference", REFERENCE) + .add("description", DESCRIPTION) + .add("return_url", RETURN_URL) + .add("metadata", Map.of()) + .build(); + + connectorMockClient.respondCreated_whenCreateCharge(GATEWAY_ACCOUNT_ID, aCreateChargeRequestParams() + .withAmount(100) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .build()); + + postPaymentResponse(payload) + .statusCode(201) + .contentType(JSON) + .body("$", not(hasKey("metadata"))); + } + + @Test + public void createCardPaymentWithMetadata() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + + CreateChargeRequestParams createChargeRequestParams = aCreateChargeRequestParams() + .withAmount(100) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .withMetadata(Map.of("reconciled", true, "ledger_code", 123, "fuh", "fuh you")) + .build(); + connectorMockClient.respondCreated_whenCreateCharge(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + + postPaymentResponse(paymentPayload(createChargeRequestParams)) + .statusCode(201) + .contentType(JSON) + .body("metadata.reconciled", is(true)) + .body("metadata.ledger_code", is(123)) + .body("metadata.fuh", is("fuh you")); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + } + + @Test + public void createAChargeWithSetUpAgreementAndSaveAgreement() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + + CreateChargeRequestParams createChargeRequestParams = aCreateChargeRequestParams() + .withAmount(100) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .withSetUpAgreement(VALID_AGREEMENT_ID) + .build(); + connectorMockClient.respondCreated_whenCreateCharge(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + + postPaymentResponse(paymentPayload(createChargeRequestParams)) + .statusCode(HttpStatus.SC_CREATED) + .contentType(JSON) + .body("agreement_id", is(VALID_AGREEMENT_ID)); + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + } + + @Test + public void createPayment_responseWith422_whenAgreementIdTooShort() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + + CreateChargeRequestParams createChargeRequestParams = aCreateChargeRequestParams() + .withAmount(100) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .withSetUpAgreement(TOO_SHORT_AGREEMENT_ID) + .build(); + + postPaymentResponse(paymentPayload(createChargeRequestParams)) + .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) + .contentType(JSON) + .body("field", is("set_up_agreement")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: set_up_agreement. Field [set_up_agreement] length must be 26")); + } + + @Test + public void createPayment_responseWith422_whenAgreementIdTooLong() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + + CreateChargeRequestParams createChargeRequestParams = aCreateChargeRequestParams() + .withAmount(100) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .withSetUpAgreement(TOO_LONG_AGREEMENT_ID) + .build(); + + postPaymentResponse(paymentPayload(createChargeRequestParams)) + .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) + .contentType(JSON) + .body("field", is("set_up_agreement")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: set_up_agreement. Field [set_up_agreement] length must be 26")); + } + + @Test + public void createPayment_respondWith400_whenAgreementNotFound() throws IOException { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + + CreateChargeRequestParams createChargeRequestParams = aCreateChargeRequestParams() + .withAmount(100) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .withSetUpAgreement(VALID_AGREEMENT_ID) + .build(); + connectorMockClient.respondAgreementNotFound_whenCreateCharge(GATEWAY_ACCOUNT_ID, VALID_AGREEMENT_ID, "Agreement with ID [%s] not found."); + + InputStream body = postPaymentResponse(paymentPayload(createChargeRequestParams)) + .statusCode(HttpStatus.SC_BAD_REQUEST) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("set_up_agreement")) + .assertThat("$.code", is("P0103")) + .assertThat("$.description", is("Invalid attribute value: set_up_agreement. Agreement ID does not exist")); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + } + + @Test + public void createPayment_responseWith400_whenIncorrectAuthorisationModeForSavePaymentInstrumentToAgreement() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + CreateChargeRequestParams createChargeRequestParams = aCreateChargeRequestParams() + .withAmount(100) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withAuthorisationMode(AuthorisationMode.AGREEMENT) + .withSetUpAgreement(VALID_AGREEMENT_ID) + .build(); + connectorMockClient.respondIncorrectAuthorisationModeForSavePaymentInstrumentToAgreement_whenCreateCharge(GATEWAY_ACCOUNT_ID, VALID_AGREEMENT_ID, "error message from connector"); + + postPaymentResponse(paymentPayload(createChargeRequestParams)) + .statusCode(400) + .contentType(JSON) + .body("field", is(nullValue())) + .body("code", is("P0104")) + .body("description", is("Unexpected attribute: set_up_agreement")); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + } + + @Test + public void createPayment_responseWith400_whenIncorrectAuthorisationModeForSavePaymentInstrumentToAgreementWithAgreementId() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + CreateChargeRequestParams createChargeRequestParams = aCreateChargeRequestParams() + .withAmount(100) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withAuthorisationMode(AuthorisationMode.AGREEMENT) + .withAgreementId(VALID_AGREEMENT_ID) + .withSetUpAgreement(VALID_AGREEMENT_ID) + .build(); + connectorMockClient.respondIncorrectAuthorisationModeForSavePaymentInstrumentToAgreement_whenCreateCharge(GATEWAY_ACCOUNT_ID, VALID_AGREEMENT_ID, "error message from connector"); + + postPaymentResponse(paymentPayload(createChargeRequestParams)) + .statusCode(400) + .contentType(JSON) + .body("field", is(nullValue())) + .body("code", is("P0104")) + .body("description", is("Unexpected attribute: set_up_agreement")); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + } + + @Test + public void createCardPaymentWithMetadataAsNull_shouldReturn422() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + + var payload = new JSONObject() + .put("amount", 100) + .put("reference", "my reference") + .put("description", "my description") + .put("metadata", JSONObject.NULL) + .put("return_url", "https://test.test") + .toString(); + + postPaymentResponse(payload) + .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) + .contentType(JSON) + .body("field", is("metadata")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: metadata. Value must not be null")); + } + + @Test + public void createCardPaymentWithPrefilledCardholderDetails() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + CreateChargeRequestParams createChargeRequestParams = aCreateChargeRequestParams() + .withAmount(100) + .withDescription("description") + .withReference("reference") + .withReturnUrl(RETURN_URL) + .withEmail("j.bogs@example.org") + .witCardHolderName("J. Bogs") + .withAddressLine1("address line 1") + .withAddressLine2("address line 2") + .withAddressPostcode("AB1 CD2") + .withAddressCity("address city") + .withAddressCountry("GB") + .build(); + connectorMockClient.respondCreated_whenCreateCharge(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + + postPaymentResponse(paymentPayload(createChargeRequestParams)) + .statusCode(201) + .contentType(JSON) + .body("email", is("j.bogs@example.org")) + .body("card_details.cardholder_name", is("J. Bogs")) + .body("card_details.billing_address.line1", is("address line 1")) + .body("card_details.billing_address.line2", is("address line 2")) + .body("card_details.billing_address.postcode", is("AB1 CD2")) + .body("card_details.billing_address.city", is("address city")) + .body("card_details.billing_address.country", is("GB")); + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + } + + @Test + public void createCardPaymentShouldRespondWith400ErrorWhenNumericFieldInPrefilledCardholderDetails() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + String payload = new JsonStringBuilder() + .add("amount", 1000) + .add("reference", "reference") + .add("description", "description") + .add("return_url", RETURN_URL) + .addToNestedMap("line1", 123, "prefilled_cardholder_details", "billing_address") + .build(); + postPaymentResponse(payload) + .statusCode(HttpStatus.SC_BAD_REQUEST) + .contentType(JSON) + .body("code", is("P0102")) + .body("field", is("line1")) + .body("description", is("Invalid attribute value: line1. Field must be a string")); + } + + @Test + public void createCardPaymentWithSomePrefilledCardholderDetails() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + CreateChargeRequestParams createChargeRequestParams = aCreateChargeRequestParams() + .withAmount(100) + .withDescription("description") + .withReference("reference") + .withReturnUrl(RETURN_URL) + .witCardHolderName("J. Bogs") + .withAddressLine1("address line 1") + .withAddressCity("address city") + .withAddressCountry("GB") + .build(); + connectorMockClient.respondCreated_whenCreateCharge(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + + postPaymentResponse(paymentPayload(createChargeRequestParams)) + .statusCode(HttpStatus.SC_CREATED) + .contentType(JSON) + .body("email", is(nullValue())) + .body("card_details.cardholder_name", is("J. Bogs")) + .body("card_details.billing_address.line1", is("address line 1")) + .body("card_details.billing_address.line2", is(nullValue())) + .body("card_details.billing_address.postcode", is(nullValue())) + .body("card_details.billing_address.city", is("address city")) + .body("card_details.billing_address.country", is("GB")); + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + } + + @Test + public void createCardPayment() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + + connectorMockClient.respondCreated_whenCreateCharge(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, aCreateOrGetChargeResponseFromConnector() + .withAmount(AMOUNT) + .withChargeId(CHARGE_ID) + .withState(CREATED) + .withReturnUrl(RETURN_URL) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withPaymentProvider(PAYMENT_PROVIDER) + .withGatewayTransactionId(GATEWAY_TRANSACTION_ID) + .withCreatedDate(CREATED_DATE) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(true) + .withRefundSummary(REFUND_SUMMARY) + .withCardDetails(CARD_DETAILS) + .build()); + + String responseBody = postPaymentResponse(SUCCESS_PAYLOAD) + .statusCode(HttpStatus.SC_CREATED) + .contentType(JSON) + .header(HttpHeaders.LOCATION, is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))) + .body("payment_id", is(CHARGE_ID)) + .body("amount", is(9999999)) + .body("reference", is(REFERENCE)) + .body("email", nullValue()) + .body("description", is(DESCRIPTION)) + .body("state.status", is(CREATED.getStatus())) + .body("return_url", is(RETURN_URL)) + .body("payment_provider", is(PAYMENT_PROVIDER)) + .body("card_brand", is(CARD_BRAND_LABEL)) + .body("created_date", is(CREATED_DATE)) + .body("delayed_capture", is(true)) + .body("provider_id", is(GATEWAY_TRANSACTION_ID)) + .body("refund_summary.status", is("pending")) + .body("refund_summary.amount_submitted", is(50)) + .body("refund_summary.amount_available", is(100)) + .body("_links.self.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))) + .body("_links.self.method", is("GET")) + .body("_links.next_url.href", is(frontendUrlFor(CARD) + CHARGE_TOKEN_ID)) + .body("_links.next_url.method", is("GET")) + .body("_links.next_url_post.href", is(frontendUrlFor(CARD))) + .body("_links.next_url_post.method", is("POST")) + .body("_links.next_url_post.type", is("application/x-www-form-urlencoded")) + .body("_links.next_url_post.params.chargeTokenId", is(CHARGE_TOKEN_ID)) + .body("_links.events.href", is(paymentEventsLocationFor(CHARGE_ID))) + .body("_links.events.method", is("GET")) + .body("_links.cancel.href", is(paymentCancelLocationFor(CHARGE_ID))) + .body("_links.cancel.method", is("POST")) + .body("_links.refunds.href", is(paymentRefundsLocationFor(CHARGE_ID))) + .body("_links.refunds.method", is("GET")) + .body("metadata", nullValue()) + .extract().body().asString(); + + JsonAssert.with(responseBody) + .assertNotDefined("_links.self.type") + .assertNotDefined("_links.self.params") + .assertNotDefined("_links.next_url.type") + .assertNotDefined("_links.next_url.params") + .assertNotDefined("_links.events.type") + .assertNotDefined("_links.events.params"); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, SUCCESS_PAYLOAD); + } + + @Test + public void createPayment_withMinimumAmount() { + int minimumAmount = 1; + + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + connectorMockClient.respondCreated_whenCreateCharge(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, aCreateOrGetChargeResponseFromConnector() + .withAmount(minimumAmount) + .withChargeId(CHARGE_ID) + .withState(CREATED) + .withReturnUrl(RETURN_URL) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withPaymentProvider(PAYMENT_PROVIDER) + .withGatewayTransactionId(GATEWAY_TRANSACTION_ID) + .withCreatedDate(CREATED_DATE) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(false) + .withRefundSummary(REFUND_SUMMARY) + .withCardDetails(CARD_DETAILS) + .build()); + + CreateChargeRequestParams params = aCreateChargeRequestParams() + .withAmount(minimumAmount) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .build(); + + postPaymentResponse(paymentPayload(params)) + .statusCode(201) + .contentType(JSON) + .body("payment_id", is(CHARGE_ID)) + .body("amount", is(minimumAmount)) + .body("reference", is(REFERENCE)) + .body("description", is(DESCRIPTION)) + .body("return_url", is(RETURN_URL)) + .body("payment_provider", is(PAYMENT_PROVIDER)) + .body("created_date", is(CREATED_DATE)); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, params); + } + + @Test + public void createMOTOPayment() { + int amount = 1; + + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + connectorMockClient.respondCreated_whenCreateCharge(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, aCreateOrGetChargeResponseFromConnector() + .withAmount(amount) + .withChargeId(CHARGE_ID) + .withState(CREATED) + .withReturnUrl(RETURN_URL) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withPaymentProvider(PAYMENT_PROVIDER) + .withGatewayTransactionId(GATEWAY_TRANSACTION_ID) + .withCreatedDate(CREATED_DATE) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(false) + .withMoto(true) + .withRefundSummary(REFUND_SUMMARY) + .withCardDetails(CARD_DETAILS) + .build()); + + CreateChargeRequestParams params = aCreateChargeRequestParams() + .withAmount(amount) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .withMoto(true) + .build(); + + postPaymentResponse(paymentPayload(params)) + .statusCode(201) + .contentType(JSON) + .body("payment_id", is(CHARGE_ID)) + .body("amount", is(amount)) + .body("reference", is(REFERENCE)) + .body("description", is(DESCRIPTION)) + .body("return_url", is(RETURN_URL)) + .body("moto", is(true)) + .body("payment_provider", is(PAYMENT_PROVIDER)) + .body("created_date", is(CREATED_DATE)); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, params); + } + + @Test + public void createPaymentWithAuthorisationModeMotoApi() { + int amount = 1; + + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + connectorMockClient.respondCreated_whenCreateCharge_withAuthorisationMode_MotoApi(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, aCreateOrGetChargeResponseFromConnector() + .withAmount(amount) + .withChargeId(CHARGE_ID) + .withState(CREATED) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withPaymentProvider(PAYMENT_PROVIDER) + .withGatewayTransactionId(GATEWAY_TRANSACTION_ID) + .withCreatedDate(CREATED_DATE) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(false) + .withRefundSummary(REFUND_SUMMARY) + .withCardDetails(CARD_DETAILS) + .withAuthorisationMode(AuthorisationMode.MOTO_API) + .build()); + + CreateChargeRequestParams params = aCreateChargeRequestParams() + .withAmount(amount) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withAuthorisationMode(AuthorisationMode.MOTO_API) + .build(); + + postPaymentResponse(paymentPayload(params)) + .statusCode(201) + .contentType(JSON) + .body("$", not(hasKey("return_url"))) + .body("payment_id", is(CHARGE_ID)) + .body("amount", is(amount)) + .body("reference", is(REFERENCE)) + .body("description", is(DESCRIPTION)) + .body("payment_provider", is(PAYMENT_PROVIDER)) + .body("created_date", is(CREATED_DATE)) + .body("moto", is(true)) + .body("authorisation_mode", is(AuthorisationMode.MOTO_API.getName())) + .body("_links.auth_url_post.type", is("application/json")) + .body("_links.auth_url_post.method", is("POST")) + .body("_links.auth_url_post.href", is("http://publicapi.url/v1/auth")) + .body("_links.auth_url_post.params.one_time_token", is(CHARGE_TOKEN_ID)); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, params); + } + + @Test + public void createPaymentWithAuthorisationModeAgreement() { + int amount = 1; + + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + connectorMockClient.respondCreated_whenCreateCharge_withAuthorisationMode_Agreement(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, aCreateOrGetChargeResponseFromConnector() + .withAmount(amount) + .withChargeId(CHARGE_ID) + .withState(CREATED) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withPaymentProvider(PAYMENT_PROVIDER) + .withGatewayTransactionId(GATEWAY_TRANSACTION_ID) + .withCreatedDate(CREATED_DATE) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(false) + .withRefundSummary(REFUND_SUMMARY) + .withCardDetails(CARD_DETAILS) + .withAuthorisationMode(AuthorisationMode.AGREEMENT) + .withAgreementId(VALID_AGREEMENT_ID) + .build()); + + CreateChargeRequestParams params = aCreateChargeRequestParams() + .withAmount(amount) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withAuthorisationMode(AuthorisationMode.AGREEMENT) + .withAgreementId(VALID_AGREEMENT_ID) + .build(); + + postPaymentResponse(paymentPayload(params)) + .statusCode(201) + .contentType(JSON) + .body("$", not(hasKey("return_url"))) + .body("$_links", not(hasKey("cancel"))) + .body("payment_id", is(CHARGE_ID)) + .body("amount", is(amount)) + .body("reference", is(REFERENCE)) + .body("description", is(DESCRIPTION)) + .body("payment_provider", is(PAYMENT_PROVIDER)) + .body("created_date", is(CREATED_DATE)) + .body("moto", is(false)) + .body("authorisation_mode", is(AuthorisationMode.AGREEMENT.getName())); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, params); + } + + @Test + public void createPayment_withAllFieldsUpToMaxLengthBoundaries_shouldBeAccepted() { + int amount = 10000000; + String reference = randomAlphanumeric(255); + String description = randomAlphanumeric(255); + String email = randomAlphanumeric(242) + "@example.org"; + String return_url = "https://govdemopay.gov.uk?data=" + randomAlphanumeric(1969); + + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + connectorMockClient.respondCreated_whenCreateCharge(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, aCreateOrGetChargeResponseFromConnector() + .withAmount(amount) + .withChargeId(CHARGE_ID) + .withState(CREATED) + .withReturnUrl(return_url) + .withDescription(description) + .withReference(reference) + .withEmail(email) + .withPaymentProvider(PAYMENT_PROVIDER) + .withGatewayTransactionId(GATEWAY_TRANSACTION_ID) + .withCreatedDate(CREATED_DATE) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(false) + .withRefundSummary(REFUND_SUMMARY) + .withCardDetails(CARD_DETAILS) + .build()); + + String body = new JsonStringBuilder() + .add("amount", amount) + .add("reference", reference) + .add("email", email) + .add("card_brand", CARD_BRAND_LABEL) + .add("description", description) + .add("return_url", return_url) + .build(); + + postPaymentResponse(body) + .statusCode(201) + .contentType(JSON) + .body("payment_id", is(CHARGE_ID)) + .body("amount", is(amount)) + .body("reference", is(reference)) + .body("email", is(email)) + .body("description", is(description)) + .body("return_url", is(return_url)) + .body("payment_provider", is(PAYMENT_PROVIDER)) + .body("card_brand", is(CARD_BRAND_LABEL)) + .body("created_date", is(CREATED_DATE)); + } + + @Test + public void createPayment_responseWith500_whenConnectorResponseIsAnUnrecognisedError() throws Exception { + String gatewayAccountId = "1234567"; + String errorMessage = "something went wrong"; + + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, gatewayAccountId); + + connectorMockClient.respondBadRequest_whenCreateCharge(gatewayAccountId, errorMessage); + + InputStream body = postPaymentResponse(SUCCESS_PAYLOAD) + .statusCode(500) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0198")) + .assertThat("$.description", is("Downstream system error")); + + connectorMockClient.verifyCreateChargeConnectorRequest(gatewayAccountId, SUCCESS_PAYLOAD); + } + + @Test + public void createPayment_responseWith500_whenTokenForGatewayAccountIsValidButConnectorResponseIsNotFound() { + String notFoundGatewayAccountId = "9876545"; + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, notFoundGatewayAccountId); + + connectorMockClient.respondNotFound_whenCreateCharge(notFoundGatewayAccountId); + + postPaymentResponse(SUCCESS_PAYLOAD) + .statusCode(500) + .contentType(JSON) + .body("code", is("P0199")) + .body("description", is("There is an error with this account. Contact support with your error code - https://www.payments.service.gov.uk/support/ .")); + + connectorMockClient.verifyCreateChargeConnectorRequest(notFoundGatewayAccountId, SUCCESS_PAYLOAD); + } + + @Test + public void createPayment_responseWith422_whenMotoNotAllowed() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + connectorMockClient.respondMotoPaymentNotAllowed(GATEWAY_ACCOUNT_ID); + + String createMotoPaymentPayload = paymentPayload(aCreateChargeRequestParams() + .withAmount(AMOUNT) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .withMoto(true) + .build()); + + postPaymentResponse(createMotoPaymentPayload) + .statusCode(422) + .contentType(JSON) + .body("code", is("P0196")) + .body("description", is("MOTO payments are not enabled for this account. Please contact support if you would like to process MOTO payments - https://www.payments.service.gov.uk/support/ .")); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, createMotoPaymentPayload); + } + + @Test + public void createPayment_responseWith422_whenAuthApiNotAllowed() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + connectorMockClient.respondAuthorisationApiNotAllowed(GATEWAY_ACCOUNT_ID); + + String payload = paymentPayload(aCreateChargeRequestParams() + .withAmount(AMOUNT) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .withAuthorisationMode(AuthorisationMode.MOTO_API) + .build()); + + postPaymentResponse(payload) + .statusCode(422) + .contentType(JSON) + .body("code", is("P0195")) + .body("description", is("Using authorisation_mode of moto_api is not allowed for this account")); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, payload); + } + + @Test + public void createPayment_responseWith422_whenZeroAmountNotAllowed() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + connectorMockClient.respondZeroAmountNotAllowed(GATEWAY_ACCOUNT_ID); + + postPaymentResponse(SUCCESS_PAYLOAD) + .statusCode(422) + .contentType(JSON) + .body("code", is("P0102")) + .body("field", is("amount")) + .body("description", is("Invalid attribute value: amount. Must be greater than or equal to 1")); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, SUCCESS_PAYLOAD); + } + + @Test + public void createPayment_Returns401_WhenUnauthorised() { + publicAuthMockClient.respondUnauthorised(); + postPaymentResponse(SUCCESS_PAYLOAD).statusCode(401); + } + + @Test + public void createPayment_Returns403_WhenGatewayAccountCredentialsNotFullyConfigured() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + connectorMockClient.respondGatewayAccountCredentialNotConfigured(GATEWAY_ACCOUNT_ID); + postPaymentResponse(SUCCESS_PAYLOAD) + .statusCode(403) + .contentType(JSON) + .body("code", is("P0940")) + .body("description", is("Account is not fully configured. Please refer to documentation to setup your account or contact support with your error code - https://www.payments.service.gov.uk/support/ .")); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, SUCCESS_PAYLOAD); + } + + @Test + public void createPayment_Returns400_WhenCardNumberIsEnteredInPaymentLinkReference() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + connectorMockClient.respondCardNumberInReferenceError(GATEWAY_ACCOUNT_ID); + + String payload = paymentPayload(aCreateChargeRequestParams() + .withAmount(AMOUNT) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .withSource(CARD_PAYMENT_LINK) + .build()); + postPaymentResponse(payload) + .statusCode(400) + .contentType(JSON) + .body("code", is("P0105")) + .body("description", is("Card number entered in a payment link reference")); + + String connectorPayload = new JsonStringBuilder().add("amount", AMOUNT).add("reference", REFERENCE).add("description", DESCRIPTION) + .add("return_url", RETURN_URL).add("source", CARD_PAYMENT_LINK).build(); + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, connectorPayload); + } + + @Test + public void createPayment_Returns_WhenPublicAuthInaccessible() { + publicAuthMockClient.respondWithError(); + postPaymentResponse(SUCCESS_PAYLOAD).statusCode(503); + } + + @Test + public void createCardPaymentWithSource() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + + CreateChargeRequestParams createChargeRequestParams = aCreateChargeRequestParams() + .withAmount(100) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .withSource(CARD_PAYMENT_LINK) + .build(); + connectorMockClient.respondCreated_whenCreateCharge(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + + postPaymentResponse(paymentPayload(createChargeRequestParams)) + .statusCode(201) + .contentType(JSON); + + connectorMockClient.verifyCreateChargeConnectorRequest(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + } + + public static String paymentPayload(CreateChargeRequestParams params) { + JsonStringBuilder payload = new JsonStringBuilder() + .add("amount", params.getAmount()) + .add("reference", params.getReference()) + .add("description", params.getDescription()) + .add("return_url", params.getReturnUrl()); + + if (!params.getMetadata().isEmpty()) { + payload.add("metadata", params.getMetadata()); + } + + if (params.getEmail() != null) { + payload.add("email", params.getEmail()); + } + + if (params.isMoto() != null) { + payload.add("moto", params.isMoto()); + } + + params.getCardholderName().ifPresent(cardholderName -> { + payload.addToNestedMap("cardholder_name", cardholderName, "prefilled_cardholder_details"); + }); + + params.getAddressLine1().ifPresent(addressLine1 -> { + payload.addToNestedMap("line1", addressLine1, "prefilled_cardholder_details", "billing_address"); + }); + + params.getAddressLine2().ifPresent(addressLine2 -> { + payload.addToNestedMap("line2", addressLine2, "prefilled_cardholder_details", "billing_address"); + }); + + params.getAddressPostcode().ifPresent(addressPostcode -> { + payload.addToNestedMap("postcode", addressPostcode, "prefilled_cardholder_details", "billing_address"); + }); + + params.getAddressCity().ifPresent(addressCity -> { + payload.addToNestedMap("city", addressCity, "prefilled_cardholder_details", "billing_address"); + }); + + params.getAddressCountry().ifPresent(addressCountry -> { + payload.addToNestedMap("country", addressCountry, "prefilled_cardholder_details", "billing_address"); + }); + + params.getSource().ifPresent(source -> payload.addToNestedMap("source", source, "internal")); + params.getSetUpAgreement().ifPresent(setUpAgreement -> payload.add("set_up_agreement", setUpAgreement)); + params.getAgreementId().ifPresent(agreementId -> payload.add("agreement_id", agreementId)); + params.getAuthorisationMode().ifPresent(authorisationMode -> payload.add("authorisation_mode", authorisationMode)); + + return payload.build(); + } + + protected ValidatableResponse postPaymentResponse(String payload) { + return given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .post(PAYMENTS_PATH) + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCreateWithIdempotencyKeyIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCreateWithIdempotencyKeyIT.java new file mode 100644 index 000000000..d153f2c2b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceCreateWithIdempotencyKeyIT.java @@ -0,0 +1,214 @@ +package uk.gov.pay.api.it; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.matching.StringValuePattern; +import io.restassured.response.ValidatableResponse; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Test; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.RefundSummary; +import uk.gov.pay.api.utils.JsonStringBuilder; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ChargeResponseFromConnector; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; +import uk.gov.pay.api.utils.mocks.CreateChargeRequestParams; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.validation.DateTimeUtils; + +import java.time.ZonedDateTime; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.model.TokenPaymentType.CARD; +import static uk.gov.pay.api.utils.mocks.ChargeResponseFromConnector.ChargeResponseFromConnectorBuilder.aCreateOrGetChargeResponseFromConnector; +import static uk.gov.pay.api.utils.mocks.CreateChargeRequestParams.CreateChargeRequestParamsBuilder.aCreateChargeRequestParams; +import static uk.gov.service.payments.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION; + +public class PaymentsResourceCreateWithIdempotencyKeyIT extends PaymentResourceITestBase { + + private static final ZonedDateTime TIMESTAMP = DateTimeUtils.toUTCZonedDateTime("2016-01-01T12:00:00Z").get(); + private static final int AMOUNT = 9999999; + private static final String CHARGE_ID = "ch_ab2341da231434l"; + private static final String CHARGE_TOKEN_ID = "token_1234567asdf"; + private static final PaymentState CREATED = new PaymentState("created", false, null, null); + private static final RefundSummary REFUND_SUMMARY = new RefundSummary("pending", 100L, 50L); + private static final String PAYMENT_PROVIDER = "Sandbox"; + private static final String CARD_BRAND_LABEL = "Mastercard"; + private static final String CARD_TYPE = "credit"; + private static final String RETURN_URL = "https://somewhere.gov.uk/rainbow/1"; + private static final String REFERENCE = "Some reference"; + private static final String DESCRIPTION = "Some description"; + private static final String CREATED_DATE = ISO_INSTANT_MILLISECOND_PRECISION.format(TIMESTAMP); + private static final Address BILLING_ADDRESS = new Address("line1", "line2", "NR2 5 6EG", "city", "UK"); + private static final CardDetailsFromResponse CARD_DETAILS = new CardDetailsFromResponse("1234", "123456", "Mr. Payment", "12/19", BILLING_ADDRESS, CARD_BRAND_LABEL, CARD_TYPE); + public static final String VALID_AGREEMENT_ID = "12345678901234567890123456"; + private static final String SUCCESS_PAYLOAD = paymentPayload(aCreateChargeRequestParams() + .withAmount(AMOUNT) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL).build()); + private static final String GATEWAY_TRANSACTION_ID = "gateway-tx-123456"; + private static final String IDEMPOTENCY_KEY = "an-idempotency-key"; + private ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Test + public void createCardPaymentWithIdempotencyKeyReturns201() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + + CreateChargeRequestParams createChargeRequestParams = aCreateChargeRequestParams() + .withAmount(100) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .withAgreementId(VALID_AGREEMENT_ID) + .withAuthorisationMode(AuthorisationMode.AGREEMENT) + .build(); + connectorMockClient.respondCreated_whenCreateCharge(GATEWAY_ACCOUNT_ID, createChargeRequestParams); + + StringValuePattern idempotencyKeyMatcher = WireMock.equalTo(IDEMPOTENCY_KEY); + + postPaymentResponseWithIdempotencyKey(paymentPayload(createChargeRequestParams), IDEMPOTENCY_KEY) + .statusCode(201) + .contentType(JSON); + + connectorMockClient.verifyCreateChargeConnectorRequestWithHeader(GATEWAY_ACCOUNT_ID, paymentPayload(createChargeRequestParams), idempotencyKeyMatcher); + } + + @Test + public void createPaymentShouldReturn200_whenConnectorReturns200() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + CreateChargeRequestParams createChargeRequestParams = aCreateChargeRequestParams() + .withAmount(100) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .withAgreementId(VALID_AGREEMENT_ID) + .withAuthorisationMode(AuthorisationMode.AGREEMENT) + .build(); + connectorMockClient.respondOK_whenCreateChargeIdempotencyKey(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, getConnectorCharge() + .build()); + + StringValuePattern idempotencyKeyMatcher = WireMock.equalTo(IDEMPOTENCY_KEY); + + postPaymentResponseWithIdempotencyKey(paymentPayload(createChargeRequestParams), IDEMPOTENCY_KEY) + .statusCode(200) + .contentType(JSON); + + connectorMockClient.verifyCreateChargeConnectorRequestWithHeader(GATEWAY_ACCOUNT_ID, paymentPayload(createChargeRequestParams), idempotencyKeyMatcher); + } + + @Test + public void createPaymentShouldReturn409_whenConnectorReturns409() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + CreateChargeRequestParams createChargeRequestParams = aCreateChargeRequestParams() + .withAmount(100) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withReturnUrl(RETURN_URL) + .withAgreementId(VALID_AGREEMENT_ID) + .withAuthorisationMode(AuthorisationMode.AGREEMENT) + .build(); + connectorMockClient.respondConflict_whenCreateChargeAndIdempotencyKeyUSed(GATEWAY_ACCOUNT_ID); + + StringValuePattern idempotencyKeyMatcher = WireMock.equalTo(IDEMPOTENCY_KEY); + + postPaymentResponseWithIdempotencyKey(paymentPayload(createChargeRequestParams), IDEMPOTENCY_KEY) + .statusCode(409) + .contentType(JSON) + .body("code", is("P0191")) + .body("description", is("The `Idempotency-Key` you sent in the request header has already been used to create a payment.")); + + connectorMockClient.verifyCreateChargeConnectorRequestWithHeader(GATEWAY_ACCOUNT_ID, paymentPayload(createChargeRequestParams), idempotencyKeyMatcher); + } + + @Test + public void createPayment_responseWith422_whenIdempotencyKeyIsEmpty() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + postPaymentResponseWithIdempotencyKey(SUCCESS_PAYLOAD, "") + .statusCode(422) + .contentType(JSON) + .body("code", is("P0102")) + .body("header", is("Idempotency-Key")) + .body("description", is("Header [Idempotency-Key] can have a size between 1 and 255")); + } + + @Test + public void createPayment_responseWith422_whenIdempotencyKeyIsTooLong() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + postPaymentResponseWithIdempotencyKey(SUCCESS_PAYLOAD, RandomStringUtils.randomAlphanumeric(256)) + .statusCode(422) + .contentType(JSON) + .body("code", is("P0102")) + .body("header", is("Idempotency-Key")) + .body("description", is("Header [Idempotency-Key] can have a size between 1 and 255")); + } + + @Test + public void createPayment_responseWith422_whenIdempotencyKeyContainsSpecialCharacters() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + postPaymentResponseWithIdempotencyKey(SUCCESS_PAYLOAD, "123$@!?") + .statusCode(422) + .contentType(JSON) + .body("code", is("P0102")) + .body("header", is("Idempotency-Key")) + .body("description", is("Header [Idempotency-Key] can only contain alphanumeric characters and hyphens")); + } + + + private static String paymentPayload(CreateChargeRequestParams params) { + JsonStringBuilder payload = new JsonStringBuilder() + .add("amount", params.getAmount()) + .add("reference", params.getReference()) + .add("description", params.getDescription()) + .add("return_url", params.getReturnUrl()); + + if (!params.getMetadata().isEmpty()) { + payload.add("metadata", params.getMetadata()); + } + + params.getSource().ifPresent(source -> payload.addToNestedMap("source", source, "internal")); + params.getSetUpAgreement().ifPresent(setUpAgreement -> payload.add("set_up_agreement", setUpAgreement)); + params.getAgreementId().ifPresent(agreementId -> payload.add("agreement_id", agreementId)); + params.getAuthorisationMode().ifPresent(authorisationMode -> payload.add("authorisation_mode", authorisationMode)); + + return payload.build(); + } + + private ChargeResponseFromConnector.ChargeResponseFromConnectorBuilder getConnectorCharge() { + return aCreateOrGetChargeResponseFromConnector() + .withAmount(AMOUNT) + .withChargeId(CHARGE_ID) + .withState(CREATED) + .withReturnUrl(RETURN_URL) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withPaymentProvider(PAYMENT_PROVIDER) + .withCreatedDate(CREATED_DATE) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(true) + .withMoto(false) + .withRefundSummary(REFUND_SUMMARY) + .withCardDetails(CARD_DETAILS) + .withGatewayTransactionId(GATEWAY_TRANSACTION_ID); + } + + private ValidatableResponse postPaymentResponseWithIdempotencyKey(String payload, String idempotencyKey) { + return given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .header("Idempotency-Key", idempotencyKey) + .post(PAYMENTS_PATH) + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceGetIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceGetIT.java new file mode 100644 index 000000000..20c48e2f5 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceGetIT.java @@ -0,0 +1,861 @@ +package uk.gov.pay.api.it; + +import com.jayway.jsonassert.JsonAssert; +import io.restassured.response.ValidatableResponse; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.AuthorisationSummary; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.PaymentSettlementSummary; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.RefundSummary; +import uk.gov.pay.api.model.ThreeDSecure; +import uk.gov.pay.api.model.Wallet; +import uk.gov.pay.api.utils.ChargeEventBuilder; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ChargeResponseFromConnector; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; +import uk.gov.pay.api.utils.mocks.LedgerMockClient; +import uk.gov.pay.api.utils.mocks.TransactionFromLedgerFixture; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.validation.DateTimeUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.apache.http.HttpHeaders.CACHE_CONTROL; +import static org.apache.http.HttpHeaders.PRAGMA; +import static org.apache.http.HttpStatus.SC_NOT_ACCEPTABLE; +import static org.apache.http.HttpStatus.SC_NOT_FOUND; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.model.TokenPaymentType.CARD; +import static uk.gov.pay.api.utils.Urls.paymentLocationFor; +import static uk.gov.pay.api.utils.mocks.ChargeResponseFromConnector.ChargeResponseFromConnectorBuilder.aCreateOrGetChargeResponseFromConnector; +import static uk.gov.pay.api.utils.mocks.TransactionEventFixture.TransactionEventFixtureBuilder.aTransactionEventFixture; +import static uk.gov.pay.api.utils.mocks.TransactionFromLedgerFixture.TransactionFromLedgerBuilder.aTransactionFromLedgerFixture; +import static uk.gov.service.payments.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION; + +public class PaymentsResourceGetIT extends PaymentResourceITestBase { + + private static final ZonedDateTime CAPTURED_DATE = ZonedDateTime.parse("2016-01-02T14:03:00Z"); + private static final ZonedDateTime CAPTURE_SUBMIT_TIME = ZonedDateTime.parse("2016-01-02T15:02:00Z"); + private static final ZonedDateTime SETTLED_DATE = ZonedDateTime.parse("2016-01-06T15:02:00Z"); + private static final PaymentSettlementSummary SETTLEMENT_SUMMARY_CONNECTOR = new PaymentSettlementSummary(ISO_INSTANT_MILLISECOND_PRECISION.format(CAPTURE_SUBMIT_TIME), + DateTimeUtils.toLocalDateString(CAPTURED_DATE), null); + private static final PaymentSettlementSummary SETTLEMENT_SUMMARY_LEDGER = new PaymentSettlementSummary(ISO_INSTANT_MILLISECOND_PRECISION.format(CAPTURE_SUBMIT_TIME), + DateTimeUtils.toLocalDateString(CAPTURED_DATE), DateTimeUtils.toLocalDateString(SETTLED_DATE)); + private static final int AMOUNT = 9999999; + private static final Long FEE = 5L; + private static final Long NET_AMOUNT = 9999994L; + private static final String CHARGE_ID = "ch_ab2341da231434l"; + private static final String CHARGE_TOKEN_ID = "token_1234567asdf"; + private static final PaymentState CREATED = new PaymentState("created", false, null, null); + private static final PaymentState CAPTURED = new PaymentState("captured", false, null, null); + public static final PaymentState AWAITING_CAPTURE_REQUEST = new PaymentState("submitted", false, null, null); + public static final PaymentState REJECTED = new PaymentState("failed", true, "Payment method rejected", "P0010", true); + private static final RefundSummary REFUND_SUMMARY = new RefundSummary("pending", 100L, 50L); + private static final String PAYMENT_PROVIDER = "Sandbox"; + private static final String CARD_BRAND_LABEL = "Mastercard"; + private static final String CARD_TYPE = "debit"; + private static final String RETURN_URL = "https://somewhere.gov.uk/rainbow/1"; + private static final String REFERENCE = "Some reference "; + private static final String EMAIL = "alice.111@mail.fake"; + private static final String GATEWAY_TRANSACTION_ID = "gateway-tx-123456"; + private static final String DESCRIPTION = "Some description "; + private static final String CREATED_DATE = ISO_INSTANT_MILLISECOND_PRECISION.format(ZonedDateTime.parse("2010-12-31T22:59:59.132012345Z")); + private static final Map PAYMENT_CREATED = new ChargeEventBuilder(CREATED, CREATED_DATE).build(); + private static final List> EVENTS = Collections.singletonList(PAYMENT_CREATED); + private static final Address BILLING_ADDRESS = new Address("line1", "line2", "NR2 5 6EG", "city", "UK"); + private static final CardDetailsFromResponse CARD_DETAILS = new CardDetailsFromResponse("1234", "123456", "Mr. Payment", "12/19", BILLING_ADDRESS, CARD_BRAND_LABEL, CARD_TYPE); + + private final ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + private final PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + private final LedgerMockClient ledgerMockClient = new LedgerMockClient(ledgerMock); + + @Before + public void setUp() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void getPaymentWithMetadataThroughConnector() { + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .withMetadata(Map.of("reconciled", true, "ledger_code", 123, "fuh", "fuh you", "surcharge", 1.23)) + .build()); + + ValidatableResponse response = getPaymentResponse(CHARGE_ID); + + response.header(PRAGMA, "no-cache").header(CACHE_CONTROL, "no-store"); + assertCommonPaymentFields(response); + assertConnectorOnlyPaymentFields(response); + assertPaymentWithMetadata(response); + } + + @Test + public void getPaymentWithMetadataThroughLedger() { + ledgerMockClient.respondWithTransaction(CHARGE_ID, + getLedgerTransaction() + .withMetadata(Map.of("reconciled", true, "ledger_code", 123, "fuh", "fuh you", "surcharge", 1.23)) + .build()); + + ValidatableResponse response = getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY); + + response.header(PRAGMA, "no-cache").header(CACHE_CONTROL, "no-store"); + assertCommonPaymentFields(response); + assertPaymentWithMetadata(response); + } + + private void assertPaymentWithMetadata(ValidatableResponse response) { + response + .body("metadata.reconciled", is(true)) + .body("metadata.ledger_code", is(123)) + .body("metadata.fuh", is("fuh you")) + .body("metadata.surcharge", is(1.23f)); + } + + @Test + public void getPaymentWithMotoThroughConnector() { + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .withMoto(true) + .build()); + + ValidatableResponse response = getPaymentResponse(CHARGE_ID); + + assertCommonPaymentFields(response); + assertConnectorOnlyPaymentFields(response); + assertPaymentWithMoto(response); + } + + @Test + public void getPaymentWithMotoThroughLedger() { + ledgerMockClient.respondWithTransaction(CHARGE_ID, + getLedgerTransaction() + .withMoto(true) + .build()); + + ValidatableResponse response = getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY); + + assertCommonPaymentFields(response); + assertPaymentWithMoto(response); + } + + private void assertPaymentWithMoto(ValidatableResponse response) { + response.body("moto", is(true)); + } + + private void assertPaymentWithAuthorisationSummary(ValidatableResponse response) { + response.body("authorisation_summary", is(notNullValue())); + response.body("authorisation_summary.three_d_secure", is(notNullValue())); + response.body("authorisation_summary.three_d_secure.required", is(true)); + } + + @Test + public void getPaymentThroughConnector_ReturnsPayment() { + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, getConnectorCharge().build()); + + ValidatableResponse response = getPaymentResponse(CHARGE_ID); + + assertCommonPaymentFields(response); + assertConnectorOnlyPaymentFields(response); + response.body("metadata", is(nullValue())); + } + + @Test + public void getPaymentThroughLedger_ReturnsPayment() { + ledgerMockClient.respondWithTransaction(CHARGE_ID, getLedgerTransaction().build()); + + ValidatableResponse response = getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY); + + assertCommonPaymentFields(response); + response.body("metadata", is(nullValue())); + response.body("settlement_summary.settled_date", is(DateTimeUtils.toLocalDateString(SETTLED_DATE))); + response.body("settlement_summary.captured_date", is(DateTimeUtils.toLocalDateString(CAPTURED_DATE))); + response.body("settlement_summary.capture_submit_time", is(ISO_INSTANT_MILLISECOND_PRECISION.format(CAPTURE_SUBMIT_TIME))); + } + + @Test + public void getPaymentWithAuthorisationModeMotoApiThroughConnector_ReturnsPayment() { + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, getConnectorCharge() + .withAuthorisationMode(AuthorisationMode.MOTO_API) + .build(), true); + + ValidatableResponse response = getPaymentResponse(CHARGE_ID); + + assertCommonPaymentFields(response); + assertConnectorOnlyMotoApiPaymentFields(response); + response.body("authorisation_mode", is("moto_api")); + } + + @Test + public void getPaymentWithAuthorisationModeMotoApiThroughLedger_ReturnsPayment() { + ledgerMockClient.respondWithTransaction(CHARGE_ID, getLedgerTransaction() + .withAuthorisationMode(AuthorisationMode.MOTO_API) + .build()); + + ValidatableResponse response = getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY); + + assertCommonPaymentFields(response); + response.body("authorisation_mode", is("moto_api")); + } + + private void assertCommonPaymentFields(ValidatableResponse paymentResponse) { + paymentResponse + .statusCode(200) + .contentType(JSON) + .body("payment_id", is(CHARGE_ID)) + .body("reference", is(REFERENCE)) + .body("email", is(EMAIL)) + .body("description", is(DESCRIPTION)) + .body("amount", is(AMOUNT)) + .body("state.status", is(CAPTURED.getStatus())) + .body("return_url", is(RETURN_URL)) + .body("payment_provider", is(PAYMENT_PROVIDER)) + .body("card_brand", is(CARD_BRAND_LABEL)) + .body("created_date", is(CREATED_DATE)) + .body("language", is("en")) + .body("delayed_capture", is(true)) + .body("provider_id", is(GATEWAY_TRANSACTION_ID)) + .body("refund_summary.status", is("pending")) + .body("refund_summary.amount_submitted", is(50)) + .body("refund_summary.amount_available", is(100)) + .body("settlement_summary.capture_submit_time", is(ISO_INSTANT_MILLISECOND_PRECISION.format(CAPTURE_SUBMIT_TIME))) + .body("settlement_summary.captured_date", is(DateTimeUtils.toLocalDateString(CAPTURED_DATE))) + .body("card_details.card_brand", is(CARD_BRAND_LABEL)) + .body("card_details.card_type", is(CARD_TYPE)) + .body("card_details.cardholder_name", is(CARD_DETAILS.getCardHolderName())) + .body("card_details.first_digits_card_number", is(CARD_DETAILS.getFirstDigitsCardNumber())) + .body("card_details.last_digits_card_number", is(CARD_DETAILS.getLastDigitsCardNumber())) + .body("card_details.expiry_date", is(CARD_DETAILS.getExpiryDate())) + .body("card_details.billing_address.line1", is(CARD_DETAILS.getBillingAddress().get().getLine1())) + .body("card_details.billing_address.line2", is(CARD_DETAILS.getBillingAddress().get().getLine2())) + .body("card_details.billing_address.postcode", is(CARD_DETAILS.getBillingAddress().get().getPostcode())) + .body("card_details.billing_address.country", is(CARD_DETAILS.getBillingAddress().get().getCountry())) + .body("_links.self.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))) + .body("_links.self.method", is("GET")) + .body("_links.events.href", is(paymentEventsLocationFor(CHARGE_ID))) + .body("_links.events.method", is("GET")) + .body("_links.cancel.href", is(paymentCancelLocationFor(CHARGE_ID))) + .body("_links.cancel.method", is("POST")) + .body("_links.refunds.href", is(paymentRefundsLocationFor(CHARGE_ID))) + .body("_links.refunds.method", is("GET")) + .body("containsKey('corporate_card_surcharge')", is(false)) + .body("containsKey('total_amount')", is(false)); + } + + private void assertConnectorOnlyPaymentFields(ValidatableResponse paymentResponse) { + paymentResponse + .body("_links.next_url.href", is(frontendUrlFor(CARD) + CHARGE_ID)) + .body("_links.next_url.method", is("GET")) + .body("_links.next_url_post.href", is(frontendUrlFor(CARD))) + .body("_links.next_url_post.method", is("POST")) + .body("_links.next_url_post.type", is("application/x-www-form-urlencoded")) + .body("_links.next_url_post.params.chargeTokenId", is(CHARGE_TOKEN_ID)); + } + + private void assertConnectorOnlyMotoApiPaymentFields(ValidatableResponse paymentResponse) { + paymentResponse + .body("_links.auth_url_post.href", is("http://publicapi.url/v1/auth")) + .body("_links.auth_url_post.method", is("POST")) + .body("_links.auth_url_post.type", is("application/json")) + .body("_links.auth_url_post.params.one_time_token", is(CHARGE_TOKEN_ID)); + } + + @Test + public void getPaymentThroughConnector_DoesNotReturnCardDigits_IfNotPresentInCardDetails() { + CardDetailsFromResponse cardDetails = new CardDetailsFromResponse(null, null, "Mr. Payment", "12/19", BILLING_ADDRESS, CARD_BRAND_LABEL, CARD_TYPE); + + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .withCardDetails(cardDetails) + .build()); + + assertPaymentWithoutCardDetails(getPaymentResponse(CHARGE_ID)); + } + + @Test + public void getPaymentThroughLedger_DoesNotReturnCardDigits_IfNotPresentInCardDetails() { + CardDetailsFromResponse cardDetails = new CardDetailsFromResponse(null, null, "Mr. Payment", "12/19", BILLING_ADDRESS, CARD_BRAND_LABEL, CARD_TYPE); + + ledgerMockClient.respondWithTransaction(CHARGE_ID, + getLedgerTransaction() + .withCardDetails(cardDetails) + .build()); + + assertPaymentWithoutCardDetails(getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY)); + } + + private void assertPaymentWithoutCardDetails(ValidatableResponse paymentResponse) { + paymentResponse + .statusCode(200) + .contentType(JSON) + .body("payment_id", is(CHARGE_ID)) + .body("card_details.first_digits_card_number", is(nullValue())) + .body("card_details.last_digits_card_number", is(nullValue())); + } + + @Test + public void getPaymentThroughConnector_ShouldNotIncludeCancelLinkIfPaymentCannotBeCancelled() { + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .withState(new PaymentState("success", true, null, null)) + .withSettlementSummary(null) + .build()); + + assertPaymentWithoutCancelLink(getPaymentResponse(CHARGE_ID)); + } + + @Test + public void getPaymentThroughLedger_ShouldNotIncludeCancelLinkIfPaymentCannotBeCancelled() { + ledgerMockClient.respondWithTransaction(CHARGE_ID, + getLedgerTransaction() + .withState(new PaymentState("success", true, null, null)) + .withSettlementSummary(null) + .build()); + + assertPaymentWithoutCancelLink(getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY)); + } + + private void assertPaymentWithoutCancelLink(ValidatableResponse paymentResponse) { + paymentResponse + .statusCode(200) + .contentType(JSON) + .body("_links.cancel", is(nullValue())); + } + + @Test + public void getPaymentThroughConnector_ShouldNotIncludeSettlementFieldsIfNull() { + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .withState(CREATED) + .withSettlementSummary(new PaymentSettlementSummary(null, null, null)) + .build()); + + assertPaymentWithoutSettlementSummary(getPaymentResponse(CHARGE_ID)); + } + + @Test + public void getPaymentThroughLedger_ShouldNotIncludeSettlementFieldsIfNull() { + ledgerMockClient.respondWithTransaction(CHARGE_ID, + getLedgerTransaction() + .withState(CREATED) + .withSettlementSummary(new PaymentSettlementSummary(null, null, null)) + .build()); + + assertPaymentWithoutSettlementSummary(getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY)); + } + + private void assertPaymentWithoutSettlementSummary(ValidatableResponse paymentResponse) { + paymentResponse + .statusCode(200) + .contentType(JSON) + .rootPath("settlement_summary") + .body("containsKey('capture_submit_time')", is(false)) + .body("containsKey('captured_date')", is(false)); + } + + @Test + public void getPayment_BillingAddressShouldBeNullWhenNotPresentInConnectorResponse() { + getPayment_BillingAddressShouldBeNullWhenNotPresentInServiceResponse( + cd -> connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .withCardDetails(cd) + .build()), + CONNECTOR_ONLY_STRATEGY); + } + + @Test + public void getPayment_BillingAddressShouldBeNullWhenNotPresentInLedgerResponse() { + getPayment_BillingAddressShouldBeNullWhenNotPresentInServiceResponse( + cd -> ledgerMockClient.respondWithTransaction(CHARGE_ID, + getLedgerTransaction() + .withCardDetails(cd) + .build()), + LEDGER_ONLY_STRATEGY); + } + + private void getPayment_BillingAddressShouldBeNullWhenNotPresentInServiceResponse(Consumer mockResponseFunction, String strategy) { + CardDetailsFromResponse cardDetails = new CardDetailsFromResponse("1234", + "123456", + "Mr. Payment", + "12/19", + null, + CARD_BRAND_LABEL, + CARD_TYPE); + + mockResponseFunction.accept(cardDetails); + + getPaymentResponse(CHARGE_ID, strategy) + .statusCode(200) + .contentType(JSON) + .body("card_details", hasKey("billing_address")) + .body("card_details.billing_address", is(nullValue())) + .body("payment_id", is(CHARGE_ID)); + } + + @Test + public void getPayment_Returns401_WhenUnauthorised() { + publicAuthMockClient.respondUnauthorised(); + + getPaymentResponse(CHARGE_ID) + .statusCode(401); + } + + @Test + public void getPayment_returns404_whenConnectorAndLedgerRespondWith404() throws IOException { + String paymentId = "ds2af2afd3df112"; + String errorMessage = "backend-error-message"; + connectorMockClient.respondChargeNotFound(GATEWAY_ACCOUNT_ID, paymentId, errorMessage); + ledgerMockClient.respondTransactionNotFound(paymentId, errorMessage); + + InputStream body = getPaymentResponse(paymentId, "future-behaviour") + .statusCode(404) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0200")) + .assertThat("$.description", is("Not found")); + } + + @Test + public void getPayment_returns500_whenConnectorRespondsWithResponseOtherThan200Or404() throws IOException { + String paymentId = "ds2af2afd3df112"; + String errorMessage = "backend-error-message"; + connectorMockClient.respondWhenGetCharge(GATEWAY_ACCOUNT_ID, paymentId, errorMessage, SC_NOT_ACCEPTABLE); + + assertErrorPaymentResponse(getPaymentResponse(paymentId, CONNECTOR_ONLY_STRATEGY)); + } + + @Test + public void getPayment_returns500_whenLedgerRespondsWithResponseOtherThan200Or404() throws IOException { + String paymentId = "ds2af2afd3df112"; + String errorMessage = "backend-error-message"; + ledgerMockClient.respondTransactionWithError(paymentId, errorMessage, SC_NOT_ACCEPTABLE); + + assertErrorPaymentResponse(getPaymentResponse(paymentId, LEDGER_ONLY_STRATEGY)); + } + + private void assertErrorPaymentResponse(ValidatableResponse response) throws IOException { + InputStream body = response + .statusCode(500) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0298")) + .assertThat("$.description", is("Downstream system error")); + } + + @Test + public void getPaymentEventsThroughConnector_ReturnsPaymentEvents() { + connectorMockClient.respondWithChargeEventsFound(GATEWAY_ACCOUNT_ID, CHARGE_ID, EVENTS); + + assertPaymentEventsResponse(getPaymentEventsResponse(CHARGE_ID)); + } + + @Test + public void getPaymentEventsThroughLedger_ReturnsPaymentEvents() { + var eventFixture = aTransactionEventFixture().withState(CREATED).withTimestamp(CREATED_DATE).build(); + ledgerMockClient.respondWithTransactionEvents(CHARGE_ID, eventFixture); + + assertPaymentEventsResponse(getPaymentEventsResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY)); + } + + private void assertPaymentEventsResponse(ValidatableResponse paymentEventsResponse) { + paymentEventsResponse + .statusCode(200) + .contentType(JSON) + .body("payment_id", is(CHARGE_ID)) + .body("_links.self.href", is(paymentEventsLocationFor(CHARGE_ID))) + .body("events", hasSize(1)) + .body("events[0].payment_id", is(CHARGE_ID)) + .body("events[0].state.status", is(CREATED.getStatus())) + .body("events[0].updated", is("2010-12-31T22:59:59.132Z")) + .body("events[0]._links.payment_url.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))); + } + + @Test + public void getPaymentEvents_Returns401_WhenUnauthorised() { + publicAuthMockClient.respondUnauthorised(); + + getPaymentEventsResponse(CHARGE_ID) + .statusCode(401); + } + + @Test + public void getPaymentEvents_returns404_whenConnectorRespondsWith404() throws IOException { + String paymentId = "ds2af2afd3df112"; + String errorMessage = "backend-error-message"; + connectorMockClient.respondChargeEventsNotFound(GATEWAY_ACCOUNT_ID, paymentId, errorMessage); + + assertEventsNotFoundResponse(getPaymentEventsResponse(paymentId)); + } + + @Test + public void getPaymentEvents_returns404_whenLedgerRespondsWith404() throws IOException { + String paymentId = "ds2af2afd3df112"; + String errorMessage = "backend-error-message"; + ledgerMockClient.respondTransactionEventsWithError(paymentId, errorMessage, SC_NOT_FOUND); + + assertEventsNotFoundResponse(getPaymentEventsResponse(paymentId, LEDGER_ONLY_STRATEGY)); + } + + private void assertEventsNotFoundResponse(ValidatableResponse response) throws IOException { + InputStream body = response + .statusCode(404) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0300")) + .assertThat("$.description", is("Not found")); + } + + @Test + public void getPaymentEvents_returns500_whenConnectorRespondsWithResponseOtherThan200Or404() throws IOException { + String paymentId = "ds2af2afd3df112"; + String errorMessage = "backend-error-message"; + connectorMockClient.respondWhenGetChargeEvents(GATEWAY_ACCOUNT_ID, paymentId, errorMessage, SC_NOT_ACCEPTABLE); + + assertErrorEventsResponse(getPaymentEventsResponse(paymentId, CONNECTOR_ONLY_STRATEGY)); + } + + @Test + public void getPaymentEvents_returns500_whenLedgerRespondsWithResponseOtherThan200Or404() throws IOException { + String paymentId = "ds2af2afd3df112"; + String errorMessage = "backend-error-message"; + ledgerMockClient.respondTransactionEventsWithError(paymentId, errorMessage, SC_NOT_ACCEPTABLE); + + assertErrorEventsResponse(getPaymentEventsResponse(paymentId, LEDGER_ONLY_STRATEGY)); + } + + private void assertErrorEventsResponse(ValidatableResponse response) throws IOException { + InputStream body = response + .statusCode(500) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0398")) + .assertThat("$.description", is("Downstream system error")); + } + + @Test + public void getPaymentThroughConnector_ReturnsPaymentWithCorporateCardSurcharge() { + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .withCorporateCardSurcharge(250L) + .withTotalAmount(AMOUNT + 250L) + .build()); + + assertPaymentCorporateCardSurcharge(getPaymentResponse(CHARGE_ID)); + } + + @Test + public void getPaymentThroughLedger_ReturnsPaymentWithCorporateCardSurcharge() { + ledgerMockClient.respondWithTransaction(CHARGE_ID, + getLedgerTransaction() + .withCorporateCardSurcharge(250L) + .withTotalAmount(AMOUNT + 250L) + .build()); + + assertPaymentCorporateCardSurcharge(getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY)); + } + + private void assertPaymentCorporateCardSurcharge(ValidatableResponse paymentResponse) { + paymentResponse + .statusCode(200) + .contentType(JSON) + .body("corporate_card_surcharge", is(250)) + .body("total_amount", is(AMOUNT + 250)); + } + + @Test + public void getPaymentThroughConnector_ReturnsPaymentWithFeeAndNetAmount() { + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .withFee(FEE) + .withNetAmount(NET_AMOUNT) + .build()); + + assertPaymentWithFeeAndNetAmount(getPaymentResponse(CHARGE_ID)); + } + + @Test + public void getPaymentThroughLedger_ReturnsPaymentWithFeeAndNetAmount() { + ledgerMockClient.respondWithTransaction(CHARGE_ID, + getLedgerTransaction() + .withFee(FEE) + .withNetAmount(NET_AMOUNT) + .build()); + + assertPaymentWithFeeAndNetAmount(getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY)); + } + + private void assertPaymentWithFeeAndNetAmount(ValidatableResponse paymentResponse) { + paymentResponse + .statusCode(200) + .contentType(JSON) + .body("fee", is(FEE.intValue())) + .body("net_amount", is(NET_AMOUNT.intValue())) + .body("amount", is(AMOUNT)); + } + + @Test + public void getPayment_ReturnsPaymentWithOutFeeAndNetAmount_IfNotAvailableFromConnector() { + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, getConnectorCharge().build()); + + assertPaymentWithoutFeeAndNetAmount(getPaymentResponse(CHARGE_ID)); + } + + @Test + public void getPaymentThroughLedger_ReturnsPaymentWithOutFeeAndNetAmount_IfNotAvailable() { + ledgerMockClient.respondWithTransaction(CHARGE_ID, getLedgerTransaction().build()); + + assertPaymentWithoutFeeAndNetAmount(getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY)); + } + + private void assertPaymentWithoutFeeAndNetAmount(ValidatableResponse paymentResponse) { + paymentResponse + .statusCode(200) + .contentType(JSON) + .body("containsKey('fee')", is(false)) + .body("containsKey('net_amount')", is(false)) + .body("amount", is(AMOUNT)); + } + + @Test + public void getPaymentWithNullCardTypeThroughConnector_ReturnsPaymentWithNullCardType() { + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .withState(AWAITING_CAPTURE_REQUEST) + .withCardDetails(new CardDetailsFromResponse("1234", "123456", "Mr. Payment", "12/19", BILLING_ADDRESS, CARD_BRAND_LABEL, null)) + .withCorporateCardSurcharge(0L) + .withTotalAmount(0L) + .build()); + + getPaymentResponse(CHARGE_ID) + .statusCode(200) + .contentType(JSON) + .body("card_details.card_type", is(nullValue())); + } + + @Test + public void getPaymentWithNullCardTypeThroughLedger_ReturnsPaymentWithNullCardType() { + ledgerMockClient.respondWithTransaction(CHARGE_ID, + getLedgerTransaction() + .withState(AWAITING_CAPTURE_REQUEST) + .withCardDetails(new CardDetailsFromResponse("1234", "123456", "Mr. Payment", "12/19", BILLING_ADDRESS, CARD_BRAND_LABEL, null)) + .withCorporateCardSurcharge(0L) + .withTotalAmount(0L) + .build()); + + getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY) + .statusCode(200) + .contentType(JSON) + .body("card_details.card_type", is(nullValue())); + } + + @Test + public void getPaymentThroughConnector_ReturnsPaymentWithCaptureUrl() { // only through connector (based on response links) + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .withState(AWAITING_CAPTURE_REQUEST) + .withCorporateCardSurcharge(0L) + .withTotalAmount(0L) + .build()); + + getPaymentResponse(CHARGE_ID) + .statusCode(200) + .contentType(JSON) + .body("_links.capture.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID) + "/capture")) + .body("_links.capture.method", is("POST")); + } + + @Test + public void getPaymentWithAuthorisationSummaryThroughLedger() { + AuthorisationSummary authorisationSummary = new AuthorisationSummary(new ThreeDSecure(true)); + ledgerMockClient.respondWithTransaction(CHARGE_ID, + getLedgerTransaction() + .withAuthorisationSummary(authorisationSummary) + .build()); + + ValidatableResponse response = getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY); + + assertCommonPaymentFields(response); + assertPaymentWithAuthorisationSummary(response); + } + + @Test + public void getPaymentWithAuthorisationSummaryThroughConnector() { + AuthorisationSummary authorisationSummary = new AuthorisationSummary(new ThreeDSecure(true)); + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .withAuthorisationSummary(authorisationSummary) + .build()); + + ValidatableResponse response = getPaymentResponse(CHARGE_ID); + + assertCommonPaymentFields(response); + assertConnectorOnlyPaymentFields(response); + assertPaymentWithAuthorisationSummary(response); + } + + @Test + public void getPaymentWithWalletTypeThroughLedger() { + ledgerMockClient.respondWithTransaction(CHARGE_ID, + getLedgerTransaction() + .withWalletType(Wallet.GOOGLE_PAY.toString()) + .build()); + + ValidatableResponse response = getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY); + + // PublicAPI expected response behaviour is to move wallet type into card_details + response.body(not(hasKey("wallet_type"))); + response.body("card_details.wallet_type", is(Wallet.GOOGLE_PAY.getTitleCase())); + } + + @Test + public void getPaymentWithWalletTypeThroughConnector() { + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .withWalletType(Wallet.APPLE_PAY.toString()) + .build()); + + ValidatableResponse response = getPaymentResponse(CHARGE_ID); + + assertConnectorOnlyPaymentFields(response); + // PublicAPI expected response behaviour is to move wallet type into card_details + response.body(not(hasKey("wallet_type"))); + response.body("card_details.wallet_type", is(Wallet.APPLE_PAY.getTitleCase())); + } + + @Test + public void getPaymentWithNoAuthorisationSummaryThroughConnector() { + AuthorisationSummary authorisationSummary = new AuthorisationSummary(new ThreeDSecure(true)); + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .build()); + + ValidatableResponse response = getPaymentResponse(CHARGE_ID); + + assertCommonPaymentFields(response); + assertConnectorOnlyPaymentFields(response); + response.body("authorisation_summary", is(nullValue())); + } + + @Test + public void getPaymentShouldIncludeCanRetryIfThePaymentWasRejected() { + ledgerMockClient.respondWithTransaction(CHARGE_ID, + getLedgerTransaction() + .withState(REJECTED) + .withAuthorisationMode(AuthorisationMode.AGREEMENT) + .build()); + + getPaymentResponse(CHARGE_ID, LEDGER_ONLY_STRATEGY) + .statusCode(200) + .contentType(JSON) + .body("authorisation_mode", is("agreement")) + .body("state.can_retry", is(true)); + } + + @Test + public void getRejectedPaymentShouldIncludeCanRetryWhenThroughConnector() { + connectorMockClient.respondWithChargeFound(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, + getConnectorCharge() + .withState(REJECTED) + .withAuthorisationMode(AuthorisationMode.AGREEMENT) + .build()); + + getPaymentResponse(CHARGE_ID) + .statusCode(200) + .contentType(JSON) + .body("authorisation_mode", is("agreement")) + .body("state.can_retry", is(true)); + } + + private ChargeResponseFromConnector.ChargeResponseFromConnectorBuilder getConnectorCharge() { + return aCreateOrGetChargeResponseFromConnector() + .withAmount(AMOUNT) + .withChargeId(CHARGE_ID) + .withState(CAPTURED) + .withReturnUrl(RETURN_URL) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withEmail(EMAIL) + .withPaymentProvider(PAYMENT_PROVIDER) + .withCreatedDate(CREATED_DATE) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(true) + .withMoto(false) + .withRefundSummary(REFUND_SUMMARY) + .withSettlementSummary(SETTLEMENT_SUMMARY_CONNECTOR) + .withCardDetails(CARD_DETAILS) + .withGatewayTransactionId(GATEWAY_TRANSACTION_ID); + } + + private TransactionFromLedgerFixture.TransactionFromLedgerBuilder getLedgerTransaction() { + return aTransactionFromLedgerFixture() + .withAmount((long) AMOUNT) + .withTransactionId(CHARGE_ID) + .withState(CAPTURED) + .withReturnUrl(RETURN_URL) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withEmail(EMAIL) + .withPaymentProvider(PAYMENT_PROVIDER) + .withCreatedDate(CREATED_DATE) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(true) + .withMoto(false) + .withRefundSummary(REFUND_SUMMARY) + .withSettlementSummary(SETTLEMENT_SUMMARY_LEDGER) + .withCardDetails(CARD_DETAILS) + .withGatewayTransactionId(GATEWAY_TRANSACTION_ID); + } + + private ValidatableResponse getPaymentResponse(String paymentId) { + return getPaymentResponse(paymentId, ""); + } + + private ValidatableResponse getPaymentResponse(String paymentId, String strategy) { + return given().port(app.getLocalPort()) + .header("X-Ledger", strategy) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .get(PAYMENTS_PATH + paymentId) + .then(); + } + + private ValidatableResponse getPaymentEventsResponse(String paymentId) { + return getPaymentEventsResponse(paymentId, ""); + } + + private ValidatableResponse getPaymentEventsResponse(String paymentId, String strategy) { + return given().port(app.getLocalPort()) + .header("X-Ledger", strategy) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .get(String.format("/v1/payments/%s/events", paymentId)) + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceSearchIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceSearchIT.java new file mode 100644 index 000000000..816c9180d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PaymentsResourceSearchIT.java @@ -0,0 +1,470 @@ +package uk.gov.pay.api.it; + +import com.google.common.collect.ImmutableMap; +import io.restassured.response.ValidatableResponse; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.it.fixtures.PaymentNavigationLinksFixture; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.PaymentSettlementSummary; +import uk.gov.pay.api.model.Wallet; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.LedgerMockClient; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.validation.DateTimeUtils; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.apache.http.HttpHeaders.CACHE_CONTROL; +import static org.apache.http.HttpHeaders.PRAGMA; +import static org.eclipse.jetty.http.HttpStatus.OK_200; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.it.fixtures.PaginatedPaymentSearchResultFixture.aPaginatedPaymentSearchResult; +import static uk.gov.pay.api.it.fixtures.PaymentSearchResultBuilder.DEFAULT_AMOUNT; +import static uk.gov.pay.api.it.fixtures.PaymentSearchResultBuilder.DEFAULT_CAPTURED_DATE; +import static uk.gov.pay.api.it.fixtures.PaymentSearchResultBuilder.DEFAULT_CAPTURE_SUBMIT_TIME; +import static uk.gov.pay.api.it.fixtures.PaymentSearchResultBuilder.DEFAULT_CREATED_DATE; +import static uk.gov.pay.api.it.fixtures.PaymentSearchResultBuilder.DEFAULT_PAYMENT_PROVIDER; +import static uk.gov.pay.api.it.fixtures.PaymentSearchResultBuilder.DEFAULT_RETURN_URL; +import static uk.gov.pay.api.it.fixtures.PaymentSearchResultBuilder.DEFAULT_SETTLED_DATE; +import static uk.gov.pay.api.it.fixtures.PaymentSearchResultBuilder.aSuccessfulSearchPayment; +import static uk.gov.pay.api.utils.Urls.paymentLocationFor; + +public class PaymentsResourceSearchIT extends PaymentResourceITestBase { + + private static final String TEST_REFERENCE = "test_reference"; + private static final String TEST_EMAIL = "alice.111@mail.fake"; + private static final String TEST_FIRST_DIGITS_CARD_NUMBER = "123456"; + private static final String TEST_LAST_DIGITS_CARD_NUMBER = "1234"; + private static final String TEST_CARDHOLDER_NAME = "Mr. Payment"; + private static final String TEST_STATE = "created"; + private static final String TEST_CARD_BRAND_LABEL = "Mastercard"; + private static final String TEST_CARD_TYPE = "credit"; + private static final String TEST_FROM_DATE = "2016-01-28T00:00:00Z"; + private static final String TEST_TO_DATE = "2016-01-28T12:00:00Z"; + private static final String SEARCH_PATH = "/v1/payments"; + private static final Address BILLING_ADDRESS = new Address("line1", "line2", "NR2 5 6EG", "city", "UK"); + private static final CardDetailsFromResponse CARD_DETAILS = new CardDetailsFromResponse(TEST_LAST_DIGITS_CARD_NUMBER, TEST_FIRST_DIGITS_CARD_NUMBER, TEST_CARDHOLDER_NAME, "12/19", BILLING_ADDRESS, TEST_CARD_BRAND_LABEL, TEST_CARD_TYPE); + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + private LedgerMockClient ledgerMockClient = new LedgerMockClient(ledgerMock); + + @Before + public void mapBearerTokenToAccountId() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void searchPaymentsWithMetadata() { + String payments = aPaginatedPaymentSearchResult() + .withCount(2) + .withPage(1) + .withTotal(2) + .withPayments(aSuccessfulSearchPayment() + .withInProgressState(TEST_STATE) + .withReference(TEST_REFERENCE) + .withCardDetails(CARD_DETAILS) + .withNumberOfResults(2) + .withEmail(TEST_EMAIL) + .withMetadata(Map.of("reconciled", true, "ledger_code", 123, "fuh", "fuh you", "surcharge", 1.23)) + .getResults()) + .build(); + + ledgerMockClient.respondOk_whenSearchCharges(payments); + + searchPayments(Map.of()) + .statusCode(200) + .header(PRAGMA, "no-cache") + .header(CACHE_CONTROL, "no-store") + .contentType(JSON) + .body("results[0].metadata.reconciled", is(true)) + .body("results[0].metadata.ledger_code", is(123)) + .body("results[0].metadata.fuh", is("fuh you")) + .body("results[0].metadata.surcharge", is(1.23f)) + .body("results[1].metadata.reconciled", is(true)) + .body("results[1].metadata.ledger_code", is(123)) + .body("results[1].metadata.fuh", is("fuh you")) + .body("results[1].metadata.surcharge", is(1.23f)); + } + + @Test + public void searchPayments_shouldOnlyReturnAllowedProperties() { + String payments = aPaginatedPaymentSearchResult() + .withCount(10) + .withPage(2) + .withTotal(20) + .withPayments(aSuccessfulSearchPayment() + .withInProgressState(TEST_STATE) + .withReference(TEST_REFERENCE) + .withReturnUrl(DEFAULT_RETURN_URL) + .withCardDetails(CARD_DETAILS) + .withDelayedCapture(true) + .withNumberOfResults(1) + .withEmail(TEST_EMAIL) + .withFee(5L) + .withNetAmount(9995L) + .withGatewayTransactionId("gateway-tx-123456") + .getResults()) + .build(); + + ledgerMockClient.respondOk_whenSearchCharges(payments); + + searchPayments(ImmutableMap.of("reference", TEST_REFERENCE)) + .statusCode(200) + .contentType(JSON) + .body("results[0].created_date", is(DEFAULT_CREATED_DATE)) + .body("results[0].reference", is(TEST_REFERENCE)) + .body("results[0].email", is(TEST_EMAIL)) + .body("results[0].return_url", is(DEFAULT_RETURN_URL)) + .body("results[0].description", is("description-0")) + .body("results[0].state.status", is(TEST_STATE)) + .body("results[0].amount", is(DEFAULT_AMOUNT)) + .body("results[0].fee", is(5)) + .body("results[0].net_amount", is(9995)) + .body("results[0].payment_provider", is(DEFAULT_PAYMENT_PROVIDER)) + .body("results[0].payment_id", is("0")) + .body("results[0].language", is("en")) + .body("results[0].delayed_capture", is(true)) + .body("results[0].provider_id", is("gateway-tx-123456")) + .body("results[0]._links.self.method", is("GET")) + .body("results[0]._links.self.href", is(paymentLocationFor(configuration.getBaseUrl(), "0"))) + .body("results[0]._links.self", not(hasKey("type"))) + .body("results[0]._links.self", not(hasKey("params"))) + .body("results[0]._links.events.href", is(paymentEventsLocationFor("0"))) + .body("results[0]._links.events.method", is("GET")) + .body("results[0]._links.cancel.href", is(paymentCancelLocationFor("0"))) + .body("results[0]._links.cancel.method", is("POST")) + .body("results[0]._links.refunds.href", is(paymentRefundsLocationFor("0"))) + .body("results[0]._links.refunds.method", is("GET")) + .body("results[0]._links", not(hasKey("next_url"))) + .body("results[0].refund_summary.status", is("available")) + .body("results[0].refund_summary.amount_available", is(100)) + .body("results[0].refund_summary.amount_submitted", is(300)) + .body("results[0].settlement_summary.capture_submit_time", is(DEFAULT_CAPTURE_SUBMIT_TIME)) + .body("results[0].settlement_summary.captured_date", is(DEFAULT_CAPTURED_DATE)) + .body("results[0].card_details.card_brand", is(TEST_CARD_BRAND_LABEL)) + .body("results[0].card_details.cardholder_name", is(CARD_DETAILS.getCardHolderName())) + .body("results[0].card_details.expiry_date", is(CARD_DETAILS.getExpiryDate())) + .body("results[0].card_details.last_digits_card_number", is(CARD_DETAILS.getLastDigitsCardNumber())) + .body("results[0].card_details.first_digits_card_number", is(CARD_DETAILS.getFirstDigitsCardNumber())) + .body("results[0].card_details.billing_address.line1", is(CARD_DETAILS.getBillingAddress().get().getLine1())) + .body("results[0].card_details.billing_address.line2", is(CARD_DETAILS.getBillingAddress().get().getLine2())) + .body("results[0].card_details.billing_address.postcode", is(CARD_DETAILS.getBillingAddress().get().getPostcode())) + .body("results[0].card_details.billing_address.country", is(CARD_DETAILS.getBillingAddress().get().getCountry())) + .body("results[0].card_details.card_brand", is(CARD_DETAILS.getCardBrand())) + .body("results[0].card_details", hasKey("card_type")) + .body("results[0].metadata", is(nullValue())) + .body("results[0].authorisation_mode", is("web")); + } + + @Test + public void searchPayments_ShouldNotIncludeCancelLinkIfThePaymentCannotBeCancelled() { + String payments = aPaginatedPaymentSearchResult() + .withCount(10) + .withPage(2) + .withTotal(20) + .withPayments(aSuccessfulSearchPayment() + .withSuccessState("success") + .withReference(TEST_REFERENCE) + .withNumberOfResults(1) + .getResults()) + .build(); + + ledgerMockClient.respondOk_whenSearchCharges(payments); + + searchPayments(ImmutableMap.of("reference", TEST_REFERENCE)) + .statusCode(200) + .contentType(JSON) + .body("results[0]._links.cancel", is(nullValue())); + } + + @Test + public void searchPayments_ShouldIncludeCanRetryIfThePaymentWasRejected() { + String payments = aPaginatedPaymentSearchResult() + .withCount(10) + .withPage(2) + .withTotal(20) + .withPayments(aSuccessfulSearchPayment() + .withRejectedState("failed", "P0010", "Payment method rejected", true) + .withReference(TEST_REFERENCE) + .withAuthorisationMode(AuthorisationMode.AGREEMENT) + .withNumberOfResults(1) + .getResults()) + .build(); + + ledgerMockClient.respondOk_whenSearchCharges(payments); + + searchPayments(ImmutableMap.of("reference", TEST_REFERENCE)) + .statusCode(200) + .contentType(JSON) + .body("results[0].authorisation_mode", is("agreement")) + .body("results[0].state.can_retry", is(true)); + } + + @Test + public void searchPayments_getsPaginatedResults() { + PaymentNavigationLinksFixture links = new PaymentNavigationLinksFixture() + .withPrevLink("http://server:port/path?query=prev&from_date=2016-01-01T23:59:59Z") + .withNextLink("http://server:port/path?query=next&from_date=2016-01-01T23:59:59Z") + .withSelfLink("http://server:port/path?query=self&from_date=2016-01-01T23:59:59Z") + .withFirstLink("http://server:port/path?query=first&from_date=2016-01-01T23:59:59Z") + .withLastLink("http://server:port/path?query=last&from_date=2016-01-01T23:59:59Z"); + + String payments = aPaginatedPaymentSearchResult() + .withCount(10) + .withPage(2) + .withTotal(40) + .withPayments(aSuccessfulSearchPayment() + .withReference(TEST_REFERENCE) + .withInProgressState(TEST_STATE) + .withCreatedDateBetween(TEST_FROM_DATE, TEST_TO_DATE) + .withNumberOfResults(10) + .withEmail(TEST_EMAIL) + .getResults()) + .withLinks(links) + .build(); + + ledgerMockClient.respondOk_whenSearchCharges(payments); + + ImmutableMap queryParams = ImmutableMap.of( + "reference", TEST_REFERENCE, + "state", TEST_STATE, + "email", TEST_EMAIL, + "page", "2", + "display_size", "10" + ); + ValidatableResponse response = searchPayments(queryParams) + .statusCode(200) + .contentType(JSON) + .body("results.size()", equalTo(10)) + .body("total", is(40)) + .body("count", is(10)) + .body("page", is(2)) + .body("_links.next_page.href", is(expectedChargesLocationFor("?from_date=2016-01-01T23%3A59%3A59Z&query=next"))) + .body("_links.prev_page.href", is(expectedChargesLocationFor("?from_date=2016-01-01T23%3A59%3A59Z&query=prev"))) + .body("_links.first_page.href", is(expectedChargesLocationFor("?from_date=2016-01-01T23%3A59%3A59Z&query=first"))) + .body("_links.last_page.href", is(expectedChargesLocationFor("?from_date=2016-01-01T23%3A59%3A59Z&query=last"))) + .body("_links.self.href", is(expectedChargesLocationFor("?from_date=2016-01-01T23%3A59%3A59Z&query=self"))); + + List> results = response.extract().body().jsonPath().getList("results"); + assertThat(results, matchesField("reference", TEST_REFERENCE)); + assertThat(results, matchesField("email", TEST_EMAIL)); + assertThat(results, matchesState(TEST_STATE)); + assertThat(results, matchesCreatedDateInBetween(TEST_FROM_DATE, TEST_TO_DATE)); + } + + private String expectedChargesLocationFor(String queryParams) { + return "http://publicapi.url" + SEARCH_PATH + queryParams; + } + + @Test + public void searchPayments_errorIfLedgerRespondsWith404() { + searchPayments( + ImmutableMap.of("reference", TEST_REFERENCE, "state", TEST_STATE, "from_date", TEST_FROM_DATE, "to_date", TEST_TO_DATE)) + .statusCode(404) + .contentType(JSON) + .body("size()", is(2)) + .body("code", is("P0402")) + .body("description", is("Page not found")); + } + + @Test + public void searchPayments_errorIfLedgerResponseIsInvalid() throws Exception { + ledgerMockClient.whenSearchTransactions( + aResponse().withStatus(OK_200).withHeader(CONTENT_TYPE, APPLICATION_JSON).withBody("wtf")); + + searchPayments( + ImmutableMap.of( + "reference", TEST_REFERENCE, + "email", TEST_EMAIL, + "state", TEST_STATE, + "from_date", TEST_FROM_DATE, + "to_date", TEST_TO_DATE)) + .statusCode(500) + .contentType(JSON) + .body("size()", is(2)) + .body("code", is("P0498")) + .body("description", is("Downstream system error")) + .extract(); + } + + @Test + public void searchPayments_filterByInvalidCardBrand() throws Exception { + searchPayments( + ImmutableMap.of("card_brand", "my_credit_card")) + .statusCode(404) + .contentType(JSON) + .body("size()", is(2)) + .body("description", is("Page not found")); + } + + @Test + public void searchPayments_getsResults_withNoBillingAddressAndNoWalletType() { + String payments = aPaginatedPaymentSearchResult() + .withPayments(aSuccessfulSearchPayment() + .withCardDetails(new CardDetailsFromResponse("1234", + "1234", + "Card Holder", + "11/21", + null, + "Visa", + "credit")) + .withWalletType(null) + .withNumberOfResults(1) + .getResults()) + .build(); + + ledgerMockClient.respondOk_whenSearchCharges(payments); + + ImmutableMap queryParams = ImmutableMap.of(); + searchPayments(queryParams) + .statusCode(200) + .contentType(JSON) + .body("results[0].card_details.billing_address", is(nullValue())) + .body("results[0].card_details.wallet_type", is(nullValue())) + .body("results[0].card_details.first_digits_card_number", is("1234")); + } + + @Test + public void searchPayments_getsResults_withWalletType() { + String payments = aPaginatedPaymentSearchResult() + .withPayments(aSuccessfulSearchPayment() + .withWalletType(Wallet.APPLE_PAY.toString()) + .withNumberOfResults(1) + .getResults()) + .build(); + + ledgerMockClient.respondOk_whenSearchCharges(payments); + + ImmutableMap queryParams = ImmutableMap.of(); + searchPayments(queryParams) + .statusCode(200) + .contentType(JSON) + .body("results[0]", not(hasKey("wallet_type"))) + .body("results[0].card_details", hasKey("wallet_type")) + .body("results[0].card_details.wallet_type", is(Wallet.APPLE_PAY.getTitleCase())); + } + + @Test + public void shouldReturnEmptyArray_ifLedgerReturnsNoResult() { + String payments = aPaginatedPaymentSearchResult() + .withPage(1) + .withPayments(aSuccessfulSearchPayment() + .withNumberOfResults(0) + .getResults()) + .build(); + + ledgerMockClient.respondOk_whenSearchCharges(payments); + + searchPayments(ImmutableMap.of( + "reference", "junk yard", + "email", TEST_EMAIL, + "state", TEST_STATE, + "from_date", TEST_FROM_DATE, + "to_date", TEST_TO_DATE, + "agreement_id", "does_not_exist")) + .statusCode(200) + .contentType(JSON) + .body("results.size()", is(0)) + .body("total", is(0)) + .body("count", is(0)) + .body("page", is(1)); + } + + @Test + public void shouldReturnSettledDate_whenLedgerReturnsSettledDateInSettlementSummary() { + String payments = aPaginatedPaymentSearchResult() + .withPage(1) + .withPayments(aSuccessfulSearchPayment() + .withSettlementSummary(new PaymentSettlementSummary(DEFAULT_CAPTURE_SUBMIT_TIME, + DEFAULT_CAPTURED_DATE, DEFAULT_SETTLED_DATE)) + .withNumberOfResults(1) + .getResults()) + .build(); + ledgerMockClient.respondOk_whenSearchCharges(payments); + + searchPayments(Map.of()).statusCode(200) + .contentType(JSON) + .body("results[0].settlement_summary.settled_date", is(DEFAULT_SETTLED_DATE)) + .body("results[0].settlement_summary.captured_date", is(DEFAULT_CAPTURED_DATE)) + .body("results[0].settlement_summary.capture_submit_time", is(DEFAULT_CAPTURE_SUBMIT_TIME)); + } + + private Matcher>> matchesState(final String state) { + return new TypeSafeMatcher<>() { + @Override + protected boolean matchesSafely(List> maps) { + return maps.stream().allMatch(result -> state.equals(((Map) result.get("state")).get("status"))); + } + + @Override + public void describeTo(Description description) { + description.appendText(String.format("result state did not match %s", state)); + } + }; + } + + private Matcher>> matchesField(final String field, final String value) { + return new TypeSafeMatcher>>() { + @Override + protected boolean matchesSafely(List> maps) { + return maps.stream().allMatch(result -> value.equals(result.get(field))); + } + + @Override + public void describeTo(Description description) { + description.appendText(String.format("not all result %s match %s", field, value)); + } + }; + } + + private Matcher>> matchesCreatedDateInBetween(final String fromDate, final String toDate) { + return new TypeSafeMatcher>>() { + @Override + protected boolean matchesSafely(List> results) { + return results.stream().allMatch(result -> { + ZonedDateTime createdDate = zonedDateTimeOf(result.get("created_date").toString()); + return createdDate.isAfter(zonedDateTimeOf(fromDate)) && createdDate.isBefore(zonedDateTimeOf(toDate)); + } + ); + } + + @Override + public void describeTo(Description description) { + description.appendText(String.format("created date of results does not fall in between %s and %s", fromDate, toDate)); + } + }; + } + + private ZonedDateTime zonedDateTimeOf(String dateString) { + return DateTimeUtils.toUTCZonedDateTime(dateString).get(); + } + + private ValidatableResponse searchPayments(Map queryParams) { + return given().port(app.getLocalPort()) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .queryParams(queryParams) + .get(SEARCH_PATH) + .then(); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PingIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PingIT.java new file mode 100644 index 000000000..bd11bfa7d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/PingIT.java @@ -0,0 +1,25 @@ +package uk.gov.pay.api.it; + +import io.dropwizard.testing.junit.DropwizardAppRule; +import org.junit.Rule; +import org.junit.Test; +import uk.gov.pay.api.app.PublicApi; +import uk.gov.pay.api.app.config.PublicApiConfig; + +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +public class PingIT { + + @Rule + public DropwizardAppRule app = new DropwizardAppRule<>(PublicApi.class, resourceFilePath("config/test-config.yaml")); + + @Test + public void testPing() { + given().port(app.getAdminPort()) + .get("/healthcheck") + .then() + .body("ping.healthy", is(true)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/RequestDeniedResourceIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/RequestDeniedResourceIT.java new file mode 100644 index 000000000..c1b391b01 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/RequestDeniedResourceIT.java @@ -0,0 +1,87 @@ +package uk.gov.pay.api.it; + +import com.jayway.jsonassert.JsonAssert; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.core.Is.is; + +public class RequestDeniedResourceIT extends PaymentResourceITestBase { + + @Test + public void requestDeniedPost() throws IOException { + + InputStream body = given().port(app.getLocalPort()) + .header("x-naxsi_sig", "rules violated") + .post("request-denied") + .then() + .statusCode(400) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0920")) + .assertThat("$.description", is("Request blocked by security rules. Please consult API documentation for more information.")); + + } + + @Test + public void requestDeniedGet() throws IOException { + + InputStream body = given().port(app.getLocalPort()) + .header("x-naxsi_sig", "rules violated") + .get("request-denied") + .then() + .statusCode(400) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0920")) + .assertThat("$.description", is("Request blocked by security rules. Please consult API documentation for more information.")); + + } + + @Test + public void requestDeniedPut() throws IOException { + + InputStream body = given().port(app.getLocalPort()) + .header("x-naxsi_sig", "rules violated") + .put("request-denied") + .then() + .statusCode(400) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0920")) + .assertThat("$.description", is("Request blocked by security rules. Please consult API documentation for more information.")); + + } + + @Test + public void requestDeniedDelete() throws IOException { + + InputStream body = given().port(app.getLocalPort()) + .header("x-naxsi_sig", "rules violated") + .delete("request-denied") + .then() + .statusCode(400) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0920")) + .assertThat("$.description", is("Request blocked by security rules. Please consult API documentation for more information.")); + + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ResourcesFilterAuthorisationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ResourcesFilterAuthorisationIT.java new file mode 100644 index 000000000..5ec3ae38a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ResourcesFilterAuthorisationIT.java @@ -0,0 +1,83 @@ +package uk.gov.pay.api.it; + +import com.google.common.collect.ImmutableMap; +import io.restassured.response.ValidatableResponse; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; + +public class ResourcesFilterAuthorisationIT extends ResourcesFilterITestBase { + + @Test + public void createPayment_whenInvalidAuthorizationHeader_shouldReturn401Response() throws Exception { + + List> tasks = Arrays.asList( + () -> postPaymentResponse("InvalidToken", PAYLOAD), + () -> postPaymentResponse("InvalidToken", PAYLOAD) + ); + + List finishedTasks = invokeAll(tasks); + + finishedTasks.get(0).statusCode(401); + finishedTasks.get(1).statusCode(401); + } + + @Test + public void getPayment_whenInvalidAuthorizationHeader_shouldReturn401Response() throws Exception { + + List> tasks = Arrays.asList( + () -> getPaymentResponse("InvalidToken2"), + () -> getPaymentResponse("InvalidToken2") + ); + + List finishedTasks = invokeAll(tasks); + + finishedTasks.get(0).statusCode(401); + finishedTasks.get(1).statusCode(401); + } + + @Test + public void getPaymentEvents_whenInvalidAuthorizationHeader_shouldReturn401Response() throws Exception { + + List> tasks = Arrays.asList( + () -> getPaymentEventsResponse("InvalidToken3"), + () -> getPaymentEventsResponse("InvalidToken3") + ); + + List finishedTasks = invokeAll(tasks); + + finishedTasks.get(0).statusCode(401); + finishedTasks.get(1).statusCode(401); + } + + @Test + public void searchPayments_whenInvalidAuthorizationHeader_shouldReturn401Response() throws Exception { + + List> tasks = Arrays.asList( + () -> searchPayments("InvalidToken5", ImmutableMap.of("reference", REFERENCE)), + () -> searchPayments("InvalidToken5", ImmutableMap.of("reference", REFERENCE)) + ); + + List finishedTasks = invokeAll(tasks); + + finishedTasks.get(0).statusCode(401); + finishedTasks.get(1).statusCode(401); + } + + @Test + public void cancelPayment_whenInvalidAuthorizationHeader_shouldReturn401Response() throws Exception { + + List> tasks = Arrays.asList( + () -> postCancelPaymentResponse("InvalidToken6"), + () -> postCancelPaymentResponse("InvalidToken6") + ); + + List finishedTasks = invokeAll(tasks); + + finishedTasks.get(0).statusCode(401); + finishedTasks.get(1).statusCode(401); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ResourcesFilterITestBase.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ResourcesFilterITestBase.java new file mode 100644 index 000000000..24d140e5b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ResourcesFilterITestBase.java @@ -0,0 +1,197 @@ +package uk.gov.pay.api.it; + +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.google.common.collect.ImmutableMap; +import io.dropwizard.testing.junit.DropwizardAppRule; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import io.restassured.response.ValidatableResponse; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import uk.gov.pay.api.app.PublicApi; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.it.rule.RedisDockerRule; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.utils.ApiKeyGenerator; +import uk.gov.pay.api.utils.ChargeEventBuilder; +import uk.gov.pay.api.utils.JsonStringBuilder; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.service.payments.commons.validation.DateTimeUtils; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +import static io.dropwizard.testing.ConfigOverride.config; +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.eclipse.jetty.http.HttpStatus.TOO_MANY_REQUESTS_429; +import static org.junit.Assert.fail; +import static uk.gov.service.payments.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION; +import static uk.gov.service.payments.commons.testing.port.PortFactory.findFreePort; + +abstract public class ResourcesFilterITestBase { + + static final String API_KEY = ApiKeyGenerator.apiKeyValueOf("TEST_BEARER_TOKEN", "qwer9yuhgf"); //Must use same secret set in test-config.xml's apiKeyHmacSecret + static final ZonedDateTime TIMESTAMP = DateTimeUtils.toUTCZonedDateTime("2016-01-01T12:00:00Z").get(); + static final String PAYMENTS_PATH = "/v1/payments/"; + static final int AMOUNT = 9999999; + static final String CHARGE_ID = "ch_ab2341da231434l"; + static final PaymentState CREATED = new PaymentState("created", false, null, null); + static final String RETURN_URL = "https://somewhere.gov.uk/rainbow/1"; + static final String REFERENCE = "Some reference"; + static final String DESCRIPTION = "Some description"; + static final String CREATED_DATE = ISO_INSTANT_MILLISECOND_PRECISION.format(TIMESTAMP); + static final List> EVENTS = List.of(new ChargeEventBuilder(CREATED, CREATED_DATE).build()); + static final String GATEWAY_ACCOUNT_ID = "GATEWAY_ACCOUNT_ID"; + static final String PAYLOAD = paymentPayload(); + + private static final int CONNECTOR_PORT = findFreePort(); + private static final int PUBLIC_AUTH_PORT = findFreePort(); + private static final int LEDGER_PORT = findFreePort(); + + private final ExecutorService executor = Executors.newFixedThreadPool(2); + + @ClassRule + public static RedisDockerRule redisDockerRule = new RedisDockerRule(); + + @ClassRule + public static WireMockClassRule connectorMock = new WireMockClassRule(CONNECTOR_PORT); + + @ClassRule + public static WireMockClassRule ledgerMock = new WireMockClassRule(LEDGER_PORT); + + @ClassRule + public static WireMockClassRule publicAuthMock = new WireMockClassRule(PUBLIC_AUTH_PORT); + + @Rule + public DropwizardAppRule app = new DropwizardAppRule<>( + PublicApi.class, + resourceFilePath("config/test-config.yaml"), + config("connectorUrl", "http://localhost:" + CONNECTOR_PORT), + config("publicAuthUrl", "http://localhost:" + PUBLIC_AUTH_PORT + "/v1/auth"), + config("redis.endpoint", redisDockerRule.getRedisUrl()), + config("ledgerUrl", "http://localhost:" + LEDGER_PORT), + config("rateLimiter.noOfReq", "1"), + config("rateLimiter.noOfReqForPost", "1") + ); + + private final PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setupApiKey() { + redisDockerRule.clearCache(); + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + List invokeAll(List> tasks) throws InterruptedException { + return executor.invokeAll(tasks) + .stream() + .map(future -> { + try { + return future.get(); + } catch (Exception e) { + fail("Test fail with exception calling resource"); + return null; + } + }) + .collect(Collectors.toList()); + } + + TypeSafeMatcher aResponse(final int statusCode) { + return new TypeSafeMatcher<>() { + @Override + protected boolean matchesSafely(ValidatableResponse validatableResponse) { + return validatableResponse.extract().statusCode() == statusCode; + } + + @Override + public void describeTo(Description description) { + description.appendText(" Status code: ") + .appendValue(statusCode); + } + }; + } + + TypeSafeMatcher anErrorResponse() { + return new TypeSafeMatcher<>() { + @Override + protected boolean matchesSafely(ValidatableResponse validatableResponse) { + ExtractableResponse extract = validatableResponse.extract(); + return extract.statusCode() == TOO_MANY_REQUESTS_429 + && "P0900".equals(extract.body().path("code")) + && "Too many requests".equals(extract.body().path("description")); + } + + @Override + public void describeTo(Description description) { + description.appendText(" Status code: ") + .appendValue(TOO_MANY_REQUESTS_429) + .appendText(", error code: ") + .appendValue("P0900") + .appendText(", message: ") + .appendValue("Too many requests") + ; + } + }; + } + + private static String paymentPayload() { + return new JsonStringBuilder() + .add("amount", (long) ResourcesFilterITestBase.AMOUNT) + .add("reference", ResourcesFilterITestBase.REFERENCE) + .add("description", ResourcesFilterITestBase.DESCRIPTION) + .add("return_url", ResourcesFilterITestBase.RETURN_URL) + .build(); + } + + ValidatableResponse getPaymentResponse(String bearerToken) { + return given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + bearerToken) + .get(PAYMENTS_PATH + CHARGE_ID) + .then(); + } + + ValidatableResponse getPaymentEventsResponse(String bearerToken) { + return given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + bearerToken) + .get(String.format("/v1/payments/%s/events", CHARGE_ID)) + .then(); + } + + protected ValidatableResponse postPaymentResponse(String bearerToken, String payload) { + return given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + bearerToken) + .post(PAYMENTS_PATH) + .then(); + } + + ValidatableResponse searchPayments(String bearerToken, ImmutableMap queryParams) { + return given().port(app.getLocalPort()) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + bearerToken) + .queryParams(queryParams) + .get("/v1/payments") + .then(); + } + + ValidatableResponse postCancelPaymentResponse(String bearerToken) { + return given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + bearerToken) + .post(String.format("/v1/payments/%s/cancel", CHARGE_ID)) + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ResourcesFilterLocalRateLimiterIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ResourcesFilterLocalRateLimiterIT.java new file mode 100644 index 000000000..706bd6f5c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ResourcesFilterLocalRateLimiterIT.java @@ -0,0 +1,184 @@ +package uk.gov.pay.api.it; + +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import io.dropwizard.testing.junit.DropwizardAppRule; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import io.restassured.response.ValidatableResponse; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import uk.gov.pay.api.app.PublicApi; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.RefundSummary; +import uk.gov.pay.api.utils.ApiKeyGenerator; +import uk.gov.pay.api.utils.JsonStringBuilder; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.validation.DateTimeUtils; + +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +import static io.dropwizard.testing.ConfigOverride.config; +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.MatcherAssert.assertThat; +import static uk.gov.pay.api.utils.mocks.ChargeResponseFromConnector.ChargeResponseFromConnectorBuilder.aCreateOrGetChargeResponseFromConnector; +import static uk.gov.service.payments.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION; +import static uk.gov.service.payments.commons.testing.port.PortFactory.findFreePort; + +public class ResourcesFilterLocalRateLimiterIT { + + private static final int CONNECTOR_PORT = findFreePort(); + private static final int PUBLIC_AUTH_PORT = findFreePort(); + + private static final String API_KEY = ApiKeyGenerator.apiKeyValueOf("TEST_BEARER_TOKEN", "qwer9yuhgf"); + private static final String GATEWAY_ACCOUNT_ID = "GATEWAY_ACCOUNT_ID"; + private static final String PAYMENTS_PATH = "/v1/payments/"; + + private static final int AMOUNT = 9999999; + private static final String CHARGE_ID = "ch_ab2341da231434l"; + private static final String CHARGE_TOKEN_ID = "token_1234567asdf"; + private static final PaymentState CREATED = new PaymentState("created", false, null, null); + private static final RefundSummary REFUND_SUMMARY = new RefundSummary("pending", 100L, 50L); + private static final String PAYMENT_PROVIDER = "Sandbox"; + private static final String RETURN_URL = "https://somewhere.gov.uk/rainbow/1"; + private static final String REFERENCE = "Some reference"; + private static final String DESCRIPTION = "Some description"; + private static final ZonedDateTime TIMESTAMP = DateTimeUtils.toUTCZonedDateTime("2016-01-01T12:00:00Z").get(); + private static final String CREATED_DATE = ISO_INSTANT_MILLISECOND_PRECISION.format(TIMESTAMP); + private static final Address BILLING_ADDRESS = new Address("line1", "line2", "NR2 5 6EG", "city", "UK"); + private static final CardDetailsFromResponse CARD_DETAILS = new CardDetailsFromResponse("1234", "123456", "Mr. Payment", "12/19", BILLING_ADDRESS, "Visa", "credit"); + + private static final String PAYLOAD = paymentPayload(AMOUNT, RETURN_URL, DESCRIPTION, REFERENCE); + private ExecutorService executor = Executors.newFixedThreadPool(2); + + @ClassRule + public static WireMockClassRule connectorMock = new WireMockClassRule(CONNECTOR_PORT); + + @ClassRule + public static WireMockClassRule publicAuthMock = new WireMockClassRule(PUBLIC_AUTH_PORT); + + @ClassRule + public static DropwizardAppRule app = new DropwizardAppRule<>( + PublicApi.class + , resourceFilePath("config/test-config.yaml") + , config("connectorUrl", "http://localhost:" + CONNECTOR_PORT) + , config("publicAuthUrl", "http://localhost:" + PUBLIC_AUTH_PORT + "/v1/auth") + ); + + @Before + public void setup() { + ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + connectorMockClient.respondCreated_whenCreateCharge(CHARGE_TOKEN_ID, GATEWAY_ACCOUNT_ID, aCreateOrGetChargeResponseFromConnector() + .withAmount(AMOUNT) + .withChargeId(CHARGE_ID) + .withState(CREATED) + .withReturnUrl(RETURN_URL) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withPaymentProvider(PAYMENT_PROVIDER) + .withCreatedDate(CREATED_DATE) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(false) + .withRefundSummary(REFUND_SUMMARY) + .withCardDetails(CARD_DETAILS) + .build()); + } + + @Test + public void shouldFallbackToLocalRateLimiter_whenRedisIsUnavailableAndRateLimitIsReached_send429Response() throws Exception { + + List> tasks = Arrays.asList( + () -> postPaymentResponse(API_KEY, PAYLOAD), + () -> postPaymentResponse(API_KEY, PAYLOAD), + () -> postPaymentResponse(API_KEY, PAYLOAD) + ); + + List finishedTasks = invokeAll(tasks); + + assertThat(finishedTasks, hasItem(anErrorResponse(429, "P0900", "Too many requests"))); + } + + @Test + public void shouldFallbackToLocalRateLimiter_whenRedisIsUnavailableAndContinueRequest() { + + ValidatableResponse response = postPaymentResponse(API_KEY, PAYLOAD); + + response.statusCode(201); + } + + private List invokeAll(List> tasks) throws InterruptedException { + return executor.invokeAll(tasks) + .stream() + .map(future -> { + try { + return future.get(); + } catch (Exception e) { + return null; + } + }) + .collect(Collectors.toList()); + } + + private TypeSafeMatcher anErrorResponse(int statusCode, String publicApiErrorCode, String expectedDescription) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(ValidatableResponse validatableResponse) { + ExtractableResponse extract = validatableResponse.extract(); + return extract.statusCode() == statusCode + && publicApiErrorCode.equals(extract.body().path("code")) + && expectedDescription.equals(extract.body().path("description")); + } + + @Override + public void describeTo(Description description) { + description.appendText(" Status code: ") + .appendValue(statusCode) + .appendText(", error code: ") + .appendValue(publicApiErrorCode) + .appendText(", message: ") + .appendValue(expectedDescription) + ; + } + }; + } + + private static String paymentPayload(long amount, String returnUrl, String description, String reference) { + return new JsonStringBuilder() + .add("amount", amount) + .add("reference", reference) + .add("description", description) + .add("return_url", returnUrl) + .build(); + } + + private ValidatableResponse postPaymentResponse(String bearerToken, String payload) { + return given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + bearerToken) + .post(PAYMENTS_PATH) + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ResourcesFilterRateLimiterIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ResourcesFilterRateLimiterIT.java new file mode 100644 index 000000000..8841860d9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ResourcesFilterRateLimiterIT.java @@ -0,0 +1,169 @@ +package uk.gov.pay.api.it; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; +import com.google.common.collect.ImmutableMap; +import io.restassured.response.ValidatableResponse; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.slf4j.LoggerFactory; +import uk.gov.pay.api.filter.RateLimiterFilter; +import uk.gov.pay.api.it.fixtures.PaymentNavigationLinksFixture; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; +import uk.gov.pay.api.utils.mocks.LedgerMockClient; +import uk.gov.service.payments.commons.model.SupportedLanguage; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.core.IsIterableContaining.hasItem; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static uk.gov.pay.api.it.fixtures.PaginatedTransactionSearchResultFixture.aPaginatedTransactionSearchResult; +import static uk.gov.pay.api.it.fixtures.PaymentSearchResultBuilder.aSuccessfulSearchPayment; +import static uk.gov.pay.api.utils.mocks.ChargeResponseFromConnector.ChargeResponseFromConnectorBuilder.aCreateOrGetChargeResponseFromConnector; + +public class ResourcesFilterRateLimiterIT extends ResourcesFilterITestBase { + + private ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + private LedgerMockClient ledgerMockClient = new LedgerMockClient(ledgerMock); + ArgumentCaptor loggingEventArgumentCaptor = ArgumentCaptor.forClass(LoggingEvent.class); + private Appender mockAppender = mock(Appender.class); + + @Test + public void createPayment_whenRateLimitIsReached_shouldReturn429Response() throws Exception { + Logger logger = (Logger) LoggerFactory.getLogger(RateLimiterFilter.class); + logger.setLevel(Level.INFO); + logger.addAppender(mockAppender); + + connectorMockClient.respondCreated_whenCreateCharge("token_1234567asdf", GATEWAY_ACCOUNT_ID, aCreateOrGetChargeResponseFromConnector() + .withAmount(AMOUNT) + .withChargeId(CHARGE_ID) + .withState(CREATED) + .withReturnUrl(RETURN_URL) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withPaymentProvider("Sandbox") + .withCreatedDate(CREATED_DATE) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(false) + .withGatewayTransactionId("gatewayTxId") + .build()); + + List> tasks = Arrays.asList( + () -> postPaymentResponse(API_KEY, PAYLOAD), + () -> postPaymentResponse(API_KEY, PAYLOAD), + () -> postPaymentResponse(API_KEY, PAYLOAD), + () -> postPaymentResponse(API_KEY, PAYLOAD)); + + List finishedTasks = invokeAll(tasks); + + assertThat(finishedTasks, hasItem(aResponse(201))); + assertThat(finishedTasks, hasItem(anErrorResponse())); + + verify(mockAppender, atLeastOnce()).doAppend(loggingEventArgumentCaptor.capture()); + List logEvents = loggingEventArgumentCaptor.getAllValues(); + + // ensure api token link is in log MDC when requests are rate limited + assertThat(logEvents.get(0).getMDCPropertyMap().get("token_link"), is("a-token-link")); + } + + @Test + public void getPayment_whenRateLimitIsReached_shouldReturn429Response() throws Exception { + + connectorMockClient.respondWithChargeFound("token_1234567asdf", GATEWAY_ACCOUNT_ID, + aCreateOrGetChargeResponseFromConnector() + .withAmount(AMOUNT) + .withChargeId(CHARGE_ID) + .withState(CREATED) + .withReturnUrl(RETURN_URL) + .withDescription(DESCRIPTION) + .withReference(REFERENCE) + .withPaymentProvider("Sandbox") + .withCreatedDate(CREATED_DATE) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(false) + .build()); + + List> tasks = Arrays.asList( + () -> getPaymentResponse(API_KEY), + () -> getPaymentResponse(API_KEY), + () -> getPaymentResponse(API_KEY), + () -> getPaymentResponse(API_KEY) + ); + + List finishedTasks = invokeAll(tasks); + + assertThat(finishedTasks, hasItem(aResponse(200))); + assertThat(finishedTasks, hasItem(anErrorResponse())); + } + + @Test + public void getPaymentEvents_whenRateLimitIsReached_shouldReturn429Response() throws Exception { + + connectorMockClient.respondWithChargeEventsFound(GATEWAY_ACCOUNT_ID, CHARGE_ID, EVENTS); + + List> tasks = Arrays.asList( + () -> getPaymentEventsResponse(API_KEY), + () -> getPaymentEventsResponse(API_KEY), + () -> getPaymentEventsResponse(API_KEY), + () -> getPaymentEventsResponse(API_KEY) + ); + + List finishedTasks = invokeAll(tasks); + + assertThat(finishedTasks, hasItem(aResponse(200))); + assertThat(finishedTasks, hasItem(anErrorResponse())); + } + + @Test + public void searchPayments_whenRateLimitIsReached_shouldReturn429Response() throws Exception { + String payments = aPaginatedTransactionSearchResult() + .withCount(10) + .withPage(2) + .withTotal(20) + .withLinks(new PaymentNavigationLinksFixture().withSelfLink("/self")) + .withPayments(aSuccessfulSearchPayment() + .withInProgressState("created") + .withReference(REFERENCE) + .withNumberOfResults(1).getResults()) + .build(); + + ledgerMockClient.respondOk_whenSearchCharges(payments); + + List> tasks = Arrays.asList( + () -> searchPayments(API_KEY, ImmutableMap.of("reference", REFERENCE)), + () -> searchPayments(API_KEY, ImmutableMap.of("reference", REFERENCE)), + () -> searchPayments(API_KEY, ImmutableMap.of("reference", REFERENCE)) + ); + + List finishedTasks = invokeAll(tasks); + + assertThat(finishedTasks, hasItem(aResponse(200))); + assertThat(finishedTasks, hasItem(anErrorResponse())); + } + + @Test + public void cancelPayment_whenRateLimitIsReached_shouldReturn429Response() throws Exception { + + connectorMockClient.respondOk_whenCancelCharge(CHARGE_ID, GATEWAY_ACCOUNT_ID); + + List> tasks = Arrays.asList( + () -> postCancelPaymentResponse(API_KEY), + () -> postCancelPaymentResponse(API_KEY), + () -> postCancelPaymentResponse(API_KEY) + ); + + List finishedTasks = invokeAll(tasks); + + assertThat(finishedTasks, hasItem(aResponse(204))); + assertThat(finishedTasks, hasItem(anErrorResponse())); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/SearchDisputesResourceIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/SearchDisputesResourceIT.java new file mode 100644 index 000000000..f02b7cde8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/SearchDisputesResourceIT.java @@ -0,0 +1,121 @@ +package uk.gov.pay.api.it; + +import io.restassured.response.ValidatableResponse; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.model.ledger.DisputeSettlementSummary; +import uk.gov.pay.api.model.ledger.TransactionState; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.LedgerMockClient; + +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.utils.mocks.DisputeTransactionFromLedgerFixture.DisputeTransactionFromLedgerBuilder.aDisputeTransactionFromLedgerFixture; + +public class SearchDisputesResourceIT extends PaymentResourceITestBase { + + private static final String SEARCH_PATH = "/v1/disputes"; + private LedgerMockClient ledgerMockClient = new LedgerMockClient(ledgerMock); + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setUp() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void searchDisputes_shouldReturnValidResponse() { + var dispute = aDisputeTransactionFromLedgerFixture() + .withTransactionId("a-transaction-id") + .withAmount(1000L) + .withCreatedDate("2022-05-20T19:05:00.000Z") + .withEvidenceDueDate("2022-05-27T19:05:00.000Z") + .withFee(1500L) + .withNetAmount(-2500L) + .withParentTransactionId("a-parent-transaction-id") + .withReason("fraudulent") + .withSettlementSummary(new DisputeSettlementSummary("2022-05-27")) + .withState(new TransactionState("lost", true)) + .build(); + + ledgerMockClient.respondWithSearchDisputes(dispute); + + searchDisputes(Map.of()) + .statusCode(200) + .contentType(JSON) + .body("page", is(1)) + .body("total", is(1)) + .body("count", is(1)) + .body("links.self.href", containsString("/v1/disputes")) + .body("links.first_page.href", containsString("/v1/disputes")) + .body("links.last_page.href", containsString("/v1/disputes")) + + .body("results[0].amount", is(1000)) + .body("results[0].fee", is(1500)) + .body("results[0].reason", is("fraudulent")) + .body("results[0].status", is("lost")) + .body("results[0].created_date", is("2022-05-20T19:05:00.000Z")) + .body("results[0].dispute_id", is("a-transaction-id")) + .body("results[0].evidence_due_date", is("2022-05-27T19:05:00.000Z")) + .body("results[0].net_amount", is(-2500)) + .body("results[0].payment_id", is("a-parent-transaction-id")) + .body("results[0].settlement_summary.settled_date", is("2022-05-27")) + .body("results[0]._links.payment.href", containsString("/v1/payments/a-parent-transaction-id")) + .body("results[0]._links.payment.method", is("GET")); + } + + @Test + public void shouldError_whenInvalidSearchParams() { + Map queryParams = Map.of( + "from_date", "27th of July, 2022, 11:23", + "to_date", "27th of July, 2022, 13:05", + "from_settled_date", "3rd of July", + "to_settled_date", "30th of July", + "status", "disputed", + "page", "second", + "display_size", "short" + ); + + searchDisputes(queryParams) + .statusCode(422) + .contentType(JSON) + .body("code", is("P0401")) + .body("description", is("Invalid parameters: from_date, to_date, from_settled_date, to_settled_date, page, display_size, state. See Public API documentation for the correct data formats")); + } + + @Test + public void searchDisputesShouldReturnPageNotFound_whenLedgerRespondsWith404() { + ledgerMockClient.respondWithSearchDisputesNotFound(); + + searchDisputes(Map.of( + "status", "lost", + "page", "2")) + .statusCode(404) + .contentType(JSON) + .body("code", is ("P0402")) + .body("description", is("Page not found")); + } + + @Test + public void searchDisputes_shouldReturns401WhenUnauthorised() { + publicAuthMockClient.respondUnauthorised(); + + searchDisputes(Map.of()) + .statusCode(401); + } + + private ValidatableResponse searchDisputes(Map queryParams) { + return given().port(app.getLocalPort()) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .queryParams(queryParams) + .get(SEARCH_PATH) + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/SearchRefundsResourceIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/SearchRefundsResourceIT.java new file mode 100644 index 000000000..8e9aecc79 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/SearchRefundsResourceIT.java @@ -0,0 +1,103 @@ +package uk.gov.pay.api.it; + + +import io.restassured.response.ValidatableResponse; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.model.ledger.TransactionState; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.LedgerMockClient; +import uk.gov.pay.api.utils.mocks.RefundTransactionFromLedgerFixture; +import uk.gov.service.payments.commons.validation.DateTimeUtils; + +import java.time.ZonedDateTime; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.utils.Urls.paymentLocationFor; +import static uk.gov.pay.api.utils.mocks.RefundTransactionFromLedgerFixture.RefundTransactionFromLedgerBuilder.aRefundTransactionFromLedgerFixture; +import static uk.gov.service.payments.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION; + +public class SearchRefundsResourceIT extends PaymentResourceITestBase { + + private static final String CHARGE_ID = "ch_ab2341da231434l"; + private static final ZonedDateTime TIMESTAMP = DateTimeUtils.toUTCZonedDateTime("2016-01-01T12:00:00Z").get(); + private static final String CREATED_DATE = ISO_INSTANT_MILLISECOND_PRECISION.format(TIMESTAMP); + + private LedgerMockClient ledgerMockClient = new LedgerMockClient(ledgerMock); + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setUp() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void searchRefunds_shouldReturnValidResponse() { + RefundTransactionFromLedgerFixture refund1 = aRefundTransactionFromLedgerFixture() + .withAmount(100L) + .withCreatedDate(CREATED_DATE) + .withTransactionId("100") + .withParentTransactionId(CHARGE_ID) + .withState(new TransactionState("available", false)) + .build(); + + RefundTransactionFromLedgerFixture refund2 = aRefundTransactionFromLedgerFixture() + .withAmount(300L) + .withCreatedDate(CREATED_DATE) + .withTransactionId("300") + .withSettlementSummary("2020-10-06") + .withParentTransactionId(CHARGE_ID) + .withState(new TransactionState("pending", false)) + .build(); + + ledgerMockClient.respondWithSearchRefunds(refund1, refund2); + + getRefundsSearchResponse() + .statusCode(200) + .contentType(JSON) + .body("total", is(1)) + .body("count", is(1)) + .body("page", is(1)) + .body("results[0].refund_id", is("100")) + .body("results[0].created_date", is("2016-01-01T12:00:00.000Z")) + .body("results[0].payment_id", is("ch_ab2341da231434l")) + .body("results[0].amount", is(100)) + .body("results[0].status", is("available")) + .body("results[0]._links.self.href", is(paymentRefundLocationFor(CHARGE_ID, "100"))) + .body("results[0]._links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))) + .body("results[1].refund_id", is("300")) + .body("results[1].created_date", is("2016-01-01T12:00:00.000Z")) + .body("results[1].payment_id", is("ch_ab2341da231434l")) + .body("results[1].amount", is(300)) + .body("results[1].status", is("pending")) + .body("results[1].settlement_summary.settled_date", is("2020-10-06")) + .body("results[1]._links.self.href", is(paymentRefundLocationFor(CHARGE_ID, "300"))) + .body("results[1]._links.payment.href", is(paymentLocationFor(configuration.getBaseUrl(), CHARGE_ID))) + .body("_links.first_page.href", is("http://publicapi.url/v1/refunds?page=1")) + .body("_links.prev_page.href", is("http://publicapi.url/v1/refunds?page=2")) + .body("_links.self.href", is("http://publicapi.url/v1/refunds?page=3")) + .body("_links.next_page.href", is("http://publicapi.url/v1/refunds?page=4")) + .body("_links.last_page.href", is("http://publicapi.url/v1/refunds?page=5")); + } + + @Test + public void searchRefunds_errorIfLedgerRespondsWith404() { + ledgerMockClient.respondWithSearchRefundsNotFound(); + + getRefundsSearchResponse() + .statusCode(404) + .contentType(JSON) + .body("code", is ("P1100")) + .body("description", is("Page not found")); + } + + private ValidatableResponse getRefundsSearchResponse() { + return given().port(app.getLocalPort()) + .header(AUTHORIZATION, "Bearer " + PaymentResourceITestBase.API_KEY) + .get("/v1/refunds") + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaginatedPaymentSearchResultFixture.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaginatedPaymentSearchResultFixture.java new file mode 100644 index 000000000..f82ccf0c1 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaginatedPaymentSearchResultFixture.java @@ -0,0 +1,50 @@ +package uk.gov.pay.api.it.fixtures; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.gson.Gson; + +import java.util.List; + +public class PaginatedPaymentSearchResultFixture { + + private int total; + private int count; + private int page; + private List results; + private PaymentNavigationLinksFixture _links; + + public static PaginatedPaymentSearchResultFixture aPaginatedPaymentSearchResult() { + return new PaginatedPaymentSearchResultFixture(); + } + + public PaginatedPaymentSearchResultFixture withTotal(int total) { + this.total = total; + return this; + } + + public PaginatedPaymentSearchResultFixture withCount(int count) { + this.count = count; + return this; + } + + public PaginatedPaymentSearchResultFixture withPage(int page) { + this.page = page; + return this; + } + + public PaginatedPaymentSearchResultFixture withLinks(PaymentNavigationLinksFixture links) { + this._links = links; + return this; + } + + public PaginatedPaymentSearchResultFixture withPayments(List results) { + this.results = results; + return this; + } + + public String build() { + return new Gson() + .toJson(this, new TypeReference() { + }.getType()); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaginatedTransactionSearchResultFixture.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaginatedTransactionSearchResultFixture.java new file mode 100644 index 000000000..a96743a7c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaginatedTransactionSearchResultFixture.java @@ -0,0 +1,50 @@ +package uk.gov.pay.api.it.fixtures; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.gson.Gson; + +import java.util.List; + +public class PaginatedTransactionSearchResultFixture { + + private int total; + private int count; + private int page; + private List results; + private PaymentNavigationLinksFixture _links; + + public static PaginatedTransactionSearchResultFixture aPaginatedTransactionSearchResult() { + return new PaginatedTransactionSearchResultFixture(); + } + + public PaginatedTransactionSearchResultFixture withTotal(int total) { + this.total = total; + return this; + } + + public PaginatedTransactionSearchResultFixture withCount(int count) { + this.count = count; + return this; + } + + public PaginatedTransactionSearchResultFixture withPage(int page) { + this.page = page; + return this; + } + + public PaginatedTransactionSearchResultFixture withLinks(PaymentNavigationLinksFixture links) { + this._links = links; + return this; + } + + public PaginatedTransactionSearchResultFixture withPayments(List results) { + this.results = results; + return this; + } + + public String build() { + return new Gson() + .toJson(this, new TypeReference() { + }.getType()); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentNavigationLinksFixture.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentNavigationLinksFixture.java new file mode 100644 index 000000000..0ea476788 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentNavigationLinksFixture.java @@ -0,0 +1,33 @@ +package uk.gov.pay.api.it.fixtures; + +import uk.gov.pay.api.model.links.Link; + +public class PaymentNavigationLinksFixture { + + private Link self; + private Link first_page; + private Link last_page; + private Link prev_page; + private Link next_page; + + public PaymentNavigationLinksFixture withSelfLink(String href) { + this.self = new Link(href); + return this; + } + public PaymentNavigationLinksFixture withPrevLink(String href) { + this.prev_page = new Link(href); + return this; + } + public PaymentNavigationLinksFixture withNextLink(String href) { + this.next_page = new Link(href); + return this; + } + public PaymentNavigationLinksFixture withFirstLink(String href) { + this.first_page = new Link(href); + return this; + } + public PaymentNavigationLinksFixture withLastLink(String href) { + this.last_page = new Link(href); + return this; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentRefundJsonFixture.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentRefundJsonFixture.java new file mode 100644 index 000000000..1775d91f5 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentRefundJsonFixture.java @@ -0,0 +1,52 @@ +package uk.gov.pay.api.it.fixtures; + +import com.fasterxml.jackson.annotation.JsonProperty; +import uk.gov.pay.api.model.links.Link; + +import java.util.List; + +public class PaymentRefundJsonFixture { + + private Long amount; + + @JsonProperty(value = "created_date") + private String createdDate; + + @JsonProperty(value = "refund_id") + private String refundId; + + private String status; + + @JsonProperty(value = "_links") + private List links; + + public PaymentRefundJsonFixture() {} + + public PaymentRefundJsonFixture(Long amount, String createdDate, String refundId, String status, List links) { + this.amount = amount; + this.createdDate = createdDate; + this.refundId = refundId; + this.status = status; + this.links = links; + } + + public Long getAmount() { + return amount; + } + + public String getCreatedDate() { + return createdDate; + } + + public String getRefundId() { + return refundId; + } + + public String getStatus() { + return status; + } + + public List getLinks() { + return links; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentRefundSearchJsonFixture.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentRefundSearchJsonFixture.java new file mode 100644 index 000000000..4656dd783 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentRefundSearchJsonFixture.java @@ -0,0 +1,71 @@ +package uk.gov.pay.api.it.fixtures; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import uk.gov.pay.api.model.links.Link; + +import java.util.List; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(NON_NULL) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class PaymentRefundSearchJsonFixture { + + @JsonProperty(value = "amount_submitted") + private Long amount; + + @JsonProperty(value = "created_date") + private String createdDate; + + @JsonProperty(value = "refund_id") + private String refundId; + + @JsonProperty(value = "charge_id") + private String chargeId; + + private String status; + + @JsonProperty(value = "links") + private List links; + + public PaymentRefundSearchJsonFixture() { + } + + public PaymentRefundSearchJsonFixture(Long amount, String createdDate, String refundId, String chargeId, String status, List links) { + this.amount = amount; + this.createdDate = createdDate; + this.refundId = refundId; + this.status = status; + this.links = links; + this.chargeId = chargeId; + } + + public Long getAmount() { + return amount; + } + + public String getCreatedDate() { + return createdDate; + } + + public String getRefundId() { + return refundId; + } + + public String getStatus() { + return status; + } + + public List getLinks() { + return links; + } + + public String getChargeId() { + return chargeId; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentResultBuilder.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentResultBuilder.java new file mode 100644 index 000000000..13a117786 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentResultBuilder.java @@ -0,0 +1,277 @@ +package uk.gov.pay.api.it.fixtures; + +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.PaymentSettlementSummary; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.validation.DateTimeUtils; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import static java.util.UUID.randomUUID; +import static uk.gov.service.payments.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION; + +public abstract class PaymentResultBuilder { + + protected static final int DEFAULT_NUMBER_OF_RESULTS = 3; + + public static final String DEFAULT_CREATED_DATE = ISO_INSTANT_MILLISECOND_PRECISION.format(ZonedDateTime.now()); + public static final String DEFAULT_CAPTURE_SUBMIT_TIME = ISO_INSTANT_MILLISECOND_PRECISION.format(ZonedDateTime.now()); + public static final String DEFAULT_CAPTURED_DATE = DateTimeUtils.toLocalDateString(ZonedDateTime.now()); + public static final String DEFAULT_SETTLED_DATE = DateTimeUtils.toLocalDateString(ZonedDateTime.now()); + public static final String DEFAULT_RETURN_URL = "http://example.com/service"; + public static final int DEFAULT_AMOUNT = 10000; + public static final String DEFAULT_PAYMENT_PROVIDER = "worldpay"; + public static final String DEFAULT_CARD_BRAND_LABEL = "Mastercard"; + + protected static class Address { + public String line1; + public String line2; + public String postcode; + public String city; + public String county = null; + public String country; + + public Address() { + } + + public Address(uk.gov.pay.api.model.Address billingAddress) { + this.line1 = billingAddress.getLine1(); + this.line2 = billingAddress.getLine2(); + this.postcode = billingAddress.getPostcode(); + this.city = billingAddress.getCity(); + this.country = billingAddress.getCountry(); + } + } + + protected static class CardDetails { + public String last_digits_card_number; + public String first_digits_card_number; + public String cardholder_name; + public String expiry_date; + public Address billing_address; + public String card_brand; + public String card_type; + public CardDetails() { + } + + public CardDetails(CardDetailsFromResponse cardDetails) { + this.last_digits_card_number = cardDetails.getLastDigitsCardNumber(); + this.first_digits_card_number = cardDetails.getFirstDigitsCardNumber(); + this.cardholder_name = cardDetails.getCardHolderName(); + this.expiry_date = cardDetails.getExpiryDate(); + this.card_brand = cardDetails.getCardBrand(); + this.billing_address = cardDetails.getBillingAddress().map(Address::new).orElse(null); + this.card_type = cardDetails.getCardType(); + } + } + + protected static class RefundSummary { + public String status; + public long amount_available; + public long amount_submitted; + public String user_external_id; + + public RefundSummary(uk.gov.pay.api.model.RefundSummary refundSummary) { + this.status = refundSummary.getStatus(); + this.amount_available = refundSummary.getAmountAvailable(); + this.amount_submitted = refundSummary.getAmountSubmitted(); + this.user_external_id = null; + } + + public RefundSummary(String status, long amountAvailable, long amountSubmitted) { + this.status = status; + this.amount_available = amountAvailable; + this.amount_submitted = amountSubmitted; + this.user_external_id = null; + } + } + + protected static class SettlementSummary { + public String capture_submit_time; + public String captured_date; + public String settled_date; + + public SettlementSummary() {} + + public SettlementSummary(PaymentSettlementSummary paymentSettlementSummary) { + this.capture_submit_time = paymentSettlementSummary.getCaptureSubmitTime(); + this.captured_date = paymentSettlementSummary.getCapturedDate(); + this.settled_date = paymentSettlementSummary.getSettledDate(); + } + + public SettlementSummary(String captureSubmitTime, String capturedDate) { + this.capture_submit_time = captureSubmitTime; + this.captured_date = capturedDate; + } + } + + protected static class AuthorisationSummary { + public ThreeDSecure three_d_secure; + + public AuthorisationSummary(uk.gov.pay.api.model.ThreeDSecure threeDSecure) { + this.three_d_secure = new ThreeDSecure(threeDSecure); + } + } + + protected static class ThreeDSecure { + public boolean required; + + public ThreeDSecure(uk.gov.pay.api.model.ThreeDSecure threeDSecure) { + this.required = threeDSecure.isRequired(); + } + } + + protected static class TestPayment { + public TestPaymentState state; + public String charge_id, transaction_id, description, reference, email, created_date, gateway_transaction_id, + return_url, payment_provider, language; + public long amount; + public boolean delayed_capture; + public boolean moto; + public RefundSummary refund_summary; + public SettlementSummary settlement_summary; + public CardDetails card_details = new CardDetails(); + public Long corporate_card_surcharge, total_amount, fee, net_amount; + public List> links; + public Map metadata; + public AuthorisationSummary authorisation_summary; + public String agreement_id; + public String authorisation_mode; + public String wallet_type; + } + + protected static class TestPaymentState { + public String status; + public boolean finished; + + protected TestPaymentState(String status, boolean finished) { + this.status = status; + this.finished = finished; + } + } + + protected static class TestPaymentSuccessState extends TestPaymentState { + private boolean success; + + protected TestPaymentSuccessState(String status) { + super(status, true); + this.success = true; + } + } + + protected static class TestPaymentRejectedState extends TestPaymentState { + private boolean success; + private boolean can_retry; + private String code; + private String message; + + protected TestPaymentRejectedState(String status, String code, String message, boolean canRetry) { + super(status, true); + this.success = true; + this.code = code; + this.message = message; + this.can_retry = canRetry; + } + } + + protected static final List states = new LinkedList<>(); + + static { + states.add(new TestPaymentState("created", false)); + states.add(new TestPaymentState("started", false)); + states.add(new TestPaymentState("submitted", false)); + states.add(new TestPaymentSuccessState("success")); + } + + protected long amount = DEFAULT_AMOUNT; + protected String reference = null; + protected String email = null; + protected String language = SupportedLanguage.ENGLISH.toString(); + protected Boolean delayedCapture; + protected boolean moto; + protected String agreementId; + protected TestPaymentState state; + protected String fromDate = null; + protected String toDate = null; + protected CardDetails cardDetails; + protected Long corporateCardSurcharge = null; + protected Long totalAmount = null; + protected Long fee = null; + protected Long netAmount = null; + protected String chargeId = null; + protected String description; + protected String returnUrl; + protected String paymentProvider; + protected String createdDate; + protected List> links = new ArrayList<>(); + protected RefundSummary refundSummary; + protected SettlementSummary settlementSummary; + protected String gatewayTransactionId; + protected Map metadata; + protected AuthorisationSummary authorisationSummary; + protected AuthorisationMode authorisationMode = AuthorisationMode.WEB; + protected String walletType = null; + + public abstract String build(); + + protected TestPayment getPayment(int i) { + this.chargeId = chargeId == null ? "" + i : chargeId; + this.description = "description-" + i; + return defaultPaymentResult(); + } + + protected TestPayment getPayment() { + return defaultPaymentResult(); + } + + private TestPayment defaultPaymentResult() { + TestPayment payment = new TestPayment(); + + payment.charge_id = chargeId; + payment.transaction_id = chargeId; + payment.description = description; + payment.reference = reference == null ? randomUUID().toString() : reference; + payment.email = email; + payment.state = state == null ? states.get(new Random().nextInt(states.size())) : state; + payment.amount = amount; + payment.gateway_transaction_id = gatewayTransactionId == null ? randomUUID().toString() : gatewayTransactionId; + payment.created_date = getCreatedDate(); + payment.return_url = returnUrl; + payment.payment_provider = paymentProvider == null ? DEFAULT_PAYMENT_PROVIDER : paymentProvider; + payment.language = SupportedLanguage.ENGLISH.toString(); + payment.delayed_capture = delayedCapture == null ? false : delayedCapture; + payment.moto = moto; + payment.agreement_id = agreementId; + payment.card_details.card_brand = cardDetails == null ? DEFAULT_CARD_BRAND_LABEL : cardDetails.card_brand; + payment.card_details = cardDetails == null ? new CardDetails() : cardDetails; + payment.refund_summary = refundSummary == null ? + new RefundSummary("available", 100, 300) : refundSummary; + payment.settlement_summary = settlementSummary == null ? + new SettlementSummary(DEFAULT_CAPTURE_SUBMIT_TIME, DEFAULT_CAPTURED_DATE) : settlementSummary; + payment.corporate_card_surcharge = corporateCardSurcharge; + payment.total_amount = totalAmount; + payment.fee = fee; + payment.net_amount = netAmount; + payment.links = links ; + payment.metadata = metadata; + payment.authorisation_summary = authorisationSummary; + payment.authorisation_mode = authorisationMode.getName(); + payment.wallet_type = walletType; + + return payment; + } + + private String getCreatedDate() { + if (fromDate != null) { + ZonedDateTime updatedFromDate = DateTimeUtils.toUTCZonedDateTime(fromDate).get().plusMinutes(new Random().nextInt(15) + 1); + return ISO_INSTANT_MILLISECOND_PRECISION.format(updatedFromDate); + } + return createdDate == null ? DEFAULT_CREATED_DATE : createdDate; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentSearchResultBuilder.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentSearchResultBuilder.java new file mode 100644 index 000000000..09df681c1 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentSearchResultBuilder.java @@ -0,0 +1,145 @@ +package uk.gov.pay.api.it.fixtures; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.PaymentSettlementSummary; +import uk.gov.service.payments.commons.model.AuthorisationMode; + +import java.util.List; +import java.util.Map; + +import static com.google.common.collect.Lists.newArrayList; + +public class PaymentSearchResultBuilder extends PaymentResultBuilder { + + private int noOfResults = DEFAULT_NUMBER_OF_RESULTS; + + + public static PaymentSearchResultBuilder aSuccessfulSearchPayment() { + return new PaymentSearchResultBuilder(); + } + + public PaymentSearchResultBuilder withCaptureLink(String href) { + this.links.add(ImmutableMap.of( + "href", href, + "rel", "capture", + "method", "POST")); + return this; + } + + public PaymentSearchResultBuilder withChargeId(String chargeId) { + this.chargeId = chargeId; + return this; + } + + public PaymentSearchResultBuilder withCardDetails(CardDetailsFromResponse cardDetails) { + this.cardDetails = new CardDetails(cardDetails); + return this; + } + + public PaymentSearchResultBuilder withReference(String reference) { + this.reference = reference; + return this; + } + + public PaymentSearchResultBuilder withReturnUrl(String returnUrl) { + this.returnUrl = returnUrl; + return this; + } + + public PaymentSearchResultBuilder withFee(Long fee) { + this.fee = fee; + return this; + } + + public PaymentSearchResultBuilder withNetAmount(Long netAmount) { + this.netAmount = netAmount; + return this; + } + + public PaymentSearchResultBuilder withEmail(String email) { + this.email = email; + return this; + } + + public PaymentSearchResultBuilder withMetadata(Map metadata) { + this.metadata = metadata; + return this; + } + + public PaymentSearchResultBuilder withGatewayTransactionId(String gatewayTransactionId) { + this.gatewayTransactionId = gatewayTransactionId; + return this; + } + + public PaymentSearchResultBuilder withInProgressState(String status) { + this.state = new TestPaymentState(status, false); + return this; + } + + public PaymentSearchResultBuilder withSuccessState(String status) { + this.state = new TestPaymentSuccessState(status); + return this; + } + + public PaymentSearchResultBuilder withRejectedState(String status, String code, String message, boolean canRetry) { + this.state = new TestPaymentRejectedState(status, code, message, canRetry); + return this; + } + + public PaymentSearchResultBuilder withDelayedCapture(boolean delayedCapture) { + this.delayedCapture = delayedCapture; + return this; + } + + public PaymentSearchResultBuilder withCreatedDateBetween(String fromDate, String toDate) { + this.fromDate = fromDate; + this.toDate = toDate; + return this; + } + + public PaymentSearchResultBuilder withNumberOfResults(int numberOfResults) { + this.noOfResults = numberOfResults; + return this; + } + + public PaymentSearchResultBuilder withSettlementSummary(PaymentSettlementSummary settlementSummary) { + this.settlementSummary = new SettlementSummary(settlementSummary); + return this; + } + + public PaymentSearchResultBuilder withAuthorisationSummary(uk.gov.pay.api.model.AuthorisationSummary authorisationSummary) { + this.authorisationSummary = authorisationSummary == null ? null : new AuthorisationSummary(authorisationSummary.getThreeDSecure()); + return this; + } + + public PaymentSearchResultBuilder withAuthorisationMode(AuthorisationMode authorisationMode) { + this.authorisationMode = authorisationMode; + return this; + } + + public PaymentSearchResultBuilder withWalletType(String walletType) { + this.walletType = walletType; + return this; + } + + public List getResults() { + List results = newArrayList(); + for (int i = 0; i < noOfResults; i++) { + results.add(getPayment(i)); + } + return results; + } + + public String build() { + List results = getResults(); + + return new GsonBuilder().create().toJson( + ImmutableMap.of("results", results), + new TypeToken>>() { + }.getType() + ); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentSingleResultBuilder.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentSingleResultBuilder.java new file mode 100644 index 000000000..c902d1f81 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/fixtures/PaymentSingleResultBuilder.java @@ -0,0 +1,159 @@ +package uk.gov.pay.api.it.fixtures; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.gson.Gson; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.PaymentSettlementSummary; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; + +import java.util.List; +import java.util.Map; + +public class PaymentSingleResultBuilder extends PaymentResultBuilder { + + public static PaymentSingleResultBuilder aSuccessfulSinglePayment() { + return new PaymentSingleResultBuilder(); + } + + + public PaymentSingleResultBuilder withCardDetails(CardDetailsFromResponse cardDetails) { + this.cardDetails = new CardDetails(cardDetails); + return this; + } + + public PaymentSingleResultBuilder withMatchingReference(String reference) { + this.reference = reference; + return this; + } + + public PaymentSingleResultBuilder withEmail(String email) { + this.email = email; + return this; + } + + public PaymentSingleResultBuilder withLanguage(SupportedLanguage language) { + this.language = language.toString(); + return this; + } + + public PaymentSingleResultBuilder withDelayedCapture(boolean delayedCapture) { + this.delayedCapture = delayedCapture; + return this; + } + + public PaymentSingleResultBuilder withMoto(boolean moto) { + this.moto = moto; + return this; + } + + public PaymentSingleResultBuilder withAgreementId(String agreementId) { + this.agreementId = agreementId; + return this; + } + + public PaymentSingleResultBuilder withCorporateCardSurcharge(Long surcharge) { + this.corporateCardSurcharge = surcharge; + return this; + } + + public PaymentSingleResultBuilder withTotalAmount(Long totalAmount) { + this.totalAmount = totalAmount; + return this; + } + + public PaymentSingleResultBuilder withFee(Long fee) { + this.fee = fee; + return this; + } + + public PaymentSingleResultBuilder withNetAmount(Long netAmount) { + this.netAmount = netAmount; + return this; + } + + public PaymentSingleResultBuilder withAmount(long amount) { + this.amount = amount; + return this; + } + + public PaymentSingleResultBuilder withDescription(String description) { + this.description = description; + return this; + } + + public PaymentSingleResultBuilder withChargeId(String chargeId) { + this.chargeId = chargeId; + return this; + } + + public PaymentSingleResultBuilder withState(PaymentState paymentState) { + this.state = paymentState == null ? + new TestPaymentState("submitted", false) : + paymentState.getCanRetry() == null ? + new TestPaymentState(paymentState.getStatus(), paymentState.isFinished()) : + new TestPaymentRejectedState(paymentState.getStatus(), paymentState.getCode(), paymentState.getMessage(), paymentState.getCanRetry()); + return this; + } + + public PaymentSingleResultBuilder withReturnUrl(String returnUrl) { + this.returnUrl = returnUrl; + return this; + } + + public PaymentSingleResultBuilder withPaymentProvider(String paymentProvider) { + this.paymentProvider = paymentProvider; + return this; + } + + public PaymentSingleResultBuilder withCreatedDate(String createdDate) { + this.createdDate = createdDate; + return this; + } + + public PaymentSingleResultBuilder withLinks(List> links) { + this.links = links; + return this; + } + + public PaymentSingleResultBuilder withRefundSummary(uk.gov.pay.api.model.RefundSummary refundSummary) { + this.refundSummary = new RefundSummary(refundSummary); + return this; + } + + public PaymentSingleResultBuilder withSettlementSummary(PaymentSettlementSummary settlementSummary) { + this.settlementSummary = settlementSummary == null ? new SettlementSummary() : new SettlementSummary(settlementSummary); + return this; + } + + public PaymentSingleResultBuilder withGatewayTransactionId(String gatewayTransactionId) { + this.gatewayTransactionId = gatewayTransactionId; + return this; + } + + public PaymentSingleResultBuilder withMetadata(Map metadata) { + this.metadata = metadata; + return this; + } + + public PaymentSingleResultBuilder withAuthorisationSummary(uk.gov.pay.api.model.AuthorisationSummary authorisationSummary) { + this.authorisationSummary = authorisationSummary == null ? null : new AuthorisationSummary(authorisationSummary.getThreeDSecure()); + return this; + } + + public PaymentSingleResultBuilder withAuthorisationMode(AuthorisationMode authorisationMode) { + this.authorisationMode = authorisationMode; + return this; + } + + public PaymentSingleResultBuilder withWalletType(String walletType) { + this.walletType = walletType; + return this; + } + + public String build() { + TestPayment result = getPayment(); + return new Gson().toJson(result, new TypeReference() {}.getType()); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ledger/TransactionsResourceIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ledger/TransactionsResourceIT.java new file mode 100644 index 000000000..8690ea4ad --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/ledger/TransactionsResourceIT.java @@ -0,0 +1,167 @@ +package uk.gov.pay.api.it.ledger; + + +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.google.common.collect.ImmutableMap; +import io.dropwizard.testing.junit.DropwizardAppRule; +import io.restassured.response.ValidatableResponse; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import uk.gov.pay.api.app.PublicApi; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.it.fixtures.PaymentNavigationLinksFixture; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.AuthorisationSummary; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.ThreeDSecure; +import uk.gov.pay.api.utils.ApiKeyGenerator; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.LedgerMockClient; + +import java.util.Map; + +import static io.dropwizard.testing.ConfigOverride.config; +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.it.fixtures.PaginatedTransactionSearchResultFixture.aPaginatedTransactionSearchResult; +import static uk.gov.pay.api.it.fixtures.PaymentResultBuilder.DEFAULT_AMOUNT; +import static uk.gov.pay.api.it.fixtures.PaymentResultBuilder.DEFAULT_CREATED_DATE; +import static uk.gov.pay.api.it.fixtures.PaymentResultBuilder.DEFAULT_RETURN_URL; +import static uk.gov.pay.api.it.fixtures.PaymentSearchResultBuilder.aSuccessfulSearchPayment; +import static uk.gov.pay.api.utils.Urls.paymentLocationFor; +import static uk.gov.service.payments.commons.testing.port.PortFactory.findFreePort; + +public class TransactionsResourceIT { + + private static final String API_KEY = ApiKeyGenerator.apiKeyValueOf("TEST_BEARER_TOKEN", "qwer9yuhgf"); + private static final int LEDGER_PORT = findFreePort(); + private static final int PUBLIC_AUTH_PORT = findFreePort(); + private static final String GATEWAY_ACCOUNT_ID = "1234"; + + @ClassRule + public static WireMockClassRule ledgerMock = new WireMockClassRule(LEDGER_PORT); + + @ClassRule + public static WireMockClassRule publicAuthMock = new WireMockClassRule(PUBLIC_AUTH_PORT); + + @Rule + public DropwizardAppRule app = new DropwizardAppRule<>( + PublicApi.class, + resourceFilePath("config/test-config.yaml"), + config("ledgerUrl", "http://localhost:" + LEDGER_PORT), + config("publicAuthUrl", "http://localhost:" + PUBLIC_AUTH_PORT + "/v1/auth") + ); + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + private LedgerMockClient ledgerMockClient = new LedgerMockClient(ledgerMock); + private PublicApiConfig configuration; + + @Before + public void mapBearerTokenToAccountId() { + configuration = app.getConfiguration(); + publicAuthMock.resetAll(); + ledgerMock.resetAll(); + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void shouldReturnAListOfTransactions() { + Address billingAddress = new Address("line1", null, "AB1 CD2", "London", "GB"); + CardDetailsFromResponse cardDetails = new CardDetailsFromResponse(null, null, "J. Doe", + null, billingAddress, "", null); + PaymentNavigationLinksFixture fixture = new PaymentNavigationLinksFixture(); + fixture.withSelfLink("https://ledger/v1/transaction?account_id=1&reference=reference&page=1&display_size=500"); + fixture.withFirstLink("https://ledger/v1/transaction?account_id=1&reference=reference&page=1&display_size=500"); + fixture.withLastLink("https://ledger/v1/transaction?account_id=1&reference=reference&page=1&display_size=500"); + String transactions = aPaginatedTransactionSearchResult() + .withCount(2) + .withPage(1) + .withTotal(2) + .withLinks(fixture) + .withPayments(aSuccessfulSearchPayment() + .withInProgressState("created") + .withReference("reference") + .withReturnUrl(DEFAULT_RETURN_URL) + .withCardDetails(cardDetails) + .withNumberOfResults(2) + .withEmail("j.doe@example.org") + .withAuthorisationSummary(new AuthorisationSummary(new ThreeDSecure(true))) + .getResults()) + .build(); + ledgerMockClient.respondOk_whenSearchCharges(transactions); + + searchTransactions(ImmutableMap.of("reference", "reference")) + .statusCode(200) + .contentType(JSON) + .body("results[0].created_date", is(DEFAULT_CREATED_DATE)) + .body("results[0].reference", is("reference")) + .body("results[0].email", is("j.doe@example.org")) + .body("results[0].return_url", is(DEFAULT_RETURN_URL)) + .body("results[0].description", is("description-0")) + .body("results[0].state.status", is("created")) + .body("results[0].amount", is(DEFAULT_AMOUNT)) + .body("results[0].payment_provider", is("worldpay")) + .body("results[0].payment_id", is("0")) + .body("results[0].language", is("en")) + .body("results[0].delayed_capture", is(false)) + .body("results[0]._links.self.method", is("GET")) + .body("results[0]._links.self.href", is(paymentLocationFor(configuration.getBaseUrl(), "0"))) + .body("results[0]._links.events.href", is(paymentEventsLocationFor("0"))) + .body("results[0]._links.events.method", is("GET")) + .body("results[0]._links.cancel.href", is(paymentCancelLocationFor("0"))) + .body("results[0]._links.cancel.method", is("POST")) + .body("results[0]._links.refunds.href", is(paymentRefundsLocationFor("0"))) + .body("results[0]._links.refunds.method", is("GET")) + .body("results[0].card_details.cardholder_name", is("J. Doe")) + .body("results[0].card_details.expiry_date", is(nullValue())) + .body("results[0].card_details.last_digits_card_number", is(nullValue())) + .body("results[0].card_details.first_digits_card_number", is(nullValue())) + .body("results[0].card_details.billing_address.line1", is("line1")) + .body("results[0].card_details.billing_address.line2", is(nullValue())) + .body("results[0].card_details.billing_address.postcode", is("AB1 CD2")) + .body("results[0].card_details.billing_address.country", is("GB")) + .body("results[0].card_details.card_brand", is(emptyString())) + .body("results[0].authorisation_summary", is(notNullValue())) + .body("results[0].authorisation_summary.three_d_secure", is(notNullValue())) + .body("results[0].authorisation_summary.three_d_secure.required", is(true)) + .body("results[0].authorisation_mode", is("web")) + .body("_links.self.href", is(expectedChargesLocationFor("?reference=reference&display_size=500&page=1"))) + .body("_links.first_page.href", is(expectedChargesLocationFor("?reference=reference&display_size=500&page=1"))) + .body("_links.last_page.href", is(expectedChargesLocationFor("?reference=reference&display_size=500&page=1"))); + } + + private ValidatableResponse searchTransactions(Map queryParams) { + return given().port(app.getLocalPort()) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + API_KEY) + .queryParams(queryParams) + .get("/v1/transactions") + .then(); + } + + private String expectedChargesLocationFor(String queryParams) { + return "http://publicapi.url/v1/transactions" + queryParams; + } + + private String paymentEventsLocationFor(String chargeId) { + return paymentLocationFor(configuration.getBaseUrl(), chargeId) + "/events"; + } + + private String paymentRefundsLocationFor(String chargeId) { + return paymentLocationFor(configuration.getBaseUrl(), chargeId) + "/refunds"; + } + + private String paymentCancelLocationFor(String chargeId) { + return paymentLocationFor(configuration.getBaseUrl(), chargeId) + "/cancel"; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/rule/RedisContainer.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/rule/RedisContainer.java new file mode 100644 index 000000000..84e0a3fc8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/rule/RedisContainer.java @@ -0,0 +1,41 @@ +package uk.gov.pay.api.it.rule; + +import io.lettuce.core.RedisClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import static java.lang.String.format; + +public class RedisContainer { + + private static final Logger logger = LoggerFactory.getLogger(RedisContainer.class); + private static GenericContainer REDIS_CONTAINER; + private static final int PORT = 6379; + + static GenericContainer getOrCreateRedisContainer() { + if (REDIS_CONTAINER == null) { + REDIS_CONTAINER = new GenericContainer<>("redis:5.0.6") // elasticache engine version + .withExposedPorts(PORT) + .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*", 1)); + + REDIS_CONTAINER.start(); + logger.info(format("Redis container started, mapped %o to host port %o", PORT, REDIS_CONTAINER.getMappedPort(PORT))); + } + return REDIS_CONTAINER; + } + + static String getConnectionUrl() { + return format("%s:%s", REDIS_CONTAINER.getHost(), REDIS_CONTAINER.getMappedPort(PORT)) ; + } + + static void clearRedisCache() { + try (RedisClient redisClient = RedisClient.create(format("redis://%s", getConnectionUrl()))) { + String response = redisClient.connect().sync().flushall(); + if (!response.equals("OK")) { + logger.warn("Unexpected response from redis flushAll command: " + response); + } + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/rule/RedisDockerRule.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/rule/RedisDockerRule.java new file mode 100644 index 000000000..0933fb18c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/rule/RedisDockerRule.java @@ -0,0 +1,29 @@ +package uk.gov.pay.api.it.rule; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import static uk.gov.pay.api.it.rule.RedisContainer.getConnectionUrl; +import static uk.gov.pay.api.it.rule.RedisContainer.getOrCreateRedisContainer; +import static uk.gov.pay.api.it.rule.RedisContainer.clearRedisCache; + +public class RedisDockerRule implements TestRule { + + public RedisDockerRule() { + getOrCreateRedisContainer(); + } + + @Override + public Statement apply(Statement statement, Description description) { + return statement; + } + + public String getRedisUrl() { + return getConnectionUrl(); + } + + public void clearCache() { + clearRedisCache(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/CardExpiryValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/CardExpiryValidationIT.java new file mode 100644 index 000000000..ceff31ae3 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/CardExpiryValidationIT.java @@ -0,0 +1,40 @@ +package uk.gov.pay.api.it.telephone; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.utils.PublicAuthMockClient; + +import java.util.Map; + +public class CardExpiryValidationIT extends TelephonePaymentResourceITBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setUpBearerTokenAndRequestBody() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + requestBody.put("amount", 100); + requestBody.put("reference", "Some refeerence"); + requestBody.put("description", "Some description"); + requestBody.put("processor_id", "1PROC"); + requestBody.put("provider_id", "1PROV"); + requestBody.put("payment_outcome", Map.of("status", "success")); + requestBody.put("card_type", "visa"); + requestBody.put("last_four_digits", "1234"); + requestBody.put("first_six_digits", "123456"); + } + + @After + public void tearDown() { + requestBody.clear(); + } + + @Test + public void respondWith422_whenMonthis00() { + requestBody.put("card_expiry", "00/99"); + postPaymentResponse(toJson(requestBody)) + .statusCode(422); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/CreateTelephonePaymentIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/CreateTelephonePaymentIT.java new file mode 100644 index 000000000..d50e21cd1 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/CreateTelephonePaymentIT.java @@ -0,0 +1,160 @@ +package uk.gov.pay.api.it.telephone; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import uk.gov.pay.api.model.telephone.PaymentOutcome; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; + +import java.util.Map; + +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.core.Is.is; +import static io.restassured.http.ContentType.JSON; + +public class CreateTelephonePaymentIT extends TelephonePaymentResourceITBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + private ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + + @Before + public void setUpBearerTokenAndRequestBody() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + requestBody.put("amount", 100); + requestBody.put("reference", "Some reference"); + requestBody.put("description", "Some description"); + requestBody.put("processor_id", "1PROC"); + requestBody.put("provider_id", "1PROV"); + requestBody.put("payment_outcome", Map.of("status", "success")); + + createTelephonePaymentRequest + .withAmount(100) + .withReference("Some reference") + .withDescription("Some description") + .withProcessorId("1PROC") + .withProviderId("1PROV") + .withPaymentOutcome(new PaymentOutcome("success")); + } + + @After + public void tearDown() { + requestBody.clear(); + createTelephonePaymentRequest + .withAuthCode(null) + .withCreatedDate(null) + .withAuthorisedDate(null) + .withNameOnCard(null) + .withEmailAddress(null) + .withTelephoneNumber(null) + .withCardExpiry(null) + .withCardType(null) + .withLastFourDigits(null) + .withFirstSixDigits(null); + } + + @Test + public void createTelephonePaymentWithAllFields() { + requestBody.put("auth_code", "666"); + requestBody.put("created_date", "2018-02-21T16:04:25Z"); + requestBody.put("authorised_date", "2018-02-21T16:05:33Z"); + requestBody.put("name_on_card", "Jane Doe"); + requestBody.put("email_address", "jane_doe@example.com"); + requestBody.put("telephone_number", "+447700900796"); + requestBody.put("card_expiry", "01/08"); + requestBody.put("card_type", "visa"); + requestBody.put("last_four_digits", "1234"); + requestBody.put("first_six_digits", "123456"); + + createTelephonePaymentRequest + .withAuthCode("666") + .withCreatedDate("2018-02-21T16:04:25Z") + .withAuthorisedDate("2018-02-21T16:05:33Z") + .withNameOnCard("Jane Doe") + .withEmailAddress("jane_doe@example.com") + .withTelephoneNumber("+447700900796") + .withCardExpiry("01/08") + .withCardType("visa") + .withLastFourDigits("1234") + .withFirstSixDigits("123456"); + + connectorMockClient.respondCreated_whenCreateTelephoneCharge(GATEWAY_ACCOUNT_ID, createTelephonePaymentRequest + .build()); + + postPaymentResponse(toJson(requestBody)) + .statusCode(201) + .contentType(JSON) + .body("amount", is(100)) + .body("reference", is("Some reference")) + .body("description", is("Some description")) + .body("created_date", is("2018-02-21T16:04:25Z")) + .body("authorised_date", is("2018-02-21T16:05:33Z")) + .body("processor_id", is("1PROC")) + .body("provider_id", is("1PROV")) + .body("auth_code", is("666")) + .body("payment_outcome.status", is("success")) + .body("card_type", is("visa")) + .body("name_on_card", is("Jane Doe")) + .body("email_address", is("jane_doe@example.com")) + .body("card_expiry", is("01/08")) + .body("last_four_digits", is("1234")) + .body("first_six_digits", is("123456")) + .body("telephone_number", is("+447700900796")) + .body("payment_id", is("dummypaymentid123notpersisted")) + .body("state.status", is("success")) + .body("state.finished", is(true)); + } + + @Test + public void createTelephonePaymentWithRequiredFields() { + connectorMockClient.respondCreated_whenCreateTelephoneCharge(GATEWAY_ACCOUNT_ID, createTelephonePaymentRequest + .build()); + + postPaymentResponse(toJson(requestBody)) + .statusCode(201) + .contentType(JSON) + .body("amount", is(100)) + .body("reference", is("Some reference")) + .body("description", is("Some description")) + .body("processor_id", is("1PROC")) + .body("payment_outcome.status", is("success")) + .body("payment_id", is("dummypaymentid123notpersisted")) + .body("state.status", is("success")) + .body("state.finished", is(true)); + } + + @Test + public void returnExistingTelephonePayment() { + connectorMockClient.respondOk_whenCreateTelephoneCharge(GATEWAY_ACCOUNT_ID, createTelephonePaymentRequest + .build()); + + postPaymentResponse(toJson(requestBody)) + .statusCode(200) + .contentType(JSON) + .body("amount", is(100)) + .body("reference", is("Some reference")) + .body("description", is("Some description")) + .body("processor_id", is("1PROC")) + .body("payment_outcome.status", is("success")) + .body("card_type", is(nullValue())) + .body("card_expiry", is(nullValue())) + .body("last_four_digits", is(nullValue())) + .body("first_six_digits", is(nullValue())) + .body("payment_id", is("dummypaymentid123notpersisted")) + .body("state.status", is("success")) + .body("state.finished", is(true)); + } + + @Test + public void telephonePaymentNotificationsNotEnabledForAccount_shouldRespondWith403() { + connectorMockClient.respondTelephoneNotificationsNotEnabled(GATEWAY_ACCOUNT_ID); + + postPaymentResponse(toJson(requestBody)) + .statusCode(403) + .contentType(JSON) + .body("code", is("P0930")) + .body("description", is("Access to this resource is not enabled for this account. Contact support with your error code - https://www.payments.service.gov.uk/support/ .")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/FirstSixCardDigitsValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/FirstSixCardDigitsValidationIT.java new file mode 100644 index 000000000..6baa0bc93 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/FirstSixCardDigitsValidationIT.java @@ -0,0 +1,40 @@ +package uk.gov.pay.api.it.telephone; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.utils.PublicAuthMockClient; + +import java.util.Map; + +public class FirstSixCardDigitsValidationIT extends TelephonePaymentResourceITBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setUpBearerTokenAndRequestBody() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + requestBody.put("amount", 100); + requestBody.put("reference", "Some refeerence"); + requestBody.put("description", "Some description"); + requestBody.put("processor_id", "1PROC"); + requestBody.put("provider_id", "1PROV"); + requestBody.put("payment_outcome", Map.of("status", "success")); + requestBody.put("card_type", "visa"); + requestBody.put("card_expiry", "00/99"); + requestBody.put("last_four_digits", "1234"); + } + + @After + public void tearDown() { + requestBody.clear(); + } + + @Test + public void respondWith422_whenFiveDigitsProvideOnly() { + requestBody.put("first_six_digits", "12345"); + postPaymentResponse(toJson(requestBody)) + .statusCode(422); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/LastFourCardDigitsValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/LastFourCardDigitsValidationIT.java new file mode 100644 index 000000000..30a8e5700 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/LastFourCardDigitsValidationIT.java @@ -0,0 +1,40 @@ +package uk.gov.pay.api.it.telephone; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.utils.PublicAuthMockClient; + +import java.util.Map; + +public class LastFourCardDigitsValidationIT extends TelephonePaymentResourceITBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setUpBearerTokenAndRequestBody() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + requestBody.put("amount", 100); + requestBody.put("reference", "Some refeerence"); + requestBody.put("description", "Some description"); + requestBody.put("processor_id", "1PROC"); + requestBody.put("provider_id", "1PROV"); + requestBody.put("payment_outcome", Map.of("status", "success")); + requestBody.put("card_type", "visa"); + requestBody.put("card_expiry", "01/99"); + requestBody.put("first_six_digits", "123456"); + } + + @After + public void tearDown() { + requestBody.clear(); + } + + @Test + public void respondWith422_whenThreeDigitsProvideOnly() { + requestBody.put("last_four_digits", "123"); + postPaymentResponse(toJson(requestBody)) + .statusCode(422); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/PaymentOutcomeValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/PaymentOutcomeValidationIT.java new file mode 100644 index 000000000..3d4383a5c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/PaymentOutcomeValidationIT.java @@ -0,0 +1,53 @@ +package uk.gov.pay.api.it.telephone; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.utils.PublicAuthMockClient; + +import java.util.Map; + +public class PaymentOutcomeValidationIT extends TelephonePaymentResourceITBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setUpBearerTokenAndRequestBody() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + requestBody.put("amount", 100); + requestBody.put("reference", "Some refeerence"); + requestBody.put("description", "Some description"); + requestBody.put("processor_id", "1PROC"); + requestBody.put("provider_id", "1PROV"); + requestBody.put("card_expiry", "01/99"); + requestBody.put("card_type", "visa"); + requestBody.put("last_four_digits", "1234"); + requestBody.put("first_six_digits", "123456"); + } + + @After + public void tearDown() { + requestBody.clear(); + } + + @Test + public void respondWith422_whenStatusIsOtherThanSuccessOrFailed() { + requestBody.put("payment_outcome", Map.of("status", "other")); + postPaymentResponse(toJson(requestBody)) + .statusCode(422); + } + + @Test + public void respondWith422_whenPaymentOutcomeIsNull() { + requestBody.put("payment_outcome", null); + postPaymentResponse(toJson(requestBody)) + .statusCode(422); + } + + @Test + public void respondWith422_whenPaymentOutcomeIsMissing() { + postPaymentResponse(toJson(requestBody)) + .statusCode(422); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/PredefinedValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/PredefinedValidationIT.java new file mode 100644 index 000000000..e0ec0f183 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/PredefinedValidationIT.java @@ -0,0 +1,77 @@ +package uk.gov.pay.api.it.telephone; + +import org.apache.commons.lang.StringUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.utils.PublicAuthMockClient; + +import java.util.Map; + +public class PredefinedValidationIT extends TelephonePaymentResourceITBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setUpBearerTokenAndRequestBody() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + requestBody.put("amount", 100); + requestBody.put("reference", "Some reference"); + requestBody.put("description", "Some description"); + requestBody.put("processor_id", "1PROC"); + requestBody.put("provider_id", "1PROV"); + requestBody.put("payment_outcome", Map.of("status", "success")); + requestBody.put("card_type", "visa"); + requestBody.put("card_expiry", "01/08"); + requestBody.put("last_four_digits", "1234"); + requestBody.put("first_six_digits", "123456"); + } + + @After + public void tearDown() { + requestBody.clear(); + } + + @Test + public void respondWith422_whenReferenceLengthIsGreaterThanMaxValue() { + requestBody.replace("reference", StringUtils.repeat("*", 256)); + postPaymentResponse(toJson(requestBody)) + .statusCode(422); + } + + @Test + public void respondWith422_whenDescriptionLengthIsGreaterThanMaxValue() { + requestBody.replace("description", StringUtils.repeat("*", 256)); + postPaymentResponse(toJson(requestBody)) + .statusCode(422); + } + + @Test + public void respondWith422_whenProcessorIdIsMissing() { + requestBody.remove("processor_id"); + postPaymentResponse(toJson(requestBody)) + .statusCode(422); + } + + @Test + public void respondWith422_whenProcessorIdIsNull() { + requestBody.replace("processor_id", null); + postPaymentResponse(toJson(requestBody)) + .statusCode(422); + } + + @Test + public void respondWith422_whenProviderIdIsMissing() { + requestBody.remove("provider_id"); + postPaymentResponse(toJson(requestBody)) + .statusCode(422); + } + + @Test + public void respondWith422_whenProviderIdIsNull() { + requestBody.replace("provider_id", null); + postPaymentResponse(toJson(requestBody)) + .statusCode(422); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/TelephonePaymentResourceITBase.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/TelephonePaymentResourceITBase.java new file mode 100644 index 000000000..011612c3a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/TelephonePaymentResourceITBase.java @@ -0,0 +1,76 @@ +package uk.gov.pay.api.it.telephone; + +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.dropwizard.testing.junit.DropwizardAppRule; +import io.restassured.response.ValidatableResponse; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import uk.gov.pay.api.app.PublicApi; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.it.rule.RedisDockerRule; +import uk.gov.pay.api.model.telephone.CreateTelephonePaymentRequest; +import uk.gov.pay.api.utils.ApiKeyGenerator; + +import java.util.HashMap; +import java.util.Map; + +import static io.dropwizard.testing.ConfigOverride.config; +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static uk.gov.service.payments.commons.testing.port.PortFactory.findFreePort; + +public abstract class TelephonePaymentResourceITBase { + //Must use same secret set in test-config.xml's apiKeyHmacSecret + protected static final String API_KEY = ApiKeyGenerator.apiKeyValueOf("TEST_BEARER_TOKEN", "qwer9yuhgf"); + protected static final String GATEWAY_ACCOUNT_ID = "GATEWAY_ACCOUNT_ID"; + protected static final String PAYMENTS_PATH = "/v1/payment_notification/"; + protected static final HashMap requestBody = new HashMap<>(); + protected static final CreateTelephonePaymentRequest.Builder createTelephonePaymentRequest = new CreateTelephonePaymentRequest.Builder(); + + @ClassRule + public static RedisDockerRule redisDockerRule = new RedisDockerRule(); + + private static final int CONNECTOR_PORT = findFreePort(); + private static final int PUBLIC_AUTH_PORT = findFreePort(); + private static final Gson GSON = new GsonBuilder().serializeNulls().create(); + + @ClassRule + public static WireMockClassRule connectorMock = new WireMockClassRule(CONNECTOR_PORT); + + @ClassRule + public static WireMockClassRule publicAuthMock = new WireMockClassRule(PUBLIC_AUTH_PORT); + + @Rule + public DropwizardAppRule app = new DropwizardAppRule<>( + PublicApi.class, + resourceFilePath("config/test-config.yaml"), + config("connectorUrl", "http://localhost:" + CONNECTOR_PORT), + config("publicAuthUrl", "http://localhost:" + PUBLIC_AUTH_PORT + "/v1/auth"), + config("redis.endpoint", redisDockerRule.getRedisUrl()) + ); + + @Before + public void setup() { + connectorMock.resetAll(); + publicAuthMock.resetAll(); + } + + protected ValidatableResponse postPaymentResponse(String payload) { + return given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + API_KEY) + .post(PAYMENTS_PATH) + .then(); + } + + protected static String toJson(Map map) { + return GSON.toJson(map); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/ZonedDateTimeValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/ZonedDateTimeValidationIT.java new file mode 100644 index 000000000..967b3992d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/ZonedDateTimeValidationIT.java @@ -0,0 +1,41 @@ +package uk.gov.pay.api.it.telephone; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.utils.PublicAuthMockClient; + +import java.util.Map; + +public class ZonedDateTimeValidationIT extends TelephonePaymentResourceITBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setUpBearerTokenAndRequestBody() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + + requestBody.put("amount", 100); + requestBody.put("reference", "Some refeerence"); + requestBody.put("description", "Some description"); + requestBody.put("processor_id", "1PROC"); + requestBody.put("provider_id", "1PROV"); + requestBody.put("payment_outcome", Map.of("status", "success")); + requestBody.put("card_expiry", "01/99"); + requestBody.put("card_type", "visa"); + requestBody.put("last_four_digits", "1234"); + requestBody.put("first_six_digits", "123456"); + } + + @After + public void tearDown() { + requestBody.clear(); + } + + @Test + public void respondWith422_whenDateIsInvalid() { + requestBody.put("created_date", "invalid"); + postPaymentResponse(toJson(requestBody)) + .statusCode(422); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/pact/CreateTelephonePaymentServiceTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/pact/CreateTelephonePaymentServiceTest.java new file mode 100644 index 000000000..03a4a9ba4 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/telephone/pact/CreateTelephonePaymentServiceTest.java @@ -0,0 +1,103 @@ +package uk.gov.pay.api.it.telephone.pact; + +import au.com.dius.pact.consumer.PactVerification; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.model.telephone.CreateTelephonePaymentRequest; +import uk.gov.pay.api.model.telephone.PaymentOutcome; +import uk.gov.pay.api.model.telephone.Supplemental; +import uk.gov.pay.api.model.telephone.TelephonePaymentResponse; +import uk.gov.pay.api.service.ConnectorUriGenerator; +import uk.gov.pay.api.service.telephone.CreateTelephonePaymentService; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; +import java.util.Optional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CreateTelephonePaymentServiceTest { + + private CreateTelephonePaymentService createTelephonePaymentService; + private static CreateTelephonePaymentRequest.Builder builder = new CreateTelephonePaymentRequest.Builder(); + + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + + @Mock + private PublicApiConfig configuration; + + @Before + public void setup() { + when(configuration.getConnectorUrl()).thenReturn(connectorRule.getUrl()); // We will actually send real requests here, which will be intercepted by pact + + ConnectorUriGenerator connectorUriGenerator = new ConnectorUriGenerator(configuration); + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + + createTelephonePaymentService = new CreateTelephonePaymentService(client, connectorUriGenerator); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-telephone-payment-notification"}) + public void testCreatePaymentWithMetadata() { + Account account = new Account("123456", TokenPaymentType.CARD, "a-token-link"); + var createTelephonePaymentNotification =builder + .withAmount(12000) + .withReference("MRPC12345") + .withDescription("New passport application") + .withCreatedDate("2018-02-21T16:04:25Z") + .withAuthorisedDate("2018-02-21T16:05:33Z") + .withProcessorId("183f2j8923j8") + .withProviderId("17498-8412u9-1273891239") + .withAuthCode("auth12345") + .withPaymentOutcome(new PaymentOutcome("failed", "P0010", new Supplemental("ECKOH01234", "textual message describing error code"))) + .withCardType("master-card") + .withNameOnCard("J Doe") + .withEmailAddress("j.doe@example.com") + .withCardExpiry("02/19") + .withLastFourDigits("1234") + .withFirstSixDigits("654321") + .withTelephoneNumber("+447700900796") + .build(); + Pair responseAndStatusCode = createTelephonePaymentService.create(account, createTelephonePaymentNotification); + TelephonePaymentResponse telephonePaymentResponse = responseAndStatusCode.getLeft(); + int statusCode = responseAndStatusCode.getRight(); + + assertThat(statusCode, is(201)); + + assertThat(telephonePaymentResponse.getAmount(), is(12000L)); + assertThat(telephonePaymentResponse.getReference(), is("MRPC12345")); + assertThat(telephonePaymentResponse.getDescription(), is("New passport application")); + assertThat(telephonePaymentResponse.getCreatedDate(), is(Optional.of("2018-02-21T16:04:25.000Z"))); + assertThat(telephonePaymentResponse.getAuthorisedDate(), is(Optional.of("2018-02-21T16:05:33.000Z"))); + assertThat(telephonePaymentResponse.getProcessorId(), is("183f2j8923j8")); + assertThat(telephonePaymentResponse.getProviderId(), is("17498-8412u9-1273891239")); + assertThat(telephonePaymentResponse.getAuthCode(), is(Optional.of("auth12345"))); + assertThat(telephonePaymentResponse.getPaymentOutcome().getStatus(), is("failed")); + assertThat(telephonePaymentResponse.getPaymentOutcome().getCode().get(), is("P0010")); + assertThat(telephonePaymentResponse.getPaymentOutcome().getSupplemental().get().getErrorCode().get(), is("ECKOH01234")); + assertThat(telephonePaymentResponse.getPaymentOutcome().getSupplemental().get().getErrorMessage().get(), is("textual message describing error code")); + assertThat(telephonePaymentResponse.getCardType(), is("master-card")); + assertThat(telephonePaymentResponse.getNameOnCard(), is(Optional.of("J Doe"))); + assertThat(telephonePaymentResponse.getCardExpiry(), is("02/19")); + assertThat(telephonePaymentResponse.getLastFourDigits(), is("1234")); + assertThat(telephonePaymentResponse.getFirstSixDigits(), is("654321")); + assertThat(telephonePaymentResponse.getEmailAddress(), is(Optional.of("j.doe@example.com"))); + assertThat(telephonePaymentResponse.getTelephoneNumber(), is(Optional.of("+447700900796"))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/CreatePaymentWithHttpReturnUrlIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/CreatePaymentWithHttpReturnUrlIT.java new file mode 100644 index 000000000..b412f010f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/CreatePaymentWithHttpReturnUrlIT.java @@ -0,0 +1,113 @@ +package uk.gov.pay.api.it.validation; + +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import io.dropwizard.testing.junit.DropwizardAppRule; +import io.restassured.response.ValidatableResponse; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import uk.gov.pay.api.app.PublicApi; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.it.rule.RedisDockerRule; +import uk.gov.pay.api.utils.ApiKeyGenerator; +import uk.gov.pay.api.utils.JsonStringBuilder; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; + +import java.util.Map; + +import static io.dropwizard.testing.ConfigOverride.config; +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static uk.gov.pay.api.model.TokenPaymentType.CARD; +import static uk.gov.pay.api.utils.mocks.CreateChargeRequestParams.CreateChargeRequestParamsBuilder.aCreateChargeRequestParams; +import static uk.gov.service.payments.commons.testing.port.PortFactory.findFreePort; + +public class CreatePaymentWithHttpReturnUrlIT { + + private static final String API_KEY = ApiKeyGenerator.apiKeyValueOf("TEST_BEARER_TOKEN", "qwer9yuhgf"); + private static final String GATEWAY_ACCOUNT_ID = "GATEWAY_ACCOUNT_ID"; + private static final String PAYMENTS_PATH = "/v1/payments/"; + + @ClassRule + public static RedisDockerRule redisDockerRule = new RedisDockerRule(); + + private static final int CONNECTOR_PORT = findFreePort(); + private static final int PUBLIC_AUTH_PORT = findFreePort(); + + @ClassRule + public static WireMockClassRule connectorMock = new WireMockClassRule(CONNECTOR_PORT); + + @ClassRule + public static WireMockClassRule publicAuthMock = new WireMockClassRule(PUBLIC_AUTH_PORT); + + @ClassRule + public static DropwizardAppRule app = new DropwizardAppRule<>( + PublicApi.class, + resourceFilePath("config/test-config.yaml"), + config("connectorUrl", "http://localhost:" + CONNECTOR_PORT), + config("publicAuthUrl", "http://localhost:" + PUBLIC_AUTH_PORT + "/v1/auth"), + config("redis.endpoint", redisDockerRule.getRedisUrl()), + config("allowHttpForReturnUrl", "true") + ); + + private final PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + private final ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + + @Before + public void setup() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + } + + @Test + public void createSuccessfullyWhenHttpReturnUrl() { + String payload = new JsonStringBuilder() + .add("amount", 100) + .add("reference", "ref") + .add("description", "desc") + .add("return_url", "http://somewhere.com") + .add("metadata", Map.of()) + .build(); + + connectorMockClient.respondCreated_whenCreateCharge(GATEWAY_ACCOUNT_ID, aCreateChargeRequestParams() + .withAmount(100) + .withDescription("desc") + .withReference("ref") + .withReturnUrl("http://somewhere.com") + .build()); + + postPaymentResponse(payload).statusCode(201); + } + + @Test + public void createSuccessfullyWhenHttpsReturnUrl() { + String payload = new JsonStringBuilder() + .add("amount", 100) + .add("reference", "ref") + .add("description", "desc") + .add("return_url", "https://somewhere.com") + .add("metadata", Map.of()) + .build(); + + connectorMockClient.respondCreated_whenCreateCharge(GATEWAY_ACCOUNT_ID, aCreateChargeRequestParams() + .withAmount(100) + .withDescription("desc") + .withReference("ref") + .withReturnUrl("https://somewhere.com") + .build()); + + postPaymentResponse(payload).statusCode(201); + } + + private ValidatableResponse postPaymentResponse(String payload) { + return given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + API_KEY) + .post(PAYMENTS_PATH) + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/CreatePaymentWithPrefilledCardholderDetailsValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/CreatePaymentWithPrefilledCardholderDetailsValidationIT.java new file mode 100644 index 000000000..0468670b2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/CreatePaymentWithPrefilledCardholderDetailsValidationIT.java @@ -0,0 +1,83 @@ +package uk.gov.pay.api.it.validation; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import uk.gov.pay.api.it.PaymentResourceITestBase; +import uk.gov.pay.api.utils.JsonStringBuilder; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; + +import java.util.Map; + +import static io.restassured.http.ContentType.JSON; +import static java.lang.String.format; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.utils.mocks.CreateChargeRequestParams.CreateChargeRequestParamsBuilder.aCreateChargeRequestParams; + +@RunWith(JUnitParamsRunner.class) +public class CreatePaymentWithPrefilledCardholderDetailsValidationIT extends PaymentResourceITestBase { + + private final PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + private final ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + private JsonStringBuilder payload; + + @Before + public void setUpBearerToken() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + payload = new JsonStringBuilder() + .add("amount", 100) + .add("reference", "Ref") + .add("description", "hi") + .add("return_url", "https://somewhere.gov.uk/rainbow/1"); + } + + @Test + public void shouldFailOnInvalidCardHolderName() { + payload.add("prefilled_cardholder_details", Map.of("cardholder_name", randomAlphanumeric(256))); + postPaymentResponse(payload.build()) + .statusCode(422) + .contentType(JSON) + .body("size()", is(3)) + .body("field", is("cardholder_name")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: cardholder_name. Must be less than or equal to 255 characters length")); + } + + @Test + @Parameters({ + "line1, Must be less than or equal to 255 characters length", + "line2, Must be less than or equal to 255 characters length", + "city, Must be less than or equal to 255 characters length", + "postcode, Must be less than or equal to 25 characters length" + }) + public void shouldFailOnInvalidAddress(String addressField, String message) { + payload.add("prefilled_cardholder_details", Map.of("billing_address", + Map.of(addressField, randomAlphanumeric(256)))); + postPaymentResponse(payload.build()) + .statusCode(422) + .contentType(JSON) + .body("size()", is(3)) + .body("field", is(addressField)) + .body("code", is("P0102")) + .body("description", is(format("Invalid attribute value: %s. %s", addressField, message))); + } + + @Test + public void shouldCreateSuccessfullyWithEmptyCountry() { + payload.add("prefilled_cardholder_details", Map.of("billing_address", + Map.of("country", ""))); + + connectorMockClient.respondCreated_whenCreateCharge(GATEWAY_ACCOUNT_ID, aCreateChargeRequestParams() + .withAmount(100) + .withDescription("hi") + .withReference("Ref") + .withReturnUrl("https://somewhere.gov.uk/rainbow/1") + .build()); + + postPaymentResponse(payload.build()).statusCode(201); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsRefundsResourceAmountValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsRefundsResourceAmountValidationIT.java new file mode 100644 index 000000000..b518192ec --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsRefundsResourceAmountValidationIT.java @@ -0,0 +1,349 @@ +package uk.gov.pay.api.it.validation; + +import com.jayway.jsonassert.JsonAssert; +import io.restassured.response.ValidatableResponse; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.it.PaymentResourceITestBase; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.RefundSummary; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; +import uk.gov.service.payments.commons.model.SupportedLanguage; + +import java.io.IOException; +import java.io.InputStream; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.utils.mocks.ChargeResponseFromConnector.ChargeResponseFromConnectorBuilder.aCreateOrGetChargeResponseFromConnector; + +public class PaymentsRefundsResourceAmountValidationIT extends PaymentResourceITestBase { + + private static final int REFUND_AMOUNT_AVAILABLE = 9000; + private static final Address BILLING_ADDRESS = new Address("line1", "line2", "NR2 5 6EG", "city", "UK"); + private static final CardDetailsFromResponse CARD_DETAILS = new CardDetailsFromResponse("1234", "123456", "Mr. Payment", "12/19", BILLING_ADDRESS, "Visa", "credit"); + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + private ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + + @Before + public void setUpBearerToken() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void createPaymentRefund_responseWith422_whenAmountIsNegative() throws IOException { + // language=JSON + String payload = "{\n" + + " \"amount\": -123\n" + + "}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, "chargeId", payload) + .statusCode(422) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0602")) + .assertThat("$.description", is("Invalid attribute value: amount. Must be greater than or equal to 1")); + } + + @Test + public void createPaymentRefund_responseWith422_whenAmountIsBiggerThanTheMaximumAllowed() throws IOException { + // language=JSON + String payload = "{\n" + + " \"amount\": 10000001\n" + + "}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, "chargeId", payload) + .statusCode(422) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0602")) + .assertThat("$.description", is("Invalid attribute value: amount. Must be less than or equal to 10000000")); + } + + @Test + public void createPaymentRefund_responseWith400_whenAmountFieldHasNullValue() throws IOException { + // language=JSON + String payload = "{\n" + + " \"amount\": null\n" + + "}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, "chargeId", payload) + .statusCode(400) + .contentType(JSON) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0601")) + .assertThat("$.description", is("Missing mandatory attribute: amount")); + } + + @Test + public void createPaymentRefund_responseWith400_whenAmountFieldIsNotNumeric() throws IOException { + // language=JSON + String payload = "{\n" + + " \"amount\": \"hola world!\"\n" + + "}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, "chargeId", payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0602")) + .assertThat("$.description", is("Invalid attribute value: amount. Must be a valid numeric format")); + } + + @Test + public void createPaymentRefund_responseWith400_whenAmountFieldIsNotAValidJsonField() throws IOException { + // language=JSON + String payload = "{\n" + + " \"amount\": {\n" + + " \"whatever\": 1\n" + + " }\n" + + "}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, "chargeId", payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0602")) + .assertThat("$.description", is("Invalid attribute value: amount. Must be a valid numeric format")); + } + + @Test + public void createPaymentRefund_responseWith400_whenAmountFieldIsBlank() throws IOException { + // language=JSON + String payload = "{\n" + + " \"amount\": \" \"\n" + + "}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, "chargeId", payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0602")) + .assertThat("$.description", is("Invalid attribute value: amount. Must be a valid numeric format")); + } + + @Test + public void createPaymentRefund_responseWith400_whenAmountFieldIsMissing() throws IOException { + // language=JSON + String payload = "{}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, "chargeId", payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0601")) + .assertThat("$.description", is("Missing mandatory attribute: amount")); + } + + @Test + public void createPaymentRefund_responseWith400_whenAmountIsHexadecimal() throws IOException { + // language=JSON + String payload = "{\n" + + " \"amount\": 0x1000\n" + + "}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, "chargeId", payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0697")) + .assertThat("$.description", is("Unable to parse JSON")); + } + + @Test + public void createPaymentRefund_responseWith400_whenAmountIsBinary() throws IOException { + // language=JSON + String payload = "{\n" + + " \"amount\": 0B101\n" + + "}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, "chargeId", payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0697")) + .assertThat("$.description", is("Unable to parse JSON")); + } + + @Test + public void createPaymentRefund_responseWith400_whenAmountIsOctal() throws IOException { + // language=JSON + String payload = "{\n" + + " \"amount\": 017\n" + + "}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, "chargeId", payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0697")) + .assertThat("$.description", is("Unable to parse JSON")); + } + + @Test + public void createPaymentRefund_responseWith400_whenAmountIsNullByteEncoded() throws IOException { + // language=JSON + String payload = "{\n" + + " \"amount\": %00\n" + + "}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, "chargeId", payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0697")) + .assertThat("$.description", is("Unable to parse JSON")); + } + + @Test + public void createPaymentRefund_responseWith400_whenAmountIsFloat() throws IOException { + // language=JSON + String payload = "{\n" + + " \"amount\": 27.55\n" + + "}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, "chargeId", payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0602")) + .assertThat("$.description", is("Invalid attribute value: amount. Must be a valid numeric format")); + } + + @Test + public void createPaymentRefund_responseWith400_whenConnectorResponseIsErrorDueToAmountRequestedIsNotAvailableForRefund() throws IOException { + int amount = 1000; + String externalChargeId = "charge_12345"; + + connectorMockClient.respondWithChargeFound(null, GATEWAY_ACCOUNT_ID, + aCreateOrGetChargeResponseFromConnector() + .withAmount(amount) + .withChargeId(externalChargeId) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(false) + .withRefundSummary(new RefundSummary("available", REFUND_AMOUNT_AVAILABLE, 1000)) + .withCardDetails(CARD_DETAILS) + .withGatewayTransactionId("gatewayTransactionId") + .build()); + + connectorMockClient.respondBadRequest_whenCreateARefund("full", GATEWAY_ACCOUNT_ID, externalChargeId); + + String refundRequest = "{\"amount\":" + amount + "}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, externalChargeId, refundRequest) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0603")) + .assertThat("$.description", is("The payment is not available for refund. Payment refund status: full")); + } + + @Test + public void createPaymentRefund_responseWith400_whenConnectorResponseIsErrorDueToChargeStatusMakesPaymentNonRefundable() throws IOException { + int amount = 1000; + String externalChargeId = "charge_12345"; + + connectorMockClient.respondWithChargeFound(null, GATEWAY_ACCOUNT_ID, + aCreateOrGetChargeResponseFromConnector() + .withAmount(amount) + .withChargeId(externalChargeId) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(false) + .withRefundSummary(new RefundSummary("available", REFUND_AMOUNT_AVAILABLE, 1000)) + .withCardDetails(CARD_DETAILS) + .withGatewayTransactionId("gatewayTransactionId") + .build()); + + connectorMockClient.respondBadRequest_whenCreateARefund("pending", GATEWAY_ACCOUNT_ID, externalChargeId); + + String refundRequest = "{\"amount\":" + amount + "}"; + + InputStream body = postPaymentRefundAndThen(API_KEY, externalChargeId, refundRequest) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0603")) + .assertThat("$.description", is("The payment is not available for refund. Payment refund status: pending")); + } + + private ValidatableResponse postPaymentRefundAndThen(String bearerToken, String chargeId, String payload) { + return given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + bearerToken) + .post(PAYMENTS_PATH + chargeId + "/refunds") + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceAmountValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceAmountValidationIT.java new file mode 100644 index 000000000..743aaab49 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceAmountValidationIT.java @@ -0,0 +1,330 @@ +package uk.gov.pay.api.it.validation; + +import com.jayway.jsonassert.JsonAssert; +import io.restassured.response.ValidatableResponse; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.it.PaymentResourceITestBase; +import uk.gov.pay.api.utils.PublicAuthMockClient; + +import java.io.IOException; +import java.io.InputStream; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.core.Is.is; + +public class PaymentsResourceAmountValidationIT extends PaymentResourceITestBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setUpBearerToken() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void createPayment_responseWith422_whenAmountIsNegative() throws IOException { + + String payload = "{" + + " \"amount\" : -123," + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://somewhere.gov.uk/rainbow/1\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(422) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0102")) + .assertThat("$.description", is("Invalid attribute value: amount. Must be greater than or equal to 1")); + } + + @Test + public void createPayment_responseWith422_whenAmountIsBiggerThanTheMaximumAllowed() throws IOException { + + String payload = "{" + + " \"amount\" : 10000001," + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://somewhere.gov.uk/rainbow/1\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(422) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0102")) + .assertThat("$.description", is("Invalid attribute value: amount. Must be less than or equal to 10000000")); + } + + @Test + public void createPayment_responseWith400_whenAmountFieldHasNullValue() throws IOException { + + String payload = "{" + + " \"amount\" : null," + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://somewhere.gov.uk/rainbow/1\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0101")) + .assertThat("$.description", is("Missing mandatory attribute: amount")); + } + + @Test + public void createPayment_responseWith400_whenAmountFieldIsNotNumeric() throws IOException { + + String payload = "{" + + " \"amount\" : \"hola world!\"," + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://somewhere.gov.uk/rainbow/1\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0102")) + .assertThat("$.description", is("Invalid attribute value: amount. Must be a valid numeric format")); + } + + @Test + public void createPayment_responseWith400_whenAmountFieldIsNotAValidJsonField() throws IOException { + + String payload = "{" + + " \"amount\" : { \"whatever\": 1 }," + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://somewhere.gov.uk/rainbow/1\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0102")) + .assertThat("$.description", is("Invalid attribute value: amount. Must be a valid numeric format")); + } + + @Test + public void createPayment_responseWith400_whenAmountFieldIsBlank() throws IOException { + + String payload = "{" + + " \"amount\" : \" \"," + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://somewhere.gov.uk/rainbow/1\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0102")) + .assertThat("$.description", is("Invalid attribute value: amount. Must be a valid numeric format")); + } + + @Test + public void createPayment_responseWith400_whenAmountFieldIsMissing() throws IOException { + + String payload = "{" + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://somewhere.gov.uk/rainbow/1\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0101")) + .assertThat("$.description", is("Missing mandatory attribute: amount")); + } + + @Test + public void createPayment_responseWith400_whenAmountIsHexadecimal() throws IOException { + + String payload = "{" + + " \"amount\" : 0x1000," + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://somewhere.gov.uk/rainbow/1\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0197")) + .assertThat("$.description", is("Unable to parse JSON")); + } + + @Test + public void createPayment_responseWith400_whenAmountIsBinary() throws IOException { + + String payload = "{" + + " \"amount\" : 0B101," + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://somewhere.gov.uk/rainbow/1\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0197")) + .assertThat("$.description", is("Unable to parse JSON")); + } + + @Test + public void createPayment_responseWith400_whenAmountIsOctal() throws IOException { + + String payload = "{" + + " \"amount\" : 017," + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://somewhere.gov.uk/rainbow/1\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0197")) + .assertThat("$.description", is("Unable to parse JSON")); + } + + @Test + public void createPayment_responseWith400_whenAmountIsNullByteEncoded() throws IOException { + + String payload = "{" + + " \"amount\" : %00," + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://somewhere.gov.uk/rainbow/1\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0197")) + .assertThat("$.description", is("Unable to parse JSON")); + } + + @Test + public void createPayment_responseWith400_whenAmountIsFloat() throws IOException { + + String payload = "{" + + " \"amount\" : 27.55," + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://somewhere.gov.uk/rainbow/1\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0102")) + .assertThat("$.description", is("Invalid attribute value: amount. Must be a valid numeric format")); + } + + @Test + public void createPayment_responseWith400_whenAmountMissing_failFast() throws IOException { + + String payload = "{" + + " \"reference\" : \"whatever\"," + + " \"return_url\" : \"whatever\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("amount")) + .assertThat("$.code", is("P0101")) + .assertThat("$.description", is("Missing mandatory attribute: amount")); + } + + private ValidatableResponse postPaymentResponse(String bearerToken, String payload) { + return given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + bearerToken) + .post(PAYMENTS_PATH) + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceDescriptionValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceDescriptionValidationIT.java new file mode 100644 index 000000000..a7bae6e74 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceDescriptionValidationIT.java @@ -0,0 +1,223 @@ +package uk.gov.pay.api.it.validation; + +import com.jayway.jsonassert.JsonAssert; +import io.restassured.response.ValidatableResponse; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.it.PaymentResourceITestBase; +import uk.gov.pay.api.utils.PublicAuthMockClient; + +import java.io.IOException; +import java.io.InputStream; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.core.Is.is; + +public class PaymentsResourceDescriptionValidationIT extends PaymentResourceITestBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setUpBearerToken() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void createPayment_responseWith400_whenDescriptionIsNumeric() throws IOException { + + String payload = "{" + + " \"amount\" : 9900," + + " \"description\" : 1234," + + " \"reference\" : \"Some reference\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("description")) + .assertThat("$.code", is("P0102")) + .assertThat("$.description", is("Invalid attribute value: description. Must be a valid string format")); + } + + @Test + public void createPayment_responseWith400_whenDescriptionIsEmpty() throws IOException { + + String payload = "{" + + " \"amount\" : 9900," + + " \"description\" : \"\"," + + " \"reference\" : \"Some reference\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("description")) + .assertThat("$.code", is("P0101")) + .assertThat("$.description", is("Missing mandatory attribute: description")); + } + + @Test + public void createPayment_responseWith400_whenDescriptionIsBlank() throws IOException { + + String payload = "{" + + " \"amount\" : 9900," + + " \"description\" : \" \"," + + " \"reference\" : \"Some reference\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("description")) + .assertThat("$.code", is("P0101")) + .assertThat("$.description", is("Missing mandatory attribute: description")); + } + + @Test + public void createPayment_responseWith400_whenDescriptionIsMissing() throws IOException { + + String payload = "{" + + " \"amount\" : 9900," + + " \"reference\" : \"Some reference\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("description")) + .assertThat("$.code", is("P0101")) + .assertThat("$.description", is("Missing mandatory attribute: description")); + } + + @Test + public void createPayment_responseWith400_whenDescriptionIsNull() throws IOException { + + + String payload = "{" + + " \"amount\" : 9900," + + " \"description\" : null," + + " \"reference\" : \"Some reference\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("description")) + .assertThat("$.code", is("P0101")) + .assertThat("$.description", is("Missing mandatory attribute: description")); + } + + @Test + public void createPayment_responseWith422_whenDescriptionSizeIsGreaterThanMaxLength() throws IOException { + + String aVeryLongReference = RandomStringUtils.randomAlphanumeric(256); + + String payload = "{" + + " \"amount\" : 9900," + + " \"description\" : \"" + aVeryLongReference + "\"," + + " \"reference\" : \"Some reference\"," + + " \"return_url\" : \"https://www.example.com/return_url\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(422) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("description")) + .assertThat("$.code", is("P0102")) + .assertThat("$.description", is("Invalid attribute value: description. Must be less than or equal to 255 characters length")); + } + + @Test + public void createPayment_responseWith400_whenDescriptionHasNotAValidJsonValue() throws IOException { + + String payload = "{" + + " \"amount\" : 9900," + + " \"description\" : " + + " \"reference\" : \"Some reference\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0197")) + .assertThat("$.description", is("Unable to parse JSON")); + } + + @Test + public void createPayment_responseWith400_whenDescriptionFieldIsNotExpectedJsonField() throws IOException { + + String payload = "{" + + " \"amount\" : 9900," + + " \"description\" : {\"whatever\" : 1}," + + " \"reference\" : \"Some reference\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("description")) + .assertThat("$.code", is("P0102")) + .assertThat("$.description", is("Invalid attribute value: description. Must be a valid string format")); + } + + private ValidatableResponse postPaymentResponse(String bearerToken, String payload) { + return given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + bearerToken) + .post(PAYMENTS_PATH) + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceEmailValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceEmailValidationIT.java new file mode 100644 index 000000000..26d220c13 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceEmailValidationIT.java @@ -0,0 +1,40 @@ +package uk.gov.pay.api.it.validation; + +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.it.PaymentResourceITestBase; +import uk.gov.pay.api.utils.PublicAuthMockClient; + +import java.util.Map; + +import static io.restassured.http.ContentType.JSON; +import static org.hamcrest.core.Is.is; + +public class PaymentsResourceEmailValidationIT extends PaymentResourceITestBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setUpBearerToken() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void shouldRespondWith422_whenEmailIsGreaterThan254Chars() { + + String payload = toJson( + Map.of("amount", 100, + "reference", "Some ref", + "description", + "hi", "return_url", "https://somewhere.gov.uk/rainbow/1", + "email", "aVeryLongEmail".repeat(20) + "@email.invalid")); + + postPaymentResponse(payload) + .statusCode(422) + .contentType(JSON) + .body("size()", is(3)) + .body("field", is("email")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: email. Must be less than or equal to 254 characters length")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceLanguageValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceLanguageValidationIT.java new file mode 100644 index 000000000..442ae4c98 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceLanguageValidationIT.java @@ -0,0 +1,149 @@ +package uk.gov.pay.api.it.validation; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import uk.gov.pay.api.it.PaymentResourceITestBase; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; + +import java.util.Map; + +import static io.restassured.http.ContentType.JSON; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.utils.mocks.CreateChargeRequestParams.CreateChargeRequestParamsBuilder.aCreateChargeRequestParams; + +@RunWith(JUnitParamsRunner.class) +public class PaymentsResourceLanguageValidationIT extends PaymentResourceITestBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + private ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorMock); + + @Before + public void setUpBearerToken() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + @Parameters({"en", "cy"}) + public void valid(String language) { + String payload = toJson( + Map.of("amount", 100, + "reference", "Some ref", + "description","hi", + "return_url", "https://somewhere.gov.uk/rainbow/1", + "email", "dorothy@rainbow.com", + "language", language)); + + connectorMockClient.respondCreated_whenCreateCharge(GATEWAY_ACCOUNT_ID, aCreateChargeRequestParams() + .withAmount(100) + .withDescription("hi") + .withReference("Some ref") + .withReturnUrl("https://somewhere.gov.uk/rainbow/1") + .build()); + + postPaymentResponse(payload).statusCode(201); + } + + @Test + @Parameters({"fr,422", " ,400", ",400"}) + public void invalidLanguage(String language, int statusCode) { + String payload = toJson( + Map.of("amount", 100, + "reference", "Some ref", + "description","hi", + "return_url", "https://somewhere.gov.uk/rainbow/1", + "email", "dorothy@rainbow.com", + "language", language)); + + postPaymentResponse(payload) + .statusCode(statusCode) + .contentType(JSON) + .body("size()", is(3)) + .body("field", is("language")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: language. Must be \"en\" or \"cy\"")); + } + + @Test + public void createPayment_responseWith400_whenLanguageIsNumeric() { + // language=JSON + String payload = "{\n" + + " \"amount\": 9900,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://example.com\",\n" + + " \"language\": 1337\n" + + "}"; + + postPaymentResponse(payload) + .statusCode(400) + .contentType(JSON) + .body("size()", is(3)) + .body("field", is("language")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: language. Must be \"en\" or \"cy\"")); + } + + @Test + public void createPayment_responseWith400_whenLanguageIsNull() { + // language=JSON + String payload = "{\n" + + " \"amount\": 9900,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://example.com\",\n" + + " \"language\": null\n" + + "}"; + + postPaymentResponse(payload) + .statusCode(400) + .contentType(JSON) + .body("size()", is(3)) + .body("field", is("language")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: language. Must be \"en\" or \"cy\"")); + } + + @Test + public void createPayment_responseWith400_whenLanguageHasNotAValidJsonValue() { + String payload = "{" + + " \"amount\" : 9900," + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://example.com\"," + + " \"language\" : " + + "}"; + + postPaymentResponse(payload) + .statusCode(400) + .contentType(JSON) + .body("size()", is(2)) + .body("code", is("P0197")) + .body("description", is("Unable to parse JSON")); + } + + @Test + public void createPayment_responseWith400_whenLanguageFieldIsNotExpectedJsonField() { + // language=JSON + String payload = "{\n" + + " \"amount\": 9900,\n" + + " \"language\": {\n" + + " \"whatever\": 1\n" + + " },\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://example.com\"\n" + + "}"; + + postPaymentResponse(payload) + .statusCode(400) + .contentType(JSON) + .body("size()", is(3)) + .body("field", is("language")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: language. Must be \"en\" or \"cy\"")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceMetadataValidationFailuresIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceMetadataValidationFailuresIT.java new file mode 100644 index 000000000..7469fd28f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceMetadataValidationFailuresIT.java @@ -0,0 +1,163 @@ +package uk.gov.pay.api.it.validation; + +import com.fasterxml.jackson.databind.JsonNode; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.apache.http.HttpStatus; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import uk.gov.pay.api.it.PaymentResourceITestBase; +import uk.gov.pay.api.utils.JsonStringBuilder; +import uk.gov.pay.api.utils.PublicAuthMockClient; +import uk.gov.pay.api.utils.mocks.CreateChargeRequestParams; +import uk.gov.pay.api.utils.mocks.CreateChargeRequestParams.CreateChargeRequestParamsBuilder; +import uk.gov.service.payments.commons.model.charge.ExternalMetadata; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +import static io.restassured.http.ContentType.JSON; +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toUnmodifiableMap; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.it.PaymentsResourceCreateIT.paymentPayload; +import static uk.gov.pay.api.model.TokenPaymentType.CARD; +import static uk.gov.pay.api.utils.mocks.CreateChargeRequestParams.CreateChargeRequestParamsBuilder.aCreateChargeRequestParams; + +@RunWith(JUnitParamsRunner.class) +public class PaymentsResourceMetadataValidationFailuresIT extends PaymentResourceITestBase { + + private static CreateChargeRequestParamsBuilder createChargeRequestParamsBuilder = aCreateChargeRequestParams() + .withAmount(100) + .withDescription("DESCRIPTION") + .withReference("REFERENCE") + .withReturnUrl("https://somewhere.gov.uk/rainbow/1"); + + private static final String TOO_LONG_KEY = IntStream.rangeClosed(1, ExternalMetadata.MAX_KEY_LENGTH + 1).mapToObj(i -> "k").collect(joining()); + private static final String TOO_LONG_VALUE = IntStream.rangeClosed(1, ExternalMetadata.MAX_VALUE_LENGTH + 1).mapToObj(i -> "v").collect(joining()); + + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void before() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, CARD); + } + + @Test + public void valueIsNotAStringBooleanOrNumber() { + CreateChargeRequestParams createChargeRequestParams = createChargeRequestParamsBuilder + .withMetadata(Map.of("foo", List.of("cake", "chocolate"), "bar", Map.of("a", "b"))) + .build(); + + assertMetadataValidationError(createChargeRequestParams, + "Invalid attribute value: metadata. Values must be of type String, Boolean or Number"); + } + + @Test + @Parameters({"", " "}) + public void keyIsInvalid(String key) { + var createChargeRequestParams = createChargeRequestParamsBuilder.withMetadata(Map.of(key, "boo")).build(); + + assertMetadataValidationError(createChargeRequestParams, + "Invalid attribute value: metadata. Keys must be between " + ExternalMetadata.MIN_KEY_LENGTH + " and " + + ExternalMetadata.MAX_KEY_LENGTH + " characters long"); + } + + @Test + public void keyIsTooLong() { + var createChargeRequestParams = createChargeRequestParamsBuilder.withMetadata(Map.of(TOO_LONG_KEY, "boo")).build(); + + assertMetadataValidationError(createChargeRequestParams, + "Invalid attribute value: metadata. Keys must be between " + ExternalMetadata.MIN_KEY_LENGTH + " and " + + ExternalMetadata.MAX_KEY_LENGTH + " characters long"); + } + + @Test + public void valueIsNull() { + Map metadata = new HashMap<>() {{ put("key", null); }}; + var createChargeRequestParams = createChargeRequestParamsBuilder.withMetadata(metadata).build(); + + assertMetadataValidationError(createChargeRequestParams, "Invalid attribute value: metadata. Must not have null values"); + } + + @Test + public void valueIsTooLong() { + var createChargeRequestParams = createChargeRequestParamsBuilder.withMetadata(Map.of("key", TOO_LONG_VALUE)).build(); + + assertMetadataValidationError(createChargeRequestParams, + "Invalid attribute value: metadata. Values must be no greater than " + ExternalMetadata.MAX_VALUE_LENGTH + " characters long"); + } + + @Test + public void moreThanMaxKeyValuePairs() { + Map metadata = IntStream.rangeClosed(1, ExternalMetadata.MAX_KEY_VALUE_PAIRS + 1) + .boxed().collect(toUnmodifiableMap(i -> "key " + i, i -> "value " + i)); + + var createChargeRequestParams = createChargeRequestParamsBuilder.withMetadata(metadata).build(); + + assertMetadataValidationError(createChargeRequestParams, + "Invalid attribute value: metadata. Cannot have more than " + ExternalMetadata.MAX_KEY_VALUE_PAIRS + " key-value pairs"); + } + + @Test + public void metadataIsNotAnObject() { + var createChargeRequestParams = createChargeRequestParamsBuilder.build(); + + JsonStringBuilder payload = new JsonStringBuilder() + .add("amount", createChargeRequestParams.getAmount()) + .add("reference", createChargeRequestParams.getReference()) + .add("description", createChargeRequestParams.getDescription()) + .add("return_url", createChargeRequestParams.getReturnUrl()) + .add("metadata", "something"); + + postPaymentResponse(payload.build()) + .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) + .contentType(JSON) + .body("field", is("metadata")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: metadata. Must be an object of JSON key-value pairs")); + } + + @Test + public void testMultipleValidationErrors() { + var metadata = Map.of( + "key", TOO_LONG_VALUE, + TOO_LONG_KEY, "fuh", + "keyForBadValue", List.of("cake", "chocolate") + ); + + var createChargeRequestParams = createChargeRequestParamsBuilder.withMetadata(metadata).build(); + + JsonNode jsonBody = postPaymentResponse(paymentPayload(createChargeRequestParams)) + .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) + .contentType(JSON) + .body("field", is("metadata")) + .body("code", is("P0102")) + .extract().body().as(JsonNode.class); + + var descriptions = asList(jsonBody.get("description").asText() + .replace("Invalid attribute value: metadata. ", "") + .split("\\. ")); + assertThat(descriptions, hasItems( + "Values must be no greater than " + ExternalMetadata.MAX_VALUE_LENGTH + " characters long", + "Keys must be between " + ExternalMetadata.MIN_KEY_LENGTH + " and " + ExternalMetadata.MAX_KEY_LENGTH + " characters long", + "Values must be of type String, Boolean or Number" + )); + } + + private void assertMetadataValidationError(CreateChargeRequestParams createChargeRequestParams, String message) { + postPaymentResponse(paymentPayload(createChargeRequestParams)) + .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) + .contentType(JSON) + .body("field", is("metadata")) + .body("code", is("P0102")) + .body("description", is(message)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceReferenceValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceReferenceValidationIT.java new file mode 100644 index 000000000..140a2968b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceReferenceValidationIT.java @@ -0,0 +1,223 @@ +package uk.gov.pay.api.it.validation; + +import com.jayway.jsonassert.JsonAssert; +import io.restassured.response.ValidatableResponse; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.it.PaymentResourceITestBase; +import uk.gov.pay.api.utils.PublicAuthMockClient; + +import java.io.IOException; +import java.io.InputStream; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.core.Is.is; + +public class PaymentsResourceReferenceValidationIT extends PaymentResourceITestBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setUpBearerToken() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void createPayment_responseWith400_whenReferenceIsNumeric() throws IOException { + + String payload = "{" + + " \"amount\" : 9900," + + " \"reference\" : 1234," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("reference")) + .assertThat("$.code", is("P0102")) + .assertThat("$.description", is("Invalid attribute value: reference. Must be a valid string format")); + } + + @Test + public void createPayment_responseWith400_whenReferenceIsEmpty() throws IOException { + + String payload = "{" + + " \"amount\" : 9900," + + " \"reference\" : \"\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("reference")) + .assertThat("$.code", is("P0101")) + .assertThat("$.description", is("Missing mandatory attribute: reference")); + } + + @Test + public void createPayment_responseWith400_whenReferenceIsBlank() throws IOException { + + String payload = "{" + + " \"amount\" : 9900," + + " \"reference\" : \" \"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("reference")) + .assertThat("$.code", is("P0101")) + .assertThat("$.description", is("Missing mandatory attribute: reference")); + } + + @Test + public void createPayment_responseWith400_whenReferenceIsMissing() throws IOException { + + String payload = "{" + + " \"amount\" : 9900," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("reference")) + .assertThat("$.code", is("P0101")) + .assertThat("$.description", is("Missing mandatory attribute: reference")); + } + + @Test + public void createPayment_responseWith400_whenReferenceIsNull() throws IOException { + + + String payload = "{" + + " \"amount\" : 9900," + + " \"reference\" : null," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("reference")) + .assertThat("$.code", is("P0101")) + .assertThat("$.description", is("Missing mandatory attribute: reference")); + } + + @Test + public void createPayment_responseWith422_whenReferenceSizeIsGreaterThanMaxLength() throws IOException { + + String aVeryLongReference = RandomStringUtils.randomAlphanumeric(256); + + String payload = "{" + + " \"amount\" : 9900," + + " \"reference\" : \"" + aVeryLongReference + "\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://www.example.com/return_url\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(422) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("reference")) + .assertThat("$.code", is("P0102")) + .assertThat("$.description", is("Invalid attribute value: reference. Must be less than or equal to 255 characters length")); + } + + @Test + public void createPayment_responseWith400_whenReferenceHasNotAValidJsonValue() throws IOException { + + String payload = "{" + + " \"amount\" : 9900," + + " \"reference\" : " + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0197")) + .assertThat("$.description", is("Unable to parse JSON")); + } + + @Test + public void createPayment_responseWith400_whenReferenceFieldIsNotExpectedJsonField() throws IOException { + + String payload = "{" + + " \"amount\" : 9900," + + " \"reference\" : {\"whatever\" : 1}," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://example.com\"" + + "}"; + + InputStream body = postPaymentResponse(API_KEY, payload) + .statusCode(400) + .contentType(JSON) + .extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(3)) + .assertThat("$.field", is("reference")) + .assertThat("$.code", is("P0102")) + .assertThat("$.description", is("Invalid attribute value: reference. Must be a valid string format")); + } + + private ValidatableResponse postPaymentResponse(String bearerToken, String payload) { + return given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + bearerToken) + .post(PAYMENTS_PATH) + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceSearchValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceSearchValidationIT.java new file mode 100644 index 000000000..db73184b6 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PaymentsResourceSearchValidationIT.java @@ -0,0 +1,255 @@ +package uk.gov.pay.api.it.validation; + +import com.google.common.collect.ImmutableMap; +import com.jayway.jsonassert.JsonAssert; +import io.restassured.response.ValidatableResponse; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Before; +import org.junit.Test; +import uk.gov.pay.api.it.PaymentResourceITestBase; +import uk.gov.pay.api.utils.PublicAuthMockClient; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.core.Is.is; + +public class PaymentsResourceSearchValidationIT extends PaymentResourceITestBase { + + private static final String VALID_REFERENCE = "test_reference"; + private static final String VALID_LAST_DIGITS_CARD_NUMBER = "4242"; + private static final String VALID_FIRST_DIGITS_CARD_NUMBER = "123456"; + private static final String VALID_STATE = "success"; + private static final String VALID_EMAIL = "alice.111@mail.fake"; + private static final String VALID_FROM_DATE = "2016-01-28T00:00:00Z"; + private static final String VALID_TO_DATE = "2016-01-28T12:00:00Z"; + private static final String INVALID_EMAIL = RandomStringUtils.randomAlphanumeric(254) + "@mail.fake"; + + private static final String SEARCH_PATH = "/v1/payments"; + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void mapBearerTokenToAccountId() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID); + } + + @Test + public void searchPayments_errorWhenToDateIsNotInZoneDateTimeFormat() throws Exception { + InputStream body = searchPayments(API_KEY, + ImmutableMap.builder() + .put("reference", VALID_REFERENCE) + .put("email", VALID_EMAIL) + .put("state", VALID_STATE) + .put("first_digits_card_number", VALID_FIRST_DIGITS_CARD_NUMBER) + .put("last_digits_card_number", VALID_LAST_DIGITS_CARD_NUMBER) + .put("from_date", VALID_FROM_DATE) + .put("to_date", "2016-01-01 00:00") + .build() + ) + .statusCode(422) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0401")) + .assertThat("$.description", is("Invalid parameters: to_date. See Public API documentation for the correct data formats")); + } + + @Test + public void searchPayments_errorWhenFromDateIsNotInZoneDateTimeFormat() throws Exception { + InputStream body = searchPayments(API_KEY, + ImmutableMap.of( + "reference", VALID_REFERENCE, + "email", VALID_EMAIL, + "state", VALID_STATE, + "from_date", "2016-01-01 00:00", + "to_date", VALID_TO_DATE)) + .statusCode(422) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0401")) + .assertThat("$.description", is("Invalid parameters: from_date. See Public API documentation for the correct data formats")); + } + + @Test + public void searchPayments_errorWhenStatusNotMatchingWithExpectedExternalStatuses() throws Exception { + InputStream body = searchPayments(API_KEY, + ImmutableMap.of( + "reference", VALID_REFERENCE, + "email", VALID_EMAIL, + "state", "invalid state", + "from_date", VALID_FROM_DATE, + "to_date", VALID_TO_DATE)) + .statusCode(422) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0401")) + .assertThat("$.description", is("Invalid parameters: state. See Public API documentation for the correct data formats")); + } + + @Test + public void searchPayments_errorWhenReferenceSizeIsLongerThan255() throws Exception { + InputStream body = searchPayments(API_KEY, + ImmutableMap.of( + "reference", RandomStringUtils.randomAlphanumeric(256), + "email", VALID_EMAIL, + "state", VALID_STATE, + "from_date", VALID_FROM_DATE, + "to_date", VALID_TO_DATE)) + .statusCode(422) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0401")) + .assertThat("$.description", is("Invalid parameters: reference. See Public API documentation for the correct data formats")); + } + + @Test + public void searchPayments_errorWhenEmailSizeIsLongerThan254() throws Exception { + InputStream body = searchPayments(API_KEY, + ImmutableMap.of( + "reference", "ref", + "email", RandomStringUtils.randomAlphanumeric(254) + "@mail.fake", + "state", VALID_STATE, + "from_date", VALID_FROM_DATE, + "to_date", VALID_TO_DATE)) + .statusCode(422) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0401")) + .assertThat("$.description", is("Invalid parameters: email. See Public API documentation for the correct data formats")); + } + + @Test + public void searchPayments_errorWhenToDateNotInZoneDateTimeFormat_andInvalidStatus() throws Exception { + InputStream body = searchPayments(API_KEY, + ImmutableMap.of( + "reference", VALID_REFERENCE, + "email", VALID_EMAIL, + "state", "invalid state", + "from_date", VALID_FROM_DATE, + "to_date", "2016-01-01 00:00")) + .statusCode(422) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0401")) + .assertThat("$.description", is("Invalid parameters: state, to_date. See Public API documentation for the correct data formats")); + } + + @Test + public void searchPayments_errorWhenFromAndToDatesAreNotInZoneDateTimeFormat() throws Exception { + InputStream body = searchPayments(API_KEY, + ImmutableMap.of( + "reference", VALID_REFERENCE, + "email", VALID_EMAIL, + "state", VALID_STATE, + "from_date", "12345", + "to_date", "2016-01-01 00:00")) + .statusCode(422) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0401")) + .assertThat("$.description", is("Invalid parameters: from_date, to_date. See Public API documentation for the correct data formats")); + } + + @Test + public void searchPayments_errorWhenCardDigitsAreInvalid() throws Exception { + InputStream body = searchPayments(API_KEY, + ImmutableMap.builder() + .put("reference", VALID_REFERENCE) + .put("email", VALID_EMAIL) + .put("state", VALID_STATE) + .put("first_digits_card_number", "4a4234") + .put("last_digits_card_number", "423") + .put("from_date", VALID_FROM_DATE) + .put("to_date", VALID_TO_DATE) + .build() + ) + .statusCode(422) + .contentType(JSON).extract() + .body().asInputStream(); + + String json = IOUtils.toString(body, StandardCharsets.UTF_8); + + JsonAssert.with(json) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0401")) + .assertThat("$.description", is("Invalid parameters: first_digits_card_number, last_digits_card_number. See Public API documentation for the correct data formats")); + } + + @Test + public void searchPayments_errorWhenAllFieldsAreInvalid() throws Exception { + InputStream body = searchPayments(API_KEY, + ImmutableMap.of( + "reference", RandomStringUtils.randomAlphanumeric(256), + "email", INVALID_EMAIL, + "state", "invalid state", + "from_date", "12345", + "to_date", "98765")) + .statusCode(422) + .contentType(JSON).extract() + .body().asInputStream(); + + String json = IOUtils.toString(body, StandardCharsets.UTF_8); + + JsonAssert.with(json) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0401")) + .assertThat("$.description", is("Invalid parameters: state, reference, email, from_date, to_date. See Public API documentation for the correct data formats")); + } + + @Test + public void searchPayments_errorWhenDisplaySizeInvalid() throws Exception { + InputStream body = searchPayments(API_KEY, + ImmutableMap.builder() + .put("reference", VALID_REFERENCE) + .put("email", VALID_EMAIL) + .put("state", VALID_STATE) + .put("from_date", VALID_FROM_DATE) + .put("to_date", VALID_TO_DATE) + .put("display_size", "501") + .build()) + .statusCode(422) + .contentType(JSON).extract() + .body().asInputStream(); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.code", is("P0401")) + .assertThat("$.description", is("Invalid parameters: display_size. See Public API documentation for the correct data formats")); + } + + private ValidatableResponse searchPayments(String bearerToken, ImmutableMap queryParams) { + return given().port(app.getLocalPort()) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + bearerToken) + .queryParams(queryParams) + .get(SEARCH_PATH) + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PublicApiConfigIT/EmptyElevatedAccountsPublicApiConfigIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PublicApiConfigIT/EmptyElevatedAccountsPublicApiConfigIT.java new file mode 100644 index 000000000..5d8f59072 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PublicApiConfigIT/EmptyElevatedAccountsPublicApiConfigIT.java @@ -0,0 +1,34 @@ +package uk.gov.pay.api.it.validation.PublicApiConfigIT; + +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit.DropwizardAppRule; +import org.junit.Rule; +import org.junit.Test; +import uk.gov.pay.api.app.PublicApi; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RateLimiterConfig; + +import java.util.Collections; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class EmptyElevatedAccountsPublicApiConfigIT { + + @Rule + public final DropwizardAppRule RULE = + new DropwizardAppRule<>(PublicApi.class, ResourceHelpers.resourceFilePath("config/empty-elevated-accounts-test-config.yaml")); + + @Test + public void shouldParseConfiguration() { + RateLimiterConfig rateLimiterConfig = RULE.getConfiguration().getRateLimiterConfig(); + assertThat(rateLimiterConfig.getNoOfReq(), is(1000)); + assertThat(rateLimiterConfig.getPerMillis(), is(1000)); + assertThat(rateLimiterConfig.getNoOfReqForPost(), is(1000)); + assertThat(rateLimiterConfig.getNoOfReqPerNode(), is(1)); + assertThat(rateLimiterConfig.getNoOfReqForPostPerNode(), is(1)); + assertThat(rateLimiterConfig.getNoOfReqForElevatedAccounts(), is(1000)); + assertThat(rateLimiterConfig.getNoOfPostReqForElevatedAccounts(), is(1000)); + assertThat(rateLimiterConfig.getElevatedAccounts(), is(Collections.emptyList())); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PublicApiConfigIT/EmptyLowTrafficAccountsPublicApiConfigIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PublicApiConfigIT/EmptyLowTrafficAccountsPublicApiConfigIT.java new file mode 100644 index 000000000..e7d20eb2e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PublicApiConfigIT/EmptyLowTrafficAccountsPublicApiConfigIT.java @@ -0,0 +1,30 @@ +package uk.gov.pay.api.it.validation.PublicApiConfigIT; + +import io.dropwizard.testing.junit.DropwizardAppRule; +import org.junit.Rule; +import org.junit.Test; +import uk.gov.pay.api.app.PublicApi; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RateLimiterConfig; + +import java.util.Collections; + +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class EmptyLowTrafficAccountsPublicApiConfigIT { + + @Rule + public final DropwizardAppRule RULE = new DropwizardAppRule<>(PublicApi.class, + resourceFilePath("config/empty-low-traffic-accounts-test-config.yaml")); + + @Test + public void shouldParseRateLimitConfigurationForLowTrafficAccounts() { + RateLimiterConfig rateLimiterConfig = RULE.getConfiguration().getRateLimiterConfig(); + assertThat(rateLimiterConfig.getNoOfReqForLowTrafficAccounts(), is(4500)); + assertThat(rateLimiterConfig.getNoOfPostReqForLowTrafficAccounts(), is(2)); + assertThat(rateLimiterConfig.getIntervalInMillisForLowTrafficAccounts(), is(60000)); + assertThat(rateLimiterConfig.getLowTrafficAccounts(), is(Collections.emptyList())); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PublicApiConfigIT/PublicApiConfigIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PublicApiConfigIT/PublicApiConfigIT.java new file mode 100644 index 000000000..425d358d0 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/PublicApiConfigIT/PublicApiConfigIT.java @@ -0,0 +1,34 @@ +package uk.gov.pay.api.it.validation.PublicApiConfigIT; + +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit.DropwizardAppRule; +import org.junit.Rule; +import org.junit.Test; +import uk.gov.pay.api.app.PublicApi; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RateLimiterConfig; + +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class PublicApiConfigIT { + + @Rule + public final DropwizardAppRule RULE = + new DropwizardAppRule<>(PublicApi.class, ResourceHelpers.resourceFilePath("config/test-config.yaml")); + + @Test + public void shouldParseConfiguration() { + RateLimiterConfig rateLimiterConfig = RULE.getConfiguration().getRateLimiterConfig(); + assertThat(rateLimiterConfig.getNoOfReq(), is(1000)); + assertThat(rateLimiterConfig.getPerMillis(), is(1000)); + assertThat(rateLimiterConfig.getNoOfReqForPost(), is(1000)); + assertThat(rateLimiterConfig.getNoOfReqPerNode(), is(1)); + assertThat(rateLimiterConfig.getNoOfReqForPostPerNode(), is(1)); + assertThat(rateLimiterConfig.getNoOfReqForElevatedAccounts(), is(1000)); + assertThat(rateLimiterConfig.getNoOfPostReqForElevatedAccounts(), is(1000)); + assertThat(rateLimiterConfig.getElevatedAccounts(), is(List.of("1", "2", "3"))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/StringDeserializerValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/StringDeserializerValidationIT.java new file mode 100644 index 000000000..dd875fdf9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/it/validation/StringDeserializerValidationIT.java @@ -0,0 +1,53 @@ +package uk.gov.pay.api.it.validation; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.apache.http.HttpStatus; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import uk.gov.pay.api.it.telephone.TelephonePaymentResourceITBase; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.utils.PublicAuthMockClient; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static java.lang.String.format; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.core.Is.is; + +@RunWith(JUnitParamsRunner.class) +public class StringDeserializerValidationIT extends TelephonePaymentResourceITBase { + + private PublicAuthMockClient publicAuthMockClient = new PublicAuthMockClient(publicAuthMock); + + @Before + public void setup() { + publicAuthMockClient.mapBearerTokenToAccountId(API_KEY, GATEWAY_ACCOUNT_ID, TokenPaymentType.CARD); + } + + @Test + @Parameters({"2", "2.5", "true"}) + public void shouldFailForConversions(String value) { + + String payload = format("{" + + " \"amount\" : 100," + + " \"description\" : \"desc\"," + + " \"reference\" : %s," + + " \"processor_id\" : \"1PROC\"" + + "}", value); + + given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .header(AUTHORIZATION, "Bearer " + API_KEY) + .post("/v1/payment_notification/") + .then() + .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) + .body("size()", is(3)) + .body("field", is("reference")) + .body("code", is("P0102")) + .body("description", is("Invalid attribute value: reference. Must be of type String")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/json/CreateAgreementRequestDeserializerTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/json/CreateAgreementRequestDeserializerTest.java new file mode 100644 index 000000000..4e6a03a1f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/json/CreateAgreementRequestDeserializerTest.java @@ -0,0 +1,92 @@ +package uk.gov.pay.api.json; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.After; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import uk.gov.pay.api.agreement.model.CreateAgreementRequest; +import uk.gov.pay.api.exception.BadRequestException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.verifyNoInteractions; +import static uk.gov.pay.api.matcher.BadRequestExceptionMatcher.aBadRequestExceptionWithError; + +public class CreateAgreementRequestDeserializerTest { + + @Mock + private DeserializationContext ctx; + + private JsonFactory jsonFactory = new JsonFactory(new ObjectMapper()); + private CreateAgreementRequestDeserializer deserializer; + + @BeforeEach + public void setup() { + deserializer = new CreateAgreementRequestDeserializer(); + } + + @Test + public void deserialize_shouldDeserializeACreateAgreementRequestWithPayloadSuccessfully() throws Exception { + String validJson = "{\"reference\": \"Some reference\", \"description\": \"A valid description\", \"user_identifier\": \"a-valid-user-identifier\"}"; + CreateAgreementRequest agreementRequest = deserializer.deserialize(jsonFactory.createParser(validJson), ctx); + assertThat(agreementRequest.getReference(), is("Some reference")); + } + + @Test + public void deserialize_shouldThrowBadRequestException_whenJsonIsNotWellFormed() { + String invalidJson = "{\"reference\": \"Some reference\""; + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(invalidJson), ctx)); + assertThat(badRequestException, aBadRequestExceptionWithError("P2197", "Unable to parse JSON")); + } + + @Test + public void deserialize_shouldThrowBadRequestException_whenReferenceIsMissing() { + String invalidJson = "{}"; + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(invalidJson), ctx)); + assertThat(badRequestException, aBadRequestExceptionWithError("P2101", "Missing mandatory attribute: reference")); + } + + @Test + public void deserialize_shouldThrowValidationException_whenReferenceIsIsNullValue() { + String json = "{ \"reference\": null}"; + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + assertThat(badRequestException, aBadRequestExceptionWithError("P2101", "Missing mandatory attribute: reference")); + } + + @Test + public void deserialize_shouldThrowValidationException_whenReferenceIsIsEmptyString() { + String json = "{ \"reference\": \"\"}"; + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + assertThat(badRequestException, aBadRequestExceptionWithError("P2101", "Missing mandatory attribute: reference")); + } + + @Test + public void deserialize_shouldThrowBadRequestException_whenDescriptionIsMissing() { + String invalidJson = "{\"reference\": \"Some reference\"}"; + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(invalidJson), ctx)); + assertThat(badRequestException, aBadRequestExceptionWithError("P2101", "Missing mandatory attribute: description")); + } + + @Test + public void deserialize_shouldThrowValidationException_whenReferenceIsNumericValue() { + String jsonWithNumericReference = "{\"reference\": 123}"; + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(jsonWithNumericReference), ctx)); + assertThat(badRequestException, aBadRequestExceptionWithError("P2102", + "Invalid attribute value: reference. Must be a valid string format")); + } + + @After + public void tearDown() { + verifyNoInteractions(ctx); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/json/CreateCardPaymentRequestDeserializerTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/json/CreateCardPaymentRequestDeserializerTest.java new file mode 100644 index 000000000..c32de768b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/json/CreateCardPaymentRequestDeserializerTest.java @@ -0,0 +1,693 @@ +package uk.gov.pay.api.json; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import junitparams.converters.Nullable; +import org.junit.After; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import uk.gov.pay.api.exception.BadRequestException; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.CreateCardPaymentRequest; +import uk.gov.pay.api.model.PrefilledCardholderDetails; +import uk.gov.service.payments.commons.model.SupportedLanguage; + +import java.util.Optional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.verifyNoInteractions; +import static uk.gov.pay.api.matcher.BadRequestExceptionMatcher.aBadRequestExceptionWithError; +import static uk.gov.service.payments.commons.model.Source.CARD_PAYMENT_LINK; + +class CreateCardPaymentRequestDeserializerTest { + + @Mock + private DeserializationContext ctx; + + private JsonFactory jsonFactory = new JsonFactory(new ObjectMapper()); + private CreateCardPaymentRequestDeserializer deserializer; + + @BeforeEach + void setup() { + deserializer = new CreateCardPaymentRequestDeserializer(); + } + + @Test + void deserialize_shouldDeserializeARequestWithReturnUrlSuccessfully() throws Exception { + // language=JSON + String validJson = "{\n" + + " \"amount\": 27432,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + CreateCardPaymentRequest paymentRequest = deserializer.deserialize(jsonFactory.createParser(validJson), ctx); + + assertThat(paymentRequest.getAmount(), is(27432)); + assertThat(paymentRequest.getReference(), is("Some reference")); + assertThat(paymentRequest.getDescription(), is("Some description")); + assertThat(paymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentRequest.getLanguage(), is(Optional.empty())); + assertThat(paymentRequest.getDelayedCapture(), is(Optional.empty())); + assertThat(paymentRequest.getEmail(), is(Optional.empty())); + assertThat(paymentRequest.getPrefilledCardholderDetails(), is(Optional.empty())); + } + + @Test + void deserialize_shouldDeserializeARequestWithoutReturnUrlSuccessfully() throws Exception { + // language=JSON + String validJson = "{\n" + + " \"amount\": 27432,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\"\n" + + "}"; + + CreateCardPaymentRequest paymentRequest = deserializer.deserialize(jsonFactory.createParser(validJson), ctx); + + assertThat(paymentRequest.getAmount(), is(27432)); + assertThat(paymentRequest.getReference(), is("Some reference")); + assertThat(paymentRequest.getDescription(), is("Some description")); + assertThat(paymentRequest.getReturnUrl(), is(nullValue())); + assertThat(paymentRequest.getLanguage(), is(Optional.empty())); + assertThat(paymentRequest.getDelayedCapture(), is(Optional.empty())); + assertThat(paymentRequest.getEmail(), is(Optional.empty())); + assertThat(paymentRequest.getPrefilledCardholderDetails(), is(Optional.empty())); + } + + @Test + void deserialize_shouldDeserializeARequestWithEnglishLanguageSuccessfully() throws Exception { + // language=JSON + String validJson = "{\n" + + " \"amount\": 27432,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"language\": \"en\"\n" + + "}"; + + CreateCardPaymentRequest paymentRequest = deserializer.deserialize(jsonFactory.createParser(validJson), ctx); + + assertThat(paymentRequest.getAmount(), is(27432)); + assertThat(paymentRequest.getReference(), is("Some reference")); + assertThat(paymentRequest.getDescription(), is("Some description")); + assertThat(paymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentRequest.getLanguage(), is(Optional.of(SupportedLanguage.ENGLISH))); + } + + @Test + void deserialize_shouldDeserializeARequestWithWelshLanguageSuccessfully() throws Exception { + // language=JSON + String validJson = "{\n" + + " \"amount\": 27432,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"language\": \"cy\"\n" + + "}"; + + CreateCardPaymentRequest paymentRequest = deserializer.deserialize(jsonFactory.createParser(validJson), ctx); + + assertThat(paymentRequest.getAmount(), is(27432)); + assertThat(paymentRequest.getReference(), is("Some reference")); + assertThat(paymentRequest.getDescription(), is("Some description")); + assertThat(paymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentRequest.getLanguage(), is(Optional.of(SupportedLanguage.WELSH))); + } + + @Test + void deserialize_shouldDeserializeARequestWithDelayedCaptureEqualsTrueSuccessfully() throws Exception { + // language=JSON + String validJson = "{\n" + + " \"amount\": 27432,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"delayed_capture\": true\n" + + "}"; + + CreateCardPaymentRequest paymentRequest = deserializer.deserialize(jsonFactory.createParser(validJson), ctx); + + assertThat(paymentRequest.getAmount(), is(27432)); + assertThat(paymentRequest.getReference(), is("Some reference")); + assertThat(paymentRequest.getDescription(), is("Some description")); + assertThat(paymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentRequest.getDelayedCapture(), is(Optional.of(Boolean.TRUE))); + } + + @Test + void deserialize_shouldDeserializeARequestWithDelayedCaptureEqualsFalseSuccessfully() throws Exception { + // language=JSON + String validJson = "{\n" + + " \"amount\": 27432,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"delayed_capture\": false\n" + + "}"; + + CreateCardPaymentRequest paymentRequest = deserializer.deserialize(jsonFactory.createParser(validJson), ctx); + + assertThat(paymentRequest.getAmount(), is(27432)); + assertThat(paymentRequest.getReference(), is("Some reference")); + assertThat(paymentRequest.getDescription(), is("Some description")); + assertThat(paymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentRequest.getDelayedCapture(), is(Optional.of(Boolean.FALSE))); + } + + @ParameterizedTest + @ValueSource(strings = {"true", "false"}) + void deserialize_shouldDeserializeARequestWithMotoFieldSuccessfully(String value) throws Exception { + // language=JSON + String validJson = "{\n" + + " \"amount\": 27432,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"moto\":" + value + "\n" + + "}"; + + CreateCardPaymentRequest paymentRequest = deserializer.deserialize(jsonFactory.createParser(validJson), ctx); + + assertThat(paymentRequest.getAmount(), is(27432)); + assertThat(paymentRequest.getReference(), is("Some reference")); + assertThat(paymentRequest.getDescription(), is("Some description")); + assertThat(paymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentRequest.getMoto(), is(Optional.of(Boolean.parseBoolean(value)))); + } + + @Test + void deserialize_shouldThrowBadRequestException_whenJsonIsNotWellFormed() throws Exception { + String invalidJson = "{" + + " \"amount\" : " + + " \"reference\" : \"Some reference\"," + + " \"description\" : \"Some description\"," + + " \"return_url\" : \"https://somewhere.gov.uk/rainbow/1\"" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(invalidJson), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0197", "Unable to parse JSON")); + } + + @Test + void deserialize_shouldThrowBadRequestException_whenAmountIsMissing() throws Exception { + // language=JSON + String json = "{\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0101", "Missing mandatory attribute: amount")); + } + + @Test + void deserialize_shouldThrowValidationException_asAmountIsMissing_whenAmountIsNullValue() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": null,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0101", "Missing mandatory attribute: amount")); + } + + @Test + void deserialize_shouldThrowValidationException_whenAmountIsNotInteger() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": \"\",\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: amount. Must be a valid numeric format")); + } + + @Test + void deserialize_shouldThrowValidationException_whenReturnUrlIsNotAStringValue() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 1000000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": 1\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: return_url. Must be a valid URL format")); + } + + @Test + void deserialize_shouldThrowValidationException_whenReferenceIsMissing() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 666,\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0101", + "Missing mandatory attribute: reference")); + } + + @Test + void deserialize_shouldThrowValidationException_AsReferenceIsMissing_whenReferenceIsNullValue() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 666,\n" + + " \"reference\": null,\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0101", + "Missing mandatory attribute: reference")); + } + + @Test + void deserialize_shouldThrowValidationException_whenReferenceIsNotAString() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 666,\n" + + " \"reference\": 123,\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: reference. Must be a valid string format")); + } + + @Test + void deserialize_shouldThrowValidationException_whenDescriptionIsMissing() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 666,\n" + + " \"reference\": \"Some reference\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0101", + "Missing mandatory attribute: description")); + } + + @Test + void deserialize_shouldThrowValidationException_asDescriptionIsMissing_whenDescriptionIsNullValue() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 666,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": null,\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0101", + "Missing mandatory attribute: description")); + } + + @Test + void deserialize_shouldThrowValidationException_whenDescriptionIsNotAString() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 666,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": 432,\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: description. Must be a valid string format")); + } + + @Test + void deserialize_shouldThrowValidationException_whenLanguageIsNotAString() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 1337,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"language\": 1234\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: language. Must be \"en\" or \"cy\"")); + } + + @Test + void deserialize_shouldThrowValidationException_whenLanguageIsNullValue() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 1337,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"language\": null\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: language. Must be \"en\" or \"cy\"")); + } + + @Test + void deserialize_shouldThrowValidationException_whenLanguageIsEmptyString() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 1337,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"language\": \"\"\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: language. Must be \"en\" or \"cy\"")); + } + + @Test + void deserialize_shouldThrowValidationException_whenDelayedCaptureIsNotABoolean() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 1337,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"delayed_capture\": \"true\"\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: delayed_capture. Must be true or false")); + } + + @Test + void deserialize_shouldThrowValidationException_whenDelayedCaptureIsNullValue() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 1337,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"delayed_capture\": null\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: delayed_capture. Must be true or false")); + } + + @Test + void deserialize_shouldThrowValidationException_whenDelayedCaptureIsNumeric() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 1337,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"delayed_capture\": 0\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: delayed_capture. Must be true or false")); + } + + @ParameterizedTest + @ValueSource(strings = {"null", "\"true\"", "0"}) + void deserialize_shouldThrowValidationException_whenMotoIsNotABoolean(@Nullable String value) throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 1337,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"moto\": " + value + "\n" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: moto. Must be true or false")); + } + + @Test + void shouldDeserializeARequestWithPrefilledCardholderDetailsSuccessfully() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"email\": \"j.bogs@example.org\",\n" + + "\"prefilled_cardholder_details\": {\n" + + "\"cardholder_name\": \"J Bogs\",\n" + + "\"billing_address\": {\n" + + "\"line1\": \"address line 1\",\n" + + "\"line2\": null,\n" + + "\"city\": \"address city\",\n" + + "\"postcode\": \"AB1 CD2\",\n" + + "\"country\": \"GB\"\n" + + "}" + "}" + "}"; + + CreateCardPaymentRequest paymentRequest = deserializer.deserialize(jsonFactory.createParser(payload), ctx); + assertThat(paymentRequest.getAmount(), is(1000)); + assertThat(paymentRequest.getReference(), is("Some reference")); + assertThat(paymentRequest.getDescription(), is("Some description")); + assertThat(paymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentRequest.getLanguage(), is(Optional.empty())); + assertThat(paymentRequest.getDelayedCapture(), is(Optional.empty())); + assertThat(paymentRequest.getEmail(), is(Optional.of("j.bogs@example.org"))); + + assertThat(paymentRequest.getPrefilledCardholderDetails().isPresent(), is(true)); + PrefilledCardholderDetails prefilledCardholderDetails = paymentRequest.getPrefilledCardholderDetails().get(); + assertThat(prefilledCardholderDetails.getCardholderName().isPresent(), is(true)); + assertThat(prefilledCardholderDetails.getCardholderName().get(), is("J Bogs")); + assertThat(prefilledCardholderDetails.getBillingAddress().isPresent(), is(true)); + Address billingAddress = prefilledCardholderDetails.getBillingAddress().get(); + assertThat(billingAddress.getLine1(), is("address line 1")); + assertThat(billingAddress.getLine2(), is(nullValue())); + assertThat(billingAddress.getPostcode(), is("AB1 CD2")); + assertThat(billingAddress.getCity(), is("address city")); + assertThat(billingAddress.getCountry(), is("GB")); + } + + @Test + void shouldDeserializeARequestWithCardholderNameAndNoBillingAddressSuccessfully() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"email\": \"j.bogs@example.org\",\n" + + "\"prefilled_cardholder_details\": {\n" + + "\"cardholder_name\": \"J Bogs\"\n" + + "}" + "}"; + + CreateCardPaymentRequest paymentRequest = deserializer.deserialize(jsonFactory.createParser(payload), ctx); + assertThat(paymentRequest.getAmount(), is(1000)); + assertThat(paymentRequest.getReference(), is("Some reference")); + assertThat(paymentRequest.getDescription(), is("Some description")); + assertThat(paymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentRequest.getEmail(), is(Optional.of("j.bogs@example.org"))); + assertThat(paymentRequest.getPrefilledCardholderDetails().isPresent(), is(true)); + assertThat(paymentRequest.getPrefilledCardholderDetails().get().getCardholderName().isPresent(), is(true)); + assertThat(paymentRequest.getPrefilledCardholderDetails().get().getCardholderName().get(), is("J Bogs")); + assertThat(paymentRequest.getPrefilledCardholderDetails().get().getBillingAddress().isPresent(), is(false)); + } + + @Test + void shouldDeserializeARequestWithBillingAddressSuccessfully() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"email\": null,\n" + + "\"prefilled_cardholder_details\": {\n" + + "\"billing_address\": {\n" + + "\"line1\": \"address line 1\",\n" + + "\"line2\": \"address line 2\",\n" + + "\"city\": \"address city\",\n" + + "\"postcode\": \"AB1 CD2\",\n" + + "\"country\": null\n" + + "}" + "}" + "}"; + + CreateCardPaymentRequest paymentRequest = deserializer.deserialize(jsonFactory.createParser(payload), ctx); + assertThat(paymentRequest.getAmount(), is(1000)); + assertThat(paymentRequest.getReference(), is("Some reference")); + assertThat(paymentRequest.getDescription(), is("Some description")); + assertThat(paymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentRequest.getLanguage(), is(Optional.empty())); + assertThat(paymentRequest.getDelayedCapture(), is(Optional.empty())); + assertThat(paymentRequest.getPrefilledCardholderDetails().isPresent(), is(true)); + assertThat(paymentRequest.getPrefilledCardholderDetails().get().getCardholderName(), is(Optional.empty())); + assertThat(paymentRequest.getPrefilledCardholderDetails().get().getBillingAddress().isPresent(), is(true)); + Address billingAddress = paymentRequest.getPrefilledCardholderDetails().get().getBillingAddress().get(); + assertThat(billingAddress.getLine1(), is("address line 1")); + assertThat(billingAddress.getLine2(), is("address line 2")); + assertThat(billingAddress.getPostcode(), is("AB1 CD2")); + assertThat(billingAddress.getCity(), is("address city")); + assertThat(billingAddress.getCountry(), is(nullValue())); + } + + @Test + void shouldDeserializeARequestWithEmptyCountrySuccessfully() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"email\": null,\n" + + "\"prefilled_cardholder_details\": {\n" + + "\"billing_address\": {\n" + + "\"line1\": \"address line 1\",\n" + + "\"line2\": \"address line 2\",\n" + + "\"city\": \"address city\",\n" + + "\"postcode\": \"AB1 CD2\",\n" + + "\"country\": \"\"\n" + + "}" + "}" + "}"; + + CreateCardPaymentRequest paymentRequest = deserializer.deserialize(jsonFactory.createParser(payload), ctx); + assertThat(paymentRequest.getAmount(), is(1000)); + assertThat(paymentRequest.getReference(), is("Some reference")); + assertThat(paymentRequest.getDescription(), is("Some description")); + assertThat(paymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentRequest.getLanguage(), is(Optional.empty())); + assertThat(paymentRequest.getDelayedCapture(), is(Optional.empty())); + + assertThat(paymentRequest.getPrefilledCardholderDetails().isPresent(), is(true)); + assertThat(paymentRequest.getPrefilledCardholderDetails().get().getCardholderName().isPresent(), is(false)); + assertThat(paymentRequest.getPrefilledCardholderDetails().get().getBillingAddress().isPresent(), is(true)); + Address billingAddress = paymentRequest.getPrefilledCardholderDetails().get().getBillingAddress().get(); + assertThat(billingAddress.getLine1(), is("address line 1")); + assertThat(billingAddress.getLine2(), is("address line 2")); + assertThat(billingAddress.getPostcode(), is("AB1 CD2")); + assertThat(billingAddress.getCity(), is("address city")); + assertThat(billingAddress.getCountry(), is("")); + } + + @Test + void deserialize_shouldThrowValidationException_whenLine1IsNumeric() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"email\": null,\n" + + "\"prefilled_cardholder_details\": {\n" + + "\"billing_address\": {\n" + + "\"line1\": 172,\n" + + "\"line2\": \"address line 2\",\n" + + "\"city\": \"address city\",\n" + + "\"postcode\": \"AB1 CD2\",\n" + + "\"country\": null\n" + + "}" + "}" + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: line1. Field must be a string")); + } + + @Test + void deserialize_shouldNotThrowValidationException_whenCountryIsEmptyString() throws Exception { + // language=JSON + String json = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"prefilled_cardholder_details\": {\n" + + "\"billing_address\": {\n" + + "\"country\": \"\"\n" + + "}" + "}" + "}"; + deserializer.deserialize(jsonFactory.createParser(json), ctx); + assertThat(true, is(true)); + } + + @Test + void shouldDeserializeARequestAndSetSourceCorrectly() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"internal\": {\n" + + "\"source\": \"CARD_PAYMENT_LINK\"\n" + + "}" + "}"; + + CreateCardPaymentRequest paymentRequest = deserializer.deserialize(jsonFactory.createParser(payload), ctx); + assertThat(paymentRequest.getInternal().get().getSource().get(), is(CARD_PAYMENT_LINK)); + } + + @After + void tearDown() { + verifyNoInteractions(ctx); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/json/CreatePaymentRefundRequestDeserializerTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/json/CreatePaymentRefundRequestDeserializerTest.java new file mode 100644 index 000000000..93e430568 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/json/CreatePaymentRefundRequestDeserializerTest.java @@ -0,0 +1,133 @@ +package uk.gov.pay.api.json; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.exception.BadRequestException; +import uk.gov.pay.api.exception.PaymentValidationException; +import uk.gov.pay.api.model.CreatePaymentRefundRequest; +import uk.gov.pay.api.validation.PaymentRefundRequestValidator; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.verifyNoInteractions; +import static uk.gov.pay.api.matcher.BadRequestExceptionMatcher.aBadRequestExceptionWithError; +import static uk.gov.pay.api.matcher.PaymentValidationExceptionMatcher.aValidationExceptionContaining; + +@RunWith(MockitoJUnitRunner.class) +public class CreatePaymentRefundRequestDeserializerTest { + + @Mock + private DeserializationContext ctx; + + private JsonFactory jsonFactory = new JsonFactory(new ObjectMapper()); + private CreatePaymentRefundRequestDeserializer deserializer; + + @Before + public void setup() { + deserializer = new CreatePaymentRefundRequestDeserializer(new PaymentRefundRequestValidator()); + } + + @Test + public void deserialize_shouldDeserializeARequestSuccessfully() throws Exception { + + String validJson = "{" + + " \"amount\" : 12345" + + "}"; + + CreatePaymentRefundRequest paymentRefundRequest = deserializer.deserialize(jsonFactory.createParser(validJson), ctx); + + assertThat(paymentRefundRequest.getAmount(), is(12345)); + } + + @Test + public void deserialize_shouldThrowBadRequestException_whenJsonIsNotWellFormed() throws Exception { + + String invalidJson = "{" + + " \"amount\" : " + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(invalidJson), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0697", "Unable to parse JSON")); + } + + @Test + public void deserialize_shouldThrowBadRequestException_whenAmountIsMissing() throws Exception { + + String json = "{}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0601", "Missing mandatory attribute: amount")); + } + + @Test + public void deserialize_shouldThrowValidationException_asAmountIsMissing_whenAmountIsNullValue() throws Exception { + + String json = "{" + + " \"amount\" : null" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0601", "Missing mandatory attribute: amount")); + } + + @Test + public void deserialize_shouldThrowValidationException_whenAmountIsNotInteger() throws Exception { + + String json = "{" + + " \"amount\" : \"\"" + + "}"; + + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(badRequestException, aBadRequestExceptionWithError("P0602", + "Invalid attribute value: amount. Must be a valid numeric format")); + } + + @Test + public void deserialize_shouldThrowValidationException_whenAmountIsLessThanMinimum() throws Exception { + + String json = "{" + + " \"amount\" : 0" + + "}"; + + PaymentValidationException exception = assertThrows(PaymentValidationException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(exception, aValidationExceptionContaining("P0602", + "Invalid attribute value: amount. Must be greater than or equal to 1")); + } + + @Test + public void deserialize_shouldThrowValidationException_whenAmountIsMoreThanMaximum() throws Exception { + + String json = "{" + + " \"amount\" : 10000001" + + "}"; + + PaymentValidationException exception = assertThrows(PaymentValidationException.class, + () -> deserializer.deserialize(jsonFactory.createParser(json), ctx)); + + assertThat(exception, aValidationExceptionContaining("P0602", + "Invalid attribute value: amount. Must be less than or equal to 10000000")); + } + + @After + public void tearDown() { + verifyNoInteractions(ctx); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/json/RequestJsonParserTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/json/RequestJsonParserTest.java new file mode 100644 index 000000000..1787b027b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/json/RequestJsonParserTest.java @@ -0,0 +1,679 @@ +package uk.gov.pay.api.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import uk.gov.pay.api.exception.BadRequestException; +import uk.gov.pay.api.exception.PaymentValidationException; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.CreateCardPaymentRequest; +import uk.gov.pay.api.model.CreatePaymentRefundRequest; +import uk.gov.pay.api.model.PrefilledCardholderDetails; +import uk.gov.service.payments.commons.model.SupportedLanguage; + +import java.util.Optional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.Assert.assertThrows; +import static uk.gov.pay.api.json.RequestJsonParser.parsePaymentRequest; +import static uk.gov.pay.api.json.RequestJsonParser.parseRefundRequest; +import static uk.gov.pay.api.matcher.BadRequestExceptionMatcher.aBadRequestExceptionWithError; +import static uk.gov.service.payments.commons.model.AuthorisationMode.MOTO_API; +import static uk.gov.service.payments.commons.model.Source.CARD_AGENT_INITIATED_MOTO; +import static uk.gov.service.payments.commons.model.Source.CARD_API; +import static uk.gov.service.payments.commons.model.Source.CARD_PAYMENT_LINK; + +class RequestJsonParserTest { + + private ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void parsePaymentRequest_withReturnUrl_shouldParseSuccessfully() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + CreateCardPaymentRequest createPaymentRequest = parsePaymentRequest(jsonNode); + + assertThat(createPaymentRequest, is(notNullValue())); + assertThat(createPaymentRequest.getAmount(), is(1000)); + assertThat(createPaymentRequest.getReference(), is("Some reference")); + assertThat(createPaymentRequest.getDescription(), is("Some description")); + assertThat(createPaymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + } + + @Test + void parsePaymentRequest_withoutReturnUrl_shouldParseSuccessfully() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + CreateCardPaymentRequest createPaymentRequest = parsePaymentRequest(jsonNode); + + assertThat(createPaymentRequest, is(notNullValue())); + assertThat(createPaymentRequest.getAmount(), is(1000)); + assertThat(createPaymentRequest.getReference(), is("Some reference")); + assertThat(createPaymentRequest.getDescription(), is("Some description")); + assertThat(createPaymentRequest.getReturnUrl(), is(nullValue())); + } + + @Test + void parsePaymentRequest_withReturnUrlAndLanguageAndDelayedCaptureAndMoto_shouldParseSuccessfully() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"language\": \"en\",\n" + + " \"delayed_capture\": true,\n" + + " \"moto\": true\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + CreateCardPaymentRequest createPaymentRequest = parsePaymentRequest(jsonNode); + + assertThat(createPaymentRequest, is(notNullValue())); + assertThat(createPaymentRequest.getAmount(), is(1000)); + assertThat(createPaymentRequest.getReference(), is("Some reference")); + assertThat(createPaymentRequest.getDescription(), is("Some description")); + assertThat(createPaymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(createPaymentRequest.getLanguage(), is(Optional.of(SupportedLanguage.ENGLISH))); + assertThat(createPaymentRequest.getDelayedCapture(), is(Optional.of(true))); + assertThat(createPaymentRequest.getMoto(), is(Optional.of(true))); + } + + @Test + void parsePaymentRefundRequest_shouldParseSuccessfully() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + CreatePaymentRefundRequest createPaymentRefundRequest = parseRefundRequest(jsonNode); + + assertThat(createPaymentRefundRequest, is(notNullValue())); + assertThat(createPaymentRefundRequest.getAmount(), is(1000)); + } + + @Test + void parsePaymentRequest_whenReferenceFieldIsNotAString() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": 1234,\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: reference. Must be a valid string format")); + } + + @Test + void parsePaymentRequest_whenDescriptionFieldIsNotAString() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": 1234,\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: description. Must be a valid string format")); + } + + @Test + void parsePaymentRequest_whenLanguageFieldIsNotAString() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"language\": 0\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: language. Must be \"en\" or \"cy\"")); + } + + @Test + void parsePaymentRequest_whenDelayedCaptureFieldIsNotABoolean() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"delayed_capture\": \"true\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: delayed_capture. Must be true or false")); + } + + @Test + void parsePaymentRequest_whenReturnUrlIsNotAString_shouldOverrideFormattingErrorMessage() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": 1234\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: return_url. Must be a valid URL format")); + } + + @Test + void parsePaymentRequest_whenReferenceFieldIsNullValue() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": null,\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0101", + "Missing mandatory attribute: reference")); + } + + @Test + void parsePaymentRequest_whenDescriptionFieldIsNullValue() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": null,\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0101", + "Missing mandatory attribute: description")); + } + + @Test + void parsePaymentRequest_whenLanguageFieldIsNullValue() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"language\": null\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: language. Must be \"en\" or \"cy\"")); + } + + @Test + void parsePaymentRefundRequest_whenAmountFieldIsNullValue() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": null\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parseRefundRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0601", + "Missing mandatory attribute: amount")); + } + + @Test + void parsePaymentRequest_whenAmountFieldIsMissing() throws Exception { + // language=JSON + String payload = "{\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": 1234\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0101", + "Missing mandatory attribute: amount")); + } + + @Test + void parsePaymentRequest_whenReferenceFieldIsMissing() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": 1234\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0101", + "Missing mandatory attribute: reference")); + } + + @Test + void parsePaymentRequest_whenDescriptionFieldIsMissing() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"return_url\": 1234\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0101", + "Missing mandatory attribute: description")); + } + + @Test + void parsePaymentRefundRequest_whenAmountFieldIsMissing() throws Exception { + // language=JSON + String payload = "{}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parseRefundRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0601", + "Missing mandatory attribute: amount")); + } + + @Test + void parsePaymentRequest_withAllPrefilledCardholderDetails_shouldParseSuccessfully() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"email\": \"j.bogs@example.org\",\n" + + "\"prefilled_cardholder_details\": {\n" + + "\"cardholder_name\": \"J Bogs\",\n" + + "\"billing_address\": {\n" + + "\"line1\": \"address line 1\",\n" + + "\"line2\": \"address line 2\",\n" + + "\"city\": \"address city\",\n" + + "\"postcode\": \"AB1 CD2\",\n" + + "\"country\": \"GB\"\n" + + "}" + "}" + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + CreateCardPaymentRequest createPaymentRequest = parsePaymentRequest(jsonNode); + + assertThat(createPaymentRequest, is(notNullValue())); + assertThat(createPaymentRequest.getAmount(), is(1000)); + assertThat(createPaymentRequest.getReference(), is("Some reference")); + assertThat(createPaymentRequest.getDescription(), is("Some description")); + assertThat(createPaymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(createPaymentRequest.getEmail(), is(Optional.of("j.bogs@example.org"))); + assertThat(createPaymentRequest.getPrefilledCardholderDetails(), is(notNullValue())); + PrefilledCardholderDetails cardholderDetails = createPaymentRequest.getPrefilledCardholderDetails().get(); + assertThat(cardholderDetails.getCardholderName().isPresent(), is(true)); + assertThat(cardholderDetails.getCardholderName().get(), is("J Bogs")); + assertThat(cardholderDetails.getBillingAddress().isPresent(), is(true)); + Address billingAddress = cardholderDetails.getBillingAddress().get(); + assertThat(billingAddress.getLine1(), is("address line 1")); + assertThat(billingAddress.getLine2(), is("address line 2")); + assertThat(billingAddress.getCity(), is("address city")); + assertThat(billingAddress.getPostcode(), is("AB1 CD2")); + assertThat(billingAddress.getCountry(), is("GB")); + } + + @Test + void parsePaymentRequest_withSomePrefilledCardholderDetails_shouldParseSuccessfully() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"email\": null,\n" + + "\"prefilled_cardholder_details\": {\n" + + "\"cardholder_name\": null,\n" + + "\"billing_address\": {\n" + + "\"line1\": \"address line 1\",\n" + + "\"line2\": null,\n" + + "\"city\": \"address city\",\n" + + "\"postcode\": \"AB1 CD2\",\n" + + "\"country\": \"GB\"\n" + + "}" + "}" + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + CreateCardPaymentRequest createPaymentRequest = parsePaymentRequest(jsonNode); + + assertThat(createPaymentRequest, is(notNullValue())); + assertThat(createPaymentRequest.getAmount(), is(1000)); + assertThat(createPaymentRequest.getReference(), is("Some reference")); + assertThat(createPaymentRequest.getDescription(), is("Some description")); + assertThat(createPaymentRequest.getReturnUrl(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(createPaymentRequest.getEmail(), is(Optional.empty())); + assertThat(createPaymentRequest.getPrefilledCardholderDetails(), is(notNullValue())); + assertThat(createPaymentRequest.getPrefilledCardholderDetails().isPresent(), is(true)); + PrefilledCardholderDetails cardholderDetails = createPaymentRequest.getPrefilledCardholderDetails().get(); + assertThat(cardholderDetails.getCardholderName().isPresent(), is(false)); + assertThat(cardholderDetails.getBillingAddress().isPresent(), is(true)); + Address billingAddress = cardholderDetails.getBillingAddress().get(); + assertThat(billingAddress.getLine1(), is("address line 1")); + assertThat(billingAddress.getLine2(), is(nullValue())); + assertThat(billingAddress.getCity(), is("address city")); + assertThat(billingAddress.getPostcode(), is("AB1 CD2")); + assertThat(billingAddress.getCountry(), is("GB")); + } + + @Test + void parsePaymentRequest_withEmailFieldIsNotAString() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"email\": false,\n" + + "\"prefilled_cardholder_details\": {\n" + + "\"cardholder_name\": \"J Bogs\",\n" + + "\"billing_address\": {\n" + + "\"line1\": \"address line 1\",\n" + + "\"line2\": \"address line 2\",\n" + + "\"city\": \"address city\",\n" + + "\"postcode\": \"AB1 CD2\",\n" + + "\"country\": \"GB\"\n" + + "}" + "}" + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: email. Field must be a string")); + } + + @Test + void parsePaymentRequest_withLine1FieldIsNotAString() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"email\": \"j.bogs@example.com\",\n" + + "\"prefilled_cardholder_details\": {\n" + + "\"cardholder_name\": \"J Bogs\",\n" + + "\"billing_address\": {\n" + + "\"line1\": 182,\n" + + "\"line2\": \"address line 2\",\n" + + "\"city\": \"address city\",\n" + + "\"postcode\": \"AB1 CD2\",\n" + + "\"country\": \"GB\"\n" + + "}" + "}" + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: line1. Field must be a string")); + } + + @Test + void parsePaymentRequest_shouldSetSourceToDefaultIfNotInPayload() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 27432,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"language\": \"en\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + CreateCardPaymentRequest paymentRequest = parsePaymentRequest(jsonNode); + assertThat(paymentRequest.getInternal().get().getSource().get(), is(CARD_API)); + } + + @Test + void parsePaymentRequest_shouldParseCardPaymentLinkSourceCorrectly() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"internal\": {\n" + + "\"source\": \"CARD_PAYMENT_LINK\"\n" + + "}" + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + CreateCardPaymentRequest paymentRequest = parsePaymentRequest(jsonNode); + assertThat(paymentRequest.getInternal().get().getSource().get(), is(CARD_PAYMENT_LINK)); + } + + @Test + void parsePaymentRequest_shouldCardAgentInitiatedMotoSourceCorrectly() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"internal\": {\n" + + "\"source\": \"CARD_AGENT_INITIATED_MOTO\"\n" + + "}" + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + CreateCardPaymentRequest paymentRequest = parsePaymentRequest(jsonNode); + assertThat(paymentRequest.getInternal().get().getSource().get(), is(CARD_AGENT_INITIATED_MOTO)); + } + + @Test + void parsePaymentRequest_shouldThrowValidationException_whenSourceIsInvalidType() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"internal\": {\n" + + "\"source\": true\n" + + "}" + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: source. Accepted values are only CARD_PAYMENT_LINK, CARD_AGENT_INITIATED_MOTO")); + } + + @Test + void parsePaymentRequest_shouldThrowValidationException_whenSourceIsValidEnumTypeButNotAccepted() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + "\"internal\": {\n" + + "\"source\": \"CARD_EXTERNAL_TELEPHONE\"\n" + + "}" + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0102", + "Invalid attribute value: source. Accepted values are only CARD_PAYMENT_LINK, CARD_AGENT_INITIATED_MOTO")); + } + + @Test + void parsePaymentRequest_shouldParseValidAuthorisationMode() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"authorisation_mode\": \"moto_api\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + CreateCardPaymentRequest paymentRequest = parsePaymentRequest(jsonNode); + assertThat(paymentRequest.getAuthorisationMode().get(), is(MOTO_API)); + } + + @Test + void parsePaymentRequest_shouldThrowValidationException_whenAuthorisationModeIsNotValidEnumValue() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"authorisation_mode\": \"foo\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(paymentValidationException.getRequestError().getCode(), is("P0102")); + assertThat(paymentValidationException.getRequestError().getDescription(), is("Invalid attribute value: authorisation_mode. Must be one of web, moto_api, agreement")); + } + + @Test + void parsePaymentRequest_shouldThrowValidationException_whenAuthorisationModeIsValidEnumValueButNotAccepted() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"authorisation_mode\": \"external\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(paymentValidationException.getRequestError().getCode(), is("P0102")); + assertThat(paymentValidationException.getRequestError().getDescription(), is("Invalid attribute value: authorisation_mode. Must be one of web, moto_api, agreement")); + } + + @Test + void parsePaymentRequest_shouldParseAgreementId_whenAuthorisationModeAgreement() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"authorisation_mode\": \"agreement\",\n" + + " \"agreement_id\": \"abcdefghijklmnopqrstuvwxyz\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + CreateCardPaymentRequest paymentRequest = parsePaymentRequest(jsonNode); + assertThat(paymentRequest.getAgreementId().get(), is("abcdefghijklmnopqrstuvwxyz")); + } + + @Test + void parsePaymentRequest_shouldThrowValidationException_whenAgreementIdIsInvalidType_butAuthorisationModeIsAgreement() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"authorisation_mode\": \"agreement\",\n" + + " \"agreement_id\": true\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException.getRequestError().getCode(), is("P0102")); + assertThat(badRequestException.getRequestError().getDescription(), is("Invalid attribute value: agreement_id. Must be a valid string format")); + } + + @Test + void parsePaymentRequest_shouldThrowValidationException_whenAgreementIdIsProvidedAndAuthorisationModeIsNotAgreement() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"authorisation_mode\": \"web\",\n" + + " \"agreement_id\": \"abcdefgklmnopqrstuvwxyz\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException.getRequestError().getCode(), is("P0104")); + assertThat(badRequestException.getRequestError().getDescription(), is("Unexpected attribute: agreement_id")); + } + + @Test + void parsePaymentRequest_shouldThrowValidationException_whenAgreementIdIsProvidedAndAuthorisationModeIsNotSpecified() throws Exception { + // language=JSON + String payload = "{\n" + + " \"amount\": 1000,\n" + + " \"reference\": \"Some reference\",\n" + + " \"description\": \"Some description\",\n" + + " \"return_url\": \"https://somewhere.gov.uk/rainbow/1\",\n" + + " \"agreement_id\": \"abcdefgklmnopqrstuvwxyz\"\n" + + "}"; + + JsonNode jsonNode = objectMapper.readTree(payload); + + BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> parsePaymentRequest(jsonNode)); + assertThat(badRequestException.getRequestError().getCode(), is("P0104")); + assertThat(badRequestException.getRequestError().getDescription(), is("Unexpected attribute: agreement_id")); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/ledger/service/TransactionSearchServiceTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/ledger/service/TransactionSearchServiceTest.java new file mode 100644 index 000000000..c9be9610a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/ledger/service/TransactionSearchServiceTest.java @@ -0,0 +1,120 @@ +package uk.gov.pay.api.ledger.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.BadRequestException; +import uk.gov.pay.api.ledger.model.SearchResults; +import uk.gov.pay.api.ledger.model.TransactionSearchParams; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.model.search.card.PaymentForSearchResult; +import uk.gov.pay.api.service.PaymentUriGenerator; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static uk.gov.pay.api.matcher.BadRequestExceptionMatcher.aBadRequestExceptionWithError; + +@RunWith(MockitoJUnitRunner.class) +public class TransactionSearchServiceTest { + + @Mock + private PublicApiConfig mockPublicApiConfiguration; + + private TransactionSearchService transactionSearchService; + + private static final String ACCOUNT_ID = "123456"; + private static final String CHARGE_ID = "charge97837509646393e3C"; + + @Rule + public PactProviderRule ledgerRule = new PactProviderRule("ledger", this); + + @Before + public void setup() { + when(mockPublicApiConfiguration.getLedgerUrl()).thenReturn(ledgerRule.getUrl()); + when(mockPublicApiConfiguration.getBaseUrl()).thenReturn("http://publicapi.test.localhost/"); + + PaymentUriGenerator paymentUriGenerator = new PaymentUriGenerator(); + LedgerUriGenerator ledgerUriGenerator = new LedgerUriGenerator(mockPublicApiConfiguration); + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + + transactionSearchService = new TransactionSearchService(client, mockPublicApiConfiguration, ledgerUriGenerator, + paymentUriGenerator); + } + + @Test + public void shouldThrowBadRequestException() { + TransactionSearchParams searchParams = mock(TransactionSearchParams.class); + when(searchParams.getQueryMap()).thenReturn(Map.of("not_supported", "hello")); + BadRequestException badRequestException = assertThrows(BadRequestException.class, + () -> transactionSearchService.doSearch(new Account("1", TokenPaymentType.CARD, "a-token-link"), searchParams)); + assertThat(badRequestException, aBadRequestExceptionWithError("P0401", + "Invalid parameters: not_supported. See Public API documentation for the correct data formats")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-transaction"}) + public void testSearchTransaction() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + TransactionSearchParams searchParams = new TransactionSearchParams(); + + SearchResults searchResults = transactionSearchService.doSearch(account, searchParams); + + PaymentForSearchResult payment = searchResults.getResults().get(0); + + assertThat(payment.getAmount(), is(1000L)); + assertThat(payment.getState(), is(new PaymentState("created", false))); + assertThat(payment.getDescription(), is("Test description")); + assertThat(payment.getReference(), is("aReference")); + assertThat(payment.getLanguage(), is(SupportedLanguage.ENGLISH)); + assertThat(payment.getPaymentId(), is(CHARGE_ID)); + assertThat(payment.getReturnUrl().get(), is("https://example.org")); + assertThat(payment.getEmail().get(), is("someone@example.org")); + assertThat(payment.getPaymentProvider(), is("sandbox")); + assertThat(payment.getCreatedDate(), is("2018-09-22T10:13:16.067Z")); + assertThat(payment.getDelayedCapture(), is(false)); + + assertThat(payment.getCardDetails().get().getCardHolderName(), is("J. Smith")); + assertThat(payment.getCardDetails().get().getCardBrand(), is("")); + + Address address = payment.getCardDetails().get().getBillingAddress().get(); + assertThat(address.getLine1(), is("line1")); + assertThat(address.getLine2(), is("line2")); + assertThat(address.getPostcode(), is("AB1 2CD")); + assertThat(address.getCity(), is("London")); + assertThat(address.getCountry(), is("GB")); + + assertThat(payment.getLinks().getSelf().getHref(), containsString("v1/payments/" + CHARGE_ID)); + assertThat(payment.getLinks().getSelf().getMethod(), is("GET")); + assertThat(payment.getLinks().getRefunds().getHref(), containsString("v1/payments/" + CHARGE_ID + "/refunds")); + assertThat(payment.getLinks().getRefunds().getMethod(), is("GET")); + + assertThat(searchResults.getCount(), is(1)); + assertThat(searchResults.getTotal(), is(1)); + assertThat(searchResults.getPage(), is(1)); + + assertThat(searchResults.getLinks().getSelf().getHref(), containsString("/v1/transactions?display_size=500&page=1")); + assertThat(searchResults.getLinks().getFirstPage().getHref(), containsString("/v1/transactions?display_size=500&page=1")); + assertThat(searchResults.getLinks().getLastPage().getHref(), containsString("/v1/transactions?display_size=500&page=1")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/matcher/BadRefundsRequestExceptionMatcher.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/matcher/BadRefundsRequestExceptionMatcher.java new file mode 100644 index 000000000..eb62b9014 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/matcher/BadRefundsRequestExceptionMatcher.java @@ -0,0 +1,39 @@ +package uk.gov.pay.api.matcher; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import uk.gov.pay.api.exception.BadRefundsRequestException; +import uk.gov.pay.api.model.RequestError; + +public class BadRefundsRequestExceptionMatcher extends TypeSafeMatcher { + + private final String code; + private final String description; + + private BadRefundsRequestExceptionMatcher(String code, String description) { + this.code = code; + this.description = description; + } + + public static BadRefundsRequestExceptionMatcher aBadRefundsRequestExceptionWithError(String code, String description) { + return new BadRefundsRequestExceptionMatcher(code, description); + } + + @Override + protected boolean matchesSafely(BadRefundsRequestException e) { + RequestError requestError = e.getRequestError(); + return code.equals(requestError.getCode()) && + description.equals(requestError.getDescription()); + } + + @Override + public void describeTo(Description description) { + description.appendText(BadRefundsRequestException.class.getCanonicalName()) + .appendText(" with ") + .appendText(" RefundError. { code = ") + .appendValue(code) + .appendText(", description = ") + .appendValue(this.description) + .appendText(" }"); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/matcher/BadRequestExceptionMatcher.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/matcher/BadRequestExceptionMatcher.java new file mode 100644 index 000000000..956a950a2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/matcher/BadRequestExceptionMatcher.java @@ -0,0 +1,39 @@ +package uk.gov.pay.api.matcher; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import uk.gov.pay.api.exception.BadRequestException; +import uk.gov.pay.api.model.RequestError; + +public class BadRequestExceptionMatcher extends TypeSafeMatcher { + + private final String code; + private final String description; + + private BadRequestExceptionMatcher(String code, String description) { + this.code = code; + this.description = description; + } + + public static BadRequestExceptionMatcher aBadRequestExceptionWithError(String code, String description) { + return new BadRequestExceptionMatcher(code, description); + } + + @Override + protected boolean matchesSafely(BadRequestException e) { + RequestError requestError = e.getRequestError(); + return code.equals(requestError.getCode()) && + description.equals(requestError.getDescription()); + } + + @Override + public void describeTo(Description description) { + description.appendText(BadRequestException.class.getCanonicalName()) + .appendText(" with ") + .appendText(" RequestError. { code = ") + .appendValue(code) + .appendText(", description = ") + .appendValue(this.description) + .appendText(" }"); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/matcher/PaymentValidationExceptionMatcher.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/matcher/PaymentValidationExceptionMatcher.java new file mode 100644 index 000000000..f28071873 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/matcher/PaymentValidationExceptionMatcher.java @@ -0,0 +1,39 @@ +package uk.gov.pay.api.matcher; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import uk.gov.pay.api.exception.PaymentValidationException; +import uk.gov.pay.api.model.RequestError; + +public class PaymentValidationExceptionMatcher extends TypeSafeMatcher { + + private final String code; + private final String description; + + private PaymentValidationExceptionMatcher(String code, String description) { + this.code = code; + this.description = description; + } + + public static PaymentValidationExceptionMatcher aValidationExceptionContaining(String code, String description) { + return new PaymentValidationExceptionMatcher(code, description); + } + + @Override + protected boolean matchesSafely(PaymentValidationException e) { + RequestError requestError = e.getRequestError(); + return code.equals(requestError.getCode()) && + description.equals(requestError.getDescription()); + } + + @Override + public void describeTo(Description description) { + description.appendText(PaymentValidationException.class.getCanonicalName()) + .appendText(" with ") + .appendText(" RequestError. { code = ") + .appendValue(code) + .appendText(", description = ") + .appendValue(this.description) + .appendText(" }"); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/matcher/RefundValidationExceptionMatcher.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/matcher/RefundValidationExceptionMatcher.java new file mode 100644 index 000000000..5d09a336c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/matcher/RefundValidationExceptionMatcher.java @@ -0,0 +1,39 @@ +package uk.gov.pay.api.matcher; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import uk.gov.pay.api.exception.RefundsValidationException; +import uk.gov.pay.api.model.RequestError; + +public class RefundValidationExceptionMatcher extends TypeSafeMatcher { + + private final String code; + private final String description; + + private RefundValidationExceptionMatcher(String code, String description) { + this.code = code; + this.description = description; + } + + public static RefundValidationExceptionMatcher aValidationExceptionContaining(String code, String description) { + return new RefundValidationExceptionMatcher(code, description); + } + + @Override + protected boolean matchesSafely(RefundsValidationException e) { + RequestError requestError = e.getRequestError(); + return code.equals(requestError.getCode()) && + description.equals(requestError.getDescription()); + } + + @Override + public void describeTo(Description description) { + description.appendText(RefundsValidationException.class.getCanonicalName()) + .appendText(" with ") + .appendText(" RequestError. { code = ") + .appendValue(code) + .appendText(", description = ") + .appendValue(this.description) + .appendText(" }"); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/model/PaymentTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/model/PaymentTest.java new file mode 100644 index 000000000..5dd1a862d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/model/PaymentTest.java @@ -0,0 +1,64 @@ +package uk.gov.pay.api.model; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import uk.gov.pay.api.model.links.PaymentWithAllLinks; + +import java.io.IOException; +import java.net.URI; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +public class PaymentTest { + + private ObjectMapper objectMapper = new ObjectMapper(); + + @Test + public void shouldNotPrintEmailAndCardDetailsWhenToString() throws IOException { + + URI selfUri = URI.create("http://self.link.com"); + URI eventsUri = URI.create("http://self.link.com/events"); + URI cancelUri = URI.create("http://self.link.com/cancel"); + URI refundsUri = URI.create("http://self.link.com/cancel"); + URI captureUri = URI.create("self.link.com/capture"); + URI authUri = URI.create("self.link.com/auth"); + + // language=JSON + ChargeFromResponse paymentFromConnector = objectMapper.readValue("{\n" + + " \"email\": \"user@example.com\",\n" + + " \"card_details\": {\n" + + " \"card_brand\": \"Visa\",\n" + + " \"expiry_date\": \"12/19\",\n" + + " \"cardholder_name\": \"Mr. payment\",\n" + + " \"billing_address\": {\n" + + " \"line1\": \"line1\",\n" + + " \"postcode\": \"NR25 6EG\",\n" + + " \"country\": \"UK\"\n" + + " },\n" + + " \"last_digits_card_number\": \"4321\",\n" + + " \"first_digits_card_number\": \"654321\"\n" + + " },\n" + + " \"amount\": 500,\n" + + " \"language\": \"en\",\n" + + " \"state\": {\n" + + " \"status\": \"created\",\n" + + " \"finished\": false\n" + + " }\n" + + "}", ChargeFromResponse.class); + + PaymentWithAllLinks payment = PaymentWithAllLinks.valueOf(Charge.from(paymentFromConnector), selfUri, eventsUri, + cancelUri, refundsUri, captureUri, authUri); + + assertThat(payment.toString(), not(containsString("user@example.com"))); + assertThat(payment.toString(), not(containsString("last_digits_card_number"))); + assertThat(payment.toString(), not(containsString("first_digits_card_number"))); + assertThat(payment.toString(), not(containsString("654321"))); + assertThat(payment.toString(), not(containsString("4321"))); + assertThat(payment.toString(), not(containsString("12/19"))); + assertThat(payment.toString(), not(containsString("Mr. payment"))); + assertThat(payment.toString(), not(containsString("NR25 6EG"))); + assertThat(payment.toString(), not(containsString("UK"))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/model/RequestErrorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/model/RequestErrorTest.java new file mode 100644 index 000000000..c85f0b8af --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/model/RequestErrorTest.java @@ -0,0 +1,23 @@ +package uk.gov.pay.api.model; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.model.RequestError.aRequestError; + +public class RequestErrorTest { + + @Test + void shouldGetExpectedValuesWhenToStringIsCalled() { + RequestError requestError = aRequestError(RequestError.Code.CREATE_PAYMENT_ACCOUNT_ERROR); + assertThat(requestError.toString(), is("RequestError{field=null, code=P0199, name=CREATE_PAYMENT_ACCOUNT_ERROR, description='There is an error with this account. Contact support with your error code - https://www.payments.service.gov.uk/support/ .'}")); + } + + @Test + void shouldGetExpectedValuesForAgreementNotFoundError() { + RequestError requestError = aRequestError("set_up_agreement", RequestError.Code.CREATE_PAYMENT_AGREEMENT_ID_ERROR); + assertThat(requestError.toString(), is("RequestError{field=set_up_agreement, code=P0103, name=CREATE_PAYMENT_AGREEMENT_ID_ERROR, description='Invalid attribute value: set_up_agreement. Agreement ID does not exist'}")); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/AuthorisationResourceValidationIT.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/AuthorisationResourceValidationIT.java new file mode 100644 index 000000000..2336474f1 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/AuthorisationResourceValidationIT.java @@ -0,0 +1,152 @@ +package uk.gov.pay.api.resources; + +import com.google.gson.GsonBuilder; +import io.restassured.response.ValidatableResponse; +import org.junit.Test; +import uk.gov.pay.api.it.PaymentResourceITestBase; + +import java.util.HashMap; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static org.hamcrest.core.Is.is; + +public class AuthorisationResourceValidationIT extends PaymentResourceITestBase { + + private static final String AUTH_PATH = "/v1/auth"; + + @Test + public void authorisation_responseWith422_whenOneTimeTokenFieldIsNumeric() { + String payload = toJson( + Map.of("one_time_token", 1234567890, + "card_number", "1234567890123456", + "cvc", "123", + "expiry_date", "09/27", + "cardholder_name", "Joe Boggs")); + + postAuthRequest(payload) + .statusCode(422) + .contentType(JSON) + .body("code", is("P0102")) + .body("field", is("one_time_token")) + .body("description", is("Invalid attribute value: one_time_token. Must be of type String")); + } + + @Test + public void authorisation_responseWith422_whenOneTimeTokenFieldIsMissing() { + String payload = toJson( + Map.of("card_number", "1234567890123456", + "cvc", "123", + "expiry_date", "09/27", + "cardholder_name", "Joe Boggs")); + + postAuthRequest(payload) + .statusCode(422) + .contentType(JSON) + .body("code", is("P0101")) + .body("field", is("one_time_token")) + .body("description", is("Missing mandatory attribute: one_time_token")); + } + + @Test + public void authorisation_responseWith422_whenCardNumberFieldHasNullValue() { + Map payloadMap = new HashMap<>(); + payloadMap.put("one_time_token", "1234567890"); + payloadMap.put("card_number", "1234567890123456"); + payloadMap.put("cvc", null); + payloadMap.put("expiry_date", "09/27"); + payloadMap.put("cardholder_name", "Joe Boggs"); + + String payload = new GsonBuilder() + .serializeNulls() + .setPrettyPrinting() + .create() + .toJson(payloadMap); + + + postAuthRequest(payload) + .statusCode(422) + .contentType(JSON) + .body("code", is("P0101")) + .body("field", is("cvc")) + .body("description", is("Missing mandatory attribute: cvc")); + } + + @Test + public void authorisation_responseWith422_whenCardholderNameIsEmpty() { + String payload = toJson( + Map.of("one_time_token", "1234567890", + "card_number", "1234567890123456", + "cvc", "123", + "expiry_date", "09/27", + "cardholder_name", "")); + + postAuthRequest(payload) + .statusCode(422) + .contentType(JSON) + .body("code", is("P0101")) + .body("field", is("cardholder_name")) + .body("description", is("Missing mandatory attribute: cardholder_name")); + } + + @Test + public void authorisation_responseWith422_whenCvcIsEmpty() { + String payload = toJson( + Map.of("one_time_token", "1234567890", + "card_number", "1234567890123456", + "cvc", "", + "expiry_date", "09/27", + "cardholder_name", "Joe Boggs")); + + postAuthRequest(payload) + .statusCode(422) + .contentType(JSON) + .body("code", is("P0102")) + .body("field", is("cvc")) + .body("description", is("Invalid attribute value: cvc. Must be between 3 and 4 characters long")); + } + + @Test + public void authorisation_responseWith422_whenCardholderNameIsTooLong() { + String payload = toJson( + Map.of("one_time_token", "1234567890", + "card_number", "1234567890123456", + "cvc", "123", + "expiry_date", "09/27", + "cardholder_name", "Joe Boggs".repeat(29))); + + postAuthRequest(payload) + .statusCode(422) + .contentType(JSON) + .body("code", is("P0102")) + .body("field", is("cardholder_name")) + .body("description", is("Invalid attribute value: cardholder_name. Must be less than or equal to 255 characters long")); + } + + @Test + public void authorisation_responseWith422_whenExpiryDateIsTooLong() { + String payload = toJson( + Map.of("one_time_token", "1234567890", + "card_number", "1234567890123456", + "cvc", "123", + "expiry_date", "Sep 28", + "cardholder_name", "Joe Boggs")); + + postAuthRequest(payload) + .statusCode(422) + .contentType(JSON) + .body("code", is("P0102")) + .body("field", is("expiry_date")) + .body("description", is("Invalid attribute value: expiry_date. Must be a valid date with the format MM/YY")); + } + + protected ValidatableResponse postAuthRequest(String payload) { + return given().port(app.getLocalPort()) + .body(payload) + .accept(JSON) + .contentType(JSON) + .post(AUTH_PATH) + .then(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/GetOnePaymentStrategyTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/GetOnePaymentStrategyTest.java new file mode 100644 index 000000000..45d51300a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/GetOnePaymentStrategyTest.java @@ -0,0 +1,57 @@ +package uk.gov.pay.api.resources; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.service.GetPaymentService; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class GetOnePaymentStrategyTest { + + @Mock + private GetPaymentService mockGetPaymentService; + + private GetOnePaymentStrategy getOnePaymentStrategy; + private String mockPaymentId = "some-payment-id"; + private Account mockAccountId = new Account("some-account-id", TokenPaymentType.CARD, "a-token-link"); + + @Test + public void validateAndExecuteUsesLedgerOnlyStrategy() { + String strategy = "ledger-only"; + getOnePaymentStrategy = new GetOnePaymentStrategy(strategy, mockAccountId, mockPaymentId, mockGetPaymentService); + getOnePaymentStrategy.validateAndExecute(); + + verify(mockGetPaymentService).getLedgerTransaction(mockAccountId, mockPaymentId); + verify(mockGetPaymentService, never()).getConnectorCharge(mockAccountId, mockPaymentId); + verify(mockGetPaymentService, never()).getPayment(mockAccountId, mockPaymentId); + } + + @ParameterizedTest + @ValueSource(strings = {"", "unknown"}) + public void validateAndExecuteUsesDefaultStrategy(String strategy) { + getOnePaymentStrategy = new GetOnePaymentStrategy(strategy, mockAccountId, mockPaymentId, mockGetPaymentService); + getOnePaymentStrategy.validateAndExecute(); + + verify(mockGetPaymentService).getPayment(mockAccountId, mockPaymentId); + } + + @Test + public void validateAndExecuteShouldUseConnectorOnlyStrategy() { + String strategy = "connector-only"; + getOnePaymentStrategy = new GetOnePaymentStrategy(strategy, mockAccountId, mockPaymentId, mockGetPaymentService); + + getOnePaymentStrategy.validateAndExecute(); + + verify(mockGetPaymentService).getConnectorCharge(mockAccountId, mockPaymentId); + verify(mockGetPaymentService, never()).getLedgerTransaction(mockAccountId, mockPaymentId); + verify(mockGetPaymentService, never()).getPayment(mockAccountId, mockPaymentId); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/GetPaymentEventsStrategyTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/GetPaymentEventsStrategyTest.java new file mode 100644 index 000000000..9f8d648ed --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/GetPaymentEventsStrategyTest.java @@ -0,0 +1,57 @@ +package uk.gov.pay.api.resources; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.service.GetPaymentEventsService; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class GetPaymentEventsStrategyTest { + + @Mock + private GetPaymentEventsService getPaymentEventsService; + + private GetPaymentEventsStrategy getPaymentEventsStrategy; + private String mockPaymentId = "some-payment-id"; + private Account mockAccountId = new Account("some-account-id", TokenPaymentType.CARD, "a-token-link"); + + @Test + public void validateAndExecuteUsesLedgerOnlyStrategy() { + String strategy = "ledger-only"; + getPaymentEventsStrategy = new GetPaymentEventsStrategy(strategy, mockAccountId, mockPaymentId, getPaymentEventsService); + getPaymentEventsStrategy.validateAndExecute(); + + verify(getPaymentEventsService).getPaymentEventsFromLedger(mockAccountId, mockPaymentId); + verify(getPaymentEventsService, never()).getPaymentEventsFromConnector(mockAccountId, mockPaymentId); + verify(getPaymentEventsService, never()).getPaymentEvents(mockAccountId, mockPaymentId); + } + + @ParameterizedTest + @ValueSource(strings = {"", "unknown"}) + public void validateAndExecuteUsesDefaultStrategy(String strategy) { + getPaymentEventsStrategy = new GetPaymentEventsStrategy(strategy, mockAccountId, mockPaymentId, getPaymentEventsService); + getPaymentEventsStrategy.validateAndExecute(); + + verify(getPaymentEventsService).getPaymentEvents(mockAccountId, mockPaymentId); + } + + @Test + public void validateAndExecuteShouldUseConnectorOnlyStrategy() { + String strategy = "connector-only"; + getPaymentEventsStrategy = new GetPaymentEventsStrategy(strategy, mockAccountId, mockPaymentId, getPaymentEventsService); + + getPaymentEventsStrategy.validateAndExecute(); + + verify(getPaymentEventsService).getPaymentEventsFromConnector(mockAccountId, mockPaymentId); + verify(getPaymentEventsService, never()).getPaymentEventsFromLedger(mockAccountId, mockPaymentId); + verify(getPaymentEventsService, never()).getPaymentEvents(mockAccountId, mockPaymentId); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/GetPaymentRefundStrategyTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/GetPaymentRefundStrategyTest.java new file mode 100644 index 000000000..9bc22beb7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/GetPaymentRefundStrategyTest.java @@ -0,0 +1,57 @@ +package uk.gov.pay.api.resources; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.service.GetPaymentRefundService; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class GetPaymentRefundStrategyTest { + + private GetPaymentRefundService mockGetPaymentRefundService = mock(GetPaymentRefundService.class); + private GetPaymentRefundStrategy getPaymentRefundStrategy; + + private String paymentId = "payment-id"; + private String refundId = "refund-id"; + private Account account = new Account("account-id", TokenPaymentType.CARD, "a-token-link"); + + @Test + public void validateAndExecuteUsesLedgerOnlyStrategy() { + String strategy = "ledger-only"; + getPaymentRefundStrategy = new GetPaymentRefundStrategy(strategy, account, paymentId, refundId, mockGetPaymentRefundService); + getPaymentRefundStrategy.validateAndExecute(); + + verify(mockGetPaymentRefundService).getLedgerPaymentRefund(account, paymentId, refundId); + verify(mockGetPaymentRefundService, never()).getConnectorPaymentRefund(account, paymentId, refundId); + verify(mockGetPaymentRefundService, never()).getPaymentRefund(account, paymentId, refundId); + } + + @ParameterizedTest + @ValueSource(strings = {"", "unknown"}) + public void validateAndExecuteUsesDefaultStrategy(String strategy) { + getPaymentRefundStrategy = new GetPaymentRefundStrategy(strategy, account, paymentId, refundId, mockGetPaymentRefundService); + getPaymentRefundStrategy.validateAndExecute(); + + verify(mockGetPaymentRefundService).getPaymentRefund(account, paymentId, refundId); + } + + @Test + public void validateAndExecuteShouldUseConnectorOnlyStrategy() { + String strategy = "connector-only"; + getPaymentRefundStrategy = new GetPaymentRefundStrategy(strategy, account, paymentId, refundId, mockGetPaymentRefundService); + + getPaymentRefundStrategy.validateAndExecute(); + + verify(mockGetPaymentRefundService).getConnectorPaymentRefund(account, paymentId, refundId); + verify(mockGetPaymentRefundService, never()).getLedgerPaymentRefund(account, paymentId, refundId); + verify(mockGetPaymentRefundService, never()).getPaymentRefund(account, paymentId, refundId); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/GetPaymentRefundsStrategyTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/GetPaymentRefundsStrategyTest.java new file mode 100644 index 000000000..7fd1690d6 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/GetPaymentRefundsStrategyTest.java @@ -0,0 +1,44 @@ +package uk.gov.pay.api.resources; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.service.GetPaymentRefundsService; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class GetPaymentRefundsStrategyTest { + + private GetPaymentRefundsService mockGetPaymentRefundsService = mock(GetPaymentRefundsService.class); + private GetPaymentRefundsStrategy getPaymentRefundsStrategy; + + private String paymentId = "payment-id"; + private Account account = new Account("account-id", TokenPaymentType.CARD, "a-token-link"); + + @ParameterizedTest + @ValueSource(strings = {"ledger-only", "", "unknown"}) + public void validateAndExecuteShouldUseLedgerOnlyForListedStrategies(String strategy) { + getPaymentRefundsStrategy = new GetPaymentRefundsStrategy(strategy, account, paymentId, mockGetPaymentRefundsService); + getPaymentRefundsStrategy.validateAndExecute(); + + verify(mockGetPaymentRefundsService).getLedgerTransactionTransactions(account, paymentId); + verify(mockGetPaymentRefundsService, never()).getConnectorPaymentRefunds(account, paymentId); + } + + @Test + public void validateAndExecuteShouldUseConnectorOnlyStrategy() { + getPaymentRefundsStrategy = new GetPaymentRefundsStrategy("connector-only", account, paymentId, mockGetPaymentRefundsService); + + getPaymentRefundsStrategy.validateAndExecute(); + + verify(mockGetPaymentRefundsService).getConnectorPaymentRefunds(account, paymentId); + verify(mockGetPaymentRefundsService, never()).getLedgerTransactionTransactions(account, paymentId); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/HealthCheckResourceTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/HealthCheckResourceTest.java new file mode 100644 index 000000000..e07b91556 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/HealthCheckResourceTest.java @@ -0,0 +1,103 @@ +package uk.gov.pay.api.resources; + +import com.codahale.metrics.health.HealthCheck; +import com.codahale.metrics.health.HealthCheckRegistry; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.jayway.jsonassert.JsonAssert; +import io.dropwizard.setup.Environment; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.core.Response; +import java.util.SortedMap; +import java.util.TreeMap; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class HealthCheckResourceTest { + @Mock + private Environment environment; + + @Mock + private HealthCheckRegistry healthCheckRegistry; + + private HealthCheckResource resource; + + @BeforeEach + public void setup() { + when(environment.healthChecks()).thenReturn(healthCheckRegistry); + resource = new HealthCheckResource(environment); + } + + @Test + public void checkHealthCheck_isUnHealthy() throws JsonProcessingException { + SortedMap map = new TreeMap<>(); + map.put("ping", HealthCheck.Result.unhealthy("application is unavailable")); + map.put("deadlocks", HealthCheck.Result.unhealthy("no new threads available")); + + when(healthCheckRegistry.runHealthChecks()).thenReturn(map); + + Response response = resource.healthCheck(); + + assertThat(response.getStatus(), is(503)); + + ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); + String body = ow.writeValueAsString(response.getEntity()); + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.ping.healthy", is(false)) + .assertThat("$.deadlocks.healthy", is(false)); + } + + @Test + public void checkHealthCheck_isHealthy() throws JsonProcessingException { + SortedMap map = new TreeMap<>(); + map.put("ping", HealthCheck.Result.healthy()); + map.put("deadlocks", HealthCheck.Result.healthy()); + + when(healthCheckRegistry.runHealthChecks()).thenReturn(map); + + Response response = resource.healthCheck(); + + assertThat(response.getStatus(), is(200)); + + ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); + String body = ow.writeValueAsString(response.getEntity()); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.ping.healthy", is(true)) + .assertThat("$.deadlocks.healthy", is(true)); + } + + @Test + public void checkHealthCheck_isUnhealthyIfPartiallyHealthy() throws JsonProcessingException { + SortedMap map = new TreeMap<>(); + map.put("ping", HealthCheck.Result.healthy()); + map.put("deadlocks", HealthCheck.Result.unhealthy("no new threads available")); + + when(healthCheckRegistry.runHealthChecks()).thenReturn(map); + + Response response = resource.healthCheck(); + + assertThat(response.getStatus(), is(503)); + + ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); + String body = ow.writeValueAsString(response.getEntity()); + + JsonAssert.with(body) + .assertThat("$.*", hasSize(2)) + .assertThat("$.ping.healthy", is(true)) + .assertThat("$.deadlocks.healthy", is(false)); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/PaymentsResourceCreatePaymentTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/PaymentsResourceCreatePaymentTest.java new file mode 100644 index 000000000..610283484 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/resources/PaymentsResourceCreatePaymentTest.java @@ -0,0 +1,139 @@ +package uk.gov.pay.api.resources; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.CardDetails; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.CreateCardPaymentRequestBuilder; +import uk.gov.pay.api.model.CreatedPaymentWithAllLinks; +import uk.gov.pay.api.model.PaymentSettlementSummary; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.RefundSummary; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.model.links.PaymentWithAllLinks; +import uk.gov.pay.api.service.CancelPaymentService; +import uk.gov.pay.api.service.CapturePaymentService; +import uk.gov.pay.api.service.CreatePaymentService; +import uk.gov.pay.api.service.GetPaymentEventsService; +import uk.gov.pay.api.service.GetPaymentService; +import uk.gov.pay.api.service.PaymentSearchService; +import uk.gov.pay.api.service.PublicApiUriGenerator; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; + +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.Collections; + +import static org.apache.http.HttpHeaders.CACHE_CONTROL; +import static org.apache.http.HttpHeaders.PRAGMA; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +import static uk.gov.pay.api.model.CreatedPaymentWithAllLinks.WhenCreated.BRAND_NEW; + +@ExtendWith(MockitoExtension.class) +public class PaymentsResourceCreatePaymentTest { + + @InjectMocks + private PaymentsResource paymentsResource; + + @Mock + private CreatePaymentService createPaymentService; + + @Mock + private PaymentSearchService paymentSearchService; + + @Mock + private PublicApiUriGenerator publicApiUriGenerator; + + @Mock + private GetPaymentService getPaymentService; + + @Mock + private CapturePaymentService capturePaymentService; + + @Mock + private CancelPaymentService cancelPaymentService; + + @Mock + private GetPaymentEventsService getPaymentEventsService; + + private final String PAYMENT_URI = "https://my.link/v1/payments/abc123"; + + @BeforeEach + public void setup() { + when(publicApiUriGenerator.getPaymentURI(anyString())).thenReturn(URI.create(PAYMENT_URI)); + } + + @Test + void createNewPayment_withCardPayment_invokesCreatePaymentService() { + Account account = new Account("foo", TokenPaymentType.CARD, "a-token-link"); + var createPaymentRequest = CreateCardPaymentRequestBuilder.builder() + .amount(100) + .returnUrl("https://somewhere.test") + .reference("my_ref") + .description("New passport") + .build(); + + PaymentWithAllLinks injectedResponse = aSuccessfullyCreatedPayment(); + CreatedPaymentWithAllLinks payment = CreatedPaymentWithAllLinks.of(injectedResponse, BRAND_NEW); + + when(createPaymentService.create(account, createPaymentRequest, null)).thenReturn(payment); + + Response newPayment = paymentsResource.createNewPayment(account, createPaymentRequest, null); + + assertThat(newPayment.getHeaderString(PRAGMA), is("no-cache")); + assertThat(newPayment.getHeaderString(CACHE_CONTROL), is("no-store")); + assertThat(newPayment.getStatus(), is(201)); + assertThat(newPayment.getLocation(), is(URI.create(PAYMENT_URI))); + assertThat(newPayment.getEntity(), sameInstance(injectedResponse)); + } + + @NotNull + private PaymentWithAllLinks aSuccessfullyCreatedPayment() { + final Address cardholderAddress = new Address("123 Acacia Ave", "", "", "London", "GB"); + return new PaymentWithAllLinks.PaymentWithAllLinksBuilder() + .withChargeId("abc123") + .withAmount(100L) + .withState(new PaymentState("created", false)) + .withReturnUrl("https://somewhere.test") + .withDescription("New Passport") + .withReference("my_ref") + .withEmail("made.up@example.com") + .withPaymentProvider("sandbox") + .withCreatedDate("2018-01-01T11:12:13Z") + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(false) + .withMoto(false) + .withRefundSummary(new RefundSummary()) + .withSettlementSummary(new PaymentSettlementSummary()) + .withCardDetails(new CardDetails("9876", "482393", "Anne Onymous", "12/20", cardholderAddress, "visa", null, null)) + .withPaymentConnectorResponseLinks(Collections.emptyList()) + .withSelfLink(URI.create(PAYMENT_URI)) + .withPaymentEventsUri(URI.create(PAYMENT_URI + "/events")) + .withPaymentCancelUri(URI.create(PAYMENT_URI + "/cancel")) + .withPaymentRefundsUri(URI.create(PAYMENT_URI + "/refunds")) + .withPaymentCaptureUri(URI.create(PAYMENT_URI + "/capture")) + .withPaymentAuthorisationUri(URI.create(PAYMENT_URI + "/auth")) + .withCorporateCardSurcharge(null) + .withTotalAmount(null) + .withProviderId("providerId") + .withMetadata(null) + .withFee(null) + .withNetAmount(null) + .withAuthorisationSummary(null) + .withAgreementId(null) + .withAuthorisationMode(AuthorisationMode.WEB) + .build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/AgreementsServiceTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/AgreementsServiceTest.java new file mode 100644 index 000000000..ae753e3cd --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/AgreementsServiceTest.java @@ -0,0 +1,140 @@ +package uk.gov.pay.api.service; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.agreement.model.AgreementCreatedResponse; +import uk.gov.pay.api.agreement.model.CreateAgreementRequest; +import uk.gov.pay.api.agreement.service.AgreementsService; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.CancelAgreementException; +import uk.gov.pay.api.exception.CreateAgreementException; +import uk.gov.pay.api.ledger.service.LedgerUriGenerator; +import uk.gov.pay.api.model.CreateAgreementRequestBuilder; +import uk.gov.pay.api.model.search.PaginationDecorator; +import uk.gov.pay.api.utils.mocks.CreateAgreementRequestParams; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import static javax.ws.rs.client.Entity.json; +import static javax.ws.rs.core.Response.Status.NO_CONTENT; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static uk.gov.pay.api.utils.Payloads.agreementPayload; +import static uk.gov.pay.api.utils.mocks.CreateAgreementRequestParams.CreateAgreementRequestParamsBuilder.aCreateAgreementRequestParams; + +@ExtendWith(MockitoExtension.class) +class AgreementsServiceTest { + + private static final String REFERENCE_ID = "test"; + private static final String AGREEMENT_ID = "12345678901234567890123456"; + private static final String AGREEMENTS_CONNECTOR_URI = "/v1/api/accounts/GATEWAY_ACCOUNT_ID/agreements"; // pragma: allowlist secret + private static final String AGREEMENT_CONNECTOR_CANCEL_URI = AGREEMENTS_CONNECTOR_URI + '/' + AGREEMENT_ID + "/cancel"; + @Mock + private Account mockAccount; + @Mock + private ConnectorUriGenerator mockConnectorUriGenerator; + @Mock + private LedgerUriGenerator mockLedgerUriGenerator; + @Mock + private Client mockClient; + @Mock + private PublicApiConfig mockConfiguration; + @Mock + private WebTarget mockWebTarget; + @Mock + private Invocation.Builder mockInvocationBuilder; + @Mock + private Response mockConnectorResponse; + + private AgreementsService underTest; + + @BeforeEach + void setUp() { + underTest = new AgreementsService(new ConnectorService(mockClient, mockConnectorUriGenerator), + new LedgerService(mockClient, mockLedgerUriGenerator), new PaginationDecorator(mockConfiguration)); + } + + @Test + void shouldCreateAgreement() { + when(mockConnectorUriGenerator.getAgreementURI(mockAccount)).thenReturn(AGREEMENTS_CONNECTOR_URI); + when(mockClient.target(AGREEMENTS_CONNECTOR_URI)).thenReturn(mockWebTarget); + when(mockWebTarget.request()).thenReturn(mockInvocationBuilder); + when(mockInvocationBuilder.accept(MediaType.APPLICATION_JSON)).thenReturn(mockInvocationBuilder); + + CreateAgreementRequestParams createAgreementRequestParams = aCreateAgreementRequestParams() + .withReference(REFERENCE_ID) + .build(); + when(mockInvocationBuilder.post(json(agreementPayload(createAgreementRequestParams)))).thenReturn(mockConnectorResponse); + + when(mockConnectorResponse.getStatus()).thenReturn(HttpStatus.SC_CREATED); + + var agreementResponse = new AgreementCreatedResponse(AGREEMENT_ID); + when(mockConnectorResponse.readEntity(AgreementCreatedResponse.class)).thenReturn(agreementResponse); + + CreateAgreementRequest agreementCreateRequest = new CreateAgreementRequest(CreateAgreementRequestBuilder + .builder().reference(REFERENCE_ID)); + AgreementCreatedResponse agreementResponseFromService = underTest.createAgreement(mockAccount, agreementCreateRequest); + + assertThat(agreementResponseFromService.getAgreementId(), is(AGREEMENT_ID)); + } + + @Test + void shouldThrowExceptionIfConnectorReturnsRecurringPaymentsNotAllowedExceptionWhenCreatingAgreement() { + when(mockConnectorUriGenerator.getAgreementURI(mockAccount)).thenReturn(AGREEMENTS_CONNECTOR_URI); + when(mockClient.target(AGREEMENTS_CONNECTOR_URI)).thenReturn(mockWebTarget); + when(mockWebTarget.request()).thenReturn(mockInvocationBuilder); + when(mockInvocationBuilder.accept(MediaType.APPLICATION_JSON)).thenReturn(mockInvocationBuilder); + + CreateAgreementRequestParams createAgreementRequestParams = aCreateAgreementRequestParams() + .withReference(REFERENCE_ID) + .build(); + when(mockInvocationBuilder.post(json(agreementPayload(createAgreementRequestParams)))).thenReturn(mockConnectorResponse); + + when(mockConnectorResponse.getStatus()).thenReturn(HttpStatus.SC_UNPROCESSABLE_ENTITY); + CreateAgreementRequest agreementCreateRequest = new CreateAgreementRequest(CreateAgreementRequestBuilder + .builder().reference(REFERENCE_ID)); + assertThrows(CreateAgreementException.class, () -> underTest.createAgreement(mockAccount, agreementCreateRequest)); + + verify(mockConnectorResponse).close(); + } + + @Test + void shouldCancelAgreement() { + when(mockConnectorUriGenerator.cancelAgreementURI(mockAccount, AGREEMENT_ID)).thenReturn(AGREEMENT_CONNECTOR_CANCEL_URI); + when(mockClient.target(AGREEMENT_CONNECTOR_CANCEL_URI)).thenReturn(mockWebTarget); + when(mockWebTarget.request()).thenReturn(mockInvocationBuilder); + when(mockInvocationBuilder.post(null)).thenReturn(mockConnectorResponse); + when(mockConnectorResponse.getStatus()).thenReturn(HttpStatus.SC_NO_CONTENT); + + Response response = underTest.cancelAgreement(mockAccount, AGREEMENT_ID); + + assertThat(response.getStatus(), is(NO_CONTENT.getStatusCode())); + + verify(mockConnectorResponse).close(); + } + + @Test + void shouldThrowExceptionIfConnectorReturnsUnexpectedStatusCodeWhenCancellingAgreement() { + when(mockConnectorUriGenerator.cancelAgreementURI(mockAccount, AGREEMENT_ID)).thenReturn(AGREEMENT_CONNECTOR_CANCEL_URI); + when(mockClient.target(AGREEMENT_CONNECTOR_CANCEL_URI)).thenReturn(mockWebTarget); + when(mockWebTarget.request()).thenReturn(mockInvocationBuilder); + when(mockInvocationBuilder.post(null)).thenReturn(mockConnectorResponse); + when(mockConnectorResponse.getStatus()).thenReturn(HttpStatus.SC_BAD_REQUEST); + + assertThrows(CancelAgreementException.class, () -> underTest.cancelAgreement(mockAccount, AGREEMENT_ID)); + + verify(mockConnectorResponse).close(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/AuthorisationServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/AuthorisationServicePactTest.java new file mode 100644 index 000000000..e848da583 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/AuthorisationServicePactTest.java @@ -0,0 +1,107 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.exception.AuthorisationRequestException; +import uk.gov.pay.api.exception.mapper.AuthorisationRequestExceptionMapper; +import uk.gov.pay.api.model.AuthorisationRequest; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Response; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasProperty; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class AuthorisationServicePactTest { + private AuthorisationService authorisationService; + private AuthorisationRequest request; + private AuthorisationRequestExceptionMapper mapper; + + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + + @Mock + private PublicApiConfig mockConfiguration; + + @Before + public void setup() { + when(mockConfiguration.getConnectorUrl()).thenReturn(connectorRule.getUrl()); + ConnectorUriGenerator uriGenerator = new ConnectorUriGenerator(mockConfiguration); + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + authorisationService = new AuthorisationService(client, uriGenerator); + mapper = new AuthorisationRequestExceptionMapper(); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-authorise-moto-api-payment-with-valid-token"}) + public void testAuthorisePayment() { + request = new AuthorisationRequest("onetime-12345-token", "4242424242424242", "123", + "09/29", "Joe Boggs"); + Response response = authorisationService.authoriseRequest(request); + assertThat(response.getStatus(), is(204)); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-authorise-moto-api-payment-with-invalid-token"}) + public void testInvalidToken() { + request = new AuthorisationRequest("invalid-token", "4242424242424242", "123", + "09/29", "Joe Boggs"); + AuthorisationRequestException authorisationRequestException = assertThrows(AuthorisationRequestException.class, + () -> authorisationService.authoriseRequest(request)); + + assertThat(authorisationRequestException, hasProperty("errorStatus", Matchers.is(400))); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-authorise-moto-api-payment-with-an-already-used-token"}) + public void testAlreadyUsedToken() { + request = new AuthorisationRequest("onetime-12345-token", "4242424242424242", "123", + "09/29", "Joe Boggs"); + AuthorisationRequestException authorisationRequestException = assertThrows(AuthorisationRequestException.class, + () -> authorisationService.authoriseRequest(request)); + + assertThat(authorisationRequestException, hasProperty("errorStatus", Matchers.is(400))); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-authorise-moto-api-payment-with-invalid-card-number"}) + public void testInvalidCardNumber() { + request = new AuthorisationRequest("onetime-12345-token", "0000000000000000", "123", + "09/29", "Joe Boggs"); + AuthorisationRequestException authorisationRequestException = assertThrows(AuthorisationRequestException.class, + () -> authorisationService.authoriseRequest(request)); + + assertThat(authorisationRequestException, hasProperty("errorStatus", Matchers.is(402))); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-authorise-moto-api-payment-with-invalid-expiry-date"}) + public void testMissingExpiryDate() { + request = new AuthorisationRequest("onetime-12345-token", "4242424242424242", "123", + "09/21", "Joe Boggs"); + AuthorisationRequestException authorisationRequestException = assertThrows(AuthorisationRequestException.class, + () -> authorisationService.authoriseRequest(request)); + + assertThat(authorisationRequestException, hasProperty("errorStatus", Matchers.is(422))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CancelAgreementConnectorServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CancelAgreementConnectorServicePactTest.java new file mode 100644 index 000000000..b3d2a52fc --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CancelAgreementConnectorServicePactTest.java @@ -0,0 +1,67 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.CancelAgreementException; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.service.payments.commons.model.ErrorIdentifier; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CancelAgreementConnectorServicePactTest { + + private final Account ACCOUNT = new Account("123456", TokenPaymentType.CARD, "a-token-link"); + + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + + private ConnectorService connectorService; + @Mock + private PublicApiConfig mockConfiguration; + + @Before + public void setUp() { + when(mockConfiguration.getConnectorUrl()).thenReturn(connectorRule.getUrl()); + ConnectorUriGenerator connectorUriGenerator = new ConnectorUriGenerator(mockConfiguration); + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + connectorService = new ConnectorService(client, connectorUriGenerator); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-cancel-agreement-with-active-status"}) + public void cancelAnAgreementWithActiveStatus() { + connectorService.cancelAgreement(ACCOUNT, "agreement1234567"); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-cancel-agreement-with-cancelled-status"}) + public void cancelAnAgreementWithCancelledStatus() { + CancelAgreementException cancelAgreementException = assertThrows(CancelAgreementException.class, + () -> connectorService.cancelAgreement(ACCOUNT, "agreement9876543")); + assertThat(cancelAgreementException, hasProperty("errorStatus", Matchers.is(400))); + assertThat(cancelAgreementException, hasProperty("errorIdentifier", is(ErrorIdentifier.AGREEMENT_NOT_ACTIVE))); + assertThat(cancelAgreementException.getConnectorErrorMessage(), is("Payment instrument not active.")); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CancelPaymentServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CancelPaymentServicePactTest.java new file mode 100644 index 000000000..be9e89285 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CancelPaymentServicePactTest.java @@ -0,0 +1,53 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Response; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CancelPaymentServicePactTest { + + private final Account ACCOUNT = new Account("123456", TokenPaymentType.CARD, "a-token-link"); + + private CancelPaymentService cancelPaymentService; + + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + + @Mock + private PublicApiConfig mockConfiguration; + + @Before + public void setUp() { + when(mockConfiguration.getConnectorUrl()).thenReturn(connectorRule.getUrl()); + ConnectorUriGenerator connectorUriGenerator = new ConnectorUriGenerator(mockConfiguration); + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + cancelPaymentService = new CancelPaymentService(client, connectorUriGenerator); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-cancel-payment-with-created-state"}) + public void cancelAPaymentWithCreatedState() { + Response cancelPaymentResponse = cancelPaymentService.cancel(ACCOUNT, "charge8133029783750964639"); + assertThat(cancelPaymentResponse.getStatus(), is(204)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CapturePaymentServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CapturePaymentServicePactTest.java new file mode 100644 index 000000000..d4febdf02 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CapturePaymentServicePactTest.java @@ -0,0 +1,59 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Response; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CapturePaymentServicePactTest { + + private static final String ACCOUNT_ID = "123456"; + private static final String CHARGE_ID = "ch_e36c168c41a0"; + + private CapturePaymentService capturePaymentService; + + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + + @Mock + private PublicApiConfig mockConfiguration; + + @Before + public void setup() { + when(mockConfiguration.getConnectorUrl()).thenReturn(connectorRule.getUrl()); + + ConnectorUriGenerator connectorUriGenerator = new ConnectorUriGenerator(mockConfiguration); + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + capturePaymentService = new CapturePaymentService(client, connectorUriGenerator); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-capture-payment-with-delayed-capture-true-and-awaiting-capture-request-status"}) + public void testCapturePayment() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + + Response capturePaymentResponse = capturePaymentService.capture(account, CHARGE_ID); + + assertThat(capturePaymentResponse.getStatus(), is(204)); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CardPaymentSearchServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CardPaymentSearchServicePactTest.java new file mode 100644 index 000000000..10a98e252 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CardPaymentSearchServicePactTest.java @@ -0,0 +1,229 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import com.jayway.jsonassert.JsonAssert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.SearchPaymentsException; +import uk.gov.pay.api.ledger.service.LedgerUriGenerator; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.model.search.PaginationDecorator; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Response; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CardPaymentSearchServicePactTest { + + @Rule + public PactProviderRule ledgerRule = new PactProviderRule("ledger", this); + + @Mock + private PublicApiConfig configuration; + private PaymentSearchService paymentSearchService; + private static final String tokenLink = "a-token-link"; + + @Before + public void setUp() { + when(configuration.getLedgerUrl()).thenReturn(ledgerRule.getUrl()); + + when(configuration.getBaseUrl()).thenReturn("http://publicapi.test.localhost/"); + + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + LedgerUriGenerator ledgerUriGenerator = new LedgerUriGenerator(configuration); + paymentSearchService = new PaymentSearchService( + new PublicApiUriGenerator(configuration), + new PaginationDecorator(configuration), + new LedgerService(client, ledgerUriGenerator)); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-payment-by-first-digits-card-number"}) + public void searchShouldReturnAResponseWithOneTransaction_whenFilteringByFirstDigitsCardNumberFromLedger() { + Account account = new Account("123456", TokenPaymentType.CARD, tokenLink); + var searchParams = new PaymentSearchParams.Builder() + .withFirstDigitsCardNumber("424242") + .withPageNumber("1") + .withDisplaySize("500") + .build(); + Response response = paymentSearchService.searchLedgerPayments(account, searchParams); + JsonAssert.with(response.getEntity().toString()) + .assertThat("count", is(1)) + .assertThat("total", is(1)) + .assertThat("page", is(1)) + .assertThat("results", hasSize(equalTo(1))) + .assertThat("results[0]", hasKey("amount")) + .assertThat("results[0]", hasKey("state")) + .assertThat("results[0]", hasKey("reference")) + .assertThat("results[0]", hasKey("email")) + .assertThat("results[0].card_details.cardholder_name", is("J Doe")) + .assertThat("results[0].card_details", hasKey("first_digits_card_number")) + .assertThat("results[0].card_details", hasKey("last_digits_card_number")) + .assertThat("results[0].state", hasKey("status")) + .assertThat("results[0].state", hasKey("finished")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-payment-by-last-digits-card-number"}) + public void searchShouldReturnAResponseWithOneTransaction_whenFilteringByLastDigitsCardNumberFromLedger() { + Account account = new Account("123456", TokenPaymentType.CARD, tokenLink); + var searchParams = new PaymentSearchParams.Builder() + .withLastDigitsCardNumber("4242") + .withPageNumber("1") + .withDisplaySize("500") + .build(); + Response response = paymentSearchService.searchLedgerPayments(account, searchParams); + JsonAssert.with(response.getEntity().toString()) + .assertThat("count", is(1)) + .assertThat("total", is(1)) + .assertThat("page", is(1)) + .assertThat("results", hasSize(equalTo(1))) + .assertThat("results[0]", hasKey("amount")) + .assertThat("results[0]", hasKey("state")) + .assertThat("results[0]", hasKey("reference")) + .assertThat("results[0]", hasKey("email")) + .assertThat("results[0].card_details.cardholder_name", is("J Doe")) + .assertThat("results[0].card_details", hasKey("first_digits_card_number")) + .assertThat("results[0].card_details", hasKey("last_digits_card_number")) + .assertThat("results[0].state", hasKey("status")) + .assertThat("results[0].state", hasKey("finished")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-payment-with-charge-in-success-state"}) + public void searchShouldReturnAResponseWithOneTransaction_whenFilteringByStateFromLedger() { + Account account = new Account("123456", TokenPaymentType.CARD, tokenLink); + var searchParams = new PaymentSearchParams.Builder() + .withState("success") + .withPageNumber("1") + .withDisplaySize("500") + .build(); + Response response = paymentSearchService.searchLedgerPayments(account, searchParams); + JsonAssert.with(response.getEntity().toString()) + .assertThat("count", is(1)) + .assertThat("total", is(1)) + .assertThat("page", is(1)) + .assertThat("results", hasSize(equalTo(1))) + .assertThat("results[0]", hasKey("amount")) + .assertThat("results[0]", hasKey("state")) + .assertThat("results[0]", hasKey("reference")) + .assertThat("results[0]", hasKey("email")) + .assertThat("results[0].card_details.cardholder_name", is("J Doe")) + .assertThat("results[0].card_details", hasKey("first_digits_card_number")) + .assertThat("results[0].card_details", hasKey("last_digits_card_number")) + .assertThat("results[0].state", hasKey("status")) + .assertThat("results[0].state", hasKey("finished")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-payments"}) + public void ledgerSearchShouldReturnAResponseWithOneTransaction() { + Account account = new Account("123456", TokenPaymentType.CARD, tokenLink); + var searchParams = new PaymentSearchParams.Builder() + .withCardHolderName("j.doe@example.org") + .build(); + + Response response = paymentSearchService.searchLedgerPayments(account, searchParams); + JsonAssert.with(response.getEntity().toString()) + .assertThat("count", is(1)) + .assertThat("total", is(1)) + .assertThat("page", is(1)) + .assertThat("results", hasSize(equalTo(1))) + .assertThat("results[0]", hasKey("amount")) + .assertThat("results[0]", hasKey("state")) + .assertThat("results[0]", hasKey("reference")) + .assertThat("results[0]", hasKey("email")) + .assertThat("results[0].card_details.cardholder_name", is("j.doe@example.org")) + .assertThat("results[0].card_details", hasKey("first_digits_card_number")) + .assertThat("results[0].card_details", hasKey("last_digits_card_number")) + .assertThat("results[0].state", hasKey("status")) + .assertThat("results[0].state", hasKey("finished")) + .assertThat("results[0]", hasKey("authorisation_summary")) + .assertThat("results[0].authorisation_summary", hasKey("three_d_secure")) + .assertThat("results[0].authorisation_summary.three_d_secure.required", is(true)) + .assertThat("results[0].authorisation_mode", is("web")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-payments-page-not-found"}) + public void shouldReturn404WhenSearchingWithNonExistentPageNumber() { + Account account = new Account("123456", TokenPaymentType.CARD, tokenLink); + var searchParams = new PaymentSearchParams.Builder() + .withDisplaySize("500") + .withPageNumber("999") + .build(); + + SearchPaymentsException searchPaymentsException = assertThrows(SearchPaymentsException.class, + () -> paymentSearchService.searchLedgerPayments(account, searchParams)); + + assertThat(searchPaymentsException, hasProperty("errorStatus", is(404))); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = ("publicapi-ledger-search-payments-with-settled_dates")) + public void shouldReturnAPaymentWhenSearchedBySettledDates() { + String accountId = "123456"; + Account account = new Account(accountId, TokenPaymentType.CARD, tokenLink); + var searchParams = new PaymentSearchParams.Builder() + .withFromSettledDate("2020-09-19") + .withToSettledDate("2020-09-20") + .withDisplaySize("500") + .withPageNumber("1") + .build(); + Response response = paymentSearchService.searchLedgerPayments(account, searchParams); + JsonAssert.with(response.getEntity().toString()) + .assertThat("count", is(1)) + .assertThat("total", is(1)) + .assertThat("page", is(1)) + .assertThat("results", hasSize(equalTo(1))) + .assertThat("results[0].settlement_summary", hasKey("settled_date")) + .assertThat("results[0].settlement_summary.settled_date", is("2020-09-19")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = ("publicapi-ledger-search-payment-by-agreement-id")) + public void shouldReturnAPaymentWhenSearchedByAgreementId() { + String accountId = "123456"; + String agreementId = "agreement-1"; + Account account = new Account(accountId, TokenPaymentType.CARD, tokenLink); + var searchParams = new PaymentSearchParams.Builder() + .withAgreementId(agreementId) + .withDisplaySize("500") + .withPageNumber("1") + .build(); + Response response = paymentSearchService.searchLedgerPayments(account, searchParams); + JsonAssert.with(response.getEntity().toString()) + .assertThat("count", is(1)) + .assertThat("total", is(1)) + .assertThat("page", is(1)) + .assertThat("results", hasSize(equalTo(1))) + .assertThat("results[0].authorisation_mode", is(AuthorisationMode.AGREEMENT.getName())); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/ConnectorUriGeneratorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/ConnectorUriGeneratorTest.java new file mode 100644 index 000000000..b27aa3ed0 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/ConnectorUriGeneratorTest.java @@ -0,0 +1,74 @@ +package uk.gov.pay.api.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.auth.Account; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; +import static uk.gov.pay.api.model.TokenPaymentType.CARD; + +@ExtendWith(MockitoExtension.class) +public class ConnectorUriGeneratorTest { + @Mock + private PublicApiConfig mockPublicApiConfig; + + private ConnectorUriGenerator connectorUriGenerator; + + private final Account cardAccount = new Account("accountId", CARD, "a-token-link"); + + private final String chargeId = "charge_id_123"; + + @BeforeEach + public void setUp() { + connectorUriGenerator = new ConnectorUriGenerator(mockPublicApiConfig); + when(mockPublicApiConfig.getConnectorUrl()).thenReturn("https://bla.test"); + } + + @Test + public void shouldGenerateTheRightChargeURIForCardConnector() { + String uri = connectorUriGenerator.chargesURI(cardAccount); + assertThat(uri, is("https://bla.test/v1/api/accounts/accountId/charges")); + } + + @Test + public void shouldGenerateTheRightChargeURI() { + String uri = connectorUriGenerator.chargeURI(cardAccount, chargeId); + assertThat(uri, is("https://bla.test/v1/api/accounts/accountId/charges/" + chargeId)); + } + + @Test + public void shouldGenerateTheRightTelephoneChargeURI() { + String uri = connectorUriGenerator.telephoneChargesURI(cardAccount); + assertThat(uri, is("https://bla.test/v1/api/accounts/accountId/telephone-charges")); + } + + @Test + public void shouldGenerateTheRightCancelURI() { + String uri = connectorUriGenerator.cancelURI(cardAccount, chargeId); + assertThat(uri, is("https://bla.test/v1/api/accounts/accountId/charges/" + chargeId + "/cancel")); + } + + @Test + public void shouldGenerateTheRightCaptureURI() { + String uri = connectorUriGenerator.captureURI(cardAccount, chargeId); + assertThat(uri, is("https://bla.test/v1/api/accounts/" + cardAccount.getAccountId() + "/charges/" + chargeId + "/capture")); + } + + @Test + public void shouldGenerateTheRightEventsURI() { + String uri = connectorUriGenerator.chargeEventsURI(cardAccount, chargeId); + assertThat(uri, is("https://bla.test/v1/api/accounts/accountId/charges/" + chargeId + "/events")); + } + + @Test + public void shouldGenerateTheRightAuthoriseURI() { + String uri = connectorUriGenerator.authorisationURI(); + assertThat(uri, is("https://bla.test/v1/api/charges/authorise")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CreateAgreementLedgerServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CreateAgreementLedgerServicePactTest.java new file mode 100644 index 000000000..4828da9f7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CreateAgreementLedgerServicePactTest.java @@ -0,0 +1,86 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.agreement.model.AgreementCreatedResponse; +import uk.gov.pay.api.agreement.model.CreateAgreementRequest; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.CreateAgreementException; +import uk.gov.pay.api.model.CreateAgreementRequestBuilder; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.when; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.RECURRING_CARD_PAYMENTS_NOT_ALLOWED; + +@RunWith(MockitoJUnitRunner.class) +public class CreateAgreementLedgerServicePactTest { + + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + + @Mock + private PublicApiConfig configuration; + + private ConnectorService connectorService; + + private Account account; + + @Before + public void setup() { + when(configuration.getConnectorUrl()).thenReturn(connectorRule.getUrl()); + + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + + ConnectorUriGenerator connectorUriGenerator = new ConnectorUriGenerator(configuration); + connectorService = new ConnectorService(client, connectorUriGenerator); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-agreement"}) + public void shouldCreateAgreement() { + account = new Account("123456", TokenPaymentType.CARD, "a-token-link"); + CreateAgreementRequest agreementCreateRequest = new CreateAgreementRequest(CreateAgreementRequestBuilder + .builder() + .description("Description for the paying user describing the purpose of the agreement") + .userIdentifier("reference for the paying user") + .reference("Service agreement reference")); + + AgreementCreatedResponse agreementResponse = connectorService.createAgreement(account, agreementCreateRequest); + + assertThat(agreementResponse.getAgreementId(), is("1jikqomeib6j18vp2i153b9dtu")); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-agreement-for-non-rcp-account"}) + public void createAgreementForNonRCPAccountShouldReturnError() { + account = new Account("123456789", TokenPaymentType.CARD, "a-token-link"); + CreateAgreementRequest agreementCreateRequest = new CreateAgreementRequest(CreateAgreementRequestBuilder + .builder() + .description("Description for the paying user") + .userIdentifier("reference for the paying user") + .reference("Service agreement reference")); + + CreateAgreementException exception = assertThrows(CreateAgreementException.class, () -> { + connectorService.createAgreement(account, agreementCreateRequest); + }); + + assertThat(exception.getErrorIdentifier(), is(RECURRING_CARD_PAYMENTS_NOT_ALLOWED)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CreatePaymentServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CreatePaymentServicePactTest.java new file mode 100644 index 000000000..88662adf3 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CreatePaymentServicePactTest.java @@ -0,0 +1,415 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.CreateChargeException; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.CreateCardPaymentRequestBuilder; +import uk.gov.pay.api.model.CreatedPaymentWithAllLinks; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.model.links.Link; +import uk.gov.pay.api.model.links.PaymentWithAllLinks; +import uk.gov.pay.api.model.links.PostLink; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.ErrorIdentifier; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.model.charge.ExternalMetadata; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.CARD_NUMBER_IN_PAYMENT_LINK_REFERENCE_REJECTED; +import static uk.gov.service.payments.commons.model.Source.CARD_PAYMENT_LINK; + +@RunWith(MockitoJUnitRunner.class) +public class CreatePaymentServicePactTest { + + private CreatePaymentService createPaymentService; + + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + + @Mock + private PublicApiConfig configuration; + + private Account account; + + @Before + public void setup() { + when(configuration.getConnectorUrl()).thenReturn(connectorRule.getUrl()); // We will actually send real requests here, which will be intercepted by pact + when(configuration.getBaseUrl()).thenReturn("http://publicapi.test.localhost/"); + + PublicApiUriGenerator publicApiUriGenerator = new PublicApiUriGenerator(configuration); + ConnectorUriGenerator connectorUriGenerator = new ConnectorUriGenerator(configuration); + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + createPaymentService = new CreatePaymentService(client, publicApiUriGenerator, connectorUriGenerator); + account = new Account("123456", TokenPaymentType.CARD, "a-token-link"); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment-with-minimum-fields"}) + public void testCreatePaymentWithMinimumFields() { + Account account = new Account("123456", TokenPaymentType.CARD, "a-token-link"); + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(100) + .returnUrl("https://somewhere.gov.uk/rainbow/1") + .reference("a reference") + .description("a description") + .build(); + + CreatedPaymentWithAllLinks createdPaymentWithAllLinks = createPaymentService.create(account, requestPayload, null); + PaymentWithAllLinks paymentResponse = createdPaymentWithAllLinks.getPayment(); + + assertThat(paymentResponse.getPaymentId(), is("ch_ab2341da231434l")); + assertThat(paymentResponse.getAmount(), is(100L)); + assertThat(paymentResponse.getReference(), is("a reference")); + assertThat(paymentResponse.getDescription(), is("a description")); + assertThat(paymentResponse.getEmail(), is(Optional.empty())); + assertThat(paymentResponse.getState(), is(new PaymentState("created", false))); + assertThat(paymentResponse.getReturnUrl().get(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentResponse.getPaymentProvider(), is("Sandbox")); + assertThat(paymentResponse.getCreatedDate(), is("2016-01-01T12:00:00Z")); + assertThat(paymentResponse.getLanguage(), is(SupportedLanguage.ENGLISH)); + assertThat(paymentResponse.getDelayedCapture(), is(false)); + assertThat(paymentResponse.getMoto(), is(false)); + assertThat(paymentResponse.getLinks().getSelf(), is(new Link("http://publicapi.test.localhost/v1/payments/ch_ab2341da231434l", "GET"))); + assertThat(paymentResponse.getLinks().getNextUrl(), is(new Link("http://frontend_connector/charge/token_1234567asdf", "GET"))); + PostLink expectedLink = new PostLink("http://frontend_connector/charge/", "POST", "application/x-www-form-urlencoded", Collections.singletonMap("chargeTokenId", "token_1234567asdf")); + assertThat(paymentResponse.getLinks().getNextUrlPost(), is(expectedLink)); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment"}) + public void testCreatePayment() { + Map metadata = Map.of( + "ledger_code", 123, + "fund_code", "ISIN122038", + "cancellable", false); + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(100) + .returnUrl("https://somewhere.gov.uk/rainbow/1") + .reference("a reference") + .description("a description") + .metadata(new ExternalMetadata(metadata)) + .delayedCapture(Boolean.TRUE) + .moto(Boolean.TRUE) + .language(SupportedLanguage.WELSH) + .email("joe.bogs@example.org") + .cardholderName("J. Bogs") + .addressLine1("address line 1") + .addressLine2("address line 2") + .city("address city") + .postcode("AB1 CD2") + .country("GB") + .build(); + + CreatedPaymentWithAllLinks paymentWithAllLinks = createPaymentService.create(account, requestPayload, null); + PaymentWithAllLinks paymentResponse = paymentWithAllLinks.getPayment(); + + assertThat(paymentResponse.getPaymentId(), is("ch_ab2341da231434l")); + assertThat(paymentResponse.getAmount(), is(100L)); + assertThat(paymentResponse.getReference(), is("a reference")); + assertThat(paymentResponse.getDescription(), is("a description")); + assertThat(paymentResponse.getState(), is(new PaymentState("created", false))); + assertThat(paymentResponse.getReturnUrl().get(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentResponse.getPaymentProvider(), is("Sandbox")); + assertThat(paymentResponse.getCreatedDate(), is("2016-01-01T12:00:00Z")); + assertThat(paymentResponse.getLinks().getSelf(), is(new Link("http://publicapi.test.localhost/v1/payments/ch_ab2341da231434l", "GET"))); + assertThat(paymentResponse.getLinks().getNextUrl(), is(new Link("http://frontend_connector/charge/token_1234567asdf", "GET"))); + PostLink expectedLink = new PostLink("http://frontend_connector/charge/", "POST", "application/x-www-form-urlencoded", Collections.singletonMap("chargeTokenId", "token_1234567asdf")); + assertThat(paymentResponse.getLinks().getNextUrlPost(), is(expectedLink)); + assertThat(paymentResponse.getMetadata().getMetadata(), is(metadata)); + assertThat(paymentResponse.getDelayedCapture(), is(true)); + assertThat(paymentResponse.getLanguage(), is(SupportedLanguage.WELSH)); + assertThat(paymentResponse.getEmail().isPresent(), is(true)); + assertThat(paymentResponse.getEmail().get(), is("joe.bogs@example.org")); + assertThat(paymentResponse.getCardDetails().isPresent(), is(true)); + assertThat(paymentResponse.getCardDetails().get().getCardHolderName(), is("J. Bogs")); + assertThat(paymentResponse.getCardDetails().get().getBillingAddress().isPresent(), is(true)); + Address billingAddress = paymentResponse.getCardDetails().get().getBillingAddress().get(); + assertThat(billingAddress.getLine1(), is("address line 1")); + assertThat(billingAddress.getLine2(), is("address line 2")); + assertThat(billingAddress.getCity(), is("address city")); + assertThat(billingAddress.getPostcode(), is("AB1 CD2")); + assertThat(billingAddress.getCountry(), is("GB")); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment-for-disabled-account"}) + public void creating_payment_for_disabled_account_should_return_403() { + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(100) + .returnUrl("https://somewhere.gov.uk/rainbow/1") + .reference("a reference") + .description("a description") + .build(); + + try { + createPaymentService.create(account, requestPayload, null); + fail("Expected CreateChargeException to be thrown"); + } catch (CreateChargeException e) { + assertThat(e.getErrorIdentifier(), is(ErrorIdentifier.ACCOUNT_DISABLED)); + } + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment-with-authorisation-mode-moto-api"}) + public void testCreatePaymentWithAuthorisationModeMotoApi() { + Account account = new Account("123456", TokenPaymentType.CARD, "a-token-link"); + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(100) + .reference("a reference") + .description("a description") + .authorisationMode(AuthorisationMode.MOTO_API) + .build(); + + CreatedPaymentWithAllLinks paymentWithAllLinks = createPaymentService.create(account, requestPayload, null); + PaymentWithAllLinks paymentResponse = paymentWithAllLinks.getPayment(); + assertThat(paymentResponse.getLinks().getAuthUrlPost(), is(new PostLink("http://publicapi.test.localhost/v1/auth", "POST", "application/json", Collections.singletonMap("one_time_token", "token_1234567asdf")))); + assertThat(paymentResponse.getPaymentId(), is("ch_123abc456def")); + assertThat(paymentResponse.getAuthorisationMode(), is(AuthorisationMode.MOTO_API)); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment-with-authorisation-mode-moto-api-not-allowed"}) + public void testCreatePaymentWithAuthorisationModeMotoApi_whenNotAllowedForAccount_shouldReturn422() { + Account account = new Account("667", TokenPaymentType.CARD, "a-token-link"); + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(100) + .reference("a reference") + .description("a description") + .authorisationMode(AuthorisationMode.MOTO_API) + .build(); + try { + createPaymentService.create(account, requestPayload, null); + fail("Expected CreateChargeException to be thrown"); + } catch (CreateChargeException e) { + assertThat(e.getErrorIdentifier(), is(ErrorIdentifier.AUTHORISATION_API_NOT_ALLOWED)); + } + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment-with-zero-amount-not-allowed"}) + public void creating_payment_with_zero_amount_when_not_allowed_for_account_should_return_422() { + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(0) + .returnUrl("https://somewhere.gov.uk/rainbow/1") + .reference("a reference") + .description("a description") + .build(); + + try { + createPaymentService.create(account, requestPayload, null); + fail("Expected CreateChargeException to be thrown"); + } catch (CreateChargeException e) { + assertThat(e.getErrorIdentifier(), is(ErrorIdentifier.ZERO_AMOUNT_NOT_ALLOWED)); + } + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment-with-moto-not-allowed"}) + public void creating_payment_with_moto_when_not_allowed_for_account_should_return_422() { + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(100) + .returnUrl("https://somewhere.gov.uk/rainbow/1") + .reference("a reference") + .description("a description") + .moto(true) + .build(); + + try { + createPaymentService.create(account, requestPayload, null); + fail("Expected CreateChargeException to be thrown"); + } catch (CreateChargeException e) { + assertThat(e.getErrorIdentifier(), is(ErrorIdentifier.MOTO_NOT_ALLOWED)); + } + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment-with-credentials-in-created-not-allowed"}) + public void shouldThrowExceptionWithIdentifierAccountNotLinkedToPSP_IfGatewayAccountCredentialIsNotConfiguredInConnector() { + Account account = new Account("444", TokenPaymentType.CARD, "a-token-link"); + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(100) + .returnUrl("https://somewhere.gov.uk/rainbow/1") + .reference("a reference") + .description("a description") + .build(); + + try { + createPaymentService.create(account, requestPayload, null); + fail("Expected CreateChargeException to be thrown"); + } catch (CreateChargeException e) { + assertThat(e.getErrorIdentifier(), is(ErrorIdentifier.ACCOUNT_NOT_LINKED_WITH_PSP)); + } + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment-with-invalid-return-url"}) + public void shouldThrowExceptionWithIdentifierInvalidAttributeValue_whenReturnUrlInvalid() { + Account account = new Account("444", TokenPaymentType.CARD, "a-token-link"); + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(100) + .returnUrl("invalid") + .reference("a reference") + .description("a description") + .build(); + + try { + createPaymentService.create(account, requestPayload, null); + fail("Expected CreateChargeException to be thrown"); + } catch (CreateChargeException e) { + assertThat(e.getErrorIdentifier(), is(ErrorIdentifier.INVALID_ATTRIBUTE_VALUE)); + assertThat(e.getConnectorErrorMessage(), is("Invalid attribute value: return_url. Must be a valid URL format")); + } + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment-with-missing-return-url"}) + public void shouldThrowExceptionWithIdentifierMissingMandatoryAttribute_whenReturnUrlMissing() { + Account account = new Account("444", TokenPaymentType.CARD, "a-token-link"); + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(100) + .reference("a reference") + .description("a description") + .build(); + + try { + createPaymentService.create(account, requestPayload, null); + fail("Expected CreateChargeException to be thrown"); + } catch (CreateChargeException e) { + assertThat(e.getErrorIdentifier(), is(ErrorIdentifier.MISSING_MANDATORY_ATTRIBUTE)); + assertThat(e.getConnectorErrorMessage(), is("Missing mandatory attribute: return_url")); + } + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment-link-payment-with-card-number-in-reference"}) + public void shouldThrowExceptionWhenCardNumberIsEnteredInAReferenceForPaymentLinkPayment() { + Account account = new Account("444", TokenPaymentType.CARD, "a-token-link"); + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(100) + .reference("4242 4242 4242 4242") + .description("a description") + .returnUrl("https://gov.uk") + .source(CARD_PAYMENT_LINK) + .build(); + + var exception = assertThrows(CreateChargeException.class, () -> { + createPaymentService.create(account, requestPayload, null); + }); + + assertThat(exception.getErrorIdentifier(), is(CARD_NUMBER_IN_PAYMENT_LINK_REFERENCE_REJECTED)); + assertThat(exception.getConnectorErrorMessage(), is("Card number entered in a payment link reference")); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment-with-idempotency-key-200-response"}) + public void testCreatePaymentWithMatchingBodyAndSameIdempotencyKeyAsExistingPayment() { + var idempotencyKey = "Ida the idempotency key"; + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(2046) + .reference("referential") + .description("describable") + .agreementId("abcdefghijklmnopqrstuvwxyz") + .authorisationMode(AuthorisationMode.AGREEMENT) + .build(); + + CreatedPaymentWithAllLinks paymentResponse = createPaymentService.create(account, requestPayload, idempotencyKey); + PaymentWithAllLinks paymentWithAllLinks = paymentResponse.getPayment(); + + assertThat(paymentWithAllLinks.getPaymentId(), is("chargeable")); + assertThat(paymentWithAllLinks.getAmount(), is(2046L)); + assertThat(paymentWithAllLinks.getReference(), is("referential")); + assertThat(paymentWithAllLinks.getDescription(), is("describable")); + assertThat(paymentWithAllLinks.getAgreementId(), is("abcdefghijklmnopqrstuvwxyz")); + assertThat(paymentWithAllLinks.getAuthorisationMode(), is(AuthorisationMode.AGREEMENT)); + assertThat(paymentWithAllLinks.getState(), is(new PaymentState("created", false))); + assertThat(paymentWithAllLinks.getPaymentProvider(), is("sandbox")); + assertThat(paymentWithAllLinks.getCreatedDate(), is("2023-04-20T13:30:00.000Z")); + assertThat(paymentWithAllLinks.getLinks().getSelf(), is(new Link("http://publicapi.test.localhost/v1/payments/chargeable", "GET"))); + assertThat(paymentWithAllLinks.getLinks().getRefunds().getHref(), containsString("v1/payments/chargeable/refunds")); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment-with-idempotency-key-409-response"}) + public void testCreatePaymentWithDifferentBodyAndSameIdempotencyKeyAsExistingPayment() { + var idempotencyKey = "Ida the idempotency key"; + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(2046) + .reference("different referential") + .description("describable") + .agreementId("abcdefghijklmnopqrstuvwxyz") + .authorisationMode(AuthorisationMode.AGREEMENT) + .build(); + + CreateChargeException cr = assertThrows(CreateChargeException.class, + () -> createPaymentService.create(account, requestPayload, idempotencyKey)); + + assertThat(cr.getErrorStatus(), is(409)); + assertThat(cr.getErrorIdentifier(), is(ErrorIdentifier.IDEMPOTENCY_KEY_USED)); + assertThat(cr.getConnectorErrorMessage(), is("The Idempotency-Key has already been used to create a payment")); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = "publicapi-connector-take-a-recurring-payment") + public void testTakeARecurringPayment() { + var requestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(2046) + .reference("a-reference") + .description("a description") + .agreementId("abcdefghijklmnopqrstuvwxyz") + .authorisationMode(AuthorisationMode.AGREEMENT) + .build(); + + CreatedPaymentWithAllLinks paymentResponse = createPaymentService.create(account, requestPayload, null); + PaymentWithAllLinks paymentWithAllLinks = paymentResponse.getPayment(); + + assertThat(paymentWithAllLinks.getPaymentId(), is("valid-charge-id")); + assertThat(paymentWithAllLinks.getAmount(), is(2046L)); + assertThat(paymentWithAllLinks.getReference(), is("a-reference")); + assertThat(paymentWithAllLinks.getDescription(), is("a description")); + assertThat(paymentWithAllLinks.getAgreementId(), is("abcdefghijklmnopqrstuvwxyz")); + assertThat(paymentWithAllLinks.getAuthorisationMode(), is(AuthorisationMode.AGREEMENT)); + assertThat(paymentWithAllLinks.getState(), is(new PaymentState("created", false))); + assertThat(paymentWithAllLinks.getPaymentProvider(), is("sandbox")); + assertThat(paymentWithAllLinks.getCreatedDate(), is(notNullValue())); + assertThat(paymentWithAllLinks.getLinks().getSelf(), is(new Link("http://publicapi.test.localhost/v1/payments/valid-charge-id", "GET"))); + assertThat(paymentWithAllLinks.getLinks().getRefunds().getHref(), containsString("v1/payments/valid-charge-id/refunds")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CreateRefundServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CreateRefundServicePactTest.java new file mode 100644 index 000000000..c9333de86 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/CreateRefundServicePactTest.java @@ -0,0 +1,68 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.CreateRefundException; +import uk.gov.pay.api.model.CreatePaymentRefundRequest; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.service.payments.commons.model.ErrorIdentifier; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CreateRefundServicePactTest { + + private CreateRefundService createRefundService; + + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + + @Mock + private PublicApiConfig configuration; + + @Mock + private GetPaymentService getPaymentService; + + private Account account; + + @Before + public void setup() { + when(configuration.getConnectorUrl()).thenReturn(connectorRule.getUrl()); // We will actually send real requests here, which will be intercepted by pact + when(configuration.getBaseUrl()).thenReturn("http://publicapi.test.localhost/"); + + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + + createRefundService = new CreateRefundService(getPaymentService, client, configuration); + account = new Account("123456", TokenPaymentType.CARD, "a-token-link"); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-refund-for-disabled-account"}) + public void creating_refund_for_disabled_account_should_return_403() { + var requestPayload = new CreatePaymentRefundRequest(100, 100); + + try { + createRefundService.createRefund(account,"654321" , requestPayload); + fail("Expected CreateRefundException to be thrown"); + } catch (CreateRefundException e) { + assertThat(e.getErrorIdentifier(), is(ErrorIdentifier.ACCOUNT_DISABLED)); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetOneAgreementLedgerServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetOneAgreementLedgerServicePactTest.java new file mode 100644 index 000000000..bdbe24e9d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetOneAgreementLedgerServicePactTest.java @@ -0,0 +1,68 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.agreement.model.AgreementLedgerResponse; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.GetAgreementException; +import uk.gov.pay.api.ledger.service.LedgerUriGenerator; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GetOneAgreementLedgerServicePactTest { + + private final Account ACCOUNT = new Account("3456", TokenPaymentType.CARD, "a-token-link"); + + @Rule + public PactProviderRule ledgerRule = new PactProviderRule("ledger", this); + + private LedgerService ledgerService; + @Mock + private PublicApiConfig mockConfiguration; + + @Before + public void setUp() { + when(mockConfiguration.getLedgerUrl()).thenReturn(ledgerRule.getUrl()); + LedgerUriGenerator ledgerUriGenerator = new LedgerUriGenerator(mockConfiguration); + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + ledgerService = new LedgerService(client, ledgerUriGenerator); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-get-one-agreement"}) + public void getOneAgreement() { + AgreementLedgerResponse getAgreementResponse = ledgerService.getAgreement(ACCOUNT, "abcdefghijklmnopqrstuvwxyz"); + assertThat(getAgreementResponse.getStatus(), is("ACTIVE")); + assertThat(getAgreementResponse.getExternalId(), is("abcdefghijklmnopqrstuvwxyz")); + assertThat(getAgreementResponse.getPaymentInstrument().getAgreementExternalId(), is("abcdefghijklmnopqrstuvwxyz")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-get-one-agreement-not-found"}) + public void getOneAgreementNotFound() { + GetAgreementException getAgreementException = assertThrows(GetAgreementException.class, + () -> ledgerService.getAgreement(ACCOUNT, "non-existent-agreement-id")); + assertThat(getAgreementException, hasProperty("errorStatus", is(404))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentEventsServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentEventsServicePactTest.java new file mode 100644 index 000000000..f1dfd709b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentEventsServicePactTest.java @@ -0,0 +1,102 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.ledger.service.LedgerUriGenerator; +import uk.gov.pay.api.model.PaymentEventsResponse; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GetPaymentEventsServicePactTest { + + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + + @Rule + public PactProviderRule ledgerRule = new PactProviderRule("ledger", this); + + @Mock + private PublicApiConfig mockConfiguration; + + private GetPaymentEventsService getPaymentEventsService; + private static final String ACCOUNT_ID = "42"; + + @Before + public void setUp() { + when(mockConfiguration.getConnectorUrl()).thenReturn(connectorRule.getUrl()); + when(mockConfiguration.getLedgerUrl()).thenReturn(ledgerRule.getUrl()); + when(mockConfiguration.getBaseUrl()).thenReturn("http://publicapi.test.localhost/"); + + PublicApiUriGenerator publicApiUriGenerator = new PublicApiUriGenerator(mockConfiguration); + ConnectorUriGenerator connectorUriGenerator = new ConnectorUriGenerator(mockConfiguration); + LedgerUriGenerator ledgerUriGenerator = new LedgerUriGenerator(mockConfiguration); + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + LedgerService ledgerService = new LedgerService(client, ledgerUriGenerator); + ConnectorService connectorService = new ConnectorService(client, connectorUriGenerator); + + getPaymentEventsService = new GetPaymentEventsService(publicApiUriGenerator, connectorService, ledgerService); + } + + @Test + @PactVerification("connector") + @Pacts(pacts = {"publicapi-connector-get-payment-events"}) + public void shouldReturnPaymentEventsWhenCallingConnector() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + PaymentEventsResponse paymentEventsResponse = getPaymentEventsService.getPaymentEventsFromConnector(account, "abc123"); + assertThat(paymentEventsResponse.getPaymentId(), is("abc123")); + assertThat(paymentEventsResponse.getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/payments/abc123/events")); + assertThat(paymentEventsResponse.getEvents().size(), is(2)); + assertThat(paymentEventsResponse.getEvents().get(0).getPaymentId(), is("abc123")); + assertThat(paymentEventsResponse.getEvents().get(0).getPaymentLink().getPaymentLink().getHref(), is("http://publicapi.test.localhost/v1/payments/abc123")); + assertThat(paymentEventsResponse.getEvents().get(0).getState().getStatus(), is("created")); + assertThat(paymentEventsResponse.getEvents().get(0).getState().isFinished(), is(false)); + assertThat(paymentEventsResponse.getEvents().get(0).getUpdated(), is("2019-08-06T10:34:43.487Z")); + assertThat(paymentEventsResponse.getEvents().get(1).getPaymentId(), is("abc123")); + assertThat(paymentEventsResponse.getEvents().get(1).getPaymentLink().getPaymentLink().getHref(), is("http://publicapi.test.localhost/v1/payments/abc123")); + assertThat(paymentEventsResponse.getEvents().get(1).getState().getStatus(), is("failed")); + assertThat(paymentEventsResponse.getEvents().get(1).getState().isFinished(), is(true)); + assertThat(paymentEventsResponse.getEvents().get(1).getState().getCode(), is("P0010")); + assertThat(paymentEventsResponse.getEvents().get(1).getState().getMessage(), is("Payment method rejected")); + assertThat(paymentEventsResponse.getEvents().get(1).getUpdated(), is("2019-08-06T10:34:48.487Z")); + } + + @Test + @PactVerification("ledger") + @Pacts(pacts = {"publicapi-ledger-get-payment-events"}) + public void shouldReturnPaymentEventsWhenCallingLedger() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + PaymentEventsResponse paymentEventsResponse = getPaymentEventsService.getPaymentEventsFromLedger(account, "abc123"); + assertThat(paymentEventsResponse.getPaymentId(), is("abc123")); + assertThat(paymentEventsResponse.getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/payments/abc123/events")); + assertThat(paymentEventsResponse.getEvents().size(), is(2)); + assertThat(paymentEventsResponse.getEvents().get(0).getPaymentId(), is("abc123")); + assertThat(paymentEventsResponse.getEvents().get(0).getPaymentLink().getPaymentLink().getHref(), is("http://publicapi.test.localhost/v1/payments/abc123")); + assertThat(paymentEventsResponse.getEvents().get(0).getState().getStatus(), is("created")); + assertThat(paymentEventsResponse.getEvents().get(0).getState().isFinished(), is(false)); + assertThat(paymentEventsResponse.getEvents().get(0).getUpdated(), is("2019-08-06T10:34:43.487123Z")); + assertThat(paymentEventsResponse.getEvents().get(1).getPaymentId(), is("abc123")); + assertThat(paymentEventsResponse.getEvents().get(1).getPaymentLink().getPaymentLink().getHref(), is("http://publicapi.test.localhost/v1/payments/abc123")); + assertThat(paymentEventsResponse.getEvents().get(1).getState().getStatus(), is("failed")); + assertThat(paymentEventsResponse.getEvents().get(1).getState().isFinished(), is(true)); + assertThat(paymentEventsResponse.getEvents().get(1).getState().getCode(), is("P0010")); + assertThat(paymentEventsResponse.getEvents().get(1).getState().getMessage(), is("Payment method rejected")); + assertThat(paymentEventsResponse.getEvents().get(1).getUpdated(), is("2019-08-06T10:34:48.123456Z")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentRefundServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentRefundServicePactTest.java new file mode 100644 index 000000000..dec348e65 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentRefundServicePactTest.java @@ -0,0 +1,89 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.ledger.service.LedgerUriGenerator; +import uk.gov.pay.api.model.RefundResponse; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GetPaymentRefundServicePactTest { + + private static final String ACCOUNT_ID = "123456"; + private static final String CHARGE_ID = "123456789"; + private static final String REFUND_ID = "r_123abc456def"; + + private GetPaymentRefundService getPaymentService; + + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + + @Rule + public PactProviderRule ledgerRule = new PactProviderRule("ledger", this); + + @Mock + private PublicApiConfig mockConfiguration; + + @Before + public void setup() { + when(mockConfiguration.getConnectorUrl()).thenReturn(connectorRule.getUrl()); + when(mockConfiguration.getLedgerUrl()).thenReturn(ledgerRule.getUrl()); + when(mockConfiguration.getBaseUrl()).thenReturn("http://publicapi.test.localhost/"); + + PublicApiUriGenerator publicApiUriGenerator = new PublicApiUriGenerator(mockConfiguration); + ConnectorUriGenerator connectorUriGenerator = new ConnectorUriGenerator(mockConfiguration); + LedgerUriGenerator ledgerUriGenerator = new LedgerUriGenerator(mockConfiguration); + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + getPaymentService = new GetPaymentRefundService(new ConnectorService(client, connectorUriGenerator), + new LedgerService(client, ledgerUriGenerator), publicApiUriGenerator); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-get-payment-refund"}) + public void testGetPayment() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + + RefundResponse refund = getPaymentService.getConnectorPaymentRefund(account, CHARGE_ID, REFUND_ID); + + assertThat(refund.getRefundId(), is(REFUND_ID)); + assertThat(refund.getStatus(), is("success")); + assertThat(refund.getAmount(), is(100L)); + assertThat(refund.getCreatedDate(), is("2018-09-22T10:14:16.067Z")); + assertThat(refund.getLinks().getPayment().getHref(), is("http://publicapi.test.localhost/v1/payments/123456789")); + assertThat(refund.getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/payments/123456789/refunds/r_123abc456def")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-get-payment-refund"}) + public void testGetLedgerPaymentRefund() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + + RefundResponse refund = getPaymentService.getLedgerPaymentRefund(account, CHARGE_ID, REFUND_ID); + + assertThat(refund.getRefundId(), is(REFUND_ID)); + assertThat(refund.getStatus(), is("success")); + assertThat(refund.getAmount(), is(100L)); + assertThat(refund.getCreatedDate(), is("2018-09-22T10:14:16.067Z")); + assertThat(refund.getLinks().getPayment().getHref(), is("http://publicapi.test.localhost/v1/payments/123456789")); + assertThat(refund.getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/payments/123456789/refunds/r_123abc456def")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentRefundsServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentRefundsServicePactTest.java new file mode 100644 index 000000000..2e5a3b2f3 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentRefundsServicePactTest.java @@ -0,0 +1,113 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.ledger.service.LedgerUriGenerator; +import uk.gov.pay.api.model.RefundResponse; +import uk.gov.pay.api.model.RefundsResponse; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GetPaymentRefundsServicePactTest { + + private static final String ACCOUNT_ID = "123456"; + @Rule + public PactProviderRule ledgerRule = new PactProviderRule("ledger", this); + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + @Mock + private PublicApiConfig mockConfiguration; + private GetPaymentRefundsService getPaymentRefundsService; + + @Before + public void setUp() { + when(mockConfiguration.getLedgerUrl()).thenReturn(ledgerRule.getUrl()); + when(mockConfiguration.getConnectorUrl()).thenReturn(connectorRule.getUrl()); + when(mockConfiguration.getBaseUrl()).thenReturn("http://publicapi.test.localhost/"); + + PublicApiUriGenerator publicApiUriGenerator = new PublicApiUriGenerator(mockConfiguration); + LedgerUriGenerator ledgerUriGenerator = new LedgerUriGenerator(mockConfiguration); + + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + LedgerService ledgerService = new LedgerService(client, ledgerUriGenerator); + + ConnectorUriGenerator connectorUriGenerator = new ConnectorUriGenerator(mockConfiguration); + ConnectorService connectorService = new ConnectorService(client, connectorUriGenerator); + + getPaymentRefundsService = new GetPaymentRefundsService(connectorService, ledgerService, publicApiUriGenerator); + } + + @Test + @PactVerification("ledger") + @Pacts(pacts = {"publicapi-ledger-get-payment-refunds"}) + public void shouldReturnRefundsForPaymentCorrectlyFromLedger() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + RefundsResponse response = getPaymentRefundsService.getLedgerTransactionTransactions(account, "ch_123abc456xyz"); + assertThat(response.getPaymentId(), is("ch_123abc456xyz")); + assertThat(response.getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/payments/ch_123abc456xyz/refunds")); + assertThat(response.getLinks().getPayment().getHref(), is("http://publicapi.test.localhost/v1/payments/ch_123abc456xyz")); + + List refunds = response.getEmbedded().getRefunds(); + assertThat(refunds.size(), is(2)); + + assertThat(refunds.get(0).getRefundId(), is("refund-transaction-id1")); + assertThat(refunds.get(0).getStatus(), is("submitted")); + assertThat(refunds.get(0).getAmount(), is(100L)); + assertThat(refunds.get(0).getCreatedDate(), is("2018-09-22T10:14:16.067Z")); + assertThat(refunds.get(0).getLinks().getPayment().getHref(), is("http://publicapi.test.localhost/v1/payments/ch_123abc456xyz")); + assertThat(refunds.get(0).getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/payments/ch_123abc456xyz/refunds/refund-transaction-id1")); + + assertThat(refunds.get(1).getRefundId(), is("refund-transaction-id2")); + assertThat(refunds.get(1).getStatus(), is("error")); + assertThat(refunds.get(1).getAmount(), is(200L)); + assertThat(refunds.get(1).getCreatedDate(), is("2018-09-22T10:16:16.067Z")); + assertThat(refunds.get(1).getLinks().getPayment().getHref(), is("http://publicapi.test.localhost/v1/payments/ch_123abc456xyz")); + assertThat(refunds.get(1).getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/payments/ch_123abc456xyz/refunds/refund-transaction-id2")); + } + + @Test + @PactVerification("connector") + @Pacts(pacts = {"publicapi-connector-get-payment-refunds"}) + public void shouldReturnRefundsForPaymentCorrectlyFromConnector() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + RefundsResponse response = getPaymentRefundsService.getConnectorPaymentRefunds(account, "charge8133029783750222"); + assertThat(response.getPaymentId(), is("charge8133029783750222")); + assertThat(response.getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/payments/charge8133029783750222/refunds")); + assertThat(response.getLinks().getPayment().getHref(), is("http://publicapi.test.localhost/v1/payments/charge8133029783750222")); + + List refunds = response.getEmbedded().getRefunds(); + assertThat(refunds.size(), is(2)); + + assertThat(refunds.get(0).getRefundId(), is("di0qnu9ucdo7aslhatci6h90jk")); + assertThat(refunds.get(0).getStatus(), is("success")); + assertThat(refunds.get(0).getAmount(), is(1L)); + assertThat(refunds.get(0).getCreatedDate(), is("2016-01-25T13:23:55.000Z")); + assertThat(refunds.get(0).getLinks().getPayment().getHref(), is("http://publicapi.test.localhost/v1/payments/charge8133029783750222")); + assertThat(refunds.get(0).getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/payments/charge8133029783750222/refunds/di0qnu9ucdo7aslhatci6h90jk")); + + assertThat(refunds.get(1).getRefundId(), is("m16ufgc3t23l766ljhv9eicsn5")); + assertThat(refunds.get(1).getStatus(), is("error")); + assertThat(refunds.get(1).getAmount(), is(1L)); + assertThat(refunds.get(1).getCreatedDate(), is("2016-01-25T16:23:55.000Z")); + assertThat(refunds.get(1).getLinks().getPayment().getHref(), is("http://publicapi.test.localhost/v1/payments/charge8133029783750222")); + assertThat(refunds.get(1).getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/payments/charge8133029783750222/refunds/m16ufgc3t23l766ljhv9eicsn5")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentServiceLedgerPactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentServiceLedgerPactTest.java new file mode 100644 index 000000000..26d937839 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentServiceLedgerPactTest.java @@ -0,0 +1,201 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.ledger.service.LedgerUriGenerator; +import uk.gov.pay.api.model.CardPayment; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.model.Wallet; +import uk.gov.pay.api.model.links.PaymentWithAllLinks; +import uk.gov.pay.api.utils.mocks.ConnectorMockClient; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; +import java.util.Optional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; +import static uk.gov.service.payments.commons.testing.port.PortFactory.findFreePort; + +@RunWith(MockitoJUnitRunner.class) +public class GetPaymentServiceLedgerPactTest { + + private static final String ACCOUNT_ID = "123456"; + private static final String tokenLink = "a-token-link"; + private static final String CHARGE_ID_NON_EXISTENT_IN_CONNECTOR = "ch_123abc456xyz"; + private static final int CONNECTOR_PORT = findFreePort(); + + private GetPaymentService getPaymentService; + + @ClassRule + public static WireMockClassRule connectorRule = new WireMockClassRule(CONNECTOR_PORT); + + @Rule + public PactProviderRule ledgerRule = new PactProviderRule("ledger", this); + + @Mock + private PublicApiConfig mockConfiguration; + + @Before + public void setup() { + connectorRule.resetAll(); + when(mockConfiguration.getConnectorUrl()).thenReturn("http://localhost:" + CONNECTOR_PORT); + when(mockConfiguration.getLedgerUrl()).thenReturn(ledgerRule.getUrl()); + when(mockConfiguration.getBaseUrl()).thenReturn("http://publicapi.test.localhost/"); + + ConnectorMockClient connectorMockClient = new ConnectorMockClient(connectorRule); + connectorMockClient.respondChargeNotFound(ACCOUNT_ID, CHARGE_ID_NON_EXISTENT_IN_CONNECTOR, "not found"); + + PublicApiUriGenerator publicApiUriGenerator = new PublicApiUriGenerator(mockConfiguration); + ConnectorUriGenerator connectorUriGenerator = new ConnectorUriGenerator(mockConfiguration); + LedgerUriGenerator ledgerUriGenerator = new LedgerUriGenerator(mockConfiguration); + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + getPaymentService = new GetPaymentService(publicApiUriGenerator, + new ConnectorService(client, connectorUriGenerator), + new LedgerService(client, ledgerUriGenerator)); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-get-payment-with-metadata"}) + public void testGetPaymentWithMetadataFromLedger() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, tokenLink); + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID_NON_EXISTENT_IN_CONNECTOR); + assertThat(paymentResponse.getMetadata(), is(notNullValue())); + assertThat(paymentResponse.getMetadata().getMetadata().isEmpty(), is(false)); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-get-payment-with-gateway-transaction-id"}) + public void providerIdIsAvailableWhenPaymentIsSubmitted_Ledger() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, tokenLink); + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID_NON_EXISTENT_IN_CONNECTOR); + assertThat(paymentResponse.getProviderId(), is("gateway-tx-123456")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-get-payment-with-delayed-capture-true"}) + public void testGetPaymentFromLedger() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, tokenLink); + + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID_NON_EXISTENT_IN_CONNECTOR); + + assertThat(paymentResponse.getAmount(), is(100L)); + assertThat(paymentResponse.getState(), is(new PaymentState("created", false))); + assertThat(paymentResponse.getDescription(), is("Test description")); + assertThat(paymentResponse.getReference(), is("aReference")); + assertThat(paymentResponse.getLanguage(), is(SupportedLanguage.ENGLISH)); + assertThat(paymentResponse.getPaymentId(), is(CHARGE_ID_NON_EXISTENT_IN_CONNECTOR)); + assertThat(paymentResponse.getReturnUrl().get(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentResponse.getPaymentProvider(), is("sandbox")); + assertThat(paymentResponse.getCreatedDate(), is("2018-09-07T13:12:02.121Z")); + assertThat(paymentResponse.getDelayedCapture(), is(true)); + assertThat(paymentResponse.getCorporateCardSurcharge(), is(Optional.empty())); + assertThat(paymentResponse.getTotalAmount(), is(Optional.empty())); + assertThat(paymentResponse.getAuthorisationMode(), is(AuthorisationMode.WEB)); + assertThat(paymentResponse.getLinks().getSelf().getHref(), containsString("v1/payments/" + CHARGE_ID_NON_EXISTENT_IN_CONNECTOR)); + assertThat(paymentResponse.getLinks().getSelf().getMethod(), is("GET")); + assertThat(paymentResponse.getLinks().getRefunds().getHref(), containsString("v1/payments/" + CHARGE_ID_NON_EXISTENT_IN_CONNECTOR + "/refunds")); + assertThat(paymentResponse.getLinks().getRefunds().getMethod(), is("GET")); + assertThat(paymentResponse.getLinks().getNextUrl(), is(nullValue())); + assertThat(paymentResponse.getLinks().getNextUrlPost(), is(nullValue())); + assertThat(paymentResponse.getLinks().getCapture(), is(nullValue())); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-get-wallet-payment"}) + public void testGetPaymentFromLedgerWithWalletType() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, tokenLink); + + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID_NON_EXISTENT_IN_CONNECTOR); + + assertThat(paymentResponse.getAmount(), is(100L)); + assertThat(paymentResponse.getState(), is(new PaymentState("capturable", false))); + assertThat(paymentResponse.getPaymentId(), is(CHARGE_ID_NON_EXISTENT_IN_CONNECTOR)); + assertThat(paymentResponse.getPaymentProvider(), is("sandbox")); + assertThat(paymentResponse.getAuthorisationMode(), is(AuthorisationMode.WEB)); + assertThat(paymentResponse.getCardDetails().get().getCardHolderName(), is("J Doe")); + assertThat(paymentResponse.getCardDetails().get().getWalletType().get(), is(Wallet.APPLE_PAY.getTitleCase())); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-get-payment-with-corporate-surcharge"}) + public void testGetPaymentWithCorporateCardSurchargeFromLedger() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, tokenLink); + + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID_NON_EXISTENT_IN_CONNECTOR); + assertThat(paymentResponse.getCorporateCardSurcharge().get(), is(250L)); + assertThat(paymentResponse.getTotalAmount().get(), is(2250L)); + assertThat(paymentResponse.getLinks().getCapture(), is(nullValue())); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-get-payment-with-fee-and-net-amount"}) + public void testGetPaymentWithFeeAndNetAmountFromLedger() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, tokenLink); + + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID_NON_EXISTENT_IN_CONNECTOR); + + assertThat(paymentResponse.getPaymentId(), is(CHARGE_ID_NON_EXISTENT_IN_CONNECTOR)); + assertThat(paymentResponse.getPaymentProvider(), is("sandbox")); + assertThat(paymentResponse.getAmount(), is(100L)); + assertThat(paymentResponse.getFee().get(), is(5L)); + assertThat(paymentResponse.getNetAmount().get(), is(95L)); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-get-payment-with-settled-date"}) + public void testGetPaymentWithSettledDate() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, tokenLink); + + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, "ch_123abc456settlement"); + assertThat(paymentResponse.getSettlementSummary().isPresent(), is(true)); + assertThat(paymentResponse.getSettlementSummary().get().getSettledDate(), is("2020-09-19")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-get-payment-with-authorisation-summary"}) + public void testGetPaymentWithAuthorisationSummaryFromLedger() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, tokenLink); + + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID_NON_EXISTENT_IN_CONNECTOR); + assertThat(paymentResponse.getAuthorisationSummary().getThreeDSecure().isRequired(), is(true)); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-get-rejected-recurring-payment-with-can-retry-true"}) + public void testGetRejectedPaymentWithNoRetryTrueFromLedger() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, tokenLink); + + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID_NON_EXISTENT_IN_CONNECTOR); + assertThat(paymentResponse.getState().getCanRetry(), is(true)); + assertThat(paymentResponse.getAuthorisationMode(), is(AuthorisationMode.AGREEMENT)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentServicePactTest.java new file mode 100644 index 000000000..d04585b7c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/GetPaymentServicePactTest.java @@ -0,0 +1,187 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.ledger.service.LedgerUriGenerator; +import uk.gov.pay.api.model.CardPayment; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.model.Wallet; +import uk.gov.pay.api.model.links.PaymentWithAllLinks; +import uk.gov.pay.api.model.links.PostLink; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; +import java.util.Collections; +import java.util.Optional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GetPaymentServicePactTest { + + private static final String ACCOUNT_ID = "123456"; + private static final String CHARGE_ID = "ch_123abc456def"; + + private GetPaymentService getPaymentService; + + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + + @Mock + private PublicApiConfig mockConfiguration; + + @Before + public void setup() { + when(mockConfiguration.getConnectorUrl()).thenReturn(connectorRule.getUrl()); + when(mockConfiguration.getBaseUrl()).thenReturn("http://publicapi.test.localhost/"); + + PublicApiUriGenerator publicApiUriGenerator = new PublicApiUriGenerator(mockConfiguration); + ConnectorUriGenerator connectorUriGenerator = new ConnectorUriGenerator(mockConfiguration); + LedgerUriGenerator ledgerUriGenerator = new LedgerUriGenerator(mockConfiguration); + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + getPaymentService = new GetPaymentService(publicApiUriGenerator, + new ConnectorService(client, connectorUriGenerator), + new LedgerService(client, ledgerUriGenerator)); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-get-created-payment"}) + public void testGetPayment() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID); + + assertThat(paymentResponse.getAmount(), is(100L)); + assertThat(paymentResponse.getState(), is(new PaymentState("created", false))); + assertThat(paymentResponse.getDescription(), is("Test description")); + assertThat(paymentResponse.getReference(), is("aReference")); + assertThat(paymentResponse.getLanguage(), is(SupportedLanguage.ENGLISH)); + assertThat(paymentResponse.getPaymentId(), is(CHARGE_ID)); + assertThat(paymentResponse.getReturnUrl().get(), is("https://somewhere.gov.uk/rainbow/1")); + assertThat(paymentResponse.getPaymentProvider(), is("sandbox")); + assertThat(paymentResponse.getCreatedDate(), is("2018-09-07T13:12:02.121Z")); + assertThat(paymentResponse.getMoto(), is (false)); + assertThat(paymentResponse.getDelayedCapture(), is(true)); + assertThat(paymentResponse.getCorporateCardSurcharge(), is(Optional.empty())); + assertThat(paymentResponse.getTotalAmount(), is(Optional.empty())); + assertThat(paymentResponse.getAuthorisationMode(), is(AuthorisationMode.WEB)); + assertThat(paymentResponse.getLinks().getSelf().getHref(), containsString("v1/payments/" + CHARGE_ID)); + assertThat(paymentResponse.getLinks().getSelf().getMethod(), is("GET")); + assertThat(paymentResponse.getLinks().getRefunds().getHref(), containsString("v1/payments/" + CHARGE_ID + "/refunds")); + assertThat(paymentResponse.getLinks().getRefunds().getMethod(), is("GET")); + assertThat(paymentResponse.getLinks().getNextUrl().getHref(), containsString("secure/ae749781-6562-4e0e-8f56-32d9639079dc")); + assertThat(paymentResponse.getLinks().getNextUrl().getMethod(), is("GET")); + assertThat(paymentResponse.getLinks().getNextUrlPost(), is(new PostLink( + "https://card_frontend/secure", + "POST", + "application/x-www-form-urlencoded", + Collections.singletonMap("chargeTokenId", "ae749781-6562-4e0e-8f56-32d9639079dc") + ))); + assertThat(paymentResponse.getLinks().getCapture(), is(nullValue())); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-get-wallet-payment"}) + public void testGetPaymentWithWalletType() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID); + + assertThat(paymentResponse.getAmount(), is(100L)); + assertThat(paymentResponse.getState(), is(new PaymentState("success", true))); + assertThat(paymentResponse.getPaymentId(), is(CHARGE_ID)); + assertThat(paymentResponse.getPaymentProvider(), is("sandbox")); + assertThat(paymentResponse.getAuthorisationMode(), is(AuthorisationMode.WEB)); + assertThat(paymentResponse.getCardDetails().get().getCardHolderName(), is("aName")); + assertThat(paymentResponse.getCardDetails().get().getWalletType().get(), is(Wallet.APPLE_PAY.getTitleCase())); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-get-capturable-payment"}) + public void testGetCapturablePaymentWithMetadataAndCorporateSurcharge() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID); + + assertThat(paymentResponse.getProviderId(), is("gateway-tx-123456")); + assertThat(paymentResponse.getCorporateCardSurcharge().get(), is(250L)); + assertThat(paymentResponse.getTotalAmount().get(), is(350L)); + assertThat(paymentResponse.getLinks().getCapture().getHref(), + containsString("v1/payments/" + CHARGE_ID + "/capture")); + assertThat(paymentResponse.getLinks().getCapture().getMethod(), is("POST")); + assertThat(paymentResponse.getMetadata(), is(notNullValue())); + assertThat(paymentResponse.getMetadata().getMetadata().isEmpty(), is(false)); + assertThat(paymentResponse.getLinks().getNextUrl(), is(nullValue())); + assertThat(paymentResponse.getLinks().getNextUrlPost(), is(nullValue())); + assertThat(paymentResponse.getAuthorisationSummary().getThreeDSecure().isRequired(), is(true)); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-get-payment-with-fee-and-net-amount"}) + public void testGetPaymentWithFeeAndNetAmount() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID); + + assertThat(paymentResponse.getProviderId(), is("gateway-tx-123456")); + assertThat(paymentResponse.getFee().get(), is(5L)); + assertThat(paymentResponse.getNetAmount().get(), is(345L)); + assertThat(paymentResponse.getLinks().getNextUrl(), is(nullValue())); + assertThat(paymentResponse.getLinks().getNextUrlPost(), is(nullValue())); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-get-rejected-rcp-payment-with-can-retry-true"}) + public void testGetRejectedRecurringPaymentWithCanRetryTrue() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID); + + assertThat(paymentResponse.getState().getCanRetry(), is(true)); + assertThat(paymentResponse.getAuthorisationMode(), is(AuthorisationMode.AGREEMENT)); + assertThat(paymentResponse.getLinks().getNextUrl(), is(nullValue())); + assertThat(paymentResponse.getLinks().getNextUrlPost(), is(nullValue())); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-get-motoapi-created-payment"}) + public void testGetMotoApiPayment() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, "a-token-link"); + + PaymentWithAllLinks paymentResponse = getPaymentService.getPayment(account, CHARGE_ID); + + assertThat(paymentResponse.getAmount(), is(100L)); + assertThat(paymentResponse.getState(), is(new PaymentState("created", false))); + assertThat(paymentResponse.getDescription(), is("a description")); + assertThat(paymentResponse.getReference(), is("a reference")); + assertThat(paymentResponse.getPaymentId(), is(CHARGE_ID)); + assertThat(paymentResponse.getMoto(), is (true)); + assertThat(paymentResponse.getAuthorisationMode(), is(AuthorisationMode.MOTO_API)); + assertThat(paymentResponse.getLinks().getSelf().getHref(), containsString("v1/payments/" + CHARGE_ID)); + assertThat(paymentResponse.getLinks().getSelf().getMethod(), is("GET")); + assertThat(paymentResponse.getLinks().getRefunds().getHref(), containsString("v1/payments/" + CHARGE_ID + "/refunds")); + assertThat(paymentResponse.getLinks().getRefunds().getMethod(), is("GET")); + assertThat(paymentResponse.getLinks().getAuthUrlPost(), is(new PostLink("http://publicapi.test.localhost/v1/auth", "POST", "application/json", Collections.singletonMap("one_time_token", "token_1234567asdf")))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/PublicApiUriGeneratorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/PublicApiUriGeneratorTest.java new file mode 100644 index 000000000..b5292ab5d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/PublicApiUriGeneratorTest.java @@ -0,0 +1,41 @@ +package uk.gov.pay.api.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.pay.api.app.config.PublicApiConfig; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class PublicApiUriGeneratorTest { + + @Mock + PublicApiConfig publicApiConfig; + + PublicApiUriGenerator publicApiUriGenerator; + private String publicApiTestBaseUrl = "http://publicapi.test"; + + @BeforeEach + public void setUp() { + when(publicApiConfig.getBaseUrl()).thenReturn(publicApiTestBaseUrl); + publicApiUriGenerator = new PublicApiUriGenerator(publicApiConfig); + } + + @Test + public void convertHostToPublicAPI_with_http() { + String originalHost = "http://something.test"; + String pathAndQuery = "/v1/api/events?queryParam1=value1&queryParam2=value2"; + assertEquals(publicApiTestBaseUrl + pathAndQuery, publicApiUriGenerator.convertHostToPublicAPI(originalHost + pathAndQuery) ); + } + + @Test + public void convertHostToPublicAPI_with_https() { + String originalHost = "https://something.test"; + String pathAndQuery = "/v1/api/events?queryParam1=value1&queryParam2=value2"; + assertEquals(publicApiTestBaseUrl + pathAndQuery, publicApiUriGenerator.convertHostToPublicAPI(originalHost + pathAndQuery) ); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SearchAgreementsLedgerServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SearchAgreementsLedgerServicePactTest.java new file mode 100644 index 000000000..160d936bf --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SearchAgreementsLedgerServicePactTest.java @@ -0,0 +1,101 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.agreement.model.AgreementLedgerResponse; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.ledger.model.AgreementSearchParams; +import uk.gov.pay.api.ledger.model.SearchResults; +import uk.gov.pay.api.ledger.service.LedgerUriGenerator; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SearchAgreementsLedgerServicePactTest { + @Rule + public PactProviderRule ledgerRule = new PactProviderRule("ledger", this); + @Mock + private PublicApiConfig mockConfiguration; + + private LedgerService ledgerService; + + private static final String LEDGER_SERVICE_URL = "http://ledger.service.backend/"; + + @Before + public void setUp() { + when(mockConfiguration.getLedgerUrl()).thenReturn(ledgerRule.getUrl()); + + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + LedgerUriGenerator ledgerUriGenerator = new LedgerUriGenerator(mockConfiguration); + + ledgerService = new LedgerService(client, ledgerUriGenerator); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-agreements-with-page-and-display-size"}) + public void searchWithPageAndDisplaySize_shouldReturnCorrectPageAndPaginationLinks() { + AgreementSearchParams params = new AgreementSearchParams(null, null, "2", "1"); + Account account = new Account("777", TokenPaymentType.CARD, "a-token-link"); + SearchResults results = ledgerService.searchAgreements(account, params); + + assertThat(results.getResults().size(), is(1)); + assertThat(results.getCount(), is(1)); + assertThat(results.getTotal(), is(3)); + assertThat(results.getPage(), is(2)); + assertThat(results.getLinks().getSelf().getHref(), is(LEDGER_SERVICE_URL + "v1/agreement?account_id=777&display_size=1&page=2")); + assertThat(results.getLinks().getFirstPage().getHref(), is(LEDGER_SERVICE_URL + "v1/agreement?account_id=777&display_size=1&page=1")); + assertThat(results.getLinks().getLastPage().getHref(), is(LEDGER_SERVICE_URL + "v1/agreement?account_id=777&display_size=1&page=3")); + assertThat(results.getLinks().getPrevPage().getHref(), is(LEDGER_SERVICE_URL + "v1/agreement?account_id=777&display_size=1&page=1")); + assertThat(results.getLinks().getNextPage().getHref(), is(LEDGER_SERVICE_URL + "v1/agreement?account_id=777&display_size=1&page=3")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-agreements-with-status"}) + public void searchWithStatus_shouldReturnCorrectAgreement() { + AgreementSearchParams params = new AgreementSearchParams(null, "active", "1", "20"); + Account account = new Account("777", TokenPaymentType.CARD, "a-token-link"); + SearchResults results = ledgerService.searchAgreements(account, params); + assertThat(results.getResults().size(), is(1)); + assertThat(results.getResults().get(0).getStatus(), is("ACTIVE")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-agreements-with-reference"}) + public void searchWithReference_shouldReturnCorrectAgreement() { + AgreementSearchParams params = new AgreementSearchParams("a-valid-reference", null, "1", "20"); + Account account = new Account("3456", TokenPaymentType.CARD, "a-token-link"); + SearchResults results = ledgerService.searchAgreements(account, params); + assertThat(results.getResults().size(), is(1)); + assertThat(results.getResults().get(0).getReference(), is("a-valid-reference")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-agreement-not-found"}) + public void getOneAgreementNotFound() { + AgreementSearchParams params = new AgreementSearchParams("invalid-reference", null, "1", "20"); + Account account = new Account("3456", TokenPaymentType.CARD, "a-token-link"); + SearchResults results = ledgerService.searchAgreements(account, params); + assertThat(results.getResults().size(), is(0)); + assertThat(results.getCount(), is(0)); + assertThat(results.getTotal(), is(0)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SearchDisputesServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SearchDisputesServicePactTest.java new file mode 100644 index 000000000..ed7cb72ac --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SearchDisputesServicePactTest.java @@ -0,0 +1,191 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.SearchDisputesException; +import uk.gov.pay.api.ledger.service.LedgerUriGenerator; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.model.search.PaginationDecorator; +import uk.gov.pay.api.model.search.dispute.DisputeForSearchResult; +import uk.gov.pay.api.model.search.dispute.DisputesSearchResults; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasProperty; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SearchDisputesServicePactTest { + @Rule + public PactProviderRule ledgerRule = new PactProviderRule("ledger", this); + + @Mock + private PublicApiConfig mockConfiguration; + + private SearchDisputesService service; + + private static final String accountId = "123456"; + private static final String tokenLink = "a-token-link"; + + @Before + public void setUp() { + when(mockConfiguration.getLedgerUrl()).thenReturn(ledgerRule.getUrl()); + when(mockConfiguration.getBaseUrl()).thenReturn("http://publicapi.test.localhost/"); + + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + LedgerUriGenerator ledgerUriGenerator = new LedgerUriGenerator(mockConfiguration); + + service = new SearchDisputesService(new LedgerService(client, ledgerUriGenerator), + new PublicApiUriGenerator(mockConfiguration), + new PaginationDecorator(mockConfiguration)); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-disputes_by_settled_dates"}) + public void searchDisputesBySettledDatesShouldReturnFromLedger() { + Account account = new Account(accountId, TokenPaymentType.CARD, tokenLink); + DisputesSearchParams.Builder paramsBuilder = new DisputesSearchParams.Builder(); + DisputesSearchParams params = paramsBuilder + .withFromSettledDate("2022-05-27") + .withToSettledDate("2022-05-27") + .build(); + DisputesSearchResults results = service.searchDisputes(account, params); + assertThat(results.getResults().size(), is(1)); + assertThat(results.getCount(), is(1)); + assertThat(results.getPage(), is(1)); + assertThat(results.getTotal(), is(1)); + assertThat(results.getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/disputes?from_settled_date=2022-05-27&to_settled_date=2022-05-27&display_size=500&page=1")); + assertThat(results.getLinks().getFirstPage().getHref(), is("http://publicapi.test.localhost/v1/disputes?from_settled_date=2022-05-27&to_settled_date=2022-05-27&display_size=500&page=1")); + assertThat(results.getLinks().getLastPage().getHref(), is("http://publicapi.test.localhost/v1/disputes?from_settled_date=2022-05-27&to_settled_date=2022-05-27&display_size=500&page=1")); + + DisputeForSearchResult dispute = results.getResults().get(0); + assertThat(dispute.getDisputeId(), is("dispute97837509646393e3C")); + assertThat(dispute.getReason(), is("fraudulent")); + assertThat(dispute.getAmount(), is(1000L)); + assertThat(dispute.getFee(), is(1500L)); + assertThat(dispute.getNetAmount(), is(-2500L)); + assertThat(dispute.getPaymentId(), is("parent-abcde-12345")); + assertThat(dispute.getCreatedDate(), is("2022-05-20T19:05:00.000Z")); + assertThat(dispute.getEvidenceDueDate(), is("2022-05-27T19:05:00.000Z")); + assertThat(dispute.getSettlementSummary().getSettledDate(), is("2022-05-27")); + assertThat(dispute.getStatus(), is("lost")); + assertThat(dispute.getLinks().getPayment().getHref(), is("http://publicapi.test.localhost/v1/payments/parent-abcde-12345")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-disputes_by_state"}) + public void searchDisputesByStatus_ShouldReturnFromLedger_andShouldRewriteQueryParamsAndLinks() { + Account account = new Account(accountId, TokenPaymentType.CARD, tokenLink); + DisputesSearchParams.Builder paramsBuilder = new DisputesSearchParams.Builder(); + DisputesSearchParams params = paramsBuilder + .withStatus("lost") + .build(); + DisputesSearchResults results = service.searchDisputes(account, params); + assertThat(results.getResults().size(), is(1)); + assertThat(results.getCount(), is(1)); + assertThat(results.getPage(), is(1)); + assertThat(results.getTotal(), is(1)); + assertThat(results.getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/disputes?display_size=500&page=1&status=lost")); + assertThat(results.getLinks().getFirstPage().getHref(), is("http://publicapi.test.localhost/v1/disputes?display_size=500&page=1&status=lost")); + assertThat(results.getLinks().getLastPage().getHref(), is("http://publicapi.test.localhost/v1/disputes?display_size=500&page=1&status=lost")); + + DisputeForSearchResult dispute = results.getResults().get(0); + assertThat(dispute.getDisputeId(), is("dispute97837509646393e3C")); + assertThat(dispute.getReason(), is("fraudulent")); + assertThat(dispute.getAmount(), is(1000L)); + assertThat(dispute.getFee(), is(1500L)); + assertThat(dispute.getNetAmount(), is(-2500L)); + assertThat(dispute.getPaymentId(), is("parent-abcde-12345")); + assertThat(dispute.getCreatedDate(), is("2022-05-20T19:05:00.000Z")); + assertThat(dispute.getEvidenceDueDate(), is("2022-05-27T19:05:00.000Z")); + assertThat(dispute.getSettlementSummary().getSettledDate(), is("2022-05-27")); + assertThat(dispute.getStatus(), is("lost")); + assertThat(dispute.getLinks().getPayment().getHref(), is("http://publicapi.test.localhost/v1/payments/parent-abcde-12345")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-disputes-by-dates"}) + public void searchDisputesByDatesShouldReturnFromLedger() { + Account account = new Account(accountId, TokenPaymentType.CARD, tokenLink); + DisputesSearchParams.Builder paramsBuilder = new DisputesSearchParams.Builder(); + DisputesSearchParams params = paramsBuilder + .withFromDate("2022-05-20T19:04:00Z") + .withToDate("2022-05-20T19:06:00Z") + .build(); + DisputesSearchResults results = service.searchDisputes(account, params); + assertThat(results.getResults().size(), is(1)); + assertThat(results.getCount(), is(1)); + assertThat(results.getPage(), is(1)); + assertThat(results.getTotal(), is(1)); + assertThat(results.getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/disputes?from_date=2022-05-20T19%3A04%3A00Z&to_date=2022-05-20T19%3A06%3A00Z&display_size=500&page=1")); + assertThat(results.getLinks().getFirstPage().getHref(), is("http://publicapi.test.localhost/v1/disputes?from_date=2022-05-20T19%3A04%3A00Z&to_date=2022-05-20T19%3A06%3A00Z&display_size=500&page=1")); + assertThat(results.getLinks().getLastPage().getHref(), is("http://publicapi.test.localhost/v1/disputes?from_date=2022-05-20T19%3A04%3A00Z&to_date=2022-05-20T19%3A06%3A00Z&display_size=500&page=1")); + + DisputeForSearchResult dispute = results.getResults().get(0); + assertThat(dispute.getDisputeId(), is("dispute97837509646393e3C")); + assertThat(dispute.getReason(), is("fraudulent")); + assertThat(dispute.getAmount(), is(1000L)); + assertThat(dispute.getFee(), is(1500L)); + assertThat(dispute.getNetAmount(), is(-2500L)); + assertThat(dispute.getPaymentId(), is("parent-abcde-12345")); + assertThat(dispute.getCreatedDate(), is("2022-05-20T19:05:00.000Z")); + assertThat(dispute.getEvidenceDueDate(), is("2022-05-27T19:05:00.000Z")); + assertThat(dispute.getSettlementSummary().getSettledDate(), is("2022-05-27")); + assertThat(dispute.getStatus(), is("lost")); + assertThat(dispute.getLinks().getPayment().getHref(), is("http://publicapi.test.localhost/v1/payments/parent-abcde-12345")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-disputes-no-result"}) + public void searchDisputesNoResult() { + Account account = new Account(accountId, TokenPaymentType.CARD, tokenLink); + DisputesSearchParams.Builder paramsBuilder = new DisputesSearchParams.Builder(); + DisputesSearchParams params = paramsBuilder + .withFromDate("2021-05-20T19:04:00Z") + .withToDate("2021-05-20T19:06:00Z") + .build(); + DisputesSearchResults results = service.searchDisputes(account, params); + assertThat(results.getResults().size(), is(0)); + assertThat(results.getCount(), is(0)); + assertThat(results.getPage(), is(1)); + assertThat(results.getTotal(), is(0)); + assertThat(results.getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/disputes?from_date=2021-05-20T19%3A04%3A00Z&to_date=2021-05-20T19%3A06%3A00Z&display_size=500&page=1")); + assertThat(results.getLinks().getFirstPage().getHref(), is("http://publicapi.test.localhost/v1/disputes?from_date=2021-05-20T19%3A04%3A00Z&to_date=2021-05-20T19%3A06%3A00Z&display_size=500&page=1")); + assertThat(results.getLinks().getLastPage().getHref(), is("http://publicapi.test.localhost/v1/disputes?from_date=2021-05-20T19%3A04%3A00Z&to_date=2021-05-20T19%3A06%3A00Z&display_size=500&page=1")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-disputes-page-not-found"}) + public void shouldReturn404WhenSearchingWithNonExistentPageNumber() { + Account account = new Account(accountId, TokenPaymentType.CARD, tokenLink); + DisputesSearchParams.Builder paramsBuilder = new DisputesSearchParams.Builder(); + DisputesSearchParams params = paramsBuilder + .withPage("999") + .build(); + + SearchDisputesException searchDisputesException = assertThrows(SearchDisputesException.class, + () -> service.searchDisputes(account, params)); + + assertThat(searchDisputesException, hasProperty("errorStatus", Matchers.is(404))); + } +} \ No newline at end of file diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SearchRefundsServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SearchRefundsServicePactTest.java new file mode 100644 index 000000000..a4b909b6e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SearchRefundsServicePactTest.java @@ -0,0 +1,146 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.SearchRefundsException; +import uk.gov.pay.api.ledger.service.LedgerUriGenerator; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.model.search.PaginationDecorator; +import uk.gov.pay.api.model.search.card.SearchRefundsResults; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; + +import static java.lang.String.format; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasProperty; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SearchRefundsServicePactTest { + + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + @Rule + public PactProviderRule ledgerRule = new PactProviderRule("ledger", this); + @Mock + private PublicApiConfig mockConfiguration; + + private SearchRefundsService searchRefundsService; + private String ACCOUNT_ID = "888"; + private static final String tokenLink = "a-token-link"; + + @Before + public void setUp() { + when(mockConfiguration.getLedgerUrl()).thenReturn(ledgerRule.getUrl()); + when(mockConfiguration.getBaseUrl()).thenReturn("http://publicapi.test.localhost/"); + + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + LedgerUriGenerator ledgerUriGenerator = new LedgerUriGenerator(mockConfiguration); + + searchRefundsService = new SearchRefundsService( + new LedgerService(client, ledgerUriGenerator), + new PublicApiUriGenerator(mockConfiguration), + new PaginationDecorator(mockConfiguration)); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-refunds"}) + public void getAllRefundsShouldReturnFromLedger() { + RefundsParams params = new RefundsParams("2018-09-21T13:22:55Z", "2018-10-23T13:24:55Z", "1", "500", null, null); + String accountId = "777"; + String refundId1 = "111111"; + String refundId2 = "222222"; + Account account = new Account(accountId, TokenPaymentType.CARD, tokenLink); + SearchRefundsResults results = searchRefundsService.searchLedgerRefunds(account, params); + + assertThat(results.getResults().size(), is(2)); + assertThat(results.getCount(), is(2)); + assertThat(results.getTotal(), is(2)); + assertThat(results.getPage(), is(1)); + assertThat(results.getResults().get(0).getStatus(), is("success")); + assertThat(results.getResults().get(0).getCreatedDate(), is("2018-09-22T10:14:16.067Z")); + assertThat(results.getResults().get(0).getRefundId(), is(refundId1)); + assertThat(results.getResults().get(0).getChargeId(), is("someExternalId1")); + assertThat(results.getResults().get(0).getAmount(), is(150L)); + assertThat(results.getResults().get(0).getLinks().getSelf().getHref(), is(format("http://publicapi.test.localhost/v1/payments/someExternalId1/refunds/%s", refundId1))); + assertThat(results.getResults().get(0).getLinks().getPayment().getHref(), is("http://publicapi.test.localhost/v1/payments/someExternalId1")); + + assertThat(results.getResults().get(1).getStatus(), is("success")); + assertThat(results.getResults().get(1).getCreatedDate(), is("2018-10-22T10:16:16.067Z")); + assertThat(results.getResults().get(1).getRefundId(), is(refundId2)); + assertThat(results.getResults().get(1).getChargeId(), is("someExternalId2")); + assertThat(results.getResults().get(1).getAmount(), is(250L)); + assertThat(results.getResults().get(1).getLinks().getSelf().getHref(), is(format("http://publicapi.test.localhost/v1/payments/someExternalId2/refunds/%s", refundId2))); + assertThat(results.getResults().get(1).getLinks().getPayment().getHref(), is("http://publicapi.test.localhost/v1/payments/someExternalId2")); + + assertThat(results.getLinks().getSelf().getHref(), is("http://publicapi.test.localhost/v1/refunds?from_date=2018-09-21T13%3A22%3A55Z&to_date=2018-10-23T13%3A24%3A55Z&display_size=500&page=1")); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-refunds-display-size-two"}) + public void shouldSearchForAllExistingRefundsWithDisplaySizeTwo() { + String accountId = "777"; + Account account = new Account(accountId, TokenPaymentType.CARD, tokenLink); + RefundsParams params = new RefundsParams(null, null, "1", "2", null, null); + SearchRefundsResults results = searchRefundsService.searchLedgerRefunds(account, params); + assertThat(results.getResults().size(), is(2)); + assertThat(results.getCount(), is(2)); + assertThat(results.getTotal(), is(2)); + assertThat(results.getPage(), is(1)); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-refunds-with-page-and-display-when-no-refunds-exist"}) + public void getAllRefundsShouldReturnNoRefundsFromLedgerWhenThereAreNone() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, tokenLink); + RefundsParams params = new RefundsParams(null, null, "1", "1", null, null); + SearchRefundsResults results = searchRefundsService.searchLedgerRefunds(account, params); + assertThat(results.getCount(), is(0)); + assertThat(results.getTotal(), is(0)); + assertThat(results.getPage(), is(1)); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-refunds-page-not-found"}) + public void shouldReturn404WhenSearchingWithNonExistentPageNumber() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, tokenLink); + RefundsParams params = new RefundsParams(null, null, "999", "500", null, null); + + SearchRefundsException searchRefundsException = assertThrows(SearchRefundsException.class, + () -> searchRefundsService.searchLedgerRefunds(account, params)); + + assertThat(searchRefundsException, hasProperty("errorStatus", is(404))); + } + + @Test + @PactVerification({"ledger"}) + @Pacts(pacts = {"publicapi-ledger-search-refunds_with_settled_dates"}) + public void shouldReturnARefundWhenSearchingWithSettledDates() { + String accountId = "123456"; + Account account = new Account(accountId, TokenPaymentType.CARD, tokenLink); + RefundsParams params = new RefundsParams(null, null, "1", "500", "2020-09-19", "2020-09-20"); + SearchRefundsResults results = searchRefundsService.searchLedgerRefunds(account, params); + assertThat(results.getResults().size(), is(1)); + assertThat(results.getCount(), is(1)); + assertThat(results.getTotal(), is(1)); + assertThat(results.getPage(), is(1)); + assertThat(results.getResults().get(0).getSettlementSummary().getSettledDate(), is("2020-09-19")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SearchRefundsServiceTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SearchRefundsServiceTest.java new file mode 100644 index 000000000..72b709a6e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SearchRefundsServiceTest.java @@ -0,0 +1,71 @@ +package uk.gov.pay.api.service; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.exception.RefundsValidationException; +import uk.gov.pay.api.ledger.service.LedgerUriGenerator; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.model.search.PaginationDecorator; + +import javax.ws.rs.client.Client; + +import static java.lang.String.format; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertThrows; +import static uk.gov.pay.api.matcher.RefundValidationExceptionMatcher.aValidationExceptionContaining; + +@RunWith(MockitoJUnitRunner.class) +public class SearchRefundsServiceTest { + @Mock + private PublicApiConfig mockConfiguration; + @Mock + private Client mockClient; + @Mock + private LedgerUriGenerator mockLedgerUriGenerator; + private SearchRefundsService searchRefundsService; + private String ACCOUNT_ID = "888"; + private static final String tokenLink = "a-token-link"; + + @Before + public void setUp() { + searchRefundsService = new SearchRefundsService( + new LedgerService(mockClient, mockLedgerUriGenerator), + new PublicApiUriGenerator(mockConfiguration), + new PaginationDecorator(mockConfiguration)); + } + + @Test + public void getSearchResponse_shouldThrowRefundsValidationExceptionWhenParamsAreInvalid() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, tokenLink); + String invalid = "invalid_param"; + RefundsParams params = new RefundsParams(null, null, invalid, invalid, null, null); + + RefundsValidationException refundsValidationException = assertThrows(RefundsValidationException.class, + () -> searchRefundsService.searchLedgerRefunds(account, params)); + + assertThat(refundsValidationException, aValidationExceptionContaining( + "P1101", + format("Invalid parameters: %s. See Public API documentation for the correct data formats", + "page, display_size"))); + } + + @Test + public void getSearchResponseFromLedger_shouldThrowRefundsValidationExceptionWhenParamsAreInvalid() { + Account account = new Account(ACCOUNT_ID, TokenPaymentType.CARD, tokenLink); + String invalid = "invalid_param"; + RefundsParams params = new RefundsParams(null, null, invalid, invalid, null, null); + + RefundsValidationException refundsValidationException = assertThrows(RefundsValidationException.class, + () -> searchRefundsService.searchLedgerRefunds(account, params)); + + assertThat(refundsValidationException, aValidationExceptionContaining( + "P1101", + format("Invalid parameters: %s. See Public API documentation for the correct data formats", + "page, display_size"))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SetUpAgreementWithPaymentConnectorServicePactTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SetUpAgreementWithPaymentConnectorServicePactTest.java new file mode 100644 index 000000000..79c1c10a9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/service/SetUpAgreementWithPaymentConnectorServicePactTest.java @@ -0,0 +1,78 @@ +package uk.gov.pay.api.service; + +import au.com.dius.pact.consumer.PactVerification; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import uk.gov.pay.api.app.RestClientFactory; +import uk.gov.pay.api.app.config.PublicApiConfig; +import uk.gov.pay.api.app.config.RestClientConfig; +import uk.gov.pay.api.auth.Account; +import uk.gov.pay.api.model.CardPayment; +import uk.gov.pay.api.model.CreateCardPaymentRequestBuilder; +import uk.gov.pay.api.model.CreatedPaymentWithAllLinks; +import uk.gov.pay.api.model.TokenPaymentType; +import uk.gov.pay.api.model.links.Link; +import uk.gov.pay.api.model.links.PaymentWithAllLinks; +import uk.gov.service.payments.commons.testing.pact.consumers.PactProviderRule; +import uk.gov.service.payments.commons.testing.pact.consumers.Pacts; + +import javax.ws.rs.client.Client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SetUpAgreementWithPaymentConnectorServicePactTest { + + private final Account ACCOUNT = new Account("123456", TokenPaymentType.CARD, "a-token-link"); + + @Rule + public PactProviderRule connectorRule = new PactProviderRule("connector", this); + + private ConnectorService connectorService; + + private CreatePaymentService createPaymentService; + + @Mock + private PublicApiConfig mockConfiguration; + + @Before + public void setUp() { + when(mockConfiguration.getConnectorUrl()).thenReturn(connectorRule.getUrl()); + when(mockConfiguration.getBaseUrl()).thenReturn("http://publicapi.test.localhost/"); + PublicApiUriGenerator publicApiUriGenerator = new PublicApiUriGenerator(mockConfiguration); + ConnectorUriGenerator connectorUriGenerator = new ConnectorUriGenerator(mockConfiguration); + Client client = RestClientFactory.buildClient(new RestClientConfig(false)); + connectorService = new ConnectorService(client, connectorUriGenerator); + createPaymentService = new CreatePaymentService(client, publicApiUriGenerator, connectorUriGenerator); + } + + @Test + @PactVerification({"connector"}) + @Pacts(pacts = {"publicapi-connector-create-payment-to-setup-agreement"}) + public void setUpAnAgreementWithPayment() { + var buildRequestPayload = CreateCardPaymentRequestBuilder.builder() + .amount(1968) + .returnUrl("https://www.google.com") + .reference("a-valid-reference") + .description("a-valid-description"); + + buildRequestPayload.setUpAgreement("i6sjhoa36s1lhtjl07vuuhbm72"); + var requestPayload = buildRequestPayload.build(); + + CreatedPaymentWithAllLinks createdPaymentWithAllLinks = createPaymentService.create(ACCOUNT, requestPayload, null); + PaymentWithAllLinks paymentResponse = createdPaymentWithAllLinks.getPayment(); + + assertThat(paymentResponse.getPaymentId(), is("iinvkbkkrt8kcl0atps9q7p7cm")); + assertThat(paymentResponse.getAmount(), is(1968L)); + assertThat(paymentResponse.getReference(), is("a-valid-reference")); + assertThat(paymentResponse.getDescription(), is("a-valid-description")); + assertThat(paymentResponse.getAgreementId(), is("i6sjhoa36s1lhtjl07vuuhbm72")); + assertThat(paymentResponse.getLinks().getNextUrl(), is(new Link("http://CardFrontend/secure/efbdf987-3c91-4005-b892-9d056a4bd414", "GET"))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/ApiKeyGenerator.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/ApiKeyGenerator.java new file mode 100644 index 000000000..b100da3ec --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/ApiKeyGenerator.java @@ -0,0 +1,14 @@ +package uk.gov.pay.api.utils; + +import com.google.common.io.BaseEncoding; +import org.apache.commons.codec.digest.HmacAlgorithms; +import org.apache.commons.codec.digest.HmacUtils; + +public class ApiKeyGenerator { + + public static String apiKeyValueOf(String token, String secret) { + byte[] hmacBytes = new HmacUtils(HmacAlgorithms.HMAC_SHA_1, secret).hmac(token); + String encodedHmac = BaseEncoding.base32Hex().lowerCase().omitPadding().encode(hmacBytes); + return token + encodedHmac; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/ChargeEventBuilder.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/ChargeEventBuilder.java new file mode 100644 index 000000000..7d8f44c61 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/ChargeEventBuilder.java @@ -0,0 +1,29 @@ +package uk.gov.pay.api.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import uk.gov.pay.api.model.PaymentState; + +import java.util.Map; + +public class ChargeEventBuilder { + @JsonDeserialize + @JsonSerialize + private PaymentState state; + + @JsonDeserialize + @JsonSerialize + private String updated; + + public ChargeEventBuilder(PaymentState state, String updated) { + this.state = state; + this.updated = updated; + } + + public Map build() { + ObjectMapper mapper = new ObjectMapper(); + return mapper.convertValue(this, Map.class); + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/JsonStringBuilderTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/JsonStringBuilderTest.java new file mode 100644 index 000000000..d1d4d91dd --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/JsonStringBuilderTest.java @@ -0,0 +1,74 @@ +package uk.gov.pay.api.utils; + +import org.junit.jupiter.api.Test; + +import static org.junit.Assert.assertEquals; + +public class JsonStringBuilderTest { + @Test + public void testObjectToJsonString() { + String message = "There was an error"; + String code = "#the code of error!"; + + String result = new JsonStringBuilder() + .addRoot("error") + .add("message", message) + .add("type", "card_error") + .add("param", "number") + .add("code", code) + .noPrettyPrint() + .build(); + + assertEquals("{\"error\":{\"message\":\"There was an error\",\"type\":\"card_error\",\"param\":\"number\",\"code\":\"#the code of error!\"}}", result); + } + + @Test + public void nullValues() { + String message = "There was an error"; + + String result = new JsonStringBuilder() + .addRoot("error") + .add("message", message) + .add("type", "card_error") + .add("param", "number") + .add("code", null) + .noPrettyPrint() + .build(); + + assertEquals("{\"error\":{\"message\":\"There was an error\",\"type\":\"card_error\",\"param\":\"number\"}}", result); + } + + @Test + public void nestedMaps() { + String message = "There was an error"; + + String result = new JsonStringBuilder() + .addRoot("error") + .add("message", message) + .add("type", "card_error") + .add("param", "number") + .addToMap("metadata", "orderid", "our-order-id") + .addToMap("empty") + .noPrettyPrint() + .build(); + + assertEquals("{\"error\":{\"message\":\"There was an error\",\"type\":\"card_error\",\"param\":\"number\",\"metadata\":{\"orderid\":\"our-order-id\"},\"empty\":{}}}", result); + } + + @Test + public void nestedMapsWithMapKeyVarArgs() { + String message = "There was an error"; + + String result = new JsonStringBuilder() + .addRoot("error") + .add("message", message) + .add("type", "card_error") + .add("param", "number") + .addToMap("metadata", "orderid", "our-order-id") + .addToNestedMap("error_meta", "meta data of error", "metadata", "meta_error") + .noPrettyPrint() + .build(); + + assertEquals("{\"error\":{\"message\":\"There was an error\",\"type\":\"card_error\",\"param\":\"number\",\"metadata\":{\"orderid\":\"our-order-id\",\"meta_error\":{\"error_meta\":\"meta data of error\"}}}}", result); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/PathHelperTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/PathHelperTest.java new file mode 100644 index 000000000..7d68eb4ca --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/PathHelperTest.java @@ -0,0 +1,31 @@ +package uk.gov.pay.api.utils; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +public class PathHelperTest { + + @ParameterizedTest + @MethodSource("rateLimitParams") + public void returnsPathType(String path, String method, String pathType) { + assertThat(PathHelper.getPathType(path, method), is(pathType)); + } + + static Stream rateLimitParams() { + return Stream.of( + arguments("/v1/payments", "POST", "create_payment"), + arguments("/v1/payments/", "POST", "create_payment"), + arguments("/v1/payments/paymentId/capture", "POST", "capture_payment"), + arguments("/v1/payments/paymentId/capture/", "POST", "capture_payment"), + arguments("/v1/payments/paymentId/cancel", "POST", ""), + arguments("/v1/payments", "GET", "") + ); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/Payloads.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/Payloads.java new file mode 100644 index 000000000..e9f0968a0 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/Payloads.java @@ -0,0 +1,33 @@ +package uk.gov.pay.api.utils; + +import uk.gov.pay.api.utils.mocks.CreateAgreementRequestParams; + +public class Payloads { + + public static String aSuccessfulPaymentPayload() { + int amount = 100; + String returnUrl = "https://somewhere.gov.uk/rainbow/1"; + String reference = "a reference"; + String description = "a description"; + return aSuccessfulPaymentPayload(amount, returnUrl, description, reference); + } + + public static String aSuccessfulPaymentPayload(int amount, String returnUrl, String description, String reference) { + return new JsonStringBuilder() + .add("amount", amount) + .add("reference", reference) + .add("description", description) + .add("return_url", returnUrl) + .build(); + } + + public static String agreementPayload(CreateAgreementRequestParams params) { + var stringBuilder = new JsonStringBuilder() + .add("reference", params.getReference()) + .add("description", params.getDescription()); + if (params.getUserIdentifier() != null) { + stringBuilder.add("user_identifier", params.getUserIdentifier()); + } + return stringBuilder.build(); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/PublicAuthMockClient.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/PublicAuthMockClient.java new file mode 100644 index 000000000..f3d91b5f9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/PublicAuthMockClient.java @@ -0,0 +1,63 @@ +package uk.gov.pay.api.utils; + +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import uk.gov.pay.api.model.TokenPaymentType; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static javax.ws.rs.core.HttpHeaders.ACCEPT; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AUTH_TOKEN_INVALID; + +public class PublicAuthMockClient { + + private WireMockClassRule publicAuthClassRule; + + public PublicAuthMockClient(WireMockClassRule publicAuthClassRule) { + this.publicAuthClassRule = publicAuthClassRule; + } + + public void respondUnauthorised() { + publicAuthClassRule.stubFor(get("/v1/auth").withHeader(ACCEPT, matching(APPLICATION_JSON)) + .willReturn(aResponse() + .withStatus(401) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody("{\"error_identifier\" : \"" + AUTH_TOKEN_INVALID + "\"}"))); + } + + public void mapBearerTokenToAccountId(String bearerToken, String gatewayAccountId) { + mapBearerTokenToAccountId(bearerToken, gatewayAccountId, TokenPaymentType.CARD); + } + + public void mapBearerTokenToAccountId(String bearerToken, String gatewayAccountId, TokenPaymentType tokenType) { + publicAuthClassRule.stubFor(get("/v1/auth") + .withHeader(ACCEPT, matching(APPLICATION_JSON)) + .withHeader(AUTHORIZATION, matching("Bearer " + bearerToken)) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody("{\"account_id\" : \"" + gatewayAccountId + "\", " + + "\"token_link\" : \"a-token-link\", " + + "\"token_type\" : \"" + tokenType.toString() + "\"}"))); + } + + public void respondWithInvalidTokenType(String bearerToken, String gatewayAccountId) { + publicAuthClassRule.stubFor(get("/v1/auth") + .withHeader(ACCEPT, matching(APPLICATION_JSON)) + .withHeader(AUTHORIZATION, matching("Bearer " + bearerToken)) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody("{\"account_id\" : \"" + gatewayAccountId + "\", " + + "\"token_link\" : \"a-token-link\", " + + "\"token_type\" : \"AN_UNKNOWN_TYPE\"}"))); + } + + public void respondWithError() { + publicAuthClassRule.stubFor(get("").withHeader(ACCEPT, matching(APPLICATION_JSON)) + .willReturn(aResponse().withStatus(500))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/Urls.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/Urls.java new file mode 100644 index 000000000..f8e3b276e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/Urls.java @@ -0,0 +1,8 @@ +package uk.gov.pay.api.utils; + +public class Urls { + + public static String paymentLocationFor(String publicApiBaseUrl, String chargeId) { + return publicApiBaseUrl + "v1/payments/" + chargeId; + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/WiremockStubbing.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/WiremockStubbing.java new file mode 100644 index 000000000..640d89af6 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/WiremockStubbing.java @@ -0,0 +1,30 @@ +package uk.gov.pay.api.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.google.common.collect.ImmutableMap; +import uk.gov.pay.api.auth.Account; + +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +public class WiremockStubbing { + + public static void stubPublicAuthV1ApiAuth(WireMockRule wireMockRule, Account account, String token) throws JsonProcessingException { + Map entity = ImmutableMap.of("account_id", account.getAccountId(), "token_type", account.getPaymentType().name()); + String json = new ObjectMapper().writeValueAsString(entity); + wireMockRule.stubFor(get(urlEqualTo("/v1/api/auth")) + .withHeader(AUTHORIZATION, equalTo("Bearer " + token)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", APPLICATION_JSON) + .withBody(json))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/AgreementFromLedgerFixture.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/AgreementFromLedgerFixture.java new file mode 100644 index 000000000..b94304ca9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/AgreementFromLedgerFixture.java @@ -0,0 +1,154 @@ +package uk.gov.pay.api.utils.mocks; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import uk.gov.pay.api.agreement.model.AgreementLedgerResponse; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.CardDetailsFromResponse; + +import java.time.ZonedDateTime; + +import static uk.gov.service.payments.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class AgreementFromLedgerFixture { + // this should be used to generate pacts with Ledger + private final String externalId; + private final String serviceId; + private final String reference; + private final String description; + private final String status; + private final String createdDate; + private final AgreementLedgerResponse.PaymentInstrumentLedgerResponse paymentInstrument; + private String userIdentifier; + private String cancelledDate; + + private AgreementFromLedgerFixture(AgreementFromLedgerFixtureBuilder builder) { + this.externalId = builder.externalId; + this.serviceId = builder.serviceId; + this.reference = builder.reference; + this.description = builder.description; + this.status = builder.status; + this.createdDate = builder.createdDate; + this.paymentInstrument = builder.paymentInstrument; + this.userIdentifier = builder.userIdentifier; + this.cancelledDate = builder.cancelledDate; + } + + public String getExternalId() { + return externalId; + } + + public String getServiceId() { + return serviceId; + } + + public String getReference() { + return reference; + } + + public String getDescription() { + return description; + } + + public String getStatus() { + return status; + } + + public String getCreatedDate() { + return createdDate; + } + + public String getCancelledDate() { + return cancelledDate; + } + + public AgreementLedgerResponse.PaymentInstrumentLedgerResponse getPaymentInstrument() { + return paymentInstrument; + } + + public String getUserIdentifier() { + return userIdentifier; + } + + public static final class AgreementFromLedgerFixtureBuilder { + private String externalId = "agreement-external-id"; + private String serviceId = "a-service-id"; + private String reference = "valid-reference"; + private String description = "An agreement description"; + private String status = "CREATED"; + private String createdDate = ISO_INSTANT_MILLISECOND_PRECISION.format(ZonedDateTime.parse("2022-07-20T11:01:00.132012345Z")); + private AgreementLedgerResponse.PaymentInstrumentLedgerResponse paymentInstrument; + private String userIdentifier; + private String cancelledDate = null; + + private AgreementFromLedgerFixtureBuilder() { + paymentInstrument = new AgreementLedgerResponse.PaymentInstrumentLedgerResponse.Builder() + .withExternalId("payment-instrument-external-id") + .withAgreementExternalId("agreement-external-id") + .withCreatedDate("2022-08-02T15:20:00.000Z") + .withType("card") + .withCardDetails(new CardDetailsFromResponse("1234", "123456", "Rio Curring", "12/27", + new Address("Line 1", "Line 2", "E1 8QS", "London", "GB"), + "Mastercard", "debit")) + .build(); + } + + public static AgreementFromLedgerFixtureBuilder anAgreementFromLedgerWithPaymentInstrumentFixture() { + return new AgreementFromLedgerFixtureBuilder(); + } + + public static AgreementFromLedgerFixtureBuilder anAgreementFromLedgerWithoutPaymentInstrumentFixture() { + return new AgreementFromLedgerFixtureBuilder().withPaymentInstrument(null); + } + + public AgreementFromLedgerFixtureBuilder withExternalId(String externalId) { + this.externalId = externalId; + return this; + } + + public AgreementFromLedgerFixtureBuilder withServiceId(String serviceId) { + this.serviceId = serviceId; + return this; + } + + public AgreementFromLedgerFixtureBuilder withReference(String reference) { + this.reference = reference; + return this; + } + + public AgreementFromLedgerFixtureBuilder withDescription(String description) { + this.description = description; + return this; + } + + public AgreementFromLedgerFixtureBuilder withStatus(String status) { + this.status = status; + return this; + } + + public AgreementFromLedgerFixtureBuilder withCreatedDate(String createdDate) { + this.createdDate = createdDate; + return this; + } + + public AgreementFromLedgerFixtureBuilder withPaymentInstrument(AgreementLedgerResponse.PaymentInstrumentLedgerResponse paymentInstrument) { + this.paymentInstrument = paymentInstrument; + return this; + } + + public AgreementFromLedgerFixtureBuilder withUserIdentifier(String userIdentifier) { + this.userIdentifier = userIdentifier; + return this; + } + + public AgreementFromLedgerFixtureBuilder withCancelledDate(String cancelledDate) { + this.cancelledDate = cancelledDate; + return this; + } + + public AgreementFromLedgerFixture build() { + return new AgreementFromLedgerFixture(this); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/AgreementResponseFromConnector.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/AgreementResponseFromConnector.java new file mode 100644 index 000000000..bf62a0b8d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/AgreementResponseFromConnector.java @@ -0,0 +1,93 @@ +package uk.gov.pay.api.utils.mocks; +import java.util.List; +import java.util.Objects; + +public class AgreementResponseFromConnector { + private final String agreementId; + private final String reference; + private final String createdDate; + private final String serviceId; + private final boolean live; + + public String getAgreementId() { + return agreementId; + } + + public String getCreatedDate() { + return createdDate; + } + + public String getServiceId() { + return serviceId; + } + + public boolean isLive() { + return live; + } + + public String getReference() { + return reference; + } + + private AgreementResponseFromConnector(AgreementResponseFromConnectorBuilder builder) { + this.createdDate = builder.createdDate; + this.agreementId = builder.agreementId; + this.reference = builder.reference; + this.serviceId = builder.serviceId; + this.live = builder.live; + } + + public static final class AgreementResponseFromConnectorBuilder { + private String agreementId; + private String reference; + private String createdDate; + private String serviceId; + private boolean live; + + private AgreementResponseFromConnectorBuilder() { + } + + public static AgreementResponseFromConnectorBuilder aCreateAgreementResponseFromConnector() { + return new AgreementResponseFromConnectorBuilder(); + } + + public static AgreementResponseFromConnectorBuilder aCreateAgreementResponseFromConnector(AgreementResponseFromConnector responseFromConnector) { + return new AgreementResponseFromConnectorBuilder() + .withReference(responseFromConnector.reference) + .withCreatedDate(responseFromConnector.createdDate) + .withAgreementId(responseFromConnector.agreementId) + .withServiceId(responseFromConnector.serviceId) + .withLive(responseFromConnector.live); + } + + public AgreementResponseFromConnectorBuilder withAgreementId(String agreementId) { + this.agreementId = agreementId; + return this; + } + + public AgreementResponseFromConnectorBuilder withServiceId(String serviceId) { + this.serviceId = serviceId; + return this; + } + + public AgreementResponseFromConnectorBuilder withLive(boolean isLive) { + this.live = isLive; + return this; + } + + public AgreementResponseFromConnectorBuilder withReference(String reference) { + this.reference = reference; + return this; + } + + public AgreementResponseFromConnectorBuilder withCreatedDate(String createdDate) { + this.createdDate = createdDate; + return this; + } + + public AgreementResponseFromConnector build() { + List.of(agreementId, serviceId,createdDate, reference).forEach(Objects::requireNonNull); + return new AgreementResponseFromConnector(this); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/BaseConnectorMockClient.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/BaseConnectorMockClient.java new file mode 100644 index 000000000..9e917db1c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/BaseConnectorMockClient.java @@ -0,0 +1,161 @@ +package uk.gov.pay.api.utils.mocks; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.github.tomakehurst.wiremock.matching.StringValuePattern; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import uk.gov.pay.api.utils.JsonStringBuilder; + +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.lang.String.format; +import static javax.ws.rs.HttpMethod.GET; +import static javax.ws.rs.HttpMethod.POST; +import static uk.gov.service.payments.commons.model.Source.CARD_API; + +public abstract class BaseConnectorMockClient { + + static String CONNECTOR_MOCK_ACCOUNTS_PATH = "/v1/api/accounts/%s"; + static String CONNECTOR_MOCK_CHARGES_PATH = CONNECTOR_MOCK_ACCOUNTS_PATH + "/charges"; + static String CONNECTOR_MOCK_TELEPHONE_CHARGES_PATH = CONNECTOR_MOCK_ACCOUNTS_PATH + "/telephone-charges"; + static String CONNECTOR_MOCK_CHARGE_PATH = CONNECTOR_MOCK_CHARGES_PATH + "/%s"; + static String CONNECTOR_MOCK_AGREEMENTS_PATH = CONNECTOR_MOCK_ACCOUNTS_PATH + "/agreements"; + static String CONNECTOR_MOCK_AGREEMENT_PATH = CONNECTOR_MOCK_AGREEMENTS_PATH + "/%s"; + static String CONNECTOR_MOCK_AUTHORISATION_PATH = "/v1/api/charges/authorise"; + + WireMockClassRule wireMockClassRule; + Gson gson = new Gson(); + ObjectMapper objectMapper = new ObjectMapper(); + + BaseConnectorMockClient(WireMockClassRule wireMockClassRule) { + this.wireMockClassRule = wireMockClassRule; + } + + ImmutableMap validGetLink(String href, String rel) { + return ImmutableMap.of( + "href", href, + "rel", rel, + "method", GET); + } + + ImmutableMap validPostLink(String href, String rel, String type, Map params) { + return ImmutableMap.of( + "href", href, + "rel", rel, + "type", type, + "params", params, + "method", POST); + } + + String chargeLocation(String accountId, String chargeId) { + return format(CONNECTOR_MOCK_CHARGE_PATH, accountId, chargeId); + } + + abstract String nextUrlPost(); + + String nextUrl(String tokenId) { + return nextUrlPost() + tokenId; + } + + void whenGetCharge(String gatewayAccountId, String chargeId, ResponseDefinitionBuilder response) { + wireMockClassRule.stubFor(get(urlPathEqualTo(format(CONNECTOR_MOCK_CHARGE_PATH, gatewayAccountId, chargeId))) + .willReturn(response)); + } + + String createChargePayload(CreateChargeRequestParams params) { + JsonStringBuilder payload = new JsonStringBuilder() + .add("amount", params.getAmount()) + .add("reference", params.getReference()) + .add("description", params.getDescription()) + .add("return_url", params.getReturnUrl()); + + if (!params.getMetadata().isEmpty()) + payload.add("metadata", params.getMetadata()); + + if (params.getEmail() != null) { + payload.add("email", params.getEmail()); + } + + if (params.getCardholderName().isPresent()) { + payload.addToNestedMap("cardholder_name", params.getCardholderName().get(), "prefilled_cardholder_details"); + } + + if (params.getAddressLine1().isPresent()) { + payload.addToNestedMap("line1", params.getAddressLine1().get(), "prefilled_cardholder_details", "billing_address"); + } + + if (params.getAddressLine2().isPresent()) { + payload.addToNestedMap("line2", params.getAddressLine2().get(), "prefilled_cardholder_details", "billing_address"); + } + + if (params.getAddressPostcode().isPresent()) { + payload.addToNestedMap("postcode", params.getAddressPostcode().get(), "prefilled_cardholder_details", "billing_address"); + } + + if (params.getAddressCity().isPresent()) { + payload.addToNestedMap("city", params.getAddressCity().get(), "prefilled_cardholder_details", "billing_address"); + } + + if (params.getAddressCountry().isPresent()) { + payload.addToNestedMap("country", params.getAddressCountry().get(), "prefilled_cardholder_details", "billing_address"); + } + + params.getSetUpAgreement().ifPresent(setUpAgreement -> { + payload.add("agreement_id", setUpAgreement); + payload.add("save_payment_instrument_to_agreement", true); + }); + + params.getAgreementId().ifPresent(agreementId -> { + payload.add("agreement_id", agreementId); + }); + + params.getAuthorisationMode().ifPresent(authorisationMode -> { + payload.add("authorisation_mode", authorisationMode); + }); + + payload.add("source", params.getSource().orElse(CARD_API)); + + return payload.build(); + } + + String createAgreementPayload(CreateAgreementRequestParams params) { + return new JsonStringBuilder().add("reference", params.getReference()).build(); + } + + public void verifyCreateChargeConnectorRequest(String gatewayAccountId, CreateChargeRequestParams createChargeRequestParams) { + verifyCreateChargeConnectorRequest(gatewayAccountId, createChargePayload(createChargeRequestParams)); + } + + public void verifyCreateChargeConnectorRequest(String gatewayAccountId, String payload) { + wireMockClassRule.getAllServeEvents(); + wireMockClassRule.verify(1, + postRequestedFor(urlEqualTo(format(CONNECTOR_MOCK_CHARGES_PATH, gatewayAccountId))) + .withRequestBody(equalToJson(payload, true, true))); + } + + public void verifyCreateChargeConnectorRequestWithHeader(String gatewayAccountId, String payload, StringValuePattern idempotencyKey) { + wireMockClassRule.getAllServeEvents(); + wireMockClassRule.verify(1, + postRequestedFor(urlEqualTo(format(CONNECTOR_MOCK_CHARGES_PATH, gatewayAccountId))) + .withRequestBody(equalToJson(payload, true, true)) + .withHeader("Idempotency-Key", idempotencyKey)); + } + + public void verifyCreateAgreementConnectorRequest(String gatewayAccountId, CreateAgreementRequestParams createChargeRequestParams) { + verifyCreateAgreementConnectorRequest(gatewayAccountId, createAgreementPayload(createChargeRequestParams)); + } + + public void verifyCreateAgreementConnectorRequest(String gatewayAccountId, String payload) { + wireMockClassRule.getAllServeEvents(); + wireMockClassRule.verify(1, + postRequestedFor(urlEqualTo(format(CONNECTOR_MOCK_AGREEMENTS_PATH, gatewayAccountId))) + .withRequestBody(equalToJson(payload, true, true))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/ChargeResponseFromConnector.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/ChargeResponseFromConnector.java new file mode 100644 index 000000000..e9a07c886 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/ChargeResponseFromConnector.java @@ -0,0 +1,426 @@ +package uk.gov.pay.api.utils.mocks; + +import uk.gov.pay.api.model.AuthorisationSummary; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.PaymentSettlementSummary; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.RefundSummary; +import uk.gov.pay.api.model.telephone.PaymentOutcome; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class ChargeResponseFromConnector { + private final Long amount, corporateCardSurcharge, totalAmount; + private final PaymentState state; + private final String chargeId, returnUrl, description, reference, email, telephoneNumber, paymentProvider, gatewayTransactionId, processorId, providerId, authCode, createdDate, authorisedDate; + private final SupportedLanguage language; + private final PaymentOutcome paymentOutcome; + private final boolean delayedCapture; + private final boolean moto; + private final RefundSummary refundSummary; + private final PaymentSettlementSummary settlementSummary; + private final CardDetailsFromResponse cardDetails; + private final List> links; + private final Optional> metadata; + private final Long fee; + private final Long netAmount; + private final AuthorisationSummary authorisationSummary; + private final String agreementId; + private final AuthorisationMode authorisationMode; + private final String walletType; + + public Long getAmount() { + return amount; + } + + public Long getCorporateCardSurcharge() { + return corporateCardSurcharge; + } + + public Long getTotalAmount() { + return totalAmount; + } + + public PaymentState getState() { + return state; + } + + public String getChargeId() { + return chargeId; + } + + public String getReturnUrl() { + return returnUrl; + } + + public String getDescription() { + return description; + } + + public String getReference() { + return reference; + } + + public String getEmail() { + return email; + } + + public String getTelephoneNumber() { + return telephoneNumber; + } + + public String getPaymentProvider() { + return paymentProvider; + } + + public String getGatewayTransactionId() { + return gatewayTransactionId; + } + + public String getProcessorId() { + return processorId; + } + + public String getProviderId() { + return providerId; + } + + public String getAuthCode() { + return authCode; + } + + public String getCreatedDate() { + return createdDate; + } + + public String getAuthorisedDate() { + return authorisedDate; + } + + public SupportedLanguage getLanguage() { + return language; + } + + public PaymentOutcome getPaymentOutcome() { + return paymentOutcome; + } + + public boolean isDelayedCapture() { + return delayedCapture; + } + + public boolean isMoto() { + return moto; + } + + public RefundSummary getRefundSummary() { + return refundSummary; + } + + public PaymentSettlementSummary getSettlementSummary() { + return settlementSummary; + } + + public CardDetailsFromResponse getCardDetails() { + return cardDetails; + } + + public List> getLinks() { + return links; + } + + public Optional> getMetadata() { + return metadata; + } + + public Long getFee() { + return fee; + } + + public Long getNetAmount() { + return netAmount; + } + + public AuthorisationSummary getAuthorisationSummary() { + return authorisationSummary; + } + + public String getAgreementId() { + return this.agreementId; + } + + public AuthorisationMode getAuthorisationMode() { + return authorisationMode; + } + + public String getWalletType() { return walletType; } + + private ChargeResponseFromConnector(ChargeResponseFromConnectorBuilder builder) { + this.amount = builder.amount; + this.chargeId = builder.chargeId; + this.state = builder.state; + this.returnUrl = builder.returnUrl; + this.description = builder.description; + this.reference = builder.reference; + this.email = builder.email; + this.telephoneNumber = builder.telephoneNumber; + this.paymentProvider = builder.paymentProvider; + this.gatewayTransactionId = builder.gatewayTransactionId; + this.processorId = builder.processorId; + this.providerId = builder.providerId; + this.authCode = builder.authCode; + this.createdDate = builder.createdDate; + this.authorisedDate = builder.authorisedDate; + this.language = builder.language; + this.paymentOutcome = builder.paymentOutcome; + this.delayedCapture = builder.delayedCapture; + this.moto = builder.moto; + this.corporateCardSurcharge = builder.corporateCardSurcharge; + this.totalAmount = builder.totalAmount; + this.refundSummary = builder.refundSummary; + this.settlementSummary = builder.settlementSummary; + this.cardDetails = builder.cardDetails; + this.links = builder.links; + this.metadata = builder.metadata == null || builder.metadata.isEmpty() ? Optional.empty() : Optional.of(builder.metadata); + this.fee = builder.fee; + this.netAmount = builder.netAmount; + this.authorisationSummary = builder.authorisationSummary; + this.agreementId = builder.agreementId; + this.authorisationMode = builder.authorisationMode; + this.walletType = builder.walletType; + } + + public static final class ChargeResponseFromConnectorBuilder { + private Long amount, corporateCardSurcharge, totalAmount; + private PaymentState state; + private String chargeId, returnUrl, description, reference, email, telephoneNumber, paymentProvider, gatewayTransactionId, processorId, providerId, authCode, createdDate, authorisedDate; + private PaymentOutcome paymentOutcome; + private SupportedLanguage language; + private Boolean delayedCapture; + private boolean moto; + private RefundSummary refundSummary; + private PaymentSettlementSummary settlementSummary; + private CardDetailsFromResponse cardDetails; + private List> links = new ArrayList<>(); + private Map metadata = Map.of(); + private Long fee = null; + private Long netAmount = null; + private AuthorisationSummary authorisationSummary = null; + private String agreementId; + private AuthorisationMode authorisationMode = AuthorisationMode.WEB; + private String walletType = null; + + private ChargeResponseFromConnectorBuilder() { + } + + public static ChargeResponseFromConnectorBuilder aCreateOrGetChargeResponseFromConnector() { + return new ChargeResponseFromConnectorBuilder(); + } + + public static ChargeResponseFromConnectorBuilder aCreateOrGetChargeResponseFromConnector(ChargeResponseFromConnector responseFromConnector) { + return new ChargeResponseFromConnectorBuilder() + .withAmount(responseFromConnector.amount) + .withChargeId(responseFromConnector.chargeId) + .withState(responseFromConnector.state) + .withReturnUrl(responseFromConnector.returnUrl) + .withDescription(responseFromConnector.description) + .withReference(responseFromConnector.reference) + .withEmail(responseFromConnector.email) + .withPaymentProvider(responseFromConnector.paymentProvider) + .withGatewayTransactionId(responseFromConnector.gatewayTransactionId) + .withCreatedDate(responseFromConnector.createdDate) + .withLanguage(responseFromConnector.language) + .withDelayedCapture(responseFromConnector.delayedCapture) + .withMoto(responseFromConnector.moto) + .withCorporateCardSurcharge(responseFromConnector.corporateCardSurcharge) + .withTotalAmount(responseFromConnector.totalAmount) + .withRefundSummary(responseFromConnector.refundSummary) + .withSettlementSummary(responseFromConnector.settlementSummary) + .withCardDetails(responseFromConnector.cardDetails) + .withMetadata(responseFromConnector.metadata.orElse(null)) + .withNetAmount(responseFromConnector.getNetAmount()) + .withFee(responseFromConnector.getFee()) + .withAuthorisationSummary(responseFromConnector.getAuthorisationSummary()) + .withAgreementId(responseFromConnector.getAgreementId()) + .withAuthorisationMode(responseFromConnector.getAuthorisationMode()) + .withWalletType(responseFromConnector.walletType); + } + + public ChargeResponseFromConnectorBuilder withAmount(long amount) { + this.amount = amount; + return this; + } + + public ChargeResponseFromConnectorBuilder withChargeId(String chargeId) { + this.chargeId = chargeId; + return this; + } + + public ChargeResponseFromConnectorBuilder withState(PaymentState state) { + this.state = state; + return this; + } + + public ChargeResponseFromConnectorBuilder withReturnUrl(String returnUrl) { + this.returnUrl = returnUrl; + return this; + } + + public ChargeResponseFromConnectorBuilder withDescription(String description) { + this.description = description; + return this; + } + + public ChargeResponseFromConnectorBuilder withReference(String reference) { + this.reference = reference; + return this; + } + + public ChargeResponseFromConnectorBuilder withEmail(String email) { + this.email = email; + return this; + } + + public ChargeResponseFromConnectorBuilder withTelephoneNumber(String telephoneNumber) { + this.telephoneNumber = telephoneNumber; + return this; + } + + public ChargeResponseFromConnectorBuilder withPaymentProvider(String paymentProvider) { + this.paymentProvider = paymentProvider; + return this; + } + + public ChargeResponseFromConnectorBuilder withProcessorId(String processorId) { + this.processorId = processorId; + return this; + } + + public ChargeResponseFromConnectorBuilder withProviderId(String providerId) { + this.providerId = providerId; + return this; + } + + public ChargeResponseFromConnectorBuilder withGatewayTransactionId(String gatewayTransactionId) { + this.gatewayTransactionId = gatewayTransactionId; + return this; + } + + public ChargeResponseFromConnectorBuilder withAuthCode(String authCode) { + this.authCode = authCode; + return this; + } + + public ChargeResponseFromConnectorBuilder withCreatedDate(String createdDate) { + this.createdDate = createdDate; + return this; + } + + public ChargeResponseFromConnectorBuilder withAuthorisedDate(String authorisedDate) { + this.authorisedDate = authorisedDate; + return this; + } + + public ChargeResponseFromConnectorBuilder withLanguage(SupportedLanguage language) { + this.language = language; + return this; + } + + public ChargeResponseFromConnectorBuilder withPaymentOutcome(PaymentOutcome paymentOutcome) { + this.paymentOutcome = paymentOutcome; + return this; + } + + public ChargeResponseFromConnectorBuilder withDelayedCapture(boolean delayedCapture) { + this.delayedCapture = delayedCapture; + return this; + } + + public ChargeResponseFromConnectorBuilder withMoto(boolean moto) { + this.moto = moto; + return this; + } + + public ChargeResponseFromConnectorBuilder withCorporateCardSurcharge(Long corporateCardSurcharge) { + this.corporateCardSurcharge = corporateCardSurcharge; + return this; + } + + public ChargeResponseFromConnectorBuilder withTotalAmount(Long totalAmount) { + this.totalAmount = totalAmount; + return this; + } + + public ChargeResponseFromConnectorBuilder withRefundSummary(RefundSummary refundSummary) { + this.refundSummary = refundSummary; + return this; + } + + public ChargeResponseFromConnectorBuilder withSettlementSummary(PaymentSettlementSummary settlementSummary) { + this.settlementSummary = settlementSummary; + return this; + } + + public ChargeResponseFromConnectorBuilder withCardDetails(CardDetailsFromResponse cardDetails) { + this.cardDetails = cardDetails; + return this; + } + + public ChargeResponseFromConnector buildTelephoneChargeResponse() { + return new ChargeResponseFromConnector(this); + } + + public ChargeResponseFromConnector build() { + List.of(amount, chargeId, language, links).forEach(Objects::requireNonNull); + + return new ChargeResponseFromConnector(this); + } + + public ChargeResponseFromConnectorBuilder withLink(Map link) { + links.add(link); + return this; + } + + public ChargeResponseFromConnectorBuilder withMetadata(Map metadata) { + this.metadata = metadata; + return this; + } + + public ChargeResponseFromConnectorBuilder withFee(Long fee) { + this.fee = fee; + return this; + } + + public ChargeResponseFromConnectorBuilder withNetAmount(Long netAmount) { + this.netAmount = netAmount; + return this; + } + + public ChargeResponseFromConnectorBuilder withAuthorisationSummary(AuthorisationSummary authorisationSummary) { + this.authorisationSummary = authorisationSummary; + return this; + } + + public ChargeResponseFromConnectorBuilder withAgreementId(String agreementId) { + this.agreementId = agreementId; + return this; + } + + public ChargeResponseFromConnectorBuilder withAuthorisationMode(AuthorisationMode authorisationMode) { + this.authorisationMode = authorisationMode; + return this; + } + + public ChargeResponseFromConnectorBuilder withWalletType(String walletType) { + this.walletType = walletType; + return this; + } + } + +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/ConnectorMockClient.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/ConnectorMockClient.java new file mode 100644 index 000000000..84bc6237b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/ConnectorMockClient.java @@ -0,0 +1,674 @@ +package uk.gov.pay.api.utils.mocks; + +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.google.common.collect.ImmutableMap; +import com.google.gson.GsonBuilder; +import uk.gov.pay.api.it.fixtures.PaymentRefundJsonFixture; +import uk.gov.pay.api.it.fixtures.PaymentSingleResultBuilder; +import uk.gov.pay.api.model.Address; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.links.Link; +import uk.gov.pay.api.model.telephone.CreateTelephonePaymentRequest; +import uk.gov.pay.api.utils.JsonStringBuilder; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.ErrorIdentifier; +import uk.gov.service.payments.commons.model.SupportedLanguage; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static java.util.Optional.ofNullable; +import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; +import static javax.ws.rs.core.HttpHeaders.LOCATION; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.eclipse.jetty.http.HttpStatus.ACCEPTED_202; +import static org.eclipse.jetty.http.HttpStatus.BAD_REQUEST_400; +import static org.eclipse.jetty.http.HttpStatus.CONFLICT_409; +import static org.eclipse.jetty.http.HttpStatus.CREATED_201; +import static org.eclipse.jetty.http.HttpStatus.FORBIDDEN_403; +import static org.eclipse.jetty.http.HttpStatus.INTERNAL_SERVER_ERROR_500; +import static org.eclipse.jetty.http.HttpStatus.NOT_FOUND_404; +import static org.eclipse.jetty.http.HttpStatus.NO_CONTENT_204; +import static org.eclipse.jetty.http.HttpStatus.OK_200; +import static org.eclipse.jetty.http.HttpStatus.PRECONDITION_FAILED_412; +import static org.eclipse.jetty.http.HttpStatus.UNPROCESSABLE_ENTITY_422; +import static uk.gov.pay.api.it.PaymentsResourceGetIT.AWAITING_CAPTURE_REQUEST; +import static uk.gov.pay.api.it.fixtures.PaymentSingleResultBuilder.aSuccessfulSinglePayment; +import static uk.gov.pay.api.utils.mocks.AgreementResponseFromConnector.AgreementResponseFromConnectorBuilder.aCreateAgreementResponseFromConnector; +import static uk.gov.pay.api.utils.mocks.ChargeResponseFromConnector.ChargeResponseFromConnectorBuilder.aCreateOrGetChargeResponseFromConnector; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.ACCOUNT_NOT_LINKED_WITH_PSP; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AGREEMENT_NOT_ACTIVE; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AGREEMENT_NOT_FOUND; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.AUTHORISATION_API_NOT_ALLOWED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.CARD_NUMBER_IN_PAYMENT_LINK_REFERENCE_REJECTED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.GENERIC; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.IDEMPOTENCY_KEY_USED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.INCORRECT_AUTHORISATION_MODE_FOR_SAVE_PAYMENT_INSTRUMENT_TO_AGREEMENT; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.MOTO_NOT_ALLOWED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.REFUND_AMOUNT_AVAILABLE_MISMATCH; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.REFUND_NOT_AVAILABLE; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.TELEPHONE_PAYMENT_NOTIFICATIONS_NOT_ALLOWED; +import static uk.gov.service.payments.commons.model.ErrorIdentifier.ZERO_AMOUNT_NOT_ALLOWED; + +public class ConnectorMockClient extends BaseConnectorMockClient { + + private static final String CONNECTOR_MOCK_CHARGE_EVENTS_PATH = CONNECTOR_MOCK_CHARGE_PATH + "/events"; + private static final String CONNECTOR_MOCK_CHARGE_REFUNDS_PATH = CONNECTOR_MOCK_CHARGE_PATH + "/refunds"; + private static final String CONNECTOR_MOCK_CHARGE_REFUND_BY_ID_PATH = CONNECTOR_MOCK_CHARGE_REFUNDS_PATH + "/%s"; + private static final DateFormat SDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + public static final String VALID_AGREEMENT_ID = "12345678901234567890123456"; + + public ConnectorMockClient(WireMockClassRule connectorMock) { + super(connectorMock); + } + + private String buildChargeResponse(ChargeResponseFromConnector responseFromConnector) { + PaymentSingleResultBuilder resultBuilder = aSuccessfulSinglePayment() + .withChargeId(responseFromConnector.getChargeId()) + .withAmount(responseFromConnector.getAmount()) + .withMatchingReference(responseFromConnector.getReference()) + .withEmail(responseFromConnector.getEmail()) + .withDescription(responseFromConnector.getDescription()) + .withState(responseFromConnector.getState()) + .withReturnUrl(responseFromConnector.getReturnUrl()) + .withCreatedDate(responseFromConnector.getCreatedDate()) + .withLanguage(responseFromConnector.getLanguage()) + .withPaymentProvider(responseFromConnector.getPaymentProvider()) + .withDelayedCapture(responseFromConnector.isDelayedCapture()) + .withMoto(responseFromConnector.isMoto()) + .withAgreementId(responseFromConnector.getAgreementId()) + .withLinks(responseFromConnector.getLinks()) + .withSettlementSummary(responseFromConnector.getSettlementSummary()) + .withAuthorisationMode(responseFromConnector.getAuthorisationMode()); + + ofNullable(responseFromConnector.getCardDetails()).ifPresent(resultBuilder::withCardDetails); + ofNullable(responseFromConnector.getRefundSummary()).ifPresent(resultBuilder::withRefundSummary); + ofNullable(responseFromConnector.getGatewayTransactionId()).ifPresent(resultBuilder::withGatewayTransactionId); + ofNullable(responseFromConnector.getCorporateCardSurcharge()).ifPresent(resultBuilder::withCorporateCardSurcharge); + ofNullable(responseFromConnector.getTotalAmount()).ifPresent(resultBuilder::withTotalAmount); + ofNullable(responseFromConnector.getFee()).ifPresent(resultBuilder::withFee); + ofNullable(responseFromConnector.getNetAmount()).ifPresent(resultBuilder::withNetAmount); + ofNullable(responseFromConnector.getAuthorisationSummary()).ifPresent(resultBuilder::withAuthorisationSummary); + ofNullable(responseFromConnector.getWalletType()).ifPresent(resultBuilder::withWalletType); + responseFromConnector.getMetadata().ifPresent(resultBuilder::withMetadata); + + return resultBuilder.build(); + } + + private String buildAgreementResponse(AgreementResponseFromConnector responseFromConnector) { + return new JsonStringBuilder() + .add("reference", responseFromConnector.getReference()) + .add("agreement_id", responseFromConnector.getAgreementId()) + .add("service_id", responseFromConnector.getServiceId()) + .add("created_date", responseFromConnector.getCreatedDate()) + .add("live", responseFromConnector.isLive()).build(); + } + + private String buildTelephoneChargeResponse(ChargeResponseFromConnector responseFromConnector) { + List> links = new ArrayList<>(); + JsonStringBuilder request = new JsonStringBuilder() + .add("amount", responseFromConnector.getAmount()) + .add("reference", responseFromConnector.getReference()) + .add("links", links) + .add("description", responseFromConnector.getDescription()) + .add("processor_id", responseFromConnector.getProcessorId()) + .add("provider_id", responseFromConnector.getProviderId()) + .add("charge_id", responseFromConnector.getChargeId()) + .add("delayed_capture", false) + .addToMap("state", "finished", responseFromConnector.getState().isFinished()) + .addToMap("state", "status", responseFromConnector.getState().getStatus()) + .addToMap("card_details", "card_brand", responseFromConnector.getCardDetails().getCardBrand()) + .addToMap("card_details", "expiry_date", responseFromConnector.getCardDetails().getExpiryDate()) + .addToMap("card_details", "last_digits_card_number", responseFromConnector.getCardDetails().getLastDigitsCardNumber()) + .addToMap("card_details", "first_digits_card_number", responseFromConnector.getCardDetails().getFirstDigitsCardNumber()) + .addToMap("payment_outcome", "status", responseFromConnector.getPaymentOutcome().getStatus()); + + ofNullable(responseFromConnector.getAuthCode()).ifPresent(authCode -> request.add("auth_code", authCode)); + ofNullable(responseFromConnector.getCreatedDate()).ifPresent(createdDate -> request.add("created_date", createdDate)); + ofNullable(responseFromConnector.getAuthorisedDate()).ifPresent(authorisedDate -> request.add("authorised_date", authorisedDate)); + ofNullable(responseFromConnector.getEmail()).ifPresent(email -> request.add("email", email)); + ofNullable(responseFromConnector.getTelephoneNumber()).ifPresent(telephoneNumber -> request.add("telephone_number", telephoneNumber)); + ofNullable(responseFromConnector.getCardDetails().getCardHolderName()).ifPresent(cardHolderName -> request.addToMap("card_details", "cardholder_name", cardHolderName)); + + return request.build(); + } + + private String buildGetRefundResponse(String refundId, int amount, int refundAmountAvailable, String status, String createdDate) { + List> links = new ArrayList<>(); + links.add(ImmutableMap.of("self", new Link("http://server:port/self-link"))); + links.add(ImmutableMap.of("payment", new Link("http://server:port/payment-link"))); + + JsonStringBuilder jsonStringBuilder = new JsonStringBuilder() + .add("refund_id", refundId) + .add("amount", amount) + .add("refund_amount_available", refundAmountAvailable) + .add("status", status) + .add("created_date", createdDate) + .add("_links", links); + + return jsonStringBuilder.build(); + } + + private String buildChargeEventsResponse(String chargeId, List> events, ImmutableMap... links) { + return new JsonStringBuilder() + .add("charge_id", chargeId) + .add("events", events) + .add("links", asList(links)) + .build(); + } + + @Override + String nextUrlPost() { + return "http://frontend_card/charge/"; + } + + private String chargeEventsLocation(String accountId, String chargeId) { + return format(CONNECTOR_MOCK_CHARGE_EVENTS_PATH, accountId, chargeId); + } + + public void respondCreated_whenCreateTelephoneCharge(String gatewayAccountId, CreateTelephonePaymentRequest requestParams) { + var responseFromConnector = aCreateOrGetChargeResponseFromConnector() + .withAmount(requestParams.getAmount()) + .withDescription(requestParams.getDescription()) + .withReference(requestParams.getReference()) + .withProcessorId(requestParams.getProcessorId()) + .withProviderId(requestParams.getProviderId()) + .withCardDetails(new CardDetailsFromResponse( + requestParams.getLastFourDigits().orElse(null), + requestParams.getFirstSixDigits().orElse(null), + requestParams.getNameOnCard().orElse(null), + requestParams.getCardExpiry().orElse(null), + null, + requestParams.getCardType().orElse(null), + null + ) + ) + .withPaymentOutcome(requestParams.getPaymentOutcome()) + .withChargeId("dummypaymentid123notpersisted") + .withDelayedCapture(false) + .withState(new PaymentState("success", true, null, null)); + + requestParams.getCreatedDate().ifPresent(responseFromConnector::withCreatedDate); + requestParams.getAuthorisedDate().ifPresent(responseFromConnector::withAuthorisedDate); + requestParams.getEmailAddress().ifPresent(responseFromConnector::withEmail); + requestParams.getTelephoneNumber().ifPresent(responseFromConnector::withTelephoneNumber); + requestParams.getAuthCode().ifPresent(responseFromConnector::withAuthCode); + + mockCreateTelephoneCharge(gatewayAccountId, aResponse() + .withStatus(CREATED_201) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(buildTelephoneChargeResponse(responseFromConnector.buildTelephoneChargeResponse())) + + ); + } + + public void respondOk_whenCreateTelephoneCharge(String gatewayAccountId, CreateTelephonePaymentRequest requestParams) { + var responseFromConnector = aCreateOrGetChargeResponseFromConnector() + .withAmount(requestParams.getAmount()) + .withDescription(requestParams.getDescription()) + .withReference(requestParams.getReference()) + .withProcessorId(requestParams.getProcessorId()) + .withProviderId(requestParams.getProviderId()) + .withCardDetails(new CardDetailsFromResponse( + requestParams.getLastFourDigits().orElse(null), + requestParams.getFirstSixDigits().orElse(null), + requestParams.getNameOnCard().orElse(null), + requestParams.getCardExpiry().orElse(null), + null, + requestParams.getCardType().orElse(null), + null + ) + ) + .withPaymentOutcome(requestParams.getPaymentOutcome()) + .withChargeId("dummypaymentid123notpersisted") + .withDelayedCapture(false) + .withState(new PaymentState("success", true, null, null)); + + requestParams.getCreatedDate().ifPresent(responseFromConnector::withCreatedDate); + requestParams.getAuthorisedDate().ifPresent(responseFromConnector::withAuthorisedDate); + requestParams.getEmailAddress().ifPresent(responseFromConnector::withEmail); + requestParams.getTelephoneNumber().ifPresent(responseFromConnector::withTelephoneNumber); + requestParams.getAuthCode().ifPresent(responseFromConnector::withAuthCode); + + mockCreateTelephoneCharge(gatewayAccountId, aResponse() + .withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(buildTelephoneChargeResponse(responseFromConnector.buildTelephoneChargeResponse())) + ); + } + + public void respondCreated_whenCreateCharge(String gatewayAccountId, CreateChargeRequestParams requestParams) { + + var responseFromConnector = aCreateOrGetChargeResponseFromConnector() + .withAmount(requestParams.getAmount()) + .withChargeId("chargeId") + .withState(new PaymentState("created", false, null, null)) + .withReturnUrl(requestParams.getReturnUrl()) + .withDescription(requestParams.getDescription()) + .withReference(requestParams.getReference()) + .withPaymentProvider("Sandbox") + .withGatewayTransactionId("gatewayTransactionId") + .withCreatedDate(SDF.format(new Date())) + .withLanguage(SupportedLanguage.ENGLISH) + .withDelayedCapture(false) + .withCardDetails(new CardDetailsFromResponse("1234", "123456", "Mr. Payment", "12/19", null, "Mastercard", "debit")) + .withLink(validGetLink(chargeLocation(gatewayAccountId, "chargeId"), "self")) + .withLink(validGetLink(nextUrl("chargeTokenId"), "next_url")) + .withLink(validPostLink(nextUrlPost(), "next_url_post", "application/x-www-form-urlencoded", getChargeIdTokenMap("chargeTokenId", false))); + + requestParams.getSetUpAgreement().ifPresent(responseFromConnector::withAgreementId); + + if (!requestParams.getMetadata().isEmpty()) + responseFromConnector.withMetadata(requestParams.getMetadata()); + + if (requestParams.getEmail() != null) { + responseFromConnector.withEmail(requestParams.getEmail()); + } + + if (requestParams.getCardholderName().isPresent() || requestParams.getAddressLine1().isPresent() || + requestParams.getAddressLine2().isPresent() || requestParams.getAddressPostcode().isPresent() || + requestParams.getAddressCity().isPresent() || requestParams.getAddressCountry().isPresent()) { + Address billingAddress = new Address(requestParams.getAddressLine1().orElse(null), requestParams.getAddressLine2().orElse(null), + requestParams.getAddressPostcode().orElse(null), requestParams.getAddressCity().orElse(null), requestParams.getAddressCountry().orElse(null)); + CardDetailsFromResponse cardDetails = new CardDetailsFromResponse(null, null, requestParams.getCardholderName().orElse(null), + null, billingAddress, null, null); + responseFromConnector.withCardDetails(cardDetails); + } + + mockCreateCharge(gatewayAccountId, aResponse() + .withStatus(CREATED_201) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withHeader(LOCATION, chargeLocation(gatewayAccountId, "chargeId")) + .withBody(buildChargeResponse(responseFromConnector.build()))); + } + + public void respondCreated_whenCreateCharge(String chargeTokenId, String gatewayAccountId, ChargeResponseFromConnector responseFromConnector) { + ChargeResponseFromConnector build = aCreateOrGetChargeResponseFromConnector(responseFromConnector) + .withLink(validGetLink(chargeLocation(gatewayAccountId, responseFromConnector.getChargeId()), "self")) + .withLink(validGetLink(nextUrl(chargeTokenId), "next_url")) + .withLink(validPostLink(nextUrlPost(), "next_url_post", "application/x-www-form-urlencoded", getChargeIdTokenMap(chargeTokenId, false))).build(); + + mockCreateCharge(gatewayAccountId, + aResponse().withStatus(CREATED_201) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withHeader(LOCATION, chargeLocation(gatewayAccountId, responseFromConnector.getChargeId())) + .withBody(buildChargeResponse(build))); + } + + public void respondOK_whenCreateChargeIdempotencyKey(String chargeTokenId, String gatewayAccountId, ChargeResponseFromConnector responseFromConnector) { + ChargeResponseFromConnector build = aCreateOrGetChargeResponseFromConnector(responseFromConnector) + .withLink(validGetLink(chargeLocation(gatewayAccountId, responseFromConnector.getChargeId()), "self")) + .build(); + + mockCreateCharge(gatewayAccountId, + aResponse().withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withHeader(LOCATION, chargeLocation(gatewayAccountId, responseFromConnector.getChargeId())) + .withBody(buildChargeResponse(build))); + } + + public void respondCreated_whenCreateCharge_withAuthorisationMode_MotoApi(String chargeTokenId, String gatewayAccountId, ChargeResponseFromConnector responseFromConnector) { + ChargeResponseFromConnector build = aCreateOrGetChargeResponseFromConnector(responseFromConnector) + .withMoto(true) + .withLink(validGetLink(chargeLocation(gatewayAccountId, responseFromConnector.getChargeId()), "self")) + .withLink(validPostLink(CONNECTOR_MOCK_AUTHORISATION_PATH, "auth_url_post", "application/json", getChargeIdTokenMap(chargeTokenId, true))).build(); + + mockCreateCharge(gatewayAccountId, + aResponse().withStatus(CREATED_201) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withHeader(LOCATION, chargeLocation(gatewayAccountId, responseFromConnector.getChargeId())) + .withBody(buildChargeResponse(build))); + } + + public void respondCreated_whenCreateCharge_withAuthorisationMode_Agreement(String chargeTokenId, String gatewayAccountId, ChargeResponseFromConnector responseFromConnector) { + ChargeResponseFromConnector build = aCreateOrGetChargeResponseFromConnector(responseFromConnector) + .withAuthorisationMode(AuthorisationMode.AGREEMENT) + .withLink(validGetLink(chargeLocation(gatewayAccountId, responseFromConnector.getChargeId()), "self")) + .build(); + + mockCreateCharge(gatewayAccountId, + aResponse().withStatus(CREATED_201) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withHeader(LOCATION, chargeLocation(gatewayAccountId, responseFromConnector.getChargeId())) + .withBody(buildChargeResponse(build))); + } + + public void respondCreated_whenCreateAgreement(String gatewayAccountId, CreateAgreementRequestParams requestParams) { + var responseFromConnector = aCreateAgreementResponseFromConnector() + .withReference(requestParams.getReference()) + .withAgreementId(VALID_AGREEMENT_ID) + .withServiceId("service-id") + .withCreatedDate("created-date") + .withLive(true); + mockCreateAgreement(gatewayAccountId, aResponse() + .withStatus(CREATED_201) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(buildAgreementResponse(responseFromConnector.build()))); + } + + public void respondOk_whenCancelAgreement(String agreementId, String accountId) { + whenCancelAgreement(agreementId, accountId, aResponse().withStatus(NO_CONTENT_204)); + } + + public void respondAgreementNotFound_WhenCancelAgreement(String agreementId, String accountId, String errorMsg) { + respond_WhenCancelAgreement(agreementId, accountId, errorMsg, NOT_FOUND_404, AGREEMENT_NOT_FOUND); + } + + public void respondAgreementNotActive_WhenCancelAgreement(String agreementId, String accountId, String errorMsg) { + respond_WhenCancelAgreement(agreementId, accountId, errorMsg, BAD_REQUEST_400, AGREEMENT_NOT_ACTIVE); + } + + public void respondError_WhenCancelAgreement(String agreementId, String accountId, String errorMsg) { + respond_WhenCancelAgreement(agreementId, accountId, errorMsg, INTERNAL_SERVER_ERROR_500, GENERIC); + } + + public void mockCreateTelephoneCharge(String gatewayAccountId, ResponseDefinitionBuilder responseDefinitionBuilder) { + wireMockClassRule.stubFor(post(urlPathEqualTo(format(CONNECTOR_MOCK_TELEPHONE_CHARGES_PATH, gatewayAccountId))) + .withHeader(CONTENT_TYPE, matching(APPLICATION_JSON)).willReturn(responseDefinitionBuilder)); + } + + public void respondAccepted_whenCreateARefund(int amount, int refundAmountAvailable, String gatewayAccountId, String chargeId, String refundId, String status, String createdDate) { + whenCreateRefund(gatewayAccountId, chargeId, aResponse() + .withStatus(ACCEPTED_202) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(buildGetRefundResponse(refundId, amount, refundAmountAvailable, status, createdDate))); + } + + public void respondNotFound_whenCreateCharge(String gatewayAccountId) { + mockCreateCharge(gatewayAccountId, aResponse().withStatus(NOT_FOUND_404)); + } + + public void respondBadRequest_whenCreateCharge(String gatewayAccountId, String errorMsg) { + mockCreateCharge(gatewayAccountId, withStatusAndErrorMessage(BAD_REQUEST_400, errorMsg, GENERIC)); + } + + public void respondBadRequest_whenCreateAgreement(String gatewayAccountId, String errorMsg) { + mockCreateAgreement(gatewayAccountId, withStatusAndErrorMessage(BAD_REQUEST_400, errorMsg, GENERIC)); + } + + public void respondAgreementNotFound_whenCreateCharge(String gatewayAccountId, String agreementId, String errorMsg) { + mockCreateCharge(gatewayAccountId, withStatusAndErrorMessage(NOT_FOUND_404, format(errorMsg, agreementId), AGREEMENT_NOT_FOUND)); + } + + public void respondIncorrectAuthorisationModeForSavePaymentInstrumentToAgreement_whenCreateCharge(String gatewayAccountId, String agreementId, String errorMsg) { + mockCreateCharge(gatewayAccountId, withStatusAndErrorMessage(BAD_REQUEST_400, format(errorMsg, agreementId), INCORRECT_AUTHORISATION_MODE_FOR_SAVE_PAYMENT_INSTRUMENT_TO_AGREEMENT)); + } + + public void respondPreconditionFailed_whenCreateRefund(String gatewayAccountId, String errorMsg, String chargeId) { + whenCreateRefund(gatewayAccountId, chargeId, withStatusAndErrorMessage(PRECONDITION_FAILED_412, errorMsg, REFUND_AMOUNT_AVAILABLE_MISMATCH)); + } + + public void respondZeroAmountNotAllowed(String gatewayAccountId) { + mockCreateCharge(gatewayAccountId, withStatusAndErrorMessage(UNPROCESSABLE_ENTITY_422, "anything", ZERO_AMOUNT_NOT_ALLOWED)); + } + + public void respondMotoPaymentNotAllowed(String gatewayAccountId) { + mockCreateCharge(gatewayAccountId, withStatusAndErrorMessage(UNPROCESSABLE_ENTITY_422, "anything", MOTO_NOT_ALLOWED)); + } + + public void respondAuthorisationApiNotAllowed(String gatewayAccountId) { + mockCreateCharge(gatewayAccountId, withStatusAndErrorMessage(UNPROCESSABLE_ENTITY_422, "anything", AUTHORISATION_API_NOT_ALLOWED)); + } + + public void respondTelephoneNotificationsNotEnabled(String gatewayAccountId) { + mockCreateTelephoneCharge(gatewayAccountId, withStatusAndErrorMessage(FORBIDDEN_403, "anything", TELEPHONE_PAYMENT_NOTIFICATIONS_NOT_ALLOWED)); + } + + public void respondGatewayAccountCredentialNotConfigured(String gatewayAccountId) { + mockCreateCharge(gatewayAccountId, withStatusAndErrorMessage(BAD_REQUEST_400, "Payment provider details are not configured on this account", ACCOUNT_NOT_LINKED_WITH_PSP)); + } + + public void respondCardNumberInReferenceError(String gatewayAccountId) { + mockCreateCharge(gatewayAccountId, withStatusAndErrorMessage(BAD_REQUEST_400, "Card number entered in a payment link reference", CARD_NUMBER_IN_PAYMENT_LINK_REFERENCE_REJECTED)); + } + + public void respondWithChargeFound(String chargeTokenId, String gatewayAccountId, ChargeResponseFromConnector chargeResponseFromConnector) { + respondWithChargeFound(chargeTokenId, gatewayAccountId, chargeResponseFromConnector, false); + } + + public void respondWithChargeFound(String chargeTokenId, String gatewayAccountId, ChargeResponseFromConnector chargeResponseFromConnector, boolean isMotoApi) { + String chargeResponseBody; + String chargeId = chargeResponseFromConnector.getChargeId(); + + var responseFromConnector = aCreateOrGetChargeResponseFromConnector(chargeResponseFromConnector) + .withLink(validGetLink(chargeLocation(gatewayAccountId, chargeId), "self")) + .withLink(validGetLink(chargeLocation(gatewayAccountId, chargeId) + "/refunds", "refunds")); + + if (AWAITING_CAPTURE_REQUEST == chargeResponseFromConnector.getState()) { + responseFromConnector + .withLink(validPostLink(chargeLocation(gatewayAccountId, chargeId) + "/capture", "capture", "application/x-www-form-urlencoded", new HashMap<>())) + .build(); + } else { + if (isMotoApi) { + responseFromConnector + .withLink(validPostLink(CONNECTOR_MOCK_AUTHORISATION_PATH, "auth_url_post", "application/json", getChargeIdTokenMap(chargeTokenId, true))) + .build(); + } else { + responseFromConnector + .withLink(validGetLink(nextUrl(chargeId), "next_url")) + .withLink(validPostLink(nextUrlPost(), "next_url_post", "application/x-www-form-urlencoded", getChargeIdTokenMap(chargeTokenId, false))) + .build(); + } + } + + chargeResponseBody = buildChargeResponse(responseFromConnector.build()); + whenGetCharge(gatewayAccountId, chargeId, aResponse() + .withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(chargeResponseBody)); + } + + public void respondWithGetRefundById(String gatewayAccountId, String chargeId, String refundId, int amount, int totalRefundAmountAvailable, String refundStatus, String createdDate) { + String refundResponse = buildGetRefundResponse(refundId, amount, totalRefundAmountAvailable, refundStatus, createdDate); + whenGetRefundById(gatewayAccountId, chargeId, refundId, aResponse() + .withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(refundResponse)); + } + + public void respondWithGetAllRefunds(String gatewayAccountId, String chargeId, PaymentRefundJsonFixture... refunds) { + + Map> refundList = new HashMap<>(); + refundList.put("refunds", Arrays.asList(refunds)); + + List> links = new ArrayList<>(); + links.add(ImmutableMap.of("self", new Link("http://server:port/self-link"))); + links.add(ImmutableMap.of("payment", new Link("http://server:port/payment-link"))); + + JsonStringBuilder embedded = new JsonStringBuilder().noPrettyPrint(); + embedded.add("refunds", refundList); + + JsonStringBuilder jsonStringBuilder = new JsonStringBuilder() + .add("payment_id", chargeId) + .add("_links", links) + .add("_embedded", refundList); + + whenGetAllRefunds(gatewayAccountId, chargeId, aResponse() + .withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(jsonStringBuilder.build())); + } + + public void respondRefundNotFound(String gatewayAccountId, String chargeId, String refundId) { + whenGetRefundById(gatewayAccountId, chargeId, refundId, + withStatusAndErrorMessage(BAD_REQUEST_400, String.format("Refund with id [%s] not found.", refundId), GENERIC)); + + } + + public void respondRefundWithError(String gatewayAccountId, String chargeId, String refundId) { + whenGetRefundById(gatewayAccountId, chargeId, refundId, + withStatusAndErrorMessage(INTERNAL_SERVER_ERROR_500, "server error", GENERIC)); + + } + + public void respondWithChargeEventsFound(String gatewayAccountId, String chargeId, List> events) { + whenGetChargeEvents(gatewayAccountId, chargeId, aResponse() + .withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(buildChargeEventsResponse(chargeId, events, validGetLink(chargeEventsLocation(gatewayAccountId, chargeId), "self")))); + } + + + public void respondChargeNotFound(String gatewayAccountId, String chargeId, String errorMsg) { + respondWhenGetCharge(gatewayAccountId, chargeId, errorMsg, NOT_FOUND_404); + } + + public void respondWhenGetCharge(String gatewayAccountId, String chargeId, String errorMsg, int status) { + whenGetCharge(gatewayAccountId, chargeId, withStatusAndErrorMessage(status, errorMsg, GENERIC)); + } + + public void respondChargeEventsNotFound(String gatewayAccountId, String chargeId, String errorMsg) { + respondWhenGetChargeEvents(gatewayAccountId, chargeId, errorMsg, NOT_FOUND_404); + } + + public void respondWhenGetChargeEvents(String gatewayAccountId, String chargeId, String errorMsg, int status) { + whenGetChargeEvents(gatewayAccountId, chargeId, withStatusAndErrorMessage(status, errorMsg, GENERIC)); + } + + public void respondOk_whenCancelCharge(String paymentId, String accountId) { + whenCancelCharge(paymentId, accountId, aResponse().withStatus(NO_CONTENT_204)); + } + + public void respondOk_whenCaptureCharge(String paymentId, String accountId) { + whenCaptureCharge(paymentId, accountId, aResponse().withStatus(NO_CONTENT_204)); + } + + public void respondChargeNotFound_WhenCancelCharge(String paymentId, String accountId, String errorMsg) { + respond_WhenCancelCharge(paymentId, accountId, errorMsg, NOT_FOUND_404); + } + + public void respondConflict_whenCreateChargeAndIdempotencyKeyUSed(String gatewayAccountId) { + mockCreateCharge(gatewayAccountId, withStatusAndErrorMessage(CONFLICT_409, + "The Idempotency-Key has already been used to create a payment", IDEMPOTENCY_KEY_USED, null)); + } + + public void respondChargeNotFound_WhenCaptureCharge(String paymentId, String accountId, String errorMsg) { + respond_WhenCaptureCharge(paymentId, accountId, errorMsg, NOT_FOUND_404); + } + + public void respondBadRequest_WhenCaptureCharge(String paymentId, String accountId, String errorMessage) { + respond_WhenCaptureCharge(paymentId, accountId, errorMessage, BAD_REQUEST_400); + } + + public void respond_WhenCancelCharge(String paymentId, String accountId, String errorMessage, int status) { + whenCancelCharge(paymentId, accountId, withStatusAndErrorMessage(status, errorMessage, GENERIC, null)); + } + + public void respond_WhenCaptureCharge(String paymentId, String accountId, String errorMessage, int status) { + whenCaptureCharge(paymentId, accountId, withStatusAndErrorMessage(status, errorMessage, GENERIC, null)); + } + + public void respond_WhenCancelAgreement(String paymentId, String accountId, String errorMessage, int status, ErrorIdentifier errorIdentifier) { + whenCancelAgreement(paymentId, accountId, withStatusAndErrorMessage(status, errorMessage, errorIdentifier, null)); + } + + public void mockCreateCharge(String gatewayAccountId, ResponseDefinitionBuilder responseDefinitionBuilder) { + wireMockClassRule.stubFor(post(urlPathEqualTo(format(CONNECTOR_MOCK_CHARGES_PATH, gatewayAccountId))) + .withHeader(CONTENT_TYPE, matching(APPLICATION_JSON)).willReturn(responseDefinitionBuilder)); + } + + public void mockCreateAgreement(String gatewayAccountId, ResponseDefinitionBuilder responseDefinitionBuilder) { + wireMockClassRule.stubFor(post(urlPathEqualTo(format(CONNECTOR_MOCK_AGREEMENTS_PATH, gatewayAccountId))) + .withHeader(CONTENT_TYPE, matching(APPLICATION_JSON)).willReturn(responseDefinitionBuilder)); + } + + public void whenCancelAgreement(String agreementId, String gatewayAccountId, ResponseDefinitionBuilder responseDefinitionBuilder) { + wireMockClassRule.stubFor(post(urlPathEqualTo(connectorCancelAgreementPathFor(agreementId, gatewayAccountId))).willReturn(responseDefinitionBuilder)); + } + + private void whenCreateRefund(String gatewayAccountId, String chargeId, ResponseDefinitionBuilder response) { + wireMockClassRule.stubFor(post(urlPathEqualTo(format(CONNECTOR_MOCK_CHARGE_REFUNDS_PATH, gatewayAccountId, chargeId))) + .willReturn(response)); + } + + private void whenGetRefundById(String gatewayAccountId, String chargeId, String refundId, ResponseDefinitionBuilder response) { + wireMockClassRule.stubFor(get(urlPathEqualTo(format(CONNECTOR_MOCK_CHARGE_REFUND_BY_ID_PATH, gatewayAccountId, chargeId, refundId))) + .willReturn(response)); + } + + private void whenGetAllRefunds(String gatewayAccountId, String chargeId, ResponseDefinitionBuilder response) { + wireMockClassRule.stubFor(get(urlPathEqualTo(format(CONNECTOR_MOCK_CHARGE_REFUNDS_PATH, gatewayAccountId, chargeId))) + .willReturn(response)); + } + + private void whenGetChargeEvents(String gatewayAccountId, String chargeId, ResponseDefinitionBuilder response) { + wireMockClassRule.stubFor(get(urlPathEqualTo(format(CONNECTOR_MOCK_CHARGE_EVENTS_PATH, gatewayAccountId, chargeId))) + .willReturn(response)); + } + + private void whenCancelCharge(String paymentId, String accountId, ResponseDefinitionBuilder response) { + wireMockClassRule.stubFor(post(urlPathEqualTo(connectorCancelChargePathFor(paymentId, accountId))).willReturn(response)); + } + + private void whenCaptureCharge(String paymentId, String accountId, ResponseDefinitionBuilder response) { + wireMockClassRule.stubFor(post(urlPathEqualTo(connectorCaptureChargePathFor(paymentId, accountId))).willReturn(response)); + } + + private String connectorCancelChargePathFor(String paymentId, String accountId) { + return format(CONNECTOR_MOCK_CHARGE_PATH + "/cancel", accountId, paymentId); + } + + private String connectorCaptureChargePathFor(String paymentId, String accountId) { + return format(CONNECTOR_MOCK_CHARGE_PATH + "/capture", accountId, paymentId); + } + + private String connectorCancelAgreementPathFor(String agreementId, String accountId) { + return format(CONNECTOR_MOCK_AGREEMENT_PATH + "/cancel", accountId, agreementId); + } + + private ResponseDefinitionBuilder withStatusAndErrorMessage(int statusCode, String errorMsg, ErrorIdentifier errorIdentifier) { + return withStatusAndErrorMessage(statusCode, errorMsg, errorIdentifier, null); + } + + private ResponseDefinitionBuilder withStatusAndErrorMessage(int statusCode, String errorMsg, ErrorIdentifier errorIdentifier, String reason) { + Map payload = new HashMap<>(); + payload.put("message", List.of(errorMsg)); + payload.put("error_identifier", errorIdentifier.toString()); + if (reason != null) { + payload.put("reason", reason); + } + + return aResponse() + .withStatus(statusCode) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(new GsonBuilder().create().toJson(payload)); + } + + //"Gson can not automatically deserialize the pure inner classes since their no-args constructor" + private Map getChargeIdTokenMap(String chargeTokenId, boolean isMotoApi) { + final Map chargeTokenIdMap = new HashMap<>(); + chargeTokenIdMap.put(isMotoApi ? "one_time_token" : "chargeTokenId", chargeTokenId); + return chargeTokenIdMap; + } + + public void verifyCancelCharge(String paymentId, String accountId) { + wireMockClassRule.verify(1, postRequestedFor(urlEqualTo(connectorCancelChargePathFor(paymentId, accountId)))); + } + + public void verifyCaptureCharge(String paymentId, String accountId) { + wireMockClassRule.verify(1, postRequestedFor(urlEqualTo(connectorCaptureChargePathFor(paymentId, accountId)))); + } + + public void respondBadRequest_whenCreateARefund(String reason, String gatewayAccountId, String chargeId) { + whenCreateRefund(gatewayAccountId, chargeId, + withStatusAndErrorMessage(BAD_REQUEST_400, + "A message that should be completely ignored (only log)", REFUND_NOT_AVAILABLE, reason)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/CreateAgreementRequestParams.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/CreateAgreementRequestParams.java new file mode 100644 index 000000000..11fa0aba3 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/CreateAgreementRequestParams.java @@ -0,0 +1,61 @@ +package uk.gov.pay.api.utils.mocks; + +import java.util.List; +import java.util.Objects; + +public class CreateAgreementRequestParams { + private final String reference; + private final String description; + private final String userIdentifier; + + private CreateAgreementRequestParams(CreateAgreementRequestParamsBuilder builder) { + this.reference = builder.reference; + this.description = builder.description; + this.userIdentifier = builder.userIdentifier; + } + + public String getReference() { + return reference; + } + + public String getDescription() { + return description; + } + + public String getUserIdentifier() { + return userIdentifier; + } + + public static final class CreateAgreementRequestParamsBuilder { + private String reference; + private String description; + private String userIdentifier; + + private CreateAgreementRequestParamsBuilder() { + } + + public static CreateAgreementRequestParamsBuilder aCreateAgreementRequestParams() { + return new CreateAgreementRequestParamsBuilder(); + } + + public CreateAgreementRequestParamsBuilder withReference(String reference) { + this.reference = reference; + return this; + } + + public CreateAgreementRequestParamsBuilder withDescription(String description) { + this.description = description; + return this; + } + + public CreateAgreementRequestParamsBuilder withUserIdentifier(String userIdentifier) { + this.userIdentifier = userIdentifier; + return this; + } + + public CreateAgreementRequestParams build() { + List.of(reference).forEach(Objects::requireNonNull); + return new CreateAgreementRequestParams(this); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/CreateChargeRequestParams.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/CreateChargeRequestParams.java new file mode 100644 index 000000000..fb3ca28bd --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/CreateChargeRequestParams.java @@ -0,0 +1,246 @@ +package uk.gov.pay.api.utils.mocks; + +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.Source; +import uk.gov.service.payments.commons.model.SupportedLanguage; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class CreateChargeRequestParams { + + private final int amount; + private final String returnUrl, description, reference; + private final Map metadata; + private final Boolean moto; + private final String email; + private final String cardholderName; + private final String addressLine1; + private final String addressLine2; + private final String addressPostcode; + private final String addressCity; + private final String addressCountry; + private final SupportedLanguage language; + private final Source source; + private final String setUpAgreement; + private final AuthorisationMode authorisationMode; + private final String agreementId; + + private CreateChargeRequestParams(CreateChargeRequestParamsBuilder builder) { + this.amount = builder.amount; + this.returnUrl = builder.returnUrl; + this.description = builder.description; + this.reference = builder.reference; + this.metadata = builder.metadata; + this.moto = builder.moto; + this.email = builder.email; + this.cardholderName = builder.cardholderName; + this.addressLine1 = builder.addressLine1; + this.addressLine2 = builder.addressLine2; + this.addressPostcode = builder.addressPostcode; + this.addressCity = builder.addressCity; + this.addressCountry = builder.addressCountry; + this.language = builder.language; + this.source = builder.source; + this.setUpAgreement = builder.setUpAgreement; + this.authorisationMode = builder.authorisationMode; + this.agreementId = builder.agreementId; + } + + public int getAmount() { + return amount; + } + + public String getReturnUrl() { + return returnUrl; + } + + public String getDescription() { + return description; + } + + public String getReference() { + return reference; + } + + public Map getMetadata() { + return metadata; + } + + public Boolean isMoto() { + return moto; + } + + public String getEmail() { + return email; + } + + public Optional getCardholderName() { + return Optional.ofNullable(cardholderName); + } + + public Optional getAddressLine1() { + return Optional.ofNullable(addressLine1); + } + + public Optional getAddressLine2() { + return Optional.ofNullable(addressLine2); + } + + public Optional getAddressPostcode() { + return Optional.ofNullable(addressPostcode); + } + + public Optional getAddressCity() { + return Optional.ofNullable(addressCity); + } + + public Optional getAddressCountry() { + return Optional.ofNullable(addressCountry); + } + + public SupportedLanguage getLanguage() { + return language; + } + + public Optional getSource() { + return Optional.ofNullable(source); + } + + public Optional getSetUpAgreement() { + return Optional.ofNullable(setUpAgreement); + } + + public Optional getAuthorisationMode() { + return Optional.ofNullable(authorisationMode); + } + + public Optional getAgreementId() { + return Optional.ofNullable(agreementId); + } + + public static final class CreateChargeRequestParamsBuilder { + private Integer amount; + private String returnUrl; + private String description; + private String reference; + private Map metadata = Map.of(); + private Boolean moto; + private String email; + private String cardholderName; + private String addressLine1; + private String addressLine2; + private String addressPostcode; + private String addressCity; + private String addressCountry; + private SupportedLanguage language; + private Source source; + public String setUpAgreement; + public AuthorisationMode authorisationMode; + public String agreementId; + + private CreateChargeRequestParamsBuilder() { + } + + public static CreateChargeRequestParamsBuilder aCreateChargeRequestParams() { + return new CreateChargeRequestParamsBuilder(); + } + + public CreateChargeRequestParamsBuilder withAmount(int amount) { + this.amount = amount; + return this; + } + + public CreateChargeRequestParamsBuilder withReturnUrl(String returnUrl) { + this.returnUrl = returnUrl; + return this; + } + + public CreateChargeRequestParamsBuilder withDescription(String description) { + this.description = description; + return this; + } + + public CreateChargeRequestParamsBuilder withReference(String reference) { + this.reference = reference; + return this; + } + + public CreateChargeRequestParamsBuilder withMetadata(Map metadata) { + this.metadata = metadata; + return this; + } + + public CreateChargeRequestParamsBuilder withEmail(String email) { + this.email = email; + return this; + } + + public CreateChargeRequestParamsBuilder witCardHolderName(String cardholderName) { + this.cardholderName = cardholderName; + return this; + } + + public CreateChargeRequestParamsBuilder withAddressLine1(String addressLine1) { + this.addressLine1 = addressLine1; + return this; + } + + public CreateChargeRequestParamsBuilder withAddressLine2(String addressLine2) { + this.addressLine2 = addressLine2; + return this; + } + + public CreateChargeRequestParamsBuilder withAddressPostcode(String addressPostcode) { + this.addressPostcode = addressPostcode; + return this; + } + + public CreateChargeRequestParamsBuilder withAddressCity(String addressCity) { + this.addressCity = addressCity; + return this; + } + + public CreateChargeRequestParamsBuilder withAddressCountry(String addressCountry) { + this.addressCountry = addressCountry; + return this; + } + + public CreateChargeRequestParams build() { + List.of(amount, reference, description).forEach(Objects::requireNonNull); + return new CreateChargeRequestParams(this); + } + + public CreateChargeRequestParamsBuilder withLanguage(SupportedLanguage language) { + this.language = language; + return this; + } + + public CreateChargeRequestParamsBuilder withSource(Source source) { + this.source = source; + return this; + } + + public CreateChargeRequestParamsBuilder withMoto(boolean moto) { + this.moto = moto; + return this; + } + + public CreateChargeRequestParamsBuilder withSetUpAgreement(String setUpAgreement) { + this.setUpAgreement = setUpAgreement; + return this; + } + + public CreateChargeRequestParamsBuilder withAuthorisationMode(AuthorisationMode authorisationMode) { + this.authorisationMode = authorisationMode; + return this; + } + + public CreateChargeRequestParamsBuilder withAgreementId(String agreementId) { + this.agreementId = agreementId; + return this; + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/DisputeTransactionFromLedgerFixture.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/DisputeTransactionFromLedgerFixture.java new file mode 100644 index 000000000..d46793df4 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/DisputeTransactionFromLedgerFixture.java @@ -0,0 +1,161 @@ +package uk.gov.pay.api.utils.mocks; + +import com.fasterxml.jackson.annotation.JsonProperty; +import uk.gov.pay.api.model.ledger.DisputeSettlementSummary; +import uk.gov.pay.api.model.ledger.TransactionState; + +public class DisputeTransactionFromLedgerFixture { + @JsonProperty("amount") + private Long amount; + @JsonProperty("created_date") + private String createdDate; + @JsonProperty("transaction_id") + private String transactionId; + @JsonProperty("evidence_due_date") + private String evidenceDueDate; + @JsonProperty("fee") + private Long fee; + @JsonProperty("net_amount") + private Long netAmount; + @JsonProperty("parent_transaction_id") + private String parentTransactionId; + @JsonProperty("reason") + private String reason; + @JsonProperty("settlement_summary") + private DisputeSettlementSummary settlementSummary; + @JsonProperty("state") + private TransactionState state; + @JsonProperty("transaction_type") + private String transActionType = "DISPUTE"; + + public DisputeTransactionFromLedgerFixture(DisputeTransactionFromLedgerBuilder builder) { + this.amount = builder.amount; + this.createdDate = builder.createdDate; + this.transactionId = builder.transactionId; + this.evidenceDueDate = builder.evidenceDueDate; + this.fee = builder.fee; + this.netAmount = builder.netAmount; + this.parentTransactionId = builder.parentTransactionId; + this.reason = builder.reason; + this.settlementSummary = builder.settlementSummary; + this.state = builder.state; + } + + public Long getAmount() { + return amount; + } + + public String getCreatedDate() { + return createdDate; + } + + public String getTransactionId() { + return transactionId; + } + + public String getEvidenceDueDate() { + return evidenceDueDate; + } + + public Long getFee() { + return fee; + } + + public Long getNetAmount() { + return netAmount; + } + + public String getParentTransactionId() { + return parentTransactionId; + } + + public String getReason() { + return reason; + } + + public DisputeSettlementSummary getSettlementSummary() { + return settlementSummary; + } + + public TransactionState getState() { + return state; + } + + public String getTransActionType() { + return transActionType; + } + + public static final class DisputeTransactionFromLedgerBuilder { + private Long amount; + private String createdDate; + private String transactionId; + private String evidenceDueDate; + private Long fee; + private Long netAmount; + private String parentTransactionId; + private String reason; + private DisputeSettlementSummary settlementSummary; + private TransactionState state; + + private DisputeTransactionFromLedgerBuilder() { + } + + public static DisputeTransactionFromLedgerBuilder aDisputeTransactionFromLedgerFixture() { + return new DisputeTransactionFromLedgerBuilder(); + } + + public DisputeTransactionFromLedgerBuilder withAmount(Long amount) { + this.amount = amount; + return this; + } + + public DisputeTransactionFromLedgerBuilder withCreatedDate(String createdDate) { + this.createdDate = createdDate; + return this; + } + + public DisputeTransactionFromLedgerBuilder withTransactionId(String transactionId) { + this.transactionId = transactionId; + return this; + } + + public DisputeTransactionFromLedgerBuilder withEvidenceDueDate(String evidenceDueDate) { + this.evidenceDueDate = evidenceDueDate; + return this; + } + + public DisputeTransactionFromLedgerBuilder withFee(Long fee) { + this.fee = fee; + return this; + } + + public DisputeTransactionFromLedgerBuilder withNetAmount(Long netAmount) { + this.netAmount = netAmount; + return this; + } + + public DisputeTransactionFromLedgerBuilder withParentTransactionId(String parentTransactionId) { + this.parentTransactionId = parentTransactionId; + return this; + } + + public DisputeTransactionFromLedgerBuilder withReason(String reason) { + this.reason = reason; + return this; + } + + public DisputeTransactionFromLedgerBuilder withSettlementSummary(DisputeSettlementSummary settlementSummary) { + this.settlementSummary = settlementSummary; + return this; + } + + public DisputeTransactionFromLedgerBuilder withState(TransactionState state) { + this.state = state; + return this; + } + + public DisputeTransactionFromLedgerFixture build() { + return new DisputeTransactionFromLedgerFixture(this); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/LedgerMockClient.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/LedgerMockClient.java new file mode 100644 index 000000000..5aa7dba2c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/LedgerMockClient.java @@ -0,0 +1,264 @@ +package uk.gov.pay.api.utils.mocks; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.google.common.collect.ImmutableMap; +import com.google.gson.GsonBuilder; +import uk.gov.pay.api.model.links.Link; +import uk.gov.pay.api.utils.JsonStringBuilder; +import uk.gov.service.payments.commons.model.ErrorIdentifier; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.lang.String.format; +import static javax.ws.rs.core.HttpHeaders.ACCEPT; +import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.eclipse.jetty.http.HttpStatus.BAD_REQUEST_400; +import static org.eclipse.jetty.http.HttpStatus.INTERNAL_SERVER_ERROR_500; +import static org.eclipse.jetty.http.HttpStatus.NOT_FOUND_404; +import static org.eclipse.jetty.http.HttpStatus.OK_200; + +public class LedgerMockClient { + + private final WireMockClassRule ledgerMock; + private final ObjectMapper mapper = new ObjectMapper(); + + public LedgerMockClient(WireMockClassRule ledgerMock) { + this.ledgerMock = ledgerMock; + mapper.registerModule(new Jdk8Module()); + } + + public void respondOk_whenSearchCharges(String expectedResponse) { + whenSearchTransactions(aResponse() + .withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(expectedResponse)); + } + + public void whenSearchTransactions(ResponseDefinitionBuilder response) { + ledgerMock.stubFor(get(urlPathEqualTo("/v1/transaction")) + .withHeader(ACCEPT, matching(APPLICATION_JSON)).willReturn(response)); + } + + public void respondTransactionNotFound(String paymentId, String errorMessage) { + Map payload = new HashMap<>(); + payload.put("message", List.of(errorMessage)); + + ResponseDefinitionBuilder response = aResponse() + .withStatus(NOT_FOUND_404) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(new GsonBuilder().create().toJson(payload)); + + ledgerMock.stubFor(get(urlPathEqualTo(format("/v1/transaction/%s", paymentId))) + .willReturn(response)); + } + + public void respondWithGetAllRefunds(String transactionId, + RefundTransactionFromLedgerFixture... refunds) { + + JsonStringBuilder jsonStringBuilder = new JsonStringBuilder() + .add("parent_transaction_id", transactionId) + .add("transactions", refunds); + + ledgerMock.stubFor(get(urlPathEqualTo(format("/v1/transaction/%s/transaction", transactionId))) + .willReturn(aResponse() + .withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(jsonStringBuilder.build()))); + } + + public void respondWithSearchRefunds(RefundTransactionFromLedgerFixture... refunds) { + + Map links = (ImmutableMap.of("first_page", new Link("http://server:port/first-link?page=1"), + "prev_page", new Link("http://server:port/prev-link?page=2"), + "self", new Link("http://server:port/self-link?page=3"), + "last_page", new Link("http://server:port/last-link?page=5"), + "next_page", new Link("http://server:port/next-link?page=4"))); + + JsonStringBuilder jsonStringBuilder = new JsonStringBuilder() + .add("total", 1) + .add("count", 1) + .add("page", 1) + .add("results", Arrays.asList(refunds)) + .add("_links", links); + + ledgerMock.stubFor(get(urlPathEqualTo("/v1/transaction")) + .willReturn(aResponse() + .withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(jsonStringBuilder.build()))); + } + + public void respondWithSearchDisputes(DisputeTransactionFromLedgerFixture... disputes) { + + Map links = (ImmutableMap.of( + "first_page", new Link("http://server:port/first-link?page=1"), + "self", new Link("http://server:port/self-link?page=1"), + "last_page", new Link("http://server:port/last-link?page=1"))); + + JsonStringBuilder jsonStringBuilder = new JsonStringBuilder() + .add("total", 1) + .add("count", 1) + .add("page", 1) + .add("results", Arrays.asList(disputes)) + .add("_links", links); + + ledgerMock.stubFor(get(urlPathEqualTo("/v1/transaction")) + .willReturn(aResponse() + .withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(jsonStringBuilder.build()))); + } + + public void respondWithSearchRefundsNotFound() { + ledgerMock.stubFor(get(urlPathEqualTo("/v1/transaction")) + .willReturn(aResponse() + .withStatus(NOT_FOUND_404) + .withHeader(CONTENT_TYPE, APPLICATION_JSON))); + } + + public void respondWithSearchDisputesNotFound() { + ledgerMock.stubFor(get(urlPathEqualTo("/v1/transaction")) + .willReturn(aResponse() + .withStatus(NOT_FOUND_404) + .withHeader(CONTENT_TYPE, APPLICATION_JSON))); + } + + public void respondWithTransaction(String transactionId, TransactionFromLedgerFixture transaction) { + try { + var body = mapper.writeValueAsString(transaction); + respondWithTransaction(transactionId, body); + } catch (JsonProcessingException e) { + } + } + + public void respondWithRefund(String refundId, RefundTransactionFromLedgerFixture refund) { + try { + var body = mapper.writeValueAsString(refund); + respondWithTransaction(refundId, body); + } catch (JsonProcessingException e) { + } + } + + private void respondWithTransaction(String transactionId, String body) { + ledgerMock.stubFor(get(urlPathEqualTo(format("/v1/transaction/%s", transactionId))) + .willReturn(aResponse() + .withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(body))); + } + + public void respondWithTransactionEvents(String transactionId, TransactionEventFixture... events) { + JsonStringBuilder jsonStringBuilder = new JsonStringBuilder() + .add("transaction_id", transactionId) + .add("events", Arrays.asList(events)); + String path = format("/v1/transaction/%s/event", transactionId); + + String test = jsonStringBuilder.build(); + ledgerMock.stubFor(get(urlPathEqualTo(path)) + .willReturn(aResponse() + .withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(test))); + } + + public void respondRefundWithError(String refundId) { + respondTransactionError(refundId, "Downstream system error", INTERNAL_SERVER_ERROR_500); + } + + public void respondRefundNotFound(String refundId) { + respondTransactionError(refundId, format("Refund with id [%s] not found.", refundId), BAD_REQUEST_400); + } + + public void respondTransactionWithError(String paymentId, String errorMessage, int status) { + respondTransactionError(paymentId, errorMessage, status); + } + + public void respondTransactionEventsWithError(String transactionId, String errorMessage, int status) { + String path = format("/v1/transaction/%s/event", transactionId); + respondError(errorMessage, status, path); + } + + public void respondWithAgreement(String agreementId, AgreementFromLedgerFixture agreementFromLedgerFixture) throws JsonProcessingException { + respondWithAgreement(agreementId, mapper.writeValueAsString(agreementFromLedgerFixture)); + } + + public void respondWithAgreement(String agreementId, String body) { + ledgerMock.stubFor(get(urlPathEqualTo(format("/v1/agreement/%s", agreementId))) + .withHeader("X-Consistent", WireMock.equalTo("true")) + .willReturn(aResponse() + .withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(body))); + } + + public void respondWithSearchAgreements(String gatewayAccountId, String status, AgreementFromLedgerFixture... agreements) throws JsonProcessingException { + var links = Map.of( + "first_page", new Link("http://server:port/first-link?page=1"), + "prev_page", new Link("http://server:port/prev-link?page=2"), + "self", new Link("http://server:port/self-link?page=3"), + "last_page", new Link("http://server:port/last-link?page=5"), + "next_page", new Link("http://server:port/next-link?page=4") + ); + + var responseBody = new HashMap(); + responseBody.put("total", 9); + responseBody.put("count", 2); + responseBody.put("page", 3); + responseBody.put("results", List.copyOf(Arrays.asList(agreements))); + responseBody.put("_links", links); + + ledgerMock.stubFor(get(urlPathEqualTo("/v1/agreement")) + .withQueryParam("page", equalTo("3")) + .withQueryParam("status", equalTo(status)) + .withQueryParam("account_id", equalTo(gatewayAccountId)) + .withQueryParam("exact_reference_match", equalTo("true")) + .willReturn(aResponse() + .withStatus(OK_200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(mapper.writeValueAsString(responseBody)))); + } + + public void respondAgreementNotFound(String agreementId) { + Map payload = new HashMap<>(); + payload.put("message", List.of("Agreement not found")); + + ResponseDefinitionBuilder response = aResponse() + .withStatus(NOT_FOUND_404) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(new GsonBuilder().create().toJson(payload)); + + ledgerMock.stubFor(get(urlPathEqualTo(format("/v1/agreement/%s", agreementId))) + .willReturn(response)); + } + + private void respondTransactionError(String transactionId, String message, int status) { + String path = format("/v1/transaction/%s", transactionId); + respondError(message, status, path); + } + + private void respondError(String message, int status, String path) { + Map payload = new HashMap<>(); + payload.put("message", List.of(message)); + payload.put("error_identifier", ErrorIdentifier.GENERIC.toString()); + + ledgerMock.stubFor(get(urlPathEqualTo(path)) + .willReturn(aResponse() + .withStatus(status) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(new GsonBuilder().create().toJson(payload)))); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/RefundTransactionFromLedgerFixture.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/RefundTransactionFromLedgerFixture.java new file mode 100644 index 000000000..245f3cead --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/RefundTransactionFromLedgerFixture.java @@ -0,0 +1,99 @@ +package uk.gov.pay.api.utils.mocks; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import uk.gov.pay.api.model.RefundSettlementSummary; +import uk.gov.pay.api.model.ledger.TransactionState; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class RefundTransactionFromLedgerFixture { + private final Long amount; + private final TransactionState state; + private final String createdDate; + private final String transactionId; + private final String parentTransactionId; + private final RefundSettlementSummary settlementSummary; + + public RefundTransactionFromLedgerFixture(RefundTransactionFromLedgerBuilder builder) { + this.amount = builder.amount; + this.state = builder.state; + this.createdDate = builder.createdDate; + this.transactionId = builder.transactionId; + this.parentTransactionId = builder.parentTransactionId; + this.settlementSummary = builder.settlementSummary; + } + + public Long getAmount() { + return amount; + } + + public TransactionState getState() { + return state; + } + + public String getCreatedDate() { + return createdDate; + } + + public String getTransactionId() { + return transactionId; + } + + public String getParentTransactionId() { + return parentTransactionId; + } + + public RefundSettlementSummary getSettlementSummary() { + return settlementSummary; + } + + public static final class RefundTransactionFromLedgerBuilder { + private Long amount; + private TransactionState state; + private String createdDate; + private String transactionId; + private String parentTransactionId; + private RefundSettlementSummary settlementSummary = new RefundSettlementSummary(); + + private RefundTransactionFromLedgerBuilder() { + } + + public static RefundTransactionFromLedgerBuilder aRefundTransactionFromLedgerFixture() { + return new RefundTransactionFromLedgerBuilder(); + } + + public RefundTransactionFromLedgerBuilder withAmount(Long amount) { + this.amount = amount; + return this; + } + + public RefundTransactionFromLedgerBuilder withState(TransactionState state) { + this.state = state; + return this; + } + + public RefundTransactionFromLedgerBuilder withCreatedDate(String createdDate) { + this.createdDate = createdDate; + return this; + } + + public RefundTransactionFromLedgerBuilder withTransactionId(String transactionId) { + this.transactionId = transactionId; + return this; + } + + public RefundTransactionFromLedgerBuilder withParentTransactionId(String parentTransactionId) { + this.parentTransactionId = parentTransactionId; + return this; + } + + public RefundTransactionFromLedgerFixture build() { + return new RefundTransactionFromLedgerFixture(this); + } + + public RefundTransactionFromLedgerBuilder withSettlementSummary(String settledDate) { + this.settlementSummary = new RefundSettlementSummary(settledDate); + return this; + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/TransactionEventFixture.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/TransactionEventFixture.java new file mode 100644 index 000000000..1cbdccc4d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/TransactionEventFixture.java @@ -0,0 +1,47 @@ +package uk.gov.pay.api.utils.mocks; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import uk.gov.pay.api.model.PaymentState; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class TransactionEventFixture { + private PaymentState state; + private String timestamp; + + public PaymentState getState() { + return state; + } + + public String getTimestamp() { + return timestamp; + } + + public TransactionEventFixture(TransactionEventFixtureBuilder builder) { + this.state = builder.state; + this.timestamp = builder.timestamp; + } + + public static final class TransactionEventFixtureBuilder { + private PaymentState state; + private String timestamp; + + public TransactionEventFixtureBuilder withState(PaymentState state) { + this.state = state; + return this; + } + + public TransactionEventFixtureBuilder withTimestamp(String timestamp) { + this.timestamp = timestamp; + return this; + } + + public static TransactionEventFixtureBuilder aTransactionEventFixture() { + return new TransactionEventFixtureBuilder(); + } + + public TransactionEventFixture build() { + return new TransactionEventFixture(this); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/TransactionFromLedgerFixture.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/TransactionFromLedgerFixture.java new file mode 100644 index 000000000..ca6339e9a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/utils/mocks/TransactionFromLedgerFixture.java @@ -0,0 +1,338 @@ +package uk.gov.pay.api.utils.mocks; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import uk.gov.pay.api.model.AuthorisationSummary; +import uk.gov.pay.api.model.CardDetailsFromResponse; +import uk.gov.pay.api.model.PaymentConnectorResponseLink; +import uk.gov.pay.api.model.PaymentSettlementSummary; +import uk.gov.pay.api.model.PaymentState; +import uk.gov.pay.api.model.RefundSummary; +import uk.gov.service.payments.commons.model.AuthorisationMode; +import uk.gov.service.payments.commons.model.SupportedLanguage; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class TransactionFromLedgerFixture { + private String transactionId; + private String returnUrl; + private String paymentProvider; + private List links; + private RefundSummary refundSummary; + private PaymentSettlementSummary settlementSummary; + private CardDetailsFromResponse cardDetails; + private Long amount; + private PaymentState state; + private String description; + private String reference; + private String email; + private String language; + private boolean delayedCapture; + private boolean moto; + private Long corporateCardSurcharge; + private Long totalAmount; + private Long fee; + private Long netAmount; + private String createdDate; + private String gatewayTransactionId; + private Map metadata; + private AuthorisationSummary authorisationSummary; + private AuthorisationMode authorisationMode; + private final Optional walletType; + + public String getReturnUrl() { + return returnUrl; + } + + public String getPaymentProvider() { + return paymentProvider; + } + + public List getLinks() { + return links; + } + + public RefundSummary getRefundSummary() { + return refundSummary; + } + + public PaymentSettlementSummary getSettlementSummary() { + return settlementSummary; + } + + public CardDetailsFromResponse getCardDetails() { + return cardDetails; + } + + public String getDescription() { + return description; + } + + public String getReference() { + return reference; + } + + public String getEmail() { + return email; + } + + public String getLanguage() { + return language; + } + + public boolean isDelayedCapture() { + return delayedCapture; + } + + public boolean isMoto() { + return moto; + } + + public Long getCorporateCardSurcharge() { + return corporateCardSurcharge; + } + + public Long getTotalAmount() { + return totalAmount; + } + + public Long getFee() { + return fee; + } + + public Long getNetAmount() { + return netAmount; + } + + public String getGatewayTransactionId() { + return gatewayTransactionId; + } + + public Map getMetadata() { + return metadata; + } + + public AuthorisationSummary getAuthorisationSummary() { + return authorisationSummary; + } + + public AuthorisationMode getAuthorisationMode() { + return authorisationMode; + } + + public Optional getWalletType() { return walletType; } + + public TransactionFromLedgerFixture(TransactionFromLedgerBuilder builder) { + this.amount = builder.amount; + this.state = builder.state; + this.createdDate = builder.createdDate; + this.transactionId = builder.transactionId; + this.returnUrl = builder.returnUrl; + this.paymentProvider = builder.paymentProvider; + this.links = builder.links; + this.refundSummary = builder.refundSummary; + this.settlementSummary = builder.settlementSummary; + this.cardDetails = builder.cardDetails; + this.description = builder.description; + this.reference = builder.reference; + this.email = builder.email; + this.language = builder.language; + this.delayedCapture = builder.delayedCapture; + this.moto = builder.moto; + this.corporateCardSurcharge = builder.corporateCardSurcharge; + this.totalAmount = builder.totalAmount; + this.fee = builder.fee; + this.netAmount = builder.netAmount; + this.gatewayTransactionId = builder.gatewayTransactionId; + this.metadata = builder.metadata; + this.authorisationSummary = builder.authorisationSummary; + this.authorisationMode = builder.authorisationMode; + this.walletType = builder.walletType == null || builder.walletType.isEmpty() ? Optional.empty() : Optional.of(builder.walletType); + } + + public Long getAmount() { + return amount; + } + + public PaymentState getState() { + return state; + } + + public String getCreatedDate() { + return createdDate; + } + + public String getTransactionId() { + return transactionId; + } + + + public static final class TransactionFromLedgerBuilder { + private String transactionId; + private String returnUrl; + private String paymentProvider; + private List links = new ArrayList<>(); + private RefundSummary refundSummary; + private PaymentSettlementSummary settlementSummary; + private CardDetailsFromResponse cardDetails; + private Long amount; + private PaymentState state; + private String description; + private String reference; + private String email; + private String language; + private boolean delayedCapture; + private boolean moto; + private Long corporateCardSurcharge; + private Long totalAmount; + private Long fee; + private Long netAmount; + private String createdDate; + private String gatewayTransactionId; + private Map metadata; + private AuthorisationSummary authorisationSummary; + private AuthorisationMode authorisationMode = AuthorisationMode.WEB; + private String walletType; + + + private TransactionFromLedgerBuilder() { + } + + public static TransactionFromLedgerBuilder aTransactionFromLedgerFixture() { + return new TransactionFromLedgerBuilder(); + } + + public TransactionFromLedgerBuilder withAmount(Long amount) { + this.amount = amount; + return this; + } + + public TransactionFromLedgerBuilder withState(PaymentState state) { + this.state = state; + return this; + } + + public TransactionFromLedgerBuilder withCreatedDate(String createdDate) { + this.createdDate = createdDate; + return this; + } + + public TransactionFromLedgerBuilder withTransactionId(String transactionId) { + this.transactionId = transactionId; + return this; + } + + public TransactionFromLedgerBuilder withReturnUrl(String returnUrl) { + this.returnUrl = returnUrl; + return this; + } + + public TransactionFromLedgerBuilder withPaymentProvider(String paymentProvider) { + this.paymentProvider = paymentProvider; + return this; + } + + public TransactionFromLedgerBuilder withLinks(List links) { + this.links = links; + return this; + } + + public TransactionFromLedgerBuilder withRefundSummary(RefundSummary refundSummary) { + this.refundSummary = refundSummary; + return this; + } + + public TransactionFromLedgerBuilder withSettlementSummary(PaymentSettlementSummary settlementSummary) { + this.settlementSummary = settlementSummary; + return this; + } + + public TransactionFromLedgerBuilder withCardDetails(CardDetailsFromResponse cardDetails) { + this.cardDetails = cardDetails; + return this; + } + + public TransactionFromLedgerBuilder withDescription(String description) { + this.description = description; + return this; + } + + public TransactionFromLedgerBuilder withReference(String reference) { + this.reference = reference; + return this; + } + + public TransactionFromLedgerBuilder withEmail(String email) { + this.email = email; + return this; + } + + public TransactionFromLedgerBuilder withLanguage(SupportedLanguage language) { + this.language = language.toString(); + return this; + } + + public TransactionFromLedgerBuilder withDelayedCapture(boolean delayedCapture) { + this.delayedCapture = delayedCapture; + return this; + } + + public TransactionFromLedgerBuilder withMoto(boolean moto) { + this.moto = moto; + return this; + } + + public TransactionFromLedgerBuilder withCorporateCardSurcharge(Long corporateCardSurcharge) { + this.corporateCardSurcharge = corporateCardSurcharge; + return this; + } + + public TransactionFromLedgerBuilder withTotalAmount(Long totalAmount) { + this.totalAmount = totalAmount; + return this; + } + + public TransactionFromLedgerBuilder withFee(Long fee) { + this.fee = fee; + return this; + } + + public TransactionFromLedgerBuilder withNetAmount(Long netAmount) { + this.netAmount = netAmount; + return this; + } + + public TransactionFromLedgerBuilder withGatewayTransactionId(String gatewayTransactionId) { + this.gatewayTransactionId = gatewayTransactionId; + return this; + } + + public TransactionFromLedgerBuilder withMetadata(Map metadata) { + this.metadata = metadata; + return this; + } + + public TransactionFromLedgerBuilder withAuthorisationSummary(AuthorisationSummary authorisationSummary) { + this.authorisationSummary = authorisationSummary; + return this; + } + + public TransactionFromLedgerBuilder withAuthorisationMode(AuthorisationMode authorisationMode) { + this.authorisationMode = authorisationMode; + return this; + } + + public TransactionFromLedgerBuilder withWalletType(String walletType) { + this.walletType = walletType; + return this; + } + + public TransactionFromLedgerFixture build() { + return new TransactionFromLedgerFixture(this); + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/AgreementSearchValidatorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/AgreementSearchValidatorTest.java new file mode 100644 index 000000000..e4624db39 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/AgreementSearchValidatorTest.java @@ -0,0 +1,51 @@ +package uk.gov.pay.api.validation; + + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.Test; +import uk.gov.pay.api.exception.AgreementValidationException; +import uk.gov.pay.api.ledger.model.AgreementSearchParams; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class AgreementSearchValidatorTest { + + @Test + public void agreementSearchValidator_shouldNotThrowWithValidParams() { + var params = validParams(); + assertDoesNotThrow(() -> AgreementSearchValidator.validateSearchParameters(params)); + } + + @Test + public void agreementSearchValidator_shouldThrowForInvalidReference() { + var params = validParams(); + params.setReference(RandomStringUtils.randomAlphanumeric(300)); + assertThrows(AgreementValidationException.class, () -> AgreementSearchValidator.validateSearchParameters(params)); + } + + @Test + public void agreementSearchValidator_shouldThrowForInvalidStatus() { + var params = validParams(); + params.setStatus("NOT-A-STATUS"); + assertThrows(AgreementValidationException.class, () -> AgreementSearchValidator.validateSearchParameters(params)); + } + + @Test + public void agreementSearchValidator_shouldThrowForInvalidPageNumber() { + var params = validParams(); + params.setPageNumber("NOT-A-PAGE-NUMBER"); + assertThrows(AgreementValidationException.class, () -> AgreementSearchValidator.validateSearchParameters(params)); + } + + @Test + public void agreementSearchValidator_shouldThrowForInvalidDisplaySize() { + var params = validParams(); + params.setDisplaySize("NOT-A-DISPLAY-SIZE"); + assertThrows(AgreementValidationException.class, () -> AgreementSearchValidator.validateSearchParameters(params)); + } + + private AgreementSearchParams validParams() { + return new AgreementSearchParams(RandomStringUtils.randomAlphanumeric(200), "created", "1", "1"); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/CardExpiryValidatorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/CardExpiryValidatorTest.java new file mode 100644 index 000000000..9d3466080 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/CardExpiryValidatorTest.java @@ -0,0 +1,89 @@ +package uk.gov.pay.api.validation; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import uk.gov.pay.api.model.telephone.CreateTelephonePaymentRequest; +import uk.gov.pay.api.model.telephone.PaymentOutcome; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +public class CardExpiryValidatorTest { + + private static CreateTelephonePaymentRequest.Builder builder = new CreateTelephonePaymentRequest.Builder(); + + private static Validator validator; + + @BeforeAll + public static void setUpValidator() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + + builder + .withAmount(1200) + .withDescription("Some description") + .withReference("Some reference") + .withProcessorId("1PROC") + .withProviderId("1PROV") + .withLastFourDigits("1234") + .withFirstSixDigits("123456") + .withCardType("visa") + .withPaymentOutcome(new PaymentOutcome("success")); + } + + @Test + public void failsValidationForInvalidMonth00() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withCardExpiry("00/99") + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.size(), is(1)); + assertThat(constraintViolations.iterator().next().getMessage(), is("Field [card_expiry] must have valid MM/YY")); + } + + @Test + public void failsValidationForInvalidMonth99() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withCardExpiry("99/99") + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.size(), is(1)); + assertThat(constraintViolations.iterator().next().getMessage(), is("Field [card_expiry] must have valid MM/YY")); + } + + @Test + public void passesValidationForValidCardExpiry() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withCardExpiry("01/99") + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.isEmpty(), is(true)); + } + + @Test + public void passesValidationForNullCardExpiry() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withCardExpiry(null) + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.isEmpty(), is(true)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/CardFirstSixDigitsValidatorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/CardFirstSixDigitsValidatorTest.java new file mode 100644 index 000000000..3b2dbf89b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/CardFirstSixDigitsValidatorTest.java @@ -0,0 +1,89 @@ +package uk.gov.pay.api.validation; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import uk.gov.pay.api.model.telephone.CreateTelephonePaymentRequest; +import uk.gov.pay.api.model.telephone.PaymentOutcome; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +public class CardFirstSixDigitsValidatorTest { + + private static CreateTelephonePaymentRequest.Builder builder = new CreateTelephonePaymentRequest.Builder(); + + private static Validator validator; + + @BeforeAll + public static void setUpValidator() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + + builder + .withAmount(1200) + .withDescription("Some description") + .withReference("Some reference") + .withProcessorId("1PROC") + .withProviderId("1PROV") + .withCardExpiry("01/99") + .withCardType("visa") + .withLastFourDigits("1234") + .withPaymentOutcome(new PaymentOutcome("success")); + } + + @Test + public void failsValidationForFiveDigits() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withFirstSixDigits("12345") + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.size(), is(1)); + assertThat(constraintViolations.iterator().next().getMessage(), is("Field [first_six_digits] must be exactly 6 digits")); + } + + @Test + public void failsValidationForSevenDigits() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withFirstSixDigits("1234567") + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.size(), is(1)); + assertThat(constraintViolations.iterator().next().getMessage(), is("Field [first_six_digits] must be exactly 6 digits")); + } + + @Test + public void passesValidationForSixDigits() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withFirstSixDigits("123456") + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.isEmpty(), is(true)); + } + + @Test + public void passesValidationForNullDigits() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withFirstSixDigits(null) + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.isEmpty(), is(true)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/CardLastFourDigitsValidatorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/CardLastFourDigitsValidatorTest.java new file mode 100644 index 000000000..e2c77fafa --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/CardLastFourDigitsValidatorTest.java @@ -0,0 +1,89 @@ +package uk.gov.pay.api.validation; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import uk.gov.pay.api.model.telephone.CreateTelephonePaymentRequest; +import uk.gov.pay.api.model.telephone.PaymentOutcome; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +public class CardLastFourDigitsValidatorTest { + + private static CreateTelephonePaymentRequest.Builder builder = new CreateTelephonePaymentRequest.Builder(); + + private static Validator validator; + + @BeforeAll + public static void setUpValidator() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + + builder + .withAmount(1200) + .withDescription("Some description") + .withReference("Some reference") + .withProcessorId("1PROC") + .withProviderId("1PROV") + .withCardExpiry("01/99") + .withCardType("visa") + .withFirstSixDigits("123456") + .withPaymentOutcome(new PaymentOutcome("success")); + } + + @Test + public void failsValidationForThreeDigits() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withLastFourDigits("123") + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.size(), is(1)); + assertThat(constraintViolations.iterator().next().getMessage(), is("Field [last_four_digits] must be exactly 4 digits")); + } + + @Test + public void failsValidationForFiveDigits() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withLastFourDigits("12345") + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.size(), is(1)); + assertThat(constraintViolations.iterator().next().getMessage(), is("Field [last_four_digits] must be exactly 4 digits")); + } + + @Test + public void passesValidationForFourDigits() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withLastFourDigits("1234") + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.isEmpty(), is(true)); + } + + @Test + public void passesValidationForNullDigits() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withLastFourDigits(null) + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.isEmpty(), is(true)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/CardTypeValidatorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/CardTypeValidatorTest.java new file mode 100644 index 000000000..6e4ac37b0 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/CardTypeValidatorTest.java @@ -0,0 +1,76 @@ +package uk.gov.pay.api.validation; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import uk.gov.pay.api.model.telephone.CreateTelephonePaymentRequest; +import uk.gov.pay.api.model.telephone.PaymentOutcome; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +public class CardTypeValidatorTest { + + private static CreateTelephonePaymentRequest.Builder builder = new CreateTelephonePaymentRequest.Builder(); + + private static Validator validator; + + @BeforeAll + public static void setUpValidator() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + + builder + .withAmount(1200) + .withDescription("Some description") + .withReference("Some reference") + .withProcessorId("1PROC") + .withProviderId("1PROV") + .withCardExpiry("01/99") + .withLastFourDigits("1234") + .withFirstSixDigits("123456") + .withPaymentOutcome(new PaymentOutcome("success")); + } + + @Test + public void failsValidationForInvalidCardType() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withCardType("bad-card") + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.size(), is(1)); + assertThat(constraintViolations.iterator().next().getMessage(), is("Field [card_type] must be either master-card, visa, maestro, diners-club, american-express or jcb")); + } + + @Test + public void passesValidationForValidCardType() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withCardType("visa") + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.isEmpty(), is(true)); + } + + @Test + public void passesValidationForNullCardType() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withCardType(null) + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.isEmpty(), is(true)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/DisputeSearchValidatorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/DisputeSearchValidatorTest.java new file mode 100644 index 000000000..3b6dc0532 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/DisputeSearchValidatorTest.java @@ -0,0 +1,145 @@ +package uk.gov.pay.api.validation; + +import org.junit.jupiter.api.Test; +import uk.gov.pay.api.exception.DisputesValidationException; +import uk.gov.pay.api.service.DisputesSearchParams; + +import static java.lang.String.format; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static uk.gov.pay.api.validation.DisputeSearchValidator.validateDisputeParameters; + +class DisputeSearchValidatorTest { + private static String VALIDATION_EXCEPTION_MESSAGE = "Invalid parameters: %s. See Public API documentation for the correct data formats"; + @Test + void validateSearchParameters_shouldSuccessValidation() { + DisputesSearchParams params = new DisputesSearchParams.Builder() + .withFromDate("2022-07-22T11:23:55Z") + .withToDate("2022-07-22T12:23:55Z") + .withDisplaySize("1") + .withFromSettledDate("2022-07-20") + .withToSettledDate("2022-07-21") + .withPage("1") + .withStatus("won") + .build(); + + validateDisputeParameters(params); + } + + @Test + void validateParams_shouldNotGiveAnErrorValidation_ForMissingPageDisplaySize() { + DisputesSearchParams params = new DisputesSearchParams.Builder() + .withFromDate("2022-07-22T11:23:55Z") + .withToDate("2022-07-22T12:23:55Z") + .withDisplaySize(null) + .withFromSettledDate("2022-07-20") + .withToSettledDate("2022-07-21") + .withPage(null) + .withStatus("lost") + .build(); + validateDisputeParameters(params); + } + + @Test + void validateSearchParameters_shouldGiveAValidationError_ForNonValidToDate() { + DisputesSearchParams params = new DisputesSearchParams.Builder() + .withToDate("alpha") + .build(); + DisputesValidationException validationException = assertThrows(DisputesValidationException.class, + () -> validateDisputeParameters(params)); + assertThat(validationException.getRequestError().getCode(), is("P0401")); + assertThat(validationException.getRequestError().getDescription(), is(format(VALIDATION_EXCEPTION_MESSAGE, "to_date"))); + } + + @Test + void validateSearchParameters_shouldGiveAValidationError_ForNonValidFromDate() { + DisputesSearchParams params = new DisputesSearchParams.Builder() + .withFromDate("bravo") + .build(); + DisputesValidationException validationException = assertThrows(DisputesValidationException.class, + () -> validateDisputeParameters(params)); + assertThat(validationException.getRequestError().getCode(), is("P0401")); + assertThat(validationException.getRequestError().getDescription(), is(format(VALIDATION_EXCEPTION_MESSAGE, "from_date"))); + } + + @Test + void validateSearchParameters_shouldGiveAValidationError_ForNonNumericPageAndSize() { + DisputesSearchParams params = new DisputesSearchParams.Builder() + .withDisplaySize("charlie") + .withPage("delta") + .build(); + DisputesValidationException validationException = assertThrows(DisputesValidationException.class, + () -> validateDisputeParameters(params)); + assertThat(validationException.getRequestError().getCode(), is("P0401")); + assertThat(validationException.getRequestError().getDescription(), is(format(VALIDATION_EXCEPTION_MESSAGE, "page, display_size"))); + } + + @Test + void validateParams_shouldGiveAnErrorValidation_forZeroPageDisplay() { + DisputesSearchParams params = new DisputesSearchParams.Builder() + .withDisplaySize("0") + .withPage("0") + .build(); + DisputesValidationException validationException = assertThrows(DisputesValidationException.class, + () -> validateDisputeParameters(params)); + assertThat(validationException.getRequestError().getCode(), is("P0401")); + assertThat(validationException.getRequestError().getDescription(), is(format(VALIDATION_EXCEPTION_MESSAGE, "page, display_size"))); + } + + @Test + void validateParams_shouldGiveAnErrorValidation_forMaxedOutValuesPageDisplaySize() { + DisputesSearchParams params = new DisputesSearchParams.Builder() + .withDisplaySize(String.valueOf(Integer.MAX_VALUE + 1)) + .withPage(String.valueOf(Integer.MAX_VALUE + 1)) + .build(); + DisputesValidationException validationException = assertThrows(DisputesValidationException.class, + () -> validateDisputeParameters(params)); + assertThat(validationException.getRequestError().getCode(), is("P0401")); + assertThat(validationException.getRequestError().getDescription(), is(format(VALIDATION_EXCEPTION_MESSAGE, "page, display_size"))); + } + + @Test + void validateParams_shouldGiveAnErrorValidation_forTooLargeDisplaySize() { + DisputesSearchParams params = new DisputesSearchParams.Builder() + .withDisplaySize("501") + .build(); + DisputesValidationException validationException = assertThrows(DisputesValidationException.class, + () -> validateDisputeParameters(params)); + assertThat(validationException.getRequestError().getCode(), is("P0401")); + assertThat(validationException.getRequestError().getDescription(), is(format(VALIDATION_EXCEPTION_MESSAGE, "display_size"))); + } + + @Test + void validateSearchParameters_shouldGiveAValidationError_ForNonValidToSettledDate() { + DisputesSearchParams params = new DisputesSearchParams.Builder() + .withToSettledDate("January 7") + .build(); + DisputesValidationException validationException = assertThrows(DisputesValidationException.class, + () -> validateDisputeParameters(params)); + assertThat(validationException.getRequestError().getCode(), is("P0401")); + assertThat(validationException.getRequestError().getDescription(), is(format(VALIDATION_EXCEPTION_MESSAGE, "to_settled_date"))); + } + + @Test + void validateSearchParameters_shouldGiveAValidationError_ForNonValidFromSettledDate() { + DisputesSearchParams params = new DisputesSearchParams.Builder() + .withFromSettledDate("19th September") + .build(); + DisputesValidationException validationException = assertThrows(DisputesValidationException.class, + () -> validateDisputeParameters(params)); + assertThat(validationException.getRequestError().getCode(), is("P0401")); + assertThat(validationException.getRequestError().getDescription(), is(format(VALIDATION_EXCEPTION_MESSAGE, "from_settled_date"))); + } + + @Test + void validateSearchParameters_shouldGiveAValidationError_ForNonExistentStatus() { + DisputesSearchParams params = new DisputesSearchParams.Builder() + .withStatus("blew it") + .build(); + DisputesValidationException validationException = assertThrows(DisputesValidationException.class, + () -> validateDisputeParameters(params)); + assertThat(validationException.getRequestError().getCode(), is("P0401")); + assertThat(validationException.getRequestError().getDescription(), is(format(VALIDATION_EXCEPTION_MESSAGE, "state"))); + } +} \ No newline at end of file diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/PaymentOutcomeValidatorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/PaymentOutcomeValidatorTest.java new file mode 100644 index 000000000..722ffca3c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/PaymentOutcomeValidatorTest.java @@ -0,0 +1,149 @@ +package uk.gov.pay.api.validation; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import uk.gov.pay.api.model.telephone.CreateTelephonePaymentRequest; +import uk.gov.pay.api.model.telephone.PaymentOutcome; +import uk.gov.pay.api.model.telephone.Supplemental; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +public class PaymentOutcomeValidatorTest { + + private static CreateTelephonePaymentRequest.Builder builder = new CreateTelephonePaymentRequest.Builder(); + + private static Validator validator; + + @BeforeAll + public static void setUpValidator() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + + builder + .withAmount(1200) + .withDescription("Some description") + .withReference("Some reference") + .withCreatedDate("2018-02-21T16:04:25Z") + .withAuthorisedDate("2018-02-21T16:05:33Z") + .withAuthCode("666") + .withProcessorId("1PROC") + .withProviderId("1PROV") + .withLastFourDigits("1234") + .withFirstSixDigits("123456") + .withCardExpiry("01/13") + .withCardType("visa") + .withNameOnCard("Jane Doe") + .withEmailAddress("jane_doe@example.com") + .withTelephoneNumber("+447700900796"); + } + + @Test + public void failsValidationForInvalidPaymentOutcomeStatus() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withPaymentOutcome(new PaymentOutcome("invalid")) + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.size(), is(1)); + assertThat(constraintViolations.iterator().next().getMessage(), is("Field [payment_outcome] must include a valid status and error code")); + } + + @Test + public void failsValidationForPaymentOutcomeStatusSuccessAndErrorCodeGiven() { + + PaymentOutcome paymentOutcome = new PaymentOutcome( + "success", + "error", + new Supplemental( + "ECKOH01234", + "textual message describing error code" + ) + ); + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withPaymentOutcome(paymentOutcome) + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.size(), is(1)); + assertThat(constraintViolations.iterator().next().getMessage(), is("Field [payment_outcome] must include a valid status and error code")); + } + + @Test + public void failsValidationForInvalidErrorCode() { + + PaymentOutcome paymentOutcome = new PaymentOutcome( + "failed", + "error", + new Supplemental( + "ECKOH01234", + "textual message describing error code" + ) + ); + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withPaymentOutcome(paymentOutcome) + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.size(), is(1)); + assertThat(constraintViolations.iterator().next().getMessage(), is("Field [payment_outcome] must include a valid status and error code")); + } + + @Test + public void passesValidationForCorrectErrorCode() { + + PaymentOutcome paymentOutcome = new PaymentOutcome( + "failed", + "P0010", + new Supplemental( + "ECKOH01234", + "textual message describing error code" + ) + ); + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withPaymentOutcome(paymentOutcome) + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.isEmpty(), is(true)); + } + + @Test + public void passesValidationForPaymentOutcomeStatusOfSuccess() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withPaymentOutcome(new PaymentOutcome("success")) + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.isEmpty(), is(true)); + } + + @Test + public void passesValidationForNullPaymentOutcome() { + + CreateTelephonePaymentRequest telephonePaymentRequest = builder + .withPaymentOutcome(null) + .build(); + + Set> constraintViolations = validator.validate(telephonePaymentRequest); + + assertThat(constraintViolations.size(), is(1)); + assertThat(constraintViolations.iterator().next().getMessage(), is("Field [payment_outcome] cannot be null")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/PaymentSearchValidatorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/PaymentSearchValidatorTest.java new file mode 100644 index 000000000..7a613bbb9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/PaymentSearchValidatorTest.java @@ -0,0 +1,264 @@ +package uk.gov.pay.api.validation; + +import org.junit.jupiter.api.Test; +import uk.gov.pay.api.exception.PaymentValidationException; + +import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static uk.gov.pay.api.matcher.PaymentValidationExceptionMatcher.aValidationExceptionContaining; + +public class PaymentSearchValidatorTest { + + private static final String SUCCESSFUL_TEST_EMAIL = "alice.111@mail.fake"; + private static final String UNSUCCESSFUL_TEST_EMAIL = randomAlphanumeric(255) + "@mail.fake"; + private static final String UNSUCCESSFUL_TEST_CARD_BRAND = "123456789012345678901"; + + @Test + public void validateParams_shouldSuccessValidation() { + PaymentSearchValidator.validateSearchParameters("success", "ref", SUCCESSFUL_TEST_EMAIL, + "", "2016-01-25T13:23:55Z", "2016-01-25T13:23:55Z", + "1", "1", "424242", "4242", + "2020-09-25", "2020-09-25"); + } + + @Test + public void validateParams_reference_shouldGiveAnErrorValidation() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters("success", randomAlphanumeric(500), + SUCCESSFUL_TEST_EMAIL, "", "2016-01-25T13:23:55Z", "2016-01-25T13:23:55Z", + "1", "1", "424242", "4242", + "2020-09-25", "2020-09-25")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", "Invalid parameters: reference. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_email_shouldGiveAnErrorValidation() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters("success", "ref", UNSUCCESSFUL_TEST_EMAIL, + "", "2016-01-25T13:23:55Z", "2016-01-25T13:23:55Z", + "1", "1", "424242", "4242", + "2020-09-25", "2020-09-25")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", "Invalid parameters: email. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_state_shouldGiveAnErrorValidation() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters("invalid", "ref", SUCCESSFUL_TEST_EMAIL, + "", "2016-01-25T13:23:55Z", "2016-01-25T13:23:55Z", "1", + "1", "424242", "4242", + "2020-09-25", "2020-09-25")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", "Invalid parameters: state. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_toDate_shouldGiveAnErrorValidation() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters("success", "ref", SUCCESSFUL_TEST_EMAIL, + "", "2016-01-25T13:23:55Z", "2016-01-25T13-23:55Z", + "1", "1", "424242", "4242", + "2020-09-25", "2020-09-25")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", "Invalid parameters: to_date. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_fromDate_shouldGiveAnErrorValidation() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters("success", "ref", SUCCESSFUL_TEST_EMAIL, + "", "2016-01-25T13-23:55Z", "2016-01-25T13:23:55Z", + "1", "1", "424242", "4242", + "2020-09-25", "2020-09-25")); + assertThat(paymentValidationException, + aValidationExceptionContaining("P0401", "Invalid parameters: from_date. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnErrorValidation_forAllParams() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters("invalid", randomAlphanumeric(500), UNSUCCESSFUL_TEST_EMAIL, + "", "2016-01-25T13-23:55Z", "2016-01-25T13-23:55Z", + "-1", "-1", + "424242", "4242", "2020-09-25", "2020-09-25")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", "Invalid parameters: state, reference, email, from_date, to_date, page, display_size. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnErrorValidation_forZeroPageDisplay() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters("invalid", randomAlphanumeric(500), UNSUCCESSFUL_TEST_EMAIL, + "", "2016-01-25T13-23:55Z", "2016-01-25T13-23:55Z", + "0", "0", "424242", "4242", + "2020-09-25", "2020-09-25")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", "Invalid parameters: state, reference, email, from_date, to_date, page, display_size. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnErrorValidation_forMaxedOutValuesPageDisplaySize() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters("invalid", randomAlphanumeric(500), UNSUCCESSFUL_TEST_EMAIL, + "", "2016-01-25T13-23:55Z", "2016-01-25T13-23:55Z", + String.valueOf(Integer.MAX_VALUE + 1), String.valueOf(Integer.MAX_VALUE + 1), "424242", "4242", + "2020-09-25", "2020-09-25")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", + "Invalid parameters: state, reference, email, from_date, to_date, page, display_size. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldNotGiveAnErrorValidation_ForMissingPageDisplaySize() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters("invalid", randomAlphanumeric(500), UNSUCCESSFUL_TEST_EMAIL, + "", "2016-01-25T13-23:55Z", "2016-01-25T13-23:55Z", + null, null, "424242", "4242", "2020-09-25", "2020-09-25")); + assertThat(paymentValidationException, + aValidationExceptionContaining("P0401", "Invalid parameters: state, reference, email, from_date, to_date. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnErrorValidation_forTooLargePageDisplay() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters("invalid", randomAlphanumeric(500), UNSUCCESSFUL_TEST_EMAIL, + "", "2016-01-25T13-23:55Z", "2016-01-25T13-23:55Z", + "0", "501", "424242", "4242", + "2020-09-25", "2020-09-25")); + assertThat(paymentValidationException, + aValidationExceptionContaining("P0401", "Invalid parameters: state, reference, email, from_date, to_date, page, display_size. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldNotGiveAnErrorValidation_ForNonNumberPageAndSize() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters("invalid", randomAlphanumeric(500), UNSUCCESSFUL_TEST_EMAIL, + "", "2016-01-25T13-23:55Z", "2016-01-25T13-23:55Z", + "non-numeric-page", "non-numeric-size", "424242", "4242", + "2020-09-25", "2020-09-25")); + assertThat(paymentValidationException, + aValidationExceptionContaining("P0401", "Invalid parameters: state, reference, email, from_date, to_date, page, display_size. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_card_brand_shouldGiveAnErrorValidation() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters("success", "ref", SUCCESSFUL_TEST_EMAIL, + UNSUCCESSFUL_TEST_CARD_BRAND, "2016-01-25T13:23:55Z", "2016-01-25T13:23:55Z", + "1", "1", "424242", "4242", + "2020-09-25", "2020-09-25")); + assertThat(paymentValidationException, + aValidationExceptionContaining("P0401", "Invalid parameters: card_brand. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnError_forInvalidCardPaymentState() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters("pending", "", "", + "", "", "", + "", "", "424242", + "4242", "2020-09-25", "2020-09-25")); + assertThat(paymentValidationException, + aValidationExceptionContaining("P0401", "Invalid parameters: state. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnError_forWrongLengthFirstDigits() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters( + "created", "", "", + "", "", "", + "", "", "424", "", "", "")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", + "Invalid parameters: first_digits_card_number. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnError_forInvalidFirstDigits() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters( + "", "", "", + "", "", "", + "", "", "42424b", "", "", "")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", + "Invalid parameters: first_digits_card_number. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnError_forNegativeFirstDigits() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters( + "", "", "", + "", "", "", + "", "", "-42422", "", "", "")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", + "Invalid parameters: first_digits_card_number. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnError_forNonArabicFirstDigits() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters( + "", "", "", + "", "", "", + "", "", "१२३१२३", "", "", "")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", + "Invalid parameters: first_digits_card_number. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnError_forWrongLengthLastDigits() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters( + "", "", "", + "", "", "", + "", "", "", "422", "", "")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", + "Invalid parameters: last_digits_card_number. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnError_forInvalidLastDigits() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters( + "", "", "", + "", "", "", + "", "", + "", "422a", + "", "")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", + "Invalid parameters: last_digits_card_number. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnError_forNegativeLastDigits() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters( + "", "", "", + "", "", "", + "", "", + "", "-433", + "", "")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", + "Invalid parameters: last_digits_card_number. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnError_forNonArabicLastDigits() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters( + "", "", "", + "", "", "", + "", "", + "", "१२३२", + "", "")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", "Invalid parameters: last_digits_card_number. See Public API documentation for the correct data formats")); + } + + @Test + public void validateDateParams_shouldGiveAnError_forNonISO_8601Dates() { + PaymentValidationException paymentValidationException = assertThrows(PaymentValidationException.class, + () -> PaymentSearchValidator.validateSearchParameters( + "", "", "", + "", "", "", + "", "", "", "", + "2020.09.25", "2020-09-25T10:35:00")); + assertThat(paymentValidationException, aValidationExceptionContaining("P0401", "Invalid parameters: from_settled_date, to_settled_date. See Public API documentation for the correct data formats")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/RefundSearchValidatorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/RefundSearchValidatorTest.java new file mode 100644 index 000000000..80a0fcdc2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/RefundSearchValidatorTest.java @@ -0,0 +1,117 @@ +package uk.gov.pay.api.validation; + +import org.junit.Test; +import uk.gov.pay.api.exception.RefundsValidationException; +import uk.gov.pay.api.service.RefundsParams; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertThrows; +import static uk.gov.pay.api.matcher.RefundValidationExceptionMatcher.aValidationExceptionContaining; + +public class RefundSearchValidatorTest { + + @Test + public void validateSearchParameters_shouldSuccessValidation() { + RefundSearchValidator.validateSearchParameters( + new RefundsParams("2016-01-25T13:23:55Z", "2016-01-25T13:23:55Z", "1", "1", "2016-01-25", "2016-01-25")); + } + + @Test + public void validateParams_shouldNotGiveAnErrorValidation_ForMissingPageDisplaySize() { + RefundSearchValidator.validateSearchParameters(new RefundsParams("2016-01-25T13:23:55Z", "2016-01-25T13:23:55Z", null, null, "2016-01-25", "2016-01-25")); + } + + @Test + public void validateSearchParameters_shouldGiveAValidationError_ForNonValidFromDate() { + RefundsValidationException validationException = assertThrows(RefundsValidationException.class, + () -> RefundSearchValidator.validateSearchParameters( + new RefundsParams("nope", "2016-01-25T13:23:55Z", "1", "1", "2016-01-25", "2016-01-25"))); + + assertThat(validationException, aValidationExceptionContaining("P1101", + "Invalid parameters: from_date. See Public API documentation for the correct data formats")); + } + + @Test + public void validateSearchParameters_shouldGiveAValidationError_ForNonValidToDate() { + RefundsValidationException validationException = assertThrows(RefundsValidationException.class, + () -> RefundSearchValidator.validateSearchParameters( + new RefundsParams("2016-01-25T13:23:55Z", "nope", "1", "1", "2016-01-25", "2016-01-25"))); + + assertThat(validationException, aValidationExceptionContaining("P1101", + "Invalid parameters: to_date. See Public API documentation for the correct data formats")); + } + + @Test + public void validateSearchParameters_shouldGiveAValidationError_ForNonNumericPageAndSize() { + String NON_NUMERIC_STRING = "non-numeric-string"; + RefundsValidationException validationException = assertThrows(RefundsValidationException.class, + () -> RefundSearchValidator.validateSearchParameters(new RefundsParams("2016-01-25T13:23:55Z", + "2016-01-25T13:23:55Z", NON_NUMERIC_STRING, NON_NUMERIC_STRING, + "2016-01-25", "2016-01-25"))); + + assertThat(validationException, aValidationExceptionContaining("P1101", + "Invalid parameters: page, display_size. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnErrorValidation_forZeroPageDisplay() { + RefundsValidationException validationException = assertThrows(RefundsValidationException.class, + () -> RefundSearchValidator.validateSearchParameters( + new RefundsParams("2016-01-25T13:23:55Z", "2016-01-25T13:23:55Z", "0", "0", "2016-01-25", "2016-01-25"))); + + assertThat(validationException, aValidationExceptionContaining("P1101", + "Invalid parameters: page, display_size. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnErrorValidation_forMaxedOutValuesPageDisplaySize() { + RefundsValidationException validationException = assertThrows(RefundsValidationException.class, + () -> RefundSearchValidator.validateSearchParameters(new RefundsParams("2016-01-25T13:23:55Z", + "2016-01-25T13:23:55Z", String.valueOf(Integer.MAX_VALUE + 1), String.valueOf(Integer.MAX_VALUE + 1), + "2016-01-25", "2016-01-25"))); + + assertThat(validationException, aValidationExceptionContaining("P1101", + "Invalid parameters: page, display_size. See Public API documentation for the correct data formats")); + } + + @Test + public void validateParams_shouldGiveAnErrorValidation_forTooLargePageDisplay() { + RefundsValidationException validationException = assertThrows(RefundsValidationException.class, + () -> RefundSearchValidator.validateSearchParameters( + new RefundsParams("2016-01-25T13:23:55Z", "2016-01-25T13:23:55Z", "0", "501", + "2016-01-25", "2016-01-25"))); + + assertThat(validationException, aValidationExceptionContaining("P1101", + "Invalid parameters: page, display_size. See Public API documentation for the correct data formats")); + } + + @Test + public void validateSearchParameters_shouldGiveAValidationError_ForNonValidToSettledDate() { + RefundsValidationException validationException = assertThrows(RefundsValidationException.class, + () -> RefundSearchValidator.validateSearchParameters( + new RefundsParams("2016-01-25T13:23:55Z", "2016-01-25T13:23:55Z", "1", "1", "2016-01-25", "nope"))); + + assertThat(validationException, aValidationExceptionContaining("P1101", + "Invalid parameters: to_settled_date. See Public API documentation for the correct data formats")); + } + + @Test + public void validateSearchParameters_shouldGiveAValidationError_ForNonValidFromSettledDate() { + RefundsValidationException validationException = assertThrows(RefundsValidationException.class, + () -> RefundSearchValidator.validateSearchParameters( + new RefundsParams("2016-01-25T13:23:55Z", "2016-01-25T13:23:55Z", "1", "1", "nope", "2016-01-25"))); + + assertThat(validationException, aValidationExceptionContaining("P1101", + "Invalid parameters: from_settled_date. See Public API documentation for the correct data formats")); + } + + @Test + public void validateSearchParameters_shouldGiveAValidationError_ForNonValidSettledDates() { + RefundsValidationException validationException = assertThrows(RefundsValidationException.class, + () -> RefundSearchValidator.validateSearchParameters( + new RefundsParams("2016-01-25T13:23:55Z", "2016-01-25T13:23:55Z", "1", "1", "2016-01-25T13:23:55Z", "2016-01-25T13:23:55Z"))); + + assertThat(validationException, aValidationExceptionContaining("P1101", + "Invalid parameters: from_settled_date, to_settled_date. See Public API documentation for the correct data formats")); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/URLValidatorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/URLValidatorTest.java new file mode 100644 index 000000000..8da6b1439 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/URLValidatorTest.java @@ -0,0 +1,95 @@ +package uk.gov.pay.api.validation; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static uk.gov.pay.api.validation.URLValidator.urlValidatorValueOf; + +public class URLValidatorTest { + + + @Test + public void whenIsDisabledSecureConnection_httpUrlAreValid() { + assertThat(urlValidatorValueOf(true).isValid("http://my-valid-url.com"), is(true)); + } + + @Test + public void whenIsDisabledSecureConnection_allowingLocalUrl() { + assertThat(urlValidatorValueOf(true).isValid("http://localhost:8080/pay"), is(true)); + } + + @Test + public void whenIsDisabledSecureConnection_httpsUrlsAreValid() { + assertThat(urlValidatorValueOf(true).isValid("https://my-valid-url.com"), is(true)); + } + + @Test + public void whenIsDisabledSecureConnection_doubleSlashUrlsAreValid() { + assertThat(urlValidatorValueOf(true).isValid("https://www.example.com/path-here//path-there"), is(true)); + } + + @Test + public void whenIsEnabledSecureConnection_httpUrlsAreNotValid() { + assertThat(urlValidatorValueOf(false).isValid("http://my-valid-url.com"), is(false)); + } + + @Test + public void whenIsEnabledSecureConnection_httpsUrlsAreValid() { + assertThat(urlValidatorValueOf(false).isValid("https://my-valid-url.com"), is(true)); + } + + @Test + public void whenIsEnabledSecureConnection_allowingLocalUrl() { + assertThat(urlValidatorValueOf(false).isValid("https://localhost:8080/pay"), is(true)); + } + + @Test + public void whenIsEnabledSecureConnection_allowingLocalUrl_shouldFailForHttp() { + assertThat(urlValidatorValueOf(false).isValid("http://localhost:8080/pay"), is(false)); + } + + @Test + public void whenIsEnabledSecureConnection_allowingInternalDomains() { + assertThat(urlValidatorValueOf(false).isValid("https://staging.service.core.internal/claim/pay/id/receiver"), is(true)); + } + + @Test + public void whenIsEnabledSecureConnection_disallowingEvilDomains() { + assertThat(urlValidatorValueOf(false).isValid("https://an.evil/claim/pay/id/receiver"), is(false)); + } + + @Test + public void whenIsEnabledSecureConnection_doubleSlashUrlsAreValid() { + assertThat(urlValidatorValueOf(false).isValid("https://www.example.com/path-here//path-there"), is(true)); + } + + @Test + public void whenUrlIsBlank_shouldFailValidation_whenDisabledSecureConnection() { + assertThat(urlValidatorValueOf(true).isValid(" "), is(false)); + } + + @Test + public void whenUrlIsBlank_shouldFailValidation_whenEnabledSecureConnection() { + assertThat(urlValidatorValueOf(false).isValid(" "), is(false)); + } + + @Test + public void whenUrlTldIsLocal_shouldAllowAsValid() { + assertThat(urlValidatorValueOf(false).isValid("https://a-fake-test-env.fakeservice.local/public/web/govuk-return"), is(true)); + } + + @Test + public void whenUrlIsNotAnAcceptedProtocol_disabledSecureConnection_shouldFailValidation() { + assertThat(urlValidatorValueOf(true).isValid("ftp://ftp.funet.fi/pub/standards/RFC/rfc959.txt"), is(false)); + } + + @Test + public void whenUrlIsNotAnAcceptedProtocol_enabledSecureConnection_shouldFailValidation() { + assertThat(urlValidatorValueOf(false).isValid("ftp://ftp.funet.fi/pub/standards/RFC/rfc959.txt"), is(false)); + } +} + diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/ZoneDateTimeValidatorTest.java b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/ZoneDateTimeValidatorTest.java new file mode 100644 index 000000000..6baae6f38 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/java/uk/gov/pay/api/validation/ZoneDateTimeValidatorTest.java @@ -0,0 +1,76 @@ +package uk.gov.pay.api.validation; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import uk.gov.pay.api.model.telephone.CreateTelephonePaymentRequest; +import uk.gov.pay.api.model.telephone.PaymentOutcome; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +public class ZoneDateTimeValidatorTest { + + private static CreateTelephonePaymentRequest.Builder telephoneRequestBuilder = new CreateTelephonePaymentRequest.Builder(); + + private static Validator validator; + + @BeforeAll + public static void setUpValidator() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + telephoneRequestBuilder + .withAmount(1200) + .withDescription("Some description") + .withReference("Some reference") + .withProcessorId("1PROC") + .withProviderId("1PROV") + .withCardExpiry("01/99") + .withCardType("visa") + .withLastFourDigits("1234") + .withFirstSixDigits("123456") + .withPaymentOutcome(new PaymentOutcome("success")); + } + + @Test + public void failsValidationForInvalidCreatedDate() { + + CreateTelephonePaymentRequest createTelephonePaymentRequest = telephoneRequestBuilder + .withCreatedDate("invalid date") + .build(); + + Set> constraintViolations = validator.validate(createTelephonePaymentRequest); + + assertThat(constraintViolations.size(), is(1)); + assertThat(constraintViolations.iterator().next().getMessage(), is("Field [created_date] must be a valid ISO-8601 time and date format")); + } + + @Test + public void passesValidationForNullCreatedDate() { + + CreateTelephonePaymentRequest telephoneChargeCreateRequest = telephoneRequestBuilder + .withCreatedDate(null) + .build(); + + Set> constraintViolations = validator.validate(telephoneChargeCreateRequest); + + assertThat(constraintViolations.isEmpty(), is(true)); + } + + @Test + public void passesValidationForValidCreatedDate() { + + CreateTelephonePaymentRequest telephoneChargeCreateRequest = telephoneRequestBuilder + .withCreatedDate("2018-02-21T16:04:25Z") + .build(); + + Set> constraintViolations = validator.validate(telephoneChargeCreateRequest); + + assertThat(constraintViolations.isEmpty(), is(true)); + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/config/empty-elevated-accounts-test-config.yaml b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/config/empty-elevated-accounts-test-config.yaml new file mode 100644 index 000000000..f750c0117 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/config/empty-elevated-accounts-test-config.yaml @@ -0,0 +1,51 @@ +server: + applicationConnectors: + - type: http + port: 0 + adminConnectors: + - type: http + port: 0 + +logging: + level: INFO + appenders: + - type: logstash-console + threshold: ALL + target: stdout + customFields: + container: "publicapi" + +baseUrl: http://publicapi.url/ +connectorUrl: http://connector_card.url/ +publicAuthUrl: http://publicauth.url/v1/auth +ledgerUrl: http://ledger.url/ + +jerseyClientConfig: + disabledSecureConnection: "true" + +rateLimiter: + noOfReq: 1000 + perMillis: 1000 + noOfReqForPost: 1000 + noOfReqPerNode: 1 + noOfReqForPostPerNode: 1 + elevatedAccounts: + noOfReqForElevatedAccounts: 1000 + noOfPostReqForElevatedAccounts: 1000 + lowTrafficAccounts: + noOfReqForLowTrafficAccounts: 1 + noOfPostReqForLowTrafficAccounts: 1 + intervalInMillisForLowTrafficAccounts: 1000 + +redis: + endpoint: localhost:6379 + ssl: false + commandTimeout: 250ms + connectTimeout: 500ms + +allowHttpForReturnUrl: false + +apiKeyHmacSecret: qwer9yuhgf + +# Caching authenticator. +authenticationCachePolicy: expireAfterWrite=3s diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/config/empty-low-traffic-accounts-test-config.yaml b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/config/empty-low-traffic-accounts-test-config.yaml new file mode 100644 index 000000000..8f0cae0f9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/config/empty-low-traffic-accounts-test-config.yaml @@ -0,0 +1,51 @@ +server: + applicationConnectors: + - type: http + port: 0 + adminConnectors: + - type: http + port: 0 + +logging: + level: INFO + appenders: + - type: logstash-console + threshold: ALL + target: stdout + customFields: + container: "publicapi" + +baseUrl: http://publicapi.url/ +connectorUrl: http://connector_card.url/ +publicAuthUrl: http://publicauth.url/v1/auth +ledgerUrl: http://ledger.url/ + +jerseyClientConfig: + disabledSecureConnection: "true" + +rateLimiter: + noOfReq: 1000 + perMillis: 1000 + noOfReqForPost: 1000 + noOfReqPerNode: 1 + noOfReqForPostPerNode: 1 + elevatedAccounts: 3 + noOfReqForElevatedAccounts: 1000 + noOfPostReqForElevatedAccounts: 1000 + lowTrafficAccounts: + noOfReqForLowTrafficAccounts: 4500 + noOfPostReqForLowTrafficAccounts: 2 + intervalInMillisForLowTrafficAccounts: 60000 + +redis: + endpoint: localhost:6379 + ssl: false + commandTimeout: 250ms + connectTimeout: 500ms + +allowHttpForReturnUrl: false + +apiKeyHmacSecret: qwer9yuhgf #pragma: allowlist secret + +# Caching authenticator. +authenticationCachePolicy: expireAfterWrite=3s diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/config/test-config.yaml b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/config/test-config.yaml new file mode 100644 index 000000000..939b327fb --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/config/test-config.yaml @@ -0,0 +1,55 @@ +server: + applicationConnectors: + - type: http + port: 0 + adminConnectors: + - type: http + port: 0 + requestLog: + appenders: [] + +logging: + level: ERROR + appenders: + - type: logstash-console + threshold: ERROR + target: stdout + customFields: + container: "publicapi" + +baseUrl: http://publicapi.url/ +connectorUrl: http://connector_card.url/ +publicAuthUrl: http://publicauth.url/v1/auth +ledgerUrl: http://ledger.url/ + +jerseyClientConfig: + disabledSecureConnection: "true" + +rateLimiter: + noOfReq: 1000 + perMillis: 1000 + noOfReqForPost: 1000 + noOfReqPerNode: 1 + noOfReqForPostPerNode: 1 + elevatedAccounts: 1, 2, 3 + noOfReqForElevatedAccounts: 1000 + noOfPostReqForElevatedAccounts: 1000 + lowTrafficAccounts: + noOfReqForLowTrafficAccounts: 1 + noOfPostReqForLowTrafficAccounts: 1 + intervalInMillisForLowTrafficAccounts: 1000 + +redis: + endpoint: localhost:6379 + ssl: false + commandTimeout: 250ms + connectTimeout: 500ms + +allowHttpForReturnUrl: false + +apiKeyHmacSecret: qwer9yuhgf + +# Caching authenticator. +authenticationCachePolicy: expireAfterWrite=3s + +ecsContainerMetadataUriV4: ${ECS_CONTAINER_METADATA_URI_V4:-} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/gds-test.pem b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/gds-test.pem new file mode 100644 index 000000000..db701c36a --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/gds-test.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIIDQzCCAwGgAwIBAgIEc6j17TALBgcqhkjOOAQDBQAwazELMAkGA1UEBhMCR0IxDzANBgNVBAgT +BkxvbmRvbjEPMA0GA1UEBxMGTG9uZG9uMRcwFQYDVQQKEw5DYWJpbmV0IE9mZmljZTEMMAoGA1UE +CxMDR0RTMRMwEQYDVQQDEwpHT1YuVUsgUGF5MB4XDTE2MDIyMzExMDIwOVoXDTE2MDUyMzExMDIw +OVowazELMAkGA1UEBhMCR0IxDzANBgNVBAgTBkxvbmRvbjEPMA0GA1UEBxMGTG9uZG9uMRcwFQYD +VQQKEw5DYWJpbmV0IE9mZmljZTEMMAoGA1UECxMDR0RTMRMwEQYDVQQDEwpHT1YuVUsgUGF5MIIB +tzCCASwGByqGSM44BAEwggEfAoGBAP1/U4EddRIpUt9KnC7s5Of2EbdSPO9EAMMeP4C2USZpRV1A +IlH7WT2NWPq/xfW6MPbLm1Vs14E7gB00b/JmYLdrmVClpJ+f6AR7ECLCT7up1/63xhv4O1fnxqim +FQ8E+4P208UewwI1VBNaFpEy9nXzrith1yrv8iIDGZ3RSAHHAhUAl2BQjxUjC8yykrmCouuEC/BY +HPUCgYEA9+GghdabPd7LvKtcNrhXuXmUr7v6OuqC+VdMCz0HgmdRWVeOutRZT+ZxBxCBgLRJFnEj +6EwoFhO3zwkyjMim4TwWeotUfI0o4KOuHiuzpnWRbqN/C/ohNWLx+2J6ASQ7zKTxvqhRkImog9/h +WuWfBpKLZl6Ae1UlZAFMO/7PSSoDgYQAAoGAT87XqqNgvxdt5WQmXFoK3v6ODyTn0fnPQYhplNMn +zsKXJymXYo3icuO5MEjJberRh4oxQczwlCUSIM7OcHZ/uSkKGsqwMiMDYx8MNSIhiigpfz9o3u5G +yGNmcs87aVo1Tf6j6GDg33vJdpHCSe4NrMbYPqyGYhg4gnjhMMZHpOOjMjAwMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFPq4iIoLxpLv9HG1oylt/Ioissh2MAsGByqGSM44BAMFAAMvADAsAhQG +DYECpIasjxQ6k40/xVULdYU97QIUUPRz08uA0yOYSybCXuXtun1tT1c= +-----END CERTIFICATE----- diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/logback.xml b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/logback.xml new file mode 100644 index 000000000..75328ec8e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-an-already-used-token.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-an-already-used-token.json new file mode 100644 index 000000000..a741a356c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-an-already-used-token.json @@ -0,0 +1,52 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "an authorise charge request with moto api and already used one time token", + "providerStates": [ + { + "name": "a charge with authorisation mode moto_api and one_time_token exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "ch_e36c168c41a02", + "one_time_token": "onetime-12345-token", + "one_time_token_used": "true" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/charges/authorise", + "body": { + "one_time_token": "onetime-12345-token", + "card_number": "4242424242424242", + "cvc": "123", + "expiry_date": "09/29", + "cardholder_name": "Joe Boggs" + } + }, + "response": { + "status": 400, + "body": { + "error_identifier": "ONE_TIME_TOKEN_ALREADY_USED", + "message": [ + "The one_time_token has already been used" + ] + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-invalid-card-number.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-invalid-card-number.json new file mode 100644 index 000000000..97a6b4fe0 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-invalid-card-number.json @@ -0,0 +1,52 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "an authorise charge request with moto api and invalid card number", + "providerStates": [ + { + "name": "a charge with authorisation mode moto_api and one_time_token exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "ch_e36c168c41a01", + "one_time_token": "onetime-12345-token", + "one_time_token_used": "false" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/charges/authorise", + "body": { + "one_time_token": "onetime-12345-token", + "card_number": "0000000000000000", + "cvc": "123", + "expiry_date": "09/29", + "cardholder_name": "Joe Boggs" + } + }, + "response": { + "status": 402, + "body": { + "error_identifier": "CARD_NUMBER_REJECTED", + "message": [ + "The card_number is not a valid card number" + ] + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-invalid-expiry-date.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-invalid-expiry-date.json new file mode 100644 index 000000000..de5a8e398 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-invalid-expiry-date.json @@ -0,0 +1,52 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "an authorise charge request with moto api and invalid attribute", + "providerStates": [ + { + "name": "a charge with authorisation mode moto_api and one_time_token exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "ch_e36c168c41a00", + "one_time_token": "onetime-12345-token", + "one_time_token_used": "false" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/charges/authorise", + "body": { + "one_time_token": "onetime-12345-token", + "card_number": "4242424242424242", + "cvc": "123", + "expiry_date": "09/21", + "cardholder_name": "Joe Boggs" + } + }, + "response": { + "status": 422, + "body": { + "error_identifier": "INVALID_ATTRIBUTE_VALUE", + "message": [ + "Invalid attribute value: expiry_date. Must be a valid date in the future" + ] + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-invalid-token.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-invalid-token.json new file mode 100644 index 000000000..7f6cf2dec --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-invalid-token.json @@ -0,0 +1,52 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "an authorise charge with moto api and invalid one time token", + "providerStates": [ + { + "name": "a charge with authorisation mode moto_api and one_time_token exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "ch_e36c168c41a03", + "one_time_token": "onetime-12345-token", + "one_time_token_used": "false" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/charges/authorise", + "body": { + "one_time_token": "invalid-token", + "card_number": "4242424242424242", + "cvc": "123", + "expiry_date": "09/29", + "cardholder_name": "Joe Boggs" + } + }, + "response": { + "status": 400, + "body": { + "error_identifier": "ONE_TIME_TOKEN_INVALID", + "message": [ + "The one_time_token is not valid" + ] + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-valid-token.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-valid-token.json new file mode 100644 index 000000000..63c1f4f75 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-authorise-moto-api-payment-with-valid-token.json @@ -0,0 +1,46 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "an authorise charge request with moto api and valid one time token", + "providerStates": [ + { + "name": "a charge with authorisation mode moto_api and one_time_token exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "ch_e36c168c41a04", + "one_time_token": "onetime-12345-token", + "one_time_token_used": "false" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/charges/authorise", + "body": { + "one_time_token": "onetime-12345-token", + "card_number": "4242424242424242", + "cvc": "123", + "expiry_date": "09/29", + "cardholder_name": "Joe Boggs" + } + }, + "response": { + "status": 204 + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-cancel-agreement-with-active-status.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-cancel-agreement-with-active-status.json new file mode 100644 index 000000000..a11f023de --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-cancel-agreement-with-active-status.json @@ -0,0 +1,37 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "cancel an agreement with active status", + "providerStates": [ + { + "name": "a gateway account and an active agreement exists", + "params": { + "agreement_external_id": "agreement1234567", + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/agreements/agreement1234567/cancel" + }, + "response": { + "status": 204 + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-cancel-agreement-with-cancelled-status.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-cancel-agreement-with-cancelled-status.json new file mode 100644 index 000000000..b9e5ddce5 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-cancel-agreement-with-cancelled-status.json @@ -0,0 +1,46 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "cancel an agreement with cancelled status", + "providerStates": [ + { + "name": "a gateway account and an agreement with cancelled status exists", + "params": { + "agreement_external_id": "agreement9876543", + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/agreements/agreement9876543/cancel" + }, + "response": { + "status": 400, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "error_identifier": "AGREEMENT_NOT_ACTIVE", + "message": [ + "Payment instrument not active." + ] + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-cancel-already-canceled-payment.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-cancel-already-canceled-payment.json new file mode 100644 index 000000000..ff62520fb --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-cancel-already-canceled-payment.json @@ -0,0 +1,37 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "cancel an already canceled charge", + "providerStates": [ + { + "name": "a canceled charge exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "charge8133029783750964630" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges/charge8133029783750964630/cancel" + }, + "response": { + "status": 400 + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-cancel-payment-with-created-state.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-cancel-payment-with-created-state.json new file mode 100644 index 000000000..ae1916a29 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-cancel-payment-with-created-state.json @@ -0,0 +1,37 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "cancel a charge in created state", + "providerStates": [ + { + "name": "a charge with card details exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "charge8133029783750964639" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges/charge8133029783750964639/cancel" + }, + "response": { + "status": 204 + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-capture-payment-with-delayed-capture-true-and-awaiting-capture-request-status.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-capture-payment-with-delayed-capture-true-and-awaiting-capture-request-status.json new file mode 100644 index 000000000..896cd8732 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-capture-payment-with-delayed-capture-true-and-awaiting-capture-request-status.json @@ -0,0 +1,37 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a capture charge request with delayed capture true", + "providerStates": [ + { + "name": "a charge with delayed capture true and awaiting capture request status exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "ch_e36c168c41a0" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges/ch_e36c168c41a0/capture" + }, + "response": { + "status": 204 + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-agreement-for-non-rcp-account.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-agreement-for-non-rcp-account.json new file mode 100644 index 000000000..c1fd334b7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-agreement-for-non-rcp-account.json @@ -0,0 +1,47 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create agreement request for an non RCP account", + "providerStates": [ + { + "name": "a gateway account with external id exists", + "params": { + "gateway_account_id": "123456789" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456789/agreements", + "body": { + "description": "Description for the paying user", + "reference": "Service agreement reference", + "user_identifier": "reference for the paying user" + } + }, + "response": { + "status": 422, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "error_identifier": "RECURRING_CARD_PAYMENTS_NOT_ALLOWED" + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-agreement.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-agreement.json new file mode 100644 index 000000000..3a4ff6722 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-agreement.json @@ -0,0 +1,90 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create agreement request", + "providerStates": [ + { + "name": "a gateway account with external id and recurring payment enabled exists", + "params": { + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/agreements", + "body": { + "description": "Description for the paying user describing the purpose of the agreement", + "reference": "Service agreement reference", + "user_identifier": "reference for the paying user" + } + }, + "response": { + "status": 201, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "agreement_id": "1jikqomeib6j18vp2i153b9dtu", + "created_date": "2023-06-14T13:49:06.367Z", + "reference": "Service agreement reference", + "description": "Description for the paying user describing the purpose of the agreement", + "user_identifier": "reference for the paying user" + }, + "matchingRules": { + "body": { + "$.agreement_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.reference": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.description": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.user_identifier": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-for-disabled-account.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-for-disabled-account.json new file mode 100644 index 000000000..2cfdd2968 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-for-disabled-account.json @@ -0,0 +1,54 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create charge request for disabled account", + "providerStates": [ + { + "name": "a gateway account with external id exists", + "params": { + "gateway_account_id": "123456" + } + }, + { + "name": "the gateway account is disabled", + "params": { + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges", + "body": { + "amount": 100, + "reference": "a reference", + "description": "a description", + "return_url": "https://somewhere.gov.uk/rainbow/1" + } + }, + "response": { + "status": 403, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "error_identifier": "ACCOUNT_DISABLED" + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-link-payment-with-card-number-in-reference.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-link-payment-with-card-number-in-reference.json new file mode 100644 index 000000000..99c7c382b --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-link-payment-with-card-number-in-reference.json @@ -0,0 +1,52 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create payment link payment request with card number in reference", + "providerStates": [ + { + "name": "a gateway account with external id exists", + "params": { + "gateway_account_id": "444" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/444/charges", + "body": { + "amount": 100, + "reference": "4242 4242 4242 4242", + "description": "a description", + "return_url": "https://gov.uk", + "source": "CARD_PAYMENT_LINK" + } + }, + "response": { + "status": 400, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "error_identifier": "CARD_NUMBER_IN_PAYMENT_LINK_REFERENCE_REJECTED", + "message": [ + "Card number entered in a payment link reference" + ] + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-to-setup-agreement.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-to-setup-agreement.json new file mode 100644 index 000000000..d6e87e251 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-to-setup-agreement.json @@ -0,0 +1,174 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create payment to setup agreement request", + "providerStates": [ + { + "name": "a gateway account and a new agreement exists", + "params": { + "gateway_account_id": "123456", + "agreement_external_id": "i6sjhoa36s1lhtjl07vuuhbm72" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges", + "body": { + "amount": 1968, + "reference" : "a-valid-reference", + "description": "a-valid-description", + "return_url": "https://www.google.com", + "agreement_id": "i6sjhoa36s1lhtjl07vuuhbm72", + "save_payment_instrument_to_agreement": true + } + }, + "response": { + "status": 201, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 1968, + "state": { + "finished": false, + "status": "created" + }, + "description": "a-valid-description", + "reference": "a-valid-reference", + "language": "en", + "links": [ + { + "rel": "self", + "method": "GET", + "href": "https://localhost:62970/v1/api/accounts/652132679/charges/iinvkbkkrt8kcl0atps9q7p7cm" + }, + { + "rel": "refunds", + "method": "GET", + "href": "https://localhost:62970/v1/api/accounts/652132679/charges/iinvkbkkrt8kcl0atps9q7p7cm/refunds" + }, + { + "rel": "next_url", + "method": "GET", + "href": "http://CardFrontend/secure/efbdf987-3c91-4005-b892-9d056a4bd414" + }, + { + "rel": "next_url_post", + "method": "POST", + "href": "http://CardFrontend/secure", + "type": "application/x-www-form-urlencoded", + "params": { + "chargeTokenId": "efbdf987-3c91-4005-b892-9d056a4bd414" + } + } + ], + "charge_id": "iinvkbkkrt8kcl0atps9q7p7cm", + "return_url": "http://service.local/success-page/", + "payment_provider": "sandbox", + "created_date": "2023-06-28T13:15:47.097Z", + "refund_summary": { + "status": "pending", + "user_external_id": null, + "amount_available": 1968, + "amount_submitted": 0 + }, + "settlement_summary": { + "capture_submit_time": null, + "captured_date": null + }, + "delayed_capture": false, + "moto": false, + "agreement_id": "i6sjhoa36s1lhtjl07vuuhbm72", + "authorisation_mode": "web" + }, + "matchingRules": { + "header": { + "Location": { + "matchers": [ + { + "regex": "https*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/[a-z0-9]*" + } + ] + } + }, + "body": { + "$.links[0].href": { + "matchers": [ + { + "regex": "https*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/[a-z0-9]*" + } + ] + }, + "$.links[1].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[2].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[2].params.chargeTokenId": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[3].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[3].params.chargeTokenId": { + "matchers": [ + {"match": "type"} + ] + }, + "$.charge_id": { + "matchers": [{"match": "type"}] + }, + "$.amount": { + "matchers": [{"match": "type"}] + }, + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.email": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.state.status": { + "matchers": [{"match": "type"}] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.payment_provider": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-authorisation-mode-moto-api-not-allowed.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-authorisation-mode-moto-api-not-allowed.json new file mode 100644 index 000000000..7601483a1 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-authorisation-mode-moto-api-not-allowed.json @@ -0,0 +1,48 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create charge request when gateway account has moto_api auth mode disabled", + "providerStates": [ + { + "name": "a gateway account with MOTO enabled and an external id 667 exists in the database", + "params": { + "gateway_account_id": "667" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/667/charges", + "body": { + "amount": 100, + "reference": "a reference", + "description": "a description", + "authorisation_mode": "moto_api" + } + }, + "response": { + "status": 422, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "error_identifier": "AUTHORISATION_API_NOT_ALLOWED" + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-authorisation-mode-moto-api.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-authorisation-mode-moto-api.json new file mode 100644 index 000000000..c19789ad2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-authorisation-mode-moto-api.json @@ -0,0 +1,154 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create charge request with authorisation_mode of moto_api", + "providerStates": [ + { + "name": "a gateway account with external id exists", + "params": { + "gateway_account_id": "123456" + } + }, + { + "name": "a gateway account has authorisation_api enabled", + "params": { + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges", + "body": { + "amount": 100, + "reference": "a reference", + "description": "a description", + "authorisation_mode": "moto_api" + } + }, + "response": { + "status": 201, + "headers": { + "Content-Type": "application/json", + "Location": "/v1/api/accounts/123456/charges/ch_123abc456def" + }, + "body": { + "charge_id": "ch_123abc456def", + "amount": 100, + "reference": "a reference", + "description": "a description", + "state": { + "status": "created", + "finished": false + }, + "payment_provider": "Sandbox", + "created_date": "2016-01-01T12:00:00Z", + "language": "en", + "delayed_capture": false, + "moto": true, + "authorisation_mode": "moto_api", + "links": [ + { + "href": "https://connector/v1/api/accounts/123456/charges/ch_123abc456def", + "rel": "self", + "method": "GET" + }, + { + "rel": "refunds", + "href": "url" + }, + { + "href": "https://connector/v1/api/charges/authorise", + "rel": "auth_url_post", + "type": "application/json", + "params": { + "one_time_token": "token_1234567asdf" + }, + "method": "POST" + } + ] + }, + "matchingRules": { + "header": { + "Location": { + "matchers": [ + { + "regex": "https*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/[a-z0-9]*" + } + ] + } + }, + "body": { + "$.charge_id": { + "matchers": [{"match": "type"}] + }, + "$.amount": { + "matchers": [{"match": "type"}] + }, + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.email": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.state.status": { + "matchers": [{"match": "type"}] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.payment_provider": { + "matchers": [{"match": "type"}] + }, + "$.moto": { + "matchers": [{"match": "type"}] + }, + "$.delayed_capture": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.links[0].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[1].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[2].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[2].params.one_time_token": { + "matchers": [ + {"match": "type"} + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-credentials-in-created-not-allowed.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-credentials-in-created-not-allowed.json new file mode 100644 index 000000000..227876300 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-credentials-in-created-not-allowed.json @@ -0,0 +1,48 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create charge request with gateway account credentials in CREATED state is not allowed", + "providerStates": [ + { + "name": "a Worldpay gateway account with id 444 with gateway account credentials with id 555 and valid credentials", + "params": { + "gateway_account_id": "444" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/444/charges", + "body": { + "amount": 100, + "reference": "a reference", + "description": "a description", + "return_url": "https://somewhere.gov.uk/rainbow/1" + } + }, + "response": { + "status": 400, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "error_identifier": "ACCOUNT_NOT_LINKED_WITH_PSP" + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-idempotency-key-200-response.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-idempotency-key-200-response.json new file mode 100644 index 000000000..972477883 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-idempotency-key-200-response.json @@ -0,0 +1,104 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create charge request with idempotency key and same request body", + "providerStates": [ + { + "name": "a gateway account and an active agreement exists", + "params": { + "agreement_external_id": "abcdefghijklmnopqrstuvwxyz", + "gateway_account_id": "123456" + } + }, + { + "name": "a charge created with an idempotency key for an agreement exists", + "params": { + "created": "2023-04-20T13:30:00.000Z", + "gateway_account_id": "123456", + "agreement_external_id": "abcdefghijklmnopqrstuvwxyz", + "charge_external_id": "chargeable", + "idempotency_key": "Ida the idempotency key", + "amount": "2046", + "reference": "referential", + "description": "describable" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges", + "headers" : { + "Idempotency-Key": "Ida the idempotency key" + }, + "body": { + "amount": 2046, + "reference": "referential", + "description": "describable", + "agreement_id": "abcdefghijklmnopqrstuvwxyz", + "authorisation_mode": "agreement" + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "charge_id": "chargeable", + "amount": 2046, + "reference": "referential", + "description": "describable", + "state": { + "status": "created", + "finished": false + }, + "payment_provider": "sandbox", + "created_date": "2023-04-20T13:30:00.000Z", + "agreement_id": "abcdefghijklmnopqrstuvwxyz", + "authorisation_mode": "agreement", + "links": [ + { + "href": "http://connector.service.backend/v1/api/accounts/123456/charges/chargeable", + "rel": "self", + "method": "GET" + }, + { + "rel": "refunds", + "href": "url" + } + ] + }, + "matchingRules": { + "body": { + "$.links[0].href": { + "matchers": [ + { + "regex": "https*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/[a-z0-9]*" + } + ] + }, + "$.links[1].href": { + "matchers": [ + {"match": "type"} + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-idempotency-key-409-response.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-idempotency-key-409-response.json new file mode 100644 index 000000000..04e01c181 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-idempotency-key-409-response.json @@ -0,0 +1,67 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create charge request with idempotency key and different request body", + "providerStates": [ + { + "name": "a gateway account and an active agreement exists", + "params": { + "agreement_external_id": "abcdefghijklmnopqrstuvwxyz", + "gateway_account_id": "123456" + } + }, + { + "name": "a charge created with an idempotency key for an agreement exists", + "params": { + "created": "2023-04-20T13:30:00.000Z", + "gateway_account_id": "123456", + "agreement_external_id": "abcdefghijklmnopqrstuvwxyz", + "charge_external_id": "chargeable", + "idempotency_key": "Ida the idempotency key", + "amount": "2046", + "reference": "referential", + "description": "describable" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges", + "headers" : { + "Idempotency-Key": "Ida the idempotency key" + }, + "body": { + "amount": 2046, + "reference": "different referential", + "description": "describable", + "agreement_id": "abcdefghijklmnopqrstuvwxyz", + "authorisation_mode": "agreement" + } + }, + "response": { + "status": 409, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "message": ["The Idempotency-Key has already been used to create a payment"], + "error_identifier": "IDEMPOTENCY_KEY_USED" + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-invalid-return-url.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-invalid-return-url.json new file mode 100644 index 000000000..a4978b484 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-invalid-return-url.json @@ -0,0 +1,51 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create web payment request with invalid return_url", + "providerStates": [ + { + "name": "a gateway account with external id exists", + "params": { + "gateway_account_id": "444" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/444/charges", + "body": { + "amount": 100, + "reference": "a reference", + "description": "a description", + "return_url": "invalid" + } + }, + "response": { + "status": 422, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "error_identifier": "INVALID_ATTRIBUTE_VALUE", + "message": [ + "Invalid attribute value: return_url. Must be a valid URL format" + ] + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-minimum-fields.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-minimum-fields.json new file mode 100644 index 000000000..b3efc83a2 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-minimum-fields.json @@ -0,0 +1,159 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create charge request with minimum fields", + "providerStates": [ + { + "name": "a gateway account with external id exists", + "params": { + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges", + "body": { + "amount": 100, + "reference": "a reference", + "description": "a description", + "return_url": "https://somewhere.gov.uk/rainbow/1" + } + }, + "response": { + "status": 201, + "headers": { + "Content-Type": "application/json", + "Location": "/v1/api/accounts/123456/charges/ch_ab2341da231434l" + }, + "body": { + "charge_id": "ch_ab2341da231434l", + "amount": 100, + "reference": "a reference", + "description": "a description", + "state": { + "status": "created", + "finished": false + }, + "return_url": "https://somewhere.gov.uk/rainbow/1", + "payment_provider": "Sandbox", + "created_date": "2016-01-01T12:00:00Z", + "language": "en", + "delayed_capture": false, + "moto": false, + "links": [ + { + "href": "http://connector.service.backend/v1/api/accounts/123456/charges/ch_ab2341da231434l", + "rel": "self", + "method": "GET" + }, + { + "rel": "refunds", + "href": "url" + }, + { + "href": "http://frontend_connector/charge/token_1234567asdf", + "rel": "next_url", + "method": "GET" + }, + { + "href": "http://frontend_connector/charge/", + "rel": "next_url_post", + "type": "application/x-www-form-urlencoded", + "params": { + "chargeTokenId": "token_1234567asdf" + }, + "method": "POST" + } + ] + }, + "matchingRules": { + "header": { + "Location": { + "matchers": [ + { + "regex": "https*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/[a-z0-9]*" + } + ] + } + }, + "body": { + "$.links[0].href": { + "matchers": [ + { + "regex": "https*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/[a-z0-9]*" + } + ] + }, + "$.links[1].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[2].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[2].params.chargeTokenId": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[3].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[3].params.chargeTokenId": { + "matchers": [ + {"match": "type"} + ] + }, + "$.charge_id": { + "matchers": [{"match": "type"}] + }, + "$.amount": { + "matchers": [{"match": "type"}] + }, + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.email": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.state.status": { + "matchers": [{"match": "type"}] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.payment_provider": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-missing-return-url.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-missing-return-url.json new file mode 100644 index 000000000..4a6c61731 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-missing-return-url.json @@ -0,0 +1,50 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create web payment request with missing return_url", + "providerStates": [ + { + "name": "a gateway account with external id exists", + "params": { + "gateway_account_id": "444" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/444/charges", + "body": { + "amount": 100, + "reference": "a reference", + "description": "a description" + } + }, + "response": { + "status": 422, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "error_identifier": "MISSING_MANDATORY_ATTRIBUTE", + "message": [ + "Missing mandatory attribute: return_url" + ] + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-moto-not-allowed.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-moto-not-allowed.json new file mode 100644 index 000000000..bebd2e550 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-moto-not-allowed.json @@ -0,0 +1,49 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create charge request when gateway account has moto disabled", + "providerStates": [ + { + "name": "a gateway account with external id exists", + "params": { + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges", + "body": { + "amount": 100, + "reference": "a reference", + "description": "a description", + "return_url": "https://somewhere.gov.uk/rainbow/1", + "moto": true + } + }, + "response": { + "status": 422, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "error_identifier": "MOTO_NOT_ALLOWED" + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-zero-amount-not-allowed.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-zero-amount-not-allowed.json new file mode 100644 index 000000000..3fcb64be9 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment-with-zero-amount-not-allowed.json @@ -0,0 +1,48 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create charge request with zero amount when zero amount is not allowed", + "providerStates": [ + { + "name": "a gateway account with external id exists", + "params": { + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges", + "body": { + "amount": 0, + "reference": "a reference", + "description": "a description", + "return_url": "https://somewhere.gov.uk/rainbow/1" + } + }, + "response": { + "status": 422, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "error_identifier": "ZERO_AMOUNT_NOT_ALLOWED" + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment.json new file mode 100644 index 000000000..13e410ba3 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-payment.json @@ -0,0 +1,200 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create charge request", + "providerStates": [ + { + "name": "a gateway account with external id exists", + "params": { + "gateway_account_id": "123456" + } + }, + { + "name": "a gateway account has moto payments enabled", + "params": { + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges", + "body": { + "amount": 100, + "reference": "a reference", + "description": "a description", + "return_url": "https://somewhere.gov.uk/rainbow/1", + "email": "joe.bogs@example.org", + "language": "cy", + "prefilled_cardholder_details": { + "cardholder_name": "J. Bogs", + "billing_address": { + "line1": "address line 1", + "line2": "address line 2", + "postcode": "AB1 CD2", + "city": "address city", + "country": "GB" + } + }, + "delayed_capture": true, + "moto": true, + "metadata": { + "ledger_code": 123, + "fund_code": "ISIN122038", + "cancellable": false + } + } + }, + "response": { + "status": 201, + "headers": { + "Content-Type": "application/json", + "Location": "/v1/api/accounts/123456/charges/ch_ab2341da231434l" + }, + "body": { + "charge_id": "ch_ab2341da231434l", + "amount": 100, + "reference": "a reference", + "description": "a description", + "state": { + "status": "created", + "finished": false + }, + "return_url": "https://somewhere.gov.uk/rainbow/1", + "payment_provider": "Sandbox", + "created_date": "2016-01-01T12:00:00Z", + "language": "cy", + "delayed_capture": true, + "moto": true, + "email": "joe.bogs@example.org", + "card_details": { + "cardholder_name": "J. Bogs", + "billing_address": { + "line1": "address line 1", + "line2": "address line 2", + "postcode": "AB1 CD2", + "city": "address city", + "country": "GB" + } + }, + "metadata": { + "ledger_code": 123, + "fund_code": "ISIN122038", + "cancellable": false + }, + "links": [ + { + "href": "http://connector.service.backend/v1/api/accounts/123456/charges/ch_ab2341da231434l", + "rel": "self", + "method": "GET" + }, + { + "rel": "refunds", + "href": "url" + }, + { + "href": "http://frontend_connector/charge/token_1234567asdf", + "rel": "next_url", + "method": "GET" + }, + { + "href": "http://frontend_connector/charge/", + "rel": "next_url_post", + "type": "application/x-www-form-urlencoded", + "params": { + "chargeTokenId": "token_1234567asdf" + }, + "method": "POST" + } + ] + }, + "matchingRules": { + "header": { + "Location": { + "matchers": [ + { + "regex": "https*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/[a-z0-9]*" + } + ] + } + }, + "body": { + "$.links[0].href": { + "matchers": [ + { + "regex": "https*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/[a-z0-9]*" + } + ] + }, + "$.links[1].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[2].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[2].params.chargeTokenId": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[3].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[3].params.chargeTokenId": { + "matchers": [ + {"match": "type"} + ] + }, + "$.charge_id": { + "matchers": [{"match": "type"}] + }, + "$.amount": { + "matchers": [{"match": "type"}] + }, + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.email": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.state.status": { + "matchers": [{"match": "type"}] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.payment_provider": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-refund-for-disabled-account.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-refund-for-disabled-account.json new file mode 100644 index 000000000..bbf950942 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-refund-for-disabled-account.json @@ -0,0 +1,53 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create refund request for disabled account", + "providerStates": [ + { + "name": "a charge exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "654321" + } + }, + { + "name": "the gateway account is disabled", + "params": { + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges/654321/refunds", + "body": { + "amount": 100, + "refund_amount_available": 100 + } + }, + "response": { + "status": 403, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "error_identifier": "ACCOUNT_DISABLED" + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-telephone-payment-notification.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-telephone-payment-notification.json new file mode 100644 index 000000000..d5dccd39e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-create-telephone-payment-notification.json @@ -0,0 +1,278 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a create payment notification for telephone payments", + "providerStates": [ + { + "name": "a gateway account with external id exists", + "params": { + "gateway_account_id": "123456" + } + }, + { + "name": "a gateway account has telephone payment notifications enabled", + "params": { + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/telephone-charges", + "body": { + "amount": 12000, + "reference": "MRPC12345", + "description": "New passport application", + "created_date": "2018-02-21T16:04:25Z", + "authorised_date": "2018-02-21T16:05:33Z", + "processor_id": "183f2j8923j8", + "provider_id": "17498-8412u9-1273891239", + "auth_code": "auth12345", + "payment_outcome": { + "status": "failed", + "code": "P0010", + "supplemental": { + "error_code": "ECKOH01234", + "error_message": "textual message describing error code" + } + }, + "card_type": "master-card", + "name_on_card": "J Doe", + "email_address": "j.doe@example.com", + "card_expiry": "02/19", + "last_four_digits": "1234", + "first_six_digits": "654321", + "telephone_number": "+447700900796" + } + }, + "response": { + "status": 201, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 12000, + "state": { + "finished": true, + "code": "P0010", + "message": "Payment method rejected", + "status": "failed" + }, + "description": "New passport application", + "reference": "MRPC12345", + "links": [], + "charge_id": "t081qm4gudopckmq9r4ernnu1r", + "email": "j.doe@example.com", + "telephone_number": "+447700900796", + "processor_id": "183f2j8923j8", + "provider_id": "17498-8412u9-1273891239", + "created_date": "2018-02-21T16:04:25.000Z", + "authorised_date": "2018-02-21T16:05:33.000Z", + "payment_outcome": { + "status": "failed", + "code": "P0010", + "supplemental": { + "error_code": "ECKOH01234", + "error_message": "textual message describing error code" + } + }, + "auth_code": "auth12345", + "card_details": { + "last_digits_card_number": "1234", + "first_digits_card_number": "654321", + "cardholder_name": "J Doe", + "expiry_date": "02/19", + "card_brand": "master-card" + }, + "delayed_capture": false + }, + "matchingRules": { + "body": { + "$.charge_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.reference": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.email": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.description": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.state.code": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.state.finished": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.state.message": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.telephone_number": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.processor_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.provider_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.authorised_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.payment_outcome.code": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.payment_outcome.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.payment_outcome.supplemental.error_code": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.payment_outcome.supplemental.error_message": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.auth_code": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.card_details.last_digits_card_number": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.card_details.first_digits_card_number": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.card_details.cardholder_name": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.card_details.expiry_date": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.card_details.card_brand": { + "matchers": [ + { + "match": "type" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } + } diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-capturable-payment.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-capturable-payment.json new file mode 100644 index 000000000..67c3e2ac7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-capturable-payment.json @@ -0,0 +1,142 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "get a delayed capture charge with metadata that is in a capturable status", + "providerStates": [ + { + "name": "a charge exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "ch_123abc456def", + "gateway_transaction_id": "gateway-tx-123456", + "account_id": "123456", + "metadata": "{\"ledger_code\":123, \"some_key\":\"key\"}" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/api/accounts/123456/charges/ch_123abc456def" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "metadata": { + "ledger_code": 123, + "some_key": "key" + }, + "amount": 100, + "state": { + "finished": false, + "status": "capturable" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "links": [ + { + "rel": "self", + "method": "GET", + "href": "https://connector/v1/api/accounts/123456/charges/ch_123abc456def" + }, + { + "rel": "refunds", + "method": "GET", + "href": "https://connector/v1/api/accounts/123456/charges/ch_123abc456def/refunds" + }, + { + "rel": "capture", + "method": "POST", + "href": "https://connector/v1/api/accounts/123456/charges/ch_123abc456def/capture" + } + ], + "charge_id": "ch_123abc456def", + "gateway_transaction_id": "gateway-tx-123456", + "return_url": "https://somewhere.gov.uk/rainbow/1", + "payment_provider": "sandbox", + "created_date": "2018-10-16T10:46:02.121Z", + "corporate_card_surcharge": 250, + "total_amount": 350, + "refund_summary": { + "status": "unavailable", + "user_external_id": null, + "amount_available": 350, + "amount_submitted": 0 + }, + "settlement_summary": { + "capture_submit_time": null, + "captured_date": null + }, + "delayed_capture": true, + "moto": false, + "authorisation_summary": { + "three_d_secure": { + "required": true, + "version": "2.1.0" + } + } + }, + "matchingRules": { + "body": { + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.state.status": { + "matchers": [{"match": "regex", "regex": "submitted|capturable"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.links[0].href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/ch_123abc456def" + } + ] + }, + "$.links[1].href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/ch_123abc456def\/refunds" + } + ] + }, + "$.links[2].href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/ch_123abc456def\/capture" + } + ] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.refund_summary.amount_available": { + "matchers": [{"match": "type"}] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-created-payment.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-created-payment.json new file mode 100644 index 000000000..499fbd8b8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-created-payment.json @@ -0,0 +1,154 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "get a charge request for a created payment", + "providerStates": [ + { + "name": "a charge with delayed capture true exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "ch_123abc456def" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/api/accounts/123456/charges/ch_123abc456def" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 100, + "state": { + "finished": false, + "status": "created" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "links": [ + { + "rel": "self", + "method": "GET", + "href": "https://connector/v1/api/accounts/123456/charges/ch_123abc456def" + }, + { + "rel": "refunds", + "method": "GET", + "href": "https://connector/v1/api/accounts/123456/charges/ch_123abc456def/refunds" + }, + { + "rel": "next_url", + "method": "GET", + "href": "https://card_frontend/secure/ae749781-6562-4e0e-8f56-32d9639079dc" + }, + { + "rel": "next_url_post", + "method": "POST", + "href": "https://card_frontend/secure", + "type": "application/x-www-form-urlencoded", + "params": { + "chargeTokenId": "ae749781-6562-4e0e-8f56-32d9639079dc" + } + } + ], + "charge_id": "ch_123abc456def", + "return_url": "https://somewhere.gov.uk/rainbow/1", + "payment_provider": "sandbox", + "created_date": "2018-09-07T13:12:02.121Z", + "refund_summary": { + "status": "pending", + "user_external_id": null, + "amount_available": 100, + "amount_submitted": 0 + }, + "settlement_summary": { + "capture_submit_time": null, + "captured_date": null + }, + "delayed_capture": true, + "moto": false, + "authorisation_mode": "web" + }, + "matchingRules": { + "body": { + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.links[0].href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/ch_123abc456def" + } + ] + }, + "$.links[1].href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/ch_123abc456def\/refunds" + } + ] + }, + "$.links[2].href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/secure\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" + } + ] + }, + "$.links[3].href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/secure" + } + ] + }, + "$.links[3].params.chargeTokenId": { + "matchers": [ + {"match": "type"} + ] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.delayed_capture": { + "matchers": [{"match": "type"}] + }, + "$.moto": { + "matchers": [{"match": "type"}] + }, + "$.payment_provider": { + "matchers": [{"match": "type"}] + }, + "$.authorisation_mode": { + "matchers": [{"match": "type"}] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-motoapi-created-payment.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-motoapi-created-payment.json new file mode 100644 index 000000000..674cd8901 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-motoapi-created-payment.json @@ -0,0 +1,133 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "get a charge request for a moto_api created payment", + "providerStates": [ + { + "name": "a charge with authorisation mode moto_api exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "ch_123abc456def" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/api/accounts/123456/charges/ch_123abc456def" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "charge_id": "ch_123abc456def", + "amount": 100, + "reference": "a reference", + "description": "a description", + "state": { + "status": "created", + "finished": false + }, + "payment_provider": "Sandbox", + "created_date": "2016-01-01T12:00:00Z", + "language": "en", + "delayed_capture": false, + "moto": true, + "authorisation_mode": "moto_api", + "links": [ + { + "href": "https://connector/v1/api/accounts/123456/charges/ch_123abc456def", + "rel": "self", + "method": "GET" + }, + { + "rel": "refunds", + "href": "url" + }, + { + "href": "https://connector/v1/api/charges/authorise", + "rel": "auth_url_post", + "type": "application/json", + "params": { + "one_time_token": "token_1234567asdf" + }, + "method": "POST" + } + ] + }, + "matchingRules": { + "body": { + "$.charge_id": { + "matchers": [{"match": "type"}] + }, + "$.amount": { + "matchers": [{"match": "type"}] + }, + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.email": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.state.status": { + "matchers": [{"match": "type"}] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.payment_provider": { + "matchers": [{"match": "type"}] + }, + "$.moto": { + "matchers": [{"match": "type"}] + }, + "$.delayed_capture": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.links[0].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[1].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[2].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.links[2].params.one_time_token": { + "matchers": [ + {"match": "type"} + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-payment-events.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-payment-events.json new file mode 100644 index 000000000..c28c456f0 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-payment-events.json @@ -0,0 +1,166 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "get payment events", + "providerStates": [ + { + "name": "Gateway account 42 exists and has a charge with id abc123 and has CREATED and AUTHORISATION_REJECTED charge events", + "params": { + "gateway_account_id": "42", + "charge_id": "abc123" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/api/accounts/42/charges/abc123/events" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "charge_id": "abc123", + "events": [ + { + "type": "PAYMENT", + "submitted_by": null, + "state": { + "status": "created", + "finished": false + }, + "amount": 100, + "updated": "2019-08-06T10:34:43.487Z", + "refund_reference": null + }, + { + "type": "PAYMENT", + "submitted_by": null, + "state": { + "status": "failed", + "finished": true, + "code": "P0010", + "message": "Payment method rejected" + }, + "amount": 100, + "updated": "2019-08-06T10:34:48.487Z", + "refund_reference": null + } + ] + }, + "matchingRules": { + "body": { + "$.charge_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.events[0].type": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.events[0].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.events[0].state.finished": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.events[0].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.events[0].updated": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + + }, + "$.events[1].type": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.events[1].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.events[1].state.finished": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.events[1].state.code": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.events[1].state.message": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.events[1].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.events[1].updated": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-payment-refund.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-payment-refund.json new file mode 100644 index 000000000..c29d7e8ab --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-payment-refund.json @@ -0,0 +1,81 @@ + +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "get payment refund", + "providerStates": [ + { + "name": "a payment refund exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "123456789", + "refund_id": "r_123abc456def", + "created_date": "2018-09-22T10:14:16.067Z" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/api/accounts/123456/charges/123456789/refunds/r_123abc456def" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "refund_id": "r_123abc456def", + "amount": 100, + "status": "success", + "created_date": "2018-09-22T10:14:16.067Z" + }, + "matchingRules": { + "body": { + "$.amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.status": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.refund_id": { + "matchers": [ + { + "match": "type" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-payment-refunds.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-payment-refunds.json new file mode 100644 index 000000000..675617340 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-payment-refunds.json @@ -0,0 +1,210 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "Get refunds for a payment", + "providerStates": [ + { + "name": "Refunds exist for a charge", + "params": { + "account_id": "123456", + "charge_id": "charge8133029783750222" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/api/accounts/123456/charges/charge8133029783750222/refunds" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "payment_id": "charge8133029783750222", + "_links": { + "self": { + "href": "https://connector/v1/api/accounts/123456/charges/charge8133029783750222/refunds" + }, + "payment": { + "href": "https://connector/v1/api/accounts/123456/charges/charge8133029783750222" + } + }, + "_embedded": { + "refunds": [ + { + "amount": 1, + "created_date": "2016-01-25T13:23:55.000Z", + "refund_id": "di0qnu9ucdo7aslhatci6h90jk", + "user_external_id": "d77458fb878b4aba88b8164368be3d16", + "status": "success", + "_links": { + "self": { + "href": "https://app.com/v1/api/accounts/123456/charges/charge8133029783750222/refunds/di0qnu9ucdo7aslhatci6h90jk" + }, + "payment": { + "href": "https://app.com/v1/api/accounts/123456/charges/charge8133029783750222" + } + } + }, + { + "amount": 1, + "created_date": "2016-01-25T16:23:55.000Z", + "refund_id": "m16ufgc3t23l766ljhv9eicsn5", + "user_external_id": "d77458fb878b4aba88b8164368be3d16", + "status": "error", + "_links": { + "self": { + "href": "https://app.com/v1/api/accounts/123456/charges/charge8133029783750222/refunds/m16ufgc3t23l766ljhv9eicsn5" + }, + "payment": { + "href": "https://app.com/v1/api/accounts/123456/charges/charge8133029783750222" + } + } + } + ] + } + }, + "matchingRules": { + "body": { + "$.payment_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*://.*/v1/api/accounts/123456/charges/charge8133029783750222/refunds" + } + ] + }, + "$._links.payment.href": { + "matchers": [ + { + "regex": "http.*://.*/v1/api/accounts/123456/charges/charge8133029783750222" + } + ] + }, + "$._embedded.refunds[0].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$._embedded.refunds[0].created_date": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$._embedded.refunds[0].refund_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$._embedded.refunds[0].user_external_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$._embedded.refunds[0].status": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$._embedded.refunds[0]._links.self.href": { + "matchers": [ + { + "regex": "http.*://.*/v1/api/accounts/123456/charges\/[a-z0-9]*\/refunds\/[a-z0-9]*" + } + ] + }, + "$._embedded.refunds[0]._links.payment.href": { + "matchers": [ + { + "regex": "http.*://.*/v1/api/accounts/123456/charges/charge8133029783750222" + } + ] + }, + + + "$._embedded.refunds[1].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$._embedded.refunds[1].created_date": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$._embedded.refunds[1].refund_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$._embedded.refunds[1].user_external_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$._embedded.refunds[1].status": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$._embedded.refunds[1]._links.self.href": { + "matchers": [ + { + "regex": "http.*://.*/v1/api/accounts/123456/charges\/[a-z0-9]*\/refunds\/[a-z0-9]*" + } + ] + }, + "$._embedded.refunds[1]._links.payment.href": { + "matchers": [ + { + "regex": "http.*://.*/v1/api/accounts/123456/charges/charge8133029783750222" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-payment-with-fee-and-net-amount.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-payment-with-fee-and-net-amount.json new file mode 100644 index 000000000..807053e60 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-payment-with-fee-and-net-amount.json @@ -0,0 +1,130 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "get charge which has a fee and a net amount", + "providerStates": [ + { + "name": "a charge with fee and net_amount exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "ch_123abc456def" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/api/accounts/123456/charges/ch_123abc456def" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 100, + "fee": 5, + "net_amount": 345, + "state": { + "finished": true, + "status": "success" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "links": [ + { + "rel": "self", + "method": "GET", + "href": "https://connector/v1/api/accounts/123456/charges/ch_123abc456def" + }, + { + "rel": "refunds", + "method": "GET", + "href": "https://connector/v1/api/accounts/123456/charges/ch_123abc456def/refunds" + } + ], + "charge_id": "ch_123abc456def", + "gateway_transaction_id": "gateway-tx-123456", + "return_url": "https://somewhere.gov.uk/rainbow/1", + "payment_provider": "sandbox", + "created_date": "2018-10-16T10:46:02.121Z", + "refund_summary": { + "status": "available", + "user_external_id": null, + "amount_available": 350, + "amount_submitted": 0 + }, + "settlement_summary": { + "capture_submit_time": null, + "captured_date": null + }, + "delayed_capture": false, + "moto": false + }, + "matchingRules": { + "body": { + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.links[0].href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/ch_123abc456def" + } + ] + }, + "$.links[1].href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/ch_123abc456def\/refunds" + } + ] + }, + "$.links[2].href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/ch_123abc456def\/capture" + } + ] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.refund_summary.amount_available": { + "matchers": [{"match": "type"}] + }, + "$.gateway_transaction_id": { + "matchers": [{"match": "type"}] + }, + "$.fee": { + "matchers": [{"match": "type"}] + }, + "$.net_amount": { + "matchers": [{"match": "type"}] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-rejected-rcp-payment-with-can-retry-true.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-rejected-rcp-payment-with-can-retry-true.json new file mode 100644 index 000000000..a6aa4c958 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-rejected-rcp-payment-with-can-retry-true.json @@ -0,0 +1,122 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "get a charge request for a rejected rcp payment", + "providerStates": [ + { + "name": "a gateway account and an active agreement exists", + "params": { + "agreement_external_id": "abcdefghijklmnopqrstuvwxyz", + "gateway_account_id": "123456" + } + }, + { + "name": "a charge with authorisation mode agreement and rejected status exists", + "params": { + "agreement_external_id": "abcdefghijklmnopqrstuvwxyz", + "charge_external_id": "ch_123abc456def", + "amount": "1968" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/api/accounts/123456/charges/ch_123abc456def" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 1968, + "state": { + "finished": true, + "status": "failed", + "code": "P0010", + "message": "Payment method rejected", + "can_retry": true + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "links": [ + { + "rel": "self", + "method": "GET", + "href": "https://connector/v1/api/accounts/123456/charges/ch_123abc456def" + }, + { + "rel": "refunds", + "method": "GET", + "href": "https://connector/v1/api/accounts/123456/charges/ch_123abc456def/refunds" + } + ], + "charge_id": "ch_123abc456def", + "return_url": "https://somewhere.gov.uk/rainbow/1", + "payment_provider": "sandbox", + "created_date": "2018-09-07T13:12:02.121Z", + "delayed_capture": false, + "moto": false, + "authorisation_mode": "agreement" + }, + "matchingRules": { + "body": { + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.links[0].href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/ch_123abc456def" + } + ] + }, + "$.links[1].href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/ch_123abc456def\/refunds" + } + ] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.delayed_capture": { + "matchers": [{"match": "type"}] + }, + "$.moto": { + "matchers": [{"match": "type"}] + }, + "$.payment_provider": { + "matchers": [{"match": "type"}] + }, + "$.authorisation_mode": { + "matchers": [{"match": "type"}] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-wallet-payment.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-wallet-payment.json new file mode 100644 index 000000000..190d32e09 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-get-wallet-payment.json @@ -0,0 +1,61 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "get a charge request for a wallet payment - connector", + "providerStates": [ + { + "name": "a charge with wallet type APPLE_PAY exists", + "params": { + "gateway_account_id": "123456", + "charge_id": "ch_123abc456def" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/api/accounts/123456/charges/ch_123abc456def" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 100, + "state": { + "finished": true, + "status": "success" + }, + "charge_id": "ch_123abc456def", + "payment_provider": "sandbox", + "card_details": { + "cardholder_name": "aName" + }, + "authorisation_mode": "web", + "wallet_type": "APPLE_PAY" + }, + "matchingRules": { + "body": { + "$.wallet_type": { + "matchers": [{"match": "type"}] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-take-a-recurring-payment.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-take-a-recurring-payment.json new file mode 100644 index 000000000..a3cd84b4f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-connector-take-a-recurring-payment.json @@ -0,0 +1,107 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "connector" + }, + "interactions": [ + { + "description": "a take a recurring payment request", + "providerStates": [ + { + "name": "a gateway account and an active agreement exists", + "params": { + "agreement_external_id": "abcdefghijklmnopqrstuvwxyz", + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "POST", + "path": "/v1/api/accounts/123456/charges", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 2046, + "reference": "a-reference", + "description": "a description", + "agreement_id": "abcdefghijklmnopqrstuvwxyz", + "authorisation_mode": "agreement" + } + }, + "response": { + "status": 201, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "charge_id": "valid-charge-id", + "amount": 2046, + "reference": "a-reference", + "description": "a description", + "state": { + "status": "created", + "finished": false + }, + "payment_provider": "sandbox", + "created_date": "2023-04-20T13:30:00.000Z", + "agreement_id": "abcdefghijklmnopqrstuvwxyz", + "authorisation_mode": "agreement", + "links": [ + { + "href": "http://connector.service.backend/v1/api/accounts/123456/charges/valid-charge-id", + "rel": "self", + "method": "GET" + }, + { + "rel": "refunds", + "href": "url" + } + ] + }, + "matchingRules": { + "body": { + "$.links[0].href": { + "matchers": [ + { + "regex": "https*:\/\/.*\/v1\/api\/accounts\/123456\/charges\/[a-z0-9]*" + } + ] + }, + "$.links[1].href": { + "matchers": [ + {"match": "type"} + ] + }, + "$.charge_id": { + "matchers": [{"match": "type"}] + }, + "$.amount": { + "matchers": [{"match": "type"}] + }, + , + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-one-agreement-not-found.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-one-agreement-not-found.json new file mode 100644 index 000000000..54d87fbf0 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-one-agreement-not-found.json @@ -0,0 +1,47 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get one agreement no result", + "providerStates": [ + { + "name": "an agreement with payment instrument exists", + "params": { + "account_id": "3456", + "agreement_external_id": "abcdefghijklmnopqrstuvwxyz" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/agreement/non-existent-agreement-id", + "headers": { + "X-Consistent": "true" + }, + "query": { + "account_id": ["3456"] + } + }, + "response": { + "status": 404 + }, + "body": { + "code": 404, + "message": "HTTP 404 Not Found" + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-one-agreement.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-one-agreement.json new file mode 100644 index 000000000..2405b9c14 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-one-agreement.json @@ -0,0 +1,94 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get one agreement", + "providerStates": [ + { + "name": "an agreement with payment instrument exists", + "params": { + "account_id": "3456", + "agreement_external_id": "abcdefghijklmnopqrstuvwxyz" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/agreement/abcdefghijklmnopqrstuvwxyz", + "headers": { + "X-Consistent": "true" + }, + "query": { + "account_id": ["3456"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "external_id": "abcdefghijklmnopqrstuvwxyz", + "service_id": "a-service-id", + "reference": "a-reference", + "description": "a description", + "status": "ACTIVE", + "live": false, + "created_date": "2023-06-20T11:07:17.021Z", + "payment_instrument": { + "external_id": "a-payment-instrument-id", + "agreement_external_id": "abcdefghijklmnopqrstuvwxyz", + "card_details": { + "cardholder_name": "J Doe", + "billing_address": { + "line1": "Address line 1", + "line2": "Address line 2", + "postcode": "EC3R8BT", + "city": "London", + "country": "UK" + }, + "card_brand": "visa", + "last_digits_card_number": "4242", + "first_digits_card_number": "424242", + "expiry_date": "10/21", + "card_type": "credit" + }, + "type": "CARD", + "created_date": "2023-06-20T11:07:17.073Z" + } + }, + "matchingRules": { + "body": { + "$.created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" + } + ] + }, + "$.payment_instrument.created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-events.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-events.json new file mode 100644 index 000000000..803da9f64 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-events.json @@ -0,0 +1,134 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get payment events", + "providerStates": [ + { + "name": "a transaction has CREATED and AUTHORISATION_REJECTED payment events", + "params": { + "gateway_account_id": "42", + "transaction_id": "abc123" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction/abc123/event", + "query": { + "gateway_account_id": ["42"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "transaction_id": "abc123", + "events": [ + { + "state": { + "status": "created", + "finished": false + }, + "timestamp": "2019-08-06T10:34:43.487123Z" + }, + { + "state": { + "status": "failed", + "finished": true, + "code": "P0010", + "message": "Payment method rejected" + }, + "timestamp": "2019-08-06T10:34:48.123456Z" + } + ] + }, + "matchingRules": { + "body": { + "$.transaction_id": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.events[0].state.status": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.events[0].state.finished": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.events[0].timestamp": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" + } + ] + + }, + "$.events[1].state.status": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.events[1].state.finished": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.events[1].state.code": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.events[1].state.message": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.events[1].timestamp": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" + } + ] + + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-refund.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-refund.json new file mode 100644 index 000000000..2c6cb5eb8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-refund.json @@ -0,0 +1,95 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get a refund transaction for a transaction", + "providerStates": [ + { + "name": "a refund transaction for a transaction exists", + "params": { + "gateway_account_id": "123456", + "transaction_external_id": "r_123abc456def", + "parent_external_id": "123456789" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction/r_123abc456def", + "query": { + "account_id": ["123456"], + "parent_external_id" :["123456789"], + "transaction_type":["REFUND"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "transaction_id": "r_123abc456def", + "amount": 100, + "state": { + "status": "success", + "finished": true + }, + "created_date": "2018-09-22T10:14:16.067Z" + }, + "matchingRules": { + "body": { + "$.amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.state.status": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.state.finished": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.created_date": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.transaction_id": { + "matchers": [ + { + "match": "value" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-refunds.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-refunds.json new file mode 100644 index 000000000..b6dc983b4 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-refunds.json @@ -0,0 +1,168 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get refund transactions for a transaction", + "providerStates": [ + { + "name": "refund transactions for a transaction exist", + "params": { + "gateway_account_id": "123456", + "transaction_external_id": "ch_123abc456xyz" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction/ch_123abc456xyz/transaction", + "query": { + "gateway_account_id": [ + "123456" + ], + "transaction_type": [ + "REFUND" + ] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "parent_transaction_id": "ch_123abc456xyz", + "transactions": [ + { + "transaction_id": "refund-transaction-id1", + "amount": 100, + "state": { + "status": "submitted", + "finished": false + }, + "created_date": "2018-09-22T10:14:16.067Z" + }, + { + "transaction_id": "refund-transaction-id2", + "amount": 200, + "state": { + "status": "error", + "finished": true, + "code": "P0050", + "message": "Payment provider returned an error" + }, + "created_date": "2018-09-22T10:16:16.067Z" + } + ] + }, + "matchingRules": { + "body": { + "$.parent_transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.transactions[0].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.transactions[0].state.status": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.transactions[0].state.finished": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.transactions[0].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.transactions[0].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.transactions[1].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.transactions[1].state.status": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.transactions[1].state.finished": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.transactions[1].state.code": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.transactions[1].state.message": { + "matchers": [ + { + "match": "value" + } + ] + }, + "$.transactions[1].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.transactions[1].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-authorisation-summary.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-authorisation-summary.json new file mode 100644 index 000000000..f67aff555 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-authorisation-summary.json @@ -0,0 +1,92 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get a payment request with 3d secure version", + "providerStates": [ + { + "name": "a transaction with 3ds version exists", + "params": { + "account_id": "123456", + "charge_id": "ch_123abc456xyz" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction/ch_123abc456xyz", + "query": { + "account_id": ["123456"], + "transaction_type": ["PAYMENT"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 100, + "state": { + "finished": false, + "status": "created" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "transaction_id": "ch_123abc456xyz", + "return_url": "https://somewhere.gov.uk/rainbow/1", + "payment_provider": "sandbox", + "created_date": "2021-09-15T17:12:02.121Z", + "refund_summary": { + "status": "pending", + "user_external_id": null, + "amount_available": 100, + "amount_submitted": 0 + }, + "settlement_summary": { + "capture_submit_time": null, + "captured_date": null + }, + "delayed_capture": false, + "authorisation_summary": { + "three_d_secure": { + "required": true, + "version": "2.1.0" + } + } + }, + "matchingRules": { + "body": { + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-corporate-surcharge.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-corporate-surcharge.json new file mode 100644 index 000000000..551f1639f --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-corporate-surcharge.json @@ -0,0 +1,97 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get a charge request with corporate surcharge", + "providerStates": [ + { + "name": "a transaction with corporate surcharge exists", + "params": { + "account_id": "123456", + "charge_id": "ch_123abc456xyz" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction/ch_123abc456xyz", + "query": { + "account_id": ["123456"], + "transaction_type": ["PAYMENT"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 2000, + "state": { + "finished": false, + "status": "created" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "transaction_id": "ch_123abc456xyz", + "return_url": "https://somewhere.gov.uk/rainbow/1", + "payment_provider": "sandbox", + "created_date": "2018-10-16T10:46:02.121Z", + "corporate_card_surcharge": 250, + "total_amount": 2250, + "refund_summary": { + "status": "available", + "user_external_id": null, + "amount_available": 2250, + "amount_submitted": 0 + }, + "settlement_summary": { + "capture_submit_time": "2018-10-16T10:46:03.121Z", + "captured_date": "2018-10-16" + }, + "delayed_capture": false + }, + "matchingRules": { + "body": { + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.settlement_summary.capture_submit_time": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.settlement_summary.captured_date": { + "matchers": [{ "date": "yyyy-MM-dd" }] + }, + "$.refund_summary.amount_available": { + "matchers": [{"match": "type"}] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-delayed-capture-true.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-delayed-capture-true.json new file mode 100644 index 000000000..86ddcdcc7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-delayed-capture-true.json @@ -0,0 +1,90 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get a charge request with delayed capture true", + "providerStates": [ + { + "name": "a transaction with delayed capture true exists", + "params": { + "account_id": "123456", + "charge_id": "ch_123abc456xyz" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction/ch_123abc456xyz", + "query": { + "account_id": ["123456"], + "transaction_type": ["PAYMENT"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 100, + "state": { + "finished": false, + "status": "created" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "transaction_id": "ch_123abc456xyz", + "return_url": "https://somewhere.gov.uk/rainbow/1", + "payment_provider": "sandbox", + "created_date": "2018-09-07T13:12:02.121Z", + "refund_summary": { + "status": "pending", + "user_external_id": null, + "amount_available": 100, + "amount_submitted": 0 + }, + "settlement_summary": { + "capture_submit_time": null, + "captured_date": null + }, + "delayed_capture": true, + "authorisation_mode": "web" + }, + "matchingRules": { + "body": { + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.authorisation_mode": { + "matchers": [{"match": "type"}] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-fee-and-net-amount.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-fee-and-net-amount.json new file mode 100644 index 000000000..eaed2f374 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-fee-and-net-amount.json @@ -0,0 +1,88 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get a charge with fee and net amount", + "providerStates": [ + { + "name": "a transaction with fee and net_amount exists", + "params": { + "account_id": "123456", + "charge_id": "ch_123abc456xyz" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction/ch_123abc456xyz", + "query": { + "account_id": ["123456"], + "transaction_type": ["PAYMENT"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 100, + "fee": 5, + "net_amount": 95, + "state": { + "finished": false, + "status": "created" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "transaction_id": "ch_123abc456xyz", + "return_url": "https://somewhere.gov.uk/rainbow/1", + "payment_provider": "sandbox", + "created_date": "2018-09-07T13:12:02.121Z", + "refund_summary": { + "status": "available", + "user_external_id": null, + "amount_available": 100, + "amount_submitted": 0 + }, + "settlement_summary": { + "capture_submit_time": null, + "captured_date": null + }, + "delayed_capture": false + }, + "matchingRules": { + "body": { + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-gateway-transaction-id.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-gateway-transaction-id.json new file mode 100644 index 000000000..a9996db02 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-gateway-transaction-id.json @@ -0,0 +1,91 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get a charge request with authorisation success state", + "providerStates": [ + { + "name": "a transaction with a gateway transaction id exists", + "params": { + "account_id": "123456", + "charge_id": "ch_123abc456xyz", + "gateway_transaction_id": "gateway-tx-123456" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction/ch_123abc456xyz", + "query": { + "account_id": ["123456"], + "transaction_type": ["PAYMENT"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 100, + "state": { + "finished": false, + "status": "created" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "transaction_id": "ch_123abc456xyz", + "gateway_transaction_id": "gateway-tx-123456", + "return_url": "https://somewhere.gov.uk/rainbow/1", + "payment_provider": "sandbox", + "created_date": "2018-10-16T10:46:02.121Z", + "refund_summary": { + "status": "available", + "user_external_id": null, + "amount_available": 100, + "amount_submitted": 0 + }, + "settlement_summary": { + "capture_submit_time": null, + "captured_date": null + }, + "delayed_capture": false + }, + "matchingRules": { + "body": { + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.refund_summary.amount_available": { + "matchers": [{"match": "type"}] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-metadata.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-metadata.json new file mode 100644 index 000000000..48fc802e7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-metadata.json @@ -0,0 +1,94 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get a charge with metadata", + "providerStates": [ + { + "name": "a transaction with metadata exists", + "params": { + "account_id": "123456", + "charge_id": "ch_123abc456xyz", + "metadata": "{\"ledger_code\":123, \"some_key\":\"key\"}" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction/ch_123abc456xyz", + "query": { + "account_id": ["123456"], + "transaction_type": ["PAYMENT"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "metadata": { + "ledger_code": 123, + "some_key": "key" + }, + "amount": 100, + "state": { + "finished": false, + "status": "created" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "transaction_id": "ch_123abc456xyz", + "return_url": "https://somewhere.gov.uk/rainbow/1", + "payment_provider": "sandbox", + "created_date": "2018-10-16T10:46:02.121Z", + "refund_summary": { + "status": "available", + "user_external_id": null, + "amount_available": 100, + "amount_submitted": 0 + }, + "settlement_summary": { + "capture_submit_time": null, + "captured_date": null + }, + "delayed_capture": false + }, + "matchingRules": { + "body": { + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.refund_summary.amount_available": { + "matchers": [{"match": "type"}] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-settled-date.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-settled-date.json new file mode 100644 index 000000000..742eda451 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-payment-with-settled-date.json @@ -0,0 +1,96 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get a charge request with settlement date", + "providerStates": [ + { + "name": "a payment with payout date exists", + "params": { + "account_id": "123456", + "charge_id": "ch_123abc456settlement", + "created_date": "2020-09-19T00:00:01.000Z", + "settled_date": "2020-09-19T19:05:01.000Z" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction/ch_123abc456settlement", + "query": { + "account_id": ["123456"], + "transaction_type": ["PAYMENT"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 1000, + "state": { + "finished": true, + "status": "success" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "transaction_id": "ch_123abc456settlement", + "settlement_summary": { + "settled_date": "2020-09-19" + }, + "return_url": "https://example.org/transactions", + "payment_provider": "sandbox", + "created_date": "2020-09-19T00:00:01.000Z", + "refund_summary": { + "status": "available", + "user_external_id": null, + "amount_available": 100, + "amount_submitted": 0 + }, + "delayed_capture": false + }, + "matchingRules": { + "body": { + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.settlement_summary.settled_date": { + "matchers": [{ "date": "yyyy-MM-dd" }] + }, + "$.settlement_summary.capture_submit_time": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.settlement_summary.captured_date": { + "matchers": [{ "date": "yyyy-MM-dd" }] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-rejected-recurring-payment-with-can-retry-true.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-rejected-recurring-payment-with-can-retry-true.json new file mode 100644 index 000000000..0d40c6310 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-rejected-recurring-payment-with-can-retry-true.json @@ -0,0 +1,100 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get a recurring charge request with rejected state and can_retry equal to true", + "providerStates": [ + { + "name": "a recurring card payment with rejected state and can_retry equal to true exists", + "params": { + "account_id": "123456", + "agreement_id": "123abc456agreement", + "charge_id": "ch_123abc456xyz" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction/ch_123abc456xyz", + "query": { + "account_id": ["123456"], + "transaction_type": ["PAYMENT"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 1000, + "state": { + "finished": true, + "code": "P0010", + "message": "Payment method rejected", + "can_retry": true, + "status": "failed" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "transaction_id": "ch_123abc456xyz", + "settlement_summary": { + "capture_submit_time": null, + "captured_date": null + }, + "return_url": "https://example.org/transactions", + "payment_provider": "sandbox", + "created_date": "2020-09-19T00:00:01.000Z", + "refund_summary": { + "status": "available", + "user_external_id": null, + "amount_available": 1000, + "amount_submitted": 0 + }, + "delayed_capture": false, + "authorisation_mode": "agreement" + }, + "matchingRules": { + "body": { + "$.amount": { + "matchers": [{"match": "type"}] + }, + "$.reference": { + "matchers": [{"match": "type"}] + }, + "$.description": { + "matchers": [{"match": "type"}] + }, + "$.return_url": { + "matchers": [{"match": "type"}] + }, + "$.created_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.refund_summary.status": { + "matchers": [{"match": "type"}] + }, + "$.refund_summary.amount_available": { + "matchers": [{"match": "type"}] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-wallet-payment.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-wallet-payment.json new file mode 100644 index 000000000..074e289cb --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-get-wallet-payment.json @@ -0,0 +1,66 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "get a charge request for a wallet payment - ledger", + "providerStates": [ + { + "name": "a transaction with wallet type APPLE_PAY exists", + "params": { + "account_id": "123456", + "charge_id": "ch_123abc456xyz" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction/ch_123abc456xyz", + "query": { + "account_id": ["123456"], + "transaction_type": ["PAYMENT"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "amount": 100, + "state": { + "finished": false, + "status": "capturable" + }, + "transaction_id": "ch_123abc456xyz", + "payment_provider": "sandbox", + "card_details": { + "cardholder_name": "J Doe" + }, + "authorisation_mode": "web", + "wallet_type": "APPLE_PAY" + }, + "matchingRules": { + "body": { + "$.wallet_type": { + "matchers": [{"match": "type"}] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-agreement-not-found.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-agreement-not-found.json new file mode 100644 index 000000000..e01788d3e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-agreement-not-found.json @@ -0,0 +1,95 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search agreement not found", + "providerStates": [ + { + "name": "an agreement with payment instrument exists", + "params": { + "account_id": "3456", + "agreement_external_id": "abcdefghijklmnopqrstuvwxyz", + "reference": "a-valid-reference" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/agreement", + "query": { + "page": [ + "1" + ], + "display_size": [ + "20" + ], + "reference" : ["invalid-reference"], + "account_id": ["3456"], + "exact_reference_match": ["true"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 0, + "count": 0, + "results": [ + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/agreement?account_id=3456&reference=invalid-reference&page=1&display_size=20" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/agreement?account_id=3456&reference=invalid-reference&page=1&display_size=20" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/agreement?account_id=3456&reference=invalid-reference&page=1&display_size=20" + } + } + }, + "matchingRules": { + "body": { + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=3456&reference=invalid-reference&page=1&display_size=20" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=3456&reference=invalid-reference&page=1&display_size=20" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=3456&reference=invalid-reference&page=1&display_size=20" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-agreements-with-page-and-display-size.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-agreements-with-page-and-display-size.json new file mode 100644 index 000000000..077d42a37 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-agreements-with-page-and-display-size.json @@ -0,0 +1,162 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search agreements with page and display size params", + "providerStates": [ + { + "name": "3 agreements exist for account", + "params": { + "account_id": "777" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/agreement", + "query": { + "page": [ + "2" + ], + "display_size": [ + "1" + ], + "account_id": ["777"], + "exact_reference_match": ["true"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 2, + "total": 3, + "count": 1, + "results": [ + { + "external_id": "5f8cgoad1k3q216rul5v2m3v0c", + "service_id": "bbd0591e01dc4410839978a6ef5a8a81", + "reference": "ref", + "description": "descr", + "status": "CREATED", + "created_date": "2023-01-10T11:09:17.443Z" + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/agreement?account_id=777&display_size=1&page=2" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/agreement?account_id=777&display_size=1&page=1" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/agreement?account_id=777&display_size=1&page=3" + }, + "prev_page": { + "href": "http://ledger.service.backend/v1/agreement?account_id=777&display_size=1&page=1" + }, + "next_page": { + "href": "http://ledger.service.backend/v1/agreement?account_id=777&display_size=1&page=3" + } + } + }, + "matchingRules": { + "body": { + "$.results[*].external_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].service_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].reference": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].description": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=777&page=2&display_size=1" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=777&page=1&display_size=1" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=777&page=3&display_size=1" + } + ] + }, + "$._links.prev_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=777&page=1&display_size=1" + } + ] + }, + "$._links.next_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=777&page=3&display_size=1" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-agreements-with-reference.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-agreements-with-reference.json new file mode 100644 index 000000000..37cda067e --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-agreements-with-reference.json @@ -0,0 +1,138 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search agreements with reference", + "providerStates": [ + { + "name": "an agreement with payment instrument exists", + "params": { + "account_id": "3456", + "agreement_external_id": "abcdefghijklmnopqrstuvwxyz", + "reference": "a-valid-reference" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/agreement", + "query": { + "page": [ + "1" + ], + "display_size": [ + "20" + ], + "reference" : ["a-valid-reference"], + "account_id": ["3456"], + "exact_reference_match": ["true"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 1, + "count": 1, + "results": [ + { + "external_id": "5f8cgoad1k3q216rul5v2m3v0c", + "service_id": "bbd0591e01dc4410839978a6ef5a8a81", + "reference": "a-valid-reference", + "description": "descr", + "status": "ACTIVE", + "created_date": "2023-01-10T11:09:17.443Z" + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/agreement?account_id=3456&reference=a-valid-reference&page=1&display_size=20" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/agreement?account_id=3456&reference=a-valid-reference&page=1&display_size=20" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/agreement?account_id=3456&reference=a-valid-reference&page=1&display_size=20" + } + } + }, + "matchingRules": { + "body": { + "$.results[*].external_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].service_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].description": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=3456&reference=a-valid-reference&page=1&display_size=20" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=3456&reference=a-valid-reference&page=1&display_size=20" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=3456&reference=a-valid-reference&page=1&display_size=20" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-agreements-with-status.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-agreements-with-status.json new file mode 100644 index 000000000..5a81f0c72 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-agreements-with-status.json @@ -0,0 +1,143 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search agreements with status", + "providerStates": [ + { + "name": "3 agreements exist for account", + "params": { + "account_id": "777" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/agreement", + "query": { + "page": [ + "1" + ], + "display_size": [ + "20" + ], + "status" : ["active"], + "account_id": ["777"], + "exact_reference_match": ["true"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 1, + "count": 1, + "results": [ + { + "external_id": "5f8cgoad1k3q216rul5v2m3v0c", + "service_id": "bbd0591e01dc4410839978a6ef5a8a81", + "reference": "ref", + "description": "descr", + "status": "ACTIVE", + "created_date": "2023-01-10T11:09:17.443Z" + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/agreement?account_id=777&status=ACTIVE&page=1&display_size=20" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/agreement?account_id=777&status=ACTIVE&page=1&display_size=20" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/agreement?account_id=777&status=ACTIVE&page=1&display_size=20" + } + } + }, + "matchingRules": { + "body": { + "$.results[*].external_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].service_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].reference": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].description": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=777&status=ACTIVE&page=1&display_size=20" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=777&status=ACTIVE&page=1&display_size=20" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/agreement\\?account_id=777&status=ACTIVE&page=1&display_size=20" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes-by-dates.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes-by-dates.json new file mode 100644 index 000000000..1c07b5d34 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes-by-dates.json @@ -0,0 +1,191 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search disputes by dates", + "providerStates": [ + { + "name": "a dispute lost transaction exists", + "params": { + "transaction_external_id": "dispute97837509646393e3C", + "gateway_account_id": "123456", + "parent_external_id": "parent-abcde-12345" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "page": ["1"], + "display_size": ["500"], + "from_date": ["2022-05-20T19:04:00Z"], + "to_date": ["2022-05-20T19:06:00Z"], + "account_id": ["123456"], + "transaction_type": ["DISPUTE"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 1, + "count": 1, + "results": [ + { + "transaction_id": "dispute97837509646393e3C", + "amount": 1000, + "created_date": "2022-05-20T19:05:00.000Z", + "evidence_due_date": "2022-05-27T19:05:00.000Z", + "fee": 1500, + "net_amount": -2500, + "parent_transaction_id": "parent-abcde-12345", + "reason": "fraudulent", + "settlement_summary": { + "settled_date": "2022-05-27" + }, + "state": { + "finished": true, + "status": "lost" + }, + "transaction_type": "DISPUTE" + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&from_date=2022-05-20T19%3A04%3A00Z&to_date=2022-05-20T19%3A06%3A00Z&transaction_type=DISPUTE&page=1&display_size=500" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&from_date=2022-05-20T19%3A04%3A00Z&to_date=2022-05-20T19%3A06%3A00Z&transaction_type=DISPUTE&page=1&display_size=500" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&from_date=2022-05-20T19%3A04%3A00Z&to_date=2022-05-20T19%3A06%3A00Z&transaction_type=DISPUTE&page=1&display_size=500" + } + } + }, + "matchingRules": { + "body": { + "$.results[*].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.results[*].evidence_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.results[*].fee": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].net_amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].parent_transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].reason": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.finished": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.settlement_summary.settled_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.results[*].transaction_type": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&from_date=2022-05-20T19%3A04%3A00Z&to_date=2022-05-20T19%3A06%3A00Z&transaction_type=DISPUTE&page=1&display_size=500" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&from_date=2022-05-20T19%3A04%3A00Z&to_date=2022-05-20T19%3A06%3A00Z&transaction_type=DISPUTE&page=1&display_size=500" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&from_date=2022-05-20T19%3A04%3A00Z&to_date=2022-05-20T19%3A06%3A00Z&transaction_type=DISPUTE&page=1&display_size=500" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes-no-result.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes-no-result.json new file mode 100644 index 000000000..f6c4407e8 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes-no-result.json @@ -0,0 +1,100 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search payments", + "providerStates": [ + { + "name": "a dispute lost transaction exists", + "params": { + "transaction_external_id": "dispute97837509646393e3C", + "gateway_account_id": "123456", + "parent_external_id": "parent-abcde-12345" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "page": ["1"], + "display_size": ["500"], + "from_date": ["2021-05-20T19:04:00Z"], + "to_date": ["2021-05-20T19:06:00Z"], + "account_id": ["123456"], + "transaction_type": ["DISPUTE"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 0, + "count": 0, + "results": [ + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&from_date=2021-05-20T19%3A04%3A00Z&to_date=2021-05-20T19%3A06%3A00Z&transaction_type=DISPUTE&page=1&display_size=500" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&from_date=2021-05-20T19%3A04%3A00Z&to_date=2021-05-20T19%3A06%3A00Z&transaction_type=DISPUTE&page=1&display_size=500" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&from_date=2021-05-20T19%3A04%3A00Z&to_date=2021-05-20T19%3A06%3A00Z&transaction_type=DISPUTE&page=1&display_size=500" + } + } + }, + "matchingRules": { + "body": { + "results": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&from_date=2021-05-20T19%3A04%3A00Z&to_date=2021-05-20T19%3A06%3A00Z&transaction_type=DISPUTE&page=1&display_size=500" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&from_date=2021-05-20T19%3A04%3A00Z&to_date=2021-05-20T19%3A06%3A00Z&transaction_type=DISPUTE&page=1&display_size=500" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&from_date=2021-05-20T19%3A04%3A00Z&to_date=2021-05-20T19%3A06%3A00Z&transaction_type=DISPUTE&page=1&display_size=500" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes-page-not-found.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes-page-not-found.json new file mode 100644 index 000000000..62591afe7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes-page-not-found.json @@ -0,0 +1,45 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search disputes no result", + "providerStates": [ + { + "name": "a dispute lost transaction exists", + "params": { + "transaction_external_id": "dispute97837509646393e3C", + "gateway_account_id": "123456", + "parent_external_id": "parent-abcde-12345" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "page": ["999"], + "display_size": ["500"], + "account_id": ["123456"], + "transaction_type": ["DISPUTE"], + "status_version": ["1"] + } + }, + "response": { + "status": 404 + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes_by_settled_dates.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes_by_settled_dates.json new file mode 100644 index 000000000..460f88caf --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes_by_settled_dates.json @@ -0,0 +1,191 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "Search disputes by settled dates", + "providerStates": [ + { + "name": "a dispute lost transaction exists", + "params": { + "transaction_external_id": "dispute97837509646393e3C", + "gateway_account_id": "123456", + "parent_external_id": "parent-abcde-12345" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "page": ["1"], + "display_size": ["500"], + "from_settled_date": ["2022-05-27"], + "to_settled_date": ["2022-05-27"], + "account_id": ["123456"], + "transaction_type": ["DISPUTE"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 1, + "count": 1, + "results": [ + { + "transaction_id": "dispute97837509646393e3C", + "amount": 1000, + "created_date": "2022-05-20T19:05:00.000Z", + "evidence_due_date": "2022-05-27T19:05:00.000Z", + "fee": 1500, + "net_amount": -2500, + "parent_transaction_id": "parent-abcde-12345", + "reason": "fraudulent", + "settlement_summary": { + "settled_date": "2022-05-27" + }, + "state": { + "finished": true, + "status": "lost" + }, + "transaction_type": "DISPUTE" + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction?account_id=1234566&transaction_type=DISPUTE&from_settled_date=2022-05-27&to_settled_date=2022-05-27&page=1&display_size=500" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=1234566&transaction_type=DISPUTE&from_settled_date=2022-05-27&to_settled_date=2022-05-27&page=1&display_size=500" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=1234566&transaction_type=DISPUTE&from_settled_date=2022-05-27&to_settled_date=2022-05-27&page=1&display_size=500" + } + } + }, + "matchingRules": { + "body": { + "$.results[*].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.results[*].evidence_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.results[*].fee": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].net_amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].parent_transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].reason": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.finished": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.settlement_summary.settled_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.results[*].transaction_type": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&transaction_type=DISPUTE&from_settled_date=2022-05-27&to_settled_date=2022-05-27&page=1&display_size=500" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&transaction_type=DISPUTE&from_settled_date=2022-05-27&to_settled_date=2022-05-27&page=1&display_size=500" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&transaction_type=DISPUTE&from_settled_date=2022-05-27&to_settled_date=2022-05-27&page=1&display_size=500" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes_by_state.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes_by_state.json new file mode 100644 index 000000000..7e4fad00c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-disputes_by_state.json @@ -0,0 +1,190 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search disputes by state", + "providerStates": [ + { + "name": "a dispute lost transaction exists", + "params": { + "transaction_external_id": "dispute97837509646393e3C", + "gateway_account_id": "123456", + "parent_external_id": "parent-abcde-12345" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "page": ["1"], + "display_size": ["500"], + "state": ["lost"], + "account_id": ["123456"], + "transaction_type": ["DISPUTE"], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 1, + "count": 1, + "results": [ + { + "transaction_id": "dispute97837509646393e3C", + "amount": 1000, + "created_date": "2022-05-20T19:05:00.000Z", + "evidence_due_date": "2022-05-27T19:05:00.000Z", + "fee": 1500, + "net_amount": -2500, + "parent_transaction_id": "parent-abcde-12345", + "reason": "fraudulent", + "settlement_summary": { + "settled_date": "2022-05-27" + }, + "state": { + "finished": true, + "status": "lost" + }, + "transaction_type": "DISPUTE" + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&state=lost&transaction_type=DISPUTE&page=1&display_size=500" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&state=lost&transaction_type=DISPUTE&page=1&display_size=500" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&state=lost&transaction_type=DISPUTE&page=1&display_size=500" + } + } + }, + "matchingRules": { + "body": { + "$.results[*].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.results[*].evidence_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.results[*].fee": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].net_amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].parent_transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].reason": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.finished": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.settlement_summary.settled_date": { + "matchers": [{ "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" }] + }, + "$.results[*].transaction_type": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&state=lost&transaction_type=DISPUTE&page=1&display_size=500" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&state=lost&transaction_type=DISPUTE&page=1&display_size=500" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&state=lost&transaction_type=DISPUTE&page=1&display_size=500" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payment-by-agreement-id.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payment-by-agreement-id.json new file mode 100644 index 000000000..6c05e7efd --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payment-by-agreement-id.json @@ -0,0 +1,175 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search payment by agreement id", + "providerStates": [ + { + "name": "a recurring card payment exists for agreement", + "params": { + "account_id": "123456", + "agreement_id": "agreement-1" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "agreement_id": [ + "agreement-1" + ], + "status_version": [ + "1" + ], + "page": [ + "1" + ], + "exact_reference_match": [ + "true" + ], + "display_size": [ + "500" + ], + "transaction_type": [ + "PAYMENT" + ], + "account_id": [ + "123456" + ] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 1, + "count": 1, + "results": [ + { + "amount": 1000, + "state": { + "finished": true, + "status": "created" + }, + "description": "Test description", + "reference": "aReference", + "transaction_id": "charge97837509646393e3C", + "payment_provider": "sandbox", + "created_date": "2018-09-22T10:13:16.067Z", + "authorisation_mode": "agreement" + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&transaction_type=PAYMENT&agreement_id=agreement-1&page=1&display_size=500" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&transaction_type=PAYMENT&agreement_id=agreement-1&page=1&display_size=500" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&transaction_type=PAYMENT&agreement_id=agreement-1&page=1&display_size=500" + } + } + }, + "matchingRules": { + "body": { + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&transaction_type=PAYMENT&agreement_id=agreement-1&page=1&display_size=500" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&transaction_type=PAYMENT&agreement_id=agreement-1&page=1&display_size=500" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&transaction_type=PAYMENT&agreement_id=agreement-1&page=1&display_size=500" + } + ] + }, + "$.results[*].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].reference": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].description": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.finished": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].payment_provider": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payment-by-first-digits-card-number.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payment-by-first-digits-card-number.json new file mode 100644 index 000000000..882870725 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payment-by-first-digits-card-number.json @@ -0,0 +1,203 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search payment with first digits card number", + "providerStates": [ + { + "name": "a payment with success state exists", + "params": { + "transaction_external_id": "charge97837509646393e3C", + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "first_digits_card_number": [ + "424242" + ], + "status_version" : [ + "1" + ], + "page" : [ + "1" + ], + "exact_reference_match" : [ + "true" + ], + "display_size" : [ + "500" + ], + "transaction_type" : [ + "PAYMENT" + ], + "account_id" : [ + "123456" + ] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 1, + "count": 1, + "results": [ + { + "amount": 1000, + "state": { + "finished": true, + "status": "created" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "transaction_id": "charge97837509646393e3C", + "return_url": "https://example.org", + "email": "someone@example.org", + "payment_provider": "sandbox", + "created_date": "2018-09-22T10:13:16.067Z", + "card_details": { + "cardholder_name": "J Doe", + "billing_address": { + "line1": "line1", + "line2": "line2", + "postcode": "AB1 2CD", + "city": "London", + "country": "GB" + }, + "card_brand": "Visa" + }, + "delayed_capture": false + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&first_digits_card_number=424242&transaction_type=PAYMENT&page=1&display_size=500" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&first_digits_card_number=424242&transaction_type=PAYMENT&page=1&display_size=500" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&first_digits_card_number=424242&transaction_type=PAYMENT&page=1&display_size=500" + } + } + }, + "matchingRules": { + "body": { + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&first_digits_card_number=424242&transaction_type=PAYMENT&page=1&display_size=500" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&first_digits_card_number=424242&transaction_type=PAYMENT&page=1&display_size=500" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&first_digits_card_number=424242&transaction_type=PAYMENT&page=1&display_size=500" + } + ] + }, + "$.results[*].card_details.cardholder_name": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].reference": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].email": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].description": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].return_url": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].payment_provider": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payment-by-last-digits-card-number.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payment-by-last-digits-card-number.json new file mode 100644 index 000000000..269155a3c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payment-by-last-digits-card-number.json @@ -0,0 +1,203 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search payment with last digit card number", + "providerStates": [ + { + "name": "a payment with success state exists", + "params": { + "transaction_external_id": "charge97837509646393e3C", + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "last_digits_card_number": [ + "4242" + ], + "status_version" : [ + "1" + ], + "page" : [ + "1" + ], + "exact_reference_match" : [ + "true" + ], + "display_size" : [ + "500" + ], + "transaction_type" : [ + "PAYMENT" + ], + "account_id" : [ + "123456" + ] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 1, + "count": 1, + "results": [ + { + "amount": 1000, + "state": { + "finished": true, + "status": "created" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "transaction_id": "charge97837509646393e3C", + "return_url": "https://example.org", + "email": "someone@example.org", + "payment_provider": "sandbox", + "created_date": "2018-09-22T10:13:16.067Z", + "card_details": { + "cardholder_name": "J Doe", + "billing_address": { + "line1": "line1", + "line2": "line2", + "postcode": "AB1 2CD", + "city": "London", + "country": "GB" + }, + "card_brand": "Visa" + }, + "delayed_capture": false + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&last_digits_card_number=4242&transaction_type=PAYMENT&page=1&display_size=500" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&last_digits_card_number=4242&transaction_type=PAYMENT&page=1&display_size=500" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&last_digits_card_number=4242&transaction_type=PAYMENT&page=1&display_size=500" + } + } + }, + "matchingRules": { + "body": { + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&last_digits_card_number=4242&transaction_type=PAYMENT&page=1&display_size=500" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&last_digits_card_number=4242&transaction_type=PAYMENT&page=1&display_size=500" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&last_digits_card_number=4242&transaction_type=PAYMENT&page=1&display_size=500" + } + ] + }, + "$.results[*].card_details.cardholder_name": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].reference": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].email": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].description": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].return_url": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].payment_provider": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payment-with-charge-in-success-state.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payment-with-charge-in-success-state.json new file mode 100644 index 000000000..49124f327 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payment-with-charge-in-success-state.json @@ -0,0 +1,203 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search payment in success state", + "providerStates": [ + { + "name": "a payment with success state exists", + "params": { + "transaction_external_id": "charge97837509646393e3C", + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "state": [ + "success" + ], + "status_version" : [ + "1" + ], + "page" : [ + "1" + ], + "exact_reference_match" : [ + "true" + ], + "display_size" : [ + "500" + ], + "transaction_type" : [ + "PAYMENT" + ], + "account_id" : [ + "123456" + ] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 1, + "count": 1, + "results": [ + { + "amount": 1000, + "state": { + "finished": true, + "status": "created" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "transaction_id": "charge97837509646393e3C", + "return_url": "https://example.org", + "email": "someone@example.org", + "payment_provider": "sandbox", + "created_date": "2018-09-22T10:13:16.067Z", + "card_details": { + "cardholder_name": "J Doe", + "billing_address": { + "line1": "line1", + "line2": "line2", + "postcode": "AB1 2CD", + "city": "London", + "country": "GB" + }, + "card_brand": "Visa" + }, + "delayed_capture": false + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&state=success&transaction_type=PAYMENT&page=1&display_size=500" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&state=success&transaction_type=PAYMENT&page=1&display_size=500" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&state=success&transaction_type=PAYMENT&page=1&display_size=500" + } + } + }, + "matchingRules": { + "body": { + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&state=success&transaction_type=PAYMENT&page=1&display_size=500" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&state=success&transaction_type=PAYMENT&page=1&display_size=500" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&state=success&transaction_type=PAYMENT&page=1&display_size=500" + } + ] + }, + "$.results[*].card_details.cardholder_name": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].reference": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].email": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].description": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].return_url": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].payment_provider": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payments-page-not-found.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payments-page-not-found.json new file mode 100644 index 000000000..9aa1b7145 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payments-page-not-found.json @@ -0,0 +1,57 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "Search payments with a page number that does not exist", + "providerStates": [ + { + "name": "a payment transaction exists", + "params": { + "transaction_external_id": "charge97837509646393e3C", + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "account_id": [ + "123456" + ], + "status_version": [ + "1" + ], + "transaction_type": [ + "PAYMENT" + ], + "exact_reference_match": [ + "true" + ], + "page": [ + "999" + ], + "display_size": [ + "500" + ] + } + }, + "response": { + "status": 404 + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payments-with-settled_dates.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payments-with-settled_dates.json new file mode 100644 index 000000000..202c47f48 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payments-with-settled_dates.json @@ -0,0 +1,200 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "Return a payment, filtering by from_settled_date and to date_settled_date", + "providerStates": [ + { + "name": "three payments with payout dates exists", + "params": { + "gateway_account_id": "123456", + "from_settled_date": [ + "2020-09-19" + ], + "to_settled_date": [ + "2020-09-20" + ] + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "account_id": [ + "123456" + ], + "status_version": [ + "1" + ], + "transaction_type": [ + "PAYMENT" + ], + "from_settled_date": [ + "2020-09-19" + ], + "to_settled_date": [ + "2020-09-20" + ], + "page": [ + "1" + ], + "display_size": [ + "500" + ], + "exact_reference_match": [ + "true" + ] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 1, + "count": 1, + "results": [ + { + "amount": 1000, + "state": { + "finished": false, + "status": "submitted" + }, + "description": "Test description", + "reference": "aReference", + "transaction_id": "charge97837509646393e3C", + "email": "someone@example.org", + "created_date": "2018-09-22T10:13:16.067Z", + "delayed_capture": false, + "settlement_summary": { + "settled_date": "2020-09-19" + } + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&transaction_type=PAYMENT&from_settled_date=2020-09-19&to_settled_date=2020-09-20&page=1&display_size=500" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&transaction_type=PAYMENT&from_settled_date=2020-09-19&to_settled_date=2020-09-20&page=1&display_size=500" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&transaction_type=PAYMENT&from_settled_date=2020-09-19&to_settled_date=2020-09-20&page=1&display_size=500" + } + } + }, + "matchingRules": { + "body": { + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&transaction_type=PAYMENT&from_settled_date=2020-09-19&to_settled_date=2020-09-20&page=1&display_size=500" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&transaction_type=PAYMENT&from_settled_date=2020-09-19&to_settled_date=2020-09-20&page=1&display_size=500" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&transaction_type=PAYMENT&from_settled_date=2020-09-19&to_settled_date=2020-09-20&page=1&display_size=500" + } + ] + }, + "$.results[*].reference": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].description": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].return_url": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].email": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.results[*].settlement_summary.settled_date": { + "matchers": [ + { + "date": "yyyy-MM-dd" + } + ] + }, + "$.results[*].settlement_summary.capture_submit_time": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.results[*].settlement_summary.captured_date": { + "matchers": [ + { + "date": "yyyy-MM-dd" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payments.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payments.json new file mode 100644 index 000000000..3b9e895f3 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-payments.json @@ -0,0 +1,205 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search payments", + "providerStates": [ + { + "name": "a payment transaction exists", + "params": { + "transaction_external_id": "charge97837509646393e3C", + "gateway_account_id": "123456", + "cardholder_name": "j.doe@example.org" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "account_id": [ + "123456" + ], + "status_version": ["1"], + "cardholder_name": ["j.doe@example.org"], + "transaction_type": ["PAYMENT"], + "exact_reference_match": ["true"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 1, + "count": 1, + "results": [ + { + "amount": 1000, + "state": { + "finished": false, + "status": "created" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "transaction_id": "charge97837509646393e3C", + "return_url": "https://example.org", + "email": "someone@example.org", + "payment_provider": "sandbox", + "created_date": "2018-09-22T10:13:16.067Z", + "card_details": { + "cardholder_name": "j.doe@example.org", + "billing_address": { + "line1": "line1", + "line2": "line2", + "postcode": "AB1 2CD", + "city": "London", + "country": "GB" + }, + "card_brand": "" + }, + "delayed_capture": false, + "moto": false, + "authorisation_summary": { + "three_d_secure": { + "required": true, + "version": "2.1.0" + } + }, + "authorisation_mode": "web" + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction?account_id=1234566&cardholder_name=j.doe@example.org&transaction_type=PAYMENT&page=1&display_size=500" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&cardholder_name=j.doe@example.org&transaction_type=PAYMENT&page=1&display_size=500" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&cardholder_name=j.doe@example.org&transaction_type=PAYMENT&page=1&display_size=500" + } + } + }, + "matchingRules": { + "body": { + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&cardholder_name=j.doe%40example.org&transaction_type=PAYMENT&page=1&display_size=500" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&cardholder_name=j.doe%40example.org&transaction_type=PAYMENT&page=1&display_size=500" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&cardholder_name=j.doe%40example.org&transaction_type=PAYMENT&page=1&display_size=500" + } + ] + }, + "$.results[*].card_details.cardholder_name": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].reference": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].email": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].description": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].return_url": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].payment_provider": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + }, + "$.results[*].authorisation_mode": { + "matchers": [ + { + "match": "type" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds-display-size-two.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds-display-size-two.json new file mode 100644 index 000000000..508c6cf3c --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds-display-size-two.json @@ -0,0 +1,151 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "Return all refunds when refunds exist, filtering by from and to date, with display size of two", + "providerStates": [ + { + "name": "refund transactions exists for a gateway account", + "params": { + "gateway_account_id": "777" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "account_id": [ + "777" + ], + "status_version": ["1"], + "transaction_type": ["REFUND"], + "page": [ + "1" + ], + "display_size": [ + "2" + ] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "total": 2, + "count": 2, + "page": 1, + "results": [ + { + "transaction_id": "111111", + "created_date": "2018-09-22T10:14:16.067Z", + "state": { + "status": "success", + "finished": true + }, + "parent_transaction_id": "someExternalId1", + "amount": 150 + }, + { + "transaction_id": "222222", + "created_date": "2018-10-22T10:16:16.067Z", + "state": { + "status": "success", + "finished": true + }, + "parent_transaction_id": "someExternalId2", + "amount": 250 + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction?account_id=777&transaction_type=REFUND&page=1&display_size=2" + } + } + }, + "matchingRules": { + "body": { + "$.total": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.count": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.page": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].parent_transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$._links.self.href": { + "matchers": [ + { + "regex": "http:\/\/.*\/v1\/transaction\\?account_id=777&transaction_type=REFUND&page=1&display_size=2" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds-page-not-found.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds-page-not-found.json new file mode 100644 index 000000000..3ee66e55d --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds-page-not-found.json @@ -0,0 +1,49 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "Search refunds with a page number that does not exist", + "providerStates": [ + { + "name": "refund transactions exists for a gateway account", + "params": { + "gateway_account_id": "888" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "account_id": [ + "888" + ], + "status_version": ["1"], + "transaction_type": ["REFUND"], + "page": [ + "999" + ], + "display_size": [ + "500" + ] + } + }, + "response": { + "status": 404 + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds-with-page-and-display-when-no-refunds-exist.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds-with-page-and-display-when-no-refunds-exist.json new file mode 100644 index 000000000..50a25b854 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds-with-page-and-display-when-no-refunds-exist.json @@ -0,0 +1,83 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "Return no refunds when no refunds exist", + "providerStates": [ + { + "name": "refund transactions exists for a gateway account", + "params": { + "account_id": "888" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "account_id": [ + "888" + ], + "status_version": ["1"], + "transaction_type": ["REFUND"], + "page": [ + "1" + ], + "display_size": [ + "1" + ] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "total": 0, + "count": 0, + "page": 1, + "results": [] + }, + "matchingRules": { + "body": { + "$.total": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.count": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.page": { + "matchers": [ + { + "match": "type" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds.json new file mode 100644 index 000000000..46b46c414 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds.json @@ -0,0 +1,157 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "Return all refunds when refunds exist, filtering by from and to date", + "providerStates": [ + { + "name": "refund transactions exists for a gateway account", + "params": { + "gateway_account_id": "777" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "account_id": [ + "777" + ], + "status_version": ["1"], + "transaction_type": ["REFUND"], + "from_date": [ + "2018-09-21T13:22:55Z" + ], + "to_date": [ + "2018-10-23T13:24:55Z" + ], + "page": [ + "1" + ], + "display_size": [ + "500" + ] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "total": 2, + "count": 2, + "page": 1, + "results": [ + { + "transaction_id": "111111", + "created_date": "2018-09-22T10:14:16.067Z", + "state": { + "status": "success", + "finished": true + }, + "parent_transaction_id": "someExternalId1", + "amount": 150 + }, + { + "transaction_id": "222222", + "created_date": "2018-10-22T10:16:16.067Z", + "state": { + "status": "success", + "finished": true + }, + "parent_transaction_id": "someExternalId2", + "amount": 250 + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction?account_id=777&from_date=2018-09-21T13%3A22%3A55Z&to_date=2018-10-23T13%3A24%3A55Z&transaction_type=REFUND&page=1&display_size=500" + } + } + }, + "matchingRules": { + "body": { + "$.total": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.count": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.page": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].parent_transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$._links.self.href": { + "matchers": [ + { + "regex": "http:\/\/.*\/v1\/transaction\\?account_id=777&from_date=2018-09-21T13%3A22%3A55Z&to_date=2018-10-23T13%3A24%3A55Z&transaction_type=REFUND&page=1&display_size=500" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds_with_settled_dates.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds_with_settled_dates.json new file mode 100644 index 000000000..0e2d59482 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-refunds_with_settled_dates.json @@ -0,0 +1,153 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "Return a refund, filtering by from_settled_date and to date_settled_date", + "providerStates": [ + { + "name": "three payments with payout dates exists", + "params": { + "gateway_account_id": "123456" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "account_id": [ + "123456" + ], + "status_version": ["1"], + "transaction_type": ["REFUND"], + "from_settled_date": [ + "2020-09-19" + ], + "to_settled_date": [ + "2020-09-20" + ], + "page": [ + "1" + ], + "display_size": [ + "500" + ] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "total": 1, + "count": 1, + "page": 1, + "results": [ + { + "transaction_id": "111111", + "created_date": "2018-09-22T10:14:16.067Z", + "parent_transaction_id": "someExternalId1", + "state": { + "status": "submitted", + "finished": false + }, + "amount": 150, + "settlement_summary": { + "settled_date": "2020-09-19" + } + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456transaction_type=REFUND&&from_settled_date=2020-09-19&to_settled_date=2020-09-20&page=1&display_size=500" + } + } + }, + "matchingRules": { + "body": { + "$.total": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.count": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.page": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].parent_transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.settlement_summary.settled_date": { + "matchers": [{ "date": "yyyy-MM-dd" }] + }, + "$._links.self.href": { + "matchers": [ + { + "regex": "http:\/\/.*\/v1\/transaction\\?account_id=123456&transaction_type=REFUND&from_settled_date=2020-09-19&to_settled_date=2020-09-20&page=1&display_size=500" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-transaction.json b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-transaction.json new file mode 100644 index 000000000..25f0ac521 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/src/test/resources/pacts/publicapi-ledger-search-transaction.json @@ -0,0 +1,187 @@ +{ + "consumer": { + "name": "publicapi" + }, + "provider": { + "name": "ledger" + }, + "interactions": [ + { + "description": "search transactions", + "providerStates": [ + { + "name": "a payment transaction exists", + "params": { + "transaction_external_id": "charge97837509646393e3C", + "gateway_account_id": "123456", + "cardholder_name": "J. Smith" + } + } + ], + "request": { + "method": "GET", + "path": "/v1/transaction", + "query": { + "account_id": [ + "123456" + ], + "status_version": ["1"] + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": { + "page": 1, + "total": 1, + "count": 1, + "results": [ + { + "amount": 1000, + "state": { + "finished": false, + "status": "created" + }, + "description": "Test description", + "reference": "aReference", + "language": "en", + "transaction_id": "charge97837509646393e3C", + "return_url": "https://example.org", + "email": "someone@example.org", + "payment_provider": "sandbox", + "created_date": "2018-09-22T10:13:16.067Z", + "card_details": { + "cardholder_name": "J. Smith", + "billing_address": { + "line1": "line1", + "line2": "line2", + "postcode": "AB1 2CD", + "city": "London", + "country": "GB" + }, + "card_brand": "" + }, + "delayed_capture": false + } + ], + "_links": { + "self": { + "href": "http://ledger.service.backend/v1/transaction/account_id=123456?page=1&display_size=500" + }, + "last_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&page=1&display_size=500" + }, + "first_page": { + "href": "http://ledger.service.backend/v1/transaction?account_id=123456&page=1&display_size=500" + } + } + }, + "matchingRules": { + "body": { + "$._links.self.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&page=1&display_size=500" + } + ] + }, + "$._links.last_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&page=1&display_size=500" + } + ] + }, + "$._links.first_page.href": { + "matchers": [ + { + "regex": "http.*:\/\/.*\/v1\/transaction\\?account_id=123456&page=1&display_size=500" + } + ] + }, + "$.results[*].card_details.cardholder_name": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].transaction_id": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].amount": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].reference": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].email": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].description": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].state.status": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].return_url": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].payment_provider": { + "matchers": [ + { + "match": "type" + } + ] + }, + "$.results[*].created_date": { + "matchers": [ + { + "date": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + } + ] + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.16" + } + } +} diff --git a/jdk_11_maven/cs/rest/pay-publicapi/swagger/swagger.json b/jdk_11_maven/cs/rest/pay-publicapi/swagger/swagger.json new file mode 100644 index 000000000..9246c8ee7 --- /dev/null +++ b/jdk_11_maven/cs/rest/pay-publicapi/swagger/swagger.json @@ -0,0 +1,1713 @@ +{ + "swagger" : "2.0", + "info" : { + "description" : "GOV.UK Pay API (This version is no longer maintained. See openapi/publicapi_spec.json for latest API specification)", + "version" : "1.0.3", + "title" : "GOV.UK Pay API" + }, + "host" : "publicapi.payments.service.gov.uk", + "tags" : [ { + "name" : "Card payments" + }, { + "name" : "Refunding card payments" + } ], + "schemes" : [ "https" ], + "paths" : { + "/v1/payments" : { + "get" : { + "tags" : [ "Card payments" ], + "summary" : "Search payments", + "description" : "Search payments by reference, state, 'from' and 'to' date. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Search payments", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "reference", + "in" : "query", + "description" : "Your payment reference to search (exact match, case insensitive)", + "required" : false, + "type" : "string" + }, { + "name" : "email", + "in" : "query", + "description" : "The user email used in the payment to be searched", + "required" : false, + "type" : "string" + }, { + "name" : "state", + "in" : "query", + "description" : "State of payments to be searched. Example=success", + "required" : false, + "type" : "string", + "enum" : [ "created", "started", "submitted", "success", "failed", "cancelled", "error" ] + }, { + "name" : "card_brand", + "in" : "query", + "description" : "Card brand used for payment. Example=master-card", + "required" : false, + "type" : "string" + }, { + "name" : "from_date", + "in" : "query", + "description" : "From date of payments to be searched (this date is inclusive). Example=2015-08-13T12:35:00Z", + "required" : false, + "type" : "string" + }, { + "name" : "to_date", + "in" : "query", + "description" : "To date of payments to be searched (this date is exclusive). Example=2015-08-14T12:35:00Z", + "required" : false, + "type" : "string" + }, { + "name" : "page", + "in" : "query", + "description" : "Page number requested for the search, should be a positive integer (optional, defaults to 1)", + "required" : false, + "type" : "string" + }, { + "name" : "display_size", + "in" : "query", + "description" : "Number of results to be shown per page, should be a positive integer (optional, defaults to 500, max 500)", + "required" : false, + "type" : "string" + }, { + "name" : "cardholder_name", + "in" : "query", + "description" : "Name on card used to make payment", + "required" : false, + "type" : "string" + }, { + "name" : "first_digits_card_number", + "in" : "query", + "description" : "First six digits of the card used to make payment", + "required" : false, + "type" : "string" + }, { + "name" : "last_digits_card_number", + "in" : "query", + "description" : "Last four digits of the card used to make payment", + "required" : false, + "type" : "string" + }, { + "name" : "from_settled_date", + "in" : "query", + "description" : "From settled date of payment to be searched (this date is inclusive). Example=2015-08-13", + "required" : false, + "type" : "string" + }, { + "name" : "to_settled_date", + "in" : "query", + "description" : "To settled date of payment to be searched (this date is inclusive). Example=2015-08-14", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "OK", + "schema" : { + "$ref" : "#/definitions/PaymentSearchResults" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "422" : { + "description" : "Invalid parameters: from_date, to_date, status, display_size. See Public API documentation for the correct data formats", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + }, + "post" : { + "tags" : [ "Card payments" ], + "summary" : "Create new payment", + "description" : "Create a new payment for the account associated to the Authorisation token. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Create a payment", + "consumes" : [ "application/json" ], + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "body", + "description" : "requestPayload", + "required" : true, + "schema" : { + "$ref" : "#/definitions/CreateCardPaymentRequest" + } + } ], + "responses" : { + "201" : { + "description" : "Created", + "schema" : { + "$ref" : "#/definitions/CreatePaymentResult" + } + }, + "400" : { + "description" : "Bad request", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "422" : { + "description" : "Invalid attribute value: description. Must be less than or equal to 255 characters length", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/payments/{paymentId}" : { + "get" : { + "tags" : [ "Card payments" ], + "summary" : "Find payment by ID", + "description" : "Return information about the payment The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Get a payment", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "description" : "Payment identifier", + "required" : true, + "type" : "string", + "x-example" : "hu20sqlact5260q2nanm0q8u93" + } ], + "responses" : { + "200" : { + "description" : "OK", + "schema" : { + "$ref" : "#/definitions/GetPaymentResult" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/payments/{paymentId}/cancel" : { + "post" : { + "tags" : [ "Card payments" ], + "summary" : "Cancel payment", + "description" : "Cancel a payment based on the provided payment ID and the Authorisation token. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'. A payment can only be cancelled if it's in a state that isn't finished.", + "operationId" : "Cancel a payment", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "description" : "Payment identifier", + "required" : true, + "type" : "string", + "x-example" : "hu20sqlact5260q2nanm0q8u93" + } ], + "responses" : { + "204" : { + "description" : "No Content" + }, + "400" : { + "description" : "Cancellation of payment failed", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "409" : { + "description" : "Conflict", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/payments/{paymentId}/capture" : { + "post" : { + "tags" : [ "Card payments" ], + "summary" : "Capture payment", + "description" : "Capture a payment based on the provided payment ID and the Authorisation token. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'. A payment can only be captured if it's in 'submitted' state", + "operationId" : "Capture a payment", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "description" : "Payment identifier", + "required" : true, + "type" : "string", + "x-example" : "hu20sqlact5260q2nanm0q8u93" + } ], + "responses" : { + "204" : { + "description" : "No Content" + }, + "400" : { + "description" : "Capture of payment failed", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "409" : { + "description" : "Conflict", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/payments/{paymentId}/events" : { + "get" : { + "tags" : [ "Card payments" ], + "summary" : "Return payment events by ID", + "description" : "Return payment events information about a certain payment The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Get events for a payment", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "description" : "Payment identifier", + "required" : true, + "type" : "string", + "x-example" : "hu20sqlact5260q2nanm0q8u93" + } ], + "responses" : { + "200" : { + "description" : "OK", + "schema" : { + "$ref" : "#/definitions/PaymentEvents" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/payments/{paymentId}/refunds" : { + "get" : { + "tags" : [ "Refunding card payments" ], + "summary" : "Get all refunds for a payment", + "description" : "Return refunds for a payment. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Get all refunds for a payment", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "OK", + "schema" : { + "$ref" : "#/definitions/RefundForSearchResult" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + }, + "post" : { + "tags" : [ "Refunding card payments" ], + "summary" : "Submit a refund for a payment", + "description" : "Return issued refund information. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Submit a refund for a payment", + "consumes" : [ "application/json" ], + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "description" : "paymentId", + "required" : true, + "type" : "string" + }, { + "in" : "body", + "name" : "body", + "description" : "requestPayload", + "required" : true, + "schema" : { + "$ref" : "#/definitions/PaymentRefundRequest" + } + } ], + "responses" : { + "200" : { + "description" : "successful operation", + "schema" : { + "$ref" : "#/definitions/Refund" + } + }, + "202" : { + "description" : "ACCEPTED" + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "412" : { + "description" : "Refund amount available mismatch" + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/payments/{paymentId}/refunds/{refundId}" : { + "get" : { + "tags" : [ "Refunding card payments" ], + "summary" : "Find payment refund by ID", + "description" : "Return payment refund information by Refund ID The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Get a payment refund", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "paymentId", + "in" : "path", + "required" : true, + "type" : "string" + }, { + "name" : "refundId", + "in" : "path", + "required" : true, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "OK", + "schema" : { + "$ref" : "#/definitions/Refund" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "404" : { + "description" : "Not found", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + }, + "429" : { + "description" : "Too many requests", + "schema" : { + "$ref" : "#/definitions/ErrorResponse" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/PaymentError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + }, + "/v1/refunds" : { + "get" : { + "tags" : [ "Refunding card payments" ], + "summary" : "Search refunds", + "description" : "Search refunds by 'from' and 'to' date. The Authorisation token needs to be specified in the 'authorization' header as 'authorization: Bearer YOUR_API_KEY_HERE'", + "operationId" : "Search refunds", + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "from_date", + "in" : "query", + "description" : "From date of refunds to be searched (this date is inclusive). Example=2015-08-13T12:35:00Z", + "required" : false, + "type" : "string" + }, { + "name" : "to_date", + "in" : "query", + "description" : "To date of refunds to be searched (this date is exclusive). Example=2015-08-14T12:35:00Z", + "required" : false, + "type" : "string" + }, { + "name" : "from_settled_date", + "in" : "query", + "description" : "From settled date of refund to be searched (this date is inclusive). Example=2015-08-13", + "required" : false, + "type" : "string" + }, { + "name" : "to_settled_date", + "in" : "query", + "description" : "To settled date of refund to be searched (this date is inclusive). Example=2015-08-13", + "required" : false, + "type" : "string" + }, { + "name" : "page", + "in" : "query", + "description" : "Page number requested for the search, should be a positive integer (optional, defaults to 1)", + "required" : false, + "type" : "string" + }, { + "name" : "display_size", + "in" : "query", + "description" : "Number of results to be shown per page, should be a positive integer (optional, defaults to 500, max 500)", + "required" : false, + "type" : "string" + } ], + "responses" : { + "200" : { + "description" : "OK", + "schema" : { + "$ref" : "#/definitions/RefundSearchResults" + } + }, + "401" : { + "description" : "Credentials are required to access this resource" + }, + "422" : { + "description" : "Invalid parameters. See Public API documentation for the correct data formats", + "schema" : { + "$ref" : "#/definitions/RefundError" + } + }, + "500" : { + "description" : "Downstream system error", + "schema" : { + "$ref" : "#/definitions/RefundError" + } + } + }, + "security" : [ { + "Authorization" : [ ] + } ] + } + } + }, + "securityDefinitions" : { + "Authorization" : { + "type" : "apiKey", + "name" : "Authorization", + "in" : "header" + } + }, + "definitions" : { + "Address" : { + "type" : "object", + "properties" : { + "line1" : { + "type" : "string", + "example" : "address line 1", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 255 + }, + "line2" : { + "type" : "string", + "example" : "address line 2", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 255 + }, + "postcode" : { + "type" : "string", + "example" : "AB1 2CD", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 25 + }, + "city" : { + "type" : "string", + "example" : "address city", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 255 + }, + "country" : { + "type" : "string", + "example" : "GB", + "readOnly" : true + } + }, + "description" : "A structure representing the billing address of a card" + }, + "CardDetails" : { + "type" : "object", + "properties" : { + "last_digits_card_number" : { + "type" : "string", + "example" : "1234", + "readOnly" : true + }, + "first_digits_card_number" : { + "type" : "string", + "example" : "123456", + "readOnly" : true + }, + "cardholder_name" : { + "type" : "string", + "example" : "Mr. Card holder", + "readOnly" : true + }, + "expiry_date" : { + "type" : "string", + "example" : "04/24", + "description" : "The expiry date of the card in MM/yy format", + "readOnly" : true + }, + "billing_address" : { + "readOnly" : true, + "$ref" : "#/definitions/Address" + }, + "card_brand" : { + "type" : "string", + "example" : "Visa", + "readOnly" : true + }, + "card_type" : { + "type" : "string", + "example" : "debit", + "description" : "The card type, `debit` or `credit` or `null` if not able to determine", + "readOnly" : true, + "enum" : [ "debit", "credit", "null" ] + } + }, + "description" : "A structure representing the payment card" + }, + "CreateCardPaymentRequest" : { + "type" : "object", + "required" : [ "amount", "description", "reference", "return_url" ], + "properties" : { + "amount" : { + "type" : "integer", + "format" : "int32", + "example" : 12000, + "description" : "amount in pence", + "readOnly" : true, + "minimum" : 0, + "maximum" : 10000000 + }, + "reference" : { + "type" : "string", + "example" : "12345", + "description" : "payment reference", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 255 + }, + "description" : { + "type" : "string", + "example" : "New passport application", + "description" : "payment description", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 255 + }, + "language" : { + "type" : "string", + "example" : "en", + "description" : "ISO-639-1 Alpha-2 code of a supported language to use on the payment pages", + "readOnly" : true, + "enum" : [ "en", "cy" ] + }, + "email" : { + "type" : "string", + "example" : "Joe.Bogs@example.org", + "description" : "email", + "readOnly" : true + }, + "return_url" : { + "type" : "string", + "example" : "https://service-name.gov.uk/transactions/12345", + "description" : "service return url", + "readOnly" : true, + "minLength" : 0, + "maxLength" : 2000 + }, + "delayed_capture" : { + "type" : "boolean", + "example" : false, + "description" : "delayed capture flag", + "readOnly" : true + }, + "moto" : { + "type" : "boolean", + "example" : false, + "description" : "Mail Order / Telephone Order (MOTO) payment flag", + "readOnly" : true + }, + "metadata" : { + "type" : "object", + "example" : "{\"ledger_code\":\"123\", \"reconciled\": true}", + "description" : "Additional metadata - up to 10 name/value pairs - on the payment. Each key must be between 1 and 30 characters long. The value, if a string, must be no greater than 50 characters long. Other permissible value types: boolean, number.", + "readOnly" : true, + "additionalProperties" : { + "type" : "object" + } + }, + "prefilled_cardholder_details" : { + "description" : "prefilled_cardholder_details", + "readOnly" : true, + "$ref" : "#/definitions/PrefilledCardholderDetails" + } + }, + "description" : "The Payment Request Payload" + }, + "CreatePaymentResult" : { + "type" : "object", + "properties" : { + "amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1200, + "description" : "The amount in pence." + }, + "state" : { + "$ref" : "#/definitions/PaymentState" + }, + "description" : { + "type" : "string", + "example" : "New passport application", + "description" : "The human-readable description you gave the payment." + }, + "reference" : { + "type" : "string", + "example" : "12345", + "description" : "The reference number you associated with this payment." + }, + "language" : { + "type" : "string", + "example" : "en", + "description" : "Which language your users will see on the payment pages when they make a payment.", + "enum" : [ "en", "cy" ] + }, + "payment_id" : { + "type" : "string", + "example" : "hu20sqlact5260q2nanm0q8u93", + "description" : "The unique identifier of the payment." + }, + "payment_provider" : { + "type" : "string", + "example" : "worldpay" + }, + "return_url" : { + "type" : "string", + "example" : "https://service-name.gov.uk/transactions/12345", + "description" : "An HTTPS URL on your site that your user will be sent back to once they have completed their payment attempt on GOV.UK Pay." + }, + "created_date" : { + "type" : "string", + "example" : "2016-01-21T17:15:00Z", + "description" : "The date you created the payment." + }, + "delayed_capture" : { + "type" : "boolean", + "example" : false, + "description" : "Whether to [delay capturing](https://docs.payments.service.gov.uk/optional_features/delayed_capture/) this payment." + }, + "moto" : { + "type" : "boolean", + "example" : false, + "description" : "Mail Order / Telephone Order (MOTO) payment flag." + }, + "_links" : { + "description" : "API endpoints related to the payment.", + "$ref" : "#/definitions/PaymentLinks" + }, + "provider_id" : { + "type" : "string", + "example" : "null", + "description" : "The reference number the payment gateway associated with the payment." + }, + "metadata" : { + "type" : "object", + "description" : "[Custom metadata](https://docs.payments.service.gov.uk/optional_features/custom_metadata/) you added to the payment.", + "additionalProperties" : { + "type" : "string" + } + }, + "email" : { + "type" : "string", + "example" : "citizen@example.org", + "description" : "The email address of your user." + }, + "refund_summary" : { + "$ref" : "#/definitions/RefundSummary" + }, + "settlement_summary" : { + "$ref" : "#/definitions/PaymentSettlementSummary" + }, + "card_details" : { + "$ref" : "#/definitions/CardDetails" + } + } + }, + "EmbeddedRefunds" : { + "type" : "object", + "properties" : { + "refunds" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/Refund" + } + } + } + }, + "ErrorResponse" : { + "type" : "object", + "properties" : { + "code" : { + "type" : "string", + "example" : "P0900" + }, + "description" : { + "type" : "string", + "example" : "Too many requests" + } + }, + "description" : "An error response" + }, + "GetPaymentResult" : { + "type" : "object", + "properties" : { + "amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1200 + }, + "description" : { + "type" : "string", + "example" : "Your Service Description" + }, + "reference" : { + "type" : "string", + "example" : "your-reference" + }, + "language" : { + "type" : "string", + "example" : "en", + "enum" : [ "en", "cy" ] + }, + "metadata" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "email" : { + "type" : "string", + "example" : "your email" + }, + "state" : { + "$ref" : "#/definitions/PaymentState" + }, + "payment_id" : { + "type" : "string", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "payment_provider" : { + "type" : "string", + "example" : "worldpay", + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "example" : "2016-01-21T17:15:000Z", + "readOnly" : true + }, + "refund_summary" : { + "readOnly" : true, + "$ref" : "#/definitions/RefundSummary" + }, + "settlement_summary" : { + "readOnly" : true, + "$ref" : "#/definitions/PaymentSettlementSummary" + }, + "card_details" : { + "readOnly" : true, + "$ref" : "#/definitions/CardDetails" + }, + "delayed_capture" : { + "type" : "boolean", + "example" : false, + "description" : "delayed capture flag", + "readOnly" : true + }, + "moto" : { + "type" : "boolean", + "example" : false, + "description" : "Mail Order / Telephone Order (MOTO) payment flag", + "readOnly" : true + }, + "corporate_card_surcharge" : { + "type" : "integer", + "format" : "int64", + "example" : 250, + "readOnly" : true + }, + "total_amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1450, + "readOnly" : true + }, + "fee" : { + "type" : "integer", + "format" : "int64", + "example" : 5, + "description" : "processing fee taken by the GOV.UK Pay platform, in pence. Only available depending on payment service provider", + "readOnly" : true + }, + "net_amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1195, + "description" : "amount including all surcharges and less all fees, in pence. Only available depending on payment service provider", + "readOnly" : true + }, + "provider_id" : { + "type" : "string", + "example" : "reference-from-payment-gateway", + "readOnly" : true + }, + "return_url" : { + "type" : "string", + "example" : "http://your.service.domain/your-reference", + "readOnly" : true + }, + "_links" : { + "$ref" : "#/definitions/PaymentLinks" + }, + "card_brand" : { + "type" : "string", + "example" : "Visa", + "description" : "Card Brand", + "readOnly" : true + } + } + }, + "Link" : { + "type" : "object", + "properties" : { + "href" : { + "type" : "string", + "example" : "https://an.example.link/from/payment/platform", + "readOnly" : true + }, + "method" : { + "type" : "string", + "example" : "GET", + "readOnly" : true + } + }, + "description" : "A link related to a payment" + }, + "Payer" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "readOnly" : true + }, + "email" : { + "type" : "string", + "readOnly" : true + } + } + }, + "PaymentDetailForSearch" : { + "type" : "object", + "properties" : { + "amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1200 + }, + "description" : { + "type" : "string", + "example" : "Your Service Description" + }, + "reference" : { + "type" : "string", + "example" : "your-reference" + }, + "language" : { + "type" : "string", + "example" : "en", + "enum" : [ "en", "cy" ] + }, + "metadata" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "email" : { + "type" : "string", + "example" : "your email" + }, + "state" : { + "$ref" : "#/definitions/PaymentState" + }, + "payment_id" : { + "type" : "string", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "payment_provider" : { + "type" : "string", + "example" : "worldpay", + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "example" : "2016-01-21T17:15:000Z", + "readOnly" : true + }, + "refund_summary" : { + "readOnly" : true, + "$ref" : "#/definitions/RefundSummary" + }, + "settlement_summary" : { + "readOnly" : true, + "$ref" : "#/definitions/PaymentSettlementSummary" + }, + "card_details" : { + "readOnly" : true, + "$ref" : "#/definitions/CardDetails" + }, + "delayed_capture" : { + "type" : "boolean", + "example" : false, + "description" : "delayed capture flag", + "readOnly" : true + }, + "moto" : { + "type" : "boolean", + "example" : false, + "description" : "Mail Order / Telephone Order (MOTO) payment flag", + "readOnly" : true + }, + "corporate_card_surcharge" : { + "type" : "integer", + "format" : "int64", + "example" : 250, + "readOnly" : true + }, + "total_amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1450, + "readOnly" : true + }, + "fee" : { + "type" : "integer", + "format" : "int64", + "example" : 5, + "description" : "processing fee taken by the GOV.UK Pay platform, in pence. Only available depending on payment service provider", + "readOnly" : true + }, + "net_amount" : { + "type" : "integer", + "format" : "int64", + "example" : 1195, + "description" : "amount including all surcharges and less all fees, in pence. Only available depending on payment service provider", + "readOnly" : true + }, + "provider_id" : { + "type" : "string", + "example" : "reference-from-payment-gateway", + "readOnly" : true + }, + "return_url" : { + "type" : "string", + "example" : "http://your.service.domain/your-reference", + "readOnly" : true + }, + "_links" : { + "readOnly" : true, + "$ref" : "#/definitions/PaymentLinksForSearch" + }, + "card_brand" : { + "type" : "string", + "example" : "Visa", + "description" : "Card Brand", + "readOnly" : true + } + } + }, + "PaymentError" : { + "type" : "object", + "properties" : { + "field" : { + "type" : "string", + "example" : "amount" + }, + "code" : { + "type" : "string", + "example" : "P0102" + }, + "description" : { + "type" : "string", + "example" : "Invalid attribute value: amount. Must be less than or equal to 10000000" + } + }, + "description" : "A Payment Error response" + }, + "PaymentEvent" : { + "type" : "object", + "properties" : { + "payment_id" : { + "type" : "string", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "state" : { + "description" : "state", + "readOnly" : true, + "$ref" : "#/definitions/PaymentState" + }, + "updated" : { + "type" : "string", + "example" : "2017-01-10T16:44:48.646Z", + "description" : "updated", + "readOnly" : true + }, + "_links" : { + "readOnly" : true, + "$ref" : "#/definitions/PaymentEventLink" + } + }, + "description" : "A List of Payment Events information" + }, + "PaymentEventLink" : { + "type" : "object", + "properties" : { + "payment_url" : { + "description" : "payment_url", + "readOnly" : true, + "$ref" : "#/definitions/Link" + } + }, + "description" : "Resource link for a payment of a payment event" + }, + "PaymentEvents" : { + "type" : "object", + "properties" : { + "events" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/PaymentEvent" + } + }, + "payment_id" : { + "type" : "string", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "_links" : { + "readOnly" : true, + "$ref" : "#/definitions/PaymentLinksForEvents" + } + }, + "description" : "A List of Payment Events information" + }, + "PaymentLinks" : { + "type" : "object", + "properties" : { + "self" : { + "description" : "self", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "next_url" : { + "description" : "next_url", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "next_url_post" : { + "description" : "next_url_post", + "readOnly" : true, + "$ref" : "#/definitions/PostLink" + }, + "events" : { + "description" : "events", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "refunds" : { + "description" : "refunds", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "cancel" : { + "description" : "cancel", + "readOnly" : true, + "$ref" : "#/definitions/PostLink" + }, + "capture" : { + "description" : "capture", + "readOnly" : true, + "$ref" : "#/definitions/PostLink" + } + }, + "description" : "links for payment" + }, + "PaymentLinksForEvents" : { + "type" : "object", + "properties" : { + "self" : { + "description" : "self", + "readOnly" : true, + "$ref" : "#/definitions/Link" + } + }, + "description" : "links for events resource" + }, + "PaymentLinksForSearch" : { + "type" : "object", + "properties" : { + "self" : { + "description" : "self", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "cancel" : { + "description" : "cancel", + "readOnly" : true, + "$ref" : "#/definitions/PostLink" + }, + "events" : { + "description" : "events", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "refunds" : { + "description" : "refunds", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "capture" : { + "description" : "capture", + "readOnly" : true, + "$ref" : "#/definitions/PostLink" + } + }, + "description" : "links for search payment resource" + }, + "PaymentRefundRequest" : { + "type" : "object", + "required" : [ "amount" ], + "properties" : { + "amount" : { + "type" : "integer", + "format" : "int32", + "example" : 150000, + "description" : "Amount in pence. Can't be more than the available amount for refunds", + "minimum" : 1, + "maximum" : 10000000 + }, + "refund_amount_available" : { + "type" : "integer", + "format" : "int32", + "example" : 200000, + "description" : "Amount in pence. Total amount still available before issuing the refund", + "readOnly" : true, + "minimum" : 1, + "maximum" : 10000000 + } + }, + "description" : "The Payment Refund Request Payload" + }, + "PaymentSearchResults" : { + "type" : "object", + "properties" : { + "total" : { + "type" : "integer", + "format" : "int32", + "example" : 100 + }, + "count" : { + "type" : "integer", + "format" : "int32", + "example" : 20 + }, + "page" : { + "type" : "integer", + "format" : "int32", + "example" : 1 + }, + "_links" : { + "$ref" : "#/definitions/SearchNavigationLinks" + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/PaymentDetailForSearch" + } + } + } + }, + "PaymentSettlementSummary" : { + "type" : "object", + "properties" : { + "capture_submit_time" : { + "type" : "string", + "example" : "2016-01-21T17:15:000Z", + "description" : "Date and time capture request has been submitted. May be null if capture request was not immediately acknowledged by payment gateway.", + "readOnly" : true + }, + "captured_date" : { + "type" : "string", + "example" : "2016-01-21", + "description" : "Date of the capture event.", + "readOnly" : true + }, + "settled_date" : { + "type" : "string", + "example" : "2016-01-21", + "description" : "The date that the transaction was paid into the service's account.", + "readOnly" : true + } + }, + "description" : "A structure representing information about a settlement" + }, + "PaymentState" : { + "type" : "object", + "properties" : { + "status" : { + "type" : "string", + "example" : "created", + "description" : "Current progress of the payment in its lifecycle", + "readOnly" : true + }, + "finished" : { + "type" : "boolean", + "description" : "Whether the payment has finished", + "readOnly" : true + }, + "message" : { + "type" : "string", + "example" : "User cancelled the payment", + "description" : "What went wrong with the Payment if it finished with an error - English message", + "readOnly" : true + }, + "code" : { + "type" : "string", + "example" : "P010", + "description" : "What went wrong with the Payment if it finished with an error - error code", + "readOnly" : true + } + }, + "description" : "A structure representing the current state of the payment in its lifecycle." + }, + "PostLink" : { + "type" : "object", + "properties" : { + "type" : { + "type" : "string", + "example" : "application/x-www-form-urlencoded" + }, + "params" : { + "type" : "object", + "example" : "\"description\":\"This is a value for a parameter called description\"", + "additionalProperties" : { + "type" : "object" + } + }, + "href" : { + "type" : "string", + "example" : "https://an.example.link/from/payment/platform", + "readOnly" : true + }, + "method" : { + "type" : "string", + "example" : "POST", + "readOnly" : true + } + }, + "description" : "A POST link related to a payment" + }, + "PrefilledCardholderDetails" : { + "type" : "object", + "properties" : { + "cardholder_name" : { + "type" : "string", + "example" : "J. Bogs", + "description" : "prefilled cardholder name", + "minLength" : 0, + "maxLength" : 255 + }, + "billing_address" : { + "description" : "prefilled billing address", + "readOnly" : true, + "$ref" : "#/definitions/Address" + } + } + }, + "Refund" : { + "type" : "object", + "properties" : { + "refund_id" : { + "type" : "string", + "example" : "act4c33g40j3edfmi8jknab84x", + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "example" : "2017-01-10T16:52:07.855Z", + "readOnly" : true + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "example" : 120, + "readOnly" : true + }, + "_links" : { + "$ref" : "#/definitions/RefundLinksForSearch" + }, + "status" : { + "type" : "string", + "example" : "success", + "readOnly" : true, + "enum" : [ "submitted", "success", "error" ] + }, + "settlement_summary" : { + "readOnly" : true, + "$ref" : "#/definitions/RefundSettlementSummary" + } + } + }, + "RefundDetailForSearch" : { + "type" : "object", + "properties" : { + "refund_id" : { + "type" : "string", + "example" : "act4c33g40j3edfmi8jknab84x", + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "example" : "2017-01-10T16:52:07.855Z", + "readOnly" : true + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "example" : 120, + "readOnly" : true + }, + "_links" : { + "readOnly" : true, + "$ref" : "#/definitions/RefundLinksForSearch" + }, + "status" : { + "type" : "string", + "example" : "success", + "readOnly" : true, + "enum" : [ "submitted", "success", "error" ] + }, + "settlement_summary" : { + "readOnly" : true, + "$ref" : "#/definitions/RefundSettlementSummary" + } + } + }, + "RefundError" : { + "type" : "object", + "properties" : { + "field" : { + "type" : "string", + "example" : "amount_submitted" + }, + "code" : { + "type" : "string", + "example" : "P0102" + }, + "description" : { + "type" : "string", + "example" : "Invalid attribute value: amountSubmitted. Must be less than or equal to 10000000" + } + }, + "description" : "A Refund Error response" + }, + "RefundForSearchResult" : { + "type" : "object", + "properties" : { + "payment_id" : { + "type" : "string", + "example" : "hu20sqlact5260q2nanm0q8u93" + }, + "_links" : { + "$ref" : "#/definitions/RefundLinksForSearch" + }, + "_embedded" : { + "readOnly" : true, + "$ref" : "#/definitions/EmbeddedRefunds" + } + } + }, + "RefundLinksForSearch" : { + "type" : "object", + "properties" : { + "self" : { + "description" : "self", + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "payment" : { + "description" : "payment", + "readOnly" : true, + "$ref" : "#/definitions/Link" + } + }, + "description" : "links for search refunds resource" + }, + "RefundSearchResults" : { + "type" : "object", + "properties" : { + "total" : { + "type" : "integer", + "format" : "int32", + "example" : 100 + }, + "count" : { + "type" : "integer", + "format" : "int32", + "example" : 20 + }, + "page" : { + "type" : "integer", + "format" : "int32", + "example" : 1 + }, + "results" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/RefundDetailForSearch" + } + }, + "_links" : { + "readOnly" : true, + "$ref" : "#/definitions/SearchNavigationLinks" + } + } + }, + "RefundSettlementSummary" : { + "type" : "object", + "properties" : { + "settled_date" : { + "type" : "string", + "example" : "2016-01-21", + "description" : "The date that the transaction was refunded from the service's account.", + "readOnly" : true + } + }, + "description" : "A structure representing information about a settlement for refunds" + }, + "RefundSummary" : { + "type" : "object", + "properties" : { + "status" : { + "type" : "string", + "example" : "available", + "description" : "Availability status of the refund" + }, + "amount_available" : { + "type" : "integer", + "format" : "int64", + "example" : 100, + "description" : "Amount available for refund in pence", + "readOnly" : true + }, + "amount_submitted" : { + "type" : "integer", + "format" : "int64", + "description" : "Amount submitted for refunds on this Payment in pence", + "readOnly" : true + } + }, + "description" : "A structure representing the refunds availability" + }, + "SearchNavigationLinks" : { + "type" : "object", + "properties" : { + "self" : { + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "first_page" : { + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "last_page" : { + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "prev_page" : { + "readOnly" : true, + "$ref" : "#/definitions/Link" + }, + "next_page" : { + "readOnly" : true, + "$ref" : "#/definitions/Link" + } + }, + "description" : "Links to navigate through pages" + } + } +} diff --git a/jdk_11_maven/cs/rest/pom.xml b/jdk_11_maven/cs/rest/pom.xml index 2281c93c0..4c6982e07 100644 --- a/jdk_11_maven/cs/rest/pom.xml +++ b/jdk_11_maven/cs/rest/pom.xml @@ -14,6 +14,7 @@ cwa-verification-server + pay-publicapi diff --git a/jdk_11_maven/em/embedded/rest/pay-publicapi/pom.xml b/jdk_11_maven/em/embedded/rest/pay-publicapi/pom.xml new file mode 100644 index 000000000..8411817de --- /dev/null +++ b/jdk_11_maven/em/embedded/rest/pay-publicapi/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + org.evomaster + evomaster-benchmark-jdk11-em-embedded-rest + 2.0.1-SNAPSHOT + + + evomaster-benchmark-jdk11-em-embedded-rest-pay-publicapi + + + 11 + 11 + UTF-8 + + + + + uk.gov.pay + pay-publicapi + 0.1-SNAPSHOT + compile + + + + org.testcontainers + testcontainers + compile + + + + redis.clients + jedis + compile + 5.1.0 + + + + diff --git a/jdk_11_maven/em/embedded/rest/pay-publicapi/src/main/java/em/embedded/uk/gov/pay/api/app/EmbeddedEvoMasterController.java b/jdk_11_maven/em/embedded/rest/pay-publicapi/src/main/java/em/embedded/uk/gov/pay/api/app/EmbeddedEvoMasterController.java new file mode 100644 index 000000000..c0f0fca9d --- /dev/null +++ b/jdk_11_maven/em/embedded/rest/pay-publicapi/src/main/java/em/embedded/uk/gov/pay/api/app/EmbeddedEvoMasterController.java @@ -0,0 +1,153 @@ +package em.embedded.uk.gov.pay.api.app; + +import org.evomaster.client.java.controller.AuthUtils; +import org.evomaster.client.java.controller.EmbeddedSutController; +import org.evomaster.client.java.controller.InstrumentedSutStarter; +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto; +import org.evomaster.client.java.controller.api.dto.SutInfoDto; +import org.evomaster.client.java.controller.problem.ProblemInfo; +import org.evomaster.client.java.controller.problem.RestProblem; +import org.evomaster.client.java.sql.DbSpecification; +import org.testcontainers.containers.GenericContainer; +import uk.gov.pay.api.app.PublicApi; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +import java.util.Arrays; +import java.util.List; + +public class EmbeddedEvoMasterController extends EmbeddedSutController { + + private static final int REDIS_PORT = 6379; + + private static final String REDIS_VERSION = "7.2.3"; + + private static final GenericContainer redisContainer = new GenericContainer("redis:" + REDIS_VERSION) + .withExposedPorts(REDIS_PORT); + + private static String REDIS_URL = ""; + + private static JedisPool jedisPool; + + public static void main(String[] args) { + int port = 40100; + if (args.length > 0) { + port = Integer.parseInt(args[0]); + } + + EmbeddedEvoMasterController controller = new EmbeddedEvoMasterController(port); + InstrumentedSutStarter starter = new InstrumentedSutStarter(controller); + + starter.start(); + } + + private PublicApi application; + + public EmbeddedEvoMasterController() { + this(40100); + } + + public EmbeddedEvoMasterController(int port) { + setControllerPort(port); + } + + @Override + public boolean isSutRunning() { + if (application == null) { + return false; + } + + return application.getJettyServer().isRunning(); + } + + @Override + public String getPackagePrefixesToCover() { + return "uk.gov.pay.api."; + } + + @Override + public List getInfoForAuthentication() { + AuthenticationDto dto = AuthUtils.getForAuthorizationHeader("foo", "Bearer asdfghdasdjlguuolnga94upq3nrd2642sq7uel0oo"); + dto.requireMockHandling = true; + return Arrays.asList(dto); + } + + @Override + public ProblemInfo getProblemInfo() { + return new RestProblem( + "http://localhost:" + application.getJettyPort() + "/assets/swagger.json", + null + ); + } + + @Override + public SutInfoDto.OutputFormat getPreferredOutputFormat() { + return SutInfoDto.OutputFormat.JAVA_JUNIT_5; + } + + @Override + public String startSut() { + redisContainer.start(); + + REDIS_URL = redisContainer.getHost() + redisContainer.getMappedPort(REDIS_PORT); + + jedisPool = new JedisPool(redisContainer.getHost(), redisContainer.getMappedPort(REDIS_PORT)); + + application = new PublicApi(); + + //Dirty hack for DW... + System.setProperty("dw.server.applicationConnectors[0].port", "0"); + System.setProperty("dw.server.adminConnectors[0].port", "0"); + System.setProperty("dw.redis.endpoint", REDIS_URL); + + /* + Note: When running using IntelliJ, make sure the working directory is set to the + driver module. + */ + try { + application.run("server", "src/main/resources/em_config.yaml"); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + + try { + Thread.sleep(3_000); + } catch (InterruptedException e) { + + } + while (!application.getJettyServer().isStarted()) { + try { + Thread.sleep(1_000); + } catch (InterruptedException e) { + } + } + + return "http://localhost:" + application.getJettyPort(); + } + + @Override + public void stopSut() { + if (application != null) { + try { + application.getJettyServer().stop(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + redisContainer.stop(); + } + + @Override + public void resetStateOfSUT() { + try (Jedis jedis = jedisPool.getResource()) { + jedis.flushAll(); + } + } + + @Override + public List getDbSpecifications() { + return null; + } +} diff --git a/jdk_11_maven/em/embedded/rest/pay-publicapi/src/main/resources/em_config.yaml b/jdk_11_maven/em/embedded/rest/pay-publicapi/src/main/resources/em_config.yaml new file mode 100644 index 000000000..e863f3409 --- /dev/null +++ b/jdk_11_maven/em/embedded/rest/pay-publicapi/src/main/resources/em_config.yaml @@ -0,0 +1,69 @@ +server: + applicationConnectors: + - type: http + port: ${PORT:-8080} + adminConnectors: + - type: http + port: ${ADMIN_PORT:-8081} + requestLog: + appenders: + - type: console + layout: + type: govuk-pay-access-json + additionalFields: + container: "publicapi" + environment: ${ENVIRONMENT} + +logging: + level: INFO + appenders: + - type: logstash-console + threshold: ALL + target: stdout + customFields: + container: "publicapi" + environment: ${ENVIRONMENT} + - type: sentry + threshold: ERROR + dsn: ${SENTRY_DSN:-https://example.com@dummy/1} + environment: ${ENVIRONMENT} + +baseUrl: ${PUBLICAPI_BASE} +connectorUrl: ${CONNECTOR_URL} +publicAuthUrl: http://public.auth.url.com:8080 +#publicAuthUrl: ${PUBLIC_AUTH_URL} +ledgerUrl: ${LEDGER_URL} + +jerseyClientConfig: + disabledSecureConnection: ${DISABLE_INTERNAL_HTTPS:-false} + +rateLimiter: # rate = noOfReq per perMillis + noOfReq: ${RATE_LIMITER_VALUE:-75} # for requests except POST and across all publicapi instances. + noOfReqForPost: ${RATE_LIMITER_VALUE_POST:-15} # for POST requests across all publicapi instances. + elevatedAccounts: ${RATE_LIMITER_ELEVATED_ACCOUNTS} + noOfReqForElevatedAccounts: ${RATE_LIMITER_ELEVATED_VALUE_GET:-100} + noOfPostReqForElevatedAccounts: ${RATE_LIMITER_ELEVATED_VALUE_POST:-40} + noOfReqPerNode: ${RATE_LIMITER_VALUE_PER_NODE:-25} # per public api instance, if Redis is unavailable + noOfReqForPostPerNode: ${RATE_LIMITER_VALUE_PER_NODE_POST:-5} # per public api instance, if Redis is unavailable + perMillis: ${RATE_LIMITER_PER_MILLIS:-1000} + lowTrafficAccounts: ${RATE_LIMITER_LOW_TRAFFIC_ACCOUNTS} + noOfReqForLowTrafficAccounts: ${RATE_LIMITER_LOW_TRAFFIC_VALUE_GET:-4500} + noOfPostReqForLowTrafficAccounts: ${RATE_LIMITER_LOW_TRAFFIC_VALUE_POST:-1} + intervalInMillisForLowTrafficAccounts: ${RATE_LIMITER_LOW_TRAFFIC_PER_MILLIS:-60000} + +redis: + endpoint: ${REDIS_URL:-localhost:6379} + ssl: false + commandTimeout: ${REDIS_COMMAND_TIMEOUT:-250ms} + connectTimeout: ${REDIS_CONNECT_TIMEOUT:-500ms} + +allowHttpForReturnUrl: ${ALLOW_HTTP_FOR_RETURN_URL:-false} + +#apiKeyHmacSecret: ${TOKEN_API_HMAC_SECRET} +apiKeyHmacSecret: "mysupersecret" + + +# Caching authenticator. +authenticationCachePolicy: expireAfterWrite=1m + +ecsContainerMetadataUriV4: ${ECS_CONTAINER_METADATA_URI_V4:-} diff --git a/jdk_11_maven/em/embedded/rest/pom.xml b/jdk_11_maven/em/embedded/rest/pom.xml index c0199d623..aa38926ee 100644 --- a/jdk_11_maven/em/embedded/rest/pom.xml +++ b/jdk_11_maven/em/embedded/rest/pom.xml @@ -15,6 +15,7 @@ cwa-verification market + pay-publicapi - \ No newline at end of file + diff --git a/jdk_11_maven/em/external/rest/pay-publicapi/pom.xml b/jdk_11_maven/em/external/rest/pay-publicapi/pom.xml new file mode 100644 index 000000000..3812b717c --- /dev/null +++ b/jdk_11_maven/em/external/rest/pay-publicapi/pom.xml @@ -0,0 +1,65 @@ + + + + 4.0.0 + + evomaster-benchmark-jdk11-em-external-rest-pay-publicapi + jar + + + org.evomaster + evomaster-benchmark-jdk11-em-external-rest + 2.0.1-SNAPSHOT + + + + + org.testcontainers + testcontainers + compile + + + redis.clients + jedis + 5.1.0 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + pay-publicapi-evomaster-runner + + + + em.external.uk.gov.pay.api.app.ExternalEvoMasterController + + org.evomaster.client.java.instrumentation.InstrumentingAgent + + org.evomaster.client.java.instrumentation.InstrumentingAgent + + true + true + + + + + + + + + + + + \ No newline at end of file diff --git a/jdk_11_maven/em/external/rest/pay-publicapi/src/main/java/em/external/uk/gov/pay/api/app/ExternalEvoMasterController.java b/jdk_11_maven/em/external/rest/pay-publicapi/src/main/java/em/external/uk/gov/pay/api/app/ExternalEvoMasterController.java new file mode 100644 index 000000000..7f3821dd0 --- /dev/null +++ b/jdk_11_maven/em/external/rest/pay-publicapi/src/main/java/em/external/uk/gov/pay/api/app/ExternalEvoMasterController.java @@ -0,0 +1,221 @@ +package em.external.uk.gov.pay.api.app; + +import org.evomaster.client.java.controller.AuthUtils; +import org.evomaster.client.java.controller.ExternalSutController; +import org.evomaster.client.java.controller.InstrumentedSutStarter; +import org.evomaster.client.java.controller.api.dto.SutInfoDto; +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto; +import org.evomaster.client.java.controller.problem.ProblemInfo; +import org.evomaster.client.java.controller.problem.RestProblem; +import org.evomaster.client.java.sql.DbSpecification; +import org.testcontainers.containers.GenericContainer; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.List; + +public class ExternalEvoMasterController extends ExternalSutController { + + public static void main(String[] args) { + + int controllerPort = 40100; + if (args.length > 0) { + controllerPort = Integer.parseInt(args[0]); + } + int sutPort = 12345; + if (args.length > 1) { + sutPort = Integer.parseInt(args[1]); + } + String jarLocation = "cs/rest/pay-publicapi/target"; + if (args.length > 2) { + jarLocation = args[2]; + } + if (!jarLocation.endsWith(".jar")) { + jarLocation += "/pay-publicapi-sut.jar"; + } + int timeoutSeconds = 120; + if (args.length > 3) { + timeoutSeconds = Integer.parseInt(args[3]); + } + String command = "java"; + if (args.length > 4) { + command = args[4]; + } + + + ExternalEvoMasterController controller = + new ExternalEvoMasterController(controllerPort, jarLocation, sutPort, timeoutSeconds, command); + InstrumentedSutStarter starter = new InstrumentedSutStarter(controller); + + starter.start(); + } + + + private final int timeoutSeconds; + private final int sutPort; + + private String jarLocation; + private final String CONFIG_FILE = "em_config.yaml"; + + private static final int REDIS_PORT = 6379; + + private static final String REDIS_VERSION = "7.2.3"; + + private static final GenericContainer redisContainer = new GenericContainer("redis:" + REDIS_VERSION) + .withExposedPorts(REDIS_PORT); + + private static String REDIS_URL = ""; + + private static JedisPool jedisPool; + + public ExternalEvoMasterController() { + this(40100, "../api/target", 12345, 120, "java"); + } + + public ExternalEvoMasterController(String jarLocation) { + this(); + this.jarLocation = jarLocation; + } + + public ExternalEvoMasterController(int controllerPort, + String jarLocation, + int sutPort, + int timeoutSeconds, + String command) { + this.sutPort = sutPort; + this.jarLocation = jarLocation; + this.timeoutSeconds = timeoutSeconds; + setControllerPort(controllerPort); + setJavaCommand(command); + createConfigurationFile(); + } + + + /** + * Unfortunately, it seems like Dropwizard is buggy, and has + * problems with overriding params without a YML file :( + */ + private void createConfigurationFile() { + + //save config to same folder of JAR file + Path path = getConfigPath(); + + try (InputStream is = this.getClass().getResourceAsStream("/" + CONFIG_FILE)) { + Files.copy(is, path, StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private Path getConfigPath() { + return Paths.get(jarLocation) + .toAbsolutePath() + .getParent() + .resolve(CONFIG_FILE) + .normalize(); + } + + @Override + public String[] getInputParameters() { + return new String[]{"server", getConfigPath().toAbsolutePath().toString()}; + } + + @Override + public String[] getJVMParameters() { + + return new String[]{ + "-Ddw.server.applicationConnectors[0].port=" + sutPort, + "-Ddw.server.adminConnectors[0].port=0", + "-Ddw.redis.endpoint=" + REDIS_URL + }; + } + + @Override + public String getBaseURL() { + return "http://localhost:" + sutPort; + } + + @Override + public String getPathToExecutableJar() { + return jarLocation; + } + + @Override + public String getLogMessageOfInitializedServer() { + return "Started application"; + } + + @Override + public long getMaxAwaitForInitializationInSeconds() { + return timeoutSeconds; + } + + @Override + public void preStart() { + redisContainer.start(); + + REDIS_URL = redisContainer.getHost() + redisContainer.getMappedPort(REDIS_PORT); + + jedisPool = new JedisPool(redisContainer.getHost(), redisContainer.getMappedPort(REDIS_PORT)); + } + + @Override + public void postStart() { + + resetStateOfSUT(); + } + + @Override + public void preStop() { + + } + + @Override + public void postStop() { + redisContainer.stop(); + } + + @Override + public String getPackagePrefixesToCover() { + return "uk.gov.pay.api."; + } + + @Override + public void resetStateOfSUT() { + try (Jedis jedis = jedisPool.getResource()) { + jedis.flushAll(); + } + } + + @Override + public ProblemInfo getProblemInfo() { + return new RestProblem( + getBaseURL() + "/assets/swagger.json", + null + ); + } + + @Override + public SutInfoDto.OutputFormat getPreferredOutputFormat() { + return SutInfoDto.OutputFormat.JAVA_JUNIT_5; + } + + @Override + public List getInfoForAuthentication() { + AuthenticationDto dto = AuthUtils.getForAuthorizationHeader("foo", "Bearer asdfghdasdjlguuolnga94upq3nrd2642sq7uel0oo"); + dto.requireMockHandling = true; + return Arrays.asList(dto); + } + + + @Override + public List getDbSpecifications() { + return null; + } +} diff --git a/jdk_11_maven/em/external/rest/pay-publicapi/src/main/resources/em_config.yaml b/jdk_11_maven/em/external/rest/pay-publicapi/src/main/resources/em_config.yaml new file mode 100644 index 000000000..e863f3409 --- /dev/null +++ b/jdk_11_maven/em/external/rest/pay-publicapi/src/main/resources/em_config.yaml @@ -0,0 +1,69 @@ +server: + applicationConnectors: + - type: http + port: ${PORT:-8080} + adminConnectors: + - type: http + port: ${ADMIN_PORT:-8081} + requestLog: + appenders: + - type: console + layout: + type: govuk-pay-access-json + additionalFields: + container: "publicapi" + environment: ${ENVIRONMENT} + +logging: + level: INFO + appenders: + - type: logstash-console + threshold: ALL + target: stdout + customFields: + container: "publicapi" + environment: ${ENVIRONMENT} + - type: sentry + threshold: ERROR + dsn: ${SENTRY_DSN:-https://example.com@dummy/1} + environment: ${ENVIRONMENT} + +baseUrl: ${PUBLICAPI_BASE} +connectorUrl: ${CONNECTOR_URL} +publicAuthUrl: http://public.auth.url.com:8080 +#publicAuthUrl: ${PUBLIC_AUTH_URL} +ledgerUrl: ${LEDGER_URL} + +jerseyClientConfig: + disabledSecureConnection: ${DISABLE_INTERNAL_HTTPS:-false} + +rateLimiter: # rate = noOfReq per perMillis + noOfReq: ${RATE_LIMITER_VALUE:-75} # for requests except POST and across all publicapi instances. + noOfReqForPost: ${RATE_LIMITER_VALUE_POST:-15} # for POST requests across all publicapi instances. + elevatedAccounts: ${RATE_LIMITER_ELEVATED_ACCOUNTS} + noOfReqForElevatedAccounts: ${RATE_LIMITER_ELEVATED_VALUE_GET:-100} + noOfPostReqForElevatedAccounts: ${RATE_LIMITER_ELEVATED_VALUE_POST:-40} + noOfReqPerNode: ${RATE_LIMITER_VALUE_PER_NODE:-25} # per public api instance, if Redis is unavailable + noOfReqForPostPerNode: ${RATE_LIMITER_VALUE_PER_NODE_POST:-5} # per public api instance, if Redis is unavailable + perMillis: ${RATE_LIMITER_PER_MILLIS:-1000} + lowTrafficAccounts: ${RATE_LIMITER_LOW_TRAFFIC_ACCOUNTS} + noOfReqForLowTrafficAccounts: ${RATE_LIMITER_LOW_TRAFFIC_VALUE_GET:-4500} + noOfPostReqForLowTrafficAccounts: ${RATE_LIMITER_LOW_TRAFFIC_VALUE_POST:-1} + intervalInMillisForLowTrafficAccounts: ${RATE_LIMITER_LOW_TRAFFIC_PER_MILLIS:-60000} + +redis: + endpoint: ${REDIS_URL:-localhost:6379} + ssl: false + commandTimeout: ${REDIS_COMMAND_TIMEOUT:-250ms} + connectTimeout: ${REDIS_CONNECT_TIMEOUT:-500ms} + +allowHttpForReturnUrl: ${ALLOW_HTTP_FOR_RETURN_URL:-false} + +#apiKeyHmacSecret: ${TOKEN_API_HMAC_SECRET} +apiKeyHmacSecret: "mysupersecret" + + +# Caching authenticator. +authenticationCachePolicy: expireAfterWrite=1m + +ecsContainerMetadataUriV4: ${ECS_CONTAINER_METADATA_URI_V4:-} diff --git a/jdk_11_maven/em/external/rest/pom.xml b/jdk_11_maven/em/external/rest/pom.xml index b0a21df3c..ba9f56533 100644 --- a/jdk_11_maven/em/external/rest/pom.xml +++ b/jdk_11_maven/em/external/rest/pom.xml @@ -16,6 +16,7 @@ cwa-verification market ind1 + pay-publicapi \ No newline at end of file diff --git a/jdk_11_maven/em/pom.xml b/jdk_11_maven/em/pom.xml index 3a94df51b..3838f5f0a 100644 --- a/jdk_11_maven/em/pom.xml +++ b/jdk_11_maven/em/pom.xml @@ -80,7 +80,7 @@ com.github.tomakehurst - wiremock-jre8 + wiremock-jre8-standalone 2.32.0 @@ -129,4 +129,4 @@ - \ No newline at end of file + diff --git a/jdk_17_maven/cs/pom.xml b/jdk_17_maven/cs/pom.xml index 5c736295d..0748a3ee0 100644 --- a/jdk_17_maven/cs/pom.xml +++ b/jdk_17_maven/cs/pom.xml @@ -14,6 +14,7 @@ web grpc + rest diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/ba-sak-proxy/Dockerfile b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/ba-sak-proxy/Dockerfile new file mode 100644 index 000000000..1b542ddde --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/ba-sak-proxy/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx + +COPY nginx.conf /etc/nginx/conf.d/default.conf \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/ba-sak-proxy/nais.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/ba-sak-proxy/nais.yaml new file mode 100644 index 000000000..238fb4ead --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/ba-sak-proxy/nais.yaml @@ -0,0 +1,85 @@ +apiVersion: "nais.io/v1alpha1" +kind: "Application" +metadata: + name: familie-ba-sak + namespace: teamfamilie + labels: + team: teamfamilie + +spec: + image: {{image}} + port: 8089 + leaderElection: true + liveness: + path: /internal/health + initialDelay: 20 + failureThreshold: 10 + readiness: + path: /internal/health + initialDelay: 20 + failureThreshold: 10 + prometheus: + enabled: true + path: /internal/prometheus + replicas: + min: 2 + max: 4 + cpuThresholdPercentage: 50 + resources: + limits: + memory: 64Mi + cpu: 200m + requests: + memory: 32Mi + cpu: 50m + ingresses: # Optional. List of ingress URLs that will route HTTP traffic to the application. + - https://familie-ba-sak.dev-fss-pub.nais.io # Deprecated - erstattes av dev.adeo.no + - https://familie-ba-sak.dev.adeo.no + secureLogs: + enabled: true + kafka: + pool: nav-dev + env: + - name: SPRING_PROFILES_ACTIVE + value: preprod + - name: SRVUSER_BA_SAK_SECRET + value: /var/run/secrets/srvfamilie-ba-sak/password + azure: + application: + enabled: true + allowAllUsers: false + claims: + groups: + - id: "928636f4-fd0d-4149-978e-a6fb68bb19de" # 0000-GA-STDAPPS + - id: "93a26831-9866-4410-927b-74ff51a9107c" # VEILEDER_ROLLE + - id: "d21e00a4-969d-4b28-8782-dc818abfae65" # SAKSBEHANDLER_ROLLE + - id: "9449c153-5a1e-44a7-84c6-7cc7a8867233" # BESLUTTER_ROLLE + extra: + - "NAVident" + accessPolicy: + inbound: + rules: + - application: familie-ba-sak-frontend + - application: familie-ba-sak-frontend + cluster: dev-gcp + - application: familie-prosessering + cluster: dev-gcp + - application: familie-ba-skatteetaten-api + cluster: dev-gcp + - application: familie-ba-migrering + cluster: dev-gcp + - application: bidrag-grunnlag-feature + namespace: bidrag + cluster: dev-gcp + - application: bidrag-grunnlag + namespace: bidrag + cluster: dev-gcp + outbound: + rules: + - application: familie-brev + cluster: dev-gcp + - application: familie-integrasjoner + - application: familie-ba-infotrygd-feed + - application: familie-ba-infotrygd + - application: familie-ef-sak + cluster: dev-gcp \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/ba-sak-proxy/nginx.conf b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/ba-sak-proxy/nginx.conf new file mode 100644 index 000000000..50f570cf2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/ba-sak-proxy/nginx.conf @@ -0,0 +1,8 @@ +server { + listen 8089; + server_name ba-sak-proxy; + + location / { + proxy_pass https://familie-ba-sak.intern.dev.nav.no; + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/app-preprod.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/app-preprod.yaml new file mode 100644 index 000000000..0ef3f6a26 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/app-preprod.yaml @@ -0,0 +1,141 @@ +apiVersion: "nais.io/v1alpha1" +kind: "Application" +metadata: + name: familie-ba-sak + namespace: teamfamilie + labels: + team: teamfamilie + annotations: + nginx.ingress.kubernetes.io/proxy-next-upstream-tries: '1' +spec: + envFrom: + - secret: familie-ba-sak + - secret: familie-ba-sak-unleash-api-token + image: {{image}} + port: 8089 + leaderElection: true + liveness: + path: /internal/health/liveness + initialDelay: 30 + failureThreshold: 10 + readiness: + path: /internal/health/readyness + initialDelay: 30 + failureThreshold: 10 + prometheus: + enabled: true + path: /internal/prometheus + gcp: # Database + sqlInstances: + - type: POSTGRES_15 # IF This is changed, all data will be lost. Read on nais.io how to upgrade + tier: db-custom-1-3840 + name: familie-ba-sak + autoBackupTime: "02:00" + pointInTimeRecovery: true + diskAutoresize: true + highAvailability: false + databases: + - name: familie-ba-sak + envVarPrefix: DB + azure: + application: + enabled: true + allowAllUsers: false + claims: + extra: + - "NAVident" + groups: + - id: "928636f4-fd0d-4149-978e-a6fb68bb19de" # 0000-GA-STDAPPS + - id: "93a26831-9866-4410-927b-74ff51a9107c" # VEILEDER_ROLLE + - id: "d21e00a4-969d-4b28-8782-dc818abfae65" # SAKSBEHANDLER_ROLLE + - id: "9449c153-5a1e-44a7-84c6-7cc7a8867233" # BESLUTTER_ROLLE + - id: "c62e908a-cf20-4ad0-b7b3-3ff6ca4bf38b" # teamfamilie-forvaltning + - id: "5ef775f2-61f8-4283-bf3d-8d03f428aa14" # 0000-GA-Strengt_Fortrolig_Adresse + - id: "ea930b6b-9397-44d9-b9e6-f4cf527a632a" # 0000-GA-Fortrolig_Adresse + replyURLs: + - "https://familie-ba-sak.intern.dev.nav.no/swagger-ui/oauth2-redirect.html" + - "http://localhost:8089/swagger-ui/oauth2-redirect.html" + singlePageApplication: true + accessPolicy: + inbound: + rules: + - application: familie-ba-sak-frontend + cluster: dev-gcp + - application: familie-ba-sak-frontend-lokal + cluster: dev-gcp + - application: familie-baks-mottak + cluster: dev-gcp + - application: familie-prosessering + cluster: dev-gcp + - application: familie-ba-skatteetaten-api + cluster: dev-gcp + - application: familie-klage + cluster: dev-gcp + - application: familie-ba-statistikk + cluster: dev-fss + - application: bidrag-grunnlag-feature + namespace: bidrag + cluster: dev-gcp + - application: bidrag-grunnlag + namespace: bidrag + cluster: dev-gcp + - application: omsorgsopptjening-start-innlesning + namespace: pensjonopptjening + cluster: dev-gcp + outbound: + rules: + - application: familie-brev + cluster: dev-gcp + - application: familie-integrasjoner + cluster: dev-fss + - application: familie-ba-infotrygd-feed + cluster: dev-fss + - application: familie-ba-infotrygd + cluster: dev-fss + - application: familie-ef-sak + cluster: dev-gcp + - application: familie-tilbake + cluster: dev-gcp + - application: familie-oppdrag + cluster: dev-fss + - application: familie-klage + cluster: dev-gcp + external: + - host: xsrv1mh6.api.sanity.io + - host: unleash.nais.io + - host: pdl-api.dev-fss-pub.nais.io + - host: familie-integrasjoner.dev-fss-pub.nais.io + - host: familie-oppdrag.dev-fss-pub.nais.io + - host: familie-ba-statistikk.dev-fss-pub.nais.io + - host: familie-ba-infotrygd.dev-fss-pub.nais.io + - host: familie-ba-infotrygd-feed.dev-fss-pub.nais.io + - host: sdw-wsrest.ecb.europa.eu + - host: sentry.gc.nav.no + - host: data-api.ecb.europa.eu + - host: teamfamilie-unleash-api.nav.cloud.nais.io + replicas: + min: 2 + max: 2 + resources: + limits: + memory: 1024Mi + requests: + memory: 1024Mi + cpu: 500m + ingresses: + - https://familie-ba-sak.intern.dev.nav.no + secureLogs: + enabled: true + env: + - name: SPRING_PROFILES_ACTIVE + value: preprod + + - name: JAVA_OPTS + value: "-XX:MinRAMPercentage=25.0 -XX:MaxRAMPercentage=75.0 -XX:+HeapDumpOnOutOfMemoryError" + kafka: + pool: nav-dev + strategy: + rollingUpdate: + maxSurge: 100% + maxUnavailable: 99% + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/app-prod.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/app-prod.yaml new file mode 100644 index 000000000..39fe10199 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/app-prod.yaml @@ -0,0 +1,139 @@ +apiVersion: "nais.io/v1alpha1" +kind: "Application" +metadata: + name: familie-ba-sak + namespace: teamfamilie + labels: + team: teamfamilie + annotations: + nginx.ingress.kubernetes.io/proxy-next-upstream-tries: '1' + nginx.ingress.kubernetes.io/proxy-read-timeout: "180" + nginx.ingress.kubernetes.io/proxy-send-timeout: "180" + +spec: + envFrom: + - secret: familie-ba-sak + - secret: familie-ba-sak-unleash-api-token + image: {{image}} + port: 8089 + leaderElection: true + liveness: + path: /internal/health/liveness + initialDelay: 30 + failureThreshold: 10 + readiness: + path: /internal/health/readyness + initialDelay: 30 + failureThreshold: 10 + prometheus: + enabled: true + path: /internal/prometheus + gcp: # Database + sqlInstances: + - type: POSTGRES_15 # Read nais doc and https://github.com/navikt/familie/blob/master/doc/Google%20cloud/oppgrader-database.md on how to upgrade + tier: db-custom-4-3840 + name: familie-ba-sak + autoBackupTime: "02:00" + diskAutoresize: true + highAvailability: true + databases: + - name: familie-ba-sak + envVarPrefix: DB + azure: + application: + enabled: true + allowAllUsers: false + claims: + extra: + - "NAVident" + groups: + - id: "87190cf3-b278-457d-8ab7-1a5c55a9edd7" # Group_87190cf3-b278-457d-8ab7-1a5c55a9edd7 tilgang til prosessering + - id: "199c2b39-e535-4ae8-ac59-8ccbee7991ae" # VEILEDER_ROLLE + - id: "847e3d72-9dc1-41c3-80ff-f5d4acdd5d46" # SAKSBEHANDLER_ROLLE + - id: "7a271f87-39fb-468b-a9ee-6cf3c070f548" # BESLUTTER_ROLLE + - id: "3d718ae5-f25e-47a4-b4b3-084a97604c1d" # Forvalterrolle + - id: "ad7b87a6-9180-467c-affc-20a566b0fec0" # 0000-GA-Strengt_Fortrolig_Adresse + - id: "9ec6487d-f37a-4aad-a027-cd221c1ac32b" # 0000-GA-Fortrolig_Adresse + replyURLs: + - "https://familie-ba-sak.intern.nav.no/swagger-ui/oauth2-redirect.html" + singlePageApplication: true + accessPolicy: + inbound: + rules: + - application: familie-ba-sak-frontend + cluster: prod-gcp + - application: familie-baks-mottak + cluster: prod-gcp + - application: familie-prosessering + cluster: prod-gcp + - application: familie-ba-skatteetaten-api + cluster: prod-gcp + - application: familie-klage + cluster: prod-gcp + - application: familie-ba-statistikk + cluster: prod-fss + - application: bidrag-grunnlag-feature + namespace: bidrag + cluster: prod-gcp + - application: bidrag-grunnlag + namespace: bidrag + cluster: prod-gcp + - application: omsorgsopptjening-start-innlesning + namespace: pensjonopptjening + cluster: prod-gcp + outbound: + rules: + - application: familie-brev + cluster: prod-gcp + - application: familie-integrasjoner + cluster: prod-fss + - application: familie-ba-infotrygd-feed + cluster: prod-fss + - application: familie-ba-infotrygd + cluster: prod-fss + - application: familie-ef-sak + cluster: prod-gcp + - application: familie-tilbake + cluster: prod-gcp + - application: familie-oppdrag + cluster: prod-fss + - application: familie-klage + cluster: prod-gcp + external: + - host: xsrv1mh6.api.sanity.io + - host: unleash.nais.io + - host: pdl-api.prod-fss-pub.nais.io + - host: familie-integrasjoner.prod-fss-pub.nais.io + - host: familie-oppdrag.prod-fss-pub.nais.io + - host: familie-ba-statistikk.prod-fss-pub.nais.io + - host: familie-ba-infotrygd.prod-fss-pub.nais.io + - host: familie-ba-infotrygd-feed.prod-fss-pub.nais.io + - host: sdw-wsrest.ecb.europa.eu + - host: sentry.gc.nav.no + - host: data-api.ecb.europa.eu + - host: teamfamilie-unleash-api.nav.cloud.nais.io + replicas: + min: 3 + max: 5 + resources: + limits: + memory: 4096Mi + requests: + memory: 4096Mi + cpu: 500m + ingresses: + - https://familie-ba-sak.intern.nav.no + secureLogs: + enabled: true + env: + - name: SPRING_PROFILES_ACTIVE + value: prod + + - name: JAVA_OPTS + value: "-XX:MinRAMPercentage=25.0 -XX:MaxRAMPercentage=75.0 -XX:+HeapDumpOnOutOfMemoryError" + kafka: + pool: nav-prod + strategy: + rollingUpdate: + maxSurge: 100% + maxUnavailable: 99% diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/azure-ad-app-lokal.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/azure-ad-app-lokal.yaml new file mode 100644 index 000000000..0479a713b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/azure-ad-app-lokal.yaml @@ -0,0 +1,31 @@ +####### +# Denne er brukt til å gjøre det mulig å bruke tokens lokalt +# +# secret kan hentes fra cluster med "kubectl -n teamfamilie get secret azuread-familie-ba-sak-lokal -o json | jq '.data | map_values(@base64d)'" +# +# Kjøres en gang eller ved endringer med +# kubectl apply -f .deploy/azure-ad-app-lokal.yaml +### +apiVersion: nais.io/v1 +kind: AzureAdApplication +metadata: + name: familie-ba-sak-lokal + namespace: teamfamilie + labels: + team: teamfamilie +spec: + preAuthorizedApplications: + - application: familie-ba-sak-frontend-lokal + cluster: dev-gcp + namespace: teamfamilie + - application: familie-prosessering-lokal + cluster: dev-gcp + namespace: teamfamilie + tenant: trygdeetaten.no + secretName: azuread-familie-ba-sak-lokal + claims: + groups: + - id: "928636f4-fd0d-4149-978e-a6fb68bb19de" # 0000-GA-STDAPPS + - id: "93a26831-9866-4410-927b-74ff51a9107c" # VEILEDER_ROLLE + - id: "d21e00a4-969d-4b28-8782-dc818abfae65" # SAKSBEHANDLER_ROLLE + - id: "9449c153-5a1e-44a7-84c6-7cc7a8867233" # BESLUTTER_ROLLE \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/aapen-familie-ba-sak-identer-med-barnetrygd.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/aapen-familie-ba-sak-identer-med-barnetrygd.yaml new file mode 100644 index 000000000..93be2ed32 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/aapen-familie-ba-sak-identer-med-barnetrygd.yaml @@ -0,0 +1,26 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-familie-ba-sak-identer-med-barnetrygd + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-dev + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 2 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-ba-sak + access: write + - team: pensjonopptjening + application: omsorgsopptjening-start-innlesning + access: read + - team: teamfamilie + application: familie-ba-kafka-manager + access: read \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/bisys_opphoer_av_barnetrygd.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/bisys_opphoer_av_barnetrygd.yaml new file mode 100644 index 000000000..8940b6f31 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/bisys_opphoer_av_barnetrygd.yaml @@ -0,0 +1,29 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-familie-ba-sak-opphoer-barnetrygd + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-dev + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 168 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-ba-sak #owner + access: write # readwrite + - team: bidrag + application: bidrag-grunnlag-feature #consumer + access: read + - team: bidrag + application: bidrag-grunnlag #consumer + access: read + - team: teamfamilie + application: familie-ba-kafka-manager + access: read \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/dvh_saksstatistikk_sak_v1_topic.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/dvh_saksstatistikk_sak_v1_topic.yaml new file mode 100644 index 000000000..415ae4d33 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/dvh_saksstatistikk_sak_v1_topic.yaml @@ -0,0 +1,32 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-barnetrygd-saksstatistikk-sak-v1 + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-dev + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 336 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-ba-sak #owner + access: write # readwrite + - team: teamfamilie + application: familie-ba-statistikk + access: read + - team: teamfamilie + application: familie-ba-kafka-manager + access: read + - team: ptsak + application: pt-sak-barnetrygd-dev + access: read + - team: ptsak + application: pt-sak-barnetrygd-preprod + access: read \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/dvh_sakstatistikk_behandling_v1_topic.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/dvh_sakstatistikk_behandling_v1_topic.yaml new file mode 100644 index 000000000..63142845e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/dvh_sakstatistikk_behandling_v1_topic.yaml @@ -0,0 +1,32 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-barnetrygd-saksstatistikk-behandling-v1 + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-dev + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 336 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-ba-sak #owner + access: write # readwrite + - team: teamfamilie + application: familie-ba-statistikk + access: read + - team: teamfamilie + application: familie-ba-kafka-manager + access: read + - team: ptsak + application: pt-sak-barnetrygd-dev + access: read + - team: ptsak + application: pt-sak-barnetrygd-preprod + access: read \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/dvh_vedtak_topic.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/dvh_vedtak_topic.yaml new file mode 100644 index 000000000..edcd8aa81 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/dev/dvh_vedtak_topic.yaml @@ -0,0 +1,32 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-barnetrygd-vedtak-v2 + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-dev + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 168 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-ba-sak #owner + access: write # readwrite + - team: teamfamilie + application: familie-ba-statistikk + access: read + - team: teamfamilie + application: familie-ba-kafka-manager + access: read + - team: dv-familie + application: dvh-fambt-konsumer + access: read + - team: dv-familie + application: dvh-familie-konsument + access: read \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/aapen-familie-ba-sak-identer-med-barnetrygd.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/aapen-familie-ba-sak-identer-med-barnetrygd.yaml new file mode 100644 index 000000000..720959cff --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/aapen-familie-ba-sak-identer-med-barnetrygd.yaml @@ -0,0 +1,26 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-familie-ba-sak-identer-med-barnetrygd + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-prod + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 168 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-ba-sak + access: write + - team: pensjonopptjening + application: omsorgsopptjening-start-innlesning + access: read + - team: teamfamilie + application: familie-ba-kafka-manager + access: read \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/bisys_opphoer_av_barnetrygd.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/bisys_opphoer_av_barnetrygd.yaml new file mode 100644 index 000000000..4173e0036 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/bisys_opphoer_av_barnetrygd.yaml @@ -0,0 +1,26 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-familie-ba-sak-opphoer-barnetrygd + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-prod + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 168 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-ba-sak #owner + access: write # readwrite + - team: bidrag + application: bidrag-grunnlag #consumer + access: read + - team: teamfamilie + application: familie-ba-kafka-manager + access: read \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/dvh_saksstatistikk_sak_v1_topic.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/dvh_saksstatistikk_sak_v1_topic.yaml new file mode 100644 index 000000000..0ad96e1fe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/dvh_saksstatistikk_sak_v1_topic.yaml @@ -0,0 +1,29 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-barnetrygd-saksstatistikk-sak-v1 + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-prod + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 2016 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-ba-sak #owner + access: write # readwrite + - team: teamfamilie + application: familie-ba-statistikk + access: read + - team: teamfamilie + application: familie-ba-kafka-manager + access: read + - team: ptsak + application: pt-sak-barnetrygd + access: read \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/dvh_sakstatistikk_behandling_v1_topic.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/dvh_sakstatistikk_behandling_v1_topic.yaml new file mode 100644 index 000000000..b2c0a6d02 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/dvh_sakstatistikk_behandling_v1_topic.yaml @@ -0,0 +1,29 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-barnetrygd-saksstatistikk-behandling-v1 + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-prod + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 2016 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-ba-sak #owner + access: write # readwrite + - team: teamfamilie + application: familie-ba-statistikk + access: read + - team: teamfamilie + application: familie-ba-kafka-manager + access: read + - team: ptsak + application: pt-sak-barnetrygd + access: read \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/dvh_vedtak_topic.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/dvh_vedtak_topic.yaml new file mode 100644 index 000000000..299143841 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/kafka/prod/dvh_vedtak_topic.yaml @@ -0,0 +1,32 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-barnetrygd-vedtak-v2 + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-prod + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 1440 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-ba-sak #owner + access: write # readwrite + - team: teamfamilie + application: familie-ba-statistikk + access: read + - team: teamfamilie + application: familie-ba-kafka-manager + access: read + - team: dv-familie + application: dvh-fambt-konsumer + access: read + - team: dv-familie + application: dvh-familie-konsument + access: read \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/unleash-apitoken-preprod.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/unleash-apitoken-preprod.yaml new file mode 100644 index 000000000..a4daae5b3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/unleash-apitoken-preprod.yaml @@ -0,0 +1,14 @@ +apiVersion: unleash.nais.io/v1 +kind: ApiToken +metadata: + name: familie-ba-sak + namespace: teamfamilie + labels: + team: teamfamilie +spec: + unleashInstance: + apiVersion: unleash.nais.io/v1 + kind: RemoteUnleash + name: teamfamilie + secretName: familie-ba-sak-unleash-api-token + environment: development \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/unleash-apitoken-prod.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/unleash-apitoken-prod.yaml new file mode 100644 index 000000000..1ba85287a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/.deploy/nais/unleash-apitoken-prod.yaml @@ -0,0 +1,14 @@ +apiVersion: unleash.nais.io/v1 +kind: ApiToken +metadata: + name: familie-ba-sak + namespace: teamfamilie + labels: + team: teamfamilie +spec: + unleashInstance: + apiVersion: unleash.nais.io/v1 + kind: RemoteUnleash + name: teamfamilie + secretName: familie-ba-sak-unleash-api-token + environment: production \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/CODEOWNERS b/jdk_17_maven/cs/rest/familie-ba-sak/CODEOWNERS new file mode 100644 index 000000000..d5ade9f35 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/CODEOWNERS @@ -0,0 +1 @@ +* @navikt/teamfamilie diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/Dockerfile b/jdk_17_maven/cs/rest/familie-ba-sak/Dockerfile new file mode 100644 index 000000000..5e63fba65 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/Dockerfile @@ -0,0 +1,7 @@ +FROM ghcr.io/navikt/baseimages/temurin:17 + +ENV APPD_ENABLED=true +ENV APP_NAME=familie-ba-sak + +COPY ./target/familie-ba-sak.jar "app.jar" + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/LICENSE b/jdk_17_maven/cs/rest/familie-ba-sak/LICENSE new file mode 100644 index 000000000..0ced1b1f2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 NAV + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/README.md b/jdk_17_maven/cs/rest/familie-ba-sak/README.md new file mode 100644 index 000000000..1b5c585f1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/README.md @@ -0,0 +1,294 @@ +# familie-ba-sak + +Saksbehandling for barnetrygd + +## Kjøring lokalt + +For å kjøre opp appen lokalt kan en kjøre + +* `DevLauncher`, som kjører opp en H2-(minnebasert) database (obs: kjører med task-rammeverket deaktivert) +* `DevLauncherPostgres`, som kjører opp med Spring-profilen `postgres` satt, og forventer en kjørende database. Samme + effekt kan du med `DevLauncher` med + `-Dspring.profiles.active=postgres` satt under `Edit Configurations -> VM Options`. +* `DevLauncherPostgresPreprod`. Kjører mot intergrasjoner og pdl i preprod(ikke q1, men syntetisk). Har støtte for å + kjøre mot andre miljøer, men da må mock manuelt kommenteres ut i DevLauncherPostgresPreprod. BA_SAK_CLIENT_ID og + CLIENT_SECRET må settes til familie-ba-sak sin azure client id og secret for å få tilgang til pdl og integrasjoner. + Frontend må derfor bruke scope mot familie-ba-sak og ikke familie-ba-sak-lokal + +Appen tilgjengeliggjøres da på `localhost:8089`. Se [Database](#database) for hvordan du setter opp databasen. For å +tillate kall fra frontend, se [Autentisering](#autentisering). + +### Database + +#### Embedded database + +Bruker du `DevLauncherPostgres`, kan du kjøre opp en embedded database. Da må du sette `--dbcontainer` +under `Edit Configurations -> VM Options` + +#### Database i egen container + +Postgres-databasen kan settes opp slik: + +1. Lag en dockercontainer: +``` +docker run --name familie-ba-sak-postgres -e POSTGRES_PASSWORD=test -d -p 5432:5432 postgres +``` +2. List opp alle containerne og finn container id for container med name = familie-ba-sak-postgres: + +``` +docker ps +``` +3. Kjør docker container: +``` +docker exec -it bash +``` + +4. Åpne postgres som brukeren "postgres": +``` +psql -U postgres +``` + +5. Lag en database med navn "familie-ba-sak": +``` +CREATE DATABASE "familie-ba-sak"; +``` + +Legg til databasen i Intellij: +1. Trykk på database på høyre side og "+" -> data source -> postgreSQL +2. Fyll inn port=5432, user=postgres, passord=test og database=familie-ba-sak + +OBS: Pass på at du ikke kjører postgres lokalt på samme port (5432) + + +### Autentisering + +For å kalle applikasjonen fra fontend må du sette miljøvariablene BA_SAK_CLIENT_ID og CLIENT_SECRET. Dette kan gjøres +under `Edit Configurations -> Environment Variables`. Miljøvariablene kan hentes fra `azuread-familie-ba-sak-lokal` i +dev-gcp-clusteret ved å gjøre følgende: + +1. Logg på `gcloud`, typisk med kommandoen: `gcloud auth login` +2. Koble deg til dev-gcp-cluster'et: `kubectl config use-context dev-gcp` +3. Hent info: + `kubectl -n teamfamilie get secret azuread-familie-ba-sak-lokal -o json | jq '.data | map_values(@base64d)'`. + +BA_SAK_CLIENT_ID må settes til `AZURE_APP_CLIENT_ID` og CLIENT_SECRET til`AZURE_APP_CLIENT_SECRET` + +Se `.deploy/nais/azure-ad-app-lokal.yaml` dersom du ønsker å deploye `azuread-familie-ba-sak-lokal` + +Til slutt skal miljøvariablene se slik ut: + +DevLauncher/DevLauncherPostgres + +* BA_SAK_CLIENT_ID=`AZURE_APP_CLIENT_ID` (fra `azuread-familie-ba-sak-lokal`) +* CLIENT_SECRET=`AZURE_APP_CLIENT_SECRET` (fra `azuread-familie-ba-sak-lokal`) + +DevLauncherPostgresPreprod: +Trenger i utgangspunktet ikke å sette miljøvariabler manuelt. De hentes automatisk fra Nais. +Krever at man er logget på naisdevice og gcloud. +Husk å sette `BA_SAK_SCOPE=api://dev-gcp.teamfamilie.familie-ba-sak/.default` i `.env`-filen frontend. + +Alternativt kan du starte med flagget '--manuellMiljø', og manuelt setje miljøvariablane. +Det krever at man henter azuread fra en pod til familie-ba-sak. Som rulleres oftere enn azuread-familie-ba-sak-lokal +`kubectl -n teamfamilie exec -c familie-ba-sak -it familie-ba-sak-byttmegmedpodid -- env | grep AZURE_APP_CLIENT` + +* BA_SAK_CLIENT_ID=`AZURE_APP_CLIENT_ID` (fra `familie-ba-sak`) +* CLIENT_SECRET=`AZURE_APP_CLIENT_SECRET` (fra `familie-ba-sak`) + +### Funksjonsbrytere + +Vi bruker [unleash](https://unleash.nais.io) til å håndtere funksjonsbrytere. + +#### Skru av og på ved lokal testing + +Setter du `-D=[true|false]` på VM Options, vil den gjeldende bryteren skrus av eller på + +### Bruke Postman + +Du kan bruke Postman til å kalle APIene i ba-sak. Det krever at du har satt opp [autentisering](#autentisering) riktig, +og har et token som gjør at du kaller som ba-sak-frontend. + +#### Preprod + +Den nødvendige informasjonen for å få frontend-token'et får du ved å kalle: + +`kubectl -n teamfamilie get secret azuread-familie-ba-sak-frontend-lokal -o json | jq '.data | map_values(@base64d)'`. + +I Postman gjør du et GET-kall med følgende oppsett: + +* URL: `https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token` +* Headers -> Cookie: `fpc=AsRNnIJ3MI9FqfN68mC5KW4` +* Body: `x-www-form-urlencoded` med følgende key-values + * `grant_type`: `client_credentials` + * `client_id`: <`AZURE_APP_CLIENT_ID`> fra kubectl-kallet over + * `client_secret`: <`AZURE_APP_CLIENT_SECRET`> fra kubectl-kallet over + * `scope`: `api://dev-gcp.teamfamilie.familie-ba-sak-lokal/.default` + +
+PROD +
+ +#### Oppskrift for kall fra Postman mot prod + +For å finne den nødvendige informasjonen for å få frontend-token'et i prod må du: + +1. Endre kontekst til prod-gcp `kubectl config use-context prod-gcp` +2. Finne navn på secret ved å kjøre `kubectl -n teamfamilie get secrets` og finne navnet på en secret som starter + med `azure-familie-ba-sak-frontend-`. Kopier navnet på secreten. +3. Kjør `kubectl -n teamfamilie get secret [NAVN PÅ SECRET FRA STEG 2] -o json | jq '.data | map_values(@base64d)'` + +I Postman gjør du et GET-kall med følgende oppsett (OBS OBS - husk at dette er rett mot prod!): + +* URL: `https://login.microsoftonline.com/navno.onmicrosoft.com/oauth2/v2.0/token` +* Headers -> Cookie: `fpc=AsRNnIJ3MI9FqfN68mC5KW4` +* Body: `x-www-form-urlencoded` med følgende key-values + * `grant_type`: `client_credentials` + * `client_id`: <`AZURE_APP_CLIENT_ID`> fra kubectl-kallet over + * `client_secret`: <`AZURE_APP_CLIENT_SECRET`> fra kubectl-kallet over + * `scope`: `api://prod-gcp.teamfamilie.familie-ba-sak/.default` + +
+
+ +#### Lagre token globalt i Postman + +Et triks kan være å sette opp en "test" under *Tests* i request'en: + +``` +pm.test("Lagre token globalt", function () { + var jsonData = pm.response.json(); + pm.globals.set("azure-familie-ba-sak", jsonData.access_token); +}); +``` + +som vil plukke ut token'et og lagre det i en global variabel, her `azure-familie-ba-sak` + +Når du lager kall mot APIet, så kan du sette følgende i header'en for å sende med token'et: + +* `Authorization`: `Bearer {{azure-familie-ba-sak}}` + +### Ktlint + +* Vi bruker ktlint i dette prosjektet for å formatere kode. +* Du kan skru på automatisk reformattering av filer ved å installere en plugin som heter`Ktlint (unofficial)` + fra `Preferences > Plugins > Marketplace` +* Gå til `Preferences > Tools > Actions on Save` og huk av så `Reformat code` og `Optimize imports` er markert. +* Gå til `Preferences > Tools > ktlint`og pass på at `Enable ktlint` og `Lint after Reformat` er huket av. + +#### Manuel kjøring av ktlint + +* Kjør `mvn antrun:run@ktlint-format` i terminalen + +## Kafka + +Dersom man vil kjøre med kafka, kan man bytte sette property funksjonsbrytere.vedtak.producer.enabled=true. Da må man +sette opp kafka, dette gjøres gjennom å +kjøre [navkafka-docker-compose](https://github.com/navikt/navkafka-docker-compose) lokal, se README i +navkafka-docker-compose for mer info om hvordan man kjører den. + +Topicen vi lytter på må da opprettes via deres api med følgende kommando: + +``` +curl -X POST "http://igroup:itest@localhost:8840/api/v1/topics" -H "Accept: application/json" -H "Content-Type: application/json" --data "{"name": "aapen-barnetrygd-vedtak-v1", "members": [{ "member": "srvc01", "role": "CONSUMER" }], "numPartitions": 1 }" +curl -X POST "http://igroup:itest@localhost:8840/api/v1/topics" -H "Accept: application/json" -H "Content-Type: application/json" --data "{"name": "aapen-barnetrygd-saksstatistikk-sak-v1", "members": [{ "member": "srvc01", "role": "CONSUMER" }], "numPartitions": 1 }" +curl -X POST "http://igroup:itest@localhost:8840/api/v1/topics" -H "Accept: application/json" -H "Content-Type: application/json" --data "{"name": "aapen-barnetrygd-saksstatistikk-behandling-v1", "members": [{ "member": "srvc01", "role": "CONSUMER" }], "numPartitions": 1 }" + +``` + +## Produksjonssetting + +Main-branchen blir automatisk bygget ved merge og deployet først til preprod og dernest til prod. + +### Hastedeploy + +Hvis vi trenger å deploye raskt til prod, har vi egne byggejobber for den biten, som trigges manuelt. + +Den ene (krise-rett-i-prod) sjekker ut koden og bygger fra den. + +Den andre (krise-eksisterende-image-rett-i-prod) lar deg deploye et tidligere bygd image. Det slår til for eksempel hvis +du skal rulle tilbake til forrige versjon. Denne tar som parameter taggen til imaget du vil deploye. Denne finner du +under actions på GitHub, finn byggejobben du vil gå tilbake til, og kopier taggen derfra. + +### Oppretting av Kafka kø + +Kafka kø må opprettes manuelt i hver miljø, en gang. Detter gjørs som beskrevet +i [Opprett kø](https://confluence.adeo.no/display/AURA/Kafka#Kafka-NavngivningavTopic) +bruk topic definisjon og konfigurasjon som beskrevet i resources/kafka/topic-.json + +## Testing i Postman + +Det kan være praktisk å teste ut api'et i et verktøy som Postman. Da må vi late som vi er familie-ba-sak-frontend. + +Oppsettet består av tre deler: + +* Conf'e BA-sak riktig +* Få tak i et gyldig token +* Sende en request med et token'et + +### Conf'e BA-sak riktig + +Vi trenger å sette tre miljøvariable i familie-ba-sak: + +* `BA_SAK_CLIENT_ID`: Id'en til familie-ba-sak +* `CLIENT_SECRET`: Hemmeligheten til familie-ba-sak +* `BA_SAK_FRONTEND_CLIENT_ID`: Id'en til frontend-app'en + +Se [Autentisering](#autentisering) for de to første + +Verdien for `BA_SAK_FRONTEND_CLIENT_ID` får du tilsvarende med følgende kall: + +``` +kubectl -n teamfamilie get secret azuread-familie-ba-sak-frontend-lokal -o json | jq '.data | map_values(@base64d)' +``` + +`AZURE_APP_CLIENT_ID` inneholder client-id'en + +### Få tak i et gyldig token (dev-fss) + +I postman lagrer du følgende request, som gir deg et token som familie-ba-sak-frontend for å kalle familie-ba-sak: + +* Verb: `GET` +* Url: `https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token` + +Under *Body*, sjekk av for `x-www-form-urlencoded`, og legg inn følgende key-value-par: + +* `grant_type`: `client_credentials` +* `client_id`: <`AZURE_APP_CLIENT_ID` for familie-ba-sak-frontend> +* `client_secret`: <`AZURE_APP_CLIENT_SECRET` for familie-ba-sak-frontend> +* `scope`: `api://dev-fss.teamfamilie.familie-ba-sak-lokal/.default` + +Under *Tests*, legg inn følgende script: + +``` +pm.test("Lagre token", function () { + var jsonData = pm.response.json(); + pm.globals.set("azure-familie-ba-sak", jsonData.access_token); +}); +``` + +Lagre gjerne request'en med et hyggelig navn, f.eks 'Token familie-ba-sak-frontend -> familie-ba-sak' + +### Sende en request med et token'et + +1. `Headers`: Her MÅ du ha `Authorization=Bearer {{azure-familie-ba-sak}}`, altså referanse til token'et som ble lagret + i scriptet over +2. `Verb`: F.eks `GET` +3. `Url`: F.eks `localhost:8089/api/kompetanse`` + Lagre gjerne request'en med et hyggelig navn, f.eks 'GET kompetanse' + +Kjør så: + +* 'Token familie-ba-sak-frontend -> familie-ba-sak', for å få token +* 'GET kompetanse' (f.eks) for å gjøre det du VIL gjøre + +## Les også +* [vilkårsperiodeProdusent - README](src%2Fmain%2Fkotlin%2Fno%2Fnav%2Ffamilie%2Fba%2Fsak%2Fkjerne%2Fvedtak%2Fvedtaksperiode%2Fprodusent%2FREADME.md) + +## Kontaktinformasjon + +For NAV-interne kan henvendelser om applikasjonen rettes til #team-familie på slack. Ellers kan man opprette et issue +her på github. + +## Tilgang til databasene i prod og preprod + +Se https://github.com/navikt/familie/blob/master/doc/utvikling/gcp/gcp_kikke_i_databasen.md + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/lagBaseline.sh b/jdk_17_maven/cs/rest/familie-ba-sak/lagBaseline.sh new file mode 100644 index 000000000..290c39ca0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/lagBaseline.sh @@ -0,0 +1,3 @@ +PGPASSWORD="test" pg_dump --host="localhost" --port="5432" --username="postgres" --dbname="familie-ba-sak" --no-owner --file="src/test/resources/db/migration-tests/V1__create_table.sql" --exclude-table=public."flyway_schema_history" && +PGPASSWORD="test" pg_dump --host="localhost" --port="5432" --username="postgres" --dbname="familie-ba-sak" --no-owner --file="src/test/resources/db/migration-tests/V2__FyllFlywaySchemaHistory.sql" --table=public."flyway_schema_history" --column-inserts --data-only && +sed -i '1, 24d' src/test/resources/db/migration-tests/V2__FyllFlywaySchemaHistory.sql \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/pom.xml b/jdk_17_maven/cs/rest/familie-ba-sak/pom.xml new file mode 100644 index 000000000..14588c70a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/pom.xml @@ -0,0 +1,545 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.3 + + + + no.nav.familie.ba.sak + familie-ba-sak + ${revision}${sha1}${changelist} + familie-ba-sak + Saksbehandling Barnetrygd + + + 17 + 1.9.10 + 1 + + -SNAPSHOT + 4.0.0 + 2.20231005144526_f184554 + 2.20230905101454_06fa3d7 + 2.0_20230214104704_706e9c0 + 3.0_20230921075936_3adfc44 + 2.0_20230214104704_706e9c0 + 2.0_20230912090318_2267c05 + 1.0_20230927085000_900743f + 2.0_20230214104704_706e9c0 + 7.14.0 + 1.13.5 + 3.2.0 + 3.1.7 + 1.0-SNAPSHOT.6 + 2.2.0 + 6.30.0 + 1.7.3 + + + 4.9.0 + 2.0.1 + + + 1.1.1 + 1.0.0 + 1.19.0 + 7.1.0 + 3.9.5 + 2.15.2 + 2.0.5 + 2.6.8 + 4.2.0 + + + + + + no.nav.familie.felles + felles + ${felles.version} + pom + import + + + org.jetbrains.kotlin + kotlin-bom + ${kotlin.version} + pom + import + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + io.sentry + sentry-bom + ${sentry.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-jetty + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + com.fasterxml.jackson.module + jackson-module-kotlin + ${jackson.module.kotlin.version} + + + org.flywaydb + flyway-core + + + org.springframework.kafka + spring-kafka + + + com.neovisionaries + nv-i18n + 1.29 + + + + com.papertrailapp + logback-syslog4j + ${logback-syslog4j.version} + + + + org.postgresql + postgresql + + + org.testcontainers + postgresql + ${testcontainers.postgresql.version} + test + + + + io.getunleash + unleash-client-java + ${unleash.version} + + + + + org.eclipse.jetty + jetty-server + + + + org.apache.maven + maven-model + ${maven.model.version} + + + + io.sentry + sentry-spring-boot-starter + + + + io.sentry + sentry-logback + + + + + org.springdoc + springdoc-openapi-starter-common + ${springdoc.version} + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} + + + + + + no.nav.security + token-client-spring + ${token-validation-spring.version} + + + no.nav.security + token-validation-test-support + ${token-validation-test-support.version} + test + + + no.nav.security + token-validation-spring-test + ${token-validation-spring.version} + test + + + no.nav.security + mock-oauth2-server + ${mock-oauth2-server.version} + test + + + nav-foedselsnummer + testutils + ${nav-foedselsnummer.version} + test + + + nav-foedselsnummer + core + ${nav-foedselsnummer.version} + + + no.nav.familie.felles + sikkerhet + + + no.nav.familie + prosessering-core + ${prosessering.version} + + + no.nav.familie.felles + log + + + no.nav.familie.felles + leader + + + no.nav.familie.felles + unleash + + + no.nav.familie.felles + http-client + + + no.nav.familie.felles + modell + + + no.nav.familie.felles + util + + + no.nav.familie.felles + valutakurs-klient + + + no.nav.familie.felles + unleash + + + no.nav.familie.kontrakter + felles + ${felles-kontrakter.version} + + + no.nav.familie.kontrakter + barnetrygd + ${felles-kontrakter.version} + + + no.nav.familie.eksterne.kontrakter + bisys + ${eksterne-kontrakter-bisys.version} + + + no.nav.fpsak.tidsserie + fpsak-tidsserie + ${fpsak-tidslinje.version} + + + no.nav.familie.eksterne.kontrakter + stonadsstatistikk + ${familie.kontrakter.stønadsstatistikk} + + + no.nav.familie.eksterne.kontrakter + saksstatistikk + ${familie.kontrakter.saksstatistikk} + + + no.nav.familie.eksterne.kontrakter + skatteetaten + ${familie.kontrakter.skatteetaten} + + + no.nav.familie.felles + familie-utbetalingsgenerator + ${utbetalingsgenerator.version} + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + io.mockk + mockk-jvm + ${mockk.version} + test + + + org.wiremock + wiremock + ${wiremock.version} + test + + + + org.springframework.retry + spring-retry + + + + com.worldturner.medeia + medeia-validator-jackson + ${medeia-validator-jackson.version} + test + + + + io.cucumber + cucumber-java + ${cucumber.version} + test + + + io.cucumber + cucumber-junit-platform-engine + ${cucumber.version} + test + + + org.junit.platform + junit-platform-suite + test + + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + ${kotlinx.version} + pom + + + + + + github + https://maven.pkg.github.com/navikt/familie-felles + + + + + + ${project.basedir}/src/main/kotlin + ${project.artifactId} + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + ktlint + verify + + + + + + + + + run + + + + ktlint-format + + + + + + + + + + + + + run + + + + + + com.pinterest + ktlint + 0.50.0 + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + familie-ba-sak + sut + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + -Xjsr305=strict + + + spring + jpa + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-maven-noarg + ${kotlin.version} + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + + ${excludedGroups} + 1 + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.4.0 + + + generate-test-sources + + add-test-source + + + + ${project.basedir}/src/test/integrasjonstester/kotlin + ${project.basedir}/src/test/enhetstester/kotlin + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.4.1 + + + enforce-banned-dependencies + + enforce + + + + + + org.junit.*:*:*:jar:compile + + + + true + + + + + + + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/run-java.sh b/jdk_17_maven/cs/rest/familie-ba-sak/run-java.sh new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/Application.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/Application.kt new file mode 100644 index 000000000..219c442ba --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/Application.kt @@ -0,0 +1,11 @@ +package no.nav.familie.ba.sak + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication + +@SpringBootApplication +class FamilieBaSakApplication + +fun main(args: Array) { + SpringApplication.run(FamilieBaSakApplication::class.java, *args) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/BaseEntitet.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/BaseEntitet.kt new file mode 100644 index 000000000..a9a80603d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/BaseEntitet.kt @@ -0,0 +1,43 @@ +package no.nav.familie.ba.sak.common + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.MappedSuperclass +import jakarta.persistence.PreUpdate +import jakarta.persistence.Version +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import java.io.Serializable +import java.time.LocalDateTime + +/** + * En basis [Entity] klasse som håndtere felles standarder for utformign av tabeller (eks. sporing av hvem som har + * opprettet eller oppdatert en rad, og når). + */ +@MappedSuperclass +abstract class BaseEntitet : Serializable { + + // The properties have to be open because when a subclass is lazy class, hibernate needs to override the accessor + // to intercept its behavior. If they are final, hibernate will complain and it also can cause potential bug. + // See: https://stackoverflow.com/questions/55958667/kotlin-inheritance-and-jpa + @Column(name = "opprettet_av", nullable = false, updatable = false) + open val opprettetAv: String = SikkerhetContext.hentSaksbehandler() + + @Column(name = "opprettet_tid", nullable = false, updatable = false) + open val opprettetTidspunkt: LocalDateTime = LocalDateTime.now() + + @Column(name = "endret_av") + open var endretAv: String = SikkerhetContext.hentSaksbehandler() + + @Column(name = "endret_tid") + open var endretTidspunkt: LocalDateTime = LocalDateTime.now() + + @Version + @Column(name = "versjon", nullable = false) + open var versjon: Long = 0 + + @PreUpdate + protected fun onUpdate() { + endretAv = SikkerhetContext.hentSaksbehandler() + endretTidspunkt = LocalDateTime.now() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/BehandlingValidering.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/BehandlingValidering.kt new file mode 100644 index 000000000..c12eb8c96 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/BehandlingValidering.kt @@ -0,0 +1,27 @@ +package no.nav.familie.ba.sak.common + +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus + +object BehandlingValidering { + + fun validerBehandlingKanRedigeres(behandling: Behandling) { + validerBehandlingKanRedigeres(behandling.status) + } + + fun validerBehandlingKanRedigeres(status: BehandlingStatus) { + feilHvis(status.erLåstForVidereRedigering()) { + "Behandlingen er låst for videre redigering ($status)" + } + } + + fun validerBehandlingIkkeErAvsluttet(behandling: Behandling) { + validerBehandlingIkkeErAvsluttet(behandling.status) + } + + fun validerBehandlingIkkeErAvsluttet(status: BehandlingStatus) { + feilHvis(status == BehandlingStatus.AVSLUTTET) { + "Behandlingen er avsluttet ($status)" + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/BigDecimalUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/BigDecimalUtils.kt new file mode 100644 index 000000000..b93b9be9b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/BigDecimalUtils.kt @@ -0,0 +1,13 @@ +package no.nav.familie.ba.sak.common + +import java.math.BigDecimal +import java.math.MathContext +import java.math.RoundingMode + +object BigDecimalKonstanter { + val AVRUND_OPP: RoundingMode = RoundingMode.HALF_UP +} + +fun BigDecimal.del(divident: BigDecimal, scale: Int) = this.divide(divident, scale, BigDecimalKonstanter.AVRUND_OPP) + +fun BigDecimal.multipliser(multiplikator: BigDecimal, precision: Int) = this.multiply(multiplikator, MathContext(precision, BigDecimalKonstanter.AVRUND_OPP)) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/CollectionUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/CollectionUtils.kt new file mode 100644 index 000000000..59c3c94b6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/CollectionUtils.kt @@ -0,0 +1,8 @@ +package no.nav.familie.ba.sak.common + +inline fun Collection.zeroSingleOrThrow(exception: Collection.() -> Exception): T? = + if (size in 0..1) { + singleOrNull() + } else { + throw exception() + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/DatoIntervallEntitet.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/DatoIntervallEntitet.kt new file mode 100644 index 000000000..c6a600e24 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/DatoIntervallEntitet.kt @@ -0,0 +1,14 @@ +package no.nav.familie.ba.sak.common + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable +import java.time.LocalDate + +@Embeddable +data class DatoIntervallEntitet( + @Column(name = "fom") + val fom: LocalDate? = null, + + @Column(name = "tom") + val tom: LocalDate? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/EksternTjenesteKaller.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/EksternTjenesteKaller.kt new file mode 100644 index 000000000..8af50cfb9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/EksternTjenesteKaller.kt @@ -0,0 +1,136 @@ +package no.nav.familie.ba.sak.common + +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonException +import no.nav.familie.http.client.RessursException +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.getDataOrThrow +import org.slf4j.LoggerFactory +import org.springframework.web.client.HttpClientErrorException +import java.net.URI + +val eksternTjenesteKallerLogger = LoggerFactory.getLogger("eksternTjenesteKaller") +inline fun kallEksternTjeneste( + tjeneste: String, + uri: URI, + formål: String, + eksterntKall: () -> Data, +): Data { + loggEksternKall(tjeneste, uri, formål) + + return try { + val startTid = System.currentTimeMillis() + val data = eksterntKall() + val sluttTid = System.currentTimeMillis() + + eksternTjenesteKallerLogger.info( + "${lagEksternKallPreMelding(tjeneste, uri)} Kall ok. Dette tok ${sluttTid - startTid} ms.", + ) + + data + } catch (exception: Exception) { + throw handleException(exception = exception, tjeneste = tjeneste, uri = uri, formål = formål) + } +} + +inline fun kallEksternTjenesteRessurs( + tjeneste: String, + uri: URI, + formål: String, + eksterntKall: () -> Ressurs, +): Data { + loggEksternKall(tjeneste, uri, formål) + + return try { + eksterntKall().getDataOrThrow().also { + eksternTjenesteKallerLogger.info("${lagEksternKallPreMelding(tjeneste, uri)} Kall ok") + } + } catch (exception: Exception) { + throw handleException(exception = exception, tjeneste = tjeneste, uri = uri, formål = formål) + } +} + +inline fun kallEksternTjenesteUtenRespons( + tjeneste: String, + uri: URI, + formål: String, + eksterntKall: () -> Ressurs, +) { + loggEksternKall(tjeneste, uri, formål) + + try { + eksterntKall().also { + eksternTjenesteKallerLogger.info("${lagEksternKallPreMelding(tjeneste, uri)} Kall ok") + } + } catch (exception: Exception) { + throw handleException(exception = exception, tjeneste = tjeneste, uri = uri, formål = formål) + } +} + +fun lagEksternKallPreMelding( + tjeneste: String, + uri: URI, +) = "[tjeneste=$tjeneste, uri=$uri]" + +fun loggEksternKall( + tjeneste: String, + uri: URI, + formål: String, +) { + eksternTjenesteKallerLogger.info("${lagEksternKallPreMelding(tjeneste, uri)} $formål") +} + +fun handleException( + exception: Exception, + tjeneste: String, + uri: URI, + formål: String, +): Exception { + return when (exception) { + is RessursException -> { + secureLogger.info( + "${ + lagEksternKallPreMelding( + tjeneste, + uri, + ) + } Kall mot $tjeneste feilet. Formål: $formål. Feilmelding: ${exception.ressurs.melding}", + exception.cause, + ) + eksternTjenesteKallerLogger.warn( + "${ + lagEksternKallPreMelding( + tjeneste, + uri, + ) + } Kall mot $tjeneste feilet. Formål: $formål.", + ) + exception + } + + is HttpClientErrorException -> exception + else -> opprettIntegrasjonsException(tjeneste, uri, exception, formål) + } +} + +private fun opprettIntegrasjonsException( + tjeneste: String, + uri: URI, + exception: Exception, + formål: String, +): IntegrasjonException { + val melding = if (exception is RessursException) { + exception.ressurs.melding + } else { + exception.message + } + return IntegrasjonException( + msg = "${ + lagEksternKallPreMelding( + tjeneste, + uri, + ) + } Kall mot \"$tjeneste\" feilet. Formål: $formål. Feilmelding: $melding", + uri = uri, + throwable = exception, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/EnvService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/EnvService.kt new file mode 100644 index 000000000..1c8ef677b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/EnvService.kt @@ -0,0 +1,16 @@ +package no.nav.familie.ba.sak.common + +import no.nav.familie.ba.sak.config.featureToggle.miljø.Profil +import no.nav.familie.ba.sak.config.featureToggle.miljø.erAktiv +import org.springframework.core.env.Environment +import org.springframework.stereotype.Service + +@Service +class EnvService(private val environment: Environment) { + + fun erProd() = environment.erAktiv(Profil.Prod) + + fun erPreprod() = environment.erAktiv(Profil.Preprod) + + fun erDev() = environment.erAktiv(Profil.Dev) || environment.erAktiv(Profil.Postgres) || environment.erAktiv(Profil.DevPostgresPreprod) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/Feil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/Feil.kt new file mode 100644 index 000000000..050d40c14 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/Feil.kt @@ -0,0 +1,99 @@ +package no.nav.familie.ba.sak.common + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonPropertyOrder +import org.springframework.http.HttpStatus +import java.time.LocalDateTime +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +open class Feil( + message: String, + open val frontendFeilmelding: String? = null, + open val httpStatus: HttpStatus = HttpStatus.OK, + open val throwable: Throwable? = null, + override val cause: Throwable? = throwable, + +) : RuntimeException(message) + +open class FunksjonellFeil( + open val melding: String, + open val frontendFeilmelding: String? = melding, + open val httpStatus: HttpStatus = HttpStatus.OK, + open val throwable: Throwable? = null, + override val cause: Throwable? = throwable, +) : RuntimeException(melding) + +class UtbetalingsikkerhetFeil( + melding: String, + override val frontendFeilmelding: String? = null, + override val httpStatus: HttpStatus = HttpStatus.OK, + override val throwable: Throwable? = null, + override val cause: Throwable? = throwable, +) : FunksjonellFeil( + melding, + frontendFeilmelding, + httpStatus, + throwable, +) + +class RolleTilgangskontrollFeil( + melding: String, + override val frontendFeilmelding: String = melding, + override val httpStatus: HttpStatus = HttpStatus.OK, + override val throwable: Throwable? = null, + override val cause: Throwable? = throwable, +) : FunksjonellFeil( + melding, + frontendFeilmelding, + httpStatus, + throwable, +) + +class PdlRequestException(message: String) : Feil(message) +class PdlNotFoundException : FunksjonellFeil("Fant ikke person") +class PdlPersonKanIkkeBehandlesIFagsystem(val årsak: String) : + FunksjonellFeil("Person kan ikke behandles i fagsystem: $årsak") + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonPropertyOrder(value = ["melding", "path", "timestamp", "status", "exception", "stackTrace"]) +data class EksternTjenesteFeil( + val path: String, + val status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR, + var exception: String? = null, + val timestamp: LocalDateTime = LocalDateTime.now(), + var stackTrace: String? = null, +) { + lateinit var melding: String +} + +open class EksternTjenesteFeilException( + val eksternTjenesteFeil: EksternTjenesteFeil, + val melding: String, + val request: Any?, + val throwable: Throwable? = null, +) : RuntimeException(melding, throwable) { + + init { + eksternTjenesteFeil.melding = melding + } + + override fun toString(): String { + return """EksternTjenesteFeil( + | melding='$melding' + | eksternTjeneste=$eksternTjenesteFeil + | request=$request + | throwable=$throwable) + """.trimMargin() + } +} + +@OptIn(ExperimentalContracts::class) +inline fun feilHvis(boolean: Boolean, httpStatus: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR, lazyMessage: () -> String) { + contract { + returns() implies !boolean + } + if (boolean) { + throw Feil(message = lazyMessage(), frontendFeilmelding = lazyMessage(), httpStatus) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/Feilmeldinger.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/Feilmeldinger.kt new file mode 100644 index 000000000..bb1f0a0aa --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/Feilmeldinger.kt @@ -0,0 +1,3 @@ +package no.nav.familie.ba.sak.common + +const val KONTAKT_TEAMET_SUFFIX = "Kontakt teamet for hjelp." diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/LocalDateService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/LocalDateService.kt new file mode 100644 index 000000000..32644a688 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/LocalDateService.kt @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.common + +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class LocalDateService { + fun now(): LocalDate = LocalDate.now() +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/MDCOperations.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/MDCOperations.kt new file mode 100644 index 000000000..b1719d22c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/MDCOperations.kt @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.common + +import no.nav.familie.log.mdc.MDCConstants.MDC_CALL_ID +import org.slf4j.MDC + +object MDCOperations { + + fun getCallId(): String = MDC.get(MDC_CALL_ID) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/RessursUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/RessursUtils.kt new file mode 100644 index 000000000..7e256a9e0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/RessursUtils.kt @@ -0,0 +1,101 @@ +package no.nav.familie.ba.sak.common + +import io.sentry.Sentry +import no.nav.familie.kontrakter.felles.Ressurs +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity + +object RessursUtils { + + private val logger = LoggerFactory.getLogger(RessursUtils::class.java) + + fun unauthorized(errorMessage: String): ResponseEntity> = + ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Ressurs.failure(errorMessage)) + + fun badRequest(errorMessage: String, throwable: Throwable): ResponseEntity> = + errorResponse(HttpStatus.BAD_REQUEST, errorMessage, throwable) + + fun forbidden(errorMessage: String): ResponseEntity> = + ikkeTilgangResponse(errorMessage) + + fun illegalState(errorMessage: String, throwable: Throwable): ResponseEntity> = + errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, errorMessage, throwable) + + fun funksjonellFeil(funksjonellFeil: FunksjonellFeil): ResponseEntity> = funksjonellErrorResponse( + funksjonellFeil, + ) + + fun frontendFeil(feil: Feil, throwable: Throwable?): ResponseEntity> = + frontendErrorResponse(feil, throwable) + + fun ok(data: T): ResponseEntity> = ResponseEntity.ok(Ressurs.success(data)) + + fun rolleTilgangResponse(rolleTilgangskontrollFeil: RolleTilgangskontrollFeil): ResponseEntity> { + secureLogger.warn( + "En håndtert tilgangsfeil har oppstått - ${rolleTilgangskontrollFeil.frontendFeilmelding}", + rolleTilgangskontrollFeil, + ) + logger.warn("En håndtert tilgangsfeil har oppstått - ${rolleTilgangskontrollFeil.melding}") + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body( + Ressurs.ikkeTilgang(rolleTilgangskontrollFeil.melding) + .copy(frontendFeilmelding = rolleTilgangskontrollFeil.frontendFeilmelding.ifBlank { "Mangler tilgang" }), + ) + } + + private fun errorResponse( + httpStatus: HttpStatus, + errorMessage: String, + throwable: Throwable, + ): ResponseEntity> { + val className = "[${throwable::class.java.name}] " + + secureLogger.warn("$className En feil har oppstått: $errorMessage", throwable) + logger.warn("$className En feil har oppstått. Se securelogs for detaljer.") + + Sentry.captureException(throwable) + return ResponseEntity.status(httpStatus).body(Ressurs.failure(errorMessage)) + } + + private fun ikkeTilgangResponse( + errorMessage: String, + ): ResponseEntity> { + secureLogger.warn("Saksbehandler har ikke tilgang: $errorMessage") + logger.warn("Saksbehandler har ikke tilgang. Se securelogs for detaljer.") + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Ressurs.ikkeTilgang(errorMessage)) + } + + private fun frontendErrorResponse(feil: Feil, throwable: Throwable?): ResponseEntity> { + val className = if (throwable != null) "[${throwable::class.java.name}] " else "" + + secureLogger.info( + "$className En håndtert feil har oppstått(${feil.httpStatus}): " + + "${feil.message}, ${feil.frontendFeilmelding}", + feil, + ) + logger.warn("$className En håndtert feil har oppstått(${feil.httpStatus}): ${feil.message} ", feil) + + Sentry.captureException(feil) + return ResponseEntity.status(feil.httpStatus).body( + Ressurs.failure( + frontendFeilmelding = feil.frontendFeilmelding, + errorMessage = feil.message.toString(), + ), + ) + } + + private fun funksjonellErrorResponse(funksjonellFeil: FunksjonellFeil): ResponseEntity> { + val className = + if (funksjonellFeil.throwable != null) "[${funksjonellFeil.throwable!!::class.java.name}] " else "" + + logger.info("$className En funksjonell feil har oppstått(${funksjonellFeil.httpStatus}): ${funksjonellFeil.message} ") + + return ResponseEntity.status(funksjonellFeil.httpStatus).body( + Ressurs.funksjonellFeil( + frontendFeilmelding = funksjonellFeil.frontendFeilmelding, + melding = funksjonellFeil.melding, + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/StringListConverter.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/StringListConverter.kt new file mode 100644 index 000000000..749425300 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/StringListConverter.kt @@ -0,0 +1,21 @@ +package no.nav.familie.ba.sak.common + +import jakarta.persistence.AttributeConverter +import jakarta.persistence.Converter + +@Converter +class StringListConverter : AttributeConverter, String> { + + override fun convertToDatabaseColumn(stringList: List): String { + return java.lang.String.join(SPLIT_CHAR, stringList) + } + + override fun convertToEntityAttribute(string: String?): List { + return if (string.isNullOrBlank()) emptyList() else string.split(SPLIT_CHAR) + } + + companion object { + + private const val SPLIT_CHAR = ";" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/Tid.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/Tid.kt new file mode 100644 index 000000000..d0196cded --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/Tid.kt @@ -0,0 +1,292 @@ +package no.nav.familie.ba.sak.common + +import no.nav.familie.ba.sak.ekstern.restDomene.RestVilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import java.time.LocalDate +import java.time.LocalDate.now +import java.time.YearMonth +import java.time.format.DateTimeFormatter +import java.util.NavigableMap +import java.util.TreeMap + +val TIDENES_MORGEN = LocalDate.MIN +val TIDENES_ENDE = LocalDate.MAX + +private val FORMAT_DATE_DDMMYY = DateTimeFormatter.ofPattern("ddMMyy", nbLocale) +private val FORMAT_DATE_ISO = DateTimeFormatter.ofPattern("yyyy-MM-dd", nbLocale) +private val FORMAT_DATO_NORSK_KORT_ÅR = DateTimeFormatter.ofPattern("dd.MM.yy", nbLocale) +private val FORMAT_DATO_NORSK = DateTimeFormatter.ofPattern("dd.MM.yyyy", nbLocale) +private val FORMAT_DATO_MÅNED_ÅR_KORT = DateTimeFormatter.ofPattern("MM.yy", nbLocale) +private val FORMAT_DATO_DAG_MÅNED_ÅR = DateTimeFormatter.ofPattern("d. MMMM yyyy", nbLocale) +private val FORMAT_DATO_MÅNED_ÅR = DateTimeFormatter.ofPattern("MMMM yyyy", nbLocale) + +fun LocalDate.tilddMMyy() = this.format(FORMAT_DATE_DDMMYY) +fun LocalDate.tilyyyyMMdd() = this.format(FORMAT_DATE_ISO) +fun LocalDate.tilKortString() = this.format(FORMAT_DATO_NORSK_KORT_ÅR) +fun LocalDate.tilddMMyyyy() = this.format(FORMAT_DATO_NORSK) +fun YearMonth.tilKortString() = this.format(FORMAT_DATO_MÅNED_ÅR_KORT) +fun LocalDate.tilDagMånedÅr() = this.format(FORMAT_DATO_DAG_MÅNED_ÅR) +fun LocalDate.tilMånedÅr() = this.format(FORMAT_DATO_MÅNED_ÅR) +fun YearMonth.tilMånedÅr() = this.format(FORMAT_DATO_MÅNED_ÅR) + +fun erBack2BackIMånedsskifte(tilOgMed: LocalDate?, fraOgMed: LocalDate?): Boolean { + return tilOgMed?.erDagenFør(fraOgMed) == true && + tilOgMed.toYearMonth() != fraOgMed?.toYearMonth() +} + +fun LocalDate.sisteDagIForrigeMåned(): LocalDate { + val sammeDagForrigeMåned = this.minusMonths(1) + return sammeDagForrigeMåned.sisteDagIMåned() +} + +fun LocalDate.toYearMonth() = YearMonth.from(this) +fun YearMonth.toLocalDate() = LocalDate.of(this.year, this.month, 1) + +fun YearMonth.førsteDagIInneværendeMåned() = this.atDay(1) +fun YearMonth.sisteDagIInneværendeMåned() = this.atEndOfMonth() + +fun LocalDate.forrigeMåned(): YearMonth { + return this.toYearMonth().minusMonths(1) +} + +fun YearMonth.forrigeMåned(): YearMonth { + return this.minusMonths(1) +} + +fun LocalDate.nesteMåned(): YearMonth { + return this.toYearMonth().plusMonths(1) +} + +fun YearMonth.nesteMåned(): YearMonth { + return this.plusMonths(1) +} + +fun inneværendeMåned(): YearMonth { + return now().toYearMonth() +} + +fun senesteDatoAv(dato1: LocalDate, dato2: LocalDate): LocalDate { + return maxOf(dato1, dato2) +} + +fun LocalDate.til18ÅrsVilkårsdato() = this.plusYears(18).minusDays(1) + +fun LocalDate.sisteDagIMåned(): LocalDate { + return YearMonth.from(this).atEndOfMonth() +} + +fun LocalDate.førsteDagINesteMåned() = this.plusMonths(1).withDayOfMonth(1) +fun LocalDate.førsteDagIInneværendeMåned() = this.withDayOfMonth(1) + +fun LocalDate.erSenereEnnInneværendeMåned(): Boolean = this.isAfter(now().sisteDagIMåned()) + +fun LocalDate.erDagenFør(other: LocalDate?) = other != null && this.plusDays(1).equals(other) + +fun LocalDate.erFraInneværendeMåned(now: LocalDate = now()): Boolean { + val førsteDatoInneværendeMåned = now.withDayOfMonth(1) + val førsteDatoNesteMåned = førsteDatoInneværendeMåned.plusMonths(1) + return this.isSameOrAfter(førsteDatoInneværendeMåned) && isBefore(førsteDatoNesteMåned) +} + +fun LocalDate.erFraInneværendeEllerForrigeMåned(now: LocalDate = now()): Boolean { + val førsteDatoForrigeMåned = now.withDayOfMonth(1).minusMonths(1) + val førsteDatoNesteMåned = førsteDatoForrigeMåned.plusMonths(2) + return this.isSameOrAfter(førsteDatoForrigeMåned) && isBefore(førsteDatoNesteMåned) +} + +fun YearMonth.isSameOrBefore(toCompare: YearMonth): Boolean { + return this.isBefore(toCompare) || this == toCompare +} + +fun YearMonth.isSameOrAfter(toCompare: YearMonth): Boolean { + return this.isAfter(toCompare) || this == toCompare +} + +fun LocalDate.isSameOrBefore(toCompare: LocalDate): Boolean { + return this.isBefore(toCompare) || this == toCompare +} + +fun LocalDate.isSameOrAfter(toCompare: LocalDate): Boolean { + return this.isAfter(toCompare) || this == toCompare +} + +fun LocalDate.isBetween(toCompare: Periode): Boolean { + return this.isSameOrAfter(toCompare.fom) && this.isSameOrBefore(toCompare.tom) +} + +fun Periode.overlapperHeltEllerDelvisMed(annenPeriode: Periode) = + this.fom.isBetween(annenPeriode) || + this.tom.isBetween(annenPeriode) || + annenPeriode.fom.isBetween(this) || + annenPeriode.tom.isBetween(this) + +fun MånedPeriode.inkluderer(yearMonth: YearMonth) = yearMonth >= this.fom && yearMonth <= this.tom + +fun MånedPeriode.overlapperHeltEllerDelvisMed(annenPeriode: MånedPeriode) = + this.inkluderer(annenPeriode.fom) || + this.inkluderer(annenPeriode.tom) || + annenPeriode.inkluderer(this.fom) || + annenPeriode.inkluderer(this.tom) + +fun MånedPeriode.erMellom(annenPeriode: MånedPeriode) = + annenPeriode.inkluderer(this.fom) && annenPeriode.inkluderer(this.tom) + +fun Periode.kanErstatte(other: Periode): Boolean { + return this.fom.isSameOrBefore(other.fom) && this.tom.isSameOrAfter(other.tom) +} + +fun LocalDate.erMellomIkkeLik(other: Periode): Boolean { + return this.isAfter(other.fom) && this.isBefore(other.tom) +} + +fun Periode.kanSplitte(other: Periode): Boolean { + return this.fom.erMellomIkkeLik(other) && this.tom.erMellomIkkeLik(other) && + (this.tom != TIDENES_ENDE || other.tom != TIDENES_ENDE) +} + +fun Periode.kanFlytteFom(other: Periode): Boolean { + return this.fom.isSameOrBefore(other.fom) && this.tom.isBetween(other) +} + +fun Periode.kanFlytteTom(other: Periode): Boolean { + return this.fom.isBetween(other) && this.tom.isSameOrAfter(other.tom) +} + +fun Periode.tilMånedPeriode(): MånedPeriode = MånedPeriode(fom = this.fom.toYearMonth(), tom = this.tom.toYearMonth()) + +data class Periode(val fom: LocalDate, val tom: LocalDate) + +data class MånedPeriode(val fom: YearMonth, val tom: YearMonth) +data class NullablePeriode(val fom: LocalDate?, val tom: LocalDate?) { + fun tilNullableMånedPeriode() = NullableMånedPeriode(fom?.toYearMonth(), tom?.toYearMonth()) +} + +data class NullableMånedPeriode(val fom: YearMonth?, val tom: YearMonth?) + +fun VilkårResultat.erEtterfølgendePeriode(other: VilkårResultat): Boolean { + return (other.toPeriode().fom.monthValue - this.toPeriode().tom.monthValue <= 1) && + this.toPeriode().tom.year == other.toPeriode().fom.year +} + +fun lagOgValiderPeriodeFraVilkår( + periodeFom: LocalDate?, + periodeTom: LocalDate?, + erEksplisittAvslagPåSøknad: Boolean? = null, +): Periode { + return when { + periodeFom !== null -> { + Periode( + fom = periodeFom, + tom = periodeTom ?: TIDENES_ENDE, + ) + } + + erEksplisittAvslagPåSøknad == true && periodeTom == null -> { + Periode( + fom = TIDENES_MORGEN, + tom = TIDENES_ENDE, + ) + } + + else -> { + throw FunksjonellFeil("Ugyldig periode. Periode må ha t.o.m.-dato eller være et avslag uten datoer.") + } + } +} + +fun RestVilkårResultat.toPeriode(): Periode = lagOgValiderPeriodeFraVilkår( + this.periodeFom, + this.periodeTom, + this.erEksplisittAvslagPåSøknad, +) + +fun VilkårResultat.toPeriode(): Periode = lagOgValiderPeriodeFraVilkår( + this.periodeFom, + this.periodeTom, + this.erEksplisittAvslagPåSøknad, +) + +fun DatoIntervallEntitet.erInnenfor(dato: LocalDate): Boolean { + return when { + fom == null && tom == null -> true + fom == null -> dato.isSameOrBefore(tom!!) + tom == null -> dato.isSameOrAfter(fom) + else -> dato.isSameOrAfter(fom) && dato.isSameOrBefore(tom) + } +} + +fun slåSammenOverlappendePerioder(input: Collection): List { + val map: NavigableMap = + TreeMap() + for (periode in input) { + if (periode.fom != null && + (!map.containsKey(periode.fom) || periode.tom == null || periode.tom.isAfter(map[periode.fom])) + ) { + map[periode.fom] = periode.tom + } + } + val result = mutableListOf() + var prevIntervall: DatoIntervallEntitet? = null + for ((key, value) in map) { + prevIntervall = if (prevIntervall != null && prevIntervall.erInnenfor(key)) { + val fom = prevIntervall.fom + val tom = if (prevIntervall.tom == null) { + null + } else { + if (value != null && prevIntervall.tom!!.isAfter(value)) { + prevIntervall.tom + } else { + value + } + } + result.remove(prevIntervall) + val nyttIntervall = DatoIntervallEntitet(fom, tom) + result.add(nyttIntervall) + nyttIntervall + } else { + val nyttIntervall = DatoIntervallEntitet(key, value) + result.add(nyttIntervall) + nyttIntervall + } + } + return result +} + +class YearMonthIterator( + startMåned: YearMonth, + val tilOgMedMåned: YearMonth, + val hoppMåneder: Long, +) : Iterator { + + private var gjeldendeMåned = startMåned + + override fun hasNext() = + if (hoppMåneder > 0) { + gjeldendeMåned.plusMonths(hoppMåneder) <= tilOgMedMåned.plusMonths(1) + } else if (hoppMåneder < 0) { + gjeldendeMåned.plusMonths(hoppMåneder) >= tilOgMedMåned.plusMonths(-1) + } else { + throw IllegalStateException("Steglengde kan ikke være null") + } + + override fun next(): YearMonth { + val next = gjeldendeMåned + gjeldendeMåned = gjeldendeMåned.plusMonths(hoppMåneder) + return next + } +} + +class YearMonthProgression( + override val start: YearMonth, + override val endInclusive: YearMonth, + val hoppMåneder: Long = 1, +) : Iterable, + ClosedRange { + + override fun iterator(): Iterator = + YearMonthIterator(start, endInclusive, hoppMåneder) + + infix fun step(måneder: Long) = YearMonthProgression(start, endInclusive, måneder) +} + +operator fun YearMonth.rangeTo(andre: YearMonth) = YearMonthProgression(this, andre) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/TidslinjeUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/TidslinjeUtil.kt new file mode 100644 index 000000000..3b64e8ffb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/TidslinjeUtil.kt @@ -0,0 +1,52 @@ +package no.nav.familie.ba.sak.common + +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.forrige +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.neste +import java.time.LocalDate +import java.time.YearMonth + +fun erUnder18ÅrVilkårTidslinje(fødselsdato: LocalDate): Tidslinje = tidslinje { + listOf( + Periode( + fødselsdato.toYearMonth().tilTidspunkt().neste(), + fødselsdato.plusYears(18).toYearMonth().tilTidspunkt().forrige(), + true, + ), + ) +} + +fun erUnder6ÅrTidslinje(person: Person) = tidslinje { + listOf( + Periode( + person.fødselsdato.toYearMonth().tilTidspunkt(), + person.fødselsdato.toYearMonth().plusYears(6).tilTidspunkt().forrige(), + true, + ), + ) +} + +fun erTilogMed3ÅrTidslinje(fødselsdato: LocalDate): Tidslinje = tidslinje { + listOf( + Periode( + fødselsdato.toYearMonth().tilTidspunkt().neste(), + fødselsdato.plusYears(3).toYearMonth().tilTidspunkt(), + true, + ), + ) +} + +fun opprettBooleanTidslinje(fraÅrMåned: YearMonth, tilÅrMåned: YearMonth) = tidslinje { + listOf( + Periode( + fraÅrMåned.tilTidspunkt(), + tilÅrMåned.tilTidspunkt(), + true, + ), + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/Utils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/Utils.kt new file mode 100644 index 000000000..9374bcb47 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/Utils.kt @@ -0,0 +1,68 @@ +package no.nav.familie.ba.sak.common + +import no.nav.familie.kontrakter.felles.objectMapper +import org.apache.maven.model.Model +import org.apache.maven.model.io.xpp3.MavenXpp3Reader +import org.slf4j.LoggerFactory +import org.springframework.core.io.ClassPathResource +import java.io.File +import java.io.FileReader +import java.io.InputStreamReader +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Locale +import java.util.Properties + +val nbLocale = Locale("nb", "Norway") + +val secureLogger = LoggerFactory.getLogger("secureLogger") + +object Utils { + + fun slåSammen(values: List): String = Regex("(.*),").replace(values.joinToString(", "), "$1 og") + + fun formaterBeløp(beløp: Int): String = NumberFormat.getNumberInstance(nbLocale).format(beløp) + + val properties: Properties by lazy { + val reader = MavenXpp3Reader() + val model: Model = if (File("pom.xml").exists()) { + reader.read(FileReader("pom.xml")) + } else { + reader.read( + InputStreamReader( + ClassPathResource( + "META-INF/maven/no.nav.familie.ba.sak/familie-ba-sak/pom.xml", + ).inputStream, + ), + ) + } + model.properties + } + + fun hentPropertyFraMaven(key: String): String? = this.properties[key]?.toString() + + fun BigDecimal.avrundetHeltallAvProsent(prosent: BigDecimal) = this.times(prosent) + .divide(100.toBigDecimal()).setScale(0, RoundingMode.HALF_UP) + .toInt() + + fun Int.avrundetHeltallAvProsent(prosent: BigDecimal) = this.toBigDecimal().avrundetHeltallAvProsent(prosent) + + fun String.storForbokstav() = this.lowercase().replaceFirstChar { it.uppercase() } + fun String.storForbokstavIHvertOrd() = this.split(" ").joinToString(" ") { it.storForbokstav() }.trimEnd() + fun String.storForbokstavIAlleNavn() = this.split(" ") + .joinToString(" ") { navn -> + navn.split("-").joinToString("-") { it.storForbokstav() } + }.trimEnd() + + fun Any?.nullableTilString() = this?.toString() ?: "" + + inline fun > konverterEnumsTilString(liste: List) = liste.joinToString(separator = ";") + + inline fun > konverterStringTilEnums(string: String?): List = + if (string.isNullOrBlank()) emptyList() else string.split(";").map { enumValueOf(it) } +} + +fun Any.convertDataClassToJson(): String { + return objectMapper.writeValueAsString(this) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/YearMonthConverter.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/YearMonthConverter.kt new file mode 100644 index 000000000..d1cfdf18e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/YearMonthConverter.kt @@ -0,0 +1,18 @@ +package no.nav.familie.ba.sak.common + +import jakarta.persistence.AttributeConverter +import java.sql.Date +import java.time.YearMonth + +class YearMonthConverter : AttributeConverter { + + override fun convertToDatabaseColumn(yearMonth: YearMonth?): Date? { + return yearMonth?.let { + Date.valueOf(it.toLocalDate()) + } + } + + override fun convertToEntityAttribute(date: Date?): YearMonth? { + return date?.toLocalDate()?.toYearMonth() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/http/interceptor/RolletilgangInterceptor.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/http/interceptor/RolletilgangInterceptor.kt new file mode 100644 index 000000000..c2793cd56 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/common/http/interceptor/RolletilgangInterceptor.kt @@ -0,0 +1,31 @@ +package no.nav.familie.ba.sak.common.http.interceptor + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import no.nav.familie.ba.sak.config.RolleConfig +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.Import +import org.springframework.stereotype.Component +import org.springframework.web.servlet.HandlerInterceptor + +@Component +@Import(RolleConfig::class) +class RolletilgangInterceptor(private val rolleConfig: RolleConfig) : HandlerInterceptor { + + override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean = + SikkerhetContext.hentRolletilgangFraSikkerhetscontext(rolleConfig, BehandlerRolle.VEILEDER) + .takeIf { it != BehandlerRolle.UKJENT } + ?.let { super.preHandle(request, response, handler) } + ?: run { + logger.info("Bruker ${SikkerhetContext.hentSaksbehandler()} har ikke tilgang.") + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Bruker har ikke tilgang") + false + } + + companion object { + + private val logger = LoggerFactory.getLogger(RolletilgangInterceptor::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/ApiExceptionHandler.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/ApiExceptionHandler.kt new file mode 100644 index 000000000..26c63d0ae --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/ApiExceptionHandler.kt @@ -0,0 +1,135 @@ +package no.nav.familie.ba.sak.config + +import no.nav.familie.ba.sak.common.EksternTjenesteFeil +import no.nav.familie.ba.sak.common.EksternTjenesteFeilException +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.PdlNotFoundException +import no.nav.familie.ba.sak.common.PdlPersonKanIkkeBehandlesIFagsystem +import no.nav.familie.ba.sak.common.RessursUtils.forbidden +import no.nav.familie.ba.sak.common.RessursUtils.frontendFeil +import no.nav.familie.ba.sak.common.RessursUtils.funksjonellFeil +import no.nav.familie.ba.sak.common.RessursUtils.illegalState +import no.nav.familie.ba.sak.common.RessursUtils.rolleTilgangResponse +import no.nav.familie.ba.sak.common.RessursUtils.unauthorized +import no.nav.familie.ba.sak.common.RolleTilgangskontrollFeil +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.ecb.ECBServiceException +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonException +import no.nav.familie.http.client.RessursException +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.spring.validation.interceptor.JwtTokenUnauthorizedException +import org.slf4j.LoggerFactory +import org.springframework.core.NestedExceptionUtils +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.client.HttpClientErrorException +import java.io.PrintWriter +import java.io.StringWriter + +@ControllerAdvice +class ApiExceptionHandler { + + private val logger = LoggerFactory.getLogger(ApiExceptionHandler::class.java) + + @ExceptionHandler(JwtTokenUnauthorizedException::class) + fun handleThrowable(jwtTokenUnauthorizedException: JwtTokenUnauthorizedException): ResponseEntity> { + return unauthorized("Unauthorized") + } + + @ExceptionHandler(RolleTilgangskontrollFeil::class) + fun handleRolleTilgangskontrollFeil(rolleTilgangskontrollFeil: RolleTilgangskontrollFeil): ResponseEntity> { + return rolleTilgangResponse(rolleTilgangskontrollFeil) + } + + @ExceptionHandler(Exception::class) + fun handleException(exception: Exception): ResponseEntity> { + val mostSpecificCause = NestedExceptionUtils.getMostSpecificCause(exception) + + return illegalState(mostSpecificCause.message.toString(), mostSpecificCause) + } + + @ExceptionHandler(RessursException::class) + fun handleRessursException(ressursException: RessursException): ResponseEntity> { + return ResponseEntity.status(ressursException.httpStatus).body(ressursException.ressurs) + } + + @ExceptionHandler(HttpClientErrorException.Forbidden::class) + fun handleForbidden(foriddenException: HttpClientErrorException.Forbidden): ResponseEntity> { + val mostSpecificCause = NestedExceptionUtils.getMostSpecificCause(foriddenException) + + return forbidden(mostSpecificCause.message ?: "Ikke tilgang") + } + + @ExceptionHandler(IntegrasjonException::class) + fun handleIntegrasjonException(integrasjonException: IntegrasjonException): ResponseEntity> { + return illegalState(integrasjonException.message.toString(), integrasjonException) + } + + @ExceptionHandler(PdlPersonKanIkkeBehandlesIFagsystem::class) + fun handlePdlPersonKanIkkeBehandlesIFagsystem(feil: PdlPersonKanIkkeBehandlesIFagsystem): ResponseEntity> { + logger.warn("Person kan ikke behandles i fagsystem ${feil.årsak}") + secureLogger.warn("Person kan ikke behandles i fagsystem", feil) + return funksjonellFeil(feil) + } + + @ExceptionHandler(PdlNotFoundException::class) + fun handlePdlNotFoundException(feil: PdlNotFoundException): ResponseEntity> { + logger.warn("Finner ikke personen i PDL") + return ResponseEntity.ok() + .body(Ressurs.failure(frontendFeilmelding = "Fant ikke person")) + } + + @ExceptionHandler(ECBServiceException::class) + fun handleECBClientException(feil: ECBServiceException): ResponseEntity> { + logger.warn(feil.message) + return ResponseEntity.internalServerError() + .body(Ressurs.failure(frontendFeilmelding = feil.message)) + } + + @ExceptionHandler(Feil::class) + fun handleFeil(feil: Feil): ResponseEntity> { + val mostSpecificCause = + if (feil.throwable != null) NestedExceptionUtils.getMostSpecificCause(feil.throwable!!) else null + + return frontendFeil(feil, mostSpecificCause) + } + + @ExceptionHandler(FunksjonellFeil::class) + fun handleFunksjonellFeil(funksjonellFeil: FunksjonellFeil): ResponseEntity> { + return funksjonellFeil(funksjonellFeil) + } + + @ExceptionHandler(EksternTjenesteFeilException::class) + fun handleEksternTjenesteFeil(feil: EksternTjenesteFeilException): ResponseEntity { + val mostSpecificThrowable = + if (feil.throwable != null) NestedExceptionUtils.getMostSpecificCause(feil.throwable) else null + feil.eksternTjenesteFeil.exception = + if (mostSpecificThrowable != null) "[${mostSpecificThrowable::class.java.name}] " else null + + if (mostSpecificThrowable != null) { + val sw = StringWriter() + feil.printStackTrace(PrintWriter(sw)) + feil.eksternTjenesteFeil.stackTrace = sw.toString() + } + + secureLogger.info("$feil") + logger.info("Feil ekstern tjeneste: path:${feil.eksternTjenesteFeil.path} status:${feil.eksternTjenesteFeil.status} exception:${feil.eksternTjenesteFeil.exception}") + + return ResponseEntity.status(feil.eksternTjenesteFeil.status).body(feil.eksternTjenesteFeil) + } + + @ExceptionHandler(MethodArgumentNotValidException::class) + fun handleInputValideringFeil(valideringFeil: MethodArgumentNotValidException): ResponseEntity> { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body( + Ressurs.failure( + valideringFeil.bindingResult.fieldErrors.map { fieldError -> fieldError.defaultMessage } + .joinToString(" ,"), + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/ApplicationConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/ApplicationConfig.kt new file mode 100644 index 000000000..669ba63ad --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/ApplicationConfig.kt @@ -0,0 +1,111 @@ +package no.nav.familie.ba.sak.config + +import com.fasterxml.jackson.databind.ObjectMapper +import no.nav.familie.http.client.RetryOAuth2HttpClient +import no.nav.familie.log.filter.LogFilter +import no.nav.familie.log.filter.RequestTimeFilter +import no.nav.familie.prosessering.config.ProsesseringInfoProvider +import no.nav.security.token.support.client.core.http.OAuth2HttpClient +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse +import no.nav.security.token.support.client.spring.oauth2.EnableOAuth2Client +import no.nav.security.token.support.spring.SpringTokenValidationContextHolder +import no.nav.security.token.support.spring.api.EnableJwtTokenValidation +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.context.properties.ConfigurationPropertiesScan +import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.boot.web.servlet.FilterRegistrationBean +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Primary +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter +import org.springframework.retry.annotation.EnableRetry +import org.springframework.web.client.RestTemplate +import java.time.Duration +import java.time.temporal.ChronoUnit + +@SpringBootConfiguration +@EntityScan("no.nav.familie.prosessering", ApplicationConfig.PAKKENAVN) +@ComponentScan("no.nav.familie.prosessering", "no.nav.familie.unleash", ApplicationConfig.PAKKENAVN) +@EnableRetry +@ConfigurationPropertiesScan +@EnableJwtTokenValidation(ignore = ["org.springdoc"]) +@EnableOAuth2Client(cacheEnabled = true) +class ApplicationConfig { + + @Bean + fun logFilter(): FilterRegistrationBean { + log.info("Registering LogFilter filter") + val filterRegistration: FilterRegistrationBean = FilterRegistrationBean() + filterRegistration.filter = LogFilter() + filterRegistration.order = 1 + return filterRegistration + } + + @Bean + fun requestTimeFilter(): FilterRegistrationBean { + log.info("Registering RequestTimeFilter") + val filterRegistration = FilterRegistrationBean() + filterRegistration.filter = RequestTimeFilter() + filterRegistration.order = 2 + return filterRegistration + } + + /** + * Overskriver felles sin som bruker proxy, som ikke skal brukes på gcp. + */ + @Bean + @Primary + fun restTemplateBuilder(objectMapper: ObjectMapper): RestTemplateBuilder { + val jackson2HttpMessageConverter = MappingJackson2HttpMessageConverter(objectMapper) + return RestTemplateBuilder() + .setConnectTimeout(Duration.of(2, ChronoUnit.SECONDS)) + .setReadTimeout(Duration.of(30, ChronoUnit.SECONDS)) + .additionalMessageConverters(listOf(jackson2HttpMessageConverter) + RestTemplate().messageConverters) + } + + /** + * Overskriver OAuth2HttpClient som settes opp i token-support som ikke kan få med objectMapper fra felles + * pga. .setVisibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.NONE) + * og [OAuth2AccessTokenResponse] som burde settes med setters, då feltnavn heter noe annet enn feltet i json + */ + @Bean + @Primary + fun oAuth2HttpClient(): OAuth2HttpClient { + return RetryOAuth2HttpClient( + RestTemplateBuilder() + .setConnectTimeout(Duration.of(2, ChronoUnit.SECONDS)) + .setReadTimeout(Duration.of(4, ChronoUnit.SECONDS)), + ) + } + + @Bean + fun prosesseringInfoProvider( + @Value("\${prosessering.rolle}") prosesseringRolle: String, + ) = object : ProsesseringInfoProvider { + override fun hentBrukernavn(): String = try { + SpringTokenValidationContextHolder().tokenValidationContext.getClaims("azuread").getStringClaim("preferred_username") + } catch (e: Exception) { + "VL" + } + + override fun harTilgang(): Boolean = grupper().contains(prosesseringRolle) + + private fun grupper(): List { + return try { + SpringTokenValidationContextHolder().tokenValidationContext.getClaims("azuread") + ?.get("groups") as List? ?: emptyList() + } catch (e: Exception) { + emptyList() + } + } + } + + companion object { + + private val log = LoggerFactory.getLogger(ApplicationConfig::class.java) + const val PAKKENAVN = "no.nav.familie.ba.sak" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/AuditLoggerEvent.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/AuditLoggerEvent.kt new file mode 100644 index 000000000..01083bdcb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/AuditLoggerEvent.kt @@ -0,0 +1,8 @@ +package no.nav.familie.ba.sak.config + +enum class AuditLoggerEvent(val type: String) { + CREATE("create"), + UPDATE("update"), + DELETE("delete"), + ACCESS("access"), +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/BisysConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/BisysConfig.kt new file mode 100644 index 000000000..dbe241e32 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/BisysConfig.kt @@ -0,0 +1,47 @@ +package no.nav.familie.ba.sak.config + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import no.nav.familie.sikkerhet.OIDCUtil +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.filter.OncePerRequestFilter + +@Configuration +class BisysConfig( + private val oidcUtil: OIDCUtil, + @Value("\${BISYS_CLIENT_ID:dummy}") + private val bisysClientId: String, +) { + + @Bean + fun bisysFilter() = object : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain, + ) { + val clientId: String? = try { + oidcUtil.getClaim("azp") + } catch (throwable: Throwable) { + null + } + + if (clientId == null) { + // Dersom requesten mangler auth token, skal ikke dette filteret gjøre autorisasjonen + filterChain.doFilter(request, response) + } else if (bisysClientId == clientId && !request.requestURI.startsWith("/api/bisys")) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Kun autorisert for kall mot /api/bisys*") + } else { + filterChain.doFilter(request, response) + } + } + + override fun shouldNotFilter(request: HttpServletRequest) = + request.requestURI.contains("/internal") || + request.requestURI.startsWith("/swagger") || + request.requestURI.startsWith("/v2") // i bruk av swagger + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/CacheConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/CacheConfig.kt new file mode 100644 index 000000000..413471cc4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/CacheConfig.kt @@ -0,0 +1,96 @@ +package no.nav.familie.ba.sak.config + +import com.github.benmanes.caffeine.cache.Caffeine +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import org.springframework.cache.Cache +import org.springframework.cache.CacheManager +import org.springframework.cache.annotation.EnableCaching +import org.springframework.cache.concurrent.ConcurrentMapCache +import org.springframework.cache.concurrent.ConcurrentMapCacheManager +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary +import java.util.concurrent.TimeUnit + +@Configuration +@EnableCaching +class CacheConfig { + + @Bean + @Primary + fun cacheManager(): CacheManager = object : ConcurrentMapCacheManager() { + override fun createConcurrentMapCache(name: String): Cache { + val concurrentMap = Caffeine + .newBuilder() + .maximumSize(1000) + .expireAfterWrite(60, TimeUnit.MINUTES) + .recordStats().build().asMap() + return ConcurrentMapCache(name, concurrentMap, true) + } + } + + @Bean("shortCache") + fun shortCache(): CacheManager = object : ConcurrentMapCacheManager() { + override fun createConcurrentMapCache(name: String): Cache { + val concurrentMap = Caffeine + .newBuilder() + .maximumSize(1000) + .expireAfterWrite(10, TimeUnit.MINUTES) + .recordStats().build().asMap() + return ConcurrentMapCache(name, concurrentMap, true) + } + } + + @Bean("dailyCache") + fun dailyCache(): CacheManager = object : ConcurrentMapCacheManager() { + override fun createConcurrentMapCache(name: String): Cache { + val concurrentMap = Caffeine + .newBuilder() + .maximumSize(1000) + .expireAfterWrite(24, TimeUnit.HOURS) + .recordStats().build().asMap() + return ConcurrentMapCache(name, concurrentMap, true) + } + } + + @Bean("skattPersonerCache") + fun skattPersonerCache(): CacheManager = object : ConcurrentMapCacheManager() { + override fun createConcurrentMapCache(name: String): Cache { + val concurrentMap = Caffeine + .newBuilder() + .initialCapacity(100) + .maximumSize(1000) + .expireAfterWrite(1, TimeUnit.DAYS) + .recordStats().build().asMap() + return ConcurrentMapCache(name, concurrentMap, true) + } + } +} + +fun CacheManager.getCacheOrThrow(cache: String) = this.getCache(cache) ?: error("Finner ikke cache=$cache") + +/** + * Henter tidligere cachet verdier, og henter ucachet verdier med [valueLoader] + * Caches per saksbehandler, sånn at man eks kan hente tilgang for gitt saksbehandler + */ +@Suppress("UNCHECKED_CAST", "NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") +fun CacheManager.hentCacheForSaksbehandler( + cacheName: String, + values: List, + valueLoader: (List) -> Map, +): Map { + val cache = this.getCacheOrThrow(cacheName) + val saksbehandler = SikkerhetContext.hentSaksbehandler() + + val previousValues: List> = values.distinct() + .map { it to cache.get(Pair(saksbehandler, it))?.get() as RESULT? } + + val cachedValues = previousValues.mapNotNull { if (it.second == null) null else it }.toMap() as Map + val valuesWithoutCache = previousValues.filter { it.second == null }.map { it.first } + val loadedValues: Map = valuesWithoutCache + .takeIf { it.isNotEmpty() } + ?.let { valueLoader(it) } ?: emptyMap() + loadedValues.forEach { cache.put(Pair(saksbehandler, it.key), it.value) } + + return cachedValues + loadedValues +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/DatabaseConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/DatabaseConfig.kt new file mode 100644 index 000000000..a2c167d73 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/DatabaseConfig.kt @@ -0,0 +1,31 @@ +package no.nav.familie.ba.sak.config + +import no.nav.familie.prosessering.PropertiesWrapperTilStringConverter +import no.nav.familie.prosessering.StringTilPropertiesWrapperConverter +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.jdbc.core.convert.JdbcCustomConversions +import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration +import org.springframework.data.jdbc.repository.config.EnableJdbcAuditing +import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories +import org.springframework.data.jpa.repository.config.EnableJpaAuditing +import org.springframework.data.jpa.repository.config.EnableJpaRepositories + +@Configuration +@EnableJdbcAuditing +@EnableJdbcRepositories("no.nav.familie.prosessering") +@EnableJpaRepositories( + "no.nav.familie", +) +@EnableJpaAuditing +class DatabaseConfig : AbstractJdbcConfiguration() { + @Bean + override fun jdbcCustomConversions(): JdbcCustomConversions { + return JdbcCustomConversions( + listOf( + PropertiesWrapperTilStringConverter(), + StringTilPropertiesWrapperConverter(), + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/FlywayConfiguration.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/FlywayConfiguration.kt new file mode 100644 index 000000000..386a96400 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/FlywayConfiguration.kt @@ -0,0 +1,19 @@ +package no.nav.familie.ba.sak.config + +import org.flywaydb.core.api.configuration.FluentConfiguration +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.boot.autoconfigure.flyway.FlywayConfigurationCustomizer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile + +@Profile("!dev") +@ConditionalOnProperty("spring.flyway.enabled") +data class FlywayConfiguration(private val role: String) { + + @Bean + fun flywayConfig(): FlywayConfigurationCustomizer { + return FlywayConfigurationCustomizer { c: FluentConfiguration -> + c.initSql("SET ROLE \"$role\"") + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/JacksonJsonConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/JacksonJsonConfig.kt new file mode 100644 index 000000000..bfe57b3c9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/JacksonJsonConfig.kt @@ -0,0 +1,36 @@ +package no.nav.familie.ba.sak.config + +import com.fasterxml.jackson.annotation.JsonAutoDetect +import com.fasterxml.jackson.annotation.PropertyAccessor +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.KotlinModule +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class JacksonJsonConfig { + + companion object { + private val OM = ObjectMapper() + + init { + OM.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE) + OM.setVisibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.NONE) + OM.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + OM.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + OM.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + OM.setVisibility(PropertyAccessor.CREATOR, JsonAutoDetect.Visibility.ANY) + OM.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + OM.registerModule(JavaTimeModule()) + OM.registerModule(KotlinModule.Builder().build()) + } + } + + @Bean + fun objectMapper(): ObjectMapper { + return OM + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/KafkaAivenConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/KafkaAivenConfig.kt new file mode 100644 index 000000000..a71f6e03b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/KafkaAivenConfig.kt @@ -0,0 +1,117 @@ +package no.nav.familie.ba.sak.config + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.ObjectMapper +import no.nav.familie.kontrakter.felles.Applikasjon +import no.nav.familie.kontrakter.felles.objectMapper +import org.apache.kafka.clients.CommonClientConfigs +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.config.SslConfigs +import org.apache.kafka.common.serialization.StringDeserializer +import org.apache.kafka.common.serialization.StringSerializer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.env.Environment +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory +import org.springframework.kafka.config.KafkaListenerConfigUtils +import org.springframework.kafka.config.KafkaListenerEndpointRegistry +import org.springframework.kafka.core.ConsumerFactory +import org.springframework.kafka.core.DefaultKafkaConsumerFactory +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.core.ProducerFactory +import org.springframework.kafka.listener.ContainerProperties +import org.springframework.kafka.support.LoggingProducerListener + +@Configuration +class KafkaAivenConfig(val environment: Environment) { + + @Bean + fun producerFactory(): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs()) + } + + @Bean + fun kafkaAivenTemplate(): KafkaTemplate { + val producerListener = LoggingProducerListener() + producerListener.setIncludeContents(false) + return KafkaTemplate(producerFactory()).apply> { + setProducerListener(producerListener) + } + } + + @Bean + fun consumerFactory(): ConsumerFactory { + return DefaultKafkaConsumerFactory(consumerConfigs()) + } + + @Bean + fun concurrentKafkaListenerContainerFactory(kafkaErrorHandler: KafkaAivenErrorHandler): ConcurrentKafkaListenerContainerFactory { + val factory = ConcurrentKafkaListenerContainerFactory() + factory.setConcurrency(1) + factory.containerProperties.ackMode = ContainerProperties.AckMode.MANUAL + factory.consumerFactory = consumerFactory() + factory.setCommonErrorHandler(kafkaErrorHandler) + return factory + } + + @Bean(name = [KafkaListenerConfigUtils.KAFKA_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME]) + fun kafkaListenerEndpointRegistry(): KafkaListenerEndpointRegistry? { + return KafkaListenerEndpointRegistry() + } + + @Bean("kafkaObjectMapper") + fun kafkaObjectMapper(): ObjectMapper { + return objectMapper.copy().setSerializationInclusion(JsonInclude.Include.NON_NULL) + } + + private fun producerConfigs(): Map { + val kafkaBrokers = System.getenv("KAFKA_BROKERS") ?: "http://localhost:9092" + val producerConfigs = mutableMapOf( + ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to kafkaBrokers, + ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, + ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG to true, // Den sikrer rekkefølge + ProducerConfig.ACKS_CONFIG to "all", // Den sikrer at data ikke mistes + ProducerConfig.CLIENT_ID_CONFIG to Applikasjon.FAMILIE_BA_SAK.name, + ) + if (environment.activeProfiles.none { it.contains("dev") || it.contains("postgres") }) { + return producerConfigs + securityConfig() + } + return producerConfigs.toMap() + } + + fun consumerConfigs(): Map { + val kafkaBrokers = System.getenv("KAFKA_BROKERS") ?: "http://localhost:9092" + val consumerConfigs = mutableMapOf( + ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to kafkaBrokers, + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, + ConsumerConfig.GROUP_ID_CONFIG to "familie-ba-sak", + ConsumerConfig.CLIENT_ID_CONFIG to "consumer-familie-ba-sak-1", + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "latest", + ) + if (environment.activeProfiles.none { it.contains("dev") || it.contains("postgres") }) { + return consumerConfigs + securityConfig() + } + return consumerConfigs.toMap() + } + + private fun securityConfig(): Map { + val kafkaTruststorePath = System.getenv("KAFKA_TRUSTSTORE_PATH") + val kafkaCredstorePassword = System.getenv("KAFKA_CREDSTORE_PASSWORD") + val kafkaKeystorePath = System.getenv("KAFKA_KEYSTORE_PATH") + return mapOf( + CommonClientConfigs.SECURITY_PROTOCOL_CONFIG to "SSL", + SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG to "", // Disable server host name verification + SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG to "JKS", + SslConfigs.SSL_KEYSTORE_TYPE_CONFIG to "PKCS12", + SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG to kafkaTruststorePath, + SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG to kafkaCredstorePassword, + SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG to kafkaKeystorePath, + SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG to kafkaCredstorePassword, + SslConfigs.SSL_KEY_PASSWORD_CONFIG to kafkaCredstorePassword, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/KafkaAivenErrorHandler.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/KafkaAivenErrorHandler.kt new file mode 100644 index 000000000..69290ebee --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/KafkaAivenErrorHandler.kt @@ -0,0 +1,101 @@ +package no.nav.familie.ba.sak.config + +import no.nav.familie.ba.sak.common.secureLogger +import org.apache.kafka.clients.consumer.Consumer +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.core.task.SimpleAsyncTaskExecutor +import org.springframework.kafka.listener.CommonContainerStoppingErrorHandler +import org.springframework.kafka.listener.MessageListenerContainer +import org.springframework.stereotype.Component +import java.time.Duration +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong + +@Component +class KafkaAivenErrorHandler : CommonContainerStoppingErrorHandler() { + + val logger: Logger = LoggerFactory.getLogger(KafkaAivenErrorHandler::class.java) + + private val executor: Executor + private val teller = AtomicInteger(0) + private val sisteFeil = AtomicLong(0) + override fun handleRemaining( + e: Exception, + records: List>, + consumer: Consumer<*, *>, + container: MessageListenerContainer, + ) { + if (records.isNullOrEmpty()) { + logger.error("Feil ved konsumering av melding. Ingen records. ${consumer.subscription()}", e) + scheduleRestart( + e, + records, + consumer, + container, + "Ukjent topic", + ) + } else { + records.first().run { + logger.error( + "Feil ved konsumering av melding fra ${this.topic()}. id ${this.key()}, " + + "offset: ${this.offset()}, partition: ${this.partition()}", + ) + secureLogger.error("${this.topic()} - Problemer med prosessering av $records", e) + scheduleRestart( + e, + records, + consumer, + container, + this.topic(), + ) + } + } + } + + private fun scheduleRestart( + e: Exception, + records: List>, + consumer: Consumer<*, *>, + container: MessageListenerContainer, + topic: String, + ) { + val now = System.currentTimeMillis() + if (now - sisteFeil.getAndSet(now) > COUNTER_RESET_TID) { + teller.set(0) + } + val numErrors = teller.incrementAndGet() + val stopTime = + if (numErrors > MAKS_ANTALL_FEIL) MAKS_STOP_TID else MIN_STOP_TID * numErrors + executor.execute { + try { + Thread.sleep(stopTime) + logger.warn("Starter kafka container for $topic") + container.start() + } catch (exception: Exception) { + logger.error("Feil oppstod ved venting og oppstart av kafka container", exception) + } + } + logger.warn("Stopper kafka container for $topic i ${Duration.ofMillis(stopTime)}") + super.handleRemaining( + Exception("Sjekk securelogs for mer info - ${e::class.java.simpleName}"), + records, + consumer, + container, + ) + } + + companion object { + + private val MAKS_STOP_TID = Duration.ofHours(3).toMillis() + private val MIN_STOP_TID = Duration.ofSeconds(20).toMillis() + private const val MAKS_ANTALL_FEIL = 10 + private val COUNTER_RESET_TID = MIN_STOP_TID * MAKS_ANTALL_FEIL * 2 + } + + init { + this.executor = SimpleAsyncTaskExecutor() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/PensjonConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/PensjonConfig.kt new file mode 100644 index 000000000..7e18b0e94 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/PensjonConfig.kt @@ -0,0 +1,55 @@ +package no.nav.familie.ba.sak.config + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.sikkerhet.OIDCUtil +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import org.springframework.web.filter.OncePerRequestFilter + +@Configuration +@Profile("!integrasjonstest") +class PensjonConfig( + private val oidcUtil: OIDCUtil, + private val rolleConfig: RolleConfig, +) { + + @Bean + fun pensjonFilter() = object : OncePerRequestFilter() { + override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) { + val clientNavn: String? = try { + oidcUtil.getClaim("azp_name") + } catch (throwable: Throwable) { + null + } + val erKallerPensjon = clientNavn?.contains("omsorgsopptjening") ?: false + val harForvalterRolle = SikkerhetContext.harInnloggetBrukerForvalterRolle(rolleConfig) + val erPensjonRequest = request.requestURI.startsWith("/api/ekstern/pensjon") + + when { + erKallerPensjon && !erPensjonRequest -> { + response.sendError( + HttpServletResponse.SC_UNAUTHORIZED, + "Pensjon applikasjon kan ikke kalle andre tjenester", + ) + } + erPensjonRequest && (!harForvalterRolle && !erKallerPensjon) -> { + response.sendError( + HttpServletResponse.SC_UNAUTHORIZED, + "Pensjon tjeneste kan kun kalles av pensjon eller innlogget bruker med FORVALTER rolle", + ) + } + erPensjonRequest && (harForvalterRolle || erKallerPensjon) -> filterChain.doFilter(request, response) + !erPensjonRequest && !erKallerPensjon -> filterChain.doFilter(request, response) + } + } + + override fun shouldNotFilter(request: HttpServletRequest) = + request.requestURI.contains("/internal") || + request.requestURI.startsWith("/swagger") || + request.requestURI.startsWith("/v3") // i bruk av swagger + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/RestTemplateConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/RestTemplateConfig.kt new file mode 100644 index 000000000..83e210e19 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/RestTemplateConfig.kt @@ -0,0 +1,128 @@ +package no.nav.familie.ba.sak.config + +import no.nav.familie.http.interceptor.BearerTokenClientCredentialsClientInterceptor +import no.nav.familie.http.interceptor.BearerTokenClientInterceptor +import no.nav.familie.http.interceptor.ConsumerIdClientInterceptor +import no.nav.familie.http.interceptor.MdcValuesPropagatingClientInterceptor +import no.nav.familie.kontrakter.felles.objectMapper +import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.context.annotation.Profile +import org.springframework.http.converter.ByteArrayHttpMessageConverter +import org.springframework.http.converter.StringHttpMessageConverter +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter +import org.springframework.web.client.RestOperations +import org.springframework.web.client.RestTemplate +import java.nio.charset.StandardCharsets +import java.time.Duration + +@Configuration +@Import( + ConsumerIdClientInterceptor::class, + BearerTokenClientInterceptor::class, + MdcValuesPropagatingClientInterceptor::class, + BearerTokenClientCredentialsClientInterceptor::class, +) +@Profile("!mock-rest-template-config") +class RestTemplateConfig { + + @Bean("jwtBearerClientCredentials") + fun restTemplateJwtBearerClientCredentials( + consumerIdClientInterceptor: ConsumerIdClientInterceptor, + bearerTokenClientCredentialsClientInterceptor: BearerTokenClientCredentialsClientInterceptor, + ): RestOperations { + return RestTemplateBuilder() + .interceptors( + consumerIdClientInterceptor, + bearerTokenClientCredentialsClientInterceptor, + MdcValuesPropagatingClientInterceptor(), + ) + .additionalMessageConverters( + ByteArrayHttpMessageConverter(), + MappingJackson2HttpMessageConverter(objectMapper), + ) + .build() + } + + @Bean("jwtBearer") + fun restTemplateJwtBearer( + consumerIdClientInterceptor: ConsumerIdClientInterceptor, + bearerTokenClientInterceptor: BearerTokenClientInterceptor, + ): RestOperations { + return RestTemplateBuilder() + .interceptors( + consumerIdClientInterceptor, + bearerTokenClientInterceptor, + MdcValuesPropagatingClientInterceptor(), + ) + .additionalMessageConverters( + ByteArrayHttpMessageConverter(), + MappingJackson2HttpMessageConverter(objectMapper), + ) + .build() + } + + @Bean("jwtBearerMedLangTimeout") + fun restTemplateJwtBearerMedLangTimeout( + consumerIdClientInterceptor: ConsumerIdClientInterceptor, + bearerTokenClientInterceptor: BearerTokenClientInterceptor, + ): RestOperations { + return RestTemplateBuilder() + .setReadTimeout(Duration.ofMinutes(12L)) + .setConnectTimeout(Duration.ofMinutes(12L)) + .interceptors( + consumerIdClientInterceptor, + bearerTokenClientInterceptor, + MdcValuesPropagatingClientInterceptor(), + ) + .additionalMessageConverters( + ByteArrayHttpMessageConverter(), + MappingJackson2HttpMessageConverter(objectMapper), + ) + .build() + } + + @Bean + fun restTemplate(): RestTemplate { + return restTemplate + } + + @Bean + fun restOperations( + consumerIdClientInterceptor: ConsumerIdClientInterceptor, + mdcValuesPropagatingClientInterceptor: MdcValuesPropagatingClientInterceptor, + ): RestOperations { + return RestTemplateBuilder() + .interceptors(consumerIdClientInterceptor, mdcValuesPropagatingClientInterceptor) + .additionalMessageConverters( + ByteArrayHttpMessageConverter(), + MappingJackson2HttpMessageConverter(objectMapper), + ) + .build() + } + + @Bean + fun restTemplateBuilderMedProxy( + consumerIdClientInterceptor: ConsumerIdClientInterceptor, + mdcValuesPropagatingClientInterceptor: MdcValuesPropagatingClientInterceptor, + ): RestTemplateBuilder { + return RestTemplateBuilder() + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(5)) + .additionalInterceptors(consumerIdClientInterceptor, mdcValuesPropagatingClientInterceptor) + } + + companion object { + const val RETRY_BACKOFF_500MS = "\${retry.backoff.delay:500}" + } +} + +val restTemplate = RestTemplate( + listOf( + StringHttpMessageConverter(StandardCharsets.UTF_8), + ByteArrayHttpMessageConverter(), + MappingJackson2HttpMessageConverter(objectMapper), + ), +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/RolleConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/RolleConfig.kt new file mode 100644 index 000000000..2daeca8eb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/RolleConfig.kt @@ -0,0 +1,20 @@ +package no.nav.familie.ba.sak.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Configuration + +@Configuration +class RolleConfig( + @Value("\${rolle.beslutter}") + val BESLUTTER_ROLLE: String, + @Value("\${rolle.saksbehandler}") + val SAKSBEHANDLER_ROLLE: String, + @Value("\${rolle.veileder}") + val VEILEDER_ROLLE: String, + @Value("\${rolle.forvalter}") + val FORVALTER_ROLLE: String, + @Value("\${rolle.kode6}") + val KODE6: String, + @Value("\${rolle.kode7}") + val KODE7: String, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/SchedulingConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/SchedulingConfig.kt new file mode 100644 index 000000000..a78430f4b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/SchedulingConfig.kt @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.config + +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import org.springframework.scheduling.annotation.EnableScheduling + +@Profile("prod", "preprod", "task-scheduling") +@Configuration +@EnableScheduling +class SchedulingConfig diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/SentryConfiguration.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/SentryConfiguration.kt new file mode 100644 index 000000000..b3719dcf9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/SentryConfiguration.kt @@ -0,0 +1,79 @@ +package no.nav.familie.ba.sak.config + +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.protocol.User +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Configuration +import org.springframework.core.NestedExceptionUtils + +@Configuration +class SentryConfiguration( + @Value("\${sentry.environment}") val environment: String, + @Value("\${sentry.dsn}") val dsn: String, + @Value("\${sentry.logging.enabled}") val enabled: Boolean, +) { + init { + Sentry.init { options -> + options.dsn = if (enabled) dsn else "" // Tom streng betryr at Sentry er disabled + options.environment = environment + options.beforeSend = SentryOptions.BeforeSendCallback { event, _ -> + Sentry.configureScope { scope -> + scope.user = User().apply { + id = SikkerhetContext.hentSaksbehandler() + email = SikkerhetContext.hentSaksbehandlerEpost() + username = SikkerhetContext.hentSaksbehandler() + } + } + + val mostSpecificThrowable = + if (event.throwable != null) NestedExceptionUtils.getMostSpecificCause(event.throwable!!) else event.throwable + val metodeSomFeiler = finnMetodeSomFeiler(mostSpecificThrowable) + val prosess = MDC.get("prosess") + + event.setTag("metodeSomFeier", metodeSomFeiler) + event.setTag("bruker", SikkerhetContext.hentSaksbehandlerEpost()) + event.setTag("kibanalenke", hentKibanalenke(MDC.get("callId"))) + event.setTag("prosess", prosess) + + event.fingerprints = listOf( + "{{ default }}", + prosess, + event.transaction, + mostSpecificThrowable?.message, + ) + + if (metodeSomFeiler != UKJENT_METODE_SOM_FEILER) { + event.fingerprints = (event.fingerprints ?: emptyList()) + listOf( + metodeSomFeiler, + ) + } + + event + } + } + } + + private fun hentKibanalenke(callId: String) = + "https://logs.adeo.no/app/discover#/?_g=(time:(from:now-1M,to:now))&_a=(filters:!((query:(match_phrase:(x_callId:'$callId')))))" + + fun finnMetodeSomFeiler(e: Throwable?): String { + val firstElement = e?.stackTrace?.firstOrNull { + it.className.startsWith("no.nav.familie.ba.sak") && + !it.className.contains("$") + } + if (firstElement != null) { + val className = firstElement.className.split(".").lastOrNull() + return "$className::${firstElement.methodName}(${firstElement.lineNumber})" + } + return e?.cause?.let { finnMetodeSomFeiler(it) } ?: UKJENT_METODE_SOM_FEILER + } + + companion object { + val logger = LoggerFactory.getLogger(SentryConfiguration::class.java) + const val UKJENT_METODE_SOM_FEILER = "(Ukjent metode som feiler)" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/SwaggerConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/SwaggerConfig.kt new file mode 100644 index 000000000..248a4793e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/SwaggerConfig.kt @@ -0,0 +1,59 @@ +package no.nav.familie.ba.sak.config + +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.security.OAuthFlow +import io.swagger.v3.oas.models.security.OAuthFlows +import io.swagger.v3.oas.models.security.Scopes +import io.swagger.v3.oas.models.security.SecurityRequirement +import io.swagger.v3.oas.models.security.SecurityScheme +import org.springdoc.core.models.GroupedOpenApi +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class SwaggerConfig( + @Value("\${AUTHORIZATION_URL}") + val authorizationUrl: String, + @Value("\${TOKEN_URL}") + val tokenUrl: String, + @Value("\${API_SCOPE}") + val apiScope: String, +) { + + @Bean + fun openApi(): OpenAPI { + return OpenAPI() + .components(Components().addSecuritySchemes("oauth2", securitySchemes())) + .addSecurityItem(SecurityRequirement().addList("oauth2", listOf("read", "write"))) + } + + @Bean + fun eksternOpenApi(): GroupedOpenApi { + return GroupedOpenApi.builder().group("ekstern").packagesToScan("no.nav.familie.ba.sak.ekstern.bisys", "no.nav.familie.ba.sak.ekstern.pensjon") + .build() + } + + @Bean + fun internOpenApi(): GroupedOpenApi { + return GroupedOpenApi.builder().group("intern").packagesToScan("no.nav.familie.ba.sak") + .build() + } + + private fun securitySchemes(): SecurityScheme { + return SecurityScheme() + .name("oauth2") + .type(SecurityScheme.Type.OAUTH2) + .scheme("oauth2") + .`in`(SecurityScheme.In.HEADER) + .flows( + OAuthFlows() + .authorizationCode( + OAuthFlow().authorizationUrl(authorizationUrl) + .tokenUrl(tokenUrl) + .scopes(Scopes().addString(apiScope, "read,write")), + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/TaskRepositoryWrapper.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/TaskRepositoryWrapper.kt new file mode 100644 index 000000000..ec5b782f5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/TaskRepositoryWrapper.kt @@ -0,0 +1,25 @@ +package no.nav.familie.ba.sak.config + +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import org.springframework.context.annotation.Profile +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Component + +/* +TaskRepository in familie-prosessering is @Primary, which is not able to mock so we use this wrapper class for testibility + */ +@Profile("!mock-task-repository") +@Component +class TaskRepositoryWrapper(private val taskService: TaskService) { + + fun save(task: Task) = + taskService.save(task) + + fun findAll(): Iterable = + taskService.findAll() + + fun findByStatus(status: Status): List = + taskService.finnTasksMedStatus(listOf(status), type = null, page = Pageable.unpaged()) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/WebConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/WebConfig.kt new file mode 100644 index 000000000..fd506992a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/WebConfig.kt @@ -0,0 +1,25 @@ +package no.nav.familie.ba.sak.config + +import no.nav.familie.ba.sak.common.http.interceptor.RolletilgangInterceptor +import no.nav.familie.sikkerhet.OIDCUtil +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.web.servlet.config.annotation.InterceptorRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +@Import(OIDCUtil::class, RolleConfig::class) +class WebConfig( + private val rolleConfig: RolleConfig, +) : WebMvcConfigurer { + + override fun addInterceptors(registry: InterceptorRegistry) { + registry.addInterceptor(RolletilgangInterceptor(rolleConfig)) + .excludePathPatterns("/api/task/**") + .excludePathPatterns("/api/v2/task/**") + .excludePathPatterns("/internal") + .excludePathPatterns("/testverktoy") + .excludePathPatterns("/api/feature") + super.addInterceptors(registry) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/ByAnsvarligSaksbehandler.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/ByAnsvarligSaksbehandler.kt new file mode 100644 index 000000000..c1ef3b1d1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/ByAnsvarligSaksbehandler.kt @@ -0,0 +1,15 @@ +package no.nav.familie.ba.sak.config.featureToggle + +import io.getunleash.strategy.Strategy +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext + +class ByAnsvarligSaksbehandler : Strategy { + + override fun isEnabled(parameters: MutableMap): Boolean { + if (parameters.isEmpty()) return false + + return parameters["saksbehandler"]?.contains(SikkerhetContext.hentSaksbehandlerEpost()) ?: false + } + + override fun getName(): String = "byAnsvarligSaksbehandler" +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/ByClusterStrategy.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/ByClusterStrategy.kt new file mode 100644 index 000000000..425d7db59 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/ByClusterStrategy.kt @@ -0,0 +1,13 @@ +package no.nav.familie.ba.sak.config.featureToggle + +import io.getunleash.strategy.Strategy + +class ByClusterStrategy(private val clusterName: String) : Strategy { + + override fun isEnabled(parameters: MutableMap): Boolean { + if (parameters.isEmpty()) return false + return parameters["cluster"]?.contains(clusterName) ?: false + } + + override fun getName(): String = "byCluster" +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/DummyFeatureToggleService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/DummyFeatureToggleService.kt new file mode 100644 index 000000000..a0673e413 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/DummyFeatureToggleService.kt @@ -0,0 +1,21 @@ +package no.nav.familie.ba.sak.config.featureToggle + +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService + +class DummyFeatureToggleService( + private val unleash: FeatureToggleProperties.Unleash, +) : FeatureToggleService { + + private val overstyrteBrytere = mapOf( + Pair(FeatureToggleConfig.TEKNISK_VEDLIKEHOLD_HENLEGGELSE, true), + ) + + override fun isEnabled(toggleId: String, defaultValue: Boolean): Boolean { + if (unleash.cluster == "lokalutvikling") { + return true + } + + return overstyrteBrytere.getOrDefault(toggleId, true) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleConfig.kt new file mode 100644 index 000000000..e9925313c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleConfig.kt @@ -0,0 +1,40 @@ +package no.nav.familie.ba.sak.config + +class FeatureToggleConfig { + companion object { + // Operasjonelle + const val KAN_MANUELT_KORRIGERE_MED_VEDTAKSBREV = "familie-ba-sak.behandling.korreksjon-vedtaksbrev" + const val SKATTEETATEN_API_EKTE_DATA = "familie-ba-sak.skatteetaten-api-ekte-data-i-respons" + const val IKKE_STOPP_MIGRERINGSBEHANDLING = "familie-ba-sak.ikke.stopp.migeringsbehandling" + const val TEKNISK_VEDLIKEHOLD_HENLEGGELSE = "familie-ba-sak.teknisk-vedlikehold-henleggelse.tilgangsstyring" + const val TEKNISK_ENDRING = "familie-ba-sak.behandling.teknisk-endring" + + // Release + const val EØS_INFORMASJON_OM_ÅRLIG_KONTROLL = "familie-ba-sak.eos-informasjon-om-aarlig-kontroll" + const val ER_MANUEL_POSTERING_TOGGLE_PÅ = "familie-ba-sak.manuell-postering" + const val FEILUTBETALT_VALUTA_PR_MND = "familie-ba-sak.feilutbetalt-valuta-pr-mnd" + const val EØS_PRAKSISENDRING_SEPTEMBER2023 = + "familie-ba-sak.behandling.eos-annen-forelder-omfattet-av-norsk-lovgivning" + + // unleash toggles for satsendring, kan slettes etter at satsendring er skrudd på for alle satstyper + const val SATSENDRING_ENABLET: String = "familie-ba-sak.satsendring-enablet" + const val SATSENDRING_SNIKE_I_KØEN = "familie-ba-sak.satsendring-snike-i-koen" + + // Ny utbetalingsgenerator + const val KONTROLLER_NY_UTBETALINGSGENERATOR = "familie.ba.sak.kontroller-ny-utbetalingsgenerator" + const val BRUK_NY_UTBETALINGSGENERATOR = "familie.ba.sak.bruk-ny-utbetalingsgenerator" + + // Unleash Next toggles + const val ENDRET_EØS_REGELVERKFILTER_FOR_BARN = "familie-ba-sak.endret-eos-regelverkfilter-for-barn" + const val NY_GENERERING_AV_BREVOBJEKTER = "familie-ba-sak.ny-generering-av-brevobjekter" + } +} + +interface FeatureToggleService { + + fun isEnabled(toggleId: String): Boolean { + return isEnabled(toggleId, false) + } + + fun isEnabled(toggleId: String, defaultValue: Boolean): Boolean +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleController.kt new file mode 100644 index 000000000..a493a1277 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleController.kt @@ -0,0 +1,32 @@ +package no.nav.familie.ba.sak.config.featureToggle + +import no.nav.familie.ba.sak.common.RessursUtils +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/feature") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class FeatureToggleController( + private val featureToggleService: FeatureToggleService, +) { + + @PostMapping(produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentToggles(@RequestBody toggles: List): ResponseEntity>> { + return RessursUtils.ok( + toggles.fold(mutableMapOf()) { acc, toggleId -> + acc[toggleId] = featureToggleService.isEnabled(toggleId) + acc + }, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleInitializer.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleInitializer.kt new file mode 100644 index 000000000..1deb52250 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleInitializer.kt @@ -0,0 +1,33 @@ +package no.nav.familie.ba.sak.config.featureToggle + +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.config.featureToggle.miljø.Profil +import no.nav.familie.ba.sak.config.featureToggle.miljø.erAktiv +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.env.Environment + +@Configuration +class FeatureToggleInitializer( + private val featureToggleProperties: FeatureToggleProperties, + private val environment: Environment, +) { + + @Bean + fun featureToggle(): FeatureToggleService = + if (featureToggleProperties.enabled || environment.erAktiv(Profil.DevPostgresPreprod)) { + UnleashFeatureToggleService(featureToggleProperties.unleash) + } else { + logger.warn( + "Funksjonsbryter-funksjonalitet er skrudd AV. " + + "Gir standardoppførsel for alle funksjonsbrytere, dvs 'false'", + ) + DummyFeatureToggleService(featureToggleProperties.unleash) + } + + companion object { + + private val logger = LoggerFactory.getLogger(FeatureToggleProperties::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleProperties.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleProperties.kt new file mode 100644 index 000000000..e49be3886 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleProperties.kt @@ -0,0 +1,17 @@ +package no.nav.familie.ba.sak.config.featureToggle + +import org.springframework.boot.context.properties.ConfigurationProperties +import java.net.URI + +@ConfigurationProperties("funksjonsbrytere") +class FeatureToggleProperties( + val enabled: Boolean, + val unleash: Unleash, +) { + + data class Unleash( + val uri: URI, + val cluster: String, + val applicationName: String, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/UnleashFeatureToggleService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/UnleashFeatureToggleService.kt new file mode 100644 index 000000000..cd0b5ef29 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/UnleashFeatureToggleService.kt @@ -0,0 +1,40 @@ +package no.nav.familie.ba.sak.config.featureToggle + +import io.getunleash.DefaultUnleash +import io.getunleash.UnleashContext +import io.getunleash.UnleashContextProvider +import io.getunleash.strategy.GradualRolloutRandomStrategy +import io.getunleash.util.UnleashConfig +import no.nav.familie.ba.sak.config.FeatureToggleService + +class UnleashFeatureToggleService(unleash: FeatureToggleProperties.Unleash) : FeatureToggleService { + + private val defaultUnleash: DefaultUnleash + private val unleash: FeatureToggleProperties.Unleash + + init { + defaultUnleash = DefaultUnleash( + UnleashConfig.builder() + .appName(unleash.applicationName) + .unleashAPI(unleash.uri) + .unleashContextProvider(lagUnleashContextProvider()) + .build(), + ByClusterStrategy(unleash.cluster), + ByAnsvarligSaksbehandler(), + GradualRolloutRandomStrategy(), + ) + this.unleash = unleash + } + + private fun lagUnleashContextProvider(): UnleashContextProvider { + return UnleashContextProvider { + UnleashContext.builder() + .appName(unleash.applicationName) + .build() + } + } + + override fun isEnabled(toggleId: String, defaultValue: Boolean): Boolean { + return defaultUnleash.isEnabled(toggleId, defaultValue) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/milj\303\270/EnvironmentConfig.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/milj\303\270/EnvironmentConfig.kt" new file mode 100644 index 000000000..33e1cbb5a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/milj\303\270/EnvironmentConfig.kt" @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.config.featureToggle.miljø + +import org.springframework.core.env.Environment + +fun Environment.erAktiv(profil: Profil) = activeProfiles.any { it == profil.navn.trim() } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/milj\303\270/Profil.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/milj\303\270/Profil.kt" new file mode 100644 index 000000000..9568aa21c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/config/featureToggle/milj\303\270/Profil.kt" @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.config.featureToggle.miljø + +enum class Profil(val navn: String) { + DevPostgresPreprod("dev-postgres-preprod"), + Integrasjonstest("integrasjonstest"), + Prod("prod"), + Preprod("preprod"), + Dev("dev"), + Postgres("postgres"), +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/EksternKlageController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/EksternKlageController.kt new file mode 100644 index 000000000..e84de66fc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/EksternKlageController.kt @@ -0,0 +1,82 @@ +package no.nav.familie.ba.sak.ekstern + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.kjerne.klage.KlageService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.klage.FagsystemVedtak +import no.nav.familie.kontrakter.felles.klage.KanOppretteRevurderingResponse +import no.nav.familie.kontrakter.felles.klage.OpprettRevurderingResponse +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +// Kalles av familie-klage +@RestController +@RequestMapping( + path = ["/api/klage/"], + produces = [MediaType.APPLICATION_JSON_VALUE], +) +@ProtectedWithClaims(issuer = "azuread") +class EksternKlageController( + private val tilgangService: TilgangService, + private val klageService: KlageService, +) { + + @GetMapping("fagsaker/{fagsakId}/kan-opprette-revurdering-klage") + fun kanOppretteRevurderingKlage(@PathVariable fagsakId: Long): Ressurs { + tilgangService.validerTilgangTilHandlingOgFagsak( + fagsakId = fagsakId, + handling = "Valider vi kan opprette revurdering med årsak klage på fagsak=$fagsakId", + event = AuditLoggerEvent.CREATE, + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + ) + + if (!SikkerhetContext.kallKommerFraKlage()) { + throw Feil("Kallet utføres ikke av en autorisert klient") + } + + return Ressurs.success(klageService.kanOppretteRevurdering(fagsakId)) + } + + @PostMapping("fagsaker/{fagsakId}/opprett-revurdering-klage/") + fun opprettRevurderingKlage(@PathVariable fagsakId: Long): Ressurs { + tilgangService.validerTilgangTilHandlingOgFagsak( + fagsakId = fagsakId, + handling = "Opprett revurdering med årask klage på fagsak=$fagsakId", + event = AuditLoggerEvent.CREATE, + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + ) + + if (!SikkerhetContext.kallKommerFraKlage()) { + throw Feil("Kallet utføres ikke av en autorisert klient") + } + return Ressurs.success( + klageService.validerOgOpprettRevurderingKlage( + fagsakId, + ), + ) + } + + @GetMapping("fagsaker/{fagsakId}/vedtak") + @ProtectedWithClaims(issuer = "azuread") + fun hentVedtak(@PathVariable fagsakId: Long): Ressurs> { + if (!SikkerhetContext.erMaskinTilMaskinToken()) { + tilgangService.validerTilgangTilHandlingOgFagsak( + fagsakId = fagsakId, + handling = "Kan hente vedtak på fagsak=$fagsakId", + event = AuditLoggerEvent.ACCESS, + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + ) + } + + return Ressurs.success(klageService.hentFagsystemVedtak(fagsakId)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/bisys/BisysController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/bisys/BisysController.kt new file mode 100644 index 000000000..255ca0dac --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/bisys/BisysController.kt @@ -0,0 +1,129 @@ +package no.nav.familie.ba.sak.ekstern.bisys + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.ArraySchema +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import no.nav.familie.ba.sak.common.EksternTjenesteFeil +import no.nav.familie.ba.sak.common.EksternTjenesteFeilException +import no.nav.familie.ba.sak.common.Feil +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate +import java.time.YearMonth + +@RestController +@RequestMapping("/api/bisys") +@ProtectedWithClaims(issuer = "azuread") +class BisysController(private val bisysService: BisysService) { + + @Operation( + description = "Tjeneste for BISYS for å hente utvidet barnetrygd og småbarnstillegg for en gitt person.", + + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = + """Liste over perioder som brukeren har hatt innvilget ytelse. Returnerer både fra Infotrygd og BA-SAK + + stønadstype: Hva slags type stønad. UTVIDET eller SMÅBARNSTILLEGG + fomMåned: Første måned i perioden + tomMåned: Den siste måneden i perioden. Hvis null, så er stønaden løpende + beløp: utbetalingsbeløp + manueltBeregnet: Beløpet er manuelt beregnet og kan inneholde andre stønader som barnetrygd + + """, + content = [ + ( + Content( + mediaType = "application/json", + array = ( + ArraySchema(schema = Schema(implementation = UtvidetBarnetrygdPeriode::class)) + ), + ) + ), + ], + ), + ApiResponse( + responseCode = "400", + description = "Ugyldig input. fraDato maks tilbake 5 år", + content = [Content()], + ), + ApiResponse(responseCode = "500", description = "Uventet feil", content = [Content()]), + ], + ) + @PostMapping( + path = ["/hent-utvidet-barnetrygd"], + consumes = [MediaType.APPLICATION_JSON_VALUE], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + fun hentUtvidetBarnetrygd( + @RequestBody + request: BisysUtvidetBarnetrygdRequest, + ): ResponseEntity { + val path = "/api/bisys/hent-utvidet-barnetrygd" + if (LocalDate.now().minusYears(5).isAfter(request.fraDato)) { + throw EksternTjenesteFeilException( + EksternTjenesteFeil( + path, + HttpStatus.BAD_REQUEST, + ), + "fraDato kan ikke være lenger enn 5 år tilbake i tid", + request, + ) + } + + try { + return ResponseEntity.ok(bisysService.hentUtvidetBarnetrygd(request.personIdent, request.fraDato)) + } catch (e: RuntimeException) { + if ((e is Feil) && (e.httpStatus == HttpStatus.NOT_FOUND)) { + throw EksternTjenesteFeilException( + EksternTjenesteFeil( + "/api/bisys/hent-utvidet-barnetrygd", + HttpStatus.BAD_REQUEST, + ), + "Fant ikke personIdent i PDL", + request, + e, + ) + } + + throw EksternTjenesteFeilException( + EksternTjenesteFeil(path), + e.message ?: "Ukjent feil ved hent utvidet barnetrygd", + request, + e, + ) + } + } +} + +data class BisysUtvidetBarnetrygdRequest( + val personIdent: String, + @Schema(implementation = String::class, example = "2020-12-01") val fraDato: LocalDate, +) + +class BisysUtvidetBarnetrygdResponse(val perioder: List) +data class UtvidetBarnetrygdPeriode( + val stønadstype: BisysStønadstype, + @Schema(implementation = String::class, example = "2020-12") val fomMåned: YearMonth, + @Schema(implementation = String::class, example = "2020-12") val tomMåned: YearMonth?, + val beløp: Double, + val manueltBeregnet: Boolean, + val deltBosted: Boolean? = null, +) + +enum class BisysStønadstype { + UTVIDET, + SMÅBARNSTILLEGG, +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/bisys/BisysService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/bisys/BisysService.kt new file mode 100644 index 000000000..61c2ccbbe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/bisys/BisysService.kt @@ -0,0 +1,107 @@ +package no.nav.familie.ba.sak.ekstern.bisys + +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdBarnetrygdClient +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class BisysService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val infotrygdBarnetrygdClient: InfotrygdBarnetrygdClient, + private val fagsakRepository: FagsakRepository, + private val personidentService: PersonidentService, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, +) { + fun hentUtvidetBarnetrygd(personIdent: String, fraDato: LocalDate): BisysUtvidetBarnetrygdResponse { + val aktør = personidentService.hentAktør(personIdent) + + val samledeUtvidetBarnetrygdPerioder = mutableListOf() + samledeUtvidetBarnetrygdPerioder.addAll(hentBisysPerioderFraInfotrygd(aktør, fraDato)) + + val perioderFraBasak = hentBisysPerioderFraBaSak(aktør, fraDato) + secureLogger.info("Bisysperioder for $personIdent i ba-sak: perioderFraBasak=$perioderFraBasak") + samledeUtvidetBarnetrygdPerioder.addAll(perioderFraBasak) + + val sammenslåttePerioder = + samledeUtvidetBarnetrygdPerioder.filter { it.stønadstype == BisysStønadstype.UTVIDET } + .groupBy { it.beløp }.values + .flatMap(::slåSammenSammenhengendePerioder).toMutableList() + + sammenslåttePerioder.addAll( + samledeUtvidetBarnetrygdPerioder.filter { it.stønadstype == BisysStønadstype.SMÅBARNSTILLEGG } + .groupBy { it.beløp }.values + .flatMap(::slåSammenSammenhengendePerioder), + ) + + return BisysUtvidetBarnetrygdResponse( + sammenslåttePerioder.sortedWith( + compareBy( + { it.stønadstype }, + { it.fomMåned }, + ), + ), + ) + } + + private fun hentBisysPerioderFraInfotrygd( + aktør: Aktør, + fraDato: LocalDate, + ): List = + personidentService.hentAlleFødselsnummerForEnAktør(aktør).flatMap { + infotrygdBarnetrygdClient.hentUtvidetBarnetrygd(it, fraDato.toYearMonth()).perioder + } + + private fun hentBisysPerioderFraBaSak( + aktør: Aktør, + fraDato: LocalDate, + ): List { + val fagsak = fagsakRepository.finnFagsakForAktør(aktør) + val behandling = fagsak?.let { behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(it.id) } + if (fagsak == null || behandling == null) { + return emptyList() + } + logger.info("Henter bisysperioder for siste iverksette behandlong for fagsakId=${fagsak.id}, behandlingId=${behandling.id}") + return tilkjentYtelseRepository.findByBehandlingAndHasUtbetalingsoppdrag(behandling.id)?.andelerTilkjentYtelse + ?.filter { it.erSøkersAndel() } + ?.filter { + it.stønadTom.isSameOrAfter(fraDato.toYearMonth()) + } + ?.map { + UtvidetBarnetrygdPeriode( + stønadstype = if (it.erUtvidet()) BisysStønadstype.UTVIDET else BisysStønadstype.SMÅBARNSTILLEGG, + fomMåned = it.stønadFom, + tomMåned = it.stønadTom, + beløp = it.kalkulertUtbetalingsbeløp.toDouble(), + manueltBeregnet = false, + deltBosted = it.erDeltBosted(), + ) + } ?: emptyList() + } + + private fun slåSammenSammenhengendePerioder(utbetalingerAvSammeBeløp: List): List { + return utbetalingerAvSammeBeløp.sortedBy { it.fomMåned } + .fold(mutableListOf()) { sammenslåttePerioder, nesteUtbetaling -> + if (sammenslåttePerioder.lastOrNull()?.tomMåned?.isSameOrAfter(nesteUtbetaling.fomMåned.minusMonths(1)) != false && + sammenslåttePerioder.lastOrNull()?.manueltBeregnet == nesteUtbetaling.manueltBeregnet && + sammenslåttePerioder.lastOrNull()?.deltBosted == nesteUtbetaling.deltBosted + ) { + sammenslåttePerioder.apply { add(removeLast().copy(tomMåned = nesteUtbetaling.tomMåned)) } + } else { + sammenslåttePerioder.apply { add(nesteUtbetaling) } + } + } + } + + companion object { + private val logger = LoggerFactory.getLogger(BisysService::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/Barnetrygd.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/Barnetrygd.kt new file mode 100644 index 000000000..d529fd919 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/Barnetrygd.kt @@ -0,0 +1,39 @@ +package no.nav.familie.ba.sak.ekstern.pensjon + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate +import java.time.YearMonth + +data class BarnetrygdTilPensjonRequest( + val ident: String, + @Schema(implementation = String::class, example = "2020-12-01") + val fraDato: LocalDate, +) + +/* + * Finnes barna til personen det spørres på i flere fagsaker vil det være flere elementer i listen + * Ett element pr. fagsak barnet er knyttet til. + * Kan være andre personer enn mor og far. + */ +data class BarnetrygdTilPensjonResponse(val fagsaker: List) + +data class BarnetrygdTilPensjon( + val fagsakId: String, + val fagsakEiersIdent: String, + val barnetrygdPerioder: List, +) + +data class BarnetrygdPeriode( + val personIdent: String, + val delingsprosentYtelse: Int, + val ytelseTypeEkstern: YtelseTypeEkstern?, + val utbetaltPerMnd: Int, + val stønadFom: YearMonth, + val stønadTom: YearMonth, +) + +enum class YtelseTypeEkstern { + ORDINÆR_BARNETRYGD, + UTVIDET_BARNETRYGD, + SMÅBARNSTILLEGG, +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/HentAlleIdenterTilPsysResponseDTO.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/HentAlleIdenterTilPsysResponseDTO.kt new file mode 100644 index 000000000..860083ce0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/HentAlleIdenterTilPsysResponseDTO.kt @@ -0,0 +1,8 @@ +package no.nav.familie.ba.sak.ekstern.pensjon + +import java.util.UUID +data class HentAlleIdenterTilPsysResponseDTO(val meldingstype: Meldingstype, val requestId: UUID, val personident: String?) + +enum class Meldingstype { + START, DATA, SLUTT +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/PensjonController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/PensjonController.kt new file mode 100644 index 000000000..3bb8c50ff --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/PensjonController.kt @@ -0,0 +1,154 @@ +package no.nav.familie.ba.sak.ekstern.pensjon + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.ArraySchema +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import no.nav.familie.ba.sak.common.EksternTjenesteFeil +import no.nav.familie.ba.sak.common.EksternTjenesteFeilException +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.lang.IllegalArgumentException +import java.time.LocalDate + +@RestController +@RequestMapping("/api/ekstern/pensjon") +@ProtectedWithClaims(issuer = "azuread") +class PensjonController(private val pensjonService: PensjonService) { + + @Operation( + description = "Tjeneste for Pensjon for å hente barnetrygd og relaterte saker for en gitt person.", + + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = + """Liste over fagsaker og relaterte fagsaker(hvis barna finnes i flere fagsaker) fra ba-sak + + fagsakId: unik id for fagsaken + fagsakEiersIdent: Fnr for eier av fagsaken + barnetrygdPerioder: Liste over perioder med barnetrygd + + """, + content = [ + Content( + mediaType = "application/json", + array = ( + ArraySchema(schema = Schema(implementation = BarnetrygdTilPensjon::class)) + ), + ), + ], + ), + ApiResponse( + responseCode = "400", + description = "Ugyldig input. fraDato maks tilbake 2 år", + content = [Content()], + ), + ApiResponse( + responseCode = "500", + description = "Uventet feil", + content = [ + Content( + mediaType = "application/json", + array = ( + ArraySchema(schema = Schema(implementation = Ressurs::class)) + ), + ), + ], + ), + ], + ) + @PostMapping( + path = ["/hent-barnetrygd"], + consumes = [MediaType.APPLICATION_JSON_VALUE], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + fun hentBarnetrygd( + @RequestBody + request: BarnetrygdTilPensjonRequest, + ): ResponseEntity { + if (LocalDate.now().minusYears(2).isAfter(request.fraDato)) { + throw EksternTjenesteFeilException( + EksternTjenesteFeil( + "/api/ekstern/pensjon/hent-barnetrygd", + HttpStatus.BAD_REQUEST, + ), + "fraDato kan ikke være lenger enn 2 år tilbake i tid", + request, + ) + } + + return ResponseEntity.ok( + BarnetrygdTilPensjonResponse( + pensjonService.hentBarnetrygd( + request.ident, + request.fraDato, + ), + ), + ) + } + + @Operation( + description = "Tjeneste for Pensjon for å bestille identer med barnetrygd for et gitt år på kafka.", + + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "202", + description = + """ + RequestId som blir med kafka-meldingene + """, + content = [ + Content( + mediaType = MediaType.TEXT_PLAIN_VALUE, + ), + ], + ), + ApiResponse( + responseCode = "400", + description = "Ugyldig input. År må være av type string - 4 tegn", + content = [Content()], + ), + ApiResponse( + responseCode = "500", + description = "Uventet feil", + content = [ + Content( + mediaType = MediaType.TEXT_PLAIN_VALUE, + ), + ], + ), + ], + ) + @GetMapping( + path = ["/bestill-personer-med-barnetrygd/{år}"], + produces = [MediaType.TEXT_PLAIN_VALUE], + ) + fun bestillPersonerMedBarnetrygdForGittÅrPåKafka( + @PathVariable + år: Int, + ): ResponseEntity { + val minÅr: Long = 1970 + val maxÅr: Long = 2300 + if (år in minÅr..maxÅr) { + return ResponseEntity.accepted().body(pensjonService.lagTaskForHentingAvIdenterTilPensjon(år)) + } else { + throw IllegalArgumentException("$år er ikke et gyldig år. År må være et tall i intervallet $minÅr til $maxÅr") + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/PensjonService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/PensjonService.kt new file mode 100644 index 000000000..e35aa2e47 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/PensjonService.kt @@ -0,0 +1,108 @@ +package no.nav.familie.ba.sak.ekstern.pensjon + +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.ekstern.bisys.BisysService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.task.HentAlleIdenterTilPsysTask +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.util.UUID + +@Service +class PensjonService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val fagsakRepository: FagsakRepository, + private val personidentService: PersonidentService, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + private val taskRepository: TaskRepositoryWrapper, +) { + fun hentBarnetrygd(personIdent: String, fraDato: LocalDate): List { + val aktør = personidentService.hentAktør(personIdent) + val fagsak = fagsakRepository.finnFagsakForAktør(aktør) ?: return emptyList() + val barnetrygdTilPensjon = hentBarnetrygdTilPensjon(fagsak, fraDato) ?: return emptyList() + + // Sjekk om det finnes relaterte saker, dvs om barna finnes i andre behandlinger + val barnetrygdMedRelaterteSaker = barnetrygdTilPensjon.barnetrygdPerioder + .filter { it.personIdent != aktør.aktivFødselsnummer() } + .map { it.personIdent }.distinct() + .map { hentBarnetrygdForRelatertPersonTilPensjon(it, fraDato, aktør) } + .flatten() + return barnetrygdMedRelaterteSaker.plus(barnetrygdTilPensjon).distinct() + } + + fun lagTaskForHentingAvIdenterTilPensjon(år: Int): String { + val uuid = UUID.randomUUID() + taskRepository.save(HentAlleIdenterTilPsysTask.lagTask(år, uuid)) + return uuid.toString() + } + + private fun hentBarnetrygdForRelatertPersonTilPensjon( + personIdent: String, + fraDato: LocalDate, + forelderAktør: Aktør, + ): List { + val aktør = personidentService.hentAktør(personIdent) + val fagsaker = fagsakRepository.finnFagsakerSomHarAndelerForAktør(aktør) + .filter { it.type == FagsakType.NORMAL } // skal kun ha normale fagsaker til med her + .filter { it.aktør != forelderAktør } // trenger ikke å hente data til forelderen på nytt + .distinct() + return fagsaker.mapNotNull { fagsak -> hentBarnetrygdTilPensjon(fagsak, fraDato) } + } + + private fun hentBarnetrygdTilPensjon(fagsak: Fagsak, fraDato: LocalDate): BarnetrygdTilPensjon? { + val behandling = behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsak.id) + ?: return null + logger.info("Henter perioder med barnetrygd til pensjon for fagsakId=${fagsak.id}, behandlingId=${behandling.id}") + + val perioder = hentPerioder(behandling, fraDato) + + return BarnetrygdTilPensjon( + fagsakId = fagsak.id.toString(), + barnetrygdPerioder = perioder, + fagsakEiersIdent = fagsak.aktør.aktivFødselsnummer(), + ) + } + + private fun hentPerioder( + behandling: Behandling, + fraDato: LocalDate, + ): List { + val tilkjentYtelse = tilkjentYtelseRepository.findByBehandlingAndHasUtbetalingsoppdrag(behandling.id) + ?: error("Finner ikke tilkjent ytelse for behandling=${behandling.id}") + return tilkjentYtelse.andelerTilkjentYtelse + .filter { it.stønadTom.isSameOrAfter(fraDato.toYearMonth()) } + .map { + BarnetrygdPeriode( + ytelseTypeEkstern = it.type.tilPensjonYtelsesType(), + personIdent = it.aktør.aktivFødselsnummer(), + stønadFom = it.stønadFom, + stønadTom = it.stønadTom, + utbetaltPerMnd = it.kalkulertUtbetalingsbeløp, + delingsprosentYtelse = it.prosent.toInt(), + ) + } + } + + fun YtelseType.tilPensjonYtelsesType(): YtelseTypeEkstern { + return when (this) { + YtelseType.ORDINÆR_BARNETRYGD -> YtelseTypeEkstern.ORDINÆR_BARNETRYGD + YtelseType.SMÅBARNSTILLEGG -> YtelseTypeEkstern.SMÅBARNSTILLEGG + YtelseType.UTVIDET_BARNETRYGD -> YtelseTypeEkstern.UTVIDET_BARNETRYGD + } + } + + companion object { + private val logger = LoggerFactory.getLogger(BisysService::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/AbstractUtfyltStatus.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/AbstractUtfyltStatus.kt new file mode 100644 index 000000000..1266e52fb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/AbstractUtfyltStatus.kt @@ -0,0 +1,24 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +abstract class AbstractUtfyltStatus { + abstract val status: UtfyltStatus + abstract fun medUtfyltStatus(): T + + fun finnAntallUtfylt(felter: Collection): Int { + return felter.fold(0) { antallUtfylte, felt -> antallUtfylte + (felt?.let { 1 } ?: 0) } + } + + fun utfyltStatus(antallUtfylt: Int, antallFelter: Int): UtfyltStatus { + return when (antallUtfylt) { + antallFelter -> UtfyltStatus.OK + in 1 until antallFelter -> UtfyltStatus.UFULLSTENDIG + else -> UtfyltStatus.IKKE_UTFYLT + } + } +} + +enum class UtfyltStatus { + IKKE_UTFYLT, + UFULLSTENDIG, + OK, +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestAnnenVurdering.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestAnnenVurdering.kt new file mode 100644 index 000000000..6149abc14 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestAnnenVurdering.kt @@ -0,0 +1,19 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurderingType + +data class RestAnnenVurdering( + val id: Long, + val resultat: Resultat, + val type: AnnenVurderingType, + val begrunnelse: String?, +) + +fun AnnenVurdering.tilRestAnnenVurdering() = RestAnnenVurdering( + id = this.id, + resultat = this.resultat, + type = this.type, + begrunnelse = this.begrunnelse, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestArbeidsfordelingP\303\245Behandling.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestArbeidsfordelingP\303\245Behandling.kt" new file mode 100644 index 000000000..d87ea754f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestArbeidsfordelingP\303\245Behandling.kt" @@ -0,0 +1,15 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.domene.ArbeidsfordelingPåBehandling + +data class RestArbeidsfordelingPåBehandling( + val behandlendeEnhetId: String, + val behandlendeEnhetNavn: String, + val manueltOverstyrt: Boolean = false, +) + +fun ArbeidsfordelingPåBehandling.tilRestArbeidsfordelingPåBehandling() = RestArbeidsfordelingPåBehandling( + behandlendeEnhetId = this.behandlendeEnhetId, + behandlendeEnhetNavn = this.behandlendeEnhetNavn, + manueltOverstyrt = this.manueltOverstyrt, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestBehandlingStegTilstand.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestBehandlingStegTilstand.kt new file mode 100644 index 000000000..68ac57be3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestBehandlingStegTilstand.kt @@ -0,0 +1,16 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.steg.BehandlingStegStatus +import no.nav.familie.ba.sak.kjerne.steg.StegType + +class RestBehandlingStegTilstand( + val behandlingSteg: StegType, + val behandlingStegStatus: BehandlingStegStatus, +) + +fun BehandlingStegTilstand.tilRestBehandlingStegTilstand() = + RestBehandlingStegTilstand( + behandlingSteg = this.behandlingSteg, + behandlingStegStatus = this.behandlingStegStatus, + ) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestBrevmottaker.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestBrevmottaker.kt new file mode 100644 index 000000000..0f78e8708 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestBrevmottaker.kt @@ -0,0 +1,26 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.brev.mottaker.Brevmottaker +import no.nav.familie.ba.sak.kjerne.brev.mottaker.MottakerType + +data class RestBrevmottaker( + val id: Long?, + val type: MottakerType, + val navn: String, + val adresselinje1: String, + val adresselinje2: String?, + val postnummer: String, + val poststed: String, + val landkode: String, +) + +fun RestBrevmottaker.tilBrevMottaker(behandlingId: Long) = Brevmottaker( + behandlingId = behandlingId, + type = type, + navn = navn, + adresselinje1 = adresselinje1, + adresselinje2 = adresselinje2, + postnummer = postnummer.trim(), + poststed = poststed.trim(), + landkode = landkode, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestEndretUtbetalingAndel.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestEndretUtbetalingAndel.kt new file mode 100644 index 000000000..9a8517d85 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestEndretUtbetalingAndel.kt @@ -0,0 +1,19 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class RestEndretUtbetalingAndel( + val id: Long?, + val personIdent: String?, + val prosent: BigDecimal?, + val fom: YearMonth?, + val tom: YearMonth?, + val årsak: Årsak?, + val avtaletidspunktDeltBosted: LocalDate?, + val søknadstidspunkt: LocalDate?, + val begrunnelse: String?, + val erTilknyttetAndeler: Boolean?, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestFagsak.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestFagsak.kt new file mode 100644 index 000000000..9a0aa9f6a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestFagsak.kt @@ -0,0 +1,129 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.tilbakekreving.domene.RestTilbakekrevingsbehandling +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Utbetalingsperiode +import java.time.LocalDate +import java.time.LocalDateTime + +open class RestBaseFagsak( + open val opprettetTidspunkt: LocalDateTime, + open val id: Long, + open val søkerFødselsnummer: String, + open val status: FagsakStatus, + open val underBehandling: Boolean, + open val løpendeKategori: BehandlingKategori?, + open val løpendeUnderkategori: BehandlingUnderkategori?, + open val gjeldendeUtbetalingsperioder: List, + open val fagsakType: FagsakType = FagsakType.NORMAL, + open val institusjon: InstitusjonInfo? = null, +) + +fun Fagsak.tilRestBaseFagsak( + underBehandling: Boolean, + gjeldendeUtbetalingsperioder: List = emptyList(), + løpendeKategori: BehandlingKategori?, + løpendeUnderkategori: BehandlingUnderkategori?, +): RestBaseFagsak = RestBaseFagsak( + opprettetTidspunkt = this.opprettetTidspunkt, + id = this.id, + søkerFødselsnummer = this.aktør.aktivFødselsnummer(), + status = this.status, + underBehandling = underBehandling, + løpendeKategori = løpendeKategori, + løpendeUnderkategori = løpendeUnderkategori, + gjeldendeUtbetalingsperioder = gjeldendeUtbetalingsperioder, + fagsakType = this.type, +) + +data class RestFagsak( + override val opprettetTidspunkt: LocalDateTime, + override val id: Long, + override val søkerFødselsnummer: String, + override val status: FagsakStatus, + override val underBehandling: Boolean, + override val løpendeKategori: BehandlingKategori?, + override val løpendeUnderkategori: BehandlingUnderkategori?, + override val gjeldendeUtbetalingsperioder: List, + val behandlinger: List, + val tilbakekrevingsbehandlinger: List, + override val fagsakType: FagsakType = FagsakType.NORMAL, +) : RestBaseFagsak( + opprettetTidspunkt = opprettetTidspunkt, + id = id, + søkerFødselsnummer = søkerFødselsnummer, + status = status, + underBehandling = underBehandling, + løpendeKategori = løpendeKategori, + løpendeUnderkategori = løpendeUnderkategori, + gjeldendeUtbetalingsperioder = gjeldendeUtbetalingsperioder, + fagsakType = fagsakType, +) + +fun RestBaseFagsak.tilRestFagsak( + restUtvidetBehandlinger: List, + tilbakekrevingsbehandlinger: List, +) = RestFagsak( + opprettetTidspunkt = this.opprettetTidspunkt, + id = this.id, + søkerFødselsnummer = this.søkerFødselsnummer, + status = this.status, + underBehandling = this.underBehandling, + løpendeKategori = this.løpendeKategori, + løpendeUnderkategori = this.løpendeUnderkategori, + gjeldendeUtbetalingsperioder = this.gjeldendeUtbetalingsperioder, + behandlinger = restUtvidetBehandlinger, + tilbakekrevingsbehandlinger = tilbakekrevingsbehandlinger, + fagsakType = this.fagsakType, +) + +data class RestMinimalFagsak( + override val opprettetTidspunkt: LocalDateTime, + override val id: Long, + override val søkerFødselsnummer: String, + override val status: FagsakStatus, + override val løpendeKategori: BehandlingKategori?, + override val løpendeUnderkategori: BehandlingUnderkategori?, + override val underBehandling: Boolean, + override val gjeldendeUtbetalingsperioder: List, + val behandlinger: List, + val tilbakekrevingsbehandlinger: List, + val migreringsdato: LocalDate? = null, + override val fagsakType: FagsakType, + override val institusjon: InstitusjonInfo?, +) : RestBaseFagsak( + opprettetTidspunkt = opprettetTidspunkt, + id = id, + søkerFødselsnummer = søkerFødselsnummer, + status = status, + underBehandling = underBehandling, + løpendeKategori = løpendeKategori, + løpendeUnderkategori = løpendeUnderkategori, + gjeldendeUtbetalingsperioder = gjeldendeUtbetalingsperioder, + fagsakType = fagsakType, + institusjon = institusjon, +) + +fun RestBaseFagsak.tilRestMinimalFagsak( + restVisningBehandlinger: List, + tilbakekrevingsbehandlinger: List, + migreringsdato: LocalDate?, +) = RestMinimalFagsak( + opprettetTidspunkt = this.opprettetTidspunkt, + id = this.id, + søkerFødselsnummer = this.søkerFødselsnummer, + status = this.status, + underBehandling = this.underBehandling, + løpendeKategori = this.løpendeKategori, + løpendeUnderkategori = this.løpendeUnderkategori, + gjeldendeUtbetalingsperioder = this.gjeldendeUtbetalingsperioder, + behandlinger = restVisningBehandlinger, + tilbakekrevingsbehandlinger = tilbakekrevingsbehandlinger, + migreringsdato = migreringsdato, + fagsakType = this.fagsakType, + institusjon = this.institusjon, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestFagsakS\303\270k.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestFagsakS\303\270k.kt" new file mode 100644 index 000000000..611b4e773 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestFagsakS\303\270k.kt" @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING + +data class RestSøkParam( + val personIdent: String, + val barnasIdenter: List = emptyList(), +) + +enum class FagsakDeltagerRolle { + BARN, + FORELDER, + UKJENT, +} + +data class RestFagsakDeltager( + val navn: String? = null, + val ident: String = "", + val rolle: FagsakDeltagerRolle, + val fagsakType: FagsakType? = null, + val kjønn: Kjønn? = Kjønn.UKJENT, + val fagsakId: Long? = null, + val fagsakStatus: FagsakStatus? = null, + val adressebeskyttelseGradering: ADRESSEBESKYTTELSEGRADERING? = null, + val harTilgang: Boolean = true, +) { + + override fun toString(): String { + return "RestFagsakDeltager(rolle=$rolle, kjønn=$kjønn, fagsakId=$fagsakId)" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestFeilutbetaltValuta.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestFeilutbetaltValuta.kt new file mode 100644 index 000000000..1c383eb00 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestFeilutbetaltValuta.kt @@ -0,0 +1,11 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import java.time.LocalDate + +data class RestFeilutbetaltValuta( + val id: Long?, + val fom: LocalDate, + val tom: LocalDate, + val feilutbetaltBeløp: Int, + val erPerMåned: Boolean? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestFerdigstillOppgaveKnyttJournalpost.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestFerdigstillOppgaveKnyttJournalpost.kt new file mode 100644 index 000000000..fb98f6374 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestFerdigstillOppgaveKnyttJournalpost.kt @@ -0,0 +1,20 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import java.time.LocalDateTime + +data class RestFerdigstillOppgaveKnyttJournalpost( + val journalpostId: String, + val tilknyttedeBehandlingIder: List = emptyList(), + val opprettOgKnyttTilNyBehandling: Boolean = false, + val navIdent: String, + val bruker: NavnOgIdent, + val nyBehandlingstype: BehandlingType, + val nyBehandlingsårsak: BehandlingÅrsak, + val kategori: BehandlingKategori?, + val underkategori: BehandlingUnderkategori?, + val datoMottatt: LocalDateTime?, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestF\303\270dselshendelsefiltreringResultat.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestF\303\270dselshendelsefiltreringResultat.kt" new file mode 100644 index 000000000..92b8325c5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestF\303\270dselshendelsefiltreringResultat.kt" @@ -0,0 +1,18 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.Filtreringsregel +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.domene.FødselshendelsefiltreringResultat + +data class RestFødselshendelsefiltreringResultat( + val filtreringsregel: Filtreringsregel, + val resultat: Resultat, + val begrunnelse: String, +) + +fun FødselshendelsefiltreringResultat.tilRestFødselshendelsefiltreringResultat() = + RestFødselshendelsefiltreringResultat( + filtreringsregel = this.filtreringsregel, + resultat = this.resultat, + begrunnelse = this.begrunnelse, + ) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestHentFagsakForPerson.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestHentFagsakForPerson.kt new file mode 100644 index 000000000..2593f0d8a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestHentFagsakForPerson.kt @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType + +data class RestHentFagsakForPerson(val personIdent: String, val fagsakType: FagsakType = FagsakType.NORMAL) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestHentFagsakerForPerson.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestHentFagsakerForPerson.kt new file mode 100644 index 000000000..35dd6d26e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestHentFagsakerForPerson.kt @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType + +data class RestHentFagsakerForPerson(val personIdent: String, val fagsakTyper: List = FagsakType.values().toList()) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestJournalf\303\270ring.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestJournalf\303\270ring.kt" new file mode 100644 index 000000000..502bd96ed --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestJournalf\303\270ring.kt" @@ -0,0 +1,79 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.Bruker +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.OppdaterJournalpostRequest +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.kontrakter.felles.BrukerIdType +import no.nav.familie.kontrakter.felles.dokarkiv.AvsenderMottaker +import no.nav.familie.kontrakter.felles.journalpost.DokumentInfo +import no.nav.familie.kontrakter.felles.journalpost.Dokumentstatus +import no.nav.familie.kontrakter.felles.journalpost.LogiskVedlegg +import no.nav.familie.kontrakter.felles.journalpost.Sak +import java.time.LocalDateTime + +data class RestJournalpostDokument( + val dokumentTittel: String?, + val dokumentInfoId: String, + val brevkode: String?, + val logiskeVedlegg: List?, + val eksisterendeLogiskeVedlegg: List?, +) + +data class RestJournalføring( + val avsender: NavnOgIdent, + val bruker: NavnOgIdent, + val datoMottatt: LocalDateTime?, + val journalpostTittel: String?, + val kategori: BehandlingKategori?, + val underkategori: BehandlingUnderkategori?, + val knyttTilFagsak: Boolean, + val opprettOgKnyttTilNyBehandling: Boolean, + val tilknyttedeBehandlingIder: List, + val dokumenter: List, + val navIdent: String, + val nyBehandlingstype: BehandlingType, + val nyBehandlingsårsak: BehandlingÅrsak, + val fagsakType: FagsakType, + val institusjon: InstitusjonInfo? = null, +) { + + fun oppdaterMedDokumentOgSak(sak: Sak): OppdaterJournalpostRequest { + return OppdaterJournalpostRequest( + avsenderMottaker = AvsenderMottaker( + id = this.avsender.id, + idType = if (this.avsender.id != "") BrukerIdType.FNR else null, + navn = this.avsender.navn, + ), + bruker = Bruker( + this.bruker.id, + navn = this.bruker.navn, + ), + sak = sak, + tittel = this.journalpostTittel, + dokumenter = dokumenter.map { dokument -> + DokumentInfo( + dokumentInfoId = dokument.dokumentInfoId, + tittel = dokument.dokumentTittel, + brevkode = dokument.brevkode, + dokumentstatus = Dokumentstatus.FERDIGSTILT, + dokumentvarianter = null, + logiskeVedlegg = null, + ) + }, + ) + } + + fun hentUnderkategori(): BehandlingUnderkategori { + if (underkategori is BehandlingUnderkategori) return underkategori + return when { + journalpostTittel?.contains("ordinær") == true -> BehandlingUnderkategori.ORDINÆR + journalpostTittel?.contains("utvidet") == true -> BehandlingUnderkategori.UTVIDET + // Defaulter til ordinær inntil videre. + else -> BehandlingUnderkategori.ORDINÆR + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestKompetanse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestKompetanse.kt new file mode 100644 index 000000000..0d78cfada --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestKompetanse.kt @@ -0,0 +1,79 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import java.time.YearMonth + +data class RestKompetanse( + val id: Long, + val fom: YearMonth?, + val tom: YearMonth?, + val barnIdenter: List, + val søkersAktivitet: KompetanseAktivitet? = null, + val søkersAktivitetsland: String? = null, + val annenForeldersAktivitet: KompetanseAktivitet? = null, + val annenForeldersAktivitetsland: String? = null, + val barnetsBostedsland: String? = null, + val resultat: KompetanseResultat? = null, + override val status: UtfyltStatus = UtfyltStatus.IKKE_UTFYLT, + val erAnnenForelderOmfattetAvNorskLovgivning: Boolean? = false, +) : AbstractUtfyltStatus() { + override fun medUtfyltStatus(): RestKompetanse { + var antallUtfylteFelter = finnAntallUtfylt( + listOf( + this.annenForeldersAktivitet, + this.barnetsBostedsland, + this.annenForeldersAktivitetsland, + this.resultat, + this.søkersAktivitet, + this.søkersAktivitetsland, + ), + ) + if (annenForeldersAktivitetsland == null) { + antallUtfylteFelter += ( + if (annenForeldersAktivitet in listOf( + KompetanseAktivitet.INAKTIV, + KompetanseAktivitet.IKKE_AKTUELT, + ) + ) { + 1 + } else { + 0 + } + ) + } + if (søkersAktivitetsland == null) { + antallUtfylteFelter += (if (søkersAktivitet == KompetanseAktivitet.INAKTIV) 1 else 0) + } + return this.copy(status = utfyltStatus(antallUtfylteFelter, 6)) + } +} + +fun Kompetanse.tilRestKompetanse() = RestKompetanse( + id = this.id, + fom = this.fom, + tom = this.tom, + barnIdenter = this.barnAktører.map { it.aktivFødselsnummer() }, + søkersAktivitet = this.søkersAktivitet, + søkersAktivitetsland = this.søkersAktivitetsland, + annenForeldersAktivitet = this.annenForeldersAktivitet, + annenForeldersAktivitetsland = this.annenForeldersAktivitetsland, + barnetsBostedsland = this.barnetsBostedsland, + resultat = this.resultat, + erAnnenForelderOmfattetAvNorskLovgivning = this.erAnnenForelderOmfattetAvNorskLovgivning, +).medUtfyltStatus() + +fun RestKompetanse.tilKompetanse(barnAktører: List) = Kompetanse( + fom = this.fom, + tom = this.tom, + barnAktører = barnAktører.toSet(), + søkersAktivitet = this.søkersAktivitet, + søkersAktivitetsland = this.søkersAktivitetsland, + annenForeldersAktivitet = this.annenForeldersAktivitet, + annenForeldersAktivitetsland = this.annenForeldersAktivitetsland, + barnetsBostedsland = this.barnetsBostedsland, + resultat = this.resultat, + erAnnenForelderOmfattetAvNorskLovgivning = this.erAnnenForelderOmfattetAvNorskLovgivning, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestKorrigertEtterbetaling.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestKorrigertEtterbetaling.kt new file mode 100644 index 000000000..d7d378346 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestKorrigertEtterbetaling.kt @@ -0,0 +1,24 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.korrigertetterbetaling.KorrigertEtterbetaling +import no.nav.familie.ba.sak.kjerne.korrigertetterbetaling.KorrigertEtterbetalingÅrsak +import java.time.LocalDateTime + +data class RestKorrigertEtterbetaling( + val id: Long, + val årsak: KorrigertEtterbetalingÅrsak, + val begrunnelse: String?, + val opprettetTidspunkt: LocalDateTime, + val beløp: Int, + val aktiv: Boolean, +) + +fun KorrigertEtterbetaling.tilRestKorrigertEtterbetaling(): RestKorrigertEtterbetaling = + RestKorrigertEtterbetaling( + id = id, + årsak = årsak, + begrunnelse = begrunnelse, + opprettetTidspunkt = opprettetTidspunkt, + beløp = beløp, + aktiv = aktiv, + ) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestKorrigertVedtak.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestKorrigertVedtak.kt new file mode 100644 index 000000000..197f3a9de --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestKorrigertVedtak.kt @@ -0,0 +1,18 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.korrigertvedtak.KorrigertVedtak +import java.time.LocalDate + +class RestKorrigertVedtak( + val id: Long, + val vedtaksdato: LocalDate?, + val begrunnelse: String?, + val aktiv: Boolean, +) + +fun KorrigertVedtak.tilRestKorrigertVedtak(): RestKorrigertVedtak = RestKorrigertVedtak( + id = id, + vedtaksdato = vedtaksdato, + begrunnelse = begrunnelse, + aktiv = aktiv, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestManuellD\303\270dsfall.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestManuellD\303\270dsfall.kt" new file mode 100644 index 000000000..464ed7127 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestManuellD\303\270dsfall.kt" @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import java.time.LocalDate + +data class RestManuellDødsfall( + val dødsfallDato: LocalDate, + val personIdent: String, + val begrunnelse: String, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestNyttVilk\303\245r.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestNyttVilk\303\245r.kt" new file mode 100644 index 000000000..ef2ff87ee --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestNyttVilk\303\245r.kt" @@ -0,0 +1,8 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår + +data class RestNyttVilkår( + val personIdent: String, + val vilkårType: Vilkår, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestOppdaterJournalpost.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestOppdaterJournalpost.kt new file mode 100644 index 000000000..466471312 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestOppdaterJournalpost.kt @@ -0,0 +1,55 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.Bruker +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.OppdaterJournalpostRequest +import no.nav.familie.kontrakter.felles.BrukerIdType +import no.nav.familie.kontrakter.felles.dokarkiv.AvsenderMottaker +import no.nav.familie.kontrakter.felles.journalpost.DokumentInfo +import no.nav.familie.kontrakter.felles.journalpost.Dokumentstatus +import no.nav.familie.kontrakter.felles.journalpost.LogiskVedlegg +import no.nav.familie.kontrakter.felles.journalpost.Sak +import java.time.LocalDateTime + +data class RestOppdaterJournalpost( + val avsender: NavnOgIdent, + val bruker: NavnOgIdent, + val datoMottatt: LocalDateTime?, + val dokumentTittel: String, + val dokumentInfoId: String, + val knyttTilFagsak: Boolean, + val tilknyttedeBehandlingIder: List, + val eksisterendeLogiskeVedlegg: List, + val logiskeVedlegg: List, + val navIdent: String, +) { + fun oppdaterMedDokumentOgSak(sak: Sak): OppdaterJournalpostRequest { + val dokument = DokumentInfo( + dokumentInfoId = this.dokumentInfoId, + tittel = this.dokumentTittel, + brevkode = null, + dokumentstatus = Dokumentstatus.FERDIGSTILT, + dokumentvarianter = null, + logiskeVedlegg = null, + ) + + return OppdaterJournalpostRequest( + avsenderMottaker = AvsenderMottaker( + id = this.avsender.id, + idType = if (this.avsender.id != "") BrukerIdType.FNR else null, + navn = this.avsender.navn, + ), + bruker = Bruker( + this.bruker.id, + navn = this.bruker.navn, + ), + sak = sak, + tittel = this.dokumentTittel, + dokumenter = listOf(dokument), + ) + } +} + +data class NavnOgIdent( + val navn: String, + val id: String, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestPerson.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestPerson.kt new file mode 100644 index 000000000..99d2bfb7e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestPerson.kt @@ -0,0 +1,33 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import java.time.LocalDate + +/** + * NB: Bør ikke brukes internt, men kun ut mot eksterne tjenester siden klassen + * inneholder aktiv personIdent og ikke aktørId. + */ +data class RestPerson( + val type: PersonType, + val fødselsdato: LocalDate?, + val personIdent: String, + val navn: String, + val kjønn: Kjønn, + val registerhistorikk: RestRegisterhistorikk? = null, + val målform: Målform, + val dødsfallDato: LocalDate? = null, +) + +fun Person.tilRestPerson() = RestPerson( + type = this.type, + fødselsdato = this.fødselsdato, + personIdent = this.aktør.aktivFødselsnummer(), + navn = this.navn, + kjønn = this.kjønn, + registerhistorikk = this.tilRestRegisterhistorikk(), + målform = this.målform, + dødsfallDato = this.dødsfall?.dødsfallDato, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestPersonInfo.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestPersonInfo.kt new file mode 100644 index 000000000..79319d06c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestPersonInfo.kt @@ -0,0 +1,98 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjonMaskert +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import java.time.LocalDate + +data class RestPersonInfo( + val personIdent: String, + var fødselsdato: LocalDate? = null, + val navn: String? = null, + val kjønn: Kjønn? = null, + val adressebeskyttelseGradering: ADRESSEBESKYTTELSEGRADERING? = null, + var harTilgang: Boolean = true, + val forelderBarnRelasjon: List = emptyList(), + val forelderBarnRelasjonMaskert: List = emptyList(), + val kommunenummer: String = "ukjent", + val dødsfallDato: String? = null, + val bostedsadresse: RestBostedsadresse? = null, +) + +data class RestForelderBarnRelasjon( + val personIdent: String, + val relasjonRolle: FORELDERBARNRELASJONROLLE, + val navn: String, + val fødselsdato: LocalDate?, + val adressebeskyttelseGradering: ADRESSEBESKYTTELSEGRADERING? = null, +) + +data class RestForelderBarnRelasjonnMaskert( + val relasjonRolle: FORELDERBARNRELASJONROLLE, + val adressebeskyttelseGradering: ADRESSEBESKYTTELSEGRADERING, +) + +data class RestBostedsadresse( + val adresse: String?, + val postnummer: String, +) + +private fun ForelderBarnRelasjonMaskert.tilRestForelderBarnRelasjonMaskert() = RestForelderBarnRelasjonnMaskert( + relasjonRolle = this.relasjonsrolle, + adressebeskyttelseGradering = this.adressebeskyttelseGradering, +) + +private fun ForelderBarnRelasjon.tilRestForelderBarnRelasjon() = RestForelderBarnRelasjon( + personIdent = this.aktør.aktivFødselsnummer(), + relasjonRolle = this.relasjonsrolle, + navn = this.navn ?: "", + fødselsdato = this.fødselsdato, + adressebeskyttelseGradering = this.adressebeskyttelseGradering, + +) + +fun PersonInfo.tilRestPersonInfo(personIdent: String): RestPersonInfo { + val bostedsadresse = + this.bostedsadresser.filter { it.angittFlyttedato != null }.maxByOrNull { it.angittFlyttedato!! } + val kommunenummer: String = when { + bostedsadresse == null -> null + bostedsadresse.vegadresse != null -> bostedsadresse.vegadresse?.kommunenummer + bostedsadresse.matrikkeladresse != null -> bostedsadresse.matrikkeladresse?.kommunenummer + bostedsadresse.ukjentBosted != null -> null + else -> null + } ?: "ukjent" + + val dødsfallDato = if (this.dødsfall != null && this.dødsfall.erDød) this.dødsfall.dødsdato else null + + return RestPersonInfo( + personIdent = personIdent, + fødselsdato = this.fødselsdato, + navn = this.navn, + kjønn = this.kjønn, + adressebeskyttelseGradering = this.adressebeskyttelseGradering, + forelderBarnRelasjon = this.forelderBarnRelasjon.map { it.tilRestForelderBarnRelasjon() }, + forelderBarnRelasjonMaskert = this.forelderBarnRelasjonMaskert.map { it.tilRestForelderBarnRelasjonMaskert() }, + kommunenummer = kommunenummer, + dødsfallDato = dødsfallDato, + ) +} + +fun PersonInfo.tilRestPersonInfoMedNavnOgAdresse(personIdent: String): RestPersonInfo { + val bostedsadresse = this.bostedsadresser.singleOrNull() // det skal kun være en bostedsadresse i PersonInfo uten historikk + val postnummer = bostedsadresse?.vegadresse?.postnummer ?: bostedsadresse?.matrikkeladresse?.postnummer + return RestPersonInfo( + personIdent = personIdent, + fødselsdato = this.fødselsdato, + navn = this.navn, + bostedsadresse = when (postnummer) { + null -> null + else -> RestBostedsadresse( + adresse = bostedsadresse?.vegadresse?.adressenavn?.plus(" ${bostedsadresse.vegadresse?.husnummer ?: ""}")?.trim(), + postnummer = postnummer, + ) + }, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestPersonResultat.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestPersonResultat.kt new file mode 100644 index 000000000..5727cf8cb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestPersonResultat.kt @@ -0,0 +1,74 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelseDeserializer +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.ResultatBegrunnelse +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import java.time.LocalDate +import java.time.LocalDateTime + +data class RestPersonResultat( + val personIdent: String, + val vilkårResultater: List, + val andreVurderinger: List = emptyList(), +) + +data class RestVilkårResultat( + val id: Long, + val vilkårType: Vilkår, + val resultat: Resultat, + val periodeFom: LocalDate?, + val periodeTom: LocalDate?, + val begrunnelse: String, + val endretAv: String, + val endretTidspunkt: LocalDateTime, + val behandlingId: Long, + val erVurdert: Boolean = false, + val erAutomatiskVurdert: Boolean = false, + val erEksplisittAvslagPåSøknad: Boolean? = null, + @JsonDeserialize(using = IVedtakBegrunnelseDeserializer::class) + val avslagBegrunnelser: List? = emptyList(), + val vurderesEtter: Regelverk? = null, + val utdypendeVilkårsvurderinger: List = emptyList(), + val resultatBegrunnelse: ResultatBegrunnelse? = null, +) { + + fun erAvslagUtenPeriode() = + this.erEksplisittAvslagPåSøknad == true && this.periodeFom == null && this.periodeTom == null + + fun harFremtidigTom() = this.periodeTom == null || this.periodeTom.isAfter(LocalDate.now().sisteDagIMåned()) +} + +fun PersonResultat.tilRestPersonResultat() = + RestPersonResultat( + personIdent = this.aktør.aktivFødselsnummer(), + vilkårResultater = this.vilkårResultater.map { vilkårResultat -> + RestVilkårResultat( + resultat = vilkårResultat.resultat, + resultatBegrunnelse = vilkårResultat.resultatBegrunnelse, + erAutomatiskVurdert = vilkårResultat.erAutomatiskVurdert, + erEksplisittAvslagPåSøknad = vilkårResultat.erEksplisittAvslagPåSøknad, + id = vilkårResultat.id, + vilkårType = vilkårResultat.vilkårType, + periodeFom = vilkårResultat.periodeFom, + periodeTom = vilkårResultat.periodeTom, + begrunnelse = vilkårResultat.begrunnelse, + endretAv = vilkårResultat.endretAv, + endretTidspunkt = vilkårResultat.endretTidspunkt, + behandlingId = vilkårResultat.sistEndretIBehandlingId, + erVurdert = vilkårResultat.resultat != Resultat.IKKE_VURDERT || vilkårResultat.versjon > 0, + avslagBegrunnelser = vilkårResultat.standardbegrunnelser, + vurderesEtter = vilkårResultat.vurderesEtter, + utdypendeVilkårsvurderinger = vilkårResultat.utdypendeVilkårsvurderinger, + ) + }, + andreVurderinger = this.andreVurderinger.map { annenVurdering -> + annenVurdering.tilRestAnnenVurdering() + }, + ) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestRefusjonE\303\270s.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestRefusjonE\303\270s.kt" new file mode 100644 index 000000000..d8eeba818 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestRefusjonE\303\270s.kt" @@ -0,0 +1,12 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import java.time.LocalDate + +data class RestRefusjonEøs( + val id: Long?, + val fom: LocalDate, + val tom: LocalDate, + val refusjonsbeløp: Int, + val land: String, + val refusjonAvklart: Boolean, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestRegisterhistorikk.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestRegisterhistorikk.kt new file mode 100644 index 000000000..6b502731a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestRegisterhistorikk.kt @@ -0,0 +1,42 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import java.time.LocalDate +import java.time.LocalDateTime + +data class RestRegisterhistorikk( + val hentetTidspunkt: LocalDateTime, + val sivilstand: List? = emptyList(), + val oppholdstillatelse: List? = emptyList(), + val statsborgerskap: List? = emptyList(), + val bostedsadresse: List? = emptyList(), + val dødsboadresse: List? = emptyList(), +) + +fun Person.tilRestRegisterhistorikk() = RestRegisterhistorikk( + hentetTidspunkt = this.personopplysningGrunnlag.opprettetTidspunkt, + oppholdstillatelse = opphold.map { it.tilRestRegisteropplysning() }, + statsborgerskap = statsborgerskap.map { it.tilRestRegisteropplysning() }, + bostedsadresse = this.bostedsadresser.map { it.tilRestRegisteropplysning() }.fyllInnTomDatoer(), + sivilstand = this.sivilstander.map { it.tilRestRegisteropplysning() }, + dødsboadresse = if (this.dødsfall == null) emptyList() else listOf(this.dødsfall!!.tilRestRegisteropplysning()), +) + +data class RestRegisteropplysning( + val fom: LocalDate?, + val tom: LocalDate?, + var verdi: String, +) + +fun List.fyllInnTomDatoer(): List = + this + .sortedBy { it.fom } + .foldRight(mutableListOf()) { foregående, acc -> + if (acc.isEmpty() || foregående.tom != null || foregående.fom == null) { + acc.add(foregående) + } else { + acc.add(foregående.copy(tom = acc.last().fom?.minusDays(1))) + } + acc + } + .reversed() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestRegistrerInstitusjonOgVerge.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestRegistrerInstitusjonOgVerge.kt new file mode 100644 index 000000000..dacb1517d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestRegistrerInstitusjonOgVerge.kt @@ -0,0 +1,33 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.institusjon.Institusjon +import no.nav.familie.ba.sak.kjerne.verge.Verge + +data class VergeInfo(val ident: String) + +data class InstitusjonInfo(val orgNummer: String?, val tssEksternId: String?, val navn: String? = null) + +data class RestRegistrerInstitusjonOgVerge( + val vergeInfo: VergeInfo?, + val institusjonInfo: InstitusjonInfo?, +) { + + fun tilVerge(behandling: Behandling): Verge? = if (vergeInfo != null) { + Verge( + ident = vergeInfo.ident, + behandling = behandling, + ) + } else { + null + } + + fun tilInstitusjon(): Institusjon? = if (institusjonInfo?.orgNummer == null) { + null + } else { + Institusjon( + orgNummer = institusjonInfo.orgNummer, + tssEksternId = institusjonInfo.tssEksternId, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestSettP\303\245Vent.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestSettP\303\245Vent.kt" new file mode 100644 index 000000000..045d5d316 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestSettP\303\245Vent.kt" @@ -0,0 +1,15 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVent +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentÅrsak +import java.time.LocalDate + +data class RestSettPåVent( + val frist: LocalDate, + val årsak: SettPåVentÅrsak, +) + +fun SettPåVent.tilRestSettPåVent(): RestSettPåVent = RestSettPåVent( + frist = this.frist, + årsak = this.årsak, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestSlettVilk\303\245r.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestSlettVilk\303\245r.kt" new file mode 100644 index 000000000..d17e6f380 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestSlettVilk\303\245r.kt" @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår + +data class RestSlettVilkår(val personIdent: String, val vilkårType: Vilkår) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestTilbakekreving.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestTilbakekreving.kt new file mode 100644 index 000000000..f0c536311 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestTilbakekreving.kt @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg + +class RestTilbakekreving( + val valg: Tilbakekrevingsvalg, + val varsel: String? = null, + val begrunnelse: String, + val tilbakekrevingsbehandlingId: String? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestTilkjentYtelse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestTilkjentYtelse.kt new file mode 100644 index 000000000..d5ec00b17 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestTilkjentYtelse.kt @@ -0,0 +1,53 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.beregning.domene.slåSammenBack2BackAndelsperioderMedSammeBeløp +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +data class RestPersonMedAndeler( + val personIdent: String?, + val beløp: Int, + val stønadFom: YearMonth, + val stønadTom: YearMonth, + val ytelsePerioder: List, +) + +data class RestYtelsePeriode( + val beløp: Int, + val stønadFom: YearMonth, + val stønadTom: YearMonth, + val ytelseType: YtelseType, + val skalUtbetales: Boolean, +) + +fun PersonopplysningGrunnlag.tilRestPersonerMedAndeler(andelerKnyttetTilPersoner: List): List = + andelerKnyttetTilPersoner + .groupBy { it.aktør } + .map { andelerForPerson -> + val personId = andelerForPerson.key + val andeler = andelerForPerson.value + + val sammenslåtteAndeler = + andeler.groupBy { it.type }.flatMap { it.value.slåSammenBack2BackAndelsperioderMedSammeBeløp() } + + RestPersonMedAndeler( + personIdent = this.søkerOgBarn.find { person -> person.aktør == personId }?.aktør?.aktivFødselsnummer(), + beløp = sammenslåtteAndeler.sumOf { it.kalkulertUtbetalingsbeløp }, + stønadFom = sammenslåtteAndeler.map { it.stønadFom }.minOrNull() ?: LocalDate.MIN.toYearMonth(), + stønadTom = sammenslåtteAndeler.map { it.stønadTom }.maxOrNull() ?: LocalDate.MAX.toYearMonth(), + ytelsePerioder = sammenslåtteAndeler.map { it1 -> + RestYtelsePeriode( + beløp = it1.kalkulertUtbetalingsbeløp, + stønadFom = it1.stønadFom, + stønadTom = it1.stønadTom, + ytelseType = it1.type, + skalUtbetales = it1.prosent > BigDecimal.ZERO, + ) + }, + ) + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestTotrinnskontroll.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestTotrinnskontroll.kt new file mode 100644 index 000000000..9cfed2448 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestTotrinnskontroll.kt @@ -0,0 +1,18 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.domene.Totrinnskontroll +import java.time.LocalDateTime + +data class RestTotrinnskontroll( + val saksbehandler: String, + val beslutter: String? = null, + val godkjent: Boolean = false, + val opprettetTidspunkt: LocalDateTime, +) + +fun Totrinnskontroll.tilRestTotrinnskontroll() = RestTotrinnskontroll( + saksbehandler = this.saksbehandler, + beslutter = this.beslutter, + godkjent = this.godkjent, + opprettetTidspunkt = this.opprettetTidspunkt, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestUtenlandskPeriodebel\303\270p.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestUtenlandskPeriodebel\303\270p.kt" new file mode 100644 index 000000000..d435088a6 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestUtenlandskPeriodebel\303\270p.kt" @@ -0,0 +1,71 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import jakarta.validation.constraints.DecimalMin +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Intervall +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.konverterBeløpTilMånedlig +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløp +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import java.math.BigDecimal +import java.time.YearMonth + +data class RestUtenlandskPeriodebeløp( + val id: Long, + val fom: YearMonth?, + val tom: YearMonth?, + val barnIdenter: List, + @field:DecimalMin(value = "0.0", message = "Beløp kan ikke være negativt.") val beløp: BigDecimal?, + val valutakode: String?, + val intervall: Intervall?, + val kalkulertMånedligBeløp: BigDecimal?, + override val status: UtfyltStatus = UtfyltStatus.IKKE_UTFYLT, +) : AbstractUtfyltStatus() { + override fun medUtfyltStatus(): RestUtenlandskPeriodebeløp { + return this.copy( + status = utfyltStatus( + finnAntallUtfylt(listOf(this.beløp, this.valutakode, this.intervall)), + 3, + ), + ) + } +} + +fun RestUtenlandskPeriodebeløp.tilUtenlandskPeriodebeløp( + barnAktører: List, + eksisterendeUtenlandskPeriodebeløp: UtenlandskPeriodebeløp, +) = UtenlandskPeriodebeløp( + fom = this.fom, + tom = this.tom, + barnAktører = barnAktører.toSet(), + beløp = this.beløp, + valutakode = this.valutakode, + intervall = this.intervall, + utbetalingsland = eksisterendeUtenlandskPeriodebeløp.utbetalingsland, + kalkulertMånedligBeløp = this.tilKalkulertMånedligBeløp(), +) + +fun RestUtenlandskPeriodebeløp.tilKalkulertMånedligBeløp(): BigDecimal? { + if (this.beløp == null || this.intervall == null) { + return null + } + + return this.intervall.konverterBeløpTilMånedlig(this.beløp) +} + +fun UtenlandskPeriodebeløp.tilKalkulertMånedligBeløp(): BigDecimal? { + if (this.beløp == null || this.intervall == null) { + return null + } + + return this.intervall.konverterBeløpTilMånedlig(this.beløp) +} + +fun UtenlandskPeriodebeløp.tilRestUtenlandskPeriodebeløp() = RestUtenlandskPeriodebeløp( + id = this.id, + fom = this.fom, + tom = this.tom, + barnIdenter = this.barnAktører.map { it.aktivFødselsnummer() }, + beløp = this.beløp, + valutakode = this.valutakode, + intervall = this.intervall, + kalkulertMånedligBeløp = this.kalkulertMånedligBeløp, +).medUtfyltStatus() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestUtvidetBehandling.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestUtvidetBehandling.kt new file mode 100644 index 000000000..e8059762f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestUtvidetBehandling.kt @@ -0,0 +1,48 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Utbetalingsperiode +import java.time.LocalDate +import java.time.LocalDateTime + +data class RestUtvidetBehandling( + val behandlingId: Long, + val steg: StegType, + val stegTilstand: List, + val status: BehandlingStatus, + val resultat: Behandlingsresultat, + val skalBehandlesAutomatisk: Boolean, + val type: BehandlingType, + val kategori: BehandlingKategori, + val underkategori: BehandlingUnderkategoriDTO, + val årsak: BehandlingÅrsak, + val opprettetTidspunkt: LocalDateTime, + val endretAv: String, + val arbeidsfordelingPåBehandling: RestArbeidsfordelingPåBehandling, + val søknadsgrunnlag: SøknadDTO?, + val personer: List, + val personResultater: List, + val fødselshendelsefiltreringResultater: List, + val utbetalingsperioder: List, + val personerMedAndelerTilkjentYtelse: List, + val endretUtbetalingAndeler: List, + val kompetanser: List, + val tilbakekreving: RestTilbakekreving?, + val vedtak: RestVedtak?, + val totrinnskontroll: RestTotrinnskontroll?, + val aktivSettPåVent: RestSettPåVent?, + val migreringsdato: LocalDate?, + val valutakurser: List, + val utenlandskePeriodebeløp: List, + val verge: VergeInfo?, + val korrigertEtterbetaling: RestKorrigertEtterbetaling?, + val korrigertVedtak: RestKorrigertVedtak?, + val feilutbetaltValuta: List, + val brevmottakere: List, + val refusjonEøs: List, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestValutakurs.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestValutakurs.kt new file mode 100644 index 000000000..829fee366 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestValutakurs.kt @@ -0,0 +1,41 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.eøs.valutakurs.Valutakurs +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +data class RestValutakurs( + val id: Long, + val fom: YearMonth?, + val tom: YearMonth?, + val barnIdenter: List, + val valutakursdato: LocalDate?, + val valutakode: String?, + val kurs: BigDecimal?, + override val status: UtfyltStatus = UtfyltStatus.IKKE_UTFYLT, +) : AbstractUtfyltStatus() { + override fun medUtfyltStatus(): RestValutakurs { + return this.copy(status = utfyltStatus(finnAntallUtfylt(listOf(this.valutakursdato, this.kurs)), 2)) + } +} + +fun RestValutakurs.tilValutakurs(barnAktører: List) = Valutakurs( + fom = this.fom, + tom = this.tom, + barnAktører = barnAktører.toSet(), + valutakursdato = this.valutakursdato, + valutakode = this.valutakode, + kurs = this.kurs, +) + +fun Valutakurs.tilRestValutakurs() = RestValutakurs( + id = this.id, + fom = this.fom, + tom = this.tom, + barnIdenter = this.barnAktører.map { it.aktivFødselsnummer() }, + valutakursdato = this.valutakursdato, + valutakode = this.valutakode, + kurs = this.kurs, +).medUtfyltStatus() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestVedtak.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestVedtak.kt new file mode 100644 index 000000000..09d3710ed --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestVedtak.kt @@ -0,0 +1,24 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import java.time.LocalDateTime + +data class RestVedtak( + val aktiv: Boolean, + val vedtaksdato: LocalDateTime?, + val id: Long, +) + +data class RestVedtakBegrunnelseTilknyttetVilkår( + val id: String, + val navn: String, + val vilkår: Vilkår?, +) + +fun Vedtak.tilRestVedtak() = + RestVedtak( + aktiv = this.aktiv, + vedtaksdato = this.vedtaksdato, + id = this.id, + ) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestVedtakBarn.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestVedtakBarn.kt new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestVedtaksperiodeMedBegrunnelser.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestVedtaksperiodeMedBegrunnelser.kt new file mode 100644 index 000000000..a286474de --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestVedtaksperiodeMedBegrunnelser.kt @@ -0,0 +1,21 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import java.time.LocalDate + +data class RestPutVedtaksperiodeMedFritekster( + val fritekster: List = emptyList(), +) + +data class RestPutVedtaksperiodeMedStandardbegrunnelser( + val standardbegrunnelser: List, +) + +data class RestGenererVedtaksperioderForOverstyrtEndringstidspunkt( + val behandlingId: Long, + val overstyrtEndringstidspunkt: LocalDate, +) + +data class RestPutGenererFortsattInnvilgetVedtaksperioder( + val skalGenererePerioderForFortsattInnvilget: Boolean, + val behandlingId: Long, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestVisningBehandling.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestVisningBehandling.kt new file mode 100644 index 000000000..31a3058de --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/RestVisningBehandling.kt @@ -0,0 +1,53 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import java.time.LocalDateTime + +class RestVisningBehandling( + val behandlingId: Long, + val opprettetTidspunkt: LocalDateTime, + val aktivertTidspunkt: LocalDateTime, + val kategori: BehandlingKategori, + val underkategori: BehandlingUnderkategoriDTO, + val aktiv: Boolean, + val årsak: BehandlingÅrsak?, + val type: BehandlingType, + val status: BehandlingStatus, + val resultat: Behandlingsresultat, + val vedtaksdato: LocalDateTime?, +) + +fun Behandling.tilRestVisningBehandling(vedtaksdato: LocalDateTime?) = RestVisningBehandling( + behandlingId = this.id, + opprettetTidspunkt = this.opprettetTidspunkt, + aktivertTidspunkt = this.aktivertTidspunkt, + kategori = this.kategori, + underkategori = this.underkategori.tilDto(), + aktiv = this.aktiv, + årsak = this.opprettetÅrsak, + type = this.type, + status = this.status, + resultat = this.resultat, + vedtaksdato = vedtaksdato, +) + +enum class BehandlingUnderkategoriDTO { + ORDINÆR, + UTVIDET, +} + +fun BehandlingUnderkategoriDTO.tilDomene() = when (this) { + BehandlingUnderkategoriDTO.ORDINÆR -> BehandlingUnderkategori.ORDINÆR + BehandlingUnderkategoriDTO.UTVIDET -> BehandlingUnderkategori.UTVIDET +} + +fun BehandlingUnderkategori.tilDto() = when (this) { + BehandlingUnderkategori.ORDINÆR -> BehandlingUnderkategoriDTO.ORDINÆR + BehandlingUnderkategori.UTVIDET -> BehandlingUnderkategoriDTO.UTVIDET +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/S\303\270knadDTO.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/S\303\270knadDTO.kt" new file mode 100644 index 000000000..cc3d35a2d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/S\303\270knadDTO.kt" @@ -0,0 +1,33 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.kontrakter.felles.objectMapper +import java.time.LocalDate + +data class RestRegistrerSøknad( + val søknad: SøknadDTO, + val bekreftEndringerViaFrontend: Boolean, +) + +data class SøknadDTO( + val underkategori: BehandlingUnderkategoriDTO, + val søkerMedOpplysninger: SøkerMedOpplysninger, + val barnaMedOpplysninger: List, + val endringAvOpplysningerBegrunnelse: String, +) + +fun SøknadDTO.writeValueAsString(): String = objectMapper.writeValueAsString(this) + +data class SøkerMedOpplysninger( + val ident: String, + val målform: Målform = Målform.NB, +) + +data class BarnMedOpplysninger( + val ident: String, + val navn: String = "", + val fødselsdato: LocalDate? = null, + val inkludertISøknaden: Boolean = true, + val manueltRegistrert: Boolean = false, + val erFolkeregistrert: Boolean = true, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/TilgangDTO.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/TilgangDTO.kt new file mode 100644 index 000000000..e39549c0e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/restDomene/TilgangDTO.kt @@ -0,0 +1,8 @@ +package no.nav.familie.ba.sak.ekstern.restDomene + +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING + +data class TilgangDTO( + val saksbehandlerHarTilgang: Boolean, + val adressebeskyttelsegradering: ADRESSEBESKYTTELSEGRADERING, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/AndelTilkjentYtelsePeriode.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/AndelTilkjentYtelsePeriode.kt new file mode 100644 index 000000000..4bc9703f1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/AndelTilkjentYtelsePeriode.kt @@ -0,0 +1,20 @@ +package no.nav.familie.ba.sak.ekstern.skatteetaten + +import java.time.LocalDateTime + +interface AndelTilkjentYtelsePeriode { + + fun getId(): Long + + fun getIdent(): String + + fun getFom(): LocalDateTime + + fun getTom(): LocalDateTime + + fun getProsent(): String + + fun getEndretDato(): LocalDateTime + + fun getBehandlingId(): Long +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/SkatteetatenController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/SkatteetatenController.kt new file mode 100644 index 000000000..e3a811c45 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/SkatteetatenController.kt @@ -0,0 +1,182 @@ +package no.nav.familie.ba.sak.ekstern.skatteetaten + +import jakarta.validation.Valid +import jakarta.validation.constraints.NotNull +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPeriode +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPeriode.Delingsprosent +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPerioder +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPerioderRequest +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPerioderResponse +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPerson +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPersonerResponse +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDateTime + +@RestController +@RequestMapping("/api/skatt") +@ProtectedWithClaims(issuer = "azuread") +class SkatteetatenController( + private val skatteetatenService: SkatteetatenService, + private val featureToggleService: FeatureToggleService, +) { + + @GetMapping( + value = ["/personer"], + produces = ["application/json;charset=UTF-8"], + ) + fun finnPersonerMedUtvidetBarnetrygd( + @NotNull + @RequestParam(value = "aar", required = true) + aar: String, + ): ResponseEntity> { + logger.info("Treff på finnPersonerMedUtvidetBarnetrygd") + val respons = if (featureToggleService.isEnabled(FeatureToggleConfig.SKATTEETATEN_API_EKTE_DATA)) { + skatteetatenService.finnPersonerMedUtvidetBarnetrygd(aar) + } else { + SkatteetatenPersonerResponse( + listeMedTestdataPerioder().filter { it.sisteVedtakPaaIdent.year == aar.toInt() } + .map { SkatteetatenPerson(it.ident, it.sisteVedtakPaaIdent) }, + ) + } + return ResponseEntity(Ressurs.success(respons), HttpStatus.valueOf(200)) + } + + @GetMapping( + value = ["/personer/test"], + produces = ["application/json;charset=UTF-8"], + ) + fun finnPersonerMedUtvidetBarnetrygdTest( + @NotNull + @RequestParam(value = "aar", required = true) + aar: String, + ): ResponseEntity> { + logger.info("Treff på finnPersonerMedUtvidetBarnetrygdTest") + val respons = skatteetatenService.finnPersonerMedUtvidetBarnetrygd(aar) + return ResponseEntity(Ressurs.success(respons), HttpStatus.valueOf(200)) + } + + @PostMapping( + value = ["/perioder"], + produces = ["application/json;charset=UTF-8"], + consumes = ["application/json"], + ) + fun hentPerioderMedUtvidetBarnetrygd( + @Valid @RequestBody + perioderRequest: SkatteetatenPerioderRequest, + ): ResponseEntity> { + logger.info("Treff på hentPerioderMedUtvidetBarnetrygd") + val response = if (featureToggleService.isEnabled(FeatureToggleConfig.SKATTEETATEN_API_EKTE_DATA)) { + skatteetatenService.finnPerioderMedUtvidetBarnetrygd(perioderRequest.identer, perioderRequest.aar) + } else { + SkatteetatenPerioderResponse(listeMedTestdataPerioder().filter { it.sisteVedtakPaaIdent.year == perioderRequest.aar.toInt() && it.ident in perioderRequest.identer }) + } + return ResponseEntity( + Ressurs.Companion.success(response), + HttpStatus.valueOf(200), + ) + } + + @PostMapping( + value = ["/perioder/test"], + produces = ["application/json;charset=UTF-8"], + consumes = ["application/json"], + ) + fun hentPerioderMedUtvidetBarnetrygdForMidlertidigTest( + @Valid @RequestBody + perioderRequest: SkatteetatenPerioderRequest, + ): ResponseEntity> { + logger.info("Treff på hentPerioderMedUtvidetBarnetrygdForMidlertidigTest") + val response = + skatteetatenService.finnPerioderMedUtvidetBarnetrygd(perioderRequest.identer, perioderRequest.aar) + + return ResponseEntity( + Ressurs.Companion.success(response), + HttpStatus.valueOf(200), + ) + } + + private fun listeMedTestdataPerioder(): List { + val fraMaaned = "2021-02" + return listOf( + SkatteetatenPerioder( + "01838398495", + LocalDateTime.of(2021, 1, 1, 0, 0), + perioder = listOf( + SkatteetatenPeriode(fraMaaned, Delingsprosent._50, tomMaaned = "2022-12"), + + ), + ), + + SkatteetatenPerioder( + "09919094319", + LocalDateTime.of(2021, 1, 1, 0, 0), + perioder = listOf( + SkatteetatenPeriode(fraMaaned, Delingsprosent._0, tomMaaned = "2024-12"), + + ), + ), + + SkatteetatenPerioder( + "15830699233", + LocalDateTime.of(2021, 1, 1, 0, 0), + perioder = listOf( + SkatteetatenPeriode(fraMaaned, Delingsprosent.usikker, tomMaaned = "2024-12"), + + ), + ), + + SkatteetatenPerioder( + "01828499633", + LocalDateTime.of(2021, 2, 1, 0, 0), + perioder = listOf( + SkatteetatenPeriode(fraMaaned, Delingsprosent._50, tomMaaned = null), + + ), + ), + + SkatteetatenPerioder( + "27903249671", + LocalDateTime.of(2021, 1, 1, 0, 0), + perioder = listOf( + SkatteetatenPeriode("2021-01", Delingsprosent._50, tomMaaned = "2021-03"), + SkatteetatenPeriode("2021-04", Delingsprosent._0, tomMaaned = "2021-08"), + SkatteetatenPeriode("2021-09", Delingsprosent.usikker, tomMaaned = null), + ), + ), + + SkatteetatenPerioder( + "24835498561", + LocalDateTime.of(2020, 1, 3, 0, 0), + perioder = listOf( + SkatteetatenPeriode("2020-01", Delingsprosent._50, tomMaaned = "2020-12"), + + ), + ), + + SkatteetatenPerioder( + "02889197172", + LocalDateTime.of(2019, 2, 1, 0, 0), + perioder = listOf( + SkatteetatenPeriode("2019-02", Delingsprosent._0, tomMaaned = "2019-09"), + + ), + ), + ) + } + + companion object { + private val logger = LoggerFactory.getLogger(SkatteetatenController::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/SkatteetatenService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/SkatteetatenService.kt new file mode 100644 index 000000000..cf8346d05 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/SkatteetatenService.kt @@ -0,0 +1,195 @@ +package no.nav.familie.ba.sak.ekstern.skatteetaten + +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdBarnetrygdClient +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.Behandlingutils +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPeriode +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPerioder +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPerioderResponse +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPerson +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPersonerResponse +import org.slf4j.LoggerFactory +import org.springframework.cache.annotation.Cacheable +import org.springframework.stereotype.Service +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.YearMonth +import java.time.format.DateTimeFormatter + +@Service +class SkatteetatenService( + private val infotrygdBarnetrygdClient: InfotrygdBarnetrygdClient, + private val fagsakRepository: FagsakRepository, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, +) { + @Cacheable("skatt_personer", cacheManager = "skattPersonerCache", unless = "#result == null") + fun finnPersonerMedUtvidetBarnetrygd(år: String): SkatteetatenPersonerResponse { + LOG.info("Kaller finnPersonerMedUtvidetBarnetrygd for år=$år") + val personerFraInfotrygd = infotrygdBarnetrygdClient.hentPersonerMedUtvidetBarnetrygd(år) + LOG.info("Hentet ${personerFraInfotrygd.brukere.size} saker med utvidet fra infotrygd i $år") + val personerFraBaSak = hentPersonerMedUtvidetBarnetrygd(år) + LOG.info("Hentet ${personerFraBaSak.size} saker med utvidet fra basak i $år") + + val personIdentSet = personerFraBaSak.map { it.ident }.toSet() + + // Assumes that vedtak in ba-sak is always newer than that in Infotrygd for the same person ident + val kombinertListe = + personerFraBaSak + personerFraInfotrygd.brukere.filter { !personIdentSet.contains(it.ident) } + + return SkatteetatenPersonerResponse(kombinertListe) + } + + fun finnPerioderMedUtvidetBarnetrygd(personer: List, år: String): SkatteetatenPerioderResponse { + val unikePersoner = personer.distinct() + val perioderFraBaSak = hentPerioderMedUtvidetBarnetrygdFraBaSak(unikePersoner, år) + LOG.info("Fant ${perioderFraBaSak.size} skatteetatenperioder fra ba-sak") + val perioderFraInfotrygd = + infotrygdBarnetrygdClient.hentPerioderMedUtvidetBarnetrygdForPersoner(unikePersoner, år) + LOG.info("Fant ${perioderFraInfotrygd.size} skatteetatenperioder fra infotrygd") + + val samletPerioder = mutableListOf() + unikePersoner.forEach { personIdent -> + val resultatInfotrygdForPerson = + perioderFraInfotrygd.firstOrNull { perioder -> perioder.ident == personIdent } + val perioderFraInfotrygdForPerson = + resultatInfotrygdForPerson?.perioder ?: emptyList() + + val resultatBaSakForPerson = perioderFraBaSak.firstOrNull { perioder -> perioder.ident == personIdent } + + val perioderFraBasak = resultatBaSakForPerson?.perioder ?: emptyList() + + val perioder = + (perioderFraBasak + perioderFraInfotrygdForPerson).groupBy { periode -> periode.delingsprosent }.values + .flatMap(::slåSammenSkatteetatenPeriode).toMutableList() + if (perioder.isNotEmpty()) { + samletPerioder.add( + SkatteetatenPerioder( + ident = personIdent, + perioder = perioder, + sisteVedtakPaaIdent = resultatBaSakForPerson?.sisteVedtakPaaIdent + ?: resultatInfotrygdForPerson!!.sisteVedtakPaaIdent, + ), + ) + } + } + + // Assumes that vedtak in ba-sak is always newer than that in Infotrygd for the same person ident + return SkatteetatenPerioderResponse(samletPerioder) + } + + private fun hentPersonerMedUtvidetBarnetrygd(år: String): List { + return fagsakRepository.finnFagsakerMedUtvidetBarnetrygdInnenfor( + fom = LocalDate.of(år.toInt(), 1, 1).atStartOfDay(), + tom = LocalDate.of(år.toInt() + 1, 1, 1).atStartOfDay(), + ) + .map { SkatteetatenPerson(it.fnr, it.sisteVedtaksdato.atStartOfDay()) } + } + + private fun hentPerioderMedUtvidetBarnetrygdFraBaSak( + personer: List, + år: String, + ): List { + val stonadPerioder = hentUtvidetStonadPerioderForPersoner(personer, år) + val aktivAndelTilkjentYtelsePeriode = mutableListOf() + stonadPerioder.groupBy { it.getId() }.values.forEach { perioderGroupedByPerson -> + if (perioderGroupedByPerson.size > 1) { + val behandlinger = + perioderGroupedByPerson.map { behandlingHentOgPersisterService.hent(behandlingId = it.getBehandlingId()) } + val sisteIverksatteBehandling = Behandlingutils.hentSisteBehandlingSomErIverksatt(behandlinger) + if (sisteIverksatteBehandling != null) { + aktivAndelTilkjentYtelsePeriode.addAll(perioderGroupedByPerson.filter { it.getBehandlingId() == sisteIverksatteBehandling.id }) + } + } else { + aktivAndelTilkjentYtelsePeriode.add(perioderGroupedByPerson.first()) + } + } + + val skatteetatenPerioderMap = + stonadPerioder.fold(mutableMapOf()) { perioderMap, period -> + val ident = period.getIdent() + val nyList = listOf( + SkatteetatenPeriode( + fraMaaned = period.getFom().format(DateTimeFormatter.ofPattern("yyyy-MM")), + delingsprosent = period.getProsent().tilDelingsprosent(), + tomMaaned = period.getTom().format(DateTimeFormatter.ofPattern("yyyy-MM")), + ), + ) + val samletPerioder = if (perioderMap.containsKey(ident)) { + perioderMap[ident]!!.perioder + nyList + } else { + nyList + } + perioderMap[ident] = SkatteetatenPerioder(ident, period.getEndretDato(), samletPerioder) + perioderMap + } + + return skatteetatenPerioderMap.toList().map { + // Slå sammen perioder basert på delingsprosent + SkatteetatenPerioder( + ident = it.second.ident, + sisteVedtakPaaIdent = it.second.sisteVedtakPaaIdent, + perioder = it.second.perioder.groupBy { periode -> periode.delingsprosent }.values + .flatMap(::slåSammenSkatteetatenPeriode).toMutableList(), + ) + } + } + + private fun hentUtvidetStonadPerioderForPersoner( + personIdenter: List, + år: String, + ): List { + val yearStart = LocalDateTime.of(år.toInt(), 1, 1, 0, 0, 0) + val yearEnd = LocalDateTime.of(år.toInt(), 12, 31, 23, 59, 59) + return andelTilkjentYtelseRepository.finnPerioderMedUtvidetBarnetrygdForPersoner( + personIdenter, + yearStart, + yearEnd, + ) + } + + private fun slåSammenSkatteetatenPeriode(perioderAvEtGittDelingsprosent: List): List { + return perioderAvEtGittDelingsprosent.sortedBy { it.fraMaaned } + .fold(mutableListOf()) { sammenslåttePerioder, nesteUtbetaling -> + val nesteUtbetalingFraMåned = YearMonth.parse(nesteUtbetaling.fraMaaned) + val forrigeUtbetalingTomMåned = + sammenslåttePerioder.lastOrNull()?.tomMaaned?.let { YearMonth.parse(it) } + + if (forrigeUtbetalingTomMåned?.isSameOrAfter(nesteUtbetalingFraMåned.minusMonths(1)) == true || (sammenslåttePerioder.isNotEmpty() && forrigeUtbetalingTomMåned == null)) { + val nySammenslåing = + sammenslåttePerioder.removeLast().copy(tomMaaned = nesteUtbetaling.tomMaaned) + sammenslåttePerioder.apply { add(nySammenslåing) } + } else { + sammenslåttePerioder.apply { add(nesteUtbetaling) } + } + } + } + + companion object { + val LOG = LoggerFactory.getLogger(SkatteetatenService::class.java) + } +} + +fun String.tilDelingsprosent(): SkatteetatenPeriode.Delingsprosent = + if (this == "100") { + SkatteetatenPeriode.Delingsprosent._0 + } else if (this == "50") { + SkatteetatenPeriode.Delingsprosent._50 + } else { + SkatteetatenPeriode.Delingsprosent.usikker + } + +fun SkatteetatenPeriode.Delingsprosent.tilBigDecimal(): BigDecimal = when (this) { + SkatteetatenPeriode.Delingsprosent._0 -> BigDecimal.valueOf(100) + SkatteetatenPeriode.Delingsprosent._50 -> BigDecimal.valueOf(50) + else -> BigDecimal.valueOf(0) +} + +interface UtvidetSkatt { + val fnr: String + val sisteVedtaksdato: LocalDate +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/tilbakekreving/FagsystemsbehandlingService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/tilbakekreving/FagsystemsbehandlingService.kt new file mode 100644 index 000000000..59a5182f4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/tilbakekreving/FagsystemsbehandlingService.kt @@ -0,0 +1,76 @@ +package no.nav.familie.ba.sak.ekstern.tilbakekreving + +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.tilbakekreving.TilbakekrevingService +import no.nav.familie.ba.sak.kjerne.tilbakekreving.hentTilbakekrevingInstitusjon +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.statistikk.producer.KafkaProducer +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandling +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRespons +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class FagsystemsbehandlingService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val persongrunnlagService: PersongrunnlagService, + private val arbeidsfordelingService: ArbeidsfordelingService, + private val vedtakService: VedtakService, + private val tilbakekrevingService: TilbakekrevingService, + private val kafkaProducer: KafkaProducer, +) { + + private val logger = LoggerFactory.getLogger(this::class.java) + + fun hentFagsystemsbehandling(request: HentFagsystemsbehandlingRequest): HentFagsystemsbehandlingRespons { + logger.info("Henter behandling for behandlingId=${request.eksternId}") + val behandling = behandlingHentOgPersisterService.hent(request.eksternId.toLong()) + + return lagRespons(request, behandling) + } + + fun sendFagsystemsbehandling( + respons: HentFagsystemsbehandlingRespons, + key: String, + behandlingId: String, + ) { + kafkaProducer.sendFagsystemsbehandlingResponsForTopicTilbakekreving(respons, key, behandlingId) + } + + private fun lagRespons( + request: HentFagsystemsbehandlingRequest, + behandling: Behandling, + ): HentFagsystemsbehandlingRespons { + val behandlingId = behandling.id + val persongrunnlag = persongrunnlagService.hentAktivThrows(behandlingId = behandlingId) + val arbeidsfordeling = arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId) + val vedtaksdato = vedtakService.hentVedtaksdatoForBehandlingThrows(behandlingId) + + val faktainfo = Faktainfo( + revurderingsårsak = behandling.opprettetÅrsak.visningsnavn, + revurderingsresultat = behandling.resultat.displayName, + tilbakekrevingsvalg = tilbakekrevingService.hentTilbakekrevingsvalg(behandlingId), + ) + + val hentFagsystemsbehandling = HentFagsystemsbehandling( + eksternFagsakId = request.eksternFagsakId, + eksternId = request.eksternId, + ytelsestype = request.ytelsestype, + regelverk = behandling.kategori.tilRegelverk(), + personIdent = behandling.fagsak.aktør.aktivFødselsnummer(), + språkkode = persongrunnlag.søker.målform.tilSpråkkode(), + enhetId = arbeidsfordeling.behandlendeEnhetId, + enhetsnavn = arbeidsfordeling.behandlendeEnhetNavn, + revurderingsvedtaksdato = vedtaksdato.toLocalDate(), + faktainfo = faktainfo, + institusjon = hentTilbakekrevingInstitusjon(behandling.fagsak), + ) + + return HentFagsystemsbehandlingRespons(hentFagsystemsbehandling = hentFagsystemsbehandling) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/tilbakekreving/HentFagsystemsbehandlingRequestConsumer.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/tilbakekreving/HentFagsystemsbehandlingRequestConsumer.kt new file mode 100644 index 000000000..04a5e7d49 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/ekstern/tilbakekreving/HentFagsystemsbehandlingRequestConsumer.kt @@ -0,0 +1,58 @@ +package no.nav.familie.ba.sak.ekstern.tilbakekreving + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRespons +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.slf4j.LoggerFactory +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.kafka.annotation.KafkaListener +import org.springframework.kafka.support.Acknowledgment +import org.springframework.stereotype.Service +import java.util.concurrent.CountDownLatch + +@Service +@ConditionalOnProperty( + value = ["funksjonsbrytere.kafka.producer.enabled"], + havingValue = "true", + matchIfMissing = false, +) +class HentFagsystemsbehandlingRequestConsumer(private val fagsystemsbehandlingService: FagsystemsbehandlingService) { + + private val logger = LoggerFactory.getLogger(this::class.java) + + var latch: CountDownLatch = CountDownLatch(1) + + @KafkaListener( + id = "familie-ba-sak", + topics = ["teamfamilie.privat-tbk-hentfagsystemsbehandling-request-topic"], + containerFactory = "concurrentKafkaListenerContainerFactory", + ) + fun listen(consumerRecord: ConsumerRecord, ack: Acknowledgment) { + val data: String = consumerRecord.value() + val key: String = consumerRecord.key() + val request: HentFagsystemsbehandlingRequest = + objectMapper.readValue(data, HentFagsystemsbehandlingRequest::class.java) + + if (request.ytelsestype != Ytelsestype.BARNETRYGD) { + return + } + logger.info("HentFagsystemsbehandlingRequest er mottatt i kafka $consumerRecord") + secureLogger.info("HentFagsystemsbehandlingRequest er mottatt i kafka $consumerRecord") + + val fagsystemsbehandling = try { + fagsystemsbehandlingService.hentFagsystemsbehandling(request) + } catch (e: Exception) { + logger.warn( + "Noe gikk galt mens sender HentFagsystemsbehandlingRespons for behandling=${request.eksternId}. " + + "Feiler med ${e.message}", + ) + HentFagsystemsbehandlingRespons(feilMelding = e.message) + } + fagsystemsbehandlingService.sendFagsystemsbehandling(fagsystemsbehandling, key, request.eksternId) + latch.countDown() + ack.acknowledge() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/ecb/ECBService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/ecb/ECBService.kt new file mode 100644 index 000000000..543b54e33 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/ecb/ECBService.kt @@ -0,0 +1,76 @@ +package no.nav.familie.ba.sak.integrasjoner.ecb + +import no.nav.familie.ba.sak.common.del +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.valutakurs.Frequency +import no.nav.familie.valutakurs.ValutakursRestClient +import no.nav.familie.valutakurs.domene.ExchangeRate +import no.nav.familie.valutakurs.domene.exchangeRateForCurrency +import no.nav.familie.valutakurs.exception.ValutakursClientException +import org.springframework.context.annotation.Import +import org.springframework.stereotype.Service +import java.math.BigDecimal +import java.time.LocalDate + +@Service +@Import(ValutakursRestClient::class) +class ECBService(private val ecbClient: ValutakursRestClient) { + + /** + * @param utenlandskValuta valutaen vi skal konvertere til NOK + * @param kursDato datoen vi skal hente valutakurser for + * @return Henter valutakurs for *utenlandskValuta* -> EUR og NOK -> EUR på *kursDato*, og returnerer en beregnet kurs for *utenlandskValuta* -> NOK. + */ + @Throws(ECBServiceException::class) + fun hentValutakurs(utenlandskValuta: String, kursDato: LocalDate): BigDecimal { + try { + val exchangeRates = + ecbClient.hentValutakurs(Frequency.Daily, listOf(ECBConstants.NOK, utenlandskValuta), kursDato) + validateExchangeRates(utenlandskValuta, kursDato, exchangeRates) + val valutakursNOK = exchangeRates.exchangeRateForCurrency(ECBConstants.NOK)!! + if (utenlandskValuta == ECBConstants.EUR) { + return valutakursNOK.exchangeRate + } + val valutakursUtenlandskValuta = exchangeRates.exchangeRateForCurrency(utenlandskValuta)!! + return beregnValutakurs(valutakursUtenlandskValuta.exchangeRate, valutakursNOK.exchangeRate) + } catch (e: ValutakursClientException) { + throw ECBServiceException(e.message, e) + } + } + + private fun beregnValutakurs(valutakursUtenlandskValuta: BigDecimal, valutakursNOK: BigDecimal) = + valutakursNOK.del(valutakursUtenlandskValuta, 10) + + private fun validateExchangeRates( + currency: String, + exchangeRateDate: LocalDate, + exchangeRates: List, + ) { + val expectedSize = if (currency != ECBConstants.EUR) 2 else 1 + val currencies = + if (currency != ECBConstants.EUR) listOf(currency, ECBConstants.NOK) else listOf(ECBConstants.NOK) + + if (!isValid(exchangeRates, currencies, exchangeRateDate, expectedSize)) { + throwValidationException(currency, exchangeRateDate) + } + } + + private fun isValid( + exchangeRates: List, + currencies: List, + exchangeRateDate: LocalDate, + expectedSize: Int, + ) = + exchangeRates.size == expectedSize && + exchangeRates.all { it.date == exchangeRateDate } && + exchangeRates.map { it.currency }.containsAll(currencies) + + private fun throwValidationException(currency: String, exchangeRateDate: LocalDate) { + throw ECBServiceException("Fant ikke nødvendige valutakurser for valutakursdato ${exchangeRateDate.tilKortString()} for å bestemme valutakursen $currency - NOK") + } +} + +object ECBConstants { + const val NOK = "NOK" + const val EUR = "EUR" +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/ecb/ECBServiceException.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/ecb/ECBServiceException.kt new file mode 100644 index 000000000..b61b3d703 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/ecb/ECBServiceException.kt @@ -0,0 +1,3 @@ +package no.nav.familie.ba.sak.integrasjoner.ecb + +class ECBServiceException(override val message: String, override val cause: Throwable? = null) : RuntimeException(message, cause) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/ef/EfSakRestClient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/ef/EfSakRestClient.kt new file mode 100644 index 000000000..6f49a1fa6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/ef/EfSakRestClient.kt @@ -0,0 +1,31 @@ +package no.nav.familie.ba.sak.integrasjoner.ef + +import no.nav.familie.ba.sak.common.kallEksternTjeneste +import no.nav.familie.http.client.AbstractRestClient +import no.nav.familie.http.util.UriUtil +import no.nav.familie.kontrakter.felles.PersonIdent +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.ef.EksternePerioderResponse +import no.nav.familie.kontrakter.felles.getDataOrThrow +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.web.client.RestOperations +import java.net.URI + +@Component +class EfSakRestClient( + @Value("\${FAMILIE_EF_SAK_API_URL}") private val efSakBaseUrl: URI, + @Qualifier("jwtBearer") restTemplate: RestOperations, +) : AbstractRestClient(restTemplate, "ef-sak") { + + fun hentPerioderMedFullOvergangsstønad(personIdent: String): EksternePerioderResponse { + val uri = UriUtil.uri(efSakBaseUrl, "ekstern/perioder/full-overgangsstonad") + + return kallEksternTjeneste>( + tjeneste = "ef-sak overgangsstønad", + uri = uri, + formål = "Hente perioder med full overgangsstønad", + ) { postForEntity(uri, PersonIdent(personIdent)) }.getDataOrThrow() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/FamilieIntegrasjonerTilgangskontrollClient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/FamilieIntegrasjonerTilgangskontrollClient.kt new file mode 100644 index 000000000..ec55470f6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/FamilieIntegrasjonerTilgangskontrollClient.kt @@ -0,0 +1,50 @@ +package no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner + +import no.nav.familie.ba.sak.common.kallEksternTjeneste +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.http.client.AbstractRestClient +import no.nav.familie.kontrakter.felles.Tema +import no.nav.familie.kontrakter.felles.tilgangskontroll.Tilgang +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpHeaders +import org.springframework.stereotype.Component +import org.springframework.web.client.RestOperations +import org.springframework.web.util.UriComponentsBuilder +import java.net.URI + +@Component +class FamilieIntegrasjonerTilgangskontrollClient( + @Value("\${FAMILIE_INTEGRASJONER_API_URL}") private val integrasjonUri: URI, + @Qualifier("jwtBearer") restOperations: RestOperations, +) : AbstractRestClient(restOperations, "integrasjon-tilgangskontroll") { + + val tilgangPersonUri: URI = + UriComponentsBuilder.fromUri(integrasjonUri).pathSegment(PATH_TILGANG_PERSON).build().toUri() + + fun sjekkTilgangTilPersoner(personIdenter: List): List { + if (SikkerhetContext.erSystemKontekst()) { + return personIdenter.map { Tilgang(personIdent = it, harTilgang = true, begrunnelse = null) } + } + return kallEksternTjeneste>( + tjeneste = "tilgangskontroll", + uri = tilgangPersonUri, + formål = "Sjekk tilgang til personer", + ) { + postForEntity( + tilgangPersonUri, + personIdenter, + HttpHeaders().also { + it.set(HEADER_NAV_TEMA, HEADER_NAV_TEMA_BAR) + }, + ) + } + } + + companion object { + + private const val PATH_TILGANG_PERSON = "tilgang/v2/personer" + private const val HEADER_NAV_TEMA = "Nav-Tema" + private val HEADER_NAV_TEMA_BAR = Tema.BAR.name + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/FamilieIntegrasjonerTilgangskontrollService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/FamilieIntegrasjonerTilgangskontrollService.kt new file mode 100644 index 000000000..02d5d63ea --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/FamilieIntegrasjonerTilgangskontrollService.kt @@ -0,0 +1,53 @@ +package no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner + +import no.nav.familie.ba.sak.config.hentCacheForSaksbehandler +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonInfo +import no.nav.familie.ba.sak.integrasjoner.pdl.SystemOnlyPdlRestClient +import no.nav.familie.ba.sak.integrasjoner.pdl.tilAdressebeskyttelse +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.familie.kontrakter.felles.tilgangskontroll.Tilgang +import org.springframework.cache.CacheManager +import org.springframework.stereotype.Service + +@Service +class FamilieIntegrasjonerTilgangskontrollService( + private val familieIntegrasjonerTilgangskontrollClient: FamilieIntegrasjonerTilgangskontrollClient, + private val cacheManager: CacheManager, + private val systemOnlyPdlRestClient: SystemOnlyPdlRestClient, +) { + + fun hentMaskertPersonInfoVedManglendeTilgang(aktør: Aktør): RestPersonInfo? { + val harTilgang = sjekkTilgangTilPerson(personIdent = aktør.aktivFødselsnummer()).harTilgang + return if (!harTilgang) { + val adressebeskyttelse = systemOnlyPdlRestClient.hentAdressebeskyttelse(aktør).tilAdressebeskyttelse() + RestPersonInfo( + personIdent = aktør.aktivFødselsnummer(), + adressebeskyttelseGradering = adressebeskyttelse, + harTilgang = false, + ) + } else { + null + } + } + + fun sjekkTilgangTilPerson(personIdent: String): Tilgang { + return sjekkTilgangTilPersoner(listOf(personIdent)).values.single() + } + + fun sjekkTilgangTilPersoner(personIdenter: List): Map { + return cacheManager.hentCacheForSaksbehandler("sjekkTilgangTilPersoner", personIdenter) { + familieIntegrasjonerTilgangskontrollClient.sjekkTilgangTilPersoner(it).associateBy { it.personIdent } + } + } + + fun hentIdenterMedStrengtFortroligAdressebeskyttelse(personIdenter: List): List { + val adresseBeskyttelseBolk = systemOnlyPdlRestClient.hentAdressebeskyttelseBolk(personIdenter) + return adresseBeskyttelseBolk.filter { (_, person) -> + person.adressebeskyttelse.any { adressebeskyttelse -> + adressebeskyttelse.gradering == ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG || + adressebeskyttelse.gradering == ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG_UTLAND + } + }.map { it.key } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/HttpHeaderUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/HttpHeaderUtils.kt new file mode 100644 index 000000000..fe5b58966 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/HttpHeaderUtils.kt @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner + +import org.springframework.http.HttpHeaders + +fun HttpHeaders.medContentTypeJsonUTF8(): HttpHeaders { + this.add("Content-Type", "application/json;charset=UTF-8") + this.acceptCharset = listOf(Charsets.UTF_8) + return this +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/IntegrasjonClient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/IntegrasjonClient.kt new file mode 100644 index 000000000..5bb5daddd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/IntegrasjonClient.kt @@ -0,0 +1,498 @@ +package no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.kallEksternTjenesteRessurs +import no.nav.familie.ba.sak.common.kallEksternTjenesteUtenRespons +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Arbeidsfordelingsenhet +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Arbeidsforhold +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.ArbeidsforholdRequest +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Skyggesak +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.LogiskVedleggRequest +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.LogiskVedleggResponse +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.OppdaterJournalpostRequest +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.OppdaterJournalpostResponse +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.steg.domene.ManuellAdresseInfo +import no.nav.familie.ba.sak.task.DistribuerDokumentDTO +import no.nav.familie.ba.sak.task.OpprettTaskService.Companion.RETRY_BACKOFF_5000MS +import no.nav.familie.http.client.AbstractRestClient +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.PersonIdent +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.Tema +import no.nav.familie.kontrakter.felles.dokarkiv.ArkiverDokumentResponse +import no.nav.familie.kontrakter.felles.dokarkiv.v2.ArkiverDokumentRequest +import no.nav.familie.kontrakter.felles.dokdist.AdresseType +import no.nav.familie.kontrakter.felles.dokdist.DistribuerJournalpostRequest +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstidspunkt +import no.nav.familie.kontrakter.felles.dokdist.ManuellAdresse +import no.nav.familie.kontrakter.felles.journalpost.Journalpost +import no.nav.familie.kontrakter.felles.journalpost.JournalposterForBrukerRequest +import no.nav.familie.kontrakter.felles.kodeverk.KodeverkDto +import no.nav.familie.kontrakter.felles.navkontor.NavKontorEnhet +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveRequest +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveResponseDto +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import no.nav.familie.kontrakter.felles.oppgave.OppgaveResponse +import no.nav.familie.kontrakter.felles.oppgave.OpprettOppgaveRequest +import no.nav.familie.kontrakter.felles.organisasjon.Organisasjon +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.cache.annotation.Cacheable +import org.springframework.http.HttpHeaders +import org.springframework.retry.annotation.Backoff +import org.springframework.retry.annotation.Retryable +import org.springframework.stereotype.Component +import org.springframework.web.client.RestOperations +import org.springframework.web.util.UriComponentsBuilder +import java.net.URI +import java.time.LocalDate + +const val DEFAULT_JOURNALFØRENDE_ENHET = "9999" + +@Component +class IntegrasjonClient( + @Value("\${FAMILIE_INTEGRASJONER_API_URL}") private val integrasjonUri: URI, + @Qualifier("jwtBearer") restOperations: RestOperations, +) : AbstractRestClient(restOperations, "integrasjon") { + + @Cacheable("alle-eøs-land", cacheManager = "dailyCache") + fun hentAlleEØSLand(): KodeverkDto { + val uri = URI.create("$integrasjonUri/kodeverk/landkoder/eea") + + return kallEksternTjenesteRessurs( + tjeneste = "kodeverk", + uri = uri, + formål = "Hent EØS land", + ) { + getForEntity(uri) + } + } + + @Cacheable("land", cacheManager = "dailyCache") + fun hentLand(landkode: String): String { + if (landkode.length != 3) { + throw Feil("Støtter bare landkoder med tre bokstaver") + } + + val uri = URI.create("$integrasjonUri/kodeverk/landkoder/$landkode") + + return kallEksternTjenesteRessurs( + tjeneste = "kodeverk", + uri = uri, + formål = "Hent landkoder for $landkode", + ) { + getForEntity(uri) + } + } + + @Retryable( + value = [Exception::class], + maxAttempts = 3, + backoff = Backoff(delayExpression = RETRY_BACKOFF_5000MS), + ) + @Cacheable("behandlendeEnhet", cacheManager = "shortCache") + fun hentBehandlendeEnhet(ident: String): List { + val uri = UriComponentsBuilder.fromUri(integrasjonUri) + .pathSegment("arbeidsfordeling", "enhet", "BAR") + .build().toUri() + + return kallEksternTjenesteRessurs( + tjeneste = "arbeidsfordeling", + uri = uri, + formål = "Hent behandlende enhet", + ) { + postForEntity(uri, mapOf("ident" to ident)) + } + } + + @Cacheable("behandlendeEnhetForPersonMedRelasjon", cacheManager = "shortCache") + fun hentBehandlendeEnhetForPersonIdentMedRelasjoner(ident: String): Arbeidsfordelingsenhet { + val uri = UriComponentsBuilder + .fromUri(integrasjonUri) + .pathSegment("arbeidsfordeling", "enhet", Tema.KON.name, "med-relasjoner") + .build().toUri() + + return kallEksternTjenesteRessurs>( + tjeneste = "arbeidsfordeling", + uri = uri, + formål = "Hent strengeste behandlende enhet for person og alle relasjoner til personen", + ) { + postForEntity(uri, PersonIdent(ident)) + }.single() + } + + @Retryable( + value = [Exception::class], + maxAttempts = 3, + backoff = Backoff(delayExpression = RETRY_BACKOFF_5000MS), + ) + fun hentArbeidsforhold(ident: String, ansettelsesperiodeFom: LocalDate): List { + val uri = UriComponentsBuilder.fromUri(integrasjonUri) + .pathSegment("aareg", "arbeidsforhold") + .build().toUri() + + return kallEksternTjenesteRessurs( + tjeneste = "aareg", + uri = uri, + formål = "Hent arbeidsforhold", + ) { + postForEntity(uri, ArbeidsforholdRequest(ident, ansettelsesperiodeFom)) + } + } + + fun distribuerBrev(distribuerDokumentDTO: DistribuerDokumentDTO): String { + val uri = URI.create("$integrasjonUri/dist/v1") + + val resultat: String = kallEksternTjenesteRessurs( + tjeneste = "dokdist", + uri = uri, + formål = "Distribuer brev", + ) { + val journalpostRequest = DistribuerJournalpostRequest( + journalpostId = distribuerDokumentDTO.journalpostId, + bestillendeFagsystem = Fagsystem.BA, + dokumentProdApp = "FAMILIE_BA_SAK", + distribusjonstidspunkt = Distribusjonstidspunkt.KJERNETID, + distribusjonstype = distribuerDokumentDTO.brevmal.distribusjonstype, + adresse = distribuerDokumentDTO.manuellAdresseInfo?.let { lagManuellAdresse(it) }, + ) + postForEntity(uri, journalpostRequest, HttpHeaders().medContentTypeJsonUTF8()) + } + + if (resultat.isBlank()) error("BestillingsId fra integrasjonstjenesten mot dokdist er tom") + return resultat + } + + private fun lagManuellAdresse(manuellAdresseInfo: ManuellAdresseInfo) = + ManuellAdresse( + adresseType = when (manuellAdresseInfo.landkode) { + "NO" -> AdresseType.norskPostadresse + else -> AdresseType.utenlandskPostadresse + }, + adresselinje1 = manuellAdresseInfo.adresselinje1, + adresselinje2 = manuellAdresseInfo.adresselinje2, + postnummer = manuellAdresseInfo.postnummer, + poststed = manuellAdresseInfo.poststed, + land = manuellAdresseInfo.landkode, + ) + + fun ferdigstillOppgave(oppgaveId: Long) { + val uri = URI.create("$integrasjonUri/oppgave/$oppgaveId/ferdigstill") + + kallEksternTjenesteUtenRespons( + tjeneste = "oppgave", + uri = uri, + formål = "Ferdigstill oppgave", + ) { + patchForEntity>(uri, "") + } + } + + fun oppdaterOppgave(oppgaveId: Long, oppdatertOppgave: Oppgave) { + val uri = URI.create("$integrasjonUri/oppgave/$oppgaveId/oppdater") + + kallEksternTjenesteUtenRespons( + tjeneste = "oppgave", + uri = uri, + formål = "Oppdater oppgave", + ) { + patchForEntity>(uri, oppdatertOppgave) + } + } + + @Cacheable("enhet", cacheManager = "dailyCache") + fun hentEnhet(enhetId: String?): NavKontorEnhet { + val uri = URI.create("$integrasjonUri/arbeidsfordeling/nav-kontor/$enhetId") + + return kallEksternTjenesteRessurs( + tjeneste = "arbeidsfordeling", + uri = uri, + formål = "Hent nav kontor for enhet $enhetId", + ) { + getForEntity(uri) + } + } + + fun opprettOppgave(opprettOppgave: OpprettOppgaveRequest): OppgaveResponse { + val uri = URI.create("$integrasjonUri/oppgave/opprett") + + return kallEksternTjenesteRessurs( + tjeneste = "oppgave", + uri = uri, + formål = "Opprett oppgave", + ) { + postForEntity( + uri, + opprettOppgave, + HttpHeaders().medContentTypeJsonUTF8(), + ) + } + } + + fun patchOppgave(patchOppgave: Oppgave): OppgaveResponse { + val uri = URI.create("$integrasjonUri/oppgave/${patchOppgave.id}/oppdater") + + return kallEksternTjenesteRessurs( + tjeneste = "oppgave", + uri = uri, + formål = "Patch oppgave", + ) { + patchForEntity( + uri, + patchOppgave, + HttpHeaders().medContentTypeJsonUTF8(), + ) + } + } + + fun fordelOppgave(oppgaveId: Long, saksbehandler: String?): OppgaveResponse { + val baseUri = URI.create("$integrasjonUri/oppgave/$oppgaveId/fordel") + val uri = if (saksbehandler == null) { + baseUri + } else { + UriComponentsBuilder.fromUri(baseUri).queryParam("saksbehandler", saksbehandler).build().toUri() + } + + return kallEksternTjenesteRessurs( + tjeneste = "oppgave", + uri = uri, + formål = "Fordel oppgave", + ) { + postForEntity( + uri, + HttpHeaders().medContentTypeJsonUTF8(), + ) + } + } + + fun tilordneEnhetForOppgave(oppgaveId: Long, nyEnhet: String): OppgaveResponse { + val baseUri = URI.create("$integrasjonUri/oppgave/$oppgaveId/enhet/$nyEnhet") + val uri = UriComponentsBuilder.fromUri(baseUri).queryParam("fjernMappeFraOppgave", true).build() + .toUri() // fjerner alltid mappe fra Barnetrygd siden hver enhet sin mappestruktur + + return kallEksternTjenesteRessurs( + tjeneste = "oppgave", + uri = uri, + formål = "Bytt enhet", + ) { + patchForEntity( + uri, + HttpHeaders().medContentTypeJsonUTF8(), + ) + } + } + + @Retryable( + value = [Exception::class], + maxAttempts = 3, + backoff = Backoff(delayExpression = RETRY_BACKOFF_5000MS), + ) + fun fjernBehandlesAvApplikasjon(oppgaveId: Long): OppgaveResponse { + val oppgave = finnOppgaveMedId(oppgaveId) + if (oppgave.behandlesAvApplikasjon == null) { + logger.info("behandlesAvApplikasjon er allerede null for $oppgaveId") + return OppgaveResponse(oppgaveId) + } + + val baseUri = URI.create("$integrasjonUri/oppgave/$oppgaveId/fjern-behandles-av-applikasjon") + val uri = UriComponentsBuilder.fromUri(baseUri).queryParam("versjon", oppgave.versjon).build() + .toUri() + + return kallEksternTjenesteRessurs( + tjeneste = "oppgave", + uri = uri, + formål = "fjern behandlesAvApplikasjon", + ) { + patchForEntity( + uri, + HttpHeaders().medContentTypeJsonUTF8(), + ) + } + } + + fun finnOppgaveMedId(oppgaveId: Long): Oppgave { + val uri = URI.create("$integrasjonUri/oppgave/$oppgaveId") + + return kallEksternTjenesteRessurs( + tjeneste = "oppgave", + uri = uri, + formål = "Finn oppgave med id $oppgaveId", + ) { + getForEntity(uri) + } + } + + @Retryable( + value = [Exception::class], + maxAttempts = 3, + backoff = Backoff(delayExpression = RETRY_BACKOFF_5000MS), + ) + fun hentJournalpost(journalpostId: String): Journalpost { + val uri = URI.create("$integrasjonUri/journalpost?journalpostId=$journalpostId") + + return kallEksternTjenesteRessurs( + tjeneste = "dokarkiv", + uri = uri, + formål = "Hent journalpost id $journalpostId", + ) { + getForEntity(uri) + } + } + + @Retryable( + value = [Exception::class], + maxAttempts = 3, + backoff = Backoff(delayExpression = RETRY_BACKOFF_5000MS), + ) + fun hentJournalposterForBruker(journalposterForBrukerRequest: JournalposterForBrukerRequest): List { + val uri = URI.create("$integrasjonUri/journalpost") + + return kallEksternTjenesteRessurs( + tjeneste = "dokarkiv", + uri = uri, + formål = "Hent journalposter for bruker", + ) { + postForEntity(uri, journalposterForBrukerRequest) + } + } + + fun hentOppgaver(finnOppgaveRequest: FinnOppgaveRequest): FinnOppgaveResponseDto { + val uri = URI.create("$integrasjonUri/oppgave/v4") + + return kallEksternTjenesteRessurs( + tjeneste = "oppgave", + uri = uri, + formål = "Hent oppgaver", + ) { + postForEntity( + uri, + finnOppgaveRequest, + HttpHeaders().medContentTypeJsonUTF8(), + ) + } + } + + fun ferdigstillJournalpost(journalpostId: String, journalførendeEnhet: String) { + val uri = + URI.create("$integrasjonUri/arkiv/v2/$journalpostId/ferdigstill?journalfoerendeEnhet=$journalførendeEnhet") + + kallEksternTjenesteUtenRespons( + tjeneste = "dokarkiv", + uri = uri, + formål = "Hent journalposter for bruker", + ) { + putForEntity>(uri, "") + } + } + + fun oppdaterJournalpost(request: OppdaterJournalpostRequest, journalpostId: String): OppdaterJournalpostResponse { + val uri = URI.create("$integrasjonUri/arkiv/v2/$journalpostId") + + return kallEksternTjenesteRessurs( + tjeneste = "dokarkiv", + uri = uri, + formål = "Oppdater journalpost", + ) { + putForEntity(uri, request) + } + } + + fun leggTilLogiskVedlegg(request: LogiskVedleggRequest, dokumentinfoId: String): LogiskVedleggResponse { + val uri = URI.create("$integrasjonUri/arkiv/dokument/$dokumentinfoId/logiskVedlegg") + + return kallEksternTjenesteRessurs( + tjeneste = "dokarkiv", + uri = uri, + formål = "Legg til logisk vedlegg på dokument $dokumentinfoId", + ) { + postForEntity(uri, request) + } + } + + fun slettLogiskVedlegg(logiskVedleggId: String, dokumentinfoId: String): LogiskVedleggResponse { + val uri = URI.create("$integrasjonUri/arkiv/dokument/$dokumentinfoId/logiskVedlegg/$logiskVedleggId") + return kallEksternTjenesteRessurs( + tjeneste = "dokarkiv", + uri = uri, + formål = "Slett logisk vedlegg på dokument $dokumentinfoId", + ) { + deleteForEntity(uri) + } + } + + fun hentDokument(dokumentInfoId: String, journalpostId: String): ByteArray { + val uri = URI.create("$integrasjonUri/journalpost/hentdokument/$journalpostId/$dokumentInfoId") + + return kallEksternTjenesteRessurs( + tjeneste = "dokarkiv", + uri = uri, + formål = "Hent dokument $dokumentInfoId", + ) { + getForEntity(uri) + } + } + + fun journalførDokument( + arkiverDokumentRequest: ArkiverDokumentRequest, + ): ArkiverDokumentResponse { + val uri = URI.create("$integrasjonUri/arkiv/v4") + + return kallEksternTjenesteRessurs( + tjeneste = "dokarkiv", + uri = uri, + formål = "Journalfør dokument på fagsak ${arkiverDokumentRequest.fagsakId}", + ) { + postForEntity(uri, arkiverDokumentRequest) + } + } + + fun opprettSkyggesak(aktør: Aktør, fagsakId: Long) { + val uri = URI.create("$integrasjonUri/skyggesak/v1") + + kallEksternTjenesteUtenRespons( + tjeneste = "skyggesak", + uri = uri, + formål = "Opprett skyggesak på fagsak $fagsakId", + ) { + postForEntity(uri, Skyggesak(aktør.aktørId, fagsakId.toString())) + } + } + + @Cacheable("landkoder-ISO_3166-1_alfa-2", cacheManager = "dailyCache") + fun hentLandkoderISO2(): Map { + val uri = URI.create("$integrasjonUri/kodeverk/landkoderISO2") + + return kallEksternTjenesteRessurs( + tjeneste = "kodeverk", + uri = uri, + formål = "Hent landkoderISO2", + ) { + getForEntity(uri) + } + } + + fun hentOrganisasjon(organisasjonsnummer: String): Organisasjon { + val uri = URI.create("$integrasjonUri/organisasjon/$organisasjonsnummer") + return kallEksternTjenesteRessurs( + tjeneste = "organisasjon", + uri = uri, + formål = "Hent organisasjon $organisasjonsnummer", + ) { + getForEntity(uri) + } + } + + companion object { + + private val logger = LoggerFactory.getLogger(IntegrasjonClient::class.java) + const val VEDTAK_VEDLEGG_FILNAVN = "NAV_33-0005bm-10.2016.pdf" + const val VEDTAK_VEDLEGG_TITTEL = "Stønadsmottakerens rettigheter og plikter (Barnetrygd)" + + fun hentVedlegg(vedleggsnavn: String): ByteArray? { + val inputStream = this::class.java.classLoader.getResourceAsStream("dokumenter/$vedleggsnavn") + return inputStream?.readAllBytes() + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/IntegrasjonException.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/IntegrasjonException.kt new file mode 100644 index 000000000..f04f925e9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/IntegrasjonException.kt @@ -0,0 +1,29 @@ +package no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner + +import no.nav.familie.ba.sak.common.secureLogger +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.client.RestClientResponseException +import java.net.URI + +@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) +class IntegrasjonException( + msg: String, + throwable: Throwable? = null, + uri: URI? = null, + ident: String? = null, +) : RuntimeException(msg, throwable) { + + init { + val message = if (throwable is RestClientResponseException) throwable.responseBodyAsString else "" + + secureLogger.info("Ukjent feil ved integrasjon mot $uri. ident=$ident msg=$msg, message=$message", throwable) + logger.warn("Ukjent feil ved integrasjon mot '$uri'.") + } + + companion object { + + private val logger = LoggerFactory.getLogger(IntegrasjonException::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/domene/Arbeidsfordelingsenhet.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/domene/Arbeidsfordelingsenhet.kt new file mode 100644 index 000000000..559e11f08 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/domene/Arbeidsfordelingsenhet.kt @@ -0,0 +1,3 @@ +package no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene + +data class Arbeidsfordelingsenhet(val enhetId: String, val enhetNavn: String) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/domene/Arbeidsforhold.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/domene/Arbeidsforhold.kt new file mode 100644 index 000000000..050b1971e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/domene/Arbeidsforhold.kt @@ -0,0 +1,55 @@ +package no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene + +import java.time.LocalDate + +class Arbeidsforhold( + val navArbeidsforholdId: Long? = null, + val arbeidsforholdId: String? = null, + val arbeidstaker: Arbeidstaker? = null, + val arbeidsgiver: Arbeidsgiver? = null, + val type: String? = null, + val ansettelsesperiode: Ansettelsesperiode? = null, + val arbeidsavtaler: List? = null, +) + +class Arbeidstaker( + val type: String? = null, + val offentligIdent: String? = null, + val aktoerId: String? = null, +) + +class Arbeidsgiver( + val type: ArbeidsgiverType? = null, + val organisasjonsnummer: String? = null, + val offentligIdent: String? = null, +) + +class Ansettelsesperiode( + val periode: Periode? = null, + val bruksperiode: Periode? = null, +) + +class Arbeidsavtaler( + val arbeidstidsordning: String? = null, + val yrke: String? = null, + val stillingsprosent: Double? = null, + val antallTimerPrUke: Double? = null, + val beregnetAntallTimerPrUke: Double? = null, + val bruksperiode: Periode? = null, + val gyldighetsperiode: Periode? = null, +) + +class Periode( + val fom: LocalDate? = null, + val tom: LocalDate? = null, +) + +enum class ArbeidsgiverType { + Organisasjon, + Person, +} + +class ArbeidsforholdRequest( + val personIdent: String, + val ansettelsesperiodeFom: LocalDate, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/domene/Skyggesak.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/domene/Skyggesak.kt new file mode 100644 index 000000000..8b086e8be --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/domene/Skyggesak.kt @@ -0,0 +1,8 @@ +package no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene + +data class Skyggesak( + val aktoerId: String, + val fagsakNr: String, + val tema: String = "BAR", + val applikasjon: String = "BA", +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdBarnetrygdClient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdBarnetrygdClient.kt new file mode 100644 index 000000000..e19dd5e09 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdBarnetrygdClient.kt @@ -0,0 +1,201 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd + +import no.nav.commons.foedselsnummer.FoedselsNr +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.ekstern.bisys.BisysUtvidetBarnetrygdResponse +import no.nav.familie.ba.sak.task.OpprettTaskService.Companion.RETRY_BACKOFF_5000MS +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPerioder +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPerioderRequest +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPerioderResponse +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPersonerResponse +import no.nav.familie.http.client.AbstractRestClient +import no.nav.familie.kontrakter.ba.infotrygd.InfotrygdSøkRequest +import no.nav.familie.kontrakter.ba.infotrygd.InfotrygdSøkResponse +import no.nav.familie.kontrakter.ba.infotrygd.Sak +import no.nav.familie.kontrakter.ba.infotrygd.Stønad +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpStatus +import org.springframework.retry.annotation.Backoff +import org.springframework.retry.annotation.Retryable +import org.springframework.stereotype.Component +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.RestOperations +import java.net.URI +import java.time.YearMonth + +@Component +class InfotrygdBarnetrygdClient( + @Value("\${FAMILIE_BA_INFOTRYGD_API_URL}") private val clientUri: URI, + @Qualifier("jwtBearerMedLangTimeout") restOperations: RestOperations, +) : AbstractRestClient(restOperations, "infotrygd") { + + fun harLøpendeSakIInfotrygd(søkersIdenter: List, barnasIdenter: List = emptyList()): Boolean { + val uri = URI.create("$clientUri/infotrygd/barnetrygd/lopende-barnetrygd") + + val request = InfotrygdSøkRequest(søkersIdenter, barnasIdenter) + + return try { + postForEntity(uri, request).harLøpendeBarnetrygd + } catch (ex: Exception) { + loggFeil(ex, uri) + throw ex + } + } + + fun harÅpenSakIInfotrygd(søkersIdenter: List, barnasIdenter: List = emptyList()): Boolean { + val uri = URI.create("$clientUri/infotrygd/barnetrygd/aapen-sak") + + val request = InfotrygdSøkRequest(søkersIdenter, barnasIdenter) + + return try { + postForEntity(uri, request).harÅpenSak + } catch (ex: Exception) { + loggFeil(ex, uri) + throw ex + } + } + + fun hentSaker(søkersIdenter: List, barnasIdenter: List = emptyList()): InfotrygdSøkResponse { + val uri = URI.create("$clientUri/infotrygd/barnetrygd/saker") + + return try { + postForEntity(uri, InfotrygdSøkRequest(søkersIdenter, barnasIdenter)) + } catch (ex: Exception) { + loggFeil(ex, uri) + throw Feil( + message = "Henting av infotrygdsaker feilet. Gav feil: ${ex.message}", + frontendFeilmelding = "Henting av infotrygdsaker feilet.", + httpStatus = HttpStatus.INTERNAL_SERVER_ERROR, + throwable = ex, + ) + } + } + + fun hentStønader( + søkersIdenter: List, + barnasIdenter: List, + historikk: Boolean = false, + ): InfotrygdSøkResponse { + val uri = URI.create("$clientUri/infotrygd/barnetrygd/stonad?historikk=$historikk") + + return try { + postForEntity(uri, InfotrygdSøkRequest(søkersIdenter, barnasIdenter)) + } catch (ex: Exception) { + loggFeil(ex, uri) + throw Feil( + message = "Henting av infotrygdstønader feilet. Gav feil: ${ex.message}", + frontendFeilmelding = "Henting av infotrygdstønader feilet.", + httpStatus = HttpStatus.INTERNAL_SERVER_ERROR, + throwable = ex, + ) + } + } + + data class HentUtvidetBarnetrygdRequest(val personIdent: String, val fraDato: YearMonth) + + @Retryable( + value = [Exception::class], + maxAttempts = 3, + backoff = Backoff(delayExpression = RETRY_BACKOFF_5000MS), + ) + fun hentUtvidetBarnetrygd(personIdent: String, fraDato: YearMonth): BisysUtvidetBarnetrygdResponse { + val uri = URI.create("$clientUri/infotrygd/barnetrygd/utvidet") + val body = HentUtvidetBarnetrygdRequest(personIdent, fraDato) + return try { + postForEntity(uri, body) + } catch (ex: Exception) { + loggFeil(ex, uri) + throw RuntimeException("Henting av utvidet barnetrygd feilet. Gav feil: ${ex.message}", ex) + } + } + + fun hentPersonerMedUtvidetBarnetrygd(år: String): SkatteetatenPersonerResponse { + val uri = URI.create("$clientUri/infotrygd/barnetrygd/utvidet?aar=$år") + return try { + getForEntity(uri) + } catch (ex: Exception) { + loggFeil(ex, uri) + throw RuntimeException("Henting av personer med utvidet barnetrygd feilet. Gav feil: ${ex.message}", ex) + } + } + + fun hentPerioderMedUtvidetBarnetrygdForPersoner(identer: List, år: String): List { + val uri = URI.create("$clientUri/infotrygd/barnetrygd/utvidet/skatteetaten/perioder") + + val request = SkatteetatenPerioderRequest(identer = identer, aar = år) + return try { + postForEntity>(uri, request).flatMap { it.brukere } + } catch (ex: Exception) { + loggFeil(ex, uri) + throw RuntimeException("Henting av perioder med utvidet barnetrygd feilet. Gav feil: ${ex.message}", ex) + } + } + + fun harNyligSendtBrevFor(søkersIdenter: List, brevkoder: List): SendtBrevResponse { + val uri = URI.create("$clientUri/infotrygd/barnetrygd/brev") + return try { + postForEntity( + uri, + SendtBrevRequest(søkersIdenter, brevkoder.map { it.kode }), + ) + } catch (ex: Exception) { + loggFeil(ex, uri) + throw RuntimeException( + "Sjekk mot infotrygd for å sjekke om brev er sendt feilet . Gav feil: ${ex.message}", + ex, + ) + } + } + + class SendtBrevRequest( + val personidenter: List, + val brevkoder: List, + ) + + data class SendtBrevResponse( + val harSendtBrev: Boolean, + val listeBrevhendelser: List = emptyList(), + ) + + data class InfotrygdHendelse( + val id: Long, + val personKey: Long, + val saksblokk: String, + val saksnummer: String, + val aksjonsdatoSeq: Long, + val tekstKode1: String, + val fnr: FoedselsNr, + val tkNr: String, + val region: String, + ) + + private fun loggFeil(ex: Exception, uri: URI) { + when (ex) { + is HttpClientErrorException -> secureLogger.warn( + "Http feil mot ${uri.path}: httpkode: ${ex.statusCode}, feilmelding ${ex.message}", + ex, + ) + else -> secureLogger.warn("Feil mot ${uri.path}; melding ${ex.message}", ex) + } + logger.warn("Feil mot ${uri.path}.") + } + + companion object { + + private val logger: Logger = LoggerFactory.getLogger(InfotrygdBarnetrygdClient::class.java) + } +} + +enum class InfotrygdBrevkode(val kode: String) { + BREV_BATCH_OPPHØR_SMÅBARNSTILLLEGG("BA04"), + BREV_BATCH_INNVILGET_SMÅBARNSTILLEGG("BA05"), + BREV_BATCH_OMREGNING_BARN_18_ÅR("BA37"), + BREV_BATCH_OMREGNING_BARN_6_ÅR("BA18"), + BREV_MANUELL_OPPHØR_SMÅBARNSTILLLEGG("B001"), + BREV_MANUELL_INNVILGET_SMÅBARNSTILLEGG("B002"), + BREV_MANUELL_OMREGNING_BARN_18_ÅR("B003"), + BREV_MANUELL_OMREGNING_BARN_6_ÅR("BA19"), +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdController.kt new file mode 100644 index 000000000..fe3fec87b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdController.kt @@ -0,0 +1,65 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd + +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.kontrakter.ba.infotrygd.Sak +import no.nav.familie.kontrakter.ba.infotrygd.Stønad +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/infotrygd") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class InfotrygdController( + private val infotrygdBarnetrygdClient: InfotrygdBarnetrygdClient, + private val personidentService: PersonidentService, + private val infotrygdService: InfotrygdService, +) { + + @PostMapping(path = ["/hent-infotrygdsaker-for-soker"]) + fun hentInfotrygdsakerForSøker(@RequestBody personIdent: Personident): ResponseEntity> { + val aktør = personidentService.hentAktør(personIdent.ident) + val infotrygdsaker = infotrygdService.hentMaskertRestInfotrygdsakerVedManglendeTilgang(aktør) + ?: RestInfotrygdsaker(infotrygdService.hentInfotrygdsakerForSøker(aktør).bruker) + + return ResponseEntity.ok(Ressurs.success(infotrygdsaker)) + } + + @PostMapping(path = ["/hent-infotrygdstonader-for-soker"]) + fun hentInfotrygdstønaderForSøker(@RequestBody personIdent: Personident): ResponseEntity> { + val aktør = personidentService.hentAktør(personIdent.ident) + val infotrygdstønader = infotrygdService.hentMaskertRestInfotrygdstønaderVedManglendeTilgang(aktør) + ?: RestInfotrygdstønader(infotrygdService.hentInfotrygdstønaderForSøker(personIdent.ident).bruker) + + return ResponseEntity.ok(Ressurs.success(infotrygdstønader)) + } + + @PostMapping(path = ["/har-lopende-sak"]) + fun harLøpendeSak(@RequestBody personIdent: Personident): ResponseEntity> { + val harLøpendeSak = infotrygdBarnetrygdClient.harLøpendeSakIInfotrygd(listOf(personIdent.ident)) + return ResponseEntity.ok(Ressurs.success(RestLøpendeSak(harLøpendeSak))) + } +} + +class Personident(val ident: String) + +class RestInfotrygdsaker( + val saker: List = emptyList(), + val adressebeskyttelsegradering: ADRESSEBESKYTTELSEGRADERING? = null, + val harTilgang: Boolean = true, +) + +class RestInfotrygdstønader( + val stønader: List = emptyList(), + val adressebeskyttelsegradering: ADRESSEBESKYTTELSEGRADERING? = null, + val harTilgang: Boolean = true, +) + +class RestLøpendeSak(val harLøpendeSak: Boolean) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedClient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedClient.kt new file mode 100644 index 000000000..d01995373 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedClient.kt @@ -0,0 +1,80 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd + +import no.nav.familie.ba.sak.integrasjoner.infotrygd.domene.InfotrygdFødselhendelsesFeedDto +import no.nav.familie.ba.sak.integrasjoner.infotrygd.domene.InfotrygdVedtakFeedDto +import no.nav.familie.ba.sak.integrasjoner.infotrygd.domene.StartBehandlingDto +import no.nav.familie.ba.sak.task.OpprettTaskService.Companion.RETRY_BACKOFF_5000MS +import no.nav.familie.http.client.AbstractRestClient +import no.nav.familie.kontrakter.felles.Ressurs +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.retry.annotation.Backoff +import org.springframework.retry.annotation.Retryable +import org.springframework.stereotype.Component +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.RestOperations +import java.io.IOException +import java.net.URI + +@Component +class InfotrygdFeedClient( + @Value("\${FAMILIE_BA_INFOTRYGD_FEED_API_URL}") private val clientUri: URI, + @Qualifier("jwtBearer") restOperations: RestOperations, +) : AbstractRestClient(restOperations, "infotrygd_feed") { + + fun sendFødselhendelsesFeedTilInfotrygd(infotrygdFødselhendelsesFeedDto: InfotrygdFødselhendelsesFeedDto) { + return try { + sendFeedTilInfotrygd( + URI.create("$clientUri/barnetrygd/v1/feed/foedselsmelding"), + infotrygdFødselhendelsesFeedDto, + ) + } catch (e: Exception) { + loggOgKastException(e) + } + } + + fun sendVedtakFeedTilInfotrygd(infotrygdVedtakFeedDto: InfotrygdVedtakFeedDto) { + try { + sendFeedTilInfotrygd(URI.create("$clientUri/barnetrygd/v1/feed/vedtaksmelding"), infotrygdVedtakFeedDto) + } catch (e: Exception) { + loggOgKastException(e) + } + } + + fun sendStartBehandlingTilInfotrygd(startBehandlingDto: StartBehandlingDto) { + try { + sendFeedTilInfotrygd( + URI.create("$clientUri/barnetrygd/v1/feed/startbehandlingsmelding"), + startBehandlingDto, + ) + } catch (e: Exception) { + loggOgKastException(e) + } + } + + private fun loggOgKastException(e: Exception) { + if (e is HttpClientErrorException) { + logger.warn("Http feil mot infotrygd feed: httpkode: ${e.statusCode}, feilmelding ${e.message}", e) + } else { + logger.warn("Feil mot infotrygd feed; melding ${e.message}", e) + } + + throw e + } + + @Retryable( + value = [IOException::class], + maxAttempts = 3, + backoff = Backoff(delayExpression = RETRY_BACKOFF_5000MS), + ) + private fun sendFeedTilInfotrygd(endpoint: URI, feed: Any) { + postForEntity>(endpoint, feed) + } + + companion object { + + private val logger: Logger = LoggerFactory.getLogger(InfotrygdFeedClient::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedService.kt new file mode 100644 index 000000000..f3a5aa703 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedService.kt @@ -0,0 +1,22 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd + +import jakarta.transaction.Transactional +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.task.OpprettTaskService +import org.springframework.stereotype.Service + +@Service +class InfotrygdFeedService( + val opprettTaskService: OpprettTaskService, +) { + + @Transactional + fun sendTilInfotrygdFeed(barnsIdenter: List) { + opprettTaskService.opprettSendFeedTilInfotrygdTask(barnsIdenter) + } + + @Transactional + fun sendStartBehandlingTilInfotrygdFeed(aktørStoenadsmottaker: Aktør) { + opprettTaskService.opprettSendStartBehandlingTilInfotrygdTask(aktørStoenadsmottaker) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdService.kt new file mode 100644 index 000000000..6cc8d57da --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdService.kt @@ -0,0 +1,66 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.kontrakter.ba.infotrygd.InfotrygdSøkResponse +import no.nav.familie.kontrakter.ba.infotrygd.Sak +import no.nav.familie.kontrakter.ba.infotrygd.Stønad +import org.springframework.stereotype.Service + +@Service +class InfotrygdService( + private val infotrygdBarnetrygdClient: InfotrygdBarnetrygdClient, + private val familieIntegrasjonerTilgangskontrollService: FamilieIntegrasjonerTilgangskontrollService, + private val personidentService: PersonidentService, +) { + fun hentInfotrygdsakerForSøker(aktør: Aktør): InfotrygdSøkResponse { + return infotrygdBarnetrygdClient.hentSaker(listOf(aktør.aktivFødselsnummer()), emptyList()) + } + + fun hentMaskertRestInfotrygdsakerVedManglendeTilgang(aktør: Aktør): RestInfotrygdsaker? { + return familieIntegrasjonerTilgangskontrollService.hentMaskertPersonInfoVedManglendeTilgang(aktør) + ?.let { + RestInfotrygdsaker( + adressebeskyttelsegradering = it.adressebeskyttelseGradering, + harTilgang = false, + ) + } + } + + fun hentInfotrygdstønaderForSøker(ident: String, historikk: Boolean = false): InfotrygdSøkResponse { + val søkerIdenter = personidentService.hentIdenter(personIdent = ident, historikk = true) + .filter { it.gruppe == "FOLKEREGISTERIDENT" } + .map { it.ident } + return infotrygdBarnetrygdClient.hentStønader(søkerIdenter, emptyList(), historikk) + } + + fun hentMaskertRestInfotrygdstønaderVedManglendeTilgang(aktør: Aktør): RestInfotrygdstønader? { + return familieIntegrasjonerTilgangskontrollService.hentMaskertPersonInfoVedManglendeTilgang(aktør) + ?.let { + RestInfotrygdstønader( + adressebeskyttelsegradering = it.adressebeskyttelseGradering, + harTilgang = false, + ) + } + } + + fun harÅpenSakIInfotrygd(søkerIdenter: List, barnasIdenter: List = emptyList()): Boolean { + return infotrygdBarnetrygdClient.harÅpenSakIInfotrygd(søkerIdenter, barnasIdenter) + } + + fun harLøpendeSakIInfotrygd(søkerIdenter: List, barnasIdenter: List = emptyList()): Boolean { + return infotrygdBarnetrygdClient.harLøpendeSakIInfotrygd(søkerIdenter, barnasIdenter) + } + + fun harSendtbrev(søkerIdenter: List, brevkoder: List): Boolean { + if (brevkoder.isEmpty()) { + return false + } + + val infotrygdbrevrespons = infotrygdBarnetrygdClient.harNyligSendtBrevFor(søkerIdenter, brevkoder) + secureLogger.info("InfotrygdBrevRespons $infotrygdbrevrespons") + return infotrygdbrevrespons.harSendtBrev + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/ReplikatjenesteDto.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/ReplikatjenesteDto.kt new file mode 100644 index 000000000..564b1c7c1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/ReplikatjenesteDto.kt @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd + +class InfotrygdLøpendeBarnetrygdResponse(val harLøpendeBarnetrygd: Boolean) + +class InfotrygdÅpenSakResponse(val harÅpenSak: Boolean) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/domene/FeedDto.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/domene/FeedDto.kt new file mode 100644 index 000000000..e338d05f2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/domene/FeedDto.kt @@ -0,0 +1,11 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd.domene + +import java.time.LocalDate + +data class InfotrygdFødselhendelsesFeedTaskDto(val fnrBarn: List) +data class InfotrygdVedtakFeedTaskDto(val fnrStoenadsmottaker: String, val behandlingId: Long) + +data class InfotrygdFødselhendelsesFeedDto(val fnrBarn: String) +data class InfotrygdVedtakFeedDto(val fnrStoenadsmottaker: String, val datoStartNyBa: LocalDate) + +data class StartBehandlingDto(val fnrStoenadsmottaker: String) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/InnkommendeJournalf\303\270ringService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/InnkommendeJournalf\303\270ringService.kt" new file mode 100644 index 000000000..ca26e7780 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/InnkommendeJournalf\303\270ringService.kt" @@ -0,0 +1,390 @@ +package no.nav.familie.ba.sak.integrasjoner.journalføring + +import jakarta.transaction.Transactional +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.ekstern.restDomene.InstitusjonInfo +import no.nav.familie.ba.sak.ekstern.restDomene.RestFerdigstillOppgaveKnyttJournalpost +import no.nav.familie.ba.sak.ekstern.restDomene.RestJournalføring +import no.nav.familie.ba.sak.ekstern.restDomene.RestOppdaterJournalpost +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.DbJournalpost +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.DbJournalpostType +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.FagsakSystem +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.JournalføringRepository +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.LogiskVedleggRequest +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.OppdaterJournalpostRequest +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.Sakstype.FAGSAK +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.Sakstype.GENERELL_SAK +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.Søknadsinfo +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingSøknadsinfoService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.task.OpprettOppgaveTask +import no.nav.familie.kontrakter.ba.søknad.v4.Søknadstype +import no.nav.familie.kontrakter.felles.BrukerIdType +import no.nav.familie.kontrakter.felles.Tema +import no.nav.familie.kontrakter.felles.journalpost.Bruker +import no.nav.familie.kontrakter.felles.journalpost.Journalpost +import no.nav.familie.kontrakter.felles.journalpost.JournalposterForBrukerRequest +import no.nav.familie.kontrakter.felles.journalpost.Journalstatus.FERDIGSTILT +import no.nav.familie.kontrakter.felles.journalpost.Sak +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class InnkommendeJournalføringService( + private val integrasjonClient: IntegrasjonClient, + private val fagsakService: FagsakService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val journalføringRepository: JournalføringRepository, + private val loggService: LoggService, + private val stegService: StegService, + private val journalføringMetrikk: JournalføringMetrikk, + private val behandlingSøknadsinfoService: BehandlingSøknadsinfoService, +) { + + fun hentDokument(journalpostId: String, dokumentInfoId: String): ByteArray { + return integrasjonClient.hentDokument(dokumentInfoId, journalpostId) + } + + fun hentJournalpost(journalpostId: String): Journalpost { + return integrasjonClient.hentJournalpost(journalpostId) + } + + fun hentJournalposterForBruker(brukerId: String): List { + return integrasjonClient.hentJournalposterForBruker( + JournalposterForBrukerRequest( + antall = 1000, + brukerId = Bruker(id = brukerId, type = BrukerIdType.FNR), + tema = listOf(Tema.BAR), + ), + ) + } + + @Transactional + fun ferdigstill( + request: RestOppdaterJournalpost, + journalpostId: String, + behandlendeEnhet: String, + oppgaveId: String, + ): String { + val (sak, behandlinger) = lagreJournalpostOgKnyttFagsakTilJournalpost( + request.tilknyttedeBehandlingIder, + journalpostId, + ) + + håndterLogiskeVedlegg(request, journalpostId) + + oppdaterOgFerdigstill( + request = request.oppdaterMedDokumentOgSak(sak), + journalpostId = journalpostId, + behandlendeEnhet = behandlendeEnhet, + oppgaveId = oppgaveId, + behandlinger = behandlinger, + ) + + when (val aktivBehandling = behandlinger.find { it.aktiv }) { + null -> logger.info("Knytter til ${behandlinger.size} behandlinger som ikke er aktive") + else -> opprettOppgaveFor(aktivBehandling, request.navIdent) + } + + return sak.fagsakId ?: "" + } + + private fun oppdaterLogiskeVedlegg(request: RestJournalføring) { + request.dokumenter.forEach { dokument -> + val fjernedeVedlegg = (dokument.eksisterendeLogiskeVedlegg ?: emptyList()) + .partition { (dokument.logiskeVedlegg ?: emptyList()).contains(it) }.second + val nyeVedlegg = (dokument.logiskeVedlegg ?: emptyList()).partition { + (dokument.eksisterendeLogiskeVedlegg ?: emptyList()).contains(it) + }.second + fjernedeVedlegg.forEach { + integrasjonClient.slettLogiskVedlegg(it.logiskVedleggId, dokument.dokumentInfoId) + } + nyeVedlegg.forEach { + integrasjonClient.leggTilLogiskVedlegg(LogiskVedleggRequest(it.tittel), dokument.dokumentInfoId) + } + } + } + + private fun opprettBehandlingOgEvtFagsakForJournalføring( + personIdent: String, + navIdent: String, + type: BehandlingType, + årsak: BehandlingÅrsak, + kategori: BehandlingKategori? = null, + underkategori: BehandlingUnderkategori? = null, + søknadMottattDato: LocalDate? = null, + søknadsinfo: Søknadsinfo? = null, + fagsakType: FagsakType = FagsakType.NORMAL, + institusjon: InstitusjonInfo? = null, + ): Behandling { + val fagsak = fagsakService.hentEllerOpprettFagsak(personIdent, type = fagsakType, institusjon = institusjon) + return stegService.håndterNyBehandlingOgSendInfotrygdFeed( + NyBehandling( + kategori = kategori, + underkategori = underkategori, + søkersIdent = personIdent, + behandlingType = type, + behandlingÅrsak = årsak, + navIdent = navIdent, + søknadMottattDato = søknadMottattDato, + søknadsinfo = søknadsinfo, + fagsakId = fagsak.id, + ), + ) + } + + @Transactional + fun journalfør( + request: RestJournalføring, + journalpostId: String, + behandlendeEnhet: String, + oppgaveId: String, + ): String { + val tilknyttedeBehandlingIder: MutableList = request.tilknyttedeBehandlingIder.toMutableList() + val journalpost = integrasjonClient.hentJournalpost(journalpostId) + val brevkode = journalpost.dokumenter?.firstNotNullOfOrNull { it.brevkode } + + if (request.opprettOgKnyttTilNyBehandling) { + val nyBehandling = + opprettBehandlingOgEvtFagsakForJournalføring( + personIdent = request.bruker.id, + navIdent = request.navIdent, + type = request.nyBehandlingstype, + årsak = request.nyBehandlingsårsak, + kategori = request.kategori, + underkategori = request.underkategori, + søknadMottattDato = request.datoMottatt?.toLocalDate(), + søknadsinfo = brevkode?.let { + Søknadsinfo( + journalpostId = journalpost.journalpostId, + brevkode = it, + erDigital = journalpost.kanal == NAV_NO, + ) + }, + fagsakType = request.fagsakType, + institusjon = request.institusjon, + ) + tilknyttedeBehandlingIder.add(nyBehandling.id.toString()) + } + + val (sak, behandlinger) = lagreJournalpostOgKnyttFagsakTilJournalpost(tilknyttedeBehandlingIder, journalpostId) + + val erSøknad = brevkode == Søknadstype.ORDINÆR.søknadskode || brevkode == Søknadstype.UTVIDET.søknadskode + + if (erSøknad && !request.opprettOgKnyttTilNyBehandling) { + behandlinger.forEach { tidligereBehandling -> + lagreNedSøknadsinfoKnyttetTilBehandling(journalpost, brevkode!!, tidligereBehandling) + } + } + + oppdaterLogiskeVedlegg(request) + + oppdaterOgFerdigstill( + request = request.oppdaterMedDokumentOgSak(sak), + journalpostId = journalpostId, + behandlendeEnhet = behandlendeEnhet, + oppgaveId = oppgaveId, + behandlinger = behandlinger, + ) + + journalføringMetrikk.tellManuellJournalføringsmetrikker(journalpost, request, behandlinger) + return sak.fagsakId ?: "" + } + + fun knyttJournalpostTilFagsakOgFerdigstillOppgave( + request: RestFerdigstillOppgaveKnyttJournalpost, + oppgaveId: Long, + ): String { + val tilknyttedeBehandlingIder: MutableList = request.tilknyttedeBehandlingIder.toMutableList() + + val journalpost = hentJournalpost(request.journalpostId) + journalpost.sak?.fagsakId + + if (request.opprettOgKnyttTilNyBehandling) { + val brevkode = journalpost.dokumenter?.firstNotNullOfOrNull { it.brevkode } + val nyBehandling = + opprettBehandlingOgEvtFagsakForJournalføring( + personIdent = request.bruker.id, + navIdent = request.navIdent, + type = request.nyBehandlingstype, + årsak = request.nyBehandlingsårsak, + kategori = request.kategori, + underkategori = request.underkategori, + søknadMottattDato = request.datoMottatt?.toLocalDate(), + søknadsinfo = brevkode?.let { + Søknadsinfo( + journalpostId = journalpost.journalpostId, + brevkode = it, + erDigital = journalpost.kanal == NAV_NO, + ) + }, + ) + tilknyttedeBehandlingIder.add(nyBehandling.id.toString()) + } + + val (sak) = lagreJournalpostOgKnyttFagsakTilJournalpost(tilknyttedeBehandlingIder, journalpost.journalpostId) + + integrasjonClient.ferdigstillOppgave(oppgaveId = oppgaveId) + + return sak.fagsakId ?: "" + } + + fun lagreJournalpostOgKnyttFagsakTilJournalpost( + tilknyttedeBehandlingIder: List, + journalpostId: String, + ): Pair> { + val behandlinger = tilknyttedeBehandlingIder.map { + behandlingHentOgPersisterService.hent(it.toLong()) + } + + val journalpost = hentJournalpost(journalpostId) + behandlinger.forEach { + journalføringRepository.save( + DbJournalpost( + behandling = it, + journalpostId = journalpostId, + type = DbJournalpostType.valueOf(journalpost.journalposttype.name), + ), + ) + } + + val fagsak = when (tilknyttedeBehandlingIder.isNotEmpty()) { + true -> { + behandlinger.map { it.fagsak }.toSet().firstOrNull() + ?: throw FunksjonellFeil( + melding = "Behandlings'idene tilhørerer ikke samme fagsak, eller vi fant ikke fagsaken.", + frontendFeilmelding = "Oppslag på fagsak feilet med behandlingene som ble sendt inn.", + ) + } + + false -> null + } + + val sak = Sak( + fagsakId = fagsak?.id?.toString(), + fagsaksystem = fagsak?.let { FagsakSystem.BA.name }, + sakstype = fagsak?.let { FAGSAK.type } ?: GENERELL_SAK.type, + arkivsaksystem = null, + arkivsaksnummer = null, + ) + + return Pair(sak, behandlinger) + } + + private fun håndterLogiskeVedlegg(request: RestOppdaterJournalpost, journalpostId: String) { + val fjernedeVedlegg = + request.eksisterendeLogiskeVedlegg.partition { request.logiskeVedlegg.contains(it) }.second + val nyeVedlegg = request.logiskeVedlegg.partition { request.eksisterendeLogiskeVedlegg.contains(it) }.second + + val dokumentInfoId = request.dokumentInfoId.takeIf { it.isNotEmpty() } + ?: hentJournalpost(journalpostId).dokumenter?.first()?.dokumentInfoId + ?: error("Fant ikke dokumentInfoId på journalpost") + + fjernedeVedlegg.forEach { + integrasjonClient.slettLogiskVedlegg(it.logiskVedleggId, dokumentInfoId) + } + nyeVedlegg.forEach { + integrasjonClient.leggTilLogiskVedlegg(LogiskVedleggRequest(it.tittel), dokumentInfoId) + } + } + + private fun oppdaterOgFerdigstill( + request: OppdaterJournalpostRequest, + journalpostId: String, + behandlendeEnhet: String, + oppgaveId: String, + behandlinger: List, + ) { + runCatching { + secureLogger.info("Oppdaterer journalpost $journalpostId med $request") + integrasjonClient.oppdaterJournalpost(request, journalpostId) + genererOgOpprettLogg(journalpostId, behandlinger) + secureLogger.info("Ferdigstiller journalpost $journalpostId") + integrasjonClient.ferdigstillJournalpost( + journalpostId = journalpostId, + journalførendeEnhet = behandlendeEnhet, + ) + integrasjonClient.ferdigstillOppgave(oppgaveId = oppgaveId.toLong()) + }.onFailure { + hentJournalpost(journalpostId).journalstatus.apply { + if (this == FERDIGSTILT) { + integrasjonClient.ferdigstillOppgave(oppgaveId = oppgaveId.toLong()) + } else { + throw it + } + } + } + } + + private fun opprettOppgaveFor(behandling: Behandling, navIdent: String) { + OpprettOppgaveTask.opprettTask( + behandlingId = behandling.id, + oppgavetype = Oppgavetype.BehandleSak, + fristForFerdigstillelse = LocalDate.now(), + tilordnetRessurs = navIdent, + ) + } + + private fun genererOgOpprettLogg(journalpostId: String, behandlinger: List) { + val journalpost = hentJournalpost(journalpostId) + val loggTekst = journalpost.dokumenter?.fold("") { loggTekst, dokumentInfo -> + loggTekst + + "${dokumentInfo.tittel}" + + dokumentInfo.logiskeVedlegg?.fold("") { logiskeVedleggTekst, logiskVedlegg -> + logiskeVedleggTekst + + "\n\u2002\u2002${logiskVedlegg.tittel}" + } + "\n" + } ?: throw FunksjonellFeil( + "Fant ingen dokumenter", + frontendFeilmelding = "Noe gikk galt. Prøv igjen eller kontakt brukerstøtte hvis problemet vedvarer.", + ) + + val datoMottatt = journalpost.datoMottatt ?: throw FunksjonellFeil( + "Fant ingen dokumenter", + frontendFeilmelding = "Noe gikk galt. Prøv igjen eller kontakt brukerstøtte hvis problemet vedvarer.", + ) + behandlinger.forEach { + loggService.opprettMottattDokument( + behandling = it, + tekst = loggTekst, + mottattDato = datoMottatt, + ) + } + } + + private fun lagreNedSøknadsinfoKnyttetTilBehandling( + journalpost: Journalpost, + brevkode: String, + behandling: Behandling, + ) { + behandlingSøknadsinfoService.lagreNedSøknadsinfo( + mottattDato = journalpost.datoMottatt?.toLocalDate() ?: LocalDate.now(), + søknadsinfo = Søknadsinfo( + journalpostId = journalpost.journalpostId, + brevkode = brevkode, + erDigital = journalpost.kanal == NAV_NO, + ), + behandling = behandling, + ) + } + + companion object { + + private val logger = LoggerFactory.getLogger(InnkommendeJournalføringService::class.java) + const val NAV_NO = "NAV_NO" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/Journalf\303\270ringController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/Journalf\303\270ringController.kt" new file mode 100644 index 000000000..0810288bb --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/Journalf\303\270ringController.kt" @@ -0,0 +1,95 @@ +package no.nav.familie.ba.sak.integrasjoner.journalføring + +import jakarta.validation.Valid +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.ekstern.restDomene.RestJournalføring +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.PersonIdent +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.journalpost.Journalpost +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/journalpost") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class JournalføringController( + private val innkommendeJournalføringService: InnkommendeJournalføringService, + private val tilgangService: TilgangService, +) { + + @GetMapping(path = ["/{journalpostId}/hent"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentJournalpost(@PathVariable journalpostId: String): ResponseEntity> { + return ResponseEntity.ok(Ressurs.success(innkommendeJournalføringService.hentJournalpost(journalpostId))) + } + + @PostMapping(path = ["/for-bruker"]) + fun hentJournalposterForBruker(@RequestBody personIdentBody: PersonIdent): ResponseEntity>> { + return ResponseEntity.ok( + Ressurs.success( + innkommendeJournalføringService.hentJournalposterForBruker( + personIdentBody.ident, + ), + ), + ) + } + + @GetMapping("/{journalpostId}/hent/{dokumentInfoId}") + fun hentDokument( + @PathVariable journalpostId: String, + @PathVariable dokumentInfoId: String, + ): ResponseEntity> { + return ResponseEntity.ok( + Ressurs.success( + innkommendeJournalføringService.hentDokument( + journalpostId, + dokumentInfoId, + ), + ), + ) + } + + @GetMapping( + path = ["/{journalpostId}/dokument/{dokumentInfoId}"], + produces = [MediaType.APPLICATION_PDF_VALUE], + ) + fun hentDokumentBytearray( + @PathVariable journalpostId: String, + @PathVariable dokumentInfoId: String, + ): ResponseEntity { + return ResponseEntity.ok(innkommendeJournalføringService.hentDokument(journalpostId, dokumentInfoId)) + } + + @PostMapping(path = ["/{journalpostId}/journalfør/{oppgaveId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun journalførV2( + @PathVariable journalpostId: String, + @PathVariable oppgaveId: String, + @RequestParam(name = "journalfoerendeEnhet") journalførendeEnhet: String, + @RequestBody @Valid + request: RestJournalføring, + ): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "journalføre", + ) + + if (request.dokumenter.any { it.dokumentTittel == null || it.dokumentTittel == "" }) { + throw FunksjonellFeil("Minst ett av dokumentene mangler dokumenttittel.") + } + + val fagsakId = + innkommendeJournalføringService.journalfør(request, journalpostId, journalførendeEnhet, oppgaveId) + return ResponseEntity.ok(Ressurs.success(fagsakId, "Journalpost $journalpostId Journalført")) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/Journalf\303\270ringMetrikk.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/Journalf\303\270ringMetrikk.kt" new file mode 100644 index 000000000..3d8456d17 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/Journalf\303\270ringMetrikk.kt" @@ -0,0 +1,64 @@ +package no.nav.familie.ba.sak.integrasjoner.journalføring + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.ekstern.restDomene.RestJournalføring +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.kontrakter.felles.journalpost.Journalpost +import org.springframework.stereotype.Component + +@Component +class JournalføringMetrikk { + + private val antallGenerellSak: Counter = Metrics.counter("journalfoering.behandling", "behandlingstype", "Fagsak") + + private val antallTilBehandling = BehandlingType.values().associateWith { + Metrics.counter("journalfoering.behandling", "behandlingstype", it.visningsnavn) + } + + private val journalpostTittelMap = mapOf( + "søknad om ordinær barnetrygd" to "Søknad om ordinær barnetrygd", + "søknad om barnetrygd ordinær" to "Søknad om ordinær barnetrygd", + "søknad om utvidet barnetrygd" to "Søknad om utvidet barnetrygd", + "søknad om barnetrygd utvidet" to "Søknad om utvidet barnetrygd", + "ettersendelse til søknad om ordinær barnetrygd" to "Ettersendelse til søknad om ordinær barnetrygd", + "ettersendelse til søknad om barnetrygd ordinær" to "Ettersendelse til søknad om ordinær barnetrygd", + "ettersendelse til søknad om utvidet barnetrygd" to "Ettersendelse til søknad om utvidet barnetrygd", + "ettersendelse til søknad om barnetrygd utvidet" to "Ettersendelse til søknad om utvidet barnetrygd", + "tilleggskjema eøs" to "Tilleggskjema EØS", + ) + + private val antallJournalpostTittel = journalpostTittelMap.values.toSet().associateWith { + Metrics.counter( + "journalfoering.journalpost", + "tittel", + it, + ) + } + + private val antallJournalpostTittelFritekst = + Metrics.counter("journalfoering.journalpost", "tittel", "Fritekst") + + fun tellManuellJournalføringsmetrikker( + journalpost: Journalpost?, + oppdatert: RestJournalføring, + behandlinger: List, + ) { + if (oppdatert.knyttTilFagsak) { + behandlinger.forEach { + antallTilBehandling[it.type]?.increment() + } + } else { + antallGenerellSak.increment() + } + + val tittelLower = oppdatert.journalpostTittel?.lowercase() + val kjentTittel = journalpostTittelMap[tittelLower] + if (kjentTittel != null) { + antallJournalpostTittel[kjentTittel]?.increment() + } else { + antallJournalpostTittelFritekst.increment() + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/Utg\303\245endeJournalf\303\270ringService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/Utg\303\245endeJournalf\303\270ringService.kt" new file mode 100644 index 000000000..de769b2a2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/Utg\303\245endeJournalf\303\270ringService.kt" @@ -0,0 +1,124 @@ +package no.nav.familie.ba.sak.integrasjoner.journalføring + +import no.nav.familie.ba.sak.common.MDCOperations +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.DEFAULT_JOURNALFØRENDE_ENHET +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.http.client.RessursException +import no.nav.familie.kontrakter.felles.BrukerIdType +import no.nav.familie.kontrakter.felles.dokarkiv.AvsenderMottaker +import no.nav.familie.kontrakter.felles.dokarkiv.Dokumenttype +import no.nav.familie.kontrakter.felles.dokarkiv.v2.ArkiverDokumentRequest +import no.nav.familie.kontrakter.felles.dokarkiv.v2.Dokument +import no.nav.familie.kontrakter.felles.dokarkiv.v2.Filtype +import no.nav.familie.kontrakter.felles.dokarkiv.v2.Førsteside +import no.nav.familie.kontrakter.felles.journalpost.Bruker +import no.nav.familie.kontrakter.felles.journalpost.JournalposterForBrukerRequest +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service + +@Service +class UtgåendeJournalføringService( + private val integrasjonClient: IntegrasjonClient, +) { + + fun journalførManueltBrev( + fnr: String, + fagsakId: String, + journalførendeEnhet: String, + brev: ByteArray, + dokumenttype: Dokumenttype, + førsteside: Førsteside?, + avsenderMottaker: AvsenderMottaker? = null, + tilManuellMottakerEllerVerge: Boolean = false, + ): String { + return journalførDokument( + fnr = fnr, + fagsakId = fagsakId, + journalførendeEnhet = journalførendeEnhet, + brev = listOf( + Dokument( + dokument = brev, + filtype = Filtype.PDFA, + dokumenttype = dokumenttype, + ), + ), + førsteside = førsteside, + avsenderMottaker = avsenderMottaker, + tilManuellMottakerEllerVerge = tilManuellMottakerEllerVerge, + ) + } + + fun journalførDokument( + fnr: String, + fagsakId: String, + journalførendeEnhet: String? = null, + brev: List, + vedlegg: List = emptyList(), + førsteside: Førsteside? = null, + behandlingId: Long? = null, + avsenderMottaker: AvsenderMottaker? = null, + tilManuellMottakerEllerVerge: Boolean = false, + ): String { + if (journalførendeEnhet == DEFAULT_JOURNALFØRENDE_ENHET) { + logger.warn("Informasjon om enhet mangler på bruker og er satt til fallback-verdi, $DEFAULT_JOURNALFØRENDE_ENHET") + } + + val eksternReferanseId = + genererEksternReferanseIdForJournalpost(fagsakId, behandlingId, tilManuellMottakerEllerVerge) + + val journalpostId = try { + val journalpost = integrasjonClient.journalførDokument( + ArkiverDokumentRequest( + fnr = fnr, + avsenderMottaker = avsenderMottaker, + forsøkFerdigstill = true, + hoveddokumentvarianter = brev, + vedleggsdokumenter = vedlegg, + fagsakId = fagsakId, + journalførendeEnhet = journalførendeEnhet, + førsteside = førsteside, + eksternReferanseId = eksternReferanseId, + ), + ) + + if (!journalpost.ferdigstilt) { + error("Klarte ikke ferdigstille journalpost med id ${journalpost.journalpostId}") + } + + journalpost.journalpostId + } catch (ressursException: RessursException) { + when (ressursException.httpStatus) { + HttpStatus.CONFLICT -> { + logger.warn( + "Klarte ikke journalføre dokument på fagsak=$fagsakId fordi det allerede finnes en journalpost " + + "med eksternReferanseId=$eksternReferanseId. Bruker eksisterende journalpost.", + ) + + hentEksisterendeJournalpost(eksternReferanseId, fnr) + } + else -> throw ressursException + } + } + + return journalpostId + } + + private fun hentEksisterendeJournalpost( + eksternReferanseId: String, + fnr: String, + ): String = integrasjonClient.hentJournalposterForBruker( + JournalposterForBrukerRequest( + brukerId = Bruker(id = fnr, type = BrukerIdType.FNR), + antall = 50, + ), + ).single { it.eksternReferanseId == eksternReferanseId }.journalpostId + + fun genererEksternReferanseIdForJournalpost(fagsakId: String, behandlingId: Long?, tilVerge: Boolean) = + "${fagsakId}_${behandlingId}${if (tilVerge) "_verge" else ""}_${MDCOperations.getCallId()}" + + companion object { + + private val logger = LoggerFactory.getLogger(this::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/DbJournalpost.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/DbJournalpost.kt" new file mode 100644 index 000000000..d8f37a40d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/DbJournalpost.kt" @@ -0,0 +1,67 @@ +package no.nav.familie.ba.sak.integrasjoner.journalføring.domene + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import java.time.LocalDateTime +import java.util.Objects + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Journalpost") +@Table(name = "JOURNALPOST") +data class DbJournalpost( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "journalpost_seq_generator") + @SequenceGenerator(name = "journalpost_seq_generator", sequenceName = "journalpost_seq", allocationSize = 50) + val id: Long = 0, + + @Column(name = "opprettet_av", nullable = false, updatable = false) + val opprettetAv: String = SikkerhetContext.hentSaksbehandlerNavn(), + + @Column(name = "opprettet_tid", nullable = false, updatable = false) + val opprettetTidspunkt: LocalDateTime = LocalDateTime.now(), + + @JsonIgnore + @ManyToOne + @JoinColumn(name = "fk_behandling_id", nullable = false) + val behandling: Behandling, + + @Column(name = "journalpost_id") + val journalpostId: String, + + @Enumerated(EnumType.STRING) + @Column(name = "type") + val type: DbJournalpostType? = null, +) { + override fun hashCode(): Int { + return Objects.hashCode(id) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DbJournalpost + + if (id != other.id) return false + + return true + } +} + +enum class DbJournalpostType { + I, U +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/Journalf\303\270ringRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/Journalf\303\270ringRepository.kt" new file mode 100644 index 000000000..6963ddffd --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/Journalf\303\270ringRepository.kt" @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.integrasjoner.journalføring.domene + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface JournalføringRepository : JpaRepository { + + @Query("SELECT j FROM Journalpost j JOIN j.behandling b WHERE b.id = :behandlingId") + fun findByBehandlingId(behandlingId: Long): List +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/LogiskVedleggRequest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/LogiskVedleggRequest.kt" new file mode 100644 index 000000000..740b031fe --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/LogiskVedleggRequest.kt" @@ -0,0 +1,3 @@ +package no.nav.familie.ba.sak.integrasjoner.journalføring.domene + +data class LogiskVedleggRequest(val tittel: String) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/LogiskVedleggResponse.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/LogiskVedleggResponse.kt" new file mode 100644 index 000000000..c241b9dd2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/LogiskVedleggResponse.kt" @@ -0,0 +1,3 @@ +package no.nav.familie.ba.sak.integrasjoner.journalføring.domene + +data class LogiskVedleggResponse(val logiskVedleggId: Long) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/OppdaterJournalpostRequest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/OppdaterJournalpostRequest.kt" new file mode 100644 index 000000000..084725e05 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/OppdaterJournalpostRequest.kt" @@ -0,0 +1,35 @@ +package no.nav.familie.ba.sak.integrasjoner.journalføring.domene + +import com.fasterxml.jackson.annotation.JsonInclude +import no.nav.familie.kontrakter.felles.dokarkiv.AvsenderMottaker +import no.nav.familie.kontrakter.felles.journalpost.DokumentInfo +import no.nav.familie.kontrakter.felles.journalpost.Sak + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class OppdaterJournalpostRequest( + val avsenderMottaker: AvsenderMottaker?, + val bruker: Bruker, + val tema: String? = "BAR", + val tittel: String? = null, + val sak: Sak? = null, + val dokumenter: List? = null, +) + +class Bruker( + val id: String, + val idType: IdType? = IdType.FNR, + val navn: String, +) + +enum class IdType { + FNR, ORGNR, AKTOERID +} + +enum class Sakstype(val type: String) { + FAGSAK("FAGSAK"), + GENERELL_SAK("GENERELL_SAK"), +} + +enum class FagsakSystem { + BA, +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/OppdaterJournalpostResponse.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/OppdaterJournalpostResponse.kt" new file mode 100644 index 000000000..5193f7d6d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/domene/OppdaterJournalpostResponse.kt" @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.integrasjoner.journalføring.domene + +data class OppdaterJournalpostResponse( + val journalpostId: String? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveController.kt new file mode 100644 index 000000000..ada95e095 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveController.kt @@ -0,0 +1,179 @@ +package no.nav.familie.ba.sak.integrasjoner.oppgave + +import jakarta.validation.Valid +import no.nav.familie.ba.sak.common.RessursUtils.illegalState +import no.nav.familie.ba.sak.ekstern.restDomene.RestFerdigstillOppgaveKnyttJournalpost +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPersonInfo +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.journalføring.InnkommendeJournalføringService +import no.nav.familie.ba.sak.integrasjoner.oppgave.domene.DataForManuellJournalføring +import no.nav.familie.ba.sak.integrasjoner.oppgave.domene.RestFinnOppgaveRequest +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.journalpost.Journalpost +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveResponseDto +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.slf4j.LoggerFactory +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/oppgave") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class OppgaveController( + private val oppgaveService: OppgaveService, + private val fagsakService: FagsakService, + private val personidentService: PersonidentService, + private val integrasjonClient: IntegrasjonClient, + private val personopplysningerService: PersonopplysningerService, + private val tilgangService: TilgangService, + private val innkommendeJournalføringService: InnkommendeJournalføringService, +) { + private val logger = LoggerFactory.getLogger(OppgaveController::class.java) + + @PostMapping( + path = ["/hent-oppgaver"], + consumes = [MediaType.APPLICATION_JSON_VALUE], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + fun hentOppgaver(@RequestBody restFinnOppgaveRequest: RestFinnOppgaveRequest): ResponseEntity> = + try { + val oppgaver: FinnOppgaveResponseDto = + oppgaveService.hentOppgaver(restFinnOppgaveRequest.tilFinnOppgaveRequest()) + ResponseEntity.ok().body(Ressurs.success(oppgaver, "Finn oppgaver OK")) + } catch (e: Throwable) { + illegalState("Henting av oppgaver feilet", e) + } + + @PostMapping(path = ["/{oppgaveId}/fordel"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun fordelOppgave( + @PathVariable(name = "oppgaveId") oppgaveId: Long, + @RequestParam("saksbehandler") saksbehandler: String, + ): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "fordele oppgave", + ) + + val oppgaveIdFraRespons = + oppgaveService.fordelOppgave( + oppgaveId = oppgaveId, + saksbehandler = saksbehandler, + overstyrFordeling = false, + ) + + return ResponseEntity.ok().body(Ressurs.success(oppgaveIdFraRespons)) + } + + @PostMapping(path = ["/{oppgaveId}/tilbakestill"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun tilbakestillFordelingPåOppgave(@PathVariable(name = "oppgaveId") oppgaveId: Long): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "tilbakestille fordeling på oppgave", + ) + + Result.runCatching { + oppgaveService.tilbakestillFordelingPåOppgave(oppgaveId) + }.fold( + onSuccess = { return ResponseEntity.ok().body(Ressurs.Companion.success(it)) }, + onFailure = { return illegalState("Feil ved tilbakestilling av tildeling på oppgave", it) }, + ) + } + + @GetMapping(path = ["/{oppgaveId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentDataForManuellJournalføring(@PathVariable(name = "oppgaveId") oppgaveId: Long): ResponseEntity> { + val oppgave = oppgaveService.hentOppgave(oppgaveId) + val aktør = oppgave.aktoerId?.let { personidentService.hentAktør(it) } + + val dataForManuellJournalføring = DataForManuellJournalføring( + oppgave = oppgave, + journalpost = null, + person = aktør?.let { + personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(it) + .tilRestPersonInfo(it.aktivFødselsnummer()) + }, + minimalFagsak = if (aktør != null) fagsakService.hentMinimalFagsakForPerson(aktør).data else null, + ) + + val journalpost: Journalpost? = + if (oppgave.journalpostId == null) null else integrasjonClient.hentJournalpost(oppgave.journalpostId!!) + + return when (journalpost) { + null -> { + ResponseEntity.ok(Ressurs.success(dataForManuellJournalføring)) + } + + else -> ResponseEntity.ok( + Ressurs.success( + dataForManuellJournalføring.copy( + journalpost = journalpost, + ), + ), + ) + } + } + + @GetMapping("/{oppgaveId}/ferdigstill") + fun ferdigstillOppgave(@PathVariable oppgaveId: Long): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "ferdigstill oppgave", + ) + val oppgave = oppgaveService.hentOppgave(oppgaveId) + oppgaveService.ferdigstillOppgave(oppgave) + + return ResponseEntity.ok(Ressurs.success("Oppgaven lukket")) + } + + @PostMapping(path = ["/{oppgaveId}/ferdigstillOgKnyttjournalpost"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun ferdigstillOppgaveOgKnyttJournalpostTilBehandling( + @PathVariable oppgaveId: Long, + @RequestBody @Valid + request: RestFerdigstillOppgaveKnyttJournalpost, + ): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "ferdigstill oppgave og knytt journalpost", + ) + // Validerer at oppgave med gitt oppgaveId eksisterer + oppgaveService.hentOppgave(oppgaveId) + + val fagsakId = innkommendeJournalføringService.knyttJournalpostTilFagsakOgFerdigstillOppgave(request, oppgaveId) + + return ResponseEntity.ok(Ressurs.success(fagsakId, "Oppgaven $oppgaveId er lukket")) + } + + @PostMapping("/hent-frister-for-apne-utvidet-barnetrygd-behandlinger") + fun hentFristerForÅpneUtvidetBarnetrygdBehandlinger(): ResponseEntity> { + val behandleSakOppgaveFrister = oppgaveService.hentFristerForÅpneUtvidetBarnetrygdBehandlinger() + + return ResponseEntity.ok(Ressurs.success(behandleSakOppgaveFrister)) + } + + @PostMapping("/fjern-behandles-av-applikasjon") + fun fjernBehandlesAvApplikasjonFor(@RequestBody oppgaver: List): ResponseEntity> { + val fjernetBehandlesAvApplikasjonForOppgaver = oppgaveService.fjernBehandlesAvApplikasjon( + oppgaver, + ) + logger.info("Fjernet behandlesAvApplikasjon for oppgaver=$fjernetBehandlesAvApplikasjonForOppgaver") + return ResponseEntity.ok( + Ressurs.success( + "Fjernet behandlesAvApplikasjon for $fjernetBehandlesAvApplikasjonForOppgaver", + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveService.kt new file mode 100644 index 000000000..c53c68814 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveService.kt @@ -0,0 +1,352 @@ +package no.nav.familie.ba.sak.integrasjoner.oppgave + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.oppgave.domene.DbOppgave +import no.nav.familie.ba.sak.integrasjoner.oppgave.domene.OppgaveRepository +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.domene.ArbeidsfordelingPåBehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.beregning.endringstidspunkt.AktørId +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.ba.sak.task.dto.ManuellOppgaveType +import no.nav.familie.kontrakter.felles.Tema +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveRequest +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveResponseDto +import no.nav.familie.kontrakter.felles.oppgave.IdentGruppe +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import no.nav.familie.kontrakter.felles.oppgave.OppgaveIdentV2 +import no.nav.familie.kontrakter.felles.oppgave.OppgaveResponse +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.kontrakter.felles.oppgave.OpprettOppgaveRequest +import no.nav.familie.kontrakter.felles.oppgave.StatusEnum.FEILREGISTRERT +import no.nav.familie.kontrakter.felles.oppgave.StatusEnum.FERDIGSTILT +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.Period +import java.time.format.DateTimeFormatter + +@Service +class OppgaveService( + private val integrasjonClient: IntegrasjonClient, + private val behandlingRepository: BehandlingRepository, + private val oppgaveRepository: OppgaveRepository, + private val arbeidsfordelingPåBehandlingRepository: ArbeidsfordelingPåBehandlingRepository, + private val opprettTaskService: OpprettTaskService, + private val loggService: LoggService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, +) { + private val antallOppgaveTyper: MutableMap = mutableMapOf() + + fun opprettOppgave( + behandlingId: Long, + oppgavetype: Oppgavetype, + fristForFerdigstillelse: LocalDate, + tilordnetNavIdent: String? = null, + beskrivelse: String? = null, + manuellOppgaveType: ManuellOppgaveType? = null, + ): String { + val behandling = behandlingHentOgPersisterService.hent(behandlingId = behandlingId) + val fagsakId = behandling.fagsak.id + + val eksisterendeOppgave = + oppgaveRepository.findByOppgavetypeAndBehandlingAndIkkeFerdigstilt(oppgavetype, behandling) + + return if (eksisterendeOppgave != null && oppgavetype != Oppgavetype.Journalføring) { + logger.warn( + "Fant eksisterende oppgave med samme oppgavetype som ikke er ferdigstilt " + + "ved opprettelse av ny oppgave $eksisterendeOppgave. " + + "Vi oppretter ikke ny oppgave, men gjenbruker eksisterende.", + ) + + eksisterendeOppgave.gsakId + } else { + val arbeidsfordelingsenhet = + arbeidsfordelingPåBehandlingRepository.finnArbeidsfordelingPåBehandling(behandling.id) + + if (arbeidsfordelingsenhet == null) { + logger.warn( + "Fant ikke behandlende enhet på behandling ${behandling.id} " + + "ved opprettelse av $oppgavetype-oppgave.", + ) + } + + val opprettOppgave = OpprettOppgaveRequest( + ident = OppgaveIdentV2(ident = behandling.fagsak.aktør.aktørId, gruppe = IdentGruppe.AKTOERID), + saksId = fagsakId.toString(), + tema = Tema.BAR, + oppgavetype = oppgavetype, + fristFerdigstillelse = fristForFerdigstillelse, + beskrivelse = lagOppgaveTekst(fagsakId, beskrivelse), + enhetsnummer = arbeidsfordelingsenhet?.behandlendeEnhetId, + behandlingstema = behandling.tilOppgaveBehandlingTema().value, + behandlingstype = behandling.kategori.tilOppgavebehandlingType().value, + tilordnetRessurs = tilordnetNavIdent, + behandlesAvApplikasjon = when { + oppgavetyperSomBehandlesAvBaSak.contains(oppgavetype) -> "familie-ba-sak" + manuellOppgaveType?.settBehandlesAvApplikasjon == true -> "familie-ba-sak" + else -> null + }, + ) + val opprettetOppgaveId = integrasjonClient.opprettOppgave(opprettOppgave).oppgaveId.toString() + + val oppgave = DbOppgave(gsakId = opprettetOppgaveId, behandling = behandling, type = oppgavetype) + oppgaveRepository.save(oppgave) + + økTellerForAntallOppgaveTyper(oppgavetype) + + opprettetOppgaveId + } + } + + fun opprettOppgaveForManuellBehandling( + behandling: Behandling, + begrunnelse: String = "", + opprettLogginnslag: Boolean = false, + manuellOppgaveType: ManuellOppgaveType, + ): String { + logger.info("Sender autovedtak til manuell behandling, se secureLogger for mer detaljer.") + secureLogger.info("Sender autovedtak til manuell behandling. Begrunnelse: $begrunnelse") + opprettTaskService.opprettOppgaveForManuellBehandlingTask( + behandlingId = behandling.id, + beskrivelse = begrunnelse, + manuellOppgaveType = manuellOppgaveType, + ) + + if (opprettLogginnslag) { + loggService.opprettAutovedtakTilManuellBehandling( + behandling = behandling, + tekst = begrunnelse, + ) + } + + return begrunnelse + } + + fun opprettOppgaveForFødselshendelse( + ident: AktørId, + oppgavetype: Oppgavetype, + fristForFerdigstillelse: LocalDate, + beskrivelse: String, + ): String { + val opprettOppgave = OpprettOppgaveRequest( + ident = OppgaveIdentV2(ident = ident, gruppe = IdentGruppe.AKTOERID), + tema = Tema.BAR, + oppgavetype = oppgavetype, + fristFerdigstillelse = fristForFerdigstillelse, + beskrivelse = beskrivelse, + saksId = null, + behandlingstema = null, + enhetsnummer = null, + ) + val opprettetOppgaveId = integrasjonClient.opprettOppgave(opprettOppgave).oppgaveId.toString() + + økTellerForAntallOppgaveTyper(oppgavetype) + + return opprettetOppgaveId + } + + private fun økTellerForAntallOppgaveTyper(oppgavetype: Oppgavetype) { + if (antallOppgaveTyper[oppgavetype] == null) { + antallOppgaveTyper[oppgavetype] = Metrics.counter("oppgave.opprettet", "type", oppgavetype.name) + } + + antallOppgaveTyper[oppgavetype]?.increment() + } + + fun patchOppgave(patchOppgave: Oppgave): OppgaveResponse { + return integrasjonClient.patchOppgave(patchOppgave) + } + + fun patchOppgaverForBehandling(behandling: Behandling, copyOppgave: (oppgave: Oppgave) -> Oppgave?) { + hentOppgaverSomIkkeErFerdigstilt(behandling).forEach { dbOppgave -> + val oppgave = hentOppgave(dbOppgave.gsakId.toLong()) + if (oppgave.status != FERDIGSTILT) { + copyOppgave(oppgave)?.also { patchOppgave(it) } + } else { + logger.warn("Kan ikke patch'e ferdigstilt oppgave ${oppgave.id}, for behandling ${behandling.id}.") + dbOppgave.erFerdigstilt = true + oppgaveRepository.saveAndFlush(dbOppgave) + } + } + } + + fun endreTilordnetEnhetPåOppgaverForBehandling(behandling: Behandling, nyEnhet: String) { + hentOppgaverSomIkkeErFerdigstilt(behandling).forEach { dbOppgave -> + val oppgave = hentOppgave(dbOppgave.gsakId.toLong()) + logger.info("Oppdaterer enhet fra ${oppgave.tildeltEnhetsnr} til $nyEnhet på oppgave ${oppgave.id}") + if (oppgave.status == FERDIGSTILT && oppgave.oppgavetype == Oppgavetype.VurderLivshendelse.value) { + dbOppgave.erFerdigstilt = true + } else { + integrasjonClient.tilordneEnhetForOppgave(oppgaveId = oppgave.id!!, nyEnhet = nyEnhet) + } + } + } + + fun fordelOppgave(oppgaveId: Long, saksbehandler: String, overstyrFordeling: Boolean = false): String { + if (!overstyrFordeling) { + val oppgave = integrasjonClient.finnOppgaveMedId(oppgaveId) + if (oppgave.tilordnetRessurs != null) { + throw FunksjonellFeil( + melding = "Oppgaven er allerede fordelt", + frontendFeilmelding = "Oppgaven er allerede fordelt til ${oppgave.tilordnetRessurs}", + ) + } + } + + return integrasjonClient.fordelOppgave(oppgaveId, saksbehandler).oppgaveId.toString() + } + + fun tilbakestillFordelingPåOppgave(oppgaveId: Long): Oppgave { + integrasjonClient.fordelOppgave(oppgaveId, null) + return integrasjonClient.finnOppgaveMedId(oppgaveId) + } + + fun hentOppgaverSomIkkeErFerdigstilt(oppgavetype: Oppgavetype, behandling: Behandling): List { + return oppgaveRepository.finnOppgaverSomSkalFerdigstilles(oppgavetype, behandling) + } + + fun hentOppgaverSomIkkeErFerdigstilt(behandling: Behandling): List { + return oppgaveRepository.findByBehandlingAndIkkeFerdigstilt(behandling) + } + + fun hentOppgave(oppgaveId: Long): Oppgave { + return integrasjonClient.finnOppgaveMedId(oppgaveId) + } + + fun ferdigstillOppgaver(behandlingId: Long, oppgavetype: Oppgavetype) { + oppgaveRepository.finnOppgaverSomSkalFerdigstilles( + oppgavetype = oppgavetype, + behandling = behandlingHentOgPersisterService.hent( + behandlingId = behandlingId, + ), + ).forEach { + val oppgave = hentOppgave(it.gsakId.toLong()) + + if (oppgave.status == FERDIGSTILT || oppgave.status == FEILREGISTRERT) { + it.erFerdigstilt = true + + // Her sørger vi for at oppgaver som blir ferdigstilt riktig får samme status hos oss selv om en av de andre dbOppgavene feiler. + oppgaveRepository.saveAndFlush(it) + } else { + try { + integrasjonClient.ferdigstillOppgave(it.gsakId.toLong()) + + it.erFerdigstilt = true + // I tilfelle noen av de andre dbOppgavene feiler + oppgaveRepository.saveAndFlush(it) + } catch (exception: Exception) { + throw Feil(message = "Klarte ikke å ferdigstille oppgave med id ${it.gsakId}.", cause = exception) + } + } + } + } + + fun forlengFristÅpneOppgaverPåBehandling(behandlingId: Long, forlengelse: Period) { + val dbOppgaver = oppgaveRepository.findByBehandlingIdAndIkkeFerdigstilt(behandlingId) + + dbOppgaver.forEach { dbOppgave -> + val gammelOppgave = hentOppgave(dbOppgave.gsakId.toLong()) + val oppgaveErAvsluttet = gammelOppgave.ferdigstiltTidspunkt != null + + when { + gammelOppgave.id == null -> + logger.warn("Finner ikke oppgave ${dbOppgave.gsakId} ved oppdatering av frist") + + gammelOppgave.fristFerdigstillelse == null -> + logger.warn("Oppgave ${dbOppgave.gsakId} har ingen oppgavefrist ved oppdatering av frist") + + oppgaveErAvsluttet -> {} + else -> { + val nyFrist = LocalDate.parse(gammelOppgave.fristFerdigstillelse!!).plus(forlengelse) + val nyOppgave = gammelOppgave.copy(fristFerdigstillelse = nyFrist?.toString()) + integrasjonClient.oppdaterOppgave(nyOppgave.id!!, nyOppgave) + } + } + } + } + + fun hentFristerForÅpneUtvidetBarnetrygdBehandlinger(): String { + val åpneUtvidetBarnetrygdBehandlinger = behandlingRepository.finnÅpneUtvidetBarnetrygdBehandlinger() + + val behandlingsfrister = åpneUtvidetBarnetrygdBehandlinger.map { behandling -> + val behandleSakOppgave = try { + oppgaveRepository.findByOppgavetypeAndBehandlingAndIkkeFerdigstilt(Oppgavetype.BehandleSak, behandling) + ?.let { + hentOppgave(it.gsakId.toLong()) + } + } catch (e: Exception) { + secureLogger.warn("Klarte ikke hente BehandleSak-oppgaven for behandling ${behandling.id}", e) + null + } + "${behandling.id};${behandleSakOppgave?.id};${behandleSakOppgave?.fristFerdigstillelse}\n" + }.reduce { csvString, behandlingsfrist -> csvString + behandlingsfrist } + + return "behandlingId;oppgaveId;frist\n" + behandlingsfrister + } + + fun settFristÅpneOppgaverPåBehandlingTil(behandlingId: Long, nyFrist: LocalDate) { + val dbOppgaver = oppgaveRepository.findByBehandlingIdAndIkkeFerdigstilt(behandlingId) + + dbOppgaver.forEach { dbOppgave -> + val gammelOppgave = hentOppgave(dbOppgave.gsakId.toLong()) + val oppgaveErAvsluttet = gammelOppgave.ferdigstiltTidspunkt != null + + when { + gammelOppgave.id == null -> logger.warn("Finner ikke oppgave ${dbOppgave.gsakId} ved oppdatering av frist") + oppgaveErAvsluttet -> {} + else -> { + val nyOppgave = gammelOppgave.copy(fristFerdigstillelse = nyFrist.toString()) + integrasjonClient.oppdaterOppgave(nyOppgave.id!!, nyOppgave) + } + } + } + } + + fun lagOppgaveTekst(fagsakId: Long, beskrivelse: String? = null): String { + return if (beskrivelse != null) { + beskrivelse + "\n" + } else { + "" + } + + "----- Opprettet av familie-ba-sak ${LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)} --- \n" + + "https://barnetrygd.intern.nav.no/fagsak/$fagsakId" + } + + fun hentOppgaver(finnOppgaveRequest: FinnOppgaveRequest): FinnOppgaveResponseDto { + return integrasjonClient.hentOppgaver(finnOppgaveRequest) + } + + fun ferdigstillOppgave(oppgave: Oppgave) { + require(oppgave.id != null) { "Oppgaven må ha en id for å kunne ferdigstilles" } + integrasjonClient.ferdigstillOppgave(oppgaveId = oppgave.id!!) + } + + fun fjernBehandlesAvApplikasjon(oppgaver: List): Set { + return oppgaver.fold(LinkedHashSet()) { accumulator, oppgaveId -> + val dbOppgave = oppgaveRepository.findByGsakId(oppgaveId.toString()) + if (dbOppgave != null) { + integrasjonClient.fjernBehandlesAvApplikasjon(oppgaveId) + accumulator.add(oppgaveId) + } + accumulator + } + } + + companion object { + private val logger = LoggerFactory.getLogger(OppgaveService::class.java) + private val oppgavetyperSomBehandlesAvBaSak = listOf( + Oppgavetype.BehandleSak, + Oppgavetype.GodkjenneVedtak, + Oppgavetype.BehandleUnderkjentVedtak, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/domene/DataForManuellJournalf\303\270ring.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/domene/DataForManuellJournalf\303\270ring.kt" new file mode 100644 index 000000000..e68df50d2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/domene/DataForManuellJournalf\303\270ring.kt" @@ -0,0 +1,13 @@ +package no.nav.familie.ba.sak.integrasjoner.oppgave.domene + +import no.nav.familie.ba.sak.ekstern.restDomene.RestMinimalFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonInfo +import no.nav.familie.kontrakter.felles.journalpost.Journalpost +import no.nav.familie.kontrakter.felles.oppgave.Oppgave + +data class DataForManuellJournalføring( + val oppgave: Oppgave, + val person: RestPersonInfo?, + val journalpost: Journalpost?, + val minimalFagsak: RestMinimalFagsak?, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/domene/DbOppgave.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/domene/DbOppgave.kt new file mode 100644 index 000000000..e1406f5d0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/domene/DbOppgave.kt @@ -0,0 +1,45 @@ +package no.nav.familie.ba.sak.integrasjoner.oppgave.domene + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import java.time.LocalDateTime + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Oppgave") +@Table(name = "OPPGAVE") +data class DbOppgave( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "oppgave_seq_generator") + @SequenceGenerator(name = "oppgave_seq_generator", sequenceName = "oppgave_seq", allocationSize = 50) + val id: Long = 0, + + @ManyToOne + @JoinColumn(name = "fk_behandling_id", nullable = false, updatable = false) + val behandling: Behandling, + + @Column(name = "gsak_id", nullable = false, updatable = false) + val gsakId: String, + + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false, updatable = false) + val type: Oppgavetype, + + @Column(name = "opprettet_tid", nullable = false, updatable = false) + val opprettetTidspunkt: LocalDateTime = LocalDateTime.now(), + + @Column(name = "ferdigstilt", nullable = false, updatable = true) + var erFerdigstilt: Boolean = false, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/domene/OppgaveRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/domene/OppgaveRepository.kt new file mode 100644 index 000000000..4a0ea875d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/domene/OppgaveRepository.kt @@ -0,0 +1,28 @@ +package no.nav.familie.ba.sak.integrasjoner.oppgave.domene + +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository + +@Repository +interface OppgaveRepository : JpaRepository { + + @Query(value = "SELECT o FROM Oppgave o WHERE o.erFerdigstilt = false AND o.behandling = :behandling AND o.type = :oppgavetype") + fun findByOppgavetypeAndBehandlingAndIkkeFerdigstilt(oppgavetype: Oppgavetype, behandling: Behandling): DbOppgave? + + @Query(value = "SELECT o FROM Oppgave o WHERE o.erFerdigstilt = false AND o.behandling = :behandling AND o.type = :oppgavetype") + fun finnOppgaverSomSkalFerdigstilles(oppgavetype: Oppgavetype, behandling: Behandling): List + + @Query(value = "SELECT o FROM Oppgave o WHERE o.erFerdigstilt = false AND o.behandling = :behandling") + fun findByBehandlingAndIkkeFerdigstilt(behandling: Behandling): List + + @Query(value = "SELECT o FROM Oppgave o WHERE o.erFerdigstilt = false AND o.behandling.id = :behandlingId") + fun findByBehandlingIdAndIkkeFerdigstilt(behandlingId: Long): List + + fun findByGsakId(gsakId: String): DbOppgave? + + @Query(value = "SELECT o FROM Oppgave o WHERE o.erFerdigstilt = true AND o.behandling.id = :behandlingId AND o.type = :oppgavetype") + fun findByBehandlingAndTypeAndErFerdigstilt(behandlingId: Long, oppgavetype: Oppgavetype): List +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/domene/RestFinnOppgaveRequest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/domene/RestFinnOppgaveRequest.kt new file mode 100644 index 000000000..e87cfbcdd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/domene/RestFinnOppgaveRequest.kt @@ -0,0 +1,49 @@ +package no.nav.familie.ba.sak.integrasjoner.oppgave.domene + +import no.nav.familie.kontrakter.felles.Behandlingstema +import no.nav.familie.kontrakter.felles.Tema +import no.nav.familie.kontrakter.felles.oppgave.Behandlingstype +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveRequest +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import java.time.LocalDate +import java.time.LocalDateTime + +data class RestFinnOppgaveRequest( + val behandlingstema: String? = null, + val behandlingstype: String? = null, + val oppgavetype: String? = null, + val enhet: String? = null, + val saksbehandler: String? = null, + val journalpostId: String? = null, + val tilordnetRessurs: String? = null, + val tildeltRessurs: Boolean? = null, + val opprettetFomTidspunkt: LocalDateTime? = null, + val opprettetTomTidspunkt: LocalDateTime? = null, + val fristFomDato: LocalDate? = null, + val fristTomDato: LocalDate? = null, + val aktivFomDato: LocalDate? = null, + val aktivTomDato: LocalDate? = null, + val limit: Long? = null, + val offset: Long? = null, +) { + + fun tilFinnOppgaveRequest(): FinnOppgaveRequest = FinnOppgaveRequest( + tema = Tema.BAR, + behandlingstema = Behandlingstema.values().find { it.value == this.behandlingstema }, + behandlingstype = Behandlingstype.values().find { it.value == this.behandlingstype }, + oppgavetype = Oppgavetype.values().find { it.value == this.oppgavetype }, + enhet = this.enhet, + saksbehandler = this.saksbehandler, + journalpostId = this.journalpostId, + tildeltRessurs = this.tildeltRessurs, + tilordnetRessurs = this.tilordnetRessurs, + opprettetFomTidspunkt = this.opprettetFomTidspunkt, + opprettetTomTidspunkt = this.opprettetTomTidspunkt, + fristFomDato = this.fristFomDato, + fristTomDato = this.fristTomDato, + aktivFomDato = this.aktivFomDato, + aktivTomDato = this.aktivTomDato, + limit = this.limit, + offset = this.offset, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/organisasjon/OrganisasjonService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/organisasjon/OrganisasjonService.kt new file mode 100644 index 000000000..1ced9733b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/organisasjon/OrganisasjonService.kt @@ -0,0 +1,14 @@ +package no.nav.familie.ba.sak.integrasjoner.organisasjon + +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.kontrakter.felles.organisasjon.Organisasjon +import org.springframework.stereotype.Service + +@Service +class OrganisasjonService(private val integrasjonClient: IntegrasjonClient) { + + fun hentOrganisasjon(orgnummer: String): Organisasjon { + val organisasjon = integrasjonClient.hentOrganisasjon(orgnummer) + return organisasjon + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlBolkResponse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlBolkResponse.kt new file mode 100644 index 000000000..1a9445d27 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlBolkResponse.kt @@ -0,0 +1,18 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl + +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlError +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlExtensions + +data class PdlBolkResponse(val data: PersonBolk?, val errors: List?, val extensions: PdlExtensions?) { + + fun errorMessages(): String { + return errors?.joinToString { it -> it.message } ?: "" + } + fun harAdvarsel(): Boolean { + return !extensions?.warnings.isNullOrEmpty() + } +} + +data class PersonBolk(val personBolk: List>) + +data class PersonDataBolk(val ident: String, val code: String, val person: T?) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlIdentRestClient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlIdentRestClient.kt new file mode 100644 index 000000000..5f4af0d50 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlIdentRestClient.kt @@ -0,0 +1,81 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl + +import no.nav.familie.ba.sak.common.kallEksternTjeneste +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.IdentInformasjon +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlBaseResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlHentIdenterResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlIdenter +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlPersonRequest +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlPersonRequestVariables +import no.nav.familie.http.client.AbstractRestClient +import no.nav.familie.http.util.UriUtil +import no.nav.familie.kontrakter.felles.Tema +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.cache.annotation.Cacheable +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.client.RestOperations +import java.net.URI + +@Component +class PdlIdentRestClient( + @Value("\${PDL_URL}") pdlBaseUrl: URI, + @Qualifier("jwtBearer") val restTemplate: RestOperations, +) : AbstractRestClient(restTemplate, "pdl.ident") { + protected val pdlUri = UriUtil.uri(pdlBaseUrl, PATH_GRAPHQL) + + private val hentIdenterQuery = hentGraphqlQuery("hentIdenter") + + @Cacheable("identer", cacheManager = "shortCache") + fun hentIdenter(personIdent: String, historikk: Boolean): List { + val pdlIdenter = hentIdenter(personIdent) + + return if (historikk) { + pdlIdenter.identer.map { it } + } else { + pdlIdenter.identer.filter { !it.historisk }.map { it } + } + } + + private fun hentIdenter(personIdent: String): PdlIdenter { + val pdlPersonRequest = PdlPersonRequest( + variables = PdlPersonRequestVariables(personIdent), + query = hentIdenterQuery, + ) + val pdlResponse: PdlBaseResponse = kallEksternTjeneste( + tjeneste = "pdl", + uri = pdlUri, + formål = "Hent identer", + ) { + postForEntity( + pdlUri, + pdlPersonRequest, + httpHeaders(), + ) + } + + return feilsjekkOgReturnerData( + ident = personIdent, + pdlResponse = pdlResponse, + ) { + it.pdlIdenter + } + } + + fun httpHeaders(): HttpHeaders { + return HttpHeaders().apply { + contentType = MediaType.APPLICATION_JSON + accept = listOf(MediaType.APPLICATION_JSON) + add("Tema", PDL_TEMA) + add("behandlingsnummer", Tema.BAR.behandlingsnummer) + } + } + + companion object { + + private const val PATH_GRAPHQL = "graphql" + private const val PDL_TEMA = "BAR" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlRestClient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlRestClient.kt new file mode 100644 index 000000000..ae1faf2dd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlRestClient.kt @@ -0,0 +1,250 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl + +import no.nav.familie.ba.sak.common.kallEksternTjeneste +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.Doedsfall +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.DødsfallData +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlBaseResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlHentPersonRelasjonerResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlHentPersonResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlOppholdResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlPersonRequest +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlPersonRequestVariables +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlStatsborgerskapResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlUtenlandskAdresssePersonUtenlandskAdresse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlUtenlandskAdressseResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlVergeResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.VergemaalEllerFremtidsfullmakt +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.http.client.AbstractRestClient +import no.nav.familie.http.util.UriUtil +import no.nav.familie.kontrakter.felles.Tema +import no.nav.familie.kontrakter.felles.personopplysning.Opphold +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap +import org.apache.commons.lang3.StringUtils +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.cache.annotation.Cacheable +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.stereotype.Service +import org.springframework.web.client.RestOperations +import java.net.URI +import java.time.LocalDate + +@Service +class PdlRestClient( + @Value("\${PDL_URL}") pdlBaseUrl: URI, + @Qualifier("jwtBearer") val restTemplate: RestOperations, + val personidentService: PersonidentService, +) : AbstractRestClient(restTemplate, "pdl.personinfo") { + + protected val pdlUri = UriUtil.uri(pdlBaseUrl, PATH_GRAPHQL) + + @Cacheable("personopplysninger", cacheManager = "shortCache") + fun hentPerson(aktør: Aktør, personInfoQuery: PersonInfoQuery): PersonInfo { + val pdlPersonRequest = PdlPersonRequest( + variables = PdlPersonRequestVariables(aktør.aktivFødselsnummer()), + query = personInfoQuery.graphQL, + ) + val pdlResponse: PdlBaseResponse = kallEksternTjeneste( + tjeneste = "pdl", + uri = pdlUri, + formål = "Hent person med query ${personInfoQuery.name}", + ) { + postForEntity( + pdlUri, + pdlPersonRequest, + httpHeaders(), + ) + } + + return feilsjekkOgReturnerData( + ident = aktør.aktivFødselsnummer(), + pdlResponse = pdlResponse, + ) { pdlPerson -> + pdlPerson.person!!.validerOmPersonKanBehandlesIFagsystem() + + val forelderBarnRelasjon: Set = + when (personInfoQuery) { + PersonInfoQuery.MED_RELASJONER_OG_REGISTERINFORMASJON -> { + pdlPerson.person.forelderBarnRelasjon + .mapNotNull { relasjon -> + relasjon.relatertPersonsIdent?.let { ident -> + ForelderBarnRelasjon( + aktør = personidentService.hentAktør(ident), + relasjonsrolle = relasjon.relatertPersonsRolle, + ) + } + }.toSet() + } + else -> emptySet() + } + + pdlPerson.person.let { + PersonInfo( + fødselsdato = LocalDate.parse(it.foedsel.first().foedselsdato), + navn = it.navn.firstOrNull()?.fulltNavn(), + kjønn = it.kjoenn.firstOrNull()?.kjoenn, + forelderBarnRelasjon = forelderBarnRelasjon, + adressebeskyttelseGradering = it.adressebeskyttelse.firstOrNull()?.gradering, + bostedsadresser = it.bostedsadresse, + statsborgerskap = it.statsborgerskap, + opphold = it.opphold, + sivilstander = it.sivilstand, + dødsfall = hentDødsfallDataFraListeMedDødsfall(it.doedsfall), + kontaktinformasjonForDoedsbo = it.kontaktinformasjonForDoedsbo.firstOrNull(), + ) + } + } + } + + private fun hentDødsfallDataFraListeMedDødsfall(doedsfall: List): DødsfallData? { + val dødsdato = doedsfall.filter { it.doedsdato != null } + .map { it.doedsdato } + .firstOrNull() + + if (doedsfall.isEmpty() || dødsdato == null) { + return null + } + return DødsfallData(erDød = true, dødsdato = dødsdato) + } + + @Cacheable("vergedata", cacheManager = "shortCache") + fun hentVergemaalEllerFremtidsfullmakt(aktør: Aktør): List { + val pdlPersonRequest = PdlPersonRequest( + variables = PdlPersonRequestVariables(aktør.aktivFødselsnummer()), + query = hentGraphqlQuery("verge"), + ) + val pdlResponse: PdlBaseResponse = kallEksternTjeneste( + tjeneste = "pdl", + uri = pdlUri, + formål = "Hent vergemål eller fremtidsfullmakt", + ) { postForEntity(pdlUri, pdlPersonRequest, httpHeaders()) } + + return feilsjekkOgReturnerData( + ident = aktør.aktivFødselsnummer(), + pdlResponse = pdlResponse, + ) { + it.person!!.vergemaalEllerFremtidsfullmakt + } + } + + fun hentStatsborgerskapUtenHistorikk(aktør: Aktør): List { + val pdlPersonRequest = PdlPersonRequest( + variables = PdlPersonRequestVariables(aktør.aktivFødselsnummer()), + query = hentGraphqlQuery("statsborgerskap-uten-historikk"), + ) + val pdlResponse: PdlBaseResponse = kallEksternTjeneste( + tjeneste = "pdl", + uri = pdlUri, + formål = "Hent statsborgerskap uten historikk", + ) { postForEntity(pdlUri, pdlPersonRequest, httpHeaders()) } + + return feilsjekkOgReturnerData( + ident = aktør.aktivFødselsnummer(), + pdlResponse = pdlResponse, + ) { + it.person!!.statsborgerskap + } + } + + fun hentOppholdUtenHistorikk(aktør: Aktør): List { + val pdlPersonRequest = PdlPersonRequest( + variables = PdlPersonRequestVariables(aktør.aktivFødselsnummer()), + query = hentGraphqlQuery("opphold-uten-historikk"), + ) + val pdlResponse: PdlBaseResponse = kallEksternTjeneste( + tjeneste = "pdl", + uri = pdlUri, + formål = "Hent opphold uten historikk", + ) { + postForEntity(pdlUri, pdlPersonRequest, httpHeaders()) + } + + return feilsjekkOgReturnerData( + ident = aktør.aktivFødselsnummer(), + pdlResponse = pdlResponse, + ) { + it.person!!.opphold + } + } + + fun hentUtenlandskBostedsadresse(aktør: Aktør): PdlUtenlandskAdresssePersonUtenlandskAdresse? { + val pdlPersonRequest = PdlPersonRequest( + variables = PdlPersonRequestVariables(aktør.aktivFødselsnummer()), + query = hentGraphqlQuery("bostedsadresse-utenlandsk"), + ) + val pdlResponse: PdlBaseResponse = kallEksternTjeneste( + tjeneste = "pdl", + uri = pdlUri, + formål = "Hent utenlandsk bostedsadresse", + ) { + postForEntity(pdlUri, pdlPersonRequest, httpHeaders()) + } + + val bostedsadresser = feilsjekkOgReturnerData( + ident = aktør.aktivFødselsnummer(), + pdlResponse = pdlResponse, + ) { + it.person!!.bostedsadresse + } + return bostedsadresser.firstOrNull { bostedsadresse -> bostedsadresse.utenlandskAdresse != null }?.utenlandskAdresse + } + + /** + * Til bruk for migrering. Vurder hentPerson som gir maskerte data for personer med adressebeskyttelse. + * + */ + fun hentForelderBarnRelasjoner(aktør: Aktør): List { + val pdlPersonRequest = PdlPersonRequest( + variables = PdlPersonRequestVariables(aktør.aktivFødselsnummer()), + query = hentGraphqlQuery("hentperson-relasjoner"), + ) + val pdlResponse: PdlBaseResponse = kallEksternTjeneste( + tjeneste = "pdl", + uri = pdlUri, + formål = "Hent forelder barn relasjoner", + ) { + postForEntity(pdlUri, pdlPersonRequest, httpHeaders()) + } + + return feilsjekkOgReturnerData( + ident = aktør.aktivFødselsnummer(), + pdlResponse = pdlResponse, + ) { + it.person!!.forelderBarnRelasjon + } + } + + fun httpHeaders(): HttpHeaders { + return HttpHeaders().apply { + contentType = MediaType.APPLICATION_JSON + accept = listOf(MediaType.APPLICATION_JSON) + add("Tema", PDL_TEMA) + add("behandlingsnummer", Tema.BAR.behandlingsnummer) + } + } + + companion object { + + private const val PATH_GRAPHQL = "graphql" + private const val PDL_TEMA = "BAR" + } +} + +enum class PersonInfoQuery(val graphQL: String) { + ENKEL(hentGraphqlQuery("hentperson-enkel")), + MED_RELASJONER_OG_REGISTERINFORMASJON(hentGraphqlQuery("hentperson-med-relasjoner-og-registerinformasjon")), + NAVN_OG_ADRESSE(hentGraphqlQuery("hentperson-navn-og-adresse")), +} + +fun hentGraphqlQuery(pdlResource: String): String { + return PersonInfoQuery::class.java.getResource("/pdl/$pdlResource.graphql").readText().graphqlCompatible() +} + +private fun String.graphqlCompatible(): String { + return StringUtils.normalizeSpace(this.replace("\n", "")) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlUtil.kt new file mode 100644 index 000000000..c11993a87 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlUtil.kt @@ -0,0 +1,58 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl + +import no.nav.familie.ba.sak.common.PdlNotFoundException +import no.nav.familie.ba.sak.common.PdlRequestException +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlBaseResponse +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +val logger: Logger = LoggerFactory.getLogger("PdlUtil") + +inline fun feilsjekkOgReturnerData( + ident: String?, + pdlResponse: PdlBaseResponse, + dataMapper: (DATA) -> T?, +): T { + if (pdlResponse.harFeil()) { + if (pdlResponse.errors?.any { it.extensions?.notFound() == true } == true) { + throw PdlNotFoundException() + } + secureLogger.error("Feil ved henting av ${T::class} fra PDL: ${pdlResponse.errorMessages()}") + throw PdlRequestException("Feil ved henting av ${T::class} fra PDL. Se secure logg for detaljer.") + } + + if (pdlResponse.harAdvarsel()) { + logger.warn("Advarsel ved henting av ${T::class} fra PDL. Se securelogs for detaljer.") + secureLogger.warn("Advarsel ved henting av ${T::class} fra PDL: ${pdlResponse.extensions?.warnings}") + } + + val data = dataMapper.invoke(pdlResponse.data) + if (data == null) { + val errorMelding = if (ident != null) "Feil ved oppslag på ident $ident. " else "Feil ved oppslag på person." + secureLogger.error( + errorMelding + + "PDL rapporterte ingen feil men returnerte tomt datafelt", + ) + throw PdlRequestException("Manglende ${T::class} ved feilfri respons fra PDL. Se secure logg for detaljer.") + } + return data +} + +inline fun feilsjekkOgReturnerData(pdlResponse: PdlBolkResponse): Map { + if (pdlResponse.data == null) { + secureLogger.error("Data fra pdl er null ved bolkoppslag av ${T::class} fra PDL: ${pdlResponse.errorMessages()}") + throw PdlRequestException("Data er null fra PDL - ${T::class}. Se secure logg for detaljer.") + } + + val feil = pdlResponse.data.personBolk.filter { it.code != "ok" }.associate { it.ident to it.code } + if (feil.isNotEmpty()) { + secureLogger.error("Feil ved henting av ${T::class} fra PDL: $feil") + throw PdlRequestException("Feil ved henting av ${T::class} fra PDL. Se secure logg for detaljer.") + } + if (pdlResponse.harAdvarsel()) { + logger.warn("Advarsel ved henting av ${T::class} fra PDL. Se securelogs for detaljer.") + secureLogger.warn("Advarsel ved henting av ${T::class} fra PDL: ${pdlResponse.extensions?.warnings}") + } + return pdlResponse.data.personBolk.associateBy({ it.ident }, { it.person!! }) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PersonopplysningerService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PersonopplysningerService.kt new file mode 100644 index 000000000..8ba709ed7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PersonopplysningerService.kt @@ -0,0 +1,135 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl + +import com.neovisionaries.i18n.CountryCode +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.PdlPersonKanIkkeBehandlesIFagsystem +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollService +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjonMaskert +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.VergeData +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import no.nav.familie.kontrakter.felles.personopplysning.Opphold +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class PersonopplysningerService( + private val pdlRestClient: PdlRestClient, + private val systemOnlyPdlRestClient: SystemOnlyPdlRestClient, + private val familieIntegrasjonerTilgangskontrollService: FamilieIntegrasjonerTilgangskontrollService, +) { + + fun hentPersoninfoMedRelasjonerOgRegisterinformasjon(aktør: Aktør): PersonInfo { + val personinfo = hentPersoninfoMedQuery(aktør, PersonInfoQuery.MED_RELASJONER_OG_REGISTERINFORMASJON) + val identerMedAdressebeskyttelse = mutableSetOf>() + val relasjonsidenter = personinfo.forelderBarnRelasjon.map { it.aktør.aktivFødselsnummer() } + val tilgangPerIdent = familieIntegrasjonerTilgangskontrollService.sjekkTilgangTilPersoner(relasjonsidenter) + val forelderBarnRelasjon = personinfo.forelderBarnRelasjon.mapNotNull { + if (tilgangPerIdent.getValue(it.aktør.aktivFødselsnummer()).harTilgang) { + try { + val relasjonsinfo = hentPersoninfoEnkel(it.aktør) + ForelderBarnRelasjon( + aktør = it.aktør, + relasjonsrolle = it.relasjonsrolle, + fødselsdato = relasjonsinfo.fødselsdato, + navn = relasjonsinfo.navn, + adressebeskyttelseGradering = relasjonsinfo.adressebeskyttelseGradering, + ) + } catch (pdlPersonKanIkkeBehandlesIFagsystem: PdlPersonKanIkkeBehandlesIFagsystem) { + logger.warn("Ignorerer relasjon: ${pdlPersonKanIkkeBehandlesIFagsystem.årsak}") + secureLogger.warn("Ignorerer relasjon ${it.aktør.aktivFødselsnummer()} til ${aktør.aktivFødselsnummer()}: ${pdlPersonKanIkkeBehandlesIFagsystem.årsak}") + null + } + } else { + identerMedAdressebeskyttelse.add(Pair(it.aktør, it.relasjonsrolle)) + null + } + }.toSet() + + val forelderBarnRelasjonMaskert = identerMedAdressebeskyttelse.map { + ForelderBarnRelasjonMaskert( + relasjonsrolle = it.second, + adressebeskyttelseGradering = hentAdressebeskyttelseSomSystembruker(it.first), + ) + }.toSet() + return personinfo.copy( + forelderBarnRelasjon = forelderBarnRelasjon, + forelderBarnRelasjonMaskert = forelderBarnRelasjonMaskert, + ) + } + + fun hentPersoninfoEnkel(aktør: Aktør): PersonInfo { + return hentPersoninfoMedQuery(aktør, PersonInfoQuery.ENKEL) + } + + fun hentPersoninfoNavnOgAdresse(aktør: Aktør): PersonInfo { + return hentPersoninfoMedQuery(aktør, PersonInfoQuery.NAVN_OG_ADRESSE) + } + + private fun hentPersoninfoMedQuery(aktør: Aktør, personInfoQuery: PersonInfoQuery): PersonInfo { + return pdlRestClient.hentPerson(aktør, personInfoQuery) + } + + fun hentVergeData(aktør: Aktør): VergeData { + return VergeData(harVerge = harVerge(aktør).harVerge) + } + + fun harVerge(aktør: Aktør): VergeResponse { + val harVerge = pdlRestClient.hentVergemaalEllerFremtidsfullmakt(aktør) + .any { it.type != "stadfestetFremtidsfullmakt" } + + return VergeResponse(harVerge) + } + + fun hentGjeldendeStatsborgerskap(aktør: Aktør): Statsborgerskap { + return pdlRestClient.hentStatsborgerskapUtenHistorikk(aktør).firstOrNull() ?: UKJENT_STATSBORGERSKAP + } + + fun hentGjeldendeOpphold(aktør: Aktør): Opphold = pdlRestClient.hentOppholdUtenHistorikk(aktør).firstOrNull() + ?: throw Feil( + message = "Bruker mangler opphold", + frontendFeilmelding = "Person (${aktør.aktivFødselsnummer()}) mangler opphold.", + ) + + fun hentLandkodeAlpha2UtenlandskBostedsadresse(aktør: Aktør): String { + val landkode = pdlRestClient.hentUtenlandskBostedsadresse(aktør)?.landkode + + if (landkode.isNullOrEmpty()) return UKJENT_LANDKODE + + return if (landkode.length == 3) { + if (landkode == PDL_UKJENT_LANDKODE) { + UKJENT_LANDKODE + } else { + CountryCode.getByAlpha3Code(landkode.uppercase()).alpha2 + } + } else { + landkode + } + } + + fun hentAdressebeskyttelseSomSystembruker(aktør: Aktør): ADRESSEBESKYTTELSEGRADERING = + systemOnlyPdlRestClient.hentAdressebeskyttelse(aktør).tilAdressebeskyttelse() + + companion object { + + const val UKJENT_LANDKODE = "ZZ" + const val PDL_UKJENT_LANDKODE = "XUK" + val UKJENT_STATSBORGERSKAP = + Statsborgerskap( + land = PDL_UKJENT_LANDKODE, + bekreftelsesdato = null, + gyldigFraOgMed = null, + gyldigTilOgMed = null, + ) + private val logger: Logger = + LoggerFactory.getLogger(PersonopplysningerService::class.java) + } +} + +data class VergeResponse(val harVerge: Boolean) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/SystemOnlyPdlRestClient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/SystemOnlyPdlRestClient.kt new file mode 100644 index 000000000..515f451f7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/SystemOnlyPdlRestClient.kt @@ -0,0 +1,74 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl + +import no.nav.familie.ba.sak.common.kallEksternTjeneste +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlAdressebeskyttelsePerson +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlAdressebeskyttelseResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlBaseResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlPersonBolkRequest +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlPersonBolkRequestVariables +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlPersonRequest +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlPersonRequestVariables +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.familie.kontrakter.felles.personopplysning.Adressebeskyttelse +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.cache.annotation.Cacheable +import org.springframework.stereotype.Service +import org.springframework.web.client.RestOperations +import java.net.URI + +@Service +class SystemOnlyPdlRestClient( + @Value("\${PDL_URL}") pdlBaseUrl: URI, + @Qualifier("jwtBearerClientCredentials") override val restTemplate: RestOperations, + override val personidentService: PersonidentService, +) : PdlRestClient(pdlBaseUrl, restTemplate, personidentService) { + + @Cacheable("adressebeskyttelse", cacheManager = "shortCache") + fun hentAdressebeskyttelse(aktør: Aktør): List { + val pdlPersonRequest = PdlPersonRequest( + variables = PdlPersonRequestVariables(aktør.aktivFødselsnummer()), + query = hentGraphqlQuery("hent-adressebeskyttelse"), + ) + + val pdlResponse: PdlBaseResponse = kallEksternTjeneste( + tjeneste = "pdl", + uri = pdlUri, + formål = "Hent adressebeskyttelse", + ) { + postForEntity(pdlUri, pdlPersonRequest, httpHeaders()) + } + + return feilsjekkOgReturnerData( + ident = aktør.aktivFødselsnummer(), + pdlResponse = pdlResponse, + ) { + it.person!!.adressebeskyttelse + } + } + + @Cacheable("adressebeskyttelsebolk", cacheManager = "shortCache") + fun hentAdressebeskyttelseBolk(personIdentList: List): Map { + val pdlPersonRequest = PdlPersonBolkRequest( + variables = PdlPersonBolkRequestVariables(personIdentList), + query = hentGraphqlQuery("hent-adressebeskyttelse-bolk"), + ) + + val pdlResponse: PdlBolkResponse = kallEksternTjeneste( + tjeneste = "pdl", + uri = pdlUri, + formål = "Hent adressebeskyttelse i bolk", + ) { + postForEntity(pdlUri, pdlPersonRequest, httpHeaders()) + } + + return feilsjekkOgReturnerData( + pdlResponse = pdlResponse, + ) + } +} + +fun List.tilAdressebeskyttelse() = + this.firstOrNull()?.gradering ?: ADRESSEBESKYTTELSEGRADERING.UGRADERT diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlAdressebeskyttelseResponse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlAdressebeskyttelseResponse.kt new file mode 100644 index 000000000..7d32c3241 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlAdressebeskyttelseResponse.kt @@ -0,0 +1,6 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl.domene + +import no.nav.familie.kontrakter.felles.personopplysning.Adressebeskyttelse + +class PdlAdressebeskyttelseResponse(val person: PdlAdressebeskyttelsePerson?) +class PdlAdressebeskyttelsePerson(val adressebeskyttelse: List) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlBaseResponse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlBaseResponse.kt new file mode 100644 index 000000000..9e1da6d08 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlBaseResponse.kt @@ -0,0 +1,36 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl.domene + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +open class PdlBaseResponse( + val data: T, + open val errors: List?, + open val extensions: PdlExtensions?, +) { + + fun harFeil(): Boolean { + return errors != null && errors!!.isNotEmpty() + } + fun harAdvarsel(): Boolean { + return !extensions?.warnings.isNullOrEmpty() + } + + fun errorMessages(): String { + return errors?.joinToString { it -> it.message } ?: "" + } +} + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PdlError( + val message: String, + val extensions: PdlErrorExtensions?, +) + +data class PdlErrorExtensions(val code: String?) { + + fun notFound() = code == "not_found" +} + +data class PdlExtensions(val warnings: List?) + +data class PdlWarning(val details: Any?, val id: String?, val message: String?, val query: String?) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlD\303\270dsfallResponse.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlD\303\270dsfallResponse.kt" new file mode 100644 index 000000000..73a704f46 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlD\303\270dsfallResponse.kt" @@ -0,0 +1,6 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl.domene + +class PdlDødsfallResponse(val person: PdlDødsfallPerson?) +class PdlDødsfallPerson(val doedsfall: List) + +class Doedsfall(val doedsdato: String?) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlHentIdenterResponse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlHentIdenterResponse.kt new file mode 100644 index 000000000..895b75a71 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlHentIdenterResponse.kt @@ -0,0 +1,11 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl.domene + +class PdlHentIdenterResponse(val pdlIdenter: PdlIdenter?) + +data class PdlIdenter(val identer: List) + +data class IdentInformasjon( + val ident: String, + val historisk: Boolean, + val gruppe: String, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlHentPersonRelasjonResponse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlHentPersonRelasjonResponse.kt new file mode 100644 index 000000000..b697a5216 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlHentPersonRelasjonResponse.kt @@ -0,0 +1,11 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl.domene + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import no.nav.familie.kontrakter.felles.personopplysning.ForelderBarnRelasjon + +class PdlHentPersonRelasjonerResponse(val person: PdlPersonRelasjonData?) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PdlPersonRelasjonData( + val forelderBarnRelasjon: List = emptyList(), +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlHentPersonResponse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlHentPersonResponse.kt new file mode 100644 index 000000000..a8d8ba453 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlHentPersonResponse.kt @@ -0,0 +1,69 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl.domene + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import no.nav.familie.ba.sak.common.PdlPersonKanIkkeBehandlesIFagsystem +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.kontrakter.felles.personopplysning.Adressebeskyttelse +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.ForelderBarnRelasjon +import no.nav.familie.kontrakter.felles.personopplysning.Opphold +import no.nav.familie.kontrakter.felles.personopplysning.Sivilstand +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap + +data class PdlHentPersonResponse(val person: PdlPersonData?) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PdlPersonData( + val folkeregisteridentifikator: List, + val foedsel: List, + val navn: List = emptyList(), + val kjoenn: List = emptyList(), + val forelderBarnRelasjon: List = emptyList(), + val adressebeskyttelse: List = emptyList(), + val sivilstand: List = emptyList(), + val bostedsadresse: List, + val opphold: List = emptyList(), + val statsborgerskap: List = emptyList(), + val doedsfall: List = emptyList(), + val kontaktinformasjonForDoedsbo: List = emptyList(), +) { + fun validerOmPersonKanBehandlesIFagsystem() { + if (foedsel.isEmpty()) throw PdlPersonKanIkkeBehandlesIFagsystem("mangler fødselsdato") + if (folkeregisteridentifikator.firstOrNull()?.status == FolkeregisteridentifikatorStatus.OPPHOERT) { + throw PdlPersonKanIkkeBehandlesIFagsystem( + "er opphørt", + ) + } + } +} + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PdlFolkeregisteridentifikator( + val identifikasjonsnummer: String?, + val status: FolkeregisteridentifikatorStatus, + val type: FolkeregisteridentifikatorType?, +) + +enum class FolkeregisteridentifikatorStatus { I_BRUK, OPPHOERT } +enum class FolkeregisteridentifikatorType { FNR, DNR } + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PdlFødselsDato(val foedselsdato: String?) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PdlNavn( + val fornavn: String, + val mellomnavn: String? = null, + val etternavn: String, +) { + + fun fulltNavn(): String { + return when (mellomnavn) { + null -> "$fornavn $etternavn" + else -> "$fornavn $mellomnavn $etternavn" + } + } +} + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PdlKjoenn(val kjoenn: Kjønn) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlOppholdResponse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlOppholdResponse.kt new file mode 100644 index 000000000..8704d3f1c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlOppholdResponse.kt @@ -0,0 +1,6 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl.domene + +import no.nav.familie.kontrakter.felles.personopplysning.Opphold + +class PdlOppholdResponse(val person: PdlOppholdPerson?) +class PdlOppholdPerson(val opphold: List?) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlPersonRequest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlPersonRequest.kt new file mode 100644 index 000000000..da08b6583 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlPersonRequest.kt @@ -0,0 +1,11 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl.domene + +data class PdlPersonRequest( + val variables: PdlPersonRequestVariables, + val query: String, +) + +data class PdlPersonBolkRequest( + val variables: PdlPersonBolkRequestVariables, + val query: String, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlPersonRequestVariables.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlPersonRequestVariables.kt new file mode 100644 index 000000000..51b0ea586 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlPersonRequestVariables.kt @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl.domene + +data class PdlPersonRequestVariables(var ident: String) + +data class PdlPersonBolkRequestVariables(var identer: List) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlStatsborgerskapResponse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlStatsborgerskapResponse.kt new file mode 100644 index 000000000..9e928db64 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlStatsborgerskapResponse.kt @@ -0,0 +1,6 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl.domene + +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap + +class PdlStatsborgerskapResponse(val person: PdlStatsborgerskapPerson?) +class PdlStatsborgerskapPerson(val statsborgerskap: List) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlUtenlandskAdressseResponse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlUtenlandskAdressseResponse.kt new file mode 100644 index 000000000..84f30fa54 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlUtenlandskAdressseResponse.kt @@ -0,0 +1,13 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl.domene + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +class PdlUtenlandskAdressseResponse(val person: PdlUtenlandskAdresssePerson?) + +class PdlUtenlandskAdresssePerson(val bostedsadresse: List) +class PdlUtenlandskAdresssePersonBostedsadresse(val utenlandskAdresse: PdlUtenlandskAdresssePersonUtenlandskAdresse?) + +@JsonIgnoreProperties(ignoreUnknown = true) +class PdlUtenlandskAdresssePersonUtenlandskAdresse( + val landkode: String, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlVergeResponse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlVergeResponse.kt new file mode 100644 index 000000000..581c51266 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PdlVergeResponse.kt @@ -0,0 +1,6 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl.domene + +class PdlVergeResponse(val person: PdlVergePerson?) +class PdlVergePerson(val vergemaalEllerFremtidsfullmakt: List) + +class VergemaalEllerFremtidsfullmakt(val type: String?) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PersonInfo.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PersonInfo.kt new file mode 100644 index 000000000..20bed5df3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/domene/PersonInfo.kt @@ -0,0 +1,95 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl.domene + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import no.nav.familie.kontrakter.felles.personopplysning.Opphold +import no.nav.familie.kontrakter.felles.personopplysning.Sivilstand +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap +import java.time.LocalDate + +data class PersonInfo( + val fødselsdato: LocalDate, + val navn: String? = null, + @JsonDeserialize(using = KjonnDeserializer::class) + val kjønn: Kjønn? = null, + // Observer at ForelderBarnRelasjon og ForelderBarnRelasjonMaskert ikke er en PDL-objekt. + val forelderBarnRelasjon: Set = emptySet(), + val forelderBarnRelasjonMaskert: Set = emptySet(), + val adressebeskyttelseGradering: ADRESSEBESKYTTELSEGRADERING? = null, + val bostedsadresser: List = emptyList(), + val sivilstander: List = emptyList(), + val opphold: List? = emptyList(), + val statsborgerskap: List? = emptyList(), + val dødsfall: DødsfallData? = null, + val kontaktinformasjonForDoedsbo: PdlKontaktinformasjonForDødsbo? = null, +) + +fun List.filtrerUtKunNorskeBostedsadresser() = + this.filter { it.vegadresse != null || it.matrikkeladresse != null || it.ukjentBosted != null } + +data class ForelderBarnRelasjon( + val aktør: Aktør, + val relasjonsrolle: FORELDERBARNRELASJONROLLE, + val navn: String? = null, + val fødselsdato: LocalDate? = null, + val adressebeskyttelseGradering: ADRESSEBESKYTTELSEGRADERING? = null, +) { + override fun toString(): String { + return "ForelderBarnRelasjon(personIdent=XXX, relasjonsrolle=$relasjonsrolle, navn=XXX, fødselsdato=$fødselsdato)" + } + + fun toSecureString(): String { + return "ForelderBarnRelasjon(personIdent=${aktør.aktivFødselsnummer()}, relasjonsrolle=$relasjonsrolle, navn=XXX, fødselsdato=$fødselsdato)" + } +} + +data class ForelderBarnRelasjonMaskert( + val relasjonsrolle: FORELDERBARNRELASJONROLLE, + val adressebeskyttelseGradering: ADRESSEBESKYTTELSEGRADERING, +) { + override fun toString(): String { + return "ForelderBarnRelasjonMaskert(relasjonsrolle=$relasjonsrolle)" + } +} + +data class Personident( + val id: String, +) + +data class DødsfallData( + val erDød: Boolean, + val dødsdato: String?, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PdlKontaktinformasjonForDødsbo( + val adresse: PdlKontaktinformasjonForDødsboAdresse, +) + +data class PdlKontaktinformasjonForDødsboAdresse( + val adresselinje1: String, + val poststedsnavn: String, + val postnummer: String, +) + +data class VergeData(val harVerge: Boolean) + +class KjonnDeserializer : StdDeserializer(Kjønn::class.java) { + override fun deserialize(jp: JsonParser?, p1: DeserializationContext?): Kjønn { + val node: JsonNode = jp!!.codec.readTree(jp) + return when (val kjønn = node.asText()) { + "M" -> Kjønn.MANN + "K" -> Kjønn.KVINNE + else -> Kjønn.valueOf(kjønn) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/samhandler/SamhandlerKlient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/samhandler/SamhandlerKlient.kt new file mode 100644 index 000000000..3e1c2be69 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/samhandler/SamhandlerKlient.kt @@ -0,0 +1,46 @@ +package no.nav.familie.ba.sak.integrasjoner.samhandler + +import no.nav.familie.ba.sak.common.kallEksternTjenesteRessurs +import no.nav.familie.http.client.AbstractRestClient +import no.nav.familie.kontrakter.ba.tss.SamhandlerInfo +import no.nav.familie.kontrakter.ba.tss.SøkSamhandlerInfo +import no.nav.familie.kontrakter.ba.tss.SøkSamhandlerInfoRequest +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.cache.annotation.Cacheable +import org.springframework.stereotype.Service +import org.springframework.web.client.RestOperations +import java.net.URI + +@Service +class SamhandlerKlient( + @Value("\${FAMILIE_OPPDRAG_API_URL}") + private val familieOppdragUri: String, + @Qualifier("jwtBearer") restOperations: RestOperations, +) : AbstractRestClient(restOperations, "samhandler") { + + @Cacheable("hent-samhandler", cacheManager = "dailyCache") + fun hentSamhandler(orgNummer: String): SamhandlerInfo { + val uri = URI.create("$familieOppdragUri/tss/orgnr/$orgNummer") + + return kallEksternTjenesteRessurs( + tjeneste = "familie-oppdrag", + uri = uri, + formål = "Henter samhandler fra TSS", + ) { + getForEntity(uri = uri) + } + } + + fun søkSamhandlere(navn: String?, postnummer: String?, område: String?, side: Int): SøkSamhandlerInfo { + val uri = URI.create("$familieOppdragUri/tss/navn") + + return kallEksternTjenesteRessurs( + tjeneste = "familie-oppdrag", + uri = uri, + formål = "Søk samhandler fra TSS", + ) { + postForEntity(uri = uri, SøkSamhandlerInfoRequest(navn, side, postnummer, område)) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/sanity/SanityKlient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/sanity/SanityKlient.kt new file mode 100644 index 000000000..46dabf0a6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/sanity/SanityKlient.kt @@ -0,0 +1,82 @@ +package no.nav.familie.ba.sak.integrasjoner.sanity + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.kallEksternTjeneste +import no.nav.familie.ba.sak.kjerne.brev.domene.RestSanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.RestSanityEØSBegrunnelse +import no.nav.familie.ba.sak.task.OpprettTaskService +import org.springframework.beans.factory.annotation.Value +import org.springframework.retry.annotation.Backoff +import org.springframework.retry.annotation.Retryable +import org.springframework.stereotype.Component +import org.springframework.web.client.RestTemplate +import org.springframework.web.client.getForEntity +import java.net.URI + +const val sanityBaseUrl = "https://xsrv1mh6.api.sanity.io/v2021-06-07/data/query" + +@Component +class SanityKlient( + @Value("\${SANITY_DATASET}") private val datasett: String, + private val restTemplate: RestTemplate, +) { + @Retryable( + value = [Exception::class], + maxAttempts = 3, + backoff = Backoff(delayExpression = OpprettTaskService.RETRY_BACKOFF_5000MS), + ) + fun hentBegrunnelser(): List { + val sanityUrl = "$sanityBaseUrl/$datasett" + val hentBegrunnelserQuery = java.net.URLEncoder.encode(hentBegrunnelser, "utf-8") + + val uri = URI.create("$sanityUrl?query=$hentBegrunnelserQuery") + + val restSanityBegrunnelser = + kallEksternTjeneste( + tjeneste = "Sanity", + uri = uri, + formål = "Henter begrunnelser fra sanity", + ) { + restTemplate.getForEntity(uri).body?.result + ?: throw Feil("Klarer ikke å hente begrunnelser fra sanity") + } + + return restSanityBegrunnelser.mapNotNull { it.tilSanityBegrunnelse() } + } + + @Retryable( + value = [Exception::class], + maxAttempts = 3, + backoff = Backoff(delayExpression = OpprettTaskService.RETRY_BACKOFF_5000MS), + ) + fun hentEØSBegrunnelser(): List { + val sanityUrl = "$sanityBaseUrl/$datasett" + val hentEØSBegrunnelserQuery = java.net.URLEncoder.encode(hentEØSBegrunnelser, "utf-8") + + val uri = URI.create("$sanityUrl?query=$hentEØSBegrunnelserQuery") + + return kallEksternTjeneste( + tjeneste = "Sanity", + uri = uri, + formål = "Henter EØS-begrunnelser fra sanity", + ) { + restTemplate.getForEntity(uri).body?.result + ?.mapNotNull { it.tilSanityEØSBegrunnelse() } + ?: throw Feil("Klarer ikke å hente begrunnelser fra sanity") + } + } +} + +data class SanityBegrunnelserRespons( + val ms: Int, + val query: String, + val result: List, +) + +data class SanityEØSBegrunnelserRespons( + val ms: Int, + val query: String, + val result: List, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/sanity/SanityQueries.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/sanity/SanityQueries.kt new file mode 100644 index 000000000..9c21e94c1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/sanity/SanityQueries.kt @@ -0,0 +1,7 @@ +package no.nav.familie.ba.sak.integrasjoner.sanity + +const val hentBegrunnelser = + "*[_type == \"begrunnelse\" && tema != \"EØS\" && apiNavn != null && navnISystem != null]" + +const val hentEØSBegrunnelser = + "*[_type == \"begrunnelse\" && tema == \"EØS\" && apiNavn != null && navnISystem != null]" diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/sanity/SanityService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/sanity/SanityService.kt new file mode 100644 index 000000000..a775d7354 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/sanity/SanityService.kt @@ -0,0 +1,79 @@ +package no.nav.familie.ba.sak.integrasjoner.sanity + +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.kjerne.brev.domene.ISanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import org.slf4j.LoggerFactory +import org.springframework.cache.annotation.Cacheable +import org.springframework.stereotype.Service + +@Service +class SanityService( + private val sanityKlient: SanityKlient, + private val featureToggleService: FeatureToggleService, +) { + + private val logger = LoggerFactory.getLogger(javaClass) + + @Cacheable("sanityBegrunnelser", cacheManager = "shortCache") + fun hentSanityBegrunnelser(): Map { + val enumPåApiNavn = Standardbegrunnelse.values().associateBy { it.sanityApiNavn } + val sanityBegrunnelser = sanityKlient.hentBegrunnelser() + logManglerSanityBegrunnelseForEnum(enumPåApiNavn, sanityBegrunnelser, "SanityBegrunnelse") + return sanityBegrunnelser + .mapNotNull { + val begrunnelseEnum = enumPåApiNavn[it.apiNavn] + if (begrunnelseEnum == null) { + logger.warn("Finner ikke Standardbegrunnelse for ${it.apiNavn}") + null + } else { + begrunnelseEnum to it + } + }.toMap() + } + + @Cacheable("sanityEØSBegrunnelser", cacheManager = "shortCache") + fun hentSanityEØSBegrunnelser(): Map { + val eøsPraksisEndringFeatureToggleErSlåttPå = + featureToggleService.isEnabled(FeatureToggleConfig.EØS_PRAKSISENDRING_SEPTEMBER2023) + + // TODO: Fjern filtrering av begrunnelser etter at EØS praksisendringen er live i prod + val enumPåApiNavn = if (eøsPraksisEndringFeatureToggleErSlåttPå) { + EØSStandardbegrunnelse.values().associateBy { it.sanityApiNavn } + } else { + EØSStandardbegrunnelse.values().subtract(EØSStandardbegrunnelse.eøsPraksisendringBegrunnelser()) + .associateBy { it.sanityApiNavn } + } + + val sanityEØSBegrunnelser = sanityKlient.hentEØSBegrunnelser() + logManglerSanityBegrunnelseForEnum(enumPåApiNavn, sanityEØSBegrunnelser, "SanityEØSBegrunnelse") + return sanityEØSBegrunnelser + .mapNotNull { + val begrunnelseEnum = enumPåApiNavn[it.apiNavn] + if (begrunnelseEnum == null) { + logger.warn("Finner ikke EØSStandardbegrunnelse for ${it.apiNavn}") + null + } else { + begrunnelseEnum to it + } + }.toMap() + } + + private fun logManglerSanityBegrunnelseForEnum( + enumPåApiNavn: Map, + sanityBegrunnelser: List, + navn: String, + ) { + val sanityBegrunnelseApiNavn = sanityBegrunnelser.map { it.apiNavn }.toSet() + enumPåApiNavn.values.forEach { + if (!sanityBegrunnelseApiNavn.contains(it.sanityApiNavn)) { + logger.warn("Finner ikke $navn for enum=$it sanityApiNavn=${it.sanityApiNavn}") + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/Skyggesak.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/Skyggesak.kt new file mode 100644 index 000000000..c74d02a36 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/Skyggesak.kt @@ -0,0 +1,29 @@ +package no.nav.familie.ba.sak.integrasjoner.skyggesak + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import java.time.LocalDateTime + +@Entity(name = "Skyggesak") +@Table(name = "SKYGGESAK") +data class Skyggesak( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "skyggesak_seq_generator") + @SequenceGenerator( + name = "skyggesak_seq_generator", + sequenceName = "SKYGGESAK_SEQ", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "fk_fagsak_id", nullable = false, updatable = false) + val fagsakId: Long, + + @Column(name = "sendt_tid") + var sendtTidspunkt: LocalDateTime? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/SkyggesakRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/SkyggesakRepository.kt new file mode 100644 index 000000000..691b12bb9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/SkyggesakRepository.kt @@ -0,0 +1,16 @@ +package no.nav.familie.ba.sak.integrasjoner.skyggesak + +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository + +@Repository +interface SkyggesakRepository : JpaRepository { + + @Query(value = "SELECT s FROM Skyggesak s WHERE s.sendtTidspunkt IS NULL") + fun finnSkyggesakerKlareForSending(page: Pageable): List + + @Query(value = "SELECT s FROM Skyggesak s WHERE s.sendtTidspunkt IS NOT NULL") + fun finnSkyggesakerSomErSendt(): List +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/SkyggesakScheduler.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/SkyggesakScheduler.kt new file mode 100644 index 000000000..01c7ccd3f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/SkyggesakScheduler.kt @@ -0,0 +1,74 @@ +package no.nav.familie.ba.sak.integrasjoner.skyggesak + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.leader.LeaderClient +import no.nav.familie.log.mdc.MDCConstants +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.data.domain.Pageable +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.util.UUID + +@Component +class SkyggesakScheduler( + val skyggesakRepository: SkyggesakRepository, + val fagsakRepository: FagsakRepository, + val integrasjonClient: IntegrasjonClient, +) { + + @Scheduled(fixedDelay = 60000) + fun opprettSkyggesaker() { + if (LeaderClient.isLeader() == true) { + sendSkyggesaker() + } + } + + @Scheduled(cron = "0 0 6 * * *") + fun ryddOppISendteSkyggesaker() { + if (LeaderClient.isLeader() == true) { + fjernGamleSkyggesakInnslag() + } + } + + @Transactional + fun sendSkyggesaker() { + val skyggesaker = skyggesakRepository.finnSkyggesakerKlareForSending(Pageable.ofSize(400)) + + for (skyggesak in skyggesaker) { + try { + MDC.put(MDCConstants.MDC_CALL_ID, UUID.randomUUID().toString()) + val fagsak = fagsakRepository.finnFagsak(skyggesak.fagsakId)!! + logger.info("Opprettet skyggesak for fagsak ${fagsak.id}") + integrasjonClient.opprettSkyggesak(fagsak.aktør, fagsak.id) + skyggesakRepository.save(skyggesak.copy(sendtTidspunkt = LocalDateTime.now())) + } catch (e: Exception) { + logger.warn("Kunne ikke opprette skyggesak for fagsak ${skyggesak.fagsakId}") + secureLogger.warn("Kunne ikke opprette skyggesak for fagsak ${skyggesak.fagsakId}", e) + } finally { + MDC.clear() + } + } + } + + @Transactional + fun fjernGamleSkyggesakInnslag() { + skyggesakRepository.finnSkyggesakerSomErSendt() + .filter { it.sendtTidspunkt!!.isBefore(LocalDateTime.now().minusDays(SKYGGESAK_RETENTION.toLong())) } + .run { + logger.info("Fjerner ${this.size} rader fra Skyggesak, sendt for mer enn $SKYGGESAK_RETENTION dager siden") + secureLogger.info("Fjerner følgende rader eldre enn $SKYGGESAK_RETENTION fra Skyggesak:\n$this ") + skyggesakRepository.deleteAll(this) + } + } + + companion object { + private const val SKYGGESAK_RETENTION = 14 + + private val logger = LoggerFactory.getLogger(SkyggesakScheduler::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/SkyggesakService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/SkyggesakService.kt new file mode 100644 index 000000000..4126b72cb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/SkyggesakService.kt @@ -0,0 +1,13 @@ +package no.nav.familie.ba.sak.integrasjoner.skyggesak + +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import org.springframework.stereotype.Service + +@Service +class SkyggesakService( + private val skyggesakRepository: SkyggesakRepository, +) { + fun opprettSkyggesak(fagsak: Fagsak) { + skyggesakRepository.save(Skyggesak(fagsakId = fagsak.id)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/statistikk/StatistikkClient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/statistikk/StatistikkClient.kt new file mode 100644 index 000000000..da5355d57 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/statistikk/StatistikkClient.kt @@ -0,0 +1,48 @@ +package no.nav.familie.ba.sak.integrasjoner.statistikk + +import no.nav.familie.http.client.AbstractRestClient +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.getDataOrThrow +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpHeaders +import org.springframework.stereotype.Service +import org.springframework.web.client.HttpStatusCodeException +import org.springframework.web.client.RestOperations +import java.net.URI + +@Service +class StatistikkClient( + @Value("\${FAMILIE_STATISTIKK_URL}") val baseUri: URI, + @Qualifier("jwtBearer") val restTemplate: RestOperations, +) : AbstractRestClient(restTemplate, "statistikk") { + + fun harSendtVedtaksmeldingForBehandling(behandlingId: Long): Boolean { + val uri = URI.create("$baseUri/vedtak/$behandlingId") + + return try { + val response: Ressurs = getForEntity(uri, httpHeaders()) + response.getDataOrThrow() + } catch (e: Exception) { + if (e is HttpStatusCodeException) { + logger.error( + "Kall mot statistikk feilet: httpkode: ${e.statusCode}, body ${e.responseBodyAsString} ", + e, + ) + } + throw e + } + } + + private fun httpHeaders(): HttpHeaders { + return HttpHeaders().apply { + add(HttpHeaders.CONTENT_TYPE, "application/json") + } + } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(StatistikkClient::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/AvstemmingService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/AvstemmingService.kt" new file mode 100644 index 000000000..6d03b5a6a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/AvstemmingService.kt" @@ -0,0 +1,192 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.task.KonsistensavstemMotOppdragAvsluttTask +import no.nav.familie.ba.sak.task.KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingAvsluttTaskDTO +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppdrag.PerioderForBehandling +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.util.Properties +import java.util.UUID + +@Service +class AvstemmingService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val økonomiKlient: ØkonomiKlient, + private val beregningService: BeregningService, + private val taskService: TaskService, + private val batchRepository: BatchRepository, + private val dataChunkRepository: DataChunkRepository, +) { + fun grensesnittavstemOppdrag(fraDato: LocalDateTime, tilDato: LocalDateTime) { + økonomiKlient.grensesnittavstemOppdrag(fraDato, tilDato) + } + + fun sendKonsistensavstemmingStart(avstemmingsdato: LocalDateTime, transaksjonsId: UUID) { + økonomiKlient.konsistensavstemOppdragStart( + avstemmingsdato, + transaksjonsId, + ) + } + + fun harBatchStatusFerdig(batchId: Long): Boolean { + val batch = batchRepository.getReferenceById(batchId) + return batch.status == KjøreStatus.FERDIG + } + + fun erKonsistensavstemmingStartet(transaksjonsId: UUID): Boolean { + return dataChunkRepository.findByTransaksjonsId(transaksjonsId).isNotEmpty() + } + + fun skalOppretteFinnPerioderForRelevanteBehandlingerTask(transaksjonsId: UUID, chunkNr: Int): Boolean { + logger.info("Sjekker om konsistensavstemming er gjort for=$transaksjonsId og chunkNr=$chunkNr") + return dataChunkRepository.findByTransaksjonsIdAndChunkNr(transaksjonsId, chunkNr) == null + } + + fun erKonsistensavstemmingKjørtForTransaksjonsidOgChunk(transaksjonsId: UUID, chunkNr: Int): Boolean { + val dataChunk = dataChunkRepository.findByTransaksjonsIdAndChunkNr(transaksjonsId, chunkNr) + return dataChunk?.erSendt == true + } + + fun konsistensavstemOppdragData( + avstemmingsdato: LocalDateTime, + perioderTilAvstemming: List, + transaksjonsId: UUID, + chunkNr: Int, + sendTilØkonomi: Boolean, + ) { + logger.info("Utfører konsistensavstemOppdragData: Sender perioder for transaksjonsId $transaksjonsId og chunk nr $chunkNr") + val dataChunk = dataChunkRepository.findByTransaksjonsIdAndChunkNr(transaksjonsId, chunkNr) + ?: error("Finner ingen datachunk for $transaksjonsId og $chunkNr") + + if (dataChunk.erSendt) { + logger.info("Utfører konsistensavstemOppdragData: Perioder for transaksjonsId $transaksjonsId og chunk nr $chunkNr er allerede sendt.") + return + } + + if (sendTilØkonomi) { + økonomiKlient.konsistensavstemOppdragData( + avstemmingsdato, + perioderTilAvstemming, + transaksjonsId, + ) + } else { + logger.info("Send datamelding til økonomi i dry-run modus for $transaksjonsId og $chunkNr") + } + + dataChunkRepository.save(dataChunk.also { it.erSendt = true }) + } + + fun konsistensavstemOppdragAvslutt(avstemmingsdato: LocalDateTime, transaksjonsId: UUID) { + logger.info("Avslutter konsistensavstemming for $transaksjonsId") + + økonomiKlient.konsistensavstemOppdragAvslutt(avstemmingsdato, transaksjonsId) + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun opprettKonsistensavstemmingAvsluttTask( + konsistensavstemmingAvsluttTaskDTO: KonsistensavstemmingAvsluttTaskDTO, + ) { + logger.info("Oppretter avsluttingstask for transaksjonsId=${konsistensavstemmingAvsluttTaskDTO.transaksjonsId}") + val konsistensavstemmingAvsluttTask = Task( + type = KonsistensavstemMotOppdragAvsluttTask.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString(konsistensavstemmingAvsluttTaskDTO), + properties = Properties().apply { + this["transaksjonsId"] = konsistensavstemmingAvsluttTaskDTO.transaksjonsId.toString() + }, + ) + taskService.save(konsistensavstemmingAvsluttTask) + } + + fun hentSisteIverksatteBehandlingerFraLøpendeFagsaker() = + behandlingHentOgPersisterService.hentSisteIverksatteBehandlingerFraLøpendeFagsaker() + + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun opprettKonsistensavstemmingFinnPerioderForRelevanteBehandlingerTask( + konsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO: KonsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO, + triggerTid: LocalDateTime, + ) { + val batch = + batchRepository.getReferenceById(konsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO.batchId) + dataChunkRepository.save( + DataChunk( + batch = batch, + chunkNr = konsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO.chunkNr, + transaksjonsId = konsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO.transaksjonsId, + ), + ) + + logger.info("Oppretter task for å finne perioder for relevante behandlinger. transaksjonsId=${konsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO.transaksjonsId} og chunk=${konsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO.chunkNr} for ${konsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO.relevanteBehandlinger.size} behandlinger") + val task = Task( + type = KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + konsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO, + ), + properties = Properties().apply { + this["transaksjonsId"] = + konsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO.transaksjonsId.toString() + this["chunkNr"] = konsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO.chunkNr.toString() + }, + ).medTriggerTid(triggerTid) + taskService.save(task) + } + + fun hentDataForKonsistensavstemming( + avstemmingstidspunkt: LocalDateTime, + relevanteBehandlinger: List, + ): List { + return relevanteBehandlinger + .chunked(1000) + .map { chunk -> + val relevanteAndeler = beregningService.hentLøpendeAndelerTilkjentYtelseMedUtbetalingerForBehandlinger( + behandlingIder = chunk, + avstemmingstidspunkt = avstemmingstidspunkt, + ) + val aktiveFødselsnummere = + behandlingHentOgPersisterService.hentAktivtFødselsnummerForBehandlinger( + relevanteAndeler.mapNotNull { it.kildeBehandlingId }, + ) + + val tssEksternIdForBehandlinger = + behandlingHentOgPersisterService.hentTssEksternIdForBehandlinger( + relevanteAndeler.mapNotNull { it.kildeBehandlingId }, + ) + + relevanteAndeler.groupBy { it.kildeBehandlingId } + .map { (kildeBehandlingId, andeler) -> + if (kildeBehandlingId == null) { + secureLogger.warn("Finner ikke behandlingsId for andeler=$andeler") + } + PerioderForBehandling( + behandlingId = kildeBehandlingId.toString(), + aktivFødselsnummer = aktiveFødselsnummere[kildeBehandlingId] + ?: error("Finnes ikke et aktivt fødselsnummer for behandling $kildeBehandlingId"), + perioder = andeler + .map { + it.periodeOffset + ?: error("Andel ${it.id} på iverksatt behandling på løpende fagsak mangler periodeOffset") + } + .toSet(), + utebetalesTil = tssEksternIdForBehandlinger[kildeBehandlingId], + ) + } + }.flatten() + } + + companion object { + private val logger: Logger = LoggerFactory.getLogger(AvstemmingService::class.java) + + const val KONSISTENSAVSTEMMING_DATA_CHUNK_STORLEK = 500 + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/Batch.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/Batch.kt" new file mode 100644 index 000000000..86b20378e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/Batch.kt" @@ -0,0 +1,32 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Batch") +@Table(name = "BATCH") +data class Batch( + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "batch_seq") + @SequenceGenerator(name = "batch_seq") + val id: Long = 0, + + @Column(name = "kjoredato", nullable = false) + val kjøreDato: LocalDate, + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + var status: KjøreStatus = KjøreStatus.LEDIG, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/BatchRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/BatchRepository.kt" new file mode 100644 index 000000000..2ac116329 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/BatchRepository.kt" @@ -0,0 +1,23 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import jakarta.persistence.LockModeType +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository +import java.time.LocalDate +import java.util.Optional + +@Repository +interface BatchRepository : JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_FORCE_INCREMENT) + fun save(batch: Batch): Batch + + @Lock(LockModeType.PESSIMISTIC_WRITE) + override fun findById(id: Long): Optional + + @Query("SELECT k FROM Batch k where kjøreDato = :dato AND status = 'LEDIG'") + @Lock(LockModeType.PESSIMISTIC_WRITE) + fun findByKjøredatoAndLedig(dato: LocalDate): Batch? +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/BatchService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/BatchService.kt" new file mode 100644 index 000000000..fab127c95 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/BatchService.kt" @@ -0,0 +1,28 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate + +@Service +class BatchService(val batchRepository: BatchRepository) { + + @Transactional + fun plukkLedigeBatchKjøringerFor(dato: LocalDate): Batch? { + val batch = batchRepository.findByKjøredatoAndLedig(dato) + if (batch != null) lagreNyStatus(batch, KjøreStatus.TATT) + + return batch + } + + fun lagreNyStatus(batch: Batch, status: KjøreStatus) { + batch.status = status + batchRepository.saveAndFlush(batch) + } + + fun lagreNyStatus(batchId: Long, status: KjøreStatus) { + val batch = batchRepository.getReferenceById(batchId) + batch.status = status + batchRepository.saveAndFlush(batch) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/DataChunk.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/DataChunk.kt" new file mode 100644 index 000000000..92751cb6a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/DataChunk.kt" @@ -0,0 +1,38 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.util.UUID + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "DataChunk") +@Table(name = "DATA_CHUNK") +data class DataChunk( + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "data_chunk_seq") + @SequenceGenerator(name = "data_chunk_seq") + val id: Long = 0, + + @ManyToOne(optional = false) + @JoinColumn(name = "fk_batch_id", nullable = false, updatable = false) + val batch: Batch, + + @Column(name = "transaksjons_id", nullable = false) + val transaksjonsId: UUID, + + @Column(name = "chunk_nr", nullable = false) + val chunkNr: Int, + + @Column(name = "er_sendt", nullable = false) + var erSendt: Boolean = false, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/DataChunkRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/DataChunkRepository.kt" new file mode 100644 index 000000000..3bf68176d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/DataChunkRepository.kt" @@ -0,0 +1,15 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository +interface DataChunkRepository : JpaRepository { + + @Query("SELECT dc FROM DataChunk dc WHERE dc.transaksjonsId = :transaksjonsId AND dc.chunkNr = :chunkNr") + fun findByTransaksjonsIdAndChunkNr(transaksjonsId: UUID, chunkNr: Int): DataChunk? + fun findByTransaksjonsId(transaksjonsId: UUID): List + fun findByErSendt(erSendt: Boolean): List +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/IdentOgYtelse.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/IdentOgYtelse.kt" new file mode 100644 index 000000000..2b40a4c69 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/IdentOgYtelse.kt" @@ -0,0 +1,11 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType + +/** + * Data class for å gruppere andeler per ident og [YtelseType], sånn at man kan lage kjeder per ident/type + */ +data class IdentOgYtelse( + val ident: String, + val type: YtelseType, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingController.kt" new file mode 100644 index 000000000..c35ecd88a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingController.kt" @@ -0,0 +1,71 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import jakarta.transaction.Transactional +import no.nav.familie.ba.sak.task.KonsistensavstemMotOppdragStartTask +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingStartTaskDTO +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.UUID + +@RestController +@RequestMapping("/api/konsistensavstemming") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class KonsistensavstemmingController( + private val taskService: TaskService, + private val batchRepository: BatchRepository, +) { + + @PostMapping(path = ["/dryrun"], produces = [MediaType.APPLICATION_JSON_VALUE]) + @Transactional + fun kjørKonsistensavstemmingUtenSendingTilØkonomi(): ResponseEntity> { + val (transaksjonsId, task) = opprettKonsistensavstemMotOppdragStartTask(false, LocalDateTime.now()) + + return ResponseEntity.ok(Ressurs.success("Testkjører konsistensavstemming uten å sende til økonomi. transaksjonsId=$transaksjonsId callId=${task.callId}")) + } + + @PostMapping(path = ["/run"], produces = [MediaType.APPLICATION_JSON_VALUE]) + @Transactional + fun kjørKonsistensavstemming(@RequestBody request: StartKonsistensavstemming): ResponseEntity> { + val (transaksjonsId, task) = opprettKonsistensavstemMotOppdragStartTask(true, request.triggerTid) + + return ResponseEntity.ok(Ressurs.success("Kjører konsistensavstemming. transaksjonsId=$transaksjonsId callId=${task.callId}")) + } + + data class StartKonsistensavstemming(val triggerTid: LocalDateTime) + + private fun opprettKonsistensavstemMotOppdragStartTask( + sendTilØkonomi: Boolean, + triggerTid: LocalDateTime, + ): Pair { + val transaksjonsId = UUID.randomUUID() + val batch = batchRepository.saveAndFlush(Batch(kjøreDato = LocalDate.now(), status = KjøreStatus.MANUELL)) + val task = taskService.save( + Task( + type = KonsistensavstemMotOppdragStartTask.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + KonsistensavstemmingStartTaskDTO( + batchId = batch.id, + avstemmingdato = triggerTid, + transaksjonsId = transaksjonsId, + sendTilØkonomi = sendTilØkonomi, + ), + ), + triggerTid = triggerTid, + ), + ) + return Pair(transaksjonsId, task) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingScheduler.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingScheduler.kt" new file mode 100644 index 000000000..7f088c30d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingScheduler.kt" @@ -0,0 +1,69 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.task.KonsistensavstemMotOppdragStartTask +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingStartTaskDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate.now +import java.time.LocalDateTime +import java.time.YearMonth +import java.util.Properties +import java.util.UUID + +@Component +class KonsistensavstemmingScheduler( + val batchService: BatchService, + val behandlingService: BehandlingService, + val fagsakService: FagsakService, + val taskRepository: TaskRepositoryWrapper, + val featureToggleService: FeatureToggleService, +) { + + @Scheduled(cron = "0 0 22 * * *") + @Transactional + fun utførKonsistensavstemming() { + val inneværendeMåned = YearMonth.from(now()) + val plukketBatch = batchService.plukkLedigeBatchKjøringerFor(dato = now()) ?: return + + logger.info("Kjører konsistensavstemming for $inneværendeMåned") + + val transaksjonsId = UUID.randomUUID() + taskRepository.save( + Task( + type = KonsistensavstemMotOppdragStartTask.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + KonsistensavstemmingStartTaskDTO( + batchId = plukketBatch.id, + avstemmingdato = LocalDateTime.now(), + transaksjonsId = transaksjonsId, + ), + ), + properties = Properties().apply { + this["transaksjonsId"] = transaksjonsId.toString() + }, + ), + ) + + batchService.lagreNyStatus(plukketBatch, KjøreStatus.TATT) + } + + companion object { + + private val logger = LoggerFactory.getLogger(KonsistensavstemmingScheduler::class.java) + } +} + +enum class KjøreStatus { + FERDIG, + TATT, + LEDIG, + MANUELL, +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragGenerator.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragGenerator.kt" new file mode 100644 index 000000000..f02acbc1c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragGenerator.kt" @@ -0,0 +1,391 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.andelerTilOpphørMedDato +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.andelerTilOpprettelse +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.sisteAndelPerKjede +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.sisteBeståendeAndelPerKjede +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.task.dto.FAGSYSTEM +import no.nav.familie.felles.utbetalingsgenerator.Utbetalingsgenerator +import no.nav.familie.felles.utbetalingsgenerator.domain.AndelDataLongId +import no.nav.familie.felles.utbetalingsgenerator.domain.Behandlingsinformasjon +import no.nav.familie.felles.utbetalingsgenerator.domain.BeregnetUtbetalingsoppdragLongId +import no.nav.familie.felles.utbetalingsgenerator.domain.Fagsystem +import no.nav.familie.felles.utbetalingsgenerator.domain.IdentOgType +import no.nav.familie.kontrakter.felles.oppdrag.Opphør +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsperiode +import org.springframework.stereotype.Component +import java.time.LocalDate +import java.time.YearMonth + +@Component +class UtbetalingsoppdragGenerator( + private val beregningService: BeregningService, +) { + + fun lagUtbetalingsoppdrag( + saksbehandlerId: String, + vedtak: Vedtak, + forrigeTilkjentYtelse: TilkjentYtelse?, + nyTilkjentYtelse: TilkjentYtelse, + sisteAndelPerKjede: Map, + erSimulering: Boolean, + endretMigreringsDato: YearMonth? = null, + ): BeregnetUtbetalingsoppdragLongId { + return Utbetalingsgenerator().lagUtbetalingsoppdrag( + behandlingsinformasjon = Behandlingsinformasjon( + saksbehandlerId = saksbehandlerId, + behandlingId = vedtak.behandling.id.toString(), + eksternBehandlingId = vedtak.behandling.id, + eksternFagsakId = vedtak.behandling.fagsak.id, + fagsystem = FagsystemBA.BARNETRYGD, + personIdent = vedtak.behandling.fagsak.aktør.aktivFødselsnummer(), + vedtaksdato = vedtak.vedtaksdato?.toLocalDate() ?: LocalDate.now(), + opphørAlleKjederFra = finnOpphørsdatoForAlleKjeder( + forrigeTilkjentYtelse = forrigeTilkjentYtelse, + sisteAndelPerKjede = sisteAndelPerKjede, + endretMigreringsDato = endretMigreringsDato, + ), + utbetalesTil = hentUtebetalesTil(vedtak.behandling.fagsak), + opphørKjederFraFørsteUtbetaling = if (endretMigreringsDato != null) false else erSimulering, // Ved simulering når migreringsdato er endret, skal vi opphøre fra den nye datoen og ikke fra første utbetaling per kjede. + ), + forrigeAndeler = forrigeTilkjentYtelse?.tilAndelData() ?: emptyList(), + nyeAndeler = nyTilkjentYtelse.tilAndelData(), + sisteAndelPerKjede = sisteAndelPerKjede.mapValues { it.value.tilAndelDataLongId() }, + ) + } + + private fun TilkjentYtelse.tilAndelData(): List = + this.andelerTilkjentYtelse.map { it.tilAndelDataLongId() } + + private fun AndelTilkjentYtelse.tilAndelDataLongId(): AndelDataLongId = + AndelDataLongId( + id = id, + fom = periode.fom, + tom = periode.tom, + beløp = kalkulertUtbetalingsbeløp, + personIdent = aktør.aktivFødselsnummer(), + type = type.tilYtelseType(), + periodeId = periodeOffset, + forrigePeriodeId = forrigePeriodeOffset, + kildeBehandlingId = kildeBehandlingId, + ) + + private fun finnOpphørsdatoForAlleKjeder( + forrigeTilkjentYtelse: TilkjentYtelse?, + sisteAndelPerKjede: Map, + endretMigreringsDato: YearMonth?, + ): YearMonth? { + if (forrigeTilkjentYtelse == null || sisteAndelPerKjede.isEmpty()) return null + if (endretMigreringsDato != null) return endretMigreringsDato + return null + } + + private fun hentUtebetalesTil(fagsak: Fagsak): String { + return when (fagsak.type) { + FagsakType.INSTITUSJON -> { + fagsak.institusjon?.tssEksternId + ?: error("Fagsak ${fagsak.id} er av type institusjon og mangler informasjon om institusjonen") + } + + else -> { + fagsak.aktør.aktivFødselsnummer() + } + } + } + + /** + * Lager utbetalingsoppdrag med kjedede perioder av andeler. + * Ved opphør sendes kun siste utbetalingsperiode (med opphørsdato). + * + * @param[saksbehandlerId] settes på oppdragsnivå + * @param[vedtak] for å hente fagsakid, behandlingid, vedtaksdato, ident, og evt opphørsdato + * @param[erFørsteBehandlingPåFagsak] for å sette aksjonskode på oppdragsnivå og bestemme om vi skal telle fra start + * @param[forrigeKjeder] Et sett med kjeder som var gjeldende for forrige behandling på fagsaken + * @param[sisteAndelPerIdent] Siste iverksatte andel mot økonomi per ident. + * @param[oppdaterteKjeder] Et sett med andeler knyttet til en person (dvs en kjede), hvor andeler er helt nye, + * @param[erSimulering] flag for om beregnet er en simulering, da skal komplett nytt betlaingsoppdrag generes + * og ny tilkjentytelse skal ikke persisteres, + * @param[endretMigreringsDato] Satt betyr at en endring skjedd fra før den eksisterende migreringsdatoen, som en konsekevens + * skal hele betalingsoppdraget opphøre. + * flag for om beregnet er en simulering, da skal komplett nytt betlaingsoppdrag generes + * og ny tilkjentytelse skal ikke persisteres, + * har endrede datoer eller må bygges opp igjen pga endringer før i kjeden + * @return Utbetalingsoppdrag for vedtak + */ + fun lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + saksbehandlerId: String, + vedtak: Vedtak, + erFørsteBehandlingPåFagsak: Boolean, + forrigeKjeder: Map> = emptyMap(), + sisteAndelPerIdent: Map = emptyMap(), + oppdaterteKjeder: Map> = emptyMap(), + erSimulering: Boolean = false, + endretMigreringsDato: YearMonth? = null, + ): Utbetalingsoppdrag { + // Hos økonomi skiller man på endring på oppdragsnivå 110 og på linjenivå 150 (periodenivå). + // Da de har opplevd å motta + // UEND på oppdrag som skulle vært ENDR anbefaler de at kun ENDR brukes når sak + // ikke er ny, så man slipper å forholde seg til om det er endring over 150-nivå eller ikke. + val aksjonskodePåOppdragsnivå = + if (erFørsteBehandlingPåFagsak) Utbetalingsoppdrag.KodeEndring.NY else Utbetalingsoppdrag.KodeEndring.ENDR + + // endretMigreringsDato satt betyr at endring skjedd for migreringsdato og som en + // konsekvens så skal hele betalingsoppdraget opphøre. + val erEndretMigreringsDato = endretMigreringsDato != null + + // Generer et komplett nytt eller bare endringer på et eksisterende betalingsoppdrag. + val sisteBeståenAndelIHverKjede = if (erSimulering || erEndretMigreringsDato) { + // Gjennom å sette andeler til null markeres at alle perioder i kjeden skal opphøres. + sisteAndelPerKjede(forrigeKjeder, oppdaterteKjeder) + } else { + // For å kunne behandling alle forlengelser/forkortelser av perioder likt har vi valgt å konsekvent opphøre og erstatte. + // Det vil si at vi alltid gjenoppbygger kjede fra første endring, selv om vi i realiteten av og til kun endrer datoer + // på en eksisterende linje (endring på 150 linjenivå). + sisteBeståendeAndelPerKjede(forrigeKjeder, oppdaterteKjeder) + } + + val andelerTilOpphør = + andelerTilOpphørMedDato( + forrigeKjeder, + sisteBeståenAndelIHverKjede, + endretMigreringsDato, + sisteAndelPerIdent, + ) + val andelerTilOpprettelse: List> = + andelerTilOpprettelse(oppdaterteKjeder, sisteBeståenAndelIHverKjede) + + val opprettes: List = if (andelerTilOpprettelse.isNotEmpty()) { + val sisteOffsetIKjedeOversikt = + sisteAndelPerIdent.map { it.key to it.value.periodeOffset!!.toInt() }.toMap() + lagUtbetalingsperioderForOpprettelseOgOppdaterTilkjentYtelse( + andeler = andelerTilOpprettelse, + erFørsteBehandlingPåFagsak = erFørsteBehandlingPåFagsak, + vedtak = vedtak, + sisteOffsetIKjedeOversikt = sisteOffsetIKjedeOversikt, + sisteOffsetPåFagsak = sisteOffsetIKjedeOversikt.maxOfOrNull { it.value }, + skalOppdatereTilkjentYtelse = !erSimulering, + ) + } else { + emptyList() + } + + val opphøres: List = if (andelerTilOpphør.isNotEmpty()) { + lagUtbetalingsperioderForOpphør( + andeler = andelerTilOpphør, + vedtak = vedtak, + ) + } else { + emptyList() + } + + return Utbetalingsoppdrag( + saksbehandlerId = saksbehandlerId, + kodeEndring = aksjonskodePåOppdragsnivå, + fagSystem = FAGSYSTEM, + saksnummer = vedtak.behandling.fagsak.id.toString(), + aktoer = vedtak.behandling.fagsak.aktør.aktivFødselsnummer(), + utbetalingsperiode = listOf(opphøres, opprettes).flatten(), + ) + } + + private fun lagUtbetalingsperioderForOpphør( + andeler: List>, + vedtak: Vedtak, + ): List { + val utbetalingsperiodeMal = UtbetalingsperiodeMal( + vedtak = vedtak, + erEndringPåEksisterendePeriode = true, + ) + + return andeler.map { (sisteAndelIKjede, opphørKjedeFom) -> + utbetalingsperiodeMal.lagPeriodeFraAndel( + andel = sisteAndelIKjede, + periodeIdOffset = sisteAndelIKjede.periodeOffset!!.toInt(), + forrigePeriodeIdOffset = sisteAndelIKjede.forrigePeriodeOffset?.toInt(), + opphørKjedeFom = opphørKjedeFom, + ) + } + } + + private fun lagUtbetalingsperioderForOpprettelseOgOppdaterTilkjentYtelse( + andeler: List>, + vedtak: Vedtak, + erFørsteBehandlingPåFagsak: Boolean, + sisteOffsetIKjedeOversikt: Map, + sisteOffsetPåFagsak: Int? = null, + skalOppdatereTilkjentYtelse: Boolean, + ): List { + var offset = + if (!erFørsteBehandlingPåFagsak) { + sisteOffsetPåFagsak?.plus(1) + ?: throw IllegalStateException("Skal finnes offset når ikke første behandling på fagsak") + } else { + 0 + } + + val utbetalingsperiodeMal = UtbetalingsperiodeMal( + vedtak = vedtak, + ) + + val utbetalingsperioder = andeler.filter { kjede -> kjede.isNotEmpty() } + .flatMap { kjede: List -> + val ident = kjede.first().aktør.aktivFødselsnummer() + val ytelseType = kjede.first().type + var forrigeOffsetIKjede: Int? = null + if (!erFørsteBehandlingPåFagsak) { + forrigeOffsetIKjede = sisteOffsetIKjedeOversikt[IdentOgYtelse(ident, ytelseType)] + } + kjede.sortedBy { it.stønadFom }.mapIndexed { index, andel -> + val forrigeOffset = if (index == 0) forrigeOffsetIKjede else offset - 1 + utbetalingsperiodeMal.lagPeriodeFraAndel(andel, offset, forrigeOffset).also { + andel.periodeOffset = offset.toLong() + andel.forrigePeriodeOffset = forrigeOffset?.toLong() + andel.kildeBehandlingId = + andel.behandlingId // Trengs for å finne tilbake ved konsistensavstemming + offset++ + } + } + } + + // TODO Vi bør se om vi kan flytte ut denne side effecten + if (skalOppdatereTilkjentYtelse) { + val oppdatertTilkjentYtelse = andeler.flatten().firstOrNull()?.tilkjentYtelse ?: throw Feil( + "Andeler mangler ved generering av utbetalingsperioder. Får tom liste.", + ) + beregningService.lagreTilkjentYtelseMedOppdaterteAndeler(oppdatertTilkjentYtelse) + } + + return utbetalingsperioder + } +} + +abstract class AndelTilkjentYtelseForUtbetalingsoppdrag(private val andelTilkjentYtelse: AndelTilkjentYtelse) { + val behandlingId: Long? = andelTilkjentYtelse.behandlingId + val tilkjentYtelse: TilkjentYtelse = andelTilkjentYtelse.tilkjentYtelse + val kalkulertUtbetalingsbeløp: Int = andelTilkjentYtelse.kalkulertUtbetalingsbeløp + val stønadFom: YearMonth = andelTilkjentYtelse.stønadFom + val stønadTom: YearMonth = andelTilkjentYtelse.stønadTom + val aktør: Aktør = andelTilkjentYtelse.aktør + val type: YtelseType = andelTilkjentYtelse.type + abstract var periodeOffset: Long? + abstract var forrigePeriodeOffset: Long? + abstract var kildeBehandlingId: Long? + + override fun equals(other: Any?): Boolean { + return if (other is AndelTilkjentYtelseForUtbetalingsoppdrag) { + this.andelTilkjentYtelse.equals(other.andelTilkjentYtelse) + } else { + false + } + } + + override fun hashCode(): Int { + return andelTilkjentYtelse.hashCode() + } +} + +interface AndelTilkjentYtelseForUtbetalingsoppdragFactory { + fun pakkInnForUtbetaling(andelerTilkjentYtelse: Collection): List +} + +class AndelTilkjentYtelseForSimuleringFactory : AndelTilkjentYtelseForUtbetalingsoppdragFactory { + override fun pakkInnForUtbetaling(andelerTilkjentYtelse: Collection): List = + andelerTilkjentYtelse.map { AndelTilkjentYtelseForSimulering(it) } + + private class AndelTilkjentYtelseForSimulering( + andelTilkjentYtelse: AndelTilkjentYtelse, + ) : AndelTilkjentYtelseForUtbetalingsoppdrag(andelTilkjentYtelse) { + override var periodeOffset: Long? = andelTilkjentYtelse.periodeOffset + override var forrigePeriodeOffset: Long? = andelTilkjentYtelse.forrigePeriodeOffset + override var kildeBehandlingId: Long? = andelTilkjentYtelse.kildeBehandlingId + } +} + +class AndelTilkjentYtelseForIverksettingFactory : AndelTilkjentYtelseForUtbetalingsoppdragFactory { + override fun pakkInnForUtbetaling(andelerTilkjentYtelse: Collection): List = + andelerTilkjentYtelse.map { AndelTilkjentYtelseForIverksetting(it) } + + private class AndelTilkjentYtelseForIverksetting( + private val andelTilkjentYtelse: AndelTilkjentYtelse, + ) : AndelTilkjentYtelseForUtbetalingsoppdrag(andelTilkjentYtelse) { + override var periodeOffset: Long? + get() = andelTilkjentYtelse.periodeOffset + set(value) { + andelTilkjentYtelse.periodeOffset = value + } + + override var forrigePeriodeOffset: Long? + get() = andelTilkjentYtelse.forrigePeriodeOffset + set(value) { + andelTilkjentYtelse.forrigePeriodeOffset = value + } + + override var kildeBehandlingId: Long? + get() = andelTilkjentYtelse.kildeBehandlingId + set(value) { + andelTilkjentYtelse.kildeBehandlingId = value + } + } +} + +fun Collection.pakkInnForUtbetaling( + andelTilkjentYtelseForUtbetalingsoppdragFactory: AndelTilkjentYtelseForUtbetalingsoppdragFactory, +) = andelTilkjentYtelseForUtbetalingsoppdragFactory.pakkInnForUtbetaling(this) + +enum class YtelsetypeBA( + override val klassifisering: String, + override val satsType: no.nav.familie.felles.utbetalingsgenerator.domain.Utbetalingsperiode.SatsType = no.nav.familie.felles.utbetalingsgenerator.domain.Utbetalingsperiode.SatsType.MND, +) : no.nav.familie.felles.utbetalingsgenerator.domain.Ytelsestype { + ORDINÆR_BARNETRYGD("BATR"), + UTVIDET_BARNETRYGD("BATR"), + SMÅBARNSTILLEGG("BATRSMA"), +} + +enum class FagsystemBA( + override val kode: String, + override val gyldigeSatstyper: Set, +) : Fagsystem { + BARNETRYGD( + "BA", + setOf(YtelsetypeBA.ORDINÆR_BARNETRYGD, YtelsetypeBA.UTVIDET_BARNETRYGD, YtelsetypeBA.SMÅBARNSTILLEGG), + ), +} + +fun no.nav.familie.felles.utbetalingsgenerator.domain.Utbetalingsoppdrag.tilRestUtbetalingsoppdrag(): Utbetalingsoppdrag = + Utbetalingsoppdrag( + kodeEndring = Utbetalingsoppdrag.KodeEndring.valueOf(this.kodeEndring.name), + fagSystem = this.fagSystem, + saksnummer = this.saksnummer, + aktoer = this.aktoer, + saksbehandlerId = this.saksbehandlerId, + avstemmingTidspunkt = this.avstemmingTidspunkt, + utbetalingsperiode = this.utbetalingsperiode.map { it.tilRestUtbetalingsperiode() }, + gOmregning = this.gOmregning, + ) + +fun no.nav.familie.felles.utbetalingsgenerator.domain.Utbetalingsperiode.tilRestUtbetalingsperiode(): Utbetalingsperiode = + Utbetalingsperiode( + erEndringPåEksisterendePeriode = this.erEndringPåEksisterendePeriode, + opphør = this.opphør?.let { Opphør(it.opphørDatoFom) }, + periodeId = this.periodeId, + forrigePeriodeId = this.forrigePeriodeId, + datoForVedtak = this.datoForVedtak, + klassifisering = this.klassifisering, + vedtakdatoFom = this.vedtakdatoFom, + vedtakdatoTom = this.vedtakdatoTom, + sats = this.sats, + satsType = Utbetalingsperiode.SatsType.valueOf(this.satsType.name), + utbetalesTil = this.utbetalesTil, + behandlingId = this.behandlingId, + utbetalingsgrad = this.utbetalingsgrad, + ) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragGeneratorService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragGeneratorService.kt" new file mode 100644 index 000000000..c8373b47f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragGeneratorService.kt" @@ -0,0 +1,262 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.grupperAndeler +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.oppdaterBeståendeAndelerMedOffset +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.felles.utbetalingsgenerator.domain.AndelMedPeriodeIdLongId +import no.nav.familie.felles.utbetalingsgenerator.domain.BeregnetUtbetalingsoppdragLongId +import no.nav.familie.felles.utbetalingsgenerator.domain.IdentOgType +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.unleash.UnleashContextFields +import no.nav.familie.unleash.UnleashService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.YearMonth + +@Service +class UtbetalingsoppdragGeneratorService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val behandlingService: BehandlingService, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val utbetalingsoppdragGenerator: UtbetalingsoppdragGenerator, + private val beregningService: BeregningService, + private val unleashService: UnleashService, +) { + + @Transactional + fun genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak: Vedtak, + saksbehandlerId: String, + erSimulering: Boolean = false, + ): BeregnetUtbetalingsoppdragLongId { + val forrigeTilkjentYtelse = hentForrigeTilkjentYtelse(vedtak.behandling) + val nyTilkjentYtelse = tilkjentYtelseRepository.findByBehandling(behandlingId = vedtak.behandling.id) + val endretMigreringsDato = beregnOmMigreringsDatoErEndret( + vedtak.behandling, + forrigeTilkjentYtelse?.andelerTilkjentYtelse?.minOfOrNull { it.stønadFom }, + ) + val sisteAndelPerKjede = hentSisteAndelTilkjentYtelse(vedtak.behandling.fagsak) + val beregnetUtbetalingsoppdrag = utbetalingsoppdragGenerator.lagUtbetalingsoppdrag( + saksbehandlerId = saksbehandlerId, + vedtak = vedtak, + forrigeTilkjentYtelse = forrigeTilkjentYtelse, + nyTilkjentYtelse = nyTilkjentYtelse, + sisteAndelPerKjede = sisteAndelPerKjede, + erSimulering = erSimulering, + endretMigreringsDato = endretMigreringsDato, + ) + + if (!erSimulering && unleashService.isEnabled( + FeatureToggleConfig.BRUK_NY_UTBETALINGSGENERATOR, + mapOf(UnleashContextFields.FAGSAK_ID to vedtak.behandling.fagsak.id.toString()), + ) + ) { + oppdaterTilkjentYtelse(nyTilkjentYtelse, beregnetUtbetalingsoppdrag) + } + + return beregnetUtbetalingsoppdrag + } + + private fun oppdaterTilkjentYtelse( + tilkjentYtelse: TilkjentYtelse, + beregnetUtbetalingsoppdrag: BeregnetUtbetalingsoppdragLongId, + ) { + secureLogger.info("Oppdaterer TilkjentYtelse med utbetalingsoppdrag og offsets på andeler for behandling ${tilkjentYtelse.behandling.id}") + + oppdaterTilkjentYtelseMedUtbetalingsoppdrag( + tilkjentYtelse = tilkjentYtelse, + utbetalingsoppdrag = beregnetUtbetalingsoppdrag.utbetalingsoppdrag, + ) + oppdaterAndelerMedPeriodeOffset( + tilkjentYtelse = tilkjentYtelse, + andelerMedPeriodeId = beregnetUtbetalingsoppdrag.andeler, + ) + tilkjentYtelseRepository.save(tilkjentYtelse) + } + + private fun hentForrigeTilkjentYtelse(behandling: Behandling): TilkjentYtelse? = + behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(behandling = behandling) + ?.let { tilkjentYtelseRepository.findByBehandlingAndHasUtbetalingsoppdrag(behandlingId = it.id) } + + private fun hentSisteAndelTilkjentYtelse(fagsak: Fagsak) = + andelTilkjentYtelseRepository.hentSisteAndelPerIdentOgType(fagsakId = fagsak.id) + .associateBy { IdentOgType(it.aktør.aktivFødselsnummer(), it.type.tilYtelseType()) } + + @Transactional + fun genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak: Vedtak, + saksbehandlerId: String, + andelTilkjentYtelseForUtbetalingsoppdragFactory: AndelTilkjentYtelseForUtbetalingsoppdragFactory, + erSimulering: Boolean = false, + skalValideres: Boolean = true, + ): Utbetalingsoppdrag { + val oppdatertBehandling = vedtak.behandling + val oppdatertTilstand = + beregningService.hentAndelerTilkjentYtelseMedUtbetalingerForBehandling(behandlingId = oppdatertBehandling.id) + .pakkInnForUtbetaling(andelTilkjentYtelseForUtbetalingsoppdragFactory) + + val oppdaterteKjeder = grupperAndeler(oppdatertTilstand) + + val erFørsteIverksatteBehandlingPåFagsak = + beregningService.hentSisteAndelPerIdent(fagsakId = oppdatertBehandling.fagsak.id) + .isEmpty() + + val utbetalingsoppdrag = if (erFørsteIverksatteBehandlingPåFagsak) { + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + saksbehandlerId = saksbehandlerId, + vedtak = vedtak, + erFørsteBehandlingPåFagsak = erFørsteIverksatteBehandlingPåFagsak, + oppdaterteKjeder = oppdaterteKjeder, + erSimulering = erSimulering, + ) + } else { + val forrigeBehandling = + behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(behandling = oppdatertBehandling) + ?: error("Finner ikke forrige behandling ved oppdatering av tilkjent ytelse og iverksetting av vedtak") + + val forrigeTilstand = + beregningService.hentAndelerTilkjentYtelseMedUtbetalingerForBehandling(forrigeBehandling.id) + .pakkInnForUtbetaling(andelTilkjentYtelseForUtbetalingsoppdragFactory) + + val forrigeKjeder = grupperAndeler(forrigeTilstand) + + val sisteAndelPerIdent = beregningService.hentSisteAndelPerIdent(forrigeBehandling.fagsak.id) + + if (oppdatertTilstand.isNotEmpty()) { + oppdaterBeståendeAndelerMedOffset(oppdaterteKjeder = oppdaterteKjeder, forrigeKjeder = forrigeKjeder) + val tilkjentYtelseMedOppdaterteAndeler = oppdatertTilstand.first().tilkjentYtelse + beregningService.lagreTilkjentYtelseMedOppdaterteAndeler(tilkjentYtelseMedOppdaterteAndeler) + } + + val utbetalingsoppdrag = utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + saksbehandlerId = saksbehandlerId, + vedtak = vedtak, + erFørsteBehandlingPåFagsak = erFørsteIverksatteBehandlingPåFagsak, + forrigeKjeder = forrigeKjeder, + sisteAndelPerIdent = sisteAndelPerIdent, + oppdaterteKjeder = oppdaterteKjeder, + erSimulering = erSimulering, + endretMigreringsDato = beregnOmMigreringsDatoErEndret( + vedtak.behandling, + forrigeTilstand.minByOrNull { it.stønadFom }?.stønadFom, + ), + ) + + if (!erSimulering && ( + oppdatertBehandling.type == BehandlingType.MIGRERING_FRA_INFOTRYGD_OPPHØRT || behandlingHentOgPersisterService.hent( + oppdatertBehandling.id, + ).resultat == Behandlingsresultat.OPPHØRT + ) + ) { + utbetalingsoppdrag.validerOpphørsoppdrag() + } + + utbetalingsoppdrag + } + + if (skalValideres) { + utbetalingsoppdrag.validerNullutbetaling( + behandlingskategori = vedtak.behandling.kategori, + andelerTilkjentYtelse = beregningService.hentAndelerTilkjentYtelseForBehandling(vedtak.behandling.id), + ) + } + + opprettAdvarselLoggVedForstattInnvilgetMedUtbetaling(utbetalingsoppdrag, vedtak.behandling) + + return utbetalingsoppdrag + } + + private fun beregnOmMigreringsDatoErEndret(behandling: Behandling, forrigeTilstandFraDato: YearMonth?): YearMonth? { + val erMigrertSak = + behandlingHentOgPersisterService.hentBehandlinger(behandling.fagsak.id) + .any { it.type == BehandlingType.MIGRERING_FRA_INFOTRYGD } + + if (!erMigrertSak) { + return null + } + + val nyttTilstandFraDato = behandlingService.hentMigreringsdatoPåFagsak(fagsakId = behandling.fagsak.id) + ?.toYearMonth() + ?.plusMonths(1) + + return if (forrigeTilstandFraDato != null && + nyttTilstandFraDato != null && + forrigeTilstandFraDato.isAfter(nyttTilstandFraDato) + ) { + nyttTilstandFraDato + } else { + null + } + } + + private fun utledOpphør( + utbetalingsoppdrag: no.nav.familie.felles.utbetalingsgenerator.domain.Utbetalingsoppdrag, + behandling: Behandling, + ): Opphør { + val erRentOpphør = + utbetalingsoppdrag.utbetalingsperiode.isNotEmpty() && utbetalingsoppdrag.utbetalingsperiode.all { it.opphør != null } + var opphørsdato: LocalDate? = null + if (erRentOpphør) { + opphørsdato = utbetalingsoppdrag.utbetalingsperiode.minOf { it.opphør!!.opphørDatoFom } + } + + if (behandling.type == BehandlingType.REVURDERING) { + val opphørPåRevurdering = utbetalingsoppdrag.utbetalingsperiode.filter { it.opphør != null } + if (opphørPåRevurdering.isNotEmpty()) { + opphørsdato = opphørPåRevurdering.maxOfOrNull { it.opphør!!.opphørDatoFom } + } + } + return Opphør(erRentOpphør = erRentOpphør, opphørsdato = opphørsdato) + } + + private fun oppdaterTilkjentYtelseMedUtbetalingsoppdrag( + tilkjentYtelse: TilkjentYtelse, + utbetalingsoppdrag: no.nav.familie.felles.utbetalingsgenerator.domain.Utbetalingsoppdrag, + ) { + val opphør = utledOpphør(utbetalingsoppdrag, tilkjentYtelse.behandling) + + tilkjentYtelse.utbetalingsoppdrag = objectMapper.writeValueAsString(utbetalingsoppdrag) + tilkjentYtelse.stønadTom = tilkjentYtelse.andelerTilkjentYtelse.maxOfOrNull { it.stønadTom } + tilkjentYtelse.stønadFom = + if (opphør.erRentOpphør) null else tilkjentYtelse.andelerTilkjentYtelse.minOfOrNull { it.stønadFom } + tilkjentYtelse.endretDato = LocalDate.now() + tilkjentYtelse.opphørFom = opphør.opphørsdato?.toYearMonth() + } + + private fun oppdaterAndelerMedPeriodeOffset( + tilkjentYtelse: TilkjentYtelse, + andelerMedPeriodeId: List, + ) { + val andelerPåId = andelerMedPeriodeId.associateBy { it.id } + val andelerTilkjentYtelse = tilkjentYtelse.andelerTilkjentYtelse + val andelerSomSkalSendesTilOppdrag = andelerTilkjentYtelse.filter { it.erAndelSomSkalSendesTilOppdrag() } + if (andelerMedPeriodeId.size != andelerSomSkalSendesTilOppdrag.size) { + error("Antallet andeler med oppdatert periodeOffset, forrigePeriodeOffset og kildeBehandlingId fra ny generator skal være likt antallet andeler med kalkulertUtbetalingsbeløp != 0. Generator gir ${andelerMedPeriodeId.size} andeler men det er ${andelerSomSkalSendesTilOppdrag.size} andeler med kalkulertUtbetalingsbeløp != 0") + } + andelerSomSkalSendesTilOppdrag.forEach { andel -> + val andelMedOffset = andelerPåId[andel.id] + ?: error("Feil ved oppdaterig av offset på andeler. Finner ikke andel med id ${andel.id} blandt andelene med oppdatert offset fra ny generator. Ny generator returnerer andeler med ider [${andelerPåId.values.map { it.id }}]") + andel.periodeOffset = andelMedOffset.periodeId + andel.forrigePeriodeOffset = andelMedOffset.forrigePeriodeId + andel.kildeBehandlingId = andelMedOffset.kildeBehandlingId + } + } + + data class Opphør(val erRentOpphør: Boolean, val opphørsdato: LocalDate?) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragValidator.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragValidator.kt" new file mode 100644 index 000000000..78a08273e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragValidator.kt" @@ -0,0 +1,54 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.KONTAKT_TEAMET_SUFFIX +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.logger +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag + +fun Utbetalingsoppdrag.validerNullutbetaling( + behandlingskategori: BehandlingKategori, + andelerTilkjentYtelse: List, +) { + if (this.utbetalingsperiode.isEmpty() && !kanHaNullutbetaling(behandlingskategori, andelerTilkjentYtelse)) { + throw FunksjonellFeil( + "Utbetalingsoppdraget inneholder ingen utbetalingsperioder " + + "og det er grunn til å tro at denne ikke bør simuleres eller iverksettes. $KONTAKT_TEAMET_SUFFIX", + ) + } +} + +fun opprettAdvarselLoggVedForstattInnvilgetMedUtbetaling( + utbetalingsoppdrag: Utbetalingsoppdrag, + behandling: Behandling, +) { + if (utbetalingsoppdrag.utbetalingsperiode.isNotEmpty() && + behandling.resultat == Behandlingsresultat.FORTSATT_INNVILGET && + behandling.opprettetÅrsak != BehandlingÅrsak.ENDRE_MIGRERINGSDATO + ) { + logger.warn( + "Behandling=$behandling med resultat fortsatt innvilget har utbetalingsperioder. " + + "Dette kan tyde på at noe er galt og burde sjekkes opp.", + ) + } +} + +fun Utbetalingsoppdrag.validerOpphørsoppdrag() { + if (this.harLøpendeUtbetaling()) { + error("Generert utbetalingsoppdrag for opphør inneholder oppdragsperioder med løpende utbetaling.") + } + + if (this.utbetalingsperiode.isNotEmpty() && this.utbetalingsperiode.none { it.opphør != null }) { + error("Generert utbetalingsoppdrag for opphør mangler opphørsperioder.") + } +} + +private fun kanHaNullutbetaling( + behandlingskategori: BehandlingKategori, + andelerTilkjentYtelse: List, +) = behandlingskategori == BehandlingKategori.EØS && + andelerTilkjentYtelse.any { it.erAndelSomharNullutbetaling() } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsperiodeMal.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsperiodeMal.kt" new file mode 100644 index 000000000..62ae62a75 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsperiodeMal.kt" @@ -0,0 +1,78 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.kontrakter.felles.oppdrag.Opphør +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsperiode +import java.math.BigDecimal +import java.time.LocalDate.now +import java.time.YearMonth + +/** + * Lager mal for generering av utbetalingsperioder med tilpasset setting av verdier basert på parametre + * + * @param[vedtak] for vedtakdato og opphørsdato hvis satt + * @param[erEndringPåEksisterendePeriode] ved true vil oppdrag sette asksjonskode ENDR på linje og ikke referere bakover + * @return mal med tilpasset lagPeriodeFraAndel + */ +data class UtbetalingsperiodeMal( + val vedtak: Vedtak, + val erEndringPåEksisterendePeriode: Boolean = false, +) { + + /** + * Lager utbetalingsperioder som legges på utbetalingsoppdrag. En utbetalingsperiode tilsvarer linjer hos økonomi + * + * Denne metoden brukes også til simulering og på dette tidspunktet er ikke vedtaksdatoen satt. + * Derfor defaulter vi til now() når vedtaksdato mangler. + * + * @param[andel] andel som skal mappes til periode + * @param[periodeIdOffset] brukes til å synce våre linjer med det som ligger hos økonomi + * @param[forrigePeriodeIdOffset] peker til forrige i kjeden. Kun relevant når IKKE erEndringPåEksisterendePeriode + * @param[opphørKjedeFom] fom-dato fra tidligste periode i kjede med endring + * @return Periode til utbetalingsoppdrag + */ + fun lagPeriodeFraAndel( + andel: AndelTilkjentYtelseForUtbetalingsoppdrag, + periodeIdOffset: Int, + forrigePeriodeIdOffset: Int?, + opphørKjedeFom: YearMonth? = null, + ): Utbetalingsperiode = + Utbetalingsperiode( + erEndringPåEksisterendePeriode = erEndringPåEksisterendePeriode, + opphør = if (erEndringPåEksisterendePeriode) { + Opphør( + opphørKjedeFom?.førsteDagIInneværendeMåned() + ?: error("Mangler opphørsdato for kjede"), + ) + } else { + null + }, + forrigePeriodeId = forrigePeriodeIdOffset?.let { forrigePeriodeIdOffset.toLong() }, + periodeId = periodeIdOffset.toLong(), + datoForVedtak = vedtak.vedtaksdato?.toLocalDate() ?: now(), + klassifisering = andel.type.klassifisering, + vedtakdatoFom = andel.stønadFom.førsteDagIInneværendeMåned(), + vedtakdatoTom = andel.stønadTom.sisteDagIInneværendeMåned(), + sats = BigDecimal(andel.kalkulertUtbetalingsbeløp), + satsType = Utbetalingsperiode.SatsType.MND, + utbetalesTil = hentUtebetalesTil(vedtak.behandling.fagsak), + behandlingId = vedtak.behandling.id, + ) + + private fun hentUtebetalesTil(fagsak: Fagsak): String { + return when (fagsak.type) { + FagsakType.INSTITUSJON -> { + fagsak.institusjon?.tssEksternId + ?: error("Fagsak ${fagsak.id} er av type institusjon og mangler informasjon om institusjonen") + } + + else -> { + fagsak.aktør.aktivFødselsnummer() + } + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/internkonsistensavstemming/InternKonsistensavstemmingScheduler.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/internkonsistensavstemming/InternKonsistensavstemmingScheduler.kt" new file mode 100644 index 000000000..1d68c1bff --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/internkonsistensavstemming/InternKonsistensavstemmingScheduler.kt" @@ -0,0 +1,20 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi.internkonsistensavstemming + +import no.nav.familie.ba.sak.task.internkonsistensavstemming.OpprettInternKonsistensavstemmingTaskerTask +import no.nav.familie.leader.LeaderClient +import no.nav.familie.prosessering.internal.TaskService +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component + +@Component +class InternKonsistensavstemmingScheduler( + val taskService: TaskService, +) { + + @Scheduled(cron = "0 0 0 29 * *") + fun startInternKonsistensavstemming() { + if (LeaderClient.isLeader() == true) { + taskService.save(OpprettInternKonsistensavstemmingTaskerTask.opprettTask()) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/internkonsistensavstemming/InternKonsistensavstemmingService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/internkonsistensavstemming/InternKonsistensavstemmingService.kt" new file mode 100644 index 000000000..05ec6e788 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/internkonsistensavstemming/InternKonsistensavstemmingService.kt" @@ -0,0 +1,103 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi.internkonsistensavstemming + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiKlient +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.task.InternKonsistensavstemmingTask +import no.nav.familie.ba.sak.task.OpprettTaskService.Companion.overstyrTaskMedNyCallId +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.log.IdUtils +import no.nav.familie.prosessering.internal.TaskService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDateTime + +@Service +class InternKonsistensavstemmingService( + val økonomiKlient: ØkonomiKlient, + val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + val fagsakRepository: FagsakRepository, + val taskService: TaskService, +) { + fun validerLikUtbetalingIAndeleneOgUtbetalingsoppdragetPåAlleFagsaker(maksAntallTasker: Int = Int.MAX_VALUE) { + val fagsakerSomIkkeErArkivert = fagsakRepository + .hentFagsakerSomIkkeErArkivert() + .map { it.id } + .sortedBy { it } + + val startTid = LocalDateTime.now() + + fagsakerSomIkkeErArkivert + // Antall fagsaker per task burde være en multippel av 3000 siden vi chunker databasekallet i 3000 i familie-oppdrag + .chunked(3000) + .take(maksAntallTasker) + .forEachIndexed { index, fagsaker -> + // Venter 15 sekunder mellom hver task for å ikke overkjøre familie-oppdrag siden ba-sak har mer ressurser + val startTidForTask = startTid.plusSeconds(15 * index.toLong()) + overstyrTaskMedNyCallId(IdUtils.generateId()) { + val task = InternKonsistensavstemmingTask.opprettTask(fagsaker.toSet(), startTidForTask) + taskService.save(task) + } + } + } + + fun validerLikUtbetalingIAndeleneOgUtbetalingsoppdraget(fagsaker: Set) { + val fagsakTilSisteUtbetalingsoppdragOgSisteAndelerMap = + hentFagsakTilSisteUtbetalingsoppdragOgSisteAndelerMap(fagsaker) + + val fagsakerMedFeil = fagsakTilSisteUtbetalingsoppdragOgSisteAndelerMap.mapNotNull { entry -> + val fagsakId = entry.key + val andeler = entry.value.first + val utbetalingsoppdrag = entry.value.second + + fagsakId.takeIf { erForskjellMellomAndelerOgOppdrag(andeler, utbetalingsoppdrag, fagsakId) } + } + + if (fagsakerMedFeil.isNotEmpty()) { + throw Feil( + "Tilkjent ytelse og utbetalingsoppdraget som er lagret i familie-oppdrag er inkonsistent" + + "\nSe secure logs for mer detaljer." + + "\nDette gjelder fagsakene $fagsakerMedFeil", + ) + } + } + + private fun hentFagsakTilSisteUtbetalingsoppdragOgSisteAndelerMap(fagsakIder: Set): Map, Utbetalingsoppdrag?>> { + val scope = CoroutineScope(SupervisorJob()) + val utbetalingsoppdragDeferred = scope.async { + økonomiKlient.hentSisteUtbetalingsoppdragForFagsaker(fagsakIder) + } + + val fagsakTilAndelerISisteBehandlingSendTilØkonomiMap = + hentFagsakTilAndelerISisteBehandlingSendtTilØkonomiMap(fagsakIder) + + val fagsakTilSisteUtbetalingsoppdragMap = runBlocking { utbetalingsoppdragDeferred.await() } + .associate { it.fagsakId to it.utbetalingsoppdrag } + + val fagsakTilSisteUtbetalingsoppdragOgSisteAndeler = + fagsakTilAndelerISisteBehandlingSendTilØkonomiMap.mapValues { (fagsakId, andel) -> + Pair(andel, fagsakTilSisteUtbetalingsoppdragMap[fagsakId]) + } + return fagsakTilSisteUtbetalingsoppdragOgSisteAndeler + } + + private fun hentFagsakTilAndelerISisteBehandlingSendtTilØkonomiMap(fagsaker: Set): Map> { + val behandlinger = behandlingHentOgPersisterService.hentSisteBehandlingSomErSendtTilØkonomiPerFagsak(fagsaker) + + return andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandlinger(behandlinger.map { it.id }) + .groupBy { it.behandlingId } + .mapKeys { (behandlingId, _) -> behandlinger.find { it.id == behandlingId }?.fagsak?.id!! } + } + + companion object { + val logger = LoggerFactory.getLogger(this::class.java)!! + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/internkonsistensavstemming/InternKonsistensavstemmingUtil.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/internkonsistensavstemming/InternKonsistensavstemmingUtil.kt" new file mode 100644 index 000000000..f6693e4a8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/internkonsistensavstemming/InternKonsistensavstemmingUtil.kt" @@ -0,0 +1,111 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi.internkonsistensavstemming + +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsperiode +import java.math.BigDecimal + +fun erForskjellMellomAndelerOgOppdrag( + andeler: List, + utbetalingsoppdrag: Utbetalingsoppdrag?, + fagsakId: Long, +): Boolean { + val utbetalingsperioder = + utbetalingsoppdrag?.utbetalingsperiode + ?.filter { it.opphør == null } + ?: emptyList() + + val forskjellMellomAndeleneOgUtbetalingsoppdraget = + hentForskjellIAndelerOgUtbetalingsoppdrag(utbetalingsperioder, andeler) + + when (forskjellMellomAndeleneOgUtbetalingsoppdraget) { + is UtbetalingsperioderUtenTilsvarendeAndel -> secureLogger.info( + "Fagsak $fagsakId har sendt utbetalingsperiode(r) til økonomi som ikke har tilsvarende andel tilkjent ytelse." + + "\nDet er differanse i perioden(e) ${forskjellMellomAndeleneOgUtbetalingsoppdraget.utbetalingsperioder.tilTidStrenger()}." + + "\n\nSiste utbetalingsoppdrag som er sendt til familie-øknonomi på fagsaken er:" + + "\n$utbetalingsoppdrag", + ) + + is IngenForskjell -> Unit + } + + return forskjellMellomAndeleneOgUtbetalingsoppdraget !is IngenForskjell +} + +private fun hentForskjellIAndelerOgUtbetalingsoppdrag( + utbetalingsperioder: List, + andeler: List, +): AndelOgOppdragForskjell { + val utbetalingsperioderUtenTilsvarendeAndel = utbetalingsperioder.filter { + it.erIngenPersonerMedTilsvarendeAndelITidsrommet(andeler) + } + + return if (utbetalingsperioderUtenTilsvarendeAndel.isEmpty()) { + IngenForskjell + } else { + UtbetalingsperioderUtenTilsvarendeAndel(utbetalingsperioderUtenTilsvarendeAndel) + } +} + +private fun Utbetalingsperiode.erIngenPersonerMedTilsvarendeAndelITidsrommet( + andeler: List, +): Boolean { + val andelsTidslinjerPerPersonOgYtelsetype = andeler + .groupBy { Pair(it.aktør, it.type) } + .map { (_, andeler) -> andeler.tilBeløpstidslinje() } + + return andelsTidslinjerPerPersonOgYtelsetype.all { + !this.harTilsvarendeAndelerForPersonOgYtelsetype(it) + } +} + +private fun Utbetalingsperiode.harTilsvarendeAndelerForPersonOgYtelsetype( + andelerTidslinjeForEnPersonOgYtelsetype: Tidslinje, +): Boolean { + val erAndelLikUtbetalingTidslinje = this.tilBeløpstidslinje() + .kombinerMed(andelerTidslinjeForEnPersonOgYtelsetype) { utbetalingsperiode, andel -> + utbetalingsperiode?.let { utbetalingsperiode == andel } + } + + return erAndelLikUtbetalingTidslinje.perioder().all { it.innhold != false } +} + +private fun Utbetalingsperiode.tilBeløpstidslinje(): Tidslinje = tidslinje { + listOf( + Periode( + fraOgMed = this.vedtakdatoFom.tilMånedTidspunkt(), + tilOgMed = this.vedtakdatoTom.tilMånedTidspunkt(), + innhold = this.sats, + ), + ) +} + +private fun List.tilBeløpstidslinje(): Tidslinje = tidslinje { + this.map { + Periode( + fraOgMed = it.stønadFom.tilTidspunkt(), + tilOgMed = it.stønadTom.tilTidspunkt(), + innhold = it.kalkulertUtbetalingsbeløp.toBigDecimal(), + ) + } +} + +private fun List.tilTidStrenger() = + Utils.slåSammen(this.map { "${it.vedtakdatoFom.toYearMonth()} til ${it.vedtakdatoTom.toYearMonth()}" }) + +private sealed interface AndelOgOppdragForskjell + +private data class UtbetalingsperioderUtenTilsvarendeAndel(val utbetalingsperioder: List) : + AndelOgOppdragForskjell + +private object IngenForskjell : AndelOgOppdragForskjell diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiKlient.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiKlient.kt" new file mode 100644 index 000000000..057a7340e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiKlient.kt" @@ -0,0 +1,191 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.kallEksternTjenesteRessurs +import no.nav.familie.ba.sak.config.RestTemplateConfig.Companion.RETRY_BACKOFF_500MS +import no.nav.familie.ba.sak.task.dto.FAGSYSTEM +import no.nav.familie.http.client.AbstractRestClient +import no.nav.familie.kontrakter.felles.oppdrag.GrensesnittavstemmingRequest +import no.nav.familie.kontrakter.felles.oppdrag.KonsistensavstemmingRequestV2 +import no.nav.familie.kontrakter.felles.oppdrag.OppdragId +import no.nav.familie.kontrakter.felles.oppdrag.OppdragStatus +import no.nav.familie.kontrakter.felles.oppdrag.PerioderForBehandling +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.kontrakter.felles.simulering.DetaljertSimuleringResultat +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.retry.annotation.Backoff +import org.springframework.retry.annotation.Retryable +import org.springframework.stereotype.Service +import org.springframework.web.client.RestOperations +import java.net.URI +import java.time.LocalDateTime +import java.util.UUID + +@Service +class ØkonomiKlient( + @Value("\${FAMILIE_OPPDRAG_API_URL}") + private val familieOppdragUri: String, + @Qualifier("jwtBearer") restOperations: RestOperations, +) : AbstractRestClient(restOperations, "økonomi_barnetrygd") { + + fun iverksettOppdrag(utbetalingsoppdrag: Utbetalingsoppdrag): String { + val uri = URI.create("$familieOppdragUri/oppdrag") + return kallEksternTjenesteRessurs( + tjeneste = "familie-oppdrag", + uri = uri, + formål = "Iverksetter mot oppdrag", + ) { + postForEntity(uri = uri, utbetalingsoppdrag) + } + } + + fun iverksettOppdragPåNytt(utbetalingsoppdrag: Utbetalingsoppdrag, versjon: Int = 1): String { + val uri = URI.create("$familieOppdragUri/oppdragPaaNytt/$versjon") + return kallEksternTjenesteRessurs( + tjeneste = "familie-oppdrag", + uri = uri, + formål = "Iverksetter mot oppdrag på nytt", + ) { + postForEntity(uri = uri, utbetalingsoppdrag) + } + } + + @Retryable( + value = [Exception::class], + maxAttempts = 3, + backoff = Backoff(delayExpression = RETRY_BACKOFF_500MS), + ) + fun hentSimulering(utbetalingsoppdrag: Utbetalingsoppdrag): DetaljertSimuleringResultat { + val uri = URI.create("$familieOppdragUri/simulering/v1") + + return kallEksternTjenesteRessurs( + tjeneste = "familie-oppdrag", + uri = uri, + formål = "Henter simulering på fagsak ${utbetalingsoppdrag.saksnummer} fra Økonomi", + ) { + postForEntity(uri = uri, utbetalingsoppdrag) + } + } + + fun hentStatus(oppdragId: OppdragId): OppdragStatus { + val uri = URI.create("$familieOppdragUri/status") + return kallEksternTjenesteRessurs( + tjeneste = "familie-oppdrag", + uri = uri, + formål = "Henter oppdragstatus fra Økonomi", + ) { + postForEntity(uri = uri, oppdragId) + } + } + + fun grensesnittavstemOppdrag(fraDato: LocalDateTime, tilDato: LocalDateTime): String { + val uri = URI.create("$familieOppdragUri/grensesnittavstemming") + return kallEksternTjenesteRessurs( + tjeneste = "familie-oppdrag", + uri = uri, + formål = "Gjør grensesnittavstemming mot oppdrag", + ) { + postForEntity( + uri = uri, + GrensesnittavstemmingRequest( + fagsystem = FAGSYSTEM, + fra = fraDato, + til = tilDato, + ), + ) + } + } + + fun konsistensavstemOppdragStart( + avstemmingsdato: LocalDateTime, + transaksjonsId: UUID, + ): String { + val uri = URI.create( + "$familieOppdragUri/v2/konsistensavstemming" + + "?sendStartmelding=true&sendAvsluttmelding=false&transaksjonsId=$transaksjonsId", + ) + + return kallEksternTjenesteRessurs( + tjeneste = "familie-oppdrag", + uri = uri, + formål = "Start konsistensavstemming mot oppdrag i batch", + ) { + postForEntity( + uri = uri, + KonsistensavstemmingRequestV2( + fagsystem = FAGSYSTEM, + avstemmingstidspunkt = avstemmingsdato, + perioderForBehandlinger = emptyList(), + ), + ) + } + } + + fun konsistensavstemOppdragData( + avstemmingsdato: LocalDateTime, + perioderTilAvstemming: List, + transaksjonsId: UUID, + ): String { + val uri = URI.create( + "$familieOppdragUri/v2/konsistensavstemming" + + "?sendStartmelding=false&sendAvsluttmelding=false&transaksjonsId=$transaksjonsId", + ) + + return kallEksternTjenesteRessurs( + tjeneste = "familie-oppdrag", + uri = uri, + formål = "Konsistenstavstemmer chunk mot oppdrag", + ) { + postForEntity( + uri = uri, + KonsistensavstemmingRequestV2( + fagsystem = FAGSYSTEM, + avstemmingstidspunkt = avstemmingsdato, + perioderForBehandlinger = perioderTilAvstemming, + ), + ) + } + } + + fun konsistensavstemOppdragAvslutt( + avstemmingsdato: LocalDateTime, + transaksjonsId: UUID, + ): String { + val uri = URI.create( + "$familieOppdragUri/v2/konsistensavstemming" + + "?sendStartmelding=false&sendAvsluttmelding=true&transaksjonsId=$transaksjonsId", + ) + return kallEksternTjenesteRessurs( + tjeneste = "familie-oppdrag", + uri = uri, + formål = "Avslutt konsistensavstemming mot oppdrag", + ) { + postForEntity( + uri = uri, + KonsistensavstemmingRequestV2( + fagsystem = FAGSYSTEM, + avstemmingstidspunkt = avstemmingsdato, + perioderForBehandlinger = emptyList(), + ), + ) + } + } + + fun hentSisteUtbetalingsoppdragForFagsaker( + fagsakIder: Set, + ): List { + val uri = URI.create("$familieOppdragUri/$FAGSYSTEM/fagsaker/siste-utbetalingsoppdrag") + + return kallEksternTjenesteRessurs( + tjeneste = "familie-oppdrag", + uri = uri, + formål = "Hent utbetalingsoppdrag for fagsaker", + ) { postForEntity(uri = uri, payload = fagsakIder) } + } +} + +data class UtbetalingsoppdragMedBehandlingOgFagsak( + val fagsakId: Long, + val behandlingId: Long, + val utbetalingsoppdrag: Utbetalingsoppdrag, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiService.kt" new file mode 100644 index 000000000..29c5a9fe0 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiService.kt" @@ -0,0 +1,118 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValideringService +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.utbetalingsoppdrag +import no.nav.familie.ba.sak.kjerne.simulering.KontrollerNyUtbetalingsgeneratorService +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.http.client.RessursException +import no.nav.familie.kontrakter.felles.oppdrag.OppdragId +import no.nav.familie.kontrakter.felles.oppdrag.OppdragStatus +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.unleash.UnleashContextFields +import no.nav.familie.unleash.UnleashService +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service + +@Service +class ØkonomiService( + private val økonomiKlient: ØkonomiKlient, + private val beregningService: BeregningService, + private val tilkjentYtelseValideringService: TilkjentYtelseValideringService, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + private val kontrollerNyUtbetalingsgeneratorService: KontrollerNyUtbetalingsgeneratorService, + private val utbetalingsoppdragGeneratorService: UtbetalingsoppdragGeneratorService, + private val unleashService: UnleashService, + +) { + private val sammeOppdragSendtKonflikt = Metrics.counter("familie.ba.sak.samme.oppdrag.sendt.konflikt") + + fun oppdaterTilkjentYtelseMedUtbetalingsoppdragOgIverksett( + vedtak: Vedtak, + saksbehandlerId: String, + andelTilkjentYtelseForUtbetalingsoppdragFactory: AndelTilkjentYtelseForUtbetalingsoppdragFactory, + ): Utbetalingsoppdrag { + val oppdatertBehandling = vedtak.behandling + + val brukNyUtbetalingsoppdragGenerator = unleashService.isEnabled( + FeatureToggleConfig.BRUK_NY_UTBETALINGSGENERATOR, + mapOf(UnleashContextFields.FAGSAK_ID to vedtak.behandling.fagsak.id.toString()), + ) + + if (!brukNyUtbetalingsoppdragGenerator) { + kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = vedtak, + saksbehandlerId = saksbehandlerId, + ) + } + + val utbetalingsoppdrag: Utbetalingsoppdrag = + if (brukNyUtbetalingsoppdragGenerator) { + logger.info("Bruker ny utbetalingsgenerator for behandling ${vedtak.behandling.id}") + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak, + saksbehandlerId, + ).utbetalingsoppdrag.tilRestUtbetalingsoppdrag() + } else { + val utbetalingsoppdrag = + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak, + saksbehandlerId, + andelTilkjentYtelseForUtbetalingsoppdragFactory, + ) + + beregningService.oppdaterTilkjentYtelseMedUtbetalingsoppdrag(oppdatertBehandling, utbetalingsoppdrag) + utbetalingsoppdrag + } + + tilkjentYtelseValideringService.validerIngenAndelerTilkjentYtelseMedSammeOffsetIBehandling(behandlingId = vedtak.behandling.id) + + iverksettOppdrag(utbetalingsoppdrag, oppdatertBehandling.id) + return utbetalingsoppdrag + } + + private fun iverksettOppdrag(utbetalingsoppdrag: Utbetalingsoppdrag, behandlingId: Long) { + if (!utbetalingsoppdrag.skalIverksettesMotOppdrag()) { + logger.warn( + "Iverksetter ikke noe mot oppdrag. " + + "Ingen utbetalingsperioder for behandlingId=$behandlingId", + ) + return + } + try { + økonomiKlient.iverksettOppdrag(utbetalingsoppdrag) + } catch (exception: Exception) { + if (exception is RessursException && + exception.httpStatus == HttpStatus.CONFLICT + ) { + sammeOppdragSendtKonflikt.increment() + logger.info("Bypasset feil med HttpKode 409 ved iverksetting mot økonomi for fagsak ${utbetalingsoppdrag.saksnummer}") + return + } else { + throw exception + } + } + } + + fun hentStatus(oppdragId: OppdragId, behandlingId: Long): OppdragStatus = + if (tilkjentYtelseRepository.findByBehandling(behandlingId).skalIverksettesMotOppdrag()) { + økonomiKlient.hentStatus(oppdragId) + } else { + OppdragStatus.KVITTERT_OK + } + + companion object { + + val logger = LoggerFactory.getLogger(ØkonomiService::class.java) + } +} + +fun Utbetalingsoppdrag.skalIverksettesMotOppdrag(): Boolean = utbetalingsperiode.isNotEmpty() + +private fun TilkjentYtelse.skalIverksettesMotOppdrag(): Boolean = + this.utbetalingsoppdrag()?.skalIverksettesMotOppdrag() ?: false diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiUtils.kt" new file mode 100644 index 000000000..ee9af177b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiUtils.kt" @@ -0,0 +1,216 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +object ØkonomiUtils { + + /** + * Deler andeler inn i gruppene de skal kjedes i. Utbetalingsperioder kobles i kjeder per person, per type. + * + * @param[andeler] andeler som skal sorteres i grupper for kjeding + * @return ident med kjedegruppe. + */ + fun grupperAndeler(andeler: List): Map> { + val grupperteAndeler = andeler + .groupBy { IdentOgYtelse(it.aktør.aktivFødselsnummer(), it.type) } + + if (grupperteAndeler.keys.count { it.type == YtelseType.SMÅBARNSTILLEGG } > 1) { + throw IllegalArgumentException("Finnes flere personer med småbarnstillegg") + } + return grupperteAndeler + } + + /** + * Lager oversikt over siste andel i hver kjede som finnes uten endring i oppdatert tilstand. + * Vi må opphøre og eventuelt gjenoppbygge hver kjede etter denne. Må ta vare på andel og ikke kun offset da + * filtrering av oppdaterte andeler senere skjer før offset blir satt. + * Personident er identifikator for hver kjede, med unntak av småbarnstillegg som vil være en egen "person". + * + * @param[forrigeKjeder] forrige behandlings tilstand + * @param[oppdaterteKjeder] nåværende tilstand + * @return map med personident og siste bestående andel. Bestående andel=null dersom alle opphøres eller ny person. + */ + fun sisteBeståendeAndelPerKjede( + forrigeKjeder: Map>, + oppdaterteKjeder: Map>, + ): Map { + val allePersoner = forrigeKjeder.keys.union(oppdaterteKjeder.keys) + return allePersoner.associateWith { kjedeIdentifikator -> + beståendeAndelerIKjede( + forrigeKjede = forrigeKjeder[kjedeIdentifikator], + oppdatertKjede = oppdaterteKjeder[kjedeIdentifikator], + ) + ?.sortedBy { it.periodeOffset }?.lastOrNull() + } + } + + /** + * Finn alle presidenter i forrige og oppdatert liste. Presidentene er identifikatorn for hver kjede. + * Set andeler tilkjentytelse til null som indikerer at hele kjeden skal opphøre. + * + * @param[forrigeKjeder] forrige behandlings tilstand + * @param[oppdaterteKjeder] nåværende tilstand + * @return map med personident og andel=null som markerer at alle andeler skal opphøres. + */ + fun sisteAndelPerKjede( + forrigeKjeder: Map>, + oppdaterteKjeder: Map>, + ): Map = + forrigeKjeder.keys.union(oppdaterteKjeder.keys).associateWith { null } + + private fun beståendeAndelerIKjede( + forrigeKjede: List?, + oppdatertKjede: List?, + ): List? { + val forrige = forrigeKjede?.toSet() ?: emptySet() + val oppdatert = oppdatertKjede?.toSet() ?: emptySet() + val førsteEndring = forrige.disjunkteAndeler(oppdatert).minByOrNull { it.stønadFom }?.stønadFom + return if (førsteEndring != null) { + forrige.snittAndeler(oppdatert) + .filter { it.stønadFom.isBefore(førsteEndring) } + } else { + forrigeKjede ?: emptyList() + } + } + + /** + * Setter eksisterende offset og kilde på andeler som skal bestå + * + * @param[forrigeKjeder] forrige behandlings tilstand + * @param[oppdaterteKjeder] nåværende tilstand + * @return map med personident og oppdaterte kjeder + */ + fun oppdaterBeståendeAndelerMedOffset( + oppdaterteKjeder: Map>, + forrigeKjeder: Map>, + ): Map> { + oppdaterteKjeder + .filter { forrigeKjeder.containsKey(it.key) } + .forEach { (kjedeIdentifikator, oppdatertKjede) -> + val beståendeFraForrige = + beståendeAndelerIKjede( + forrigeKjede = forrigeKjeder.getValue(kjedeIdentifikator), + oppdatertKjede = oppdatertKjede, + ) + beståendeFraForrige?.forEach { bestående -> + val beståendeIOppdatert = oppdatertKjede.find { it.erTilsvarendeForUtbetaling(bestående) } + ?: error("Kan ikke finne andel fra utledet bestående andeler i oppdatert tilstand.") + beståendeIOppdatert.periodeOffset = bestående.periodeOffset + beståendeIOppdatert.forrigePeriodeOffset = bestående.forrigePeriodeOffset + beståendeIOppdatert.kildeBehandlingId = bestående.kildeBehandlingId + } + } + return oppdaterteKjeder + } + + /** + * Tar utgangspunkt i ny tilstand og finner andeler som må bygges opp (nye, endrede og bestående etter første endring) + * + * @param[oppdaterteKjeder] ny tilstand + * @param[sisteBeståendeAndelIHverKjede] andeler man må bygge opp etter + * @return andeler som må bygges fordelt på kjeder + */ + fun andelerTilOpprettelse( + oppdaterteKjeder: Map>, + sisteBeståendeAndelIHverKjede: Map, + ): List> = + oppdaterteKjeder.map { (kjedeIdentifikator, oppdatertKjedeTilstand) -> + if (sisteBeståendeAndelIHverKjede[kjedeIdentifikator] != null) { + oppdatertKjedeTilstand.filter { it.stønadFom.isAfter(sisteBeståendeAndelIHverKjede[kjedeIdentifikator]!!.stønadTom) } + } else { + oppdatertKjedeTilstand + } + }.filter { it.isNotEmpty() } + + /** + * Tar utgangspunkt i forrige tilstand og finner kjeder med andeler til opphør og tilhørende opphørsdato + * + * @param[forrigeKjeder] ny tilstand + * @param[sisteBeståendeAndelIHverKjede] andeler man må bygge opp etter + * @param[endretMigreringsDato] Satt betyr at opphørsdato skal settes fra før tidligeste dato i eksisterende kjede. + * @param[sisteAndelPerIdent] Vi skal alltid opphøre mot siste andelen i en kjede. + * I de tillfeller der man har en førstegångsbehandling med 2 andeler + * <----> lid 0 + * <----> lid 1 + * Og man i en revurdering opphør den siste andelen + * <----> lid 0 + * Så skal man i en ny revurdering fortsatt peke til lid 0, og ikke mot 1 + * + * @return map av siste andel og opphørsdato fra kjeder med opphør + */ + fun andelerTilOpphørMedDato( + forrigeKjeder: Map>, + sisteBeståendeAndelIHverKjede: Map, + endretMigreringsDato: YearMonth? = null, + sisteAndelPerIdent: Map, + ): List> = + forrigeKjeder + .mapValues { (person, forrigeAndeler) -> + forrigeAndeler.filter { + altIKjedeOpphøres(person, sisteBeståendeAndelIHverKjede) || + andelOpphøres(person, it, sisteBeståendeAndelIHverKjede) + } + } + .filter { (_, andelerSomOpphøres) -> andelerSomOpphøres.isNotEmpty() } + .map { (identOgYtelse, kjedeEtterFørsteEndring) -> + val sisteAndel = + sisteAndelPerIdent[identOgYtelse] ?: error("Finner ikke siste andel for $identOgYtelse") + sisteAndel to (endretMigreringsDato ?: kjedeEtterFørsteEndring.minOf { it.stønadFom }) + } + + private fun altIKjedeOpphøres( + kjedeidentifikator: IdentOgYtelse, + sisteBeståendeAndelIHverKjede: Map, + ): Boolean = sisteBeståendeAndelIHverKjede[kjedeidentifikator] == null + + private fun andelOpphøres( + kjedeidentifikator: IdentOgYtelse, + andel: AndelTilkjentYtelseForUtbetalingsoppdrag, + sisteBeståendeAndelIHverKjede: Map, + ): Boolean = andel.stønadFom > sisteBeståendeAndelIHverKjede[kjedeidentifikator]!!.stønadTom + + const val SMÅBARNSTILLEGG_SUFFIX = "_SMÅBARNSTILLEGG" +} + +/** + * Merk at det søkes snitt på visse attributter (erTilsvarendeForUtbetaling) + * og man kun returnerer objekter fra receiver (ikke other) + */ +private fun Set.snittAndeler(other: Set): Set { + val andelerKunIDenne = this.subtractAndeler(other) + return this.subtractAndeler(andelerKunIDenne) +} + +private fun Set.disjunkteAndeler(other: Set): Set { + val andelerKunIDenne = this.subtractAndeler(other) + val andelerKunIAnnen = other.subtractAndeler(this) + return andelerKunIDenne.union(andelerKunIAnnen) +} + +private fun Set.subtractAndeler(other: Set): Set { + return this.filter { a -> + other.none { b -> a.erTilsvarendeForUtbetaling(b) } + }.toSet() +} + +private fun AndelTilkjentYtelseForUtbetalingsoppdrag.erTilsvarendeForUtbetaling(other: AndelTilkjentYtelseForUtbetalingsoppdrag): Boolean { + return ( + this.aktør == other.aktør && + this.stønadFom == other.stønadFom && + this.stønadTom == other.stønadTom && + this.kalkulertUtbetalingsbeløp == other.kalkulertUtbetalingsbeløp && + this.type == other.type + ) +} + +fun Utbetalingsoppdrag.harLøpendeUtbetaling() = + this.utbetalingsperiode.any { + it.opphør == null && + it.sats > BigDecimal.ZERO && + it.vedtakdatoTom > LocalDate.now().sisteDagIMåned() + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/ForvalterController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/ForvalterController.kt new file mode 100644 index 000000000..8a513b017 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/ForvalterController.kt @@ -0,0 +1,242 @@ +package no.nav.familie.ba.sak.internal + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.oppgave.domene.OppgaveRepository +import no.nav.familie.ba.sak.kjerne.autovedtak.småbarnstillegg.RestartAvSmåbarnstilleggService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID +import kotlin.concurrent.thread + +@RestController +@RequestMapping("/api/forvalter") +@ProtectedWithClaims(issuer = "azuread") +class ForvalterController( + private val oppgaveRepository: OppgaveRepository, + private val integrasjonClient: IntegrasjonClient, + private val restartAvSmåbarnstilleggService: RestartAvSmåbarnstilleggService, + private val forvalterService: ForvalterService, + private val behandlingsRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, + private val fagsakService: FagsakService, +) { + private val logger: Logger = LoggerFactory.getLogger(ForvalterController::class.java) + + @PostMapping( + path = ["/ferdigstill-oppgaver"], + consumes = [MediaType.APPLICATION_JSON_VALUE], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + fun ferdigstillListeMedOppgaver(@RequestBody oppgaveListe: List): ResponseEntity { + var antallFeil = 0 + oppgaveListe.forEach { oppgaveId -> + Result.runCatching { + ferdigstillOppgave(oppgaveId) + }.fold( + onSuccess = { logger.info("Har ferdigstilt oppgave med oppgaveId=$oppgaveId") }, + onFailure = { + logger.warn("Klarte ikke å ferdigstille oppgaveId=$oppgaveId", it) + antallFeil = antallFeil.inc() + }, + ) + } + return ResponseEntity.ok("Ferdigstill oppgaver kjørt. Antall som ikke ble ferdigstilt: $antallFeil") + } + + @PostMapping( + path = ["/start-manuell-restart-av-smaabarnstillegg-jobb/skalOppretteOppgaver/{skalOppretteOppgaver}"], + consumes = [MediaType.APPLICATION_JSON_VALUE], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + fun triggManuellStartAvSmåbarnstillegg(@PathVariable skalOppretteOppgaver: Boolean = true): ResponseEntity { + restartAvSmåbarnstilleggService.finnOgOpprettetOppgaveForSmåbarnstilleggSomSkalRestartesIDenneMåned( + skalOppretteOppgaver, + ) + return ResponseEntity.ok("OK") + } + + private fun ferdigstillOppgave(oppgaveId: Long) { + integrasjonClient.ferdigstillOppgave(oppgaveId) + oppgaveRepository.findByGsakId(oppgaveId.toString()).also { + if (it != null && !it.erFerdigstilt) { + it.erFerdigstilt = true + oppgaveRepository.saveAndFlush(it) + } + } + } + + @PostMapping(path = ["/lag-og-send-utbetalingsoppdrag-til-økonomi"]) + fun lagOgSendUtbetalingsoppdragTilØkonomi(@RequestBody behandlinger: Set): ResponseEntity { + behandlinger.forEach { + try { + forvalterService.lagOgSendUtbetalingsoppdragTilØkonomiForBehandling(it) + } catch (exception: Exception) { + secureLogger.info( + "Kunne ikke sende behandling med id $it til økonomi" + + "\n$exception", + ) + } + } + + return ResponseEntity.ok("OK") + } + + @PostMapping("/kjor-satsendring-uten-validering") + @Transactional + fun kjørSatsendringFor(@RequestBody fagsakListe: List) { + fagsakListe.parallelStream().forEach { fagsakId -> + try { + logger.info("Kjører satsendring uten validering for $fagsakId") + forvalterService.kjørForenkletSatsendringFor(fagsakId) + } catch (e: Exception) { + logger.warn("Klarte ikke kjøre satsendring for fagsakId=$fagsakId", e) + } + } + } + + @PostMapping("/identifiser-utbetalinger-over-100-prosent") + fun identifiserUtbetalingerOver100Prosent(): ResponseEntity> { + val callId = UUID.randomUUID().toString() + thread { + forvalterService.identifiserUtbetalingerOver100Prosent(callId) + } + return ResponseEntity.ok(Pair("callId", callId)) + } + + @PostMapping("/finnBehandlingerMedPotensieltFeilUtbetalingsoppdrag") + fun identifiserBehandlingerSomKanKrevePatching(): ResponseEntity { + logger.info("Starter identifiserBehandlingerSomKanKrevePatching") + val validerteUtbetalingsoppdragMedFeil = + forvalterService.identifiserPåvirkedeBehandlingerOgValiderOpphørsdatoIUtbetalingsoppdrag() + secureLogger.warn("Følgende behandlinger har ikke korrekte opphørsdatoer: [$validerteUtbetalingsoppdragMedFeil]") + return ResponseEntity.ok(validerteUtbetalingsoppdragMedFeil) + } + + @PostMapping("/sjekkOmTilkjentYtelseForBehandlingHarUkorrektOpphørsdato") + fun sjekkOmTilkjentYtelseForBehandlingHarUkorrektOpphørsdato(@RequestBody behandlingListe: List): ResponseEntity { + val validerteUtbetalingsoppdragMedFeil: Set = + behandlingListe.fold(LinkedHashSet()) { accumulator, behandlingId -> + val validertUtbetalingsoppdrag = + forvalterService.validerOpphørsdatoIUtbetalingsoppdragForBehandling(behandlingId) + if (!validertUtbetalingsoppdrag.harKorrekteOpphørsdatoer) { + accumulator.add(validertUtbetalingsoppdrag) + } + accumulator + } + + return ResponseEntity.ok( + BehandlingerMedFeilIUtbetalingsoppdrag( + behandlinger = validerteUtbetalingsoppdragMedFeil.map { it.behandlingId }, + validerteUtbetalingsoppdrag = validerteUtbetalingsoppdragMedFeil, + ), + ) + } + + @PostMapping("/sendKorrigertUtbetalingsoppdragForBehandlinger") + fun sendKorrigertUtbetalingsoppdragForBehandlinger(@RequestBody behandlinger: List): ResponseEntity { + val harFeil = mutableSetOf>() + val iverksattOk = mutableSetOf() + behandlinger.forEach { behandlingId -> + try { + forvalterService.lagKorrigertUtbetalingsoppdragOgIverksettMotØkonomi(behandlingId) + iverksattOk.add(behandlingId) + } catch (e: Exception) { + secureLogger.warn("Feil ved iverksettelse mot økonomi. ${e.message}", e) + harFeil.add( + Pair( + behandlingId, + e.message ?: "Ukjent feil ved iverksettelse av oppdrag på nytt for behandling $behandlingId", + ), + ) + } + } + return ResponseEntity.ok(SendUtbetalingsoppdragPåNyttResponse(iverksattOk = iverksattOk, harFeil = harFeil)) + } + + @PostMapping("/sendKorrigertUtbetalingsoppdragForBehandling/{behandlingId}/{versjon}") + fun sendKorrigertUtbetalingsoppdragForBehandling( + @PathVariable behandlingId: Long, + @PathVariable versjon: Int, + ): ResponseEntity { + val harFeil = mutableSetOf>() + val iverksattOk = mutableSetOf() + try { + forvalterService.lagKorrigertUtbetalingsoppdragOgIverksettMotØkonomi(behandlingId, versjon) + iverksattOk.add(behandlingId) + } catch (e: Exception) { + secureLogger.warn("Feil ved iverksettelse mot økonomi. ${e.message}", e) + harFeil.add( + Pair( + behandlingId, + e.message ?: "Ukjent feil ved iverksettelse av oppdrag på nytt for behandling $behandlingId", + ), + ) + } + + return ResponseEntity.ok(SendUtbetalingsoppdragPåNyttResponse(iverksattOk = iverksattOk, harFeil = harFeil)) + } + + @PostMapping("/populer-stonad-fom-tom/{behandlingId}") + fun populerStønadFomTomForBehandling(@PathVariable behandlingId: Long): ResponseEntity { + return ResponseEntity.ok(forvalterService.oppdaterStønadFomTomForBehandling(behandlingId)) + } + + @PostMapping("/populer-stonad-fom-tom-alle/{limit}") + fun populerStønadFomTom(@PathVariable limit: Int): ResponseEntity { + behandlingsRepository.finnAktiveBehandlingerSomManglerStønadTom(limit).forEach { behandlingId -> + try { + val harOppdatertTom = forvalterService.oppdaterStønadFomTomForBehandling(behandlingId) + logger.info("oppdaterStønadFomTomForBehandling for behandlingId=$behandlingId resultat=$harOppdatertTom") + } catch (e: Exception) { + logger.warn("Fikk ikke satt stønadTom for behandling=$behandlingId", e) + } + } + + return ResponseEntity.ok("ok") + } + + @GetMapping("/finnFagsakerSomSkalAvsluttes") + fun finnFagsakerSomSkalAvsluttes(): ResponseEntity> { + return ResponseEntity.ok(fagsakRepository.finnFagsakerSomSkalAvsluttes()) + } + + @PostMapping("oppdaterLøpendeStatusPåFagsaker") + fun oppdaterLøpendeStatusPåFagsaker() { + fagsakService.oppdaterLøpendeStatusPåFagsaker() + } + + @GetMapping("/finnÅpneFagsakerMedFlereMigreringsbehandlingerOgLøpendeSakIInfotrygd") + fun finnÅpneFagsakerMedFlereMigreringsbehandlingerOgLøpendeSakIInfotrygd(): ResponseEntity>> { + val åpneFagsakerMedFlereMigreringsbehandlingerOgLøpendeSakIInfotrygd = + forvalterService.finnÅpneFagsakerMedFlereMigreringsbehandlingerOgLøpendeSakIInfotrygd() + logger.info("Følgende fagsaker har flere migreringsbehandlinger og løpende sak i Infotrygd: $åpneFagsakerMedFlereMigreringsbehandlingerOgLøpendeSakIInfotrygd") + return ResponseEntity.ok(åpneFagsakerMedFlereMigreringsbehandlingerOgLøpendeSakIInfotrygd) + } + + @GetMapping("/finnÅpneFagsakerMedFlereMigreringsbehandlinger") + fun finnÅpneFagsakerMedFlereMigreringsbehandlinger(): ResponseEntity>> { + val åpneFagsakerMedFlereMigreringsbehandlinger = + forvalterService.finnÅpneFagsakerMedFlereMigreringsbehandlinger() + logger.info("Følgende fagsaker har flere migreringsbehandlinger og løper i ba-sak: $åpneFagsakerMedFlereMigreringsbehandlinger") + return ResponseEntity.ok(åpneFagsakerMedFlereMigreringsbehandlinger) + } + + data class SendUtbetalingsoppdragPåNyttResponse( + val iverksattOk: Set, + val harFeil: Set>, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/ForvalterService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/ForvalterService.kt new file mode 100644 index 000000000..e0eec6465 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/ForvalterService.kt @@ -0,0 +1,446 @@ +package no.nav.familie.ba.sak.internal + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.UtbetalingsikkerhetFeil +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdService +import no.nav.familie.ba.sak.integrasjoner.økonomi.AndelTilkjentYtelseForIverksettingFactory +import no.nav.familie.ba.sak.integrasjoner.økonomi.AndelTilkjentYtelseForSimuleringFactory +import no.nav.familie.ba.sak.integrasjoner.økonomi.pakkInnForUtbetaling +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiKlient +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiService +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.grupperAndeler +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValideringService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.utbetalingsoppdrag +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.task.IverksettMotOppdragTask +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsperiode +import no.nav.familie.log.mdc.MDCConstants +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.data.domain.PageRequest +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.time.YearMonth + +@Service +class ForvalterService( + private val økonomiService: ØkonomiService, + private val økonomiKlient: ØkonomiKlient, + private val vedtakService: VedtakService, + private val beregningService: BeregningService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val endretUtbetalingAndelService: EndretUtbetalingAndelService, + private val stegService: StegService, + private val fagsakService: FagsakService, + private val behandlingService: BehandlingService, + private val taskRepository: TaskRepositoryWrapper, + private val autovedtakService: AutovedtakService, + private val fagsakRepository: FagsakRepository, + private val behandlingRepository: BehandlingRepository, + private val tilkjentYtelseValideringService: TilkjentYtelseValideringService, + private val arbeidsfordelingService: ArbeidsfordelingService, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + private val infotrygdService: InfotrygdService, +) { + private val logger = LoggerFactory.getLogger(ForvalterService::class.java) + + @Transactional + fun lagOgSendUtbetalingsoppdragTilØkonomiForBehandling(behandlingId: Long) { + val tilkjentYtelse = beregningService.hentTilkjentYtelseForBehandling(behandlingId) + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + + val forrigeBehandlingSendtTilØkonomi = + behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(behandling) + val erBehandlingOpprettetEtterDenneSomErSendtTilØkonomi = forrigeBehandlingSendtTilØkonomi != null && + forrigeBehandlingSendtTilØkonomi.aktivertTidspunkt.isAfter(behandling.aktivertTidspunkt) + + if (tilkjentYtelse.utbetalingsoppdrag != null) { + throw Feil("Behandling $behandlingId har allerede opprettet utbetalingsoppdrag") + } + if (erBehandlingOpprettetEtterDenneSomErSendtTilØkonomi) { + throw Feil("Det finnes en behandling opprettet etter $behandlingId som er sendt til økonomi") + } + + økonomiService.oppdaterTilkjentYtelseMedUtbetalingsoppdragOgIverksett( + vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingId), + saksbehandlerId = "VL", + andelTilkjentYtelseForUtbetalingsoppdragFactory = AndelTilkjentYtelseForIverksettingFactory(), + ) + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun kopierEndretUtbetalingFraForrigeBehandling( + sisteVedtatteBehandling: Behandling, + nestSisteVedtatteBehandling: Behandling, + ) { + endretUtbetalingAndelService.kopierEndretUtbetalingAndelFraForrigeBehandling( + behandling = sisteVedtatteBehandling, + forrigeBehandling = nestSisteVedtatteBehandling, + ) + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun kjørForenkletSatsendringFor(fagsakId: Long) { + val fagsak = fagsakService.hentPåFagsakId(fagsakId) + + val nyBehandling = stegService.håndterNyBehandling( + NyBehandling( + behandlingType = BehandlingType.REVURDERING, + behandlingÅrsak = BehandlingÅrsak.SATSENDRING, + søkersIdent = fagsak.aktør.aktivFødselsnummer(), + skalBehandlesAutomatisk = true, + fagsakId = fagsakId, + ), + ) + + val behandlingEtterVilkårsvurdering = + stegService.håndterVilkårsvurdering(nyBehandling) + + val opprettetVedtak = + autovedtakService.opprettToTrinnskontrollOgVedtaksbrevForAutomatiskBehandling( + behandlingEtterVilkårsvurdering, + ) + behandlingService.oppdaterStatusPåBehandling(nyBehandling.id, BehandlingStatus.IVERKSETTER_VEDTAK) + val task = + IverksettMotOppdragTask.opprettTask(nyBehandling, opprettetVedtak, SikkerhetContext.hentSaksbehandler()) + taskRepository.save(task) + } + + fun identifiserUtbetalingerOver100Prosent(callId: String) { + MDC.put(MDCConstants.MDC_CALL_ID, callId) + + runBlocking { + finnOgLoggUtbetalingerOver100Prosent(callId) + } + + logger.info("Ferdig med å kjøre identifiserUtbetalingerOver100Prosent") + } + + @OptIn(InternalCoroutinesApi::class) // for å få lov til å hente CancellationException + suspend fun finnOgLoggUtbetalingerOver100Prosent(callId: String) { + var slice = fagsakRepository.finnLøpendeFagsaker(PageRequest.of(0, 10000)) + val scope = CoroutineScope(Dispatchers.Default.limitedParallelism(10)) + val deffereds = mutableListOf>() + + // coroutineScope { + while (slice.pageable.isPaged) { + val sideNr = slice.number + val fagsaker = slice.get().toList() + logger.info("Starter kjøring av identifiserUtbetalingerOver100Prosent side=$sideNr") + deffereds.add( + scope.async { + MDC.put(MDCConstants.MDC_CALL_ID, callId) + sjekkChunkMedFagsakerOmDeHarUtbetalingerOver100Prosent(fagsaker) + logger.info("Avslutter kjøring av identifiserUtbetalingerOver100Prosent side=$sideNr") + }, + ) + + slice = fagsakRepository.finnLøpendeFagsaker(slice.nextPageable()) + } + deffereds.forEach { + if (it.isCancelled) { + logger.warn("Async jobb med status kansellert. Se securelog") + secureLogger.warn( + "Async jobb kansellert med: ${it.getCancellationException().message} ${ + it.getCancellationException().stackTraceToString() + }", + ) + } + + it.await() + } + + logger.info("Alle async jobber er kjørt. Totalt antall sider=${deffereds.size}") + } + + private fun sjekkChunkMedFagsakerOmDeHarUtbetalingerOver100Prosent(fagsaker: List) { + fagsaker.forEach { fagsakId -> + val sisteIverksatteBehandling = + behandlingRepository.finnSisteIverksatteBehandling(fagsakId = fagsakId) + if (sisteIverksatteBehandling != null) { + try { + tilkjentYtelseValideringService.validerAtBarnIkkeFårFlereUtbetalingerSammePeriode( + sisteIverksatteBehandling, + ) + } catch (e: UtbetalingsikkerhetFeil) { + val arbeidsfordelingService = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = sisteIverksatteBehandling.id) + secureLogger.warn("Over 100% utbetaling for fagsak=$fagsakId, enhet=${arbeidsfordelingService.behandlendeEnhetId}, melding=${e.message}") + } + } else { + logger.warn("Skipper sjekk 100% for fagsak $fagsakId pga manglende sisteIverksettBehandling") + } + } + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun lagKorrigertUtbetalingsoppdragOgIverksettMotØkonomi(behandlingId: Long, versjon: Int = 1) { + val tilkjentYtelse = tilkjentYtelseRepository.findByBehandling(behandlingId) + if (tilkjentYtelse.behandling.aktiv == false) throw Exception("Behandling $behandlingId er ikke den aktive behandlingen på fagsaken") + val validertUtbetalingsoppdrag = validerOpphørsdatoIUtbetalingsoppdrag(tilkjentYtelse) + if (!validertUtbetalingsoppdrag.harKorrekteOpphørsdatoer && validertUtbetalingsoppdrag.nyttUtbetalingsoppdrag != null && detFinnesUtbetalingsperioderMedFeilOgTilhørendeKorrigerteUtbetalingsperioder( + validertUtbetalingsoppdrag, + ) + ) { + secureLogger.info("Iverksetter korrigert utbetalingsoppdrag ${validertUtbetalingsoppdrag.nyttUtbetalingsoppdrag} for behandling $behandlingId") + økonomiKlient.iverksettOppdragPåNytt(validertUtbetalingsoppdrag.nyttUtbetalingsoppdrag, versjon) + secureLogger.info("Oppdaterer TilkjentYtelse med korrigert utbetalingsoppdrag ${validertUtbetalingsoppdrag.nyttUtbetalingsoppdrag} for behandling $behandlingId") + beregningService.oppdaterTilkjentYtelseMedUtbetalingsoppdrag( + tilkjentYtelse.behandling, + validertUtbetalingsoppdrag.nyttUtbetalingsoppdrag, + ) + } else { + throw Exception("Nytt utbetalingsoppdrag ikke sendt for behandling $behandlingId. HarKorrekteOpphørsdatoer: ${validertUtbetalingsoppdrag.harKorrekteOpphørsdatoer}, Nytt utbetalingsoppdrag: ${validertUtbetalingsoppdrag.nyttUtbetalingsoppdrag}, ") + } + } + + private fun detFinnesUtbetalingsperioderMedFeilOgTilhørendeKorrigerteUtbetalingsperioder(validertUtbetalingsoppdrag: ValidertUtbetalingsoppdrag): Boolean { + if ( + !validertUtbetalingsoppdrag.korrigerteUtbetalingsperioder.isNullOrEmpty() && !validertUtbetalingsoppdrag.utbetalingsperioderMedFeilOpphørsdato.isNullOrEmpty() && + // De korrigerte utbetalingsperiodene matcher utbetalingsperiodene med feil + validertUtbetalingsoppdrag.korrigerteUtbetalingsperioder.size == validertUtbetalingsoppdrag.utbetalingsperioderMedFeilOpphørsdato.size && + validertUtbetalingsoppdrag.utbetalingsperioderMedFeilOpphørsdato.all { utbetalingsperiodeMedFeil -> + validertUtbetalingsoppdrag.korrigerteUtbetalingsperioder.any { korrigertUtbetalingsperiode -> + korrigertUtbetalingsperiode.periodeId == utbetalingsperiodeMedFeil.periodeId + } + } + + ) { + return true + } else { + throw Exception("Korrigerte utbetalingsperioder matcher ikke utbetalingsperiodene med feil. UtbetalingsperioderMedFeil: ${validertUtbetalingsoppdrag.utbetalingsperioderMedFeilOpphørsdato}, KorrigerteUtbetalingsperioder: ${validertUtbetalingsoppdrag.korrigerteUtbetalingsperioder}") + } + } + + fun validerOpphørsdatoIUtbetalingsoppdragForBehandling(behandlingId: Long): ValidertUtbetalingsoppdrag { + val tilkjentYtelse = tilkjentYtelseRepository.findByBehandling(behandlingId) + return validerOpphørsdatoIUtbetalingsoppdrag(tilkjentYtelse) + } + + fun identifiserPåvirkedeBehandlingerOgValiderOpphørsdatoIUtbetalingsoppdrag(): BehandlingerMedFeilIUtbetalingsoppdrag { + val tilkjenteYtelserMedOpphørSomKanVæreFeil = + tilkjentYtelseRepository.findTilkjentYtelseMedFeilUtbetalingsoppdrag() + logger.info("Behandlinger som potensielt har feil: ${tilkjenteYtelserMedOpphørSomKanVæreFeil.map { it.behandling.id }}") + + val validerteUtbetalingsoppdragMedFeil: Set = + tilkjenteYtelserMedOpphørSomKanVæreFeil + .map { validerOpphørsdatoIUtbetalingsoppdrag(it) } + .filter { !it.harKorrekteOpphørsdatoer }.toSet() + + return BehandlingerMedFeilIUtbetalingsoppdrag( + behandlinger = validerteUtbetalingsoppdragMedFeil.map { it.behandlingId }, + validerteUtbetalingsoppdrag = validerteUtbetalingsoppdragMedFeil, + ) + } + + private fun validerOpphørsdatoIUtbetalingsoppdrag(tilkjentYtelse: TilkjentYtelse): ValidertUtbetalingsoppdrag { + val utbetalingsoppdrag = tilkjentYtelse.utbetalingsoppdrag() ?: return ValidertUtbetalingsoppdrag( + harKorrekteOpphørsdatoer = true, + behandlingId = tilkjentYtelse.behandling.id, + ) + logger.info("Sjekker behandling for korrekt opphørsdato ${tilkjentYtelse.behandling.id}") + try { + val grupperteNyeAndeler = grupperAndeler( + beregningService.hentAndelerTilkjentYtelseMedUtbetalingerForBehandling(behandlingId = tilkjentYtelse.behandling.id) + .pakkInnForUtbetaling(AndelTilkjentYtelseForSimuleringFactory()), + ) + + val forrigeIverksatteBehandling = + behandlingRepository.finnIverksatteBehandlinger(tilkjentYtelse.behandling.fagsak.id) + .filter { it.id != tilkjentYtelse.behandling.id && it.aktivertTidspunkt < tilkjentYtelse.behandling.aktivertTidspunkt } + .maxByOrNull { it.aktivertTidspunkt }!! + + val grupperteForrigeAndeler = grupperAndeler( + beregningService.hentAndelerTilkjentYtelseMedUtbetalingerForBehandling(behandlingId = forrigeIverksatteBehandling.id) + .pakkInnForUtbetaling(AndelTilkjentYtelseForSimuleringFactory()), + ) + + val sisteBeståendeAndelPerKjede = + ØkonomiUtils.sisteBeståendeAndelPerKjede(grupperteForrigeAndeler, grupperteNyeAndeler) + + val endretMigreringsdato = beregnOmMigreringsDatoErEndret( + tilkjentYtelse.behandling, + grupperteForrigeAndeler.values.flatten().minByOrNull { it.stønadFom }?.stønadFom, + ) + + // Finner andeler som skal opphøres slik vi gjorde før + val andelerTilOpphør = grupperteForrigeAndeler + .mapValues { (person, forrigeAndeler) -> + forrigeAndeler.filter { + sisteBeståendeAndelPerKjede[person] == null || + it.stønadFom > sisteBeståendeAndelPerKjede[person]!!.stønadTom + } + } + .filter { (_, andelerSomOpphøres) -> andelerSomOpphøres.isNotEmpty() } + .mapValues { andelForKjede -> andelForKjede.value.sortedBy { it.stønadFom } } + .map { (_, kjedeEtterFørsteEndring) -> + kjedeEtterFørsteEndring.last() to ( + endretMigreringsdato + ?: kjedeEtterFørsteEndring.minOf { it.stønadFom } + ) + } + + secureLogger.info("Andeler som som skal opphøres: ${andelerTilOpphør.map { "PeriodeId: ${it.first.periodeOffset} ForrigePeriodeId: ${it.first.forrigePeriodeOffset} Opphørsdato: ${it.second}" }} for behandling ${tilkjentYtelse.behandling.id}") + val utbetalingsperioderMedOpphør = utbetalingsoppdrag.utbetalingsperiode.filter { it.opphør != null } + secureLogger.info("Utbetalingsperioder med opphør: $utbetalingsperioderMedOpphør for behandling ${tilkjentYtelse.behandling.id}") + + val utbetalingsperioderMedFeilOpphørsdato = mutableListOf() + val korrigerteUtbetalingsperioder = mutableListOf() + + // Finner ut hvilken opphørsAndel som tilhører hvilken utbetalingsperiodeMedOpphør + for (periodeMedOpphør in utbetalingsperioderMedOpphør) { + val andelerTilPersonMedOpphør = + andelerTilOpphør.filter { andelForPerson -> andelForPerson.first.periodeOffset == periodeMedOpphør.periodeId } + if (andelerTilPersonMedOpphør.size != 1) { + secureLogger.info("Mer enn 1 eller ingen andeler med samme periodeOffsett som opphørsperioden $periodeMedOpphør for behandling ${tilkjentYtelse.behandling.id}") + utbetalingsperioderMedFeilOpphørsdato.add(periodeMedOpphør) + // Nullstiller korrigerteUtbetalingsperioder slik at validering før iverksettelse feiler. + korrigerteUtbetalingsperioder.clear() + break + } else { + secureLogger.info("Andel fra forrige med korrekt opphørsdato: ${andelerTilPersonMedOpphør.first().second.førsteDagIInneværendeMåned()}. Opphørsperiode sendt til økonomi med opphørsdato: ${periodeMedOpphør.opphør!!.opphørDatoFom} for behandling ${tilkjentYtelse.behandling.id}") + if (andelerTilPersonMedOpphør.first().second + .førsteDagIInneværendeMåned() != periodeMedOpphør.opphør!!.opphørDatoFom + ) { + utbetalingsperioderMedFeilOpphørsdato.add(periodeMedOpphør) + korrigerteUtbetalingsperioder.add( + periodeMedOpphør.copy( + opphør = periodeMedOpphør.opphør!!.copy( + opphørDatoFom = andelerTilPersonMedOpphør.first().second + .førsteDagIInneværendeMåned(), + ), + ), + ) + } + } + } + + if (utbetalingsperioderMedFeilOpphørsdato.isEmpty()) { + return ValidertUtbetalingsoppdrag( + harKorrekteOpphørsdatoer = true, + behandlingId = tilkjentYtelse.behandling.id, + ) + } + return ValidertUtbetalingsoppdrag( + harKorrekteOpphørsdatoer = false, + behandlingId = tilkjentYtelse.behandling.id, + utbetalingsperioderMedFeilOpphørsdato = utbetalingsperioderMedFeilOpphørsdato, + korrigerteUtbetalingsperioder = korrigerteUtbetalingsperioder, + gammeltUtbetalingsoppdrag = utbetalingsoppdrag, + nyttUtbetalingsoppdrag = utbetalingsoppdrag.copy( + avstemmingTidspunkt = LocalDateTime.now(), + utbetalingsperiode = korrigerteUtbetalingsperioder + .map { it.copy(erEndringPåEksisterendePeriode = true) }, + ), + ) + } catch (e: Exception) { + secureLogger.warn( + "opphørsdatoErKorrekt kaster feil: ${e.message} for behandling ${tilkjentYtelse.behandling.id}", + e, + ) + return ValidertUtbetalingsoppdrag( + harKorrekteOpphørsdatoer = false, + behandlingId = tilkjentYtelse.behandling.id, + ) + } + } + + private fun beregnOmMigreringsDatoErEndret(behandling: Behandling, forrigeTilstandFraDato: YearMonth?): YearMonth? { + val erMigrertSak = + behandlingHentOgPersisterService.hentBehandlinger(behandling.fagsak.id) + .any { it.type == BehandlingType.MIGRERING_FRA_INFOTRYGD } + + if (!erMigrertSak) { + return null + } + + val nyttTilstandFraDato = behandlingService.hentMigreringsdatoPåFagsak(fagsakId = behandling.fagsak.id) + ?.toYearMonth() + ?.plusMonths(1) + + return if (forrigeTilstandFraDato != null && + nyttTilstandFraDato != null && + forrigeTilstandFraDato.isAfter(nyttTilstandFraDato) + ) { + nyttTilstandFraDato + } else { + null + } + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun oppdaterStønadFomTomForBehandling(behandlingId: Long): Boolean { + tilkjentYtelseRepository.findByBehandling(behandlingId).apply { + if (this.stønadFom == null && this.stønadTom == null && this.utbetalingsoppdrag == null && this.andelerTilkjentYtelse.isNotEmpty()) { + this.stønadTom = this.andelerTilkjentYtelse.maxOfOrNull { it.stønadTom } + this.stønadFom = this.andelerTilkjentYtelse.minOfOrNull { it.stønadFom } + tilkjentYtelseRepository.save(this) + return true + } else if (this.stønadFom == null && this.stønadTom == null && this.utbetalingsoppdrag == null && this.andelerTilkjentYtelse.isEmpty()) { + logger.info("Skipper oppdatering av tilkjent ytelse for behandlingId=$behandlingId fordi aty er tom, så får ikke satt tom/fom") + return false + } + } + return false + } + + fun finnÅpneFagsakerMedFlereMigreringsbehandlingerOgLøpendeSakIInfotrygd(): List> { + val løpendeFagsakerMedFlereMigreringsbehandlinger = + fagsakRepository.finnFagsakerMedFlereMigreringsbehandlinger() + return løpendeFagsakerMedFlereMigreringsbehandlinger.filter { infotrygdService.harLøpendeSakIInfotrygd(listOf(it.aktør.aktivFødselsnummer())) } + .map { Pair(it.id, it.aktør.aktivFødselsnummer()) } + } + + fun finnÅpneFagsakerMedFlereMigreringsbehandlinger(): List> { + return fagsakRepository.finnFagsakerMedFlereMigreringsbehandlinger() + .map { Pair(it.id, it.aktør.aktivFødselsnummer()) } + } +} + +data class ValidertUtbetalingsoppdrag( + val harKorrekteOpphørsdatoer: Boolean, + val behandlingId: Long, + val utbetalingsperioderMedFeilOpphørsdato: List? = null, + val korrigerteUtbetalingsperioder: List? = null, + val gammeltUtbetalingsoppdrag: Utbetalingsoppdrag? = null, + val nyttUtbetalingsoppdrag: Utbetalingsoppdrag? = null, +) + +data class BehandlingerMedFeilIUtbetalingsoppdrag( + val behandlinger: List, + val validerteUtbetalingsoppdrag: Set, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/PreprodController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/PreprodController.kt new file mode 100644 index 000000000..195a740e8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/PreprodController.kt @@ -0,0 +1,43 @@ +package no.nav.familie.ba.sak.internal + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.config.featureToggle.miljø.Profil +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.core.env.Environment +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/preprod") +@ProtectedWithClaims(issuer = "azuread") +class PreprodController( + private val testVerktøyService: TestVerktøyService, + private val tilgangService: TilgangService, + private val environment: Environment, +) { + + @PutMapping(path = ["/{behandlingId}/fyll-ut-vilkarsvurdering"]) + fun settFomPåTommeVilkårTilFødselsdato(@PathVariable behandlingId: Long): ResponseEntity> { + val erProd = environment.activeProfiles.any { it == Profil.Prod.navn.trim() } + val erDevPostgresPreprod = environment.activeProfiles.any { it == Profil.DevPostgresPreprod.navn.trim() } + val erPreprod = environment.activeProfiles.any { it == Profil.Preprod.navn.trim() } + + if (erProd) { + throw Feil("Skal ikke være tilgjengelig i prod") + } else if (!(erDevPostgresPreprod || erPreprod)) { + throw Feil("Skal bare være tilgjengelig i for preprod eller lokalt") + } + + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.CREATE) + + testVerktøyService.oppdaterVilkårUtenFomTilFødselsdato(behandlingId) + + return ResponseEntity.ok(Ressurs.success("Oppdaterte vilkårsvurdering")) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/TestVerkt\303\270yController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/TestVerkt\303\270yController.kt" new file mode 100644 index 000000000..b3a46cbbc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/TestVerkt\303\270yController.kt" @@ -0,0 +1,180 @@ +package no.nav.familie.ba.sak.internal + +import no.nav.familie.ba.sak.common.EnvService +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakStegService +import no.nav.familie.ba.sak.kjerne.autovedtak.omregning.AutobrevScheduler +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.StartSatsendring +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.simulering.SimuleringService +import no.nav.familie.ba.sak.kjerne.simulering.domene.ØkonomiSimuleringMottaker +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.ba.sak.task.BehandleFødselshendelseTask +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.ba.sak.task.TaBehandlingerEtterVentefristAvVentTask +import no.nav.familie.ba.sak.task.dto.BehandleFødselshendelseTaskDTO +import no.nav.familie.ba.sak.task.internkonsistensavstemming.OpprettInternKonsistensavstemmingTaskerTask +import no.nav.familie.kontrakter.felles.PersonIdent +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.security.token.support.core.api.Unprotected +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.net.URI + +@RestController +@RequestMapping(value = ["/internal", "/testverktoy"]) +class TestVerktøyController( + private val scheduler: AutobrevScheduler, + private val personidentService: PersonidentService, + private val envService: EnvService, + private val autovedtakStegService: AutovedtakStegService, + private val taskRepository: TaskRepositoryWrapper, + private val tilgangService: TilgangService, + private val simuleringService: SimuleringService, + private val opprettTaskService: OpprettTaskService, + private val taskService: TaskService, + private val startSatsendring: StartSatsendring, + private val testVerktøyService: TestVerktøyService, + private val behandlingRepository: BehandlingRepository, +) { + + @GetMapping(path = ["/autobrev"]) + @Unprotected + fun kjørSchedulerForAutobrev(): ResponseEntity> { + return if (envService.erPreprod() || envService.erDev()) { + scheduler.opprettTask() + ResponseEntity.ok(Ressurs.success("Laget task.")) + } else { + ResponseEntity.ok(Ressurs.success(ENDEPUNKTET_GJØR_IKKE_NOE_I_PROD_MELDING)) + } + } + + @GetMapping(path = ["/test-satsendring/{fagsakId}"]) + @Unprotected + fun utførSatsendringPåFagsak(@PathVariable fagsakId: Long): ResponseEntity> { + return if (envService.erPreprod() || envService.erDev()) { + opprettTaskService.opprettSatsendringTask(fagsakId, startSatsendring.hentAktivSatsendringstidspunkt()) + ResponseEntity.ok(Ressurs.success("Trigget satsendring for fagsak $fagsakId")) + } else { + ResponseEntity.ok(Ressurs.success(ENDEPUNKTET_GJØR_IKKE_NOE_I_PROD_MELDING)) + } + } + + @PostMapping(path = ["/vedtak-om-overgangsstønad"]) + @Unprotected + fun mottaHendelseOmVedtakOmOvergangsstønad(@RequestBody personIdent: PersonIdent): ResponseEntity> { + return if (envService.erPreprod() || envService.erDev()) { + val aktør = personidentService.hentAktør(personIdent.ident) + val melding = autovedtakStegService.kjørBehandlingSmåbarnstillegg( + mottakersAktør = aktør, + aktør = aktør, + ) + ResponseEntity.ok(Ressurs.success(melding)) + } else { + ResponseEntity.ok(Ressurs.success(ENDEPUNKTET_GJØR_IKKE_NOE_I_PROD_MELDING)) + } + } + + @PostMapping(path = ["/foedselshendelse"]) + @Unprotected + fun mottaFødselshendelse(@RequestBody nyBehandlingHendelse: NyBehandlingHendelse): ResponseEntity> { + return if (envService.erPreprod() || envService.erDev()) { + val task = BehandleFødselshendelseTask.opprettTask(BehandleFødselshendelseTaskDTO(nyBehandlingHendelse)) + taskRepository.save(task) + ResponseEntity.ok(Ressurs.success("Task for behandling av fødselshendelse på ${nyBehandlingHendelse.morsIdent} er opprettet")) + } else { + ResponseEntity.ok(Ressurs.success(ENDEPUNKTET_GJØR_IKKE_NOE_I_PROD_MELDING)) + } + } + + @GetMapping(path = ["/kjor-intern-konsistensavstemming/{maksAntallTasker}"]) + @Unprotected + fun kjørInternKonsistensavstemming(@PathVariable maksAntallTasker: Int): ResponseEntity> { + if (!envService.erPreprod() && !envService.erDev()) { + return ResponseEntity.ok(Ressurs.success(ENDEPUNKTET_GJØR_IKKE_NOE_I_PROD_MELDING)) + } + + taskService.save(OpprettInternKonsistensavstemmingTaskerTask.opprettTask(maksAntallTasker)) + return ResponseEntity.ok(Ressurs.success("Kjørt ok")) + } + + @GetMapping(path = ["/ta-behandlinger-etter-ventefrist-av-vent"]) + @Unprotected + fun taBehandlingerEtterVentefristAvVent(): ResponseEntity> { + return if (envService.erPreprod() || envService.erDev()) { + val taBehandlingerEtterVentefristAvVentTask = + Task(type = TaBehandlingerEtterVentefristAvVentTask.TASK_STEP_TYPE, payload = "") + taskRepository.save(taBehandlingerEtterVentefristAvVentTask) + ResponseEntity.ok(Ressurs.success("Task for å ta behandlinger av vent etter at fristen har gått ut er opprettet")) + } else { + ResponseEntity.ok(Ressurs.success(ENDEPUNKTET_GJØR_IKKE_NOE_I_PROD_MELDING)) + } + } + + @GetMapping(path = ["/hent-simulering-pa-behandling/{behandlingId}"]) + fun hentSimuleringPåBehandling(@PathVariable behandlingId: Long): List<ØkonomiSimuleringMottaker> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + + return simuleringService.hentSimuleringPåBehandling(behandlingId) + } + + @GetMapping(path = ["/behandling/{behandlingId}/begrunnelsetest"]) + @Unprotected + fun hentBegrunnelsetestPåBehandling(@PathVariable behandlingId: Long): String { + return if (envService.erPreprod() || envService.erDev()) { + testVerktøyService.hentBegrunnelsetest(behandlingId) + .replace("\n", System.lineSeparator()) + } else { + throw FunksjonellFeil( + httpStatus = HttpStatus.BAD_REQUEST, + melding = ENDEPUNKTET_GJØR_IKKE_NOE_I_PROD_MELDING, + ) + } + } + + @GetMapping(path = ["/behandling/{behandlingId}/vedtaksperiodertest"]) + @Unprotected + fun hentVedtaksperioderTestPåBehandling(@PathVariable behandlingId: Long): String { + return if (envService.erPreprod() || envService.erDev()) { + testVerktøyService.hentVedtaksperioderTest(behandlingId) + .replace("\n", System.lineSeparator()) + } else { + throw FunksjonellFeil( + httpStatus = HttpStatus.BAD_REQUEST, + melding = ENDEPUNKTET_GJØR_IKKE_NOE_I_PROD_MELDING, + ) + } + } + + @GetMapping("/redirect/behandling/{behandlingId}") + @Unprotected + fun redirectTilBarnetrygd(@PathVariable behandlingId: Long): ResponseEntity { + val hostname = if (envService.erDev()) { + "http://localhost:8000" + } else if (envService.erPreprod()) { + "https://barnetrygd.intern.dev.nav.no" + } else if (envService.erProd()) { + "https://barnetrygd.intern.nav.no" + } else { + error("Klarer ikke å utlede miljø for redirect til fagsak") + } + val behandling = behandlingRepository.finnBehandling(behandlingId) + return ResponseEntity.status(302).location(URI.create("$hostname/fagsak/${behandling.fagsak.id}/$behandlingId/")).build() + } + + companion object { + const val ENDEPUNKTET_GJØR_IKKE_NOE_I_PROD_MELDING = "Endepunktet gjør ingenting i prod." + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/TestVerkt\303\270yService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/TestVerkt\303\270yService.kt" new file mode 100644 index 000000000..fe76f09b0 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/TestVerkt\303\270yService.kt" @@ -0,0 +1,133 @@ +package no.nav.familie.ba.sak.internal + +import no.nav.familie.ba.sak.internal.vedtak.begrunnelser.lagGyldigeBegrunnelserTest +import no.nav.familie.ba.sak.internal.vedtak.vedtaksperioder.lagVedtaksperioderTest +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndelRepository +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.KompetanseRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class TestVerktøyService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val vilkårService: VilkårService, + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val endretUtbetalingRepository: EndretUtbetalingAndelRepository, + private val vedtaksperiodeHentOgPersisterService: VedtaksperiodeHentOgPersisterService, + private val vedtakService: VedtakService, + private val kompetanseRepository: KompetanseRepository, +) { + + @Transactional + fun oppdaterVilkårUtenFomTilFødselsdato(behandlingId: Long) { + val vilkårsvurdering = vilkårService.hentVilkårsvurdering(behandlingId) + + val persongrunnlag = personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId) + + vilkårsvurdering?.personResultater?.forEach { personResultat -> + personResultat.vilkårResultater.forEach { vilkårResultat -> + if (vilkårResultat.resultat == Resultat.IKKE_VURDERT) { + vilkårResultat.periodeFom = + persongrunnlag?.personer?.find { it.aktør == personResultat.aktør }?.fødselsdato + vilkårResultat.resultat = Resultat.OPPFYLT + vilkårResultat.begrunnelse = "Opprettet automatisk fra \"Fyll ut vilkårsvurdering\"-knappen" + } + } + } + } + + fun hentBegrunnelsetest(behandlingId: Long): String { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + val forrigeBehandling = behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling) + + val persongrunnlag: PersonopplysningGrunnlag = + personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId)!! + val persongrunnlagForrigeBehandling = + forrigeBehandling?.let { personopplysningGrunnlagRepository.findByBehandlingAndAktiv(it.id)!! } + + val personResultater = vilkårService.hentVilkårsvurderingThrows(behandlingId).personResultater + val personResultaterForrigeBehandling = + forrigeBehandling?.let { vilkårService.hentVilkårsvurderingThrows(it.id).personResultater } + + val andeler = andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId) + val andelerForrigeBehandling = + forrigeBehandling?.let { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(it.id) } + + val endredeUtbetalinger = endretUtbetalingRepository.findByBehandlingId(behandlingId) + val endredeUtbetalingerForrigeBehandling = + forrigeBehandling?.let { endretUtbetalingRepository.findByBehandlingId(it.id) } + + val kompetanse = kompetanseRepository.finnFraBehandlingId(behandlingId) + val kompetanseForrigeBehandling = + forrigeBehandling?.let { kompetanseRepository.finnFraBehandlingId(it.id) } + + val vedtaksperioder = vedtaksperiodeHentOgPersisterService.finnVedtaksperioderFor( + vedtakService.hentAktivForBehandlingThrows(behandlingId).id, + ) + + return lagGyldigeBegrunnelserTest( + behandling = behandling, + forrigeBehandling = forrigeBehandling, + persongrunnlag = persongrunnlag, + persongrunnlagForrigeBehandling = persongrunnlagForrigeBehandling, + personResultater = personResultater, + personResultaterForrigeBehandling = personResultaterForrigeBehandling, + andeler = andeler, + andelerForrigeBehandling = andelerForrigeBehandling, + vedtaksperioder = vedtaksperioder, + endredeUtbetalinger = endredeUtbetalinger, + endredeUtbetalingerForrigeBehandling = endredeUtbetalingerForrigeBehandling, + kompetanse = kompetanse, + kompetanseForrigeBehandling = kompetanseForrigeBehandling, + ) + } + + fun hentVedtaksperioderTest(behandlingId: Long): String { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + val forrigeBehandling = behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling) + val persongrunnlag: PersonopplysningGrunnlag = + personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId)!! + val persongrunnlagForrigeBehandling = + forrigeBehandling?.let { personopplysningGrunnlagRepository.findByBehandlingAndAktiv(it.id)!! } + val personResultater = vilkårService.hentVilkårsvurderingThrows(behandlingId).personResultater + val personResultaterForrigeBehandling = + forrigeBehandling?.let { vilkårService.hentVilkårsvurderingThrows(it.id).personResultater } + val andeler = andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId) + val andelerForrigeBehandling = + forrigeBehandling?.let { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(it.id) } + val endredeUtbetalinger = endretUtbetalingRepository.findByBehandlingId(behandlingId) + val endredeUtbetalingerForrigeBehandling = + forrigeBehandling?.let { endretUtbetalingRepository.findByBehandlingId(it.id) } + val kompetanse = kompetanseRepository.finnFraBehandlingId(behandlingId) + val kompetanseForrigeBehandling = + forrigeBehandling?.let { kompetanseRepository.finnFraBehandlingId(it.id) } + val vedtaksperioder = vedtaksperiodeHentOgPersisterService.finnVedtaksperioderFor( + vedtakService.hentAktivForBehandlingThrows(behandlingId).id, + ) + + return lagVedtaksperioderTest( + behandling = behandling, + forrigeBehandling = forrigeBehandling, + persongrunnlag = persongrunnlag, + persongrunnlagForrigeBehandling = persongrunnlagForrigeBehandling, + personResultater = personResultater, + personResultaterForrigeBehandling = personResultaterForrigeBehandling, + andeler = andeler, + andelerForrigeBehandling = andelerForrigeBehandling, + vedtaksperioder = vedtaksperioder, + endredeUtbetalinger = endredeUtbetalinger, + endredeUtbetalingerForrigeBehandling = endredeUtbetalingerForrigeBehandling, + kompetanse = kompetanse, + kompetanseForrigeBehandling = kompetanseForrigeBehandling, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/vedtak/begrunnelser/LagGyldigeBegrunnelserTestUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/vedtak/begrunnelser/LagGyldigeBegrunnelserTestUtil.kt new file mode 100644 index 000000000..d446ae683 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/vedtak/begrunnelser/LagGyldigeBegrunnelserTestUtil.kt @@ -0,0 +1,263 @@ +package no.nav.familie.ba.sak.internal.vedtak.begrunnelser + +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.tilddMMyyyy +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.IUtfyltEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.tilIEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.UtfyltKompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.tilIKompetanse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import org.apache.commons.lang3.RandomStringUtils +import java.time.LocalDate + +fun lagGyldigeBegrunnelserTest( + behandling: Behandling, + forrigeBehandling: Behandling?, + persongrunnlag: PersonopplysningGrunnlag, + persongrunnlagForrigeBehandling: PersonopplysningGrunnlag?, + personResultater: Set, + personResultaterForrigeBehandling: Set?, + andeler: List, + andelerForrigeBehandling: List?, + endredeUtbetalinger: List, + endredeUtbetalingerForrigeBehandling: List?, + vedtaksperioder: List, + kompetanse: Collection, + kompetanseForrigeBehandling: Collection?, +) = """ +
+# language: no
+# encoding: UTF-8
+
+Egenskap: Plassholdertekst for egenskap - ${RandomStringUtils.randomAlphanumeric(10)}
+
+  Bakgrunn:""" +
+    hentTekstForFagsak(behandling) +
+    hentTekstForBehandlinger(behandling, forrigeBehandling) +
+    hentTekstForPersongrunnlag(persongrunnlag, persongrunnlagForrigeBehandling) +
+    """
+      
+  Scenario: Plassholdertekst for scenario - ${RandomStringUtils.randomAlphanumeric(10)}
+    Og følgende dagens dato ${LocalDate.now().tilddMMyyyy()}""" +
+    lagPersonresultaterTekst(forrigeBehandling) +
+    lagPersonresultaterTekst(behandling) +
+    hentTekstForVilkårresultater(personResultaterForrigeBehandling, forrigeBehandling?.id) +
+    hentTekstForVilkårresultater(personResultater, behandling.id) +
+    hentTekstForTilkjentYtelse(andeler, andelerForrigeBehandling) +
+    hentTekstForEndretUtbetaling(endredeUtbetalinger, endredeUtbetalingerForrigeBehandling) +
+    hentTekstForKompetanse(kompetanse, kompetanseForrigeBehandling) + """
+    
+    Når begrunnelsetekster genereres for behandling ${behandling.id}""" +
+    hentTekstForVedtaksperioder(vedtaksperioder) + """
+
+ """ + +private fun lagPersonresultaterTekst(behandling: Behandling?) = behandling?.let { + """ + Og lag personresultater for begrunnelse for behandling ${it.id}""" +} ?: "" + +fun hentTekstForFagsak(behandling: Behandling) = + """ + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | ${behandling.fagsak.id} | ${behandling.fagsak.type} |""" + +fun hentTekstForBehandlinger(behandling: Behandling, forrigeBehandling: Behandling?) = + """ + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak |${ + forrigeBehandling?.let { + """ + | ${it.id} | ${it.fagsak.id} | | ${it.resultat} | ${it.opprettetÅrsak} |""" + } ?: "" + } + | ${behandling.id} | ${behandling.fagsak.id} | ${forrigeBehandling?.id ?: ""} |${behandling.resultat} | ${behandling.opprettetÅrsak} |""" + +fun hentTekstForPersongrunnlag( + persongrunnlag: PersonopplysningGrunnlag, + persongrunnlagForrigeBehandling: PersonopplysningGrunnlag?, +) = + """ + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato |""" + + hentPersongrunnlagRader(persongrunnlagForrigeBehandling) + + hentPersongrunnlagRader(persongrunnlag) + +private fun hentPersongrunnlagRader(persongrunnlag: PersonopplysningGrunnlag?): String = + persongrunnlag?.personer?.joinToString("") { + """ + | ${persongrunnlag.behandlingId} |${it.aktør.aktørId}|${it.type}|${it.fødselsdato.tilddMMyyyy()}|""" + } ?: "" + +fun hentTekstForVilkårresultater( + personResultater: Set?, + behandlingId: Long?, +): String { + if (personResultater == null || behandlingId == null) { + return "" + } + + return """ + + Og legg til nye vilkårresultater for begrunnelse for behandling $behandlingId + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag |""" + + tilVilkårResultatRader(personResultater) +} + +data class VilkårResultatRad( + val aktørId: String, + val utdypendeVilkårsvurderinger: Set, + val fom: LocalDate?, + val tom: LocalDate?, + val resultat: Resultat, + val erEksplisittAvslagPåSøknad: Boolean?, +) + +private fun tilVilkårResultatRader(personResultater: Set?) = + personResultater?.joinToString("\n") { personResultat -> + personResultat.vilkårResultater + .sortedBy { it.periodeFom } + .groupBy { + VilkårResultatRad( + personResultat.aktør.aktørId, + it.utdypendeVilkårsvurderinger.toSet(), + it.periodeFom, + it.periodeTom, + it.resultat, + it.erEksplisittAvslagPåSøknad, + ) + }.toList().joinToString("") { (vilkårResultatRad, vilkårResultater) -> + """ + | ${vilkårResultatRad.aktørId} |${vilkårResultater.map { it.vilkårType }.joinToString(",")}|${ + vilkårResultatRad.utdypendeVilkårsvurderinger.joinToString(",") + }|${vilkårResultatRad.fom?.tilddMMyyyy() ?: ""}|${vilkårResultatRad.tom?.tilddMMyyyy() ?: ""}| ${vilkårResultatRad.resultat} | ${if (vilkårResultatRad.erEksplisittAvslagPåSøknad == true) "Ja" else "Nei"} |""" + } + } ?: "" + +fun hentTekstForTilkjentYtelse( + andeler: List, + andelerForrigeBehandling: List?, +) = + """ + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | """ + + hentAndelRader(andelerForrigeBehandling) + + hentAndelRader(andeler) + +private fun hentAndelRader(andeler: List?): String = andeler + ?.sortedWith(compareBy({ it.aktør.aktivFødselsnummer() }, { it.stønadFom }, { it.stønadTom })) + ?.joinToString("") { + """ + | ${it.aktør.aktørId} |${it.behandlingId}|${ + it.stønadFom.førsteDagIInneværendeMåned().tilddMMyyyy() + }|${ + it.stønadTom.sisteDagIInneværendeMåned().tilddMMyyyy() + }|${it.kalkulertUtbetalingsbeløp}| ${it.type} | ${it.prosent} | ${it.sats} | """ + } ?: "" + +fun hentTekstForEndretUtbetaling( + endredeUtbetalinger: List, + endredeUtbetalingerForrigeBehandling: List?, +): String { + val rader = hentEndretUtbetalingRader(endredeUtbetalingerForrigeBehandling) + + hentEndretUtbetalingRader(endredeUtbetalinger) + + return if (rader.isEmpty()) { + "" + } else { + """ + + Og med endrede utbetalinger for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent |""" + + hentEndretUtbetalingRader(endredeUtbetalingerForrigeBehandling) + + hentEndretUtbetalingRader(endredeUtbetalinger) + } +} + +private fun hentEndretUtbetalingRader(endredeUtbetalinger: List?): String = + endredeUtbetalinger + ?.map { it.tilIEndretUtbetalingAndel() } + ?.filterIsInstance() + ?.joinToString("") { + """ + | ${it.person.aktør.aktørId} |${it.behandlingId}|${ + it.fom.førsteDagIInneværendeMåned().tilddMMyyyy() + }|${ + it.tom.sisteDagIInneværendeMåned().tilddMMyyyy() + }|${it.årsak} | ${it.prosent} |""" + } ?: "" + +fun hentTekstForKompetanse( + kompetanse: Collection, + kompetanseForrigeBehandling: Collection?, +): String { + val rader = hentKompetanseRader(kompetanseForrigeBehandling) + + hentKompetanseRader(kompetanse) + + return if (rader.isEmpty()) { + "" + } else { + """ + + Og med kompetanser for begrunnelse + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland |""" + + rader + } +} + +private fun hentKompetanseRader(kompetanser: Collection?): String = + kompetanser + ?.map { it.tilIKompetanse() } + ?.filterIsInstance() + ?.joinToString("") { kompetanse -> + """ + | ${ + kompetanse.barnAktører.joinToString(", ") { it.aktørId } + } |${ + kompetanse.fom.førsteDagIInneværendeMåned().tilddMMyyyy() + }|${ + kompetanse.tom?.sisteDagIInneværendeMåned()?.tilddMMyyyy() ?: "" + }|${ + kompetanse.resultat + }|${ + kompetanse.behandlingId + }|${ + kompetanse.søkersAktivitet + }|${ + kompetanse.annenForeldersAktivitet + }|${ + kompetanse.søkersAktivitetsland + }|${ + kompetanse.annenForeldersAktivitetsland ?: "" + }|${ + kompetanse.barnetsBostedsland + } |""" + } ?: "" + +fun hentTekstForVedtaksperioder( + vedtaksperioder: List, +) = + """ + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser |""" + + hentVedtaksperiodeRader(vedtaksperioder) + +fun hentVedtaksperiodeRader(vedtaksperioder: List) = + vedtaksperioder.joinToString("") { + """ + | ${it.fom?.tilddMMyyyy() ?: ""} |${it.tom?.tilddMMyyyy() ?: ""} |${it.type} | | | |""" + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/vedtak/vedtaksperioder/LagVedtaksperiodeTestUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/vedtak/vedtaksperioder/LagVedtaksperiodeTestUtil.kt new file mode 100644 index 000000000..db41bf048 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/internal/vedtak/vedtaksperioder/LagVedtaksperiodeTestUtil.kt @@ -0,0 +1,253 @@ +package no.nav.familie.ba.sak.internal.vedtak.vedtaksperioder + +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.tilddMMyyyy +import no.nav.familie.ba.sak.internal.vedtak.begrunnelser.VilkårResultatRad +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.IUtfyltEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.tilIEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.UtfyltKompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.tilIKompetanse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import org.apache.commons.lang3.RandomStringUtils +import java.time.LocalDate + +fun lagVedtaksperioderTest( + behandling: Behandling, + forrigeBehandling: Behandling?, + persongrunnlag: PersonopplysningGrunnlag, + persongrunnlagForrigeBehandling: PersonopplysningGrunnlag?, + personResultater: Set, + personResultaterForrigeBehandling: Set?, + andeler: List, + andelerForrigeBehandling: List?, + endredeUtbetalinger: List, + endredeUtbetalingerForrigeBehandling: List?, + vedtaksperioder: List, + kompetanse: Collection, + kompetanseForrigeBehandling: Collection?, +) = """ +
+# language: no
+# encoding: UTF-8
+
+Egenskap: Plassholdertekst for egenskap - ${RandomStringUtils.randomAlphanumeric(10)}
+
+  Bakgrunn:""" +
+    hentTekstForFagsak(behandling) +
+    hentTekstForBehandlinger(behandling, forrigeBehandling) +
+    hentTekstForPersongrunnlag(persongrunnlag, persongrunnlagForrigeBehandling) +
+    """
+      
+  Scenario: Plassholdertekst for scenario - ${RandomStringUtils.randomAlphanumeric(10)}
+    Og følgende dagens dato ${LocalDate.now().tilddMMyyyy()}""" +
+    lagPersonresultaterTekst(forrigeBehandling) +
+    lagPersonresultaterTekst(behandling) +
+    hentTekstForVilkårresultater(personResultaterForrigeBehandling, forrigeBehandling?.id) +
+    hentTekstForVilkårresultater(personResultater, behandling.id) +
+    hentTekstForTilkjentYtelse(andeler, andelerForrigeBehandling) +
+    hentTekstForEndretUtbetaling(endredeUtbetalinger, endredeUtbetalingerForrigeBehandling) +
+    hentTekstForKompetanse(kompetanse, kompetanseForrigeBehandling) + """
+    
+    Når vedtaksperioder med begrunnelser genereres for behandling ${behandling.id}""" +
+    hentTekstForVedtaksperioder(vedtaksperioder) + """
+
+ """ + +private fun lagPersonresultaterTekst(behandling: Behandling?) = behandling?.let { + """ + Og lag personresultater for behandling ${it.id}""" +} ?: "" + +private fun hentTekstForFagsak(behandling: Behandling) = + """ + Gitt følgende fagsak + | FagsakId | Fagsaktype | + | ${behandling.fagsak.id} | ${behandling.fagsak.type} |""" + +private fun hentTekstForBehandlinger(behandling: Behandling, forrigeBehandling: Behandling?) = + """ + + Gitt følgende vedtak + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak |${ + forrigeBehandling?.let { + """ + | ${it.id} | ${it.fagsak.id} | | ${it.resultat} | ${it.opprettetÅrsak} |""" + } ?: "" + } + | ${behandling.id} | ${behandling.fagsak.id} | ${forrigeBehandling?.id ?: ""} |${behandling.resultat} | ${behandling.opprettetÅrsak} |""" + +private fun hentTekstForPersongrunnlag( + persongrunnlag: PersonopplysningGrunnlag, + persongrunnlagForrigeBehandling: PersonopplysningGrunnlag?, +) = + """ + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato |""" + + hentPersongrunnlagRader(persongrunnlagForrigeBehandling) + + hentPersongrunnlagRader(persongrunnlag) + +private fun hentPersongrunnlagRader(persongrunnlag: PersonopplysningGrunnlag?): String = + persongrunnlag?.personer?.joinToString("") { + """ + | ${persongrunnlag.behandlingId} |${it.aktør.aktørId}|${it.type}|${it.fødselsdato.tilddMMyyyy()}|""" + } ?: "" + +private fun hentTekstForVilkårresultater( + personResultater: Set?, + behandlingId: Long?, +): String { + if (personResultater == null || behandlingId == null) { + return "" + } + + return """ + + Og legg til nye vilkårresultater for behandling $behandlingId + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag |""" + + tilVilkårResultatRader(personResultater) +} + +private fun tilVilkårResultatRader(personResultater: Set?) = + personResultater?.joinToString("\n") { personResultat -> + personResultat.vilkårResultater + .sortedBy { it.periodeFom } + .groupBy { + VilkårResultatRad( + personResultat.aktør.aktørId, + it.utdypendeVilkårsvurderinger.toSet(), + it.periodeFom, + it.periodeTom, + it.resultat, + it.erEksplisittAvslagPåSøknad, + ) + }.toList().joinToString("") { (vilkårResultatRad, vilkårResultater) -> + """ + | ${vilkårResultatRad.aktørId} |${vilkårResultater.map { it.vilkårType }.joinToString(",")}|${ + vilkårResultatRad.utdypendeVilkårsvurderinger.joinToString(",") + }|${vilkårResultatRad.fom?.tilddMMyyyy() ?: ""}|${vilkårResultatRad.tom?.tilddMMyyyy() ?: ""}| ${vilkårResultatRad.resultat} | ${if (vilkårResultatRad.erEksplisittAvslagPåSøknad == true) "Ja" else "Nei"} |""" + } + } ?: "" + +private fun hentTekstForTilkjentYtelse( + andeler: List, + andelerForrigeBehandling: List?, +) = + """ + + Og med andeler tilkjent ytelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | """ + + hentAndelRader(andelerForrigeBehandling) + + hentAndelRader(andeler) + +private fun hentAndelRader(andeler: List?): String = andeler + ?.sortedWith(compareBy({ it.aktør.aktivFødselsnummer() }, { it.stønadFom }, { it.stønadTom })) + ?.joinToString("") { + """ + | ${it.aktør.aktørId} |${it.behandlingId}|${ + it.stønadFom.førsteDagIInneværendeMåned().tilddMMyyyy() + }|${ + it.stønadTom.sisteDagIInneværendeMåned().tilddMMyyyy() + }|${it.kalkulertUtbetalingsbeløp}| ${it.type} | ${it.prosent} | ${it.sats} | """ + } ?: "" + +private fun hentTekstForEndretUtbetaling( + endredeUtbetalinger: List, + endredeUtbetalingerForrigeBehandling: List?, +): String { + val rader = hentEndretUtbetalingRader(endredeUtbetalingerForrigeBehandling) + + hentEndretUtbetalingRader(endredeUtbetalinger) + + return if (rader.isEmpty()) { + "" + } else { + """ + + Og med endrede utbetalinger + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent |""" + + hentEndretUtbetalingRader(endredeUtbetalingerForrigeBehandling) + + hentEndretUtbetalingRader(endredeUtbetalinger) + } +} + +private fun hentEndretUtbetalingRader(endredeUtbetalinger: List?): String = + endredeUtbetalinger + ?.map { it.tilIEndretUtbetalingAndel() } + ?.filterIsInstance() + ?.joinToString("") { + """ + | ${it.person.aktør.aktørId} |${it.behandlingId}|${ + it.fom.førsteDagIInneværendeMåned().tilddMMyyyy() + }|${ + it.tom.sisteDagIInneværendeMåned().tilddMMyyyy() + }|${it.årsak} | ${it.prosent} |""" + } ?: "" + +private fun hentTekstForKompetanse( + kompetanse: Collection, + kompetanseForrigeBehandling: Collection?, +): String { + val rader = hentKompetanseRader(kompetanseForrigeBehandling) + + hentKompetanseRader(kompetanse) + + return if (rader.isEmpty()) { + "" + } else { + """ + + Og med kompetanser + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland |""" + + rader + } +} + +private fun hentKompetanseRader(kompetanser: Collection?): String = + kompetanser + ?.map { it.tilIKompetanse() } + ?.filterIsInstance() + ?.joinToString("") { kompetanse -> + """ + | ${ + kompetanse.barnAktører.joinToString(", ") { it.aktørId } + } |${ + kompetanse.fom.førsteDagIInneværendeMåned().tilddMMyyyy() + }|${ + kompetanse.tom?.sisteDagIInneværendeMåned()?.tilddMMyyyy() ?: "" + }|${ + kompetanse.resultat + }|${ + kompetanse.behandlingId + }|${ + kompetanse.søkersAktivitet + }|${ + kompetanse.annenForeldersAktivitet + }|${ + kompetanse.søkersAktivitetsland + }|${ + kompetanse.annenForeldersAktivitetsland ?: "" + }|${ + kompetanse.barnetsBostedsland + } |""" + } ?: "" + +private fun hentTekstForVedtaksperioder( + vedtaksperioder: List, +) = + """ + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar |""" + + hentVedtaksperiodeRader(vedtaksperioder) + +private fun hentVedtaksperiodeRader(vedtaksperioder: List) = + vedtaksperioder.joinToString("") { + """ + | ${it.fom?.tilddMMyyyy() ?: ""} |${it.tom?.tilddMMyyyy() ?: ""} |${it.type} | |""" + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/ArbeidsfordelingController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/ArbeidsfordelingController.kt new file mode 100644 index 000000000..fee17c331 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/ArbeidsfordelingController.kt @@ -0,0 +1,64 @@ +package no.nav.familie.ba.sak.kjerne.arbeidsfordeling + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/arbeidsfordeling") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class ArbeidsfordelingController( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val utvidetBehandlingService: UtvidetBehandlingService, + private val arbeidsfordelingService: ArbeidsfordelingService, + private val tilgangService: TilgangService, +) { + + @PutMapping(path = ["{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun endreBehandlendeEnhet( + @PathVariable behandlingId: Long, + @RequestBody + endreBehandlendeEnhet: RestEndreBehandlendeEnhet, + ): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Endre behandlende enhet", + ) + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + + if (endreBehandlendeEnhet.begrunnelse.isBlank()) { + throw FunksjonellFeil( + melding = "Begrunnelse kan ikke være tom", + frontendFeilmelding = "Du må skrive en begrunnelse for endring av enhet", + ) + } + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + arbeidsfordelingService.manueltOppdaterBehandlendeEnhet( + behandling = behandling, + endreBehandlendeEnhet = endreBehandlendeEnhet, + ) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandling.id))) + } +} + +data class RestEndreBehandlendeEnhet( + val enhetId: String, + val begrunnelse: String, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/ArbeidsfordelingService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/ArbeidsfordelingService.kt new file mode 100644 index 000000000..bd7aba602 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/ArbeidsfordelingService.kt @@ -0,0 +1,227 @@ +package no.nav.familie.ba.sak.kjerne.arbeidsfordeling + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.PdlPersonKanIkkeBehandlesIFagsystem +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Arbeidsfordelingsenhet +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.domene.ArbeidsfordelingPåBehandling +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.domene.ArbeidsfordelingPåBehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.barn +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.statistikk.saksstatistikk.SaksstatistikkEventPublisher +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class ArbeidsfordelingService( + private val arbeidsfordelingPåBehandlingRepository: ArbeidsfordelingPåBehandlingRepository, + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + private val personidentService: PersonidentService, + private val oppgaveService: OppgaveService, + private val loggService: LoggService, + private val integrasjonClient: IntegrasjonClient, + private val personopplysningerService: PersonopplysningerService, + private val saksstatistikkEventPublisher: SaksstatistikkEventPublisher, +) { + + @Transactional + fun manueltOppdaterBehandlendeEnhet(behandling: Behandling, endreBehandlendeEnhet: RestEndreBehandlendeEnhet) { + val aktivArbeidsfordelingPåBehandling = + arbeidsfordelingPåBehandlingRepository.finnArbeidsfordelingPåBehandling(behandling.id) + ?: throw Feil("Finner ikke tilknyttet arbeidsfordelingsenhet på behandling ${behandling.id}") + + val forrigeArbeidsfordelingsenhet = Arbeidsfordelingsenhet( + enhetId = aktivArbeidsfordelingPåBehandling.behandlendeEnhetId, + enhetNavn = aktivArbeidsfordelingPåBehandling.behandlendeEnhetNavn, + ) + + val oppdatertArbeidsfordelingPåBehandling = arbeidsfordelingPåBehandlingRepository.save( + aktivArbeidsfordelingPåBehandling.copy( + behandlendeEnhetId = endreBehandlendeEnhet.enhetId, + behandlendeEnhetNavn = integrasjonClient.hentEnhet(endreBehandlendeEnhet.enhetId).navn, + manueltOverstyrt = true, + ), + ) + + postFastsattBehandlendeEnhet( + behandling = behandling, + forrigeArbeidsfordelingsenhet = forrigeArbeidsfordelingsenhet, + oppdatertArbeidsfordelingPåBehandling = oppdatertArbeidsfordelingPåBehandling, + manuellOppdatering = true, + begrunnelse = endreBehandlendeEnhet.begrunnelse, + ) + saksstatistikkEventPublisher.publiserBehandlingsstatistikk(behandling.id) + } + + fun fastsettBehandlendeEnhet(behandling: Behandling, sisteBehandlingSomErIverksatt: Behandling? = null) { + val aktivArbeidsfordelingPåBehandling = + arbeidsfordelingPåBehandlingRepository.finnArbeidsfordelingPåBehandling(behandling.id) + + val forrigeArbeidsfordelingsenhet = + if (aktivArbeidsfordelingPåBehandling != null) { + Arbeidsfordelingsenhet( + enhetId = aktivArbeidsfordelingPåBehandling.behandlendeEnhetId, + enhetNavn = aktivArbeidsfordelingPåBehandling.behandlendeEnhetNavn, + ) + } else { + null + } + + val oppdatertArbeidsfordelingPåBehandling = + if (behandling.erSatsendring()) { + fastsettArbeidsfordelingsenhetPåSatsendringsbehandling( + behandling, + sisteBehandlingSomErIverksatt, + aktivArbeidsfordelingPåBehandling, + ) + } else { + val arbeidsfordelingsenhet = hentArbeidsfordelingsenhet(behandling) + + when (aktivArbeidsfordelingPåBehandling) { + null -> { + arbeidsfordelingPåBehandlingRepository.save( + ArbeidsfordelingPåBehandling( + behandlingId = behandling.id, + behandlendeEnhetId = arbeidsfordelingsenhet.enhetId, + behandlendeEnhetNavn = arbeidsfordelingsenhet.enhetNavn, + ), + ) + } + + else -> { + if (!aktivArbeidsfordelingPåBehandling.manueltOverstyrt && + (aktivArbeidsfordelingPåBehandling.behandlendeEnhetId != arbeidsfordelingsenhet.enhetId) + ) { + aktivArbeidsfordelingPåBehandling.also { + it.behandlendeEnhetId = arbeidsfordelingsenhet.enhetId + it.behandlendeEnhetNavn = arbeidsfordelingsenhet.enhetNavn + } + arbeidsfordelingPåBehandlingRepository.save(aktivArbeidsfordelingPåBehandling) + } + aktivArbeidsfordelingPåBehandling + } + } + } + + postFastsattBehandlendeEnhet( + behandling = behandling, + forrigeArbeidsfordelingsenhet = forrigeArbeidsfordelingsenhet, + oppdatertArbeidsfordelingPåBehandling = oppdatertArbeidsfordelingPåBehandling, + manuellOppdatering = false, + ) + } + + private fun fastsettArbeidsfordelingsenhetPåSatsendringsbehandling( + behandling: Behandling, + sisteBehandlingSomErIverksatt: Behandling?, + aktivArbeidsfordelingPåBehandling: ArbeidsfordelingPåBehandling?, + ): ArbeidsfordelingPåBehandling { + return aktivArbeidsfordelingPåBehandling + ?: if (sisteBehandlingSomErIverksatt != null) { + val forrigeIverksattesBehandlingArbeidsfordelingsenhet = + arbeidsfordelingPåBehandlingRepository.finnArbeidsfordelingPåBehandling( + sisteBehandlingSomErIverksatt.id, + ) + + arbeidsfordelingPåBehandlingRepository.save( + forrigeIverksattesBehandlingArbeidsfordelingsenhet?.copy( + id = 0, + behandlingId = behandling.id, + ) + ?: throw Feil("Finner ikke arbeidsfordelingsenhet på forrige iverksatte behandling på satsendringsbehandling"), + ) + } else { + throw Feil("Klarte ikke å fastsette arbeidsfordelingsenhet på satsendringsbehandling.") + } + } + + private fun postFastsattBehandlendeEnhet( + behandling: Behandling, + forrigeArbeidsfordelingsenhet: Arbeidsfordelingsenhet?, + oppdatertArbeidsfordelingPåBehandling: ArbeidsfordelingPåBehandling, + manuellOppdatering: Boolean, + begrunnelse: String = "", + ) { + logger.info("Fastsatt behandlende enhet ${if (manuellOppdatering) "manuelt" else "automatisk"} på behandling ${behandling.id}: $oppdatertArbeidsfordelingPåBehandling") + secureLogger.info("Fastsatt behandlende enhet ${if (manuellOppdatering) "manuelt" else "automatisk"} på behandling ${behandling.id}: ${oppdatertArbeidsfordelingPåBehandling.toSecureString()}") + + if (forrigeArbeidsfordelingsenhet != null && forrigeArbeidsfordelingsenhet.enhetId != oppdatertArbeidsfordelingPåBehandling.behandlendeEnhetId) { + loggService.opprettBehandlendeEnhetEndret( + behandling = behandling, + fraEnhet = forrigeArbeidsfordelingsenhet, + tilEnhet = oppdatertArbeidsfordelingPåBehandling, + manuellOppdatering = manuellOppdatering, + begrunnelse = begrunnelse, + ) + + oppgaveService.endreTilordnetEnhetPåOppgaverForBehandling( + behandling, + oppdatertArbeidsfordelingPåBehandling.behandlendeEnhetId, + ) + } + } + + fun hentArbeidsfordelingPåBehandling(behandlingId: Long): ArbeidsfordelingPåBehandling { + return arbeidsfordelingPåBehandlingRepository.finnArbeidsfordelingPåBehandling(behandlingId) + ?: error("Finner ikke tilknyttet arbeidsfordeling på behandling med id $behandlingId") + } + + fun hentArbeidsfordelingsenhet(behandling: Behandling): Arbeidsfordelingsenhet { + val søker: IdentMedAdressebeskyttelse = identMedAdressebeskyttelse(behandling.fagsak.aktør) + + val personinfoliste: List = personopplysningGrunnlagRepository.finnSøkerOgBarnAktørerTilAktiv(behandling.id) + .barn() + .mapNotNull { + try { + identMedAdressebeskyttelse(it.aktør) + } catch (e: PdlPersonKanIkkeBehandlesIFagsystem) { + logger.warn("Ignorerer barn fra hentArbeidsfordelingsenhet for behandling ${behandling.id} : ${e.årsak}") + secureLogger.warn("Ignorerer barn ${it.aktør.aktivFødselsnummer()} hentArbeidsfordelingsenhet for behandling ${behandling.id}: ${e.årsak}") + null + } + }.plus(søker) + + val identMedStrengeste = finnPersonMedStrengesteAdressebeskyttelse(personinfoliste) + + return integrasjonClient.hentBehandlendeEnhet(identMedStrengeste ?: søker.ident).singleOrNull() + ?: throw Feil(message = "Fant flere eller ingen enheter på behandling.") + } + + fun hentArbeidsfordelingsenhetPåIdenter(søkerIdent: String, barnIdenter: List): Arbeidsfordelingsenhet { + val identMedStrengeste = + finnPersonMedStrengesteAdressebeskyttelse((barnIdenter + søkerIdent).map { identMedAdressebeskyttelse(it) }) + + return integrasjonClient.hentBehandlendeEnhet(identMedStrengeste ?: søkerIdent).singleOrNull() + ?: throw Feil(message = "Fant flere eller ingen enheter på behandling.") + } + + private fun identMedAdressebeskyttelse(ident: String) = IdentMedAdressebeskyttelse( + ident = ident, + adressebeskyttelsegradering = personopplysningerService.hentPersoninfoEnkel( + personidentService.hentAktør(ident), + ).adressebeskyttelseGradering, + ) + + private fun identMedAdressebeskyttelse(aktør: Aktør) = IdentMedAdressebeskyttelse( + ident = aktør.aktivFødselsnummer(), + adressebeskyttelsegradering = personopplysningerService.hentPersoninfoEnkel(aktør).adressebeskyttelseGradering, + ) + + data class IdentMedAdressebeskyttelse( + val ident: String, + val adressebeskyttelsegradering: ADRESSEBESKYTTELSEGRADERING?, + ) + + companion object { + private val logger = LoggerFactory.getLogger(ArbeidsfordelingService::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/DiskresjonskodeUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/DiskresjonskodeUtils.kt new file mode 100644 index 000000000..f2d2a7243 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/DiskresjonskodeUtils.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.kjerne.arbeidsfordeling + +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService.IdentMedAdressebeskyttelse +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING + +fun finnPersonMedStrengesteAdressebeskyttelse(personer: List): String? { + return personer.fold( + null, + fun( + person: IdentMedAdressebeskyttelse?, + neste: IdentMedAdressebeskyttelse, + ): IdentMedAdressebeskyttelse? { + return when { + person?.adressebeskyttelsegradering == ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG -> { + person + } + neste.adressebeskyttelsegradering == ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG -> { + neste + } + person?.adressebeskyttelsegradering == ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG_UTLAND -> { + person + } + neste.adressebeskyttelsegradering == ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG_UTLAND -> { + neste + } + person?.adressebeskyttelsegradering == ADRESSEBESKYTTELSEGRADERING.FORTROLIG -> { + person + } + neste.adressebeskyttelsegradering == ADRESSEBESKYTTELSEGRADERING.FORTROLIG + -> { + neste + } + else -> null + } + }, + )?.ident +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/domene/ArbeidsfordelingP\303\245Behandling.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/domene/ArbeidsfordelingP\303\245Behandling.kt" new file mode 100644 index 000000000..298de544f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/domene/ArbeidsfordelingP\303\245Behandling.kt" @@ -0,0 +1,45 @@ +package no.nav.familie.ba.sak.kjerne.arbeidsfordeling.domene + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "ArbeidsfordelingPåBehandling") +@Table(name = "ARBEIDSFORDELING_PA_BEHANDLING") +data class ArbeidsfordelingPåBehandling( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "arbeidsfordeling_pa_behandling_seq_generator") + @SequenceGenerator( + name = "arbeidsfordeling_pa_behandling_seq_generator", + sequenceName = "arbeidsfordeling_pa_behandling_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "fk_behandling_id", nullable = false, updatable = false, unique = true) + val behandlingId: Long, + + @Column(name = "behandlende_enhet_id", nullable = false) + var behandlendeEnhetId: String, + + @Column(name = "behandlende_enhet_navn", nullable = false) + var behandlendeEnhetNavn: String, + + @Column(name = "manuelt_overstyrt", nullable = false) + var manueltOverstyrt: Boolean = false, +) { + override fun toString(): String { + return "ArbeidsfordelingPåBehandling(id=$id, manueltOverstyrt=$manueltOverstyrt)" + } + + fun toSecureString(): String { + return "ArbeidsfordelingPåBehandling(id=$id, behandlendeEnhetId=$behandlendeEnhetId, behandlendeEnhetNavn=$behandlendeEnhetNavn, manueltOverstyrt=$manueltOverstyrt)" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/domene/ArbeidsfordelingP\303\245BehandlingRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/domene/ArbeidsfordelingP\303\245BehandlingRepository.kt" new file mode 100644 index 000000000..894b57ff7 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/domene/ArbeidsfordelingP\303\245BehandlingRepository.kt" @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.kjerne.arbeidsfordeling.domene + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface ArbeidsfordelingPåBehandlingRepository : JpaRepository { + @Query(value = "SELECT apb FROM ArbeidsfordelingPåBehandling apb WHERE apb.behandlingId = :behandlingId") + fun finnArbeidsfordelingPåBehandling(behandlingId: Long): ArbeidsfordelingPåBehandling? +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/AutovedtakService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/AutovedtakService.kt new file mode 100644 index 000000000..7749b5969 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/AutovedtakService.kt @@ -0,0 +1,79 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.steg.TilbakestillBehandlingTilBehandlingsresultatService +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollService +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class AutovedtakService( + private val stegService: StegService, + private val behandlingService: BehandlingService, + private val vedtakService: VedtakService, + private val loggService: LoggService, + private val totrinnskontrollService: TotrinnskontrollService, + private val tilbakestillBehandlingTilBehandlingsresultatService: TilbakestillBehandlingTilBehandlingsresultatService, +) { + fun opprettAutomatiskBehandlingOgKjørTilBehandlingsresultat( + aktør: Aktør, + behandlingType: BehandlingType, + behandlingÅrsak: BehandlingÅrsak, + fagsakId: Long, + ): Behandling { + val nyBehandling = stegService.håndterNyBehandling( + NyBehandling( + behandlingType = behandlingType, + behandlingÅrsak = behandlingÅrsak, + søkersIdent = aktør.aktivFødselsnummer(), + skalBehandlesAutomatisk = true, + fagsakId = fagsakId, + ), + ) + + val behandlingEtterBehandlingsresultat = stegService.håndterVilkårsvurdering(nyBehandling) + return behandlingEtterBehandlingsresultat + } + + fun opprettToTrinnskontrollOgVedtaksbrevForAutomatiskBehandling(behandling: Behandling): Vedtak { + totrinnskontrollService.opprettAutomatiskTotrinnskontroll(behandling) + + loggService.opprettBeslutningOmVedtakLogg( + behandling = behandling, + beslutning = Beslutning.GODKJENT, + behandlingErAutomatiskBesluttet = true, + ) + + val vedtak = vedtakService.hentAktivForBehandling(behandlingId = behandling.id) + ?: error("Fant ikke aktivt vedtak på behandling ${behandling.id}") + return vedtakService.oppdaterVedtakMedStønadsbrev(vedtak = vedtak) + } + + fun omgjørBehandlingTilManuellOgKjørSteg(behandling: Behandling, steg: StegType): Behandling { + val omgjortBehandling = behandlingService.omgjørTilManuellBehandling(behandling) + + return when (steg) { + StegType.VILKÅRSVURDERING -> + tilbakestillBehandlingTilBehandlingsresultatService + .tilbakestillBehandlingTilBehandlingsresultat(behandlingId = omgjortBehandling.id) + + else -> throw Feil("Steg $steg er ikke støttet ved omgjøring av automatisk behandling til manuell.") + } + } + + companion object { + val logger = LoggerFactory.getLogger(AutovedtakService::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/AutovedtakStegService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/AutovedtakStegService.kt new file mode 100644 index 000000000..6624431d8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/AutovedtakStegService.kt @@ -0,0 +1,207 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.AutovedtakFødselshendelseService +import no.nav.familie.ba.sak.kjerne.autovedtak.omregning.AutovedtakBrevService +import no.nav.familie.ba.sak.kjerne.autovedtak.småbarnstillegg.AutovedtakSmåbarnstilleggService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.task.dto.ManuellOppgaveType +import no.nav.familie.prosessering.error.RekjørSenereException +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +interface AutovedtakBehandlingService { + fun skalAutovedtakBehandles(behandlingsdata: Behandlingsdata): Boolean + + fun kjørBehandling(behandlingsdata: Behandlingsdata): String +} + +enum class Autovedtaktype(val displayName: String) { + FØDSELSHENDELSE("Fødselshendelse"), + SMÅBARNSTILLEGG("Småbarnstillegg"), + OMREGNING_BREV("Omregning"), +} + +sealed interface AutomatiskBehandlingData { + val type: Autovedtaktype +} + +data class FødselshendelseData( + val nyBehandlingHendelse: NyBehandlingHendelse, +) : AutomatiskBehandlingData { + override val type = Autovedtaktype.FØDSELSHENDELSE +} + +data class SmåbarnstilleggData( + val aktør: Aktør, +) : AutomatiskBehandlingData { + override val type = Autovedtaktype.SMÅBARNSTILLEGG +} + +data class OmregningBrevData( + val aktør: Aktør, + val behandlingsårsak: BehandlingÅrsak, + val standardbegrunnelse: Standardbegrunnelse, + val fagsakId: Long, +) : AutomatiskBehandlingData { + override val type = Autovedtaktype.OMREGNING_BREV +} + +@Service +class AutovedtakStegService( + private val fagsakService: FagsakService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val oppgaveService: OppgaveService, + private val autovedtakFødselshendelseService: AutovedtakFødselshendelseService, + private val autovedtakBrevService: AutovedtakBrevService, + private val autovedtakSmåbarnstilleggService: AutovedtakSmåbarnstilleggService, +) { + + private val antallAutovedtak: Map = Autovedtaktype.values().associateWith { + Metrics.counter("behandling.saksbehandling.autovedtak", "type", it.name) + } + private val antallAutovedtakÅpenBehandling: Map = Autovedtaktype.values().associateWith { + Metrics.counter("behandling.saksbehandling.autovedtak.aapen_behandling", "type", it.name) + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun kjørBehandlingFødselshendelse(mottakersAktør: Aktør, nyBehandlingHendelse: NyBehandlingHendelse): String { + return kjørBehandling( + mottakersAktør = mottakersAktør, + automatiskBehandlingData = FødselshendelseData(nyBehandlingHendelse), + ) + } + + fun kjørBehandlingOmregning(mottakersAktør: Aktør, behandlingsdata: OmregningBrevData): String { + return kjørBehandling( + mottakersAktør = mottakersAktør, + automatiskBehandlingData = behandlingsdata, + ) + } + + fun kjørBehandlingSmåbarnstillegg(mottakersAktør: Aktør, aktør: Aktør): String { + return kjørBehandling( + mottakersAktør = mottakersAktør, + automatiskBehandlingData = SmåbarnstilleggData(aktør), + ) + } + + private fun kjørBehandling( + automatiskBehandlingData: AutomatiskBehandlingData, + mottakersAktør: Aktør, + ): String { + secureLoggAutovedtakBehandling(automatiskBehandlingData.type, mottakersAktør, BEHANDLING_STARTER) + antallAutovedtak[automatiskBehandlingData.type]?.increment() + + val skalAutovedtakBehandles = when (automatiskBehandlingData) { + is FødselshendelseData -> autovedtakFødselshendelseService.skalAutovedtakBehandles(automatiskBehandlingData) + is OmregningBrevData -> autovedtakBrevService.skalAutovedtakBehandles(automatiskBehandlingData) + is SmåbarnstilleggData -> autovedtakSmåbarnstilleggService.skalAutovedtakBehandles(automatiskBehandlingData) + } + + if (!skalAutovedtakBehandles) { + secureLoggAutovedtakBehandling( + automatiskBehandlingData.type, + mottakersAktør, + "Skal ikke behandles", + ) + return "${automatiskBehandlingData.type.displayName}: Skal ikke behandles" + } + + if (håndterÅpenBehandlingOgAvbrytAutovedtak( + aktør = mottakersAktør, + autovedtaktype = automatiskBehandlingData.type, + fagsakId = hentFagsakIdFraBehandlingsdata(automatiskBehandlingData), + ) + ) { + secureLoggAutovedtakBehandling( + automatiskBehandlingData.type, + mottakersAktør, + "Bruker har åpen behandling", + ) + return "${automatiskBehandlingData.type.displayName}: Bruker har åpen behandling" + } + + val resultatAvKjøring = when (automatiskBehandlingData) { + is FødselshendelseData -> autovedtakFødselshendelseService.kjørBehandling(automatiskBehandlingData) + is OmregningBrevData -> autovedtakBrevService.kjørBehandling(automatiskBehandlingData) + is SmåbarnstilleggData -> autovedtakSmåbarnstilleggService.kjørBehandling(automatiskBehandlingData) + } + + secureLoggAutovedtakBehandling( + automatiskBehandlingData.type, + mottakersAktør, + resultatAvKjøring, + ) + + return resultatAvKjøring + } + + private fun hentFagsakIdFraBehandlingsdata( + behandlingsdata: AutomatiskBehandlingData, + ): Long? = when (behandlingsdata) { + is OmregningBrevData -> behandlingsdata.fagsakId + is FødselshendelseData, + is SmåbarnstilleggData, + -> null + } + + private fun håndterÅpenBehandlingOgAvbrytAutovedtak( + aktør: Aktør, + autovedtaktype: Autovedtaktype, + fagsakId: Long?, + ): Boolean { + val fagsak = if (fagsakId != null) { + fagsakService.hentPåFagsakId(fagsakId) + } else { + fagsakService.hentNormalFagsak(aktør = aktør) + } + val åpenBehandling = fagsak?.let { + behandlingHentOgPersisterService.finnAktivOgÅpenForFagsak(it.id) + } + + return if (åpenBehandling == null) { + false + } else if (åpenBehandling.status == BehandlingStatus.UTREDES || åpenBehandling.status == BehandlingStatus.FATTER_VEDTAK || åpenBehandling.status == BehandlingStatus.SATT_PÅ_VENT) { + antallAutovedtakÅpenBehandling[autovedtaktype]?.increment() + oppgaveService.opprettOppgaveForManuellBehandling( + behandling = åpenBehandling, + begrunnelse = "${autovedtaktype.displayName}: Bruker har åpen behandling", + manuellOppgaveType = ManuellOppgaveType.ÅPEN_BEHANDLING, + ) + true + } else if (åpenBehandling.status == BehandlingStatus.IVERKSETTER_VEDTAK || åpenBehandling.status == BehandlingStatus.SATT_PÅ_MASKINELL_VENT) { + throw RekjørSenereException( + årsak = "Åpen behandling med status ${åpenBehandling.status}, prøver igjen om 1 time", + triggerTid = LocalDateTime.now().plusHours(1), + ) + } else { + throw Feil("Ikke håndtert feilsituasjon på $åpenBehandling") + } + } + + private fun secureLoggAutovedtakBehandling( + autovedtaktype: Autovedtaktype, + aktør: Aktør, + melding: String, + ) { + secureLogger.info("$autovedtaktype(${aktør.aktivFødselsnummer()}): $melding") + } + + companion object { + const val BEHANDLING_STARTER = "Behandling starter" + const val BEHANDLING_FERDIG = "Behandling ferdig" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/AutovedtakF\303\270dselshendelseService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/AutovedtakF\303\270dselshendelseService.kt" new file mode 100644 index 000000000..3db9d9087 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/AutovedtakF\303\270dselshendelseService.kt" @@ -0,0 +1,305 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse + +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakBehandlingService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakStegService +import no.nav.familie.ba.sak.kjerne.autovedtak.FødselshendelseData +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.FiltreringsreglerService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall.VilkårIkkeOppfyltÅrsak +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall.VilkårKanskjeOppfyltÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.HenleggÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.RestHenleggBehandlingInfo +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.StatsborgerskapService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.søker +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.task.BehandleFødselshendelseTask +import no.nav.familie.ba.sak.task.IverksettMotOppdragTask +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.ba.sak.task.dto.ManuellOppgaveType +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class AutovedtakFødselshendelseService( + private val fagsakService: FagsakService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val filtreringsreglerService: FiltreringsreglerService, + private val taskRepository: TaskRepositoryWrapper, + private val vilkårsvurderingRepository: VilkårsvurderingRepository, + private val persongrunnlagService: PersongrunnlagService, + private val personidentService: PersonidentService, + private val stegService: StegService, + private val vedtakService: VedtakService, + private val vedtaksperiodeService: VedtaksperiodeService, + private val autovedtakService: AutovedtakService, + private val personopplysningerService: PersonopplysningerService, + private val statsborgerskapService: StatsborgerskapService, + private val opprettTaskService: OpprettTaskService, + private val oppgaveService: OppgaveService, +) : AutovedtakBehandlingService { + + val stansetIAutomatiskFiltreringCounter = + Metrics.counter("familie.ba.sak.henvendelse.stanset", "steg", "filtrering") + val stansetIAutomatiskVilkårsvurderingCounter = + Metrics.counter("familie.ba.sak.henvendelse.stanset", "steg", "vilkaarsvurdering") + val passertFiltreringOgVilkårsvurderingCounter = Metrics.counter("familie.ba.sak.henvendelse.passert") + + override fun skalAutovedtakBehandles(behandlingsdata: FødselshendelseData): Boolean { + val nyBehandlingHendelse = behandlingsdata.nyBehandlingHendelse + val morsAktør = personidentService.hentAktør(nyBehandlingHendelse.morsIdent) + val morsÅpneBehandling = hentÅpenNormalBehandling(aktør = morsAktør) + val barnsAktører = personidentService.hentAktørIder(nyBehandlingHendelse.barnasIdenter) + + if (morsÅpneBehandling != null) { + val barnaPåÅpenBehandling = + persongrunnlagService.hentBarna(behandling = morsÅpneBehandling).map { it.aktør } + + if (barnPåHendelseBlirAlleredeBehandletIÅpenBehandling( + barnaPåHendelse = barnsAktører, + barnaPåÅpenBehandling = barnaPåÅpenBehandling, + ) + ) { + logger.info("Ignorerer fødselshendelse fordi åpen behandling inneholder alle barna i hendelsen.") + secureLogger.info( + "Ignorerer fødselshendelse fordi åpen behandling inneholder alle barna i hendelsen." + + "Barn på hendelse=${nyBehandlingHendelse.barnasIdenter}, barn på åpen behandling=$barnaPåÅpenBehandling", + ) + return false + } + } + + val (barnSomSkalBehandlesForMor, alleBarnSomKanBehandles) = finnBarnSomSkalBehandlesForMor( + fagsak = fagsakService.hentNormalFagsak(aktør = morsAktør), + nyBehandlingHendelse = nyBehandlingHendelse, + ) + + if (barnSomSkalBehandlesForMor.isEmpty()) { + logger.info("Ignorere fødselshendelse, alle barna fra hendelse er allerede behandlet") + secureLogger.info( + "Ignorere fødselshendelse, alle barna fra hendelse er allerede behandlet. " + + "Alle barna som kan behandles=$alleBarnSomKanBehandles, ", + ) + return false + } + + return true + } + + override fun kjørBehandling(behandlingsdata: FødselshendelseData): String { + val nyBehandling = behandlingsdata.nyBehandlingHendelse + val morsAktør = personidentService.hentAktør(nyBehandling.morsIdent) + + val (barnSomSkalBehandlesForMor, _) = finnBarnSomSkalBehandlesForMor( + fagsak = fagsakService.hentNormalFagsak(aktør = morsAktør), + nyBehandlingHendelse = nyBehandling, + ) + + val behandling = stegService.opprettNyBehandlingOgRegistrerPersongrunnlagForFødselhendelse( + nyBehandling.copy( + barnasIdenter = barnSomSkalBehandlesForMor, + ), + ) + + val behandlingEtterFiltrering = + stegService.håndterFiltreringsreglerForFødselshendelser(behandling, nyBehandling) + + return if (behandlingEtterFiltrering.steg == StegType.HENLEGG_BEHANDLING) { + stansetIAutomatiskFiltreringCounter.increment() + + henleggBehandlingOgOpprettManuellOppgave( + behandling = behandlingEtterFiltrering, + begrunnelse = filtreringsreglerService.hentFødselshendelsefiltreringResultater(behandlingId = behandling.id) + .first { it.resultat == Resultat.IKKE_OPPFYLT }.begrunnelse, + ) + } else { + vurderVilkår(behandling = behandlingEtterFiltrering, barnaSomVurderes = barnSomSkalBehandlesForMor) + } + } + + private fun vurderVilkår(behandling: Behandling, barnaSomVurderes: List): String { + val behandlingEtterVilkårsvurdering = stegService.håndterVilkårsvurdering(behandling = behandling) + + return if (behandlingEtterVilkårsvurdering.resultat == Behandlingsresultat.INNVILGET) { + val vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingId = behandling.id) + vedtaksperiodeService.oppdaterVedtaksperioderForBarnVurdertIFødselshendelse(vedtak, barnaSomVurderes) + + val vedtakEtterToTrinn = + autovedtakService.opprettToTrinnskontrollOgVedtaksbrevForAutomatiskBehandling(behandling = behandlingEtterVilkårsvurdering) + + val task = IverksettMotOppdragTask.opprettTask( + behandling, + vedtakEtterToTrinn, + SikkerhetContext.hentSaksbehandler(), + ) + taskRepository.save(task) + + opprettFremleggsoppgaveDersomEØSMedlem(behandling) + + passertFiltreringOgVilkårsvurderingCounter.increment() + + AutovedtakStegService.BEHANDLING_FERDIG + } else { + stansetIAutomatiskVilkårsvurderingCounter + + henleggBehandlingOgOpprettManuellOppgave(behandling = behandlingEtterVilkårsvurdering) + } + } + + internal fun opprettFremleggsoppgaveDersomEØSMedlem(behandling: Behandling) { + val gjeldendeStatsborgerskap = + personopplysningerService.hentGjeldendeStatsborgerskap(behandling.fagsak.aktør) + val medlemskap = statsborgerskapService.hentSterkesteMedlemskap(statsborgerskap = gjeldendeStatsborgerskap) + if (medlemskap == Medlemskap.EØS) { + logger.info("Oppretter task for opprettelse av fremleggsoppgave på $behandling") + opprettTaskService.opprettOppgaveTask( + behandlingId = behandling.id, + oppgavetype = Oppgavetype.Fremlegg, + beskrivelse = "Kontroller gyldig opphold", + fristForFerdigstillelse = LocalDate.now().plusYears(1), + ) + } + } + + private fun hentÅpenNormalBehandling(aktør: Aktør): Behandling? { + return fagsakService.hentNormalFagsak(aktør)?.let { + behandlingHentOgPersisterService.finnAktivOgÅpenForFagsak(it.id) + } + } + + private fun finnBarnSomSkalBehandlesForMor( + fagsak: Fagsak?, + nyBehandlingHendelse: NyBehandlingHendelse, + ): Pair, List> { + val morsAktør = personidentService.hentAktør(nyBehandlingHendelse.morsIdent) + val barnaTilMor = personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon( + aktør = morsAktør, + ).forelderBarnRelasjon.filter { it.relasjonsrolle == FORELDERBARNRELASJONROLLE.BARN } + + val barnaSomHarBlittBehandlet = + if (fagsak != null) { + behandlingHentOgPersisterService.hentBehandlinger(fagsakId = fagsak.id).filter { !it.erHenlagt() } + .flatMap { + persongrunnlagService.hentBarna(behandling = it).map { barn -> barn.aktør.aktivFødselsnummer() } + }.distinct() + } else { + emptyList() + } + + return finnBarnSomSkalBehandlesForMor( + nyBehandlingHendelse = nyBehandlingHendelse, + barnaTilMor = barnaTilMor, + barnaSomHarBlittBehandlet = barnaSomHarBlittBehandlet, + secureLogger = secureLogger, + ) + } + + private fun henleggBehandlingOgOpprettManuellOppgave( + behandling: Behandling, + begrunnelse: String = "", + ): String { + val begrunnelseForManuellOppgave = if (begrunnelse == "") { + hentBegrunnelseFraVilkårsvurdering(behandlingId = behandling.id) + } else { + begrunnelse + } + + stegService.håndterHenleggBehandling( + behandling = behandling, + henleggBehandlingInfo = RestHenleggBehandlingInfo( + årsak = HenleggÅrsak.FØDSELSHENDELSE_UGYLDIG_UTFALL, + begrunnelse = begrunnelseForManuellOppgave, + ), + ) + + oppgaveService.opprettOppgaveForManuellBehandling( + behandling = behandling, + begrunnelse = "Fødselshendelse: $begrunnelseForManuellOppgave", + manuellOppgaveType = ManuellOppgaveType.FØDSELSHENDELSE, + ) + + return "Henlegger behandling $behandling automatisk på grunn av ugyldig resultat" + } + + private fun hentBegrunnelseFraVilkårsvurdering(behandlingId: Long): String { + val vilkårsvurdering = vilkårsvurderingRepository.findByBehandlingAndAktiv(behandlingId) + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + val søker = persongrunnlagService.hentSøkerOgBarnPåBehandlingThrows(behandling.id).søker() + val søkerResultat = vilkårsvurdering?.personResultater?.find { it.aktør == søker.aktør } + + val bosattIRiketResultat = søkerResultat?.vilkårResultater?.find { it.vilkårType == Vilkår.BOSATT_I_RIKET } + val lovligOppholdResultat = søkerResultat?.vilkårResultater?.find { it.vilkårType == Vilkår.LOVLIG_OPPHOLD } + if (bosattIRiketResultat?.resultat == Resultat.IKKE_OPPFYLT && bosattIRiketResultat.evalueringÅrsaker.any { + VilkårIkkeOppfyltÅrsak.valueOf( + it, + ) == VilkårIkkeOppfyltÅrsak.BOR_IKKE_I_RIKET_FLERE_ADRESSER_UTEN_FOM + } + ) { + return "Mor har flere bostedsadresser uten fra- og med dato" + } else if (bosattIRiketResultat?.resultat == Resultat.IKKE_OPPFYLT) { + return "Mor er ikke bosatt i riket." + } else if (lovligOppholdResultat?.resultat != Resultat.OPPFYLT) { + return lovligOppholdResultat?.evalueringÅrsaker?.joinToString("\n") { + when (lovligOppholdResultat.resultat) { + Resultat.IKKE_OPPFYLT -> VilkårIkkeOppfyltÅrsak.valueOf(it).beskrivelse + Resultat.IKKE_VURDERT -> VilkårKanskjeOppfyltÅrsak.valueOf(it).beskrivelse + else -> "" + } + } + ?: "Mor har ikke lovlig opphold" + } + + persongrunnlagService.hentBarna(behandling).forEach { barn -> + val vilkårsresultat = + vilkårsvurdering.personResultater.find { it.aktør == barn.aktør }?.vilkårResultater + + if (vilkårsresultat?.find { it.vilkårType == Vilkår.UNDER_18_ÅR }?.resultat == Resultat.IKKE_OPPFYLT) { + return "Barnet (fødselsdato: ${barn.fødselsdato.tilKortString()}) er over 18 år." + } + + if (vilkårsresultat?.find { it.vilkårType == Vilkår.BOR_MED_SØKER }?.resultat == Resultat.IKKE_OPPFYLT) { + return "Barnet (fødselsdato: ${barn.fødselsdato.tilKortString()}) er ikke bosatt med mor." + } + + if (vilkårsresultat?.find { it.vilkårType == Vilkår.GIFT_PARTNERSKAP }?.resultat == Resultat.IKKE_OPPFYLT) { + return "Barnet (fødselsdato: ${barn.fødselsdato.tilKortString()}) er gift." + } + + if (vilkårsresultat?.find { it.vilkårType == Vilkår.BOSATT_I_RIKET }?.resultat == Resultat.IKKE_OPPFYLT) { + return "Barnet (fødselsdato: ${barn.fødselsdato.tilKortString()}) er ikke bosatt i riket." + } + } + + logger.error("Fant ikke begrunnelse for at fødselshendelse ikke kunne automatisk behandles.") + + return "" + } + + companion object { + private val logger = LoggerFactory.getLogger(BehandleFødselshendelseTask::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/Evaluering.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/Evaluering.kt" new file mode 100644 index 000000000..2e9a9dbac --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/Evaluering.kt" @@ -0,0 +1,48 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse + +interface EvalueringÅrsak { + + fun hentBeskrivelse(): String + + fun hentMetrikkBeskrivelse(): String + + fun hentIdentifikator(): String +} + +data class Evaluering( + val resultat: Resultat, + val evalueringÅrsaker: List, + val begrunnelse: String, + val beskrivelse: String = "", + val identifikator: String = "", +) { + + companion object { + + fun oppfylt(evalueringÅrsak: EvalueringÅrsak) = Evaluering( + Resultat.OPPFYLT, + listOf(evalueringÅrsak), + evalueringÅrsak.hentBeskrivelse(), + ) + + fun ikkeOppfylt(evalueringÅrsak: EvalueringÅrsak) = Evaluering( + Resultat.IKKE_OPPFYLT, + listOf(evalueringÅrsak), + evalueringÅrsak.hentBeskrivelse(), + ) + + fun ikkeVurdert(evalueringÅrsak: EvalueringÅrsak) = Evaluering( + Resultat.IKKE_VURDERT, + listOf(evalueringÅrsak), + evalueringÅrsak.hentBeskrivelse(), + ) + } +} + +enum class Resultat { + OPPFYLT, + IKKE_OPPFYLT, + IKKE_VURDERT, +} + +fun List.erOppfylt() = this.all { it.resultat == Resultat.OPPFYLT } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/FlerlingUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/FlerlingUtils.kt" new file mode 100644 index 000000000..b6c2b7810 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/FlerlingUtils.kt" @@ -0,0 +1,47 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse + +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import org.slf4j.Logger + +fun finnBarnSomSkalBehandlesForMor( + nyBehandlingHendelse: NyBehandlingHendelse, + barnaTilMor: List, + barnaSomHarBlittBehandlet: List, + secureLogger: Logger? = null, +): Pair, List> { + val barnaPåHendelse = + barnaTilMor.filter { nyBehandlingHendelse.barnasIdenter.contains(it.aktør.aktivFødselsnummer()) } + val andreBarnFødtInnenEnDag = barnaTilMor.filter { + barnaPåHendelse.any { barnPåHendelse -> + barnPåHendelse.aktør != it.aktør && + ( + barnPåHendelse.fødselsdato == it.fødselsdato || + barnPåHendelse.fødselsdato?.plusDays(1) == it.fødselsdato || + barnPåHendelse.fødselsdato?.minusDays(1) == it.fødselsdato + ) + } + } + + val alleBarnSomKanBehandles = (barnaPåHendelse + andreBarnFødtInnenEnDag).map { it.aktør.aktivFødselsnummer() } + val barnSomSkalBehandlesForMor = alleBarnSomKanBehandles + .filter { !barnaSomHarBlittBehandlet.contains(it) } + + secureLogger?.info( + "Behandler fødselshendelse på ${nyBehandlingHendelse.morsIdent}. " + + "Alle barna til mor: ${barnaTilMor.map { it.toSecureString() }}\n" + + "Barn på hendelse: ${barnaPåHendelse.map { it.aktør.aktivFødselsnummer() }}\n" + + "Barn med tilstøtende fødselsdato som også behandles: ${andreBarnFødtInnenEnDag.map { it.aktør.aktivFødselsnummer() }}\n" + + "Barn som faktisk skal behandles for mor: ${barnSomSkalBehandlesForMor.map { it }}", + ) + + return Pair(barnSomSkalBehandlesForMor, alleBarnSomKanBehandles) +} + +fun barnPåHendelseBlirAlleredeBehandletIÅpenBehandling( + barnaPåHendelse: List, + barnaPåÅpenBehandling: List, +): Boolean { + return barnaPåHendelse.all { barnaPåÅpenBehandling.contains(it) } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/VelgFagSystemService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/VelgFagSystemService.kt" new file mode 100644 index 000000000..7d4606f74 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/VelgFagSystemService.kt" @@ -0,0 +1,162 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdService +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.FagsystemRegelVurdering.SEND_TIL_BA +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.FagsystemRegelVurdering.SEND_TIL_INFOTRYGD +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.FagsystemUtfall.FAGSAK_UTEN_IVERKSATTE_BEHANDLINGER_I_BA_SAK +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.FagsystemUtfall.IVERKSATTE_BEHANDLINGER_I_BA_SAK +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.FagsystemUtfall.LØPENDE_SAK_I_INFOTRYGD +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.FagsystemUtfall.MOR_IKKE_GYLDIG_MEDLEMSKAP_FOR_AUTOMATISK_VURDERING +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.FagsystemUtfall.SAKER_I_INFOTRYGD_MEN_IKKE_LØPENDE_UTBETALINGER +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.FagsystemUtfall.STØTTET_I_BA_SAK +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.FagsystemUtfall.values +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap.EØS +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap.NORDEN +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap.STATSLØS +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap.TREDJELANDSBORGER +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap.UKJENT +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.StatsborgerskapService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class VelgFagSystemService( + private val fagsakService: FagsakService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val infotrygdService: InfotrygdService, + private val personidentService: PersonidentService, + private val personopplysningerService: PersonopplysningerService, + private val statsborgerskapService: StatsborgerskapService, +) { + + val utfallForValgAvFagsystem = mutableMapOf() + + init { + values().forEach { + utfallForValgAvFagsystem[it] = Metrics.counter( + "familie.ba.sak.velgfagsystem", + "navn", + it.name, + "beskrivelse", + it.beskrivelse, + ) + } + } + + internal fun morHarLøpendeEllerTidligereUtbetalinger(fagsak: Fagsak?): Boolean { + return if (fagsak == null) { + false + } else if (behandlingHentOgPersisterService.hentBehandlinger(fagsakId = fagsak.id) + .any { it.status == BehandlingStatus.UTREDES } + ) { + true + } else { + behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId = fagsak.id) != null + } + } + + internal fun morHarSakerMenIkkeLøpendeIInfotrygd(morsIdent: String): Boolean { + val stønader = infotrygdService.hentInfotrygdstønaderForSøker(morsIdent, historikk = false).bruker + return stønader.isNotEmpty() + } + + internal fun morEllerBarnHarLøpendeSakIInfotrygd(morsIdent: String, barnasIdenter: List): Boolean { + val morsIdenter = personidentService.hentIdenter(personIdent = morsIdent, historikk = true) + .filter { it.gruppe == "FOLKEREGISTERIDENT" } + .map { it.ident } + val alleBarnasIdenter = barnasIdenter.flatMap { + personidentService.hentIdenter(personIdent = it, historikk = true) + .filter { identinfo -> identinfo.gruppe == "FOLKEREGISTERIDENT" } + .map { identinfo -> identinfo.ident } + } + + return infotrygdService.harLøpendeSakIInfotrygd(morsIdenter, alleBarnasIdenter) + } + + internal fun harMorGyldigStatsborgerskapForAutomatiskVurdering(morsAktør: Aktør): Boolean { + val gjeldendeStatsborgerskap = personopplysningerService.hentGjeldendeStatsborgerskap(morsAktør) + val medlemskap = statsborgerskapService.hentSterkesteMedlemskap(statsborgerskap = gjeldendeStatsborgerskap) + + secureLogger.info( + "Gjeldende statsborgerskap for ${morsAktør.aktivFødselsnummer()}=" + + "(${gjeldendeStatsborgerskap.land}, bekreftelsesdato=${gjeldendeStatsborgerskap.bekreftelsesdato}, gyldigFom=${gjeldendeStatsborgerskap.gyldigFraOgMed}, gyldigTom=${gjeldendeStatsborgerskap.gyldigTilOgMed}), " + + "medlemskap=$medlemskap", + ) + + return when (medlemskap) { + NORDEN, TREDJELANDSBORGER, STATSLØS, EØS -> true + UKJENT, null -> false + } + } + + fun velgFagsystem(nyBehandlingHendelse: NyBehandlingHendelse): Pair { + val morsAktør = personidentService.hentAktør(nyBehandlingHendelse.morsIdent) + + val fagsak = fagsakService.hentNormalFagsak(morsAktør) + + val (fagsystemUtfall: FagsystemUtfall, fagsystem: FagsystemRegelVurdering) = when { + morHarLøpendeEllerTidligereUtbetalinger(fagsak) -> Pair( + IVERKSATTE_BEHANDLINGER_I_BA_SAK, + SEND_TIL_BA, + ) + morEllerBarnHarLøpendeSakIInfotrygd( + nyBehandlingHendelse.morsIdent, + nyBehandlingHendelse.barnasIdenter, + ) -> Pair( + LØPENDE_SAK_I_INFOTRYGD, + SEND_TIL_INFOTRYGD, + ) + fagsak != null -> Pair( + FAGSAK_UTEN_IVERKSATTE_BEHANDLINGER_I_BA_SAK, + SEND_TIL_BA, + ) + morHarSakerMenIkkeLøpendeIInfotrygd(nyBehandlingHendelse.morsIdent) -> Pair( + SAKER_I_INFOTRYGD_MEN_IKKE_LØPENDE_UTBETALINGER, + SEND_TIL_INFOTRYGD, + ) + !harMorGyldigStatsborgerskapForAutomatiskVurdering( + morsAktør, + ) -> Pair( + MOR_IKKE_GYLDIG_MEDLEMSKAP_FOR_AUTOMATISK_VURDERING, + SEND_TIL_INFOTRYGD, + ) + else -> Pair( + STØTTET_I_BA_SAK, + SEND_TIL_BA, + ) + } + + secureLogger.info("Sender fødselshendelse for ${nyBehandlingHendelse.morsIdent} til $fagsystem med utfall $fagsystemUtfall") + utfallForValgAvFagsystem[fagsystemUtfall]?.increment() + return Pair(fagsystem, fagsystemUtfall) + } + + companion object { + val logger = LoggerFactory.getLogger(VelgFagSystemService::class.java) + } +} + +enum class FagsystemRegelVurdering { + SEND_TIL_BA, + SEND_TIL_INFOTRYGD, +} + +enum class FagsystemUtfall(val beskrivelse: String) { + IVERKSATTE_BEHANDLINGER_I_BA_SAK("Mor har fagsak med tidligere eller løpende utbetalinger i ba-sak"), + LØPENDE_SAK_I_INFOTRYGD("Mor har løpende sak i infotrygd"), + FAGSAK_UTEN_IVERKSATTE_BEHANDLINGER_I_BA_SAK("Mor har fagsak uten iverksatte behandlinger"), + SAKER_I_INFOTRYGD_MEN_IKKE_LØPENDE_UTBETALINGER("Mor har saker i infotrygd, men ikke løpende utbetalinger"), + MOR_IKKE_GYLDIG_MEDLEMSKAP_FOR_AUTOMATISK_VURDERING("Mor har ikke gyldig medlemskap for automatisk vurdering"), + STØTTET_I_BA_SAK("Person kan automatisk vurderes i ba-sak"), +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/Filtreringsregel.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/Filtreringsregel.kt" new file mode 100644 index 000000000..2a486307e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/Filtreringsregel.kt" @@ -0,0 +1,202 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Evaluering +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.utfall.FiltreringsregelIkkeOppfylt +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.utfall.FiltreringsregelOppfylt +import java.time.temporal.ChronoUnit +import kotlin.math.abs + +enum class Filtreringsregel(val vurder: FiltreringsreglerFakta.() -> Evaluering) { + MOR_GYLDIG_FNR(vurder = { FiltreringsregelEvaluering.morHarGyldigFnr(this) }), + BARN_GYLDIG_FNR(vurder = { FiltreringsregelEvaluering.barnHarGyldigFnr(this) }), + MOR_LEVER(vurder = { FiltreringsregelEvaluering.morLever(this) }), + BARN_LEVER(vurder = { FiltreringsregelEvaluering.barnLever(this) }), + MER_ENN_5_MND_SIDEN_FORRIGE_BARN(vurder = { + FiltreringsregelEvaluering.merEnn5mndEllerMindreEnnFemDagerSidenForrigeBarn( + this, + ) + }), + MOR_ER_OVER_18_ÅR(vurder = { FiltreringsregelEvaluering.morErOver18år(this) }), + MOR_HAR_IKKE_VERGE(vurder = { FiltreringsregelEvaluering.morHarIkkeVerge(this) }), + MOR_MOTTAR_IKKE_LØPENDE_UTVIDET(vurder = { FiltreringsregelEvaluering.morMottarIkkeLøpendeUtvidet(this) }), + MOR_HAR_IKKE_LØPENDE_EØS_BARNETRYGD(vurder = { FiltreringsregelEvaluering.morHarIkkeLøpendeEøsBarnetrygd(this) }), + FAGSAK_IKKE_MIGRERT_UT_AV_INFOTRYGD_ETTER_BARN_FØDT(vurder = { + FiltreringsregelEvaluering.fagsakIkkeMigrertEtterBarnBleFødt( + this, + ) + }), + LØPER_IKKE_BARNETRYGD_FOR_BARNET(vurder = { FiltreringsregelEvaluering.løperIkkeBarnetrygdPåAnnenForelder(this) }), + MOR_HAR_IKKE_OPPFYLT_UTVIDET_VILKÅR_VED_FØDSELSDATO(vurder = { + FiltreringsregelEvaluering.morOppfyllerIkkeVilkårForUtvidetBarnetrygd( + this, + ) + }), + MOR_HAR_IKKE_OPPHØRT_BARNETRYGD(vurder = { FiltreringsregelEvaluering.morHarIkkeOpphørtBarnetrygd(this) }), +} + +object FiltreringsregelEvaluering { + fun evaluerFiltreringsregler(fakta: FiltreringsreglerFakta) = Filtreringsregel.values() + .fold(mutableListOf()) { acc, filtreringsregel -> + if (acc.any { it.resultat == Resultat.IKKE_OPPFYLT }) { + acc.add( + Evaluering( + resultat = Resultat.IKKE_VURDERT, + identifikator = filtreringsregel.name, + begrunnelse = "Ikke vurdert", + evalueringÅrsaker = emptyList(), + ), + ) + } else { + acc.add(filtreringsregel.vurder(fakta).copy(identifikator = filtreringsregel.name)) + } + + acc + } + + fun morHarGyldigFnr(fakta: FiltreringsreglerFakta): Evaluering { + val erMorFnrGyldig = + (!erBostNummer(fakta.mor.aktør.aktivFødselsnummer()) && !erFDatnummer(fakta.mor.aktør.aktivFødselsnummer())) + + return if (erMorFnrGyldig) { + Evaluering.oppfylt(FiltreringsregelOppfylt.MOR_HAR_GYLDIG_FNR) + } else { + Evaluering.ikkeOppfylt( + FiltreringsregelIkkeOppfylt.MOR_HAR_UGYLDIG_FNR, + ) + } + } + + fun barnHarGyldigFnr(fakta: FiltreringsreglerFakta): Evaluering { + val erbarnFnrGyldig = + fakta.barnaFraHendelse.all { (!erBostNummer(it.aktør.aktivFødselsnummer()) && !erFDatnummer(it.aktør.aktivFødselsnummer())) } + + return if (erbarnFnrGyldig) { + Evaluering.oppfylt(FiltreringsregelOppfylt.BARN_HAR_GYLDIG_FNR) + } else { + Evaluering.ikkeOppfylt( + FiltreringsregelIkkeOppfylt.BARN_HAR_UGYLDIG_FNR, + ) + } + } + + fun morErOver18år(fakta: FiltreringsreglerFakta): Evaluering = if (fakta.mor.hentAlder() >= 18) { + Evaluering.oppfylt( + FiltreringsregelOppfylt.MOR_ER_OVER_18_ÅR, + ) + } else { + Evaluering.ikkeOppfylt(FiltreringsregelIkkeOppfylt.MOR_ER_UNDER_18_ÅR) + } + + fun morLever(fakta: FiltreringsreglerFakta): Evaluering = + if (fakta.morLever) { + Evaluering.oppfylt(FiltreringsregelOppfylt.MOR_LEVER) + } else { + Evaluering.ikkeOppfylt( + FiltreringsregelIkkeOppfylt.MOR_LEVER_IKKE, + ) + } + + fun barnLever(fakta: FiltreringsreglerFakta): Evaluering = + if (fakta.barnaLever) { + Evaluering.oppfylt(FiltreringsregelOppfylt.BARNET_LEVER) + } else { + Evaluering.ikkeOppfylt( + FiltreringsregelIkkeOppfylt.BARNET_LEVER_IKKE, + ) + } + + fun morHarIkkeVerge(fakta: FiltreringsreglerFakta): Evaluering = if (!fakta.morHarVerge) { + Evaluering.oppfylt( + FiltreringsregelOppfylt.MOR_ER_MYNDIG, + ) + } else { + Evaluering.ikkeOppfylt( + FiltreringsregelIkkeOppfylt.MOR_ER_UNDER_VERGEMÅL, + ) + } + + fun morMottarIkkeLøpendeUtvidet(fakta: FiltreringsreglerFakta): Evaluering = + if (!fakta.morMottarLøpendeUtvidet) { + Evaluering.oppfylt( + FiltreringsregelOppfylt.MOR_MOTTAR_IKKE_LØPENDE_UTVIDET, + ) + } else { + Evaluering.ikkeOppfylt( + FiltreringsregelIkkeOppfylt.MOR_MOTTAR_LØPENDE_UTVIDET, + ) + } + + fun morOppfyllerIkkeVilkårForUtvidetBarnetrygd(fakta: FiltreringsreglerFakta): Evaluering = + if (!fakta.morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato) { + Evaluering.oppfylt(FiltreringsregelOppfylt.MOR_OPPFYLLER_IKKE_VILKÅR_FOR_UTVIDET_BARNETRYGD_VED_FØDSELSDATO) + } else { + Evaluering.ikkeOppfylt(FiltreringsregelIkkeOppfylt.MOR_OPPFYLLER_VILKÅR_FOR_UTVIDET_BARNETRYGD_VED_FØDSELSDATO) + } + + fun morHarIkkeLøpendeEøsBarnetrygd(fakta: FiltreringsreglerFakta): Evaluering = + if (!fakta.morMottarEøsBarnetrygd) { + Evaluering.oppfylt( + FiltreringsregelOppfylt.MOR_HAR_IKKE_LØPENDE_EØS_BARNETRYGD, + ) + } else { + Evaluering.ikkeOppfylt( + FiltreringsregelIkkeOppfylt.MOR_HAR_LØPENDE_EØS_BARNETRYGD, + ) + } + + fun fagsakIkkeMigrertEtterBarnBleFødt(fakta: FiltreringsreglerFakta): Evaluering = + if (!fakta.erFagsakenMigrertEtterBarnFødt) { + Evaluering.oppfylt( + FiltreringsregelOppfylt.FAGSAK_IKKE_MIGRERT_UT_AV_INFOTRYGD_ETTER_BARN_FØDT, + ) + } else { + Evaluering.ikkeOppfylt( + FiltreringsregelIkkeOppfylt.FAGSAK_MIGRERT_UT_AV_INFOTRYGD_ETTER_BARN_FØDT, + ) + } + + fun løperIkkeBarnetrygdPåAnnenForelder(fakta: FiltreringsreglerFakta): Evaluering = + if (!fakta.løperBarnetrygdForBarnetPåAnnenForelder) { + Evaluering.oppfylt( + FiltreringsregelOppfylt.LØPER_IKKE_BARNETRYGD_FOR_BARNET, + ) + } else { + Evaluering.ikkeOppfylt( + FiltreringsregelIkkeOppfylt.LØPER_ALLEREDE_FOR_ANNEN_FORELDER, + ) + } + + fun morHarIkkeOpphørtBarnetrygd(fakta: FiltreringsreglerFakta): Evaluering = if (fakta.morHarIkkeOpphørtBarnetrygd) { + Evaluering.oppfylt(FiltreringsregelOppfylt.MOR_HAR_IKKE_OPPHØRT_BARNETRYGD) + } else { + Evaluering.ikkeOppfylt(FiltreringsregelIkkeOppfylt.MOR_HAR_OPPHØRT_BARNETRYGD) + } + + fun merEnn5mndEllerMindreEnnFemDagerSidenForrigeBarn(fakta: FiltreringsreglerFakta): Evaluering { + return when ( + fakta.barnaFraHendelse.all { barnFraHendelse -> + fakta.restenAvBarna.all { + abs(ChronoUnit.MONTHS.between(barnFraHendelse.fødselsdato, it.fødselsdato)) > 5 || + abs(ChronoUnit.DAYS.between(barnFraHendelse.fødselsdato, it.fødselsdato)) <= 6 + } + } + ) { + true -> Evaluering.oppfylt(FiltreringsregelOppfylt.MER_ENN_5_MND_SIDEN_FORRIGE_BARN_UTFALL) + false -> Evaluering.ikkeOppfylt(FiltreringsregelIkkeOppfylt.MINDRE_ENN_5_MND_SIDEN_FORRIGE_BARN_UTFALL) + } + } +} + +internal fun erFDatnummer(personIdent: String): Boolean { + return personIdent.substring(6).toInt() == 0 +} + +/** + * BOST-nr har måned mellom 21 og 32 + */ +internal fun erBostNummer(personIdent: String): Boolean { + personIdent.substring(2, 4).toInt().also { måned -> + return måned in 21..32 + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsreglerFakta.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsreglerFakta.kt" new file mode 100644 index 000000000..dbe7a8139 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsreglerFakta.kt" @@ -0,0 +1,22 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler + +import com.fasterxml.jackson.annotation.JsonIgnore +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import java.time.LocalDate + +data class FiltreringsreglerFakta( + val mor: Person, + val morMottarLøpendeUtvidet: Boolean = false, + val morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato: Boolean, + val morMottarEøsBarnetrygd: Boolean = false, + val barnaFraHendelse: List, + val restenAvBarna: List, + val morLever: Boolean, + val barnaLever: Boolean, + val morHarVerge: Boolean, + val erFagsakenMigrertEtterBarnFødt: Boolean, + val løperBarnetrygdForBarnetPåAnnenForelder: Boolean, + val morHarIkkeOpphørtBarnetrygd: Boolean, + @JsonIgnore val dagensDato: LocalDate = LocalDate.now(), +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsreglerService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsreglerService.kt" new file mode 100644 index 000000000..6db3f64c2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsreglerService.kt" @@ -0,0 +1,209 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.LocalDateService +import no.nav.familie.ba.sak.common.convertDataClassToJson +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Evaluering +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.erOppfylt +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.domene.FødselshendelsefiltreringResultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.domene.FødselshendelsefiltreringResultatRepository +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValideringService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.YearMonth + +@Service +class FiltreringsreglerService( + private val personopplysningerService: PersonopplysningerService, + private val personidentService: PersonidentService, + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + private val vilkårsvurderingRepository: VilkårsvurderingRepository, + private val localDateService: LocalDateService, + private val fødselshendelsefiltreringResultatRepository: FødselshendelsefiltreringResultatRepository, + private val behandlingService: BehandlingService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val tilkjentYtelseValideringService: TilkjentYtelseValideringService, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, +) { + + val filtreringsreglerMetrics = mutableMapOf() + val filtreringsreglerFørsteUtfallMetrics = mutableMapOf() + + init { + Filtreringsregel.values().map { + Resultat.values().forEach { resultat -> + filtreringsreglerMetrics["${it.name}_${resultat.name}"] = + Metrics.counter( + "familie.ba.sak.filtreringsregler.utfall", + "beskrivelse", + it.name, + "resultat", + resultat.name, + ) + + filtreringsreglerFørsteUtfallMetrics[it.name] = + Metrics.counter( + "familie.ba.sak.filtreringsregler.foersteutfall", + "beskrivelse", + it.name, + ) + } + } + } + + fun lagreFiltreringsregler( + evalueringer: List, + behandlingId: Long, + fakta: FiltreringsreglerFakta, + ): List { + return fødselshendelsefiltreringResultatRepository.saveAll( + evalueringer.map { + FødselshendelsefiltreringResultat( + behandlingId = behandlingId, + filtreringsregel = Filtreringsregel.valueOf(it.identifikator), + resultat = it.resultat, + begrunnelse = it.begrunnelse, + evalueringsårsaker = it.evalueringÅrsaker.map { evalueringÅrsak -> evalueringÅrsak.toString() }, + regelInput = fakta.convertDataClassToJson(), + ) + }, + ) + } + + fun hentFødselshendelsefiltreringResultater(behandlingId: Long): List { + return fødselshendelsefiltreringResultatRepository.finnFødselshendelsefiltreringResultater(behandlingId = behandlingId) + } + + fun kjørFiltreringsregler( + nyBehandlingHendelse: NyBehandlingHendelse, + behandling: Behandling, + ): List { + val morsAktørId = personidentService.hentAktør(nyBehandlingHendelse.morsIdent) + val barnasAktørId = personidentService.hentAktørIder(nyBehandlingHendelse.barnasIdenter) + + val personopplysningGrunnlag = personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandling.id) + ?: throw IllegalStateException("Fant ikke personopplysninggrunnlag for behandling ${behandling.id}") + + val barnaFraHendelse = personopplysningGrunnlag.barna.filter { barnasAktørId.contains(it.aktør) } + + val migreringsdatoPåFagsak = behandlingService.hentMigreringsdatoPåFagsak(behandling.fagsak.id) + + val sisteBehandling = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = behandling.fagsak.id) + val andelerPåSisteBehandling = sisteBehandling?.let { + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId = it.id) + } ?: emptyList() + val sisteMånedMedBarnetrygd = andelerPåSisteBehandling.maxOfOrNull { it.stønadTom } + val harAndelerFremoverITid = sisteMånedMedBarnetrygd != null && sisteMånedMedBarnetrygd > YearMonth.now() + + val fakta = FiltreringsreglerFakta( + mor = personopplysningGrunnlag.søker, + morMottarLøpendeUtvidet = behandling.underkategori == BehandlingUnderkategori.UTVIDET, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato( + behandling, + barnaFraHendelse, + ), + morMottarEøsBarnetrygd = behandling.kategori == BehandlingKategori.EØS, + barnaFraHendelse = barnaFraHendelse, + restenAvBarna = finnRestenAvBarnasPersonInfo(morsAktørId, barnaFraHendelse), + morLever = !personopplysningGrunnlag.søker.erDød(), + barnaLever = personopplysningGrunnlag.barna.none { it.erDød() }, + morHarVerge = personopplysningerService.harVerge(morsAktørId).harVerge, + dagensDato = localDateService.now(), + erFagsakenMigrertEtterBarnFødt = erSakenMigrertEtterBarnFødt( + barnaFraHendelse, + migreringsdatoPåFagsak, + ), + løperBarnetrygdForBarnetPåAnnenForelder = tilkjentYtelseValideringService.barnetrygdLøperForAnnenForelder( + behandling = behandling, + barna = barnaFraHendelse, + ), + morHarIkkeOpphørtBarnetrygd = andelerPåSisteBehandling.isEmpty() || harAndelerFremoverITid, + ) + val evalueringer = FiltreringsregelEvaluering.evaluerFiltreringsregler(fakta) + oppdaterMetrikker(evalueringer) + + logger.info("Resultater fra filtreringsregler på behandling $behandling: ${evalueringer.map { "${it.identifikator}: ${it.resultat}" }}") + if (!evalueringer.erOppfylt()) { + secureLogger.info("Resultater fra filtreringsregler på behandling $behandling: (Fakta: ${fakta.convertDataClassToJson()}): ${evalueringer.map { "${it.identifikator}: ${it.resultat}" }}") + } + + return lagreFiltreringsregler( + evalueringer = evalueringer, + behandlingId = behandling.id, + fakta = fakta, + ) + } + + private fun erSakenMigrertEtterBarnFødt( + barnaFraHendelse: List, + migreringsdatoForFagsak: LocalDate?, + ): Boolean = migreringsdatoForFagsak?.isAfter(barnaFraHendelse.minOf { it.fødselsdato }) == true + + private fun finnRestenAvBarnasPersonInfo(morsAktørId: Aktør, barnaFraHendelse: List): List { + return personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(morsAktørId).forelderBarnRelasjon.filter { + it.relasjonsrolle == FORELDERBARNRELASJONROLLE.BARN && barnaFraHendelse.none { barn -> barn.aktør == it.aktør } + }.map { + personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(it.aktør) + } + } + + private fun økTellereForFørsteUtfall(evaluering: Evaluering, førsteutfall: Boolean): Boolean { + if (evaluering.resultat == Resultat.IKKE_OPPFYLT && førsteutfall) { + filtreringsreglerFørsteUtfallMetrics[evaluering.identifikator]!!.increment() + return false + } + return førsteutfall + } + + private fun oppdaterMetrikker(evalueringer: List) { + var førsteutfall = true + evalueringer.forEach { + filtreringsreglerMetrics["${it.identifikator}_${it.resultat.name}"]!!.increment() + førsteutfall = økTellereForFørsteUtfall(it, førsteutfall) + } + } + + private fun morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato( + behandling: Behandling, + barnaFraHendelse: List, + ): Boolean { + val forrigeVedtatteBehandling = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(behandling.fagsak.id) + return forrigeVedtatteBehandling?.let { vedtattBehandling -> + vilkårsvurderingRepository.findByBehandlingAndAktiv(vedtattBehandling.id)?.let { vilkårsvurdering -> + vilkårsvurdering.personResultater.single { personResultat -> personResultat.erSøkersResultater() }.vilkårResultater.any { vilkårResultat -> + vilkårResultat.vilkårType == Vilkår.UTVIDET_BARNETRYGD && vilkårResultat.erOppfylt() && barnaFraHendelse.any { barnFraHendelse -> + vilkårResultat.periodeTom?.isAfter(barnFraHendelse.fødselsdato) ?: true && + vilkårResultat.periodeFom!!.isBefore(barnFraHendelse.fødselsdato.plusYears(18)) + } + } + } ?: false + } ?: false + } + + companion object { + + val logger = LoggerFactory.getLogger(FiltreringsreglerService::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/domene/F\303\270dselshendelsefiltreringResultat.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/domene/F\303\270dselshendelsefiltreringResultat.kt" new file mode 100644 index 000000000..139710c0f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/domene/F\303\270dselshendelsefiltreringResultat.kt" @@ -0,0 +1,65 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.domene + +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.StringListConverter +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.Filtreringsregel +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "FødselshendelsefiltreringResultat") +@Table(name = "FOEDSELSHENDELSEFILTRERING_RESULTAT") +class FødselshendelsefiltreringResultat( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "foedselshendelsefiltrering_resultat_seq_generator") + @SequenceGenerator( + name = "foedselshendelsefiltrering_resultat_seq_generator", + sequenceName = "foedselshendelsefiltrering_resultat_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "fk_behandling_id", nullable = false, updatable = false, unique = true) + val behandlingId: Long, + + @Enumerated(EnumType.STRING) + @Column(name = "filtreringsregel") + val filtreringsregel: Filtreringsregel, + + @Enumerated(EnumType.STRING) + @Column(name = "resultat") + val resultat: Resultat, + + @Column(name = "begrunnelse", columnDefinition = "TEXT", nullable = false) + val begrunnelse: String, + + @Column(name = "evalueringsaarsaker") + @Convert(converter = StringListConverter::class) + val evalueringsårsaker: List = emptyList(), + + @Column(name = "regel_input", columnDefinition = "TEXT") + val regelInput: String? = null, +) : BaseEntitet() { + + override fun toString(): String { + return "FødselshendelsefiltreringResultat(" + + "id=$id, " + + "filtreringsregel=$filtreringsregel, " + + "resultat=$resultat, " + + "evalueringÅrsaker=$evalueringsårsaker" + + ")" + } +} + +fun List.erOppfylt() = this.all { it.resultat == Resultat.OPPFYLT } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/domene/F\303\270dselshendelsefiltreringResultatRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/domene/F\303\270dselshendelsefiltreringResultatRepository.kt" new file mode 100644 index 000000000..276432a33 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/domene/F\303\270dselshendelsefiltreringResultatRepository.kt" @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.domene + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface FødselshendelsefiltreringResultatRepository : JpaRepository { + + @Query(value = "SELECT f FROM FødselshendelsefiltreringResultat f WHERE f.behandlingId = :behandlingId") + fun finnFødselshendelsefiltreringResultater(behandlingId: Long): List +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/utfall/FiltreringsregelIkkeOppfylt.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/utfall/FiltreringsregelIkkeOppfylt.kt" new file mode 100644 index 000000000..cb470da97 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/utfall/FiltreringsregelIkkeOppfylt.kt" @@ -0,0 +1,53 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.utfall + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.EvalueringÅrsak +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.Filtreringsregel + +enum class FiltreringsregelIkkeOppfylt(val beskrivelse: String, private val filtreringsregel: Filtreringsregel) : + EvalueringÅrsak { + + MOR_HAR_UGYLDIG_FNR("Mor har ugyldig fødselsnummer", Filtreringsregel.MOR_GYLDIG_FNR), + BARN_HAR_UGYLDIG_FNR("Barn har ugyldig fødselsnummer", Filtreringsregel.BARN_GYLDIG_FNR), + MOR_ER_UNDER_18_ÅR("Mor er under 18 år.", Filtreringsregel.MOR_ER_OVER_18_ÅR), + MOR_ER_UNDER_VERGEMÅL("Mor er under vergemål.", Filtreringsregel.MOR_HAR_IKKE_VERGE), + MOR_MOTTAR_LØPENDE_UTVIDET("Mor mottar utvidet barnetrygd.", Filtreringsregel.MOR_MOTTAR_IKKE_LØPENDE_UTVIDET), + MOR_LEVER_IKKE("Det er registrert dødsdato på mor.", Filtreringsregel.MOR_LEVER), + BARNET_LEVER_IKKE("Det er registrert dødsdato på barnet.", Filtreringsregel.BARN_LEVER), + MINDRE_ENN_5_MND_SIDEN_FORRIGE_BARN_UTFALL( + "Det har gått mindre enn fem måneder siden forrige barn ble født.", + Filtreringsregel.MER_ENN_5_MND_SIDEN_FORRIGE_BARN, + ), + FAGSAK_MIGRERT_UT_AV_INFOTRYGD_ETTER_BARN_FØDT( + "Fagsaken ble migrert fra infotrygd etter barn ble født.", + Filtreringsregel.FAGSAK_IKKE_MIGRERT_UT_AV_INFOTRYGD_ETTER_BARN_FØDT, + ), + LØPER_ALLEREDE_FOR_ANNEN_FORELDER( + "Annen mottaker har barnetrygd for barnet", + Filtreringsregel.LØPER_IKKE_BARNETRYGD_FOR_BARNET, + ), + MOR_HAR_LØPENDE_EØS_BARNETRYGD( + "Mor har EØS-barnetrygd", + Filtreringsregel.MOR_HAR_IKKE_LØPENDE_EØS_BARNETRYGD, + ), + MOR_OPPFYLLER_VILKÅR_FOR_UTVIDET_BARNETRYGD_VED_FØDSELSDATO( + "Mor oppfyller vilkår for utvidet barnetrygd", + Filtreringsregel.MOR_HAR_IKKE_OPPFYLT_UTVIDET_VILKÅR_VED_FØDSELSDATO, + ), + MOR_HAR_OPPHØRT_BARNETRYGD( + "Mor har vedtak om opphørt barnetrygd.", + Filtreringsregel.MOR_HAR_IKKE_OPPHØRT_BARNETRYGD, + ), + ; + + override fun hentBeskrivelse(): String { + return beskrivelse + } + + override fun hentMetrikkBeskrivelse(): String { + return beskrivelse + } + + override fun hentIdentifikator(): String { + return filtreringsregel.name + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/utfall/FiltreringsregelOppfylt.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/utfall/FiltreringsregelOppfylt.kt" new file mode 100644 index 000000000..f72d61908 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/utfall/FiltreringsregelOppfylt.kt" @@ -0,0 +1,56 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.utfall + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.EvalueringÅrsak +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.Filtreringsregel + +enum class FiltreringsregelOppfylt(val beskrivelse: String, private val filtreringsregel: Filtreringsregel) : + EvalueringÅrsak { + + MOR_HAR_GYLDIG_FNR("Mor har gyldig fødselsnummer", Filtreringsregel.MOR_GYLDIG_FNR), + BARN_HAR_GYLDIG_FNR("Barn har gyldig fødselsnummer", Filtreringsregel.BARN_GYLDIG_FNR), + MOR_ER_OVER_18_ÅR("Mor er over 18 år.", Filtreringsregel.MOR_ER_OVER_18_ÅR), + MOR_ER_MYNDIG("Mor er myndig.", Filtreringsregel.MOR_HAR_IKKE_VERGE), + MOR_MOTTAR_IKKE_LØPENDE_UTVIDET( + "Mor mottar ikke utvidet barnetrygd.", + Filtreringsregel.MOR_MOTTAR_IKKE_LØPENDE_UTVIDET, + ), + MOR_LEVER("Det er ikke registrert dødsdato på mor.", Filtreringsregel.MOR_LEVER), + BARNET_LEVER("Det er ikke registrert dødsdato på barnet.", Filtreringsregel.BARN_LEVER), + MER_ENN_5_MND_SIDEN_FORRIGE_BARN_UTFALL( + "Det har gått mer enn fem måneder siden forrige barn ble født.", + Filtreringsregel.MER_ENN_5_MND_SIDEN_FORRIGE_BARN, + ), + FAGSAK_IKKE_MIGRERT_UT_AV_INFOTRYGD_ETTER_BARN_FØDT( + "Fagsaken har ikke blitt migrert fra infotrygd etter barn ble født.", + Filtreringsregel.FAGSAK_IKKE_MIGRERT_UT_AV_INFOTRYGD_ETTER_BARN_FØDT, + ), + LØPER_IKKE_BARNETRYGD_FOR_BARNET( + "Det løper ikke barnetrygd for barnet på annen forelder", + Filtreringsregel.LØPER_IKKE_BARNETRYGD_FOR_BARNET, + ), + MOR_HAR_IKKE_LØPENDE_EØS_BARNETRYGD( + "Mor har ikke løpende EØS-barnetrygd", + Filtreringsregel.MOR_HAR_IKKE_LØPENDE_EØS_BARNETRYGD, + ), + MOR_OPPFYLLER_IKKE_VILKÅR_FOR_UTVIDET_BARNETRYGD_VED_FØDSELSDATO( + "Mor oppfyller ikke vilkår for utvidet barnetrygd", + Filtreringsregel.MOR_HAR_IKKE_OPPFYLT_UTVIDET_VILKÅR_VED_FØDSELSDATO, + ), + MOR_HAR_IKKE_OPPHØRT_BARNETRYGD( + "Mor har ikke opphørt barnetrygd", + Filtreringsregel.MOR_HAR_IKKE_OPPHØRT_BARNETRYGD, + ), + ; + + override fun hentBeskrivelse(): String { + return beskrivelse + } + + override fun hentMetrikkBeskrivelse(): String { + return beskrivelse + } + + override fun hentIdentifikator(): String { + return filtreringsregel.name + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/gdpr/domene/F\303\270dselshendelsePreLansering.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/gdpr/domene/F\303\270dselshendelsePreLansering.kt" new file mode 100644 index 000000000..fb8691949 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/gdpr/domene/F\303\270dselshendelsePreLansering.kt" @@ -0,0 +1,62 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.gdpr.domene + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.OneToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.util.Objects + +/** + * Ikke i bruk, men tar vare på den i tilfelle vi trenger dataene som ligger i prod for perioden + * fødselshendelser var påskrudd for å telle metrikker. + */ +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "FødselshendelsePreLansering") +@Table(name = "FOEDSELSHENDELSE_PRE_LANSERING") +data class FødselshendelsePreLansering( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "foedselshendelse_pre_lansering_seq_generator") + @SequenceGenerator( + name = "foedselshendelse_pre_lansering_seq_generator", + sequenceName = "foedselshendelse_pre_lansering_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "fk_behandling_id", nullable = false, updatable = false) + val behandlingId: Long, + + @OneToOne(optional = false) + @JoinColumn(name = "fk_aktoer_id", nullable = false, updatable = false) + val aktør: Aktør, + + @Column(name = "ny_behandling_hendelse", nullable = false, updatable = false, columnDefinition = "TEXT") + val nyBehandlingHendelse: String = "", + + @Column(name = "filtreringsregler_input", columnDefinition = "TEXT") + val filtreringsreglerInput: String = "", + + @Column(name = "filtreringsregler_output", columnDefinition = "TEXT") + val filtreringsreglerOutput: String = "", + + @Column(name = "vilkaarsvurderinger_for_foedselshendelse", columnDefinition = "TEXT") + var vilkårsvurderingerForFødselshendelse: String = "", +) : BaseEntitet() { + + override fun hashCode(): Int { + return Objects.hashCode(id) + } + + override fun toString(): String { + return "FødselshendelsePreLansering(id=$id)" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/Regler.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/Regler.kt" new file mode 100644 index 000000000..87944f768 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/Regler.kt" @@ -0,0 +1,301 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering + +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.isBetween +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Evaluering +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall.VilkårIkkeOppfyltÅrsak +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall.VilkårKanskjeOppfyltÅrsak +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall.VilkårOppfyltÅrsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.arbeidsforhold.GrArbeidsforhold +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.arbeidsforhold.harLøpendeArbeidsforhold +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrBostedsadresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.filtrerGjeldendeNå +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.vurderOmPersonerBorSammen +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.opphold.GrOpphold +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.opphold.gyldigGjeldendeOppholdstillatelseFødselshendelse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.sivilstand.GrSivilstand +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.GrStatsborgerskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.hentSterkesteMedlemskap +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import java.time.Duration +import java.time.LocalDate + +interface Vilkårsregel { + + fun vurder(): Evaluering +} + +data class VurderPersonErBosattIRiket( + val adresser: List, + val vurderFra: LocalDate, +) : Vilkårsregel { + + override fun vurder(): Evaluering { + if (adresser.any { !it.harGyldigFom() }) { + val person = adresser.first().person + secureLogger.info( + "Har ugyldige adresser på person (${person?.aktør?.aktivFødselsnummer()}, ${person?.type}): ${ + adresser.filter { !it.harGyldigFom() } + .map { "(${it.periode?.fom}, ${it.periode?.tom}): ${it.toSecureString()}" } + }", + ) + } + + if (adresser.isEmpty()) return Evaluering.ikkeOppfylt(VilkårIkkeOppfyltÅrsak.BOR_IKKE_I_RIKET) + if (harPersonKunAdresserUtenFom(adresser)) { + return Evaluering.oppfylt(VilkårOppfyltÅrsak.BOR_I_RIKET_KUN_ADRESSER_UTEN_FOM) + } else if (harPersonBoddPåSisteAdresseMinstFraVurderingstidspunkt(adresser, vurderFra)) { + return Evaluering.oppfylt( + VilkårOppfyltÅrsak.BOR_I_RIKET, + ) + } else if (adresser.filter { !it.harGyldigFom() }.size > 1) return Evaluering.ikkeOppfylt(VilkårIkkeOppfyltÅrsak.BOR_IKKE_I_RIKET_FLERE_ADRESSER_UTEN_FOM) + + val adresserMedGyldigFom = adresser.filter { it.harGyldigFom() } + + /** + * En person med registrert bostedsadresse er bosatt i Norge. + * En person som mangler registrert bostedsadresse er utflyttet. + * See: https://navikt.github.io/pdl/#_utflytting + */ + return if (adresserMedGyldigFom.isNotEmpty() && erPersonBosattFraVurderingstidspunktet( + adresserMedGyldigFom, + vurderFra, + ) + ) { + Evaluering.oppfylt(VilkårOppfyltÅrsak.BOR_I_RIKET) + } else { + Evaluering.ikkeOppfylt(VilkårIkkeOppfyltÅrsak.BOR_IKKE_I_RIKET) + } + } + + /** + * Bruker har kun adresser uten fom som betyr at vedkommende har bodd på samme + * adresse hele livet eller flyttet før man innførte fom ved flytting. + */ + private fun harPersonKunAdresserUtenFom(adresser: List): Boolean = + adresser.all { !it.harGyldigFom() } + + private fun harPersonBoddPåSisteAdresseMinstFraVurderingstidspunkt( + adresser: List, + vurderFra: LocalDate, + ): Boolean { + val sisteAdresse = adresser + .filter { it.harGyldigFom() } + .maxByOrNull { it.periode?.fom!! } ?: return false + + return sisteAdresse.periode?.fom!!.toYearMonth() + .isBefore(vurderFra.toYearMonth()) && sisteAdresse.periode?.tom == null + } + + private fun erPersonBosattFraVurderingstidspunktet(adresser: List, vurderFra: LocalDate) = + hentMaxAvstandAvDagerMellomPerioder( + adresser.mapNotNull { it.periode }, + vurderFra, + LocalDate.now(), + ) == 0L +} + +data class VurderBarnErUnder18( + val alder: Int, +) : Vilkårsregel { + + override fun vurder(): Evaluering = + if (alder < 18) { + Evaluering.oppfylt(VilkårOppfyltÅrsak.ER_UNDER_18_ÅR) + } else { + Evaluering.ikkeOppfylt(VilkårIkkeOppfyltÅrsak.ER_IKKE_UNDER_18_ÅR) + } +} + +data class VurderBarnErBosattMedSøker( + val søkerAdresser: List, + val barnAdresser: List, +) : Vilkårsregel { + + override fun vurder(): Evaluering { + return if (vurderOmPersonerBorSammen( + adresser = barnAdresser, + andreAdresser = søkerAdresser, + ) + ) { + Evaluering.oppfylt(VilkårOppfyltÅrsak.BARNET_BOR_MED_MOR) + } else { + Evaluering.ikkeOppfylt(VilkårIkkeOppfyltÅrsak.BARNET_BOR_IKKE_MED_MOR) + } + } +} + +data class VurderBarnErUgift( + val sivilstander: List, +) : Vilkårsregel { + + override fun vurder(): Evaluering { + val sivilstanderMedGyldigFom = sivilstander.filter { it.harGyldigFom() } + + return when { + sivilstanderMedGyldigFom.singleOrNull { it.type == SIVILSTAND.UOPPGITT } != null -> + Evaluering.oppfylt(VilkårOppfyltÅrsak.BARN_MANGLER_SIVILSTAND) + sivilstanderMedGyldigFom.any { it.type == SIVILSTAND.GIFT || it.type == SIVILSTAND.REGISTRERT_PARTNER } -> + Evaluering.ikkeOppfylt(VilkårIkkeOppfyltÅrsak.BARN_ER_GIFT_ELLER_HAR_PARTNERSKAP) + else -> Evaluering.oppfylt(VilkårOppfyltÅrsak.BARN_ER_IKKE_GIFT_ELLER_HAR_PARTNERSKAP) + } + } +} + +data class VurderBarnHarLovligOpphold( + val aktør: Aktør, +) : Vilkårsregel { + override fun vurder(): Evaluering { + return Evaluering.oppfylt(VilkårOppfyltÅrsak.AUTOMATISK_VURDERING_BARN_LOVLIG_OPPHOLD) + } +} + +data class LovligOppholdFaktaEØS( + val arbeidsforhold: List, + val bostedsadresser: List, + val statsborgerskap: List, +) + +data class VurderPersonHarLovligOpphold( + val morLovligOppholdFaktaEØS: LovligOppholdFaktaEØS, + val annenForelderLovligOppholdFaktaEØS: LovligOppholdFaktaEØS?, + val opphold: List, +) : Vilkårsregel { + + override fun vurder(): Evaluering { + return when (morLovligOppholdFaktaEØS.statsborgerskap.hentSterkesteMedlemskap()) { + Medlemskap.NORDEN -> Evaluering.oppfylt(VilkårOppfyltÅrsak.NORDISK_STATSBORGER) + Medlemskap.TREDJELANDSBORGER -> { + val morErUkrainskStatsborger = morLovligOppholdFaktaEØS.statsborgerskap.any { it.landkode == "UKR" } + // Midlertidig regel for Ukrainakonflikten + if (morErUkrainskStatsborger) { + Evaluering.ikkeVurdert(VilkårKanskjeOppfyltÅrsak.LOVLIG_OPPHOLD_MÅ_VURDERE_LENGDEN_PÅ_OPPHOLDSTILLATELSEN) + } else if (opphold.gyldigGjeldendeOppholdstillatelseFødselshendelse()) { + Evaluering.oppfylt(VilkårOppfyltÅrsak.TREDJELANDSBORGER_MED_LOVLIG_OPPHOLD) + } else { + Evaluering.ikkeOppfylt(VilkårIkkeOppfyltÅrsak.TREDJELANDSBORGER_UTEN_LOVLIG_OPPHOLD) + } + } + Medlemskap.EØS -> vurderLovligOppholdForEØSBorger( + morLovligOppholdFaktaEØS, + annenForelderLovligOppholdFaktaEØS, + ) + Medlemskap.STATSLØS, Medlemskap.UKJENT -> { + if (opphold.gyldigGjeldendeOppholdstillatelseFødselshendelse()) { + Evaluering.oppfylt(VilkårOppfyltÅrsak.UKJENT_STATSBORGERSKAP_MED_LOVLIG_OPPHOLD) + } else { + Evaluering.ikkeOppfylt(VilkårIkkeOppfyltÅrsak.STATSLØS) + } + } + else -> Evaluering.ikkeVurdert(VilkårKanskjeOppfyltÅrsak.LOVLIG_OPPHOLD_IKKE_MULIG_Å_FASTSETTE) + } + } +} + +private fun vurderLovligOppholdForEØSBorger( + morLovligOppholdFaktaEØS: LovligOppholdFaktaEØS, + annenForelderLovligOppholdFaktaEØS: LovligOppholdFaktaEØS?, +): Evaluering { + if (morLovligOppholdFaktaEØS.arbeidsforhold.harLøpendeArbeidsforhold()) { + return Evaluering.oppfylt(VilkårOppfyltÅrsak.EØS_MED_LØPENDE_ARBEIDSFORHOLD) + } + + if (annenForelderLovligOppholdFaktaEØS == null) { + return Evaluering.ikkeOppfylt(VilkårIkkeOppfyltÅrsak.EØS_STATSBORGERSKAP_ANNEN_FORELDER_UKLART) + } + + if (!vurderOmPersonerBorSammen( + adresser = morLovligOppholdFaktaEØS.bostedsadresser.filtrerGjeldendeNå(), + andreAdresser = annenForelderLovligOppholdFaktaEØS.bostedsadresser.filtrerGjeldendeNå(), + ) + ) { + return Evaluering.ikkeOppfylt(VilkårIkkeOppfyltÅrsak.EØS_BOR_IKKE_SAMMEN_MED_ANNEN_FORELDER) + } + + return when (annenForelderLovligOppholdFaktaEØS.statsborgerskap.hentSterkesteMedlemskap()) { + Medlemskap.NORDEN -> Evaluering.oppfylt(VilkårOppfyltÅrsak.ANNEN_FORELDER_NORDISK) + Medlemskap.EØS -> { + if (annenForelderLovligOppholdFaktaEØS.arbeidsforhold.harLøpendeArbeidsforhold()) { + Evaluering.oppfylt(VilkårOppfyltÅrsak.ANNEN_FORELDER_EØS_MEN_MED_LØPENDE_ARBEIDSFORHOLD) + } else { + Evaluering.ikkeOppfylt(VilkårIkkeOppfyltÅrsak.EØS_ANNEN_FORELDER_EØS_MEN_IKKE_MED_LØPENDE_ARBEIDSFORHOLD) + } + } + Medlemskap.TREDJELANDSBORGER -> Evaluering.ikkeOppfylt(VilkårIkkeOppfyltÅrsak.EØS_MEDFORELDER_TREDJELANDSBORGER) + Medlemskap.STATSLØS, Medlemskap.UKJENT -> Evaluering.ikkeOppfylt(VilkårIkkeOppfyltÅrsak.EØS_MEDFORELDER_STATSLØS) + else -> Evaluering.ikkeVurdert(VilkårKanskjeOppfyltÅrsak.LOVLIG_OPPHOLD_ANNEN_FORELDER_IKKE_MULIG_Å_FASTSETTE) + } +} + +private fun hentMaxAvstandAvDagerMellomPerioder( + perioder: List, + fom: LocalDate, + tom: LocalDate, +): Long { + val perioderMedTilkobletTom = + perioder.sortedBy { it.fom } + .fold(mutableListOf()) { acc: MutableList, datoIntervallEntitet: DatoIntervallEntitet -> + if (acc.isNotEmpty() && acc.last().tom == null) { + val sisteDatoIntervall = acc.last().copy( + tom = datoIntervallEntitet.fom?.minusDays(1), + ) + + acc.removeLast() + acc.add(sisteDatoIntervall) + } + + acc.add(datoIntervallEntitet) + acc.sortBy { it.fom } + acc + } + .toList() + + val perioderInnenAngittTidsrom = + perioderMedTilkobletTom.filter { + it.tom == null || + fom.isBetween( + Periode( + fom = it.fom!!, + tom = it.tom, + ), + ) || + tom.isBetween( + Periode( + fom = it.fom, + tom = it.tom, + ), + ) || + it.fom >= fom && it.tom <= tom + } + + if (perioderInnenAngittTidsrom.isEmpty()) return Duration.between(fom.atStartOfDay(), tom.atStartOfDay()).toDays() + + val defaultAvstand = if (perioderInnenAngittTidsrom.first().fom!!.isAfter(fom)) { + Duration.between( + fom.atStartOfDay(), + perioderInnenAngittTidsrom.first().fom!!.atStartOfDay(), + ) + .toDays() + } else if (perioderInnenAngittTidsrom.last().tom != null && perioderInnenAngittTidsrom.last().tom!!.isBefore(tom)) { + Duration.between( + perioderInnenAngittTidsrom.last().tom!!.atStartOfDay(), + tom.atStartOfDay(), + ).toDays() + } else { + 0L + } + + return perioderInnenAngittTidsrom + .zipWithNext() + .fold(defaultAvstand) { maksimumAvstand, pairs -> + val avstand = + Duration.between(pairs.first.tom!!.atStartOfDay().plusDays(1), pairs.second.fom!!.atStartOfDay()) + .toDays() + maxOf(avstand, maksimumAvstand) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/utfall/Vilk\303\245rIkkeOppfylt\303\205rsak.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/utfall/Vilk\303\245rIkkeOppfylt\303\205rsak.kt" new file mode 100644 index 000000000..0a2d67c36 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/utfall/Vilk\303\245rIkkeOppfylt\303\205rsak.kt" @@ -0,0 +1,78 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.EvalueringÅrsak +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår + +enum class VilkårIkkeOppfyltÅrsak(val beskrivelse: String, val metrikkBeskrivelse: String? = null, val vilkår: Vilkår) : + EvalueringÅrsak { + + // Under 18 år + ER_IKKE_UNDER_18_ÅR(beskrivelse = "Barn er ikke under 18 år", vilkår = Vilkår.UNDER_18_ÅR), + + // Bor med søker + SØKER_ER_IKKE_MOR(beskrivelse = "Søker er ikke mor", vilkår = Vilkår.BOR_MED_SØKER), + BARNET_BOR_IKKE_MED_MOR(beskrivelse = "Barnet bor ikke med mor", vilkår = Vilkår.BOR_MED_SØKER), + + // Gift eller partnerskap hos barn + BARN_ER_GIFT_ELLER_HAR_PARTNERSKAP( + beskrivelse = "Person er gift eller har registrert partner", + vilkår = Vilkår.GIFT_PARTNERSKAP, + ), + + // Bosatt i riket + BOR_IKKE_I_RIKET(beskrivelse = "Er ikke bosatt i riket", vilkår = Vilkår.BOSATT_I_RIKET), + BOR_IKKE_I_RIKET_FLERE_ADRESSER_UTEN_FOM( + beskrivelse = "Er ikke bosatt i riket, flere adresser uten fom", + vilkår = Vilkår.BOSATT_I_RIKET, + ), + + // Lovlig opphold + TREDJELANDSBORGER_UTEN_LOVLIG_OPPHOLD( + beskrivelse = "Mor har ikke lovlig opphold - tredjelandsborger.", + metrikkBeskrivelse = "Mor tredjelandsborger", + vilkår = Vilkår.LOVLIG_OPPHOLD, + ), + STATSLØS( + beskrivelse = "Mor har ikke lovlig opphold - er statsløs eller mangler statsborgerskap.", + metrikkBeskrivelse = "Mor statsløs eller mangler statsborgerskap", + vilkår = Vilkår.LOVLIG_OPPHOLD, + ), + EØS_STATSBORGERSKAP_ANNEN_FORELDER_UKLART( + beskrivelse = "Mor EØS borger uten oppholdsrett", + metrikkBeskrivelse = "Statsborgerskap for annen forelder kan ikke avgjøres.", + vilkår = Vilkår.LOVLIG_OPPHOLD, + ), + EØS_BOR_IKKE_SAMMEN_MED_ANNEN_FORELDER( + beskrivelse = "Mor EØS borger uten oppholdsrett", + metrikkBeskrivelse = "Mor har ikke lovlig opphold - bor ikke med annen forelder", + vilkår = Vilkår.LOVLIG_OPPHOLD, + ), + EØS_ANNEN_FORELDER_EØS_MEN_IKKE_MED_LØPENDE_ARBEIDSFORHOLD( + beskrivelse = "Mor EØS borger uten oppholdsrett", + metrikkBeskrivelse = "Annen forelder er fra EØS, men har ikke et løpende arbeidsforhold i Norge.", + vilkår = Vilkår.LOVLIG_OPPHOLD, + ), + EØS_MEDFORELDER_TREDJELANDSBORGER( + beskrivelse = "Mor EØS borger uten oppholdsrett", + metrikkBeskrivelse = "Mor EØS. Ikke arb. MF tredjelandsborger", + vilkår = Vilkår.LOVLIG_OPPHOLD, + ), + EØS_MEDFORELDER_STATSLØS( + beskrivelse = "Mor EØS borger uten oppholdsrett", + metrikkBeskrivelse = "Mor EØS. Ikke arb. MF statsløs", + vilkår = Vilkår.LOVLIG_OPPHOLD, + ), + ; + + override fun hentBeskrivelse(): String { + return beskrivelse + } + + override fun hentMetrikkBeskrivelse(): String { + return metrikkBeskrivelse ?: beskrivelse + } + + override fun hentIdentifikator(): String { + return vilkår.name + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/utfall/Vilk\303\245rKanskjeOppfylt\303\205rsak.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/utfall/Vilk\303\245rKanskjeOppfylt\303\205rsak.kt" new file mode 100644 index 000000000..4c9204dda --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/utfall/Vilk\303\245rKanskjeOppfylt\303\205rsak.kt" @@ -0,0 +1,31 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.EvalueringÅrsak +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår + +enum class VilkårKanskjeOppfyltÅrsak(val beskrivelse: String, val vilkår: Vilkår) : EvalueringÅrsak { + + // Lovlig opphold + LOVLIG_OPPHOLD_MÅ_VURDERE_LENGDEN_PÅ_OPPHOLDSTILLATELSEN( + "Må vurdere lengden på oppholdstillatelsen.", + Vilkår.LOVLIG_OPPHOLD, + ), + LOVLIG_OPPHOLD_IKKE_MULIG_Å_FASTSETTE("Kan ikke avgjøre om personen har lovlig opphold.", Vilkår.LOVLIG_OPPHOLD), + LOVLIG_OPPHOLD_ANNEN_FORELDER_IKKE_MULIG_Å_FASTSETTE( + "Kan ikke avgjøre om annen har lovlig opphold.", + Vilkår.LOVLIG_OPPHOLD, + ), + ; + + override fun hentBeskrivelse(): String { + return beskrivelse + } + + override fun hentMetrikkBeskrivelse(): String { + return beskrivelse + } + + override fun hentIdentifikator(): String { + return vilkår.name + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/utfall/Vilk\303\245rOppfylt\303\205rsak.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/utfall/Vilk\303\245rOppfylt\303\205rsak.kt" new file mode 100644 index 000000000..8e409d2d2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/utfall/Vilk\303\245rOppfylt\303\205rsak.kt" @@ -0,0 +1,63 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.EvalueringÅrsak +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår + +enum class VilkårOppfyltÅrsak(val beskrivelse: String, val vilkår: Vilkår) : EvalueringÅrsak { + + // Under 18 år + ER_UNDER_18_ÅR("Barn er under 18 år", Vilkår.UNDER_18_ÅR), + + // Bor med søker + SØKER_ER_MOR("Søker er mor", Vilkår.BOR_MED_SØKER), + BARNET_BOR_MED_MOR("Barnet bor med mor", Vilkår.BOR_MED_SØKER), + + // Gift eller partnerskap hos barn + BARN_MANGLER_SIVILSTAND("Barn mangler informasjon om sivilstand.", Vilkår.GIFT_PARTNERSKAP), + BARN_ER_IKKE_GIFT_ELLER_HAR_PARTNERSKAP( + "Person er ikke gift eller har registrert partner", + Vilkår.GIFT_PARTNERSKAP, + ), + + // Bosatt i riket + BOR_I_RIKET("Er bosatt i riket", Vilkår.BOSATT_I_RIKET), + BOR_I_RIKET_KUN_ADRESSER_UTEN_FOM( + "Er bosatt i riket - har kun adresser uten fra- og med dato", + Vilkår.BOSATT_I_RIKET, + ), + + // Lovlig opphold + AUTOMATISK_VURDERING_BARN_LOVLIG_OPPHOLD( + "Ikke separat oppholdsvurdering for barnet ved automatisk vedtak.", + Vilkår.LOVLIG_OPPHOLD, + ), + NORDISK_STATSBORGER("Er nordisk statsborger.", Vilkår.LOVLIG_OPPHOLD), + TREDJELANDSBORGER_MED_LOVLIG_OPPHOLD("Er tredjelandsborger med lovlig opphold", Vilkår.LOVLIG_OPPHOLD), + UKJENT_STATSBORGERSKAP_MED_LOVLIG_OPPHOLD( + "Er statsløs eller mangler statsborgerskap med lovlig opphold", + Vilkår.LOVLIG_OPPHOLD, + ), + EØS_MED_LØPENDE_ARBEIDSFORHOLD( + "Mor er EØS-borger, men har et løpende arbeidsforhold i Norge.", + Vilkår.LOVLIG_OPPHOLD, + ), + ANNEN_FORELDER_NORDISK("Annen forelder er norsk eller nordisk statsborger.", Vilkår.LOVLIG_OPPHOLD), + ANNEN_FORELDER_EØS_MEN_MED_LØPENDE_ARBEIDSFORHOLD( + "Annen forelder er fra EØS, men har et løpende arbeidsforhold i Norge.", + Vilkår.LOVLIG_OPPHOLD, + ), + MOR_BODD_OG_JOBBET_I_NORGE_SISTE_5_ÅR("Mor har bodd og jobbet i Norge siste 5 år.", Vilkår.LOVLIG_OPPHOLD), + ; + + override fun hentBeskrivelse(): String { + return beskrivelse + } + + override fun hentMetrikkBeskrivelse(): String { + return beskrivelse + } + + override fun hentIdentifikator(): String { + return vilkår.name + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/Autobrev6og18\303\205rService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/Autobrev6og18\303\205rService.kt" new file mode 100644 index 000000000..3bf052411 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/Autobrev6og18\303\205rService.kt" @@ -0,0 +1,169 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.omregning + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakStegService +import no.nav.familie.ba.sak.kjerne.autovedtak.OmregningBrevData +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.StartSatsendring +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.task.dto.Autobrev6og18ÅrDTO +import no.nav.familie.prosessering.error.RekjørSenereException +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.time.YearMonth + +@Service +class Autobrev6og18ÅrService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + private val autovedtakBrevService: AutovedtakBrevService, + private val autovedtakStegService: AutovedtakStegService, + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + private val startSatsendring: StartSatsendring, +) { + + @Transactional + fun opprettOmregningsoppgaveForBarnIBrytingsalder(autobrev6og18ÅrDTO: Autobrev6og18ÅrDTO) { + logger.info("opprettOmregningsoppgaveForBarnIBrytingsalder for fagsak ${autobrev6og18ÅrDTO.fagsakId}") + + val behandling = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(autobrev6og18ÅrDTO.fagsakId) + ?: error("Fant ikke aktiv behandling") + + val behandlingsårsak = finnBehandlingÅrsakForAlder( + autobrev6og18ÅrDTO.alder, + ) + + if (!autovedtakBrevService.skalAutobrevBehandlingOpprettes( + fagsakId = autobrev6og18ÅrDTO.fagsakId, + behandlingsårsak = behandlingsårsak, + standardbegrunnelser = AutobrevUtils.hentStandardbegrunnelserReduksjonForAlder(autobrev6og18ÅrDTO.alder), + ) + ) { + return + } + + if (behandling.fagsak.status != FagsakStatus.LØPENDE) { + logger.info("Fagsak ${behandling.fagsak.id} har ikke status løpende, og derfor prosesseres den ikke videre.") + return + } + + if (!barnMedAngittAlderInneværendeMånedEksisterer( + behandlingId = behandling.id, + alder = autobrev6og18ÅrDTO.alder, + ) + ) { + logger.warn("Fagsak ${behandling.fagsak.id} har ikke noe barn med alder ${autobrev6og18ÅrDTO.alder} ") + return + } + + if (barnetrygdOpphører(autobrev6og18ÅrDTO, behandling)) { + logger.info("Fagsak ${behandling.fagsak.id} har ikke løpende utbetalinger for barn under 18 år og vil opphøre.") + return + } + + if (!barnIBrytningsalderHarLøpendeYtelse( + alder = autobrev6og18ÅrDTO.alder, + behandlingId = behandling.id, + årMåned = autobrev6og18ÅrDTO.årMåned, + + ) + ) { + logger.info("Ingen løpende ytelse for barnet i brytningsalder for fagsak=${behandling.fagsak.id} behandlingsårsak=$behandlingsårsak") + return + } + + if (startSatsendring.sjekkOgOpprettSatsendringVedGammelSats(autobrev6og18ÅrDTO.fagsakId)) { + throw RekjørSenereException( + "Satsedring skal kjøre ferdig før man behandler autobrev 6 og 18 år", + LocalDateTime.now().plusHours(1), + ) + } + + autovedtakStegService.kjørBehandlingOmregning( + mottakersAktør = behandling.fagsak.aktør, + behandlingsdata = OmregningBrevData( + aktør = behandling.fagsak.aktør, + behandlingsårsak = behandlingsårsak, + standardbegrunnelse = AutobrevUtils.hentGjeldendeVedtakbegrunnelseReduksjonForAlder( + autobrev6og18ÅrDTO.alder, + ), + fagsakId = behandling.fagsak.id, + ), + ) + } + + private fun barnetrygdOpphører( + autobrev6og18ÅrDTO: Autobrev6og18ÅrDTO, + behandling: Behandling, + ) = + autobrev6og18ÅrDTO.alder == Alder.ATTEN.år && + !barnUnder18årInneværendeMånedEksisterer(behandlingId = behandling.id) + + private fun finnBehandlingÅrsakForAlder(alder: Int): BehandlingÅrsak = + when (alder) { + Alder.SEKS.år -> BehandlingÅrsak.OMREGNING_6ÅR + Alder.ATTEN.år -> BehandlingÅrsak.OMREGNING_18ÅR + else -> throw Feil("Alder må være oppgitt til enten 6 eller 18 år.") + } + + private fun barnMedAngittAlderInneværendeMånedEksisterer(behandlingId: Long, alder: Int): Boolean = + barnMedAngittAlderInneværendeMåned(behandlingId, alder).isNotEmpty() + + private fun barnMedAngittAlderInneværendeMåned(behandlingId: Long, alder: Int): List = + personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = behandlingId)?.barna + ?.filter { it.type == PersonType.BARN && it.fyllerAntallÅrInneværendeMåned(alder) }?.toList() ?: listOf() + + private fun barnUnder18årInneværendeMånedEksisterer(behandlingId: Long): Boolean = + personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = behandlingId)?.barna + ?.any { it.type == PersonType.BARN && it.erYngreEnnInneværendeMåned(Alder.ATTEN.år) } ?: false + + private fun barnIBrytningsalderHarLøpendeYtelse( + alder: Int, + behandlingId: Long, + årMåned: YearMonth, + ): Boolean { + val barnIBrytningsalder = + barnMedAngittAlderInneværendeMåned(behandlingId = behandlingId, alder = alder).map { it.aktør } + + if (barnIBrytningsalder.isEmpty()) { + throw Feil("Forventer å finne minst et barn i brytningsalder for omregning 6 eller 18 år for behandling=$behandlingId") + } + + val andelerTilBarnIBrytningsalder = + andelerTilkjentYtelseOgEndreteUtbetalingerService.finnAndelerTilkjentYtelseMedEndreteUtbetalinger( + behandlingId, + ).filter { it.aktør in barnIBrytningsalder } + + return harBarnIBrytningsalderLøpendeAndeler(alder, andelerTilBarnIBrytningsalder, årMåned) + } + + private fun harBarnIBrytningsalderLøpendeAndeler( + alder: Int, + andelerTilBarnIBrytningsalder: List, + årMåned: YearMonth, + ) = when (alder) { + Alder.ATTEN.år -> andelerTilBarnIBrytningsalder.any { it.stønadTom.plusMonths(1) == årMåned } + Alder.SEKS.år -> andelerTilBarnIBrytningsalder.any { it.stønadTom.plusMonths(1) == årMåned } && andelerTilBarnIBrytningsalder.any { it.stønadFom == årMåned } + else -> throw Feil("Ugyldig alder") + } + + companion object { + + private val logger = LoggerFactory.getLogger(Autobrev6og18ÅrService::class.java) + } +} + +enum class Alder(val år: Int) { + SEKS(år = 6), + ATTEN(år = 18), +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevOpph\303\270rSm\303\245barnstilleggService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevOpph\303\270rSm\303\245barnstilleggService.kt" new file mode 100644 index 000000000..c86a822fc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevOpph\303\270rSm\303\245barnstilleggService.kt" @@ -0,0 +1,99 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.omregning + +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakStegService +import no.nav.familie.ba.sak.kjerne.autovedtak.OmregningBrevData +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.StartSatsendring +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.småbarnstillegg.PeriodeOvergangsstønadGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.småbarnstillegg.PeriodeOvergangsstønadGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.prosessering.error.RekjørSenereException +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.time.YearMonth + +@Service +class AutobrevOpphørSmåbarnstilleggService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val autovedtakBrevService: AutovedtakBrevService, + private val autovedtakStegService: AutovedtakStegService, + private val persongrunnlagService: PersongrunnlagService, + private val periodeOvergangsstønadGrunnlagRepository: PeriodeOvergangsstønadGrunnlagRepository, + private val startSatsendring: StartSatsendring, +) { + @Transactional + fun kjørBehandlingOgSendBrevForOpphørAvSmåbarnstillegg(fagsakId: Long) { + val behandling = + behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = fagsakId) + ?: error("Fant ikke aktiv behandling") + + val personopplysningGrunnlag: PersonopplysningGrunnlag = + persongrunnlagService.hentAktivThrows(behandling.id) + + val listePeriodeOvergangsstønadGrunnlag: List = + periodeOvergangsstønadGrunnlagRepository.findByBehandlingId(behandlingId = behandling.id) + + val behandlingsårsak = BehandlingÅrsak.OMREGNING_SMÅBARNSTILLEGG + + val standardbegrunnelse = + if (yngsteBarnFylteTreÅrForrigeMåned(personopplysningGrunnlag = personopplysningGrunnlag)) { + Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_BARN_UNDER_TRE_ÅR + } else if (overgangstønadOpphørteForrigeMåned(listePeriodeOvergangsstønadGrunnlag = listePeriodeOvergangsstønadGrunnlag)) { + Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_FULL_OVERGANGSSTØNAD + } else { + logger.info( + "For fagsak $fagsakId ble verken yngste barn 3 år forrige måned eller har overgangsstønad som utløper denne måneden. " + + "Avbryter sending av autobrev for opphør av småbarnstillegg.", + ) + return + } + + if (!autovedtakBrevService.skalAutobrevBehandlingOpprettes( + fagsakId = fagsakId, + behandlingsårsak = behandlingsårsak, + standardbegrunnelser = listOf(standardbegrunnelse), + ) + ) { + return + } + + if (startSatsendring.sjekkOgOpprettSatsendringVedGammelSats(fagsakId)) { + throw RekjørSenereException( + "Satsedring skal kjøre ferdig før man behandler autobrev småbarnstillegg", + LocalDateTime.now().plusHours(1), + ) + } + + autovedtakStegService.kjørBehandlingOmregning( + mottakersAktør = behandling.fagsak.aktør, + behandlingsdata = OmregningBrevData( + aktør = behandling.fagsak.aktør, + behandlingsårsak = behandlingsårsak, + standardbegrunnelse = standardbegrunnelse, + fagsakId = fagsakId, + ), + ) + } + + fun overgangstønadOpphørteForrigeMåned(listePeriodeOvergangsstønadGrunnlag: List): Boolean = + listePeriodeOvergangsstønadGrunnlag.any { periodeOvergangsstønadGrunnlag -> + periodeOvergangsstønadGrunnlag.tom.toYearMonth() == YearMonth.now().minusMonths(1) + } && listePeriodeOvergangsstønadGrunnlag.none { periodeOvergangsstønadGrunnlag -> periodeOvergangsstønadGrunnlag.fom.toYearMonth() == YearMonth.now() } + + fun yngsteBarnFylteTreÅrForrigeMåned(personopplysningGrunnlag: PersonopplysningGrunnlag): Boolean { + val yngsteBarnSinFødselsdato: YearMonth = + personopplysningGrunnlag.yngsteBarnSinFødselsdato.toYearMonth() + + return yngsteBarnSinFødselsdato.plusYears(3) == YearMonth.now().minusMonths(1) + } + + companion object { + private val logger = LoggerFactory.getLogger(AutobrevOpphørSmåbarnstilleggService::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevScheduler.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevScheduler.kt new file mode 100644 index 000000000..0951ab896 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevScheduler.kt @@ -0,0 +1,57 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.omregning + +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.leader.LeaderClient +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.util.VirkedagerProvider +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.LocalDateTime + +@Component +class AutobrevScheduler(val taskRepository: TaskRepositoryWrapper) { + + /** + * Denne funksjonen kjøres kl.7 den første dagen i måneden og setter triggertid på tasken til kl.8 den første virkedagen i måneden. + * For testformål kan funksjonen opprettTask også kalles direkte via et restendepunkt. + */ + @Transactional + @Scheduled(cron = "0 0 $KLOKKETIME_SCHEDULER_TRIGGES 1 * *") + fun opprettAutobrevTask() { + when (LeaderClient.isLeader()) { + true -> { + // Timen for triggertid økes med en. Det er nødvendig å sette klokkeslettet litt frem dersom den 1. i + // måneden også er en virkedag (slik at både denne skeduleren og tasken som opprettes vil kjøre på samme dato). + opprettTask( + triggerTid = VirkedagerProvider.nesteVirkedag( + LocalDate.now().minusDays(1), + ).atTime(KLOKKETIME_SCHEDULER_TRIGGES.inc(), 0), + ) + } + + false -> logger.info("Poden er ikke satt opp som leader - oppretter ikke task") + null -> logger.info("Poden svarer ikke om den er leader eller ikke - oppretter ikke task") + } + } + + fun opprettTask(triggerTid: LocalDateTime = LocalDateTime.now().plusSeconds(30)) { + logger.info("Opprett månedlig task") + taskRepository.save( + Task( + type = AutobrevTask.TASK_STEP_TYPE, + payload = "", + ).medTriggerTid( + triggerTid = triggerTid, + ), + ) + } + + companion object { + + private val logger = LoggerFactory.getLogger(AutobrevScheduler::class.java) + const val KLOKKETIME_SCHEDULER_TRIGGES = 6 + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevTask.kt new file mode 100644 index 000000000..f414c5ea5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevTask.kt @@ -0,0 +1,74 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.omregning + +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +@TaskStepBeskrivelse( + taskStepType = AutobrevTask.TASK_STEP_TYPE, + beskrivelse = "Opprett oppgaver for sending av autobrev", + maxAntallFeil = 1, +) +class AutobrevTask( + private val fagsakRepository: FagsakRepository, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val opprettTaskService: OpprettTaskService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + opprettTaskerForReduksjonPgaAlder() + opprettTaskerForReduksjonSmåbarnstillegg() + } + + private fun opprettTaskerForReduksjonPgaAlder() { + listOf(6, 18).forEach { alder -> + val berørteFagsaker = finnAlleBarnMedFødselsdagInneværendeMåned(alder) + logger.info("Oppretter tasker for ${berørteFagsaker.size} fagsaker med barn som fyller $alder år inneværende måned.") + berørteFagsaker.forEach { fagsak -> + opprettTaskService.opprettAutovedtakFor6Og18ÅrBarn( + fagsakId = fagsak.id, + alder = alder.toInt(), + ) + } + } + } + + private fun opprettTaskerForReduksjonSmåbarnstillegg() { + val berørteFagsaker = behandlingHentOgPersisterService.partitionByIverksatteBehandlinger { + fagsakRepository.finnAlleFagsakerMedOpphørSmåbarnstilleggIMåned( + iverksatteLøpendeBehandlinger = it, + ) + } + logger.info("Oppretter tasker for ${berørteFagsaker.size} fagsaker med opphør av småbarnstillegg.") + berørteFagsaker.forEach { fagsakId -> + opprettTaskService.opprettAutovedtakForOpphørSmåbarnstilleggTask( + fagsakId = fagsakId, + ) + } + } + + private fun finnAlleBarnMedFødselsdagInneværendeMåned(alder: Long): Set = + LocalDate.now().minusYears(alder).let { + fagsakRepository.finnLøpendeFagsakMedBarnMedFødselsdatoInnenfor( + it.førsteDagIInneværendeMåned(), + it.sisteDagIMåned(), + ) + } + + companion object { + + const val TASK_STEP_TYPE = "AutobrevTask" + private val logger: Logger = LoggerFactory.getLogger(AutobrevTask::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevUtils.kt new file mode 100644 index 000000000..b6d141cf4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevUtils.kt @@ -0,0 +1,26 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.omregning + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse + +object AutobrevUtils { + fun hentStandardbegrunnelserReduksjonForAlder(alder: Int): List = + when (alder) { + Alder.SEKS.år -> listOf( + Standardbegrunnelse.REDUKSJON_UNDER_6_ÅR_AUTOVEDTAK, + Standardbegrunnelse.REDUKSJON_UNDER_6_ÅR, + ) + Alder.ATTEN.år -> listOf( + Standardbegrunnelse.REDUKSJON_UNDER_18_ÅR_AUTOVEDTAK, + Standardbegrunnelse.REDUKSJON_UNDER_18_ÅR, + ) + else -> throw Feil("Alder må være oppgitt til enten 6 eller 18 år.") + } + + fun hentGjeldendeVedtakbegrunnelseReduksjonForAlder(alder: Int): Standardbegrunnelse = + when (alder) { + Alder.SEKS.år -> Standardbegrunnelse.REDUKSJON_UNDER_6_ÅR_AUTOVEDTAK + Alder.ATTEN.år -> Standardbegrunnelse.REDUKSJON_UNDER_18_ÅR_AUTOVEDTAK + else -> throw Feil("Alder må være oppgitt til enten 6 eller 18 år.") + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutovedtakBrevService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutovedtakBrevService.kt new file mode 100644 index 000000000..eb6248283 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutovedtakBrevService.kt @@ -0,0 +1,167 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.omregning + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdBrevkode +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakBehandlingService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakStegService +import no.nav.familie.ba.sak.kjerne.autovedtak.OmregningBrevData +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.domene.erAlleredeBegrunnetMedBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.task.JournalførVedtaksbrevTask +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.YearMonth + +@Service +class AutovedtakBrevService( + private val fagsakService: FagsakService, + private val behandlingService: BehandlingService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val autovedtakService: AutovedtakService, + private val vedtakService: VedtakService, + private val vedtaksperiodeService: VedtaksperiodeService, + private val taskRepository: TaskRepositoryWrapper, + private val infotrygdService: InfotrygdService, +) : AutovedtakBehandlingService { + + override fun kjørBehandling( + behandlingsdata: OmregningBrevData, + ): String { + val behandlingEtterBehandlingsresultat = + autovedtakService.opprettAutomatiskBehandlingOgKjørTilBehandlingsresultat( + aktør = behandlingsdata.aktør, + behandlingType = BehandlingType.REVURDERING, + behandlingÅrsak = behandlingsdata.behandlingsårsak, + fagsakId = behandlingsdata.fagsakId, + ) + + vedtaksperiodeService.oppdaterFortsattInnvilgetPeriodeMedAutobrevBegrunnelse( + vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingEtterBehandlingsresultat.id), + standardbegrunnelse = behandlingsdata.standardbegrunnelse, + ) + + val opprettetVedtak = + autovedtakService.opprettToTrinnskontrollOgVedtaksbrevForAutomatiskBehandling( + behandlingEtterBehandlingsresultat, + ) + + opprettTaskJournalførVedtaksbrev(vedtakId = opprettetVedtak.id) + + return AutovedtakStegService.BEHANDLING_FERDIG + } + + fun skalAutobrevBehandlingOpprettes( + fagsakId: Long, + behandlingsårsak: BehandlingÅrsak, + standardbegrunnelser: List, + ): Boolean { + if (!behandlingsårsak.erOmregningsårsak()) { + throw Feil("Sjekk om autobrevbehandling skal opprettes sjekker på årsak som ikke er omregning.") + } + + if (harSendtBrevFraInfotrygd(fagsakId, behandlingsårsak)) { + return false + } + + if (behandlingService.harBehandlingsårsakAlleredeKjørt( + fagsakId = fagsakId, + behandlingÅrsak = behandlingsårsak, + måned = YearMonth.now(), + ) + ) { + logger.info("Brev for ${behandlingsårsak.visningsnavn} har allerede kjørt for $fagsakId") + return false + } + + val vedtaksperioderForVedtatteBehandlinger = + behandlingHentOgPersisterService.hentBehandlinger(fagsakId = fagsakId) + .filter { behandling -> + behandling.erVedtatt() + } + .flatMap { behandling -> + val vedtak = vedtakService.hentAktivForBehandlingThrows(behandling.id) + vedtaksperiodeService.hentPersisterteVedtaksperioder(vedtak) + } + + if (barnAlleredeBegrunnet( + vedtaksperioderMedBegrunnelser = vedtaksperioderForVedtatteBehandlinger, + standardbegrunnelser = standardbegrunnelser, + ) + ) { + logger.info("Begrunnelser $standardbegrunnelser for ${behandlingsårsak.visningsnavn} har allerede kjørt for $fagsakId") + return false + } + + return true + } + + fun harSendtBrevFraInfotrygd(fagsakId: Long, behandlingsårsak: BehandlingÅrsak): Boolean { + val personidenter = fagsakService.hentAktør(fagsakId).personidenter + val harSendtBrev = + infotrygdService.harSendtbrev(personidenter.map { it.fødselsnummer }, behandlingsårsak.tilBrevkoder()) + return if (harSendtBrev) { + logger.info("Har sendt autobrev fra infotrygd, dropper å lage behandling for å sende brev fra ba-sak. fagsakId=$fagsakId behandlingsårsak=$behandlingsårsak") + true + } else { + logger.info("Har ikke sendt autobrev fra infotrygd, lager ny behandling og sender brev på vanlig måte. fagsakId=$fagsakId behandlingsårsak=$behandlingsårsak") + false + } + } + + private fun barnAlleredeBegrunnet( + vedtaksperioderMedBegrunnelser: List, + standardbegrunnelser: List, + ): Boolean { + return vedtaksperioderMedBegrunnelser.erAlleredeBegrunnetMedBegrunnelse( + standardbegrunnelser = standardbegrunnelser, + måned = YearMonth.now(), + ) + } + + private fun opprettTaskJournalførVedtaksbrev(vedtakId: Long) { + val task = Task( + JournalførVedtaksbrevTask.TASK_STEP_TYPE, + "$vedtakId", + ) + taskRepository.save(task) + } + + companion object { + val logger = LoggerFactory.getLogger(AutovedtakBrevService::class.java) + } + + override fun skalAutovedtakBehandles(behandlingsdata: OmregningBrevData): Boolean = true +} + +private fun BehandlingÅrsak.tilBrevkoder(): List { + return when (this) { + BehandlingÅrsak.OMREGNING_6ÅR -> listOf( + InfotrygdBrevkode.BREV_BATCH_OMREGNING_BARN_6_ÅR, + InfotrygdBrevkode.BREV_MANUELL_OMREGNING_BARN_6_ÅR, + ) + + BehandlingÅrsak.OMREGNING_18ÅR -> listOf( + InfotrygdBrevkode.BREV_BATCH_OMREGNING_BARN_18_ÅR, + InfotrygdBrevkode.BREV_MANUELL_OMREGNING_BARN_18_ÅR, + ) + + BehandlingÅrsak.OMREGNING_SMÅBARNSTILLEGG -> listOf( + InfotrygdBrevkode.BREV_BATCH_OPPHØR_SMÅBARNSTILLLEGG, + InfotrygdBrevkode.BREV_MANUELL_OPPHØR_SMÅBARNSTILLLEGG, + ) + + else -> emptyList() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/AutovedtakSatsendringScheduler.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/AutovedtakSatsendringScheduler.kt new file mode 100644 index 000000000..9276ed203 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/AutovedtakSatsendringScheduler.kt @@ -0,0 +1,30 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.satsendring + +import no.nav.familie.leader.LeaderClient +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service + +@Service +class AutovedtakSatsendringScheduler( + private val startSatsendring: StartSatsendring, +) { + @Scheduled(cron = CRON_HVERT_10_MIN_UKEDAG) + fun triggSatsendringJuli2023() { + startSatsendring(1200) + } + + private fun startSatsendring(antallFagsaker: Int) { + if (LeaderClient.isLeader() == true) { + logger.info("Starter schedulert jobb for satsendring juli 2023") + startSatsendring.startSatsendring( + antallFagsaker = antallFagsaker, + ) + } + } + + companion object { + val logger = LoggerFactory.getLogger(AutovedtakSatsendringScheduler::class.java) + const val CRON_HVERT_10_MIN_UKEDAG = "0 */10 7-18 * * MON-FRI" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/AutovedtakSatsendringService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/AutovedtakSatsendringService.kt new file mode 100644 index 000000000..ac309e3e0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/AutovedtakSatsendringService.kt @@ -0,0 +1,215 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.satsendring + +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.UtbetalingsikkerhetFeil +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.config.FeatureToggleConfig.Companion.SATSENDRING_SNIKE_I_KØEN +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakService +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene.Satskjøring +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene.SatskjøringRepository +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.SettPåMaskinellVentÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.SnikeIKøenService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValideringService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.task.FerdigstillBehandlingTask +import no.nav.familie.ba.sak.task.IverksettMotOppdragTask +import no.nav.familie.ba.sak.task.SatsendringTaskDto +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Service +class AutovedtakSatsendringService( + private val taskRepository: TaskRepositoryWrapper, + private val behandlingRepository: BehandlingRepository, + private val autovedtakService: AutovedtakService, + private val satskjøringRepository: SatskjøringRepository, + private val behandlingService: BehandlingService, + private val satsendringService: SatsendringService, + private val loggService: LoggService, + private val featureToggleService: FeatureToggleService, + private val snikeIKøenService: SnikeIKøenService, + private val tilkjentYtelseValideringService: TilkjentYtelseValideringService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, +) { + + private val satsendringAlleredeUtført = Metrics.counter("satsendring.allerede.utfort") + private val satsendringIverksatt = Metrics.counter("satsendring.iverksatt") + private val satsendringIgnorertÅpenBehandling = Metrics.counter("satsendring.ignorert.aapenbehandling") + + /** + * Gjennomfører og commiter revurderingsbehandling + * med årsak satsendring og uten endring i vilkår. + * + */ + @Transactional + fun kjørBehandling(behandlingsdata: SatsendringTaskDto): SatsendringSvar { + val fagsakId = behandlingsdata.fagsakId + + val satskjøringForFagsak = + satskjøringRepository.findByFagsakIdAndSatsTidspunkt(fagsakId, behandlingsdata.satstidspunkt) + ?: satskjøringRepository.save(Satskjøring(fagsakId = fagsakId, satsTidspunkt = behandlingsdata.satstidspunkt)) + + val sisteVedtatteBehandling = behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId) ?: error("Fant ikke siste vedtatte behandling for $fagsakId") + + if (satsendringService.erFagsakOppdatertMedSisteSatser(fagsakId)) { + satskjøringForFagsak.ferdigTidspunkt = LocalDateTime.now() + satskjøringRepository.save(satskjøringForFagsak) + logger.info("Satsendring allerede utført for fagsak=$fagsakId") + satsendringAlleredeUtført.increment() + return SatsendringSvar.SATSENDRING_ER_ALLEREDE_UTFØRT + } + + val aktivOgÅpenBehandling = + behandlingRepository.findByFagsakAndAktivAndOpen(fagsakId = sisteVedtatteBehandling.fagsak.id) + val søkerAktør = sisteVedtatteBehandling.fagsak.aktør + + logger.info("Kjører satsendring på $sisteVedtatteBehandling") + secureLogger.info("Kjører satsendring på $sisteVedtatteBehandling for ${søkerAktør.aktivFødselsnummer()}") + if (sisteVedtatteBehandling.fagsak.status != FagsakStatus.LØPENDE) throw Feil("Forsøker å utføre satsendring på ikke løpende fagsak ${sisteVedtatteBehandling.fagsak.id}") + + if (aktivOgÅpenBehandling != null) { + val brukerHarÅpenBehandlingSvar = hentBrukerHarÅpenBehandlingSvar(aktivOgÅpenBehandling) + if (brukerHarÅpenBehandlingSvar == SatsendringSvar.BEHANDLING_KAN_SNIKES_FORBI && + featureToggleService.isEnabled(SATSENDRING_SNIKE_I_KØEN) + ) { + snikeIKøenService.settAktivBehandlingTilPåMaskinellVent( + aktivOgÅpenBehandling.id, + SettPåMaskinellVentÅrsak.SATSENDRING, + ) + } else { + satskjøringForFagsak.feiltype = brukerHarÅpenBehandlingSvar.name + satskjøringRepository.save(satskjøringForFagsak) + + logger.info(brukerHarÅpenBehandlingSvar.melding) + satsendringIgnorertÅpenBehandling.increment() + + return brukerHarÅpenBehandlingSvar + } + } + + if (harUtbetalingerSomOverstiger100Prosent(sisteVedtatteBehandling)) { + logger.warn("Det løper over 100 prosent utbetaling på fagsak=${sisteVedtatteBehandling.fagsak.id}") + } + + val behandlingEtterBehandlingsresultat = + autovedtakService.opprettAutomatiskBehandlingOgKjørTilBehandlingsresultat( + aktør = søkerAktør, + behandlingType = BehandlingType.REVURDERING, + behandlingÅrsak = BehandlingÅrsak.SATSENDRING, + fagsakId = sisteVedtatteBehandling.fagsak.id, + ) + + val opprettetVedtak = + autovedtakService.opprettToTrinnskontrollOgVedtaksbrevForAutomatiskBehandling( + behandlingEtterBehandlingsresultat, + ) + + val task = when (behandlingEtterBehandlingsresultat.steg) { + StegType.IVERKSETT_MOT_OPPDRAG -> { + IverksettMotOppdragTask.opprettTask( + behandlingEtterBehandlingsresultat, + opprettetVedtak, + SikkerhetContext.hentSaksbehandler(), + ) + } + + StegType.FERDIGSTILLE_BEHANDLING -> { + behandlingService.oppdaterStatusPåBehandling( + behandlingEtterBehandlingsresultat.id, + BehandlingStatus.IVERKSETTER_VEDTAK, + ) + FerdigstillBehandlingTask.opprettTask( + søkerAktør.aktivFødselsnummer(), + behandlingEtterBehandlingsresultat.id, + ) + } + + else -> throw Feil("Ugyldig neste steg ${behandlingEtterBehandlingsresultat.steg} ved satsendring for fagsak=$fagsakId") + } + + satskjøringForFagsak.ferdigTidspunkt = LocalDateTime.now() + satskjøringRepository.save(satskjøringForFagsak) + taskRepository.save(task) + + satsendringIverksatt.increment() + + return SatsendringSvar.SATSENDRING_KJØRT_OK + } + + private fun hentBrukerHarÅpenBehandlingSvar( + aktivOgÅpenBehandling: Behandling, + ): SatsendringSvar { + val status = aktivOgÅpenBehandling.status + return when { + status != BehandlingStatus.UTREDES && status != BehandlingStatus.SATT_PÅ_VENT -> + SatsendringSvar.BEHANDLING_ER_LÅST_SATSENDRING_TRIGGES_NESTE_VIRKEDAG + kanSnikeIKøen(aktivOgÅpenBehandling) -> SatsendringSvar.BEHANDLING_KAN_SNIKES_FORBI + else -> SatsendringSvar.BEHANDLING_KAN_IKKE_SETTES_PÅ_VENT + } + } + + private fun kanSnikeIKøen(aktivOgÅpenBehandling: Behandling): Boolean { + val behandlingId = aktivOgÅpenBehandling.id + val loggSuffix = "endrer status på behandling til på vent" + if (aktivOgÅpenBehandling.status == BehandlingStatus.SATT_PÅ_VENT) { + logger.info("Behandling=$behandlingId er satt på vent av saksbehandler, $loggSuffix") + return true + } + val sisteLogghendelse = loggService.hentLoggForBehandling(behandlingId).maxBy { it.opprettetTidspunkt } + val tid4TimerSiden = LocalDateTime.now().minusHours(4) + if (aktivOgÅpenBehandling.endretTidspunkt.isAfter(tid4TimerSiden)) { + logger.info( + "Behandling=$behandlingId har endretTid=${aktivOgÅpenBehandling.endretTidspunkt} " + + "kan ikke sette behandlingen på maskinell vent", + ) + return false + } + if (sisteLogghendelse.opprettetTidspunkt.isAfter(tid4TimerSiden)) { + logger.info( + "Behandling=$behandlingId siste logginslag er " + + "type=${sisteLogghendelse.type} tid=${sisteLogghendelse.opprettetTidspunkt}, $loggSuffix", + ) + return false + } + return true + } + + private fun harUtbetalingerSomOverstiger100Prosent(sisteIverksatteBehandling: Behandling): Boolean { + try { + tilkjentYtelseValideringService.validerAtBarnIkkeFårFlereUtbetalingerSammePeriode(sisteIverksatteBehandling) + } catch (e: UtbetalingsikkerhetFeil) { + secureLogger.info("fagsakId=${sisteIverksatteBehandling.fagsak.id} har UtbetalingsikkerhetFeil. Skipper satsendring: ${e.frontendFeilmelding}") + return true + } + return false + } + + companion object { + val logger = LoggerFactory.getLogger(AutovedtakSatsendringService::class.java) + } +} + +enum class SatsendringSvar(val melding: String) { + SATSENDRING_KJØRT_OK(melding = "Satsendring kjørt OK"), + SATSENDRING_ER_ALLEREDE_UTFØRT(melding = "Satsendring allerede utført for fagsak"), + BEHANDLING_ER_LÅST_SATSENDRING_TRIGGES_NESTE_VIRKEDAG( + melding = "Behandlingen er låst for endringer og satsendring vil bli trigget neste virkedag.", + ), + BEHANDLING_KAN_SNIKES_FORBI("Behandling kan snikes forbi (toggle er slått av)"), + BEHANDLING_KAN_IKKE_SETTES_PÅ_VENT("Behandlingen kan ikke settes på vent"), +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringController.kt new file mode 100644 index 000000000..20a5516e9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringController.kt @@ -0,0 +1,107 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.satsendring + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import no.nav.familie.ba.sak.common.RessursUtils.badRequest +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.kjerne.behandling.HenleggÅrsak +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.slf4j.LoggerFactory +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate +import java.util.UUID + +const val SATSENDRING = "Satsendring" + +@RestController +@RequestMapping("/api/satsendring") +@ProtectedWithClaims(issuer = "azuread") +class SatsendringController( + private val startSatsendring: StartSatsendring, + private val tilgangService: TilgangService, + private val opprettTaskService: OpprettTaskService, + private val satsendringService: SatsendringService, +) { + private val logger = LoggerFactory.getLogger(SatsendringController::class.java) + + @GetMapping(path = ["/kjorsatsendring/{fagsakId}"]) + fun utførSatsendringITaskPåFagsak(@PathVariable fagsakId: Long): ResponseEntity> { + startSatsendring.opprettSatsendringForFagsak(fagsakId) + return ResponseEntity.ok(Ressurs.success("Trigget satsendring for fagsak $fagsakId")) + } + + @PostMapping(path = ["/kjorsatsendring"]) + fun utførSatsendringITaskPåFagsaker(@RequestBody fagsaker: Set): ResponseEntity> { + fagsaker.forEach { startSatsendring.opprettSatsendringForFagsak(it) } + return ResponseEntity.ok(Ressurs.success("Trigget satsendring for fagsakene $fagsaker")) + } + + @PutMapping(path = ["/{fagsakId}/kjor-satsendring-synkront"]) + fun utførSatsendringSynkrontPåFagsak(@PathVariable fagsakId: Long): ResponseEntity> { + tilgangService.validerTilgangTilHandlingOgFagsak( + fagsakId = fagsakId, + handling = "Valider vi kan kjøre satsendring", + event = AuditLoggerEvent.UPDATE, + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + ) + + startSatsendring.gjennomførSatsendringManuelt(fagsakId) + return ResponseEntity.ok(Ressurs.success(Unit)) + } + + @GetMapping(path = ["/{fagsakId}/kan-kjore-satsendring"]) + fun kanKjøreSatsendringPåFagsak(@PathVariable fagsakId: Long): ResponseEntity> { + return ResponseEntity.ok(Ressurs.success(startSatsendring.kanGjennomføreSatsendringManuelt(fagsakId))) + } + + @PostMapping(path = ["/kjorsatsendringForListeMedIdenter"]) + fun utførSatsendringPåListeIdenter(@RequestBody listeMedIdenter: Set): ResponseEntity> { + listeMedIdenter.forEach { + startSatsendring.sjekkOgOpprettSatsendringVedGammelSats(it) + } + return ResponseEntity.ok(Ressurs.success("Trigget satsendring for liste med identer ${listeMedIdenter.size}")) + } + + @PostMapping(path = ["/henleggBehandlingerMedLangFristSenereEnn/{valideringsdato}"]) + fun henleggBehandlingerMedLangLiggetid( + @RequestBody behandlinger: Set, + @PathVariable valideringsdato: String, + ): ResponseEntity> { + val dato = try { + LocalDate.parse(valideringsdato).also { assert(it.isAfter(LocalDate.now().plusMonths(1))) } + } catch (e: Exception) { + return badRequest("Ugyldig dato", e) + } + behandlinger.forEach { + opprettTaskService.opprettHenleggBehandlingTask( + behandlingId = it.toLong(), + årsak = HenleggÅrsak.TEKNISK_VEDLIKEHOLD, + begrunnelse = SATSENDRING, + validerOppgavefristErEtterDato = dato, + ) + } + return ResponseEntity.ok(Ressurs.Companion.success("Trigget henleggelse for ${behandlinger.size} behandlinger")) + } + + @PostMapping(path = ["/saker-uten-sats"]) + fun finnSakerUtenSisteSats(): ResponseEntity> { + val callId = UUID.randomUUID().toString() + val scope = CoroutineScope(SupervisorJob()) + scope.launch { + satsendringService.finnLøpendeFagsakerUtenSisteSats(callId) + } + return ResponseEntity.ok(Pair("callId", callId)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringService.kt new file mode 100644 index 000000000..564a45e6c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringService.kt @@ -0,0 +1,60 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.satsendring + +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.log.mdc.MDCConstants +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Slice +import org.springframework.stereotype.Service +import java.util.stream.Collectors + +@Service +class SatsendringService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + private val fagsakRepository: FagsakRepository, +) { + private val logger = LoggerFactory.getLogger(SatsendringService::class.java) + fun erFagsakOppdatertMedSisteSatser(fagsakId: Long): Boolean { + val sisteVedtatteBehandling = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId) + + return sisteVedtatteBehandling == null || + andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnAndelerTilkjentYtelseMedEndreteUtbetalinger(sisteVedtatteBehandling.id) + .erOppdatertMedSisteSatser() + } + + fun finnLøpendeFagsakerUtenSisteSats(callId: String) { + MDC.put(MDCConstants.MDC_CALL_ID, callId) + val fagsakerUtenSisteSats = mutableListOf() + var slice: Slice = fagsakRepository.finnLøpendeFagsaker(PageRequest.of(0, 10000)) + val løpendeFagsaker: List = slice.getContent() + fagsakerUtenSisteSats.addAll( + løpendeFagsaker.parallelStream().filter { + !erFagsakOppdatertMedSisteSatser(it) + }.collect( + Collectors.toList(), + ), + ) + + while (slice.hasNext()) { + logger.info("Next slice") + slice = fagsakRepository.finnLøpendeFagsaker(slice.nextPageable()) + fagsakerUtenSisteSats.addAll( + slice.get().toList().parallelStream().filter { + !erFagsakOppdatertMedSisteSatser(it) + }.collect( + Collectors.toList(), + ), + ) + } + logger.warn("Følgende saker mangler satsendring:") + fagsakerUtenSisteSats.chunked(1000) { + logger.warn("$it") + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringStatistikk.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringStatistikk.kt new file mode 100644 index 000000000..a1f600260 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringStatistikk.kt @@ -0,0 +1,80 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.satsendring + +import io.micrometer.core.instrument.Metrics +import io.micrometer.core.instrument.MultiGauge +import io.micrometer.core.instrument.Tags +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene.SatskjøringRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.log.mdc.MDCConstants +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import java.util.UUID +import java.util.concurrent.TimeUnit + +@Component +class SatsendringStatistikk( + private val fagsakRepository: FagsakRepository, + private val satskjøringRepository: SatskjøringRepository, + private val startSatsendring: StartSatsendring, +) { + + val satsendringGauge = + MultiGauge.builder("satsendring").register(Metrics.globalRegistry) + + @Scheduled( + fixedDelay = 60, + timeUnit = TimeUnit.MINUTES, + initialDelay = 5, + ) + fun antallSatsendringerKjørt() { + try { + MDC.put(MDCConstants.MDC_CALL_ID, UUID.randomUUID().toString()) + logger.info("Kjører statistikk satsendring") + val satsTidspunkt = startSatsendring.hentAktivSatsendringstidspunkt() + val antallKjørt = satskjøringRepository.countByFerdigTidspunktIsNotNullAndSatsTidspunkt(satsTidspunkt) + val antallTriggetTotalt = satskjøringRepository.countBySatsTidspunkt(satsTidspunkt) + val antallLøpendeFagsakerTotalt = fagsakRepository.finnAntallFagsakerLøpende() + + val rows = listOf( + MultiGauge.Row.of( + Tags.of( + "satsendring", + "totalt", + ), + antallTriggetTotalt, + ), + MultiGauge.Row.of( + Tags.of( + "satsendring", + "antallkjort", + ), + antallKjørt, + ), + MultiGauge.Row.of( + Tags.of( + "satsendring", + "antallfagsaker", + ), + antallLøpendeFagsakerTotalt, + ), + MultiGauge.Row.of( + Tags.of( + "satsendring", + "antallgjenstaaende", + ), + antallLøpendeFagsakerTotalt - antallKjørt, + ), + ) + + satsendringGauge.register(rows, true) + } finally { + MDC.clear() + } + } + + companion object { + private val logger = LoggerFactory.getLogger(SatsendringStatistikk::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringUtil.kt new file mode 100644 index 000000000..427f00f27 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringUtil.kt @@ -0,0 +1,30 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.satsendring + +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.SatsService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import java.math.BigDecimal + +fun List.erOppdatertMedSisteSatser(): Boolean = + SatsType.values() + .filter { it != SatsType.FINN_SVAL } + .all { this.erOppdatertFor(it) } + +private fun List.erOppdatertFor(satstype: SatsType): Boolean { + val sisteSatsForSatstype = SatsService.finnSisteSatsFor(satstype) + val fomSisteSatsForSatstype = sisteSatsForSatstype.gyldigFom.toYearMonth() + + val satsTyperMedTilsvarendeYtelsestype = satstype + .tilYtelseType() + .hentSatsTyper() + + return this.filter { it.stønadTom.isSameOrAfter(fomSisteSatsForSatstype) } + .filter { it.type == satstype.tilYtelseType() } + .filter { it.prosent != BigDecimal.ZERO } + .all { andelTilkjentYtelse -> + satsTyperMedTilsvarendeYtelsestype + .any { andelTilkjentYtelse.sats == SatsService.finnSisteSatsFor(it).beløp } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/StartSatsendring.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/StartSatsendring.kt new file mode 100644 index 000000000..fb8c1bd21 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/StartSatsendring.kt @@ -0,0 +1,178 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.satsendring + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene.SatskjøringRepository +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.ba.sak.task.SatsendringTaskDto +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.YearMonth + +@Service +class StartSatsendring( + private val fagsakRepository: FagsakRepository, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val opprettTaskService: OpprettTaskService, + private val satskjøringRepository: SatskjøringRepository, + private val featureToggleService: FeatureToggleService, + private val personidentService: PersonidentService, + private val autovedtakSatsendringService: AutovedtakSatsendringService, + private val satsendringService: SatsendringService, +) { + + private val ignorerteFagsaker = mutableSetOf() + + @Transactional + fun startSatsendring( + antallFagsaker: Int, + ) { + if (!featureToggleService.isEnabled(FeatureToggleConfig.SATSENDRING_ENABLET, false)) { + logger.info("Skipper satsendring da toggle er skrudd av.") + return + } + + var antallSatsendringerStartet = 0 + var startSide = 0 + while (antallSatsendringerStartet < antallFagsaker) { + val page = fagsakRepository.finnLøpendeFagsakerForSatsendring( + hentAktivSatsendringstidspunkt().atDay(1), + Pageable.ofSize(antallFagsaker + 200).withPage(startSide), + ) + + val fagsakerForSatsendring = page.toList() + logger.info("Fant ${fagsakerForSatsendring.size} personer for satsendring på side $startSide") + if (fagsakerForSatsendring.isNotEmpty()) { + antallSatsendringerStartet = + oppretteEllerSkipSatsendring( + fagsakerForSatsendring, + antallSatsendringerStartet, + antallFagsaker, + hentAktivSatsendringstidspunkt(), + ) + } + logger.info("Opprettet $antallSatsendringerStartet satsendringer (akkumulerende)") + + if (++startSide >= page.totalPages) break + } + } + + private fun oppretteEllerSkipSatsendring( + fagsakForSatsendring: List, + antallAlleredeTriggetSatsendring: Int, + antallFagsakerTilSatsendring: Int, + satsTidspunkt: YearMonth, + ): Int { + var antallFagsakerSatsendring = antallAlleredeTriggetSatsendring + + for (fagsakId in fagsakForSatsendring) { + if (skalTriggeSatsendring(fagsakId, satsTidspunkt)) { + antallFagsakerSatsendring++ + } + + if (antallFagsakerSatsendring == antallFagsakerTilSatsendring) { + return antallFagsakerSatsendring + } + } + return antallFagsakerSatsendring + } + + private fun skalTriggeSatsendring(fagsakId: Long, satsTidspunkt: YearMonth): Boolean { + if (ignorerteFagsaker.contains(fagsakId)) { + return false + } + + val sisteVedtatteBehandling = behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId) + return if (sisteVedtatteBehandling != null) { + opprettTaskService.opprettSatsendringTask(fagsakId, satsTidspunkt) + true + } else { + logger.info("Satsendring trigges ikke på fagsak=$fagsakId fordi fagsaken mangler en vedtatt behandling") + ignorerteFagsaker.add(fagsakId) + false + } + } + + fun sjekkOgOpprettSatsendringVedGammelSats(ident: String): Boolean { + val aktør = personidentService.hentAktør(ident) + val løpendeFagsakerForAktør = fagsakRepository.finnFagsakerForAktør(aktør) + .filter { !it.arkivert && it.status == FagsakStatus.LØPENDE } + + var harOpprettetSatsendring = false + løpendeFagsakerForAktør.forEach { fagsak -> + if (opprettSatsendringTaskVedGammelSats(fagsak.id)) { + harOpprettetSatsendring = true + } + } + return harOpprettetSatsendring + } + + fun sjekkOgOpprettSatsendringVedGammelSats(fagsakId: Long): Boolean { + return opprettSatsendringTaskVedGammelSats(fagsakId) + } + + private fun opprettSatsendringTaskVedGammelSats(fagsakId: Long): Boolean = + if (kanStarteSatsendringPåFagsak(fagsakId)) { + logger.info("Oppretter satsendringtask fagsakID=$fagsakId") + opprettSatsendringForFagsak(fagsakId = fagsakId) + true + } else { + false + } + + fun kanStarteSatsendringPåFagsak(fagsakId: Long): Boolean { + return satskjøringRepository.findByFagsakIdAndSatsTidspunkt(fagsakId, hentAktivSatsendringstidspunkt()) == null && + !satsendringService.erFagsakOppdatertMedSisteSatser(fagsakId) + } + + fun kanGjennomføreSatsendringManuelt(fagsakId: Long): Boolean = + !satsendringService.erFagsakOppdatertMedSisteSatser(fagsakId) + + @Transactional + fun gjennomførSatsendringManuelt(fagsakId: Long) { + if (!kanGjennomføreSatsendringManuelt(fagsakId)) { + throw Feil("Kan ikke starte Satsendring på fagsak=$fagsakId") + } + + val resultatSatsendringBehandling = autovedtakSatsendringService.kjørBehandling( + SatsendringTaskDto(fagsakId = fagsakId, hentAktivSatsendringstidspunkt()), + ) + + when (resultatSatsendringBehandling) { + SatsendringSvar.SATSENDRING_KJØRT_OK -> Unit + + SatsendringSvar.SATSENDRING_ER_ALLEREDE_UTFØRT -> + throw FunksjonellFeil("Satsendring er allerede gjennomført på fagsaken. Last inn siden på nytt for å få opp siste behandling.") + + SatsendringSvar.BEHANDLING_ER_LÅST_SATSENDRING_TRIGGES_NESTE_VIRKEDAG, + SatsendringSvar.BEHANDLING_KAN_IKKE_SETTES_PÅ_VENT, + -> + throw FunksjonellFeil("Det finnes en åpen behandling på fagsaken som må avsluttes før satsendring kan gjennomføres.") + SatsendringSvar.BEHANDLING_KAN_SNIKES_FORBI -> + throw FunksjonellFeil(resultatSatsendringBehandling.melding) + } + } + + fun hentAktivSatsendringstidspunkt(): YearMonth { + return SATSENDRINGMÅNED_JULI_2023 + } + + fun opprettSatsendringForFagsak(fagsakId: Long) { + opprettTaskService.opprettSatsendringTask(fagsakId, hentAktivSatsendringstidspunkt()) + } + + companion object { + val logger: Logger = LoggerFactory.getLogger(StartSatsendring::class.java) + val SATSENDRINGMÅNED_MARS_2023: YearMonth = YearMonth.of(2023, 3) + val SATSENDRINGMÅNED_JULI_2023: YearMonth = YearMonth.of(2023, 7) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/domene/Satskj\303\270ring.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/domene/Satskj\303\270ring.kt" new file mode 100644 index 000000000..3b2665070 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/domene/Satskj\303\270ring.kt" @@ -0,0 +1,59 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene + +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.YearMonthConverter +import org.hibernate.Hibernate +import java.time.LocalDateTime +import java.time.YearMonth + +@Entity(name = "Satskjøring") +@Table(name = "satskjoering") +data class Satskjøring( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "satskjoering_seq_generator") + @SequenceGenerator( + name = "satskjoering_seq_generator", + sequenceName = "satskjoering_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "fk_fagsak_id", nullable = false, updatable = false, unique = true) + val fagsakId: Long, + + @Column(name = "start_tid", nullable = false, updatable = false) + val startTidspunkt: LocalDateTime = LocalDateTime.now(), + + @Column(name = "ferdig_tid") + var ferdigTidspunkt: LocalDateTime? = null, + + @Column(name = "feiltype") + var feiltype: String? = null, + + @Column(name = "sats_tid", columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + val satsTidspunkt: YearMonth, +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + other as Satskjøring + + return id == other.id + } + + override fun hashCode(): Int = javaClass.hashCode() + + @Override + override fun toString(): String { + return this::class.simpleName + "(id = $id , fagsakId = $fagsakId )" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/domene/Satskj\303\270ringRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/domene/Satskj\303\270ringRepository.kt" new file mode 100644 index 000000000..c09f0c2ec --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/domene/Satskj\303\270ringRepository.kt" @@ -0,0 +1,13 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import java.time.YearMonth + +@Repository +interface SatskjøringRepository : JpaRepository { + fun countByFerdigTidspunktIsNotNullAndSatsTidspunkt(satsTidspunkt: YearMonth): Long + + fun countBySatsTidspunkt(satsTidspunkt: YearMonth): Long + fun findByFagsakIdAndSatsTidspunkt(fagsakId: Long, satsTidspunkt: YearMonth): Satskjøring? +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/sm\303\245barnstillegg/AutovedtakSm\303\245barnstilleggService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/sm\303\245barnstillegg/AutovedtakSm\303\245barnstilleggService.kt" new file mode 100644 index 000000000..b61b79020 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/sm\303\245barnstillegg/AutovedtakSm\303\245barnstilleggService.kt" @@ -0,0 +1,205 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.småbarnstillegg + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakBehandlingService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakStegService +import no.nav.familie.ba.sak.kjerne.autovedtak.SmåbarnstilleggData +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.SmåbarnstilleggService +import no.nav.familie.ba.sak.kjerne.beregning.VedtaksperiodefinnerSmåbarnstilleggFeil +import no.nav.familie.ba.sak.kjerne.beregning.finnAktuellVedtaksperiodeOgLeggTilSmåbarnstilleggbegrunnelse +import no.nav.familie.ba.sak.kjerne.beregning.hentInnvilgedeOgReduserteAndelerSmåbarnstillegg +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.task.IverksettMotOppdragTask +import no.nav.familie.ba.sak.task.dto.ManuellOppgaveType +import no.nav.familie.prosessering.internal.TaskService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AutovedtakSmåbarnstilleggService( + private val fagsakService: FagsakService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val vedtakService: VedtakService, + private val behandlingService: BehandlingService, + private val vedtaksperiodeService: VedtaksperiodeService, + private val småbarnstilleggService: SmåbarnstilleggService, + private val taskService: TaskService, + private val beregningService: BeregningService, + private val autovedtakService: AutovedtakService, + private val oppgaveService: OppgaveService, + private val vedtaksperiodeHentOgPersisterService: VedtaksperiodeHentOgPersisterService, +) : AutovedtakBehandlingService { + + private val antallVedtakOmOvergangsstønad: Counter = + Metrics.counter("behandling", "saksbehandling", "hendelse", "smaabarnstillegg", "antall") + private val antallVedtakOmOvergangsstønadPåvirkerFagsak: Counter = + Metrics.counter("behandling", "saksbehandling", "hendelse", "smaabarnstillegg", "paavirker_fagsak") + private val antallVedtakOmOvergangsstønadPåvirkerIkkeFagsak: Counter = + Metrics.counter("behandling", "saksbehandling", "hendelse", "smaabarnstillegg", "paavirker_ikke_fagsak") + + enum class TilManuellBehandlingÅrsak(val beskrivelse: String) { + NYE_UTBETALINGSPERIODER_FØRER_TIL_MANUELL_BEHANDLING("Endring i OS gir etterbetaling, feilutbetaling eller endring mer enn 1 måned frem i tid"), + KLARER_IKKE_BEGRUNNE("Klarer ikke å begrunne"), + } + + private val antallVedtakOmOvergangsstønadTilManuellBehandling: Map = + TilManuellBehandlingÅrsak.values().associateWith { + Metrics.counter( + "behandling", + "saksbehandling", + "hendelse", + "smaabarnstillegg", + "til_manuell_behandling", + "aarsak", + it.name, + "beskrivelse", + it.beskrivelse, + ) + } + + override fun skalAutovedtakBehandles(behandlingsdata: SmåbarnstilleggData): Boolean { + val fagsak = fagsakService.hentNormalFagsak(aktør = behandlingsdata.aktør) ?: return false + val påvirkerFagsak = småbarnstilleggService.vedtakOmOvergangsstønadPåvirkerFagsak(fagsak) + return if (!påvirkerFagsak) { + antallVedtakOmOvergangsstønadPåvirkerIkkeFagsak.increment() + + logger.info("Påvirker ikke fagsak") + false + } else { + antallVedtakOmOvergangsstønadPåvirkerFagsak.increment() + true + } + } + + @Transactional + override fun kjørBehandling(behandlingsdata: SmåbarnstilleggData): String { + antallVedtakOmOvergangsstønad.increment() + val aktør = behandlingsdata.aktør + val fagsak = fagsakService.hentNormalFagsak(aktør) + ?: throw Feil(message = "Fant ikke fagsak av typen NORMAL for aktør ${aktør.aktørId}") + val behandlingEtterBehandlingsresultat = + autovedtakService.opprettAutomatiskBehandlingOgKjørTilBehandlingsresultat( + aktør = aktør, + behandlingType = BehandlingType.REVURDERING, + behandlingÅrsak = BehandlingÅrsak.SMÅBARNSTILLEGG, + fagsakId = fagsak.id, + ) + + if (behandlingEtterBehandlingsresultat.status != BehandlingStatus.IVERKSETTER_VEDTAK) { + return kanIkkeBehandleAutomatisk( + behandling = behandlingEtterBehandlingsresultat, + metric = antallVedtakOmOvergangsstønadTilManuellBehandling[TilManuellBehandlingÅrsak.NYE_UTBETALINGSPERIODER_FØRER_TIL_MANUELL_BEHANDLING]!!, + meldingIOppgave = "Småbarnstillegg: endring i overgangsstønad må behandles manuelt", + ) + } + + try { + begrunnAutovedtakForSmåbarnstillegg(behandlingEtterBehandlingsresultat) + } catch (e: VedtaksperiodefinnerSmåbarnstilleggFeil) { + logger.warn(e.message, e) + + val behandlingSomSkalManueltBehandles = behandlingService.oppdaterStatusPåBehandling( + behandlingEtterBehandlingsresultat.id, + BehandlingStatus.UTREDES, + ) + return kanIkkeBehandleAutomatisk( + behandling = behandlingSomSkalManueltBehandles, + metric = antallVedtakOmOvergangsstønadTilManuellBehandling[TilManuellBehandlingÅrsak.KLARER_IKKE_BEGRUNNE]!!, + meldingIOppgave = "Småbarnstillegg: klarer ikke bestemme vedtaksperiode som skal begrunnes, må behandles manuelt", + ) + } + + val vedtakEtterTotrinn = autovedtakService.opprettToTrinnskontrollOgVedtaksbrevForAutomatiskBehandling( + behandlingEtterBehandlingsresultat, + ) + + val task = IverksettMotOppdragTask.opprettTask( + behandlingEtterBehandlingsresultat, + vedtakEtterTotrinn, + SikkerhetContext.hentSaksbehandler(), + ) + taskService.save(task) + + return AutovedtakStegService.BEHANDLING_FERDIG + } + + private fun begrunnAutovedtakForSmåbarnstillegg( + behandlingEtterBehandlingsresultat: Behandling, + ) { + val sistIverksatteBehandling = + behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId = behandlingEtterBehandlingsresultat.fagsak.id) + val forrigeSmåbarnstilleggAndeler = + if (sistIverksatteBehandling == null) { + emptyList() + } else { + beregningService.hentAndelerTilkjentYtelseMedUtbetalingerForBehandling( + behandlingId = sistIverksatteBehandling.id, + ).filter { it.erSmåbarnstillegg() } + } + + val nyeSmåbarnstilleggAndeler = + if (sistIverksatteBehandling == null) { + emptyList() + } else { + beregningService.hentAndelerTilkjentYtelseMedUtbetalingerForBehandling( + behandlingId = behandlingEtterBehandlingsresultat.id, + ).filter { it.erSmåbarnstillegg() } + } + + val (innvilgedeMånedPerioder, reduserteMånedPerioder) = hentInnvilgedeOgReduserteAndelerSmåbarnstillegg( + forrigeSmåbarnstilleggAndeler = forrigeSmåbarnstilleggAndeler, + nyeSmåbarnstilleggAndeler = nyeSmåbarnstilleggAndeler, + ) + + vedtaksperiodeHentOgPersisterService.lagre( + finnAktuellVedtaksperiodeOgLeggTilSmåbarnstilleggbegrunnelse( + innvilgetMånedPeriode = innvilgedeMånedPerioder.singleOrNull(), + redusertMånedPeriode = reduserteMånedPerioder.singleOrNull(), + vedtaksperioderMedBegrunnelser = vedtaksperiodeService.hentPersisterteVedtaksperioder( + vedtak = vedtakService.hentAktivForBehandlingThrows( + behandlingId = behandlingEtterBehandlingsresultat.id, + ), + ), + ), + ) + } + + private fun kanIkkeBehandleAutomatisk( + behandling: Behandling, + metric: Counter, + meldingIOppgave: String, + ): String { + metric.increment() + val omgjortBehandling = autovedtakService.omgjørBehandlingTilManuellOgKjørSteg( + behandling = behandling, + steg = StegType.VILKÅRSVURDERING, + ) + return oppgaveService.opprettOppgaveForManuellBehandling( + behandling = omgjortBehandling, + begrunnelse = meldingIOppgave, + opprettLogginnslag = true, + manuellOppgaveType = ManuellOppgaveType.SMÅBARNSTILLEGG, + ) + } + + companion object { + val logger = LoggerFactory.getLogger(AutovedtakSmåbarnstilleggService::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/sm\303\245barnstillegg/RestartAvSm\303\245barnstilleggService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/sm\303\245barnstillegg/RestartAvSm\303\245barnstilleggService.kt" new file mode 100644 index 000000000..e134662db --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/sm\303\245barnstillegg/RestartAvSm\303\245barnstilleggService.kt" @@ -0,0 +1,136 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.småbarnstillegg + +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingMigreringsinfoRepository +import no.nav.familie.ba.sak.kjerne.beregning.SatsService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.erAlleredeBegrunnetMedBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.leader.LeaderClient +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.YearMonth + +@Service +class RestartAvSmåbarnstilleggService( + private val fagsakRepository: FagsakRepository, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val opprettTaskService: OpprettTaskService, + private val vedtakService: VedtakService, + private val vedtaksperiodeService: VedtaksperiodeService, + private val behandlingMigreringsinfoRepository: BehandlingMigreringsinfoRepository, + private val andelerTilkjentYtelseRepository: AndelTilkjentYtelseRepository, +) { + + /** + * Første dag hver måned sjekkes det om noen fagsaker har oppstart av småbarnstillegg inneværende måned, etter å ha + * hatt et opphold. Hvis perioden ikke allerede er begrunnet, skal det opprettes en "vurder livshendelse"-oppgave + * med mindre forrige behandling var en migrering fra Infotrygd. + */ + @Scheduled(cron = "0 0 7 1 * *") + @Transactional + fun scheduledFinnRestartetSmåbarnstilleggOgOpprettOppgave() { + if (LeaderClient.isLeader() == true) { + finnOgOpprettetOppgaveForSmåbarnstilleggSomSkalRestartesIDenneMåned(true) + } + } + + fun finnOgOpprettetOppgaveForSmåbarnstilleggSomSkalRestartesIDenneMåned(skalOppretteOppgaver: Boolean) { + logger.info("Starter jobb for å finne småbarnstillegg som skal restartes, men som ikke allerede begrunnet. skalOppretteOppgaver=$skalOppretteOppgaver") + finnAlleFagsakerMedRestartetSmåbarnstilleggIMåned().forEach { fagsakId -> + logger.info("Oppretter 'vurder livshendelse'-oppgave på fagsak $fagsakId fordi småbarnstillegg har startet opp igjen denne måneden") + + val sisteIverksatteBehandling = + behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId = fagsakId) + + if (sisteIverksatteBehandling != null) { + if (skalOppretteOppgaver) { + opprettTaskService.opprettOppgaveTask( + behandlingId = sisteIverksatteBehandling.id, + oppgavetype = Oppgavetype.VurderLivshendelse, + beskrivelse = "Småbarnstillegg: endring i overgangsstønad må behandles manuelt", + ) + } else { + logger.info("DryRun av RestartAvSmåbarnstilleggService. Ville ha opprettet en VurderLivshendelse for behandling=${sisteIverksatteBehandling.id}, fagsakId=$fagsakId") + } + } + } + logger.info("Avslutter jobb for å finne småbarnstillegg som skal restartes, men som ikke allerede begrunnet") + } + + fun finnAlleFagsakerMedRestartetSmåbarnstilleggIMåned(måned: YearMonth = YearMonth.now()): List { + return behandlingHentOgPersisterService.partitionByIverksatteBehandlinger { + finnAlleFagsakerMedOppstartSmåbarnstilleggIMåned(iverksatteLøpendeBehandlinger = it, måned = måned) + }.filter { fagsakId -> + val migreringsdato = behandlingMigreringsinfoRepository.finnSisteMigreringsdatoPåFagsak(fagsakId) + migreringsdato?.month != LocalDate.now().minusMonths(1).month + }.filter { fagsakId -> + !periodeMedRestartetSmåbarnstilleggErAlleredeBegrunnet(fagsakId = fagsakId, måned = måned) + } + } + + private fun finnAlleFagsakerMedOppstartSmåbarnstilleggIMåned( + iverksatteLøpendeBehandlinger: List, + måned: YearMonth, + ): List { + val fagsaker = fagsakRepository.finnAlleFagsakerMedOppstartSmåbarnstilleggIMåned( + iverksatteLøpendeBehandlinger = iverksatteLøpendeBehandlinger, + stønadFom = måned, + ) + if (SatsService.finnSisteSatsFor(SatsType.SMA).gyldigFom.toYearMonth() == måned) { + return fagsaker.mapNotNull { fagsakId -> + val sisteVedtatteBehandling = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId) + + if (sisteVedtatteBehandling != null) { + val atySmåbarnstillegg = + andelerTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(sisteVedtatteBehandling.id) + .filter { it.erSmåbarnstillegg() } + val harSmåbarnstilleggForrigeMåned = atySmåbarnstillegg.any { it.stønadTom == måned.minusMonths(1) } + if (harSmåbarnstilleggForrigeMåned) { + null + } else { + fagsakId + } + } else { + null + } + } + } else { + return fagsaker + } + } + + internal fun periodeMedRestartetSmåbarnstilleggErAlleredeBegrunnet(fagsakId: Long, måned: YearMonth): Boolean { + val vedtaksperioderForVedtatteBehandlinger = + behandlingHentOgPersisterService.hentBehandlinger(fagsakId = fagsakId) + .filter { behandling -> + behandling.erVedtatt() + } + .flatMap { behandling -> + val vedtak = vedtakService.hentAktivForBehandlingThrows(behandling.id) + vedtaksperiodeService.hentPersisterteVedtaksperioder(vedtak) + } + + val standardbegrunnelser = listOf(Standardbegrunnelse.INNVILGET_SMÅBARNSTILLEGG) + + return vedtaksperioderForVedtatteBehandlinger.erAlleredeBegrunnetMedBegrunnelse( + standardbegrunnelser = standardbegrunnelser, + måned = måned, + ) + } + + companion object { + private val logger = LoggerFactory.getLogger(RestartAvSmåbarnstilleggService::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/sm\303\245barnstillegg/VedtakOmOvergangsst\303\270nadController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/sm\303\245barnstillegg/VedtakOmOvergangsst\303\270nadController.kt" new file mode 100644 index 000000000..5516a6811 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/sm\303\245barnstillegg/VedtakOmOvergangsst\303\270nadController.kt" @@ -0,0 +1,25 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.småbarnstillegg + +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.infotrygd.Personident +import no.nav.familie.ba.sak.task.VedtakOmOvergangsstønadTask +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/overgangsstonad") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class VedtakOmOvergangsstønadController(private val taskRepository: TaskRepositoryWrapper) { + + @PostMapping + fun håndterVedtakOmOvergangsstønad(@RequestBody personIdent: Personident): Ressurs { + taskRepository.save(VedtakOmOvergangsstønadTask.opprettTask(personIdent.ident)) + return Ressurs.success("Ok", "Ok") + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/AutomatiskBeslutningService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/AutomatiskBeslutningService.kt new file mode 100644 index 000000000..b7c2d37b0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/AutomatiskBeslutningService.kt @@ -0,0 +1,20 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.simulering.SimuleringService +import org.springframework.stereotype.Service + +@Service +class AutomatiskBeslutningService(private val simuleringService: SimuleringService) { + + fun behandlingSkalAutomatiskBesluttes(behandling: Behandling): Boolean { + val harMigreringsbehandlingAvvikInnenforbeløpsgrenser by lazy { + simuleringService.harMigreringsbehandlingAvvikInnenforBeløpsgrenser(behandling) + } + + val harMigreringsbehandlingManuellePosteringer by lazy { + simuleringService.harMigreringsbehandlingManuellePosteringer(behandling) + } + return (behandling.erHelmanuellMigrering() && harMigreringsbehandlingAvvikInnenforbeløpsgrenser && !harMigreringsbehandlingManuellePosteringer) || behandling.erManuellMigreringForEndreMigreringsdato() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingController.kt new file mode 100644 index 000000000..01be47e10 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingController.kt @@ -0,0 +1,195 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.RessursUtils.illegalState +import no.nav.familie.ba.sak.common.RessursUtils.ok +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.BehandlingstemaService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValideringService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.ba.sak.task.BehandleFødselshendelseTask +import no.nav.familie.ba.sak.task.dto.BehandleFødselshendelseTaskDTO +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate + +@RestController +@RequestMapping("/api/behandlinger") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class BehandlingController( + private val stegService: StegService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val behandlingstemaService: BehandlingstemaService, + private val taskRepository: TaskRepositoryWrapper, + private val tilgangService: TilgangService, + private val utvidetBehandlingService: UtvidetBehandlingService, + private val tilkjentYtelseValideringService: TilkjentYtelseValideringService, +) { + + @PostMapping(produces = [MediaType.APPLICATION_JSON_VALUE]) + fun opprettBehandling(@RequestBody nyBehandling: NyBehandling): ResponseEntity> { + tilgangService.validerTilgangTilPersoner( + personIdenter = listOf(nyBehandling.søkersIdent), + event = AuditLoggerEvent.CREATE, + ) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "opprette behandling", + ) + // Basert på hvilke personer som ble hentet inn på behandlingen kan saksbehandler ha mistet tilgangen til behandlingen + val behandling = stegService.håndterNyBehandlingOgSendInfotrygdFeed(nyBehandling) + + // Basert på hvilke personer som ble hentet inn på behandlingen kan saksbehandler ha mistet tilgangen til behandlingen + tilgangService.validerTilgangTilBehandling(behandlingId = behandling.id, AuditLoggerEvent.UPDATE) + return ResponseEntity.ok( + Ressurs.success( + utvidetBehandlingService + .lagRestUtvidetBehandling(behandlingId = behandling.id), + ), + ) + } + + @PutMapping(produces = [MediaType.APPLICATION_JSON_VALUE]) + fun opprettEllerOppdaterBehandlingFraHendelse( + @RequestBody + nyBehandling: NyBehandlingHendelse, + ): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SYSTEM, + handling = "opprette behandling fra hendelse", + ) + + return try { + val task = BehandleFødselshendelseTask.opprettTask(BehandleFødselshendelseTaskDTO(nyBehandling)) + taskRepository.save(task) + ok("Task opprettet for behandling av fødselshendelse.") + } catch (ex: Throwable) { + illegalState("Task kunne ikke opprettes for behandling av fødselshendelse: ${ex.message}", ex) + } + } + + @PutMapping(path = ["/{behandlingId}/behandlingstema"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun endreBehandlingstema( + @PathVariable behandlingId: Long, + @RequestBody + endreBehandling: RestEndreBehandlingstema, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "endre behandlingstema", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + val behandling = behandlingstemaService.oppdaterBehandlingstema( + behandling = behandlingHentOgPersisterService.hent(behandlingId), + overstyrtUnderkategori = endreBehandling.behandlingUnderkategori, + overstyrtKategori = endreBehandling.behandlingKategori, + manueltOppdatert = true, + ) + + return ResponseEntity.ok( + Ressurs.success( + utvidetBehandlingService + .lagRestUtvidetBehandling(behandlingId = behandling.id), + ), + ) + } + + @GetMapping(path = ["/{behandlingId}/personer-med-ugyldig-etterbetalingsperiode"]) + fun hentPersonerMedUgyldigEtterbetalingsperiode( + @PathVariable behandlingId: Long, + ): ResponseEntity>> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.VEILEDER, + handling = "hent gyldig etterbetaling", + ) + + val aktørerMedUgyldigEtterbetalingsperiode = + tilkjentYtelseValideringService.finnAktørerMedUgyldigEtterbetalingsperiode( + behandlingId = behandlingId, + ) + val personerMedUgyldigEtterbetalingsperiode = + aktørerMedUgyldigEtterbetalingsperiode.map { it.aktivFødselsnummer() } + + return ResponseEntity.ok(Ressurs.success(personerMedUgyldigEtterbetalingsperiode)) + } +} + +data class NyBehandling( + val kategori: BehandlingKategori? = null, + val underkategori: BehandlingUnderkategori? = null, + val søkersIdent: String, + val behandlingType: BehandlingType, + val behandlingÅrsak: BehandlingÅrsak = BehandlingÅrsak.SØKNAD, + val skalBehandlesAutomatisk: Boolean = false, + val navIdent: String? = null, + val barnasIdenter: List = emptyList(), + val nyMigreringsdato: LocalDate? = null, + val søknadMottattDato: LocalDate? = null, + val søknadsinfo: Søknadsinfo? = null, + val fagsakId: Long, +) { + + init { // Initiell validering på request + when { + søkersIdent.isBlank() -> throw Feil( + message = "Søkers ident kan ikke være blank", + frontendFeilmelding = "Klarte ikke å opprette behandling. Mangler ident på bruker.", + ) + BehandlingType.MIGRERING_FRA_INFOTRYGD == behandlingType && + behandlingÅrsak.erManuellMigreringsårsak() && + nyMigreringsdato == null -> { + throw FunksjonellFeil( + melding = "Du må sette ny migreringsdato før du kan fortsette videre", + frontendFeilmelding = "Du må sette ny migreringsdato før du kan fortsette videre", + ) + } + behandlingType in listOf(BehandlingType.FØRSTEGANGSBEHANDLING, BehandlingType.REVURDERING) && + behandlingÅrsak == BehandlingÅrsak.SØKNAD && + søknadMottattDato == null -> { + throw FunksjonellFeil( + melding = "Du må sette søknads mottatt dato før du kan fortsette videre", + frontendFeilmelding = "Du må sette søknads mottatt dato før du kan fortsette videre", + ) + } + } + } +} + +data class NyBehandlingHendelse( + val morsIdent: String, + val barnasIdenter: List, +) + +data class RestEndreBehandlingstema( + val behandlingUnderkategori: BehandlingUnderkategori, + val behandlingKategori: BehandlingKategori, +) + +data class Søknadsinfo( + val journalpostId: String, + val brevkode: String, + val erDigital: Boolean, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingHentOgPersisterService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingHentOgPersisterService.kt new file mode 100644 index 000000000..400d74df8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingHentOgPersisterService.kt @@ -0,0 +1,137 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.statistikk.saksstatistikk.SaksstatistikkEventPublisher +import org.springframework.stereotype.Service + +@Service +class BehandlingHentOgPersisterService( + private val behandlingRepository: BehandlingRepository, + private val saksstatistikkEventPublisher: SaksstatistikkEventPublisher, +) { + fun lagreEllerOppdater(behandling: Behandling, sendTilDvh: Boolean = true): Behandling { + return behandlingRepository.save(behandling).also { + if (sendTilDvh) { + saksstatistikkEventPublisher.publiserBehandlingsstatistikk(it.id) + } + } + } + + fun lagreOgFlush(behandling: Behandling): Behandling { + return behandlingRepository.saveAndFlush(behandling) + } + + fun finnAktivForFagsak(fagsakId: Long): Behandling? { + return behandlingRepository.findByFagsakAndAktiv(fagsakId) + } + + fun finnAktivOgÅpenForFagsak(fagsakId: Long): Behandling? { + return behandlingRepository.findByFagsakAndAktivAndOpen(fagsakId) + } + + fun erÅpenBehandlingPåFagsak(fagsakId: Long): Boolean { + return finnAktivOgÅpenForFagsak(fagsakId) != null + } + + fun hent(behandlingId: Long): Behandling { + return behandlingRepository.finnBehandlingNullable(behandlingId) + ?: throw Feil("Finner ikke behandling med id $behandlingId") + } + + fun hentStatus(behandlingId: Long): BehandlingStatus { + return behandlingRepository.finnStatus(behandlingId) + } + + /** + * Henter siste iverksatte behandling på fagsak + */ + fun hentSisteBehandlingSomErIverksatt(fagsakId: Long): Behandling? { + return behandlingRepository.finnSisteIverksatteBehandling(fagsakId = fagsakId) + } + + /** + * Henter siste iverksatte behandling FØR en gitt behandling. + * Bør kun brukes i forbindelse med oppdrag mot økonomisystemet + * eller ved behandlingsresultat. + */ + fun hentForrigeBehandlingSomErIverksatt(behandling: Behandling): Behandling? { + val iverksatteBehandlinger = hentIverksatteBehandlinger(behandling.fagsak.id) + return Behandlingutils.hentForrigeIverksatteBehandling(iverksatteBehandlinger, behandling) + } + + fun hentForrigeBehandlingSomErIverksattFraBehandlingsId(behandlingId: Long): Behandling? { + val behandling = hent(behandlingId) + return hentForrigeBehandlingSomErIverksatt(behandling) + } + + /** + * Henter iverksatte behandlinger FØR en gitt behandling. + * Bør kun brukes i forbindelse med oppdrag mot økonomisystemet + * eller ved behandlingsresultat. + */ + fun hentBehandlingerSomErIverksatt(behandling: Behandling): List { + val iverksatteBehandlinger = hentIverksatteBehandlinger(behandling.fagsak.id) + return Behandlingutils.hentIverksatteBehandlinger(iverksatteBehandlinger, behandling) + } + + fun hentSisteBehandlingSomErVedtatt(fagsakId: Long): Behandling? { + val behandlingerPåFagsak = behandlingRepository.finnBehandlinger(fagsakId) + return behandlingerPåFagsak.hentSisteSomErVedtatt() + } + + fun hentSisteBehandlingSomErSendtTilØkonomiPerFagsak(fagsakIder: Set): List { + val behandlingerPåFagsakene = behandlingRepository.finnBehandlinger(fagsakIder) + + return behandlingerPåFagsakene + .groupBy { it.fagsak.id } + .mapNotNull { (_, behandling) -> behandling.hentSisteSomErSentTilØkonomi() } + } + + private fun List.hentSisteSomErVedtatt() = + filter { !it.erHenlagt() && it.status == BehandlingStatus.AVSLUTTET } + .maxByOrNull { it.aktivertTidspunkt } + + private fun List.hentSisteSomErSentTilØkonomi() = + filter { !it.erHenlagt() && (it.status == BehandlingStatus.AVSLUTTET || it.status == BehandlingStatus.IVERKSETTER_VEDTAK) } + .maxByOrNull { it.aktivertTidspunkt } + + /** + * Henter siste behandling som er vedtatt FØR en gitt behandling + */ + fun hentForrigeBehandlingSomErVedtatt(behandling: Behandling): Behandling? { + val behandlinger = behandlingRepository.finnBehandlinger(behandling.fagsak.id) + return Behandlingutils.hentForrigeBehandlingSomErVedtatt(behandlinger, behandling) + } + + fun partitionByIverksatteBehandlinger(funksjon: (iverksatteBehandlinger: List) -> List): List { + return behandlingRepository.finnSisteIverksatteBehandlingFraLøpendeFagsaker().chunked(10000) + .flatMap { funksjon(it) } + } + + fun hentSisteIverksatteBehandlingerFraLøpendeFagsaker(): List = + behandlingRepository.finnSisteIverksatteBehandlingFraLøpendeFagsaker() + + fun hentBehandlinger(fagsakId: Long): List { + return behandlingRepository.finnBehandlinger(fagsakId) + } + + fun hentFerdigstilteBehandlinger(fagsakId: Long): List = + hentBehandlinger(fagsakId).filter { it.erVedtatt() } + + fun hentAktivtFødselsnummerForBehandlinger(behandlingIder: List): Map = + behandlingRepository.finnAktivtFødselsnummerForBehandlinger(behandlingIder).associate { it.first to it.second } + + fun hentTssEksternIdForBehandlinger(behandlingIder: List): Map = + behandlingRepository.finnTssEksternIdForBehandlinger(behandlingIder).associate { it.first to it.second } + + fun hentIverksatteBehandlinger(fagsakId: Long): List { + return behandlingRepository.finnIverksatteBehandlinger(fagsakId = fagsakId) + } + + fun finnAvsluttedeBehandlingerPåFagsak(fagsakId: Long): List { + return behandlingRepository.findByFagsakAndAvsluttet(fagsakId = fagsakId) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingMetrikker.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingMetrikker.kt new file mode 100644 index 000000000..2cbef7991 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingMetrikker.kt @@ -0,0 +1,178 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.DistributionSummary +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.integrasjoner.sanity.SanityService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.EØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.Vedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeHentOgPersisterService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +@Component +class BehandlingMetrikker( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val vedtakRepository: VedtakRepository, + private val vedtaksperiodeHentOgPersisterService: VedtaksperiodeHentOgPersisterService, + private val sanityService: SanityService, +) { + private var sanityBegrunnelser: Map = emptyMap() + private var antallGangerBruktStandardbegrunnelse: Map = emptyMap() + + private var sanityEØSBegrunnelser: Map = emptyMap() + private var antallGangerBruktEØSBegrunnelse: Map = emptyMap() + + private val antallManuelleBehandlinger: Counter = + Metrics.counter("behandling.behandlinger", "saksbehandling", "manuell") + private val antallAutomatiskeBehandlinger: Counter = + Metrics.counter("behandling.behandlinger", "saksbehandling", "automatisk") + + private val antallManuelleBehandlingerOpprettet: Map = + initBehandlingTypeMetrikker("manuell") + private val antallAutomatiskeBehandlingerOpprettet: Map = + initBehandlingTypeMetrikker("automatisk") + private val behandlingÅrsak: Map = initBehandlingÅrsakMetrikker() + + private val antallBehandlingsresultat: Map = + Behandlingsresultat.values().associateWith { + Metrics.counter( + "behandling.resultat", + "type", + it.name, + "beskrivelse", + it.displayName, + ) + } + + private val behandlingstid: DistributionSummary = Metrics.summary("behandling.tid") + + fun hentBegrunnelserOgByggMetrikker() { + try { + sanityEØSBegrunnelser = sanityService.hentSanityEØSBegrunnelser() + } catch (exception: Exception) { + logger.warn("Kunne ikke hente EØS-begrunnelser fra sanity-api", exception) + } + + antallGangerBruktEØSBegrunnelse = EØSStandardbegrunnelse.values().associateWith { + val tittel = sanityEØSBegrunnelser[it]?.navnISystem ?: it.name + + Metrics.counter( + "eøs-begrunnelse", + "type", + it.name, + "beskrivelse", + tittel, + ) + } + + try { + sanityBegrunnelser = sanityService.hentSanityBegrunnelser() + } catch (exception: Exception) { + logger.warn("Klarte ikke å bygge tellere for begrunnelser") + } + + antallGangerBruktStandardbegrunnelse = Standardbegrunnelse.values().associateWith { + val tittel = sanityBegrunnelser[it]?.navnISystem ?: it.name + + Metrics.counter( + "brevbegrunnelse", + "type", + it.name, + "beskrivelse", + tittel, + ) + } + } + + fun tellNøkkelTallVedOpprettelseAvBehandling(behandling: Behandling) { + if (behandling.skalBehandlesAutomatisk) { + antallAutomatiskeBehandlingerOpprettet[behandling.type]?.increment() + antallAutomatiskeBehandlinger.increment() + } else { + antallManuelleBehandlingerOpprettet[behandling.type]?.increment() + antallManuelleBehandlinger.increment() + } + + behandlingÅrsak[behandling.opprettetÅrsak]?.increment() + } + + fun oppdaterBehandlingMetrikker(behandling: Behandling) { + tellBehandlingstidMetrikk(behandling) + økBehandlingsresultatTypeMetrikk(behandling) + økBegrunnelseMetrikk(behandling) + } + + private fun tellBehandlingstidMetrikk(behandling: Behandling) { + val dagerSidenOpprettet = ChronoUnit.DAYS.between(behandling.opprettetTidspunkt, LocalDateTime.now()) + behandlingstid.record(dagerSidenOpprettet.toDouble()) + } + + private fun økBehandlingsresultatTypeMetrikk(behandling: Behandling) { + val behandlingsresultat = behandlingHentOgPersisterService.hent(behandlingId = behandling.id).resultat + antallBehandlingsresultat[behandlingsresultat]?.increment() + } + + private fun økBegrunnelseMetrikk(behandling: Behandling) { + if (antallGangerBruktStandardbegrunnelse.isEmpty()) hentBegrunnelserOgByggMetrikker() + + if (!behandlingHentOgPersisterService.hent(behandlingId = behandling.id).erHenlagt()) { + val vedtak = vedtakRepository.findByBehandlingAndAktivOptional(behandlingId = behandling.id) + ?: error("Finner ikke aktivt vedtak på behandling ${behandling.id}") + + val vedtaksperiodeMedBegrunnelser = + vedtaksperiodeHentOgPersisterService.finnVedtaksperioderFor(vedtakId = vedtak.id) + + vedtaksperiodeMedBegrunnelser.forEach { + it.begrunnelser.forEach { vedtaksbegrunnelse: Vedtaksbegrunnelse -> + antallGangerBruktStandardbegrunnelse[vedtaksbegrunnelse.standardbegrunnelse]?.increment() + } + + it.eøsBegrunnelser.forEach { eøsBegrunnelse: EØSBegrunnelse -> + antallGangerBruktEØSBegrunnelse[eøsBegrunnelse.begrunnelse]?.increment() + } + } + } + } + + private fun initBehandlingTypeMetrikker(type: String): Map { + return BehandlingType.values().associateWith { + Metrics.counter( + "behandling.opprettet", + "type", + it.name, + "beskrivelse", + it.visningsnavn, + "saksbehandling", + type, + ) + } + } + + private fun initBehandlingÅrsakMetrikker(): Map { + return BehandlingÅrsak.values().associateWith { + Metrics.counter( + "behandling.aarsak", + "aarsak", + it.name, + "beskrivelse", + it.visningsnavn, + ) + } + } + + companion object { + val logger = LoggerFactory.getLogger(BehandlingMetrikker::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingService.kt new file mode 100644 index 000000000..2ae4b0e1b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingService.kt @@ -0,0 +1,321 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.BehandlingstemaService +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.bestemKategoriVedOpprettelse +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.bestemUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingMigreringsinfo +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingMigreringsinfoRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus.AVSLUTTET +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus.FATTER_VEDTAK +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingSøknadsinfoService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.initStatus +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatValideringUtils +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.logg.BehandlingLoggRequest +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.steg.FØRSTE_STEG +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.statistikk.saksstatistikk.SaksstatistikkEventPublisher +import no.nav.familie.ba.sak.task.OpprettOppgaveTask +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.YearMonth + +@Service +class BehandlingService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val behandlingstemaService: BehandlingstemaService, + private val behandlingSøknadsinfoService: BehandlingSøknadsinfoService, + private val behandlingMigreringsinfoRepository: BehandlingMigreringsinfoRepository, + private val behandlingMetrikker: BehandlingMetrikker, + private val saksstatistikkEventPublisher: SaksstatistikkEventPublisher, + private val fagsakRepository: FagsakRepository, + private val vedtakRepository: VedtakRepository, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val loggService: LoggService, + private val arbeidsfordelingService: ArbeidsfordelingService, + private val infotrygdService: InfotrygdService, + private val vedtaksperiodeService: VedtaksperiodeService, + private val taskRepository: TaskRepositoryWrapper, + private val vilkårsvurderingService: VilkårsvurderingService, +) { + + @Transactional + fun opprettBehandling(nyBehandling: NyBehandling): Behandling { + val fagsak = fagsakRepository.finnFagsak(nyBehandling.fagsakId) ?: throw FunksjonellFeil( + melding = "Kan ikke lage behandling på person. Fant ikke fagsak ${nyBehandling.fagsakId}", + frontendFeilmelding = "Kan ikke lage behandling på person. Fant ikke fagsak.", + ) + + val aktivBehandling = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = fagsak.id) + val sisteBehandlingSomErVedtatt = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = fagsak.id) + + return if (aktivBehandling == null || aktivBehandling.status == AVSLUTTET) { + val kategori = bestemKategoriVedOpprettelse( + overstyrtKategori = nyBehandling.kategori, + behandlingType = nyBehandling.behandlingType, + behandlingÅrsak = nyBehandling.behandlingÅrsak, + kategoriFraLøpendeBehandling = behandlingstemaService.hentLøpendeKategori(fagsak.id), + ) + + val underkategori = bestemUnderkategori( + overstyrtUnderkategori = nyBehandling.underkategori, + underkategoriFraLøpendeBehandling = behandlingstemaService.hentLøpendeUnderkategori(fagsakId = fagsak.id), + underkategoriFraInneværendeBehandling = behandlingstemaService.hentUnderkategoriFraInneværendeBehandling( + fagsak.id, + ), + ) + + val behandling = Behandling( + fagsak = fagsak, + opprettetÅrsak = nyBehandling.behandlingÅrsak, + type = nyBehandling.behandlingType, + kategori = kategori, + underkategori = underkategori, + skalBehandlesAutomatisk = nyBehandling.skalBehandlesAutomatisk, + ) + .initBehandlingStegTilstand() + + behandling.validerBehandlingstype( + sisteBehandlingSomErVedtatt = sisteBehandlingSomErVedtatt, + ) + val lagretBehandling = lagreNyOgDeaktiverGammelBehandling(behandling).also { + if (nyBehandling.søknadMottattDato != null) { + behandlingSøknadsinfoService.lagreNedSøknadsinfo( + mottattDato = nyBehandling.søknadMottattDato, + søknadsinfo = nyBehandling.søknadsinfo, + behandling = behandling, + ) + } + saksstatistikkEventPublisher.publiserBehandlingsstatistikk(it.id) + } + opprettOgInitierNyttVedtakForBehandling(behandling = lagretBehandling) + + loggService.opprettBehandlingLogg( + BehandlingLoggRequest(behandling = lagretBehandling, barnasIdenter = nyBehandling.barnasIdenter), + ) + if (lagretBehandling.opprettBehandleSakOppgave()) { + /** + * Oppretter oppgave via task slik at dersom noe feiler i forbindelse med opprettelse + * av behandling så rulles også tasken tilbake og vi forhindrer å opprette oppgave + */ + taskRepository.save( + OpprettOppgaveTask.opprettTask( + behandlingId = lagretBehandling.id, + oppgavetype = Oppgavetype.BehandleSak, + fristForFerdigstillelse = LocalDate.now(), + tilordnetRessurs = nyBehandling.navIdent, + ), + ) + } + + lagretBehandling + } else if (aktivBehandling.steg < StegType.BESLUTTE_VEDTAK) { + aktivBehandling.leggTilBehandlingStegTilstand(FØRSTE_STEG) + aktivBehandling.status = initStatus() + + behandlingHentOgPersisterService.lagreEllerOppdater(aktivBehandling) + } else { + throw FunksjonellFeil( + melding = "Kan ikke lage ny behandling. Fagsaken har en aktiv behandling som ikke er ferdigstilt.", + frontendFeilmelding = "Kan ikke lage ny behandling. Fagsaken har en aktiv behandling som ikke er ferdigstilt.", + ) + } + } + + fun nullstillEndringstidspunkt(behandlingId: Long) { + val behandling = behandlingHentOgPersisterService.hent(behandlingId = behandlingId) + behandling.overstyrtEndringstidspunkt = null + behandlingHentOgPersisterService.lagreEllerOppdater(behandling = behandling, sendTilDvh = false) + } + + @Transactional + fun opprettOgInitierNyttVedtakForBehandling( + behandling: Behandling, + kopierVedtakBegrunnelser: Boolean = false, + begrunnelseVilkårPekere: List = emptyList(), + ) { + behandling.steg.takeUnless { it !== StegType.BESLUTTE_VEDTAK && it !== StegType.REGISTRERE_PERSONGRUNNLAG } + ?: error("Forsøker å initiere vedtak på steg ${behandling.steg}") + + val deaktivertVedtak = + vedtakRepository.findByBehandlingAndAktivOptional(behandlingId = behandling.id) + ?.let { vedtakRepository.saveAndFlush(it.also { it.aktiv = false }) } + + val nyttVedtak = Vedtak( + behandling = behandling, + vedtaksdato = if (behandling.skalBehandlesAutomatisk) LocalDateTime.now() else null, + ) + + if (kopierVedtakBegrunnelser && deaktivertVedtak != null) { + vedtaksperiodeService.kopierOverVedtaksperioder( + deaktivertVedtak = deaktivertVedtak, + aktivtVedtak = nyttVedtak, + ) + } + + vedtakRepository.save(nyttVedtak) + } + + fun omgjørTilManuellBehandling(behandling: Behandling): Behandling { + if (!behandling.skalBehandlesAutomatisk) return behandling + + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} omgjør automatisk behandling $behandling til manuell.") + behandling.skalBehandlesAutomatisk = false + return behandlingHentOgPersisterService.lagreEllerOppdater(behandling = behandling, sendTilDvh = true) + } + + @Transactional + fun lagreNyOgDeaktiverGammelBehandling(behandling: Behandling): Behandling { + val aktivBehandling = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = behandling.fagsak.id) + + if (aktivBehandling != null) { + behandlingHentOgPersisterService.lagreOgFlush(aktivBehandling.also { it.aktiv = false }) + saksstatistikkEventPublisher.publiserBehandlingsstatistikk(aktivBehandling.id) + } else if (harAktivInfotrygdSak(behandling)) { + throw FunksjonellFeil( + "Kan ikke lage behandling på person med aktiv sak i Infotrygd", + "Kan ikke lage behandling på person med aktiv sak i Infotrygd", + ) + } + + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} oppretter behandling $behandling") + return behandlingHentOgPersisterService.lagreEllerOppdater(behandling, false).also { + arbeidsfordelingService.fastsettBehandlendeEnhet( + it, + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(it.fagsak.id), + ) + if (it.versjon == 0L) { + behandlingMetrikker.tellNøkkelTallVedOpprettelseAvBehandling(it) + } + } + } + + fun harAktivInfotrygdSak(behandling: Behandling): Boolean { + val søkerIdenter = behandling.fagsak.aktør.personidenter.map { it.fødselsnummer } + return infotrygdService.harÅpenSakIInfotrygd(søkerIdenter) || + !behandling.erMigrering() && infotrygdService.harLøpendeSakIInfotrygd(søkerIdenter) + } + + fun sendBehandlingTilBeslutter(behandling: Behandling) { + oppdaterStatusPåBehandling(behandlingId = behandling.id, status = FATTER_VEDTAK) + } + + fun oppdaterStatusPåBehandling(behandlingId: Long, status: BehandlingStatus): Behandling { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} endrer status på behandling $behandlingId fra ${behandling.status} til $status") + + behandling.status = status + return behandlingHentOgPersisterService.lagreEllerOppdater(behandling) + } + + fun oppdaterBehandlingsresultat(behandlingId: Long, resultat: Behandlingsresultat): Behandling { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + BehandlingsresultatValideringUtils.validerBehandlingsresultat(behandling, resultat) + + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} endrer resultat på behandling $behandlingId fra ${behandling.resultat} til $resultat") + loggService.opprettVilkårsvurderingLogg( + behandling = behandling, + forrigeBehandlingsresultat = behandling.resultat, + nyttBehandlingsresultat = resultat, + ) + + behandling.resultat = resultat + return behandlingHentOgPersisterService.lagreEllerOppdater(behandling) + } + + fun leggTilStegPåBehandlingOgSettTidligereStegSomUtført( + behandlingId: Long, + steg: StegType, + ): Behandling { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + behandling.leggTilBehandlingStegTilstand(steg) + + return behandlingHentOgPersisterService.lagreEllerOppdater(behandling) + } + + fun harBehandlingsårsakAlleredeKjørt( + fagsakId: Long, + behandlingÅrsak: BehandlingÅrsak, + måned: YearMonth, + ): Boolean { + return Behandlingutils.harBehandlingsårsakAlleredeKjørt( + behandlinger = behandlingHentOgPersisterService.hentBehandlinger(fagsakId = fagsakId), + behandlingÅrsak = behandlingÅrsak, + måned = måned, + ) + } + + @Transactional + fun lagreNedMigreringsdato(migreringsdato: LocalDate, behandling: Behandling) { + val forrigeMigreringsdato: YearMonth? = + behandlingMigreringsinfoRepository + .finnSisteMigreringsdatoPåFagsak(fagsakId = behandling.fagsak.id) + ?.toYearMonth() + ?: behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = behandling.fagsak.id) + ?.takeIf { it.erMigrering() }?.let { // fordi migreringsdato kun kan lagret for migreringsbehandling + vilkårsvurderingService.hentTidligsteVilkårsvurderingKnyttetTilMigrering( + behandlingId = it.id, + ) + } + + if (behandling.erManuellMigreringForEndreMigreringsdato() && + forrigeMigreringsdato != null && + migreringsdato.toYearMonth().isSameOrAfter(forrigeMigreringsdato) + ) { + throw FunksjonellFeil("Migreringsdatoen du har lagt inn er lik eller senere enn eksisterende migreringsdato. Du må velge en tidligere migreringsdato for å fortsette.") + } + + val behandlingMigreringsinfo = + BehandlingMigreringsinfo(behandling = behandling, migreringsdato = migreringsdato) + behandlingMigreringsinfoRepository.save(behandlingMigreringsinfo) + } + + fun hentMigreringsdatoIBehandling(behandlingId: Long): LocalDate? { + return behandlingMigreringsinfoRepository.findByBehandlingId(behandlingId)?.migreringsdato + } + + fun hentMigreringsdatoPåFagsak(fagsakId: Long): LocalDate? { + return behandlingMigreringsinfoRepository.finnSisteMigreringsdatoPåFagsak(fagsakId) + } + + @Transactional + fun deleteMigreringsdatoVedHenleggelse(behandlingId: Long) { + behandlingMigreringsinfoRepository.findByBehandlingId(behandlingId) + ?.let { behandlingMigreringsinfoRepository.delete(it) } + } + + fun erLøpende(behandling: Behandling): Boolean = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId = behandling.id) + .any { it.erLøpende() } + + companion object { + + private val logger: Logger = LoggerFactory.getLogger(BehandlingService::class.java) + } +} + +typealias OriginalOgKopiertVilkårResultat = Pair diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingStegController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingStegController.kt new file mode 100644 index 000000000..a8d90fc55 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingStegController.kt @@ -0,0 +1,235 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.config.FeatureToggleConfig.Companion.TEKNISK_ENDRING +import no.nav.familie.ba.sak.config.FeatureToggleConfig.Companion.TEKNISK_VEDLIKEHOLD_HENLEGGELSE +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerInstitusjonOgVerge +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.Behandlingutils.validerBehandlingIkkeSendtTilEksterneTjenester +import no.nav.familie.ba.sak.kjerne.behandling.Behandlingutils.validerhenleggelsestype +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatSteg +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +/** + * tilgangService.validerTilgangTilBehandling gjøres inne i stegService for hvert endepunkt + */ +@RestController +@RequestMapping("/api/behandlinger/{behandlingId}/steg") +@ProtectedWithClaims(issuer = "azuread") +class BehandlingStegController( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val utvidetBehandlingService: UtvidetBehandlingService, + private val stegService: StegService, + private val tilgangService: TilgangService, + private val featureToggleService: FeatureToggleService, +) { + + @PostMapping( + path = ["registrer-søknad"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + fun registrereSøknadOgHentPersongrunnlag( + @PathVariable behandlingId: Long, + @RequestBody restRegistrerSøknad: RestRegistrerSøknad, + ): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "registrere søknad", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId = behandlingId) + + stegService.håndterSøknad(behandling = behandling, restRegistrerSøknad = restRegistrerSøknad) + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandling.id))) + } + + @PostMapping(path = ["vilkårsvurdering"]) + fun validerVilkårsvurdering(@PathVariable behandlingId: Long): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "vurdere vilkårsvurdering", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + stegService.håndterVilkårsvurdering(behandling) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandling.id))) + } + + @GetMapping(path = ["behandlingsresultat/valider"]) + fun validerBehandlingsresultat(@PathVariable behandlingId: Long): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.VEILEDER, + handling = "validere behandlingsresultat", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + val behandlingSteg: BehandlingsresultatSteg = + stegService.hentBehandlingSteg(StegType.BEHANDLINGSRESULTAT) as BehandlingsresultatSteg + + behandlingSteg.preValiderSteg( + behandling = behandling, + stegService = stegService, + ) + + return ResponseEntity.ok( + Ressurs.success( + true, + ), + ) + } + + @PostMapping(path = ["behandlingsresultat"]) + fun utledBehandlingsresultat(@PathVariable behandlingId: Long): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "vurdere behandlingsresultat", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + stegService.håndterBehandlingsresultat(behandling) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandling.id))) + } + + @PostMapping(path = ["tilbakekreving"]) + fun lagreTilbakekrevingOgGåVidereTilNesteSteg( + @PathVariable behandlingId: Long, + @RequestBody restTilbakekreving: RestTilbakekreving?, + ): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "vurdere tilbakekreving", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + stegService.håndterVurderTilbakekreving(behandling, restTilbakekreving) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandling.id))) + } + + @PostMapping(path = ["send-til-beslutter"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun sendTilBeslutter( + @PathVariable behandlingId: Long, + @RequestParam behandlendeEnhet: String, + ): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "foreslå vedtak", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + + stegService.håndterSendTilBeslutter(behandling, behandlendeEnhet) + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandling.id))) + } + + @PostMapping(path = ["iverksett-vedtak"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun iverksettVedtak( + @PathVariable behandlingId: Long, + @RequestBody restBeslutningPåVedtak: RestBeslutningPåVedtak, + ): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.BESLUTTER, + handling = "iverksette vedtak", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + + stegService.håndterBeslutningForVedtak(behandling, restBeslutningPåVedtak) + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandling.id))) + } + + @PutMapping(path = ["henlegg"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun henleggBehandlingOgSendBrev( + @PathVariable behandlingId: Long, + @RequestBody henleggInfo: RestHenleggBehandlingInfo, + ): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "henlegge behandling", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + + validerhenleggelsestype( + henleggÅrsak = henleggInfo.årsak, + tekniskVedlikeholdToggel = featureToggleService.isEnabled(TEKNISK_VEDLIKEHOLD_HENLEGGELSE), + behandlingId = behandling.id, + ) + + validerTilgangTilHenleggelseAvBehandling( + behandling = behandling, + tekniskEndringToggle = featureToggleService.isEnabled(TEKNISK_ENDRING), + ) + + validerBehandlingIkkeSendtTilEksterneTjenester(behandling = behandling) + + stegService.håndterHenleggBehandling(behandling, henleggInfo) + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandling.id))) + } + + private fun validerTilgangTilHenleggelseAvBehandling( + behandling: Behandling, + tekniskEndringToggle: Boolean, + ) { + if (behandling.erTekniskBehandling() && !tekniskEndringToggle) { + throw FunksjonellFeil("Du har ikke tilgang til å henlegge en behandling som er opprettet med årsak=${behandling.opprettetÅrsak.visningsnavn}. Ta kontakt med teamet dersom dette ikke stemmer.") + } + } + + @PostMapping(path = ["registrer-institusjon-og-verge"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun registerInstitusjonOgVerge( + @PathVariable behandlingId: Long, + @RequestBody institusjonOgVergeInfo: RestRegistrerInstitusjonOgVerge, + ): ResponseEntity> { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + if (institusjonOgVergeInfo.tilVerge(behandling) == null && institusjonOgVergeInfo.tilInstitusjon() == null) { + return ResponseEntity.ok(Ressurs.failure("Ugydig verge info")) + } + + stegService.håndterRegistrerVerge(behandling, institusjonOgVergeInfo) + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandling.id))) + } +} + +class RestHenleggBehandlingInfo( + val årsak: HenleggÅrsak, + val begrunnelse: String, +) + +enum class HenleggÅrsak(val beskrivelse: String) { + SØKNAD_TRUKKET("Søknad trukket"), + FEILAKTIG_OPPRETTET("Behandling feilaktig opprettet"), + FØDSELSHENDELSE_UGYLDIG_UTFALL("Behandlingen er automatisk henlagt"), + TEKNISK_VEDLIKEHOLD("Teknisk vedlikehold"), + ; + + fun tilBehandlingsresultat() = when (this) { + FEILAKTIG_OPPRETTET -> Behandlingsresultat.HENLAGT_FEILAKTIG_OPPRETTET + SØKNAD_TRUKKET -> Behandlingsresultat.HENLAGT_SØKNAD_TRUKKET + FØDSELSHENDELSE_UGYLDIG_UTFALL -> Behandlingsresultat.HENLAGT_AUTOMATISK_FØDSELSHENDELSE + TEKNISK_VEDLIKEHOLD -> Behandlingsresultat.HENLAGT_TEKNISK_VEDLIKEHOLD + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/Behandlingutils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/Behandlingutils.kt new file mode 100644 index 000000000..7225fcbcc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/Behandlingutils.kt @@ -0,0 +1,77 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import java.time.YearMonth + +object Behandlingutils { + + fun hentSisteBehandlingSomErIverksatt(iverksatteBehandlinger: List): Behandling? { + return iverksatteBehandlinger + .filter { it.steg == StegType.BEHANDLING_AVSLUTTET } + .maxByOrNull { it.aktivertTidspunkt } + } + + fun hentForrigeBehandlingSomErVedtatt( + behandlinger: List, + behandlingFørFølgende: Behandling, + ): Behandling? { + return behandlinger + .filter { it.aktivertTidspunkt.isBefore(behandlingFørFølgende.aktivertTidspunkt) && it.steg == StegType.BEHANDLING_AVSLUTTET && !it.erHenlagt() } + .maxByOrNull { it.aktivertTidspunkt } + } + + fun hentForrigeIverksatteBehandling( + iverksatteBehandlinger: List, + behandlingFørFølgende: Behandling, + ): Behandling? { + return hentIverksatteBehandlinger( + iverksatteBehandlinger, + behandlingFørFølgende, + ).maxByOrNull { it.aktivertTidspunkt } + } + + fun hentIverksatteBehandlinger( + iverksatteBehandlinger: List, + behandlingFørFølgende: Behandling, + ): List { + return iverksatteBehandlinger + .filter { it.aktivertTidspunkt.isBefore(behandlingFørFølgende.aktivertTidspunkt) && it.steg == StegType.BEHANDLING_AVSLUTTET } + } + + fun harBehandlingsårsakAlleredeKjørt( + behandlingÅrsak: BehandlingÅrsak, + behandlinger: List, + måned: YearMonth, + ): Boolean { + return behandlinger.any { + it.aktivertTidspunkt.toLocalDate().toYearMonth() == måned && it.opprettetÅrsak == behandlingÅrsak + } + } + + fun validerhenleggelsestype(henleggÅrsak: HenleggÅrsak, tekniskVedlikeholdToggel: Boolean, behandlingId: Long) { + if (!tekniskVedlikeholdToggel && henleggÅrsak == HenleggÅrsak.TEKNISK_VEDLIKEHOLD) { + throw Feil( + "Teknisk vedlikehold henleggele er ikke påslått for " + + "${SikkerhetContext.hentSaksbehandlerNavn()}. Kan ikke henlegge behandling $behandlingId.", + ) + } + } + + fun validerBehandlingIkkeSendtTilEksterneTjenester(behandling: Behandling) { + if (behandling.harUtførtSteg(StegType.IVERKSETT_MOT_OPPDRAG)) { + throw FunksjonellFeil("Kan ikke henlegge behandlingen. Den er allerede sendt til økonomi.") + } + if (behandling.harUtførtSteg(StegType.DISTRIBUER_VEDTAKSBREV)) { + throw FunksjonellFeil("Kan ikke henlegge behandlingen. Brev er allerede distribuert.") + } + if (behandling.harUtførtSteg(StegType.JOURNALFØR_VEDTAKSBREV)) { + throw FunksjonellFeil("Kan ikke henlegge behandlingen. Brev er allerede journalført.") + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/SnikeIK\303\270enService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/SnikeIK\303\270enService.kt" new file mode 100644 index 000000000..a30b89e21 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/SnikeIK\303\270enService.kt" @@ -0,0 +1,115 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentService +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.steg.TilbakestillBehandlingService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Service +class SnikeIKøenService( + private val behandlingRepository: BehandlingRepository, + private val påVentService: SettPåVentService, + private val loggService: LoggService, + private val tilbakestillBehandlingService: TilbakestillBehandlingService, +) { + + private val logger = LoggerFactory.getLogger(javaClass) + + @Transactional + fun settAktivBehandlingTilPåMaskinellVent(behandlingId: Long, årsak: SettPåMaskinellVentÅrsak) { + val behandling = behandlingRepository.finnBehandling(behandlingId) + if (!behandling.aktiv) { + error("Behandling=$behandlingId er ikke aktiv") + } + val behandlingStatus = behandling.status + if (behandlingStatus !== BehandlingStatus.UTREDES && behandlingStatus !== BehandlingStatus.SATT_PÅ_VENT) { + error("Behandling=$behandlingId kan ikke settes på maskinell vent då status=$behandlingStatus") + } + behandling.status = BehandlingStatus.SATT_PÅ_MASKINELL_VENT + behandling.aktiv = false + behandlingRepository.saveAndFlush(behandling) + loggService.opprettSettPåMaskinellVent(behandling, årsak.årsak) + } + + /** + * @param behandlingSomFerdigstilles er behandlingen som ferdigstilles i [no.nav.familie.ba.sak.kjerne.steg.FerdigstillBehandling] + * Den er mest brukt for å logge hvilken behandling det er som ferdigstilles og hvilken som blir deaktivert + * + * @return boolean som tilsier om en behandling er reaktivert eller ikke + */ + @Transactional + fun reaktiverBehandlingPåMaskinellVent(behandlingSomFerdigstilles: Behandling): Boolean { + val fagsakId = behandlingSomFerdigstilles.fagsak.id + + val behandlingPåVent = finnBehandlingPåMaskinellVent(fagsakId) ?: return false + val aktivBehandling = behandlingRepository.findByFagsakAndAktiv(fagsakId) + + validerBehandlinger(aktivBehandling, behandlingPåVent) + + aktiverBehandlingPåVent(aktivBehandling, behandlingPåVent, behandlingSomFerdigstilles) + tilbakestillBehandlingService.tilbakestillBehandlingTilVilkårsvurdering(behandlingPåVent) + loggService.opprettTattAvMaskinellVent(behandlingPåVent) + return true + } + + private fun finnBehandlingPåMaskinellVent( + fagsakId: Long, + ): Behandling? = + behandlingRepository.finnBehandlinger(fagsakId, BehandlingStatus.SATT_PÅ_MASKINELL_VENT) + .takeIf { it.isNotEmpty() } + ?.let { it.singleOrNull() ?: error("Forventer kun en behandling på vent for fagsak=$fagsakId") } + + private fun aktiverBehandlingPåVent( + aktivBehandling: Behandling?, + behandlingPåVent: Behandling, + behandlingSomFerdigstilles: Behandling, + ) { + logger.info( + "Deaktiverer aktivBehandling=${aktivBehandling?.id}" + + " aktiverer behandlingPåVent=${behandlingPåVent.id}" + + " behandlingSomFerdigstilles=${behandlingSomFerdigstilles.id}", + ) + + if (aktivBehandling != null) { + aktivBehandling.aktiv = false + behandlingRepository.saveAndFlush(aktivBehandling) + } + + behandlingPåVent.aktiv = true + behandlingPåVent.aktivertTidspunkt = LocalDateTime.now() + behandlingPåVent.status = utledStatusForBehandlingPåVent(behandlingPåVent) + + behandlingRepository.saveAndFlush(behandlingPåVent) + } + + private fun validerBehandlinger(aktivBehandling: Behandling?, behandlingPåVent: Behandling) { + if (behandlingPåVent.aktiv) { + error("Åpen behandling har feil tilstand $behandlingPåVent") + } + if (aktivBehandling != null && aktivBehandling.status != BehandlingStatus.AVSLUTTET) { + throw BehandlingErIkkeAvsluttetException(aktivBehandling) + } + } + + /** + * Hvis behandlingen er satt på vent av saksbehandler så skal statusen settes tilbake til SATT_PÅ_VENT + * Ellers settes UTREDES + */ + private fun utledStatusForBehandlingPåVent(behandlingPåVent: Behandling) = + påVentService.finnAktivSettPåVentPåBehandling(behandlingPåVent.id) + ?.let { BehandlingStatus.SATT_PÅ_VENT } + ?: BehandlingStatus.UTREDES +} + +enum class SettPåMaskinellVentÅrsak(val årsak: String) { + SATSENDRING("Satsendring"), +} + +class BehandlingErIkkeAvsluttetException(val behandling: Behandling) : + RuntimeException("Behandling=${behandling.id} har status=${behandling.status} og er ikke avsluttet") diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/UtvidetBehandlingController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/UtvidetBehandlingController.kt new file mode 100644 index 000000000..dbbb0ff8a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/UtvidetBehandlingController.kt @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/behandlinger") +@ProtectedWithClaims(issuer = "azuread") +class UtvidetBehandlingController( + private val utvidetBehandlingService: UtvidetBehandlingService, + private val tilgangService: TilgangService, +) { + + @GetMapping(path = ["/{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentUtvidetBehandling(@PathVariable behandlingId: Long): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.VEILEDER, + handling = "Henter utvidet behandling", + ) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/UtvidetBehandlingService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/UtvidetBehandlingService.kt new file mode 100644 index 000000000..b2ab511e4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/UtvidetBehandlingService.kt @@ -0,0 +1,149 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.ekstern.restDomene.VergeInfo +import no.nav.familie.ba.sak.ekstern.restDomene.tilDto +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestArbeidsfordelingPåBehandling +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestBehandlingStegTilstand +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestFødselshendelsefiltreringResultat +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestKompetanse +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestKorrigertEtterbetaling +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestKorrigertVedtak +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPersonerMedAndeler +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestSettPåVent +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestTotrinnskontroll +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestUtenlandskPeriodebeløp +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestValutakurs +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestVedtak +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.domene.FødselshendelsefiltreringResultatRepository +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.brev.mottaker.BrevmottakerService +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.tilRestEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.KompetanseRepository +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløpRepository +import no.nav.familie.ba.sak.kjerne.eøs.valutakurs.ValutakursRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.søknad.SøknadGrunnlagService +import no.nav.familie.ba.sak.kjerne.korrigertetterbetaling.KorrigertEtterbetalingService +import no.nav.familie.ba.sak.kjerne.korrigertvedtak.KorrigertVedtakService +import no.nav.familie.ba.sak.kjerne.tilbakekreving.domene.TilbakekrevingRepository +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollRepository +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.kjerne.vedtak.feilutbetaltValuta.FeilutbetaltValutaService +import no.nav.familie.ba.sak.kjerne.vedtak.refusjonEøs.RefusjonEøsService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import org.springframework.stereotype.Service + +@Service +class UtvidetBehandlingService( + private val arbeidsfordelingService: ArbeidsfordelingService, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val persongrunnlagService: PersongrunnlagService, + private val behandlingService: BehandlingService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val vilkårsvurderingService: VilkårsvurderingService, + private val vedtakRepository: VedtakRepository, + private val totrinnskontrollRepository: TotrinnskontrollRepository, + private val vedtaksperiodeService: VedtaksperiodeService, + private val søknadGrunnlagService: SøknadGrunnlagService, + private val tilbakekrevingRepository: TilbakekrevingRepository, + private val fødselshendelsefiltreringResultatRepository: FødselshendelsefiltreringResultatRepository, + private val settPåVentService: SettPåVentService, + private val kompetanseRepository: KompetanseRepository, + private val valutakursRepository: ValutakursRepository, + private val utenlandskPeriodebeløpRepository: UtenlandskPeriodebeløpRepository, + private val korrigertEtterbetalingService: KorrigertEtterbetalingService, + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + private val korrigertVedtakService: KorrigertVedtakService, + private val feilutbetaltValutaService: FeilutbetaltValutaService, + private val brevmottakerService: BrevmottakerService, + private val refusjonEøsService: RefusjonEøsService, +) { + fun lagRestUtvidetBehandling(behandlingId: Long): RestUtvidetBehandling { + val behandling = behandlingHentOgPersisterService.hent(behandlingId = behandlingId) + + val søknadsgrunnlag = søknadGrunnlagService.hentAktiv(behandlingId = behandling.id) + val personopplysningGrunnlag = persongrunnlagService.hentAktiv(behandlingId = behandling.id) + val personer = personopplysningGrunnlag?.søkerOgBarn + + val arbeidsfordeling = arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id) + + val vedtak = vedtakRepository.findByBehandlingAndAktivOptional(behandling.id) + + val personResultater = vilkårsvurderingService.hentAktivForBehandling(behandling.id)?.personResultater + + val andelerTilkjentYtelse = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandlinger(listOf(behandling.id)) + + val totrinnskontroll = + totrinnskontrollRepository.findByBehandlingAndAktiv(behandlingId = behandling.id) + + val tilbakekreving = tilbakekrevingRepository.findByBehandlingId(behandling.id) + + val kompetanser: Collection = kompetanseRepository.finnFraBehandlingId(behandlingId) + + val valutakurser = valutakursRepository.finnFraBehandlingId(behandlingId) + + val utenlandskePeriodebeløp = utenlandskPeriodebeløpRepository.finnFraBehandlingId(behandlingId) + + val endreteUtbetalingerMedAndeler = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnEndreteUtbetalingerMedAndelerTilkjentYtelse(behandlingId) + + val feilutbetaltValuta = feilutbetaltValutaService.hentFeilutbetaltValutaPerioder(behandlingId) + + val refusjonEøs = refusjonEøsService.hentRefusjonEøsPerioder(behandlingId) + + val brevmottakere = brevmottakerService.hentRestBrevmottakere(behandlingId) + + return RestUtvidetBehandling( + behandlingId = behandling.id, + steg = behandling.steg, + stegTilstand = behandling.behandlingStegTilstand.map { it.tilRestBehandlingStegTilstand() }, + status = behandling.status, + resultat = behandling.resultat, + skalBehandlesAutomatisk = behandling.skalBehandlesAutomatisk, + type = behandling.type, + kategori = behandling.kategori, + underkategori = behandling.underkategori.tilDto(), + årsak = behandling.opprettetÅrsak, + opprettetTidspunkt = behandling.opprettetTidspunkt, + endretAv = behandling.endretAv, + arbeidsfordelingPåBehandling = arbeidsfordeling.tilRestArbeidsfordelingPåBehandling(), + søknadsgrunnlag = søknadsgrunnlag?.hentSøknadDto(), + personer = personer?.map { persongrunnlagService.mapTilRestPersonMedStatsborgerskapLand(it) } + ?: emptyList(), + personResultater = personResultater?.map { it.tilRestPersonResultat() } ?: emptyList(), + fødselshendelsefiltreringResultater = fødselshendelsefiltreringResultatRepository.finnFødselshendelsefiltreringResultater( + behandlingId = behandling.id, + ).map { it.tilRestFødselshendelsefiltreringResultat() }, + utbetalingsperioder = vedtaksperiodeService.hentUtbetalingsperioder(behandling, personopplysningGrunnlag), + personerMedAndelerTilkjentYtelse = personopplysningGrunnlag?.tilRestPersonerMedAndeler(andelerTilkjentYtelse) + ?: emptyList(), + endretUtbetalingAndeler = endreteUtbetalingerMedAndeler + .map { it.tilRestEndretUtbetalingAndel() }, + tilbakekreving = tilbakekreving?.tilRestTilbakekreving(), + vedtak = vedtak?.tilRestVedtak(), + kompetanser = kompetanser.map { it.tilRestKompetanse() }.sortedByDescending { it.fom }, + totrinnskontroll = totrinnskontroll?.tilRestTotrinnskontroll(), + aktivSettPåVent = settPåVentService.finnAktivSettPåVentPåBehandling(behandlingId = behandlingId) + ?.tilRestSettPåVent(), + migreringsdato = behandlingService.hentMigreringsdatoIBehandling(behandlingId = behandlingId), + valutakurser = valutakurser.map { it.tilRestValutakurs() }, + utenlandskePeriodebeløp = utenlandskePeriodebeløp.map { it.tilRestUtenlandskPeriodebeløp() }, + verge = behandling.verge?.let { VergeInfo(it.ident) }, + korrigertEtterbetaling = korrigertEtterbetalingService.finnAktivtKorrigeringPåBehandling(behandlingId) + ?.tilRestKorrigertEtterbetaling(), + korrigertVedtak = korrigertVedtakService.finnAktivtKorrigertVedtakPåBehandling(behandlingId) + ?.tilRestKorrigertVedtak(), + feilutbetaltValuta = feilutbetaltValuta, + brevmottakere = brevmottakere, + refusjonEøs = refusjonEøs, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/ValiderBrevmottakerService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/ValiderBrevmottakerService.kt new file mode 100644 index 000000000..0c25ff31d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/ValiderBrevmottakerService.kt @@ -0,0 +1,36 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollService +import no.nav.familie.ba.sak.kjerne.brev.mottaker.Brevmottaker +import no.nav.familie.ba.sak.kjerne.brev.mottaker.BrevmottakerRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import org.springframework.stereotype.Service + +@Service +class ValiderBrevmottakerService( + private val brevmottakerRepository: BrevmottakerRepository, + private val persongrunnlagService: PersongrunnlagService, + private val familieIntegrasjonerTilgangskontrollService: FamilieIntegrasjonerTilgangskontrollService, +) { + fun validerAtBehandlingIkkeInneholderStrengtFortroligePersonerMedManuelleBrevmottakere(behandlingId: Long, nyBrevmottaker: Brevmottaker? = null) { + var brevmottakere = brevmottakerRepository.finnBrevMottakereForBehandling(behandlingId) + nyBrevmottaker?.let { + brevmottakere += it + } + brevmottakere.takeIf { it.isNotEmpty() } ?: return + val personopplysningGrunnlag = persongrunnlagService.hentAktiv(behandlingId = behandlingId) ?: return + val personIdenter = personopplysningGrunnlag.søkerOgBarn + .takeIf { it.isNotEmpty() } + ?.map { it.aktør.aktivFødselsnummer() } + ?: return + val strengtFortroligePersonIdenter = + familieIntegrasjonerTilgangskontrollService.hentIdenterMedStrengtFortroligAdressebeskyttelse(personIdenter) + if (strengtFortroligePersonIdenter.isNotEmpty()) { + val melding = "Behandlingen (id: $behandlingId) inneholder ${strengtFortroligePersonIdenter.size} person(er) med strengt fortrolig adressebeskyttelse og kan ikke kombineres med manuelle brevmottakere (${brevmottakere.size} stk)." + val frontendFeilmelding = + "Behandlingen inneholder personer med strengt fortrolig adressebeskyttelse og kan ikke kombineres med manuelle brevmottakere." + throw FunksjonellFeil(melding, frontendFeilmelding) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/behandlingstema/BehandlingstemaService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/behandlingstema/BehandlingstemaService.kt new file mode 100644 index 000000000..16ad243e1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/behandlingstema/BehandlingstemaService.kt @@ -0,0 +1,156 @@ +package no.nav.familie.ba.sak.kjerne.behandling.behandlingstema + +import jakarta.transaction.Transactional +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjeService +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import org.springframework.stereotype.Service + +@Service +class BehandlingstemaService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val loggService: LoggService, + private val oppgaveService: OppgaveService, + private val vilkårsvurderingTidslinjeService: VilkårsvurderingTidslinjeService, + private val vilkårsvurderingRepository: VilkårsvurderingRepository, +) { + + @Transactional + fun oppdaterBehandlingstema( + behandling: Behandling, + overstyrtKategori: BehandlingKategori? = null, + overstyrtUnderkategori: BehandlingUnderkategori? = null, + manueltOppdatert: Boolean = false, + ): Behandling { + if (behandling.skalBehandlesAutomatisk) return behandling + if (manueltOppdatert && (overstyrtKategori == null || overstyrtUnderkategori == null)) { + throw FunksjonellFeil("Du må velge behandlingstema.") + } + + val utledetKategori = bestemKategori( + overstyrtKategori = overstyrtKategori, + kategoriFraSisteIverksattBehandling = hentLøpendeKategori(behandling.fagsak.id), + kategoriFraInneværendeBehandling = hentKategoriFraInneværendeBehandling(behandling.fagsak.id), + ) + + val utledetUnderkategori = bestemUnderkategori( + overstyrtUnderkategori = overstyrtUnderkategori, + underkategoriFraLøpendeBehandling = hentLøpendeUnderkategori(fagsakId = behandling.fagsak.id), + underkategoriFraInneværendeBehandling = hentUnderkategoriFraInneværendeBehandling(fagsakId = behandling.fagsak.id), + ) + + val forrigeUnderkategori = behandling.underkategori + val forrigeKategori = behandling.kategori + val skalOppdatereKategori = utledetKategori != forrigeKategori + val skalOppdatereUnderkategori = utledetUnderkategori != forrigeUnderkategori + val skalOppdatereKategoriEllerUnderkategori = skalOppdatereKategori || skalOppdatereUnderkategori + + return if (skalOppdatereKategoriEllerUnderkategori) { + behandling.apply { + kategori = utledetKategori + underkategori = utledetUnderkategori + } + + behandlingHentOgPersisterService.lagreEllerOppdater(behandling).also { lagretBehandling -> + oppgaveService.patchOppgaverForBehandling(lagretBehandling) { + val lagretUnderkategori = lagretBehandling.underkategori + if (it.behandlingstema != lagretBehandling.tilOppgaveBehandlingTema().value || it.behandlingstype != lagretBehandling.kategori.tilOppgavebehandlingType().value) { + it.copy( + behandlingstema = when (lagretUnderkategori) { + BehandlingUnderkategori.ORDINÆR, BehandlingUnderkategori.UTVIDET -> + behandling.tilOppgaveBehandlingTema().value + }, + behandlingstype = lagretBehandling.kategori.tilOppgavebehandlingType().value, + ) + } else { + null + } + } + + if (manueltOppdatert) { + loggService.opprettEndretBehandlingstema( + behandling = lagretBehandling, + forrigeKategori = forrigeKategori, + forrigeUnderkategori = forrigeUnderkategori, + nyKategori = utledetKategori, + nyUnderkategori = utledetUnderkategori, + ) + } + } + } else { + behandling + } + } + + fun hentLøpendeKategori(fagsakId: Long): BehandlingKategori { + val forrigeVedtatteBehandling = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = fagsakId) + ?: return BehandlingKategori.NASJONAL + + val barnasTidslinjer = + vilkårsvurderingTidslinjeService.hentTidslinjer(behandlingId = BehandlingId(forrigeVedtatteBehandling.id)) + ?.barnasTidslinjer() + return utledLøpendeKategori(barnasTidslinjer) + } + + fun hentKategoriFraInneværendeBehandling(fagsakId: Long): BehandlingKategori { + val aktivBehandling = + behandlingHentOgPersisterService.finnAktivOgÅpenForFagsak(fagsakId = fagsakId) + ?: return BehandlingKategori.NASJONAL + val vilkårsvurdering = + vilkårsvurderingRepository.findByBehandlingAndAktiv(behandlingId = aktivBehandling.id) + ?: return aktivBehandling.kategori + val erVilkårMedEØSRegelverkBehandlet = vilkårsvurdering.personResultater + .flatMap { it.vilkårResultater } + .filter { it.sistEndretIBehandlingId == aktivBehandling.id } + .any { it.vurderesEtter == Regelverk.EØS_FORORDNINGEN } + + return if (erVilkårMedEØSRegelverkBehandlet) { + BehandlingKategori.EØS + } else { + BehandlingKategori.NASJONAL + } + } + + fun hentLøpendeUnderkategori(fagsakId: Long): BehandlingUnderkategori? { + val forrigeAndeler = hentForrigeAndeler(fagsakId) + return if (forrigeAndeler != null) utledLøpendeUnderkategori(forrigeAndeler) else null + } + + fun hentUnderkategoriFraInneværendeBehandling(fagsakId: Long): BehandlingUnderkategori { + val aktivBehandling = + behandlingHentOgPersisterService.finnAktivOgÅpenForFagsak(fagsakId = fagsakId) + ?: return BehandlingUnderkategori.ORDINÆR + + val erUtvidetVilkårBehandlet = + vilkårsvurderingRepository.findByBehandlingAndAktiv(behandlingId = aktivBehandling.id) + ?.personResultater + ?.flatMap { it.vilkårResultater } + ?.filter { it.sistEndretIBehandlingId == aktivBehandling.id } + ?.any { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + + return if (erUtvidetVilkårBehandlet == true) { + BehandlingUnderkategori.UTVIDET + } else { + BehandlingUnderkategori.ORDINÆR + } + } + + private fun hentForrigeAndeler(fagsakId: Long): List? { + val forrigeVedtatteBehandling = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = fagsakId) ?: return null + return andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId = forrigeVedtatteBehandling.id) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/behandlingstema/BehandlingstemaUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/behandlingstema/BehandlingstemaUtils.kt new file mode 100644 index 000000000..6134da135 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/behandlingstema/BehandlingstemaUtils.kt @@ -0,0 +1,96 @@ +package no.nav.familie.ba.sak.kjerne.behandling.behandlingstema + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.finnHøyesteKategori +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjer +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.innholdForTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk + +fun bestemKategoriVedOpprettelse( + overstyrtKategori: BehandlingKategori?, + behandlingType: BehandlingType, + behandlingÅrsak: BehandlingÅrsak, + // siste iverksatt behandling som har løpende utbetaling. Hvis løpende utbetaling ikke finnes, settes det til NASJONAL + kategoriFraLøpendeBehandling: BehandlingKategori, +): BehandlingKategori { + return when { + behandlingType == BehandlingType.FØRSTEGANGSBEHANDLING || + behandlingType == BehandlingType.REVURDERING && behandlingÅrsak == BehandlingÅrsak.SØKNAD -> { + overstyrtKategori + ?: throw FunksjonellFeil( + "Behandling med type ${behandlingType.visningsnavn} " + + "og årsak ${behandlingÅrsak.visningsnavn} $ krever behandlingskategori", + ) + } + behandlingType == BehandlingType.MIGRERING_FRA_INFOTRYGD && behandlingÅrsak.erFørstegangMigreringsårsak() -> { + overstyrtKategori ?: throw FunksjonellFeil( + "Behandling med type ${behandlingType.visningsnavn} " + + "og årsak ${behandlingÅrsak.visningsnavn} $ krever behandlingskategori", + ) + } + else -> { + kategoriFraLøpendeBehandling + } + } +} + +fun bestemKategori( + overstyrtKategori: BehandlingKategori?, + // kategori fra siste iverksatt behandling eller NASJONAL når det ikke finnes noe + kategoriFraSisteIverksattBehandling: BehandlingKategori, + kategoriFraInneværendeBehandling: BehandlingKategori, +): BehandlingKategori { + // når saksbehandler overstyrer behandlingstema manuelt + if (overstyrtKategori != null) return overstyrtKategori + + // når saken har en løpende EØS utbetaling + if (kategoriFraSisteIverksattBehandling == BehandlingKategori.EØS) return BehandlingKategori.EØS + + // når løpende utbetaling er NASJONAL og inneværende behandling får EØS + val oppdatertKategori = + listOf(kategoriFraSisteIverksattBehandling, kategoriFraInneværendeBehandling).finnHøyesteKategori() + + return oppdatertKategori ?: BehandlingKategori.NASJONAL +} + +fun bestemUnderkategori( + overstyrtUnderkategori: BehandlingUnderkategori?, + underkategoriFraLøpendeBehandling: BehandlingUnderkategori?, + underkategoriFraInneværendeBehandling: BehandlingUnderkategori? = null, +): BehandlingUnderkategori { + if (underkategoriFraLøpendeBehandling == BehandlingUnderkategori.UTVIDET) return BehandlingUnderkategori.UTVIDET + + val oppdatertUnderkategori = overstyrtUnderkategori ?: underkategoriFraInneværendeBehandling + + return oppdatertUnderkategori ?: BehandlingUnderkategori.ORDINÆR +} + +fun utledLøpendeUnderkategori(andeler: List): BehandlingUnderkategori { + return if (andeler.any { it.erUtvidet() && it.erLøpende() }) BehandlingUnderkategori.UTVIDET else BehandlingUnderkategori.ORDINÆR +} + +fun utledLøpendeKategori( + barnasTidslinjer: Map?, +): BehandlingKategori { + if (barnasTidslinjer == null) return BehandlingKategori.NASJONAL + + val nå = MånedTidspunkt.nå() + + val etBarnHarMinstEnLøpendeEØSPeriode = barnasTidslinjer + .values + .map { it.egetRegelverkResultatTidslinje.innholdForTidspunkt(nå) } + .any { it.innhold?.regelverk == Regelverk.EØS_FORORDNINGEN } + + return if (etBarnHarMinstEnLøpendeEØSPeriode) { + BehandlingKategori.EØS + } else { + BehandlingKategori.NASJONAL + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/Behandling.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/Behandling.kt new file mode 100644 index 000000000..d7acf19fa --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/Behandling.kt @@ -0,0 +1,469 @@ +package no.nav.familie.ba.sak.kjerne.behandling.domene + +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import jakarta.persistence.OneToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.steg.BehandlingStegStatus +import no.nav.familie.ba.sak.kjerne.steg.FØRSTE_STEG +import no.nav.familie.ba.sak.kjerne.steg.SISTE_STEG +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.verge.Verge +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.Regelverk +import org.hibernate.annotations.SortComparator +import java.time.LocalDate +import java.time.LocalDateTime +import no.nav.familie.kontrakter.felles.Behandlingstema as OppgaveBehandlingTema +import no.nav.familie.kontrakter.felles.oppgave.Behandlingstype as OppgaveBehandlingType + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Behandling") +@Table(name = "BEHANDLING") +data class Behandling( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "behandling_seq_generator") + @SequenceGenerator(name = "behandling_seq_generator", sequenceName = "behandling_seq", allocationSize = 50) + val id: Long = 0, + + @ManyToOne(optional = false) + @JoinColumn(name = "fk_fagsak_id", nullable = false, updatable = false) + val fagsak: Fagsak, + + @OneToMany(mappedBy = "behandling", cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) + @SortComparator(BehandlingStegComparator::class) + val behandlingStegTilstand: MutableSet = sortedSetOf(comparator), + + @Enumerated(EnumType.STRING) + @Column(name = "resultat", nullable = false) + var resultat: Behandlingsresultat = Behandlingsresultat.IKKE_VURDERT, + + @Enumerated(EnumType.STRING) + @Column(name = "behandling_type", nullable = false) + val type: BehandlingType, + + @Enumerated(EnumType.STRING) + @Column(name = "opprettet_aarsak", nullable = false) + val opprettetÅrsak: BehandlingÅrsak, + + @Column(name = "skal_behandles_automatisk", nullable = false, updatable = true) + var skalBehandlesAutomatisk: Boolean = false, + + @Enumerated(EnumType.STRING) + @Column(name = "kategori", nullable = false, updatable = true) + var kategori: BehandlingKategori, + + @Enumerated(EnumType.STRING) + @Column(name = "underkategori", nullable = false, updatable = true) + var underkategori: BehandlingUnderkategori, + + @Column(name = "aktiv", nullable = false) + var aktiv: Boolean = true, + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + var status: BehandlingStatus = initStatus(), + + var overstyrtEndringstidspunkt: LocalDate? = null, + + @OneToOne(mappedBy = "behandling", optional = true) + val verge: Verge? = null, + + @Column(name = "aktivert_tid", nullable = false) + var aktivertTidspunkt: LocalDateTime = LocalDateTime.now(), +) : BaseEntitet() { + + val steg: StegType + get() = behandlingStegTilstand.last().behandlingSteg + + fun opprettBehandleSakOppgave(): Boolean { + return !skalBehandlesAutomatisk && ( + type == BehandlingType.FØRSTEGANGSBEHANDLING || + type == BehandlingType.REVURDERING + ) + } + + override fun toString(): String { + return "Behandling(" + + "id=$id, " + + "fagsak=${fagsak.id}, " + + "fagsakType=${fagsak.type}, " + + "type=$type, " + + "kategori=$kategori, " + + "underkategori=$underkategori, " + + "automatisk=$skalBehandlesAutomatisk, " + + "opprettetÅrsak=$opprettetÅrsak, " + + "status=$status, " + + "resultat=$resultat, " + + "steg=$steg)" + } + + // Skal kun brukes på gamle behandlinger + fun erTekniskOpphør(): Boolean { + return if (type == BehandlingType.TEKNISK_OPPHØR || + opprettetÅrsak == BehandlingÅrsak.TEKNISK_OPPHØR + ) { + if (type == BehandlingType.TEKNISK_OPPHØR && + opprettetÅrsak == BehandlingÅrsak.TEKNISK_OPPHØR + ) { + true + } else { + throw Feil( + "Behandling er teknisk opphør, men årsak $opprettetÅrsak " + + "og type $type samsvarer ikke.", + ) + } + } else { + false + } + } + + fun validerBehandlingstype(sisteBehandlingSomErVedtatt: Behandling? = null) { + if (type == BehandlingType.TEKNISK_OPPHØR) { + throw FunksjonellFeil( + melding = "Kan ikke lage teknisk opphør behandling.", + frontendFeilmelding = "Kan ikke lage teknisk opphør behandling, bruk heller teknisk endring.", + ) + } + + if (type == BehandlingType.TEKNISK_ENDRING || + opprettetÅrsak == BehandlingÅrsak.TEKNISK_ENDRING + ) { + if (type != BehandlingType.TEKNISK_ENDRING || + opprettetÅrsak != BehandlingÅrsak.TEKNISK_ENDRING + ) { + throw Feil("Behandling er teknisk endring, men årsak $opprettetÅrsak og type $type samsvarer ikke.") + } + } + + if (type == BehandlingType.REVURDERING && sisteBehandlingSomErVedtatt == null) { + throw Feil("Kan ikke opprette revurdering på $fagsak uten noen andre behandlinger som er vedtatt") + } + } + + fun erBehandlingMedVedtaksbrevutsending(): Boolean { + return when { + type == BehandlingType.TEKNISK_ENDRING -> false + opprettetÅrsak == BehandlingÅrsak.SATSENDRING -> false + erManuellMigrering() -> false + erMigrering() -> false + else -> true + } + } + + fun erHenlagt() = + resultat == Behandlingsresultat.HENLAGT_FEILAKTIG_OPPRETTET || + resultat == Behandlingsresultat.HENLAGT_SØKNAD_TRUKKET || + resultat == Behandlingsresultat.HENLAGT_AUTOMATISK_FØDSELSHENDELSE || + resultat == Behandlingsresultat.HENLAGT_TEKNISK_VEDLIKEHOLD + + fun erVedtatt() = status == BehandlingStatus.AVSLUTTET && !erHenlagt() + + fun leggTilBehandlingStegTilstand(nesteSteg: StegType): Behandling { + if (nesteSteg != StegType.HENLEGG_BEHANDLING) { + fjernAlleSenereSteg(nesteSteg) + } + + if (steg != nesteSteg) { + setSisteStegSomUtført() + } else { + setSisteStegSomIkkeUtført() + } + + leggTilStegOmDetIkkeFinnesFraFør(nesteSteg) + return this + } + + fun leggTilHenleggStegOmDetIkkeFinnesFraFør(): Behandling { + behandlingStegTilstand.filter { it.behandlingSteg == StegType.FERDIGSTILLE_BEHANDLING } + .forEach { behandlingStegTilstand.remove(it) } + leggTilStegOmDetIkkeFinnesFraFør(StegType.HENLEGG_BEHANDLING) + return this + } + + fun skalRettFraBehandlingsresultatTilIverksetting(erEndringFraForrigeBehandlingSendtTilØkonomi: Boolean): Boolean { + return when { + skalBehandlesAutomatisk && erOmregning() && + resultat in listOf(Behandlingsresultat.FORTSATT_INNVILGET, Behandlingsresultat.FORTSATT_OPPHØRT) -> true + + skalBehandlesAutomatisk && erMigrering() && !erManuellMigreringForEndreMigreringsdato() && resultat == Behandlingsresultat.INNVILGET -> true + skalBehandlesAutomatisk && erFødselshendelse() -> true + skalBehandlesAutomatisk && erSatsendring() && erEndringFraForrigeBehandlingSendtTilØkonomi -> true + else -> false + } + } + + private fun leggTilStegOmDetIkkeFinnesFraFør(steg: StegType) { + if (behandlingStegTilstand.none { it.behandlingSteg == steg }) { + behandlingStegTilstand.add( + BehandlingStegTilstand( + behandling = this, + behandlingSteg = steg, + behandlingStegStatus = if (steg == SISTE_STEG) { + BehandlingStegStatus.UTFØRT + } else { + BehandlingStegStatus.IKKE_UTFØRT + }, + ), + ) + } + } + + private fun setSisteStegSomUtført() { + behandlingStegTilstand.last().behandlingStegStatus = BehandlingStegStatus.UTFØRT + } + + private fun setSisteStegSomIkkeUtført() { + behandlingStegTilstand.last().behandlingStegStatus = BehandlingStegStatus.IKKE_UTFØRT + } + + private fun fjernAlleSenereSteg(steg: StegType) { + behandlingStegTilstand.filter { steg.rekkefølge < it.behandlingSteg.rekkefølge } + .forEach { + behandlingStegTilstand.remove(it) + } + } + + fun initBehandlingStegTilstand(): Behandling { + behandlingStegTilstand.add( + BehandlingStegTilstand( + behandling = this, + behandlingSteg = FØRSTE_STEG, + ), + ) + return this + } + + fun erSmåbarnstillegg() = this.opprettetÅrsak == BehandlingÅrsak.SMÅBARNSTILLEGG + + fun erKlage() = this.opprettetÅrsak == BehandlingÅrsak.KLAGE + + fun erMigrering() = + type == BehandlingType.MIGRERING_FRA_INFOTRYGD || type == BehandlingType.MIGRERING_FRA_INFOTRYGD_OPPHØRT + + fun erSatsendring() = this.opprettetÅrsak == BehandlingÅrsak.SATSENDRING + + fun erManuellMigreringForEndreMigreringsdato() = erMigrering() && + opprettetÅrsak == BehandlingÅrsak.ENDRE_MIGRERINGSDATO + + fun erHelmanuellMigrering() = erMigrering() && opprettetÅrsak == BehandlingÅrsak.HELMANUELL_MIGRERING + + fun erManuellMigrering() = erManuellMigreringForEndreMigreringsdato() || erHelmanuellMigrering() + + fun erAutomatiskEøsMigrering() = + erMigrering() && opprettetÅrsak == BehandlingÅrsak.MIGRERING && kategori == BehandlingKategori.EØS + + fun erTekniskEndring() = opprettetÅrsak == BehandlingÅrsak.TEKNISK_ENDRING + + fun erTekniskEndringMedOpphør() = + erTekniskEndring() && resultat.erOpphør() + + fun erTekniskBehandling() = opprettetÅrsak == BehandlingÅrsak.TEKNISK_OPPHØR || erTekniskEndring() + + fun erKorrigereVedtak() = opprettetÅrsak == BehandlingÅrsak.KORREKSJON_VEDTAKSBREV + + fun kanLeggeTilOgFjerneUtvidetVilkår() = + erManuellMigrering() || erTekniskEndring() || erKorrigereVedtak() || erKlage() + + private fun erOmregning() = + this.opprettetÅrsak.erOmregningsårsak() + + private fun erFødselshendelse() = this.opprettetÅrsak == BehandlingÅrsak.FØDSELSHENDELSE + + fun harUtførtSteg(steg: StegType) = + this.behandlingStegTilstand.any { + it.behandlingSteg == steg && it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + + fun tilOppgaveBehandlingTema(): OppgaveBehandlingTema { + return when { + this.fagsak.type == FagsakType.INSTITUSJON -> OppgaveBehandlingTema.NasjonalInstitusjon + this.underkategori == BehandlingUnderkategori.UTVIDET -> OppgaveBehandlingTema.UtvidetBarnetrygd + else -> OppgaveBehandlingTema.OrdinærBarnetrygd + } + } + + companion object { + + val comparator = BehandlingStegComparator() + } +} + +/** + * Enum for de ulike hovedresultatene en behandling kan ha. + * + * Et behandlingsresultater beskriver det samlede resultatet for vurderinger gjort i inneværende behandling. + * Behandlingsresultatet er delt opp i tre deler: + * 1. Hvis søknad - hva er resultatet på søknaden. + * 2. Finnes det noen andre endringer (utenom rent opphør) + * 3. Fører behandlingen til et opphør + * + * @displayName benyttes for visning av resultat + */ +enum class Behandlingsresultat(val displayName: String) { + + // Søknad + INNVILGET(displayName = "Innvilget"), + INNVILGET_OG_OPPHØRT(displayName = "Innvilget og opphørt"), + INNVILGET_OG_ENDRET(displayName = "Innvilget og endret"), + INNVILGET_ENDRET_OG_OPPHØRT(displayName = "Innvilget, endret og opphørt"), + ENDRET_OG_FORTSATT_INNVILGET("Endret og fortsatt innvilget"), + + DELVIS_INNVILGET(displayName = "Delvis innvilget"), + DELVIS_INNVILGET_OG_OPPHØRT(displayName = "Delvis innvilget og opphørt"), + DELVIS_INNVILGET_OG_ENDRET(displayName = "Delvis innvilget og endret"), + DELVIS_INNVILGET_ENDRET_OG_OPPHØRT(displayName = "Delvis innvilget, endret og opphørt"), + + AVSLÅTT(displayName = "Avslått"), + AVSLÅTT_OG_OPPHØRT(displayName = "Avslått og opphørt"), + AVSLÅTT_OG_ENDRET(displayName = "Avslått og endret"), + AVSLÅTT_ENDRET_OG_OPPHØRT(displayName = "Avslått, endret og opphørt"), + + // Revurdering uten søknad + ENDRET_UTBETALING(displayName = "Endret utbetaling"), + ENDRET_UTEN_UTBETALING(displayName = "Endret, uten endret utbetaling"), + ENDRET_OG_OPPHØRT(displayName = "Endret og opphørt"), + OPPHØRT(displayName = "Opphørt"), + FORTSATT_OPPHØRT(displayName = "Fortsatt opphørt"), + FORTSATT_INNVILGET(displayName = "Fortsatt innvilget"), + + // Henlagt + HENLAGT_FEILAKTIG_OPPRETTET(displayName = "Henlagt feilaktig opprettet"), + HENLAGT_SØKNAD_TRUKKET(displayName = "Henlagt søknad trukket"), + HENLAGT_AUTOMATISK_FØDSELSHENDELSE(displayName = "Henlagt avslått i automatisk vilkårsvurdering"), + HENLAGT_TEKNISK_VEDLIKEHOLD(displayName = "Henlagt teknisk vedlikehold"), + + IKKE_VURDERT(displayName = "Ikke vurdert"), + ; + + fun kanIkkeSendesTilOppdrag(): Boolean = + this in listOf(FORTSATT_INNVILGET, AVSLÅTT, FORTSATT_OPPHØRT, ENDRET_UTEN_UTBETALING) + + fun erAvslått(): Boolean = this in listOf(AVSLÅTT, AVSLÅTT_OG_OPPHØRT, AVSLÅTT_OG_ENDRET, AVSLÅTT_ENDRET_OG_OPPHØRT) + + fun erFortsattInnvilget(): Boolean = this in listOf(FORTSATT_INNVILGET, ENDRET_OG_FORTSATT_INNVILGET) + + fun erOpphør(): Boolean = this in listOf( + OPPHØRT, + ENDRET_OG_OPPHØRT, + FORTSATT_OPPHØRT, + ) +} + +/** + * Årsak er knyttet til en behandling og sier noe om hvorfor behandling ble opprettet. + */ +enum class BehandlingÅrsak(val visningsnavn: String) { + + SØKNAD("Søknad"), + FØDSELSHENDELSE("Fødselshendelse"), + ÅRLIG_KONTROLL("Årsak kontroll"), + DØDSFALL_BRUKER("Dødsfall bruker"), + NYE_OPPLYSNINGER("Nye opplysninger"), + KLAGE("Klage"), + TEKNISK_OPPHØR("Teknisk opphør"), // Ikke lenger i bruk. Bruk heller teknisk endring + TEKNISK_ENDRING("Teknisk endring"), // Brukes i tilfeller ved systemfeil og vi ønsker å iverksette mot OS på nytt + KORREKSJON_VEDTAKSBREV("Korrigere vedtak med egen brevmal"), + OMREGNING_6ÅR("Omregning 6 år"), + OMREGNING_18ÅR("Omregning 18 år"), + OMREGNING_SMÅBARNSTILLEGG("Omregning småbarnstillegg"), + SATSENDRING("Satsendring"), + SMÅBARNSTILLEGG("Småbarnstillegg"), + MIGRERING("Migrering"), + ENDRE_MIGRERINGSDATO("Endre migreringsdato"), + HELMANUELL_MIGRERING("Manuell migrering"), + ; + + fun erOmregningsårsak(): Boolean = + this == OMREGNING_6ÅR || this == OMREGNING_18ÅR || this == OMREGNING_SMÅBARNSTILLEGG + + fun hentOverstyrtDokumenttittelForOmregningsbehandling(): String? { + return when (this) { + OMREGNING_6ÅR -> "Vedtak om endret barnetrygd - barn 6 år" + OMREGNING_18ÅR -> "Vedtak om endret barnetrygd - barn 18 år" + OMREGNING_SMÅBARNSTILLEGG -> "Vedtak om endret barnetrygd - småbarnstillegg" + else -> null + } + } + + fun erManuellMigreringsårsak(): Boolean = this == HELMANUELL_MIGRERING || this == ENDRE_MIGRERINGSDATO + + fun erFørstegangMigreringsårsak(): Boolean = this == HELMANUELL_MIGRERING || this == MIGRERING +} + +enum class BehandlingType(val visningsnavn: String) { + FØRSTEGANGSBEHANDLING("Førstegangsbehandling"), + REVURDERING("Revurdering"), + MIGRERING_FRA_INFOTRYGD("Migrering fra infotrygd"), + MIGRERING_FRA_INFOTRYGD_OPPHØRT("Opphør migrering fra infotrygd"), + TEKNISK_OPPHØR("Teknisk opphør"), // Ikke lenger i bruk. Bruk heller teknisk endring + TEKNISK_ENDRING("Teknisk endring"), +} + +enum class BehandlingKategori(val visningsnavn: String, val nivå: Int) { + EØS("EØS", 2), + NASJONAL("Nasjonal", 1), + ; + + fun tilOppgavebehandlingType(): OppgaveBehandlingType { + return when (this) { + EØS -> OppgaveBehandlingType.EØS + NASJONAL -> OppgaveBehandlingType.NASJONAL + } + } + + fun tilRegelverk(): Regelverk = + when (this) { + EØS -> Regelverk.EØS + NASJONAL -> Regelverk.NASJONAL + } +} + +fun List.finnHøyesteKategori(): BehandlingKategori? = this.maxByOrNull { it.nivå } + +enum class BehandlingUnderkategori(val visningsnavn: String, val nivå: Int) { + UTVIDET("Utvidet", 2), + ORDINÆR("Ordinær", 1), +} + +fun initStatus(): BehandlingStatus { + return BehandlingStatus.UTREDES +} + +enum class BehandlingStatus { + UTREDES, + SATT_PÅ_VENT, + SATT_PÅ_MASKINELL_VENT, + FATTER_VEDTAK, + IVERKSETTER_VEDTAK, + AVSLUTTET, + ; + + fun erLåstMenIkkeAvsluttet() = this == FATTER_VEDTAK || this == IVERKSETTER_VEDTAK + fun erLåstForVidereRedigering() = this != UTREDES +} + +class BehandlingStegComparator : Comparator { + + override fun compare(bst1: BehandlingStegTilstand, bst2: BehandlingStegTilstand): Int { + return bst1.opprettetTidspunkt.compareTo(bst2.opprettetTidspunkt) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingMigreringsinfo.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingMigreringsinfo.kt new file mode 100644 index 000000000..6d4fae0f6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingMigreringsinfo.kt @@ -0,0 +1,35 @@ +package no.nav.familie.ba.sak.kjerne.behandling.domene + +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "BehandlingMigreringsinfo") +@Table(name = "BEHANDLING_MIGRERINGSINFO") +data class BehandlingMigreringsinfo( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "behandling_migreringsinfo_seq_generator") + @SequenceGenerator( + name = "behandling_migreringsinfo_seq_generator", + sequenceName = "behandling_migreringsinfo_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @ManyToOne(optional = false) + @JoinColumn(name = "fk_behandling_id", nullable = false, updatable = false) + val behandling: Behandling, + + val migreringsdato: LocalDate, + +) : BaseEntitet() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingMigreringsinfoRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingMigreringsinfoRepository.kt new file mode 100644 index 000000000..a46ff2d72 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingMigreringsinfoRepository.kt @@ -0,0 +1,29 @@ +package no.nav.familie.ba.sak.kjerne.behandling.domene + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import java.time.LocalDate + +interface BehandlingMigreringsinfoRepository : JpaRepository { + + @Query( + """SELECT MIN(bm.migreringsdato) FROM BehandlingMigreringsinfo bm + INNER JOIN Behandling b ON bm.behandling.id = b.id + INNER JOIN Fagsak f ON b.fagsak.id = f.id + WHERE f.id=:fagsakId""", + ) + fun finnSisteMigreringsdatoPåFagsak(fagsakId: Long): LocalDate? + + @Query("SELECT bm FROM BehandlingMigreringsinfo bm where bm.behandling.id=:behandlingId ") + fun findByBehandlingId(behandlingId: Long): BehandlingMigreringsinfo? + + @Query( + """SELECT DISTINCT(f.id) FROM BehandlingMigreringsinfo bm + INNER JOIN Behandling b ON bm.behandling.id = b.id + INNER JOIN Fagsak f ON b.fagsak.id = f.id + WHERE bm.migreringsdato >= :migreringsdato + AND b.opprettetÅrsak = 'MIGRERING' + """, + ) + fun finnMuligeMigreringerMedManglendeSats(migreringsdato: LocalDate): List +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingRepository.kt new file mode 100644 index 000000000..e175d9268 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingRepository.kt @@ -0,0 +1,172 @@ +package no.nav.familie.ba.sak.kjerne.behandling.domene + +import jakarta.persistence.LockModeType +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock +import org.springframework.data.jpa.repository.Query +import java.time.LocalDateTime + +interface BehandlingRepository : JpaRepository { + + @Query(value = "SELECT b FROM Behandling b WHERE b.id = :behandlingId") + fun finnBehandling(behandlingId: Long): Behandling + + @Query(value = "SELECT b FROM Behandling b WHERE b.id = :behandlingId") + fun finnBehandlingNullable(behandlingId: Long): Behandling? + + @Query(value = "SELECT b FROM Behandling b JOIN b.fagsak f WHERE f.id = :fagsakId AND f.arkivert = false") + fun finnBehandlinger(fagsakId: Long): List + + @Query(value = "SELECT b FROM Behandling b WHERE b.fagsak.id = :fagsakId AND status = :status") + fun finnBehandlinger(fagsakId: Long, status: BehandlingStatus): List + + @Query(value = "SELECT b FROM Behandling b JOIN b.fagsak f WHERE f.id in :fagsakIder AND f.arkivert = false") + fun finnBehandlinger(fagsakIder: Set): List + + @Query("SELECT b FROM Behandling b JOIN b.fagsak f WHERE f.id = :fagsakId AND b.aktiv = true AND f.arkivert = false") + fun findByFagsakAndAktiv(fagsakId: Long): Behandling? + + @Query("SELECT b FROM Behandling b JOIN b.fagsak f WHERE f.id = :fagsakId AND b.aktiv = true AND b.status <> 'AVSLUTTET' AND f.arkivert = false") + fun findByFagsakAndAktivAndOpen(fagsakId: Long): Behandling? + + @Query( + value = """WITH sisteiverksattebehandlingfraløpendefagsak AS ( + SELECT DISTINCT ON (b.fk_fagsak_id) b.id + FROM behandling b + INNER JOIN fagsak f ON f.id = b.fk_fagsak_id + INNER JOIN tilkjent_ytelse ty ON b.id = ty.fk_behandling_id + WHERE f.status = 'LØPENDE' + AND ty.utbetalingsoppdrag IS NOT NULL + AND f.arkivert = false + ORDER BY b.fk_fagsak_id, b.aktivert_tid DESC) + + select sum(aty.kalkulert_utbetalingsbelop) + from andel_tilkjent_ytelse aty + where aty.stonad_fom <= :måned + AND aty.stonad_tom >= :måned + AND aty.fk_behandling_id in (SELECT silp.id FROM sisteiverksattebehandlingfraløpendefagsak silp)""", + nativeQuery = true, + ) + fun hentTotalUtbetalingForMåned(måned: LocalDateTime): Long + + /* Denne henter først siste iverksatte behandling på en løpende fagsak. + * Finner så alle perioder på siste iverksatte behandling + * Finner deretter første behandling en periode oppstod i, som er det som skal avstemmes + */ + @Query( + value = """SELECT DISTINCT ON (b.fk_fagsak_id) b.id + FROM behandling b + INNER JOIN fagsak f ON f.id = b.fk_fagsak_id + INNER JOIN tilkjent_ytelse ty ON b.id = ty.fk_behandling_id + WHERE f.status = 'LØPENDE' + AND ty.utbetalingsoppdrag IS NOT NULL + AND f.arkivert = false + ORDER BY b.fk_fagsak_id, b.aktivert_tid DESC""", + nativeQuery = true, + ) + fun finnSisteIverksatteBehandlingFraLøpendeFagsaker(): List + + @Query( + """select b from Behandling b + inner join TilkjentYtelse ty on b.id = ty.behandling.id + where b.fagsak.id = :fagsakId AND ty.utbetalingsoppdrag IS NOT NULL""", + ) + fun finnIverksatteBehandlinger(fagsakId: Long): List + + @Query( + """SELECT DISTINCT ON(b.fk_fagsak_id) b.* + FROM behandling b + INNER JOIN fagsak f ON f.id = b.fk_fagsak_id + INNER JOIN tilkjent_ytelse ty ON b.id = ty.fk_behandling_id + WHERE f.id = :fagsakId + AND ty.utbetalingsoppdrag IS NOT NULL + AND f.arkivert = false + AND b.status = 'AVSLUTTET' + ORDER BY b.fk_fagsak_id, b.aktivert_tid DESC""", + nativeQuery = true, + ) + fun finnSisteIverksatteBehandling(fagsakId: Long): Behandling? + + @Query( + """ + select b from Behandling b + where b.fagsak.id = :fagsakId and b.status = 'IVERKSETTER_VEDTAK' + """, + ) + fun finnBehandlingerSomHolderPåÅIverksettes(fagsakId: Long): List + + /** + * Finner behandlinger som ligger til godkjenning. + * Dvs. behandlingen er på 'beslutte vedtak'-steget ('beslutte vedtak' er det siste steget på behandlingen) og dette steget er ikke utført enda + */ + @Query( + """select b from Behandling b + inner join BehandlingStegTilstand bst on b.id = bst.behandling.id + where b.fagsak.id = :fagsakId AND bst.behandlingSteg = 'BESLUTTE_VEDTAK' AND bst.behandlingStegStatus = 'IKKE_UTFØRT' + AND bst.id = ( + select bst2.id + from BehandlingStegTilstand bst2 + where bst2.behandling.id = b.id + ORDER BY bst2.opprettetTidspunkt DESC LIMIT 1 + )""", + ) + fun finnBehandlingerSomLiggerTilGodkjenning(fagsakId: Long): List + + @Query("SELECT b FROM Behandling b JOIN b.fagsak f WHERE f.id = :fagsakId AND b.status = 'AVSLUTTET' AND f.arkivert = false") + fun findByFagsakAndAvsluttet(fagsakId: Long): List + + @Lock(LockModeType.NONE) + @Query("SELECT count(*) FROM Behandling b JOIN b.fagsak f WHERE NOT b.status = 'AVSLUTTET' AND f.arkivert = false") + fun finnAntallBehandlingerIkkeAvsluttet(): Long + + @Lock(LockModeType.NONE) + @Query("SELECT b.opprettetTidspunkt FROM Behandling b JOIN b.fagsak f WHERE NOT b.status = 'AVSLUTTET' AND f.arkivert = false") + fun finnOpprettelsestidspunktPåÅpneBehandlinger(): List + + @Lock(LockModeType.NONE) + @Query("SELECT b FROM Behandling b JOIN b.fagsak f WHERE b.opprettetTidspunkt < :opprettetFør AND b.status <> 'AVSLUTTET' AND f.arkivert = false") + fun finnÅpneBehandlinger(opprettetFør: LocalDateTime): List + + @Lock(LockModeType.NONE) + @Query("SELECT b FROM Behandling b JOIN b.fagsak f WHERE b.status <> 'AVSLUTTET' AND b.underkategori = 'UTVIDET' AND f.arkivert = false") + fun finnÅpneUtvidetBarnetrygdBehandlinger(): List + + @Query("SELECT new kotlin.Pair(b.opprettetÅrsak, count(*)) from Behandling b group by b.opprettetÅrsak") + fun finnAntallBehandlingerPerÅrsak(): List> + + @Query("SELECT b.id from Behandling b where b.opprettetÅrsak in (:opprettetÅrsak)") + fun finnBehandlingIdMedOpprettetÅrsak(opprettetÅrsak: List): List + + @Query( + "SELECT new kotlin.Pair(b.id, p.fødselsnummer) from Behandling b " + + "INNER JOIN Fagsak f ON f.id = b.fagsak.id INNER JOIN Aktør a on f.aktør.aktørId = a.aktørId " + + "INNER JOIN Personident p on p.aktør.aktørId = a.aktørId " + + "where b.id in (:behandlingIder) AND p.aktiv=true AND f.status = 'LØPENDE' ", + ) + fun finnAktivtFødselsnummerForBehandlinger(behandlingIder: List): List> + + @Query( + "SELECT new kotlin.Pair(b.id, i.tssEksternId) from Behandling b " + + "INNER JOIN Fagsak f ON f.id = b.fagsak.id " + + "INNER JOIN Institusjon i on i.id = f.institusjon.id " + + "where b.id in (:behandlingIder) AND f.institusjon IS NOT NULL AND f.status = 'LØPENDE' ", + ) + fun finnTssEksternIdForBehandlinger(behandlingIder: List): List> + + @Query(value = "SELECT b.status FROM Behandling b WHERE b.id = :behandlingId") + fun finnStatus(behandlingId: Long): BehandlingStatus + + @Query( + """select distinct(b.id) from behandling b + join fagsak f on f.id = b.fk_fagsak_id + join tilkjent_ytelse ty on b.id = ty.fk_behandling_id + where b.aktiv = true + AND f.status = 'LØPENDE' + AND b.status = 'AVSLUTTET' + AND ty.stonad_tom is null + AND ty.utbetalingsoppdrag is null + LIMIT :limit""", + nativeQuery = true, + ) + fun finnAktiveBehandlingerSomManglerStønadTom(limit: Int): List +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingS\303\270knadsinfo.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingS\303\270knadsinfo.kt" new file mode 100644 index 000000000..57e8d645b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingS\303\270knadsinfo.kt" @@ -0,0 +1,45 @@ +package no.nav.familie.ba.sak.kjerne.behandling.domene + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDateTime + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "BehandlingSøknadsinfo") +@Table(name = "BEHANDLING_SOKNADSINFO") +data class BehandlingSøknadsinfo( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "behandling_søknadsinfo_seq_generator") + @SequenceGenerator( + name = "behandling_søknadsinfo_seq_generator", + sequenceName = "behandling_soknadsinfo_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "journalpost_id") + val journalpostId: String? = null, + + @Column(name = "brevkode") + val brevkode: String? = null, + + @ManyToOne(optional = false) + @JoinColumn(name = "fk_behandling_id", nullable = false, updatable = false) + val behandling: Behandling, + + val mottattDato: LocalDateTime, + + @Column(name = "er_digital") + val erDigital: Boolean? = null, + +) : BaseEntitet() diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingS\303\270knadsinfoRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingS\303\270knadsinfoRepository.kt" new file mode 100644 index 000000000..ef388f0f2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingS\303\270knadsinfoRepository.kt" @@ -0,0 +1,31 @@ +package no.nav.familie.ba.sak.kjerne.behandling.domene + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import java.time.LocalDateTime + +interface BehandlingSøknadsinfoRepository : JpaRepository { + + @Query("SELECT bs FROM BehandlingSøknadsinfo bs where bs.behandling.id=:behandlingId ") + fun findByBehandlingId(behandlingId: Long): Set + + @Query( + """ + SELECT count(distinct bs.journalpostId) AS antall, + bs.brevkode AS brevkode, + bs.erDigital AS erDigital + FROM BehandlingSøknadsinfo bs + WHERE bs.journalpostId IS NOT NULL + AND bs.mottattDato >= :fomDato + AND bs.mottattDato <= :tomDato + GROUP BY bs.brevkode, bs.erDigital + """, + ) + fun hentAntallSøknaderIPeriode(fomDato: LocalDateTime, tomDato: LocalDateTime): List +} + +interface AntallSøknaderPerGruppe { + val antall: Int + val brevkode: String + val erDigital: Boolean +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingS\303\270knadsinfoService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingS\303\270knadsinfoService.kt" new file mode 100644 index 000000000..d73304e39 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingS\303\270knadsinfoService.kt" @@ -0,0 +1,78 @@ +package no.nav.familie.ba.sak.kjerne.behandling.domene + +import no.nav.familie.ba.sak.kjerne.behandling.Søknadsinfo +import no.nav.familie.kontrakter.ba.søknad.v4.Søknadstype.ORDINÆR +import no.nav.familie.kontrakter.ba.søknad.v4.Søknadstype.UTVIDET +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime + +@Service +class BehandlingSøknadsinfoService( + private val behandlingSøknadsinfoRepository: BehandlingSøknadsinfoRepository, +) { + + @Transactional + fun lagreNedSøknadsinfo(mottattDato: LocalDate, søknadsinfo: Søknadsinfo?, behandling: Behandling) { + val behandlingSøknadsinfo = BehandlingSøknadsinfo( + behandling = behandling, + mottattDato = mottattDato.atStartOfDay(), + journalpostId = søknadsinfo?.journalpostId, + brevkode = søknadsinfo?.brevkode, + erDigital = søknadsinfo?.erDigital, + ) + behandlingSøknadsinfoRepository.save(behandlingSøknadsinfo) + } + + fun hentSøknadMottattDato(behandlingId: Long): LocalDateTime? { + return behandlingSøknadsinfoRepository.findByBehandlingId(behandlingId).minOfOrNull { it.mottattDato } + } + + fun hentSøknadsstatistikk(fom: LocalDate, tom: LocalDate): SøknadsstatistikkForPeriode { + val antallSøknaderPerGruppe = + behandlingSøknadsinfoRepository.hentAntallSøknaderIPeriode(fom.atStartOfDay(), tom.atTime(LocalTime.MAX)) + + val antallOrdinære = + antallSøknaderPerGruppe.filter { it.brevkode == ORDINÆR.søknadskode }.sumOf { it.antall } + val antallOrdinæreDigitale = + antallSøknaderPerGruppe.singleOrNull { it.brevkode == ORDINÆR.søknadskode && it.erDigital }?.antall ?: 0 + + val antallUtvidet = + antallSøknaderPerGruppe.filter { it.brevkode == UTVIDET.søknadskode }.sumOf { it.antall } + val antallUtvidetDigitale = + antallSøknaderPerGruppe.singleOrNull { it.erDigital && it.brevkode == UTVIDET.søknadskode }?.antall ?: 0 + + return SøknadsstatistikkForPeriode( + fom = fom, + tom = tom, + ordinærBarnetrygd = AntallSøknader( + totalt = antallOrdinære, + papirsøknader = antallOrdinære - antallOrdinæreDigitale, + digitaleSøknader = antallOrdinæreDigitale, + digitaliseringsgrad = antallOrdinæreDigitale / antallOrdinære.toFloat(), + ), + utvidetBarnetrygd = AntallSøknader( + totalt = antallUtvidet, + papirsøknader = antallUtvidet - antallUtvidetDigitale, + digitaleSøknader = antallUtvidetDigitale, + digitaliseringsgrad = antallUtvidetDigitale / antallUtvidet.toFloat(), + ), + ) + } +} + +class SøknadsstatistikkForPeriode( + val fom: LocalDate, + val tom: LocalDate, + val ordinærBarnetrygd: AntallSøknader, + val utvidetBarnetrygd: AntallSøknader, +) + +class AntallSøknader( + val totalt: Int, + val papirsøknader: Int, + val digitaleSøknader: Int, + val digitaliseringsgrad: Float, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/tilstand/BehandlingStegTilstand.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/tilstand/BehandlingStegTilstand.kt new file mode 100644 index 000000000..39e7b7a69 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/tilstand/BehandlingStegTilstand.kt @@ -0,0 +1,66 @@ +package no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.steg.BehandlingStegStatus +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.util.Objects + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "BehandlingStegTilstand") +@Table(name = "BEHANDLING_STEG_TILSTAND") +data class BehandlingStegTilstand( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "behandling_steg_tilstand_seq_generator") + @SequenceGenerator( + name = "behandling_steg_tilstand_seq_generator", + sequenceName = "behandling_steg_tilstand_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @ManyToOne(optional = false) + @JoinColumn(name = "fk_behandling_id", nullable = false, updatable = false) + @JsonIgnore + val behandling: Behandling, + + @Enumerated(EnumType.STRING) + @Column(name = "behandling_steg", nullable = false) + val behandlingSteg: StegType, + + @Enumerated(EnumType.STRING) + @Column(name = "behandling_steg_status", nullable = false) + var behandlingStegStatus: BehandlingStegStatus = BehandlingStegStatus.IKKE_UTFØRT, +) : BaseEntitet() { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BehandlingStegTilstand + + return behandlingSteg == other.behandlingSteg + } + + override fun hashCode(): Int { + return Objects.hash(behandlingSteg) + } + + override fun toString(): String { + return "BehandlingStegTilstand(id=$id, behandling=${behandling.id}, behandlingSteg=$behandlingSteg, behandlingStegStatus=$behandlingStegStatus)" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245Vent.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245Vent.kt" new file mode 100644 index 000000000..877a6ca8d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245Vent.kt" @@ -0,0 +1,49 @@ +package no.nav.familie.ba.sak.kjerne.behandling.settpåvent + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import java.time.LocalDate + +@Entity(name = "sett_paa_vent") +@Table(name = "sett_paa_vent") +data class SettPåVent( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sett_paa_vent_seq_generator") + @SequenceGenerator(name = "sett_paa_vent_seq_generator", sequenceName = "sett_paa_vent_seq", allocationSize = 50) + val id: Long = 0, + + @ManyToOne(optional = false) + @JoinColumn(name = "fk_behandling_id", nullable = false, updatable = false) + val behandling: Behandling, + + @Column(name = "frist", nullable = false) + var frist: LocalDate, + + @Column(name = "tid_tatt_av_vent", nullable = true) + var tidTattAvVent: LocalDate? = null, + + @Column(name = "tid_satt_paa_vent", nullable = false) + var tidSattPåVent: LocalDate = LocalDate.now(), + + @Enumerated(EnumType.STRING) + @Column(name = "aarsak", nullable = false) + var årsak: SettPåVentÅrsak, + + @Column(name = "aktiv", nullable = false) + var aktiv: Boolean = true, +) : BaseEntitet() + +enum class SettPåVentÅrsak(val visningsnavn: String) { + AVVENTER_DOKUMENTASJON("Avventer dokumentasjon"), +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentController.kt" new file mode 100644 index 000000000..8df9af05f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentController.kt" @@ -0,0 +1,68 @@ +package no.nav.familie.ba.sak.kjerne.behandling.settpåvent + +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestSettPåVent +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/sett-på-vent/") +@ProtectedWithClaims(issuer = "azuread") +class SettPåVentController( + private val tilgangService: TilgangService, + private val settPåVentService: SettPåVentService, + private val utvidetBehandlingService: UtvidetBehandlingService, +) { + @PostMapping(path = ["{behandlingId}"]) + fun settBehandlingPåVent( + @PathVariable behandlingId: Long, + @RequestBody restSettPåVent: RestSettPåVent, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Sett behandling på vent", + ) + settPåVentService.settBehandlingPåVent(behandlingId, restSettPåVent.frist, restSettPåVent.årsak) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @PutMapping(path = ["{behandlingId}"]) + fun oppdaterSettBehandlingPåVent( + @PathVariable behandlingId: Long, + @RequestBody restSettPåVent: RestSettPåVent, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Sett behandling på vent", + ) + settPåVentService.oppdaterSettBehandlingPåVent(behandlingId, restSettPåVent.frist, restSettPåVent.årsak) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @PutMapping(path = ["{behandlingId}/fortsettbehandling"]) + fun gjenopptaBehandling(@PathVariable behandlingId: Long): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Sett behandling på vent", + ) + settPåVentService.gjenopptaBehandling(behandlingId) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentRepository.kt" new file mode 100644 index 000000000..04f1dd6fa --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentRepository.kt" @@ -0,0 +1,11 @@ +package no.nav.familie.ba.sak.kjerne.behandling.settpåvent + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Service + +@Service +interface SettPåVentRepository : JpaRepository { + fun findByBehandlingIdAndAktiv(behandlingId: Long, aktiv: Boolean): SettPåVent? + + fun findByAktivTrue(): List +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentScheduler.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentScheduler.kt" new file mode 100644 index 000000000..ccdbe16ec --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentScheduler.kt" @@ -0,0 +1,33 @@ +package no.nav.familie.ba.sak.kjerne.behandling.settpåvent + +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.task.TaBehandlingerEtterVentefristAvVentTask +import no.nav.familie.leader.LeaderClient +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component + +@Component +class SettPåVentScheduler(val taskRepository: TaskRepositoryWrapper) { + + @Scheduled(cron = "0 0 7 * * *") + fun taBehandlingerEtterVentefristAvVent() { + when (LeaderClient.isLeader()) { + true -> { + val taBehandlingerEtterVentefristAvVentTask = + Task(type = TaBehandlingerEtterVentefristAvVentTask.TASK_STEP_TYPE, payload = "") + taskRepository.save(taBehandlingerEtterVentefristAvVentTask) + logger.info("Opprettet taBehandlingerAvVentTask") + } + false, null -> { + logger.info("Ikke opprettet taBehandlingerAvVentTask på denne poden") + } + } + } + + companion object { + + private val logger = LoggerFactory.getLogger(SettPåVentScheduler::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentService.kt" new file mode 100644 index 000000000..e31eeaeaa --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentService.kt" @@ -0,0 +1,124 @@ +package no.nav.familie.ba.sak.kjerne.behandling.settpåvent + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.statistikk.saksstatistikk.SaksstatistikkEventPublisher +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.Period + +@Service +class SettPåVentService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val saksstatistikkEventPublisher: SaksstatistikkEventPublisher, + private val settPåVentRepository: SettPåVentRepository, + private val loggService: LoggService, + private val oppgaveService: OppgaveService, +) { + fun finnAktivSettPåVentPåBehandling(behandlingId: Long): SettPåVent? { + return settPåVentRepository.findByBehandlingIdAndAktiv(behandlingId, true) + } + + fun finnAktiveSettPåVent(): List = settPåVentRepository.findByAktivTrue() + + private fun finnAktivSettPåVentPåBehandlingThrows(behandlingId: Long): SettPåVent { + return finnAktivSettPåVentPåBehandling(behandlingId) + ?: throw Feil("Behandling $behandlingId er ikke satt på vent.") + } + + private fun lagreEllerOppdater(settPåVent: SettPåVent): SettPåVent { + saksstatistikkEventPublisher.publiserBehandlingsstatistikk(behandlingId = settPåVent.behandling.id) + return settPåVentRepository.save(settPåVent) + } + + @Transactional + fun settBehandlingPåVent(behandlingId: Long, frist: LocalDate, årsak: SettPåVentÅrsak): SettPåVent { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + val gammelSettPåVent: SettPåVent? = finnAktivSettPåVentPåBehandling(behandlingId) + validerBehandlingKanSettesPåVent(gammelSettPåVent, frist, behandling) + + loggService.opprettSettPåVentLogg(behandling, årsak.visningsnavn) + logger.info("Sett på vent behandling $behandlingId med frist $frist og årsak $årsak") + + val settPåVent = lagreEllerOppdater(SettPåVent(behandling = behandling, frist = frist, årsak = årsak)) + + behandling.status = BehandlingStatus.SATT_PÅ_VENT + behandlingHentOgPersisterService.lagreOgFlush(behandling) + oppgaveService.forlengFristÅpneOppgaverPåBehandling( + behandlingId = behandling.id, + forlengelse = Period.between(LocalDate.now(), frist), + ) + + return settPåVent + } + + @Transactional + fun oppdaterSettBehandlingPåVent(behandlingId: Long, frist: LocalDate, årsak: SettPåVentÅrsak): SettPåVent { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + val aktivSettPåVent = finnAktivSettPåVentPåBehandlingThrows(behandlingId) + + if (frist == aktivSettPåVent.frist && årsak == aktivSettPåVent.årsak) { + throw FunksjonellFeil("Behandlingen er allerede satt på vent med frist $frist og årsak $årsak.") + } + validerFristErFremITiden(behandling, frist) + + loggService.opprettOppdaterVentingLogg( + behandling = behandling, + endretÅrsak = if (årsak != aktivSettPåVent.årsak) årsak.visningsnavn else null, + endretFrist = if (frist != aktivSettPåVent.frist) frist else null, + ) + logger.info("Oppdater sett på vent behandling $behandlingId med frist $frist og årsak $årsak") + + val gammelFrist = aktivSettPåVent.frist + aktivSettPåVent.frist = frist + aktivSettPåVent.årsak = årsak + val settPåVent = lagreEllerOppdater(aktivSettPåVent) + + oppgaveService.forlengFristÅpneOppgaverPåBehandling( + behandlingId = behandlingId, + forlengelse = Period.between(gammelFrist, frist), + ) + + return settPåVent + } + + @Transactional + fun gjenopptaBehandling(behandlingId: Long, nå: LocalDate = LocalDate.now()): SettPåVent { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + + val aktivSettPåVent = + finnAktivSettPåVentPåBehandling(behandlingId) + ?: throw FunksjonellFeil( + melding = "Behandling $behandlingId er ikke satt på vent.", + frontendFeilmelding = "Behandlingen er ikke på vent og det er ikke mulig å gjenoppta behandling.", + ) + validerKanGjenopptaBehandling(behandling) + + loggService.gjenopptaBehandlingLogg(behandling) + logger.info("Gjenopptar behandling $behandlingId") + + aktivSettPåVent.aktiv = false + aktivSettPåVent.tidTattAvVent = nå + val settPåVent = lagreEllerOppdater(aktivSettPåVent) + + oppgaveService.settFristÅpneOppgaverPåBehandlingTil( + behandlingId = behandlingId, + nyFrist = LocalDate.now().plusDays(1), + ) + behandling.status = BehandlingStatus.UTREDES + behandlingHentOgPersisterService.lagreOgFlush(behandling) + + return settPåVent + } + + companion object { + val logger: Logger = LoggerFactory.getLogger(SettPåVentService::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentUtils.kt" new file mode 100644 index 000000000..7c33db0da --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentUtils.kt" @@ -0,0 +1,65 @@ +package no.nav.familie.ba.sak.kjerne.behandling.settpåvent + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import java.time.LocalDate + +fun validerBehandlingKanSettesPåVent( + gammelSettPåVent: SettPåVent?, + frist: LocalDate, + behandling: Behandling, +) { + if (gammelSettPåVent != null) { + throw FunksjonellFeil( + melding = "Behandling ${behandling.id} er allerede satt på vent.", + frontendFeilmelding = "Behandlingen er allerede satt på vent.", + ) + } + + validerFristErFremITiden(behandling, frist) + + if (behandling.status != BehandlingStatus.UTREDES) { + throw FunksjonellFeil( + melding = "Behandling ${behandling.id} har status=${behandling.status} og kan ikke settes på vent.", + frontendFeilmelding = "Behandlingen må ha status utredes for å kunne settes på vent", + ) + } + + if (!behandling.aktiv) { + throw Feil( + "Behandling ${behandling.id} er ikke aktiv og kan ikke settes på vent.", + ) + } +} + +fun validerFristErFremITiden( + behandling: Behandling, + frist: LocalDate, +) { + if (frist.isBefore(LocalDate.now())) { + throw FunksjonellFeil( + melding = "Frist for å vente på behandling ${behandling.id} er satt før dagens dato.", + frontendFeilmelding = "Fristen er satt før dagens dato.", + ) + } +} + +fun validerKanGjenopptaBehandling(behandling: Behandling) { + val status = behandling.status + if (status != BehandlingStatus.SATT_PÅ_VENT) { + val melding = "Behandling ${behandling.id} har status=$status og kan ikke gjenopptas." + if (status == BehandlingStatus.SATT_PÅ_MASKINELL_VENT) { + throw FunksjonellFeil( + melding = melding, + frontendFeilmelding = "Behandlingen er under maskinell vent, og kan gjenopptas senere.", + ) + } else { + throw Feil( + message = melding, + frontendFeilmelding = "Behandlingen må ha status satt på vent for å kunne gjenopptas.", + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingStegUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingStegUtils.kt new file mode 100644 index 000000000..0252549d5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingStegUtils.kt @@ -0,0 +1,31 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import java.time.YearMonth + +fun Tidslinje.kastFeilVedEndringEtter( + migreringsdatoForrigeIverksatteBehandling: YearMonth, + behandling: Behandling, +) { + val endringIUtbetalingEtterDato = perioder() + .filter { it.tilOgMed.tilYearMonth().isSameOrAfter(migreringsdatoForrigeIverksatteBehandling) } + + val erEndringIUtbetalingEtterMigreringsdato = endringIUtbetalingEtterDato.any { it.innhold == true } + + if (erEndringIUtbetalingEtterMigreringsdato) { + BehandlingsresultatSteg.logger.warn("Feil i behandling $behandling.\n\nEndring i måned ${endringIUtbetalingEtterDato.first { it.innhold == true }.fraOgMed.tilYearMonth()}.") + throw FunksjonellFeil( + "Det finnes endringer i behandlingen som har økonomisk konsekvens for bruker." + + "Det skal ikke skje for endre migreringsdatobehandlinger." + + "Endringer må gjøres i en separat behandling.", + "Det finnes endringer i behandlingen som har økonomisk konsekvens for bruker." + + "Det skal ikke skje for endre migreringsdatobehandlinger." + + "Endringer må gjøres i en separat behandling.", + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/Behandlingsresultat.md b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/Behandlingsresultat.md new file mode 100644 index 000000000..6a2999487 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/Behandlingsresultat.md @@ -0,0 +1,100 @@ +# Behandlingsresultat +Behandlingsresultatet skal gjenspeile hva som har skjedd i en behandling, og er et resultat av vurderinger og endringer som er gjort i denne behandlingen. Behandlingsresultatet er styrende for hvilken brevmal som skal brukes. + +For å utlede behandlingsresultat er det tre ting som peker seg ut som spesielt viktig: +- **Søknad**: Har vi mottatt en søknad eller er det fremstilt krav for noen personer? Isåfall, må vi gi et svar på søknaden i form av innvilgelse/avslag/delvis innvilget. +- **Endringer**: Har noe endret seg siden sist? +- **Opphør**: Har barnetrygden opphørt i denne behandlingen? + +Den tekniske løsningen vi har gått for prøver å utlede de tre aspektene hver for seg, før man til slutt sitter igjen med ett søknadsresultat, ett endringsresultat og ett opphørsresultat som man kan kombinere til et behandlingsresultat. + +## 1. Søknadsresultat +Søknadsresultat skal kun genereres for behandlinger med årsak søknad, fødselshendelse, klage eller grunnet manuell migrering. En viktig ting å legge merke til er også at søknadsresultat ikke utledes for _alle_ personer i disse behandlingene, men kun personene det er fremstilt krav for. + +### Personer fremstilt krav for +Det er ulik utledning for hvilke personer det er fremstilt krav for avhengig av type sak: +- **Søknad**: barn som er krysset av på "Registrer søknad"-steget + søker hvis det er søkt om utvidet barnetrygd +- **Fødselshendelse**: barn som er nye på behandlingen siden forrige gang +- **Manuell migrering** eller **klage**: alle personer i persongrunnlaget + +### Mulige søknadsresultater + +| Resultat | Forklaring | +|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Innvilget | Flere muligheter (gjelder kun personer fremstilt krav for):
1. Det er lagt til en ny andel med beløp > 0
2. Det er lagt til en ny andel med beløp satt til 0 kr pga. differanseberegning/delt bosted
3. Andel har endret beløp siden sist, hvor det nye beløpet er større enn 0 | +| Avslått | Flere muligheter:
1. Eksplisitt avslag for person fremstilt krav for
2. Lagt til ny andel med beløp satt til 0 kr pga. etterbetaling 3 år/allerede utbetalt/endre mottaker (for person fremstilt krav for)
3. Det finnes uregistrerte barn
4. Fødselshendelse hvor det finnes vilkår som enten er ikke vurdert eller ikke oppfylt | +| Delvis innvilget | Vi har både innvilget og avslått (trenger ikke være på samme person). | +| Ingen relevante endringer | Ingen av alternativene over.
F.eks. hvis en andel er fjernet, eller at andel har samme beløp nå som forrige gang. | +| null | Ikke søknad/fødselshendelse (dermed ingen personer fremstilt krav for) eller manuell migrering. | + + +## 2. Endringer +Skal utledes for **alle** behandlinger når det finnes en forrige behandling. Målet med endringsresultatet er å vise om det har vært en endring i behandlingen siden sist. +Dette kan være både endringer i beløp og endringer i andre ting som ikke påvirker beløpet (som lovverk, kompetanse osv.). + + +| Resultater | Forklaring | +|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| null | Ikke søknad/fødselshendelse (dermed ingen personer fremstilt krav for) eller manuell migrering. | +| Endringer | Flere muligheter:
1. Endring i beløp
  a) For personer fremstilt krav for: kun hvis beløp var større enn 0, men nå er andelen fjernet eller satt til 0kr
  b) Ellers: alle endringer i beløp
2. Endring i vilkårsvurdering
3. Endring i endret utbetaling andeler
4. Endring i kompetanse | +| Ingen endringer | Ingen endring i det som er nevnt i raden over. | + + + **OBS! Det er viktig å ikke ta med endringer som også fører til opphørsresultat eller søknadsresultat.** F.eks. det eneste som er gjort på vilkårsvurderingen er å sette sluttdato på et vilkår, noe som fører til opphør. Dette skal ikke utløse resultatet "endring" også. + +Endringer i **vilkårsvurdering** innebærer: +- Endringer i utdypende vilkårsvurdering +- Endringer i lovverk/regelverk +- Nye splitter i vilkår + +Vi ser kun på perioder som var oppfylt både i forrige behandling og i nåværende behandling. Dvs. hvis det eneste som er gjort er å sette tom-dato på et vilkår tidligere for å opphøre ytelsen, så blir ikke det regnet som en endring. + +På **kompetanser** regner man endring som endring av: +- Søkers aktivitet +- Søkers aktivitetsland +- Annen forelders aktivitet +- Annen forelders aktivitetsland +- Barnets bostedsland +- Resultat (primærland/sekundærland osv.) + +Hvis forrige kompetanse ikke var fylt ut ordentlig (som skjer ved migrering + evt autovedtak) så blir det returnert ingen endring. + +For **endret utbetaling andeler** bryr vi oss kun om endringer av: +- Avtaletidspunkt delt bosted +- Årsak +- Søknadstidspunkt + +_Eksempel: Forrige behandling og nåværende behandling ser helt like ut, med unntak av kompetansen som har endret annen forelders aktivitetsland fra Polen til Spania._ + +## 3. Opphør +Skal utledes for **alle** behandlinger. Opphørsresultatet reflekterer om det løper barnetrygd (finnes utbetalinger i fremtiden) eller ikke, og om opphøret skjedde i inneværende behandling. + +| Resultater | Forklaring | +|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Opphørt | To muligheter:
1. Ikke opphørt i forrige behandling, opphørt i denne behandlingen
2. Opphør i forrige behandling, men tidligere opphørsdato i denne behandlingen | +| Fortsatt opphørt | Barnetrygden var opphørt forrige behandling og har samme opphørsdato inneværende behandling | +| Ikke opphørt | Ikke opphør i denne behandlingen, det løper fortsatt barnetrygd | + + +## Kombinasjon av resultater +Behandlingsresultat = søknadsresultat + endringsresultat + opphørsresultat + +De fleste resultatene forklarer seg selv, som f.eks. "innvilget" + "endring" + "opphørt" = "innvilget, endret og opphørt". + +Vi har noen unntak når resultatet fra søknadssteget er "ingen relevante endringer". Grunnen til dette er fordi man alltid skal gi et resultat på søknaden, men "ingen relevante endringer" gjør ikke det alene. Dermed er man helt avhengig av kombinasjonene denne verdien kommer med. "Ingen relevante endringer" er kun lovlig i noen få kombinasjoner, ellers kastes det feil. Se tabell under for forklaring: + +| Søknadsresultat | Endringsresultat | Opphørsresultat | Behandlingsresultat | +|---------------------------|------------------|------------------|---------------------------------------------------------------------------------------| +| Ingen relevante endringer | Endring | Opphørt | Ugyldig - ville ha blitt "endret og opphørt" som er ugyldig på søknad | +| Ingen relevante endringer | Endring | Fortsatt opphørt | Ugyldig - ville ha blitt "endret/endret og fortsatt opphørt" som er ugyldig på søknad | +| Ingen relevante endringer | Endring | Ikke opphørt | **Endret og fortsatt innvilget** | +| Ingen relevante endringer | Ingen endring | Opphørt | Ugyldig - ville ha blitt "opphørt" som er ugyldig på søknad | +| Ingen relevante endringer | Ingen endring | Fortsatt opphørt | Ugyldig - ville ha blitt "fortsatt opphørt" som er ugydlig på søknad | +| Ingen relevante endringer | Ingen endring | Ikke opphørt | **Fortsatt innvilget** | + +En annen ting det er verdt å være obs på er: +- Fortsatt opphørt i kombinasjon med noe annet som er av betydning (f.eks. "Endret") tar ikke med fortsatt opphørt i resultatet. Vi ønsker kun å snakke om det som skjer i _denne_ behandlingen, og kommuniserer derfor kun ut "fortsatt opphørt" om det er det eneste som gjelder. + +## Valideringer +- Ikke lov med eksplisitt avslag for personer det ikke er fremstilt krav for (som ikke er søker) +- Søknadsresultat-steget må returnere et resultat (altså ikke null) hvis det er søknad/fødselshendelse/manuell migrering \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatEndringUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatEndringUtils.kt new file mode 100644 index 000000000..83dc8492f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatEndringUtils.kt @@ -0,0 +1,174 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.forrigeMåned +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatOpphørUtils.utledOpphørsdatoForNåværendeBehandlingMedFallback +import no.nav.familie.ba.sak.kjerne.beregning.AndelTilkjentYtelseTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.forrigebehandling.EndringIEndretUtbetalingAndelUtil +import no.nav.familie.ba.sak.kjerne.forrigebehandling.EndringIKompetanseUtil +import no.nav.familie.ba.sak.kjerne.forrigebehandling.EndringIVilkårsvurderingUtil +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.beskjær +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import java.time.YearMonth + +internal enum class Endringsresultat { + ENDRING, + INGEN_ENDRING, +} +object BehandlingsresultatEndringUtils { + + internal fun utledEndringsresultat( + nåværendeAndeler: List, + forrigeAndeler: List, + personerFremstiltKravFor: List, + nåværendeKompetanser: List, + forrigeKompetanser: List, + nåværendePersonResultat: Set, + forrigePersonResultat: Set, + nåværendeEndretAndeler: List, + forrigeEndretAndeler: List, + personerIBehandling: Set, + personerIForrigeBehandling: Set, + ): Endringsresultat { + val erEndringIBeløp = erEndringIBeløp( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = nåværendeEndretAndeler, + personerFremstiltKravFor = personerFremstiltKravFor, + ) + + val erEndringIKompetanse = erEndringIKompetanse( + nåværendeKompetanser = nåværendeKompetanser, + forrigeKompetanser = forrigeKompetanser, + ) + + val erEndringIVilkårsvurdering = erEndringIVilkårsvurdering( + nåværendePersonResultat = nåværendePersonResultat, + forrigePersonResultat = forrigePersonResultat, + personerIBehandling = personerIBehandling, + personerIForrigeBehandling = personerIForrigeBehandling, + ) + + val erEndringIEndretUtbetalingAndeler = erEndringIEndretUtbetalingAndeler( + nåværendeEndretAndeler = nåværendeEndretAndeler, + forrigeEndretAndeler = forrigeEndretAndeler, + ) + + val erMinstEnEndring = erEndringIBeløp || erEndringIKompetanse || erEndringIVilkårsvurdering || erEndringIEndretUtbetalingAndeler + + return if (erMinstEnEndring) Endringsresultat.ENDRING else Endringsresultat.INGEN_ENDRING + } + + // NB: For personer fremstilt krav for tar vi ikke hensyn til alle endringer i beløp i denne funksjonen + internal fun erEndringIBeløp( + nåværendeAndeler: List, + nåværendeEndretAndeler: List, + forrigeAndeler: List, + personerFremstiltKravFor: List, + ): Boolean { + val allePersonerMedAndeler = (nåværendeAndeler.map { it.aktør } + forrigeAndeler.map { it.aktør }).distinct() + val opphørstidspunkt = nåværendeAndeler.utledOpphørsdatoForNåværendeBehandlingMedFallback( + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = nåværendeEndretAndeler, + ) ?: return false // Returnerer false hvis verken forrige eller nåværende behandling har andeler + + val erEndringIBeløpForMinstEnPerson = allePersonerMedAndeler.any { aktør -> + val ytelseTyperForPerson = (nåværendeAndeler.map { it.type } + forrigeAndeler.map { it.type }).distinct() + + ytelseTyperForPerson.any { ytelseType -> + erEndringIBeløpForPersonOgType( + nåværendeAndeler = nåværendeAndeler.filter { it.aktør == aktør && it.type == ytelseType }, + forrigeAndeler = forrigeAndeler.filter { it.aktør == aktør && it.type == ytelseType }, + opphørstidspunkt = opphørstidspunkt, + erFremstiltKravForPerson = personerFremstiltKravFor.contains(aktør), + ) + } + } + + return erEndringIBeløpForMinstEnPerson + } + + // Kun interessert i endringer i beløp FØR opphørstidspunkt + private fun erEndringIBeløpForPersonOgType( + nåværendeAndeler: List, + forrigeAndeler: List, + opphørstidspunkt: YearMonth, + erFremstiltKravForPerson: Boolean, + ): Boolean { + val nåværendeTidslinje = AndelTilkjentYtelseTidslinje(nåværendeAndeler) + val forrigeTidslinje = AndelTilkjentYtelseTidslinje(forrigeAndeler) + + val endringIBeløpTidslinje = nåværendeTidslinje.kombinerMed(forrigeTidslinje) { nåværende, forrige -> + val nåværendeBeløp = nåværende?.kalkulertUtbetalingsbeløp ?: 0 + val forrigeBeløp = forrige?.kalkulertUtbetalingsbeløp ?: 0 + + if (erFremstiltKravForPerson) { + // Hvis det er søkt for person vil vi kun ha med endringer som går fra beløp > 0 til 0/null + when { + forrigeBeløp > 0 && nåværendeBeløp == 0 -> true + else -> false + } + } else { + // Hvis det ikke er søkt for person vil vi ha med alle endringer i beløp + when { + forrigeBeløp != nåværendeBeløp -> true + else -> false + } + } + }.fjernPerioderEtterOpphørsdato(opphørstidspunkt) + + return endringIBeløpTidslinje.perioder().any { it.innhold == true } + } + + private fun Tidslinje.fjernPerioderEtterOpphørsdato(opphørstidspunkt: YearMonth) = + this.beskjær(fraOgMed = TIDENES_MORGEN.tilMånedTidspunkt(), tilOgMed = opphørstidspunkt.forrigeMåned().tilTidspunkt()) + + internal fun erEndringIKompetanse( + nåværendeKompetanser: List, + forrigeKompetanser: List, + ): Boolean { + val endringIKompetanseTidslinje = EndringIKompetanseUtil.lagEndringIKompetanseTidslinje( + nåværendeKompetanser = nåværendeKompetanser, + forrigeKompetanser = forrigeKompetanser, + ) + + return endringIKompetanseTidslinje.perioder().any { it.innhold == true } + } + + internal fun erEndringIVilkårsvurdering( + nåværendePersonResultat: Set, + forrigePersonResultat: Set, + personerIBehandling: Set, + personerIForrigeBehandling: Set, + ): Boolean { + val endringIVilkårsvurderingTidslinje = EndringIVilkårsvurderingUtil.lagEndringIVilkårsvurderingTidslinje( + nåværendePersonResultater = nåværendePersonResultat, + forrigePersonResultater = forrigePersonResultat, + personerIBehandling = personerIBehandling, + personerIForrigeBehandling = personerIForrigeBehandling, + ) + return endringIVilkårsvurderingTidslinje.perioder().any { it.innhold == true } + } + + internal fun erEndringIEndretUtbetalingAndeler( + nåværendeEndretAndeler: List, + forrigeEndretAndeler: List, + ): Boolean { + val endringIEndretUtbetalingAndelTidslinje = EndringIEndretUtbetalingAndelUtil.lagEndringIEndretUtbetalingAndelTidslinje( + nåværendeEndretAndeler = nåværendeEndretAndeler, + forrigeEndretAndeler = forrigeEndretAndeler, + ) + + return endringIEndretUtbetalingAndelTidslinje.perioder().any { it.innhold == true } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatOpph\303\270rUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatOpph\303\270rUtils.kt" new file mode 100644 index 000000000..c2eb89b2c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatOpph\303\270rUtils.kt" @@ -0,0 +1,111 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.kjerne.beregning.AndelTilkjentYtelseTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.EndretUtbetalingAndelTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.tilAndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import java.time.YearMonth + +internal enum class Opphørsresultat { + OPPHØRT, + FORTSATT_OPPHØRT, + IKKE_OPPHØRT, +} + +object BehandlingsresultatOpphørUtils { + + internal fun hentOpphørsresultatPåBehandling( + nåværendeAndeler: List, + forrigeAndeler: List, + nåværendeEndretAndeler: List, + forrigeEndretAndeler: List, + ): Opphørsresultat { + val nåværendeBehandlingOpphørsdato = + nåværendeAndeler.utledOpphørsdatoForNåværendeBehandlingMedFallback( + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = nåværendeEndretAndeler, + ) + + val forrigeBehandlingOpphørsdato = + forrigeAndeler.utledOpphørsdatoForForrigeBehandling(forrigeEndretAndeler = forrigeEndretAndeler) + + val nesteMåned = YearMonth.now().plusMonths(1) + + return when { + // Rekkefølgen av sjekkene er viktig for å komme fram til riktig opphørsresultat. + nåværendeBehandlingOpphørsdato == null -> Opphørsresultat.IKKE_OPPHØRT // Både forrige og nåværende behandling har ingen andeler + nåværendeBehandlingOpphørsdato <= nesteMåned && forrigeBehandlingOpphørsdato > nåværendeBehandlingOpphørsdato -> Opphørsresultat.OPPHØRT // Nåværende behandling er opphørt og forrige har senere opphørsdato + nåværendeBehandlingOpphørsdato <= nesteMåned && nåværendeBehandlingOpphørsdato == forrigeBehandlingOpphørsdato -> Opphørsresultat.FORTSATT_OPPHØRT + else -> Opphørsresultat.IKKE_OPPHØRT + } + } + + private fun List.finnOpphørsdato() = this.maxOfOrNull { it.stønadTom }?.nesteMåned() + + /** + * Hvis opphørsdato ikke finnes i denne behandlingen så ønsker vi å bruke tidligste fom-dato fra forrige behandling + * Ingen opphørsdato i denne behandlingen skjer kun hvis det ikke finnes noen andeler, og da har vi to scenarier: + * 1. Ingen andeler i denne behandlingen, men andeler i forrige behandling. Da ønsker vi at opphørsdatoen i denne behandlingen skal være "første endring" som altså er lik tidligste fom-dato + * 2. Ingen andeler i denne behandlingen, ingen andeler i forrige behandling. Da vil denne funksjonen returnere null + */ + internal fun List.utledOpphørsdatoForNåværendeBehandlingMedFallback( + forrigeAndeler: List, + nåværendeEndretAndeler: List, + ): YearMonth? { + return this.filtrerBortIrrelevanteAndeler(endretAndeler = nåværendeEndretAndeler).finnOpphørsdato() + ?: forrigeAndeler.minOfOrNull { it.stønadFom } + } + + /** + * Hvis det ikke fantes noen andeler i forrige behandling defaulter vi til inneværende måned + */ + private fun List.utledOpphørsdatoForForrigeBehandling(forrigeEndretAndeler: List): YearMonth = + this.filtrerBortIrrelevanteAndeler(endretAndeler = forrigeEndretAndeler).finnOpphørsdato() ?: YearMonth.now() + .nesteMåned() + + /** + * Hvis det eksisterer andeler med beløp == 0 så ønsker vi å filtrere bort disse dersom det eksisterer endret utbetaling andel for perioden + * med årsak ALLEREDE_UTBETALT, ENDRE_MOTTAKER eller ETTERBETALING_3ÅR. Vi grupperer type andeler før vi oppretter tidslinjer da det kan oppstå + * overlapp hvis vi ikke gjør dette. + */ + internal fun List.filtrerBortIrrelevanteAndeler(endretAndeler: List): List { + val personerMedAndeler = this.map { it.aktør }.distinct() + + return personerMedAndeler.flatMap { aktør -> + val andelerGruppertPerTypePåPerson = this.filter { it.aktør == aktør }.groupBy { it.type } + val endretUtbetalingAndelerPåPerson = endretAndeler.filter { it.person?.aktør == aktør } + + andelerGruppertPerTypePåPerson.values.flatMap { andelerPerType -> + filtrerBortIrrelevanteAndelerPerPersonOgType(andelerPerType, endretUtbetalingAndelerPåPerson) + } + } + } + + private fun filtrerBortIrrelevanteAndelerPerPersonOgType( + andelerPåPersonFiltrertPåType: List, + endretAndelerPåPerson: List, + ): List { + val andelTilkjentYtelseTidslinje = AndelTilkjentYtelseTidslinje(andelerPåPersonFiltrertPåType) + val endretUtbetalingAndelTidslinje = EndretUtbetalingAndelTidslinje(endretAndelerPåPerson) + + return andelTilkjentYtelseTidslinje.kombinerMed(endretUtbetalingAndelTidslinje) { andelTilkjentYtelse, endretUtbetalingAndel -> + val kalkulertUtbetalingsbeløp = andelTilkjentYtelse?.kalkulertUtbetalingsbeløp ?: return@kombinerMed null + val endringsperiodeÅrsak = endretUtbetalingAndel?.årsak ?: return@kombinerMed andelTilkjentYtelse + + when (endringsperiodeÅrsak) { + Årsak.ALLEREDE_UTBETALT, + Årsak.ENDRE_MOTTAKER, + Årsak.ETTERBETALING_3ÅR, + -> + // Vi ønsker å filtrere bort andeler som har 0 i kalkulertUtbetalingsbeløp + if (kalkulertUtbetalingsbeløp == 0) null else andelTilkjentYtelse + + Årsak.DELT_BOSTED -> andelTilkjentYtelse + } + }.tilAndelTilkjentYtelse() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatService.kt new file mode 100644 index 000000000..3594eb91a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatService.kt @@ -0,0 +1,134 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import no.nav.familie.ba.sak.ekstern.restDomene.BehandlingUnderkategoriDTO +import no.nav.familie.ba.sak.ekstern.restDomene.SøknadDTO +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatUtils.skalUtledeSøknadsresultatForBehandling +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.KompetanseService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.søknad.SøknadGrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import org.springframework.stereotype.Service + +@Service +class BehandlingsresultatService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val søknadGrunnlagService: SøknadGrunnlagService, + private val personidentService: PersonidentService, + private val persongrunnlagService: PersongrunnlagService, + private val vilkårsvurderingService: VilkårsvurderingService, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val endretUtbetalingAndelHentOgPersisterService: EndretUtbetalingAndelHentOgPersisterService, + private val kompetanseService: KompetanseService, +) { + + internal fun finnPersonerFremstiltKravFor(behandling: Behandling, søknadDTO: SøknadDTO?, forrigeBehandling: Behandling?): List { + val personerFremstiltKravFor = when { + behandling.opprettetÅrsak == BehandlingÅrsak.SØKNAD -> { + // alle barna som er krysset av på søknad + val barnFraSøknad = søknadDTO?.barnaMedOpplysninger + ?.filter { it.erFolkeregistrert && it.inkludertISøknaden } + ?.map { personidentService.hentAktør(it.ident) } + ?: emptyList() + + // hvis det søkes om utvidet skal søker med + val utvidetBarnetrygdSøker = + if (søknadDTO?.underkategori == BehandlingUnderkategoriDTO.UTVIDET) listOf(behandling.fagsak.aktør) else emptyList() + + barnFraSøknad + utvidetBarnetrygdSøker + } + + behandling.opprettetÅrsak == BehandlingÅrsak.FØDSELSHENDELSE -> persongrunnlagService.finnNyeBarn(behandling, forrigeBehandling).map { it.aktør } + behandling.erManuellMigrering() || behandling.opprettetÅrsak == BehandlingÅrsak.KLAGE -> persongrunnlagService.hentAktivThrows(behandling.id).personer.map { it.aktør } + else -> emptyList() + } + + return personerFremstiltKravFor.distinct() + } + + internal fun utledBehandlingsresultat(behandlingId: Long): Behandlingsresultat { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + val forrigeBehandling = behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = behandling.fagsak.id) + + val søknadGrunnlag = søknadGrunnlagService.hentAktiv(behandlingId = behandling.id) + val søknadDTO = søknadGrunnlag?.hentSøknadDto() + + val forrigeAndelerTilkjentYtelse = forrigeBehandling?.let { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId = it.id) } ?: emptyList() + val andelerTilkjentYtelse = andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId = behandlingId) + + val forrigeEndretUtbetalingAndeler = forrigeBehandling?.let { endretUtbetalingAndelHentOgPersisterService.hentForBehandling(behandlingId = it.id) } ?: emptyList() + val endretUtbetalingAndeler = endretUtbetalingAndelHentOgPersisterService.hentForBehandling(behandlingId = behandlingId) + + val vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandlingThrows(behandlingId = behandlingId) + + val personerIBehandling = persongrunnlagService.hentAktivThrows(behandlingId = behandling.id).personer.toSet() + val personerIForrigeBehandling = forrigeBehandling?.let { persongrunnlagService.hentAktivThrows(behandlingId = forrigeBehandling.id).personer.toSet() } ?: emptySet() + + val personerFremstiltKravFor = finnPersonerFremstiltKravFor( + behandling = behandling, + søknadDTO = søknadDTO, + forrigeBehandling = forrigeBehandling, + ) + + BehandlingsresultatValideringUtils.validerAtBarePersonerFremstiltKravForEllerSøkerHarFåttEksplisittAvslag(personerFremstiltKravFor = personerFremstiltKravFor, personResultater = vilkårsvurdering.personResultater) + + // 1 SØKNAD + val søknadsresultat = if (skalUtledeSøknadsresultatForBehandling(behandling)) { + BehandlingsresultatSøknadUtils.utledResultatPåSøknad( + nåværendeAndeler = andelerTilkjentYtelse, + forrigeAndeler = forrigeAndelerTilkjentYtelse, + endretUtbetalingAndeler = endretUtbetalingAndeler, + personerFremstiltKravFor = personerFremstiltKravFor, + nåværendePersonResultater = vilkårsvurdering.personResultater, + behandlingÅrsak = behandling.opprettetÅrsak, + finnesUregistrerteBarn = søknadGrunnlag?.hentUregistrerteBarn()?.isNotEmpty() ?: false, + ) + } else { + null + } + + // 2 ENDRINGER + val endringsresultat = if (forrigeBehandling != null) { + val forrigeVilkårsvurdering = vilkårsvurderingService.hentAktivForBehandlingThrows(behandlingId = forrigeBehandling.id) + val kompetanser = kompetanseService.hentKompetanser(behandlingId = BehandlingId(behandlingId)) + val forrigeKompetanser = kompetanseService.hentKompetanser(behandlingId = BehandlingId(forrigeBehandling.id)) + + BehandlingsresultatEndringUtils.utledEndringsresultat( + nåværendeAndeler = andelerTilkjentYtelse, + forrigeAndeler = forrigeAndelerTilkjentYtelse, + nåværendeEndretAndeler = endretUtbetalingAndeler, + forrigeEndretAndeler = forrigeEndretUtbetalingAndeler, + nåværendePersonResultat = vilkårsvurdering.personResultater, + forrigePersonResultat = forrigeVilkårsvurdering.personResultater, + nåværendeKompetanser = kompetanser.toList(), + forrigeKompetanser = forrigeKompetanser.toList(), + personerFremstiltKravFor = personerFremstiltKravFor, + personerIBehandling = personerIBehandling, + personerIForrigeBehandling = personerIForrigeBehandling, + ) + } else { + Endringsresultat.INGEN_ENDRING + } + + // 3 OPPHØR + val opphørsresultat = BehandlingsresultatOpphørUtils.hentOpphørsresultatPåBehandling( + nåværendeAndeler = andelerTilkjentYtelse, + forrigeAndeler = forrigeAndelerTilkjentYtelse, + nåværendeEndretAndeler = endretUtbetalingAndeler, + forrigeEndretAndeler = forrigeEndretUtbetalingAndeler, + ) + + // KOMBINER + val behandlingsresultat = BehandlingsresultatUtils.kombinerResultaterTilBehandlingsresultat(søknadsresultat, endringsresultat, opphørsresultat) + + return behandlingsresultat + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatSteg.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatSteg.kt new file mode 100644 index 000000000..4516272cd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatSteg.kt @@ -0,0 +1,217 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat.FORTSATT_INNVILGET +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValidering.validerAtSatsendringKunOppdatererSatsPåEksisterendePerioder +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValidering.validerAtTilkjentYtelseHarFornuftigePerioderOgBeløp +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerAtAlleOpprettedeEndringerErUtfylt +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerAtEndringerErTilknyttetAndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerPeriodeInnenforTilkjentytelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerÅrsak +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.endretutbetaling.validerAtDetFinnesDeltBostedEndringerMedSammeProsentForUtvidedeEndringer +import no.nav.familie.ba.sak.kjerne.endretutbetaling.validerBarnasVilkår +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.barn +import no.nav.familie.ba.sak.kjerne.simulering.SimuleringService +import no.nav.familie.ba.sak.kjerne.steg.BehandlingSteg +import no.nav.familie.ba.sak.kjerne.steg.EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class BehandlingsresultatSteg( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val behandlingService: BehandlingService, + private val simuleringService: SimuleringService, + private val vedtakService: VedtakService, + private val vedtaksperiodeService: VedtaksperiodeService, + private val behandlingsresultatService: BehandlingsresultatService, + private val vilkårService: VilkårService, + private val persongrunnlagService: PersongrunnlagService, + private val beregningService: BeregningService, + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, +) : BehandlingSteg { + + override fun preValiderSteg(behandling: Behandling, stegService: StegService?) { + if (!behandling.erSatsendring() && behandling.skalBehandlesAutomatisk) return + + val søkerOgBarn = persongrunnlagService.hentSøkerOgBarnPåBehandlingThrows(behandling.id) + if (behandling.type != BehandlingType.TEKNISK_ENDRING && behandling.type != BehandlingType.MIGRERING_FRA_INFOTRYGD_OPPHØRT) { + val vilkårsvurdering = vilkårService.hentVilkårsvurderingThrows(behandlingId = behandling.id) + + validerBarnasVilkår(søkerOgBarn.barn(), vilkårsvurdering) + } + + val tilkjentYtelse = beregningService.hentTilkjentYtelseForBehandling(behandlingId = behandling.id) + + if (behandling.erSatsendring()) { + validerSatsendring(tilkjentYtelse) + } + + validerAtTilkjentYtelseHarFornuftigePerioderOgBeløp( + tilkjentYtelse = tilkjentYtelse, + søkerOgBarn = søkerOgBarn, + ) + + val endreteUtbetalingerMedAndeler = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnEndreteUtbetalingerMedAndelerTilkjentYtelse(behandling.id) + + validerAtAlleOpprettedeEndringerErUtfylt(endreteUtbetalingerMedAndeler.map { it.endretUtbetalingAndel }) + validerAtEndringerErTilknyttetAndelTilkjentYtelse(endreteUtbetalingerMedAndeler) + validerAtDetFinnesDeltBostedEndringerMedSammeProsentForUtvidedeEndringer( + endretUtbetalingAndelerMedÅrsakDeltBosted = endreteUtbetalingerMedAndeler.filter { it.årsak == Årsak.DELT_BOSTED }, + ) + + validerPeriodeInnenforTilkjentytelse( + endreteUtbetalingerMedAndeler.map { it.endretUtbetalingAndel }, + tilkjentYtelse.andelerTilkjentYtelse, + ) + + validerÅrsak( + endreteUtbetalingerMedAndeler.map { it.endretUtbetalingAndel }, + vilkårService.hentVilkårsvurdering(behandling.id), + ) + + if (behandling.opprettetÅrsak == BehandlingÅrsak.ENDRE_MIGRERINGSDATO) { + validerIngenEndringIUtbetalingEtterMigreringsdatoenTilForrigeIverksatteBehandling(behandling) + } + } + + @Transactional + override fun utførStegOgAngiNeste(behandling: Behandling, data: String): StegType { + val behandlingMedOppdatertBehandlingsresultat = + if (behandling.erMigrering() && behandling.skalBehandlesAutomatisk) { + settBehandlingsresultat(behandling, Behandlingsresultat.INNVILGET) + } else { + val resultat = behandlingsresultatService.utledBehandlingsresultat(behandlingId = behandling.id) + + behandlingService.oppdaterBehandlingsresultat( + behandlingId = behandling.id, + resultat = resultat, + ) + } + + validerBehandlingsresultatErGyldigForÅrsak(behandlingMedOppdatertBehandlingsresultat) + + if (behandlingMedOppdatertBehandlingsresultat.erBehandlingMedVedtaksbrevutsending()) { + behandlingService.nullstillEndringstidspunkt(behandling.id) + vedtaksperiodeService.oppdaterVedtakMedVedtaksperioder( + vedtak = vedtakService.hentAktivForBehandlingThrows( + behandlingId = behandling.id, + ), + ) + } + + val endringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi = + beregningService.hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(behandling) + + if (behandlingMedOppdatertBehandlingsresultat.skalRettFraBehandlingsresultatTilIverksetting( + endringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi == ENDRING_I_UTBETALING, + ) || beregningService.kanAutomatiskIverksetteSmåbarnstilleggEndring( + behandling = behandlingMedOppdatertBehandlingsresultat, + sistIverksatteBehandling = behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt( + behandling = behandlingMedOppdatertBehandlingsresultat, + ), + ) + ) { + behandlingService.oppdaterStatusPåBehandling( + behandlingMedOppdatertBehandlingsresultat.id, + BehandlingStatus.IVERKSETTER_VEDTAK, + ) + } else { + simuleringService.oppdaterSimuleringPåBehandling(behandlingMedOppdatertBehandlingsresultat) + } + + return hentNesteStegGittEndringerIUtbetaling( + behandling, + endringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi, + ) + } + + override fun postValiderSteg(behandling: Behandling) { + if (behandling.opprettetÅrsak.erOmregningsårsak() && behandling.resultat != FORTSATT_INNVILGET) { + throw Feil("Behandling ${behandling.id} er omregningssak men er ikke fortsatt innvilget.") + } + } + + override fun stegType(): StegType { + return StegType.BEHANDLINGSRESULTAT + } + + private fun validerBehandlingsresultatErGyldigForÅrsak(behandlingMedOppdatertBehandlingsresultat: Behandling) { + if (behandlingMedOppdatertBehandlingsresultat.erManuellMigrering() && + ( + behandlingMedOppdatertBehandlingsresultat.resultat.erAvslått() || + behandlingMedOppdatertBehandlingsresultat.resultat == Behandlingsresultat.DELVIS_INNVILGET + ) + ) { + throw FunksjonellFeil( + "Du har fått behandlingsresultatet " + + "${behandlingMedOppdatertBehandlingsresultat.resultat.displayName}. " + + "Dette er ikke støttet på migreringsbehandlinger. " + + "Meld sak i Porten om du er uenig i resultatet.", + ) + } + } + + private fun settBehandlingsresultat(behandling: Behandling, resultat: Behandlingsresultat): Behandling { + behandling.resultat = resultat + return behandlingHentOgPersisterService.lagreEllerOppdater(behandling) + } + + private fun validerIngenEndringIUtbetalingEtterMigreringsdatoenTilForrigeIverksatteBehandling(behandling: Behandling) { + if (behandling.status == BehandlingStatus.AVSLUTTET) return + + val endringIUtbetalingTidslinje = + beregningService.hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomiTidslinje(behandling) + + val migreringsdatoForrigeIverksatteBehandling = beregningService + .hentAndelerFraForrigeIverksattebehandling(behandling) + .minOfOrNull { it.stønadFom } + + endringIUtbetalingTidslinje.kastFeilVedEndringEtter( + migreringsdatoForrigeIverksatteBehandling = migreringsdatoForrigeIverksatteBehandling + ?: TIDENES_ENDE.toYearMonth(), + behandling = behandling, + ) + } + + private fun validerSatsendring(tilkjentYtelse: TilkjentYtelse) { + val forrigeBehandling = + behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(tilkjentYtelse.behandling) + ?: throw FunksjonellFeil("Kan ikke kjøre satsendring når det ikke finnes en tidligere behandling på fagsaken") + val andelerFraForrigeBehandling = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId = forrigeBehandling.id) + + validerAtSatsendringKunOppdatererSatsPåEksisterendePerioder( + andelerFraForrigeBehandling = andelerFraForrigeBehandling, + andelerTilkjentYtelse = tilkjentYtelse.andelerTilkjentYtelse.toList(), + ) + } + + companion object { + val logger = LoggerFactory.getLogger(this::class.java)!! + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatS\303\270knadUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatS\303\270knadUtils.kt" new file mode 100644 index 000000000..8bbd55f85 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatS\303\270knadUtils.kt" @@ -0,0 +1,168 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.AndelTilkjentYtelseTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.EndretUtbetalingAndelTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat + +internal enum class Søknadsresultat { + INNVILGET, + AVSLÅTT, + DELVIS_INNVILGET, + INGEN_RELEVANTE_ENDRINGER, +} + +object BehandlingsresultatSøknadUtils { + + internal fun utledResultatPåSøknad( + forrigeAndeler: List, + nåværendeAndeler: List, + nåværendePersonResultater: Set, + personerFremstiltKravFor: List, + endretUtbetalingAndeler: List, + behandlingÅrsak: BehandlingÅrsak, + finnesUregistrerteBarn: Boolean, + ): Søknadsresultat { + val resultaterFraAndeler = utledSøknadResultatFraAndelerTilkjentYtelse( + forrigeAndeler = forrigeAndeler, + nåværendeAndeler = nåværendeAndeler, + personerFremstiltKravFor = personerFremstiltKravFor, + endretUtbetalingAndeler = endretUtbetalingAndeler, + ) + + val erEksplisittAvslagPåMinstEnPersonFremstiltKravFor = erEksplisittAvslagPåMinstEnPersonFremstiltKravForEllerSøker( + nåværendePersonResultater = nåværendePersonResultater, + personerFremstiltKravFor = personerFremstiltKravFor, + ) + + val erFødselshendelseMedAvslag = if (behandlingÅrsak == BehandlingÅrsak.FØDSELSHENDELSE) { + nåværendePersonResultater.any { personResultat -> + personResultat.vilkårResultater + .any { it.resultat == Resultat.IKKE_OPPFYLT || it.resultat == Resultat.IKKE_VURDERT } + } + } else { + false + } + + val alleResultater = ( + if (erEksplisittAvslagPåMinstEnPersonFremstiltKravFor || erFødselshendelseMedAvslag || finnesUregistrerteBarn) { + resultaterFraAndeler.plus(Søknadsresultat.AVSLÅTT) + } else { + resultaterFraAndeler + } + ).distinct() + + return alleResultater.kombinerSøknadsresultater(behandlingÅrsak = behandlingÅrsak) + } + + internal fun utledSøknadResultatFraAndelerTilkjentYtelse( + forrigeAndeler: List, + nåværendeAndeler: List, + personerFremstiltKravFor: List, + endretUtbetalingAndeler: List, + ): List { + val alleSøknadsresultater = personerFremstiltKravFor.flatMap { aktør -> + val ytelseTyper = (forrigeAndeler.map { it.type } + nåværendeAndeler.map { it.type }).distinct() + + ytelseTyper.flatMap { ytelseType -> + utledSøknadResultatFraAndelerTilkjentYtelsePerPersonOgType( + forrigeAndelerForPerson = forrigeAndeler.filter { it.aktør == aktør && it.type == ytelseType }, + nåværendeAndelerForPerson = nåværendeAndeler.filter { it.aktør == aktør && it.type == ytelseType }, + endretUtbetalingAndelerForPerson = endretUtbetalingAndeler.filter { it.person?.aktør == aktør }, + ) + } + } + + return alleSøknadsresultater.distinct() + } + + private fun utledSøknadResultatFraAndelerTilkjentYtelsePerPersonOgType( + forrigeAndelerForPerson: List, + nåværendeAndelerForPerson: List, + endretUtbetalingAndelerForPerson: List, + ): List { + val forrigeTidslinje = AndelTilkjentYtelseTidslinje(forrigeAndelerForPerson) + val nåværendeTidslinje = AndelTilkjentYtelseTidslinje(nåværendeAndelerForPerson) + val endretUtbetalingTidslinje = EndretUtbetalingAndelTidslinje(endretUtbetalingAndelerForPerson) + + val resultatTidslinje = nåværendeTidslinje.kombinerMed(forrigeTidslinje, endretUtbetalingTidslinje) { nåværende, forrige, endretUtbetalingAndel -> + val forrigeBeløp = forrige?.kalkulertUtbetalingsbeløp + val nåværendeBeløp = nåværende?.kalkulertUtbetalingsbeløp + + when { + nåværendeBeløp == null -> Søknadsresultat.INGEN_RELEVANTE_ENDRINGER // Finnes ikke andel i denne behandlingen + forrigeBeløp == null && nåværendeBeløp == 0 -> { // Lagt til ny andel, men den er overstyrt til 0 kr. Må se på årsak for å finne resultat + when (endretUtbetalingAndel?.årsak) { + null -> if (nåværende.differanseberegnetPeriodebeløp != null) { + Søknadsresultat.INNVILGET + } else { + secureLogger.info( + "Andel $nåværende er satt til 0kr, men det skyldes verken differanseberegning eller endret utbetaling andel." + + "\nNåværende andeler: $nåværendeAndelerForPerson" + + "\nEndret utbetaling andeler: $endretUtbetalingAndelerForPerson", + ) + throw Feil("Andel er satt til 0 kr, men det skyldes verken differanseberegning eller endret utbetaling andel") + } + Årsak.DELT_BOSTED -> Søknadsresultat.INNVILGET + Årsak.ALLEREDE_UTBETALT, + Årsak.ENDRE_MOTTAKER, + Årsak.ETTERBETALING_3ÅR, + -> Søknadsresultat.AVSLÅTT + } + } + forrigeBeløp != nåværendeBeløp && nåværendeBeløp > 0 -> Søknadsresultat.INNVILGET // Innvilget beløp som er annerledes enn forrige + else -> Søknadsresultat.INGEN_RELEVANTE_ENDRINGER + } + } + + return resultatTidslinje.perioder().mapNotNull { it.innhold }.distinct() + } + + private fun erEksplisittAvslagPåMinstEnPersonFremstiltKravForEllerSøker( + nåværendePersonResultater: Set, + personerFremstiltKravFor: List, + ): Boolean = + nåværendePersonResultater + .filter { personerFremstiltKravFor.contains(it.aktør) || it.erSøkersResultater() } + .any { + it.harEksplisittAvslag() + } + + internal fun List.kombinerSøknadsresultater(behandlingÅrsak: BehandlingÅrsak): Søknadsresultat { + val resultaterUtenIngenEndringer = this.filter { it != Søknadsresultat.INGEN_RELEVANTE_ENDRINGER } + + val ingenSøknadsresultatFeil = if (behandlingÅrsak == BehandlingÅrsak.KLAGE) { + FunksjonellFeil( + frontendFeilmelding = "Du har opprettet en revurdering med årsak klage, men ikke innvilget noen perioder. Denne behandlingen kan kun brukes til full omgjøring.", + melding = "Klarer ikke utlede søknadsresultat for behandling med årsak klage. Det er ikke innvilget noen perioder.", + ) + } else { + FunksjonellFeil( + frontendFeilmelding = "Du har opprettet en behandling som følge av søknad, men har enten ikke krysset av for noen barn det er søkt for eller avslått/innvilget noen perioder.", + melding = "Klarer ikke utlede søknadsresultat. Finner ingen resultater.", + ) + } + + return when { + this.isEmpty() -> throw ingenSøknadsresultatFeil + this.size == 1 -> this.single() + resultaterUtenIngenEndringer.size == 1 -> resultaterUtenIngenEndringer.single() + resultaterUtenIngenEndringer.size == 2 && resultaterUtenIngenEndringer.containsAll( + listOf( + Søknadsresultat.INNVILGET, + Søknadsresultat.AVSLÅTT, + ), + ) -> Søknadsresultat.DELVIS_INNVILGET + else -> throw Feil("Klarer ikke kombinere søknadsresultater: $this") + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatUtils.kt new file mode 100644 index 000000000..7ab590366 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatUtils.kt @@ -0,0 +1,117 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.fpsak.tidsserie.LocalDateSegment +import no.nav.fpsak.tidsserie.LocalDateTimeline +import no.nav.fpsak.tidsserie.StandardCombinators + +object BehandlingsresultatUtils { + + internal fun skalUtledeSøknadsresultatForBehandling(behandling: Behandling): Boolean { + return behandling.erManuellMigrering() || behandling.opprettetÅrsak in listOf( + BehandlingÅrsak.SØKNAD, + BehandlingÅrsak.FØDSELSHENDELSE, + BehandlingÅrsak.KLAGE, + ) + } + + internal fun kombinerResultaterTilBehandlingsresultat( + søknadsresultat: Søknadsresultat?, // Søknadsresultat er null hvis det ikke er en søknad/fødselshendelse/manuell migrering + endringsresultat: Endringsresultat, + opphørsresultat: Opphørsresultat, + ): Behandlingsresultat { + fun sjekkResultat( + ønsketSøknadsresultat: Søknadsresultat?, + ønsketEndringsresultat: Endringsresultat, + ønsketOpphørsresultat: Opphørsresultat, + ): Boolean = + søknadsresultat == ønsketSøknadsresultat && endringsresultat == ønsketEndringsresultat && opphørsresultat == ønsketOpphørsresultat + + fun ugyldigBehandlingsresultatFeil(behandlingsresultatString: String) = + FunksjonellFeil( + frontendFeilmelding = "Du har fått behandlingsresultatet $behandlingsresultatString, men behandlingen er registrert med årsak søknad. Du må enten innvilge eller avslå noe for å kunne fortsette. Om du er uenig i resultatet ta kontakt med Superbruker.", + melding = "Kombinasjonen av (søknadsresultat=$søknadsresultat, endringsresultat=$endringsresultat, opphørsresultat=$opphørsresultat) er ikke støttet i løsningen.", + ) + + return when { + // Søknad/fødselshendelse/manuell migrering + sjekkResultat(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, Endringsresultat.ENDRING, Opphørsresultat.OPPHØRT) -> throw ugyldigBehandlingsresultatFeil("Endret og opphørt") + sjekkResultat(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, Endringsresultat.ENDRING, Opphørsresultat.FORTSATT_OPPHØRT) -> throw ugyldigBehandlingsresultatFeil("Endret og fortsatt opphørt") + sjekkResultat(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, Endringsresultat.ENDRING, Opphørsresultat.IKKE_OPPHØRT) -> Behandlingsresultat.ENDRET_OG_FORTSATT_INNVILGET + sjekkResultat(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, Endringsresultat.INGEN_ENDRING, Opphørsresultat.OPPHØRT) -> throw ugyldigBehandlingsresultatFeil("Opphørt") + sjekkResultat(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, Endringsresultat.INGEN_ENDRING, Opphørsresultat.FORTSATT_OPPHØRT) -> throw ugyldigBehandlingsresultatFeil("Fortsatt opphørt") + sjekkResultat(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, Endringsresultat.INGEN_ENDRING, Opphørsresultat.IKKE_OPPHØRT) -> Behandlingsresultat.FORTSATT_INNVILGET + + sjekkResultat(Søknadsresultat.INNVILGET, Endringsresultat.ENDRING, Opphørsresultat.OPPHØRT) -> Behandlingsresultat.INNVILGET_ENDRET_OG_OPPHØRT + sjekkResultat(Søknadsresultat.INNVILGET, Endringsresultat.ENDRING, Opphørsresultat.FORTSATT_OPPHØRT) -> Behandlingsresultat.INNVILGET_OG_ENDRET + sjekkResultat(Søknadsresultat.INNVILGET, Endringsresultat.ENDRING, Opphørsresultat.IKKE_OPPHØRT) -> Behandlingsresultat.INNVILGET_OG_ENDRET + sjekkResultat(Søknadsresultat.INNVILGET, Endringsresultat.INGEN_ENDRING, Opphørsresultat.OPPHØRT) -> Behandlingsresultat.INNVILGET_OG_OPPHØRT + sjekkResultat(Søknadsresultat.INNVILGET, Endringsresultat.INGEN_ENDRING, Opphørsresultat.FORTSATT_OPPHØRT) -> Behandlingsresultat.INNVILGET + sjekkResultat(Søknadsresultat.INNVILGET, Endringsresultat.INGEN_ENDRING, Opphørsresultat.IKKE_OPPHØRT) -> Behandlingsresultat.INNVILGET + + sjekkResultat(Søknadsresultat.AVSLÅTT, Endringsresultat.ENDRING, Opphørsresultat.OPPHØRT) -> Behandlingsresultat.AVSLÅTT_ENDRET_OG_OPPHØRT + sjekkResultat(Søknadsresultat.AVSLÅTT, Endringsresultat.ENDRING, Opphørsresultat.FORTSATT_OPPHØRT) -> Behandlingsresultat.AVSLÅTT_OG_ENDRET + sjekkResultat(Søknadsresultat.AVSLÅTT, Endringsresultat.ENDRING, Opphørsresultat.IKKE_OPPHØRT) -> Behandlingsresultat.AVSLÅTT_OG_ENDRET + sjekkResultat(Søknadsresultat.AVSLÅTT, Endringsresultat.INGEN_ENDRING, Opphørsresultat.OPPHØRT) -> Behandlingsresultat.AVSLÅTT_OG_OPPHØRT + sjekkResultat(Søknadsresultat.AVSLÅTT, Endringsresultat.INGEN_ENDRING, Opphørsresultat.FORTSATT_OPPHØRT) -> Behandlingsresultat.AVSLÅTT + sjekkResultat(Søknadsresultat.AVSLÅTT, Endringsresultat.INGEN_ENDRING, Opphørsresultat.IKKE_OPPHØRT) -> Behandlingsresultat.AVSLÅTT + + sjekkResultat(Søknadsresultat.DELVIS_INNVILGET, Endringsresultat.ENDRING, Opphørsresultat.OPPHØRT) -> Behandlingsresultat.DELVIS_INNVILGET_ENDRET_OG_OPPHØRT + sjekkResultat(Søknadsresultat.DELVIS_INNVILGET, Endringsresultat.ENDRING, Opphørsresultat.FORTSATT_OPPHØRT) -> Behandlingsresultat.DELVIS_INNVILGET_OG_ENDRET + sjekkResultat(Søknadsresultat.DELVIS_INNVILGET, Endringsresultat.ENDRING, Opphørsresultat.IKKE_OPPHØRT) -> Behandlingsresultat.DELVIS_INNVILGET_OG_ENDRET + sjekkResultat(Søknadsresultat.DELVIS_INNVILGET, Endringsresultat.INGEN_ENDRING, Opphørsresultat.OPPHØRT) -> Behandlingsresultat.DELVIS_INNVILGET_OG_OPPHØRT + sjekkResultat(Søknadsresultat.DELVIS_INNVILGET, Endringsresultat.INGEN_ENDRING, Opphørsresultat.FORTSATT_OPPHØRT) -> Behandlingsresultat.DELVIS_INNVILGET + sjekkResultat(Søknadsresultat.DELVIS_INNVILGET, Endringsresultat.INGEN_ENDRING, Opphørsresultat.IKKE_OPPHØRT) -> Behandlingsresultat.DELVIS_INNVILGET + + // Ikke søknad/fødselshendelse/manuell migrering + sjekkResultat(null, Endringsresultat.ENDRING, Opphørsresultat.OPPHØRT) -> Behandlingsresultat.ENDRET_OG_OPPHØRT + sjekkResultat(null, Endringsresultat.ENDRING, Opphørsresultat.FORTSATT_OPPHØRT) -> Behandlingsresultat.ENDRET_UTBETALING + sjekkResultat(null, Endringsresultat.ENDRING, Opphørsresultat.IKKE_OPPHØRT) -> Behandlingsresultat.ENDRET_UTBETALING + sjekkResultat(null, Endringsresultat.INGEN_ENDRING, Opphørsresultat.OPPHØRT) -> Behandlingsresultat.OPPHØRT + sjekkResultat(null, Endringsresultat.INGEN_ENDRING, Opphørsresultat.FORTSATT_OPPHØRT) -> Behandlingsresultat.FORTSATT_OPPHØRT + sjekkResultat(null, Endringsresultat.INGEN_ENDRING, Opphørsresultat.IKKE_OPPHØRT) -> Behandlingsresultat.FORTSATT_INNVILGET + + // Skal egentlig aldri kunne komme hit, alle kombinasjoner skal være skrevet ut + else -> throw Feil( + frontendFeilmelding = "Du har fått et behandlingsresultat vi ikke støtter. Meld sak i Porten om du er uenig i resultatet.", + message = "Klarer ikke utlede behandlingsresultat fra (søknadsresultat=$søknadsresultat, endringsresultat=$endringsresultat, opphørsresultat=$opphørsresultat)", + ) + } + } +} + +fun hentUtbetalingstidslinjeForSøker(andeler: List): LocalDateTimeline { + val utvidetTidslinje = LocalDateTimeline( + andeler.filter { it.type == YtelseType.UTVIDET_BARNETRYGD } + .map { + LocalDateSegment( + it.stønadFom.førsteDagIInneværendeMåned(), + it.stønadTom.sisteDagIInneværendeMåned(), + it.kalkulertUtbetalingsbeløp, + ) + }, + ) + val småbarnstilleggAndeler = LocalDateTimeline( + andeler.filter { it.type == YtelseType.SMÅBARNSTILLEGG }.map { + LocalDateSegment( + it.stønadFom.førsteDagIInneværendeMåned(), + it.stønadTom.sisteDagIInneværendeMåned(), + it.kalkulertUtbetalingsbeløp, + ) + }, + ) + + return utvidetTidslinje.combine( + småbarnstilleggAndeler, + StandardCombinators::sum, + LocalDateTimeline.JoinStyle.CROSS_JOIN, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatValideringUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatValideringUtils.kt new file mode 100644 index 000000000..995540a10 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatValideringUtils.kt @@ -0,0 +1,56 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat + +object BehandlingsresultatValideringUtils { + internal fun validerAtBarePersonerFremstiltKravForEllerSøkerHarFåttEksplisittAvslag( + personerFremstiltKravFor: List, + personResultater: Set, + ) { + val personerSomHarEksplisittAvslag = personResultater.filter { it.harEksplisittAvslag() } + + if (personerSomHarEksplisittAvslag.any { !personerFremstiltKravFor.contains(it.aktør) && !it.erSøkersResultater() }) { + throw FunksjonellFeil( + frontendFeilmelding = "Det eksisterer personer som har fått eksplisitt avslag, men som det ikke er blitt fremstilt krav for.", + melding = "Det eksisterer personer som har fått eksplisitt avslag, men som det ikke har blitt fremstilt krav for.", + ) + } + } + + internal fun validerBehandlingsresultat(behandling: Behandling, resultat: Behandlingsresultat) { + if (( + behandling.type == BehandlingType.FØRSTEGANGSBEHANDLING && setOf( + Behandlingsresultat.AVSLÅTT_OG_OPPHØRT, + Behandlingsresultat.ENDRET_UTBETALING, + Behandlingsresultat.ENDRET_UTEN_UTBETALING, + Behandlingsresultat.ENDRET_OG_OPPHØRT, + Behandlingsresultat.OPPHØRT, + Behandlingsresultat.FORTSATT_INNVILGET, + Behandlingsresultat.IKKE_VURDERT, + ).contains(resultat) + ) || + (behandling.type == BehandlingType.REVURDERING && resultat == Behandlingsresultat.IKKE_VURDERT) + ) { + val feilmelding = "Behandlingsresultatet ${resultat.displayName.lowercase()} " + + "er ugyldig i kombinasjon med behandlingstype '${behandling.type.visningsnavn}'." + throw FunksjonellFeil(frontendFeilmelding = feilmelding, melding = feilmelding) + } + if (behandling.opprettetÅrsak == BehandlingÅrsak.KLAGE && setOf( + Behandlingsresultat.AVSLÅTT_OG_OPPHØRT, + Behandlingsresultat.AVSLÅTT_ENDRET_OG_OPPHØRT, + Behandlingsresultat.AVSLÅTT_OG_ENDRET, + Behandlingsresultat.AVSLÅTT, + ).contains(resultat) + ) { + val feilmelding = "Behandlingsårsak ${behandling.opprettetÅrsak.visningsnavn.lowercase()} " + + "er ugyldig i kombinasjon med resultat '${resultat.displayName.lowercase()}'." + throw FunksjonellFeil(frontendFeilmelding = feilmelding, melding = feilmelding) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/AndelTilkjentYtelseMedEndreteUtbetalingerTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/AndelTilkjentYtelseMedEndreteUtbetalingerTidslinje.kt new file mode 100644 index 000000000..f3d4f6ea7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/AndelTilkjentYtelseMedEndreteUtbetalingerTidslinje.kt @@ -0,0 +1,22 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt + +class AndelTilkjentYtelseMedEndreteUtbetalingerTidslinje( + private val andelerTilkjentYtelse: List, +) : Tidslinje() { + + override fun lagPerioder(): List> { + return andelerTilkjentYtelse.map { + Periode( + fraOgMed = it.stønadFom.tilTidspunkt(), + tilOgMed = it.stønadTom.tilTidspunkt(), + innhold = it, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/AndelTilkjentYtelseTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/AndelTilkjentYtelseTidslinje.kt new file mode 100644 index 000000000..cafa9a04d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/AndelTilkjentYtelseTidslinje.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.AndelForVedtaksperiode + +class AndelTilkjentYtelseTidslinje( + private val andelerTilkjentYtelse: List, +) : Tidslinje() { + + override fun lagPerioder(): List> { + return andelerTilkjentYtelse.map { + Periode( + fraOgMed = it.stønadFom.tilTidspunkt(), + tilOgMed = it.stønadTom.tilTidspunkt(), + innhold = it, + ) + } + } +} + +class AndelTilkjentYtelseForVedtaksperioderTidslinje( + private val andelerTilkjentYtelse: List, +) : Tidslinje() { + override fun lagPerioder(): List> { + return andelerTilkjentYtelse.map { + Periode( + fraOgMed = it.stønadFom.tilTidspunkt(), + tilOgMed = it.stønadTom.tilTidspunkt(), + innhold = AndelForVedtaksperiode(it), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/AndelTilkjentYtelseTidslinjeUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/AndelTilkjentYtelseTidslinjeUtil.kt new file mode 100644 index 000000000..d108cf1d4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/AndelTilkjentYtelseTidslinjeUtil.kt @@ -0,0 +1,135 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.erTilogMed3ÅrTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.eøs.felles.util.MAX_MÅNED +import no.nav.familie.ba.sak.kjerne.eøs.felles.util.MIN_MÅNED +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.joinIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import java.math.BigDecimal +import java.time.YearMonth + +fun Iterable.tilSeparateTidslinjerForBarna(): Map> { + return this + .filter { !it.erSøkersAndel() } + .groupBy { it.aktør } + .mapValues { (_, andeler) -> tidslinje { andeler.map { it.tilPeriode() } } } +} + +fun Map>.tilAndelerTilkjentYtelse(): List { + return this.values.flatMap { it.tilAndelTilkjentYtelse() } +} + +fun Iterable>.tilAndelerTilkjentYtelse(): List { + return this.flatMap { it.tilAndelTilkjentYtelse() } +} + +fun Tidslinje.tilAndelTilkjentYtelse(): List { + return this + .perioder().map { + it.innhold?.medPeriode( + it.fraOgMed.tilYearMonth(), + it.tilOgMed.tilYearMonth(), + ) + }.filterNotNull() +} + +fun AndelTilkjentYtelse.tilPeriode() = Periode( + this.stønadFom.tilTidspunkt(), + this.stønadTom.tilTidspunkt(), + // Ta bort periode, slik at det ikke blir med på innholdet som vurderes for likhet + this.medPeriode(null, null), +) + +fun AndelTilkjentYtelse.medPeriode(fraOgMed: YearMonth?, tilOgMed: YearMonth?) = + copy( + id = 0, + stønadFom = fraOgMed ?: MIN_MÅNED, + stønadTom = tilOgMed ?: MAX_MÅNED, + ).also { versjon = this.versjon } + +/** + * Ivaretar fom og tom, slik at eventuelle splitter blir med videre. + */ +fun Iterable.tilTidslinjeForSøkersYtelse(ytelseType: YtelseType) = this + .filter { it.erSøkersAndel() } + .filter { it.type == ytelseType } + .let { + tidslinje { + it.map { Periode(it.stønadFom.tilTidspunkt(), it.stønadTom.tilTidspunkt(), it) } + } + } + +fun Map>.kunAndelerTilOgMed3År(barna: List): Map> { + val barnasErInntil3ÅrTidslinjer = barna.associate { it.aktør to erTilogMed3ÅrTidslinje(it.fødselsdato) } + + // For hvert barn kombiner andel-tidslinjen med 3-års-tidslinjen. Resultatet er andelene når barna er inntil 3 år + return this.joinIkkeNull(barnasErInntil3ÅrTidslinjer) { andel, _ -> andel } +} + +data class AndelTilkjentYtelseForTidslinje( + val aktør: Aktør, + val beløp: Int, + val sats: Int, + val ytelseType: YtelseType, + val prosent: BigDecimal, + val nasjonaltPeriodebeløp: Int = beløp, + val differanseberegnetPeriodebeløp: Int? = null, +) + +fun AndelTilkjentYtelse.tilpassTilTidslinje() = + AndelTilkjentYtelseForTidslinje( + aktør = this.aktør, + beløp = this.kalkulertUtbetalingsbeløp, + ytelseType = this.type, + sats = this.sats, + prosent = this.prosent, + nasjonaltPeriodebeløp = this.nasjonaltPeriodebeløp ?: this.kalkulertUtbetalingsbeløp, + differanseberegnetPeriodebeløp = this.differanseberegnetPeriodebeløp, + ) + +fun Tidslinje.tilAndelerTilkjentYtelse(tilkjentYtelse: TilkjentYtelse) = + perioder() + .filter { it.innhold != null } + .map { + AndelTilkjentYtelse( + behandlingId = tilkjentYtelse.behandling.id, + tilkjentYtelse = tilkjentYtelse, + aktør = it.innhold!!.aktør, + type = it.innhold.ytelseType, + kalkulertUtbetalingsbeløp = it.innhold.beløp, + nasjonaltPeriodebeløp = it.innhold.nasjonaltPeriodebeløp, + differanseberegnetPeriodebeløp = it.innhold.differanseberegnetPeriodebeløp, + sats = it.innhold.sats, + prosent = it.innhold.prosent, + stønadFom = it.fraOgMed.tilYearMonth(), + stønadTom = it.tilOgMed.tilYearMonth(), + ) + } + +/** + * Lager tidslinje med AndelTilkjentYtelseForTidslinje-objekter, som derfor er "trygg" mtp DB-endringer + */ +fun Iterable.tilTryggTidslinjeForSøkersYtelse(ytelseType: YtelseType) = this + .filter { it.erSøkersAndel() } + .filter { it.type == ytelseType } + .let { + tidslinje { + it.map { + Periode( + it.stønadFom.tilTidspunkt(), + it.stønadTom.tilTidspunkt(), + it.tilpassTilTidslinje(), + ) + } + } + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Beregning.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Beregning.kt new file mode 100644 index 000000000..f61a276d0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Beregning.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.fpsak.tidsserie.LocalDateSegment +import no.nav.fpsak.tidsserie.LocalDateTimeline +import no.nav.fpsak.tidsserie.StandardCombinators + +fun beregnUtbetalingsperioderUtenKlassifisering(andelerTilkjentYtelse: Collection): LocalDateTimeline { + return andelerTilkjentYtelse + .map { personTilTimeline(it) } + .reduce(::reducer) +} + +private fun personTilTimeline(it: AndelTilkjentYtelseMedEndreteUtbetalinger) = + LocalDateTimeline( + listOf( + LocalDateSegment( + it.stønadFom.førsteDagIInneværendeMåned(), + it.stønadTom.sisteDagIInneværendeMåned(), + it.kalkulertUtbetalingsbeløp, + ), + ), + ) + +private fun reducer( + sammenlagtTidslinje: LocalDateTimeline, + tidslinje: LocalDateTimeline, +): LocalDateTimeline { + sammenlagtTidslinje.disjoint(tidslinje) + return sammenlagtTidslinje.combine( + tidslinje, + StandardCombinators::sum, + LocalDateTimeline.JoinStyle.CROSS_JOIN, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningService.kt new file mode 100644 index 000000000..570096dfb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningService.kt @@ -0,0 +1,321 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.integrasjoner.økonomi.AndelTilkjentYtelseForSimuleringFactory +import no.nav.familie.ba.sak.integrasjoner.økonomi.AndelTilkjentYtelseForUtbetalingsoppdrag +import no.nav.familie.ba.sak.integrasjoner.økonomi.IdentOgYtelse +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.beregning.domene.EndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.forrigebehandling.EndringIUtbetalingUtil +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.barn +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.steg.EndringerIUtbetalingForBehandlingSteg +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.LocalDateTime + +@Service +class BeregningService( + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val fagsakService: FagsakService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + private val vilkårsvurderingRepository: VilkårsvurderingRepository, + private val behandlingRepository: BehandlingRepository, + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + private val småbarnstilleggService: SmåbarnstilleggService, + private val tilkjentYtelseEndretAbonnenter: List = emptyList(), + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + private val featureToggleService: FeatureToggleService, +) { + fun slettTilkjentYtelseForBehandling(behandlingId: Long) = + tilkjentYtelseRepository.findByBehandlingOptional(behandlingId) + ?.let { tilkjentYtelseRepository.delete(it) } + + fun hentLøpendeAndelerTilkjentYtelseMedUtbetalingerForBehandlinger( + behandlingIder: List, + avstemmingstidspunkt: LocalDateTime, + ): List = + andelTilkjentYtelseRepository.finnLøpendeAndelerTilkjentYtelseForBehandlinger( + behandlingIder, + avstemmingstidspunkt.toLocalDate().toYearMonth(), + ) + .filter { it.erAndelSomSkalSendesTilOppdrag() } + + fun hentAndelerTilkjentYtelseMedUtbetalingerForBehandling(behandlingId: Long): List = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId) + .filter { it.erAndelSomSkalSendesTilOppdrag() } + + fun hentAndelerTilkjentYtelseForBehandling(behandlingId: Long): List = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId) + + fun lagreTilkjentYtelseMedOppdaterteAndeler(tilkjentYtelse: TilkjentYtelse) = + tilkjentYtelseRepository.save(tilkjentYtelse) + + fun hentTilkjentYtelseForBehandling(behandlingId: Long) = + tilkjentYtelseRepository.findByBehandling(behandlingId) + + fun hentOptionalTilkjentYtelseForBehandling(behandlingId: Long) = + tilkjentYtelseRepository.findByBehandlingOptional(behandlingId) + + fun hentSisteAndelPerIdent(fagsakId: Long): Map { + return andelTilkjentYtelseRepository.hentSisteAndelPerIdentOgType(fagsakId) + .groupBy { IdentOgYtelse(it.aktør.aktivFødselsnummer(), it.type) } + .mapValues { AndelTilkjentYtelseForSimuleringFactory().pakkInnForUtbetaling(it.value).single() } + } + + /** + * Denne metoden henter alle relaterte behandlinger på en person. + * Per fagsak henter man tilkjent ytelse fra: + * 1. Behandling som er til godkjenning + * 2. Siste behandling som er vedtatt + * 3. Filtrer bort behandlinger der barnet ikke lenger finnes + */ + fun hentRelevanteTilkjentYtelserForBarn( + barnAktør: Aktør, + fagsakId: Long, + ): List { + val andreFagsaker = fagsakService.hentFagsakerPåPerson(barnAktør) + .filter { it.id != fagsakId } + + return andreFagsaker.mapNotNull { fagsak -> + val behandlingSomLiggerTilGodkjenning = behandlingRepository.finnBehandlingerSomLiggerTilGodkjenning( + fagsakId = fagsak.id, + ).singleOrNull() + + if (behandlingSomLiggerTilGodkjenning != null) { + behandlingSomLiggerTilGodkjenning + } else { + val godkjenteBehandlingerSomIkkeErIverksattEnda = + behandlingRepository.finnBehandlingerSomHolderPåÅIverksettes(fagsakId = fagsak.id).singleOrNull() + if (godkjenteBehandlingerSomIkkeErIverksattEnda != null) { + godkjenteBehandlingerSomIkkeErIverksattEnda + } else { + val sisteVedtatteBehandling = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = fagsak.id) + sisteVedtatteBehandling + } + } + }.map { + hentTilkjentYtelseForBehandling(behandlingId = it.id) + }.filter { + personopplysningGrunnlagRepository + .finnSøkerOgBarnAktørerTilAktiv(behandlingId = it.behandling.id) + .barn().map { barn -> barn.aktør } + .contains(barnAktør) + }.map { it } + } + + fun erEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(behandling: Behandling): Boolean = + hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(behandling) == EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING + + fun hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(behandling: Behandling): EndringerIUtbetalingForBehandlingSteg { + val endringerIUtbetaling = + hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomiTidslinje(behandling) + .perioder() + .any { it.innhold == true } + + return if (endringerIUtbetaling) EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING else EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING + } + + fun hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomiTidslinje(behandling: Behandling): Tidslinje { + val nåværendeAndeler = andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandling.id) + val forrigeAndeler = hentAndelerFraForrigeIverksattebehandling(behandling) + + if (nåværendeAndeler.isEmpty() && forrigeAndeler.isEmpty()) return TomTidslinje() + + return EndringIUtbetalingUtil.lagEndringIUtbetalingTidslinje( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + ) + } + + fun hentAndelerFraForrigeIverksattebehandling(behandling: Behandling): List { + val forrigeBehandling = behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(behandling) + return forrigeBehandling?.let { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(it.id) } + ?: emptyList() + } + + @Transactional + fun oppdaterBehandlingMedBeregning( + behandling: Behandling, + personopplysningGrunnlag: PersonopplysningGrunnlag, + nyEndretUtbetalingAndel: EndretUtbetalingAndel? = null, + ): TilkjentYtelse { + val endreteUtbetalingAndeler = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnEndreteUtbetalingerMedAndelerTilkjentYtelse(behandling.id).filter { + // Ved automatiske behandlinger ønsker vi alltid å ta vare på de gamle endrede andelene + if (behandling.skalBehandlesAutomatisk) { + true + } else if (nyEndretUtbetalingAndel != null) { + it.id == nyEndretUtbetalingAndel.id || it.andelerTilkjentYtelse.isNotEmpty() + } else { + it.andelerTilkjentYtelse.isNotEmpty() + } + } + + return genererOgLagreTilkjentYtelse( + behandling = behandling, + personopplysningGrunnlag = personopplysningGrunnlag, + endreteUtbetalingAndeler = endreteUtbetalingAndeler, + ) + } + + private fun genererOgLagreTilkjentYtelse( + behandling: Behandling, + personopplysningGrunnlag: PersonopplysningGrunnlag, + endreteUtbetalingAndeler: List, + ): TilkjentYtelse { + tilkjentYtelseRepository.slettTilkjentYtelseFor(behandling) + val vilkårsvurdering = vilkårsvurderingRepository.findByBehandlingAndAktiv(behandling.id) + ?: throw IllegalStateException("Kunne ikke hente vilkårsvurdering for behandling med id ${behandling.id}") + + val tilkjentYtelse = + TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + endretUtbetalingAndeler = endreteUtbetalingAndeler, + fagsakType = behandling.fagsak.type, + ) { søkerAktør -> + småbarnstilleggService.hentOgLagrePerioderMedOvergangsstønadForBehandling( + søkerAktør = søkerAktør, + behandling = behandling, + ) + + småbarnstilleggService.hentPerioderMedFullOvergangsstønad(behandling) + } + + val lagretTilkjentYtelse = tilkjentYtelseRepository.save(tilkjentYtelse) + tilkjentYtelseEndretAbonnenter.forEach { it.endretTilkjentYtelse(lagretTilkjentYtelse) } + return lagretTilkjentYtelse + } + + // For at endret utbetaling andeler skal fungere så må man generere andeler før man kobler endringene på andelene +// Dette er fordi en endring regnes som gyldig når den overlapper med en andel og har gyldig årsak +// Hvis man ikke genererer andeler før man kobler på endringene så vil ingen av endringene ses på som gyldige, altså ikke oppdatere noen andeler + fun genererTilkjentYtelseFraVilkårsvurdering( + behandling: Behandling, + personopplysningGrunnlag: PersonopplysningGrunnlag, + ): TilkjentYtelse { + // 1: Genererer andeler fra vilkårsvurderingen uten å ta hensyn til endret utbetaling andeler + genererOgLagreTilkjentYtelse( + behandling = behandling, + personopplysningGrunnlag = personopplysningGrunnlag, + endreteUtbetalingAndeler = emptyList(), + ) + + // 2: Genererer andeler som også tar hensyn til endret utbetaling andeler + return oppdaterBehandlingMedBeregning(behandling, personopplysningGrunnlag) + } + + fun oppdaterTilkjentYtelseMedUtbetalingsoppdrag( + behandling: Behandling, + utbetalingsoppdrag: Utbetalingsoppdrag, + ): TilkjentYtelse { + val nyTilkjentYtelse = populerTilkjentYtelse(behandling, utbetalingsoppdrag) + return tilkjentYtelseRepository.save(nyTilkjentYtelse) + } + + fun kanAutomatiskIverksetteSmåbarnstilleggEndring( + behandling: Behandling, + sistIverksatteBehandling: Behandling?, + ): Boolean { + if (!behandling.skalBehandlesAutomatisk || !behandling.erSmåbarnstillegg()) return false + + val forrigeSmåbarnstilleggAndeler = + if (sistIverksatteBehandling == null) { + emptyList() + } else { + hentAndelerTilkjentYtelseMedUtbetalingerForBehandling( + behandlingId = sistIverksatteBehandling.id, + ).filter { it.erSmåbarnstillegg() } + } + + val nyeSmåbarnstilleggAndeler = + if (sistIverksatteBehandling == null) { + emptyList() + } else { + hentAndelerTilkjentYtelseMedUtbetalingerForBehandling( + behandlingId = behandling.id, + ).filter { it.erSmåbarnstillegg() } + } + + val (innvilgedeMånedPerioder, reduserteMånedPerioder) = hentInnvilgedeOgReduserteAndelerSmåbarnstillegg( + forrigeSmåbarnstilleggAndeler = forrigeSmåbarnstilleggAndeler, + nyeSmåbarnstilleggAndeler = nyeSmåbarnstilleggAndeler, + ) + + return kanAutomatiskIverksetteSmåbarnstillegg( + innvilgedeMånedPerioder = innvilgedeMånedPerioder, + reduserteMånedPerioder = reduserteMånedPerioder, + ) + } + + /** + * Henter alle barn på behandlingen som har minst en periode med tilkjentytelse. + */ + fun finnBarnFraBehandlingMedTilkjentYtelse(behandlingId: Long): List { + val andelerTilkjentYtelse = andelTilkjentYtelseRepository + .finnAndelerTilkjentYtelseForBehandling(behandlingId) + + return personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId)?.barna?.map { it.aktør } + ?.filter { + andelerTilkjentYtelse.any { aty -> aty.aktør == it } + } ?: emptyList() + } + + fun populerTilkjentYtelse( + behandling: Behandling, + utbetalingsoppdrag: Utbetalingsoppdrag, + ): TilkjentYtelse { + val erRentOpphør = + utbetalingsoppdrag.utbetalingsperiode.isNotEmpty() && utbetalingsoppdrag.utbetalingsperiode.all { it.opphør != null } + var opphørsdato: LocalDate? = null + if (erRentOpphør) { + opphørsdato = utbetalingsoppdrag.utbetalingsperiode.minOf { it.opphør!!.opphørDatoFom } + } + + if (behandling.type == BehandlingType.REVURDERING) { + val opphørPåRevurdering = utbetalingsoppdrag.utbetalingsperiode.filter { it.opphør != null } + if (opphørPåRevurdering.isNotEmpty()) { + opphørsdato = opphørPåRevurdering.maxByOrNull { it.opphør!!.opphørDatoFom }!!.opphør!!.opphørDatoFom + } + } + + val tilkjentYtelse = + tilkjentYtelseRepository.findByBehandling(behandling.id) + + return tilkjentYtelse.apply { + this.utbetalingsoppdrag = objectMapper.writeValueAsString(utbetalingsoppdrag) + this.stønadTom = tilkjentYtelse.andelerTilkjentYtelse.maxOfOrNull { it.stønadTom } + this.stønadFom = + if (erRentOpphør) null else tilkjentYtelse.andelerTilkjentYtelse.minOfOrNull { it.stønadFom } + this.endretDato = LocalDate.now() + this.opphørFom = opphørsdato?.toYearMonth() + } + } +} + +interface TilkjentYtelseEndretAbonnent { + fun endretTilkjentYtelse(tilkjentYtelse: TilkjentYtelse) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/EndretUtbetalingAndelTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/EndretUtbetalingAndelTidslinje.kt new file mode 100644 index 000000000..7701bba3f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/EndretUtbetalingAndelTidslinje.kt @@ -0,0 +1,23 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt + +class EndretUtbetalingAndelTidslinje( + private val endretUtbetalingAndeler: List, +) : Tidslinje() { + + override fun lagPerioder(): Collection> { + return endretUtbetalingAndeler.map { + Periode( + fraOgMed = it.fom?.tilTidspunkt() ?: throw Feil("Endret utbetaling andel har ingen fom-dato: $it"), + tilOgMed = it.tom?.tilTidspunkt() ?: throw Feil("Endret utbetaling andel har ingen tom-dato: $it"), + innhold = it, + ) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Ordin\303\246rBarnetrygdUtil.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Ordin\303\246rBarnetrygdUtil.kt" new file mode 100644 index 000000000..c406ef458 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Ordin\303\246rBarnetrygdUtil.kt" @@ -0,0 +1,123 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.Utils.avrundetHeltallAvProsent +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombiner +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.slåSammenLike +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.alleOrdinæreVilkårErOppfylt +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.tilForskjøvetTidslinjerForHvertOppfylteVilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import java.math.BigDecimal + +object OrdinærBarnetrygdUtil { + + internal fun beregnAndelerTilkjentYtelseForBarna( + personopplysningGrunnlag: PersonopplysningGrunnlag, + personResultater: Set, + fagsakType: FagsakType, + ): List { + val tidslinjerMedRettTilProsentPerBarn = + personResultater.lagTidslinjerMedRettTilProsentPerBarn(personopplysningGrunnlag, fagsakType) + + return tidslinjerMedRettTilProsentPerBarn.flatMap { (barn, tidslinjeMedRettTilProsentForBarn) -> + val satsTidslinje = lagOrdinærTidslinje(barn) + val satsProsentTidslinje = kombinerProsentOgSatsTidslinjer(tidslinjeMedRettTilProsentForBarn, satsTidslinje) + + satsProsentTidslinje.perioder().map { + val innholdIPeriode = it.innhold + ?: throw Feil("Finner ikke sats og prosent i periode (${it.fraOgMed} - ${it.tilOgMed}) ved generering av andeler tilkjent ytelse") + BeregnetAndel( + person = barn, + stønadFom = it.fraOgMed.tilYearMonth(), + stønadTom = it.tilOgMed.tilYearMonth(), + beløp = innholdIPeriode.sats.avrundetHeltallAvProsent(innholdIPeriode.prosent), + sats = innholdIPeriode.sats, + prosent = innholdIPeriode.prosent, + ) + } + } + } + + private fun kombinerProsentOgSatsTidslinjer( + tidslinjeMedRettTilProsentForBarn: Tidslinje, + satsTidslinje: Tidslinje, + ) = tidslinjeMedRettTilProsentForBarn.kombinerMed(satsTidslinje) { rettTilProsent, sats -> + when { + rettTilProsent == null -> null + sats == null -> throw Feil("Finner ikke sats i periode med rett til utbetaling") + else -> SatsProsent(sats, rettTilProsent) + } + }.slåSammenLike().filtrerIkkeNull() + + private data class SatsProsent( + val sats: Int, + val prosent: BigDecimal, + ) + + private fun Set.lagTidslinjerMedRettTilProsentPerBarn(personopplysningGrunnlag: PersonopplysningGrunnlag, fagsakType: FagsakType): Map> { + val tidslinjerPerPerson = lagTidslinjerMedRettTilProsentPerPerson(personopplysningGrunnlag, fagsakType) + + if (tidslinjerPerPerson.isEmpty()) return emptyMap() + + val søkerTidslinje = tidslinjerPerPerson[personopplysningGrunnlag.søker] ?: return emptyMap() + val barnasTidslinjer = tidslinjerPerPerson.filter { it.key in personopplysningGrunnlag.barna } + + return kombinerSøkerMedHvertBarnSinTidslinje(barnasTidslinjer, søkerTidslinje) + } + + private fun kombinerSøkerMedHvertBarnSinTidslinje( + barnasTidslinjer: Map>, + søkerTidslinje: Tidslinje, + ) = barnasTidslinjer.mapValues { (_, barnTidslinje) -> + barnTidslinje.kombinerMed(søkerTidslinje) { barnProsent, søkerProsent -> + when { + barnProsent == null || søkerProsent == null -> null + else -> barnProsent + } + }.slåSammenLike().filtrerIkkeNull() + } + + private fun Set.lagTidslinjerMedRettTilProsentPerPerson( + personopplysningGrunnlag: PersonopplysningGrunnlag, + fagsakType: FagsakType, + ) = this.associate { personResultat -> + val person = personopplysningGrunnlag.personer.find { it.aktør == personResultat.aktør } + ?: throw Feil("Finner ikke person med aktørId=${personResultat.aktør.aktørId} i persongrunnlaget ved generering av andeler tilkjent ytelse") + person to personResultat.tilTidslinjeMedRettTilProsentForPerson( + person = person, + fagsakType = fagsakType, + ) + } + + internal fun PersonResultat.tilTidslinjeMedRettTilProsentForPerson( + person: Person, + fagsakType: FagsakType, + ): Tidslinje { + val tidslinjer = vilkårResultater.tilForskjøvetTidslinjerForHvertOppfylteVilkår(person.fødselsdato) + + return tidslinjer.kombiner { it.mapTilProsentEllerNull(person.type, fagsakType) }.slåSammenLike().filtrerIkkeNull() + } + + internal fun Iterable.mapTilProsentEllerNull(personType: PersonType, fagsakType: FagsakType): BigDecimal? { + return if (alleOrdinæreVilkårErOppfylt(personType, fagsakType)) { + if (any { it.utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.DELT_BOSTED) }) { + BigDecimal(50) + } else { + BigDecimal(100) + } + } else { + null + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/SatsService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/SatsService.kt new file mode 100644 index 000000000..e1bd3857e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/SatsService.kt @@ -0,0 +1,123 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.erUnder6ÅrTidslinje +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.isBetween +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.domene.Sats +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunktEllerUendeligSent +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunktEllerUendeligTidlig +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.beskjær +import java.time.LocalDate +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode as TidslinjePeriode + +object SatsTidspunkt { + val senesteSatsTidspunkt: LocalDate = LocalDate.MAX +} + +object SatsService { + + private val satser = listOf( + Sats(SatsType.ORBA, 970, LocalDate.MIN, LocalDate.of(2019, 2, 28)), + Sats(SatsType.ORBA, 1054, LocalDate.of(2019, 3, 1), LocalDate.of(2023, 2, 28)), + Sats(SatsType.ORBA, 1083, LocalDate.of(2023, 3, 1), LocalDate.of(2023, 6, 30)), + Sats(SatsType.ORBA, 1310, LocalDate.of(2023, 7, 1), LocalDate.MAX), + + Sats(SatsType.SMA, 660, LocalDate.MIN, LocalDate.of(2023, 2, 28)), + Sats(SatsType.SMA, 678, LocalDate.of(2023, 3, 1), LocalDate.of(2023, 6, 30)), + Sats(SatsType.SMA, 696, LocalDate.of(2023, 7, 1), LocalDate.MAX), + + Sats(SatsType.TILLEGG_ORBA, 970, LocalDate.MIN, LocalDate.of(2019, 2, 28)), + Sats(SatsType.TILLEGG_ORBA, 1054, LocalDate.of(2019, 3, 1), LocalDate.of(2020, 8, 31)), + Sats(SatsType.TILLEGG_ORBA, 1354, LocalDate.of(2020, 9, 1), LocalDate.of(2021, 8, 31)), + Sats(SatsType.TILLEGG_ORBA, 1654, LocalDate.of(2021, 9, 1), LocalDate.of(2021, 12, 31)), + Sats(SatsType.TILLEGG_ORBA, 1676, LocalDate.of(2022, 1, 1), LocalDate.of(2023, 2, 28)), + Sats(SatsType.TILLEGG_ORBA, 1723, LocalDate.of(2023, 3, 1), LocalDate.of(2023, 6, 30)), + Sats(SatsType.TILLEGG_ORBA, 1766, LocalDate.of(2023, 7, 1), LocalDate.MAX), + + Sats(SatsType.FINN_SVAL, 1054, LocalDate.MIN, LocalDate.of(2014, 3, 31)), + + Sats(SatsType.UTVIDET_BARNETRYGD, 970, LocalDate.MIN, LocalDate.of(2019, 2, 28)), + Sats(SatsType.UTVIDET_BARNETRYGD, 1054, LocalDate.of(2019, 3, 1), LocalDate.of(2023, 2, 28)), + Sats(SatsType.UTVIDET_BARNETRYGD, 2489, LocalDate.of(2023, 3, 1), LocalDate.of(2023, 6, 30)), + Sats(SatsType.UTVIDET_BARNETRYGD, 2516, LocalDate.of(2023, 7, 1), LocalDate.MAX), + ) + + fun finnSisteSatsFor(satstype: SatsType) = finnAlleSatserFor(satstype).maxBy { it.gyldigTom } + + fun finnGjeldendeSatsForDato(satstype: SatsType, dato: LocalDate): Int { + val gjeldendeSatsForPeriode = + satser.find { it.type == satstype && dato.isBetween(Periode(it.gyldigFom, it.gyldigTom)) } + ?: throw Feil("Finnes ingen sats for SatsType: $satstype for dato: $dato") + return gjeldendeSatsForPeriode.beløp + } + + fun finnSisteSatsendringsDato(): LocalDate = hentAllesatser().maxBy { it.gyldigFom }.gyldigFom + + fun finnSatsendring(startDato: LocalDate): List = hentAllesatser() + .filter { it.gyldigFom == startDato } + .filter { it.gyldigFom != LocalDate.MIN } + + /** + * SatsService.senesteSatsTidspunkt brukes for å mocke inn et tidspunkt som ligger tidligere enn gjeldende satser + * alle satser som er gyldige fra etter dette tidspunktet vil filtreres bort + * gyldigTom vil settes til LocalDate.MAX for det som nå blir siste gyldige sats, dvs varer uendelig + */ + internal fun hentAllesatser() = satser + .filter { it.gyldigFom <= SatsTidspunkt.senesteSatsTidspunkt } + .map { + val overstyrtTom = if (SatsTidspunkt.senesteSatsTidspunkt < it.gyldigTom) LocalDate.MAX else it.gyldigTom + it.copy(gyldigTom = overstyrtTom) + } + + fun finnAlleSatserFor(type: SatsType): List = hentAllesatser().filter { it.type == type } + + fun hentDatoForSatsendring( + satstype: SatsType, + oppdatertBeløp: Int, + ): LocalDate? = hentAllesatser().find { it.type == satstype && it.beløp == oppdatertBeløp }?.gyldigFom +} + +fun fomErPåSatsendring(fom: LocalDate?): Boolean = + SatsService + .finnSatsendring(fom?.førsteDagIInneværendeMåned() ?: TIDENES_MORGEN) + .isNotEmpty() + +fun satstypeTidslinje(satsType: SatsType) = + tidslinje { + SatsService.finnAlleSatserFor(satsType) + .map { + val fom = if (it.gyldigFom == LocalDate.MIN) null else it.gyldigFom.toYearMonth() + val tom = if (it.gyldigTom == LocalDate.MAX) null else it.gyldigTom.toYearMonth() + TidslinjePeriode( + fraOgMed = fom.tilTidspunktEllerUendeligTidlig(tom), + tilOgMed = tom.tilTidspunktEllerUendeligSent(fom), + it.beløp, + ) + } + } + +fun lagOrdinærTidslinje(barn: Person): Tidslinje { + val orbaTidslinje = satstypeTidslinje(SatsType.ORBA) + val tilleggOrbaTidslinje = satstypeTidslinje(SatsType.TILLEGG_ORBA).filtrerMed(erUnder6ÅrTidslinje(barn)) + return orbaTidslinje + .kombinerMed(tilleggOrbaTidslinje) { orba, tillegg -> tillegg ?: orba } + .klippBortPerioderFørBarnetBleFødt(fødselsdato = barn.fødselsdato) +} + +private fun Tidslinje.klippBortPerioderFørBarnetBleFødt(fødselsdato: LocalDate) = this.beskjær( + fraOgMed = fødselsdato.tilMånedTidspunkt(), + tilOgMed = MånedTidspunkt.uendeligLengeTil(fødselsdato.toYearMonth()), +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggBarnetrygdGenerator.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggBarnetrygdGenerator.kt" new file mode 100644 index 000000000..41b467b0f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggBarnetrygdGenerator.kt" @@ -0,0 +1,72 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.Utils.avrundetHeltallAvProsent +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønadTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNullMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import java.time.LocalDate + +data class SmåbarnstilleggBarnetrygdGenerator( + val behandlingId: Long, + val tilkjentYtelse: TilkjentYtelse, +) { + + fun lagSmåbarnstilleggAndeler( + perioderMedFullOvergangsstønad: List, + utvidetAndeler: List, + barnasAndeler: List, + barnasAktørerOgFødselsdatoer: List>, + ): List { + if (perioderMedFullOvergangsstønad.isEmpty() || utvidetAndeler.isEmpty() || barnasAndeler.isEmpty()) return emptyList() + + validerUtvidetOgBarnasAndeler(utvidetAndeler = utvidetAndeler, barnasAndeler = barnasAndeler) + + val søkerAktør = utvidetAndeler.first().aktør + + val perioderMedFullOvergangsstønadTidslinje = + InternPeriodeOvergangsstønadTidslinje(perioderMedFullOvergangsstønad) + + val utvidetBarnetrygdTidslinje = AndelTilkjentYtelseMedEndreteUtbetalingerTidslinje(andelerTilkjentYtelse = utvidetAndeler) + + val barnSomGirRettTilSmåbarnstilleggTidslinje = lagTidslinjeForPerioderMedBarnSomGirRettTilSmåbarnstillegg( + barnasAndeler = barnasAndeler, + barnasAktørerOgFødselsdatoer = barnasAktørerOgFødselsdatoer, + ) + + val kombinertProsentTidslinje = kombinerAlleTidslinjerTilProsentTidslinje( + perioderMedFullOvergangsstønadTidslinje, + utvidetBarnetrygdTidslinje, + barnSomGirRettTilSmåbarnstilleggTidslinje, + ) + + return kombinertProsentTidslinje.filtrerIkkeNull().lagSmåbarnstilleggAndeler( + søkerAktør = søkerAktør, + ) + } + + private fun Tidslinje.lagSmåbarnstilleggAndeler( + søkerAktør: Aktør, + ): List { + return this.kombinerUtenNullMed(satstypeTidslinje(SatsType.SMA)) { småbarnstilleggPeriode, sats -> + val prosentIPeriode = småbarnstilleggPeriode.prosent + val beløpIPeriode = sats.avrundetHeltallAvProsent(prosent = prosentIPeriode) + + AndelTilkjentYtelseForTidslinje( + aktør = søkerAktør, + beløp = beløpIPeriode, + ytelseType = YtelseType.SMÅBARNSTILLEGG, + sats = sats, + prosent = prosentIPeriode, + ) + }.tilAndelerTilkjentYtelse(tilkjentYtelse) + .map { AndelTilkjentYtelseMedEndreteUtbetalinger.utenEndringer(it) } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggService.kt" new file mode 100644 index 000000000..d76ac8181 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggService.kt" @@ -0,0 +1,157 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.ef.EfSakRestClient +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.slåSammenTidligerePerioder +import no.nav.familie.ba.sak.kjerne.beregning.domene.tilInternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.småbarnstillegg.PeriodeOvergangsstønadGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.småbarnstillegg.PeriodeOvergangsstønadGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.småbarnstillegg.tilPeriodeOvergangsstønadGrunnlag +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.kontrakter.felles.ef.EksternPeriode +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate + +@Service +class SmåbarnstilleggService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val efSakRestClient: EfSakRestClient, + private val periodeOvergangsstønadGrunnlagRepository: PeriodeOvergangsstønadGrunnlagRepository, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + private val persongrunnlagService: PersongrunnlagService, + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, +) { + + @Transactional + fun hentOgLagrePerioderMedOvergangsstønadForBehandling( + søkerAktør: Aktør, + behandling: Behandling, + ) { + if (behandling.erSatsendring()) { + kopierPerioderMedOvergangsstønadFraForrigeBehandling( + behandling, + ) + } else { + hentOgLagrePerioderMedFullOvergangsstønadFraEf( + søkerAktør = søkerAktør, + behandlingId = behandling.id, + ) + } + } + + private fun hentOgLagrePerioderMedFullOvergangsstønadFraEf( + søkerAktør: Aktør, + behandlingId: Long, + ) { + val periodeOvergangsstønad = hentPerioderMedFullOvergangsstønad(aktør = søkerAktør) + + periodeOvergangsstønadGrunnlagRepository.deleteByBehandlingId(behandlingId = behandlingId) + + periodeOvergangsstønadGrunnlagRepository.saveAll( + periodeOvergangsstønad.map { + it.tilPeriodeOvergangsstønadGrunnlag( + behandlingId = behandlingId, + aktør = søkerAktør, + ) + }, + ) + } + + private fun kopierPerioderMedOvergangsstønadFraForrigeBehandling( + inneværendeBehandling: Behandling, + ) { + val perioderFraForrigeBehandling = + hentPerioderMedOvergangsstønadFraForrigeVedtatteBehandling(behandling = inneværendeBehandling) + + periodeOvergangsstønadGrunnlagRepository.deleteByBehandlingId(behandlingId = inneværendeBehandling.id) + + periodeOvergangsstønadGrunnlagRepository.saveAll( + perioderFraForrigeBehandling.map { + PeriodeOvergangsstønadGrunnlag( + behandlingId = inneværendeBehandling.id, + aktør = it.aktør, + fom = it.fom, + tom = it.tom, + datakilde = it.datakilde, + ) + }, + ) + } + + fun hentPerioderMedFullOvergangsstønad( + behandling: Behandling, + ): List { + val dagensDato = LocalDate.now() + + val perioderOvergangsstønad = periodeOvergangsstønadGrunnlagRepository.findByBehandlingId(behandlingId = behandling.id).map { it.tilInternPeriodeOvergangsstønad() } + val overgangsstønadPerioderFraForrigeBehandling = + hentPerioderMedOvergangsstønadFraForrigeVedtatteBehandling(behandling).map { it.tilInternPeriodeOvergangsstønad() }.slåSammenTidligerePerioder(dagensDato) + + return perioderOvergangsstønad.splittOgSlåSammen(overgangsstønadPerioderFraForrigeBehandling, dagensDato) + } + + private fun hentPerioderMedOvergangsstønadFraForrigeVedtatteBehandling(behandling: Behandling): List { + val forrigeVedtatteBehandling = + behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling = behandling) + + return if (forrigeVedtatteBehandling != null) { + periodeOvergangsstønadGrunnlagRepository.findByBehandlingId( + behandlingId = forrigeVedtatteBehandling.id, + ) + } else { + emptyList() + } + } + + fun vedtakOmOvergangsstønadPåvirkerFagsak(fagsak: Fagsak): Boolean { + val sistIverksatteBehandling = + behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId = fagsak.id) + ?: return false + + val tilkjentYtelseFraSistIverksatteBehandling = + tilkjentYtelseRepository.findByBehandling(behandlingId = sistIverksatteBehandling.id) + + val persongrunnlagFraSistIverksatteBehandling = + persongrunnlagService.hentAktivThrows(behandlingId = sistIverksatteBehandling.id) + + val dagensDato = LocalDate.now() + + val nyePerioderMedFullOvergangsstønad = + hentPerioderMedFullOvergangsstønad(aktør = fagsak.aktør).map { it.tilInternPeriodeOvergangsstønad() } + .slåSammenTidligerePerioder(dagensDato) + + val andelerMedEndringerFraSistIverksatteBehandling = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnAndelerTilkjentYtelseMedEndreteUtbetalinger(sistIverksatteBehandling.id) + + secureLogger.info("Perioder med overgangsstønad fra EF: ${nyePerioderMedFullOvergangsstønad.map { "Periode(fom=${it.fomDato}, tom=${it.tomDato})" }}") + + return vedtakOmOvergangsstønadPåvirkerFagsak( + småbarnstilleggBarnetrygdGenerator = SmåbarnstilleggBarnetrygdGenerator( + behandlingId = sistIverksatteBehandling.id, + tilkjentYtelse = tilkjentYtelseFraSistIverksatteBehandling, + ), + nyePerioderMedFullOvergangsstønad = nyePerioderMedFullOvergangsstønad, + forrigeAndelerTilkjentYtelse = andelerMedEndringerFraSistIverksatteBehandling, + barnasAktørerOgFødselsdatoer = persongrunnlagFraSistIverksatteBehandling.barna.map { + Pair( + it.aktør, + it.fødselsdato, + ) + }, + ) + } + + private fun hentPerioderMedFullOvergangsstønad(aktør: Aktør): List { + return efSakRestClient.hentPerioderMedFullOvergangsstønad( + aktør.aktivFødselsnummer(), + ).perioder + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggUtils.kt" new file mode 100644 index 000000000..3c5222d50 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggUtils.kt" @@ -0,0 +1,291 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.erTilogMed3ÅrTidslinje +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønadTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.domene.slåSammenTidligerePerioder +import no.nav.familie.ba.sak.kjerne.beregning.domene.splitFramtidigePerioderFraForrigeBehandling +import no.nav.familie.ba.sak.kjerne.forrigebehandling.EndringIUtbetalingUtil +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNull +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.beskjærEtter +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.tilMåned +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.Vedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.fpsak.tidsserie.LocalDateSegment +import no.nav.fpsak.tidsserie.LocalDateTimeline +import org.springframework.http.HttpStatus +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +fun List.splittOgSlåSammen( + overgangsstønadPerioderFraForrigeBehandling: List, + dagensDato: LocalDate, +) = this + .slåSammenTidligerePerioder(dagensDato) + .splitFramtidigePerioderFraForrigeBehandling(overgangsstønadPerioderFraForrigeBehandling, LocalDate.now()) + +class VedtaksperiodefinnerSmåbarnstilleggFeil( + melding: String, + override val frontendFeilmelding: String? = null, + override val httpStatus: HttpStatus = HttpStatus.OK, + override val throwable: Throwable? = null, +) : Feil( + melding, + frontendFeilmelding, + httpStatus, + throwable, +) + +fun vedtakOmOvergangsstønadPåvirkerFagsak( + småbarnstilleggBarnetrygdGenerator: SmåbarnstilleggBarnetrygdGenerator, + nyePerioderMedFullOvergangsstønad: List, + forrigeAndelerTilkjentYtelse: List, + barnasAktørerOgFødselsdatoer: List>, +): Boolean { + val (forrigeSmåbarnstilleggAndeler, forrigeAndelerIkkeSmåbarnstillegg) = forrigeAndelerTilkjentYtelse.partition { it.erSmåbarnstillegg() } + + val (forrigeUtvidetAndeler, forrigeBarnasAndeler) = forrigeAndelerIkkeSmåbarnstillegg.partition { it.erUtvidet() } + + val nyeSmåbarnstilleggAndeler = småbarnstilleggBarnetrygdGenerator.lagSmåbarnstilleggAndeler( + perioderMedFullOvergangsstønad = nyePerioderMedFullOvergangsstønad, + barnasAndeler = forrigeBarnasAndeler, + utvidetAndeler = forrigeUtvidetAndeler, + barnasAktørerOgFødselsdatoer = barnasAktørerOgFødselsdatoer, + ) + + return nyeSmåbarnstilleggAndeler.førerTilEndringIUtbetalingFraForrigeBehandling( + forrigeAndeler = forrigeSmåbarnstilleggAndeler, + ) +} + +private fun List.førerTilEndringIUtbetalingFraForrigeBehandling( + forrigeAndeler: List, +): Boolean { + val endringstidslinje = EndringIUtbetalingUtil.lagEndringIUtbetalingTidslinje( + nåværendeAndeler = this.map { it.andel }, + forrigeAndeler = forrigeAndeler.map { it.andel }, + ) + + return endringstidslinje.perioder().any { it.innhold == true } +} + +fun hentInnvilgedeOgReduserteAndelerSmåbarnstillegg( + forrigeSmåbarnstilleggAndeler: List, + nyeSmåbarnstilleggAndeler: List, +): Pair, List> { + val forrigeAndelerTidslinje = LocalDateTimeline( + forrigeSmåbarnstilleggAndeler.map { + LocalDateSegment( + it.stønadFom.førsteDagIInneværendeMåned(), + it.stønadTom.sisteDagIInneværendeMåned(), + it, + ) + }, + ) + val andelerTidslinje = LocalDateTimeline( + nyeSmåbarnstilleggAndeler.map { + LocalDateSegment( + it.stønadFom.førsteDagIInneværendeMåned(), + it.stønadTom.sisteDagIInneværendeMåned(), + it, + ) + }, + ) + + val segmenterLagtTil = andelerTidslinje.disjoint(forrigeAndelerTidslinje) + val segmenterFjernet = forrigeAndelerTidslinje.disjoint(andelerTidslinje) + + return Pair( + segmenterLagtTil.toSegments().map { MånedPeriode(fom = it.fom.toYearMonth(), tom = it.tom.toYearMonth()) }, + segmenterFjernet.toSegments().map { MånedPeriode(fom = it.fom.toYearMonth(), tom = it.tom.toYearMonth()) }, + ) +} + +fun kanAutomatiskIverksetteSmåbarnstillegg( + innvilgedeMånedPerioder: List, + reduserteMånedPerioder: List, +): Boolean { + // Kan ikke automatisk innvilge perioder mer enn en måned frem i tid + if ((innvilgedeMånedPerioder + reduserteMånedPerioder).any { + it.fom.isAfter( + YearMonth.now().nesteMåned(), + ) + } + ) { + return false + } + + return innvilgedeMånedPerioder.all { + it.fom.isSameOrAfter( + YearMonth.now(), + ) + } && reduserteMånedPerioder.all { + it.fom.isSameOrAfter( + YearMonth.now(), + ) + } +} + +@Throws(VedtaksperiodefinnerSmåbarnstilleggFeil::class) +fun finnAktuellVedtaksperiodeOgLeggTilSmåbarnstilleggbegrunnelse( + innvilgetMånedPeriode: MånedPeriode?, + redusertMånedPeriode: MånedPeriode?, + vedtaksperioderMedBegrunnelser: List, +): VedtaksperiodeMedBegrunnelser { + val vedtaksperiodeSomSkalOppdateresOgBegrunnelse: Pair? = + when { + innvilgetMånedPeriode == null && redusertMånedPeriode == null -> null + innvilgetMånedPeriode != null && redusertMånedPeriode == null -> { + Pair( + vedtaksperioderMedBegrunnelser.find { it.fom?.toYearMonth() == innvilgetMånedPeriode.fom && it.type == Vedtaksperiodetype.UTBETALING }, + Standardbegrunnelse.INNVILGET_SMÅBARNSTILLEGG, + ) + } + + innvilgetMånedPeriode == null && redusertMånedPeriode != null -> { + Pair( + vedtaksperioderMedBegrunnelser.find { it.fom?.toYearMonth() == redusertMånedPeriode.fom && it.type == Vedtaksperiodetype.UTBETALING }, + Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_FULL_OVERGANGSSTØNAD, + ) + } + + else -> null + } + + val vedtaksperiodeSomSkalOppdateres = vedtaksperiodeSomSkalOppdateresOgBegrunnelse?.first + if (vedtaksperiodeSomSkalOppdateres == null) { + secureLogger.info( + "Finner ikke aktuell periode å begrunne ved autovedtak småbarnstillegg.\n" + + "Innvilget periode: $innvilgetMånedPeriode.\n" + + "Redusert periode: $redusertMånedPeriode.\n" + + "Perioder: ${vedtaksperioderMedBegrunnelser.map { "Periode(type=${it.type}, fom=${it.fom}, tom=${it.tom})" }}", + ) + + throw VedtaksperiodefinnerSmåbarnstilleggFeil("Finner ikke aktuell periode å begrunne ved autovedtak småbarnstillegg. Se securelogger for å periodene som ble generert.") + } + + vedtaksperiodeSomSkalOppdateres.settBegrunnelser( + vedtaksperiodeSomSkalOppdateres.begrunnelser.toList() + listOf( + Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = vedtaksperiodeSomSkalOppdateres, + standardbegrunnelse = vedtaksperiodeSomSkalOppdateresOgBegrunnelse.second, + ), + ), + ) + + return vedtaksperiodeSomSkalOppdateres +} + +fun kombinerBarnasTidslinjerTilUnder3ÅrResultat( + alleAndelerForBarnUnder3År: Iterable, +): BarnSinRettTilSmåbarnstillegg? { + val høyesteProsentIPeriode = alleAndelerForBarnUnder3År.maxOfOrNull { it.prosent } + + return when { + høyesteProsentIPeriode == null -> null + høyesteProsentIPeriode > BigDecimal.ZERO -> BarnSinRettTilSmåbarnstillegg.UNDER_3_ÅR_UTBETALING + høyesteProsentIPeriode == BigDecimal.ZERO -> BarnSinRettTilSmåbarnstillegg.UNDER_3_ÅR_NULLUTBETALING + else -> throw Feil("Høyeste prosent for barna i perioden er et negativt tall.") + } +} + +fun lagTidslinjeForPerioderMedBarnSomGirRettTilSmåbarnstillegg( + barnasAndeler: List, + barnasAktørerOgFødselsdatoer: List>, +): Tidslinje { + val barnasAndelerTidslinjer = + barnasAndeler.groupBy { it.aktør }.mapValues { AndelTilkjentYtelseMedEndreteUtbetalingerTidslinje(it.value) } + + val barnasAndelerUnder3ÅrTidslinje = barnasAndelerTidslinjer.map { (barnAktør, barnTidslinje) -> + val barnetsFødselsdato = barnasAktørerOgFødselsdatoer.find { it.first == barnAktør }?.second + ?: throw Feil("Kan ikke beregne småbarnstillegg for et barn som ikke har fødselsdato.") + + val erTilOgMed3ÅrTidslinje = erTilogMed3ÅrTidslinje(barnetsFødselsdato) + + barnTidslinje.beskjærEtter(erTilOgMed3ÅrTidslinje) + } + + return barnasAndelerUnder3ÅrTidslinje.kombinerUtenNull { kombinerBarnasTidslinjerTilUnder3ÅrResultat(it) } + .filtrerIkkeNull() +} + +data class SmåbarnstilleggPeriode( + val overgangsstønadPeriode: InternPeriodeOvergangsstønad, + val prosent: BigDecimal, +) + +fun kombinerAlleTidslinjerTilProsentTidslinje( + perioderMedFullOvergangsstønadTidslinje: InternPeriodeOvergangsstønadTidslinje, + utvidetBarnetrygdTidslinje: AndelTilkjentYtelseMedEndreteUtbetalingerTidslinje, + barnSomGirRettTilSmåbarnstilleggTidslinje: Tidslinje, +): Tidslinje { + return perioderMedFullOvergangsstønadTidslinje + .tilMåned { kombinatorInternPeriodeOvergangsstønadDagTilMåned(it) } + .kombinerMed( + tidslinjeB = utvidetBarnetrygdTidslinje, + tidslinjeC = barnSomGirRettTilSmåbarnstilleggTidslinje, + ) { overgangsstønad, utvidet, under3År -> + if (overgangsstønad == null || utvidet == null || under3År == null) { + null + } else if (utvidet.prosent > BigDecimal.ZERO && under3År == BarnSinRettTilSmåbarnstillegg.UNDER_3_ÅR_UTBETALING) { + SmåbarnstilleggPeriode( + overgangsstønad, + BigDecimal(100), + ) + } else if (utvidet.prosent == BigDecimal.ZERO || under3År == BarnSinRettTilSmåbarnstillegg.UNDER_3_ÅR_NULLUTBETALING) { + SmåbarnstilleggPeriode( + overgangsstønad, + BigDecimal.ZERO, + ) + } else { + throw Feil("Ugyldig kombinasjon av overgangsstønad, utvidet og barn under 3 år ved generering av småbarnstillegg.") + } + } + .filtrerIkkeNull() +} + +/** + * EF sender alltid overgangsstønad-perioder som gjelder hele måneder, men formatet vi får er på LocalDate + * Returverdier: + * Null - Søker får ikke overgangsstønad noen dager den måneden + * InternPeriodeOvergangsstønad - Det finnes minst 1 dag i måneden hvor søker får overgangsstønad, den første av disse blir returnert + */ +fun kombinatorInternPeriodeOvergangsstønadDagTilMåned(dagverdier: List): InternPeriodeOvergangsstønad? { + val dagverdierSomErSatt = dagverdier.filterNotNull() + return if (dagverdierSomErSatt.isEmpty()) { + null + } else { + dagverdierSomErSatt.first() + } +} + +enum class BarnSinRettTilSmåbarnstillegg { + UNDER_3_ÅR_UTBETALING, + UNDER_3_ÅR_NULLUTBETALING, +} + +fun validerUtvidetOgBarnasAndeler( + utvidetAndeler: List, + barnasAndeler: List, +) { + if (utvidetAndeler.any { !it.erUtvidet() }) throw Feil("Det finnes andre ytelser enn utvidet blandt utvidet-andelene") + if (barnasAndeler.any { it.erSøkersAndel() }) throw Feil("Finner andeler for søker blandt barnas andeler") +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseRepositoryUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseRepositoryUtil.kt new file mode 100644 index 000000000..c5fba16f5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseRepositoryUtil.kt @@ -0,0 +1,57 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.AndelTilkjentYtelsePraktiskLikhet.erIPraksisLik +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.AndelTilkjentYtelsePraktiskLikhet.inneholderIPraksis +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje + +/** + * En litt risikabel funksjon, som benytter "funksjonell likhet" for å sjekke etter endringer på andel tilkjent ytelse + */ +fun TilkjentYtelseRepository.oppdaterTilkjentYtelse( + tilkjentYtelse: TilkjentYtelse, + oppdaterteAndeler: Collection, +): TilkjentYtelse { + if (tilkjentYtelse.andelerTilkjentYtelse.erIPraksisLik(oppdaterteAndeler)) { + return tilkjentYtelse + } + + // Her er det viktig å beholde de originale andelene, som styres av JPA og har alt av innhold + val skalBeholdes = tilkjentYtelse.andelerTilkjentYtelse + .filter { oppdaterteAndeler.inneholderIPraksis(it) } + + val skalLeggesTil = oppdaterteAndeler + .filter { !tilkjentYtelse.andelerTilkjentYtelse.inneholderIPraksis(it) } + + // Forsikring: Sjekk at det ikke oppstår eller forsvinner andeler når de sjekkes for likhet + if (oppdaterteAndeler.size != (skalBeholdes.size + skalLeggesTil.size)) { + throw IllegalStateException("Avvik mellom antall innsendte andeler og kalkulerte endringer") + } + + tilkjentYtelse.andelerTilkjentYtelse.clear() + tilkjentYtelse.andelerTilkjentYtelse.addAll(skalBeholdes + skalLeggesTil) + + // Ekstra forsikring: Bygger tidslinjene på nytt for å sjekke at det ikke er introdusert duplikater + // Krasjer med Exception hvis det forekommer perioder per aktør og ytelsetype som overlapper + // Bør fjernes hvis det ikke forekommer feil + tilkjentYtelse.andelerTilkjentYtelse.sjekkForDuplikater() + + return this.saveAndFlush(tilkjentYtelse) +} + +@Deprecated("Brukes som sikkerhetsnett for å sjekke at det ikke oppstår duplikater. Burde være unødvendig") +private fun Iterable.sjekkForDuplikater() { + try { + // Det skal ikke være overlapp i andeler for en gitt ytelsestype og aktør + this.groupBy { it.aktør.aktørId + it.type } + .mapValues { (_, andeler) -> tidslinje { andeler.map { it.tilPeriode() } } } + .values.forEach { it.perioder() } + } catch (throwable: Throwable) { + throw IllegalStateException( + "Endring av andeler tilkjent ytelse i differanseberegning holder på å introdusere duplikater", + throwable, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseUtils.kt new file mode 100644 index 000000000..1449e8c45 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseUtils.kt @@ -0,0 +1,305 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.Utils.avrundetHeltallAvProsent +import no.nav.familie.ba.sak.common.erDagenFør +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.inkluderer +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.kjerne.beregning.UtvidetBarnetrygdUtil.finnUtvidetVilkår +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.EndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.beregning.domene.medEndring +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +object TilkjentYtelseUtils { + + fun beregnTilkjentYtelse( + vilkårsvurdering: Vilkårsvurdering, + personopplysningGrunnlag: PersonopplysningGrunnlag, + endretUtbetalingAndeler: List = emptyList(), + fagsakType: FagsakType, + hentPerioderMedFullOvergangsstønad: (aktør: Aktør) -> List = { _ -> emptyList() }, + ): TilkjentYtelse { + val tilkjentYtelse = TilkjentYtelse( + behandling = vilkårsvurdering.behandling, + opprettetDato = LocalDate.now(), + endretDato = LocalDate.now(), + ) + + val (endretUtbetalingAndelerSøker, endretUtbetalingAndelerBarna) = endretUtbetalingAndeler.partition { it.person?.type == PersonType.SØKER } + + val andelerTilkjentYtelseBarnaUtenEndringer = OrdinærBarnetrygdUtil.beregnAndelerTilkjentYtelseForBarna( + personopplysningGrunnlag = personopplysningGrunnlag, + personResultater = vilkårsvurdering.personResultater, + fagsakType = fagsakType, + ) + .map { + if (it.person.type != PersonType.BARN) throw Feil("Prøver å generere ordinær andel for person av typen ${it.person.type}") + + AndelTilkjentYtelse( + behandlingId = vilkårsvurdering.behandling.id, + tilkjentYtelse = tilkjentYtelse, + aktør = it.person.aktør, + stønadFom = it.stønadFom, + stønadTom = it.stønadTom, + kalkulertUtbetalingsbeløp = it.beløp, + nasjonaltPeriodebeløp = it.beløp, + type = YtelseType.ORDINÆR_BARNETRYGD, + sats = it.sats, + prosent = it.prosent, + ) + } + + val barnasAndelerInkludertEtterbetaling3ÅrEndringer = oppdaterTilkjentYtelseMedEndretUtbetalingAndeler( + andelTilkjentYtelserUtenEndringer = andelerTilkjentYtelseBarnaUtenEndringer, + endretUtbetalingAndeler = endretUtbetalingAndelerBarna.filter { it.årsak == Årsak.ETTERBETALING_3ÅR }, + ) + + val andelerTilkjentYtelseUtvidetMedAlleEndringer = UtvidetBarnetrygdUtil.beregnTilkjentYtelseUtvidet( + utvidetVilkår = finnUtvidetVilkår(vilkårsvurdering), + tilkjentYtelse = tilkjentYtelse, + andelerTilkjentYtelseBarnaMedEtterbetaling3ÅrEndringer = barnasAndelerInkludertEtterbetaling3ÅrEndringer, + endretUtbetalingAndelerSøker = endretUtbetalingAndelerSøker, + personResultater = vilkårsvurdering.personResultater, + ) + + val småbarnstilleggErMulig = erSmåbarnstilleggMulig( + utvidetAndeler = andelerTilkjentYtelseUtvidetMedAlleEndringer, + barnasAndeler = barnasAndelerInkludertEtterbetaling3ÅrEndringer, + ) + + val andelerTilkjentYtelseSmåbarnstillegg = if (småbarnstilleggErMulig) { + SmåbarnstilleggBarnetrygdGenerator( + behandlingId = vilkårsvurdering.behandling.id, + tilkjentYtelse = tilkjentYtelse, + ) + .lagSmåbarnstilleggAndeler( + perioderMedFullOvergangsstønad = hentPerioderMedFullOvergangsstønad( + personopplysningGrunnlag.søker.aktør, + ), + utvidetAndeler = andelerTilkjentYtelseUtvidetMedAlleEndringer, + barnasAndeler = barnasAndelerInkludertEtterbetaling3ÅrEndringer, + barnasAktørerOgFødselsdatoer = personopplysningGrunnlag.barna.map { + Pair( + it.aktør, + it.fødselsdato, + ) + }, + ) + } else { + emptyList() + } + + val andelerTilkjentYtelseBarnaMedAlleEndringer = oppdaterTilkjentYtelseMedEndretUtbetalingAndeler( + andelTilkjentYtelserUtenEndringer = andelerTilkjentYtelseBarnaUtenEndringer, + endretUtbetalingAndeler = endretUtbetalingAndelerBarna, + ) + + tilkjentYtelse.andelerTilkjentYtelse.addAll(andelerTilkjentYtelseBarnaMedAlleEndringer.map { it.andel } + andelerTilkjentYtelseUtvidetMedAlleEndringer.map { it.andel } + andelerTilkjentYtelseSmåbarnstillegg.map { it.andel }) + + return tilkjentYtelse + } + + private fun erSmåbarnstilleggMulig( + utvidetAndeler: List, + barnasAndeler: List, + ): Boolean = utvidetAndeler.isNotEmpty() && barnasAndeler.isNotEmpty() + + fun oppdaterTilkjentYtelseMedEndretUtbetalingAndeler( + andelTilkjentYtelserUtenEndringer: Collection, + endretUtbetalingAndeler: List, + ): List { + if (endretUtbetalingAndeler.isEmpty()) { + return andelTilkjentYtelserUtenEndringer + .map { AndelTilkjentYtelseMedEndreteUtbetalinger.utenEndringer(it.copy()) } + } + + val (andelerUtenSmåbarnstillegg, andelerMedSmåbarnstillegg) = andelTilkjentYtelserUtenEndringer.partition { !it.erSmåbarnstillegg() } + + val nyeAndelTilkjentYtelse = mutableListOf() + + andelerUtenSmåbarnstillegg.groupBy { it.aktør }.forEach { andelerForPerson -> + val aktør = andelerForPerson.key + val endringerForPerson = + endretUtbetalingAndeler.filter { it.person?.aktør == aktør } + + val nyeAndelerForPerson = mutableListOf() + + andelerForPerson.value.forEach { andelForPerson -> + // Deler opp hver enkelt andel i perioder som hhv blir berørt av endringene og de som ikke berøres av de. + val (perioderMedEndring, perioderUtenEndring) = andelForPerson.stønadsPeriode() + .perioderMedOgUtenOverlapp( + endringerForPerson.map { endringerForPerson -> endringerForPerson.periode }, + ) + // Legger til nye AndelTilkjentYtelse for perioder som er berørt av endringer. + nyeAndelerForPerson.addAll( + perioderMedEndring.map { månedPeriodeEndret -> + val endretUtbetalingMedAndeler = + endringerForPerson.single { it.overlapperMed(månedPeriodeEndret) } + val nyttNasjonaltPeriodebeløp = andelForPerson.sats + .avrundetHeltallAvProsent(endretUtbetalingMedAndeler.prosent!!) + + val andelTilkjentYtelse = andelForPerson.copy( + prosent = endretUtbetalingMedAndeler.prosent!!, + stønadFom = månedPeriodeEndret.fom, + stønadTom = månedPeriodeEndret.tom, + kalkulertUtbetalingsbeløp = nyttNasjonaltPeriodebeløp, + nasjonaltPeriodebeløp = nyttNasjonaltPeriodebeløp, + ) + + andelTilkjentYtelse.medEndring(endretUtbetalingMedAndeler) + }, + ) + // Legger til nye AndelTilkjentYtelse for perioder som ikke berøres av endringer. + nyeAndelerForPerson.addAll( + perioderUtenEndring.map { månedPeriodeUendret -> + val andelTilkjentYtelse = andelForPerson.copy( + stønadFom = månedPeriodeUendret.fom, + stønadTom = månedPeriodeUendret.tom, + ) + AndelTilkjentYtelseMedEndreteUtbetalinger.utenEndringer(andelTilkjentYtelse) + }, + ) + } + + val nyeAndelerForPersonEtterSammenslåing = + slåSammenPerioderSomIkkeSkulleHaVærtSplittet( + andelerTilkjentYtelseMedEndreteUtbetalinger = nyeAndelerForPerson, + skalAndelerSlåsSammen = ::skalAndelerSlåsSammen, + ) + + nyeAndelTilkjentYtelse.addAll(nyeAndelerForPersonEtterSammenslåing) + } + + // Ettersom vi aldri ønsker å overstyre småbarnstillegg perioder fjerner vi dem og legger dem til igjen her + nyeAndelTilkjentYtelse.addAll( + andelerMedSmåbarnstillegg.map { + AndelTilkjentYtelseMedEndreteUtbetalinger.utenEndringer(it) + }, + ) + + // Sorterer primært av hensyn til måten testene er implementert og kan muligens fjernes dersom dette skrives om. + nyeAndelTilkjentYtelse.sortWith( + compareBy( + { it.aktør.aktivFødselsnummer() }, + { it.stønadFom }, + ), + ) + return nyeAndelTilkjentYtelse + } + + fun slåSammenPerioderSomIkkeSkulleHaVærtSplittet( + andelerTilkjentYtelseMedEndreteUtbetalinger: MutableList, + skalAndelerSlåsSammen: (førsteAndel: AndelTilkjentYtelseMedEndreteUtbetalinger, nesteAndel: AndelTilkjentYtelseMedEndreteUtbetalinger) -> Boolean, + ): MutableList { + val sorterteAndeler = andelerTilkjentYtelseMedEndreteUtbetalinger.sortedBy { it.stønadFom }.toMutableList() + var periodenViSerPå = sorterteAndeler.first() + val oppdatertListeMedAndeler = mutableListOf() + + for (index in 0 until sorterteAndeler.size) { + val andel = sorterteAndeler[index] + val nesteAndel = if (index == sorterteAndeler.size - 1) null else sorterteAndeler[index + 1] + + periodenViSerPå = if (nesteAndel != null) { + val andelerSkalSlåsSammen = + skalAndelerSlåsSammen(andel, nesteAndel) + + if (andelerSkalSlåsSammen) { + val nyAndel = periodenViSerPå.slåSammenMed(nesteAndel) + nyAndel + } else { + oppdatertListeMedAndeler.add(periodenViSerPå) + sorterteAndeler[index + 1] + } + } else { + oppdatertListeMedAndeler.add(periodenViSerPå) + break + } + } + return oppdatertListeMedAndeler + } + + /** + * Slår sammen andeler for barn når beløpet er nedjuster til 0kr som er blitt splittet av + * for eksempel satsendring. + */ + fun skalAndelerSlåsSammen( + førsteAndel: AndelTilkjentYtelseMedEndreteUtbetalinger, + nesteAndel: AndelTilkjentYtelseMedEndreteUtbetalinger, + ): Boolean = + førsteAndel.stønadTom.sisteDagIInneværendeMåned() + .erDagenFør(nesteAndel.stønadFom.førsteDagIInneværendeMåned()) && førsteAndel.prosent == BigDecimal(0) && nesteAndel.prosent == BigDecimal( + 0, + ) && førsteAndel.endreteUtbetalinger.isNotEmpty() && førsteAndel.endreteUtbetalinger.singleOrNull() == nesteAndel.endreteUtbetalinger.singleOrNull() +} + +fun MånedPeriode.perioderMedOgUtenOverlapp(perioder: List): Pair, List> { + if (perioder.isEmpty()) return Pair(emptyList(), listOf(this)) + + val alleMånederMedOverlappstatus = mutableMapOf() + var nesteMåned = this.fom + while (nesteMåned <= this.tom) { + alleMånederMedOverlappstatus[nesteMåned] = + perioder.any { månedPeriode -> månedPeriode.inkluderer(nesteMåned) } + nesteMåned = nesteMåned.plusMonths(1) + } + + var periodeStart: YearMonth? = this.fom + + val perioderMedOverlapp = mutableListOf() + val perioderUtenOverlapp = mutableListOf() + while (periodeStart != null) { + val periodeMedOverlapp = alleMånederMedOverlappstatus[periodeStart]!! + + val nesteMånedMedNyOverlappstatus = alleMånederMedOverlappstatus + .filter { it.key > periodeStart && it.value != periodeMedOverlapp } + .minByOrNull { it.key } + ?.key?.minusMonths(1) ?: this.tom + + // Når tom skal utledes for en periode det eksisterer en endret periode for må den minste av følgende to datoer velges: + // 1. tom for den aktuelle endrete perioden + // 2. neste måned uten overlappende endret periode, eller hvis null, tom for this (som representerer en AndelTilkjentYtelse). + // Dersom tom gjelder periode uberørt av endringer så vil alltid alt.2 være korrekt. + val periodeSlutt = if (periodeMedOverlapp) { + val nesteMånedUtenOverlapp = perioder.single { it.inkluderer(periodeStart!!) }.tom + minOf(nesteMånedUtenOverlapp, nesteMånedMedNyOverlappstatus) + } else { + nesteMånedMedNyOverlappstatus + } + + if (periodeMedOverlapp) { + perioderMedOverlapp.add(MånedPeriode(periodeStart, periodeSlutt)) + } else { + perioderUtenOverlapp.add(MånedPeriode(periodeStart, periodeSlutt)) + } + + periodeStart = alleMånederMedOverlappstatus + .filter { it.key > periodeSlutt } + .minByOrNull { it.key }?.key + } + return Pair(perioderMedOverlapp, perioderUtenOverlapp) +} + +internal data class BeregnetAndel( + val person: Person, + val stønadFom: YearMonth, + val stønadTom: YearMonth, + val beløp: Int, + val sats: Int, + val prosent: BigDecimal, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseValidering.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseValidering.kt new file mode 100644 index 000000000..9d3c8d7dc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseValidering.kt @@ -0,0 +1,299 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.KONTAKT_TEAMET_SUFFIX +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.UtbetalingsikkerhetFeil +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValidering.maksBeløp +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.beregning.domene.tilTidslinjeMedAndeler +import no.nav.familie.ba.sak.kjerne.beregning.domene.tilTidslinjerPerPersonOgType +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.forrigebehandling.EndringIUtbetalingUtil +import no.nav.familie.ba.sak.kjerne.forrigebehandling.EndringUtil.tilFørsteEndringstidspunkt +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonEnkel +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.barn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.søker +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombiner +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.outerJoin +import no.nav.familie.ba.sak.kjerne.tidslinje.månedPeriodeAv +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import no.nav.familie.ba.sak.kjerne.tidslinje.tilTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.YearMonth + +// 3 år (krav i loven) +fun hentGyldigEtterbetalingFom(kravDato: LocalDate) = + kravDato.minusYears(3) + .toYearMonth() + +fun hentSøkersAndeler( + andeler: List, + søker: PersonEnkel, +) = andeler.filter { it.aktør == søker.aktør } + +fun hentBarnasAndeler(andeler: List, barna: List) = barna.map { barn -> + barn to andeler.filter { it.aktør == barn.aktør } +} + +/** + * Ekstra sikkerhet rundt hva som utbetales som på sikt vil legges inn i + * de respektive stegene SB håndterer slik at det er lettere for SB å rette feilene. + */ +object TilkjentYtelseValidering { + + internal fun validerAtSatsendringKunOppdatererSatsPåEksisterendePerioder( + andelerFraForrigeBehandling: List, + andelerTilkjentYtelse: List, + ) { + val andelerGruppert = andelerTilkjentYtelse.tilTidslinjerPerPersonOgType() + val forrigeAndelerGruppert = andelerFraForrigeBehandling.tilTidslinjerPerPersonOgType() + + andelerGruppert.outerJoin(forrigeAndelerGruppert) { nåværendeAndel, forrigeAndel -> + when { + forrigeAndel == null && nåværendeAndel != null -> + throw Feil("Satsendring kan ikke legge til en andel som ikke var der i forrige behandling") + + forrigeAndel != null && nåværendeAndel == null -> + throw Feil("Satsendring kan ikke fjerne en andel som fantes i forrige behandling") + + forrigeAndel != null && forrigeAndel.prosent != nåværendeAndel?.prosent -> + throw Feil("Satsendring kan ikke endre på prosenten til en andel") + + forrigeAndel != null && forrigeAndel.type != nåværendeAndel?.type -> + throw Feil("Satsendring kan ikke endre YtelseType til en andel") + + else -> false + } + }.values.map { it.perioder() } // Må kalle på .perioder() for at feilene over skal bli kastet + } + + fun finnAktørIderMedUgyldigEtterbetalingsperiode( + forrigeAndelerTilkjentYtelse: Collection, + andelerTilkjentYtelse: Collection, + kravDato: LocalDateTime, + ): List { + val gyldigEtterbetalingFom = hentGyldigEtterbetalingFom(kravDato.toLocalDate()) + + val aktører = unikeAntører(andelerTilkjentYtelse, forrigeAndelerTilkjentYtelse) + + val personerMedUgyldigEtterbetaling = + aktører.mapNotNull { aktør -> + val andelerTilkjentYtelseForPerson = andelerTilkjentYtelse.filter { it.aktør == aktør } + val forrigeAndelerTilkjentYtelseForPerson = forrigeAndelerTilkjentYtelse.filter { it.aktør == aktør } + + aktør.takeIf { + erUgyldigEtterbetalingPåPerson( + forrigeAndelerTilkjentYtelseForPerson, + andelerTilkjentYtelseForPerson, + gyldigEtterbetalingFom, + ) + } + } + + return personerMedUgyldigEtterbetaling + } + + private fun unikeAntører( + andelerTilkjentYtelse: Collection, + forrigeAndelerTilkjentYtelse: Collection, + ): Set { + val aktørIderFraAndeler = andelerTilkjentYtelse.map { it.aktør } + val aktøerIderFraForrigeAndeler = forrigeAndelerTilkjentYtelse.map { it.aktør } + return (aktørIderFraAndeler + aktøerIderFraForrigeAndeler).toSet() + } + + fun erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson: List, + andelerForPerson: List, + gyldigEtterbetalingFom: YearMonth, + ): Boolean { + return YtelseType.values().any { ytelseType -> + val forrigeAndelerForPersonOgType = forrigeAndelerForPerson.filter { it.type == ytelseType } + val andelerForPersonOgType = andelerForPerson.filter { it.type == ytelseType } + + val etterbetalingTidslinje = EndringIUtbetalingUtil.lagEtterbetalingstidslinjeForPersonOgType( + nåværendeAndeler = andelerForPersonOgType, + forrigeAndeler = forrigeAndelerForPersonOgType, + ) + + val førsteMånedMedEtterbetaling = etterbetalingTidslinje.tilFørsteEndringstidspunkt() + + førsteMånedMedEtterbetaling != null && førsteMånedMedEtterbetaling < gyldigEtterbetalingFom + } + } + + fun validerAtTilkjentYtelseHarFornuftigePerioderOgBeløp( + tilkjentYtelse: TilkjentYtelse, + søkerOgBarn: List, + ) { + val søker = søkerOgBarn.søker() + val barna = søkerOgBarn.barn() + + val tidslinjeMedAndeler = tilkjentYtelse.tilTidslinjeMedAndeler() + + val fagsakType = tilkjentYtelse.behandling.fagsak.type + + tidslinjeMedAndeler.toSegments().forEach { + val søkersAndeler = hentSøkersAndeler(it.value, søker) + val barnasAndeler = hentBarnasAndeler(it.value, barna) + + validerAtBeløpForPartStemmerMedSatser(person = søker, andeler = søkersAndeler, fagsakType = fagsakType) + + barnasAndeler.forEach { (barn, andeler) -> + validerAtBeløpForPartStemmerMedSatser(person = barn, andeler = andeler, fagsakType = fagsakType) + } + } + } + + fun validerAtBarnIkkeFårFlereUtbetalingerSammePeriode( + behandlendeBehandlingTilkjentYtelse: TilkjentYtelse, + barnMedAndreRelevanteTilkjentYtelser: List>>, + søkerOgBarn: List, + ) { + val barna = søkerOgBarn.barn().sortedBy { it.fødselsdato } + + val barnasAndeler = hentBarnasAndeler(behandlendeBehandlingTilkjentYtelse.andelerTilkjentYtelse.toList(), barna) + + val barnMedUtbetalingsikkerhetFeil = mutableMapOf>() + barnasAndeler.forEach { (barn, andeler) -> + val barnsAndelerFraAndreBehandlinger = + barnMedAndreRelevanteTilkjentYtelser.filter { it.first.aktør == barn.aktør } + .flatMap { it.second } + .flatMap { it.andelerTilkjentYtelse } + .filter { it.aktør == barn.aktør } + + val perioderMedOverlapp = finnPeriodeMedOverlappAvAndeler( + andeler = andeler, + barnsAndelerFraAndreBehandlinger = barnsAndelerFraAndreBehandlinger, + ) + if (perioderMedOverlapp.isNotEmpty()) { + barnMedUtbetalingsikkerhetFeil.put(barn, perioderMedOverlapp) + } + } + if (barnMedUtbetalingsikkerhetFeil.isNotEmpty()) { + throw UtbetalingsikkerhetFeil( + melding = "Vi finner utbetalinger som overstiger 100% på hvert av barna: ${ + barnMedUtbetalingsikkerhetFeil.tilFeilmeldingTekst() + }", + frontendFeilmelding = "Du kan ikke godkjenne dette vedtaket fordi det vil betales ut mer enn 100% for barn født ${ + barnMedUtbetalingsikkerhetFeil.tilFeilmeldingTekst() + }. Reduksjonsvedtak til annen person må være sendt til godkjenning før du kan gå videre.", + ) + } + } + + fun MutableMap>.tilFeilmeldingTekst() = + Utils.slåSammen(this.map { "${it.key.fødselsdato.tilKortString()} i perioden ${it.value.joinToString(", ") { "${it.fom} til ${it.tom}" }}" }) + + fun maksBeløp(personType: PersonType, fagsakType: FagsakType): Int { + val satser = SatsService.hentAllesatser() + val småbarnsTillegg = satser.filter { it.type == SatsType.SMA } + val ordinærMedTillegg = satser.filter { it.type == SatsType.TILLEGG_ORBA } + val utvidet = satser.filter { it.type == SatsType.UTVIDET_BARNETRYGD } + if (småbarnsTillegg.isEmpty() || ordinærMedTillegg.isEmpty() || utvidet.isEmpty()) error("Fant ikke satser ved validering") + val maksSmåbarnstillegg = småbarnsTillegg.maxByOrNull { it.beløp }!!.beløp + val maksOrdinærMedTillegg = ordinærMedTillegg.maxByOrNull { it.beløp }!!.beløp + val maksUtvidet = utvidet.maxBy { it.beløp }.beløp + + return if (fagsakType == FagsakType.BARN_ENSLIG_MINDREÅRIG) { + maksOrdinærMedTillegg + maksUtvidet + } else { + when (personType) { + PersonType.BARN -> maksOrdinærMedTillegg + PersonType.SØKER -> maksUtvidet + maksSmåbarnstillegg + else -> throw Feil("Ikke støtte for å utbetale til persontype ${personType.name}") + } + } + } + + fun finnPeriodeMedOverlappAvAndeler( + andeler: List, + barnsAndelerFraAndreBehandlinger: List, + ): List { + val kombinertOverlappTidslinje = YtelseType.values().map { ytelseType -> + lagErOver100ProsentUtbetalingPåYtelseTidslinje( + andeler = andeler.filter { it.type == ytelseType }, + barnsAndelerFraAndreBehandlinger = barnsAndelerFraAndreBehandlinger.filter { it.type == ytelseType }, + ) + }.kombiner { it.minstEnYtelseHarOverlapp() } + + return kombinertOverlappTidslinje.perioder().filter { it.innhold == true } + .map { MånedPeriode(it.fraOgMed.tilYearMonth(), it.tilOgMed.tilYearMonth()) } + } + + internal fun Iterable.minstEnYtelseHarOverlapp(): Boolean { + return any { it } + } + + fun lagErOver100ProsentUtbetalingPåYtelseTidslinje( + andeler: List, + barnsAndelerFraAndreBehandlinger: List, + ): Tidslinje { + if (barnsAndelerFraAndreBehandlinger.isEmpty()) { + return emptyList>().tilTidslinje() + } + val prosenttidslinjerPerBehandling = + (andeler + barnsAndelerFraAndreBehandlinger).groupBy { it.behandlingId }.values + .map { it.tilProsentAvYtelseUtbetaltTidslinje() } + + val erOver100ProsentTidslinje = + prosenttidslinjerPerBehandling.fold(emptyList>().tilTidslinje()) { summertProsentTidslinje, prosentTidslinje -> + summertProsentTidslinje.kombinerMed(prosentTidslinje) { sumProsentForPeriode, prosentForAndel -> + (sumProsentForPeriode ?: BigDecimal.ZERO) + (prosentForAndel ?: BigDecimal.ZERO) + } + }.map { sumProsentForPeriode -> (sumProsentForPeriode ?: BigDecimal.ZERO) > BigDecimal.valueOf(100) } + + return erOver100ProsentTidslinje + } +} + +private fun List.tilProsentAvYtelseUtbetaltTidslinje() = + this.map { + månedPeriodeAv( + fraOgMed = it.periode.fom, + tilOgMed = it.periode.tom, + innhold = it.prosent, + ) + }.tilTidslinje() + +private fun validerAtBeløpForPartStemmerMedSatser( + person: PersonEnkel, + andeler: List, + fagsakType: FagsakType, +) { + val maksAntallAndeler = + if (fagsakType == FagsakType.BARN_ENSLIG_MINDREÅRIG) 2 else if (person.type == PersonType.BARN) 1 else 2 + val maksTotalBeløp = maksBeløp(personType = person.type, fagsakType = fagsakType) + + if (andeler.size > maksAntallAndeler) { + throw UtbetalingsikkerhetFeil( + melding = "Validering av andeler for ${person.type} i perioden (${andeler.first().stønadFom} - ${andeler.first().stønadTom}) feilet: Tillatte andeler = $maksAntallAndeler, faktiske andeler = ${andeler.size}.", + frontendFeilmelding = "Det har skjedd en systemfeil, og beløpene stemmer ikke overens med dagens satser. $KONTAKT_TEAMET_SUFFIX", + ) + } + + val totalbeløp = andeler.map { it.kalkulertUtbetalingsbeløp } + .fold(0) { sum, beløp -> sum + beløp } + if (totalbeløp > maksTotalBeløp) { + throw UtbetalingsikkerhetFeil( + melding = "Validering av andeler for ${person.type} i perioden (${andeler.first().stønadFom} - ${andeler.first().stønadTom}) feilet: Tillatt totalbeløp = $maksTotalBeløp, faktiske totalbeløp = $totalbeløp.", + frontendFeilmelding = "Det har skjedd en systemfeil, og beløpene stemmer ikke overens med dagens satser. $KONTAKT_TEAMET_SUFFIX", + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseValideringService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseValideringService.kt new file mode 100644 index 000000000..edf49e035 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseValideringService.kt @@ -0,0 +1,104 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValidering.finnAktørIderMedUgyldigEtterbetalingsperiode +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.barn +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class TilkjentYtelseValideringService( + private val totrinnskontrollService: TotrinnskontrollService, + private val beregningService: BeregningService, + private val persongrunnlagService: PersongrunnlagService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, +) { + fun validerAtIngenUtbetalingerOverstiger100Prosent(behandling: Behandling) { + if (behandling.erMigrering() || behandling.erTekniskEndring() || behandling.erSatsendring()) return + val totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(behandling.id) + + if (totrinnskontroll?.godkjent == true) { + validerAtBarnIkkeFårFlereUtbetalingerSammePeriode(behandling) + } + } + + fun validerAtBarnIkkeFårFlereUtbetalingerSammePeriode(behandling: Behandling) { + val tilkjentYtelse = beregningService.hentTilkjentYtelseForBehandling(behandlingId = behandling.id) + + val søkerOgBarn = persongrunnlagService.hentSøkerOgBarnPåBehandlingThrows(behandlingId = behandling.id) + + val barnMedAndreRelevanteTilkjentYtelser = søkerOgBarn.barn().map { + Pair( + it, + beregningService.hentRelevanteTilkjentYtelserForBarn(it.aktør, behandling.fagsak.id), + ) + } + + secureLogger.info("Andeler tilkjent ytelse i inneværende behandling: " + tilkjentYtelse.andelerTilkjentYtelse) + secureLogger.info( + "Barn og deres andeler tilkjent ytelse fra andre fagsaker: " + barnMedAndreRelevanteTilkjentYtelser.map { + "${it.first} -> ${it.second}" + }, + ) + + TilkjentYtelseValidering.validerAtBarnIkkeFårFlereUtbetalingerSammePeriode( + behandlendeBehandlingTilkjentYtelse = tilkjentYtelse, + barnMedAndreRelevanteTilkjentYtelser = barnMedAndreRelevanteTilkjentYtelser, + søkerOgBarn = søkerOgBarn, + ) + } + + fun validerIngenAndelerTilkjentYtelseMedSammeOffsetIBehandling(behandlingId: Long) { + val tilkjentYtelse = beregningService.hentTilkjentYtelseForBehandling(behandlingId = behandlingId) + + if (tilkjentYtelse.harAndelerTilkjentYtelseMedSammeOffset()) { + secureLogger.info("Behandling har flere andeler med likt offset: ${tilkjentYtelse.andelerTilkjentYtelse}") + throw Feil("Behandling $behandlingId har andel tilkjent ytelse med offset lik en annen andel i behandlingen.") + } + } + + private fun TilkjentYtelse.harAndelerTilkjentYtelseMedSammeOffset(): Boolean { + val periodeOffsetForAndeler = this.andelerTilkjentYtelse.mapNotNull { it.periodeOffset } + + return periodeOffsetForAndeler.size != periodeOffsetForAndeler.distinct().size + } + + fun barnetrygdLøperForAnnenForelder(behandling: Behandling, barna: List): Boolean { + return barna.any { + beregningService.hentRelevanteTilkjentYtelserForBarn(barnAktør = it.aktør, fagsakId = behandling.fagsak.id) + .isNotEmpty() + } + } + + fun finnAktørerMedUgyldigEtterbetalingsperiode( + behandlingId: Long, + ): List { + val tilkjentYtelse = beregningService.hentTilkjentYtelseForBehandling(behandlingId = behandlingId) + + val forrigeBehandling = behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt( + behandling = behandlingHentOgPersisterService.hent(behandlingId), + ) + val forrigeAndelerTilkjentYtelse = + forrigeBehandling?.let { beregningService.hentOptionalTilkjentYtelseForBehandling(behandlingId = it.id) } + ?.andelerTilkjentYtelse + + return finnAktørIderMedUgyldigEtterbetalingsperiode( + forrigeAndelerTilkjentYtelse = forrigeAndelerTilkjentYtelse ?: emptyList(), + andelerTilkjentYtelse = tilkjentYtelse.andelerTilkjentYtelse.toList(), + kravDato = tilkjentYtelse.behandling.opprettetTidspunkt, + ) + } + + companion object { + + val logger = LoggerFactory.getLogger(TilkjentYtelseValideringService::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtvidetBarnetrygdGenerator.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtvidetBarnetrygdGenerator.kt new file mode 100644 index 000000000..e6435f62a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtvidetBarnetrygdGenerator.kt @@ -0,0 +1,67 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.Utils.avrundetHeltallAvProsent +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMedKunVerdi +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNullOgIkkeTom +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.leftJoin +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.tilForskjøvetTidslinjeForOppfyltVilkårForVoksenPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat + +data class UtvidetBarnetrygdGenerator( + val behandlingId: Long, + val tilkjentYtelse: TilkjentYtelse, +) { + fun lagUtvidetBarnetrygdAndeler( + utvidetVilkår: List, + andelerBarna: List, + tidslinjerMedPerioderBarnaBorMedSøker: Map>, + ): List { + if (utvidetVilkår.isEmpty() || andelerBarna.isEmpty()) return emptyList() + + val søkerAktør = utvidetVilkår.first().personResultat?.aktør ?: error("Vilkår mangler PersonResultat") + + val utvidetVilkårTidslinje = utvidetVilkår.tilForskjøvetTidslinjeForOppfyltVilkårForVoksenPerson(Vilkår.UTVIDET_BARNETRYGD) + + val barnasAndelerFiltrertForPerioderBarnaBorMedSøker = andelerBarna.tilSeparateTidslinjerForBarna().leftJoin(tidslinjerMedPerioderBarnaBorMedSøker) { andelerBarna, barnBorMedSøker -> + when (barnBorMedSøker) { + true -> andelerBarna + else -> null + } + } + + val størsteProsentTidslinje = barnasAndelerFiltrertForPerioderBarnaBorMedSøker.values + .kombinerUtenNullOgIkkeTom { andeler -> andeler.maxOf { it.prosent } } + + val utvidetAndeler = utvidetVilkårTidslinje.kombinerMedKunVerdi( + størsteProsentTidslinje, + satstypeTidslinje(SatsType.UTVIDET_BARNETRYGD), + ) { _, prosent, sats -> + val nasjonaltPeriodebeløp = sats.avrundetHeltallAvProsent(prosent) + AndelTilkjentYtelseForTidslinje( + aktør = søkerAktør, + beløp = nasjonaltPeriodebeløp, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + sats = sats, + prosent = prosent, + ) + }.tilAndelerTilkjentYtelse(tilkjentYtelse) + + if (utvidetAndeler.isEmpty()) { + throw FunksjonellFeil( + "Du har lagt til utvidet barnetrygd for en periode der det ikke er rett til barnetrygd for " + + "noen av barna. Hvis du trenger hjelp, meld sak i Porten.", + ) + } + + return utvidetAndeler + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtvidetBarnetrygdUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtvidetBarnetrygdUtil.kt new file mode 100644 index 000000000..0bf3d3f12 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtvidetBarnetrygdUtil.kt @@ -0,0 +1,83 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.erBack2BackIMånedsskifte +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.EndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.lagForskjøvetTidslinjeForOppfylteVilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering + +object UtvidetBarnetrygdUtil { + internal fun beregnTilkjentYtelseUtvidet( + utvidetVilkår: List, + andelerTilkjentYtelseBarnaMedEtterbetaling3ÅrEndringer: List, + tilkjentYtelse: TilkjentYtelse, + endretUtbetalingAndelerSøker: List, + personResultater: Set, + ): List { + val tidslinjerMedPerioderBarnaBorMedSøker = finnPerioderBarnaBorMedSøker(personResultater) + + val andelerTilkjentYtelseUtvidet = UtvidetBarnetrygdGenerator( + behandlingId = tilkjentYtelse.behandling.id, + tilkjentYtelse = tilkjentYtelse, + ) + .lagUtvidetBarnetrygdAndeler( + utvidetVilkår = utvidetVilkår, + andelerBarna = andelerTilkjentYtelseBarnaMedEtterbetaling3ÅrEndringer.map { it.andel }, + tidslinjerMedPerioderBarnaBorMedSøker = tidslinjerMedPerioderBarnaBorMedSøker, + ) + + return TilkjentYtelseUtils.oppdaterTilkjentYtelseMedEndretUtbetalingAndeler( + andelTilkjentYtelserUtenEndringer = andelerTilkjentYtelseUtvidet, + endretUtbetalingAndeler = endretUtbetalingAndelerSøker, + ) + } + + private fun finnPerioderBarnaBorMedSøker(personResultater: Set) = + personResultater.associate { personResultat -> + personResultat.aktør to personResultat.vilkårResultater + .lagForskjøvetTidslinjeForOppfylteVilkår(Vilkår.BOR_MED_SØKER) + .map { vilkårResultat -> + vilkårResultat?.utdypendeVilkårsvurderinger?.none { + it in listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_EØS_MED_ANNEN_FORELDER, + UtdypendeVilkårsvurdering.BARN_BOR_I_STORBRITANNIA_MED_ANNEN_FORELDER, + ) + } + } + } + + internal fun finnUtvidetVilkår(vilkårsvurdering: Vilkårsvurdering): List { + val utvidetVilkårResultater = vilkårsvurdering.personResultater + .flatMap { it.vilkårResultater } + .filter { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD && it.resultat == Resultat.OPPFYLT } + + utvidetVilkårResultater.forEach { validerUtvidetVilkårsresultat(vilkårResultat = it, utvidetVilkårResultater = utvidetVilkårResultater) } + return utvidetVilkårResultater + } + + internal fun validerUtvidetVilkårsresultat(vilkårResultat: VilkårResultat, utvidetVilkårResultater: List) { + val fom = vilkårResultat.periodeFom?.toYearMonth() + val tom = vilkårResultat.periodeTom?.toYearMonth() + + val finnesEtterfølgendeBack2BackPeriode = utvidetVilkårResultater.any { erBack2BackIMånedsskifte(tilOgMed = vilkårResultat.periodeTom, fraOgMed = it.periodeFom) } + + if (fom == null) { + throw Feil("Fom må være satt på søkers periode ved utvidet barnetrygd") + } + if (fom == tom && !finnesEtterfølgendeBack2BackPeriode) { + secureLogger.warn("Du kan ikke legge inn fom og tom innenfor samme kalendermåned: $vilkårResultat") + throw FunksjonellFeil("Du kan ikke legge inn fom og tom innenfor samme kalendermåned. Gå til utvidet barnetrygd vilkåret for å endre") + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/AndelTilkjentYtelse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/AndelTilkjentYtelse.kt new file mode 100644 index 000000000..4847634bb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/AndelTilkjentYtelse.kt @@ -0,0 +1,316 @@ +package no.nav.familie.ba.sak.kjerne.beregning.domene + +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.YearMonthConverter +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.integrasjoner.økonomi.YtelsetypeBA +import no.nav.familie.ba.sak.kjerne.beregning.AndelTilkjentYtelseForVedtaksperioderTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.AndelTilkjentYtelseTidslinje +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.utledSegmenter +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.fpsak.tidsserie.LocalDateInterval +import no.nav.fpsak.tidsserie.LocalDateSegment +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth +import java.util.Objects + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "AndelTilkjentYtelse") +@Table(name = "ANDEL_TILKJENT_YTELSE") +data class AndelTilkjentYtelse( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "andel_tilkjent_ytelse_seq_generator") + @SequenceGenerator( + name = "andel_tilkjent_ytelse_seq_generator", + sequenceName = "andel_tilkjent_ytelse_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "fk_behandling_id", nullable = false, updatable = false) + val behandlingId: Long, + + @ManyToOne(cascade = [CascadeType.MERGE]) + @JoinColumn(name = "tilkjent_ytelse_id", nullable = false, updatable = false) + var tilkjentYtelse: TilkjentYtelse, + + @OneToOne(optional = false) + @JoinColumn(name = "fk_aktoer_id", nullable = false, updatable = false) + val aktør: Aktør, + + @Column(name = "kalkulert_utbetalingsbelop", nullable = false) + val kalkulertUtbetalingsbeløp: Int, + + @Column(name = "stonad_fom", nullable = false, columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + val stønadFom: YearMonth, + + @Column(name = "stonad_tom", nullable = false, columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + val stønadTom: YearMonth, + + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false) + val type: YtelseType, + + @Column(name = "sats", nullable = false) + val sats: Int, + + @Column(name = "prosent", nullable = false) + val prosent: BigDecimal, + + // kildeBehandlingId, periodeOffset og forrigePeriodeOffset trengs kun i forbindelse med + // iverksetting/konsistensavstemming, og settes først ved generering av selve oppdraget mot økonomi. + + // Samme informasjon finnes i utbetalingsoppdraget på hver enkelt sak, men for å gjøre operasjonene mer forståelig + // og enklere å jobbe med har vi valgt å trekke det ut hit. + + @Column(name = "kilde_behandling_id") + var kildeBehandlingId: Long? = null, // Brukes til å finne hvilke behandlinger som skal konsistensavstemmes + + @Column(name = "periode_offset") + var periodeOffset: Long? = null, // Brukes for å koble seg på tidligere kjeder sendt til økonomi + + @Column(name = "forrige_periode_offset") + var forrigePeriodeOffset: Long? = null, + + @Column(name = "nasjonalt_periodebelop") + val nasjonaltPeriodebeløp: Int?, + + @Column(name = "differanseberegnet_periodebelop") + val differanseberegnetPeriodebeløp: Int? = null, + +) : BaseEntitet() { + + val periode + get() = MånedPeriode(stønadFom, stønadTom) + + override fun equals(other: Any?): Boolean { + if (other == null || javaClass != other.javaClass) { + return false + } else if (this === other) { + return true + } + + val annen = other as AndelTilkjentYtelse + return Objects.equals(behandlingId, annen.behandlingId) && + Objects.equals(type, annen.type) && + Objects.equals(kalkulertUtbetalingsbeløp, annen.kalkulertUtbetalingsbeløp) && + Objects.equals(stønadFom, annen.stønadFom) && + Objects.equals(stønadTom, annen.stønadTom) && + Objects.equals(aktør, annen.aktør) && + Objects.equals(nasjonaltPeriodebeløp, annen.nasjonaltPeriodebeløp) && + Objects.equals(differanseberegnetPeriodebeløp, annen.differanseberegnetPeriodebeløp) + } + + override fun hashCode(): Int { + return Objects.hash( + id, + behandlingId, + type, + kalkulertUtbetalingsbeløp, + stønadFom, + stønadTom, + aktør, + nasjonaltPeriodebeløp, + differanseberegnetPeriodebeløp, + ) + } + + override fun toString(): String { + return "AndelTilkjentYtelse(id = $id, behandling = $behandlingId, type = $type, prosent = $prosent," + + "beløp = $kalkulertUtbetalingsbeløp, stønadFom = $stønadFom, stønadTom = $stønadTom, periodeOffset = $periodeOffset, " + + "forrigePeriodeOffset = $forrigePeriodeOffset, kildeBehandlingId = $kildeBehandlingId, nasjonaltPeriodebeløp = $nasjonaltPeriodebeløp, differanseberegnetBeløp = $differanseberegnetPeriodebeløp)" + } + + fun overlapperMed(andelFraAnnenBehandling: AndelTilkjentYtelse): Boolean { + return this.type == andelFraAnnenBehandling.type && + this.overlapperPeriode(andelFraAnnenBehandling.periode) + } + + fun overlapperPeriode(måndePeriode: MånedPeriode): Boolean = + this.stønadFom <= måndePeriode.tom && + this.stønadTom >= måndePeriode.fom + + fun stønadsPeriode() = MånedPeriode(this.stønadFom, this.stønadTom) + + fun erUtvidet() = this.type == YtelseType.UTVIDET_BARNETRYGD + + fun erSmåbarnstillegg() = this.type == YtelseType.SMÅBARNSTILLEGG + + fun erSøkersAndel() = erUtvidet() || erSmåbarnstillegg() + + fun erLøpende(): Boolean = this.stønadTom > YearMonth.now() + + fun erDeltBosted() = this.prosent == BigDecimal(50) + + fun erEøs(personResultater: Set) = vurdertEtter(personResultater) == Regelverk.EØS_FORORDNINGEN + + fun vurdertEtter(personResultater: Set): Regelverk { + val relevanteVilkårsResultaer = finnRelevanteVilkårsresulaterForRegelverk(personResultater) + + return if (relevanteVilkårsResultaer.isEmpty()) { + Regelverk.NASJONALE_REGLER + } else if (relevanteVilkårsResultaer.all { it.vurderesEtter == Regelverk.EØS_FORORDNINGEN }) { + Regelverk.EØS_FORORDNINGEN + } else if (relevanteVilkårsResultaer.all { it.vurderesEtter == Regelverk.NASJONALE_REGLER }) { + Regelverk.NASJONALE_REGLER + } else { + Regelverk.NASJONALE_REGLER + } + } + + fun erAndelSomSkalSendesTilOppdrag(): Boolean { + return this.kalkulertUtbetalingsbeløp != 0 + } + + fun erAndelSomharNullutbetaling() = this.kalkulertUtbetalingsbeløp == 0 && + this.differanseberegnetPeriodebeløp != null && + this.differanseberegnetPeriodebeløp <= 0 + + private fun finnRelevanteVilkårsresulaterForRegelverk( + personResultater: Set, + ): List = + personResultater + .filter { !it.erSøkersResultater() } + .filter { this.aktør == it.aktør } + .flatMap { it.vilkårResultater } + .filter { + this.stønadFom > (it.periodeFom ?: TIDENES_MORGEN).toYearMonth() && + (it.periodeTom == null || this.stønadFom <= it.periodeTom?.toYearMonth()) + } + .filter { vilkårResultat -> + regelverkavhenigeVilkår().any { it == vilkårResultat.vilkårType } + } +} + +fun List.slåSammenBack2BackAndelsperioderMedSammeBeløp(): List { + if (this.size <= 1) return this + val sorterteAndeler = this.sortedBy { it.stønadFom } + val sammenslåtteAndeler = mutableListOf() + var andel = sorterteAndeler.firstOrNull() + sorterteAndeler.forEach { andelTilkjentYtelse -> + andel = andel ?: andelTilkjentYtelse + val back2BackAndelsperiodeMedSammeBeløp = this.singleOrNull { + andel!!.stønadTom.plusMonths(1).equals(it.stønadFom) && + andel!!.aktør == it.aktør && + andel!!.kalkulertUtbetalingsbeløp == it.kalkulertUtbetalingsbeløp && + andel!!.type == it.type + } + andel = if (back2BackAndelsperiodeMedSammeBeløp != null) { + andel!!.copy(stønadTom = back2BackAndelsperiodeMedSammeBeløp.stønadTom) + } else { + sammenslåtteAndeler.add(andel!!) + null + } + } + if (andel != null) sammenslåtteAndeler.add(andel!!) + return sammenslåtteAndeler +} + +fun List.lagVertikaleSegmenter(): Map, List> { + return this.utledSegmenter() + .fold(mutableMapOf()) { acc, segment -> + val andelerForSegment = this.filter { + segment.localDateInterval.overlaps( + LocalDateInterval( + it.stønadFom.førsteDagIInneværendeMåned(), + it.stønadTom.sisteDagIInneværendeMåned(), + ), + ) + } + acc[segment] = andelerForSegment + acc + } +} + +enum class YtelseType(val klassifisering: String) { + ORDINÆR_BARNETRYGD("BATR"), + UTVIDET_BARNETRYGD("BATR"), + SMÅBARNSTILLEGG("BATRSMA"), + ; + + fun erKnyttetTilSøker() = this == SMÅBARNSTILLEGG || this == UTVIDET_BARNETRYGD + + fun hentSatsTyper(): List = when (this) { + ORDINÆR_BARNETRYGD -> listOf(SatsType.ORBA, SatsType.TILLEGG_ORBA) + UTVIDET_BARNETRYGD -> listOf(SatsType.UTVIDET_BARNETRYGD) + SMÅBARNSTILLEGG -> listOf(SatsType.SMA) + } + + fun tilYtelseType(): YtelsetypeBA = when (this) { + ORDINÆR_BARNETRYGD -> YtelsetypeBA.ORDINÆR_BARNETRYGD + UTVIDET_BARNETRYGD -> YtelsetypeBA.UTVIDET_BARNETRYGD + SMÅBARNSTILLEGG -> YtelsetypeBA.SMÅBARNSTILLEGG + } + + fun tilSatsType(person: Person, ytelseDato: LocalDate) = when (this) { + ORDINÆR_BARNETRYGD -> if (ytelseDato.toYearMonth() < person.hentSeksårsdag().toYearMonth()) { + SatsType.TILLEGG_ORBA + } else { + SatsType.ORBA + } + + UTVIDET_BARNETRYGD -> SatsType.UTVIDET_BARNETRYGD + SMÅBARNSTILLEGG -> SatsType.SMA + } +} + +private fun regelverkavhenigeVilkår(): List { + return listOf( + Vilkår.BOR_MED_SØKER, + Vilkår.BOSATT_I_RIKET, + Vilkår.LOVLIG_OPPHOLD, + ) +} + +fun List.hentAndelerForSegment( + vertikaltSegmentForVedtaksperiode: LocalDateSegment, +) = this.filter { + vertikaltSegmentForVedtaksperiode.localDateInterval.overlaps( + LocalDateInterval( + it.stønadFom.førsteDagIInneværendeMåned(), + it.stønadTom.sisteDagIInneværendeMåned(), + ), + ) +} + +fun List.tilTidslinjerPerPersonOgType(): Map, AndelTilkjentYtelseTidslinje> = + groupBy { Pair(it.aktør, it.type) }.mapValues { (_, andelerTilkjentYtelsePåPerson) -> + AndelTilkjentYtelseTidslinje( + andelerTilkjentYtelsePåPerson, + ) + } + +fun List.tilTidslinjerPerAktørOgType() = + groupBy { Pair(it.aktør, it.type) }.mapValues { (_, andelerTilkjentYtelsePåPerson) -> + AndelTilkjentYtelseForVedtaksperioderTidslinje( + andelerTilkjentYtelsePåPerson, + ) + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/AndelTilkjentYtelseRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/AndelTilkjentYtelseRepository.kt new file mode 100644 index 000000000..0c13bee63 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/AndelTilkjentYtelseRepository.kt @@ -0,0 +1,107 @@ +package no.nav.familie.ba.sak.kjerne.beregning.domene + +import io.micrometer.core.annotation.Timed +import no.nav.familie.ba.sak.ekstern.skatteetaten.AndelTilkjentYtelsePeriode +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import java.time.LocalDateTime +import java.time.YearMonth + +interface AndelTilkjentYtelseRepository : JpaRepository { + + @Query(value = "SELECT aty FROM AndelTilkjentYtelse aty WHERE aty.behandlingId IN :behandlingIder") + fun finnAndelerTilkjentYtelseForBehandlinger(behandlingIder: List): List + + @Query(value = "SELECT aty FROM AndelTilkjentYtelse aty WHERE aty.behandlingId = :behandlingId") + fun finnAndelerTilkjentYtelseForBehandling(behandlingId: Long): List + + @Query(value = "SELECT aty FROM AndelTilkjentYtelse aty WHERE aty.behandlingId = :behandlingId AND aty.aktør = :barnAktør") + fun finnAndelerTilkjentYtelseForBehandlingOgBarn(behandlingId: Long, barnAktør: Aktør): List + + @Query(value = "SELECT aty from AndelTilkjentYtelse aty WHERE aty.aktør = :aktør") + fun finnAndelerTilkjentYtelseForAktør(aktør: Aktør): List + + @Query(value = "SELECT aty FROM AndelTilkjentYtelse aty WHERE aty.behandlingId IN :behandlingIder AND aty.stønadTom >= :avstemmingstidspunkt") + fun finnLøpendeAndelerTilkjentYtelseForBehandlinger( + behandlingIder: List, + avstemmingstidspunkt: YearMonth, + ): List + + @Query( + """ + SELECT aty.id AS id, + p.foedselsnummer AS ident, + aty.stonad_fom AS fom, + aty.stonad_tom AS tom, + aty.prosent AS prosent, + ty.endret_dato AS endretdato, + aty.fk_behandling_id AS behandlingid + FROM andel_tilkjent_ytelse aty + INNER JOIN + tilkjent_ytelse ty ON aty.tilkjent_ytelse_id = ty.id + INNER JOIN + personident p ON aty.fk_aktoer_id = p.fk_aktoer_id + WHERE aty.tilkjent_ytelse_id IN ( + SELECT MAX(ty.id) + FROM andel_tilkjent_ytelse aty + INNER JOIN + tilkjent_ytelse ty ON aty.tilkjent_ytelse_id = ty.id + INNER JOIN + personident p ON aty.fk_aktoer_id = p.fk_aktoer_id + WHERE p.foedselsnummer IN :personIdenter + AND ty.utbetalingsoppdrag IS NOT NULL + GROUP BY p.foedselsnummer + ) + AND aty.type = 'UTVIDET_BARNETRYGD' + AND aty.stonad_fom <= :tom + AND aty.stonad_tom >= :fom + """, + nativeQuery = true, + ) + @Timed + fun finnPerioderMedUtvidetBarnetrygdForPersoner( + personIdenter: List, + fom: LocalDateTime, + tom: LocalDateTime, + ): List + + @Query( + """ + SELECT DISTINCT p.foedselsnummer AS ident + FROM andel_tilkjent_ytelse aty + INNER JOIN tilkjent_ytelse ty ON aty.fk_behandling_id = ty.fk_behandling_id + INNER JOIN behandling b ON aty.fk_behandling_id = b.id + INNER JOIN fagsak f ON b.fk_fagsak_id = f.id + INNER JOIN personident p ON f.fk_aktoer_id = p.fk_aktoer_id + WHERE p.aktiv = true + AND ty.utbetalingsoppdrag is not null + AND EXTRACT('Year' FROM aty.stonad_fom) <= CAST(:år AS INTEGER ) + AND EXTRACT('Year' FROM aty.stonad_tom) >= CAST(:år AS INTEGER ); + """, + nativeQuery = true, + ) + @Timed + fun finnIdenterMedLøpendeBarnetrygdForGittÅr( + år: Int, + ): List + + @Query( + """ + WITH andeler AS ( + SELECT + aty.id, + row_number() OVER (PARTITION BY aty.type, aty.fk_aktoer_id ORDER BY aty.periode_offset DESC) rn + FROM andel_tilkjent_ytelse aty + JOIN tilkjent_ytelse ty ON ty.id = aty.tilkjent_ytelse_id + JOIN Behandling b ON b.id = aty.fk_behandling_id + WHERE b.fk_fagsak_id = :fagsakId + AND ty.utbetalingsoppdrag IS NOT NULL + AND aty.periode_offset IS NOT NULL + AND b.status = 'AVSLUTTET') + SELECT aty.* FROM andel_tilkjent_ytelse aty WHERE id IN (SELECT id FROM andeler WHERE rn = 1) + """, + nativeQuery = true, + ) + fun hentSisteAndelPerIdentOgType(fagsakId: Long): List +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/AndelerTilkjentYtelseOgEndreteUtbetalingerService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/AndelerTilkjentYtelseOgEndreteUtbetalingerService.kt new file mode 100644 index 000000000..80da2bea7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/AndelerTilkjentYtelseOgEndreteUtbetalingerService.kt @@ -0,0 +1,180 @@ +package no.nav.familie.ba.sak.kjerne.beregning.domene + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.overlapperHeltEllerDelvisMed +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseUtils.skalAndelerSlåsSammen +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerPeriodeInnenforTilkjentytelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerÅrsak +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndelRepository +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AndelerTilkjentYtelseOgEndreteUtbetalingerService( + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val endretUtbetalingAndelRepository: EndretUtbetalingAndelRepository, + private val vilkårsvurderingRepository: VilkårsvurderingRepository, +) { + @Transactional + fun finnAndelerTilkjentYtelseMedEndreteUtbetalinger(behandlingId: Long): List { + return lagKombinator(behandlingId).lagAndelerMedEndringer() + } + + @Transactional + fun finnEndreteUtbetalingerMedAndelerTilkjentYtelse(behandlingId: Long): List { + // Hvis noen valideringer feiler, så signalerer vi det til frontend ved å fjerne tilknyttede andeler + // SB vil få en feilmelding og løsningen blir å slette eller oppdatere endringen + // Da vil forhåpentligvis valideringen være ok, koblingene til andelene være beholdt + return lagKombinator(behandlingId).lagEndreteUtbetalingMedAndeler() + .map { + it.utenAndelerVedValideringsfeil { + validerPeriodeInnenforTilkjentytelse( + it.endretUtbetalingAndel, + it.andelerTilkjentYtelse, + ) + }.utenAndelerVedValideringsfeil { + validerÅrsak( + it.endretUtbetalingAndel, + vilkårsvurderingRepository.findByBehandlingAndAktiv(behandlingId), + ) + } + } + } + + private fun lagKombinator(behandlingId: Long) = + AndelTilkjentYtelseOgEndreteUtbetalingerKombinator( + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId), + endretUtbetalingAndelRepository.findByBehandlingId(behandlingId), + ) +} + +private class AndelTilkjentYtelseOgEndreteUtbetalingerKombinator( + private val andelerTilkjentYtelse: Collection, + private val endretUtbetalingAndeler: Collection, +) { + fun lagAndelerMedEndringer(): List { + return andelerTilkjentYtelse.map { lagAndelMedEndringer(it) } + } + + fun lagEndreteUtbetalingMedAndeler(): List { + return endretUtbetalingAndeler.map { lagEndringMedAndeler(it) } + } + + private fun lagAndelMedEndringer(andelTilkjentYtelse: AndelTilkjentYtelse): AndelTilkjentYtelseMedEndreteUtbetalinger { + val endreteUtbetalinger = endretUtbetalingAndeler + .filter { overlapper(andelTilkjentYtelse, it) } + + return AndelTilkjentYtelseMedEndreteUtbetalinger( + andelTilkjentYtelse, + endreteUtbetalinger, + ) + } + + private fun lagEndringMedAndeler(endretUtbetalingAndel: EndretUtbetalingAndel): EndretUtbetalingAndelMedAndelerTilkjentYtelse { + val andeler = andelerTilkjentYtelse + .filter { overlapper(it, endretUtbetalingAndel) } + + return EndretUtbetalingAndelMedAndelerTilkjentYtelse( + endretUtbetalingAndel, + andeler, + ) + } + + private fun overlapper( + andelTilkjentYtelse: AndelTilkjentYtelse, + endretUtbetalingAndel: EndretUtbetalingAndel, + ): Boolean { + return andelTilkjentYtelse.aktør == endretUtbetalingAndel.person?.aktør && + endretUtbetalingAndel.fom != null && endretUtbetalingAndel.tom != null && + endretUtbetalingAndel.periode.overlapperHeltEllerDelvisMed(andelTilkjentYtelse.periode) + } +} + +data class AndelTilkjentYtelseMedEndreteUtbetalinger internal constructor( + private val andelTilkjentYtelse: AndelTilkjentYtelse, + private val endreteUtbetalingerAndeler: Collection, +) { + val periodeOffset get() = andelTilkjentYtelse.periodeOffset + val sats get() = andelTilkjentYtelse.sats + val type get() = andelTilkjentYtelse.type + val kalkulertUtbetalingsbeløp get() = andelTilkjentYtelse.kalkulertUtbetalingsbeløp + val aktør get() = andelTilkjentYtelse.aktør + fun erSøkersAndel() = andelTilkjentYtelse.erSøkersAndel() + fun erSmåbarnstillegg() = andelTilkjentYtelse.erSmåbarnstillegg() + fun erUtvidet(): Boolean = andelTilkjentYtelse.erUtvidet() + fun erAndelSomSkalSendesTilOppdrag() = andelTilkjentYtelse.erAndelSomSkalSendesTilOppdrag() + fun overlapperPeriode(månedPeriode: MånedPeriode) = andelTilkjentYtelse.overlapperPeriode(månedPeriode) + fun slåSammenMed(naboAndel: AndelTilkjentYtelseMedEndreteUtbetalinger): AndelTilkjentYtelseMedEndreteUtbetalinger { + // Skal allerede være sjekket at disse er naboer som kan slås sammen, bla. at de eventuelt har samme endringsperiode + // Dermed skal en en enkel utvidelse med stønadTom fra naboen fungere + check(skalAndelerSlåsSammen(this, naboAndel)) + return AndelTilkjentYtelseMedEndreteUtbetalinger( + andelTilkjentYtelse.copy(stønadTom = naboAndel.stønadTom), + endreteUtbetalinger, + ) + } + + val stønadFom get() = andelTilkjentYtelse.stønadFom + val stønadTom get() = andelTilkjentYtelse.stønadTom + val prosent get() = andelTilkjentYtelse.prosent + val andel get() = andelTilkjentYtelse + val endreteUtbetalinger = endreteUtbetalingerAndeler + + companion object { + fun utenEndringer(andelTilkjentYtelse: AndelTilkjentYtelse): AndelTilkjentYtelseMedEndreteUtbetalinger = + AndelTilkjentYtelseMedEndreteUtbetalinger( + andelTilkjentYtelse = andelTilkjentYtelse, + endreteUtbetalingerAndeler = emptyList(), + ) + } +} + +data class EndretUtbetalingAndelMedAndelerTilkjentYtelse( + val endretUtbetalingAndel: EndretUtbetalingAndel, + private val andeler: List, +) { + fun overlapperMed(månedPeriode: MånedPeriode) = endretUtbetalingAndel.overlapperMed(månedPeriode) + fun årsakErDeltBosted() = endretUtbetalingAndel.årsakErDeltBosted() + + val periode get() = endretUtbetalingAndel.periode + val person get() = endretUtbetalingAndel.person + val begrunnelse get() = endretUtbetalingAndel.begrunnelse + val søknadstidspunkt get() = endretUtbetalingAndel.søknadstidspunkt + val avtaletidspunktDeltBosted get() = endretUtbetalingAndel.avtaletidspunktDeltBosted + val prosent get() = endretUtbetalingAndel.prosent + val aktivtFødselsnummer get() = endretUtbetalingAndel.person?.aktør?.aktivFødselsnummer() + val årsak get() = endretUtbetalingAndel.årsak + val id get() = endretUtbetalingAndel.id + val fom get() = endretUtbetalingAndel.fom + val tom get() = endretUtbetalingAndel.tom + val andelerTilkjentYtelse = andeler +} + +/** + * Fjerner andelene hvis det funksjonen som sendes inn kaster en exception + * Brukes som en wrapper rundt en del valideringsfunksjoner som kaster exception når ting ikke validerer + * Manglende andeler brukes et par steder som et signal om at noe er feil + */ +private fun EndretUtbetalingAndelMedAndelerTilkjentYtelse.utenAndelerVedValideringsfeil( + validator: () -> Unit, +) = try { + validator() + this +} catch (e: FunksjonellFeil) { + this.copy(andeler = emptyList()) +} + +/** + * Hjelpefunksjon som oppretter AndelTilkjentYtelseMedEndreteUtbetalinger fra AndelTilkjentYtelse og legger til en endring. + * Utnytter at vet om funksjonsbryteren er satt + * og viderefører den til den opprettede AndelTilkjentYtelseMedEndreteUtbetalinger + */ +fun AndelTilkjentYtelse.medEndring( + endretUtbetalingAndelMedAndelerTilkjentYtelse: EndretUtbetalingAndelMedAndelerTilkjentYtelse, +) = AndelTilkjentYtelseMedEndreteUtbetalinger( + andelTilkjentYtelse = this, + endreteUtbetalingerAndeler = listOf(endretUtbetalingAndelMedAndelerTilkjentYtelse.endretUtbetalingAndel), +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/InternPeriodeOvergangsst\303\270nad.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/InternPeriodeOvergangsst\303\270nad.kt" new file mode 100644 index 000000000..2280ae11a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/InternPeriodeOvergangsst\303\270nad.kt" @@ -0,0 +1,85 @@ +package no.nav.familie.ba.sak.kjerne.beregning.domene + +import no.nav.familie.ba.sak.common.forrigeMåned +import no.nav.familie.ba.sak.common.isSameOrBefore +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.grunnlag.småbarnstillegg.PeriodeOvergangsstønadGrunnlag +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.kontrakter.felles.ef.EksternPeriode +import java.time.LocalDate + +data class InternPeriodeOvergangsstønad( + val personIdent: String, + val fomDato: LocalDate, + val tomDato: LocalDate, +) { + constructor(periodeOvergangsstønadGrunnlag: PeriodeOvergangsstønadGrunnlag) : this( + personIdent = periodeOvergangsstønadGrunnlag.aktør.aktivFødselsnummer(), + fomDato = periodeOvergangsstønadGrunnlag.fom, + tomDato = periodeOvergangsstønadGrunnlag.tom, + ) +} + +fun EksternPeriode.tilInternPeriodeOvergangsstønad() = InternPeriodeOvergangsstønad( + personIdent = this.personIdent, + fomDato = this.fomDato, + tomDato = this.tomDato, +) + +fun List.slåSammenTidligerePerioder( + dagensDato: LocalDate, +): List { + val tidligerePerioder = this.filter { it.fomDato.isSameOrBefore(dagensDato) } + + val nyePerioder = this.minus(tidligerePerioder) + return tidligerePerioder.slåSammenSammenhengendePerioder() + nyePerioder +} + +fun List.slåSammenSammenhengendePerioder(): List { + return this.sortedBy { it.fomDato } + .fold(mutableListOf()) { sammenslåttePerioder, nestePeriode -> + if (sammenslåttePerioder.lastOrNull()?.tomDato?.toYearMonth() == nestePeriode.fomDato.forrigeMåned() + ) { + sammenslåttePerioder.apply { add(removeLast().copy(tomDato = nestePeriode.tomDato)) } + } else { + sammenslåttePerioder.apply { add(nestePeriode) } + } + } +} + +/*** + * Dersom vi i en behandling har overgangsstønad i tre måneder: + * |OOO-----| + * som fører til småbarnstillegg. + * Og så utvides overgangsstønadsperioden til fem måneder: + * |OOOOO---| + * som fører til småbarnstillegg i alle månedene. + * Ønsker vi å kunne begrunne de to siste månedene med småbarnstillegg. + * Splitter derfor opp overgangsstønads-perioden slik at vi kan begrunne endringen for søker i riktig periode. + * |OOO-----| + * |---OO---| + * + ***/ +fun List.splitFramtidigePerioderFraForrigeBehandling( + overgangsstønadPerioderFraForrigeBehandling: List, + dagensDato: LocalDate, +): List { + val tidligerePerioder = this.filter { it.tomDato.isSameOrBefore(dagensDato) } + val framtidigePerioder = this.minus(tidligerePerioder) + val nyeOvergangsstønadTidslinje = InternPeriodeOvergangsstønadTidslinje(framtidigePerioder) + + val gammelOvergangsstønadTidslinje = + InternPeriodeOvergangsstønadTidslinje(overgangsstønadPerioderFraForrigeBehandling) + + val oppsplittedeFramtigigePerioder = gammelOvergangsstønadTidslinje + .kombinerMed(nyeOvergangsstønadTidslinje) { gammelOvergangsstønadPeriode, nyOvergangsstønadPeriode -> + if (nyOvergangsstønadPeriode == null) { + null + } else { + gammelOvergangsstønadPeriode ?: nyOvergangsstønadPeriode + } + } + .lagInternePerioderOvergangsstønad() + + return tidligerePerioder + oppsplittedeFramtigigePerioder +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/InternPeriodeOvergangsst\303\270nadTidslinje.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/InternPeriodeOvergangsst\303\270nadTidslinje.kt" new file mode 100644 index 000000000..c66afa658 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/InternPeriodeOvergangsst\303\270nadTidslinje.kt" @@ -0,0 +1,32 @@ +package no.nav.familie.ba.sak.kjerne.beregning.domene + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligSent +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligTidlig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilFørsteDagIMåneden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilSisteDagIMåneden + +open class InternPeriodeOvergangsstønadTidslinje( + private val internePeriodeOvergangsstønader: List, +) : Tidslinje() { + + override fun lagPerioder(): List> { + return internePeriodeOvergangsstønader.map { + Periode( + fraOgMed = it.fomDato.tilTidspunktEllerUendeligTidlig(it.tomDato), + tilOgMed = it.tomDato.tilTidspunktEllerUendeligSent(it.fomDato), + innhold = it, + ) + } + } +} + +fun Tidslinje.lagInternePerioderOvergangsstønad(): List = + this.perioder().mapNotNull { + it.innhold?.copy( + fomDato = it.fraOgMed.tilFørsteDagIMåneden().tilLocalDate(), + tomDato = it.tilOgMed.tilSisteDagIMåneden().tilLocalDate(), + ) + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/Sats.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/Sats.kt new file mode 100644 index 000000000..6784e083c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/Sats.kt @@ -0,0 +1,28 @@ +package no.nav.familie.ba.sak.kjerne.beregning.domene + +import no.nav.familie.ba.sak.common.Feil +import java.time.LocalDate + +data class Sats( + val type: SatsType, + val beløp: Int, + val gyldigFom: LocalDate = LocalDate.MIN, + val gyldigTom: LocalDate = LocalDate.MAX, +) + +enum class SatsType(val beskrivelse: String) { + ORBA("Ordinær barnetrygd"), + SMA("Småbarnstillegg"), + TILLEGG_ORBA("Tillegg til barnetrygd for barn 0-6 år"), + FINN_SVAL("Finnmark- og Svalbardtillegg"), + UTVIDET_BARNETRYGD("Utvidet barnetrygd"), + ; + + fun tilYtelseType(): YtelseType = when (this) { + ORBA -> YtelseType.ORDINÆR_BARNETRYGD + SMA -> YtelseType.SMÅBARNSTILLEGG + TILLEGG_ORBA -> YtelseType.ORDINÆR_BARNETRYGD + FINN_SVAL -> throw Feil("FINN_SVAL har ikke noen tilsvarende ytelsestype") + UTVIDET_BARNETRYGD -> YtelseType.UTVIDET_BARNETRYGD + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/TilkjentYtelse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/TilkjentYtelse.kt new file mode 100644 index 000000000..861185846 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/TilkjentYtelse.kt @@ -0,0 +1,127 @@ +package no.nav.familie.ba.sak.kjerne.beregning.domene + +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.OneToMany +import jakarta.persistence.OneToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.YearMonthConverter +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.fpsak.tidsserie.LocalDateSegment +import no.nav.fpsak.tidsserie.LocalDateTimeline +import no.nav.fpsak.tidsserie.StandardCombinators +import java.time.LocalDate +import java.time.YearMonth + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "TilkjentYtelse") +@Table(name = "TILKJENT_YTELSE") +data class TilkjentYtelse( + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tilkjent_ytelse_seq_generator") + @SequenceGenerator( + name = "tilkjent_ytelse_seq_generator", + sequenceName = "tilkjent_ytelse_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @OneToOne(optional = false) + @JoinColumn(name = "fk_behandling_id", nullable = false, updatable = false) + val behandling: Behandling, + + @Column(name = "stonad_fom", nullable = true, columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + var stønadFom: YearMonth? = null, + + @Column(name = "stonad_tom", nullable = true, columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + var stønadTom: YearMonth? = null, + + @Column(name = "opphor_fom", nullable = true, columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + var opphørFom: YearMonth? = null, + + @Column(name = "opprettet_dato", nullable = false) + val opprettetDato: LocalDate, + + @Column(name = "endret_dato", nullable = false) + var endretDato: LocalDate, + + @Column(name = "utbetalingsoppdrag", columnDefinition = "TEXT") + var utbetalingsoppdrag: String? = null, + + @OneToMany( + fetch = FetchType.EAGER, + mappedBy = "tilkjentYtelse", + cascade = [CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.MERGE, CascadeType.REMOVE], + orphanRemoval = true, + ) + val andelerTilkjentYtelse: MutableSet = mutableSetOf(), +) + +private fun kombinerAndeler( + lhs: LocalDateTimeline>, + rhs: LocalDateTimeline, +): LocalDateTimeline> { + return lhs.combine( + rhs, + { datoIntervall, sammenlagt, neste -> + StandardCombinators.allValues( + datoIntervall, + sammenlagt, + neste, + ) + }, + LocalDateTimeline.JoinStyle.CROSS_JOIN, + ) +} + +fun lagTidslinjeMedOverlappendePerioderForAndeler(tidslinjer: List>): LocalDateTimeline> { + if (tidslinjer.isEmpty()) return LocalDateTimeline(emptyList()) + + val førsteSegment = tidslinjer.first().toSegments().first() + val initiellSammenlagt = + LocalDateTimeline(listOf(LocalDateSegment(førsteSegment.fom, førsteSegment.tom, listOf(førsteSegment.value)))) + val resterende = tidslinjer.drop(1) + + return resterende.fold(initiellSammenlagt) { sammenlagt, neste -> + kombinerAndeler(sammenlagt, neste) + } +} + +fun TilkjentYtelse.tilTidslinjeMedAndeler(): LocalDateTimeline> { + val tidslinjer = this.andelerTilkjentYtelse.map { andelTilkjentYtelse -> + LocalDateTimeline( + listOf( + LocalDateSegment( + andelTilkjentYtelse.stønadFom.førsteDagIInneværendeMåned(), + andelTilkjentYtelse.stønadTom.sisteDagIInneværendeMåned(), + andelTilkjentYtelse, + ), + ), + ) + } + + return lagTidslinjeMedOverlappendePerioderForAndeler(tidslinjer) +} + +fun TilkjentYtelse.utbetalingsoppdrag(): Utbetalingsoppdrag? = + objectMapper.readValue(this.utbetalingsoppdrag, Utbetalingsoppdrag::class.java) + +fun TilkjentYtelse.utbetalingsperioder() = this.utbetalingsoppdrag()?.utbetalingsperiode ?: emptyList() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/TilkjentYtelseRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/TilkjentYtelseRepository.kt new file mode 100644 index 000000000..312dbc31b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/domene/TilkjentYtelseRepository.kt @@ -0,0 +1,24 @@ +package no.nav.familie.ba.sak.kjerne.beregning.domene + +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query + +interface TilkjentYtelseRepository : JpaRepository { + @Modifying + @Query("DELETE FROM TilkjentYtelse ty WHERE ty.behandling = :behandling") + fun slettTilkjentYtelseFor(behandling: Behandling) + + @Query("SELECT ty FROM TilkjentYtelse ty JOIN ty.behandling b WHERE b.id = :behandlingId") + fun findByBehandling(behandlingId: Long): TilkjentYtelse + + @Query("SELECT ty FROM TilkjentYtelse ty JOIN ty.behandling b WHERE b.id = :behandlingId") + fun findByBehandlingOptional(behandlingId: Long): TilkjentYtelse? + + @Query("SELECT ty FROM TilkjentYtelse ty JOIN ty.behandling b WHERE b.id = :behandlingId AND ty.utbetalingsoppdrag is not null") + fun findByBehandlingAndHasUtbetalingsoppdrag(behandlingId: Long): TilkjentYtelse? + + @Query("select ty from TilkjentYtelse ty where DATE(ty.endretDato) > '2023-08-22 00:00:00.000000' and Date(ty.endretDato) < '2023-08-25 00:00:00.000000' and ty.utbetalingsoppdrag is not null and ty.opphørFom is not null") + fun findTilkjentYtelseMedFeilUtbetalingsoppdrag(): List +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/endringstidspunkt/EndringstidspunktController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/endringstidspunkt/EndringstidspunktController.kt new file mode 100644 index 000000000..9185a557c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/endringstidspunkt/EndringstidspunktController.kt @@ -0,0 +1,30 @@ +package no.nav.familie.ba.sak.kjerne.beregning.endringstidspunkt + +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate + +@RestController +@RequestMapping("/api") +@ProtectedWithClaims(issuer = "azuread") +class EndringstidspunktController( + val vedtaksperiodeService: VedtaksperiodeService, + val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, +) { + @GetMapping("/behandlinger/{behandlingId}/endringstidspunkt") + fun hentEndringstidspunkt( + @PathVariable behandlingId: Long, + ): ResponseEntity> = ResponseEntity.ok( + Ressurs.success( + behandlingHentOgPersisterService.hent(behandlingId).overstyrtEndringstidspunkt + ?: vedtaksperiodeService.finnEndringstidspunktForBehandling(behandlingId), + ), + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/endringstidspunkt/EndringstidspunktUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/endringstidspunkt/EndringstidspunktUtils.kt new file mode 100644 index 000000000..f7c5e136a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/beregning/endringstidspunkt/EndringstidspunktUtils.kt @@ -0,0 +1,151 @@ +package no.nav.familie.ba.sak.kjerne.beregning.endringstidspunkt + +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.hentUtbetalingstidslinjeForSøker +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.fpsak.tidsserie.LocalDateSegment +import no.nav.fpsak.tidsserie.LocalDateTimeline +import no.nav.fpsak.tidsserie.StandardCombinators +import java.time.LocalDate + +enum class BehandlingAlder { + NY, + GAMMEL, +} + +typealias Beløpsdifferanse = Int +typealias AktørId = String + +data class AndelTilkjentYtelseDataForÅKalkulereEndring( + val aktørId: AktørId, + val kalkulertBeløp: Int, + val endretUtbetalingÅrsaker: List<Årsak>, + val behandlingAlder: BehandlingAlder, +) + +fun List.hentPerioderMedEndringerFra( + forrigeAndelerTilkjentYtelse: List, +): Map> { + val andelerTidslinje = this.hentTidslinjerForPersoner(BehandlingAlder.NY) + val forrigeAndelerTidslinje = + forrigeAndelerTilkjentYtelse.hentTidslinjerForPersoner(BehandlingAlder.GAMMEL) + + val personerFraForrigeEllerDenneBehandlinger = + (this.map { it.aktør.aktørId } + forrigeAndelerTilkjentYtelse.map { it.aktør.aktørId }).toSet() + + return personerFraForrigeEllerDenneBehandlinger.associateWith { aktørId -> + val tidslinjeForPerson = andelerTidslinje[aktørId] ?: LocalDateTimeline(emptyList()) + val forrigeTidslinjeForPerson = forrigeAndelerTidslinje[aktørId] ?: LocalDateTimeline(emptyList()) + + val kombinertTidslinje = tidslinjeForPerson.combine( + forrigeTidslinjeForPerson, + StandardCombinators::bothValues, + LocalDateTimeline.JoinStyle.CROSS_JOIN, + ) as LocalDateTimeline> + + LocalDateTimeline( + kombinertTidslinje.toSegments().mapNotNull { it.tilSegmentMedEndringer() }, + ) + }.filter { it.value.toSegments().isNotEmpty() } +} + +private fun LocalDateSegment>.tilSegmentMedEndringer(): LocalDateSegment? { + val erEndring = erEndringPåPersonISegment(this.value) + + return if (erEndring) { + LocalDateSegment( + this.localDateInterval, + hentBeløpsendringPåPersonISegment(this.value), + ) + } else { + null + } +} + +private fun erEndringPåPersonISegment(nyOgGammelDataPåBrukerISegmentet: List): Boolean { + val nyttBeløp = nyOgGammelDataPåBrukerISegmentet.finnKalkulertBeløp(BehandlingAlder.NY) + val gammeltBeløp = nyOgGammelDataPåBrukerISegmentet.finnKalkulertBeløp(BehandlingAlder.GAMMEL) + + val nyEndretUtbetalingÅrsaker = + nyOgGammelDataPåBrukerISegmentet.find { it.behandlingAlder == BehandlingAlder.NY }?.endretUtbetalingÅrsaker?.sorted() + val gammelEndretUtbetalingÅrsaker = + nyOgGammelDataPåBrukerISegmentet.find { it.behandlingAlder == BehandlingAlder.GAMMEL }?.endretUtbetalingÅrsaker?.sorted() + + return nyttBeløp != gammeltBeløp || nyEndretUtbetalingÅrsaker != gammelEndretUtbetalingÅrsaker +} + +private fun hentBeløpsendringPåPersonISegment(nyOgGammelDataPåBrukerISegmentet: List): Int { + val nyttBeløp = nyOgGammelDataPåBrukerISegmentet.finnKalkulertBeløp(BehandlingAlder.NY) ?: 0 + val gammeltBeløp = nyOgGammelDataPåBrukerISegmentet.finnKalkulertBeløp(BehandlingAlder.GAMMEL) ?: 0 + + return nyttBeløp - gammeltBeløp +} + +private fun List.finnKalkulertBeløp(behandlingAlder: BehandlingAlder) = + singleOrNull { it.behandlingAlder == behandlingAlder } + ?.kalkulertBeløp + +private fun List.hentTidslinjerForPersoner(behandlingAlder: BehandlingAlder): Map> { + return this.groupBy { it.aktør.aktørId } + .map { (aktørId, andeler) -> + if (andeler.any { it.erSøkersAndel() }) { + aktørId to kombinerOverlappendeAndelerForSøker( + andeler = andeler, + behandlingAlder = behandlingAlder, + aktørId = aktørId, + ) + } else { + aktørId to andeler.hentTidslinje(behandlingAlder) + } + }.toMap() +} + +private fun List.hentTidslinje( + behandlingAlder: BehandlingAlder, +): LocalDateTimeline = LocalDateTimeline( + map { + LocalDateSegment( + it.stønadFom.førsteDagIInneværendeMåned(), + it.stønadTom.sisteDagIInneværendeMåned(), + AndelTilkjentYtelseDataForÅKalkulereEndring( + aktørId = it.aktør.aktørId, + kalkulertBeløp = it.kalkulertUtbetalingsbeløp, + endretUtbetalingÅrsaker = it.endreteUtbetalinger.mapNotNull { endretUtbetalingAndel -> endretUtbetalingAndel.årsak }, + behandlingAlder = behandlingAlder, + ), + ) + }, +) + +private fun kombinerOverlappendeAndelerForSøker( + andeler: List, + behandlingAlder: BehandlingAlder, + aktørId: AktørId, +): LocalDateTimeline { + val segmenter = hentUtbetalingstidslinjeForSøker(andeler).toSegments() + + return LocalDateTimeline( + segmenter.map { + LocalDateSegment( + it.localDateInterval, + AndelTilkjentYtelseDataForÅKalkulereEndring( + aktørId = aktørId, + behandlingAlder = behandlingAlder, + endretUtbetalingÅrsaker = emptyList(), // TODO() her bør man nok prøve å hente overstyringer på søker også, men haster mest å fikse endringstidspunkt pga overstyringer på barn. + kalkulertBeløp = it.value, + ), + ) + }, + ) +} + +fun List.filtrerLikEllerEtterEndringstidspunkt( + endringstidspunkt: LocalDate, +): List { + return filter { (it.tom ?: TIDENES_ENDE).isSameOrAfter(endringstidspunkt) } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BegrunnelseUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BegrunnelseUtil.kt new file mode 100644 index 000000000..a87cef02f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BegrunnelseUtil.kt @@ -0,0 +1,172 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.common.NullablePeriode +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.kjerne.brev.domene.EndretUtbetalingsperiodeDeltBostedTriggere +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertRestEndretAndel +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertUtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.kjerne.brev.domene.RestBehandlingsgrunnlagForBrev +import no.nav.familie.ba.sak.kjerne.brev.domene.harPersonerSomManglerOpplysninger +import no.nav.familie.ba.sak.kjerne.brev.domene.somOverlapper +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.hentPersonerForEtterEndretUtbetalingsperiode +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.TriggesAv +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vedtak.domene.MinimertRestPerson +import no.nav.familie.ba.sak.kjerne.vedtak.domene.barnMedSeksårsdagPåFom +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår + +fun hentPersonidenterGjeldendeForBegrunnelse( + triggesAv: TriggesAv, + begrunnelse: IVedtakBegrunnelse, + periode: NullablePeriode, + vedtakBegrunnelseType: VedtakBegrunnelseType, + vedtaksperiodetype: Vedtaksperiodetype, + restBehandlingsgrunnlagForBrev: RestBehandlingsgrunnlagForBrev, + identerMedUtbetalingPåPeriode: List, + erFørsteVedtaksperiodePåFagsak: Boolean, + identerMedReduksjonPåPeriode: List = emptyList(), + minimerteUtbetalingsperiodeDetaljer: List, + dødeBarnForrigePeriode: List, +): Set { + val erFortsattInnvilgetBegrunnelse = vedtakBegrunnelseType.erFortsattInnvilget() + val erEndretUtbetalingBegrunnelse = vedtakBegrunnelseType == VedtakBegrunnelseType.ENDRET_UTBETALING + val erUtbetalingMedReduksjonFraSistIverksatteBehandling = + vedtaksperiodetype == Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING && vedtakBegrunnelseType.erReduksjon() && !triggesAv.vilkår.contains( + Vilkår.UNDER_18_ÅR, + ) + + fun hentPersonerForUtgjørendeVilkår() = hentPersonerForAlleUtgjørendeVilkår( + minimertePersonResultater = restBehandlingsgrunnlagForBrev.minimertePersonResultater, + vedtaksperiode = Periode( + fom = periode.fom ?: TIDENES_MORGEN, + tom = periode.tom ?: TIDENES_ENDE, + ), + oppdatertBegrunnelseType = vedtakBegrunnelseType, + aktuellePersonerForVedtaksperiode = hentAktuellePersonerForVedtaksperiode( + restBehandlingsgrunnlagForBrev.personerPåBehandling, + vedtakBegrunnelseType, + identerMedUtbetalingPåPeriode, + ), + begrunnelse = begrunnelse, + triggesAv = triggesAv, + erFørsteVedtaksperiodePåFagsak = erFørsteVedtaksperiodePåFagsak, + ).map { person -> person.personIdent } + + return when { + (triggesAv.vilkår.contains(Vilkår.UTVIDET_BARNETRYGD) || triggesAv.småbarnstillegg) && !erEndretUtbetalingBegrunnelse -> hentPersonerForUtvidetOgSmåbarnstilleggBegrunnelse( + identerMedUtbetaling = identerMedUtbetalingPåPeriode, + restBehandlingsgrunnlagForBrev = restBehandlingsgrunnlagForBrev, + periode = periode, + fagsakType = restBehandlingsgrunnlagForBrev.fagsakType, + ) + when { + triggesAv.vilkår.any { it != Vilkår.UTVIDET_BARNETRYGD } -> hentPersonerForUtgjørendeVilkår() + else -> emptyList() + } + + triggesAv.barnMedSeksårsdag -> restBehandlingsgrunnlagForBrev.personerPåBehandling.barnMedSeksårsdagPåFom( + periode.fom, + ).map { person -> person.personIdent } + + triggesAv.personerManglerOpplysninger -> if (restBehandlingsgrunnlagForBrev.minimertePersonResultater.harPersonerSomManglerOpplysninger()) { + emptyList() + } else { + error("Legg til opplysningsplikt ikke oppfylt begrunnelse men det er ikke person med det resultat") + } + + erFortsattInnvilgetBegrunnelse -> identerMedUtbetalingPåPeriode + erEndretUtbetalingBegrunnelse -> hentPersonerForEndretUtbetalingBegrunnelse( + triggesAv = triggesAv, + endredeUtbetalingAndelerSomOverlapperMedPeriode = restBehandlingsgrunnlagForBrev.minimerteEndredeUtbetalingAndeler.filter { + it.erOverlappendeMed( + nullableMånedPeriode = periode.tilNullableMånedPeriode(), + ) + }, + minimerteUtbetalingsperiodeDetaljer = minimerteUtbetalingsperiodeDetaljer, + ) + + erUtbetalingMedReduksjonFraSistIverksatteBehandling -> identerMedReduksjonPåPeriode + + triggesAv.etterEndretUtbetaling -> hentPersonerForEtterEndretUtbetalingsperiode( + minimerteEndredeUtbetalingAndeler = restBehandlingsgrunnlagForBrev.minimerteEndredeUtbetalingAndeler, + fom = periode.fom, + endringsaarsaker = triggesAv.endringsaarsaker, + ) + + triggesAv.barnDød -> dødeBarnForrigePeriode + + else -> hentPersonerForUtgjørendeVilkår() + }.toSet() +} + +private fun hentPersonerForEndretUtbetalingBegrunnelse( + triggesAv: TriggesAv, + endredeUtbetalingAndelerSomOverlapperMedPeriode: List, + minimerteUtbetalingsperiodeDetaljer: List, +): List { + val personerMedRiktigTypeEndringer = + endredeUtbetalingAndelerSomOverlapperMedPeriode.filter { triggesAv.endringsaarsaker.contains(it.årsak) } + .map { it.personIdent } + return minimerteUtbetalingsperiodeDetaljer.filter { it.erPåvirketAvEndring }.filter { + when (triggesAv.endretUtbetalingSkalUtbetales) { + EndretUtbetalingsperiodeDeltBostedTriggere.UTBETALING_IKKE_RELEVANT -> true + EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_UTBETALES -> { + it.utbetaltPerMnd > 0 + } + + EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_IKKE_UTBETALES -> it.utbetaltPerMnd == 0 + } + }.map { it.person.personIdent }.filter { personerMedRiktigTypeEndringer.contains(it) } +} + +/** + * Selv om utvidet kun gjelder for søker ønsker vi å si noe om hvilke barn søker får utvidet for. + * Dette vil være alle barn med utbetaling og alle barn med endret utbetaling i samme periode. + * + * For eksempel om søker oppfyller vilkårene til delt bosted og utvidet barnetrygd, men barnetrygden allerede er + * sendt ut til partner, og delt bosted er endret til at det ikke er noen utbetaling, ønsker vi fremdeles å ta med + * barna uten utbetaling i begrunnelsen. + * + * Søker må med selv om det ikke er utbetaling på søker slik at det blir riktig ved avslag. + */ +private fun hentPersonerForUtvidetOgSmåbarnstilleggBegrunnelse( + identerMedUtbetaling: List, + restBehandlingsgrunnlagForBrev: RestBehandlingsgrunnlagForBrev, + periode: NullablePeriode, + fagsakType: FagsakType, +): List { + val identerFraSammenfallendeEndringsperioder = + restBehandlingsgrunnlagForBrev.minimerteEndredeUtbetalingAndeler.somOverlapper(periode.tilNullableMånedPeriode()) + .map { it.personIdent } + + val søkersIdent = + restBehandlingsgrunnlagForBrev.personerPåBehandling.find { + when (fagsakType) { + FagsakType.NORMAL, + FagsakType.INSTITUSJON, + -> it.type == PersonType.SØKER + + FagsakType.BARN_ENSLIG_MINDREÅRIG -> it.type == PersonType.BARN + } + }?.personIdent + ?: throw IllegalStateException("Søker mangler i behandlingsgrunnlag for brev") + + return identerMedUtbetaling + identerFraSammenfallendeEndringsperioder + søkersIdent +} + +private fun hentAktuellePersonerForVedtaksperiode( + personerPåBehandling: List, + vedtakBegrunnelseType: VedtakBegrunnelseType, + identerMedUtbetalingPåPeriode: List, +): List = personerPåBehandling.filter { person -> + if (vedtakBegrunnelseType.erInnvilget()) { + identerMedUtbetalingPåPeriode.contains(person.personIdent) || person.type == PersonType.SØKER + } else { + true + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevKlient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevKlient.kt new file mode 100644 index 000000000..2b3e5a509 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevKlient.kt @@ -0,0 +1,40 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.common.kallEksternTjeneste +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brev +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BegrunnelseMedData +import no.nav.familie.http.client.AbstractRestClient +import org.springframework.beans.factory.annotation.Value +import org.springframework.cache.annotation.Cacheable +import org.springframework.stereotype.Component +import org.springframework.web.client.RestTemplate +import java.net.URI + +val FAMILIE_BREV_TJENESTENAVN = "famile-brev" + +@Component +class BrevKlient( + @Value("\${FAMILIE_BREV_API_URL}") private val familieBrevUri: String, + @Value("\${SANITY_DATASET}") private val sanityDataset: String, + restTemplate: RestTemplate, +) : AbstractRestClient(restTemplate, "familie-brev") { + + fun genererBrev(målform: String, brev: Brev): ByteArray { + val uri = URI.create("$familieBrevUri/api/$sanityDataset/dokument/$målform/${brev.mal.apiNavn}/pdf") + + secureLogger.info("Kaller familie brev($uri) med data ${brev.data.toBrevString()}") + return kallEksternTjeneste(FAMILIE_BREV_TJENESTENAVN, uri, "Hente pdf for vedtaksbrev") { + postForEntity(uri, brev.data) + } + } + + @Cacheable("begrunnelsestekst", cacheManager = "shortCache") + fun hentBegrunnelsestekst(begrunnelseData: BegrunnelseMedData): String { + val uri = URI.create("$familieBrevUri/ba-sak/begrunnelser/${begrunnelseData.apiNavn}/tekst/") + secureLogger.info("Kaller familie brev($uri) med data $begrunnelseData") + + return kallEksternTjeneste(FAMILIE_BREV_TJENESTENAVN, uri, "Henter begrunnelsestekst") { + postForEntity(uri, begrunnelseData) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevPeriodeGenerator.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevPeriodeGenerator.kt new file mode 100644 index 000000000..e292fa789 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevPeriodeGenerator.kt @@ -0,0 +1,318 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.NullablePeriode +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.erSenereEnnInneværendeMåned +import no.nav.familie.ba.sak.common.førsteDagINesteMåned +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.common.tilMånedÅr +import no.nav.familie.ba.sak.kjerne.brev.domene.BrevBegrunnelseGrunnlagMedPersoner +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertKompetanse +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertUregistrertBarn +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertVedtaksperiode +import no.nav.familie.ba.sak.kjerne.brev.domene.RestBehandlingsgrunnlagForBrev +import no.nav.familie.ba.sak.kjerne.brev.domene.Valgbarhet +import no.nav.familie.ba.sak.kjerne.brev.domene.eøs.EØSBegrunnelseMedKompetanser +import no.nav.familie.ba.sak.kjerne.brev.domene.eøs.hentKompetanserForEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.BrevPeriodeType +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode +import no.nav.familie.ba.sak.kjerne.brev.domene.totaltUtbetalt +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BrevBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.EØSBegrunnelseData +import no.nav.familie.ba.sak.kjerne.vedtak.domene.EØSBegrunnelseDataMedKompetanse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.EØSBegrunnelseDataUtenKompetanse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.FritekstBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.MinimertRestPerson +import no.nav.familie.ba.sak.kjerne.vedtak.domene.hentBrevPeriodeType +import no.nav.familie.ba.sak.kjerne.vedtak.domene.tilBrevBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import java.time.LocalDate + +class BrevPeriodeGenerator( + private val restBehandlingsgrunnlagForBrev: RestBehandlingsgrunnlagForBrev, + private val erFørsteVedtaksperiodePåFagsak: Boolean, + private val uregistrerteBarn: List, + private val brevMålform: Målform, + private val minimertVedtaksperiode: MinimertVedtaksperiode, + private val barnMedReduksjonFraForrigeBehandlingIdent: List, + private val minimerteKompetanserForPeriode: List, + private val minimerteKompetanserSomStopperRettFørPeriode: List, + private val dødeBarnForrigePeriode: List, +) { + + fun genererBrevPeriode(): BrevPeriode? { + val begrunnelseGrunnlagMedPersoner = hentBegrunnelsegrunnlagMedPersoner() + val eøsBegrunnelserMedKompetanser = hentEøsBegrunnelserMedKompetanser() + + val begrunnelserOgFritekster = + byggBegrunnelserOgFritekster( + begrunnelserGrunnlagMedPersoner = begrunnelseGrunnlagMedPersoner, + eøsBegrunnelserMedKompetanser = eøsBegrunnelserMedKompetanser, + ) + + if (begrunnelserOgFritekster.isEmpty()) return null + + val identerIBegrunnelene = begrunnelseGrunnlagMedPersoner + .filter { it.vedtakBegrunnelseType.erInnvilget() } + .flatMap { it.personIdenter } + + return byggBrevPeriode( + begrunnelserOgFritekster = begrunnelserOgFritekster, + identerIBegrunnelene = identerIBegrunnelene, + ) + } + + fun hentEØSBegrunnelseData(eøsBegrunnelserMedKompetanser: List): List = + eøsBegrunnelserMedKompetanser.flatMap { begrunnelseMedData -> + val begrunnelse = begrunnelseMedData.begrunnelse + + if (begrunnelseMedData.kompetanser.isEmpty() && begrunnelse.vedtakBegrunnelseType == VedtakBegrunnelseType.EØS_AVSLAG) { + val minimertePersonResultater = + restBehandlingsgrunnlagForBrev.minimertePersonResultater.filter { personResultat -> + personResultat.minimerteVilkårResultater.any { + it.erEksplisittAvslagPåSøknad == true && + it.periodeFom?.førsteDagINesteMåned() == minimertVedtaksperiode.fom && + it.standardbegrunnelser.contains(begrunnelse) + } + } + + val personerIBegrunnelse = + restBehandlingsgrunnlagForBrev.personerPåBehandling.filter { person -> minimertePersonResultater.any { personResultat -> personResultat.personIdent == person.personIdent } } + val barnPåBehandling = + restBehandlingsgrunnlagForBrev.personerPåBehandling.filter { it.type == PersonType.BARN } + val barnIBegrunnelse = personerIBegrunnelse.filter { it.type == PersonType.BARN } + val gjelderSøker = personerIBegrunnelse.any { it.type == PersonType.SØKER } + + val barnasFødselsdatoer = hentBarnasFødselsdatoerForAvslagsbegrunnelse( + barnIBegrunnelse = barnIBegrunnelse, + barnPåBehandling = barnPåBehandling, + uregistrerteBarn = uregistrerteBarn, + gjelderSøker = gjelderSøker, + ) + val antallBarn = hentAntallBarnForAvslagsbegrunnelse( + barnIBegrunnelse = barnIBegrunnelse, + barnPåBehandling = barnPåBehandling, + uregistrerteBarn = uregistrerteBarn, + gjelderSøker = gjelderSøker, + ) + + listOf( + EØSBegrunnelseDataUtenKompetanse( + vedtakBegrunnelseType = begrunnelse.vedtakBegrunnelseType, + apiNavn = begrunnelse.sanityApiNavn, + barnasFodselsdatoer = barnasFødselsdatoer, + antallBarn = antallBarn, + maalform = brevMålform.tilSanityFormat(), + gjelderSoker = gjelderSøker, + ), + ) + } else { + begrunnelseMedData.kompetanser.map { kompetanse -> + EØSBegrunnelseDataMedKompetanse( + vedtakBegrunnelseType = begrunnelse.vedtakBegrunnelseType, + apiNavn = begrunnelse.sanityApiNavn, + annenForeldersAktivitet = kompetanse.annenForeldersAktivitet, + annenForeldersAktivitetsland = kompetanse.annenForeldersAktivitetslandNavn?.navn, + barnetsBostedsland = kompetanse.barnetsBostedslandNavn.navn, + barnasFodselsdatoer = Utils.slåSammen(kompetanse.personer.map { it.fødselsdato.tilKortString() }), + antallBarn = kompetanse.personer.size, + maalform = brevMålform.tilSanityFormat(), + sokersAktivitet = kompetanse.søkersAktivitet, + sokersAktivitetsland = kompetanse.søkersAktivitetsland?.navn, + ) + } + } + } + + fun hentEøsBegrunnelserMedKompetanser(): List = + minimertVedtaksperiode.eøsBegrunnelser.map { eøsBegrunnelseMedTriggere -> + val kompetanser = when (eøsBegrunnelseMedTriggere.eøsBegrunnelse.vedtakBegrunnelseType) { + VedtakBegrunnelseType.EØS_INNVILGET, VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET -> hentKompetanserForEØSBegrunnelse( + eøsBegrunnelseMedTriggere, + minimerteKompetanserForPeriode, + ) + + VedtakBegrunnelseType.EØS_OPPHØR, VedtakBegrunnelseType.EØS_REDUKSJON -> hentKompetanserForEØSBegrunnelse( + eøsBegrunnelseMedTriggere, + minimerteKompetanserSomStopperRettFørPeriode, + ) + + else -> emptyList() + } + EØSBegrunnelseMedKompetanser( + begrunnelse = eøsBegrunnelseMedTriggere.eøsBegrunnelse, + kompetanser = kompetanser, + ) + } + + fun hentBegrunnelsegrunnlagMedPersoner() = minimertVedtaksperiode.begrunnelser.flatMap { + it.tilBrevBegrunnelseGrunnlagMedPersoner( + periode = NullablePeriode( + fom = minimertVedtaksperiode.fom, + tom = minimertVedtaksperiode.tom, + ), + vedtaksperiodetype = minimertVedtaksperiode.type, + restBehandlingsgrunnlagForBrev = restBehandlingsgrunnlagForBrev, + identerMedUtbetalingPåPeriode = minimertVedtaksperiode.minimerteUtbetalingsperiodeDetaljer + .map { utbetalingsperiodeDetalj -> utbetalingsperiodeDetalj.person.personIdent }, + minimerteUtbetalingsperiodeDetaljer = minimertVedtaksperiode.minimerteUtbetalingsperiodeDetaljer, + erFørsteVedtaksperiodePåFagsak = erFørsteVedtaksperiodePåFagsak, + erUregistrerteBarnPåbehandling = uregistrerteBarn.isNotEmpty(), + barnMedReduksjonFraForrigeBehandlingIdent = barnMedReduksjonFraForrigeBehandlingIdent, + dødeBarnForrigePeriode = dødeBarnForrigePeriode, + ) + } + + fun byggBegrunnelserOgFritekster( + begrunnelserGrunnlagMedPersoner: List, + eøsBegrunnelserMedKompetanser: List, + ): List { + val brevBegrunnelser = begrunnelserGrunnlagMedPersoner + .map { + it.tilBrevBegrunnelse( + vedtaksperiode = NullablePeriode(minimertVedtaksperiode.fom, minimertVedtaksperiode.tom), + personerIPersongrunnlag = restBehandlingsgrunnlagForBrev.personerPåBehandling, + brevMålform = brevMålform, + uregistrerteBarn = uregistrerteBarn, + minimerteUtbetalingsperiodeDetaljer = minimertVedtaksperiode.minimerteUtbetalingsperiodeDetaljer, + minimerteRestEndredeAndeler = restBehandlingsgrunnlagForBrev.minimerteEndredeUtbetalingAndeler, + ) + } + + val eøsBegrunnelser = hentEØSBegrunnelseData(eøsBegrunnelserMedKompetanser) + + val fritekster = minimertVedtaksperiode.fritekster.map { FritekstBegrunnelse(it) } + + return (brevBegrunnelser + eøsBegrunnelser + fritekster).sorted() + } + + private fun byggBrevPeriode( + begrunnelserOgFritekster: List, + identerIBegrunnelene: List, + ): BrevPeriode { + val (utbetalingerBarn, nullutbetalingerBarn) = minimertVedtaksperiode.minimerteUtbetalingsperiodeDetaljer + .filter { it.person.type == PersonType.BARN } + .partition { it.utbetaltPerMnd != 0 } + + val tomDato = + if (minimertVedtaksperiode.tom?.erSenereEnnInneværendeMåned() == false) { + minimertVedtaksperiode.tom.tilMånedÅr() + } else { + null + } + + val barnMedUtbetaling = utbetalingerBarn.map { it.person } + val barnMedNullutbetaling = nullutbetalingerBarn.map { it.person } + + val barnIPeriode: List = when (minimertVedtaksperiode.type) { + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + Vedtaksperiodetype.UTBETALING, + -> finnBarnIUtbetalingPeriode(identerIBegrunnelene) + + Vedtaksperiodetype.OPPHØR -> emptyList() + Vedtaksperiodetype.AVSLAG -> emptyList() + Vedtaksperiodetype.FORTSATT_INNVILGET -> barnMedUtbetaling + barnMedNullutbetaling + Vedtaksperiodetype.ENDRET_UTBETALING -> throw Feil("Endret utbetaling skal ikke benyttes lenger.") + } + + val utbetalingsbeløp = minimertVedtaksperiode.minimerteUtbetalingsperiodeDetaljer.totaltUtbetalt() + val brevPeriodeType = hentBrevPeriodeType( + vedtaksperiodetype = minimertVedtaksperiode.type, + fom = minimertVedtaksperiode.fom, + erUtbetalingEllerDeltBostedIPeriode = minimertVedtaksperiode.minimerteUtbetalingsperiodeDetaljer.any { it.endringsårsak == Årsak.DELT_BOSTED || it.utbetaltPerMnd > 0 }, + ) + + val duEllerInstitusjonen = hentDuEllerInstitusjonenTekst(brevPeriodeType) + + return BrevPeriode( + + fom = this.hentFomTekst(), + tom = when { + tomDato.isNullOrBlank() -> "" + minimertVedtaksperiode.type == Vedtaksperiodetype.FORTSATT_INNVILGET -> "" + minimertVedtaksperiode.type == Vedtaksperiodetype.AVSLAG -> "til og med $tomDato " + brevPeriodeType == BrevPeriodeType.INGEN_UTBETALING -> "" + brevPeriodeType == BrevPeriodeType.INNVILGELSE_INGEN_UTBETALING -> " til $tomDato" + else -> "til $tomDato " + }, + beløp = Utils.formaterBeløp(utbetalingsbeløp), + begrunnelser = begrunnelserOgFritekster, + brevPeriodeType = brevPeriodeType, + antallBarn = barnIPeriode.size.toString(), + barnasFodselsdager = barnIPeriode.tilBarnasFødselsdatoer(), + duEllerInstitusjonen = duEllerInstitusjonen, + ) + } + + private fun hentFomTekst(): String = if (minimertVedtaksperiode.fom != null) minimertVedtaksperiode.fom.tilMånedÅr() else "" + + @Deprecated("Erstattes av hentFomTekst når nye begrunnelse logikk går live") + private fun hentFomTekstGammel(): String = when (minimertVedtaksperiode.type) { + Vedtaksperiodetype.FORTSATT_INNVILGET -> hentFomtekstFortsattInnvilget( + brevMålform, + minimertVedtaksperiode.fom, + minimertVedtaksperiode.begrunnelser.map { it.standardbegrunnelse }, + ) ?: "Du får:" + + Vedtaksperiodetype.UTBETALING -> minimertVedtaksperiode.fom!!.tilDagMånedÅr() + Vedtaksperiodetype.OPPHØR -> minimertVedtaksperiode.fom!!.tilDagMånedÅr() + Vedtaksperiodetype.AVSLAG -> if (minimertVedtaksperiode.fom != null) minimertVedtaksperiode.fom.tilDagMånedÅr() else "" + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING -> minimertVedtaksperiode.fom!!.tilDagMånedÅr() + Vedtaksperiodetype.ENDRET_UTBETALING -> throw Feil("Endret utbetaling skal ikke benyttes lenger.") + } + + private fun hentDuEllerInstitusjonenTekst(brevPeriodeType: BrevPeriodeType): String = + when (restBehandlingsgrunnlagForBrev.fagsakType) { + FagsakType.INSTITUSJON -> { + when (brevPeriodeType) { + BrevPeriodeType.UTBETALING, BrevPeriodeType.INGEN_UTBETALING -> "institusjonen" + else -> "Institusjonen" + } + } + + FagsakType.NORMAL, FagsakType.BARN_ENSLIG_MINDREÅRIG -> { + when (brevPeriodeType) { + BrevPeriodeType.UTBETALING, BrevPeriodeType.INGEN_UTBETALING -> "du" + else -> "Du" + } + } + } + + fun finnBarnIUtbetalingPeriode(identerIBegrunnelene: List): List { + val identerMedUtbetaling = + minimertVedtaksperiode.minimerteUtbetalingsperiodeDetaljer.map { it.person.personIdent } + + val barnIPeriode = (identerIBegrunnelene + identerMedUtbetaling) + .toSet() + .mapNotNull { personIdent -> + restBehandlingsgrunnlagForBrev.personerPåBehandling.find { it.personIdent == personIdent } + } + .filter { it.type == PersonType.BARN } + + return barnIPeriode + } + + private fun hentFomtekstFortsattInnvilget( + målform: Målform, + fom: LocalDate?, + begrunnelser: List, + ): String? { + val erAutobrev = begrunnelser.any { + this.minimertVedtaksperiode.begrunnelser.any { it.triggesAv.valgbarhet == Valgbarhet.AUTOMATISK } + } + return if (erAutobrev && fom != null) { + val fra = if (målform == Målform.NB) "Fra" else "Frå" + "$fra ${fom.tilDagMånedÅr()} får du:" + } else { + null + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevPeriodeService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevPeriodeService.kt new file mode 100644 index 000000000..c876c9f0d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevPeriodeService.kt @@ -0,0 +1,238 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.common.convertDataClassToJson +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.BarnMedOpplysninger +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.sanity.SanityService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.beregning.endringstidspunkt.Beløpsdifferanse +import no.nav.familie.ba.sak.kjerne.beregning.endringstidspunkt.hentPerioderMedEndringerFra +import no.nav.familie.ba.sak.kjerne.brev.domene.BrevperiodeData +import no.nav.familie.ba.sak.kjerne.brev.domene.RestBehandlingsgrunnlagForBrev +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertKompetanse +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertUregistrertBarn +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertVedtaksperiode +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.KompetanseService +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.søknad.SøknadGrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.tilMinimertePersoner +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.dødeBarnForrigePeriode +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BrevBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.tilUtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.erFørsteVedtaksperiodePåFagsak +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.ytelseErFraForrigePeriode +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.fpsak.tidsserie.LocalDateSegment +import no.nav.fpsak.tidsserie.LocalDateTimeline +import org.springframework.stereotype.Service + +@Service +class BrevPeriodeService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val persongrunnlagService: PersongrunnlagService, + private val vedtaksperiodeHentOgPersisterService: VedtaksperiodeHentOgPersisterService, + private val sanityService: SanityService, + private val søknadGrunnlagService: SøknadGrunnlagService, + private val vilkårsvurderingService: VilkårsvurderingService, + private val personidentService: PersonidentService, + private val kompetanseService: KompetanseService, + private val integrasjonClient: IntegrasjonClient, + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + private val featureToggleService: FeatureToggleService, +) { + + fun hentBrevperioderData( + vedtaksperioder: List, + behandling: Behandling, + skalLogge: Boolean = true, + ): List { + val vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandling(behandlingId = behandling.id) + ?: error("Finner ikke vilkårsvurdering ved begrunning av vedtak") + + val endredeUtbetalingAndeler = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnEndreteUtbetalingerMedAndelerTilkjentYtelse(behandlingId = behandling.id) + + val personopplysningGrunnlag = persongrunnlagService.hentAktivThrows(behandlingId = behandling.id) + + val andelerMedEndringer = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnAndelerTilkjentYtelseMedEndreteUtbetalinger(behandling.id) + + val uregistrerteBarn = + søknadGrunnlagService.hentAktiv(behandlingId = behandling.id)?.hentUregistrerteBarn() + ?: emptyList() + + val forrigeBehandling = + behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksattFraBehandlingsId(behandlingId = behandling.id) + + val kompetanser = kompetanseService.hentKompetanser(behandlingId = BehandlingId(behandling.id)) + .filter { + if (forrigeBehandling?.erAutomatiskEøsMigrering() == true && behandling.skalBehandlesAutomatisk) { + it.erObligatoriskeFelterSatt() + } else { + true + } + } + + val sanityBegrunnelser = sanityService.hentSanityBegrunnelser() + val sanityEØSBegrunnelser = sanityService.hentSanityEØSBegrunnelser() + + val restBehandlingsgrunnlagForBrev = hentRestBehandlingsgrunnlagForBrev( + vilkårsvurdering = vilkårsvurdering, + endredeUtbetalingAndeler = endredeUtbetalingAndeler, + persongrunnlag = personopplysningGrunnlag, + ) + + return vedtaksperioder.map { + hentBrevperiodeData( + vedtaksperiodeMedBegrunnelser = it, + restBehandlingsgrunnlagForBrev = restBehandlingsgrunnlagForBrev, + personopplysningGrunnlag = personopplysningGrunnlag, + andelerTilkjentYtelse = andelerMedEndringer, + uregistrerteBarn = uregistrerteBarn, + skalLogge = skalLogge, + kompetanser = kompetanser.toList(), + sanityBegrunnelser = sanityBegrunnelser, + sanityEØSBegrunnelser = sanityEØSBegrunnelser, + ) + } + } + + private fun hentBrevperiodeData( + vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser, + restBehandlingsgrunnlagForBrev: RestBehandlingsgrunnlagForBrev, + personopplysningGrunnlag: PersonopplysningGrunnlag, + andelerTilkjentYtelse: List, + uregistrerteBarn: List, + kompetanser: List, + sanityBegrunnelser: Map, + sanityEØSBegrunnelser: Map, + + skalLogge: Boolean = true, + ): BrevperiodeData { + val minimerteUregistrerteBarn = uregistrerteBarn.map { it.tilMinimertUregistrertBarn() } + + val utvidetVedtaksperiodeMedBegrunnelse = vedtaksperiodeMedBegrunnelser.tilUtvidetVedtaksperiodeMedBegrunnelser( + personopplysningGrunnlag = personopplysningGrunnlag, + andelerTilkjentYtelse = andelerTilkjentYtelse, + ) + + val ytelserForrigePeriode = + andelerTilkjentYtelse.filter { ytelseErFraForrigePeriode(it, utvidetVedtaksperiodeMedBegrunnelse) } + + val dødeBarnForrigePeriode = + dødeBarnForrigePeriode(ytelserForrigePeriode, personopplysningGrunnlag.barna.tilMinimertePersoner()) + + val minimertVedtaksperiode = + utvidetVedtaksperiodeMedBegrunnelse.tilMinimertVedtaksperiode( + sanityBegrunnelser = sanityBegrunnelser, + sanityEØSBegrunnelser = sanityEØSBegrunnelser, + ) + + val landkoderISO2 = integrasjonClient.hentLandkoderISO2() + + val brevperiodeData = BrevperiodeData( + minimertVedtaksperiode = minimertVedtaksperiode, + restBehandlingsgrunnlagForBrev = restBehandlingsgrunnlagForBrev, + uregistrerteBarn = minimerteUregistrerteBarn, + brevMålform = personopplysningGrunnlag.søker.målform, + erFørsteVedtaksperiodePåFagsak = erFørsteVedtaksperiodePåFagsak( + andelerTilkjentYtelse = andelerTilkjentYtelse, + periodeFom = utvidetVedtaksperiodeMedBegrunnelse.fom, + ), + barnMedReduksjonFraForrigeBehandlingIdent = hentBarnsPersonIdentMedRedusertPeriode( + vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, + andelerTilkjentYtelse = andelerTilkjentYtelse, + ), + minimerteKompetanserForPeriode = hentMinimerteKompetanserForPeriode( + kompetanser = kompetanser, + fom = vedtaksperiodeMedBegrunnelser.fom?.toYearMonth(), + tom = vedtaksperiodeMedBegrunnelser.tom?.toYearMonth(), + personopplysningGrunnlag = personopplysningGrunnlag, + landkoderISO2 = landkoderISO2, + ), + minimerteKompetanserSomStopperRettFørPeriode = hentKompetanserSomStopperRettFørPeriode( + kompetanser = kompetanser, + periodeFom = minimertVedtaksperiode.fom?.toYearMonth(), + ).filter { + it.erObligatoriskeFelterSatt() + }.map { + it.tilMinimertKompetanse( + personopplysningGrunnlag = personopplysningGrunnlag, + landkoderISO2 = landkoderISO2, + ) + }, + dødeBarnForrigePeriode = dødeBarnForrigePeriode, + ) + + if (skalLogge) { + secureLogger.info( + "Data for brevperiode på behandling ${vedtaksperiodeMedBegrunnelser.vedtak.behandling}: \n" + + brevperiodeData.tilBrevperiodeForLogging().convertDataClassToJson(), + ) + } + + return brevperiodeData + } + + private fun hentBarnsPersonIdentMedRedusertPeriode( + vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser, + andelerTilkjentYtelse: List, + ): List { + val forrigeBehandling = + behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(vedtaksperiodeMedBegrunnelser.vedtak.behandling) + return if (forrigeBehandling != null) { + val forrigeAndelerMedEndringer = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnAndelerTilkjentYtelseMedEndreteUtbetalinger(forrigeBehandling.id) + val endringerITilkjentYtelsePerBarn = + andelerTilkjentYtelse.hentPerioderMedEndringerFra(forrigeAndelerMedEndringer) + endringerITilkjentYtelsePerBarn.keys.filter { barn -> + endringerITilkjentYtelsePerBarn.getValue(barn).any { + it.overlapper( + LocalDateSegment( + vedtaksperiodeMedBegrunnelser.fom, + vedtaksperiodeMedBegrunnelser.tom, + null, + ), + ) + } + }.mapNotNull { barn -> + val result: LocalDateTimeline = endringerITilkjentYtelsePerBarn.getValue(barn) + if (!result.filterValue { beløp -> beløp < 0 }.isEmpty) { + personidentService.hentAktør(barn).aktivFødselsnummer() + } else { + null + } + } + } else { + emptyList() + } + } + + fun genererBrevBegrunnelserForPeriode(vedtaksperiodeId: Long): List { + val vedtaksperiodeMedBegrunnelser = + vedtaksperiodeHentOgPersisterService.hentVedtaksperiodeThrows(vedtaksperiodeId) + + val begrunnelseDataForVedtaksperiode = + hentBrevperioderData( + vedtaksperioder = listOf(vedtaksperiodeMedBegrunnelser), + behandling = vedtaksperiodeMedBegrunnelser.vedtak.behandling, + ).single() + return begrunnelseDataForVedtaksperiode.hentBegrunnelserOgFritekster() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevPeriodeUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevPeriodeUtil.kt new file mode 100644 index 000000000..8b289d29c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevPeriodeUtil.kt @@ -0,0 +1,109 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.kjerne.beregning.domene.EndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertKompetanse +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertUregistrertBarn +import no.nav.familie.ba.sak.kjerne.brev.domene.RestBehandlingsgrunnlagForBrev +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertKompetanse +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertPersonResultat +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertRestEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.tilSeparateTidslinjerForBarna +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.tilSkjemaer +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunktEllerUendeligSent +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunktEllerUendeligTidlig +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.beskjær +import no.nav.familie.ba.sak.kjerne.vedtak.domene.MinimertRestPerson +import no.nav.familie.ba.sak.kjerne.vedtak.domene.tilMinimertPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import java.time.LocalDate +import java.time.YearMonth + +fun List.tilBarnasFødselsdatoer(): String = + Utils.slåSammen( + this + .filter { it.type == PersonType.BARN } + .sortedBy { person -> + person.fødselsdato + } + .map { person -> + person.fødselsdato.tilKortString() + }, + ) + +fun List.tilSammenslåttKortString(): String = Utils.slåSammen(this.sorted().map { it.tilKortString() }) + +fun hentBarnasFødselsdatoerForAvslagsbegrunnelse( + barnIBegrunnelse: List, + barnPåBehandling: List, + uregistrerteBarn: List, + gjelderSøker: Boolean, +): String { + val registrerteBarnFødselsdatoer = + if (gjelderSøker) barnPåBehandling.map { it.fødselsdato } else barnIBegrunnelse.map { it.fødselsdato } + val uregistrerteBarnFødselsdatoer = + uregistrerteBarn.mapNotNull { it.fødselsdato } + val alleBarnaFødselsdatoer = registrerteBarnFødselsdatoer + uregistrerteBarnFødselsdatoer + return alleBarnaFødselsdatoer.tilSammenslåttKortString() +} + +fun hentAntallBarnForAvslagsbegrunnelse( + barnIBegrunnelse: List, + barnPåBehandling: List, + uregistrerteBarn: List, + gjelderSøker: Boolean, +): Int { + val antallRegistrerteBarn = if (gjelderSøker) barnPåBehandling.size else barnIBegrunnelse.size + return antallRegistrerteBarn + uregistrerteBarn.size +} + +fun hentRestBehandlingsgrunnlagForBrev( + persongrunnlag: PersonopplysningGrunnlag, + vilkårsvurdering: Vilkårsvurdering, + endredeUtbetalingAndeler: List, +): RestBehandlingsgrunnlagForBrev { + return RestBehandlingsgrunnlagForBrev( + personerPåBehandling = persongrunnlag.søkerOgBarn.map { it.tilMinimertPerson() }, + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimerteEndredeUtbetalingAndeler = endredeUtbetalingAndeler.map { it.tilMinimertRestEndretUtbetalingAndel() }, + fagsakType = vilkårsvurdering.behandling.fagsak.type, + ) +} + +fun hentMinimerteKompetanserForPeriode( + kompetanser: List, + fom: YearMonth?, + tom: YearMonth?, + personopplysningGrunnlag: PersonopplysningGrunnlag, + landkoderISO2: Map, +): List { + val minimerteKompetanser = kompetanser.hentIPeriode(fom, tom) + .filter { it.erObligatoriskeFelterSatt() } + .map { + it.tilMinimertKompetanse( + personopplysningGrunnlag = personopplysningGrunnlag, + landkoderISO2 = landkoderISO2, + ) + } + + return minimerteKompetanser +} + +fun hentKompetanserSomStopperRettFørPeriode( + kompetanser: List, + periodeFom: YearMonth?, +) = kompetanser.filter { it.tom?.plusMonths(1) == periodeFom } + +fun Collection.hentIPeriode( + fom: YearMonth?, + tom: YearMonth?, +): Collection = tilSeparateTidslinjerForBarna().mapValues { (_, tidslinje) -> + tidslinje.beskjær( + fraOgMed = fom.tilTidspunktEllerUendeligTidlig(tom), + tilOgMed = tom.tilTidspunktEllerUendeligSent(fom), + ) +}.tilSkjemaer() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevService.kt new file mode 100644 index 000000000..28badff93 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevService.kt @@ -0,0 +1,423 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.Utils.storForbokstavIAlleNavn +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import no.nav.familie.ba.sak.config.FeatureToggleConfig.Companion.NY_GENERERING_AV_BREVOBJEKTER +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.organisasjon.OrganisasjonService +import no.nav.familie.ba.sak.integrasjoner.sanity.SanityService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.brev.brevPeriodeProdusent.lagBrevPeriode +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertVedtaksperiode +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Autovedtak6og18årOgSmåbarnstillegg +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.AutovedtakNyfødtBarnFraFør +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.AutovedtakNyfødtFørsteBarn +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Avslag +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Dødsfall +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.DødsfallData +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Etterbetaling +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.EtterbetalingInstitusjon +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.FeilutbetaltValuta +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.ForsattInnvilget +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Førstegangsvedtak +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Hjemmeltekst +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.KorreksjonVedtaksbrev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.KorreksjonVedtaksbrevData +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.KorrigertVedtakData +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.OpphørMedEndring +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Opphørt +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.RefusjonEøsAvklart +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.RefusjonEøsUavklart +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.SignaturVedtak +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.VedtakEndring +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.VedtakFellesfelter +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Vedtaksbrev +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.korrigertetterbetaling.KorrigertEtterbetalingService +import no.nav.familie.ba.sak.kjerne.korrigertvedtak.KorrigertVedtakService +import no.nav.familie.ba.sak.kjerne.simulering.SimuleringService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollService +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.domene.Totrinnskontroll +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.refusjonEøs.RefusjonEøsRepository +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.sikkerhet.SaksbehandlerContext +import no.nav.familie.unleash.UnleashService +import org.springframework.stereotype.Service +import java.math.BigDecimal + +@Service +class BrevService( + private val totrinnskontrollService: TotrinnskontrollService, + private val persongrunnlagService: PersongrunnlagService, + private val arbeidsfordelingService: ArbeidsfordelingService, + private val simuleringService: SimuleringService, + private val vedtaksperiodeService: VedtaksperiodeService, + private val brevPeriodeService: BrevPeriodeService, + private val sanityService: SanityService, + private val vilkårsvurderingService: VilkårsvurderingService, + private val korrigertEtterbetalingService: KorrigertEtterbetalingService, + private val organisasjonService: OrganisasjonService, + private val korrigertVedtakService: KorrigertVedtakService, + private val saksbehandlerContext: SaksbehandlerContext, + private val brevmalService: BrevmalService, + private val refusjonEøsRepository: RefusjonEøsRepository, + private val unleashNext: UnleashService, + private val integrasjonClient: IntegrasjonClient, +) { + + fun hentVedtaksbrevData(vedtak: Vedtak): Vedtaksbrev { + val behandling = vedtak.behandling + + val brevmal = brevmalService.hentBrevmal( + behandling, + ) + + val vedtakFellesfelter = lagVedtaksbrevFellesfelter(vedtak) + validerBrevdata(brevmal, vedtakFellesfelter) + + val skalMeldeFraOmEndringerEøsSelvstendigRett by lazy { + vedtaksperiodeService.skalMeldeFraOmEndringerEøsSelvstendigRett(vedtak) + } + + return when (brevmal) { + Brevmal.VEDTAK_FØRSTEGANGSVEDTAK -> Førstegangsvedtak( + vedtakFellesfelter = vedtakFellesfelter, + etterbetaling = hentEtterbetaling(vedtak), + informasjonOmAarligKontroll = vedtaksperiodeService.skalHaÅrligKontroll(vedtak), + refusjonEosAvklart = beskrivPerioderMedAvklartRefusjonEøs(vedtak), + refusjonEosUavklart = beskrivPerioderMedUavklartRefusjonEøs(vedtak), + duMåMeldeFraOmEndringer = !skalMeldeFraOmEndringerEøsSelvstendigRett, + duMåMeldeFraOmEndringerEøsSelvstendigRett = skalMeldeFraOmEndringerEøsSelvstendigRett, + ) + + Brevmal.VEDTAK_FØRSTEGANGSVEDTAK_INSTITUSJON -> Førstegangsvedtak( + mal = Brevmal.VEDTAK_FØRSTEGANGSVEDTAK_INSTITUSJON, + vedtakFellesfelter = vedtakFellesfelter, + etterbetalingInstitusjon = hentEtterbetalingInstitusjon(vedtak), + ) + + Brevmal.VEDTAK_ENDRING -> VedtakEndring( + vedtakFellesfelter = vedtakFellesfelter, + etterbetaling = hentEtterbetaling(vedtak), + erKlage = behandling.erKlage(), + erFeilutbetalingPåBehandling = erFeilutbetalingPåBehandling(behandlingId = behandling.id), + informasjonOmAarligKontroll = vedtaksperiodeService.skalHaÅrligKontroll(vedtak), + feilutbetaltValuta = vedtaksperiodeService.beskrivPerioderMedFeilutbetaltValuta(vedtak)?.let { + FeilutbetaltValuta(perioderMedForMyeUtbetalt = it) + }, + refusjonEosAvklart = beskrivPerioderMedAvklartRefusjonEøs(vedtak), + refusjonEosUavklart = beskrivPerioderMedUavklartRefusjonEøs(vedtak), + duMåMeldeFraOmEndringer = !skalMeldeFraOmEndringerEøsSelvstendigRett, + duMåMeldeFraOmEndringerEøsSelvstendigRett = skalMeldeFraOmEndringerEøsSelvstendigRett, + ) + + Brevmal.VEDTAK_ENDRING_INSTITUSJON -> VedtakEndring( + mal = Brevmal.VEDTAK_ENDRING_INSTITUSJON, + vedtakFellesfelter = vedtakFellesfelter, + etterbetalingInstitusjon = hentEtterbetalingInstitusjon(vedtak), + erKlage = behandling.erKlage(), + erFeilutbetalingPåBehandling = erFeilutbetalingPåBehandling(behandlingId = behandling.id), + informasjonOmAarligKontroll = vedtaksperiodeService.skalHaÅrligKontroll(vedtak), + ) + + Brevmal.VEDTAK_OPPHØRT -> Opphørt( + vedtakFellesfelter = vedtakFellesfelter, + erFeilutbetalingPåBehandling = erFeilutbetalingPåBehandling(behandlingId = behandling.id), + ) + + Brevmal.VEDTAK_OPPHØRT_INSTITUSJON -> Opphørt( + mal = Brevmal.VEDTAK_OPPHØRT_INSTITUSJON, + vedtakFellesfelter = vedtakFellesfelter, + erFeilutbetalingPåBehandling = erFeilutbetalingPåBehandling(behandlingId = behandling.id), + ) + + Brevmal.VEDTAK_OPPHØR_MED_ENDRING -> OpphørMedEndring( + vedtakFellesfelter = vedtakFellesfelter, + etterbetaling = hentEtterbetaling(vedtak), + erFeilutbetalingPåBehandling = erFeilutbetalingPåBehandling(behandlingId = behandling.id), + refusjonEosAvklart = beskrivPerioderMedAvklartRefusjonEøs(vedtak), + refusjonEosUavklart = beskrivPerioderMedUavklartRefusjonEøs(vedtak), + ) + + Brevmal.VEDTAK_OPPHØR_MED_ENDRING_INSTITUSJON -> OpphørMedEndring( + mal = Brevmal.VEDTAK_OPPHØR_MED_ENDRING_INSTITUSJON, + vedtakFellesfelter = vedtakFellesfelter, + etterbetalingInstitusjon = hentEtterbetalingInstitusjon(vedtak), + erFeilutbetalingPåBehandling = erFeilutbetalingPåBehandling(behandlingId = behandling.id), + ) + + Brevmal.VEDTAK_AVSLAG -> Avslag(vedtakFellesfelter = vedtakFellesfelter) + Brevmal.VEDTAK_AVSLAG_INSTITUSJON -> Avslag( + mal = Brevmal.VEDTAK_AVSLAG_INSTITUSJON, + vedtakFellesfelter = vedtakFellesfelter, + ) + + Brevmal.VEDTAK_FORTSATT_INNVILGET -> ForsattInnvilget( + vedtakFellesfelter = vedtakFellesfelter, + etterbetaling = hentEtterbetaling(vedtak), + informasjonOmAarligKontroll = vedtaksperiodeService.skalHaÅrligKontroll(vedtak), + refusjonEosAvklart = beskrivPerioderMedAvklartRefusjonEøs(vedtak), + refusjonEosUavklart = beskrivPerioderMedUavklartRefusjonEøs(vedtak), + duMåMeldeFraOmEndringer = !skalMeldeFraOmEndringerEøsSelvstendigRett, + duMåMeldeFraOmEndringerEøsSelvstendigRett = skalMeldeFraOmEndringerEøsSelvstendigRett, + ) + + Brevmal.VEDTAK_FORTSATT_INNVILGET_INSTITUSJON -> ForsattInnvilget( + mal = Brevmal.VEDTAK_FORTSATT_INNVILGET_INSTITUSJON, + vedtakFellesfelter = vedtakFellesfelter, + etterbetalingInstitusjon = hentEtterbetalingInstitusjon(vedtak), + ) + + Brevmal.AUTOVEDTAK_BARN_6_OG_18_ÅR_OG_SMÅBARNSTILLEGG -> Autovedtak6og18årOgSmåbarnstillegg( + vedtakFellesfelter = vedtakFellesfelter, + ) + + Brevmal.AUTOVEDTAK_NYFØDT_FØRSTE_BARN -> AutovedtakNyfødtFørsteBarn( + vedtakFellesfelter = vedtakFellesfelter, + etterbetaling = hentEtterbetaling(vedtak), + ) + + Brevmal.AUTOVEDTAK_NYFØDT_BARN_FRA_FØR -> AutovedtakNyfødtBarnFraFør( + vedtakFellesfelter = vedtakFellesfelter, + etterbetaling = hentEtterbetaling(vedtak), + ) + + else -> throw Feil("Forsøker å hente vedtaksbrevdata for brevmal ${brevmal.visningsTekst}") + } + } + + private fun beskrivPerioderMedUavklartRefusjonEøs(vedtak: Vedtak) = + vedtaksperiodeService.beskrivPerioderMedRefusjonEøs(behandling = vedtak.behandling, avklart = false) + ?.let { RefusjonEøsUavklart(perioderMedRefusjonEøsUavklart = it) } + + private fun beskrivPerioderMedAvklartRefusjonEøs(vedtak: Vedtak) = + vedtaksperiodeService.beskrivPerioderMedRefusjonEøs(behandling = vedtak.behandling, avklart = true) + ?.let { RefusjonEøsAvklart(perioderMedRefusjonEøsAvklart = it) } + + private fun validerBrevdata( + brevmal: Brevmal, + vedtakFellesfelter: VedtakFellesfelter, + ) { + if (brevmal in listOf( + Brevmal.VEDTAK_OPPHØRT, + Brevmal.VEDTAK_OPPHØRT_INSTITUSJON, + ) && vedtakFellesfelter.perioder.size > 1 + ) { + throw FunksjonellFeil( + "Behandlingsstatusen er \"Opphørt\", men mer enn én periode er begrunnet. Du skal kun begrunne perioden uten utbetaling.", + ) + } + } + + fun hentDødsfallbrevData(vedtak: Vedtak): Brev = + hentGrunnlagOgSignaturData(vedtak).let { data -> + Dødsfall( + data = DødsfallData( + delmalData = DødsfallData.DelmalData( + signaturVedtak = SignaturVedtak( + enhet = data.enhet, + saksbehandler = data.saksbehandler, + beslutter = data.beslutter, + ), + ), + flettefelter = DødsfallData.Flettefelter( + navn = data.grunnlag.søker.navn, + fodselsnummer = data.grunnlag.søker.aktør.aktivFødselsnummer(), + // Selv om det er feil å anta at alle navn er på dette formatet er det ønskelig å skrive + // det slik, da uppercase kan oppleves som skrikende i et brev som skal være skånsomt + navnAvdode = data.grunnlag.søker.navn.storForbokstavIAlleNavn(), + virkningstidspunkt = hentVirkningstidspunkt( + opphørsperioder = vedtaksperiodeService.hentOpphørsperioder(vedtak.behandling), + behandlingId = vedtak.behandling.id, + ), + ), + ), + ) + } + + fun hentKorreksjonbrevData(vedtak: Vedtak): Brev = + hentGrunnlagOgSignaturData(vedtak).let { data -> + KorreksjonVedtaksbrev( + data = KorreksjonVedtaksbrevData( + delmalData = KorreksjonVedtaksbrevData.DelmalData( + signaturVedtak = SignaturVedtak( + enhet = data.enhet, + saksbehandler = data.saksbehandler, + beslutter = data.beslutter, + ), + ), + flettefelter = KorreksjonVedtaksbrevData.Flettefelter( + navn = data.grunnlag.søker.navn, + fodselsnummer = data.grunnlag.søker.aktør.aktivFødselsnummer(), + ), + ), + ) + } + + fun lagVedtaksbrevFellesfelter(vedtak: Vedtak): VedtakFellesfelter { + val vedtaksperioder = vedtaksperiodeService.hentPersisterteVedtaksperioder(vedtak) + .filter { + !(it.begrunnelser.isEmpty() && it.fritekster.isEmpty() && it.eøsBegrunnelser.isEmpty()) + }.sortedBy { it.fom } + + if (vedtaksperioder.isEmpty()) { + throw FunksjonellFeil( + "Vedtaket mangler begrunnelser. Du må legge til begrunnelser for å generere vedtaksbrevet.", + ) + } + + val grunnlagOgSignaturData = hentGrunnlagOgSignaturData(vedtak) + val brevPerioderData = brevPeriodeService.hentBrevperioderData( + vedtaksperioder = vedtaksperioder, + behandling = vedtak.behandling, + ) + + val brevperioder = if (unleashNext.isEnabled(NY_GENERERING_AV_BREVOBJEKTER) && false) { + val grunnlagForBegrunnelser = vedtaksperiodeService.hentGrunnlagForBegrunnelse(vedtak.behandling) + vedtaksperioder.mapNotNull { + it.lagBrevPeriode( + grunnlagForBegrunnelse = grunnlagForBegrunnelser, + landkoder = integrasjonClient.hentLandkoderISO2(), + ) + } + } else { + brevPerioderData.sorted().mapNotNull { + it.tilBrevPeriodeGenerator().genererBrevPeriode() + } + } + + val korrigertVedtak = korrigertVedtakService.finnAktivtKorrigertVedtakPåBehandling(vedtak.behandling.id) + val refusjonEøs = refusjonEøsRepository.finnRefusjonEøsForBehandling(vedtak.behandling.id) + + val hjemler = hentHjemler( + behandlingId = vedtak.behandling.id, + minimerteVedtaksperioder = brevPerioderData.map { it.minimertVedtaksperiode }, + målform = brevPerioderData.first().brevMålform, + vedtakKorrigertHjemmelSkalMedIBrev = korrigertVedtak != null, + refusjonEøsHjemmelSkalMedIBrev = refusjonEøs.isNotEmpty(), + ) + + val organisasjonsnummer = vedtak.behandling.fagsak.institusjon?.orgNummer + val organisasjonsnavn = organisasjonsnummer?.let { organisasjonService.hentOrganisasjon(it).navn } + + return VedtakFellesfelter( + enhet = grunnlagOgSignaturData.enhet, + saksbehandler = grunnlagOgSignaturData.saksbehandler, + beslutter = grunnlagOgSignaturData.beslutter, + hjemmeltekst = Hjemmeltekst(hjemler), + søkerNavn = organisasjonsnavn ?: grunnlagOgSignaturData.grunnlag.søker.navn, + søkerFødselsnummer = grunnlagOgSignaturData.grunnlag.søker.aktør.aktivFødselsnummer(), + perioder = brevperioder, + organisasjonsnummer = organisasjonsnummer, + gjelder = if (organisasjonsnummer != null) grunnlagOgSignaturData.grunnlag.søker.navn else null, + korrigertVedtakData = korrigertVedtak?.let { KorrigertVedtakData(datoKorrigertVedtak = it.vedtaksdato.tilDagMånedÅr()) }, + ) + } + + private fun hentHjemler( + behandlingId: Long, + minimerteVedtaksperioder: List, + målform: Målform, + vedtakKorrigertHjemmelSkalMedIBrev: Boolean = false, + refusjonEøsHjemmelSkalMedIBrev: Boolean, + ): String { + val vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandling(behandlingId = behandlingId) + ?: error("Finner ikke vilkårsvurdering ved begrunning av vedtak") + + val opplysningspliktHjemlerSkalMedIBrev = + vilkårsvurdering.finnOpplysningspliktVilkår()?.resultat == Resultat.IKKE_OPPFYLT + + return hentHjemmeltekst( + minimerteVedtaksperioder = minimerteVedtaksperioder, + sanityBegrunnelser = sanityService.hentSanityBegrunnelser(), + opplysningspliktHjemlerSkalMedIBrev = opplysningspliktHjemlerSkalMedIBrev, + målform = målform, + vedtakKorrigertHjemmelSkalMedIBrev = vedtakKorrigertHjemmelSkalMedIBrev, + refusjonEøsHjemmelSkalMedIBrev = refusjonEøsHjemmelSkalMedIBrev, + ) + } + + private fun hentAktivtPersonopplysningsgrunnlag(behandlingId: Long) = + persongrunnlagService.hentAktivThrows(behandlingId = behandlingId) + + private fun hentEtterbetaling(vedtak: Vedtak): Etterbetaling? = + hentEtterbetalingsbeløp(vedtak)?.let { Etterbetaling(it) } + + private fun hentEtterbetalingInstitusjon(vedtak: Vedtak): EtterbetalingInstitusjon? = + hentEtterbetalingsbeløp(vedtak)?.let { EtterbetalingInstitusjon(it) } + + private fun hentEtterbetalingsbeløp(vedtak: Vedtak): String? { + val etterbetalingsBeløp = + korrigertEtterbetalingService.finnAktivtKorrigeringPåBehandling(vedtak.behandling.id)?.beløp?.toBigDecimal() + ?: simuleringService.hentEtterbetaling(vedtak.behandling.id) + + return etterbetalingsBeløp.takeIf { it > BigDecimal.ZERO }?.run { Utils.formaterBeløp(this.toInt()) } + } + + private fun erFeilutbetalingPåBehandling(behandlingId: Long): Boolean = + simuleringService.hentFeilutbetaling(behandlingId) > BigDecimal.ZERO + + private fun hentGrunnlagOgSignaturData(vedtak: Vedtak): GrunnlagOgSignaturData { + val personopplysningGrunnlag = hentAktivtPersonopplysningsgrunnlag(vedtak.behandling.id) + val (saksbehandler, beslutter) = hentSaksbehandlerOgBeslutter( + behandling = vedtak.behandling, + totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(vedtak.behandling.id), + ) + val enhet = arbeidsfordelingService.hentArbeidsfordelingPåBehandling(vedtak.behandling.id).behandlendeEnhetNavn + return GrunnlagOgSignaturData( + grunnlag = personopplysningGrunnlag, + saksbehandler = saksbehandler, + beslutter = beslutter, + enhet = enhet, + ) + } + + fun hentSaksbehandlerOgBeslutter( + behandling: Behandling, + totrinnskontroll: Totrinnskontroll?, + ): Pair { + return when { + behandling.steg <= StegType.SEND_TIL_BESLUTTER || totrinnskontroll == null -> { + Pair(saksbehandlerContext.hentSaksbehandlerSignaturTilBrev(), "Beslutter") + } + + totrinnskontroll.erBesluttet() -> { + Pair(totrinnskontroll.saksbehandler, totrinnskontroll.beslutter!!) + } + + behandling.steg == StegType.BESLUTTE_VEDTAK -> { + Pair( + totrinnskontroll.saksbehandler, + if (totrinnskontroll.saksbehandler == saksbehandlerContext.hentSaksbehandlerSignaturTilBrev()) { + "Beslutter" + } else { + saksbehandlerContext.hentSaksbehandlerSignaturTilBrev() + }, + ) + } + + else -> { + throw Feil("Prøver å hente saksbehandler og beslutters navn for generering av brev i en ukjent tilstand.") + } + } + } + + private data class GrunnlagOgSignaturData( + val grunnlag: PersonopplysningGrunnlag, + val saksbehandler: String, + val beslutter: String, + val enhet: String, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevUtil.kt new file mode 100644 index 000000000..7fd4d8f74 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevUtil.kt @@ -0,0 +1,256 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.tilMånedÅr +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat.DELVIS_INNVILGET +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat.DELVIS_INNVILGET_OG_OPPHØRT +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat.ENDRET_OG_OPPHØRT +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat.INNVILGET +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat.INNVILGET_OG_ENDRET +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat.INNVILGET_OG_OPPHØRT +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertVedtaksperiode +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.hjemlerTilhørendeFritekst +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Opphørsperiode + +fun hentAutomatiskVedtaksbrevtype(behandling: Behandling): Brevmal { + val behandlingÅrsak = behandling.opprettetÅrsak + val fagsakStatus = behandling.fagsak.status + + return when (behandlingÅrsak) { + BehandlingÅrsak.FØDSELSHENDELSE -> { + if (fagsakStatus == FagsakStatus.LØPENDE) { + Brevmal.AUTOVEDTAK_NYFØDT_BARN_FRA_FØR + } else { + Brevmal.AUTOVEDTAK_NYFØDT_FØRSTE_BARN + } + } + + BehandlingÅrsak.OMREGNING_6ÅR, + BehandlingÅrsak.OMREGNING_18ÅR, + BehandlingÅrsak.SMÅBARNSTILLEGG, + BehandlingÅrsak.OMREGNING_SMÅBARNSTILLEGG, + -> Brevmal.AUTOVEDTAK_BARN_6_OG_18_ÅR_OG_SMÅBARNSTILLEGG + + else -> throw Feil("Det er ikke laget funksjonalitet for automatisk behandling for $behandlingÅrsak") + } +} + +// Dokumenttittel legges på i familie-integrasjoner basert på dokumenttype +// Denne funksjonen bestemmer om dokumenttittelen skal overstyres eller ikke +fun hentOverstyrtDokumenttittel(behandling: Behandling): String? { + return if (behandling.type == BehandlingType.REVURDERING) { + behandling.opprettetÅrsak.hentOverstyrtDokumenttittelForOmregningsbehandling() ?: when { + listOf( + INNVILGET, + DELVIS_INNVILGET, + INNVILGET_OG_ENDRET, + INNVILGET_OG_OPPHØRT, + DELVIS_INNVILGET_OG_OPPHØRT, + ENDRET_OG_OPPHØRT, + ).contains(behandling.resultat) -> "Vedtak om endret barnetrygd" + + behandling.resultat.erFortsattInnvilget() -> "Vedtak om fortsatt barnetrygd" + else -> null + } + } else { + null + } +} + +fun hjemlerTilHjemmeltekst(hjemler: List, lovForHjemmel: String): String { + return when (hjemler.size) { + 0 -> throw Feil("Kan ikke lage hjemmeltekst for $lovForHjemmel når ingen begrunnelser har hjemler fra $lovForHjemmel knyttet til seg.") + 1 -> "§ ${hjemler[0]}" + else -> "§§ ${Utils.slåSammen(hjemler)}" + } +} + +fun hentHjemmeltekst( + minimerteVedtaksperioder: List, + sanityBegrunnelser: Map, + opplysningspliktHjemlerSkalMedIBrev: Boolean = false, + målform: Målform, + vedtakKorrigertHjemmelSkalMedIBrev: Boolean = false, + refusjonEøsHjemmelSkalMedIBrev: Boolean = false, +): String { + val sanityStandardbegrunnelser = minimerteVedtaksperioder.flatMap { vedtaksperiode -> + vedtaksperiode.begrunnelser.mapNotNull { begrunnelse -> + sanityBegrunnelser[begrunnelse.standardbegrunnelse] + } + } + + val sanityEøsBegrunnelser = minimerteVedtaksperioder.flatMap { vedtaksperiode -> + vedtaksperiode.eøsBegrunnelser.map { begrunnelse -> + begrunnelse.sanityEØSBegrunnelse + } + } + + val ordinæreHjemler = + hentOrdinæreHjemler( + hjemler = (sanityStandardbegrunnelser.flatMap { it.hjemler } + sanityEøsBegrunnelser.flatMap { it.hjemler }) + .toMutableSet(), + opplysningspliktHjemlerSkalMedIBrev = opplysningspliktHjemlerSkalMedIBrev, + finnesVedtaksperiodeMedFritekst = minimerteVedtaksperioder.flatMap { it.fritekster }.isNotEmpty(), + ) + + val forvaltningsloverHjemler = hentForvaltningsloverHjemler(vedtakKorrigertHjemmelSkalMedIBrev) + + val alleHjemlerForBegrunnelser = hentAlleTyperHjemler( + hjemlerSeparasjonsavtaleStorbritannia = sanityEøsBegrunnelser.flatMap { it.hjemlerSeperasjonsavtalenStorbritannina } + .distinct(), + ordinæreHjemler = ordinæreHjemler.distinct(), + hjemlerFraFolketrygdloven = (sanityStandardbegrunnelser.flatMap { it.hjemlerFolketrygdloven } + sanityEøsBegrunnelser.flatMap { it.hjemlerFolketrygdloven }) + .distinct(), + hjemlerEØSForordningen883 = sanityEøsBegrunnelser.flatMap { it.hjemlerEØSForordningen883 }.distinct(), + hjemlerEØSForordningen987 = hentHjemlerForEøsForordningen987(sanityEøsBegrunnelser, refusjonEøsHjemmelSkalMedIBrev), + målform = målform, + hjemlerFraForvaltningsloven = forvaltningsloverHjemler, + ) + + return slåSammenHjemlerAvUlikeTyper(alleHjemlerForBegrunnelser) +} + +private fun hentHjemlerForEøsForordningen987(sanityEøsBegrunnelser: List, refusjonEøsHjemmelSkalMedIBrev: Boolean): List { + val hjemler = mutableListOf() + + hjemler.addAll(sanityEøsBegrunnelser.flatMap { it.hjemlerEØSForordningen987 }) + + if (refusjonEøsHjemmelSkalMedIBrev) { + hjemler.add("60") + } + + return hjemler.distinct() +} + +private fun slåSammenHjemlerAvUlikeTyper(hjemler: List) = when (hjemler.size) { + 0 -> throw FunksjonellFeil("Ingen hjemler var knyttet til begrunnelsen(e) som er valgt. Du må velge minst én begrunnelse som er knyttet til en hjemmel.") + 1 -> hjemler.single() + else -> slåSammenListeMedHjemler(hjemler) +} + +private fun slåSammenListeMedHjemler(hjemler: List): String { + return hjemler.reduceIndexed { index, acc, s -> + when (index) { + 0 -> acc + s + hjemler.size - 1 -> "$acc og $s" + else -> "$acc, $s" + } + } +} + +private fun hentAlleTyperHjemler( + hjemlerSeparasjonsavtaleStorbritannia: List, + ordinæreHjemler: List, + hjemlerFraFolketrygdloven: List, + hjemlerEØSForordningen883: List, + hjemlerEØSForordningen987: List, + målform: Målform, + hjemlerFraForvaltningsloven: List, +): List { + val alleHjemlerForBegrunnelser = mutableListOf() + + // Rekkefølgen her er viktig + if (hjemlerSeparasjonsavtaleStorbritannia.isNotEmpty()) { + alleHjemlerForBegrunnelser.add( + "${ + when (målform) { + Målform.NB -> "Separasjonsavtalen mellom Storbritannia og Norge artikkel" + Målform.NN -> "Separasjonsavtalen mellom Storbritannia og Noreg artikkel" + } + } ${ + Utils.slåSammen( + hjemlerSeparasjonsavtaleStorbritannia, + ) + }", + ) + } + if (ordinæreHjemler.isNotEmpty()) { + alleHjemlerForBegrunnelser.add( + "${ + when (målform) { + Målform.NB -> "barnetrygdloven" + Målform.NN -> "barnetrygdlova" + } + } ${ + hjemlerTilHjemmeltekst( + hjemler = ordinæreHjemler, + lovForHjemmel = "barnetrygdloven", + ) + }", + ) + } + if (hjemlerFraFolketrygdloven.isNotEmpty()) { + alleHjemlerForBegrunnelser.add( + "${ + when (målform) { + Målform.NB -> "folketrygdloven" + Målform.NN -> "folketrygdlova" + } + } ${ + hjemlerTilHjemmeltekst( + hjemler = hjemlerFraFolketrygdloven, + lovForHjemmel = "folketrygdloven", + ) + }", + ) + } + if (hjemlerEØSForordningen883.isNotEmpty()) { + alleHjemlerForBegrunnelser.add("EØS-forordning 883/2004 artikkel ${Utils.slåSammen(hjemlerEØSForordningen883)}") + } + if (hjemlerEØSForordningen987.isNotEmpty()) { + alleHjemlerForBegrunnelser.add("EØS-forordning 987/2009 artikkel ${Utils.slåSammen(hjemlerEØSForordningen987)}") + } + if (hjemlerFraForvaltningsloven.isNotEmpty()) { + alleHjemlerForBegrunnelser.add( + "${ + when (målform) { + Målform.NB -> "forvaltningsloven" + Målform.NN -> "forvaltningslova" + } + } ${ + hjemlerTilHjemmeltekst(hjemler = hjemlerFraForvaltningsloven, lovForHjemmel = "forvaltningsloven") + }", + ) + } + return alleHjemlerForBegrunnelser +} + +private fun hentOrdinæreHjemler( + hjemler: MutableSet, + opplysningspliktHjemlerSkalMedIBrev: Boolean, + finnesVedtaksperiodeMedFritekst: Boolean, +): List { + if (opplysningspliktHjemlerSkalMedIBrev) { + val hjemlerNårOpplysningspliktIkkeOppfylt = listOf("17", "18") + hjemler.addAll(hjemlerNårOpplysningspliktIkkeOppfylt) + } + + if (finnesVedtaksperiodeMedFritekst) { + hjemler.addAll(hjemlerTilhørendeFritekst.map { it.toString() }.toSet()) + } + + val sorterteHjemler = hjemler.map { it.toInt() }.sorted().map { it.toString() } + return sorterteHjemler +} + +fun hentVirkningstidspunkt(opphørsperioder: List, behandlingId: Long) = ( + opphørsperioder + .maxOfOrNull { it.periodeFom } + ?.tilMånedÅr() + ?: throw Feil("Fant ikke opphørdato ved generering av dødsfallbrev på behandling $behandlingId") + ) + +fun hentForvaltningsloverHjemler(vedtakKorrigertHjemmelSkalMedIBrev: Boolean): List { + return if (vedtakKorrigertHjemmelSkalMedIBrev) listOf("35") else emptyList() +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevmalService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevmalService.kt new file mode 100644 index 000000000..df1b61b8a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevmalService.kt @@ -0,0 +1,218 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import org.springframework.stereotype.Service + +@Service +class BrevmalService( + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, +) { + + fun hentBrevmal(behandling: Behandling): Brevmal = + when (behandling.opprettetÅrsak) { + BehandlingÅrsak.DØDSFALL_BRUKER -> Brevmal.VEDTAK_OPPHØR_DØDSFALL + BehandlingÅrsak.KORREKSJON_VEDTAKSBREV -> Brevmal.VEDTAK_KORREKSJON_VEDTAKSBREV + else -> hentVedtaksbrevmal(behandling) + } + + fun hentVedtaksbrevmal(behandling: Behandling): Brevmal { + if (behandling.resultat == Behandlingsresultat.IKKE_VURDERT) { + throw Feil("Kan ikke opprette brev. Behandlingen er ikke vurdert.") + } + + val brevmal = if (behandling.skalBehandlesAutomatisk) { + hentAutomatiskVedtaksbrevtype(behandling) + } else { + hentManuellVedtaksbrevtype(behandling) + } + + return if (brevmal.erVedtaksbrev) brevmal else throw Feil("Brevmal ${brevmal.visningsTekst} er ikke vedtaksbrev") + } + + fun hentManuellVedtaksbrevtype( + behandling: Behandling, + ): Brevmal { + val behandlingType = behandling.type + val behandlingsresultat = behandling.resultat + val erInstitusjon = behandling.fagsak.institusjon != null + val ytelseErLøpende by lazy { + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandling.id) + .any { it.erLøpende() } + } + + val feilmeldingBehandlingTypeOgResultat = + "Brev ikke støttet for behandlingstype=$behandlingType og behandlingsresultat=$behandlingsresultat" + val feilmelidingBehandlingType = + "Brev ikke støttet for behandlingstype=$behandlingType" + val frontendFeilmelding = + "Vi finner ikke vedtaksbrev som matcher med behandlingen og resultatet du har fått. " + + "Meld sak i Porten slik at vi kan se nærmere på saken." + + return when (behandlingType) { + BehandlingType.FØRSTEGANGSBEHANDLING -> + if (erInstitusjon) { + when (behandlingsresultat) { + Behandlingsresultat.INNVILGET, + Behandlingsresultat.INNVILGET_OG_ENDRET, + Behandlingsresultat.INNVILGET_OG_OPPHØRT, + Behandlingsresultat.INNVILGET_ENDRET_OG_OPPHØRT, + Behandlingsresultat.DELVIS_INNVILGET, + Behandlingsresultat.DELVIS_INNVILGET_OG_ENDRET, + Behandlingsresultat.DELVIS_INNVILGET_OG_OPPHØRT, + Behandlingsresultat.DELVIS_INNVILGET_ENDRET_OG_OPPHØRT, + Behandlingsresultat.AVSLÅTT_OG_ENDRET, + Behandlingsresultat.AVSLÅTT_OG_OPPHØRT, + Behandlingsresultat.AVSLÅTT_ENDRET_OG_OPPHØRT, + -> Brevmal.VEDTAK_FØRSTEGANGSVEDTAK_INSTITUSJON + + Behandlingsresultat.AVSLÅTT -> Brevmal.VEDTAK_AVSLAG_INSTITUSJON + + Behandlingsresultat.ENDRET_OG_FORTSATT_INNVILGET, + Behandlingsresultat.ENDRET_UTBETALING, + Behandlingsresultat.ENDRET_UTEN_UTBETALING, + Behandlingsresultat.ENDRET_OG_OPPHØRT, + Behandlingsresultat.OPPHØRT, + Behandlingsresultat.FORTSATT_OPPHØRT, + Behandlingsresultat.FORTSATT_INNVILGET, + Behandlingsresultat.HENLAGT_FEILAKTIG_OPPRETTET, + Behandlingsresultat.HENLAGT_SØKNAD_TRUKKET, + Behandlingsresultat.HENLAGT_AUTOMATISK_FØDSELSHENDELSE, + Behandlingsresultat.HENLAGT_TEKNISK_VEDLIKEHOLD, + Behandlingsresultat.IKKE_VURDERT, + -> throw FunksjonellFeil( + melding = feilmeldingBehandlingTypeOgResultat, + frontendFeilmelding = frontendFeilmelding, + ) + } + } else { + when (behandlingsresultat) { + Behandlingsresultat.INNVILGET, + Behandlingsresultat.INNVILGET_OG_ENDRET, + Behandlingsresultat.INNVILGET_OG_OPPHØRT, + Behandlingsresultat.INNVILGET_ENDRET_OG_OPPHØRT, + Behandlingsresultat.DELVIS_INNVILGET, + Behandlingsresultat.DELVIS_INNVILGET_OG_ENDRET, + Behandlingsresultat.DELVIS_INNVILGET_OG_OPPHØRT, + Behandlingsresultat.DELVIS_INNVILGET_ENDRET_OG_OPPHØRT, + Behandlingsresultat.AVSLÅTT_OG_ENDRET, + Behandlingsresultat.AVSLÅTT_OG_OPPHØRT, + Behandlingsresultat.AVSLÅTT_ENDRET_OG_OPPHØRT, + -> Brevmal.VEDTAK_FØRSTEGANGSVEDTAK + + Behandlingsresultat.AVSLÅTT -> Brevmal.VEDTAK_AVSLAG + + Behandlingsresultat.ENDRET_OG_FORTSATT_INNVILGET, + Behandlingsresultat.ENDRET_UTBETALING, + Behandlingsresultat.ENDRET_UTEN_UTBETALING, + Behandlingsresultat.ENDRET_OG_OPPHØRT, + Behandlingsresultat.OPPHØRT, + Behandlingsresultat.FORTSATT_OPPHØRT, + Behandlingsresultat.FORTSATT_INNVILGET, + Behandlingsresultat.HENLAGT_FEILAKTIG_OPPRETTET, + Behandlingsresultat.HENLAGT_SØKNAD_TRUKKET, + Behandlingsresultat.HENLAGT_AUTOMATISK_FØDSELSHENDELSE, + Behandlingsresultat.HENLAGT_TEKNISK_VEDLIKEHOLD, + Behandlingsresultat.IKKE_VURDERT, + -> throw FunksjonellFeil( + melding = feilmeldingBehandlingTypeOgResultat, + frontendFeilmelding = frontendFeilmelding, + ) + } + } + + BehandlingType.REVURDERING -> + if (erInstitusjon) { + when (behandlingsresultat) { + Behandlingsresultat.INNVILGET, + Behandlingsresultat.INNVILGET_OG_ENDRET, + Behandlingsresultat.INNVILGET_OG_OPPHØRT, + Behandlingsresultat.INNVILGET_ENDRET_OG_OPPHØRT, + Behandlingsresultat.DELVIS_INNVILGET, + Behandlingsresultat.DELVIS_INNVILGET_OG_ENDRET, + Behandlingsresultat.DELVIS_INNVILGET_OG_OPPHØRT, + Behandlingsresultat.DELVIS_INNVILGET_ENDRET_OG_OPPHØRT, + Behandlingsresultat.AVSLÅTT_OG_ENDRET, + Behandlingsresultat.AVSLÅTT_OG_OPPHØRT, + Behandlingsresultat.AVSLÅTT_ENDRET_OG_OPPHØRT, + Behandlingsresultat.ENDRET_UTBETALING, + Behandlingsresultat.ENDRET_OG_OPPHØRT, + -> if (ytelseErLøpende) Brevmal.VEDTAK_ENDRING_INSTITUSJON else Brevmal.VEDTAK_OPPHØR_MED_ENDRING_INSTITUSJON + + Behandlingsresultat.OPPHØRT, + Behandlingsresultat.FORTSATT_OPPHØRT, + -> Brevmal.VEDTAK_OPPHØRT_INSTITUSJON + + Behandlingsresultat.FORTSATT_INNVILGET, + Behandlingsresultat.ENDRET_OG_FORTSATT_INNVILGET, + -> Brevmal.VEDTAK_FORTSATT_INNVILGET_INSTITUSJON + + Behandlingsresultat.AVSLÅTT -> Brevmal.VEDTAK_AVSLAG_INSTITUSJON + + Behandlingsresultat.ENDRET_UTEN_UTBETALING, + Behandlingsresultat.HENLAGT_FEILAKTIG_OPPRETTET, + Behandlingsresultat.HENLAGT_SØKNAD_TRUKKET, + Behandlingsresultat.HENLAGT_AUTOMATISK_FØDSELSHENDELSE, + Behandlingsresultat.HENLAGT_TEKNISK_VEDLIKEHOLD, + Behandlingsresultat.IKKE_VURDERT, + -> throw FunksjonellFeil( + melding = feilmeldingBehandlingTypeOgResultat, + frontendFeilmelding = frontendFeilmelding, + ) + } + } else { + when (behandlingsresultat) { + Behandlingsresultat.INNVILGET, + Behandlingsresultat.INNVILGET_OG_ENDRET, + Behandlingsresultat.INNVILGET_OG_OPPHØRT, + Behandlingsresultat.INNVILGET_ENDRET_OG_OPPHØRT, + Behandlingsresultat.DELVIS_INNVILGET, + Behandlingsresultat.DELVIS_INNVILGET_OG_ENDRET, + Behandlingsresultat.DELVIS_INNVILGET_OG_OPPHØRT, + Behandlingsresultat.DELVIS_INNVILGET_ENDRET_OG_OPPHØRT, + Behandlingsresultat.AVSLÅTT_OG_ENDRET, + Behandlingsresultat.AVSLÅTT_OG_OPPHØRT, + Behandlingsresultat.AVSLÅTT_ENDRET_OG_OPPHØRT, + Behandlingsresultat.ENDRET_UTBETALING, + Behandlingsresultat.ENDRET_OG_OPPHØRT, + -> if (ytelseErLøpende) Brevmal.VEDTAK_ENDRING else Brevmal.VEDTAK_OPPHØR_MED_ENDRING + + Behandlingsresultat.OPPHØRT, + Behandlingsresultat.FORTSATT_OPPHØRT, + -> Brevmal.VEDTAK_OPPHØRT + + Behandlingsresultat.FORTSATT_INNVILGET, + Behandlingsresultat.ENDRET_OG_FORTSATT_INNVILGET, + -> Brevmal.VEDTAK_FORTSATT_INNVILGET + + Behandlingsresultat.AVSLÅTT -> Brevmal.VEDTAK_AVSLAG + Behandlingsresultat.ENDRET_UTEN_UTBETALING, + Behandlingsresultat.HENLAGT_FEILAKTIG_OPPRETTET, + Behandlingsresultat.HENLAGT_SØKNAD_TRUKKET, + Behandlingsresultat.HENLAGT_AUTOMATISK_FØDSELSHENDELSE, + Behandlingsresultat.HENLAGT_TEKNISK_VEDLIKEHOLD, + Behandlingsresultat.IKKE_VURDERT, + -> throw FunksjonellFeil( + melding = feilmeldingBehandlingTypeOgResultat, + frontendFeilmelding = frontendFeilmelding, + ) + } + } + + BehandlingType.MIGRERING_FRA_INFOTRYGD, + BehandlingType.MIGRERING_FRA_INFOTRYGD_OPPHØRT, + BehandlingType.TEKNISK_OPPHØR, + BehandlingType.TEKNISK_ENDRING, + -> throw FunksjonellFeil( + melding = feilmelidingBehandlingType, + frontendFeilmelding = frontendFeilmelding, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumenDistribueringUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumenDistribueringUtils.kt new file mode 100644 index 000000000..03cb9f055 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumenDistribueringUtils.kt @@ -0,0 +1,20 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.http.client.RessursException +import org.springframework.http.HttpStatus + +// 410 GONE er unikt for bruker død og ingen dødsboadresse mot Dokdist +// https://nav-it.slack.com/archives/C6W9E5GPJ/p1647956660364779?thread_ts=1647936835.099329&cid=C6W9E5GPJ +fun mottakerErDødUtenDødsboadresse(ressursException: RessursException): Boolean = + ressursException.httpStatus == HttpStatus.GONE + +// 400 BAD_REQUEST + kanal print er eneste måten å vite at bruker ikke er digital og har ukjent adresse fra Dokdist +// https://nav-it.slack.com/archives/C6W9E5GPJ/p1647947002270879?thread_ts=1647936835.099329&cid=C6W9E5GPJ +fun mottakerErIkkeDigitalOgHarUkjentAdresse(ressursException: RessursException) = + ressursException.httpStatus == HttpStatus.BAD_REQUEST && + ressursException.cause?.message?.contains("Mottaker har ukjent adresse") == true + +// 409 Conflict betyr duplikatdistribusjon +// https://nav-it.slack.com/archives/C6W9E5GPJ/p1657610907144549?thread_ts=1657610829.116619&cid=C6W9E5GPJ +fun dokumentetErAlleredeDistribuert(ressursException: RessursException) = + ressursException.httpStatus == HttpStatus.CONFLICT diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentController.kt new file mode 100644 index 000000000..b69356d1c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentController.kt @@ -0,0 +1,179 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestMinimalFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.brev.domene.ManueltBrevRequest +import no.nav.familie.ba.sak.kjerne.brev.domene.byggMottakerdata +import no.nav.familie.ba.sak.kjerne.brev.domene.leggTilEnhet +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.slf4j.LoggerFactory +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/dokument") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class DokumentController( + private val fagsakService: FagsakService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val dokumentService: DokumentService, + private val dokumentGenereringService: DokumentGenereringService, + private val vedtakService: VedtakService, + private val tilgangService: TilgangService, + private val persongrunnlagService: PersongrunnlagService, + private val arbeidsfordelingService: ArbeidsfordelingService, + private val utvidetBehandlingService: UtvidetBehandlingService, +) { + + @PostMapping(path = ["vedtaksbrev/{vedtakId}"]) + fun genererVedtaksbrev(@PathVariable vedtakId: Long): Ressurs { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} generer vedtaksbrev") + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "generere vedtaksbrev", + ) + + val vedtak = vedtakService.hent(vedtakId) + tilgangService.validerTilgangTilBehandling(behandlingId = vedtak.behandling.id, event = AuditLoggerEvent.UPDATE) + + return dokumentGenereringService.genererBrevForVedtak(vedtak).let { + vedtak.stønadBrevPdF = it + vedtakService.oppdater(vedtak) + Ressurs.success(it) + } + } + + @GetMapping(path = ["vedtaksbrev/{vedtakId}"]) + fun hentVedtaksbrev(@PathVariable vedtakId: Long): Ressurs { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} henter vedtaksbrev") + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.VEILEDER, + handling = "hente vedtaksbrev", + ) + + val vedtak = vedtakService.hent(vedtakId) + + tilgangService.validerTilgangTilBehandling(behandlingId = vedtak.behandling.id, event = AuditLoggerEvent.ACCESS) + + return dokumentService.hentBrevForVedtak(vedtak) + } + + @PostMapping(path = ["forhaandsvis-brev/{behandlingId}"]) + fun hentForhåndsvisning( + @PathVariable behandlingId: Long, + @RequestBody manueltBrevRequest: ManueltBrevRequest, + ): Ressurs { + logger.info( + "${SikkerhetContext.hentSaksbehandlerNavn()} henter forhåndsvisning av brev " + + "for mal: ${manueltBrevRequest.brevmal}", + ) + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "hente forhåndsvisning brev", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + + return dokumentGenereringService.genererManueltBrev( + manueltBrevRequest = manueltBrevRequest.byggMottakerdata( + behandling, + persongrunnlagService, + arbeidsfordelingService, + ), + erForhåndsvisning = true, + ).let { Ressurs.success(it) } + } + + @PostMapping(path = ["send-brev/{behandlingId}"]) + fun sendBrev( + @PathVariable behandlingId: Long, + @RequestBody manueltBrevRequest: ManueltBrevRequest, + ): ResponseEntity> { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} genererer og sender brev: ${manueltBrevRequest.brevmal}") + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "sende brev", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + + dokumentService.sendManueltBrev( + manueltBrevRequest = manueltBrevRequest.byggMottakerdata( + behandling, + persongrunnlagService, + arbeidsfordelingService, + ), + behandling = behandling, + fagsakId = behandling.fagsak.id, + ) + return ResponseEntity.ok( + Ressurs.success( + utvidetBehandlingService + .lagRestUtvidetBehandling(behandlingId = behandlingId), + ), + ) + } + + @PostMapping(path = ["/fagsak/{fagsakId}/forhaandsvis-brev"]) + fun hentForhåndsvisningPåFagsak( + @PathVariable fagsakId: Long, + @RequestBody manueltBrevRequest: ManueltBrevRequest, + ): Ressurs { + logger.info( + "${SikkerhetContext.hentSaksbehandlerNavn()} henter forhåndsvisning av brev på fagsak $fagsakId " + + "for mal: ${manueltBrevRequest.brevmal}", + ) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "hente forhåndsvisning brev", + ) + + return dokumentGenereringService.genererManueltBrev( + manueltBrevRequest = manueltBrevRequest.leggTilEnhet(arbeidsfordelingService), + erForhåndsvisning = true, + ).let { Ressurs.success(it) } + } + + @PostMapping(path = ["/fagsak/{fagsakId}/send-brev"]) + fun sendBrevPåFagsak( + @PathVariable fagsakId: Long, + @RequestBody manueltBrevRequest: ManueltBrevRequest, + ): ResponseEntity> { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} genererer og sender brev på fagsak $fagsakId: ${manueltBrevRequest.brevmal}") + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "sende brev", + ) + + dokumentService.sendManueltBrev( + manueltBrevRequest = manueltBrevRequest.leggTilEnhet(arbeidsfordelingService), + fagsakId = fagsakId, + ) + return ResponseEntity.ok(Ressurs.success(fagsakService.lagRestMinimalFagsak(fagsakId = fagsakId))) + } + + companion object { + + private val logger = LoggerFactory.getLogger(DokumentController::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentDistribueringService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentDistribueringService.kt new file mode 100644 index 000000000..f97e185af --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentDistribueringService.kt @@ -0,0 +1,144 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.task.DistribuerDokumentDTO +import no.nav.familie.ba.sak.task.DistribuerDokumentPåJournalpostIdTask +import no.nav.familie.http.client.RessursException +import no.nav.familie.prosessering.internal.TaskService +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class DokumentDistribueringService( + private val taskService: TaskService, + private val integrasjonClient: IntegrasjonClient, + private val loggService: LoggService, +) { + + fun prøvDistribuerBrevOgLoggHendelse( + distribuerDokumentDTO: DistribuerDokumentDTO, + loggBehandlerRolle: BehandlerRolle, + ) = try { + distribuerBrevOgLoggHendelse(distribuerDokumentDTO, loggBehandlerRolle) + } catch (ressursException: RessursException) { + val journalpostId = distribuerDokumentDTO.journalpostId + val behandlingId = distribuerDokumentDTO.behandlingId + + logger.info( + "Klarte ikke å distribuere brev til journalpost $journalpostId på behandling $behandlingId. " + + "Httpstatus ${ressursException.httpStatus}", + ) + secureLogger.info( + "Klarte ikke å distribuere brev til journalpost $journalpostId på behandling $behandlingId.\n" + + "Httpstatus: ${ressursException.httpStatus}\n" + + "Melding: ${ressursException.cause?.message}", + ) + + if (dokumentetErAlleredeDistribuert(ressursException)) { + logger.warn(alleredeDistribuertMelding(journalpostId, behandlingId)) + } else { + throw ressursException + } + } + + fun prøvDistribuerBrevOgLoggHendelseFraBehandling( + distribuerDokumentDTO: DistribuerDokumentDTO, + loggBehandlerRolle: BehandlerRolle, + ) { + try { + prøvDistribuerBrevOgLoggHendelse(distribuerDokumentDTO, loggBehandlerRolle) + } catch (ressursException: RessursException) { + val journalpostId = distribuerDokumentDTO.journalpostId + val behandlingId = distribuerDokumentDTO.behandlingId + val brevmal = distribuerDokumentDTO.brevmal + + when { + mottakerErDødUtenDødsboadresse(ressursException) && behandlingId != null -> + opprettLogginnslagPåBehandlingOgNyTaskSomDistribuererPåJournalpostId(distribuerDokumentDTO) + + mottakerErIkkeDigitalOgHarUkjentAdresse(ressursException) && behandlingId != null -> + loggBrevIkkeDistribuertUkjentAdresse(journalpostId, behandlingId, brevmal) + + else -> throw ressursException + } + } + } + + internal fun opprettLogginnslagPåBehandlingOgNyTaskSomDistribuererPåJournalpostId(distribuerDokumentDTO: DistribuerDokumentDTO) { + val task = DistribuerDokumentPåJournalpostIdTask.opprettTask(distribuerDokumentDTO.copy(behandlingId = null)) + taskService.save(task) + + logger.info( + "Klarte ikke å distribuere brev for journalpostId ${distribuerDokumentDTO.journalpostId} " + + "på behandling ${distribuerDokumentDTO.behandlingId}. Bruker har ukjent dødsboadresse.", + ) + + loggService.opprettBrevIkkeDistribuertUkjentDødsboadresseLogg( + behandlingId = checkNotNull(distribuerDokumentDTO.behandlingId), + brevnavn = distribuerDokumentDTO.brevmal.visningsTekst, + ) + } + + internal fun loggBrevIkkeDistribuertUkjentAdresse( + journalpostId: String, + behandlingId: Long, + brevMal: Brevmal, + ) { + logger.info("Klarte ikke å distribuere brev for journalpostId $journalpostId på behandling $behandlingId. Bruker har ukjent adresse.") + loggService.opprettBrevIkkeDistribuertUkjentAdresseLogg( + behandlingId = behandlingId, + brevnavn = brevMal.visningsTekst, + ) + antallBrevIkkeDistribuertUkjentAndresse[brevMal]?.increment() + } + + private fun distribuerBrevOgLoggHendelse( + distribuerDokumentDTO: DistribuerDokumentDTO, + loggBehandlerRolle: BehandlerRolle, + ) { + val brevmal = distribuerDokumentDTO.brevmal + integrasjonClient.distribuerBrev(distribuerDokumentDTO) + + if (distribuerDokumentDTO.behandlingId != null) { + loggService.opprettDistribuertBrevLogg( + behandlingId = distribuerDokumentDTO.behandlingId, + tekst = brevmal.visningsTekst, + rolle = loggBehandlerRolle, + ) + } + + antallBrevSendt[brevmal]?.increment() + } + + private val antallBrevSendt: Map = mutableListOf().plus(Brevmal.values()).associateWith { + Metrics.counter( + "brev.sendt", + "brevtype", + it.visningsTekst, + ) + } + + private val antallBrevIkkeDistribuertUkjentAndresse: Map = + mutableListOf().plus(Brevmal.values()).associateWith { + Metrics.counter( + "brev.ikke.sendt.ukjent.andresse", + "brevtype", + it.visningsTekst, + ) + } + + fun alleredeDistribuertMelding(journalpostId: String, behandlingId: Long?) = + "Journalpost med Id=$journalpostId er allerede distiribuert. Hopper over distribuering." + + if (behandlingId != null) " BehandlingId=$behandlingId." else "" + + companion object { + val logger: Logger = LoggerFactory.getLogger(this::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentGenereringService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentGenereringService.kt new file mode 100644 index 000000000..3e65e34f9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentGenereringService.kt @@ -0,0 +1,77 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.brev.domene.ManueltBrevRequest +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brev +import no.nav.familie.ba.sak.kjerne.brev.domene.tilBrev +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.sikkerhet.SaksbehandlerContext +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import java.lang.Exception + +@Service +class DokumentGenereringService( + private val persongrunnlagService: PersongrunnlagService, + private val brevService: BrevService, + private val brevKlient: BrevKlient, + private val integrasjonClient: IntegrasjonClient, + private val saksbehandlerContext: SaksbehandlerContext, +) { + + fun genererBrevForVedtak(vedtak: Vedtak): ByteArray { + try { + if (!vedtak.behandling.skalBehandlesAutomatisk && vedtak.behandling.steg > StegType.BESLUTTE_VEDTAK) { + throw FunksjonellFeil("Ikke tillatt å generere brev etter at behandlingen er sendt fra beslutter") + } + + val målform = persongrunnlagService.hentSøkersMålform(vedtak.behandling.id) + val vedtaksbrev = + when (vedtak.behandling.opprettetÅrsak) { + BehandlingÅrsak.DØDSFALL_BRUKER -> brevService.hentDødsfallbrevData(vedtak) + BehandlingÅrsak.KORREKSJON_VEDTAKSBREV -> brevService.hentKorreksjonbrevData(vedtak) + else -> brevService.hentVedtaksbrevData(vedtak) + } + return brevKlient.genererBrev(målform.tilSanityFormat(), vedtaksbrev) + } catch (feil: Throwable) { + if (feil is FunksjonellFeil) throw feil + + throw Feil( + message = "Klarte ikke generere vedtaksbrev på behandling ${vedtak.behandling}: ${feil.message}", + frontendFeilmelding = "Det har skjedd en feil, og brevet er ikke sendt. Prøv igjen, og ta kontakt med brukerstøtte hvis problemet vedvarer.", + httpStatus = HttpStatus.INTERNAL_SERVER_ERROR, + throwable = feil, + ) + } + } + + fun genererManueltBrev( + manueltBrevRequest: ManueltBrevRequest, + erForhåndsvisning: Boolean = false, + ): ByteArray { + try { + val brev: Brev = + manueltBrevRequest.tilBrev(saksbehandlerContext.hentSaksbehandlerSignaturTilBrev()) { integrasjonClient.hentLandkoderISO2() } + return brevKlient.genererBrev( + målform = manueltBrevRequest.mottakerMålform.tilSanityFormat(), + brev = brev, + ) + } catch (exception: Exception) { + if (exception is Feil || exception is FunksjonellFeil) { + throw exception + } + + throw Feil( + message = "Klarte ikke generere brev for ${manueltBrevRequest.brevmal}. ${exception.message}", + frontendFeilmelding = "${if (erForhåndsvisning) "Det har skjedd en feil" else "Det har skjedd en feil, og brevet er ikke sendt"}. Prøv igjen, og ta kontakt med brukerstøtte hvis problemet vedvarer.", + httpStatus = HttpStatus.INTERNAL_SERVER_ERROR, + throwable = exception, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentService.kt new file mode 100644 index 000000000..8cf67fd49 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentService.kt @@ -0,0 +1,235 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.config.RolleConfig +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.DEFAULT_JOURNALFØRENDE_ENHET +import no.nav.familie.ba.sak.integrasjoner.journalføring.UtgåendeJournalføringService +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.DbJournalpost +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.DbJournalpostType +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.JournalføringRepository +import no.nav.familie.ba.sak.integrasjoner.organisasjon.OrganisasjonService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.ValiderBrevmottakerService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentService +import no.nav.familie.ba.sak.kjerne.brev.domene.ManueltBrevRequest +import no.nav.familie.ba.sak.kjerne.brev.domene.erTilInstitusjon +import no.nav.familie.ba.sak.kjerne.brev.mottaker.BrevmottakerService +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.kjerne.steg.domene.MottakerInfo +import no.nav.familie.ba.sak.kjerne.steg.domene.toList +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.VilkårsvurderingForNyBehandlingService +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurderingType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.leggTilBlankAnnenVurdering +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.task.DistribuerDokumentDTO +import no.nav.familie.ba.sak.task.DistribuerDokumentTask +import no.nav.familie.kontrakter.felles.BrukerIdType +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.dokarkiv.AvsenderMottaker +import no.nav.familie.kontrakter.felles.dokarkiv.v2.Førsteside +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.util.Properties + +@Service +class DokumentService( + private val journalføringRepository: JournalføringRepository, + private val taskRepository: TaskRepositoryWrapper, + private val vilkårsvurderingService: VilkårsvurderingService, + private val vilkårsvurderingForNyBehandlingService: VilkårsvurderingForNyBehandlingService, + private val rolleConfig: RolleConfig, + private val settPåVentService: SettPåVentService, + private val utgåendeJournalføringService: UtgåendeJournalføringService, + private val fagsakRepository: FagsakRepository, + private val organisasjonService: OrganisasjonService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val dokumentGenereringService: DokumentGenereringService, + private val brevmottakerService: BrevmottakerService, + private val validerBrevmottakerService: ValiderBrevmottakerService, +) { + + val logger: Logger = LoggerFactory.getLogger(this::class.java) + + fun hentBrevForVedtak(vedtak: Vedtak): Ressurs { + if (SikkerhetContext.hentHøyesteRolletilgangForInnloggetBruker(rolleConfig) == BehandlerRolle.VEILEDER && vedtak.stønadBrevPdF == null) { + throw FunksjonellFeil("Det finnes ikke noe vedtaksbrev.") + } else { + val pdf = + vedtak.stønadBrevPdF ?: throw Feil("Klarte ikke finne vedtaksbrevbrev for vedtak med id ${vedtak.id}") + return Ressurs.success(pdf) + } + } + + @Transactional + fun sendManueltBrev(manueltBrevRequest: ManueltBrevRequest, behandling: Behandling? = null, fagsakId: Long) { + behandling?.let { + validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligePersonerMedManuelleBrevmottakere(behandlingId = it.id) + } + val generertBrev = dokumentGenereringService.genererManueltBrev(manueltBrevRequest) + val førsteside = if (manueltBrevRequest.brevmal.skalGenerereForside()) { + Førsteside( + språkkode = manueltBrevRequest.mottakerMålform.tilSpråkkode(), + navSkjemaId = "NAV 33.00-07", + overskriftstittel = "Ettersendelse til søknad om barnetrygd ordinær NAV 33-00.07", + ) + } else { + null + } + + val fagsak = fagsakRepository.finnFagsak(fagsakId) ?: error("Finnes ikke fagsak for fagsakId=$fagsakId") + val søkersident = fagsak.aktør.aktivFødselsnummer() + + val mottakere = lagMottakere(manueltBrevRequest, fagsak, behandling) + val journalposterTilDistribusjon = mutableMapOf() + + mottakere.forEach { mottakerInfo -> + val journalpostId = utgåendeJournalføringService.journalførManueltBrev( + fnr = fagsak.aktør.aktivFødselsnummer(), + fagsakId = fagsakId.toString(), + journalførendeEnhet = manueltBrevRequest.enhet?.enhetId ?: DEFAULT_JOURNALFØRENDE_ENHET, + brev = generertBrev, + førsteside = førsteside, + dokumenttype = manueltBrevRequest.brevmal.tilFamilieKontrakterDokumentType(), + avsenderMottaker = utledAvsenderMottaker(manueltBrevRequest, mottakerInfo), + // mottakersnavn fyller ut kun når manuell mottaker finnes + tilManuellMottakerEllerVerge = mottakerInfo.navn != null && + mottakerInfo.navn != brevmottakerService.hentMottakerNavn(søkersident), + ).also { journalposterTilDistribusjon[it] = mottakerInfo } + + behandling?.let { + journalføringRepository.save( + DbJournalpost(behandling = it, journalpostId = journalpostId, type = DbJournalpostType.U), + ) + } + } + + if (behandling != null && manueltBrevRequest.brevmal.førerTilOpplysningsplikt()) { + leggTilOpplysningspliktIVilkårsvurdering(behandling) + } + + lagTaskerForÅDistribuereBrev(journalposterTilDistribusjon, behandling, manueltBrevRequest, fagsak) + + if (behandling != null && manueltBrevRequest.brevmal.setterBehandlingPåVent()) { + settPåVentService.settBehandlingPåVent( + behandlingId = behandling.id, + frist = LocalDate.now() + .plusDays( + manueltBrevRequest.brevmal.ventefristDager( + manuellFrist = manueltBrevRequest.antallUkerSvarfrist?.let { it * 7 }?.toLong(), + behandlingKategori = behandling.kategori, + ), + ), + årsak = manueltBrevRequest.brevmal.venteårsak(), + ) + } + } + + private fun lagMottakere( + manueltBrevRequest: ManueltBrevRequest, + fagsak: Fagsak, + behandling: Behandling?, + ): List { + val søkersident = fagsak.aktør.aktivFødselsnummer() + val brevmottakere = behandling?.let { brevmottakerService.hentBrevmottakere(it.id) } ?: emptyList() + return when { + behandling == null -> MottakerInfo( + brukerId = søkersident, + brukerIdType = BrukerIdType.FNR, + erInstitusjonVerge = false, + ).toList() + manueltBrevRequest.erTilInstitusjon -> MottakerInfo( + brukerId = checkNotNull(fagsak.institusjon).orgNummer, + brukerIdType = BrukerIdType.ORGNR, + erInstitusjonVerge = false, + ).toList() + brevmottakere.isNotEmpty() -> brevmottakerService.lagMottakereFraBrevMottakere( + brevmottakere, + søkersident, + ) + else -> MottakerInfo( + brukerIdType = BrukerIdType.FNR, + brukerId = søkersident, + erInstitusjonVerge = false, + ).toList() + } + } + + private fun utledAvsenderMottaker( + manueltBrevRequest: ManueltBrevRequest, + mottakerInfo: MottakerInfo, + ): AvsenderMottaker? { + return when { + manueltBrevRequest.erTilInstitusjon -> { + AvsenderMottaker( + idType = BrukerIdType.ORGNR, + id = manueltBrevRequest.mottakerIdent, + navn = utledInstitusjonNavn(manueltBrevRequest), + ) + } + mottakerInfo.brukerIdType != BrukerIdType.ORGNR && mottakerInfo.navn != null -> { + AvsenderMottaker( + idType = mottakerInfo.brukerIdType, + id = mottakerInfo.brukerIdType?.let { mottakerInfo.brukerId }, + navn = mottakerInfo.navn, + ) + } + else -> { + null + } + } + } + + private fun utledInstitusjonNavn(manueltBrevRequest: ManueltBrevRequest): String { + return manueltBrevRequest.mottakerNavn.ifBlank { + organisasjonService.hentOrganisasjon(manueltBrevRequest.mottakerIdent).navn + } + } + + private fun leggTilOpplysningspliktIVilkårsvurdering(behandling: Behandling) { + val vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandling(behandling.id) + ?: vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling, + bekreftEndringerViaFrontend = false, + forrigeBehandlingSomErVedtatt = behandlingHentOgPersisterService + .hentForrigeBehandlingSomErVedtatt(behandling), + ) + vilkårsvurdering.personResultater.single { it.erSøkersResultater() } + .leggTilBlankAnnenVurdering(AnnenVurderingType.OPPLYSNINGSPLIKT) + } + + private fun lagTaskerForÅDistribuereBrev( + journalposterTilDistribusjon: Map, + behandling: Behandling?, + manueltBrevRequest: ManueltBrevRequest, + fagsak: Fagsak, + ) = journalposterTilDistribusjon.forEach { journalPostTilDistribusjon -> + DistribuerDokumentTask.opprettDistribuerDokumentTask( + distribuerDokumentDTO = DistribuerDokumentDTO( + personEllerInstitusjonIdent = journalPostTilDistribusjon.value.brukerId, + behandlingId = behandling?.id, + journalpostId = journalPostTilDistribusjon.key, + brevmal = manueltBrevRequest.brevmal, + erManueltSendt = true, + manuellAdresseInfo = journalPostTilDistribusjon.value.manuellAdresseInfo, + ), + properties = Properties().apply + { + this["fagsakIdent"] = fagsak.aktør.aktivFødselsnummer() + this["mottakerIdent"] = journalPostTilDistribusjon.value.brukerId + this["journalpostId"] = journalPostTilDistribusjon.key + this["behandlingId"] = behandling?.id.toString() + this["fagsakId"] = fagsak.id.toString() + }, + ).also { taskRepository.save(it) } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/Utgj\303\270rendeVilk\303\245rUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/Utgj\303\270rendeVilk\303\245rUtils.kt" new file mode 100644 index 000000000..21bd36c62 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/Utgj\303\270rendeVilk\303\245rUtils.kt" @@ -0,0 +1,305 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.overlapperHeltEllerDelvisMed +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertRestPersonResultat +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertVilkårResultat +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.TriggesAv +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vedtak.domene.MinimertRestPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår + +/** + * Funksjonen henter personer som trigger den gitte vedtaksperioden ved å hente vilkårResultater + * basert på de attributter som definerer om en vedtaksbegrunnelse er trigget for en periode. + * + * @param minimertePersonResultater - Resultatene fra vilkårsvurderingen for hver person + * @param vedtaksperiode - Perioden det skal sjekkes for + * @param oppdatertBegrunnelseType - Begrunnelsestype det skal sjekkes for + * @param aktuellePersonerForVedtaksperiode - Personer på behandlingen som er aktuelle for vedtaksperioden + * @param triggesAv - Hva som trigger en vedtaksbegrynnelse. + * @param erFørsteVedtaksperiodePåFagsak - Om vedtaksperioden er første periode på fagsak. + * Brukes for opphør som har egen logikk dersom det er første periode. + * @return List med personene det trigges endring på + */ + +fun hentPersonerForAlleUtgjørendeVilkår( + minimertePersonResultater: List, + vedtaksperiode: Periode, + oppdatertBegrunnelseType: VedtakBegrunnelseType, + aktuellePersonerForVedtaksperiode: List, + triggesAv: TriggesAv, + begrunnelse: IVedtakBegrunnelse, + erFørsteVedtaksperiodePåFagsak: Boolean, +): Set { + return triggesAv.vilkår.fold(setOf()) { acc, vilkår -> + acc + hentPersonerMedUtgjørendeVilkår( + minimertRestPersonResultater = minimertePersonResultater, + vedtaksperiode = vedtaksperiode, + begrunnelseType = oppdatertBegrunnelseType, + vilkårGjeldendeForBegrunnelse = vilkår, + aktuellePersonerForVedtaksperiode = aktuellePersonerForVedtaksperiode, + triggesAv = triggesAv, + begrunnelse = begrunnelse, + erFørsteVedtaksperiodePåFagsak = erFørsteVedtaksperiodePåFagsak, + ) + } +} + +private fun hentPersonerMedUtgjørendeVilkår( + minimertRestPersonResultater: List, + vedtaksperiode: Periode, + begrunnelseType: VedtakBegrunnelseType, + vilkårGjeldendeForBegrunnelse: Vilkår, + aktuellePersonerForVedtaksperiode: List, + triggesAv: TriggesAv, + begrunnelse: IVedtakBegrunnelse, + erFørsteVedtaksperiodePåFagsak: Boolean, +): List { + val aktuellePersonidenter = aktuellePersonerForVedtaksperiode.map { it.personIdent } + + return minimertRestPersonResultater + .filter { aktuellePersonidenter.contains(it.personIdent) } + .fold(mutableListOf()) { acc, personResultat -> + val utgjørendeVilkårResultat = + personResultat.minimerteVilkårResultater + .filter { it.vilkårType == vilkårGjeldendeForBegrunnelse } + .firstOrNull { minimertVilkårResultat -> + val nesteMinimerteVilkårResultatAvSammeType: MinimertVilkårResultat? = + personResultat.minimerteVilkårResultater.finnEtterfølgende(minimertVilkårResultat) + erVilkårResultatUtgjørende( + minimertVilkårResultat = minimertVilkårResultat, + nesteMinimerteVilkårResultat = nesteMinimerteVilkårResultatAvSammeType, + forrigeMinimerteVilkårResultat = personResultat.minimerteVilkårResultater.finnForeliggende( + minimertVilkårResultat, + ), + begrunnelseType = begrunnelseType, + triggesAv = triggesAv, + vedtaksperiode = vedtaksperiode, + erFørsteVedtaksperiodePåFagsak = erFørsteVedtaksperiodePåFagsak, + begrunnelse = begrunnelse, + ) + } + + val person = aktuellePersonerForVedtaksperiode.firstOrNull { person -> + person.personIdent == personResultat.personIdent + } + + if (utgjørendeVilkårResultat != null && person != null) { + acc.add(person) + } + acc + } +} + +private fun List.finnEtterfølgende( + minimertVilkårResultat: MinimertVilkårResultat, +): MinimertVilkårResultat? = + minimertVilkårResultat.periodeTom?.let { tom -> this.find { it.periodeFom?.isEqual(tom.plusDays(1)) == true } } + +private fun List.finnForeliggende( + minimertVilkårResultat: MinimertVilkårResultat, +): MinimertVilkårResultat? = + minimertVilkårResultat.let { vilkårResultat -> + this.find { + it.periodeTom?.isEqual( + vilkårResultat.periodeFom?.minusDays( + 1, + ), + ) == true && it.vilkårType == vilkårResultat.vilkårType + } + } + +private fun erVilkårResultatUtgjørende( + minimertVilkårResultat: MinimertVilkårResultat, + nesteMinimerteVilkårResultat: MinimertVilkårResultat?, + forrigeMinimerteVilkårResultat: MinimertVilkårResultat?, + begrunnelseType: VedtakBegrunnelseType, + triggesAv: TriggesAv, + begrunnelse: IVedtakBegrunnelse, + vedtaksperiode: Periode, + erFørsteVedtaksperiodePåFagsak: Boolean, +): Boolean { + if (minimertVilkårResultat.periodeFom == null && !begrunnelseType.erAvslag()) { + return false + } + + return when (begrunnelseType) { + VedtakBegrunnelseType.INNVILGET, + VedtakBegrunnelseType.INSTITUSJON_INNVILGET, + -> + erInnvilgetVilkårResultatUtgjørende( + triggesAv, + minimertVilkårResultat, + forrigeMinimerteVilkårResultat, + vedtaksperiode, + ) + + VedtakBegrunnelseType.OPPHØR, + VedtakBegrunnelseType.INSTITUSJON_OPPHØR, + -> if (triggesAv.gjelderFørstePeriode) { + erFørstePeriodeOgVilkårIkkeOppfylt( + erFørsteVedtaksperiodePåFagsak = erFørsteVedtaksperiodePåFagsak, + vedtaksperiode = vedtaksperiode, + triggesAv = triggesAv, + vilkårResultat = minimertVilkårResultat, + ) + } else { + erOpphørResultatUtgjøreneForPeriode( + minimertVilkårResultat = minimertVilkårResultat, + triggesAv = triggesAv, + vedtaksperiode = vedtaksperiode, + ) + } + + VedtakBegrunnelseType.REDUKSJON, VedtakBegrunnelseType.INSTITUSJON_REDUKSJON -> { + erReduksjonResultatUtgjøreneForPeriode( + vilkårSomAvsluttesRettFørDennePerioden = minimertVilkårResultat, + triggesAv = triggesAv, + vedtaksperiode = vedtaksperiode, + vilkårSomStarterIDennePerioden = nesteMinimerteVilkårResultat, + ) + } + + VedtakBegrunnelseType.AVSLAG, VedtakBegrunnelseType.INSTITUSJON_AVSLAG -> + vilkårResultatPasserForAvslagsperiode( + minimertVilkårResultat = minimertVilkårResultat, + vedtaksperiode = vedtaksperiode, + begrunnelse = begrunnelse, + ) + + else -> throw Feil("Henting av personer med utgjørende vilkår when: Ikke implementert") + } +} + +private fun erOpphørResultatUtgjøreneForPeriode( + minimertVilkårResultat: MinimertVilkårResultat, + triggesAv: TriggesAv, + vedtaksperiode: Periode, +): Boolean { + val erOppfyltTomMånedEtter = erOppfyltTomMånedEtter(minimertVilkårResultat) + + val vilkårsluttForForrigePeriode = vedtaksperiode.fom.minusMonths( + if (erOppfyltTomMånedEtter) 1 else 0, + ) + return triggesAv.erUtdypendeVilkårsvurderingOppfylt(minimertVilkårResultat) && + minimertVilkårResultat.periodeTom != null && + minimertVilkårResultat.resultat == Resultat.OPPFYLT && + minimertVilkårResultat.periodeTom.toYearMonth() == vilkårsluttForForrigePeriode.toYearMonth() +} + +private fun erReduksjonResultatUtgjøreneForPeriode( + vilkårSomAvsluttesRettFørDennePerioden: MinimertVilkårResultat, + triggesAv: TriggesAv, + vedtaksperiode: Periode, + vilkårSomStarterIDennePerioden: MinimertVilkårResultat?, +): Boolean { + if (vilkårSomAvsluttesRettFørDennePerioden.periodeTom == null) { + return false + } + + val erOppfyltTomMånedEtter = erOppfyltTomMånedEtter(vilkårSomAvsluttesRettFørDennePerioden) + + val erStartPåDeltBosted = + vilkårSomAvsluttesRettFørDennePerioden.vilkårType == Vilkår.BOR_MED_SØKER && + !vilkårSomAvsluttesRettFørDennePerioden.utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.DELT_BOSTED) && + vilkårSomStarterIDennePerioden?.utdypendeVilkårsvurderinger?.contains(UtdypendeVilkårsvurdering.DELT_BOSTED) == true && + triggesAv.deltbosted + + val startNestePeriodeEtterVilkår = vilkårSomAvsluttesRettFørDennePerioden.periodeTom + .plusDays(if (erStartPåDeltBosted) 1 else 0) + .plusMonths(if (erOppfyltTomMånedEtter) 1 else 0) + + return triggesAv.erUtdypendeVilkårsvurderingOppfyltReduksjon( + vilkårSomAvsluttesRettFørDennePerioden = vilkårSomAvsluttesRettFørDennePerioden, + vilkårSomStarterIDennePerioden = vilkårSomStarterIDennePerioden, + ) && + vilkårSomAvsluttesRettFørDennePerioden.resultat == Resultat.OPPFYLT && + startNestePeriodeEtterVilkår.toYearMonth() == vedtaksperiode.fom.toYearMonth() +} + +private fun erOppfyltTomMånedEtter(minimertVilkårResultat: MinimertVilkårResultat) = + minimertVilkårResultat.vilkårType != Vilkår.UNDER_18_ÅR || + minimertVilkårResultat.periodeTom == minimertVilkårResultat.periodeTom?.sisteDagIMåned() + +private fun erInnvilgetVilkårResultatUtgjørende( + triggesAv: TriggesAv, + minimertVilkårResultat: MinimertVilkårResultat, + forrigeMinimerteVilkårResultat: MinimertVilkårResultat?, + vedtaksperiode: Periode, +): Boolean { + val vilkårResultatFomMåned = minimertVilkårResultat.periodeFom!!.toYearMonth() + val vedtaksperiodeFomMåned = vedtaksperiode.fom.toYearMonth() + + val erVilkårOgVedtakFomSammeMåned = + if (forrigeMinimerteVilkårResultat != null && + erForskjøvet(forrigeMinimerteVilkårResultat, minimertVilkårResultat) + ) { + vilkårResultatFomMåned == vedtaksperiodeFomMåned + } else { + vilkårResultatFomMåned == vedtaksperiodeFomMåned.minusMonths(1) + } + + return triggesAv.erUtdypendeVilkårsvurderingOppfylt(minimertVilkårResultat) && + erVilkårOgVedtakFomSammeMåned && + minimertVilkårResultat.resultat == Resultat.OPPFYLT +} + +private fun erForskjøvet( + forrigeMinimerteVilkårResultat: MinimertVilkårResultat, + minimertVilkårResultat: MinimertVilkårResultat, +) = (minimertVilkårResultat.vilkårType == Vilkår.BOR_MED_SØKER && (minimertVilkårResultat.erDeltBosted())) || + forrigeMinimerteVilkårResultat.resultat == Resultat.OPPFYLT && minimertVilkårResultat.resultat == Resultat.OPPFYLT + +private fun MinimertVilkårResultat.erDeltBosted() = + utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.DELT_BOSTED) || + utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.DELT_BOSTED_SKAL_IKKE_DELES) + +private fun vilkårResultatPasserForAvslagsperiode( + minimertVilkårResultat: MinimertVilkårResultat, + vedtaksperiode: Periode, + begrunnelse: IVedtakBegrunnelse, +): Boolean { + val erAvslagUtenFomDato = minimertVilkårResultat.periodeFom == null + + val fomVilkår = + if (erAvslagUtenFomDato) { + TIDENES_MORGEN.toYearMonth() + } else { + minimertVilkårResultat.periodeFom!!.toYearMonth().plusMonths(1) + } + + return fomVilkår == vedtaksperiode.fom.toYearMonth() && + minimertVilkårResultat.resultat == Resultat.IKKE_OPPFYLT && + minimertVilkårResultat.standardbegrunnelser.contains(begrunnelse) +} + +fun erFørstePeriodeOgVilkårIkkeOppfylt( + erFørsteVedtaksperiodePåFagsak: Boolean, + vedtaksperiode: Periode, + triggesAv: TriggesAv, + vilkårResultat: MinimertVilkårResultat, +): Boolean { + val vilkårIkkeOppfyltForPeriode = + vilkårResultat.resultat == Resultat.IKKE_OPPFYLT && + vilkårResultat.toPeriode().overlapperHeltEllerDelvisMed(vedtaksperiode) + + val vilkårOppfyltRettEtterPeriode = + vilkårResultat.resultat == Resultat.OPPFYLT && + vedtaksperiode.tom.toYearMonth() == vilkårResultat.periodeFom!!.toYearMonth() + + val vilkårAvsluttesInnenforSammeMåned = + (vilkårResultat.periodeFom?.toYearMonth() == vilkårResultat.periodeTom?.toYearMonth()) && + vilkårResultat.resultat == Resultat.OPPFYLT + + return erFørsteVedtaksperiodePåFagsak && + triggesAv.erUtdypendeVilkårsvurderingOppfylt(vilkårResultat) && + (vilkårIkkeOppfyltForPeriode || vilkårOppfyltRettEtterPeriode || vilkårAvsluttesInnenforSammeMåned) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/brevBegrunnelseProdusent/BrevBegrunnelseProdusent.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/brevBegrunnelseProdusent/BrevBegrunnelseProdusent.kt new file mode 100644 index 000000000..cb0fd449d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/brevBegrunnelseProdusent/BrevBegrunnelseProdusent.kt @@ -0,0 +1,246 @@ +package no.nav.familie.ba.sak.kjerne.brev.brevBegrunnelseProdusent + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.forrigeMåned +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.common.tilMånedÅr +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.domene.ISanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityPeriodeResultat +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.erAvslagUregistrerteBarnBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.tilBrevTekst +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BegrunnelseData +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent.IBegrunnelseGrunnlagForPeriode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent.hentGyldigeBegrunnelserPerPerson +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.AndelForVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.BehandlingsGrunnlagForVedtaksperioder +import java.math.BigDecimal +import java.time.LocalDate + +data class GrunnlagForBegrunnelse( + val behandlingsGrunnlagForVedtaksperioder: BehandlingsGrunnlagForVedtaksperioder, + val behandlingsGrunnlagForVedtaksperioderForrigeBehandling: BehandlingsGrunnlagForVedtaksperioder?, + val sanityBegrunnelser: Map, + val sanityEØSBegrunnelser: Map, + val nåDato: LocalDate, +) + +fun Standardbegrunnelse.lagBrevBegrunnelse( + vedtaksperiode: VedtaksperiodeMedBegrunnelser, + grunnlag: GrunnlagForBegrunnelse, + begrunnelsesGrunnlagPerPerson: Map, +): BegrunnelseData { + val sanityBegrunnelse = hentSanityBegrunnelse(grunnlag) + + val personerGjeldeneForBegrunnelse = vedtaksperiode.hentGyldigeBegrunnelserPerPerson(grunnlag) + .mapNotNull { (person, begrunnelserPåPerson) -> person.takeIf { this in begrunnelserPåPerson } } + + val gjelderSøker = gjelderBegrunnelseSøker(personerGjeldeneForBegrunnelse) + val barnasFødselsdatoer = sanityBegrunnelse.hentBarnasFødselsdatoerForBegrunnelse( + grunnlag = grunnlag, + gjelderSøker = gjelderSøker, + personerIBegrunnelse = personerGjeldeneForBegrunnelse, + personerMedUtbetaling = hentPersonerMedAndelIPeriode(begrunnelsesGrunnlagPerPerson), + ) + + val antallBarn = hentAntallBarnForBegrunnelse( + this, + grunnlag = grunnlag, + gjelderSøker = gjelderSøker, + barnasFødselsdatoer = barnasFødselsdatoer, + ) + + val månedOgÅrBegrunnelsenGjelderFor = vedtaksperiode.hentMånedOgÅrForBegrunnelse() + + val grunnlagForPersonerIBegrunnelsen = + begrunnelsesGrunnlagPerPerson.filtrerPåErPersonIBegrunnelse(personerGjeldeneForBegrunnelse) + + val beløp = hentBeløp( + gjelderSøker = gjelderSøker, + begrunnelsesGrunnlagPerPerson = begrunnelsesGrunnlagPerPerson, + grunnlagForPersonerIBegrunnelsen = grunnlagForPersonerIBegrunnelsen, + ) + + val endreteUtbetalingsAndelerForBegrunnelse = + sanityBegrunnelse.hentRelevanteEndringsperioderForBegrunnelse(grunnlagForPersonerIBegrunnelsen) + + val søknadstidspunktEndretUtbetaling = endreteUtbetalingsAndelerForBegrunnelse.sortedBy { it.søknadstidspunkt } + .firstOrNull { sanityBegrunnelse is SanityBegrunnelse && it.årsak in sanityBegrunnelse.endringsaarsaker }?.søknadstidspunkt + + sanityBegrunnelse.validerBrevbegrunnelse( + gjelderSøker, + barnasFødselsdatoer, + ) + + // Kan ikke se at "kanDelesOpp" noen gang er true i gammel løsning + if (this.kanDelesOpp) { + throw Feil("Ingen støtte for begrunnelse som kan deles opp. Gjelder $this") + } + + return BegrunnelseData( + gjelderSoker = gjelderSøker, + barnasFodselsdatoer = barnasFødselsdatoer.tilBrevTekst(), + fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling = "", // TODO Kan dette fjernes? + fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling = "", // TODO Kan dette fjernes? + antallBarn = antallBarn, + antallBarnOppfyllerTriggereOgHarUtbetaling = 0, // TODO Kan dette fjernes? + antallBarnOppfyllerTriggereOgHarNullutbetaling = 0, // TODO Kan dette fjernes? + maanedOgAarBegrunnelsenGjelderFor = månedOgÅrBegrunnelsenGjelderFor, + maalform = grunnlag.behandlingsGrunnlagForVedtaksperioder.persongrunnlag.søker.målform.tilSanityFormat(), + apiNavn = this.sanityApiNavn, + belop = Utils.formaterBeløp(beløp), + soknadstidspunkt = søknadstidspunktEndretUtbetaling?.tilKortString() ?: "", + avtaletidspunktDeltBosted = "", // TODO Kan dette fjernes? + sokersRettTilUtvidet = hentSøkersRettTilUtvidet( + utvidetUtbetalingsdetaljer = hentUtvidetAndelerIPeriode( + begrunnelsesGrunnlagPerPerson, + ), + ).tilSanityFormat(), + vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET, // TODO kan denne fjernes? + ) +} + +private fun hentUtvidetAndelerIPeriode(begrunnelsesGrunnlagPerPerson: Map) = + begrunnelsesGrunnlagPerPerson.values.flatMap { it.dennePerioden.andeler } + .filter { it.type == YtelseType.UTVIDET_BARNETRYGD } + +fun IVedtakBegrunnelse.hentSanityBegrunnelse(grunnlag: GrunnlagForBegrunnelse) = when (this) { + is EØSStandardbegrunnelse -> grunnlag.sanityEØSBegrunnelser[this] + is Standardbegrunnelse -> grunnlag.sanityBegrunnelser[this] +} ?: throw Feil("Fant ikke tilsvarende sanitybegrunnelse for $this") + +private fun hentPersonerMedAndelIPeriode(begrunnelsesGrunnlagPerPerson: Map) = + begrunnelsesGrunnlagPerPerson.filter { (_, begrunnelseGrunnlagForPersonIPeriode) -> + begrunnelseGrunnlagForPersonIPeriode.dennePerioden.andeler.toList().isNotEmpty() + }.keys + +private fun gjelderBegrunnelseSøker(personerGjeldeneForBegrunnelse: List) = + personerGjeldeneForBegrunnelse.any { it.type == PersonType.SØKER } + +fun ISanityBegrunnelse.hentBarnasFødselsdatoerForBegrunnelse( + grunnlag: GrunnlagForBegrunnelse, + gjelderSøker: Boolean, + personerIBegrunnelse: List, + personerMedUtbetaling: Set, +): List { + val barnPåBegrunnelse = personerIBegrunnelse.filter { it.type == PersonType.BARN } + val barnMedUtbetaling = personerMedUtbetaling.filter { it.type == PersonType.BARN } + val barnPåBehandlingen = grunnlag.behandlingsGrunnlagForVedtaksperioder.persongrunnlag.barna + val uregistrerteBarnPåBehandlingen = grunnlag.behandlingsGrunnlagForVedtaksperioder.uregistrerteBarn + return when { + this.erAvslagUregistrerteBarnBegrunnelse() -> grunnlag.behandlingsGrunnlagForVedtaksperioder.uregistrerteBarn.mapNotNull { it.fødselsdato } + + gjelderSøker && !this.gjelderEtterEndretUtbetaling && !this.gjelderEndretutbetaling -> { + when (this.periodeResultat) { + SanityPeriodeResultat.IKKE_INNVILGET -> + barnPåBehandlingen.map { it.fødselsdato } + uregistrerteBarnPåBehandlingen.mapNotNull { it.fødselsdato } + + else -> (barnMedUtbetaling + barnPåBegrunnelse).toSet().map { it.fødselsdato } + } + } + + else -> { + barnPåBegrunnelse.map { it.fødselsdato } + } + } +} + +fun hentAntallBarnForBegrunnelse( + begrunnelse: IVedtakBegrunnelse, + grunnlag: GrunnlagForBegrunnelse, + gjelderSøker: Boolean, + barnasFødselsdatoer: List, +): Int { + val uregistrerteBarnPåBehandlingen = grunnlag.behandlingsGrunnlagForVedtaksperioder.uregistrerteBarn + val erAvslagUregistrerteBarn = begrunnelse.erAvslagUregistrerteBarnBegrunnelse() + + return when { + erAvslagUregistrerteBarn -> uregistrerteBarnPåBehandlingen.size + gjelderSøker && begrunnelse.vedtakBegrunnelseType == VedtakBegrunnelseType.AVSLAG -> 0 + else -> barnasFødselsdatoer.size + } +} + +fun IVedtakBegrunnelse.erAvslagUregistrerteBarnBegrunnelse() = + this in setOf(Standardbegrunnelse.AVSLAG_UREGISTRERT_BARN, EØSStandardbegrunnelse.AVSLAG_EØS_UREGISTRERT_BARN) + +fun VedtaksperiodeMedBegrunnelser.hentMånedOgÅrForBegrunnelse(): String? { + return if (this.fom == null || fom == TIDENES_MORGEN) { + null + } else { + fom.forrigeMåned().tilMånedÅr() + } +} + +private fun hentBeløp( + gjelderSøker: Boolean, + begrunnelsesGrunnlagPerPerson: Map, + grunnlagForPersonerIBegrunnelsen: Map, +) = if (gjelderSøker) { + begrunnelsesGrunnlagPerPerson.values.sumOf { it.dennePerioden.andeler.sumOf { andeler -> andeler.kalkulertUtbetalingsbeløp } } +} else { + grunnlagForPersonerIBegrunnelsen.values.sumOf { it.dennePerioden.andeler.sumOf { andeler -> andeler.kalkulertUtbetalingsbeløp } } +} + +private fun Map.filtrerPåErPersonIBegrunnelse( + personerGjeldeneForBegrunnelse: List, +) = this.filter { (k, _) -> k in personerGjeldeneForBegrunnelse } + +fun ISanityBegrunnelse.hentRelevanteEndringsperioderForBegrunnelse( + grunnlagForPersonerIBegrunnelsen: Map, +) = when { + this.gjelderEtterEndretUtbetaling -> { + grunnlagForPersonerIBegrunnelsen.mapNotNull { it.value.forrigePeriode?.endretUtbetalingAndel } + } + + this.gjelderEndretutbetaling -> { + grunnlagForPersonerIBegrunnelsen.mapNotNull { it.value.dennePerioden.endretUtbetalingAndel } + } + + else -> emptyList() +} + +private fun ISanityBegrunnelse.validerBrevbegrunnelse( + gjelderSøker: Boolean, + barnasFødselsdatoer: List, +) { + if (!gjelderSøker && barnasFødselsdatoer.isEmpty() && !this.gjelderSatsendring && !this.erAvslagUregistrerteBarnBegrunnelse()) { + throw IllegalStateException("Ingen personer på brevbegrunnelse") + } +} + +private fun hentSøkersRettTilUtvidet(utvidetUtbetalingsdetaljer: List): SøkersRettTilUtvidet { + return when { + utvidetUtbetalingsdetaljer.any { it.prosent > BigDecimal.ZERO } -> SøkersRettTilUtvidet.SØKER_FÅR_UTVIDET + utvidetUtbetalingsdetaljer.isNotEmpty() && utvidetUtbetalingsdetaljer.all { it.prosent == BigDecimal.ZERO } -> SøkersRettTilUtvidet.SØKER_HAR_RETT_MEN_FÅR_IKKE + + else -> SøkersRettTilUtvidet.SØKER_HAR_IKKE_RETT + } +} + +enum class SøkersRettTilUtvidet { + SØKER_FÅR_UTVIDET, SØKER_HAR_RETT_MEN_FÅR_IKKE, SØKER_HAR_IKKE_RETT, ; + + fun tilSanityFormat() = when (this) { + SØKER_FÅR_UTVIDET -> "sokerFaarUtvidet" + SØKER_HAR_RETT_MEN_FÅR_IKKE -> "sokerHarRettMenFaarIkke" + SØKER_HAR_IKKE_RETT -> "sokerHarIkkeRett" + } +} + +fun ISanityBegrunnelse.erAvslagUregistrerteBarnBegrunnelse() = + this.apiNavn in setOf( + Standardbegrunnelse.AVSLAG_UREGISTRERT_BARN.sanityApiNavn, + EØSStandardbegrunnelse.AVSLAG_EØS_UREGISTRERT_BARN.sanityApiNavn, + ) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/brevBegrunnelseProdusent/BrevE\303\270sBegrunnelseProdusent.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/brevBegrunnelseProdusent/BrevE\303\270sBegrunnelseProdusent.kt" new file mode 100644 index 000000000..99b6e8f45 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/brevBegrunnelseProdusent/BrevE\303\270sBegrunnelseProdusent.kt" @@ -0,0 +1,122 @@ +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.Utils.storForbokstav +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.ekstern.restDomene.BarnMedOpplysninger +import no.nav.familie.ba.sak.kjerne.brev.brevBegrunnelseProdusent.GrunnlagForBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.brevBegrunnelseProdusent.hentSanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityPeriodeResultat +import no.nav.familie.ba.sak.kjerne.brev.tilSammenslåttKortString +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.EØSBegrunnelseData +import no.nav.familie.ba.sak.kjerne.vedtak.domene.EØSBegrunnelseDataMedKompetanse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.EØSBegrunnelseDataUtenKompetanse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent.IBegrunnelseGrunnlagForPeriode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent.hentGyldigeBegrunnelserPerPerson +import java.time.LocalDate + +fun EØSStandardbegrunnelse.lagBrevBegrunnelse( + vedtaksperiode: VedtaksperiodeMedBegrunnelser, + grunnlag: GrunnlagForBegrunnelse, + begrunnelsesGrunnlagPerPerson: Map, + landkoder: Map, +): List { + val sanityBegrunnelse = hentSanityBegrunnelse(grunnlag) + val personerGjeldeneForBegrunnelse = vedtaksperiode.hentGyldigeBegrunnelserPerPerson( + grunnlag, + ).mapNotNull { (person, begrunnelserPåPerson) -> person.takeIf { this in begrunnelserPåPerson } } + val periodegrunnlagForPersonerIBegrunnelse = + begrunnelsesGrunnlagPerPerson.filter { (person, _) -> person in personerGjeldeneForBegrunnelse } + + val kompetanser = when (sanityBegrunnelse.periodeResultat) { + SanityPeriodeResultat.INNVILGET_ELLER_ØKNING, + SanityPeriodeResultat.INGEN_ENDRING, + -> periodegrunnlagForPersonerIBegrunnelse.values.mapNotNull { it.dennePerioden.kompetanse } + + SanityPeriodeResultat.IKKE_INNVILGET, + SanityPeriodeResultat.REDUKSJON, + -> periodegrunnlagForPersonerIBegrunnelse.values.mapNotNull { it.forrigePeriode?.kompetanse } + + null -> error("Feltet 'periodeResultat' er ikke satt for begrunnelse fra sanity '${sanityBegrunnelse.apiNavn}'.") + } + + return if (kompetanser.isEmpty() && sanityBegrunnelse.periodeResultat == SanityPeriodeResultat.IKKE_INNVILGET) { + val personerIBegrunnelse = personerGjeldeneForBegrunnelse + val barnPåBehandling = grunnlag.behandlingsGrunnlagForVedtaksperioder.persongrunnlag.barna + val barnIBegrunnelse = personerGjeldeneForBegrunnelse.filter { it.type == PersonType.BARN } + val gjelderSøker = personerIBegrunnelse.any { it.type == PersonType.SØKER } + + val barnasFødselsdatoer = hentBarnasFødselsdatoerForAvslagsbegrunnelse( + barnIBegrunnelse = barnIBegrunnelse, + barnPåBehandling = barnPåBehandling, + uregistrerteBarn = grunnlag.behandlingsGrunnlagForVedtaksperioder.uregistrerteBarn, + gjelderSøker = gjelderSøker, + ) + + listOf( + EØSBegrunnelseDataUtenKompetanse( + vedtakBegrunnelseType = this.vedtakBegrunnelseType, + apiNavn = sanityBegrunnelse.apiNavn, + barnasFodselsdatoer = barnasFødselsdatoer.tilSammenslåttKortString(), + antallBarn = barnasFødselsdatoer.size, + maalform = grunnlag.behandlingsGrunnlagForVedtaksperioder.persongrunnlag.søker.målform.tilSanityFormat(), + gjelderSoker = gjelderSøker, + ), + ) + } else { + kompetanser.map { kompetanse -> + EØSBegrunnelseDataMedKompetanse( + vedtakBegrunnelseType = this.vedtakBegrunnelseType, + apiNavn = sanityBegrunnelse.apiNavn, + annenForeldersAktivitet = kompetanse.annenForeldersAktivitet, + annenForeldersAktivitetsland = kompetanse.annenForeldersAktivitetsland?.tilLandNavn(landkoder)?.navn, + barnetsBostedsland = kompetanse.barnetsBostedsland.tilLandNavn(landkoder).navn, + barnasFodselsdatoer = Utils.slåSammen( + kompetanse.barnAktører.map { aktør -> + grunnlag.hent(aktør).fødselsdato.tilKortString() + }, + ), + antallBarn = kompetanse.barnAktører.size, + maalform = grunnlag.behandlingsGrunnlagForVedtaksperioder.persongrunnlag.søker.målform.tilSanityFormat(), + sokersAktivitet = kompetanse.søkersAktivitet, + sokersAktivitetsland = kompetanse.søkersAktivitetsland.tilLandNavn(landkoder).navn, + ) + } + } +} + +private fun GrunnlagForBegrunnelse.hent( + aktør: Aktør, +) = behandlingsGrunnlagForVedtaksperioder.persongrunnlag.personer.single { it.aktør == aktør } + +fun hentBarnasFødselsdatoerForAvslagsbegrunnelse( + barnIBegrunnelse: List, + barnPåBehandling: List, + uregistrerteBarn: List, + gjelderSøker: Boolean, +): List { + val registrerteBarnFødselsdatoer = + if (gjelderSøker) barnPåBehandling.map { it.fødselsdato } else barnIBegrunnelse.map { it.fødselsdato } + val uregistrerteBarnFødselsdatoer = + uregistrerteBarn.mapNotNull { it.fødselsdato } + val alleBarnaFødselsdatoer = registrerteBarnFødselsdatoer + uregistrerteBarnFødselsdatoer + return alleBarnaFødselsdatoer +} + +data class Landkode(val kode: String, val navn: String) { + init { + if (this.kode.length != 2) { + throw Feil("Forventer landkode på 'ISO 3166-1 alpha-2'-format") + } + } +} + +fun String.tilLandNavn(landkoderISO2: Map): Landkode { + val kode = landkoderISO2.entries.find { it.key == this } ?: throw Feil("Fant ikke navn for landkode $this.") + + return Landkode(kode.key, kode.value.storForbokstav()) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/brevPeriodeProdusent/BrevPeriodeProdusent.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/brevPeriodeProdusent/BrevPeriodeProdusent.kt new file mode 100644 index 000000000..922266652 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/brevPeriodeProdusent/BrevPeriodeProdusent.kt @@ -0,0 +1,144 @@ +package no.nav.familie.ba.sak.kjerne.brev.brevPeriodeProdusent + +import lagBrevBegrunnelse +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.common.tilMånedÅr +import no.nav.familie.ba.sak.kjerne.brev.brevBegrunnelseProdusent.GrunnlagForBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.brevBegrunnelseProdusent.lagBrevBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.BrevPeriodeType +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BrevBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.FritekstBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.domene.hentBrevPeriodeType +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent.IBegrunnelseGrunnlagForPeriode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent.erUtbetalingEllerDeltBostedIPeriode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent.finnBegrunnelseGrunnlagPerPerson + +fun VedtaksperiodeMedBegrunnelser.lagBrevPeriode( + grunnlagForBegrunnelse: GrunnlagForBegrunnelse, + landkoder: Map, +): BrevPeriode? { + val begrunnelsesGrunnlagPerPerson = this.finnBegrunnelseGrunnlagPerPerson(grunnlagForBegrunnelse) + + val standardbegrunnelser = + this.begrunnelser.map { + it.standardbegrunnelse.lagBrevBegrunnelse( + this, + grunnlagForBegrunnelse, + begrunnelsesGrunnlagPerPerson, + ) + } + + val eøsBegrunnelser = + this.eøsBegrunnelser.flatMap { + it.begrunnelse.lagBrevBegrunnelse( + this, + grunnlagForBegrunnelse, + begrunnelsesGrunnlagPerPerson, + landkoder, + ) + } + + val fritekster = this.fritekster.map { FritekstBegrunnelse(it.fritekst) } + + val begrunnelserOgFritekster = + standardbegrunnelser + eøsBegrunnelser + fritekster + + if (begrunnelserOgFritekster.isEmpty()) return null + + return this.byggBrevPeriode( + begrunnelserOgFritekster = begrunnelserOgFritekster, + begrunnelseGrunnlagPerPerson = begrunnelsesGrunnlagPerPerson, + grunnlagForBegrunnelse = grunnlagForBegrunnelse, + + ) +} + +private fun VedtaksperiodeMedBegrunnelser.byggBrevPeriode( + begrunnelserOgFritekster: List, + begrunnelseGrunnlagPerPerson: Map, + grunnlagForBegrunnelse: GrunnlagForBegrunnelse, +): BrevPeriode { + val barnMedUtbetaling = begrunnelseGrunnlagPerPerson.finnBarnMedUtbetaling().keys + val beløp = begrunnelseGrunnlagPerPerson.hentTotaltUtbetaltIPeriode() + + val brevPeriodeType = hentBrevPeriodeType( + vedtaksperiodeMedBegrunnelser = this, + erUtbetalingEllerDeltBostedIPeriode = erUtbetalingEllerDeltBostedIPeriode(begrunnelseGrunnlagPerPerson), + ) + + return BrevPeriode( + fom = this.fom?.tilMånedÅr() ?: "", + tom = hentTomTekstForBrev(brevPeriodeType), + beløp = beløp.toString(), + begrunnelser = begrunnelserOgFritekster, + brevPeriodeType = brevPeriodeType, + antallBarn = barnMedUtbetaling.size.toString(), + barnasFodselsdager = barnMedUtbetaling.tilBarnasFødselsdatoer(), + duEllerInstitusjonen = hentDuEllerInstitusjonenTekst( + brevPeriodeType = brevPeriodeType, + fagsakType = grunnlagForBegrunnelse.behandlingsGrunnlagForVedtaksperioder.fagsakType, + ), + ) +} + +private fun VedtaksperiodeMedBegrunnelser.hentTomTekstForBrev( + brevPeriodeType: BrevPeriodeType, +) = if (this.tom == null) { + "" +} else { + val tomDato = this.tom.tilMånedÅr() + when (brevPeriodeType) { + BrevPeriodeType.UTBETALING -> "til $tomDato" + BrevPeriodeType.INGEN_UTBETALING -> if (this.type == Vedtaksperiodetype.AVSLAG) "til og med $tomDato " else "" + BrevPeriodeType.INGEN_UTBETALING_UTEN_PERIODE -> "" + BrevPeriodeType.FORTSATT_INNVILGET -> "" + BrevPeriodeType.FORTSATT_INNVILGET_NY -> "" + else -> error("$brevPeriodeType skal ikke brukes") + } +} + +private fun Map.hentTotaltUtbetaltIPeriode() = + this.values.sumOf { it.dennePerioden.andeler.sumOf { andeler -> andeler.kalkulertUtbetalingsbeløp } } + +private fun Map.finnBarnMedUtbetaling() = + filterKeys { it.type == PersonType.BARN } + .filterValues { + val endretUtbetalingAndelIPeriodeErDeltBosted = + it.dennePerioden.endretUtbetalingAndel?.årsak == Årsak.DELT_BOSTED + val utbetalingssumIPeriode = it.dennePerioden.andeler.sumOf { andel -> andel.kalkulertUtbetalingsbeløp } + + utbetalingssumIPeriode != 0 || endretUtbetalingAndelIPeriodeErDeltBosted + } + +fun Set.tilBarnasFødselsdatoer(): String { + val barnasFødselsdatoerListe: List = this.filter { it.type == PersonType.BARN } + .sortedBy { it.fødselsdato } + .map { it.fødselsdato.tilKortString() } + + return Utils.slåSammen(barnasFødselsdatoerListe) +} + +private fun hentDuEllerInstitusjonenTekst(brevPeriodeType: BrevPeriodeType, fagsakType: FagsakType): String = + when (fagsakType) { + FagsakType.INSTITUSJON -> { + when (brevPeriodeType) { + BrevPeriodeType.UTBETALING, BrevPeriodeType.INGEN_UTBETALING -> "institusjonen" + else -> "Institusjonen" + } + } + + FagsakType.NORMAL, FagsakType.BARN_ENSLIG_MINDREÅRIG -> { + when (brevPeriodeType) { + BrevPeriodeType.UTBETALING, BrevPeriodeType.INGEN_UTBETALING -> "du" + else -> "Du" + } + } + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BegrunnelseMedTriggere.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BegrunnelseMedTriggere.kt new file mode 100644 index 000000000..d51b4d317 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BegrunnelseMedTriggere.kt @@ -0,0 +1,84 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.NullablePeriode +import no.nav.familie.ba.sak.kjerne.brev.hentPersonidenterGjeldendeForBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.TriggesAv +import no.nav.familie.ba.sak.kjerne.vedtak.domene.Vedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype + +data class BegrunnelseMedTriggere( + val standardbegrunnelse: IVedtakBegrunnelse, + val triggesAv: TriggesAv, +) { + fun tilBrevBegrunnelseGrunnlagMedPersoner( + periode: NullablePeriode, + vedtaksperiodetype: Vedtaksperiodetype, + restBehandlingsgrunnlagForBrev: RestBehandlingsgrunnlagForBrev, + identerMedUtbetalingPåPeriode: List, + erFørsteVedtaksperiodePåFagsak: Boolean, + erUregistrerteBarnPåbehandling: Boolean, + barnMedReduksjonFraForrigeBehandlingIdent: List, + minimerteUtbetalingsperiodeDetaljer: List, + dødeBarnForrigePeriode: List, + ): List { + return if (this.standardbegrunnelse.kanDelesOpp) { + this.standardbegrunnelse.delOpp( + restBehandlingsgrunnlagForBrev = restBehandlingsgrunnlagForBrev, + triggesAv = this.triggesAv, + periode = periode, + ) + } else { + val personidenterGjeldendeForBegrunnelse: Set = hentPersonidenterGjeldendeForBegrunnelse( + begrunnelse = this.standardbegrunnelse, + triggesAv = this.triggesAv, + vedtakBegrunnelseType = this.standardbegrunnelse.vedtakBegrunnelseType, + periode = periode, + vedtaksperiodetype = vedtaksperiodetype, + restBehandlingsgrunnlagForBrev = restBehandlingsgrunnlagForBrev, + identerMedUtbetalingPåPeriode = identerMedUtbetalingPåPeriode, + erFørsteVedtaksperiodePåFagsak = erFørsteVedtaksperiodePåFagsak, + identerMedReduksjonPåPeriode = barnMedReduksjonFraForrigeBehandlingIdent, + minimerteUtbetalingsperiodeDetaljer = minimerteUtbetalingsperiodeDetaljer, + dødeBarnForrigePeriode = dødeBarnForrigePeriode, + ) + + if ( + personidenterGjeldendeForBegrunnelse.isEmpty() && + !erUregistrerteBarnPåbehandling && + !this.triggesAv.satsendring + ) { + throw FunksjonellFeil( + "Begrunnelse '${this.standardbegrunnelse}' var ikke knyttet til noen personer.", + ) + } + + listOf( + BrevBegrunnelseGrunnlagMedPersoner( + standardbegrunnelse = this.standardbegrunnelse, + vedtakBegrunnelseType = this.standardbegrunnelse.vedtakBegrunnelseType, + triggesAv = this.triggesAv, + personIdenter = personidenterGjeldendeForBegrunnelse.toList(), + ), + ) + } + } + + fun tilBrevBegrunnelseGrunnlagForLogging() = BrevBegrunnelseGrunnlagForLogging( + standardbegrunnelse = this.standardbegrunnelse, + ) +} + +fun Vedtaksbegrunnelse.tilBegrunnelseMedTriggere( + sanityBegrunnelser: Map, +): BegrunnelseMedTriggere { + val sanityBegrunnelse = sanityBegrunnelser[this.standardbegrunnelse] + ?: throw Feil("Finner ikke sanityBegrunnelse med apiNavn=${this.standardbegrunnelse.sanityApiNavn}") + return BegrunnelseMedTriggere( + standardbegrunnelse = this.standardbegrunnelse, + triggesAv = sanityBegrunnelse.triggesAv, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevBegrunnelseGrunnlagMedPersoner.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevBegrunnelseGrunnlagMedPersoner.kt new file mode 100644 index 000000000..63d98098f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevBegrunnelseGrunnlagMedPersoner.kt @@ -0,0 +1,62 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.TriggesAv +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.erAvslagUregistrerteBarnBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.MinimertRestPerson +import java.time.LocalDate + +data class BrevBegrunnelseGrunnlagMedPersoner( + val standardbegrunnelse: IVedtakBegrunnelse, + val vedtakBegrunnelseType: VedtakBegrunnelseType, + val triggesAv: TriggesAv, + val personIdenter: List, + val avtaletidspunktDeltBosted: LocalDate? = null, +) { + fun hentAntallBarnForBegrunnelse( + uregistrerteBarn: List, + gjelderSøker: Boolean, + barnasFødselsdatoer: List, + ): Int { + val erAvslagUregistrerteBarn = standardbegrunnelse.erAvslagUregistrerteBarnBegrunnelse() + + return when { + erAvslagUregistrerteBarn -> uregistrerteBarn.size + gjelderSøker && this.vedtakBegrunnelseType == VedtakBegrunnelseType.AVSLAG -> 0 + else -> barnasFødselsdatoer.size + } + } + + fun hentBarnasFødselsdagerForBegrunnelse( + uregistrerteBarn: List, + gjelderSøker: Boolean, + personerIBehandling: List, + personerPåBegrunnelse: List, + personerMedUtbetaling: List, + ) = when { + this.standardbegrunnelse.erAvslagUregistrerteBarnBegrunnelse() -> uregistrerteBarn.mapNotNull { it.fødselsdato } + + gjelderSøker && this.vedtakBegrunnelseType != VedtakBegrunnelseType.ENDRET_UTBETALING && this.vedtakBegrunnelseType != VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING -> { + when (this.vedtakBegrunnelseType) { + VedtakBegrunnelseType.AVSLAG, VedtakBegrunnelseType.OPPHØR -> { + personerIBehandling + .filter { it.type == PersonType.BARN } + .map { it.fødselsdato } + + uregistrerteBarn.mapNotNull { it.fødselsdato } + } + + else -> { + (personerMedUtbetaling + personerPåBegrunnelse).toSet() + .filter { it.type == PersonType.BARN } + .map { it.fødselsdato } + } + } + } + else -> + personerPåBegrunnelse + .filter { it.type == PersonType.BARN } + .map { it.fødselsdato } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevPeriodeLogging.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevPeriodeLogging.kt new file mode 100644 index 000000000..2cf21137a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevPeriodeLogging.kt @@ -0,0 +1,74 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import java.math.BigDecimal +import java.time.LocalDate + +data class BrevPeriodeForLogging( + val fom: LocalDate?, + val tom: LocalDate?, + val vedtaksperiodetype: Vedtaksperiodetype, + val begrunnelser: List, + val fritekster: List, + + val personerPåBehandling: List, + + val uregistrerteBarn: List, + val erFørsteVedtaksperiodePåFagsak: Boolean = false, + val brevMålform: Målform, +) + +data class BrevPeriodePersonForLogging( + val fødselsdato: LocalDate, + val type: PersonType, + val overstyrteVilkårresultater: List, + val andreVurderinger: List, + val endredeUtbetalinger: List, + val utbetalinger: List, + val harReduksjonFraForrigeBehandling: Boolean, +) + +data class UtbetalingPåPersonForLogging( + val ytelseType: YtelseType, + val utbetaltPerMnd: Int, + val erPåvirketAvEndring: Boolean, + val prosent: BigDecimal, +) + +data class EndretUtbetalingAndelPåPersonForLogging( + val periode: MånedPeriode, + val årsak: Årsak, +) + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type", + defaultImpl = BegrunnelseDataForLogging::class, +) +@JsonSubTypes(value = [JsonSubTypes.Type(value = FritekstBegrunnelseTestForLogging::class, name = "fritekst")]) +interface TestBegrunnelse + +data class FritekstBegrunnelseTestForLogging(val fritekst: String) : TestBegrunnelse + +data class BegrunnelseDataForLogging( + val gjelderSoker: Boolean, + val barnasFodselsdatoer: String, + val antallBarn: Int, + val maanedOgAarBegrunnelsenGjelderFor: String?, + val maalform: String, + val apiNavn: String, + val belop: Int, +) : TestBegrunnelse + +data class BrevBegrunnelseGrunnlagForLogging( + val standardbegrunnelse: IVedtakBegrunnelse, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevType.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevType.kt new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevperiodeData.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevperiodeData.kt new file mode 100644 index 000000000..08d7e8ec5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevperiodeData.kt @@ -0,0 +1,69 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.kjerne.brev.BrevPeriodeGenerator +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BrevBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype + +@Deprecated("Skal bort. Bruk GrunnlagForBegrunnelse i stedet") +data class BrevperiodeData( + val restBehandlingsgrunnlagForBrev: RestBehandlingsgrunnlagForBrev, + val erFørsteVedtaksperiodePåFagsak: Boolean, + val uregistrerteBarn: List, + val brevMålform: Målform, + val minimertVedtaksperiode: MinimertVedtaksperiode, + val barnMedReduksjonFraForrigeBehandlingIdent: List = emptyList(), + val minimerteKompetanserForPeriode: List, + val minimerteKompetanserSomStopperRettFørPeriode: List, + val dødeBarnForrigePeriode: List, +) : Comparable { + + fun tilBrevPeriodeGenerator() = BrevPeriodeGenerator( + restBehandlingsgrunnlagForBrev = restBehandlingsgrunnlagForBrev, + erFørsteVedtaksperiodePåFagsak = erFørsteVedtaksperiodePåFagsak, + uregistrerteBarn = uregistrerteBarn, + brevMålform = brevMålform, + minimertVedtaksperiode = minimertVedtaksperiode, + barnMedReduksjonFraForrigeBehandlingIdent = barnMedReduksjonFraForrigeBehandlingIdent, + minimerteKompetanserForPeriode = minimerteKompetanserForPeriode, + minimerteKompetanserSomStopperRettFørPeriode = minimerteKompetanserSomStopperRettFørPeriode, + dødeBarnForrigePeriode = dødeBarnForrigePeriode, + ) + + fun hentBegrunnelserOgFritekster(): List { + val brevPeriodeGenerator = this.tilBrevPeriodeGenerator() + return brevPeriodeGenerator.byggBegrunnelserOgFritekster( + begrunnelserGrunnlagMedPersoner = brevPeriodeGenerator.hentBegrunnelsegrunnlagMedPersoner(), + eøsBegrunnelserMedKompetanser = brevPeriodeGenerator.hentEøsBegrunnelserMedKompetanser(), + ) + } + + fun tilBrevperiodeForLogging() = + minimertVedtaksperiode.tilBrevPeriodeForLogging( + restBehandlingsgrunnlagForBrev = this.restBehandlingsgrunnlagForBrev, + uregistrerteBarn = this.uregistrerteBarn, + brevMålform = this.brevMålform, + barnMedReduksjonFraForrigeBehandlingIdent = this.barnMedReduksjonFraForrigeBehandlingIdent, + ) + + override fun compareTo(other: BrevperiodeData): Int { + val fomCompared = (this.minimertVedtaksperiode.fom ?: TIDENES_MORGEN).compareTo( + other.minimertVedtaksperiode.fom ?: TIDENES_MORGEN, + ) + + return when { + this.erGenereltAvslag() -> 1 + other.erGenereltAvslag() -> -1 + fomCompared == 0 && this.minimertVedtaksperiode.type == Vedtaksperiodetype.AVSLAG -> 1 + fomCompared == 0 && other.minimertVedtaksperiode.type == Vedtaksperiodetype.AVSLAG -> -1 + else -> fomCompared + } + } + + private fun BrevperiodeData.erGenereltAvslag(): Boolean { + return minimertVedtaksperiode.type == Vedtaksperiodetype.AVSLAG && + minimertVedtaksperiode.fom == null && + minimertVedtaksperiode.tom == null + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/ISanityBegrunnelse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/ISanityBegrunnelse.kt new file mode 100644 index 000000000..37da16cbc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/ISanityBegrunnelse.kt @@ -0,0 +1,136 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.BrevPeriodeType +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.BarnetsBostedsland +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.TriggesAv +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår + +sealed interface ISanityBegrunnelse { + val apiNavn: String + val navnISystem: String + val periodeResultat: SanityPeriodeResultat? + val vilkår: Set + val borMedSokerTriggere: List + val giftPartnerskapTriggere: List + val bosattIRiketTriggere: List + val lovligOppholdTriggere: List + val utvidetBarnetrygdTriggere: List + val fagsakType: FagsakType? + val tema: Tema? + val valgbarhet: Valgbarhet? + val periodeType: BrevPeriodeType? + + val gjelderEtterEndretUtbetaling + get() = this is SanityBegrunnelse && + this.endretUtbetalingsperiodeTriggere.contains(EndretUtbetalingsperiodeTrigger.ETTER_ENDRET_UTBETALINGSPERIODE) + + val gjelderEndretutbetaling + get() = this is SanityBegrunnelse && + this.endringsaarsaker.isNotEmpty() && !gjelderEtterEndretUtbetaling() + + val gjelderSatsendring + get() = this is SanityBegrunnelse && + ØvrigTrigger.SATSENDRING in this.ovrigeTriggere +} + +data class SanityBegrunnelse( + override val apiNavn: String, + override val navnISystem: String, + override val periodeResultat: SanityPeriodeResultat? = null, + override val vilkår: Set = emptySet(), + override val lovligOppholdTriggere: List = emptyList(), + override val bosattIRiketTriggere: List = emptyList(), + override val giftPartnerskapTriggere: List = emptyList(), + override val borMedSokerTriggere: List = emptyList(), + override val utvidetBarnetrygdTriggere: List = emptyList(), + override val fagsakType: FagsakType? = null, + override val tema: Tema? = null, + override val valgbarhet: Valgbarhet? = null, + override val periodeType: BrevPeriodeType? = null, + @Deprecated("Bruk vilkår") + val vilkaar: List = emptyList(), + val rolle: List = emptyList(), + val ovrigeTriggere: List<ØvrigTrigger> = emptyList(), + val hjemler: List = emptyList(), + val hjemlerFolketrygdloven: List = emptyList(), + val endringsaarsaker: List<Årsak> = emptyList(), + val endretUtbetalingsperiodeDeltBostedUtbetalingTrigger: EndretUtbetalingsperiodeDeltBostedTriggere? = null, + val endretUtbetalingsperiodeTriggere: List = emptyList(), +) : ISanityBegrunnelse { + + val triggesAv: TriggesAv by lazy { this.tilTriggesAv() } + + fun gjelderEtterEndretUtbetaling() = + this.endretUtbetalingsperiodeTriggere.contains(EndretUtbetalingsperiodeTrigger.ETTER_ENDRET_UTBETALINGSPERIODE) +} + +data class SanityEØSBegrunnelse( + override val apiNavn: String, + override val navnISystem: String, + override val periodeResultat: SanityPeriodeResultat? = null, + override val vilkår: Set, + override val fagsakType: FagsakType?, + override val tema: Tema?, + override val periodeType: BrevPeriodeType?, + val annenForeldersAktivitet: List, + val barnetsBostedsland: List, + val kompetanseResultat: List, + val hjemler: List, + val hjemlerFolketrygdloven: List, + val hjemlerEØSForordningen883: List, + val hjemlerEØSForordningen987: List, + val hjemlerSeperasjonsavtalenStorbritannina: List, +) : ISanityBegrunnelse { + override val lovligOppholdTriggere: List = emptyList() + override val utvidetBarnetrygdTriggere: List = emptyList() + override val valgbarhet = null + override val bosattIRiketTriggere: List = emptyList() + override val giftPartnerskapTriggere: List = emptyList() + override val borMedSokerTriggere: List = emptyList() +} + +private fun SanityBegrunnelse.tilTriggesAv(): TriggesAv { + return TriggesAv( + vilkår = this.vilkaar.map { it.tilVilkår() }.toSet(), + personTyper = if (this.rolle.isEmpty()) { + when { + this.inneholderVilkår(SanityVilkår.BOSATT_I_RIKET) -> setOf(PersonType.BARN, PersonType.SØKER) + this.inneholderVilkår(SanityVilkår.LOVLIG_OPPHOLD) -> setOf(PersonType.BARN, PersonType.SØKER) + this.inneholderVilkår(SanityVilkår.GIFT_PARTNERSKAP) -> setOf(PersonType.BARN) + this.inneholderVilkår(SanityVilkår.UNDER_18_ÅR) -> setOf(PersonType.BARN) + this.inneholderVilkår(SanityVilkår.BOR_MED_SOKER) -> setOf(PersonType.BARN) + else -> setOf(PersonType.BARN, PersonType.SØKER) + } + } else { + this.rolle.map { it.tilPersonType() }.toSet() + }, + personerManglerOpplysninger = this.inneholderØvrigTrigger(ØvrigTrigger.MANGLER_OPPLYSNINGER), + satsendring = this.inneholderØvrigTrigger(ØvrigTrigger.SATSENDRING), + barnMedSeksårsdag = this.inneholderØvrigTrigger(ØvrigTrigger.BARN_MED_6_ÅRS_DAG), + vurderingAnnetGrunnlag = ( + this.inneholderLovligOppholdTrigger(VilkårTrigger.VURDERING_ANNET_GRUNNLAG) || + this.inneholderBosattIRiketTrigger(VilkårTrigger.VURDERING_ANNET_GRUNNLAG) || + this.inneholderGiftPartnerskapTrigger(VilkårTrigger.VURDERING_ANNET_GRUNNLAG) || + this.inneholderBorMedSøkerTrigger(VilkårTrigger.VURDERING_ANNET_GRUNNLAG) + ), + medlemskap = this.inneholderBosattIRiketTrigger(VilkårTrigger.MEDLEMSKAP), + deltbosted = this.inneholderBorMedSøkerTrigger(VilkårTrigger.DELT_BOSTED), + deltBostedSkalIkkeDeles = this.inneholderBorMedSøkerTrigger(VilkårTrigger.DELT_BOSTED_SKAL_IKKE_DELES), + valgbar = !this.inneholderØvrigTrigger(ØvrigTrigger.ALLTID_AUTOMATISK), + valgbarhet = this.valgbarhet, + etterEndretUtbetaling = this.endretUtbetalingsperiodeTriggere + .contains(EndretUtbetalingsperiodeTrigger.ETTER_ENDRET_UTBETALINGSPERIODE) ?: false, + endretUtbetalingSkalUtbetales = this.endretUtbetalingsperiodeDeltBostedUtbetalingTrigger + ?: EndretUtbetalingsperiodeDeltBostedTriggere.UTBETALING_IKKE_RELEVANT, + endringsaarsaker = this.endringsaarsaker.toSet(), + småbarnstillegg = this.inneholderUtvidetBarnetrygdTrigger(UtvidetBarnetrygdTrigger.SMÅBARNSTILLEGG), + gjelderFørstePeriode = this.inneholderØvrigTrigger(ØvrigTrigger.GJELDER_FØRSTE_PERIODE), + gjelderFraInnvilgelsestidspunkt = this.inneholderØvrigTrigger(ØvrigTrigger.GJELDER_FRA_INNVILGELSESTIDSPUNKT), + barnDød = this.inneholderØvrigTrigger(ØvrigTrigger.BARN_DØD), + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/ManueltBrevRequest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/ManueltBrevRequest.kt new file mode 100644 index 000000000..2da90fc69 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/ManueltBrevRequest.kt @@ -0,0 +1,456 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.Utils.storForbokstav +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.EnkeltInformasjonsbrev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.FlettefelterForDokumentImpl +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.ForlengetSvartidsbrev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.HenleggeTrukketSøknadBrev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.HenleggeTrukketSøknadData +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.InformasjonsbrevDeltBostedBrev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.InformasjonsbrevDeltBostedData +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.InformasjonsbrevKanSøke +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.InformasjonsbrevTilForelderBrev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.InformasjonsbrevTilForelderData +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.InnhenteOpplysningerBrev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.InnhenteOpplysningerData +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.InnhenteOpplysningerOmBarn +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.SignaturDelmal +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Svartidsbrev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.VarselOmRevurderingDeltBostedParagraf14Brev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.VarselOmRevurderingDeltBostedParagraf14Data +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.VarselOmRevurderingSamboerBrev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.VarselOmRevurderingSamboerData +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.VarselbrevMedÅrsaker +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.VarselbrevÅrlegKontrollEøs +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.VarselbrevMedÅrsakerOgBarn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.kontrakter.felles.arbeidsfordeling.Enhet +import java.time.LocalDate + +interface Person { + val navn: String + val fødselsnummer: String +} + +data class ManueltBrevRequest( + val brevmal: Brevmal, + val multiselectVerdier: List = emptyList(), + val mottakerIdent: String, + val barnIBrev: List = emptyList(), + val datoAvtale: String? = null, + // Settes av backend ved utsending fra behandling + val mottakerMålform: Målform = Målform.NB, + val mottakerNavn: String = "", + val enhet: Enhet? = null, + val antallUkerSvarfrist: Int? = null, + val barnasFødselsdager: List? = null, + val behandlingKategori: BehandlingKategori? = null, + val vedrørende: Person? = null, + val mottakerlandSed: List = emptyList(), +) { + + override fun toString(): String { + return "${ManueltBrevRequest::class}, $brevmal" + } + + fun enhetNavn(): String = this.enhet?.enhetNavn ?: error("Finner ikke enhetsnavn på manuell brevrequest") + + fun mottakerlandSED(): List { + if (this.mottakerlandSed.contains("NO")) { + throw FunksjonellFeil( + frontendFeilmelding = "Norge kan ikke velges som mottakerland.", + melding = "Ugyldig mottakerland for brevtype 'varsel om årlig revurdering EØS'", + ) + } + return this.mottakerlandSed.takeIf { it.isNotEmpty() } + ?: error("Finner ikke noen mottakerland for SED på manuell brevrequest") + } +} + +fun ManueltBrevRequest.byggMottakerdata( + behandling: Behandling, + persongrunnlagService: PersongrunnlagService, + arbeidsfordelingService: ArbeidsfordelingService, +): ManueltBrevRequest { + val hentPerson = { ident: String -> + persongrunnlagService.hentPersonerPåBehandling(listOf(ident), behandling).singleOrNull() + ?: error("Fant flere eller ingen personer med angitt personident på behandling $behandling") + } + val enhet = arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandling.id).run { + Enhet(enhetId = behandlendeEnhetId, enhetNavn = behandlendeEnhetNavn) + } + return when { + erTilInstitusjon -> + this.copy( + enhet = enhet, + vedrørende = object : Person { + override val fødselsnummer = behandling.fagsak.aktør.aktivFødselsnummer() + override val navn = hentPerson(fødselsnummer).navn + }, + ) + + else -> hentPerson(mottakerIdent).let { mottakerPerson -> + this.copy( + enhet = enhet, + mottakerMålform = mottakerPerson.målform, + mottakerNavn = mottakerPerson.navn, + ) + } + } +} + +fun ManueltBrevRequest.leggTilEnhet(arbeidsfordelingService: ArbeidsfordelingService): ManueltBrevRequest { + val arbeidsfordelingsenhet = arbeidsfordelingService.hentArbeidsfordelingsenhetPåIdenter( + søkerIdent = mottakerIdent, + barnIdenter = barnIBrev, + ) + return this.copy( + enhet = Enhet( + enhetNavn = arbeidsfordelingsenhet.enhetNavn, + enhetId = arbeidsfordelingsenhet.enhetId, + ), + ) +} + +fun ManueltBrevRequest.tilBrev(saksbehandlerNavn: String, hentLandkoder: (() -> Map)): Brev { + val signaturDelmal = SignaturDelmal( + enhet = this.enhetNavn(), + saksbehandlerNavn = saksbehandlerNavn, + ) + + return when (this.brevmal) { + Brevmal.INFORMASJONSBREV_DELT_BOSTED -> + InformasjonsbrevDeltBostedBrev( + data = InformasjonsbrevDeltBostedData( + delmalData = InformasjonsbrevDeltBostedData.DelmalData( + signatur = signaturDelmal, + ), + flettefelter = InformasjonsbrevDeltBostedData.Flettefelter( + navn = this.mottakerNavn, + fodselsnummer = this.mottakerIdent, + barnMedDeltBostedAvtale = this.multiselectVerdier, + ), + ), + ) + + Brevmal.INFORMASJONSBREV_TIL_FORELDER_MED_SELVSTENDIG_RETT_VI_HAR_FÅTT_F016_KAN_SØKE_OM_BARNETRYGD, + Brevmal.INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_FÅTT_EN_SØKNAD_FRA_ANNEN_FORELDER, + Brevmal.INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_GJORT_VEDTAK_TIL_ANNEN_FORELDER, + Brevmal.INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_VARSEL_OM_ÅRLIG_KONTROLL, + -> + InformasjonsbrevTilForelderBrev( + mal = this.brevmal, + data = InformasjonsbrevTilForelderData( + delmalData = InformasjonsbrevTilForelderData.DelmalData( + signatur = signaturDelmal, + ), + flettefelter = InformasjonsbrevTilForelderData.Flettefelter( + navn = this.mottakerNavn, + fodselsnummer = this.mottakerIdent, + barnSøktFor = this.multiselectVerdier, + ), + ), + ) + + Brevmal.INNHENTE_OPPLYSNINGER, + Brevmal.INNHENTE_OPPLYSNINGER_INSTITUSJON, + -> + InnhenteOpplysningerBrev( + mal = brevmal, + data = InnhenteOpplysningerData( + delmalData = InnhenteOpplysningerData.DelmalData(signatur = signaturDelmal), + flettefelter = InnhenteOpplysningerData.Flettefelter( + navn = this.mottakerNavn, + fodselsnummer = this.vedrørende?.fødselsnummer ?: mottakerIdent, + organisasjonsnummer = if (erTilInstitusjon) mottakerIdent else null, + gjelder = this.vedrørende?.navn, + dokumentliste = this.multiselectVerdier, + ), + ), + ) + + Brevmal.HENLEGGE_TRUKKET_SØKNAD -> + HenleggeTrukketSøknadBrev( + data = HenleggeTrukketSøknadData( + delmalData = HenleggeTrukketSøknadData.DelmalData(signatur = signaturDelmal), + flettefelter = FlettefelterForDokumentImpl( + navn = this.mottakerNavn, + fodselsnummer = this.mottakerIdent, + ), + ), + ) + + Brevmal.VARSEL_OM_REVURDERING -> + VarselbrevMedÅrsaker( + mal = Brevmal.VARSEL_OM_REVURDERING, + navn = this.mottakerNavn, + fødselsnummer = this.mottakerIdent, + varselÅrsaker = this.multiselectVerdier, + enhet = this.enhetNavn(), + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.VARSEL_OM_REVURDERING_INSTITUSJON -> + VarselbrevMedÅrsaker( + mal = Brevmal.VARSEL_OM_REVURDERING_INSTITUSJON, + navn = this.mottakerNavn, + fødselsnummer = this.vedrørende?.fødselsnummer ?: mottakerIdent, + varselÅrsaker = this.multiselectVerdier, + enhet = this.enhetNavn(), + organisasjonsnummer = mottakerIdent, + gjelder = this.vedrørende?.navn, + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.VARSEL_OM_REVURDERING_DELT_BOSTED_PARAGRAF_14 -> + VarselOmRevurderingDeltBostedParagraf14Brev( + data = VarselOmRevurderingDeltBostedParagraf14Data( + delmalData = VarselOmRevurderingDeltBostedParagraf14Data.DelmalData(signatur = signaturDelmal), + flettefelter = VarselOmRevurderingDeltBostedParagraf14Data.Flettefelter( + navn = this.mottakerNavn, + fodselsnummer = this.mottakerIdent, + barnMedDeltBostedAvtale = this.multiselectVerdier, + ), + ), + ) + + Brevmal.VARSEL_OM_REVURDERING_SAMBOER -> + if (this.datoAvtale == null) { + throw FunksjonellFeil( + frontendFeilmelding = "Du må sette dato for samboerskap for å sende dette brevet.", + melding = "Dato er ikke satt for brevtype 'varsel om revurdering samboer'", + ) + } else { + VarselOmRevurderingSamboerBrev( + data = VarselOmRevurderingSamboerData( + delmalData = VarselOmRevurderingSamboerData.DelmalData(signatur = signaturDelmal), + flettefelter = VarselOmRevurderingSamboerData.Flettefelter( + navn = this.mottakerNavn, + fodselsnummer = this.mottakerIdent, + datoAvtale = LocalDate.parse(this.datoAvtale).tilDagMånedÅr(), + ), + ), + ) + } + + Brevmal.VARSEL_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_SØKT -> + VarselbrevMedÅrsakerOgBarn( + mal = Brevmal.VARSEL_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_SØKT, + navn = this.mottakerNavn, + fødselsnummer = this.vedrørende?.fødselsnummer ?: mottakerIdent, + varselÅrsaker = this.multiselectVerdier, + barnasFødselsdager = this.barnasFødselsdager.tilFormaterteFødselsdager(), + enhet = this.enhetNavn(), + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.SVARTIDSBREV -> + Svartidsbrev( + navn = this.mottakerNavn, + fodselsnummer = this.mottakerIdent, + enhet = this.enhetNavn(), + mal = Brevmal.SVARTIDSBREV, + erEøsBehandling = if (this.behandlingKategori == null) { + throw Feil("Trenger å vite om behandling er EØS for å sende ut svartidsbrev.") + } else { + this.behandlingKategori == BehandlingKategori.EØS + }, + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.SVARTIDSBREV_INSTITUSJON -> + Svartidsbrev( + navn = this.mottakerNavn, + fodselsnummer = this.vedrørende?.fødselsnummer ?: mottakerIdent, + enhet = this.enhetNavn(), + mal = Brevmal.SVARTIDSBREV_INSTITUSJON, + erEøsBehandling = false, + organisasjonsnummer = mottakerIdent, + gjelder = this.vedrørende?.navn, + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.FORLENGET_SVARTIDSBREV, + Brevmal.FORLENGET_SVARTIDSBREV_INSTITUSJON, + -> + ForlengetSvartidsbrev( + mal = brevmal, + navn = this.mottakerNavn, + fodselsnummer = this.vedrørende?.fødselsnummer ?: mottakerIdent, + enhetNavn = this.enhetNavn(), + årsaker = this.multiselectVerdier, + antallUkerSvarfrist = this.antallUkerSvarfrist ?: throw FunksjonellFeil( + melding = "Antall uker svarfrist er ikke satt", + frontendFeilmelding = "Antall uker svarfrist er ikke satt", + ), + organisasjonsnummer = if (erTilInstitusjon) mottakerIdent else null, + gjelder = this.vedrørende?.navn, + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.INFORMASJONSBREV_FØDSEL_MINDREÅRIG -> + EnkeltInformasjonsbrev( + navn = this.mottakerNavn, + fodselsnummer = this.mottakerIdent, + enhet = this.enhetNavn(), + mal = Brevmal.INFORMASJONSBREV_FØDSEL_MINDREÅRIG, + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.INFORMASJONSBREV_FØDSEL_VERGEMÅL -> + EnkeltInformasjonsbrev( + navn = this.mottakerNavn, + fodselsnummer = this.mottakerIdent, + enhet = this.enhetNavn(), + mal = Brevmal.INFORMASJONSBREV_FØDSEL_VERGEMÅL, + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.INFORMASJONSBREV_FØDSEL_GENERELL -> + EnkeltInformasjonsbrev( + navn = this.mottakerNavn, + fodselsnummer = this.mottakerIdent, + enhet = this.enhetNavn(), + mal = Brevmal.INFORMASJONSBREV_FØDSEL_GENERELL, + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.INFORMASJONSBREV_KAN_SØKE -> + InformasjonsbrevKanSøke( + navn = this.mottakerNavn, + fodselsnummer = this.mottakerIdent, + enhet = this.enhetNavn(), + dokumentliste = this.multiselectVerdier, + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.VARSEL_OM_VEDTAK_ETTER_SØKNAD_I_SED -> + VarselbrevMedÅrsakerOgBarn( + mal = Brevmal.VARSEL_OM_VEDTAK_ETTER_SØKNAD_I_SED, + navn = this.mottakerNavn, + fødselsnummer = this.mottakerIdent, + enhet = this.enhetNavn(), + varselÅrsaker = this.multiselectVerdier, + barnasFødselsdager = this.barnasFødselsdager.tilFormaterteFødselsdager(), + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.VARSEL_OM_REVURDERING_FRA_NASJONAL_TIL_EØS -> + VarselbrevMedÅrsaker( + mal = Brevmal.VARSEL_OM_REVURDERING_FRA_NASJONAL_TIL_EØS, + navn = this.mottakerNavn, + fødselsnummer = this.mottakerIdent, + varselÅrsaker = this.multiselectVerdier, + enhet = this.enhetNavn(), + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS -> + VarselbrevÅrlegKontrollEøs( + mal = Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS, + navn = this.mottakerNavn, + fødselsnummer = this.mottakerIdent, + enhet = this.enhetNavn(), + mottakerlandSed = Utils.slåSammen(this.mottakerlandSED().map { tilLandNavn(hentLandkoder(), it) }), + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER -> + VarselbrevÅrlegKontrollEøs( + mal = Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER, + navn = this.mottakerNavn, + fødselsnummer = this.mottakerIdent, + enhet = this.enhetNavn(), + mottakerlandSed = Utils.slåSammen(this.mottakerlandSED().map { tilLandNavn(hentLandkoder(), it) }), + dokumentliste = this.multiselectVerdier, + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.INNHENTE_OPPLYSNINGER_ETTER_SØKNAD_I_SED -> + InnhenteOpplysningerOmBarn( + mal = Brevmal.INNHENTE_OPPLYSNINGER_ETTER_SØKNAD_I_SED, + navn = this.mottakerNavn, + fødselsnummer = this.mottakerIdent, + dokumentliste = this.multiselectVerdier, + enhet = this.enhetNavn(), + barnasFødselsdager = this.barnasFødselsdager.tilFormaterteFødselsdager(), + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.INNHENTE_OPPLYSNINGER_OG_INFORMASJON_OM_AT_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_HAR_SØKT -> + InnhenteOpplysningerOmBarn( + mal = Brevmal.INNHENTE_OPPLYSNINGER_OG_INFORMASJON_OM_AT_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_HAR_SØKT, + navn = this.mottakerNavn, + fødselsnummer = this.mottakerIdent, + dokumentliste = this.multiselectVerdier, + enhet = this.enhetNavn(), + barnasFødselsdager = this.barnasFødselsdager.tilFormaterteFødselsdager(), + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.INFORMASJONSBREV_KAN_SØKE_EØS -> + EnkeltInformasjonsbrev( + navn = this.mottakerNavn, + fodselsnummer = this.mottakerIdent, + enhet = this.enhetNavn(), + mal = Brevmal.INFORMASJONSBREV_KAN_SØKE_EØS, + saksbehandlerNavn = saksbehandlerNavn, + ) + + Brevmal.VEDTAK_FØRSTEGANGSVEDTAK, + Brevmal.VEDTAK_ENDRING, + Brevmal.VEDTAK_OPPHØRT, + Brevmal.VEDTAK_OPPHØR_MED_ENDRING, + Brevmal.VEDTAK_AVSLAG, + Brevmal.VEDTAK_FORTSATT_INNVILGET, + Brevmal.VEDTAK_KORREKSJON_VEDTAKSBREV, + Brevmal.VEDTAK_OPPHØR_DØDSFALL, + Brevmal.VEDTAK_FØRSTEGANGSVEDTAK_INSTITUSJON, + Brevmal.VEDTAK_AVSLAG_INSTITUSJON, + Brevmal.VEDTAK_OPPHØRT_INSTITUSJON, + Brevmal.VEDTAK_ENDRING_INSTITUSJON, + Brevmal.VEDTAK_FORTSATT_INNVILGET_INSTITUSJON, + Brevmal.VEDTAK_OPPHØR_MED_ENDRING_INSTITUSJON, + Brevmal.AUTOVEDTAK_BARN_6_OG_18_ÅR_OG_SMÅBARNSTILLEGG, + Brevmal.AUTOVEDTAK_NYFØDT_FØRSTE_BARN, + Brevmal.AUTOVEDTAK_NYFØDT_BARN_FRA_FØR, + -> throw Feil("Kan ikke mappe fra manuel brevrequest til ${this.brevmal}.") + } +} + +private fun tilLandNavn(landkoderISO2: Map, landKode: String): String { + if (landKode.length != 2) { + throw Feil("LandkoderISO2 forventer en landkode med to tegn") + } + + val landNavn = ( + landkoderISO2[landKode] + ?: throw Feil("Fant ikke navn for landkode $landKode ") + ) + + return landNavn.storForbokstav() +} + +private fun List?.tilFormaterteFødselsdager() = Utils.slåSammen( + this?.map { it.tilKortString() } + ?: throw Feil("Fikk ikke med barna sine fødselsdager"), +) + +val ManueltBrevRequest.erTilInstitusjon + get() = when { + erOrgNr(mottakerIdent) -> true + else -> false + } + +private fun erOrgNr(ident: String): Boolean = ident.length == 9 && ident.all { it.isDigit() } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertEndretAndel.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertEndretAndel.kt new file mode 100644 index 000000000..6c9ce74d9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertEndretAndel.kt @@ -0,0 +1,56 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.NullableMånedPeriode +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.overlapperHeltEllerDelvisMed +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import java.math.BigDecimal +import java.time.YearMonth + +class MinimertEndretAndel( + val aktørId: String, + val fom: YearMonth?, + val tom: YearMonth?, + val årsak: Årsak?, + val prosent: BigDecimal?, +) { + fun månedPeriode() = MånedPeriode(fom!!, tom!!) + + fun erOverlappendeMed(nullableMånedPeriode: NullableMånedPeriode): Boolean { + if (nullableMånedPeriode.fom == null) { + throw Feil("Fom ble null ved sjekk av overlapp av periode til endretUtbetalingAndel") + } + + return MånedPeriode( + this.fom!!, + this.tom!!, + ).overlapperHeltEllerDelvisMed( + MånedPeriode( + nullableMånedPeriode.fom, + nullableMånedPeriode.tom ?: TIDENES_ENDE.toYearMonth(), + ), + ) + } +} + +fun EndretUtbetalingAndel.tilMinimertEndretUtbetalingAndel(): MinimertEndretAndel { + this.validerUtfyltEndring() + + return MinimertEndretAndel( + fom = this.fom!!, + tom = this.tom!!, + aktørId = this.person?.aktør?.aktørId ?: throw Feil( + "Finner ikke aktørId på endretUtbetalingsandel ${this.id} " + + "ved konvertering til minimertEndretUtbetalingsandel", + ), + årsak = this.årsak ?: throw Feil( + "Har ikke årsak på endretUtbetalingsandel ${this.id} " + + "ved konvertering til minimertEndretUtbetalingsandel", + ), + prosent = this.prosent, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertKompetanse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertKompetanse.kt new file mode 100644 index 000000000..29e2d6c71 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertKompetanse.kt @@ -0,0 +1,64 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.Utils.storForbokstav +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.vedtak.domene.MinimertRestPerson + +data class MinimertKompetanse( + val søkersAktivitet: KompetanseAktivitet, + val søkersAktivitetsland: LandNavn?, + val annenForeldersAktivitet: KompetanseAktivitet, + val annenForeldersAktivitetslandNavn: LandNavn?, + val barnetsBostedslandNavn: LandNavn, + val resultat: KompetanseResultat, + val personer: List, +) + +fun Kompetanse.tilMinimertKompetanse( + personopplysningGrunnlag: PersonopplysningGrunnlag, + landkoderISO2: Map, +): MinimertKompetanse { + this.validerFelterErSatt() + + val barnetsBostedslandNavn = this.barnetsBostedsland!!.tilLandNavn(landkoderISO2) + val annenForeldersAktivitetslandNavn = this.annenForeldersAktivitetsland?.tilLandNavn(landkoderISO2) + val sokersAktivitetslandNavn = this.søkersAktivitetsland?.tilLandNavn(landkoderISO2) + + return MinimertKompetanse( + søkersAktivitet = this.søkersAktivitet!!, + søkersAktivitetsland = sokersAktivitetslandNavn, + annenForeldersAktivitet = this.annenForeldersAktivitet!!, + annenForeldersAktivitetslandNavn = annenForeldersAktivitetslandNavn, + barnetsBostedslandNavn = barnetsBostedslandNavn, + resultat = this.resultat!!, + personer = this.barnAktører.map { aktør -> + val fødselsdato = personopplysningGrunnlag.barna.find { it.aktør == aktør }?.fødselsdato + ?: throw Feil("Fant ikke aktør i personopplysninggrunnlaget") + MinimertRestPerson( + personIdent = aktør.aktivFødselsnummer(), + fødselsdato = fødselsdato, + type = PersonType.BARN, + ) + }, + ) +} + +data class LandNavn(val navn: String) + +private fun String.tilLandNavn(landkoderISO2: Map): LandNavn { + if (this.length != 2) { + throw Feil("LandkoderISO2 forventer en landkode med to tegn") + } + + val landNavn = ( + landkoderISO2[this] + ?: throw Feil("Fant ikke navn for landkode $this ") + ) + + return LandNavn(landNavn.storForbokstav()) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertRestEndretAndel.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertRestEndretAndel.kt new file mode 100644 index 000000000..6703b9468 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertRestEndretAndel.kt @@ -0,0 +1,65 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.NullableMånedPeriode +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.overlapperHeltEllerDelvisMed +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.domene.EndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import java.time.LocalDate + +/** + * NB: Bør ikke brukes internt, men kun ut mot eksterne tjenester siden klassen + * inneholder personIdent og ikke aktørId. + */ +data class MinimertRestEndretAndel( + val periode: MånedPeriode, + val personIdent: String, + val årsak: Årsak, + val søknadstidspunkt: LocalDate, + val avtaletidspunktDeltBosted: LocalDate?, +) { + fun erOverlappendeMed(nullableMånedPeriode: NullableMånedPeriode): Boolean { + return MånedPeriode( + this.periode.fom, + this.periode.tom, + ).overlapperHeltEllerDelvisMed( + MånedPeriode( + nullableMånedPeriode.fom ?: TIDENES_MORGEN.toYearMonth(), + nullableMånedPeriode.tom ?: TIDENES_ENDE.toYearMonth(), + ), + ) + } +} + +fun List.somOverlapper(nullableMånedPeriode: NullableMånedPeriode) = + this.filter { it.erOverlappendeMed(nullableMånedPeriode) } + +fun EndretUtbetalingAndelMedAndelerTilkjentYtelse.tilMinimertRestEndretUtbetalingAndel() = MinimertRestEndretAndel( + periode = this.periode, + personIdent = this.person?.aktør?.aktivFødselsnummer() ?: throw Feil( + "Har ikke ident på endretUtbetalingsandel ${this.id} " + + "ved konvertering til minimertRestEndretUtbetalingsandel", + ), + årsak = this.årsak ?: throw Feil( + "Har ikke årsak på endretUtbetalingsandel ${this.id} " + + "ved konvertering til minimertRestEndretUtbetalingsandel", + ), + søknadstidspunkt = this.søknadstidspunkt ?: throw Feil( + "Har ikke søknadstidspunk på endretUtbetalingsandel ${this.id} " + + "ved konvertering til minimertRestEndretUtbetalingsandel", + ), + avtaletidspunktDeltBosted = this.avtaletidspunktDeltBosted ?: ( + if (this.årsakErDeltBosted()) { + throw Feil( + "Har ikke avtaletidspunktDeltBosted på endretUtbetalingsandel ${this.id} " + + "ved konvertering til minimertRestEndretUtbetalingsandel", + ) + } else { + null + } + ), +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertRestPersonResultat.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertRestPersonResultat.kt new file mode 100644 index 000000000..c73fa8294 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertRestPersonResultat.kt @@ -0,0 +1,39 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurderingType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat + +/** + * NB: Bør ikke brukes internt, men kun ut mot eksterne tjenester siden klassen + * inneholder aktiv personIdent og ikke aktørId. + */ +data class MinimertRestPersonResultat( + val personIdent: String, + val minimerteVilkårResultater: List, + val minimerteAndreVurderinger: List, +) + +data class MinimertAnnenVurdering( + val type: AnnenVurderingType, + val resultat: Resultat, +) + +fun AnnenVurdering.tilMinimertAnnenVurdering(): MinimertAnnenVurdering { + return MinimertAnnenVurdering(type = this.type, resultat = this.resultat) +} + +fun PersonResultat.tilMinimertPersonResultat() = + MinimertRestPersonResultat( + personIdent = this.aktør.aktivFødselsnummer(), + minimerteVilkårResultater = this.vilkårResultater.map { it.tilMinimertVilkårResultat() }, + minimerteAndreVurderinger = this.andreVurderinger.map { it.tilMinimertAnnenVurdering() }, + ) + +fun List.harPersonerSomManglerOpplysninger(): Boolean = + this.any { personResultat -> + personResultat.minimerteAndreVurderinger.any { + it.type == AnnenVurderingType.OPPLYSNINGSPLIKT && it.resultat == Resultat.IKKE_OPPFYLT + } + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertUregistrertBarn.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertUregistrertBarn.kt new file mode 100644 index 000000000..e61d7fa2a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertUregistrertBarn.kt @@ -0,0 +1,16 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.ekstern.restDomene.BarnMedOpplysninger +import java.time.LocalDate + +data class MinimertUregistrertBarn( + val personIdent: String, + val navn: String, + val fødselsdato: LocalDate? = null, +) + +fun BarnMedOpplysninger.tilMinimertUregistrertBarn() = MinimertUregistrertBarn( + personIdent = this.ident, + navn = this.navn, + fødselsdato = this.fødselsdato, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertUtbetalingsperiodeDetalj.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertUtbetalingsperiodeDetalj.kt new file mode 100644 index 000000000..a47c03a21 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertUtbetalingsperiodeDetalj.kt @@ -0,0 +1,39 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.domene.MinimertRestPerson +import no.nav.familie.ba.sak.kjerne.vedtak.domene.tilMinimertPerson +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.UtbetalingsperiodeDetalj +import java.math.BigDecimal + +data class MinimertUtbetalingsperiodeDetalj( + val person: MinimertRestPerson, + val ytelseType: YtelseType, + val utbetaltPerMnd: Int, + val erPåvirketAvEndring: Boolean, + val endringsårsak: Årsak?, + val prosent: BigDecimal, +) + +fun UtbetalingsperiodeDetalj.tilMinimertUtbetalingsperiodeDetalj() = MinimertUtbetalingsperiodeDetalj( + person = this.person.tilMinimertPerson(), + ytelseType = this.ytelseType, + utbetaltPerMnd = this.utbetaltPerMnd, + erPåvirketAvEndring = this.erPåvirketAvEndring, + prosent = this.prosent, + endringsårsak = this.endringsårsak, +) + +fun List.antallBarn(): Int = + this.filter { it.person.type == PersonType.BARN }.size + +fun List.totaltUtbetalt(): Int = + this.sumOf { it.utbetaltPerMnd } + +fun List.beløpUtbetaltFor( + personIdenter: List, +) = this + .filter { utbetalingsperiodeDetalj -> personIdenter.contains(utbetalingsperiodeDetalj.person.personIdent) } + .totaltUtbetalt() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertVedtaksperiode.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertVedtaksperiode.kt new file mode 100644 index 000000000..d48a2f878 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertVedtaksperiode.kt @@ -0,0 +1,65 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.kjerne.brev.domene.eøs.EØSBegrunnelseMedTriggere +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.tilBrevPeriodeTestPerson +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.UtvidetVedtaksperiodeMedBegrunnelser +import java.time.LocalDate + +data class MinimertVedtaksperiode( + val fom: LocalDate?, + val tom: LocalDate?, + val type: Vedtaksperiodetype, + val begrunnelser: List, + val eøsBegrunnelser: List, + val fritekster: List = emptyList(), + val minimerteUtbetalingsperiodeDetaljer: List = emptyList(), +) + +fun UtvidetVedtaksperiodeMedBegrunnelser.tilMinimertVedtaksperiode( + sanityBegrunnelser: Map, + sanityEØSBegrunnelser: Map, +): MinimertVedtaksperiode { + return MinimertVedtaksperiode( + fom = this.fom, + tom = this.tom, + type = this.type, + fritekster = this.fritekster, + minimerteUtbetalingsperiodeDetaljer = this.utbetalingsperiodeDetaljer.map { it.tilMinimertUtbetalingsperiodeDetalj() }, + begrunnelser = this.begrunnelser.map { it.tilBegrunnelseMedTriggere(sanityBegrunnelser) }, + eøsBegrunnelser = this.eøsBegrunnelser.mapNotNull { + it.begrunnelse.tilEØSBegrunnelseMedTriggere( + sanityEØSBegrunnelser, + ) + }, + ) +} + +fun MinimertVedtaksperiode.tilBrevPeriodeForLogging( + restBehandlingsgrunnlagForBrev: RestBehandlingsgrunnlagForBrev, + uregistrerteBarn: List = emptyList(), + erFørsteVedtaksperiodePåFagsak: Boolean = false, + brevMålform: Målform, + barnMedReduksjonFraForrigeBehandlingIdent: List = emptyList(), +): BrevPeriodeForLogging { + return BrevPeriodeForLogging( + fom = this.fom, + tom = this.tom, + vedtaksperiodetype = this.type, + begrunnelser = this.begrunnelser.map { it.tilBrevBegrunnelseGrunnlagForLogging() }, + fritekster = this.fritekster, + personerPåBehandling = restBehandlingsgrunnlagForBrev.personerPåBehandling.map { + it.tilBrevPeriodeTestPerson( + brevPeriodeGrunnlag = this, + restBehandlingsgrunnlagForBrev = restBehandlingsgrunnlagForBrev, + barnMedReduksjonFraForrigeBehandlingIdent = barnMedReduksjonFraForrigeBehandlingIdent, + ) + }, + uregistrerteBarn = uregistrerteBarn.map { it.copy(personIdent = "", navn = "") }, + erFørsteVedtaksperiodePåFagsak = erFørsteVedtaksperiodePåFagsak, + brevMålform = brevMålform, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertVilk\303\245rResultat.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertVilk\303\245rResultat.kt" new file mode 100644 index 000000000..029632c5b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/MinimertVilk\303\245rResultat.kt" @@ -0,0 +1,41 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.lagOgValiderPeriodeFraVilkår +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelseDeserializer +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import java.time.LocalDate + +data class MinimertVilkårResultat( + val vilkårType: Vilkår, + val periodeFom: LocalDate?, + val periodeTom: LocalDate?, + val resultat: Resultat, + val utdypendeVilkårsvurderinger: List, + val erEksplisittAvslagPåSøknad: Boolean?, + @JsonDeserialize(using = IVedtakBegrunnelseDeserializer::class) + val standardbegrunnelser: List, +) { + + fun toPeriode(): Periode = lagOgValiderPeriodeFraVilkår( + this.periodeFom, + this.periodeTom, + this.erEksplisittAvslagPåSøknad, + ) +} + +fun VilkårResultat.tilMinimertVilkårResultat() = + MinimertVilkårResultat( + vilkårType = this.vilkårType, + periodeFom = this.periodeFom, + periodeTom = this.periodeTom, + resultat = this.resultat, + utdypendeVilkårsvurderinger = this.utdypendeVilkårsvurderinger, + erEksplisittAvslagPåSøknad = this.erEksplisittAvslagPåSøknad, + standardbegrunnelser = this.standardbegrunnelser, + ) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/RestBehandlingsgrunnlagForBrev.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/RestBehandlingsgrunnlagForBrev.kt new file mode 100644 index 000000000..8291b5414 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/RestBehandlingsgrunnlagForBrev.kt @@ -0,0 +1,11 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.vedtak.domene.MinimertRestPerson + +data class RestBehandlingsgrunnlagForBrev( + val personerPåBehandling: List, + val minimertePersonResultater: List, + val minimerteEndredeUtbetalingAndeler: List, + val fagsakType: FagsakType, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/RestSanityBegrunnelse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/RestSanityBegrunnelse.kt new file mode 100644 index 000000000..feac3f9ba --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/RestSanityBegrunnelse.kt @@ -0,0 +1,225 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityVilkår.BOR_MED_SOKER +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityVilkår.BOSATT_I_RIKET +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityVilkår.GIFT_PARTNERSKAP +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityVilkår.LOVLIG_OPPHOLD +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityVilkår.UNDER_18_ÅR +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityVilkår.UTVIDET_BARNETRYGD +import no.nav.familie.ba.sak.kjerne.brev.domene.VilkårRolle.BARN +import no.nav.familie.ba.sak.kjerne.brev.domene.VilkårRolle.SOKER +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.BrevPeriodeType +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +data class RestSanityBegrunnelse( + val apiNavn: String?, + val navnISystem: String, + val vilkaar: List? = emptyList(), + val rolle: List? = emptyList(), + val lovligOppholdTriggere: List? = emptyList(), + val bosattIRiketTriggere: List? = emptyList(), + val giftPartnerskapTriggere: List? = emptyList(), + val borMedSokerTriggere: List? = emptyList(), + val ovrigeTriggere: List? = emptyList(), + val endringsaarsaker: List? = emptyList(), + val hjemler: List? = emptyList(), + val hjemlerFolketrygdloven: List?, + val endretUtbetalingsperiodeDeltBostedUtbetalingTrigger: String?, + val endretUtbetalingsperiodeTriggere: List? = emptyList(), + val utvidetBarnetrygdTriggere: List? = emptyList(), + val valgbarhet: String? = null, + val vedtakResultat: String?, + val fagsakType: String?, + val tema: String?, + val periodeType: String?, +) { + fun tilSanityBegrunnelse(): SanityBegrunnelse? { + if (apiNavn == null || apiNavn !in Standardbegrunnelse.entries.map { it.sanityApiNavn }) return null + return SanityBegrunnelse( + apiNavn = apiNavn, + navnISystem = navnISystem, + vilkaar = vilkaar?.mapNotNull { + it.finnEnumverdi(apiNavn) + } ?: emptyList(), + vilkår = vilkaar?.mapNotNull { + it.finnEnumverdi(apiNavn) + }?.map { it.tilVilkår() }?.toSet() + ?: emptySet(), + rolle = rolle?.mapNotNull { it.finnEnumverdi(apiNavn) } + ?: emptyList(), + lovligOppholdTriggere = lovligOppholdTriggere?.mapNotNull { + it.finnEnumverdi(apiNavn) + } ?: emptyList(), + bosattIRiketTriggere = bosattIRiketTriggere?.mapNotNull { + it.finnEnumverdi(apiNavn) + } ?: emptyList(), + giftPartnerskapTriggere = giftPartnerskapTriggere?.mapNotNull { + it.finnEnumverdi(apiNavn) + } ?: emptyList(), + borMedSokerTriggere = borMedSokerTriggere?.mapNotNull { + it.finnEnumverdi(apiNavn) + } ?: emptyList(), + ovrigeTriggere = ovrigeTriggere?.mapNotNull { + it.finnEnumverdi<ØvrigTrigger>(apiNavn) + } ?: emptyList(), + endringsaarsaker = endringsaarsaker?.mapNotNull { + it.finnEnumverdi<Årsak>(apiNavn) + } ?: emptyList(), + hjemler = hjemler ?: emptyList(), + hjemlerFolketrygdloven = hjemlerFolketrygdloven ?: emptyList(), + endretUtbetalingsperiodeDeltBostedUtbetalingTrigger = endretUtbetalingsperiodeDeltBostedUtbetalingTrigger + .finnEnumverdiNullable(), + endretUtbetalingsperiodeTriggere = endretUtbetalingsperiodeTriggere?.mapNotNull { + it.finnEnumverdi(apiNavn) + } ?: emptyList(), + utvidetBarnetrygdTriggere = utvidetBarnetrygdTriggere?.mapNotNull { + it.finnEnumverdi(apiNavn) + } ?: emptyList(), + valgbarhet = valgbarhet.finnEnumverdi(apiNavn), + periodeResultat = vedtakResultat.finnEnumverdi(apiNavn), + fagsakType = fagsakType.finnEnumverdiNullable(), + tema = tema.finnEnumverdi(apiNavn), + periodeType = periodeType.finnEnumverdi(apiNavn), + ) + } +} + +inline fun > String?.finnEnumverdi(apiNavn: String): T? { + val enumverdi = enumValues().find { this != null && it.name == this } + if (enumverdi == null) { + val logger: Logger = LoggerFactory.getLogger(RestSanityBegrunnelse::class.java) + logger.error( + "$this på begrunnelsen $apiNavn er ikke blant verdiene til enumen ${enumValues().javaClass.simpleName}", + ) + } + return enumverdi +} + +inline fun > String?.finnEnumverdiNullable(): T? { + return enumValues().find { this != null && it.name == this } +} + +enum class SanityVilkår { + UNDER_18_ÅR, + BOR_MED_SOKER, + GIFT_PARTNERSKAP, + BOSATT_I_RIKET, + LOVLIG_OPPHOLD, + UTVIDET_BARNETRYGD, +} + +fun SanityVilkår.tilVilkår() = when (this) { + UNDER_18_ÅR -> Vilkår.UNDER_18_ÅR + BOR_MED_SOKER -> Vilkår.BOR_MED_SØKER + GIFT_PARTNERSKAP -> Vilkår.GIFT_PARTNERSKAP + BOSATT_I_RIKET -> Vilkår.BOSATT_I_RIKET + LOVLIG_OPPHOLD -> Vilkår.LOVLIG_OPPHOLD + UTVIDET_BARNETRYGD -> Vilkår.UTVIDET_BARNETRYGD +} + +fun VilkårRolle.tilPersonType() = + when (this) { + SOKER -> PersonType.SØKER + BARN -> PersonType.BARN + } + +enum class VilkårRolle { + SOKER, + BARN, +} + +enum class VilkårTrigger { + VURDERING_ANNET_GRUNNLAG, + MEDLEMSKAP, + DELT_BOSTED, + DELT_BOSTED_SKAL_IKKE_DELES, +} + +fun List.tilUtdypendeVilkårsvurderinger() = this.map { + when (it) { + VilkårTrigger.VURDERING_ANNET_GRUNNLAG -> UtdypendeVilkårsvurdering.VURDERING_ANNET_GRUNNLAG + VilkårTrigger.MEDLEMSKAP -> UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP + VilkårTrigger.DELT_BOSTED -> UtdypendeVilkårsvurdering.DELT_BOSTED + VilkårTrigger.DELT_BOSTED_SKAL_IKKE_DELES -> UtdypendeVilkårsvurdering.DELT_BOSTED_SKAL_IKKE_DELES + } +} + +enum class SanityPeriodeResultat { + INNVILGET_ELLER_ØKNING, + INGEN_ENDRING, + IKKE_INNVILGET, + REDUKSJON, +} + +enum class ØvrigTrigger { + MANGLER_OPPLYSNINGER, + SATSENDRING, + BARN_MED_6_ÅRS_DAG, + ALLTID_AUTOMATISK, + ETTER_ENDRET_UTBETALING, + ENDRET_UTBETALING, + OPPHØR_FRA_FORRIGE_BEHANDLING, + REDUKSJON_FRA_FORRIGE_BEHANDLING, + BARN_DØD, + + @Deprecated("Skal erstattes med OPPHØR_FRA_FORRIGE_BEHANDLING, må endres i sanity") + GJELDER_FØRSTE_PERIODE, + + @Deprecated("Skal erstattes med REDUKSJON_FRA_FORRIGE_BEHANDLING, må endres i sanity") + GJELDER_FRA_INNVILGELSESTIDSPUNKT, +} + +enum class EndretUtbetalingsperiodeTrigger { + ETTER_ENDRET_UTBETALINGSPERIODE, +} + +enum class EndretUtbetalingsperiodeDeltBostedTriggere { + SKAL_UTBETALES, + SKAL_IKKE_UTBETALES, + UTBETALING_IKKE_RELEVANT, +} + +enum class UtvidetBarnetrygdTrigger { + SMÅBARNSTILLEGG, +} + +enum class Valgbarhet { + STANDARD, + AUTOMATISK, + TILLEGGSTEKST, + SAKSPESIFIKK, +} + +enum class Tema { + NASJONAL, + EØS, + FELLES, +} + +fun SanityBegrunnelse.inneholderVilkår(vilkår: SanityVilkår) = + this.vilkaar.contains(vilkår) + +fun SanityBegrunnelse.inneholderØvrigTrigger(øvrigTrigger: ØvrigTrigger) = + this.ovrigeTriggere.contains(øvrigTrigger) + +fun SanityBegrunnelse.inneholderLovligOppholdTrigger(vilkårTrigger: VilkårTrigger) = + this.lovligOppholdTriggere.contains(vilkårTrigger) + +fun SanityBegrunnelse.inneholderBosattIRiketTrigger(vilkårTrigger: VilkårTrigger) = + this.bosattIRiketTriggere.contains(vilkårTrigger) + +fun SanityBegrunnelse.inneholderGiftPartnerskapTrigger(vilkårTrigger: VilkårTrigger) = + this.giftPartnerskapTriggere.contains(vilkårTrigger) + +fun SanityBegrunnelse.inneholderBorMedSøkerTrigger(vilkårTrigger: VilkårTrigger) = + this.borMedSokerTriggere.contains(vilkårTrigger) + +fun SanityBegrunnelse.inneholderUtvidetBarnetrygdTrigger(utvidetBarnetrygdTrigger: UtvidetBarnetrygdTrigger) = + this.utvidetBarnetrygdTriggere.contains(utvidetBarnetrygdTrigger) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/e\303\270s/E\303\230SBegrunnelseMedKompetanser.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/e\303\270s/E\303\230SBegrunnelseMedKompetanser.kt" new file mode 100644 index 000000000..48da99801 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/e\303\270s/E\303\230SBegrunnelseMedKompetanser.kt" @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.eøs + +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertKompetanse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse + +data class EØSBegrunnelseMedKompetanser( + val begrunnelse: EØSStandardbegrunnelse, + val kompetanser: List, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/e\303\270s/E\303\230SBegrunnelseMedTriggere.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/e\303\270s/E\303\230SBegrunnelseMedTriggere.kt" new file mode 100644 index 000000000..1473e0887 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/e\303\270s/E\303\230SBegrunnelseMedTriggere.kt" @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.eøs + +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse + +data class EØSBegrunnelseMedTriggere( + val eøsBegrunnelse: EØSStandardbegrunnelse, + val sanityEØSBegrunnelse: SanityEØSBegrunnelse, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/e\303\270s/E\303\230SBegrunnelseUtil.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/e\303\270s/E\303\230SBegrunnelseUtil.kt" new file mode 100644 index 000000000..b0aff944d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/e\303\270s/E\303\230SBegrunnelseUtil.kt" @@ -0,0 +1,33 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.eøs + +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertKompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.BarnetsBostedsland + +fun hentKompetanserForEØSBegrunnelse( + eøsBegrunnelseMedTriggere: EØSBegrunnelseMedTriggere, + minimerteKompetanser: List, +) = + minimerteKompetanser.filter { + eøsBegrunnelseMedTriggere.erGyldigForKompetanseMedData( + annenForeldersAktivitetFraKompetanse = it.annenForeldersAktivitet, + barnetsBostedslandFraKompetanse = when (it.barnetsBostedslandNavn.navn) { + "Norge" -> BarnetsBostedsland.NORGE + else -> BarnetsBostedsland.IKKE_NORGE + }, + resultatFraKompetanse = it.resultat, + ) + } + +fun EØSBegrunnelseMedTriggere.erGyldigForKompetanseMedData( + annenForeldersAktivitetFraKompetanse: KompetanseAktivitet, + barnetsBostedslandFraKompetanse: BarnetsBostedsland, + resultatFraKompetanse: KompetanseResultat, +): Boolean = sanityEØSBegrunnelse.annenForeldersAktivitet + .contains(annenForeldersAktivitetFraKompetanse) && + sanityEØSBegrunnelse.barnetsBostedsland + .contains(barnetsBostedslandFraKompetanse) && + sanityEØSBegrunnelse.kompetanseResultat.contains( + resultatFraKompetanse, + ) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Autovedtak6og18\303\245rOgSm\303\245barnstillegg.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Autovedtak6og18\303\245rOgSm\303\245barnstillegg.kt" new file mode 100644 index 000000000..f4b9eda3e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Autovedtak6og18\303\245rOgSm\303\245barnstillegg.kt" @@ -0,0 +1,40 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode + +data class Autovedtak6og18årOgSmåbarnstillegg( + override val mal: Brevmal = Brevmal.AUTOVEDTAK_BARN_6_OG_18_ÅR_OG_SMÅBARNSTILLEGG, + override val data: Autovedtak6og18årData, +) : Vedtaksbrev { + + constructor( + vedtakFellesfelter: VedtakFellesfelter, + ) : + this( + data = Autovedtak6og18årData( + delmalData = Autovedtak6og18årData.Delmaler( + hjemmeltekst = vedtakFellesfelter.hjemmeltekst, + autoUnderskrift = AutoUnderskrift( + enhet = vedtakFellesfelter.enhet, + ), + ), + flettefelter = FlettefelterForDokumentImpl( + navn = vedtakFellesfelter.søkerNavn, + fodselsnummer = vedtakFellesfelter.søkerFødselsnummer, + ), + perioder = vedtakFellesfelter.perioder, + ), + ) +} + +data class Autovedtak6og18årData( + override val delmalData: Delmaler, + override val flettefelter: FlettefelterForDokumentImpl, + override val perioder: List, +) : VedtaksbrevData { + + data class Delmaler( + val hjemmeltekst: Hjemmeltekst, + val autoUnderskrift: AutoUnderskrift, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/AutovedtakNyf\303\270dtBarnFraF\303\270r.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/AutovedtakNyf\303\270dtBarnFraF\303\270r.kt" new file mode 100644 index 000000000..b3caaf68c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/AutovedtakNyf\303\270dtBarnFraF\303\270r.kt" @@ -0,0 +1,43 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode + +data class AutovedtakNyfødtBarnFraFør( + override val mal: Brevmal = Brevmal.AUTOVEDTAK_NYFØDT_BARN_FRA_FØR, + override val data: AutovedtakNyfødtBarnFraFørData, +) : Vedtaksbrev { + + constructor( + vedtakFellesfelter: VedtakFellesfelter, + etterbetaling: Etterbetaling?, + ) : + this( + data = AutovedtakNyfødtBarnFraFørData( + delmalData = AutovedtakNyfødtBarnFraFørData.Delmaler( + etterbetaling = etterbetaling, + hjemmeltekst = vedtakFellesfelter.hjemmeltekst, + autoUnderskrift = AutoUnderskrift( + vedtakFellesfelter.enhet, + ), + ), + flettefelter = FlettefelterForDokumentImpl( + navn = vedtakFellesfelter.søkerNavn, + fodselsnummer = vedtakFellesfelter.søkerFødselsnummer, + ), + perioder = vedtakFellesfelter.perioder, + ), + ) +} + +data class AutovedtakNyfødtBarnFraFørData( + override val delmalData: Delmaler, + override val flettefelter: FlettefelterForDokumentImpl, + override val perioder: List, +) : VedtaksbrevData { + + data class Delmaler( + val etterbetaling: Etterbetaling?, + val hjemmeltekst: Hjemmeltekst, + val autoUnderskrift: AutoUnderskrift, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/AutovedtakNyf\303\270dtF\303\270rsteBarn.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/AutovedtakNyf\303\270dtF\303\270rsteBarn.kt" new file mode 100644 index 000000000..efd77abb8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/AutovedtakNyf\303\270dtF\303\270rsteBarn.kt" @@ -0,0 +1,43 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode + +data class AutovedtakNyfødtFørsteBarn( + override val mal: Brevmal = Brevmal.AUTOVEDTAK_NYFØDT_FØRSTE_BARN, + override val data: AutovedtakNyfødtFørsteBarnData, +) : Vedtaksbrev { + + constructor( + vedtakFellesfelter: VedtakFellesfelter, + etterbetaling: Etterbetaling?, + ) : + this( + data = AutovedtakNyfødtFørsteBarnData( + delmalData = AutovedtakNyfødtFørsteBarnData.Delmaler( + etterbetaling = etterbetaling, + hjemmeltekst = vedtakFellesfelter.hjemmeltekst, + autoUnderskrift = AutoUnderskrift( + vedtakFellesfelter.enhet, + ), + ), + flettefelter = FlettefelterForDokumentImpl( + navn = vedtakFellesfelter.søkerNavn, + fodselsnummer = vedtakFellesfelter.søkerFødselsnummer, + ), + perioder = vedtakFellesfelter.perioder, + ), + ) +} + +data class AutovedtakNyfødtFørsteBarnData( + override val delmalData: Delmaler, + override val flettefelter: FlettefelterForDokumentImpl, + override val perioder: List, +) : VedtaksbrevData { + + data class Delmaler( + val etterbetaling: Etterbetaling?, + val hjemmeltekst: Hjemmeltekst, + val autoUnderskrift: AutoUnderskrift, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Avslag.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Avslag.kt new file mode 100644 index 000000000..822281e50 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Avslag.kt @@ -0,0 +1,48 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode + +data class Avslag( + override val mal: Brevmal, + override val data: AvslagData, +) : Vedtaksbrev { + + constructor( + mal: Brevmal = Brevmal.VEDTAK_AVSLAG, + vedtakFellesfelter: VedtakFellesfelter, + ) : + this( + mal = mal, + data = AvslagData( + delmalData = AvslagData.Delmaler( + signaturVedtak = SignaturVedtak( + enhet = vedtakFellesfelter.enhet, + saksbehandler = vedtakFellesfelter.saksbehandler, + beslutter = vedtakFellesfelter.beslutter, + ), + hjemmeltekst = vedtakFellesfelter.hjemmeltekst, + korrigertVedtak = vedtakFellesfelter.korrigertVedtakData, + ), + flettefelter = FlettefelterForDokumentImpl( + navn = vedtakFellesfelter.søkerNavn, + fodselsnummer = vedtakFellesfelter.søkerFødselsnummer, + organisasjonsnummer = vedtakFellesfelter.organisasjonsnummer, + gjelder = vedtakFellesfelter.gjelder, + ), + perioder = vedtakFellesfelter.perioder, + ), + ) +} + +data class AvslagData( + override val delmalData: Delmaler, + override val flettefelter: FlettefelterForDokumentImpl, + override val perioder: List, +) : VedtaksbrevData { + + data class Delmaler( + val signaturVedtak: SignaturVedtak, + val hjemmeltekst: Hjemmeltekst, + val korrigertVedtak: KorrigertVedtakData?, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Brev.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Brev.kt new file mode 100644 index 000000000..e5ef08e4a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Brev.kt @@ -0,0 +1,415 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentÅrsak +import no.nav.familie.kontrakter.felles.dokarkiv.Dokumenttype +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstype +import no.nav.familie.kontrakter.felles.objectMapper +import java.time.LocalDate + +interface Brev { + + val mal: Brevmal + val data: BrevData +} + +/*** + * Se https://github.com/navikt/familie/blob/master/doc/ba-sak/legg-til-nytt-brev.md + * for detaljer om alt som skal inn når du legger til en ny brevmal. + ***/ +enum class Brevmal(val erVedtaksbrev: Boolean, val apiNavn: String, val visningsTekst: String) { + INFORMASJONSBREV_DELT_BOSTED(false, "informasjonsbrevDeltBosted", "Informasjonsbrev delt bosted"), + INNHENTE_OPPLYSNINGER(false, "innhenteOpplysninger", "Innhente opplysninger"), + INNHENTE_OPPLYSNINGER_ETTER_SØKNAD_I_SED( + false, + "innhenteOpplysningerEtterSoknadISED", + "Innhente opplysninger etter søknad i SED", + ), + INNHENTE_OPPLYSNINGER_OG_INFORMASJON_OM_AT_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_HAR_SØKT( + erVedtaksbrev = false, + apiNavn = "innhentingOgInfoAnnenForelderMedSelvstendigRettSokt", + visningsTekst = "Innhente opplysninger og informasjon om at annen forelder med selvstendig rett har søkt", + ), + INNHENTE_OPPLYSNINGER_INSTITUSJON(false, "innhenteOpplysningerInstitusjon", "Innhente opplysninger institusjon"), + HENLEGGE_TRUKKET_SØKNAD(false, "henleggeTrukketSoknad", "Henlegge trukket søknad"), + VARSEL_OM_REVURDERING(false, "varselOmRevurdering", "Varsel om revurdering"), + VARSEL_OM_REVURDERING_INSTITUSJON(false, "varselOmRevurderingInstitusjon", "Varsel om revurdering institusjon"), + VARSEL_OM_REVURDERING_DELT_BOSTED_PARAGRAF_14( + false, + "varselOmRevurderingDeltBostedParagrafFjorten", + "Varsel om revurdering delt bosted § 14", + ), + VARSEL_OM_REVURDERING_SAMBOER( + false, + "varselOmRevurderingSamboer", + "Varsel om revurdering samboer", + ), + VARSEL_OM_VEDTAK_ETTER_SØKNAD_I_SED( + false, + "varselOmVedtakEtterSoknadISED", + "Varsel om vedtak etter søknad i SED", + ), + VARSEL_OM_REVURDERING_FRA_NASJONAL_TIL_EØS( + false, + "varselOmRevurderingFraNasjonalTilEOS", + "Varsel om revurdering fra nasjonal til EØS", + ), + VARSEL_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_SØKT( + false, + "varselAnnenForelderMedSelvstendigRettSoekt", + "Varsel annen forelder med selvstendig rett søkt", + ), + VARSEL_OM_ÅRLIG_REVURDERING_EØS( + false, + "varselOmAarligRevurderingEos", + "Varsel om årlig revurdering EØS", + ), + VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER( + false, + "varselOmAarligRevurderingEosMedInnhentingAvOpplysninger", + "Varsel om årlig revurdering EØS med innhenting av opplysninger", + ), + + SVARTIDSBREV(false, "svartidsbrev", "Svartidsbrev"), + SVARTIDSBREV_INSTITUSJON(false, "svartidsbrevInstitusjon", "Svartidsbrev institusjon"), + FORLENGET_SVARTIDSBREV(false, "forlengetSvartidsbrev", "Forlenget svartidsbrev"), + FORLENGET_SVARTIDSBREV_INSTITUSJON(false, "forlengetSvartidsbrevInstitusjon", "Forlenget svartidsbrev institusjon"), + INFORMASJONSBREV_FØDSEL_MINDREÅRIG( + false, + "informasjonsbrevFodselMindreaarig", + "Informasjonsbrev fødsel mindreårig", + ), + + INFORMASJONSBREV_FØDSEL_VERGEMÅL(false, "informasjonsbrevFodselVergemaal", "Informasjonsbrev fødsel vergemål"), + INFORMASJONSBREV_KAN_SØKE(false, "informasjonsbrevKanSoke", "Informasjonsbrev kan søke"), + INFORMASJONSBREV_KAN_SØKE_EØS(false, "informasjonsbrevKanSokeEOS", "Informasjonsbrev kan søke EØS"), + INFORMASJONSBREV_FØDSEL_GENERELL(false, "informasjonsbrevFodselGenerell", "Informasjonsbrev fødsel generell"), + + INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_FÅTT_EN_SØKNAD_FRA_ANNEN_FORELDER( + erVedtaksbrev = false, + apiNavn = "tilForelderOmfattetNorskLovgivningHarFaattSoknadFraAnnenForelder", + visningsTekst = "Informasjon til forelder omfattet norsk lovgivning - har fått en søknad fra annen forelder", + ), + + INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_GJORT_VEDTAK_TIL_ANNEN_FORELDER( + erVedtaksbrev = false, + apiNavn = "tilForelderOmfattetNorskLovgivningHarGjortVedtakTilAnnenForelder", + visningsTekst = "Informasjon til forelder omfattet norsk lovgivning - har gjort vedtak til annen forelder", + ), + + INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_VARSEL_OM_ÅRLIG_KONTROLL( + erVedtaksbrev = false, + apiNavn = "tilForelderOmfattetNorskLovgivningVarselOmAarligKontroll", + visningsTekst = "Informasjon til forelder omfattet norsk lovgivning - varsel om årlig kontroll", + ), + + INFORMASJONSBREV_TIL_FORELDER_MED_SELVSTENDIG_RETT_VI_HAR_FÅTT_F016_KAN_SØKE_OM_BARNETRYGD( + erVedtaksbrev = false, + apiNavn = "tilForelderMedSelvstendigRettKanSokeOmBarnetrygd", + visningsTekst = "Informasjon til forelder med selvstendig rett vi har fått F016 - kan søke om barnetrygd", + ), + + VEDTAK_FØRSTEGANGSVEDTAK(true, "forstegangsvedtak", "Førstegangsvedtak"), + VEDTAK_ENDRING(true, "vedtakEndring", "Vedtak endring"), + VEDTAK_OPPHØRT(true, "opphort", "Opphørt"), + VEDTAK_OPPHØR_MED_ENDRING(true, "opphorMedEndring", "Opphør med endring"), + VEDTAK_AVSLAG(true, "vedtakAvslag", "Avslag"), + VEDTAK_FORTSATT_INNVILGET(true, "vedtakFortsattInnvilget", "Vedtak fortstatt innvilget"), + VEDTAK_KORREKSJON_VEDTAKSBREV(true, "korrigertVedtakEgenBrevmal", "Korrigere vedtak med egen brevmal"), + VEDTAK_OPPHØR_DØDSFALL(true, "dodsfall", "Dødsfall"), + VEDTAK_FØRSTEGANGSVEDTAK_INSTITUSJON(true, "foerstegangsvedtakInstitusjon", "Førstegangsvedtak"), + VEDTAK_ENDRING_INSTITUSJON(true, "vedtakEndringInstitusjon", "Vedtak endring"), + VEDTAK_OPPHØRT_INSTITUSJON(true, "vedtakOpphoerInstitusjon", "Opphørt"), + VEDTAK_OPPHØR_MED_ENDRING_INSTITUSJON(true, "opphorMedEndringInstitusjon", "Opphør med endring"), + VEDTAK_AVSLAG_INSTITUSJON(true, "vedtakAvslagInstitusjon", "Avslag"), + VEDTAK_FORTSATT_INNVILGET_INSTITUSJON(true, "vedtakFortsattInnvilgetInstitusjon", "Vedtak fortstatt innvilget"), + + AUTOVEDTAK_BARN_6_OG_18_ÅR_OG_SMÅBARNSTILLEGG( + true, + "autovedtakBarn6AarOg18AarOgSmaabarnstillegg", + "Autovedtak - Barn 6 og 18 år og småbarnstillegg", + ), + AUTOVEDTAK_NYFØDT_FØRSTE_BARN(true, "autovedtakNyfodtForsteBarn", "Autovedtak nyfødt - første barn"), + AUTOVEDTAK_NYFØDT_BARN_FRA_FØR(true, "autovedtakNyfodtBarnFraFor", "Autovedtak nyfødt - barn fra før"), + ; + + fun skalGenerereForside(): Boolean = + when (this) { + INNHENTE_OPPLYSNINGER, + INNHENTE_OPPLYSNINGER_ETTER_SØKNAD_I_SED, + INNHENTE_OPPLYSNINGER_OG_INFORMASJON_OM_AT_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_HAR_SØKT, + INNHENTE_OPPLYSNINGER_INSTITUSJON, + INFORMASJONSBREV_TIL_FORELDER_MED_SELVSTENDIG_RETT_VI_HAR_FÅTT_F016_KAN_SØKE_OM_BARNETRYGD, + INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_FÅTT_EN_SØKNAD_FRA_ANNEN_FORELDER, + INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_GJORT_VEDTAK_TIL_ANNEN_FORELDER, + INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_VARSEL_OM_ÅRLIG_KONTROLL, + VARSEL_OM_REVURDERING, + VARSEL_OM_REVURDERING_INSTITUSJON, + VARSEL_OM_REVURDERING_DELT_BOSTED_PARAGRAF_14, + VARSEL_OM_REVURDERING_SAMBOER, + VARSEL_OM_VEDTAK_ETTER_SØKNAD_I_SED, + VARSEL_OM_REVURDERING_FRA_NASJONAL_TIL_EØS, + VARSEL_OM_ÅRLIG_REVURDERING_EØS, + VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER, + VARSEL_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_SØKT, + -> true + + INFORMASJONSBREV_DELT_BOSTED, + HENLEGGE_TRUKKET_SØKNAD, + SVARTIDSBREV, + SVARTIDSBREV_INSTITUSJON, + FORLENGET_SVARTIDSBREV, + FORLENGET_SVARTIDSBREV_INSTITUSJON, + INFORMASJONSBREV_FØDSEL_VERGEMÅL, + INFORMASJONSBREV_FØDSEL_MINDREÅRIG, + INFORMASJONSBREV_KAN_SØKE, + INFORMASJONSBREV_FØDSEL_GENERELL, + INFORMASJONSBREV_KAN_SØKE_EØS, + -> false + + VEDTAK_FØRSTEGANGSVEDTAK, + VEDTAK_ENDRING, + VEDTAK_OPPHØRT, + VEDTAK_OPPHØR_MED_ENDRING, + VEDTAK_AVSLAG, + VEDTAK_FORTSATT_INNVILGET, + VEDTAK_KORREKSJON_VEDTAKSBREV, + VEDTAK_OPPHØR_DØDSFALL, + VEDTAK_FØRSTEGANGSVEDTAK_INSTITUSJON, + VEDTAK_AVSLAG_INSTITUSJON, + VEDTAK_OPPHØRT_INSTITUSJON, + VEDTAK_ENDRING_INSTITUSJON, + VEDTAK_FORTSATT_INNVILGET_INSTITUSJON, + VEDTAK_OPPHØR_MED_ENDRING_INSTITUSJON, + AUTOVEDTAK_BARN_6_OG_18_ÅR_OG_SMÅBARNSTILLEGG, + AUTOVEDTAK_NYFØDT_FØRSTE_BARN, + AUTOVEDTAK_NYFØDT_BARN_FRA_FØR, + -> throw Feil("Ikke avgjort om $this skal generere forside") + } + + fun tilFamilieKontrakterDokumentType(): Dokumenttype = + when (this) { + INNHENTE_OPPLYSNINGER -> Dokumenttype.BARNETRYGD_INNHENTE_OPPLYSNINGER + VARSEL_OM_REVURDERING -> Dokumenttype.BARNETRYGD_VARSEL_OM_REVURDERING + VARSEL_OM_REVURDERING_INSTITUSJON -> Dokumenttype.BARNETRYGD_VARSEL_OM_REVURDERING_INSTITUSJON + VARSEL_OM_REVURDERING_DELT_BOSTED_PARAGRAF_14 -> Dokumenttype.BARNETRYGD_VARSEL_OM_REVURDERING_DELT_BOSTED_PARAGRAF_14 + VARSEL_OM_REVURDERING_SAMBOER -> Dokumenttype.BARNETRYGD_VARSEL_OM_REVURDERING_SAMBOER + INFORMASJONSBREV_DELT_BOSTED -> Dokumenttype.BARNETRYGD_INFORMASJONSBREV_DELT_BOSTED + HENLEGGE_TRUKKET_SØKNAD -> Dokumenttype.BARNETRYGD_HENLEGGE_TRUKKET_SØKNAD + SVARTIDSBREV -> Dokumenttype.BARNETRYGD_SVARTIDSBREV + FORLENGET_SVARTIDSBREV -> Dokumenttype.BARNETRYGD_FORLENGET_SVARTIDSBREV + INFORMASJONSBREV_FØDSEL_VERGEMÅL -> Dokumenttype.BARNETRYGD_INFORMASJONSBREV_FØDSEL_VERGEMÅL + INFORMASJONSBREV_FØDSEL_MINDREÅRIG -> Dokumenttype.BARNETRYGD_INFORMASJONSBREV_FØDSEL_MINDREÅRIG + INFORMASJONSBREV_KAN_SØKE -> Dokumenttype.BARNETRYGD_INFORMASJONSBREV_KAN_SØKE + INFORMASJONSBREV_FØDSEL_GENERELL -> Dokumenttype.BARNETRYGD_INFORMASJONSBREV_FØDSEL_GENERELL + INFORMASJONSBREV_TIL_FORELDER_MED_SELVSTENDIG_RETT_VI_HAR_FÅTT_F016_KAN_SØKE_OM_BARNETRYGD -> Dokumenttype.BARNETRYGD_INFORMASJONSBREV_TIL_FORELDER_MED_SELVSTENDIG_RETT_VI_HAR_FÅTT_F016_KAN_SØKE_OM_BARNETRYGD + INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_FÅTT_EN_SØKNAD_FRA_ANNEN_FORELDER -> Dokumenttype.BARNETRYGD_INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_FÅTT_EN_SØKNAD_FRA_ANNEN_FORELDER + INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_GJORT_VEDTAK_TIL_ANNEN_FORELDER -> Dokumenttype.BARNETRYGD_INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_GJORT_VEDTAK_TIL_ANNEN_FORELDER + INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_VARSEL_OM_ÅRLIG_KONTROLL -> Dokumenttype.BARNETRYGD_INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_VARSEL_OM_ÅRLIG_KONTROLL + INNHENTE_OPPLYSNINGER_ETTER_SØKNAD_I_SED -> Dokumenttype.BARNETRYGD_INNHENTE_OPPLYSNINGER_ETTER_SØKNAD_I_SED + INNHENTE_OPPLYSNINGER_OG_INFORMASJON_OM_AT_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_HAR_SØKT -> Dokumenttype.BARNETRYGD_INNHENTE_OPPLYSNINGER_OG_INFORMASJON_OM_AT_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_HAR_SØKT + VARSEL_OM_VEDTAK_ETTER_SØKNAD_I_SED -> Dokumenttype.BARNETRYGD_VARSEL_OM_VEDTAK_ETTER_SØKNAD_I_SED + VARSEL_OM_REVURDERING_FRA_NASJONAL_TIL_EØS -> Dokumenttype.BARNETRYGD_VARSEL_OM_REVURDERING_FRA_NASJONAL_TIL_EØS + VARSEL_OM_ÅRLIG_REVURDERING_EØS -> Dokumenttype.BARNETRYGD_VARSEL_OM_ÅRLIG_REVURDERING_EØS + VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER -> Dokumenttype.BARNETRYGD_VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER + INFORMASJONSBREV_KAN_SØKE_EØS -> Dokumenttype.BARNETRYGD_INFORMASJONSBREV_KAN_SØKE_EØS + INNHENTE_OPPLYSNINGER_INSTITUSJON -> Dokumenttype.BARNETRYGD_INNHENTE_OPPLYSNINGER_INSTITUSJON + SVARTIDSBREV_INSTITUSJON -> Dokumenttype.BARNETRYGD_SVARTIDSBREV_INSTITUSJON + FORLENGET_SVARTIDSBREV_INSTITUSJON -> Dokumenttype.BARNETRYGD_FORLENGET_SVARTIDSBREV_INSTITUSJON + VARSEL_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_SØKT -> Dokumenttype.BARNETRYGD_VARSEL_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_SØKT + + VEDTAK_ENDRING, + VEDTAK_OPPHØRT, + VEDTAK_OPPHØR_MED_ENDRING, + VEDTAK_FORTSATT_INNVILGET, + VEDTAK_AVSLAG, + VEDTAK_FØRSTEGANGSVEDTAK, + VEDTAK_KORREKSJON_VEDTAKSBREV, + VEDTAK_OPPHØR_DØDSFALL, + VEDTAK_FØRSTEGANGSVEDTAK_INSTITUSJON, + VEDTAK_AVSLAG_INSTITUSJON, + VEDTAK_OPPHØRT_INSTITUSJON, + VEDTAK_ENDRING_INSTITUSJON, + VEDTAK_FORTSATT_INNVILGET_INSTITUSJON, + VEDTAK_OPPHØR_MED_ENDRING_INSTITUSJON, + AUTOVEDTAK_BARN_6_OG_18_ÅR_OG_SMÅBARNSTILLEGG, + AUTOVEDTAK_NYFØDT_FØRSTE_BARN, + AUTOVEDTAK_NYFØDT_BARN_FRA_FØR, + -> throw Feil("Ingen dokumenttype for $this") + } + + val distribusjonstype: Distribusjonstype + get() = when (this) { + INFORMASJONSBREV_DELT_BOSTED -> Distribusjonstype.VIKTIG + INNHENTE_OPPLYSNINGER, INNHENTE_OPPLYSNINGER_INSTITUSJON -> Distribusjonstype.VIKTIG + INNHENTE_OPPLYSNINGER_ETTER_SØKNAD_I_SED -> Distribusjonstype.VIKTIG + INNHENTE_OPPLYSNINGER_OG_INFORMASJON_OM_AT_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_HAR_SØKT -> Distribusjonstype.VIKTIG + HENLEGGE_TRUKKET_SØKNAD -> Distribusjonstype.ANNET + VARSEL_OM_REVURDERING -> Distribusjonstype.VIKTIG + VARSEL_OM_REVURDERING_INSTITUSJON -> Distribusjonstype.VIKTIG + VARSEL_OM_REVURDERING_DELT_BOSTED_PARAGRAF_14 -> Distribusjonstype.VIKTIG + VARSEL_OM_REVURDERING_SAMBOER -> Distribusjonstype.ANNET + VARSEL_OM_VEDTAK_ETTER_SØKNAD_I_SED -> Distribusjonstype.VIKTIG + VARSEL_OM_REVURDERING_FRA_NASJONAL_TIL_EØS -> Distribusjonstype.VIKTIG + VARSEL_OM_ÅRLIG_REVURDERING_EØS -> Distribusjonstype.VIKTIG + VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER -> Distribusjonstype.VIKTIG + VARSEL_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_SØKT -> Distribusjonstype.VIKTIG + SVARTIDSBREV, SVARTIDSBREV_INSTITUSJON -> Distribusjonstype.ANNET + FORLENGET_SVARTIDSBREV, FORLENGET_SVARTIDSBREV_INSTITUSJON -> Distribusjonstype.ANNET + INFORMASJONSBREV_FØDSEL_MINDREÅRIG -> Distribusjonstype.ANNET + INFORMASJONSBREV_FØDSEL_VERGEMÅL -> Distribusjonstype.ANNET + INFORMASJONSBREV_KAN_SØKE -> Distribusjonstype.ANNET + INFORMASJONSBREV_KAN_SØKE_EØS -> Distribusjonstype.ANNET + INFORMASJONSBREV_FØDSEL_GENERELL -> Distribusjonstype.ANNET + INFORMASJONSBREV_TIL_FORELDER_MED_SELVSTENDIG_RETT_VI_HAR_FÅTT_F016_KAN_SØKE_OM_BARNETRYGD -> Distribusjonstype.VIKTIG + INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_FÅTT_EN_SØKNAD_FRA_ANNEN_FORELDER -> Distribusjonstype.VIKTIG + INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_GJORT_VEDTAK_TIL_ANNEN_FORELDER -> Distribusjonstype.VIKTIG + INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_VARSEL_OM_ÅRLIG_KONTROLL -> Distribusjonstype.VIKTIG + VEDTAK_FØRSTEGANGSVEDTAK -> Distribusjonstype.VEDTAK + VEDTAK_ENDRING -> Distribusjonstype.VEDTAK + VEDTAK_OPPHØRT -> Distribusjonstype.VEDTAK + VEDTAK_OPPHØR_MED_ENDRING -> Distribusjonstype.VEDTAK + VEDTAK_AVSLAG -> Distribusjonstype.VEDTAK + VEDTAK_FORTSATT_INNVILGET -> Distribusjonstype.VEDTAK + VEDTAK_KORREKSJON_VEDTAKSBREV -> Distribusjonstype.VEDTAK + VEDTAK_OPPHØR_DØDSFALL -> Distribusjonstype.VEDTAK + VEDTAK_FØRSTEGANGSVEDTAK_INSTITUSJON -> Distribusjonstype.VEDTAK + VEDTAK_AVSLAG_INSTITUSJON -> Distribusjonstype.VEDTAK + VEDTAK_OPPHØRT_INSTITUSJON -> Distribusjonstype.VEDTAK + VEDTAK_ENDRING_INSTITUSJON -> Distribusjonstype.VEDTAK + VEDTAK_FORTSATT_INNVILGET_INSTITUSJON -> Distribusjonstype.VEDTAK + VEDTAK_OPPHØR_MED_ENDRING_INSTITUSJON -> Distribusjonstype.VEDTAK + AUTOVEDTAK_BARN_6_OG_18_ÅR_OG_SMÅBARNSTILLEGG -> Distribusjonstype.VEDTAK + AUTOVEDTAK_NYFØDT_FØRSTE_BARN -> Distribusjonstype.VEDTAK + AUTOVEDTAK_NYFØDT_BARN_FRA_FØR -> Distribusjonstype.VEDTAK + } + + fun førerTilOpplysningsplikt(): Boolean = + when (this) { + INNHENTE_OPPLYSNINGER, + INNHENTE_OPPLYSNINGER_INSTITUSJON, + VARSEL_OM_REVURDERING, + VARSEL_OM_REVURDERING_INSTITUSJON, + -> true + + else -> false + } + + fun setterBehandlingPåVent(): Boolean = + when (this) { + FORLENGET_SVARTIDSBREV, + INNHENTE_OPPLYSNINGER, + INNHENTE_OPPLYSNINGER_INSTITUSJON, + VARSEL_OM_REVURDERING, + VARSEL_OM_REVURDERING_INSTITUSJON, + VARSEL_OM_REVURDERING_DELT_BOSTED_PARAGRAF_14, + INNHENTE_OPPLYSNINGER_ETTER_SØKNAD_I_SED, + VARSEL_OM_REVURDERING_FRA_NASJONAL_TIL_EØS, + VARSEL_OM_VEDTAK_ETTER_SØKNAD_I_SED, + SVARTIDSBREV, + SVARTIDSBREV_INSTITUSJON, + FORLENGET_SVARTIDSBREV_INSTITUSJON, + VARSEL_OM_ÅRLIG_REVURDERING_EØS, + VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER, + -> true + + else -> false + } + + fun ventefristDager(manuellFrist: Long? = null, behandlingKategori: BehandlingKategori?): Long = + when (this) { + INNHENTE_OPPLYSNINGER, + INNHENTE_OPPLYSNINGER_INSTITUSJON, + VARSEL_OM_REVURDERING, + VARSEL_OM_REVURDERING_INSTITUSJON, + VARSEL_OM_REVURDERING_DELT_BOSTED_PARAGRAF_14, + INNHENTE_OPPLYSNINGER_ETTER_SØKNAD_I_SED, + VARSEL_OM_REVURDERING_FRA_NASJONAL_TIL_EØS, + VARSEL_OM_VEDTAK_ETTER_SØKNAD_I_SED, + -> 3 * 7 + + SVARTIDSBREV -> when (behandlingKategori) { + BehandlingKategori.EØS -> 30 * 3 + BehandlingKategori.NASJONAL -> 3 * 7 + else -> throw Feil("Behandlingskategori er ikke satt fot $this") + } + + SVARTIDSBREV_INSTITUSJON -> 3 * 7 + FORLENGET_SVARTIDSBREV, FORLENGET_SVARTIDSBREV_INSTITUSJON -> + manuellFrist + ?: throw Feil("Ventefrist var ikke satt for $this") + + VARSEL_OM_ÅRLIG_REVURDERING_EØS, + VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER, + -> 30 * 2 + + else -> throw Feil("Ventefrist ikke definert for brevtype $this") + } + + fun venteårsak() = + when (this) { + FORLENGET_SVARTIDSBREV, + INNHENTE_OPPLYSNINGER, + INNHENTE_OPPLYSNINGER_INSTITUSJON, + VARSEL_OM_REVURDERING, + VARSEL_OM_REVURDERING_INSTITUSJON, + VARSEL_OM_REVURDERING_DELT_BOSTED_PARAGRAF_14, + INNHENTE_OPPLYSNINGER_ETTER_SØKNAD_I_SED, + VARSEL_OM_REVURDERING_FRA_NASJONAL_TIL_EØS, + VARSEL_OM_VEDTAK_ETTER_SØKNAD_I_SED, + SVARTIDSBREV, + SVARTIDSBREV_INSTITUSJON, + FORLENGET_SVARTIDSBREV_INSTITUSJON, + VARSEL_OM_ÅRLIG_REVURDERING_EØS, + VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER, + -> SettPåVentÅrsak.AVVENTER_DOKUMENTASJON + + else -> throw Feil("Venteårsak ikke definert for brevtype $this") + } +} + +interface BrevData { + + val delmalData: Any + val flettefelter: FlettefelterForDokument + fun toBrevString(): String = objectMapper.writeValueAsString(this) +} + +interface FlettefelterForDokument { + + val navn: Flettefelt + val fodselsnummer: Flettefelt + val brevOpprettetDato: Flettefelt + val organisasjonsnummer: Flettefelt + get() = null + val gjelder: Flettefelt + get() = null +} + +data class FlettefelterForDokumentImpl( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + override val organisasjonsnummer: Flettefelt, + override val gjelder: Flettefelt, +) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + organisasjonsnummer: String? = null, + gjelder: String? = null, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + organisasjonsnummer = flettefelt(organisasjonsnummer), + gjelder = flettefelt(gjelder), + ) +} + +typealias Flettefelt = List? + +fun flettefelt(flettefeltData: String?): Flettefelt = if (flettefeltData != null) listOf(flettefeltData) else null +fun flettefelt(flettefeltData: List): Flettefelt = flettefeltData diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/D\303\270dsfall.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/D\303\270dsfall.kt" new file mode 100644 index 000000000..aa2b0194e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/D\303\270dsfall.kt" @@ -0,0 +1,40 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class Dødsfall( + override val mal: Brevmal = Brevmal.VEDTAK_OPPHØR_DØDSFALL, + override val data: DødsfallData, +) : Brev + +data class DødsfallData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + val virkningstidspunkt: Flettefelt, + val navnAvdode: Flettefelt, + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + virkningstidspunkt: String, + navnAvdode: String, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + virkningstidspunkt = flettefelt(virkningstidspunkt), + navnAvdode = flettefelt(navnAvdode), + ) + } + + data class DelmalData( + val signaturVedtak: SignaturVedtak, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/EnkeltInformasjonsbrev.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/EnkeltInformasjonsbrev.kt new file mode 100644 index 000000000..3b7332693 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/EnkeltInformasjonsbrev.kt @@ -0,0 +1,57 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class EnkeltInformasjonsbrev( + override val mal: Brevmal, + override val data: EnkeltInformasjonsbrevData, +) : Brev { + + constructor( + navn: String, + fodselsnummer: String, + enhet: String, + mal: Brevmal, + saksbehandlerNavn: String, + ) : this( + mal = mal, + data = EnkeltInformasjonsbrevData( + flettefelter = EnkeltInformasjonsbrevData.Flettefelter( + navn = navn, + fodselsnummer = fodselsnummer, + ), + delmalData = EnkeltInformasjonsbrevData.DelmalData( + SignaturDelmal( + enhet = enhet, + saksbehandlerNavn = saksbehandlerNavn, + ), + ), + ), + ) +} + +data class EnkeltInformasjonsbrevData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + ) + } + + data class DelmalData( + val signatur: SignaturDelmal, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/FellesDelmaler.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/FellesDelmaler.kt new file mode 100644 index 000000000..404c81727 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/FellesDelmaler.kt @@ -0,0 +1,95 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +data class SignaturDelmal( + val enhet: Flettefelt, + val saksbehandler: Flettefelt, +) { + + constructor(enhet: String, saksbehandlerNavn: String) : this( + enhet = flettefelt(enhet), + saksbehandler = flettefelt(saksbehandlerNavn), + ) +} + +data class SignaturVedtak( + val enhet: Flettefelt, + val saksbehandler: Flettefelt, + val beslutter: Flettefelt, +) { + + constructor(enhet: String, saksbehandler: String, beslutter: String) : this( + flettefelt(enhet), + flettefelt(saksbehandler), + flettefelt(beslutter), + ) +} + +data class Etterbetaling( + val etterbetalingsbelop: Flettefelt, +) { + + constructor(etterbetalingsbeløp: String) : this( + flettefelt(etterbetalingsbeløp), + ) +} + +data class EtterbetalingInstitusjon( + val etterbetalingsbelop: Flettefelt, +) { + + constructor(etterbetalingsbeløp: String) : this( + flettefelt(etterbetalingsbeløp), + ) +} + +data class Hjemmeltekst( + val hjemler: Flettefelt, +) { + + constructor(hjemler: String) : this( + flettefelt(hjemler), + ) +} + +data class AutoUnderskrift( + val enhet: Flettefelt, +) { + + constructor(enhet: String) : this( + flettefelt(enhet), + ) +} + +data class KorrigertVedtakData( + val datoKorrigertVedtak: Flettefelt, +) { + + constructor(datoKorrigertVedtak: String) : this( + flettefelt(datoKorrigertVedtak), + ) +} + +data class FeilutbetaltValuta( + val perioderMedForMyeUtbetalt: Flettefelt, +) { + + constructor(perioderMedForMyeUtbetalt: Set) : this( + flettefelt(perioderMedForMyeUtbetalt.toList()), + ) +} + +data class RefusjonEøsAvklart( + val perioderMedRefusjonEosAvklart: Flettefelt, +) { + constructor(perioderMedRefusjonEøsAvklart: Set) : this( + flettefelt(perioderMedRefusjonEøsAvklart.toList()), + ) +} + +data class RefusjonEøsUavklart( + val perioderMedRefusjonEosUavklart: Flettefelt, +) { + constructor(perioderMedRefusjonEøsUavklart: Set) : this( + flettefelt(perioderMedRefusjonEøsUavklart.toList()), + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/ForlengetSvartidsbrev.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/ForlengetSvartidsbrev.kt new file mode 100644 index 000000000..f1f643478 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/ForlengetSvartidsbrev.kt @@ -0,0 +1,58 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class ForlengetSvartidsbrev( + override val mal: Brevmal, + override val data: ForlengetSvartidsbrevData, +) : Brev { + constructor( + navn: String, + fodselsnummer: String, + enhetNavn: String, + mal: Brevmal, + årsaker: List, + antallUkerSvarfrist: Int, + organisasjonsnummer: String? = null, + gjelder: String? = null, + saksbehandlerNavn: String, + ) : this( + mal = mal, + data = ForlengetSvartidsbrevData( + delmalData = ForlengetSvartidsbrevData.DelmalData( + signatur = SignaturDelmal( + enhet = enhetNavn, + saksbehandlerNavn = saksbehandlerNavn, + ), + ), + flettefelter = ForlengetSvartidsbrevData.Flettefelter( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + antallUkerSvarfrist = flettefelt(antallUkerSvarfrist.toString()), + aarsakerSvartidsbrev = flettefelt(årsaker), + organisasjonsnummer = flettefelt(organisasjonsnummer), + gjelder = flettefelt(gjelder), + ), + ), + ) +} + +data class ForlengetSvartidsbrevData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + val antallUkerSvarfrist: Flettefelt, + val aarsakerSvartidsbrev: Flettefelt, + override val organisasjonsnummer: Flettefelt, + override val gjelder: Flettefelt, + ) : FlettefelterForDokument + + data class DelmalData( + val signatur: SignaturDelmal, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/ForsattInnvilget.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/ForsattInnvilget.kt new file mode 100644 index 000000000..89d7f0fe3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/ForsattInnvilget.kt @@ -0,0 +1,69 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode + +data class ForsattInnvilget( + override val mal: Brevmal, + override val data: ForsattInnvilgetData, +) : Vedtaksbrev { + + constructor( + mal: Brevmal = Brevmal.VEDTAK_FORTSATT_INNVILGET, + vedtakFellesfelter: VedtakFellesfelter, + etterbetaling: Etterbetaling? = null, + etterbetalingInstitusjon: EtterbetalingInstitusjon? = null, + informasjonOmAarligKontroll: Boolean = false, + refusjonEosAvklart: RefusjonEøsAvklart? = null, + refusjonEosUavklart: RefusjonEøsUavklart? = null, + duMåMeldeFraOmEndringer: Boolean = true, + duMåMeldeFraOmEndringerEøsSelvstendigRett: Boolean = false, + ) : + this( + mal = mal, + data = ForsattInnvilgetData( + delmalData = ForsattInnvilgetData.Delmaler( + signaturVedtak = SignaturVedtak( + enhet = vedtakFellesfelter.enhet, + saksbehandler = vedtakFellesfelter.saksbehandler, + beslutter = vedtakFellesfelter.beslutter, + ), + hjemmeltekst = vedtakFellesfelter.hjemmeltekst, + etterbetaling = etterbetaling, + etterbetalingInstitusjon = etterbetalingInstitusjon, + korrigertVedtak = vedtakFellesfelter.korrigertVedtakData, + informasjonOmAarligKontroll = informasjonOmAarligKontroll, + refusjonEosAvklart = refusjonEosAvklart, + refusjonEosUavklart = refusjonEosUavklart, + duMaaMeldeFraOmEndringer = duMåMeldeFraOmEndringer, + duMaaMeldeFraOmEndringerEosSelvstendigRett = duMåMeldeFraOmEndringerEøsSelvstendigRett, + ), + flettefelter = FlettefelterForDokumentImpl( + gjelder = flettefelt(vedtakFellesfelter.gjelder), + navn = flettefelt(vedtakFellesfelter.søkerNavn), + fodselsnummer = flettefelt(vedtakFellesfelter.søkerFødselsnummer), + organisasjonsnummer = flettefelt(vedtakFellesfelter.organisasjonsnummer), + ), + perioder = vedtakFellesfelter.perioder, + ), + ) +} + +data class ForsattInnvilgetData( + override val delmalData: Delmaler, + override val flettefelter: FlettefelterForDokument, + override val perioder: List, +) : VedtaksbrevData { + + data class Delmaler( + val signaturVedtak: SignaturVedtak, + val hjemmeltekst: Hjemmeltekst, + val etterbetaling: Etterbetaling?, + val etterbetalingInstitusjon: EtterbetalingInstitusjon?, + val korrigertVedtak: KorrigertVedtakData?, + val informasjonOmAarligKontroll: Boolean, + val refusjonEosAvklart: RefusjonEøsAvklart?, + val refusjonEosUavklart: RefusjonEøsUavklart?, + val duMaaMeldeFraOmEndringerEosSelvstendigRett: Boolean, + val duMaaMeldeFraOmEndringer: Boolean, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/F\303\270rstegangsvedtak.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/F\303\270rstegangsvedtak.kt" new file mode 100644 index 000000000..33afbb853 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/F\303\270rstegangsvedtak.kt" @@ -0,0 +1,69 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode + +data class Førstegangsvedtak( + override val mal: Brevmal, + override val data: FørstegangsvedtakData, +) : Vedtaksbrev { + + constructor( + mal: Brevmal = Brevmal.VEDTAK_FØRSTEGANGSVEDTAK, + vedtakFellesfelter: VedtakFellesfelter, + etterbetaling: Etterbetaling? = null, + etterbetalingInstitusjon: EtterbetalingInstitusjon? = null, + informasjonOmAarligKontroll: Boolean = false, + refusjonEosAvklart: RefusjonEøsAvklart? = null, + refusjonEosUavklart: RefusjonEøsUavklart? = null, + duMåMeldeFraOmEndringer: Boolean = true, + duMåMeldeFraOmEndringerEøsSelvstendigRett: Boolean = false, + ) : + this( + mal = mal, + data = FørstegangsvedtakData( + delmalData = FørstegangsvedtakData.Delmaler( + signaturVedtak = SignaturVedtak( + enhet = vedtakFellesfelter.enhet, + saksbehandler = vedtakFellesfelter.saksbehandler, + beslutter = vedtakFellesfelter.beslutter, + ), + etterbetaling = etterbetaling, + hjemmeltekst = vedtakFellesfelter.hjemmeltekst, + etterbetalingInstitusjon = etterbetalingInstitusjon, + korrigertVedtak = vedtakFellesfelter.korrigertVedtakData, + informasjonOmAarligKontroll = informasjonOmAarligKontroll, + refusjonEosAvklart = refusjonEosAvklart, + refusjonEosUavklart = refusjonEosUavklart, + duMaaMeldeFraOmEndringerEosSelvstendigRett = duMåMeldeFraOmEndringerEøsSelvstendigRett, + duMaaMeldeFraOmEndringer = duMåMeldeFraOmEndringer, + ), + perioder = vedtakFellesfelter.perioder, + flettefelter = FlettefelterForDokumentImpl( + gjelder = flettefelt(vedtakFellesfelter.gjelder), + navn = flettefelt(vedtakFellesfelter.søkerNavn), + fodselsnummer = flettefelt(vedtakFellesfelter.søkerFødselsnummer), + organisasjonsnummer = flettefelt(vedtakFellesfelter.organisasjonsnummer), + ), + ), + ) +} + +data class FørstegangsvedtakData( + override val delmalData: Delmaler, + override val flettefelter: FlettefelterForDokument, + override val perioder: List, +) : VedtaksbrevData { + + data class Delmaler( + val signaturVedtak: SignaturVedtak, + val etterbetaling: Etterbetaling?, + val hjemmeltekst: Hjemmeltekst, + val etterbetalingInstitusjon: EtterbetalingInstitusjon?, + val korrigertVedtak: KorrigertVedtakData?, + val informasjonOmAarligKontroll: Boolean, + val refusjonEosAvklart: RefusjonEøsAvklart?, + val refusjonEosUavklart: RefusjonEøsUavklart?, + val duMaaMeldeFraOmEndringerEosSelvstendigRett: Boolean, + val duMaaMeldeFraOmEndringer: Boolean, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/HenleggeTrukketS\303\270knadBrev.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/HenleggeTrukketS\303\270knadBrev.kt" new file mode 100644 index 000000000..26b7b9275 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/HenleggeTrukketS\303\270knadBrev.kt" @@ -0,0 +1,16 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +data class HenleggeTrukketSøknadBrev( + override val mal: Brevmal = Brevmal.HENLEGGE_TRUKKET_SØKNAD, + override val data: HenleggeTrukketSøknadData, +) : Brev + +data class HenleggeTrukketSøknadData( + override val delmalData: DelmalData, + override val flettefelter: FlettefelterForDokumentImpl, +) : BrevData { + + data class DelmalData( + val signatur: SignaturDelmal, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InformasjonsbrevDeltBosted.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InformasjonsbrevDeltBosted.kt new file mode 100644 index 000000000..f43b3cce9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InformasjonsbrevDeltBosted.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class InformasjonsbrevDeltBostedBrev( + override val mal: Brevmal = Brevmal.INFORMASJONSBREV_DELT_BOSTED, + override val data: InformasjonsbrevDeltBostedData, +) : Brev + +data class InformasjonsbrevDeltBostedData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + val barnMedDeltBostedAvtale: Flettefelt, + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + barnMedDeltBostedAvtale: List, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + barnMedDeltBostedAvtale = flettefelt(barnMedDeltBostedAvtale), + ) + } + + data class DelmalData( + val signatur: SignaturDelmal, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InformasjonsbrevKanS\303\270ke.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InformasjonsbrevKanS\303\270ke.kt" new file mode 100644 index 000000000..3f68bd7de --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InformasjonsbrevKanS\303\270ke.kt" @@ -0,0 +1,59 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class InformasjonsbrevKanSøke( + override val mal: Brevmal = Brevmal.INFORMASJONSBREV_KAN_SØKE, + override val data: InformasjonsbrevKanSøkeData, +) : Brev { + constructor( + navn: String, + fodselsnummer: String, + dokumentliste: List, + enhet: String, + saksbehandlerNavn: String, + ) : this( + data = InformasjonsbrevKanSøkeData( + delmalData = InformasjonsbrevKanSøkeData.DelmalData( + signatur = SignaturDelmal( + enhet = enhet, + saksbehandlerNavn = saksbehandlerNavn, + ), + ), + flettefelter = InformasjonsbrevKanSøkeData.Flettefelter( + navn = navn, + fodselsnummer = fodselsnummer, + dokumentliste = dokumentliste, + ), + ), + ) +} + +data class InformasjonsbrevKanSøkeData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + val dokumentliste: Flettefelt, + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + dokumentliste: List, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + dokumentliste = flettefelt(dokumentliste), + ) + } + + data class DelmalData( + val signatur: SignaturDelmal, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InformasjonsbrevTilForelder.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InformasjonsbrevTilForelder.kt new file mode 100644 index 000000000..f182cab06 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InformasjonsbrevTilForelder.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class InformasjonsbrevTilForelderBrev( + override val mal: Brevmal, + override val data: InformasjonsbrevTilForelderData, +) : Brev + +data class InformasjonsbrevTilForelderData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + val barnSoktFor: Flettefelt, + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + barnSøktFor: List, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + barnSoktFor = flettefelt(barnSøktFor), + ) + } + + data class DelmalData( + val signatur: SignaturDelmal, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InnhenteOpplysningerBrev.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InnhenteOpplysningerBrev.kt new file mode 100644 index 000000000..72aed1a3a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InnhenteOpplysningerBrev.kt @@ -0,0 +1,43 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class InnhenteOpplysningerBrev( + override val mal: Brevmal = Brevmal.INNHENTE_OPPLYSNINGER, + override val data: InnhenteOpplysningerData, +) : Brev + +data class InnhenteOpplysningerData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + override val organisasjonsnummer: Flettefelt, + override val gjelder: Flettefelt, + val dokumentliste: Flettefelt, + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + dokumentliste: List, + organisasjonsnummer: String? = null, + gjelder: String? = null, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + dokumentliste = flettefelt(dokumentliste), + organisasjonsnummer = flettefelt(organisasjonsnummer), + gjelder = flettefelt(gjelder), + ) + } + + data class DelmalData( + val signatur: SignaturDelmal, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InnhenteOpplysningerOmBarn.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InnhenteOpplysningerOmBarn.kt new file mode 100644 index 000000000..65ed9bef8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/InnhenteOpplysningerOmBarn.kt @@ -0,0 +1,66 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class InnhenteOpplysningerOmBarn( + override val mal: Brevmal, + override val data: InnhenteOpplysningerOmBarnData, +) : Brev { + constructor( + mal: Brevmal, + navn: String, + fødselsnummer: String, + barnasFødselsdager: String, + enhet: String, + dokumentliste: List, + saksbehandlerNavn: String, + ) : this( + mal = mal, + data = InnhenteOpplysningerOmBarnData( + delmalData = InnhenteOpplysningerOmBarnData.DelmalData( + signatur = SignaturDelmal( + enhet = enhet, + saksbehandlerNavn = saksbehandlerNavn, + ), + ), + flettefelter = InnhenteOpplysningerOmBarnData.Flettefelter( + navn = navn, + fodselsnummer = fødselsnummer, + barnasFødselsdager = barnasFødselsdager, + dokumentliste = dokumentliste, + ), + ), + ) +} + +data class InnhenteOpplysningerOmBarnData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + val barnasFodselsdatoer: Flettefelt, + val dokumentliste: Flettefelt, + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + barnasFødselsdager: String, + dokumentliste: List, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + barnasFodselsdatoer = flettefelt(barnasFødselsdager), + dokumentliste = flettefelt(dokumentliste), + ) + } + + data class DelmalData( + val signatur: SignaturDelmal, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/KorreksjonVedtaksbrev.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/KorreksjonVedtaksbrev.kt new file mode 100644 index 000000000..db707ed9b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/KorreksjonVedtaksbrev.kt @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class KorreksjonVedtaksbrev( + override val mal: Brevmal = Brevmal.VEDTAK_KORREKSJON_VEDTAKSBREV, + override val data: KorreksjonVedtaksbrevData, +) : Brev + +data class KorreksjonVedtaksbrevData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + ) + } + + data class DelmalData( + val signaturVedtak: SignaturVedtak, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Opph\303\270rMedEndring.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Opph\303\270rMedEndring.kt" new file mode 100644 index 000000000..9d7072b76 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Opph\303\270rMedEndring.kt" @@ -0,0 +1,62 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode + +data class OpphørMedEndring( + override val mal: Brevmal, + override val data: OpphørMedEndringData, +) : Vedtaksbrev { + + constructor( + mal: Brevmal = Brevmal.VEDTAK_OPPHØR_MED_ENDRING, + vedtakFellesfelter: VedtakFellesfelter, + etterbetaling: Etterbetaling? = null, + erFeilutbetalingPåBehandling: Boolean, + etterbetalingInstitusjon: EtterbetalingInstitusjon? = null, + refusjonEosAvklart: RefusjonEøsAvklart? = null, + refusjonEosUavklart: RefusjonEøsUavklart? = null, + ) : + this( + mal = mal, + data = OpphørMedEndringData( + delmalData = OpphørMedEndringData.Delmaler( + signaturVedtak = SignaturVedtak( + enhet = vedtakFellesfelter.enhet, + saksbehandler = vedtakFellesfelter.saksbehandler, + beslutter = vedtakFellesfelter.beslutter, + ), + hjemmeltekst = vedtakFellesfelter.hjemmeltekst, + feilutbetaling = erFeilutbetalingPåBehandling, + etterbetaling = etterbetaling, + etterbetalingInstitusjon = etterbetalingInstitusjon, + korrigertVedtak = vedtakFellesfelter.korrigertVedtakData, + refusjonEosAvklart = refusjonEosAvklart, + refusjonEosUavklart = refusjonEosUavklart, + ), + flettefelter = FlettefelterForDokumentImpl( + gjelder = flettefelt(vedtakFellesfelter.gjelder), + navn = flettefelt(vedtakFellesfelter.søkerNavn), + fodselsnummer = flettefelt(vedtakFellesfelter.søkerFødselsnummer), + organisasjonsnummer = flettefelt(vedtakFellesfelter.organisasjonsnummer), + ), + perioder = vedtakFellesfelter.perioder, + ), + ) +} + +data class OpphørMedEndringData( + override val delmalData: Delmaler, + override val flettefelter: FlettefelterForDokument, + override val perioder: List, +) : VedtaksbrevData { + data class Delmaler( + val signaturVedtak: SignaturVedtak, + val feilutbetaling: Boolean, + val hjemmeltekst: Hjemmeltekst, + val etterbetaling: Etterbetaling?, + val etterbetalingInstitusjon: EtterbetalingInstitusjon?, + val korrigertVedtak: KorrigertVedtakData?, + val refusjonEosAvklart: RefusjonEøsAvklart?, + val refusjonEosUavklart: RefusjonEøsUavklart?, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Opph\303\270rt.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Opph\303\270rt.kt" new file mode 100644 index 000000000..e0014b60a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Opph\303\270rt.kt" @@ -0,0 +1,51 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode + +data class Opphørt( + override val mal: Brevmal, + override val data: OpphørtData, +) : Vedtaksbrev { + + constructor( + mal: Brevmal = Brevmal.VEDTAK_OPPHØRT, + vedtakFellesfelter: VedtakFellesfelter, + erFeilutbetalingPåBehandling: Boolean, + ) : + this( + mal = mal, + data = OpphørtData( + delmalData = OpphørtData.Delmaler( + signaturVedtak = SignaturVedtak( + enhet = vedtakFellesfelter.enhet, + saksbehandler = vedtakFellesfelter.saksbehandler, + beslutter = vedtakFellesfelter.beslutter, + ), + hjemmeltekst = vedtakFellesfelter.hjemmeltekst, + feilutbetaling = erFeilutbetalingPåBehandling, + korrigertVedtak = vedtakFellesfelter.korrigertVedtakData, + ), + flettefelter = FlettefelterForDokumentImpl( + navn = vedtakFellesfelter.søkerNavn, + fodselsnummer = vedtakFellesfelter.søkerFødselsnummer, + organisasjonsnummer = vedtakFellesfelter.organisasjonsnummer, + gjelder = vedtakFellesfelter.gjelder, + ), + perioder = vedtakFellesfelter.perioder, + ), + ) +} + +data class OpphørtData( + override val delmalData: Delmaler, + override val flettefelter: FlettefelterForDokumentImpl, + override val perioder: List, +) : VedtaksbrevData { + + data class Delmaler( + val signaturVedtak: SignaturVedtak, + val feilutbetaling: Boolean, + val hjemmeltekst: Hjemmeltekst, + val korrigertVedtak: KorrigertVedtakData?, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Svartidsbrev.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Svartidsbrev.kt new file mode 100644 index 000000000..71d0dae06 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Svartidsbrev.kt @@ -0,0 +1,70 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class Svartidsbrev( + override val mal: Brevmal, + override val data: SvartidsbrevData, +) : Brev { + constructor( + navn: String, + fodselsnummer: String, + enhet: String, + mal: Brevmal, + erEøsBehandling: Boolean, + organisasjonsnummer: String? = null, + gjelder: String? = null, + saksbehandlerNavn: String, + ) : this( + mal = mal, + data = SvartidsbrevData( + flettefelter = SvartidsbrevData.Flettefelter( + navn = navn, + fodselsnummer = fodselsnummer, + organisasjonsnummer = organisasjonsnummer, + gjelder = gjelder, + ), + delmalData = SvartidsbrevData.DelmalData( + signatur = SignaturDelmal( + enhet = enhet, + saksbehandlerNavn = saksbehandlerNavn, + ), + kontonummer = erEøsBehandling, + + ), + ), + ) +} + +data class SvartidsbrevData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + override val organisasjonsnummer: Flettefelt, + override val gjelder: Flettefelt, + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + organisasjonsnummer: String? = null, + gjelder: String? = null, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + organisasjonsnummer = flettefelt(organisasjonsnummer), + gjelder = flettefelt(gjelder), + ) + } + + data class DelmalData( + val signatur: SignaturDelmal, + val kontonummer: Boolean, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/VarselOmRevurderingDeltBostedParagraf14Brev.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/VarselOmRevurderingDeltBostedParagraf14Brev.kt new file mode 100644 index 000000000..8f99abfca --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/VarselOmRevurderingDeltBostedParagraf14Brev.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class VarselOmRevurderingDeltBostedParagraf14Brev( + override val mal: Brevmal = Brevmal.VARSEL_OM_REVURDERING_DELT_BOSTED_PARAGRAF_14, + override val data: VarselOmRevurderingDeltBostedParagraf14Data, +) : Brev + +data class VarselOmRevurderingDeltBostedParagraf14Data( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + val barnMedDeltBostedAvtale: Flettefelt, + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + barnMedDeltBostedAvtale: List, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + barnMedDeltBostedAvtale = flettefelt(barnMedDeltBostedAvtale), + ) + } + + data class DelmalData( + val signatur: SignaturDelmal, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/VarselOmRevurderingSamboerBrev.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/VarselOmRevurderingSamboerBrev.kt new file mode 100644 index 000000000..e3bc5ace7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/VarselOmRevurderingSamboerBrev.kt @@ -0,0 +1,36 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class VarselOmRevurderingSamboerBrev( + override val mal: Brevmal = Brevmal.VARSEL_OM_REVURDERING_SAMBOER, + override val data: VarselOmRevurderingSamboerData, +) : Brev + +data class VarselOmRevurderingSamboerData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + val datoAvtale: Flettefelt, + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + datoAvtale: String, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + datoAvtale = flettefelt(datoAvtale), + ) + } + + data class DelmalData( + val signatur: SignaturDelmal, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/VarselbrevMed\303\205rsaker.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/VarselbrevMed\303\205rsaker.kt" new file mode 100644 index 000000000..350c5d3f9 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/VarselbrevMed\303\205rsaker.kt" @@ -0,0 +1,71 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class VarselbrevMedÅrsaker( + override val mal: Brevmal, + override val data: VarselOmRevurderingData, +) : Brev { + constructor( + mal: Brevmal, + navn: String, + fødselsnummer: String, + varselÅrsaker: List, + enhet: String, + organisasjonsnummer: String? = null, + gjelder: String? = null, + saksbehandlerNavn: String, + ) : this( + mal = mal, + data = VarselOmRevurderingData( + delmalData = VarselOmRevurderingData.DelmalData( + signatur = SignaturDelmal( + enhet = enhet, + saksbehandlerNavn = saksbehandlerNavn, + ), + ), + flettefelter = VarselOmRevurderingData.Flettefelter( + navn = navn, + fodselsnummer = fødselsnummer, + varselÅrsaker = varselÅrsaker, + organisasjonsnummer = organisasjonsnummer, + gjelder = gjelder, + ), + ), + ) +} + +data class VarselOmRevurderingData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + val varselAarsaker: Flettefelt, + override val organisasjonsnummer: Flettefelt, + override val gjelder: Flettefelt, + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + varselÅrsaker: List, + organisasjonsnummer: String? = null, + gjelder: String? = null, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + varselAarsaker = flettefelt(varselÅrsaker), + organisasjonsnummer = flettefelt(organisasjonsnummer), + gjelder = flettefelt(gjelder), + ) + } + + data class DelmalData( + val signatur: SignaturDelmal, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Varselbrev\303\205rlegKontrollE\303\230S.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Varselbrev\303\205rlegKontrollE\303\230S.kt" new file mode 100644 index 000000000..88145c72e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Varselbrev\303\205rlegKontrollE\303\230S.kt" @@ -0,0 +1,67 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import java.time.LocalDate + +data class VarselbrevÅrlegKontrollEøs( + override val mal: Brevmal, + override val data: VarselbrevÅrlegKontrollEøsData, +) : Brev { + + constructor( + mal: Brevmal, + navn: String, + fødselsnummer: String, + enhet: String, + mottakerlandSed: String, + dokumentliste: List = emptyList(), + saksbehandlerNavn: String, + ) : this( + mal = mal, + data = VarselbrevÅrlegKontrollEøsData( + delmalData = VarselbrevÅrlegKontrollEøsData.DelmalData( + signatur = SignaturDelmal( + enhet = enhet, + saksbehandlerNavn = saksbehandlerNavn, + ), + ), + flettefelter = VarselbrevÅrlegKontrollEøsData.Flettefelter( + navn = navn, + fodselsnummer = fødselsnummer, + mottakerlandSed = mottakerlandSed, + dokumentliste = dokumentliste, + ), + ), + ) +} + +data class VarselbrevÅrlegKontrollEøsData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + val mottakerlandSed: Flettefelt, + val dokumentliste: Flettefelt, + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + mottakerlandSed: String, + dokumentliste: List, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + mottakerlandSed = flettefelt(mottakerlandSed), + dokumentliste = flettefelt(dokumentliste), + ) + } + + data class DelmalData( + val signatur: SignaturDelmal, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/VedtakEndring.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/VedtakEndring.kt new file mode 100644 index 000000000..a9230d92b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/VedtakEndring.kt @@ -0,0 +1,80 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode + +data class VedtakEndring( + override val mal: Brevmal, + override val data: EndringVedtakData, +) : Vedtaksbrev { + + constructor( + mal: Brevmal = Brevmal.VEDTAK_ENDRING, + vedtakFellesfelter: VedtakFellesfelter, + etterbetaling: Etterbetaling? = null, + erFeilutbetalingPåBehandling: Boolean, + erKlage: Boolean, + etterbetalingInstitusjon: EtterbetalingInstitusjon? = null, + informasjonOmAarligKontroll: Boolean, + feilutbetaltValuta: FeilutbetaltValuta? = null, + refusjonEosAvklart: RefusjonEøsAvklart? = null, + refusjonEosUavklart: RefusjonEøsUavklart? = null, + duMåMeldeFraOmEndringer: Boolean = true, + duMåMeldeFraOmEndringerEøsSelvstendigRett: Boolean = false, + ) : + this( + mal = mal, + data = EndringVedtakData( + delmalData = EndringVedtakData.Delmaler( + signaturVedtak = SignaturVedtak( + enhet = vedtakFellesfelter.enhet, + saksbehandler = vedtakFellesfelter.saksbehandler, + beslutter = vedtakFellesfelter.beslutter, + ), + etterbetaling = etterbetaling, + hjemmeltekst = vedtakFellesfelter.hjemmeltekst, + klage = erKlage, + klageInstitusjon = erKlage, + feilutbetaling = erFeilutbetalingPåBehandling, + etterbetalingInstitusjon = etterbetalingInstitusjon, + korrigertVedtak = vedtakFellesfelter.korrigertVedtakData, + informasjonOmAarligKontroll = informasjonOmAarligKontroll, + forMyeUtbetaltBarnetrygd = feilutbetaltValuta, + refusjonEosAvklart = refusjonEosAvklart, + refusjonEosUavklart = refusjonEosUavklart, + duMaaMeldeFraOmEndringerEosSelvstendigRett = duMåMeldeFraOmEndringerEøsSelvstendigRett, + duMaaMeldeFraOmEndringer = duMåMeldeFraOmEndringer, + ), + flettefelter = FlettefelterForDokumentImpl( + gjelder = flettefelt(vedtakFellesfelter.gjelder), + navn = flettefelt(vedtakFellesfelter.søkerNavn), + fodselsnummer = flettefelt(vedtakFellesfelter.søkerFødselsnummer), + organisasjonsnummer = flettefelt(vedtakFellesfelter.organisasjonsnummer), + ), + perioder = vedtakFellesfelter.perioder, + ), + ) +} + +data class EndringVedtakData( + override val delmalData: Delmaler, + override val flettefelter: FlettefelterForDokument, + override val perioder: List, +) : VedtaksbrevData { + + data class Delmaler( + val signaturVedtak: SignaturVedtak, + val etterbetaling: Etterbetaling?, + val feilutbetaling: Boolean, + val hjemmeltekst: Hjemmeltekst, + val klage: Boolean, + val klageInstitusjon: Boolean, + val etterbetalingInstitusjon: EtterbetalingInstitusjon?, + val korrigertVedtak: KorrigertVedtakData?, + val informasjonOmAarligKontroll: Boolean, + val forMyeUtbetaltBarnetrygd: FeilutbetaltValuta?, + val refusjonEosAvklart: RefusjonEøsAvklart?, + val refusjonEosUavklart: RefusjonEøsUavklart?, + val duMaaMeldeFraOmEndringerEosSelvstendigRett: Boolean, + val duMaaMeldeFraOmEndringer: Boolean, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Vedtaksbrev.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Vedtaksbrev.kt new file mode 100644 index 000000000..13fd74812 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/Vedtaksbrev.kt @@ -0,0 +1,76 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler + +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode + +interface Vedtaksbrev : Brev { + + override val mal: Brevmal + override val data: VedtaksbrevData +} + +interface VedtaksbrevData : BrevData { + + val perioder: List +} + +enum class BrevPeriodeType(val apiNavn: String) { + UTBETALING("utbetaling"), + INGEN_UTBETALING("ingenUtbetaling"), + INGEN_UTBETALING_UTEN_PERIODE("ingenUtbetalingUtenPeriode"), + FORTSATT_INNVILGET("fortsattInnvilget"), + + @Deprecated("Skal renames til FORTSATT_INNVILGET når det gamle implementasjonen er fjernet") + FORTSATT_INNVILGET_NY("fortsattInnvilgetNy"), + + @Deprecated("Kun UTBETALING, INGEN_UTBETALING, INGEN_UTBETALING_UTEN_PERIODE, FORTSATT_INNVILGET skal brukes") + INNVILGELSE("innvilgelse"), + + @Deprecated("Kun UTBETALING, INGEN_UTBETALING, INGEN_UTBETALING_UTEN_PERIODE, FORTSATT_INNVILGET skal brukes") + INNVILGELSE_INGEN_UTBETALING("innvilgelseIngenUtbetaling"), + + @Deprecated("Kun UTBETALING, INGEN_UTBETALING, INGEN_UTBETALING_UTEN_PERIODE, FORTSATT_INNVILGET skal brukes") + INNVILGELSE_KUN_UTBETALING_PÅ_SØKER("innvilgelseKunUtbetalingPaSoker"), + + @Deprecated("Kun UTBETALING, INGEN_UTBETALING, INGEN_UTBETALING_UTEN_PERIODE, FORTSATT_INNVILGET skal brukes") + OPPHOR("opphor"), + + @Deprecated("Kun UTBETALING, INGEN_UTBETALING, INGEN_UTBETALING_UTEN_PERIODE, FORTSATT_INNVILGET skal brukes") + AVSLAG("avslag"), + + @Deprecated("Kun UTBETALING, INGEN_UTBETALING, INGEN_UTBETALING_UTEN_PERIODE, FORTSATT_INNVILGET skal brukes") + AVSLAG_UTEN_PERIODE("avslagUtenPeriode"), + + @Deprecated("Kun UTBETALING, INGEN_UTBETALING, INGEN_UTBETALING_UTEN_PERIODE, FORTSATT_INNVILGET skal brukes") + INNVILGELSE_INSTITUSJON("innvilgelseInstitusjon"), + + @Deprecated("Kun UTBETALING, INGEN_UTBETALING, INGEN_UTBETALING_UTEN_PERIODE, FORTSATT_INNVILGET skal brukes") + OPPHOR_INSTITUSJON("opphorInstitusjon"), + + @Deprecated("Kun UTBETALING, INGEN_UTBETALING, INGEN_UTBETALING_UTEN_PERIODE, FORTSATT_INNVILGET skal brukes") + AVSLAG_INSTITUSJON("avslagInstitusjon"), + + @Deprecated("Kun UTBETALING, INGEN_UTBETALING, INGEN_UTBETALING_UTEN_PERIODE, FORTSATT_INNVILGET skal brukes") + AVSLAG_UTEN_PERIODE_INSTITUSJON("avslagUtenPeriodeInstitusjon"), + + @Deprecated("Kun UTBETALING, INGEN_UTBETALING, INGEN_UTBETALING_UTEN_PERIODE, FORTSATT_INNVILGET skal brukes") + FORTSATT_INNVILGET_INSTITUSJON("fortsattInnvilgetInstitusjon"), +} + +enum class EndretUtbetalingBrevPeriodeType(val apiNavn: String) { + ENDRET_UTBETALINGSPERIODE("endretUtbetalingsperiode"), + ENDRET_UTBETALINGSPERIODE_DELVIS_UTBETALING("endretUtbetalingsperiodeDelvisUtbetaling"), + ENDRET_UTBETALINGSPERIODE_INGEN_UTBETALING("endretUtbetalingsperiodeIngenUtbetaling"), +} + +data class VedtakFellesfelter( + val enhet: String, + val saksbehandler: String, + val beslutter: String, + val hjemmeltekst: Hjemmeltekst, + val søkerNavn: String, + val søkerFødselsnummer: String, + val perioder: List, + val organisasjonsnummer: String? = null, + val gjelder: String? = null, + val korrigertVedtakData: KorrigertVedtakData? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/brevperioder/BrevPeriode.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/brevperioder/BrevPeriode.kt new file mode 100644 index 000000000..138487bd2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/brevperioder/BrevPeriode.kt @@ -0,0 +1,38 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder + +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.BrevPeriodeType +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Flettefelt +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.flettefelt +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BrevBegrunnelse + +data class BrevPeriode( + val fom: Flettefelt, + val tom: Flettefelt, + val belop: Flettefelt, + val antallBarn: Flettefelt, + val barnasFodselsdager: Flettefelt, + val begrunnelser: List, + val type: Flettefelt, + val duEllerInstitusjonen: Flettefelt, +) { + + constructor( + fom: String, + tom: String, + beløp: String, + begrunnelser: List, + brevPeriodeType: BrevPeriodeType, + antallBarn: String, + barnasFodselsdager: String, + duEllerInstitusjonen: String, + ) : this( + fom = flettefelt(fom), + tom = flettefelt(tom), + belop = flettefelt(beløp), + antallBarn = flettefelt(antallBarn), + barnasFodselsdager = flettefelt(barnasFodselsdager), + begrunnelser = begrunnelser, + type = flettefelt(brevPeriodeType.apiNavn), + duEllerInstitusjonen = flettefelt(duEllerInstitusjonen), + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/brevperioder/VarselbrevMed\303\205rsakerOgBarn.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/brevperioder/VarselbrevMed\303\205rsakerOgBarn.kt" new file mode 100644 index 000000000..8e3afb60b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/maler/brevperioder/VarselbrevMed\303\205rsakerOgBarn.kt" @@ -0,0 +1,73 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder + +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.BrevData +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Flettefelt +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.FlettefelterForDokument +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.SignaturDelmal +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.flettefelt +import java.time.LocalDate + +data class VarselbrevMedÅrsakerOgBarn( + override val mal: Brevmal, + override val data: VarselbrevMedÅrsakerOgBarnData, +) : Brev { + constructor( + mal: Brevmal, + navn: String, + fødselsnummer: String, + enhet: String, + varselÅrsaker: List, + barnasFødselsdager: String, + saksbehandlerNavn: String, + ) : this( + mal = mal, + data = VarselbrevMedÅrsakerOgBarnData( + delmalData = VarselbrevMedÅrsakerOgBarnData.DelmalData( + signatur = SignaturDelmal( + enhet = enhet, + saksbehandlerNavn = saksbehandlerNavn, + ), + ), + flettefelter = VarselbrevMedÅrsakerOgBarnData.Flettefelter( + navn = navn, + fodselsnummer = fødselsnummer, + varselÅrsaker = varselÅrsaker, + barnasFødselsdager = barnasFødselsdager, + ), + ), + ) +} + +data class VarselbrevMedÅrsakerOgBarnData( + override val delmalData: DelmalData, + override val flettefelter: Flettefelter, +) : BrevData { + + data class Flettefelter( + override val navn: Flettefelt, + override val fodselsnummer: Flettefelt, + override val brevOpprettetDato: Flettefelt = flettefelt(LocalDate.now().tilDagMånedÅr()), + val varselAarsaker: Flettefelt, + val barnasFodselsdatoer: Flettefelt, + ) : FlettefelterForDokument { + + constructor( + navn: String, + fodselsnummer: String, + varselÅrsaker: List, + barnasFødselsdager: String, + ) : this( + navn = flettefelt(navn), + fodselsnummer = flettefelt(fodselsnummer), + varselAarsaker = flettefelt(varselÅrsaker), + barnasFodselsdatoer = flettefelt(barnasFødselsdager), + ) + } + + data class DelmalData( + val signatur: SignaturDelmal, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/Brevmottaker.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/Brevmottaker.kt new file mode 100644 index 000000000..f14e366f4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/Brevmottaker.kt @@ -0,0 +1,75 @@ +package no.nav.familie.ba.sak.kjerne.brev.mottaker + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import org.hibernate.Hibernate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Brevmottaker") +@Table(name = "BREVMOTTAKER") +data class Brevmottaker( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "brevmottaker_seq_generator") + @SequenceGenerator(name = "brevmottaker_seq_generator", sequenceName = "brevmottaker_seq", allocationSize = 50) + val id: Long = 0, + + @Column(name = "fk_behandling_id", updatable = false, nullable = false) + val behandlingId: Long, + + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false) + var type: MottakerType, + + @Column(name = "navn", nullable = false, length = 70) + var navn: String, + + @Column(name = "adresselinje_1", nullable = false, length = 40) + var adresselinje1: String, + + @Column(name = "adresselinje_2", length = 40) + var adresselinje2: String? = null, + + @Column(name = "postnummer", nullable = false, length = 10) + var postnummer: String, + + @Column(name = "poststed", nullable = false, length = 30) + var poststed: String, + + @Column(name = "landkode", nullable = false, length = 2) + var landkode: String, +) : BaseEntitet() { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false + other as Brevmottaker + + return id == other.id + } + + override fun hashCode(): Int = javaClass.hashCode() + + @Override + override fun toString(): String { + return this::class.simpleName + "(" + + "id = $id, " + + "behandlingId = $behandlingId)" + } +} + +enum class MottakerType(val visningsnavn: String) { + BRUKER_MED_UTENLANDSK_ADRESSE("Bruker med utenlandsk adresse"), + FULLMEKTIG("Fullmektig"), + VERGE("Verge"), + DØDSBO("Dødsbo"), +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerController.kt new file mode 100644 index 000000000..62bd7a36e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerController.kt @@ -0,0 +1,72 @@ +package no.nav.familie.ba.sak.kjerne.brev.mottaker + +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestBrevmottaker +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType.APPLICATION_JSON_VALUE +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/brevmottaker") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class BrevmottakerController( + private val tilgangService: TilgangService, + private val brevmottakerService: BrevmottakerService, + private val utvidetBehandlingService: UtvidetBehandlingService, +) { + @PostMapping(path = ["{behandlingId}"], produces = [APPLICATION_JSON_VALUE], consumes = [APPLICATION_JSON_VALUE]) + fun leggTilBrevmottaker( + @PathVariable behandlingId: Long, + @RequestBody brevmottaker: RestBrevmottaker, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "legge til brevmottaker", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + brevmottakerService.leggTilBrevmottaker(brevmottaker, behandlingId) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @DeleteMapping(path = ["{behandlingId}/{mottakerId}"]) + fun fjernBrevmottaker( + @PathVariable behandlingId: Long, + @PathVariable mottakerId: Long, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.DELETE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "fjerne brevmottaker", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + brevmottakerService.fjernBrevmottaker(id = mottakerId) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @GetMapping(path = ["{behandlingId}"], produces = [APPLICATION_JSON_VALUE]) + fun hentBrevmottakere(@PathVariable behandlingId: Long): ResponseEntity>> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.VEILEDER, + handling = "hente brevmottakere", + ) + return ResponseEntity.ok(Ressurs.success(brevmottakerService.hentRestBrevmottakere(behandlingId = behandlingId))) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerRepository.kt new file mode 100644 index 000000000..e01c5bbf0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerRepository.kt @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.kjerne.brev.mottaker + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface BrevmottakerRepository : JpaRepository { + @Query(value = "SELECT b FROM Brevmottaker b WHERE b.behandlingId = :behandlingId") + fun finnBrevMottakereForBehandling(behandlingId: Long): List +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerService.kt new file mode 100644 index 000000000..12407bbb4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerService.kt @@ -0,0 +1,144 @@ +package no.nav.familie.ba.sak.kjerne.brev.mottaker + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.zeroSingleOrThrow +import no.nav.familie.ba.sak.ekstern.restDomene.RestBrevmottaker +import no.nav.familie.ba.sak.ekstern.restDomene.tilBrevMottaker +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.behandling.ValiderBrevmottakerService +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.domene.ManuellAdresseInfo +import no.nav.familie.ba.sak.kjerne.steg.domene.MottakerInfo +import no.nav.familie.ba.sak.kjerne.steg.domene.toList +import no.nav.familie.kontrakter.felles.BrukerIdType +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class BrevmottakerService( + private val brevmottakerRepository: BrevmottakerRepository, + private val loggService: LoggService, + private val personidentService: PersonidentService, + private val personopplysningerService: PersonopplysningerService, + private val validerBrevmottakerService: ValiderBrevmottakerService, +) { + + @Transactional + fun leggTilBrevmottaker(restBrevMottaker: RestBrevmottaker, behandlingId: Long) { + val brevmottaker = restBrevMottaker.tilBrevMottaker(behandlingId) + + validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligePersonerMedManuelleBrevmottakere(behandlingId, brevmottaker) + + loggService.opprettBrevmottakerLogg( + brevmottaker = brevmottaker, + brevmottakerFjernet = false, + ) + + brevmottakerRepository.save(brevmottaker) + } + + @Transactional + fun fjernBrevmottaker(id: Long) { + val brevmottaker = + brevmottakerRepository.findByIdOrNull(id) ?: throw Feil("Finner ikke brevmottaker med id=$id") + + loggService.opprettBrevmottakerLogg( + brevmottaker = brevmottaker, + brevmottakerFjernet = true, + ) + + brevmottakerRepository.deleteById(id) + } + + fun hentBrevmottakere(behandlingId: Long) = brevmottakerRepository.finnBrevMottakereForBehandling(behandlingId) + + fun hentRestBrevmottakere(behandlingId: Long) = + brevmottakerRepository.finnBrevMottakereForBehandling(behandlingId).map { + RestBrevmottaker( + id = it.id, + type = it.type, + navn = it.navn, + adresselinje1 = it.adresselinje1, + adresselinje2 = it.adresselinje2, + postnummer = it.postnummer, + poststed = it.poststed, + landkode = it.landkode, + ) + } + + fun lagMottakereFraBrevMottakere( + manueltRegistrerteMottakere: List, + søkersident: String, + søkersnavn: String = hentMottakerNavn(søkersident), + ): List { + manueltRegistrerteMottakere.singleOrNull { it.type == MottakerType.DØDSBO }?.let { + // brev sendes kun til den manuelt registerte dødsboadressen + return lagMottakerInfoUtenBrukerId(navn = søkersnavn, manuellAdresseInfo = lagManuellAdresseInfo(it)).toList() + } + + val manuellAdresseUtenlands = manueltRegistrerteMottakere.filter { it.type == MottakerType.BRUKER_MED_UTENLANDSK_ADRESSE } + .zeroSingleOrThrow { + FunksjonellFeil("Mottakerfeil: Det er registrert mer enn en utenlandsk adresse tilhørende bruker") + }?.let { + lagMottakerInfoMedBrukerId( + brukerId = søkersident, + navn = søkersnavn, + manuellAdresseInfo = lagManuellAdresseInfo(it), + ) + } + + // brev sendes til brukers (manuelt) registerte adresse (i utlandet) + val bruker = manuellAdresseUtenlands ?: lagMottakerInfoMedBrukerId(brukerId = søkersident, navn = søkersnavn) + + // ...og evt. til en manuelt registrert verge eller fullmektig i tillegg + val manuellTilleggsmottaker = manueltRegistrerteMottakere.filter { it.type != MottakerType.BRUKER_MED_UTENLANDSK_ADRESSE } + .zeroSingleOrThrow { + FunksjonellFeil("Mottakerfeil: ${first().type.visningsnavn} kan ikke kombineres med ${last().type.visningsnavn}") + }?.let { + lagMottakerInfoUtenBrukerId(navn = it.navn, manuellAdresseInfo = lagManuellAdresseInfo(it)) + } + + return listOfNotNull(bruker, manuellTilleggsmottaker) + } + + fun hentMottakerNavn(personIdent: String): String { + val aktør = personidentService.hentAktør(personIdent) + return personopplysningerService.hentPersoninfoNavnOgAdresse(aktør).let { + it.navn!! + } + } + + private fun lagManuellAdresseInfo(brevmottaker: Brevmottaker) = ManuellAdresseInfo( + adresselinje1 = brevmottaker.adresselinje1, + adresselinje2 = brevmottaker.adresselinje2, + postnummer = brevmottaker.postnummer, + poststed = brevmottaker.poststed, + landkode = brevmottaker.landkode, + ) + + private fun lagMottakerInfoUtenBrukerId( + navn: String, + manuellAdresseInfo: ManuellAdresseInfo, + ): MottakerInfo = MottakerInfo( + brukerId = "", + brukerIdType = null, + erInstitusjonVerge = false, + navn = navn, + manuellAdresseInfo = manuellAdresseInfo, + ) + + private fun lagMottakerInfoMedBrukerId( + brukerId: String, + navn: String, + manuellAdresseInfo: ManuellAdresseInfo? = null, + ) = MottakerInfo( + brukerId = brukerId, + brukerIdType = BrukerIdType.FNR, + erInstitusjonVerge = false, + navn = navn, + manuellAdresseInfo = manuellAdresseInfo, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelController.kt new file mode 100644 index 000000000..4d72b9925 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelController.kt @@ -0,0 +1,141 @@ +package no.nav.familie.ba.sak.kjerne.endretutbetaling + +import no.nav.familie.ba.sak.common.BehandlingValidering.validerBehandlingKanRedigeres +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestEndretUtbetalingAndel +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.kjerne.steg.TilbakestillBehandlingTilBehandlingsresultatService +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/endretutbetalingandel") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class EndretUtbetalingAndelController( + private val endretUtbetalingAndelService: EndretUtbetalingAndelService, + private val tilgangService: TilgangService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val utvidetBehandlingService: UtvidetBehandlingService, + private val tilbakestillBehandlingTilBehandlingsresultatService: TilbakestillBehandlingTilBehandlingsresultatService, +) { + + @PutMapping(path = ["{behandlingId}/{endretUtbetalingAndelId}"]) + fun oppdaterEndretUtbetalingAndelOgOppdaterTilkjentYtelse( + @PathVariable behandlingId: Long, + @PathVariable endretUtbetalingAndelId: Long, + @RequestBody restEndretUtbetalingAndel: RestEndretUtbetalingAndel, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Oppdater endretutbetalingandel", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + validerBehandlingKanRedigeres(behandling) + + endretUtbetalingAndelService.oppdaterEndretUtbetalingAndelOgOppdaterTilkjentYtelse( + behandling, + endretUtbetalingAndelId, + restEndretUtbetalingAndel, + ) + + tilbakestillBehandlingTilBehandlingsresultatService + .tilbakestillBehandlingTilBehandlingsresultat(behandlingId = behandling.id) + + return ResponseEntity.ok( + Ressurs.success( + utvidetBehandlingService + .lagRestUtvidetBehandling(behandlingId = behandling.id), + ), + ) + } + + @DeleteMapping(path = ["{behandlingId}/{endretUtbetalingAndelId}"]) + fun fjernEndretUtbetalingAndelOgOppdaterTilkjentYtelse( + @PathVariable behandlingId: Long, + @PathVariable endretUtbetalingAndelId: Long, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.DELETE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Oppdater endretutbetalingandel", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + validerBehandlingKanRedigeres(behandling) + + endretUtbetalingAndelService.fjernEndretUtbetalingAndelOgOppdaterTilkjentYtelse( + behandling, + endretUtbetalingAndelId, + ) + + tilbakestillBehandlingTilBehandlingsresultatService + .tilbakestillBehandlingTilBehandlingsresultat(behandlingId = behandling.id) + return ResponseEntity.ok( + Ressurs.success( + utvidetBehandlingService + .lagRestUtvidetBehandling(behandlingId = behandling.id), + ), + ) + } + + @PostMapping(path = ["/{behandlingId}"]) + fun lagreEndretUtbetalingAndelOgOppdaterTilkjentYtelse( + @PathVariable behandlingId: Long, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Opprett endretutbetalingandel", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + validerBehandlingKanRedigeres(behandling) + endretUtbetalingAndelService.opprettTomEndretUtbetalingAndelOgOppdaterTilkjentYtelse(behandling) + + tilbakestillBehandlingTilBehandlingsresultatService + .tilbakestillBehandlingTilBehandlingsresultat(behandlingId = behandling.id) + + return ResponseEntity.ok( + Ressurs.success( + utvidetBehandlingService + .lagRestUtvidetBehandling(behandlingId = behandling.id), + ), + ) + } + + @PostMapping(path = ["/{behandlingId}/tilbakestill"]) + fun tilbakestillBehandlingTilBehandlingsresultat( + @PathVariable behandlingId: Long, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Opprett endretutbetalingandel", + ) + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + validerBehandlingKanRedigeres(behandling) + + tilbakestillBehandlingTilBehandlingsresultatService + .tilbakestillBehandlingTilBehandlingsresultat(behandlingId = behandling.id) + + return ResponseEntity.ok( + Ressurs.success("OK"), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelHentOgPersisterService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelHentOgPersisterService.kt new file mode 100644 index 000000000..a34dfeaea --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelHentOgPersisterService.kt @@ -0,0 +1,12 @@ +package no.nav.familie.ba.sak.kjerne.endretutbetaling + +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndelRepository +import org.springframework.stereotype.Service + +@Service +class EndretUtbetalingAndelHentOgPersisterService( + private val endretUtbetalingAndelRepository: EndretUtbetalingAndelRepository, +) { + + fun hentForBehandling(behandlingId: Long) = endretUtbetalingAndelRepository.findByBehandlingId(behandlingId) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelService.kt new file mode 100644 index 000000000..ef5aab097 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelService.kt @@ -0,0 +1,151 @@ +package no.nav.familie.ba.sak.kjerne.endretutbetaling + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.ekstern.restDomene.RestEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerIngenOverlappendeEndring +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerPeriodeInnenforTilkjentytelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerÅrsak +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndelRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.fraRestEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal + +@Service +class EndretUtbetalingAndelService( + private val endretUtbetalingAndelRepository: EndretUtbetalingAndelRepository, + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + private val beregningService: BeregningService, + private val persongrunnlagService: PersongrunnlagService, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val vilkårsvurderingService: VilkårsvurderingService, + private val endretUtbetalingAndelOppdatertAbonnementer: List = emptyList(), + private val endretUtbetalingAndelHentOgPersisterService: EndretUtbetalingAndelHentOgPersisterService, +) { + @Transactional + fun oppdaterEndretUtbetalingAndelOgOppdaterTilkjentYtelse( + behandling: Behandling, + endretUtbetalingAndelId: Long, + restEndretUtbetalingAndel: RestEndretUtbetalingAndel, + ) { + val endretUtbetalingAndel = endretUtbetalingAndelRepository.getById(endretUtbetalingAndelId) + val person = + persongrunnlagService.hentPersonerPåBehandling(listOf(restEndretUtbetalingAndel.personIdent!!), behandling) + .first() + + val personopplysningGrunnlag = + personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = behandling.id) + ?: throw Feil("Fant ikke personopplysninggrunnlag på behandling ${behandling.id}") + + val andelTilkjentYtelser = andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandling.id) + + endretUtbetalingAndel.fraRestEndretUtbetalingAndel(restEndretUtbetalingAndel, person) + + val andreEndredeAndelerPåBehandling = endretUtbetalingAndelHentOgPersisterService.hentForBehandling(behandling.id) + .filter { it.id != endretUtbetalingAndelId } + + val gyldigTomEtterDagensDato = beregnGyldigTomIFremtiden( + andreEndredeAndelerPåBehandling = andreEndredeAndelerPåBehandling, + endretUtbetalingAndel = endretUtbetalingAndel, + andelTilkjentYtelser = andelTilkjentYtelser, + ) + + validerTomDato( + tomDato = endretUtbetalingAndel.tom, + gyldigTomEtterDagensDato = gyldigTomEtterDagensDato, + årsak = endretUtbetalingAndel.årsak, + ) + + if (endretUtbetalingAndel.tom == null) { + endretUtbetalingAndel.tom = gyldigTomEtterDagensDato + } + validerÅrsak( + endretUtbetalingAndel = endretUtbetalingAndel, + vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandling(behandlingId = behandling.id), + ) + + validerUtbetalingMotÅrsak( + årsak = endretUtbetalingAndel.årsak, + skalUtbetales = endretUtbetalingAndel.prosent != BigDecimal(0), + ) + + validerIngenOverlappendeEndring( + endretUtbetalingAndel = endretUtbetalingAndel, + eksisterendeEndringerPåBehandling = andreEndredeAndelerPåBehandling, + ) + + validerPeriodeInnenforTilkjentytelse(endretUtbetalingAndel, andelTilkjentYtelser) + + endretUtbetalingAndelRepository.saveAndFlush(endretUtbetalingAndel) + + beregningService.oppdaterBehandlingMedBeregning( + behandling, + personopplysningGrunnlag, + endretUtbetalingAndel, + ) + + endretUtbetalingAndelOppdatertAbonnementer.forEach { + it.endretUtbetalingAndelerOppdatert( + behandlingId = behandling.id, + endretUtbetalingAndeler = andreEndredeAndelerPåBehandling + endretUtbetalingAndel, + ) + } + } + + @Transactional + fun fjernEndretUtbetalingAndelOgOppdaterTilkjentYtelse( + behandling: Behandling, + endretUtbetalingAndelId: Long, + ) { + endretUtbetalingAndelRepository.deleteById(endretUtbetalingAndelId) + + val personopplysningGrunnlag = + personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = behandling.id) + ?: throw Feil("Fant ikke personopplysninggrunnlag på behandling ${behandling.id}") + + beregningService.oppdaterBehandlingMedBeregning(behandling, personopplysningGrunnlag) + + endretUtbetalingAndelOppdatertAbonnementer.forEach { abonnent -> + abonnent.endretUtbetalingAndelerOppdatert( + behandlingId = behandling.id, + endretUtbetalingAndeler = endretUtbetalingAndelRepository.findByBehandlingId(behandling.id), + ) + } + } + + @Transactional + fun opprettTomEndretUtbetalingAndelOgOppdaterTilkjentYtelse( + behandling: Behandling, + ) = + endretUtbetalingAndelRepository.save( + EndretUtbetalingAndel( + behandlingId = behandling.id, + ), + ) + + @Transactional + fun kopierEndretUtbetalingAndelFraForrigeBehandling(behandling: Behandling, forrigeBehandling: Behandling) { + endretUtbetalingAndelHentOgPersisterService.hentForBehandling(forrigeBehandling.id).forEach { + endretUtbetalingAndelRepository.save( + it.copy( + id = 0, + behandlingId = behandling.id, + ), + ) + } + } +} + +interface EndretUtbetalingAndelerOppdatertAbonnent { + fun endretUtbetalingAndelerOppdatert( + behandlingId: Long, + endretUtbetalingAndeler: List, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelUtils.kt new file mode 100644 index 000000000..29c3a8e99 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelUtils.kt @@ -0,0 +1,32 @@ +package no.nav.familie.ba.sak.kjerne.endretutbetaling + +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import java.time.YearMonth + +fun erStartPåUtvidetSammeMåned( + andelTilkjentYtelser: List, + fom: YearMonth?, +) = andelTilkjentYtelser.any { it.stønadFom == fom && it.type == YtelseType.UTVIDET_BARNETRYGD } + +fun beregnGyldigTomIFremtiden( + andreEndredeAndelerPåBehandling: List, + endretUtbetalingAndel: EndretUtbetalingAndel, + andelTilkjentYtelser: List, +): YearMonth? { + val førsteEndringEtterDenneEndringen = andreEndredeAndelerPåBehandling.filter { + it.fom?.isAfter(endretUtbetalingAndel.fom) == true && + it.person == endretUtbetalingAndel.person + }.sortedBy { it.fom }.firstOrNull() + + if (førsteEndringEtterDenneEndringen != null) { + return førsteEndringEtterDenneEndringen.fom?.minusMonths(1) + } else { + val sisteTomAndeler = andelTilkjentYtelser.filter { + it.aktør == endretUtbetalingAndel.person?.aktør + }.maxOf { it.stønadTom } + + return sisteTomAndeler + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelValidering.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelValidering.kt new file mode 100644 index 000000000..fca99b757 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelValidering.kt @@ -0,0 +1,397 @@ +package no.nav.familie.ba.sak.kjerne.endretutbetaling + +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.erBack2BackIMånedsskifte +import no.nav.familie.ba.sak.common.erDagenFør +import no.nav.familie.ba.sak.common.erMellom +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.førsteDagINesteMåned +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.common.isSameOrBefore +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.common.slåSammenOverlappendePerioder +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import no.nav.familie.ba.sak.common.tilMånedPeriode +import no.nav.familie.ba.sak.common.toPeriode +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.EndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.hentGyldigEtterbetalingFom +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonEnkel +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +object EndretUtbetalingAndelValidering { + + fun validerIngenOverlappendeEndring( + endretUtbetalingAndel: EndretUtbetalingAndel, + eksisterendeEndringerPåBehandling: List, + ) { + endretUtbetalingAndel.validerUtfyltEndring() + if (eksisterendeEndringerPåBehandling.any + { + it.overlapperMed(endretUtbetalingAndel.periode) && + it.person == endretUtbetalingAndel.person && + it.årsak == endretUtbetalingAndel.årsak + } + ) { + throw FunksjonellFeil( + melding = "Perioden som blir forsøkt lagt til overlapper med eksisterende periode på person.", + frontendFeilmelding = "Perioden du forsøker å legge til overlapper med eksisterende periode på personen. Om dette er ønskelig må du først endre den eksisterende perioden.", + ) + } + } + + fun validerPeriodeInnenforTilkjentytelse( + endretUtbetalingAndel: EndretUtbetalingAndel, + andelTilkjentYtelser: Collection, + ) { + endretUtbetalingAndel.validerUtfyltEndring() + val minsteDatoForTilkjentYtelse = andelTilkjentYtelser.filter { + it.aktør == endretUtbetalingAndel.person!!.aktør + }.minByOrNull { it.stønadFom }?.stønadFom + + val størsteDatoForTilkjentYtelse = andelTilkjentYtelser.filter { + it.aktør == endretUtbetalingAndel.person!!.aktør + }.maxByOrNull { it.stønadTom }?.stønadTom + + if ((minsteDatoForTilkjentYtelse == null || størsteDatoForTilkjentYtelse == null) || + ( + endretUtbetalingAndel.fom!!.isBefore(minsteDatoForTilkjentYtelse) || + endretUtbetalingAndel.tom!!.isAfter(størsteDatoForTilkjentYtelse) + ) + ) { + throw FunksjonellFeil( + melding = "Det er ingen tilkjent ytelse for personen det blir forsøkt lagt til en endret periode for.", + frontendFeilmelding = "Du har valgt en periode der det ikke finnes tilkjent ytelse for valgt person i hele eller deler av perioden.", + ) + } + } + + fun validerPeriodeInnenforTilkjentytelse( + endretUtbetalingAndeler: List, + andelTilkjentYtelser: Collection, + ) = endretUtbetalingAndeler.forEach { validerPeriodeInnenforTilkjentytelse(it, andelTilkjentYtelser) } + + fun validerÅrsak( + endretUtbetalingAndeler: List, + vilkårsvurdering: Vilkårsvurdering?, + ) = + endretUtbetalingAndeler.forEach { validerÅrsak(it, vilkårsvurdering) } + + fun validerÅrsak( + endretUtbetalingAndel: EndretUtbetalingAndel, + vilkårsvurdering: Vilkårsvurdering?, + ) { + val årsak = endretUtbetalingAndel.årsak ?: return + + return when (årsak) { + Årsak.DELT_BOSTED -> { + val deltBostedPerioder = finnDeltBostedPerioder( + person = endretUtbetalingAndel.person, + vilkårsvurdering = vilkårsvurdering, + ).map { it.tilMånedPeriode() } + + validerDeltBosted( + endretUtbetalingAndel = endretUtbetalingAndel, + deltBostedPerioder = deltBostedPerioder, + ) + } + + Årsak.ETTERBETALING_3ÅR -> validerEtterbetaling3År( + endretUtbetalingAndel = endretUtbetalingAndel, + behandlingOpprettetTidspunkt = vilkårsvurdering?.behandling?.opprettetTidspunkt?.toLocalDate(), + ) + + Årsak.ALLEREDE_UTBETALT -> validerAlleredeUtbetalt(endretUtbetalingAndel = endretUtbetalingAndel) + + Årsak.ENDRE_MOTTAKER -> validerEndreMottaker(endretUtbetalingAndel = endretUtbetalingAndel) + } + } + + private fun validerEndreMottaker(endretUtbetalingAndel: EndretUtbetalingAndel) { + val innværendeÅrMåned = YearMonth.now() + + if (endretUtbetalingAndel.fom?.isBefore(innværendeÅrMåned) == true) { + throw FunksjonellFeil("Du har valgt årsaken Foreldre bor sammen, endre mottaker. Du kan ikke velge denne årsaken og en fra og med dato tilbake i tid. Ta kontakt med superbruker om du er usikker på hva du skal gjøre.") + } + + if (endretUtbetalingAndel.tom?.isSameOrBefore(innværendeÅrMåned) == true) { + throw FunksjonellFeil("Du har valgt årsaken Foreldre bor sammen, endre mottaker. Du kan ikke velge denne årsaken og en til og med dato tilbake i tid. Ta kontakt med superbruker om du er usikker på hva du skal gjøre.") + } + } + + private fun validerAlleredeUtbetalt(endretUtbetalingAndel: EndretUtbetalingAndel) { + if (endretUtbetalingAndel.tom?.isAfter(YearMonth.now()) == true) { + throw FunksjonellFeil("Du har valgt årsaken allerede utbetalt. Du kan ikke velge denne årsaken og en til og med dato frem i tid. Ta kontakt med superbruker om du er usikker på hva du skal gjøre.") + } + } + + private fun validerEtterbetaling3År( + endretUtbetalingAndel: EndretUtbetalingAndel, + behandlingOpprettetTidspunkt: LocalDate?, + ) { + val kravDato = endretUtbetalingAndel.søknadstidspunkt ?: behandlingOpprettetTidspunkt + if (endretUtbetalingAndel.prosent != BigDecimal.ZERO) { + throw FunksjonellFeil( + "Du kan ikke sette årsak etterbetaling 3 år når du har valgt at perioden skal utbetales.", + ) + } else if ( + endretUtbetalingAndel.tom?.isAfter( + hentGyldigEtterbetalingFom( + kravDato = kravDato ?: LocalDate.now(), + ), + ) == true + ) { + throw FunksjonellFeil( + "Du kan ikke stoppe etterbetaling for en periode som ikke strekker seg mer enn 3 år tilbake i tid.", + ) + } + } + + internal fun validerDeltBosted( + endretUtbetalingAndel: EndretUtbetalingAndel, + deltBostedPerioder: List, + ) { + if (endretUtbetalingAndel.årsak != Årsak.DELT_BOSTED) return + + if (endretUtbetalingAndel.fom == null || endretUtbetalingAndel.tom == null) { + throw FunksjonellFeil("Du må sette fom og tom.") + } + val endringsperiode = MånedPeriode(fom = endretUtbetalingAndel.fom!!, tom = endretUtbetalingAndel.tom!!) + + if ( + !deltBostedPerioder.any { + endringsperiode.erMellom(MånedPeriode(fom = it.fom, tom = it.tom)) + } + ) { + throw FunksjonellFeil( + melding = "Det er ingen sats for delt bosted i perioden det opprettes en endring med årsak delt bosted for.", + frontendFeilmelding = "Du har valgt årsaken 'delt bosted', denne samstemmer ikke med vurderingene gjort på vilkårsvurderingssiden i perioden du har valgt.", + ) + } + } + + fun validerAtAlleOpprettedeEndringerErUtfylt(endretUtbetalingAndeler: List) { + runCatching { + endretUtbetalingAndeler.forEach { it.validerUtfyltEndring() } + }.onFailure { + throw FunksjonellFeil( + melding = "Det er opprettet instanser av EndretUtbetalingandel som ikke er fylt ut før navigering til neste steg.", + frontendFeilmelding = "Du har opprettet en eller flere endrede utbetalingsperioder som er ufullstendig utfylt. Disse må enten fylles ut eller slettes før du kan gå videre.", + ) + } + } + + fun validerAtEndringerErTilknyttetAndelTilkjentYtelse(endretUtbetalingAndeler: List) { + if (endretUtbetalingAndeler.any { it.andelerTilkjentYtelse.isEmpty() }) { + throw FunksjonellFeil( + melding = "Det er opprettet instanser av EndretUtbetalingandel som ikke er tilknyttet noen andeler. De må enten lagres eller slettes av SB.", + frontendFeilmelding = "Du har endrede utbetalingsperioder. Bekreft, slett eller oppdater periodene i listen.", + ) + } + } +} + +fun validerAtDetFinnesDeltBostedEndringerMedSammeProsentForUtvidedeEndringer( + endretUtbetalingAndelerMedÅrsakDeltBosted: List, +) { + val endredeUtvidetUtbetalingerAndeler = endretUtbetalingAndelerMedÅrsakDeltBosted + .filter { endretUtbetaling -> + endretUtbetaling.andelerTilkjentYtelse.any { it.erUtvidet() } + } + + endredeUtvidetUtbetalingerAndeler.forEach { endretPåUtvidetUtbetalinger -> + val deltBostedEndringerISammePeriode = endretUtbetalingAndelerMedÅrsakDeltBosted.filter { + it.årsak == Årsak.DELT_BOSTED && + it.fom!!.isSameOrBefore(endretPåUtvidetUtbetalinger.fom!!) && + it.tom!!.isSameOrAfter(endretPåUtvidetUtbetalinger.tom!!) && + it.id != endretPåUtvidetUtbetalinger.id + } + + if (deltBostedEndringerISammePeriode.isEmpty()) { + val feilmelding = + "Det kan ikke være en endring på en utvidet ytelse uten en endring på en delt bosted ytelse. " + + "Legg til en delt bosted endring i perioden ${endretPåUtvidetUtbetalinger.fom} til " + + "${endretPåUtvidetUtbetalinger.tom} eller fjern endringen på den utvidede ytelsen." + throw FunksjonellFeil(frontendFeilmelding = feilmelding, melding = feilmelding) + } + } +} + +fun validerUtbetalingMotÅrsak(årsak: Årsak?, skalUtbetales: Boolean) { + if (skalUtbetales && (årsak == Årsak.ENDRE_MOTTAKER || årsak == Årsak.ALLEREDE_UTBETALT)) { + val feilmelding = "Du kan ikke velge denne årsaken og si at barnetrygden skal utbetales." + throw FunksjonellFeil(frontendFeilmelding = feilmelding, melding = feilmelding) + } +} + +fun validerTomDato(tomDato: YearMonth?, gyldigTomEtterDagensDato: YearMonth?, årsak: Årsak?) { + val dagensDato = YearMonth.now() + if (årsak == Årsak.ALLEREDE_UTBETALT && tomDato?.isAfter(dagensDato) == true) { + val feilmelding = + "For årsak '${årsak.visningsnavn}' kan du ikke legge inn til og med dato som er i neste måned eller senere." + throw FunksjonellFeil( + frontendFeilmelding = feilmelding, + melding = feilmelding, + ) + } + if (tomDato?.isAfter(dagensDato) == true && tomDato != gyldigTomEtterDagensDato) { + val feilmelding = + "Du kan ikke legge inn til og med dato som er i neste måned eller senere. Om det gjelder en løpende periode vil systemet legge inn riktig dato for deg." + throw FunksjonellFeil( + frontendFeilmelding = feilmelding, + melding = feilmelding, + ) + } +} + +private fun slåSammenDeltBostedPerioderSomHengerSammen( + perioder: MutableList, +): MutableList { + if (perioder.isEmpty()) return mutableListOf() + val sortertePerioder = perioder.sortedBy { it.fom }.toMutableList() + var periodenViSerPå: Periode = sortertePerioder.first() + val oppdatertListeMedPerioder = mutableListOf() + + for (index in 0 until sortertePerioder.size) { + val periode = sortertePerioder[index] + val nestePeriode = if (index == sortertePerioder.size - 1) null else sortertePerioder[index + 1] + + periodenViSerPå = if (nestePeriode != null) { + val andelerSkalSlåsSammen = + periode.tom.sisteDagIMåned().erDagenFør(nestePeriode.fom.førsteDagIInneværendeMåned()) + + if (andelerSkalSlåsSammen) { + val nyPeriode = periodenViSerPå.copy(tom = nestePeriode.tom) + nyPeriode + } else { + oppdatertListeMedPerioder.add(periodenViSerPå) + sortertePerioder[index + 1] + } + } else { + oppdatertListeMedPerioder.add(periodenViSerPå) + break + } + } + return oppdatertListeMedPerioder +} + +private fun VilkårResultat.tilPeriode( + vilkår: List, +): Periode? { + if (this.periodeFom == null) return null + val fraOgMedDato = this.periodeFom!!.førsteDagINesteMåned() + val tilOgMedDato = finnTilOgMedDato(tilOgMed = this.periodeTom, vilkårResultater = vilkår) + if (fraOgMedDato.toYearMonth().isAfter(tilOgMedDato.toYearMonth())) return null + return Periode( + fom = fraOgMedDato, + tom = tilOgMedDato, + ) +} + +fun finnDeltBostedPerioder( + person: Person?, + vilkårsvurdering: Vilkårsvurdering?, +): List { + if (vilkårsvurdering == null || person == null) return emptyList() + val deltBostedPerioder = if (person.type == PersonType.SØKER) { + val deltBostedVilkårResultater = vilkårsvurdering.personResultater.flatMap { personResultat -> + personResultat.vilkårResultater.filter { + it.utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.DELT_BOSTED) && it.resultat == Resultat.OPPFYLT + } + } + + val deltBostedPerioder = deltBostedVilkårResultater.groupBy { it.personResultat?.aktør } + .flatMap { (_, vilkårResultater) -> vilkårResultater.mapNotNull { it.tilPeriode(vilkår = vilkårResultater) } } + + slåSammenOverlappendePerioder( + deltBostedPerioder.map { + DatoIntervallEntitet( + fom = it.fom, + tom = it.tom, + ) + }, + ).filter { it.fom != null && it.tom != null }.map { + Periode( + fom = it.fom!!, + tom = it.tom!!, + ) + } + } else { + val personensVilkår = vilkårsvurdering.personResultater.single { it.aktør == person.aktør } + + val deltBostedVilkårResultater = personensVilkår.vilkårResultater.filter { + it.utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.DELT_BOSTED) && it.resultat == Resultat.OPPFYLT + } + + deltBostedVilkårResultater.mapNotNull { it.tilPeriode(vilkår = deltBostedVilkårResultater) } + } + return slåSammenDeltBostedPerioderSomHengerSammen( + perioder = deltBostedPerioder.toMutableList(), + ) +} + +fun validerBarnasVilkår(barna: List, vilkårsvurdering: Vilkårsvurdering) { + val listeAvFeil = mutableListOf() + + barna.map { barn -> + vilkårsvurdering.personResultater + .flatMap { it.vilkårResultater } + .filter { it.personResultat?.aktør == barn.aktør } + .forEach { vilkårResultat -> + if (vilkårResultat.resultat == Resultat.OPPFYLT && vilkårResultat.periodeFom == null) { + listeAvFeil.add("Vilkår '${vilkårResultat.vilkårType}' for barn med fødselsdato ${barn.fødselsdato.tilDagMånedÅr()} mangler fom dato.") + } + if (vilkårResultat.periodeFom != null && vilkårResultat.toPeriode().fom.isBefore(barn.fødselsdato)) { + listeAvFeil.add("Vilkår '${vilkårResultat.vilkårType}' for barn med fødselsdato ${barn.fødselsdato.tilDagMånedÅr()} har fra-og-med dato før barnets fødselsdato.") + } + if (vilkårResultat.periodeFom != null && + vilkårResultat.toPeriode().fom.isAfter(barn.fødselsdato.plusYears(18)) && + vilkårResultat.vilkårType == Vilkår.UNDER_18_ÅR && + vilkårResultat.erEksplisittAvslagPåSøknad != true + ) { + listeAvFeil.add("Vilkår '${vilkårResultat.vilkårType}' for barn med fødselsdato ${barn.fødselsdato.tilDagMånedÅr()} har fra-og-med dato etter barnet har fylt 18.") + } + } + } + + if (listeAvFeil.isNotEmpty()) { + throw FunksjonellFeil(listeAvFeil.joinToString(separator = "\n")) + } +} + +fun finnTilOgMedDato( + tilOgMed: LocalDate?, + vilkårResultater: List, +): LocalDate { + // LocalDateTimeline krasjer i isTimelineOutsideInterval funksjonen dersom vi sender med TIDENES_ENDE, + // så bruker tidenes ende minus én dag. + if (tilOgMed == null) return TIDENES_ENDE.minusDays(1) + val skalVidereføresEnMndEkstra = vilkårResultater.any { vilkårResultat -> + erBack2BackIMånedsskifte( + tilOgMed = tilOgMed, + fraOgMed = vilkårResultat.periodeFom, + ) + } + + return if (skalVidereføresEnMndEkstra) { + tilOgMed.plusMonths(1).sisteDagIMåned() + } else { + tilOgMed.sisteDagIMåned() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/domene/EndretUtbetalingAndel.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/domene/EndretUtbetalingAndel.kt new file mode 100644 index 000000000..6bd007f8a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/domene/EndretUtbetalingAndel.kt @@ -0,0 +1,255 @@ +package no.nav.familie.ba.sak.kjerne.endretutbetaling.domene + +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.YearMonthConverter +import no.nav.familie.ba.sak.common.erDagenFør +import no.nav.familie.ba.sak.common.overlapperHeltEllerDelvisMed +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.ekstern.restDomene.RestEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.beregning.domene.EndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertRestEndretAndel +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tilTidslinje +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "EndretUtbetalingAndel") +@Table(name = "ENDRET_UTBETALING_ANDEL") +data class EndretUtbetalingAndel( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "endret_utbetaling_andel_seq_generator") + @SequenceGenerator( + name = "endret_utbetaling_andel_seq_generator", + sequenceName = "endret_utbetaling_andel_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "fk_behandling_id", updatable = false, nullable = false) + val behandlingId: Long, + + @ManyToOne + @JoinColumn(name = "fk_po_person_id") + var person: Person? = null, + + @Column(name = "prosent") + var prosent: BigDecimal? = null, + + @Column(name = "fom", columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + var fom: YearMonth? = null, + + @Column(name = "tom", columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + var tom: YearMonth? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "aarsak") + var årsak: Årsak? = null, + + @Column(name = "avtaletidspunkt_delt_bosted") + var avtaletidspunktDeltBosted: LocalDate? = null, + + @Column(name = "soknadstidspunkt") + var søknadstidspunkt: LocalDate? = null, + + @Column(name = "begrunnelse") + var begrunnelse: String? = null, +) : BaseEntitet() { + + fun overlapperMed(periode: MånedPeriode) = periode.overlapperHeltEllerDelvisMed(this.periode) + + val periode + get(): MånedPeriode { + validerUtfyltEndring() + return MånedPeriode(this.fom!!, this.tom!!) + } + + fun validerUtfyltEndring(): Boolean { + if (manglerObligatoriskFelt() + ) { + val feilmelding = + "Person, prosent, fom, tom, årsak, begrunnese og søknadstidspunkt skal være utfylt: $this.tostring()" + throw FunksjonellFeil(melding = feilmelding, frontendFeilmelding = feilmelding) + } + + if (fom!! > tom!!) { + throw FunksjonellFeil( + melding = "fom må være lik eller komme før tom", + frontendFeilmelding = "Du kan ikke sette en f.o.m. dato som er etter t.o.m. dato", + ) + } + + if (årsak == Årsak.DELT_BOSTED && avtaletidspunktDeltBosted == null) { + throw FunksjonellFeil("Avtaletidspunkt skal være utfylt når årsak er delt bosted: $this.tostring()") + } + + return true + } + + fun manglerObligatoriskFelt() = person == null || + prosent == null || + fom == null || + tom == null || + årsak == null || + søknadstidspunkt == null || + (begrunnelse == null || begrunnelse!!.isEmpty()) + + fun årsakErDeltBosted() = this.årsak == Årsak.DELT_BOSTED +} + +enum class Årsak(val visningsnavn: String) { + DELT_BOSTED("Delt bosted"), + ETTERBETALING_3ÅR("Etterbetaling 3 år"), + ENDRE_MOTTAKER("Foreldrene bor sammen, endret mottaker"), + ALLEREDE_UTBETALT("Allerede utbetalt"), +} + +fun EndretUtbetalingAndelMedAndelerTilkjentYtelse.tilRestEndretUtbetalingAndel() = + RestEndretUtbetalingAndel( + id = this.id, + personIdent = this.aktivtFødselsnummer, + prosent = this.prosent, + fom = this.fom, + tom = this.tom, + årsak = this.årsak, + avtaletidspunktDeltBosted = this.avtaletidspunktDeltBosted, + søknadstidspunkt = this.søknadstidspunkt, + begrunnelse = this.begrunnelse, + erTilknyttetAndeler = this.andelerTilkjentYtelse.isNotEmpty(), + ) + +fun EndretUtbetalingAndel.fraRestEndretUtbetalingAndel( + restEndretUtbetalingAndel: RestEndretUtbetalingAndel, + person: Person, +): EndretUtbetalingAndel { + this.fom = restEndretUtbetalingAndel.fom + this.tom = restEndretUtbetalingAndel.tom + this.prosent = restEndretUtbetalingAndel.prosent ?: BigDecimal(0) + this.årsak = restEndretUtbetalingAndel.årsak + this.avtaletidspunktDeltBosted = restEndretUtbetalingAndel.avtaletidspunktDeltBosted + this.søknadstidspunkt = restEndretUtbetalingAndel.søknadstidspunkt + this.begrunnelse = restEndretUtbetalingAndel.begrunnelse + this.person = person + return this +} + +fun hentPersonerForEtterEndretUtbetalingsperiode( + minimerteEndredeUtbetalingAndeler: List, + fom: LocalDate?, + endringsaarsaker: Set<Årsak>, +) = minimerteEndredeUtbetalingAndeler.filter { endretUtbetalingAndel -> + endretUtbetalingAndel.periode.tom.sisteDagIInneværendeMåned() + .erDagenFør(fom) && + endringsaarsaker.contains(endretUtbetalingAndel.årsak) +}.map { it.personIdent } + +sealed interface IEndretUtbetalingAndel + +data class TomEndretUtbetalingAndel( + val id: Long, + val behandlingId: Long, +) : IEndretUtbetalingAndel + +sealed interface IUtfyltEndretUtbetalingAndel : IEndretUtbetalingAndel { + val id: Long + val behandlingId: Long + val person: Person + val prosent: BigDecimal + val fom: YearMonth + val tom: YearMonth + val årsak: Årsak + val søknadstidspunkt: LocalDate + val begrunnelse: String +} + +data class UtfyltEndretUtbetalingAndel( + override val id: Long, + override val behandlingId: Long, + override val person: Person, + override val prosent: BigDecimal, + override val fom: YearMonth, + override val tom: YearMonth, + override val årsak: Årsak, + override val søknadstidspunkt: LocalDate, + override val begrunnelse: String, +) : IUtfyltEndretUtbetalingAndel + +data class UtfyltEndretUtbetalingAndelDeltBosted( + override val id: Long, + override val behandlingId: Long, + override val person: Person, + override val prosent: BigDecimal, + override val fom: YearMonth, + override val tom: YearMonth, + override val årsak: Årsak, + override val søknadstidspunkt: LocalDate, + override val begrunnelse: String, + + val avtaletidspunktDeltBosted: LocalDate, +) : IUtfyltEndretUtbetalingAndel + +fun EndretUtbetalingAndel.tilIEndretUtbetalingAndel(): IEndretUtbetalingAndel { + return if (this.manglerObligatoriskFelt()) { + TomEndretUtbetalingAndel( + this.id, + this.behandlingId, + ) + } else { + if (this.årsakErDeltBosted()) { + UtfyltEndretUtbetalingAndelDeltBosted( + id = this.id, + behandlingId = this.behandlingId, + person = this.person!!, + prosent = this.prosent!!, + fom = this.fom!!, + tom = this.tom!!, + årsak = this.årsak!!, + avtaletidspunktDeltBosted = this.avtaletidspunktDeltBosted!!, + søknadstidspunkt = this.søknadstidspunkt!!, + begrunnelse = this.begrunnelse!!, + ) + } + + UtfyltEndretUtbetalingAndel( + id = this.id, + behandlingId = this.behandlingId, + person = this.person!!, + prosent = this.prosent!!, + fom = this.fom!!, + tom = this.tom!!, + årsak = this.årsak!!, + søknadstidspunkt = this.søknadstidspunkt!!, + begrunnelse = this.begrunnelse!!, + ) + } +} + +fun List.tilTidslinje() = + this.map { betalingAndel -> + Periode( + fraOgMed = betalingAndel.fom.tilTidspunkt(), + tilOgMed = betalingAndel.tom.tilTidspunkt(), + innhold = betalingAndel, + ) + }.tilTidslinje() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/domene/EndretUtbetalingAndelRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/domene/EndretUtbetalingAndelRepository.kt new file mode 100644 index 000000000..f7f9740dd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/domene/EndretUtbetalingAndelRepository.kt @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.kjerne.endretutbetaling.domene + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface EndretUtbetalingAndelRepository : JpaRepository { + + @Query("SELECT eua FROM EndretUtbetalingAndel eua WHERE eua.behandlingId = :behandlingId") + fun findByBehandlingId(behandlingId: Long): List +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/AndelTilkjentYtelsePraktiskLikhet.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/AndelTilkjentYtelsePraktiskLikhet.kt" new file mode 100644 index 000000000..cff4d205f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/AndelTilkjentYtelsePraktiskLikhet.kt" @@ -0,0 +1,24 @@ +package no.nav.familie.ba.sak.kjerne.eøs.differanseberegning + +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse + +object AndelTilkjentYtelsePraktiskLikhet { + internal fun Iterable.erIPraksisLik(oppdaterteAndeler: Iterable): Boolean { + val venstre = this.andelerSomKanSammenliknes().toSet() + val høyre = oppdaterteAndeler.andelerSomKanSammenliknes().toSet() + + return venstre == høyre + } + + internal fun Iterable.inneholderIPraksis(andelTilkjentYtelse: AndelTilkjentYtelse): Boolean { + return this.andelerSomKanSammenliknes().contains(andelTilkjentYtelse.andelSomKanSammenliknes()) + } + + private fun Iterable.andelerSomKanSammenliknes() = this.map { it.andelSomKanSammenliknes() } + + private fun AndelTilkjentYtelse.andelSomKanSammenliknes() = + copy( + id = 0, // Er med i hashCode, men ikke i equals i AndelTilkjentYtelse + // Andre felter som er funksjonelt viktige for praktisk likhet er med i equals og hashCode + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/Differanseberegning.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/Differanseberegning.kt" new file mode 100644 index 000000000..c18e9fdda --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/Differanseberegning.kt" @@ -0,0 +1,194 @@ +package no.nav.familie.ba.sak.kjerne.eøs.differanseberegning + +import minsteAvHver +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.beregning.kunAndelerTilOgMed3År +import no.nav.familie.ba.sak.kjerne.beregning.tilAndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.tilAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.tilSeparateTidslinjerForBarna +import no.nav.familie.ba.sak.kjerne.beregning.tilTidslinjeForSøkersYtelse +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.tilKronerPerValutaenhet +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.tilMånedligValutabeløp +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.times +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.tilSeparateTidslinjerForBarna +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat.NORGE_ER_SEKUNDÆRLAND +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløp +import no.nav.familie.ba.sak.kjerne.eøs.valutakurs.Valutakurs +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerHverKunVerdi +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerKunVerdiMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNullMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNullOgIkkeTom +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.leftJoin +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.outerJoin +import no.nav.familie.ba.sak.kjerne.tidslinje.matematikk.minus +import no.nav.familie.ba.sak.kjerne.tidslinje.matematikk.rundAvTilHeltall +import no.nav.familie.ba.sak.kjerne.tidslinje.matematikk.sum +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import java.math.BigDecimal +import java.math.MathContext + +/** + * ADVARSEL: Muterer TilkjentYtelse + * Denne BURDE gjøres ikke-muterbar og returnere en ny instans av TilkjentYtelse + * Muteringen skyldes at TilkjentYtelse er under JPA-kontekst og ikke "tåler" copy(andelerTilkjentYtelse = ...) + * Starten på én løsning er at EndretUtebetalingPeriode kobles løs fra AndelTilkjentYtelse og kobles rett på behandlingen + */ +fun beregnDifferanse( + andelerTilkjentYtelse: Collection, + utenlandskePeriodebeløp: Collection, + valutakurser: Collection, +): List { + val utenlandskePeriodebeløpTidslinjer = utenlandskePeriodebeløp.tilSeparateTidslinjerForBarna() + val valutakursTidslinjer = valutakurser.tilSeparateTidslinjerForBarna() + val andelTilkjentYtelseTidslinjer = andelerTilkjentYtelse.tilSeparateTidslinjerForBarna() + + val barnasUtenlandskePeriodebeløpINorskeKronerTidslinjer = + utenlandskePeriodebeløpTidslinjer.outerJoin(valutakursTidslinjer) { upb, valutakurs -> + upb.tilMånedligValutabeløp() * valutakurs.tilKronerPerValutaenhet() + } + + val barnasDifferanseberegneteAndelTilkjentYtelseTidslinjer = + andelTilkjentYtelseTidslinjer.outerJoin(barnasUtenlandskePeriodebeløpINorskeKronerTidslinjer) { aty, beløp -> + aty.oppdaterDifferanseberegning(beløp) + } + + val barnasAndeler = barnasDifferanseberegneteAndelTilkjentYtelseTidslinjer.tilAndelerTilkjentYtelse() + val søkersAndeler = andelerTilkjentYtelse.filter { it.erSøkersAndel() } + + return søkersAndeler + barnasAndeler +} + +/** + * ADVARSEL: Muterer TilkjentYtelse + * Differanseberegner søkers ytelser, dvs utvidet barnetrygd og småbarnstillegg + * Forutsetningen er at barnas andeler allerede er differanseberegnet + * Funksjonen returnerer det nye settet av andeler tilkjent ytelse, inklusive barnas + */ +fun Collection.differanseberegnSøkersYtelser( + barna: List, + kompetanser: Collection, +): List { + // Ta bort eventuell eksisterende differanseberegning, slik at kalkulertUtbetalingsbeløp er nasjonal sats + // Men behold funksjonelle splitter som er påført tidligere ved å beholde fom og tom på andelene + val utvidetBarnetrygdTidslinje = this.tilTidslinjeForSøkersYtelse(YtelseType.UTVIDET_BARNETRYGD) + .utenDifferanseberegning() + + val småbarnstilleggTidslinje = this.tilTidslinjeForSøkersYtelse(YtelseType.SMÅBARNSTILLEGG) + .utenDifferanseberegning() + + val barnasAndelerTidslinjer = this.tilSeparateTidslinjerForBarna() + + // Finn alle andelene frem til barna er 18 år. Det vil i praksis være ALLE andelene + // Bruk bare andelene der perioden BARE inneholder sekundærlandsandeler + val barnasRelevanteAndelerInntil18År = barnasAndelerTidslinjer + .kunReneSekundærlandsperioder(kompetanser) + + // Lag tidslinjer for hvert barn som inneholder underskuddet fra differanseberegningen på ordinær barnetrygd. + // Resultatet er tidslinjer med underskuddet som positivt beløp der det inntreffer + // Dette er det utenlandske beløpet som gjenstår til å redusere søkers ytelser + val barnasUnderskuddPåDifferanseberegningTidslinjer = + barnasAndelerTidslinjer.tilUnderskuddPåDifferanseberegningen() + + // Vi finner hvor mye hvert barn skal ha som andel av utvidet barnetrygd på hvert tidspunkt. + // Det tilsvarer utvidet barnetrygd på et gitt tidspunkt delt på antall relevante barn som har ytelse på det tidspunktet + val barnasDelAvUtvidetBarnetrygdTidslinjer = + utvidetBarnetrygdTidslinje.fordelBeløpPåBarnaMedAndeler(barnasRelevanteAndelerInntil18År) + + // Vi finner den utenlandske delen av utvidet barnetrygd, + // som det minste av barnets underskudd og dets del av utvidet barnetrygd, og summerer resultatet for alle barna + // Runder av summen HALF_UP, som betyr en mulig ulempe for søker + // Avrundingen er valgt for i størst mulig grad få summen til å bli lik utvidet barnetrygd når alle barnas deler blir brukt, + // slik at utvidet barnetrrygd blir 0. Ellers ville den av og til kunne blitt 1, selv om det var ytterligere underskudd + val utenlandskDelAvUtvidetBarnetrygdTidslinje = minsteAvHver( + barnasDelAvUtvidetBarnetrygdTidslinjer, + barnasUnderskuddPåDifferanseberegningTidslinjer, + ).sum().rundAvTilHeltall() + + // Til slutt oppdaterer vi differanseberegningen på utvidet barnetrygd med den utenlandske delen + val differanseberegnetUtvidetBarnetrygdTidslinje = + utvidetBarnetrygdTidslinje.oppdaterDifferanseberegning(utenlandskDelAvUtvidetBarnetrygdTidslinje) + + // For hvert barn finner vi ut hvor mye underskudd som gjenstår etter at delen av utvidet barnetrygd er trukket fra + val barnasGjenståendeUnderskuddTidslinjer = barnasUnderskuddPåDifferanseberegningTidslinjer + .minus(barnasDelAvUtvidetBarnetrygdTidslinjer) + .filtrerHverKunVerdi { it > BigDecimal.ZERO } + + // For hvert barn kombiner andel-tidslinjen med 3-års-tidslinjen. Resultatet er andelene når barna er inntil 3 år + // Bruk bare andelene der perioden BARE inneholder sekundærlandsandeler + // Må ta utgangspunkt i alle andeler for være sikker på at vi vurderer sekundærlandsperioder bare når barna er inntil 3 år + val barnasRelevanteAndelerInntil3ÅrTidslinjer = barnasAndelerTidslinjer + .kunAndelerTilOgMed3År(barna) + .kunReneSekundærlandsperioder(kompetanser) + + // Vi finner hvor mye hvert barn skal ha som andel av småbarnstillegget på hvert tidspunkt. + // Det tilsvarer småbarnstillegget på et gitt tidspunkt delt på antall relevante barn under 3 år som har ytelse på det tidspunktet + val barnasDelAvSmåbarnstilleggetTidslinjer = + småbarnstilleggTidslinje.fordelBeløpPåBarnaMedAndeler(barnasRelevanteAndelerInntil3ÅrTidslinjer) + + // Vi finner den utenlandske delen av småbarnstillegget + // som det minste av barnets underskudd og dets del av småbarnstillegget, og summerer resultatet for alle barna. + // Runder av HALF_UP, som betyr en mulig ulempe for søker + // Avrundingen er valgt for i størst mulig grad få summen til å bli lik småbarnstillegget når alle barnas deler blir brukt, + // slik at småbarnstillegget blir 0. Ellers ville den av og til kunne blitt 1, selv om det var ytterligere underskudd + val utenlandskDelAvSmåbarnstilleggTidslinje = minsteAvHver( + barnasDelAvSmåbarnstilleggetTidslinjer, + barnasGjenståendeUnderskuddTidslinjer, + ).sum().rundAvTilHeltall() + + // Til slutt oppdaterer vi differanseberegningen på småbarnstillegget med den utenlandske delen + val differanseberegnetSmåbarnstilleggTidslinje = + småbarnstilleggTidslinje.oppdaterDifferanseberegning(utenlandskDelAvSmåbarnstilleggTidslinje) + + // Returner det fulle settet av andeler, både barnas andeler og de potensielt nye andelene for søkers ytelser + return this.filter { !it.erSøkersAndel() } + + differanseberegnetUtvidetBarnetrygdTidslinje.tilAndelTilkjentYtelse() + + differanseberegnetSmåbarnstilleggTidslinje.tilAndelTilkjentYtelse() +} + +/** + * Funksjon som sjekker at hvert barns andel i en periode er vurdert med sekundærland-kompetanser + * Hvis ja beholdes andelene for alle barna + * Hvis nei fjernes alle andelene, slik at perioden ikke har noen andeler + */ +fun Map>.kunReneSekundærlandsperioder( + kompetanser: Collection, +): Map> { + val barnasKompetanseTidslinjer = kompetanser.tilSeparateTidslinjerForBarna() + + val barnasErSekundærlandTidslinjer = + this.leftJoin(barnasKompetanseTidslinjer) { andel, kompetanse -> + when { + andel != null && kompetanse != null -> kompetanse.resultat == NORGE_ER_SEKUNDÆRLAND + andel != null -> false + else -> null + } + } + + val kunSekundærlandsperiodeTidslinje = + barnasErSekundærlandTidslinjer.values.kombinerUtenNullOgIkkeTom { erSekundærlandListe -> + erSekundærlandListe.all { it } + } + + return this.kombinerKunVerdiMed(kunSekundærlandsperiodeTidslinje) { andel, erSekundærland -> + andel.takeIf { erSekundærland } + } +} + +fun Tidslinje.fordelBeløpPåBarnaMedAndeler( + barnasAndeler: Map>, +): Map> { + val antallAktørerMedYtelseTidslinje = + barnasAndeler.values.kombinerUtenNullOgIkkeTom { it.count() } + + val ytelsePerBarnTidslinje = + this.kombinerUtenNullMed(antallAktørerMedYtelseTidslinje) { andel, antall -> + andel.kalkulertUtbetalingsbeløp.toBigDecimal().divide(antall.toBigDecimal(), MathContext.DECIMAL32) + } + + return barnasAndeler.kombinerKunVerdiMed(ytelsePerBarnTidslinje) { _, ytelsePerBarn -> ytelsePerBarn } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/DifferanseberegningsUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/DifferanseberegningsUtils.kt" new file mode 100644 index 000000000..4a783562c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/DifferanseberegningsUtils.kt" @@ -0,0 +1,111 @@ +package no.nav.familie.ba.sak.kjerne.eøs.differanseberegning + +import no.nav.familie.ba.sak.common.del +import no.nav.familie.ba.sak.common.multipliser +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.medPeriode +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Intervall +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrer +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.mapIkkeNull +import java.math.BigDecimal + +fun Intervall.konverterBeløpTilMånedlig(beløp: BigDecimal): BigDecimal = + when (this) { + Intervall.ÅRLIG -> beløp.del(12.toBigDecimal(), 10) + Intervall.KVARTALSVIS -> beløp.del(3.toBigDecimal(), 10) + Intervall.MÅNEDLIG -> beløp + Intervall.UKENTLIG -> beløp.multipliser(4.35.toBigDecimal(), 10) + }.stripTrailingZeros().toPlainString().toBigDecimal() + +/** + * Kalkulerer nytt utbetalingsbeløp fra [utenlandskPeriodebeløpINorskeKroner] + * Beløpet konverteres fra desimaltall til heltall ved å strippe desimalene, og dermed øke den norske ytelsen med inntil én krone + * Må håndtere tilfellet der [kalkulertUtebetalngsbeløp] blir modifisert andre steder i koden, men antar at det aldri vil være negativt + * [nasjonaltPeriodebeløp] settes til den originale, nasjonale beregningen (aldri negativt) + * [differanseberegnetBeløp] er differansen mellom [nasjonaltPeriodebeløp] og (avrundet) [utenlandskPeriodebeløpINorskeKroner] (kan bli negativt) + * [kalkulertUtebetalngsbeløp] blir satt til [differanseberegnetBeløp], med mindre det er negativt. Da blir det 0 (null) + * Hvis [utenlandskPeriodebeløpINorskeKroner] er , så skal utbetalingsbeløpet reverteres til det originale nasjonale beløpet + */ +fun AndelTilkjentYtelse?.oppdaterDifferanseberegning( + utenlandskPeriodebeløpINorskeKroner: BigDecimal?, +): AndelTilkjentYtelse? { + val nyAndelTilkjentYtelse = when { + this == null -> null + utenlandskPeriodebeløpINorskeKroner == null -> this.utenDifferanseberegning() + else -> this.medDifferanseberegning(utenlandskPeriodebeløpINorskeKroner) + } + + return nyAndelTilkjentYtelse +} + +fun AndelTilkjentYtelse.medDifferanseberegning( + utenlandskPeriodebeløpINorskeKroner: BigDecimal, +): AndelTilkjentYtelse { + val avrundetUtenlandskPeriodebeløp = utenlandskPeriodebeløpINorskeKroner + .toBigInteger().intValueExact() // Fjern desimaler for å gi fordel til søker + + val nyttDifferanseberegnetBeløp = ( + nasjonaltPeriodebeløp + ?: kalkulertUtbetalingsbeløp + ) - avrundetUtenlandskPeriodebeløp + + return copy( + id = 0, + kalkulertUtbetalingsbeløp = maxOf(nyttDifferanseberegnetBeløp, 0), + differanseberegnetPeriodebeløp = nyttDifferanseberegnetBeløp, + ) +} + +private fun AndelTilkjentYtelse.utenDifferanseberegning(): AndelTilkjentYtelse { + return copy( + id = 0, + kalkulertUtbetalingsbeløp = nasjonaltPeriodebeløp ?: this.kalkulertUtbetalingsbeløp, + differanseberegnetPeriodebeløp = null, + ) +} + +/** + * Gjør et forsøk på fjerne differanseberegning på andelen, samtidig som tidligere, funksjonelle splitter bevares + * Det kan være en funksjonell grunn til at en splitt finnes, selv om nabo-andelene ellers like, f.eks + * endring i overgangsstønad, som ikke fører til endring i småbarnstillegget. Splitten er nødvendig for riktige vedtaksperioder + * Splitten opprettholdes når fom og tom er satt på andelen og er forskjellig fra fom og tom på nabo-andelen. Det vil gjelde søkers ytelser + * Ved å sette fom og tom til på alle andeler som har differanseberegning, vil naboer slås sammen hvis de ellers er like + * Det er en potensiell bug her: Det er en mulighet for at en funksjonell splitt i andeler + * sammenfaller med splitt pga differanseberegning, og blir fjernet + * Det beste hadde vært om andelene IKKE inneholdt splitter av denne typen, men at ekstra splitter ble utledet der de trengs + */ +fun Tidslinje.utenDifferanseberegning() = + mapIkkeNull { + when { + it.differanseberegnetPeriodebeløp != null -> it.medPeriode(null, null) + else -> it + } + }.mapIkkeNull { it.utenDifferanseberegning() } + +fun Tidslinje.oppdaterDifferanseberegning( + utenlandskBeløpINorskeKronerTidslinje: Tidslinje, +): Tidslinje { + return this.kombinerMed(utenlandskBeløpINorskeKronerTidslinje) { andel, utenlandskBeløpINorskeKroner -> + andel.oppdaterDifferanseberegning(utenlandskBeløpINorskeKroner) + } +} + +/** + * Konverterer negativt differanseberegnet periodebeløp på andelene til underskudd som positiv BigDecimal + * Altså: + * AndelTilkjentYtelse{ differanseberegnetPeriodebeløp: -700 } => BigDecimal{ 700 } + * AndelTilkjentYtelse{ differanseberegnetPeriodebeløp: 200 } => null + * AndelTilkjentYtelse{ differanseberegnetPeriodebeløp: null } => null + */ +fun Map>.tilUnderskuddPåDifferanseberegningen(): Map> = + mapValues { (_, tidslinje) -> + tidslinje + .mapIkkeNull { innhold -> innhold.differanseberegnetPeriodebeløp } + .mapIkkeNull { maxOf(-it, 0) } + .filtrer { it != null && it > 0 } + .mapIkkeNull { it.toBigDecimal() } + } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/TilpassDifferanseberegningService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/TilpassDifferanseberegningService.kt" new file mode 100644 index 000000000..bf6016c02 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/TilpassDifferanseberegningService.kt" @@ -0,0 +1,107 @@ +package no.nav.familie.ba.sak.kjerne.eøs.differanseberegning + +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseEndretAbonnent +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.oppdaterTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEndringAbonnent +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.KompetanseRepository +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløp +import no.nav.familie.ba.sak.kjerne.eøs.valutakurs.Valutakurs +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface BarnasDifferanseberegningEndretAbonnent { + fun barnasDifferanseberegningEndret(tilkjentYtelse: TilkjentYtelse) +} + +@Service +class TilpassDifferanseberegningEtterTilkjentYtelseService( + private val valutakursRepository: PeriodeOgBarnSkjemaRepository, + private val utenlandskPeriodebeløpRepository: PeriodeOgBarnSkjemaRepository, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + private val barnasDifferanseberegningEndretAbonnenter: List, +) : TilkjentYtelseEndretAbonnent { + + @Transactional + override fun endretTilkjentYtelse(tilkjentYtelse: TilkjentYtelse) { + val behandlingId = BehandlingId(tilkjentYtelse.behandling.id) + val valutakurser = valutakursRepository.finnFraBehandlingId(behandlingId.id) + val utenlandskePeriodebeløp = utenlandskPeriodebeløpRepository.finnFraBehandlingId(behandlingId.id) + + val oppdaterteAndeler = beregnDifferanse( + tilkjentYtelse.andelerTilkjentYtelse, + utenlandskePeriodebeløp, + valutakurser, + ) + + val oppdatertTilkjentYtelse = tilkjentYtelseRepository.oppdaterTilkjentYtelse(tilkjentYtelse, oppdaterteAndeler) + barnasDifferanseberegningEndretAbonnenter.forEach { it.barnasDifferanseberegningEndret(oppdatertTilkjentYtelse) } + } +} + +@Service +class TilpassDifferanseberegningEtterUtenlandskPeriodebeløpService( + private val valutakursRepository: PeriodeOgBarnSkjemaRepository, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + private val barnasDifferanseberegningEndretAbonnenter: List, +) : PeriodeOgBarnSkjemaEndringAbonnent { + @Transactional + override fun skjemaerEndret( + behandlingId: BehandlingId, + utenlandskePeriodebeløp: Collection, + ) { + val tilkjentYtelse = tilkjentYtelseRepository.findByBehandlingOptional(behandlingId.id) ?: return + val valutakurser = valutakursRepository.finnFraBehandlingId(behandlingId.id) + + val oppdaterteAndeler = beregnDifferanse( + tilkjentYtelse.andelerTilkjentYtelse, + utenlandskePeriodebeløp, + valutakurser, + ) + + val oppdatertTilkjentYtelse = tilkjentYtelseRepository.oppdaterTilkjentYtelse(tilkjentYtelse, oppdaterteAndeler) + barnasDifferanseberegningEndretAbonnenter.forEach { it.barnasDifferanseberegningEndret(oppdatertTilkjentYtelse) } + } +} + +@Service +class TilpassDifferanseberegningEtterValutakursService( + private val utenlandskPeriodebeløpRepository: PeriodeOgBarnSkjemaRepository, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + private val barnasDifferanseberegningEndretAbonnenter: List, +) : PeriodeOgBarnSkjemaEndringAbonnent { + + @Transactional + override fun skjemaerEndret(behandlingId: BehandlingId, valutakurser: Collection) { + val tilkjentYtelse = tilkjentYtelseRepository.findByBehandlingOptional(behandlingId.id) ?: return + val utenlandskePeriodebeløp = utenlandskPeriodebeløpRepository.finnFraBehandlingId(behandlingId.id) + + val oppdaterteAndeler = beregnDifferanse( + tilkjentYtelse.andelerTilkjentYtelse, + utenlandskePeriodebeløp, + valutakurser, + ) + + val oppdatertTilkjentYtelse = tilkjentYtelseRepository.oppdaterTilkjentYtelse(tilkjentYtelse, oppdaterteAndeler) + barnasDifferanseberegningEndretAbonnenter.forEach { it.barnasDifferanseberegningEndret(oppdatertTilkjentYtelse) } + } +} + +@Service +class TilpassDifferanseberegningSøkersYtelserService( + private val persongrunnlagService: PersongrunnlagService, + private val kompetanseRepository: KompetanseRepository, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, +) : BarnasDifferanseberegningEndretAbonnent { + override fun barnasDifferanseberegningEndret(tilkjentYtelse: TilkjentYtelse) { + val oppdaterteAndeler = tilkjentYtelse.andelerTilkjentYtelse.differanseberegnSøkersYtelser( + barna = persongrunnlagService.hentBarna(tilkjentYtelse.behandling.id), + kompetanser = kompetanseRepository.finnFraBehandlingId(tilkjentYtelse.behandling.id), + ) + tilkjentYtelseRepository.oppdaterTilkjentYtelse(tilkjentYtelse, oppdaterteAndeler) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/domene/Intervall.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/domene/Intervall.kt" new file mode 100644 index 000000000..87f05f363 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/domene/Intervall.kt" @@ -0,0 +1,8 @@ +package no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene + +enum class Intervall { + ÅRLIG, + KVARTALSVIS, + MÅNEDLIG, + UKENTLIG, +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/domene/Valutaberegning.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/domene/Valutaberegning.kt" new file mode 100644 index 000000000..6b53452e3 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/domene/Valutaberegning.kt" @@ -0,0 +1,43 @@ +package no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene + +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløp +import no.nav.familie.ba.sak.kjerne.eøs.valutakurs.Valutakurs +import java.math.BigDecimal + +data class KronerPerValutaenhet( + val kronerPerValutaenhet: BigDecimal, + val valutakode: String, +) + +data class Valutabeløp( + val beløp: BigDecimal, + val valutakode: String, +) + +operator fun Valutabeløp?.times(kronerPerValutaenhet: KronerPerValutaenhet?): BigDecimal? { + if (this == null || kronerPerValutaenhet == null) { + return null + } + + if (this.valutakode != kronerPerValutaenhet.valutakode) { + return null + } + + return this.beløp * kronerPerValutaenhet.kronerPerValutaenhet +} + +fun UtenlandskPeriodebeløp?.tilMånedligValutabeløp(): Valutabeløp? { + if (this?.kalkulertMånedligBeløp == null || this.valutakode == null) { + return null + } + + return Valutabeløp(this.kalkulertMånedligBeløp, this.valutakode) +} + +fun Valutakurs?.tilKronerPerValutaenhet(): KronerPerValutaenhet? { + if (this?.kurs == null || this.valutakode == null) { + return null + } + + return KronerPerValutaenhet(this.kurs, this.valutakode) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/endringsabonnement/TilbakestillBehandling.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/endringsabonnement/TilbakestillBehandling.kt" new file mode 100644 index 000000000..a99716dfd --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/endringsabonnement/TilbakestillBehandling.kt" @@ -0,0 +1,39 @@ +package no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement + +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEndringAbonnent +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløp +import no.nav.familie.ba.sak.kjerne.eøs.valutakurs.Valutakurs +import no.nav.familie.ba.sak.kjerne.steg.TilbakestillBehandlingTilBehandlingsresultatService +import org.springframework.stereotype.Service + +@Service +class TilbakestillBehandlingFraKompetanseEndringService( + private val tilbakestillBehandlingTilBehandlingsresultatService: TilbakestillBehandlingTilBehandlingsresultatService, +) : PeriodeOgBarnSkjemaEndringAbonnent { + override fun skjemaerEndret(behandlingId: BehandlingId, endretTil: Collection) { + tilbakestillBehandlingTilBehandlingsresultatService + .tilbakestillBehandlingTilBehandlingsresultat(behandlingId.id) + } +} + +@Service +class TilbakestillBehandlingFraUtenlandskPeriodebeløpEndringService( + private val tilbakestillBehandlingTilBehandlingsresultatService: TilbakestillBehandlingTilBehandlingsresultatService, +) : PeriodeOgBarnSkjemaEndringAbonnent { + override fun skjemaerEndret(behandlingId: BehandlingId, endretTil: Collection) { + tilbakestillBehandlingTilBehandlingsresultatService + .tilbakestillBehandlingTilBehandlingsresultat(behandlingId.id) + } +} + +@Service +class TilbakestillBehandlingFraValutakursEndringService( + private val tilbakestillBehandlingTilBehandlingsresultatService: TilbakestillBehandlingTilBehandlingsresultatService, +) : PeriodeOgBarnSkjemaEndringAbonnent { + override fun skjemaerEndret(behandlingId: BehandlingId, endretTil: Collection) { + tilbakestillBehandlingTilBehandlingsresultatService + .tilbakestillBehandlingTilBehandlingsresultat(behandlingId.id) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/endringsabonnement/TilpassKompetanserService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/endringsabonnement/TilpassKompetanserService.kt" new file mode 100644 index 000000000..749d246b0 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/endringsabonnement/TilpassKompetanserService.kt" @@ -0,0 +1,195 @@ +package no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement + +import no.nav.familie.ba.sak.config.FeatureToggleConfig.Companion.ENDRET_EØS_REGELVERKFILTER_FOR_BARN +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelerOppdatertAbonnent +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEndringAbonnent +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaService +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.tilSeparateTidslinjerForBarna +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.tilSkjemaer +import no.nav.familie.ba.sak.kjerne.eøs.felles.medBehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.util.replaceLast +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.EndretUtbetalingAndelTidslinjeService +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.KombinertRegelverkResultat +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat.OPPFYLT_BLANDET_REGELVERK +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjeService +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.tilBarnasSkalIkkeUtbetalesTidslinjer +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrer +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.fraOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.leftJoin +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.outerJoin +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.tilOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.unleash.UnleashService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class TilpassKompetanserTilRegelverkService( + private val vilkårsvurderingTidslinjeService: VilkårsvurderingTidslinjeService, + private val endretUtbetalingAndelTidslinjeService: EndretUtbetalingAndelTidslinjeService, + private val unleashNext: UnleashService, + kompetanseRepository: PeriodeOgBarnSkjemaRepository, + endringsabonnenter: Collection>, +) { + val skjemaService = PeriodeOgBarnSkjemaService( + kompetanseRepository, + endringsabonnenter, + ) + + @Transactional + fun tilpassKompetanserTilRegelverk(behandlingId: BehandlingId) { + val gjeldendeKompetanser = skjemaService.hentMedBehandlingId(behandlingId) + val barnasRegelverkResultatTidslinjer = + vilkårsvurderingTidslinjeService.hentBarnasRegelverkResultatTidslinjer(behandlingId) + + val barnasSkalIkkeUtbetalesTidslinjer = + endretUtbetalingAndelTidslinjeService.hentBarnasSkalIkkeUtbetalesTidslinjer(behandlingId) + + val annenForelderOmfattetAvNorskLovgivningTidslinje = + vilkårsvurderingTidslinjeService.hentAnnenForelderOmfattetAvNorskLovgivningTidslinje(behandlingId = behandlingId) + + val oppdaterteKompetanser = tilpassKompetanserTilRegelverk( + gjeldendeKompetanser, + barnasRegelverkResultatTidslinjer, + barnasSkalIkkeUtbetalesTidslinjer, + annenForelderOmfattetAvNorskLovgivningTidslinje, + brukBarnetsRegelverkVedBlandetResultat = unleashNext.isEnabled(ENDRET_EØS_REGELVERKFILTER_FOR_BARN), + ).medBehandlingId(behandlingId) + + skjemaService.lagreDifferanseOgVarsleAbonnenter(behandlingId, gjeldendeKompetanser, oppdaterteKompetanser) + } +} + +@Service +class TilpassKompetanserTilEndretUtebetalingAndelerService( + private val vilkårsvurderingTidslinjeService: VilkårsvurderingTidslinjeService, + private val unleashNext: UnleashService, + kompetanseRepository: PeriodeOgBarnSkjemaRepository, + endringsabonnenter: Collection>, +) : EndretUtbetalingAndelerOppdatertAbonnent { + val skjemaService = PeriodeOgBarnSkjemaService( + kompetanseRepository, + endringsabonnenter, + ) + + @Transactional + override fun endretUtbetalingAndelerOppdatert( + behandlingIdLong: Long, + endretUtbetalingAndeler: List, + ) { + val behandlingId = BehandlingId(behandlingIdLong) + val gjeldendeKompetanser = skjemaService.hentMedBehandlingId(behandlingId) + val barnasRegelverkResultatTidslinjer = + vilkårsvurderingTidslinjeService.hentBarnasRegelverkResultatTidslinjer(behandlingId) + + val barnasSkalIkkeUtbetalesTidslinjer = endretUtbetalingAndeler + .tilBarnasSkalIkkeUtbetalesTidslinjer() + + val annenForelderOmfattetAvNorskLovgivningTidslinje = + vilkårsvurderingTidslinjeService.hentAnnenForelderOmfattetAvNorskLovgivningTidslinje(behandlingId = behandlingId) + + val oppdaterteKompetanser = tilpassKompetanserTilRegelverk( + gjeldendeKompetanser, + barnasRegelverkResultatTidslinjer, + barnasSkalIkkeUtbetalesTidslinjer, + annenForelderOmfattetAvNorskLovgivningTidslinje, + brukBarnetsRegelverkVedBlandetResultat = unleashNext.isEnabled(ENDRET_EØS_REGELVERKFILTER_FOR_BARN), + ).medBehandlingId(behandlingId) + + skjemaService.lagreDifferanseOgVarsleAbonnenter(behandlingId, gjeldendeKompetanser, oppdaterteKompetanser) + } +} + +fun tilpassKompetanserTilRegelverk( + gjeldendeKompetanser: Collection, + barnaRegelverkTidslinjer: Map>, + barnasSkalIkkeUtbetalesTidslinjer: Map>, + annenForelderOmfattetAvNorskLovgivningTidslinje: Tidslinje = TomTidslinje(), + brukBarnetsRegelverkVedBlandetResultat: Boolean = true, +): Collection { + val barnasEøsRegelverkTidslinjer = barnaRegelverkTidslinjer.tilBarnasEøsRegelverkTidslinjer( + brukBarnetsRegelverkVedBlandetResultat, + ) + .leftJoin(barnasSkalIkkeUtbetalesTidslinjer) { regelverk, harEtterbetaling3År -> + when (harEtterbetaling3År) { + true -> null // ta bort regelverk hvis barnet har etterbetaling 3 år + else -> regelverk + } + } + + return gjeldendeKompetanser.tilSeparateTidslinjerForBarna() + .outerJoin(barnasEøsRegelverkTidslinjer) { kompetanse, regelverk -> + regelverk?.let { kompetanse ?: Kompetanse.NULL } + } + .mapValues { (_, value) -> + value.kombinerMed(annenForelderOmfattetAvNorskLovgivningTidslinje) { kompetanse, annenForelderOmfattet -> + kompetanse?.copy(erAnnenForelderOmfattetAvNorskLovgivning = annenForelderOmfattet ?: false) + } + } + .tilSkjemaer() +} + +fun VilkårsvurderingTidslinjeService.hentBarnasRegelverkResultatTidslinjer(behandlingId: BehandlingId) = + this.hentTidslinjerThrows(behandlingId).barnasTidslinjer() + .mapValues { (_, tidslinjer) -> + tidslinjer.regelverkResultatTidslinje + } + +private fun Map>.tilBarnasEøsRegelverkTidslinjer( + brukBarnetsRegelverkVedBlandetResultat: Boolean, +) = + this.mapValues { (_, tidslinjer) -> + tidslinjer.mapTilRegelverk(brukBarnetsRegelverkVedBlandetResultat) + .filtrer { it == Regelverk.EØS_FORORDNINGEN } + .filtrerIkkeNull() + .forlengFremtidTilUendelig(MånedTidspunkt.nå()) + } + +private fun Tidslinje.mapTilRegelverk(brukBarnetsRegelverkVedBlandetResultat: Boolean) = + map { + if (it?.kombinertResultat == OPPFYLT_BLANDET_REGELVERK && brukBarnetsRegelverkVedBlandetResultat) { + it.barnetsResultat?.regelverk + } else { + it?.kombinertResultat?.regelverk + } + } + +private fun Tidslinje.forlengFremtidTilUendelig(nå: Tidspunkt): Tidslinje { + val tilOgMed = this.tilOgMed() + return if (tilOgMed != null && tilOgMed > nå) { + this.flyttTilOgMed(tilOgMed.somUendeligLengeTil()) + } else { + this + } +} + +private fun Tidslinje.flyttTilOgMed(tilTidspunkt: Tidspunkt): Tidslinje { + val tidslinje = this + val fraOgMed = tidslinje.fraOgMed() + + return if (fraOgMed == null || tilTidspunkt < fraOgMed) { + TomTidslinje() + } else { + object : Tidslinje() { + override fun lagPerioder(): Collection> = tidslinje.perioder() + .filter { it.fraOgMed <= tilTidspunkt } + .replaceLast { Periode(it.fraOgMed, tilTidspunkt, it.innhold) } + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/endringsabonnement/TilpassUtenlandskePeriodebel\303\270pTilKompetanserService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/endringsabonnement/TilpassUtenlandskePeriodebel\303\270pTilKompetanserService.kt" new file mode 100644 index 000000000..9dc17266c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/endringsabonnement/TilpassUtenlandskePeriodebel\303\270pTilKompetanserService.kt" @@ -0,0 +1,85 @@ +package no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement + +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.FinnPeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEndringAbonnent +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaService +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.tilSeparateTidslinjerForBarna +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.tilSkjemaer +import no.nav.familie.ba.sak.kjerne.eøs.felles.medBehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløp +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrer +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.outerJoin +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class TilpassUtenlandskePeriodebeløpTilKompetanserService( + utenlandskPeriodebeløpRepository: PeriodeOgBarnSkjemaRepository, + endringsabonnenter: Collection>, + private val kompetanseRepository: FinnPeriodeOgBarnSkjemaRepository, +) : PeriodeOgBarnSkjemaEndringAbonnent { + val skjemaService = PeriodeOgBarnSkjemaService( + utenlandskPeriodebeløpRepository, + endringsabonnenter, + ) + + @Transactional + fun tilpassUtenlandskPeriodebeløpTilKompetanser(behandlingId: BehandlingId) { + val gjeldendeKompetanser = kompetanseRepository.finnFraBehandlingId(behandlingId.id) + + tilpassUtenlandskPeriodebeløpTilKompetanser(behandlingId, gjeldendeKompetanser) + } + + @Transactional + override fun skjemaerEndret(behandlingId: BehandlingId, endretTil: Collection) { + tilpassUtenlandskPeriodebeløpTilKompetanser(behandlingId, endretTil) + } + + private fun tilpassUtenlandskPeriodebeløpTilKompetanser( + behandlingId: BehandlingId, + gjeldendeKompetanser: Collection, + ) { + val forrigeUtenlandskePeriodebeløp = skjemaService.hentMedBehandlingId(behandlingId) + + val oppdaterteUtenlandskPeriodebeløp = tilpassUtenlandskePeriodebeløpTilKompetanser( + forrigeUtenlandskePeriodebeløp, + gjeldendeKompetanser, + ).medBehandlingId(behandlingId) + + skjemaService.lagreDifferanseOgVarsleAbonnenter( + behandlingId, + forrigeUtenlandskePeriodebeløp, + oppdaterteUtenlandskPeriodebeløp, + ) + } +} + +internal fun tilpassUtenlandskePeriodebeløpTilKompetanser( + forrigeUtenlandskePeriodebeløp: Iterable, + gjeldendeKompetanser: Iterable, +): Collection { + val barnasKompetanseTidslinjer = gjeldendeKompetanser + .tilSeparateTidslinjerForBarna() + .filtrerSekundærland() + + return forrigeUtenlandskePeriodebeløp.tilSeparateTidslinjerForBarna() + .outerJoin(barnasKompetanseTidslinjer) { upb, kompetanse -> + when { + kompetanse == null -> null + upb == null || upb.utbetalingsland != kompetanse.annenForeldersAktivitetsland -> + UtenlandskPeriodebeløp.NULL.copy(utbetalingsland = kompetanse.annenForeldersAktivitetsland) + else -> upb + } + } + .tilSkjemaer() +} + +fun Map>.filtrerSekundærland() = + this.mapValues { (_, tidslinje) -> tidslinje.filtrer { it?.resultat == KompetanseResultat.NORGE_ER_SEKUNDÆRLAND } } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/endringsabonnement/TilpassValutakurserTilUtenlandskePeriodebel\303\270pService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/endringsabonnement/TilpassValutakurserTilUtenlandskePeriodebel\303\270pService.kt" new file mode 100644 index 000000000..3ab5f1edb --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/endringsabonnement/TilpassValutakurserTilUtenlandskePeriodebel\303\270pService.kt" @@ -0,0 +1,75 @@ +package no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement + +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.FinnPeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEndringAbonnent +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaService +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.tilSeparateTidslinjerForBarna +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.tilSkjemaer +import no.nav.familie.ba.sak.kjerne.eøs.felles.medBehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløp +import no.nav.familie.ba.sak.kjerne.eøs.valutakurs.Valutakurs +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.outerJoin +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class TilpassValutakurserTilUtenlandskePeriodebeløpService( + valutakursRepository: PeriodeOgBarnSkjemaRepository, + private val utenlandskPeriodebeløpRepository: FinnPeriodeOgBarnSkjemaRepository, + endringsabonnenter: Collection>, +) : PeriodeOgBarnSkjemaEndringAbonnent { + val skjemaService = PeriodeOgBarnSkjemaService( + valutakursRepository, + endringsabonnenter, + ) + + @Transactional + fun tilpassValutakursTilUtenlandskPeriodebeløp(behandlingId: BehandlingId) { + val gjeldendeUtenlandskePeriodebeløp = utenlandskPeriodebeløpRepository.finnFraBehandlingId(behandlingId.id) + + tilpassValutakursTilUtenlandskPeriodebeløp(behandlingId, gjeldendeUtenlandskePeriodebeløp) + } + + @Transactional + override fun skjemaerEndret( + behandlingId: BehandlingId, + endretTil: Collection, + ) { + tilpassValutakursTilUtenlandskPeriodebeløp(behandlingId, endretTil) + } + + private fun tilpassValutakursTilUtenlandskPeriodebeløp( + behandlingId: BehandlingId, + gjeldendeUtenlandskePeriodebeløp: Collection, + ) { + val forrigeValutakurser = skjemaService.hentMedBehandlingId(behandlingId) + + val oppdaterteValutakurser = tilpassValutakurserTilUtenlandskePeriodebeløp( + forrigeValutakurser, + gjeldendeUtenlandskePeriodebeløp, + ).medBehandlingId(behandlingId) + + skjemaService.lagreDifferanseOgVarsleAbonnenter(behandlingId, forrigeValutakurser, oppdaterteValutakurser) + } +} + +internal fun tilpassValutakurserTilUtenlandskePeriodebeløp( + forrigeValutakurser: Collection, + gjeldendeUtenlandskePeriodebeløp: Collection, +): Collection { + val barnasUtenlandskePeriodebeløpTidslinjer = gjeldendeUtenlandskePeriodebeløp + .tilSeparateTidslinjerForBarna() + + return forrigeValutakurser.tilSeparateTidslinjerForBarna() + .outerJoin(barnasUtenlandskePeriodebeløpTidslinjer) { valutakurs, utenlandskPeriodebeløp -> + when { + utenlandskPeriodebeløp == null -> null + valutakurs == null || valutakurs.valutakode != utenlandskPeriodebeløp.valutakode -> + Valutakurs.NULL.copy(valutakode = utenlandskPeriodebeløp.valutakode) + else -> valutakurs + } + } + .tilSkjemaer() +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/BehandlingId.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/BehandlingId.kt" new file mode 100644 index 000000000..2502e8e99 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/BehandlingId.kt" @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles + +data class BehandlingId( + val id: Long, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjema.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjema.kt" new file mode 100644 index 000000000..da178e77b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjema.kt" @@ -0,0 +1,77 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles + +import no.nav.familie.ba.sak.kjerne.eøs.felles.util.MAX_MÅNED +import no.nav.familie.ba.sak.kjerne.eøs.felles.util.MIN_MÅNED +import no.nav.familie.ba.sak.kjerne.eøs.felles.util.erEkteDelmengdeAv +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import java.time.YearMonth + +interface PeriodeOgBarnSkjema where T : PeriodeOgBarnSkjema { + val fom: YearMonth? + val tom: YearMonth? + val barnAktører: Set + fun utenInnhold(): T + fun kopier( + fom: YearMonth? = this.fom, + tom: YearMonth? = this.tom, + barnAktører: Set = this.barnAktører.map { it.copy() }.toSet(), + ): T +} + +fun > T.medBarnOgPeriodeSomOverlapperMed(skjema: T): T? { + val fom = maxOf(this.fom ?: MIN_MÅNED, skjema.fom ?: MIN_MÅNED) + val tom = minOf(this.tom ?: MAX_MÅNED, skjema.tom ?: MAX_MÅNED) + + val snitt = this.kopier( + fom = if (fom == MIN_MÅNED) null else fom, + tom = if (tom == MAX_MÅNED) null else tom, + barnAktører = this.barnAktører.intersect(skjema.barnAktører), + ) + + return if (snitt.harBarnOgPeriode()) snitt else null +} + +fun > T.harBarnOgPeriode(): Boolean { + val harGyldigPeriode = fom == null || tom == null || fom!! <= tom + return harGyldigPeriode && barnAktører.isNotEmpty() +} + +fun > T.inneholder(skjema: T): Boolean { + return this.bareInnhold() == skjema.bareInnhold() && + (this.fom == null || this.fom!! <= skjema.fom) && + (this.tom == null || this.tom!! >= skjema.tom) && + this.barnAktører.containsAll(skjema.barnAktører) +} + +fun > T.bareInnhold(): T = + this.kopier(fom = null, tom = null, barnAktører = emptySet()) + +fun > T.utenBarn(): T = + this.kopier(fom = this.fom, tom = this.tom, barnAktører = emptySet()) + +fun > T.utenPeriode(): T = + this.kopier(fom = null, tom = null, barnAktører = this.barnAktører) + +fun > T.utenInnholdTilOgMed(tom: YearMonth?) = + this.kopier( + fom = this.tom?.plusMonths(1), + tom = tom, + ).utenInnhold() + +fun > T.medBarnaSomForsvinnerFra(skjema: T): T = + this.kopier(barnAktører = skjema.barnAktører.minus(this.barnAktører)) + +fun > T.tilOgMedBlirForkortetEllerLukketAv(skjema: T): Boolean = + skjema.tom != null && (this.tom == null || this.tom!! > skjema.tom) + +fun > T.erLikBortsettFraTilOgMed(skjema: T): Boolean = + this.kopier(tom = skjema.tom) == skjema + +fun > T.erLikBortsettFraBarn(skjema: T): Boolean = + this.kopier(barnAktører = skjema.barnAktører) == skjema + +fun > T.erLikBortsettFraBarnOgTilOgMed(skjema: T): Boolean = + this.kopier(barnAktører = skjema.barnAktører, tom = skjema.tom) == skjema + +fun > T.harEkteDelmengdeAvBarna(skjema: T): Boolean = + this.barnAktører.erEkteDelmengdeAv(skjema.barnAktører) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjemaEndringAbonnent.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjemaEndringAbonnent.kt" new file mode 100644 index 000000000..31922d07c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjemaEndringAbonnent.kt" @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles + +interface PeriodeOgBarnSkjemaEndringAbonnent> { + fun skjemaerEndret(behandlingId: BehandlingId, endretTil: Collection) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjemaEntitet.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjemaEntitet.kt" new file mode 100644 index 000000000..23e4e874b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjemaEntitet.kt" @@ -0,0 +1,13 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles + +import jakarta.persistence.MappedSuperclass +import no.nav.familie.ba.sak.common.BaseEntitet + +@MappedSuperclass +abstract class PeriodeOgBarnSkjemaEntitet> : + BaseEntitet(), + PeriodeOgBarnSkjema { + + abstract var id: Long + abstract var behandlingId: Long +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjemaRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjemaRepository.kt" new file mode 100644 index 000000000..cb53d6ddb --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjemaRepository.kt" @@ -0,0 +1,12 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.NoRepositoryBean + +interface FinnPeriodeOgBarnSkjemaRepository> { + fun finnFraBehandlingId(behandlingId: Long): Collection +} + +@NoRepositoryBean +interface PeriodeOgBarnSkjemaRepository> : + JpaRepository, FinnPeriodeOgBarnSkjemaRepository diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjemaService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjemaService.kt" new file mode 100644 index 000000000..9907e2a37 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/PeriodeOgBarnSkjemaService.kt" @@ -0,0 +1,79 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles + +import no.nav.familie.ba.sak.common.feilHvis +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.oppdaterSkjemaerRekursivt +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.slåSammen +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.somInversOppdateringEllersNull + +class PeriodeOgBarnSkjemaService>( + val periodeOgBarnSkjemaRepository: PeriodeOgBarnSkjemaRepository, + val endringsabonnenter: Collection>, +) { + + fun hentMedBehandlingId(behandlingId: BehandlingId): Collection { + return periodeOgBarnSkjemaRepository.finnFraBehandlingId(behandlingId.id) + } + + fun hentMedId(id: Long): S { + return periodeOgBarnSkjemaRepository.getById(id) + } + + fun endreSkjemaer(behandlingId: BehandlingId, oppdatering: S) { + val gjeldendeSkjemaer = hentMedBehandlingId(behandlingId) + + val justertOppdatering = oppdatering.somInversOppdateringEllersNull(gjeldendeSkjemaer) ?: oppdatering + val oppdaterteKompetanser = oppdaterSkjemaerRekursivt(gjeldendeSkjemaer, justertOppdatering) + + lagreDifferanseOgVarsleAbonnenter( + behandlingId, + gjeldendeSkjemaer, + oppdaterteKompetanser.medBehandlingId(behandlingId), + ) + } + + fun slettSkjema(behandlingId: BehandlingId, skjemaId: Long) { + val skjemaTilSletting = periodeOgBarnSkjemaRepository.getById(skjemaId) + feilHvis(skjemaTilSletting.behandlingId != behandlingId.id) { + "Prøver å slette et skjema som ikke er koblet til behandlingen man sender inn" + } + val gjeldendeSkjemaer = hentMedBehandlingId(behandlingId) + val blanktSkjema = skjemaTilSletting.utenInnhold() + + val oppdaterteKompetanser = gjeldendeSkjemaer.minus(skjemaTilSletting).plus(blanktSkjema) + .slåSammen().medBehandlingId(behandlingId) + + lagreDifferanseOgVarsleAbonnenter(behandlingId, gjeldendeSkjemaer, oppdaterteKompetanser) + } + + fun kopierOgErstattSkjemaer(fraBehandlingId: BehandlingId, tilBehandlingId: BehandlingId) { + val gjeldendeTilSkjemaer = hentMedBehandlingId(tilBehandlingId) + val kopiAvFraSkjemaer = hentMedBehandlingId(fraBehandlingId) + .map { it.kopier() } + .medBehandlingId(tilBehandlingId) + + periodeOgBarnSkjemaRepository.deleteAll(gjeldendeTilSkjemaer) + periodeOgBarnSkjemaRepository.saveAll(kopiAvFraSkjemaer) + } + + fun lagreDifferanseOgVarsleAbonnenter( + behandlingId: BehandlingId, + gjeldende: Collection, + oppdaterte: Collection, + ) { + val skalSlettes = gjeldende - oppdaterte + val skalLagres = oppdaterte - gjeldende + + periodeOgBarnSkjemaRepository.deleteAll(skalSlettes) + periodeOgBarnSkjemaRepository.saveAll(skalLagres) + + val endringer = skalSlettes + skalLagres + if (endringer.isNotEmpty()) { + endringsabonnenter.forEach { it.skjemaerEndret(behandlingId, oppdaterte) } + } + } +} + +fun > Collection.medBehandlingId(behandlingId: BehandlingId): Collection { + this.forEach { it.behandlingId = behandlingId.id } + return this +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/JusteringAvOppdatering.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/JusteringAvOppdatering.kt" new file mode 100644 index 000000000..2a21e575e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/JusteringAvOppdatering.kt" @@ -0,0 +1,58 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles.beregning + +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEntitet +import no.nav.familie.ba.sak.kjerne.eøs.felles.erLikBortsettFraBarn +import no.nav.familie.ba.sak.kjerne.eøs.felles.erLikBortsettFraBarnOgTilOgMed +import no.nav.familie.ba.sak.kjerne.eøs.felles.erLikBortsettFraTilOgMed +import no.nav.familie.ba.sak.kjerne.eøs.felles.harEkteDelmengdeAvBarna +import no.nav.familie.ba.sak.kjerne.eøs.felles.medBarnaSomForsvinnerFra +import no.nav.familie.ba.sak.kjerne.eøs.felles.tilOgMedBlirForkortetEllerLukketAv +import no.nav.familie.ba.sak.kjerne.eøs.felles.utenInnholdTilOgMed + +/** + * Funksjon som inverterer en skjema-oppdatering,[this], som skal endre et sett av [gjeldendeSkjemaer] + * + * I tilfellet der oppdateringen: + * 1. Gjelder ett gjeldende skjema + * 2a. Lukker periode på det gjeldende skjemaet, dvs til-og-med går fra til en verdi, eller til-og-med er tidligere + * 2b. og/eller reduserer antall barn + * så skal det lages en ny oppdatering med blankt skjema for det som ligger "utenfor" [oppdatering], dvs har + * 1. Perioden som starter måneden etter ny til-og-med-dato, og frem frem til eksisterende til-og-med (kan være ) + * 2. Barnet/barna som blir fjernet + * + * Problemet som skal løses er at skjemaer som kun varierer i periode eller barn, slås sammen fordi de ellers er like + * Lukking/forkorting av periode eller fjerning av barn vil føre til en umiddelbar sammenslåing og nulle ut oppdateringen + * Ved å lage den "motsatte" endringen med et tomt skjema "utenfor" det gjeldende skjemaet, + * blir nettoeffekten at den ønskede oppdateringen oppstår, og et tomt skjema dekker området rundt + * + */ +fun > T.somInversOppdateringEllersNull(gjeldendeSkjemaer: Collection): T? { + val oppdatering = this + + val skjemaetDerTilOgMedForkortes = gjeldendeSkjemaer.filter { gjeldende -> + gjeldende.tilOgMedBlirForkortetEllerLukketAv(oppdatering) && + gjeldende.erLikBortsettFraTilOgMed(oppdatering) + }.singleOrNull() + + val skjemaetDerBarnFjernes = gjeldendeSkjemaer.filter { gjeldende -> + oppdatering.harEkteDelmengdeAvBarna(gjeldende) && + gjeldende.erLikBortsettFraBarn(oppdatering) + }.singleOrNull() + + val skjemaetDerTilOgMedForkortesOgBarnFjernes = gjeldendeSkjemaer.filter { gjeldende -> + gjeldende.tilOgMedBlirForkortetEllerLukketAv(oppdatering) && + oppdatering.harEkteDelmengdeAvBarna(gjeldende) && + gjeldende.erLikBortsettFraBarnOgTilOgMed(oppdatering) + }.singleOrNull() + + return when { + skjemaetDerTilOgMedForkortesOgBarnFjernes != null -> + oppdatering.medBarnaSomForsvinnerFra(skjemaetDerTilOgMedForkortesOgBarnFjernes) + .utenInnholdTilOgMed(skjemaetDerTilOgMedForkortesOgBarnFjernes.tom) + skjemaetDerBarnFjernes != null -> + oppdatering.medBarnaSomForsvinnerFra(skjemaetDerBarnFjernes).utenInnhold() + skjemaetDerTilOgMedForkortes != null -> + oppdatering.utenInnholdTilOgMed(skjemaetDerTilOgMedForkortes.tom) + else -> null + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/OppdaterSkjemaer.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/OppdaterSkjemaer.kt" new file mode 100644 index 000000000..58a27afbc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/OppdaterSkjemaer.kt" @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles.beregning + +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjema +import no.nav.familie.ba.sak.kjerne.eøs.felles.bareInnhold +import no.nav.familie.ba.sak.kjerne.eøs.felles.medBarnOgPeriodeSomOverlapperMed + +/** + * Lager nye skjemaer der [oppdatering] overskriver skjemaet i [skjemaer] + * som helt eller delvis overlapper. Hvis ingenting overlapper, så returneres [skjemaer] + * @param[skjemaer] + * @param[oppdatering] + */ +fun > oppdaterSkjemaerRekursivt(skjemaer: Collection, oppdatering: T): Collection { + val førsteSkjemaSomOppdateres = skjemaer + .filter { it.medBarnOgPeriodeSomOverlapperMed(oppdatering) != null } // Må overlappe i periode og barn + .filter { it.bareInnhold() != oppdatering.bareInnhold() } // Må være en endring i selve innholdet i skjemaet + .firstOrNull() ?: return skjemaer + + // oppdatertSkjema har innholdet fra oppdateringen, samt felles barn og perioder + // Vi sjekket at det VAR en overlapp rett over, så det er ikke fare for NullPointerException + val oppdatertSkjema = oppdatering.medBarnOgPeriodeSomOverlapperMed(førsteSkjemaSomOppdateres)!! + + // førsteSkjemaFratrukketOppdatering inneholder det som "blir igjen", + // dvs det originale innholdet "utenfor" overlappende barn og periode + val førsteSkjemaFratrukketOppdatering = førsteSkjemaSomOppdateres.trekkFra(oppdatertSkjema) + + val oppdaterteSkjemaer = skjemaer + .minus(førsteSkjemaSomOppdateres) + .plus(oppdatertSkjema) + .plus(førsteSkjemaFratrukketOppdatering) + .slåSammen() + + return oppdaterSkjemaerRekursivt(oppdaterteSkjemaer, oppdatering) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/README.md" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/README.md" new file mode 100644 index 000000000..bf49c3669 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/README.md" @@ -0,0 +1,281 @@ +# Beregning på skjemaer for Kompetanse, Valutakurs og Utenlandsk periodebeløp + +## Hva er de ulike skjemaene + +Under vilkårsvurdering avgjør saksbehandler om et vilkår skal vurderes etter _EØS-forordningen_ eller _nasjonale regler_ +. Hvis vilkårene er oppfylt og et gitt sett av vilkår er vurdert etter EØS-forordning for søker og ett eller flere barn, +oppstår det _EØS-perioder_ på barnet + +**Kompetanse** er et skjema som saksbehandler fyller ut for å avgjøre om Norge er _primærland_ eller _sekundærland_ i +barnas EØS-perioder. + +* Norge som primærland betyr at utbetaling av barnetrygd skal skje som vanlig etter nasjonale regler +* Norge som sekundærland betyr at utbetalingen mot differanseberegnes mot tilsvarende ytelse i EØS + +Hvis Norge er sekundærland, må to andre skjeamer fylles ut: + +* **Utenlandsk periodebeløp** inneholder hva brukeren har fått utbetalt i ytelse tilsvarende barnetygd fra annet + EØS-land. Skjemaet inneholder beløpet, valutaen og intervallet beløpet utbetales med (ukentlig, månedlig, kvartalsvis, + årlig etc) +* **Valutakurs** inneholder informasjon om hvilken valutkurs som skal benyttes for å omregne utenlandsk periodebeløp til + norske kroner. Det inneholder også hvilken dato valuktakursen er hentet fra. + +## Konsept + +Et skjema består av to hoveddeler + +* Barn og periode, som er likt for alle +* Skjemafelter, som varier fra skjema til skjema + +_Barn_ er ett eller flere barn, representert ved aktør-id'er. + +_Periode_ er gitt ved 'fom' og 'tom' og representerer tiden mellom fra-og-med-måned og til-og-med-måned. + +Alle skjemaer implementerer `PeriodeOgBarnSkjema`, som ivaretar de grunnleggende konseptene. + +For å vise konseptene tar vi utgangspunkt i _Kompetanse_, som blant annet har følgende felter: + +* Barnets bostedsland, representert som en String med ISO-kode +* Annen forelders bostedsland (String) +* Søkers aktivitet (enum) +* Annen forelders aktivitet (enum) +* Kompetent land (enum) + +_Kompetent land_ er en av: + +* NORGE_ER_PRIMÆRLAND, +* NORGE_ER_SEKUNDÆRLAND +* BEGGE_ER_PRIMÆRLAND + +Heretter benyttes følgende syntaks for å beskrive kompetanse-skjemaer + +``` +2020-03 +" PPPPPPP SSSS->", B1, B2 +``` + +som leses som: + +* Startdato er mars 2020 +* Kompetanse-skjema er for to barn (B1 og B2). +* Det er en primærland-periode ('P') fra og med mai 2020 (2 måneder etter startdato) og 7 måneder frem (til og med + november 2020) +* Det er en sekundærland-periode ('S') fra og med mai 2021 og til og med august 2021 +* Det er en periode der kompetanse-skjemaet finnes, men ikke er utfylt ('-'). Det er fra og med september 2021. +* '>' betyr "herfra og fremover", dvs at perioden er "åpen", så det ikke utfylte skjemaet gjelder fra-og-med september + og videre fremover + +## Beregningsregler + +### Like, etterfølgende skjemaer for ett barn slås sammen + +Altså: + +``` +2020-03 +"P", B1 +" P", B1 +" P", B1 +``` + +blir til + +``` +2020-03 +"PPP", B1 +``` + +### Like skjemaer i samme periode for flere barn slås sammen + +Altså: + +``` +2020-03 +"PPP", B1 +"PPP", B2 +"PPP", B3 +``` + +blir til + +``` +2020-03 +"PPP", B1, B2, B3 +``` + +### Slå sammen barn foretrekkes over å slå sammen perioder + +Altså: + +``` +2020-03 +"PPP", B1 +" PPP", B2 +" PPP", B3 +``` + +blir til + +``` +2020-03 +"P", B1 +" P", B1, B2 +" P", B1, B2, B3 +" P", B2, B3 +" P", B3 +``` + +### Oppdatering vil kunne føre til flere skjemaer for å oppfylle reglene ovenfor + +Eksisterende kompetanser som ser slik ut: + +``` +2020-03 +"PPPPPPPP", B1, B2, B3 +``` + +og oppdateres med + +``` +2020-06 +" SSS", B2 +``` + +vil resultere i: + +``` +2020-03 +"PPP", B1, B2, B3 +" PPP", B1, B3 +" SSS", B2 +" PP", B1, B2, B3 + +``` + +### Oppdatering vil respektere eksisterende skjema-grenser + +Eksisterende kompetanser som ser slik ut: + +``` +2020-03 +"---- SSSS ---", B1, B2, B3 +``` + +som oppdateres med (primærlanf fra start-dato og uendelig fremover) + +``` +2020-03 +"P>", B1, B2, B3 +``` + +vil resultere i: + +``` +2020-03 +"PPPP PPPP PPP", B1, B2, B3 +``` + +### Innsnevring av ett skjema fører til at opprettes tomme skjemaer "rundt" + +Eksisterende skjema (som altså løper fra 2020-03, men ikke har avslutning) + +``` +2020-03 +"S>", B1, B2, B3 +``` + +som oppdateres med (altså at perioden blir avsluttet, og ett barn fjernes) + +``` +2020-03 +"SSS", B1, B2 +``` + +vil resultere i: + +``` +2020-03 +"SSS->", B1, B2 +"->", B3 +``` + +Her blir det altså opprettet et tomt skjema for barn B1 og B2 fra og med juni, mens barn B3 får et tomt skjema for hele +perioden, altså fra og med mars. + +### Spesielt for Kompetanse + +#### Kompetanser matcher alltid EØS-periodene. + +Hvis vilkårsvurderingen fører til at barnets EØS-perioder endrer seg, så vil kompetanse endre seg. Det er et par +tilfeller: + +* EØS-perioden reduseres for et barn. +* EØS-perioden utvides for et barn + +#### EØS-perioden reduseres for et barn + +Barnet beholder skjema-innholdet for den "smalere" perioden. Men det kan føre til at settet av kompetanser endrer seg. + +F.eks hvis regleverk-periodene ser slik ut (E = EØS. N = Nasjonalt) for tre barn + +``` +2020-03 +"EEEEEEEEEEEEEE", B1, B2, B3 +``` + +og har fått følgende kompetansevurdering: + +``` +2020-03 +"PPPPPSSSSSSSSS", B1, B2, B3 +``` + +Når EØS-periodene endrer seg for ett barn til dette: + +``` +2020-03 +"NNNEEEEEEEEEEE", B1 +``` + +så vil samlet kompetanse bli + +``` +2020-03 +"PPP", B2, B3 +" PPSSSSSSSSS", B1, B2, B3 +``` + +#### EØS-perioden utvides for et barn + +Barnet beholder skjema-innholdet for den orignale perioden, men får ett eller to uutfylte kompetanse-skjemaer for de(n) +ekstra tidsperioden(e) + +F.eks hvis regleverk-periodene ser slik ut + +``` +2020-03 +"EEEEEEEEEEEEEE", B1, B2, B3 +``` + +og har fått følgende kompetansevurdering: + +``` +2020-03 +"PPPPPSSSSSSSSS", B1, B2, B3 +``` + +Når EØS-periodene endrer seg for ett barn til dette (starter 2 mnd tidligere og slutter 3 mnd senere): + +``` +2020-01 +"EEEEEEEEEEEEEEEE", B1 +``` + +så vil samlet kompetanse bli + +``` +2020-01 +"--", B1 +" PPPPPSSSSSSSSS", B1, B2, B3 +" ---", B1 +``` diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/SkjemaTidslinjer.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/SkjemaTidslinjer.kt" new file mode 100644 index 000000000..a329a4ee5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/SkjemaTidslinjer.kt" @@ -0,0 +1,60 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles.beregning + +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjema +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEntitet +import no.nav.familie.ba.sak.kjerne.eøs.felles.utenPeriode +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunktEllerUendeligSent +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunktEllerUendeligTidlig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonthEllerNull + +fun > S.tilTidslinje() = listOf(this).tilTidslinje() + +internal fun > Iterable.tilTidslinje() = + tidslinje { + this.map { + Periode( + it.fom.tilTidspunktEllerUendeligTidlig(), + it.tom.tilTidspunktEllerUendeligSent(), + it.utenPeriode(), + ) + } + } + +fun > Iterable.tilSeparateTidslinjerForBarna(): Map> { + val skjemaer = this + if (skjemaer.toList().isEmpty()) return emptyMap() + + val alleBarnAktørIder = skjemaer.map { it.barnAktører }.reduce { akk, neste -> akk + neste } + + return alleBarnAktørIder.associateWith { aktør -> + tidslinje { + skjemaer + .filter { it.barnAktører.contains(aktør) } + .map { + Periode( + fraOgMed = it.fom.tilTidspunktEllerUendeligTidlig(it.tom), + tilOgMed = it.tom.tilTidspunktEllerUendeligSent(it.fom), + innhold = it.kopier(fom = null, tom = null, barnAktører = setOf(aktør)), + ) + } + } + } +} + +fun > Map>.tilSkjemaer() = + this.flatMap { (aktør, tidslinjer) -> tidslinjer.tilSkjemaer(aktør) } + .slåSammen() + +private fun > Tidslinje.tilSkjemaer(aktør: Aktør) = + this.perioder().mapNotNull { periode -> + periode.innhold?.kopier( + fom = periode.fraOgMed.tilYearMonthEllerNull(), + tom = periode.tilOgMed.tilYearMonthEllerNull(), + barnAktører = setOf(aktør), + ) + } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/Sl\303\245SammenSkjemaer.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/Sl\303\245SammenSkjemaer.kt" new file mode 100644 index 000000000..ea5edc881 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/Sl\303\245SammenSkjemaer.kt" @@ -0,0 +1,52 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles.beregning + +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjema +import no.nav.familie.ba.sak.kjerne.eøs.felles.utenBarn +import no.nav.familie.ba.sak.kjerne.eøs.felles.utenPeriode +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNull +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.slåSammenLike +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonthEllerNull + +fun > Collection.slåSammen(): Collection { + if (this.isEmpty()) { + return this + } + + val kompetanseSettTidslinje: Tidslinje, Måned> = this.map { it.tilTidslinje() } + .kombinerUtenNull { + it.groupingBy { it.utenBarn() }.reduce { _, acc, kompetanse -> acc.leggSammenBarn(kompetanse) } + .values.toSet() + } + + val kompetanserSlåttSammenVertikalt = kompetanseSettTidslinje.perioder().flatMap { periode -> + periode.innhold?.settFomOgTom(periode) ?: emptyList() + } + + val kompetanseSlåttSammenHorisontalt = kompetanserSlåttSammenVertikalt + .groupBy { it.utenPeriode() } + .mapValues { (_, kompetanser) -> kompetanser.tilTidslinje().slåSammenLike() } + .mapValues { (_, tidslinje) -> tidslinje.perioder() } + .values.flatten().mapNotNull { periode -> periode.innhold?.settFomOgTom(periode) } + + return kompetanseSlåttSammenHorisontalt +} + +private fun > T.leggSammenBarn(kompetanse: T) = + this.kopier( + fom = this.fom, + tom = this.tom, + barnAktører = this.barnAktører + kompetanse.barnAktører, + ) + +fun > Iterable?.settFomOgTom(periode: Periode<*, Måned>) = + this?.map { skjema -> skjema.settFomOgTom(periode) } + +fun > T.settFomOgTom(periode: Periode<*, Måned>) = + this.kopier( + fom = periode.fraOgMed.tilYearMonthEllerNull(), + tom = periode.tilOgMed.tilYearMonthEllerNull(), + barnAktører = this.barnAktører, + ) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/TrekkFraSkjemaer.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/TrekkFraSkjemaer.kt" new file mode 100644 index 000000000..63af9ed72 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/TrekkFraSkjemaer.kt" @@ -0,0 +1,41 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles.beregning + +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjema +import no.nav.familie.ba.sak.kjerne.eøs.felles.inneholder +import no.nav.familie.ba.sak.kjerne.eøs.felles.util.MAX_MÅNED + +/** + * Reduser innholdet i this-kompetansen med innholdet i oppdaterKompetanse + * En viktig forutsetning er at oppdatertKompetanse alltid er "mindre" enn kompetansen som reduseres + */ +fun > T.trekkFra(skjema: T): Collection { + val gammeltSkjema = this + val skjemaForRestBarn = gammeltSkjema + .kopier( + barnAktører = gammeltSkjema.barnAktører.minus(skjema.barnAktører), + ).takeIf { it.barnAktører.isNotEmpty() } + + val skjemaForForegåendePerioder = gammeltSkjema + .kopier( + fom = gammeltSkjema.fom, + tom = skjema.fom?.minusMonths(1), + barnAktører = skjema.barnAktører, + ).takeIf { it.fom != null && it.fom!! <= it.tom } + + val skjemaForEtterfølgendePerioder = gammeltSkjema.kopier( + fom = skjema.tom?.plusMonths(1), + tom = gammeltSkjema.tom, + barnAktører = skjema.barnAktører, + ).takeIf { it.fom != null && it.fom!! <= (it.tom ?: MAX_MÅNED) } + + return listOfNotNull(skjemaForRestBarn, skjemaForForegåendePerioder, skjemaForEtterfølgendePerioder) +} + +fun > Iterable.trekkFra(skalFjernes: T) = + this.flatMap { skjema -> + if (skjema.inneholder(skalFjernes)) { + skjema.trekkFra(skalFjernes) + } else { + listOf(skjema) + } + } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/util/Collections.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/util/Collections.kt" new file mode 100644 index 000000000..548296a25 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/util/Collections.kt" @@ -0,0 +1,7 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles.util + +fun Collection.erEkteDelmengdeAv(mengde: Collection) = + this.size < mengde.size && mengde.containsAll(this) + +fun Collection.replaceLast(replacer: (T) -> T) = + this.take(this.size - 1) + replacer(this.last()) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/util/Tid.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/util/Tid.kt" new file mode 100644 index 000000000..d4be4ee0b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/util/Tid.kt" @@ -0,0 +1,7 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles.util + +import no.nav.familie.ba.sak.common.toYearMonth +import java.time.LocalDate + +val MAX_MÅNED = LocalDate.MAX.toYearMonth() +val MIN_MÅNED = LocalDate.MIN.toYearMonth() diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseController.kt" new file mode 100644 index 000000000..e9d315ab1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseController.kt" @@ -0,0 +1,106 @@ +package no.nav.familie.ba.sak.kjerne.eøs.kompetanse + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestKompetanse +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.ekstern.restDomene.tilKompetanse +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/kompetanse") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class KompetanseController( + private val tilgangService: TilgangService, + private val kompetanseService: KompetanseService, + private val personidentService: PersonidentService, + private val utvidetBehandlingService: UtvidetBehandlingService, +) { + + @PutMapping(path = ["{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun oppdaterKompetanse( + @PathVariable behandlingId: Long, + @RequestBody restKompetanse: RestKompetanse, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Oppdaterer kompetanse", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + val barnAktører = restKompetanse.barnIdenter.map { personidentService.hentAktør(it) } + val kompetanse = restKompetanse.tilKompetanse(barnAktører = barnAktører) + + validerOppdatering(kompetanse) + + kompetanseService.oppdaterKompetanse(BehandlingId(behandlingId), kompetanse) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @DeleteMapping(path = ["{behandlingId}/{kompetanseId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun slettKompetanse( + @PathVariable behandlingId: Long, + @PathVariable kompetanseId: Long, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.DELETE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Sletter kompetanse", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + kompetanseService.slettKompetanse(BehandlingId(behandlingId), kompetanseId) + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + private fun validerOppdatering(oppdatertKompetanse: Kompetanse) { + if (oppdatertKompetanse.fom == null) { + throw FunksjonellFeil("Manglende fra-og-med", httpStatus = HttpStatus.BAD_REQUEST) + } + if (oppdatertKompetanse.tom != null && oppdatertKompetanse.fom > oppdatertKompetanse.tom) { + throw FunksjonellFeil("Fra-og-med er etter til-og-med", httpStatus = HttpStatus.BAD_REQUEST) + } + if (oppdatertKompetanse.barnAktører.isEmpty()) { + throw FunksjonellFeil("Mangler barn", httpStatus = HttpStatus.BAD_REQUEST) + } + + if ( + (oppdatertKompetanse.erAnnenForelderOmfattetAvNorskLovgivning == true && oppdatertKompetanse.søkersAktivitet?.gyldigForAnnenForelder == false) || + (oppdatertKompetanse.erAnnenForelderOmfattetAvNorskLovgivning == false && oppdatertKompetanse.søkersAktivitet?.gyldigForSøker == false) + ) { + throw FunksjonellFeil( + "Valgt verdi for søkers aktivitet er ikke gyldig ${if (oppdatertKompetanse.erAnnenForelderOmfattetAvNorskLovgivning) "når annen forelder er omfattet av norsk lovgivning" else ""}" + .trim(), + ) + } + if ( + (oppdatertKompetanse.erAnnenForelderOmfattetAvNorskLovgivning == true && oppdatertKompetanse.annenForeldersAktivitet?.gyldigForSøker == false) || + (oppdatertKompetanse.erAnnenForelderOmfattetAvNorskLovgivning == false && oppdatertKompetanse.annenForeldersAktivitet?.gyldigForAnnenForelder == false) + ) { + throw FunksjonellFeil( + "Valgt verdi for annen forelders aktivitet er ikke gyldig ${if (oppdatertKompetanse.erAnnenForelderOmfattetAvNorskLovgivning) "når annen forelder er omfattet av norsk lovgivning" else ""}" + .trim(), + ) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseRepository.kt" new file mode 100644 index 000000000..30a5b5d18 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseRepository.kt" @@ -0,0 +1,11 @@ +package no.nav.familie.ba.sak.kjerne.eøs.kompetanse + +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import org.springframework.data.jpa.repository.Query + +interface KompetanseRepository : PeriodeOgBarnSkjemaRepository { + + @Query("SELECT k FROM Kompetanse k WHERE k.behandlingId = :behandlingId") + override fun finnFraBehandlingId(behandlingId: Long): Collection +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseService.kt" new file mode 100644 index 000000000..9fd92d022 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseService.kt" @@ -0,0 +1,38 @@ +package no.nav.familie.ba.sak.kjerne.eøs.kompetanse + +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEndringAbonnent +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaService +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class KompetanseService( + kompetanseRepository: PeriodeOgBarnSkjemaRepository, + endringsabonnenter: Collection>, +) { + val skjemaService = PeriodeOgBarnSkjemaService( + kompetanseRepository, + endringsabonnenter, + ) + + fun hentKompetanser(behandlingId: BehandlingId) = + skjemaService.hentMedBehandlingId(behandlingId) + + fun hentKompetanse(kompetanseId: Long) = + skjemaService.hentMedId(kompetanseId) + + @Transactional + fun oppdaterKompetanse(behandlingId: BehandlingId, oppdatering: Kompetanse) = + skjemaService.endreSkjemaer(behandlingId, oppdatering) + + @Transactional + fun slettKompetanse(behandlingId: BehandlingId, kompetanseId: Long) = + skjemaService.slettSkjema(behandlingId, kompetanseId) + + @Transactional + fun kopierOgErstattKompetanser(fraBehandlingId: BehandlingId, tilBehandlingId: BehandlingId) = + skjemaService.kopierOgErstattSkjemaer(fraBehandlingId, tilBehandlingId) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/domene/Kompetanse.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/domene/Kompetanse.kt" new file mode 100644 index 000000000..2db391d76 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/domene/Kompetanse.kt" @@ -0,0 +1,211 @@ +package no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene + +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.JoinTable +import jakarta.persistence.ManyToMany +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.YearMonthConverter +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEntitet +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tilTidslinje +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.YearMonth + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Kompetanse") +@Table(name = "KOMPETANSE") +data class Kompetanse( + @Column(name = "fom", columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + override val fom: YearMonth?, + + @Column(name = "tom", columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + override val tom: YearMonth?, + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "AKTOER_TIL_KOMPETANSE", + joinColumns = [JoinColumn(name = "fk_kompetanse_id")], + inverseJoinColumns = [JoinColumn(name = "fk_aktoer_id")], + ) + override val barnAktører: Set = emptySet(), // kan ikke være tom + + @Enumerated(EnumType.STRING) + @Column(name = "soekers_aktivitet") + val søkersAktivitet: KompetanseAktivitet? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "annen_forelderes_aktivitet") + val annenForeldersAktivitet: KompetanseAktivitet? = null, + + @Column(name = "annen_forelderes_aktivitetsland") + val annenForeldersAktivitetsland: String? = null, + + @Column(name = "sokers_aktivitetsland") + val søkersAktivitetsland: String? = null, + + @Column(name = "barnets_bostedsland") + val barnetsBostedsland: String? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "resultat") + val resultat: KompetanseResultat? = null, + + @Column(name = "er_annen_forelder_omfattet_av_norsk_lovgivning") + val erAnnenForelderOmfattetAvNorskLovgivning: Boolean? = false, +) : PeriodeOgBarnSkjemaEntitet() { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "kompetanse_seq_generator") + @SequenceGenerator( + name = "kompetanse_seq_generator", + sequenceName = "kompetanse_seq", + allocationSize = 50, + ) + override var id: Long = 0 + + @Column(name = "fk_behandling_id", updatable = false, nullable = false) + override var behandlingId: Long = 0 + + override fun utenInnhold() = this.copy( + søkersAktivitet = null, + søkersAktivitetsland = null, + annenForeldersAktivitet = null, + annenForeldersAktivitetsland = null, + barnetsBostedsland = null, + resultat = null, + ) + + override fun kopier(fom: YearMonth?, tom: YearMonth?, barnAktører: Set) = + copy( + fom = fom, + tom = tom, + barnAktører = barnAktører, + ) + + fun validerFelterErSatt() { + if (!erObligatoriskeFelterSatt() + ) { + throw Feil("Kompetanse mangler verdier") + } + } + + fun erObligatoriskeFelterSatt() = fom != null && + erObligatoriskeFelterUtenomTidsperioderSatt() + + fun erObligatoriskeFelterUtenomTidsperioderSatt() = + this.søkersAktivitet != null && + this.annenForeldersAktivitet != null && + this.søkersAktivitetsland != null && + this.barnetsBostedsland != null && + this.resultat != null && + this.barnAktører.isNotEmpty() + + companion object { + val NULL = Kompetanse(null, null, emptySet()) + } +} + +enum class KompetanseAktivitet( + val gyldigForSøker: Boolean, + val gyldigForAnnenForelder: Boolean, +) { + ARBEIDER(true, false), + SELVSTENDIG_NÆRINGSDRIVENDE(true, false), + UTSENDT_ARBEIDSTAKER_FRA_NORGE(true, false), + MOTTAR_UFØRETRYGD(true, false), + ARBEIDER_PÅ_NORSKREGISTRERT_SKIP(true, false), + ARBEIDER_PÅ_NORSK_SOKKEL(true, false), + ARBEIDER_FOR_ET_NORSK_FLYSELSKAP(true, false), + ARBEIDER_VED_UTENLANDSK_UTENRIKSSTASJON(true, false), + MOTTAR_UTBETALING_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET(true, false), + MOTTAR_UFØRETRYGD_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET(true, false), + MOTTAR_PENSJON_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET(true, false), + + MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN(true, true), + MOTTAR_PENSJON(true, true), + INAKTIV(true, true), + + I_ARBEID(false, true), + FORSIKRET_I_BOSTEDSLAND(false, true), + IKKE_AKTUELT(false, true), + UTSENDT_ARBEIDSTAKER(false, true), +} + +enum class KompetanseResultat { + NORGE_ER_PRIMÆRLAND, + NORGE_ER_SEKUNDÆRLAND, + TO_PRIMÆRLAND, +} + +sealed interface IKompetanse { + val id: Long + val behandlingId: Long +} + +data class TomKompetanse( + override val id: Long, + override val behandlingId: Long, +) : IKompetanse + +data class UtfyltKompetanse( + override val id: Long, + override val behandlingId: Long, + val fom: YearMonth, + val tom: YearMonth?, + val barnAktører: Set, + val søkersAktivitet: KompetanseAktivitet, + val annenForeldersAktivitet: KompetanseAktivitet, + val annenForeldersAktivitetsland: String?, + val søkersAktivitetsland: String, + val barnetsBostedsland: String, + val resultat: KompetanseResultat, +) : IKompetanse + +fun Kompetanse.tilIKompetanse(): IKompetanse { + return if (this.erObligatoriskeFelterSatt()) { + UtfyltKompetanse( + id = this.id, + behandlingId = this.behandlingId, + fom = this.fom!!, + tom = this.tom, + barnAktører = this.barnAktører, + søkersAktivitet = this.søkersAktivitet!!, + annenForeldersAktivitet = this.annenForeldersAktivitet!!, + annenForeldersAktivitetsland = this.annenForeldersAktivitetsland, + søkersAktivitetsland = this.søkersAktivitetsland!!, + barnetsBostedsland = this.barnetsBostedsland!!, + resultat = this.resultat!!, + ) + } else { + TomKompetanse( + id = this.id, + behandlingId = this.behandlingId, + ) + } +} + +fun List.tilTidslinje() = + this.map { + Periode( + fraOgMed = it.fom.tilTidspunkt(), + tilOgMed = it.tom?.tilTidspunkt() ?: MånedTidspunkt.uendeligLengeTil(), + innhold = it, + ) + }.tilTidslinje() diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/domene/Vilk\303\245rRegelverkResultat.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/domene/Vilk\303\245rRegelverkResultat.kt" new file mode 100644 index 000000000..2f459a71b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/domene/Vilk\303\245rRegelverkResultat.kt" @@ -0,0 +1,110 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat.IKKE_FULLT_VURDERT +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat.IKKE_OPPFYLT +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat.OPPFYLT_BLANDET_REGELVERK +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat.OPPFYLT_EØS_FORORDNINGEN +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat.OPPFYLT_NASJONALE_REGLER +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat.OPPFYLT_REGELVERK_IKKE_SATT +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat + +data class VilkårRegelverkResultat( + val vilkår: Vilkår, + val regelverkResultat: RegelverkResultat, + val utdypendeVilkårsvurderinger: List = emptyList(), +) { + val resultat get() = regelverkResultat.resultat + val regelverk get() = regelverkResultat.regelverk +} + +fun VilkårRegelverkResultat.medRegelverk(regelverk: Regelverk) = + VilkårRegelverkResultat( + this.vilkår, + RegelverkResultat.values().first { it.regelverk == regelverk && it.resultat == this.resultat }, + this.utdypendeVilkårsvurderinger, + ) + +enum class RegelverkResultat(val regelverk: Regelverk?, val resultat: Resultat?) { + OPPFYLT_EØS_FORORDNINGEN(Regelverk.EØS_FORORDNINGEN, Resultat.OPPFYLT), + OPPFYLT_NASJONALE_REGLER(Regelverk.NASJONALE_REGLER, Resultat.OPPFYLT), + OPPFYLT_REGELVERK_IKKE_SATT(null, Resultat.OPPFYLT), + OPPFYLT_BLANDET_REGELVERK(null, Resultat.OPPFYLT), + IKKE_OPPFYLT(null, Resultat.IKKE_OPPFYLT), + IKKE_FULLT_VURDERT(null, Resultat.IKKE_VURDERT), +} + +fun VilkårResultat.tilRegelverkResultat() = when (this.resultat) { + Resultat.OPPFYLT -> when (this.vurderesEtter) { + Regelverk.EØS_FORORDNINGEN -> OPPFYLT_EØS_FORORDNINGEN + Regelverk.NASJONALE_REGLER -> OPPFYLT_NASJONALE_REGLER + null -> OPPFYLT_REGELVERK_IKKE_SATT + } + Resultat.IKKE_OPPFYLT -> IKKE_OPPFYLT + Resultat.IKKE_VURDERT -> IKKE_FULLT_VURDERT +} + +fun RegelverkResultat?.kombinerMed(resultat: RegelverkResultat?) = when (this) { + null -> when (resultat) { + null -> null + else -> IKKE_FULLT_VURDERT + } + + OPPFYLT_EØS_FORORDNINGEN -> when (resultat) { + null -> IKKE_FULLT_VURDERT + OPPFYLT_EØS_FORORDNINGEN -> OPPFYLT_EØS_FORORDNINGEN + OPPFYLT_NASJONALE_REGLER -> OPPFYLT_BLANDET_REGELVERK + OPPFYLT_BLANDET_REGELVERK -> OPPFYLT_BLANDET_REGELVERK + OPPFYLT_REGELVERK_IKKE_SATT -> OPPFYLT_BLANDET_REGELVERK + IKKE_FULLT_VURDERT -> IKKE_FULLT_VURDERT + IKKE_OPPFYLT -> IKKE_OPPFYLT + } + OPPFYLT_NASJONALE_REGLER -> when (resultat) { + null -> IKKE_FULLT_VURDERT + OPPFYLT_EØS_FORORDNINGEN -> OPPFYLT_BLANDET_REGELVERK + OPPFYLT_NASJONALE_REGLER -> OPPFYLT_NASJONALE_REGLER + OPPFYLT_BLANDET_REGELVERK -> OPPFYLT_BLANDET_REGELVERK + OPPFYLT_REGELVERK_IKKE_SATT -> OPPFYLT_BLANDET_REGELVERK + IKKE_FULLT_VURDERT -> IKKE_FULLT_VURDERT + IKKE_OPPFYLT -> IKKE_OPPFYLT + } + + OPPFYLT_BLANDET_REGELVERK -> when (resultat) { + null -> IKKE_FULLT_VURDERT + OPPFYLT_EØS_FORORDNINGEN -> OPPFYLT_BLANDET_REGELVERK + OPPFYLT_NASJONALE_REGLER -> OPPFYLT_BLANDET_REGELVERK + OPPFYLT_BLANDET_REGELVERK -> OPPFYLT_BLANDET_REGELVERK + OPPFYLT_REGELVERK_IKKE_SATT -> OPPFYLT_BLANDET_REGELVERK + IKKE_FULLT_VURDERT -> IKKE_FULLT_VURDERT + IKKE_OPPFYLT -> IKKE_OPPFYLT + } + + OPPFYLT_REGELVERK_IKKE_SATT -> when (resultat) { + null -> IKKE_FULLT_VURDERT + OPPFYLT_EØS_FORORDNINGEN -> OPPFYLT_BLANDET_REGELVERK + OPPFYLT_NASJONALE_REGLER -> OPPFYLT_BLANDET_REGELVERK + OPPFYLT_BLANDET_REGELVERK -> OPPFYLT_BLANDET_REGELVERK + OPPFYLT_REGELVERK_IKKE_SATT -> OPPFYLT_REGELVERK_IKKE_SATT + IKKE_FULLT_VURDERT -> IKKE_FULLT_VURDERT + IKKE_OPPFYLT -> IKKE_OPPFYLT + } + + IKKE_OPPFYLT -> IKKE_OPPFYLT + IKKE_FULLT_VURDERT -> IKKE_FULLT_VURDERT +} + +fun VilkårRegelverkResultat?.erOppfylt() = this?.resultat == Resultat.OPPFYLT + +data class KombinertRegelverkResultat( + val barnetsResultat: RegelverkResultat?, + val søkersResultat: RegelverkResultat?, +) { + val kombinertResultat get() = barnetsResultat.kombinerMed(søkersResultat) + + override fun toString(): String { + return kombinertResultat.toString() + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270p.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270p.kt" new file mode 100644 index 000000000..73dffb4bc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270p.kt" @@ -0,0 +1,90 @@ +package no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp + +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.JoinTable +import jakarta.persistence.ManyToMany +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.YearMonthConverter +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Intervall +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEntitet +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.math.BigDecimal +import java.time.YearMonth + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "UtenlandskPeriodebeløp") +@Table(name = "UTENLANDSK_PERIODEBELOEP") +data class UtenlandskPeriodebeløp( + @Column(name = "fom", columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + override val fom: YearMonth?, + + @Column(name = "tom", columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + override val tom: YearMonth?, + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "AKTOER_TIL_UTENLANDSK_PERIODEBELOEP", + joinColumns = [JoinColumn(name = "fk_utenlandsk_periodebeloep_id")], + inverseJoinColumns = [JoinColumn(name = "fk_aktoer_id")], + ) + override val barnAktører: Set = emptySet(), + + @Column(name = "beloep") + val beløp: BigDecimal? = null, + + @Column(name = "valutakode") + val valutakode: String? = null, + + @Column(name = "intervall") + @Enumerated(EnumType.STRING) + val intervall: Intervall? = null, + + @Column(name = "utbetalingsland") + val utbetalingsland: String? = null, + + @Column(name = "kalkulert_maanedlig_beloep") + val kalkulertMånedligBeløp: BigDecimal? = null, +) : PeriodeOgBarnSkjemaEntitet() { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "utenlandsk_periodebeloep_seq_generator") + @SequenceGenerator( + name = "utenlandsk_periodebeloep_seq_generator", + sequenceName = "utenlandsk_periodebeloep_seq", + allocationSize = 50, + ) + override var id: Long = 0 + + @Column(name = "fk_behandling_id", updatable = false, nullable = false) + override var behandlingId: Long = 0 + + override fun utenInnhold(): UtenlandskPeriodebeløp = copy( + beløp = null, + valutakode = null, + intervall = null, + kalkulertMånedligBeløp = null, + ) + + override fun kopier(fom: YearMonth?, tom: YearMonth?, barnAktører: Set) = copy( + fom = fom, + tom = tom, + barnAktører = barnAktører.toSet(), // .toSet() brukes for at det skal bli et nytt sett (to objekter kan ikke ha referanse til samme sett) + ) + + companion object { + val NULL = UtenlandskPeriodebeløp(null, null, emptySet()) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pController.kt" new file mode 100644 index 000000000..aef25b17d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pController.kt" @@ -0,0 +1,78 @@ +package no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp + +import jakarta.validation.Valid +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtenlandskPeriodebeløp +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.ekstern.restDomene.tilUtenlandskPeriodebeløp +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/differanseberegning/utenlandskperidebeløp") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class UtenlandskPeriodebeløpController( + private val tilgangService: TilgangService, + private val utenlandskPeriodebeløpService: UtenlandskPeriodebeløpService, + private val utenlandskPeriodebeløpRepository: UtenlandskPeriodebeløpRepository, + private val personidentService: PersonidentService, + private val utvidetBehandlingService: UtvidetBehandlingService, +) { + @PutMapping(path = ["{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun oppdaterUtenlandskPeriodebeløp( + @PathVariable behandlingId: Long, + @Valid @RequestBody + restUtenlandskPeriodebeløp: RestUtenlandskPeriodebeløp, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Oppdaterer utenlandsk periodebeløp", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + val barnAktører = restUtenlandskPeriodebeløp.barnIdenter.map { personidentService.hentAktør(it) } + + val eksisterendeUtenlandskPeriodeBeløp = utenlandskPeriodebeløpRepository.getById(restUtenlandskPeriodebeløp.id) + + val utenlandskPeriodebeløp = + restUtenlandskPeriodebeløp.tilUtenlandskPeriodebeløp(barnAktører, eksisterendeUtenlandskPeriodeBeløp) + + utenlandskPeriodebeløpService + .oppdaterUtenlandskPeriodebeløp(BehandlingId(behandlingId), utenlandskPeriodebeløp) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId))) + } + + @DeleteMapping(path = ["{behandlingId}/{utenlandskPeriodebeløpId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun slettUtenlandskPeriodebeløp( + @PathVariable behandlingId: Long, + @PathVariable utenlandskPeriodebeløpId: Long, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.DELETE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Sletter utenlandsk periodebeløp", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + utenlandskPeriodebeløpService.slettUtenlandskPeriodebeløp(BehandlingId(behandlingId), utenlandskPeriodebeløpId) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pRepository.kt" new file mode 100644 index 000000000..525aaec7b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pRepository.kt" @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp + +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import org.springframework.data.jpa.repository.Query + +interface UtenlandskPeriodebeløpRepository : PeriodeOgBarnSkjemaRepository { + + @Query("SELECT upb FROM UtenlandskPeriodebeløp upb WHERE upb.behandlingId = :behandlingId") + override fun finnFraBehandlingId(behandlingId: Long): Collection +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pService.kt" new file mode 100644 index 000000000..3b633c3da --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pService.kt" @@ -0,0 +1,32 @@ +package no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp + +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEndringAbonnent +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class UtenlandskPeriodebeløpService( + utenlandskPeriodebeløpRepository: PeriodeOgBarnSkjemaRepository, + endringsabonnenter: Collection>, +) { + val skjemaService = PeriodeOgBarnSkjemaService( + utenlandskPeriodebeløpRepository, + endringsabonnenter, + ) + + fun hentUtenlandskePeriodebeløp(behandlingId: BehandlingId) = + skjemaService.hentMedBehandlingId(behandlingId) + + fun oppdaterUtenlandskPeriodebeløp(behandlingId: BehandlingId, utenlandskPeriodebeløp: UtenlandskPeriodebeløp) = + skjemaService.endreSkjemaer(behandlingId, utenlandskPeriodebeløp) + + fun slettUtenlandskPeriodebeløp(behandlingId: BehandlingId, utenlandskPeriodebeløpId: Long) = + skjemaService.slettSkjema(behandlingId, utenlandskPeriodebeløpId) + + @Transactional + fun kopierOgErstattUtenlandskPeriodebeløp(fraBehandlingId: BehandlingId, tilBehandlingId: BehandlingId) = + skjemaService.kopierOgErstattSkjemaer(fraBehandlingId, tilBehandlingId) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/Valutakurs.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/Valutakurs.kt" new file mode 100644 index 000000000..a4663c2d2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/Valutakurs.kt" @@ -0,0 +1,81 @@ +package no.nav.familie.ba.sak.kjerne.eøs.valutakurs + +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.JoinTable +import jakarta.persistence.ManyToMany +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.YearMonthConverter +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEntitet +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Valutakurs") +@Table(name = "VALUTAKURS") +data class Valutakurs( + @Column(name = "fom", columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + override val fom: YearMonth?, + + @Column(name = "tom", columnDefinition = "DATE") + @Convert(converter = YearMonthConverter::class) + override val tom: YearMonth?, + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "AKTOER_TIL_VALUTAKURS", + joinColumns = [JoinColumn(name = "fk_valutakurs_id")], + inverseJoinColumns = [JoinColumn(name = "fk_aktoer_id")], + ) + override val barnAktører: Set = emptySet(), + + @Column(name = "valutakursdato", columnDefinition = "DATE") + val valutakursdato: LocalDate? = null, + + @Column(name = "valutakode") + val valutakode: String? = null, + + @Column(name = "kurs", nullable = false) + val kurs: BigDecimal? = null, +) : PeriodeOgBarnSkjemaEntitet() { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "valutakurs_seq_generator") + @SequenceGenerator( + name = "valutakurs_seq_generator", + sequenceName = "valutakurs_seq", + allocationSize = 50, + ) + override var id: Long = 0 + + @Column(name = "fk_behandling_id", updatable = false, nullable = false) + override var behandlingId: Long = 0 + + // Valutakode skal alltid være satt (muligens til null), så den slettes ikke + override fun utenInnhold() = copy( + valutakursdato = null, + kurs = null, + ) + + override fun kopier(fom: YearMonth?, tom: YearMonth?, barnAktører: Set) = + copy( + fom = fom, + tom = tom, + barnAktører = barnAktører, + ) + + companion object { + val NULL = Valutakurs(null, null, emptySet()) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursController.kt" new file mode 100644 index 000000000..e71b96996 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursController.kt" @@ -0,0 +1,106 @@ +package no.nav.familie.ba.sak.kjerne.eøs.valutakurs + +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.ekstern.restDomene.RestValutakurs +import no.nav.familie.ba.sak.ekstern.restDomene.tilValutakurs +import no.nav.familie.ba.sak.integrasjoner.ecb.ECBService +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate + +@RestController +@RequestMapping("/api/differanseberegning/valutakurs") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class ValutakursController( + private val tilgangService: TilgangService, + private val valutakursService: ValutakursService, + private val personidentService: PersonidentService, + private val utvidetBehandlingService: UtvidetBehandlingService, + private val ecbService: ECBService, +) { + @PutMapping(path = ["{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun oppdaterValutakurs( + @PathVariable behandlingId: Long, + @RequestBody restValutakurs: RestValutakurs, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Oppdaterer valutakurs", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + val barnAktører = restValutakurs.barnIdenter.map { personidentService.hentAktør(it) } + + val valutaKurs = if (skalManueltSetteValutakurs(restValutakurs)) { + restValutakurs.tilValutakurs(barnAktører) + } else { + oppdaterValutakursMedKursFraECB(restValutakurs, restValutakurs.tilValutakurs(barnAktører = barnAktører)) + } + + valutakursService.oppdaterValutakurs(BehandlingId(behandlingId), valutaKurs) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @DeleteMapping(path = ["{behandlingId}/{valutakursId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun slettValutakurs( + @PathVariable behandlingId: Long, + @PathVariable valutakursId: Long, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.DELETE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Sletter valutakurs", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + valutakursService.slettValutakurs(BehandlingId(behandlingId), valutakursId) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + private fun oppdaterValutakursMedKursFraECB(restValutakurs: RestValutakurs, valutakurs: Valutakurs) = + if (valutakursErEndret(restValutakurs, valutakursService.hentValutakurs(restValutakurs.id))) { + valutakurs.copy( + kurs = ecbService.hentValutakurs( + restValutakurs.valutakode!!, + restValutakurs.valutakursdato!!, + ), + ) + } else { + valutakurs + } + + /** + * Sjekker om valuta er Islandske Kroner og kursdato er før 01.02.2018 + */ + private fun skalManueltSetteValutakurs(restValutakurs: RestValutakurs): Boolean { + return restValutakurs.valutakursdato != null && restValutakurs.valutakode == "ISK" && restValutakurs.valutakursdato.isBefore( + LocalDate.of(2018, 2, 1), + ) + } + + /** + * Sjekker om *restValutakurs* inneholder nødvendige verdier og sammenligner disse med *eksisterendeValutakurs* + */ + private fun valutakursErEndret(restValutakurs: RestValutakurs, eksisterendeValutakurs: Valutakurs): Boolean { + return restValutakurs.valutakode != null && restValutakurs.valutakursdato != null && (eksisterendeValutakurs.valutakursdato != restValutakurs.valutakursdato || eksisterendeValutakurs.valutakode != restValutakurs.valutakode) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursRepository.kt" new file mode 100644 index 000000000..5f095a643 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursRepository.kt" @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.kjerne.eøs.valutakurs + +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import org.springframework.data.jpa.repository.Query + +interface ValutakursRepository : PeriodeOgBarnSkjemaRepository { + + @Query("SELECT vk FROM Valutakurs vk WHERE vk.behandlingId = :behandlingId") + override fun finnFraBehandlingId(behandlingId: Long): List +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursService.kt" new file mode 100644 index 000000000..ad239689c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursService.kt" @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.kjerne.eøs.valutakurs + +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEndringAbonnent +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class ValutakursService( + valutakursRepository: PeriodeOgBarnSkjemaRepository, + endringsabonnenter: Collection>, +) { + val skjemaService = PeriodeOgBarnSkjemaService( + valutakursRepository, + endringsabonnenter, + ) + + fun hentValutakurs(valutakursId: Long): Valutakurs = skjemaService.hentMedId(valutakursId) + + fun hentValutakurser(behandlingId: BehandlingId) = + skjemaService.hentMedBehandlingId(behandlingId) + + fun oppdaterValutakurs(behandlingId: BehandlingId, valutakurs: Valutakurs) = + skjemaService.endreSkjemaer(behandlingId, valutakurs) + + fun slettValutakurs(behandlingId: BehandlingId, valutakursId: Long) = + skjemaService.slettSkjema(behandlingId, valutakursId) + + @Transactional + fun kopierOgErstattValutakurser(fraBehandlingId: BehandlingId, tilBehandlingId: BehandlingId) = + skjemaService.kopierOgErstattSkjemaer(fraBehandlingId, tilBehandlingId) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/EndretUtbetalingAndelTidslinjeService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/EndretUtbetalingAndelTidslinjeService.kt" new file mode 100644 index 000000000..d146d7d7e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/EndretUtbetalingAndelTidslinjeService.kt" @@ -0,0 +1,42 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering + +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunktEllerUendeligSent +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunktEllerUendeligTidlig +import org.springframework.stereotype.Service +import java.math.BigDecimal + +@Service +class EndretUtbetalingAndelTidslinjeService( + val endretUtbetalingAndelHentOgPersisterService: EndretUtbetalingAndelHentOgPersisterService, +) { + fun hentBarnasSkalIkkeUtbetalesTidslinjer(behandlingId: BehandlingId) = + endretUtbetalingAndelHentOgPersisterService + .hentForBehandling(behandlingId.id) + .tilBarnasSkalIkkeUtbetalesTidslinjer() +} + +internal fun Iterable.tilBarnasSkalIkkeUtbetalesTidslinjer(): Map> { + return this + .filter { it.årsak in listOf(Årsak.ETTERBETALING_3ÅR, Årsak.ALLEREDE_UTBETALT, Årsak.ENDRE_MOTTAKER) && it.prosent == BigDecimal.ZERO } + .filter { it.person?.type == PersonType.BARN } + .filter { it.person?.aktør != null } + .groupBy { it.person?.aktør!! } + .mapValues { (_, endringer) -> endringer.map { it.tilPeriode { true } } } + .mapValues { (_, perioder) -> tidslinje { perioder } } +} + +private fun EndretUtbetalingAndel.tilPeriode(mapper: (EndretUtbetalingAndel) -> I?) = Periode( + fraOgMed = this.fom.tilTidspunktEllerUendeligTidlig(tom), + tilOgMed = this.tom.tilTidspunktEllerUendeligSent(fom), + innhold = mapper(this), +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/RegelverkKombinatorer.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/RegelverkKombinatorer.kt" new file mode 100644 index 000000000..ef7348d7a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/RegelverkKombinatorer.kt" @@ -0,0 +1,58 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat.IKKE_FULLT_VURDERT +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat.OPPFYLT_BLANDET_REGELVERK +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat.OPPFYLT_EØS_FORORDNINGEN +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat.OPPFYLT_NASJONALE_REGLER +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat.OPPFYLT_REGELVERK_IKKE_SATT +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår + +fun kombinerVilkårResultaterTilRegelverkResultat( + personType: PersonType, + alleVilkårResultater: Iterable, + fagsakType: FagsakType, + behandlingUnderkategori: BehandlingUnderkategori, +): RegelverkResultat? { + val nødvendigeVilkår = Vilkår.hentVilkårFor( + personType = personType, + fagsakType = fagsakType, + behandlingUnderkategori = behandlingUnderkategori, + ) + .filter { it != Vilkår.UTVIDET_BARNETRYGD } + + val regelverkVilkår = nødvendigeVilkår + .filter { it.harRegelverk } + + val alleVilkårResultaterMedEøs = alleVilkårResultater + .filter { it.regelverk == Regelverk.EØS_FORORDNINGEN }.map { it.vilkår } + + val alleVilkårResultaterMedNasjonalt = alleVilkårResultater + .filter { it.regelverk == Regelverk.NASJONALE_REGLER }.map { it.vilkår } + + val erAlleVilkårUtenResultat = alleVilkårResultater.all { it.resultat == null } + + val erAlleNødvendigeVilkårOppfylt = alleVilkårResultater.all { it.resultat == Resultat.OPPFYLT } && + alleVilkårResultater.map { it.vilkår }.distinct().containsAll(nødvendigeVilkår) + + val erEttEllerFlereVilkårIkkeOppfylt = alleVilkårResultater.any { it.resultat == Resultat.IKKE_OPPFYLT } + + return when { + erAlleVilkårUtenResultat -> null + erEttEllerFlereVilkårIkkeOppfylt -> RegelverkResultat.IKKE_OPPFYLT + erAlleNødvendigeVilkårOppfylt -> when { + alleVilkårResultaterMedEøs.containsAll(regelverkVilkår) -> + OPPFYLT_EØS_FORORDNINGEN + alleVilkårResultaterMedNasjonalt.containsAll(regelverkVilkår) -> + OPPFYLT_NASJONALE_REGLER + (alleVilkårResultaterMedEøs + alleVilkårResultaterMedNasjonalt).isNotEmpty() -> + OPPFYLT_BLANDET_REGELVERK + else -> OPPFYLT_REGELVERK_IKKE_SATT + } + else -> IKKE_FULLT_VURDERT + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsresultatDagTidslinje.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsresultatDagTidslinje.kt" new file mode 100644 index 000000000..7f95dbb40 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsresultatDagTidslinje.kt" @@ -0,0 +1,36 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligSent +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligTidlig +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat + +/** + * Lager tidslinje av VilkårRegelverkResultat for ett vilkår og én aktør + * For beregning er vi strengt tatt bare interessert i oppfylte vilkår, og her fjernes alle andre vilkårsresultater + * Antakelsen er at IKKE_OPPFYLT i ALLE tilfeller kan ignoreres for beregning, og evt bare brukes for info i brev + * Løser problemet med BOR_MED_SØKER-vilkår som kan være oppfylt mens undervilkåret DELT_BOSTED ikke er oppfylt. + * Ikke oppfylt DELT_BOSTED er løst funksjonelt ved at BOR_MED_SØKER settes til IKKE_OPPFYLT med fom og tom lik null. + * fom og tom lik null tolkes som fra uendelig lenge siden til uendelig lenge til, som ville skapt overlapp med oppfylt vilkår + * Overlapp er ikke støttet av tidsliner, og ville gitt exception + */ +fun Iterable.tilVilkårRegelverkResultatTidslinje() = tidslinje { + this.filter { it.erOppfylt() } + .map { it.tilPeriode() } +} + +fun VilkårResultat.tilPeriode(): Periode { + val fom = periodeFom.tilTidspunktEllerUendeligTidlig(periodeTom) + val tom = periodeTom.tilTidspunktEllerUendeligSent(periodeFom) + return Periode( + fom, + tom, + VilkårRegelverkResultat( + vilkår = vilkårType, + regelverkResultat = this.tilRegelverkResultat(), + utdypendeVilkårsvurderinger = this.utdypendeVilkårsvurderinger, + ), + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsresultatM\303\245nedTidslinje.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsresultatM\303\245nedTidslinje.kt" new file mode 100644 index 000000000..ae4cb4473 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsresultatM\303\245nedTidslinje.kt" @@ -0,0 +1,25 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering + +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.tilMånedFraMånedsskifteIkkeNull + +/** + * Extension-funksjon som konverterer en dag-basert tidslinje til en måned-basert tidslinje med VilkårRegelverkResultat + * Funksjonen itererer fra måneden FØR fra-og-med-måned til måneden ETTER til-og-med-måneden for å ta hensyn til uendelighet + * Reglene er at vilkårret for siste dag i forrige måned og første dag i inneværende måned må være oppfylt + * Da brukes regelverket for inneværende måned. Dvs slik: + * 2020-04-30 | 2020-05-01 -> Resultat + * Oppfylt EØS | Oppfylt Nasj. -> 2020-05 Oppfylt Nasj + * Oppfylt Nasj | Opppfylt EØS -> 2020-05 Oppfylt EØS + * Oppfylt Nasj | Opppfylt Nasj -> 2020-05 Oppfylt Nasj + * Oppfylt EØS | Opppfylt EØS -> 2020-05 Oppfylt EØS + * Oppfylt EØS | Ikke oppfylt -> + * Oppfylt Nasj | Ikke oppfylt -> + * Ikke oppfylt | Oppfylt EØS -> + * Ikke oppfylt | Oppfylt Nasj -> + */ +fun Tidslinje.tilMånedsbasertTidslinjeForVilkårRegelverkResultat() = this + .tilMånedFraMånedsskifteIkkeNull { sisteDagForrigeMåned, førsteDagDenneMåned -> + if (sisteDagForrigeMåned.erOppfylt() && førsteDagDenneMåned.erOppfylt()) førsteDagDenneMåned else null + } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTidslinjeService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTidslinjeService.kt" new file mode 100644 index 000000000..171f1ba3a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTidslinjeService.kt" @@ -0,0 +1,51 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering + +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.lagForskjøvetTidslinjeForOppfylteVilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import org.springframework.stereotype.Service + +@Service +class VilkårsvurderingTidslinjeService( + private val vilkårsvurderingRepository: VilkårsvurderingRepository, + private val vilkårsvurderingService: VilkårsvurderingService, + private val persongrunnlagService: PersongrunnlagService, +) { + + fun hentTidslinjerThrows(behandlingId: BehandlingId): VilkårsvurderingTidslinjer { + val vilkårsvurdering = vilkårsvurderingRepository.findByBehandlingAndAktiv(behandlingId = behandlingId.id)!! + val søkerOgBarn = persongrunnlagService.hentSøkerOgBarnPåBehandlingThrows(behandlingId = behandlingId.id) + + return VilkårsvurderingTidslinjer( + vilkårsvurdering = vilkårsvurdering, + søkerOgBarn = søkerOgBarn, + ) + } + + fun hentTidslinjer(behandlingId: BehandlingId): VilkårsvurderingTidslinjer? { + return try { + hentTidslinjerThrows(behandlingId) + } catch (exception: NullPointerException) { + return null + } + } + + fun hentAnnenForelderOmfattetAvNorskLovgivningTidslinje(behandlingId: BehandlingId): Tidslinje { + val søker = persongrunnlagService.hentAktivThrows(behandlingId = behandlingId.id).søker + val søkerPersonresultater = vilkårsvurderingService.hentAktivForBehandlingThrows(behandlingId = behandlingId.id) + .personResultater.single { it.aktør == søker.aktør } + + val erAnnenForelderOmfattetAvNorskLovgivingTidslinje = søkerPersonresultater.vilkårResultater + .lagForskjøvetTidslinjeForOppfylteVilkår(Vilkår.BOSATT_I_RIKET) + .map { it?.utdypendeVilkårsvurderinger?.contains(UtdypendeVilkårsvurdering.ANNEN_FORELDER_OMFATTET_AV_NORSK_LOVGIVNING) } + + return erAnnenForelderOmfattetAvNorskLovgivingTidslinje + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTidslinjer.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTidslinjer.kt" new file mode 100644 index 000000000..bfab45dfd --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTidslinjer.kt" @@ -0,0 +1,133 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering + +import no.nav.familie.ba.sak.common.erUnder18ÅrVilkårTidslinje +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonEnkel +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.barn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.søker +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNull +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.beskjærEtter +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering + +class VilkårsvurderingTidslinjer( + vilkårsvurdering: Vilkårsvurdering, + søkerOgBarn: List, +) { + private val barna: List = søkerOgBarn.barn() + private val søker: Aktør = søkerOgBarn.søker().aktør + + internal val barnOgFødselsdatoer = barna.associate { it.aktør to it.fødselsdato } + + private val aktørTilPersonResultater = + vilkårsvurdering.personResultater.associateBy { it.aktør } + + private val vilkårsresultaterTidslinjeMap = aktørTilPersonResultater + .entries.associate { (aktør, personResultat) -> + aktør to personResultat.vilkårResultater.groupBy { it.vilkårType } + .map { it.value.tilVilkårRegelverkResultatTidslinje() } + } + + private val søkersTidslinje: SøkersTidslinjer = + SøkersTidslinjer( + tidslinjer = this, + aktør = søker, + fagsakType = vilkårsvurdering.behandling.fagsak.type, + behandlingUnderkategori = vilkårsvurdering.behandling.underkategori, + ) + + fun søkersTidslinjer(): SøkersTidslinjer = søkersTidslinje + + private val barnasTidslinjer: Map = + barna.map { + it.aktør to BarnetsTidslinjer( + tidslinjer = this, + aktør = it.aktør, + fagsakType = vilkårsvurdering.behandling.fagsak.type, + behandlingUnderkategori = vilkårsvurdering.behandling.underkategori, + ) + }.toMap() + + fun forBarn(barn: Person) = barnasTidslinjer[barn.aktør]!! + + fun barnasTidslinjer(): Map = barnasTidslinjer + + class SøkersTidslinjer( + tidslinjer: VilkårsvurderingTidslinjer, + aktør: Aktør, + fagsakType: FagsakType, + behandlingUnderkategori: BehandlingUnderkategori, + ) { + val vilkårsresultatTidslinjer = tidslinjer.vilkårsresultaterTidslinjeMap[aktør] ?: listOf(TomTidslinje()) + + private val vilkårsresultatMånedTidslinjer = + vilkårsresultatTidslinjer.map { it.tilMånedsbasertTidslinjeForVilkårRegelverkResultat() } + + val regelverkResultatTidslinje = vilkårsresultatMånedTidslinjer + .kombinerUtenNull { + kombinerVilkårResultaterTilRegelverkResultat( + personType = PersonType.SØKER, + alleVilkårResultater = it, + fagsakType = fagsakType, + behandlingUnderkategori = behandlingUnderkategori, + ) + } + } + + class BarnetsTidslinjer( + tidslinjer: VilkårsvurderingTidslinjer, + aktør: Aktør, + fagsakType: FagsakType, + behandlingUnderkategori: BehandlingUnderkategori, + ) { + private val søkersTidslinje = tidslinjer.søkersTidslinje + + val vilkårsresultatTidslinjer: List> = + tidslinjer.vilkårsresultaterTidslinjeMap[aktør] ?: listOf(TomTidslinje()) + + private val vilkårsresultatMånedTidslinjer: List> = + vilkårsresultatTidslinjer.map { it.tilMånedsbasertTidslinjeForVilkårRegelverkResultat() } + + val erUnder18ÅrVilkårTidslinje = erUnder18ÅrVilkårTidslinje(tidslinjer.barnOgFødselsdatoer.getValue(aktør)) + + val egetRegelverkResultatTidslinje: Tidslinje = + vilkårsresultatMånedTidslinjer + .kombinerUtenNull { + kombinerVilkårResultaterTilRegelverkResultat( + personType = PersonType.BARN, + alleVilkårResultater = it, + fagsakType = fagsakType, + behandlingUnderkategori = behandlingUnderkategori, + ) + } + .beskjærEtter(erUnder18ÅrVilkårTidslinje) + + val regelverkResultatTidslinje = egetRegelverkResultatTidslinje + .kombinerMed(søkersTidslinje.regelverkResultatTidslinje) { barnetsResultat, søkersResultat -> + KombinertRegelverkResultat( + barnetsResultat = barnetsResultat, + søkersResultat = søkersResultat, + ) + } + // Barnets egne tidslinjer kan på dette tidspunktet strekke seg 18 år frem i tid, + // og mye lenger enn søkers regelverk-tidslinje, som skal være begrensningen. Derfor besjærer vi mot den + .beskjærEtter(søkersTidslinje.regelverkResultatTidslinje) + } +} + +fun VilkårsvurderingTidslinjer.harBlandetRegelverk(): Boolean { + return søkersTidslinjer().regelverkResultatTidslinje.inneholder(RegelverkResultat.OPPFYLT_BLANDET_REGELVERK) || + barnasTidslinjer().values.any { it.egetRegelverkResultatTidslinje.inneholder(RegelverkResultat.OPPFYLT_BLANDET_REGELVERK) } +} + +fun Tidslinje.inneholder(innhold: I): Boolean = + this.perioder().any { it.innhold == innhold } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/rest/RestTidslinjer.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/rest/RestTidslinjer.kt" new file mode 100644 index 000000000..9f66dccc0 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/rest/RestTidslinjer.kt" @@ -0,0 +1,89 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.rest + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårRegelverkResultat +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjer +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNull +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilFørsteDagIMåneden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilSisteDagIMåneden +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.beskjærEtter +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.beskjærTilOgMedEtter +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.tilDag +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import java.time.LocalDate + +fun VilkårsvurderingTidslinjer.tilRestTidslinjer(): RestTidslinjer { + val barnasTidslinjer = this.barnasTidslinjer() + val søkersTidslinjer = this.søkersTidslinjer() + + val erNoenAvBarnaMellom0Og18ÅrTidslinje: Tidslinje = barnasTidslinjer.values + .map { it.erUnder18ÅrVilkårTidslinje } + .kombinerUtenNull { barnaEr0Til18ÅrListe -> barnaEr0Til18ÅrListe.any { it } } + + return RestTidslinjer( + barnasTidslinjer = barnasTidslinjer.entries.associate { + val erUnder18årTidslinje = it.value.erUnder18ÅrVilkårTidslinje + it.key.aktivFødselsnummer() to RestTidslinjerForBarn( + vilkårTidslinjer = it.value.vilkårsresultatTidslinjer.map { + it.beskjærEtter(erUnder18årTidslinje.tilDag()) + .tilRestTidslinje() + }, + oppfyllerEgneVilkårIKombinasjonMedSøkerTidslinje = it.value + .regelverkResultatTidslinje + .map { it?.kombinertResultat?.resultat } + .beskjærEtter(erUnder18årTidslinje) + .tilRestTidslinje(), + regelverkTidslinje = it.value.regelverkResultatTidslinje + .map { it?.kombinertResultat?.regelverk } + .beskjærEtter(erUnder18årTidslinje) + .tilRestTidslinje(), + ) + }, + søkersTidslinjer = RestTidslinjerForSøker( + vilkårTidslinjer = søkersTidslinjer.vilkårsresultatTidslinjer.map { + it.beskjærTilOgMedEtter(erNoenAvBarnaMellom0Og18ÅrTidslinje.tilDag()) + .tilRestTidslinje() + }, + oppfyllerEgneVilkårTidslinje = søkersTidslinjer + .regelverkResultatTidslinje.map { it?.resultat } + .beskjærTilOgMedEtter(erNoenAvBarnaMellom0Og18ÅrTidslinje) + .tilRestTidslinje(), + ), + ) +} + +fun Tidslinje.tilRestTidslinje(): List> = + this.filtrerIkkeNull().perioder().map { periode -> + RestTidslinjePeriode( + fraOgMed = periode.fraOgMed.tilFørsteDagIMåneden().tilLocalDate(), + tilOgMed = periode.tilOgMed.tilSisteDagIMåneden().tilLocalDate(), + innhold = periode.innhold!!, + ) + } + +data class RestTidslinjer( + val barnasTidslinjer: Map, + val søkersTidslinjer: RestTidslinjerForSøker, +) + +data class RestTidslinjerForBarn( + val vilkårTidslinjer: List>>, + val oppfyllerEgneVilkårIKombinasjonMedSøkerTidslinje: List>, + val regelverkTidslinje: List>, +) + +data class RestTidslinjerForSøker( + val vilkårTidslinjer: List>>, + val oppfyllerEgneVilkårTidslinje: List>, +) + +data class RestTidslinjePeriode( + val fraOgMed: LocalDate, + val tilOgMed: LocalDate?, + val innhold: T, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/rest/TidslinjeController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/rest/TidslinjeController.kt" new file mode 100644 index 000000000..515eff804 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/rest/TidslinjeController.kt" @@ -0,0 +1,40 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.rest + +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjeService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.Ressurs.Companion.success +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/tidslinjer") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class TidslinjeController( + private val tilgangService: TilgangService, + private val tidslinjeService: VilkårsvurderingTidslinjeService, +) { + + @GetMapping("/{behandlingId}") + fun hentTidslinjer(@PathVariable behandlingId: Long): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.VEILEDER, + handling = "Henter tidslinjer", + ) + return ResponseEntity.ok( + success( + tidslinjeService.hentTidslinjerThrows(BehandlingId(behandlingId)).tilRestTidslinjer(), + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/Fagsak.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/Fagsak.kt new file mode 100644 index 000000000..05badc9c2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/Fagsak.kt @@ -0,0 +1,89 @@ +package no.nav.familie.ba.sak.kjerne.fagsak + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.kjerne.institusjon.Institusjon +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import java.util.Objects + +@Entity(name = "Fagsak") +@Table(name = "FAGSAK") +data class Fagsak( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "fagsak_seq_generator") + @SequenceGenerator(name = "fagsak_seq_generator", sequenceName = "fagsak_seq", allocationSize = 50) + val id: Long = 0, + + @OneToOne(optional = false) + @JoinColumn( + name = "fk_aktoer_id", + nullable = false, + updatable = false, + ) + val aktør: Aktør, + + @ManyToOne(optional = true) + @JoinColumn( + name = "fk_institusjon_id", + nullable = true, + updatable = true, + ) + var institusjon: Institusjon? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + var status: FagsakStatus = FagsakStatus.OPPRETTET, + + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false) + val type: FagsakType = FagsakType.NORMAL, + + @Column(name = "arkivert", nullable = false) + var arkivert: Boolean = false, +) : BaseEntitet() { + + override fun hashCode(): Int { + return Objects.hashCode(id) + } + + override fun toString(): String { + return "Fagsak(id=$id)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Fagsak + + if (id != other.id) return false + + return true + } +} + +enum class FagsakStatus { + OPPRETTET, + LØPENDE, // Har minst én behandling gjeldende for fremtidig utbetaling + AVSLUTTET, +} + +enum class FagsakType { + NORMAL, + BARN_ENSLIG_MINDREÅRIG, + INSTITUSJON, + ; + + fun erBarnSøker() = this == BARN_ENSLIG_MINDREÅRIG || this == INSTITUSJON +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakController.kt new file mode 100644 index 000000000..2d725afcd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakController.kt @@ -0,0 +1,236 @@ +package no.nav.familie.ba.sak.kjerne.fagsak + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.RessursUtils.illegalState +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.InstitusjonInfo +import no.nav.familie.ba.sak.ekstern.restDomene.RestFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestFagsakDeltager +import no.nav.familie.ba.sak.ekstern.restDomene.RestHentFagsakForPerson +import no.nav.familie.ba.sak.ekstern.restDomene.RestHentFagsakerForPerson +import no.nav.familie.ba.sak.ekstern.restDomene.RestMinimalFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestSøkParam +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.kjerne.tilbakekreving.TilbakekrevingService +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/fagsaker") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class FagsakController( + private val fagsakService: FagsakService, + private val personidentService: PersonidentService, + private val tilgangService: TilgangService, + private val tilbakekrevingService: TilbakekrevingService, +) { + + @PostMapping(produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentEllerOpprettFagsak(@RequestBody fagsakRequest: FagsakRequest): ResponseEntity> { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} henter eller oppretter ny fagsak") + tilgangService.validerTilgangTilPersoner( + personIdenter = listOf(fagsakRequest.personIdent), + event = AuditLoggerEvent.CREATE, + ) + tilgangService.verifiserHarTilgangTilHandling(BehandlerRolle.SAKSBEHANDLER, "opprette fagsak") + + return Result.runCatching { fagsakService.hentEllerOpprettFagsak(fagsakRequest) } + .fold( + onSuccess = { ResponseEntity.status(HttpStatus.CREATED).body(it) }, + onFailure = { throw it }, + ) + } + + @GetMapping(path = ["/{fagsakId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentRestFagsak(@PathVariable fagsakId: Long): ResponseEntity> { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} henter fagsak med id $fagsakId") + tilgangService.validerTilgangTilFagsak(fagsakId = fagsakId, event = AuditLoggerEvent.ACCESS) + + val fagsak = fagsakService.hentRestFagsak(fagsakId) + return ResponseEntity.ok().body(fagsak) + } + + @GetMapping(path = ["/minimal/{fagsakId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentMinimalFagsak(@PathVariable fagsakId: Long): ResponseEntity> { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} henter minimal fagsak med id $fagsakId") + tilgangService.validerTilgangTilFagsak(fagsakId = fagsakId, event = AuditLoggerEvent.ACCESS) + + val fagsak = fagsakService.hentRestMinimalFagsak(fagsakId) + return ResponseEntity.ok().body(fagsak) + } + + @PostMapping(path = ["/hent-fagsak-paa-person"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentMinimalFagsakForPerson(@RequestBody request: RestHentFagsakForPerson): ResponseEntity> { + tilgangService.validerTilgangTilPersoner( + personIdenter = listOf(request.personIdent), + event = AuditLoggerEvent.ACCESS, + ) + + return Result.runCatching { + val aktør = personidentService.hentAktør(request.personIdent) + fagsakService.hentMinimalFagsakForPerson(aktør, request.fagsakType) + }.fold( + onSuccess = { return ResponseEntity.ok().body(it) }, + onFailure = { illegalState("Ukjent feil ved henting data for manuell journalføring.", it) }, + ) + } + + @PostMapping(path = ["/hent-fagsaker-paa-person"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentMinimalFagsakerForPerson( + @RequestBody + request: RestHentFagsakerForPerson, + ): ResponseEntity>> { + tilgangService.validerTilgangTilPersoner( + personIdenter = listOf(request.personIdent), + event = AuditLoggerEvent.ACCESS, + ) + + return Result.runCatching { + val aktør = personidentService.hentAktør(request.personIdent) + fagsakService.hentMinimalFagsakerForPerson(aktør = aktør, fagsakTyper = request.fagsakTyper) + }.fold( + onSuccess = { return ResponseEntity.ok().body(it) }, + onFailure = { illegalState("Ukjent feil ved henting data for manuell journalføring.", it) }, + ) + } + + @PostMapping(path = ["/sok"]) + fun søkFagsak(@RequestBody søkParam: RestSøkParam): ResponseEntity>> { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} søker fagsak") + + val fagsakDeltagere = fagsakService.hentFagsakDeltager(søkParam.personIdent) + return ResponseEntity.ok().body(Ressurs.success(fagsakDeltagere)) + } + + @PostMapping(path = ["/sok/fagsaker-hvor-person-er-deltaker"]) + fun søkFagsakerHvorPersonErSøkerEllerMottarOrdinærBarnetrygd(@RequestBody request: RestSøkFagsakRequest): ResponseEntity>> { + tilgangService.validerTilgangTilPersoner( + personIdenter = listOf(request.personIdent), + event = AuditLoggerEvent.ACCESS, + ) + + val aktør = personidentService.hentAktør(request.personIdent) + + val fagsakerHvorAktørErSøkerEllerMottarLøpendeOrdinær = + fagsakService.finnAlleFagsakerHvorAktørErSøkerEllerMottarLøpendeOrdinær(aktør) + + val fagsakIdOgTilknyttetAktørId = fagsakerHvorAktørErSøkerEllerMottarLøpendeOrdinær.map { + RestFagsakIdOgTilknyttetAktørId(aktørId = it.aktør.aktørId, fagsakId = it.id) + } + + return ResponseEntity.ok().body(Ressurs.success(fagsakIdOgTilknyttetAktørId)) + } + + @PostMapping(path = ["/sok/fagsaker-hvor-person-mottar-lopende-ytelse"]) + fun søkFagsakerHvorPersonMottarLøpendeYtelse(@RequestBody request: RestSøkFagsakRequest): ResponseEntity>> { + tilgangService.validerTilgangTilPersoner( + personIdenter = listOf(request.personIdent), + event = AuditLoggerEvent.ACCESS, + ) + + val aktør = personidentService.hentAktør(request.personIdent) + + val fagsakerHvorAktørMottarLøpendeUtvidetEllerOrdinær = + fagsakService.finnAlleFagsakerHvorAktørHarLøpendeYtelseAvType( + aktør = aktør, + ytelseTyper = listOf(YtelseType.ORDINÆR_BARNETRYGD, YtelseType.UTVIDET_BARNETRYGD), + ) + + val fagsakIdOgTilknyttetAktørId = fagsakerHvorAktørMottarLøpendeUtvidetEllerOrdinær.map { + RestFagsakIdOgTilknyttetAktørId(aktørId = it.aktør.aktørId, fagsakId = it.id) + } + + return ResponseEntity.ok().body(Ressurs.success(fagsakIdOgTilknyttetAktørId)) + } + + data class RestSøkFagsakRequest(val personIdent: String) + data class RestFagsakIdOgTilknyttetAktørId( + val aktørId: String, + val fagsakId: Long, + ) + + @PostMapping(path = ["/sok/fagsakdeltagere"]) + fun oppgiFagsakdeltagere(@RequestBody restSøkParam: RestSøkParam): ResponseEntity>> { + return Result.runCatching { + val aktør = personidentService.hentAktør(restSøkParam.personIdent) + val barnsAktørId = personidentService.hentAktørIder(restSøkParam.barnasIdenter) + + fagsakService.oppgiFagsakdeltagere(aktør, barnsAktørId) + } + .fold( + onSuccess = { ResponseEntity.ok(Ressurs.success(it)) }, + onFailure = { + logger.info("Henting av fagsakdeltagere feilet.") + secureLogger.info("Henting av fagsakdeltagere feilet: ${it.message}", it) + ResponseEntity + .status(if (it is Feil) it.httpStatus else HttpStatus.OK) + .body( + Ressurs.failure( + error = it, + errorMessage = "Henting av fagsakdeltagere feilet: ${it.message}", + ), + ) + }, + ) + } + + @GetMapping(path = ["/{fagsakId}/har-apen-tilbakekreving"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun harÅpenTilbakekreving(@PathVariable fagsakId: Long): ResponseEntity> { + return ResponseEntity.ok( + Ressurs.success(tilbakekrevingService.søkerHarÅpenTilbakekreving(fagsakId)), + ) + } + + @GetMapping(path = ["/{fagsakId}/opprett-tilbakekreving"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun opprettTilbakekrevingsbehandling(@PathVariable fagsakId: Long): Ressurs { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "opprette tilbakekrevingbehandling", + ) + + return tilbakekrevingService.opprettTilbakekrevingsbehandlingManuelt(fagsakId) + } + + companion object { + + private val logger: Logger = LoggerFactory.getLogger(FagsakController::class.java) + } +} + +data class FagsakRequest( + val personIdent: String, + val fagsakType: FagsakType? = FagsakType.NORMAL, + val institusjon: InstitusjonInfo? = null, +) + +data class RestBeslutningPåVedtak( + val beslutning: Beslutning, + val begrunnelse: String? = null, + val kontrollerteSider: List = emptyList(), +) + +enum class Beslutning { + GODKJENT, + UNDERKJENT, + ; + + fun erGodkjent() = this == GODKJENT +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakRepository.kt new file mode 100644 index 000000000..cfbbc182a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakRepository.kt @@ -0,0 +1,195 @@ +package no.nav.familie.ba.sak.kjerne.fagsak + +import io.micrometer.core.annotation.Timed +import jakarta.persistence.LockModeType +import no.nav.familie.ba.sak.ekstern.skatteetaten.UtvidetSkatt +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Slice +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.YearMonth +import java.util.Optional + +@Repository +interface FagsakRepository : JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_FORCE_INCREMENT) + fun save(fagsak: Fagsak): Fagsak + + @Lock(LockModeType.NONE) + override fun findById(id: Long): Optional + + @Lock(LockModeType.NONE) + @Query(value = "SELECT f FROM Fagsak f WHERE f.id = :fagsakId AND f.arkivert = false") + fun finnFagsak(fagsakId: Long): Fagsak? + + @Lock(LockModeType.NONE) + @Query(value = "SELECT f FROM Fagsak f WHERE f.aktør = :aktør and f.type = :type and f.arkivert = false") + fun finnFagsakForAktør(aktør: Aktør, type: FagsakType = FagsakType.NORMAL): Fagsak? + + @Lock(LockModeType.NONE) + @Query(value = "SELECT f FROM Fagsak f WHERE f.aktør = :aktør and f.type = 'INSTITUSJON' and f.status <> 'AVSLUTTET' and f.arkivert = false and f.institusjon.orgNummer = :orgNummer") + fun finnFagsakForInstitusjonOgOrgnummer(aktør: Aktør, orgNummer: String): Fagsak? + + @Lock(LockModeType.NONE) + @Query(value = "SELECT f FROM Fagsak f WHERE f.aktør = :aktør and f.arkivert = false") + fun finnFagsakerForAktør(aktør: Aktør): List + + @Lock(LockModeType.NONE) + @Query(value = "SELECT f from Fagsak f WHERE f.status = 'LØPENDE' AND f.arkivert = false") + fun finnLøpendeFagsaker(): List + + @Lock(LockModeType.NONE) + @Query(value = "SELECT f.id from Fagsak f WHERE f.status = 'LØPENDE' AND f.arkivert = false") + fun finnLøpendeFagsaker(page: Pageable): Slice + + @Query( + value = """SELECT f.id + FROM Fagsak f + WHERE NOT EXISTS ( + SELECT 1 + FROM satskjoering + WHERE fk_fagsak_id = f.id + AND sats_tid = :satsTidspunkt + ) AND f.status = 'LØPENDE' AND f.arkivert = false""", + nativeQuery = true, + ) + fun finnLøpendeFagsakerForSatsendring(satsTidspunkt: LocalDate, page: Pageable): Page + + @Query( + value = """WITH sisteiverksatte AS ( + SELECT DISTINCT ON (b.fk_fagsak_id) b.id, b.fk_fagsak_id, stonad_tom + FROM behandling b + INNER JOIN tilkjent_ytelse ty ON b.id = ty.fk_behandling_id + INNER JOIN fagsak f ON f.id = b.fk_fagsak_id + WHERE f.status = 'LØPENDE' + AND f.arkivert = FALSE + ORDER BY b.fk_fagsak_id, b.aktivert_tid DESC) + + SELECT silp.fk_fagsak_id + FROM sisteiverksatte silp + WHERE silp.stonad_tom < DATE_TRUNC('month', NOW())""", + nativeQuery = true, + ) + fun finnFagsakerSomSkalAvsluttes(): List + + /** + * Denne skal plukke fagsaker som løper _og_ har barn født innenfor anngitt tidsintervall. + * Brukes til å sende ut automatiske brev ved reduksjon 6 og 18 år blant annet. + * Ved 18 år og dersom hele fagsaken opphører så skal det ikke sendes ut brev og derfor sjekker + * vi kun løpende fagsaker. + */ + @Query( + value = """ + SELECT f FROM Fagsak f + WHERE f.arkivert = false AND f.status = 'LØPENDE' AND f IN ( + SELECT b.fagsak FROM Behandling b + WHERE b.aktiv=true AND b.id IN ( + SELECT pg.behandlingId FROM PersonopplysningGrunnlag pg + WHERE pg.aktiv=true AND pg.id IN ( + SELECT p.personopplysningGrunnlag FROM Person p + WHERE p.fødselsdato BETWEEN :fom AND :tom + AND p.type = 'BARN' + ) + ) + ) + """, + ) + fun finnLøpendeFagsakMedBarnMedFødselsdatoInnenfor(fom: LocalDate, tom: LocalDate): Set + + @Lock(LockModeType.NONE) + @Query(value = "SELECT count(*) from Fagsak where arkivert = false") + fun finnAntallFagsakerTotalt(): Long + + @Query(value = "SELECT f from Fagsak f where f.arkivert = false") + fun hentFagsakerSomIkkeErArkivert(): List + + @Lock(LockModeType.NONE) + @Query(value = "SELECT count(*) from Fagsak f where f.status='LØPENDE' and f.arkivert = false") + fun finnAntallFagsakerLøpende(): Long + + @Query( + value = """ + SELECT p.foedselsnummer AS fnr, + MAX(ty.endret_dato) AS sistevedtaksdato + FROM andel_tilkjent_ytelse aty + INNER JOIN + tilkjent_ytelse ty ON aty.tilkjent_ytelse_id = ty.id + INNER JOIN personident p ON aty.fk_aktoer_id = p.fk_aktoer_id + WHERE ty.utbetalingsoppdrag IS NOT NULL + AND aty.type = 'UTVIDET_BARNETRYGD' + AND aty.stonad_fom <= :tom + AND aty.stonad_tom >= :fom + AND p.aktiv = TRUE + GROUP BY p.foedselsnummer + """, + nativeQuery = true, + ) + @Timed + fun finnFagsakerMedUtvidetBarnetrygdInnenfor(fom: LocalDateTime, tom: LocalDateTime): List + + @Query( + """ + SELECT DISTINCT b.fagsak.id + FROM AndelTilkjentYtelse aty + JOIN Behandling b ON b.id = aty.behandlingId + JOIN TilkjentYtelse ty ON b.id = ty.behandling.id + WHERE + b.id in :iverksatteLøpendeBehandlinger + AND NOT EXISTS (SELECT b2 from Behandling b2 where b2.fagsak.id = b.fagsak.id AND b2.status <> 'AVSLUTTET') + AND NOT EXISTS (SELECT aty2 from AndelTilkjentYtelse aty2 where aty2.behandlingId = b.id AND aty2.type = 'SMÅBARNSTILLEGG' AND aty.stønadFom = :innværendeMåned) + AND aty.type = 'SMÅBARNSTILLEGG' + AND aty.stønadTom = :stønadTom + """, + ) + fun finnAlleFagsakerMedOpphørSmåbarnstilleggIMåned( + iverksatteLøpendeBehandlinger: List, + stønadTom: YearMonth = YearMonth.now().minusMonths(1), + innværendeMåned: YearMonth = YearMonth.now(), + ): List + + @Query( + """ + SELECT DISTINCT b.fagsak.id + FROM AndelTilkjentYtelse aty + JOIN Behandling b ON b.id = aty.behandlingId + JOIN TilkjentYtelse ty ON b.id = ty.behandling.id + WHERE + b.id in :iverksatteLøpendeBehandlinger + AND NOT EXISTS (SELECT b2 from Behandling b2 where b2.fagsak.id = b.fagsak.id AND b2.status <> 'AVSLUTTET') + AND aty.type = 'SMÅBARNSTILLEGG' + AND aty.stønadFom = :stønadFom + """, + ) + fun finnAlleFagsakerMedOppstartSmåbarnstilleggIMåned( + iverksatteLøpendeBehandlinger: List, + stønadFom: YearMonth = YearMonth.now(), + ): List + + @Query( + """ + SELECT distinct f from Fagsak f + JOIN Behandling b ON b.fagsak.id = f.id + JOIN AndelTilkjentYtelse aty ON aty.behandlingId = b.id + WHERE aty.aktør = :aktør + """, + ) + fun finnFagsakerSomHarAndelerForAktør(aktør: Aktør): List + + @Query( + """ + SELECT distinct f FROM Fagsak f + JOIN Behandling b ON f.id = b.fagsak.id + WHERE f.status = 'LØPENDE' AND b.opprettetÅrsak in ('HELMANUELL_MIGRERING', 'MIGRERING') AND b.resultat NOT IN ('HENLAGT_FEILAKTIG_OPPRETTET', 'HENLAGT_SØKNAD_TRUKKET', 'HENLAGT_AUTOMATISK_FØDSELSHENDELSE', 'HENLAGT_TEKNISK_VEDLIKEHOLD') + GROUP BY f.id + HAVING COUNT(*) >= 2 + """, + ) + fun finnFagsakerMedFlereMigreringsbehandlinger(): List +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakService.kt new file mode 100644 index 000000000..c2a49cb0e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakService.kt @@ -0,0 +1,525 @@ +package no.nav.familie.ba.sak.kjerne.fagsak + +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.ekstern.restDomene.FagsakDeltagerRolle +import no.nav.familie.ba.sak.ekstern.restDomene.InstitusjonInfo +import no.nav.familie.ba.sak.ekstern.restDomene.RestBaseFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestFagsakDeltager +import no.nav.familie.ba.sak.ekstern.restDomene.RestMinimalFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestMinimalFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestVisningBehandling +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollService +import no.nav.familie.ba.sak.integrasjoner.organisasjon.OrganisasjonService +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.integrasjoner.skyggesak.SkyggesakService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.BehandlingstemaService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonRepository +import no.nav.familie.ba.sak.kjerne.institusjon.InstitusjonService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.tilbakekreving.TilbakekrevingsbehandlingService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.statistikk.saksstatistikk.SaksstatistikkEventPublisher +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.client.HttpStatusCodeException +import java.time.LocalDate +import java.time.Period + +@Service +class FagsakService( + private val fagsakRepository: FagsakRepository, + private val personRepository: PersonRepository, + private val andelerTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val personidentService: PersonidentService, + private val behandlingstemaService: BehandlingstemaService, + private val utvidetBehandlingService: UtvidetBehandlingService, + private val behandlingService: BehandlingService, + private val vedtakRepository: VedtakRepository, + private val personopplysningerService: PersonopplysningerService, + private val familieIntegrasjonerTilgangskontrollService: FamilieIntegrasjonerTilgangskontrollService, + private val saksstatistikkEventPublisher: SaksstatistikkEventPublisher, + private val skyggesakService: SkyggesakService, + private val vedtaksperiodeService: VedtaksperiodeService, + private val tilbakekrevingsbehandlingService: TilbakekrevingsbehandlingService, + private val institusjonService: InstitusjonService, + private val organisasjonService: OrganisasjonService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, +) { + + private val antallFagsakerOpprettetFraManuell = + Metrics.counter("familie.ba.sak.fagsak.opprettet", "saksbehandling", "manuell") + private val antallFagsakerOpprettetFraAutomatisk = + Metrics.counter("familie.ba.sak.fagsak.opprettet", "saksbehandling", "automatisk") + + @Transactional + fun oppdaterLøpendeStatusPåFagsaker(): Int { + val fagsaker = fagsakRepository.finnFagsakerSomSkalAvsluttes() + for (fagsakId in fagsaker) { + val fagsak = fagsakRepository.getById(fagsakId) + oppdaterStatus(fagsak, FagsakStatus.AVSLUTTET) + } + return fagsaker.size + } + + @Transactional + fun hentEllerOpprettFagsak(fagsakRequest: FagsakRequest): Ressurs { + val fagsak = hentEllerOpprettFagsak( + fagsakRequest.personIdent, + type = fagsakRequest.fagsakType ?: FagsakType.NORMAL, + institusjon = fagsakRequest.institusjon, + ) + return hentRestMinimalFagsak(fagsakId = fagsak.id) + } + + @Transactional + fun hentEllerOpprettFagsak( + personIdent: String, + fraAutomatiskBehandling: Boolean = false, + type: FagsakType = FagsakType.NORMAL, + institusjon: InstitusjonInfo? = null, + ): Fagsak { + val aktør = personidentService.hentOgLagreAktør(personIdent, true) + + val eksisterendeFagsak = when (type) { + FagsakType.INSTITUSJON -> { + if (institusjon?.orgNummer == null) throw FunksjonellFeil("Mangler påkrevd variabel orgnummer for institusjon") + fagsakRepository.finnFagsakForInstitusjonOgOrgnummer(aktør, institusjon.orgNummer) + } + else -> fagsakRepository.finnFagsakForAktør(aktør, type) + } + + return if (eksisterendeFagsak == null) { + val nyFagsak = Fagsak(aktør = aktør, type = type) + if (fraAutomatiskBehandling) { + antallFagsakerOpprettetFraAutomatisk.increment() + } else { + antallFagsakerOpprettetFraManuell.increment() + } + + if (type == FagsakType.INSTITUSJON) { + institusjonService.hentEllerOpprettInstitusjon(institusjon?.orgNummer!!, institusjon.tssEksternId) + .apply { + nyFagsak.institusjon = this + } + } + val nyOgLagretFagsak = lagre(nyFagsak) + skyggesakService.opprettSkyggesak(nyOgLagretFagsak) + nyOgLagretFagsak + } else { + eksisterendeFagsak + } + } + + fun hentFagsakerPåPerson(aktør: Aktør): List { + return personRepository.findFagsakerByAktør(aktør) + } + + @Transactional + fun lagre(fagsak: Fagsak): Fagsak { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} oppretter fagsak $fagsak") + return fagsakRepository.save(fagsak).also { saksstatistikkEventPublisher.publiserSaksstatistikk(it.id) } + } + + fun oppdaterStatus(fagsak: Fagsak, nyStatus: FagsakStatus): Fagsak { + logger.info( + "${SikkerhetContext.hentSaksbehandlerNavn()} endrer status på fagsak ${fagsak.id} fra ${fagsak.status}" + + " til $nyStatus", + ) + fagsak.status = nyStatus + + return lagre(fagsak) + } + + fun hentMinimalFagsakForPerson( + aktør: Aktør, + fagsakType: FagsakType = FagsakType.NORMAL, + ): Ressurs { + val fagsak = fagsakRepository.finnFagsakForAktør(aktør, fagsakType) + return if (fagsak != null) { + Ressurs.success(data = lagRestMinimalFagsak(fagsakId = fagsak.id)) + } else { + Ressurs.failure( + errorMessage = "Fant ikke fagsak på person", + ) + } + } + + fun hentMinimalFagsakerForPerson( + aktør: Aktør, + fagsakTyper: List = FagsakType.values().toList(), + ): Ressurs> { + val fagsaker = fagsakRepository.finnFagsakerForAktør(aktør).filter { fagsakTyper.contains(it.type) } + return if (!fagsaker.isEmpty()) { + Ressurs.success(data = lagRestMinimalFagsaker(fagsaker)) + } else { + Ressurs.failure( + errorMessage = "Fant ikke fagsaker på person", + ) + } + } + + fun hentRestFagsak(fagsakId: Long): Ressurs = Ressurs.success(data = lagRestFagsak(fagsakId)) + + fun hentRestMinimalFagsak(fagsakId: Long): Ressurs = + Ressurs.success(data = lagRestMinimalFagsak(fagsakId)) + + fun lagRestMinimalFagsaker(fagsaker: List): List { + return fagsaker.map { lagRestMinimalFagsak(it.id) } + } + + fun lagRestMinimalFagsak(fagsakId: Long): RestMinimalFagsak { + val restBaseFagsak = lagRestBaseFagsak(fagsakId) + + val tilbakekrevingsbehandlinger = + tilbakekrevingsbehandlingService.hentRestTilbakekrevingsbehandlinger((fagsakId)) + val visningsbehandlinger = behandlingHentOgPersisterService.hentBehandlinger(fagsakId = fagsakId).map { + it.tilRestVisningBehandling( + vedtaksdato = vedtakRepository.finnVedtaksdatoForBehandling(it.id), + ) + } + val migreringsdato = behandlingService.hentMigreringsdatoPåFagsak(fagsakId) + return restBaseFagsak.tilRestMinimalFagsak( + restVisningBehandlinger = visningsbehandlinger, + tilbakekrevingsbehandlinger = tilbakekrevingsbehandlinger, + migreringsdato = migreringsdato, + ) + } + + private fun lagRestFagsak(fagsakId: Long): RestFagsak { + val restBaseFagsak = lagRestBaseFagsak(fagsakId) + + val tilbakekrevingsbehandlinger = + tilbakekrevingsbehandlingService.hentRestTilbakekrevingsbehandlinger((fagsakId)) + val utvidedeBehandlinger = + behandlingHentOgPersisterService.hentBehandlinger(fagsakId = fagsakId) + .map { utvidetBehandlingService.lagRestUtvidetBehandling(it.id) } + + return restBaseFagsak.tilRestFagsak(utvidedeBehandlinger, tilbakekrevingsbehandlinger) + } + + private fun lagRestBaseFagsak(fagsakId: Long): RestBaseFagsak { + val fagsak = hentPåFagsakId(fagsakId = fagsakId) + + val aktivBehandling = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = fagsakId) + + val sistVedtatteBehandling = behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = fagsakId) + + val gjeldendeUtbetalingsperioder = + if (sistVedtatteBehandling != null) vedtaksperiodeService.hentUtbetalingsperioder(behandling = sistVedtatteBehandling) else emptyList() + + return RestBaseFagsak( + opprettetTidspunkt = fagsak.opprettetTidspunkt, + id = fagsak.id, + søkerFødselsnummer = fagsak.aktør.aktivFødselsnummer(), + status = fagsak.status, + underBehandling = + if (aktivBehandling == null) { + false + } else { + aktivBehandling.status == BehandlingStatus.UTREDES || (aktivBehandling.steg >= StegType.BESLUTTE_VEDTAK && aktivBehandling.steg != StegType.BEHANDLING_AVSLUTTET) + }, + løpendeKategori = behandlingstemaService.hentLøpendeKategori(fagsakId = fagsakId), + løpendeUnderkategori = behandlingstemaService.hentLøpendeUnderkategori(fagsakId = fagsakId), + gjeldendeUtbetalingsperioder = gjeldendeUtbetalingsperioder, + fagsakType = fagsak.type, + institusjon = fagsak.institusjon?.let { + InstitusjonInfo( + orgNummer = it.orgNummer, + tssEksternId = it.tssEksternId, + navn = organisasjonService.hentOrganisasjon(it.orgNummer).navn, + ) + }, + ) + } + + @Transactional + fun hentEllerOpprettFagsakForPersonIdent( + fødselsnummer: String, + fraAutomatiskBehandling: Boolean = false, + fagsakType: FagsakType = FagsakType.NORMAL, + institusjon: InstitusjonInfo? = null, + ): Fagsak { + return hentEllerOpprettFagsak(fødselsnummer, fraAutomatiskBehandling, fagsakType, institusjon) + } + + fun hentNormalFagsak(aktør: Aktør): Fagsak? { + return fagsakRepository.finnFagsakForAktør( + aktør, + FagsakType.NORMAL, + ) + } + + fun hentPåFagsakId(fagsakId: Long): Fagsak { + return fagsakRepository.finnFagsak(fagsakId) ?: throw FunksjonellFeil( + melding = "Finner ikke fagsak med id $fagsakId", + frontendFeilmelding = "Finner ikke fagsak med id $fagsakId", + ) + } + + fun hentAktør(fagsakId: Long): Aktør { + return hentPåFagsakId(fagsakId).aktør + } + + fun hentFagsakPåPerson(aktør: Aktør, fagsakType: FagsakType = FagsakType.NORMAL): Fagsak? { + return fagsakRepository.finnFagsakForAktør(aktør, fagsakType) + } + + fun hentAlleFagsakerForAktør(aktør: Aktør): List { + return fagsakRepository.finnFagsakerForAktør(aktør) + } + + fun hentLøpendeFagsaker(): List { + return fagsakRepository.finnLøpendeFagsaker() + } + + fun hentFagsakDeltager(personIdent: String): List { + val aktør = personidentService.hentAktør(personIdent) + + val maskertDeltaker = runCatching { + hentMaskertFagsakdeltakerVedManglendeTilgang(aktør) + }.fold( + onSuccess = { it }, + onFailure = { return sjekkStatuskodeOgHåndterFeil(it) }, + ) + + if (maskertDeltaker != null) { + return listOf(maskertDeltaker) + } + + val personInfoMedRelasjoner = runCatching { + personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(aktør) + }.fold( + onSuccess = { it }, + onFailure = { return sjekkStatuskodeOgHåndterFeil(it) }, + ) + val assosierteFagsakDeltagere = hentAssosierteFagsakdeltagere(aktør, personInfoMedRelasjoner) + + val erBarn = Period.between(personInfoMedRelasjoner.fødselsdato, LocalDate.now()).years < 18 + + val fagsaker = fagsakRepository.finnFagsakerForAktør(aktør).ifEmpty { listOf(null) } + fagsaker.forEach { fagsak -> + if (assosierteFagsakDeltagere.find { it.ident == aktør.aktivFødselsnummer() && it.fagsakId == fagsak?.id } == null) { + assosierteFagsakDeltagere.add( + RestFagsakDeltager( + navn = personInfoMedRelasjoner.navn, + ident = aktør.aktivFødselsnummer(), + // we set the role to unknown when the person is not a child because the person may not have a child + rolle = if (erBarn) FagsakDeltagerRolle.BARN else FagsakDeltagerRolle.UKJENT, + kjønn = personInfoMedRelasjoner.kjønn, + fagsakId = fagsak?.id, + fagsakType = fagsak?.type, + ), + ) + } + } + + if (erBarn) { + personInfoMedRelasjoner.forelderBarnRelasjon.filter { relasjon -> + relasjon.relasjonsrolle == FORELDERBARNRELASJONROLLE.FAR || + relasjon.relasjonsrolle == FORELDERBARNRELASJONROLLE.MOR || + relasjon.relasjonsrolle == FORELDERBARNRELASJONROLLE.MEDMOR + }.forEach { relasjon -> + if (assosierteFagsakDeltagere.find { fagsakDeltager -> + fagsakDeltager.ident == relasjon.aktør.aktivFødselsnummer() + } == null + ) { + val maskertForelder = + hentMaskertFagsakdeltakerVedManglendeTilgang(relasjon.aktør) + if (maskertForelder != null) { + assosierteFagsakDeltagere.add(maskertForelder.copy(rolle = FagsakDeltagerRolle.FORELDER)) + } else { + val forelderInfo = runCatching { + personopplysningerService.hentPersoninfoEnkel(relasjon.aktør) + }.fold( + onSuccess = { it }, + onFailure = { + throw IllegalStateException("Feil ved henting av person fra PDL", it) + }, + ) + + val fagsakerForRelasjon = fagsakRepository.finnFagsakerForAktør(relasjon.aktør).ifEmpty { listOf(null) } + fagsakerForRelasjon.forEach { fagsak -> + assosierteFagsakDeltagere.add( + RestFagsakDeltager( + navn = forelderInfo.navn, + ident = relasjon.aktør.aktivFødselsnummer(), + rolle = FagsakDeltagerRolle.FORELDER, + kjønn = forelderInfo.kjønn, + fagsakId = fagsak?.id, + fagsakType = fagsak?.type, + ), + ) + } + } + } + } + } + return assosierteFagsakDeltagere + } + + private fun sjekkStatuskodeOgHåndterFeil(throwable: Throwable): List { + val clientError = throwable as? HttpStatusCodeException? + return if ((clientError != null && clientError.statusCode == HttpStatus.NOT_FOUND) || + throwable.message?.contains("Fant ikke person") == true + ) { + emptyList() + } else { + throw throwable + } + } + + // We find all cases that either have the given person as applicant, or have it as a child + private fun hentAssosierteFagsakdeltagere( + aktør: Aktør, + personInfoMedRelasjoner: PersonInfo, + ): MutableList { + val assosierteFagsakDeltagerMap = mutableMapOf() + + personRepository.findByAktør(aktør).forEach { person: Person -> + if (person.personopplysningGrunnlag.aktiv) { + val behandling = behandlingHentOgPersisterService.hent(behandlingId = person.personopplysningGrunnlag.behandlingId) + if (behandling.aktiv && !behandling.fagsak.arkivert && !assosierteFagsakDeltagerMap.containsKey( + behandling.fagsak.id, + ) + ) { + // get applicant info from PDL. we assume that the applicant is always a person whose info is stored in PDL. + if (behandling.fagsak.aktør == aktør) { + assosierteFagsakDeltagerMap[behandling.fagsak.id] = RestFagsakDeltager( + navn = personInfoMedRelasjoner.navn, + ident = behandling.fagsak.aktør.aktivFødselsnummer(), + rolle = + if (behandling.fagsak.type == FagsakType.NORMAL) { + FagsakDeltagerRolle.FORELDER + } else { + FagsakDeltagerRolle.UKJENT + }, + kjønn = personInfoMedRelasjoner.kjønn, + fagsakId = behandling.fagsak.id, + fagsakType = behandling.fagsak.type, + ) + } else { + val maskertForelder = + hentMaskertFagsakdeltakerVedManglendeTilgang(behandling.fagsak.aktør) + if (maskertForelder != null) { + assosierteFagsakDeltagerMap[behandling.fagsak.id] = + maskertForelder.copy( + rolle = FagsakDeltagerRolle.FORELDER, + fagsakType = behandling.fagsak.type, + ) + } else { + val personinfo = + runCatching { + personopplysningerService.hentPersoninfoEnkel(behandling.fagsak.aktør) + }.fold( + onSuccess = { it }, + onFailure = { + throw IllegalStateException("Feil ved henting av person fra PDL", it) + }, + ) + + assosierteFagsakDeltagerMap[behandling.fagsak.id] = RestFagsakDeltager( + navn = personinfo.navn, + ident = behandling.fagsak.aktør.aktivFødselsnummer(), + rolle = FagsakDeltagerRolle.FORELDER, + kjønn = personinfo.kjønn, + fagsakId = behandling.fagsak.id, + fagsakType = behandling.fagsak.type, + ) + } + } + } + } + } + + // The given person and its parents may be included in the result, no matter whether they have a case. + return assosierteFagsakDeltagerMap.values.toMutableList() + } + + private fun hentMaskertFagsakdeltakerVedManglendeTilgang(aktør: Aktør): RestFagsakDeltager? { + return familieIntegrasjonerTilgangskontrollService.hentMaskertPersonInfoVedManglendeTilgang(aktør) + ?.let { + RestFagsakDeltager( + rolle = FagsakDeltagerRolle.UKJENT, + adressebeskyttelseGradering = it.adressebeskyttelseGradering, + harTilgang = false, + ) + } + } + + fun finnAlleFagsakerHvorAktørHarLøpendeYtelseAvType(aktør: Aktør, ytelseTyper: List): List { + val ordinæreAndelerPåAktør = andelerTilkjentYtelseRepository.finnAndelerTilkjentYtelseForAktør(aktør = aktør) + .filter { it.type in ytelseTyper } + + val løpendeAndeler = ordinæreAndelerPåAktør.filter { it.erLøpende() } + + val behandlingerMedLøpendeAndeler = løpendeAndeler + .map { it.behandlingId }.toSet() + .map { behandlingHentOgPersisterService.hent(behandlingId = it) } + + val behandlingerSomErSisteIverksattePåFagsak = behandlingerMedLøpendeAndeler.filter { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(it.fagsak.id) == it } + + return behandlingerSomErSisteIverksattePåFagsak.map { it.fagsak } + } + + fun finnAlleFagsakerHvorAktørErSøkerEllerMottarLøpendeOrdinær(aktør: Aktør): List { + val alleLøpendeFagsakerPåAktør = hentAlleFagsakerForAktør(aktør).filter { it.status == FagsakStatus.LØPENDE } + + val fagsakerHvorAktørHarLøpendeOrdinærBarnetrygd = finnAlleFagsakerHvorAktørHarLøpendeYtelseAvType(aktør = aktør, ytelseTyper = listOf(YtelseType.ORDINÆR_BARNETRYGD)) + + return (alleLøpendeFagsakerPåAktør + fagsakerHvorAktørHarLøpendeOrdinærBarnetrygd).distinct() + } + + fun oppgiFagsakdeltagere(aktør: Aktør, barnasAktørId: List): List { + val fagsakDeltagere = mutableListOf() + + hentFagsakPåPerson(aktør)?.also { fagsak -> + fagsakDeltagere.add( + RestFagsakDeltager( + ident = aktør.aktivFødselsnummer(), + fagsakId = fagsak.id, + fagsakStatus = fagsak.status, + rolle = FagsakDeltagerRolle.FORELDER, + ), + ) + } + + barnasAktørId.forEach { barnsAktørId -> + hentFagsakerPåPerson(barnsAktørId).toSet().forEach { fagsak -> + fagsakDeltagere.add( + RestFagsakDeltager( + ident = barnsAktørId.aktivFødselsnummer(), + fagsakId = fagsak.id, + fagsakStatus = fagsak.status, + rolle = FagsakDeltagerRolle.BARN, + ), + ) + } + } + + return fagsakDeltagere + } + + companion object { + + private val logger = LoggerFactory.getLogger(FagsakService::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakStatusScheduler.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakStatusScheduler.kt new file mode 100644 index 000000000..876b3b742 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakStatusScheduler.kt @@ -0,0 +1,41 @@ +package no.nav.familie.ba.sak.kjerne.fagsak + +import no.nav.familie.ba.sak.common.EnvService +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.task.OppdaterLøpendeFlagg +import no.nav.familie.leader.LeaderClient +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component + +@Component +class FagsakStatusScheduler( + private val taskRepository: TaskRepositoryWrapper, + private val envService: EnvService, +) { + + /* + * Siden barnetrygd er en månedsytelse vil en fagsak alltid løpe ut en måned + * Det er derfor nok å finne alle fagsaker som ikke lenger har noen løpende utbetalinger den 1 hver måned. + */ + + @Scheduled(cron = "\${CRON_FAGSAKSTATUS_SCHEDULER}") + fun oppdaterFagsakStatuser() { + when (LeaderClient.isLeader() == true || envService.erDev()) { + true -> { + val oppdaterLøpendeFlaggTask = Task(type = OppdaterLøpendeFlagg.TASK_STEP_TYPE, payload = "") + taskRepository.save(oppdaterLøpendeFlaggTask) + logger.info("Opprettet oppdaterLøpendeFlaggTask") + } + false -> { + logger.info("Ikke opprettet oppdaterLøpendeFlaggTask på denne poden") + } + } + } + + companion object { + + private val logger = LoggerFactory.getLogger(FagsakStatusScheduler::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIEndretUtbetalingAndelUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIEndretUtbetalingAndelUtil.kt new file mode 100644 index 000000000..aa37633cd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIEndretUtbetalingAndelUtil.kt @@ -0,0 +1,63 @@ +package no.nav.familie.ba.sak.kjerne.forrigebehandling + +import no.nav.familie.ba.sak.kjerne.beregning.EndretUtbetalingAndelTidslinje +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.forrigebehandling.EndringUtil.tilFørsteEndringstidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombiner +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNullMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import java.time.YearMonth + +object EndringIEndretUtbetalingAndelUtil { + + fun utledEndringstidspunktForEndretUtbetalingAndel( + nåværendeEndretAndeler: List, + forrigeEndretAndeler: List, + ): YearMonth? { + val endringIEndretUtbetalingAndelTidslinje = lagEndringIEndretUtbetalingAndelTidslinje( + nåværendeEndretAndeler = nåværendeEndretAndeler, + forrigeEndretAndeler = forrigeEndretAndeler, + ) + + return endringIEndretUtbetalingAndelTidslinje.tilFørsteEndringstidspunkt() + } + + fun lagEndringIEndretUtbetalingAndelTidslinje( + nåværendeEndretAndeler: List, + forrigeEndretAndeler: List, + ): Tidslinje { + val allePersoner = (nåværendeEndretAndeler.mapNotNull { it.person?.aktør } + forrigeEndretAndeler.mapNotNull { it.person?.aktør }).distinct() + + val tidslinjePerPerson = allePersoner.map { aktør -> + lagEndringIEndretUbetalingAndelPerPersonTidslinje( + nåværendeEndretAndelerForPerson = nåværendeEndretAndeler.filter { it.person?.aktør == aktør }, + forrigeEndretAndelerForPerson = forrigeEndretAndeler.filter { it.person?.aktør == aktør }, + ) + } + + return tidslinjePerPerson.kombiner { finnesMinstEnEndringIPeriode(it) } + } + + private fun finnesMinstEnEndringIPeriode( + endringer: Iterable, + ): Boolean = endringer.any { it } + + private fun lagEndringIEndretUbetalingAndelPerPersonTidslinje( + nåværendeEndretAndelerForPerson: List, + forrigeEndretAndelerForPerson: List, + ): Tidslinje { + val nåværendeTidslinje = EndretUtbetalingAndelTidslinje(nåværendeEndretAndelerForPerson) + val forrigeTidslinje = EndretUtbetalingAndelTidslinje(forrigeEndretAndelerForPerson) + + val endringerTidslinje = nåværendeTidslinje.kombinerUtenNullMed(forrigeTidslinje) { nåværende, forrige -> + ( + nåværende.avtaletidspunktDeltBosted != forrige.avtaletidspunktDeltBosted || + nåværende.årsak != forrige.årsak || + nåværende.søknadstidspunkt != forrige.søknadstidspunkt + ) + } + + return endringerTidslinje + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIKompetanseUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIKompetanseUtil.kt new file mode 100644 index 000000000..66d5d14ee --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIKompetanseUtil.kt @@ -0,0 +1,68 @@ +package no.nav.familie.ba.sak.kjerne.forrigebehandling + +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.tilTidslinje +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.forrigebehandling.EndringUtil.tilFørsteEndringstidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombiner +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNullMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import java.time.YearMonth + +object EndringIKompetanseUtil { + + fun utledEndringstidspunktForKompetanse( + nåværendeKompetanser: List, + forrigeKompetanser: List, + ): YearMonth? { + val endringIKompetanseTidslinje = lagEndringIKompetanseTidslinje( + nåværendeKompetanser = nåværendeKompetanser, + forrigeKompetanser = forrigeKompetanser, + ) + + return endringIKompetanseTidslinje.tilFørsteEndringstidspunkt() + } + + fun lagEndringIKompetanseTidslinje( + nåværendeKompetanser: List, + forrigeKompetanser: List, + ): Tidslinje { + val allePersonerMedKompetanser = (nåværendeKompetanser.flatMap { it.barnAktører } + forrigeKompetanser.flatMap { it.barnAktører }).distinct() + + val endringstidslinjerPrPerson = allePersonerMedKompetanser.map { aktør -> + lagEndringIKompetanseForPersonTidslinje( + nåværendeKompetanserForPerson = nåværendeKompetanser.filter { it.barnAktører.contains(aktør) }, + forrigeKompetanserForPerson = forrigeKompetanser.filter { it.barnAktører.contains(aktør) }, + ) + } + + return endringstidslinjerPrPerson.kombiner { finnesMinstEnEndringIPeriode(it) } + } + + private fun finnesMinstEnEndringIPeriode( + endringer: Iterable, + ): Boolean = endringer.any { it } + + private fun lagEndringIKompetanseForPersonTidslinje( + nåværendeKompetanserForPerson: List, + forrigeKompetanserForPerson: List, + ): Tidslinje { + val nåværendeTidslinje = nåværendeKompetanserForPerson.tilTidslinje() + val forrigeTidslinje = forrigeKompetanserForPerson.tilTidslinje() + + val endringerTidslinje = nåværendeTidslinje.kombinerUtenNullMed(forrigeTidslinje) { nåværende, forrige -> + forrige.erObligatoriskeFelterUtenomTidsperioderSatt() && nåværende.felterHarEndretSegSidenForrigeBehandling(forrigeKompetanse = forrige) + } + + return endringerTidslinje + } + + private fun Kompetanse.felterHarEndretSegSidenForrigeBehandling(forrigeKompetanse: Kompetanse): Boolean { + return this.søkersAktivitet != forrigeKompetanse.søkersAktivitet || + this.søkersAktivitetsland != forrigeKompetanse.søkersAktivitetsland || + this.annenForeldersAktivitet != forrigeKompetanse.annenForeldersAktivitet || + this.annenForeldersAktivitetsland != forrigeKompetanse.annenForeldersAktivitetsland || + this.barnetsBostedsland != forrigeKompetanse.barnetsBostedsland || + this.resultat != forrigeKompetanse.resultat + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIUtbetalingUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIUtbetalingUtil.kt new file mode 100644 index 000000000..4442fbaf2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIUtbetalingUtil.kt @@ -0,0 +1,86 @@ +package no.nav.familie.ba.sak.kjerne.forrigebehandling + +import no.nav.familie.ba.sak.kjerne.beregning.AndelTilkjentYtelseTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.forrigebehandling.EndringUtil.tilFørsteEndringstidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombiner +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import java.time.YearMonth + +object EndringIUtbetalingUtil { + + fun utledEndringstidspunktForUtbetalingsbeløp( + nåværendeAndeler: List, + forrigeAndeler: List, + ): YearMonth? { + val endringIUtbetalingTidslinje = lagEndringIUtbetalingTidslinje( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + ) + + return endringIUtbetalingTidslinje.tilFørsteEndringstidspunkt() + } + + internal fun lagEndringIUtbetalingTidslinje( + nåværendeAndeler: List, + forrigeAndeler: List, + ): Tidslinje { + val allePersonerMedAndeler = (nåværendeAndeler.map { it.aktør } + forrigeAndeler.map { it.aktør }).distinct() + + val endringstidslinjePerPersonOgType = allePersonerMedAndeler.flatMap { aktør -> + val ytelseTyperForPerson = (nåværendeAndeler.map { it.type } + forrigeAndeler.map { it.type }).distinct() + + ytelseTyperForPerson.map { ytelseType -> + lagEndringIUtbetalingForPersonOgTypeTidslinje( + nåværendeAndeler = nåværendeAndeler.filter { it.aktør == aktør && it.type == ytelseType }, + forrigeAndeler = forrigeAndeler.filter { it.aktør == aktør && it.type == ytelseType }, + ) + } + } + + return endringstidslinjePerPersonOgType.kombiner { finnesMinstEnEndringIPeriode(it) } + } + + private fun finnesMinstEnEndringIPeriode( + endringer: Iterable, + ): Boolean = endringer.any { it } + + // Det regnes ikke ut som en endring dersom + // 1. Vi har fått nye andeler som har 0 i utbetalingsbeløp + // 2. Vi har mistet andeler som har hatt 0 i utbetalingsbeløp + // 3. Vi har lik utbetalingsbeløp mellom nåværende og forrige andeler + private fun lagEndringIUtbetalingForPersonOgTypeTidslinje( + nåværendeAndeler: List, + forrigeAndeler: List, + ): Tidslinje { + val nåværendeTidslinje = AndelTilkjentYtelseTidslinje(nåværendeAndeler) + val forrigeTidslinje = AndelTilkjentYtelseTidslinje(forrigeAndeler) + + val endringIBeløpTidslinje = nåværendeTidslinje.kombinerMed(forrigeTidslinje) { nåværende, forrige -> + val nåværendeBeløp = nåværende?.kalkulertUtbetalingsbeløp ?: 0 + val forrigeBeløp = forrige?.kalkulertUtbetalingsbeløp ?: 0 + + nåværendeBeløp != forrigeBeløp + } + + return endringIBeløpTidslinje + } + internal fun lagEtterbetalingstidslinjeForPersonOgType( + nåværendeAndeler: List, + forrigeAndeler: List, + ): Tidslinje { + val nåværendeTidslinje = AndelTilkjentYtelseTidslinje(nåværendeAndeler) + val forrigeTidslinje = AndelTilkjentYtelseTidslinje(forrigeAndeler) + + val etterbetaling = nåværendeTidslinje.kombinerMed(forrigeTidslinje) { nåværende, forrige -> + val nåværendeBeløp = nåværende?.kalkulertUtbetalingsbeløp ?: 0 + val forrigeBeløp = forrige?.kalkulertUtbetalingsbeløp ?: 0 + + nåværendeBeløp > forrigeBeløp + } + + return etterbetaling + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIVilk\303\245rsvurderingUtil.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIVilk\303\245rsvurderingUtil.kt" new file mode 100644 index 000000000..4de6cd05a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIVilk\303\245rsvurderingUtil.kt" @@ -0,0 +1,126 @@ +package no.nav.familie.ba.sak.kjerne.forrigebehandling + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.forrigebehandling.EndringUtil.tilFørsteEndringstidspunkt +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombiner +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNullMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.tilForskjøvetTidslinjeForOppfyltVilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import java.time.YearMonth + +object EndringIVilkårsvurderingUtil { + + fun utledEndringstidspunktForVilkårsvurdering( + nåværendePersonResultat: Set, + forrigePersonResultat: Set, + personerIBehandling: Set, + personerIForrigeBehandling: Set, + ): YearMonth? { + val endringIVilkårsvurderingTidslinje = lagEndringIVilkårsvurderingTidslinje( + nåværendePersonResultater = nåværendePersonResultat, + forrigePersonResultater = forrigePersonResultat, + personerIBehandling = personerIBehandling, + personerIForrigeBehandling = personerIForrigeBehandling, + ) + + return endringIVilkårsvurderingTidslinje.tilFørsteEndringstidspunkt() + } + + fun lagEndringIVilkårsvurderingTidslinje( + nåværendePersonResultater: Set, + forrigePersonResultater: Set, + personerIBehandling: Set, + personerIForrigeBehandling: Set, + ): Tidslinje { + val allePersonerMedPersonResultat = + (nåværendePersonResultater.map { it.aktør } + forrigePersonResultater.map { it.aktør }).distinct() + + val tidslinjerPerPersonOgVilkår = allePersonerMedPersonResultat.flatMap { aktør -> + val personIBehandling = personerIBehandling.singleOrNull { it.aktør == aktør } + val personIForrigeBehandling = personerIForrigeBehandling.singleOrNull { it.aktør == aktør } + + Vilkår.values().map { vilkår -> + lagEndringIVilkårsvurderingForPersonOgVilkårTidslinje( + nåværendeOppfylteVilkårResultater = nåværendePersonResultater + .filter { it.aktør == aktør } + .flatMap { it.vilkårResultater } + .filter { it.vilkårType == vilkår && it.resultat == Resultat.OPPFYLT }, + forrigeOppfylteVilkårResultater = forrigePersonResultater + .filter { it.aktør == aktør } + .flatMap { it.vilkårResultater } + .filter { it.vilkårType == vilkår && it.resultat == Resultat.OPPFYLT }, + vilkår = vilkår, + personIBehandling = personIBehandling, + personIForrigeBehandling = personIForrigeBehandling, + ) + } + } + + return tidslinjerPerPersonOgVilkår.kombiner { finnesMinstEnEndringIPeriode(it) } + } + + private fun finnesMinstEnEndringIPeriode( + endringer: Iterable, + ): Boolean = endringer.any { it } + + // Relevante endringer er + // 1. Endringer i utdypende vilkårsvurdering + // 2. Endringer i regelverk + // 3. Splitt i vilkårsvurderingen + private fun lagEndringIVilkårsvurderingForPersonOgVilkårTidslinje( + nåværendeOppfylteVilkårResultater: List, + forrigeOppfylteVilkårResultater: List, + vilkår: Vilkår, + personIBehandling: Person?, + personIForrigeBehandling: Person?, + ): Tidslinje { + val nåværendeVilkårResultatTidslinje = nåværendeOppfylteVilkårResultater + .tilForskjøvetTidslinjeForOppfyltVilkår(vilkår = vilkår, fødselsdato = personIBehandling?.fødselsdato) + + val tidligereVilkårResultatTidslinje = forrigeOppfylteVilkårResultater + .tilForskjøvetTidslinjeForOppfyltVilkår(vilkår = vilkår, fødselsdato = personIForrigeBehandling?.fødselsdato) + + val endringIVilkårResultat = + nåværendeVilkårResultatTidslinje.kombinerUtenNullMed(tidligereVilkårResultatTidslinje) { nåværende, forrige -> + + val erEndringerIUtdypendeVilkårsvurdering = + nåværende.utdypendeVilkårsvurderinger.toSet() != forrige.utdypendeVilkårsvurderinger.toSet() + val erEndringerIRegelverk = nåværende.vurderesEtter != forrige.vurderesEtter + val erVilkårSomErSplittetOpp = nåværende.periodeFom != forrige.periodeFom + + (forrige.obligatoriskUtdypendeVilkårsvurderingErSatt() && erEndringerIUtdypendeVilkårsvurdering) || + erEndringerIRegelverk || + erVilkårSomErSplittetOpp + } + + return endringIVilkårResultat + } + + private fun VilkårResultat.obligatoriskUtdypendeVilkårsvurderingErSatt(): Boolean { + return this.utdypendeVilkårsvurderinger.isNotEmpty() || !this.utdypendeVilkårsvurderingErObligatorisk() + } + + private fun VilkårResultat.utdypendeVilkårsvurderingErObligatorisk(): Boolean { + return if (this.vurderesEtter == Regelverk.NASJONALE_REGLER) { + false + } else { + when (this.vilkårType) { + Vilkår.BOSATT_I_RIKET, + Vilkår.BOR_MED_SØKER, + -> true + + Vilkår.UNDER_18_ÅR, + Vilkår.LOVLIG_OPPHOLD, + Vilkår.GIFT_PARTNERSKAP, + Vilkår.UTVIDET_BARNETRYGD, + -> false + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringUtil.kt new file mode 100644 index 000000000..bbd5677af --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringUtil.kt @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.kjerne.forrigebehandling + +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth + +object EndringUtil { + internal fun Tidslinje.tilFørsteEndringstidspunkt() = this.perioder().filter { it.innhold == true }.minOfOrNull { it.fraOgMed }?.tilYearMonth() +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/D\303\270dsfall.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/D\303\270dsfall.kt" new file mode 100644 index 000000000..0a71723fc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/D\303\270dsfall.kt" @@ -0,0 +1,97 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.OneToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegisteropplysning +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlKontaktinformasjonForDødsboAdresse +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Dødsfall") +@Table(name = "po_doedsfall") +data class Dødsfall( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "po_doedsfall_seq_generator") + @SequenceGenerator(name = "po_doedsfall_seq_generator", sequenceName = "po_doedsfall_seq", allocationSize = 50) + val id: Long = 0, + + @JsonIgnore + @OneToOne + @JoinColumn(name = "fk_po_person_id", referencedColumnName = "id", nullable = false) + val person: Person, + + @Column(name = "doedsfall_dato", nullable = false) + val dødsfallDato: LocalDate, + + @Column(name = "doedsfall_adresse", nullable = true) + val dødsfallAdresse: String? = null, + + @Column(name = "doedsfall_postnummer", nullable = true) + val dødsfallPostnummer: String? = null, + + @Column(name = "doedsfall_poststed", nullable = true) + val dødsfallPoststed: String? = null, + + @Column(name = "manuell_registrert", nullable = false) + val manuellRegistrert: Boolean = false, +) : BaseEntitet() { + + fun tilKopiForNyPerson(nyPerson: Person): Dødsfall = + copy(id = 0, person = nyPerson) + + fun hentAdresseToString(): String { + return """$dødsfallAdresse, $dødsfallPostnummer $dødsfallPoststed""" + } + + fun tilRestRegisteropplysning() = RestRegisteropplysning( + fom = this.dødsfallDato, + tom = null, + verdi = if (dødsfallAdresse == null) "-" else hentAdresseToString(), + ) +} + +fun lagDødsfallFraPdl( + person: Person, + dødsfallDatoFraPdl: String?, + dødsfallAdresseFraPdl: PdlKontaktinformasjonForDødsboAdresse?, +): Dødsfall? { + if (dødsfallDatoFraPdl.isNullOrBlank()) { + return null + } + return Dødsfall( + person = person, + dødsfallDato = LocalDate.parse(dødsfallDatoFraPdl), + dødsfallAdresse = dødsfallAdresseFraPdl?.adresselinje1, + dødsfallPostnummer = dødsfallAdresseFraPdl?.postnummer, + dødsfallPoststed = dødsfallAdresseFraPdl?.poststedsnavn, + manuellRegistrert = false, + ) +} + +fun lagDødsfall( + person: Person, + dødsfallDato: LocalDate, + dødsfallAdresse: String? = null, + dødsfallPostnummer: String? = null, + dødsfallPoststed: String? = null, + +): Dødsfall { + return Dødsfall( + person = person, + dødsfallDato = dødsfallDato, + dødsfallAdresse = dødsfallAdresse, + dødsfallPostnummer = dødsfallPostnummer, + dødsfallPoststed = dødsfallPoststed, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/Person.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/Person.kt new file mode 100644 index 000000000..38c092b68 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/Person.kt @@ -0,0 +1,203 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import jakarta.persistence.OneToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.arbeidsforhold.GrArbeidsforhold +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrBostedsadresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.opphold.GrOpphold +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.sivilstand.GrSivilstand +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.GrStatsborgerskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.finnNåværendeMedlemskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.finnSterkesteMedlemskap +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.Språkkode +import org.hibernate.annotations.Fetch +import org.hibernate.annotations.FetchMode +import java.time.LocalDate +import java.time.LocalDate.now +import java.time.Period +import java.util.Objects + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Person") +@Table(name = "PO_PERSON") +data class Person( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "po_person_seq_generator") + @SequenceGenerator(name = "po_person_seq_generator", sequenceName = "po_person_seq", allocationSize = 50) + val id: Long = 0, + + // SØKER, BARN, ANNENPART + @Enumerated(EnumType.STRING) + @Column(name = "type") + val type: PersonType, + + @Column(name = "foedselsdato", nullable = false) + val fødselsdato: LocalDate, + + @Column(name = "navn", nullable = false) + val navn: String = "", + + @Enumerated(EnumType.STRING) + @Column(name = "kjoenn", nullable = false) + val kjønn: Kjønn, + + @Enumerated(EnumType.STRING) + @Column(name = "maalform", nullable = false) + val målform: Målform = Målform.NB, + + @JsonIgnore + @ManyToOne(optional = false) + @JoinColumn(name = "fk_gr_personopplysninger_id", nullable = false, updatable = false) + val personopplysningGrunnlag: PersonopplysningGrunnlag, + + @JsonIgnore + @ManyToOne(optional = false) + @JoinColumn(name = "fk_aktoer_id", nullable = false, updatable = false) + val aktør: Aktør, + + @OneToMany(mappedBy = "person", cascade = [CascadeType.ALL], fetch = FetchType.EAGER) + // Workaround før Hibernatebug https://hibernate.atlassian.net/browse/HHH-1718 + @Fetch(value = FetchMode.SUBSELECT) + var bostedsadresser: MutableList = mutableListOf(), + + @OneToMany(mappedBy = "person", cascade = [CascadeType.ALL], fetch = FetchType.EAGER) + // Workaround før Hibernatebug https://hibernate.atlassian.net/browse/HHH-1718 + @Fetch(value = FetchMode.SUBSELECT) + var statsborgerskap: MutableList = mutableListOf(), + + @OneToMany(mappedBy = "person", cascade = [CascadeType.ALL], fetch = FetchType.EAGER) + // Workaround før Hibernatebug https://hibernate.atlassian.net/browse/HHH-1718 + @Fetch(value = FetchMode.SUBSELECT) + var opphold: MutableList = mutableListOf(), + + @OneToMany(mappedBy = "person", cascade = [CascadeType.ALL], fetch = FetchType.EAGER) + // Workaround før Hibernatebug https://hibernate.atlassian.net/browse/HHH-1718 + @Fetch(value = FetchMode.SUBSELECT) + var arbeidsforhold: MutableList = mutableListOf(), + + @OneToMany(mappedBy = "person", cascade = [CascadeType.ALL], fetch = FetchType.EAGER) + // Workaround før Hibernatebug https://hibernate.atlassian.net/browse/HHH-1718 + @Fetch(value = FetchMode.SUBSELECT) + var sivilstander: MutableList = mutableListOf(), + + @OneToOne(mappedBy = "person", cascade = [CascadeType.ALL], fetch = FetchType.EAGER, optional = true) + var dødsfall: Dødsfall? = null, +) : BaseEntitet() { + + fun tilKopiForNyttPersonopplysningGrunnlag(nyttPersonopplysningGrunnlag: PersonopplysningGrunnlag): Person = + copy( + id = 0, + personopplysningGrunnlag = nyttPersonopplysningGrunnlag, + bostedsadresser = mutableListOf(), + statsborgerskap = mutableListOf(), + opphold = mutableListOf(), + arbeidsforhold = mutableListOf(), + sivilstander = mutableListOf(), + ) + .also { + it.bostedsadresser.addAll( + bostedsadresser.map { grBostedsadresse -> + grBostedsadresse.tilKopiForNyPerson( + it, + ) + }, + ) + it.statsborgerskap.addAll( + statsborgerskap.map { grStatsborgerskap -> + grStatsborgerskap.tilKopiForNyPerson( + it, + ) + }, + ) + it.opphold.addAll(opphold.map { grOpphold -> grOpphold.tilKopiForNyPerson(it) }) + it.arbeidsforhold.addAll(arbeidsforhold.map { grArbeidsforhold -> grArbeidsforhold.tilKopiForNyPerson(it) }) + it.sivilstander.addAll(sivilstander.map { grSivilstand -> grSivilstand.tilKopiForNyPerson(it) }) + it.dødsfall = dødsfall?.tilKopiForNyPerson(it) + } + + override fun toString(): String { + return """Person(aktørId=$aktør, + |type=$type + |fødselsdato=$fødselsdato) + """.trimMargin() + } + + override fun equals(other: Any?): Boolean { + if (other == null || javaClass != other.javaClass) { + return false + } + val entitet: Person = other as Person + return Objects.equals(hashCode(), entitet.hashCode()) + } + + override fun hashCode(): Int { + return Objects.hash(aktør, fødselsdato) + } + + fun hentAlder(): Int = Period.between(fødselsdato, now()).years + + fun hentSeksårsdag(): LocalDate = fødselsdato.plusYears(6) + + fun fyllerAntallÅrInneværendeMåned(år: Int): Boolean = + this.fødselsdato.toYearMonth() == now().minusYears(år.toLong()).toYearMonth() + + fun erYngreEnnInneværendeMåned(år: Int): Boolean = + this.fødselsdato.isAfter(now().minusYears(år.toLong()).sisteDagIMåned()) + + fun erDød(): Boolean = dødsfall != null + + fun hentSterkesteMedlemskap(): Medlemskap? { + val nåværendeMedlemskap = finnNåværendeMedlemskap(statsborgerskap) + return finnSterkesteMedlemskap(nåværendeMedlemskap) + } +} + +enum class Kjønn { + MANN, + KVINNE, + UKJENT, +} + +enum class Medlemskap { + NORDEN, + EØS, + TREDJELANDSBORGER, + STATSLØS, + UKJENT, +} + +enum class Målform { + NB, + NN, + ; + + fun tilSanityFormat() = when (this) { + NB -> "bokmaal" + NN -> "nynorsk" + } + + fun tilSpråkkode() = when (this) { + NB -> Språkkode.NB + NN -> Språkkode.NN + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonController.kt new file mode 100644 index 000000000..16d1e5f89 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonController.kt @@ -0,0 +1,110 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestManuellDødsfall +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonInfo +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPersonInfo +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPersonInfoMedNavnOgAdresse +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollService +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.PersonIdent +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/person") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class PersonController( + private val personopplysningerService: PersonopplysningerService, + private val persongrunnlagService: PersongrunnlagService, + private val personidentService: PersonidentService, + private val familieIntegrasjonerTilgangskontrollService: FamilieIntegrasjonerTilgangskontrollService, + private val utvidetBehandlingService: UtvidetBehandlingService, + private val tilgangService: TilgangService, +) { + + @GetMapping + fun hentPerson( + @RequestHeader personIdent: String, + @RequestBody personIdentBody: PersonIdent?, + ): ResponseEntity> { + val aktør = personidentService.hentAktør(personIdent) + val personinfo = familieIntegrasjonerTilgangskontrollService.hentMaskertPersonInfoVedManglendeTilgang(aktør) + ?: personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(aktør) + .tilRestPersonInfo(personIdent) + return ResponseEntity.ok(Ressurs.success(personinfo)) + } + + @GetMapping(path = ["/enkel"]) + fun hentPersonEnkel( + @RequestHeader personIdent: String, + @RequestBody personIdentBody: PersonIdent?, + ): ResponseEntity> { + val aktør = personidentService.hentAktør(personIdent) + val personinfo = familieIntegrasjonerTilgangskontrollService.hentMaskertPersonInfoVedManglendeTilgang(aktør) + ?: personopplysningerService.hentPersoninfoEnkel(aktør) + .tilRestPersonInfo(personIdent) + return ResponseEntity.ok(Ressurs.success(personinfo)) + } + + @GetMapping(path = ["/adresse"]) + fun hentPersonAdresse( + @RequestHeader personIdent: String, + ): ResponseEntity> { + val aktør = personidentService.hentAktør(personIdent) + val personinfo = familieIntegrasjonerTilgangskontrollService.hentMaskertPersonInfoVedManglendeTilgang(aktør) + ?: personopplysningerService.hentPersoninfoNavnOgAdresse(aktør) + .tilRestPersonInfoMedNavnOgAdresse(personIdent) + return ResponseEntity.ok(Ressurs.success(personinfo)) + } + + @GetMapping(path = ["/oppdater-registeropplysninger/{behandlingId}"]) + fun hentOgOppdaterRegisteropplysninger(@PathVariable behandlingId: Long): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + + val personopplysningGrunnlag = persongrunnlagService.oppdaterRegisteropplysninger(behandlingId) + return ResponseEntity.ok( + Ressurs.success( + utvidetBehandlingService + .lagRestUtvidetBehandling(behandlingId = personopplysningGrunnlag.behandlingId), + ), + ) + } + + @PostMapping(path = ["/registrer-manuell-dodsfall/{behandlingId}"]) + fun registrerManuellDødsfallPåPerson( + @PathVariable behandlingId: Long, + @RequestBody restManuellDødsfall: RestManuellDødsfall, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + + persongrunnlagService.registrerManuellDødsfallPåPerson( + behandlingId = BehandlingId(behandlingId), + personIdent = PersonIdent(restManuellDødsfall.personIdent), + dødsfallDato = restManuellDødsfall.dødsfallDato, + begrunnelse = restManuellDødsfall.begrunnelse, + ) + + return ResponseEntity.ok( + Ressurs.success( + utvidetBehandlingService + .lagRestUtvidetBehandling(behandlingId = behandlingId), + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonEnkel.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonEnkel.kt new file mode 100644 index 000000000..d898b7927 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonEnkel.kt @@ -0,0 +1,22 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import java.time.LocalDate + +/** + * Støtteobjekt for å ikke hente inn hele [Person] som henter mye annet som mange ganger er unødvendig + */ +data class PersonEnkel( + val type: PersonType, + val aktør: Aktør, + val fødselsdato: LocalDate, + val dødsfallDato: LocalDate?, + val målform: Målform, +) + +// Vil returnere barnet på EM-saker, som da i prinsippet også er søkeren. Vil også returnere barnet på inst. saker +fun Collection.søker() = this.singleOrNull { it.type == PersonType.SØKER } + ?: this.singleOrNull()?.takeIf { it.type == PersonType.BARN } + ?: error("Persongrunnlag mangler søker eller det finnes flere personer i grunnlaget med type=SØKER") + +fun Collection.barn(): List = this.filter { it.type == PersonType.BARN } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonRepository.kt new file mode 100644 index 000000000..c48750773 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonRepository.kt @@ -0,0 +1,24 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface PersonRepository : JpaRepository { + + @Query( + "SELECT p FROM Person p" + + " WHERE p.aktør = :aktør", + ) + fun findByAktør(aktør: Aktør): List + + @Query( + "SELECT distinct b.fagsak FROM Person p" + + " JOIN p.personopplysningGrunnlag pg" + + " JOIN Behandling b ON b.id = pg.behandlingId" + + " WHERE p.aktør = :aktør" + + " AND pg.aktiv = true", + ) + fun findFagsakerByAktør(aktør: Aktør): List +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonType.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonType.kt new file mode 100644 index 000000000..e3bbf4e88 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonType.kt @@ -0,0 +1,17 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType + +enum class PersonType { + SØKER, + ANNENPART, + BARN, + ; + + fun ytelseType() = when (this) { + SØKER -> YtelseType.UTVIDET_BARNETRYGD + BARN -> YtelseType.ORDINÆR_BARNETRYGD + ANNENPART -> throw Feil("Finner ikke ytelsetype for annen part") + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersongrunnlagService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersongrunnlagService.kt new file mode 100644 index 000000000..60d708c3b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersongrunnlagService.kt @@ -0,0 +1,369 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import no.nav.familie.ba.sak.common.BehandlingValidering.validerBehandlingKanRedigeres +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.Utils.storForbokstav +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.ekstern.restDomene.RestPerson +import no.nav.familie.ba.sak.ekstern.restDomene.SøknadDTO +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPerson +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.filtrerUtKunNorskeBostedsadresser +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType.BARN_ENSLIG_MINDREÅRIG +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType.INSTITUSJON +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType.NORMAL +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.arbeidsforhold.ArbeidsforholdService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrBostedsadresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.opphold.GrOpphold +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.sivilstand.GrSivilstand +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.StatsborgerskapService +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.statistikk.saksstatistikk.SaksstatistikkEventPublisher +import no.nav.familie.kontrakter.felles.PersonIdent +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate + +@Service +class PersongrunnlagService( + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + private val statsborgerskapService: StatsborgerskapService, + private val arbeidsfordelingService: ArbeidsfordelingService, + private val personopplysningerService: PersonopplysningerService, + private val personidentService: PersonidentService, + private val saksstatistikkEventPublisher: SaksstatistikkEventPublisher, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val loggService: LoggService, + private val arbeidsforholdService: ArbeidsforholdService, + private val vilkårsvurderingService: VilkårsvurderingService, +) { + + fun mapTilRestPersonMedStatsborgerskapLand(person: Person): RestPerson { + val restPerson = person.tilRestPerson() + restPerson.registerhistorikk?.statsborgerskap + ?.forEach { lagret -> + val landkode = lagret.verdi + val land = statsborgerskapService.hentLand(landkode) + lagret.verdi = if (land.lowercase().contains("uoppgitt")) "$land ($landkode)" else land.storForbokstav() + } + return restPerson + } + + fun hentSøker(behandlingId: Long): Person { + return personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId)!!.søker + } + + fun hentBarna(behandling: Behandling): List { + return hentBarna(behandling.id) + } + + fun hentSøkerOgBarnPåBehandlingThrows(behandlingId: Long): List = + hentSøkerOgBarnPåBehandling(behandlingId) + ?: error("Finner ikke søker/barn på behandling=$behandlingId") + + fun hentSøkerOgBarnPåBehandling(behandlingId: Long): List? = + personopplysningGrunnlagRepository.finnSøkerOgBarnAktørerTilAktiv(behandlingId) + .takeIf { it.isNotEmpty() } + + fun hentSøkerOgBarnPåFagsak(fagsakId: Long): Set? = + personopplysningGrunnlagRepository.finnSøkerOgBarnAktørerTilFagsak(fagsakId) + .takeIf { it.isNotEmpty() } + + fun hentBarna(behandlingId: Long): List = personopplysningGrunnlagRepository + .findByBehandlingAndAktiv(behandlingId)!!.barna + + fun hentPersonerPåBehandling(identer: List, behandling: Behandling): List { + val aktørIder = personidentService.hentAktørIder(identer) + + val grunnlag = personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandling.id) + ?: throw Feil("Finner ikke personopplysningsgrunnlag på behandling ${behandling.id}") + return grunnlag.søkerOgBarn.filter { person -> aktørIder.contains(person.aktør) } + } + + fun hentAktiv(behandlingId: Long): PersonopplysningGrunnlag? { + return personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = behandlingId) + } + + fun hentAktivThrows(behandlingId: Long): PersonopplysningGrunnlag { + return hentAktiv(behandlingId = behandlingId) + ?: throw Feil("Finner ikke personopplysningsgrunnlag på behandling $behandlingId") + } + + @Transactional + fun oppdaterRegisteropplysninger(behandlingId: Long): PersonopplysningGrunnlag { + val nåværendeGrunnlag = hentAktivThrows(behandlingId = behandlingId) + val behandling = behandlingHentOgPersisterService.hent(behandlingId = behandlingId) + + validerBehandlingKanRedigeres(behandling) + + return hentOgLagreSøkerOgBarnINyttGrunnlag( + aktør = nåværendeGrunnlag.søker.aktør, + barnFraInneværendeBehandling = nåværendeGrunnlag.barna.map { it.aktør }, + behandling = behandling, + målform = nåværendeGrunnlag.søker.målform, + ) + } + + /** + * Legger til barn i nytt personopplysningsgrunnlag + */ + @Transactional + fun leggTilBarnIPersonopplysningsgrunnlag( + nyttBarnIdent: String, + behandling: Behandling, + ) { + val nyttbarnAktør = personidentService.hentOgLagreAktør(nyttBarnIdent, true) + + val personopplysningGrunnlag = hentAktivThrows(behandlingId = behandling.id) + + val barnIGrunnlag = personopplysningGrunnlag.barna.map { it.aktør } + + if (barnIGrunnlag.contains(nyttbarnAktør)) { + throw FunksjonellFeil( + melding = "Forsøker å legge til barn som allerede finnes i personopplysningsgrunnlag ${personopplysningGrunnlag.id}", + frontendFeilmelding = "Barn finnes allerede på behandling og er derfor ikke lagt til.", + ) + } + + val oppdatertGrunnlag = hentOgLagreSøkerOgBarnINyttGrunnlag( + aktør = personopplysningGrunnlag.søker.aktør, + barnFraInneværendeBehandling = barnIGrunnlag.plus(nyttbarnAktør).toList(), + behandling = behandling, + målform = personopplysningGrunnlag.søker.målform, + ) + + oppdatertGrunnlag.barna.singleOrNull { nyttbarnAktør == it.aktør } + ?.also { loggService.opprettBarnLagtTilLogg(behandling, it) } ?: run { + secureLogger.info("Klarte ikke legge til barn med aktør $nyttbarnAktør på personopplysningsgrunnlag ${personopplysningGrunnlag.id}") + throw Feil("Nytt barn ikke lagt til i personopplysningsgrunnlag ${personopplysningGrunnlag.id}. Se securelog for mer informasjon.") + } + } + + fun finnNyeBarn(behandling: Behandling, forrigeBehandling: Behandling?): List { + val barnIForrigeGrunnlag = forrigeBehandling?.let { hentAktiv(behandlingId = it.id)?.barna } ?: emptySet() + val barnINyttGrunnlag = behandling.let { hentAktivThrows(behandlingId = it.id).barna } + + return barnINyttGrunnlag.filter { barn -> barnIForrigeGrunnlag.none { barn.aktør == it.aktør } } + } + + /** + * Registrerer barn valgt i søknad og barn fra forrige behandling + */ + @Transactional + fun registrerBarnFraSøknad( + søknadDTO: SøknadDTO, + behandling: Behandling, + forrigeBehandlingSomErVedtatt: Behandling? = null, + ) { + val søkerAktør = personidentService.hentOgLagreAktør(søknadDTO.søkerMedOpplysninger.ident, true) + val valgteBarnsAktør = + søknadDTO.barnaMedOpplysninger.filter { it.inkludertISøknaden && it.erFolkeregistrert } + .map { barn -> personidentService.hentOgLagreAktør(barn.ident, true) } + + val barnMedTilkjentYtelseIForrigeBehandling = + if (skalTaMedBarnFraForrigeBehandling(behandling) && forrigeBehandlingSomErVedtatt != null) { + finnBarnMedTilkjentYtelseIBehandling(forrigeBehandlingSomErVedtatt) + } else { + emptyList() + } + + hentOgLagreSøkerOgBarnINyttGrunnlag( + aktør = søkerAktør, + barnFraInneværendeBehandling = valgteBarnsAktør, + barnFraForrigeBehandling = barnMedTilkjentYtelseIForrigeBehandling, + behandling = behandling, + målform = søknadDTO.søkerMedOpplysninger.målform, + ) + } + + private fun finnBarnMedTilkjentYtelseIBehandling(behandling: Behandling): List = + hentAktiv(behandlingId = behandling.id)?.barna?.map { it.aktør }?.filter { + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandlingOgBarn(behandling.id, it).isNotEmpty() + } ?: emptyList() + + /** + * Henter oppdatert registerdata og lagrer i nytt aktivt personopplysningsgrunnlag + */ + @Transactional + fun hentOgLagreSøkerOgBarnINyttGrunnlag( + aktør: Aktør, + barnFraInneværendeBehandling: List, + behandling: Behandling, + målform: Målform, + barnFraForrigeBehandling: List = emptyList(), + ): PersonopplysningGrunnlag { + val personopplysningGrunnlag = lagreOgDeaktiverGammel(PersonopplysningGrunnlag(behandlingId = behandling.id)) + + val enkelPersonInfo = behandling.erMigrering() || behandling.erSatsendring() + val søker = hentPerson( + aktør = aktør, + personopplysningGrunnlag = personopplysningGrunnlag, + målform = målform, + personType = when (behandling.fagsak.type) { + NORMAL -> PersonType.SØKER + BARN_ENSLIG_MINDREÅRIG, INSTITUSJON -> PersonType.BARN + }, + enkelPersonInfo = enkelPersonInfo, + hentArbeidsforhold = behandling.skalBehandlesAutomatisk, + ) + personopplysningGrunnlag.personer.add(søker) + + barnFraInneværendeBehandling.union(barnFraForrigeBehandling).forEach { barnsAktør -> + personopplysningGrunnlag.personer.add( + hentPerson( + aktør = barnsAktør, + personopplysningGrunnlag = personopplysningGrunnlag, + målform = målform, + personType = PersonType.BARN, + enkelPersonInfo = enkelPersonInfo, + ), + ) + } + + if (søker.hentSterkesteMedlemskap() == Medlemskap.EØS && behandling.skalBehandlesAutomatisk) { + hentFarEllerMedmorAktør(barnFraInneværendeBehandling)?.also { farEllerMedmor -> + personopplysningGrunnlag.personer.add( + hentPerson( + aktør = farEllerMedmor, + personopplysningGrunnlag = personopplysningGrunnlag, + målform = målform, + personType = PersonType.ANNENPART, + enkelPersonInfo = enkelPersonInfo, + hentArbeidsforhold = true, + ), + ) + } + } + + return personopplysningGrunnlagRepository.save(personopplysningGrunnlag).also { + /** + * For sikkerhetsskyld fastsetter vi alltid behandlende enhet når nytt personopplysningsgrunnlag opprettes. + * Dette gjør vi fordi det kan ha blitt introdusert personer med fortrolig adresse. + */ + arbeidsfordelingService.fastsettBehandlendeEnhet(behandling) + saksstatistikkEventPublisher.publiserSaksstatistikk(behandling.fagsak.id) + } + } + + private fun hentPerson( + aktør: Aktør, + personopplysningGrunnlag: PersonopplysningGrunnlag, + målform: Målform, + personType: PersonType, + enkelPersonInfo: Boolean = false, + hentArbeidsforhold: Boolean = false, + ): Person { + val personinfo = + if (enkelPersonInfo) { + personopplysningerService.hentPersoninfoEnkel(aktør) + } else { + personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(aktør) + } + + return Person( + type = personType, + personopplysningGrunnlag = personopplysningGrunnlag, + fødselsdato = personinfo.fødselsdato, + aktør = aktør, + navn = personinfo.navn ?: "", + kjønn = personinfo.kjønn ?: Kjønn.UKJENT, + målform = målform, + ).also { person -> + person.opphold = + personinfo.opphold?.map { GrOpphold.fraOpphold(it, person) }?.toMutableList() ?: mutableListOf() + person.bostedsadresser = + personinfo.bostedsadresser.filtrerUtKunNorskeBostedsadresser() + .map { GrBostedsadresse.fraBostedsadresse(it, person) } + .toMutableList() + person.sivilstander = personinfo.sivilstander.map { GrSivilstand.fraSivilstand(it, person) }.toMutableList() + person.statsborgerskap = + personinfo.statsborgerskap?.flatMap { + statsborgerskapService.hentStatsborgerskapMedMedlemskap( + statsborgerskap = it, + person = person, + ) + }?.sortedBy { it.gyldigPeriode?.fom }?.toMutableList() ?: mutableListOf() + person.dødsfall = lagDødsfallFraPdl( + person = person, + dødsfallDatoFraPdl = personinfo.dødsfall?.dødsdato, + dødsfallAdresseFraPdl = personinfo.kontaktinformasjonForDoedsbo?.adresse, + ) + if (person.hentSterkesteMedlemskap() == Medlemskap.EØS && hentArbeidsforhold) { + person.arbeidsforhold = arbeidsforholdService.hentArbeidsforhold( + person = person, + ).toMutableList() + } + } + } + + private fun hentFarEllerMedmorAktør(barna: List): Aktør? { + val barnasFarEllerMedmorAktører = + barna.map { personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(aktør = it) } + .flatMap { barn -> + barn.forelderBarnRelasjon.filter { it.relasjonsrolle == FORELDERBARNRELASJONROLLE.FAR || it.relasjonsrolle == FORELDERBARNRELASJONROLLE.MEDMOR } + }.map { it.aktør }.toSet() + + return barnasFarEllerMedmorAktører.singleOrNull()?.also { + personidentService.hentOgLagreAktør(ident = it.aktørId, lagre = true) + } + } + + fun lagreOgDeaktiverGammel(personopplysningGrunnlag: PersonopplysningGrunnlag): PersonopplysningGrunnlag { + val aktivPersongrunnlag = hentAktiv(personopplysningGrunnlag.behandlingId) + + if (aktivPersongrunnlag != null) { + personopplysningGrunnlagRepository.saveAndFlush(aktivPersongrunnlag.also { it.aktiv = false }) + } + + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} oppretter persongrunnlag $personopplysningGrunnlag") + return personopplysningGrunnlagRepository.save(personopplysningGrunnlag) + } + + fun hentSøkersMålform(behandlingId: Long) = + hentSøkerOgBarnPåBehandlingThrows(behandlingId).søker().målform + + @Transactional + fun registrerManuellDødsfallPåPerson( + behandlingId: BehandlingId, + personIdent: PersonIdent, + dødsfallDato: LocalDate, + begrunnelse: String, + ) { + val personopplysningGrunnlag = hentAktivThrows(behandlingId.id) + val aktør = personidentService.hentAktør(personIdent.ident) + + val person = personopplysningGrunnlag.personer.singleOrNull { it.aktør == aktør } ?: run { + secureLogger.info("Klarte ikke registrere manuell dødsfall dato siden $aktør ikke finnes i personopplysningsgrunnlaget til behandlingen") + throw Feil("Manuell registrering av dødsfall dato feilet i behandling ${behandlingId.id}. Se securelog for mer informasjon.") + } + + validerAtDødsfallKanManueltRegistreresPåPerson(person, dødsfallDato) + + person.dødsfall = Dødsfall(person = person, dødsfallDato = dødsfallDato, manuellRegistrert = true) + vilkårsvurderingService.oppdaterVilkårVedDødsfall(behandlingId, dødsfallDato, aktør) + loggService.loggManueltRegistrertDødsfallDato(behandlingId, person, begrunnelse) + } + + private fun validerAtDødsfallKanManueltRegistreresPåPerson(person: Person, dødsfallDato: LocalDate) { + when { + person.erDød() -> throw FunksjonellFeil("Dødsfall dato er allerede registrert på person med navn ${person.navn}") + person.fødselsdato > dødsfallDato -> throw FunksjonellFeil("Du kan ikke sette dødsfall dato til en dato som er før ${person.navn} sin fødselsdato") + } + } + companion object { + private val logger = LoggerFactory.getLogger(PersongrunnlagService::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonopplysningGrunnlag.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonopplysningGrunnlag.kt new file mode 100644 index 000000000..ed7cc3bf7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonopplysningGrunnlag.kt @@ -0,0 +1,97 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.OneToMany +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity +@Table(name = "GR_PERSONOPPLYSNINGER") +data class PersonopplysningGrunnlag( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "GR_PERSONOPPLYSNINGER_SEQ_GENERATOR") + @SequenceGenerator( + name = "GR_PERSONOPPLYSNINGER_SEQ_GENERATOR", + sequenceName = "GR_PERSONOPPLYSNINGER_SEQ", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "fk_behandling_id", updatable = false, nullable = false) + val behandlingId: Long, + + @OneToMany( + fetch = FetchType.EAGER, + mappedBy = "personopplysningGrunnlag", + cascade = [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH], + ) + val personer: MutableSet = mutableSetOf(), + + @Column(name = "aktiv", nullable = false) + var aktiv: Boolean = true, + +) : BaseEntitet() { + + val barna: List + get() = personer.filter { it.type == PersonType.BARN } + + val yngsteBarnSinFødselsdato: LocalDate + get() = barna.maxOf { it.fødselsdato } + + val søker: Person + get() = personer.singleOrNull { it.type == PersonType.SØKER } + // Vil returnere barnet på EM-saker, som da i prinsippet også er søkeren. Vil også returnere barnet på inst. saker + ?: personer.singleOrNull()?.takeIf { it.type == PersonType.BARN } + ?: error("Persongrunnlag mangler søker eller det finnes flere personer i grunnlaget med type=SØKER") + + val annenForelder: Person? + get() = personer.singleOrNull { it.type == PersonType.ANNENPART } + + val søkerOgBarn: List + get() = personer.filter { it.type == PersonType.SØKER || it.type == PersonType.BARN } + + fun harBarnMedSeksårsdagPåFom(fom: LocalDate?) = personer.any { person -> + person + .hentSeksårsdag() + .toYearMonth() == (fom?.toYearMonth() ?: TIDENES_ENDE.toYearMonth()) + } + + fun tilKopiForNyBehandling( + behandling: Behandling, + søkerOgBarnMedTilkjentYtelseFraForrigeBehandling: List, + ): PersonopplysningGrunnlag = + copy(id = 0, behandlingId = behandling.id, personer = mutableSetOf()).also { it -> + it.personer + .addAll( + personer.filter { person -> søkerOgBarnMedTilkjentYtelseFraForrigeBehandling.any { søkerEllerBarn -> søkerEllerBarn.aktørId == person.aktør.aktørId } } + .map { person -> person.tilKopiForNyttPersonopplysningGrunnlag(it) }, + ) + } + + override fun toString(): String { + val sb = StringBuilder("PersonopplysningGrunnlagEntitet{") + sb.append("id=").append(id) + sb.append(", personer=").append(personer.toString()) + sb.append(", aktiv=").append(aktiv) + sb.append('}') + return sb.toString() + } +} + +fun Aktør.tilPerson(personopplysningGrunnlag: PersonopplysningGrunnlag): Person? = + personopplysningGrunnlag.personer.find { it.aktør == this } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonopplysningGrunnlagRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonopplysningGrunnlagRepository.kt new file mode 100644 index 000000000..9cf8e4de6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonopplysningGrunnlagRepository.kt @@ -0,0 +1,39 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface PersonopplysningGrunnlagRepository : JpaRepository { + + @Query("SELECT gr FROM PersonopplysningGrunnlag gr WHERE gr.behandlingId = :behandlingId AND gr.aktiv = true") + fun findByBehandlingAndAktiv(behandlingId: Long): PersonopplysningGrunnlag? + + @Query( + """ + SELECT new no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonEnkel(p.type, a, p.fødselsdato, d.dødsfallDato, p.målform) + FROM Person p + JOIN p.personopplysningGrunnlag gr + JOIN p.aktør a + LEFT JOIN p.dødsfall d + WHERE gr.behandlingId = :behandlingId + AND gr.aktiv = true + AND p.type IN ('SØKER', 'BARN') + """, + ) + fun finnSøkerOgBarnAktørerTilAktiv(behandlingId: Long): List + + @Query( + """ + SELECT new no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonEnkel(p.type, a, p.fødselsdato, d.dødsfallDato, p.målform) + FROM Person p + JOIN p.personopplysningGrunnlag gr + JOIN p.aktør a + JOIN Behandling b ON b.id = gr.behandlingId + LEFT JOIN p.dødsfall d + WHERE b.fagsak.id = :fagsakId + AND gr.aktiv = true + AND p.type IN ('SØKER', 'BARN') + """, + ) + fun finnSøkerOgBarnAktørerTilFagsak(fagsakId: Long): Set +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonopplysningGrunnlagUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonopplysningGrunnlagUtil.kt new file mode 100644 index 000000000..d2529e561 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonopplysningGrunnlagUtil.kt @@ -0,0 +1,6 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling + +fun skalTaMedBarnFraForrigeBehandling(behandling: Behandling) = + !behandling.erMigrering() && !behandling.erTekniskBehandling() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/arbeidsforhold/ArbeidsforholdService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/arbeidsforhold/ArbeidsforholdService.kt new file mode 100644 index 000000000..0ca0dae3b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/arbeidsforhold/ArbeidsforholdService.kt @@ -0,0 +1,32 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.arbeidsforhold + +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.ArbeidsgiverType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class ArbeidsforholdService(private val integrasjonClient: IntegrasjonClient) { + fun hentArbeidsforhold(person: Person): List { + val arbeidsforholdForSisteFemÅr = + integrasjonClient.hentArbeidsforhold(person.aktør.aktivFødselsnummer(), LocalDate.now().minusYears(5)) + + return arbeidsforholdForSisteFemÅr.map { + val periode = DatoIntervallEntitet(it.ansettelsesperiode?.periode?.fom, it.ansettelsesperiode?.periode?.tom) + val arbeidsgiverId = when (it.arbeidsgiver?.type) { + ArbeidsgiverType.Organisasjon -> it.arbeidsgiver.organisasjonsnummer + ArbeidsgiverType.Person -> it.arbeidsgiver.offentligIdent + else -> null + } + + GrArbeidsforhold( + periode = periode, + arbeidsgiverType = it.arbeidsgiver?.type?.name, + arbeidsgiverId = arbeidsgiverId, + person = person, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/arbeidsforhold/GrArbeidsforhold.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/arbeidsforhold/GrArbeidsforhold.kt new file mode 100644 index 000000000..06f4ad4e2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/arbeidsforhold/GrArbeidsforhold.kt @@ -0,0 +1,54 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.arbeidsforhold + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Embedded +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "GrArbeidsforhold") +@Table(name = "PO_ARBEIDSFORHOLD") +data class GrArbeidsforhold( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "po_arbeidsforhold_seq_generator") + @SequenceGenerator( + name = "po_arbeidsforhold_seq_generator", + sequenceName = "po_arbeidsforhold_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Embedded + val periode: DatoIntervallEntitet? = null, + + @Column(name = "arbeidsgiver_id") + val arbeidsgiverId: String?, + + @Column(name = "arbeidsgiver_type") + val arbeidsgiverType: String?, + + @JsonIgnore + @ManyToOne(optional = false) + @JoinColumn(name = "fk_po_person_id", nullable = false, updatable = false) + val person: Person, +) : BaseEntitet() { + fun tilKopiForNyPerson(nyPerson: Person) = + copy(id = 0, person = nyPerson) +} + +fun List.harLøpendeArbeidsforhold(): Boolean = this.any { + it.periode?.tom == null || it.periode.tom >= LocalDate.now() +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrBostedsadresse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrBostedsadresse.kt new file mode 100644 index 000000000..cd2a9b8bb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrBostedsadresse.kt @@ -0,0 +1,144 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.DiscriminatorColumn +import jakarta.persistence.Embedded +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Inheritance +import jakarta.persistence.InheritanceType +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.erInnenfor +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.common.isSameOrBefore +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegisteropplysning +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "GrBostedsadresse") +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "type") +@Table(name = "PO_BOSTEDSADRESSE") +abstract class GrBostedsadresse( + // Alle attributter må være open ellers kastes feil ved oppsrart. + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "po_bostedsadresse_seq_generator") + @SequenceGenerator( + name = "po_bostedsadresse_seq_generator", + sequenceName = "po_bostedsadresse_seq", + allocationSize = 50, + ) + open val id: Long = 0, + + @Embedded + open var periode: DatoIntervallEntitet? = null, + + @JsonIgnore + @ManyToOne + @JoinColumn(name = "fk_po_person_id") + open var person: Person? = null, +) : BaseEntitet() { + + abstract fun toSecureString(): String + + abstract fun tilFrontendString(): String + + protected abstract fun tilKopiForNyPerson(): GrBostedsadresse + + fun tilKopiForNyPerson(nyPerson: Person): GrBostedsadresse = + tilKopiForNyPerson().also { + it.periode = periode + it.person = nyPerson + } + + fun gjeldendeNå(): Boolean { + if (periode == null) return true + return periode!!.erInnenfor(LocalDate.now()) + } + + fun tilRestRegisteropplysning() = RestRegisteropplysning( + fom = this.periode?.fom.takeIf { it != fregManglendeFlytteDato }, + tom = this.periode?.tom, + verdi = this.tilFrontendString(), + ) + + fun harGyldigFom() = this.periode?.fom != null && this.periode?.fom != fregManglendeFlytteDato + + companion object { + + // Når flyttedato er satt til 0001-01-01, så mangler den egentlig. + // Det er en feil i Freg, som har arvet mangelfulle data fra DSF. + val fregManglendeFlytteDato = LocalDate.of(1, 1, 1) + + fun MutableList.sisteAdresse(): GrBostedsadresse? { + if (this.filter { it.periode?.fom == null || it.periode?.fom == fregManglendeFlytteDato }.size > 1) { + throw Feil( + "Finnes flere bostedsadresser uten fom-dato", + ) + } + return this.sortedBy { it.periode?.fom }.lastOrNull() + } + + fun fraBostedsadresse(bostedsadresse: Bostedsadresse, person: Person): GrBostedsadresse { + val mappetAdresse = when { + bostedsadresse.vegadresse != null -> { + GrVegadresse.fraVegadresse(bostedsadresse.vegadresse!!) + } + + bostedsadresse.matrikkeladresse != null -> { + GrMatrikkeladresse.fraMatrikkeladresse(bostedsadresse.matrikkeladresse!!) + } + + bostedsadresse.ukjentBosted != null -> { + GrUkjentBosted.fraUkjentBosted(bostedsadresse.ukjentBosted!!) + } + + else -> throw Feil("Vegadresse, matrikkeladresse og ukjent bosted har verdi null ved mapping fra bostedadresse") + } + return mappetAdresse.also { + it.person = person + it.periode = DatoIntervallEntitet(bostedsadresse.angittFlyttedato, bostedsadresse.gyldigTilOgMed) + } + } + + fun erSammeAdresse(adresse: GrBostedsadresse?, andreAdresse: GrBostedsadresse?): Boolean { + return adresse != null && + adresse !is GrUkjentBosted && + adresse == andreAdresse + } + } +} + +fun List.filtrerGjeldendeNå(): List { + return this.filter { it.gjeldendeNå() } +} + +fun vurderOmPersonerBorSammen(adresser: List, andreAdresser: List): Boolean { + return adresser.isNotEmpty() && adresser.any { + andreAdresser.any { søkerAdresse -> + val søkerAdresseFom = søkerAdresse.periode?.fom ?: TIDENES_MORGEN + val søkerAdresseTom = søkerAdresse.periode?.tom ?: TIDENES_ENDE + + val barnAdresseFom = it.periode?.fom ?: TIDENES_MORGEN + val barnAdresseTom = it.periode?.tom ?: TIDENES_ENDE + + søkerAdresseFom.isSameOrBefore(barnAdresseFom) && + søkerAdresseTom.isSameOrAfter(barnAdresseTom) && + GrBostedsadresse.erSammeAdresse(søkerAdresse, it) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrBostedsadresseperiode.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrBostedsadresseperiode.kt new file mode 100644 index 000000000..1fb96b683 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrBostedsadresseperiode.kt @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse + +import jakarta.persistence.Embedded +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase + +/** + * Ble brukt i tidlig fase av automatisk vurdering av fødselshendelser, men brukes ikke lenger. + * Tar vare på i tilfelle vi må hente opp dataene igjen. + */ +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "GrBostedsadresseperiode") +@Table(name = "PO_BOSTEDSADRESSEPERIODE") +data class GrBostedsadresseperiode( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "po_bostedsadresseperiode_seq_generator") + @SequenceGenerator( + name = "po_bostedsadresseperiode_seq_generator", + sequenceName = "po_bostedsadresseperiode_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Embedded + val periode: DatoIntervallEntitet? = null, +) : BaseEntitet() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrMatrikkeladresse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrMatrikkeladresse.kt new file mode 100644 index 000000000..27e95a881 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrMatrikkeladresse.kt @@ -0,0 +1,78 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse + +import jakarta.persistence.Column +import jakarta.persistence.DiscriminatorValue +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.personopplysning.Matrikkeladresse +import java.util.Objects + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "GrMatrikkeladresse") +@DiscriminatorValue("Matrikkeladresse") +data class GrMatrikkeladresse( + @Column(name = "matrikkel_id") + val matrikkelId: Long?, + + @Column(name = "bruksenhetsnummer") + val bruksenhetsnummer: String?, + + @Column(name = "tilleggsnavn") + val tilleggsnavn: String?, + + @Column(name = "postnummer") + val postnummer: String?, + + @Column(name = "kommunenummer") + val kommunenummer: String?, + +) : GrBostedsadresse() { + + override fun tilKopiForNyPerson(): GrBostedsadresse = + GrMatrikkeladresse( + matrikkelId, + bruksenhetsnummer, + tilleggsnavn, + postnummer, + kommunenummer, + ) + + override fun toSecureString(): String { + return """MatrikkeladresseDao(matrikkelId=$matrikkelId,bruksenhetsnummer=$bruksenhetsnummer,tilleggsnavn=$tilleggsnavn, +| postnummer=$postnummer,kommunenummer=$kommunenummer + """.trimMargin() + } + + override fun toString(): String { + return "Matrikkeladresse(detaljer skjult)" + } + + override fun tilFrontendString() = + """Matrikkel $matrikkelId, bruksenhet $bruksenhetsnummer, postnummer $postnummer""".trimMargin() + + override fun equals(other: Any?): Boolean { + if (other == null || javaClass != other.javaClass) { + return false + } + val otherMatrikkeladresse = other as GrMatrikkeladresse + return this === other || + matrikkelId != null && + matrikkelId == otherMatrikkeladresse.matrikkelId && + bruksenhetsnummer == otherMatrikkeladresse.bruksenhetsnummer + } + + override fun hashCode(): Int = Objects.hash(matrikkelId) + + companion object { + + fun fraMatrikkeladresse(matrikkeladresse: Matrikkeladresse): GrMatrikkeladresse = + GrMatrikkeladresse( + matrikkelId = matrikkeladresse.matrikkelId, + bruksenhetsnummer = matrikkeladresse.bruksenhetsnummer, + tilleggsnavn = matrikkeladresse.tilleggsnavn, + postnummer = matrikkeladresse.postnummer, + kommunenummer = matrikkeladresse.kommunenummer, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrUkjentBosted.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrUkjentBosted.kt new file mode 100644 index 000000000..8a5225172 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrUkjentBosted.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse + +import jakarta.persistence.Column +import jakarta.persistence.DiscriminatorValue +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.personopplysning.UkjentBosted + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "GrUkjentBosted") +@DiscriminatorValue("ukjentBosted") +data class GrUkjentBosted( + @Column(name = "bostedskommune") + val bostedskommune: String, + +) : GrBostedsadresse() { + + override fun tilKopiForNyPerson(): GrBostedsadresse = + GrUkjentBosted(bostedskommune) + + override fun toSecureString(): String { + return """UkjentadresseDao(bostedskommune=$bostedskommune""".trimMargin() + } + + override fun tilFrontendString() = """Ukjent adresse, kommune $bostedskommune""".trimMargin() + + override fun toString(): String { + return "UkjentBostedAdresse(detaljer skjult)" + } + + companion object { + + fun fraUkjentBosted(ukjentBosted: UkjentBosted): GrUkjentBosted = + GrUkjentBosted(bostedskommune = ukjentBosted.bostedskommune) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrVegadresse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrVegadresse.kt new file mode 100644 index 000000000..2a4e4ff75 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/bostedsadresse/GrVegadresse.kt @@ -0,0 +1,107 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse + +import jakarta.persistence.Column +import jakarta.persistence.DiscriminatorValue +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import no.nav.familie.ba.sak.common.Utils.nullableTilString +import no.nav.familie.ba.sak.common.Utils.storForbokstav +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.personopplysning.Vegadresse +import java.util.Objects + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "GrVegadresse") +@DiscriminatorValue("Vegadresse") +data class GrVegadresse( + @Column(name = "matrikkel_id") + val matrikkelId: Long?, + + @Column(name = "husnummer") + val husnummer: String?, + + @Column(name = "husbokstav") + val husbokstav: String?, + + @Column(name = "bruksenhetsnummer") + val bruksenhetsnummer: String?, + + @Column(name = "adressenavn") + val adressenavn: String?, + + @Column(name = "kommunenummer") + val kommunenummer: String?, + + @Column(name = "tilleggsnavn") + val tilleggsnavn: String?, + + @Column(name = "postnummer") + val postnummer: String?, + +) : GrBostedsadresse() { + + override fun tilKopiForNyPerson(): GrBostedsadresse = + GrVegadresse( + matrikkelId, + husnummer, + husbokstav, + bruksenhetsnummer, + adressenavn, + kommunenummer, + tilleggsnavn, + postnummer, + ) + + override fun toSecureString(): String { + return """VegadresseDao(husnummer=$husnummer,husbokstav=$husbokstav,matrikkelId=$matrikkelId,bruksenhetsnummer=$bruksenhetsnummer, +| adressenavn=$adressenavn,kommunenummer=$kommunenummer,tilleggsnavn=$tilleggsnavn,postnummer=$postnummer + """.trimMargin() + } + + override fun toString(): String { + return "Vegadresse(detaljer skjult)" + } + + override fun tilFrontendString() = """${ + adressenavn.nullableTilString() + .storForbokstav() + } ${husnummer.nullableTilString()}${husbokstav.nullableTilString()}${postnummer.let { ", $it" }}""".trimMargin() + + override fun equals(other: Any?): Boolean { + if (other == null || javaClass != other.javaClass) { + return false + } + val otherVegadresse = other as GrVegadresse + + return this === other || + ( + (matrikkelId != null && matrikkelId == otherVegadresse.matrikkelId) || + ( + (matrikkelId == null && otherVegadresse.matrikkelId == null) && + postnummer != null && + !(adressenavn == null && husnummer == null && husbokstav == null) && + (adressenavn == otherVegadresse.adressenavn) && + (husnummer == otherVegadresse.husnummer) && + (husbokstav == otherVegadresse.husbokstav) && + (postnummer == otherVegadresse.postnummer) + ) + ) + } + + override fun hashCode(): Int = Objects.hash(matrikkelId) + + companion object { + + fun fraVegadresse(vegadresse: Vegadresse): GrVegadresse = + GrVegadresse( + matrikkelId = vegadresse.matrikkelId, + husnummer = vegadresse.husnummer, + husbokstav = vegadresse.husbokstav, + bruksenhetsnummer = vegadresse.bruksenhetsnummer, + adressenavn = vegadresse.adressenavn, + kommunenummer = vegadresse.kommunenummer, + tilleggsnavn = vegadresse.tilleggsnavn, + postnummer = vegadresse.postnummer, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/domene/PersonIdent.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/domene/PersonIdent.kt new file mode 100644 index 000000000..8913c127e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/domene/PersonIdent.kt @@ -0,0 +1,43 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.domene + +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.persistence.Column +import jakarta.persistence.Embeddable +import java.util.Objects + +/** + * Denne mapper p.t Norsk person ident (fødselsnummer, inkl F-nr, D-nr eller FDAT) + * + * * F-nr: http://lovdata.no/forskrift/2007-11-09-1268/%C2%A72-2 (F-nr) + * + * * D-nr: http://lovdata.no/forskrift/2007-11-09-1268/%C2%A72-5 (D-nr), samt hvem som kan utstede + * (http://lovdata.no/forskrift/2007-11-09-1268/%C2%A72-6) + * + * * FDAT: Personer uten FNR. Disse har fødselsdato + 00000 (normalt) eller fødselsdato + 00001 (dødfødt). + * + */ +@Embeddable +class PersonIdent( + @JsonProperty("id") + @Column(name = "person_ident", updatable = false, length = 50) + val ident: String, +) : Comparable { + + override fun compareTo(other: PersonIdent): Int { + return ident.compareTo(other.ident) + } + + override fun equals(other: Any?): Boolean { + if (other === this) { + return true + } else if (other == null || this.javaClass != other.javaClass) { + return false + } + val otherObject = other as PersonIdent + return ident == otherObject.ident + } + + override fun hashCode(): Int { + return Objects.hash(ident) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/opphold/GrOpphold.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/opphold/GrOpphold.kt new file mode 100644 index 000000000..09a8b2c24 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/opphold/GrOpphold.kt @@ -0,0 +1,98 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.opphold + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Embedded +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.common.Utils.storForbokstav +import no.nav.familie.ba.sak.common.erInnenfor +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegisteropplysning +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.personopplysning.OPPHOLDSTILLATELSE +import no.nav.familie.kontrakter.felles.personopplysning.Opphold +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "GrOpphold") +@Table(name = "PO_OPPHOLD") +data class GrOpphold( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "po_opphold_seq_generator") + @SequenceGenerator( + name = "po_opphold_seq_generator", + sequenceName = "po_opphold_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Embedded + val gyldigPeriode: DatoIntervallEntitet? = null, + + @Column(name = "type", nullable = false) + val type: OPPHOLDSTILLATELSE, + + @JsonIgnore + @ManyToOne(optional = false) + @JoinColumn(name = "fk_po_person_id", nullable = false, updatable = false) + val person: Person, +) : BaseEntitet() { + + fun tilKopiForNyPerson(nyPerson: Person): GrOpphold = + copy(id = 0, person = nyPerson) + + fun gjeldendeNå(): Boolean { + if (gyldigPeriode == null) return true + return gyldigPeriode.erInnenfor(LocalDate.now()) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GrOpphold + + if (gyldigPeriode != other.gyldigPeriode) return false + if (type != other.type) return false + + return true + } + + override fun hashCode(): Int { + var result = gyldigPeriode.hashCode() + result = 31 * result + type.hashCode() + return result + } + + fun tilRestRegisteropplysning() = RestRegisteropplysning( + fom = this.gyldigPeriode?.fom, + tom = this.gyldigPeriode?.tom, + verdi = this.type.name.replace('_', ' ').storForbokstav(), + ) + + companion object { + + fun fraOpphold(opphold: Opphold, person: Person) = + GrOpphold( + gyldigPeriode = DatoIntervallEntitet( + fom = opphold.oppholdFra, + tom = opphold.oppholdTil, + ), + type = opphold.type, + person = person, + ) + } +} + +fun List.gyldigGjeldendeOppholdstillatelseFødselshendelse() = + this.any { it.gjeldendeNå() && it.type != OPPHOLDSTILLATELSE.OPPLYSNING_MANGLER } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/sivilstand/GrSivilstand.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/sivilstand/GrSivilstand.kt new file mode 100644 index 000000000..f14b553b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/sivilstand/GrSivilstand.kt @@ -0,0 +1,96 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.sivilstand + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.Utils.storForbokstav +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegisteropplysning +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import no.nav.familie.kontrakter.felles.personopplysning.Sivilstand +import java.time.LocalDate +import java.util.Objects + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "GrSivilstand") +@Table(name = "PO_SIVILSTAND") +data class GrSivilstand( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "po_sivilstand_seq_generator") + @SequenceGenerator( + name = "po_sivilstand_seq_generator", + sequenceName = "po_sivilstand_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "fom") + val fom: LocalDate? = null, + + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false) + val type: SIVILSTAND, + + @JsonIgnore + @ManyToOne(optional = false) + @JoinColumn(name = "fk_po_person_id", nullable = false, updatable = false) + val person: Person, +) : BaseEntitet() { + + fun tilKopiForNyPerson(nyPerson: Person) = + copy(id = 0, person = nyPerson) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GrSivilstand + + return !( + fom != other.fom || + type != other.type || + person != other.person + ) + } + + override fun hashCode() = Objects.hash(fom, type, person) + + fun tilRestRegisteropplysning() = RestRegisteropplysning( + fom = this.fom, + tom = null, + verdi = this.type.toString() + .replace("_", " ") + .storForbokstav(), + ) + + fun harGyldigFom() = this.fom != null + + companion object { + + fun List.sisteSivilstand(): GrSivilstand? { + if (this.size == 1) return this.single() + + val sivilstandMedFom = this.filter { it.harGyldigFom() } + return sivilstandMedFom.maxByOrNull { it.fom!! } + } + + fun fraSivilstand(sivilstand: Sivilstand, person: Person) = + GrSivilstand( + fom = sivilstand.gyldigFraOgMed, + type = sivilstand.type, + person = person, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/statsborgerskap/GrStatsborgerskap.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/statsborgerskap/GrStatsborgerskap.kt new file mode 100644 index 000000000..b4d903134 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/statsborgerskap/GrStatsborgerskap.kt @@ -0,0 +1,114 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Embedded +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.common.erInnenfor +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegisteropplysning +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "GrStatsborgerskap") +@Table(name = "PO_STATSBORGERSKAP") +data class GrStatsborgerskap( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "po_statsborgerskap_seq_generator") + @SequenceGenerator( + name = "po_statsborgerskap_seq_generator", + sequenceName = "po_statsborgerskap_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Embedded + val gyldigPeriode: DatoIntervallEntitet? = null, + + @Column(name = "landkode", nullable = false) + val landkode: String, + + @Enumerated(EnumType.STRING) + @Column(name = "medlemskap", nullable = false) + val medlemskap: Medlemskap = Medlemskap.UKJENT, + + @JsonIgnore + @ManyToOne(optional = false) + @JoinColumn(name = "fk_po_person_id", nullable = false, updatable = false) + val person: Person, +) : BaseEntitet() { + + fun tilKopiForNyPerson(nyPerson: Person): GrStatsborgerskap = + copy(id = 0, person = nyPerson) + + fun gjeldendeNå(): Boolean { + if (gyldigPeriode == null) return true + return gyldigPeriode.erInnenfor(LocalDate.now()) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GrStatsborgerskap + + if (gyldigPeriode != other.gyldigPeriode) return false + if (landkode != other.landkode) return false + + return true + } + + override fun hashCode(): Int { + var result = gyldigPeriode.hashCode() + result = 31 * result + landkode.hashCode() + return result + } + + fun tilRestRegisteropplysning() = RestRegisteropplysning( + fom = this.gyldigPeriode?.fom, + tom = this.gyldigPeriode?.tom, + verdi = this.landkode, + ) +} + +fun Statsborgerskap.fom() = this.gyldigFraOgMed ?: this.bekreftelsesdato + +fun List.filtrerGjeldendeNå(): List { + return this.filter { it.gjeldendeNå() } +} + +fun List.hentSterkesteMedlemskap(): Medlemskap? { + val nåværendeMedlemskap = finnNåværendeMedlemskap(this) + return finnSterkesteMedlemskap(nåværendeMedlemskap) +} + +fun finnNåværendeMedlemskap(statsborgerskap: List?): List = + statsborgerskap?.filtrerGjeldendeNå()?.map { it.medlemskap } ?: emptyList() + +fun finnSterkesteMedlemskap(medlemskap: List): Medlemskap? { + return with(medlemskap) { + when { + contains(Medlemskap.NORDEN) -> Medlemskap.NORDEN + contains(Medlemskap.EØS) -> Medlemskap.EØS + contains(Medlemskap.TREDJELANDSBORGER) -> Medlemskap.TREDJELANDSBORGER + contains(Medlemskap.STATSLØS) -> Medlemskap.STATSLØS + contains(Medlemskap.UKJENT) -> Medlemskap.UKJENT + else -> null + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/statsborgerskap/StatsborgerskapService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/statsborgerskap/StatsborgerskapService.kt new file mode 100644 index 000000000..4b7a882ad --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/statsborgerskap/StatsborgerskapService.kt @@ -0,0 +1,220 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap + +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.kontrakter.felles.kodeverk.BetydningDto +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class StatsborgerskapService( + private val integrasjonClient: IntegrasjonClient, +) { + + fun hentLand(landkode: String): String = integrasjonClient.hentLand(landkode) + + fun hentStatsborgerskapMedMedlemskap( + statsborgerskap: Statsborgerskap, + person: Person, + ): List { + if (statsborgerskap.iNordiskLand()) { + return listOf( + GrStatsborgerskap( + gyldigPeriode = DatoIntervallEntitet( + fom = statsborgerskap.hentFom(), + tom = statsborgerskap.gyldigTilOgMed, + ), + landkode = statsborgerskap.land, + medlemskap = Medlemskap.NORDEN, + person = person, + ), + ) + } + + val eøsMedlemskapsPerioderForValgtLand = + integrasjonClient.hentAlleEØSLand().betydninger[statsborgerskap.land] ?: emptyList() + + var datoFra = statsborgerskap.hentFom() + + return if (datoFra == null && statsborgerskap.gyldigTilOgMed == null) { + val idag = LocalDate.now() + listOf( + GrStatsborgerskap( + gyldigPeriode = DatoIntervallEntitet( + fom = idag, + tom = null, + ), + landkode = statsborgerskap.land, + medlemskap = finnMedlemskap( + statsborgerskap = statsborgerskap, + eøsMedlemskapsperioderForValgtLand = eøsMedlemskapsPerioderForValgtLand, + gyldigFraOgMed = idag, + ), + person = person, + ), + ) + } else { + hentMedlemskapsperioderUnderStatsborgerskapsperioden( + medlemskapsperioderForValgtLand = eøsMedlemskapsPerioderForValgtLand, + statsborgerFra = datoFra, + statsborgerTil = statsborgerskap.gyldigTilOgMed, + ).fold(emptyList()) { medlemskapsperioder, periode -> + val medlemskapsperiode = GrStatsborgerskap( + gyldigPeriode = periode, + landkode = statsborgerskap.land, + medlemskap = finnMedlemskap( + statsborgerskap = statsborgerskap, + eøsMedlemskapsperioderForValgtLand = eøsMedlemskapsPerioderForValgtLand, + gyldigFraOgMed = periode.fom, + ), + person = person, + ) + medlemskapsperioder + listOf(medlemskapsperiode) + } + } + } + + fun hentSterkesteMedlemskap(statsborgerskap: Statsborgerskap): Medlemskap? { + if (statsborgerskap.iNordiskLand()) { + return Medlemskap.NORDEN + } + + val eøsMedlemskapsPerioderForValgtLand = + integrasjonClient.hentAlleEØSLand().betydninger[statsborgerskap.land] ?: emptyList() + var datoFra = statsborgerskap.hentFom() + + return if (datoFra == null && statsborgerskap.gyldigTilOgMed == null) { + val idag = LocalDate.now() + finnMedlemskap( + statsborgerskap = statsborgerskap, + eøsMedlemskapsperioderForValgtLand = eøsMedlemskapsPerioderForValgtLand, + gyldigFraOgMed = idag, + ) + } else { + val alleMedlemskap = hentMedlemskapsperioderUnderStatsborgerskapsperioden( + eøsMedlemskapsPerioderForValgtLand, + datoFra, + statsborgerskap.gyldigTilOgMed, + ).fold(emptyList()) { acc, periode -> + acc + listOf( + finnMedlemskap( + statsborgerskap = statsborgerskap, + eøsMedlemskapsperioderForValgtLand = eøsMedlemskapsPerioderForValgtLand, + gyldigFraOgMed = periode.fom, + ), + ) + } + + finnSterkesteMedlemskap(alleMedlemskap.toList()) + } + } + + private fun hentMedlemskapsperioderUnderStatsborgerskapsperioden( + medlemskapsperioderForValgtLand: List, + statsborgerFra: LocalDate?, + statsborgerTil: LocalDate?, + ): List { + val datoerMedlemskapEndrerSeg = medlemskapsperioderForValgtLand + .flatMap { + listOf( + it.gyldigFra, + it.gyldigTil.plusDays(1), + ) + } + val endringsdatoerUnderStatsborgerskapsperioden = datoerMedlemskapEndrerSeg + .filter { datoForEndringIMedlemskap -> + erInnenforDatoerSomBetegnerUendelighetIKodeverk(datoForEndringIMedlemskap) + }.filter { datoForEndringIMedlemskap -> + erInnenforDatoerForStatsborgerskapet(datoForEndringIMedlemskap, statsborgerFra, statsborgerTil) + } + + val datoerMedlemskapEllerStatsborgerskapEndrerSeg = + listOf(statsborgerFra) + endringsdatoerUnderStatsborgerskapsperioden + listOf(statsborgerTil) + val naivePerioder = datoerMedlemskapEllerStatsborgerskapEndrerSeg.windowed(2, 1) + return hentDatointervallerMedSluttdatoFørNesteStarter(naivePerioder) + } + + private fun finnMedlemskap( + statsborgerskap: Statsborgerskap, + eøsMedlemskapsperioderForValgtLand: List, + gyldigFraOgMed: LocalDate?, + ): Medlemskap = + when { + statsborgerskap.iNordiskLand() -> Medlemskap.NORDEN + erEØSMedlemPåGittDato(eøsMedlemskapsperioderForValgtLand, gyldigFraOgMed) -> Medlemskap.EØS + statsborgerskap.iTredjeland() -> Medlemskap.TREDJELANDSBORGER + statsborgerskap.erStatsløs() -> Medlemskap.STATSLØS + else -> Medlemskap.UKJENT + } + + private fun erEØSMedlemPåGittDato( + eøsMedlemskapsperioderForValgtLand: List, + gjeldendeDato: LocalDate?, + ): Boolean = + eøsMedlemskapsperioderForValgtLand.any { + gjeldendeDato == null || ( + it.gyldigFra <= gjeldendeDato && + it.gyldigTil >= gjeldendeDato + ) + } + + private fun erInnenforDatoerSomBetegnerUendelighetIKodeverk(dato: LocalDate) = + dato.isAfter(TIDLIGSTE_DATO_I_KODEVERK) && dato.isBefore(SENESTE_DATO_I_KODEVERK) + + private fun erInnenforDatoerForStatsborgerskapet( + dato: LocalDate, + statsborgerFra: LocalDate?, + statsborgerTil: LocalDate?, + ) = + (statsborgerFra == null || dato.isAfter(statsborgerFra)) && + (statsborgerTil == null || dato.isBefore(statsborgerTil)) + + private fun hentDatointervallerMedSluttdatoFørNesteStarter(intervaller: List>): List { + return intervaller.mapIndexed { index, endringsdatoPar -> + val fra = endringsdatoPar[0] + val nesteEndringsdato = endringsdatoPar[1] + if (index != (intervaller.size - 1)) { + if (nesteEndringsdato == null) { + throw Feil("EØS-medlemskap skal ikke kunne ha null som fra/til-dato") + } + DatoIntervallEntitet(fra, nesteEndringsdato.minusDays(1)) + } else { + DatoIntervallEntitet(fra, nesteEndringsdato) + } + } + } + + companion object { + + const val LANDKODE_UKJENT = "XUK" + const val LANDKODE_STATSLØS = "XXX" + val TIDLIGSTE_DATO_I_KODEVERK: LocalDate = LocalDate.parse("1900-01-02") + val SENESTE_DATO_I_KODEVERK: LocalDate = LocalDate.parse("9990-01-01") + } +} + +fun Statsborgerskap.hentFom() = this.bekreftelsesdato ?: this.gyldigFraOgMed + +fun Statsborgerskap.iNordiskLand() = Norden.values().map { it.name }.contains(this.land) + +fun Statsborgerskap.iTredjeland() = this.land != StatsborgerskapService.LANDKODE_UKJENT + +fun Statsborgerskap.erStatsløs() = this.land == StatsborgerskapService.LANDKODE_STATSLØS + +/** + * Norge, Sverige, Finland, Danmark, Island, Grønland, Færøyene og Åland + */ +enum class Norden { + NOR, + SWE, + FIN, + DNK, + ISL, + FRO, + GRL, + ALA, +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/sm\303\245barnstillegg/PeriodeOvergangsst\303\270nadGrunnlag.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/sm\303\245barnstillegg/PeriodeOvergangsst\303\270nadGrunnlag.kt" new file mode 100644 index 000000000..9698abc95 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/sm\303\245barnstillegg/PeriodeOvergangsst\303\270nadGrunnlag.kt" @@ -0,0 +1,81 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.småbarnstillegg + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.OneToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.ef.Datakilde +import no.nav.familie.kontrakter.felles.ef.EksternPeriode +import java.time.LocalDate + +/** + * Periode vi har hentet fra ef-sak som representerer når en person + * har hatt full overgangsstønad. + */ +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "PeriodeOvergangsstønadGrunnlag") +@Table(name = "GR_PERIODE_OVERGANGSSTONAD") +data class PeriodeOvergangsstønadGrunnlag( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "gr_periode_overgangsstonad_seq_generator") + @SequenceGenerator( + name = "gr_periode_overgangsstonad_seq_generator", + sequenceName = "gr_periode_overgangsstonad_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "fk_behandling_id", nullable = false, updatable = false) + val behandlingId: Long, + + @OneToOne(optional = false) + @JoinColumn(name = "fk_aktoer_id", nullable = false, updatable = false) + val aktør: Aktør, + + @Column(name = "fom", nullable = false, columnDefinition = "DATE") + val fom: LocalDate, + + @Column(name = "tom", nullable = false, columnDefinition = "DATE") + val tom: LocalDate, + + @Enumerated(EnumType.STRING) + @Column(name = "datakilde", nullable = false) + val datakilde: Datakilde, +) : BaseEntitet() { + + override fun toString(): String { + return "PeriodeOvergangsstønadGrunnlag(" + + "id=$id, " + + "behandlingId=$behandlingId, " + + "aktør=$aktør, " + + "fom=$fom, " + + "tom=$tom, " + + "datakilde=$datakilde)" + } + fun tilInternPeriodeOvergangsstønad() = InternPeriodeOvergangsstønad( + personIdent = this.aktør.aktivFødselsnummer(), + fomDato = this.fom, + tomDato = this.tom, + ) +} + +fun EksternPeriode.tilPeriodeOvergangsstønadGrunnlag(behandlingId: Long, aktør: Aktør) = + PeriodeOvergangsstønadGrunnlag( + behandlingId = behandlingId, + aktør = aktør, + fom = this.fomDato, + tom = this.tomDato, + datakilde = this.datakilde, + ) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/sm\303\245barnstillegg/PeriodeOvergangsst\303\270nadGrunnlagRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/sm\303\245barnstillegg/PeriodeOvergangsst\303\270nadGrunnlagRepository.kt" new file mode 100644 index 000000000..2235ea88a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/sm\303\245barnstillegg/PeriodeOvergangsst\303\270nadGrunnlagRepository.kt" @@ -0,0 +1,15 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.småbarnstillegg + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query + +interface PeriodeOvergangsstønadGrunnlagRepository : JpaRepository { + + @Query("SELECT pog FROM PeriodeOvergangsstønadGrunnlag pog WHERE pog.behandlingId = :behandlingId") + fun findByBehandlingId(behandlingId: Long): List + + @Query("DELETE FROM PeriodeOvergangsstønadGrunnlag pog WHERE pog.behandlingId = :behandlingId") + @Modifying + fun deleteByBehandlingId(behandlingId: Long) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/GrunnlagController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/GrunnlagController.kt" new file mode 100644 index 000000000..e7bbabdb8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/GrunnlagController.kt" @@ -0,0 +1,61 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.søknad + +import no.nav.familie.ba.sak.common.BehandlingValidering.validerBehandlingKanRedigeres +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.kjerne.steg.TilbakestillBehandlingService +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/behandlinger") +@ProtectedWithClaims(issuer = "azuread") +class GrunnlagController( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val utvidetBehandlingService: UtvidetBehandlingService, + private val persongrunnlagService: PersongrunnlagService, + private val tilbakestillService: TilbakestillBehandlingService, + private val tilgangService: TilgangService, +) { + + @PostMapping(path = ["/{behandlingId}/legg-til-barn"]) + fun leggTilBarnIPersonopplysningsgrunnlag( + @PathVariable behandlingId: Long, + @RequestBody + leggTilBarnDto: LeggTilBarnDto, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "legge til barn", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId = behandlingId) + validerBehandlingKanRedigeres(behandling) + + persongrunnlagService.leggTilBarnIPersonopplysningsgrunnlag( + behandling = behandling, + nyttBarnIdent = leggTilBarnDto.barnIdent, + ) + tilbakestillService.initierOgSettBehandlingTilVilkårsvurdering(behandling) + return ResponseEntity.ok( + Ressurs.success( + utvidetBehandlingService + .lagRestUtvidetBehandling(behandlingId = behandling.id), + ), + ) + } + + class LeggTilBarnDto(val barnIdent: String) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/S\303\270knadGrunnlag.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/S\303\270knadGrunnlag.kt" new file mode 100644 index 000000000..743a92a2a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/S\303\270knadGrunnlag.kt" @@ -0,0 +1,50 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.søknad + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.ekstern.restDomene.BarnMedOpplysninger +import no.nav.familie.ba.sak.ekstern.restDomene.SøknadDTO +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.kontrakter.felles.objectMapper +import java.time.LocalDateTime + +@EntityListeners(RollestyringMotDatabase::class) +@Entity +@Table(name = "GR_SOKNAD") +data class SøknadGrunnlag( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "gr_soknad_seq_generator") + @SequenceGenerator(name = "gr_soknad_seq_generator", sequenceName = "gr_soknad_seq", allocationSize = 50) + val id: Long = 0, + + @Column(name = "opprettet_av", nullable = false, updatable = false) + val opprettetAv: String = SikkerhetContext.hentSaksbehandler(), + + @Column(name = "opprettet_tid", nullable = false, updatable = false) + val opprettetTidspunkt: LocalDateTime = LocalDateTime.now(), + + @Column(name = "fk_behandling_id", updatable = false, nullable = false) + val behandlingId: Long, + + @Column(name = "soknad", nullable = false, columnDefinition = "text") + var søknad: String, + + @Column(name = "aktiv", nullable = false) + var aktiv: Boolean = true, +) { + + fun hentSøknadDto(): SøknadDTO { + return objectMapper.readValue(this.søknad, SøknadDTO::class.java) + } + + fun hentUregistrerteBarn(): List { + return hentSøknadDto().barnaMedOpplysninger.filter { !it.erFolkeregistrert && it.inkludertISøknaden } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/S\303\270knadGrunnlagRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/S\303\270knadGrunnlagRepository.kt" new file mode 100644 index 000000000..a7743502b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/S\303\270knadGrunnlagRepository.kt" @@ -0,0 +1,16 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.søknad + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface SøknadGrunnlagRepository : JpaRepository { + + @Query("SELECT gr FROM SøknadGrunnlag gr WHERE gr.behandlingId = :behandlingId AND gr.aktiv = true") + fun hentAktiv(behandlingId: Long): SøknadGrunnlag? + + @Query("SELECT gr FROM SøknadGrunnlag gr WHERE gr.behandlingId = :behandlingId") + fun hent(behandlingId: Long): SøknadGrunnlag + + @Query("SELECT gr FROM SøknadGrunnlag gr WHERE gr.behandlingId = :behandlingId") + fun hentAlle(behandlingId: Long): List +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/S\303\270knadGrunnlagService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/S\303\270knadGrunnlagService.kt" new file mode 100644 index 000000000..36223ea8a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/S\303\270knadGrunnlagService.kt" @@ -0,0 +1,29 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.søknad + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class SøknadGrunnlagService( + private val søknadGrunnlagRepository: SøknadGrunnlagRepository, +) { + + @Transactional + fun lagreOgDeaktiverGammel(søknadGrunnlag: SøknadGrunnlag): SøknadGrunnlag { + val aktivSøknadGrunnlag = søknadGrunnlagRepository.hentAktiv(søknadGrunnlag.behandlingId) + + if (aktivSøknadGrunnlag != null) { + søknadGrunnlagRepository.saveAndFlush(aktivSøknadGrunnlag.also { it.aktiv = false }) + } + + return søknadGrunnlagRepository.save(søknadGrunnlag) + } + + fun hentAlle(behandlingId: Long): List { + return søknadGrunnlagRepository.hentAlle(behandlingId) + } + + fun hentAktiv(behandlingId: Long): SøknadGrunnlag? { + return søknadGrunnlagRepository.hentAktiv(behandlingId) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/Institusjon.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/Institusjon.kt new file mode 100644 index 000000000..afe1b0ad0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/Institusjon.kt @@ -0,0 +1,28 @@ +package no.nav.familie.ba.sak.kjerne.institusjon + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Institusjon") +@Table(name = "INSTITUSJON") +data class Institusjon( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "institusjon_seq_generator") + @SequenceGenerator(name = "institusjon_seq_generator", sequenceName = "institusjon_seq", allocationSize = 50) + val id: Long = 0, + + @Column(name = "org_nummer", updatable = false, length = 50) + val orgNummer: String, + + @Column(name = "tss_ekstern_id", updatable = false, length = 50) + val tssEksternId: String?, +) : BaseEntitet() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/InstitusjonRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/InstitusjonRepository.kt new file mode 100644 index 000000000..fabf09565 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/InstitusjonRepository.kt @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.kjerne.institusjon + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface InstitusjonRepository : JpaRepository { + fun findByOrgNummer(orgNummer: String): Institusjon? + fun findByTssEksternId(tssEksternId: String): Institusjon? +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/InstitusjonService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/InstitusjonService.kt new file mode 100644 index 000000000..9c9490e30 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/InstitusjonService.kt @@ -0,0 +1,39 @@ +package no.nav.familie.ba.sak.kjerne.institusjon + +import no.nav.familie.ba.sak.integrasjoner.samhandler.SamhandlerKlient +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.kontrakter.ba.tss.SamhandlerInfo +import org.springframework.stereotype.Service + +@Service +class InstitusjonService( + val fagsakRepository: FagsakRepository, + val samhandlerKlient: SamhandlerKlient, + val institusjonRepository: InstitusjonRepository, +) { + + fun hentEllerOpprettInstitusjon(orgNummer: String, tssEksternId: String?): Institusjon { + return institusjonRepository.findByOrgNummer(orgNummer) ?: institusjonRepository.saveAndFlush( + Institusjon( + orgNummer = orgNummer, + tssEksternId = tssEksternId, + ), + ) + } + + fun hentSamhandler(orgNummer: String): SamhandlerInfo { + return samhandlerKlient.hentSamhandler(orgNummer) + } + + fun søkSamhandlere(navn: String?, postnummer: String?, område: String?): List { + val komplettSamhandlerListe = mutableListOf() + var side = 0 + do { + val søkeresultat = samhandlerKlient.søkSamhandlere(navn, postnummer, område, side) + side++ + komplettSamhandlerListe.addAll(søkeresultat.samhandlere) + } while (søkeresultat.finnesMerInfo) + + return komplettSamhandlerListe + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/SamhandlerController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/SamhandlerController.kt new file mode 100644 index 000000000..79ebc7bbc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/SamhandlerController.kt @@ -0,0 +1,69 @@ +package no.nav.familie.ba.sak.kjerne.institusjon + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.http.client.RessursException +import no.nav.familie.kontrakter.ba.tss.SamhandlerInfo +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.HttpStatus +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.client.HttpClientErrorException + +@RestController +@RequestMapping("/api/samhandler") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class SamhandlerController( + private val institusjonService: InstitusjonService, +) { + + @GetMapping(path = ["/orgnr/{orgnr}"]) + fun hentSamhandlerDataForOrganisasjon( + @PathVariable("orgnr") orgNummer: String, + ): Ressurs = try { + Ressurs.success(institusjonService.hentSamhandler(orgNummer).copy(orgNummer = orgNummer)) + } catch (e: Exception) { + if (e.erNotFound()) { + throw FunksjonellFeil( + "Finner ikke institusjon. Kontakt NØS for å opprette TSS-ident.", + httpStatus = HttpStatus.NOT_FOUND, + throwable = e, + ) + } + throw e + } + + fun Exception.erNotFound() = (this is RessursException && httpStatus == HttpStatus.NOT_FOUND) || + (this is HttpClientErrorException && statusCode == HttpStatus.NOT_FOUND) + + @PostMapping(path = ["/navn"]) + fun søkSamhandlerinfoFraNavn( + @RequestBody request: SøkSamhandlerInfoRequest, + ): Ressurs> { + if (request.navn == null && request.postnummer == null && request.område == null) { + throw FunksjonellFeil( + "Påkrevd variabel for søk er navn, postnummer eller område", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + return Ressurs.success( + institusjonService.søkSamhandlere( + request.navn?.uppercase(), + request.postnummer, + request.område, + ), + ) + } +} + +data class SøkSamhandlerInfoRequest( + val navn: String?, + val postnummer: String?, + val område: String?, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageClient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageClient.kt new file mode 100644 index 000000000..88910e8fd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageClient.kt @@ -0,0 +1,51 @@ +package no.nav.familie.ba.sak.kjerne.klage + +import no.nav.familie.ba.sak.common.kallEksternTjenesteRessurs +import no.nav.familie.ba.sak.common.kallEksternTjenesteUtenRespons +import no.nav.familie.http.client.AbstractRestClient +import no.nav.familie.kontrakter.felles.klage.Fagsystem +import no.nav.familie.kontrakter.felles.klage.KlagebehandlingDto +import no.nav.familie.kontrakter.felles.klage.OpprettKlagebehandlingRequest +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.web.client.RestOperations +import org.springframework.web.util.UriComponentsBuilder +import java.net.URI + +@Component +class KlageClient( + @Qualifier("jwtBearer") restOperations: RestOperations, + @Value("\${FAMILIE_KLAGE_URL}") private val familieKlageUri: URI, +) : AbstractRestClient(restOperations, "integrasjon") { + fun opprettKlage(opprettKlagebehandlingRequest: OpprettKlagebehandlingRequest) { + val uri = UriComponentsBuilder + .fromUri(familieKlageUri) + .pathSegment("api/ekstern/behandling/opprett") + .build().toUri() + + return kallEksternTjenesteUtenRespons( + tjeneste = "klage", + uri = uri, + formål = "Opprett klagebehandling", + ) { + postForEntity(uri, opprettKlagebehandlingRequest) + } + } + + fun hentKlagebehandlinger(eksternIder: Set): Map> { + val uri = UriComponentsBuilder + .fromUri(familieKlageUri) + .pathSegment("api/ekstern/behandling/${Fagsystem.BA}") + .queryParam("eksternFagsakId", eksternIder.joinToString(",")) + .build().toUri() + + return kallEksternTjenesteRessurs( + tjeneste = "klage", + uri = uri, + formål = "Hent klagebehandlinger", + ) { + getForEntity(uri) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageController.kt new file mode 100644 index 000000000..d9f9af9df --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageController.kt @@ -0,0 +1,48 @@ +package no.nav.familie.ba.sak.kjerne.klage + +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.kjerne.klage.dto.OpprettKlageDto +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.klage.KlagebehandlingDto +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping(path = ["/api/fagsaker"], produces = [MediaType.APPLICATION_JSON_VALUE]) +@ProtectedWithClaims(issuer = "azuread") +class KlageController( + private val tilgangService: TilgangService, + private val klageService: KlageService, +) { + + @PostMapping("/{fagsakId}/opprett-klagebehandling") + fun opprettKlage(@PathVariable fagsakId: Long, @RequestBody opprettKlageDto: OpprettKlageDto): Ressurs { + tilgangService.validerTilgangTilHandlingOgFagsak( + fagsakId = fagsakId, + event = AuditLoggerEvent.CREATE, + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Opprett klagebehandling", + ) + klageService.opprettKlage(fagsakId, opprettKlageDto) + return Ressurs.success(fagsakId) + } + + @GetMapping("/{fagsakId}/hent-klagebehandlinger") + fun hentKlagebehandlinger(@PathVariable fagsakId: Long): Ressurs> { + tilgangService.validerTilgangTilHandlingOgFagsak( + fagsakId = fagsakId, + event = AuditLoggerEvent.ACCESS, + minimumBehandlerRolle = BehandlerRolle.VEILEDER, + handling = "Hent klagebehandlinger på fagsak", + ) + return Ressurs.success(klageService.hentKlagebehandlingerPåFagsak(fagsakId)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageService.kt new file mode 100644 index 000000000..ec33fc00b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageService.kt @@ -0,0 +1,185 @@ +package no.nav.familie.ba.sak.kjerne.klage + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.klage.dto.OpprettKlageDto +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.tilbakekreving.TilbakekrevingKlient +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.kontrakter.felles.klage.Fagsystem +import no.nav.familie.kontrakter.felles.klage.FagsystemType +import no.nav.familie.kontrakter.felles.klage.FagsystemVedtak +import no.nav.familie.kontrakter.felles.klage.IkkeOpprettet +import no.nav.familie.kontrakter.felles.klage.IkkeOpprettetÅrsak +import no.nav.familie.kontrakter.felles.klage.KanIkkeOppretteRevurderingÅrsak +import no.nav.familie.kontrakter.felles.klage.KanOppretteRevurderingResponse +import no.nav.familie.kontrakter.felles.klage.KlagebehandlingDto +import no.nav.familie.kontrakter.felles.klage.OpprettKlagebehandlingRequest +import no.nav.familie.kontrakter.felles.klage.OpprettRevurderingResponse +import no.nav.familie.kontrakter.felles.klage.Opprettet +import no.nav.familie.kontrakter.felles.klage.Stønadstype +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate + +@Service +class KlageService( + private val fagsakService: FagsakService, + private val klageClient: KlageClient, + private val integrasjonClient: IntegrasjonClient, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val stegService: StegService, + private val vedtakService: VedtakService, + private val tilbakekrevingKlient: TilbakekrevingKlient, + +) { + + private val logger = LoggerFactory.getLogger(javaClass) + + fun opprettKlage(fagsakId: Long, opprettKlageDto: OpprettKlageDto) { + val fagsak = fagsakService.hentPåFagsakId(fagsakId) + + opprettKlage(fagsak, opprettKlageDto.kravMottattDato) + } + + fun opprettKlage(fagsak: Fagsak, kravMottattDato: LocalDate) { + if (kravMottattDato.isAfter(LocalDate.now())) { + throw FunksjonellFeil("Kan ikke opprette klage med krav mottatt frem i tid") + } + + val aktivtFødselsnummer = fagsak.aktør.aktivFødselsnummer() + val enhetId = integrasjonClient.hentBehandlendeEnhetForPersonIdentMedRelasjoner(aktivtFødselsnummer).enhetId + + klageClient.opprettKlage( + OpprettKlagebehandlingRequest( + ident = aktivtFødselsnummer, + stønadstype = Stønadstype.BARNETRYGD, + eksternFagsakId = fagsak.id.toString(), + fagsystem = Fagsystem.BA, + klageMottatt = kravMottattDato, + behandlendeEnhet = enhetId, + ), + ) + } + + fun hentKlagebehandlingerPåFagsak(fagsakId: Long): List { + val klagebehandligerPerFagsak = klageClient.hentKlagebehandlinger(setOf(fagsakId)) + + val klagerPåFagsak = klagebehandligerPerFagsak[fagsakId] + ?: throw Feil("Fikk ikke fagsakId=$fagsakId tilbake fra kallet til klage.") + + return klagerPåFagsak.map { it.brukVedtaksdatoFraKlageinstansHvisOversendt() } + } + + @Transactional(readOnly = true) + fun kanOppretteRevurdering(fagsakId: Long): KanOppretteRevurderingResponse { + val fagsak = fagsakService.hentPåFagsakId(fagsakId) + val resultat = utledKanOppretteRevurdering(fagsak) + return when (resultat) { + is KanOppretteRevurdering -> KanOppretteRevurderingResponse(true, null) + is KanIkkeOppretteRevurdering -> KanOppretteRevurderingResponse( + false, + resultat.årsak.kanIkkeOppretteRevurderingÅrsak, + ) + } + } + + @Transactional + fun validerOgOpprettRevurderingKlage(fagsakId: Long): OpprettRevurderingResponse { + val fagsak = fagsakService.hentPåFagsakId(fagsakId) + + val resultat = utledKanOppretteRevurdering(fagsak) + return when (resultat) { + is KanOppretteRevurdering -> opprettRevurderingKlage(fagsak) + is KanIkkeOppretteRevurdering -> OpprettRevurderingResponse(IkkeOpprettet(resultat.årsak.ikkeOpprettetÅrsak)) + } + } + + private fun opprettRevurderingKlage(fagsak: Fagsak) = try { + val forrigeBehandling = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = fagsak.id) + ?: throw Feil("Finner ikke tidligere behandling") + + val nyBehandling = NyBehandling( + kategori = forrigeBehandling.kategori, + underkategori = forrigeBehandling.underkategori, + søkersIdent = forrigeBehandling.fagsak.aktør.aktivFødselsnummer(), + behandlingType = BehandlingType.REVURDERING, + behandlingÅrsak = BehandlingÅrsak.KLAGE, + navIdent = SikkerhetContext.hentSaksbehandler(), + + // barnasIdenter hentes fra forrige behandling i håndterNyBehandling() ved revurdering + barnasIdenter = emptyList(), + + fagsakId = forrigeBehandling.fagsak.id, + ) + + val revurdering = stegService.håndterNyBehandling(nyBehandling) + OpprettRevurderingResponse(Opprettet(revurdering.id.toString())) + } catch (e: Exception) { + logger.error("Feilet opprettelse av revurdering for fagsak=${fagsak.id}, se secure logg for detaljer") + secureLogger.error("Feilet opprettelse av revurdering for fagsak=$fagsak", e) + OpprettRevurderingResponse(IkkeOpprettet(IkkeOpprettetÅrsak.FEIL, e.message)) + } + + private fun utledKanOppretteRevurdering(fagsak: Fagsak): KanOppretteRevurderingResultat { + val erÅpenBehandlingPåFagsak = behandlingHentOgPersisterService.erÅpenBehandlingPåFagsak(fagsak.id) + if (erÅpenBehandlingPåFagsak) { + return KanIkkeOppretteRevurdering(Årsak.ÅPEN_BEHANDLING) + } + + val finnesVedtattBehandlingPåFagsak = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = fagsak.id) != null + if (!finnesVedtattBehandlingPåFagsak) { + return KanIkkeOppretteRevurdering(Årsak.INGEN_BEHANDLING) + } + return KanOppretteRevurdering + } + + fun hentFagsystemVedtak(fagsakId: Long): List { + val fagsak = fagsakService.hentPåFagsakId(fagsakId) + val behandlinger = behandlingHentOgPersisterService.hentFerdigstilteBehandlinger(fagsak.id) + val ferdigstilteBaBehandlinger = behandlinger.map { it.tilFagsystemVedtak() } + + val vedtakTilbakekreving = tilbakekrevingKlient.hentTilbakekrevingsvedtak(fagsakId) + + return ferdigstilteBaBehandlinger + vedtakTilbakekreving + } + + private fun Behandling.tilFagsystemVedtak(): FagsystemVedtak { + val vedtak = vedtakService.hentAktivForBehandlingThrows(id) + + return FagsystemVedtak( + eksternBehandlingId = this.id.toString(), + behandlingstype = this.type.visningsnavn, + resultat = this.resultat.displayName, + vedtakstidspunkt = vedtak.vedtaksdato ?: error("Mangler vedtakstidspunkt for behandling=$id"), + fagsystemType = FagsystemType.ORDNIÆR, + regelverk = this.kategori.tilRegelverk(), + ) + } +} + +private sealed interface KanOppretteRevurderingResultat +private object KanOppretteRevurdering : KanOppretteRevurderingResultat +private data class KanIkkeOppretteRevurdering(val årsak: Årsak) : KanOppretteRevurderingResultat + +private enum class Årsak( + val ikkeOpprettetÅrsak: IkkeOpprettetÅrsak, + val kanIkkeOppretteRevurderingÅrsak: KanIkkeOppretteRevurderingÅrsak, +) { + + ÅPEN_BEHANDLING(IkkeOpprettetÅrsak.ÅPEN_BEHANDLING, KanIkkeOppretteRevurderingÅrsak.ÅPEN_BEHANDLING), + INGEN_BEHANDLING(IkkeOpprettetÅrsak.INGEN_BEHANDLING, KanIkkeOppretteRevurderingÅrsak.INGEN_BEHANDLING), +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageUtils.kt new file mode 100644 index 000000000..13c742146 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageUtils.kt @@ -0,0 +1,17 @@ +package no.nav.familie.ba.sak.kjerne.klage + +import no.nav.familie.kontrakter.felles.klage.BehandlingEventType +import no.nav.familie.kontrakter.felles.klage.BehandlingResultat +import no.nav.familie.kontrakter.felles.klage.KlagebehandlingDto + +fun KlagebehandlingDto.brukVedtaksdatoFraKlageinstansHvisOversendt(): KlagebehandlingDto { + val erOversendtTilKlageinstans = resultat == BehandlingResultat.IKKE_MEDHOLD + val vedtaksdato = if (erOversendtTilKlageinstans) { + klageinstansResultat + .singleOrNull { klageinstansResultat -> klageinstansResultat.type == BehandlingEventType.KLAGEBEHANDLING_AVSLUTTET } + ?.mottattEllerAvsluttetTidspunkt + } else { + vedtaksdato + } + return copy(vedtaksdato = vedtaksdato) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/dto/OpprettKlageDto.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/dto/OpprettKlageDto.kt new file mode 100644 index 000000000..31b2d500d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/klage/dto/OpprettKlageDto.kt @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.kjerne.klage.dto + +import java.time.LocalDate + +data class OpprettKlageDto(val kravMottattDato: LocalDate) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetaling.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetaling.kt new file mode 100644 index 000000000..688d75061 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetaling.kt @@ -0,0 +1,70 @@ +package no.nav.familie.ba.sak.kjerne.korrigertetterbetaling + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "KorrigertEtterbetaling") +@Table(name = "KORRIGERT_ETTERBETALING") +class KorrigertEtterbetaling( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "korrigert_etterbetaling_seq_generator") + @SequenceGenerator( + name = "korrigert_etterbetaling_seq_generator", + sequenceName = "korrigert_etterbetaling_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Enumerated(EnumType.STRING) + @Column(name = "aarsak") + val årsak: KorrigertEtterbetalingÅrsak, + + @Column(name = "begrunnelse") + val begrunnelse: String?, + + @Column(name = "belop") + val beløp: Int, + + @ManyToOne(optional = false) + @JoinColumn(name = "fk_behandling_id") + val behandling: Behandling, + + @Column(name = "aktiv") + var aktiv: Boolean, +) : BaseEntitet() + +data class KorrigertEtterbetalingRequest( + val årsak: KorrigertEtterbetalingÅrsak, + val begrunnelse: String?, + val beløp: Int, +) + +fun KorrigertEtterbetalingRequest.tilKorrigertEtterbetaling(behandling: Behandling) = + KorrigertEtterbetaling( + årsak = årsak, + begrunnelse = begrunnelse, + behandling = behandling, + beløp = beløp, + aktiv = true, + ) + +enum class KorrigertEtterbetalingÅrsak(val visningsnavn: String) { + FEIL_TIDLIGERE_UTBETALT_BELØP("Feil i tidligere utbetalt beløp"), + REFUSJON_FRA_UDI("Refusjon fra UDI"), + REFUSJON_FRA_ANDRE_MYNDIGHETER("Refusjon fra andre myndigheter"), + MOTREGNING("Motregning"), +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingController.kt new file mode 100644 index 000000000..54d8ea5b6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingController.kt @@ -0,0 +1,87 @@ +package no.nav.familie.ba.sak.kjerne.korrigertetterbetaling + +import no.nav.familie.ba.sak.common.BehandlingValidering.validerBehandlingKanRedigeres +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestKorrigertEtterbetaling +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestKorrigertEtterbetaling +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/korrigertetterbetaling") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class KorrigertEtterbetalingController( + private val tilgangService: TilgangService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val korrigertEtterbetalingService: KorrigertEtterbetalingService, + private val utvidetBehandlingService: UtvidetBehandlingService, +) { + @PostMapping(path = ["/behandling/{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun opprettKorrigertEtterbetalingPåBehandling( + @PathVariable behandlingId: Long, + @RequestBody korrigertEtterbetalingRequest: KorrigertEtterbetalingRequest, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.CREATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Opprett korrigert etterbetaling", + ) + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + validerBehandlingKanRedigeres(behandling) + + val korrigertEtterbetaling = korrigertEtterbetalingRequest.tilKorrigertEtterbetaling(behandling) + + korrigertEtterbetalingService.lagreKorrigertEtterbetaling(korrigertEtterbetaling) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId))) + } + + @GetMapping(path = ["/behandling/{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentAlleKorrigerteEtterbetalingPåBehandling( + @PathVariable behandlingId: Long, + ): ResponseEntity>> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.VEILEDER, + handling = "Henter korrigert etterbetaling", + ) + + val korrigerteEtterbetalinger = korrigertEtterbetalingService.finnAlleKorrigeringerPåBehandling(behandlingId) + .map { it.tilRestKorrigertEtterbetaling() } + + return ResponseEntity.ok(Ressurs.success(korrigerteEtterbetalinger)) + } + + @PatchMapping(path = ["/behandling/{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun settKorrigertEtterbetalingTilInaktivPåBehandling( + @PathVariable behandlingId: Long, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Oppdaterer korrigert etterbetaling", + ) + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + validerBehandlingKanRedigeres(behandling) + + korrigertEtterbetalingService.settKorrigeringPåBehandlingTilInaktiv(behandling) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingRepository.kt new file mode 100644 index 000000000..8b8eb3155 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingRepository.kt @@ -0,0 +1,12 @@ +package no.nav.familie.ba.sak.kjerne.korrigertetterbetaling + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface KorrigertEtterbetalingRepository : JpaRepository { + @Query("SELECT ke FROM KorrigertEtterbetaling ke JOIN ke.behandling b WHERE b.id = :behandlingId AND ke.aktiv = true") + fun finnAktivtKorrigeringPåBehandling(behandlingId: Long): KorrigertEtterbetaling? + + @Query("SELECT ke FROM KorrigertEtterbetaling ke JOIN ke.behandling b WHERE b.id = :behandlingId") + fun finnAlleKorrigeringerPåBehandling(behandlingId: Long): List +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingService.kt new file mode 100644 index 000000000..d7e0ff1fc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingService.kt @@ -0,0 +1,39 @@ +package no.nav.familie.ba.sak.kjerne.korrigertetterbetaling + +import jakarta.transaction.Transactional +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import org.springframework.stereotype.Service + +@Service +class KorrigertEtterbetalingService( + private val korrigertEtterbetalingRepository: KorrigertEtterbetalingRepository, + private val loggService: LoggService, +) { + + fun finnAktivtKorrigeringPåBehandling(behandlingId: Long): KorrigertEtterbetaling? = + korrigertEtterbetalingRepository.finnAktivtKorrigeringPåBehandling(behandlingId) + + fun finnAlleKorrigeringerPåBehandling(behandlingId: Long): List = + korrigertEtterbetalingRepository.finnAlleKorrigeringerPåBehandling(behandlingId) + + @Transactional + fun lagreKorrigertEtterbetaling(korrigertEtterbetaling: KorrigertEtterbetaling): KorrigertEtterbetaling { + val behandling = korrigertEtterbetaling.behandling + + finnAktivtKorrigeringPåBehandling(behandling.id)?.let { + it.aktiv = false + korrigertEtterbetalingRepository.saveAndFlush(it) + } + + loggService.opprettKorrigertEtterbetalingLogg(behandling, korrigertEtterbetaling) + return korrigertEtterbetalingRepository.save(korrigertEtterbetaling) + } + + @Transactional + fun settKorrigeringPåBehandlingTilInaktiv(behandling: Behandling): KorrigertEtterbetaling? = + finnAktivtKorrigeringPåBehandling(behandling.id)?.apply { + aktiv = false + loggService.opprettKorrigertEtterbetalingLogg(behandling, this) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtak.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtak.kt new file mode 100644 index 000000000..d28323696 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtak.kt @@ -0,0 +1,51 @@ +package no.nav.familie.ba.sak.kjerne.korrigertvedtak + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "KorrigertVedtak") +@Table(name = "KORRIGERT_VEDTAK") +class KorrigertVedtak( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "korrigert_vedtak_seq_generator") + @SequenceGenerator( + name = "korrigert_vedtak_seq_generator", + sequenceName = "korrigert_vedtak_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "vedtaksdato", columnDefinition = "DATE") + val vedtaksdato: LocalDate, + + @Column(name = "begrunnelse") + val begrunnelse: String?, + + @ManyToOne(optional = false) + @JoinColumn(name = "fk_behandling_id") + val behandling: Behandling, + + @Column(name = "aktiv") + var aktiv: Boolean, +) : BaseEntitet() + +data class KorrigerVedtakRequest( + val vedtaksdato: LocalDate, + val begrunnelse: String?, +) + +fun KorrigerVedtakRequest.tilKorrigerVedtak(behandling: Behandling) = + KorrigertVedtak(vedtaksdato = vedtaksdato, begrunnelse = begrunnelse, behandling = behandling, aktiv = true) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakController.kt new file mode 100644 index 000000000..a36005dd1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakController.kt @@ -0,0 +1,71 @@ +package no.nav.familie.ba.sak.kjerne.korrigertvedtak + +import no.nav.familie.ba.sak.common.BehandlingValidering.validerBehandlingKanRedigeres +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/korrigertvedtak") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class KorrigertVedtakController( + private val tilgangService: TilgangService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val korrigertVedtakService: KorrigertVedtakService, + private val utvidetBehandlingService: UtvidetBehandlingService, +) { + + @PostMapping(path = ["/behandling/{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun opprettKorrigertVedtakPåBehandling( + @PathVariable behandlingId: Long, + @RequestBody korrigerVedtakRequest: KorrigerVedtakRequest, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.CREATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Opprett korrigert vedtak", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + validerBehandlingKanRedigeres(behandling) + + val korrigertVedtak = korrigerVedtakRequest.tilKorrigerVedtak(behandling) + + korrigertVedtakService.lagreKorrigertVedtak(korrigertVedtak) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId))) + } + + @PatchMapping(path = ["/behandling/{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun settKorrigertVedtakTilInaktivPåBehandling( + @PathVariable behandlingId: Long, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Oppdaterer korrigert vedtak", + ) + + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + validerBehandlingKanRedigeres(behandling) + + korrigertVedtakService.settKorrigertVedtakPåBehandlingTilInaktiv(behandling) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakRepository.kt new file mode 100644 index 000000000..3122f4f71 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakRepository.kt @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.kjerne.korrigertvedtak + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface KorrigertVedtakRepository : JpaRepository { + + @Query("SELECT kv FROM KorrigertVedtak kv JOIN kv.behandling b WHERE b.id = :behandlingId AND kv.aktiv = true") + fun finnAktivtKorrigertVedtakPåBehandling(behandlingId: Long): KorrigertVedtak? +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakService.kt new file mode 100644 index 000000000..d6e929c05 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakService.kt @@ -0,0 +1,36 @@ +package no.nav.familie.ba.sak.kjerne.korrigertvedtak + +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class KorrigertVedtakService( + private val korrigertVedtakRepository: KorrigertVedtakRepository, + private val loggService: LoggService, +) { + + fun finnAktivtKorrigertVedtakPåBehandling(behandlingId: Long): KorrigertVedtak? = + korrigertVedtakRepository.finnAktivtKorrigertVedtakPåBehandling(behandlingId) + + @Transactional + fun lagreKorrigertVedtak(korrigertVedtak: KorrigertVedtak): KorrigertVedtak { + val behandling = korrigertVedtak.behandling + + finnAktivtKorrigertVedtakPåBehandling(behandling.id)?.let { + it.aktiv = false + korrigertVedtakRepository.saveAndFlush(it) + } + + loggService.opprettKorrigertVedtakLogg(behandling, korrigertVedtak) + return korrigertVedtakRepository.save(korrigertVedtak) + } + + @Transactional + fun settKorrigertVedtakPåBehandlingTilInaktiv(behandling: Behandling): KorrigertVedtak? = + finnAktivtKorrigertVedtakPåBehandling(behandling.id)?.apply { + aktiv = false + loggService.opprettKorrigertVedtakLogg(behandling, this) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/BehandlingLoggRequest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/BehandlingLoggRequest.kt new file mode 100644 index 000000000..f245d3c78 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/BehandlingLoggRequest.kt @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.kjerne.logg + +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling + +data class BehandlingLoggRequest(val behandling: Behandling, val barnasIdenter: List = emptyList()) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/Logg.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/Logg.kt new file mode 100644 index 000000000..f7beaf60a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/Logg.kt @@ -0,0 +1,102 @@ +package no.nav.familie.ba.sak.kjerne.logg + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import java.time.LocalDateTime + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Logg") +@Table(name = "logg") +data class Logg( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "logg_seq_generator") + @SequenceGenerator(name = "logg_seq_generator", sequenceName = "logg_seq", allocationSize = 50) + val id: Long = 0, + + @Column(name = "opprettet_av", nullable = false, updatable = false) + val opprettetAv: String = SikkerhetContext.hentSaksbehandlerNavn(), + + @Column(name = "opprettet_tid", nullable = false, updatable = false) + val opprettetTidspunkt: LocalDateTime = LocalDateTime.now(), + + @Column(name = "fk_behandling_id") + val behandlingId: Long, + + @Enumerated(EnumType.STRING) + @Column(name = "type") + val type: LoggType, + + @Column(name = "tittel") + val tittel: String, + + @Enumerated(EnumType.STRING) + @Column(name = "rolle") + val rolle: BehandlerRolle, + + /** + * Feltet støtter markdown frontend. + */ + @Column(name = "tekst") + val tekst: String, +) { + + constructor(behandlingId: Long, type: LoggType, rolle: BehandlerRolle, tekst: String = "") : this( + behandlingId = behandlingId, + type = type, + tittel = type.tittel, + rolle = rolle, + tekst = tekst, + ) +} + +enum class LoggType(val visningsnavn: String, val tittel: String = visningsnavn) { + AUTOVEDTAK_TIL_MANUELL_BEHANDLING("Autovedtak til manuell behandling", "Automatisk behandling stoppet"), + VERGE_REGISTRERT("Verge ble registrert"), + INSTITUSJON_REGISTRERT("Institusjon ble registrert"), + FØDSELSHENDELSE("Fødselshendelse"), // Deprecated, bruk livshendelse + LIVSHENDELSE("Livshendelse"), + BEHANDLENDE_ENHET_ENDRET("Behandlende enhet endret", "Endret enhet på behandling"), + BEHANDLING_OPPRETTET("Behandling opprettet"), + BEHANDLINGSTYPE_ENDRET("Endret behandlingstype", "Endret behandlingstema"), + BARN_LAGT_TIL("Barn lagt til på behandling"), + DOKUMENT_MOTTATT("Dokument ble mottatt"), + SØKNAD_REGISTRERT("Søknaden ble registrert"), + VILKÅRSVURDERING("Vilkårsvurdering"), + SEND_TIL_BESLUTTER("Send til beslutter", "Sendt til beslutter"), + SEND_TIL_SYSTEM("Send til system", "Sendt til system"), + GODKJENNE_VEDTAK("Godkjenne vedtak", "Vedtak godkjent"), + MIGRERING_BEKREFTET("Migrering bekreftet", "Migrering bekreftet"), + DISTRIBUERE_BREV("Distribuere brev", "Brev sendt"), + BREV_IKKE_DISTRIBUERT("Brev ikke distribuert", "Brevet ble ikke distribuert fordi mottaker har ukjent adresse"), + BREV_IKKE_DISTRIBUERT_UKJENT_DØDSBO( + "Brev ikke distribuert. Ukjent dødsbo", + "Mottaker har ukjent dødsboadresse, og brevet blir ikke sendt før adressen er satt", + ), + FERDIGSTILLE_BEHANDLING("Ferdigstille behandling", "Ferdigstilt behandling"), + HENLEGG_BEHANDLING("Henlegg behandling", "Behandlingen er henlagt"), + BEHANDLIG_SATT_PÅ_VENT("Behandlingen er satt på vent"), + BEHANDLIG_GJENOPPTATT("Behandling gjenopptatt"), + BEHANDLING_SATT_PÅ_MASKINELL_VENT("Behandlingen er satt på maskinell vent"), + BEHANDLING_TATT_AV_MASKINELL_VENT("Behandlingen er tatt av maskinell vent"), + VENTENDE_BEHANDLING_ENDRET("Behandlingen er oppdatert"), + KORRIGERT_ETTERBETALING("Etterbetaling i brev er korrigert"), + MANUELT_SMÅBARNSTILLEGG_JUSTERING("Småbarnstillegg er manuelt endret."), + KORRIGERT_VEDTAK("Behandlingen er korrigering av vedtak"), + FEILUTBETALT_VALUTA_LAGT_TIL("Feilutbetalt valuta lagt til"), + FEILUTBETALT_VALUTA_FJERNET("Feilutbetalt valuta fjernet"), + BREVMOTTAKER_LAGT_TIL_ELLER_FJERNET("Brevmottaker lagt til eller fjernet"), + REFUSJON_EØS_LAGT_TIL("Refusjon EØS lagt til"), + REFUSJON_EØS_FJERNET("Refusjon EØS fjernet"), + MANUELL_DØDSFALL_DATO_REGISTRERT("Manuell dødsfallsdato registrert"), +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/LoggController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/LoggController.kt new file mode 100644 index 000000000..c7b0421e8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/LoggController.kt @@ -0,0 +1,38 @@ +package no.nav.familie.ba.sak.kjerne.logg + +import no.nav.familie.ba.sak.common.RessursUtils.badRequest +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/logg") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class LoggController( + private val tilgangService: TilgangService, + private val loggService: LoggService, +) { + + @GetMapping(path = ["/{behandlingId}"]) + fun hentLoggForBehandling( + @PathVariable + behandlingId: Long, + ): ResponseEntity>> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + return Result.runCatching { loggService.hentLoggForBehandling(behandlingId) } + .fold( + onSuccess = { ResponseEntity.ok(Ressurs.success(it)) }, + onFailure = { + badRequest("Henting av logg feilet", it) + }, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/LoggRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/LoggRepository.kt new file mode 100644 index 000000000..079f531fd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/LoggRepository.kt @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.kjerne.logg + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface LoggRepository : JpaRepository { + @Query(value = "SELECT l FROM Logg l WHERE l.behandlingId = :behandlingId ORDER BY l.opprettetTidspunkt desc, l.id desc") + fun hentLoggForBehandling(behandlingId: Long): List +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/LoggService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/LoggService.kt new file mode 100644 index 000000000..49cc79822 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/logg/LoggService.kt @@ -0,0 +1,642 @@ +package no.nav.familie.ba.sak.kjerne.logg + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.common.tilddMMyyyy +import no.nav.familie.ba.sak.config.RolleConfig +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Arbeidsfordelingsenhet +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.domene.ArbeidsfordelingPåBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.brev.mottaker.Brevmottaker +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.korrigertetterbetaling.KorrigertEtterbetaling +import no.nav.familie.ba.sak.kjerne.korrigertvedtak.KorrigertVedtak +import no.nav.familie.ba.sak.kjerne.personident.Identkonverterer +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.kjerne.vedtak.feilutbetaltValuta.FeilutbetaltValuta +import no.nav.familie.ba.sak.kjerne.vedtak.refusjonEøs.RefusjonEøs +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.kontrakter.felles.Fødselsnummer +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.LocalDateTime + +@Service +class LoggService( + private val loggRepository: LoggRepository, + private val rolleConfig: RolleConfig, +) { + + private val metrikkPerLoggType: Map = LoggType.values().associateWith { + Metrics.counter( + "behandling.logg", + "type", + it.name, + "beskrivelse", + it.visningsnavn, + ) + } + + fun opprettBehandlendeEnhetEndret( + behandling: Behandling, + fraEnhet: Arbeidsfordelingsenhet, + tilEnhet: ArbeidsfordelingPåBehandling, + manuellOppdatering: Boolean, + begrunnelse: String, + ) { + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.BEHANDLENDE_ENHET_ENDRET, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = "Behandlende enhet ${if (manuellOppdatering) "manuelt" else "automatisk"} endret fra " + + "${fraEnhet.enhetId} ${fraEnhet.enhetNavn} til ${tilEnhet.behandlendeEnhetId} ${tilEnhet.behandlendeEnhetNavn}." + + if (begrunnelse.isNotBlank()) "\n\n$begrunnelse" else "", + ), + ) + } + + fun opprettMottattDokument(behandling: Behandling, tekst: String, mottattDato: LocalDateTime) { + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.DOKUMENT_MOTTATT, + tittel = "Dokument mottatt ${mottattDato.toLocalDate().tilKortString()}", + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = tekst, + ), + ) + } + + fun opprettRegistrerInstitusjonLogg(behandling: Behandling) { + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.INSTITUSJON_REGISTRERT, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = "", + ), + ) + } + + fun opprettRegistrerVergeLogg(behandling: Behandling) { + val tittel = "Verge ble registrert" + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.VERGE_REGISTRERT, + tittel = tittel, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = "", + ), + ) + } + + fun opprettRegistrertSøknadLogg(behandling: Behandling, søknadFinnesFraFør: Boolean) { + val tittel = if (!søknadFinnesFraFør) "Søknaden ble registrert" else "Søknaden ble endret" + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.SØKNAD_REGISTRERT, + tittel = tittel, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = "", + ), + ) + } + + fun opprettEndretBehandlingstema( + behandling: Behandling, + forrigeUnderkategori: BehandlingUnderkategori, + forrigeKategori: BehandlingKategori, + nyUnderkategori: BehandlingUnderkategori, + nyKategori: BehandlingKategori, + ) { + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.BEHANDLINGSTYPE_ENDRET, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = "Behandlingstema er manuelt endret fra ${ + tilBehandlingstema( + underkategori = forrigeUnderkategori, + kategori = forrigeKategori, + ) + } til ${tilBehandlingstema(underkategori = nyUnderkategori, kategori = nyKategori)}", + ), + ) + } + + fun opprettVilkårsvurderingLogg( + behandling: Behandling, + forrigeBehandlingsresultat: Behandlingsresultat, + nyttBehandlingsresultat: Behandlingsresultat, + ): Logg? { + val tekst = when { + forrigeBehandlingsresultat == Behandlingsresultat.IKKE_VURDERT -> { + "Resultat ble ${nyttBehandlingsresultat.displayName.lowercase()}" + } + + forrigeBehandlingsresultat != nyttBehandlingsresultat -> { + "Resultat gikk fra ${forrigeBehandlingsresultat.displayName.lowercase()} til ${nyttBehandlingsresultat.displayName.lowercase()}" + } + + else -> return null + } + + return lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.VILKÅRSVURDERING, + tittel = if (forrigeBehandlingsresultat != Behandlingsresultat.IKKE_VURDERT) "Vilkårsvurdering endret" else "Vilkårsvurdering gjennomført", + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = tekst, + ), + ) + } + + fun opprettAutovedtakTilManuellBehandling(behandling: Behandling, tekst: String) { + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.AUTOVEDTAK_TIL_MANUELL_BEHANDLING, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = tekst, + ), + ) + } + + private fun opprettLivshendelseLogg(behandling: BehandlingLoggRequest, tittel: String) { + lagre( + Logg( + behandlingId = behandling.behandling.id, + type = LoggType.LIVSHENDELSE, + tittel = tittel, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = "Gjelder barn ${fødselsdatoer(behandling)}", + ), + ) + } + + private fun fødselsdatoer(behandling: BehandlingLoggRequest) = Utils.slåSammen( + behandling.barnasIdenter + .filter { Identkonverterer.er11Siffer(it) } + .distinct() + .map { Fødselsnummer(it) } + .map { it.fødselsdato } + .map { it.tilKortString() }, + ) + + fun opprettBehandlingLogg(behandlingLogg: BehandlingLoggRequest) { + val behandling = behandlingLogg.behandling + if (behandling.opprettetÅrsak == BehandlingÅrsak.FØDSELSHENDELSE) { + opprettLivshendelseLogg(behandling = behandlingLogg, tittel = "Mottok fødselshendelse") + } else if (behandling.skalBehandlesAutomatisk && behandling.erSmåbarnstillegg()) { + opprettLivshendelseLogg(behandling = behandlingLogg, tittel = "Mottok overgansstønadshendelse") + } + + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.BEHANDLING_OPPRETTET, + tittel = "${behandling.type.visningsnavn} opprettet", + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = "", + ), + ) + } + + fun opprettSendTilBeslutterLogg(behandling: Behandling, skalAutomatiskBesluttes: Boolean) { + lagre( + Logg( + behandlingId = behandling.id, + type = if (behandling.erManuellMigrering() && skalAutomatiskBesluttes) LoggType.SEND_TIL_SYSTEM else LoggType.SEND_TIL_BESLUTTER, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + ), + ) + } + + fun opprettBeslutningOmVedtakLogg( + behandling: Behandling, + beslutning: Beslutning, + begrunnelse: String? = null, + behandlingErAutomatiskBesluttet: Boolean, + ) { + val behandlingErManuellMigreringSomBleAutomatiskBesluttet = + behandling.erManuellMigrering() && behandlingErAutomatiskBesluttet + + lagre( + Logg( + behandlingId = behandling.id, + type = if (behandlingErManuellMigreringSomBleAutomatiskBesluttet) LoggType.MIGRERING_BEKREFTET else LoggType.GODKJENNE_VEDTAK, + tittel = if (beslutning.erGodkjent()) { + if (behandlingErManuellMigreringSomBleAutomatiskBesluttet) "Migrering bekreftet" else "Vedtak godkjent" + } else { + "Vedtak underkjent" + }, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext(rolleConfig, BehandlerRolle.BESLUTTER), + tekst = if (!beslutning.erGodkjent()) "Begrunnelse: $begrunnelse" else "", + opprettetAv = if (behandlingErManuellMigreringSomBleAutomatiskBesluttet) { + SikkerhetContext.SYSTEM_NAVN + } else { + SikkerhetContext.hentSaksbehandlerNavn() + }, + ), + ) + } + + fun opprettDistribuertBrevLogg(behandlingId: Long, tekst: String, rolle: BehandlerRolle) { + lagre( + Logg( + behandlingId = behandlingId, + type = LoggType.DISTRIBUERE_BREV, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext(rolleConfig, rolle), + tekst = tekst, + ), + ) + } + + fun opprettBrevIkkeDistribuertUkjentAdresseLogg(behandlingId: Long, brevnavn: String) { + lagre( + Logg( + behandlingId = behandlingId, + type = LoggType.BREV_IKKE_DISTRIBUERT, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext(rolleConfig, BehandlerRolle.SYSTEM), + tekst = brevnavn, + ), + ) + } + + fun opprettBrevIkkeDistribuertUkjentDødsboadresseLogg(behandlingId: Long, brevnavn: String) { + lagre( + Logg( + behandlingId = behandlingId, + type = LoggType.BREV_IKKE_DISTRIBUERT_UKJENT_DØDSBO, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext(rolleConfig, BehandlerRolle.SYSTEM), + tekst = brevnavn, + ), + ) + } + + fun opprettFerdigstillBehandling(behandling: Behandling) { + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.FERDIGSTILLE_BEHANDLING, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext(rolleConfig, BehandlerRolle.SYSTEM), + ), + ) + } + + fun opprettHenleggBehandling(behandling: Behandling, årsak: String, begrunnelse: String) { + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.HENLEGG_BEHANDLING, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = "$årsak: $begrunnelse", + ), + ) + } + + fun opprettBarnLagtTilLogg(behandling: Behandling, barn: Person) { + val beskrivelse = + "${barn.navn.uppercase()} (${barn.hentAlder()} år) | ${Identkonverterer.formaterIdent(barn.aktør.aktivFødselsnummer())} lagt til" + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.BARN_LAGT_TIL, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = beskrivelse, + ), + ) + } + + fun opprettSettPåVentLogg(behandling: Behandling, årsak: String) { + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.BEHANDLIG_SATT_PÅ_VENT, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = "Årsak: $årsak", + ), + ) + } + + fun opprettSettPåMaskinellVent(behandling: Behandling, årsak: String) { + val logg = Logg( + behandling.id, + type = LoggType.BEHANDLING_SATT_PÅ_MASKINELL_VENT, + // TODO FORVALTER_ROLLE + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext(rolleConfig, BehandlerRolle.UKJENT), + tekst = "Årsak: $årsak", + ) + lagre(logg) + } + + fun opprettTattAvMaskinellVent(behandling: Behandling) { + val logg = Logg( + behandling.id, + type = LoggType.BEHANDLING_TATT_AV_MASKINELL_VENT, + // TODO FORVALTER_ROLLE + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext(rolleConfig, BehandlerRolle.UKJENT), + ) + lagre(logg) + } + + fun opprettOppdaterVentingLogg(behandling: Behandling, endretÅrsak: String?, endretFrist: LocalDate?) { + val tekst = if (endretFrist != null && endretÅrsak != null) { + "Frist og årsak er endret til \"${endretÅrsak}\" og ${endretFrist.tilKortString()}" + } else if (endretFrist != null) { + "Frist er endret til ${endretFrist.tilKortString()}" + } else if (endretÅrsak != null) { + "Årsak er endret til \"${endretÅrsak}\"" + } else { + return + } + + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.VENTENDE_BEHANDLING_ENDRET, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = tekst, + ), + ) + } + + fun opprettKorrigertEtterbetalingLogg( + behandling: Behandling, + korrigertEtterbetaling: KorrigertEtterbetaling, + ) { + val tekst = if (korrigertEtterbetaling.aktiv) { + """ + Årsak: ${korrigertEtterbetaling.årsak.visningsnavn} + Nytt beløp: ${korrigertEtterbetaling.beløp} kr + Begrunnelse: ${korrigertEtterbetaling.begrunnelse ?: "Ingen begrunnelse"} + """.trimIndent() + } else { + "" + } + + val tittel = if (korrigertEtterbetaling.aktiv) { + "Etterbetaling i brev er korrigert" + } else { + "Korrigert etterbetaling er angret" + } + + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.KORRIGERT_ETTERBETALING, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tittel = tittel, + tekst = tekst, + ), + ) + } + + fun opprettSmåbarnstilleggLogg( + behandling: Behandling, + tittel: String, + ) = + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.MANUELT_SMÅBARNSTILLEGG_JUSTERING, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tittel = tittel, + tekst = "", + ), + ) + + fun gjenopptaBehandlingLogg(behandling: Behandling) { + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.BEHANDLIG_GJENOPPTATT, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + ), + ) + } + + fun opprettKorrigertVedtakLogg( + behandling: Behandling, + korrigertVedtak: KorrigertVedtak, + ) { + val tekst = if (korrigertVedtak.aktiv) { + """ + Vedtaksdato: ${korrigertVedtak.vedtaksdato.tilddMMyyyy()} + Begrunnelse: ${korrigertVedtak.begrunnelse ?: "Ingen begrunnelse"} + """.trimIndent() + } else { + "" + } + + val tittel = if (korrigertVedtak.aktiv) { + "Vedtaket er korrigert etter § 35" + } else { + "Korrigering av vedtaket etter § 35 er fjernet" + } + + lagre( + Logg( + behandlingId = behandling.id, + type = LoggType.KORRIGERT_VEDTAK, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tittel = tittel, + tekst = tekst, + ), + ) + } + + fun opprettBrevmottakerLogg( + brevmottaker: Brevmottaker, + brevmottakerFjernet: Boolean, + ) { + val lagtTilEllerFjernet = if (brevmottakerFjernet) "fjernet" else "lagt til" + val tittel = "${brevmottaker.type.visningsnavn} er $lagtTilEllerFjernet som brevmottaker" + + val tekst = listOfNotNull( + brevmottaker.navn, + brevmottaker.adresselinje1, + brevmottaker.adresselinje2, + brevmottaker.postnummer, + brevmottaker.poststed, + brevmottaker.landkode, + ).joinToString(separator = System.lineSeparator()) + + lagre( + Logg( + behandlingId = brevmottaker.behandlingId, + type = LoggType.BREVMOTTAKER_LAGT_TIL_ELLER_FJERNET, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tittel = tittel, + tekst = tekst, + ), + ) + } + + fun loggFeilutbetaltValutaPeriodeLagtTil(behandlingId: Long, feilutbetaltValuta: FeilutbetaltValuta) = + lagre( + Logg( + behandlingId = behandlingId, + type = LoggType.FEILUTBETALT_VALUTA_LAGT_TIL, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = """ + Periode: ${feilutbetaltValuta.fom.tilKortString()} - ${feilutbetaltValuta.tom.tilKortString()} + Beløp: ${feilutbetaltValuta.feilutbetaltBeløp} ${if (feilutbetaltValuta.erPerMåned) "kr/mnd" else "kr"} + """.trimIndent(), + ), + ) + + fun loggFeilutbetaltValutaPeriodeFjernet(behandlingId: Long, feilutbetaltValuta: FeilutbetaltValuta) = + lagre( + Logg( + behandlingId = behandlingId, + type = LoggType.FEILUTBETALT_VALUTA_FJERNET, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = """ + Periode: ${feilutbetaltValuta.fom.tilKortString()} - ${feilutbetaltValuta.tom.tilKortString()} + Beløp: ${feilutbetaltValuta.feilutbetaltBeløp} ${if (feilutbetaltValuta.erPerMåned) "kr/mnd" else "kr"} + """.trimIndent(), + ), + ) + + fun loggRefusjonEøsPeriodeLagtTil(refusjonEøs: RefusjonEøs) = + lagre( + Logg( + behandlingId = refusjonEøs.behandlingId, + type = LoggType.REFUSJON_EØS_LAGT_TIL, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = """ + Periode: ${refusjonEøs.fom.tilKortString()} - ${refusjonEøs.tom.tilKortString()} + Beløp: ${refusjonEøs.refusjonsbeløp} kr/mnd + """.trimIndent(), + ), + ) + + fun loggRefusjonEøsPeriodeFjernet(refusjonEøs: RefusjonEøs) = + lagre( + Logg( + behandlingId = refusjonEøs.behandlingId, + type = LoggType.REFUSJON_EØS_FJERNET, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = """ + Periode: ${refusjonEøs.fom.tilKortString()} - ${refusjonEøs.tom.tilKortString()} + Beløp: ${refusjonEøs.refusjonsbeløp} kr/mnd + """.trimIndent(), + ), + ) + + fun loggManueltRegistrertDødsfallDato(behandlingId: BehandlingId, person: Person, begrunnelse: String) = + lagre( + Logg( + behandlingId = behandlingId.id, + type = LoggType.MANUELL_DØDSFALL_DATO_REGISTRERT, + rolle = SikkerhetContext.hentRolletilgangFraSikkerhetscontext( + rolleConfig, + BehandlerRolle.SAKSBEHANDLER, + ), + tekst = "Begrunnelse: $begrunnelse", + tittel = "Dødsfall dato er manuelt registrert for barn født ${person.fødselsdato}", + ), + ) + + fun lagre(logg: Logg): Logg { + metrikkPerLoggType[logg.type]?.increment() + + return loggRepository.save(logg) + } + + fun hentLoggForBehandling(behandlingId: Long): List { + return loggRepository.hentLoggForBehandling(behandlingId) + } + + companion object { + + private fun tilBehandlingstema(underkategori: BehandlingUnderkategori, kategori: BehandlingKategori): String { + return "${kategori.visningsnavn} ${underkategori.visningsnavn.lowercase()}" + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/metrikker/TeamStatistikkService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/metrikker/TeamStatistikkService.kt new file mode 100644 index 000000000..30ef8e7a3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/metrikker/TeamStatistikkService.kt @@ -0,0 +1,185 @@ +package no.nav.familie.ba.sak.kjerne.metrikker + +import io.micrometer.core.instrument.Metrics +import io.micrometer.core.instrument.MultiGauge +import io.micrometer.core.instrument.Tags +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.leader.LeaderClient +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import java.time.Duration +import java.time.LocalDateTime +import java.time.YearMonth + +@Service +class TeamStatistikkService( + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, +) { + val utbetalingerPerMånedGauge = + MultiGauge.builder("UtbetalingerPerMaanedGauge").register(Metrics.globalRegistry) + val antallFagsakerPerMånedGauge = + MultiGauge.builder("AntallFagsakerPerMaanedGauge").register(Metrics.globalRegistry) + val løpendeFagsakerPerMånedGauge = + MultiGauge.builder("LopendeFagsakerPerMaanedGauge").register(Metrics.globalRegistry) + val åpneBehandlingerPerMånedGauge = + MultiGauge.builder("AapneBehandlingerPerMaanedGauge").register(Metrics.globalRegistry) + val tidSidenOpprettelseåpneBehandlingerPerMånedGauge = + MultiGauge.builder("TidSidenOpprettelseAapneBehandlingerPerMaanedGauge").register(Metrics.globalRegistry) + + @Scheduled(initialDelay = FEM_MINUTTER_VENTETID_FØR_OPPDATERING_FØRSTE_GANG, fixedRate = OPPDATERING_HVER_DAG) + fun utbetalinger() { + if (!erLeader()) return + + val månederMedTotalUtbetaling = + listOf( + YearMonth.now(), + YearMonth.now().plusMonths(1), + ).associateWith { + behandlingRepository.hentTotalUtbetalingForMåned(it.førsteDagIInneværendeMåned().atStartOfDay()) + } + + val rows = månederMedTotalUtbetaling.map { + MultiGauge.Row.of( + Tags.of( + ÅR_MÅNED_TAG, + "${it.key.year}-${it.key.month}", + ), + it.value, + ) + } + + utbetalingerPerMånedGauge.register(rows) + } + + @Scheduled(initialDelay = FEM_MINUTTER_VENTETID_FØR_OPPDATERING_FØRSTE_GANG, fixedRate = OPPDATERING_HVER_DAG) + fun antallFagsaker() { + if (!erLeader()) return + + val antallFagsaker = fagsakRepository.finnAntallFagsakerTotalt() + + val rows = listOf( + MultiGauge.Row.of( + Tags.of( + ÅR_MÅNED_TAG, + "${YearMonth.now().year}-${YearMonth.now().month}", + ), + antallFagsaker, + ), + ) + + antallFagsakerPerMånedGauge.register(rows) + } + + @Scheduled(initialDelay = FEM_MINUTTER_VENTETID_FØR_OPPDATERING_FØRSTE_GANG, fixedRate = OPPDATERING_HVER_DAG) + fun løpendeFagsaker() { + if (!erLeader()) return + + val løpendeFagsaker = fagsakRepository.finnAntallFagsakerLøpende() + + val rows = listOf( + MultiGauge.Row.of( + Tags.of( + ÅR_MÅNED_TAG, + "${YearMonth.now().year}-${YearMonth.now().month}", + ), + løpendeFagsaker, + ), + ) + + løpendeFagsakerPerMånedGauge.register(rows) + } + + @Scheduled(initialDelay = FEM_MINUTTER_VENTETID_FØR_OPPDATERING_FØRSTE_GANG, fixedRate = OPPDATERING_HVER_DAG) + fun åpneBehandlinger() { + if (!erLeader()) return + + val åpneBehandlinger = behandlingRepository.finnAntallBehandlingerIkkeAvsluttet() + + val rows = listOf( + MultiGauge.Row.of( + Tags.of( + ÅR_MÅNED_TAG, + "${YearMonth.now().year}-${YearMonth.now().month}", + ), + åpneBehandlinger, + ), + ) + + åpneBehandlingerPerMånedGauge.register(rows) + } + + @Scheduled(initialDelay = FEM_MINUTTER_VENTETID_FØR_OPPDATERING_FØRSTE_GANG, fixedRate = OPPDATERING_HVER_DAG) + fun tidFraOpprettelsePåÅpneBehandlinger() { + if (!erLeader()) return + + val opprettelsestidspunktPååpneBehandlinger = behandlingRepository.finnOpprettelsestidspunktPåÅpneBehandlinger() + val diffPåÅpneBehandlinger = + opprettelsestidspunktPååpneBehandlinger.map { Duration.between(it, LocalDateTime.now()).seconds } + + val snitt = diffPåÅpneBehandlinger.average() + val max = diffPåÅpneBehandlinger.maxOf { it } + val min = diffPåÅpneBehandlinger.minOf { it } + + val rows = listOf( + MultiGauge.Row.of( + Tags.of( + ÅR_MÅNED_TAG, + "${YearMonth.now().year}-${YearMonth.now().month}-snitt", + ), + snitt, + ), + MultiGauge.Row.of( + Tags.of( + ÅR_MÅNED_TAG, + "${YearMonth.now().year}-${YearMonth.now().month}-max", + ), + max, + ), + MultiGauge.Row.of( + Tags.of( + ÅR_MÅNED_TAG, + "${YearMonth.now().year}-${YearMonth.now().month}-min", + ), + min, + ), + ) + + tidSidenOpprettelseåpneBehandlingerPerMånedGauge.register(rows) + } + + @Scheduled(cron = "0 0 14 * * *") + fun loggÅpneBehandlingerSomHarLiggetLenge() { + if (!erLeader()) return + + listOf(180, 150, 120, 90, 60).fold(mutableSetOf()) { acc, dagerSiden -> + val åpneBehandlinger = behandlingRepository.finnÅpneBehandlinger( + opprettetFør = LocalDateTime.now().minusDays(dagerSiden.toLong()), + ).filter { !acc.contains(it.id) } + + if (åpneBehandlinger.isNotEmpty()) { + logger.warn( + "${åpneBehandlinger.size} åpne behandlinger har ligget i over $dagerSiden dager: \n" + + "${åpneBehandlinger.map { behandling -> "$behandling\n" }}", + ) + acc.addAll(åpneBehandlinger.map { it.id }) + } + + acc + } + } + + private fun erLeader(): Boolean { + return LeaderClient.isLeader() == true + } + + companion object { + const val OPPDATERING_HVER_DAG: Long = 1000 * 60 * 60 * 24 + const val FEM_MINUTTER_VENTETID_FØR_OPPDATERING_FØRSTE_GANG: Long = 1000 * 60 * 5 + const val ÅR_MÅNED_TAG = "aar-maaned" + val logger = LoggerFactory.getLogger(TeamStatistikkService::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/Akt\303\270r.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/Akt\303\270r.kt" new file mode 100644 index 000000000..e663860ca --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/Akt\303\270r.kt" @@ -0,0 +1,72 @@ +package no.nav.familie.ba.sak.kjerne.personident + +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.FetchType +import jakarta.persistence.Id +import jakarta.persistence.OneToMany +import jakarta.persistence.Table +import jakarta.validation.constraints.Pattern +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.util.Objects + +/** + * Id som genereres fra NAV Aktør Register. Denne iden benyttes til interne forhold i Nav og vil ikke endres f.eks. dersom bruker + * går fra DNR til FNR i Folkeregisteret. Tilsvarende vil den kunne referere personer som har ident fra et utenlandsk system. + */ +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Aktør") +@Table(name = "AKTOER") +data class Aktør( + // Er ikke kalt id ettersom den refererer til en ekstern id. + @Id + @Column(name = "aktoer_id", updatable = false, length = 50) + // Validator kommer virke først i Spring 3.0 grunnet at hibernate tatt i bruke Jakarta. + @Pattern(regexp = VALID_REGEXP) + val aktørId: String, + + @OneToMany( + fetch = FetchType.EAGER, + mappedBy = "aktør", + cascade = [CascadeType.ALL], + ) + val personidenter: MutableSet = mutableSetOf(), +) : BaseEntitet() { + + init { + require(VALID.matcher(aktørId).matches()) { + // skal ikke skje, funksjonelle feilmeldinger håndteres ikke her. + "Ugyldig aktør, støtter kun 13 siffer.)" + } + } + + override fun toString(): String { + return """aktørId=$aktørId""".trimMargin() + } + + override fun equals(other: Any?): Boolean { + if (other == null || javaClass != other.javaClass) { + return false + } + val otherAktør: Aktør = other as Aktør + return aktørId == otherAktør.aktørId + } + + override fun hashCode(): Int { + return Objects.hash(aktørId) + } + + fun aktivFødselsnummer() = personidenter.single { it.aktiv }.fødselsnummer + + fun harIdent(fødselsnummer: String) = personidenter.any { it.fødselsnummer == fødselsnummer } + + companion object { + private const val VALID_REGEXP = "^\\d{13}$" + private val VALID = java.util.regex.Pattern.compile(VALID_REGEXP) + } +} + +val dummyAktør = Aktør("0000000000000") diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/Akt\303\270rIdRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/Akt\303\270rIdRepository.kt" new file mode 100644 index 000000000..79cfe9f88 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/Akt\303\270rIdRepository.kt" @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.kjerne.personident + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface AktørIdRepository : JpaRepository { + @Query("SELECT a FROM Aktør a WHERE a.aktørId = :aktørId") + fun findByAktørIdOrNull(aktørId: String): Aktør? +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/IdentController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/IdentController.kt new file mode 100644 index 000000000..de3964763 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/IdentController.kt @@ -0,0 +1,26 @@ +package no.nav.familie.ba.sak.kjerne.personident + +import no.nav.familie.kontrakter.felles.PersonIdent +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/ident") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class IdentController( + private val personidentService: PersonidentService, +) { + + @PostMapping + fun håndterPdlHendelse(@RequestBody nyIdent: PersonIdent): ResponseEntity> { + personidentService.opprettTaskForIdentHendelse(nyIdent) + return ResponseEntity.ok(Ressurs.success("Håndtert ny ident")) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/IdentHendelseTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/IdentHendelseTask.kt new file mode 100644 index 000000000..fd2c6f36b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/IdentHendelseTask.kt @@ -0,0 +1,47 @@ +package no.nav.familie.ba.sak.kjerne.personident + +import no.nav.familie.kontrakter.felles.PersonIdent +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.util.Properties + +@Service +@TaskStepBeskrivelse( + taskStepType = IdentHendelseTask.TASK_STEP_TYPE, + beskrivelse = "Sjekker om ident-hendelse berører person registert i BA-sak og oppdaterer", + maxAntallFeil = 3, + triggerTidVedFeilISekunder = (60 * 60 * 24).toLong(), +) +class IdentHendelseTask( + private val personidentService: PersonidentService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + logger.info("Kjører task for håntering av identhendelse.") + val personIdent = objectMapper.readValue(task.payload, PersonIdent::class.java) + personidentService.håndterNyIdent(personIdent) + } + + companion object { + const val TASK_STEP_TYPE = "IdentHendelseTask" + private val logger: Logger = LoggerFactory.getLogger(IdentHendelseTask::class.java) + + fun opprettTask(nyIdent: PersonIdent): Task { + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString(nyIdent), + properties = Properties().apply { + this["nyPersonIdent"] = nyIdent.ident + }, + ).medTriggerTid( + triggerTid = LocalDateTime.now().plusMinutes(1), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/Identkonverterer.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/Identkonverterer.kt new file mode 100644 index 000000000..6060ebd5e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/Identkonverterer.kt @@ -0,0 +1,12 @@ +package no.nav.familie.ba.sak.kjerne.personident + +object Identkonverterer { + fun er11Siffer(ident: String): Boolean = ident.all { it.isDigit() } && ident.length == 11 + + fun formaterIdent(ident: String): String = + if (er11Siffer(ident)) { + "${ident.substring(0, 6)} ${ident.substring(6)}" + } else { + ident + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/Personident.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/Personident.kt new file mode 100644 index 000000000..90160861d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/Personident.kt @@ -0,0 +1,71 @@ +package no.nav.familie.ba.sak.kjerne.personident + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import jakarta.validation.constraints.Pattern +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDateTime +import java.util.Objects + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Personident") +@Table(name = "PERSONIDENT") +data class Personident( + @Id + @Column(name = "foedselsnummer", nullable = false) + // Lovlige typer er fnr, dnr eller npid + // Validator kommer virke først i Spring 3.0 grunnet at hibernate tatt i bruke Jakarta. + @Pattern(regexp = VALID_FØDSELSNUMMER) + val fødselsnummer: String, + + @JsonIgnore + @ManyToOne(optional = false) + @JoinColumn(name = "fk_aktoer_id", nullable = false, updatable = false) + val aktør: Aktør, + + @Column(name = "aktiv", nullable = false) + var aktiv: Boolean = true, + + @Column(name = "gjelder_til", columnDefinition = "DATE") + var gjelderTil: LocalDateTime? = null, + +) : BaseEntitet() { + + init { + require(VALID.matcher(fødselsnummer).matches()) { + "Ugyldig fødselsnummer, støtter kun 11 siffer.)" + } + } + + override fun toString(): String { + return """Personident(aktørId=${aktør.aktørId}, + |aktiv=$aktiv + |gjelderTil=$gjelderTil) + """.trimMargin() + } + + override fun equals(other: Any?): Boolean { + if (other == null || javaClass != other.javaClass) { + return false + } + val entitet: Personident = other as Personident + return fødselsnummer == entitet.fødselsnummer && aktiv == entitet.aktiv + } + + override fun hashCode(): Int { + return Objects.hash(fødselsnummer, aktiv) + } + + companion object { + private const val VALID_FØDSELSNUMMER = "^\\d{11}$" + private val VALID = + java.util.regex.Pattern.compile(VALID_FØDSELSNUMMER) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/PersonidentRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/PersonidentRepository.kt new file mode 100644 index 000000000..f2699b513 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/PersonidentRepository.kt @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.kjerne.personident + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface PersonidentRepository : JpaRepository { + @Query("SELECT p FROM Personident p WHERE p.fødselsnummer = :fødselsnummer") + fun findByFødselsnummerOrNull(fødselsnummer: String): Personident? +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/PersonidentService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/PersonidentService.kt new file mode 100644 index 000000000..65d01969a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/personident/PersonidentService.kt @@ -0,0 +1,198 @@ +package no.nav.familie.ba.sak.kjerne.personident + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.pdl.PdlIdentRestClient +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.IdentInformasjon +import no.nav.familie.kontrakter.felles.PersonIdent +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Service +class PersonidentService( + private val personidentRepository: PersonidentRepository, + private val aktørIdRepository: AktørIdRepository, + private val pdlIdentRestClient: PdlIdentRestClient, + private val taskRepository: TaskRepositoryWrapper, +) { + + fun hentIdenter(personIdent: String, historikk: Boolean): List { + return pdlIdentRestClient.hentIdenter(personIdent, historikk) + } + + fun identSkalLeggesTil(nyIdent: PersonIdent): Boolean { + val identerFraPdl = hentIdenter(nyIdent.ident, true) + val aktører = identerFraPdl + .filter { it.gruppe == "AKTORID" } + .map { it.ident } + .mapNotNull { aktørIdRepository.findByAktørIdOrNull(it) } + + if (aktører.isNotEmpty()) { + return aktører.firstOrNull { it.harIdent(nyIdent.ident) } == null + } + return false + } + + @Transactional + fun håndterNyIdent(nyIdent: PersonIdent): Aktør? { + logger.info("Håndterer ny ident") + secureLogger.info("Håndterer ny ident ${nyIdent.ident}") + val identerFraPdl = hentIdenter(nyIdent.ident, true) + + val aktørId = filtrerAktivtAktørId(identerFraPdl) + + validerOmAktørIdErMerget(identerFraPdl) + + val aktør = aktørIdRepository.findByAktørIdOrNull(aktørId) + + return if (aktør?.harIdent(fødselsnummer = nyIdent.ident) == false) { + logger.info("Legger til ny ident") + secureLogger.info("Legger til ny ident ${nyIdent.ident} på aktør ${aktør.aktørId}") + opprettPersonIdent(aktør, nyIdent.ident) + } else { + aktør + } + } + + @Transactional + fun opprettTaskForIdentHendelse(nyIdent: PersonIdent) { + if (identSkalLeggesTil(nyIdent)) { + logger.info("Oppretter task for senere håndterering av ny ident") + secureLogger.info("Oppretter task for senere håndterering av ny ident ${nyIdent.ident}") + taskRepository.save(IdentHendelseTask.opprettTask(nyIdent)) + } else { + logger.info("Ident er ikke knyttet til noen av aktørene våre, ignorerer hendelse.") + } + } + + fun hentAlleFødselsnummerForEnAktør(aktør: Aktør) = + hentIdenter(aktør.aktivFødselsnummer(), true) + .filter { it.gruppe == "FOLKEREGISTERIDENT" } + .map { it.ident } + + fun hentAktør(identEllerAktørId: String): Aktør { + val aktør = hentOgLagreAktør(ident = identEllerAktørId, lagre = false) + + if (aktør.personidenter.find { it.aktiv } == null) { + secureLogger.warn("Fant ikke aktiv ident for aktør med id ${aktør.aktørId} for ident $identEllerAktørId") + throw Feil("Fant ikke aktiv ident for aktør") + } + + return aktør + } + + fun hentOgLagreAktør(ident: String, lagre: Boolean): Aktør { + // Noter at ident kan være både av typen aktørid eller fødselsnummer (d- og f nummer) + val personident = try { + personidentRepository.findByFødselsnummerOrNull(ident) + } catch (e: Exception) { + secureLogger.info("Feil ved henting av ident=$ident, lagre=$lagre", e) + throw e + } + if (personident != null) { + return personident.aktør + } + + val aktørIdent = aktørIdRepository.findByAktørIdOrNull(ident) + if (aktørIdent != null) { + return aktørIdent + } + + val identerFraPdl = hentIdenter(ident, false) + val fødselsnummerAktiv = filtrerAktivtFødselsnummer(identerFraPdl) + val aktørIdStr = filtrerAktivtAktørId(identerFraPdl) + + val personidentPersistert = personidentRepository.findByFødselsnummerOrNull(fødselsnummerAktiv) + if (personidentPersistert != null) { + return personidentPersistert.aktør + } + + val aktørPersistert = aktørIdRepository.findByAktørIdOrNull(aktørIdStr) + if (aktørPersistert != null) { + return opprettPersonIdent(aktørPersistert, fødselsnummerAktiv, lagre) + } + + return opprettAktørIdOgPersonident(aktørIdStr, fødselsnummerAktiv, lagre) + } + + fun hentOgLagreAktørIder(barnasFødselsnummer: List, lagre: Boolean): List { + return barnasFødselsnummer.map { hentOgLagreAktør(it, lagre) } + } + + fun hentAktørIder(barnasFødselsnummer: List): List { + return barnasFødselsnummer.map { hentAktør(it) } + } + + /* + Ved merge vil èn av de to gjeldende aktør-IDene videreføres som gjeldende. Vi trenger dermed å sjekke + om det finnes en aktiv personident rad for den gamle aktørId + + */ + + private fun validerOmAktørIdErMerget(alleHistoriskeIdenterFraPdl: List) { + val alleHistoriskeAktørIder = alleHistoriskeIdenterFraPdl.filter { it.gruppe == "AKTORID" && it.historisk == true }.map { it.ident } + + val aktiveAktørerForHistoriskAktørIder = alleHistoriskeAktørIder + .mapNotNull { aktørId -> aktørIdRepository.findByAktørIdOrNull(aktørId) } + .filter { aktør -> aktør.personidenter.any { personident -> personident.aktiv } } + + if (aktiveAktørerForHistoriskAktørIder.isNotEmpty()) { + secureLogger.warn("Potensielt merget ident for $alleHistoriskeIdenterFraPdl") + throw Feil( + message = "Mottok potensielt en hendelse på en merget ident for aktørId=${filtrerAktivtAktørId(alleHistoriskeIdenterFraPdl)}. Sjekk securelogger for liste med identer. Sjekk om identen har flere saker. Disse må løses manuelt", + ) + } + } + + private fun opprettAktørIdOgPersonident(aktørIdStr: String, fødselsnummer: String, lagre: Boolean): Aktør { + secureLogger.info("Oppretter aktør og personIdent. aktørIdStr=$aktørIdStr fødselsnummer=$fødselsnummer lagre=$lagre") + val aktør = Aktør(aktørId = aktørIdStr).also { + it.personidenter.add( + Personident(fødselsnummer = fødselsnummer, aktør = it), + ) + } + + return if (lagre) { + aktørIdRepository.saveAndFlush(aktør) + } else { + aktør + } + } + + private fun opprettPersonIdent(aktør: Aktør, fødselsnummer: String, lagre: Boolean = true): Aktør { + secureLogger.info("Oppretter personIdent. aktørIdStr=${aktør.aktørId} fødselsnummer=$fødselsnummer lagre=$lagre, personidenter=${aktør.personidenter}") + val eksisterendePersonIdent = aktør.personidenter.filter { it.fødselsnummer == fødselsnummer && it.aktiv } + secureLogger.info("Aktøren har fødselsnummer ${aktør.personidenter.map { it.fødselsnummer } }") + if (eksisterendePersonIdent.isEmpty()) { + secureLogger.info("Fins ikke eksisterende personIdent for. aktørIdStr=${aktør.aktørId} fødselsnummer=$fødselsnummer lagre=$lagre, personidenter=${aktør.personidenter}, så lager ny") + aktør.personidenter.filter { it.aktiv }.map { + it.aktiv = false + it.gjelderTil = LocalDateTime.now() + } + if (lagre) aktørIdRepository.saveAndFlush(aktør) // Må lagre her fordi unik index er en blanding av aktørid og gjelderTil, og hvis man ikke lagerer før man legger til ny, så feiler det pga indexen. + + aktør.personidenter.add( + Personident(fødselsnummer = fødselsnummer, aktør = aktør), + ) + if (lagre) aktørIdRepository.saveAndFlush(aktør) + } + return aktør + } + + private fun filtrerAktivtFødselsnummer(identerFraPdl: List) = + ( + identerFraPdl.singleOrNull { it.gruppe == "FOLKEREGISTERIDENT" }?.ident + ?: throw Error("Finner ikke aktiv ident i Pdl") + ) + + private fun filtrerAktivtAktørId(identerFraPdl: List): String = + identerFraPdl.singleOrNull { it.gruppe == "AKTORID" && it.historisk == false }?.ident + ?: throw Error("Finner ikke aktørId i Pdl") + + companion object { + val logger = LoggerFactory.getLogger(PersonidentService::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/KontrollerNyUtbetalingsgeneratorService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/KontrollerNyUtbetalingsgeneratorService.kt new file mode 100644 index 000000000..487864353 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/KontrollerNyUtbetalingsgeneratorService.kt @@ -0,0 +1,292 @@ +package no.nav.familie.ba.sak.kjerne.simulering + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.integrasjoner.økonomi.AndelTilkjentYtelseForSimuleringFactory +import no.nav.familie.ba.sak.integrasjoner.økonomi.UtbetalingsoppdragGeneratorService +import no.nav.familie.ba.sak.integrasjoner.økonomi.skalIverksettesMotOppdrag +import no.nav.familie.ba.sak.integrasjoner.økonomi.tilRestUtbetalingsoppdrag +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiKlient +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.simulering.domene.SimuleringsPeriode +import no.nav.familie.ba.sak.kjerne.simulering.domene.tilTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.fraOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilForrigeMåned +import no.nav.familie.ba.sak.kjerne.tidslinje.tilOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.beskjær +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.felles.utbetalingsgenerator.domain.AndelMedPeriodeIdLongId +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.kontrakter.felles.simulering.DetaljertSimuleringResultat +import org.springframework.stereotype.Service +import java.math.BigDecimal +import java.time.YearMonth + +@Service +class KontrollerNyUtbetalingsgeneratorService( + private val featureToggleService: FeatureToggleService, + private val økonomiKlient: ØkonomiKlient, + private val utbetalingsoppdragGeneratorService: UtbetalingsoppdragGeneratorService, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, +) { + + fun kontrollerNyUtbetalingsgenerator( + vedtak: Vedtak, + saksbehandlerId: String, + ): List { + if (!skalKontrollereOppMotNyUtbetalingsgenerator()) return emptyList() + + val gammeltUtbetalingsoppdrag = + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = vedtak, + saksbehandlerId = saksbehandlerId, + andelTilkjentYtelseForUtbetalingsoppdragFactory = AndelTilkjentYtelseForSimuleringFactory(), + ) + + if (!gammeltUtbetalingsoppdrag.skalIverksettesMotOppdrag()) return emptyList() + + val gammeltSimuleringResultat = økonomiKlient.hentSimulering(gammeltUtbetalingsoppdrag) + + return kontrollerNyUtbetalingsgenerator( + vedtak = vedtak, + gammeltSimuleringResultat = gammeltSimuleringResultat, + gammeltUtbetalingsoppdrag = gammeltUtbetalingsoppdrag, + ) + } + + fun kontrollerNyUtbetalingsgenerator( + vedtak: Vedtak, + gammeltSimuleringResultat: DetaljertSimuleringResultat, + gammeltUtbetalingsoppdrag: Utbetalingsoppdrag, + erSimulering: Boolean = false, + ): List { + try { + if (!skalKontrollereOppMotNyUtbetalingsgenerator()) return emptyList() + + val behandling = vedtak.behandling + + if (erSimulering) { + secureLogger.info("Simulerer utbetalingsoppdrag for simulering for behandling ${behandling.id}") + } else { + secureLogger.info( + "Simulerer utbetalingsoppdrag for iverksettelse for behandling ${behandling.id}", + ) + } + + val diffFeilTyper = mutableListOf() + + val beregnetUtbetalingsoppdrag = + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = vedtak, + saksbehandlerId = SikkerhetContext.hentSaksbehandler().take(8), + erSimulering = erSimulering, + ) + + if (!beregnetUtbetalingsoppdrag.utbetalingsoppdrag.tilRestUtbetalingsoppdrag() + .skalIverksettesMotOppdrag() + ) { + return emptyList() + } + + secureLogger.info("Behandling ${behandling.id} har følgende oppdaterte andeler: ${beregnetUtbetalingsoppdrag.andeler}") + + secureLogger.info("Behandling ${behandling.id} får følgende utbetalingsoppdrag med gammel generator: $gammeltUtbetalingsoppdrag") + secureLogger.info("Behandling ${behandling.id} får følgende utbetalingsoppdrag med ny generator: ${beregnetUtbetalingsoppdrag.utbetalingsoppdrag}") + + validerAtAndelerIBeregnetUtbetalingsoppdragMatcherAndelerMedUtbetaling( + beregnetUtbetalingsoppdrag.andeler, + behandling, + )?.let { + diffFeilTyper.add(it) + } + + val nyttSimuleringResultat = + økonomiKlient.hentSimulering(beregnetUtbetalingsoppdrag.utbetalingsoppdrag.tilRestUtbetalingsoppdrag()) + + if (nyttSimuleringResultat.simuleringMottaker.isEmpty() && gammeltSimuleringResultat.simuleringMottaker.isEmpty()) return diffFeilTyper + + if (!bådeNyOgGammelGirEtResultat( + nyttSimuleringResultat = nyttSimuleringResultat, + gammeltSimuleringResultat = gammeltSimuleringResultat, + behandling = behandling, + ) + ) { + diffFeilTyper.add(DiffFeilType.DetEneSimuleringsresultatetErTomt) + return diffFeilTyper + } + + val simuleringsPerioderGammel = gammeltSimuleringResultat.tilSorterteSimuleringsPerioder(behandling) + + val simuleringsPerioderNy = nyttSimuleringResultat.tilSorterteSimuleringsPerioder(behandling) + + val simuleringsPerioderGammelTidslinje: Tidslinje = + simuleringsPerioderGammel.tilTidslinje() + + val simuleringsPerioderNyTidslinje: Tidslinje = + simuleringsPerioderNy.tilTidslinje() + + validerAtSimuleringsPerioderGammelHarResultatLik0ForPerioderFørSimuleringsPerioderNy( + simuleringsPerioderGammelTidslinje = simuleringsPerioderGammelTidslinje, + simuleringsPerioderNyTidslinje = simuleringsPerioderNyTidslinje, + behandling = behandling, + )?.let { + diffFeilTyper.add(it) + } + validerAtSimuleringsPerioderGammelHarResultatLikSimuleringsPerioderNyEtterFomTilNy( + simuleringsPerioderGammelTidslinje = simuleringsPerioderGammelTidslinje, + simuleringsPerioderNyTidslinje = simuleringsPerioderNyTidslinje, + behandling = behandling, + )?.let { + diffFeilTyper.add(it) + } + + if (diffFeilTyper.isNotEmpty()) { + loggSimuleringsPerioderMedDiff(simuleringsPerioderGammel, simuleringsPerioderNy) + } + + if (diffFeilTyper.isNotEmpty()) { + secureLogger.info("kontrollerNyUtbetalingsgenerator for ${behandling.id} ga følgende feiltyper=$diffFeilTyper") + } + + return diffFeilTyper + } catch (e: Exception) { + secureLogger.warn( + "En uventet feil har oppstått ved kontroll av ny utbetalingsoppdrag-generator for behandling ${vedtak.behandling.id}", + e, + ) + return listOf(DiffFeilType.UventetFeil) + } + } + + private fun validerAtAndelerIBeregnetUtbetalingsoppdragMatcherAndelerMedUtbetaling( + andeler: List, + behandling: Behandling, + ): DiffFeilType? { + val tilkjentYtelse = tilkjentYtelseRepository.findByBehandling(behandlingId = behandling.id) + + val andelerMedUtbetaling = tilkjentYtelse.andelerTilkjentYtelse.filter { it.erAndelSomSkalSendesTilOppdrag() } + + if (andeler.size != andelerMedUtbetaling.size) { + secureLogger.warn("Antallet andeler fra ny generator matcher ikke antallet andeler med utbetaling i behandling ${behandling.id}. Andeler fra ny generator: ${andeler.map { it.id }}, andeler med utbetaling: ${andelerMedUtbetaling.map { it.id }}.") + return DiffFeilType.FeilAntallAndeler + } + + if (!andelerMedUtbetaling.all { andelerMedUtbetaling -> andeler.any { it.id == andelerMedUtbetaling.id } }) { + secureLogger.warn("Finner ikke match for alle andeler med utbetaling i behandling ${behandling.id} blandt andelene returnert fra ny generator. Andeler fra ny generator: ${andeler.map { it.id }}, andeler med utbetaling: ${andelerMedUtbetaling.map { it.id }}.") + return DiffFeilType.AndelerMatcherIkke + } + + return null + } + + private fun bådeNyOgGammelGirEtResultat( + nyttSimuleringResultat: DetaljertSimuleringResultat, + gammeltSimuleringResultat: DetaljertSimuleringResultat, + behandling: Behandling, + ): Boolean { + val andelerEtterDagensDato = tilkjentYtelseRepository.findByBehandling(behandlingId = behandling.id) + .andelerTilkjentYtelse + .filter { andelTilkjentYtelse -> + andelTilkjentYtelse.stønadTom.isAfter(YearMonth.now()) + } + if (!(nyttSimuleringResultat.simuleringMottaker.isNotEmpty() && gammeltSimuleringResultat.simuleringMottaker.isNotEmpty())) { + secureLogger.warn("Behandling ${behandling.id} får tomt simuleringsresultat med ny eller gammel generator. Ny er tom: ${nyttSimuleringResultat.simuleringMottaker.isEmpty()}, Gammel er tom: ${gammeltSimuleringResultat.simuleringMottaker.isEmpty()}. antallAndeler=${andelerEtterDagensDato.size}, resultat=${behandling.resultat}") + return false + } + return true + } + + private fun skalKontrollereOppMotNyUtbetalingsgenerator(): Boolean = + featureToggleService.isEnabled(FeatureToggleConfig.KONTROLLER_NY_UTBETALINGSGENERATOR, false) + + private fun validerAtSimuleringsPerioderGammelHarResultatLikSimuleringsPerioderNyEtterFomTilNy( + simuleringsPerioderGammelTidslinje: Tidslinje, + simuleringsPerioderNyTidslinje: Tidslinje, + behandling: Behandling, + ): DiffFeilType? { + // Tidslinje over simuleringsperioder fra gammel simulering som starter samtidig som simulering fra ny generator + val simuleringsPerioderTidslinjeGammelFraNy = + simuleringsPerioderGammelTidslinje.beskjær( + simuleringsPerioderNyTidslinje.fraOgMed()!!, + simuleringsPerioderGammelTidslinje.tilOgMed()!!, + ) + + // Tidslinjene skal ha samme resultat for alle perioder + val månederMedUliktResultat = + simuleringsPerioderTidslinjeGammelFraNy + .kombinerMed(simuleringsPerioderNyTidslinje) { gammel, ny -> + KombinertSimuleringsResultat( + erLike = gammel?.resultat == (ny?.resultat ?: BigDecimal.ZERO), + gammel = gammel, + ny = ny, + ) + } + .perioder() + .filter { !it.innhold!!.erLike } + + if (månederMedUliktResultat.isNotEmpty()) { + secureLogger.warn("Behandling ${behandling.id} har diff i simuleringsresultat ved bruk av ny utbetalingsgenerator - følgende måneder har ulikt resultat: [${månederMedUliktResultat.joinToString { "${it.fraOgMed} - ${it.tilOgMed}: Gammel ${it.innhold!!.gammel?.resultat} vs Ny ${it.innhold.ny?.resultat}" }}]") + return DiffFeilType.UliktResultatISammePeriode + } + return null + } + + // Tidslinje over simuleringsperioder som kommer før simuleringsperiodene til simulering fra ny generator. + // Fordi vi opphører mer bakover i tiden med den gamle generatoren vil vi kunne få flere simuleringsperioder som kommer før simuleringsperiodene vi får fra ny generator. + // Disse periodene skal ha et resultat som er lik 0, ellers er det noe feil. + private fun validerAtSimuleringsPerioderGammelHarResultatLik0ForPerioderFørSimuleringsPerioderNy( + simuleringsPerioderGammelTidslinje: Tidslinje, + simuleringsPerioderNyTidslinje: Tidslinje, + behandling: Behandling, + ): DiffFeilType? { + val perioderFraGammelFørNyMedResultatUlik0 = simuleringsPerioderGammelTidslinje + .beskjær( + simuleringsPerioderGammelTidslinje.fraOgMed()!!, + simuleringsPerioderNyTidslinje.fraOgMed()!!.tilForrigeMåned(), + ).perioder() + // Bruker compareTo for å ignorere scale. 0 == 0.00 gir false, mens 0.compareTo(0.00) gir 0 som betyr at de er like. + .filter { it.innhold!!.resultat.compareTo(BigDecimal.ZERO) != 0 } + + if (perioderFraGammelFørNyMedResultatUlik0.isNotEmpty()) { + secureLogger.warn("Behandling ${behandling.id} har diff i simuleringsresultat ved bruk av ny utbetalingsgenerator - simuleringsperioder før simuleringsperioder fra gammel generator gir resultat ulik 0. [${perioderFraGammelFørNyMedResultatUlik0.joinToString() { it.toString() }}]") + return DiffFeilType.TidligerePerioderIGammelUlik0 + } + return null + } + + private fun loggSimuleringsPerioderMedDiff( + simuleringsPerioderGammel: List, + simuleringsPerioderNy: List, + ) { + secureLogger.warn("Simuleringsperioder med diff - Gammel: [${simuleringsPerioderGammel.joinToString() { "${it.fom} - ${it.tom}: ${it.resultat}" }}] Ny: [${simuleringsPerioderNy.joinToString() { "${it.fom} - ${it.tom}: ${it.resultat}" }}]") + } + + private fun DetaljertSimuleringResultat.tilSorterteSimuleringsPerioder(behandling: Behandling): List = + vedtakSimuleringMottakereTilSimuleringPerioder( + this.simuleringMottaker.map { + it.tilBehandlingSimuleringMottaker(behandling) + }, + true, + ).sortedBy { it.fom } + + data class KombinertSimuleringsResultat( + val erLike: Boolean, + val gammel: SimuleringsPeriode?, + val ny: SimuleringsPeriode?, + ) +} + +enum class DiffFeilType { + TidligerePerioderIGammelUlik0, + UliktResultatISammePeriode, + DetEneSimuleringsresultatetErTomt, + FeilAntallAndeler, + AndelerMatcherIkke, + UventetFeil, +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringController.kt new file mode 100644 index 000000000..c45afaff9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringController.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.kjerne.simulering + +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.kjerne.simulering.domene.RestSimulering +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/behandlinger") +@ProtectedWithClaims(issuer = "azuread") +class SimuleringController( + private val simuleringService: SimuleringService, + private val tilgangService: TilgangService, + private val featureToggleService: FeatureToggleService, +) { + + @GetMapping(path = ["/{behandlingId}/simulering"]) + fun hentSimulering( + @PathVariable behandlingId: Long, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + val vedtakSimuleringMottaker = simuleringService.oppdaterSimuleringPåBehandlingVedBehov(behandlingId) + val restSimulering = vedtakSimuleringMottakereTilRestSimulering( + økonomiSimuleringMottakere = vedtakSimuleringMottaker, + erManuellPosteringTogglePå = featureToggleService.isEnabled(FeatureToggleConfig.ER_MANUEL_POSTERING_TOGGLE_PÅ), + ) + return ResponseEntity.ok(Ressurs.success(restSimulering)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringService.kt new file mode 100644 index 000000000..9c919b5d9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringService.kt @@ -0,0 +1,273 @@ +package no.nav.familie.ba.sak.kjerne.simulering + +import io.micrometer.core.instrument.Metrics +import jakarta.transaction.Transactional +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.isSameOrBefore +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.integrasjoner.økonomi.AndelTilkjentYtelseForSimuleringFactory +import no.nav.familie.ba.sak.integrasjoner.økonomi.UtbetalingsoppdragGeneratorService +import no.nav.familie.ba.sak.integrasjoner.økonomi.tilRestUtbetalingsoppdrag +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiKlient +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.barn +import no.nav.familie.ba.sak.kjerne.simulering.domene.RestSimulering +import no.nav.familie.ba.sak.kjerne.simulering.domene.SimuleringsPeriode +import no.nav.familie.ba.sak.kjerne.simulering.domene.ØkonomiSimuleringMottaker +import no.nav.familie.ba.sak.kjerne.simulering.domene.ØkonomiSimuleringMottakerRepository +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.kontrakter.felles.simulering.DetaljertSimuleringResultat +import no.nav.familie.kontrakter.felles.simulering.SimuleringMottaker +import no.nav.familie.unleash.UnleashContextFields +import no.nav.familie.unleash.UnleashService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.math.BigDecimal +import java.time.LocalDate + +@Service +class SimuleringService( + private val økonomiKlient: ØkonomiKlient, + private val beregningService: BeregningService, + private val økonomiSimuleringMottakerRepository: ØkonomiSimuleringMottakerRepository, + private val tilgangService: TilgangService, + private val featureToggleService: FeatureToggleService, + private val unleashService: UnleashService, + private val vedtakRepository: VedtakRepository, + private val utbetalingsoppdragGeneratorService: UtbetalingsoppdragGeneratorService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val persongrunnlagService: PersongrunnlagService, + private val kontrollerNyUtbetalingsgeneratorService: KontrollerNyUtbetalingsgeneratorService, +) { + private val simulert = Metrics.counter("familie.ba.sak.oppdrag.simulert") + + fun hentSimuleringFraFamilieOppdrag(vedtak: Vedtak): DetaljertSimuleringResultat? { + if (!beregningService.erEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(vedtak.behandling)) { + return null + } + + val brukNyUtbetalingsoppdragGenerator = unleashService.isEnabled( + FeatureToggleConfig.BRUK_NY_UTBETALINGSGENERATOR, + mapOf(UnleashContextFields.FAGSAK_ID to vedtak.behandling.fagsak.id.toString()), + ) + + /** + * SOAP integrasjonen støtter ikke full epost som MQ, + * så vi bruker bare første 8 tegn av saksbehandlers epost for simulering. + * Denne verdien brukes ikke til noe i simulering. + */ + val utbetalingsoppdrag: Utbetalingsoppdrag = + if (brukNyUtbetalingsoppdragGenerator + ) { + logger.info("Bruker ny utbetalingsgenerator for simulering for behandling ${vedtak.behandling.id}") + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = vedtak, + saksbehandlerId = SikkerhetContext.hentSaksbehandler().take(8), + erSimulering = true, + ).utbetalingsoppdrag.tilRestUtbetalingsoppdrag() + } else { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = vedtak, + saksbehandlerId = SikkerhetContext.hentSaksbehandler().take(8), + andelTilkjentYtelseForUtbetalingsoppdragFactory = AndelTilkjentYtelseForSimuleringFactory(), + erSimulering = true, + ) + } + + // Simulerer ikke mot økonomi når det ikke finnes utbetalingsperioder + if (utbetalingsoppdrag.utbetalingsperiode.isEmpty()) return null + + val detaljertSimuleringResultat = økonomiKlient.hentSimulering(utbetalingsoppdrag) + + if (!brukNyUtbetalingsoppdragGenerator) { + kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = vedtak, + gammeltSimuleringResultat = detaljertSimuleringResultat, + gammeltUtbetalingsoppdrag = utbetalingsoppdrag, + erSimulering = true, + ) + } + + simulert.increment() + return detaljertSimuleringResultat + } + + @Transactional + fun lagreSimuleringPåBehandling( + simuleringMottakere: List, + beahndling: Behandling, + ): List<ØkonomiSimuleringMottaker> { + val vedtakSimuleringMottakere = simuleringMottakere.map { it.tilBehandlingSimuleringMottaker(beahndling) } + return økonomiSimuleringMottakerRepository.saveAll(vedtakSimuleringMottakere) + } + + @Transactional + fun slettSimuleringPåBehandling(behandlingId: Long) = + økonomiSimuleringMottakerRepository.deleteByBehandlingId(behandlingId) + + fun hentSimuleringPåBehandling(behandlingId: Long): List<ØkonomiSimuleringMottaker> { + return økonomiSimuleringMottakerRepository.findByBehandlingId(behandlingId) + } + + fun oppdaterSimuleringPåBehandlingVedBehov(behandlingId: Long): List<ØkonomiSimuleringMottaker> { + val behandling = behandlingHentOgPersisterService.hent(behandlingId = behandlingId) + val behandlingErFerdigBesluttet = + behandling.status == BehandlingStatus.IVERKSETTER_VEDTAK || + behandling.status == BehandlingStatus.AVSLUTTET + + val simulering = hentSimuleringPåBehandling(behandlingId) + val restSimulering = vedtakSimuleringMottakereTilRestSimulering( + økonomiSimuleringMottakere = simulering, + erManuellPosteringTogglePå = featureToggleService.isEnabled(FeatureToggleConfig.ER_MANUEL_POSTERING_TOGGLE_PÅ), + ) + + return if (!behandlingErFerdigBesluttet && simuleringErUtdatert(restSimulering)) { + oppdaterSimuleringPåBehandling(behandling) + } else { + simulering + } + } + + private fun simuleringErUtdatert(simulering: RestSimulering) = + simulering.tidSimuleringHentet == null || + ( + simulering.forfallsdatoNestePeriode != null && + simulering.tidSimuleringHentet < simulering.forfallsdatoNestePeriode && + LocalDate.now() > simulering.forfallsdatoNestePeriode + ) + + @Transactional + fun oppdaterSimuleringPåBehandling(behandling: Behandling): List<ØkonomiSimuleringMottaker> { + val aktivtVedtak = vedtakRepository.findByBehandlingAndAktivOptional(behandling.id) + ?: throw Feil("Fant ikke aktivt vedtak på behandling${behandling.id}") + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "opprette simulering", + ) + + val simulering: List = + hentSimuleringFraFamilieOppdrag(vedtak = aktivtVedtak)?.simuleringMottaker ?: emptyList() + + slettSimuleringPåBehandling(behandling.id) + return lagreSimuleringPåBehandling(simulering, behandling) + } + + fun hentEtterbetaling(behandlingId: Long): BigDecimal { + val vedtakSimuleringMottakere = hentSimuleringPåBehandling(behandlingId) + return hentEtterbetaling(vedtakSimuleringMottakere) + } + + fun hentFeilutbetaling(behandlingId: Long): BigDecimal { + val vedtakSimuleringMottakere = hentSimuleringPåBehandling(behandlingId) + return hentFeilutbetaling(vedtakSimuleringMottakere) + } + + fun hentEtterbetaling(økonomiSimuleringMottakere: List<ØkonomiSimuleringMottaker>): BigDecimal { + return vedtakSimuleringMottakereTilRestSimulering( + økonomiSimuleringMottakere = økonomiSimuleringMottakere, + erManuellPosteringTogglePå = featureToggleService.isEnabled(FeatureToggleConfig.ER_MANUEL_POSTERING_TOGGLE_PÅ), + ).etterbetaling + } + + fun hentFeilutbetaling(økonomiSimuleringMottakere: List<ØkonomiSimuleringMottaker>): BigDecimal { + return vedtakSimuleringMottakereTilRestSimulering( + økonomiSimuleringMottakere, + featureToggleService.isEnabled(FeatureToggleConfig.ER_MANUEL_POSTERING_TOGGLE_PÅ), + ).feilutbetaling + } + + fun harMigreringsbehandlingAvvikInnenforBeløpsgrenser(behandling: Behandling): Boolean { + if (!behandling.erManuellMigrering()) throw Feil("Avvik innenfor beløpsgrenser skal bare sjekkes for manuelle migreringsbehandlinger") + + val antallBarn = persongrunnlagService.hentSøkerOgBarnPåBehandling(behandling.id)?.barn()?.size ?: 0 + + return sjekkOmBehandlingHarEtterbetalingInnenforBeløpsgrenser(behandling, antallBarn) && + sjekkOmBehandlingHarFeilutbetalingInnenforBeløpsgrenser(behandling, antallBarn) + } + + fun harMigreringsbehandlingManuellePosteringer(behandling: Behandling): Boolean { + if (!behandling.erManuellMigrering()) throw Feil("Sjekk for manuelle posteringer skal bare gjøres for manuelle migreringsbehandlinger. Fagsak: ${behandling.fagsak.id} Behandling: ${behandling.id}") + + return filterBortUrelevanteVedtakSimuleringPosteringer(hentSimuleringPåBehandling(behandling.id)) + .flatMap { it.økonomiSimuleringPostering } + .any { it.erManuellPostering } + } + + private fun sjekkOmBehandlingHarEtterbetalingInnenforBeløpsgrenser( + behandling: Behandling, + antallBarn: Int, + ): Boolean { + val finnesEtterBetaling = hentTotalEtterbetalingFørMars2023(behandling.id) != BigDecimal.ZERO + if (!finnesEtterBetaling) return true + + val simuleringsperioderFørMars2023 = hentSimuleringsperioderFørMars2023(behandling.id) + if ( + simuleringsperioderFørMars2023.harKunPositiveResultater() && + simuleringsperioderFørMars2023.harMaks1KroneIResultatPerBarn(antallBarn) && + simuleringsperioderFørMars2023.harTotaltAvvikUnderBeløpsgrense() + ) { + return true + } + + return false + } + + private fun sjekkOmBehandlingHarFeilutbetalingInnenforBeløpsgrenser( + behandling: Behandling, + antallBarn: Int, + ): Boolean { + val finnesFeilutbetaling = hentFeilutbetaling(behandling.id) != BigDecimal.ZERO + if (!finnesFeilutbetaling) return true + + val simuleringsperioderFørMars2023 = hentSimuleringsperioderFørMars2023(behandling.id) + if ( + simuleringsperioderFørMars2023.harKunNegativeResultater() && + simuleringsperioderFørMars2023.harMaks1KroneIResultatPerBarn(antallBarn) && + simuleringsperioderFørMars2023.harTotaltAvvikUnderBeløpsgrense() + ) { + return true + } + + return false + } + + private fun hentSimuleringsperioderFørMars2023(behandlingId: Long): List { + val februar2023 = LocalDate.of(2023, 2, 1) + + return vedtakSimuleringMottakereTilSimuleringPerioder( + økonomiSimuleringMottakere = hentSimuleringPåBehandling(behandlingId), + erManuelPosteringTogglePå = featureToggleService.isEnabled(FeatureToggleConfig.ER_MANUEL_POSTERING_TOGGLE_PÅ), + ).filter { + it.fom.isSameOrBefore(februar2023) + } + } + + private fun hentTotalEtterbetalingFørMars2023(behandlingId: Long) = + hentTotalEtterbetaling(hentSimuleringsperioderFørMars2023(behandlingId), null) + + private fun List.harKunPositiveResultater() = all { it.resultat >= BigDecimal.ZERO } + + private fun List.harKunNegativeResultater() = all { it.resultat <= BigDecimal.ZERO } + + private fun List.harMaks1KroneIResultatPerBarn(antallBarn: Int) = all { + it.resultat.abs() <= BigDecimal(antallBarn) + } + + private fun List.harTotaltAvvikUnderBeløpsgrense() = + sumOf { it.resultat }.abs() < BigDecimal(MANUELL_MIGRERING_BELØPSGRENSE_FOR_TOTALT_AVVIK) + + companion object { + const val MANUELL_MIGRERING_BELØPSGRENSE_FOR_TOTALT_AVVIK = 100 + val logger = LoggerFactory.getLogger(SimuleringService::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringUtil.kt new file mode 100644 index 000000000..f870d42f4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringUtil.kt @@ -0,0 +1,301 @@ +package no.nav.familie.ba.sak.kjerne.simulering + +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.simulering.domene.RestSimulering +import no.nav.familie.ba.sak.kjerne.simulering.domene.SimuleringsPeriode +import no.nav.familie.ba.sak.kjerne.simulering.domene.ØkonomiSimuleringMottaker +import no.nav.familie.ba.sak.kjerne.simulering.domene.ØkonomiSimuleringPostering +import no.nav.familie.kontrakter.felles.simulering.PosteringType +import no.nav.familie.kontrakter.felles.simulering.SimuleringMottaker +import no.nav.familie.kontrakter.felles.simulering.SimulertPostering +import java.math.BigDecimal +import java.time.LocalDate + +fun filterBortUrelevanteVedtakSimuleringPosteringer( + økonomiSimuleringMottakere: List<ØkonomiSimuleringMottaker>, +): List<ØkonomiSimuleringMottaker> = økonomiSimuleringMottakere.map { + it.copy( + økonomiSimuleringPostering = it.økonomiSimuleringPostering.filter { postering -> + postering.posteringType == PosteringType.FEILUTBETALING || + postering.posteringType == PosteringType.YTELSE + }, + ) +} + +fun vedtakSimuleringMottakereTilRestSimulering( + økonomiSimuleringMottakere: List<ØkonomiSimuleringMottaker>, + erManuellPosteringTogglePå: Boolean, +): RestSimulering { + val perioder = + vedtakSimuleringMottakereTilSimuleringPerioder( + økonomiSimuleringMottakere, + erManuellPosteringTogglePå, + ) + val tidSimuleringHentet = økonomiSimuleringMottakere.firstOrNull()?.opprettetTidspunkt?.toLocalDate() + + val framtidigePerioder = + perioder.filter { + it.fom > tidSimuleringHentet || + (it.tom > tidSimuleringHentet && it.forfallsdato > tidSimuleringHentet) + } + + val nestePeriode = framtidigePerioder.filter { it.feilutbetaling == BigDecimal.ZERO }.minByOrNull { it.fom } + val tomSisteUtbetaling = + perioder.filter { nestePeriode == null || it.fom < nestePeriode.fom }.maxOfOrNull { it.tom } + + return RestSimulering( + perioder = perioder, + fomDatoNestePeriode = nestePeriode?.fom, + etterbetaling = hentTotalEtterbetaling(perioder, nestePeriode?.fom), + feilutbetaling = hentTotalFeilutbetaling(perioder, nestePeriode?.fom) + .let { if (it < BigDecimal.ZERO) BigDecimal.ZERO else it }, + fom = perioder.minOfOrNull { it.fom }, + tomDatoNestePeriode = nestePeriode?.tom, + forfallsdatoNestePeriode = nestePeriode?.forfallsdato, + tidSimuleringHentet = tidSimuleringHentet, + tomSisteUtbetaling = tomSisteUtbetaling, + ) +} + +fun vedtakSimuleringMottakereTilSimuleringPerioder( + økonomiSimuleringMottakere: List<ØkonomiSimuleringMottaker>, + erManuelPosteringTogglePå: Boolean, +): List { + if (økonomiSimuleringMottakere.isEmpty()) { + return emptyList() + } + val simuleringPerioder = filterBortUrelevanteVedtakSimuleringPosteringer(økonomiSimuleringMottakere) + .flatMap { it.økonomiSimuleringPostering } + .groupBy { it.fom } + + val tidSimuleringHentet = økonomiSimuleringMottakere.first().opprettetTidspunkt.toLocalDate() + + return simuleringPerioder.map { (fom, posteringListe) -> + + SimuleringsPeriode( + fom = fom, + tom = posteringListe[0].tom, + forfallsdato = posteringListe[0].forfallsdato, + nyttBeløp = if (erManuelPosteringTogglePå) { + hentNyttBeløpIPeriode(posteringListe) + } else { + hentNyttBeløpIPeriodeGammel(posteringListe) + }, + tidligereUtbetalt = if (erManuelPosteringTogglePå) { + hentTidligereUtbetaltIPeriode(posteringListe) + } else { + hentTidligereUtbetaltIPeriodeGammel(posteringListe) + }, + resultat = if (erManuelPosteringTogglePå) { + hentResultatIPeriode(posteringListe) + } else { + hentResultatIPeriodeGammel(posteringListe) + }, + manuellPostering = if (erManuelPosteringTogglePå) { + hentManuellPosteringIPeriode(posteringListe) + } else { + BigDecimal.ZERO + }, + feilutbetaling = hentPositivFeilbetalingIPeriode(posteringListe), + etterbetaling = if (erManuelPosteringTogglePå) { + hentEtterbetalingIPeriode(posteringListe, tidSimuleringHentet) + } else { + hentEtterbetalingIPeriodeGammel(posteringListe, tidSimuleringHentet) + }, + ) + } +} + +@Deprecated("Skal bruke hentNyttBeløpIPeriode når manuelle posteringer er tester ferdig") +fun hentNyttBeløpIPeriodeGammel(periode: List<ØkonomiSimuleringPostering>): BigDecimal { + val sumPositiveYtelser = periode.filter { postering -> + postering.posteringType == PosteringType.YTELSE && postering.beløp > BigDecimal.ZERO + }.sumOf { it.beløp } + val feilutbetaling = hentFeilbetalingIPeriodeGammel(periode) + return if (feilutbetaling > BigDecimal.ZERO) sumPositiveYtelser - feilutbetaling else sumPositiveYtelser +} + +fun hentNyttBeløpIPeriode(periode: List<ØkonomiSimuleringPostering>): BigDecimal { + val sumPositiveYtelser = periode + .filter { it.posteringType == PosteringType.YTELSE } + .filter { it.beløp > BigDecimal.ZERO } + .filter { !it.erManuellPostering } + .sumOf { it.beløp } + val feilutbetaling = hentFeilutbetalingIPeriode(periode, false) + + return if (feilutbetaling > BigDecimal.ZERO) { + sumPositiveYtelser - feilutbetaling + } else { + sumPositiveYtelser + } +} + +fun hentPositivFeilbetalingIPeriode(periode: List<ØkonomiSimuleringPostering>) = + periode.filter { postering -> + postering.posteringType == PosteringType.FEILUTBETALING && + postering.beløp > BigDecimal.ZERO + }.sumOf { it.beløp } + +fun hentNegativFeilutbetalingIPeriode(periode: List<ØkonomiSimuleringPostering>) = + periode.filter { postering -> + postering.posteringType == PosteringType.FEILUTBETALING && + postering.beløp < BigDecimal.ZERO + }.sumOf { it.beløp } + +@Deprecated("Skal bruke hentFeilutbetalingIPeriode når manuelle posteringer er tester ferdig") +fun hentFeilbetalingIPeriodeGammel(periode: List<ØkonomiSimuleringPostering>) = + periode.filter { postering -> + postering.posteringType == PosteringType.FEILUTBETALING && + !postering.erManuellPostering + }.sumOf { it.beløp } + +fun hentFeilutbetalingIPeriode(periode: List<ØkonomiSimuleringPostering>, inkluderManuellePosteringer: Boolean) = + periode + .filter { it.posteringType == PosteringType.FEILUTBETALING } + .filter { inkluderManuellePosteringer || !it.erManuellPostering } + .sumOf { it.beløp } + +@Deprecated("Skal bruke hentTidligereUtbetaltIPeriode når manuelle posteringer er tester ferdig") +fun hentTidligereUtbetaltIPeriodeGammel(periode: List<ØkonomiSimuleringPostering>): BigDecimal { + val sumNegativeYtelser = periode.filter { postering -> + (postering.posteringType == PosteringType.YTELSE && postering.beløp < BigDecimal.ZERO) + }.sumOf { it.beløp } + val feilutbetaling = hentFeilbetalingIPeriodeGammel(periode) + return if (feilutbetaling < BigDecimal.ZERO) -(sumNegativeYtelser - feilutbetaling) else -sumNegativeYtelser +} + +fun hentTidligereUtbetaltIPeriode(periode: List<ØkonomiSimuleringPostering>): BigDecimal { + val sumNegativeYtelser = periode + .filter { it.posteringType == PosteringType.YTELSE } + .filter { !it.erManuellPostering } + .filter { it.beløp < BigDecimal.ZERO } + .sumOf { it.beløp } + + val feilutbetaling = hentFeilutbetalingIPeriode(periode, false) + + // Manuelle posteringer brukes for å justere hva som faktisk skal bli betalt ut i en periode. + // Endrer fortegn da en negativ sum skal øke tidligere utbetalt og en positiv sum redusere tidligere utbetalt. + val sumManuellePosteringer = -hentManuellPosteringIPeriode(periode) + + return if (feilutbetaling < BigDecimal.ZERO) { + -(sumNegativeYtelser - feilutbetaling) + } else { + -sumNegativeYtelser + sumManuellePosteringer + } +} + +fun hentManuellPosteringIPeriode(periode: List<ØkonomiSimuleringPostering>): BigDecimal { + val sumManuellePosteringer = periode + .filter { it.posteringType == PosteringType.YTELSE } + .filter { it.erManuellPostering } + .sumOf { it.beløp } + + val manuellFeilutbetaling = hentManuellFeilutbetalingIPeriode(periode) + + return sumManuellePosteringer - manuellFeilutbetaling +} + +private fun hentManuellFeilutbetalingIPeriode(periode: List<ØkonomiSimuleringPostering>) = + periode + .filter { it.posteringType == PosteringType.FEILUTBETALING } + .filter { it.erManuellPostering } + .sumOf { it.beløp } + +@Deprecated("Skal bruke hentResultatIPeriode når manuelle posteringer er tester ferdig") +fun hentResultatIPeriodeGammel(periode: List<ØkonomiSimuleringPostering>): BigDecimal { + val feilutbetaling = hentFeilbetalingIPeriodeGammel(periode) + + return if (feilutbetaling > BigDecimal.ZERO) { + -feilutbetaling + } else { + hentNyttBeløpIPeriode(periode) - hentTidligereUtbetaltIPeriodeGammel(periode) + } +} + +fun hentResultatIPeriode(periode: List<ØkonomiSimuleringPostering>): BigDecimal { + val feilutbetaling = hentFeilutbetalingIPeriode(periode, true) + + return if (feilutbetaling > BigDecimal.ZERO) { + -feilutbetaling + } else { + hentNyttBeløpIPeriode(periode) - + hentTidligereUtbetaltIPeriode(periode) + } +} + +@Deprecated("Skal bruke hentEtterbetalingIPeriode når manuelle posteringer er testet ferdig") +fun hentEtterbetalingIPeriodeGammel( + periode: List<ØkonomiSimuleringPostering>, + tidSimuleringHentet: LocalDate, +): BigDecimal { + val periodeHarPositivFeilutbetaling = + periode.any { it.posteringType == PosteringType.FEILUTBETALING && it.beløp > BigDecimal.ZERO } + val sumYtelser = + periode.filter { it.posteringType == PosteringType.YTELSE && it.forfallsdato <= tidSimuleringHentet } + .sumOf { it.beløp } + return when { + periodeHarPositivFeilutbetaling -> BigDecimal.ZERO + else -> maxOf(BigDecimal.ZERO, sumYtelser) + } +} + +fun hentEtterbetalingIPeriode( + periode: List<ØkonomiSimuleringPostering>, + tidSimuleringHentet: LocalDate, +): BigDecimal { + val periodeMedForfallFørTidSimuleringHentet = periode.filter { it.forfallsdato <= tidSimuleringHentet } + val periodeHarPositivFeilutbetaling = + hentFeilutbetalingIPeriode(periodeMedForfallFørTidSimuleringHentet, true) > BigDecimal.ZERO + val resultat = hentResultatIPeriode(periodeMedForfallFørTidSimuleringHentet) + + return when { + periodeHarPositivFeilutbetaling -> BigDecimal.ZERO + else -> maxOf( + BigDecimal.ZERO, + // Vi justerer etterbetalingsbeløp med negativ feilutbetaling i periode (redusert feilutbetaling). + // Negative feilutbetalinger oppstår når man øker ytelsen i en periode det er registrert feilutbetaling på tidligere og tilbakekrevingsbehandlingen ikke er avsluttet. + // Ved overførig til Oppdrag/økonomi vil registrert feilutbetaling bli redusert. + // https://confluence.adeo.no/display/TFA/Tolkning+av+simulerte+posteringer+fra+oppdragsystemet + (resultat + hentNegativFeilutbetalingIPeriode(periodeMedForfallFørTidSimuleringHentet)), + ) + } +} + +fun hentTotalEtterbetaling(simuleringPerioder: List, fomDatoNestePeriode: LocalDate?): BigDecimal { + return simuleringPerioder.filter { + (fomDatoNestePeriode == null || it.fom < fomDatoNestePeriode) + }.sumOf { it.etterbetaling }.takeIf { it > BigDecimal.ZERO } ?: BigDecimal.ZERO +} + +fun hentTotalFeilutbetaling(simuleringPerioder: List, fomDatoNestePeriode: LocalDate?): BigDecimal { + return simuleringPerioder + .filter { fomDatoNestePeriode == null || it.fom < fomDatoNestePeriode } + .sumOf { it.feilutbetaling } +} + +fun SimuleringMottaker.tilBehandlingSimuleringMottaker(behandling: Behandling): ØkonomiSimuleringMottaker { + val behandlingSimuleringMottaker = ØkonomiSimuleringMottaker( + mottakerNummer = this.mottakerNummer, + mottakerType = this.mottakerType, + behandling = behandling, + ) + + behandlingSimuleringMottaker.økonomiSimuleringPostering = this.simulertPostering.map { + it.tilVedtakSimuleringPostering(behandlingSimuleringMottaker) + } + + return behandlingSimuleringMottaker +} + +fun SimulertPostering.tilVedtakSimuleringPostering(økonomiSimuleringMottaker: ØkonomiSimuleringMottaker) = + ØkonomiSimuleringPostering( + beløp = this.beløp, + betalingType = this.betalingType, + fagOmrådeKode = this.fagOmrådeKode, + fom = this.fom, + tom = this.tom, + posteringType = this.posteringType, + forfallsdato = this.forfallsdato, + utenInntrekk = this.utenInntrekk, + økonomiSimuleringMottaker = økonomiSimuleringMottaker, + ) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/RestSimulering.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/RestSimulering.kt new file mode 100644 index 000000000..b3af563c1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/RestSimulering.kt @@ -0,0 +1,28 @@ +package no.nav.familie.ba.sak.kjerne.simulering.domene + +import java.math.BigDecimal +import java.time.LocalDate + +data class RestSimulering( + val perioder: List, + val fomDatoNestePeriode: LocalDate?, + val etterbetaling: BigDecimal, + val feilutbetaling: BigDecimal, + val fom: LocalDate?, + val tomDatoNestePeriode: LocalDate?, + val forfallsdatoNestePeriode: LocalDate?, + val tidSimuleringHentet: LocalDate?, + val tomSisteUtbetaling: LocalDate?, +) + +data class SimuleringsPeriode( + val fom: LocalDate, + val tom: LocalDate, + val forfallsdato: LocalDate, + val nyttBeløp: BigDecimal, + val tidligereUtbetalt: BigDecimal, + val manuellPostering: BigDecimal, + val resultat: BigDecimal, + val feilutbetaling: BigDecimal, + val etterbetaling: BigDecimal, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/SimuleringPeriodeTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/SimuleringPeriodeTidslinje.kt new file mode 100644 index 000000000..fbc575787 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/SimuleringPeriodeTidslinje.kt @@ -0,0 +1,21 @@ +package no.nav.familie.ba.sak.kjerne.simulering.domene + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt + +class SimuleringPeriodeTidslinje( + val simuleringsPerioder: Collection, +) : Tidslinje() { + override fun lagPerioder(): Collection> = + simuleringsPerioder.map { + Periode( + fraOgMed = it.fom.tilMånedTidspunkt(), + tilOgMed = it.tom.tilMånedTidspunkt(), + innhold = it, + ) + } +} + +fun List.tilTidslinje(): Tidslinje = SimuleringPeriodeTidslinje(this) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/\303\230konomiSimuleringMottaker.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/\303\230konomiSimuleringMottaker.kt" new file mode 100644 index 000000000..8ce182da3 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/\303\230konomiSimuleringMottaker.kt" @@ -0,0 +1,73 @@ +package no.nav.familie.ba.sak.kjerne.simulering.domene + +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.simulering.MottakerType + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "OkonomiSimuleringMottaker") +@Table(name = "OKONOMI_SIMULERING_MOTTAKER") +data class ØkonomiSimuleringMottaker( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "okonomi_simulering_mottaker_seq_generator") + @SequenceGenerator( + name = "okonomi_simulering_mottaker_seq_generator", + sequenceName = "okonomi_simulering_mottaker_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "mottaker_nummer", nullable = false) + val mottakerNummer: String?, + + @Enumerated(EnumType.STRING) + @Column(name = "mottaker_type", nullable = false) + val mottakerType: MottakerType, + + @ManyToOne(optional = false) + @JoinColumn(name = "fk_behandling_id", nullable = false, updatable = false) + val behandling: Behandling, + + @OneToMany( + mappedBy = "økonomiSimuleringMottaker", + cascade = [CascadeType.ALL], + fetch = FetchType.EAGER, + orphanRemoval = true, + ) + var økonomiSimuleringPostering: List<ØkonomiSimuleringPostering> = emptyList(), +) : BaseEntitet() { + + override fun hashCode() = id.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other !is ØkonomiSimuleringMottaker) return false + + return (id == other.id) + } + + override fun toString(): String { + return "BrSimuleringMottaker(" + + "id=$id, " + + "mottakerType=$mottakerType, " + + "behandling=$behandling, " + + "økonomiSimuleringPostering=$økonomiSimuleringPostering" + + ")" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/\303\230konomiSimuleringMottakerRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/\303\230konomiSimuleringMottakerRepository.kt" new file mode 100644 index 000000000..014481af4 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/\303\230konomiSimuleringMottakerRepository.kt" @@ -0,0 +1,17 @@ +package no.nav.familie.ba.sak.kjerne.simulering.domene + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query +import org.springframework.transaction.annotation.Transactional + +interface ØkonomiSimuleringMottakerRepository : JpaRepository<ØkonomiSimuleringMottaker, Long> { + + @Query(value = "SELECT sm FROM OkonomiSimuleringMottaker sm JOIN sm.behandling b WHERE b.id = :behandlingId") + fun findByBehandlingId(behandlingId: Long): List<ØkonomiSimuleringMottaker> + + @Transactional + @Modifying + @Query(value = "DELETE FROM OkonomiSimuleringMottaker sm where sm.behandling.id = :behandlingId") + fun deleteByBehandlingId(behandlingId: Long) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/\303\230konomiSimuleringPostering.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/\303\230konomiSimuleringPostering.kt" new file mode 100644 index 000000000..3373e8b30 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/simulering/domene/\303\230konomiSimuleringPostering.kt" @@ -0,0 +1,102 @@ +package no.nav.familie.ba.sak.kjerne.simulering.domene + +import com.fasterxml.jackson.annotation.JsonIdentityInfo +import com.fasterxml.jackson.annotation.JsonIdentityReference +import com.fasterxml.jackson.annotation.ObjectIdGenerators +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.simulering.BetalingType +import no.nav.familie.kontrakter.felles.simulering.FagOmrådeKode +import no.nav.familie.kontrakter.felles.simulering.PosteringType +import java.math.BigDecimal +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "OkonomiSimuleringPostering") +@Table(name = "OKONOMI_SIMULERING_POSTERING") +data class ØkonomiSimuleringPostering( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "okonomi_simulering_postering_seq_generator") + @SequenceGenerator( + name = "okonomi_simulering_postering_seq_generator", + sequenceName = "okonomi_simulering_postering_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "fk_okonomi_simulering_mottaker_id", nullable = false, updatable = false) + @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id") + @JsonIdentityReference(alwaysAsId = true) + val økonomiSimuleringMottaker: ØkonomiSimuleringMottaker, + + @Enumerated(EnumType.STRING) + @Column(name = "fag_omraade_kode", nullable = false) + val fagOmrådeKode: FagOmrådeKode, + + @Column(name = "fom", updatable = false, nullable = false) + val fom: LocalDate, + + @Column(name = "tom", updatable = false, nullable = false) + val tom: LocalDate, + + @Enumerated(EnumType.STRING) + @Column(name = "betaling_type", nullable = false) + val betalingType: BetalingType, + + @Column(name = "belop", nullable = false) + val beløp: BigDecimal, + + @Enumerated(EnumType.STRING) + @Column(name = "postering_type", nullable = false) + val posteringType: PosteringType, + + @Column(name = "forfallsdato", updatable = false, nullable = false) + val forfallsdato: LocalDate, + + @Column(name = "uten_inntrekk", nullable = false) + val utenInntrekk: Boolean, +) : BaseEntitet() { + + override fun hashCode() = id.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other !is ØkonomiSimuleringPostering) return false + + return (id == other.id) + } + + override fun toString(): String { + return "BrSimuleringPostering(" + + "id=$id, " + + "økonomiSimuleringMottaker=${økonomiSimuleringMottaker.id}, " + + "fagOmrådeKode=$fagOmrådeKode, " + + "fom=$fom, " + + "tom=$tom, " + + "betalingType=$betalingType, " + + "beløp=$beløp, " + + "posteringType=$posteringType, " + + "forfallsdato=$forfallsdato, " + + "utenInntrekk=$utenInntrekk" + + ")" + } + + val erManuellPostering: Boolean + get() { + return this.fagOmrådeKode == FagOmrådeKode.BARNETRYGD_INFOTRYGD_MANUELT || this.fagOmrådeKode == FagOmrådeKode.BARNETRYGD_MANUELT + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/sm\303\245barnstilleggkorrigering/Sm\303\245barnstilleggKorrigeringController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/sm\303\245barnstilleggkorrigering/Sm\303\245barnstilleggKorrigeringController.kt" new file mode 100644 index 000000000..873f43d73 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/sm\303\245barnstilleggkorrigering/Sm\303\245barnstilleggKorrigeringController.kt" @@ -0,0 +1,76 @@ +package no.nav.familie.ba.sak.kjerne.småbarnstilleggkorrigering + +import io.swagger.v3.oas.annotations.media.Schema +import no.nav.familie.ba.sak.common.BehandlingValidering.validerBehandlingKanRedigeres +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.YearMonth + +@RestController +@RequestMapping("/api/småbarnstilleggkorrigering") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class SmåbarnstilleggController( + private val tilgangService: TilgangService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val småbarnstilleggKorrigeringService: SmåbarnstilleggKorrigeringService, + private val utvidetBehandlingService: UtvidetBehandlingService, +) { + @PostMapping(path = ["/behandling/{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun leggTilSmåBarnstilleggPåBehandling( + @PathVariable behandlingId: Long, + @RequestBody småbarnstilleggKorrigeringRequest: SmåbarnstilleggKorrigeringRequest, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Legger til småbarnstillegg", + ) + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + validerBehandlingKanRedigeres(behandling) + + småbarnstilleggKorrigeringService.leggTilSmåbarnstilleggPåBehandling(småbarnstilleggKorrigeringRequest.årMåned, behandling) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId))) + } + + @DeleteMapping(path = ["/behandling/{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun fjernSmåbarnstilleggFraMåned( + @PathVariable behandlingId: Long, + @RequestBody småBarnstilleggKorrigeringRequest: SmåbarnstilleggKorrigeringRequest, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.DELETE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Fjerner småbarnstillegg", + ) + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + validerBehandlingKanRedigeres(behandling) + + småbarnstilleggKorrigeringService.fjernSmåbarnstilleggPåBehandling(småBarnstilleggKorrigeringRequest.årMåned, behandling) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId))) + } +} + +data class SmåbarnstilleggKorrigeringRequest( + @Schema( + implementation = String::class, + example = "2020-12", + ) val årMåned: YearMonth, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/sm\303\245barnstilleggkorrigering/Sm\303\245barnstilleggKorrigeringService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/sm\303\245barnstilleggkorrigering/Sm\303\245barnstilleggKorrigeringService.kt" new file mode 100644 index 000000000..e03c918ac --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/sm\303\245barnstilleggkorrigering/Sm\303\245barnstilleggKorrigeringService.kt" @@ -0,0 +1,91 @@ +package no.nav.familie.ba.sak.kjerne.småbarnstilleggkorrigering + +import jakarta.transaction.Transactional +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.opprettBooleanTidslinje +import no.nav.familie.ba.sak.common.tilMånedÅr +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.AndelTilkjentYtelseForTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.beregning.oppdaterTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.satstypeTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.tilAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.tilTryggTidslinjeForSøkersYtelse +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.harIkkeOverlappMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.harOverlappMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNullMed +import org.springframework.stereotype.Service +import java.math.BigDecimal +import java.time.YearMonth + +@Service +class SmåbarnstilleggKorrigeringService( + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + private val loggService: LoggService, +) { + @Transactional + fun leggTilSmåbarnstilleggPåBehandling(årMåned: YearMonth, behandling: Behandling): List { + val tilkjentYtelse = tilkjentYtelseRepository.findByBehandling(behandlingId = behandling.id) + val andelTilkjentYtelser = tilkjentYtelse.andelerTilkjentYtelse + + val småbarnstilleggTidslinje = andelTilkjentYtelser.tilTryggTidslinjeForSøkersYtelse(YtelseType.SMÅBARNSTILLEGG) + val skalOpprettesTidslinje = opprettBooleanTidslinje(årMåned, årMåned) + + if (småbarnstilleggTidslinje.harOverlappMed(skalOpprettesTidslinje)) { + throw FunksjonellFeil("Det er ikke mulig å legge til småbarnstillegg for ${årMåned.tilMånedÅr()} fordi det allerede finnes småbarnstillegg for denne perioden") + } + + val nyeSmåbarnstillegg = skalOpprettesTidslinje + .kombinerUtenNullMed(satstypeTidslinje(SatsType.SMA)) { _, sats -> + AndelTilkjentYtelseForTidslinje( + aktør = behandling.fagsak.aktør, + ytelseType = YtelseType.SMÅBARNSTILLEGG, + prosent = BigDecimal(100), + sats = sats, + beløp = sats, + ) + }.tilAndelerTilkjentYtelse(tilkjentYtelse) + + andelTilkjentYtelser.addAll(nyeSmåbarnstillegg) + + loggService.opprettSmåbarnstilleggLogg(behandling, "Småbarnstillegg for ${årMåned.tilMånedÅr()} lagt til") + + return nyeSmåbarnstillegg + } + + @Transactional + fun fjernSmåbarnstilleggPåBehandling(årMåned: YearMonth, behandling: Behandling): List { + val tilkjentYtelse = tilkjentYtelseRepository.findByBehandling(behandlingId = behandling.id) + + val småbarnstilleggTidslinje = tilkjentYtelse.andelerTilkjentYtelse + .tilTryggTidslinjeForSøkersYtelse(YtelseType.SMÅBARNSTILLEGG) + val skalFjernesTidslinje = opprettBooleanTidslinje(årMåned, årMåned) + + if (småbarnstilleggTidslinje.harIkkeOverlappMed(skalFjernesTidslinje)) { + throw FunksjonellFeil("Det er ikke mulig å fjerne småbarnstillegg for ${årMåned.tilMånedÅr()} fordi det ikke finnes småbarnstillegg for denne perioden") + } + + val nyeSmåbarnstilleggAndeler = + småbarnstilleggTidslinje.kombinerMed(skalFjernesTidslinje) { andel, skalFjernes -> + when (skalFjernes) { + true -> null + else -> andel + } + }.tilAndelerTilkjentYtelse(tilkjentYtelse) + + val andelerTilkjentYtelserUtenomSmåbarnstillegg = tilkjentYtelse.andelerTilkjentYtelse + .filter { it.type != YtelseType.SMÅBARNSTILLEGG } + + val oppdaterteAndeler = andelerTilkjentYtelserUtenomSmåbarnstillegg + nyeSmåbarnstilleggAndeler + tilkjentYtelseRepository.oppdaterTilkjentYtelse(tilkjentYtelse, oppdaterteAndeler) + + loggService.opprettSmåbarnstilleggLogg(behandling, "Småbarnstillegg for ${årMåned.tilMånedÅr()} fjernet") + + return nyeSmåbarnstilleggAndeler + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/BehandlingSteg.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/BehandlingSteg.kt new file mode 100644 index 000000000..e9a6ebff9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/BehandlingSteg.kt @@ -0,0 +1,403 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.steg.StegType.BEHANDLINGSRESULTAT +import no.nav.familie.ba.sak.kjerne.steg.StegType.BEHANDLING_AVSLUTTET +import no.nav.familie.ba.sak.kjerne.steg.StegType.BESLUTTE_VEDTAK +import no.nav.familie.ba.sak.kjerne.steg.StegType.DISTRIBUER_VEDTAKSBREV +import no.nav.familie.ba.sak.kjerne.steg.StegType.FERDIGSTILLE_BEHANDLING +import no.nav.familie.ba.sak.kjerne.steg.StegType.FILTRERING_FØDSELSHENDELSER +import no.nav.familie.ba.sak.kjerne.steg.StegType.HENLEGG_BEHANDLING +import no.nav.familie.ba.sak.kjerne.steg.StegType.IVERKSETT_MOT_FAMILIE_TILBAKE +import no.nav.familie.ba.sak.kjerne.steg.StegType.IVERKSETT_MOT_OPPDRAG +import no.nav.familie.ba.sak.kjerne.steg.StegType.JOURNALFØR_VEDTAKSBREV +import no.nav.familie.ba.sak.kjerne.steg.StegType.REGISTRERE_INSTITUSJON_OG_VERGE +import no.nav.familie.ba.sak.kjerne.steg.StegType.REGISTRERE_PERSONGRUNNLAG +import no.nav.familie.ba.sak.kjerne.steg.StegType.REGISTRERE_SØKNAD +import no.nav.familie.ba.sak.kjerne.steg.StegType.SEND_TIL_BESLUTTER +import no.nav.familie.ba.sak.kjerne.steg.StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI +import no.nav.familie.ba.sak.kjerne.steg.StegType.VILKÅRSVURDERING +import no.nav.familie.ba.sak.kjerne.steg.StegType.VURDER_TILBAKEKREVING + +interface BehandlingSteg { + + fun utførStegOgAngiNeste( + behandling: Behandling, + data: T, + ): StegType + + fun stegType(): StegType + + fun hentNesteStegForNormalFlyt(behandling: Behandling): StegType { + return hentNesteSteg( + utførendeStegType = this.stegType(), + behandling = behandling, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.IKKE_RELEVANT, + ) + } + + fun hentNesteStegGittEndringerIUtbetaling( + behandling: Behandling, + endringerIUtbetaling: EndringerIUtbetalingForBehandlingSteg, + ): StegType { + return hentNesteSteg( + utførendeStegType = this.stegType(), + behandling = behandling, + endringerIUtbetaling = endringerIUtbetaling, + ) + } + + fun preValiderSteg(behandling: Behandling, stegService: StegService? = null) {} + + fun postValiderSteg(behandling: Behandling) {} +} + +enum class EndringerIUtbetalingForBehandlingSteg { + IKKE_RELEVANT, + INGEN_ENDRING_I_UTBETALING, + ENDRING_I_UTBETALING, +} + +val FØRSTE_STEG = REGISTRERE_PERSONGRUNNLAG +val SISTE_STEG = BEHANDLING_AVSLUTTET + +enum class StegType( + val rekkefølge: Int, + val tillattFor: List, + private val gyldigIKombinasjonMedStatus: List, +) { + + // Henlegg søknad går utenfor den normale stegflyten og går direkte til ferdigstilt. + // Denne typen av steg skal bli endret til å bli av type aksjonspunkt isteden for steg. + HENLEGG_BEHANDLING( + rekkefølge = 0, + tillattFor = listOf(BehandlerRolle.SYSTEM, BehandlerRolle.SAKSBEHANDLER), + gyldigIKombinasjonMedStatus = listOf( + BehandlingStatus.UTREDES, + BehandlingStatus.IVERKSETTER_VEDTAK, + ), + ), + REGISTRERE_INSTITUSJON_OG_VERGE( + rekkefølge = 1, + tillattFor = listOf(BehandlerRolle.SYSTEM, BehandlerRolle.SAKSBEHANDLER), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.UTREDES), + ), + REGISTRERE_PERSONGRUNNLAG( + rekkefølge = 1, + tillattFor = listOf(BehandlerRolle.SYSTEM, BehandlerRolle.SAKSBEHANDLER), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.UTREDES), + ), + REGISTRERE_SØKNAD( + rekkefølge = 1, + tillattFor = listOf(BehandlerRolle.SYSTEM, BehandlerRolle.SAKSBEHANDLER), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.UTREDES), + ), + FILTRERING_FØDSELSHENDELSER( + rekkefølge = 2, + tillattFor = listOf(BehandlerRolle.SYSTEM), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.UTREDES), + ), + VILKÅRSVURDERING( + rekkefølge = 3, + tillattFor = listOf(BehandlerRolle.SYSTEM, BehandlerRolle.SAKSBEHANDLER), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.UTREDES), + ), + BEHANDLINGSRESULTAT( + rekkefølge = 4, + tillattFor = listOf(BehandlerRolle.SYSTEM, BehandlerRolle.SAKSBEHANDLER), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.UTREDES), + ), + VURDER_TILBAKEKREVING( + rekkefølge = 5, + tillattFor = listOf(BehandlerRolle.SYSTEM, BehandlerRolle.SAKSBEHANDLER), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.UTREDES), + ), + SEND_TIL_BESLUTTER( + rekkefølge = 6, + tillattFor = listOf(BehandlerRolle.SYSTEM, BehandlerRolle.SAKSBEHANDLER), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.UTREDES), + ), + BESLUTTE_VEDTAK( + rekkefølge = 7, + tillattFor = listOf(BehandlerRolle.SYSTEM, BehandlerRolle.BESLUTTER), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.FATTER_VEDTAK), + ), + IVERKSETT_MOT_OPPDRAG( + rekkefølge = 8, + tillattFor = listOf(BehandlerRolle.SYSTEM), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.IVERKSETTER_VEDTAK), + ), + VENTE_PÅ_STATUS_FRA_ØKONOMI( + rekkefølge = 9, + tillattFor = listOf(BehandlerRolle.SYSTEM), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.IVERKSETTER_VEDTAK), + ), + IVERKSETT_MOT_FAMILIE_TILBAKE( + rekkefølge = 10, + tillattFor = listOf(BehandlerRolle.SYSTEM), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.IVERKSETTER_VEDTAK), + ), + JOURNALFØR_VEDTAKSBREV( + rekkefølge = 11, + tillattFor = listOf(BehandlerRolle.SYSTEM), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.IVERKSETTER_VEDTAK), + ), + DISTRIBUER_VEDTAKSBREV( + rekkefølge = 12, + tillattFor = listOf(BehandlerRolle.SYSTEM), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.IVERKSETTER_VEDTAK), + ), + FERDIGSTILLE_BEHANDLING( + rekkefølge = 13, + tillattFor = listOf(BehandlerRolle.SYSTEM, BehandlerRolle.SAKSBEHANDLER), + gyldigIKombinasjonMedStatus = listOf( + BehandlingStatus.IVERKSETTER_VEDTAK, + BehandlingStatus.UTREDES, + BehandlingStatus.FATTER_VEDTAK, + ), + ), + BEHANDLING_AVSLUTTET( + rekkefølge = 14, + tillattFor = emptyList(), + gyldigIKombinasjonMedStatus = listOf(BehandlingStatus.AVSLUTTET, BehandlingStatus.UTREDES), + ), + ; + + fun displayName(): String { + return this.name.replace('_', ' ').lowercase().replaceFirstChar { it.uppercase() } + } + + fun kommerEtter(steg: StegType): Boolean { + return this.rekkefølge > steg.rekkefølge + } + + fun erGyldigIKombinasjonMedStatus(behandlingStatus: BehandlingStatus): Boolean { + return this.gyldigIKombinasjonMedStatus.contains(behandlingStatus) + } + + fun erSaksbehandlerSteg(): Boolean { + return this.tillattFor.any { it == BehandlerRolle.SAKSBEHANDLER || it == BehandlerRolle.BESLUTTER } + } +} + +fun hentNesteSteg( + behandling: Behandling, + utførendeStegType: StegType, + endringerIUtbetaling: EndringerIUtbetalingForBehandlingSteg = EndringerIUtbetalingForBehandlingSteg.IKKE_RELEVANT, +): StegType { + if (utførendeStegType == HENLEGG_BEHANDLING) { + return FERDIGSTILLE_BEHANDLING + } + + val behandlingType = behandling.type + val behandlingÅrsak = behandling.opprettetÅrsak + + if (behandlingÅrsak.erOmregningsårsak()) { + return when (utførendeStegType) { + REGISTRERE_PERSONGRUNNLAG -> VILKÅRSVURDERING + VILKÅRSVURDERING -> BEHANDLINGSRESULTAT + BEHANDLINGSRESULTAT -> JOURNALFØR_VEDTAKSBREV + JOURNALFØR_VEDTAKSBREV -> DISTRIBUER_VEDTAKSBREV + DISTRIBUER_VEDTAKSBREV -> FERDIGSTILLE_BEHANDLING + FERDIGSTILLE_BEHANDLING -> BEHANDLING_AVSLUTTET + BEHANDLING_AVSLUTTET -> BEHANDLING_AVSLUTTET + else -> throw IllegalStateException("Stegtype ${utførendeStegType.displayName()} er ikke implementert for behandling med årsak $behandlingÅrsak og type $behandlingType.") + } + } + + return when (behandlingÅrsak) { + BehandlingÅrsak.TEKNISK_OPPHØR -> throw Feil("Teknisk opphør er ikke mulig å behandle lenger") + BehandlingÅrsak.MIGRERING -> throw Feil("Maskinell migrering er ikke mulig å behandle lenger") + + BehandlingÅrsak.HELMANUELL_MIGRERING -> { + when (utførendeStegType) { + REGISTRERE_PERSONGRUNNLAG -> VILKÅRSVURDERING + VILKÅRSVURDERING -> BEHANDLINGSRESULTAT + BEHANDLINGSRESULTAT -> VURDER_TILBAKEKREVING + VURDER_TILBAKEKREVING -> SEND_TIL_BESLUTTER + SEND_TIL_BESLUTTER -> BESLUTTE_VEDTAK + BESLUTTE_VEDTAK -> IVERKSETT_MOT_OPPDRAG + IVERKSETT_MOT_OPPDRAG -> VENTE_PÅ_STATUS_FRA_ØKONOMI + VENTE_PÅ_STATUS_FRA_ØKONOMI -> FERDIGSTILLE_BEHANDLING + FERDIGSTILLE_BEHANDLING -> BEHANDLING_AVSLUTTET + BEHANDLING_AVSLUTTET -> BEHANDLING_AVSLUTTET + else -> throw IllegalStateException( + "StegType ${utførendeStegType.displayName()} " + + "er ugyldig ved manuell migreringsbehandling", + ) + } + } + + BehandlingÅrsak.ENDRE_MIGRERINGSDATO -> { + when (utførendeStegType) { + REGISTRERE_PERSONGRUNNLAG -> VILKÅRSVURDERING + VILKÅRSVURDERING -> BEHANDLINGSRESULTAT + BEHANDLINGSRESULTAT -> VURDER_TILBAKEKREVING + VURDER_TILBAKEKREVING -> SEND_TIL_BESLUTTER + SEND_TIL_BESLUTTER -> BESLUTTE_VEDTAK + BESLUTTE_VEDTAK -> FERDIGSTILLE_BEHANDLING + FERDIGSTILLE_BEHANDLING -> BEHANDLING_AVSLUTTET + BEHANDLING_AVSLUTTET -> BEHANDLING_AVSLUTTET + else -> throw IllegalStateException( + "StegType ${utførendeStegType.displayName()} " + + "er ugyldig ved migreringsbehandling med endre migreringsdato", + ) + } + } + + BehandlingÅrsak.TEKNISK_ENDRING -> { + when (utførendeStegType) { + REGISTRERE_PERSONGRUNNLAG -> VILKÅRSVURDERING + VILKÅRSVURDERING -> BEHANDLINGSRESULTAT + BEHANDLINGSRESULTAT -> VURDER_TILBAKEKREVING + VURDER_TILBAKEKREVING -> SEND_TIL_BESLUTTER + SEND_TIL_BESLUTTER -> BESLUTTE_VEDTAK + BESLUTTE_VEDTAK -> hentStegEtterBeslutteVedtakForTekniskEndring(endringerIUtbetaling) + IVERKSETT_MOT_OPPDRAG -> VENTE_PÅ_STATUS_FRA_ØKONOMI + VENTE_PÅ_STATUS_FRA_ØKONOMI -> FERDIGSTILLE_BEHANDLING + FERDIGSTILLE_BEHANDLING -> BEHANDLING_AVSLUTTET + BEHANDLING_AVSLUTTET -> BEHANDLING_AVSLUTTET + else -> throw IllegalStateException("StegType ${utførendeStegType.displayName()} ugyldig ved teknisk endring") + } + } + + BehandlingÅrsak.FØDSELSHENDELSE -> { + when (utførendeStegType) { + REGISTRERE_PERSONGRUNNLAG -> FILTRERING_FØDSELSHENDELSER + FILTRERING_FØDSELSHENDELSER -> VILKÅRSVURDERING + VILKÅRSVURDERING -> BEHANDLINGSRESULTAT + BEHANDLINGSRESULTAT -> if (endringerIUtbetaling == EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING) IVERKSETT_MOT_OPPDRAG else HENLEGG_BEHANDLING + IVERKSETT_MOT_OPPDRAG -> VENTE_PÅ_STATUS_FRA_ØKONOMI + VENTE_PÅ_STATUS_FRA_ØKONOMI -> JOURNALFØR_VEDTAKSBREV + JOURNALFØR_VEDTAKSBREV -> DISTRIBUER_VEDTAKSBREV + DISTRIBUER_VEDTAKSBREV -> FERDIGSTILLE_BEHANDLING + FERDIGSTILLE_BEHANDLING -> BEHANDLING_AVSLUTTET + BEHANDLING_AVSLUTTET -> BEHANDLING_AVSLUTTET + else -> throw IllegalStateException("Stegtype ${utførendeStegType.displayName()} er ikke implementert for fødselshendelser") + } + } + + BehandlingÅrsak.SØKNAD -> { + when (utførendeStegType) { + REGISTRERE_PERSONGRUNNLAG -> { + if (behandling.fagsak.type == FagsakType.INSTITUSJON) { + REGISTRERE_INSTITUSJON_OG_VERGE + } else { + REGISTRERE_SØKNAD + } + } + + REGISTRERE_INSTITUSJON_OG_VERGE -> REGISTRERE_SØKNAD + REGISTRERE_SØKNAD -> VILKÅRSVURDERING + VILKÅRSVURDERING -> BEHANDLINGSRESULTAT + BEHANDLINGSRESULTAT -> VURDER_TILBAKEKREVING + VURDER_TILBAKEKREVING -> SEND_TIL_BESLUTTER + SEND_TIL_BESLUTTER -> BESLUTTE_VEDTAK + BESLUTTE_VEDTAK -> hentNesteStegTypeBasertPåOmDetErEndringIUtbetaling(endringerIUtbetaling) + IVERKSETT_MOT_OPPDRAG -> VENTE_PÅ_STATUS_FRA_ØKONOMI + VENTE_PÅ_STATUS_FRA_ØKONOMI -> IVERKSETT_MOT_FAMILIE_TILBAKE + IVERKSETT_MOT_FAMILIE_TILBAKE -> JOURNALFØR_VEDTAKSBREV + JOURNALFØR_VEDTAKSBREV -> DISTRIBUER_VEDTAKSBREV + DISTRIBUER_VEDTAKSBREV -> FERDIGSTILLE_BEHANDLING + FERDIGSTILLE_BEHANDLING -> BEHANDLING_AVSLUTTET + BEHANDLING_AVSLUTTET -> BEHANDLING_AVSLUTTET + else -> throw IllegalStateException("Stegtype ${utførendeStegType.displayName()} er ikke implementert for behandling med årsak $behandlingÅrsak og type $behandlingType.") + } + } + + BehandlingÅrsak.SMÅBARNSTILLEGG -> { + when (utførendeStegType) { + REGISTRERE_PERSONGRUNNLAG -> VILKÅRSVURDERING + VILKÅRSVURDERING -> BEHANDLINGSRESULTAT + BEHANDLINGSRESULTAT -> { + if (!behandling.skalBehandlesAutomatisk) { + VURDER_TILBAKEKREVING + } else if (behandling.skalBehandlesAutomatisk && behandling.status == BehandlingStatus.IVERKSETTER_VEDTAK) IVERKSETT_MOT_OPPDRAG else VURDER_TILBAKEKREVING + } + + VURDER_TILBAKEKREVING -> SEND_TIL_BESLUTTER + SEND_TIL_BESLUTTER -> BESLUTTE_VEDTAK + BESLUTTE_VEDTAK -> hentNesteStegTypeBasertPåOmDetErEndringIUtbetaling(endringerIUtbetaling) + IVERKSETT_MOT_OPPDRAG -> VENTE_PÅ_STATUS_FRA_ØKONOMI + VENTE_PÅ_STATUS_FRA_ØKONOMI -> IVERKSETT_MOT_FAMILIE_TILBAKE + IVERKSETT_MOT_FAMILIE_TILBAKE -> JOURNALFØR_VEDTAKSBREV + JOURNALFØR_VEDTAKSBREV -> DISTRIBUER_VEDTAKSBREV + DISTRIBUER_VEDTAKSBREV -> FERDIGSTILLE_BEHANDLING + FERDIGSTILLE_BEHANDLING -> BEHANDLING_AVSLUTTET + BEHANDLING_AVSLUTTET -> BEHANDLING_AVSLUTTET + else -> throw IllegalStateException("Stegtype ${utførendeStegType.displayName()} er ikke implementert for småbarnstillegg") + } + } + + BehandlingÅrsak.SATSENDRING -> { + when (utførendeStegType) { + REGISTRERE_PERSONGRUNNLAG -> VILKÅRSVURDERING + VILKÅRSVURDERING -> BEHANDLINGSRESULTAT + BEHANDLINGSRESULTAT -> if (endringerIUtbetaling == EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING) { + IVERKSETT_MOT_OPPDRAG + } else if (behandling.kategori == BehandlingKategori.EØS && endringerIUtbetaling == EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING) { + FERDIGSTILLE_BEHANDLING + } else { + throw Feil("Satsendringsbehandling har ingen endringer i utbetaling.") + } + + IVERKSETT_MOT_OPPDRAG -> VENTE_PÅ_STATUS_FRA_ØKONOMI + VENTE_PÅ_STATUS_FRA_ØKONOMI -> FERDIGSTILLE_BEHANDLING + FERDIGSTILLE_BEHANDLING -> BEHANDLING_AVSLUTTET + BEHANDLING_AVSLUTTET -> BEHANDLING_AVSLUTTET + else -> throw IllegalStateException("Stegtype ${utførendeStegType.displayName()} er ikke implementert for behandling med årsak $behandlingÅrsak og type $behandlingType.") + } + } + + else -> { + when (utførendeStegType) { + REGISTRERE_PERSONGRUNNLAG -> VILKÅRSVURDERING + VILKÅRSVURDERING -> BEHANDLINGSRESULTAT + BEHANDLINGSRESULTAT -> VURDER_TILBAKEKREVING + VURDER_TILBAKEKREVING -> SEND_TIL_BESLUTTER + SEND_TIL_BESLUTTER -> BESLUTTE_VEDTAK + BESLUTTE_VEDTAK -> hentNesteStegTypeBasertPåOmDetErEndringIUtbetaling(endringerIUtbetaling) + IVERKSETT_MOT_OPPDRAG -> VENTE_PÅ_STATUS_FRA_ØKONOMI + VENTE_PÅ_STATUS_FRA_ØKONOMI -> IVERKSETT_MOT_FAMILIE_TILBAKE + IVERKSETT_MOT_FAMILIE_TILBAKE -> JOURNALFØR_VEDTAKSBREV + JOURNALFØR_VEDTAKSBREV -> DISTRIBUER_VEDTAKSBREV + DISTRIBUER_VEDTAKSBREV -> FERDIGSTILLE_BEHANDLING + FERDIGSTILLE_BEHANDLING -> BEHANDLING_AVSLUTTET + BEHANDLING_AVSLUTTET -> BEHANDLING_AVSLUTTET + else -> throw IllegalStateException("Stegtype ${utførendeStegType.displayName()} er ikke implementert for behandling med årsak $behandlingÅrsak og type $behandlingType.") + } + } + } +} + +private fun hentNesteStegTypeBasertPåOmDetErEndringIUtbetaling(endringerIUtbetaling: EndringerIUtbetalingForBehandlingSteg): StegType = + when (endringerIUtbetaling) { + EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING -> IVERKSETT_MOT_OPPDRAG + EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING -> JOURNALFØR_VEDTAKSBREV + EndringerIUtbetalingForBehandlingSteg.IKKE_RELEVANT -> throw Feil("Endringer i utbetaling må utledes før man kan gå videre til neste steg.") + } + +private fun hentStegEtterBeslutteVedtakForTekniskEndring(endringerIUtbetaling: EndringerIUtbetalingForBehandlingSteg): StegType = + when (endringerIUtbetaling) { + EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING -> IVERKSETT_MOT_OPPDRAG + EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING -> FERDIGSTILLE_BEHANDLING + EndringerIUtbetalingForBehandlingSteg.IKKE_RELEVANT -> throw Feil("Endringer i utbetaling må utledes før man kan gå videre til neste steg.") + } + +enum class BehandlerRolle(val nivå: Int) { + SYSTEM(4), + BESLUTTER(3), + SAKSBEHANDLER(2), + VEILEDER(1), + UKJENT(0), +} + +enum class BehandlingStegStatus(val navn: String, val beskrivelse: String) { + IKKE_UTFØRT("IKKE_UTFØRT", "Steget er ikke utført"), + UTFØRT("UTFØRT", "Utført"), +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/BeslutteVedtak.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/BeslutteVedtak.kt new file mode 100644 index 000000000..6fb953b04 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/BeslutteVedtak.kt @@ -0,0 +1,205 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.behandling.AutomatiskBeslutningService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValideringService +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollService +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.sikkerhet.SaksbehandlerContext +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.task.FerdigstillBehandlingTask +import no.nav.familie.ba.sak.task.FerdigstillOppgaver +import no.nav.familie.ba.sak.task.IverksettMotOppdragTask +import no.nav.familie.ba.sak.task.JournalførVedtaksbrevTask +import no.nav.familie.ba.sak.task.OpprettOppgaveTask +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class BeslutteVedtak( + private val totrinnskontrollService: TotrinnskontrollService, + private val vedtakService: VedtakService, + private val behandlingService: BehandlingService, + private val beregningService: BeregningService, + private val taskRepository: TaskRepositoryWrapper, + private val loggService: LoggService, + private val vilkårsvurderingService: VilkårsvurderingService, + private val featureToggleService: FeatureToggleService, + private val tilkjentYtelseValideringService: TilkjentYtelseValideringService, + private val saksbehandlerContext: SaksbehandlerContext, + private val automatiskBeslutningService: AutomatiskBeslutningService, +) : BehandlingSteg { + + override fun utførStegOgAngiNeste( + behandling: Behandling, + data: RestBeslutningPåVedtak, + ): StegType { + if (behandling.status == BehandlingStatus.IVERKSETTER_VEDTAK) { + throw FunksjonellFeil("Behandlingen er allerede sendt til oppdrag og venter på kvittering") + } else if (behandling.status == BehandlingStatus.AVSLUTTET) { + throw FunksjonellFeil("Behandlingen er allerede avsluttet") + } else if (behandling.opprettetÅrsak == BehandlingÅrsak.KORREKSJON_VEDTAKSBREV && + !featureToggleService.isEnabled(FeatureToggleConfig.KAN_MANUELT_KORRIGERE_MED_VEDTAKSBREV) + ) { + throw FunksjonellFeil( + melding = "Årsak ${BehandlingÅrsak.KORREKSJON_VEDTAKSBREV.visningsnavn} og toggle ${FeatureToggleConfig.KAN_MANUELT_KORRIGERE_MED_VEDTAKSBREV} false", + frontendFeilmelding = "Du har ikke tilgang til å beslutte for denne behandlingen. Ta kontakt med teamet dersom dette ikke stemmer.", + ) + } else if (behandling.erTekniskBehandling() && !featureToggleService.isEnabled(FeatureToggleConfig.TEKNISK_ENDRING)) { + throw FunksjonellFeil( + "Du har ikke tilgang til å beslutte en behandling med årsak=${behandling.opprettetÅrsak.visningsnavn}. Ta kontakt med teamet dersom dette ikke stemmer.", + ) + } + + val behandlingSkalAutomatiskBesluttes = + automatiskBeslutningService.behandlingSkalAutomatiskBesluttes(behandling) + + val beslutter = + if (behandlingSkalAutomatiskBesluttes) SikkerhetContext.SYSTEM_NAVN else saksbehandlerContext.hentSaksbehandlerSignaturTilBrev() + val beslutterId = + if (behandlingSkalAutomatiskBesluttes) SikkerhetContext.SYSTEM_FORKORTELSE else SikkerhetContext.hentSaksbehandler() + + val totrinnskontroll = totrinnskontrollService.besluttTotrinnskontroll( + behandling = behandling, + beslutter = beslutter, + beslutterId = beslutterId, + beslutning = data.beslutning, + kontrollerteSider = data.kontrollerteSider, + ) + + opprettTaskFerdigstillGodkjenneVedtak( + behandling = behandling, + beslutning = data, + behandlingErAutomatiskBesluttet = behandlingSkalAutomatiskBesluttes, + ) + + return if (data.beslutning.erGodkjent()) { + val vedtak = vedtakService.hentAktivForBehandling(behandlingId = behandling.id) + ?: error("Fant ikke aktivt vedtak på behandling ${behandling.id}") + + vedtakService.oppdaterVedtaksdatoOgBrev(vedtak) + + val nesteSteg = sjekkOmBehandlingSkalIverksettesOgHentNesteSteg(behandling) + + when (nesteSteg) { + StegType.IVERKSETT_MOT_OPPDRAG -> { + opprettTaskIverksettMotOppdrag(behandling, vedtak) + } + + StegType.JOURNALFØR_VEDTAKSBREV -> { + if (!behandling.erBehandlingMedVedtaksbrevutsending()) { + throw Feil("Prøvde å opprette vedtaksbrev for behandling som ikke skal sende ut vedtaksbrev.") + } + + opprettJournalførVedtaksbrevTask(behandling, vedtak) + } + + StegType.FERDIGSTILLE_BEHANDLING -> { + if (behandling.type == BehandlingType.TEKNISK_ENDRING || behandling.erManuellMigreringForEndreMigreringsdato()) { + opprettFerdigstillBehandlingTask(behandling) + } else { + throw Feil("Neste steg 'ferdigstille behandling' er ikke implementert på 'beslutte vedtak'-steg") + } + } + + else -> throw Feil("Neste steg '$nesteSteg' er ikke implementert på 'beslutte vedtak'-steg") + } + nesteSteg + } else { + val vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandling(behandlingId = behandling.id) + ?: throw Feil("Fant ikke vilkårsvurdering på behandling") + val kopiertVilkårsVurdering = vilkårsvurdering.kopier(inkluderAndreVurderinger = true) + vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = kopiertVilkårsVurdering) + + behandlingService.opprettOgInitierNyttVedtakForBehandling( + behandling = behandling, + kopierVedtakBegrunnelser = true, + begrunnelseVilkårPekere = + VilkårsvurderingService.matchVilkårResultater( + vilkårsvurdering, + kopiertVilkårsVurdering, + ), + ) + + val behandleUnderkjentVedtakTask = OpprettOppgaveTask.opprettTask( + behandlingId = behandling.id, + oppgavetype = Oppgavetype.BehandleUnderkjentVedtak, + tilordnetRessurs = totrinnskontroll.saksbehandlerId, + fristForFerdigstillelse = LocalDate.now(), + ) + taskRepository.save(behandleUnderkjentVedtakTask) + StegType.SEND_TIL_BESLUTTER + } + } + + override fun postValiderSteg(behandling: Behandling) { + tilkjentYtelseValideringService.validerAtIngenUtbetalingerOverstiger100Prosent(behandling) + } + + override fun stegType(): StegType { + return StegType.BESLUTTE_VEDTAK + } + + private fun sjekkOmBehandlingSkalIverksettesOgHentNesteSteg(behandling: Behandling): StegType { + val endringerIUtbetaling = + beregningService.hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(behandling) + + return hentNesteStegGittEndringerIUtbetaling(behandling, endringerIUtbetaling) + } + + private fun opprettFerdigstillBehandlingTask(behandling: Behandling) { + val ferdigstillBehandlingTask = FerdigstillBehandlingTask.opprettTask( + søkerIdent = behandling.fagsak.aktør.aktivFødselsnummer(), + behandlingsId = behandling.id, + ) + taskRepository.save(ferdigstillBehandlingTask) + } + + private fun opprettTaskFerdigstillGodkjenneVedtak( + behandling: Behandling, + beslutning: RestBeslutningPåVedtak, + behandlingErAutomatiskBesluttet: Boolean, + ) { + loggService.opprettBeslutningOmVedtakLogg( + behandling = behandling, + beslutning = beslutning.beslutning, + begrunnelse = beslutning.begrunnelse, + behandlingErAutomatiskBesluttet = behandlingErAutomatiskBesluttet, + ) + + if (!behandling.erManuellMigrering() || !behandlingErAutomatiskBesluttet) { + val ferdigstillGodkjenneVedtakTask = + FerdigstillOppgaver.opprettTask(behandling.id, Oppgavetype.GodkjenneVedtak) + taskRepository.save(ferdigstillGodkjenneVedtakTask) + } + } + + private fun opprettTaskIverksettMotOppdrag(behandling: Behandling, vedtak: Vedtak) { + val task = IverksettMotOppdragTask.opprettTask(behandling, vedtak, SikkerhetContext.hentSaksbehandler()) + taskRepository.save(task) + } + + private fun opprettJournalførVedtaksbrevTask(behandling: Behandling, vedtak: Vedtak) { + val task = JournalførVedtaksbrevTask.opprettTaskJournalførVedtaksbrev( + vedtakId = vedtak.id, + personIdent = behandling.fagsak.aktør.aktivFødselsnummer(), + behandlingId = behandling.id, + ) + taskRepository.save(task) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/DistribuerVedtaksbrev.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/DistribuerVedtaksbrev.kt new file mode 100644 index 000000000..6383b8e27 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/DistribuerVedtaksbrev.kt @@ -0,0 +1,46 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.brev.DokumentDistribueringService +import no.nav.familie.ba.sak.task.DistribuerDokumentDTO +import no.nav.familie.ba.sak.task.FerdigstillBehandlingTask +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class DistribuerVedtaksbrev( + private val dokumentDistribueringService: DokumentDistribueringService, + private val taskRepository: TaskRepositoryWrapper, +) : BehandlingSteg { + + override fun utførStegOgAngiNeste( + behandling: Behandling, + data: DistribuerDokumentDTO, + ): StegType { + logger.info("Iverksetter distribusjon av vedtaksbrev med journalpostId ${data.journalpostId}") + dokumentDistribueringService.prøvDistribuerBrevOgLoggHendelseFraBehandling( + distribuerDokumentDTO = data, + loggBehandlerRolle = BehandlerRolle.SYSTEM, + ) + + val søkerIdent = behandling.fagsak.aktør.aktivFødselsnummer() + + val ferdigstillBehandlingTask = FerdigstillBehandlingTask.opprettTask( + søkerIdent = søkerIdent, + behandlingsId = data.behandlingId!!, + ) + taskRepository.save(ferdigstillBehandlingTask) + + return hentNesteStegForNormalFlyt(behandling) + } + + override fun stegType(): StegType { + return StegType.DISTRIBUER_VEDTAKSBREV + } + + companion object { + + private val logger = LoggerFactory.getLogger(DistribuerVedtaksbrev::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/FerdigstillBehandling.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/FerdigstillBehandling.kt new file mode 100644 index 000000000..72e1fb39b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/FerdigstillBehandling.kt @@ -0,0 +1,93 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingMetrikker +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.SnikeIKøenService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class FerdigstillBehandling( + private val fagsakService: FagsakService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val beregningService: BeregningService, + private val behandlingService: BehandlingService, + private val behandlingMetrikker: BehandlingMetrikker, + private val loggService: LoggService, + private val snikeIKøenService: SnikeIKøenService, +) : BehandlingSteg { + + override fun utførStegOgAngiNeste( + behandling: Behandling, + data: String, + ): StegType { + logger.info("Forsøker å ferdigstille behandling ${behandling.id}") + + val erHenlagt = behandlingHentOgPersisterService.hent(behandling.id).erHenlagt() + + if (behandling.status !== BehandlingStatus.IVERKSETTER_VEDTAK && !erHenlagt) { + error("Prøver å ferdigstille behandling ${behandling.id}, men status er ${behandling.status}") + } + + if (!erHenlagt) { + loggService.opprettFerdigstillBehandling(behandling) + } + + behandlingMetrikker.oppdaterBehandlingMetrikker(behandling) + if (behandling.status == BehandlingStatus.IVERKSETTER_VEDTAK && behandling.resultat != Behandlingsresultat.AVSLÅTT) { + oppdaterFagsakStatus(behandling = behandling) + } else { // Dette betyr henleggelse. + if (behandlingHentOgPersisterService.hentBehandlinger(behandling.fagsak.id).size == 1) { + fagsakService.oppdaterStatus(behandling.fagsak, FagsakStatus.AVSLUTTET) + } + behandlingHentOgPersisterService.finnAktivForFagsak(behandling.fagsak.id)?.aktiv = false + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(behandling.fagsak.id)?.apply { + aktiv = true + behandlingHentOgPersisterService.lagreEllerOppdater(this) + } + } + + behandlingService.oppdaterStatusPåBehandling(behandlingId = behandling.id, status = BehandlingStatus.AVSLUTTET) + snikeIKøenService.reaktiverBehandlingPåMaskinellVent(behandlingSomFerdigstilles = behandling) + + return hentNesteStegForNormalFlyt(behandling) + } + + private fun oppdaterFagsakStatus(behandling: Behandling) { + val tilkjentYtelse = beregningService.hentTilkjentYtelseForBehandling(behandlingId = behandling.id) + + if (skalOppdatereStønadFomOgTomForIverksatteBehandlingerIkkeSendtTilOppdrag(tilkjentYtelse)) { // 0-utbetalinger/omregning + tilkjentYtelse.stønadTom = tilkjentYtelse.andelerTilkjentYtelse.maxOfOrNull { it.stønadTom } + tilkjentYtelse.stønadFom = tilkjentYtelse.andelerTilkjentYtelse.minOfOrNull { it.stønadFom } + } + + val erLøpende = tilkjentYtelse.andelerTilkjentYtelse.any { it.stønadTom >= inneværendeMåned() } + if (erLøpende) { + fagsakService.oppdaterStatus(behandling.fagsak, FagsakStatus.LØPENDE) + } else { + fagsakService.oppdaterStatus(behandling.fagsak, FagsakStatus.AVSLUTTET) + } + } + + private fun skalOppdatereStønadFomOgTomForIverksatteBehandlingerIkkeSendtTilOppdrag(tilkjentYtelse: TilkjentYtelse) = + tilkjentYtelse.stønadFom == null && tilkjentYtelse.stønadTom == null && tilkjentYtelse.utbetalingsoppdrag == null + + override fun stegType(): StegType { + return StegType.FERDIGSTILLE_BEHANDLING + } + + companion object { + + private val logger = LoggerFactory.getLogger(FerdigstillBehandling::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/FiltreringF\303\270dselshendelserSteg.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/FiltreringF\303\270dselshendelserSteg.kt" new file mode 100644 index 000000000..928896ad2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/FiltreringF\303\270dselshendelserSteg.kt" @@ -0,0 +1,41 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.FiltreringsreglerService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.domene.erOppfylt +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class FiltreringFødselshendelserSteg( + private val filtreringsreglerService: FiltreringsreglerService, +) : BehandlingSteg { + + override fun utførStegOgAngiNeste( + behandling: Behandling, + data: NyBehandlingHendelse, + ): StegType { + logger.info("Kjører filtreringsregler for behandling ${behandling.id}") + + val fødselshendelsefiltreringResultat = filtreringsreglerService.kjørFiltreringsregler( + data, + behandling, + ) + + return if (!fødselshendelsefiltreringResultat.erOppfylt()) { + StegType.HENLEGG_BEHANDLING + } else { + hentNesteStegForNormalFlyt(behandling) + } + } + + override fun stegType(): StegType { + return StegType.FILTRERING_FØDSELSHENDELSER + } + + companion object { + + private val logger = LoggerFactory.getLogger(FiltreringFødselshendelserSteg::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/HenleggBehandling.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/HenleggBehandling.kt new file mode 100644 index 000000000..f18400c75 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/HenleggBehandling.kt @@ -0,0 +1,85 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.SATSENDRING +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.HenleggÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.RestHenleggBehandlingInfo +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.brev.DokumentService +import no.nav.familie.ba.sak.kjerne.brev.domene.ManueltBrevRequest +import no.nav.familie.ba.sak.kjerne.brev.domene.byggMottakerdata +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype.BehandleSak +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype.BehandleUnderkjentVedtak +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype.GodkjenneVedtak +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype.VurderLivshendelse +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class HenleggBehandling( + private val behandlingService: BehandlingService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val loggService: LoggService, + private val dokumentService: DokumentService, + private val oppgaveService: OppgaveService, + private val persongrunnlagService: PersongrunnlagService, + private val arbeidsfordelingService: ArbeidsfordelingService, +) : BehandlingSteg { + private val logger = LoggerFactory.getLogger(HenleggBehandling::class.java) + + override fun utførStegOgAngiNeste(behandling: Behandling, data: RestHenleggBehandlingInfo): StegType { + if (data.årsak == HenleggÅrsak.SØKNAD_TRUKKET) { + dokumentService.sendManueltBrev( + behandling = behandling, + fagsakId = behandling.fagsak.id, + manueltBrevRequest = ManueltBrevRequest( + mottakerIdent = behandling.fagsak.aktør.aktivFødselsnummer(), + brevmal = Brevmal.HENLEGGE_TRUKKET_SØKNAD, + ).byggMottakerdata(behandling, persongrunnlagService, arbeidsfordelingService), + ) + } + + val (oppgaverTekniskVedlikeholdPgaSatsendring, oppgaverSomSkalFerdigstilles) = oppgaveService.hentOppgaverSomIkkeErFerdigstilt( + behandling, + ) + .partition { + data.årsak == HenleggÅrsak.TEKNISK_VEDLIKEHOLD && data.begrunnelse == SATSENDRING && it.type in listOf( + BehandleSak, + GodkjenneVedtak, + BehandleUnderkjentVedtak, + VurderLivshendelse, + ) + } + + oppgaverSomSkalFerdigstilles.forEach { + oppgaveService.ferdigstillOppgaver(behandling.id, it.type) + } + + oppgaverTekniskVedlikeholdPgaSatsendring.forEach { + logger.info("Teknisk opphør pga satsendring. Fjerner behandlesAvApplikasjon for oppgaveId=${it.gsakId} slik at saksbehandler kan lukke den fra Gosys. fagsakId=${behandling.fagsak.id}, behandlingId=${behandling.id}") + oppgaveService.fjernBehandlesAvApplikasjon(listOf(it.gsakId.toLong())) + } + + loggService.opprettHenleggBehandling(behandling, data.årsak.beskrivelse, data.begrunnelse) + + behandling.resultat = data.årsak.tilBehandlingsresultat() + behandling.leggTilHenleggStegOmDetIkkeFinnesFraFør() + + behandlingHentOgPersisterService.lagreEllerOppdater(behandling) + + // Slett migreringsdato + behandlingService.deleteMigreringsdatoVedHenleggelse(behandling.id) + + return hentNesteStegForNormalFlyt(behandling) + } + + override fun stegType(): StegType { + return StegType.HENLEGG_BEHANDLING + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/IverksettMotFamilieTilbake.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/IverksettMotFamilieTilbake.kt new file mode 100644 index 000000000..b023add95 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/IverksettMotFamilieTilbake.kt @@ -0,0 +1,72 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.tilbakekreving.TilbakekrevingService +import no.nav.familie.ba.sak.kjerne.tilbakekreving.domene.TilbakekrevingRepository +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.task.JournalførVedtaksbrevTask +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.Properties + +data class IverksettMotFamilieTilbakeData( + val metadata: Properties, +) + +@Service +class IverksettMotFamilieTilbake( + private val vedtakService: VedtakService, + private val tilbakekrevingService: TilbakekrevingService, + private val taskRepository: TaskRepositoryWrapper, + private val tilbakekrevingRepository: TilbakekrevingRepository, +) : BehandlingSteg { + + override fun utførStegOgAngiNeste(behandling: Behandling, data: IverksettMotFamilieTilbakeData): StegType { + val vedtak = vedtakService.hentAktivForBehandling(behandling.id) ?: throw Feil( + "Fant ikke vedtak for behandling ${behandling.id} ved iverksetting mot familie-tilbake.", + ) + + val tilbakekreving = tilbakekrevingRepository.findByBehandlingId(behandling.id) + + if (tilbakekreving != null && + tilbakekreving.valg != Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING && + !tilbakekrevingService.søkerHarÅpenTilbakekreving(behandling.fagsak.id) + ) { + val tilbakekrevingId = tilbakekrevingService.opprettTilbakekreving(behandling) + tilbakekreving.tilbakekrevingsbehandlingId = tilbakekrevingId + + logger.info("Opprettet tilbakekreving for behandling ${behandling.id} og tilbakekrevingsid $tilbakekrevingId") + tilbakekrevingRepository.save(tilbakekreving) + } + + if (!behandling.erBehandlingMedVedtaksbrevutsending()) { + throw Feil("Neste steg på behandling $behandling er journalføring, men denne behandlingen skal ikke sende ut vedtaksbrev") + } + + opprettTaskJournalførVedtaksbrev(vedtakId = vedtak.id, data.metadata) + + return hentNesteStegForNormalFlyt(behandling) + } + + private fun opprettTaskJournalførVedtaksbrev(vedtakId: Long, metadata: Properties) { + val task = Task( + type = JournalførVedtaksbrevTask.TASK_STEP_TYPE, + payload = "$vedtakId", + properties = metadata, + ) + taskRepository.save(task) + } + + override fun stegType(): StegType { + return StegType.IVERKSETT_MOT_FAMILIE_TILBAKE + } + + companion object { + + private val logger = LoggerFactory.getLogger(StatusFraOppdrag::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/IverksettMotOppdrag.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/IverksettMotOppdrag.kt new file mode 100644 index 000000000..6b67d9d05 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/IverksettMotOppdrag.kt @@ -0,0 +1,87 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.økonomi.AndelTilkjentYtelseForIverksettingFactory +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValideringService +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.task.SendVedtakTilInfotrygdTask +import no.nav.familie.ba.sak.task.dto.IverksettingTaskDTO +import org.springframework.stereotype.Service + +@Service +class IverksettMotOppdrag( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val økonomiService: ØkonomiService, + private val totrinnskontrollService: TotrinnskontrollService, + private val vedtakService: VedtakService, + private val featureToggleService: FeatureToggleService, + private val taskRepository: TaskRepositoryWrapper, + private val tilkjentYtelseValideringService: TilkjentYtelseValideringService, +) : BehandlingSteg { + private val iverksattOppdrag = Metrics.counter("familie.ba.sak.oppdrag.iverksatt") + + override fun preValiderSteg(behandling: Behandling, stegService: StegService?) { + tilkjentYtelseValideringService.validerAtIngenUtbetalingerOverstiger100Prosent(behandling) + + val totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(behandlingId = behandling.id) + ?: throw Feil( + message = "Mangler totrinnskontroll ved iverksetting", + frontendFeilmelding = "Mangler totrinnskontroll ved iverksetting", + ) + + if (totrinnskontroll.erUgyldig()) { + throw Feil( + message = "Totrinnskontroll($totrinnskontroll) er ugyldig ved iverksetting", + frontendFeilmelding = "Totrinnskontroll er ugyldig ved iverksetting", + ) + } + + if (!totrinnskontroll.godkjent) { + throw Feil( + message = "Prøver å iverksette et underkjent vedtak", + frontendFeilmelding = "", + ) + } + } + + override fun utførStegOgAngiNeste( + behandling: Behandling, + data: IverksettingTaskDTO, + ): StegType { + økonomiService.oppdaterTilkjentYtelseMedUtbetalingsoppdragOgIverksett( + vedtak = vedtakService.hent(data.vedtaksId), + saksbehandlerId = data.saksbehandlerId, + andelTilkjentYtelseForUtbetalingsoppdragFactory = AndelTilkjentYtelseForIverksettingFactory(), + ) + iverksattOppdrag.increment() + val forrigeIverksatteBehandling = + behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(behandling) + if (forrigeIverksatteBehandling == null || + forrigeIverksatteBehandling.type == BehandlingType.MIGRERING_FRA_INFOTRYGD_OPPHØRT || + behandling.erManuellMigrering() + ) { + taskRepository.save( + SendVedtakTilInfotrygdTask.opprettTask( + hentFnrStoenadsmottaker(behandling.fagsak), + behandling.id, + ), + ) + } + return hentNesteStegForNormalFlyt(behandling) + } + + override fun stegType(): StegType { + return StegType.IVERKSETT_MOT_OPPDRAG + } + + private fun hentFnrStoenadsmottaker(fagsak: Fagsak) = fagsak.aktør.aktivFødselsnummer() +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/Journalf\303\270rVedtaksbrev.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/Journalf\303\270rVedtaksbrev.kt" new file mode 100644 index 000000000..348bc9b7a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/Journalf\303\270rVedtaksbrev.kt" @@ -0,0 +1,238 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient.Companion.VEDTAK_VEDLEGG_FILNAVN +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient.Companion.VEDTAK_VEDLEGG_TITTEL +import no.nav.familie.ba.sak.integrasjoner.journalføring.UtgåendeJournalføringService +import no.nav.familie.ba.sak.integrasjoner.organisasjon.OrganisasjonService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.brev.hentOverstyrtDokumenttittel +import no.nav.familie.ba.sak.kjerne.brev.mottaker.BrevmottakerService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.steg.domene.JournalførVedtaksbrevDTO +import no.nav.familie.ba.sak.kjerne.steg.domene.MottakerInfo +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.task.DistribuerDokumentDTO +import no.nav.familie.ba.sak.task.DistribuerDokumentTask +import no.nav.familie.ba.sak.task.DistribuerVedtaksbrevTilInstitusjonVergeEllerManuellBrevMottakerTask +import no.nav.familie.kontrakter.felles.BrukerIdType +import no.nav.familie.kontrakter.felles.dokarkiv.AvsenderMottaker +import no.nav.familie.kontrakter.felles.dokarkiv.Dokumenttype +import no.nav.familie.kontrakter.felles.dokarkiv.v2.Dokument +import no.nav.familie.kontrakter.felles.dokarkiv.v2.Filtype +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class JournalførVedtaksbrev( + private val vedtakService: VedtakService, + private val arbeidsfordelingService: ArbeidsfordelingService, + private val utgåendeJournalføringService: UtgåendeJournalføringService, + private val taskRepository: TaskRepositoryWrapper, + private val fagsakRepository: FagsakRepository, + private val organisasjonService: OrganisasjonService, + private val brevmottakerService: BrevmottakerService, + private val brevmalService: BrevmalService, +) : BehandlingSteg { + + override fun utførStegOgAngiNeste(behandling: Behandling, data: JournalførVedtaksbrevDTO): StegType { + val vedtak = vedtakService.hent(vedtakId = data.vedtakId) + val fagsakId = "${vedtak.behandling.fagsak.id}" + val fagsak = fagsakRepository.finnFagsak(vedtak.behandling.fagsak.id) + val søkersident = vedtak.behandling.fagsak.aktør.aktivFødselsnummer() + val institusjonVergeIdent = vedtak.behandling.verge?.ident + + if (fagsak == null || fagsak.type == FagsakType.INSTITUSJON && fagsak.institusjon == null) { + error("Journalfør vedtaksbrev feil: fagsak er null eller institusjon fagsak har ikke institusjonsinformasjon") + } + + val behandlendeEnhet = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id).behandlendeEnhetId + + val mottakere = mutableListOf() + + if (fagsak.type == FagsakType.INSTITUSJON) { + mottakere += MottakerInfo(fagsak.institusjon!!.orgNummer, BrukerIdType.ORGNR, false) + } else { + val brevMottakere = brevmottakerService.hentBrevmottakere(behandling.id) + if (brevMottakere.isNotEmpty()) { + mottakere += brevmottakerService.lagMottakereFraBrevMottakere(brevMottakere, søkersident) + } else { + mottakere += MottakerInfo(søkersident, BrukerIdType.FNR, false) + } + } + if (institusjonVergeIdent != null) { // brukes kun i institusjon + mottakere += MottakerInfo(vedtak.behandling.verge.ident, BrukerIdType.FNR, true) + } + + val journalposterTilDistribusjon = mutableMapOf() + mottakere.forEach { mottakerInfo -> + journalførVedtaksbrev( + fnr = fagsak.aktør.aktivFødselsnummer(), + fagsakId = fagsakId, + vedtak = vedtak, + journalførendeEnhet = behandlendeEnhet, + mottakerInfo = mottakerInfo, + tilManuellMottakerEllerVerge = if (institusjonVergeIdent != null) { + mottakerInfo.erInstitusjonVerge + } else { + (mottakerInfo.navn != null && mottakerInfo.navn != brevmottakerService.hentMottakerNavn(søkersident)) + }, // mottakersnavn fyller ut kun når manuell mottaker finnes + ).also { journalposterTilDistribusjon[it] = mottakerInfo } + } + + lagTaskForÅDistribuereVedtaksbrev(journalposterTilDistribusjon, data, behandling) + + return hentNesteStegForNormalFlyt(behandling) + } + + private fun lagTaskForÅDistribuereVedtaksbrev( + journalposterTilDistribusjon: Map, + data: JournalførVedtaksbrevDTO, + behandling: Behandling, + ) { + journalposterTilDistribusjon.forEach { + val finnesBrevMottaker = + it.value.navn != null && + it.value.navn != brevmottakerService.hentMottakerNavn(behandling.fagsak.aktør.aktivFødselsnummer()) + if (it.value.erInstitusjonVerge || finnesBrevMottaker) { // Denne tasken sender kun vedtaksbrev + val distribuerTilVergeTask = + DistribuerVedtaksbrevTilInstitusjonVergeEllerManuellBrevMottakerTask + .opprettDistribuerVedtaksbrevTilInstitusjonVergeEllerManuellBrevMottakerTask( + distribuerDokumentDTO = lagDistribuerDokumentDto( + behandling = behandling, + journalPostId = it.key, + mottakerInfo = it.value, + ), + properties = data.task.metadata, + ) + taskRepository.save(distribuerTilVergeTask) + } else { // Denne tasken sender vedtaksbrev og håndterer steg videre + val distribuerTilSøkerTask = DistribuerDokumentTask.opprettDistribuerDokumentTask( + distribuerDokumentDTO = lagDistribuerDokumentDto( + behandling = behandling, + journalPostId = it.key, + mottakerInfo = it.value, + ), + properties = data.task.metadata, + ) + taskRepository.save(distribuerTilSøkerTask) + } + } + } + + fun journalførVedtaksbrev( + fnr: String, + fagsakId: String, + vedtak: Vedtak, + journalførendeEnhet: String, + mottakerInfo: MottakerInfo, + tilManuellMottakerEllerVerge: Boolean, + ): String { + val vedleggPdf = + hentVedlegg(VEDTAK_VEDLEGG_FILNAVN) ?: error("Klarte ikke hente vedlegg $VEDTAK_VEDLEGG_FILNAVN") + + val brev = listOf( + Dokument( + vedtak.stønadBrevPdF!!, + filtype = Filtype.PDFA, + dokumenttype = vedtak.behandling.resultat.tilDokumenttype(), + tittel = hentOverstyrtDokumenttittel(vedtak.behandling), + ), + ) + logger.info( + "Journalfører vedtaksbrev for behandling ${vedtak.behandling.id} med tittel ${ + hentOverstyrtDokumenttittel(vedtak.behandling) + }", + ) + val vedlegg = listOf( + Dokument( + vedleggPdf, + filtype = Filtype.PDFA, + dokumenttype = Dokumenttype.BARNETRYGD_VEDLEGG, + tittel = VEDTAK_VEDLEGG_TITTEL, + ), + ) + return utgåendeJournalføringService.journalførDokument( + fnr = fnr, + fagsakId = fagsakId, + journalførendeEnhet = journalførendeEnhet, + brev = brev, + vedlegg = vedlegg, + behandlingId = vedtak.behandling.id, + avsenderMottaker = utledAvsenderMottaker(mottakerInfo), + tilManuellMottakerEllerVerge = tilManuellMottakerEllerVerge, + ) + } + + private fun Behandlingsresultat.tilDokumenttype() = when (this) { + Behandlingsresultat.AVSLÅTT -> Dokumenttype.BARNETRYGD_VEDTAK_AVSLAG + Behandlingsresultat.OPPHØRT -> Dokumenttype.BARNETRYGD_OPPHØR + else -> Dokumenttype.BARNETRYGD_VEDTAK_INNVILGELSE + } + + private fun utledAvsenderMottaker(mottakerInfo: MottakerInfo): AvsenderMottaker? { + return when { + mottakerInfo.brukerIdType == BrukerIdType.ORGNR -> { + AvsenderMottaker( + idType = mottakerInfo.brukerIdType, + id = mottakerInfo.brukerId, + navn = organisasjonService.hentOrganisasjon(mottakerInfo.brukerId).navn, + ) + } + + mottakerInfo.erInstitusjonVerge -> { + AvsenderMottaker( + idType = mottakerInfo.brukerIdType, + id = mottakerInfo.brukerId, + navn = brevmottakerService.hentMottakerNavn(mottakerInfo.brukerId), + ) + } + + mottakerInfo.navn != null -> { + AvsenderMottaker( + idType = mottakerInfo.brukerIdType, + id = mottakerInfo.brukerIdType?.let { mottakerInfo.brukerId }, + navn = mottakerInfo.navn, + ) + } + + else -> { + null + } + } + } + + private fun lagDistribuerDokumentDto( + behandling: Behandling, + journalPostId: String, + mottakerInfo: MottakerInfo, + ) = + DistribuerDokumentDTO( + personEllerInstitusjonIdent = mottakerInfo.brukerId, + behandlingId = behandling.id, + journalpostId = journalPostId, + brevmal = brevmalService.hentBrevmal(behandling), + erManueltSendt = false, + manuellAdresseInfo = mottakerInfo.manuellAdresseInfo, + ) + + override fun stegType(): StegType { + return StegType.JOURNALFØR_VEDTAKSBREV + } + + companion object { + + val logger = LoggerFactory.getLogger(JournalførVedtaksbrev::class.java) + + fun hentVedlegg(vedleggsnavn: String): ByteArray? { + val inputStream = this::class.java.classLoader.getResourceAsStream("dokumenter/$vedleggsnavn") + return inputStream?.readAllBytes() + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerInstitusjonOgVerge.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerInstitusjonOgVerge.kt new file mode 100644 index 000000000..4e4b6ca0f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerInstitusjonOgVerge.kt @@ -0,0 +1,58 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerInstitusjonOgVerge +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.institusjon.InstitusjonService +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.verge.VergeService +import org.springframework.stereotype.Service + +@Service +class RegistrerInstitusjonOgVerge( + val institusjonService: InstitusjonService, + val vergeService: VergeService, + val loggService: LoggService, + val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + val fagsakService: FagsakService, +) : BehandlingSteg { + + override fun utførStegOgAngiNeste( + behandling: Behandling, + data: RestRegistrerInstitusjonOgVerge, + ): StegType { + val verge = data.tilVerge(behandling) + val institusjon = data.tilInstitusjon() + if (verge != null) { + vergeService.oppdaterVergeForBehandling(behandling, verge) + loggService.opprettRegistrerVergeLogg( + behandling, + ) + } + if (institusjon != null) { + institusjonService.hentEllerOpprettInstitusjon( + orgNummer = institusjon.orgNummer, + tssEksternId = institusjon.tssEksternId, + ).apply { + val fagsak = behandling.fagsak + fagsak.institusjon = this + fagsakService.lagre(fagsak) + } + loggService.opprettRegistrerInstitusjonLogg( + behandling, + ) + } + + if (verge == null && institusjon?.orgNummer == null) { + throw Feil("Ugyldig DTO for registrer verge") + } + + return hentNesteStegForNormalFlyt(behandling = behandlingHentOgPersisterService.hent(behandlingId = behandling.id)) + } + + override fun stegType(): StegType { + return StegType.REGISTRERE_INSTITUSJON_OG_VERGE + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerPersongrunnlag.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerPersongrunnlag.kt new file mode 100644 index 000000000..381e76390 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerPersongrunnlag.kt @@ -0,0 +1,66 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.EøsSkjemaerForNyBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.PersonopplysningGrunnlagForNyBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.VilkårsvurderingForNyBehandlingService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate + +@Service +class RegistrerPersongrunnlag( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val vilkårsvurderingForNyBehandlingService: VilkårsvurderingForNyBehandlingService, + private val personopplysningGrunnlagForNyBehandlingService: PersonopplysningGrunnlagForNyBehandlingService, + private val eøsSkjemaerForNyBehandlingService: EøsSkjemaerForNyBehandlingService, +) : BehandlingSteg { + + @Transactional + override fun utførStegOgAngiNeste( + behandling: Behandling, + data: RegistrerPersongrunnlagDTO, + ): StegType { + val forrigeBehandlingSomErVedtatt = behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt( + behandling, + ) + + personopplysningGrunnlagForNyBehandlingService.opprettKopiEllerNyttPersonopplysningGrunnlag( + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErVedtatt, + søkerIdent = data.ident, + barnasIdenter = data.barnasIdenter, + ) + + vilkårsvurderingForNyBehandlingService.opprettVilkårsvurderingUtenomHovedflyt( + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErVedtatt, + nyMigreringsdato = data.nyMigreringsdato, + ) + + eøsSkjemaerForNyBehandlingService.kopierEøsSkjemaer( + forrigeBehandlingSomErVedtattId = if (forrigeBehandlingSomErVedtatt != null) { + BehandlingId( + forrigeBehandlingSomErVedtatt.id, + ) + } else { + null + }, + behandlingId = BehandlingId(behandling.id), + ) + + return hentNesteStegForNormalFlyt(behandling) + } + + override fun stegType(): StegType { + return StegType.REGISTRERE_PERSONGRUNNLAG + } +} + +data class RegistrerPersongrunnlagDTO( + val ident: String, + val barnasIdenter: List, + val nyMigreringsdato: LocalDate? = null, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrereS\303\270knad.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrereS\303\270knad.kt" new file mode 100644 index 000000000..ca4764906 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrereS\303\270knad.kt" @@ -0,0 +1,74 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.SøknadDTO +import no.nav.familie.ba.sak.ekstern.restDomene.tilDomene +import no.nav.familie.ba.sak.ekstern.restDomene.writeValueAsString +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.BehandlingstemaService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.søknad.SøknadGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.søknad.SøknadGrunnlagService +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import org.springframework.stereotype.Service + +@Service +class RegistrereSøknad( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val behandlingstemaService: BehandlingstemaService, + private val søknadGrunnlagService: SøknadGrunnlagService, + private val persongrunnlagService: PersongrunnlagService, + private val loggService: LoggService, + private val vedtakService: VedtakService, + private val tilbakestillBehandlingService: TilbakestillBehandlingService, +) : BehandlingSteg { + + override fun utførStegOgAngiNeste( + behandling: Behandling, + data: RestRegistrerSøknad, + ): StegType { + val aktivSøknadGrunnlagFinnes = søknadGrunnlagService.hentAktiv(behandlingId = behandling.id) != null + val søknadDTO: SøknadDTO = data.søknad + val innsendtSøknad = søknadDTO.writeValueAsString() + + if (behandling.underkategori != søknadDTO.underkategori.tilDomene()) { + behandlingstemaService.oppdaterBehandlingstema( + behandling = behandlingHentOgPersisterService.hent(behandlingId = behandling.id), + overstyrtUnderkategori = søknadDTO.underkategori.tilDomene(), + ) + } + + loggService.opprettRegistrertSøknadLogg(behandling, aktivSøknadGrunnlagFinnes) + søknadGrunnlagService.lagreOgDeaktiverGammel( + søknadGrunnlag = SøknadGrunnlag( + behandlingId = behandling.id, + søknad = innsendtSøknad, + ), + ) + + val forrigeBehandlingSomErVedtatt = + behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling = behandling) + persongrunnlagService.registrerBarnFraSøknad( + behandling = behandlingHentOgPersisterService.hent(behandlingId = behandling.id), + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErVedtatt, + søknadDTO = søknadDTO, + ) + + tilbakestillBehandlingService.initierOgSettBehandlingTilVilkårsvurdering( + behandling = behandlingHentOgPersisterService.hent(behandlingId = behandling.id), + bekreftEndringerViaFrontend = data.bekreftEndringerViaFrontend, + ) + + val vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingId = behandling.id) + + vedtakService.oppdater(vedtak) + + return hentNesteStegForNormalFlyt(behandling = behandlingHentOgPersisterService.hent(behandlingId = behandling.id)) + } + + override fun stegType(): StegType { + return StegType.REGISTRERE_SØKNAD + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/SendTilBeslutter.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/SendTilBeslutter.kt new file mode 100644 index 000000000..bb38240f0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/SendTilBeslutter.kt @@ -0,0 +1,152 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.AutomatiskBeslutningService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.ValiderBrevmottakerService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatSteg +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.validerPerioderInneholderBegrunnelser +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.task.FerdigstillOppgaver +import no.nav.familie.ba.sak.task.OpprettOppgaveTask +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class SendTilBeslutter( + private val behandlingService: BehandlingService, + private val taskRepository: TaskRepositoryWrapper, + private val oppgaveService: OppgaveService, + private val loggService: LoggService, + private val totrinnskontrollService: TotrinnskontrollService, + private val vilkårsvurderingService: VilkårsvurderingService, + private val vedtakService: VedtakService, + private val vedtaksperiodeService: VedtaksperiodeService, + private val automatiskBeslutningService: AutomatiskBeslutningService, + private val validerBrevmottakerService: ValiderBrevmottakerService, +) : BehandlingSteg { + + override fun preValiderSteg( + behandling: Behandling, + stegService: StegService?, + ) { + validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligePersonerMedManuelleBrevmottakere(behandlingId = behandling.id) + vilkårsvurderingService.hentAktivForBehandling(behandlingId = behandling.id) + ?.validerAtAlleAnndreVurderingerErVurdert() + + val behandlingsresultatSteg: BehandlingsresultatSteg = + stegService?.hentBehandlingSteg(StegType.BEHANDLINGSRESULTAT) as BehandlingsresultatSteg + behandlingsresultatSteg.preValiderSteg(behandling) + + behandling.validerRekkefølgeOgUnikhetPåSteg() + behandling.validerMaksimaltEtStegIkkeUtført() + + if (behandling.resultat != Behandlingsresultat.FORTSATT_INNVILGET) { + val vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingId = behandling.id) + val utvidetVedtaksperioder = vedtaksperiodeService.hentUtvidetVedtaksperiodeMedBegrunnelser(vedtak) + utvidetVedtaksperioder.validerPerioderInneholderBegrunnelser( + behandlingId = behandling.id, + fagsakId = behandling.fagsak.id, + ) + } + } + + override fun utførStegOgAngiNeste( + behandling: Behandling, + data: String, + ): StegType { + totrinnskontrollService.opprettTotrinnskontrollMedSaksbehandler(behandling) + + if (!automatiskBeslutningService.behandlingSkalAutomatiskBesluttes(behandling)) { + val godkjenneVedtakTask = OpprettOppgaveTask.opprettTask( + behandlingId = behandling.id, + oppgavetype = Oppgavetype.GodkjenneVedtak, + fristForFerdigstillelse = LocalDate.now(), + ) + loggService.opprettSendTilBeslutterLogg(behandling = behandling, skalAutomatiskBesluttes = false) + taskRepository.save(godkjenneVedtakTask) + } else { + loggService.opprettSendTilBeslutterLogg(behandling = behandling, skalAutomatiskBesluttes = true) + } + + opprettFerdigstillOppgaveTasker(behandling) + + behandlingService.sendBehandlingTilBeslutter(behandling) + + return hentNesteStegForNormalFlyt(behandling) + } + + private fun opprettFerdigstillOppgaveTasker(behandling: Behandling) { + listOf( + Oppgavetype.BehandleSak, + Oppgavetype.BehandleUnderkjentVedtak, + Oppgavetype.VurderLivshendelse, + ).forEach { oppgavetype -> + oppgaveService.hentOppgaverSomIkkeErFerdigstilt(oppgavetype, behandling).also { + if (it.isNotEmpty()) { + val ferdigstillOppgaverTask = FerdigstillOppgaver.opprettTask(behandling.id, oppgavetype) + taskRepository.save(ferdigstillOppgaverTask) + } + } + } + } + + override fun stegType(): StegType { + return StegType.SEND_TIL_BESLUTTER + } +} + +fun Behandling.validerRekkefølgeOgUnikhetPåSteg(): Boolean { + if (erHenlagt()) { + throw Feil("Valideringen kan ikke kjøres for henlagte behandlinger.") + } + + var forrigeBehandlingStegTilstand: BehandlingStegTilstand? = null + behandlingStegTilstand.forEach { + if (forrigeBehandlingStegTilstand != null && + forrigeBehandlingStegTilstand!!.behandlingSteg >= it.behandlingSteg && + ( + forrigeBehandlingStegTilstand!!.behandlingSteg.rekkefølge != it.behandlingSteg.rekkefølge || + forrigeBehandlingStegTilstand!!.behandlingSteg == it.behandlingSteg + ) + ) { + throw Feil("Rekkefølge på steg registrert på behandling $id er feil eller redundante.") + } + forrigeBehandlingStegTilstand = it + } + return true +} + +fun Behandling.validerMaksimaltEtStegIkkeUtført() { + if (erHenlagt()) { + throw Feil("Valideringen kan ikke kjøres for henlagte behandlinger.") + } + + if (behandlingStegTilstand.filter { it.behandlingStegStatus == BehandlingStegStatus.IKKE_UTFØRT }.size > 1) { + throw Feil("Behandling $id har mer enn ett ikke fullført steg.") + } +} + +fun Vilkårsvurdering.validerAtAlleAnndreVurderingerErVurdert() { + personResultater.flatMap { it.andreVurderinger } + .takeIf { it.any { annenVurdering -> annenVurdering.resultat == Resultat.IKKE_VURDERT } } + ?.let { + throw FunksjonellFeil( + melding = "Forsøker å ferdigstille uten å ha fylt ut påkrevde vurderinger", + frontendFeilmelding = "Andre vurderinger må tas stilling til før behandling kan sendes til beslutter.", + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/StatusFraOppdrag.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/StatusFraOppdrag.kt new file mode 100644 index 000000000..33ecfbace --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/StatusFraOppdrag.kt @@ -0,0 +1,107 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.task.FerdigstillBehandlingTask +import no.nav.familie.ba.sak.task.IverksettMotFamilieTilbakeTask +import no.nav.familie.ba.sak.task.JournalførVedtaksbrevTask +import no.nav.familie.ba.sak.task.dto.StatusFraOppdragDTO +import no.nav.familie.ba.sak.task.nesteGyldigeTriggertidForBehandlingIHverdager +import no.nav.familie.kontrakter.felles.oppdrag.OppdragStatus +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.error.RekjørSenereException +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.Properties + +data class StatusFraOppdragMedTask( + val statusFraOppdragDTO: StatusFraOppdragDTO, + val task: Task, +) + +@Service +class StatusFraOppdrag( + private val økonomiService: ØkonomiService, + private val taskRepository: TaskRepositoryWrapper, +) : BehandlingSteg { + + override fun utførStegOgAngiNeste( + behandling: Behandling, + data: StatusFraOppdragMedTask, + ): StegType { + val statusFraOppdragDTO = data.statusFraOppdragDTO + val task = data.task + + val oppdragStatus = økonomiService.hentStatus(statusFraOppdragDTO.oppdragId, statusFraOppdragDTO.behandlingsId) + logger.debug("Mottok status '$oppdragStatus' fra oppdrag") + if (oppdragStatus != OppdragStatus.KVITTERT_OK) { + if (oppdragStatus == OppdragStatus.LAGT_PÅ_KØ) { + throw RekjørSenereException( + årsak = "Mottok lagt på kø kvittering fra oppdrag.", + triggerTid = nesteGyldigeTriggertidForBehandlingIHverdager(minutesToAdd = if (behandling.erMigrering()) 1 else 15), + ) + } else { + taskRepository.save(task.copy(status = Status.MANUELL_OPPFØLGING)) + } + + error("Mottok status '$oppdragStatus' fra oppdrag") + } else { + val nesteSteg = hentNesteStegForNormalFlyt(behandling) + if (nesteSteg == StegType.JOURNALFØR_VEDTAKSBREV && !behandling.erBehandlingMedVedtaksbrevutsending()) { + throw Feil("Neste steg på behandling $behandling er journalføring, men denne behandlingen skal ikke sende ut vedtaksbrev") + } + + when (nesteSteg) { + StegType.JOURNALFØR_VEDTAKSBREV -> opprettTaskJournalførVedtaksbrev( + statusFraOppdragDTO.vedtaksId, + task, + ) + StegType.IVERKSETT_MOT_FAMILIE_TILBAKE -> opprettTaskIverksettMotTilbake( + statusFraOppdragDTO.behandlingsId, + task.metadata, + ) + StegType.FERDIGSTILLE_BEHANDLING -> opprettFerdigstillBehandling(statusFraOppdragDTO) + else -> error("Neste task er ikke implementert.") + } + } + + return hentNesteStegForNormalFlyt(behandling) + } + + private fun opprettFerdigstillBehandling(statusFraOppdragDTO: StatusFraOppdragDTO) { + val ferdigstillBehandling = FerdigstillBehandlingTask.opprettTask( + søkerIdent = statusFraOppdragDTO.aktørId, + behandlingsId = statusFraOppdragDTO.behandlingsId, + ) + taskRepository.save(ferdigstillBehandling) + } + + private fun opprettTaskIverksettMotTilbake(behandlingsId: Long, metadata: Properties) { + val ferdigstillBehandling = IverksettMotFamilieTilbakeTask.opprettTask( + behandlingsId, + metadata, + ) + taskRepository.save(ferdigstillBehandling) + } + + private fun opprettTaskJournalførVedtaksbrev(vedtakId: Long, gammelTask: Task) { + val task = Task( + type = JournalførVedtaksbrevTask.TASK_STEP_TYPE, + payload = "$vedtakId", + properties = gammelTask.metadata, + ) + taskRepository.save(task) + } + + override fun stegType(): StegType { + return StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI + } + + companion object { + + private val logger = LoggerFactory.getLogger(StatusFraOppdrag::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/StegService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/StegService.kt new file mode 100644 index 000000000..21207cc66 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/StegService.kt @@ -0,0 +1,597 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.PdlPersonKanIkkeBehandlesIFagsystem +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerInstitusjonOgVerge +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.ekstern.restDomene.writeValueAsString +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdFeedService +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.SatsendringService +import no.nav.familie.ba.sak.kjerne.behandling.AutomatiskBeslutningService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.HenleggÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.RestHenleggBehandlingInfo +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatSteg +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.grunnlag.søknad.SøknadGrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.domene.JournalførVedtaksbrevDTO +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.ba.sak.task.DistribuerDokumentDTO +import no.nav.familie.ba.sak.task.dto.IverksettingTaskDTO +import no.nav.familie.prosessering.error.RekjørSenereException +import org.hibernate.exception.ConstraintViolationException +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.util.Properties + +@Service +class StegService( + private val steg: List>, + private val fagsakService: FagsakService, + private val behandlingService: BehandlingService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val beregningService: BeregningService, + private val søknadGrunnlagService: SøknadGrunnlagService, + private val tilgangService: TilgangService, + private val infotrygdFeedService: InfotrygdFeedService, + private val satsendringService: SatsendringService, + private val personopplysningerService: PersonopplysningerService, + private val automatiskBeslutningService: AutomatiskBeslutningService, +) { + + private val stegSuksessMetrics: Map = initStegMetrikker("suksess") + + private val stegFeiletMetrics: Map = initStegMetrikker("feil") + private val stegFunksjonellFeilMetrics: Map = initStegMetrikker("funksjonell-feil") + + @Transactional + fun håndterNyBehandlingOgSendInfotrygdFeed(nyBehandling: NyBehandling): Behandling { + val behandling = håndterNyBehandling(nyBehandling) + if (behandling.type == BehandlingType.FØRSTEGANGSBEHANDLING) { + infotrygdFeedService.sendStartBehandlingTilInfotrygdFeed( + behandling.fagsak.aktør, + ) + } + return behandling + } + + @Transactional + fun håndterNyBehandling(nyBehandling: NyBehandling): Behandling { + when (nyBehandling.behandlingÅrsak) { + BehandlingÅrsak.HELMANUELL_MIGRERING -> validerHelmanuelMigrering(nyBehandling) + BehandlingÅrsak.ENDRE_MIGRERINGSDATO -> validerEndreMigreringsdato(nyBehandling) + else -> Unit + } + + val behandling = behandlingService.opprettBehandling(nyBehandling) + + val barnasIdenter: List = when (nyBehandling.behandlingÅrsak) { + BehandlingÅrsak.FØDSELSHENDELSE, + BehandlingÅrsak.HELMANUELL_MIGRERING, + -> { + nyBehandling.barnasIdenter + } + + else -> when (nyBehandling.behandlingType) { + BehandlingType.FØRSTEGANGSBEHANDLING -> emptyList() + BehandlingType.REVURDERING, + BehandlingType.TEKNISK_ENDRING, + BehandlingType.MIGRERING_FRA_INFOTRYGD, + -> { + if (nyBehandling.behandlingType == BehandlingType.MIGRERING_FRA_INFOTRYGD) { + validerMigreringFraInfotrygd(nyBehandling) + } + hentBarnFraForrigeAvsluttedeBehandling(behandling) + } + + else -> throw Feil(hentUkjentBehandlingTypeOgÅrsakFeilMelding(nyBehandling)) + } + } + + return håndterPersongrunnlag( + behandling, + RegistrerPersongrunnlagDTO( + ident = nyBehandling.søkersIdent, + barnasIdenter = barnasIdenter, + nyMigreringsdato = nyBehandling.nyMigreringsdato, + ), + ) + } + + private fun validerMigreringFraInfotrygd(nyBehandling: NyBehandling) { + if (nyBehandling.behandlingÅrsak != BehandlingÅrsak.ENDRE_MIGRERINGSDATO) { + throw Feil(hentUkjentBehandlingTypeOgÅrsakFeilMelding(nyBehandling)) + } + } + + fun validerEndreMigreringsdato(nyBehandling: NyBehandling) { + check(nyBehandling.behandlingÅrsak == BehandlingÅrsak.ENDRE_MIGRERINGSDATO) + + if (!satsendringService.erFagsakOppdatertMedSisteSatser(fagsakId = nyBehandling.fagsakId)) { + throw FunksjonellFeil("Fagsaken har ikke siste sats. Gjennomfør satsendring før du endrer migreringsdato.") + } + } + + private fun validerHelmanuelMigrering(nyBehandling: NyBehandling) { + val sisteBehandlingSomErVedtatt = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(nyBehandling.fagsakId) + + if (sisteBehandlingSomErVedtatt != null && behandlingService.erLøpende(sisteBehandlingSomErVedtatt)) { + throw FunksjonellFeil( + melding = "Det finnes allerede en vedtatt behandling med løpende utbetalinger på fagsak ${nyBehandling.fagsakId}." + + "Behandling kan ikke opprettes med årsak " + + BehandlingÅrsak.HELMANUELL_MIGRERING.visningsnavn, + frontendFeilmelding = "Det finnes allerede en vedtatt behandling med løpende utbetalinger på fagsak." + + "Behandling kan ikke opprettes med årsak " + + BehandlingÅrsak.HELMANUELL_MIGRERING.visningsnavn, + ) + } + } + + private fun hentBarnFraForrigeAvsluttedeBehandling(behandling: Behandling): List { + val sisteBehandling = hentSisteAvsluttetBehandling(behandling) + return beregningService.finnBarnFraBehandlingMedTilkjentYtelse(sisteBehandling.id) + .mapNotNull { + try { + personopplysningerService.hentPersoninfoEnkel(it) + it.aktivFødselsnummer() + } catch (pdlPersonKanIkkeBehandlesIFagsystem: PdlPersonKanIkkeBehandlesIFagsystem) { + logger.warn("Ignorerer barn fra forrige avsluttede behandling: ${pdlPersonKanIkkeBehandlesIFagsystem.årsak}") + secureLogger.warn("Ignorerer barn ${it.aktivFødselsnummer()} fra forrige avsluttede behandling: ${pdlPersonKanIkkeBehandlesIFagsystem.årsak}") + null + } + } + } + + @Transactional + fun opprettNyBehandlingOgRegistrerPersongrunnlagForFødselhendelse(nyBehandlingHendelse: NyBehandlingHendelse): Behandling { + val fagsak = try { + fagsakService.hentEllerOpprettFagsakForPersonIdent(nyBehandlingHendelse.morsIdent, true, FagsakType.NORMAL) + } catch (exception: Exception) { + if (exception is ConstraintViolationException) { + throw RekjørSenereException( + triggerTid = LocalDateTime.now().plusMinutes(15), + årsak = "Klarte ikke å opprette fagsak på grunn av krasj i databasen, prøver igjen om 15 minutter. Feilmelding: ${exception.message}.", + ) + } + + throw exception + } + + return håndterNyBehandlingOgSendInfotrygdFeed( + NyBehandling( + søkersIdent = nyBehandlingHendelse.morsIdent, + behandlingType = if (fagsak.status == FagsakStatus.LØPENDE) { + BehandlingType.REVURDERING + } else { + BehandlingType.FØRSTEGANGSBEHANDLING + }, + behandlingÅrsak = BehandlingÅrsak.FØDSELSHENDELSE, + skalBehandlesAutomatisk = true, + barnasIdenter = nyBehandlingHendelse.barnasIdenter, + kategori = BehandlingKategori.NASJONAL, // alltid NASJONAL for fødselshendelse + underkategori = BehandlingUnderkategori.ORDINÆR, // alltid ORDINÆR for fødselshendelse + fagsakId = fagsak.id, + ), + ) + } + + @Transactional + fun håndterSøknad( + behandling: Behandling, + restRegistrerSøknad: RestRegistrerSøknad, + ): Behandling = + fullførSøknadsHåndtering(behandling = behandling, restRegistrerSøknad = restRegistrerSøknad) + + private fun fullførSøknadsHåndtering( + behandling: Behandling, + restRegistrerSøknad: RestRegistrerSøknad, + ): Behandling { + val behandlingSteg: RegistrereSøknad = hentBehandlingSteg(StegType.REGISTRERE_SØKNAD) as RegistrereSøknad + val søknadDTO = restRegistrerSøknad.søknad + + val aktivSøknadGrunnlag = søknadGrunnlagService.hentAktiv(behandlingId = behandling.id) + val innsendtSøknad = søknadDTO.writeValueAsString() + + if (aktivSøknadGrunnlag != null && innsendtSøknad == aktivSøknadGrunnlag.søknad) { + return behandling + } + + return håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, restRegistrerSøknad) + } + } + + @Transactional + fun håndterPersongrunnlag( + behandling: Behandling, + registrerPersongrunnlagDTO: RegistrerPersongrunnlagDTO, + ): Behandling { + val behandlingSteg: RegistrerPersongrunnlag = + hentBehandlingSteg(StegType.REGISTRERE_PERSONGRUNNLAG) as RegistrerPersongrunnlag + + return håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, registrerPersongrunnlagDTO) + } + } + + @Transactional + fun håndterFiltreringsreglerForFødselshendelser( + behandling: Behandling, + nyBehandling: NyBehandlingHendelse, + ): Behandling { + val behandlingSteg: FiltreringFødselshendelserSteg = + hentBehandlingSteg(StegType.FILTRERING_FØDSELSHENDELSER) as FiltreringFødselshendelserSteg + + return håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, nyBehandling) + } + } + + @Transactional + fun håndterVilkårsvurdering(behandling: Behandling): Behandling { + val behandlingSteg: VilkårsvurderingSteg = + hentBehandlingSteg(StegType.VILKÅRSVURDERING) as VilkårsvurderingSteg + + val behandlingEtterVilkårsvurdering = håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, "") + } + + return if (behandlingEtterVilkårsvurdering.skalBehandlesAutomatisk) { + håndterBehandlingsresultat(behandlingEtterVilkårsvurdering) + } else { + behandlingEtterVilkårsvurdering + } + } + + @Transactional + fun håndterBehandlingsresultat(behandling: Behandling): Behandling { + val behandlingSteg: BehandlingsresultatSteg = + hentBehandlingSteg(StegType.BEHANDLINGSRESULTAT) as BehandlingsresultatSteg + + val behandlingEtterBehandlingsresultatSteg = håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, "") + } + + return if (behandlingEtterBehandlingsresultatSteg.resultat == Behandlingsresultat.AVSLÅTT && + !behandlingEtterBehandlingsresultatSteg.skalBehandlesAutomatisk + ) { + håndterVurderTilbakekreving( + behandling = behandlingEtterBehandlingsresultatSteg, + ) + } else { + behandlingEtterBehandlingsresultatSteg + } + } + + @Transactional + fun håndterVurderTilbakekreving( + behandling: Behandling, + restTilbakekreving: RestTilbakekreving? = null, + ): Behandling { + val behandlingSteg: VurderTilbakekrevingSteg = + hentBehandlingSteg(StegType.VURDER_TILBAKEKREVING) as VurderTilbakekrevingSteg + + return håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, restTilbakekreving) + } + } + + @Transactional + fun håndterSendTilBeslutter(behandling: Behandling, behandlendeEnhet: String): Behandling { + val behandlingSteg: SendTilBeslutter = hentBehandlingSteg(StegType.SEND_TIL_BESLUTTER) as SendTilBeslutter + + val behandlingEtterBeslutterSteg = håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, behandlendeEnhet) + } + + if (automatiskBeslutningService.behandlingSkalAutomatiskBesluttes(behandling)) { + return håndterBeslutningForVedtak( + behandlingEtterBeslutterSteg, + RestBeslutningPåVedtak(Beslutning.GODKJENT), + ) + } + return behandlingEtterBeslutterSteg + } + + @Transactional + fun håndterBeslutningForVedtak( + behandling: Behandling, + restBeslutningPåVedtak: RestBeslutningPåVedtak, + ): Behandling { + val behandlingSteg: BeslutteVedtak = + hentBehandlingSteg(StegType.BESLUTTE_VEDTAK) as BeslutteVedtak + + return håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, restBeslutningPåVedtak) + } + } + + @Transactional + fun håndterHenleggBehandling( + behandling: Behandling, + henleggBehandlingInfo: RestHenleggBehandlingInfo, + ): Behandling { + val behandlingSteg: HenleggBehandling = + hentBehandlingSteg(StegType.HENLEGG_BEHANDLING) as HenleggBehandling + + val behandlingEtterHenleggeSteg = håndterSteg( + behandling = behandling, + behandlingSteg = behandlingSteg, + henleggÅrsak = henleggBehandlingInfo.årsak, + ) { + behandlingSteg.utførStegOgAngiNeste(behandling, henleggBehandlingInfo) + } + + return håndterFerdigstillBehandling( + behandling = behandlingEtterHenleggeSteg, + ) + } + + @Transactional + fun håndterIverksettMotØkonomi(behandling: Behandling, iverksettingTaskDTO: IverksettingTaskDTO): Behandling { + val behandlingSteg: IverksettMotOppdrag = + hentBehandlingSteg(StegType.IVERKSETT_MOT_OPPDRAG) as IverksettMotOppdrag + + return håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, iverksettingTaskDTO) + } + } + + @Transactional + fun håndterStatusFraØkonomi( + behandling: Behandling, + statusFraOppdragMedTask: StatusFraOppdragMedTask, + ): Behandling { + val behandlingSteg: StatusFraOppdrag = + hentBehandlingSteg(StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI) as StatusFraOppdrag + + return håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, statusFraOppdragMedTask) + } + } + + @Transactional + fun håndterIverksettMotFamilieTilbake(behandling: Behandling, metadata: Properties): Behandling { + val behandlingSteg: IverksettMotFamilieTilbake = + hentBehandlingSteg(StegType.IVERKSETT_MOT_FAMILIE_TILBAKE) as IverksettMotFamilieTilbake + + return håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, IverksettMotFamilieTilbakeData(metadata)) + } + } + + @Transactional + fun håndterJournalførVedtaksbrev( + behandling: Behandling, + journalførVedtaksbrevDTO: JournalførVedtaksbrevDTO, + ): Behandling { + val behandlingSteg: JournalførVedtaksbrev = + hentBehandlingSteg(StegType.JOURNALFØR_VEDTAKSBREV) as JournalførVedtaksbrev + + return håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, journalførVedtaksbrevDTO) + } + } + + @Transactional + fun håndterDistribuerVedtaksbrev( + behandling: Behandling, + distribuerDokumentDTO: DistribuerDokumentDTO, + ): Behandling { + val behandlingSteg: DistribuerVedtaksbrev = + hentBehandlingSteg(StegType.DISTRIBUER_VEDTAKSBREV) as DistribuerVedtaksbrev + + return håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, distribuerDokumentDTO) + } + } + + @Transactional + fun håndterFerdigstillBehandling(behandling: Behandling): Behandling { + val behandlingSteg: FerdigstillBehandling = + hentBehandlingSteg(StegType.FERDIGSTILLE_BEHANDLING) as FerdigstillBehandling + + return håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, "") + } + } + + @Transactional + fun håndterRegistrerVerge(behandling: Behandling, vergeInfo: RestRegistrerInstitusjonOgVerge): Behandling { + val behandlingSteg: RegistrerInstitusjonOgVerge = + hentBehandlingSteg(StegType.REGISTRERE_INSTITUSJON_OG_VERGE) as RegistrerInstitusjonOgVerge + + return håndterSteg(behandling, behandlingSteg) { + behandlingSteg.utførStegOgAngiNeste(behandling, vergeInfo) + } + } + + // Generelle stegmetoder + private fun håndterSteg( + behandling: Behandling, + behandlingSteg: BehandlingSteg<*>, + henleggÅrsak: HenleggÅrsak? = null, + utførendeSteg: () -> StegType, + ): Behandling { + try { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} håndterer ${behandlingSteg.stegType()} på behandling ${behandling.id}") + tilgangService.validerTilgangTilBehandling( + behandlingId = behandling.id, + event = AuditLoggerEvent.UPDATE, + ) + if (behandling.erManuellMigrering() && behandlingSteg.stegType() == StegType.BESLUTTE_VEDTAK) { + verifiserBeslutteVedtakForManuellMigrering(behandlingSteg) + } else { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = behandlingSteg.stegType().tillattFor.minByOrNull { it.nivå } + ?: throw Feil( + "${SikkerhetContext.hentSaksbehandlerNavn()} prøver " + + "å utføre steg ${behandlingSteg.stegType()} som ikke er tillatt av noen.", + ), + handling = "utføre steg ${behandlingSteg.stegType().displayName()}", + ) + } + + validerBehandlingIkkeSattPåVent(behandling, behandlingSteg) + + if (behandling.steg == SISTE_STEG) { + throw FunksjonellFeil("Behandling med id ${behandling.id} er avsluttet og stegprosessen kan ikke gjenåpnes") + } + + if (behandlingSteg.stegType().erSaksbehandlerSteg() && behandlingSteg.stegType() + .kommerEtter(behandling.steg) + ) { + throw FunksjonellFeil( + "${SikkerhetContext.hentSaksbehandlerNavn()} prøver å utføre steg '${ + behandlingSteg.stegType() + .displayName() + }', men behandlingen er på steg '${behandling.steg.displayName()}'", + ) + } + + val erTekniskVedlikeholdHenleggelse = + behandlingSteg.stegType() == StegType.HENLEGG_BEHANDLING && henleggÅrsak == HenleggÅrsak.TEKNISK_VEDLIKEHOLD + if (behandling.steg == StegType.BESLUTTE_VEDTAK && behandlingSteg.stegType() != StegType.BESLUTTE_VEDTAK && !erTekniskVedlikeholdHenleggelse) { + throw FunksjonellFeil( + "Behandlingen er på steg '${behandling.steg.displayName()}', " + + "og er da låst for alle andre type endringer.", + ) + } + + behandlingSteg.preValiderSteg(behandling, this) + val nesteSteg = utførendeSteg() + behandlingSteg.postValiderSteg(behandling) + val behandlingEtterUtførtSteg = behandlingHentOgPersisterService.hent(behandling.id) + + stegSuksessMetrics[behandlingSteg.stegType()]?.increment() + + if (!nesteSteg.erGyldigIKombinasjonMedStatus(behandlingEtterUtførtSteg.status)) { + throw Feil("Steg '${nesteSteg.displayName()}' kan ikke settes på behandling i kombinasjon med status ${behandlingEtterUtførtSteg.status}") + } + + val returBehandling = + behandlingService.leggTilStegPåBehandlingOgSettTidligereStegSomUtført( + behandlingId = behandling.id, + steg = nesteSteg, + ) + + if (nesteSteg == SISTE_STEG) { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} er ferdig med stegprosess på behandling $behandling") + } else { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} har håndtert ${behandlingSteg.stegType()} på behandling $behandling. Neste steg er $nesteSteg") + } + return returBehandling + } catch (exception: Exception) { + when (exception) { + is RekjørSenereException -> { + stegFunksjonellFeilMetrics[behandlingSteg.stegType()]?.increment() + logger.info("Steg '${behandlingSteg.stegType()}' har trigget rekjøring senere på behandling $behandling. Årsak: ${exception.årsak}") + } + + is FunksjonellFeil -> { + stegFunksjonellFeilMetrics[behandlingSteg.stegType()]?.increment() + logger.info("Håndtering av stegtype '${behandlingSteg.stegType()}' feilet på grunn av funksjonell feil på behandling $behandling. Melding: ${exception.melding}") + } + + else -> { + stegFeiletMetrics[behandlingSteg.stegType()]?.increment() + logger.info("Håndtering av stegtype '${behandlingSteg.stegType()}' feilet på behandling $behandling.") + secureLogger.info( + "Håndtering av stegtype '${behandlingSteg.stegType()}' feilet på behandling $behandling.", + exception, + ) + } + } + + throw exception + } + } + + private fun validerBehandlingIkkeSattPåVent( + behandling: Behandling, + behandlingSteg: BehandlingSteg<*>, + ) { + if (behandling.status == BehandlingStatus.SATT_PÅ_VENT) { + throw FunksjonellFeil( + "${SikkerhetContext.hentSaksbehandlerNavn()} prøver å utføre steg " + + behandlingSteg.stegType() + + " på behandling ${behandling.id} som er på vent.", + ) + } + if (behandling.status == BehandlingStatus.SATT_PÅ_MASKINELL_VENT) { + throw FunksjonellFeil( + "${SikkerhetContext.hentSaksbehandlerNavn()} prøver å utføre steg " + + behandlingSteg.stegType() + + " på behandling ${behandling.id} som er på maskinell vent.", + ) + } + } + + private fun verifiserBeslutteVedtakForManuellMigrering(behandlingSteg: BehandlingSteg<*>) { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "utføre steg ${behandlingSteg.stegType().displayName()}", + ) + } + + fun hentBehandlingSteg(stegType: StegType): BehandlingSteg<*>? { + return steg.firstOrNull { it.stegType() == stegType } + } + + private fun hentSisteAvsluttetBehandling(behandling: Behandling): Behandling { + return behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId = behandling.fagsak.id) + ?: behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(behandling.fagsak.id) + ?: throw Feil( + "Forsøker å opprette en ${behandling.type.visningsnavn} " + + "med årsak ${behandling.opprettetÅrsak.visningsnavn}, " + + "men kan ikke finne tidligere behandling på fagsak ${behandling.fagsak.id}", + ) + } + + private fun initStegMetrikker(type: String): Map { + return steg.associate { + it.stegType() to Metrics.counter( + "behandling.steg.$type", + "steg", + it.stegType().name, + "beskrivelse", + it.stegType().rekkefølge.toString() + " " + it.stegType().displayName(), + ) + } + } + + companion object { + + private val logger = LoggerFactory.getLogger(StegService::class.java) + private fun hentUkjentBehandlingTypeOgÅrsakFeilMelding(nyBehandling: NyBehandling) = + "Ukjent oppførsel ved opprettelse av ny behandling med årsak " + + "${nyBehandling.behandlingÅrsak.visningsnavn} og " + + "type ${nyBehandling.behandlingType.visningsnavn}." + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/TilbakestillBehandlingService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/TilbakestillBehandlingService.kt new file mode 100644 index 000000000..35ae2f354 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/TilbakestillBehandlingService.kt @@ -0,0 +1,100 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.VilkårsvurderingForNyBehandlingService +import no.nav.familie.ba.sak.kjerne.tilbakekreving.TilbakekrevingService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeHentOgPersisterService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class TilbakestillBehandlingService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val behandlingService: BehandlingService, + private val beregningService: BeregningService, + private val vedtaksperiodeHentOgPersisterService: VedtaksperiodeHentOgPersisterService, + private val vedtakRepository: VedtakRepository, + private val tilbakekrevingService: TilbakekrevingService, + private val vilkårsvurderingForNyBehandlingService: VilkårsvurderingForNyBehandlingService, +) { + + @Transactional + fun initierOgSettBehandlingTilVilkårsvurdering( + behandling: Behandling, + bekreftEndringerViaFrontend: Boolean = true, + ) { + vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling, + bekreftEndringerViaFrontend = bekreftEndringerViaFrontend, + forrigeBehandlingSomErVedtatt = behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt( + behandling, + ), + ) + + val vedtak = vedtakRepository.findByBehandlingAndAktiv(behandlingId = behandling.id) + + beregningService.slettTilkjentYtelseForBehandling(behandlingId = behandling.id) + vedtaksperiodeHentOgPersisterService.slettVedtaksperioderFor(vedtak = vedtak) + + behandlingService.leggTilStegPåBehandlingOgSettTidligereStegSomUtført( + behandlingId = behandling.id, + steg = StegType.VILKÅRSVURDERING, + ) + tilbakekrevingService.slettTilbakekrevingPåBehandling(behandling.id) + + vedtakRepository.saveAndFlush(vedtak) + } + + @Transactional + fun tilbakestillBehandlingTilVilkårsvurdering(behandling: Behandling) { + if (behandling.status != BehandlingStatus.UTREDES && behandling.status != BehandlingStatus.SATT_PÅ_VENT) { + throw Feil("Prøver å tilbakestille $behandling, men den er avsluttet eller låst for endringer") + } + + beregningService.slettTilkjentYtelseForBehandling(behandlingId = behandling.id) + val vedtak = vedtakRepository.findByBehandlingAndAktivOptional(behandlingId = behandling.id) + vedtak?.let { vedtaksperiodeHentOgPersisterService.slettVedtaksperioderFor(vedtak = vedtak) } + tilbakekrevingService.slettTilbakekrevingPåBehandling(behandling.id) + + behandlingService.leggTilStegPåBehandlingOgSettTidligereStegSomUtført( + behandlingId = behandling.id, + steg = StegType.VILKÅRSVURDERING, + ) + } + + @Transactional + fun tilbakestillDataTilVilkårsvurderingssteg(behandling: Behandling) { + vedtaksperiodeHentOgPersisterService.slettVedtaksperioderFor( + vedtak = vedtakRepository.findByBehandlingAndAktiv( + behandlingId = behandling.id, + ), + ) + } + + /** + * Når et vilkår vurderes (endres) vil vi resette steget og slette data som blir generert senere i løypa + */ + @Transactional + fun resettStegVedEndringPåVilkår(behandlingId: Long): Behandling { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + + vedtaksperiodeHentOgPersisterService.slettVedtaksperioderFor( + vedtak = vedtakRepository.findByBehandlingAndAktiv( + behandling.id, + ), + ) + tilbakekrevingService.slettTilbakekrevingPåBehandling(behandling.id) + behandlingHentOgPersisterService.lagreEllerOppdater(behandling.apply { resultat = Behandlingsresultat.IKKE_VURDERT }) + return behandlingService.leggTilStegPåBehandlingOgSettTidligereStegSomUtført( + behandlingId = behandling.id, + steg = StegType.VILKÅRSVURDERING, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/TilbakestillBehandlingTilBehandlingsresultatService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/TilbakestillBehandlingTilBehandlingsresultatService.kt new file mode 100644 index 000000000..6e4205354 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/TilbakestillBehandlingTilBehandlingsresultatService.kt @@ -0,0 +1,48 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.tilbakekreving.TilbakekrevingService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeHentOgPersisterService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class TilbakestillBehandlingTilBehandlingsresultatService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val behandlingService: BehandlingService, + private val vedtaksperiodeHentOgPersisterService: VedtaksperiodeHentOgPersisterService, + private val vedtakRepository: VedtakRepository, + private val tilbakekrevingService: TilbakekrevingService, +) { + /** + * Når en andel vurderes (endres) vil vi resette steget og slette data som blir generert senere i løypa + */ + @Transactional + fun tilbakestillBehandlingTilBehandlingsresultat(behandlingId: Long): Behandling { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + + if (behandling.erTilbakestiltTilBehandlingsresultat()) { + return behandling + } + + vedtaksperiodeHentOgPersisterService.slettVedtaksperioderFor( + vedtak = vedtakRepository.findByBehandlingAndAktiv( + behandlingId, + ), + ) + tilbakekrevingService.slettTilbakekrevingPåBehandling(behandlingId) + return behandlingService.leggTilStegPåBehandlingOgSettTidligereStegSomUtført( + behandlingId = behandlingId, + steg = StegType.BEHANDLINGSRESULTAT, + ) + } +} + +private fun Behandling.erTilbakestiltTilBehandlingsresultat(): Boolean { + val gjeldendeSteg = this.behandlingStegTilstand.last() + return gjeldendeSteg.behandlingSteg == StegType.BEHANDLINGSRESULTAT && + gjeldendeSteg.behandlingStegStatus == BehandlingStegStatus.IKKE_UTFØRT +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/Vilk\303\245rsvurderingSteg.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/Vilk\303\245rsvurderingSteg.kt" new file mode 100644 index 000000000..8725244e3 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/Vilk\303\245rsvurderingSteg.kt" @@ -0,0 +1,88 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.BehandlingstemaService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement.TilpassKompetanserTilRegelverkService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.VilkårsvurderingForNyBehandlingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.valider18ÅrsVilkårEksistererFraFødselsdato +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.validerIkkeBlandetRegelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.validerIngenVilkårSattEtterSøkersDød +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class VilkårsvurderingSteg( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val behandlingstemaService: BehandlingstemaService, + private val vilkårService: VilkårService, + private val beregningService: BeregningService, + private val persongrunnlagService: PersongrunnlagService, + private val tilbakestillBehandlingService: TilbakestillBehandlingService, + private val tilpassKompetanserTilRegelverkService: TilpassKompetanserTilRegelverkService, + private val vilkårsvurderingForNyBehandlingService: VilkårsvurderingForNyBehandlingService, +) : BehandlingSteg { + + override fun preValiderSteg(behandling: Behandling, stegService: StegService?) { + val søkerOgBarn = persongrunnlagService.hentSøkerOgBarnPåBehandlingThrows(behandling.id) + + if (behandling.opprettetÅrsak == BehandlingÅrsak.DØDSFALL_BRUKER) { + val vilkårsvurdering = vilkårService.hentVilkårsvurderingThrows(behandling.id) + validerIngenVilkårSattEtterSøkersDød( + søkerOgBarn = søkerOgBarn, + vilkårsvurdering = vilkårsvurdering, + ) + } + + vilkårService.hentVilkårsvurdering(behandling.id)?.apply { + validerIkkeBlandetRegelverk( + søkerOgBarn = søkerOgBarn, + vilkårsvurdering = this, + ) + + valider18ÅrsVilkårEksistererFraFødselsdato( + søkerOgBarn = søkerOgBarn, + vilkårsvurdering = this, + behandling = behandling, + ) + } + } + + @Transactional + override fun utførStegOgAngiNeste( + behandling: Behandling, + data: String, + ): StegType { + val personopplysningGrunnlag = persongrunnlagService.hentAktivThrows(behandling.id) + + if (behandling.opprettetÅrsak == BehandlingÅrsak.FØDSELSHENDELSE) { + vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt( + behandling, + ), + ) + } + + tilbakestillBehandlingService.tilbakestillDataTilVilkårsvurderingssteg(behandling) + beregningService.genererTilkjentYtelseFraVilkårsvurdering(behandling, personopplysningGrunnlag) + + tilpassKompetanserTilRegelverkService.tilpassKompetanserTilRegelverk(BehandlingId(behandling.id)) + + behandlingstemaService.oppdaterBehandlingstema( + behandling = behandlingHentOgPersisterService.hent(behandlingId = behandling.id), + ) + + return hentNesteStegForNormalFlyt(behandling) + } + + override fun stegType(): StegType { + return StegType.VILKÅRSVURDERING + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/VurderTilbakekrevingSteg.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/VurderTilbakekrevingSteg.kt new file mode 100644 index 000000000..91e0bdfb4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/VurderTilbakekrevingSteg.kt @@ -0,0 +1,31 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.simulering.SimuleringService +import no.nav.familie.ba.sak.kjerne.tilbakekreving.TilbakekrevingService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class VurderTilbakekrevingSteg( + val featureToggleService: FeatureToggleService, + val tilbakekrevingService: TilbakekrevingService, + val simuleringService: SimuleringService, +) : BehandlingSteg { + + @Transactional + override fun utførStegOgAngiNeste(behandling: Behandling, data: RestTilbakekreving?): StegType { + if (!tilbakekrevingService.søkerHarÅpenTilbakekreving(behandling.fagsak.id)) { + tilbakekrevingService.validerRestTilbakekreving(data, behandling.id) + if (data != null) { + tilbakekrevingService.lagreTilbakekreving(data, behandling.id) + } + } + + return hentNesteStegForNormalFlyt(behandling) + } + + override fun stegType(): StegType = StegType.VURDER_TILBAKEKREVING +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/domene/Journalf\303\270rVedtaksbrevDTO.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/domene/Journalf\303\270rVedtaksbrevDTO.kt" new file mode 100644 index 000000000..7f202f672 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/domene/Journalf\303\270rVedtaksbrevDTO.kt" @@ -0,0 +1,24 @@ +package no.nav.familie.ba.sak.kjerne.steg.domene + +import no.nav.familie.kontrakter.felles.BrukerIdType +import no.nav.familie.prosessering.domene.Task + +data class JournalførVedtaksbrevDTO(val vedtakId: Long, val task: Task) + +data class MottakerInfo( + val brukerId: String, + val brukerIdType: BrukerIdType?, + val erInstitusjonVerge: Boolean, // Feltet brukes kun for institiusjon med verge + val navn: String? = null, // Feltet brukes for å sette riktig mottaker navn når brev sendes både til verge og bruker + val manuellAdresseInfo: ManuellAdresseInfo? = null, +) + +fun MottakerInfo.toList() = listOf(this) + +data class ManuellAdresseInfo( + val adresselinje1: String, + val adresselinje2: String? = null, + val postnummer: String, + val poststed: String, + val landkode: String, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/E\303\270sSkjemaerForNyBehandlingService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/E\303\270sSkjemaerForNyBehandlingService.kt" new file mode 100644 index 000000000..12e7c7afc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/E\303\270sSkjemaerForNyBehandlingService.kt" @@ -0,0 +1,32 @@ +package no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling + +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.KompetanseService +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløpService +import no.nav.familie.ba.sak.kjerne.eøs.valutakurs.ValutakursService +import org.springframework.stereotype.Service + +@Service +class EøsSkjemaerForNyBehandlingService( + private val kompetanseService: KompetanseService, + private val utenlandskPeriodebeløpService: UtenlandskPeriodebeløpService, + private val valutakursService: ValutakursService, +) { + + fun kopierEøsSkjemaer(behandlingId: BehandlingId, forrigeBehandlingSomErVedtattId: BehandlingId?) { + if (forrigeBehandlingSomErVedtattId != null) { + kompetanseService.kopierOgErstattKompetanser( + fraBehandlingId = forrigeBehandlingSomErVedtattId, + tilBehandlingId = behandlingId, + ) + utenlandskPeriodebeløpService.kopierOgErstattUtenlandskPeriodebeløp( + fraBehandlingId = forrigeBehandlingSomErVedtattId, + tilBehandlingId = behandlingId, + ) + valutakursService.kopierOgErstattValutakurser( + fraBehandlingId = forrigeBehandlingSomErVedtattId, + tilBehandlingId = behandlingId, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/PersonopplysningGrunnlagForNyBehandlingService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/PersonopplysningGrunnlagForNyBehandlingService.kt new file mode 100644 index 000000000..acba21d5d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/PersonopplysningGrunnlagForNyBehandlingService.kt @@ -0,0 +1,85 @@ +package no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.skalTaMedBarnFraForrigeBehandling +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import org.springframework.stereotype.Service + +@Service +class PersonopplysningGrunnlagForNyBehandlingService( + private val personidentService: PersonidentService, + private val beregningService: BeregningService, + private val persongrunnlagService: PersongrunnlagService, +) { + + fun opprettKopiEllerNyttPersonopplysningGrunnlag( + behandling: Behandling, + forrigeBehandlingSomErVedtatt: Behandling?, + søkerIdent: String, + barnasIdenter: List, + ) { + if (behandling.erSatsendring()) { + if (forrigeBehandlingSomErVedtatt == null) { + throw Feil("Vi kan ikke kjøre satsendring dersom det ikke finnes en tidligere behandling. Behandling: ${behandling.id}") + } + opprettKopiAvPersonopplysningGrunnlag(behandling, forrigeBehandlingSomErVedtatt, søkerIdent) + } else { + opprettPersonopplysningGrunnlag(behandling, forrigeBehandlingSomErVedtatt, søkerIdent, barnasIdenter) + } + } + + private fun opprettKopiAvPersonopplysningGrunnlag( + behandling: Behandling, + forrigeBehandlingSomErVedtatt: Behandling, + søkerIdent: String, + ) { + val søkerAktør = personidentService.hentOgLagreAktør(søkerIdent, true) + + val barnaAktør = finnBarnMedTilkjentYtelseIForrigeBehandling(behandling, forrigeBehandlingSomErVedtatt) + + val personopplysningGrunnlag = + persongrunnlagService.hentAktivThrows(forrigeBehandlingSomErVedtatt.id) + .tilKopiForNyBehandling(behandling, listOf(søkerAktør).plus(barnaAktør)) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + } + + private fun opprettPersonopplysningGrunnlag( + behandling: Behandling, + forrigeBehandlingSomErVedtatt: Behandling?, + søkerIdent: String, + barnasIdenter: List, + ) { + val aktør = personidentService.hentOgLagreAktør(søkerIdent, true) + val barnaAktør = personidentService.hentOgLagreAktørIder(barnasIdenter, true) + + val målform = forrigeBehandlingSomErVedtatt + ?.let { persongrunnlagService.hentSøkersMålform(behandlingId = it.id) } + ?: Målform.NB + + val barnMedTilkjentYtelseIForrigeBehandling = + finnBarnMedTilkjentYtelseIForrigeBehandling(behandling, forrigeBehandlingSomErVedtatt) + + persongrunnlagService.hentOgLagreSøkerOgBarnINyttGrunnlag( + aktør = aktør, + barnFraInneværendeBehandling = barnaAktør, + barnFraForrigeBehandling = barnMedTilkjentYtelseIForrigeBehandling, + behandling = behandling, + målform = målform, + ) + } + + private fun finnBarnMedTilkjentYtelseIForrigeBehandling( + behandling: Behandling, + forrigeBehandlingSomErVedtatt: Behandling?, + ): List = + if (skalTaMedBarnFraForrigeBehandling(behandling) && forrigeBehandlingSomErVedtatt != null) { + beregningService.finnBarnFraBehandlingMedTilkjentYtelse(behandlingId = forrigeBehandlingSomErVedtatt.id) + } else { + emptyList() + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/Vilk\303\245rsvurderingForNyBehandlingService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/Vilk\303\245rsvurderingForNyBehandlingService.kt" new file mode 100644 index 000000000..8316ae80a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/Vilk\303\245rsvurderingForNyBehandlingService.kt" @@ -0,0 +1,292 @@ +package no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.BehandlingstemaService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingMetrics +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingUtils +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class VilkårsvurderingForNyBehandlingService( + private val vilkårsvurderingService: VilkårsvurderingService, + private val behandlingService: BehandlingService, + private val persongrunnlagService: PersongrunnlagService, + private val behandlingstemaService: BehandlingstemaService, + private val endretUtbetalingAndelService: EndretUtbetalingAndelService, + private val vilkårsvurderingMetrics: VilkårsvurderingMetrics, + private val andelerTilkjentYtelseRepository: AndelTilkjentYtelseRepository, +) { + + fun opprettVilkårsvurderingUtenomHovedflyt( + behandling: Behandling, + forrigeBehandlingSomErVedtatt: Behandling?, + nyMigreringsdato: LocalDate? = null, + ) { + when (behandling.opprettetÅrsak) { + BehandlingÅrsak.ENDRE_MIGRERINGSDATO -> { + genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErVedtatt + ?: throw Feil("Kan ikke opprette behandling med årsak 'Endre migreringsdato' hvis det ikke finnes en tidligere behandling som er iverksatt"), + nyMigreringsdato = nyMigreringsdato + ?: throw Feil("Kan ikke opprette behandling med årsak 'Endre migreringsdato' uten en migreringsdato"), + ) + // Lagre ned migreringsdato + behandlingService.lagreNedMigreringsdato(nyMigreringsdato, behandling) + } + + BehandlingÅrsak.HELMANUELL_MIGRERING -> { + genererVilkårsvurderingForHelmanuellMigrering( + behandling = behandling, + nyMigreringsdato = nyMigreringsdato + ?: throw Feil("Kan ikke opprette behandling med årsak 'Helmanuell migrering' uten en migreringsdato"), + ) + // Lagre ned migreringsdato + behandlingService.lagreNedMigreringsdato(nyMigreringsdato, behandling) + } + + BehandlingÅrsak.SATSENDRING -> { + genererVilkårsvurderingForSatsendring( + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErVedtatt + ?: throw Feil("Kan ikke opprette behandling med årsak 'Satsendring' hvis det ikke finnes en tidligere behandling"), + inneværendeBehandling = behandling, + ) + } + + !in listOf(BehandlingÅrsak.SØKNAD, BehandlingÅrsak.FØDSELSHENDELSE) -> { + initierVilkårsvurderingForBehandling( + behandling = behandling, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErVedtatt, + ) + } + + else -> logger.info( + "Perioder i vilkårsvurdering generer ikke automatisk for " + + behandling.opprettetÅrsak.visningsnavn, + ) + } + } + + fun genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + behandling: Behandling, + forrigeBehandlingSomErVedtatt: Behandling, + nyMigreringsdato: LocalDate, + ): Vilkårsvurdering { + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling).apply { + personResultater = + VilkårsvurderingForNyBehandlingUtils( + personopplysningGrunnlag = persongrunnlagService.hentAktivThrows( + behandling.id, + ), + ).lagPersonResultaterForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + vilkårsvurdering = this, + nyMigreringsdato = nyMigreringsdato, + forrigeBehandlingVilkårsvurdering = hentVilkårsvurderingThrows( + forrigeBehandlingSomErVedtatt.id, + feilmelding = + "Kan ikke kopiere vilkårsvurdering fra forrige behandling ${forrigeBehandlingSomErVedtatt.id}" + + "til behandling ${behandling.id}", + ), + ) + } + return vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = vilkårsvurdering) + } + + fun genererVilkårsvurderingForHelmanuellMigrering( + behandling: Behandling, + nyMigreringsdato: LocalDate, + ): Vilkårsvurdering { + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling).apply { + personResultater = VilkårsvurderingForNyBehandlingUtils( + personopplysningGrunnlag = persongrunnlagService.hentAktivThrows(behandling.id), + ).lagPersonResultaterForHelmanuellMigrering( + vilkårsvurdering = this, + nyMigreringsdato = nyMigreringsdato, + ) + } + return vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = vilkårsvurdering) + } + + private fun genererVilkårsvurderingForSatsendring( + forrigeBehandlingSomErVedtatt: Behandling, + inneværendeBehandling: Behandling, + ): Vilkårsvurdering { + val personopplysningGrunnlag = persongrunnlagService.hentAktivThrows(inneværendeBehandling.id) + + val forrigeBehandlingVilkårsvurdering = hentVilkårsvurderingThrows(forrigeBehandlingSomErVedtatt.id) + + val nyVilkårsvurdering = + forrigeBehandlingVilkårsvurdering.tilKopiForNyBehandling( + nyBehandling = inneværendeBehandling, + personopplysningGrunnlag, + ) + + endretUtbetalingAndelService.kopierEndretUtbetalingAndelFraForrigeBehandling( + behandling = inneværendeBehandling, + forrigeBehandling = forrigeBehandlingSomErVedtatt, + ) + + return vilkårsvurderingService.lagreNyOgDeaktiverGammel(nyVilkårsvurdering) + } + + fun initierVilkårsvurderingForBehandling( + behandling: Behandling, + bekreftEndringerViaFrontend: Boolean, + forrigeBehandlingSomErVedtatt: Behandling?, + ): Vilkårsvurdering { + val personopplysningGrunnlag = persongrunnlagService.hentAktivThrows(behandling.id) + + validerAtFødselshendelseInneholderMinstEttBarn(behandling, personopplysningGrunnlag) + + val aktivVilkårsvurdering = hentVilkårsvurdering(behandling.id) + + val initiellVilkårsvurdering = + VilkårsvurderingForNyBehandlingUtils(personopplysningGrunnlag = personopplysningGrunnlag).genererInitiellVilkårsvurdering( + behandling = behandling, + barnaAktørSomAlleredeErVurdert = aktivVilkårsvurdering?.personResultater?.mapNotNull { + personopplysningGrunnlag.barna.firstOrNull { barn -> barn.aktør == it.aktør } + }?.filter { it.type == PersonType.BARN }?.map { it.aktør } ?: emptyList(), + ) + + tellMetrikkerForFødselshendelse( + aktivVilkårsvurdering = aktivVilkårsvurdering, + behandling = behandling, + initiellVilkårsvurdering = initiellVilkårsvurdering, + ) + + val løpendeUnderkategori = behandlingstemaService.hentLøpendeUnderkategori(behandling.fagsak.id) + + val finnesVilkårsvurderingPåInneværendeBehandling = aktivVilkårsvurdering != null + val førsteVilkårsvurderingPåBehandlingOgFinnesTidligereVedtattBehandling = + forrigeBehandlingSomErVedtatt != null && !finnesVilkårsvurderingPåInneværendeBehandling + + return if (førsteVilkårsvurderingPåBehandlingOgFinnesTidligereVedtattBehandling) { + genererVilkårsvurderingFraForrigeVedtatteBehandling( + initiellVilkårsvurdering = initiellVilkårsvurdering, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErVedtatt!!, + behandling = behandling, + personopplysningGrunnlag = personopplysningGrunnlag, + løpendeUnderkategori = løpendeUnderkategori, + ) + } else if (finnesVilkårsvurderingPåInneværendeBehandling) { + genererNyVilkårsvurderingForBehandling( + initiellVilkårsvurdering = initiellVilkårsvurdering, + aktivVilkårsvurdering = aktivVilkårsvurdering!!, + løpendeUnderkategori = løpendeUnderkategori, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErVedtatt, + bekreftEndringerViaFrontend = bekreftEndringerViaFrontend, + ) + } else { + vilkårsvurderingService.lagreInitielt(initiellVilkårsvurdering) + } + } + + private fun genererNyVilkårsvurderingForBehandling( + initiellVilkårsvurdering: Vilkårsvurdering, + aktivVilkårsvurdering: Vilkårsvurdering, + løpendeUnderkategori: BehandlingUnderkategori?, + forrigeBehandlingSomErVedtatt: Behandling?, + bekreftEndringerViaFrontend: Boolean, + ): Vilkårsvurdering { + val (initieltSomErOppdatert, aktivtSomErRedusert) = VilkårsvurderingUtils.flyttResultaterTilInitielt( + initiellVilkårsvurdering = initiellVilkårsvurdering, + aktivVilkårsvurdering = aktivVilkårsvurdering, + løpendeUnderkategori = løpendeUnderkategori, + aktørerMedUtvidetAndelerIForrigeBehandling = finnAktørerMedUtvidetBarnetrygdIForrigeBehandling(forrigeBehandlingSomErVedtatt), + ) + + if (aktivtSomErRedusert.personResultater.isNotEmpty() && !bekreftEndringerViaFrontend) { + throw FunksjonellFeil( + melding = "Saksbehandler forsøker å fjerne vilkår fra vilkårsvurdering", + frontendFeilmelding = VilkårsvurderingUtils.lagFjernAdvarsel(aktivtSomErRedusert.personResultater), + ) + } + return vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = initieltSomErOppdatert) + } + + /*** + * Utvidet vilkår kan kun kopieres med fra forrige behandling hvis det finnes utbetaling av utvidet barnetrygd i forrige behandling + */ + private fun finnAktørerMedUtvidetBarnetrygdIForrigeBehandling(forrigeBehandlingSomErVedtatt: Behandling?): List = + forrigeBehandlingSomErVedtatt?.let { + val forrigeAndeler = + andelerTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(forrigeBehandlingSomErVedtatt.id) + + finnAktørerMedUtvidetFraAndeler(andeler = forrigeAndeler) + } ?: emptyList() + + private fun tellMetrikkerForFødselshendelse( + aktivVilkårsvurdering: Vilkårsvurdering?, + behandling: Behandling, + initiellVilkårsvurdering: Vilkårsvurdering, + ) { + if (førstegangskjøringAvVilkårsvurdering(aktivVilkårsvurdering) && behandling.opprettetÅrsak == BehandlingÅrsak.FØDSELSHENDELSE) { + vilkårsvurderingMetrics.tellMetrikker(initiellVilkårsvurdering) + } + } + + private fun validerAtFødselshendelseInneholderMinstEttBarn( + behandling: Behandling, + personopplysningGrunnlag: PersonopplysningGrunnlag, + ) { + if (behandling.skalBehandlesAutomatisk && personopplysningGrunnlag.barna.isEmpty()) { + throw IllegalStateException("PersonopplysningGrunnlag for fødselshendelse skal inneholde minst ett barn") + } + } + + private fun genererVilkårsvurderingFraForrigeVedtatteBehandling( + initiellVilkårsvurdering: Vilkårsvurdering, + forrigeBehandlingSomErVedtatt: Behandling, + behandling: Behandling, + personopplysningGrunnlag: PersonopplysningGrunnlag, + løpendeUnderkategori: BehandlingUnderkategori?, + ): Vilkårsvurdering { + val vilkårsvurdering = VilkårsvurderingForNyBehandlingUtils( + personopplysningGrunnlag = personopplysningGrunnlag, + ).genererVilkårsvurderingFraForrigeVedtattBehandling( + initiellVilkårsvurdering = initiellVilkårsvurdering, + forrigeBehandlingVilkårsvurdering = hentVilkårsvurderingThrows(forrigeBehandlingSomErVedtatt.id), + behandling = behandling, + løpendeUnderkategori = løpendeUnderkategori, + aktørerMedUtvidetAndelerIForrigeBehandling = finnAktørerMedUtvidetBarnetrygdIForrigeBehandling(forrigeBehandlingSomErVedtatt), + ) + endretUtbetalingAndelService.kopierEndretUtbetalingAndelFraForrigeBehandling( + behandling, + forrigeBehandlingSomErVedtatt, + ) + return vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = vilkårsvurdering) + } + + private fun hentVilkårsvurdering(behandlingId: Long): Vilkårsvurdering? = vilkårsvurderingService.hentAktivForBehandling( + behandlingId = behandlingId, + ) + + fun hentVilkårsvurderingThrows( + behandlingId: Long, + feilmelding: String? = null, + ): Vilkårsvurdering = + hentVilkårsvurdering(behandlingId) ?: throw Feil( + message = feilmelding ?: "Fant ikke aktiv vilkårsvurdering for behandling $behandlingId", + frontendFeilmelding = feilmelding ?: "Fant ikke aktiv vilkårsvurdering for behandling.", + ) + + companion object { + private val logger = LoggerFactory.getLogger(this::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/Vilk\303\245rsvurderingForNyBehandlingUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/Vilk\303\245rsvurderingForNyBehandlingUtils.kt" new file mode 100644 index 000000000..9cffb1c9e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/Vilk\303\245rsvurderingForNyBehandlingUtils.kt" @@ -0,0 +1,270 @@ +package no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårResultatMedNyPeriode +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårResultatUtils.genererVilkårResultatForEtVilkårPåEnPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingMigreringUtils +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingUtils +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.genererPersonResultatForPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.gjelderAlltidFraBarnetsFødselsdato +import java.time.LocalDate + +data class VilkårsvurderingForNyBehandlingUtils( + val personopplysningGrunnlag: PersonopplysningGrunnlag, +) { + fun genererInitiellVilkårsvurdering( + behandling: Behandling, + barnaAktørSomAlleredeErVurdert: List, + ): Vilkårsvurdering { + return Vilkårsvurdering(behandling = behandling).apply { + when { + behandling.opprettetÅrsak == BehandlingÅrsak.FØDSELSHENDELSE -> { + personResultater = lagPersonResultaterForFødselshendelse( + vilkårsvurdering = this, + barnaAktørSomAlleredeErVurdert = barnaAktørSomAlleredeErVurdert, + ) + } + + !behandling.skalBehandlesAutomatisk -> { + personResultater = lagPersonResultaterForManuellVilkårsvurdering( + vilkårsvurdering = this, + ) + } + + else -> personResultater = lagPersonResultaterForTomVilkårsvurdering( + vilkårsvurdering = this, + ) + } + } + } + + fun genererVilkårsvurderingFraForrigeVedtattBehandling( + initiellVilkårsvurdering: Vilkårsvurdering, + forrigeBehandlingVilkårsvurdering: Vilkårsvurdering, + behandling: Behandling, + løpendeUnderkategori: BehandlingUnderkategori?, + aktørerMedUtvidetAndelerIForrigeBehandling: List, + ): Vilkårsvurdering { + val (vilkårsvurdering) = VilkårsvurderingUtils.flyttResultaterTilInitielt( + aktivVilkårsvurdering = forrigeBehandlingVilkårsvurdering, + initiellVilkårsvurdering = initiellVilkårsvurdering, + løpendeUnderkategori = løpendeUnderkategori, + aktørerMedUtvidetAndelerIForrigeBehandling = aktørerMedUtvidetAndelerIForrigeBehandling, + ) + + return if (behandling.type == BehandlingType.REVURDERING) { + hentVilkårsvurderingMedDødsdatoSomTomDato(vilkårsvurdering) + } else { + vilkårsvurdering + } + } + + fun hentVilkårsvurderingMedDødsdatoSomTomDato(vilkårsvurdering: Vilkårsvurdering): Vilkårsvurdering { + vilkårsvurdering.personResultater.forEach { personResultat -> + val person = personopplysningGrunnlag.søkerOgBarn.single { it.aktør == personResultat.aktør } + + if (person.erDød()) { + val dødsDato = person.dødsfall!!.dødsfallDato + + Vilkår.values().forEach { vilkårType -> + val vilkårAvTypeMedSenesteTom = + personResultat.vilkårResultater.filter { it.vilkårType == vilkårType } + .maxByOrNull { it.periodeTom ?: TIDENES_ENDE } + + if (vilkårAvTypeMedSenesteTom != null && dødsDato.isBefore( + vilkårAvTypeMedSenesteTom.periodeTom ?: TIDENES_ENDE, + ) && dødsDato.isAfter(vilkårAvTypeMedSenesteTom.periodeFom) + ) { + vilkårAvTypeMedSenesteTom.periodeTom = dødsDato + vilkårAvTypeMedSenesteTom.begrunnelse = "Dødsfall" + } + } + } + } + return vilkårsvurdering + } + + private fun lagPersonResultaterForFødselshendelse( + vilkårsvurdering: Vilkårsvurdering, + barnaAktørSomAlleredeErVurdert: List, + ): Set { + val annenForelder = personopplysningGrunnlag.annenForelder + val eldsteBarnSomVurderesSinFødselsdato = + personopplysningGrunnlag.barna.filter { !barnaAktørSomAlleredeErVurdert.contains(it.aktør) } + .maxByOrNull { it.fødselsdato }?.fødselsdato + ?: throw Feil("Finner ingen barn på persongrunnlag") + + return personopplysningGrunnlag.søkerOgBarn.map { person -> + val personResultat = PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = person.aktør) + + val vilkårForPerson = Vilkår.hentVilkårFor( + personType = person.type, + fagsakType = vilkårsvurdering.behandling.fagsak.type, + behandlingUnderkategori = vilkårsvurdering.behandling.underkategori, + ) + + val vilkårResultater = vilkårForPerson.map { vilkår -> + genererVilkårResultatForEtVilkårPåEnPerson( + person = person, + annenForelder = annenForelder, + eldsteBarnSinFødselsdato = eldsteBarnSomVurderesSinFødselsdato, + personResultat = personResultat, + vilkår = vilkår, + ) + } + + personResultat.setSortedVilkårResultater(vilkårResultater.toSet()) + + personResultat + }.toSet() + } + + private fun lagPersonResultaterForManuellVilkårsvurdering( + vilkårsvurdering: Vilkårsvurdering, + ): Set { + return personopplysningGrunnlag.søkerOgBarn.map { person -> + genererPersonResultatForPerson(vilkårsvurdering, person) + }.toSet() + } + + private fun lagPersonResultaterForTomVilkårsvurdering( + vilkårsvurdering: Vilkårsvurdering, + ): Set { + return personopplysningGrunnlag.søkerOgBarn.map { person -> + val personResultat = PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = person.aktør) + + val vilkårForPerson = Vilkår.hentVilkårFor( + personType = person.type, + fagsakType = vilkårsvurdering.behandling.fagsak.type, + behandlingUnderkategori = vilkårsvurdering.behandling.underkategori, + ) + + val vilkårResultater = vilkårForPerson.map { vilkår -> + VilkårResultat( + personResultat = personResultat, + erAutomatiskVurdert = true, + resultat = Resultat.IKKE_VURDERT, + vilkårType = vilkår, + begrunnelse = "", + sistEndretIBehandlingId = personResultat.vilkårsvurdering.behandling.id, + ) + }.toSortedSet(VilkårResultat.VilkårResultatComparator) + + personResultat.setSortedVilkårResultater(vilkårResultater) + + personResultat + }.toSet() + } + + fun lagPersonResultaterForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + vilkårsvurdering: Vilkårsvurdering, + forrigeBehandlingVilkårsvurdering: Vilkårsvurdering, + nyMigreringsdato: LocalDate, + ): Set { + return personopplysningGrunnlag.søkerOgBarn.map { person -> + val personResultat = PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = person.aktør) + + val oppfylteVilkårResultaterForPerson = forrigeBehandlingVilkårsvurdering.personResultater + .single { it.aktør == person.aktør }.vilkårResultater + .filter { it.erOppfylt() } + + val vilkårResultaterMedNyPeriode = + VilkårsvurderingMigreringUtils.finnVilkårResultaterMedNyPeriodePgaNyMigreringsdato( + oppfylteVilkårResultaterForPerson, + person, + nyMigreringsdato, + ) + + val kopierteVilkårResultater = oppfylteVilkårResultaterForPerson.map { oppfyltVilkårResultat -> + val vilkårResultatMedNyPeriode = + vilkårResultaterMedNyPeriode.find { it.vilkårResultat.id == oppfyltVilkårResultat.id } + oppfyltVilkårResultat.kopierMedParent(personResultat).also { kopiertVilkårResultat -> + if (vilkårResultatMedNyPeriode != null) { + kopiertVilkårResultat.sistEndretIBehandlingId = + if (vilkårResultatMedNyPeriode.harNyPeriode()) vilkårsvurdering.behandling.id else kopiertVilkårResultat.sistEndretIBehandlingId + kopiertVilkårResultat.periodeFom = vilkårResultatMedNyPeriode.fom + kopiertVilkårResultat.periodeTom = vilkårResultatMedNyPeriode.tom + if (kopiertVilkårResultat.begrunnelse.isEmpty()) { + kopiertVilkårResultat.begrunnelse = "Migrering" + } + } + } + }.toSet() + + personResultat.setSortedVilkårResultater(kopierteVilkårResultater) + + personResultat + }.toSet() + } + + // Det kan hende UNDER_18 vilkåret ikke har fått endret fom og tom + private fun VilkårResultatMedNyPeriode.harNyPeriode() = + this.vilkårResultat.periodeFom != this.fom || this.vilkårResultat.periodeTom != this.tom + + fun lagPersonResultaterForHelmanuellMigrering( + vilkårsvurdering: Vilkårsvurdering, + nyMigreringsdato: LocalDate, + ): Set { + return personopplysningGrunnlag.søkerOgBarn.map { person -> + val personResultat = PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = person.aktør) + + val vilkårTyperForPerson = Vilkår.hentVilkårFor( + personType = person.type, + fagsakType = vilkårsvurdering.behandling.fagsak.type, + behandlingUnderkategori = vilkårsvurdering.behandling.underkategori, + ) + val vilkårResultater = vilkårTyperForPerson.map { vilkår -> + val fom = when { + vilkår.gjelderAlltidFraBarnetsFødselsdato() -> person.fødselsdato + nyMigreringsdato.isBefore(person.fødselsdato) -> person.fødselsdato + else -> nyMigreringsdato + } + + val tom: LocalDate? = when (vilkår) { + Vilkår.UNDER_18_ÅR -> person.fødselsdato.plusYears(18) + .minusDays(1) + + else -> null + } + + val begrunnelse = "Migrering" + + VilkårResultat( + personResultat = personResultat, + erAutomatiskVurdert = false, + resultat = Resultat.OPPFYLT, + vilkårType = vilkår, + periodeFom = fom, + periodeTom = tom, + begrunnelse = begrunnelse, + sistEndretIBehandlingId = personResultat.vilkårsvurdering.behandling.id, + ) + }.toSortedSet(VilkårResultat.VilkårResultatComparator) + + personResultat.setSortedVilkårResultater(vilkårResultater) + + personResultat + }.toSet() + } +} + +fun førstegangskjøringAvVilkårsvurdering(aktivVilkårsvurdering: Vilkårsvurdering?): Boolean { + return aktivVilkårsvurdering == null +} + +fun finnAktørerMedUtvidetFraAndeler(andeler: List): List { + return andeler.filter { it.erUtvidet() }.map { it.aktør } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/Periode.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/Periode.kt new file mode 100644 index 000000000..95e79b538 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/Periode.kt @@ -0,0 +1,42 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje + +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligSent +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligTidlig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunktEllerUendeligSent +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunktEllerUendeligTidlig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.TidspunktClosedRange +import java.time.LocalDate +import java.time.YearMonth + +data class Periode( + val fraOgMed: Tidspunkt, + val tilOgMed: Tidspunkt, + val innhold: I? = null, +) { + constructor(tidsrom: TidspunktClosedRange, innhold: I?) : this(tidsrom.start, tidsrom.endInclusive, innhold) + + override fun toString(): String = "$fraOgMed - $tilOgMed: $innhold" +} + +fun periodeAv(fraOgMed: LocalDate?, tilOgMed: LocalDate?, innhold: I): Periode = + Periode(fraOgMed.tilTidspunktEllerUendeligTidlig(), tilOgMed.tilTidspunktEllerUendeligSent(), innhold) + +fun månedPeriodeAv(fraOgMed: YearMonth?, tilOgMed: YearMonth?, innhold: I): Periode = + Periode(fraOgMed.tilTidspunktEllerUendeligTidlig(), tilOgMed.tilTidspunktEllerUendeligSent(), innhold) + +fun periodeAv( + fraOgMed: Tidspunkt, + tilOgMed: Tidspunkt, + innhold: I, +): Periode = Periode(fraOgMed, tilOgMed, innhold) + +fun Tidspunkt.tilPeriodeMedInnhold(innhold: I?) = Periode(this, this, innhold) + +fun Tidspunkt.tilPeriodeUtenInnhold() = tilPeriodeMedInnhold(null as I) + +fun Collection>.mapInnhold(mapper: (I?) -> R?): Collection> = + this.map { Periode(it.fraOgMed, it.tilOgMed, mapper(it.innhold)) } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/Tidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/Tidslinje.kt new file mode 100644 index 000000000..b1738d395 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/Tidslinje.kt @@ -0,0 +1,111 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje + +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erUendeligLengeTil + +/** + * Base-klassen for alle tidslinjer. Bygger på en tanke om at en tidslinje inneholder en + * sortert liste av ikke-overlappende perioder med et innhold av type I, som kan være null. + * Tidslinjen og tilhørende perioder har alle tidsenheten T. + * Periodene er sortert fra tidligste til seneste. + * fraOgMed og tilOgMed i en periode kan være like, men tilOgMed kan aldri være tidligere enn fraOgMed + * fraOgMed i første periode kan være åpen, dvs "uenedelig lenge siden" + * tilOgMed i siste periode kan være åpen, dvs "uendelig lenge til" + * Generelt vil to påfølgende perioder kunne slås sammen hvis de ligger inntil hverandre + * og innholdet er likt. Likhet avgjøres av [equals()] + */ +abstract class Tidslinje { + private var periodeCache: List>? = null + + fun perioder(): Collection> { + return periodeCache ?: lagPerioder().sortedBy { it.fraOgMed }.toList() + .also { + valider(it) + periodeCache = it + } + } + + protected abstract fun lagPerioder(): Collection> + + protected open fun valider(perioder: List>) { + val feilInnenforPerioder = perioder.map { + when { + it.fraOgMed > it.tilOgMed -> + TidslinjeFeil(periode = it, tidslinje = this, type = TidslinjeFeilType.TOM_ER_FØR_FOM) + + else -> null + } + } + + val feilMellomPåfølgendePerioder = perioder.windowed(2) { (periode1, periode2) -> + when { + periode2.fraOgMed.erUendeligLengeSiden() -> + TidslinjeFeil( + periode = periode2, + tidslinje = this, + type = TidslinjeFeilType.UENDELIG_FORTID_ETTER_FØRSTE_PERIODE, + ) + + periode1.tilOgMed.erUendeligLengeTil() -> + TidslinjeFeil( + periode = periode1, + tidslinje = this, + type = TidslinjeFeilType.UENDELIG_FREMTID_FØR_SISTE_PERIODE, + ) + + periode1.tilOgMed >= periode2.fraOgMed -> + TidslinjeFeil( + periode = periode1, + tidslinje = this, + type = TidslinjeFeilType.OVERLAPPER_ETTERFØLGENDE_PERIODE, + ) + + else -> null + } + } + + val tidslinjeFeil = (feilInnenforPerioder + feilMellomPåfølgendePerioder) + .filterNotNull() + + if (tidslinjeFeil.isNotEmpty()) { + throw TidslinjeFeilException(tidslinjeFeil) + } + } + + override fun equals(other: Any?): Boolean { + return if (other is Tidslinje<*, *>) { + perioder() == other.perioder() + } else { + false + } + } + + override fun toString(): String = + lagPerioder().joinToString(" | ") { it.toString() } + + companion object { + data class TidslinjeFeil( + val type: TidslinjeFeilType, + val periode: Periode<*, *>, + val tidslinje: Tidslinje<*, *>, + ) + + enum class TidslinjeFeilType { + UENDELIG_FORTID_ETTER_FØRSTE_PERIODE, + UENDELIG_FREMTID_FØR_SISTE_PERIODE, + TOM_ER_FØR_FOM, + OVERLAPPER_ETTERFØLGENDE_PERIODE, + } + + data class TidslinjeFeilException(val tidslinjeFeil: Collection) : + IllegalStateException(tidslinjeFeil.toString()) + } +} + +fun tidslinje(lagPerioder: () -> Collection>) = + object : Tidslinje() { + override fun lagPerioder() = lagPerioder() + } + +fun List>.tilTidslinje(): Tidslinje = tidslinje { this } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeFraTidspunkt.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeFraTidspunkt.kt new file mode 100644 index 000000000..49e2b7d2d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeFraTidspunkt.kt @@ -0,0 +1,93 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erRettFør +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somEndelig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somFraOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somTilOgMed + +fun Iterable>.tidslinjeFraTidspunkt( + tidspunktMapper: (Tidspunkt) -> Innhold, +): Tidslinje = tidslinje { + map { tidspunkt -> TidspunktMedInnhold(tidspunkt, tidspunktMapper(tidspunkt)) } + .filter { it.harInnhold } + .fold(emptyList()) { perioder, tidspunktMedInnhold -> + val sistePeriode = perioder.lastOrNull() + when { + sistePeriode != null && sistePeriode.kanUtvidesMed(tidspunktMedInnhold) -> + perioder.replaceLast(sistePeriode.utvidMed(tidspunktMedInnhold)) + else -> perioder + tidspunktMedInnhold.tilPeriode() + } + } +} + +/** + * Innhold har tre tilstander + * - uten innhold -> harInnhold = false, harVerdi = false + * - innhold som er null -> harInnhold = true, harVerdi = false + * - innhold som ikke er null -> harInnhold = true, harVerdi = true + */ +data class Innhold( + val innhold: I?, + internal val harInnhold: Boolean = true, +) { + constructor(innhold: I?) : this(innhold, true) + + companion object { + fun utenInnhold() = Innhold(null, false) + } + + val harVerdi + get() = harInnhold && innhold != null + + val verdi + get() = innhold!! + + fun mapInnhold(mapper: (I?) -> R?): R? = if (this.harInnhold) mapper(innhold) else null + fun mapVerdi(mapper: (I) -> R): R? = if (this.harVerdi) mapper(verdi) else null +} + +fun I?.tilInnhold() = Innhold(this) +fun I?.tilVerdi() = this?.let { Innhold(it) } ?: Innhold.utenInnhold() + +fun Tidslinje.innholdForTidspunkt(tidspunkt: Tidspunkt): Innhold = + perioder().innholdForTidspunkt(tidspunkt) + +fun Collection>.innholdForTidspunkt( + tidspunkt: Tidspunkt, +): Innhold { + val periode = this.firstOrNull { it.omfatter(tidspunkt) } + return when (periode) { + null -> Innhold.utenInnhold() + else -> Innhold(periode.innhold, true) + } +} + +private fun Periode.omfatter(tidspunkt: Tidspunkt) = + this.fraOgMed <= tidspunkt && this.tilOgMed >= tidspunkt + +private data class TidspunktMedInnhold( + val tidspunkt: Tidspunkt, + private val innholdsresultat: Innhold, +) { + val harInnhold get() = innholdsresultat.harInnhold + val innhold get() = innholdsresultat.innhold +} + +private fun Periode.kanUtvidesMed(tidspunktMedInnhold: TidspunktMedInnhold) = + tidspunktMedInnhold.harInnhold && + this.innhold == tidspunktMedInnhold.innhold && + this.tilOgMed.erRettFør(tidspunktMedInnhold.tidspunkt.somEndelig()) + +private fun Periode.utvidMed(tidspunktMedInnhold: TidspunktMedInnhold): Periode = + this.copy(tilOgMed = tidspunktMedInnhold.tidspunkt) + +private fun TidspunktMedInnhold.tilPeriode() = + Periode(this.tidspunkt.somFraOgMed(), this.tidspunkt.somTilOgMed(), this.innhold) + +private fun Collection.replaceLast(replacement: T) = + this.take(this.size - 1) + replacement diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeJoin.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeJoin.kt new file mode 100644 index 000000000..2ac9ad4f8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeJoin.kt @@ -0,0 +1,104 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet + +fun Map>.outerJoin( + høyreTidslinjer: Map>, + kombinator: (V?, H?) -> R?, +): Map> { + val venstreTidslinjer = this + val alleNøkler = venstreTidslinjer.keys + høyreTidslinjer.keys + + return alleNøkler.associateWith { nøkkel -> + val venstreTidslinje = venstreTidslinjer.getOrDefault(nøkkel, TomTidslinje()) + val høyreTidslinje = høyreTidslinjer.getOrDefault(nøkkel, TomTidslinje()) + + venstreTidslinje.kombinerMed(høyreTidslinje, kombinator) + } +} + +fun Map>.leftJoin( + høyreTidslinjer: Map>, + kombinator: (V?, H?) -> R?, +): Map> { + val venstreTidslinjer = this + val venstreNøkler = venstreTidslinjer.keys + + return venstreNøkler.associateWith { nøkkel -> + val venstreTidslinje = venstreTidslinjer.getOrDefault(nøkkel, TomTidslinje()) + val høyreTidslinje = høyreTidslinjer.getOrDefault(nøkkel, TomTidslinje()) + + venstreTidslinje.kombinerMed(høyreTidslinje, kombinator) + } +} + +/** + * Extension-metode for å kombinere to nøkkel-verdi-map'er der verdiene er tidslinjer + * Nøkkelen må være av samme type, K, tidslinjene må være i samme tidsenhet (T) + * Innholdet i tidslinjene i map'en på venstre side må alle være av typen V + * Innholdet i tidslinjene i map'en på høyre side må alle være av typen H + * Kombinator-funksjonen kalles med verdiene av fra venstre og høyre tidslinje for samme nøkkel og tidspunkt. + * blir sendt som verdier hvis venstre, høyre eller begge tidslinjer mangler verdi for et tidspunkt + * Resultatet er en ny map der nøklene er av type K, og tidslinjene har innhold av typen (nullable) R. + * Bare nøkler som finnes i begge map'ene vil finnes i den resulterende map'en + */ +fun Map>.join( + høyreTidslinjer: Map>, + kombinator: (V?, H?) -> R?, +): Map> { + val venstreTidslinjer = this + val alleNøkler = venstreTidslinjer.keys.intersect(høyreTidslinjer.keys) + + return alleNøkler.associateWith { nøkkel -> + val venstreTidslinje = venstreTidslinjer.getOrDefault(nøkkel, TomTidslinje()) + val høyreTidslinje = høyreTidslinjer.getOrDefault(nøkkel, TomTidslinje()) + + venstreTidslinje.kombinerMed(høyreTidslinje, kombinator) + } +} + +/** + * Extension-metode for å kombinere to nøkkel-verdi-map'er der verdiene er tidslinjer + * Nøkkelen må være av samme type, K, tidslinjene må være i samme tidsenhet (T) + * Innholdet i tidslinjene i map'en på venstre side må alle være av typen V + * Innholdet i tidslinjene i map'en på høyre side må alle være av typen H + * Kombinator-funksjonen kalles med verdiene av fra venstre og høyre tidslinje for samme nøkkel og tidspunkt. + * Kombinator-funksjonen blir IKKE kalt Hvis venstre, høyre eller begge tidslinjer mangler verdi for et tidspunkt + * Resultatet er en ny map der nøklene er av type K, og tidslinjene har innhold av typen (nullable) R. + * Bare nøkler som finnes i begge map'ene vil finnes i den resulterende map'en + */ +fun Map>.joinIkkeNull( + høyreTidslinjer: Map>, + kombinator: (V, H) -> R?, +): Map> { + val venstreTidslinjer = this + val alleNøkler = venstreTidslinjer.keys.intersect(høyreTidslinjer.keys) + + return alleNøkler.associateWith { nøkkel -> + val venstreTidslinje = venstreTidslinjer.getOrDefault(nøkkel, TomTidslinje()) + val høyreTidslinje = høyreTidslinjer.getOrDefault(nøkkel, TomTidslinje()) + + venstreTidslinje.kombinerUtenNullMed(høyreTidslinje, kombinator) + } +} + +/** + * Extension-metode for å kombinere en nøkkel-verdi-map'er der verdiene er tidslinjer, med en enkelt tidslinje + * Innholdet i tidslinjene i map'en på venstre side må alle være av typen V + * Innholdet i tidslinjen på høyre side er av typen H + * Kombinator-funksjonen kalles for hvert tidspunkt med med verdien for det tidspunktet fra høyre tidslinje og + * vedien fra den enkelte av venstre tidslinjer etter tur. + * Kombinator-funksjonen blir IKKE kalt Hvis venstre, høyre eller begge tidslinjer mangler verdi for et tidspunkt + * Resultatet er en ny map der nøklene er av type K, og tidslinjene har innhold av typen (nullable) R. + */ +fun Map>.kombinerKunVerdiMed( + høyreTidslinje: Tidslinje, + kombinator: (V, H) -> R?, +): Map> { + val venstreTidslinjer = this + + return venstreTidslinjer.mapValues { (_, venstreTidslinje) -> + venstreTidslinje.kombinerUtenNullMed(høyreTidslinje, kombinator) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeKombinator.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeKombinator.kt new file mode 100644 index 000000000..e1a28eb70 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeKombinator.kt @@ -0,0 +1,201 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom + +/** + * Extension-metode for å kombinere to tidslinjer + * Kombinasjonen baserer seg på å iterere gjennom alle tidspunktene + * fra minste fraOgMed() til største tilOgMed() fra begge tidslinjene + * Tidsenhet (T) må være av samme type + * Hver av tidslinjene kan ha ulik innholdstype, hhv V og H + * Kombintor-funksjonen tar inn (nullable) av V og H og returnerer (nullable) R + * Kombinator-funksjonen blir ikke kalt hvis begge tidslinjene mangler innhold for tidspunktet + * Hvis kombinator-funksjonen returner , antas det at tidslinjen ikke skal ha verdi for tidspunktet + * Resultatet er en tidslinje med tidsenhet T og innhold R + */ +fun Tidslinje.kombinerMed( + høyreTidslinje: Tidslinje, + kombinator: (V?, H?) -> R?, +): Tidslinje = tidsrom(this, høyreTidslinje).tidslinjeFraTidspunkt { tidspunkt -> + val venstre = this.innholdForTidspunkt(tidspunkt) + val høyre = høyreTidslinje.innholdForTidspunkt(tidspunkt) + + when { + !(venstre.harInnhold || høyre.harInnhold) -> Innhold.utenInnhold() + else -> kombinator(venstre.innhold, høyre.innhold).tilInnhold() + } +} + +/** + * Extension-metode for å kombinere to tidslinjer der begge har verdi + * Kombinasjonen baserer seg på å iterere gjennom alle tidspunktene + * fra minste fraOgMed til største tilOgMed fra begge tidslinjene + * Tidsenhet (T) må være av samme type + * Hver av tidslinjene kan ha ulik innholdstype, hhv V og H + * Hvis innholdet V eller H mangler innhold, så vil ikke resulterende tidslinje få innhold for det tidspunktet + * Kombintor-funksjonen tar ellers V og H og returnerer (nullable) R + * Hvis kombinator-funksjonen returner , antas det at tidslinjen ikke skal ha verdi for tidspunktet + * Resultatet er en tidslinje med tidsenhet T og innhold R + */ +fun Tidslinje.kombinerUtenNullMed( + høyreTidslinje: Tidslinje, + kombinator: (V, H) -> R?, +): Tidslinje = tidsrom(this, høyreTidslinje).tidslinjeFraTidspunkt { tidspunkt -> + val venstre = this.innholdForTidspunkt(tidspunkt) + val høyre = høyreTidslinje.innholdForTidspunkt(tidspunkt) + + when { + venstre.harVerdi && høyre.harVerdi -> kombinator(venstre.verdi, høyre.verdi).tilVerdi() + else -> Innhold.utenInnhold() + } +} + +/** + * Extension-metode for å kombinere liste av tidslinjer + * Kombinasjonen baserer seg på å iterere gjennom alle tidspunktene + * fra minste til største fra alle tidslinjene + * Innhold (I) og tidsenhet (T) må være av samme type + * Kombintor-funksjonen tar inn Iterable og returner (nullable) R + * Null-verdier fjernes før de sendes til kombinator-funksjonen, som betyr at en tom iterator kan bli sendt + * Hvis reesultatet fra kombinatoren er null, tolkes det som at det ikke skal være innhold + * Resultatet er en tidslinje med tidsenhet T og innhold R + */ +fun Collection>.kombinerUtenNull( + listeKombinator: (Iterable) -> R?, +): Tidslinje = tidsrom().tidslinjeFraTidspunkt { tidspunkt -> + this.map { it.innholdForTidspunkt(tidspunkt) } + .filter { it.harVerdi } + .map { it.verdi } + .let(listeKombinator).tilVerdi() +} + +/** + * Extension-metode for å kombinere liste av tidslinjer + * Kombinasjonen baserer seg på å iterere gjennom alle tidspunktene + * fra minste til største fra alle tidslinjene + * Innhold (I) og tidsenhet (T) må være av samme type + * Kombintor-funksjonen tar inn Iterable og returner (nullable) R + * Null-verdier fjernes, og listen av verdier sendes til kombinator-funksjonen bare hvis den inneholder verdier + * Hvis reesultatet fra kombinatoren er null, tolkes det som at det ikke skal være innhold + * Resultatet er en tidslinje med tidsenhet T og innhold R + */ +fun Collection>.kombinerUtenNullOgIkkeTom( + listeKombinator: (Iterable) -> R?, +): Tidslinje = tidsrom().tidslinjeFraTidspunkt { tidspunkt -> + this.map { it.innholdForTidspunkt(tidspunkt) } + .filter { it.harVerdi } + .map { it.verdi } + .takeIf { it.isNotEmpty() } + ?.let(listeKombinator).tilVerdi() +} + +/** + * Extension-metode for å kombinere liste av tidslinjer + * Kombinasjonen baserer seg på å iterere gjennom alle tidspunktene + * fra minste til største fra alle tidslinjene + * Innhold (I) og tidsenhet (T) må være av samme type + * Kombintor-funksjonen tar inn Iterable og returner (nullable) R + * Resultatet er en tidslinje med tidsenhet T og innhold R + */ +fun Collection>.kombiner( + listeKombinator: (Iterable) -> R?, +): Tidslinje = tidsrom().tidslinjeFraTidspunkt { tidspunkt -> + this.map { it.innholdForTidspunkt(tidspunkt) } + .filter { it.harVerdi } + .map { it.verdi } + .let { listeKombinator(it) } + .tilVerdi() +} + +/** + * Extension-metode for å kombinere to tidslinjer + * Kombinasjonen baserer seg på å iterere gjennom alle tidspunktene + * fra minste til største fra begge tidslinjene + * Tidsenhet (T) må være av samme type + * Hver av tidslinjene kan ha ulik innholdstype, hhv V og H + * Kombintor-funksjonen tar inn tidspunktet og (nullable) av V og H og returnerer (nullable) R + * Resultatet er en tidslinje med tidsenhet T og innhold R + */ +fun Tidslinje.tidspunktKombinerMed( + høyreTidslinje: Tidslinje, + kombinator: (Tidspunkt, V?, H?) -> R?, +): Tidslinje = tidsrom(this, høyreTidslinje).tidslinjeFraTidspunkt { tidspunkt -> + kombinator( + tidspunkt, + this.innholdForTidspunkt(tidspunkt).innhold, + høyreTidslinje.innholdForTidspunkt(tidspunkt).innhold, + ).tilInnhold() +} + +/** + * Extension-metode for å kombinere tre tidslinjer + * Kombinasjonen baserer seg på å iterere gjennom alle tidspunktene + * fra minste til største fra alle tidslinjene + * Tidsenhet (T) må være av samme type + * Hver av tidslinjene kan ha ulik innholdstype, hhv A, B og C + * Kombintor-funksjonen tar inn (nullable) av A, B og C og returner (nullable) R + * Resultatet er en tidslinje med tidsenhet T og innhold R + */ +fun Tidslinje.kombinerMed( + tidslinjeB: Tidslinje, + tidslinjeC: Tidslinje, + kombinator: (A?, B?, C?) -> R?, +): Tidslinje = tidsrom(this, tidslinjeB, tidslinjeC).tidslinjeFraTidspunkt { tidspunkt -> + kombinator( + this.innholdForTidspunkt(tidspunkt).innhold, + tidslinjeB.innholdForTidspunkt(tidspunkt).innhold, + tidslinjeC.innholdForTidspunkt(tidspunkt).innhold, + ).tilInnhold() +} + +/** + * Extension-metode for å kombinere tre tidslinjer + * Kombinasjonen baserer seg på å iterere gjennom alle tidspunktene + * fra minste til største fra alle tidslinjene + * Tidsenhet (T) må være av samme type + * Hver av tidslinjene kan ha ulik innholdstype, hhv A, B og C + * Kombintor-funksjonen tar inn (nullable) av A, B og C og returner (nullable) R + * Resultatet er en tidslinje med tidsenhet T og innhold R + */ +fun Tidslinje.kombinerMedDatert( + tidslinjeB: Tidslinje, + tidslinjeC: Tidslinje, + kombinator: (A?, B?, C?, Tidspunkt) -> R?, +): Tidslinje = tidsrom(this, tidslinjeB, tidslinjeC).tidslinjeFraTidspunkt { tidspunkt -> + kombinator( + this.innholdForTidspunkt(tidspunkt).innhold, + tidslinjeB.innholdForTidspunkt(tidspunkt).innhold, + tidslinjeC.innholdForTidspunkt(tidspunkt).innhold, + tidspunkt, + ).tilInnhold() +} + +fun Tidslinje.kombinerMedKunVerdi( + tidslinjeB: Tidslinje, + tidslinjeC: Tidslinje, + kombinator: (A, B, C) -> R?, +): Tidslinje = tidsrom(this, tidslinjeB, tidslinjeC).tidslinjeFraTidspunkt { tidspunkt -> + val innholdA = this.innholdForTidspunkt(tidspunkt) + val innholdB = tidslinjeB.innholdForTidspunkt(tidspunkt) + val innholdC = tidslinjeC.innholdForTidspunkt(tidspunkt) + + when { + innholdA.harVerdi && innholdB.harVerdi && innholdC.harVerdi -> + kombinator(innholdA.verdi, innholdB.verdi, innholdC.verdi).tilVerdi() + else -> Innhold.utenInnhold() + } +} + +fun Tidslinje.harOverlappMed(tidslinje: Tidslinje) = + this.kombinerUtenNullMed(tidslinje) { v, h -> true }.erIkkeTom() + +fun Tidslinje.harIkkeOverlappMed(tidslinje: Tidslinje) = + !this.harOverlappMed(tidslinje) + +fun Tidslinje.kombinerMedNullable( + høyreTidslinje: Tidslinje?, + kombinator: (V?, H?) -> V?, +): Tidslinje = if (høyreTidslinje != null) { kombinerMed(høyreTidslinje, kombinator) } else this diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeMedAvhengigheter.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeMedAvhengigheter.kt new file mode 100644 index 000000000..c9e7df2dd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeMedAvhengigheter.kt @@ -0,0 +1,17 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet + +val MANGLER_AVHENGIGHETER = IllegalArgumentException("Det er ikke sendt med noen avhengigheter") + +abstract class TidslinjeMedAvhengigheter( + private val foregåendeTidslinjer: Collection>, +) : Tidslinje() { + + init { + if (foregåendeTidslinjer.isEmpty()) { + throw MANGLER_AVHENGIGHETER + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeSomKomprimerer.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeSomKomprimerer.kt new file mode 100644 index 000000000..2d8dd7ef7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TidslinjeSomKomprimerer.kt @@ -0,0 +1,12 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom + +/** + * Extension-funksjon som slår sammen påfølgende perioder der innholdet er likt + * Benytter tidslinjeFraTidspunkt, som bygger sammenslåtte perioder som default + */ +fun Tidslinje.slåSammenLike(): Tidslinje = + tidsrom().tidslinjeFraTidspunkt { tidspunkt -> innholdForTidspunkt(tidspunkt) } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TomTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TomTidslinje.kt new file mode 100644 index 000000000..c19dcd465 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/komposisjon/TomTidslinje.kt @@ -0,0 +1,12 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet + +class TomTidslinje : Tidslinje() { + override fun lagPerioder(): Collection> = emptyList() +} + +fun Tidslinje.erTom() = this == TomTidslinje() +fun Tidslinje.erIkkeTom() = !this.erTom() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/matematikk/BigDecimalTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/matematikk/BigDecimalTidslinje.kt new file mode 100644 index 000000000..598c89581 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/matematikk/BigDecimalTidslinje.kt @@ -0,0 +1,25 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.matematikk + +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.join +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNullOgIkkeTom +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.mapIkkeNull +import java.math.BigDecimal +import java.math.RoundingMode + +fun Map>.minus( + bTidslinjer: Map>, +) = this.join(bTidslinjer) { a, b -> + when { + a != null && b != null -> a - b + else -> a + } +} + +fun Map>.sum() = + values.kombinerUtenNullOgIkkeTom { it.reduce { sum, verdi -> sum.plus(verdi) } } + +fun Tidslinje.rundAvTilHeltall() = + this.mapIkkeNull { it.setScale(0, RoundingMode.HALF_UP) } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/matematikk/SammenliknbarTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/matematikk/SammenliknbarTidslinje.kt new file mode 100644 index 000000000..05ea8159c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/matematikk/SammenliknbarTidslinje.kt @@ -0,0 +1,8 @@ +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.joinIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet + +fun , T : Tidsenhet> minsteAvHver( + aTidslinjer: Map>, + bTidslinjer: Map>, +) = aTidslinjer.joinIkkeNull(bTidslinjer) { a, b -> minOf(a, b) } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidspunkt/DagTidspunkt.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidspunkt/DagTidspunkt.kt new file mode 100644 index 000000000..a4b80e9fa --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidspunkt/DagTidspunkt.kt @@ -0,0 +1,77 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt + +import java.time.LocalDate + +data class DagTidspunkt internal constructor( + internal val dato: LocalDate, + override val uendelighet: Uendelighet, +) : Tidspunkt(uendelighet) { + + fun tilLocalDateEllerNull(): LocalDate? { + return if (uendelighet != Uendelighet.INGEN) { + null + } else { + dato + } + } + + fun tilLocalDate(): LocalDate { + return tilLocalDateEllerNull() ?: throw IllegalStateException("Tidspunkt er uendelig") + } + + override fun flytt(tidsenheter: Long): DagTidspunkt { + return this.copy(dato = dato.plusDays(tidsenheter), uendelighet) + } + + override fun medUendelighet(uendelighet: Uendelighet): DagTidspunkt = + copy(uendelighet = uendelighet) + + override fun toString(): String { + return when (uendelighet) { + Uendelighet.FORTID -> "<--" + else -> "" + } + dato + when (uendelighet) { + Uendelighet.FREMTID -> "-->" + else -> "" + } + } + + override fun sammenliknMed(tidspunkt: Tidspunkt): Int { + return dato.compareTo((tidspunkt as DagTidspunkt).dato) + } + + override fun equals(other: Any?): Boolean { + return when (other) { + is DagTidspunkt -> compareTo(other) == 0 + is Tidspunkt<*> -> this.uendelighet != Uendelighet.INGEN && this.uendelighet == other.uendelighet + else -> false + } + } + + override fun hashCode(): Int { + return if (uendelighet == Uendelighet.INGEN) { + dato.hashCode() + } else { + uendelighet.hashCode() + } + } + + companion object { + fun nå() = DagTidspunkt(LocalDate.now(), Uendelighet.INGEN) + fun uendeligLengeSiden(dato: LocalDate = LocalDate.now()) = DagTidspunkt(dato, uendelighet = Uendelighet.FORTID) + fun uendeligLengeTil(dato: LocalDate = LocalDate.now()) = DagTidspunkt(dato, uendelighet = Uendelighet.FREMTID) + fun med(dato: LocalDate) = DagTidspunkt(dato, Uendelighet.INGEN) + + internal fun LocalDate?.tilTidspunktEllerUendeligTidlig(defaultUendelighetDato: LocalDate? = null) = + this.tilTidspunktEllerUendelig(defaultUendelighetDato, Uendelighet.FORTID) + + internal fun LocalDate?.tilTidspunktEllerUendeligSent(defaultUendelighetDato: LocalDate? = null) = + this.tilTidspunktEllerUendelig(defaultUendelighetDato, Uendelighet.FREMTID) + + private fun LocalDate?.tilTidspunktEllerUendelig(default: LocalDate?, uendelighet: Uendelighet) = + this?.let { DagTidspunkt(it, Uendelighet.INGEN) } ?: DagTidspunkt( + default ?: LocalDate.now(), + uendelighet, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidspunkt/M\303\245nedTidspunkt.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidspunkt/M\303\245nedTidspunkt.kt" new file mode 100644 index 000000000..3f89f4370 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidspunkt/M\303\245nedTidspunkt.kt" @@ -0,0 +1,84 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt + +import no.nav.familie.ba.sak.common.toYearMonth +import java.time.LocalDate +import java.time.YearMonth + +data class MånedTidspunkt internal constructor( + internal val måned: YearMonth, + override val uendelighet: Uendelighet, +) : Tidspunkt(uendelighet) { + + fun tilYearMonthEllerNull(): YearMonth? = + if (uendelighet != Uendelighet.INGEN) { + null + } else { + måned + } + + fun tilYearMonth(): YearMonth = + if (uendelighet != Uendelighet.INGEN) { + throw IllegalStateException("Tidspunktet er uendelig") + } else { + måned + } + + override fun flytt(tidsenheter: Long) = copy(måned = måned.plusMonths(tidsenheter)) + + override fun medUendelighet(uendelighet: Uendelighet): MånedTidspunkt = + copy(uendelighet = uendelighet) + + override fun toString(): String { + return when (uendelighet) { + Uendelighet.FORTID -> "<--" + else -> "" + } + måned + when (uendelighet) { + Uendelighet.FREMTID -> "-->" + else -> "" + } + } + + override fun sammenliknMed(tidspunkt: Tidspunkt): Int { + return måned.compareTo((tidspunkt as MånedTidspunkt).måned) + } + + override fun equals(other: Any?): Boolean { + return when (other) { + is MånedTidspunkt -> compareTo(other) == 0 + is Tidspunkt<*> -> this.uendelighet != Uendelighet.INGEN && this.uendelighet == other.uendelighet + else -> false + } + } + + override fun hashCode(): Int { + return if (uendelighet == Uendelighet.INGEN) { + måned.hashCode() + } else { + uendelighet.hashCode() + } + } + + companion object { + fun nå() = MånedTidspunkt(YearMonth.now(), Uendelighet.INGEN) + fun uendeligLengeTil(måned: YearMonth = YearMonth.now()) = MånedTidspunkt(måned, Uendelighet.FREMTID) + fun uendeligLengeSiden(måned: YearMonth = YearMonth.now()) = MånedTidspunkt(måned, Uendelighet.FORTID) + fun med(måned: YearMonth) = MånedTidspunkt(måned, Uendelighet.INGEN) + fun med(år: Int, måned: Int) = MånedTidspunkt(YearMonth.of(år, måned), Uendelighet.INGEN) + + internal fun YearMonth.tilTidspunkt() = MånedTidspunkt(this, Uendelighet.INGEN) + + internal fun YearMonth?.tilTidspunktEllerUendeligTidlig(defaultUendelighetMåned: YearMonth? = null) = + this.tilTidspunktEllerUendelig(defaultUendelighetMåned, Uendelighet.FORTID) + + internal fun YearMonth?.tilTidspunktEllerUendeligSent(defaultUendelighetMåned: YearMonth? = null) = + this.tilTidspunktEllerUendelig(defaultUendelighetMåned, Uendelighet.FREMTID) + + private fun YearMonth?.tilTidspunktEllerUendelig(default: YearMonth?, uendelighet: Uendelighet) = + this?.let { MånedTidspunkt(it, Uendelighet.INGEN) } ?: MånedTidspunkt( + default ?: YearMonth.now(), + uendelighet, + ) + + fun LocalDate.tilMånedTidspunkt() = MånedTidspunkt(this.toYearMonth(), Uendelighet.INGEN) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidspunkt/Tidspunkt.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidspunkt/Tidspunkt.kt new file mode 100644 index 000000000..b7ebb9ff2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidspunkt/Tidspunkt.kt @@ -0,0 +1,57 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt + +enum class Uendelighet { + INGEN, + FORTID, + FREMTID, +} + +interface Tidsenhet +class Dag : Tidsenhet +class Måned : Tidsenhet + +abstract class Tidspunkt internal constructor( + internal open val uendelighet: Uendelighet, +) : Comparable> { + abstract fun flytt(tidsenheter: Long): Tidspunkt + internal abstract fun medUendelighet(uendelighet: Uendelighet): Tidspunkt + + // Betrakter to uendeligheter som like, selv underliggende tidspunkt kan være forskjellig + override fun compareTo(other: Tidspunkt) = + if (this.uendelighet == Uendelighet.FORTID && other.uendelighet == Uendelighet.FORTID) { + 0 + } else if (this.uendelighet == Uendelighet.FREMTID && other.uendelighet == Uendelighet.FREMTID) { + 0 + } else if (this.uendelighet == Uendelighet.FORTID && other.uendelighet != Uendelighet.FORTID) { + -1 + } else if (this.uendelighet == Uendelighet.FREMTID && other.uendelighet != Uendelighet.FREMTID) { + 1 + } else if (this.uendelighet != Uendelighet.FORTID && other.uendelighet == Uendelighet.FORTID) { + 1 + } else if (this.uendelighet != Uendelighet.FREMTID && other.uendelighet == Uendelighet.FREMTID) { + -1 + } else { + sammenliknMed(other) + } + + protected abstract fun sammenliknMed(tidspunkt: Tidspunkt): Int + + /** + * Det samme som tidspunkt.somEndelig() <= tilOgMed.somEndelig() + * Men unngår å kopiere seg selv, og trenger ikke sjekke for andre verdier av [Uendelighet] i [compareTo] + */ + fun endeligMindreEllerLik(tidspunkt: Tidspunkt) = sammenliknMed(tidspunkt) <= 0 + + /** + * Det samme som + * tidspunkt.somEndelig() == tilOgMed.somEndelig() + * Men unngår å kopiere seg selv, og trenger ikke sjekke for andre verdier av [Uendelighet] i [compareTo] + */ + fun endeligLik(tidspunkt: Tidspunkt) = sammenliknMed(tidspunkt) == 0 + + /** + * Det samme som tidspunkt.somEndelig() < tilOgMed.somEndelig() + * Men unngår å kopiere seg selv, og trenger ikke sjekke for andre verdier av [Uendelighet] i [compareTo] + */ + fun endeligMindre(tidspunkt: Tidspunkt) = sammenliknMed(tidspunkt) < 0 +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidspunkt/TidspunktKonvertering.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidspunkt/TidspunktKonvertering.kt new file mode 100644 index 000000000..64fdba2b6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidspunkt/TidspunktKonvertering.kt @@ -0,0 +1,76 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.common.toYearMonth + +fun Tidspunkt.tilFørsteDagIMåneden() = when (this) { + is DagTidspunkt -> DagTidspunkt(this.dato.withDayOfMonth(1), uendelighet) + is MånedTidspunkt -> DagTidspunkt(this.måned.atDay(1), uendelighet) + else -> throw Feil("Ukjent type tidspunkt") +} + +fun Tidspunkt.tilSisteDagIMåneden() = when (this) { + is DagTidspunkt -> DagTidspunkt(this.dato.sisteDagIMåned(), uendelighet) + is MånedTidspunkt -> DagTidspunkt(this.måned.atEndOfMonth(), uendelighet) + else -> throw Feil("Ukjent type tidspunkt") +} + +fun Tidspunkt.tilInneværendeMåned() = when (this) { + is DagTidspunkt -> MånedTidspunkt(this.dato.toYearMonth(), uendelighet) + is MånedTidspunkt -> this + else -> throw Feil("Ukjent type tidspunkt") +} + +fun Tidspunkt.tilNesteMåned() = when (this) { + is DagTidspunkt -> MånedTidspunkt(this.dato.toYearMonth(), uendelighet).neste() + is MånedTidspunkt -> this.neste() + else -> throw Feil("Ukjent type tidspunkt") +} + +fun Tidspunkt.tilForrigeMåned() = when (this) { + is DagTidspunkt -> MånedTidspunkt(this.dato.toYearMonth(), uendelighet).forrige() + is MånedTidspunkt -> this.forrige() + else -> throw Feil("Ukjent type tidspunkt") +} + +fun Tidspunkt.tilDagEllerFørsteDagIPerioden() = when (this) { + is DagTidspunkt -> this + is MånedTidspunkt -> DagTidspunkt(this.måned.atDay(1), this.uendelighet) + else -> throw Feil("Ukjent type tidspunkt") +} + +fun Tidspunkt.tilDagEllerSisteDagIPerioden() = when (this) { + is DagTidspunkt -> this + is MånedTidspunkt -> DagTidspunkt(this.måned.atEndOfMonth(), this.uendelighet) + else -> throw Feil("Ukjent type tidspunkt") +} + +fun Tidspunkt.neste() = flytt(1) +fun Tidspunkt.forrige() = flytt(-1) +fun Tidspunkt.erRettFør(tidspunkt: Tidspunkt) = neste() == tidspunkt +fun Tidspunkt.erEndelig(): Boolean = uendelighet == Uendelighet.INGEN +fun Tidspunkt.erUendeligLengeSiden(): Boolean = uendelighet == Uendelighet.FORTID +fun Tidspunkt.erUendeligLengeTil(): Boolean = uendelighet == Uendelighet.FREMTID + +fun Tidspunkt.somEndelig() = medUendelighet(Uendelighet.INGEN) +fun Tidspunkt.somUendeligLengeSiden() = medUendelighet(Uendelighet.FORTID) +fun Tidspunkt.somUendeligLengeTil() = medUendelighet(Uendelighet.FREMTID) +fun Tidspunkt.somFraOgMed() = when (uendelighet) { + Uendelighet.FREMTID -> medUendelighet(Uendelighet.INGEN) + else -> this +} + +fun Tidspunkt.somTilOgMed() = when (uendelighet) { + Uendelighet.FORTID -> medUendelighet(Uendelighet.INGEN) + else -> this +} + +fun Tidspunkt.tilYearMonth() = this.tilInneværendeMåned().tilYearMonth() +fun Tidspunkt.tilYearMonthEllerNull() = this.tilInneværendeMåned().tilYearMonthEllerNull() +fun Tidspunkt.tilYearMonthEllerUendeligFortid() = this.tilInneværendeMåned().tilYearMonthEllerNull() ?: TIDENES_MORGEN.toYearMonth() +fun Tidspunkt.tilYearMonthEllerUendeligFramtid() = this.tilInneværendeMåned().tilYearMonthEllerNull() ?: TIDENES_ENDE.toYearMonth() +fun Tidspunkt.tilLocalDate() = this.tilDagEllerSisteDagIPerioden().tilLocalDate() +fun Tidspunkt.tilLocalDateEllerNull() = this.tilDagEllerSisteDagIPerioden().tilLocalDateEllerNull() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidsrom/TidspunktClosedRange.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidsrom/TidspunktClosedRange.kt new file mode 100644 index 000000000..2634143bc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidsrom/TidspunktClosedRange.kt @@ -0,0 +1,108 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom + +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.neste +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somEndelig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeTil + +/** + * ClosedRange-implementasjon for , som gir -operatoren. + * Håndterer at har en uendelighetskomponent; uendelig FORTID, uendelig FREMTID, eller ingen INGEN uendelighet + * Her brukes følgende notasjon: + * A,B,C - tidspunkt uten uendelighet. B er tidspunktet 1 tidsenhet etter A, og C er 1 tidsenhet etter B + * <--A - tidspunkt som peker bakover mot uendelig fortid + * B--> - tidspunkt som peker fremover mot uendelig fremtid + * Følgende gjelder: + * A..A = [A] + * A..B = [A,B] + * A..C = [A,B,C] + * B..A = [] + * <--A..A = [<--A] + * <--A..B = [<--A,B] + * <--A..<--A = [<--A] + * <--A..<--C = [<--A,B,C] + * <--B..A = [<--A] + * <--B..<--A = [<--A] + * A..A--> = [A-->] + * A-->..A--> = [A-->] + * A-->..C--> = [A,B,C-->] + * B..A--> = [B-->] + * B-->..A--> = [B-->] + * <--A..A--> = [<--A,B-->] + * <--A..B--> = [<--A,B-->] + * <--A..C--> = [<--A,B,C-->] + * <--B..A--> = [<--A,B-->] + * <--E..A--> = [<--A,B,C,D,E-->] + * A-->..<--A = [A] + * A-->..<--B = [A,B] + * A-->..<--E = [A,B,C,D,E] + * B-->..<--A = [] + */ +data class TidspunktClosedRange( + override val start: Tidspunkt, + override val endInclusive: Tidspunkt, +) : Iterable>, + ClosedRange> { + + override fun toString(): String = + "$start - $endInclusive" + + override fun iterator(): Iterator> = object : Iterator> { + private var tidspunkt = when { + // Bruk tidligste mulige ankerpunkt hvis start peker bakocer + // Dvs <--C..A blir til <--A..A + start.erUendeligLengeSiden() -> minOf(start.somEndelig(), endInclusive.somEndelig()) + .somUendeligLengeSiden() + else -> start + } + + private var tilOgMed = when { + // Bruk seneste mulige ankerpunkt hvis endInclusive peker fremover + // Dvs C..A--> blir til C..C--> + endInclusive.erUendeligLengeTil() -> maxOf(start.somEndelig(), endInclusive.somEndelig()) + .somUendeligLengeTil() + else -> endInclusive + } + + override fun hasNext() = tidspunkt.endeligMindreEllerLik(tilOgMed) + + override fun next(): Tidspunkt { + return when { + // Håndter spesialtilfellet <--A..A--> + tidspunkt.erUendeligLengeSiden() && tilOgMed.erUendeligLengeTil() && + tidspunkt.endeligLik(tilOgMed) -> + tidspunkt.also { + // Flytter gjeldnde tidspunkt til B-->, og må flytte tilOgMed tilsvarende for å få det med. + tidspunkt = tilOgMed.neste() + tilOgMed = tidspunkt + } + + // Hvis fraOgMed peker fremover, A-->, og tilOgMed peker bakover, <--C, + // skal vi generere de endelige tidspunktene i overlappen mellom dem, her [A,B,C] + tidspunkt.erUendeligLengeTil() && tilOgMed.erUendeligLengeSiden() -> + tidspunkt.somEndelig().also { tidspunkt = it.neste() } + + // Håndter tilfellet der tilOgMed = C-->, og tidspunkt er fremme ved C. + // Da returrnes tilOgMed, altså C-->. Tidspunkt flyttes én frem, til D-->, som avslutter iterasjonen + tilOgMed.erUendeligLengeTil() && tidspunkt.endeligLik(tilOgMed) -> + tilOgMed.also { tidspunkt = it.neste() } + + // Hvis tidspunkt = fraOgMed = A--> og tilOgMed = C--> + // så returnes A, og tidspunkt settes til neste endelige tidspunkt, B + tidspunkt.erUendeligLengeTil() && tilOgMed.erUendeligLengeTil() && + tidspunkt.endeligMindre(tilOgMed) -> + tidspunkt.somEndelig().also { tidspunkt = it.neste() } + + // Ellers returner vi tidspunkt, og forvisser om at tidspunkt endelig og flytter det én tidsenhet frem + else -> tidspunkt.also { tidspunkt = it.somEndelig().neste() } + } + } + } +} + +operator fun Tidspunkt.rangeTo(tilOgMed: Tidspunkt): TidspunktClosedRange = + TidspunktClosedRange(this, tilOgMed) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidsrom/Tidsrom.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidsrom/Tidsrom.kt new file mode 100644 index 000000000..c9e11ee40 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidsrom/Tidsrom.kt @@ -0,0 +1,80 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje + +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erEndelig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.forrige +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.neste +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somEndelig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo + +fun Tidslinje.fraOgMed() = + this.perioder().firstOrNull()?.let { + if (it.fraOgMed.erEndelig()) { + it.fraOgMed + } else { + minOf(it.fraOgMed.somEndelig(), it.tilOgMed.somEndelig()).somUendeligLengeSiden() + } + } + +fun Tidslinje.tilOgMed() = + this.perioder().lastOrNull()?.let { + if (it.tilOgMed.erEndelig()) { + it.tilOgMed + } else { + maxOf(it.fraOgMed.somEndelig(), it.tilOgMed.somEndelig()).somUendeligLengeTil() + } + } + +fun Iterable>.fraOgMed() = this + .mapNotNull { it.fraOgMed() } + .minsteEllerNull() + +fun Iterable>.tilOgMed() = this + .mapNotNull { it.tilOgMed() } + .størsteEllerNull() + +fun Tidslinje.tidsrom(): Collection> = when { + this.perioder().isEmpty() -> emptyList() + else -> (perioder().first().fraOgMed.rangeTo(perioder().last().tilOgMed)).toList() +} + +fun Iterable>.tidsrom(): Collection> { + val fraOgMed = fraOgMed() ?: return emptyList() + val tilOgMed = tilOgMed() ?: return emptyList() + return (fraOgMed..tilOgMed).toList() +} + +fun tidsrom(vararg tidslinjer: Tidslinje<*, T>) = tidslinjer.toList().tidsrom() + +private fun Iterable>.størsteEllerNull() = + this.reduceOrNull { acc, neste -> størsteAv(acc, neste) } + +private fun Iterable>.minsteEllerNull() = + this.reduceOrNull { acc, neste -> minsteAv(acc, neste) } + +internal fun størsteAv(t1: Tidspunkt, t2: Tidspunkt): Tidspunkt = + if (t1.erUendeligLengeTil() && t2.erEndelig() && t1.endeligMindreEllerLik(t2)) { + t2.neste().somUendeligLengeTil() + } else if (t2.erUendeligLengeTil() && t1.erEndelig() && t2.endeligMindreEllerLik(t1)) { + t1.neste().somUendeligLengeTil() + } else if (t1.erUendeligLengeTil() || t2.erUendeligLengeTil()) { + maxOf(t1.somEndelig(), t2.somEndelig()).somUendeligLengeTil() + } else { + maxOf(t1, t2) + } + +internal fun minsteAv(t1: Tidspunkt, t2: Tidspunkt): Tidspunkt = + if (t1.erUendeligLengeSiden() && t2.erEndelig() && t2.endeligMindreEllerLik(t1)) { + t2.forrige().somUendeligLengeSiden() + } else if (t2.erUendeligLengeSiden() && t1.erEndelig() && t1.endeligMindreEllerLik(t2)) { + t1.forrige().somUendeligLengeSiden() + } else if (t1.erUendeligLengeSiden() || t2.erUendeligLengeSiden()) { + minOf(t1.somEndelig(), t2.somEndelig()).somUendeligLengeSiden() + } else { + minOf(t1, t2) + } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/Beskj\303\246reTidslinje.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/Beskj\303\246reTidslinje.kt" new file mode 100644 index 000000000..ca788a88d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/Beskj\303\246reTidslinje.kt" @@ -0,0 +1,72 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.fraOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.innholdForTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.tidslinjeFraTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo +import no.nav.familie.ba.sak.kjerne.tidslinje.tilOgMed + +/** + * Extension-metode for å beskjære (forkorte) en tidslinje etter en annen tidslinje + * Etter beskjæringen vil tidslinjen maksimalt strekke seg fra [tidslinje]s fraOgMed() og til [tidslinje]s tilOgMed() + * Perioder som ligger helt utenfor grensene vil forsvinne. + * Perioden i hver ende som ligger delvis innenfor, vil forkortes. + * Hvis ny og eksisterende grenseverdi begge er uendelige, vil den nye benyttes + * Beskjæring mot tom tidslinje vil gi tom tidslinje + */ +fun Tidslinje.beskjærEtter(tidslinje: Tidslinje<*, T>): Tidslinje = when { + tidslinje.tidsrom().isEmpty() -> TomTidslinje() + else -> beskjær(tidslinje.fraOgMed()!!, tidslinje.tilOgMed()!!) +} + +/** + * Extension-metode for å beskjære (forkorte) en tidslinje etter til-og-med fra en annen tidslinje + * Etter beskjæringen vil tidslinjen maksimalt strekke seg fra [this]s fraOgMed() og til [tidslinje]s tilOgMed() + * Perioder som ligger helt utenfor grensene vil forsvinne. + * Perioden i hver ende som ligger delvis innenfor, vil forkortes. + * Hvis ny og eksisterende grenseverdi begge er uendelige, vil den nye benyttes + * Beskjæring mot tom tidslinje vil gi tom tidslinje + */ +fun Tidslinje.beskjærTilOgMedEtter(tidslinje: Tidslinje<*, T>): Tidslinje = when { + tidslinje.tidsrom().isEmpty() -> TomTidslinje() + else -> beskjær(this.fraOgMed()!!, tidslinje.tilOgMed()!!) +} + +/** + * Extension-metode for å beskjære (forkorte) en tidslinje + * Etter beskjæringen vil tidslinjen maksimalt strekke seg fra innsendt [fraOgMed] og til [tilOgMed] + * Perioder som ligger helt utenfor grensene vil forsvinne. + * Perioden i hver ende som ligger delvis innenfor, vil forkortes. + * Uendelige endepunkter vil beskjæres til endelig hvis [fraOgMed] eller [tilOgMed] er endelige + * Endelige endepunkter som beskjæres mot uendelige endepunkter, beholdes + * Hvis ny og eksisterende grenseverdi begge er uendelige, vil den mest ekstreme benyttes + */ +fun Tidslinje.beskjær(fraOgMed: Tidspunkt, tilOgMed: Tidspunkt): Tidslinje { + if (tidsrom().isEmpty()) { + return this + } + + val fom: Tidspunkt = when { + // <--A..F begrenset med <--C..F må sjekke verdier fra og med <--A + // <--C..F begrenset med <--A..F trenger bare å sjekke verdier fra og med <--C + // Dvs i tilfellet de tidslinjens fom og begrensningens fom begge peker bakover, skal tidslinjens fom brukes + fraOgMed()!!.erUendeligLengeSiden() && fraOgMed.erUendeligLengeSiden() -> this.fraOgMed()!! + else -> maxOf(fraOgMed()!!, fraOgMed) + } + val tom: Tidspunkt = when { + // A..F--> begrenset med A..C--> må sjekke verdier frem til og med F--> + // A..C--> begrenset med A..F--> trenger bare å sjekke verdier frem til og med C--> + // Dvs i tilfellet de tidslinjens tom og begrensningens tom begge peker fremover, skal tidslinjens tom brukes + tilOgMed()!!.erUendeligLengeTil() && tilOgMed.erUendeligLengeTil() -> this.tilOgMed()!! + else -> minOf(tilOgMed()!!, tilOgMed) + } + + return (fom..tom).tidslinjeFraTidspunkt { tidspunkt -> innholdForTidspunkt(tidspunkt) } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/EndreTid.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/EndreTid.kt new file mode 100644 index 000000000..52f25456f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/EndreTid.kt @@ -0,0 +1,107 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.fraOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.innholdForTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.tidslinjeFraTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.tilVerdi +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.forrige +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somEndelig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilForrigeMåned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilFørsteDagIMåneden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilInneværendeMåned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilNesteMåned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilSisteDagIMåneden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo +import no.nav.familie.ba.sak.kjerne.tidslinje.tilOgMed + +/** + * Extension-metode for å konvertere fra Dag-tidslinje til Måned-tidslinje + * mapper-funksjonen tar inn listen av alle dagverdiene i én måned, og returner verdien måneden skal ha + * Dagverdiene kommer i samme rekkefølge som dagene i måneden, og vil ha null-verdi hvis dagen ikke har en verdi + */ +fun Tidslinje.tilMåned(mapper: (List) -> R?): Tidslinje { + val fraOgMed = fraOgMed()?.tilInneværendeMåned() + val tilOgMed = tilOgMed()?.tilInneværendeMåned() + + if (fraOgMed == null || tilOgMed == null) { + return TomTidslinje() + } + + return (fraOgMed..tilOgMed).tidslinjeFraTidspunkt { måned -> + val dagerIMåned = måned.tilFørsteDagIMåneden()..måned.tilSisteDagIMåneden() + val innholdAlleDager = dagerIMåned.map { dag -> innholdForTidspunkt(dag).innhold } + mapper(innholdAlleDager).tilVerdi() + } +} + +/** + * Extention-metode som konverterer en dag-basert tidslinje til en måned-basert tidslinje. + * -funksjonen tar inn verdiene fra de to dagene før og etter månedsskiftet, + * det vil si verdiene fra siste dag i forrige måned og første dag i inneværemde måned. + * -funksjonen kalles bare dersom begge dagene har en verdi. + * Return-verdien er innholdet som blir brukt for inneværende måned. + * Hvis retur-verdien er , vil den resulterende måneden mangle verdi. + * Funksjonen vil bruke månedsskiftene fra måneden før tidslinjens frem til og med måneden etter + */ +fun Tidslinje.tilMånedFraMånedsskifteIkkeNull( + mapper: (innholdSisteDagForrigeMåned: I, innholdFørsteDagDenneMåned: I) -> R?, +): Tidslinje { + val fraOgMed = fraOgMed() + val tilOgMed = tilOgMed() + + return if (fraOgMed == null || tilOgMed == null) { + TomTidslinje() + } else { + (fraOgMed.tilForrigeMåned()..tilOgMed.tilNesteMåned()).tidslinjeFraTidspunkt { måned -> + val innholdSisteDagForrigeMåned = innholdForTidspunkt(måned.forrige().tilSisteDagIMåneden()) + val innholdFørsteDagDenneMåned = innholdForTidspunkt(måned.tilFørsteDagIMåneden()) + + innholdSisteDagForrigeMåned + .mapVerdi { s -> innholdFørsteDagDenneMåned.mapVerdi { mapper(s, it) } }.tilVerdi() + } + } +} + +/** + * Extension-metode for å konvertere fra Måned-tidslinje til Dag-tidslinje + * Første dag i fra-og-med-måneden brukes som første dag i perioden + * Siste dag i til-og-med-måneden brukes som siste dag i perioden + */ +fun Tidslinje.tilDag(): Tidslinje { + val månedTidslinje = this + + return object : Tidslinje() { + override fun lagPerioder(): Collection> = + månedTidslinje.perioder().map { + Periode( + it.fraOgMed.tilFørsteDagIMåneden(), + it.tilOgMed.tilSisteDagIMåneden(), + it.innhold, + ) + } + } +} + +/** + * Extension-metode for å konvertere en tidslinje med uendelig fra-og-med og/eller til-og-med + * til en endelig tidslinje basert på de underliggende tidspunktene + * Tidslinjen + * '' + * vil etter konvertering se slik ut + * aaa bbbb d + */ +fun Tidslinje.somEndelig(): Tidslinje { + val tidslinje = this + return object : Tidslinje() { + override fun lagPerioder(): Collection> = + tidslinje.perioder().map { + Periode(it.fraOgMed.somEndelig(), it.tilOgMed.somEndelig(), it.innhold) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/FiltrerTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/FiltrerTidslinje.kt new file mode 100644 index 000000000..69b9afd22 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/FiltrerTidslinje.kt @@ -0,0 +1,55 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.beskjærEtter + +/** + * Extension-metode for å filtrere tidslinjen mot et filter + * Resultatet kan bli en kortere tidslinje, og en TomTidslinje hvis ingen perioder passerer filteret + * Merk at fraOgMed() og tilOgMed() på tidslinjen vil føre til at periodene genereres for underliggende tidslinje + */ +fun Tidslinje.filtrer(filter: (I?) -> Boolean): Tidslinje { + val tidslinje = this + val fraOgMed = tidslinje.perioder().firstOrNull { filter(it.innhold) }?.fraOgMed + val tilOgMed = tidslinje.perioder().lastOrNull { filter(it.innhold) }?.tilOgMed + + // fraOgMed og tilOgMed vil enten begge ha verdi eller begge være null + // Sjekker begge for å få smart cast for begge i metodekallene under + return if (fraOgMed == null || tilOgMed == null) { + TomTidslinje() + } else { + object : Tidslinje() { + override fun lagPerioder(): Collection> = + tidslinje.perioder().filter { filter(it.innhold) } + } + } +} + +fun Tidslinje.filtrerIkkeNull(): Tidslinje = filtrer { it != null } +fun Tidslinje.filtrerIkkeNull(filter: (I) -> Boolean): Tidslinje = filtrer { it != null && filter(it) } + +/** + * Extension-metode for å filtrere tidslinjen mot en boolsk tidslinje + * Resultatet får samme lengde som tidslinjen det opereres på + * Det vil finnes perioder som tilsvarer periodene fra kilde-tidslinjen, + * men innholdet blir null hvis den boolske tidslinjen er false + */ +fun Tidslinje.filtrerMed(boolskTidslinje: Tidslinje): Tidslinje { + return this.kombinerMed(boolskTidslinje) { innhold, erSann -> + when (erSann) { + true -> innhold + else -> null + } + }.beskjærEtter(this) +} + +/** + * Extension-metode for å filtrere innholdet i en map av tidslinjer + */ +fun Map>.filtrerHverKunVerdi( + filter: (I) -> Boolean, +) = mapValues { (_, tidslinje) -> tidslinje.filtrer { if (it != null) filter(it) else false } } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/FlyttTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/FlyttTidslinje.kt new file mode 100644 index 000000000..ce65fc0b2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/FlyttTidslinje.kt @@ -0,0 +1,20 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet + +/** + * Extension-metode for å forskyve tidslinjen frem eller tilbake + * Negative flytter tidslinjen tilbake + */ +fun Tidslinje.forskyv(tidsenheter: Long): Tidslinje { + val tidslinje = this + + return object : Tidslinje() { + override fun lagPerioder(): Collection> = + tidslinje.perioder().map { + Periode(it.fraOgMed.flytt(tidsenheter), it.tilOgMed.flytt(tidsenheter), it.innhold) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/MapTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/MapTidslinje.kt new file mode 100644 index 000000000..67b490f78 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/MapTidslinje.kt @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.Innhold +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.innholdForTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.tidslinjeFraTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom + +/** + * Extension-metode for å map'e innhold fra en type og verdi til en annen + * Hvis det nå oppstår tilgrensende perioder med samme innhold, slås de sammen + */ +fun Tidslinje.map(mapper: (I?) -> R?): Tidslinje = + tidsrom().tidslinjeFraTidspunkt { tidspunkt -> + val innholdsresultat = this.innholdForTidspunkt(tidspunkt) + when (innholdsresultat.harInnhold) { + false -> Innhold.utenInnhold() + else -> Innhold(mapper(innholdsresultat.innhold)) + } + } + +/** + * Extension-metode for å map'e innhold som ikke er fra en type og verdi til en annen + * Hvis det nå oppstår tilgrensende perioder med samme innhold, slås de sammen + */ +fun Tidslinje.mapIkkeNull(mapper: (I) -> R?): Tidslinje = + tidsrom().tidslinjeFraTidspunkt { tidspunkt -> + val innholdsresultat = this.innholdForTidspunkt(tidspunkt) + when (innholdsresultat.harInnhold) { + false, (innholdsresultat.innhold == null) -> Innhold.utenInnhold() + else -> Innhold(mapper(innholdsresultat.innhold!!)) + } + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/Zip.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/Zip.kt new file mode 100644 index 000000000..a4cf4dce4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/Zip.kt @@ -0,0 +1,44 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.månedPeriodeAv +import no.nav.familie.ba.sak.kjerne.tidslinje.periodeAv +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tilTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.ZipPadding.ETTER +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.ZipPadding.FØR +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.ZipPadding.INGEN_PADDING +import java.time.YearMonth + +/** + * Returnerer en tidslinje med par av hvert etterfølgende element i tidslinjen. + * + * val aTilD = "abcd" + * val bokstavTidslinje = aTilF.tilCharTidslinje(jan(2020)) + * val bokstavParTidslinje = bokstavTidslinje.zipMedNeste(ZipPadding.FØR) + * + * println(bokstavTidslinje) // + * 2020-01 - 2020-01: a | 2020-02 - 2020-02: b | 2020-03 - 2020-03: c | 2020-04 - 2020-04: d + * + * println(bokstavParTidslinje) // + * 2020-01 - 2020-01: (null, a) | 2020-02 - 2020-02: (a, b) | 2020-03 - 2020-03: (b, c) | 2020-04 - 2020-04: (c, d) + */ +enum class ZipPadding { + FØR, + ETTER, + INGEN_PADDING, +} + +fun Tidslinje.zipMedNeste(zipPadding: ZipPadding = INGEN_PADDING): Tidslinje, Måned> { + val padding = listOf( + månedPeriodeAv(YearMonth.now(), YearMonth.now(), null), + ) + + return when (zipPadding) { + FØR -> padding + perioder() + ETTER -> perioder() + padding + INGEN_PADDING -> perioder() + }.zipWithNext { forrige, denne -> + periodeAv(denne.fraOgMed, denne.tilOgMed, Pair(forrige.innhold, denne.innhold)) + }.tilTidslinje() +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingController.kt new file mode 100644 index 000000000..8b1e7e047 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingController.kt @@ -0,0 +1,51 @@ +package no.nav.familie.ba.sak.kjerne.tilbakekreving + +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/tilbakekreving") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class TilbakekrevingController( + private val tilgangService: TilgangService, + private val tilbakekrevingService: TilbakekrevingService, +) { + + @PostMapping("/{behandlingId}/forhandsvis-varselbrev") + fun hentForhåndsvisningVarselbrev( + @PathVariable + behandlingId: Long, + @RequestBody + forhåndsvisTilbakekrevingsvarselbrevRequest: ForhåndsvisTilbakekrevingsvarselbrevRequest, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.VEILEDER, + handling = "hent forhåndsvisning av varselbrev for tilbakekreving", + ) + + return ResponseEntity.ok( + Ressurs.success( + tilbakekrevingService.hentForhåndsvisningVarselbrev( + behandlingId, + forhåndsvisTilbakekrevingsvarselbrevRequest, + ), + ), + ) + } +} + +data class ForhåndsvisTilbakekrevingsvarselbrevRequest( + val fritekst: String, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingKlient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingKlient.kt new file mode 100644 index 000000000..78a5af5f4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingKlient.kt @@ -0,0 +1,115 @@ +package no.nav.familie.ba.sak.kjerne.tilbakekreving + +import no.nav.familie.ba.sak.common.kallEksternTjeneste +import no.nav.familie.ba.sak.common.kallEksternTjenesteRessurs +import no.nav.familie.http.client.AbstractRestClient +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.klage.FagsystemVedtak +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandling +import no.nav.familie.kontrakter.felles.tilbakekreving.ForhåndsvisVarselbrevRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.KanBehandlingOpprettesManueltRespons +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettManueltTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.client.RestOperations +import java.net.URI + +typealias TilbakekrevingId = String + +data class FinnesBehandlingsresponsDto(val finnesÅpenBehandling: Boolean) + +@Component +class TilbakekrevingKlient( + @Value("\${FAMILIE_TILBAKE_API_URL}") private val familieTilbakeUri: URI, + @Qualifier("jwtBearer") restOperations: RestOperations, +) : AbstractRestClient(restOperations, "Tilbakekreving") { + + fun hentForhåndsvisningVarselbrev(forhåndsvisVarselbrevRequest: ForhåndsvisVarselbrevRequest): ByteArray { + val uri = URI.create("$familieTilbakeUri/dokument/forhandsvis-varselbrev") + + return kallEksternTjeneste( + tjeneste = "familie-tilbake", + uri = uri, + formål = "Henter forhåndsvisning av varselbrev", + ) { + postForEntity( + uri = uri, + payload = forhåndsvisVarselbrevRequest, + httpHeaders = HttpHeaders().apply { + accept = listOf(MediaType.APPLICATION_PDF) + }, + ) + } + } + + fun opprettTilbakekrevingBehandling(opprettTilbakekrevingRequest: OpprettTilbakekrevingRequest): TilbakekrevingId { + val uri = URI.create("$familieTilbakeUri/behandling/v1") + + return kallEksternTjenesteRessurs( + tjeneste = "familie-tilbake", + uri = uri, + formål = "Oppretter behandling for tilbakekreving", + ) { + postForEntity(uri, opprettTilbakekrevingRequest) + } + } + + fun harÅpenTilbakekrevingsbehandling(fagsakId: Long): Boolean { + val uri = URI.create("$familieTilbakeUri/fagsystem/${Fagsystem.BA}/fagsak/$fagsakId/finnesApenBehandling/v1") + + val finnesBehandlingsresponsDto: FinnesBehandlingsresponsDto = kallEksternTjenesteRessurs( + tjeneste = "familie-tilbake", + uri = uri, + formål = "Sjekker om en fagsak har åpen tilbakekrevingsbehandling", + ) { getForEntity(uri) } + + return finnesBehandlingsresponsDto.finnesÅpenBehandling + } + + fun hentTilbakekrevingsbehandlinger(fagsakId: Long): List { + val uri = URI.create("$familieTilbakeUri/fagsystem/${Fagsystem.BA}/fagsak/$fagsakId/behandlinger/v1") + + return kallEksternTjenesteRessurs( + tjeneste = "familie-tilbake", + uri = uri, + formål = "Henter tilbakekrevingsbehandlinger på fagsak", + ) { getForEntity(uri) } + } + + fun hentTilbakekrevingsvedtak(fagsakId: Long): List { + val uri = URI.create("$familieTilbakeUri/fagsystem/${Fagsystem.BA}/fagsak/$fagsakId/vedtak/v1") + + return kallEksternTjenesteRessurs( + tjeneste = "familie-tilbake", + uri = uri, + formål = "Henter tilbakekrevingsvedtak på fagsak", + ) { getForEntity(uri) } + } + + fun kanTilbakekrevingsbehandlingOpprettesManuelt(fagsakId: Long): KanBehandlingOpprettesManueltRespons { + val uri = URI.create( + "$familieTilbakeUri/ytelsestype/${Ytelsestype.BARNETRYGD}/fagsak/$fagsakId/kanBehandlingOpprettesManuelt/v1", + ) + + return kallEksternTjenesteRessurs( + tjeneste = "familie-tilbake", + uri = uri, + formål = "Sjekker om tilbakekrevingsbehandling kan opprettes manuelt", + ) { getForEntity(uri) } + } + + fun opprettTilbakekrevingsbehandlingManuelt(request: OpprettManueltTilbakekrevingRequest): String { + val uri = URI.create("$familieTilbakeUri/behandling/manuelt/task/v1") + + return kallEksternTjenesteRessurs( + tjeneste = "familie-tilbake", + uri = uri, + formål = "Oppretter tilbakekrevingsbehandling manuelt", + ) { postForEntity(uri, request) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingService.kt new file mode 100644 index 000000000..9776a4f53 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingService.kt @@ -0,0 +1,256 @@ +package no.nav.familie.ba.sak.kjerne.tilbakekreving + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.brev.mottaker.BrevmottakerRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.simulering.SimuleringService +import no.nav.familie.ba.sak.kjerne.tilbakekreving.domene.Tilbakekreving +import no.nav.familie.ba.sak.kjerne.tilbakekreving.domene.TilbakekrevingRepository +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollRepository +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingstype +import no.nav.familie.kontrakter.felles.tilbakekreving.Brevmottaker +import no.nav.familie.kontrakter.felles.tilbakekreving.FeilutbetaltePerioderDto +import no.nav.familie.kontrakter.felles.tilbakekreving.ForhåndsvisVarselbrevRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.ManuellAdresseInfo +import no.nav.familie.kontrakter.felles.tilbakekreving.MottakerType +import no.nav.familie.kontrakter.felles.tilbakekreving.MottakerType.FULLMEKTIG +import no.nav.familie.kontrakter.felles.tilbakekreving.MottakerType.VERGE +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettManueltTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Verge +import no.nav.familie.kontrakter.felles.tilbakekreving.Vergetype +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class TilbakekrevingService( + private val tilbakekrevingRepository: TilbakekrevingRepository, + private val vedtakRepository: VedtakRepository, + private val totrinnskontrollRepository: TotrinnskontrollRepository, + private val brevmottakerRepository: BrevmottakerRepository, + private val simuleringService: SimuleringService, + private val persongrunnlagService: PersongrunnlagService, + private val arbeidsfordelingService: ArbeidsfordelingService, + private val tilbakekrevingKlient: TilbakekrevingKlient, + private val personidentService: PersonidentService, + private val personopplysningerService: PersonopplysningerService, + private val featureToggleService: FeatureToggleService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, +) { + + fun validerRestTilbakekreving(restTilbakekreving: RestTilbakekreving?, behandlingId: Long) { + val feilutbetaling = simuleringService.hentFeilutbetaling(behandlingId) + validerVerdierPåRestTilbakekreving(restTilbakekreving, feilutbetaling) + } + + @Transactional + fun lagreTilbakekreving(restTilbakekreving: RestTilbakekreving, behandlingId: Long): Tilbakekreving? { + val behandling = behandlingHentOgPersisterService.hent(behandlingId = behandlingId) + + val tilbakekreving = Tilbakekreving( + begrunnelse = restTilbakekreving.begrunnelse, + behandling = behandling, + valg = restTilbakekreving.valg, + varsel = restTilbakekreving.varsel, + tilbakekrevingsbehandlingId = tilbakekrevingRepository + .findByBehandlingId(behandling.id)?.tilbakekrevingsbehandlingId, + ) + + tilbakekrevingRepository.deleteByBehandlingId(behandlingId) + return tilbakekrevingRepository.save(tilbakekreving) + } + + fun hentTilbakekrevingsvalg(behandlingId: Long): Tilbakekrevingsvalg? { + return tilbakekrevingRepository.findByBehandlingId(behandlingId)?.valg + } + + fun slettTilbakekrevingPåBehandling(behandlingId: Long) = + tilbakekrevingRepository.findByBehandlingId(behandlingId)?.let { tilbakekrevingRepository.delete(it) } + + fun hentForhåndsvisningVarselbrev( + behandlingId: Long, + forhåndsvisTilbakekrevingsvarselbrevRequest: ForhåndsvisTilbakekrevingsvarselbrevRequest, + ): ByteArray { + val vedtak = vedtakRepository.findByBehandlingAndAktivOptional(behandlingId) + ?: throw Feil( + "Fant ikke vedtak for behandling $behandlingId ved forhåndsvisning av varselbrev" + + " for tilbakekreving.", + ) + + val persongrunnlag = persongrunnlagService.hentAktivThrows(behandlingId) + val arbeidsfordeling = arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId) + val institusjon = hentTilbakekrevingInstitusjon(vedtak.behandling.fagsak) + val verge = hentVerge(vedtak.behandling.verge?.ident) + + return tilbakekrevingKlient.hentForhåndsvisningVarselbrev( + forhåndsvisVarselbrevRequest = ForhåndsvisVarselbrevRequest( + varseltekst = forhåndsvisTilbakekrevingsvarselbrevRequest.fritekst, + ytelsestype = Ytelsestype.BARNETRYGD, + behandlendeEnhetId = arbeidsfordeling.behandlendeEnhetId, + behandlendeEnhetsNavn = arbeidsfordeling.behandlendeEnhetNavn, + språkkode = persongrunnlag.søker.målform.tilSpråkkode(), + feilutbetaltePerioderDto = FeilutbetaltePerioderDto( + sumFeilutbetaling = simuleringService.hentFeilutbetaling(behandlingId).toLong(), + perioder = hentTilbakekrevingsperioderISimulering( + simuleringService.hentSimuleringPåBehandling(behandlingId), + featureToggleService.isEnabled(FeatureToggleConfig.ER_MANUEL_POSTERING_TOGGLE_PÅ), + ), + ), + fagsystem = Fagsystem.BA, + eksternFagsakId = vedtak.behandling.fagsak.id.toString(), + ident = persongrunnlag.søker.aktør.aktivFødselsnummer(), + saksbehandlerIdent = SikkerhetContext.hentSaksbehandlerNavn(), + verge = verge, + institusjon = institusjon, + ), + ) + } + + fun søkerHarÅpenTilbakekreving(fagsakId: Long): Boolean = + tilbakekrevingKlient.harÅpenTilbakekrevingsbehandling(fagsakId) + + fun opprettTilbakekreving(behandling: Behandling): TilbakekrevingId = + tilbakekrevingKlient.opprettTilbakekrevingBehandling(lagOpprettTilbakekrevingRequest(behandling)) + + fun lagOpprettTilbakekrevingRequest(behandling: Behandling): OpprettTilbakekrevingRequest { + val personopplysningGrunnlag = persongrunnlagService.hentAktivThrows(behandlingId = behandling.id) + + val enhet = arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandling.id) + + val aktivtVedtak = vedtakRepository.findByBehandlingAndAktivOptional(behandling.id) + ?: throw Feil("Fant ikke aktivt vedtak på behandling ${behandling.id}") + + val totrinnskontroll = totrinnskontrollRepository.findByBehandlingAndAktiv(behandling.id) + + val revurderingsvedtaksdato = aktivtVedtak.vedtaksdato?.toLocalDate() ?: throw Feil( + message = "Finner ikke revurderingsvedtaksdato på vedtak ${aktivtVedtak.id} " + + "ved iverksetting av tilbakekreving mot familie-tilbake", + ) + + val tilbakekreving = tilbakekrevingRepository.findByBehandlingId(behandling.id) + ?: throw Feil("Fant ikke tilbakekreving på behandling ${behandling.id}") + + val institusjon = hentTilbakekrevingInstitusjon(behandling.fagsak) + val verge = hentVerge(behandling.verge?.ident) + + val manuelleBrevMottakere = + brevmottakerRepository.finnBrevMottakereForBehandling(behandling.id).map { baSakBrevMottaker -> + val mottakerType = MottakerType.valueOf(baSakBrevMottaker.type.name) + val vergetype = when { + mottakerType == FULLMEKTIG -> Vergetype.ANNEN_FULLMEKTIG + mottakerType == VERGE && behandling.fagsak.type == FagsakType.NORMAL -> Vergetype.VERGE_FOR_VOKSEN + mottakerType == VERGE && behandling.fagsak.type != FagsakType.NORMAL -> Vergetype.VERGE_FOR_BARN + else -> null + } + + Brevmottaker( + type = mottakerType, + vergetype = vergetype, + navn = baSakBrevMottaker.navn, + manuellAdresseInfo = ManuellAdresseInfo( + adresselinje1 = baSakBrevMottaker.adresselinje1, + adresselinje2 = baSakBrevMottaker.adresselinje2, + postnummer = baSakBrevMottaker.postnummer, + poststed = baSakBrevMottaker.poststed, + landkode = baSakBrevMottaker.landkode, + ), + ) + }.toSet() + + return OpprettTilbakekrevingRequest( + fagsystem = Fagsystem.BA, + regelverk = behandling.kategori.tilRegelverk(), + ytelsestype = Ytelsestype.BARNETRYGD, + eksternFagsakId = behandling.fagsak.id.toString(), + personIdent = personopplysningGrunnlag.søker.aktør.aktivFødselsnummer(), + eksternId = behandling.id.toString(), + behandlingstype = Behandlingstype.TILBAKEKREVING, + // Manuelt opprettet er per nå ikke håndtert i familie-tilbake. + manueltOpprettet = false, + språkkode = personopplysningGrunnlag.søker.målform.tilSpråkkode(), + enhetId = enhet.behandlendeEnhetId, + enhetsnavn = enhet.behandlendeEnhetNavn, + saksbehandlerIdent = totrinnskontroll?.saksbehandlerId ?: SikkerhetContext.hentSaksbehandler(), + varsel = opprettVarsel( + tilbakekreving, + simuleringService.hentSimuleringPåBehandling(behandling.id), + featureToggleService.isEnabled(FeatureToggleConfig.ER_MANUEL_POSTERING_TOGGLE_PÅ), + ), + revurderingsvedtaksdato = revurderingsvedtaksdato, + // Verge er per nå ikke støttet i familie-ba-sak. + verge = verge, + faktainfo = hentFaktainfoForTilbakekreving(behandling, tilbakekreving), + institusjon = institusjon, + manuelleBrevmottakere = manuelleBrevMottakere, + ) + } + + fun opprettTilbakekrevingsbehandlingManuelt(fagsakId: Long): Ressurs { + val kanOpprettesRespons = tilbakekrevingKlient.kanTilbakekrevingsbehandlingOpprettesManuelt(fagsakId) + if (!kanOpprettesRespons.kanBehandlingOpprettes) { + return Ressurs.funksjonellFeil( + frontendFeilmelding = kanOpprettesRespons.melding, + melding = "familie-tilbake svarte nei på om tilbakekreving kunne opprettes", + ) + } + + val behandling = kanOpprettesRespons.kravgrunnlagsreferanse?.toLong() + ?.let { behandlingHentOgPersisterService.hent(it) } + ?.takeIf { it.status == BehandlingStatus.AVSLUTTET } + return if (behandling != null) { + tilbakekrevingKlient.opprettTilbakekrevingsbehandlingManuelt( + OpprettManueltTilbakekrevingRequest( + eksternFagsakId = fagsakId.toString(), + ytelsestype = Ytelsestype.BARNETRYGD, + eksternId = kanOpprettesRespons.kravgrunnlagsreferanse!!, + ), + ) + + Ressurs.success("Tilbakekreving opprettet") + } else { + logger.error("Kan ikke opprette tilbakekrevingsbehandling. Respons inneholder referanse til en ukjent behandling") + Ressurs.funksjonellFeil( + melding = "Kan ikke opprette tilbakekrevingsbehandling. Respons inneholder referanse til en ukjent behandling", + frontendFeilmelding = "Av tekniske årsaker så kan ikke behandling opprettes. Kontakt brukerstøtte for å rapportere feilen.", + ) + } + } + + private fun hentVerge(vergeIdent: String?): Verge? { + val verge: Verge? = if (vergeIdent != null) { + val aktør = personidentService.hentAktør(vergeIdent) + personopplysningerService.hentPersoninfoNavnOgAdresse(aktør).let { + Verge( + vergetype = Vergetype.VERGE_FOR_BARN, + navn = it.navn!!, + personIdent = aktør.aktivFødselsnummer(), + ) + } + } else { + null + } + return verge + } + + companion object { + + private val logger = LoggerFactory.getLogger(TilbakekrevingService::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingUtil.kt new file mode 100644 index 000000000..71c74d745 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingUtil.kt @@ -0,0 +1,105 @@ +package no.nav.familie.ba.sak.kjerne.tilbakekreving + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.simulering.domene.SimuleringsPeriode +import no.nav.familie.ba.sak.kjerne.simulering.domene.ØkonomiSimuleringMottaker +import no.nav.familie.ba.sak.kjerne.simulering.vedtakSimuleringMottakereTilRestSimulering +import no.nav.familie.ba.sak.kjerne.tilbakekreving.domene.Tilbakekreving +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.kontrakter.felles.tilbakekreving.Institusjon +import no.nav.familie.kontrakter.felles.tilbakekreving.Periode +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Varsel +import java.math.BigDecimal +import java.time.LocalDate + +fun validerVerdierPåRestTilbakekreving(restTilbakekreving: RestTilbakekreving?, feilutbetaling: BigDecimal) { + if (feilutbetaling != BigDecimal.ZERO && restTilbakekreving == null) { + throw FunksjonellFeil( + "Simuleringen har en feilutbetaling, men restTilbakekreving var null", + frontendFeilmelding = "Du må velge en tilbakekrevingsstrategi siden det er en feilutbetaling.", + ) + } + if (feilutbetaling == BigDecimal.ZERO && restTilbakekreving != null) { + throw FunksjonellFeil( + "Simuleringen har ikke en feilutbetaling, men restTilbakekreving var ikke null", + frontendFeilmelding = "Du kan ikke opprette en tilbakekreving når det ikke er en feilutbetaling.", + ) + } +} + +fun slåsammenNærliggendeFeilutbtalingPerioder(simuleringsPerioder: List): List { + val perioder: MutableList = mutableListOf() + + val sortedSimuleringsPerioder = + simuleringsPerioder.sortedBy { it.fom }.filter { it.feilutbetaling != BigDecimal.ZERO } + var aktuellFom: LocalDate = sortedSimuleringsPerioder.first().fom + var aktuellTom: LocalDate = sortedSimuleringsPerioder.first().tom + + sortedSimuleringsPerioder.forEach { periode -> + if (aktuellTom.toYearMonth().plusMonths(1) < periode.fom.toYearMonth()) { + perioder.add(Periode(fom = aktuellFom, tom = aktuellTom)) + aktuellFom = periode.fom + } + aktuellTom = periode.tom + } + perioder.add(Periode(fom = aktuellFom, tom = aktuellTom)) + return perioder +} + +fun hentTilbakekrevingsperioderISimulering( + simulering: List<ØkonomiSimuleringMottaker>, + erManuelPosteringTogglePå: Boolean, +): List = + slåsammenNærliggendeFeilutbtalingPerioder( + vedtakSimuleringMottakereTilRestSimulering( + økonomiSimuleringMottakere = simulering, + erManuellPosteringTogglePå = erManuelPosteringTogglePå, + ).perioder, + ) + +fun opprettVarsel( + tilbakekreving: Tilbakekreving?, + simulering: List<ØkonomiSimuleringMottaker>, + erManuelPosteringTogglePå: Boolean, +): Varsel? = + if (tilbakekreving?.valg == Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL) { + val varseltekst = tilbakekreving.varsel ?: throw Feil("Varseltekst er ikke satt") + val restSimulering = vedtakSimuleringMottakereTilRestSimulering( + økonomiSimuleringMottakere = simulering, + erManuellPosteringTogglePå = erManuelPosteringTogglePå, + ) + + Varsel( + varseltekst = varseltekst, + sumFeilutbetaling = restSimulering.feilutbetaling, + perioder = slåsammenNærliggendeFeilutbtalingPerioder(restSimulering.perioder), + ) + } else { + null + } + +fun hentFaktainfoForTilbakekreving(behandling: Behandling, tilbakekreving: Tilbakekreving): Faktainfo = + Faktainfo( + revurderingsårsak = behandling.opprettetÅrsak.visningsnavn, + revurderingsresultat = behandling.resultat.displayName, + tilbakekrevingsvalg = tilbakekreving.valg, + konsekvensForYtelser = emptySet(), + ) + +fun hentTilbakekrevingInstitusjon(fagsak: Fagsak): Institusjon? { + var institusjon: Institusjon? = null + if (fagsak.type == FagsakType.INSTITUSJON) { + requireNotNull( + fagsak.institusjon, + ) { "Fagsaktype er institusjon, men institusjon finnes ikke på fagsak: ${fagsak.id}" } + institusjon = Institusjon(organisasjonsnummer = fagsak.institusjon!!.orgNummer) + } + return institusjon +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingsbehandlingService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingsbehandlingService.kt new file mode 100644 index 000000000..3789d591a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingsbehandlingService.kt @@ -0,0 +1,24 @@ +package no.nav.familie.ba.sak.kjerne.tilbakekreving + +import no.nav.familie.ba.sak.kjerne.tilbakekreving.domene.RestTilbakekrevingsbehandling +import org.springframework.stereotype.Service + +@Service +class TilbakekrevingsbehandlingService(private val tilbakekrevingKlient: TilbakekrevingKlient) { + + fun hentRestTilbakekrevingsbehandlinger(fagsakId: Long): List { + val behandlinger = tilbakekrevingKlient.hentTilbakekrevingsbehandlinger(fagsakId) + return behandlinger.map { + RestTilbakekrevingsbehandling( + behandlingId = it.behandlingId, + opprettetTidspunkt = it.opprettetTidspunkt, + aktiv = it.aktiv, + årsak = it.årsak, + type = it.type, + status = it.status, + resultat = it.resultat, + vedtaksdato = it.vedtaksdato, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/domene/RestTilbakekrevingsbehandling.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/domene/RestTilbakekrevingsbehandling.kt new file mode 100644 index 000000000..7128fb275 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/domene/RestTilbakekrevingsbehandling.kt @@ -0,0 +1,19 @@ +package no.nav.familie.ba.sak.kjerne.tilbakekreving.domene + +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsresultatstype +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsstatus +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingstype +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsårsakstype +import java.time.LocalDateTime +import java.util.UUID + +class RestTilbakekrevingsbehandling( + val behandlingId: UUID, + val opprettetTidspunkt: LocalDateTime, + val aktiv: Boolean, + val årsak: Behandlingsårsakstype?, + val type: Behandlingstype, + val status: Behandlingsstatus, + val resultat: Behandlingsresultatstype?, + val vedtaksdato: LocalDateTime?, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/domene/Tilbakekreving.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/domene/Tilbakekreving.kt new file mode 100644 index 000000000..c57ac1c27 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/domene/Tilbakekreving.kt @@ -0,0 +1,77 @@ +package no.nav.familie.ba.sak.kjerne.tilbakekreving.domene + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.OneToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.simulering.domene.ØkonomiSimuleringMottaker +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Tilbakekreving") +@Table(name = "tilbakekreving") +data class Tilbakekreving( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tilbakekreving_seq_generator") + @SequenceGenerator( + name = "tilbakekreving_seq_generator", + sequenceName = "tilbakekreving_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @OneToOne(optional = false) + @JoinColumn(name = "fk_behandling_id", nullable = false, updatable = false, unique = true) + val behandling: Behandling, + + @Enumerated(EnumType.STRING) + @Column(name = "valg") + var valg: Tilbakekrevingsvalg, + + @Column(name = "varsel") + var varsel: String? = null, + + @Column(name = "begrunnelse") + var begrunnelse: String, + + @Column(name = "tilbakekrevingsbehandling_id") + var tilbakekrevingsbehandlingId: String?, +) : BaseEntitet() { + + override fun hashCode() = id.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other !is ØkonomiSimuleringMottaker) return false + + return (id == other.id) + } + + override fun toString(): String { + return "Tilbakekreving(" + + "id=$id, " + + "behandlingId=${behandling.id} " + + "valg=$valg, " + + "tilbakekrevingsbehandlingId=$tilbakekrevingsbehandlingId" + + ")" + } + + fun tilRestTilbakekreving() = RestTilbakekreving( + valg = valg, + varsel = varsel, + begrunnelse = begrunnelse, + tilbakekrevingsbehandlingId = tilbakekrevingsbehandlingId, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/domene/TilbakekrevingRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/domene/TilbakekrevingRepository.kt new file mode 100644 index 000000000..84807a3f9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/domene/TilbakekrevingRepository.kt @@ -0,0 +1,15 @@ +package no.nav.familie.ba.sak.kjerne.tilbakekreving.domene + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query + +interface TilbakekrevingRepository : JpaRepository { + + @Query(value = "SELECT t FROM Tilbakekreving t JOIN t.behandling b WHERE b.id = :behandlingId") + fun findByBehandlingId(behandlingId: Long): Tilbakekreving? + + @Modifying + @Query(value = "DELETE FROM Tilbakekreving t WHERE t.behandling.id = :behandlingId") + fun deleteByBehandlingId(behandlingId: Long) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/totrinnskontroll/TotrinnskontrollRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/totrinnskontroll/TotrinnskontrollRepository.kt new file mode 100644 index 000000000..308719ae4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/totrinnskontroll/TotrinnskontrollRepository.kt @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.kjerne.totrinnskontroll + +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.domene.Totrinnskontroll +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface TotrinnskontrollRepository : JpaRepository { + @Query("SELECT t FROM Totrinnskontroll t JOIN t.behandling b WHERE b.id = :behandlingId AND t.aktiv = true") + fun findByBehandlingAndAktiv(behandlingId: Long): Totrinnskontroll? +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/totrinnskontroll/TotrinnskontrollService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/totrinnskontroll/TotrinnskontrollService.kt new file mode 100644 index 000000000..40295cfae --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/totrinnskontroll/TotrinnskontrollService.kt @@ -0,0 +1,107 @@ +package no.nav.familie.ba.sak.kjerne.totrinnskontroll + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.domene.Totrinnskontroll +import no.nav.familie.ba.sak.sikkerhet.SaksbehandlerContext +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class TotrinnskontrollService( + private val behandlingService: BehandlingService, + private val totrinnskontrollRepository: TotrinnskontrollRepository, + private val saksbehandlerContext: SaksbehandlerContext, +) { + + fun hentAktivForBehandling(behandlingId: Long): Totrinnskontroll? { + return totrinnskontrollRepository.findByBehandlingAndAktiv(behandlingId) + } + + fun opprettTotrinnskontrollMedSaksbehandler( + behandling: Behandling, + saksbehandler: String = saksbehandlerContext.hentSaksbehandlerSignaturTilBrev(), + saksbehandlerId: String = SikkerhetContext.hentSaksbehandler(), + ): Totrinnskontroll { + return lagreOgDeaktiverGammel( + Totrinnskontroll( + behandling = behandling, + saksbehandler = saksbehandler, + saksbehandlerId = saksbehandlerId, + ), + ) + } + + fun besluttTotrinnskontroll( + behandling: Behandling, + beslutter: String, + beslutterId: String, + beslutning: Beslutning, + kontrollerteSider: List = emptyList(), + ): Totrinnskontroll { + val totrinnskontroll = hentAktivForBehandling(behandlingId = behandling.id) + ?: throw Feil(message = "Kan ikke beslutte et vedtak som ikke er sendt til beslutter") + + totrinnskontroll.beslutter = beslutter + totrinnskontroll.beslutterId = beslutterId + totrinnskontroll.godkjent = beslutning.erGodkjent() + totrinnskontroll.kontrollerteSider = kontrollerteSider + if (totrinnskontroll.erUgyldig()) { + throw FunksjonellFeil( + melding = "Samme saksbehandler kan ikke foreslå og beslutte iverksetting på samme vedtak", + frontendFeilmelding = "Du kan ikke godkjenne ditt eget vedtak", + ) + } + + lagreEllerOppdater(totrinnskontroll) + + behandlingService.oppdaterStatusPåBehandling( + behandlingId = behandling.id, + status = if (beslutning.erGodkjent()) BehandlingStatus.IVERKSETTER_VEDTAK else BehandlingStatus.UTREDES, + ) + + return totrinnskontroll + } + + fun opprettAutomatiskTotrinnskontroll(behandling: Behandling) { + if (!behandling.skalBehandlesAutomatisk) { + throw Feil(message = "Kan ikke opprette automatisk totrinnskontroll ved manuell behandling") + } + + lagreOgDeaktiverGammel( + Totrinnskontroll( + behandling = behandling, + godkjent = true, + saksbehandler = SikkerhetContext.SYSTEM_NAVN, + saksbehandlerId = SikkerhetContext.SYSTEM_FORKORTELSE, + beslutter = SikkerhetContext.SYSTEM_NAVN, + beslutterId = SikkerhetContext.SYSTEM_FORKORTELSE, + ), + ) + } + + fun lagreOgDeaktiverGammel(totrinnskontroll: Totrinnskontroll): Totrinnskontroll { + val aktivTotrinnskontroll = hentAktivForBehandling(totrinnskontroll.behandling.id) + + if (aktivTotrinnskontroll != null && aktivTotrinnskontroll.id != totrinnskontroll.id) { + totrinnskontrollRepository.saveAndFlush(aktivTotrinnskontroll.also { it.aktiv = false }) + } + + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} oppretter totrinnskontroll $totrinnskontroll") + return totrinnskontrollRepository.save(totrinnskontroll) + } + + fun lagreEllerOppdater(totrinnskontroll: Totrinnskontroll): Totrinnskontroll { + return totrinnskontrollRepository.save(totrinnskontroll) + } + + companion object { + + private val logger = LoggerFactory.getLogger(TotrinnskontrollService::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/totrinnskontroll/domene/Totrinnskontroll.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/totrinnskontroll/domene/Totrinnskontroll.kt new file mode 100644 index 000000000..ddefbb8e7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/totrinnskontroll/domene/Totrinnskontroll.kt @@ -0,0 +1,69 @@ +package no.nav.familie.ba.sak.kjerne.totrinnskontroll.domene + +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.StringListConverter +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Totrinnskontroll") +@Table(name = "TOTRINNSKONTROLL") +data class Totrinnskontroll( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "totrinnskontroll_seq_generator") + @SequenceGenerator( + name = "totrinnskontroll_seq_generator", + sequenceName = "totrinnskontroll_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @ManyToOne(optional = false) + @JoinColumn(name = "fk_behandling_id", nullable = false, updatable = false) + val behandling: Behandling, + + @Column(name = "aktiv", nullable = false) + var aktiv: Boolean = true, + + @Column(name = "saksbehandler", nullable = false) + val saksbehandler: String, + + @Column(name = "saksbehandler_id", nullable = false) + val saksbehandlerId: String, + + @Column(name = "beslutter") + var beslutter: String? = null, + + @Column(name = "beslutter_id") + var beslutterId: String? = null, + + @Column(name = "godkjent") + var godkjent: Boolean = false, + + @Column(name = "kontrollerte_sider") + @Convert(converter = StringListConverter::class) + var kontrollerteSider: List = emptyList(), +) : BaseEntitet() { + + fun erBesluttet(): Boolean { + return beslutter != null + } + + fun erUgyldig(): Boolean { + return godkjent && saksbehandlerId == beslutterId && + !(saksbehandler == SikkerhetContext.SYSTEM_NAVN && beslutter == SikkerhetContext.SYSTEM_NAVN) && + !(saksbehandlerId == SikkerhetContext.SYSTEM_FORKORTELSE && beslutterId == SikkerhetContext.SYSTEM_FORKORTELSE) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/Vedtak.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/Vedtak.kt new file mode 100644 index 000000000..5900eca54 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/Vedtak.kt @@ -0,0 +1,45 @@ +package no.nav.familie.ba.sak.kjerne.vedtak + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDateTime + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Vedtak") +@Table(name = "VEDTAK") +class Vedtak( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "vedtak_seq_generator") + @SequenceGenerator(name = "vedtak_seq_generator", sequenceName = "vedtak_seq", allocationSize = 50) + val id: Long = 0, + + @ManyToOne(optional = false) + @JoinColumn(name = "fk_behandling_id", nullable = false, updatable = false) + val behandling: Behandling, + + @Column(name = "vedtaksdato", nullable = true) + var vedtaksdato: LocalDateTime? = null, + + @Column(name = "stonad_brev_pdf", nullable = true) + var stønadBrevPdF: ByteArray? = null, + + @Column(name = "aktiv", nullable = false) + var aktiv: Boolean = true, + +) : BaseEntitet() { + + override fun toString(): String { + return "Vedtak(id=$id, behandling=$behandling, vedtaksdato=$vedtaksdato, aktiv=$aktiv)" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/VedtakRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/VedtakRepository.kt new file mode 100644 index 000000000..681922f26 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/VedtakRepository.kt @@ -0,0 +1,20 @@ +package no.nav.familie.ba.sak.kjerne.vedtak + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import java.time.LocalDateTime + +interface VedtakRepository : JpaRepository { + + @Query(value = "SELECT v FROM Vedtak v WHERE v.behandling.id = :behandlingId") + fun finnVedtakForBehandling(behandlingId: Long): List + + @Query("SELECT v FROM Vedtak v WHERE v.behandling.id = :behandlingId AND v.aktiv = true") + fun findByBehandlingAndAktivOptional(behandlingId: Long): Vedtak? + + @Query("SELECT v FROM Vedtak v WHERE v.behandling.id = :behandlingId AND v.aktiv = true") + fun findByBehandlingAndAktiv(behandlingId: Long): Vedtak + + @Query("SELECT v.vedtaksdato FROM Vedtak v WHERE v.behandling.id = :behandlingId AND v.aktiv = true") + fun finnVedtaksdatoForBehandling(behandlingId: Long): LocalDateTime? +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/VedtakService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/VedtakService.kt new file mode 100644 index 000000000..abe22e74a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/VedtakService.kt @@ -0,0 +1,65 @@ +package no.nav.familie.ba.sak.kjerne.vedtak + +import no.nav.familie.ba.sak.kjerne.brev.DokumentGenereringService +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import org.slf4j.LoggerFactory +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import java.time.LocalDateTime + +@Service +class VedtakService( + private val vedtakRepository: VedtakRepository, + private val dokumentGenereringService: DokumentGenereringService, +) { + + fun hent(vedtakId: Long): Vedtak { + return vedtakRepository.getById(vedtakId) + } + + fun hentAktivForBehandling(behandlingId: Long): Vedtak? { + return vedtakRepository.findByBehandlingAndAktivOptional(behandlingId) + } + + fun hentAktivForBehandlingThrows(behandlingId: Long): Vedtak { + return vedtakRepository.findByBehandlingAndAktiv(behandlingId) + } + + fun hentVedtaksdatoForBehandlingThrows(behandlingId: Long): LocalDateTime { + return vedtakRepository.finnVedtaksdatoForBehandling(behandlingId) + ?: error("Finner ikke vedtaksato for behandling=$behandlingId") + } + + fun oppdater(vedtak: Vedtak): Vedtak { + return if (vedtakRepository.findByIdOrNull(vedtak.id) != null) { + vedtakRepository.saveAndFlush(vedtak) + } else { + error("Forsøker å oppdatere et vedtak som ikke er lagret") + } + } + + fun oppdaterVedtakMedStønadsbrev(vedtak: Vedtak): Vedtak { + return if (vedtak.behandling.erBehandlingMedVedtaksbrevutsending()) { + val brev = dokumentGenereringService.genererBrevForVedtak(vedtak) + vedtakRepository.save(vedtak.also { it.stønadBrevPdF = brev }) + } else { + vedtak + } + } + + /** + * Oppdater vedtaksdato og brev. + * Vi oppdaterer brevet for å garantere å få riktig beslutter og vedtaksdato. + */ + fun oppdaterVedtaksdatoOgBrev(vedtak: Vedtak) { + vedtak.vedtaksdato = LocalDateTime.now() + oppdaterVedtakMedStønadsbrev(vedtak) + + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} beslutter vedtak $vedtak") + } + + companion object { + + private val logger = LoggerFactory.getLogger(VedtakService::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/E\303\230SStandardbegrunnelse.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/E\303\230SStandardbegrunnelse.kt" new file mode 100644 index 000000000..3cf4e5a48 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/E\303\230SStandardbegrunnelse.kt" @@ -0,0 +1,564 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser + +import com.fasterxml.jackson.annotation.JsonValue +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.NullablePeriode +import no.nav.familie.ba.sak.kjerne.brev.domene.BrevBegrunnelseGrunnlagMedPersoner +import no.nav.familie.ba.sak.kjerne.brev.domene.RestBehandlingsgrunnlagForBrev +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.eøs.EØSBegrunnelseMedTriggere + +enum class EØSStandardbegrunnelse : IVedtakBegrunnelse { + INNVILGET_PRIMÆRLAND_UK_STANDARD { + override val sanityApiNavn = "innvilgetPrimarlandUKStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_BARNET_BOR_I_NORGE { + override val sanityApiNavn = "innvilgetPrimarlandBarnetBorINorge" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_BARNETRYGD_ALLEREDE_UTBETALT { + override val sanityApiNavn = "innvilgetPrimarlandBarnetrygdAlleredeUtbetalt" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_UK_BARNETRYGD_ALLEREDEUTBETALT { + override val sanityApiNavn = "innvilgetPrimarlandUkBarnetrygdAlleredeUtbetalt" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE { + override val sanityApiNavn = "innvilgetPrimarlandBeggeForeldreBosattINorge" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_UK_OG_UTLAND_STANDARD { + override val sanityApiNavn = "innvilgetPrimarlandUKOgUtlandStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_SÆRKULLSBARN_ANDRE_BARN_OVERTATT_ANSVAR { + override val sanityApiNavn = "innvilgetPrimarlandSaerkullsbarnAndreBarnOvertattAnsvar" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_UK_TO_ARBEIDSLAND_NORGE_UTBETALER { + override val sanityApiNavn = "innvilgetPrimarlandUkToArbeidslandNorgeUtbetaler" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_TO_ARBEIDSLAND_NORGE_UTBETALER { + override val sanityApiNavn = "innvilgetPrimarlandToArbeidslandNorgeUtbetaler" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_STANDARD { + override val sanityApiNavn = "innvilgetPrimarlandStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_UK_ALENEANSVAR { + override val sanityApiNavn = "innvilgetPrimarlandUKAleneansvar" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_TILLEGGSBEGRUNNELSE_UTBETALING_TIL_ANNEN_FORELDER { + override val sanityApiNavn = "innvilgetTilleggsbegrunnelseUtbetalingTilAnnenForelder" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_BARNET_FLYTTET_TIL_NORGE { + override val sanityApiNavn = "innvilgetPrimarlandBarnetFlyttetTilNorge" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_JOBBER_I_NORGE { + override val sanityApiNavn = "innvilgetPrimarlandBeggeForeldreJobberINorge" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_TO_ARBEIDSLAND_ANNET_LAND_UTBETALER { + override val sanityApiNavn = "innvilgetPrimarlandToArbeidslandAnnetLandUtbetaler" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_SÆRKULLSBARN_ANDRE_BARN { + override val sanityApiNavn = "innvilgetPrimarlandSarkullsbarnAndreBarn" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_UK_TO_ARBEIDSLAND_ANNET_LAND_UTBETALER { + override val sanityApiNavn = "innvilgetPrimarlandUkToArbeidslandAnnetLandUtbetaler" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_ALENEANSVAR { + override val sanityApiNavn = "innvilgetPrimarlandAleneansvar" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_SEKUNDÆRLAND_STANDARD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + override val sanityApiNavn = "innvilgetSekundaerlandStandard" + }, + INNVILGET_SEKUNDÆRLAND_ALENEANSVAR { + override val sanityApiNavn = "innvilgetSekundaerlandAleneansvar" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_TILLEGGSTEKST_NULLUTBETALING { + override val sanityApiNavn = "innvilgetTilleggstekstNullutbetaling" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_SEKUNDÆRLAND_UK_STANDARD { + override val sanityApiNavn = "innvilgetSekundaerlandUkStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_SEKUNDÆRLAND_UK_ALENEANSVAR { + override val sanityApiNavn = "innvilgetSekundaerlandUkAleneansvar" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_SEKUNDÆRLAND_UK_OG_UTLAND_STANDARD { + override val sanityApiNavn = "innvilgetSekundaerlandUkOgUtlandStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_SEKUNDÆRLAND_TO_ARBEIDSLAND_NORGE_UTBETALER { + override val sanityApiNavn = "innvilgetSekundaerlandToArbeidslandNorgeUtbetaler" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_SEKUNDÆRLAND_UK_TO_ARBEIDSLAND_NORGE_UTBETALER { + override val sanityApiNavn = "innvilgetSekundaerlandUkToArbeidslandNorgeUtbetaler" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_TILLEGGSTEKST_SATSENDRING { + override val sanityApiNavn = "innvilgetTilleggstekstSatsendring" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_TILLEGGSTEKST_VALUTAJUSTERING { + override val sanityApiNavn = "innvilgetTilleggstekstValutajustering" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_TILLEGGSTEKST_SATSENDRING_OG_VALUTAJUSTERING { + override val sanityApiNavn = "innvilgetTilleggstekstSatsendringOgValutajustering" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_TILLEGGSTEKST_SEKUNDÆR_DELT_BOSTED_ANNEN_FORELDER_IKKE_SØKT { + override val sanityApiNavn = "innvilgetTilleggstekstSekundaerDeltBostedAnnenForelderIkkeSoekt" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_TILLEGGSTEKST_VEDTAK_FØR_SED { + override val sanityApiNavn = "innvilgetPrimaerlandTilleggstekstVedtakFoerSed" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + + INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_FÅR_YTELSE_I_UTLANDET { + override val sanityApiNavn = "innvilgetSelvstendigRettPrimaerlandFaarYtelseIUtlandet" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + + INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_FÅR_YTELSE_I_UTLANDET { + override val sanityApiNavn = "innvilgetSelvstendigRettSekundaerlandFaarYtelseIUtlandet" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + + INNVILGET_SEKUNDÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE { + override val sanityApiNavn = "innvilgetSekundaerlandBeggeForeldreBosattINorge" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_TILLEGGSTEKST_PRIMÆR_DELT_BOSTED_ANNEN_FORELDER_IKKE_RETT { + override val sanityApiNavn = "innvilgetTilleggstekstPrimaerDeltBostedAnnenForelderIkkeRett" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_TILLEGGSTEKST_SEKUNDÆR_FULL_UTBETALING { + override val sanityApiNavn = "innvilgetTilleggstekstSekundaerFullUtbetaling" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_TILLEGGSTEKST_SEKUNDÆR_AVTALE_DELT_BOSTED { + override val sanityApiNavn = "innvilgetTilleggstekstSekundaerAvtaleDeltBosted" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_TILLEGGSTEKST_SEKUNDÆR_DELT_BOSTED_ANNEN_FORELDER_IKKE_RETT { + override val sanityApiNavn = "innvilgetTilleggstekstsekundaerDeltBostedAnnenForelderIkkeRett" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_GYLDIG_KONTONUMMER_REGISTRERT_EØS { + override val sanityApiNavn = "innvilgetGyldigKontonummerRegistrertEos" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_TILLEGGSTEKST_SEKUNDÆR_IKKE_FÅTT_SVAR_PÅ_SED { + override val sanityApiNavn = "innvilgetTilleggstekstSekundaerIkkeFaattSvarPaaSed" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_TILLEGGESTEKST_UK_FULL_ETTERBETALING { + override val sanityApiNavn = "innvilgetTilleggestekstUkFullEtterbetaling" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_PRIMÆRLAND_DEN_ANDRE_FORELDEREN_UTSENDT_ARBEIDSTAKER { + override val sanityApiNavn = "innvilgetPrimaerlandDenAndreForelderenUtsendtArbeidstaker" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_STANDARD { + override val sanityApiNavn = "innvilgetSelvstendigRettPrimaerlandStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_UK_STANDARD { + override val sanityApiNavn = "innvilgetSelvstendigRettPrimaerlandUkStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_UK_OG_STANDARD { + override val sanityApiNavn = "innvilgetSelvstendigRettPrimaerlandUkOgStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_UTSENDT_ARBEIDSTAKER { + override val sanityApiNavn = "innvilgetSelvstendigRettPrimaerlandUtsendtArbeidstaker" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_STANDARD { + override val sanityApiNavn = "innvilgetSelvstendigRettSekundaerlandStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_UK_STANDARD { + override val sanityApiNavn = "innvilgetSelvstendigRettSekundaerlandUkStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_UK_OG_UTLAND_STANDARD { + override val sanityApiNavn = "innvilgetSelvstendigRettSekundaerlandUkOgUtlandStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET + }, + OPPHØR_EØS_STANDARD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorEosStandard" + }, + OPPHØR_EØS_SØKER_BER_OM_OPPHØR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorEosSokerBerOmOpphor" + }, + OPPHØR_BARN_BOR_IKKE_I_EØS_LAND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorBorIkkeIEtEOSland" + }, + OPPHØR_IKKE_STATSBORGER_I_EØS_LAND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorIkkeStatsborgerIEosLand" + }, + OPPHØR_SENTRUM_FOR_LIVSINTERESSE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorSentrumForLivsinteresse" + }, + OPPHØR_IKKE_ANSVAR_FOR_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorIkkeAnsvarForBarn" + }, + OPPHØR_IKKE_OPPHOLDSRETT_SOM_FAMILIEMEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorIkkeOppholdsrettSomFamiliemedlem" + }, + OPPHØR_SEPARASJONSAVTALEN_GJELDER_IKKE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorSeparasjonsavtaleGjelderIkke" + }, + OPPHØR_SØKER_OG_BARN_BOR_IKKE_I_EØS_LAND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorSoekerOgBarnBorIkkeIEosLand" + }, + OPPHØR_SØKER_BOR_IKKE_I_EØS_LAND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorSoekerBorIkkeIEosLand" + }, + OPPHØR_ARBEIDER_MER_ENN_25_PROSENT_I_ANNET_EØS_LAND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorArbeiderMerEnn25ProsentIAnnetEosLand" + }, + OPPHØR_UTSENDT_ARBEIDSTAKER_FRA_EØS_LAND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorUtsendtArbeidstakerFraEosLand" + }, + OPPHOR_UGYLDIG_KONTONUMMER_EØS { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorUgyldigKontonummerEos" + }, + OPPHOR_ETT_BARN_DØD_EØS { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorEttBarnDodEos" + }, + OPPHOR_FLERE_BARN_DØDE_EØS { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorFlereBarnErDodeEos" + }, + OPPHØR_SELVSTENDIG_RETT_OPPHØR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorSelvstendigRettOpphoer" + }, + OPPHØR_SELVSTENDIG_RETT_UTSENDT_ARBEIDSTAKER_FRA_ANNET_EØS_LAND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_OPPHØR + override val sanityApiNavn = "opphorSelvstendigRettUtsendtArbedstakerFraAnnetEosLand" + }, + AVSLAG_EØS_IKKE_EØS_BORGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosIkkeEosBorger" + }, + AVSLAG_EØS_IKKE_BOSATT_I_EØS_LAND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosIkkeBosattIEosLand" + }, + AVSLAG_EØS_JOBBER_IKKE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosJobberIkke" + }, + AVSLAG_EØS_UTSENDT_ARBEIDSTAKER_FRA_ANNET_EØS_LAND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosUtsendtArbeidstakerFraAnnetEosLand" + }, + AVSLAG_EØS_ARBEIDER_MER_ENN_25_PROSENT_I_ANNET_EØS_LAND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosArbeiderMerEnn25ProsentIAnnetEosLand" + }, + AVSLAG_EØS_KUN_KORTE_USAMMENHENGENDE_ARBEIDSPERIODER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosKunKorteUsammenhengendeArbeidsperioder" + }, + AVSLAG_EØS_IKKE_PENGER_FRA_NAV_SOM_ERSTATTER_LØNN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosIkkePengerFraNavSomErstatterLoenn" + }, + AVSLAG_EØS_SEPARASJONSAVTALEN_GJELDER_IKKE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosSeparasjonsavtalenGjelderIkke" + }, + AVSLAG_EØS_IKKE_LOVLIG_OPPHOLD_SOM_EØS_BORGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosIkkeLovligOppholdSomEosBorger" + }, + AVSLAG_EØS_IKKE_OPPHOLDSRETT_SOM_FAMILIEMEDLEM_AV_EØS_BORGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosIkkeOppholdsrettSomFamiliemedlemAvEosBorger" + }, + AVSLAG_EØS_IKKE_STUDENT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosIkkeStudent" + }, + AVSLAG_EØS_IKKE_ANSVAR_FOR_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosIkkeAnsvarForBarn" + }, + AVSLAG_EØS_VURDERING_IKKE_ANSVAR_FOR_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosVurderingIkkeAnsvarForBarn" + }, + AVSLAG_FAAR_DAGPENGER_FRA_ANNET_EOS_LAND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagFaarDagpengerFraAnnetEosLand" + }, + AVSLAG_SELVSTENDIG_NAERINGSDRIVENDE_NORGE_ARBEIDSTAKER_I_ANNET_EOS_LAND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagSelvstendigNaeringsdrivendeNorgeArbeidstakerIAnnetEosLand" + }, + AVSLAG_EØS_UREGISTRERT_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagEosUregistrertBarn" + }, + AVSLAG_SELVSTENDIG_RETT_STANDARD_AVSLAG { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagSelvstendigRettStandardAvslag" + }, + AVSLAG_SELVSTENDIG_RETT_UTSENDT_ARBEIDSTAKER_FRA_ANNET_EØS_LAND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagSelvstendigRettUtsendtArbeidstakerFraAnnetEosLand" + }, + AVSLAG_SELVSTENDIG_RETT_BOR_IKKE_FAST_MED_BARNET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_AVSLAG + override val sanityApiNavn = "avslagSelvstendigRettBorIkkeFastMedBarnet" + }, + FORTSATT_INNVILGET_PRIMÆRLAND_STANDARD { + override val sanityApiNavn = "fortsattInnvilgetPrimaerlandStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_PRIMÆRLAND_ALENEANSVAR { + override val sanityApiNavn = "fortsattInnvilgetPrimaerlandAleneansvar" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE { + override val sanityApiNavn = "fortsattInnvilgetPrimaerlandBeggeForeldreBosattINorge" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_JOBBER_I_NORGE { + override val sanityApiNavn = "fortsattInnvilgetPrimaerlandBeggeForeldreJobberINorge" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_PRIMÆRLAND_UK_STANDARD { + override val sanityApiNavn = "fortsattInnvilgetPrimaerlandUkStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_PRIMÆRLAND_UK_ALENEANSVAR { + override val sanityApiNavn = "fortsattInnvilgetPrimaerlandUkAleneansvar" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_PRIMÆRLAND_UK_OG_UTLAND_STANDARD { + override val sanityApiNavn = "fortsattInnvilgetPrimaerlandUkOgUtlandStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_PRIMÆRLAND_BARNET_BOR_I_NORGE { + override val sanityApiNavn = "fortsattInnvilgetPrimaerlandBarnetBorINorge" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_PRIMÆRLAND_SÆRKULLSBARN_ANDRE_BARN_OVERTATT_ANSVAR { + override val sanityApiNavn = "fortsattInnvilgetPrimaerlandSaerkullsbarnAndreBarn" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_PRIMÆRLAND_TO_ARBEIDSLAND_NORGE_UTBETALER { + override val sanityApiNavn = "fortsattInnvilgetPrimaerlandToArbeidslandNorgeUtbetaler" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_PRIMÆRLAND_TO_ARBEIDSLAND_ANNET_LAND_UTBETALER { + override val sanityApiNavn = "fortsattInnvilgetPrimaerlandToArbeidslandAnnetLandUtbetaler" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + + FORTSATT_INNVILGET_PRIMÆRLAND_UK_TO_ARBEIDSLAND_NORGE_UTBETALER { + override val sanityApiNavn = "fortsattInnvilgetPrimaerlandUkToArbeidslandNorgeUtbetaler" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_PRIMÆRLAND_UK_TO_ARBEIDSLAND_ANNET_LAND_UTBETALER { + override val sanityApiNavn = "fortsattInnvilgetPrimaerlandUkToArbeidslandAnnetLandUtbetaler" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + + FORTSATT_INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_FÅR_YTELSE_I_UTLANDET { + override val sanityApiNavn = "fortsattInnvilgetSelvstendigRettPrimaerlandFaarYtelseIUtlandet" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + + FORTSATT_INNVILGET_TILLEGGSBEGRUNNELSE_UTBETALING_TIL_ANNEN_FORELDER { + override val sanityApiNavn = "fortsattInnvilgetTilleggsbegrunnelseUtbetalingTilAnnenForelder" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSETT_INNVILGET_PRIMÆRLAND_TILLEGGSTEKST_VEDTAK_FØR_SED { + override val sanityApiNavn = "fortsattInnvilgetTilleggsbegrunnelseVedtakForSed" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSETT_INNVILGET_SEKUNDÆRLAND_STANDARD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetSekundaerlandStandard" + }, + FORTSETT_INNVILGET_TILLEGGSTEKST_NULLUTBETALING { + override val sanityApiNavn = "fortsattInnvilgetTilleggstekstNullutbetaling" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSETT_INNVILGET_SEKUNDÆRLAND_ALENEANSVAR { + override val sanityApiNavn = "fortsattInnvilgetSekundaerlandAleneansvar" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSETT_INNVILGET_SEKUNDÆRLAND_UK_STANDARD { + override val sanityApiNavn = "fortsattInnvilgetSekundaerlandUkStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + + FORTSATT_INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_FÅR_YTELSE_I_UTLANDET { + override val sanityApiNavn = "fortsattInnvilgetSelvstendigRettSekundaerlandFaarYtelseIUtlandet" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + + FORTSETT_INNVILGET_SEKUNDÆRLAND_UK_ALENEANSVAR { + override val sanityApiNavn = "fortsattInnvilgetSekundaerlandUkAleneansvar" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSETT_INNVILGET_SEKUNDÆRLAND_UK_OG_UTLAND_STANDARD { + override val sanityApiNavn = "fortsattInnvilgetSekundaerlandUkOgUtland" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSETT_INNVILGET_SEKUNDÆRLAND_TO_ARBEIDSLAND_NORGE_UTBETALER { + override val sanityApiNavn = "fortsattInnvilgetSekundaerlandToArbeidslandNorgeUtbetaler" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSETT_INNVILGET_SEKUNDÆRLAND_UK_TO_ARBEIDSLAND_NORGE_UTBETALER { + override val sanityApiNavn = "fortsattInnvilgetSekundaerlandUkToArbeidslandNorgeUtbetaler" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_SEKUNDÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE { + override val sanityApiNavn = "fortsattInnvilgetSekundaerlandBeggeForeldreBosattINorge" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_TILLEGGSTEKST_SEKUNDÆR_FULL_UTBETALING { + override val sanityApiNavn = "fortsattInnvilgetTilleggstekstSekundaerFullUtbetaling" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_TILLEGGSTEKST_SEKUNDÆR_IKKE_FÅTT_SVAR_PÅ_SED { + override val sanityApiNavn = "fortsattInnvilgetTilleggsteksterSekundaerIkkeFaattSvarPaaSed" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_TILLEGSTEKST_UK_FULL_UTBETALING { + override val sanityApiNavn = "fortsattInnvilgetTilleggstekstUkFullUtbetaling" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_STANDARD { + override val sanityApiNavn = "fortsattInnvilgetSelvstendigRettPrimaerlandStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_UK_STANDARD { + override val sanityApiNavn = "fortsattInnvilgetSelvstendigRettPrimaerlandUkStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_UK_OG_UTLAND_STANDARD { + override val sanityApiNavn = "fortsattInnvilgetSelvstendigRettPrimaerlandUkOgUtlandStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_STANDARD { + override val sanityApiNavn = "fortsattInnvilgetSelvstendigRettSekundaerlandStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_UK_STANDARD { + override val sanityApiNavn = "fortsattInnvilgetSelvstendigRettSekundaerlandUkStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + FORTSATT_INNVILGET_SELVSTENDIG_RETT_SEKUNDAERLAND_UK_OG_UTLAND_STANDARD { + override val sanityApiNavn = "fortsattInnvilgetSelvstendigRettSekundaerlandUkOgUtlandStandard" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET + }, + REDUKSJON_BARN_DØD_EØS { + override val sanityApiNavn = "reduksjonBarnDoedEos" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_REDUKSJON + }, + REDUKSJON_SØKER_BER_OM_OPPHØR_EØS { + override val sanityApiNavn = "reduksjonSokerBerOmOpphoer" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_REDUKSJON + }, + REDUKSJON_BARN_BOR_IKKE_I_EØS { + override val sanityApiNavn = "reduksjonBarnBorIkkeIEosLand" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_REDUKSJON + }, + REDUKSJON_IKKE_ANSVAR_FOR_BARN { + override val sanityApiNavn = "reduksjonIkkeAnsvarForBarn" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_REDUKSJON + }, ; + + override val kanDelesOpp: Boolean = false + + override fun delOpp( + restBehandlingsgrunnlagForBrev: RestBehandlingsgrunnlagForBrev, + triggesAv: TriggesAv, + periode: NullablePeriode, + ): List { + throw Feil("Begrunnelse $this kan ikke deles opp.") + } + + @JsonValue + override fun enumnavnTilString(): String = EØSStandardbegrunnelse::class.simpleName + "$" + this.name + + fun tilEØSBegrunnelseMedTriggere(sanityEØSBegrunnelser: Map): EØSBegrunnelseMedTriggere? { + val sanityEØSBegrunnelse = sanityEØSBegrunnelser[this] ?: return null + return EØSBegrunnelseMedTriggere( + eøsBegrunnelse = this, + sanityEØSBegrunnelse = sanityEØSBegrunnelse, + ) + } + + companion object { + fun eøsPraksisendringBegrunnelser(): Set = setOf( + EØSStandardbegrunnelse.INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_STANDARD, + EØSStandardbegrunnelse.INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_UK_STANDARD, + EØSStandardbegrunnelse.INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_UK_OG_STANDARD, + EØSStandardbegrunnelse.INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_UTSENDT_ARBEIDSTAKER, + EØSStandardbegrunnelse.INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_FÅR_YTELSE_I_UTLANDET, + EØSStandardbegrunnelse.INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_FÅR_YTELSE_I_UTLANDET, + EØSStandardbegrunnelse.INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_STANDARD, + EØSStandardbegrunnelse.INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_UK_STANDARD, + EØSStandardbegrunnelse.INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_UK_OG_UTLAND_STANDARD, + EØSStandardbegrunnelse.OPPHØR_SELVSTENDIG_RETT_OPPHØR, + EØSStandardbegrunnelse.OPPHØR_SELVSTENDIG_RETT_UTSENDT_ARBEIDSTAKER_FRA_ANNET_EØS_LAND, + EØSStandardbegrunnelse.AVSLAG_SELVSTENDIG_RETT_STANDARD_AVSLAG, + EØSStandardbegrunnelse.AVSLAG_SELVSTENDIG_RETT_UTSENDT_ARBEIDSTAKER_FRA_ANNET_EØS_LAND, + EØSStandardbegrunnelse.AVSLAG_SELVSTENDIG_RETT_BOR_IKKE_FAST_MED_BARNET, + EØSStandardbegrunnelse.FORTSATT_INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_STANDARD, + EØSStandardbegrunnelse.FORTSATT_INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_UK_STANDARD, + EØSStandardbegrunnelse.FORTSATT_INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_UK_OG_UTLAND_STANDARD, + EØSStandardbegrunnelse.FORTSATT_INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_STANDARD, + EØSStandardbegrunnelse.FORTSATT_INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_UK_STANDARD, + EØSStandardbegrunnelse.FORTSATT_INNVILGET_SELVSTENDIG_RETT_SEKUNDAERLAND_UK_OG_UTLAND_STANDARD, + EØSStandardbegrunnelse.FORTSATT_INNVILGET_SELVSTENDIG_RETT_PRIMÆRLAND_FÅR_YTELSE_I_UTLANDET, + EØSStandardbegrunnelse.FORTSATT_INNVILGET_SELVSTENDIG_RETT_SEKUNDÆRLAND_FÅR_YTELSE_I_UTLANDET, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/IVedtakBegrunnelse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/IVedtakBegrunnelse.kt new file mode 100644 index 000000000..9152ca9ad --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/IVedtakBegrunnelse.kt @@ -0,0 +1,68 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.node.ArrayNode +import jakarta.persistence.AttributeConverter +import jakarta.persistence.Converter +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.NullablePeriode +import no.nav.familie.ba.sak.kjerne.brev.domene.BrevBegrunnelseGrunnlagMedPersoner +import no.nav.familie.ba.sak.kjerne.brev.domene.RestBehandlingsgrunnlagForBrev + +sealed interface IVedtakBegrunnelse { + + val sanityApiNavn: String + val vedtakBegrunnelseType: VedtakBegrunnelseType + val kanDelesOpp: Boolean + + fun delOpp( + restBehandlingsgrunnlagForBrev: RestBehandlingsgrunnlagForBrev, + triggesAv: TriggesAv, + periode: NullablePeriode, + ): List + + fun enumnavnTilString(): String + + companion object { + fun konverterTilEnumVerdi(string: String): IVedtakBegrunnelse { + val splittet = string.split('$') + val type = splittet[0] + val enumNavn = splittet[1] + return when (type) { + EØSStandardbegrunnelse::class.simpleName -> EØSStandardbegrunnelse.valueOf(enumNavn) + Standardbegrunnelse::class.simpleName -> Standardbegrunnelse.valueOf(enumNavn) + else -> throw Feil("Fikk en begrunnelse med ugyldig type: hverken EØSStandardbegrunnelse eller Standardbegrunnelse: $this") + } + } + } +} + +fun IVedtakBegrunnelse.erAvslagUregistrerteBarnBegrunnelse() = + this in setOf(Standardbegrunnelse.AVSLAG_UREGISTRERT_BARN, EØSStandardbegrunnelse.AVSLAG_EØS_UREGISTRERT_BARN) + +class IVedtakBegrunnelseDeserializer : StdDeserializer>(List::class.java) { + override fun deserialize(jsonParser: JsonParser?, p1: DeserializationContext?): List { + val node: ArrayNode = jsonParser!!.codec.readTree(jsonParser) + return node + .map { it.asText() } + .map { IVedtakBegrunnelse.konverterTilEnumVerdi(it) } + } +} + +@Converter +class IVedtakBegrunnelseListConverter : + AttributeConverter, String> { + + override fun convertToDatabaseColumn(vedtakbegrunnelser: List) = + vedtakbegrunnelser.joinToString(";") { it.enumnavnTilString() } + + override fun convertToEntityAttribute(string: String?): List = + if (string.isNullOrBlank()) { + emptyList() + } else { + string.split(";") + .map { IVedtakBegrunnelse.konverterTilEnumVerdi(it) } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/SanityE\303\230SBegrunnelse.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/SanityE\303\230SBegrunnelse.kt" new file mode 100644 index 000000000..cc7d86126 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/SanityE\303\230SBegrunnelse.kt" @@ -0,0 +1,70 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser + +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityPeriodeResultat +import no.nav.familie.ba.sak.kjerne.brev.domene.Tema +import no.nav.familie.ba.sak.kjerne.brev.domene.finnEnumverdi +import no.nav.familie.ba.sak.kjerne.brev.domene.finnEnumverdiNullable +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.BrevPeriodeType +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår + +enum class BarnetsBostedsland { + NORGE, + IKKE_NORGE, +} + +fun landkodeTilBarnetsBostedsland(landkode: String): BarnetsBostedsland = when (landkode) { + "NO" -> BarnetsBostedsland.NORGE + else -> BarnetsBostedsland.IKKE_NORGE +} + +data class RestSanityEØSBegrunnelse( + val apiNavn: String?, + val navnISystem: String?, + val annenForeldersAktivitet: List?, + val barnetsBostedsland: List?, + val kompetanseResultat: List?, + val hjemler: List?, + val hjemlerFolketrygdloven: List?, + val hjemlerEOSForordningen883: List?, + val hjemlerEOSForordningen987: List?, + val hjemlerSeperasjonsavtalenStorbritannina: List?, + val eosVilkaar: List? = null, + val vedtakResultat: String?, + val fagsakType: String?, + val tema: String?, + val periodeType: String?, +) { + fun tilSanityEØSBegrunnelse(): SanityEØSBegrunnelse? { + if (apiNavn == null || navnISystem == null) return null + return SanityEØSBegrunnelse( + apiNavn = apiNavn, + navnISystem = navnISystem, + annenForeldersAktivitet = annenForeldersAktivitet?.mapNotNull { + konverterTilEnumverdi(it) + } ?: emptyList(), + barnetsBostedsland = barnetsBostedsland?.mapNotNull { + konverterTilEnumverdi(it) + } ?: emptyList(), + kompetanseResultat = kompetanseResultat?.mapNotNull { + konverterTilEnumverdi(it) + } ?: emptyList(), + hjemler = hjemler ?: emptyList(), + hjemlerFolketrygdloven = hjemlerFolketrygdloven ?: emptyList(), + hjemlerEØSForordningen883 = hjemlerEOSForordningen883 ?: emptyList(), + hjemlerEØSForordningen987 = hjemlerEOSForordningen987 ?: emptyList(), + hjemlerSeperasjonsavtalenStorbritannina = hjemlerSeperasjonsavtalenStorbritannina ?: emptyList(), + vilkår = eosVilkaar?.mapNotNull { konverterTilEnumverdi(it) }?.toSet() ?: emptySet(), + periodeResultat = vedtakResultat.finnEnumverdi(apiNavn), + fagsakType = fagsakType.finnEnumverdiNullable(), + tema = tema.finnEnumverdi(apiNavn), + periodeType = periodeType.finnEnumverdi(apiNavn), + ) + } + + private inline fun konverterTilEnumverdi(it: String): T? where T : Enum = + enumValues().find { enum -> enum.name == it } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/Standardbegrunnelse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/Standardbegrunnelse.kt new file mode 100644 index 000000000..dfd453505 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/Standardbegrunnelse.kt @@ -0,0 +1,1593 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser + +import com.fasterxml.jackson.annotation.JsonValue +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.NullablePeriode +import no.nav.familie.ba.sak.kjerne.brev.domene.BrevBegrunnelseGrunnlagMedPersoner +import no.nav.familie.ba.sak.kjerne.brev.domene.RestBehandlingsgrunnlagForBrev +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.domene.MinimertRestPerson +import no.nav.familie.ba.sak.kjerne.vedtak.domene.hentRelevanteEndringsperioderForBegrunnelse + +val hjemlerTilhørendeFritekst = setOf(2, 4, 11) + +enum class Standardbegrunnelse : IVedtakBegrunnelse { + INNVILGET_BOSATT_I_RIKTET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetBosattIRiket" + }, + INNVILGET_BOSATT_I_RIKTET_LOVLIG_OPPHOLD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetBosattIRiketLovligOpphold" + }, + INNVILGET_LOVLIG_OPPHOLD_OPPHOLDSTILLATELSE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetLovligOppholdOppholdstillatelse" + }, + INNVILGET_LOVLIG_OPPHOLD_EØS_BORGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetLovligOppholdEOSBorger" + }, + INNVILGET_LOVLIG_OPPHOLD_EØS_BORGER_SKJØNNSMESSIG_VURDERING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetLovligOppholdEOSBorgerSkjonnsmessigVurdering" + }, + INNVILGET_LOVLIG_OPPHOLD_SKJØNNSMESSIG_VURDERING_TREDJELANDSBORGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetLovligOppholdSkjonnsmessigVurderingTredjelandsborger" + }, + INNVILGET_LOVLIG_OPPHOLD_SKJØNNSMESSIG_VURDERING_TREDJELANDSBORGER_SØKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetLovligOppholdSkjonnsmessigVurderingTredjelandsborgerSoker" + }, + INNVILGET_TREDJELANDSBORGER_LOVLIG_OPPHOLD_FOR_BOSATT_I_NORGE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTredjelandsborgerLovligOppholdForBosattINorge" + }, + INNVILGET_OMSORG_FOR_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetOmsorgForBarn" + }, + INNVILGET_BOR_HOS_SØKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetBorHosSoker" + }, + INNVILGET_BOR_HOS_SØKER_SKJØNNSMESSIG { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetBorHosSokerSkjonnsmessig" + }, + INNVILGET_FAST_OMSORG_FOR_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetFastOmsorgForBarn" + }, + INNVILGET_NYFØDT_BARN_FØRSTE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetNyfodtBarnForste" + }, + INNVILGET_NYFØDT_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetNyfodtBarn" + }, + INNVILGET_FØDSELSHENDELSE_NYFØDT_BARN_FØRSTE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetFodselshendelseNyfodtBarnForste" + }, + INNVILGET_FØDSELSHENDELSE_NYFØDT_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetFodselshendelseNyfodtBarn" + }, + INNVILGET_MEDLEM_I_FOLKETRYGDEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetMedlemIFolketrygden" + }, + INNVILGET_BARN_BOR_SAMMEN_MED_MOTTAKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetBarnBorSammenMedMottaker" + }, + INNVILGET_BEREDSKAPSHJEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetBeredskapshjem" + }, + INNVILGET_HELE_FAMILIEN_TRYGDEAVTALE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetHeleFamilienTrygdeavtale" + }, + INNVILGET_HELE_FAMILIEN_PLIKTIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetHeleFamilienPliktigMedlem" + }, + INNVILGET_SØKER_OG_BARN_PLIKTIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetSokerOgBarnPliktigMedlem" + }, + INNVILGET_ENIGHET_OM_OPPHØR_AV_AVTALE_OM_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetEnighetOmAtAvtalenOmDeltBostedErOpphort" + }, + INNVILGET_VURDERING_HELE_FAMILIEN_FRIVILLIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetVurderingHeleFamilienFrivilligMedlem" + }, + INNVILGET_UENIGHET_OM_OPPHØR_AV_AVTALE_OM_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetUenighetOmOpphorAvAvtaleOmDeltBosted" + }, + INNVILGET_HELE_FAMILIEN_FRIVILLIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetHeleFamilienFrivilligMedlem" + }, + INNVILGET_VURDERING_HELE_FAMILIEN_PLIKTIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetVurderingHeleFamilienPliktigMedlem" + }, + INNVILGET_SØKER_OG_BARN_OPPHOLD_I_UTLANDET_IKKE_MER_ENN_3_MÅNEDER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetSokerOgBarnOppholdIUtlandetIkkeMerEnn3Maneder" + }, + INNVILGET_OPPHOLD_I_UTLANDET_IKKE_MER_ENN_3_MÅNEDER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetOppholdIUtlandetIkkeMerEnnTreMaaneder" + }, + INNVILGET_SØKER_OG_BARN_FRIVILLIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetSokerOgBarnFrivilligMedlem" + }, + INNVILGET_VURDERING_SØKER_OG_BARN_FRIVILLIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetVurderingSokerOgBarnFrivilligMedlem" + }, + INNVILGET_ETTERBETALING_3_ÅR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetEtterbetaling3Aar" + }, + INNVILGET_SØKER_OG_BARN_TRYGDEAVTALE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetSokerOgBarnTrygdeavtale" + }, + INNVILGET_ALENE_FRA_FØDSEL { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetAleneFraFodsel" + }, + INNVILGET_VURDERING_SØKER_OG_BARN_PLIKTIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetVurderingSokerOgBarnPliktigMedlem" + }, + INNVILGET_BARN_OPPHOLD_I_UTLANDET_IKKE_MER_ENN_3_MÅNEDER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetBarnOppholdIUtlandetIkkeMerEnn3Maneder" + }, + INNVILGET_SATSENDRING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetSatsendring" + }, + INNVILGET_FLYTTET_ETTER_SEPARASJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetFlyttetEtterSeparasjon" + }, + INNVILGET_SEPARERT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetSeparert" + }, + INNVILGET_VARETEKTSFENGSEL_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetVaretektsfengselSamboer" + }, + INNVILGET_AVTALE_DELT_BOSTED_FÅR_FRA_FLYTTETIDSPUNKT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetAvtaleDeltBostedFaarFraFlyttetidspunkt" + }, + INNVILGET_TVUNGENT_PSYKISK_HELSEVERN_GIFT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTvungentPsykiskHelsevernGift" + }, + INNVILGET_TVUNGENT_PSYKISK_HELSEVERN_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTvungentPsykiskHelsevernSamboer" + }, + INNVILGET_FENGSEL_GIFT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetFengselGift" + }, + INNVILGET_VURDERING_EGEN_HUSHOLDNING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetVurderingEgenHusholdning" + }, + INNVILGET_FORSVUNNET_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetForsvunnetSamboer" + }, + INNVILGET_AVTALE_DELT_BOSTED_FÅR_FRA_AVTALETIDSPUNKT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetAvtaleDeltBostedFaarFraAvtaletidspunkt" + }, + INNVILGET_FORVARING_GIFT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetForvaringGift" + }, + INNVILGET_MEKLINGSATTEST_OG_VURDERING_EGEN_HUSHOLDNING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetMeklingsattestOgVurderingEgenHusholdning" + }, + INNVILGET_FENGSEL_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetFengselSamboer" + }, + INNVILGET_FLYTTING_ETTER_MEKLINGSATTEST { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetFlyttingEtterMeklingsattest" + }, + INNVILGET_FORVARING_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetForvaringSamboer" + }, + INNVILGET_SEPARERT_OG_VURDERING_EGEN_HUSHOLDNING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetSeparertOgVurderingEgenHusholdning" + }, + INNVILGET_BARN_16_ÅR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetBarn16Ar" + }, + INNVILGET_SAMBOER_DØD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetSamboerDod" + }, + INNVILGET_MEKLINGSATTEST { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetMeklingsattest" + }, + INNVILGET_FLYTTET_ETTER_SKILT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetFlyttetEtterSkilt" + }, + INNVILGET_ENSLIG_MINDREÅRIG_FLYKTNING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetEnsligMindrearigFlyktning" + }, + INNVILGET_VARETEKTSFENGSEL_GIFT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetVaretektsfengselGift" + }, + INNVILGET_SAMBOER_UTEN_FELLES_BARN_OG_VURDERING_EGEN_HUSHOLDNING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetSamboerUtenFellesBarnOgVurderingEgenHusholdning" + }, + INNVILGET_FORSVUNNET_EKTEFELLE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetForsvunnetEktefelle" + }, + INNVILGET_FAKTISK_SEPARASJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetFaktiskSeparasjon" + }, + INNVILGET_SAMBOER_UTEN_FELLES_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetSamboerUtenFellesBarn" + }, + INNVILGET_VURDERING_AVTALE_DELT_BOSTED_FØLGES { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetVurderingAvtaleDeltBostedFolges" + }, + INNVILGET_SKILT_OG_VURDERING_EGEN_HUSHOLDNING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetSkiltOgVurderingEgenHusholdning" + }, + INNVILGET_BOR_ALENE_MED_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetBorAleneMedBarn" + }, + INNVILGET_SKILT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetSkilt" + }, + INNVILGET_RETTSAVGJØRELSE_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetRettsavgjorelseDeltBosted" + }, + INNVILGET_EKTEFELLE_DØD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetEktefelleDod" + }, + INNVILGET_SMÅBARNSTILLEGG { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetSmaabarnstillegg" + }, + INNVILGET_ANNEN_FORELDER_IKKE_SØKT_DELT_BARNETRYGD_ENKELTBARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetAnnenForelderIkkeSoktDeltBarnetrygdEnkeltbarn" + }, + INNVILGET_ANNEN_FORELDER_IKKE_SØKT_DELT_BARNETRYGD_ALLE_BARNA { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetAnnenForelderIkkeSoktDeltBarnetrygdAlleBarna" + }, + INNVILGET_TILLEGGSTEKST_SAMSBOER_12_AV_SISTE_18 { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstSamboer12AvSiste18" + }, + INNVILGET_ERKLÆRING_OM_MOTREGNING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetErklaeringOmMotregning" + }, + INNVILGET_TILLEGGSTEKST_TRANSPORTERKLÆRING_HELE_ETTERBETALINGEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstTransporterklaeringHeleEtterbetalingen" + }, + INNVILGET_TILLEGGSTEKST_TRANSPORTERKLÆRING_DELER_AV_ETTERBETALINGEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstTransporterklaeringDelerAvEtterbetalingen" + }, + INNVILGET_EØS_BORGER_JOBBER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetEosBorgerJobber" + }, + INNVILGET_EØS_BORGER_UTBETALING_FRA_NAV { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetEosBorgerUtbetalingFraNAV" + }, + INNVILGET_EØS_BORGER_EKTEFELLE_JOBBER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetEosBorgerEktefelleJobber" + }, + INNVILGET_EØS_BORGER_SAMBOER_JOBBER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetEosBorgerSamboerJobber" + }, + INNVILGET_EØS_BORGER_EKTEFELLE_UTBETALING_FRA_NAV { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetEosBorgerEktefelleUtbetalingFraNav" + }, + INNVILGET_EØS_BORGER_SAMBOER_UTBETALING_FRA_NAV { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetEosBorgerSamboerUtbetalingFraNav" + }, + INNVILGET_FAKTISK_SEPARASJON_SEPARERT_ETTERPÅ { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetFaktiskSeparasjonSeparertEtterpaa" + }, + INNVILGET_BARN_16ÅR_UTVIDET_FRA_FLYTTING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetBarn16AarUtvidetFraFlytting" + }, + INNVILGET_TILLEGGSTEKST_OPPHØR_UTVIDET_NYFØDT_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstOpphorUtvidetNyfoedtBarn" + }, + INNVILGET_TILLEGGSTEKST_SAMBOER_UNDER_12_MÅNEDER_FØR_GIFT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTillleggstekstSamboerUnder12MaanederForGift" + }, + INNVILGET_TILLEGGSTEKST_SAMBOER_UNDER_12_MÅNEDER_FØR_NYTT_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstSamboerUnder12MaanederForNyttBarn" + }, + INNVILGET_TILLEGGSTEKST_SAMBOER_UNDER_12_MÅNEDER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstSamboerUnder12Maaneder" + }, + INNVILGET_TILLEGGSTEKST_EØS_BORGER_JOBBER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstEosBorgerJobber" + }, + INNVILGET_TILLEGGSTEKST_EØS_BORGER_UTBETALING_NAV { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstEosBorgerUtbetalingNav" + }, + INNVILGET_TILLEGGSTEKST_EØS_BORGER_EKTEFELLE_JOBBER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstEosBorgerEktefelleJobber" + }, + INNVILGET_TILLEGGSTEKST_EØS_BORGER_SAMBOER_JOBBER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstEosBorgerSamboerJobber" + }, + INNVILGET_TILLEGGSTEKST_EØS_BORGER_EKTEFELLE_UTBETALING_NAV { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstEosBorgerEktefelleUtbetalingNav" + }, + INNVILGET_TILLEGGSTEKST_EØS_BORGER_SAMBOER_UTBETALING_NAV { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstEosBorgerSamboerUtbetalingNav" + }, + INNVILGET_TILLEGGSTEKST_TREDJELANDSBORGER_OPPHOLDSTILLATELSE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstTredjelandsborgerOppholdstillatelse" + }, + INNVILGET_TILLEGGSTEKST_TREDJELANDSBORGER_OPPHOLDSTILLATELSE_SØKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetTilleggstekstTredjelandsborgerOppholdstillatelseSoker" + }, + INNVILGET_MEDLEM_AV_FOLKETRYGDEN_UTEN_DATO { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetMedlemAvFolketrygdenUtenDato" + }, + INNVILGET_GYLDIG_KONTONUMMER_REGISTRERT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetGyldigKontonummerRegistrert" + }, + INNVILGET_FULL_UTBETALING_AVTALE_DELT_BOSTED_ANNEN_OMSORGSPERSON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetFullUtbetalingAvtaleDeltBostedAnnenOmsorgsperson" + }, + INNVILGET_FULL_UTBETALING_ANNEN_FORELDER_ØNSKER_IKKE_DELT_BARNETRYGD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetFullUtbetalingAnnenForelderOnskerIkkeDeltBarnetrygd" + }, + INNVILGET_OVERGANG_EØS_TIL_NASJONAL_NORSK_NORDISK_FAMILIE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetOvergangEosTilNasjonalNorskNordiskFamilie" + }, + INNVILGET_OVERGANG_EØS_TIL_NASJONAL_SEPARASJONSAVTALEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetOvergangEosTilNasjonalSeparasjonsavtalen" + }, + INNVILGET_FÅR_ETTERBETALT_UTVIDET_FOR_PRAKTISERT_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetFaarEtterbetaltUtvidetForPraktisertDeltBosted" + }, + INNVILGET_DATO_SKRIFTLIG_AVTALE_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetDatoSkriftligAvtaleDeltBosted" + }, + INNVILGET_DELT_FRA_SKRIFTLIG_AVTALE_HAR_SØKT_FOR_PRAKTISERT_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetDeltFraSkriftligAvtaleHarSoktForPraktisertDeltBosted" + }, + INNVILGET_OPPHOLD_PAA_SVALBARD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + override val sanityApiNavn = "innvilgetOppholdPaaSvalbard" + }, + REDUKSJON_BOSATT_I_RIKTET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonBosattIRiket" + }, + REDUKSJON_LOVLIG_OPPHOLD_OPPHOLDSTILLATELSE_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonLovligOppholdOppholdstillatelseBarn" + }, + REDUKSJON_FLYTTET_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonFlyttetBarn" + }, + REDUKSJON_BARN_DØD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonBarnDod" + }, + REDUKSJON_FAST_OMSORG_FOR_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonFastOmsorgForBarn" + }, + REDUKSJON_UNDER_18_ÅR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonUnder18Aar" + }, + REDUKSJON_UNDER_6_ÅR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonUnder6Aar" + }, + REDUKSJON_UNDER_18_ÅR_AUTOVEDTAK { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonAutovedtakBarn18Aar" + }, + REDUKSJON_UNDER_6_ÅR_AUTOVEDTAK { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonAutovedtakBarn6Aar" + }, + REDUKSJON_DELT_BOSTED_ENIGHET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonDeltBostedEnighet" + }, + REDUKSJON_DELT_BOSTED_UENIGHET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonDeltBostedUenighet" + }, + REDUKSJON_ENDRET_MOTTAKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonEndretMottaker" + }, + REDUKSJON_ANNEN_FORELDER_IKKE_LENGER_FRIVILLIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonAnnenForelderIkkeLengerFrivilligMedlem" + }, + REDUKSJON_ANNEN_FORELDER_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonAnnenForelderIkkeMedlem" + }, + REDUKSJON_ANNEN_FORELDER_IKKE_LENGER_MEDLEM_TRYGDEAVTALE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonAnnenForelderIkkeLengerMedlemTrygdeavtale" + }, + REDUKSJON_ANNEN_FORELDER_IKKE_LENGER_PLIKTIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonAnnenForelderIkkeLengerPliktigMedlem" + }, + REDUKSJON_VURDERING_BARN_FLERE_KORTE_OPPHOLD_I_UTLANDET_SISTE_ÅRENE_ { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonVurderingBarnFlereKorteOppholdIUtlandetSisteArene" + }, + REDUKSJON_VURDERING_BARN_FLERE_KORTE_OPPHOLD_I_UTLANDET_SISTE_TO_ÅR_ { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonVurderingBarnFlereKorteOppholdIUtlandetSisteToAr" + }, + REDUKSJON_SATSENDRING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSatsendring" + }, + REDUKSJON_NYFØDT_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonNyfodtBarn" + }, + REDUKSJON_VURDERING_SØKER_GIFTET_SEG { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonVurderingSokerGiftetSeg" + }, + REDUKSJON_VURDERING_SAMBOER_MER_ENN_12_MÅNEDER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonVurderingSamboerMerEnn12Maaneder" + }, + REDUKSJON_AVTALE_FAST_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonAvtaleFastBosted" + }, + REDUKSJON_EKTEFELLE_IKKE_I_FENGSEL { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonEktefelleIkkeIFengsel" + }, + REDUKSJON_SAMBOER_MER_ENN_12_MÅNEDER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSamboerMerEnn12Maaneder" + }, + REDUKSJON_SAMBOER_IKKE_I_TVUNGENT_PSYKISK_HELSEVERN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSamboerIkkeITvungentPsykiskHelsevern" + }, + REDUKSJON_SAMBOER_IKKE_EGEN_HUSHOLDNING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSamboerIkkeEgenHusholdning" + }, + REDUKSJON_SAMBOER_IKKE_I_FENGSEL { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSamboerIkkeIFengsel" + }, + REDUKSJON_VURDERING_FLYTTET_SAMMEN_MED_EKTEFELLE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonVurderingFlyttetSammenMedEktefelle" + }, + REDUKSJON_VURDERING_FORELDRENE_BOR_SAMMEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonVurderingForeldreneBorSammen" + }, + REDUKSJON_SAMBOER_IKKE_I_FORVARING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSamboerIkkeIForvaring" + }, + REDUKSJON_EKTEFELLE_IKKE_I_TVUNGENT_PSYKISK_HELSEVERN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonEktefelleIkkeITvungentPsykiskHelsevern" + }, + REDUKSJON_EKTEFELLE_IKKE_I_FORVARING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonEktefelleIkkeIForvaring" + }, + REDUKSJON_FORELDRENE_BOR_SAMMEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonForeldreneBorSammen" + }, + REDUKSJON_EKTEFELLE_IKKE_LENGER_FORSVUNNET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonEktefelleIkkeLengerForsvunnet" + }, + REDUKSJON_RETTSAVGJØRELSE_FAST_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonRettsavgjorelseFastBosted" + }, + REDUKSJON_FLYTTET_SAMMEN_MED_ANNEN_FORELDER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonFlyttetSammenMedAnnenForelder" + }, + REDUKSJON_GIFT_IKKE_EGEN_HUSHOLDNING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonGiftIkkeEgenHusholdning" + }, + REDUKSJON_FLYTTET_SAMMEN_MED_EKTEFELLE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonFlyttetSammenMedEktefelle" + }, + REDUKSJON_IKKE_AVTALE_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonIkkeAvtaleDeltBosted" + }, + REDUKSJON_SØKER_GIFTER_SEG { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSokerGifterSeg" + }, + REDUKSJON_SAMBOER_IKKE_LENGER_FORSVUNNET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSamboerIkkeLengerForsvunnet" + }, + REDUKSJON_VURDERING_FLYTTET_SAMMEN_MED_ANNEN_FORELDER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonVurderingFlyttetSammenMedAnnenForelder" + }, + REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_BARN_UNDER_TRE_ÅR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSmaabarnstilleggIkkeLengerBarnUnderTreAar" + }, + REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_FULL_OVERGANGSSTØNAD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSmaabarnstilleggIkkeLengerFullOvergangsstonad" + }, + REDUKSJON_DELT_BARNETRYGD_ANNEN_FORELDER_SØKT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonDeltBarnetrygdAnnenForelderSokt" + }, + REDUKSJON_DELT_BARNETRYGD_HASTEVEDTAK { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonDeltBarnetrygdHastevedtak" + }, + REDUKSJON_IKKE_BOSATT_I_NORGE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonIkkeBosattINorge" + }, + REDUKSJON_BARN_BOR_IKKE_MED_SØKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonBarnBoddeIkkeMedSoker" + }, + REDUKSJON_IKKE_OPPHOLDSTILLATELSE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonIkkeOppholdstillatelse" + }, + REDUKSJON_AVTALE_DELT_BOSTED_IKKE_GYLDIG { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonAvtaleOmDeltBostedIkkeGyldig" + }, + REDUKSJON_AVTALE_DELT_BOSTED_FØLGES_IKKE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonAvtaleDeltBostedFolgesIkke" + }, + REDUKSJON_FORELDRENE_BODDE_SAMMEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonForeldreneBoddeSammen" + }, + REDUKSJON_VURDERING_FORELDRENE_BODDE_SAMMEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonVurderingForeldreneBoddeSammen" + }, + REDUKSJON_VAR_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonVarIkkeMedlem" + }, + REDUKSJON_VURDERING_VAR_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonVurderingVarIkkeMedlem" + }, + REDUKSJON_ANDRE_FORELDER_VAR_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonDenAndreForelderenVarIkkeMedlem" + }, + REDUKSJON_VURDERING_ANDRE_FORELDER_VAR_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonVurderingDenAndreForelderenVarIkkeMedlem" + }, + REDUKSJON_DELT_BOSTED_GENERELL { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonDeltBostedGenerell" + }, + REDUKSJON_BARN_DØDE_SAMME_MÅNED_SOM_FØDT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonBarnDodeSammeMaanedSomFoedt" + }, + REDUKSJON_MANGLER_MEKLINGSATTEST { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonManglerMeklingsattest" + }, + REDUKSJON_FORELDRENE_BOR_SAMMEN_ANNEN_FORELDER_SØKT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonForeldreneBorSammenAnnenForelderSokt" + }, + REDUKSJON_SØKER_BER_OM_OPPHØR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSokerBerOmOpphor" + }, + SMÅBARNSTILLEGG_HADDE_IKKE_FULL_OVERGANGSSTØNAD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSmaabarnstilleggHaddeIkkeFullOvergangsstonad" + }, + REDUKSJON_BARN_MED_SAMBOER_FØR_BODD_SAMMEN_12_MND { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonBarnMedSamboerForBoddSammen12Mnd" + }, + REDUKSJON_SMÅBARNSTILLEGG_HAR_IKKE_UTVIDET_BARNETRYGD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSmaabarnstilleggHarIkkeUtvidetBarnetrygd" + }, + REDUKSJON_SØKER_ER_GIFT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSoekerErGift" + }, + REDUKSJON_SØKER_BER_OM_OPPHØR_UTVIDET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonSoekerBerOmOpphoerUtvidet" + }, + REDUKSJON_DELT_BOSTED_SØKER_BER_OM_OPPHØR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonDeltBostedSoekerBerOmOpphoer" + }, + REDUKSJON_FAST_BOSTED_AVTALE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonFastBostedAvtale" + }, + REDUKSJON_BEGGE_FORELDRE_FÅTT_BARNETRYGD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonBeggeForeldreFaattBarnetrygd" + }, + REDUKSJON_BARN_BOR_I_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.REDUKSJON + override val sanityApiNavn = "reduksjonBarnBorIInstitusjon" + }, + AVSLAG_BOSATT_I_RIKET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagBosattIRiket" + }, + AVSLAG_LOVLIG_OPPHOLD_TREDJELANDSBORGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagLovligOppholdTredjelandsborger" + }, + AVSLAG_BOR_HOS_SØKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagBorHosSoker" + }, + AVSLAG_OMSORG_FOR_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagOmsorgForBarn" + }, + AVSLAG_LOVLIG_OPPHOLD_EØS_BORGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagLovligOppholdEosBorger" + }, + AVSLAG_LOVLIG_OPPHOLD_SKJØNNSMESSIG_VURDERING_TREDJELANDSBORGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagLovligOppholdSkjonnsmessigVurderingTredjelandsborger" + }, + AVSLAG_MEDLEM_I_FOLKETRYGDEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagMedlemIFolketrygden" + }, + AVSLAG_FORELDRENE_BOR_SAMMEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagForeldreneBorSammen" + }, + AVSLAG_UNDER_18_ÅR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagUnder18Aar" + }, + AVSLAG_UGYLDIG_AVTALE_OM_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagUgyldigAvtaleOmDeltBosted" + }, + AVSLAG_IKKE_AVTALE_OM_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeAvtaleOmDeltBosted" + }, + AVSLAG_SÆRKULLSBARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagSaerkullsbarn" + }, + AVSLAG_UREGISTRERT_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagUregistrertBarn" + }, + AVSLAG_IKKE_DOKUMENTERT_BOSATT_I_NORGE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeDokumentertBosattINorge" + }, + AVSLAG_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeMedlem" + }, + AVSLAG_VURDERING_FLERE_KORTE_OPPHOLD_I_UTLANDET_SISTE_ÅRENE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagVurderingFlereKorteOppholdIUtlandetSisteArene" + }, + AVSLAG_VURDERING_ANNEN_FORELDER_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagVurderingAnnenForelderIkkeMedlem" + }, + AVSLAG_IKKE_FRIVILLIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeFrivilligMedlem" + }, + AVSLAG_IKKE_PLIKTIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkePliktigMedlem" + }, + AVSLAG_ANNEN_FORELDER_IKKE_MEDLEM_ETTER_TRYGDEAVTALE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagAnnenForelderIkkeMedlemEtterTrygdeavtale" + }, + AVSLAG_ANNEN_FORELDER_IKKE_PLIKTIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagAnnenForelderIkkePliktigMedlem" + }, + AVSLAG_VURDERING_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagVurderingIkkeMedlem" + }, + AVSLAG_ANNEN_FORELDER_IKKE_FRIVILLIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagAnnenForelderIkkeFrivilligMedlem" + }, + AVSLAG_IKKE_MEDLEM_ETTER_TRYGDEAVTALE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeMedlemEtterTrygdeavtale" + }, + AVSLAG_VURDERING_FLERE_KORTE_OPPHOLD_I_UTLANDET_SISTE_TO_ÅR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagVurderingFlereKorteOppholdIUtlandetSisteToAar" + }, + AVSLAG_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagSamboer" + }, + AVSLAG_SAMBOER_IKKE_FLYTTET_FRA_HVERANDRE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagSamboerIkkeFlyttetFraHverandre" + }, + AVSLAG_BARN_HAR_FAST_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagBarnHarFastBosted" + }, + AVSLAG_IKKE_EGEN_HUSHOLDNING_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeEgenHusholdningSamboer" + }, + AVSLAG_GIFT_MIDLERTIDIG_ADSKILLELSE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagGiftMidlertidigAdskillelse" + }, + AVSLAG_IKKE_EGEN_HUSHOLDNING_GIFT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeEgenHusholdningGift" + }, + AVSLAG_MANGLER_AVTALE_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagManglerAvtaleDeltBosted" + }, + AVSLAG_VURDERING_IKKE_FLYTTET_FRA_EKTEFELLE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagVurderingIkkeFlyttetFraEktefelle" + }, + AVSLAG_RETTSAVGJØRELSE_SAMVÆR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagRettsavgjorelseSamver" + }, + AVSLAG_IKKE_SEPARERT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeSeparert" + }, + AVSLAG_FENGSEL_UNDER_6_MÅNEDER_EKTEFELLE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagFengselUnder6MaanederEktefelle" + }, + AVSLAG_IKKE_DOKUMENTERT_SKILT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeDokumentertSkilt" + }, + AVSLAG_VURDERING_IKKE_MEKLINGSATTEST { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagVurderingIkkeMeklingsattest" + }, + AVSLAG_FORVARING_UNDER_6_MÅNEDER_EKTEFELLE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagForvaringUnder6MaanederEktefelle" + }, + AVSLAG_EKTEFELLE_FORSVUNNET_MINDRE_ENN_6_MÅNEDER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagEktefelleForsvunnetMindreEnn6Maaneder" + }, + AVSLAG_VURDERING_FORELDRENE_BOR_SAMMEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagVurderingForeldreneBorSammen" + }, + AVSLAG_SAMBOER_MIDLERTIDIG_ADSKILLELSE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagSamboerMidlertidigAdskillelse" + }, + AVSLAG_IKKE_FLYTTET_FRA_EKTEFELLE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeFlyttetFraEktefelle" + }, + AVSLAG_IKKE_MEKLINGSATTEST { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeMeklingsattest" + }, + AVSLAG_FORVARING_UNDER_6_MÅNEDER_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagForvaringUnder6MaanederSamboer" + }, + AVSLAG_IKKE_DOKUMENTERT_EKTEFELLE_DØD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeDokumentertEktefelleDod" + }, + AVSLAG_VURDERING_IKKE_TVUNGENT_PSYKISK_HELSEVERN_EKTEFELLE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagVurderingIkkeTvungentPsykiskHelsevernEktefelle" + }, + AVSLAG_VURDERING_IKKE_SEPARERT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagVurderingIkkeSeparert" + }, + AVSLAG_GIFT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagGift" + }, + AVSLAG_SAMBOER_FORSVUNNET_MINDRE_ENN_6_MÅNEDER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagSamboerForsvunnetMindreEnn6Maaneder" + }, + AVSLAG_VURDERING_IKKE_TVUNGENT_PSYKISK_HELSEVERN_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagVurderingIkkeTvungentPsykiskHelsevernSamboer" + }, + AVSLAG_VURDERING_SAMBOER_IKKE_FLYTTET_FRA_HVERANDRE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagVurderingSamboerIkkeFlyttetFraHverandre" + }, + AVSLAG_ENSLIG_MINDREÅRIG_FLYKTNING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagEnsligMindreaarigFlyktning" + }, + AVSLAG_IKKE_DELT_FORELDRENE_BOR_SAMMEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeDeltForeldreneBorSammen" + }, + AVSLAG_IKKE_GYLDIG_AVTALE_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeGyldigAvtaleDeltBosted" + }, + AVSLAG_FENGSEL_UNDER_6_MÅNEDER_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagFengselUnder6MaanederSamboer" + }, + AVSLAG_IKKE_DOKUMENTERT_SAMBOER_DØD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeDokumentertSamboerDod" + }, + AVSLAG_VURDERING_BOSATT_UNDER_12_MÅNEDER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagVurderingBosattUnder12Maaneder" + }, + AVSLAG_IKKE_FLYTTET_FRA_TIDLIGERE_EKTEFELLE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeFlyttetFraTidligereEktefelle" + }, + AVSLAG_VURDERING_IKKE_FLYTTET_FRA_TIDLIGERE_EKTEFELLE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagVurderingIkkeFlyttetFraTidligereEktefelle" + }, + AVSLAG_AVTALE_OM_DELT_BOSTED_FØLGES_FORTSATT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagAvtaleOmDeltBostedFolgesFortsatt" + }, + AVSLAG_IKKE_OPPHOLDSTILLATELSE_MER_ENN_12_MÅNEDER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagIkkeOppholdstillatelseMerEnn12Maaneder" + }, + AVSLAG_BOR_IKKE_FAST_MED_BARNET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagBorIkkeFastMedBarnet" + }, + AVSLAG_ENSLIG_MINDREÅRIG_FLYKTNING_BOR_I_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG + override val sanityApiNavn = "avslagEnsligMindreaarigFlyktningBorIInstitusjon" + }, + OPPHØR_BARN_FLYTTET_FRA_SØKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorBarnBorIkkeMedSoker" + }, + OPPHØR_UTVANDRET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorFlyttetFraNorge" + }, + OPPHØR_BARN_DØD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorEtBarnErDodt" + }, + OPPHØR_FLERE_BARN_DØD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorFlereBarnErDode" + }, + OPPHØR_SØKER_HAR_IKKE_FAST_OMSORG { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorSokerHarIkkeFastOmsorg" + }, + OPPHØR_HAR_IKKE_OPPHOLDSTILLATELSE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorHarIkkeOppholdstillatelse" + }, + OPPHØR_DELT_BOSTED_OPPHØRT_ENIGHET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorDeltBostedOpphortEnighet" + }, + OPPHØR_DELT_BOSTED_OPPHØRT_UENIGHET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorDeltBostedOpphortUenighet" + }, + OPPHØR_UNDER_18_ÅR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorUnder18Aar" + }, + OPPHØR_ENDRET_MOTTAKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorEndretMottaker" + }, + OPPHØR_ANNEN_FORELDER_IKKE_LENGER_PLIKTIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorAnnenForelderIkkeLengerPliktigMedlem" + }, + OPPHØR_SØKER_OG_BARN_IKKE_LENGER_PLIKTIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorSokerOgBarnIkkeLengerPliktigMedlem" + }, + OPPHØR_BOSATT_I_NORGE_UNNTATT_MEDLEMSKAP { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorBosattINorgeUnntattMedlemskap" + }, + OPPHØR_ANNEN_FORELDER_IKKE_LENGER_MEDLEM_TRYGDEAVTALE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorAnnenForelderIkkeLengerMedlemTrygdeavtale" + }, + OPPHØR_SØKER_OG_BARN_IKKE_LENGER_MEDLEM_TRYGDEAVTALE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorSokerOgBarnIkkeLengerMedlemTrygdeavtale" + }, + OPPHØR_SØKER_OG_BARN_IKKE_LENGER_FRIVILLIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorSokerOgBarnIkkeLengerFrivilligMedlem" + }, + OPPHØR_VURDERING_ANNEN_FORELDER_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorVurderingAnnenForelderIkkeMedlem" + }, + OPPHØR_VURDERING_FLERE_KORTE_OPPHOLD_I_UTLANDET_SISTE_TO_ÅR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorVurderingFlereKorteOppholdIUtlandetSisteToAr" + }, + OPPHØR_VURDERING_SØKER_OG_BARN_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorVurderingSokerOgBarnIkkeMedlem" + }, + OPPHØR_SØKER_OG_BARN_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorSokerOgBarnIkkeMedlem" + }, + OPPHØR_VURDERING_FLERE_KORTE_OPPHOLD_I_UTLANDET_SISTE_ÅRENE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorVurderingFlereKorteOppholdIUtlandetSisteArene" + }, + OPPHØR_ANNEN_FORELDER_IKKE_LENGER_FRIVILLIG_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorAnnenForelderIkkeLengerFrivilligMedlem" + }, + OPPHØR_FORELDRENE_BOR_SAMMEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorForeldreneBorSammen" + }, + OPPHØR_AVTALE_OM_FAST_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorAvtaleOmFastBosted" + }, + OPPHØR_RETTSAVGJØRELSE_FAST_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorRettsavgjorelseFastBosted" + }, + OPPHØR_IKKE_AVTALE_OM_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorIkkeAvtaleOmDeltBosted" + }, + OPPHØR_VURDERING_FORELDRENE_BOR_SAMMEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorVurderingForeldreneBorSammen" + }, + OPPHØR_FORELDRENE_BODD_SAMMEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorForeldreneBoddSammen" + }, + OPPHØR_IKKE_OPPHOLDSTILLATELSE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorIkkeOppholdstillatelse" + }, + OPPHØR_VURDERING_FORELDRENE_BODDE_SAMMEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorVurderingForeldreneBoddeSammen" + }, + OPPHØR_IKKE_BOSATT_I_NORGE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorIkkeBosattINorge" + }, + OPPHØR_BARN_BODDE_IKKE_MED_SØKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorBarnBoddeIkkeMedSoker" + }, + OPPHØR_AVTALE_DELT_BOSTED_IKKE_GYLDIG { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorAvtaleDeltBostedIkkeGyldig" + }, + OPPHØR_VURDERING_VAR_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorVurderingVarIkkeMedlem" + }, + OPPHØR_VAR_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorVarIkkeMedlem" + }, + OPPHØR_VURDERING_DEN_ANDRE_FORELDEREN_VAR_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorVurderingDenAndreForelderenVarIkkeMedlem" + }, + OPPHØR_AVTALE_DELT_BOSTED_FØLGES_IKKE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorAvtaleDeltBostedFolgesIkke" + }, + OPPHØR_DEN_ANDRE_FORELDEREN_VAR_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorDenAndreForelderenVarIkkeMedlem" + }, + OPPHØR_IKKE_OPPHOLDSRETT_EØS_BORGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorIkkeOppholdsrettEosBorger" + }, + OPPHØR_BOSATT_I_NORGE_VAR_IKKE_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorBosattINorgeVarIkkeMedlem" + }, + OPPHØR_BARN_DØD_SAMME_MÅNED_SOM_FØDT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorBarnDodSammeMaanedSomFoedt" + }, + OPPHØR_UGYLDIG_KONTONUMMER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorUgyldigKontonummer" + }, + OPPHØR_FORELDRENE_BOR_SAMMEN_ENDRE_MOTTAKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorForeldreneBorSammenEndretMottaker" + }, + OPPHØR_SØKER_BER_OM_OPPHØR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorSokerBerOmOpphor" + }, + OPPHØR_IKKE_OPPHOLDSTILLATELSE_MER_ENN_12_MÅNEDER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorIkkeOppholdstillatelseMerEnn12Maaneder" + }, + OPPHØR_DELT_BOSTED_SØKER_BER_OM_OPPHØR { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphordeltBostedSoekerBerOmOpphoer" + }, + OPPHØR_FAST_BOSTED_AVTALE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorFastBostedAvtale" + }, + OPPHØR_BEGGE_FORELDRE_FÅTT_BARNETRYGD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorBeggeForeldreFaattBarnetrygd" + }, + OPPHOR_BARNET_BOR_I_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorBarnetBorIInstitusjon" + }, + OPPHØR_BARN_BOR_IKKE_MED_SØKER_ETTER_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorBarnBorIkkeMedSokerEtterDeltBosted" + }, + OPPHØR_VURDERING_IKKE_BOSATT_I_NORGE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.OPPHØR + override val sanityApiNavn = "opphorVurderingIkkeBosattINorge" + }, + FORTSATT_INNVILGET_SØKER_OG_BARN_BOSATT_I_RIKET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetSokerOgBarnBosattIRiket" + }, + FORTSATT_INNVILGET_SØKER_BOSATT_I_RIKET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetSokerBosattIRiket" + }, + FORTSATT_INNVILGET_BARN_BOSATT_I_RIKET { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetBarnBosattIRiket" + }, + FORTSATT_INNVILGET_BARN_OG_SØKER_LOVLIG_OPPHOLD_OPPHOLDSTILLATELSE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetBarnOgSokerLovligOppholdOppholdstillatelse" + }, + FORTSATT_INNVILGET_SØKER_LOVLIG_OPPHOLD_OPPHOLDSTILLATELSE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetSokerLovligOppholdOppholdstillatelse" + }, + FORTSATT_INNVILGET_BARN_LOVLIG_OPPHOLD_OPPHOLDSTILLATELSE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetBarnLovligOppholdOppholdstillatelse" + }, + FORTSATT_INNVILGET_BOR_MED_SØKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetBorMedSoker" + }, + FORTSATT_INNVILGET_FAST_OMSORG { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetFastOmsorg" + }, + FORTSATT_INNVILGET_LOVLIG_OPPHOLD_EØS { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetLovligOppholdEOS" + }, + FORTSATT_INNVILGET_LOVLIG_OPPHOLD_TREDJELANDSBORGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetLovligOppholdTredjelandsborger" + }, + FORTSATT_INNVILGET_UENDRET_TRYGD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetUendretTrygd" + }, + FORTSATT_INNVILGET_OPPHOLD_I_UTLANDET_IKKE_MER_ENN_3_MÅNEDER_SØKER_OG_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetOppholdIUtlandetIkkeMerEnn3ManederSokerOgBarn" + }, + FORTSATT_INNVILGET_HELE_FAMILIEN_MEDLEM_ETTER_TRYGDEAVTALE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetHeleFamilienMedlemEtterTrygdeavtale" + }, + FORTSATT_INNVILGET_OPPHOLD_I_UTLANDET_IKKE_MER_ENN_3_MÅNEDER_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetOppholdIUtlandetIkkeMerEnn3ManederBarn" + }, + FORTSATT_INNVILGET_DELT_BOSTED_PRAKTISERES_FORTSATT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetDeltBostedPraktiseresFortsatt" + }, + FORTSATT_INNVILGET_VURDERING_HELE_FAMILIEN_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetVurderingHeleFamilienMedlem" + }, + FORTSATT_INNVILGET_SØKER_OG_BARN_MEDLEM_ETTER_TRYGDEAVTALE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetSokerOgBarnMedlemEtterTrygdeavtale" + }, + FORTSATT_INNVILGET_ANNEN_FORELDER_IKKE_SØKT_OM_DELT_BARNETRYGD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetAnnenForelderIkkeSokt" + }, + FORTSATT_INNVILGET_VURDERING_SØKER_OG_BARN_MEDLEM { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetVurderingSokerOgBarnMedlem" + }, + FORTSATT_INNVILGET_MEDLEM_I_FOLKETRYGDEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetMedlemIFolketrygden" + }, + FORTSATT_INNVILGET_TVUNGENT_PSYKISK_HELSEVERN_GIFT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetTvungentPsykiskHelsevernGift" + }, + FORTSATT_INNVILGET_FENGSEL_GIFT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetFengselGift" + }, + FORTSATT_INNVILGET_VURDERING_BOR_ALENE_MED_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetVurderingBorAleneMedBarn" + }, + FORTSATT_INNVILGET_SEPARERT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetSeparert" + }, + FORTSATT_INNVILGET_FORTSATT_RETTSAVGJØRELSE_OM_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetFortsattRettsavgjorelseOmDeltBosted" + }, + FORTSATT_INNVILGET_BOR_ALENE_MED_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetBorAleneMedBarn" + }, + FORTSATT_INNVILGET_TVUNGENT_PSYKISK_HELSEVERN_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetTvungentPsykiskHelsevernSamboer" + }, + FORTSATT_INNVILGET_FORVARING_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetForvaringSamboer" + }, + FORTSATT_INNVILGET_FORTSATT_AVTALE_OM_DELT_BOSTED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetFortsattAVtaleOmDeltBosted" + }, + FORTSATT_INNVILGET_VARETEKTSFENGSEL_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetVaretektsfengselSamboer" + }, + FORTSATT_INNVILGET_FENGSEL_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetFengselSamboer" + }, + FORTSATT_INNVILGET_FORVARING_GIFT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetForvaringGift" + }, + FORTSATT_INNVILGET_VAREKTEKTSFENGSEL_GIFT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetVaretektsfengselGift" + }, + FORTSATT_INNVILGET_FORSVUNNET_SAMBOER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetForsvunnetSamboer" + }, + FORTSATT_INNVILGET_FORSVUNNET_EKTEFELLE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetForsvunnetEktefelle" + }, + FORTSATT_INNVILGET_BRUKER_ER_BLITT_NORSK_STATSBORGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetBrukerErBlittNorskStatsborger" + }, + FORTSATT_INNVILGET_BRUKER_OG_BARN_ER_BLITT_NORSKE_STATSBORGERE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetBrukerOgBarnErBlittNorskeStatsborgere" + }, + FORTSATT_INNVILGET_ET_BARN_ER_BLITT_NORSK_STATSBORGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetEtBarnErBlittNorskStatsborger" + }, + FORTSATT_INNVILGET_FLERE_BARN_ER_BLITT_NORSKE_STATSBORGERE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetFlereBarnErBlittNorskeStatsborgere" + }, + FORTSATT_INNVILGET_OPPDATERT_KONTO_OPPLYSNINGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetOppdatertKontoOpplysninger" + }, + FORTSATT_INNVILGET_ADRESSE_REGISTRERT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetAdresseRegistrert" + }, + FORTSATT_INNVILGET_VARIG_OPPHOLDSTILLATELSE { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetVarigOppholdstillatelse" + }, + FORTSATT_INNVILGET_VARIG_OPPHOLDSRETT_EØS_BORGER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetVarigOppholdsrettEosBorger" + }, + FORTSATT_INNVILGET_GENERELL_BOR_SAMMEN_MED_BARN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetGenerellBorSammenMedBarn" + }, + ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_INGEN_UTBETALING_NY { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingDeltBostedIngenUtbetaling" + }, + ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_FULL_UTBETALING_FØR_SOKNAD_NY { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingNyDeltBostedFullUtbetalingForSoknad" + }, + ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_KUN_ETTERBETALT_UTVIDET_NY { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingDeltBostedFaarKunEtterbetaltUtvidet" + }, + ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_MOTTATT_FULL_ORDINÆR_ETTERBETALT_UTVIDET_NY { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingMottattFullOrdinaerFaarEtterbetaltUtvidet" + }, + ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_ENDRET_UTBETALING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingDeltBostedEndretUtbetaling" + override val kanDelesOpp: Boolean = true + }, + ENDRET_UTBETALING_SEKUNDÆR_DELT_BOSTED_FULL_UTBETALING_FØR_SØKNAD { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingSekundaerDeltBostedFullUtbetalingFoerSoeknad" + }, + ENDRET_UTBETALING_ETTERBETALT_UTVIDET_DEL_FRA_AVTALETIDSPUNKT_SØKT_FOR_PRAKTISERT_DELT { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingEtterbetaltUtvidetDelFraSkriftligAvtaleSokerPraktisert" + }, + ENDRET_UTBETALING_ALLEREDE_UTBETALT_FORELDRE_BOR_SAMMEN { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingAlleredeUtbetalt" + }, + ENDRET_UTBETALING_ETTERBETALING_UTVIDET_EØS { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingEtterbetalingUtvidetEos" + }, + ENDRET_UTBETALING_OPPHØR_ENDRE_MOTTAKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingOpphorEndreMottaker" + }, + ENDRET_UTBETALING_REDUKSJON_ENDRE_MOTTAKER { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingReduksjonEndreMottaker" + }, + ENDRET_UTBETALING_ETTERBETALING_TRE_ÅR_TILBAKE_I_TID { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingEtterbetalingTreAarTilbakeITid" + }, + ENDRET_UTBETALING_ETTERBETALING_TRE_ÅR_TILBAKE_I_TID_SED { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingEtterbetalingTreAarTilbakeITidSED" + }, + ENDRET_UTBETALING_ETTERBETALING_TRE_ÅR_TILBAKE_I_TID_KUN_UTVIDET_DEL_UTBETALING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingEtterbetalingTreAarTilbakeITidKunUtvidetDelUtbetaling" + }, + ENDRET_UTBETALING_ETTERBETALING_TRE_ÅR_TILBAKE_I_TID_SED_UTBETALING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingEtterbetalingTreAarTilbakeITidSedUtbetaling" + }, + ENDRET_UTBETALING_TRE_ÅR_TILBAKE_I_TID_UTBETALING { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ENDRET_UTBETALING + override val sanityApiNavn = "endretUtbetalingTreAarTilbakeITidUtbetaling" + }, + ETTER_ENDRET_UTBETALING_RETTSAVGJØRELSE_DELT_BOSTED { + override val sanityApiNavn = "etterEndretUtbetalingRettsavgjorelseDeltBosted" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING + }, + ETTER_ENDRET_UTBETALING_AVTALE_DELT_BOSTED_FØLGES { + override val sanityApiNavn = "etterEndretUtbetalingVurderingAvtaleDeltBostedFolges" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING + }, + ETTER_ENDRET_UTBETALING_HAR_AVTALE_DELT_BOSTED { + override val sanityApiNavn = "etterEndretUtbetalingAvtaleDeltBosted" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING + }, + ETTER_ENDRET_UTBETALING_ETTERBETALING { + override val sanityApiNavn = "etterEndretUtbetalingEtterbetalingTreAarTilbakeITid" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING + }, + ETTER_ENDRET_UTBETALING_ETTERBETALING_UTVIDET { + override val sanityApiNavn = "etterEndretUtbetalingEtterbetalingTreAarTilbakeITidKunUtvidetDel" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING + }, + ETTER_ENDRET_UTBETALING_ETTERBETALING_SED { + override val sanityApiNavn = "etterEndretUtbetalingEtterbetalingTreAarTilbakeITidSed" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING + }, + ETTER_ENDRET_UTBETALING_ETTERBETALING_TRE_AAR { + override val sanityApiNavn = "etterEndretUtbetalingEtterbetalingTreAar" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING + }, + ETTER_ENDRET_UTBETALING_EØS_BARNETRYGD_ALLEREDE_UTBETALT { + override val sanityApiNavn = "etterEndretUtbetalingEosBarnetrygdAlleredeUtbetalt" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING + }, + ETTER_ENDRET_UTBETALING_ETTERBETALING_TRE_AAR_KUN_UTVIDET_DEL { + override val sanityApiNavn = "etterEndretUtbetalingEtterbetalingTreAarKunUtvidetDel" + override val vedtakBegrunnelseType = VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING + }, + + // Begrunnelser for institusjon + INNVILGET_BOR_FAST_I_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_INNVILGET + override val sanityApiNavn = "innvilgetBorFastIInstitusjon" + }, + INNVILGET_SATSENDRING_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_INNVILGET + override val sanityApiNavn = "innvilgetSatsendringInstitusjon" + }, + REDUKSJON_BARN_6_ÅR_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_REDUKSJON + override val sanityApiNavn = "reduksjonBarn6AarInstitusjon" + }, + REDUKSJON_SATSENDRING_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_REDUKSJON + override val sanityApiNavn = "reduksjonSatsendringInstitusjon" + }, + AVSLAG_IKKE_BOSATT_I_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_AVSLAG + override val sanityApiNavn = "avslagIkkeBosattIInstitusjon" + }, + AVSLAG_IKKE_OPPHOLDSTILLATELSE_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_AVSLAG + override val sanityApiNavn = "avslagIkkeOppholdstillatelseInstitusjon" + }, + OPPHØR_FLYTTET_FRA_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_OPPHØR + override val sanityApiNavn = "opphorFlyttetFraInstitusjon" + }, + OPPHØR_BARN_DØD_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_OPPHØR + override val sanityApiNavn = "opphorBarnDodInstitusjon" + }, + OPPHØR_BARN_BODDE_IKKE_FAST_I_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_OPPHØR + override val sanityApiNavn = "opphorBarnBoddeIkkeFastIInstitusjon" + }, + OPPHØR_BARN_HADDE_IKKE_OPPHOLDSTILLATELSE_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_OPPHØR + override val sanityApiNavn = "opphorBarnHaddeIkkeOppholdstillatelseInstitusjon" + }, + OPPHØR_OPPHOLDSTILLATELSE_UTLØPT_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_OPPHØR + override val sanityApiNavn = "opphorOppholdstillatelseUtloptInstitusjon" + }, + OPPHØR_BARNET_ER_18_ÅR_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_OPPHØR + override val sanityApiNavn = "opphorBarnetEr18AarInstitusjon" + }, + FORTSATT_INNVILGET_BOSATT_I_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetBosattIInstitusjon" + }, + FORTSATT_INNVILGET_OPPHOLDSTILLATELSE_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetOppholdstillatelseInstitusjon" + }, + FORTSATT_INNVILGET_VARIG_OPPHOLDSTILLATELSE_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetVarigOppholdstillatelseInstitusjon" + }, + FORTSATT_INNVILGET_NORSK_STATSBORGER_INSTITUSJON { + override val vedtakBegrunnelseType = VedtakBegrunnelseType.INSTITUSJON_FORTSATT_INNVILGET + override val sanityApiNavn = "fortsattInnvilgetNorskStatsborgerInstitusjon" + }, ; + + override val kanDelesOpp: Boolean = false + + @JsonValue + override fun enumnavnTilString(): String = + Standardbegrunnelse::class.simpleName + "$" + this.name + + override fun delOpp( + restBehandlingsgrunnlagForBrev: RestBehandlingsgrunnlagForBrev, + triggesAv: TriggesAv, + periode: NullablePeriode, + ): List { + if (!this.kanDelesOpp) { + throw Feil("Begrunnelse $this kan ikke deles opp.") + } + return when (this) { + Standardbegrunnelse.ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_ENDRET_UTBETALING -> { + val deltBostedEndringsperioder = this.hentRelevanteEndringsperioderForBegrunnelse( + minimerteRestEndredeAndeler = restBehandlingsgrunnlagForBrev.minimerteEndredeUtbetalingAndeler, + vedtaksperiode = periode, + ) + .filter { it.årsak == Årsak.DELT_BOSTED } + .filter { endringsperiode -> + endringsperiodeGjelderBarn( + personerPåBehandling = restBehandlingsgrunnlagForBrev.personerPåBehandling, + personIdentFraEndringsperiode = endringsperiode.personIdent, + ) + } + val deltBostedEndringsperioderGruppertPåAvtaledato = + deltBostedEndringsperioder.groupBy { it.avtaletidspunktDeltBosted } + + deltBostedEndringsperioderGruppertPåAvtaledato.map { + BrevBegrunnelseGrunnlagMedPersoner( + standardbegrunnelse = this, + vedtakBegrunnelseType = this.vedtakBegrunnelseType, + triggesAv = triggesAv, + personIdenter = it.value.map { endringsperiode -> endringsperiode.personIdent }, + avtaletidspunktDeltBosted = it.key, + ) + } + } + + else -> throw Feil("Oppdeling av begrunnelse $this er ikke støttet.") + } + } +} + +private fun endringsperiodeGjelderBarn( + personerPåBehandling: List, + personIdentFraEndringsperiode: String, +) = personerPåBehandling.find { person -> person.personIdent == personIdentFraEndringsperiode }?.type == PersonType.BARN + +val endretUtbetalingsperiodeBegrunnelser: Set = setOf( + Standardbegrunnelse.ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_INGEN_UTBETALING_NY, + Standardbegrunnelse.ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_FULL_UTBETALING_FØR_SOKNAD_NY, + Standardbegrunnelse.ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_KUN_ETTERBETALT_UTVIDET_NY, + Standardbegrunnelse.ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_MOTTATT_FULL_ORDINÆR_ETTERBETALT_UTVIDET_NY, + Standardbegrunnelse.ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_ENDRET_UTBETALING, + Standardbegrunnelse.ENDRET_UTBETALING_SEKUNDÆR_DELT_BOSTED_FULL_UTBETALING_FØR_SØKNAD, + Standardbegrunnelse.ENDRET_UTBETALING_ETTERBETALT_UTVIDET_DEL_FRA_AVTALETIDSPUNKT_SØKT_FOR_PRAKTISERT_DELT, + Standardbegrunnelse.ENDRET_UTBETALING_ALLEREDE_UTBETALT_FORELDRE_BOR_SAMMEN, + Standardbegrunnelse.ENDRET_UTBETALING_ETTERBETALING_UTVIDET_EØS, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/StandardbegrunnelseUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/StandardbegrunnelseUtils.kt new file mode 100644 index 000000000..380b62446 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/StandardbegrunnelseUtils.kt @@ -0,0 +1,202 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.erDagenFør +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.beregning.fomErPåSatsendring +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertEndretAndel +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertRestPersonResultat +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.harPersonerSomManglerOpplysninger +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertUtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.kjerne.brev.hentPersonerForAlleUtgjørendeVilkår +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.MinimertPerson +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.MinimertVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.harBarnMedSeksårsdagPåFom +import no.nav.familie.ba.sak.kjerne.vedtak.domene.Vedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import java.time.LocalDate + +fun Standardbegrunnelse.triggesForPeriode( + minimertVedtaksperiode: MinimertVedtaksperiode, + minimertePersonResultater: List, + minimertePersoner: List, + aktørIderMedUtbetaling: List, + minimerteEndredeUtbetalingAndeler: List = emptyList(), + sanityBegrunnelser: Map, + erFørsteVedtaksperiodePåFagsak: Boolean, + ytelserForSøkerForrigeMåned: List, + ytelserForrigePeriode: List, +): Boolean { + val triggesAv = sanityBegrunnelser[this]?.triggesAv ?: return false + + val aktuellePersoner = minimertePersoner + .filter { person -> triggesAv.personTyper.contains(person.type) } + .filter { person -> + if (this.vedtakBegrunnelseType.erInnvilget()) { + aktørIderMedUtbetaling.contains(person.aktørId) || person.type == PersonType.SØKER + } else { + true + } + } + + val ytelseTyperForPeriode = minimertVedtaksperiode.ytelseTyperForPeriode + + fun hentPersonerForUtgjørendeVilkår() = hentPersonerForAlleUtgjørendeVilkår( + minimertePersonResultater = minimertePersonResultater, + vedtaksperiode = Periode( + fom = minimertVedtaksperiode.fom ?: TIDENES_MORGEN, + tom = minimertVedtaksperiode.tom ?: TIDENES_ENDE, + ), + oppdatertBegrunnelseType = this.vedtakBegrunnelseType, + aktuellePersonerForVedtaksperiode = aktuellePersoner.map { it.tilMinimertRestPerson() }, + triggesAv = triggesAv, + begrunnelse = this, + erFørsteVedtaksperiodePåFagsak = erFørsteVedtaksperiodePåFagsak, + ) + + return when { + !triggesAv.valgbar -> false + + triggesAv.vilkår.contains(Vilkår.UTVIDET_BARNETRYGD) && !triggesAv.erEndret() -> this.vedtakBegrunnelseType.periodeErOppyltForYtelseType( + ytelseType = if (triggesAv.småbarnstillegg) YtelseType.SMÅBARNSTILLEGG else YtelseType.UTVIDET_BARNETRYGD, + ytelseTyperForPeriode = ytelseTyperForPeriode, + ytelserGjeldeneForSøkerForrigeMåned = ytelserForSøkerForrigeMåned, + ) || when { + triggesAv.vilkår.any { it != Vilkår.UTVIDET_BARNETRYGD } -> hentPersonerForUtgjørendeVilkår().isNotEmpty() + else -> false + } + triggesAv.personerManglerOpplysninger -> minimertePersonResultater.harPersonerSomManglerOpplysninger() + triggesAv.barnMedSeksårsdag -> + minimertePersoner.harBarnMedSeksårsdagPåFom(minimertVedtaksperiode.fom) + triggesAv.satsendring -> fomErPåSatsendring(minimertVedtaksperiode.fom ?: TIDENES_MORGEN) + + triggesAv.etterEndretUtbetaling -> + erEtterEndretPeriodeAvSammeÅrsak( + minimerteEndredeUtbetalingAndeler, + minimertVedtaksperiode, + aktuellePersoner, + triggesAv, + ) + + triggesAv.erEndret() && !triggesAv.etterEndretUtbetaling -> erEndretTriggerErOppfylt( + triggesAv = triggesAv, + minimerteEndredeUtbetalingAndeler = minimerteEndredeUtbetalingAndeler, + minimertVedtaksperiode = minimertVedtaksperiode, + ) + triggesAv.gjelderFraInnvilgelsestidspunkt -> false + triggesAv.barnDød -> dødeBarnForrigePeriode( + ytelserForrigePeriode, + minimertePersoner.filter { it.type === PersonType.BARN }, + ).any() + else -> hentPersonerForUtgjørendeVilkår().isNotEmpty() + } +} + +fun dødeBarnForrigePeriode( + ytelserForrigePeriode: List, + barnIBehandling: List, +): List { + return barnIBehandling.filter { barn -> + val ytelserForrigePeriodeForBarn = ytelserForrigePeriode.filter { + it.aktør.aktivFødselsnummer() == barn.aktivPersonIdent + } + var barnDødeForrigePeriode = false + if (barn.erDød() && ytelserForrigePeriodeForBarn.isNotEmpty()) { + val fom = + ytelserForrigePeriodeForBarn.minOf { it.stønadFom } + val tom = + ytelserForrigePeriodeForBarn.maxOf { it.stønadTom } + val fomFørDødsfall = fom <= barn.dødsfallsdato!!.toYearMonth() + val tomEtterDødsfall = tom >= barn.dødsfallsdato.toYearMonth() + barnDødeForrigePeriode = fomFørDødsfall && tomEtterDødsfall + } + barnDødeForrigePeriode + }.map { it.aktivPersonIdent } +} + +private fun erEndretTriggerErOppfylt( + triggesAv: TriggesAv, + minimerteEndredeUtbetalingAndeler: List, + minimertVedtaksperiode: MinimertVedtaksperiode, +): Boolean { + val endredeAndelerSomOverlapperVedtaksperiode = minimertVedtaksperiode + .finnEndredeAndelerISammePeriode(minimerteEndredeUtbetalingAndeler) + + return endredeAndelerSomOverlapperVedtaksperiode.any { minimertEndretAndel -> + triggesAv.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = minimertEndretAndel, + minimerteUtbetalingsperiodeDetaljer = minimertVedtaksperiode + .utbetalingsperioder.map { it.tilMinimertUtbetalingsperiodeDetalj() }, + ) + } +} + +private fun erEtterEndretPeriodeAvSammeÅrsak( + endretUtbetalingAndeler: List, + minimertVedtaksperiode: MinimertVedtaksperiode, + aktuellePersoner: List, + triggesAv: TriggesAv, +) = endretUtbetalingAndeler.any { endretUtbetalingAndel -> + endretUtbetalingAndel.månedPeriode().tom.sisteDagIInneværendeMåned() + .erDagenFør(minimertVedtaksperiode.fom) && + aktuellePersoner.any { person -> person.aktørId == endretUtbetalingAndel.aktørId } && + triggesAv.endringsaarsaker.contains(endretUtbetalingAndel.årsak) +} + +fun List.tilBrevTekst(): String = Utils.slåSammen(this.sorted().map { it.tilKortString() }) + +fun Standardbegrunnelse.tilVedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser, +): Vedtaksbegrunnelse { + if (!vedtaksperiodeMedBegrunnelser + .type + .tillatteBegrunnelsestyper + .contains(this.vedtakBegrunnelseType) + ) { + throw Feil( + "Begrunnelsestype ${this.vedtakBegrunnelseType} passer ikke med " + + "typen '${vedtaksperiodeMedBegrunnelser.type}' som er satt på perioden.", + ) + } + + return Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, + standardbegrunnelse = this, + ) +} + +fun VedtakBegrunnelseType.periodeErOppyltForYtelseType( + ytelseType: YtelseType, + ytelseTyperForPeriode: Set, + ytelserGjeldeneForSøkerForrigeMåned: List, +): Boolean { + return when (this) { + VedtakBegrunnelseType.INNVILGET, VedtakBegrunnelseType.INSTITUSJON_INNVILGET -> ytelseTyperForPeriode.contains( + ytelseType, + ) + + VedtakBegrunnelseType.REDUKSJON, VedtakBegrunnelseType.INSTITUSJON_REDUKSJON -> !ytelseTyperForPeriode.contains( + ytelseType, + ) && + ytelseOppfyltForrigeMåned(ytelseType, ytelserGjeldeneForSøkerForrigeMåned) + + else -> false + } +} + +private fun ytelseOppfyltForrigeMåned( + ytelseType: YtelseType, + ytelserGjeldeneForSøkerForrigeMåned: List, +) = ytelserGjeldeneForSøkerForrigeMåned + .any { it == ytelseType } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/TriggesAv.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/TriggesAv.kt new file mode 100644 index 000000000..a4494f442 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/TriggesAv.kt @@ -0,0 +1,149 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser + +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.domene.EndretUtbetalingsperiodeDeltBostedTriggere +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertEndretAndel +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertUtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertVilkårResultat +import no.nav.familie.ba.sak.kjerne.brev.domene.Valgbarhet +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import java.math.BigDecimal + +data class TriggesAv( + val vilkår: Set, + val personTyper: Set, + val personerManglerOpplysninger: Boolean, + val satsendring: Boolean, + val barnMedSeksårsdag: Boolean, + val vurderingAnnetGrunnlag: Boolean, + val medlemskap: Boolean, + val deltbosted: Boolean, + val deltBostedSkalIkkeDeles: Boolean, + val valgbar: Boolean, + val valgbarhet: Valgbarhet?, + val endringsaarsaker: Set<Årsak>, + val etterEndretUtbetaling: Boolean, + val endretUtbetalingSkalUtbetales: EndretUtbetalingsperiodeDeltBostedTriggere, + val småbarnstillegg: Boolean, + val gjelderFørstePeriode: Boolean, + val gjelderFraInnvilgelsestidspunkt: Boolean, + val barnDød: Boolean, +) { + fun erEndret() = endringsaarsaker.isNotEmpty() + + fun erUtdypendeVilkårsvurderingOppfylt( + vilkårResultat: MinimertVilkårResultat, + ): Boolean { + return erDeltBostedOppfylt(vilkårResultat) && + erSkjønnsmessigVurderingOppfylt(vilkårResultat) && + erMedlemskapOppfylt(vilkårResultat) && + erDeltBostedSkalIkkDelesOppfylt(vilkårResultat) + } + + fun erUtdypendeVilkårsvurderingOppfyltReduksjon( + vilkårSomAvsluttesRettFørDennePerioden: MinimertVilkårResultat, + vilkårSomStarterIDennePerioden: MinimertVilkårResultat?, + ): Boolean { + return erDeltBostedOppfyltReduksjon( + vilkårSomAvsluttesRettFørDennePerioden = vilkårSomAvsluttesRettFørDennePerioden, + vilkårSomStarterIDennePerioden = vilkårSomStarterIDennePerioden, + ) && + erSkjønnsmessigVurderingOppfylt(vilkårSomAvsluttesRettFørDennePerioden) && + erMedlemskapOppfylt(vilkårSomAvsluttesRettFørDennePerioden) && + erDeltBostedSkalIkkDelesOppfylt(vilkårSomAvsluttesRettFørDennePerioden) + } + + private fun erMedlemskapOppfylt(vilkårResultat: MinimertVilkårResultat): Boolean { + val vilkårResultatInneholderMedlemsskap = + vilkårResultat.utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP) + + return this.medlemskap == vilkårResultatInneholderMedlemsskap + } + + private fun erSkjønnsmessigVurderingOppfylt(vilkårResultat: MinimertVilkårResultat): Boolean { + val vilkårResultatInneholderVurderingAnnetGrunnlag = + vilkårResultat.utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.VURDERING_ANNET_GRUNNLAG) + + return this.vurderingAnnetGrunnlag == vilkårResultatInneholderVurderingAnnetGrunnlag + } + + private fun erDeltBostedSkalIkkDelesOppfylt(vilkårResultat: MinimertVilkårResultat): Boolean { + val vilkårResultatInnholderDeltBostedSkalIkkeDeles = + vilkårResultat.utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.DELT_BOSTED_SKAL_IKKE_DELES) + + return this.deltBostedSkalIkkeDeles == vilkårResultatInnholderDeltBostedSkalIkkeDeles + } + + private fun erDeltBostedOppfylt(vilkårResultat: MinimertVilkårResultat): Boolean { + val vilkårResultatInneholderDeltBosted = + vilkårResultat.utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.DELT_BOSTED) + + return this.deltbosted == vilkårResultatInneholderDeltBosted + } + + private fun erDeltBostedOppfyltReduksjon( + vilkårSomAvsluttesRettFørDennePerioden: MinimertVilkårResultat, + vilkårSomStarterIDennePerioden: MinimertVilkårResultat?, + ): Boolean { + val avsluttetVilkårInneholdtDeltBosted = + vilkårSomAvsluttesRettFørDennePerioden.utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.DELT_BOSTED) + + val påbegyntVilkårInneholderDeltBosted = vilkårSomStarterIDennePerioden?.utdypendeVilkårsvurderinger + ?.contains(UtdypendeVilkårsvurdering.DELT_BOSTED) ?: false + + return if (this.deltbosted) { + avsluttetVilkårInneholdtDeltBosted != påbegyntVilkårInneholderDeltBosted + } else { + !avsluttetVilkårInneholdtDeltBosted && !påbegyntVilkårInneholderDeltBosted + } + } +} + +fun TriggesAv.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel: MinimertEndretAndel, + minimerteUtbetalingsperiodeDetaljer: List, +): Boolean { + val hørerTilEtterEndretUtbetaling = this.etterEndretUtbetaling + + val oppfyllerSkalUtbetalesTrigger = minimertEndretAndel.oppfyllerSkalUtbetalesTrigger(this) + + val oppfyllerUtvidetScenario = + this.endretUtbetalingSkalUtbetales == EndretUtbetalingsperiodeDeltBostedTriggere.UTBETALING_IKKE_RELEVANT || + endretUtbetalingBegrunnelseOppfyllerUtvidetScenario( + vilkårBegrunnelsenGjelderFor = this.vilkår, + minimerteUtbetalingsperiodeDetaljer = minimerteUtbetalingsperiodeDetaljer, + ) + + val erAvSammeÅrsak = this.endringsaarsaker.contains(minimertEndretAndel.årsak) + + return !hørerTilEtterEndretUtbetaling && + oppfyllerSkalUtbetalesTrigger && + oppfyllerUtvidetScenario && erAvSammeÅrsak +} + +fun MinimertEndretAndel.oppfyllerSkalUtbetalesTrigger( + triggesAv: TriggesAv, +): Boolean { + val inneholderAndelSomSkalUtbetales = this.prosent!! != BigDecimal.ZERO + return when (triggesAv.endretUtbetalingSkalUtbetales) { + EndretUtbetalingsperiodeDeltBostedTriggere.UTBETALING_IKKE_RELEVANT -> true + EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_UTBETALES -> inneholderAndelSomSkalUtbetales + EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_IKKE_UTBETALES -> !inneholderAndelSomSkalUtbetales + } +} + +private fun endretUtbetalingBegrunnelseOppfyllerUtvidetScenario( + vilkårBegrunnelsenGjelderFor: Set?, + minimerteUtbetalingsperiodeDetaljer: List, +): Boolean { + val begrunnelseGjelderUtvidet = vilkårBegrunnelsenGjelderFor?.contains(Vilkår.UTVIDET_BARNETRYGD) ?: false + + val periodeInneholderUtvidetMedEndring = minimerteUtbetalingsperiodeDetaljer.singleOrNull { + it.ytelseType == YtelseType.UTVIDET_BARNETRYGD + }?.erPåvirketAvEndring == true + + return begrunnelseGjelderUtvidet == periodeInneholderUtvidetMedEndring +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/VedtakBegrunnelseType.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/VedtakBegrunnelseType.kt new file mode 100644 index 000000000..1b259e031 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/VedtakBegrunnelseType.kt @@ -0,0 +1,70 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.forrigeMåned +import no.nav.familie.ba.sak.common.tilMånedÅr + +@Deprecated("Kan vi fjerne dette? Krever at vi kan se om det er avslag fra sanity og at vi ikke bruker det i familie-brev.") +enum class VedtakBegrunnelseType(val sorteringsrekkefølge: Int) { + INNVILGET(2), + EØS_INNVILGET(2), + INSTITUSJON_INNVILGET(2), + REDUKSJON(1), + INSTITUSJON_REDUKSJON(1), + EØS_REDUKSJON(1), + AVSLAG(3), + EØS_AVSLAG(3), + INSTITUSJON_AVSLAG(3), + OPPHØR(4), + EØS_OPPHØR(4), + INSTITUSJON_OPPHØR(4), + FORTSATT_INNVILGET(5), + EØS_FORTSATT_INNVILGET(5), + INSTITUSJON_FORTSATT_INNVILGET(5), + ENDRET_UTBETALING(7), + ETTER_ENDRET_UTBETALING(6), + ; + + fun erInnvilget(): Boolean { + return this == INNVILGET || this == INSTITUSJON_INNVILGET + } + + fun erFortsattInnvilget(): Boolean { + return this == FORTSATT_INNVILGET || this == INSTITUSJON_FORTSATT_INNVILGET + } + + fun erReduksjon(): Boolean { + return this == REDUKSJON || this == INSTITUSJON_REDUKSJON + } + + fun erAvslag(): Boolean { + return this == AVSLAG || this == INSTITUSJON_AVSLAG || this == EØS_AVSLAG + } + + fun erOpphør(): Boolean { + return this == OPPHØR || this == INSTITUSJON_OPPHØR + } +} + +fun VedtakBegrunnelseType.hentMånedOgÅrForBegrunnelse(periode: Periode) = + when (this) { + VedtakBegrunnelseType.AVSLAG, VedtakBegrunnelseType.INSTITUSJON_AVSLAG -> { + val fomTekst = periode.fom.forrigeMåned().tilMånedÅr() + + if (periode.fom == TIDENES_MORGEN && periode.tom == TIDENES_ENDE) { + "" + } else { + fomTekst + } + } + + else -> + if (periode.fom == TIDENES_MORGEN) { + throw Feil("Prøver å finne fom-dato for begrunnelse, men fikk \"TIDENES_MORGEN\".") + } else { + periode.fom.forrigeMåned().tilMånedÅr() + } + } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/domene/E\303\230SBegrunnelse.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/domene/E\303\230SBegrunnelse.kt" new file mode 100644 index 000000000..6c6bfe6c2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/domene/E\303\230SBegrunnelse.kt" @@ -0,0 +1,54 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.RestVedtaksbegrunnelse +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "eøsBegrunnelse") +@Table(name = "EOS_BEGRUNNELSE") +class EØSBegrunnelse( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "eos_begrunnelse_seq_generator") + @SequenceGenerator( + name = "eos_begrunnelse_seq_generator", + sequenceName = "eos_begrunnelse_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @JsonIgnore + @ManyToOne + @JoinColumn(name = "fk_vedtaksperiode_id", nullable = false, updatable = false) + val vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser, + + @Enumerated(EnumType.STRING) + @Column(name = "begrunnelse", updatable = false) + val begrunnelse: EØSStandardbegrunnelse, +) { + fun kopier(vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser): EØSBegrunnelse = + EØSBegrunnelse( + vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, + begrunnelse = this.begrunnelse, + ) + + fun tilRestVedtaksbegrunnelse() = RestVedtaksbegrunnelse( + standardbegrunnelse = this.begrunnelse.enumnavnTilString(), + vedtakBegrunnelseType = this.begrunnelse.vedtakBegrunnelseType, + vedtakBegrunnelseSpesifikasjon = this.begrunnelse.enumnavnTilString(), + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/domene/MinimertPerson.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/domene/MinimertPerson.kt new file mode 100644 index 000000000..e59bd3569 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/domene/MinimertPerson.kt @@ -0,0 +1,48 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene + +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.vedtak.domene.MinimertRestPerson +import java.time.LocalDate + +class MinimertPerson( + val type: PersonType, + val fødselsdato: LocalDate, + val aktørId: String, + val aktivPersonIdent: String, + val dødsfallsdato: LocalDate?, +) { + val erDød = { + dødsfallsdato != null + } + fun hentSeksårsdag(): LocalDate = fødselsdato.plusYears(6) + + fun tilMinimertRestPerson() = MinimertRestPerson( + personIdent = aktivPersonIdent, + fødselsdato = fødselsdato, + type = type, + ) +} + +fun PersonopplysningGrunnlag.tilMinimertePersoner(): List = + this.søkerOgBarn.tilMinimertePersoner() + +fun List.tilMinimertePersoner(): List = + this.map { + MinimertPerson( + it.type, + it.fødselsdato, + it.aktør.aktørId, + it.aktør.aktivFødselsnummer(), + it.dødsfall?.dødsfallDato, + ) + } + +fun List.harBarnMedSeksårsdagPåFom(fom: LocalDate?) = this.any { person -> + person + .hentSeksårsdag() + .toYearMonth() == (fom?.toYearMonth() ?: TIDENES_ENDE.toYearMonth()) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/domene/MinimertVedtaksperiode.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/domene/MinimertVedtaksperiode.kt new file mode 100644 index 000000000..42841b41e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/domene/MinimertVedtaksperiode.kt @@ -0,0 +1,39 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene + +import no.nav.familie.ba.sak.common.NullableMånedPeriode +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertEndretAndel +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.UtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.UtvidetVedtaksperiodeMedBegrunnelser +import java.time.LocalDate + +class MinimertVedtaksperiode( + val fom: LocalDate?, + val tom: LocalDate?, + val ytelseTyperForPeriode: Set, + val type: Vedtaksperiodetype, + val utbetalingsperioder: List, +) { + fun finnEndredeAndelerISammePeriode( + endretUtbetalingAndeler: List, + ) = endretUtbetalingAndeler.filter { + it.erOverlappendeMed( + NullableMånedPeriode( + this.fom?.toYearMonth(), + this.tom?.toYearMonth(), + ), + ) + } +} + +fun UtvidetVedtaksperiodeMedBegrunnelser.tilMinimertVedtaksperiode(): MinimertVedtaksperiode { + return MinimertVedtaksperiode( + fom = this.fom, + tom = this.tom, + ytelseTyperForPeriode = this.utbetalingsperiodeDetaljer.map { it.ytelseType }.toSet(), + type = this.type, + utbetalingsperioder = this.utbetalingsperiodeDetaljer, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/MinimertRestPerson.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/MinimertRestPerson.kt new file mode 100644 index 000000000..174efbd5b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/MinimertRestPerson.kt @@ -0,0 +1,85 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.domene + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.ekstern.restDomene.RestPerson +import no.nav.familie.ba.sak.kjerne.brev.domene.BrevPeriodePersonForLogging +import no.nav.familie.ba.sak.kjerne.brev.domene.EndretUtbetalingAndelPåPersonForLogging +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertVedtaksperiode +import no.nav.familie.ba.sak.kjerne.brev.domene.RestBehandlingsgrunnlagForBrev +import no.nav.familie.ba.sak.kjerne.brev.domene.UtbetalingPåPersonForLogging +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import java.time.LocalDate + +/** + * NB: Bør ikke brukes internt, men kun ut mot eksterne tjenester siden klassen + * inneholder aktiv personIdent og ikke aktørId. + */ +data class MinimertRestPerson( + val personIdent: String, + val fødselsdato: LocalDate, + val type: PersonType, +) { + fun hentSeksårsdag(): LocalDate = fødselsdato.plusYears(6) +} + +fun RestPerson.tilMinimertPerson() = MinimertRestPerson( + personIdent = this.personIdent, + fødselsdato = fødselsdato ?: throw Feil("Fødselsdato mangler"), + type = this.type, +) + +fun List.barnMedSeksårsdagPåFom(fom: LocalDate?): List { + return this + .filter { it.type == PersonType.BARN } + .filter { person -> + person.hentSeksårsdag().toYearMonth() == ( + fom?.toYearMonth() + ?: TIDENES_ENDE.toYearMonth() + ) + } +} + +fun Person.tilMinimertPerson() = MinimertRestPerson( + personIdent = this.aktør.aktivFødselsnummer(), + fødselsdato = this.fødselsdato, + type = this.type, +) + +fun MinimertRestPerson.tilBrevPeriodeTestPerson( + brevPeriodeGrunnlag: MinimertVedtaksperiode, + restBehandlingsgrunnlagForBrev: RestBehandlingsgrunnlagForBrev, + barnMedReduksjonFraForrigeBehandlingIdent: List, +): BrevPeriodePersonForLogging { + val minimertePersonResultater = + restBehandlingsgrunnlagForBrev.minimertePersonResultater.firstOrNull { it.personIdent == this.personIdent }!! + val minimerteEndretUtbetalingAndelPåPerson = + restBehandlingsgrunnlagForBrev.minimerteEndredeUtbetalingAndeler.filter { it.personIdent == this.personIdent } + val minimerteUtbetalingsperiodeDetaljer = brevPeriodeGrunnlag.minimerteUtbetalingsperiodeDetaljer.filter { + it.person.personIdent == this.personIdent + } + + return BrevPeriodePersonForLogging( + fødselsdato = this.fødselsdato, + type = this.type, + overstyrteVilkårresultater = minimertePersonResultater.minimerteVilkårResultater, + andreVurderinger = minimertePersonResultater.minimerteAndreVurderinger, + endredeUtbetalinger = minimerteEndretUtbetalingAndelPåPerson.map { + EndretUtbetalingAndelPåPersonForLogging( + periode = it.periode, + årsak = it.årsak, + ) + }, + utbetalinger = minimerteUtbetalingsperiodeDetaljer.map { + UtbetalingPåPersonForLogging( + it.ytelseType, + it.utbetaltPerMnd, + it.erPåvirketAvEndring, + it.prosent, + ) + }, + harReduksjonFraForrigeBehandling = barnMedReduksjonFraForrigeBehandlingIdent.contains(this.personIdent), + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/Vedtaksbegrunnelse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/Vedtaksbegrunnelse.kt new file mode 100644 index 000000000..481dab2f0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/Vedtaksbegrunnelse.kt @@ -0,0 +1,320 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.domene + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.NullablePeriode +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.erDagenFør +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.domene.BrevBegrunnelseGrunnlagMedPersoner +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertRestEndretAndel +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertUregistrertBarn +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertUtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.kjerne.brev.domene.beløpUtbetaltFor +import no.nav.familie.ba.sak.kjerne.brev.domene.totaltUtbetalt +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.erAvslagUregistrerteBarnBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.hentMånedOgÅrForBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.tilBrevTekst +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.RestVedtaksbegrunnelse +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Vedtaksbegrunnelse") +@Table(name = "VEDTAKSBEGRUNNELSE") +class Vedtaksbegrunnelse( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "vedtaksbegrunnelse_seq_generator") + @SequenceGenerator( + name = "vedtaksbegrunnelse_seq_generator", + sequenceName = "vedtaksbegrunnelse_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @JsonIgnore + @ManyToOne + @JoinColumn(name = "fk_vedtaksperiode_id", nullable = false, updatable = false) + val vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser, + + @Enumerated(EnumType.STRING) + @Column(name = "vedtak_begrunnelse_spesifikasjon", updatable = false) + val standardbegrunnelse: Standardbegrunnelse, +) { + + fun kopier(vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser): Vedtaksbegrunnelse = Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, + standardbegrunnelse = this.standardbegrunnelse, + ) + + override fun toString(): String { + return "Vedtaksbegrunnelse(id=$id, standardbegrunnelse=$standardbegrunnelse)" + } +} + +fun Vedtaksbegrunnelse.tilRestVedtaksbegrunnelse() = RestVedtaksbegrunnelse( + standardbegrunnelse = this.standardbegrunnelse.enumnavnTilString(), + vedtakBegrunnelseType = this.standardbegrunnelse.vedtakBegrunnelseType, + vedtakBegrunnelseSpesifikasjon = this.standardbegrunnelse.enumnavnTilString(), +) + +enum class Begrunnelsetype { + STANDARD_BEGRUNNELSE, + EØS_BEGRUNNELSE, + FRITEKST, +} + +interface BrevBegrunnelse : Comparable { + val type: Begrunnelsetype + val vedtakBegrunnelseType: VedtakBegrunnelseType? + + override fun compareTo(other: BrevBegrunnelse): Int { + return when { + this.type == Begrunnelsetype.FRITEKST -> Int.MAX_VALUE + other.type == Begrunnelsetype.FRITEKST -> -Int.MAX_VALUE + this.vedtakBegrunnelseType == null -> Int.MAX_VALUE + other.vedtakBegrunnelseType == null -> -Int.MAX_VALUE + + else -> this.vedtakBegrunnelseType!!.sorteringsrekkefølge - other.vedtakBegrunnelseType!!.sorteringsrekkefølge + } + } +} + +interface BegrunnelseMedData : BrevBegrunnelse { + val apiNavn: String +} + +data class BegrunnelseData( + override val vedtakBegrunnelseType: VedtakBegrunnelseType, + override val apiNavn: String, + + val gjelderSoker: Boolean, + val barnasFodselsdatoer: String, + val fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling: String, + val fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling: String, + val antallBarn: Int, + val antallBarnOppfyllerTriggereOgHarUtbetaling: Int, + val antallBarnOppfyllerTriggereOgHarNullutbetaling: Int, + val maanedOgAarBegrunnelsenGjelderFor: String?, + val maalform: String, + val belop: String, + val soknadstidspunkt: String, + val avtaletidspunktDeltBosted: String, + val sokersRettTilUtvidet: String, +) : BegrunnelseMedData { + override val type: Begrunnelsetype = Begrunnelsetype.STANDARD_BEGRUNNELSE +} + +data class FritekstBegrunnelse( + val fritekst: String, +) : BrevBegrunnelse { + override val vedtakBegrunnelseType: VedtakBegrunnelseType? = null + override val type: Begrunnelsetype = Begrunnelsetype.FRITEKST +} + +sealed class EØSBegrunnelseData : BegrunnelseMedData + +data class EØSBegrunnelseDataMedKompetanse( + override val vedtakBegrunnelseType: VedtakBegrunnelseType, + override val apiNavn: String, + + val annenForeldersAktivitet: KompetanseAktivitet, + val annenForeldersAktivitetsland: String?, + val barnetsBostedsland: String, + val sokersAktivitet: KompetanseAktivitet, + val sokersAktivitetsland: String?, + val barnasFodselsdatoer: String, + val antallBarn: Int, + val maalform: String, +) : EØSBegrunnelseData() { + override val type: Begrunnelsetype = Begrunnelsetype.EØS_BEGRUNNELSE +} + +data class EØSBegrunnelseDataUtenKompetanse( + override val vedtakBegrunnelseType: VedtakBegrunnelseType, + override val apiNavn: String, + + val barnasFodselsdatoer: String, + val antallBarn: Int, + val maalform: String, + val gjelderSoker: Boolean, +) : EØSBegrunnelseData() { + override val type: Begrunnelsetype = Begrunnelsetype.EØS_BEGRUNNELSE +} + +fun BrevBegrunnelseGrunnlagMedPersoner.tilBrevBegrunnelse( + vedtaksperiode: NullablePeriode, + personerIPersongrunnlag: List, + brevMålform: Målform, + uregistrerteBarn: List, + minimerteUtbetalingsperiodeDetaljer: List, + minimerteRestEndredeAndeler: List, +): BrevBegrunnelse { + val personerPåBegrunnelse = + personerIPersongrunnlag.filter { person -> this.personIdenter.contains(person.personIdent) } + + val barnSomOppfyllerTriggereOgHarUtbetaling = personerPåBegrunnelse.filter { person -> + person.type == PersonType.BARN && minimerteUtbetalingsperiodeDetaljer.any { it.utbetaltPerMnd > 0 && it.person.personIdent == person.personIdent } + } + val barnSomOppfyllerTriggereOgHarNullutbetaling = personerPåBegrunnelse.filter { person -> + person.type == PersonType.BARN && minimerteUtbetalingsperiodeDetaljer.any { it.utbetaltPerMnd == 0 && it.person.personIdent == person.personIdent } + } + + val gjelderSøker = personerPåBegrunnelse.any { it.type == PersonType.SØKER } + + val barnasFødselsdatoer = this.hentBarnasFødselsdagerForBegrunnelse( + uregistrerteBarn = uregistrerteBarn, + personerIBehandling = personerIPersongrunnlag, + personerPåBegrunnelse = personerPåBegrunnelse, + personerMedUtbetaling = minimerteUtbetalingsperiodeDetaljer.map { it.person }, + gjelderSøker = gjelderSøker, + ) + + val antallBarn = this.hentAntallBarnForBegrunnelse( + uregistrerteBarn = uregistrerteBarn, + gjelderSøker = gjelderSøker, + barnasFødselsdatoer = barnasFødselsdatoer, + ) + + val månedOgÅrBegrunnelsenGjelderFor = + if (vedtaksperiode.fom == null) { + null + } else { + this.vedtakBegrunnelseType.hentMånedOgÅrForBegrunnelse( + periode = Periode( + fom = vedtaksperiode.fom, + tom = vedtaksperiode.tom ?: TIDENES_ENDE, + ), + ) + } + + val beløp = this.hentBeløp(gjelderSøker, minimerteUtbetalingsperiodeDetaljer) + + val endringsperioder = this.standardbegrunnelse.hentRelevanteEndringsperioderForBegrunnelse( + minimerteRestEndredeAndeler = minimerteRestEndredeAndeler, + vedtaksperiode = vedtaksperiode, + ) + + val søknadstidspunkt = endringsperioder.sortedBy { it.søknadstidspunkt } + .firstOrNull { this.triggesAv.endringsaarsaker.contains(it.årsak) }?.søknadstidspunkt + + val søkersRettTilUtvidet = + finnUtOmSøkerFårUtbetaltEllerHarRettPåUtvidet(minimerteUtbetalingsperiodeDetaljer = minimerteUtbetalingsperiodeDetaljer) + + this.validerBrevbegrunnelse( + gjelderSøker = gjelderSøker, + barnasFødselsdatoer = barnasFødselsdatoer, + ) + + return BegrunnelseData( + gjelderSoker = gjelderSøker, + barnasFodselsdatoer = barnasFødselsdatoer.tilBrevTekst(), + fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling = barnSomOppfyllerTriggereOgHarUtbetaling.map { it.fødselsdato } + .tilBrevTekst(), + fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling = barnSomOppfyllerTriggereOgHarNullutbetaling.map { it.fødselsdato } + .tilBrevTekst(), + antallBarn = antallBarn, + antallBarnOppfyllerTriggereOgHarUtbetaling = barnSomOppfyllerTriggereOgHarUtbetaling.size, + antallBarnOppfyllerTriggereOgHarNullutbetaling = barnSomOppfyllerTriggereOgHarNullutbetaling.size, + maanedOgAarBegrunnelsenGjelderFor = månedOgÅrBegrunnelsenGjelderFor, + maalform = brevMålform.tilSanityFormat(), + apiNavn = this.standardbegrunnelse.sanityApiNavn, + belop = Utils.formaterBeløp(beløp), + soknadstidspunkt = søknadstidspunkt?.tilKortString() ?: "", + avtaletidspunktDeltBosted = this.avtaletidspunktDeltBosted?.tilKortString() ?: "", + sokersRettTilUtvidet = søkersRettTilUtvidet.tilSanityFormat(), + vedtakBegrunnelseType = this.vedtakBegrunnelseType, + ) +} + +private fun finnUtOmSøkerFårUtbetaltEllerHarRettPåUtvidet(minimerteUtbetalingsperiodeDetaljer: List): SøkersRettTilUtvidet { + val utvidetUtbetalingsdetaljerPåSøker = + minimerteUtbetalingsperiodeDetaljer.filter { it.person.type == PersonType.SØKER && it.ytelseType == YtelseType.UTVIDET_BARNETRYGD } + + return when { + utvidetUtbetalingsdetaljerPåSøker.any { it.utbetaltPerMnd > 0 } -> SøkersRettTilUtvidet.SØKER_FÅR_UTVIDET + utvidetUtbetalingsdetaljerPåSøker.isNotEmpty() && + utvidetUtbetalingsdetaljerPåSøker.all { it.utbetaltPerMnd == 0 } -> SøkersRettTilUtvidet.SØKER_HAR_RETT_MEN_FÅR_IKKE + + else -> SøkersRettTilUtvidet.SØKER_HAR_IKKE_RETT + } +} + +enum class SøkersRettTilUtvidet { + SØKER_FÅR_UTVIDET, + SØKER_HAR_RETT_MEN_FÅR_IKKE, + SØKER_HAR_IKKE_RETT, + ; + + fun tilSanityFormat() = when (this) { + SØKER_FÅR_UTVIDET -> "sokerFaarUtvidet" + SØKER_HAR_RETT_MEN_FÅR_IKKE -> "sokerHarRettMenFaarIkke" + SØKER_HAR_IKKE_RETT -> "sokerHarIkkeRett" + } +} + +fun IVedtakBegrunnelse.hentRelevanteEndringsperioderForBegrunnelse( + minimerteRestEndredeAndeler: List, + vedtaksperiode: NullablePeriode, +) = when (this.vedtakBegrunnelseType) { + VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING -> { + minimerteRestEndredeAndeler.filter { + it.periode.tom.sisteDagIInneværendeMåned() + ?.erDagenFør(vedtaksperiode.fom?.førsteDagIInneværendeMåned()) == true + } + } + + VedtakBegrunnelseType.ENDRET_UTBETALING -> { + minimerteRestEndredeAndeler.filter { it.erOverlappendeMed(vedtaksperiode.tilNullableMånedPeriode()) } + } + + else -> emptyList() +} + +private fun BrevBegrunnelseGrunnlagMedPersoner.validerBrevbegrunnelse( + gjelderSøker: Boolean, + barnasFødselsdatoer: List, +) { + if (!gjelderSøker && barnasFødselsdatoer.isEmpty() && + !this.triggesAv.satsendring && !this.standardbegrunnelse.erAvslagUregistrerteBarnBegrunnelse() + ) { + throw IllegalStateException("Ingen personer på brevbegrunnelse") + } +} + +private fun BrevBegrunnelseGrunnlagMedPersoner.hentBeløp( + gjelderSøker: Boolean, + minimerteUtbetalingsperiodeDetaljer: List, +) = if (gjelderSøker) { + if (this.vedtakBegrunnelseType.erAvslag() || + this.vedtakBegrunnelseType.erOpphør() + ) { + 0 + } else { + minimerteUtbetalingsperiodeDetaljer.totaltUtbetalt() + } +} else { + minimerteUtbetalingsperiodeDetaljer.beløpUtbetaltFor(this.personIdenter) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksbegrunnelseFritekst.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksbegrunnelseFritekst.kt new file mode 100644 index 000000000..ce68bd8fc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksbegrunnelseFritekst.kt @@ -0,0 +1,55 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.domene + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "VedtaksbegrunnelseFritekst") +@Table(name = "VEDTAKSBEGRUNNELSE_FRITEKST") +class VedtaksbegrunnelseFritekst( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "vedtaksbegrunnelse_fritekst_seq_generator") + @SequenceGenerator( + name = "vedtaksbegrunnelse_fritekst_seq_generator", + sequenceName = "vedtaksbegrunnelse_fritekst_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @JsonIgnore + @ManyToOne + @JoinColumn(name = "fk_vedtaksperiode_id") + val vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser, + + @Column(name = "fritekst", updatable = false) + val fritekst: String, +) { + + fun kopier(vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser): VedtaksbegrunnelseFritekst = + VedtaksbegrunnelseFritekst( + vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, + fritekst = this.fritekst, + ) + + override fun toString(): String { + return "VedtaksbegrunnelseFritekst(id=$id)" + } +} + +fun tilVedtaksbegrunnelseFritekst( + vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser, + fritekst: String, +) = VedtaksbegrunnelseFritekst( + vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, + fritekst = fritekst, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksperiodeMedBegrunnelser.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksperiodeMedBegrunnelser.kt new file mode 100644 index 000000000..80b7ef178 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksperiodeMedBegrunnelser.kt @@ -0,0 +1,235 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.domene + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.common.isSameOrBefore +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.BrevPeriodeType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombiner +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.slåSammenLike +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilFørsteDagIMåneden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilSisteDagIMåneden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonthEllerUendeligFortid +import no.nav.familie.ba.sak.kjerne.tidslinje.tilTidslinje +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.EØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.UtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.utledSegmenter +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDate +import java.time.YearMonth +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode as TidslinjePeriode + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Vedtaksperiode") +@Table(name = "VEDTAKSPERIODE") +data class VedtaksperiodeMedBegrunnelser( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "vedtaksperiode_seq_generator") + @SequenceGenerator( + name = "vedtaksperiode_seq_generator", + sequenceName = "vedtaksperiode_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @JsonIgnore + @ManyToOne + @JoinColumn(name = "fk_vedtak_id") + val vedtak: Vedtak, + + @Column(name = "fom", updatable = false) + val fom: LocalDate? = null, + + @Column(name = "tom", updatable = false) + val tom: LocalDate? = null, + + @Column(name = "type", updatable = false) + @Enumerated(EnumType.STRING) + val type: Vedtaksperiodetype, + + @OneToMany( + fetch = FetchType.EAGER, + mappedBy = "vedtaksperiodeMedBegrunnelser", + cascade = [CascadeType.ALL], + orphanRemoval = true, + ) + val begrunnelser: MutableSet = mutableSetOf(), + + @OneToMany( + fetch = FetchType.EAGER, + mappedBy = "vedtaksperiodeMedBegrunnelser", + cascade = [CascadeType.ALL], + orphanRemoval = true, + ) + val eøsBegrunnelser: MutableSet = mutableSetOf(), + + // Bruker list for å bevare rekkefølgen som settes frontend. + @OneToMany( + fetch = FetchType.EAGER, + mappedBy = "vedtaksperiodeMedBegrunnelser", + cascade = [CascadeType.ALL], + orphanRemoval = true, + ) + val fritekster: MutableList = mutableListOf(), + +) : BaseEntitet() { + + override fun toString(): String { + return "VedtaksperiodeMedBegrunnelser(id=$id, fom=$fom, tom=$tom, type=$type, begrunnelser=$begrunnelser, eøsBegrunnelser=$eøsBegrunnelser, fritekster=$fritekster)" + } + + fun settBegrunnelser(nyeBegrunnelser: List) { + begrunnelser.clear() + begrunnelser.addAll(nyeBegrunnelser) + } + + fun settEØSBegrunnelser(nyeEØSBegrunnelser: List) { + eøsBegrunnelser.clear() + eøsBegrunnelser.addAll(nyeEØSBegrunnelser) + } + + fun settFritekster(nyeFritekster: List) { + fritekster.clear() + fritekster.addAll(nyeFritekster) + } + + fun harFriteksterUtenStandardbegrunnelser(): Boolean { + return (type == Vedtaksperiodetype.OPPHØR || type == Vedtaksperiodetype.AVSLAG) && fritekster.isNotEmpty() && begrunnelser.isEmpty() && eøsBegrunnelser.isEmpty() + } + + fun harFriteksterOgStandardbegrunnelser(): Boolean { + return fritekster.isNotEmpty() && begrunnelser.isNotEmpty() + } +} + +fun List.erAlleredeBegrunnetMedBegrunnelse( + standardbegrunnelser: List, + måned: YearMonth, +): Boolean { + return this.any { + it.fom?.toYearMonth() == måned && it.begrunnelser.any { standardbegrunnelse -> standardbegrunnelse.standardbegrunnelse in standardbegrunnelser } + } +} + +fun VedtaksperiodeMedBegrunnelser.hentUtbetalingsperiodeDetaljer( + andelerTilkjentYtelse: List, + personopplysningGrunnlag: PersonopplysningGrunnlag, +): List { + val utbetalingsperiodeDetaljer = andelerTilkjentYtelse.tilUtbetalingerTidslinje(personopplysningGrunnlag) + + return when (this.type) { + Vedtaksperiodetype.AVSLAG, + -> emptyList() + + Vedtaksperiodetype.FORTSATT_INNVILGET -> { + val løpendeUtbetalingsperiode = utbetalingsperiodeDetaljer.perioder() + .lastOrNull { it.fraOgMed.tilYearMonthEllerUendeligFortid() <= inneværendeMåned() } + ?: utbetalingsperiodeDetaljer.perioder().firstOrNull() + + løpendeUtbetalingsperiode?.innhold?.toList() + ?: throw Feil("Finner ikke gjeldende segment ved fortsatt innvilget") + } + + Vedtaksperiodetype.UTBETALING, + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + Vedtaksperiodetype.ENDRET_UTBETALING, + -> finnUtbetalingsperioderRelevantForVedtaksperiode(utbetalingsperiodeDetaljer)?.toList() ?: throw Feil( + "Finner ikke segment for vedtaksperiode (${this.fom}, ${this.tom}) blant segmenter ${andelerTilkjentYtelse.utledSegmenter()}", + ) + + Vedtaksperiodetype.OPPHØR -> finnUtbetalingsperioderRelevantForOpphørVedtaksperiode(utbetalingsperiodeDetaljer)?.toList() + ?: emptyList() + } +} + +private fun VedtaksperiodeMedBegrunnelser.finnUtbetalingsperioderRelevantForVedtaksperiode( + utbetalingsperiodeDetaljer: Tidslinje, Måned>, +) = utbetalingsperiodeDetaljer.perioder().find { andelerVertikal -> + andelerVertikal.fraOgMed.tilFørsteDagIMåneden().tilLocalDate() + .isSameOrBefore(this.fom ?: TIDENES_MORGEN) && + andelerVertikal.tilOgMed.tilSisteDagIMåneden().tilLocalDate() + .isSameOrAfter(this.tom ?: TIDENES_ENDE) +}?.innhold + +private fun VedtaksperiodeMedBegrunnelser.finnUtbetalingsperioderRelevantForOpphørVedtaksperiode( + utbetalingsperiodeDetaljer: Tidslinje, Måned>, +): Iterable? { + val innhold = utbetalingsperiodeDetaljer.perioder().find { andelerVertikal -> + andelerVertikal.fraOgMed.tilFørsteDagIMåneden().tilLocalDate() == this.fom + }?.innhold + + return innhold +} + +private fun List.tilUtbetalingerTidslinje( + personopplysningGrunnlag: PersonopplysningGrunnlag, +) = groupBy { Pair(it.aktør, it.type) } + .map { (_, andelerForAktørOgType) -> + andelerForAktørOgType.map { + TidslinjePeriode( + fraOgMed = it.stønadFom.tilTidspunkt(), + tilOgMed = it.stønadTom.tilTidspunkt(), + innhold = UtbetalingsperiodeDetalj( + andel = it, + personopplysningGrunnlag = personopplysningGrunnlag, + ), + ) + }.tilTidslinje() + }.kombiner { it.takeIf { it.toList().isNotEmpty() } } + .slåSammenLike() + +fun hentBrevPeriodeType( + vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser, + erUtbetalingEllerDeltBostedIPeriode: Boolean, +): BrevPeriodeType = + hentBrevPeriodeType( + vedtaksperiodetype = vedtaksperiodeMedBegrunnelser.type, + fom = vedtaksperiodeMedBegrunnelser.fom, + erUtbetalingEllerDeltBostedIPeriode = erUtbetalingEllerDeltBostedIPeriode, + ) + +fun hentBrevPeriodeType( + vedtaksperiodetype: Vedtaksperiodetype, + fom: LocalDate?, + erUtbetalingEllerDeltBostedIPeriode: Boolean, +): BrevPeriodeType = + when (vedtaksperiodetype) { + Vedtaksperiodetype.FORTSATT_INNVILGET -> BrevPeriodeType.FORTSATT_INNVILGET_NY + Vedtaksperiodetype.UTBETALING -> when { + erUtbetalingEllerDeltBostedIPeriode -> BrevPeriodeType.UTBETALING + else -> BrevPeriodeType.INGEN_UTBETALING + } + + Vedtaksperiodetype.AVSLAG -> if (fom != null) BrevPeriodeType.INGEN_UTBETALING else BrevPeriodeType.INGEN_UTBETALING_UTEN_PERIODE + Vedtaksperiodetype.OPPHØR -> BrevPeriodeType.INGEN_UTBETALING + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING -> BrevPeriodeType.UTBETALING + Vedtaksperiodetype.ENDRET_UTBETALING -> throw Feil("Endret utbetaling skal ikke benyttes lenger.") + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksperiodeRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksperiodeRepository.kt new file mode 100644 index 000000000..993803ae0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksperiodeRepository.kt @@ -0,0 +1,29 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.domene + +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query + +interface VedtaksperiodeRepository : JpaRepository { + + @Modifying + @Query("DELETE FROM Vedtaksperiode v WHERE v.vedtak = :vedtak") + fun slettVedtaksperioderFor(vedtak: Vedtak) + + @Query(value = "SELECT v FROM Vedtaksperiode v WHERE v.id = :vedtaksperiodeId") + fun hentVedtaksperiode(vedtaksperiodeId: Long): VedtaksperiodeMedBegrunnelser? + + @Query("SELECT vp FROM Vedtaksperiode vp JOIN vp.vedtak v WHERE v.id = :vedtakId") + fun finnVedtaksperioderFor(vedtakId: Long): List + + @Query( + """SELECT v.fk_behandling_id + FROM vedtaksperiode vp + JOIN vedtak v ON v.id = vp.fk_vedtak_id + WHERE vp.id = :vedtaksperiodeId + """, + nativeQuery = true, + ) + fun finnBehandlingIdForVedtaksperiode(vedtaksperiodeId: Long): Long +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValuta.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValuta.kt new file mode 100644 index 000000000..622fd68ea --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValuta.kt @@ -0,0 +1,38 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.feilutbetaltValuta + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "FeilutbetaltValuta") +@Table(name = "FEILUTBETALT_VALUTA") +data class FeilutbetaltValuta( + @Column(name = "fk_behandling_id", updatable = false, nullable = false) + val behandlingId: Long, + @Column(name = "fom", columnDefinition = "DATE") + var fom: LocalDate, + @Column(name = "tom", columnDefinition = "DATE") + var tom: LocalDate, + @Column(name = "feilutbetalt_beloep", nullable = false) + var feilutbetaltBeløp: Int, + @Column(name = "er_per_maaned") + var erPerMåned: Boolean, + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "feilutbetalt_valuta_seq_generator") + @SequenceGenerator( + name = "feilutbetalt_valuta_seq_generator", + sequenceName = "feilutbetalt_valuta_seq", + allocationSize = 50, + ) + val id: Long = 0, +) : BaseEntitet() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValutaController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValutaController.kt new file mode 100644 index 000000000..6de5dc03d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValutaController.kt @@ -0,0 +1,100 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.feilutbetaltValuta + +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestFeilutbetaltValuta +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/feilutbetalt-valuta") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class FeilutbetaltValutaController( + private val tilgangService: TilgangService, + private val feilutbetaltValutaService: FeilutbetaltValutaService, + private val utvidetBehandlingService: UtvidetBehandlingService, +) { + @PostMapping( + path = ["behandling/{behandlingId}"], + produces = [MediaType.APPLICATION_JSON_VALUE], + consumes = [MediaType.APPLICATION_JSON_VALUE], + ) + fun leggTilFeilutbetaltValutaPeriode( + @PathVariable behandlingId: Long, + @RequestBody feilutbetaltValuta: RestFeilutbetaltValuta, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "legg til periode med feilutbetalt valuta", + ) + + feilutbetaltValutaService.leggTilFeilutbetaltValutaPeriode( + feilutbetaltValuta = feilutbetaltValuta, + behandlingId = behandlingId, + ) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @PutMapping( + path = ["behandling/{behandlingId}/periode/{id}"], + produces = [MediaType.APPLICATION_JSON_VALUE], + consumes = [MediaType.APPLICATION_JSON_VALUE], + ) + fun oppdaterFeilutbetaltValutaPeriode( + @PathVariable behandlingId: Long, + @PathVariable id: Long, + @RequestBody feilutbetaltValuta: RestFeilutbetaltValuta, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "oppdater periode med feilutbetalt valuta", + ) + + feilutbetaltValutaService.oppdatertFeilutbetaltValutaPeriode(feilutbetaltValuta = feilutbetaltValuta, id = id) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @DeleteMapping(path = ["behandling/{behandlingId}/periode/{id}"]) + fun fjernFeilutbetaltValutaPeriode( + @PathVariable behandlingId: Long, + @PathVariable id: Long, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.DELETE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Fjerner periode med feilutbetalt valuta", + ) + feilutbetaltValutaService.fjernFeilutbetaltValutaPeriode(id = id, behandlingId = behandlingId) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @GetMapping(path = ["behandling/{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentFeilutbetaltValutaPerioder(@PathVariable behandlingId: Long): ResponseEntity?>> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.VEILEDER, + handling = "hente feilutbetalt valuta for behandling", + ) + return ResponseEntity.ok(Ressurs.success(feilutbetaltValutaService.hentFeilutbetaltValutaPerioder(behandlingId = behandlingId))) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValutaRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValutaRepository.kt new file mode 100644 index 000000000..9400bb3bc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValutaRepository.kt @@ -0,0 +1,12 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.feilutbetaltValuta + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface FeilutbetaltValutaRepository : JpaRepository { + @Query(value = "SELECT t FROM FeilutbetaltValuta t WHERE t.behandlingId = :behandlingId ORDER BY t.fom ASC") + fun finnFeilutbetaltValutaForBehandling(behandlingId: Long): List + + @Query(value = "SELECT f FROM FeilutbetaltValuta f WHERE f.id= :id") + fun finnFeilutbetaltValuta(id: Long): FeilutbetaltValuta? +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValutaService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValutaService.kt new file mode 100644 index 000000000..cc2c6612c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValutaService.kt @@ -0,0 +1,74 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.feilutbetaltValuta + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.config.FeatureToggleConfig.Companion.FEILUTBETALT_VALUTA_PR_MND +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.RestFeilutbetaltValuta +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class FeilutbetaltValutaService( + @Autowired + private val feilutbetaltValutaRepository: FeilutbetaltValutaRepository, + + @Autowired + private val loggService: LoggService, + + @Autowired + private val featureToggleService: FeatureToggleService, + +) { + + private fun finnFeilutbetaltValutaThrows(id: Long): FeilutbetaltValuta { + return feilutbetaltValutaRepository.finnFeilutbetaltValuta(id) ?: throw Feil("Finner ikke feilutbetalt valuta med id=$id") + } + + @Transactional + fun leggTilFeilutbetaltValutaPeriode(feilutbetaltValuta: RestFeilutbetaltValuta, behandlingId: Long): Long { + val lagret = feilutbetaltValutaRepository.save( + FeilutbetaltValuta( + behandlingId = behandlingId, + fom = feilutbetaltValuta.fom, + tom = feilutbetaltValuta.tom, + feilutbetaltBeløp = feilutbetaltValuta.feilutbetaltBeløp, + erPerMåned = feilutbetaltValuta.erPerMåned ?: featureToggleService.isEnabled(FEILUTBETALT_VALUTA_PR_MND), + ), + ) + loggService.loggFeilutbetaltValutaPeriodeLagtTil(behandlingId = behandlingId, feilutbetaltValuta = lagret) + return lagret.id + } + + @Transactional + fun fjernFeilutbetaltValutaPeriode(id: Long, behandlingId: Long) { + loggService.loggFeilutbetaltValutaPeriodeFjernet( + behandlingId = behandlingId, + feilutbetaltValuta = finnFeilutbetaltValutaThrows(id), + ) + feilutbetaltValutaRepository.deleteById(id) + } + + fun hentFeilutbetaltValutaPerioder(behandlingId: Long) = + feilutbetaltValutaRepository.finnFeilutbetaltValutaForBehandling(behandlingId = behandlingId).map { tilRest(it) } + + private fun tilRest(it: FeilutbetaltValuta) = + RestFeilutbetaltValuta( + id = it.id, + fom = it.fom, + tom = it.tom, + feilutbetaltBeløp = it.feilutbetaltBeløp, + erPerMåned = it.erPerMåned, + ) + + @Transactional + fun oppdatertFeilutbetaltValutaPeriode(feilutbetaltValuta: RestFeilutbetaltValuta, id: Long) { + val periode = feilutbetaltValutaRepository.findById(id).orElseThrow { Feil("Finner ikke feilutbetalt valuta med id=${feilutbetaltValuta.id}") } + + periode.fom = feilutbetaltValuta.fom + periode.tom = feilutbetaltValuta.tom + periode.feilutbetaltBeløp = feilutbetaltValuta.feilutbetaltBeløp + periode.erPerMåned = feilutbetaltValuta.erPerMåned ?: featureToggleService.isEnabled(FEILUTBETALT_VALUTA_PR_MND) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270s.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270s.kt" new file mode 100644 index 000000000..e96e632cb --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270s.kt" @@ -0,0 +1,40 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.refusjonEøs + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "RefusjonEos") +@Table(name = "REFUSJON_EOS") +data class RefusjonEøs( + @Column(name = "fk_behandling_id", updatable = false, nullable = false) + val behandlingId: Long, + @Column(name = "fom", columnDefinition = "DATE", nullable = false) + var fom: LocalDate, + @Column(name = "tom", columnDefinition = "DATE", nullable = false) + var tom: LocalDate, + @Column(name = "refusjonsbeloep", nullable = false) + var refusjonsbeløp: Int, + @Column(name = "land", nullable = false) + var land: String, + @Column(name = "refusjon_avklart", nullable = false) + var refusjonAvklart: Boolean, + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "refusjon_eos_seq_generator") + @SequenceGenerator( + name = "refusjon_eos_seq_generator", + sequenceName = "refusjon_eos_seq", + allocationSize = 50, + ) + val id: Long = 0, +) : BaseEntitet() diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270sController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270sController.kt" new file mode 100644 index 000000000..bc4228c12 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270sController.kt" @@ -0,0 +1,100 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.refusjonEøs + +import no.nav.familie.ba.sak.ekstern.restDomene.RestRefusjonEøs +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/refusjon-eøs") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class RefusjonEøsController( + private val tilgangService: TilgangService, + private val refusjonEøsService: RefusjonEøsService, + private val utvidetBehandlingService: UtvidetBehandlingService, +) { + @PostMapping( + path = ["behandlinger/{behandlingId}"], + produces = [MediaType.APPLICATION_JSON_VALUE], + consumes = [MediaType.APPLICATION_JSON_VALUE], + ) + fun leggTilRefusjonEøsPeriode( + @PathVariable behandlingId: Long, + @RequestBody refusjonEøs: RestRefusjonEøs, + ): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "legg til periode med refusjon EØS", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + refusjonEøsService.leggTilRefusjonEøsPeriode( + refusjonEøs = refusjonEøs, + behandlingId = behandlingId, + ) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @PutMapping( + path = ["behandlinger/{behandlingId}/perioder/{id}"], + produces = [MediaType.APPLICATION_JSON_VALUE], + consumes = [MediaType.APPLICATION_JSON_VALUE], + ) + fun oppdaterRefusjonEøsPeriode( + @PathVariable behandlingId: Long, + @PathVariable id: Long, + @RequestBody refusjonEøs: RestRefusjonEøs, + ): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "oppdater periode med refusjon EØS", + ) + + tilgangService.validerKanRedigereBehandling(behandlingId) + + refusjonEøsService.oppdaterRefusjonEøsPeriode(restRefusjonEøs = refusjonEøs, id = id) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @DeleteMapping(path = ["behandlinger/{behandlingId}/perioder/{id}"]) + fun fjernRefusjonEøsPeriode( + @PathVariable behandlingId: Long, + @PathVariable id: Long, + ): ResponseEntity> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "fjerner periode med refusjon EØS", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + refusjonEøsService.fjernRefusjonEøsPeriode(id = id, behandlingId = behandlingId) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @GetMapping(path = ["behandlinger/{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentRefusjonEøsPerioder(@PathVariable behandlingId: Long): ResponseEntity>> { + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.VEILEDER, + handling = "hente refusjon EØS for behandling", + ) + return ResponseEntity.ok(Ressurs.success(refusjonEøsService.hentRefusjonEøsPerioder(behandlingId = behandlingId))) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270sRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270sRepository.kt" new file mode 100644 index 000000000..3c3568353 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270sRepository.kt" @@ -0,0 +1,12 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.refusjonEøs + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface RefusjonEøsRepository : JpaRepository { + @Query(value = "SELECT t FROM RefusjonEos t WHERE t.behandlingId = :behandlingId ORDER BY t.fom ASC") + fun finnRefusjonEøsForBehandling(behandlingId: Long): List + + @Query(value = "SELECT f FROM RefusjonEos f WHERE f.id= :id") + fun finnRefusjonEøs(id: Long): RefusjonEøs? +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270sService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270sService.kt" new file mode 100644 index 000000000..3c87f60a3 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270sService.kt" @@ -0,0 +1,72 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.refusjonEøs + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.ekstern.restDomene.RestRefusjonEøs +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class RefusjonEøsService( + @Autowired + private val refusjonEøsRepository: RefusjonEøsRepository, + + @Autowired + private val loggService: LoggService, +) { + + private fun hentRefusjonEøs(id: Long): RefusjonEøs { + return refusjonEøsRepository.finnRefusjonEøs(id) + ?: throw Feil("Finner ikke refusjon eøs med id=$id") + } + + @Transactional + fun leggTilRefusjonEøsPeriode(refusjonEøs: RestRefusjonEøs, behandlingId: Long): Long { + val lagretPeriode = refusjonEøsRepository.save( + RefusjonEøs( + behandlingId = behandlingId, + fom = refusjonEøs.fom, + tom = refusjonEøs.tom, + refusjonsbeløp = refusjonEøs.refusjonsbeløp, + land = refusjonEøs.land, + refusjonAvklart = refusjonEøs.refusjonAvklart, + ), + ) + loggService.loggRefusjonEøsPeriodeLagtTil(refusjonEøs = lagretPeriode) + return lagretPeriode.id + } + + @Transactional + fun fjernRefusjonEøsPeriode(id: Long, behandlingId: Long) { + loggService.loggRefusjonEøsPeriodeFjernet( + refusjonEøs = hentRefusjonEøs(id), + ) + refusjonEøsRepository.deleteById(id) + } + + fun hentRefusjonEøsPerioder(behandlingId: Long) = + refusjonEøsRepository.finnRefusjonEøsForBehandling(behandlingId = behandlingId) + .map { tilRest(it) } + + private fun tilRest(it: RefusjonEøs) = + RestRefusjonEøs( + id = it.id, + fom = it.fom, + tom = it.tom, + refusjonsbeløp = it.refusjonsbeløp, + land = it.land, + refusjonAvklart = it.refusjonAvklart, + ) + + @Transactional + fun oppdaterRefusjonEøsPeriode(restRefusjonEøs: RestRefusjonEøs, id: Long) { + val refusjonEøs = hentRefusjonEøs(id) + + refusjonEøs.fom = restRefusjonEøs.fom + refusjonEøs.tom = restRefusjonEøs.tom + refusjonEøs.refusjonsbeløp = restRefusjonEøs.refusjonsbeløp + refusjonEøs.land = restRefusjonEøs.land + refusjonEøs.refusjonAvklart = restRefusjonEøs.refusjonAvklart + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Avslagsperiode.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Avslagsperiode.kt new file mode 100644 index 000000000..019a06597 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Avslagsperiode.kt @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import java.time.LocalDate + +data class Avslagsperiode( + override val periodeFom: LocalDate?, + override val periodeTom: LocalDate?, + override val vedtaksperiodetype: Vedtaksperiodetype = Vedtaksperiodetype.AVSLAG, +) : Vedtaksperiode diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/EndringsTidspunktUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/EndringsTidspunktUtil.kt new file mode 100644 index 000000000..9f88c3530 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/EndringsTidspunktUtil.kt @@ -0,0 +1,155 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.innholdForTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.outerJoin +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunktEllerUendeligSent +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonthEllerUendeligFortid +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.AktørOgRolleBegrunnelseGrunnlag +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.BehandlingsGrunnlagForVedtaksperioder +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.VedtaksperiodeGrunnlagForPerson +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.VedtaksperiodeGrunnlagForPersonVilkårInnvilget +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.erLikUtenFomOgTom +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import java.time.LocalDate + +fun utledEndringstidspunkt( + behandlingsGrunnlagForVedtaksperioder: BehandlingsGrunnlagForVedtaksperioder, + behandlingsGrunnlagForVedtaksperioderForrigeBehandling: BehandlingsGrunnlagForVedtaksperioder?, +): LocalDate { + val grunnlagTidslinjePerPerson = + behandlingsGrunnlagForVedtaksperioder.copy( + personResultater = behandlingsGrunnlagForVedtaksperioder + .personResultater.beholdKunOppfylteVilkårResultater(), + ).utledGrunnlagTidslinjePerPerson() + .mapValues { it.value.vedtaksperiodeGrunnlagForPerson } + val grunnlagTidslinjePerPersonForrigeBehandling = + behandlingsGrunnlagForVedtaksperioderForrigeBehandling?.copy( + personResultater = behandlingsGrunnlagForVedtaksperioderForrigeBehandling + .personResultater.beholdKunOppfylteVilkårResultater(), + )?.utledGrunnlagTidslinjePerPerson() + ?.mapValues { it.value.vedtaksperiodeGrunnlagForPerson } ?: emptyMap() + + val erPeriodeLikSammePeriodeIForrigeBehandlingTidslinjer = + grunnlagTidslinjePerPerson.outerJoin(grunnlagTidslinjePerPersonForrigeBehandling) { grunnlagForVedtaksperiode, grunnlagForVedtaksperiodeForrigeBehandling -> + grunnlagForVedtaksperiode.erLik(grunnlagForVedtaksperiodeForrigeBehandling) + } + + val (aktørMedFørsteEndring, datoTidligsteForskjell) = + erPeriodeLikSammePeriodeIForrigeBehandlingTidslinjer.finnTidligsteForskjell() ?: Pair(null, TIDENES_ENDE) + + loggEndringstidspunktOgEndringer( + grunnlagTidslinjePerPerson = grunnlagTidslinjePerPerson, + grunnlagTidslinjePerPersonForrigeBehandling = grunnlagTidslinjePerPersonForrigeBehandling, + aktørMedFørsteForandring = aktørMedFørsteEndring, + datoTidligsteForskjell = datoTidligsteForskjell, + ) + + return datoTidligsteForskjell +} + +private fun Set.beholdKunOppfylteVilkårResultater(): Set = map { + it.tilKopiForNyVilkårsvurdering(it.vilkårsvurdering) +}.toSet() + +private fun loggEndringstidspunktOgEndringer( + grunnlagTidslinjePerPerson: Map>, + grunnlagTidslinjePerPersonForrigeBehandling: Map>, + aktørMedFørsteForandring: AktørOgRolleBegrunnelseGrunnlag?, + datoTidligsteForskjell: LocalDate, +) { + val grunnlagDenneBehandlingen = grunnlagTidslinjePerPerson[aktørMedFørsteForandring] + val grunnlagForrigeBehandling = grunnlagTidslinjePerPersonForrigeBehandling[aktørMedFørsteForandring] + + val grunnlagIPeriodeMedEndring = grunnlagDenneBehandlingen?.innholdForTidspunkt( + datoTidligsteForskjell.toYearMonth().tilTidspunktEllerUendeligSent(), + )?.innhold + val grunnlagIPeriodeMedEndringForrigeBehanlding = grunnlagForrigeBehandling?.innholdForTidspunkt( + datoTidligsteForskjell.toYearMonth().tilTidspunktEllerUendeligSent(), + )?.innhold + + val endringer = mutableListOf() + + when (grunnlagIPeriodeMedEndring) { + is VedtaksperiodeGrunnlagForPersonVilkårInnvilget -> { + if (grunnlagIPeriodeMedEndringForrigeBehanlding is VedtaksperiodeGrunnlagForPersonVilkårInnvilget) { + if (!grunnlagIPeriodeMedEndring.vilkårResultaterForVedtaksperiode.erLikUtenFomOgTom( + grunnlagIPeriodeMedEndringForrigeBehanlding.vilkårResultaterForVedtaksperiode, + ) + ) { + endringer.add("Endring i vilkårene") + } + if (grunnlagIPeriodeMedEndring.kompetanse != grunnlagIPeriodeMedEndringForrigeBehanlding.kompetanse) { + endringer.add("Endring i kompetansen") + } + if (grunnlagIPeriodeMedEndring.endretUtbetalingAndel != grunnlagIPeriodeMedEndringForrigeBehanlding.endretUtbetalingAndel) { + endringer.add("Endring i de endrede utbetalingene") + } + if (grunnlagIPeriodeMedEndring.overgangsstønad != grunnlagIPeriodeMedEndringForrigeBehanlding.overgangsstønad) { + endringer.add("Endring i overgangsstønaden") + } + if (grunnlagIPeriodeMedEndring.andeler.toSet() != grunnlagIPeriodeMedEndringForrigeBehanlding.andeler.toSet()) { + endringer.add("Endring i andelene") + } + } else { + endringer.add("Perioden var ikke innvilget i forrige behandling") + } + } + + is VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget -> + if (grunnlagIPeriodeMedEndringForrigeBehanlding is VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget) { + if (!grunnlagIPeriodeMedEndring.vilkårResultaterForVedtaksperiode.erLikUtenFomOgTom( + grunnlagIPeriodeMedEndringForrigeBehanlding.vilkårResultaterForVedtaksperiode, + ) + ) { + endringer.add("Endring i vilkårene") + } + } else { + endringer.add("Perioden var innvilget i forrige behandling, men er det ikke nå lenger") + } + + null -> endringer.add("Det er ingen vilkår på denne behandlingen i dette tidsrommet") + } + + logger.info( + "Endringstidspunktet for behandlingen er $datoTidligsteForskjell. Se Secure logs for å se hvem endringen er for. Dette er endringene:\n" + + endringer.joinToString("\n"), + ) + secureLogger.info("Ved endringstidspunktet $datoTidligsteForskjell er det endring for $aktørMedFørsteForandring") +} + +private fun Map>.finnTidligsteForskjell() = this + .map { (aktørOgRolleForVedtaksgrunnlag, erPeriodeLikTidslinje) -> + val førsteEndringForAktør = erPeriodeLikTidslinje.perioder() + .filter { it.innhold == false } + .minOfOrNull { it.fraOgMed.tilYearMonthEllerUendeligFortid().førsteDagIInneværendeMåned() } + ?: TIDENES_ENDE + + aktørOgRolleForVedtaksgrunnlag to førsteEndringForAktør + }.minByOrNull { it.second } + +private fun VedtaksperiodeGrunnlagForPerson?.erLik( + grunnlagForVedtaksperiodeForrigeBehandling: VedtaksperiodeGrunnlagForPerson?, +): Boolean = when (this) { + is VedtaksperiodeGrunnlagForPersonVilkårInnvilget -> + grunnlagForVedtaksperiodeForrigeBehandling is VedtaksperiodeGrunnlagForPersonVilkårInnvilget && + this.vilkårResultaterForVedtaksperiode.erLikUtenFomOgTom( + grunnlagForVedtaksperiodeForrigeBehandling.vilkårResultaterForVedtaksperiode, + ) && + this.kompetanse == grunnlagForVedtaksperiodeForrigeBehandling.kompetanse && + this.endretUtbetalingAndel == grunnlagForVedtaksperiodeForrigeBehandling.endretUtbetalingAndel && + this.overgangsstønad == grunnlagForVedtaksperiodeForrigeBehandling.overgangsstønad && + this.andeler.toSet() == grunnlagForVedtaksperiodeForrigeBehandling.andeler.toSet() + + is VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget -> + grunnlagForVedtaksperiodeForrigeBehandling is VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget && + this.vilkårResultaterForVedtaksperiode.toSet() == grunnlagForVedtaksperiodeForrigeBehandling.vilkårResultaterForVedtaksperiode.toSet() + + null -> grunnlagForVedtaksperiodeForrigeBehandling == null +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Opph\303\270rsperiode.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Opph\303\270rsperiode.kt" new file mode 100644 index 000000000..a3be6a72e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Opph\303\270rsperiode.kt" @@ -0,0 +1,179 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.isSameOrBefore +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.Innhold +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.innholdForTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombiner +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.tidslinjeFraTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilDagEllerFørsteDagIPerioden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilDagEllerSisteDagIPerioden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilLocalDate +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom +import no.nav.familie.ba.sak.kjerne.tidslinje.tilTidslinje +import java.time.LocalDate + +data class Opphørsperiode( + override val periodeFom: LocalDate, + override val periodeTom: LocalDate?, + override val vedtaksperiodetype: Vedtaksperiodetype = Vedtaksperiodetype.OPPHØR, +) : Vedtaksperiode + +fun mapTilOpphørsperioder( + forrigePersonopplysningGrunnlag: PersonopplysningGrunnlag? = null, + forrigeAndelerTilkjentYtelse: List = emptyList(), + personopplysningGrunnlag: PersonopplysningGrunnlag, + andelerTilkjentYtelse: List, +): List { + val forrigeUtbetalingsperioder = if (forrigePersonopplysningGrunnlag != null) { + forrigeAndelerTilkjentYtelse.mapTilUtbetalingsperioder(forrigePersonopplysningGrunnlag) + } else { + emptyList() + } + val utbetalingsperioder = + andelerTilkjentYtelse.mapTilUtbetalingsperioder(personopplysningGrunnlag) + + return listOf( + finnOpphørsperioderPåGrunnAvReduksjonIRevurdering( + forrigeUtbetalingsperioder = forrigeUtbetalingsperioder, + utbetalingsperioder = utbetalingsperioder, + ), + finnOpphørsperioderMellomUtbetalingsperioder(utbetalingsperioder), + finnOpphørsperiodeEtterSisteUtbetalingsperiode(utbetalingsperioder), + ).flatten().sortedBy { it.periodeFom } +} + +private fun finnOpphørsperioderPåGrunnAvReduksjonIRevurdering( + forrigeUtbetalingsperioder: List, + utbetalingsperioder: List, +): List { + val erUtbetalingOpphørtTidslinje = forrigeUtbetalingsperioder + .tilTidslinje() + .kombinerMed(utbetalingsperioder.tilTidslinje()) { forrigeUtbetaling, utbetaling -> + forrigeUtbetaling != null && utbetaling == null + } + + return erUtbetalingOpphørtTidslinje.perioder() + .mapNotNull { erUtbetalingOpphørtPeriode -> + if (erUtbetalingOpphørtPeriode.innhold == true) { + Opphørsperiode( + periodeFom = erUtbetalingOpphørtPeriode.fraOgMed.tilDagEllerFørsteDagIPerioden().tilLocalDate(), + periodeTom = erUtbetalingOpphørtPeriode.tilOgMed.tilDagEllerSisteDagIPerioden().tilLocalDate(), + vedtaksperiodetype = Vedtaksperiodetype.OPPHØR, + ) + } else { + null + } + } +} + +fun List.tilKombinertTidslinjePerAktørOgType(): Tidslinje, Måned> { + val andelTilkjentYtelsePerPersonOgType = groupBy { Pair(it.aktør, it.type) } + + val andelTilkjentYtelsePerPersonOgTypeTidslinjer = + andelTilkjentYtelsePerPersonOgType.values.map { it.tilTidslinje() } + + return andelTilkjentYtelsePerPersonOgTypeTidslinjer.kombiner { it.toList() } +} + +@JvmName("AndelTilkjentYtelseMedEndreteUtbetalingerTilTidslinje") +fun List.tilTidslinje(): Tidslinje = + this.map { + Periode( + fraOgMed = it.stønadFom.tilTidspunkt(), + tilOgMed = it.stønadTom.tilTidspunkt(), + innhold = it, + ) + }.tilTidslinje() + +private fun List.tilTidslinje() = + this.map { + Periode( + fraOgMed = it.periodeFom.tilMånedTidspunkt(), + tilOgMed = it.periodeTom.tilMånedTidspunkt(), + innhold = it, + ) + }.tilTidslinje() + +private fun finnOpphørsperioderMellomUtbetalingsperioder(utbetalingsperioder: List): List = + utbetalingsperioder.tilTidslinje().tilHarVerdiTidslinje().perioder() + .filter { erUtbetalingIPeriode -> erUtbetalingIPeriode.innhold != true } + .map { + Opphørsperiode( + periodeFom = it.fraOgMed.tilLocalDate(), + periodeTom = it.tilOgMed.tilLocalDate(), + vedtaksperiodetype = Vedtaksperiodetype.OPPHØR, + ) + } + +private fun Tidslinje.tilHarVerdiTidslinje(): Tidslinje = + this.tidsrom().tidslinjeFraTidspunkt { tidspunkt -> + Innhold(this.innholdForTidspunkt(tidspunkt).innhold != null) + } + +private fun finnOpphørsperiodeEtterSisteUtbetalingsperiode(utbetalingsperioder: List): List { + val sisteUtbetalingsperiodeTom = utbetalingsperioder + .maxOfOrNull { it.periodeTom }?.toYearMonth() + ?: TIDENES_ENDE.toYearMonth() + val nesteMåned = inneværendeMåned().nesteMåned() + + return if (sisteUtbetalingsperiodeTom.isBefore(nesteMåned)) { + listOf( + Opphørsperiode( + periodeFom = sisteUtbetalingsperiodeTom.nesteMåned().førsteDagIInneværendeMåned(), + periodeTom = null, + vedtaksperiodetype = Vedtaksperiodetype.OPPHØR, + ), + ) + } else { + emptyList() + } +} + +fun slåSammenOpphørsperioder(alleOpphørsperioder: List): List { + if (alleOpphørsperioder.isEmpty()) return emptyList() + + val sortertOpphørsperioder = alleOpphørsperioder.sortedBy { it.periodeFom } + + return sortertOpphørsperioder.fold( + mutableListOf( + sortertOpphørsperioder.first(), + ), + ) { acc: MutableList, nesteOpphørsperiode: Opphørsperiode -> + val forrigeOpphørsperiode = acc.last() + when { + nesteOpphørsperiode.periodeFom.isSameOrBefore(forrigeOpphørsperiode.periodeTom ?: TIDENES_ENDE) -> { + acc[acc.lastIndex] = + forrigeOpphørsperiode.copy( + periodeTom = maxOfOpphørsperiodeTom( + forrigeOpphørsperiode.periodeTom, + nesteOpphørsperiode.periodeTom, + ), + ) + } + + else -> { + acc.add(nesteOpphørsperiode) + } + } + + acc + } +} + +private fun maxOfOpphørsperiodeTom(a: LocalDate?, b: LocalDate?): LocalDate? { + return if (a != null && b != null) maxOf(a, b) else null +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/ReduksjonsperioderFraForrigeBehandlingTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/ReduksjonsperioderFraForrigeBehandlingTidslinje.kt new file mode 100644 index 000000000..2e3505117 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/ReduksjonsperioderFraForrigeBehandlingTidslinje.kt @@ -0,0 +1,21 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligSent +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligTidlig +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser + +class ReduksjonsperioderFraForrigeBehandlingTidslinje( + private val vedtaksperioderMedBegrunnelser: List, +) : VedtaksperiodeMedBegrunnelserTidslinje(vedtaksperioderMedBegrunnelser) { + + override fun lagPerioder(): List> = + vedtaksperioderMedBegrunnelser.map { + Periode( + fraOgMed = it.fom.tilTidspunktEllerUendeligTidlig(it.tom), + tilOgMed = it.tom.tilTidspunktEllerUendeligSent(it.fom), + innhold = it.copy(fom = null, tom = null), // Gjør at perioder med samme innhold blir slått sammen + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Utbetalingsperiode.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Utbetalingsperiode.kt new file mode 100644 index 000000000..cd41df903 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Utbetalingsperiode.kt @@ -0,0 +1,123 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.ekstern.restDomene.RestPerson +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPerson +import no.nav.familie.ba.sak.kjerne.beregning.beregnUtbetalingsperioderUtenKlassifisering +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilDagEllerFørsteDagIPerioden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilDagEllerSisteDagIPerioden +import no.nav.fpsak.tidsserie.LocalDateSegment +import java.math.BigDecimal +import java.time.LocalDate + +/** + * Dataklasser som brukes til frontend og backend når man jobber med vertikale utbetalingsperioder + */ + +data class Utbetalingsperiode( + override val periodeFom: LocalDate, + override val periodeTom: LocalDate, + override val vedtaksperiodetype: Vedtaksperiodetype = Vedtaksperiodetype.UTBETALING, + val utbetalingsperiodeDetaljer: List, + val ytelseTyper: List, + val antallBarn: Int, + val utbetaltPerMnd: Int, +) : Vedtaksperiode + +data class UtbetalingsperiodeDetalj( + val person: RestPerson, + val ytelseType: YtelseType, + val utbetaltPerMnd: Int, + val erPåvirketAvEndring: Boolean, + val endringsårsak: Årsak?, + val prosent: BigDecimal, +) { + constructor( + andel: AndelTilkjentYtelseMedEndreteUtbetalinger, + personopplysningGrunnlag: PersonopplysningGrunnlag, + ) : this( + person = personopplysningGrunnlag.søkerOgBarn.find { person -> andel.aktør == person.aktør }?.tilRestPerson() + ?: throw IllegalStateException("Fant ikke personopplysningsgrunnlag for andel"), + ytelseType = andel.type, + utbetaltPerMnd = andel.kalkulertUtbetalingsbeløp, + erPåvirketAvEndring = andel.endreteUtbetalinger.isNotEmpty(), + endringsårsak = andel.endreteUtbetalinger.singleOrNull()?.årsak, + prosent = andel.prosent, + ) +} + +fun List.totaltUtbetalt(): Int = + this.sumOf { it.utbetaltPerMnd } + +fun hentUtbetalingsperiodeForVedtaksperiode( + utbetalingsperioder: List, + fom: LocalDate?, +): Utbetalingsperiode { + if (utbetalingsperioder.isEmpty()) { + throw Feil("Det finnes ingen utbetalingsperioder ved utledning av utbetalingsperiode.") + } + val fomDato = fom?.toYearMonth() ?: inneværendeMåned() + + val sorterteUtbetalingsperioder = utbetalingsperioder.sortedBy { it.periodeFom } + + return sorterteUtbetalingsperioder.lastOrNull { it.periodeFom.toYearMonth() <= fomDato } + ?: sorterteUtbetalingsperioder.firstOrNull() + ?: throw Feil("Finner ikke gjeldende utbetalingsperiode ved fortsatt innvilget") +} + +fun List.mapTilUtbetalingsperioder( + personopplysningGrunnlag: PersonopplysningGrunnlag, +): List { + val andelerTidslinjePerAktørOgType = this.tilKombinertTidslinjePerAktørOgType() + + val utbetalingsPerioder = andelerTidslinjePerAktørOgType.perioder() + .filter { !it.innhold.isNullOrEmpty() } + .map { periode -> + Utbetalingsperiode( + periodeFom = periode.fraOgMed.tilDagEllerFørsteDagIPerioden().tilLocalDate(), + periodeTom = periode.tilOgMed.tilDagEllerSisteDagIPerioden().tilLocalDate(), + ytelseTyper = periode.innhold!!.map { andelTilkjentYtelse -> andelTilkjentYtelse.type }, + utbetaltPerMnd = periode.innhold.sumOf { andelTilkjentYtelse -> andelTilkjentYtelse.kalkulertUtbetalingsbeløp }, + antallBarn = periode.innhold + .map { it.aktør }.toSet() + .count { aktør -> personopplysningGrunnlag.barna.any { barn -> barn.aktør == aktør } }, + utbetalingsperiodeDetaljer = periode.innhold.lagUtbetalingsperiodeDetaljer(personopplysningGrunnlag), + ) + } + + return utbetalingsPerioder +} + +internal fun List.utledSegmenter(): List> { + // Dersom listen er tom så returnerer vi tom liste fordi at reduceren i + // beregnUtbetalingsperioderUtenKlassifisering ikke takler tomme lister + if (this.isEmpty()) return emptyList() + + val utbetalingsPerioder = beregnUtbetalingsperioderUtenKlassifisering(this.toSet()) + return utbetalingsPerioder.toSegments() + .sortedWith(compareBy>({ it.fom }, { it.value }, { it.tom })) +} + +fun Collection.lagUtbetalingsperiodeDetaljer( + personopplysningGrunnlag: PersonopplysningGrunnlag, +): List = + this.map { andel -> + val personForAndel = + personopplysningGrunnlag.søkerOgBarn.find { person -> andel.aktør == person.aktør } + ?: throw IllegalStateException("Fant ikke personopplysningsgrunnlag for andel") + + UtbetalingsperiodeDetalj( + person = personForAndel.tilRestPerson(), + ytelseType = andel.type, + utbetaltPerMnd = andel.kalkulertUtbetalingsbeløp, + erPåvirketAvEndring = andel.endreteUtbetalinger.isNotEmpty(), + prosent = andel.prosent, + endringsårsak = andel.endreteUtbetalinger.singleOrNull()?.årsak, + ) + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeMedBegrunnelser/UtbetalingsperiodeMedBegrunnelseUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeMedBegrunnelser/UtbetalingsperiodeMedBegrunnelseUtil.kt new file mode 100644 index 000000000..070d30c7d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeMedBegrunnelser/UtbetalingsperiodeMedBegrunnelseUtil.kt @@ -0,0 +1,48 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.utbetalingsperiodemedbegrunnelser + +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.tilSeparateTidslinjerForBarna +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNull +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilFørsteDagIMåneden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilSisteDagIMåneden +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeOgUnikId +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperioderMedUnikIdTidslinje + +fun splittUtbetalingsperioderPåKompetanser( + utbetalingsperioder: List, + kompetanser: List, +): List { + if (kompetanser.isEmpty()) return utbetalingsperioder + + val kompetanseTidslinjer = kompetanser.tilSeparateTidslinjerForBarna() + + val utbetalingsTidslinje = VedtaksperioderMedUnikIdTidslinje(utbetalingsperioder) + + return kompetanseTidslinjer.values + .kombinerUtenNull { it.toList() } + .kombinerMed(utbetalingsTidslinje) { kompetanserIPeriode, vedtaksperiodeOgUnikId -> + vedtaksperiodeOgUnikId?.let { + UtbetalingsperiodeMedOverlappendeKompetanse( + vedtaksperiodeOgUnikId, + kompetanserIPeriode ?: emptyList(), + ) + } + }.lagVedtaksperioderMedBegrunnelser() +} + +data class UtbetalingsperiodeMedOverlappendeKompetanse( + val vedtaksperiodeOgUnikId: VedtaksperiodeOgUnikId, + val kompetanser: List, +) + +fun Tidslinje.lagVedtaksperioderMedBegrunnelser(): List = + this.perioder().mapNotNull { + it.innhold?.vedtaksperiodeOgUnikId?.vedtaksperiode?.copy( + fom = it.fraOgMed.tilFørsteDagIMåneden().tilLocalDateEllerNull(), + tom = it.tilOgMed.tilSisteDagIMåneden().tilLocalDateEllerNull(), + ) + } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeMedBegrunnelser/UtbetalingsperiodeUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeMedBegrunnelser/UtbetalingsperiodeUtil.kt new file mode 100644 index 000000000..ca93b89eb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeMedBegrunnelser/UtbetalingsperiodeUtil.kt @@ -0,0 +1,49 @@ +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.tilTidslinjerPerPersonOgType +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrer +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNull +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilFørsteDagIMåneden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilSisteDagIMåneden +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.tilTidslinjeForSplitt +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat + +fun hentPerioderMedUtbetaling( + andelerTilkjentYtelse: List, + vedtak: Vedtak, + personResultater: Set, + personerIPersongrunnlag: List, + fagsakType: FagsakType, +): List { + val tidslinjeForSplitt = personResultater.tilTidslinjeForSplitt(personerIPersongrunnlag, fagsakType) + + val alleAndelerKombinertTidslinje = andelerTilkjentYtelse + .tilTidslinjerPerPersonOgType().values + .kombinerUtenNull { it } + .filtrer { !it?.toList().isNullOrEmpty() } + + val andelerSplittetOppTidslinje = + alleAndelerKombinertTidslinje.kombinerMed(tidslinjeForSplitt) { andelerIPeriode, splittVilkårIPeriode -> + when (andelerIPeriode) { + null -> null + else -> Pair(andelerIPeriode, splittVilkårIPeriode) + } + }.filtrerIkkeNull() + + return andelerSplittetOppTidslinje + .perioder() + .map { + VedtaksperiodeMedBegrunnelser( + fom = it.fraOgMed.tilFørsteDagIMåneden().tilLocalDateEllerNull(), + tom = it.tilOgMed.tilSisteDagIMåneden().tilLocalDateEllerNull(), + vedtak = vedtak, + type = Vedtaksperiodetype.UTBETALING, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtakPeriodeValidering.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtakPeriodeValidering.kt new file mode 100644 index 000000000..d7b6a1daf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtakPeriodeValidering.kt @@ -0,0 +1,76 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.UtvidetVedtaksperiodeMedBegrunnelser +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +// Håpet er at denne skal kaste feil på sikt, men enn så lenge blir det for strengt. Logger for å se behovet. +fun List.validerPerioderInneholderBegrunnelser( + behandlingId: Long, + fagsakId: Long, +) { + this.forEach { + it.validerMinstEnBegrunnelseValgt(behandlingId = behandlingId, fagsakId = fagsakId) + it.validerMinstEnReduksjonsbegrunnelseVedReduksjon(behandlingId = behandlingId, fagsakId = fagsakId) + it.validerMinstEnInnvilgetbegrunnelseVedInnvilgelse(behandlingId = behandlingId, fagsakId = fagsakId) + it.validerMinstEnEndretUtbetalingbegrunnelseVedEndretUtbetaling( + behandlingId = behandlingId, + fagsakId = fagsakId, + ) + } +} + +private fun UtvidetVedtaksperiodeMedBegrunnelser.validerMinstEnEndretUtbetalingbegrunnelseVedEndretUtbetaling( + behandlingId: Long, + fagsakId: Long, +) { + val erMuligÅVelgeEndretUtbetalingBegrunnelse = + this.gyldigeBegrunnelser.any { it.vedtakBegrunnelseType == VedtakBegrunnelseType.ENDRET_UTBETALING } + val erValgtEndretUtbetalingBegrunnelse = + this.begrunnelser.any { it.standardbegrunnelse.vedtakBegrunnelseType == VedtakBegrunnelseType.ENDRET_UTBETALING } + + if (erMuligÅVelgeEndretUtbetalingBegrunnelse && !erValgtEndretUtbetalingBegrunnelse) { + logger.warn("Vedtaksperioden ${this.fom?.tilKortString() ?: ""} - ${this.tom?.tilKortString() ?: ""} mangler endretubetalingsbegrunnelse. Fagsak: $fagsakId, behandling: $behandlingId") + } +} + +private fun UtvidetVedtaksperiodeMedBegrunnelser.validerMinstEnInnvilgetbegrunnelseVedInnvilgelse( + behandlingId: Long, + fagsakId: Long, +) { + val erMuligÅVelgeInnvilgetBegrunnelse = + this.gyldigeBegrunnelser.any { it.vedtakBegrunnelseType.erInnvilget() } + val erValgtInnvilgetBegrunnelse = + this.begrunnelser.any { it.standardbegrunnelse.vedtakBegrunnelseType.erInnvilget() } + + if (erMuligÅVelgeInnvilgetBegrunnelse && !erValgtInnvilgetBegrunnelse) { + logger.warn("Vedtaksperioden ${this.fom?.tilKortString() ?: ""} - ${this.tom?.tilKortString() ?: ""} mangler innvilgelsebegrunnelse. Fagsak: $fagsakId, behandling: $behandlingId") + } +} + +private fun UtvidetVedtaksperiodeMedBegrunnelser.validerMinstEnReduksjonsbegrunnelseVedReduksjon( + behandlingId: Long, + fagsakId: Long, +) { + val erMuligÅVelgeReduksjonBegrunnelse = + this.gyldigeBegrunnelser.any { it.vedtakBegrunnelseType.erReduksjon() } + val erValgtReduksjonBegrunnelse = + this.begrunnelser.any { it.standardbegrunnelse.vedtakBegrunnelseType.erReduksjon() } + + if (erMuligÅVelgeReduksjonBegrunnelse && !erValgtReduksjonBegrunnelse) { + logger.warn("Vedtaksperioden ${this.fom?.tilKortString() ?: ""} - ${this.tom?.tilKortString() ?: ""} mangler reduksjonsbegrunnelse. Fagsak: $fagsakId, behandling: $behandlingId") + } +} + +private fun UtvidetVedtaksperiodeMedBegrunnelser.validerMinstEnBegrunnelseValgt( + behandlingId: Long, + fagsakId: Long, +) { + if (this.begrunnelser.isEmpty()) { + logger.warn("Vedtaksperioden ${this.fom?.tilKortString() ?: ""} - ${this.tom?.tilKortString() ?: ""} har ingen begrunnelser knyttet til seg. Fagsak: $fagsakId, behandling: $behandlingId") + } +} + +val logger: Logger = LoggerFactory.getLogger("validerPerioderInneholderBegrunnelserLogger") diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Vedtaksperiode.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Vedtaksperiode.kt new file mode 100644 index 000000000..22c5b6e90 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Vedtaksperiode.kt @@ -0,0 +1,101 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import java.time.LocalDate + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "vedtaksperiodetype") +@JsonSubTypes( + JsonSubTypes.Type(value = Utbetalingsperiode::class, name = "UTBETALING"), + JsonSubTypes.Type(value = Avslagsperiode::class, name = "AVSLAG"), + JsonSubTypes.Type(value = Opphørsperiode::class, name = "OPPHØR"), +) +interface Vedtaksperiode { + + val periodeFom: LocalDate? + val periodeTom: LocalDate? + val vedtaksperiodetype: Vedtaksperiodetype +} + +enum class Vedtaksperiodetype(val tillatteBegrunnelsestyper: Set) { + UTBETALING( + setOf( + VedtakBegrunnelseType.INNVILGET, + VedtakBegrunnelseType.REDUKSJON, + VedtakBegrunnelseType.FORTSATT_INNVILGET, + VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING, + VedtakBegrunnelseType.ENDRET_UTBETALING, + VedtakBegrunnelseType.INSTITUSJON_INNVILGET, + VedtakBegrunnelseType.INSTITUSJON_REDUKSJON, + VedtakBegrunnelseType.INSTITUSJON_FORTSATT_INNVILGET, + VedtakBegrunnelseType.EØS_INNVILGET, + VedtakBegrunnelseType.EØS_REDUKSJON, + VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET, + ), + ), + + /*** + * Brukes i de tilfellene det er en reduksjon på tvers av behandlinger som vi ikke kan begrunne på vanlig måte. + * + * For eksempel: I en behandling har vi to barn med utbetaling fra mai 2020 til januar 2021. + * I neste behandling endres det ene barne til å ha utbetaling fra juni 2020 til januar 2021. + * Da har det vært en reduksjon fra den første behandlingen til den neste i mai 2020, + * og det blir en utbetaling med reduksjon fra sist iverksatte behandling. + * + * Om det ene barnet hadde mistet juli isteden for mai, altså at det fikk utbetalt 1. mai 2020 til 1. juni 2021 og + * fra juli 2020 til januar 2021, ville juni 2020 vært en vanlig utbetalingsperiode fordi vi kan begrunne + * reduksjonen uten å se på forrige behandling. + ***/ + UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING( + setOf( + VedtakBegrunnelseType.REDUKSJON, + VedtakBegrunnelseType.INNVILGET, + VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING, + VedtakBegrunnelseType.ENDRET_UTBETALING, + VedtakBegrunnelseType.INSTITUSJON_REDUKSJON, + VedtakBegrunnelseType.INSTITUSJON_INNVILGET, + VedtakBegrunnelseType.EØS_INNVILGET, + VedtakBegrunnelseType.EØS_REDUKSJON, + ), + ), + OPPHØR( + setOf( + VedtakBegrunnelseType.OPPHØR, + VedtakBegrunnelseType.ENDRET_UTBETALING, + VedtakBegrunnelseType.ETTER_ENDRET_UTBETALING, + VedtakBegrunnelseType.INSTITUSJON_OPPHØR, + VedtakBegrunnelseType.EØS_OPPHØR, + VedtakBegrunnelseType.AVSLAG, + ), + ), + AVSLAG( + setOf( + VedtakBegrunnelseType.AVSLAG, + VedtakBegrunnelseType.EØS_AVSLAG, + VedtakBegrunnelseType.INSTITUSJON_AVSLAG, + ), + ), + FORTSATT_INNVILGET( + setOf( + VedtakBegrunnelseType.FORTSATT_INNVILGET, + VedtakBegrunnelseType.INSTITUSJON_FORTSATT_INNVILGET, + VedtakBegrunnelseType.EØS_FORTSATT_INNVILGET, + ), + ), + + @Deprecated("Legacy. Kan ikke fjernes uten at det ryddes opp i Vedtaksperioder-tabellen") + ENDRET_UTBETALING(emptySet()), + ; + + fun sorteringsRekkefølge(): Int { + return when (this) { + UTBETALING -> 1 + UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING -> 2 + FORTSATT_INNVILGET -> 3 + OPPHØR -> 4 + AVSLAG -> 5 + ENDRET_UTBETALING -> 6 + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeHentOgPersisterService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeHentOgPersisterService.kt new file mode 100644 index 000000000..3fe3cbb27 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeHentOgPersisterService.kt @@ -0,0 +1,39 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeRepository +import org.springframework.stereotype.Service + +@Service +class VedtaksperiodeHentOgPersisterService( + + private val vedtaksperiodeRepository: VedtaksperiodeRepository, +) { + + fun hentVedtaksperiodeThrows(vedtaksperiodeId: Long): VedtaksperiodeMedBegrunnelser = + vedtaksperiodeRepository.hentVedtaksperiode(vedtaksperiodeId) + ?: throw Feil( + message = "Fant ingen vedtaksperiode med id $vedtaksperiodeId", + frontendFeilmelding = "Fant ikke vedtaksperiode", + ) + + fun lagre(vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser) = + lagre(listOf(vedtaksperiodeMedBegrunnelser)).first() + + fun lagre(vedtaksperiodeMedBegrunnelser: List): List { + vedtaksperiodeMedBegrunnelser.forEach { validerVedtaksperiodeMedBegrunnelser(it) } + return vedtaksperiodeRepository.saveAll(vedtaksperiodeMedBegrunnelser) + } + + fun slettVedtaksperioderFor(vedtak: Vedtak) { + vedtaksperiodeRepository.slettVedtaksperioderFor(vedtak) + } + + fun finnVedtaksperioderFor(vedtakId: Long): List = + vedtaksperiodeRepository.finnVedtaksperioderFor(vedtakId) + + fun finnBehandlingIdFor(vedtaksperiodeId: Long): Long = + vedtaksperiodeRepository.finnBehandlingIdForVedtaksperiode(vedtaksperiodeId) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelserController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelserController.kt new file mode 100644 index 000000000..a4e88bba6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelserController.kt @@ -0,0 +1,162 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestGenererVedtaksperioderForOverstyrtEndringstidspunkt +import no.nav.familie.ba.sak.ekstern.restDomene.RestPutVedtaksperiodeMedFritekster +import no.nav.familie.ba.sak.ekstern.restDomene.RestPutVedtaksperiodeMedStandardbegrunnelser +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.brev.BrevKlient +import no.nav.familie.ba.sak.kjerne.brev.BrevPeriodeService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BegrunnelseData +import no.nav.familie.ba.sak.kjerne.vedtak.domene.EØSBegrunnelseData +import no.nav.familie.ba.sak.kjerne.vedtak.domene.FritekstBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.RestUtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/vedtaksperioder") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class VedtaksperiodeMedBegrunnelserController( + private val vedtaksperiodeService: VedtaksperiodeService, + private val tilgangService: TilgangService, + private val brevKlient: BrevKlient, + private val utvidetBehandlingService: UtvidetBehandlingService, + private val brevPeriodeService: BrevPeriodeService, + private val vedtaksperiodeHentOgPersisterService: VedtaksperiodeHentOgPersisterService, +) { + + @PutMapping("/standardbegrunnelser/{vedtaksperiodeId}") + fun oppdaterVedtaksperiodeStandardbegrunnelser( + @PathVariable + vedtaksperiodeId: Long, + @RequestBody + restPutVedtaksperiodeMedStandardbegrunnelser: RestPutVedtaksperiodeMedStandardbegrunnelser, + ): ResponseEntity>> { + val behandlingId = vedtaksperiodeHentOgPersisterService.finnBehandlingIdFor(vedtaksperiodeId) + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = OPPDATERE_BEGRUNNELSER_HANDLING, + ) + + val standardbegrunnelser = restPutVedtaksperiodeMedStandardbegrunnelser.standardbegrunnelser.map { + IVedtakBegrunnelse.konverterTilEnumVerdi(it) + } + + val nasjonalebegrunnelser = standardbegrunnelser.filterIsInstance() + val eøsStandardbegrunnelser = standardbegrunnelser.filterIsInstance() + + vedtaksperiodeService.oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = vedtaksperiodeId, + standardbegrunnelserFraFrontend = nasjonalebegrunnelser, + eøsStandardbegrunnelserFraFrontend = eøsStandardbegrunnelser, + ) + + return ResponseEntity.ok( + Ressurs.success( + vedtaksperiodeService.hentRestUtvidetVedtaksperiodeMedBegrunnelser( + behandlingId, + ), + ), + ) + } + + @PutMapping("/fritekster/{vedtaksperiodeId}") + fun oppdaterVedtaksperiodeMedFritekster( + @PathVariable + vedtaksperiodeId: Long, + @RequestBody + restPutVedtaksperiodeMedFritekster: RestPutVedtaksperiodeMedFritekster, + ): ResponseEntity>> { + val behandlingId = vedtaksperiodeHentOgPersisterService.finnBehandlingIdFor(vedtaksperiodeId) + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = OPPDATERE_BEGRUNNELSER_HANDLING, + ) + + vedtaksperiodeService.oppdaterVedtaksperiodeMedFritekster( + vedtaksperiodeId, + restPutVedtaksperiodeMedFritekster, + ) + + return ResponseEntity.ok( + Ressurs.success( + vedtaksperiodeService.hentRestUtvidetVedtaksperiodeMedBegrunnelser( + behandlingId, + ), + ), + ) + } + + @PutMapping("/endringstidspunkt") + fun genererVedtaksperioderTilOgMedFørsteEndringstidspunkt( + @RequestBody restGenererVedtaksperioder: RestGenererVedtaksperioderForOverstyrtEndringstidspunkt, + ): ResponseEntity> { + val behandlingId = restGenererVedtaksperioder.behandlingId + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Oppdaterer vedtaksperiode med endringstidspunkt", + ) + + vedtaksperiodeService.oppdaterEndringstidspunktOgGenererVedtaksperioderPåNytt(restGenererVedtaksperioder) + return ResponseEntity.ok( + Ressurs.success( + utvidetBehandlingService + .lagRestUtvidetBehandling(behandlingId = behandlingId), + ), + ) + } + + @GetMapping("/brevbegrunnelser/{vedtaksperiodeId}") + fun genererBrevBegrunnelserForPeriode(@PathVariable vedtaksperiodeId: Long): ResponseEntity>> { + val behandlingId = vedtaksperiodeHentOgPersisterService.finnBehandlingIdFor(vedtaksperiodeId) + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.ACCESS) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.VEILEDER, + handling = "hente genererte begrunnelser", + ) + + val begrunnelser = brevPeriodeService.genererBrevBegrunnelserForPeriode(vedtaksperiodeId).map { + when (it) { + is FritekstBegrunnelse -> it.fritekst + is BegrunnelseData -> brevKlient.hentBegrunnelsestekst(it) + is EØSBegrunnelseData -> brevKlient.hentBegrunnelsestekst(it) + else -> throw Feil("Ukjent begrunnelsestype") + } + } + + return ResponseEntity.ok(Ressurs.Companion.success(begrunnelser.toSet())) + } + + @GetMapping(path = ["/behandling/{behandlingId}/hent-vedtaksperioder"]) + fun hentRestUtvidetVedtaksperiodeMedBegrunnelser( + @PathVariable behandlingId: Long, + ): ResponseEntity>> = ResponseEntity.ok( + Ressurs.success( + vedtaksperiodeService.hentRestUtvidetVedtaksperiodeMedBegrunnelser(behandlingId), + ), + ) + + companion object { + const val OPPDATERE_BEGRUNNELSER_HANDLING = "oppdatere vedtaksperiode med begrunnelser" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelserTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelserTidslinje.kt new file mode 100644 index 000000000..db37da3de --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelserTidslinje.kt @@ -0,0 +1,32 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligSent +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligTidlig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilFørsteDagIMåneden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilSisteDagIMåneden +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser + +open class VedtaksperiodeMedBegrunnelserTidslinje( + private val vedtaksperioderMedBegrunnelser: List, +) : Tidslinje() { + + override fun lagPerioder(): List> = + vedtaksperioderMedBegrunnelser.map { + Periode( + fraOgMed = it.fom.tilTidspunktEllerUendeligTidlig(it.tom), + tilOgMed = it.tom.tilTidspunktEllerUendeligSent(it.fom), + innhold = it, + ) + } +} + +fun Tidslinje.lagVedtaksperioderMedBegrunnelser(): List = + this.perioder().mapNotNull { + it.innhold?.copy( + fom = it.fraOgMed.tilFørsteDagIMåneden().tilLocalDateEllerNull(), + tom = it.tilOgMed.tilSisteDagIMåneden().tilLocalDateEllerNull(), + ) + } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelserTidslinjeM\303\245ned.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelserTidslinjeM\303\245ned.kt" new file mode 100644 index 000000000..d49a5e348 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelserTidslinjeM\303\245ned.kt" @@ -0,0 +1,30 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligSent +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligTidlig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilInneværendeMåned +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import org.apache.kafka.common.Uuid + +// Ønsker ikke å slå sammen like perioder, så legger ved en unikId for å unngå det. +class VedtaksperioderMedUnikIdTidslinje( + private val vedtaksperioderMedBegrunnelser: List, +) : Tidslinje() { + + override fun lagPerioder(): List> = + vedtaksperioderMedBegrunnelser.map { + Periode( + fraOgMed = it.fom.tilTidspunktEllerUendeligTidlig(it.tom).tilInneværendeMåned(), + tilOgMed = it.tom.tilTidspunktEllerUendeligSent(it.fom).tilInneværendeMåned(), + innhold = VedtaksperiodeOgUnikId(vedtaksperiode = it, uuid = Uuid.randomUuid()), + ) + } +} + +data class VedtaksperiodeOgUnikId( + val vedtaksperiode: VedtaksperiodeMedBegrunnelser, + val uuid: Uuid, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeService.kt new file mode 100644 index 000000000..2fe05b764 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeService.kt @@ -0,0 +1,636 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.BehandlingValidering.validerBehandlingIkkeErAvsluttet +import no.nav.familie.ba.sak.common.BehandlingValidering.validerBehandlingKanRedigeres +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.Utils.storForbokstav +import no.nav.familie.ba.sak.common.erSenereEnnInneværendeMåned +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.common.tilDagMånedÅr +import no.nav.familie.ba.sak.common.tilMånedÅr +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.RestGenererVedtaksperioderForOverstyrtEndringstidspunkt +import no.nav.familie.ba.sak.ekstern.restDomene.RestPutVedtaksperiodeMedFritekster +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.sanity.SanityService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.beregning.SmåbarnstilleggService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.brev.brevBegrunnelseProdusent.GrunnlagForBegrunnelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndelRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform.NB +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform.NN +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.søknad.SøknadGrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.EØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.tilVedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.Vedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.domene.hentUtbetalingsperiodeDetaljer +import no.nav.familie.ba.sak.kjerne.vedtak.domene.tilVedtaksbegrunnelseFritekst +import no.nav.familie.ba.sak.kjerne.vedtak.feilutbetaltValuta.FeilutbetaltValutaRepository +import no.nav.familie.ba.sak.kjerne.vedtak.refusjonEøs.RefusjonEøsRepository +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.RestUtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.UtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.sorter +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.tilRestUtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.tilUtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.tilVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent.hentGyldigeBegrunnelserForPeriode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.BehandlingsGrunnlagForVedtaksperioder +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.genererVedtaksperioder +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.YearMonth + +@Service +class VedtaksperiodeService( + private val personidentService: PersonidentService, + private val persongrunnlagService: PersongrunnlagService, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + private val vedtaksperiodeHentOgPersisterService: VedtaksperiodeHentOgPersisterService, + private val vedtakRepository: VedtakRepository, + private val sanityService: SanityService, + private val søknadGrunnlagService: SøknadGrunnlagService, + private val endretUtbetalingAndelRepository: EndretUtbetalingAndelRepository, + private val kompetanseRepository: PeriodeOgBarnSkjemaRepository, + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + private val featureToggleService: FeatureToggleService, + private val feilutbetaltValutaRepository: FeilutbetaltValutaRepository, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val vilkårsvurderingService: VilkårsvurderingService, + private val småbarnstilleggService: SmåbarnstilleggService, + private val refusjonEøsRepository: RefusjonEøsRepository, + private val integrasjonClient: IntegrasjonClient, +) { + fun oppdaterVedtaksperiodeMedFritekster( + vedtaksperiodeId: Long, + restPutVedtaksperiodeMedFritekster: RestPutVedtaksperiodeMedFritekster, + ): Vedtak { + val vedtaksperiodeMedBegrunnelser = + vedtaksperiodeHentOgPersisterService.hentVedtaksperiodeThrows(vedtaksperiodeId) + val behandling = vedtaksperiodeMedBegrunnelser.vedtak.behandling + validerBehandlingKanRedigeres(behandling) + + vedtaksperiodeMedBegrunnelser.settFritekster( + restPutVedtaksperiodeMedFritekster.fritekster.map { + tilVedtaksbegrunnelseFritekst( + vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, + fritekst = it, + ) + }, + ) + + vedtaksperiodeHentOgPersisterService.lagre(vedtaksperiodeMedBegrunnelser) + + return vedtaksperiodeMedBegrunnelser.vedtak + } + + fun finnEndringstidspunktForBehandling(behandlingId: Long): LocalDate { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + + val forrigeBehandling = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = behandling.fagsak.id) + + return utledEndringstidspunkt( + behandlingsGrunnlagForVedtaksperioder = behandling.hentGrunnlagForVedtaksperioder(), + behandlingsGrunnlagForVedtaksperioderForrigeBehandling = forrigeBehandling?.hentGrunnlagForVedtaksperioder(), + ) + } + + fun oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId: Long, + standardbegrunnelserFraFrontend: List, + eøsStandardbegrunnelserFraFrontend: List = emptyList(), + ): Vedtak { + val vedtaksperiodeMedBegrunnelser = + vedtaksperiodeHentOgPersisterService.hentVedtaksperiodeThrows(vedtaksperiodeId) + + validerAvslagHarAvslagBegrunnelse( + vedtaksperiodeMedBegrunnelser, + standardbegrunnelserFraFrontend, + eøsStandardbegrunnelserFraFrontend, + ) + + val behandling = vedtaksperiodeMedBegrunnelser.vedtak.behandling + validerBehandlingKanRedigeres(behandling) + + val persongrunnlag = persongrunnlagService.hentAktivThrows(behandlingId = behandling.id) + + val sanityBegrunnelser = sanityService.hentSanityBegrunnelser() + + vedtaksperiodeMedBegrunnelser.settBegrunnelser( + standardbegrunnelserFraFrontend.mapNotNull { + val triggesAv = sanityBegrunnelser[it]?.triggesAv + ?: return@mapNotNull null + + if (triggesAv.satsendring) { + validerSatsendring( + fom = vedtaksperiodeMedBegrunnelser.fom, + harBarnMedSeksårsdagPåFom = persongrunnlag.harBarnMedSeksårsdagPåFom( + vedtaksperiodeMedBegrunnelser.fom, + ), + ) + } + + it.tilVedtaksbegrunnelse(vedtaksperiodeMedBegrunnelser) + }, + ) + + vedtaksperiodeMedBegrunnelser.settEØSBegrunnelser( + eøsStandardbegrunnelserFraFrontend.map { + EØSBegrunnelse( + vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, + begrunnelse = it, + ) + }, + ) + + if ( + standardbegrunnelserFraFrontend.any { it.vedtakBegrunnelseType == VedtakBegrunnelseType.ENDRET_UTBETALING } + ) { + val andelerTilkjentYtelse = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnAndelerTilkjentYtelseMedEndreteUtbetalinger(behandling.id) + + validerEndretUtbetalingsbegrunnelse(vedtaksperiodeMedBegrunnelser, andelerTilkjentYtelse, persongrunnlag) + } + + vedtaksperiodeHentOgPersisterService.lagre(vedtaksperiodeMedBegrunnelser) + + return vedtaksperiodeMedBegrunnelser.vedtak + } + + private fun validerAvslagHarAvslagBegrunnelse( + vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser, + standardbegrunnelserFraFrontend: List, + eøsStandardbegrunnelserFraFrontend: List, + ) { + val eksisterendeAvslagBegrunnelser = + vedtaksperiodeMedBegrunnelser.begrunnelser.filter { it.standardbegrunnelse.vedtakBegrunnelseType.erAvslag() } + .map { it.standardbegrunnelse.sanityApiNavn } + + val nyeAvslagBegrunnelser = + (standardbegrunnelserFraFrontend.filter { it.vedtakBegrunnelseType.erAvslag() } + eøsStandardbegrunnelserFraFrontend.filter { it.vedtakBegrunnelseType.erAvslag() }).map { it.sanityApiNavn } + + if (!nyeAvslagBegrunnelser.containsAll(eksisterendeAvslagBegrunnelser)) { + throw FunksjonellFeil("Kan ikke fjerne avslags-begrunnelse fra vedtaksperiode som har blitt satt til avslag i vilkårsvurdering.") + } + } + + private fun validerEndretUtbetalingsbegrunnelse( + vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser, + andelerTilkjentYtelse: List, + persongrunnlag: PersonopplysningGrunnlag, + ) { + try { + vedtaksperiodeMedBegrunnelser.hentUtbetalingsperiodeDetaljer( + andelerTilkjentYtelse = andelerTilkjentYtelse, + personopplysningGrunnlag = persongrunnlag, + ) + } catch (e: Exception) { + throw FunksjonellFeil( + "Begrunnelse for endret utbetaling er ikke gyldig for vedtaksperioden", + ) + } + } + + fun oppdaterVedtaksperioderForBarnVurdertIFødselshendelse(vedtak: Vedtak, barnaSomVurderes: List) { + validerBehandlingIkkeErAvsluttet(vedtak.behandling) + val barnaAktørSomVurderes = personidentService.hentAktørIder(barnaSomVurderes) + + val vedtaksperioderMedBegrunnelser = + vedtaksperiodeHentOgPersisterService.finnVedtaksperioderFor(vedtakId = vedtak.id) + val persongrunnlag = persongrunnlagService.hentAktivThrows(behandlingId = vedtak.behandling.id) + val vurderteBarnSomPersoner = + barnaAktørSomVurderes.map { barnAktørSomVurderes -> + persongrunnlag.barna.find { it.aktør == barnAktørSomVurderes } + ?: error("Finner ikke barn som har blitt vurdert i persongrunnlaget") + } + + vurderteBarnSomPersoner.map { it.fødselsdato.toYearMonth() }.toSet().forEach { fødselsmåned -> + val vedtaksperiodeMedBegrunnelser = vedtaksperioderMedBegrunnelser.firstOrNull { + fødselsmåned.plusMonths(1).equals(it.fom?.toYearMonth() ?: TIDENES_ENDE) + } + + if (vedtaksperiodeMedBegrunnelser == null) { + val vilkårsvurdering = + vilkårsvurderingService.hentAktivForBehandling(behandlingId = vedtak.behandling.id) + secureLogger.info( + vilkårsvurdering?.personResultater?.joinToString("\n") { + "Fødselsnummer: ${it.aktør.aktivFødselsnummer()}. Resultater: ${it.vilkårResultater}" + }, + ) + throw Feil("Finner ikke vedtaksperiode å begrunne for barn fra hendelse") + } + + vedtaksperiodeMedBegrunnelser.settBegrunnelser( + listOf( + Vedtaksbegrunnelse( + standardbegrunnelse = if (vedtak.behandling.fagsak.status == FagsakStatus.LØPENDE) { + Standardbegrunnelse.INNVILGET_FØDSELSHENDELSE_NYFØDT_BARN + } else { + Standardbegrunnelse.INNVILGET_FØDSELSHENDELSE_NYFØDT_BARN_FØRSTE + }, + vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, + ), + ), + ) + vedtaksperiodeHentOgPersisterService.lagre(vedtaksperiodeMedBegrunnelser) + + /** + * Hvis barn(a) er født før desember påvirkes vedtaket av satsendring januar 2022 + * og vi må derfor også automatisk begrunne satsendringen + */ + if (fødselsmåned < YearMonth.of( + 2021, + 12, + ) + ) { + vedtaksperioderMedBegrunnelser.firstOrNull { it.fom?.toYearMonth() == YearMonth.of(2022, 1) } + ?.also { satsendringsvedtaksperiode -> + satsendringsvedtaksperiode.settBegrunnelser( + listOf( + Vedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_SATSENDRING, + vedtaksperiodeMedBegrunnelser = satsendringsvedtaksperiode, + ), + ), + ) + vedtaksperiodeHentOgPersisterService.lagre(satsendringsvedtaksperiode) + } + } + } + } + + @Transactional + fun oppdaterVedtakMedVedtaksperioder(vedtak: Vedtak) { + vedtaksperiodeHentOgPersisterService.slettVedtaksperioderFor(vedtak) + + val vedtaksperioderForBehandling = finnVedtaksperioderForBehandling(vedtak) + vedtaksperiodeHentOgPersisterService.lagre(vedtaksperioderForBehandling) + } + + fun finnVedtaksperioderForBehandling(behandlingId: Long): List = + finnVedtaksperioderForBehandling(vedtakRepository.findByBehandlingAndAktiv(behandlingId)) + + fun finnVedtaksperioderForBehandling(vedtak: Vedtak): List { + val behandling = vedtak.behandling + val forrigeBehandling = behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling) + + return genererVedtaksperioder( + grunnlagForVedtakPerioder = behandling.hentGrunnlagForVedtaksperioder(), + grunnlagForVedtakPerioderForrigeBehandling = forrigeBehandling?.hentGrunnlagForVedtaksperioder(), + vedtak = vedtak, + nåDato = LocalDate.now(), + ) + } + + fun Behandling.hentGrunnlagForVedtaksperioder(): BehandlingsGrunnlagForVedtaksperioder = + BehandlingsGrunnlagForVedtaksperioder( + persongrunnlag = persongrunnlagService.hentAktivThrows(this.id), + personResultater = vilkårsvurderingService.hentAktivForBehandling(this.id)?.personResultater ?: emptySet(), + fagsakType = fagsak.type, + kompetanser = kompetanseRepository.finnFraBehandlingId(this.id).toList(), + endredeUtbetalinger = endretUtbetalingAndelRepository.findByBehandlingId(this.id), + andelerTilkjentYtelse = andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(this.id), + perioderOvergangsstønad = småbarnstilleggService.hentPerioderMedFullOvergangsstønad(this), + uregistrerteBarn = søknadGrunnlagService.hentAktiv(behandlingId = this.id)?.hentUregistrerteBarn() + ?: emptyList(), + ) + + @Transactional + fun oppdaterEndringstidspunktOgGenererVedtaksperioderPåNytt(restGenererVedtaksperioder: RestGenererVedtaksperioderForOverstyrtEndringstidspunkt) { + val vedtak = vedtakRepository.findByBehandlingAndAktiv(restGenererVedtaksperioder.behandlingId) + + validerBehandlingKanRedigeres(vedtak.behandling) + + lagreNedOverstyrtEndringstidspunkt( + behandlingId = vedtak.behandling.id, + overstyrtEndringstidspunkt = restGenererVedtaksperioder.overstyrtEndringstidspunkt, + ) + oppdaterVedtakMedVedtaksperioder(vedtak) + } + + private fun lagreNedOverstyrtEndringstidspunkt(behandlingId: Long, overstyrtEndringstidspunkt: LocalDate) { + val behandling = behandlingHentOgPersisterService.hent(behandlingId = behandlingId) + behandling.overstyrtEndringstidspunkt = overstyrtEndringstidspunkt + behandlingHentOgPersisterService.lagreEllerOppdater(behandling = behandling, sendTilDvh = false) + } + + fun kopierOverVedtaksperioder(deaktivertVedtak: Vedtak, aktivtVedtak: Vedtak) { + val gamleVedtaksperioderMedBegrunnelser = + vedtaksperiodeHentOgPersisterService.finnVedtaksperioderFor(vedtakId = deaktivertVedtak.id) + + gamleVedtaksperioderMedBegrunnelser.forEach { vedtaksperiodeMedBegrunnelser -> + val nyVedtaksperiodeMedBegrunnelser = vedtaksperiodeHentOgPersisterService.lagre( + VedtaksperiodeMedBegrunnelser( + vedtak = aktivtVedtak, + fom = vedtaksperiodeMedBegrunnelser.fom, + tom = vedtaksperiodeMedBegrunnelser.tom, + type = vedtaksperiodeMedBegrunnelser.type, + ), + ) + + nyVedtaksperiodeMedBegrunnelser.settBegrunnelser( + vedtaksperiodeMedBegrunnelser.begrunnelser.map { + it.kopier(nyVedtaksperiodeMedBegrunnelser) + }, + ) + nyVedtaksperiodeMedBegrunnelser.settEØSBegrunnelser( + vedtaksperiodeMedBegrunnelser.eøsBegrunnelser.map { + it.kopier(nyVedtaksperiodeMedBegrunnelser) + }, + ) + nyVedtaksperiodeMedBegrunnelser.settFritekster( + vedtaksperiodeMedBegrunnelser.fritekster.map { + it.kopier(nyVedtaksperiodeMedBegrunnelser) + }, + ) + + vedtaksperiodeHentOgPersisterService.lagre(nyVedtaksperiodeMedBegrunnelser) + } + } + + fun hentPersisterteVedtaksperioder(vedtak: Vedtak): List { + return vedtaksperiodeHentOgPersisterService.finnVedtaksperioderFor(vedtakId = vedtak.id) + } + + fun hentRestUtvidetVedtaksperiodeMedBegrunnelser(behandlingId: Long): List { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + + val vedtaksperioder = if (behandling.status != BehandlingStatus.AVSLUTTET) { + val utvidetVedtaksperiodeMedBegrunnelser = hentUtvidetVedtaksperiodeMedBegrunnelser( + vedtak = vedtakRepository.findByBehandlingAndAktiv(behandlingId), + personopplysningGrunnlag = persongrunnlagService.hentAktivThrows(behandlingId), + ) + utvidetVedtaksperiodeMedBegrunnelser + .sorter() + .map { it.tilRestUtvidetVedtaksperiodeMedBegrunnelser() } + } else { + emptyList() + } + + val skalMinimeres = behandling.status != BehandlingStatus.UTREDES + + return if (skalMinimeres) { + vedtaksperioder + .filter { it.begrunnelser.isNotEmpty() } + .map { it.copy(gyldigeBegrunnelser = emptyList()) } + } else { + vedtaksperioder + } + } + + fun hentUtvidetVedtaksperiodeMedBegrunnelser( + vedtak: Vedtak, + personopplysningGrunnlag: PersonopplysningGrunnlag? = null, + ): List { + val persongrunnlag = personopplysningGrunnlag ?: persongrunnlagService.hentAktivThrows(vedtak.behandling.id) + val vedtaksperioderMedBegrunnelser = hentPersisterteVedtaksperioder(vedtak) + + val behandling = vedtak.behandling + + val andelerTilkjentYtelse = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnAndelerTilkjentYtelseMedEndreteUtbetalinger(behandling.id) + + val utvidetVedtaksperioderMedBegrunnelser = vedtaksperioderMedBegrunnelser.map { + it.tilUtvidetVedtaksperiodeMedBegrunnelser( + andelerTilkjentYtelse = andelerTilkjentYtelse, + personopplysningGrunnlag = persongrunnlag, + ) + } + + val skalSendeMedGyldigeBegrunnelser = + behandling.status == BehandlingStatus.UTREDES && utvidetVedtaksperioderMedBegrunnelser.isNotEmpty() + + return if (skalSendeMedGyldigeBegrunnelser) { + hentUtvidetVedtaksperioderMedBegrunnelserOgGyldigeBegrunnelser( + behandling = behandling, + utvidedeVedtaksperioderMedBegrunnelser = utvidetVedtaksperioderMedBegrunnelser, + vedtak = vedtak, + ) + } else { + utvidetVedtaksperioderMedBegrunnelser + } + } + + private fun hentUtvidetVedtaksperioderMedBegrunnelserOgGyldigeBegrunnelser( + behandling: Behandling, + utvidedeVedtaksperioderMedBegrunnelser: List, + vedtak: Vedtak, + ): List { + val grunnlagForBegrunnelser = hentGrunnlagForBegrunnelse(behandling) + + return utvidedeVedtaksperioderMedBegrunnelser.map { utvidetVedtaksperiodeMedBegrunnelser -> + utvidetVedtaksperiodeMedBegrunnelser.copy( + gyldigeBegrunnelser = + utvidetVedtaksperiodeMedBegrunnelser.tilVedtaksperiodeMedBegrunnelser(vedtak) + .hentGyldigeBegrunnelserForPeriode(grunnlagForBegrunnelser).toList(), + ) + } + } + + fun hentGrunnlagForBegrunnelse(behandling: Behandling): GrunnlagForBegrunnelse { + val forrigeBehandling = behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling) + + val behandlingsGrunnlagForVedtaksperioder = behandling.hentGrunnlagForVedtaksperioder() + val behandlingsGrunnlagForVedtaksperioderForrigeBehandling = forrigeBehandling?.hentGrunnlagForVedtaksperioder() + + val sanityBegrunnelser = sanityService.hentSanityBegrunnelser() + val sanityEØSBegrunnelser = sanityService.hentSanityEØSBegrunnelser() + + return GrunnlagForBegrunnelse( + behandlingsGrunnlagForVedtaksperioder = behandlingsGrunnlagForVedtaksperioder, + behandlingsGrunnlagForVedtaksperioderForrigeBehandling = behandlingsGrunnlagForVedtaksperioderForrigeBehandling, + sanityBegrunnelser = sanityBegrunnelser, + sanityEØSBegrunnelser = sanityEØSBegrunnelser, + nåDato = LocalDate.now(), + ) + } + + fun oppdaterFortsattInnvilgetPeriodeMedAutobrevBegrunnelse( + vedtak: Vedtak, + standardbegrunnelse: Standardbegrunnelse, + ) { + val vedtaksperioder = hentPersisterteVedtaksperioder(vedtak) + + val fortsattInnvilgetPeriode: VedtaksperiodeMedBegrunnelser = + vedtaksperioder.singleOrNull() + ?: throw Feil("Finner ingen eller flere vedtaksperioder ved fortsatt innvilget") + + fortsattInnvilgetPeriode.settBegrunnelser( + listOf( + Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = fortsattInnvilgetPeriode, + standardbegrunnelse = standardbegrunnelse, + ), + ), + ) + + vedtaksperiodeHentOgPersisterService.lagre(fortsattInnvilgetPeriode) + } + + fun hentUtbetalingsperioder( + behandling: Behandling, + ): List { + val personopplysningGrunnlag = persongrunnlagService.hentAktiv(behandlingId = behandling.id) + return hentUtbetalingsperioder(behandling, personopplysningGrunnlag) + } + + fun hentUtbetalingsperioder( + behandling: Behandling, + personopplysningGrunnlag: PersonopplysningGrunnlag?, + ): List { + if (personopplysningGrunnlag == null) return emptyList() + + val andelerTilkjentYtelse = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnAndelerTilkjentYtelseMedEndreteUtbetalinger(behandling.id) + + return andelerTilkjentYtelse.mapTilUtbetalingsperioder(personopplysningGrunnlag = personopplysningGrunnlag) + } + + fun hentOpphørsperioder( + behandling: Behandling, + endringstidspunkt: LocalDate = TIDENES_MORGEN, + ): List { + if (behandling.resultat == Behandlingsresultat.FORTSATT_INNVILGET) return emptyList() + + val sisteVedtattBehandling: Behandling? = + behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = behandling.fagsak.id) + + val forrigePersonopplysningGrunnlag: PersonopplysningGrunnlag? = + if (sisteVedtattBehandling != null) { + persongrunnlagService.hentAktiv(behandlingId = sisteVedtattBehandling.id) + } else { + null + } + val forrigeAndelerMedEndringer = if (sisteVedtattBehandling != null) { + andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnAndelerTilkjentYtelseMedEndreteUtbetalinger(sisteVedtattBehandling.id) + } else { + emptyList() + } + + val personopplysningGrunnlag = + persongrunnlagService.hentAktiv(behandlingId = behandling.id) + ?: return emptyList() + + val andelerTilkjentYtelse = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnAndelerTilkjentYtelseMedEndreteUtbetalinger(behandling.id) + + val alleOpphørsperioder = mapTilOpphørsperioder( + forrigePersonopplysningGrunnlag = forrigePersonopplysningGrunnlag, + forrigeAndelerTilkjentYtelse = forrigeAndelerMedEndringer, + personopplysningGrunnlag = personopplysningGrunnlag, + andelerTilkjentYtelse = andelerTilkjentYtelse, + ) + + val (perioderFørEndringstidspunkt, fraEndringstidspunktOgUtover) = + alleOpphørsperioder.partition { it.periodeFom.isBefore(endringstidspunkt) } + + return perioderFørEndringstidspunkt + slåSammenOpphørsperioder(fraEndringstidspunktOgUtover) + } + + fun skalHaÅrligKontroll(vedtak: Vedtak): Boolean { + if (!featureToggleService.isEnabled(FeatureToggleConfig.EØS_INFORMASJON_OM_ÅRLIG_KONTROLL, false)) { + return false + } + return vedtak.behandling.kategori == BehandlingKategori.EØS && + hentPersisterteVedtaksperioder(vedtak).any { it.tom?.erSenereEnnInneværendeMåned() != false } + } + + fun skalMeldeFraOmEndringerEøsSelvstendigRett(vedtak: Vedtak): Boolean { + val vilkårsvurdering = + vilkårsvurderingService.hentAktivForBehandling(behandlingId = vedtak.behandling.id) + + val annenForelderOmfattetAvNorskLovgivningErSattPåBosattIRiket = ( + vilkårsvurdering?.personResultater?.flatMap { it.vilkårResultater } + ?.any { it.utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.ANNEN_FORELDER_OMFATTET_AV_NORSK_LOVGIVNING) && it.vilkårType == Vilkår.BOSATT_I_RIKET } + ?: false + ) + + val passendeBehandlingsresultat = vedtak.behandling.resultat !in listOf( + Behandlingsresultat.AVSLÅTT, + Behandlingsresultat.ENDRET_OG_OPPHØRT, + Behandlingsresultat.OPPHØRT, + ) + + val eøsPraksisEndringFeatureToggleErSlåttPå = + featureToggleService.isEnabled(FeatureToggleConfig.EØS_PRAKSISENDRING_SEPTEMBER2023) + + return annenForelderOmfattetAvNorskLovgivningErSattPåBosattIRiket && passendeBehandlingsresultat && eøsPraksisEndringFeatureToggleErSlåttPå + } + + fun beskrivPerioderMedFeilutbetaltValuta(vedtak: Vedtak): Set? { + val målform = persongrunnlagService.hentAktiv(behandlingId = vedtak.behandling.id)?.søker?.målform + val fra = mapOf(NB to "Fra", NN to "Frå").getOrDefault(målform, "Fra") + val mye = mapOf(NB to "mye", NN to "mykje").getOrDefault(målform, "mye") + + return feilutbetaltValutaRepository.finnFeilutbetaltValutaForBehandling(vedtak.behandling.id).map { + if (it.erPerMåned) { + val måned = mapOf(NB to "måned", NN to "månad").getOrDefault(målform, "måned") + val (fom, tom) = it.fom.tilMånedÅr() to it.tom.tilMånedÅr() + "$fra $fom til $tom er det utbetalt ${it.feilutbetaltBeløp} kroner for $mye per $måned." + } else { + val (fom, tom) = it.fom.tilDagMånedÅr() to it.tom.tilDagMånedÅr() + "$fra $fom til $tom er det utbetalt ${it.feilutbetaltBeløp} kroner for $mye." + } + }.toSet().takeIf { it.isNotEmpty() } + } + + fun beskrivPerioderMedRefusjonEøs(behandling: Behandling, avklart: Boolean): Set? { + val målform = persongrunnlagService.hentAktiv(behandlingId = behandling.id)?.søker?.målform + val landkoderISO2 = integrasjonClient.hentLandkoderISO2() + + return refusjonEøsRepository.finnRefusjonEøsForBehandling(behandling.id) + .filter { it.refusjonAvklart == avklart }.map { + val (fom, tom) = it.fom.tilMånedÅr() to it.tom.tilMånedÅr() + val land = + landkoderISO2[it.land]?.storForbokstav() ?: throw Feil("Fant ikke navn for landkode ${it.land}") + val beløp = it.refusjonsbeløp + + when (målform) { + NN -> { + if (avklart) { + "Frå $fom til $tom blir etterbetaling på $beløp kroner per måned utbetalt til myndighetene i $land." + } else { + "Frå $fom til $tom blir ikkje etterbetaling på $beløp kroner per månad utbetalt no sidan det er utbetalt barnetrygd i $land." + } + } + + else -> { + if (avklart) { + "Fra $fom til $tom blir etterbetaling på $beløp kroner per måned utbetalt til myndighetene i $land." + } else { + "Fra $fom til $tom blir ikke etterbetaling på $beløp kroner per måned utbetalt nå siden det er utbetalt barnetrygd i $land." + } + } + } + }.toSet().takeIf { it.isNotEmpty() } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeUtil.kt new file mode 100644 index 000000000..86f552a3a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeUtil.kt @@ -0,0 +1,310 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.erDagenFør +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.beregning.SatsService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.beregning.domene.lagVertikaleSegmenter +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertEndretAndel +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertRestPersonResultat +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.MinimertPerson +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.MinimertVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.endretUtbetalingsperiodeBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.triggesForPeriode +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.domene.hentUtbetalingsperiodeDetaljer +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.UtvidetVedtaksperiodeMedBegrunnelser +import no.nav.fpsak.tidsserie.LocalDateSegment +import java.time.LocalDate + +fun oppdaterUtbetalingsperioderMedReduksjonFraForrigeBehandling( + utbetalingsperioder: List, + reduksjonsperioder: List, +): List { + if (reduksjonsperioder.isNotEmpty()) { + val utbetalingsperioderTidslinje = VedtaksperiodeMedBegrunnelserTidslinje(utbetalingsperioder) + val reduksjonsperioderTidslinje = ReduksjonsperioderFraForrigeBehandlingTidslinje(reduksjonsperioder) + + val kombinertTidslinje = utbetalingsperioderTidslinje.kombinerMed( + reduksjonsperioderTidslinje, + ) { utbetalingsperiode, reduksjonsperiode -> + when { + reduksjonsperiode != null && utbetalingsperiode == null -> reduksjonsperiode + reduksjonsperiode != null && utbetalingsperiode != null -> utbetalingsperiode.copy(type = reduksjonsperiode.type) + else -> utbetalingsperiode + } + } + return kombinertTidslinje.lagVedtaksperioderMedBegrunnelser() + } + return utbetalingsperioder +} + +fun validerSatsendring(fom: LocalDate?, harBarnMedSeksårsdagPåFom: Boolean) { + val satsendring = SatsService.finnSatsendring(fom ?: TIDENES_MORGEN) + + if (satsendring.isEmpty() && !harBarnMedSeksårsdagPåFom) { + throw FunksjonellFeil( + melding = "Begrunnelsen stemmer ikke med satsendring.", + frontendFeilmelding = "Begrunnelsen stemmer ikke med satsendring. Vennligst velg en annen begrunnelse.", + ) + } +} + +fun validerVedtaksperiodeMedBegrunnelser(vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser) { + if ((vedtaksperiodeMedBegrunnelser.type == Vedtaksperiodetype.OPPHØR || vedtaksperiodeMedBegrunnelser.type == Vedtaksperiodetype.AVSLAG) && vedtaksperiodeMedBegrunnelser.harFriteksterUtenStandardbegrunnelser()) { + val fritekstUtenStandardbegrunnelserFeilmelding = + "Fritekst kan kun brukes i kombinasjon med en eller flere begrunnelser. " + "Legg først til en ny begrunnelse eller fjern friteksten(e)." + throw FunksjonellFeil( + melding = fritekstUtenStandardbegrunnelserFeilmelding, + frontendFeilmelding = fritekstUtenStandardbegrunnelserFeilmelding, + ) + } + + if (vedtaksperiodeMedBegrunnelser.vedtak.behandling.resultat == Behandlingsresultat.FORTSATT_INNVILGET && vedtaksperiodeMedBegrunnelser.harFriteksterOgStandardbegrunnelser()) { + throw FunksjonellFeil( + "Det ble sendt med både fritekst og begrunnelse. " + "Vedtaket skal enten ha fritekst eller bregrunnelse, men ikke begge deler.", + ) + } +} + +/** + * Brukes for opphør som har egen logikk dersom det er første periode. + */ +fun erFørsteVedtaksperiodePåFagsak( + andelerTilkjentYtelse: List, + periodeFom: LocalDate?, +): Boolean = !andelerTilkjentYtelse.any { + it.stønadFom.isBefore( + periodeFom?.toYearMonth() ?: TIDENES_MORGEN.toYearMonth(), + ) +} + +fun identifiserReduksjonsperioderFraSistIverksatteBehandling( + forrigeAndelerTilkjentYtelse: List, + andelerTilkjentYtelse: List, + vedtak: Vedtak, + utbetalingsperioder: List, + personopplysningGrunnlag: PersonopplysningGrunnlag, + opphørsperioder: List, + aktørerIForrigePersonopplysningGrunnlag: List, +): List { + val forrigeSegmenter = forrigeAndelerTilkjentYtelse.lagVertikaleSegmenter() + + // henter segmenter for personer som finnes i forrige behandling + val nåværendeSegmenter = andelerTilkjentYtelse.filter { + aktørerIForrigePersonopplysningGrunnlag.any { forrigeAktør -> forrigeAktør == it.aktør } + }.lagVertikaleSegmenter() + + val segmenter = forrigeSegmenter.filterNot { (forrigeSegment, _) -> + nåværendeSegmenter.any { (nyttSegment, _) -> + forrigeSegment.fom == nyttSegment.fom && forrigeSegment.tom == nyttSegment.tom && forrigeSegment.value == nyttSegment.value + } + } + val reduksjonsperioderFraInnvilgelsesTidspunkt = segmenter.filter { (forrigeSegment, _) -> + nåværendeSegmenter.any { (nyttSegment, _) -> + nyttSegment.overlapper( + forrigeSegment, + ) + } + }.toList().fold(emptyList()) { acc, (gammeltSegment, gammeltAndelerTyForSegment) -> + val overlappendePerioder = nåværendeSegmenter.filter { (nåSegment, nåAndelTilkjentYtelserForSegment) -> + nåSegment.overlapper(gammeltSegment) && gammeltAndelerTyForSegment.any { gammelAndelTyForSegment -> + val fom = nåSegment.fom + nåAndelTilkjentYtelserForSegment.all { nåAndelTyForSegment -> + // Når en person mister utbetaling på et segment i behandling + !(nåAndelTyForSegment.aktør.aktørId == gammelAndelTyForSegment.aktør.aktørId && nåAndelTyForSegment.type == gammelAndelTyForSegment.type) && + // Når den personen som mister utbetaling ikke har en utbetaling av samme type i forrige måned + utbetalingsperioder.none { utbetalingsperiode -> + utbetalingsperiode.tom == fom.minusDays(1) && utbetalingsperiode.hentUtbetalingsperiodeDetaljer( + andelerTilkjentYtelse = andelerTilkjentYtelse, + personopplysningGrunnlag = personopplysningGrunnlag, + ).any { + it.person.personIdent == gammelAndelTyForSegment.aktør.aktivFødselsnummer() && it.ytelseType == gammelAndelTyForSegment.type + } + } + } + } + }.keys + + acc + overlappendePerioder.map { overlappendePeriode -> + VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = utledFom(gammeltSegment, overlappendePeriode), + tom = utledTom(gammeltSegment, overlappendePeriode), + type = Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + ) + } + } + // opphørsperioder kan ikke være inkludert i reduksjonsperioder + return reduksjonsperioderFraInnvilgelsesTidspunkt.filterNot { reduksjonsperiode -> + opphørsperioder.any { it.fom == reduksjonsperiode.fom || it.tom == reduksjonsperiode.tom } + } +} + +private fun utledFom( + gammeltSegment: LocalDateSegment, + overlappendePeriode: LocalDateSegment, +) = if (gammeltSegment.fom > overlappendePeriode.fom) gammeltSegment.fom else overlappendePeriode.fom + +private fun utledTom( + gammeltSegment: LocalDateSegment, + overlappendePeriode: LocalDateSegment, +) = if (gammeltSegment.tom > overlappendePeriode.tom) overlappendePeriode.tom else gammeltSegment.tom + +fun hentGyldigeBegrunnelserForVedtaksperiodeMinimert( + minimertVedtaksperiode: MinimertVedtaksperiode, + sanityBegrunnelser: Map, + minimertePersoner: List, + minimertePersonresultater: List, + aktørIderMedUtbetaling: List, + minimerteEndredeUtbetalingAndeler: List, + erFørsteVedtaksperiodePåFagsak: Boolean, + ytelserForSøkerForrigeMåned: List, + ytelserForrigePerioder: List, +): List { + val tillateBegrunnelserForVedtakstype = Standardbegrunnelse.entries + .filter { + minimertVedtaksperiode.type.tillatteBegrunnelsestyper.contains(it.vedtakBegrunnelseType) + }.filter { + if (it.vedtakBegrunnelseType == VedtakBegrunnelseType.ENDRET_UTBETALING) { + endretUtbetalingsperiodeBegrunnelser.contains(it) + } else { + true + } + } + + return when (minimertVedtaksperiode.type) { + Vedtaksperiodetype.FORTSATT_INNVILGET, + Vedtaksperiodetype.AVSLAG, + -> tillateBegrunnelserForVedtakstype + + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING -> velgRedusertBegrunnelser( + tillateBegrunnelserForVedtakstype, + sanityBegrunnelser, + minimertVedtaksperiode, + minimertePersonresultater, + minimertePersoner, + aktørIderMedUtbetaling, + minimerteEndredeUtbetalingAndeler, + erFørsteVedtaksperiodePåFagsak, + ytelserForSøkerForrigeMåned, + ytelserForrigePerioder, + ) + + else -> { + velgUtbetalingsbegrunnelser( + tillateBegrunnelserForVedtakstype, + sanityBegrunnelser, + minimertVedtaksperiode, + minimertePersonresultater, + minimertePersoner, + aktørIderMedUtbetaling, + minimerteEndredeUtbetalingAndeler, + erFørsteVedtaksperiodePåFagsak, + ytelserForSøkerForrigeMåned, + ytelserForrigePerioder, + ) + } + } +} + +private fun velgRedusertBegrunnelser( + tillateBegrunnelserForVedtakstype: List, + sanityBegrunnelser: Map, + minimertVedtaksperiode: MinimertVedtaksperiode, + minimertePersonresultater: List, + minimertePersoner: List, + aktørIderMedUtbetaling: List, + minimerteEndredeUtbetalingAndeler: List, + erFørsteVedtaksperiodePåFagsak: Boolean, + ytelserForSøkerForrigeMåned: List, + ytelserForrigePeriode: List, +): List { + val redusertBegrunnelser = tillateBegrunnelserForVedtakstype.filter { + sanityBegrunnelser[it]?.triggesAv?.gjelderFraInnvilgelsestidspunkt ?: false + } + if (minimertVedtaksperiode.utbetalingsperioder.any { it.utbetaltPerMnd > 0 }) { + val utbetalingsbegrunnelser = velgUtbetalingsbegrunnelser( + Standardbegrunnelse.entries, + sanityBegrunnelser, + minimertVedtaksperiode, + minimertePersonresultater, + minimertePersoner, + aktørIderMedUtbetaling, + minimerteEndredeUtbetalingAndeler, + erFørsteVedtaksperiodePåFagsak, + ytelserForSøkerForrigeMåned, + ytelserForrigePeriode, + ) + return redusertBegrunnelser + utbetalingsbegrunnelser + } + return redusertBegrunnelser +} + +private fun velgUtbetalingsbegrunnelser( + tillateBegrunnelserForVedtakstype: List, + sanityBegrunnelser: Map, + minimertVedtaksperiode: MinimertVedtaksperiode, + minimertePersonresultater: List, + minimertePersoner: List, + aktørIderMedUtbetaling: List, + minimerteEndredeUtbetalingAndeler: List, + erFørsteVedtaksperiodePåFagsak: Boolean, + ytelserForSøkerForrigeMåned: List, + ytelserForrigePeriode: List, +): List { + val standardbegrunnelser: MutableSet = + tillateBegrunnelserForVedtakstype.filter { !it.vedtakBegrunnelseType.erFortsattInnvilget() } + .filter { sanityBegrunnelser[it]?.triggesAv?.valgbar ?: false } + .fold(mutableSetOf()) { acc, standardBegrunnelse -> + if (standardBegrunnelse.triggesForPeriode( + minimertVedtaksperiode = minimertVedtaksperiode, + minimertePersonResultater = minimertePersonresultater, + minimertePersoner = minimertePersoner, + aktørIderMedUtbetaling = aktørIderMedUtbetaling, + minimerteEndredeUtbetalingAndeler = minimerteEndredeUtbetalingAndeler, + sanityBegrunnelser = sanityBegrunnelser, + erFørsteVedtaksperiodePåFagsak = erFørsteVedtaksperiodePåFagsak, + ytelserForSøkerForrigeMåned = ytelserForSøkerForrigeMåned, + ytelserForrigePeriode = ytelserForrigePeriode, + ) + ) { + acc.add(standardBegrunnelse) + } + + acc + } + + val fantIngenbegrunnelserOgSkalDerforBrukeFortsattInnvilget = + minimertVedtaksperiode.type == Vedtaksperiodetype.UTBETALING && standardbegrunnelser.isEmpty() + + return if (fantIngenbegrunnelserOgSkalDerforBrukeFortsattInnvilget) { + tillateBegrunnelserForVedtakstype.filter { it.vedtakBegrunnelseType.erFortsattInnvilget() } + } else { + standardbegrunnelser.toList() + } +} + +fun hentYtelserForSøkerForrigeMåned( + andelerTilkjentYtelse: List, + utvidetVedtaksperiodeMedBegrunnelser: UtvidetVedtaksperiodeMedBegrunnelser, +) = andelerTilkjentYtelse.filter { + it.type.erKnyttetTilSøker() && ytelseErFraForrigePeriode(it, utvidetVedtaksperiodeMedBegrunnelser) +}.map { it.type } + +fun ytelseErFraForrigePeriode( + ytelse: AndelTilkjentYtelseMedEndreteUtbetalinger, + utvidetVedtaksperiodeMedBegrunnelser: UtvidetVedtaksperiodeMedBegrunnelser, +) = ytelse.stønadTom.sisteDagIInneværendeMåned().erDagenFør(utvidetVedtaksperiodeMedBegrunnelser.fom) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/domene/RestUtvidetVedtaksperiodeMedBegrunnelser.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/domene/RestUtvidetVedtaksperiodeMedBegrunnelser.kt new file mode 100644 index 000000000..0017dd44f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/domene/RestUtvidetVedtaksperiodeMedBegrunnelser.kt @@ -0,0 +1,30 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene + +import no.nav.familie.ba.sak.kjerne.vedtak.domene.tilRestVedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.UtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import java.time.LocalDate + +data class RestUtvidetVedtaksperiodeMedBegrunnelser( + val id: Long, + val fom: LocalDate?, + val tom: LocalDate?, + val type: Vedtaksperiodetype, + val begrunnelser: List, + val fritekster: List = emptyList(), + val gyldigeBegrunnelser: List, + val utbetalingsperiodeDetaljer: List = emptyList(), +) + +fun UtvidetVedtaksperiodeMedBegrunnelser.tilRestUtvidetVedtaksperiodeMedBegrunnelser(): RestUtvidetVedtaksperiodeMedBegrunnelser { + return RestUtvidetVedtaksperiodeMedBegrunnelser( + id = this.id, + fom = this.fom, + tom = this.tom, + type = this.type, + begrunnelser = this.begrunnelser.map { it.tilRestVedtaksbegrunnelse() } + this.eøsBegrunnelser.map { it.tilRestVedtaksbegrunnelse() }, + fritekster = this.fritekster, + utbetalingsperiodeDetaljer = this.utbetalingsperiodeDetaljer, + gyldigeBegrunnelser = this.gyldigeBegrunnelser.map { it.enumnavnTilString() }, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/domene/RestVedtaksbegrunnelse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/domene/RestVedtaksbegrunnelse.kt new file mode 100644 index 000000000..30405b473 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/domene/RestVedtaksbegrunnelse.kt @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene + +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType + +data class RestVedtaksbegrunnelse( + val standardbegrunnelse: String, + val vedtakBegrunnelseSpesifikasjon: String, + val vedtakBegrunnelseType: VedtakBegrunnelseType, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/domene/UtvidetVedtaksperiodeMedBegrunnelser.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/domene/UtvidetVedtaksperiodeMedBegrunnelser.kt new file mode 100644 index 000000000..b131c3406 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/domene/UtvidetVedtaksperiodeMedBegrunnelser.kt @@ -0,0 +1,75 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene + +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.EØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.Vedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksbegrunnelseFritekst +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.domene.hentUtbetalingsperiodeDetaljer +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.UtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import java.time.LocalDate + +data class UtvidetVedtaksperiodeMedBegrunnelser( + val id: Long, + val fom: LocalDate?, + val tom: LocalDate?, + val type: Vedtaksperiodetype, + val begrunnelser: List, + val eøsBegrunnelser: List, + val fritekster: List = emptyList(), + val gyldigeBegrunnelser: List = emptyList(), + val utbetalingsperiodeDetaljer: List = emptyList(), +) + +fun List.sorter(): List { + val (perioderMedFom, perioderUtenFom) = this.partition { it.fom != null } + return perioderMedFom.sortedWith(compareBy({ it.fom }, { it.type.sorteringsRekkefølge() })) + perioderUtenFom +} + +fun VedtaksperiodeMedBegrunnelser.tilUtvidetVedtaksperiodeMedBegrunnelser( + personopplysningGrunnlag: PersonopplysningGrunnlag, + andelerTilkjentYtelse: List, +): UtvidetVedtaksperiodeMedBegrunnelser { + val utbetalingsperiodeDetaljer = this.hentUtbetalingsperiodeDetaljer( + andelerTilkjentYtelse = andelerTilkjentYtelse, + personopplysningGrunnlag = personopplysningGrunnlag, + ) + + return UtvidetVedtaksperiodeMedBegrunnelser( + id = this.id, + fom = this.fom, + tom = this.tom, + type = this.type, + begrunnelser = this.begrunnelser.toList(), + eøsBegrunnelser = this.eøsBegrunnelser.toList(), + fritekster = this.fritekster.sortedBy { it.id }.map { it.fritekst }, + utbetalingsperiodeDetaljer = utbetalingsperiodeDetaljer, + ) +} + +fun UtvidetVedtaksperiodeMedBegrunnelser.tilVedtaksperiodeMedBegrunnelser( + vedtak: Vedtak, +): VedtaksperiodeMedBegrunnelser { + return VedtaksperiodeMedBegrunnelser( + id = this.id, + fom = this.fom, + tom = this.tom, + type = this.type, + begrunnelser = this.begrunnelser.toMutableSet(), + eøsBegrunnelser = this.eøsBegrunnelser.toMutableSet(), + vedtak = vedtak, + ).also { vedtaksperiode -> + vedtaksperiode.fritekster.addAll( + this.fritekster.map { + VedtaksbegrunnelseFritekst( + fritekst = it, + vedtaksperiodeMedBegrunnelser = vedtaksperiode, + ) + }.toMutableList(), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/BegrunnelseGrunnlagForPeriode.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/BegrunnelseGrunnlagForPeriode.kt new file mode 100644 index 000000000..50002c5eb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/BegrunnelseGrunnlagForPeriode.kt @@ -0,0 +1,63 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent + +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype + +sealed interface IBegrunnelseGrunnlagForPeriode { + val dennePerioden: BegrunnelseGrunnlagForPersonIPeriode + val forrigePeriode: BegrunnelseGrunnlagForPersonIPeriode? + val erSmåbarnstilleggIForrigeBehandlingPeriode: Boolean + + companion object { + fun opprett( + dennePerioden: BegrunnelseGrunnlagForPersonIPeriode, + forrigePeriode: BegrunnelseGrunnlagForPersonIPeriode?, + sammePeriodeForrigeBehandling: BegrunnelseGrunnlagForPersonIPeriode?, + periodetype: Vedtaksperiodetype, + + ): IBegrunnelseGrunnlagForPeriode = + when (periodetype) { + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING -> { + BegrunnelseGrunnlagForPeriodeMedReduksjonPåTversAvBehandlinger( + dennePerioden = dennePerioden, + forrigePeriode = forrigePeriode, + sammePeriodeForrigeBehandling = sammePeriodeForrigeBehandling, + ) + } + Vedtaksperiodetype.OPPHØR -> { + BegrunnelseGrunnlagForPeriodeMedOpphør( + dennePerioden = dennePerioden, + forrigePeriode = forrigePeriode, + sammePeriodeForrigeBehandling = sammePeriodeForrigeBehandling, + ) + } + else -> { + BegrunnelseGrunnlagForPeriode(dennePerioden, forrigePeriode, sammePeriodeForrigeBehandling?.andeler?.any { it.type == YtelseType.SMÅBARNSTILLEGG } == true) + } + } + } +} + +data class BegrunnelseGrunnlagForPeriode( + override val dennePerioden: BegrunnelseGrunnlagForPersonIPeriode, + override val forrigePeriode: BegrunnelseGrunnlagForPersonIPeriode?, + override val erSmåbarnstilleggIForrigeBehandlingPeriode: Boolean, +) : IBegrunnelseGrunnlagForPeriode + +data class BegrunnelseGrunnlagForPeriodeMedReduksjonPåTversAvBehandlinger( + override val dennePerioden: BegrunnelseGrunnlagForPersonIPeriode, + override val forrigePeriode: BegrunnelseGrunnlagForPersonIPeriode?, + val sammePeriodeForrigeBehandling: BegrunnelseGrunnlagForPersonIPeriode?, +) : IBegrunnelseGrunnlagForPeriode { + override val erSmåbarnstilleggIForrigeBehandlingPeriode = + sammePeriodeForrigeBehandling?.andeler?.any { it.type == YtelseType.SMÅBARNSTILLEGG } == true +} + +data class BegrunnelseGrunnlagForPeriodeMedOpphør( + override val dennePerioden: BegrunnelseGrunnlagForPersonIPeriode, + override val forrigePeriode: BegrunnelseGrunnlagForPersonIPeriode?, + val sammePeriodeForrigeBehandling: BegrunnelseGrunnlagForPersonIPeriode?, +) : IBegrunnelseGrunnlagForPeriode { + override val erSmåbarnstilleggIForrigeBehandlingPeriode = + sammePeriodeForrigeBehandling?.andeler?.any { it.type == YtelseType.SMÅBARNSTILLEGG } == true +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/BegrunnelseGrunnlagForPersonIPeriode.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/BegrunnelseGrunnlagForPersonIPeriode.kt new file mode 100644 index 000000000..f824eb78a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/BegrunnelseGrunnlagForPersonIPeriode.kt @@ -0,0 +1,100 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.tilTidslinje +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.tilTidslinje +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombiner +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMedNullable +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.mapIkkeNull +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.AndelForVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.BehandlingsGrunnlagForVedtaksperioder +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.EndretUtbetalingAndelForVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.KompetanseForVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.OvergangsstønadForVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.VilkårResultatForVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.filtrerPåAktør +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.hentErUtbetalingSmåbarnstilleggTidslinje +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.tilAndelerForVedtaksPeriodeTidslinje +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.tilPeriodeOvergangsstønadForVedtaksperiodeTidslinje +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.tilForskjøvedeVilkårTidslinjer +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.Companion.hentOrdinæreVilkårFor +import java.math.BigDecimal + +data class BegrunnelseGrunnlagForPersonIPeriode( + val person: Person, + val vilkårResultater: Iterable, + val andeler: Iterable, + val kompetanse: KompetanseForVedtaksperiode? = null, + val endretUtbetalingAndel: EndretUtbetalingAndelForVedtaksperiode? = null, + val overgangsstønad: OvergangsstønadForVedtaksperiode? = null, +) { + fun erOrdinæreVilkårInnvilget() = + hentOrdinæreVilkårFor(person.type).all { ordinærtVilkårForPerson -> + vilkårResultater.any { it.vilkårType == ordinærtVilkårForPerson && it.resultat == Resultat.OPPFYLT } + } + + fun erInnvilgetEtterEndretUtbetaling(): Boolean { + val erEndretUtbetaling = endretUtbetalingAndel != null + val erEndretUtbetalingPåNullProsent = endretUtbetalingAndel?.prosent == BigDecimal.ZERO + val erÅrsakDeltBosted = endretUtbetalingAndel?.årsak == Årsak.DELT_BOSTED + + return !erEndretUtbetaling || !erEndretUtbetalingPåNullProsent || erÅrsakDeltBosted + } + + companion object { + fun tomPeriode(person: Person) = + BegrunnelseGrunnlagForPersonIPeriode(person = person, vilkårResultater = emptyList(), andeler = emptyList()) + } +} + +fun BehandlingsGrunnlagForVedtaksperioder.lagBegrunnelseGrunnlagTidslinjer(): Map> { + return this.persongrunnlag.personer.associateWith { this.lagBegrunnelseGrunnlagForPersonTidslinje(it) } +} + +fun BehandlingsGrunnlagForVedtaksperioder.lagBegrunnelseGrunnlagForPersonTidslinje(person: Person): Tidslinje { + val forskjøvedeVilkårResultaterForPerson = + this.personResultater.single { it.aktør == person.aktør } + .vilkårResultater + .filter { it.erEksplisittAvslagPåSøknad != true } + .tilForskjøvedeVilkårTidslinjer(person.fødselsdato) + .map { tidslinje -> tidslinje.map { it?.let { VilkårResultatForVedtaksperiode(it) } } } + .kombiner { it } + + val kompetanseTidslinje = this.utfylteKompetanser.filtrerPåAktør(person.aktør) + .tilTidslinje().mapIkkeNull { KompetanseForVedtaksperiode(it) } + + val endredeUtbetalingerTidslinje = this.utfylteEndredeUtbetalinger.filtrerPåAktør(person.aktør) + .tilTidslinje().mapIkkeNull { EndretUtbetalingAndelForVedtaksperiode(it) } + + val andelerTilkjentYtelseTidslinje = + this.andelerTilkjentYtelse.filtrerPåAktør(person.aktør).tilAndelerForVedtaksPeriodeTidslinje() + + val overgangsstønadTidslinje = + this.perioderOvergangsstønad.filtrerPåAktør(person.aktør) + .tilPeriodeOvergangsstønadForVedtaksperiodeTidslinje(andelerTilkjentYtelseTidslinje.hentErUtbetalingSmåbarnstilleggTidslinje()) + + return forskjøvedeVilkårResultaterForPerson + .kombinerMed( + andelerTilkjentYtelse.filtrerPåAktør(person.aktør).tilAndelerForVedtaksPeriodeTidslinje(), + ) { vilkårResultater, andeler -> + vilkårResultater?.let { + BegrunnelseGrunnlagForPersonIPeriode( + person = person, + vilkårResultater = vilkårResultater, + andeler = andeler ?: emptyList(), + ) + } + }.kombinerMedNullable(kompetanseTidslinje) { grunnlagForPerson, kompetanse -> + grunnlagForPerson?.let { grunnlagForPerson.copy(kompetanse = kompetanse) } + }.kombinerMedNullable(endredeUtbetalingerTidslinje) { grunnlagForPerson, endretUtbetalingAndel -> + grunnlagForPerson?.let { grunnlagForPerson.copy(endretUtbetalingAndel = endretUtbetalingAndel) } + }.kombinerMedNullable(overgangsstønadTidslinje) { grunnlagForPerson, overgangsstønad -> + grunnlagForPerson?.let { grunnlagForPerson.copy(overgangsstønad = overgangsstønad) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/FortsattInnvilgetFilter.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/FortsattInnvilgetFilter.kt new file mode 100644 index 000000000..5712bd70d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/FortsattInnvilgetFilter.kt @@ -0,0 +1,45 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.brev.brevBegrunnelseProdusent.GrunnlagForBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityPeriodeResultat +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser + +fun hentFortsattInnvilgetBegrunnelserPerPerson( + begrunnelseGrunnlagPerPerson: Map, + grunnlag: GrunnlagForBegrunnelse, + vedtaksperiode: VedtaksperiodeMedBegrunnelser, +): Map> { + val fagsakType = grunnlag.behandlingsGrunnlagForVedtaksperioder.fagsakType + + val erUtbetalingEllerDeltBostedIPeriode = erUtbetalingEllerDeltBostedIPeriode(begrunnelseGrunnlagPerPerson) + + val relevanteStandardbegrunnelser = grunnlag.sanityBegrunnelser + .filterValues { it.erGjeldendeForFagsakType(fagsakType) } + .filterValues { it.erGjeldendeForBrevPeriodeType(vedtaksperiode, erUtbetalingEllerDeltBostedIPeriode) } + .filterValues { it.periodeResultat == SanityPeriodeResultat.INGEN_ENDRING } + + val relevanteEøsBegrunnelser = grunnlag.sanityEØSBegrunnelser + .filterValues { it.erGjeldendeForFagsakType(fagsakType) } + .filterValues { it.erGjeldendeForBrevPeriodeType(vedtaksperiode, erUtbetalingEllerDeltBostedIPeriode) } + .filterValues { it.periodeResultat == SanityPeriodeResultat.INGEN_ENDRING } + + return begrunnelseGrunnlagPerPerson.mapValues { (person, begrunnelseGrunnlag) -> + val begrunnelseGrunnlagForPerson = begrunnelseGrunnlag.dennePerioden + + val oppfylteVilkårresultater = + begrunnelseGrunnlagForPerson.vilkårResultater.filter { it.resultat == Resultat.OPPFYLT }.toList() + + val standardbegrunnelseSomMatcherVilkår = relevanteStandardbegrunnelser + .filterValues { it.erGjeldendeForRolle(person, fagsakType) } + .filterValues { it.erLikVilkårOgUtdypendeVilkårIPeriode(oppfylteVilkårresultater) } + + val eøsBegrunnelserSomMatcherKompetanse = relevanteEøsBegrunnelser + .filterValues { it.erLikKompetanseIPeriode(begrunnelseGrunnlag) } + + standardbegrunnelseSomMatcherVilkår.keys.toSet() + + eøsBegrunnelserSomMatcherKompetanse.keys.toSet() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/VedtakBegrunnelseProdusent.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/VedtakBegrunnelseProdusent.kt new file mode 100644 index 000000000..1bd9ff105 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/VedtakBegrunnelseProdusent.kt @@ -0,0 +1,797 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent + +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.beregning.SatsService +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.brevBegrunnelseProdusent.GrunnlagForBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.EndretUtbetalingsperiodeDeltBostedTriggere +import no.nav.familie.ba.sak.kjerne.brev.domene.ISanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityPeriodeResultat +import no.nav.familie.ba.sak.kjerne.brev.domene.Tema +import no.nav.familie.ba.sak.kjerne.brev.domene.UtvidetBarnetrygdTrigger +import no.nav.familie.ba.sak.kjerne.brev.domene.Valgbarhet +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.BrevPeriodeType +import no.nav.familie.ba.sak.kjerne.brev.domene.tilPersonType +import no.nav.familie.ba.sak.kjerne.brev.domene.ØvrigTrigger +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombiner +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.mapInnhold +import no.nav.familie.ba.sak.kjerne.tidslinje.månedPeriodeAv +import no.nav.familie.ba.sak.kjerne.tidslinje.periodeAv +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonthEllerUendeligFortid +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonthEllerUendeligFramtid +import no.nav.familie.ba.sak.kjerne.tidslinje.tilTidslinje +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.landkodeTilBarnetsBostedsland +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.domene.hentBrevPeriodeType +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.AndelForVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.BehandlingsGrunnlagForVedtaksperioder +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.EndretUtbetalingAndelForVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.tilForskjøvedeVilkårTidslinjer +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +fun VedtaksperiodeMedBegrunnelser.hentGyldigeBegrunnelserForPeriode( + grunnlagForBegrunnelser: GrunnlagForBegrunnelse, +): Set { + val gyldigeBegrunnelserPerPerson = hentGyldigeBegrunnelserPerPerson( + grunnlagForBegrunnelser, + ) + + return gyldigeBegrunnelserPerPerson.values.flatten().toSet() +} + +fun VedtaksperiodeMedBegrunnelser.hentGyldigeBegrunnelserPerPerson( + grunnlag: GrunnlagForBegrunnelse, +): Map> { + val avslagsbegrunnelserPerPerson = hentAvslagsbegrunnelserPerPerson(grunnlag.behandlingsGrunnlagForVedtaksperioder) + + if (this.type == Vedtaksperiodetype.AVSLAG) { + return avslagsbegrunnelserPerPerson + } + + val begrunnelseGrunnlagPerPerson = this.finnBegrunnelseGrunnlagPerPerson( + grunnlag, + ) + + if (this.type == Vedtaksperiodetype.FORTSATT_INNVILGET) { + return hentFortsattInnvilgetBegrunnelserPerPerson( + begrunnelseGrunnlagPerPerson = begrunnelseGrunnlagPerPerson, + grunnlag = grunnlag, + vedtaksperiode = this, + + ) + } + + val erUtbetalingEllerDeltBostedIPeriode = erUtbetalingEllerDeltBostedIPeriode(begrunnelseGrunnlagPerPerson) + + return begrunnelseGrunnlagPerPerson.mapValues { (person, begrunnelseGrunnlag) -> + val relevantePeriodeResultater = + hentResultaterForPeriode(begrunnelseGrunnlag.dennePerioden, begrunnelseGrunnlag.forrigePeriode) + + val standardBegrunnelser = hentStandardBegrunnelser( + begrunnelseGrunnlag = begrunnelseGrunnlag, + sanityBegrunnelser = grunnlag.sanityBegrunnelser, + person = person, + vedtaksperiode = this, + fagsakType = grunnlag.behandlingsGrunnlagForVedtaksperioder.fagsakType, + relevantePeriodeResultater = relevantePeriodeResultater, + erUtbetalingEllerDeltBostedIPeriode = erUtbetalingEllerDeltBostedIPeriode, + ) + + val eøsBegrunnelser = hentEØSStandardBegrunnelser( + sanityEØSBegrunnelser = grunnlag.sanityEØSBegrunnelser, + begrunnelseGrunnlag = begrunnelseGrunnlag, + relevantePeriodeResultater = relevantePeriodeResultater, + erUtbetalingEllerDeltBostedIPeriode = erUtbetalingEllerDeltBostedIPeriode, + vedtaksperiode = this, + ) + + val avslagsbegrunnelser = avslagsbegrunnelserPerPerson[person] ?: emptySet() + + val temaSomPeriodeErVurdertEtter = hentTemaSomPeriodeErVurdertEtter(begrunnelseGrunnlag) + + val standardOgEøsBegrunnelser: Map = + (standardBegrunnelser + eøsBegrunnelser) + + val standardOgEøsBegrunnelserFiltrertPåTema = + standardOgEøsBegrunnelser.filtrerPåTema(temaSomPeriodeErVurdertEtter) + + standardOgEøsBegrunnelserFiltrertPåTema + avslagsbegrunnelser + } +} + +fun erUtbetalingEllerDeltBostedIPeriode(begrunnelseGrunnlagPerPerson: Map) = + begrunnelseGrunnlagPerPerson.values.any { grunnlagForPeriode -> + val dennePerioden = grunnlagForPeriode.dennePerioden + dennePerioden.endretUtbetalingAndel?.årsak == Årsak.DELT_BOSTED || + dennePerioden.andeler.any { it.prosent != BigDecimal.ZERO } + } + +private fun Map.filtrerPåTema( + temaSomPeriodeErVurdertEtter: Tema, +) = filter { + val temaPåBegrunnelse = it.value.tema + + temaSomPeriodeErVurdertEtter == temaPåBegrunnelse || temaPåBegrunnelse == Tema.FELLES +}.keys.toSet() + +fun hentTemaSomPeriodeErVurdertEtter( + begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode, +): Tema { + val harKompetanseDennePerioden = begrunnelseGrunnlag.dennePerioden.kompetanse != null + val harMistetKompetanseDennePerioden = + !harKompetanseDennePerioden && begrunnelseGrunnlag.forrigePeriode?.kompetanse != null + val harGåttFraEøsTilNasjonal = + harMistetKompetanseDennePerioden && begrunnelseGrunnlag.dennePerioden.andeler.any { it.nasjonaltPeriodebeløp != 0 } + + return when { + harGåttFraEøsTilNasjonal -> Tema.NASJONAL + harKompetanseDennePerioden || harMistetKompetanseDennePerioden -> Tema.EØS + else -> Tema.NASJONAL + } +} + +private fun VedtaksperiodeMedBegrunnelser.hentAvslagsbegrunnelserPerPerson( + behandlingsGrunnlagForVedtaksperioder: BehandlingsGrunnlagForVedtaksperioder, +): Map> { + val tidslinjeMedVedtaksperioden = this.tilTidslinjeForAktuellPeriode() + + return behandlingsGrunnlagForVedtaksperioder.persongrunnlag.personer.associateWith { person -> + val avslagsbegrunnelserTisdlinje = + behandlingsGrunnlagForVedtaksperioder.personResultater.single { it.aktør == person.aktør }.vilkårResultater.filter { it.erEksplisittAvslagPåSøknad == true } + .tilForskjøvedeVilkårTidslinjer(person.fødselsdato) + .kombiner { vilkårResultaterIPeriode -> vilkårResultaterIPeriode.flatMap { it.standardbegrunnelser } } + + tidslinjeMedVedtaksperioden.kombinerMed(avslagsbegrunnelserTisdlinje) { h, v -> + v.takeIf { h != null } + }.perioder().mapNotNull { it.innhold }.flatten().toSet() + } +} + +private fun hentStandardBegrunnelser( + begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode, + sanityBegrunnelser: Map, + person: Person, + vedtaksperiode: VedtaksperiodeMedBegrunnelser, + fagsakType: FagsakType, + relevantePeriodeResultater: List, + erUtbetalingEllerDeltBostedIPeriode: Boolean, +): Map { + val endretUtbetalingDennePerioden = hentEndretUtbetalingDennePerioden(begrunnelseGrunnlag) + + val relevantePeriodeResultaterForrigePeriode = hentResultaterForForrigePeriode(begrunnelseGrunnlag.forrigePeriode) + + val filtrertPåRolle = sanityBegrunnelser.filterValues { begrunnelse -> + begrunnelse.erGjeldendeForRolle(person, fagsakType) + } + val filtrertPåRolleOgFagsaktype = filtrertPåRolle.filterValues { + it.erGjeldendeForFagsakType(fagsakType) + } + val filtrertPåRolleFagsaktypeOgPeriodetype = filtrertPåRolleOgFagsaktype.filterValues { + it.periodeResultat in relevantePeriodeResultater + } + + val filtrertPåRolleFagsaktypePeriodeTypeOgManuelleBegrunnelser = + filtrertPåRolleFagsaktypeOgPeriodetype.filterValues { + it.erManuellBegrunnelse() + } + + val relevanteBegrunnelser = filtrertPåRolleFagsaktypePeriodeTypeOgManuelleBegrunnelser + .filterValues { it.erGjeldendeForBrevPeriodeType(vedtaksperiode, erUtbetalingEllerDeltBostedIPeriode) } + .filterValues { !it.begrunnelseGjelderReduksjonFraForrigeBehandling() && !it.begrunnelseGjelderOpphørFraForrigeBehandling() } + + val filtrertPåVilkårOgEndretUtbetaling = relevanteBegrunnelser.filterValues { + val begrunnelseErGjeldendeForUtgjørendeVilkår = it.vilkår.isNotEmpty() + val begrunnelseErGjeldendeForEndretUtbetaling = it.endringsaarsaker.isNotEmpty() + + when { + begrunnelseErGjeldendeForUtgjørendeVilkår && begrunnelseErGjeldendeForEndretUtbetaling -> filtrerPåVilkår( + it, + begrunnelseGrunnlag, + ) && filtrerPåEndretUtbetaling(it, endretUtbetalingDennePerioden) + + begrunnelseErGjeldendeForUtgjørendeVilkår -> filtrerPåVilkår(it, begrunnelseGrunnlag) + else -> it.erEndretUtbetaling(endretUtbetalingDennePerioden) + } + } + + val filtrertPåReduksjonFraForrigeBehandling = filtrertPåRolleOgFagsaktype.filterValues { + it.erGjeldendeForReduksjonFraForrigeBehandling(begrunnelseGrunnlag) + } + + val filtrertPåOpphørFraForrigeBehandling = filtrertPåRolleOgFagsaktype.filterValues { + it.erGjeldendeForOpphørFraForrigeBehandling(begrunnelseGrunnlag) + } + + val filtrertPåSmåbarnstillegg = + relevanteBegrunnelser.filterValues { begrunnelse -> + begrunnelse.erGjeldendeForSmåbarnstillegg(begrunnelseGrunnlag) + } + + val begrunnelserFiltrertPåPeriodetypeForrigePeriode = sanityBegrunnelser.filterValues { + it.periodeResultat in relevantePeriodeResultaterForrigePeriode + } + + val filtrertPåRolleOgPeriodetypeForrigePeriode = + begrunnelserFiltrertPåPeriodetypeForrigePeriode.filterValues { begrunnelse -> + begrunnelse.erGjeldendeForRolle(person, fagsakType) + } + + val filtrertPåEtterEndretUtbetaling = filtrertPåRolleOgPeriodetypeForrigePeriode.filterValues { + it.erEtterEndretUtbetaling( + endretUtbetalingDennePerioden = endretUtbetalingDennePerioden, + endretUtbetalingForrigePeriode = hentEndretUtbetalingForrigePeriode(begrunnelseGrunnlag), + ) + } + + val filtrertPåHendelser = relevanteBegrunnelser.filtrerPåHendelser( + begrunnelseGrunnlag, + vedtaksperiode.fom, + ) + + return filtrertPåVilkårOgEndretUtbetaling + filtrertPåReduksjonFraForrigeBehandling + filtrertPåOpphørFraForrigeBehandling + filtrertPåSmåbarnstillegg + filtrertPåEtterEndretUtbetaling + filtrertPåHendelser +} + +private fun SanityBegrunnelse.erManuellBegrunnelse() = ØvrigTrigger.ALLTID_AUTOMATISK !in ovrigeTriggere + +fun ISanityBegrunnelse.erGjeldendeForFagsakType( + fagsakType: FagsakType, +) = if (valgbarhet == Valgbarhet.SAKSPESIFIKK) { + fagsakType == this.fagsakType +} else { + true +} + +private fun filtrerPåEndretUtbetaling( + it: SanityBegrunnelse, + endretUtbetalingDennePerioden: EndretUtbetalingAndelForVedtaksperiode?, +) = it.erEndretUtbetaling(endretUtbetalingDennePerioden) + +private fun filtrerPåVilkår( + it: SanityBegrunnelse, + begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode, +) = + !it.begrunnelseGjelderReduksjonFraForrigeBehandling() && it.erGjeldendeForUtgjørendeVilkår(begrunnelseGrunnlag) && it.erGjeldendeForRegelverk( + begrunnelseGrunnlag, + ) + +private fun SanityBegrunnelse.erGjeldendeForRegelverk(begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode): Boolean = + begrunnelseGrunnlag.dennePerioden.vilkårResultater.none { it.vurderesEtter == Regelverk.EØS_FORORDNINGEN } || this.tema == Tema.FELLES + +private fun SanityBegrunnelse.erGjeldendeForReduksjonFraForrigeBehandling(begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode): Boolean { + if (begrunnelseGrunnlag !is BegrunnelseGrunnlagForPeriodeMedReduksjonPåTversAvBehandlinger) { + return false + } + + val oppfylteVilkårDenneBehandlingen = + begrunnelseGrunnlag.dennePerioden.vilkårResultater.filter { it.resultat == Resultat.OPPFYLT } + .map { it.vilkårType }.toSet() + val oppfylteVilkårForrigeBehandling = + begrunnelseGrunnlag.sammePeriodeForrigeBehandling?.vilkårResultater?.filter { it.resultat == Resultat.OPPFYLT } + ?.map { it.vilkårType }?.toSet() ?: emptySet() + + val vilkårMistetSidenForrigeBehandling = oppfylteVilkårForrigeBehandling - oppfylteVilkårDenneBehandlingen + + val begrunnelseGjelderMistedeVilkår = this.vilkår.all { it in vilkårMistetSidenForrigeBehandling } + + val haddeSmåbarnstilleggForrigeBehandling = begrunnelseGrunnlag.erSmåbarnstilleggIForrigeBehandlingPeriode + val harSmåbarnstilleggDennePerioden = + begrunnelseGrunnlag.dennePerioden.andeler.any { it.type == YtelseType.SMÅBARNSTILLEGG } + + val begrunnelseGjelderTaptSmåbarnstillegg = + UtvidetBarnetrygdTrigger.SMÅBARNSTILLEGG in utvidetBarnetrygdTriggere && haddeSmåbarnstilleggForrigeBehandling && !harSmåbarnstilleggDennePerioden + + return begrunnelseGjelderReduksjonFraForrigeBehandling() && (begrunnelseGjelderMistedeVilkår || begrunnelseGjelderTaptSmåbarnstillegg) +} + +private fun SanityBegrunnelse.begrunnelseGjelderReduksjonFraForrigeBehandling() = + ØvrigTrigger.GJELDER_FRA_INNVILGELSESTIDSPUNKT in this.ovrigeTriggere || ØvrigTrigger.REDUKSJON_FRA_FORRIGE_BEHANDLING in this.ovrigeTriggere + +private fun SanityBegrunnelse.erGjeldendeForOpphørFraForrigeBehandling(begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode): Boolean { + if (begrunnelseGrunnlag !is BegrunnelseGrunnlagForPeriodeMedOpphør || !begrunnelseGjelderOpphørFraForrigeBehandling()) { + return false + } + + val oppfylteVilkårDenneBehandlingen = + begrunnelseGrunnlag.dennePerioden.vilkårResultater.filter { it.resultat == Resultat.OPPFYLT } + .map { it.vilkårType }.toSet() + + val oppfylteVilkårsresultaterForrigeBehandling = + begrunnelseGrunnlag.sammePeriodeForrigeBehandling?.vilkårResultater?.filter { it.resultat == Resultat.OPPFYLT } + val oppfylteVilkårForrigeBehandling = + oppfylteVilkårsresultaterForrigeBehandling?.map { it.vilkårType }?.toSet() ?: emptySet() + + val vilkårMistetSidenForrigeBehandling = oppfylteVilkårForrigeBehandling - oppfylteVilkårDenneBehandlingen + + val begrunnelseGjelderMistedeVilkår = this.erLikVilkårOgUtdypendeVilkårIPeriode( + oppfylteVilkårsresultaterForrigeBehandling?.filter { it.vilkårType in vilkårMistetSidenForrigeBehandling } + ?: emptyList(), + ) + + val dennePeriodenErFørsteVedtaksperiodePåFagsak = + begrunnelseGrunnlag.forrigePeriode == null || begrunnelseGrunnlag.forrigePeriode!!.andeler.firstOrNull() == null + + return begrunnelseGjelderMistedeVilkår && dennePeriodenErFørsteVedtaksperiodePåFagsak +} + +private fun SanityBegrunnelse.begrunnelseGjelderOpphørFraForrigeBehandling() = + ØvrigTrigger.GJELDER_FØRSTE_PERIODE in this.ovrigeTriggere || ØvrigTrigger.OPPHØR_FRA_FORRIGE_BEHANDLING in this.ovrigeTriggere + +private fun hentEØSStandardBegrunnelser( + vedtaksperiode: VedtaksperiodeMedBegrunnelser, + sanityEØSBegrunnelser: Map, + begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode, + relevantePeriodeResultater: List, + erUtbetalingEllerDeltBostedIPeriode: Boolean, +): Map { + val begrunnelserFiltrertPåPeriodetype = sanityEØSBegrunnelser.filterValues { + it.periodeResultat in relevantePeriodeResultater + } + + val begrunnelserFiltrertPåPerioderesultatOgBrevPeriodeType = begrunnelserFiltrertPåPeriodetype + .filterValues { it.erGjeldendeForBrevPeriodeType(vedtaksperiode, erUtbetalingEllerDeltBostedIPeriode) } + + val filtrertPåVilkår = begrunnelserFiltrertPåPerioderesultatOgBrevPeriodeType.filterValues { + it.erGjeldendeForUtgjørendeVilkår(begrunnelseGrunnlag) + } + + val filtrertPåKompetanse = begrunnelserFiltrertPåPerioderesultatOgBrevPeriodeType.filterValues { begrunnelse -> + erEndringIKompetanse(begrunnelseGrunnlag) && begrunnelse.erLikKompetanseIPeriode(begrunnelseGrunnlag) + } + + val filtrertPåPeriodeResultat = begrunnelserFiltrertPåPeriodetype.filterValues { + filtrerPåPeriodeResultat(relevantePeriodeResultater, it) + } + + return filtrertPåVilkår + filtrertPåKompetanse + filtrertPåPeriodeResultat +} + +private fun filtrerPåPeriodeResultat( + relevantePeriodeResultater: List, + sanityEøsBegrunnelse: SanityEØSBegrunnelse, +): Boolean { + val periodeResultatErIngenEndring = SanityPeriodeResultat.INGEN_ENDRING in relevantePeriodeResultater + val periodeResultatPåBegrunnelseErInnvilgetEllerØkning = + sanityEøsBegrunnelse.periodeResultat == SanityPeriodeResultat.INNVILGET_ELLER_ØKNING + + return periodeResultatErIngenEndring && periodeResultatPåBegrunnelseErInnvilgetEllerØkning +} + +fun SanityBegrunnelse.erGjeldendeForRolle( + person: Person, + fagsakType: FagsakType, +): Boolean { + val rolleErRelevantForBegrunnelse = this.rolle.isNotEmpty() + + val begrunnelseGjelderPersonSinRolle = + person.type in this.rolle.map { it.tilPersonType() } || fagsakType.erBarnSøker() + + return !rolleErRelevantForBegrunnelse || begrunnelseGjelderPersonSinRolle +} + +fun SanityEØSBegrunnelse.erLikKompetanseIPeriode( + begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode, +): Boolean { + val kompetanse = when (this.periodeResultat) { + SanityPeriodeResultat.INNVILGET_ELLER_ØKNING, SanityPeriodeResultat.INGEN_ENDRING -> + begrunnelseGrunnlag.dennePerioden.kompetanse + ?: return false + + SanityPeriodeResultat.IKKE_INNVILGET, + SanityPeriodeResultat.REDUKSJON, + -> begrunnelseGrunnlag.forrigePeriode?.kompetanse ?: return false + + null, + -> return false + } + + return this.annenForeldersAktivitet.contains(kompetanse.annenForeldersAktivitet) && this.barnetsBostedsland.contains( + landkodeTilBarnetsBostedsland(kompetanse.barnetsBostedsland), + ) && this.kompetanseResultat.contains(kompetanse.resultat) +} + +fun Map.filtrerPåHendelser( + begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode, + fomVedtaksperiode: LocalDate?, +): Map = if (!begrunnelseGrunnlag.dennePerioden.erOrdinæreVilkårInnvilget()) { + val person = begrunnelseGrunnlag.dennePerioden.person + + this.filtrerPåBarnDød(person, fomVedtaksperiode) +} else { + val person = begrunnelseGrunnlag.dennePerioden.person + + this.filtrerPåBarn6år(person, fomVedtaksperiode) + this.filtrerPåSatsendring( + person, + begrunnelseGrunnlag.dennePerioden.andeler, + fomVedtaksperiode, + ) +} + +fun Map.filtrerPåBarn6år( + person: Person, + fomVedtaksperiode: LocalDate?, +): Map { + val blirPerson6DennePerioden = person.hentSeksårsdag().toYearMonth() == fomVedtaksperiode?.toYearMonth() + + return if (blirPerson6DennePerioden) { + this.filterValues { it.ovrigeTriggere.contains(ØvrigTrigger.BARN_MED_6_ÅRS_DAG) } + } else { + emptyMap() + } +} + +fun Map.filtrerPåBarnDød( + person: Person, + fomVedtaksperiode: LocalDate?, +): Map { + val dødsfall = person.dødsfall + val personDødeForrigeMåned = + dødsfall != null && dødsfall.dødsfallDato.toYearMonth().plusMonths(1) == fomVedtaksperiode?.toYearMonth() + + return if (personDødeForrigeMåned && person.type == PersonType.BARN) { + this.filterValues { it.ovrigeTriggere.contains(ØvrigTrigger.BARN_DØD) } + } else { + emptyMap() + } +} + +fun Map.filtrerPåSatsendring( + person: Person, + andeler: Iterable, + fomVedtaksperiode: LocalDate?, +): Map { + val satstyperPåAndelene = andeler.map { it.type.tilSatsType(person, fomVedtaksperiode ?: TIDENES_MORGEN) }.toSet() + + val erSatsendringIPeriodenForPerson = satstyperPåAndelene.any { satstype -> + SatsService.finnAlleSatserFor(satstype).any { it.gyldigFom == fomVedtaksperiode } + } + + return if (erSatsendringIPeriodenForPerson) { + this.filterValues { it.ovrigeTriggere.contains(ØvrigTrigger.SATSENDRING) } + } else { + emptyMap() + } +} + +private fun hentResultaterForForrigePeriode( + begrunnelseGrunnlagForrigePeriode: BegrunnelseGrunnlagForPersonIPeriode?, +) = + if (begrunnelseGrunnlagForrigePeriode?.erOrdinæreVilkårInnvilget() == true && begrunnelseGrunnlagForrigePeriode.erInnvilgetEtterEndretUtbetaling()) { + listOf( + SanityPeriodeResultat.REDUKSJON, + SanityPeriodeResultat.INNVILGET_ELLER_ØKNING, + ) + } else { + listOf( + SanityPeriodeResultat.REDUKSJON, + SanityPeriodeResultat.IKKE_INNVILGET, + ) + } + +private fun hentResultaterForPeriode( + begrunnelseGrunnlagForPeriode: BegrunnelseGrunnlagForPersonIPeriode, + begrunnelseGrunnlagForrigePeriode: BegrunnelseGrunnlagForPersonIPeriode?, +): List { + val erAndelerPåPersonHvisBarn = + begrunnelseGrunnlagForPeriode.person.type != PersonType.BARN || begrunnelseGrunnlagForPeriode.andeler.toList() + .isNotEmpty() + + val erInnvilgetEtterVilkårOgEndretUtbetaling = + begrunnelseGrunnlagForPeriode.erOrdinæreVilkårInnvilget() && begrunnelseGrunnlagForPeriode.erInnvilgetEtterEndretUtbetaling() + + val erReduksjonIAndel = erReduksjonIAndelMellomPerioder( + begrunnelseGrunnlagForPeriode, + begrunnelseGrunnlagForrigePeriode, + ) + + return if (erInnvilgetEtterVilkårOgEndretUtbetaling && erAndelerPåPersonHvisBarn) { + val erØkingIAndel = erØkningIAndelMellomPerioder( + begrunnelseGrunnlagForPeriode, + begrunnelseGrunnlagForrigePeriode, + ) + + val erSøker = begrunnelseGrunnlagForPeriode.person.type == PersonType.SØKER + val erOrdinæreVilkårOppfyltIForrigePeriode = + begrunnelseGrunnlagForrigePeriode?.erOrdinæreVilkårInnvilget() == true + + val erIngenEndring = !erØkingIAndel && !erReduksjonIAndel && erOrdinæreVilkårOppfyltIForrigePeriode + listOfNotNull( + if (erØkingIAndel || erSøker || erIngenEndring) SanityPeriodeResultat.INNVILGET_ELLER_ØKNING else null, + if (erReduksjonIAndel) SanityPeriodeResultat.REDUKSJON else null, + if (erIngenEndring) SanityPeriodeResultat.INGEN_ENDRING else null, + ) + } else { + listOfNotNull( + if (erReduksjonIAndel) SanityPeriodeResultat.REDUKSJON else null, + SanityPeriodeResultat.IKKE_INNVILGET, + ) + } +} + +private fun erReduksjonIAndelMellomPerioder( + begrunnelseGrunnlagForPeriode: BegrunnelseGrunnlagForPersonIPeriode?, + begrunnelseGrunnlagForrigePeriode: BegrunnelseGrunnlagForPersonIPeriode?, +): Boolean { + val andelerForrigePeriode = begrunnelseGrunnlagForrigePeriode?.andeler ?: emptyList() + val andelerDennePerioden = begrunnelseGrunnlagForPeriode?.andeler ?: emptyList() + + return andelerForrigePeriode.any { andelIForrigePeriode -> + val sammeAndelDennePerioden = andelerDennePerioden.singleOrNull { andelIForrigePeriode.type == it.type } + + val erAndelenMistet = + sammeAndelDennePerioden == null && begrunnelseGrunnlagForrigePeriode?.erInnvilgetEtterEndretUtbetaling() == true + val harAndelenGåttNedIProsent = + sammeAndelDennePerioden != null && andelIForrigePeriode.prosent > sammeAndelDennePerioden.prosent + val erSatsenRedusert = andelIForrigePeriode.sats > (sammeAndelDennePerioden?.sats ?: 0) + + erAndelenMistet || harAndelenGåttNedIProsent || erSatsenRedusert + } +} + +private fun erØkningIAndelMellomPerioder( + begrunnelseGrunnlagForPeriode: BegrunnelseGrunnlagForPersonIPeriode, + begrunnelseGrunnlagForrigePeriode: BegrunnelseGrunnlagForPersonIPeriode?, +): Boolean { + val andelerForrigePeriode = begrunnelseGrunnlagForrigePeriode?.andeler ?: emptyList() + val andelerDennePerioden = begrunnelseGrunnlagForPeriode.andeler + + return andelerDennePerioden.any { andelIPeriode -> + val sammeAndelForrigePeriode = andelerForrigePeriode.singleOrNull { andelIPeriode.type == it.type } + + val erAndelenTjent = + sammeAndelForrigePeriode == null && begrunnelseGrunnlagForPeriode.erInnvilgetEtterEndretUtbetaling() + val harAndelenGåttOppIProsent = + sammeAndelForrigePeriode != null && andelIPeriode.prosent > sammeAndelForrigePeriode.prosent + val erSatsenØkt = andelIPeriode.sats > (sammeAndelForrigePeriode?.sats ?: 0) + + erAndelenTjent || harAndelenGåttOppIProsent || erSatsenØkt + } +} + +private fun SanityBegrunnelse.erEtterEndretUtbetaling( + endretUtbetalingDennePerioden: EndretUtbetalingAndelForVedtaksperiode?, + endretUtbetalingForrigePeriode: EndretUtbetalingAndelForVedtaksperiode?, +): Boolean { + if (!this.erEndringsårsakOgGjelderEtterEndretUtbetaling()) return false + + return this.matcherEtterEndretUtbetaling( + endretUtbetalingDennePerioden = endretUtbetalingDennePerioden, + endretUtbetalingForrigePeriode = endretUtbetalingForrigePeriode, + ) +} + +private fun SanityBegrunnelse.matcherEtterEndretUtbetaling( + endretUtbetalingDennePerioden: EndretUtbetalingAndelForVedtaksperiode?, + endretUtbetalingForrigePeriode: EndretUtbetalingAndelForVedtaksperiode?, +): Boolean { + val begrunnelseMatcherEndretUtbetalingIForrigePeriode = + this.endringsaarsaker.all { it == endretUtbetalingForrigePeriode?.årsak } + + val begrunnelseMatcherEndretUtbetalingIDennePerioden = + this.endringsaarsaker.all { it == endretUtbetalingDennePerioden?.årsak } + + if (!begrunnelseMatcherEndretUtbetalingIForrigePeriode || begrunnelseMatcherEndretUtbetalingIDennePerioden) return false + + return endretUtbetalingForrigePeriode?.årsak != Årsak.DELT_BOSTED || this.erDeltBostedUtbetalingstype( + endretUtbetalingForrigePeriode, + ) +} + +private fun SanityBegrunnelse.erEndringsårsakOgGjelderEtterEndretUtbetaling() = + this.endringsaarsaker.isNotEmpty() && this.gjelderEtterEndretUtbetaling() + +private fun SanityBegrunnelse.erEndretUtbetaling( + endretUtbetaling: EndretUtbetalingAndelForVedtaksperiode?, +): Boolean { + return this.gjelderEndretUtbetaling() && this.erLikEndretUtbetalingIPeriode(endretUtbetaling) +} + +private fun SanityBegrunnelse.gjelderEndretUtbetaling() = + this.endringsaarsaker.isNotEmpty() && !this.gjelderEtterEndretUtbetaling() + +private fun SanityBegrunnelse.erLikEndretUtbetalingIPeriode( + endretUtbetaling: EndretUtbetalingAndelForVedtaksperiode?, +): Boolean { + if (endretUtbetaling == null) return false + + val erEndringsårsakerIBegrunnelseOgPeriodeLike = this.endringsaarsaker.all { it == endretUtbetaling.årsak } + if (!erEndringsårsakerIBegrunnelseOgPeriodeLike) return false + + return if (endretUtbetaling.årsak == Årsak.DELT_BOSTED) { + this.erDeltBostedUtbetalingstype(endretUtbetaling) + } else { + true + } +} + +private fun SanityBegrunnelse.erDeltBostedUtbetalingstype( + endretUtbetaling: EndretUtbetalingAndelForVedtaksperiode, +): Boolean { + val inneholderAndelSomSkalUtbetales = endretUtbetaling.prosent != BigDecimal.ZERO + + return when (this.endretUtbetalingsperiodeDeltBostedUtbetalingTrigger) { + EndretUtbetalingsperiodeDeltBostedTriggere.UTBETALING_IKKE_RELEVANT -> true + EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_UTBETALES -> inneholderAndelSomSkalUtbetales + EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_IKKE_UTBETALES -> !inneholderAndelSomSkalUtbetales + null -> true + } +} + +private fun hentEndretUtbetalingDennePerioden(begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode) = + begrunnelseGrunnlag.dennePerioden.endretUtbetalingAndel.takeIf { begrunnelseGrunnlag.dennePerioden.erOrdinæreVilkårInnvilget() } + +private fun hentEndretUtbetalingForrigePeriode(begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode) = + begrunnelseGrunnlag.forrigePeriode?.endretUtbetalingAndel.takeIf { begrunnelseGrunnlag.forrigePeriode?.erOrdinæreVilkårInnvilget() == true } + +fun VedtaksperiodeMedBegrunnelser.finnBegrunnelseGrunnlagPerPerson( + grunnlag: GrunnlagForBegrunnelse, +): Map { + val tidslinjeMedVedtaksperioden = this.tilTidslinjeForAktuellPeriode() + + val begrunnelsegrunnlagTidslinjerPerPerson = + grunnlag.behandlingsGrunnlagForVedtaksperioder.lagBegrunnelseGrunnlagTidslinjer() + + val grunnlagTidslinjePerPersonForrigeBehandling = + grunnlag.behandlingsGrunnlagForVedtaksperioderForrigeBehandling?.lagBegrunnelseGrunnlagTidslinjer() + + return begrunnelsegrunnlagTidslinjerPerPerson.mapValues { (person, grunnlagTidslinje) -> + val grunnlagMedForrigePeriodeOgBehandlingTidslinje = + tidslinjeMedVedtaksperioden.lagTidslinjeGrunnlagDennePeriodenForrigePeriodeOgPeriodeForrigeBehandling( + grunnlagTidslinje, + grunnlagTidslinjePerPersonForrigeBehandling, + person, + ) + + val begrunnelseperioderIVedtaksperiode = + grunnlagMedForrigePeriodeOgBehandlingTidslinje.perioder().mapNotNull { it.innhold } + + when (this.type) { + Vedtaksperiodetype.OPPHØR -> begrunnelseperioderIVedtaksperiode.first() + Vedtaksperiodetype.FORTSATT_INNVILGET -> if (this.fom == null && this.tom == null) { + val perioder = grunnlagMedForrigePeriodeOgBehandlingTidslinje.perioder() + perioder.single { grunnlag.nåDato.toYearMonth() in it.fraOgMed.tilYearMonthEllerUendeligFortid()..it.tilOgMed.tilYearMonthEllerUendeligFramtid() }.innhold!! + } else { + begrunnelseperioderIVedtaksperiode.first() + } + + else -> begrunnelseperioderIVedtaksperiode.first() + } + } +} + +private fun Tidslinje.lagTidslinjeGrunnlagDennePeriodenForrigePeriodeOgPeriodeForrigeBehandling( + grunnlagTidslinje: Tidslinje, + grunnlagTidslinjePerPersonForrigeBehandling: Map>?, + person: Person, +): Tidslinje { + val grunnlagMedForrigePeriodeTidslinje = grunnlagTidslinje.tilForrigeOgNåværendePeriodeTidslinje(this) + + val grunnlagForrigeBehandlingTidslinje = grunnlagTidslinjePerPersonForrigeBehandling?.get(person) ?: TomTidslinje() + + return this.kombinerMed( + grunnlagMedForrigePeriodeTidslinje, + grunnlagForrigeBehandlingTidslinje, + ) { vedtaksPerioden, forrigeOgDennePerioden, forrigeBehandling -> + val dennePerioden = forrigeOgDennePerioden?.denne + + if (vedtaksPerioden == null) { + null + } else { + IBegrunnelseGrunnlagForPeriode.opprett( + dennePerioden = dennePerioden ?: BegrunnelseGrunnlagForPersonIPeriode.tomPeriode(person), + forrigePeriode = forrigeOgDennePerioden?.forrige, + sammePeriodeForrigeBehandling = forrigeBehandling, + periodetype = vedtaksPerioden.type, + ) + } + } +} + +private fun VedtaksperiodeMedBegrunnelser.tilTidslinjeForAktuellPeriode(): Tidslinje { + return listOf( + månedPeriodeAv( + fraOgMed = this.fom?.toYearMonth(), + tilOgMed = this.tom?.toYearMonth(), + innhold = this, + ), + ).tilTidslinje() +} + +data class ForrigeOgDennePerioden( + val forrige: BegrunnelseGrunnlagForPersonIPeriode?, + val denne: BegrunnelseGrunnlagForPersonIPeriode?, +) + +private fun Tidslinje.tilForrigeOgNåværendePeriodeTidslinje( + vedtaksperiodeTidslinje: Tidslinje, +): Tidslinje { + val grunnlagPerioderSplittetPåVedtaksperiode = kombinerMed(vedtaksperiodeTidslinje) { grunnlag, periode -> + Pair(grunnlag, periode) + }.perioder().mapInnhold { it?.first } + + return ( + listOf( + månedPeriodeAv(YearMonth.now(), YearMonth.now(), null), + ) + grunnlagPerioderSplittetPåVedtaksperiode + ).zipWithNext { forrige, denne -> + periodeAv(denne.fraOgMed, denne.tilOgMed, ForrigeOgDennePerioden(forrige.innhold, denne.innhold)) + }.tilTidslinje() +} + +private fun SanityBegrunnelse.erGjeldendeForSmåbarnstillegg( + begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode, +): Boolean { + val erSmåbarnstilleggForrigePeriode = + begrunnelseGrunnlag.forrigePeriode?.andeler?.any { it.type == YtelseType.SMÅBARNSTILLEGG } == true + val erSmåbarnstilleggDennePerioden = + begrunnelseGrunnlag.dennePerioden.andeler.any { it.type == YtelseType.SMÅBARNSTILLEGG } + + val erSmåbarnstilleggIForrigeBehandlingPeriode = begrunnelseGrunnlag.erSmåbarnstilleggIForrigeBehandlingPeriode + + val begrunnelseGjelderSmåbarnstillegg = UtvidetBarnetrygdTrigger.SMÅBARNSTILLEGG in utvidetBarnetrygdTriggere + + val erEndringISmåbarnstilleggFraForrigeBehandling = + erSmåbarnstilleggIForrigeBehandlingPeriode != erSmåbarnstilleggDennePerioden + + val begrunnelseMatcherPeriodeResultat = this.matcherPerioderesultat( + erSmåbarnstilleggForrigePeriode, + erSmåbarnstilleggDennePerioden, + erSmåbarnstilleggIForrigeBehandlingPeriode, + ) + + val erEndringISmåbarnstillegg = erSmåbarnstilleggForrigePeriode != erSmåbarnstilleggDennePerioden + + return begrunnelseGjelderSmåbarnstillegg && begrunnelseMatcherPeriodeResultat && (erEndringISmåbarnstillegg || erEndringISmåbarnstilleggFraForrigeBehandling) +} + +private fun SanityBegrunnelse.matcherPerioderesultat( + erSmåbarnstilleggForrigePeriode: Boolean, + erSmåbarnstilleggDennePerioden: Boolean, + erSmåbarnstilleggIForrigeBehandlingPeriode: Boolean, +): Boolean { + val erReduksjon = + !erSmåbarnstilleggDennePerioden && (erSmåbarnstilleggForrigePeriode || erSmåbarnstilleggIForrigeBehandlingPeriode) + val erØkning = + erSmåbarnstilleggDennePerioden && (!erSmåbarnstilleggForrigePeriode || !erSmåbarnstilleggIForrigeBehandlingPeriode) + + val erBegrunnelseReduksjon = periodeResultat == SanityPeriodeResultat.REDUKSJON + val erBegrunnelseØkning = periodeResultat == SanityPeriodeResultat.INNVILGET_ELLER_ØKNING + + val reduksjonMatcher = erReduksjon == erBegrunnelseReduksjon + val økningMatcher = erØkning == erBegrunnelseØkning + return reduksjonMatcher && økningMatcher +} + +private fun erEndringIKompetanse(begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode) = + begrunnelseGrunnlag.dennePerioden.kompetanse != begrunnelseGrunnlag.forrigePeriode?.kompetanse + +fun ISanityBegrunnelse.erGjeldendeForBrevPeriodeType( + vedtaksperiode: VedtaksperiodeMedBegrunnelser, + erUtbetalingEllerDeltBostedIPeriode: Boolean, +): Boolean { + val brevPeriodeType = hentBrevPeriodeType( + vedtaksperiode.type, + vedtaksperiode.fom, + erUtbetalingEllerDeltBostedIPeriode, + ) + return this.periodeType == brevPeriodeType || + (this.periodeType == BrevPeriodeType.FORTSATT_INNVILGET && brevPeriodeType == BrevPeriodeType.FORTSATT_INNVILGET_NY) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/Vilk\303\245rFilterUtil.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/Vilk\303\245rFilterUtil.kt" new file mode 100644 index 000000000..e028834f2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtakBegrunnelseProdusent/Vilk\303\245rFilterUtil.kt" @@ -0,0 +1,155 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.brev.domene.ISanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityPeriodeResultat +import no.nav.familie.ba.sak.kjerne.brev.domene.UtvidetBarnetrygdTrigger +import no.nav.familie.ba.sak.kjerne.brev.domene.VilkårTrigger +import no.nav.familie.ba.sak.kjerne.brev.domene.tilUtdypendeVilkårsvurderinger +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.AndelForVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.VilkårResultatForVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår + +fun ISanityBegrunnelse.erGjeldendeForUtgjørendeVilkår( + begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode, +): Boolean { + if (this.vilkår.isEmpty()) return false + val utgjørendeVilkårResultater = finnUtgjørendeVilkår( + begrunnelseGrunnlag = begrunnelseGrunnlag, + sanityBegrunnelse = this, + ) + + return this.erLikVilkårOgUtdypendeVilkårIPeriode(utgjørendeVilkårResultater) +} + +fun ISanityBegrunnelse.erLikVilkårOgUtdypendeVilkårIPeriode( + vilkårResultaterForPerson: Collection, +): Boolean { + return this.vilkår.all { vilkårISanityBegrunnelse -> + val vilkårResultat = vilkårResultaterForPerson.find { it.vilkårType == vilkårISanityBegrunnelse } + + vilkårResultat != null && this.matcherMedUtdypendeVilkår(vilkårResultat) + } +} + +fun ISanityBegrunnelse.matcherMedUtdypendeVilkår(vilkårResultat: VilkårResultatForVedtaksperiode): Boolean { + return when (vilkårResultat.vilkårType) { + Vilkår.UNDER_18_ÅR -> true + Vilkår.BOR_MED_SØKER -> vilkårResultat.utdypendeVilkårsvurderinger.erLik(this.borMedSokerTriggere) + Vilkår.GIFT_PARTNERSKAP -> vilkårResultat.utdypendeVilkårsvurderinger.erLik(this.giftPartnerskapTriggere) + Vilkår.BOSATT_I_RIKET -> vilkårResultat.utdypendeVilkårsvurderinger.erLik(this.bosattIRiketTriggere) + Vilkår.LOVLIG_OPPHOLD -> vilkårResultat.utdypendeVilkårsvurderinger.erLik(this.lovligOppholdTriggere) + // Håndteres i `erGjeldendeForSmåbarnstillegg` + Vilkår.UTVIDET_BARNETRYGD -> UtvidetBarnetrygdTrigger.SMÅBARNSTILLEGG !in this.utvidetBarnetrygdTriggere + } +} + +private fun Collection.erLik( + utdypendeVilkårsvurderingFraSanityBegrunnelse: List?, +): Boolean { + val utdypendeVilkårPåVilkårResultat = this.toSet() + val utdypendeVilkårPåSanityBegrunnelse: Set = + utdypendeVilkårsvurderingFraSanityBegrunnelse?.tilUtdypendeVilkårsvurderinger()?.toSet() ?: emptySet() + + return utdypendeVilkårPåVilkårResultat == utdypendeVilkårPåSanityBegrunnelse +} + +private fun finnUtgjørendeVilkår( + sanityBegrunnelse: ISanityBegrunnelse, + begrunnelseGrunnlag: IBegrunnelseGrunnlagForPeriode, +): Set { + val oppfylteVilkårResultaterDennePerioden = + begrunnelseGrunnlag.dennePerioden.vilkårResultater.filter { it.resultat == Resultat.OPPFYLT } + val oppfylteVilkårResultaterForrigePeriode = + begrunnelseGrunnlag.forrigePeriode?.vilkårResultater?.filter { it.resultat == Resultat.OPPFYLT } + ?: emptyList() + + val vilkårTjent = hentVilkårResultaterTjent( + oppfylteVilkårResultaterDennePerioden = oppfylteVilkårResultaterDennePerioden, + oppfylteVilkårResultaterForrigePeriode = oppfylteVilkårResultaterForrigePeriode, + ) + val vilkårEndret = hentOppfylteVilkårResultaterEndret( + oppfylteVilkårResultaterDennePerioden = oppfylteVilkårResultaterDennePerioden, + oppfylteVilkårResultaterForrigePeriode = oppfylteVilkårResultaterForrigePeriode, + ) + val vilkårTapt = hentVilkårResultaterTapt( + oppfylteVilkårResultaterDennePerioden = oppfylteVilkårResultaterDennePerioden, + oppfylteVilkårResultaterForrigePeriode = oppfylteVilkårResultaterForrigePeriode, + ) + + return if (begrunnelseGrunnlag.dennePerioden.erOrdinæreVilkårInnvilget()) { + val utvidetTriggetAvInnvilgelse = hentUtvidetTriggetAvInnvilgelse( + sanityBegrunnelse = sanityBegrunnelse, + andelerForrigePeriode = begrunnelseGrunnlag.forrigePeriode?.andeler, + oppfylteVilkårResultaterDennePerioden = oppfylteVilkårResultaterDennePerioden, + ) + when (sanityBegrunnelse.periodeResultat) { + SanityPeriodeResultat.INNVILGET_ELLER_ØKNING -> vilkårTjent + vilkårEndret + utvidetTriggetAvInnvilgelse + SanityPeriodeResultat.INGEN_ENDRING -> vilkårEndret + SanityPeriodeResultat.IKKE_INNVILGET, + SanityPeriodeResultat.REDUKSJON, + -> vilkårTapt + vilkårEndret + + null -> emptyList() + } + } else { + vilkårTapt.takeIf { + sanityBegrunnelse.periodeResultat in listOf( + SanityPeriodeResultat.IKKE_INNVILGET, + SanityPeriodeResultat.REDUKSJON, + ) + } ?: emptyList() + }.toSet() +} + +private fun hentOppfylteVilkårResultaterEndret( + oppfylteVilkårResultaterDennePerioden: List, + oppfylteVilkårResultaterForrigePeriode: List, +): List = + oppfylteVilkårResultaterDennePerioden.filter { vilkårResultatForrigePeriode -> + val sammeVilkårResultatForrigePeriode = + oppfylteVilkårResultaterForrigePeriode.singleOrNull { it.vilkårType == vilkårResultatForrigePeriode.vilkårType } + + sammeVilkårResultatForrigePeriode != null && + vilkårResultatForrigePeriode != sammeVilkårResultatForrigePeriode + } + +private fun hentVilkårResultaterTjent( + oppfylteVilkårResultaterDennePerioden: List, + oppfylteVilkårResultaterForrigePeriode: List, +): List { + val innvilgedeVilkårDennePerioden = oppfylteVilkårResultaterDennePerioden.map { it.vilkårType } + val innvilgedeVilkårForrigePerioden = oppfylteVilkårResultaterForrigePeriode.map { it.vilkårType } + + val vilkårTjent = innvilgedeVilkårDennePerioden.toSet() - innvilgedeVilkårForrigePerioden.toSet() + + return oppfylteVilkårResultaterDennePerioden.filter { it.vilkårType in vilkårTjent } +} + +private fun hentVilkårResultaterTapt( + oppfylteVilkårResultaterDennePerioden: List, + oppfylteVilkårResultaterForrigePeriode: List, +): List { + val oppfyltDennePerioden = oppfylteVilkårResultaterDennePerioden.map { it.vilkårType }.toSet() + val oppfyltForrigePeriode = oppfylteVilkårResultaterForrigePeriode.map { it.vilkårType }.toSet() + + val vilkårTapt = oppfyltForrigePeriode - oppfyltDennePerioden + + return oppfylteVilkårResultaterForrigePeriode.filter { it.vilkårType in vilkårTapt } +} + +private fun hentUtvidetTriggetAvInnvilgelse( + sanityBegrunnelse: ISanityBegrunnelse, + andelerForrigePeriode: Iterable?, + oppfylteVilkårResultaterDennePerioden: List, +): List { + if (sanityBegrunnelse.apiNavn != Standardbegrunnelse.INNVILGET_BOR_ALENE_MED_BARN.sanityApiNavn) { + return emptyList() + } + val ingenAndelerForrigePeriode = andelerForrigePeriode == null || !andelerForrigePeriode.any() + val utvidetOppfyltDennePerioden = + oppfylteVilkårResultaterDennePerioden.filter { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + return if (ingenAndelerForrigePeriode) utvidetOppfyltDennePerioden else emptyList() +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/BehandlingsGrunnlagForVedtaksperioder.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/BehandlingsGrunnlagForVedtaksperioder.kt new file mode 100644 index 000000000..2334a98a0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/BehandlingsGrunnlagForVedtaksperioder.kt @@ -0,0 +1,511 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.common.tilMånedÅr +import no.nav.familie.ba.sak.ekstern.restDomene.BarnMedOpplysninger +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.beregning.domene.tilTidslinjerPerAktørOgType +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.IUtfyltEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.tilIEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.tilTidslinje +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.UtfyltKompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.tilIKompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.tilTidslinje +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombiner +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMedDatert +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMedNullable +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNull +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.slåSammenLike +import no.nav.familie.ba.sak.kjerne.tidslinje.månedPeriodeAv +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonthEllerNull +import no.nav.familie.ba.sak.kjerne.tidslinje.tilTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.mapIkkeNull +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.alleOrdinæreVilkårErOppfylt +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.tilForskjøvedeVilkårTidslinjer +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.tilTidslinjeForSplittForPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat + +typealias AktørId = String + +data class GrunnlagForPersonTidslinjerSplittetPåOverlappendeGenerelleAvslag( + val overlappendeGenerelleAvslagVedtaksperiodeGrunnlagForPerson: Tidslinje, + val vedtaksperiodeGrunnlagForPerson: Tidslinje, +) + +data class AktørOgRolleBegrunnelseGrunnlag( + val aktør: Aktør, + val rolleBegrunnelseGrunnlag: PersonType, +) + +data class BehandlingsGrunnlagForVedtaksperioder( + val persongrunnlag: PersonopplysningGrunnlag, + val personResultater: Set, + val fagsakType: FagsakType, + val kompetanser: List, + val endredeUtbetalinger: List, + val andelerTilkjentYtelse: List, + val perioderOvergangsstønad: List, + val uregistrerteBarn: List, +) { + val utfylteEndredeUtbetalinger = endredeUtbetalinger + .map { it.tilIEndretUtbetalingAndel() } + .filterIsInstance() + + val utfylteKompetanser = kompetanser + .map { it.tilIKompetanse() } + .filterIsInstance() + + fun utledGrunnlagTidslinjePerPerson(): Map { + val søker = persongrunnlag.søker + val ordinæreVilkårForSøkerForskjøvetTidslinje = + hentOrdinæreVilkårForSøkerForskjøvetTidslinje(søker, personResultater) + + val erMinstEttBarnMedUtbetalingTidslinje = + hentErMinstEttBarnMedUtbetalingTidslinje(personResultater, fagsakType, persongrunnlag) + + val erUtbetalingSmåbarnstilleggTidslinje = this.andelerTilkjentYtelse.hentErUtbetalingSmåbarnstilleggTidslinje() + + val personresultaterOgRolleForVilkår = if (fagsakType.erBarnSøker()) { + personResultater.single().splittOppVilkårForBarnOgSøkerRolle() + } else { + personResultater.map { + Pair(persongrunnlag.personer.single { person -> it.aktør == person.aktør }.type, it) + } + } + + val bareSøkerOgUregistrertBarn = uregistrerteBarn.isNotEmpty() && personResultater.size == 1 + + val grunnlagForPersonTidslinjer = personresultaterOgRolleForVilkår.associate { (vilkårRolle, personResultat) -> + val aktør = personResultat.aktør + val person = persongrunnlag.personer.single { person -> aktør == person.aktør } + + val (overlappendeGenerelleAvslag, vilkårResultaterUtenGenerelleAvslag) = splittOppPåErOverlappendeGenerelleAvslag( + personResultat, + ) + + val forskjøvedeVilkårResultaterForPersonsAndeler: Tidslinje, Måned> = + vilkårResultaterUtenGenerelleAvslag.hentForskjøvedeVilkårResultaterForPersonsAndelerTidslinje( + person = person, + erMinstEttBarnMedUtbetalingTidslinje = erMinstEttBarnMedUtbetalingTidslinje, + ordinæreVilkårForSøkerTidslinje = ordinæreVilkårForSøkerForskjøvetTidslinje, + fagsakType = fagsakType, + vilkårRolle = vilkårRolle, + bareSøkerOgUregistrertBarn = bareSøkerOgUregistrertBarn, + ) + + AktørOgRolleBegrunnelseGrunnlag(aktør, vilkårRolle) to + GrunnlagForPersonTidslinjerSplittetPåOverlappendeGenerelleAvslag( + overlappendeGenerelleAvslagVedtaksperiodeGrunnlagForPerson = overlappendeGenerelleAvslag.generelleAvslagTilGrunnlagForPersonTidslinje( + person, + ), + vedtaksperiodeGrunnlagForPerson = forskjøvedeVilkårResultaterForPersonsAndeler.tilGrunnlagForPersonTidslinje( + person = person, + søker = søker, + erUtbetalingSmåbarnstilleggTidslinje = erUtbetalingSmåbarnstilleggTidslinje, + vilkårRolle = vilkårRolle, + ), + ) + } + + return grunnlagForPersonTidslinjer + } + + private fun PersonResultat.splittOppVilkårForBarnOgSøkerRolle(): List> { + val personResultaterVilkårForSøker = hentDelAvPersonResultatForRolle(rolle = PersonType.SØKER) + + val personResultaterVilkårForBarn = hentDelAvPersonResultatForRolle(rolle = PersonType.BARN) + + return listOf( + Pair(PersonType.SØKER, personResultaterVilkårForSøker), + Pair(PersonType.BARN, personResultaterVilkårForBarn), + ) + } + + private fun PersonResultat.hentDelAvPersonResultatForRolle( + rolle: PersonType, + ): PersonResultat { + val personResultaterVilkårForSøker = this.kopierMedParent(this.vilkårsvurdering, true) + personResultaterVilkårForSøker.setSortedVilkårResultater( + personResultaterVilkårForSøker.vilkårResultater + .filter { it.vilkårType.gjelder(rolle) }.toSet(), + ) + return personResultaterVilkårForSøker + } + + private fun Vilkår.gjelder(persontype: PersonType) = when (this) { + Vilkår.UNDER_18_ÅR -> listOf(PersonType.BARN).contains(persontype) + Vilkår.BOR_MED_SØKER -> listOf(PersonType.BARN).contains(persontype) + Vilkår.GIFT_PARTNERSKAP -> listOf(PersonType.BARN).contains(persontype) + Vilkår.BOSATT_I_RIKET -> listOf(PersonType.BARN, PersonType.SØKER).contains(persontype) + Vilkår.LOVLIG_OPPHOLD -> listOf(PersonType.BARN, PersonType.SØKER).contains(persontype) + Vilkår.UTVIDET_BARNETRYGD -> listOf(PersonType.SØKER).contains(persontype) + } + + private fun List.generelleAvslagTilGrunnlagForPersonTidslinje( + person: Person, + ): Tidslinje = this + .map { + listOf(månedPeriodeAv(null, null, it)) + .tilTidslinje() + } + .kombinerUtenNull { it.toList() } + .map { vilkårResultater -> + vilkårResultater?.let { + VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget( + person = person, + vilkårResultaterForVedtaksperiode = it.map { vilkårResultat -> + VilkårResultatForVedtaksperiode( + vilkårResultat, + ) + }, + ) + } + } + + private fun Tidslinje, Måned>.tilGrunnlagForPersonTidslinje( + person: Person, + søker: Person, + erUtbetalingSmåbarnstilleggTidslinje: Tidslinje, + vilkårRolle: PersonType, + ): Tidslinje { + val harRettPåUtbetalingTidslinje = this.tilHarRettPåUtbetalingTidslinje( + person = person, + søker = søker, + vilkårRolle = vilkårRolle, + ) + + val kompetanseTidslinje = utfylteKompetanser.filtrerPåAktør(person.aktør) + .tilTidslinje().mapIkkeNull { KompetanseForVedtaksperiode(it) } + + val endredeUtbetalingerTidslinje = utfylteEndredeUtbetalinger.filtrerPåAktør(person.aktør) + .tilTidslinje().mapIkkeNull { EndretUtbetalingAndelForVedtaksperiode(it) } + + val overgangsstønadTidslinje = + perioderOvergangsstønad.filtrerPåAktør(person.aktør) + .tilPeriodeOvergangsstønadForVedtaksperiodeTidslinje(erUtbetalingSmåbarnstilleggTidslinje) + + val grunnlagTidslinje = harRettPåUtbetalingTidslinje + .kombinerMedDatert( + this.tilVilkårResultaterForVedtaksPeriodeTidslinje(), + andelerTilkjentYtelse.filtrerPåAktør(person.aktør).tilAndelerForVedtaksPeriodeTidslinje(), + ) { personHarRettPåUtbetalingIPeriode, vilkårResultater, andeler, tidspunkt -> + lagGrunnlagForVilkårOgAndel( + personHarRettPåUtbetalingIPeriode = personHarRettPåUtbetalingIPeriode, + vilkårResultater = vilkårResultater, + person = person, + andeler = andeler, + tidspunkt, + ) + }.kombinerMedNullable(kompetanseTidslinje) { grunnlagForPerson, kompetanse -> + lagGrunnlagMedKompetanse(grunnlagForPerson, kompetanse) + }.kombinerMedNullable(endredeUtbetalingerTidslinje) { grunnlagForPerson, endretUtbetalingAndel -> + lagGrunnlagMedEndretUtbetalingAndel(grunnlagForPerson, endretUtbetalingAndel) + }.kombinerMedNullable(overgangsstønadTidslinje) { grunnlagForPerson, overgangsstønad -> + lagGrunnlagMedOvergangsstønad(grunnlagForPerson, overgangsstønad) + }.filtrerIkkeNull() + + return grunnlagTidslinje + .slåSammenLike() + .perioder() + .dropWhile { !it.erInnvilgetEllerEksplisittAvslag() } + .tilTidslinje() + } +} + +private fun splittOppPåErOverlappendeGenerelleAvslag(personResultat: PersonResultat): Pair, List> { + val overlappendeGenerelleAvslag = + personResultat.vilkårResultater.groupBy { it.vilkårType }.mapNotNull { (_, resultat) -> + if (resultat.size > 1) { + resultat.filter { it.erGenereltAvslag() } + } else { + null + } + }.flatten() + + val vilkårResultaterUtenGenerelleAvslag = + personResultat.vilkårResultater.filterNot { overlappendeGenerelleAvslag.contains(it) } + return Pair(overlappendeGenerelleAvslag, vilkårResultaterUtenGenerelleAvslag) +} + +private fun List.filtrerVilkårErOrdinærtFor( + søker: Person, +): List? { + val ordinæreVilkårForSøker = Vilkår.hentOrdinæreVilkårFor(søker.type) + + return this + .filter { ordinæreVilkårForSøker.contains(it.vilkårType) } + .takeIf { it.isNotEmpty() } +} + +fun hentOrdinæreVilkårForSøkerForskjøvetTidslinje( + søker: Person, + personResultater: Set, +): Tidslinje, Måned> { + val søkerPersonResultater = personResultater.single { it.aktør == søker.aktør } + + val (_, vilkårResultaterUtenOverlappendeGenerelleAvslag) = splittOppPåErOverlappendeGenerelleAvslag( + søkerPersonResultater, + ) + + return vilkårResultaterUtenOverlappendeGenerelleAvslag + .tilForskjøvedeVilkårTidslinjer(søker.fødselsdato) + .kombiner { vilkårResultater -> vilkårResultater.toList().takeIf { it.isNotEmpty() } } + .map { it?.toList()?.filtrerVilkårErOrdinærtFor(søker) } +} + +fun VilkårResultat.erGenereltAvslag() = + periodeFom == null && periodeTom == null && erEksplisittAvslagPåSøknad == true + +private fun hentErMinstEttBarnMedUtbetalingTidslinje( + personResultater: Set, + fagsakType: FagsakType, + persongrunnlag: PersonopplysningGrunnlag, +): Tidslinje { + val søker = persongrunnlag.søker + val søkerSinerOrdinæreVilkårErOppfyltTidslinje = + personResultater.single { it.aktør == søker.aktør }.tilTidslinjeForSplittForPerson( + person = søker, + fagsakType = fagsakType, + ).map { it != null } + + val barnSineVilkårErOppfyltTidslinjer = personResultater + .filter { it.aktør != søker.aktør || søker.type == PersonType.BARN } + .map { personResultat -> + personResultat.tilTidslinjeForSplittForPerson( + person = persongrunnlag.barna.single { it.aktør == personResultat.aktør }, + fagsakType = fagsakType, + ).map { it != null } + } + + return barnSineVilkårErOppfyltTidslinjer + .map { + it.kombinerMed(søkerSinerOrdinæreVilkårErOppfyltTidslinje) { barnetHarAlleOrdinæreVilkårOppfylt, søkerHarAlleOrdinæreVilkårOppfylt -> + barnetHarAlleOrdinæreVilkårOppfylt == true && søkerHarAlleOrdinæreVilkårOppfylt == true + } + } + .kombiner { erOrdinæreVilkårOppfyltForSøkerOgBarn -> + erOrdinæreVilkårOppfyltForSøkerOgBarn.any { it } + } +} + +private fun List.hentForskjøvedeVilkårResultaterForPersonsAndelerTidslinje( + person: Person, + erMinstEttBarnMedUtbetalingTidslinje: Tidslinje, + ordinæreVilkårForSøkerTidslinje: Tidslinje, Måned>, + fagsakType: FagsakType, + vilkårRolle: PersonType, + bareSøkerOgUregistrertBarn: Boolean, +): Tidslinje, Måned> { + val forskjøvedeVilkårResultaterForPerson = this.tilForskjøvedeVilkårTidslinjer(person.fødselsdato).kombiner { it } + + return when (vilkårRolle) { + PersonType.SØKER -> forskjøvedeVilkårResultaterForPerson.map { vilkårResultater -> + if (bareSøkerOgUregistrertBarn) { + vilkårResultater?.toList()?.takeIf { it.isNotEmpty() } + } else { + vilkårResultater?.filtrerErIkkeOrdinærtFor(vilkårRolle)?.takeIf { it.isNotEmpty() } + } + }.kombinerMed(erMinstEttBarnMedUtbetalingTidslinje) { vilkårResultaterForSøker, erMinstEttBarnMedUtbetaling -> + vilkårResultaterForSøker?.takeIf { erMinstEttBarnMedUtbetaling == true || vilkårResultaterForSøker.any { it.erEksplisittAvslagPåSøknad == true } } + } + + PersonType.BARN -> if (fagsakType == FagsakType.BARN_ENSLIG_MINDREÅRIG || fagsakType == FagsakType.INSTITUSJON) { + forskjøvedeVilkårResultaterForPerson.map { it?.toList() } + } else { + forskjøvedeVilkårResultaterForPerson + .kombinerMed(ordinæreVilkårForSøkerTidslinje) { vilkårResultaterBarn, vilkårResultaterSøker -> + slåSammenHvisMulig(vilkårResultaterBarn, vilkårResultaterSøker)?.toList() + } + } + + PersonType.ANNENPART -> throw Feil("Ikke implementert for annenpart") + } +} + +private fun slåSammenHvisMulig( + venstre: Iterable?, + høyre: Iterable?, +) = when { + venstre == null -> høyre + høyre == null -> venstre + else -> høyre + venstre +} + +private fun Iterable.filtrerErIkkeOrdinærtFor(persontype: PersonType): List { + val ordinæreVilkårForPerson = Vilkår.hentOrdinæreVilkårFor(persontype) + + return this.filterNot { ordinæreVilkårForPerson.contains(it.vilkårType) } +} + +private fun lagGrunnlagForVilkårOgAndel( + personHarRettPåUtbetalingIPeriode: Boolean?, + vilkårResultater: List?, + person: Person, + andeler: Iterable?, + måned: Tidspunkt, +) = if (personHarRettPåUtbetalingIPeriode == true) { + if (andeler == null) { + secureLogger.info( + "Andeler må finnes for innvilgede vedtaksperioder, men det var ikke andeler i ${ + måned.tilYearMonthEllerNull()?.tilMånedÅr() ?: "uendelig ${måned.uendelighet}" + } for $person", + ) + } + + VedtaksperiodeGrunnlagForPersonVilkårInnvilget( + vilkårResultaterForVedtaksperiode = vilkårResultater + ?: error("vilkårResultatene burde alltid finnes om vi har innvilget vedtaksperiode."), + person = person, + andeler = andeler + ?: error( + "Andeler må finnes for innvilgede vedtaksperioder, men det var ikke andeler i ${ + måned.tilYearMonthEllerNull()?.tilMånedÅr() ?: "uendelig ${måned.uendelighet}" + }", + ), + ) +} else { + VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget( + vilkårResultaterForVedtaksperiode = vilkårResultater ?: emptyList(), + person = person, + ) +} + +private fun lagGrunnlagMedKompetanse( + vedtaksperiodeGrunnlagForPerson: VedtaksperiodeGrunnlagForPerson?, + kompetanse: KompetanseForVedtaksperiode?, +) = when (vedtaksperiodeGrunnlagForPerson) { + is VedtaksperiodeGrunnlagForPersonVilkårInnvilget -> vedtaksperiodeGrunnlagForPerson.copy(kompetanse = kompetanse) + is VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget -> vedtaksperiodeGrunnlagForPerson + null -> null +} + +private fun lagGrunnlagMedEndretUtbetalingAndel( + vedtaksperiodeGrunnlagForPerson: VedtaksperiodeGrunnlagForPerson?, + endretUtbetalingAndel: EndretUtbetalingAndelForVedtaksperiode?, +) = when (vedtaksperiodeGrunnlagForPerson) { + is VedtaksperiodeGrunnlagForPersonVilkårInnvilget -> vedtaksperiodeGrunnlagForPerson.copy(endretUtbetalingAndel = endretUtbetalingAndel) + is VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget -> vedtaksperiodeGrunnlagForPerson + null -> null +} + +private fun lagGrunnlagMedOvergangsstønad( + vedtaksperiodeGrunnlagForPerson: VedtaksperiodeGrunnlagForPerson?, + overgangsstønad: OvergangsstønadForVedtaksperiode?, +) = when (vedtaksperiodeGrunnlagForPerson) { + is VedtaksperiodeGrunnlagForPersonVilkårInnvilget -> vedtaksperiodeGrunnlagForPerson.copy(overgangsstønad = overgangsstønad) + is VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget -> vedtaksperiodeGrunnlagForPerson + null -> null +} + +// TODO: Kan dette erstattes ved å se på hvorvidt det er andeler eller ikke i stedet? +private fun Tidslinje, Måned>.tilHarRettPåUtbetalingTidslinje( + person: Person, + søker: Person, + vilkårRolle: PersonType, +): Tidslinje = this.map { vilkårResultater -> + if (vilkårResultater.isNullOrEmpty()) { + null + } else { + when (vilkårRolle) { + PersonType.SØKER -> vilkårResultater.filtrerPåAktør(søker.aktør).all { it.erOppfylt() } + + PersonType.BARN -> { + val barnSineVilkårErOppfylt = vilkårResultater.filtrerPåAktør(person.aktør) + .alleOrdinæreVilkårErOppfylt( + PersonType.BARN, + FagsakType.NORMAL, + ) + val søkerSineVilkårErOppfylt = vilkårResultater.filtrerPåAktør(søker.aktør) + .alleOrdinæreVilkårErOppfylt( + PersonType.SØKER, + FagsakType.NORMAL, + ) + + barnSineVilkårErOppfylt && søkerSineVilkårErOppfylt + } + + PersonType.ANNENPART -> throw Feil("Ikke implementert for annenpart") + } + } +} + +fun List.tilAndelerForVedtaksPeriodeTidslinje(): Tidslinje, Måned> = + this.tilTidslinjerPerAktørOgType() + .values + .map { tidslinje -> tidslinje.mapIkkeNull { it }.slåSammenLike() } + .kombiner { it } + +// Vi trenger dette for å kunne begrunne nye perioder med småbarnstillegg som vi ikke hadde i forrige behandling +fun List.tilPeriodeOvergangsstønadForVedtaksperiodeTidslinje( + erUtbetalingSmåbarnstilleggTidslinje: Tidslinje, +) = this + .map { OvergangsstønadForVedtaksperiode(it) } + .map { Periode(it.fom.tilMånedTidspunkt(), it.tom.tilMånedTidspunkt(), it) } + .tilTidslinje() + .kombinerMed(erUtbetalingSmåbarnstilleggTidslinje) { overgangsstønad, erUtbetalingSmåbarnstillegg -> + overgangsstønad.takeIf { erUtbetalingSmåbarnstillegg == true } + } + +private fun Tidslinje, Måned>.tilVilkårResultaterForVedtaksPeriodeTidslinje() = + this.map { vilkårResultater -> vilkårResultater?.map { VilkårResultatForVedtaksperiode(it) } } + +@JvmName("internPeriodeOvergangsstønaderFiltrerPåAktør") +fun List.filtrerPåAktør(aktør: Aktør) = + this.filter { it.personIdent == aktør.aktivFødselsnummer() } + +@JvmName("andelerTilkjentYtelserFiltrerPåAktør") +fun List.filtrerPåAktør(aktør: Aktør) = + this.filter { andelTilkjentYtelse -> andelTilkjentYtelse.aktør == aktør } + +@JvmName("endredeUtbetalingerFiltrerPåAktør") +fun List.filtrerPåAktør(aktør: Aktør) = + this.filter { endretUtbetaling -> endretUtbetaling.person.aktør == aktør } + +@JvmName("utfyltKompetanseFiltrerPåAktør") +fun List.filtrerPåAktør(aktør: Aktør) = + this.filter { it.barnAktører.contains(aktør) } + +@JvmName("vilkårResultatFiltrerPåAktør") +fun List.filtrerPåAktør(aktør: Aktør) = + filter { it.personResultat?.aktør == aktør } + +private fun Periode.erInnvilgetEllerEksplisittAvslag(): Boolean { + val grunnlagForPerson = innhold ?: return false + + val erInnvilget = grunnlagForPerson is VedtaksperiodeGrunnlagForPersonVilkårInnvilget + val erEksplisittAvslag = + grunnlagForPerson.vilkårResultaterForVedtaksperiode.any { it.erEksplisittAvslagPåSøknad == true } + + return erInnvilget || erEksplisittAvslag +} + +private fun List.hentErUtbetalingSmåbarnstilleggTidslinje(): Tidslinje { + return tilAndelerForVedtaksPeriodeTidslinje().hentErUtbetalingSmåbarnstilleggTidslinje() +} + +fun Tidslinje, Måned>.hentErUtbetalingSmåbarnstilleggTidslinje() = + this.mapIkkeNull { andelerIPeriode -> + andelerIPeriode.any { + it.type == YtelseType.SMÅBARNSTILLEGG && it.kalkulertUtbetalingsbeløp > 0 + } + } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/Eksempel sammensl\303\245ing avslagslagsperioder med andre perioder.png" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/Eksempel sammensl\303\245ing avslagslagsperioder med andre perioder.png" new file mode 100644 index 000000000..14cbe7fe3 Binary files /dev/null and "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/Eksempel sammensl\303\245ing avslagslagsperioder med andre perioder.png" differ diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/GrunnlagForGjeldendeOgForrigeBehandling.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/GrunnlagForGjeldendeOgForrigeBehandling.kt new file mode 100644 index 000000000..7989f15b5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/GrunnlagForGjeldendeOgForrigeBehandling.kt @@ -0,0 +1,6 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent + +data class GrunnlagForGjeldendeOgForrigeBehandling( + val gjeldende: VedtaksperiodeGrunnlagForPerson?, + val erReduksjonSidenForrigeBehandling: Boolean = false, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/README.md b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/README.md new file mode 100644 index 000000000..e2dd93983 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/README.md @@ -0,0 +1,57 @@ +# Generering av vedtaksperioder + +## Bakgrunn +Etter at vi vedtar en behandling ønsker vi å sende ut et dokument til brukeren som forklarer hva de har fått og ikke fått, +og i hvilke perioder de får hva. [VedtaksperiodeProdusent](VedtaksperiodeProdusent.kt) sin oppgave er å finne ut hvilke datoer +som gjelder for de forskjellige periodene. + +## Hvilke data ser vi på +Den overordnede tanken er at dersom noe av følgende endrer seg, ønsker vi å formidle det til brukeren: +* Vilkår +* Utbetalingsbeløp +* Kompetanse +* Endret utbetaling +* Overgangsstønad + +## Hvilke vilkår gjelder for persontypene +I utgangspunktet er det fem vilkår for barn og to vilkår for søker som må være oppfylt for at en periode blir innvilget. +Det betyr at de to vilkårene på søker står felles for alle barn det er blitt søkt for siden andel tilkjent ytelse blir utbetalt +for barna. + +For utvidet barnetrygd er det på søker andelene blir utbetalt og dermed blir det unaturlig å legge vilkåret på barna. Her +må derimot vilkår til minst ett av barna være oppfylt, så vi baker inn oppfylt-perioder for barna sammen med forelders +vilkår om utvidet barnetrygd. +![Vilkår relevante for personer sine andeler.png](Vilk%C3%A5r%20relevante%20for%20personer%20sine%20andeler.png) + +## Regler for sammenslåing av individuelle personers perioder til perioder for flere personer +Dataene som skal ende opp i dokumentet vi sender til bruker ønsker vi å samle på en måte som gjør det enkelt å forstå +hva som foregår når. + +* Dersom vilkårene ikke er innvilget er det ikke nødvendig å lagre resterende data som andeler tilkjent ytelse, kompetanse o.l. + +* Dersom to personer har perioder med samme fom og tom skal periodene "slås sammen" slik at de begrunnes sammen i dokumentet. + +* Dersom to innvilgede perioder følger etter hverandre men andeler tilkjent ytelse er lik for begge periodene skal periodene +slås sammen, med mindre det er en endring i vilkår eller andre data. + +* Dersom den første perioden i tidslinja ikke er innvilget ønsker vi å strippe denne (med mindre det er et avslag) siden det +ikke er interessant å snakke om at man ikke har innvilgete perioder f.eks. før barn er født eller dersom barnet ikke bodde +hos søker i den første perioden. + +* Dersom det er eksplisitte avslag skal det "skrive over" ikke-innvilgede perioder. Dersom det er innvilgede perioder i samme +tidsspenn skal avslagene stå ved siden av. Se tegning under. + +> I eksempelet under ser vi at i den første perioden for _barn1_ så slår vi sammen periodene til _barn1_ og _barn2_ til to +perioder siden begge er innvilget. +Ettersom _barn3_ sin periode ikke har samme start- og sluttdato som innvilgetperiodene, slår vi den ikke sammen med de +innvilgede periodene. +> +>I _barn1_ sin andre periode slår vi perioden til _barn2_ og _barn3_ sammen siden begge er innvilget. +I dette tilfellet har _barn1_ sin ikke-innvilgede periode samme start- og sluttdato som de kombinerte innvilgetperiodene, +og vi slår de sammen. +> +> ![Eksempel sammenslåing avslagslagsperioder med andre perioder.png](Eksempel%20sammensl%C3%A5ing%20avslagslagsperioder%20med%20andre%20perioder.png) + +## Sammenligning på tvers av behandlinger +Dersom det er en reduksjon, altså at en periode var innvilget i forrige behandling, men ikke er det nå lenger, skal dette +tydeliggjøres i dokumentet ved at perioden som nå ikke er godkjent kommer med selv hvis den kommer som første periode. \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/VedtaksperiodeGrunnlagForPerson.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/VedtaksperiodeGrunnlagForPerson.kt new file mode 100644 index 000000000..6d4d60153 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/VedtaksperiodeGrunnlagForPerson.kt @@ -0,0 +1,202 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.IUtfyltEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.UtfyltKompetanse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import java.math.BigDecimal +import java.time.LocalDate +import java.util.Objects + +sealed interface VedtaksperiodeGrunnlagForPerson { + val person: Person + val vilkårResultaterForVedtaksperiode: List + + fun erEksplisittAvslag(): Boolean = + this is VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget && this.erEksplisittAvslag + + fun erInnvilget() = this is VedtaksperiodeGrunnlagForPersonVilkårInnvilget && this.erInnvilgetEndretUtbetaling() + + fun hentInnvilgedeYtelsestyper() = + if (this is VedtaksperiodeGrunnlagForPersonVilkårInnvilget) { + this.andeler.filter { it.prosent > BigDecimal.ZERO } + .map { it.type }.toSet() + } else { + emptySet() + } + + fun kopier( + person: Person = this.person, + vilkårResultaterForVedtaksperiode: List = this.vilkårResultaterForVedtaksperiode, + ): VedtaksperiodeGrunnlagForPerson { + return when (this) { + is VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget -> this.copy( + person, + vilkårResultaterForVedtaksperiode, + ) + + is VedtaksperiodeGrunnlagForPersonVilkårInnvilget -> this.copy(person, vilkårResultaterForVedtaksperiode) + } + } +} + +data class VedtaksperiodeGrunnlagForPersonVilkårInnvilget( + override val person: Person, + override val vilkårResultaterForVedtaksperiode: List, + val andeler: Iterable, + val kompetanse: KompetanseForVedtaksperiode? = null, + val endretUtbetalingAndel: EndretUtbetalingAndelForVedtaksperiode? = null, + val overgangsstønad: OvergangsstønadForVedtaksperiode? = null, +) : VedtaksperiodeGrunnlagForPerson { + fun erInnvilgetEndretUtbetaling() = + endretUtbetalingAndel?.prosent != BigDecimal.ZERO || endretUtbetalingAndel?.årsak == Årsak.DELT_BOSTED +} + +data class VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget( + override val person: Person, + override val vilkårResultaterForVedtaksperiode: List, +) : VedtaksperiodeGrunnlagForPerson { + val erEksplisittAvslag: Boolean = vilkårResultaterForVedtaksperiode.inneholderEksplisittAvslag() + + fun List.inneholderEksplisittAvslag() = + this.any { it.erEksplisittAvslagPåSøknad == true } +} + +data class VilkårResultatForVedtaksperiode( + val vilkårType: Vilkår, + val resultat: Resultat, + val utdypendeVilkårsvurderinger: List, + val vurderesEtter: Regelverk?, + val erEksplisittAvslagPåSøknad: Boolean?, + val standardbegrunnelser: List, + val aktørId: AktørId, + val fom: LocalDate?, + val tom: LocalDate?, +) { + constructor(vilkårResultat: VilkårResultat) : this( + vilkårType = vilkårResultat.vilkårType, + resultat = vilkårResultat.resultat, + utdypendeVilkårsvurderinger = vilkårResultat.utdypendeVilkårsvurderinger, + vurderesEtter = vilkårResultat.vurderesEtter, + erEksplisittAvslagPåSøknad = vilkårResultat.erEksplisittAvslagPåSøknad, + standardbegrunnelser = vilkårResultat.standardbegrunnelser, + fom = vilkårResultat.periodeFom, + tom = vilkårResultat.periodeTom, + aktørId = vilkårResultat.personResultat?.aktør?.aktørId + ?: throw Feil("$vilkårResultat er ikke knyttet til personResultat"), + ) +} + +fun List.erLikUtenFomOgTom(other: List): Boolean { + return this.map { it.copy(fom = null, tom = null) }.toSet() == other.map { it.copy(fom = null, tom = null) }.toSet() +} + +data class EndretUtbetalingAndelForVedtaksperiode( + val prosent: BigDecimal, + val årsak: Årsak, + val søknadstidspunkt: LocalDate, +) { + constructor(endretUtbetalingAndel: IUtfyltEndretUtbetalingAndel) : this( + prosent = endretUtbetalingAndel.prosent, + årsak = endretUtbetalingAndel.årsak, + søknadstidspunkt = endretUtbetalingAndel.søknadstidspunkt, + ) +} + +data class AndelForVedtaksperiode( + val kalkulertUtbetalingsbeløp: Int, + val nasjonaltPeriodebeløp: Int?, + val type: YtelseType, + val prosent: BigDecimal, + val sats: Int, +) { + constructor(andelTilkjentYtelse: AndelTilkjentYtelse) : this( + kalkulertUtbetalingsbeløp = andelTilkjentYtelse.kalkulertUtbetalingsbeløp, + nasjonaltPeriodebeløp = andelTilkjentYtelse.nasjonaltPeriodebeløp, + type = andelTilkjentYtelse.type, + prosent = andelTilkjentYtelse.prosent, + sats = andelTilkjentYtelse.sats, + ) + + override fun equals(other: Any?): Boolean { + if (other !is AndelForVedtaksperiode) { + return false + } else if (this === other) { + return true + } + + val annen = other + return Objects.equals(kalkulertUtbetalingsbeløp, annen.kalkulertUtbetalingsbeløp) && + Objects.equals(type, annen.type) && + Objects.equals(prosent, annen.prosent) && + satsErlik(annen.sats) + } + + private fun satsErlik(annen: Int): Boolean { + return if (kalkulertUtbetalingsbeløp == 0) { + true + } else { + Objects.equals(sats, annen) + } + } + + override fun hashCode(): Int { + return if (kalkulertUtbetalingsbeløp == 0) { + Objects.hash( + kalkulertUtbetalingsbeløp, + type, + prosent, + ) + } else { + Objects.hash( + kalkulertUtbetalingsbeløp, + type, + prosent, + sats, + ) + } + } +} + +data class KompetanseForVedtaksperiode( + val søkersAktivitet: KompetanseAktivitet, + val annenForeldersAktivitet: KompetanseAktivitet, + val annenForeldersAktivitetsland: String?, + val søkersAktivitetsland: String, + val barnetsBostedsland: String, + val resultat: KompetanseResultat, + val barnAktører: Set, +) { + constructor(kompetanse: UtfyltKompetanse) : this( + søkersAktivitet = kompetanse.søkersAktivitet, + annenForeldersAktivitet = kompetanse.annenForeldersAktivitet, + annenForeldersAktivitetsland = kompetanse.annenForeldersAktivitetsland, + søkersAktivitetsland = kompetanse.søkersAktivitetsland, + barnetsBostedsland = kompetanse.barnetsBostedsland, + resultat = kompetanse.resultat, + barnAktører = kompetanse.barnAktører, + ) +} + +data class OvergangsstønadForVedtaksperiode( + val fom: LocalDate, + val tom: LocalDate, +) { + constructor(periodeOvergangsstønad: InternPeriodeOvergangsstønad) : this( + fom = periodeOvergangsstønad.fomDato, + tom = periodeOvergangsstønad.tomDato, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/VedtaksperiodeProdusent.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/VedtaksperiodeProdusent.kt new file mode 100644 index 000000000..d94d1108c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/VedtaksperiodeProdusent.kt @@ -0,0 +1,480 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent + +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak.OMREGNING_18ÅR +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak.OMREGNING_6ÅR +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak.OMREGNING_SMÅBARNSTILLEGG +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak.SMÅBARNSTILLEGG +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrer +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombiner +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.slåSammenLike +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilDagEllerFørsteDagIPerioden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilLocalDateEllerNull +import no.nav.familie.ba.sak.kjerne.tidslinje.tilTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.ZipPadding +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.zipMedNeste +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.EØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.Vedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.utledEndringstidspunkt +import java.time.LocalDate + +fun genererVedtaksperioder( + grunnlagForVedtakPerioder: BehandlingsGrunnlagForVedtaksperioder, + grunnlagForVedtakPerioderForrigeBehandling: BehandlingsGrunnlagForVedtaksperioder?, + vedtak: Vedtak, + nåDato: LocalDate, +): List { + if (vedtak.behandling.resultat == Behandlingsresultat.FORTSATT_INNVILGET || vedtak.behandling.opprettetÅrsak.erOmregningsårsak()) { + return lagFortsattInnvilgetPeriode( + vedtak = vedtak, + andelTilkjentYtelseer = grunnlagForVedtakPerioder.andelerTilkjentYtelse, + nåDato = nåDato, + ) + } + + val grunnlagTidslinjePerPersonForrigeBehandling = + grunnlagForVedtakPerioderForrigeBehandling + ?.let { grunnlagForVedtakPerioderForrigeBehandling.utledGrunnlagTidslinjePerPerson() } + ?: emptyMap() + + val grunnlagTidslinjePerPerson = grunnlagForVedtakPerioder.utledGrunnlagTidslinjePerPerson() + + val perioderSomSkalBegrunnesBasertPåDenneOgForrigeBehandling = + finnPerioderSomSkalBegrunnes( + grunnlagTidslinjePerPerson = grunnlagTidslinjePerPerson, + grunnlagTidslinjePerPersonForrigeBehandling = grunnlagTidslinjePerPersonForrigeBehandling, + endringstidspunkt = vedtak.behandling.overstyrtEndringstidspunkt ?: utledEndringstidspunkt( + behandlingsGrunnlagForVedtaksperioder = grunnlagForVedtakPerioder, + behandlingsGrunnlagForVedtaksperioderForrigeBehandling = grunnlagForVedtakPerioderForrigeBehandling, + ), + ) + + val vedtaksperioder = + perioderSomSkalBegrunnesBasertPåDenneOgForrigeBehandling.map { it.tilVedtaksperiodeMedBegrunnelser(vedtak) } + + return if (grunnlagForVedtakPerioder.uregistrerteBarn.isNotEmpty()) { + vedtaksperioder.leggTilPeriodeForUregistrerteBarn(vedtak) + } else { + vedtaksperioder + } +} + +private fun List.leggTilPeriodeForUregistrerteBarn( + vedtak: Vedtak, +): List { + fun VedtaksperiodeMedBegrunnelser.leggTilAvslagUregistrertBarnBegrunnelse() = + when (vedtak.behandling.kategori) { + BehandlingKategori.EØS -> { + this.eøsBegrunnelser.add( + EØSBegrunnelse( + vedtaksperiodeMedBegrunnelser = this, + begrunnelse = EØSStandardbegrunnelse.AVSLAG_EØS_UREGISTRERT_BARN, + ), + ) + } + + BehandlingKategori.NASJONAL -> { + this.begrunnelser.add( + Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = this, + standardbegrunnelse = Standardbegrunnelse.AVSLAG_UREGISTRERT_BARN, + ), + ) + } + } + + val avslagsperiodeUtenDatoer = this.find { it.fom == null && it.tom == null } + + return if (avslagsperiodeUtenDatoer != null) { + avslagsperiodeUtenDatoer.leggTilAvslagUregistrertBarnBegrunnelse() + this + } else { + val avslagsperiode: VedtaksperiodeMedBegrunnelser = VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = null, + tom = null, + type = Vedtaksperiodetype.AVSLAG, + ).also { it.leggTilAvslagUregistrertBarnBegrunnelse() } + + this + avslagsperiode + } +} + +fun finnPerioderSomSkalBegrunnes( + grunnlagTidslinjePerPerson: Map, + grunnlagTidslinjePerPersonForrigeBehandling: Map, + endringstidspunkt: LocalDate, +): List, Måned>> { + val gjeldendeOgForrigeGrunnlagKombinert = kombinerGjeldendeOgForrigeGrunnlag( + grunnlagTidslinjePerPerson = grunnlagTidslinjePerPerson.mapValues { it.value.vedtaksperiodeGrunnlagForPerson }, + grunnlagTidslinjePerPersonForrigeBehandling = grunnlagTidslinjePerPersonForrigeBehandling.mapValues { it.value.vedtaksperiodeGrunnlagForPerson }, + ) + + val sammenslåttePerioderUtenEksplisittAvslag = gjeldendeOgForrigeGrunnlagKombinert + .slåSammenUtenEksplisitteAvslag() + .filtrerPåEndringstidspunkt(endringstidspunkt) + .slåSammenSammenhengendeOpphørsperioder() + + val eksplisitteAvslagsperioder = gjeldendeOgForrigeGrunnlagKombinert.utledEksplisitteAvslagsperioder() + + val overlappendeGenerelleAvslagPerioder = grunnlagTidslinjePerPerson.lagOverlappendeGenerelleAvslagsPerioder() + + return (overlappendeGenerelleAvslagPerioder + sammenslåttePerioderUtenEksplisittAvslag + eksplisitteAvslagsperioder) + .slåSammenAvslagOgReduksjonsperioderMedSammeFomOgTom() + .leggTilUendelighetPåSisteOpphørsPeriode() +} + +fun List, Måned>>.slåSammenSammenhengendeOpphørsperioder(): List, Måned>> { + val sortertePerioder = this + .sortedWith(compareBy({ it.fraOgMed }, { it.tilOgMed })) + + return sortertePerioder.fold(emptyList()) { acc: List, Måned>>, dennePerioden -> + val forrigePeriode = acc.lastOrNull() + + if (forrigePeriode != null && + !forrigePeriode.erPersonMedInnvilgedeVilkårIPeriode() && + !dennePerioden.erPersonMedInnvilgedeVilkårIPeriode() + ) { + acc.dropLast(1) + forrigePeriode.copy(tilOgMed = dennePerioden.tilOgMed) + } else { + acc + dennePerioden + } + } +} + +fun List, Måned>>.leggTilUendelighetPåSisteOpphørsPeriode(): List, Måned>> { + val sortertePerioder = this + .sortedWith(compareBy({ it.fraOgMed }, { it.tilOgMed })) + + val sistePeriode = sortertePerioder.lastOrNull() + val sistePeriodeInneholderEksplisittAvslag = + sistePeriode?.innhold?.any { it.gjeldende?.erEksplisittAvslag() == true } == true + return if (sistePeriode != null && + !sistePeriode.erPersonMedInnvilgedeVilkårIPeriode() && + !sistePeriodeInneholderEksplisittAvslag + ) { + sortertePerioder.dropLast(1) + sistePeriode.copy(tilOgMed = MånedTidspunkt.uendeligLengeTil()) + } else { + sortertePerioder + } +} + +private fun Periode, Måned>.erPersonMedInnvilgedeVilkårIPeriode() = + innhold != null && innhold.any { it.gjeldende is VedtaksperiodeGrunnlagForPersonVilkårInnvilget } + +private fun Map.lagOverlappendeGenerelleAvslagsPerioder() = + map { + it.value.overlappendeGenerelleAvslagVedtaksperiodeGrunnlagForPerson + }.kombiner { + it.map { grunnlagForPerson -> + GrunnlagForGjeldendeOgForrigeBehandling( + grunnlagForPerson, + false, + ) + }.toList() + }.perioder() + +private fun Collection, Måned>>.filtrerPåEndringstidspunkt( + endringstidspunkt: LocalDate, +) = this.filter { + (it.tilOgMed.tilLocalDateEllerNull() ?: TIDENES_ENDE).isSameOrAfter(endringstidspunkt) +} + +private fun List>.slåSammenUtenEksplisitteAvslag(): Collection, Måned>> { + val kombinerteAvslagOgReduksjonsperioder = this.map { grunnlagForDenneOgForrigeBehandlingTidslinje -> + grunnlagForDenneOgForrigeBehandlingTidslinje.filtrerIkkeNull { + val gjeldendeErIkkeInnvilgetIkkeAvslag = + it.gjeldende is VedtaksperiodeGrunnlagForPersonVilkårIkkeInnvilget && !it.gjeldende.erEksplisittAvslag + val gjeldendeErInnvilget = it.gjeldende is VedtaksperiodeGrunnlagForPersonVilkårInnvilget + val erReduksjonSidenForrigeBehandling = it.erReduksjonSidenForrigeBehandling + + gjeldendeErIkkeInnvilgetIkkeAvslag || gjeldendeErInnvilget || erReduksjonSidenForrigeBehandling + } + } + + return kombinerteAvslagOgReduksjonsperioder.kombiner { grunnlagTidslinje -> + grunnlagTidslinje.toList().takeIf { it.isNotEmpty() } + }.perioder() +} + +private fun List>.utledEksplisitteAvslagsperioder(): Collection, Måned>> { + val avslagsperioderPerPerson = this.map { it.filtrerErAvslagsperiode() } + .map { tidslinje -> tidslinje.map { it?.medVilkårSomHarEksplisitteAvslag() } } + .flatMap { it.splittVilkårPerPerson() } + .map { it.slåSammenLike() } + + val avslagsperioderMedSammeFomOgTom = avslagsperioderPerPerson + .flatMap { it.perioder() } + .groupBy { Pair(it.fraOgMed, it.tilOgMed) } + + return avslagsperioderMedSammeFomOgTom + .map { (fomTomPar, avslagMedSammeFomOgTom) -> + Periode( + fraOgMed = fomTomPar.first, + tilOgMed = fomTomPar.second, + innhold = avslagMedSammeFomOgTom.mapNotNull { it.innhold }, + ) + } +} + +private fun Tidslinje.splittVilkårPerPerson(): List> { + return perioder() + .mapNotNull { it.splittOppTilVilkårPerPerson() } + .flatten() + .groupBy({ it.first }, { it.second }) + .map { it.value.tilTidslinje() } +} + +private fun Periode.splittOppTilVilkårPerPerson(): List>>? { + if (innhold?.gjeldende == null) return null + + val vilkårPerPerson = + innhold.gjeldende.vilkårResultaterForVedtaksperiode.groupBy { it.aktørId } + + return vilkårPerPerson.map { (aktørId, vilkårresultaterForPersonIPeriode) -> + aktørId to this.copy( + innhold = this.innhold.copy( + gjeldende = innhold.gjeldende.kopier( + vilkårResultaterForVedtaksperiode = vilkårresultaterForPersonIPeriode, + ), + ), + ) + } +} + +private fun Tidslinje.filtrerErAvslagsperiode() = + filtrer { it?.gjeldende?.erEksplisittAvslag() == true } + +private fun GrunnlagForGjeldendeOgForrigeBehandling.medVilkårSomHarEksplisitteAvslag(): GrunnlagForGjeldendeOgForrigeBehandling { + return copy( + gjeldende = this.gjeldende?.kopier( + vilkårResultaterForVedtaksperiode = this.gjeldende + .vilkårResultaterForVedtaksperiode + .filter { it.erEksplisittAvslagPåSøknad == true }, + ), + ) +} + +/** + * Ønsker å dra med informasjon om forrige behandling i perioder der forrige behandling var oppfylt, men gjeldende + * ikke er det. + **/ +private fun kombinerGjeldendeOgForrigeGrunnlag( + grunnlagTidslinjePerPerson: Map>, + grunnlagTidslinjePerPersonForrigeBehandling: Map>, +): List> = + grunnlagTidslinjePerPerson.map { (aktørId, grunnlagstidslinje) -> + val grunnlagForrigeBehandling = grunnlagTidslinjePerPersonForrigeBehandling[aktørId] + + val ytelsestyperInnvilgetForrigeBehandlingTidslinje = + grunnlagForrigeBehandling?.map { it?.hentInnvilgedeYtelsestyper() } ?: TomTidslinje() + + val grunnlagTidslinjeMedInnvilgedeYtelsestyperForrigeBehandling = + grunnlagstidslinje.kombinerMed(ytelsestyperInnvilgetForrigeBehandlingTidslinje) { gjeldendePeriode, innvilgedeYtelsestyperForrigeBehandling -> + GjeldendeMedInnvilgedeYtelsestyperForrigeBehandling( + gjeldendePeriode, + innvilgedeYtelsestyperForrigeBehandling, + ) + } + + grunnlagTidslinjeMedInnvilgedeYtelsestyperForrigeBehandling.zipMedNeste(ZipPadding.FØR) + .map { + val forrigePeriode = it?.first + val gjeldende = it?.second + + val erReduksjonFraForrigeBehandlingPåMinstEnYtelsestype = + erReduksjonFraForrigeBehandlingPåMinstEnYtelsestype( + innvilgedeYtelsestyperForrigePeriode = forrigePeriode?.grunnlagForPerson?.hentInnvilgedeYtelsestyper(), + innvilgedeYtelsestyperForrigePeriodeForrigeBehandling = forrigePeriode?.innvilgedeYtelsestyperForrigeBehandling, + innvilgedeYtelsestyperDennePerioden = gjeldende?.grunnlagForPerson?.hentInnvilgedeYtelsestyper(), + innvilgedeYtelsestyperDennePeriodenForrigeBehandling = gjeldende?.innvilgedeYtelsestyperForrigeBehandling, + ) + + GrunnlagForGjeldendeOgForrigeBehandling( + gjeldende = gjeldende?.grunnlagForPerson, + erReduksjonSidenForrigeBehandling = erReduksjonFraForrigeBehandlingPåMinstEnYtelsestype, + + ) + }.slåSammenSammenhengendeOpphørsPerioder() + } + +data class GjeldendeMedInnvilgedeYtelsestyperForrigeBehandling( + val grunnlagForPerson: VedtaksperiodeGrunnlagForPerson?, + val innvilgedeYtelsestyperForrigeBehandling: Set?, +) + +private fun erReduksjonFraForrigeBehandlingPåMinstEnYtelsestype( + innvilgedeYtelsestyperForrigePeriode: Set?, + innvilgedeYtelsestyperForrigePeriodeForrigeBehandling: Set?, + innvilgedeYtelsestyperDennePerioden: Set?, + innvilgedeYtelsestyperDennePeriodenForrigeBehandling: Set?, +): Boolean { + return YtelseType.values().any { ytelseType -> + val ytelseInnvilgetDennePerioden = + innvilgedeYtelsestyperDennePerioden?.contains(ytelseType) ?: false + val ytelseInnvilgetForrigePeriode = + innvilgedeYtelsestyperForrigePeriode?.contains(ytelseType) ?: false + val ytelseInnvilgetDennePeriodenForrigeBehandling = + innvilgedeYtelsestyperDennePeriodenForrigeBehandling?.contains(ytelseType) ?: false + val ytelseInnvilgetForrigePeriodeForrigeBehandling = + innvilgedeYtelsestyperForrigePeriodeForrigeBehandling?.contains(ytelseType) ?: false + + !ytelseInnvilgetForrigePeriode && + !ytelseInnvilgetDennePerioden && + !ytelseInnvilgetForrigePeriodeForrigeBehandling && + ytelseInnvilgetDennePeriodenForrigeBehandling + } +} + +private fun Tidslinje.slåSammenSammenhengendeOpphørsPerioder(): Tidslinje { + val perioder = this.perioder().sortedBy { it.fraOgMed }.toList() + + return perioder.fold(emptyList()) { acc: List>, periode -> + val sistePeriode = acc.lastOrNull() + + val erVilkårInnvilgetForrigePeriode = + sistePeriode?.innhold?.gjeldende is VedtaksperiodeGrunnlagForPersonVilkårInnvilget + val erVilkårInnvilget = periode.innhold?.gjeldende is VedtaksperiodeGrunnlagForPersonVilkårInnvilget + + if (sistePeriode != null && + !erVilkårInnvilgetForrigePeriode && + !erVilkårInnvilget && + periode.innhold?.erReduksjonSidenForrigeBehandling != true && + periode.innhold?.gjeldende?.erEksplisittAvslag() != true && + sistePeriode.innhold?.gjeldende?.erEksplisittAvslag() != true + ) { + acc.dropLast(1) + sistePeriode.copy(tilOgMed = periode.tilOgMed) + } else { + acc + periode + } + }.tilTidslinje() +} + +fun Periode, Måned>.tilVedtaksperiodeMedBegrunnelser( + vedtak: Vedtak, +): VedtaksperiodeMedBegrunnelser = VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = fraOgMed.tilDagEllerFørsteDagIPerioden().tilLocalDateEllerNull(), + tom = tilOgMed.tilLocalDateEllerNull(), + type = this.tilVedtaksperiodeType(), +).let { vedtaksperiode -> + val begrunnelser = this.innhold?.flatMap { grunnlagForGjeldendeOgForrigeBehandling -> + grunnlagForGjeldendeOgForrigeBehandling.gjeldende?.vilkårResultaterForVedtaksperiode + ?.flatMap { it.standardbegrunnelser } ?: emptyList() + } ?: emptyList() + + vedtaksperiode.begrunnelser.addAll( + begrunnelser.filterIsInstance() + .map { Vedtaksbegrunnelse(vedtaksperiodeMedBegrunnelser = vedtaksperiode, standardbegrunnelse = it) }, + ) + + vedtaksperiode.eøsBegrunnelser.addAll( + begrunnelser.filterIsInstance() + .map { EØSBegrunnelse(vedtaksperiodeMedBegrunnelser = vedtaksperiode, begrunnelse = it) }, + ) + + vedtaksperiode +} + +private fun Periode, Måned>.tilVedtaksperiodeType(): Vedtaksperiodetype { + val erUtbetalingsperiode = + this.innhold != null && this.innhold.any { it.gjeldende?.erInnvilget() == true } + val erAvslagsperiode = this.innhold != null && this.innhold.all { it.gjeldende?.erEksplisittAvslag() == true } + + return when { + erUtbetalingsperiode -> if (this.innhold?.any { it.erReduksjonSidenForrigeBehandling } == true) { + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING + } else { + Vedtaksperiodetype.UTBETALING + } + + erAvslagsperiode -> Vedtaksperiodetype.AVSLAG + + else -> Vedtaksperiodetype.OPPHØR + } +} + +data class GrupperingskriterierForVedtaksperioder( + val fom: Tidspunkt, + val tom: Tidspunkt, + val periodeInneholderInnvilgelse: Boolean, +) + +private fun List, Måned>>.slåSammenAvslagOgReduksjonsperioderMedSammeFomOgTom() = + this.groupBy { periode -> + GrupperingskriterierForVedtaksperioder( + fom = periode.fraOgMed, + tom = periode.tilOgMed, + periodeInneholderInnvilgelse = periode.innhold?.any { it.gjeldende is VedtaksperiodeGrunnlagForPersonVilkårInnvilget } == true, + ) + }.map { (grupperingskriterier, verdi) -> + Periode( + fraOgMed = grupperingskriterier.fom, + tilOgMed = grupperingskriterier.tom, + innhold = verdi.mapNotNull { periode -> periode.innhold }.flatten(), + ) + } + +fun lagFortsattInnvilgetPeriode( + vedtak: Vedtak, + andelTilkjentYtelseer: List, + nåDato: LocalDate, +): List { + val behandling = vedtak.behandling + val erAutobrevFor6År18ÅrEllerSmåbarnstillegg = behandling.opprettetÅrsak in listOf( + OMREGNING_6ÅR, + OMREGNING_18ÅR, + SMÅBARNSTILLEGG, + OMREGNING_SMÅBARNSTILLEGG, + ) + + val (fom, tom) = if (erAutobrevFor6År18ÅrEllerSmåbarnstillegg) { + Pair( + nåDato.førsteDagIInneværendeMåned(), + finnTomDatoIFørsteUtbetalingsintervallFraInneværendeMåned(behandling.id, andelTilkjentYtelseer, nåDato), + ) + } else { + Pair(null, null) + } + + return listOf( + VedtaksperiodeMedBegrunnelser( + fom = fom, + tom = tom, + vedtak = vedtak, + type = Vedtaksperiodetype.FORTSATT_INNVILGET, + ), + ) +} + +private fun finnTomDatoIFørsteUtbetalingsintervallFraInneværendeMåned( + behandlingId: Long, + andelTilkjentYtelses: List, + nåDato: LocalDate, +): LocalDate = + andelTilkjentYtelses + .filter { it.stønadFom <= nåDato.toYearMonth() && it.stønadTom >= nåDato.toYearMonth() } + .minByOrNull { it.stønadTom }?.stønadTom?.sisteDagIInneværendeMåned() + ?: error("Fant ikke andel for tilkjent ytelse inneværende måned for behandling $behandlingId.") diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/Vilk\303\245r relevante for personer sine andeler.png" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/Vilk\303\245r relevante for personer sine andeler.png" new file mode 100644 index 000000000..90a83deee Binary files /dev/null and "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/vedtaksperiodeProdusent/Vilk\303\245r relevante for personer sine andeler.png" differ diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/verge/Verge.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/verge/Verge.kt new file mode 100644 index 000000000..893159b86 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/verge/Verge.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.kjerne.verge + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.OneToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase + +// Denne tabellen brukes for å lagre verge detaljer kun til institusjon +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Verge") +@Table(name = "VERGE") +data class Verge( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "verge_seq_generator") + @SequenceGenerator(name = "verge_seq_generator", sequenceName = "verge_seq", allocationSize = 50) + val id: Long = 0, + + @Column(name = "ident", updatable = true, length = 20) + var ident: String, + + @OneToOne(optional = false) + @JoinColumn( + name = "fk_behandling_id", + nullable = false, + updatable = false, + ) + val behandling: Behandling, +) : BaseEntitet() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/verge/VergeRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/verge/VergeRepository.kt new file mode 100644 index 000000000..b4d6100cf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/verge/VergeRepository.kt @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.kjerne.verge + +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface VergeRepository : JpaRepository { + fun findByBehandling(behandling: Behandling): Verge? +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/verge/VergeService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/verge/VergeService.kt new file mode 100644 index 000000000..acb33dd34 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/verge/VergeService.kt @@ -0,0 +1,21 @@ +package no.nav.familie.ba.sak.kjerne.verge + +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class VergeService( + val vergeRepository: VergeRepository, +) { + + @Transactional + fun oppdaterVergeForBehandling(behandling: Behandling, verge: Verge) { + val vergeRegistrertFraFør = vergeRepository.findByBehandling(behandling) + if (vergeRegistrertFraFør != null) { + vergeRepository.delete(vergeRegistrertFraFør) + vergeRepository.flush() + } + vergeRepository.save(verge) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/AnnenVurderingService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/AnnenVurderingService.kt" new file mode 100644 index 000000000..ba6fe9a4d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/AnnenVurderingService.kt" @@ -0,0 +1,56 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.feilHvis +import no.nav.familie.ba.sak.ekstern.restDomene.RestAnnenVurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurderingRepository +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurderingType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AnnenVurderingService( + private val annenVurderingRepository: AnnenVurderingRepository, +) { + + fun hent(personResultat: PersonResultat, annenVurderingType: AnnenVurderingType): AnnenVurdering? = + annenVurderingRepository.findBy( + personResultat = personResultat, + type = annenVurderingType, + ) + + fun hent(annenVurderingId: Long): AnnenVurdering = annenVurderingRepository.findById(annenVurderingId) + .orElseThrow { error("Annen vurdering med id $annenVurderingId finnes ikke i db") } + + @Transactional + fun endreAnnenVurdering( + behandlingId: Long, + annenVurderingId: Long, + restAnnenVurdering: RestAnnenVurdering, + ) { + val vurdering = hent(annenVurderingId = annenVurderingId) + val behandling = vurdering.personResultat.vilkårsvurdering.behandling + + val behandlingIdForVurdering = behandling.id + feilHvis(behandlingIdForVurdering != behandlingId) { + "Prøver å oppdatere en vurdering=$annenVurderingId koblet til en annen($behandlingIdForVurdering) behandling enn $behandlingId" + } + annenVurderingRepository.save( + vurdering.also { + it.resultat = restAnnenVurdering.resultat + it.begrunnelse = restAnnenVurdering.begrunnelse + it.type = restAnnenVurdering.type + }, + ) + } +} + +fun PersonResultat.leggTilBlankAnnenVurdering(annenVurderingType: AnnenVurderingType) { + this.andreVurderinger.add( + AnnenVurdering( + personResultat = this, + type = annenVurderingType, + ), + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/README.md" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/README.md" new file mode 100644 index 000000000..9f87792c6 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/README.md" @@ -0,0 +1,52 @@ +# Vilkårsvudering + +## Hvert vilkår sett separat +### Forskyvning fra vilkår til periode med rett +Generelle regler: +- Fom: starter neste måned (eks. vilkår fom=15.mars, som i tidslinjen vil gi oppstart i april) +- Tom: slutter samme måned (eks. vilkår tom=18.oktober som i tidslinjen vil gi siste oppfylte måned oktober) + +_Eksempel: Vilkår oppfylt 15. mars - 18. oktober gir rett i perioden april til oktober_ + +Unntak (forklart under): +- Under 18-vilkåret +- Back-2-back-perioder + +### Under 18-vilkåret +- Fom: følger generelle regler (altså starter neste måned) +- Tom: + - Normalt settes tom-datoen på under 18-vilkåret til dagen før barnet fyller 18 år. Da slutter perioden med rett måneden FØR (eks. barn fyller 18 år 16.oktober og tom=15.oktober, da vil tidslinjen være oppfylt til og med september) + - I noen tilfeller setter tom-datoen til noe annet (f.eks. hvis barnet dør før fylt 18 år). Da gjelder dette: + - Tom er i samme måned som 18-årsdag? Perioden slutter måneden FØR + - Tom er tidligere enn måned hvor barn fyller 18? Perioden slutter samme måned som tom (altså likt som generelle regler) + +_Eksempel: Barn fyller 18 år 16. oktober, og under 18-vilkåret er oppfylt 15. mars - 16.oktober. Det gir rett i perioden april til september._ + +### Back-2-back-perioder + +For back-2-back-perioder gjelder dette for alle vilkår utenom bor med søker-vilkåret: + +| 2020-04-30 | 2020-05-01 | Resultat 2020-05 | +|------------|------------|------------------| +| X | Y | Y | + +Eksempel: Vilkår oppfylt +- 15.mars - 30.juni (nasjonal) +- 1.juli - 18. oktober (EØS) + +Gir rett i disse periodene +- April til juni (nasjonal) +- Juli til oktober (EØS) + +#### Bor med søker-vilkåret: + +| 2020-04-30 | 2020-05-01 | Resultat 2020-05 | +|---------------|----------------|--------------------------------| +| Oppfylt Delt | Oppfylt Fullt | Oppfylt Fullt | +| Oppfylt Fullt | Opppfylt Delt | Oppfylt Fullt | +| Oppfylt Fullt | Opppfylt Fullt | Oppfylt Fullt | +| Oppfylt Delt | Oppfylt Delt | Oppfylt Delt (pga forrige mnd) | +| Oppfylt Delt | Ikke oppfylt | Tomt | +| Oppfylt Fullt | Ikke oppfylt | Tomt | +| Ikke oppfylt | Oppfylt Delt | Tomt | +| Ikke oppfylt | Oppfylt Fullt | Tomt | \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rController.kt" new file mode 100644 index 000000000..725d33dba --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rController.kt" @@ -0,0 +1,154 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.ekstern.restDomene.RestAnnenVurdering +import no.nav.familie.ba.sak.ekstern.restDomene.RestNyttVilkår +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestSlettVilkår +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.ekstern.restDomene.RestVedtakBegrunnelseTilknyttetVilkår +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.kjerne.steg.TilbakestillBehandlingService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/vilkaarsvurdering") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class VilkårController( + private val vilkårService: VilkårService, + private val annenVurderingService: AnnenVurderingService, + private val personidentService: PersonidentService, + private val tilgangService: TilgangService, + private val vilkårsvurderingService: VilkårsvurderingService, + private val utvidetBehandlingService: UtvidetBehandlingService, + private val tilbakestillBehandlingService: TilbakestillBehandlingService, +) { + + @PutMapping(path = ["/{behandlingId}/{vilkaarId}"]) + fun endreVilkår( + @PathVariable behandlingId: Long, + @PathVariable vilkaarId: Long, + @RequestBody restPersonResultat: RestPersonResultat, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "endre vilkår", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + vilkårService.endreVilkår( + behandlingId = behandlingId, + vilkårId = vilkaarId, + restPersonResultat = restPersonResultat, + ) + tilbakestillBehandlingService.resettStegVedEndringPåVilkår(behandlingId) + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @PutMapping(path = ["/{behandlingId}/annenvurdering/{annenVurderingId}"]) + fun endreAnnenVurdering( + @PathVariable behandlingId: Long, + @PathVariable annenVurderingId: Long, + @RequestBody restAnnenVurdering: RestAnnenVurdering, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "Annen vurdering", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + annenVurderingService.endreAnnenVurdering( + behandlingId = behandlingId, + annenVurderingId = annenVurderingId, + restAnnenVurdering = restAnnenVurdering, + ) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @DeleteMapping(path = ["/{behandlingId}/{vilkaarId}"]) + fun slettVilkårsperiode( + @PathVariable behandlingId: Long, + @PathVariable vilkaarId: Long, + @RequestBody personIdent: String, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.DELETE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "slette vilkårsperiode", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + val aktør = personidentService.hentAktør(personIdent) + + vilkårService.deleteVilkårsperiode( + behandlingId = behandlingId, + vilkårId = vilkaarId, + aktør = aktør, + ) + + tilbakestillBehandlingService.resettStegVedEndringPåVilkår(behandlingId) + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @DeleteMapping(path = ["/{behandlingId}/vilkaar"]) + fun slettVilkår( + @PathVariable behandlingId: Long, + @RequestBody restSlettVilkår: RestSlettVilkår, + ): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.DELETE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "slette vilkår", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + vilkårService.deleteVilkår(behandlingId, restSlettVilkår) + + tilbakestillBehandlingService.resettStegVedEndringPåVilkår(behandlingId) + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } + + @PostMapping(path = ["/{behandlingId}"]) + fun nyttVilkår(@PathVariable behandlingId: Long, @RequestBody restNyttVilkår: RestNyttVilkår): ResponseEntity> { + tilgangService.validerTilgangTilBehandling(behandlingId = behandlingId, event = AuditLoggerEvent.UPDATE) + tilgangService.verifiserHarTilgangTilHandling( + minimumBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + handling = "legge til vilkår", + ) + tilgangService.validerKanRedigereBehandling(behandlingId) + + vilkårService.postVilkår(behandlingId, restNyttVilkår) + + tilbakestillBehandlingService.resettStegVedEndringPåVilkår(behandlingId) + return ResponseEntity.ok( + Ressurs.success( + utvidetBehandlingService + .lagRestUtvidetBehandling(behandlingId = behandlingId), + ), + ) + } + + @GetMapping(path = ["/vilkaarsbegrunnelser"]) + fun hentTeksterForVilkårsbegrunnelser(): ResponseEntity>>> { + return ResponseEntity.ok(Ressurs.success(vilkårsvurderingService.hentVilkårsbegrunnelser())) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rResultatUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rResultatUtils.kt" new file mode 100644 index 000000000..2be2d4c4d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rResultatUtils.kt" @@ -0,0 +1,47 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.til18ÅrsVilkårsdato +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import java.time.LocalDate + +object VilkårResultatUtils { + + fun genererVilkårResultatForEtVilkårPåEnPerson( + person: Person, + eldsteBarnSinFødselsdato: LocalDate, + personResultat: PersonResultat, + vilkår: Vilkår, + annenForelder: Person? = null, + ): VilkårResultat { + val automatiskVurderingResultat = vilkår.vurderVilkår( + person = person, + annenForelder = annenForelder, + vurderFra = eldsteBarnSinFødselsdato, + ) + + val fom = if (eldsteBarnSinFødselsdato >= person.fødselsdato) eldsteBarnSinFødselsdato else person.fødselsdato + + val tom: LocalDate? = + if (vilkår == Vilkår.UNDER_18_ÅR) { + person.fødselsdato.til18ÅrsVilkårsdato() + } else { + null + } + + return VilkårResultat( + regelInput = automatiskVurderingResultat.regelInput, + personResultat = personResultat, + erAutomatiskVurdert = true, + resultat = automatiskVurderingResultat.resultat, + vilkårType = vilkår, + periodeFom = fom, + periodeTom = tom, + begrunnelse = "Vurdert og satt automatisk: ${automatiskVurderingResultat.evaluering.begrunnelse}", + sistEndretIBehandlingId = personResultat.vilkårsvurdering.behandling.id, + evalueringÅrsaker = automatiskVurderingResultat.evaluering.evalueringÅrsaker.map { it.toString() }, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rService.kt" new file mode 100644 index 000000000..f356ac090 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rService.kt" @@ -0,0 +1,214 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.ekstern.restDomene.RestNyttVilkår +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestSlettVilkår +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPersonResultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.BehandlingstemaService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingUtils.muterPersonResultatDelete +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingUtils.muterPersonResultatPost +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingUtils.muterPersonVilkårResultaterPut +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class VilkårService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val behandlingstemaService: BehandlingstemaService, + private val behandlingService: BehandlingService, + private val vilkårsvurderingService: VilkårsvurderingService, + private val personidentService: PersonidentService, + private val persongrunnlagService: PersongrunnlagService, +) { + + fun hentVilkårsvurdering(behandlingId: Long): Vilkårsvurdering? = vilkårsvurderingService.hentAktivForBehandling( + behandlingId = behandlingId, + ) + + fun hentVilkårsvurderingThrows(behandlingId: Long): Vilkårsvurdering = + hentVilkårsvurdering(behandlingId) ?: throw Feil( + message = "Fant ikke aktiv vilkårsvurdering for behandling $behandlingId", + frontendFeilmelding = fantIkkeAktivVilkårsvurderingFeilmelding, + ) + + @Transactional + fun endreVilkår( + behandlingId: Long, + vilkårId: Long, + restPersonResultat: RestPersonResultat, + ): List { + val vilkårsvurdering = hentVilkårsvurderingThrows(behandlingId) + + val restVilkårResultat = restPersonResultat.vilkårResultater.singleOrNull { it.id == vilkårId } + ?: throw Feil("Fant ikke vilkårResultat med id $vilkårId ved oppdatering av vilkår") + + validerResultatBegrunnelse(restVilkårResultat) + + val personResultat = + finnPersonResultatForPersonThrows(vilkårsvurdering.personResultater, restPersonResultat.personIdent) + + muterPersonVilkårResultaterPut(personResultat, restVilkårResultat) + + val vilkårResultat = personResultat.vilkårResultater.singleOrNull { it.id == vilkårId } + ?: error("Finner ikke vilkår med vilkårId $vilkårId på personResultat ${personResultat.id}") + + vilkårResultat.also { + it.standardbegrunnelser = restVilkårResultat.avslagBegrunnelser ?: emptyList() + } + + val migreringsdatoPåFagsak = + behandlingService.hentMigreringsdatoPåFagsak(fagsakId = vilkårsvurdering.behandling.fagsak.id) + validerVilkårStarterIkkeFørMigreringsdatoForMigreringsbehandling( + vilkårsvurdering, + vilkårResultat, + migreringsdatoPåFagsak, + ) + + return vilkårsvurderingService.oppdater(vilkårsvurdering).personResultater.map { it.tilRestPersonResultat() } + } + + @Transactional + fun deleteVilkårsperiode(behandlingId: Long, vilkårId: Long, aktør: Aktør): List { + val vilkårsvurdering = hentVilkårsvurderingThrows(behandlingId) + + val personResultat = + finnPersonResultatForPersonThrows(vilkårsvurdering.personResultater, aktør.aktivFødselsnummer()) + + muterPersonResultatDelete(personResultat, vilkårId) + + return vilkårsvurderingService.oppdater(vilkårsvurdering).personResultater.map { it.tilRestPersonResultat() } + } + + @Transactional + fun deleteVilkår(behandlingId: Long, restSlettVilkår: RestSlettVilkår): List { + val vilkårsvurdering = hentVilkårsvurderingThrows(behandlingId) + val personResultat = + finnPersonResultatForPersonThrows(vilkårsvurdering.personResultater, restSlettVilkår.personIdent) + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + if (!behandling.kanLeggeTilOgFjerneUtvidetVilkår() || + Vilkår.UTVIDET_BARNETRYGD != restSlettVilkår.vilkårType || + finnesUtvidetBarnetrydIForrigeBehandling(behandling, restSlettVilkår.personIdent) + ) { + throw FunksjonellFeil( + melding = "Vilkår ${restSlettVilkår.vilkårType.beskrivelse} kan ikke slettes " + + "for behandling $behandlingId", + frontendFeilmelding = "Vilkår ${restSlettVilkår.vilkårType.beskrivelse} kan ikke slettes " + + "for behandling $behandlingId", + ) + } + + personResultat.vilkårResultater.filter { it.vilkårType == restSlettVilkår.vilkårType } + .forEach { personResultat.removeVilkårResultat(it.id) } + + if (restSlettVilkår.vilkårType == Vilkår.UTVIDET_BARNETRYGD) { + behandlingstemaService.oppdaterBehandlingstema( + behandling = behandling, + overstyrtUnderkategori = BehandlingUnderkategori.ORDINÆR, + ) + } + + return vilkårsvurderingService.oppdater(vilkårsvurdering).personResultater.map { it.tilRestPersonResultat() } + } + + @Transactional + fun postVilkår(behandlingId: Long, restNyttVilkår: RestNyttVilkår): List { + val vilkårsvurdering = hentVilkårsvurderingThrows(behandlingId) + + val behandling = vilkårsvurdering.behandling + + if (restNyttVilkår.vilkårType == Vilkår.UTVIDET_BARNETRYGD) { + validerFørLeggeTilUtvidetBarnetrygd(behandling, restNyttVilkår, vilkårsvurdering) + + behandlingstemaService.oppdaterBehandlingstema( + behandling = behandling, + overstyrtUnderkategori = BehandlingUnderkategori.UTVIDET, + ) + } + + val personResultat = + finnPersonResultatForPersonThrows(vilkårsvurdering.personResultater, restNyttVilkår.personIdent) + + muterPersonResultatPost(personResultat, restNyttVilkår.vilkårType) + + return vilkårsvurderingService.oppdater(vilkårsvurdering).personResultater.map { it.tilRestPersonResultat() } + } + + private fun validerFørLeggeTilUtvidetBarnetrygd( + behandling: Behandling, + restNyttVilkår: RestNyttVilkår, + vilkårsvurdering: Vilkårsvurdering, + ) { + if (!behandling.kanLeggeTilOgFjerneUtvidetVilkår() && !harUtvidetVilkår(vilkårsvurdering)) { + throw FunksjonellFeil( + melding = "${restNyttVilkår.vilkårType.beskrivelse} kan ikke legges til for behandling ${behandling.id} " + + "med behandlingType ${behandling.type.visningsnavn}", + frontendFeilmelding = "${restNyttVilkår.vilkårType.beskrivelse} kan ikke legges til " + + "for behandling ${behandling.id} med behandlingType ${behandling.type.visningsnavn}", + ) + } + + val personopplysningGrunnlag = persongrunnlagService.hentAktivThrows(behandling.id) + if (personopplysningGrunnlag.søkerOgBarn + .single { it.aktør == personidentService.hentAktør(restNyttVilkår.personIdent) }.type != PersonType.SØKER + ) { + throw FunksjonellFeil( + melding = "${Vilkår.UTVIDET_BARNETRYGD.beskrivelse} kan ikke legges til for BARN", + frontendFeilmelding = "${Vilkår.UTVIDET_BARNETRYGD.beskrivelse} kan ikke legges til for BARN", + ) + } + } + + private fun harUtvidetVilkår(vilkårsvurdering: Vilkårsvurdering): Boolean = + vilkårsvurdering.personResultater.find { it.erSøkersResultater() }?.vilkårResultater + ?.any { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } == true + + private fun finnesUtvidetBarnetrydIForrigeBehandling(behandling: Behandling, personIdent: String): Boolean { + val forrigeBehandlingSomErVedtatt = + behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling) + if (forrigeBehandlingSomErVedtatt != null) { + val forrigeBehandlingsvilkårsvurdering = + hentVilkårsvurdering(forrigeBehandlingSomErVedtatt.id) ?: throw Feil( + message = "Forrige behandling $${forrigeBehandlingSomErVedtatt.id} " + + "har ikke en aktiv vilkårsvurdering", + ) + val aktør = personidentService.hentAktør(personIdent) + return forrigeBehandlingsvilkårsvurdering.personResultater.single { it.aktør == aktør } + .vilkårResultater.any { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + } + return false + } + + private fun finnPersonResultatForPersonThrows( + personResultater: Set, + personIdent: String, + ): PersonResultat { + val aktør = personidentService.hentAktør(personIdent) + return personResultater.find { it.aktør == aktør } ?: throw Feil( + message = fantIkkeVilkårsvurderingForPersonFeilmelding, + frontendFeilmelding = "Fant ikke vilkårsvurdering for person med ident $personIdent", + ) + } + + companion object { + const val fantIkkeAktivVilkårsvurderingFeilmelding = "Fant ikke aktiv vilkårsvurdering" + const val fantIkkeVilkårsvurderingForPersonFeilmelding = "Fant ikke vilkårsvurdering for person" + } +} + +fun Vilkår.gjelderAlltidFraBarnetsFødselsdato() = this == Vilkår.GIFT_PARTNERSKAP || this == Vilkår.UNDER_18_ÅR + +fun SIVILSTAND.somForventetHosBarn() = this == SIVILSTAND.UOPPGITT || this == SIVILSTAND.UGIFT diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingForskyvningUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingForskyvningUtils.kt" new file mode 100644 index 000000000..eea7c95d6 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingForskyvningUtils.kt" @@ -0,0 +1,159 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.erUnder18ÅrVilkårTidslinje +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombiner +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.slåSammenLike +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.beskjærEtter +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.tilMånedFraMånedsskifteIkkeNull +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.tilTidslinje +import java.time.LocalDate + +object VilkårsvurderingForskyvningUtils { + fun Set.tilTidslinjeForSplitt( + personerIPersongrunnlag: List, + fagsakType: FagsakType, + ): Tidslinje, Måned> { + val tidslinjerPerPerson = this.map { personResultat -> + val person = personerIPersongrunnlag.find { it.aktør == personResultat.aktør } + ?: throw Feil("Finner ikke person med aktørId=${personResultat.aktør.aktørId} i persongrunnlaget ved generering av tidslinje for splitt") + personResultat.tilTidslinjeForSplittForPerson(person = person, fagsakType = fagsakType) + } + + return tidslinjerPerPerson.kombiner { it.filterNotNull().flatten() }.filtrerIkkeNull().slåSammenLike() + } + + fun PersonResultat.tilTidslinjeForSplittForPerson( + person: Person, + fagsakType: FagsakType, + ): Tidslinje, Måned> { + val tidslinjer = this.vilkårResultater.tilForskjøvetTidslinjerForHvertOppfylteVilkår(person.fødselsdato) + + return tidslinjer.kombiner { + alleOrdinæreVilkårErOppfyltEllerNull( + vilkårResultater = it, + personType = person.type, + fagsakType = fagsakType, + ) + } + .filtrerIkkeNull().slåSammenLike() + } + + /** + * Extention-funksjon som tar inn et sett med vilkårResultater og returnerer en forskjøvet måned-basert tidslinje for hvert vilkår + * Se readme-fil for utdypende forklaring av logikken for hvert vilkår + * */ + fun Collection.tilForskjøvetTidslinjerForHvertOppfylteVilkår(fødselsdato: LocalDate): List> { + return this.groupBy { it.vilkårType }.map { (vilkår, vilkårResultater) -> + vilkårResultater.tilForskjøvetTidslinjeForOppfyltVilkår(vilkår, fødselsdato) + } + } + + fun Collection.tilForskjøvedeVilkårTidslinjer(fødselsdato: LocalDate): List> { + return this.groupBy { it.vilkårType }.map { (vilkår, vilkårResultater) -> + vilkårResultater.tilForskjøvetTidslinje(vilkår, fødselsdato) + } + } + + fun Collection.tilForskjøvetTidslinjeForOppfyltVilkår(vilkår: Vilkår, fødselsdato: LocalDate?): Tidslinje { + if (this.isEmpty()) return TomTidslinje() + + val tidslinje = this.lagForskjøvetTidslinjeForOppfylteVilkår(vilkår) + + return tidslinje.beskjærPå18ÅrHvisUnder18ÅrVilkår(vilkår = vilkår, fødselsdato = fødselsdato) + } + + fun Collection.tilForskjøvetTidslinjeForOppfyltVilkårForVoksenPerson(vilkår: Vilkår): Tidslinje { + if (vilkår == Vilkår.UNDER_18_ÅR) throw Feil("Funksjonen skal ikke brukes for under 18 vilkåret") + + return this.lagForskjøvetTidslinjeForOppfylteVilkår(vilkår) + } + + fun Collection.lagForskjøvetTidslinjeForOppfylteVilkår(vilkår: Vilkår): Tidslinje { + return this + .filter { it.vilkårType == vilkår && it.erOppfylt() } + .tilTidslinje() + .tilMånedFraMånedsskifteIkkeNull { innholdSisteDagForrigeMåned, innholdFørsteDagDenneMåned -> + when { + !innholdSisteDagForrigeMåned.erOppfylt() || !innholdFørsteDagDenneMåned.erOppfylt() -> null + vilkår == Vilkår.BOR_MED_SØKER && innholdFørsteDagDenneMåned.erDeltBosted() -> innholdSisteDagForrigeMåned + else -> innholdFørsteDagDenneMåned + } + } + } + + fun Collection.tilForskjøvetTidslinje(vilkår: Vilkår, fødselsdato: LocalDate): Tidslinje { + val tidslinje = this.lagForskjøvetTidslinje(vilkår) + + return tidslinje.beskjærPå18ÅrHvisUnder18ÅrVilkår(vilkår = vilkår, fødselsdato = fødselsdato) + } + + private fun Collection.lagForskjøvetTidslinje(vilkår: Vilkår): Tidslinje { + return this + .filter { it.vilkårType == vilkår } + .tilTidslinje() + .tilMånedFraMånedsskifteIkkeNull { innholdSisteDagForrigeMåned, innholdFørsteDagDenneMåned -> + when { + vilkår == Vilkår.BOR_MED_SØKER && innholdFørsteDagDenneMåned.erDeltBosted() -> innholdSisteDagForrigeMåned + !innholdSisteDagForrigeMåned.erOppfylt() -> innholdSisteDagForrigeMåned + else -> innholdFørsteDagDenneMåned + } + } + } + + private fun Tidslinje.beskjærPå18ÅrHvisUnder18ÅrVilkår( + vilkår: Vilkår, + fødselsdato: LocalDate?, + ): Tidslinje { + return if (vilkår == Vilkår.UNDER_18_ÅR) { + this.beskjærPå18År(fødselsdato = fødselsdato ?: throw Feil("Mangler fødselsdato, men prøver å beskjære på 18-år vilkåret")) + } else { + this + } + } + + internal fun Tidslinje.beskjærPå18År(fødselsdato: LocalDate): Tidslinje { + val erUnder18Tidslinje = erUnder18ÅrVilkårTidslinje(fødselsdato) + return this.beskjærEtter(erUnder18Tidslinje) + } + + private fun VilkårResultat.erDeltBosted() = + this.utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.DELT_BOSTED) || this.utdypendeVilkårsvurderinger.contains( + UtdypendeVilkårsvurdering.DELT_BOSTED_SKAL_IKKE_DELES, + ) + + private fun alleOrdinæreVilkårErOppfyltEllerNull( + vilkårResultater: Iterable, + personType: PersonType, + fagsakType: FagsakType, + ): List? { + return if (vilkårResultater.alleOrdinæreVilkårErOppfylt(personType, fagsakType)) { + vilkårResultater.filterNotNull() + } else { + null + } + } + + fun Iterable.alleOrdinæreVilkårErOppfylt(personType: PersonType, fagsakType: FagsakType): Boolean { + val alleVilkårForPersonType = Vilkår.hentVilkårFor( + personType = personType, + fagsakType = fagsakType, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ) + return this.map { it.vilkårType } + .containsAll(alleVilkårForPersonType) && this.all { it.resultat == Resultat.OPPFYLT } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingMetrics.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingMetrics.kt" new file mode 100644 index 000000000..dda7e7c20 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingMetrics.kt" @@ -0,0 +1,171 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall.VilkårIkkeOppfyltÅrsak +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall.VilkårKanskjeOppfyltÅrsak +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall.VilkårOppfyltÅrsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonEnkel +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component + +@Component +class VilkårsvurderingMetrics( + private val persongrunnlagService: PersongrunnlagService, +) { + + private val vilkårsvurderingUtfall = mutableMapOf>() + private val vilkårsvurderingFørsteUtfall = mutableMapOf>() + + val personTypeToDisplayedType = mapOf( + PersonType.SØKER to "Mor", + PersonType.BARN to "Barn", + PersonType.ANNENPART to "Medforelder", + ) + + enum class VilkårTellerType(val navn: String) { + UTFALL("familie.ba.behandling.vilkaarsvurdering"), + FØRSTEUTFALL("familie.ba.behandling.vilkaarsvurdering.foerstutfall"), + } + + init { + initVilkårMetrikker(VilkårTellerType.UTFALL, vilkårsvurderingUtfall) + initVilkårMetrikker(VilkårTellerType.FØRSTEUTFALL, vilkårsvurderingFørsteUtfall) + } + + private fun initVilkårMetrikker( + vilkårTellerType: VilkårTellerType, + utfallMap: MutableMap>, + ) { + PersonType.values().forEach { personType -> + val vilkårUtfallMap = mutableMapOf() + listOf( + Pair(Resultat.IKKE_OPPFYLT, VilkårIkkeOppfyltÅrsak.values()), + Pair(Resultat.IKKE_VURDERT, VilkårKanskjeOppfyltÅrsak.values()), + Pair(Resultat.OPPFYLT, VilkårOppfyltÅrsak.values()), + ) + .forEach { (resultat, årsaker) -> + årsaker + .forEach { årsak -> + if (vilkårUtfallMap[årsak.toString()] != null) { + error("Årsak $årsak deler navn med minst en annen årsak") + } + + vilkårUtfallMap[årsak.toString()] = + Metrics.counter( + vilkårTellerType.navn, + "vilkaar", + årsak.hentIdentifikator(), + "resultat", + resultat.name, + "personType", + personTypeToDisplayedType[personType], + "beskrivelse", + årsak.hentMetrikkBeskrivelse(), + ) + } + } + + utfallMap[personType] = vilkårUtfallMap + } + } + + fun tellMetrikker(vilkårsvurdering: Vilkårsvurdering) { + val personer = persongrunnlagService.hentSøkerOgBarnPåBehandling(vilkårsvurdering.behandling.id) + ?: error("Finner ikke aktivt persongrunnlag ved telling av metrikker") + + vilkårsvurdering.personResultater.forEach { personResultat -> + val person = personer.firstOrNull { it.aktør == personResultat.aktør } + ?: error("Finner ikke person") + + val negativeVilkår = personResultat.vilkårResultater.filter { vilkårResultat -> + vilkårResultat.resultat == Resultat.IKKE_OPPFYLT + } + + if (negativeVilkår.isNotEmpty()) { + logger.info("Behandling: ${vilkårsvurdering.behandling.id}, personType=${person.type}. Vilkår som får negativt resultat og årsakene: ${negativeVilkår.map { "${it.vilkårType}=${it.evalueringÅrsaker}" }}.") + secureLogger.info("Behandling: ${vilkårsvurdering.behandling.id}, person=${person.aktør.aktivFødselsnummer()}. Vilkår som får negativt resultat og årsakene: ${negativeVilkår.map { "${it.vilkårType}=${it.evalueringÅrsaker}" }}.") + } + + personResultat.vilkårResultater.forEach { vilkårResultat -> + vilkårResultat.evalueringÅrsaker.forEach { årsak -> + vilkårsvurderingUtfall[person.type]?.get(årsak)?.increment() + } + } + } + + økTellereForStansetIAutomatiskVilkårsvurdering(vilkårsvurdering) + } + + private fun økTellereForStansetIAutomatiskVilkårsvurdering(vilkårsvurdering: Vilkårsvurdering) { + Vilkår.hentFødselshendelseVilkårsreglerRekkefølge() + .map { mapVilkårTilVilkårResultater(vilkårsvurdering, it) } + .firstOrNull { vilkårResultatGruppertPåPerson -> + vilkårResultatGruppertPåPerson.any { it.second?.resultat == Resultat.IKKE_OPPFYLT } + } + ?.let { vilkårResultatGruppertPåPerson -> + val vilkårResultatSøker = + vilkårResultatGruppertPåPerson.firstOrNull { it.first.type == PersonType.SØKER && it.second != null } + val vilkårResultatBarn = + vilkårResultatGruppertPåPerson.firstOrNull { it.first.type == PersonType.BARN && it.second != null } + + when { + vilkårResultatSøker != null -> { + økTellerForFørsteUtfallVilkårVedAutomatiskSaksbehandling( + vilkårResultatSøker.second!!, + ) + } + vilkårResultatBarn != null -> { + økTellerForFørsteUtfallVilkårVedAutomatiskSaksbehandling( + vilkårResultatBarn.second!!, + ) + } + } + } + } + + private fun mapVilkårTilVilkårResultater( + vilkårsvurdering: Vilkårsvurdering, + vilkår: Vilkår, + ): List> { + val personer = persongrunnlagService.hentSøkerOgBarnPåBehandling(vilkårsvurdering.behandling.id) + ?: error("Finner ikke aktivt persongrunnlag ved telling av metrikker") + + return personer.map { person -> + val personResultat = vilkårsvurdering.personResultater.firstOrNull { personResultat -> + personResultat.aktør == person.aktør + } + + Pair( + person, + personResultat?.vilkårResultater?.find { it.vilkårType == vilkår && it.resultat == Resultat.IKKE_OPPFYLT }, + ) + } + } + + private fun økTellerForFørsteUtfallVilkårVedAutomatiskSaksbehandling(vilkårResultat: VilkårResultat) { + val behandlingId = vilkårResultat.personResultat?.vilkårsvurdering?.behandling?.id!! + val personer = persongrunnlagService.hentSøkerOgBarnPåBehandling(behandlingId) + ?: error("Finner ikke aktivt persongrunnlag ved telling av metrikker") + + val person = personer.firstOrNull { it.aktør == vilkårResultat.personResultat?.aktør } + ?: error("Finner ikke person") + + logger.info("Første vilkår med feil=$vilkårResultat, på personType=${person.type}, på behandling $behandlingId") + secureLogger.info("Første vilkår med feil=$vilkårResultat, på person=${person.aktør.aktivFødselsnummer()}, på behandling $behandlingId") + vilkårResultat.evalueringÅrsaker.forEach { årsak -> + vilkårsvurderingFørsteUtfall[person.type]?.get(årsak)?.increment() + } + } + + companion object { + private val logger = LoggerFactory.getLogger(VilkårsvurderingMetrics::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingMigreringUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingMigreringUtils.kt" new file mode 100644 index 000000000..413460427 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingMigreringUtils.kt" @@ -0,0 +1,77 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.til18ÅrsVilkårsdato +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import java.time.LocalDate + +object VilkårsvurderingMigreringUtils { + + fun utledPeriodeFom( + oppfylteVilkårResultaterForType: List, + vilkår: Vilkår, + person: Person, + nyMigreringsdato: LocalDate, + ): LocalDate { + val forrigeVilkårsPeriodeFom = + oppfylteVilkårResultaterForType.minWithOrNull(VilkårResultat.VilkårResultatComparator)?.periodeFom + return when { + person.fødselsdato.isAfter(nyMigreringsdato) || + vilkår.gjelderAlltidFraBarnetsFødselsdato() -> person.fødselsdato + + forrigeVilkårsPeriodeFom != null && + forrigeVilkårsPeriodeFom.isBefore(nyMigreringsdato) -> forrigeVilkårsPeriodeFom + + else -> nyMigreringsdato + } + } + + fun utledPeriodeTom( + oppfylteVilkårResultaterForType: List, + vilkår: Vilkår, + periodeFom: LocalDate, + ): LocalDate? { + val forrigeVilkårsPeriodeTom: LocalDate? = + oppfylteVilkårResultaterForType.minWithOrNull(VilkårResultat.VilkårResultatComparator)?.periodeTom + return when (vilkår) { + Vilkår.UNDER_18_ÅR -> periodeFom.til18ÅrsVilkårsdato() + else -> forrigeVilkårsPeriodeTom + } + } + + fun finnVilkårResultaterMedNyPeriodePgaNyMigreringsdato( + oppfylteVilkårResultaterForPerson: List, + person: Person, + nyMigreringsdato: LocalDate, + ): List { + val vilkårTyperForPerson = oppfylteVilkårResultaterForPerson + .map { it.vilkårType } + + return vilkårTyperForPerson.map { vilkår -> + + val oppfylteVilkårResultaterForType = oppfylteVilkårResultaterForPerson.filter { it.vilkårType == vilkår } + + val fom = utledPeriodeFom( + oppfylteVilkårResultaterForType = oppfylteVilkårResultaterForType, + vilkår = vilkår, + person = person, + nyMigreringsdato = nyMigreringsdato, + ) + + val tom: LocalDate? = + utledPeriodeTom( + oppfylteVilkårResultaterForType, + vilkår, + fom, + ) + + // Når vi endrer migreringsdato flyttes den alltid bakover. Vilkårresultatet som forskyves vil derfor alltid være det med lavest periodeFom + val vilkårResultatSomForskyves = + oppfylteVilkårResultaterForType.minBy { it.periodeFom!! } + VilkårResultatMedNyPeriode(vilkårResultatSomForskyves, fom, tom) + } + } +} + +data class VilkårResultatMedNyPeriode(val vilkårResultat: VilkårResultat, val fom: LocalDate, val tom: LocalDate?) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingService.kt" new file mode 100644 index 000000000..f20dd6cc5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingService.kt" @@ -0,0 +1,134 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.ekstern.restDomene.RestVedtakBegrunnelseTilknyttetVilkår +import no.nav.familie.ba.sak.integrasjoner.sanity.SanityService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.YearMonth + +@Service +class VilkårsvurderingService( + private val vilkårsvurderingRepository: VilkårsvurderingRepository, + private val sanityService: SanityService, +) { + + fun hentAktivForBehandling(behandlingId: Long): Vilkårsvurdering? { + return vilkårsvurderingRepository.findByBehandlingAndAktiv(behandlingId) + } + + fun hentAktivForBehandlingThrows(behandlingId: Long): Vilkårsvurdering = hentAktivForBehandling(behandlingId) + ?: throw Feil("Fant ikke vilkårsvurdering knyttet til behandling=$behandlingId") + + fun oppdater(vilkårsvurdering: Vilkårsvurdering): Vilkårsvurdering { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} oppdaterer vilkårsvurdering $vilkårsvurdering") + return vilkårsvurderingRepository.saveAndFlush(vilkårsvurdering) + } + + fun lagreNyOgDeaktiverGammel(vilkårsvurdering: Vilkårsvurdering): Vilkårsvurdering { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} oppretter vilkårsvurdering $vilkårsvurdering") + + val aktivVilkårsvurdering = hentAktivForBehandling(vilkårsvurdering.behandling.id) + + if (aktivVilkårsvurdering != null) { + vilkårsvurderingRepository.saveAndFlush(aktivVilkårsvurdering.also { it.aktiv = false }) + } + + return vilkårsvurderingRepository.save(vilkårsvurdering) + } + + fun lagreInitielt(vilkårsvurdering: Vilkårsvurdering): Vilkårsvurdering { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} oppretter vilkårsvurdering $vilkårsvurdering") + + val aktivVilkårsvurdering = hentAktivForBehandling(vilkårsvurdering.behandling.id) + if (aktivVilkårsvurdering != null) { + error("Det finnes allerede et aktivt vilkårsvurdering for behandling ${vilkårsvurdering.behandling.id}") + } + + return vilkårsvurderingRepository.save(vilkårsvurdering) + } + + fun hentVilkårsbegrunnelser(): Map> = + standardbegrunnelserTilNedtrekksmenytekster(sanityService.hentSanityBegrunnelser()) + + eøsStandardbegrunnelserTilNedtrekksmenytekster(sanityService.hentSanityEØSBegrunnelser()) + + fun hentTidligsteVilkårsvurderingKnyttetTilMigrering(behandlingId: Long): YearMonth? { + val vilkårsvurdering = hentAktivForBehandling( + behandlingId = behandlingId, + ) + + return vilkårsvurdering?.personResultater + ?.flatMap { it.vilkårResultater } + ?.filter { it.periodeFom != null } + ?.filter { it.vilkårType != Vilkår.UNDER_18_ÅR && it.vilkårType != Vilkår.GIFT_PARTNERSKAP } + ?.minByOrNull { it.periodeFom!! } + ?.periodeFom + ?.toYearMonth() + } + + @Transactional + fun oppdaterVilkårVedDødsfall(behandlingId: BehandlingId, dødsfallsDato: LocalDate, aktør: Aktør) { + val vilkårsvurdering = hentAktivForBehandlingThrows(behandlingId.id) + + val personResultat = vilkårsvurdering.personResultater.find { it.aktør == aktør } + ?: throw Feil(message = "Fant ikke vilkårsvurdering for person under manuell registrering av dødsfall dato") + + personResultat.vilkårResultater.filter { it.periodeTom != null && it.periodeTom!! > dødsfallsDato }.forEach { + it.periodeTom = dødsfallsDato + } + } + + companion object { + + private val logger = LoggerFactory.getLogger(VilkårsvurderingService::class.java) + + fun matchVilkårResultater( + vilkårsvurdering1: Vilkårsvurdering, + vilkårsvurdering2: Vilkårsvurdering, + ): List> { + val vilkårResultater = + (vilkårsvurdering1.personResultater.map { it.vilkårResultater } + vilkårsvurdering2.personResultater.map { it.vilkårResultater }).flatten() + + data class Match( + val aktør: Aktør, + val vilkårType: Vilkår, + val resultat: Resultat, + val periodeFom: LocalDate?, + val periodeTom: LocalDate?, + val begrunnelse: String, + val erEksplisittAvslagPåSøknad: Boolean?, + ) + + val gruppert = vilkårResultater.groupBy { + Match( + aktør = it.personResultat?.aktør ?: error("VilkårResultat mangler aktør"), + vilkårType = it.vilkårType, + resultat = it.resultat, + periodeFom = it.periodeFom, + periodeTom = it.periodeTom, + begrunnelse = it.begrunnelse, + erEksplisittAvslagPåSøknad = it.erEksplisittAvslagPåSøknad, + ) + } + return gruppert.map { (_, gruppe) -> + if (gruppe.size > 2) throw Feil("Finnes flere like vilkår i én vilkårsvurdering") + val vilkår1 = gruppe.find { it.personResultat!!.vilkårsvurdering == vilkårsvurdering1 } + val vilkår2 = gruppe.find { it.personResultat!!.vilkårsvurdering == vilkårsvurdering2 } + if (vilkår1 == null && vilkår2 == null) error("Vilkårresultater mangler tilknytning til vilkårsvurdering") + Pair(vilkår1, vilkår2) + } + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingUtils.kt" new file mode 100644 index 000000000..c40962883 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingUtils.kt" @@ -0,0 +1,516 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.kanErstatte +import no.nav.familie.ba.sak.common.kanFlytteFom +import no.nav.familie.ba.sak.common.kanFlytteTom +import no.nav.familie.ba.sak.common.kanSplitte +import no.nav.familie.ba.sak.common.til18ÅrsVilkårsdato +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.common.toPeriode +import no.nav.familie.ba.sak.ekstern.restDomene.RestVedtakBegrunnelseTilknyttetVilkår +import no.nav.familie.ba.sak.ekstern.restDomene.RestVilkårResultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.sivilstand.GrSivilstand.Companion.sisteSivilstand +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import java.time.LocalDate + +object VilkårsvurderingUtils { + + /** + * Funksjon som forsøker å slette en periode på et vilkår. + * Dersom det kun finnes en periode eller perioden som skal slettes + * lager en glippe. Isåfall nullstiller vi bare perioden. + */ + fun muterPersonResultatDelete(personResultat: PersonResultat, vilkårResultatId: Long) { + personResultat.slettEllerNullstill(vilkårResultatId = vilkårResultatId) + } + + /** + * Funksjon som forsøker å legge til en periode på et vilkår. + * Dersom det allerede finnes en uvurdet periode med samme vilkårstype + * skal det kastes en feil. + */ + fun muterPersonResultatPost(personResultat: PersonResultat, vilkårType: Vilkår) { + val nyttVilkårResultat = VilkårResultat( + personResultat = personResultat, + vilkårType = vilkårType, + resultat = Resultat.IKKE_VURDERT, + begrunnelse = "", + sistEndretIBehandlingId = personResultat.vilkårsvurdering.behandling.id, + ) + if (harUvurdertePerioder(personResultat, vilkårType)) { + throw FunksjonellFeil( + melding = "Det finnes allerede uvurderte vilkår av samme vilkårType", + frontendFeilmelding = "Du må ferdigstille vilkårsvurderingen på en periode som allerede er påbegynt, før du kan legge til en ny periode", + ) + } + personResultat.addVilkårResultat(vilkårResultat = nyttVilkårResultat) + } + + /** + * Funksjon som tar inn endret vilkår og muterer personens vilkårresultater til å få plass til den endrede perioden. + * @param[personResultat] Person med vilkår som eventuelt justeres + * @param[restVilkårResultat] Det endrede vilkårresultatet + * @return VilkårResultater før og etter mutering + */ + fun muterPersonVilkårResultaterPut( + personResultat: PersonResultat, + restVilkårResultat: RestVilkårResultat, + ): Pair, List> { + validerAvslagUtenPeriodeMedLøpende( + personSomEndres = personResultat, + vilkårSomEndres = restVilkårResultat, + ) + val kopiAvVilkårResultater = personResultat.vilkårResultater.toList() + + kopiAvVilkårResultater + .filter { !(it.erAvslagUtenPeriode() && it.id != restVilkårResultat.id) } + .forEach { + tilpassVilkårForEndretVilkår( + personResultat = personResultat, + vilkårResultat = it, + restVilkårResultat = restVilkårResultat, + ) + } + + return Pair(kopiAvVilkårResultater, personResultat.vilkårResultater.toList()) + } + + fun validerAvslagUtenPeriodeMedLøpende(personSomEndres: PersonResultat, vilkårSomEndres: RestVilkårResultat) { + val resultaterPåVilkår = + personSomEndres.vilkårResultater.filter { it.vilkårType == vilkårSomEndres.vilkårType && it.id != vilkårSomEndres.id } + when { + // For bor med søker-vilkåret kan avslag og innvilgelse være overlappende, da man kan f.eks. avslå full barnetrygd, men innvilge delt + vilkårSomEndres.vilkårType == Vilkår.BOR_MED_SØKER -> return + vilkårSomEndres.erAvslagUtenPeriode() && resultaterPåVilkår.any { it.resultat == Resultat.OPPFYLT && it.harFremtidigTom() } -> + throw FunksjonellFeil( + "Finnes løpende oppfylt ved forsøk på å legge til avslag uten periode ", + "Du kan ikke legge til avslag uten datoer fordi det finnes oppfylt løpende periode på vilkåret.", + ) + + vilkårSomEndres.harFremtidigTom() && resultaterPåVilkår.any { it.erAvslagUtenPeriode() } -> + throw FunksjonellFeil( + "Finnes avslag uten periode ved forsøk på å legge til løpende oppfylt", + "Du kan ikke legge til løpende periode fordi det er vurdert avslag uten datoer på vilkåret.", + ) + } + } + + private fun harUvurdertePerioder(personResultat: PersonResultat, vilkårType: Vilkår): Boolean { + val uvurdetePerioderMedSammeVilkårType = personResultat.vilkårResultater + .filter { it.vilkårType == vilkårType } + .find { it.resultat == Resultat.IKKE_VURDERT } + return uvurdetePerioderMedSammeVilkårType != null + } + + /** + * @param [personResultat] person vilkårresultatet tilhører + * @param [vilkårResultat] vilkårresultat som skal oppdaters på person + * @param [restVilkårResultat] oppdatert resultat fra frontend + */ + fun tilpassVilkårForEndretVilkår( + personResultat: PersonResultat, + vilkårResultat: VilkårResultat, + restVilkårResultat: RestVilkårResultat, + ) { + val periodePåNyttVilkår: Periode = restVilkårResultat.toPeriode() + + if (vilkårResultat.id == restVilkårResultat.id) { + vilkårResultat.oppdater(restVilkårResultat) + } else if (vilkårResultat.vilkårType == restVilkårResultat.vilkårType && !restVilkårResultat.erAvslagUtenPeriode()) { + val periode: Periode = vilkårResultat.toPeriode() + + var nyFom = periodePåNyttVilkår.tom + if (periodePåNyttVilkår.tom != TIDENES_ENDE) { + nyFom = periodePåNyttVilkår.tom.plusDays(1) + } + + val nyTom = periodePåNyttVilkår.fom.minusDays(1) + + when { + periodePåNyttVilkår.kanErstatte(periode) -> { + personResultat.removeVilkårResultat(vilkårResultatId = vilkårResultat.id) + } + + periodePåNyttVilkår.kanSplitte(periode) -> { + personResultat.removeVilkårResultat(vilkårResultatId = vilkårResultat.id) + personResultat.addVilkårResultat( + vilkårResultat.kopierMedNyPeriode( + fom = periode.fom, + tom = nyTom, + behandlingId = personResultat.vilkårsvurdering.behandling.id, + ), + ) + personResultat.addVilkårResultat( + vilkårResultat.kopierMedNyPeriode( + fom = nyFom, + tom = periode.tom, + behandlingId = personResultat.vilkårsvurdering.behandling.id, + ), + ) + } + + periodePåNyttVilkår.kanFlytteFom(periode) -> { + vilkårResultat.periodeFom = nyFom + vilkårResultat.erAutomatiskVurdert = false + vilkårResultat.oppdaterPekerTilBehandling() + } + + periodePåNyttVilkår.kanFlytteTom(periode) -> { + vilkårResultat.periodeTom = nyTom + vilkårResultat.erAutomatiskVurdert = false + vilkårResultat.oppdaterPekerTilBehandling() + } + } + } + } + + /** + * Dersom personer i initieltResultat har vurderte vilkår i aktivtResultat vil disse flyttes til initieltResultat + * (altså vil tilsvarende vilkår overskrives i initieltResultat og slettes fra aktivtResultat). + * + * @param initiellVilkårsvurdering - Vilkårsvurdering med vilkår basert på siste behandlignsgrunnlag. Skal bli neste aktive. + * @param aktivVilkårsvurdering - Vilkårsvurdering med vilkår basert på forrige behandlingsgrunnlag + * @param løpendeUnderkategori - Den løpende underkategorien for fagsaken. Brukes for å sjekke om utvidet-vilkåret skal kopieres med videre. + * @param aktørerMedUtvidetAndelerIForrigeBehandling - Liste med aktører som hadde utvidet andeler i forrige behandling + * + * @return oppdaterte versjoner av initieltResultat og aktivtResultat: + * initieltResultat (neste aktivt) med vilkår som skal benyttes videre + * aktivtResultat med hvilke vilkår som ikke skal benyttes videre + */ + fun flyttResultaterTilInitielt( + initiellVilkårsvurdering: Vilkårsvurdering, + aktivVilkårsvurdering: Vilkårsvurdering, + løpendeUnderkategori: BehandlingUnderkategori? = null, + aktørerMedUtvidetAndelerIForrigeBehandling: List = emptyList(), + ): Pair { + // OBS!! MÅ jobbe på kopier av vilkårsvurderingen her for å ikke oppdatere databasen + // Viktig at det er vår egen implementasjon av kopier som brukes, da kotlin sin copy-funksjon er en shallow copy + val initiellVilkårsvurderingKopi = initiellVilkårsvurdering.kopier() + val aktivVilkårsvurderingKopi = aktivVilkårsvurdering.kopier(inkluderAndreVurderinger = true) + + // Identifiserer hvilke vilkår som skal legges til og hvilke som kan fjernes + val personResultaterAktivt = aktivVilkårsvurderingKopi.personResultater.toMutableSet() + val personResultaterOppdatert = mutableSetOf() + initiellVilkårsvurderingKopi.personResultater.forEach { personFraInit -> + val personTilOppdatert = PersonResultat( + vilkårsvurdering = initiellVilkårsvurderingKopi, + aktør = personFraInit.aktør, + ) + val personenSomFinnes = personResultaterAktivt.firstOrNull { it.aktør == personFraInit.aktør } + + if (personenSomFinnes == null) { + // Legg til ny person + personTilOppdatert.setSortedVilkårResultater( + personFraInit.vilkårResultater.map { it.kopierMedParent(personTilOppdatert) } + .toSet(), + ) + } else { + // Fyll inn den initierte med person fra aktiv + oppdaterEksisterendePerson( + personenSomFinnes = personenSomFinnes, + personFraInit = personFraInit, + kopieringSkjerFraForrigeBehandling = initiellVilkårsvurderingKopi.behandling.id != aktivVilkårsvurderingKopi.behandling.id, + personTilOppdatert = personTilOppdatert, + løpendeUnderkategori = løpendeUnderkategori, + personResultaterAktivt = personResultaterAktivt, + aktørerMedUtvidetAndelerIForrigeBehandling = aktørerMedUtvidetAndelerIForrigeBehandling, + ) + } + personResultaterOppdatert.add(personTilOppdatert) + } + + aktivVilkårsvurderingKopi.personResultater = personResultaterAktivt + initiellVilkårsvurderingKopi.personResultater = personResultaterOppdatert + + return Pair(initiellVilkårsvurderingKopi, aktivVilkårsvurderingKopi) + } + + private fun oppdaterEksisterendePerson( + personenSomFinnes: PersonResultat, + personFraInit: PersonResultat, + kopieringSkjerFraForrigeBehandling: Boolean, + personTilOppdatert: PersonResultat, + løpendeUnderkategori: BehandlingUnderkategori?, + personResultaterAktivt: MutableSet, + aktørerMedUtvidetAndelerIForrigeBehandling: List, + ) { + val personsVilkårAktivt = personenSomFinnes.vilkårResultater.toMutableSet() + val personsAndreVurderingerAktivt = personenSomFinnes.andreVurderinger.toMutableSet() + val personsVilkårOppdatert = mutableSetOf() + val personsAndreVurderingerOppdatert = mutableSetOf() + + personFraInit.vilkårResultater.forEach { vilkårFraInit -> + val vilkårSomFinnes = + personenSomFinnes.vilkårResultater.filter { it.vilkårType == vilkårFraInit.vilkårType } + + val vilkårSomSkalKopieresOver = vilkårSomFinnes.filtrerVilkårÅKopiere( + kopieringSkjerFraForrigeBehandling = kopieringSkjerFraForrigeBehandling, + ) + val vilkårSomSkalFjernesFraAktivt = vilkårSomFinnes - vilkårSomSkalKopieresOver + personsVilkårAktivt.removeAll(vilkårSomSkalFjernesFraAktivt) + + if (vilkårSomSkalKopieresOver.isEmpty()) { + // Legg til nytt vilkår på person + personsVilkårOppdatert.add(vilkårFraInit.kopierMedParent(personTilOppdatert)) + } else { + /* Vilkår er vurdert på person - flytt fra aktivt og overskriv initierte + ikke oppfylte eller ikke vurdert perioder skal ikke kopieres om minst en oppfylt + periode eksisterer. */ + + personsVilkårOppdatert.addAll( + vilkårSomSkalKopieresOver.map { it.kopierMedParent(personTilOppdatert) }, + ) + personsVilkårAktivt.removeAll(vilkårSomSkalKopieresOver) + } + } + if (!kopieringSkjerFraForrigeBehandling) { + personenSomFinnes.andreVurderinger.forEach { + personsAndreVurderingerOppdatert.add(it.kopierMedParent(personTilOppdatert)) + personsAndreVurderingerAktivt.remove(it) + } + } + + val personHaddeUtvidetIForrigeBehandling = aktørerMedUtvidetAndelerIForrigeBehandling.contains(personFraInit.aktør) + + // Hvis forrige behandling hadde utbetaling av utvidet på personen eller underkategorien er utvidet skal + // utvidet-vilkåret kopieres med videre uansett nåværende underkategori + if (personsVilkårOppdatert.none { vilkårResultat -> vilkårResultat.vilkårType == Vilkår.UTVIDET_BARNETRYGD } && + (personHaddeUtvidetIForrigeBehandling || løpendeUnderkategori == BehandlingUnderkategori.UTVIDET) + ) { + val utvidetVilkår = + personenSomFinnes.vilkårResultater.filter { vilkårResultat -> vilkårResultat.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + if (utvidetVilkår.isNotEmpty()) { + personsVilkårOppdatert.addAll( + utvidetVilkår.filtrerVilkårÅKopiere(kopieringSkjerFraForrigeBehandling = kopieringSkjerFraForrigeBehandling) + .map { it.kopierMedParent(personTilOppdatert) }, + ) + personsVilkårAktivt.removeAll(utvidetVilkår) + } + } + + personTilOppdatert.setSortedVilkårResultater(personsVilkårOppdatert.toSet()) + personTilOppdatert.setAndreVurderinger(personsAndreVurderingerOppdatert.toSet()) + + // Fjern person fra aktivt dersom alle vilkår er fjernet, ellers oppdater + if (personsVilkårAktivt.isEmpty()) { + personResultaterAktivt.remove(personenSomFinnes) + } else { + personenSomFinnes.setSortedVilkårResultater(personsVilkårAktivt.toSet()) + } + } + + fun lagFjernAdvarsel(personResultater: Set): String { + var advarsel = + "Du har gjort endringer i behandlingsgrunnlaget. Dersom du går videre vil vilkår for følgende personer fjernes:" + personResultater.forEach { + advarsel = advarsel.plus("\n${it.aktør.aktivFødselsnummer()}:") + it.vilkårResultater.forEach { vilkårResultat -> + advarsel = advarsel.plus("\n - ${vilkårResultat.vilkårType.beskrivelse}") + } + advarsel = advarsel.plus("\n") + } + return advarsel + } +} + +fun standardbegrunnelserTilNedtrekksmenytekster( + sanityBegrunnelser: Map, +) = + Standardbegrunnelse + .values() + .groupBy { it.vedtakBegrunnelseType } + .mapValues { begrunnelseGruppe -> + begrunnelseGruppe.value + .flatMap { vedtakBegrunnelse -> + vedtakBegrunnelseTilRestVedtakBegrunnelseTilknyttetVilkår( + sanityBegrunnelser, + vedtakBegrunnelse, + ) + } + } + +fun eøsStandardbegrunnelserTilNedtrekksmenytekster( + sanityEØSBegrunnelser: Map, +) = EØSStandardbegrunnelse.values().groupBy { it.vedtakBegrunnelseType } + .mapValues { begrunnelseGruppe -> + begrunnelseGruppe.value.flatMap { vedtakBegrunnelse -> + eøsBegrunnelseTilRestVedtakBegrunnelseTilknyttetVilkår( + sanityEØSBegrunnelser, + vedtakBegrunnelse, + ) + } + } + +fun vedtakBegrunnelseTilRestVedtakBegrunnelseTilknyttetVilkår( + sanityBegrunnelser: Map, + vedtakBegrunnelse: Standardbegrunnelse, +): List { + val sanityBegrunnelse = sanityBegrunnelser[vedtakBegrunnelse] ?: return emptyList() + + val triggesAv = sanityBegrunnelse.triggesAv + val visningsnavn = sanityBegrunnelse.navnISystem + + return if (triggesAv.vilkår.isEmpty()) { + listOf( + RestVedtakBegrunnelseTilknyttetVilkår( + id = vedtakBegrunnelse.enumnavnTilString(), + navn = visningsnavn, + vilkår = null, + ), + ) + } else { + triggesAv.vilkår.map { + RestVedtakBegrunnelseTilknyttetVilkår( + id = vedtakBegrunnelse.enumnavnTilString(), + navn = visningsnavn, + vilkår = it, + ) + } + } +} + +fun eøsBegrunnelseTilRestVedtakBegrunnelseTilknyttetVilkår( + sanityEØSBegrunnelser: Map, + vedtakBegrunnelse: EØSStandardbegrunnelse, +): List { + val eøsSanityBegrunnelse = sanityEØSBegrunnelser[vedtakBegrunnelse] ?: return emptyList() + + return if (eøsSanityBegrunnelse.vilkår.isEmpty()) { + listOf( + RestVedtakBegrunnelseTilknyttetVilkår( + id = vedtakBegrunnelse.enumnavnTilString(), + navn = eøsSanityBegrunnelse.navnISystem, + vilkår = null, + ), + ) + } else { + eøsSanityBegrunnelse.vilkår.map { + RestVedtakBegrunnelseTilknyttetVilkår( + id = vedtakBegrunnelse.enumnavnTilString(), + navn = eøsSanityBegrunnelse.navnISystem, + vilkår = it, + ) + } + } +} + +private fun List.filtrerVilkårÅKopiere(kopieringSkjerFraForrigeBehandling: Boolean): List { + return if (kopieringSkjerFraForrigeBehandling) { + this.filter { it.resultat == Resultat.OPPFYLT } + } else { + this + } +} + +fun genererPersonResultatForPerson( + vilkårsvurdering: Vilkårsvurdering, + person: Person, +): PersonResultat { + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = person.aktør, + ) + + val vilkårForPerson = Vilkår.hentVilkårFor( + personType = person.type, + fagsakType = vilkårsvurdering.behandling.fagsak.type, + behandlingUnderkategori = vilkårsvurdering.behandling.underkategori, + ) + + val vilkårResultater = vilkårForPerson.map { vilkår -> + val fom = if (vilkår.gjelderAlltidFraBarnetsFødselsdato()) person.fødselsdato else null + + val tom: LocalDate? = + when { + person.erDød() -> person.dødsfall!!.dødsfallDato + vilkår == Vilkår.UNDER_18_ÅR -> person.fødselsdato.til18ÅrsVilkårsdato() + else -> null + } + + VilkårResultat( + personResultat = personResultat, + erAutomatiskVurdert = when (vilkår) { + Vilkår.UNDER_18_ÅR, Vilkår.GIFT_PARTNERSKAP -> true + else -> false + }, + resultat = utledResultat(vilkår, person), + vilkårType = vilkår, + periodeFom = fom, + periodeTom = tom, + begrunnelse = utledBegrunnelse(vilkår, person), + sistEndretIBehandlingId = personResultat.vilkårsvurdering.behandling.id, + ) + }.toSortedSet(VilkårResultat.VilkårResultatComparator) + + personResultat.setSortedVilkårResultater(vilkårResultater) + + return personResultat +} + +private fun utledResultat( + vilkår: Vilkår, + person: Person, +) = when (vilkår) { + Vilkår.UNDER_18_ÅR -> Resultat.OPPFYLT + Vilkår.GIFT_PARTNERSKAP -> utledResultatForGiftPartnerskap(person) + else -> Resultat.IKKE_VURDERT +} + +private fun utledResultatForGiftPartnerskap(person: Person) = + if (person.sivilstander.isEmpty() || person.sivilstander.sisteSivilstand()?.type?.somForventetHosBarn() == true) { + Resultat.OPPFYLT + } else { + Resultat.IKKE_VURDERT + } + +private fun utledBegrunnelse( + vilkår: Vilkår, + person: Person, +) = when { + person.erDød() -> "Dødsfall" + vilkår == Vilkår.UNDER_18_ÅR -> "Vurdert og satt automatisk" + vilkår == Vilkår.GIFT_PARTNERSKAP -> if (person.sivilstander.sisteSivilstand()?.type?.somForventetHosBarn() == false) { + "Vilkåret er forsøkt behandlet automatisk, men barnet er registrert som gift i " + + "folkeregisteret. Vurder hvilke konsekvenser dette skal ha for behandlingen" + } else { + "" + } + + else -> "" +} + +fun validerVilkårStarterIkkeFørMigreringsdatoForMigreringsbehandling( + vilkårsvurdering: Vilkårsvurdering, + vilkårResultat: VilkårResultat, + migreringsdato: LocalDate?, +) { + val behandling = vilkårsvurdering.behandling + if (migreringsdato != null && + vilkårResultat.vilkårType !in listOf(Vilkår.UNDER_18_ÅR, Vilkår.GIFT_PARTNERSKAP) && + vilkårResultat.periodeFom?.isBefore(migreringsdato) == true + ) { + throw FunksjonellFeil( + melding = "${vilkårResultat.vilkårType} kan ikke endres før $migreringsdato " + + "for fagsak=${behandling.fagsak.id}", + frontendFeilmelding = "F.o.m. kan ikke settes tidligere " + + "enn migreringsdato ${migreringsdato.tilKortString()}. " + + "Ved behov for vurdering før dette, må behandlingen henlegges, " + + "og migreringstidspunktet endres ved å opprette en ny migreringsbehandling.", + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingValidering.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingValidering.kt" new file mode 100644 index 000000000..ac71a4749 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingValidering.kt" @@ -0,0 +1,111 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.ekstern.restDomene.RestVilkårResultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjer +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.harBlandetRegelverk +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonEnkel +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.søker +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import java.time.LocalDate + +fun validerIngenVilkårSattEtterSøkersDød( + søkerOgBarn: List, + vilkårsvurdering: Vilkårsvurdering, +) { + val søker = søkerOgBarn.søker() + val vilkårResultaterSøker = + vilkårsvurdering.hentPersonResultaterTil(søker.aktør.aktørId) + val søkersDød = søker.dødsfallDato ?: LocalDate.now() + + val vilkårSomEnderEtterSøkersDød = + vilkårResultaterSøker + .groupBy { it.vilkårType } + .mapNotNull { (vilkårType, vilkårResultater) -> + vilkårType.takeIf { + vilkårResultater.any { + it.periodeTom?.isAfter(søkersDød) ?: true + } + } + } + + if (vilkårSomEnderEtterSøkersDød.isNotEmpty()) { + throw FunksjonellFeil( + "Ved behandlingsårsak \"Dødsfall Bruker\" må vilkårene på søker avsluttes " + + "senest dagen søker døde, men " + + Utils.slåSammen(vilkårSomEnderEtterSøkersDød.map { "\"" + it.beskrivelse + "\"" }) + + " vilkåret til søker slutter etter søkers død.", + ) + } +} + +fun validerIkkeBlandetRegelverk( + søkerOgBarn: List, + vilkårsvurdering: Vilkårsvurdering, +) { + val vilkårsvurderingTidslinjer = VilkårsvurderingTidslinjer(vilkårsvurdering, søkerOgBarn) + if (vilkårsvurderingTidslinjer.harBlandetRegelverk()) { + throw FunksjonellFeil( + melding = "Det er forskjellig regelverk for en eller flere perioder for søker eller barna", + ) + } +} + +fun valider18ÅrsVilkårEksistererFraFødselsdato( + søkerOgBarn: List, + vilkårsvurdering: Vilkårsvurdering, + behandling: Behandling, +) { + vilkårsvurdering.personResultater.forEach { personResultat -> + val person = søkerOgBarn.find { it.aktør == personResultat.aktør } + if (person?.type == PersonType.BARN && !personResultat.vilkårResultater.finnesUnder18VilkårFraFødselsdato(person.fødselsdato)) { + if (behandling.erSatsendring() || behandling.opprettetÅrsak.erOmregningsårsak()) { + secureLogger.warn( + "Fødselsdato ${person.fødselsdato} ulik fom ${ + personResultat.vilkårResultater.filter { it.vilkårType == Vilkår.UNDER_18_ÅR } + .sortedBy { it.periodeFom }.first().periodeFom + } i 18års-vilkåret i fagsak ${behandling.fagsak.id}.", + ) + } else { + throw FunksjonellFeil( + melding = "Barn født ${person.fødselsdato} har ikke fått under 18-vilkåret vurdert fra fødselsdato", + frontendFeilmelding = "Det må være en periode på 18-års vilkåret som starter på barnets fødselsdato", + ) + } + } + } +} + +fun validerResultatBegrunnelse(restVilkårResultat: RestVilkårResultat) { + val resultat = restVilkårResultat.resultat + val vilkårType = restVilkårResultat.vilkårType + val resultatBegrunnelse = restVilkårResultat.resultatBegrunnelse + val regelverk = restVilkårResultat.vurderesEtter + + if (resultatBegrunnelse != null) { + if (!resultatBegrunnelse.gyldigForVilkår.contains(vilkårType)) { + "Resultatbegrunnelsen $resultatBegrunnelse kan ikke kombineres med vilkåret $vilkårType".apply { + throw FunksjonellFeil(this, this) + } + } + if (!resultatBegrunnelse.gyldigIKombinasjonMedResultat.contains(resultat)) { + "Resultatbegrunnelsen $resultatBegrunnelse kan ikke kombineres med resultatet $resultat".apply { + throw FunksjonellFeil(this, this) + } + } + if (!resultatBegrunnelse.gyldigForRegelverk.contains(regelverk)) { + "Resultatbegrunnelsen $resultatBegrunnelse kan ikke kombineres med regelverket $regelverk".apply { + throw FunksjonellFeil(this, this) + } + } + } +} + +private fun Set.finnesUnder18VilkårFraFødselsdato(fødselsdato: LocalDate): Boolean = + this.filter { it.vilkårType == Vilkår.UNDER_18_ÅR }.any { it.periodeFom == fødselsdato } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/AnnenVurdering.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/AnnenVurdering.kt" new file mode 100644 index 000000000..797d3e64c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/AnnenVurdering.kt" @@ -0,0 +1,82 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.util.Objects + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "AnnenVurdering") +@Table(name = "ANNEN_VURDERING") +data class AnnenVurdering( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "annen_vurdering_seq_generator") + @SequenceGenerator( + name = "annen_vurdering_seq_generator", + sequenceName = "annen_vurdering_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @ManyToOne + @JoinColumn(name = "fk_person_resultat_id") + var personResultat: PersonResultat, + + @Enumerated(EnumType.STRING) + @Column(name = "resultat") + var resultat: Resultat = Resultat.IKKE_VURDERT, + + @Enumerated(EnumType.STRING) + @Column(name = "type") + var type: AnnenVurderingType, + + @Column(name = "begrunnelse") + var begrunnelse: String? = null, +) : BaseEntitet() { + + fun kopierMedParent(nyPersonResultat: PersonResultat? = null): AnnenVurdering { + return AnnenVurdering( + personResultat = nyPersonResultat ?: personResultat, + type = type, + resultat = resultat, + begrunnelse = begrunnelse, + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AnnenVurdering + + return type == other.type + } + + override fun hashCode(): Int { + return Objects.hash(type) + } + + override fun toString(): String { + return "AnnenVurdering(id=$id, type=$type, personident=${personResultat.aktør.aktørId})" + } + + fun toSecureString(): String { + return "AnnenVurdering(id=$id, type=$type, personident=${personResultat.aktør.aktivFødselsnummer()})" + } +} + +enum class AnnenVurderingType { + OPPLYSNINGSPLIKT, +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/AnnenVurderingRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/AnnenVurderingRepository.kt" new file mode 100644 index 000000000..0ca2bcdcc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/AnnenVurderingRepository.kt" @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface AnnenVurderingRepository : JpaRepository { + + @Query(value = "SELECT b FROM AnnenVurdering b WHERE b.personResultat = :personResultat AND b.type = :type") + fun findBy(personResultat: PersonResultat, type: AnnenVurderingType): AnnenVurdering? +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/PersonResultat.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/PersonResultat.kt" new file mode 100644 index 000000000..fda5b7be9 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/PersonResultat.kt" @@ -0,0 +1,166 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.CascadeType +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import jakarta.persistence.OneToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.common.isSameOrBefore +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat.Companion.VilkårResultatComparator +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDate +import java.util.SortedSet + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "PersonResultat") +@Table(name = "PERSON_RESULTAT") +class PersonResultat( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "periode_resultat_seq_generator") + @SequenceGenerator( + name = "periode_resultat_seq_generator", + sequenceName = "periode_resultat_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @JsonIgnore + @ManyToOne + @JoinColumn(name = "fk_vilkaarsvurdering_id", nullable = false, updatable = false) + var vilkårsvurdering: Vilkårsvurdering, + + @OneToOne(optional = false) + @JoinColumn(name = "fk_aktoer_id", nullable = false, updatable = false) + val aktør: Aktør, + + @OneToMany( + fetch = FetchType.EAGER, + mappedBy = "personResultat", + cascade = [CascadeType.ALL], + orphanRemoval = true, + ) + val vilkårResultater: MutableSet = sortedSetOf(VilkårResultatComparator), + + @OneToMany( + fetch = FetchType.EAGER, + mappedBy = "personResultat", + cascade = [CascadeType.ALL], + orphanRemoval = true, + ) + val andreVurderinger: MutableSet = mutableSetOf(), + +) : BaseEntitet() { + + fun setSortedVilkårResultater(nyeVilkårResultater: Set) { + vilkårResultater.clear() + vilkårResultater.addAll(nyeVilkårResultater.toSortedSet(VilkårResultatComparator)) + } + + fun setAndreVurderinger(nyeAndreVurderinger: Set) { + andreVurderinger.clear() + andreVurderinger.addAll(nyeAndreVurderinger) + } + + fun getSortedVilkårResultat(index: Int): VilkårResultat? { + return vilkårResultater.toSortedSet(VilkårResultatComparator).elementAtOrNull(index) + } + + fun addVilkårResultat(vilkårResultat: VilkårResultat) { + vilkårResultater.add(vilkårResultat) + setSortedVilkårResultater(vilkårResultater.toSet()) + vilkårResultat.personResultat = this + } + + fun removeVilkårResultat(vilkårResultatId: Long) { + vilkårResultater.find { vilkårResultatId == it.id }?.personResultat = null + setSortedVilkårResultater(vilkårResultater.filter { vilkårResultatId != it.id }.toSet()) + } + + fun slettEllerNullstill(vilkårResultatId: Long) { + val vilkårResultat = vilkårResultater.find { it.id == vilkårResultatId } + ?: throw Feil( + message = "Prøver å slette et vilkår som ikke finnes", + frontendFeilmelding = "Vilkåret du prøver å slette finnes ikke i systemet.", + ) + + val perioderMedSammeVilkårType = vilkårResultater + .filter { it.vilkårType == vilkårResultat.vilkårType && it.id != vilkårResultat.id } + + if (perioderMedSammeVilkårType.isEmpty()) { + vilkårResultat.nullstill() + } else { + removeVilkårResultat(vilkårResultatId) + } + } + + fun kopierMedParent( + vilkårsvurdering: Vilkårsvurdering, + inkluderAndreVurderinger: Boolean = false, + ): PersonResultat { + val nyttPersonResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = aktør, + ) + val kopierteVilkårResultater: SortedSet = + vilkårResultater.map { it.kopierMedParent(nyttPersonResultat) }.toSortedSet(VilkårResultatComparator) + nyttPersonResultat.setSortedVilkårResultater(kopierteVilkårResultater) + + if (inkluderAndreVurderinger) { + val kopierteAndreVurderinger: MutableSet = + andreVurderinger.map { it.kopierMedParent(nyttPersonResultat) }.toMutableSet() + + nyttPersonResultat.setAndreVurderinger(kopierteAndreVurderinger) + } + return nyttPersonResultat + } + + fun tilKopiForNyVilkårsvurdering( + nyVilkårsvurdering: Vilkårsvurdering, + ): PersonResultat { + val nyttPersonResultat = PersonResultat( + vilkårsvurdering = nyVilkårsvurdering, + aktør = aktør, + andreVurderinger = mutableSetOf(), // Vi kopierer ikke over andreVurderinger da den aldri skal være med i ny behandling + ) + + val nyeVilkårResultater = vilkårResultater + .filter { it.erOppfylt() } + .map { + it.tilKopiForNyttPersonResultat( + nyttPersonResultat = nyttPersonResultat, + ) + } + .toSet() + + nyttPersonResultat.setSortedVilkårResultater(nyeVilkårResultater) + + return nyttPersonResultat + } + + fun erSøkersResultater() = vilkårResultater.none { it.vilkårType == Vilkår.UNDER_18_ÅR } || + vilkårsvurdering.behandling.fagsak.type in listOf(FagsakType.BARN_ENSLIG_MINDREÅRIG, FagsakType.INSTITUSJON) + + fun erDeltBosted(segmentFom: LocalDate): Boolean = + vilkårResultater + .filter { it.vilkårType == Vilkår.BOR_MED_SØKER } + .filter { + (it.periodeFom == null || it.periodeFom!!.isSameOrBefore(segmentFom)) && + (it.periodeTom == null || it.periodeTom!!.isSameOrAfter(segmentFom)) + }.any { it.utdypendeVilkårsvurderinger.contains(UtdypendeVilkårsvurdering.DELT_BOSTED) } + + fun harEksplisittAvslag() = vilkårResultater.any { it.erEksplisittAvslagPåSøknad == true } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/UtdypendeVilk\303\245rsvurderingerUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/UtdypendeVilk\303\245rsvurderingerUtils.kt" new file mode 100644 index 000000000..b0a3f9d84 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/UtdypendeVilk\303\245rsvurderingerUtils.kt" @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene + +import jakarta.persistence.AttributeConverter +import jakarta.persistence.Converter +import no.nav.familie.ba.sak.common.Utils + +enum class UtdypendeVilkårsvurdering { + VURDERING_ANNET_GRUNNLAG, + VURDERT_MEDLEMSKAP, + DELT_BOSTED, + DELT_BOSTED_SKAL_IKKE_DELES, + OMFATTET_AV_NORSK_LOVGIVNING, + OMFATTET_AV_NORSK_LOVGIVNING_UTLAND, + ANNEN_FORELDER_OMFATTET_AV_NORSK_LOVGIVNING, + BARN_BOR_I_NORGE, + BARN_BOR_I_EØS, + BARN_BOR_I_STORBRITANNIA, + BARN_BOR_I_NORGE_MED_SØKER, + BARN_BOR_I_EØS_MED_SØKER, + BARN_BOR_I_EØS_MED_ANNEN_FORELDER, + BARN_BOR_I_STORBRITANNIA_MED_SØKER, + BARN_BOR_I_STORBRITANNIA_MED_ANNEN_FORELDER, + BARN_BOR_ALENE_I_ANNET_EØS_LAND, +} + +@Converter +class UtdypendeVilkårsvurderingerConverter : AttributeConverter, String> { + + override fun convertToDatabaseColumn(enumListe: List) = + Utils.konverterEnumsTilString(enumListe) + + override fun convertToEntityAttribute(string: String?): List = + Utils.konverterStringTilEnums(string) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245r.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245r.kt" new file mode 100644 index 000000000..616bc8e19 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245r.kt" @@ -0,0 +1,188 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.convertDataClassToJson +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Evaluering +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.LovligOppholdFaktaEØS +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.VurderBarnErBosattMedSøker +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.VurderBarnErUgift +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.VurderBarnErUnder18 +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.VurderBarnHarLovligOpphold +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.VurderPersonErBosattIRiket +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.VurderPersonHarLovligOpphold +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType.ANNENPART +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType.BARN +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType.SØKER +import java.time.LocalDate + +enum class Vilkår( + val beskrivelse: String, + val harRegelverk: Boolean, +) { + + UNDER_18_ÅR( + beskrivelse = "Er under 18 år", + harRegelverk = false, + ), + BOR_MED_SØKER( + beskrivelse = "Bor med søker", + harRegelverk = true, + ), + GIFT_PARTNERSKAP( + beskrivelse = "Gift/partnerskap", + harRegelverk = false, + ), + BOSATT_I_RIKET( + beskrivelse = "Bosatt i riket", + harRegelverk = true, + ), + LOVLIG_OPPHOLD( + beskrivelse = "Lovlig opphold", + harRegelverk = true, + ), + UTVIDET_BARNETRYGD( + beskrivelse = "Utvidet barnetrygd", + harRegelverk = false, + ), + ; + + override fun toString(): String { + return this.name + } + + companion object { + + fun hentOrdinæreVilkårFor( + personType: PersonType, + ): List = when (personType) { + SØKER -> listOf(BOSATT_I_RIKET, LOVLIG_OPPHOLD) + ANNENPART -> throw Feil("Ikke implementert for $ANNENPART") + BARN -> listOf(BOSATT_I_RIKET, LOVLIG_OPPHOLD, UNDER_18_ÅR, BOR_MED_SØKER, GIFT_PARTNERSKAP) + } + + fun hentVilkårFor( + personType: PersonType, + fagsakType: FagsakType, + behandlingUnderkategori: BehandlingUnderkategori, + ): Set { + return when (fagsakType) { + FagsakType.NORMAL -> when (personType) { + BARN -> setOf(UNDER_18_ÅR, BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD) + SØKER -> setOf(BOSATT_I_RIKET, LOVLIG_OPPHOLD) + if (behandlingUnderkategori == BehandlingUnderkategori.UTVIDET) setOf(UTVIDET_BARNETRYGD) else emptySet() + ANNENPART -> emptySet() + } + FagsakType.INSTITUSJON -> when (personType) { + BARN -> setOf(UNDER_18_ÅR, BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD) + SØKER, ANNENPART -> emptySet() + } + FagsakType.BARN_ENSLIG_MINDREÅRIG -> when (personType) { + BARN -> setOf(UNDER_18_ÅR, BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD) + if (behandlingUnderkategori == BehandlingUnderkategori.UTVIDET) setOf(UTVIDET_BARNETRYGD) else emptySet() + SØKER, ANNENPART -> emptySet() + } + } + } + + fun hentFødselshendelseVilkårsreglerRekkefølge(): List { + return listOf( + UNDER_18_ÅR, + BOR_MED_SØKER, + GIFT_PARTNERSKAP, + BOSATT_I_RIKET, + LOVLIG_OPPHOLD, + ) + } + } + + fun defaultRegelverk(behandlingKategori: BehandlingKategori): Regelverk? { + return when (this) { + BOR_MED_SØKER, BOSATT_I_RIKET, LOVLIG_OPPHOLD -> { + if (behandlingKategori == BehandlingKategori.EØS) { + Regelverk.EØS_FORORDNINGEN + } else { + Regelverk.NASJONALE_REGLER + } + } + + UTVIDET_BARNETRYGD, UNDER_18_ÅR, GIFT_PARTNERSKAP -> null + } + } + + fun vurderVilkår( + person: Person, + vurderFra: LocalDate = LocalDate.now(), + annenForelder: Person? = null, + ): AutomatiskVurdering { + val vilkårsregel = when (this) { + UNDER_18_ÅR -> VurderBarnErUnder18( + alder = person.hentAlder(), + ) + + BOR_MED_SØKER -> VurderBarnErBosattMedSøker( + søkerAdresser = person.personopplysningGrunnlag.søker.bostedsadresser, + barnAdresser = person.bostedsadresser, + ) + + GIFT_PARTNERSKAP -> VurderBarnErUgift( + sivilstander = person.sivilstander, + ) + + BOSATT_I_RIKET -> VurderPersonErBosattIRiket( + adresser = person.bostedsadresser, + vurderFra = vurderFra, + ) + + LOVLIG_OPPHOLD -> if (person.type == BARN) { + VurderBarnHarLovligOpphold( + aktør = person.aktør, + ) + } else { + VurderPersonHarLovligOpphold( + morLovligOppholdFaktaEØS = LovligOppholdFaktaEØS( + arbeidsforhold = person.arbeidsforhold, + bostedsadresser = person.bostedsadresser, + statsborgerskap = person.statsborgerskap, + ), + annenForelderLovligOppholdFaktaEØS = if (annenForelder != null) { + LovligOppholdFaktaEØS( + arbeidsforhold = annenForelder.arbeidsforhold, + bostedsadresser = annenForelder.bostedsadresser, + statsborgerskap = annenForelder.statsborgerskap, + ) + } else { + null + }, + opphold = person.opphold, + ) + } + + UTVIDET_BARNETRYGD -> throw Feil("Ikke støtte for å automatisk vurdere vilkåret ${this.beskrivelse}") + } + + return AutomatiskVurdering( + evaluering = vilkårsregel.vurder(), + regelInput = vilkårsregel.convertDataClassToJson(), + ) + } +} + +data class AutomatiskVurdering( + val regelInput: String, + val evaluering: Evaluering, + val resultat: Resultat = evaluering.resultat, +) + +data class GyldigVilkårsperiode( + val gyldigFom: LocalDate = LocalDate.MIN, + val gyldigTom: LocalDate = LocalDate.MAX, +) { + + fun gyldigFor(dato: LocalDate): Boolean { + return !(dato.isBefore(gyldigFom) || dato.isAfter(gyldigTom)) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245rResultat.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245rResultat.kt" new file mode 100644 index 000000000..331dd7a21 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245rResultat.kt" @@ -0,0 +1,222 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.StringListConverter +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.ekstern.restDomene.RestVilkårResultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelseListConverter +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase +import java.time.LocalDate + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "VilkårResultat") +@Table(name = "VILKAR_RESULTAT") +class VilkårResultat( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "vilkar_resultat_seq_generator") + @SequenceGenerator( + name = "vilkar_resultat_seq_generator", + sequenceName = "vilkar_resultat_seq", + allocationSize = 50, + ) + val id: Long = 0, + + // Denne må være nullable=true slik at man kan slette vilkår fra person resultat + @JsonIgnore + @ManyToOne + @JoinColumn(name = "fk_person_resultat_id") + var personResultat: PersonResultat?, + + @Enumerated(EnumType.STRING) + @Column(name = "vilkar") + val vilkårType: Vilkår, + + @Enumerated(EnumType.STRING) + @Column(name = "resultat") + var resultat: Resultat, + + @Enumerated(EnumType.STRING) + @Column(name = "resultat_begrunnelse") + var resultatBegrunnelse: ResultatBegrunnelse? = null, + + @Column(name = "periode_fom") + var periodeFom: LocalDate? = null, + + @Column(name = "periode_tom") + var periodeTom: LocalDate? = null, + + @Column(name = "begrunnelse", columnDefinition = "TEXT", nullable = false) + var begrunnelse: String, + + @Column(name = "sist_endret_i_behandling_id", nullable = false) + var sistEndretIBehandlingId: Long, + + @Column(name = "er_automatisk_vurdert", nullable = false) + var erAutomatiskVurdert: Boolean = false, + + @Column(name = "er_eksplisitt_avslag_paa_soknad") + var erEksplisittAvslagPåSøknad: Boolean? = null, + + @Column(name = "evaluering_aarsak") + @Convert(converter = StringListConverter::class) + val evalueringÅrsaker: List = emptyList(), + + @Column(name = "regel_input", columnDefinition = "TEXT") + var regelInput: String? = null, + + @Column(name = "regel_output", columnDefinition = "TEXT") + var regelOutput: String? = null, + + @Column(name = "vedtak_begrunnelse_spesifikasjoner") + @Convert(converter = IVedtakBegrunnelseListConverter::class) + var standardbegrunnelser: List = emptyList(), + + @Enumerated(EnumType.STRING) + @Column(name = "vurderes_etter") + var vurderesEtter: Regelverk? = personResultat?.let { vilkårType.defaultRegelverk(it.vilkårsvurdering.behandling.kategori) }, + + @Column(name = "utdypende_vilkarsvurderinger") + @Convert(converter = UtdypendeVilkårsvurderingerConverter::class) + var utdypendeVilkårsvurderinger: List = emptyList(), +) : BaseEntitet() { + + override fun toString(): String { + return "VilkårResultat(" + + "id=$id, " + + "vilkårType=$vilkårType, " + + "periodeFom=$periodeFom, " + + "periodeTom=$periodeTom, " + + "resultat=$resultat, " + + "evalueringÅrsaker=$evalueringÅrsaker" + + ")" + } + + fun nullstill() { + periodeFom = null + periodeTom = null + begrunnelse = "" + resultat = Resultat.IKKE_VURDERT + } + + fun oppdater(restVilkårResultat: RestVilkårResultat) { + periodeFom = restVilkårResultat.periodeFom + periodeTom = restVilkårResultat.periodeTom + begrunnelse = restVilkårResultat.begrunnelse + resultat = restVilkårResultat.resultat + resultatBegrunnelse = restVilkårResultat.resultatBegrunnelse + erAutomatiskVurdert = false + erEksplisittAvslagPåSøknad = restVilkårResultat.erEksplisittAvslagPåSøknad + oppdaterPekerTilBehandling() + vurderesEtter = restVilkårResultat.vurderesEtter + utdypendeVilkårsvurderinger = restVilkårResultat.utdypendeVilkårsvurderinger + } + + fun kopierMedParent(nyPersonResultat: PersonResultat? = null): VilkårResultat { + return VilkårResultat( + personResultat = nyPersonResultat ?: personResultat, + erAutomatiskVurdert = erAutomatiskVurdert, + vilkårType = vilkårType, + resultat = resultat, + resultatBegrunnelse = resultatBegrunnelse, + periodeFom = periodeFom, + periodeTom = periodeTom, + begrunnelse = begrunnelse, + sistEndretIBehandlingId = sistEndretIBehandlingId, + regelInput = regelInput, + regelOutput = regelOutput, + erEksplisittAvslagPåSøknad = erEksplisittAvslagPåSøknad, + vurderesEtter = vurderesEtter, + utdypendeVilkårsvurderinger = utdypendeVilkårsvurderinger, + standardbegrunnelser = standardbegrunnelser, + ) + } + + fun kopierMedNyPeriode(fom: LocalDate, tom: LocalDate, behandlingId: Long): VilkårResultat { + return VilkårResultat( + personResultat = personResultat, + erAutomatiskVurdert = erAutomatiskVurdert, + vilkårType = vilkårType, + resultat = resultat, + resultatBegrunnelse = resultatBegrunnelse, + periodeFom = if (fom == TIDENES_MORGEN) null else fom, + periodeTom = if (tom == TIDENES_ENDE) null else tom, + begrunnelse = begrunnelse, + regelInput = regelInput, + regelOutput = regelOutput, + sistEndretIBehandlingId = behandlingId, + erEksplisittAvslagPåSøknad = erEksplisittAvslagPåSøknad, + vurderesEtter = vurderesEtter, + utdypendeVilkårsvurderinger = utdypendeVilkårsvurderinger, + ) + } + + fun tilKopiForNyttPersonResultat(nyttPersonResultat: PersonResultat): VilkårResultat { + return VilkårResultat( + personResultat = nyttPersonResultat, + erAutomatiskVurdert = erAutomatiskVurdert, + vilkårType = vilkårType, + resultat = resultat, + resultatBegrunnelse = resultatBegrunnelse, + periodeFom = periodeFom, + periodeTom = periodeTom, + begrunnelse = begrunnelse, + sistEndretIBehandlingId = nyttPersonResultat.vilkårsvurdering.behandling.id, + regelInput = regelInput, + regelOutput = regelOutput, + erEksplisittAvslagPåSøknad = erEksplisittAvslagPåSøknad, + vurderesEtter = vurderesEtter, + utdypendeVilkårsvurderinger = utdypendeVilkårsvurderinger, + standardbegrunnelser = standardbegrunnelser, + ) + } + + fun oppdaterPekerTilBehandling() { + sistEndretIBehandlingId = personResultat!!.vilkårsvurdering.behandling.id + } + + fun erAvslagUtenPeriode() = + this.erEksplisittAvslagPåSøknad == true && this.periodeFom == null && this.periodeTom == null + + fun harFremtidigTom() = this.periodeTom == null || this.periodeTom!!.isAfter(LocalDate.now().sisteDagIMåned()) + + fun erOppfylt() = this.resultat == Resultat.OPPFYLT + + companion object { + + val VilkårResultatComparator = compareBy({ it.periodeFom }, { it.resultat }, { it.vilkårType }) + } +} + +enum class Regelverk { + NASJONALE_REGLER, EØS_FORORDNINGEN +} + +enum class ResultatBegrunnelse( + val gyldigForVilkår: List, + val gyldigIKombinasjonMedResultat: List, + val gyldigForRegelverk: List, +) { + IKKE_AKTUELT( + gyldigForVilkår = listOf(Vilkår.LOVLIG_OPPHOLD), + gyldigIKombinasjonMedResultat = listOf(Resultat.OPPFYLT), + gyldigForRegelverk = listOf(Regelverk.EØS_FORORDNINGEN), + ), +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245rResultatTidslinje.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245rResultatTidslinje.kt" new file mode 100644 index 000000000..d4f9fdd22 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245rResultatTidslinje.kt" @@ -0,0 +1,23 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligSent +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt.Companion.tilTidspunktEllerUendeligTidlig + +class VilkårResultatTidslinje( + private val vilkårResultater: Collection, +) : Tidslinje() { + + override fun lagPerioder(): List> = + vilkårResultater.map { + Periode( + fraOgMed = it.periodeFom.tilTidspunktEllerUendeligTidlig(it.periodeTom), + tilOgMed = it.periodeTom.tilTidspunktEllerUendeligSent(it.periodeFom), + innhold = it, + ) + } +} + +fun List.tilTidslinje() = VilkårResultatTidslinje(this) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245rsvurdering.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245rsvurdering.kt" new file mode 100644 index 000000000..24fbe372c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245rsvurdering.kt" @@ -0,0 +1,95 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene + +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.sikkerhet.RollestyringMotDatabase + +@EntityListeners(RollestyringMotDatabase::class) +@Entity(name = "Vilkårsvurdering") +@Table(name = "VILKAARSVURDERING") +data class Vilkårsvurdering( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "vilkaarsvurdering_seq_generator") + @SequenceGenerator( + name = "vilkaarsvurdering_seq_generator", + sequenceName = "vilkaarsvurdering_seq", + allocationSize = 50, + ) + val id: Long = 0, + + @ManyToOne(optional = false) + @JoinColumn(name = "fk_behandling_id", nullable = false, updatable = false) + val behandling: Behandling, + + @Column(name = "aktiv", nullable = false) + var aktiv: Boolean = true, + + @OneToMany( + fetch = FetchType.EAGER, + mappedBy = "vilkårsvurdering", + cascade = [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH], + ) + var personResultater: Set = setOf(), + + @Column(name = "ytelse_personer", columnDefinition = "text") + var ytelsePersoner: String? = null, + +) : BaseEntitet() { + + override fun toString(): String { + return "Vilkårsvurdering(id=$id, behandling=${behandling.id})" + } + + fun kopier(inkluderAndreVurderinger: Boolean = false): Vilkårsvurdering { + val nyVilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + aktiv = aktiv, + ) + + nyVilkårsvurdering.personResultater = personResultater.map { + it.kopierMedParent( + vilkårsvurdering = nyVilkårsvurdering, + inkluderAndreVurderinger = inkluderAndreVurderinger, + ) + }.toSet() + return nyVilkårsvurdering + } + + fun tilKopiForNyBehandling( + nyBehandling: Behandling, + personopplysningGrunnlag: PersonopplysningGrunnlag, + ): Vilkårsvurdering { + val nyVilkårsvurdering = Vilkårsvurdering(behandling = nyBehandling) + + nyVilkårsvurdering.personResultater = + personResultater.filter { personopplysningGrunnlag.personer.any { person -> person.aktør.aktørId == it.aktør.aktørId } } + .map { + it.tilKopiForNyVilkårsvurdering(nyVilkårsvurdering = nyVilkårsvurdering) + }.toSet() + + return nyVilkårsvurdering + } + + fun finnOpplysningspliktVilkår(): AnnenVurdering? { + return personResultater.single { it.erSøkersResultater() } + .andreVurderinger.singleOrNull { it.type == AnnenVurderingType.OPPLYSNINGSPLIKT } + } + + fun hentPersonResultaterTil(aktørId: String): List = + personResultater.find { it.aktør.aktørId == aktørId }?.vilkårResultater?.toList() + ?: throw IllegalStateException("Fant ikke personresultat for $aktørId") +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245rsvurderingRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245rsvurderingRepository.kt" new file mode 100644 index 000000000..26d555afc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/domene/Vilk\303\245rsvurderingRepository.kt" @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface VilkårsvurderingRepository : JpaRepository { + + @Query("SELECT v FROM Vilkårsvurdering v JOIN v.behandling b WHERE b.id = :behandlingId AND v.aktiv = true") + fun findByBehandlingAndAktiv(behandlingId: Long): Vilkårsvurdering? +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/AuditLogger.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/AuditLogger.kt new file mode 100644 index 000000000..b0470f2ee --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/AuditLogger.kt @@ -0,0 +1,75 @@ +package no.nav.familie.ba.sak.sikkerhet + +import jakarta.servlet.http.HttpServletRequest +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.log.mdc.MDCConstants +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.web.context.request.RequestContextHolder +import org.springframework.web.context.request.ServletRequestAttributes + +/** + * [custom1], [custom2], [custom3] brukes for å logge ekstra felter, eks fagsak, behandling, + * disse logges til cs3,cs5,cs6 då cs1,cs2 og cs4 er til internt bruk + * Kan brukes med eks CustomKeyValue(key=fagsak, value=fagsakId) + */ +data class Sporingsdata( + val event: AuditLoggerEvent, + val personIdent: String, + val custom1: CustomKeyValue? = null, + val custom2: CustomKeyValue? = null, + val custom3: CustomKeyValue? = null, +) + +data class CustomKeyValue(val key: String, val value: String) + +@Component +class AuditLogger(@Value("\${NAIS_APP_NAME}") private val applicationName: String) { + + private val logger = LoggerFactory.getLogger(javaClass) + private val audit = LoggerFactory.getLogger("auditLogger") + + fun log(data: Sporingsdata) { + val request = getRequest() ?: throw IllegalArgumentException("Ikke brukt i context av en HTTP request") + + if (!SikkerhetContext.erMaskinTilMaskinToken()) { + audit.info(createAuditLogString(data, request)) + } else { + logger.debug("Maskin til maskin token i request") + } + } + + private fun getRequest(): HttpServletRequest? { + return RequestContextHolder.getRequestAttributes() + ?.takeIf { it is ServletRequestAttributes } + ?.let { it as ServletRequestAttributes } + ?.request + } + + private fun createAuditLogString(data: Sporingsdata, request: HttpServletRequest): String { + val timestamp = System.currentTimeMillis() + val name = "Saksbehandling" + return "CEF:0|Familie|$applicationName|1.0|audit:${data.event.type}|$name|INFO|end=$timestamp " + + "suid=${SikkerhetContext.hentSaksbehandler()} " + + "duid=${data.personIdent} " + + "sproc=${getCallId()} " + + "requestMethod=${request.method} " + + "request=${request.requestURI} " + + createCustomString(data) + } + + private fun createCustomString(data: Sporingsdata): String { + return listOfNotNull( + data.custom1?.let { "cs3Label=${it.key} cs3=${it.value}" }, + data.custom2?.let { "cs5Label=${it.key} cs5=${it.value}" }, + data.custom3?.let { "cs6Label=${it.key} cs6=${it.value}" }, + ) + .joinToString(" ") + } + + private fun getCallId(): String { + return MDC.get(MDCConstants.MDC_CALL_ID) ?: throw IllegalStateException("Mangler callId") + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/RollestyringMotDatabase.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/RollestyringMotDatabase.kt new file mode 100644 index 000000000..8bfb7f630 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/RollestyringMotDatabase.kt @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.sikkerhet + +import jakarta.persistence.PrePersist +import jakarta.persistence.PreRemove +import jakarta.persistence.PreUpdate +import no.nav.familie.ba.sak.common.RolleTilgangskontrollFeil +import no.nav.familie.ba.sak.config.RolleConfig +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component + +@Component +class RollestyringMotDatabase { + + @Autowired + private lateinit var rolleConfig: RolleConfig + + @PrePersist + @PreUpdate + @PreRemove + fun kontrollerSkrivetilgang(objekt: Any) { + val høyesteRolletilgang = SikkerhetContext.hentHøyesteRolletilgangForInnloggetBruker(rolleConfig) + + if (!harSkrivetilgang(høyesteRolletilgang)) { + throw RolleTilgangskontrollFeil( + melding = "${SikkerhetContext.hentSaksbehandlerNavn()} med rolle $høyesteRolletilgang har ikke skrivetilgang til databasen.", + frontendFeilmelding = "Du har ikke tilgang til å gjøre denne handlingen.", + ) + } + } + + private fun harSkrivetilgang(høyesteRolletilgang: BehandlerRolle) = + høyesteRolletilgang.nivå >= BehandlerRolle.SAKSBEHANDLER.nivå +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/SaksbehandlerContext.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/SaksbehandlerContext.kt new file mode 100644 index 000000000..6e747f672 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/SaksbehandlerContext.kt @@ -0,0 +1,21 @@ +package no.nav.familie.ba.sak.sikkerhet + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component + +@Component +class SaksbehandlerContext( + @Value("\${rolle.kode6}") + private val kode6GruppeId: String, +) { + + fun hentSaksbehandlerSignaturTilBrev(): String { + val grupper = SikkerhetContext.hentGrupper() + + return if (grupper.contains(kode6GruppeId)) { + "" + } else { + SikkerhetContext.hentSaksbehandlerNavn() + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/SikkerhetContext.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/SikkerhetContext.kt new file mode 100644 index 000000000..6fc9e0066 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/SikkerhetContext.kt @@ -0,0 +1,104 @@ +package no.nav.familie.ba.sak.sikkerhet + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.config.RolleConfig +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.security.token.support.spring.SpringTokenValidationContextHolder + +object SikkerhetContext { + + const val SYSTEM_FORKORTELSE = "VL" + const val SYSTEM_NAVN = "System" + + fun erSystemKontekst() = hentSaksbehandler() == SYSTEM_FORKORTELSE + + fun erMaskinTilMaskinToken(): Boolean { + val claims = SpringTokenValidationContextHolder().tokenValidationContext.getClaims("azuread") + return claims.get("oid") != null && + claims.get("oid") == claims.get("sub") && + claims.getAsList("roles").contains("access_as_application") + } + + fun harInnloggetBrukerForvalterRolle(rolleConfig: RolleConfig): Boolean = + hentGrupper().contains(rolleConfig.FORVALTER_ROLLE) + + fun hentSaksbehandler(): String { + return Result.runCatching { SpringTokenValidationContextHolder().tokenValidationContext } + .fold( + onSuccess = { it.getClaims("azuread")?.get("NAVident")?.toString() ?: SYSTEM_FORKORTELSE }, + onFailure = { SYSTEM_FORKORTELSE }, + ) + } + + fun hentSaksbehandlerEpost(): String { + return Result.runCatching { SpringTokenValidationContextHolder().tokenValidationContext } + .fold( + onSuccess = { it.getClaims("azuread")?.get("preferred_username")?.toString() ?: SYSTEM_FORKORTELSE }, + onFailure = { SYSTEM_FORKORTELSE }, + ) + } + + fun hentSaksbehandlerNavn(): String { + return Result.runCatching { SpringTokenValidationContextHolder().tokenValidationContext } + .fold( + onSuccess = { it.getClaims("azuread")?.get("name")?.toString() ?: SYSTEM_NAVN }, + onFailure = { SYSTEM_NAVN }, + ) + } + + fun hentRolletilgangFraSikkerhetscontext( + rolleConfig: RolleConfig, + lavesteSikkerhetsnivå: BehandlerRolle?, + ): BehandlerRolle { + if (hentSaksbehandler() == SYSTEM_FORKORTELSE) return BehandlerRolle.SYSTEM + + val grupper = hentGrupper() + val høyesteSikkerhetsnivåForInnloggetBruker: BehandlerRolle = + when { + grupper.contains(rolleConfig.BESLUTTER_ROLLE) -> BehandlerRolle.BESLUTTER + grupper.contains(rolleConfig.SAKSBEHANDLER_ROLLE) -> BehandlerRolle.SAKSBEHANDLER + grupper.contains(rolleConfig.VEILEDER_ROLLE) -> BehandlerRolle.VEILEDER + else -> BehandlerRolle.UKJENT + } + + return when { + lavesteSikkerhetsnivå == null -> BehandlerRolle.UKJENT + høyesteSikkerhetsnivåForInnloggetBruker.nivå >= lavesteSikkerhetsnivå.nivå -> lavesteSikkerhetsnivå + else -> BehandlerRolle.UKJENT + } + } + + fun hentHøyesteRolletilgangForInnloggetBruker(rolleConfig: RolleConfig): BehandlerRolle { + if (hentSaksbehandler() == SYSTEM_FORKORTELSE) return BehandlerRolle.SYSTEM + + val grupper = hentGrupper() + return when { + grupper.contains(rolleConfig.BESLUTTER_ROLLE) -> BehandlerRolle.BESLUTTER + grupper.contains(rolleConfig.SAKSBEHANDLER_ROLLE) -> BehandlerRolle.SAKSBEHANDLER + grupper.contains(rolleConfig.VEILEDER_ROLLE) -> BehandlerRolle.VEILEDER + else -> BehandlerRolle.UKJENT + } + } + + fun hentGrupper(): List { + return Result.runCatching { SpringTokenValidationContextHolder().tokenValidationContext } + .fold( + onSuccess = { + @Suppress("UNCHECKED_CAST") + it.getClaims("azuread")?.get("groups") as List? ?: emptyList() + }, + onFailure = { emptyList() }, + ) + } + + fun kallKommerFraKlage(): Boolean { + return kallKommerFra("teamfamilie:familie-klage") + } + + private fun kallKommerFra(forventetApplikasjonsSuffix: String): Boolean { + val claims = SpringTokenValidationContextHolder().tokenValidationContext.getClaims("azuread") + val applikasjonsnavn = claims.get("azp_name")?.toString() ?: "" // e.g. dev-gcp:some-team:application-name + secureLogger.info("Applikasjonsnavn: $applikasjonsnavn") + return applikasjonsnavn.endsWith(forventetApplikasjonsSuffix) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/TilgangController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/TilgangController.kt new file mode 100644 index 000000000..ec3505508 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/TilgangController.kt @@ -0,0 +1,42 @@ +package no.nav.familie.ba.sak.sikkerhet + +import no.nav.familie.ba.sak.ekstern.restDomene.TilgangDTO +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollService +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api") +@ProtectedWithClaims(issuer = "azuread") +class TilgangController( + private val personopplysningerService: PersonopplysningerService, + private val personidentService: PersonidentService, + private val familieIntegrasjonerTilgangskontrollService: FamilieIntegrasjonerTilgangskontrollService, +) { + + @PostMapping(path = ["tilgang"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentTilgangOgDiskresjonskode(@RequestBody tilgangRequestDTO: TilgangRequestDTO): ResponseEntity> { + val aktør = personidentService.hentAktør(tilgangRequestDTO.brukerIdent) + + val adressebeskyttelse = personopplysningerService.hentAdressebeskyttelseSomSystembruker(aktør) + val tilgang = familieIntegrasjonerTilgangskontrollService.sjekkTilgangTilPerson(tilgangRequestDTO.brukerIdent) + return ResponseEntity.ok( + Ressurs.success( + data = TilgangDTO( + saksbehandlerHarTilgang = tilgang.harTilgang, + adressebeskyttelsegradering = adressebeskyttelse, + ), + ), + ) + } +} + +class TilgangRequestDTO(val brukerIdent: String) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/TilgangService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/TilgangService.kt new file mode 100644 index 000000000..2a8020a08 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/sikkerhet/TilgangService.kt @@ -0,0 +1,149 @@ +package no.nav.familie.ba.sak.sikkerhet + +import no.nav.familie.ba.sak.common.BehandlingValidering.validerBehandlingKanRedigeres +import no.nav.familie.ba.sak.common.RolleTilgangskontrollFeil +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.config.RolleConfig +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import org.springframework.cache.CacheManager +import org.springframework.stereotype.Service + +@Service +class TilgangService( + private val fagsakService: FagsakService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val persongrunnlagService: PersongrunnlagService, + private val rolleConfig: RolleConfig, + private val familieIntegrasjonerTilgangskontrollService: FamilieIntegrasjonerTilgangskontrollService, + private val cacheManager: CacheManager, + private val auditLogger: AuditLogger, +) { + + /** + * Sjekk om saksbehandler har tilgang til å gjøre en gitt handling. + * + * @minimumBehandlerRolle den laveste rolle som kreves for den angitte handlingen + * @handling kort beskrivelse for handlingen. Eksempel: 'endre vilkår', 'oppprette behandling'. + * Handlingen kommer til saksbehandler så det er viktig at denne gir mening. + */ + fun verifiserHarTilgangTilHandling(minimumBehandlerRolle: BehandlerRolle, handling: String) { + val høyesteRolletilgang = SikkerhetContext.hentHøyesteRolletilgangForInnloggetBruker(rolleConfig) + if (minimumBehandlerRolle.nivå > høyesteRolletilgang.nivå) { + throw RolleTilgangskontrollFeil( + melding = "${SikkerhetContext.hentSaksbehandlerNavn()} med rolle $høyesteRolletilgang " + + "har ikke tilgang til å $handling. Krever $minimumBehandlerRolle.", + frontendFeilmelding = "Du har ikke tilgang til å $handling.", + ) + } + } + + fun validerTilgangTilPersoner(personIdenter: List, event: AuditLoggerEvent) { + personIdenter.forEach { auditLogger.log(Sporingsdata(event, it)) } + if (!harTilgangTilPersoner(personIdenter)) { + throw RolleTilgangskontrollFeil( + melding = "Saksbehandler ${SikkerhetContext.hentSaksbehandler()} " + + "har ikke tilgang.", + frontendFeilmelding = "Saksbehandler ${SikkerhetContext.hentSaksbehandler()} " + + "har ikke tilgang til $personIdenter", + ) + } + } + + /** + * sjekkTilgangTilPersoner er cachet i [familieIntegrasjonerTilgangskontrollService] + */ + private fun harTilgangTilPersoner(personIdenter: List): Boolean { + return familieIntegrasjonerTilgangskontrollService.sjekkTilgangTilPersoner(personIdenter) + .all { it.value.harTilgang } + } + + fun validerTilgangTilBehandling(behandlingId: Long, event: AuditLoggerEvent) { + val harTilgang = harSaksbehandlerTilgang("validerTilgangTilBehandling", behandlingId) { + val personIdenter = persongrunnlagService.hentSøkerOgBarnPåBehandling(behandlingId) + ?.map { it.aktør.aktivFødselsnummer() } + ?: listOf(behandlingHentOgPersisterService.hent(behandlingId).fagsak.aktør.aktivFødselsnummer()) + personIdenter.forEach { + auditLogger.log( + Sporingsdata( + event = event, + personIdent = it, + custom1 = CustomKeyValue("behandling", behandlingId.toString()), + ), + ) + } + harTilgangTilPersoner(personIdenter) + } + if (!harTilgang) { + throw RolleTilgangskontrollFeil( + "Saksbehandler ${SikkerhetContext.hentSaksbehandler()} " + + "har ikke tilgang til behandling=$behandlingId", + ) + } + } + + fun validerTilgangTilFagsak(fagsakId: Long, event: AuditLoggerEvent) { + val aktør = fagsakService.hentAktør(fagsakId) + aktør.personidenter.forEach { + Sporingsdata( + event = event, + personIdent = it.fødselsnummer, + custom1 = CustomKeyValue("fagsak", fagsakId.toString()), + ) + } + val personIdenterIFagsak = ( + persongrunnlagService.hentSøkerOgBarnPåFagsak(fagsakId) + ?.map { it.aktør.aktivFødselsnummer() } + ?: emptyList() + ) + .ifEmpty { listOf(aktør.aktivFødselsnummer()) } + val harTilgang = harTilgangTilPersoner(personIdenterIFagsak) + if (!harTilgang) { + throw RolleTilgangskontrollFeil( + melding = "Saksbehandler ${SikkerhetContext.hentSaksbehandler()} " + + "har ikke tilgang til fagsak=$fagsakId.", + frontendFeilmelding = "Saksbehandler ${SikkerhetContext.hentSaksbehandler()} " + + "har ikke tilgang til fagsak=$fagsakId.", + ) + } + } + + /** + * Sjekk om saksbehandler har tilgang til å gjøre bestemt handling og om saksbehandler kan behandle fagsak + * @param fagsakId id til fagsak det skal sjekkes tilgang til + * @param event operasjon som skal gjøres med identene + * @param minimumBehandlerRolle den laveste rolle som kreves for den angitte handlingen + * @param handling kort beskrivelse for handlingen. + */ + fun validerTilgangTilHandlingOgFagsak( + fagsakId: Long, + event: AuditLoggerEvent, + minimumBehandlerRolle: BehandlerRolle, + handling: String, + ) { + verifiserHarTilgangTilHandling(minimumBehandlerRolle, handling) + validerTilgangTilFagsak(fagsakId, event) + } + + fun validerKanRedigereBehandling(behandlingId: Long) { + validerBehandlingKanRedigeres(behandlingHentOgPersisterService.hentStatus(behandlingId)) + } + + /** + * Sjekker cache om tilgangen finnes siden tidligere, hvis ikke hentes verdiet med [hentVerdi] + * Resultatet caches sammen med identen for saksbehandleren på gitt [cacheName] + * @param cacheName navnet på cachen + * @param verdi verdiet som man ønsket å hente cache for, eks behandlingId, eller personIdent + */ + private fun harSaksbehandlerTilgang(cacheName: String, verdi: T, hentVerdi: () -> Boolean): Boolean { + if (SikkerhetContext.erSystemKontekst()) return true + + val cache = cacheManager.getCache(cacheName) ?: error("Finner ikke cache=$cacheName") + return cache.get(Pair(verdi, SikkerhetContext.hentSaksbehandler())) { + hentVerdi() + } ?: error("Finner ikke verdi fra cache=$cacheName") + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/internstatistikk/InternStatistikkController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/internstatistikk/InternStatistikkController.kt new file mode 100644 index 000000000..29e5ee013 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/internstatistikk/InternStatistikkController.kt @@ -0,0 +1,65 @@ +package no.nav.familie.ba.sak.statistikk.internstatistikk + +import no.nav.familie.ba.sak.common.RessursUtils +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingSøknadsinfoService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.SøknadsstatistikkForPeriode +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.format.annotation.DateTimeFormat +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate + +@RestController +@RequestMapping("/api/internstatistikk") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class InternStatistikkController( + private val internStatistikkService: InternStatistikkService, + private val behandlingSøknadsinfoService: BehandlingSøknadsinfoService, +) { + + @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentAntallFagsakerOpprettet(): ResponseEntity> { + logger.info("${SikkerhetContext.hentSaksbehandlerNavn()} henter internstatistikk") + val internstatistikk = InternStatistikkResponse( + antallFagsakerTotalt = internStatistikkService.finnAntallFagsakerTotalt(), + antallFagsakerLøpende = internStatistikkService.finnAntallFagsakerLøpende(), + antallBehandlingerIkkeFerdigstilt = internStatistikkService.finnAntallBehandlingerIkkeErAvsluttet(), + antallBehandlingerPerÅrsak = internStatistikkService.finnAntallBehandlingerPerÅrsak(), + ) + return ResponseEntity.ok(Ressurs.Companion.success(internstatistikk)) + } + + @GetMapping(path = ["antallSoknader"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun hentSøknadsstatistikkForPeriode( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) fom: LocalDate?, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) tom: LocalDate?, + ): ResponseEntity> { + val fomDato = fom ?: LocalDate.now().minusMonths(4).withDayOfMonth(1) + val tomDato = tom ?: fomDato.plusMonths(4).minusDays(1) + + return RessursUtils.ok(behandlingSøknadsinfoService.hentSøknadsstatistikk(fomDato, tomDato)) + } + + companion object { + + private val logger: Logger = LoggerFactory.getLogger(InternStatistikkController::class.java) + } +} + +data class InternStatistikkResponse( + val antallFagsakerTotalt: Long, + val antallFagsakerLøpende: Long, + val antallBehandlingerIkkeFerdigstilt: Long, + val antallBehandlingerPerÅrsak: Map, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/internstatistikk/InternStatistikkService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/internstatistikk/InternStatistikkService.kt new file mode 100644 index 000000000..7a1538f43 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/internstatistikk/InternStatistikkService.kt @@ -0,0 +1,17 @@ +package no.nav.familie.ba.sak.statistikk.internstatistikk + +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import org.springframework.stereotype.Service + +@Service +class InternStatistikkService( + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, +) { + fun finnAntallFagsakerTotalt() = fagsakRepository.finnAntallFagsakerTotalt() + fun finnAntallFagsakerLøpende() = fagsakRepository.finnAntallFagsakerLøpende() + fun finnAntallBehandlingerIkkeErAvsluttet() = behandlingRepository.finnAntallBehandlingerIkkeAvsluttet() + fun finnAntallBehandlingerPerÅrsak() = + behandlingRepository.finnAntallBehandlingerPerÅrsak().associate { it.first to it.second } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/producer/KafkaProducer.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/producer/KafkaProducer.kt new file mode 100644 index 000000000..38d1b3356 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/producer/KafkaProducer.kt @@ -0,0 +1,243 @@ +package no.nav.familie.ba.sak.statistikk.producer + +import com.fasterxml.jackson.databind.ObjectMapper +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.ekstern.pensjon.HentAlleIdenterTilPsysResponseDTO +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagring +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringRepository +import no.nav.familie.eksterne.kontrakter.VedtakDVHV2 +import no.nav.familie.eksterne.kontrakter.bisys.BarnetrygdBisysMelding +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRespons +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +interface KafkaProducer { + + fun sendMessageForTopicVedtakV2(vedtakV2: VedtakDVHV2): Long + fun sendMessageForTopicBehandling(melding: SaksstatistikkMellomlagring): Long + fun sendMessageForTopicSak(melding: SaksstatistikkMellomlagring): Long + + fun sendFagsystemsbehandlingResponsForTopicTilbakekreving( + melding: HentFagsystemsbehandlingRespons, + key: String, + behandlingId: String, + ) + + fun sendBarnetrygdBisysMelding( + behandlingId: String, + barnetrygdBisysMelding: BarnetrygdBisysMelding, + ) + + fun sendIdentTilPSys( + hentAlleIdenterTilPsysResponseDTO: HentAlleIdenterTilPsysResponseDTO, + ) +} + +@Service +@ConditionalOnProperty( + value = ["funksjonsbrytere.kafka.producer.enabled"], + havingValue = "true", + matchIfMissing = false, +) +@Primary +@Profile("!preprod-gcp & !prod-gcp") +class DefaultKafkaProducer(val saksstatistikkMellomlagringRepository: SaksstatistikkMellomlagringRepository) : + KafkaProducer { + + private val vedtakV2Counter = Metrics.counter(COUNTER_NAME, "type", "vedtakV2") + private val saksstatistikkSakDvhCounter = Metrics.counter(COUNTER_NAME, "type", "sak") + private val saksstatistikkBehandlingDvhCounter = Metrics.counter(COUNTER_NAME, "type", "behandling") + + @Autowired + @Qualifier("kafkaObjectMapper") + lateinit var kafkaObjectMapper: ObjectMapper + + @Autowired + lateinit var kafkaAivenTemplate: KafkaTemplate + + override fun sendMessageForTopicVedtakV2(vedtakV2: VedtakDVHV2): Long { + val vedtakForDVHV2Melding = + kafkaObjectMapper.writeValueAsString(vedtakV2) + val response = kafkaAivenTemplate.send(VEDTAKV2_TOPIC, vedtakV2.funksjonellId, vedtakForDVHV2Melding).get() + logger.info("$VEDTAKV2_TOPIC -> message sent -> ${response.recordMetadata.offset()}") + vedtakV2Counter.increment() + return response.recordMetadata.offset() + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + override fun sendMessageForTopicBehandling(melding: SaksstatistikkMellomlagring): Long { + val behandlingsMelding = kafkaObjectMapper.writeValueAsString(melding.jsonToBehandlingDVH()) + + val response = + kafkaAivenTemplate.send(SAKSSTATISTIKK_BEHANDLING_TOPIC, melding.funksjonellId, behandlingsMelding).get() + logger.info("$SAKSSTATISTIKK_BEHANDLING_TOPIC -> message sent -> offset=${response.recordMetadata.offset()}") + + saksstatistikkBehandlingDvhCounter.increment() + melding.offsetVerdi = response.recordMetadata.offset() + melding.sendtTidspunkt = LocalDateTime.now() + saksstatistikkMellomlagringRepository.save(melding) + return response.recordMetadata.offset() + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + override fun sendMessageForTopicSak(melding: SaksstatistikkMellomlagring): Long { + val saksMelding = kafkaObjectMapper.writeValueAsString(melding.jsonToSakDVH()) + + val response = + kafkaAivenTemplate.send(SAKSSTATISTIKK_SAK_TOPIC, melding.funksjonellId, saksMelding).get() + logger.info("$SAKSSTATISTIKK_SAK_TOPIC -> message sent -> offset=${response.recordMetadata.offset()}") + + saksstatistikkSakDvhCounter.increment() + melding.offsetVerdi = response.recordMetadata.offset() + melding.sendtTidspunkt = LocalDateTime.now() + saksstatistikkMellomlagringRepository.save(melding) + return response.recordMetadata.offset() + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + override fun sendFagsystemsbehandlingResponsForTopicTilbakekreving( + melding: HentFagsystemsbehandlingRespons, + key: String, + behandlingId: String, + ) { + val meldingIString: String = objectMapper.writeValueAsString(melding) + + kafkaAivenTemplate.send(FAGSYSTEMSBEHANDLING_RESPONS_TBK_TOPIC, key, meldingIString) + .thenAccept { + logger.info( + "Melding på topic $FAGSYSTEMSBEHANDLING_RESPONS_TBK_TOPIC for " + + "$behandlingId med $key er sendt. " + + "Fikk offset ${it?.recordMetadata?.offset()}", + ) + } + .exceptionally { + val feilmelding = + "Melding på topic $FAGSYSTEMSBEHANDLING_RESPONS_TBK_TOPIC kan ikke sendes for " + + "$behandlingId med $key. Feiler med ${it.message}" + logger.warn(feilmelding) + throw Feil(message = feilmelding) + } + } + + override fun sendIdentTilPSys( + hentAlleIdenterTilPsysResponseDTO: HentAlleIdenterTilPsysResponseDTO, + ) { + kafkaAivenTemplate.send(BARNETRYGD_PENSJON_TOPIC, objectMapper.writeValueAsString(hentAlleIdenterTilPsysResponseDTO)) + .exceptionally { + val feilmelding = + "Melding på topic $BARNETRYGD_PENSJON_TOPIC kan ikke sendes for " + + "RequestId: ${hentAlleIdenterTilPsysResponseDTO.requestId}. Feiler med ${it.message}" + logger.warn(feilmelding) + throw Feil(message = feilmelding) + } + } + + override fun sendBarnetrygdBisysMelding( + behandlingId: String, + barnetrygdBisysMelding: BarnetrygdBisysMelding, + ) { + val opphørBarnetrygdBisysMelding = + objectMapper.writeValueAsString(barnetrygdBisysMelding) + + kafkaAivenTemplate.send(OPPHOER_BARNETRYGD_BISYS_TOPIC, behandlingId, opphørBarnetrygdBisysMelding) + .thenAccept { + logger.info( + "Melding på topic $OPPHOER_BARNETRYGD_BISYS_TOPIC for " + + "$behandlingId er sendt. " + + "Fikk offset ${it?.recordMetadata?.offset()}", + ) + secureLogger.info("Send barnetrygd bisys melding $opphørBarnetrygdBisysMelding") + } + .exceptionally { + val feilmelding = + "Melding på topic $OPPHOER_BARNETRYGD_BISYS_TOPIC kan ikke sendes for " + + "$behandlingId. Feiler med ${it.message}" + logger.warn(feilmelding) + throw Feil(message = feilmelding) + } + } + + companion object { + + private val logger = LoggerFactory.getLogger(DefaultKafkaProducer::class.java) + private const val VEDTAKV2_TOPIC = "teamfamilie.aapen-barnetrygd-vedtak-v2" + private const val SAKSSTATISTIKK_BEHANDLING_TOPIC = "teamfamilie.aapen-barnetrygd-saksstatistikk-behandling-v1" + private const val SAKSSTATISTIKK_SAK_TOPIC = "teamfamilie.aapen-barnetrygd-saksstatistikk-sak-v1" + private const val COUNTER_NAME = "familie.ba.sak.kafka.produsert" + private const val FAGSYSTEMSBEHANDLING_RESPONS_TBK_TOPIC = + "teamfamilie.privat-tbk-hentfagsystemsbehandling-respons-topic" + const val OPPHOER_BARNETRYGD_BISYS_TOPIC = "teamfamilie.aapen-familie-ba-sak-opphoer-barnetrygd" + const val BARNETRYGD_PENSJON_TOPIC = "teamfamilie.aapen-familie-ba-sak-identer-med-barnetrygd" + } +} + +@Service +class MockKafkaProducer(val saksstatistikkMellomlagringRepository: SaksstatistikkMellomlagringRepository) : + KafkaProducer { + + override fun sendMessageForTopicVedtakV2(vedtakV2: VedtakDVHV2): Long { + logger.info("Skipper sending av vedtakV2 for ${vedtakV2.behandlingsId} fordi kafka Aiven for DVH V2 ikke er enablet") + + sendteMeldinger["vedtakV2-${vedtakV2.behandlingsId}"] = vedtakV2 + return 0 + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + override fun sendMessageForTopicBehandling(melding: SaksstatistikkMellomlagring): Long { + logger.info("Skipper sending av saksstatistikk behandling for ${melding.jsonToBehandlingDVH().behandlingId} fordi kafka ikke er enablet") + sendteMeldinger["behandling-${melding.jsonToBehandlingDVH().behandlingId}"] = melding.jsonToBehandlingDVH() + melding.offsetVerdiOnPrem = 42 + melding.sendtTidspunkt = LocalDateTime.now() + saksstatistikkMellomlagringRepository.save(melding) + return 42 + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + override fun sendMessageForTopicSak(melding: SaksstatistikkMellomlagring): Long { + logger.info("Skipper sending av saksstatistikk sak for ${melding.jsonToSakDVH().sakId} fordi kafka ikke er enablet") + sendteMeldinger["sak-${melding.jsonToSakDVH().sakId}"] = melding.jsonToSakDVH() + melding.offsetVerdiOnPrem = 43 + melding.sendtTidspunkt = LocalDateTime.now() + saksstatistikkMellomlagringRepository.save(melding) + return 43 + } + + override fun sendFagsystemsbehandlingResponsForTopicTilbakekreving( + melding: HentFagsystemsbehandlingRespons, + key: String, + behandlingId: String, + ) { + logger.info("Skipper sending av fagsystemsbehandling respons for $behandlingId fordi kafka ikke er enablet") + } + override fun sendIdentTilPSys( + hentAlleIdenterTilPsysResponseDTO: HentAlleIdenterTilPsysResponseDTO, + ) { + logger.info("Skipper sending av sendBarnetrygdBisysMelding respons for $hentAlleIdenterTilPsysResponseDTO.requestId fordi kafka ikke er enablet") + } + + override fun sendBarnetrygdBisysMelding( + behandlingId: String, + barnetrygdBisysMelding: BarnetrygdBisysMelding, + ) { + logger.info("Skipper sending av sendOpphørBarnetrygdBisys respons for $behandlingId fordi kafka ikke er enablet") + } + + companion object { + + private val logger = LoggerFactory.getLogger(MockKafkaProducer::class.java) + + var sendteMeldinger = mutableMapOf() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkController.kt new file mode 100644 index 000000000..614c78e11 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkController.kt @@ -0,0 +1,89 @@ +package no.nav.familie.ba.sak.statistikk.saksstatistikk + +import io.swagger.v3.oas.annotations.Operation +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagring +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringRepository +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringType +import no.nav.familie.eksterne.kontrakter.saksstatistikk.BehandlingDVH +import no.nav.familie.eksterne.kontrakter.saksstatistikk.SakDVH +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.slf4j.LoggerFactory +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDateTime + +@RestController +@RequestMapping("/api/saksstatistikk") +@ProtectedWithClaims(issuer = "azuread") +class SaksstatistikkController( + private val saksstatistikkService: SaksstatistikkService, + private val saksstatistikkMellomlagringRepository: SaksstatistikkMellomlagringRepository, +) { + + private val logger = LoggerFactory.getLogger(SaksstatistikkController::class.java) + + @GetMapping(path = ["/behandling/{behandlingId}"]) + fun hentBehandlingDvh(@PathVariable(name = "behandlingId", required = true) behandlingId: Long): BehandlingDVH { + try { + return saksstatistikkService.mapTilBehandlingDVH(behandlingId)!! + } catch (e: Exception) { + logger.warn("Feil ved henting av sakstatistikk behandling", e) + throw e + } + } + + @GetMapping(path = ["/sak/{fagsakId}"]) + fun hentSakDvh(@PathVariable(name = "fagsakId", required = true) fagsakId: Long): SakDVH { + try { + return saksstatistikkService.mapTilSakDvh(fagsakId)!! + } catch (e: Exception) { + logger.warn("Feil ved henting av sakstatistikk sak", e) + throw e + } + } + + @Operation( + description = "Oppdaterer saksstatistikk mellomlagring om at en melding har blitt sendt. Setter sendtTidspunkt slik at melding ikke blir sendt på nytt.", + ) + @PostMapping(path = ["/registrer-sendt-fra-statistikk"]) + fun registrerSendtFraStatistikk(@RequestBody(required = true) input: SaksstatistikkSendtRequest): ResponseEntity { + try { + val jsnoNode = sakstatistikkObjectMapper.readTree(input.json) + val funksjonellId = jsnoNode.get("funksjonellId").asText() + val typeId = if (input.type == SaksstatistikkMellomlagringType.SAK) { + jsnoNode.get("sakId").asLong() + } else { + jsnoNode.get("behandlingId").asLong() + } + val kontraktversjon = jsnoNode.get("versjon").asText() + + val sm = SaksstatistikkMellomlagring( + offsetVerdiOnPrem = input.offset, + funksjonellId = funksjonellId, + type = input.type, + json = input.json, + typeId = typeId, + kontraktVersjon = kontraktversjon, + sendtTidspunkt = input.sendtTidspunkt, + ) + + saksstatistikkMellomlagringRepository.saveAndFlush(sm) + return ResponseEntity.ok(sm) + } catch (e: Exception) { + logger.warn("Feil ved registrering av sendt", e) + throw e + } + } + + data class SaksstatistikkSendtRequest( + val offset: Long, + val type: SaksstatistikkMellomlagringType, + val json: String, + val sendtTidspunkt: LocalDateTime, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkEventListener.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkEventListener.kt new file mode 100644 index 000000000..06f5f2b13 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkEventListener.kt @@ -0,0 +1,50 @@ +package no.nav.familie.ba.sak.statistikk.saksstatistikk + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagring +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringRepository +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringType +import no.nav.familie.kontrakter.felles.objectMapper +import org.springframework.context.ApplicationListener +import org.springframework.stereotype.Component + +@Component +class SaksstatistikkEventListener( + private val saksstatistikkService: SaksstatistikkService, + private val saksstatistikkMellomlagringRepository: SaksstatistikkMellomlagringRepository, +) : ApplicationListener { + + override fun onApplicationEvent(event: SaksstatistikkEvent) { + if (event.behandlingId != null) { + saksstatistikkService.mapTilBehandlingDVH(event.behandlingId)?.also { + saksstatistikkMellomlagringRepository.save( + SaksstatistikkMellomlagring( + funksjonellId = it.funksjonellId, + kontraktVersjon = it.versjon, + json = sakstatistikkObjectMapper.writeValueAsString(it), + type = SaksstatistikkMellomlagringType.BEHANDLING, + typeId = event.behandlingId, + ), + ) + } + } else if (event.fagsakId != null) { + saksstatistikkService.mapTilSakDvh(event.fagsakId)?.also { + saksstatistikkMellomlagringRepository.save( + SaksstatistikkMellomlagring( + funksjonellId = it.funksjonellId, + kontraktVersjon = it.versjon, + json = sakstatistikkObjectMapper.writeValueAsString(it), + type = SaksstatistikkMellomlagringType.SAK, + typeId = event.fagsakId, + ), + ) + } + } + } +} + +val sakstatistikkObjectMapper: ObjectMapper = objectMapper.copy() + .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) + .configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkEventPublisher.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkEventPublisher.kt new file mode 100644 index 000000000..6440bbd86 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkEventPublisher.kt @@ -0,0 +1,27 @@ +package no.nav.familie.ba.sak.statistikk.saksstatistikk + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationEvent +import org.springframework.context.ApplicationEventPublisher +import org.springframework.stereotype.Component + +@Component +class SaksstatistikkEventPublisher { + + @Autowired + lateinit var applicationEventPublisher: ApplicationEventPublisher + + fun publiserBehandlingsstatistikk(behandlingId: Long) { + applicationEventPublisher.publishEvent(SaksstatistikkEvent(this, null, behandlingId)) + } + + fun publiserSaksstatistikk(fagsakId: Long) { + applicationEventPublisher.publishEvent(SaksstatistikkEvent(this, fagsakId, null)) + } +} + +class SaksstatistikkEvent( + source: Any, + val fagsakId: Long?, + val behandlingId: Long?, +) : ApplicationEvent(source) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkScheduler.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkScheduler.kt new file mode 100644 index 000000000..93dc155b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkScheduler.kt @@ -0,0 +1,51 @@ +package no.nav.familie.ba.sak.statistikk.saksstatistikk + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.statistikk.producer.KafkaProducer +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringRepository +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringType +import no.nav.familie.leader.LeaderClient +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +class SaksstatistikkScheduler( + val saksstatistikkMellomlagringRepository: SaksstatistikkMellomlagringRepository, + val kafkaProducer: KafkaProducer, +) { + + @Scheduled(fixedDelay = 60000) + fun sendKafkameldinger() { + if (LeaderClient.isLeader() == true) { + sendSaksstatistikk() + } + } + + @Transactional + fun sendSaksstatistikk() { + val meldinger = saksstatistikkMellomlagringRepository.finnMeldingerKlarForSending() + + for (melding in meldinger) { + try { + when (melding.type) { + SaksstatistikkMellomlagringType.SAK -> { + kafkaProducer.sendMessageForTopicSak(melding) + } + + SaksstatistikkMellomlagringType.BEHANDLING -> { + kafkaProducer.sendMessageForTopicBehandling(melding) + } + } + } catch (e: Exception) { + logger.error("Kunne ikke sende melding med ${melding.id},type ${melding.type} og fagsakId/behandlingid=${melding.typeId} til kafka") + secureLogger.error("Kunne ikke sende melding med ${melding.id},type ${melding.type} og fagsakId/behandlingid=${melding.typeId} til kafka. $melding", e) + } + } + } + + companion object { + private val logger = LoggerFactory.getLogger(SaksstatistikkScheduler::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkService.kt new file mode 100644 index 000000000..d5161ac0c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkService.kt @@ -0,0 +1,220 @@ +package no.nav.familie.ba.sak.statistikk.saksstatistikk + +import no.nav.familie.ba.sak.common.Utils.hentPropertyFraMaven +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingSøknadsinfoService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat.HENLAGT_FEILAKTIG_OPPRETTET +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat.HENLAGT_SØKNAD_TRUKKET +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType.BARN_ENSLIG_MINDREÅRIG +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType.INSTITUSJON +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType.NORMAL +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollService +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.eksterne.kontrakter.saksstatistikk.AktørDVH +import no.nav.familie.eksterne.kontrakter.saksstatistikk.BehandlingDVH +import no.nav.familie.eksterne.kontrakter.saksstatistikk.ResultatBegrunnelseDVH +import no.nav.familie.eksterne.kontrakter.saksstatistikk.SakDVH +import no.nav.familie.eksterne.kontrakter.saksstatistikk.SettPåVent +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.ZoneId +import java.time.ZonedDateTime +import java.util.UUID + +@Service +class SaksstatistikkService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val behandlingSøknadsinfoService: BehandlingSøknadsinfoService, + private val arbeidsfordelingService: ArbeidsfordelingService, + private val totrinnskontrollService: TotrinnskontrollService, + private val vedtakService: VedtakService, + private val fagsakService: FagsakService, + private val personopplysningerService: PersonopplysningerService, + private val persongrunnlagService: PersongrunnlagService, + private val vedtaksperiodeService: VedtaksperiodeService, + private val settPåVentService: SettPåVentService, +) { + + fun mapTilBehandlingDVH(behandlingId: Long): BehandlingDVH? { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + val forrigeBehandlingId = behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling) + .takeIf { erRevurderingEllerTekniskBehandling(behandling) }?.id + + val datoMottatt = when (behandling.opprettetÅrsak) { + BehandlingÅrsak.SØKNAD -> { + behandlingSøknadsinfoService.hentSøknadMottattDato(behandlingId) ?: behandling.opprettetTidspunkt + } + + else -> behandling.opprettetTidspunkt + } + + val behandlendeEnhetsKode = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId).behandlendeEnhetId + val ansvarligEnhetKode = arbeidsfordelingService.hentArbeidsfordelingsenhet(behandling).enhetId + + val aktivtVedtak = vedtakService.hentAktivForBehandling(behandlingId) + val totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(behandlingId) + + val now = ZonedDateTime.now() + + return BehandlingDVH( + funksjonellTid = now, + tekniskTid = now, + mottattDato = datoMottatt.atZone(TIMEZONE), + registrertDato = behandling.opprettetTidspunkt.atZone(TIMEZONE), + behandlingId = behandling.id.toString(), + funksjonellId = UUID.randomUUID().toString(), + sakId = behandling.fagsak.id.toString(), + behandlingType = behandling.type.name, + behandlingStatus = behandling.status.name, + behandlingKategori = when (behandling.underkategori) { // Gjøres pga. tilpasning til DVH-modell + BehandlingUnderkategori.ORDINÆR, BehandlingUnderkategori.UTVIDET -> + behandling.underkategori.name + }, + behandlingUnderkategori = when (behandling.fagsak.type) { // <-' + NORMAL -> null + BARN_ENSLIG_MINDREÅRIG -> ENSLIG_MINDREÅRIG_KODE + INSTITUSJON -> INSTITUSJON.name + }, + behandlingAarsak = behandling.opprettetÅrsak.name, + automatiskBehandlet = behandling.skalBehandlesAutomatisk, + utenlandstilsnitt = behandling.kategori.name, // Gjøres pga. tilpasning til DVH-modell + ansvarligEnhetKode = ansvarligEnhetKode, + behandlendeEnhetKode = behandlendeEnhetsKode, + ansvarligEnhetType = "NORG", + behandlendeEnhetType = "NORG", + totrinnsbehandling = !behandling.skalBehandlesAutomatisk, + avsender = "familie-ba-sak", + versjon = hentPropertyFraMaven("familie.kontrakter.saksstatistikk") ?: "2", + // Ikke påkrevde felt + vedtaksDato = aktivtVedtak?.vedtaksdato?.toLocalDate(), + relatertBehandlingId = forrigeBehandlingId?.toString(), + vedtakId = aktivtVedtak?.id?.toString(), + resultat = behandling.resultat.name, + behandlingTypeBeskrivelse = behandling.type.visningsnavn, + resultatBegrunnelser = behandling.resultatBegrunnelser(aktivtVedtak), + behandlingOpprettetAv = behandling.opprettetAv, + behandlingOpprettetType = "saksbehandlerId", + behandlingOpprettetTypeBeskrivelse = "saksbehandlerId. VL ved automatisk behandling", + beslutter = totrinnskontroll?.beslutterId, + saksbehandler = totrinnskontroll?.saksbehandlerId, + settPaaVent = hentSettPåVentDVH(behandlingId), + ) + } + + private fun hentSettPåVentDVH(behandlingId: Long): SettPåVent? { + val settPåVent = settPåVentService.finnAktivSettPåVentPåBehandling(behandlingId) ?: return null + return SettPåVent( + frist = settPåVent.frist.atStartOfDay(TIMEZONE), + tidSattPaaVent = settPåVent.tidSattPåVent.atStartOfDay(TIMEZONE), + aarsak = settPåVent.årsak.name, + ) + } + + fun mapTilSakDvh(sakId: Long): SakDVH? { + val aktivBehandling = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = sakId) + val fagsak = aktivBehandling?.fagsak ?: fagsakService.hentPåFagsakId(sakId) + + var landkodeSøker: String = PersonopplysningerService.UKJENT_LANDKODE + + val deltagere = if (aktivBehandling != null) { + val personer = persongrunnlagService.hentAktiv(behandlingId = aktivBehandling.id)?.søkerOgBarn ?: emptySet() + personer.map { + if (it.type == PersonType.SØKER) { + landkodeSøker = hentLandkode(it) + } + AktørDVH( + it.aktør.aktørId.toLong(), + it.type.name, + ) + } + } else { + landkodeSøker = hentLandkode(fagsak.aktør) + listOf(AktørDVH(fagsak.aktør.aktørId.toLong(), PersonType.SØKER.name)) + } + + return SakDVH( + funksjonellTid = ZonedDateTime.now(), + tekniskTid = ZonedDateTime.now(), + opprettetDato = LocalDate.now(), + funksjonellId = UUID.randomUUID().toString(), + sakId = sakId.toString(), + aktorId = fagsak.aktør.aktørId.toLong(), + aktorer = deltagere, + sakStatus = fagsak.status.name, + avsender = "familie-ba-sak", + versjon = hentPropertyFraMaven("familie.kontrakter.saksstatistikk") ?: "2", + bostedsland = landkodeSøker, + ) + } + + private fun hentLandkode(person: Person): String { + return if (person.bostedsadresser.isNotEmpty()) { + "NO" + } else { + personopplysningerService.hentLandkodeAlpha2UtenlandskBostedsadresse( + person.aktør, + ) + } + } + + private fun hentLandkode(aktør: Aktør): String { + val personInfo = personopplysningerService.hentPersoninfoEnkel(aktør) + + return if (personInfo.bostedsadresser.isNotEmpty()) { + "NO" + } else { + personopplysningerService.hentLandkodeAlpha2UtenlandskBostedsadresse( + aktør, + ) + } + } + + private fun erRevurderingEllerTekniskBehandling(behandling: Behandling) = + behandling.type == BehandlingType.REVURDERING || behandling.type == BehandlingType.TEKNISK_OPPHØR || behandling.type == BehandlingType.TEKNISK_ENDRING + + private fun Behandling.resultatBegrunnelser(vedtak: Vedtak?): List { + return when (resultat) { + HENLAGT_SØKNAD_TRUKKET, HENLAGT_FEILAKTIG_OPPRETTET -> emptyList() + else -> + vedtak + ?.hentResultatBegrunnelserFraVedtaksbegrunnelser() + ?: emptyList() + } + } + + private fun Vedtak.hentResultatBegrunnelserFraVedtaksbegrunnelser(): List { + return vedtaksperiodeService.hentPersisterteVedtaksperioder(this) + .flatMap { vedtaksperiode -> + vedtaksperiode.begrunnelser + .map { + ResultatBegrunnelseDVH( + fom = vedtaksperiode.fom, + tom = vedtaksperiode.tom, + type = it.standardbegrunnelse.vedtakBegrunnelseType.name, + vedtakBegrunnelse = it.standardbegrunnelse.name, + ) + } + } + } + + companion object { + + val TIMEZONE: ZoneId = ZoneId.systemDefault() + val ENSLIG_MINDREÅRIG_KODE = "ENSLIG_MINDREÅRIG" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/domene/SaksstatistikkMellomlagring.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/domene/SaksstatistikkMellomlagring.kt new file mode 100644 index 000000000..8fe8b1f94 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/domene/SaksstatistikkMellomlagring.kt @@ -0,0 +1,75 @@ +package no.nav.familie.ba.sak.statistikk.saksstatistikk.domene + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.SequenceGenerator +import jakarta.persistence.Table +import no.nav.familie.ba.sak.statistikk.saksstatistikk.sakstatistikkObjectMapper +import no.nav.familie.eksterne.kontrakter.saksstatistikk.BehandlingDVH +import no.nav.familie.eksterne.kontrakter.saksstatistikk.SakDVH +import org.hibernate.annotations.JdbcTypeCode +import org.hibernate.type.SqlTypes +import java.time.LocalDateTime + +@Entity(name = "SaksstatistikkMellomlagring") +@Table(name = "SAKSSTATISTIKK_MELLOMLAGRING") +data class SaksstatistikkMellomlagring( + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "saksstatistikk_mellomlagring_seq_generator") + @SequenceGenerator( + name = "saksstatistikk_mellomlagring_seq_generator", + sequenceName = "SAKSSTATISTIKK_MELLOMLAGRING_SEQ", + allocationSize = 50, + ) + val id: Long = 0, + + @Column(name = "offset_verdi") + var offsetVerdiOnPrem: Long? = null, + + @Column(name = "offset_aiven") + var offsetVerdi: Long? = null, + + @Column(name = "funksjonell_id") + val funksjonellId: String, + + @Column(name = "type") + @Enumerated(EnumType.STRING) + val type: SaksstatistikkMellomlagringType, + + @Column(name = "kontrakt_versjon") + val kontraktVersjon: String, + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "json") + val json: String, + + @Column(name = "opprettet_tid", nullable = false, updatable = false) + val opprettetTidspunkt: LocalDateTime = LocalDateTime.now(), + + @Column(name = "konvertert_tid") + var konvertertTidspunkt: LocalDateTime? = null, + + @Column(name = "sendt_tid") + var sendtTidspunkt: LocalDateTime? = null, + + @Column(name = "type_id") + var typeId: Long? = null, +) { + fun jsonToSakDVH(): SakDVH { + return sakstatistikkObjectMapper.readValue(json, SakDVH::class.java) + } + + fun jsonToBehandlingDVH(): BehandlingDVH { + return sakstatistikkObjectMapper.readValue(json, BehandlingDVH::class.java) + } +} + +enum class SaksstatistikkMellomlagringType { + SAK, + BEHANDLING, +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/domene/SaksstatistikkMellomlagringRepository.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/domene/SaksstatistikkMellomlagringRepository.kt new file mode 100644 index 000000000..2050ff419 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/domene/SaksstatistikkMellomlagringRepository.kt @@ -0,0 +1,14 @@ +package no.nav.familie.ba.sak.statistikk.saksstatistikk.domene + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository + +@Repository +interface SaksstatistikkMellomlagringRepository : JpaRepository { + + @Query(value = "SELECT s FROM SaksstatistikkMellomlagring s WHERE s.sendtTidspunkt IS NULL") + fun finnMeldingerKlarForSending(): List + + fun findByTypeAndTypeId(type: SaksstatistikkMellomlagringType, typeId: Long): List +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/st\303\270nadsstatistikk/St\303\270nadsstatistikkController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/st\303\270nadsstatistikk/St\303\270nadsstatistikkController.kt" new file mode 100644 index 000000000..d87e20ad7 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/st\303\270nadsstatistikk/St\303\270nadsstatistikkController.kt" @@ -0,0 +1,93 @@ +package no.nav.familie.ba.sak.statistikk.stønadsstatistikk + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.statistikk.StatistikkClient +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.task.PubliserVedtakV2Task +import no.nav.familie.eksterne.kontrakter.VedtakDVHV2 +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.slf4j.LoggerFactory +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/stonadsstatistikk") +@ProtectedWithClaims(issuer = "azuread") +class StønadsstatistikkController( + private val stønadsstatistikkService: StønadsstatistikkService, + private val taskRepository: TaskRepositoryWrapper, + private val behandlingRepository: BehandlingRepository, + private val statistikkClient: StatistikkClient, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, +) { + + private val logger = LoggerFactory.getLogger(StønadsstatistikkController::class.java) + + @PostMapping(path = ["/vedtakV2"]) + fun hentVedtakDvhV2(@RequestBody(required = true) behandlinger: List): List { + try { + return behandlinger.map { stønadsstatistikkService.hentVedtakV2(it) } + } catch (e: Exception) { + logger.warn("Feil ved henting av stønadsstatistikk V2 for $behandlinger", e) + throw e + } + } + + @PostMapping(path = ["/send-til-dvh"]) + fun sendTilStønadsstatistikk(@RequestBody(required = true) behandlinger: List) { + behandlinger.forEach { + if (!statistikkClient.harSendtVedtaksmeldingForBehandling(it)) { + val vedtakV2DVH = stønadsstatistikkService.hentVedtakV2(it) + val vedtakV2Task = PubliserVedtakV2Task.opprettTask(vedtakV2DVH.personV2.personIdent, it) + taskRepository.save(vedtakV2Task) + } + } + } + + @PostMapping(path = ["/send-til-dvh-manuell"]) + fun sendTilStønadsstatistikkManuell(@RequestBody(required = true) behandlinger: List) { + behandlinger.forEach { + val vedtakV2DVH = stønadsstatistikkService.hentVedtakV2(it) + val vedtakV2Task = PubliserVedtakV2Task.opprettTask(vedtakV2DVH.personV2.personIdent, it) + taskRepository.save(vedtakV2Task) + } + } + + @PostMapping(path = ["/ettersend-manuell-migrering/{dryRun}"]) + fun ettersendManuellMigrereringer(@PathVariable dryRun: Boolean = true) { + val manuelleMigreringer = behandlingRepository.finnBehandlingIdMedOpprettetÅrsak( + listOf( + BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + BehandlingÅrsak.HELMANUELL_MIGRERING, + ), + ) + + manuelleMigreringer.forEach { + if (!statistikkClient.harSendtVedtaksmeldingForBehandling(it) && erIverksattBehandling(it)) { + logger.info("Ettersender stønadstatistikk for behandlingId=$it dryRun=$dryRun") + val vedtakV2DVH = stønadsstatistikkService.hentVedtakV2(it) + if (!dryRun) { + secureLogger.info("Oppretter task for å ettersende vedtak $vedtakV2DVH.person.personIdent") + val vedtakV2Task = PubliserVedtakV2Task.opprettTask(vedtakV2DVH.personV2.personIdent, it) + taskRepository.save(vedtakV2Task) + } + } + } + } + + private fun erIverksattBehandling(behandlingId: Long): Boolean { + val tilkjentYtelse = tilkjentYtelseRepository.findByBehandlingOptional(behandlingId) + + return if (tilkjentYtelse != null) { + tilkjentYtelse.utbetalingsoppdrag != null + } else { + false + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/st\303\270nadsstatistikk/St\303\270nadsstatistikkService.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/st\303\270nadsstatistikk/St\303\270nadsstatistikkService.kt" new file mode 100644 index 000000000..285b57c77 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/statistikk/st\303\270nadsstatistikk/St\303\270nadsstatistikkService.kt" @@ -0,0 +1,230 @@ +package no.nav.familie.ba.sak.statistikk.stønadsstatistikk + +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.beregnUtbetalingsperioderUtenKlassifisering +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.KompetanseService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.filtrerGjeldendeNå +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.eksterne.kontrakter.BehandlingTypeV2 +import no.nav.familie.eksterne.kontrakter.BehandlingÅrsakV2 +import no.nav.familie.eksterne.kontrakter.FagsakType +import no.nav.familie.eksterne.kontrakter.KategoriV2 +import no.nav.familie.eksterne.kontrakter.Kompetanse +import no.nav.familie.eksterne.kontrakter.KompetanseAktivitet +import no.nav.familie.eksterne.kontrakter.KompetanseResultat +import no.nav.familie.eksterne.kontrakter.PersonDVHV2 +import no.nav.familie.eksterne.kontrakter.UnderkategoriV2 +import no.nav.familie.eksterne.kontrakter.UtbetalingsDetaljDVHV2 +import no.nav.familie.eksterne.kontrakter.UtbetalingsperiodeDVHV2 +import no.nav.familie.eksterne.kontrakter.VedtakDVHV2 +import no.nav.familie.eksterne.kontrakter.YtelseType.ORDINÆR_BARNETRYGD +import no.nav.familie.eksterne.kontrakter.YtelseType.SMÅBARNSTILLEGG +import no.nav.familie.eksterne.kontrakter.YtelseType.UTVIDET_BARNETRYGD +import no.nav.fpsak.tidsserie.LocalDateInterval +import no.nav.fpsak.tidsserie.LocalDateSegment +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.ZoneId +import java.util.UUID + +@Service +class StønadsstatistikkService( + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val persongrunnlagService: PersongrunnlagService, + private val vedtakService: VedtakService, + private val personopplysningerService: PersonopplysningerService, + private val vedtakRepository: VedtakRepository, + private val kompetanseService: KompetanseService, + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, +) { + + fun hentVedtakV2(behandlingId: Long): VedtakDVHV2 { + val vedtak = vedtakService.hentAktivForBehandling(behandlingId) + val behandling = vedtak?.behandling ?: behandlingHentOgPersisterService.hent(behandlingId) + val persongrunnlag = persongrunnlagService.hentAktivThrows(behandlingId) + // DVH ønsker tidspunkt med klokkeslett + + var datoVedtak = vedtak?.vedtaksdato + + if (datoVedtak == null) { + datoVedtak = vedtakRepository.finnVedtakForBehandling(behandlingId).singleOrNull()?.vedtaksdato + ?: error("Fant ikke vedtaksdato for behandling $behandlingId") + } + + val tidspunktVedtak = datoVedtak + return VedtakDVHV2( + fagsakId = behandling.fagsak.id.toString(), + fagsakType = FagsakType.valueOf(behandling.fagsak.type.name), + behandlingsId = behandlingId.toString(), + tidspunktVedtak = tidspunktVedtak.atZone(TIMEZONE), + personV2 = hentSøkerV2(persongrunnlag), + ensligForsørger = utledEnsligForsørger(behandlingId), // TODO implementere støtte for dette + kategoriV2 = KategoriV2.valueOf(behandling.kategori.name), + underkategoriV2 = when (behandling.underkategori) { + BehandlingUnderkategori.ORDINÆR -> UnderkategoriV2.ORDINÆR + BehandlingUnderkategori.UTVIDET -> UnderkategoriV2.UTVIDET + }, + behandlingTypeV2 = BehandlingTypeV2.valueOf(behandling.type.name), + utbetalingsperioderV2 = hentUtbetalingsperioderV2(behandling, persongrunnlag), + funksjonellId = UUID.randomUUID().toString(), + kompetanseperioder = hentKompetanse(BehandlingId(behandlingId)), + behandlingÅrsakV2 = BehandlingÅrsakV2.valueOf(behandling.opprettetÅrsak.name), + ) + } + + private fun hentKompetanse(behandlingId: BehandlingId): List { + val kompetanser = kompetanseService.hentKompetanser(behandlingId) + + return kompetanser.filter { it.resultat != null }.map { kompetanse -> + Kompetanse( + barnsIdenter = kompetanse.barnAktører.map { aktør -> aktør.aktivFødselsnummer() }, + annenForeldersAktivitet = if (kompetanse.annenForeldersAktivitet != null) { + KompetanseAktivitet.valueOf( + kompetanse.annenForeldersAktivitet.name, + ) + } else { + null + }, + annenForeldersAktivitetsland = kompetanse.annenForeldersAktivitetsland, + barnetsBostedsland = kompetanse.barnetsBostedsland, + fom = kompetanse.fom!!, + tom = kompetanse.tom, + resultat = KompetanseResultat.valueOf(kompetanse.resultat!!.name), + sokersaktivitet = if (kompetanse.søkersAktivitet != null) KompetanseAktivitet.valueOf(kompetanse.søkersAktivitet.name) else null, + sokersAktivitetsland = kompetanse.søkersAktivitetsland, + ) + } + } + + private fun hentSøkerV2(persongrunnlag: PersonopplysningGrunnlag): PersonDVHV2 { + val søker = persongrunnlag.søker + return lagPersonDVHV2(søker) + } + + private fun hentUtbetalingsperioderV2( + behandling: Behandling, + persongrunnlag: PersonopplysningGrunnlag, + ): List { + val andelerTilkjentYtelse = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnAndelerTilkjentYtelseMedEndreteUtbetalinger(behandling.id) + + if (andelerTilkjentYtelse.isEmpty()) return emptyList() + + val utbetalingsPerioder = beregnUtbetalingsperioderUtenKlassifisering(andelerTilkjentYtelse) + + val søkerOgBarn = persongrunnlag.søkerOgBarn + return utbetalingsPerioder.toSegments() + .sortedWith(compareBy>({ it.fom }, { it.value }, { it.tom })) + .map { segment -> + val andelerForSegment = andelerTilkjentYtelse.filter { + segment.localDateInterval.overlaps( + LocalDateInterval( + it.stønadFom.førsteDagIInneværendeMåned(), + it.stønadTom.sisteDagIInneværendeMåned(), + ), + ) + } + mapTilUtbetalingsperiodeV2( + segment, + andelerForSegment, + behandling, + søkerOgBarn, + ) + } + } + + private fun utledEnsligForsørger(behandlingId: Long): Boolean { + val andelerTilkjentYtelse = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnAndelerTilkjentYtelseMedEndreteUtbetalinger(behandlingId) + if (andelerTilkjentYtelse.isEmpty()) { + return false + } + + return andelerTilkjentYtelse.find { it.type == YtelseType.UTVIDET_BARNETRYGD } != null + } + + private fun mapTilUtbetalingsperiodeV2( + segment: LocalDateSegment, + andelerForSegment: List, + behandling: Behandling, + søkerOgBarn: List, + ): UtbetalingsperiodeDVHV2 { + return UtbetalingsperiodeDVHV2( + hjemmel = "Ikke implementert", + stønadFom = segment.fom, + stønadTom = segment.tom, + utbetaltPerMnd = segment.value, + utbetalingsDetaljer = andelerForSegment.filter { it.erAndelSomSkalSendesTilOppdrag() }.map { andel -> + val personForAndel = + søkerOgBarn.find { person -> andel.aktør == person.aktør } + ?: throw IllegalStateException("Fant ikke personopplysningsgrunnlag for andel") + UtbetalingsDetaljDVHV2( + person = lagPersonDVHV2( + personForAndel, + andel.prosent.intValueExact(), + ), + klassekode = andel.type.klassifisering, + ytelseType = when (andel.type) { + YtelseType.ORDINÆR_BARNETRYGD -> ORDINÆR_BARNETRYGD + YtelseType.UTVIDET_BARNETRYGD -> UTVIDET_BARNETRYGD + YtelseType.SMÅBARNSTILLEGG -> SMÅBARNSTILLEGG + }, + utbetaltPrMnd = andel.kalkulertUtbetalingsbeløp, + delytelseId = behandling.fagsak.id.toString() + andel.periodeOffset, + ) + }, + ) + } + + private fun lagPersonDVHV2(person: Person, delingsProsentYtelse: Int = 0): PersonDVHV2 { + return PersonDVHV2( + rolle = person.type.name, + statsborgerskap = hentStatsborgerskap(person), + bostedsland = hentLandkode(person), + delingsprosentYtelse = if (delingsProsentYtelse == 50) delingsProsentYtelse else 0, + personIdent = person.aktør.aktivFødselsnummer(), + ) + } + + private fun hentStatsborgerskap(person: Person): List { + return if (person.statsborgerskap.isNotEmpty()) { + person.statsborgerskap.filtrerGjeldendeNå().map { it.landkode } + } else { + listOf(personopplysningerService.hentGjeldendeStatsborgerskap(person.aktør).land) + } + } + + private fun hentLandkode(person: Person): String = if (person.bostedsadresser.isNotEmpty()) { + "NO" + } else if (personopplysningerService.hentPersoninfoEnkel(person.aktør).bostedsadresser.isNotEmpty()) { + "NO" + } else { + val landKode = personopplysningerService.hentLandkodeAlpha2UtenlandskBostedsadresse(person.aktør) + + if (landKode == PersonopplysningerService.UKJENT_LANDKODE) { + logger.info("Sender landkode ukjent til DVH") + secureLogger.info("Ukjent land sendt til DVH for person ${person.aktør.aktivFødselsnummer()}") + } + landKode + } + + companion object { + + private val logger = LoggerFactory.getLogger(StønadsstatistikkService::class.java) + private val TIMEZONE = ZoneId.of("Europe/Paris") + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/BehandleF\303\270dselshendelseTask.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/BehandleF\303\270dselshendelseTask.kt" new file mode 100644 index 000000000..26ec84c67 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/BehandleF\303\270dselshendelseTask.kt" @@ -0,0 +1,117 @@ +package no.nav.familie.ba.sak.task + +import io.micrometer.core.instrument.DistributionSummary +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdFeedService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakStegService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.FagsystemRegelVurdering +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.VelgFagSystemService +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.StartSatsendring +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.task.dto.BehandleFødselshendelseTaskDTO +import no.nav.familie.kontrakter.felles.Fødselsnummer +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.error.RekjørSenereException +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import java.util.Properties + +@Service +@TaskStepBeskrivelse( + taskStepType = BehandleFødselshendelseTask.TASK_STEP_TYPE, + beskrivelse = "Setter i gang behandlingsløp for fødselshendelse", + maxAntallFeil = 3, +) +class BehandleFødselshendelseTask( + private val autovedtakStegService: AutovedtakStegService, + private val velgFagsystemService: VelgFagSystemService, + private val infotrygdFeedService: InfotrygdFeedService, + private val personidentService: PersonidentService, + private val startSatsendring: StartSatsendring, + private val taskRepositoryWrapper: TaskRepositoryWrapper, +) : AsyncTaskStep { + + private val dagerSidenBarnBleFødt: DistributionSummary = Metrics.summary("foedselshendelse.dagersidenbarnfoedt") + + override fun doTask(task: Task) { + val behandleFødselshendelseTaskDTO = + objectMapper.readValue(task.payload, BehandleFødselshendelseTaskDTO::class.java) + + val nyBehandling = behandleFødselshendelseTaskDTO.nyBehandling + + logger.info("Behandler fødselshendelse") + secureLogger.info("Behandler fødselshendelse, mor=${nyBehandling.morsIdent}, barna=${nyBehandling.barnasIdenter}") + + nyBehandling.barnasIdenter.forEach { + // En litt forenklet løsning for å hente fødselsdato uten å kalle PDL. Gir ikke helt riktige data, men godt nok. + val dagerSidenBarnetBleFødt = + ChronoUnit.DAYS.between( + Fødselsnummer(it).fødselsdato, + LocalDateTime.now(), + ) + dagerSidenBarnBleFødt.record(dagerSidenBarnetBleFødt.toDouble()) + } + + try { + when (velgFagsystemService.velgFagsystem(nyBehandling).first) { + FagsystemRegelVurdering.SEND_TIL_BA -> { + val harOpprettetSatsendring = + startSatsendring.sjekkOgOpprettSatsendringVedGammelSats(nyBehandling.morsIdent) + if (harOpprettetSatsendring) { + throw RekjørSenereException( + "Satsendring skal kjøre ferdig før man behandler fødselsehendelse", + LocalDateTime.now().plusMinutes(60), + ) + } + autovedtakStegService.kjørBehandlingFødselshendelse( + mottakersAktør = personidentService.hentAktør( + nyBehandling.morsIdent, + ), + nyBehandlingHendelse = nyBehandling, + ) + } + + FagsystemRegelVurdering.SEND_TIL_INFOTRYGD -> { + infotrygdFeedService.sendTilInfotrygdFeed(nyBehandling.barnasIdenter) + } + } + } catch (e: FunksjonellFeil) { + val aktør = personidentService.hentAktør(nyBehandling.morsIdent) + taskRepositoryWrapper.save( + OpprettVurderFødselshendelseKonsekvensForYtelseOppgave.opprettTask( + ident = aktør.aktørId, + oppgavetype = Oppgavetype.VurderLivshendelse, + beskrivelse = "Saksbehandler må vurdere konsekvens for ytelse fordi fødselshendelsen ikke kunne håndteres automatisk", + ), + ) + } + } + + companion object { + + const val TASK_STEP_TYPE = "behandleFødselshendelseTask" + private val logger = LoggerFactory.getLogger(BehandleFødselshendelseTask::class.java) + + fun opprettTask(behandleFødselshendelseTaskDTO: BehandleFødselshendelseTaskDTO): Task { + val triggerTid = if (erKlokkenMellom21Og06()) kl06IdagEllerNesteDag() else LocalDateTime.now() + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString(behandleFødselshendelseTaskDTO), + properties = Properties().apply { + this["morsIdent"] = behandleFødselshendelseTaskDTO.nyBehandling.morsIdent + }, + ).copy( + triggerTid = triggerTid.plusDays(7), + ) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/DistribuerDokumentP\303\245JournalpostIdTask.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/DistribuerDokumentP\303\245JournalpostIdTask.kt" new file mode 100644 index 000000000..71ae0f0d1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/DistribuerDokumentP\303\245JournalpostIdTask.kt" @@ -0,0 +1,124 @@ +package no.nav.familie.ba.sak.task + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.Metrics +import io.sentry.Sentry +import no.nav.familie.ba.sak.kjerne.brev.DokumentDistribueringService +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.brev.mottakerErDødUtenDødsboadresse +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.http.client.RessursException +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.log.IdUtils +import no.nav.familie.log.mdc.MDCConstants +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.PropertiesWrapper +import no.nav.familie.prosessering.domene.Task +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.util.Properties + +const val ANTALL_SEKUNDER_I_EN_UKE = 604800L + +@Service +@TaskStepBeskrivelse( + taskStepType = DistribuerDokumentPåJournalpostIdTask.TASK_STEP_TYPE, + beskrivelse = "Distribuer dokument på journalpostId", + triggerTidVedFeilISekunder = ANTALL_SEKUNDER_I_EN_UKE, + // ~8 måneder dersom vi prøver én gang i uka. + // Tasken skal stoppe etter 6 måneder, så om vi kommer hit har det skjedd noe galt. + maxAntallFeil = 4 * 8, + settTilManuellOppfølgning = true, +) +class DistribuerDokumentPåJournalpostIdTask( + private val dokumentDistribueringService: DokumentDistribueringService, +) : AsyncTaskStep { + + private val antallBrevIkkeDistribuertUkjentDødsboadresse: Map = + mutableListOf().plus(Brevmal.values()).associateWith { + Metrics.counter( + "brev.ikke.sendt.ukjent.dodsbo", + "brevtype", + it.visningsTekst, + ) + } + + override fun doTask(task: Task) { + val taskData = if (task.payload.contains("personEllerInstitusjonIdent")) { + objectMapper.readValue(task.payload, DistribuerDokumentDTO::class.java) + } else { + fraGammelTilNyKontrakt(task) + } + val brevmal = taskData.brevmal + val erTaskEldreEnn6Mnd = task.opprettetTid.isBefore(LocalDateTime.now().minusMonths(6)) + + if (erTaskEldreEnn6Mnd) { + logger.info("Stopper \"DistribuerDødsfallDokumentPåFagsakTask\" fordi den er eldre enn 6 måneder.") + antallBrevIkkeDistribuertUkjentDødsboadresse[brevmal]?.increment() + } else { + try { + dokumentDistribueringService.prøvDistribuerBrevOgLoggHendelse( + distribuerDokumentDTO = taskData, + loggBehandlerRolle = BehandlerRolle.SYSTEM, + ) + } catch (e: Exception) { + if (e is RessursException && mottakerErDødUtenDødsboadresse(e)) { + logger.info( + "Klarte ikke å distribuere \"${brevmal.visningsTekst}\" på journalpost " + + "${taskData.journalpostId}. Prøver igjen om 7 dager.", + ) + throw e + } else { + Sentry.captureException(e) + throw e + } + } + } + } + + @Deprecated("TODO kan slettes når alle tasker med gammel kontrakt er ferdig kjørt. Siste ble opprettet 2023-02-08 11:46:12.218, så senest 2023-08-09") + private fun fraGammelTilNyKontrakt(task: Task): DistribuerDokumentDTO { + val dto = objectMapper.readValue(task.payload, GammelDistribuerDokumentDTO::class.java) + return DistribuerDokumentDTO( + behandlingId = dto.behandlingId, + journalpostId = dto.journalpostId, + personEllerInstitusjonIdent = "", + brevmal = dto.brevmal, + erManueltSendt = false, + manuellAdresseInfo = null, + ) + } + + companion object { + fun opprettTask(distribuerDokumentDTO: DistribuerDokumentDTO): Task { + check(distribuerDokumentDTO.behandlingId == null) + + val metadata = Properties().apply { + this["journalpostId"] = distribuerDokumentDTO.journalpostId + this["personEllerInstitusjonIdent"] = distribuerDokumentDTO.personEllerInstitusjonIdent + this[MDCConstants.MDC_CALL_ID] = MDC.get(MDCConstants.MDC_CALL_ID) ?: IdUtils.generateId() + } + + return Task( + type = this.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString(distribuerDokumentDTO), + triggerTid = LocalDateTime.now().plusMinutes(5), + metadataWrapper = PropertiesWrapper(metadata), + ) + } + + const val TASK_STEP_TYPE = "distribuerDokumentPåFagsak" + val logger: Logger = LoggerFactory.getLogger(this::class.java) + } +} + +@Deprecated("Kan slettes når alle tasker med gammel kontrakt er ferdig kjørt. Siste ble opprettet 2023-02-08 11:46:12.218, så senest 2023-08-09") +data class GammelDistribuerDokumentDTO( + val behandlingId: Long?, + val journalpostId: String, + val brevmal: Brevmal, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/DistribuerDokumentTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/DistribuerDokumentTask.kt new file mode 100644 index 000000000..66c25229e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/DistribuerDokumentTask.kt @@ -0,0 +1,80 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.brev.DokumentDistribueringService +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.domene.ManuellAdresseInfo +import no.nav.familie.ba.sak.task.DistribuerDokumentTask.Companion.TASK_STEP_TYPE +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service +import java.util.Properties + +@Service +@TaskStepBeskrivelse(taskStepType = TASK_STEP_TYPE, beskrivelse = "Send dokument til Dokdist", maxAntallFeil = 3) +class DistribuerDokumentTask( + private val stegService: StegService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val dokumentDistribueringService: DokumentDistribueringService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val distribuerDokumentDTO = objectMapper.readValue(task.payload, DistribuerDokumentDTO::class.java) + + val erManueltSendtOgIkkeVedtaksbrev = + distribuerDokumentDTO.erManueltSendt && !distribuerDokumentDTO.brevmal.erVedtaksbrev + val erVedtaksbrevOgIkkeManueltSent = + !distribuerDokumentDTO.erManueltSendt && distribuerDokumentDTO.brevmal.erVedtaksbrev + + if (erManueltSendtOgIkkeVedtaksbrev && distribuerDokumentDTO.behandlingId == null) { + dokumentDistribueringService.prøvDistribuerBrevOgLoggHendelse( + distribuerDokumentDTO = distribuerDokumentDTO, + loggBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + ) + } else if (erManueltSendtOgIkkeVedtaksbrev && distribuerDokumentDTO.behandlingId != null) { + dokumentDistribueringService.prøvDistribuerBrevOgLoggHendelseFraBehandling( + distribuerDokumentDTO = distribuerDokumentDTO, + loggBehandlerRolle = BehandlerRolle.SAKSBEHANDLER, + ) + } else if (erVedtaksbrevOgIkkeManueltSent && distribuerDokumentDTO.behandlingId != null) { + stegService.håndterDistribuerVedtaksbrev( + behandling = behandlingHentOgPersisterService.hent(distribuerDokumentDTO.behandlingId), + distribuerDokumentDTO = distribuerDokumentDTO, + ) + } else { + throw Feil("erManueltSendt=${distribuerDokumentDTO.erManueltSendt} ikke støttet for brev=${distribuerDokumentDTO.brevmal.visningsTekst}") + } + } + + companion object { + + fun opprettDistribuerDokumentTask( + distribuerDokumentDTO: DistribuerDokumentDTO, + properties: Properties, + ): Task { + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString(distribuerDokumentDTO), + properties = properties, + ).copy( + triggerTid = nesteGyldigeTriggertidForBehandlingIHverdager(), + ) + } + + const val TASK_STEP_TYPE = "distribuerDokument" + } +} + +data class DistribuerDokumentDTO( + val behandlingId: Long?, + val journalpostId: String, + val personEllerInstitusjonIdent: String, + val brevmal: Brevmal, + val erManueltSendt: Boolean, + val manuellAdresseInfo: ManuellAdresseInfo? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/DistribuerVedtaksbrevTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/DistribuerVedtaksbrevTask.kt new file mode 100644 index 000000000..3f2e51517 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/DistribuerVedtaksbrevTask.kt @@ -0,0 +1,49 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.task.DistribuerVedtaksbrevTask.Companion.TASK_STEP_TYPE +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service + +@Service +@TaskStepBeskrivelse(taskStepType = TASK_STEP_TYPE, beskrivelse = "Send vedtaksbrev til Dokdist", maxAntallFeil = 3) +class DistribuerVedtaksbrevTask( + private val stegService: StegService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val brevmalService: BrevmalService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val distribuerVedtaksbrevDTO = objectMapper.readValue(task.payload, DistribuerVedtaksbrevDTO::class.java) + + val behandling = behandlingHentOgPersisterService.hent(distribuerVedtaksbrevDTO.behandlingId) + + val distribuerDokumentDTO = DistribuerDokumentDTO( + behandlingId = distribuerVedtaksbrevDTO.behandlingId, + journalpostId = distribuerVedtaksbrevDTO.journalpostId, + personEllerInstitusjonIdent = distribuerVedtaksbrevDTO.personIdent, + brevmal = brevmalService.hentBrevmal(behandling), + erManueltSendt = false, + ) + stegService.håndterDistribuerVedtaksbrev( + behandling = behandlingHentOgPersisterService.hent(distribuerVedtaksbrevDTO.behandlingId), + distribuerDokumentDTO = distribuerDokumentDTO, + ) + } + + companion object { + + const val TASK_STEP_TYPE = "distribuerVedtaksbrev" + } +} + +data class DistribuerVedtaksbrevDTO( + val behandlingId: Long, + val journalpostId: String, + val personIdent: String, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/DistribuerVedtaksbrevTilInstitusjonVergeEllerManuellBrevMottakerTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/DistribuerVedtaksbrevTilInstitusjonVergeEllerManuellBrevMottakerTask.kt new file mode 100644 index 000000000..c634edd0a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/DistribuerVedtaksbrevTilInstitusjonVergeEllerManuellBrevMottakerTask.kt @@ -0,0 +1,47 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.kjerne.brev.DokumentDistribueringService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service +import java.util.Properties + +@Service +@TaskStepBeskrivelse( + taskStepType = DistribuerVedtaksbrevTilInstitusjonVergeEllerManuellBrevMottakerTask.TASK_STEP_TYPE, + beskrivelse = "Send vedtaksbrev til institusjon verge eller manuell brev mottaker til Dokdist", + maxAntallFeil = 3, +) +class DistribuerVedtaksbrevTilInstitusjonVergeEllerManuellBrevMottakerTask( + private val dokumentDistribueringService: DokumentDistribueringService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val distribuerDokumentDTO = objectMapper.readValue(task.payload, DistribuerDokumentDTO::class.java) + dokumentDistribueringService.prøvDistribuerBrevOgLoggHendelseFraBehandling( + distribuerDokumentDTO = distribuerDokumentDTO, + loggBehandlerRolle = BehandlerRolle.SYSTEM, + ) + } + + companion object { + + fun opprettDistribuerVedtaksbrevTilInstitusjonVergeEllerManuellBrevMottakerTask( + distribuerDokumentDTO: DistribuerDokumentDTO, + properties: Properties, + ): Task { + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString(distribuerDokumentDTO), + properties = properties, + ).copy( + triggerTid = nesteGyldigeTriggertidForBehandlingIHverdager(), + ) + } + + const val TASK_STEP_TYPE = "distribuerVedtaksbrevTilVergeEllerManuellBrevMottaker" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/FerdigstillBehandlingTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/FerdigstillBehandlingTask.kt new file mode 100644 index 000000000..24a0c9e88 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/FerdigstillBehandlingTask.kt @@ -0,0 +1,53 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.task.dto.FerdigstillBehandlingDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service +import java.util.Properties + +@Service +@TaskStepBeskrivelse( + taskStepType = FerdigstillBehandlingTask.TASK_STEP_TYPE, + beskrivelse = "Ferdigstill behandling", + maxAntallFeil = 3, +) +class FerdigstillBehandlingTask( + val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + val stegService: StegService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val ferdigstillBehandling = objectMapper.readValue(task.payload, FerdigstillBehandlingDTO::class.java) + stegService.håndterFerdigstillBehandling( + behandling = behandlingHentOgPersisterService.hent( + ferdigstillBehandling.behandlingsId, + ), + ) + } + + companion object { + + const val TASK_STEP_TYPE = "ferdigstillBehandling" + + fun opprettTask(søkerIdent: String, behandlingsId: Long): Task { + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + FerdigstillBehandlingDTO( + personIdent = søkerIdent, + behandlingsId = behandlingsId, + ), + ), + properties = Properties().apply { + this["personIdent"] = søkerIdent + this["behandlingsId"] = behandlingsId.toString() + }, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/FerdigstillOppgaver.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/FerdigstillOppgaver.kt new file mode 100644 index 000000000..df945dce2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/FerdigstillOppgaver.kt @@ -0,0 +1,45 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.task.dto.FerdigstillOppgaveDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service + +@Service +@TaskStepBeskrivelse( + taskStepType = FerdigstillOppgaver.TASK_STEP_TYPE, + beskrivelse = "Ferdigstill oppgaver i GOSYS for behandling", + maxAntallFeil = 3, +) +class FerdigstillOppgaver( + private val oppgaveService: OppgaveService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val ferdigstillOppgave = objectMapper.readValue(task.payload, FerdigstillOppgaveDTO::class.java) + oppgaveService.ferdigstillOppgaver( + behandlingId = ferdigstillOppgave.behandlingId, + oppgavetype = ferdigstillOppgave.oppgavetype, + ) + } + + companion object { + const val TASK_STEP_TYPE = "ferdigstillOppgaveTask" + + fun opprettTask(behandlingId: Long, oppgavetype: Oppgavetype): Task { + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + FerdigstillOppgaveDTO( + behandlingId = behandlingId, + oppgavetype = oppgavetype, + ), + ), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/GrensesnittavstemMotOppdrag.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/GrensesnittavstemMotOppdrag.kt new file mode 100644 index 000000000..4fdeee5cb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/GrensesnittavstemMotOppdrag.kt @@ -0,0 +1,57 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.økonomi.AvstemmingService +import no.nav.familie.ba.sak.task.dto.GrensesnittavstemmingTaskDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.util.VirkedagerProvider +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +@TaskStepBeskrivelse( + taskStepType = GrensesnittavstemMotOppdrag.TASK_STEP_TYPE, + beskrivelse = "Grensesnittavstemming mot oppdrag", + maxAntallFeil = 3, +) +class GrensesnittavstemMotOppdrag(val avstemmingService: AvstemmingService, val taskRepository: TaskRepositoryWrapper) : + AsyncTaskStep { + + override fun doTask(task: Task) { + val avstemmingTask = objectMapper.readValue(task.payload, GrensesnittavstemmingTaskDTO::class.java) + logger.info("Gjør avstemming mot oppdrag fra og med ${avstemmingTask.fomDato} til og med ${avstemmingTask.tomDato}") + + avstemmingService.grensesnittavstemOppdrag(avstemmingTask.fomDato, avstemmingTask.tomDato) + } + + override fun onCompletion(task: Task) { + val nesteAvstemmingTaskDTO = nesteAvstemmingDTO(task.triggerTid.toLocalDate()) + + val nesteAvstemmingTask = Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString(nesteAvstemmingTaskDTO), + ).medTriggerTid( + nesteAvstemmingTaskDTO.tomDato.toLocalDate().atTime(8, 0), + ) + + taskRepository.save(nesteAvstemmingTask) + } + + fun nesteAvstemmingDTO(tideligereTriggerDato: LocalDate): GrensesnittavstemmingTaskDTO = + GrensesnittavstemmingTaskDTO( + tideligereTriggerDato.atStartOfDay(), + VirkedagerProvider.nesteVirkedag(tideligereTriggerDato).atStartOfDay(), + ) + + companion object { + + const val TASK_STEP_TYPE = "avstemMotOppdrag" + + private val logger: Logger = LoggerFactory.getLogger(GrensesnittavstemMotOppdrag::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/HenleggBehandlingTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/HenleggBehandlingTask.kt new file mode 100644 index 000000000..b09b2bf32 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/HenleggBehandlingTask.kt @@ -0,0 +1,75 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.HenleggÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.RestHenleggBehandlingInfo +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +@TaskStepBeskrivelse( + taskStepType = HenleggBehandlingTask.TASK_STEP_TYPE, + beskrivelse = "Henlegg behandling", + maxAntallFeil = 1, +) +class HenleggBehandlingTask( + val arbeidsfordelingService: ArbeidsfordelingService, + val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + val stegService: StegService, + val oppgaveService: OppgaveService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val henleggBehandlingTaskDTO = objectMapper.readValue(task.payload, HenleggBehandlingTaskDTO::class.java) + val behandling = behandlingHentOgPersisterService.hent(henleggBehandlingTaskDTO.behandlingId).apply { + task.metadata["fagsakId"] = fagsak.id.toString() + } + + if (behandling.status == BehandlingStatus.AVSLUTTET) { + task.metadata["Resultat"] = "Behandlingen er allerede avsluttet" + return + } + + if (henleggBehandlingTaskDTO.validerOppgavefristErEtterDato != null) { + val valideringsdato = henleggBehandlingTaskDTO.validerOppgavefristErEtterDato + val frist = oppgaveService.hentOppgaverSomIkkeErFerdigstilt(Oppgavetype.BehandleSak, behandling).let { + it.singleOrNull()?.run { + oppgaveService.hentOppgave(gsakId.toLong()).fristFerdigstillelse ?: error("Oppgave $gsakId mangler frist") + } ?: error("Behandling ${behandling.id} har ingen, eller mer enn en behandleSak-oppgave: $it") + } + if (!LocalDate.parse(frist).isAfter(henleggBehandlingTaskDTO.validerOppgavefristErEtterDato)) { + task.metadata["Resultat"] = "Stoppet. Behandlingen har frist $frist. Må være etter $valideringsdato" + return + } + } + + stegService.håndterHenleggBehandling( + behandling = behandling, + henleggBehandlingInfo = henleggBehandlingTaskDTO.run { RestHenleggBehandlingInfo(årsak, begrunnelse) }, + ).apply { + task.metadata["behandlendeEnhetId"] = arbeidsfordelingService.hentArbeidsfordelingPåBehandling(id).behandlendeEnhetId + task.metadata["Resultat"] = "Henleggelse kjørt OK" + } + } + + companion object { + + const val TASK_STEP_TYPE = "HenleggBehandling" + } +} + +class HenleggBehandlingTaskDTO( + val behandlingId: Long, + val årsak: HenleggÅrsak, + val begrunnelse: String, + val validerOppgavefristErEtterDato: LocalDate?, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/HentAlleIdenterTilPsysTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/HentAlleIdenterTilPsysTask.kt new file mode 100644 index 000000000..a2daf6566 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/HentAlleIdenterTilPsysTask.kt @@ -0,0 +1,65 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.ekstern.pensjon.HentAlleIdenterTilPsysResponseDTO +import no.nav.familie.ba.sak.ekstern.pensjon.Meldingstype +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.statistikk.producer.KafkaProducer +import no.nav.familie.ba.sak.task.HentAlleIdenterTilPsysTask.Companion.TASK_STEP_TYPE +import no.nav.familie.ba.sak.task.dto.HentAlleIdenterTilPsysRequestDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.Properties +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = TASK_STEP_TYPE, + beskrivelse = "Henter alle identer som har barnetrygd for gjeldende år til psys", + maxAntallFeil = 1, +) +class HentAlleIdenterTilPsysTask( + private val kafkaProducer: KafkaProducer, + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, +) : AsyncTaskStep { + + private val logger = LoggerFactory.getLogger(HentAlleIdenterTilPsysTask::class.java) + + override fun doTask(task: Task) { + val hentAlleIdenterDto = objectMapper.readValue(task.payload, HentAlleIdenterTilPsysRequestDTO::class.java) + logger.info("Starter med å hente alle identer fra DB for request ${hentAlleIdenterDto.requestId}") + val identer = andelTilkjentYtelseRepository.finnIdenterMedLøpendeBarnetrygdForGittÅr(hentAlleIdenterDto.år) + logger.info("Ferdig med å hente alle identer fra DB for request ${hentAlleIdenterDto.requestId}") + logger.info("Starter på å sende alle identer til kafka for request ${hentAlleIdenterDto.requestId}") + + kafkaProducer.sendIdentTilPSys( + HentAlleIdenterTilPsysResponseDTO(meldingstype = Meldingstype.START, requestId = hentAlleIdenterDto.requestId, personident = null), + ) + identer.forEach { kafkaProducer.sendIdentTilPSys(HentAlleIdenterTilPsysResponseDTO(meldingstype = Meldingstype.DATA, personident = it, requestId = hentAlleIdenterDto.requestId)) } + kafkaProducer.sendIdentTilPSys( + HentAlleIdenterTilPsysResponseDTO(meldingstype = Meldingstype.SLUTT, requestId = hentAlleIdenterDto.requestId, personident = null), + ) + logger.info("Ferdig med å sende alle identer til kafka for request ${hentAlleIdenterDto.requestId}") + } + + override fun onCompletion(task: Task) { + } + + companion object { + fun lagTask(år: Int, uuid: UUID): Task { + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString(HentAlleIdenterTilPsysRequestDTO(år = år, requestId = uuid)), + properties = Properties().apply { + this["år"] = år.toString() + this["requestId"] = uuid.toString() + this["callId"] = uuid.toString() + }, + ) + } + const val TASK_STEP_TYPE = "hentAlleIdenterTilPsys" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/InternKonsistensavstemmingTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/InternKonsistensavstemmingTask.kt new file mode 100644 index 000000000..c8bd5315b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/InternKonsistensavstemmingTask.kt @@ -0,0 +1,61 @@ +package no.nav.familie.ba.sak.task + +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.familie.ba.sak.integrasjoner.økonomi.internkonsistensavstemming.InternKonsistensavstemmingService +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.log.mdc.MDCConstants +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.PropertiesWrapper +import no.nav.familie.prosessering.domene.Task +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.util.Properties +import kotlin.system.measureTimeMillis + +@Service +@TaskStepBeskrivelse( + taskStepType = InternKonsistensavstemmingTask.TASK_STEP_TYPE, + beskrivelse = "Kjør intern konsistensavstemming", + maxAntallFeil = 3, + triggerTidVedFeilISekunder = 600, +) +class InternKonsistensavstemmingTask( + val internKonsistensavstemmingService: InternKonsistensavstemmingService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val fagsakIder: Set = objectMapper.readValue(task.payload) + + val tidBrukt = measureTimeMillis { + internKonsistensavstemmingService.validerLikUtbetalingIAndeleneOgUtbetalingsoppdraget(fagsakIder) + } + + logger.info( + "Fullført intern konsistensavstemming på fagsak ${fagsakIder.min()} til ${fagsakIder.max()}. " + + "Tid brukt = $tidBrukt millisekunder", + ) + } + + companion object { + fun opprettTask(fagsakIder: Set, startTid: LocalDateTime): Task { + val metadata = Properties().apply { + this["fagsakerIder"] = "${fagsakIder.min()} til ${fagsakIder.max()}" + this[MDCConstants.MDC_CALL_ID] = MDC.get(MDCConstants.MDC_CALL_ID) ?: "" + } + + return Task( + type = this.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString(fagsakIder), + triggerTid = startTid, + metadataWrapper = PropertiesWrapper(metadata), + ) + } + + const val TASK_STEP_TYPE = "internKonsistensavstemming" + val logger: Logger = LoggerFactory.getLogger(this::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/IverksettMotFamilieTilbakeTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/IverksettMotFamilieTilbakeTask.kt new file mode 100644 index 000000000..7d47bc036 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/IverksettMotFamilieTilbakeTask.kt @@ -0,0 +1,45 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.task.dto.IverksettMotFamilieTilbakeDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service +import java.util.Properties + +@Service +@TaskStepBeskrivelse( + taskStepType = IverksettMotFamilieTilbakeTask.TASK_STEP_TYPE, + beskrivelse = "Iverksett mot Familie tilbake", + maxAntallFeil = 3, +) +class IverksettMotFamilieTilbakeTask( + val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + val stegService: StegService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val iverksettMotFamilieTilbake = objectMapper.readValue(task.payload, IverksettMotFamilieTilbakeDTO::class.java) + stegService.håndterIverksettMotFamilieTilbake( + behandling = behandlingHentOgPersisterService.hent(iverksettMotFamilieTilbake.behandlingsId), + task.metadata, + ) + } + + companion object { + + const val TASK_STEP_TYPE = "iverksettMotFamilieTilbake" + fun opprettTask(behandlingsId: Long, metadata: Properties): Task { + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString(IverksettMotFamilieTilbakeDTO(behandlingsId)), + properties = metadata.apply { + this["behandlingId"] = behandlingsId.toString() + }, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/IverksettMotOppdragTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/IverksettMotOppdragTask.kt new file mode 100644 index 000000000..060e12168 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/IverksettMotOppdragTask.kt @@ -0,0 +1,103 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.task.IverksettMotOppdragTask.Companion.TASK_STEP_TYPE +import no.nav.familie.ba.sak.task.dto.FAGSYSTEM +import no.nav.familie.ba.sak.task.dto.IverksettingTaskDTO +import no.nav.familie.ba.sak.task.dto.StatusFraOppdragDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service +import java.util.Properties + +@Service +@TaskStepBeskrivelse(taskStepType = TASK_STEP_TYPE, beskrivelse = "Iverksett vedtak mot oppdrag", maxAntallFeil = 3) +class IverksettMotOppdragTask( + private val stegService: StegService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val personidentService: PersonidentService, + private val taskRepository: TaskRepositoryWrapper, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val iverksettingTask = objectMapper.readValue(task.payload, IverksettingTaskDTO::class.java) + stegService.håndterIverksettMotØkonomi( + behandling = behandlingHentOgPersisterService.hent(iverksettingTask.behandlingsId), + iverksettingTaskDTO = iverksettingTask, + ) + } + + override fun onCompletion(task: Task) { + val iverksettingTask = objectMapper.readValue(task.payload, IverksettingTaskDTO::class.java) + val personIdent = personidentService.hentAktør(iverksettingTask.personIdent).aktivFødselsnummer() + val statusFraOppdragTask = Task( + type = StatusFraOppdragTask.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + StatusFraOppdragDTO( + aktørId = iverksettingTask.personIdent, + personIdent = personIdent, + fagsystem = FAGSYSTEM, + behandlingsId = iverksettingTask.behandlingsId, + vedtaksId = iverksettingTask.vedtaksId, + ), + ), + properties = task.metadata, + ) + + val sendMeldingTilBisysTask = Task( + type = SendMeldingTilBisysTask.TASK_STEP_TYPE, + payload = iverksettingTask.behandlingsId.toString(), + ) + + taskRepository.save(statusFraOppdragTask) + taskRepository.save(sendMeldingTilBisysTask) + } + + companion object { + const val TASK_STEP_TYPE = "iverksettMotOppdrag" + + fun opprettTask(behandling: Behandling, vedtak: Vedtak, saksbehandlerId: String): Task { + return opprettTask( + behandling.fagsak.aktør, + behandling.id, + vedtak.id, + saksbehandlerId, + behandling.fagsak.id, + ) + } + + fun opprettTask( + aktør: Aktør, + behandlingsId: Long, + vedtaksId: Long, + saksbehandlerId: String, + fagsakId: Long, + ): Task { + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + IverksettingTaskDTO( + personIdent = aktør.aktivFødselsnummer(), + behandlingsId = behandlingsId, + vedtaksId = vedtaksId, + saksbehandlerId = saksbehandlerId, + ), + ), + properties = Properties().apply { + this["personIdent"] = aktør.aktivFødselsnummer() + this["behandlingsId"] = behandlingsId.toString() + this["vedtakId"] = vedtaksId.toString() + this["fagsakId"] = fagsakId.toString() + }, + ) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/Journalf\303\270rVedtaksbrevTask.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/Journalf\303\270rVedtaksbrevTask.kt" new file mode 100644 index 000000000..043aaef44 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/Journalf\303\270rVedtaksbrevTask.kt" @@ -0,0 +1,48 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.domene.JournalførVedtaksbrevDTO +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.task.JournalførVedtaksbrevTask.Companion.TASK_STEP_TYPE +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service +import java.util.Properties + +@Service +@TaskStepBeskrivelse(taskStepType = TASK_STEP_TYPE, beskrivelse = "Journalfør brev i Joark", maxAntallFeil = 3) +class JournalførVedtaksbrevTask( + private val vedtakService: VedtakService, + private val stegService: StegService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val vedtakId = task.payload.toLong() + val behandling = vedtakService.hent(vedtakId).behandling + + stegService.håndterJournalførVedtaksbrev(behandling, JournalførVedtaksbrevDTO(vedtakId = vedtakId, task = task)) + } + + companion object { + + const val TASK_STEP_TYPE = "journalførTilJoark" + + fun opprettTaskJournalførVedtaksbrev( + personIdent: String, + behandlingId: Long, + vedtakId: Long, + gammelTask: Task? = null, + ): Task { + return Task( + TASK_STEP_TYPE, + "$vedtakId", + gammelTask?.metadata ?: Properties().apply { + this["personIdent"] = personIdent + this["behandlingsId"] = behandlingId.toString() + this["vedtakId"] = vedtakId.toString() + }, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragAvsluttTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragAvsluttTask.kt new file mode 100644 index 000000000..9a5d47212 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragAvsluttTask.kt @@ -0,0 +1,64 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.integrasjoner.økonomi.AvstemmingService +import no.nav.familie.ba.sak.integrasjoner.økonomi.BatchService +import no.nav.familie.ba.sak.integrasjoner.økonomi.DataChunkRepository +import no.nav.familie.ba.sak.integrasjoner.økonomi.KjøreStatus +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingAvsluttTaskDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.error.RekjørSenereException +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDateTime + +@Service +@TaskStepBeskrivelse( + taskStepType = KonsistensavstemMotOppdragAvsluttTask.TASK_STEP_TYPE, + beskrivelse = "Avslutt Konsistensavstemming mot oppdrag", + maxAntallFeil = 10, // 2.5 time bør være nok tid for å att alle datataskene har kjørt +) +class KonsistensavstemMotOppdragAvsluttTask( + val avstemmingService: AvstemmingService, + val dataChunkRepository: DataChunkRepository, + val batchService: BatchService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val konsistensavstemmingAvsluttTask = + objectMapper.readValue(task.payload, KonsistensavstemmingAvsluttTaskDTO::class.java) + + val dataChunks = dataChunkRepository.findByTransaksjonsId(konsistensavstemmingAvsluttTask.transaksjonsId) + if (dataChunks.any { !it.erSendt }) { + throw RekjørSenereException( + årsak = "Alle datatasks for konsistensavstemming med id ${konsistensavstemmingAvsluttTask.transaksjonsId} er ikke kjørt.", + triggerTid = LocalDateTime.now().plusMinutes(15), + ) + } + + if (avstemmingService.harBatchStatusFerdig(konsistensavstemmingAvsluttTask.batchId)) { + logger.info("Batch med id ${konsistensavstemmingAvsluttTask.batchId} og transaksjonsId=${konsistensavstemmingAvsluttTask.transaksjonsId} er allerede ferdig kjørt, så skipper sending til økonomi") + return + } + + if (konsistensavstemmingAvsluttTask.sendTilØkonomi) { + avstemmingService.konsistensavstemOppdragAvslutt( + avstemmingsdato = konsistensavstemmingAvsluttTask.avstemmingsdato, + transaksjonsId = konsistensavstemmingAvsluttTask.transaksjonsId, + ) + } else { + logger.info("Send avsluttmelding til økonomi i dry-run modus for transaksjonsId=${konsistensavstemmingAvsluttTask.transaksjonsId}") + } + + batchService.lagreNyStatus(konsistensavstemmingAvsluttTask.batchId, KjøreStatus.FERDIG) + } + + companion object { + const val TASK_STEP_TYPE = "konsistensavstemMotOppdragAvslutt" + private val logger: Logger = + LoggerFactory.getLogger(KonsistensavstemMotOppdragAvsluttTask::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragDataTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragDataTask.kt new file mode 100644 index 000000000..e61d2b219 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragDataTask.kt @@ -0,0 +1,38 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.integrasjoner.økonomi.AvstemmingService +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingDataTaskDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service + +@Service +@TaskStepBeskrivelse( + taskStepType = KonsistensavstemMotOppdragDataTask.TASK_STEP_TYPE, + beskrivelse = "Send batcher av Konsistensavstemming mot oppdrag", + maxAntallFeil = 3, +) +class KonsistensavstemMotOppdragDataTask( + val avstemmingService: AvstemmingService, +) : + AsyncTaskStep { + + override fun doTask(task: Task) { + val konsistensavstemmingDataTask = + objectMapper.readValue(task.payload, KonsistensavstemmingDataTaskDTO::class.java) + + avstemmingService.konsistensavstemOppdragData( + avstemmingsdato = konsistensavstemmingDataTask.avstemmingdato, + perioderTilAvstemming = konsistensavstemmingDataTask.perioderForBehandling, + transaksjonsId = konsistensavstemmingDataTask.transaksjonsId, + chunkNr = konsistensavstemmingDataTask.chunkNr, + sendTilØkonomi = konsistensavstemmingDataTask.sendTilØkonomi, + ) + } + + companion object { + const val TASK_STEP_TYPE = "konsistensavstemMotOppdragData" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.kt new file mode 100644 index 000000000..e9bbf2a27 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.kt @@ -0,0 +1,76 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.integrasjoner.økonomi.AvstemmingService +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingDataTaskDTO +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.Properties + +@Service +@TaskStepBeskrivelse( + taskStepType = KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.TASK_STEP_TYPE, + beskrivelse = "Finn perioder til avstemming for relevante behandlinger", + maxAntallFeil = 3, +) +class KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask( + val avstemmingService: AvstemmingService, + val taskService: TaskService, +) : + AsyncTaskStep { + + override fun doTask(task: Task) { + val taskDto = + objectMapper.readValue( + task.payload, + KonsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO::class.java, + ) + + if (avstemmingService.erKonsistensavstemmingKjørtForTransaksjonsidOgChunk( + taskDto.transaksjonsId, + taskDto.chunkNr, + ) + ) { + logger.info("Finn perioder for avstemming er alt kjørt for ${taskDto.transaksjonsId} og ${taskDto.chunkNr}") + return + } + + val perioderTilAvstemming = + avstemmingService.hentDataForKonsistensavstemming( + taskDto.avstemmingsdato, + taskDto.relevanteBehandlinger, + ) + + logger.info("Finner perioder til avstemming for transaksjonsId ${taskDto.transaksjonsId} og chunk ${taskDto.chunkNr} med ${perioderTilAvstemming.size} løpende saker") + val konsistensavstemmingDataTask = Task( + type = KonsistensavstemMotOppdragDataTask.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + KonsistensavstemmingDataTaskDTO( + transaksjonsId = taskDto.transaksjonsId, + chunkNr = taskDto.chunkNr, + avstemmingdato = taskDto.avstemmingsdato, + perioderForBehandling = perioderTilAvstemming, + sendTilØkonomi = taskDto.sendTilØkonomi, + ), + ), + properties = Properties().apply { + this["chunkNr"] = taskDto.chunkNr.toString() + this["transaksjonsId"] = taskDto.transaksjonsId.toString() + }, + ) + taskService.save(konsistensavstemmingDataTask) + } + + companion object { + private val logger: Logger = + LoggerFactory.getLogger(KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask::class.java) + + const val TASK_STEP_TYPE = "konsistensavstemMotOppdragFinnPerioderForRelevanteBehandlinger" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragStartTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragStartTask.kt new file mode 100644 index 000000000..31ad354dd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragStartTask.kt @@ -0,0 +1,92 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.integrasjoner.økonomi.AvstemmingService +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingAvsluttTaskDTO +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingStartTaskDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDateTime + +@Service +@TaskStepBeskrivelse( + taskStepType = KonsistensavstemMotOppdragStartTask + .TASK_STEP_TYPE, + beskrivelse = "Start Konsistensavstemming mot oppdrag", + maxAntallFeil = 1, + settTilManuellOppfølgning = true, +) +class KonsistensavstemMotOppdragStartTask(val avstemmingService: AvstemmingService) : AsyncTaskStep { + + override fun doTask(task: Task) { + val konsistensavstemmingTask = + objectMapper.readValue(task.payload, KonsistensavstemmingStartTaskDTO::class.java) + + val avstemmingsdato = LocalDateTime.now() + logger.info("Konsistensavstemming ble initielt trigget ${konsistensavstemmingTask.avstemmingdato}, men bruker $avstemmingsdato som avstemmingsdato") + + if (avstemmingService.harBatchStatusFerdig(konsistensavstemmingTask.batchId)) { + logger.info("Konsistensavstemmning er allerede kjørt for transaksjonsId=${konsistensavstemmingTask.transaksjonsId} og batchId=${konsistensavstemmingTask.batchId}") + return + } + + if (!avstemmingService.erKonsistensavstemmingStartet(konsistensavstemmingTask.transaksjonsId)) { + if (konsistensavstemmingTask.sendTilØkonomi) { + avstemmingService.sendKonsistensavstemmingStart( + avstemmingsdato, + konsistensavstemmingTask.transaksjonsId, + ) + } else { + logger.info("Send startmelding til økonomi i dry-run modus for ${konsistensavstemmingTask.transaksjonsId}") + } + } + + val relevanteBehandlinger = + avstemmingService.hentSisteIverksatteBehandlingerFraLøpendeFagsaker().toSet().sorted() + + var chunkNr = 1 + val startTid = LocalDateTime.now() + relevanteBehandlinger.chunked(AvstemmingService.KONSISTENSAVSTEMMING_DATA_CHUNK_STORLEK) + .forEachIndexed { index, oppstykketRelevanteBehandlinger -> + val triggerTidForChunk = startTid.plusSeconds(3 * index.toLong()) + if (avstemmingService.skalOppretteFinnPerioderForRelevanteBehandlingerTask( + konsistensavstemmingTask.transaksjonsId, + chunkNr, + ) + ) { + avstemmingService.opprettKonsistensavstemmingFinnPerioderForRelevanteBehandlingerTask( + KonsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO( + transaksjonsId = konsistensavstemmingTask.transaksjonsId, + chunkNr = chunkNr, + avstemmingsdato = avstemmingsdato, + batchId = konsistensavstemmingTask.batchId, + relevanteBehandlinger = oppstykketRelevanteBehandlinger.map { it }, + sendTilØkonomi = konsistensavstemmingTask.sendTilØkonomi, + ), + triggerTidForChunk, + ) + } else { + logger.info("Finn perioder for avstemming task alt kjørt for ${konsistensavstemmingTask.transaksjonsId} og chunkNr $chunkNr") + } + chunkNr = chunkNr.inc() + } + + avstemmingService.opprettKonsistensavstemmingAvsluttTask( + KonsistensavstemmingAvsluttTaskDTO( + batchId = konsistensavstemmingTask.batchId, + transaksjonsId = konsistensavstemmingTask.transaksjonsId, + avstemmingsdato = avstemmingsdato, + sendTilØkonomi = konsistensavstemmingTask.sendTilØkonomi, + ), + ) + } + + companion object { + const val TASK_STEP_TYPE = "konsistensavstemMotOppdragStart" + private val logger = LoggerFactory.getLogger(KonsistensavstemMotOppdragStartTask::class.java) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OppdaterL\303\270pendeFlagg.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OppdaterL\303\270pendeFlagg.kt" new file mode 100644 index 000000000..25987ca7a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OppdaterL\303\270pendeFlagg.kt" @@ -0,0 +1,31 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +@TaskStepBeskrivelse( + taskStepType = OppdaterLøpendeFlagg.TASK_STEP_TYPE, + beskrivelse = "Oppdater fagsakstatus fra LØPENDE til AVSLUTTET på avsluttede fagsaker", + maxAntallFeil = 3, + triggerTidVedFeilISekunder = 60, +) +class OppdaterLøpendeFlagg(val fagsakService: FagsakService) : AsyncTaskStep { + + override fun doTask(task: Task) { + val antallOppdaterte = fagsakService.oppdaterLøpendeStatusPåFagsaker() + logger.info("Oppdatert status på $antallOppdaterte fagsaker til ${FagsakStatus.AVSLUTTET.name}") + } + + companion object { + + const val TASK_STEP_TYPE = "oppdaterLøpendeFlagg" + private val logger: Logger = LoggerFactory.getLogger(OppdaterLøpendeFlagg::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OpprettInternKonsistensavstemmingTaskerTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OpprettInternKonsistensavstemmingTaskerTask.kt new file mode 100644 index 000000000..560bd5f32 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OpprettInternKonsistensavstemmingTaskerTask.kt @@ -0,0 +1,49 @@ +package no.nav.familie.ba.sak.task.internkonsistensavstemming + +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.familie.ba.sak.integrasjoner.økonomi.internkonsistensavstemming.InternKonsistensavstemmingService +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.log.IdUtils +import no.nav.familie.log.mdc.MDCConstants +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.PropertiesWrapper +import no.nav.familie.prosessering.domene.Task +import org.slf4j.MDC +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.util.Properties + +@Service +@TaskStepBeskrivelse( + taskStepType = OpprettInternKonsistensavstemmingTaskerTask.TASK_STEP_TYPE, + beskrivelse = "Start intern konsistensavstemming tasker", + maxAntallFeil = 3, +) +class OpprettInternKonsistensavstemmingTaskerTask( + val internKonsistensavstemmingService: InternKonsistensavstemmingService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val maksAntallTasker: Int = objectMapper.readValue(task.payload) + internKonsistensavstemmingService + .validerLikUtbetalingIAndeleneOgUtbetalingsoppdragetPåAlleFagsaker(maksAntallTasker) + } + + companion object { + fun opprettTask(maksAntallTasker: Int = Int.MAX_VALUE): Task { + val metadata = Properties().apply { + this[MDCConstants.MDC_CALL_ID] = MDC.get(MDCConstants.MDC_CALL_ID) ?: IdUtils.generateId() + } + + return Task( + type = TASK_STEP_TYPE, + payload = maksAntallTasker.toString(), + triggerTid = LocalDateTime.now(), + metadataWrapper = PropertiesWrapper(metadata), + ) + } + + const val TASK_STEP_TYPE = "startInternKonsistensavstemmingTasker" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OpprettOppgaveTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OpprettOppgaveTask.kt new file mode 100644 index 000000000..c4d87487e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OpprettOppgaveTask.kt @@ -0,0 +1,61 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.task.dto.OpprettOppgaveTaskDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +@TaskStepBeskrivelse( + taskStepType = OpprettOppgaveTask.TASK_STEP_TYPE, + beskrivelse = "Opprett oppgave i GOSYS for behandling", + maxAntallFeil = 3, +) +class OpprettOppgaveTask( + private val oppgaveService: OppgaveService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val opprettOppgaveTaskDTO = objectMapper.readValue(task.payload, OpprettOppgaveTaskDTO::class.java) + task.metadata["oppgaveId"] = oppgaveService.opprettOppgave( + behandlingId = opprettOppgaveTaskDTO.behandlingId, + oppgavetype = opprettOppgaveTaskDTO.oppgavetype, + fristForFerdigstillelse = opprettOppgaveTaskDTO.fristForFerdigstillelse, + tilordnetNavIdent = opprettOppgaveTaskDTO.tilordnetRessurs, + beskrivelse = opprettOppgaveTaskDTO.beskrivelse, + manuellOppgaveType = opprettOppgaveTaskDTO.manuellOppgaveType, + ) + } + + companion object { + + const val TASK_STEP_TYPE = "opprettOppgaveTask" + + fun opprettTask( + behandlingId: Long, + oppgavetype: Oppgavetype, + fristForFerdigstillelse: LocalDate, + tilordnetRessurs: String? = null, + beskrivelse: String? = null, + ): Task { + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + OpprettOppgaveTaskDTO( + behandlingId, + oppgavetype, + fristForFerdigstillelse, + tilordnetRessurs, + beskrivelse, + null, + ), + ), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OpprettTaskService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OpprettTaskService.kt new file mode 100644 index 000000000..c55fd72e4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OpprettTaskService.kt @@ -0,0 +1,175 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene.Satskjøring +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene.SatskjøringRepository +import no.nav.familie.ba.sak.kjerne.behandling.HenleggÅrsak +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.task.dto.Autobrev6og18ÅrDTO +import no.nav.familie.ba.sak.task.dto.AutobrevOpphørSmåbarnstilleggDTO +import no.nav.familie.ba.sak.task.dto.ManuellOppgaveType +import no.nav.familie.ba.sak.task.dto.OpprettOppgaveTaskDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.log.IdUtils +import no.nav.familie.log.mdc.MDCConstants +import no.nav.familie.prosessering.domene.Task +import org.slf4j.MDC +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.YearMonth +import java.util.Properties + +@Service +class OpprettTaskService( + val taskRepository: TaskRepositoryWrapper, + val satskjøringRepository: SatskjøringRepository, +) { + + fun opprettOppgaveTask( + behandlingId: Long, + oppgavetype: Oppgavetype, + beskrivelse: String? = null, + fristForFerdigstillelse: LocalDate = LocalDate.now(), + ) { + taskRepository.save( + OpprettOppgaveTask.opprettTask( + behandlingId = behandlingId, + oppgavetype = oppgavetype, + fristForFerdigstillelse = fristForFerdigstillelse, + beskrivelse = beskrivelse, + ), + ) + } + + fun opprettOppgaveForManuellBehandlingTask( + behandlingId: Long, + beskrivelse: String? = null, + fristForFerdigstillelse: LocalDate = LocalDate.now(), + manuellOppgaveType: ManuellOppgaveType, + ) { + taskRepository.save( + Task( + type = OpprettOppgaveTask.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + OpprettOppgaveTaskDTO( + behandlingId, + Oppgavetype.VurderLivshendelse, + fristForFerdigstillelse, + null, + beskrivelse, + manuellOppgaveType, + ), + ), + ), + ) + } + + fun opprettSendFeedTilInfotrygdTask(barnasIdenter: List) { + taskRepository.save(SendFødselsmeldingTilInfotrygdTask.opprettTask(barnasIdenter)) + } + + fun opprettSendStartBehandlingTilInfotrygdTask(aktørStoenadsmottaker: Aktør) { + taskRepository.save(SendStartBehandlingTilInfotrygdTask.opprettTask(aktørStoenadsmottaker)) + } + + fun opprettAutovedtakFor6Og18ÅrBarn(fagsakId: Long, alder: Int) { + overstyrTaskMedNyCallId(IdUtils.generateId()) { + taskRepository.save( + Task( + type = SendAutobrev6og18ÅrTask.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + Autobrev6og18ÅrDTO( + fagsakId = fagsakId, + alder = alder, + årMåned = inneværendeMåned(), + ), + ), + properties = Properties().apply { + this["fagsak"] = fagsakId.toString() + }, + ), + ) + } + } + + fun opprettAutovedtakForOpphørSmåbarnstilleggTask(fagsakId: Long) { + overstyrTaskMedNyCallId(IdUtils.generateId()) { + taskRepository.save( + Task( + type = SendAutobrevOpphørSmåbarnstilleggTask.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + AutobrevOpphørSmåbarnstilleggDTO( + fagsakId = fagsakId, + ), + ), + properties = Properties().apply { + this["fagsakId"] = fagsakId.toString() + }, + ), + ) + } + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun opprettSatsendringTask(fagsakId: Long, satstidspunkt: YearMonth) { + satskjøringRepository.save(Satskjøring(fagsakId = fagsakId, satsTidspunkt = satstidspunkt)) + overstyrTaskMedNyCallId(IdUtils.generateId()) { + taskRepository.save( + Task( + type = SatsendringTask.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString(SatsendringTaskDto(fagsakId, satstidspunkt)), + properties = Properties().apply { + this["fagsakId"] = fagsakId.toString() + }, + ), + ) + } + } + + @Transactional + fun opprettHenleggBehandlingTask( + behandlingId: Long, + årsak: HenleggÅrsak, + begrunnelse: String, + validerOppgavefristErEtterDato: LocalDate? = null, + ) { + taskRepository.save( + Task( + type = HenleggBehandlingTask.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + HenleggBehandlingTaskDTO( + behandlingId = behandlingId, + årsak = årsak, + begrunnelse = begrunnelse, + validerOppgavefristErEtterDato = validerOppgavefristErEtterDato, + ), + ), + properties = Properties().apply { + this["behandlingId"] = behandlingId.toString() + }, + ), + ) + } + + companion object { + const val RETRY_BACKOFF_5000MS = "\${retry.backoff.delay:5000}" + fun overstyrTaskMedNyCallId(callId: String, body: () -> T): T { + val originalCallId = MDC.get(MDCConstants.MDC_CALL_ID) ?: null + + return try { + MDC.put(MDCConstants.MDC_CALL_ID, callId) + body() + } finally { + if (originalCallId == null) { + MDC.remove(MDCConstants.MDC_CALL_ID) + } else { + MDC.put(MDCConstants.MDC_CALL_ID, originalCallId) + } + } + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OpprettVurderF\303\270dselshendelseKonsekvensForYtelseOppgave.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OpprettVurderF\303\270dselshendelseKonsekvensForYtelseOppgave.kt" new file mode 100644 index 000000000..cddb46385 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/OpprettVurderF\303\270dselshendelseKonsekvensForYtelseOppgave.kt" @@ -0,0 +1,55 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.kjerne.beregning.endringstidspunkt.AktørId +import no.nav.familie.ba.sak.task.dto.OpprettVurderFødselshendelseKonsekvensForYtelseOppgaveTaskDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +@TaskStepBeskrivelse( + taskStepType = OpprettVurderFødselshendelseKonsekvensForYtelseOppgave.TASK_STEP_TYPE, + beskrivelse = "Opprett oppgave i GOSYS for fødselshendelse som ikke lar seg utføre automatisk", + maxAntallFeil = 3, +) +class OpprettVurderFødselshendelseKonsekvensForYtelseOppgave( + private val oppgaveService: OppgaveService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val opprettVurderFødselshendelseKonsekvensForYtelseOppgaveTaskDTO = objectMapper.readValue(task.payload, OpprettVurderFødselshendelseKonsekvensForYtelseOppgaveTaskDTO::class.java) + task.metadata["oppgaveId"] = oppgaveService.opprettOppgaveForFødselshendelse( + ident = opprettVurderFødselshendelseKonsekvensForYtelseOppgaveTaskDTO.ident, + oppgavetype = opprettVurderFødselshendelseKonsekvensForYtelseOppgaveTaskDTO.oppgavetype, + fristForFerdigstillelse = LocalDate.now(), + beskrivelse = opprettVurderFødselshendelseKonsekvensForYtelseOppgaveTaskDTO.beskrivelse, + ) + } + + companion object { + + const val TASK_STEP_TYPE = "opprettVurderFødselshendelseKonsekvensForYtelseOppgave" + + fun opprettTask( + ident: AktørId, + oppgavetype: Oppgavetype, + beskrivelse: String, + ): Task { + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + OpprettVurderFødselshendelseKonsekvensForYtelseOppgaveTaskDTO( + ident, + oppgavetype, + beskrivelse, + ), + ), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/PubliserVedtakV2Task.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/PubliserVedtakV2Task.kt new file mode 100644 index 000000000..f06fbeb33 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/PubliserVedtakV2Task.kt @@ -0,0 +1,44 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.common.EnvService +import no.nav.familie.ba.sak.statistikk.producer.KafkaProducer +import no.nav.familie.ba.sak.statistikk.stønadsstatistikk.StønadsstatistikkService +import no.nav.familie.ba.sak.task.PubliserVedtakV2Task.Companion.TASK_STEP_TYPE +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.Properties + +@Service +@TaskStepBeskrivelse(taskStepType = TASK_STEP_TYPE, beskrivelse = "Publiser vedtak V2 til kafka Aiven", maxAntallFeil = 1) +class PubliserVedtakV2Task( + val kafkaProducer: KafkaProducer, + val stønadsstatistikkService: StønadsstatistikkService, + val env: EnvService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val vedtakV2DVH = stønadsstatistikkService.hentVedtakV2(task.payload.toLong()) + LOG.info("Send VedtakV2 til DVH, behandling id ${vedtakV2DVH.behandlingsId}") + task.metadata["offset"] = kafkaProducer.sendMessageForTopicVedtakV2(vedtakV2DVH).toString() + } + + companion object { + + val LOG = LoggerFactory.getLogger(PubliserVedtakV2Task::class.java) + const val TASK_STEP_TYPE = "publiserVedtakV2Task" + + fun opprettTask(personIdent: String, behandlingsId: Long): Task { + return Task( + type = TASK_STEP_TYPE, + payload = behandlingsId.toString(), + properties = Properties().apply { + this["personIdent"] = personIdent + this["behandlingsId"] = behandlingsId.toString() + }, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SatsendringTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SatsendringTask.kt new file mode 100644 index 000000000..f0d91d1b7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SatsendringTask.kt @@ -0,0 +1,38 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.AutovedtakSatsendringService +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service +import java.time.YearMonth + +@Service +@TaskStepBeskrivelse( + taskStepType = SatsendringTask.TASK_STEP_TYPE, + beskrivelse = "Utfør satsendring", + maxAntallFeil = 1, +) +class SatsendringTask( + val autovedtakSatsendringService: AutovedtakSatsendringService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val dto = objectMapper.readValue(task.payload, SatsendringTaskDto::class.java) + + val resultat = autovedtakSatsendringService.kjørBehandling(dto) + + task.metadata["resultat"] = resultat.melding + } + + companion object { + + const val TASK_STEP_TYPE = "satsendring" + } +} + +data class SatsendringTaskDto( + val fagsakId: Long, + val satstidspunkt: YearMonth, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendAutobrev6og18\303\205rTask.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendAutobrev6og18\303\205rTask.kt" new file mode 100644 index 000000000..92d5a1a80 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendAutobrev6og18\303\205rTask.kt" @@ -0,0 +1,40 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.autovedtak.omregning.Autobrev6og18ÅrService +import no.nav.familie.ba.sak.task.dto.Autobrev6og18ÅrDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +@TaskStepBeskrivelse( + taskStepType = SendAutobrev6og18ÅrTask.TASK_STEP_TYPE, + beskrivelse = "Send autobrev for barn som fyller 6 og 18 år til Dokdist", + maxAntallFeil = 3, + triggerTidVedFeilISekunder = (60 * 60 * 24).toLong(), + settTilManuellOppfølgning = true, +) +class SendAutobrev6og18ÅrTask( + private val autobrev6og18ÅrService: Autobrev6og18ÅrService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val autobrevDTO = objectMapper.readValue(task.payload, Autobrev6og18ÅrDTO::class.java) + + if (!LocalDate.now().toYearMonth().equals(autobrevDTO.årMåned)) { + throw Feil("Task for autobrev må kjøres innenfor måneden det skal sjekkes mot.") + } + + autobrev6og18ÅrService.opprettOmregningsoppgaveForBarnIBrytingsalder(autobrevDTO) + } + + companion object { + + const val TASK_STEP_TYPE = "sendAutobrevVed6og18År" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendAutobrevOpph\303\270rSm\303\245barnstilleggTask.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendAutobrevOpph\303\270rSm\303\245barnstilleggTask.kt" new file mode 100644 index 000000000..758650641 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendAutobrevOpph\303\270rSm\303\245barnstilleggTask.kt" @@ -0,0 +1,32 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.kjerne.autovedtak.omregning.AutobrevOpphørSmåbarnstilleggService +import no.nav.familie.ba.sak.task.dto.AutobrevOpphørSmåbarnstilleggDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service + +@Service +@TaskStepBeskrivelse( + taskStepType = SendAutobrevOpphørSmåbarnstilleggTask.TASK_STEP_TYPE, + beskrivelse = "Send autobrev for opphør av småbarnstillegg", + maxAntallFeil = 3, + triggerTidVedFeilISekunder = (60 * 60 * 24).toLong(), + settTilManuellOppfølgning = true, +) +class SendAutobrevOpphørSmåbarnstilleggTask( + private val autobrevOpphørSmåbarnstilleggService: AutobrevOpphørSmåbarnstilleggService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val autobrevDTO = objectMapper.readValue(task.payload, AutobrevOpphørSmåbarnstilleggDTO::class.java) + + autobrevOpphørSmåbarnstilleggService.kjørBehandlingOgSendBrevForOpphørAvSmåbarnstillegg(fagsakId = autobrevDTO.fagsakId) + } + + companion object { + const val TASK_STEP_TYPE = "sendAutobrevOpphorSmaabarnstillegg" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendF\303\270dselsmeldingTilInfotrygdTask.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendF\303\270dselsmeldingTilInfotrygdTask.kt" new file mode 100644 index 000000000..a2c311baa --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendF\303\270dselsmeldingTilInfotrygdTask.kt" @@ -0,0 +1,59 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdFeedClient +import no.nav.familie.ba.sak.integrasjoner.infotrygd.domene.InfotrygdFødselhendelsesFeedDto +import no.nav.familie.ba.sak.integrasjoner.infotrygd.domene.InfotrygdFødselhendelsesFeedTaskDto +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.log.IdUtils +import no.nav.familie.log.mdc.MDCConstants +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.MDC +import org.springframework.stereotype.Service +import java.util.Properties + +@Service +@TaskStepBeskrivelse( + taskStepType = SendFødselsmeldingTilInfotrygdTask.TASK_STEP_TYPE, + beskrivelse = "Send fødselshendelse til Infotrygd feed.", +) +class SendFødselsmeldingTilInfotrygdTask( + private val infotrygdFeedClient: InfotrygdFeedClient, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val infotrygdFeedTaskDto = objectMapper.readValue(task.payload, InfotrygdFødselhendelsesFeedTaskDto::class.java) + + infotrygdFeedTaskDto.fnrBarn.forEach { + infotrygdFeedClient.sendFødselhendelsesFeedTilInfotrygd(InfotrygdFødselhendelsesFeedDto(fnrBarn = it)) + } + } + + companion object { + + const val TASK_STEP_TYPE = "sendFeedTilInfotrygd" + + fun opprettTask(fnrBarn: List): Task { + secureLogger.info("Oppretter task for å sende fødselsmelding for $fnrBarn til Infotrygd.") + + val metadata = Properties().apply { + this["personIdenterBarn"] = fnrBarn.toString() + if (!MDC.get(MDCConstants.MDC_CALL_ID).isNullOrEmpty()) { + this["callId"] = MDC.get(MDCConstants.MDC_CALL_ID) ?: IdUtils.generateId() + } + } + + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + InfotrygdFødselhendelsesFeedTaskDto( + fnrBarn = fnrBarn, + ), + ), + properties = metadata, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendMeldingTilBisysTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendMeldingTilBisysTask.kt new file mode 100644 index 000000000..0498bff94 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendMeldingTilBisysTask.kt @@ -0,0 +1,162 @@ +package no.nav.familie.ba.sak.task + +import io.micrometer.core.instrument.Metrics +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.isSameOrAfter +import no.nav.familie.ba.sak.common.isSameOrBefore +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.statistikk.producer.KafkaProducer +import no.nav.familie.eksterne.kontrakter.bisys.BarnEndretOpplysning +import no.nav.familie.eksterne.kontrakter.bisys.BarnetrygdBisysMelding +import no.nav.familie.eksterne.kontrakter.bisys.BarnetrygdEndretType +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.YearMonth +import java.util.Properties + +@Service +@TaskStepBeskrivelse( + taskStepType = SendMeldingTilBisysTask.TASK_STEP_TYPE, + beskrivelse = "Send melding til Bisys om opphør eller reduksjon", + maxAntallFeil = 3, +) +class SendMeldingTilBisysTask( + private val kafkaProducer: KafkaProducer, + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, +) : AsyncTaskStep { + + private val logger = LoggerFactory.getLogger(SendMeldingTilBisysTask::class.java) + private val meldingsTeller = Metrics.counter("familie.ba.sak.bisys.meldinger.sendt") + + override fun doTask(task: Task) { + val behandling = behandlingHentOgPersisterService.hent(behandlingId = task.payload.toLong()) + + // Bisys vil kun ha rene manuelle opphør eller reduksjon + if (behandling.resultat == Behandlingsresultat.OPPHØRT || + behandling.resultat == Behandlingsresultat.ENDRET_UTBETALING || + behandling.resultat == Behandlingsresultat.ENDRET_OG_OPPHØRT + ) { + val barnEndretOpplysning = finnBarnEndretOpplysning(behandling) + val barnetrygdBisysMelding = BarnetrygdBisysMelding( + søker = behandling.fagsak.aktør.aktivFødselsnummer(), + barn = barnEndretOpplysning.filter { it.value.isNotEmpty() }.map { it.value.first() }, + ) + + if (barnetrygdBisysMelding.barn.isEmpty()) { + logger.info("Behandling endret men ikke reduksjon eller opphør. Send ikke melding til bisys") + return + } + + logger.info("Sender melding til bisys om opphør eller reduksjon av barnetrygd.") + + kafkaProducer.sendBarnetrygdBisysMelding( + behandling.id.toString(), + barnetrygdBisysMelding, + ) + meldingsTeller.increment() + } else { + logger.info("Sender ikke melding til bisys siden resultat ikke er opphør eller reduksjon.") + } + } + + fun finnBarnEndretOpplysning(behandling: Behandling): Map> { + val forrigeIverksatteBehandling = behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling = behandling) ?: error("Finnes ikke forrige behandling for behandling ${behandling.id}") + + val tilkjentYtelse = tilkjentYtelseRepository.findByBehandling(behandling.id) + val forrigeTilkjentYtelse = tilkjentYtelseRepository.findByBehandling(forrigeIverksatteBehandling.id) + + val endretOpplysning: MutableMap> = mutableMapOf() + + forrigeTilkjentYtelse.andelerTilkjentYtelse.groupBy { it.aktør.aktørId }.entries.forEach { entry -> + val nyAndelerTilkjentYtelse = + tilkjentYtelse.andelerTilkjentYtelse.filter { it.aktør.aktørId == entry.key } + .sortedBy { it.stønadFom } + entry.value.sortedBy { it.periode.fom }.forEach { + var forblePeriode: MånedPeriode? = it.periode + val prosent = it.prosent + val barnIdent = it.aktør.aktivFødselsnummer() + if (!endretOpplysning.contains(barnIdent)) { + endretOpplysning[barnIdent] = mutableListOf() + } + run checkEndretPerioder@{ + nyAndelerTilkjentYtelse.forEach { + val intersectPerioder = forblePeriode!!.intersect(it.periode) + if (intersectPerioder.first != null) { + endretOpplysning[it.aktør.aktivFødselsnummer()]!!.add( + BarnEndretOpplysning( + ident = barnIdent, + fom = forblePeriode!!.fom, + årsakskode = BarnetrygdEndretType.RO, + ), + ) + } + if (intersectPerioder.second != null && it.prosent < prosent) { + endretOpplysning[it.aktør.aktivFødselsnummer()]!!.add( + BarnEndretOpplysning( + ident = barnIdent, + fom = latest(it.periode.fom, forblePeriode!!.fom), + årsakskode = BarnetrygdEndretType.RR, + ), + ) + } + forblePeriode = intersectPerioder.third + if (forblePeriode == null) { + return@checkEndretPerioder + } + } + } + if (forblePeriode != null && !forblePeriode!!.erTom()) { + endretOpplysning[it.aktør.aktivFødselsnummer()]!!.add( + BarnEndretOpplysning( + ident = barnIdent, + fom = forblePeriode!!.fom, + årsakskode = BarnetrygdEndretType.RO, + ), + ) + } + } + } + return endretOpplysning + } + + companion object { + const val TASK_STEP_TYPE = "sendMeldingOmOpphørTilBisys" + + fun opprettTask(behandlingsId: Long): Task { + return Task( + type = TASK_STEP_TYPE, + payload = behandlingsId.toString(), + properties = Properties().apply { + this["behandlingsId"] = behandlingsId.toString() + }, + ) + } + } +} + +fun earlist(yearMonth1: YearMonth, yearMonth2: YearMonth): YearMonth { + return if (yearMonth1.isSameOrBefore(yearMonth2)) yearMonth1 else yearMonth2 +} + +fun latest(yearMonth1: YearMonth, yearMonth2: YearMonth): YearMonth { + return if (yearMonth1.isSameOrAfter(yearMonth2)) yearMonth1 else yearMonth2 +} + +fun MånedPeriode.intersect(periode: MånedPeriode): Triple { + val overlappetFom = latest(this.fom, periode.fom) + val overlappetTom = earlist(this.tom, periode.tom) + return Triple( + if (this.fom.isSameOrAfter(periode.fom)) null else MånedPeriode(this.fom, periode.fom.minusMonths(1)), + if (overlappetTom.isBefore(overlappetFom)) null else MånedPeriode(overlappetFom, overlappetTom), + if (this.tom.isSameOrBefore(periode.tom)) null else MånedPeriode(periode.tom.plusMonths(1), this.tom), + ) +} + +fun MånedPeriode.erTom() = fom.isAfter(tom) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendStartBehandlingTilInfotrygdTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendStartBehandlingTilInfotrygdTask.kt new file mode 100644 index 000000000..0aae087be --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendStartBehandlingTilInfotrygdTask.kt @@ -0,0 +1,56 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdFeedClient +import no.nav.familie.ba.sak.integrasjoner.infotrygd.domene.StartBehandlingDto +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.log.IdUtils +import no.nav.familie.log.mdc.MDCConstants +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.MDC +import org.springframework.stereotype.Service +import java.util.Properties + +@Service +@TaskStepBeskrivelse( + taskStepType = SendStartBehandlingTilInfotrygdTask.TASK_STEP_TYPE, + beskrivelse = "Send startbehandling til Infotrygd feed.", +) +class SendStartBehandlingTilInfotrygdTask( + private val infotrygdFeedClient: InfotrygdFeedClient, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val startBehandlingDto = objectMapper.readValue(task.payload, StartBehandlingDto::class.java) + infotrygdFeedClient.sendStartBehandlingTilInfotrygd(startBehandlingDto) + } + + companion object { + + const val TASK_STEP_TYPE = "SendStartBehandlingTilInfotrygd" + + fun opprettTask(aktørStoenadsmottaker: Aktør): Task { + secureLogger.info("Oppretter task for å sende StartBehandling for ${aktørStoenadsmottaker.aktivFødselsnummer()} til Infotrygd.") + + val metadata = Properties().apply { + this["personIdenter"] = aktørStoenadsmottaker.aktivFødselsnummer() + if (!MDC.get(MDCConstants.MDC_CALL_ID).isNullOrEmpty()) { + this["callId"] = MDC.get(MDCConstants.MDC_CALL_ID) ?: IdUtils.generateId() + } + } + + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + StartBehandlingDto( + fnrStoenadsmottaker = aktørStoenadsmottaker.aktivFødselsnummer(), + ), + ), + properties = metadata, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendVedtakTilInfotrygdTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendVedtakTilInfotrygdTask.kt new file mode 100644 index 000000000..42e1c0e53 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/SendVedtakTilInfotrygdTask.kt @@ -0,0 +1,82 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdFeedClient +import no.nav.familie.ba.sak.integrasjoner.infotrygd.domene.InfotrygdVedtakFeedDto +import no.nav.familie.ba.sak.integrasjoner.infotrygd.domene.InfotrygdVedtakFeedTaskDto +import no.nav.familie.ba.sak.kjerne.beregning.beregnUtbetalingsperioderUtenKlassifisering +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.log.IdUtils +import no.nav.familie.log.mdc.MDCConstants +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.fpsak.tidsserie.LocalDateSegment +import org.slf4j.MDC +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.util.Properties + +@Service +@TaskStepBeskrivelse( + taskStepType = SendVedtakTilInfotrygdTask.TASK_STEP_TYPE, + beskrivelse = "Send vedtaksmelding til Infotrygd feed.", +) +class SendVedtakTilInfotrygdTask( + private val infotrygdFeedClient: InfotrygdFeedClient, + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val infotrygdVedtakFeedTaskDto = objectMapper.readValue(task.payload, InfotrygdVedtakFeedTaskDto::class.java) + + infotrygdFeedClient.sendVedtakFeedTilInfotrygd( + InfotrygdVedtakFeedDto( + infotrygdVedtakFeedTaskDto.fnrStoenadsmottaker, + finnFørsteUtbetalingsperiode(infotrygdVedtakFeedTaskDto.behandlingId), + ), + ) + } + + private fun finnFørsteUtbetalingsperiode(behandlingId: Long): LocalDate { + val andelerMedEndringer = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnAndelerTilkjentYtelseMedEndreteUtbetalinger(behandlingId) + + return if (andelerMedEndringer.isNotEmpty()) { + val førsteUtbetalingsperiode = beregnUtbetalingsperioderUtenKlassifisering(andelerMedEndringer) + .sortedWith(compareBy>({ it.fom }, { it.value }, { it.tom })) + .first() + førsteUtbetalingsperiode.fom + } else { + error("Finner ikke første utbetalingsperiode") + } + } + + companion object { + + const val TASK_STEP_TYPE = "sendVedtakFeedTilInfotrygd" + + fun opprettTask(fnrStoenadsmottaker: String, behandlingId: Long): Task { + secureLogger.info("Oppretter task for å sende vedtaksmelding for $fnrStoenadsmottaker til Infotrygd.") + + val metadata = Properties().apply { + this["fnrStoenadsmottaker"] = fnrStoenadsmottaker + if (!MDC.get(MDCConstants.MDC_CALL_ID).isNullOrEmpty()) { + this["callId"] = MDC.get(MDCConstants.MDC_CALL_ID) ?: IdUtils.generateId() + } + } + + return Task( + type = TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + InfotrygdVedtakFeedTaskDto( + fnrStoenadsmottaker = fnrStoenadsmottaker, + behandlingId = behandlingId, + ), + ), + properties = metadata, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/StatusFraOppdragTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/StatusFraOppdragTask.kt new file mode 100644 index 000000000..23231eab9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/StatusFraOppdragTask.kt @@ -0,0 +1,62 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StatusFraOppdragMedTask +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.task.StatusFraOppdragTask.Companion.TASK_STEP_TYPE +import no.nav.familie.ba.sak.task.dto.StatusFraOppdragDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.springframework.stereotype.Service + +/** + * Task som kjører 100 ganger før den blir satt til feilet. + * 100 ganger tilsvarer ca 1 døgn med rekjøringsintervall 15 minutter. + * + * + * Infotrygd er vanligvis stengt mellom 21 og 6, men ikke alltid. + * Hvis tasken/steget feiler i denne tida så lager den en ny task og kjører den kl 06 + */ +@Service +@TaskStepBeskrivelse( + taskStepType = TASK_STEP_TYPE, + beskrivelse = "Henter status fra oppdrag", + maxAntallFeil = 100, +) +class StatusFraOppdragTask( + private val stegService: StegService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val personidentService: PersonidentService, + private val taskRepository: TaskRepositoryWrapper, +) : AsyncTaskStep { + + /** + * Metoden prøver å hente kvittering i ét døgn. + * Får tasken kvittering som ikke er OK feiler vi tasken. + */ + override fun doTask(task: Task) { + val statusFraOppdragDTO = objectMapper.readValue(task.payload, StatusFraOppdragDTO::class.java) + + stegService.håndterStatusFraØkonomi( + behandling = behandlingHentOgPersisterService.hent(behandlingId = statusFraOppdragDTO.behandlingsId), + statusFraOppdragMedTask = StatusFraOppdragMedTask(statusFraOppdragDTO = statusFraOppdragDTO, task = task), + ) + } + + override fun onCompletion(task: Task) { + val statusFraOppdragDTO = objectMapper.readValue(task.payload, StatusFraOppdragDTO::class.java) + val personIdent = personidentService.hentAktør(statusFraOppdragDTO.aktørId).aktivFødselsnummer() + + val nyTaskV2 = PubliserVedtakV2Task.opprettTask(personIdent, statusFraOppdragDTO.behandlingsId) + taskRepository.save(nyTaskV2) + } + + companion object { + + const val TASK_STEP_TYPE = "statusFraOppdrag" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/TaBehandlingerEtterVentefristAvVentTask.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/TaBehandlingerEtterVentefristAvVentTask.kt new file mode 100644 index 000000000..3f89dbc49 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/TaBehandlingerEtterVentefristAvVentTask.kt @@ -0,0 +1,35 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentService +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +@TaskStepBeskrivelse( + taskStepType = TaBehandlingerEtterVentefristAvVentTask.TASK_STEP_TYPE, + beskrivelse = "Gjennopptar behandlinger der ventefristen har gått", + maxAntallFeil = 3, + triggerTidVedFeilISekunder = 60, +) +class TaBehandlingerEtterVentefristAvVentTask(val settPåVentService: SettPåVentService) : AsyncTaskStep { + + override fun doTask(task: Task) { + val sakerPåVent = settPåVentService.finnAktiveSettPåVent() + + sakerPåVent.forEach { + if (it.frist.isBefore(LocalDate.now())) { + logger.info("Ventefrist på behandling ${it.behandling.id} er gått ut. Tar behandlingen av vent.") + settPåVentService.gjenopptaBehandling(behandlingId = it.behandling.id) + } + } + } + + companion object { + private val logger = LoggerFactory.getLogger(TaBehandlingerEtterVentefristAvVentTask::class.java) + const val TASK_STEP_TYPE = "taBehanldingerEtterVentefristenAvVent" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/TaskUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/TaskUtils.kt new file mode 100644 index 000000000..304bbe379 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/TaskUtils.kt @@ -0,0 +1,71 @@ +package no.nav.familie.ba.sak.task + +import java.time.DayOfWeek +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.Month +import java.time.temporal.TemporalAdjusters + +/** + * Finner neste gyldige kjøringstidspunkt for tasker som kun skal kjøre på "dagtid". + * + * Dagtid er nå definert som hverdager mellom 06-21. Faste helligdager er tatt høyde for, men flytende + * er ikke kodet inn. + */ +fun nesteGyldigeTriggertidForBehandlingIHverdager( + minutesToAdd: Long = 0, + triggerTid: LocalDateTime = LocalDateTime.now(), +): LocalDateTime { + var date = triggerTid.plusMinutes(minutesToAdd) + + date = if (erKlokkenMellom21Og06(date.toLocalTime()) && date.erHverdag(1)) { + kl06IdagEllerNesteDag(date) + } else if (erKlokkenMellom21Og06(date.toLocalTime()) || !date.erHverdag(0)) { + date.with(TemporalAdjusters.next(DayOfWeek.MONDAY)).withHour(6) + } else { + date + } + + when { + date.dayOfMonth == 1 && date.month == Month.JANUARY -> date = date.plusDays(1) + date.dayOfMonth == 1 && date.month == Month.MAY -> date = date.plusDays(1) + date.dayOfMonth == 17 && date.month == Month.MAY -> date = date.plusDays(1) + date.dayOfMonth == 25 && date.month == Month.DECEMBER -> date = date.plusDays(2) + date.dayOfMonth == 26 && date.month == Month.DECEMBER -> date = date.plusDays(1) + } + + when (date.dayOfWeek) { + DayOfWeek.SATURDAY -> date = date.plusDays(2) + DayOfWeek.SUNDAY -> date = date.plusDays(1) + else -> { + // NOP + } + } + + return date +} + +fun LocalDateTime.erHverdag(plusDays: Long): Boolean { + return when (this.plusDays(plusDays).dayOfWeek) { + DayOfWeek.MONDAY -> true + DayOfWeek.TUESDAY -> true + DayOfWeek.WEDNESDAY -> true + DayOfWeek.THURSDAY -> true + DayOfWeek.FRIDAY -> true + DayOfWeek.SATURDAY -> false + DayOfWeek.SUNDAY -> false + else -> error("Not implemented") + } +} + +fun erKlokkenMellom21Og06(localTime: LocalTime = LocalTime.now()): Boolean { + return localTime.isAfter(LocalTime.of(21, 0)) || localTime.isBefore(LocalTime.of(6, 0)) +} + +fun kl06IdagEllerNesteDag(date: LocalDateTime = LocalDateTime.now()): LocalDateTime { + return if (date.toLocalTime().isBefore(LocalTime.of(6, 0))) { + date.withHour(6) + } else { + date.plusDays(1).withHour(6) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/VedtakOmOvergangsst\303\270nadTask.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/VedtakOmOvergangsst\303\270nadTask.kt" new file mode 100644 index 000000000..b28b820e8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/VedtakOmOvergangsst\303\270nadTask.kt" @@ -0,0 +1,53 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakStegService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.Properties + +@Service +@TaskStepBeskrivelse( + taskStepType = VedtakOmOvergangsstønadTask.TASK_STEP_TYPE, + beskrivelse = "Håndterer vedtak om overgangsstønad", + maxAntallFeil = 3, +) +class VedtakOmOvergangsstønadTask( + private val autovedtakStegService: AutovedtakStegService, + private val personidentService: PersonidentService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val personIdent = task.payload + logger.info("Håndterer vedtak om overgangsstønad. Se secureLog for detaljer") + secureLogger.info("Håndterer vedtak om overgangsstønad for person $personIdent.") + + val aktør = personidentService.hentAktør(personIdent) + + val responseFraService = autovedtakStegService.kjørBehandlingSmåbarnstillegg( + mottakersAktør = aktør, + aktør = aktør, + ) + secureLogger.info("Håndterte vedtak om overgangsstønad for person $personIdent:\n$responseFraService") + } + + companion object { + + const val TASK_STEP_TYPE = "vedtakOmOvergangsstønadTask" + private val logger = LoggerFactory.getLogger(VedtakOmOvergangsstønadTask::class.java) + + fun opprettTask(personIdent: String): Task { + return Task( + type = TASK_STEP_TYPE, + payload = personIdent, + properties = Properties().apply { + this["personIdent"] = personIdent + }, + ) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/Autobrev6og18\303\205rDTO.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/Autobrev6og18\303\205rDTO.kt" new file mode 100644 index 000000000..6caad92e2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/Autobrev6og18\303\205rDTO.kt" @@ -0,0 +1,9 @@ +package no.nav.familie.ba.sak.task.dto + +import java.time.YearMonth + +class Autobrev6og18ÅrDTO( + val fagsakId: Long, + val alder: Int, + val årMåned: YearMonth, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/AutobrevOpph\303\270rSm\303\245barnstilleggDTO.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/AutobrevOpph\303\270rSm\303\245barnstilleggDTO.kt" new file mode 100644 index 000000000..1ba62f2e4 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/AutobrevOpph\303\270rSm\303\245barnstilleggDTO.kt" @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.task.dto + +class AutobrevOpphørSmåbarnstilleggDTO( + val fagsakId: Long, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/BehandleF\303\270dselshendelseTaskDTO.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/BehandleF\303\270dselshendelseTaskDTO.kt" new file mode 100644 index 000000000..661100e86 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/BehandleF\303\270dselshendelseTaskDTO.kt" @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.task.dto + +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse + +data class BehandleFødselshendelseTaskDTO(val nyBehandling: NyBehandlingHendelse) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/DefaultTaskDTO.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/DefaultTaskDTO.kt new file mode 100644 index 000000000..b678e9591 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/DefaultTaskDTO.kt @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.task.dto + +open class DefaultTaskDTO( + val personIdent: String, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/FerdigstillBehandlingDTO.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/FerdigstillBehandlingDTO.kt new file mode 100644 index 000000000..b0fb62050 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/FerdigstillBehandlingDTO.kt @@ -0,0 +1,6 @@ +package no.nav.familie.ba.sak.task.dto + +class FerdigstillBehandlingDTO( + val behandlingsId: Long, + personIdent: String, +) : DefaultTaskDTO(personIdent) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/FerdigstillOppgaveDTO.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/FerdigstillOppgaveDTO.kt new file mode 100644 index 000000000..53408df6e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/FerdigstillOppgaveDTO.kt @@ -0,0 +1,8 @@ +package no.nav.familie.ba.sak.task.dto + +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype + +data class FerdigstillOppgaveDTO( + val behandlingId: Long, + val oppgavetype: Oppgavetype, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/GrensesnittavstemmingTaskDTO.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/GrensesnittavstemmingTaskDTO.kt new file mode 100644 index 000000000..b5031b6e9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/GrensesnittavstemmingTaskDTO.kt @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.task.dto + +import java.time.LocalDateTime + +data class GrensesnittavstemmingTaskDTO(val fomDato: LocalDateTime, val tomDato: LocalDateTime) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/HentAlleIdenterTilPsysRequestDTO.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/HentAlleIdenterTilPsysRequestDTO.kt new file mode 100644 index 000000000..69526a47b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/HentAlleIdenterTilPsysRequestDTO.kt @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.task.dto + +import java.util.UUID + +data class HentAlleIdenterTilPsysRequestDTO(val requestId: UUID, val år: Int) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/IverksettMotFamilieTilbakeDTO.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/IverksettMotFamilieTilbakeDTO.kt new file mode 100644 index 000000000..5084ba33e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/IverksettMotFamilieTilbakeDTO.kt @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.task.dto + +class IverksettMotFamilieTilbakeDTO( + val behandlingsId: Long, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/IverksettingTaskDTO.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/IverksettingTaskDTO.kt new file mode 100644 index 000000000..d3e876309 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/IverksettingTaskDTO.kt @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.task.dto + +class IverksettingTaskDTO( + val behandlingsId: Long, + val vedtaksId: Long, + val saksbehandlerId: String, + personIdent: String, +) : DefaultTaskDTO(personIdent) + +const val FAGSYSTEM = "BA" diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/KonsistensavstemmingTaskDTO.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/KonsistensavstemmingTaskDTO.kt new file mode 100644 index 000000000..8e8c14a83 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/KonsistensavstemmingTaskDTO.kt @@ -0,0 +1,36 @@ +package no.nav.familie.ba.sak.task.dto + +import no.nav.familie.kontrakter.felles.oppdrag.PerioderForBehandling +import java.time.LocalDateTime +import java.util.UUID + +data class KonsistensavstemmingStartTaskDTO( + val batchId: Long, + val avstemmingdato: LocalDateTime, + val transaksjonsId: UUID = UUID.randomUUID(), + val sendTilØkonomi: Boolean = true, +) + +data class KonsistensavstemmingDataTaskDTO( + val transaksjonsId: UUID, + val chunkNr: Int, + val avstemmingdato: LocalDateTime, + val perioderForBehandling: List, + val sendTilØkonomi: Boolean = true, +) + +data class KonsistensavstemmingAvsluttTaskDTO( + val batchId: Long, + val transaksjonsId: UUID, + val avstemmingsdato: LocalDateTime, + val sendTilØkonomi: Boolean = true, +) + +data class KonsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO( + val batchId: Long, + val transaksjonsId: UUID, + val avstemmingsdato: LocalDateTime, + val chunkNr: Int, + val relevanteBehandlinger: List, + val sendTilØkonomi: Boolean = true, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/OpprettOppgaveTaskDTO.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/OpprettOppgaveTaskDTO.kt new file mode 100644 index 000000000..d9cfc7a37 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/OpprettOppgaveTaskDTO.kt @@ -0,0 +1,19 @@ +package no.nav.familie.ba.sak.task.dto + +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import java.time.LocalDate + +data class OpprettOppgaveTaskDTO( + val behandlingId: Long, + val oppgavetype: Oppgavetype, + val fristForFerdigstillelse: LocalDate, + val tilordnetRessurs: String? = null, + val beskrivelse: String?, + val manuellOppgaveType: ManuellOppgaveType? = null, +) + +enum class ManuellOppgaveType(val settBehandlesAvApplikasjon: Boolean) { + SMÅBARNSTILLEGG(true), + FØDSELSHENDELSE(false), + ÅPEN_BEHANDLING(true), +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/OpprettVurderF\303\270dselshendelseKonsekvensForYtelseOppgaveTaskDTO.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/OpprettVurderF\303\270dselshendelseKonsekvensForYtelseOppgaveTaskDTO.kt" new file mode 100644 index 000000000..e4dbab7d7 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/OpprettVurderF\303\270dselshendelseKonsekvensForYtelseOppgaveTaskDTO.kt" @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.task.dto + +import no.nav.familie.ba.sak.kjerne.beregning.endringstidspunkt.AktørId +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype + +data class OpprettVurderFødselshendelseKonsekvensForYtelseOppgaveTaskDTO( + val ident: AktørId, + val oppgavetype: Oppgavetype, + val beskrivelse: String, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/StatusFraOppdragDTO.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/StatusFraOppdragDTO.kt new file mode 100644 index 000000000..e6ed0e8c0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/kotlin/no/nav/familie/ba/sak/task/dto/StatusFraOppdragDTO.kt @@ -0,0 +1,16 @@ +package no.nav.familie.ba.sak.task.dto + +import no.nav.familie.kontrakter.felles.oppdrag.OppdragId + +data class StatusFraOppdragDTO( + val fagsystem: String, + // OppdragId trenger personIdent + val personIdent: String, + val aktørId: String, + val behandlingsId: Long, + val vedtaksId: Long, +) { + + val oppdragId + get() = OppdragId(fagsystem, personIdent, behandlingsId.toString()) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/application-dev.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/application-dev.yaml new file mode 100644 index 000000000..e69821f0f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/application-dev.yaml @@ -0,0 +1,158 @@ +no.nav.security.jwt: + issuer.azuread: + discoveryurl: https://login.microsoftonline.com/navq.onmicrosoft.com/v2.0/.well-known/openid-configuration + accepted_audience: ${BA_SAK_CLIENT_ID} + cookie_name: azure_token + client: + registration: + familie-integrasjoner-onbehalfof: + resource-url: ${FAMILIE_INTEGRASJONER_API_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl-onbehalfof: + resource-url: ${PDL_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${PDL_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl-clientcredentials: + resource-url: ${PDL_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: client_credentials + scope: ${PDL_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-integrasjoner-clientcredentials: + resource-url: ${FAMILIE_INTEGRASJONER_API_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: client_credentials + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag-clientcredentials: + resource-url: ${FAMILIE_OPPDRAG_API_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: client_credentials + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-onbehalfof: + resource-url: ${FAMILIE_BA_INFOTRYGD_API_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_BA_INFOTRYGD_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-feed-clientcredentials: + resource-url: ${FAMILIE_BA_INFOTRYGD_FEED_API_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: client_credentials + scope: ${FAMILIE_BA_INFOTRYGD_FEED_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-clientcredentials: + resource-url: ${FAMILIE_BA_INFOTRYGD_API_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: client_credentials + scope: ${FAMILIE_BA_INFOTRYGD_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag-onbehalfof: + resource-url: ${FAMILIE_OPPDRAG_API_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-tilbake-onbehalfof: + resource-url: ${FAMILIE_TILBAKE_API_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_TILBAKE_API_URL_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-tilbake-clientcredentials: + resource-url: ${FAMILIE_TILBAKE_API_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: client_credentials + scope: ${FAMILIE_TILBAKE_API_URL_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + + +logging: + config: "classpath:logback-test.xml" + level: + root: INFO +sentry.environment: local +sentry.logging.enabled: false + +funksjonsbrytere: + enabled: false + unleash: + uri: http://dummy/api/ + cluster: localhost + applicationName: familie-ba-sak + kafka: + producer: + enabled: false + +spring: + jpa: + show-sql: false + properties: + hibernate: + format_sql=false + hibernate: + ddl-auto: create + flyway: + enabled: false + kafka: + bootstrap-servers: http://localhost:9092 + properties: + security.protocol: SASL_PLAINTEXT + sasl: + jaas.config: org.apache.kafka.common.security.plain.PlainLoginModule required username="igroup" password="itest"; + +prosessering.rolle: "928636f4-fd0d-4149-978e-a6fb68bb19de" # 0000-GA-STDAPPS + +FAMILIE_INTEGRASJONER_SCOPE: "dummy" + +FAMILIE_INTEGRASJONER_API_URL: http://localhost:8085/api +FAMILIE_BA_INFOTRYGD_FEED_API_URL: http://localhost:8092/api +FAMILIE_BA_INFOTRYGD_API_URL: http://localhost:8093 +FAMILIE_TILBAKE_API_URL: http://localhost:8030/api +FAMILIE_OPPDRAG_API_URL: http://localhost:8087/api +FAMILIE_STATISTIKK_URL: dummy +PDL_URL: "dummy" +CREDENTIAL_USERNAME: not-a-real-srvuser +CREDENTIAL_PASSWORD: not-a-real-pw + +retry.backoff.delay: 5 +DEPLOY_ENV: dev diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/application-preprod.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/application-preprod.yaml new file mode 100644 index 000000000..8e9e853fe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/application-preprod.yaml @@ -0,0 +1,173 @@ +no.nav.security.jwt: + issuer.azuread: + discoveryurl: ${AZURE_APP_WELL_KNOWN_URL} + accepted_audience: ${AZURE_APP_CLIENT_ID} + cookie_name: azure_token + client: + registration: + familie-integrasjoner-onbehalfof: + resource-url: ${FAMILIE_INTEGRASJONER_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl-onbehalfof: + resource-url: ${PDL_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${PDL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl-clientcredentials: + resource-url: ${PDL_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${PDL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-integrasjoner-clientcredentials: + resource-url: ${FAMILIE_INTEGRASJONER_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag-clientcredentials: + resource-url: ${FAMILIE_OPPDRAG_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-feed-clientcredentials: + resource-url: ${FAMILIE_BA_INFOTRYGD_FEED_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_BA_INFOTRYGD_FEED_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-onbehalfof: + resource-url: ${FAMILIE_BA_INFOTRYGD_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_BA_INFOTRYGD_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-clientcredentials: + resource-url: ${FAMILIE_BA_INFOTRYGD_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_BA_INFOTRYGD_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag-onbehalfof: + resource-url: ${FAMILIE_OPPDRAG_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-tilbake-onbehalfof: + resource-url: ${FAMILIE_TILBAKE_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_TILBAKE_API_URL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-tilbake-clientcredentials: + resource-url: ${FAMILIE_TILBAKE_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_TILBAKE_API_URL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ef-sak-onbehalfof: + resource-url: ${FAMILIE_EF_SAK_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_EF_SAK_API_URL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ef-sak-clientcredentials: + resource-url: ${FAMILIE_EF_SAK_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_EF_SAK_API_URL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-statistikk-clientcredentials: + resource-url: ${FAMILIE_STATISTIKK_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: api://dev-fss.teamfamilie.familie-ba-statistikk/.default + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + +spring: + datasource: + url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/familie-ba-sak + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + +prosessering.rolle: "928636f4-fd0d-4149-978e-a6fb68bb19de" # 0000-GA-STDAPPS + +DEPLOY_ENV: dev + +logging: + level: + root: INFO +sentry.environment: preprod + +SANITY_DATASET: "ba-brev" + +PDL_URL: https://pdl-api.dev-fss-pub.nais.io +PDL_SCOPE: api://dev-fss.pdl.pdl-api/.default + +FAMILIE_TILBAKE_API_URL_SCOPE: api://dev-gcp.teamfamilie.familie-tilbake/.default + +FAMILIE_EF_SAK_API_URL_SCOPE: api://dev-gcp.teamfamilie.familie-ef-sak/.default + + +FAMILIE_INTEGRASJONER_API_URL: https://familie-integrasjoner.dev-fss-pub.nais.io/api +FAMILIE_INTEGRASJONER_SCOPE: api://dev-fss.teamfamilie.familie-integrasjoner/.default + +FAMILIE_BA_INFOTRYGD_FEED_SCOPE: api://dev-fss.teamfamilie.familie-ba-infotrygd-feed/.default +FAMILIE_BA_INFOTRYGD_SCOPE: api://dev-fss.teamfamilie.familie-ba-infotrygd/.default +FAMILIE_OPPDRAG_SCOPE: api://dev-fss.teamfamilie.familie-oppdrag/.default +FAMILIE_BA_INFOTRYGD_API_URL: https://familie-ba-infotrygd.dev-fss-pub.nais.io +FAMILIE_BA_INFOTRYGD_FEED_API_URL: https://familie-ba-infotrygd-feed.dev-fss-pub.nais.io/api + +FAMILIE_OPPDRAG_API_URL: https://familie-oppdrag.dev-fss-pub.nais.io/api +FAMILIE_STATISTIKK_URL: https://familie-ba-statistikk.dev-fss-pub.nais.io/api +CRON_FAGSAKSTATUS_SCHEDULER: "0 0/30 * ? * *" + +ENVIRONMENT_NAME: q2 diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/application-prod.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/application-prod.yaml new file mode 100644 index 000000000..1c7afb23a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/application-prod.yaml @@ -0,0 +1,180 @@ +no.nav.security.jwt: + issuer.azuread: + discoveryurl: ${AZURE_APP_WELL_KNOWN_URL} + accepted_audience: ${AZURE_APP_CLIENT_ID} + cookie_name: azure_token + client: + registration: + familie-integrasjoner-onbehalfof: + resource-url: ${FAMILIE_INTEGRASJONER_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl-onbehalfof: + resource-url: ${PDL_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${PDL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl-clientcredentials: + resource-url: ${PDL_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${PDL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-integrasjoner-clientcredentials: + resource-url: ${FAMILIE_INTEGRASJONER_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag-clientcredentials: + resource-url: ${FAMILIE_OPPDRAG_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-feed-clientcredentials: + resource-url: ${FAMILIE_BA_INFOTRYGD_FEED_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_BA_INFOTRYGD_FEED_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-onbehalfof: + resource-url: ${FAMILIE_BA_INFOTRYGD_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_BA_INFOTRYGD_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-clientcredentials: + resource-url: ${FAMILIE_BA_INFOTRYGD_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_BA_INFOTRYGD_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag-onbehalfof: + resource-url: ${FAMILIE_OPPDRAG_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-tilbake-onbehalfof: + resource-url: ${FAMILIE_TILBAKE_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_TILBAKE_API_URL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-tilbake-clientcredentials: + resource-url: ${FAMILIE_TILBAKE_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_TILBAKE_API_URL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ef-sak-onbehalfof: + resource-url: ${FAMILIE_EF_SAK_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_EF_SAK_API_URL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ef-sak-clientcredentials: + resource-url: ${FAMILIE_EF_SAK_API_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_EF_SAK_API_URL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-statistikk-clientcredentials: + resource-url: ${FAMILIE_STATISTIKK_URL} + token-endpoint-url: https://login.microsoftonline.com/navno.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: api://prod-fss.teamfamilie.familie-ba-statistikk/.default + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + +rolle: + veileder: "199c2b39-e535-4ae8-ac59-8ccbee7991ae" + saksbehandler: "847e3d72-9dc1-41c3-80ff-f5d4acdd5d46" + beslutter: "7a271f87-39fb-468b-a9ee-6cf3c070f548" + forvalter: "3d718ae5-f25e-47a4-b4b3-084a97604c1d" + kode6: "ad7b87a6-9180-467c-affc-20a566b0fec0" # 0000-GA-Strengt_Fortrolig_Adresse + kode7: "9ec6487d-f37a-4aad-a027-cd221c1ac32b" # 0000-GA-Fortrolig_Adresse + +spring: + datasource: + url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/familie-ba-sak + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + +sentry.environment: prod + +prosessering.rolle: "87190cf3-b278-457d-8ab7-1a5c55a9edd7" # Gruppen teamfamilie + +SANITY_DATASET: "ba-brev" + +PDL_URL: https://pdl-api.prod-fss-pub.nais.io +PDL_SCOPE: api://prod-fss.pdl.pdl-api/.default + +FAMILIE_TILBAKE_API_URL_SCOPE: api://prod-gcp.teamfamilie.familie-tilbake/.default + +FAMILIE_EF_SAK_API_URL_SCOPE: api://prod-gcp.teamfamilie.familie-ef-sak/.default + +FAMILIE_KLAGE_SCOPE: api://prod-gcp.teamfamilie.familie-klage/.default + +FAMILIE_INTEGRASJONER_API_URL: https://familie-integrasjoner.prod-fss-pub.nais.io/api +FAMILIE_INTEGRASJONER_SCOPE: api://prod-fss.teamfamilie.familie-integrasjoner/.default + +FAMILIE_BA_INFOTRYGD_FEED_SCOPE: api://prod-fss.teamfamilie.familie-ba-infotrygd-feed/.default +FAMILIE_BA_INFOTRYGD_SCOPE: api://prod-fss.teamfamilie.familie-ba-infotrygd/.default +FAMILIE_BA_INFOTRYGD_API_URL: https://familie-ba-infotrygd.prod-fss-pub.nais.io +FAMILIE_BA_INFOTRYGD_FEED_API_URL: https://familie-ba-infotrygd-feed.prod-fss-pub.nais.io/api + +FAMILIE_OPPDRAG_API_URL: https://familie-oppdrag.prod-fss-pub.nais.io/api +FAMILIE_OPPDRAG_SCOPE: api://prod-fss.teamfamilie.familie-oppdrag/.default +FAMILIE_STATISTIKK_URL: https://familie-ba-statistikk.prod-fss-pub.nais.io/api + +# Swagger +AUTHORIZATION_URL: https://login.microsoftonline.com/navno.onmicrosoft.com/oauth2/v2.0/authorize +TOKEN_URL: https://login.microsoftonline.com/navno.onmicrosoft.com/oauth2/v2.0/token + +ENVIRONMENT_NAME: p diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/application.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/application.yaml new file mode 100644 index 000000000..e5f702dde --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/application.yaml @@ -0,0 +1,152 @@ +application: + name: familie-ba-sak + +server: + servlet: + context-path: / + port: 8089 + shutdown: graceful + +no.nav.security.jwt: + client: + registration: + familie-klage-onbehalfof: + resource-url: ${FAMILIE_KLAGE_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_KLAGE_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + +spring: + lifecycle: + timeout-per-shutdown-phase: 20s + autoconfigure.exclude: org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration + main: + allow-bean-definition-overriding: true + banner-mode: "off" + datasource: + hikari: + maximum-pool-size: 20 + connection-test-query: "select 1" + max-lifetime: 900000 + minimum-idle: 1 + data-source-properties.stringtype: unspecified # Nødvendig for å kunde sende en String til et json-felt i PostgresSql + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: "org.hibernate.dialect.PostgreSQLDialect" + temp: + use_jdbc_metadata_defaults: false + flyway: + enabled: true + locations: classpath:db/migration,classpath:db/init + kafka: + client-id: familie-ba-sak + +springdoc: + swagger-ui: + oauth: + use-pkce-with-authorization-code-grant: true + client-id: ${AZURE_APP_CLIENT_ID} + scope-separator: "," + disable-swagger-default-url: true + +logging: + config: "classpath:logback-spring.xml" +sentry.dsn: https://dd9a6107bdda4edeb51ece7283f37af4@sentry.gc.nav.no/112 +sentry.logging.enabled: true + +retry.backoff.delay: 5000 + +rolle: + veileder: "93a26831-9866-4410-927b-74ff51a9107c" + saksbehandler: "d21e00a4-969d-4b28-8782-dc818abfae65" + beslutter: "9449c153-5a1e-44a7-84c6-7cc7a8867233" + forvalter: "c62e908a-cf20-4ad0-b7b3-3ff6ca4bf38b" + kode6: "5ef775f2-61f8-4283-bf3d-8d03f428aa14" # 0000-GA-Strengt_Fortrolig_Adresse + kode7: "ea930b6b-9397-44d9-b9e6-f4cf527a632a" # 0000-GA-Fortrolig_Adresse + +funksjonsbrytere: + enabled: true + unleash: + uri: https://unleash.nais.io/api/ + cluster: ${NAIS_CLUSTER_NAME} + applicationName: ${NAIS_APP_NAME} + kafka: + producer: + enabled: true + +management: + endpoint: + health: + show-details: always + group: + readyness: + include: db + liveness: + include: db + endpoints.web: + exposure.include: info, health, metrics, prometheus + base-path: "/internal" + metrics.export.prometheus.enabled: true + health: + livenessstate: + enabled: true + readinessstate: + enabled: true + db: + enabled: true + metrics: + web: + server: + request: + autotime: + enabled: true + + +prosessering: + continuousRunning.enabled: true + maxantall: 10 + fixedDelayString: + in: + milliseconds: 5000 + delete: + after: + weeks: 4 + +FAMILIE_EF_SAK_API_URL_SCOPE: api://dev-gcp.teamfamilie.familie-ef-sak/.default +FAMILIE_EF_SAK_API_URL: http://familie-ef-sak/api + +FAMILIE_KLAGE_URL: http://familie-klage +FAMILIE_KLAGE_SCOPE: api://${DEPLOY_ENV}-gcp.teamfamilie.familie-klage/.default + +PDL_SCOPE: api://dev-fss.pdl.pdl-api/.default +SANITY_DATASET: "ba-brev" + +BA_SAK_FRONTEND_CLIENT_ID: "dummy" +BA_MOTTAK_CLIENT_ID: "dummy" +FAMILIE_PROSESSERING_CLIENT_ID: "dummy" +BA_SKATTEETATEN_CLIENT_ID: "dummy" + +FAMILIE_BREV_API_URL: http://familie-brev +FAMILIE_BA_INFOTRYGD_FEED_API_URL: http://familie-ba-infotrygd-feed/api +FAMILIE_BA_INFOTRYGD_API_URL: http://familie-ba-infotrygd +FAMILIE_TILBAKE_API_URL: http://familie-tilbake/api +PDL_URL: http://pdl-api.default +FAMILIE_INTEGRASJONER_API_URL: http://familie-integrasjoner/api +FAMILIE_OPPDRAG_API_URL: http://familie-oppdrag/api +SANITY_FAMILIE_API_URL: https://xsrv1mh6.apicdn.sanity.io/v2021-06-07/data/query/ba-brev +ECB_API_URL: https://data-api.ecb.europa.eu/service/data/EXR/ +CRON_FAGSAKSTATUS_SCHEDULER: "0 0 7 1 * *" + +# Swagger +AUTHORIZATION_URL: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/authorize +TOKEN_URL: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token +API_SCOPE: api://${AZURE_APP_CLIENT_ID}/.default + +DEPLOY_ENV: prod diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/init/V1__sak.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/init/V1__sak.sql new file mode 100644 index 000000000..3b921892b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/init/V1__sak.sql @@ -0,0 +1,36 @@ +CREATE TABLE FAGSAK +( + ID bigint primary key, + AKTOER_ID VARCHAR(50) not null, + PERSON_IDENT VARCHAR(50) not null, + VERSJON bigint DEFAULT 0, + OPPRETTET_AV VARCHAR(20) DEFAULT 'VL', + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp, + ENDRET_AV VARCHAR(20), + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE FAGSAK_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +create index on FAGSAK (AKTOER_ID); +create index on FAGSAK (PERSON_IDENT); + +COMMENT ON COLUMN FAGSAK.AKTOER_ID is 'Søker som har stilt kravet'; + + +CREATE TABLE BEHANDLING +( + ID bigint primary key, + SAKSNUMMER varchar(19) not null unique, + FK_FAGSAK_ID bigint references FAGSAK (id), + VERSJON bigint DEFAULT 0, + OPPRETTET_AV VARCHAR(20) DEFAULT 'VL', + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp, + ENDRET_AV VARCHAR(20), + ENDRET_TID TIMESTAMP(3), + JOURNALPOST_ID VARCHAR(50) +); + +create index on BEHANDLING (fk_fagsak_id); +create index on BEHANDLING (SAKSNUMMER); +CREATE SEQUENCE BEHANDLING_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/init/V2__enkel_personopplysning.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/init/V2__enkel_personopplysning.sql new file mode 100644 index 000000000..cff43bd69 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/init/V2__enkel_personopplysning.sql @@ -0,0 +1,43 @@ +create table GR_PERSONOPPLYSNINGER +( + id bigint primary key, + fk_behandling_id bigint references BEHANDLING (id) not null, + versjon bigint default 0 not null, + opprettet_av VARCHAR(20) default 'VL' not null, + opprettet_tid TIMESTAMP(3) default localtimestamp not null, + endret_av VARCHAR(20), + endret_tid TIMESTAMP(3), + aktiv boolean default true not null +); + +CREATE SEQUENCE GR_PERSONOPPLYSNINGER_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +create index on GR_PERSONOPPLYSNINGER (fk_behandling_id); + +CREATE UNIQUE INDEX UIDX_GR_PERSONOPPLYSNINGER_01 + ON GR_PERSONOPPLYSNINGER + ( + (CASE + WHEN aktiv = true + THEN fk_behandling_id + ELSE NULL END), + (CASE + WHEN aktiv = true + THEN aktiv + ELSE NULL END) + ); + +CREATE TABLE PO_PERSON +( + id bigint primary key NOT NULL, + fk_gr_personopplysninger_id bigint references gr_personopplysninger (id) NOT NULL, + person_ident VARCHAR(50) NOT NULL, + type varchar(10) NOT NULL, + opprettet_av varchar(20) DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT current_timestamp NOT NULL, + endret_av varchar(20), + versjon bigint DEFAULT 0 NOT NULL, + endret_tid TIMESTAMP(3) +); + +CREATE SEQUENCE PO_PERSON_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +create index on PO_PERSON (fk_gr_personopplysninger_id); diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V100__fjern_behandling_gjeldende_for_fremtidig_utbetaling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V100__fjern_behandling_gjeldende_for_fremtidig_utbetaling.sql new file mode 100644 index 000000000..58602fd2d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V100__fjern_behandling_gjeldende_for_fremtidig_utbetaling.sql @@ -0,0 +1,2 @@ +ALTER TABLE BEHANDLING + DROP COLUMN gjeldende_for_fremtidig_utbetaling; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V101__legg_til_behandling_oppsto_i.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V101__legg_til_behandling_oppsto_i.sql new file mode 100644 index 000000000..ee6f6c625 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V101__legg_til_behandling_oppsto_i.sql @@ -0,0 +1,25 @@ +ALTER TABLE andel_tilkjent_ytelse + ADD COLUMN kilde_behandling_id BIGINT REFERENCES behandling (id); + +UPDATE andel_tilkjent_ytelse +SET kilde_behandling_id = andelMedKildeBehandling.kilde_behandling_id +FROM ( + WITH andelMedFagsak AS (SELECT andel_tilkjent_ytelse.id AS andelId, + andel_tilkjent_ytelse.periode_offset AS andelPeriodeId, + behandling.fk_fagsak_id AS fagsakId + FROM andel_tilkjent_ytelse, + behandling + WHERE andel_tilkjent_ytelse.fk_behandling_id = behandling.id), + kildeBehandling AS (SELECT behandling.fk_fagsak_id AS fagsakId, + andel_tilkjent_ytelse.periode_offset AS fagsakPeriodeId, + min(behandling.id) AS kilde_behandling_id + FROM behandling + INNER JOIN andel_tilkjent_ytelse + ON behandling.id = andel_tilkjent_ytelse.fk_behandling_id + GROUP BY fagsakId, fagsakPeriodeId) + SELECT andelMedFagsak.andelId, + kildeBehandling.kilde_behandling_id + FROM andelMedFagsak + INNER JOIN kildeBehandling ON andelMedFagsak.andelPeriodeId = kildeBehandling.fagsakPeriodeId AND + andelMedFagsak.fagsakId = kildeBehandling.fagsakId) AS andelMedKildeBehandling +WHERE andel_tilkjent_ytelse.id = andelMedKildeBehandling.andelId; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V102__manuelt_oppdater_fagsakstatus.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V102__manuelt_oppdater_fagsakstatus.sql new file mode 100644 index 000000000..52e1686ff --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V102__manuelt_oppdater_fagsakstatus.sql @@ -0,0 +1,19 @@ +/* Kjøring av script 01.12.20 feilet pga manglende leader election på poder */ +/* Kjører derfor scriptet manuelt her og oppdaterer status */ + +UPDATE fagsak +SET status = 'AVSLUTTET' +WHERE fagsak.id in (select id from fagsak + where fagsak.id in ( + with sisteIverksatte as ( + select b.fk_fagsak_id as fagsakId, max(b.id) as behandlingId + from behandling b + inner join tilkjent_ytelse ty on b.id = ty.fk_behandling_id + inner join fagsak f on f.id = b.fk_fagsak_id + where ty.utbetalingsoppdrag IS NOT NULL + and f.status = 'LØPENDE' + group by b.id) + select sisteIverksatte.fagsakId + from sisteIverksatte + inner join tilkjent_ytelse ty on sisteIverksatte.behandlingId = ty.fk_behandling_id + where ty.stonad_tom < now())) \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V103__behandling_resultat_til_vilk\303\245rsvurdering.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V103__behandling_resultat_til_vilk\303\245rsvurdering.sql" new file mode 100644 index 000000000..7a8385286 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V103__behandling_resultat_til_vilk\303\245rsvurdering.sql" @@ -0,0 +1,7 @@ +ALTER TABLE BEHANDLING_RESULTAT + RENAME TO VILKAARSVURDERING; +ALTER SEQUENCE BEHANDLING_RESULTAT_SEQ RENAME TO VILKAARSVURDERING_SEQ; + +ALTER TABLE PERSON_RESULTAT + RENAME COLUMN FK_BEHANDLING_RESULTAT_ID TO FK_VILKAARSVURDERING_ID; + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V104__resultat_paa_behandling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V104__resultat_paa_behandling.sql new file mode 100644 index 000000000..51342848a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V104__resultat_paa_behandling.sql @@ -0,0 +1,4 @@ +/* De gamle sakene i produksjon vil få resultat IKKE_VURDERT og vil være feil til vi migrerer over resultat */ +ALTER TABLE BEHANDLING + ADD COLUMN resultat VARCHAR DEFAULT 'IKKE_VURDERT' NOT NULL; + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V105__dupliser_behandlingresultat.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V105__dupliser_behandlingresultat.sql new file mode 100644 index 000000000..24913adb6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V105__dupliser_behandlingresultat.sql @@ -0,0 +1,7 @@ +update behandling +set resultat = behandling_med_resultat.eksisterende_resultat +from (select b.id as behandling_id, + v.samlet_resultat as eksisterende_resultat + from behandling b + inner join vilkaarsvurdering v on b.id = v.fk_behandling_id and v.aktiv = true) as behandling_med_resultat +where behandling.id = behandling_med_resultat.behandling_id; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V106__konsistensavstemming_datoer_2021.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V106__konsistensavstemming_datoer_2021.sql new file mode 100644 index 000000000..1261a5a99 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V106__konsistensavstemming_datoer_2021.sql @@ -0,0 +1,13 @@ +INSERT INTO batch (id, kjoredato) +VALUES (nextval('batch_seq'), TO_TIMESTAMP('06-01-2021', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('29-01-2021', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('26-02-2021', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('31-03-2021', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('26-04-2021', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('28-05-2021', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('29-06-2021', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('30-07-2021', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('30-08-2021', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('27-09-2021', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('29-10-2021', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('22-11-2021', 'DD-MM-YYYY SS:MS')); \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V107__vilk\303\245r_resultat_er_automatisk_vurdert.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V107__vilk\303\245r_resultat_er_automatisk_vurdert.sql" new file mode 100644 index 000000000..9b6f457d9 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V107__vilk\303\245r_resultat_er_automatisk_vurdert.sql" @@ -0,0 +1,7 @@ +ALTER TABLE VILKAR_RESULTAT + ADD COLUMN er_automatisk_vurdert BOOLEAN DEFAULT FALSE NOT NULL; + + +UPDATE VILKAR_RESULTAT +SET er_automatisk_vurdert = TRUE +WHERE vilkar = 'UNDER_18_ÅR'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V108__utbetaling_begrunnelse_til_vedtak_begrunnelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V108__utbetaling_begrunnelse_til_vedtak_begrunnelse.sql new file mode 100644 index 000000000..b108aaf59 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V108__utbetaling_begrunnelse_til_vedtak_begrunnelse.sql @@ -0,0 +1,9 @@ +ALTER TABLE UTBETALING_BEGRUNNELSE + RENAME TO VEDTAK_BEGRUNNELSE; +ALTER SEQUENCE UTBETALING_BEGRUNNELSE_SEQ RENAME TO VEDTAK_BEGRUNNELSE_SEQ; + +ALTER TABLE VEDTAK_BEGRUNNELSE + RENAME COLUMN VEDTAK_BEGRUNNELSE TO BEGRUNNELSE; + +ALTER TABLE VEDTAK_BEGRUNNELSE + DROP COLUMN BEGRUNNELSE_TYPE; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V109__oppdatere_behandlingsresultat.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V109__oppdatere_behandlingsresultat.sql new file mode 100644 index 000000000..6de1cd6e9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V109__oppdatere_behandlingsresultat.sql @@ -0,0 +1,2 @@ +UPDATE behandling SET resultat='ENDRET_OG_FORTSATT_INNVILGET' WHERE resultat='ENDRING_OG_LØPENDE'; +UPDATE behandling SET resultat='ENDRET_OG_OPPHØRT' WHERE resultat='ENDRING_OG_OPPHØRT'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V10__unique_fagsak_for_person.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V10__unique_fagsak_for_person.sql new file mode 100644 index 000000000..ee0d3bba8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V10__unique_fagsak_for_person.sql @@ -0,0 +1 @@ +alter table FAGSAK add unique (person_ident); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V110__slett_tomme_vedtakbegrunnelser.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V110__slett_tomme_vedtakbegrunnelser.sql new file mode 100644 index 000000000..9a7788378 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V110__slett_tomme_vedtakbegrunnelser.sql @@ -0,0 +1 @@ +DELETE FROM vedtak_begrunnelse WHERE begrunnelse is null; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V111__saksstatistikk_mellomlagring.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V111__saksstatistikk_mellomlagring.sql new file mode 100644 index 000000000..f7ecb7f6c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V111__saksstatistikk_mellomlagring.sql @@ -0,0 +1,13 @@ +CREATE TABLE SAKSSTATISTIKK_MELLOMLAGRING +( + ID BIGINT NOT NULL PRIMARY KEY, + OFFSET_VERDI BIGINT NOT NULL, + FUNKSJONELL_ID VARCHAR NOT NULL, + TYPE VARCHAR NOT NULL, + KONTRAKT_VERSJON VARCHAR NOT NULL, + JSON text NOT NULL, + KONVERTERT_TID TIMESTAMP(3) DEFAULT LOCALTIMESTAMP, + OPPRETTET_TID TIMESTAMP(3) DEFAULT LOCALTIMESTAMP NOT NULL +); + +CREATE SEQUENCE SAKSSTATISTIKK_MELLOMLAGRING_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V112__legg_til_avslag_flagg.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V112__legg_til_avslag_flagg.sql new file mode 100644 index 000000000..f8a1b3357 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V112__legg_til_avslag_flagg.sql @@ -0,0 +1,6 @@ +ALTER TABLE VILKAR_RESULTAT + ADD COLUMN er_eksplisitt_avslag_paa_soknad BOOLEAN; + +UPDATE VILKAR_RESULTAT +SET er_eksplisitt_avslag_paa_soknad = false +WHERE resultat = 'IKKE_OPPFYLT'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V113__altered_totrinnskontroll_legg_til_saksbehandler_id.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V113__altered_totrinnskontroll_legg_til_saksbehandler_id.sql new file mode 100644 index 000000000..18c3daeda --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V113__altered_totrinnskontroll_legg_til_saksbehandler_id.sql @@ -0,0 +1,5 @@ +ALTER TABLE TOTRINNSKONTROLL + ADD COLUMN SAKSBEHANDLER_ID VARCHAR DEFAULT 'ukjent' NOT NULL; + +ALTER TABLE TOTRINNSKONTROLL + ADD COLUMN BESLUTTER_ID VARCHAR; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V114__opprett_andre_vurderinger.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V114__opprett_andre_vurderinger.sql new file mode 100644 index 000000000..5543282c4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V114__opprett_andre_vurderinger.sql @@ -0,0 +1,15 @@ +CREATE TABLE ANNEN_VURDERING +( + ID BIGINT PRIMARY KEY, + FK_PERSON_RESULTAT_ID BIGINT REFERENCES person_resultat (id) NOT NULL, + RESULTAT VARCHAR NOT NULL, + TYPE VARCHAR NOT NULL, + BEGRUNNELSE TEXT, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE ANNEN_VURDERING_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V115__saksstatistikk_mellomlagring.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V115__saksstatistikk_mellomlagring.sql new file mode 100644 index 000000000..dda37b051 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V115__saksstatistikk_mellomlagring.sql @@ -0,0 +1,4 @@ +ALTER TABLE SAKSSTATISTIKK_MELLOMLAGRING ADD COLUMN SENDT_TID TIMESTAMP(3) DEFAULT LOCALTIMESTAMP; +ALTER TABLE SAKSSTATISTIKK_MELLOMLAGRING ADD COLUMN TYPE_ID BIGINT; + +ALTER TABLE SAKSSTATISTIKK_MELLOMLAGRING ALTER COLUMN OFFSET_VERDI DROP NOT NULL; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V116__periode_resultat_fk_til_person_resultat_fk.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V116__periode_resultat_fk_til_person_resultat_fk.sql new file mode 100644 index 000000000..c6e2c6f91 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V116__periode_resultat_fk_til_person_resultat_fk.sql @@ -0,0 +1,3 @@ +ALTER TABLE vilkar_resultat + RENAME COLUMN fk_periode_resultat_id TO fk_person_resultat_id; + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V117__bugfix_periode_resultat_fk_til_person_resultat_fk.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V117__bugfix_periode_resultat_fk_til_person_resultat_fk.sql new file mode 100644 index 000000000..1db471e42 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V117__bugfix_periode_resultat_fk_til_person_resultat_fk.sql @@ -0,0 +1 @@ +ALTER TABLE vilkar_resultat RENAME CONSTRAINT vilkar_resultat_fk_periode_resultat_id_fkey TO vilkar_resultat_fk_person_resultat_id_fkey; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V118__replikerte_fra_opplysningsplikt_til_andre_vurderinger.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V118__replikerte_fra_opplysningsplikt_til_andre_vurderinger.sql new file mode 100644 index 000000000..1ef7e27a8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V118__replikerte_fra_opplysningsplikt_til_andre_vurderinger.sql @@ -0,0 +1,31 @@ +INSERT INTO ANNEN_VURDERING(id, + fk_person_resultat_id, + resultat, + type, + begrunnelse, + versjon, + opprettet_av, + opprettet_tid, + endret_av, + endret_tid) +SELECT nextval('ANNEN_VURDERING_SEQ'), + p.id, + CASE o.status + WHEN 'IKKE_SATT' THEN 'IKKE_VURDERT' + WHEN 'MOTTATT' THEN 'OPPFYLT' + WHEN 'IKKE_MOTTATT_AVSLAG' THEN 'IKKE_OPPFYLT' + WHEN 'IKKE_MOTTATT_FORTSETT' THEN 'IKKE_OPPFYLT' + ELSE 'IKKE_VURDERT' + END, + 'OPPLYSNINGSPLIKT', + o.begrunnelse, + 0, + o.opprettet_av, + o.opprettet_tid, + o.endret_av, + o.endret_tid +FROM OPPLYSNINGSPLIKT o + INNER JOIN VILKAARSVURDERING v + ON v.fk_behandling_id = o.fk_behandling_id + INNER JOIN PERSON_RESULTAT p ON p.fk_vilkaarsvurdering_id = v.id +WHERE v.aktiv = true diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V119__drop_nullable_tom_vedtakbegrunnelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V119__drop_nullable_tom_vedtakbegrunnelse.sql new file mode 100644 index 000000000..974499486 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V119__drop_nullable_tom_vedtakbegrunnelse.sql @@ -0,0 +1 @@ +ALTER TABLE VEDTAK_BEGRUNNELSE ALTER COLUMN tom DROP NOT NULL; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V11__lenger_status_task.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V11__lenger_status_task.sql new file mode 100644 index 000000000..888edd893 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V11__lenger_status_task.sql @@ -0,0 +1,3 @@ +alter table task alter column status set data type varchar(50); + +alter table task_logg alter column type set data type varchar(50); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V120__drop_opplysningsplikt.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V120__drop_opplysningsplikt.sql new file mode 100644 index 000000000..78fe9d4e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V120__drop_opplysningsplikt.sql @@ -0,0 +1,2 @@ +DROP TABLE OPPLYSNINGSPLIKT; +DROP SEQUENCE OPPLYSNINGSPLIKT_SEQ; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V121__migrer_gammelt_behandlingresultat.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V121__migrer_gammelt_behandlingresultat.sql new file mode 100644 index 000000000..f8fe44760 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V121__migrer_gammelt_behandlingresultat.sql @@ -0,0 +1 @@ +UPDATE behandling SET resultat='ENDRET' WHERE resultat='ENDRET_OG_FORTSATT_INNVILGET'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V122__migrer_gammelt_behandlingresultat_preprod.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V122__migrer_gammelt_behandlingresultat_preprod.sql new file mode 100644 index 000000000..d8fa25ca2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V122__migrer_gammelt_behandlingresultat_preprod.sql @@ -0,0 +1 @@ +UPDATE behandling SET resultat='HENLAGT_FEILAKTIG_OPPRETTET' WHERE resultat='HENLAGT'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V123__simulering.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V123__simulering.sql new file mode 100644 index 000000000..ebb4a93a3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V123__simulering.sql @@ -0,0 +1,25 @@ +CREATE TABLE vedtak_simulering_mottaker ( + id BIGINT PRIMARY KEY, + fk_vedtak_id BIGINT REFERENCES vedtak (id), + mottaker_nummer VARCHAR(50), + mottaker_type VARCHAR(50) +); + +CREATE SEQUENCE vedtak_simulering_mottaker_seq INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON vedtak_simulering_mottaker (fk_vedtak_id); + +CREATE TABLE vedtak_simulering_postering ( + id BIGINT PRIMARY KEY, + fk_vedtak_simulering_mottaker_id BIGINT REFERENCES vedtak_simulering_mottaker (id), + fag_omraade_kode VARCHAR(50), + fom TIMESTAMP(3), + tom TIMESTAMP(3), + betaling_type VARCHAR(50), + belop BIGINT, + postering_type VARCHAR(50), + forfallsdato TIMESTAMP(3), + uten_inntrekk BOOLEAN +); + +CREATE SEQUENCE vedtak_simulering_postering_seq INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON vedtak_simulering_postering (fk_vedtak_simulering_mottaker_id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V124__nullable_fom_og_vilkaarkobling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V124__nullable_fom_og_vilkaarkobling.sql new file mode 100644 index 000000000..6052d26b4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V124__nullable_fom_og_vilkaarkobling.sql @@ -0,0 +1,4 @@ +ALTER TABLE VEDTAK_BEGRUNNELSE + ALTER COLUMN fom DROP NOT NULL; +ALTER TABLE VEDTAK_BEGRUNNELSE + ADD COLUMN fk_vilkar_resultat_id BIGINT REFERENCES vilkar_resultat (id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V125__legg_baseentitet_til_simulering.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V125__legg_baseentitet_til_simulering.sql new file mode 100644 index 000000000..de19495a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V125__legg_baseentitet_til_simulering.sql @@ -0,0 +1,17 @@ +ALTER TABLE vedtak_simulering_mottaker +ADD COLUMN opprettet_av VARCHAR(512) DEFAULT 'VL'::CHARACTER VARYING NOT NULL; +ALTER TABLE vedtak_simulering_mottaker +ADD COLUMN opprettet_tid TIMESTAMP(3) DEFAULT LOCALTIMESTAMP NOT NULL; +ALTER TABLE vedtak_simulering_mottaker +ADD COLUMN endret_av VARCHAR(512); +ALTER TABLE vedtak_simulering_mottaker +ADD COLUMN endret_tid TIMESTAMP(3); + +ALTER TABLE vedtak_simulering_postering +ADD COLUMN opprettet_av VARCHAR(512) DEFAULT 'VL'::CHARACTER VARYING NOT NULL; +ALTER TABLE vedtak_simulering_postering +ADD COLUMN opprettet_tid TIMESTAMP(3) DEFAULT LOCALTIMESTAMP NOT NULL; +ALTER TABLE vedtak_simulering_postering +ADD COLUMN endret_av VARCHAR(512); +ALTER TABLE vedtak_simulering_postering +ADD COLUMN endret_tid TIMESTAMP(3); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V126__legg_versjonsnummer_til_simulering.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V126__legg_versjonsnummer_til_simulering.sql new file mode 100644 index 000000000..77bd82276 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V126__legg_versjonsnummer_til_simulering.sql @@ -0,0 +1,5 @@ +ALTER TABLE vedtak_simulering_mottaker + ADD COLUMN versjon BIGINT DEFAULT 0; + +ALTER TABLE vedtak_simulering_postering + ADD COLUMN versjon BIGINT DEFAULT 0; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V127__tilbakekreving.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V127__tilbakekreving.sql new file mode 100644 index 000000000..1070a7010 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V127__tilbakekreving.sql @@ -0,0 +1,18 @@ +CREATE TABLE tilbakekreving ( + id BIGINT PRIMARY KEY, + fk_vedtak_id BIGINT REFERENCES vedtak (id), + + valg VARCHAR NOT NULL, + varsel TEXT, + begrunnelse TEXT NOT NULL, + tilbakekrevingsbehandling_id TEXT, + + opprettet_av VARCHAR DEFAULT 'VL'::CHARACTER VARYING NOT NULL, + opprettet_tid TIMESTAMP DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + versjon BIGINT DEFAULT 0 +); + +CREATE SEQUENCE tilbakekreving_seq INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON tilbakekreving (fk_vedtak_id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V128__slett_manuell_opphorsdato_fra_vedtak.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V128__slett_manuell_opphorsdato_fra_vedtak.sql new file mode 100644 index 000000000..9f4e29fd8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V128__slett_manuell_opphorsdato_fra_vedtak.sql @@ -0,0 +1,2 @@ +ALTER TABLE VEDTAK + DROP COLUMN opphor_dato; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V129__flytt_tilbakekreving_til_behandling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V129__flytt_tilbakekreving_til_behandling.sql new file mode 100644 index 000000000..79caaf7a5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V129__flytt_tilbakekreving_til_behandling.sql @@ -0,0 +1,19 @@ +ALTER TABLE tilbakekreving + ADD COLUMN fk_behandling_id BIGINT REFERENCES behandling (id); + +-- Set fk_behandling_id til behandlings id som er relatert til vedtaket. +UPDATE tilbakekreving +SET fk_behandling_id = behandling.id +FROM behandling + JOIN vedtak + ON behandling.ID = vedtak.fk_behandling_id +WHERE tilbakekreving.fk_vedtak_id = vedtak.id + AND vedtak.aktiv = true; + +-- Slett alle tilbakekrevinger som er relatert til en inaktiv vedtak. +DELETE +FROM tilbakekreving +WHERE fk_behandling_id is null; + +ALTER TABLE tilbakekreving + DROP COLUMN fk_vedtak_id; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V12__modell_justering.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V12__modell_justering.sql new file mode 100644 index 000000000..468eea624 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V12__modell_justering.sql @@ -0,0 +1,11 @@ +alter table fagsak add column status varchar(50) default 'OPPRETTET'; + +alter table behandling add column status varchar(50) default 'OPPRETTET'; + +alter table behandling_vedtak rename to vedtak; +alter sequence BEHANDLING_VEDTAK_SEQ RENAME TO VEDTAK_SEQ; + + +alter table behandling_vedtak_barn rename to vedtak_barn; +alter table vedtak_barn rename column fk_behandling_vedtak_id to fk_vedtak_id; +alter sequence BEHANDLING_VEDTAK_BARN_SEQ RENAME TO VEDTAK_BARN_SEQ; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V130__flytt_simulering_til_behandling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V130__flytt_simulering_til_behandling.sql new file mode 100644 index 000000000..4c8936d7e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V130__flytt_simulering_til_behandling.sql @@ -0,0 +1,29 @@ +-- vedtak_simulering_mottaker til br_simulering_mottaker +ALTER TABLE vedtak_simulering_mottaker + RENAME TO okonomi_simulering_mottaker; + +ALTER TABLE okonomi_simulering_mottaker + ADD COLUMN fk_behandling_id BIGINT REFERENCES behandling (id); + +-- Set fk_behandling_id til behandlings id som er relatert til vedtaket. +UPDATE okonomi_simulering_mottaker +SET fk_behandling_id = behandling.id +FROM behandling + JOIN vedtak + ON behandling.id = vedtak.fk_behandling_id +WHERE okonomi_simulering_mottaker.fk_vedtak_id = vedtak.id + AND vedtak.aktiv = TRUE; + +ALTER TABLE okonomi_simulering_mottaker + DROP COLUMN fk_vedtak_id; + +ALTER SEQUENCE vedtak_simulering_mottaker_seq RENAME TO okonomi_simulering_mottaker_seq; + +-- vedtak_simulering_postering til okonomi_simulering_postering +ALTER TABLE vedtak_simulering_postering + RENAME TO okonomi_simulering_postering; + +ALTER TABLE okonomi_simulering_postering + RENAME COLUMN fk_vedtak_simulering_mottaker_id TO fk_okonomi_simulering_mottaker_id; + +ALTER SEQUENCE vedtak_simulering_postering_seq RENAME TO okonomi_simulering_postering_seq; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V131__rename_steg_simulering_til_vurder_tilbakekreving.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V131__rename_steg_simulering_til_vurder_tilbakekreving.sql new file mode 100644 index 000000000..e93c16e72 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V131__rename_steg_simulering_til_vurder_tilbakekreving.sql @@ -0,0 +1,3 @@ +UPDATE behandling_steg_tilstand +SET behandling_steg = 'VURDER_TILBAKEKREVING' +WHERE behandling_steg = 'SIMULERING' \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V132__legger_til_skj\303\270nnsmessig_vurdering_p\303\245_vilk\303\245r_resultat.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V132__legger_til_skj\303\270nnsmessig_vurdering_p\303\245_vilk\303\245r_resultat.sql" new file mode 100644 index 000000000..69b5bcebc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V132__legger_til_skj\303\270nnsmessig_vurdering_p\303\245_vilk\303\245r_resultat.sql" @@ -0,0 +1,2 @@ +ALTER TABLE vilkar_resultat + ADD COLUMN er_skjonnsmessig_vurdert BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V133__vedtaksperiode_og_begrunnelser.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V133__vedtaksperiode_og_begrunnelser.sql new file mode 100644 index 000000000..4bbbdad97 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V133__vedtaksperiode_og_begrunnelser.sql @@ -0,0 +1,42 @@ +CREATE TABLE VEDTAKSPERIODE +( + id BIGINT PRIMARY KEY, + fk_vedtak_id BIGINT REFERENCES vedtak (id), + + fom TIMESTAMP DEFAULT NULL, + tom TIMESTAMP DEFAULT NULL, + type VARCHAR NOT NULL, + + opprettet_av VARCHAR DEFAULT 'VL'::CHARACTER VARYING NOT NULL, + opprettet_tid TIMESTAMP DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + versjon BIGINT DEFAULT 0, + UNIQUE (fk_vedtak_id, fom, tom, type) +); + +CREATE SEQUENCE vedtaksperiode_seq INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON vedtaksperiode (fk_vedtak_id); + +CREATE TABLE VEDTAKSBEGRUNNELSE +( + id BIGINT PRIMARY KEY, + fk_vedtaksperiode_id BIGINT REFERENCES vedtaksperiode (id), + + vedtak_begrunnelse_spesifikasjon VARCHAR NOT NULL, + person_identer TEXT DEFAULT '' NOT NULL +); + +CREATE SEQUENCE vedtaksbegrunnelse_seq INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON vedtaksbegrunnelse (fk_vedtaksperiode_id); + +CREATE TABLE VEDTAKSBEGRUNNELSE_FRITEKST +( + id BIGINT PRIMARY KEY, + fk_vedtaksperiode_id BIGINT REFERENCES vedtaksperiode (id), + + fritekst TEXT DEFAULT '' NOT NULL +); + +CREATE SEQUENCE vedtaksbegrunnelse_fritekst_seq INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON vedtaksbegrunnelse_fritekst (fk_vedtaksperiode_id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V134__periode_paa_bostedsadresser_og_flere_bostedsadresser.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V134__periode_paa_bostedsadresser_og_flere_bostedsadresser.sql new file mode 100644 index 000000000..5521c1167 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V134__periode_paa_bostedsadresser_og_flere_bostedsadresser.sql @@ -0,0 +1,10 @@ +ALTER TABLE po_bostedsadresse + ADD COLUMN fom DATE; +ALTER TABLE po_bostedsadresse + ADD COLUMN tom DATE; + +ALTER TABLE po_bostedsadresse + ADD COLUMN fk_po_person_id BIGINT REFERENCES po_person (id); + +UPDATE po_bostedsadresse bosted +SET fk_po_person_id=(SELECT person.id FROM po_person person WHERE person.bostedsadresse_id = bosted.id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V135__jdbjournalpost_utvidelser.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V135__jdbjournalpost_utvidelser.sql new file mode 100644 index 000000000..21b775dbc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V135__jdbjournalpost_utvidelser.sql @@ -0,0 +1,2 @@ +ALTER TABLE JOURNALPOST + ADD COLUMN type VARCHAR \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V136__legg_til_sivilstad.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V136__legg_til_sivilstad.sql new file mode 100644 index 000000000..997341640 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V136__legg_til_sivilstad.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS po_sivilstand +( + id BIGINT PRIMARY KEY, + fk_po_person_id BIGINT REFERENCES po_person (id) NOT NULL, + fom DATE, + type VARCHAR NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + versjon BIGINT DEFAULT 0 NOT NULL +); + +CREATE SEQUENCE po_sivilstand_seq INCREMENT BY 50 START WITH 1000000 NO CYCLE; + +INSERT INTO po_sivilstand (ID, fk_po_person_id, fom, type, opprettet_av, opprettet_tid) + (SELECT nextval('po_sivilstand_seq'), id, null, sivilstand, opprettet_av, opprettet_tid + FROM po_person + WHERE sivilstand is not null); \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V137__legger_til_medlemskapsvurdering_p\303\245_vilk\303\245r_resultat.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V137__legger_til_medlemskapsvurdering_p\303\245_vilk\303\245r_resultat.sql" new file mode 100644 index 000000000..dd0a12545 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V137__legger_til_medlemskapsvurdering_p\303\245_vilk\303\245r_resultat.sql" @@ -0,0 +1,2 @@ +ALTER TABLE vilkar_resultat + ADD COLUMN er_medlemskap_vurdert BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V138__rename_dodsfall_enum.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V138__rename_dodsfall_enum.sql new file mode 100644 index 000000000..afe939f0a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V138__rename_dodsfall_enum.sql @@ -0,0 +1,3 @@ +UPDATE behandling +SET opprettet_aarsak='DØDSFALL_BRUKER' +WHERE opprettet_aarsak = 'DØDSFALL'; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V139__cascade_delete_foreign_keys.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V139__cascade_delete_foreign_keys.sql new file mode 100644 index 000000000..96cebfb3c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V139__cascade_delete_foreign_keys.sql @@ -0,0 +1,20 @@ +ALTER TABLE vedtaksbegrunnelse + DROP CONSTRAINT vedtaksbegrunnelse_fk_vedtaksperiode_id_fkey, + ADD CONSTRAINT vedtaksbegrunnelse_fk_vedtaksperiode_id_fkey + FOREIGN KEY (fk_vedtaksperiode_id) + REFERENCES vedtaksperiode (id) + ON DELETE CASCADE; + +ALTER TABLE andel_tilkjent_ytelse + DROP CONSTRAINT andel_tilkjent_ytelse_tilkjent_ytelse_id_fkey, + ADD CONSTRAINT andel_tilkjent_ytelse_tilkjent_ytelse_id_fkey + FOREIGN KEY (tilkjent_ytelse_id) + REFERENCES tilkjent_ytelse (id) + ON DELETE CASCADE; + +ALTER TABLE okonomi_simulering_postering + DROP CONSTRAINT vedtak_simulering_postering_fk_vedtak_simulering_mottaker__fkey, -- Er to understreker i fk-navn hentet fra database + ADD CONSTRAINT vedtak_simulering_postering_fk_vedtak_simulering_mottaker__fkey + FOREIGN KEY (fk_okonomi_simulering_mottaker_id) + REFERENCES okonomi_simulering_mottaker (id) + ON DELETE CASCADE; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V13__vedtak_resultat.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V13__vedtak_resultat.sql new file mode 100644 index 000000000..5267fa53f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V13__vedtak_resultat.sql @@ -0,0 +1 @@ +ALTER TABLE vedtak RENAME COLUMN status TO resultat; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V140__fjern_deprecated_bostedsadresse_sivilstand.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V140__fjern_deprecated_bostedsadresse_sivilstand.sql new file mode 100644 index 000000000..c2fe696d6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V140__fjern_deprecated_bostedsadresse_sivilstand.sql @@ -0,0 +1,4 @@ +ALTER TABLE po_person + DROP COLUMN sivilstand; +ALTER TABLE po_person + DROP COLUMN bostedsadresse_id; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V141__legg_til_begrunnelser_paa_vilkaar.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V141__legg_til_begrunnelser_paa_vilkaar.sql new file mode 100644 index 000000000..547f2ab3a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V141__legg_til_begrunnelser_paa_vilkaar.sql @@ -0,0 +1,7 @@ +ALTER TABLE vilkar_resultat + ADD COLUMN vedtak_begrunnelse_spesifikasjoner TEXT DEFAULT ''; + +UPDATE vilkar_resultat vr +SET vedtak_begrunnelse_spesifikasjoner=(SELECT string_agg(vb.begrunnelse, ';') + FROM vedtak_begrunnelse vb + WHERE vb.fk_vilkar_resultat_id = vr.id) WHERE vr.er_eksplisitt_avslag_paa_soknad=true; \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V142__legger_til_del_bosted_p\303\245_vilk\303\245r_resultat.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V142__legger_til_del_bosted_p\303\245_vilk\303\245r_resultat.sql" new file mode 100644 index 000000000..cdc44a6d2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V142__legger_til_del_bosted_p\303\245_vilk\303\245r_resultat.sql" @@ -0,0 +1,2 @@ +ALTER TABLE vilkar_resultat + ADD COLUMN er_delt_bosted BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V143__cascade_delete_foreign_keys_fritekst.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V143__cascade_delete_foreign_keys_fritekst.sql new file mode 100644 index 000000000..000f86871 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V143__cascade_delete_foreign_keys_fritekst.sql @@ -0,0 +1,6 @@ +ALTER TABLE vedtaksbegrunnelse_fritekst + DROP CONSTRAINT vedtaksbegrunnelse_fritekst_fk_vedtaksperiode_id_fkey, + ADD CONSTRAINT vedtaksbegrunnelse_fritekst_fk_vedtaksperiode_id_fkey + FOREIGN KEY (fk_vedtaksperiode_id) + REFERENCES vedtaksperiode (id) + ON DELETE CASCADE; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V144__oppdater_enums.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V144__oppdater_enums.sql new file mode 100644 index 000000000..f25741ba4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V144__oppdater_enums.sql @@ -0,0 +1,3 @@ +UPDATE vedtak_begrunnelse SET begrunnelse='ENDRET_OG_FORTSATT_INNVILGET' WHERE begrunnelse='REDUKSJON_FLYTTET_FORELDER'; +UPDATE vedtak_begrunnelse SET begrunnelse='OPPHØR_BARN_FLYTTET_FRA_SØKER' WHERE begrunnelse='OPPHØR_SØKER_FLYTTET_FRA_BARN'; +UPDATE vedtak_begrunnelse SET begrunnelse='FORTSATT_INNVILGET_SØKER_BOSATT_I_RIKET' WHERE begrunnelse='FORTSATT_INNVILGET_TEST'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V145__migrer_vadtaksbegrunnelse_til_veddtaksperiode_og_begrunnelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V145__migrer_vadtaksbegrunnelse_til_veddtaksperiode_og_begrunnelse.sql new file mode 100644 index 000000000..a42c1c444 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V145__migrer_vadtaksbegrunnelse_til_veddtaksperiode_og_begrunnelse.sql @@ -0,0 +1,68 @@ +INSERT INTO vedtaksperiode (id, fk_vedtak_id, fom, tom, type) + (SELECT NEXTVAL('vedtaksperiode_seq'), fk_vedtak_id, fom, tom, + CASE split_part(begrunnelse,'_', 1) + WHEN 'INNVILGET' THEN 'UTBETALING' + WHEN 'REDUKSJON' THEN 'UTBETALING' + WHEN 'AVSLAG' THEN 'AVSLAG' + WHEN 'OPPHØR' THEN 'OPPHØR' + WHEN 'FORTSATT' THEN 'FORTSATT_INNVILGET' + END AS type + FROM vedtak_begrunnelse vb + INNER JOIN vedtak v + ON vb.fk_vedtak_id = v.id + INNER JOIN behandling b + ON v.fk_behandling_id = b.id + WHERE NOT EXISTS( + SELECT id + FROM vedtaksperiode v + WHERE v.fk_vedtak_id = vb.fk_vedtak_id AND + ((v.fom IS NULL AND vb.fom IS NULL) OR v.fom = vb.fom) AND + ((v.tom IS NULL AND vb.tom IS NULL) OR v.tom = vb.tom) AND + v.type = v.type) + AND b.status != 'FATTER_VEDTAK' + AND b.resultat != 'FORTSATT_INNVILGET' + GROUP BY fk_vedtak_id, fom, tom, type); + +INSERT INTO vedtaksbegrunnelse(id, fk_vedtaksperiode_id, vedtak_begrunnelse_spesifikasjon) + SELECT nextval('vedtaksbegrunnelse_seq'), vp.id, begrunnelse + FROM vedtak_begrunnelse vb + INNER JOIN vedtaksperiode vp + ON vp.fk_vedtak_id = vb.fk_vedtak_id AND + ((vp.fom IS NULL AND vb.fom IS NULL) OR vp.fom = vb.fom) AND + ((vp.tom IS NULL AND vb.tom IS NULL) OR vp.tom = vb.tom) AND + vp.type = CASE split_part(vb.begrunnelse,'_', 1) + WHEN 'INNVILGET' THEN 'UTBETALING' + WHEN 'REDUKSJON' THEN 'UTBETALING' + WHEN 'AVSLAG' THEN 'AVSLAG' + WHEN 'OPPHØR' THEN 'OPPHØR' + WHEN 'FORTSATT' THEN 'FORTSATT_INNVILGET' + END + WHERE NOT EXISTS( + SELECT id + FROM vedtaksbegrunnelse vbs + WHERE vbs.fk_vedtaksperiode_id = vp.id + AND vbs.vedtak_begrunnelse_spesifikasjon = vb.begrunnelse) + AND vb.begrunnelse NOT LIKE '%FRITEKST%' + GROUP BY vp.id, begrunnelse; + +INSERT INTO vedtaksbegrunnelse_fritekst(id, fk_vedtaksperiode_id, fritekst) + SELECT nextval('vedtaksbegrunnelse_fritekst_seq'), vp.id, brev_begrunnelse + FROM vedtak_begrunnelse vb + INNER JOIN vedtaksperiode vp + ON vp.fk_vedtak_id = vb.fk_vedtak_id AND + ((vp.fom IS NULL AND vb.fom IS NULL) OR vp.fom = vb.fom) AND + ((vp.tom IS NULL AND vb.tom IS NULL) OR vp.tom = vb.tom) AND + vp.type = CASE split_part(vb.begrunnelse,'_', 1) + WHEN 'INNVILGET' THEN 'UTBETALING' + WHEN 'REDUKSJON' THEN 'UTBETALING' + WHEN 'AVSLAG' THEN 'AVSLAG' + WHEN 'OPPHØR' THEN 'OPPHØR' + WHEN 'FORTSATT' THEN 'FORTSATT_INNVILGET' + END + WHERE NOT EXISTS( + SELECT id + FROM vedtaksbegrunnelse_fritekst vbfs + WHERE vbfs.fk_vedtaksperiode_id = vp.id + AND vbfs.fritekst = vb.brev_begrunnelse) + AND vb.brev_begrunnelse IS NOT NULL + AND vb.begrunnelse LIKE '%FRITEKST%'; \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V146__opprett_f\303\270dselshendelsefiltrering_resultat.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V146__opprett_f\303\270dselshendelsefiltrering_resultat.sql" new file mode 100644 index 000000000..e955304b7 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V146__opprett_f\303\270dselshendelsefiltrering_resultat.sql" @@ -0,0 +1,20 @@ +CREATE TABLE FOEDSELSHENDELSEFILTRERING_RESULTAT +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT REFERENCES BEHANDLING (ID) NOT NULL, + FILTRERINGSREGEL VARCHAR NOT NULL, + RESULTAT VARCHAR NOT NULL, + BEGRUNNELSE TEXT NOT NULL, + EVALUERINGSAARSAKER TEXT NOT NULL, + REGEL_INPUT TEXT, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE FOEDSELSHENDELSEFILTRERING_RESULTAT_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON FOEDSELSHENDELSEFILTRERING_RESULTAT (FK_BEHANDLING_ID); + +UPDATE BEHANDLING_STEG_TILSTAND SET behandling_steg = 'HENLEGG_BEHANDLING' where behandling_steg = 'HENLEGG_SØKNAD'; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V147__dropp_etterbetaling_filtreringsresultat.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V147__dropp_etterbetaling_filtreringsresultat.sql new file mode 100644 index 000000000..1f64b3ff9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V147__dropp_etterbetaling_filtreringsresultat.sql @@ -0,0 +1 @@ +DELETE FROM FOEDSELSHENDELSEFILTRERING_RESULTAT WHERE FILTRERINGSREGEL = 'BARNETS_FØDSELSDATO_TRIGGER_IKKE_ETTERBETALING'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V148__totrinnskontroll_kontrollerte_sider.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V148__totrinnskontroll_kontrollerte_sider.sql new file mode 100644 index 000000000..543ea16ee --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V148__totrinnskontroll_kontrollerte_sider.sql @@ -0,0 +1 @@ +ALTER TABLE TOTRINNSKONTROLL ADD COLUMN kontrollerte_sider TEXT default ''; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V149__oppdater_opphorsbegrunnelse_enum.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V149__oppdater_opphorsbegrunnelse_enum.sql new file mode 100644 index 000000000..1e37c36ec --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V149__oppdater_opphorsbegrunnelse_enum.sql @@ -0,0 +1,9 @@ +UPDATE vedtak_begrunnelse SET begrunnelse='OPPHØR_UTVANDRET' WHERE begrunnelse='OPPHØR_BARN_UTVANDRET'; +UPDATE vedtak_begrunnelse SET begrunnelse='OPPHØR_UTVANDRET' WHERE begrunnelse='OPPHØR_SØKER_UTVANDRET'; +UPDATE vedtak_begrunnelse SET begrunnelse='OPPHØR_HAR_IKKE_OPPHOLDSTILLATELSE' WHERE begrunnelse='OPPHØR_BARN_HAR_IKKE_OPPHOLDSTILLATELSE'; +UPDATE vedtak_begrunnelse SET begrunnelse='OPPHØR_HAR_IKKE_OPPHOLDSTILLATELSE' WHERE begrunnelse='OPPHØR_SØKER_HAR_IKKE_OPPHOLDSTILLATELSE'; + +UPDATE VEDTAKSBEGRUNNELSE SET vedtak_begrunnelse_spesifikasjon='OPPHØR_UTVANDRET' WHERE vedtak_begrunnelse_spesifikasjon='OPPHØR_BARN_UTVANDRET'; +UPDATE VEDTAKSBEGRUNNELSE SET vedtak_begrunnelse_spesifikasjon='OPPHØR_UTVANDRET' WHERE vedtak_begrunnelse_spesifikasjon='OPPHØR_SØKER_UTVANDRET'; +UPDATE VEDTAKSBEGRUNNELSE SET vedtak_begrunnelse_spesifikasjon='OPPHØR_HAR_IKKE_OPPHOLDSTILLATELSE' WHERE vedtak_begrunnelse_spesifikasjon='OPPHØR_BARN_HAR_IKKE_OPPHOLDSTILLATELSE'; +UPDATE VEDTAKSBEGRUNNELSE SET vedtak_begrunnelse_spesifikasjon='OPPHØR_HAR_IKKE_OPPHOLDSTILLATELSE' WHERE vedtak_begrunnelse_spesifikasjon='OPPHØR_SØKER_HAR_IKKE_OPPHOLDSTILLATELSE'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V14__vilkaar.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V14__vilkaar.sql new file mode 100644 index 000000000..409eefb94 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V14__vilkaar.sql @@ -0,0 +1,29 @@ +create table SAMLET_VILKAR_RESULTAT +( + ID bigint primary key, + VERSJON bigint default 0 not null, + OPPRETTET_AV VARCHAR(20) default 'VL' not null, + OPPRETTET_TID TIMESTAMP(3) default localtimestamp not null, + ENDRET_AV VARCHAR(20), + ENDRET_TID TIMESTAMP(3) +); +CREATE SEQUENCE SAMLET_VILKAR_RESULTAT_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; + +create table VILKAR_RESULTAT +( + ID bigint primary key, + SAMLET_VILKAR_RESULTAT_ID bigint references SAMLET_VILKAR_RESULTAT (id) not null, + FK_PERSON_ID bigint references po_person (id) not null, + VILKAR VARCHAR(50) not null, + UTFALL VARCHAR(50) not null, + REGEL_INPUT text, + REGEL_OUTPUT text, + VERSJON bigint default 0 not null, + OPPRETTET_AV VARCHAR(20) default 'VL' not null, + OPPRETTET_TID TIMESTAMP(3) default localtimestamp not null, + ENDRET_AV VARCHAR(20), + ENDRET_TID TIMESTAMP(3) +); +CREATE SEQUENCE VILKAR_RESULTAT_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; + +alter table behandling add column samlet_vilkar_resultat_id bigint references SAMLET_VILKAR_RESULTAT (id) default null; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V150__opprett_overstyrt_utbetaling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V150__opprett_overstyrt_utbetaling.sql new file mode 100644 index 000000000..4573b88c2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V150__opprett_overstyrt_utbetaling.sql @@ -0,0 +1,20 @@ +CREATE TABLE ENDRET_UTBETALING_ANDEL +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT REFERENCES BEHANDLING (ID) NOT NULL, + FK_PO_PERSON_ID BIGINT REFERENCES PO_PERSON (ID) NOT NULL, + FOM TIMESTAMP(3) NOT NULL, + TOM TIMESTAMP(3) NOT NULL, + PROSENT NUMERIC NOT NULL, + AARSAK VARCHAR NOT NULL, + BEGRUNNELSE TEXT NOT NULL, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE ENDRET_UTBETALING_ANDEL_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON ENDRET_UTBETALING_ANDEL (FK_BEHANDLING_ID); + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V151__legg_til_ytelse_personer.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V151__legg_til_ytelse_personer.sql new file mode 100644 index 000000000..9cf0b9a57 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V151__legg_til_ytelse_personer.sql @@ -0,0 +1,2 @@ +ALTER TABLE vilkaarsvurdering + ADD COLUMN ytelse_personer TEXT DEFAULT ''; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V152__prosessering_jdbc_patch.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V152__prosessering_jdbc_patch.sql new file mode 100644 index 000000000..8175fd0b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V152__prosessering_jdbc_patch.sql @@ -0,0 +1,5 @@ +ALTER TABLE task ALTER COLUMN id SET DEFAULT nextval('task_seq'); +ALTER SEQUENCE task_seq OWNED BY task.id; + +ALTER TABLE task_logg ALTER COLUMN id SET DEFAULT nextval('task_logg_seq'); +ALTER SEQUENCE task_logg_seq OWNED BY task_logg.id; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V153__patch_task_jdbc_versjon.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V153__patch_task_jdbc_versjon.sql new file mode 100644 index 000000000..3a91d5d61 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V153__patch_task_jdbc_versjon.sql @@ -0,0 +1 @@ +UPDATE task SET versjon=1 WHERE versjon=0; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V154__utvid_andel_tilkjent_ytelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V154__utvid_andel_tilkjent_ytelse.sql new file mode 100644 index 000000000..1951f3016 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V154__utvid_andel_tilkjent_ytelse.sql @@ -0,0 +1,26 @@ +ALTER TABLE andel_tilkjent_ytelse + ADD COLUMN prosent NUMERIC; +-- Setter +UPDATE andel_tilkjent_ytelse SET prosent = 50 where belop in (677,527,485,827); +UPDATE andel_tilkjent_ytelse SET prosent = 100 where prosent is null; +ALTER TABLE andel_tilkjent_ytelse + ALTER COLUMN prosent SET NOT NULL; + +ALTER TABLE andel_tilkjent_ytelse + ADD COLUMN sats BIGINT; +UPDATE andel_tilkjent_ytelse SET sats = belop * 2 where belop in (677,527,485,827); +UPDATE andel_tilkjent_ytelse SET sats = belop where sats is null; +ALTER TABLE andel_tilkjent_ytelse + ALTER COLUMN sats SET NOT NULL; + +ALTER TABLE andel_tilkjent_ytelse + RENAME COLUMN belop TO kalkulert_utbetalingsbelop; + +CREATE TABLE ANDEL_TIL_ENDRET_ANDEL +( + FK_ANDEL_TILKJENT_YTELSE_ID BIGINT REFERENCES ANDEL_TILKJENT_YTELSE (ID) NOT NULL, + FK_ENDRET_UTBETALING_ANDEL_ID BIGINT REFERENCES ENDRET_UTBETALING_ANDEL (ID) NOT NULL, + PRIMARY KEY (FK_ANDEL_TILKJENT_YTELSE_ID, FK_ENDRET_UTBETALING_ANDEL_ID) +); + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V155__fag_sak_legg_til_arkivert_og_constraint.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V155__fag_sak_legg_til_arkivert_og_constraint.sql new file mode 100644 index 000000000..2ec361628 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V155__fag_sak_legg_til_arkivert_og_constraint.sql @@ -0,0 +1,5 @@ +ALTER TABLE fagsak_person + ADD COLUMN arkivert BOOLEAN DEFAULT FALSE NOT NULL; + +ALTER TABLE fagsak + ADD COLUMN arkivert BOOLEAN DEFAULT FALSE NOT NULL; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V156__arkiver_feilende_fag_sak_i_prod.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V156__arkiver_feilende_fag_sak_i_prod.sql new file mode 100644 index 000000000..edaeb3f9f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V156__arkiver_feilende_fag_sak_i_prod.sql @@ -0,0 +1,7 @@ +UPDATE fagsak_person +SET arkivert = TRUE +WHERE fk_fagsak_id = 1078652; + +UPDATE fagsak +SET arkivert = TRUE +WHERE id = 1078652; \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V157__fagsak_legg_til_unique_index_p\303\245_arkivert.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V157__fagsak_legg_til_unique_index_p\303\245_arkivert.sql" new file mode 100644 index 000000000..2b0ad6ee2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V157__fagsak_legg_til_unique_index_p\303\245_arkivert.sql" @@ -0,0 +1,2 @@ +CREATE UNIQUE INDEX uidx_fagsak_person_ident_ikke_arkivert ON fagsak_person(ident) + WHERE arkivert = false; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V158__legg_til_begrunnelse_paa_endringer.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V158__legg_til_begrunnelse_paa_endringer.sql new file mode 100644 index 000000000..2c82bed87 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V158__legg_til_begrunnelse_paa_endringer.sql @@ -0,0 +1,2 @@ +ALTER TABLE endret_utbetaling_andel + ADD COLUMN vedtak_begrunnelse_spesifikasjoner TEXT DEFAULT ''; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V159__endret_utbetaling_andel_definer_felt_nullable.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V159__endret_utbetaling_andel_definer_felt_nullable.sql new file mode 100644 index 000000000..00be53f2f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V159__endret_utbetaling_andel_definer_felt_nullable.sql @@ -0,0 +1,7 @@ +ALTER TABLE endret_utbetaling_andel + ALTER COLUMN fom DROP NOT NULL, + ALTER COLUMN tom DROP NOT NULL, + ALTER COLUMN prosent DROP NOT NULL, + ALTER COLUMN aarsak DROP NOT NULL, + ALTER COLUMN begrunnelse DROP NOT NULL, + ALTER COLUMN fk_po_person_id DROP NOT NULL; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V15__legger_til_oppgave_til_behandling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V15__legger_til_oppgave_til_behandling.sql new file mode 100644 index 000000000..8b8931b8f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V15__legger_til_oppgave_til_behandling.sql @@ -0,0 +1 @@ +alter table BEHANDLING add column oppgave_id varchar(19) default null; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V160__endret_utbetaling_andel_legg_til_to_nye_felt.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V160__endret_utbetaling_andel_legg_til_to_nye_felt.sql new file mode 100644 index 000000000..50ac50c4f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V160__endret_utbetaling_andel_legg_til_to_nye_felt.sql @@ -0,0 +1,3 @@ +ALTER TABLE endret_utbetaling_andel + ADD COLUMN avtaletidspunkt_delt_bosted TIMESTAMP(3), + ADD COLUMN soknadstidspunkt TIMESTAMP(3); \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V161__opprett_periode_overgangsst\303\270nad.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V161__opprett_periode_overgangsst\303\270nad.sql" new file mode 100644 index 000000000..cd0a38f2e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V161__opprett_periode_overgangsst\303\270nad.sql" @@ -0,0 +1,18 @@ +CREATE TABLE GR_PERIODE_OVERGANGSSTONAD +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT REFERENCES BEHANDLING (ID) NOT NULL, + PERSON_IDENT VARCHAR NOT NULL, + FOM TIMESTAMP(3) NOT NULL, + TOM TIMESTAMP(3) NOT NULL, + DATAKILDE VARCHAR NOT NULL, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE GR_PERIODE_OVERGANGSSTONAD_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON GR_PERIODE_OVERGANGSSTONAD (FK_BEHANDLING_ID); + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V162__cascade_slett_endret_andel_tilkjent_ytelse_relasjon.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V162__cascade_slett_endret_andel_tilkjent_ytelse_relasjon.sql new file mode 100644 index 000000000..4226d7d4e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V162__cascade_slett_endret_andel_tilkjent_ytelse_relasjon.sql @@ -0,0 +1,9 @@ +ALTER TABLE ANDEL_TIL_ENDRET_ANDEL + DROP CONSTRAINT andel_til_endret_andel_fk_andel_tilkjent_ytelse_id_fkey, + ADD CONSTRAINT andel_til_endret_andel_fk_andel_tilkjent_ytelse_id_fkey + FOREIGN KEY (FK_ANDEL_TILKJENT_YTELSE_ID) + REFERENCES andel_tilkjent_ytelse (id) + ON DELETE CASCADE + + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V163__legge_til_nytt_felt_vurderes_etter.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V163__legge_til_nytt_felt_vurderes_etter.sql new file mode 100644 index 000000000..8dc737ca3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V163__legge_til_nytt_felt_vurderes_etter.sql @@ -0,0 +1,6 @@ +ALTER TABLE vilkar_resultat + ADD COLUMN vurderes_etter varchar; + +UPDATE vilkar_resultat +SET vurderes_etter = 'NASJONALE_REGLER' +WHERE vilkar not in ('UNDER_18_ÅR', 'GIFT_PARTNERSKAP'); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V164__konsistensavstemming_datoer_2022.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V164__konsistensavstemming_datoer_2022.sql new file mode 100644 index 000000000..3071cdb37 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V164__konsistensavstemming_datoer_2022.sql @@ -0,0 +1,13 @@ +INSERT INTO batch (id, kjoredato) +VALUES (nextval('batch_seq'), TO_TIMESTAMP('05-01-2022', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('28-01-2022', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('25-02-2022', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('25-03-2022', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('26-04-2022', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('27-05-2022', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('29-06-2022', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('29-07-2022', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('30-08-2022', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('29-09-2022', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('28-10-2022', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('21-11-2022', 'DD-MM-YYYY SS:MS')); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V165__standardbegrunnelser_baseline.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V165__standardbegrunnelser_baseline.sql new file mode 100644 index 000000000..e98888898 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V165__standardbegrunnelser_baseline.sql @@ -0,0 +1,91 @@ +-- INNVILGELSE -> INNVILGET +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_BEREDSKAPSHJEM' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_BEREDSKAPSHJEM'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_HELE_FAMILIEN_TRYGDEAVTALE' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_HELE_FAMILIEN_TRYGDEAVTALE'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_HELE_FAMILIEN_PLIKTIG_MEDLEM' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_HELE_FAMILIEN_PLIKTIG_MEDLEM'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_SØKER_OG_BARN_PLIKTIG_MEDLEM' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_SØKER_OG_BARN_PLIKTIG_MEDLEM'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_ENIGHET_OM_OPPHØR_AV_AVTALE_OM_DELT_BOSTED' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_ENIGHET_OM_OPPHØR_AV_AVTALE_OM_DELT_BOSTED'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_VURDERING_HELE_FAMILIEN_FRIVILLIG_MEDLEM' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_VURDERING_HELE_FAMILIEN_FRIVILLIG_MEDLEM'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_UENIGHET_OM_OPPHØR_AV_AVTALE_OM_DELT_BOSTED' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_UENIGHET_OM_OPPHØR_AV_AVTALE_OM_DELT_BOSTED'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_HELE_FAMILIEN_FRIVILLIG_MEDLEM' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_HELE_FAMILIEN_FRIVILLIG_MEDLEM'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_VURDERING_HELE_FAMILIEN_PLIKTIG_MEDLEM' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_VURDERING_HELE_FAMILIEN_PLIKTIG_MEDLEM'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_SØKER_OG_BARN_OPPHOLD_I_UTLANDET_IKKE_MER_ENN_3_MÅNEDER' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_SØKER_OG_BARN_OPPHOLD_I_UTLANDET_IKKE_MER_ENN_3_MÅNEDER'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_SØKER_OG_BARN_FRIVILLIG_MEDLEM' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_SØKER_OG_BARN_FRIVILLIG_MEDLEM'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_VURDERING_SØKER_OG_BARN_FRIVILLIG_MEDLEM' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_VURDERING_SØKER_OG_BARN_FRIVILLIG_MEDLEM'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_ETTERBETALING_3_ÅR' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_ETTERBETALING_3_ÅR'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_SØKER_OG_BARN_TRYGDEAVTALE' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_SØKER_OG_BARN_TRYGDEAVTALE'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_ALENE_FRA_FØDSEL' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_ALENE_FRA_FØDSEL'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_VURDERING_SØKER_OG_BARN_PLIKTIG_MEDLEM' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_VURDERING_SØKER_OG_BARN_PLIKTIG_MEDLEM'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'INNVILGET_BARN_OPPHOLD_I_UTLANDET_IKKE_MER_ENN_3_MÅNEDER' +WHERE vedtak_begrunnelse_spesifikasjon like 'INNVILGELSE_BARN_OPPHOLD_I_UTLANDET_IKKE_MER_ENN_3_MÅNEDER'; + + +-- ENDRET_UTBETALINGSPERIODE -> ENDRET_UTBETALING +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'ENDRET_UTBETALING_DELT_BOSTED_FULL_UTBETALING' +WHERE vedtak_begrunnelse_spesifikasjon like 'ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_FULL_UTBETALING'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'ENDRET_UTBETALING_DELT_BOSTED_INGEN_UTBETALING' +WHERE vedtak_begrunnelse_spesifikasjon like 'ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_INGEN_UTBETALING'; + +-- PERIODE_ETTER_ENDRET_UTBETALING -> ETTER_ENDRET_UTBETALING +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'ETTER_ENDRET_UTBETALING_RETTSAVGJØRELSE_DELT_BOSTED' +WHERE vedtak_begrunnelse_spesifikasjon like 'PERIODE_ETTER_ENDRET_UTBETALING_RETTSAVGJØRELSE_DELT_BOSTED'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'ETTER_ENDRET_UTBETALING_AVTALE_DELT_BOSTED_FØLGES' +WHERE vedtak_begrunnelse_spesifikasjon like 'PERIODE_ETTER_ENDRET_UTBETALING_AVTALE_DELT_BOSTED_FØLGES'; + +UPDATE vedtaksbegrunnelse +SET vedtak_begrunnelse_spesifikasjon = 'ETTER_ENDRET_UTBETALING_HAR_AVTALE_DELT_BOSTED' +WHERE vedtak_begrunnelse_spesifikasjon like 'PERIODE_ETTER_ENDRET_UTBETALING_HAR_AVTALE_DELT_BOSTED'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V166__drop_vedtak_begrunnelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V166__drop_vedtak_begrunnelse.sql new file mode 100644 index 000000000..dce97b01b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V166__drop_vedtak_begrunnelse.sql @@ -0,0 +1 @@ +drop table vedtak_begrunnelse; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V167__opprett_person_ident_og_legg_til_kolonne_aktorid.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V167__opprett_person_ident_og_legg_til_kolonne_aktorid.sql new file mode 100644 index 000000000..9499785fa --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V167__opprett_person_ident_og_legg_til_kolonne_aktorid.sql @@ -0,0 +1,35 @@ +create table PERSONIDENT +( + ID BIGINT PRIMARY KEY, + AKTOER_ID VARCHAR NOT NULL, + FOEDSELSNUMMER VARCHAR NOT NULL, + AKTIV BOOLEAN DEFAULT FALSE NOT NULL, + GJELDER_TIL TIMESTAMP(3), + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3), + UNIQUE (FOEDSELSNUMMER) +); + +create sequence PERSONIDENT_SEQ increment by 50 start with 1000000 NO CYCLE; + +create unique index UIDX_PERSONIDENT_AKTOER_ID ON PERSONIDENT(AKTOER_ID) + where AKTIV = true; + +alter table FAGSAK_PERSON + add column AKTOER_ID VARCHAR; + +alter table ANDEL_TILKJENT_YTELSE + add column AKTOER_ID VARCHAR; + +alter table PERSON_RESULTAT + add column AKTOER_ID VARCHAR; + +alter table GR_PERIODE_OVERGANGSSTONAD + add column AKTOER_ID VARCHAR; + +alter table FOEDSELSHENDELSE_PRE_LANSERING + add column AKTOER_ID VARCHAR; + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V168__utdypende_vilkarsvurderinger.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V168__utdypende_vilkarsvurderinger.sql new file mode 100644 index 000000000..f860a9c3a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V168__utdypende_vilkarsvurderinger.sql @@ -0,0 +1,13 @@ +ALTER TABLE vilkar_resultat + ADD COLUMN utdypende_vilkarsvurderinger VARCHAR; + +-- er_skjonnsmessig_vurdert -> VURDERING_ANNET_GRUNNLAG +-- er_medlemskap_vurdert -> VURDERT_MEDLEMSKAP +-- er_delt_bosted -> DELT_BOSTED +UPDATE vilkar_resultat +SET utdypende_vilkarsvurderinger + = concat_ws(';' + , CASE WHEN er_skjonnsmessig_vurdert = TRUE THEN 'VURDERING_ANNET_GRUNNLAG' END + , CASE WHEN er_medlemskap_vurdert = TRUE THEN 'VURDERT_MEDLEMSKAP' END + , CASE WHEN er_delt_bosted = TRUE THEN 'DELT_BOSTED' END) +WHERE vilkar_resultat.utdypende_vilkarsvurderinger IS NULL diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V169__oppret_tabell_akt\303\270rid.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V169__oppret_tabell_akt\303\270rid.sql" new file mode 100644 index 000000000..7f6e5c4ec --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V169__oppret_tabell_akt\303\270rid.sql" @@ -0,0 +1,21 @@ +alter table PERSONIDENT drop constraint PERSONIDENT_PKEY; + +alter table PERSONIDENT add primary key (FOEDSELSNUMMER); + +drop sequence PERSONIDENT_SEQ; + +alter table PERSONIDENT rename column AKTOER_ID to FK_AKTOER_ID; +alter table PERSONIDENT drop column ID; + +create table AKTOER +( + AKTOER_ID VARCHAR PRIMARY KEY, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +alter table PERSONIDENT + add constraint FK_PERSONIDENT foreign key (FK_AKTOER_ID) references AKTOER (AKTOER_ID); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V16__behandling_kategorier.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V16__behandling_kategorier.sql new file mode 100644 index 000000000..bc46a4d6c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V16__behandling_kategorier.sql @@ -0,0 +1,2 @@ +alter table behandling add column kategori varchar(50) default 'NATIONAL'; +alter table behandling add column underkategori varchar(50) default 'ORDINÆR'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V170__populer_kolonner_for_aktorid.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V170__populer_kolonner_for_aktorid.sql new file mode 100644 index 000000000..2a2974f83 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V170__populer_kolonner_for_aktorid.sql @@ -0,0 +1,89 @@ +insert into aktoer(aktoer_id, + opprettet_av, + opprettet_tid, + endret_av, + endret_tid) +select distinct + on (aktoer_id) aktoer_id, + opprettet_av, + opprettet_tid, + 'VL', + localtimestamp +from po_person ppy +order by aktoer_id, opprettet_tid desc; + +insert into personident(foedselsnummer, + fk_aktoer_id, + aktiv, + gjelder_til, + opprettet_av, + opprettet_tid, + endret_av, + endret_tid) +select distinct + on (person_ident) person_ident, + aktoer_id, + case when + aktoer_id IN (select y.aktoer_id from po_person i join po_person y on i.aktoer_id = y.aktoer_id where y.person_ident != i.person_ident ) then false + else true + end, + null, + opprettet_av, + opprettet_tid, + 'VL', + localtimestamp +from po_person ppy +order by person_ident, opprettet_tid desc; + +alter table FAGSAK_PERSON rename column AKTOER_ID to FK_AKTOER_ID; +alter table FAGSAK_PERSON + add constraint FK_FAGSAK_PERSON foreign key (FK_AKTOER_ID) references AKTOER (AKTOER_ID); + +alter table ANDEL_TILKJENT_YTELSE rename column AKTOER_ID to FK_AKTOER_ID; +alter table ANDEL_TILKJENT_YTELSE + add constraint FK_ANDEL_TILKJENT_YTELSE foreign key (FK_AKTOER_ID) references AKTOER (AKTOER_ID); + +alter table PERSON_RESULTAT rename column AKTOER_ID to FK_AKTOER_ID; +alter table PERSON_RESULTAT + add constraint FK_PERSON_RESULTAT foreign key (FK_AKTOER_ID) references AKTOER (AKTOER_ID); + +alter table GR_PERIODE_OVERGANGSSTONAD rename column AKTOER_ID to FK_AKTOER_ID; +alter table GR_PERIODE_OVERGANGSSTONAD + add constraint FK_GR_PERIODE_OVERGANGSSTONAD foreign key (FK_AKTOER_ID) references AKTOER (AKTOER_ID); + +alter table FOEDSELSHENDELSE_PRE_LANSERING rename column AKTOER_ID to FK_AKTOER_ID; +alter table FOEDSELSHENDELSE_PRE_LANSERING + add constraint FK_FOEDSELSHENDELSE_PRE_LANSERING foreign key (FK_AKTOER_ID) references AKTOER (AKTOER_ID); + +alter table PO_PERSON rename column AKTOER_ID to FK_AKTOER_ID; +alter table PO_PERSON + add constraint FK_PO_PERSON foreign key (FK_AKTOER_ID) references AKTOER (AKTOER_ID); + +update fagsak_person fp +set fk_aktoer_id=(select fk_aktoer_id from personident p where p.foedselsnummer = fp.ident); + +update andel_tilkjent_ytelse aty +set fk_aktoer_id=(select fk_aktoer_id from personident p where p.foedselsnummer = aty.person_ident); + +update person_resultat pr +set fk_aktoer_id=(select fk_aktoer_id from personident p where p.foedselsnummer = pr.person_ident); + +update gr_periode_overgangsstonad gpo +set fk_aktoer_id=(select fk_aktoer_id from personident p where p.foedselsnummer = gpo.person_ident); + +update foedselshendelse_pre_lansering fpl +set fk_aktoer_id=(select fk_aktoer_id from personident p where p.foedselsnummer = fpl.person_ident); + +alter table fagsak add column fk_aktoer_id varchar; +alter table FAGSAK + add constraint FAGSAK foreign key (FK_AKTOER_ID) references AKTOER (AKTOER_ID); + +update fagsak f +set fk_aktoer_id=(select fk_aktoer_id + from personident p + where p.foedselsnummer = + (select ident + from fagsak_person fp + where fk_fagsak_id = f.id + and fp.arkivert = false)); + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V171__utdypende_vilkarsvurderinger_opprydding.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V171__utdypende_vilkarsvurderinger_opprydding.sql new file mode 100644 index 000000000..26d32cda7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V171__utdypende_vilkarsvurderinger_opprydding.sql @@ -0,0 +1,7 @@ +ALTER TABLE vilkar_resultat + DROP COLUMN er_skjonnsmessig_vurdert; +ALTER TABLE vilkar_resultat + DROP COLUMN er_medlemskap_vurdert; +ALTER TABLE vilkar_resultat + DROP COLUMN er_delt_bosted; + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V172__opprydning_behandling_1106052.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V172__opprydning_behandling_1106052.sql new file mode 100644 index 000000000..48dfa2576 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V172__opprydning_behandling_1106052.sql @@ -0,0 +1,3 @@ +UPDATE BEHANDLING +SET behandling_type = 'FØRSTEGANGSBEHANDLING' +WHERE id = '1106052'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V173__oppdater_tilkjent_ytelse_stonad_fom_fra_andel_tilkjent_ytelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V173__oppdater_tilkjent_ytelse_stonad_fom_fra_andel_tilkjent_ytelse.sql new file mode 100644 index 000000000..3e9fcfc60 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V173__oppdater_tilkjent_ytelse_stonad_fom_fra_andel_tilkjent_ytelse.sql @@ -0,0 +1,27 @@ +with ny_stonad_tom (tilkjent_ytelse_id, ny_stonad_tom) as ( + select ty.id, max(aty.stonad_tom) + from andel_tilkjent_ytelse aty + join tilkjent_ytelse ty on ty.id = aty.tilkjent_ytelse_id + where ty.stonad_tom < aty.stonad_tom + group by ty.id +) + +update tilkjent_ytelse ty + set stonad_tom = st.ny_stonad_tom + from ny_stonad_tom st + where st.tilkjent_ytelse_id = ty.id +; + +with ny_stonad_fom (tilkjent_ytelse_id, ny_stonad_fom) as ( + select ty.id, min(aty.stonad_fom) + from andel_tilkjent_ytelse aty + join tilkjent_ytelse ty on ty.id = aty.tilkjent_ytelse_id + where ty.stonad_fom > aty.stonad_fom + group by ty.id +) + +update tilkjent_ytelse ty + set stonad_fom = st.ny_stonad_fom + from ny_stonad_fom st + where st.tilkjent_ytelse_id = ty.id +; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V174__kickstart_satsendring_06_01_2022.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V174__kickstart_satsendring_06_01_2022.sql new file mode 100644 index 000000000..29806f83c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V174__kickstart_satsendring_06_01_2022.sql @@ -0,0 +1,13 @@ +DO +$$ + BEGIN + IF NOT EXISTS + (SELECT 1 FROM task WHERE type = 'startsatsendringforallebehandlinger') + THEN + INSERT INTO task(payload, type, status, metadata, versjon, opprettet_tid, trigger_tid) + VALUES ('1654', 'startsatsendringforallebehandlinger', 'UBEHANDLET', + 'callId=startsatsendringforallebehandlinger-06.01.2022', 0, now(), + now()); + END IF; + END +$$; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V175__kickstart_satsendring_07_01_2022.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V175__kickstart_satsendring_07_01_2022.sql new file mode 100644 index 000000000..5e8e617dc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V175__kickstart_satsendring_07_01_2022.sql @@ -0,0 +1,9 @@ +DO +$$ + BEGIN + INSERT INTO task(payload, type, status, metadata, versjon, opprettet_tid, trigger_tid) + VALUES ('1654', 'startsatsendringforallebehandlinger', 'UBEHANDLET', + 'callId=startsatsendringforallebehandlinger-07.01.2022', 1, now(), + now()); + END +$$; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V176__fjern_kolonn_person_ident.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V176__fjern_kolonn_person_ident.sql new file mode 100644 index 000000000..0b6211d92 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V176__fjern_kolonn_person_ident.sql @@ -0,0 +1,16 @@ +drop table FAGSAK_PERSON; + +alter table ANDEL_TILKJENT_YTELSE + drop column PERSON_IDENT; + +alter table PERSON_RESULTAT + drop column PERSON_IDENT; + +alter table GR_PERIODE_OVERGANGSSTONAD + drop column PERSON_IDENT; + +alter table FOEDSELSHENDELSE_PRE_LANSERING + drop column PERSON_IDENT; + +alter table PO_PERSON + drop column PERSON_IDENT; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V177__personident_constraint_foedselsnummerog_aktiv.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V177__personident_constraint_foedselsnummerog_aktiv.sql new file mode 100644 index 000000000..e66701081 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V177__personident_constraint_foedselsnummerog_aktiv.sql @@ -0,0 +1 @@ +create unique index UIDX_PERSONIDENT_FOEDSELSNUMMER_ID ON PERSONIDENT(FOEDSELSNUMMER); \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V178__legg_til_inde_p\303\245_fk.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V178__legg_til_inde_p\303\245_fk.sql" new file mode 100644 index 000000000..b033fbc09 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V178__legg_til_inde_p\303\245_fk.sql" @@ -0,0 +1,24 @@ +create INDEX vilkaarsvurdering_fk_idx ON vilkaarsvurdering(fk_behandling_id); +create INDEX person_resultat_fk_idx ON person_resultat(fk_vilkaarsvurdering_id); +create INDEX oppgave_fk_idx ON oppgave(fk_behandling_id); +create INDEX vilkar_resultat_fk_idx ON vilkar_resultat(fk_behandling_id); +create INDEX po_statsborgerskap_fk_idx ON po_statsborgerskap(fk_po_person_id); +create INDEX po_opphold_fk_idx ON po_opphold(fk_po_person_id); +create INDEX po_arbeidsforhold_fk_idx ON po_arbeidsforhold(fk_po_person_id); +create INDEX po_bostedsadresseperiode_fk_idx ON po_bostedsadresseperiode(fk_po_person_id); +create INDEX behandling_steg_tilstand_fk_idx ON behandling_steg_tilstand(fk_behandling_id); +create INDEX andel_tilkjent_ytelse_fk_idx ON andel_tilkjent_ytelse(kilde_behandling_id); +create INDEX annen_vurdering_fk_idx ON annen_vurdering(fk_person_resultat_id); +create INDEX vilkar_resultat_fk_personr_idx ON vilkar_resultat(fk_person_resultat_id); +create INDEX tilbakekreving_fk_idx ON tilbakekreving(fk_behandling_id); +create INDEX okonomi_simulering_mottaker_fk_idx ON okonomi_simulering_mottaker(fk_behandling_id); +create INDEX po_bostedsadresse_fk_idx ON po_bostedsadresse(fk_po_person_id); +create INDEX po_sivilstand_fk_idx ON po_sivilstand(fk_po_person_id); +create INDEX andel_tilkjent_ytelse_fk_tilkjent_idx ON andel_tilkjent_ytelse(tilkjent_ytelse_id); +create INDEX endret_utbetaling_andel_fk_idx ON endret_utbetaling_andel(fk_po_person_id); +create INDEX andel_tilkjent_ytelse_fk_aktoer_idx ON andel_tilkjent_ytelse(fk_aktoer_id); +create INDEX person_resultat_fk_aktoer_idx ON person_resultat(fk_aktoer_id); +create INDEX gr_periode_overgangsstonad_fk_idx ON gr_periode_overgangsstonad(fk_aktoer_id); +create INDEX foedselshendelse_pre_lansering_fk_idx ON foedselshendelse_pre_lansering(fk_aktoer_id); +create INDEX po_person_fk_idx ON po_person(fk_aktoer_id); +create INDEX fagsak_fk_idx ON fagsak(fk_aktoer_id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V179__korreksjon_av_versjon_satsendring_06_01_2022.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V179__korreksjon_av_versjon_satsendring_06_01_2022.sql new file mode 100644 index 000000000..e16e4e789 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V179__korreksjon_av_versjon_satsendring_06_01_2022.sql @@ -0,0 +1,9 @@ +DO +$$ + BEGIN + UPDATE task + set versjon=1 + WHERE versjon = 0 + AND metadata = 'callId=startsatsendringforallebehandlinger-06.01.2022'; + END +$$; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V17__batch_tabell.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V17__batch_tabell.sql new file mode 100644 index 000000000..2604650cb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V17__batch_tabell.sql @@ -0,0 +1,8 @@ +CREATE TABLE BATCH +( + ID BIGINT PRIMARY KEY, + KJOREDATO TIMESTAMP(3) NOT NULL, + STATUS VARCHAR(50) NOT NULL DEFAULT 'LEDIG' +); + +CREATE SEQUENCE BATCH_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V180__endre_constraint_vedtaksperiode.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V180__endre_constraint_vedtaksperiode.sql new file mode 100644 index 000000000..b4a5b4371 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V180__endre_constraint_vedtaksperiode.sql @@ -0,0 +1,2 @@ +ALTER TABLE VEDTAKSPERIODE + DROP CONSTRAINT "vedtaksperiode_fk_vedtak_id_fom_tom_type_key"; \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V181__legg_til_index_p\303\245_personident.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V181__legg_til_index_p\303\245_personident.sql" new file mode 100644 index 000000000..958ed5db8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V181__legg_til_index_p\303\245_personident.sql" @@ -0,0 +1 @@ +create INDEX personident_aktoer_id_alle_idx ON personident (fk_aktoer_id); diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V182__opprett_tabell_sett_paa_vent.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V182__opprett_tabell_sett_paa_vent.sql new file mode 100644 index 000000000..7468f1825 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V182__opprett_tabell_sett_paa_vent.sql @@ -0,0 +1,22 @@ +create TABLE sett_paa_vent +( + id bigint PRIMARY KEY, + fk_behandling_id bigint REFERENCES BEHANDLING (id) NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + frist TIMESTAMP(3) NOT NULL, + aktiv BOOLEAN DEFAULT FALSE NOT NULL, + aarsak VARCHAR NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +create sequence sett_paa_vent_seq increment by 50 start with 1000000 NO CYCLE; + +create INDEX sett_paa_vent_fk_behandling_id_idx ON sett_paa_vent(fk_behandling_id); + +create UNIQUE INDEX uidx_sett_paa_vent_aktiv ON sett_paa_vent(fk_behandling_id, aktiv) + WHERE AKTIV = true; + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V183__migrerte_behandlinger_sett_behandlingstema.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V183__migrerte_behandlinger_sett_behandlingstema.sql new file mode 100644 index 000000000..fef0b8745 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V183__migrerte_behandlinger_sett_behandlingstema.sql @@ -0,0 +1,7 @@ +update behandling b +set underkategori = 'UTVIDET' +FROM andel_tilkjent_ytelse aty +WHERE aty.fk_behandling_id = b.id AND + aty.type = 'UTVIDET_BARNETRYGD' AND + b.underkategori = 'ORDINÆR' and + b.opprettet_aarsak = 'HELMANUELL_MIGRERING'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V184__opprett_tabell_po_doedsfall.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V184__opprett_tabell_po_doedsfall.sql new file mode 100644 index 000000000..fb245ac71 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V184__opprett_tabell_po_doedsfall.sql @@ -0,0 +1,17 @@ +create TABLE po_doedsfall +( + id bigint PRIMARY KEY, + fk_po_person_id bigint REFERENCES PO_PERSON (id) NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + doedsfall_dato TIMESTAMP(3) NOT NULL, + doedsfall_adresse VARCHAR DEFAULT null, + doedsfall_postnummer VARCHAR DEFAULT null, + doedsfall_poststed VARCHAR DEFAULT null +); + +create sequence po_doedsfall_seq increment by 50 start with 1000000 NO CYCLE; + +create INDEX po_doedsfall_fk_po_person_id_idx ON po_doedsfall(fk_po_person_id); + + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V185__legg_kolonner_til_sett_paa_vent.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V185__legg_kolonner_til_sett_paa_vent.sql new file mode 100644 index 000000000..84a57bf43 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V185__legg_kolonner_til_sett_paa_vent.sql @@ -0,0 +1,7 @@ +ALTER TABLE sett_paa_vent + ADD COLUMN tid_tatt_av_vent TIMESTAMP(3), + ADD COLUMN tid_satt_paa_vent TIMESTAMP(3) NOT NULL DEFAULT now(); + + + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V186__legg_kolonner_til_po_doedsfall.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V186__legg_kolonner_til_po_doedsfall.sql new file mode 100644 index 000000000..56cd939a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V186__legg_kolonner_til_po_doedsfall.sql @@ -0,0 +1,5 @@ +ALTER TABLE po_doedsfall + ADD COLUMN opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + ADD COLUMN opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ADD COLUMN endret_av VARCHAR, + ADD COLUMN endret_tid TIMESTAMP(3); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V187__skyggesak.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V187__skyggesak.sql new file mode 100644 index 000000000..cb326bf31 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V187__skyggesak.sql @@ -0,0 +1,9 @@ +CREATE TABLE SKYGGESAK +( + ID BIGINT NOT NULL PRIMARY KEY, + FK_FAGSAK_ID BIGINT NOT NULL, + SENDT_TID TIMESTAMP(3) +); + +CREATE SEQUENCE SKYGGESAK_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX skyggesak_fagsak_id_idx ON skyggesak (fk_fagsak_id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V188__opprett_skyggesaker.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V188__opprett_skyggesaker.sql new file mode 100644 index 000000000..8dd2ef6c9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V188__opprett_skyggesaker.sql @@ -0,0 +1,5 @@ +INSERT INTO skyggesak(id, fk_fagsak_id) +select nextval('skyggesak_seq'), id +from fagsak +where arkivert = false + AND status in ('LØPENDE', 'OPPRETTET'); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V189__behandling_migreringsinfo.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V189__behandling_migreringsinfo.sql new file mode 100644 index 000000000..a939da505 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V189__behandling_migreringsinfo.sql @@ -0,0 +1,14 @@ +CREATE TABLE BEHANDLING_MIGRERINGSINFO +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT REFERENCES BEHANDLING (ID) NOT NULL, + MIGRERINGSDATO DATE NOT NULL, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE BEHANDLING_MIGRERINGSINFO_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON BEHANDLING_MIGRERINGSINFO (FK_BEHANDLING_ID); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V18__samlet_vilkaar_resultat_behandling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V18__samlet_vilkaar_resultat_behandling.sql new file mode 100644 index 000000000..209783a72 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V18__samlet_vilkaar_resultat_behandling.sql @@ -0,0 +1,7 @@ +alter table SAMLET_VILKAR_RESULTAT + add column fk_behandling_id bigint references BEHANDLING (id); + +alter table SAMLET_VILKAR_RESULTAT + add column aktiv boolean default false; + +alter table BEHANDLING drop column samlet_vilkar_resultat_id; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V190__index_saksstatistikkmellomlagring.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V190__index_saksstatistikkmellomlagring.sql new file mode 100644 index 000000000..434b66cc2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V190__index_saksstatistikkmellomlagring.sql @@ -0,0 +1 @@ +create index saksstatistikk_mellomlagring_sendt_tid_null_idx on saksstatistikk_mellomlagring (sendt_tid) where sendt_tid is null; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V191__opprett_tabell_data_chunk.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V191__opprett_tabell_data_chunk.sql new file mode 100644 index 000000000..aa9d212cf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V191__opprett_tabell_data_chunk.sql @@ -0,0 +1,18 @@ +create TABLE data_chunk +( + id BIGINT PRIMARY KEY, + fk_batch_id bigint REFERENCES BATCH (id) NOT NULL, + transaksjons_id UUID NOT NULL, + chunk_nr BIGINT NOT NULL, + er_sendt BOOLEAN NOT NULL, + versjon BIGINT DEFAULT 0 NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +create sequence data_chunk_seq increment by 50 start with 1000000 NO CYCLE; + +create INDEX data_chunk_transaksjons_id_chunk_nr_idx ON data_chunk(transaksjons_id, chunk_nr); +create INDEX data_chunk_transaksjons_id_idx ON data_chunk(transaksjons_id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V192__delete_begrunnelse_opphor_ikke_mottat_opplysninger.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V192__delete_begrunnelse_opphor_ikke_mottat_opplysninger.sql new file mode 100644 index 000000000..fbbaad5a3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V192__delete_begrunnelse_opphor_ikke_mottat_opplysninger.sql @@ -0,0 +1,2 @@ +delete from vedtaksbegrunnelse +where vedtak_begrunnelse_spesifikasjon = 'OPPHØR_IKKE_MOTTATT_OPPLYSNINGER'; \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V193__behandling_s\303\270knadsinfo.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V193__behandling_s\303\270knadsinfo.sql" new file mode 100644 index 000000000..a32c79457 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V193__behandling_s\303\270knadsinfo.sql" @@ -0,0 +1,14 @@ +CREATE TABLE BEHANDLING_SOKNADSINFO +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT REFERENCES BEHANDLING (ID) NOT NULL, + MOTTATT_DATO TIMESTAMP(3) NOT NULL, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE BEHANDLING_SOKNADSINFO_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON BEHANDLING_SOKNADSINFO (FK_BEHANDLING_ID); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V194__unik_behandling_i_behandling_migreringsinfo.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V194__unik_behandling_i_behandling_migreringsinfo.sql new file mode 100644 index 000000000..ee7367dd9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V194__unik_behandling_i_behandling_migreringsinfo.sql @@ -0,0 +1,7 @@ +DELETE +FROM BEHANDLING_MIGRERINGSINFO +WHERE ID IN (1082590, 1082593); +COMMIT; + +ALTER TABLE BEHANDLING_MIGRERINGSINFO + ADD CONSTRAINT UNIK_BEHANDLING_ID UNIQUE (FK_BEHANDLING_ID); \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V195__set_fagsaker_til_l\303\270pende.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V195__set_fagsaker_til_l\303\270pende.sql" new file mode 100644 index 000000000..4c1fae944 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V195__set_fagsaker_til_l\303\270pende.sql" @@ -0,0 +1,3 @@ +update fagsak +set status = 'LØPENDE' +where ID in (1049294, 1057632, 1064151, 1070301, 1071501, 1072057, 1073351) \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V196__migrerte_behandlinger_sett_behandlingstema.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V196__migrerte_behandlinger_sett_behandlingstema.sql new file mode 100644 index 000000000..89a885af1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V196__migrerte_behandlinger_sett_behandlingstema.sql @@ -0,0 +1,7 @@ +update behandling b +set underkategori = 'UTVIDET' +FROM andel_tilkjent_ytelse aty +WHERE aty.fk_behandling_id = b.id AND + aty.type = 'UTVIDET_BARNETRYGD' AND + b.underkategori = 'ORDINÆR' and + b.opprettet_aarsak = 'ENDRE_MIGRERINGSDATO'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V197__fjern_personidenter_fra_vedtaksbegrunnelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V197__fjern_personidenter_fra_vedtaksbegrunnelse.sql new file mode 100644 index 000000000..f28dd5ad4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V197__fjern_personidenter_fra_vedtaksbegrunnelse.sql @@ -0,0 +1,2 @@ +ALTER TABLE vedtaksbegrunnelse + DROP COLUMN person_identer; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V198__kompetanse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V198__kompetanse.sql new file mode 100644 index 000000000..a96164500 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V198__kompetanse.sql @@ -0,0 +1,22 @@ +CREATE TABLE KOMPETANSE +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT REFERENCES BEHANDLING (ID) NOT NULL, + FOM TIMESTAMP(3), + TOM TIMESTAMP(3), + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE KOMPETANSE_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX KOMPETANSE_FK_BEHANDLING_ID_IDX ON KOMPETANSE (FK_BEHANDLING_ID); + +CREATE TABLE AKTOER_TIL_KOMPETANSE +( + FK_KOMPETANSE_ID BIGINT REFERENCES KOMPETANSE (ID) NOT NULL, + FK_AKTOER_ID VARCHAR REFERENCES AKTOER (AKTOER_ID) NOT NULL, + PRIMARY KEY (FK_KOMPETANSE_ID, FK_AKTOER_ID) +); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V199__endre_enum_vedtaksperiode_reduksjon_til_utbetaling_med_redukjson_.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V199__endre_enum_vedtaksperiode_reduksjon_til_utbetaling_med_redukjson_.sql new file mode 100644 index 000000000..93cb41968 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V199__endre_enum_vedtaksperiode_reduksjon_til_utbetaling_med_redukjson_.sql @@ -0,0 +1,3 @@ +UPDATE vedtaksperiode +SET type='UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING' +WHERE type = 'REDUKSJON'; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V19__vedtak_begrunnelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V19__vedtak_begrunnelse.sql new file mode 100644 index 000000000..891731cdc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V19__vedtak_begrunnelse.sql @@ -0,0 +1 @@ +alter table vedtak add column begrunnelse TEXT; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V200__behandlingsmetadata_.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V200__behandlingsmetadata_.sql new file mode 100644 index 000000000..f1fb08bef --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V200__behandlingsmetadata_.sql @@ -0,0 +1,2 @@ +ALTER TABLE BEHANDLING + ADD COLUMN overstyrt_endringstidspunkt TIMESTAMP(3); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V201__sakkstatistikk_mellomlagring_index.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V201__sakkstatistikk_mellomlagring_index.sql new file mode 100644 index 000000000..3f57e3426 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V201__sakkstatistikk_mellomlagring_index.sql @@ -0,0 +1 @@ +create index saksstatistikk_mellomlagring_type_id_idx on saksstatistikk_mellomlagring (type_id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V202__legg_paa_kolonner_for_kompetanseskjema.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V202__legg_paa_kolonner_for_kompetanseskjema.sql new file mode 100644 index 000000000..af4baaed5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V202__legg_paa_kolonner_for_kompetanseskjema.sql @@ -0,0 +1,9 @@ +ALTER TABLE kompetanse + ADD COLUMN soekers_aktivitet VARCHAR DEFAULT null, + ADD COLUMN annen_forelderes_aktivitet VARCHAR DEFAULT null, + ADD COLUMN annen_forelderes_aktivitetsland VARCHAR DEFAULT null, + ADD COLUMN barnets_bostedsland VARCHAR DEFAULT null, + ADD COLUMN resultat VARCHAR DEFAULT null; + + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V203__oppdater_tilkjent_ytelse_stonad_tom_fra_andel_tilkjent_ytelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V203__oppdater_tilkjent_ytelse_stonad_tom_fra_andel_tilkjent_ytelse.sql new file mode 100644 index 000000000..d7edd9620 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V203__oppdater_tilkjent_ytelse_stonad_tom_fra_andel_tilkjent_ytelse.sql @@ -0,0 +1,14 @@ +with ny_stonad_tom (tilkjent_ytelse_id, ny_stonad_tom) as ( + select ty.id, + grouped_aty.max_stonad_tom + from (select tilkjent_ytelse_id, MAX(stonad_tom) as max_stonad_tom + from andel_tilkjent_ytelse + group by tilkjent_ytelse_id) grouped_aty + join tilkjent_ytelse ty on grouped_aty.tilkjent_ytelse_id = ty.id + where grouped_aty.max_stonad_tom != ty.stonad_tom +) + +update tilkjent_ytelse ty +set stonad_tom = st.ny_stonad_tom +from ny_stonad_tom st +where st.tilkjent_ytelse_id = ty.id; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V204__oppdater_behandlingsresultat_endret_til_endret_utbetaling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V204__oppdater_behandlingsresultat_endret_til_endret_utbetaling.sql new file mode 100644 index 000000000..dfde99bdf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V204__oppdater_behandlingsresultat_endret_til_endret_utbetaling.sql @@ -0,0 +1,3 @@ +UPDATE BEHANDLING +SET resultat='ENDRET_UTBETALING' +WHERE resultat = 'ENDRET'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V205__index_tilkjent_ytelse_andel_tilkjent_ytelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V205__index_tilkjent_ytelse_andel_tilkjent_ytelse.sql new file mode 100644 index 000000000..4f1d40b87 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V205__index_tilkjent_ytelse_andel_tilkjent_ytelse.sql @@ -0,0 +1,2 @@ +create index tilkjent_ytelse_utbetalingsoppdrag_not_null_idx on tilkjent_ytelse (utbetalingsoppdrag) where utbetalingsoppdrag is not null; +create index aty_type_idx on andel_tilkjent_ytelse (type); diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V206__valutakurs.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V206__valutakurs.sql new file mode 100644 index 000000000..0494f3d17 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V206__valutakurs.sql @@ -0,0 +1,25 @@ +CREATE TABLE VALUTAKURS +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT REFERENCES BEHANDLING (ID) NOT NULL, + FOM TIMESTAMP(3), + TOM TIMESTAMP(3), + VALUTAKURSDATO TIMESTAMP(3) DEFAULT null, + VALUTAKODE VARCHAR DEFAULT null, + KURS DECIMAL DEFAULT null, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE VALUTAKURS_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX VALUTAKURS_FK_BEHANDLING_ID_IDX ON VALUTAKURS (FK_BEHANDLING_ID); + +CREATE TABLE AKTOER_TIL_VALUTAKURS +( + FK_VALUTAKURS_ID BIGINT REFERENCES VALUTAKURS (ID) NOT NULL, + FK_AKTOER_ID VARCHAR REFERENCES AKTOER (AKTOER_ID) NOT NULL, + PRIMARY KEY (FK_VALUTAKURS_ID, FK_AKTOER_ID) +); \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V207__utenlandsk_periodebel\303\270p.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V207__utenlandsk_periodebel\303\270p.sql" new file mode 100644 index 000000000..3c08273ca --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V207__utenlandsk_periodebel\303\270p.sql" @@ -0,0 +1,25 @@ +CREATE TABLE UTENLANDSK_PERIODEBELOEP +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT REFERENCES BEHANDLING (ID) NOT NULL, + FOM TIMESTAMP(3), + TOM TIMESTAMP(3), + INTERVALL VARCHAR DEFAULT null, + VALUTAKODE VARCHAR DEFAULT null, + BELOEP DECIMAL DEFAULT null, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE UTENLANDSK_PERIODEBELOEP_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX UTENLANDSK_PERIODEBELOEP_FK_BEHANDLING_ID_IDX ON UTENLANDSK_PERIODEBELOEP (FK_BEHANDLING_ID); + +CREATE TABLE AKTOER_TIL_UTENLANDSK_PERIODEBELOEP +( + FK_UTENLANDSK_PERIODEBELOEP_ID BIGINT REFERENCES UTENLANDSK_PERIODEBELOEP (ID) NOT NULL, + FK_AKTOER_ID VARCHAR REFERENCES AKTOER (AKTOER_ID) NOT NULL, + PRIMARY KEY (FK_UTENLANDSK_PERIODEBELOEP_ID, FK_AKTOER_ID) +); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V208__legg_til_eos_begrunnelse_tabell.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V208__legg_til_eos_begrunnelse_tabell.sql new file mode 100644 index 000000000..550a81b34 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V208__legg_til_eos_begrunnelse_tabell.sql @@ -0,0 +1,11 @@ +CREATE TABLE eos_begrunnelse +( + id BIGINT NOT NULL PRIMARY KEY, + fk_vedtaksperiode_id BIGINT REFERENCES vedtaksperiode ON DELETE CASCADE, + begrunnelse VARCHAR NOT NULL +); + +CREATE INDEX eos_begrunnelse_fk_vedtaksperiode_id_idx + ON eos_begrunnelse (fk_vedtaksperiode_id); + +CREATE SEQUENCE eos_begrunnelse_seq INCREMENT BY 50 START WITH 1000000 NO CYCLE; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V209__fagsak_eier.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V209__fagsak_eier.sql new file mode 100644 index 000000000..50c9f731d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V209__fagsak_eier.sql @@ -0,0 +1,4 @@ +ALTER TABLE FAGSAK ADD COLUMN eier VARCHAR(50) DEFAULT 'OMSORGSPERSON' NOT NULL; + +CREATE UNIQUE INDEX uidx_fagsak_eier_aktoer_ikke_arkivert ON fagsak(eier, fk_aktoer_id) + WHERE arkivert = false; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V20__typo_kategori.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V20__typo_kategori.sql new file mode 100644 index 000000000..9cda42184 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V20__typo_kategori.sql @@ -0,0 +1 @@ +update behandling set kategori = 'NASJONAL' where kategori = 'NATIONAL'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V210__upb_utbetalingsland.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V210__upb_utbetalingsland.sql new file mode 100644 index 000000000..0eef5e156 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V210__upb_utbetalingsland.sql @@ -0,0 +1 @@ +ALTER TABLE utenlandsk_periodebeloep ADD COLUMN utbetalingsland VARCHAR; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V211__kolonner_for_differanseberegning_paa_andel_tilkjent_ytelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V211__kolonner_for_differanseberegning_paa_andel_tilkjent_ytelse.sql new file mode 100644 index 000000000..9cc56bc64 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V211__kolonner_for_differanseberegning_paa_andel_tilkjent_ytelse.sql @@ -0,0 +1,3 @@ +ALTER TABLE andel_tilkjent_ytelse + ADD COLUMN nasjonalt_periodebelop numeric, + ADD COLUMN differanseberegnet_periodebelop numeric diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V212__aktoerId_splitt_update_cascade.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V212__aktoerId_splitt_update_cascade.sql new file mode 100644 index 000000000..ec23b5d22 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V212__aktoerId_splitt_update_cascade.sql @@ -0,0 +1,69 @@ +-- personident +ALTER TABLE personident DROP CONSTRAINT fk_personident; + +ALTER TABLE personident + ADD CONSTRAINT fk_personident + FOREIGN KEY (fk_aktoer_id) references aktoer ON UPDATE CASCADE; + +-- person_resultat +ALTER TABLE person_resultat DROP CONSTRAINT fk_person_resultat; + +ALTER TABLE person_resultat + ADD CONSTRAINT fk_person_resultat + FOREIGN KEY (fk_aktoer_id) references aktoer ON UPDATE CASCADE; + +-- po_person +ALTER TABLE po_person DROP CONSTRAINT fk_po_person; + +ALTER TABLE po_person + ADD CONSTRAINT fk_po_person + FOREIGN KEY (fk_aktoer_id) references aktoer ON UPDATE CASCADE; + +-- fagsak +ALTER TABLE fagsak DROP CONSTRAINT fagsak; + +ALTER TABLE fagsak + ADD CONSTRAINT fagsak + FOREIGN KEY (fk_aktoer_id) references aktoer ON UPDATE CASCADE; + +-- andel_tilkjent_ytelse +ALTER TABLE andel_tilkjent_ytelse DROP CONSTRAINT fk_andel_tilkjent_ytelse; + +ALTER TABLE andel_tilkjent_ytelse + ADD CONSTRAINT fk_andel_tilkjent_ytelse + FOREIGN KEY (fk_aktoer_id) references aktoer ON UPDATE CASCADE; + +-- foedselshendelse_pre_lansering +ALTER TABLE foedselshendelse_pre_lansering DROP CONSTRAINT fk_foedselshendelse_pre_lansering; + +ALTER TABLE foedselshendelse_pre_lansering + ADD CONSTRAINT fk_foedselshendelse_pre_lansering + FOREIGN KEY (fk_aktoer_id) references aktoer ON UPDATE CASCADE; + +-- gr_periode_overgangsstonad +ALTER TABLE gr_periode_overgangsstonad DROP CONSTRAINT fk_gr_periode_overgangsstonad; + +ALTER TABLE gr_periode_overgangsstonad + ADD CONSTRAINT fk_gr_periode_overgangsstonad + FOREIGN KEY (fk_aktoer_id) references aktoer ON UPDATE CASCADE; + +-- aktoer_til_kompetanse +ALTER TABLE aktoer_til_kompetanse DROP CONSTRAINT aktoer_til_kompetanse_fk_aktoer_id_fkey; + +ALTER TABLE aktoer_til_kompetanse + ADD CONSTRAINT aktoer_til_kompetanse_fk_aktoer_id_fkey + FOREIGN KEY (fk_aktoer_id) references aktoer ON UPDATE CASCADE; + +-- aktoer_til_utenlandsk_periodebeloep +ALTER TABLE aktoer_til_utenlandsk_periodebeloep DROP CONSTRAINT aktoer_til_utenlandsk_periodebeloep_fk_aktoer_id_fkey; + +ALTER TABLE aktoer_til_utenlandsk_periodebeloep + ADD CONSTRAINT aktoer_til_utenlandsk_periodebeloep_fk_aktoer_id_fkey + FOREIGN KEY (fk_aktoer_id) references aktoer ON UPDATE CASCADE; + +-- aktoer_til_valutakurs +ALTER TABLE aktoer_til_valutakurs DROP CONSTRAINT aktoer_til_valutakurs_fk_aktoer_id_fkey; + +ALTER TABLE aktoer_til_valutakurs + ADD CONSTRAINT aktoer_til_valutakurs_fk_aktoer_id_fkey + FOREIGN KEY (fk_aktoer_id) references aktoer ON UPDATE CASCADE; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V213__kalkulert_maanedlig_belop_upb.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V213__kalkulert_maanedlig_belop_upb.sql new file mode 100644 index 000000000..711efcbde --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V213__kalkulert_maanedlig_belop_upb.sql @@ -0,0 +1 @@ +ALTER TABLE utenlandsk_periodebeloep ADD COLUMN kalkulert_maanedlig_beloep DECIMAL; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V214__institusjon.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V214__institusjon.sql new file mode 100644 index 000000000..bd7e21190 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V214__institusjon.sql @@ -0,0 +1,15 @@ +CREATE TABLE institusjon ( + id BIGINT PRIMARY KEY, + org_nummer VARCHAR, + tss_ekstern_id VARCHAR NOT NULL, + versjon BIGINT DEFAULT 0 NOT NULL, + opprettet_av VARCHAR(20) DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av VARCHAR(20), + endret_tid TIMESTAMP(3) +); + +CREATE SEQUENCE institusjon_seq INCREMENT BY 50 START WITH 1000000 NO CYCLE; + +CREATE UNIQUE INDEX uidx_institusjon_org_nummer ON institusjon (org_nummer); +CREATE UNIQUE INDEX uidx_institusjon_tss_ekstern_id ON institusjon (tss_ekstern_id) \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V215__fagsak_type.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V215__fagsak_type.sql new file mode 100644 index 000000000..55ad56283 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V215__fagsak_type.sql @@ -0,0 +1,14 @@ +ALTER TABLE FAGSAK ADD COLUMN type VARCHAR(50) DEFAULT 'NORMAL' NOT NULL; +ALTER TABLE FAGSAK ADD COLUMN fk_institusjon_id BIGINT; +ALTER TABLE FAGSAK + ADD FOREIGN KEY (fk_institusjon_id) REFERENCES INSTITUSJON (ID); + +UPDATE FAGSAK SET type = 'BARN_ENSLIG_MINDREÅRLIG' WHERE eier = 'BARN'; + +CREATE UNIQUE INDEX uidx_fagsak_type_aktoer_institusjon_ikke_arkivert ON fagsak(type, fk_aktoer_id, fk_institusjon_id) + WHERE fagsak.fk_institusjon_id IS NOT NULL + AND arkivert = false; + +CREATE UNIQUE INDEX uidx_fagsak_type_aktoer_ikke_arkivert ON fagsak(type, fk_aktoer_id) + WHERE fagsak.fk_institusjon_id IS NULL + AND arkivert = false; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V216__verge.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V216__verge.sql new file mode 100644 index 000000000..a6c8f1470 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V216__verge.sql @@ -0,0 +1,18 @@ +CREATE TABLE verge ( + id BIGINT PRIMARY KEY, + navn VARCHAR NOT NULL, + adresse VARCHAR NOT NULL, + ident VARCHAR, + fk_behandling_id BIGINT NOT NULL REFERENCES behandling (id), + versjon BIGINT DEFAULT 0 NOT NULL, + opprettet_av VARCHAR(20) DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av VARCHAR(20), + endret_tid TIMESTAMP(3) +); + +CREATE SEQUENCE verge_seq INCREMENT BY 50 START WITH 1000000 NO CYCLE; + +CREATE UNIQUE INDEX uidx_verge_navn ON verge (navn); +CREATE UNIQUE INDEX uidx_verge_ident ON verge (ident); +CREATE UNIQUE INDEX uidx_verge_behandling_id ON verge (fk_behandling_id) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V217__fikser_skrivefeil_i_fagsak_type.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V217__fikser_skrivefeil_i_fagsak_type.sql new file mode 100644 index 000000000..acf47565f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V217__fikser_skrivefeil_i_fagsak_type.sql @@ -0,0 +1 @@ +UPDATE FAGSAK SET type = 'BARN_ENSLIG_MINDREÅRIG' WHERE type = 'BARN_ENSLIG_MINDREÅRLIG'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V218__fjerner_fagsak_eier_index.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V218__fjerner_fagsak_eier_index.sql new file mode 100644 index 000000000..e40408bb6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V218__fjerner_fagsak_eier_index.sql @@ -0,0 +1 @@ +DROP INDEX uidx_fagsak_eier_aktoer_ikke_arkivert diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V219__fjerner_fagsak_eier.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V219__fjerner_fagsak_eier.sql new file mode 100644 index 000000000..979be6a23 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V219__fjerner_fagsak_eier.sql @@ -0,0 +1,3 @@ +alter table fagsak +drop column eier; + diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V21__opph\303\270r_vedtak.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V21__opph\303\270r_vedtak.sql" new file mode 100644 index 000000000..244dee77e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V21__opph\303\270r_vedtak.sql" @@ -0,0 +1,2 @@ +alter table VEDTAK add column fk_forrige_vedtak_id bigint references VEDTAK default null; +alter table VEDTAK add column opphor_dato TIMESTAMP(3) default null; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V220__korrigert_etterbetaling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V220__korrigert_etterbetaling.sql new file mode 100644 index 000000000..fffd1a4ff --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V220__korrigert_etterbetaling.sql @@ -0,0 +1,20 @@ +CREATE TABLE KORRIGERT_ETTERBETALING +( + ID BIGINT PRIMARY KEY, + AARSAK VARCHAR NOT NULL, + BEGRUNNELSE VARCHAR, + BELOP BIGINT NOT NULL, + AKTIV BOOLEAN NOT NULL, + FK_BEHANDLING_ID BIGINT REFERENCES BEHANDLING (ID) NOT NULL, + + -- Base entitet felter + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE korrigert_etterbetaling_seq INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE UNIQUE INDEX UIDX_KORRIGERT_ETTERBETALING_FK_BEHANDLING_ID_AKTIV ON KORRIGERT_ETTERBETALING (FK_BEHANDLING_ID) where AKTIV=true; +CREATE INDEX on KORRIGERT_ETTERBETALING (fk_behandling_id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V221__drop_verge_navn_adresse_og_unique_index_verge_ident.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V221__drop_verge_navn_adresse_og_unique_index_verge_ident.sql new file mode 100644 index 000000000..2c2a7e0d6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V221__drop_verge_navn_adresse_og_unique_index_verge_ident.sql @@ -0,0 +1,4 @@ +DROP INDEX uidx_verge_navn; +DROP INDEX uidx_verge_ident; +ALTER TABLE verge DROP COLUMN navn; +Alter TABLE verge DROP COLUMN adresse; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V222__sokers_aktivitetsland_kolonne_paa_kompetanse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V222__sokers_aktivitetsland_kolonne_paa_kompetanse.sql new file mode 100644 index 000000000..ab7a3a15c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V222__sokers_aktivitetsland_kolonne_paa_kompetanse.sql @@ -0,0 +1,2 @@ +ALTER TABLE kompetanse + ADD COLUMN sokers_aktivitetsland TEXT diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V223__sokers_aktivitet_endre_enumverdier.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V223__sokers_aktivitet_endre_enumverdier.sql new file mode 100644 index 000000000..c7e034c43 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V223__sokers_aktivitet_endre_enumverdier.sql @@ -0,0 +1,40 @@ +UPDATE kompetanse +SET sokers_aktivitetsland ='NO' +WHERE soekers_aktivitet = 'ARBEIDER_I_NORGE' + and sokers_aktivitetsland is Null; + +UPDATE kompetanse +SET sokers_aktivitetsland ='NO' +WHERE soekers_aktivitet = 'SELVSTENDIG_NÆRINGSDRIVENDE' + and sokers_aktivitetsland is Null; + +UPDATE kompetanse +SET sokers_aktivitetsland ='NO' +WHERE soekers_aktivitet = 'MOTTAR_UTBETALING_FRA_NAV_SOM_ERSTATTER_LØNN' + and sokers_aktivitetsland is Null; + +UPDATE kompetanse +SET sokers_aktivitetsland ='NO' +WHERE soekers_aktivitet = 'MOTTAR_UFØRETRYGD_FRA_NORGE' + and sokers_aktivitetsland is Null; + +UPDATE kompetanse +SET sokers_aktivitetsland ='NO' +WHERE soekers_aktivitet = 'MOTTAR_PENSJON_FRA_NORGE' + and sokers_aktivitetsland is Null; + +UPDATE kompetanse +SET soekers_aktivitet='ARBEIDER' +WHERE soekers_aktivitet = 'ARBEIDER_I_NORGE'; + +UPDATE kompetanse +SET soekers_aktivitet='MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN' +WHERE soekers_aktivitet = 'MOTTAR_UTBETALING_FRA_NAV_SOM_ERSTATTER_LØNN'; + +UPDATE kompetanse +SET soekers_aktivitet='MOTTAR_UFØRETRYGD' +WHERE soekers_aktivitet = 'MOTTAR_UFØRETRYGD_FRA_NORGE'; + +UPDATE kompetanse +SET soekers_aktivitet='MOTTAR_PENSJON' +WHERE soekers_aktivitet = 'MOTTAR_PENSJON_FRA_NORGE'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V224__notnull_institusjon_schema.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V224__notnull_institusjon_schema.sql new file mode 100644 index 000000000..60ecfe017 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V224__notnull_institusjon_schema.sql @@ -0,0 +1,4 @@ +alter table institusjon + alter column tss_ekstern_id drop not null; +alter table institusjon + alter column org_nummer set not null; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V225__sokers_aktivitet_endre_enumverdier_paa_nytt.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V225__sokers_aktivitet_endre_enumverdier_paa_nytt.sql new file mode 100644 index 000000000..174b591ef --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V225__sokers_aktivitet_endre_enumverdier_paa_nytt.sql @@ -0,0 +1,15 @@ +UPDATE kompetanse +SET soekers_aktivitet = 'ARBEIDER' +WHERE soekers_aktivitet = 'ARBEIDER_I_NORGE'; + +UPDATE kompetanse +SET soekers_aktivitet ='MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN' +WHERE soekers_aktivitet = 'MOTTAR_UTBETALING_FRA_NAV_SOM_ERSTATTER_LØNN'; + +UPDATE kompetanse +SET soekers_aktivitet = 'MOTTAR_UFØRETRYGD' +WHERE soekers_aktivitet = 'MOTTAR_UFØRETRYGD_FRA_NORGE'; + +UPDATE kompetanse +SET soekers_aktivitet ='MOTTAR_PENSJON' +WHERE soekers_aktivitet = 'MOTTAR_PENSJON_FRA_NORGE'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V226__vurderes_etter_null_for_utvidet.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V226__vurderes_etter_null_for_utvidet.sql new file mode 100644 index 000000000..09c465a53 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V226__vurderes_etter_null_for_utvidet.sql @@ -0,0 +1,3 @@ +update vilkar_resultat +set vurderes_etter = null +where vilkar = 'UTVIDET_BARNETRYGD'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V227__offset_aiven_sakstatistikk_mellomlagring.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V227__offset_aiven_sakstatistikk_mellomlagring.sql new file mode 100644 index 000000000..9cef71637 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V227__offset_aiven_sakstatistikk_mellomlagring.sql @@ -0,0 +1,2 @@ +ALTER TABLE SAKSSTATISTIKK_MELLOMLAGRING + ADD COLUMN offset_aiven BIGINT DEFAULT null; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V228__index_konsistensavstemming.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V228__index_konsistensavstemming.sql new file mode 100644 index 000000000..583f73410 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V228__index_konsistensavstemming.sql @@ -0,0 +1,2 @@ +create index fagsak_status_idx on fagsak (status); +create index behandling_opprettet_tid_idx on behandling (opprettet_tid); diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V229__konsistensavstemming_datoer_2023.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V229__konsistensavstemming_datoer_2023.sql new file mode 100644 index 000000000..dfb5b27a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V229__konsistensavstemming_datoer_2023.sql @@ -0,0 +1,13 @@ +INSERT INTO batch (id, kjoredato) +VALUES (nextval('batch_seq'), TO_TIMESTAMP('05-01-2023', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('30-01-2023', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('27-02-2023', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('28-03-2023', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('25-04-2023', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('30-05-2023', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('29-06-2023', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('28-07-2023', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('30-08-2023', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('29-09-2023', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('30-10-2023', 'DD-MM-YYYY SS:MS')), + (nextval('batch_seq'), TO_TIMESTAMP('22-11-2023', 'DD-MM-YYYY SS:MS')); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V22__vedtak_barn_til_vedtak_person.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V22__vedtak_barn_til_vedtak_person.sql new file mode 100644 index 000000000..5a28435ad --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V22__vedtak_barn_til_vedtak_person.sql @@ -0,0 +1,6 @@ +alter table VEDTAK_BARN rename to VEDTAK_PERSON; + +alter table VEDTAK_PERSON add column type varchar(50) default 'ORDINÆR_BARNETRYGD'; + +alter sequence VEDTAK_BARN_SEQ RENAME TO VEDTAK_PERSON_SEQ; + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V230__korrigert_vedtak.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V230__korrigert_vedtak.sql new file mode 100644 index 000000000..8c8696001 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V230__korrigert_vedtak.sql @@ -0,0 +1,19 @@ +CREATE TABLE KORRIGERT_VEDTAK +( + ID BIGINT PRIMARY KEY, + BEGRUNNELSE VARCHAR, + VEDTAKSDATO TIMESTAMP(3) DEFAULT null, + AKTIV BOOLEAN NOT NULL, + FK_BEHANDLING_ID BIGINT REFERENCES BEHANDLING (ID) NOT NULL, + + -- Base entitet felter + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE korrigert_vedtak_seq INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE UNIQUE INDEX UIDX_KORRIGERT_VEDTAK_FK_BEHANDLING_ID_AKTIV ON KORRIGERT_VEDTAK (FK_BEHANDLING_ID) where AKTIV=true; +CREATE INDEX on KORRIGERT_VEDTAK (fk_behandling_id); diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V231__opprett_trekk_i_loepende_utbetaling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V231__opprett_trekk_i_loepende_utbetaling.sql new file mode 100644 index 000000000..8770f43b3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V231__opprett_trekk_i_loepende_utbetaling.sql @@ -0,0 +1,16 @@ +CREATE TABLE TREKK_I_LOEPENDE_UTBETALING +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT REFERENCES BEHANDLING (ID) NOT NULL, + FOM TIMESTAMP(3), + TOM TIMESTAMP(3), + FEILUTBETALT_BELOEP NUMERIC, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE TREKK_I_LOEPENDE_UTBETALING_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX TREKK_I_LOEPENDE_UTBETALING_FK_BEHANDLING_ID_IDX ON TREKK_I_LOEPENDE_UTBETALING (FK_BEHANDLING_ID); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V232__prefixer_standardbegrunnelser.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V232__prefixer_standardbegrunnelser.sql new file mode 100644 index 000000000..56f2e3016 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V232__prefixer_standardbegrunnelser.sql @@ -0,0 +1,15 @@ +UPDATE vilkar_resultat +SET vedtak_begrunnelse_spesifikasjoner = concat('Standardbegrunnelse$', vedtak_begrunnelse_spesifikasjoner) +WHERE vedtak_begrunnelse_spesifikasjoner <> ''; + +UPDATE vilkar_resultat +SET vedtak_begrunnelse_spesifikasjoner = replace(vedtak_begrunnelse_spesifikasjoner, ';', ';Standardbegrunnelse$') +WHERE vedtak_begrunnelse_spesifikasjoner like '%;%'; + +UPDATE endret_utbetaling_andel +SET vedtak_begrunnelse_spesifikasjoner = concat('Standardbegrunnelse$', vedtak_begrunnelse_spesifikasjoner) +WHERE vedtak_begrunnelse_spesifikasjoner <> ''; + +UPDATE endret_utbetaling_andel +SET vedtak_begrunnelse_spesifikasjoner = replace(vedtak_begrunnelse_spesifikasjoner, ';', ';Standardbegrunnelse$') +WHERE vedtak_begrunnelse_spesifikasjoner like '%;%'; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V233__notnull_fom_og_tom_trekk_i_loepende_utbetaling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V233__notnull_fom_og_tom_trekk_i_loepende_utbetaling.sql new file mode 100644 index 000000000..f31d03e6e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V233__notnull_fom_og_tom_trekk_i_loepende_utbetaling.sql @@ -0,0 +1,10 @@ +update TREKK_I_LOEPENDE_UTBETALING + set FOM = '1900-01-01' where FOM is null; + +update TREKK_I_LOEPENDE_UTBETALING + set TOM = '2900-01-01' where TOM is null; + + +alter table TREKK_I_LOEPENDE_UTBETALING + ALTER COLUMN FOM SET NOT NULL, + ALTER COLUMN TOM SET NOT NULL; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V234__navnendring_feilutbetalt_valuta.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V234__navnendring_feilutbetalt_valuta.sql new file mode 100644 index 000000000..918c819bd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V234__navnendring_feilutbetalt_valuta.sql @@ -0,0 +1,8 @@ +ALTER TABLE TREKK_I_LOEPENDE_UTBETALING + RENAME TO FEILUTBETALT_VALUTA; + +ALTER SEQUENCE TREKK_I_LOEPENDE_UTBETALING_SEQ + RENAME TO FEILUTBETALT_VALUTA_SEQ; + +ALTER INDEX TREKK_I_LOEPENDE_UTBETALING_FK_BEHANDLING_ID_IDX + RENAME TO FEILUTBETALT_VALUTA_FK_BEHANDLING_ID_IDX; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V235__okonomi_simulering_mottaker_legg_til_er_feilutbetaling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V235__okonomi_simulering_mottaker_legg_til_er_feilutbetaling.sql new file mode 100644 index 000000000..4e0c0d21e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V235__okonomi_simulering_mottaker_legg_til_er_feilutbetaling.sql @@ -0,0 +1,2 @@ +ALTER TABLE okonomi_simulering_postering + ADD er_feilkonto BOOLEAN; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V236__brevmottaker.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V236__brevmottaker.sql new file mode 100644 index 000000000..7156dc6dd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V236__brevmottaker.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS brevmottaker ( + id BIGINT PRIMARY KEY, + fk_behandling_id BIGINT REFERENCES behandling (id) ON DELETE CASCADE NOT NULL, + type VARCHAR(50) NOT NULL, + navn VARCHAR NOT NULL, + adresselinje_1 VARCHAR NOT NULL, + adresselinje_2 VARCHAR, + postnummer VARCHAR NOT NULL, + poststed VARCHAR NOT NULL, + landkode VARCHAR(2) NOT NULL, + versjon BIGINT DEFAULT 0 NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +CREATE SEQUENCE IF NOT EXISTS brevmottaker_seq INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX IF NOT EXISTS brevmottaker_fk_behandling_id_idx ON brevmottaker (fk_behandling_id); diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V237__satskjoering.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V237__satskjoering.sql new file mode 100644 index 000000000..755e951eb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V237__satskjoering.sql @@ -0,0 +1,10 @@ +CREATE TABLE satskjoering +( + ID BIGINT NOT NULL PRIMARY KEY, + FK_FAGSAK_ID BIGINT REFERENCES FAGSAK (ID) ON DELETE CASCADE NOT NULL, + START_TID TIMESTAMP(3) DEFAULT LOCALTIMESTAMP NOT NULL, + FERDIG_TID TIMESTAMP(3) +); + +CREATE SEQUENCE SATSKJOERING_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX satskjoering_fagsak_id_idx ON satskjoering (fk_fagsak_id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V238__satskjoering_feiltype.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V238__satskjoering_feiltype.sql new file mode 100644 index 000000000..079be3db2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V238__satskjoering_feiltype.sql @@ -0,0 +1,2 @@ +ALTER TABLE satskjoering + ADD COLUMN feiltype VARCHAR DEFAULT NULL; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V239__slett_andel_til_endret_andel_tabell.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V239__slett_andel_til_endret_andel_tabell.sql new file mode 100644 index 000000000..2b4c5b73f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V239__slett_andel_til_endret_andel_tabell.sql @@ -0,0 +1 @@ +drop table andel_til_endret_andel; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V23__drop_saksnummer.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V23__drop_saksnummer.sql new file mode 100644 index 000000000..225d4ae0a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V23__drop_saksnummer.sql @@ -0,0 +1 @@ +alter table BEHANDLING drop column saksnummer; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V240__opprett_refusjon_eos.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V240__opprett_refusjon_eos.sql new file mode 100644 index 000000000..0569052fd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V240__opprett_refusjon_eos.sql @@ -0,0 +1,18 @@ +CREATE TABLE REFUSJON_EOS +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT REFERENCES BEHANDLING (ID) NOT NULL, + FOM TIMESTAMP(3) NOT NULL, + TOM TIMESTAMP(3) NOT NULL, + REFUSJONSBELOEP NUMERIC NOT NULL, + LAND VARCHAR NOT NULL, + REFUSJON_AVKLART BOOLEAN NOT NULL, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE REFUSJON_EOS_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX REFUSJON_EOS_FK_BEHANDLING_ID_IDX ON REFUSJON_EOS (FK_BEHANDLING_ID); diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V241__oppdater_behandlinger_satt_p\303\245_vent.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V241__oppdater_behandlinger_satt_p\303\245_vent.sql" new file mode 100644 index 000000000..7333789fc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V241__oppdater_behandlinger_satt_p\303\245_vent.sql" @@ -0,0 +1,3 @@ +UPDATE behandling +SET status = 'SATT_PÅ_VENT' +WHERE id IN (SELECT fk_behandling_id from sett_paa_vent WHERE aktiv = true); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V242__behandling_aktivert_tid.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V242__behandling_aktivert_tid.sql new file mode 100644 index 000000000..8c99a6b8c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V242__behandling_aktivert_tid.sql @@ -0,0 +1,2 @@ +ALTER TABLE behandling + ADD COLUMN aktivert_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V243__flere_aktive_behandlinger.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V243__flere_aktive_behandlinger.sql new file mode 100644 index 000000000..2361e567a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V243__flere_aktive_behandlinger.sql @@ -0,0 +1,7 @@ +UPDATE behandling SET aktivert_tid = opprettet_tid; +ALTER TABLE behandling ALTER COLUMN aktivert_tid DROP DEFAULT; + +-- Kun en behandling kan ha status annet enn AVSLUTTET eller SATT_PÅ_MASKINELL_VENT +CREATE UNIQUE INDEX UIDX_BEHANDLING_02 ON behandling (fk_fagsak_id) WHERE (status <> 'AVSLUTTET' AND status <> 'SATT_PÅ_MASKINELL_VENT'); +-- Kun en behandling kan ha status SATT_PÅ_VENT +CREATE UNIQUE INDEX UIDX_BEHANDLING_03 ON behandling (fk_fagsak_id) WHERE status = 'SATT_PÅ_MASKINELL_VENT'; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V244__satstid_satskjoring.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V244__satstid_satskjoring.sql new file mode 100644 index 000000000..0ecbed6e1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V244__satstid_satskjoring.sql @@ -0,0 +1,2 @@ +ALTER TABLE satskjoering + ADD COLUMN sats_tid TIMESTAMP(3) DEFAULT TO_TIMESTAMP('01-03-2023', 'DD-MM-YYYY SS:MS') NOT NULL; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V245__feilutbetalt_valuta_per_mnd.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V245__feilutbetalt_valuta_per_mnd.sql new file mode 100644 index 000000000..2121ef7d3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V245__feilutbetalt_valuta_per_mnd.sql @@ -0,0 +1,2 @@ +ALTER TABLE feilutbetalt_valuta + ADD COLUMN IF NOT EXISTS er_per_maaned BOOLEAN DEFAULT FALSE NOT NULL; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V246__vilkar_resultat_begrunnelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V246__vilkar_resultat_begrunnelse.sql new file mode 100644 index 000000000..00c386eed --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V246__vilkar_resultat_begrunnelse.sql @@ -0,0 +1,2 @@ +ALTER TABLE vilkar_resultat + ADD COLUMN IF NOT EXISTS resultat_begrunnelse VARCHAR DEFAULT NULL; \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V247__s\303\270knad_digitaliseringsgrad_data.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V247__s\303\270knad_digitaliseringsgrad_data.sql" new file mode 100644 index 000000000..f64a0af8a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V247__s\303\270knad_digitaliseringsgrad_data.sql" @@ -0,0 +1,6 @@ +ALTER TABLE behandling_soknadsinfo + ADD COLUMN IF NOT EXISTS er_digital BOOLEAN DEFAULT NULL, + ADD COLUMN IF NOT EXISTS journalpost_id VARCHAR DEFAULT NULL, + ADD COLUMN IF NOT EXISTS brevkode VARCHAR DEFAULT NULL; + +CREATE INDEX journalpost_id_idx ON behandling_soknadsinfo (journalpost_id); diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V248__doedsfall_legg_til_manuellregistrert_kolonne.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V248__doedsfall_legg_til_manuellregistrert_kolonne.sql new file mode 100644 index 000000000..5ed7835ac --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V248__doedsfall_legg_til_manuellregistrert_kolonne.sql @@ -0,0 +1,2 @@ +ALTER TABLE po_doedsfall + ADD COLUMN IF NOT EXISTS manuell_registrert BOOLEAN DEFAULT FALSE NOT NULL; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V249__sletter_underkategori_institusjon.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V249__sletter_underkategori_institusjon.sql new file mode 100644 index 000000000..dbf6c80ab --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V249__sletter_underkategori_institusjon.sql @@ -0,0 +1,3 @@ +UPDATE behandling +SET underkategori = 'ORDINÆR' +WHERE underkategori = 'INSTITUSJON'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V24__base_entitet_oke_felter.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V24__base_entitet_oke_felter.sql new file mode 100644 index 000000000..1a9cd5d08 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V24__base_entitet_oke_felter.sql @@ -0,0 +1,17 @@ +alter table fagsak alter column endret_av set data type varchar(512); +alter table behandling alter column endret_av set data type varchar(512); +alter table po_person alter column endret_av set data type varchar(512); +alter table gr_personopplysninger alter column endret_av set data type varchar(512); +alter table vedtak alter column endret_av set data type varchar(512); +alter table vedtak_person alter column endret_av set data type varchar(512); +alter table samlet_vilkar_resultat alter column endret_av set data type varchar(512); +alter table vilkar_resultat alter column endret_av set data type varchar(512); + +alter table fagsak alter column opprettet_av set data type varchar(512); +alter table behandling alter column opprettet_av set data type varchar(512); +alter table po_person alter column opprettet_av set data type varchar(512); +alter table gr_personopplysninger alter column opprettet_av set data type varchar(512); +alter table vedtak alter column opprettet_av set data type varchar(512); +alter table vedtak_person alter column opprettet_av set data type varchar(512); +alter table samlet_vilkar_resultat alter column opprettet_av set data type varchar(512); +alter table vilkar_resultat alter column opprettet_av set data type varchar(512); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V250__fjern_begrunnelser_fra_endret_utbetaling_andel.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V250__fjern_begrunnelser_fra_endret_utbetaling_andel.sql new file mode 100644 index 000000000..1d39c44aa --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V250__fjern_begrunnelser_fra_endret_utbetaling_andel.sql @@ -0,0 +1,2 @@ +ALTER TABLE endret_utbetaling_andel + DROP COLUMN vedtak_begrunnelse_spesifikasjoner; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V251__kompetanse_annen_forelder_omfattet_av_norsk_lov.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V251__kompetanse_annen_forelder_omfattet_av_norsk_lov.sql new file mode 100644 index 000000000..a6ded0258 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V251__kompetanse_annen_forelder_omfattet_av_norsk_lov.sql @@ -0,0 +1,2 @@ +ALTER TABLE kompetanse + ADD COLUMN IF NOT EXISTS er_annen_forelder_omfattet_av_norsk_lovgivning boolean default false; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V252__navnendring_vilkar_resultat.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V252__navnendring_vilkar_resultat.sql new file mode 100644 index 000000000..161098064 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V252__navnendring_vilkar_resultat.sql @@ -0,0 +1,12 @@ +DROP INDEX IF EXISTS vilkar_resultat_fk_idx; + +ALTER TABLE VILKAR_RESULTAT +DROP CONSTRAINT IF EXISTS FK_BEHANDLING_ID_VILKAR_RESULTAT; + +ALTER TABLE VILKAR_RESULTAT + RENAME COLUMN FK_BEHANDLING_ID TO SIST_ENDRET_I_BEHANDLING_ID; + +ALTER TABLE VILKAR_RESULTAT +ADD CONSTRAINT SIST_ENDRET_I_BEHANDLING_ID_VILKAR_RESULTAT FOREIGN KEY (SIST_ENDRET_I_BEHANDLING_ID) REFERENCES BEHANDLING (ID); + +CREATE INDEX ON VILKAR_RESULTAT(SIST_ENDRET_I_BEHANDLING_ID); \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V25__akt\303\270r_id_person.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V25__akt\303\270r_id_person.sql" new file mode 100644 index 000000000..09bd124c5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V25__akt\303\270r_id_person.sql" @@ -0,0 +1 @@ +alter table PO_PERSON add column AKTOER_ID VARCHAR(50); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V26__berik_behandling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V26__berik_behandling.sql new file mode 100644 index 000000000..9e77dbc17 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V26__berik_behandling.sql @@ -0,0 +1,5 @@ +alter table VEDTAK drop column resultat; +alter table BEHANDLING add column resultat varchar; + +alter table VEDTAK drop column begrunnelse; +alter table BEHANDLING add column begrunnelse TEXT; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V27__satstabell.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V27__satstabell.sql new file mode 100644 index 000000000..4edc50d5a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V27__satstabell.sql @@ -0,0 +1,15 @@ +CREATE TABLE SATS ( + ID BIGINT PRIMARY KEY, + TYPE VARCHAR(100) NOT NULL, + BELOP BIGINT NOT NULL, + GYLDIG_FOM TIMESTAMP, + GYLDIG_TOM TIMESTAMP +); + +CREATE SEQUENCE SATS_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; + +INSERT INTO SATS (id, type, belop, gyldig_fom) VALUES (1, 'ORBA', 1054, to_date('01-03-2019', 'DD-MM-YYYY')) ON CONFLICT DO NOTHING; +INSERT INTO SATS (id, type, belop, gyldig_tom) VALUES (2, 'ORBA', 970, to_date('28-02-2019', 'DD-MM-YYYY')) ON CONFLICT DO NOTHING; +INSERT INTO SATS (id, type, belop) VALUES (3, 'SMA', 660) ON CONFLICT DO NOTHING; +INSERT INTO SATS (id, type, belop, gyldig_fom) VALUES (4, 'TILLEGG_ORBA', 1354, to_date('01-09-2020', 'DD-MM-YYYY')) ON CONFLICT DO NOTHING; +INSERT INTO SATS (id, type, belop, gyldig_tom) VALUES (5, 'FINN_SVAL', 1054, to_date('31-03-2014', 'DD-MM-YYYY')) ON CONFLICT DO NOTHING; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V28__steg.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V28__steg.sql new file mode 100644 index 000000000..dedabec8c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V28__steg.sql @@ -0,0 +1 @@ +alter table behandling add column steg varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V29__logg.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V29__logg.sql new file mode 100644 index 000000000..2f2fa5229 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V29__logg.sql @@ -0,0 +1,14 @@ +CREATE TABLE LOGG +( + ID BIGINT PRIMARY KEY, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + FK_BEHANDLING_ID BIGINT REFERENCES behandling (id) NOT NULL, + TYPE VARCHAR NOT NULL, + TITTEL VARCHAR NOT NULL, + ROLLE VARCHAR NOT NULL, + TEKST TEXT NOT NULL +); + +create INDEX ON LOGG (FK_BEHANDLING_ID); +CREATE SEQUENCE LOGG_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V30__navn_og_kjonn.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V30__navn_og_kjonn.sql new file mode 100644 index 000000000..ef1fecece --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V30__navn_og_kjonn.sql @@ -0,0 +1,2 @@ +alter table PO_PERSON add column navn varchar default ''; +alter table PO_PERSON add column kjoenn varchar default 'UKJENT'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V31__endre_navn_loggtype.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V31__endre_navn_loggtype.sql new file mode 100644 index 000000000..3c8e62238 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V31__endre_navn_loggtype.sql @@ -0,0 +1 @@ +update behandling set steg = 'SEND_TIL_BESLUTTER' where steg = 'FORESLÅ_VEDTAK'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V32__endre_utfall_til_resultat.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V32__endre_utfall_til_resultat.sql new file mode 100644 index 000000000..18fb5cf2f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V32__endre_utfall_til_resultat.sql @@ -0,0 +1,3 @@ +update vilkar_resultat set utfall = 'JA' where utfall = 'OPPFYLT'; +update vilkar_resultat set utfall = 'NEI' where utfall = 'IKKE_OPPFYLT'; +alter table vilkar_resultat rename column utfall to resultat; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V33__soknad.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V33__soknad.sql new file mode 100644 index 000000000..d990c483a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V33__soknad.sql @@ -0,0 +1,23 @@ +CREATE TABLE GR_SOKNAD +( + ID BIGINT PRIMARY KEY, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + FK_BEHANDLING_ID BIGINT REFERENCES behandling (id) NOT NULL, + SOKNAD TEXT NOT NULL, + AKTIV BOOLEAN DEFAULT TRUE NOT NULL +); + +create INDEX ON GR_SOKNAD (FK_BEHANDLING_ID); +CREATE SEQUENCE GR_SOKNAD_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; + +CREATE UNIQUE INDEX UIDX_GR_SOKNAD_01 + ON GR_SOKNAD + ((CASE + WHEN aktiv = true + THEN fk_behandling_id + END), + (CASE + WHEN aktiv = true + THEN aktiv + END)); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V34__gjeldende_behandling_utbetaling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V34__gjeldende_behandling_utbetaling.sql new file mode 100644 index 000000000..5607225ee --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V34__gjeldende_behandling_utbetaling.sql @@ -0,0 +1,2 @@ +ALTER TABLE behandling + ADD COLUMN gjeldende_for_utbetaling boolean default false not null; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V35__beregning_resultat.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V35__beregning_resultat.sql new file mode 100644 index 000000000..4948e5514 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V35__beregning_resultat.sql @@ -0,0 +1,12 @@ +CREATE TABLE BEREGNING_RESULTAT ( + ID bigint primary key, + FK_BEHANDLING_ID bigint references BEHANDLING (id), + STONAD_FOM timestamp, + STONAD_TOM timestamp not null, + OPPRETTET_DATO timestamp not null, + OPPHOR_FOM timestamp, + UTBETALINGSOPPDRAG text not null +); + +CREATE SEQUENCE BEREGNING_RESULTAT_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON BEREGNING_RESULTAT (FK_BEHANDLING_ID); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V36__vedtak_person_til_andel_tilkjent_ytelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V36__vedtak_person_til_andel_tilkjent_ytelse.sql new file mode 100644 index 000000000..d032d52f9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V36__vedtak_person_til_andel_tilkjent_ytelse.sql @@ -0,0 +1,120 @@ +-- Skal endre navn på VEDTAK_PERSON til ANDEL_TILKJENT_YTELSE og peke på BEHANDLING i stedet for VEDTAK +-- Må passe på at en kjørende instans fortsatt har VEDTAK_PERSON å skrive til under migrering +-- Derfor opprettes ny tabell ANDEL_TILKJENT_YTELSE og migrerer eksisterende data i VEDTAK_PERSON +-- og legger på en trigger som oppdaterer ANDEL_TILKJENT_YTELSE når det skjer endringer i VEDTAK_PERSON +-- Del 2 blir å fjerne VEDTAK_PERSON og trigger + +create table ANDEL_TILKJENT_YTELSE +( + id bigint primary key, + fk_behandling_id bigint references BEHANDLING (id) not null, + fk_person_id bigint references po_person (id) not null, + versjon bigint default 0 not null, + opprettet_av VARCHAR(512) default 'VL' not null, + opprettet_tid TIMESTAMP(3) default localtimestamp not null, + stonad_fom TIMESTAMP(3) not null, + stonad_tom TIMESTAMP(3) not null, + type varchar(50) not null, + belop numeric, + endret_av VARCHAR(512), + endret_tid TIMESTAMP(3) +); + +CREATE SEQUENCE ANDEL_TILKJENT_YTELSE_SEQ INCREMENT BY 50 START WITH 2000000 NO CYCLE; +create index on ANDEL_TILKJENT_YTELSE (fk_behandling_id); +create index on ANDEL_TILKJENT_YTELSE (fk_person_id); + +-- Satser på at det er nok :/ +SELECT setval('ANDEL_TILKJENT_YTELSE_SEQ', COALESCE((SELECT MAX(id)+1000000 FROM vedtak_person),2000000)); + +-- Overfør alt fra VEDTAK_PERSON +INSERT INTO ANDEL_TILKJENT_YTELSE ( + id, + fk_behandling_id, + fk_person_id, + versjon, + opprettet_av, + opprettet_tid, + stonad_fom, + stonad_tom, + type, + belop, + endret_av, + endret_tid) +SELECT + vp.id, + v.fk_behandling_id, + vp.fk_person_id, + vp.versjon, + vp.opprettet_av, + vp.opprettet_tid, + vp.stonad_fom, + vp.stonad_tom, + vp.type, + vp.belop, + vp.endret_av, + vp.endret_tid +FROM VEDTAK_PERSON vp JOIN VEDTAK v ON vp.fk_vedtak_id=v.id; + +-- Pass på at endringer i VEDTAK_PERSON etter dette migreres til ANDEL_TILKJENT_YTELSE +-- VEDTAK_PERSON og denne triggeren skal slettes i egen migrering +CREATE OR REPLACE FUNCTION oppdater_andel_tilkjent_ytelse() RETURNS TRIGGER AS +$body$ + DECLARE + behandlingId bigint; + BEGIN + IF (TG_OP = 'DELETE') THEN + DELETE FROM ANDEL_TILKJENT_YTELSE WHERE id = OLD.id; + + RETURN OLD; + ELSIF (TG_OP = 'UPDATE') THEN + SELECT vedtak.fk_behandling_id INTO behandlingId FROM vedtak WHERE vedtak.id = NEW.fk_vedtak_id; + UPDATE ANDEL_TILKJENT_YTELSE + SET fk_behandling_id=behandlingId, + versjon = NEW.versjon, + stonad_fom=NEW.stonad_fom, + stonad_tom=NEW.stonad_tom, + type=NEW.type, + belop=NEW.belop, + endret_av = NEW.endret_av, + endret_tid=NEW.endret_tid + WHERE id = NEW.id; + + RETURN NEW; + ELSIF (TG_OP = 'INSERT') THEN + SELECT vedtak.fk_behandling_id INTO behandlingId FROM vedtak WHERE vedtak.id = NEW.fk_vedtak_id; + INSERT INTO ANDEL_TILKJENT_YTELSE ( + id, + fk_behandling_id, + fk_person_id, + versjon, + opprettet_av, + opprettet_tid, + stonad_fom, + stonad_tom, + type, + belop, + endret_av, + endret_tid) + VALUES( + NEW.id, + behandlingId, + NEW.fk_person_id, + NEW.versjon, + NEW.opprettet_av, + NEW.opprettet_tid, + NEW.stonad_fom, + NEW.stonad_tom, + NEW.type, + NEW.belop, + NEW.endret_av, + NEW.endret_tid); + + RETURN NEW; + END IF; + END; +$body$ LANGUAGE plpgsql; + +CREATE TRIGGER oppdater_andel_tilkjent_ytelse + AFTER INSERT OR UPDATE OR DELETE ON vedtak_person + FOR EACH ROW EXECUTE PROCEDURE oppdater_andel_tilkjent_ytelse(); diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V37__fjern_vedtak_person.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V37__fjern_vedtak_person.sql new file mode 100644 index 000000000..6d9438406 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V37__fjern_vedtak_person.sql @@ -0,0 +1,3 @@ +DROP TABLE vedtak_person; +DROP FUNCTION oppdater_andel_tilkjent_ytelse; +DROP SEQUENCE vedtak_person_seq; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V38__periodisert_vilkaarsvurdering.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V38__periodisert_vilkaarsvurdering.sql new file mode 100644 index 000000000..32c08af26 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V38__periodisert_vilkaarsvurdering.sql @@ -0,0 +1,53 @@ +alter table behandling drop column resultat; + +CREATE TABLE BEHANDLING_RESULTAT +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT REFERENCES behandling (id) NOT NULL, + AKTIV BOOLEAN DEFAULT TRUE NOT NULL, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); +ALTER SEQUENCE SAMLET_VILKAR_RESULTAT_SEQ RENAME TO BEHANDLING_RESULTAT_SEQ; +insert into BEHANDLING_RESULTAT (id, fk_behandling_id, aktiv, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) +select id, fk_behandling_id, aktiv, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid from samlet_vilkar_resultat; + +CREATE TABLE PERIODE_RESULTAT +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_RESULTAT_ID BIGINT REFERENCES BEHANDLING_RESULTAT (id) NOT NULL, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3), + PERSON_IDENT VARCHAR, + PERIODE_FOM TIMESTAMP(3), + PERIODE_TOM TIMESTAMP(3) +); +CREATE SEQUENCE PERIODE_RESULTAT_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; + +alter table vilkar_resultat add column tmp_person_ident varchar; +update vilkar_resultat vr set tmp_person_ident=(select distinct p.person_ident from po_person p where p.id = vr.fk_person_id limit 1); + +insert into PERIODE_RESULTAT(id, fk_behandling_resultat_id, person_ident) +select nextval('PERIODE_RESULTAT_SEQ'), samlet_vilkar_resultat_id, tmp_person_ident from vilkar_resultat +group by samlet_vilkar_resultat_id, tmp_person_ident; + +update PERIODE_RESULTAT pr +set VERSJON= br.VERSJON, OPPRETTET_AV=br.OPPRETTET_AV, OPPRETTET_TID=br.OPPRETTET_TID, ENDRET_AV=br.ENDRET_AV, ENDRET_TID=br.ENDRET_TID +from BEHANDLING_RESULTAT br +where pr.FK_BEHANDLING_RESULTAT_ID = br.ID; + +alter table vilkar_resultat add column fk_periode_resultat_id BIGINT REFERENCES PERIODE_RESULTAT (id); +update vilkar_resultat vr set fk_periode_resultat_id=(select pr.id from PERIODE_RESULTAT pr where vr.samlet_vilkar_resultat_id = pr.FK_BEHANDLING_RESULTAT_ID and vr.tmp_person_ident = pr.person_ident limit 1); +ALTER TABLE vilkar_resultat ALTER COLUMN fk_periode_resultat_id SET NOT NULL; + +drop table samlet_vilkar_resultat cascade; +alter table vilkar_resultat + drop column samlet_vilkar_resultat_id, + drop column fk_person_id, + drop column tmp_person_ident; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V39__rename_beregningsresultat.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V39__rename_beregningsresultat.sql new file mode 100644 index 000000000..12237e43c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V39__rename_beregningsresultat.sql @@ -0,0 +1,7 @@ +ALTER TABLE BEREGNING_RESULTAT RENAME TO TILKJENT_YTELSE; +ALTER SEQUENCE BEREGNING_RESULTAT_SEQ RENAME TO TILKJENT_YTELSE_SEQ; + +ALTER TABLE TILKJENT_YTELSE + ALTER COLUMN stonad_tom DROP NOT NULL, + ALTER COLUMN utbetalingsoppdrag DROP NOT NULL, + ADD COLUMN endret_dato timestamp not null default current_timestamp; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V3__vedtak.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V3__vedtak.sql new file mode 100644 index 000000000..44b612d64 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V3__vedtak.sql @@ -0,0 +1,20 @@ +create table BEHANDLING_VEDTAK +( + id bigint primary key, + fk_behandling_id bigint references BEHANDLING (id) not null, + versjon bigint default 0 not null, + opprettet_av VARCHAR(20) default 'VL' not null, + opprettet_tid TIMESTAMP(3) default localtimestamp not null, + ansvarlig_saksbehandler VARCHAR(50) not null, + vedtaksdato TIMESTAMP(3) default localtimestamp not null, + stonad_fom TIMESTAMP(3) not null, + stonad_tom TIMESTAMP(3) not null, + stonad_brev_markdown TEXT, + endret_av VARCHAR(20), + endret_tid TIMESTAMP(3) +); + +CREATE SEQUENCE BEHANDLING_VEDTAK_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +create index on BEHANDLING_VEDTAK (fk_behandling_id); + +alter table BEHANDLING add column behandling_type VARCHAR(50); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V40__andel_tilkjent_ytelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V40__andel_tilkjent_ytelse.sql new file mode 100644 index 000000000..f4e554626 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V40__andel_tilkjent_ytelse.sql @@ -0,0 +1,6 @@ +ALTER TABLE ANDEL_TILKJENT_YTELSE + ADD COLUMN tilkjent_ytelse_id bigint references tilkjent_ytelse(id); + +UPDATE ANDEL_TILKJENT_YTELSE aty +SET tilkjent_ytelse_id = ty.id +FROM tilkjent_ytelse ty WHERE aty.fk_behandling_id = ty.fk_behandling_id; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V41__flytt_begrunnelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V41__flytt_begrunnelse.sql new file mode 100644 index 000000000..c05216dd7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V41__flytt_begrunnelse.sql @@ -0,0 +1,16 @@ +alter table vilkar_resultat add column begrunnelse text; + +update vilkar_resultat vr +set begrunnelse=periode_resultat_begrunnelse.begrunnelse +from ( + with behandling_resultat_begrunnelse as ( + select br.id, b.begrunnelse + from behandling_resultat br, + behandling b + where br.FK_BEHANDLING_ID = b.id) + select brb.begrunnelse, pr.id as periode_resultat_id + from behandling_resultat_begrunnelse brb + inner join periode_resultat pr on pr.fk_behandling_resultat_id = brb.ID) as periode_resultat_begrunnelse +where periode_resultat_begrunnelse.periode_resultat_id = vr.fk_periode_resultat_id; + +alter table behandling drop column begrunnelse; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V42__endringer_vilkar.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V42__endringer_vilkar.sql new file mode 100644 index 000000000..4e5282d31 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V42__endringer_vilkar.sql @@ -0,0 +1,4 @@ +DELETE FROM vilkar_resultat WHERE vilkar='STØNADSPERIODE'; +INSERT INTO vilkar_resultat (id, vilkar, begrunnelse, endret_av, endret_tid, fk_periode_resultat_id, opprettet_av, opprettet_tid, regel_input, regel_output, resultat) + (SELECT nextval('VILKAR_RESULTAT_SEQ'), 'BOR_MED_SØKER', vr.begrunnelse, vr.endret_av, vr.endret_tid, vr.fk_periode_resultat_id, vr.opprettet_av, vr.opprettet_tid, vr.regel_input, vr.regel_output, vr.resultat FROM vilkar_resultat vr WHERE vr.vilkar='UNDER_18_ÅR_OG_BOR_MED_SØKER'); +UPDATE vilkar_resultat SET vilkar='UNDER_18_ÅR' WHERE vilkar='UNDER_18_ÅR_OG_BOR_MED_SØKER'; diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V43__flytte-perioder-til-vilk\303\245rvurdering.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V43__flytte-perioder-til-vilk\303\245rvurdering.sql" new file mode 100644 index 000000000..406bc2089 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V43__flytte-perioder-til-vilk\303\245rvurdering.sql" @@ -0,0 +1,5 @@ +ALTER TABLE vilkar_resultat ADD COLUMN PERIODE_FOM TIMESTAMP(3) DEFAULT NULL; +ALTER TABLE vilkar_resultat ADD COLUMN PERIODE_TOM TIMESTAMP(3) DEFAULT NULL; +ALTER TABLE periode_resultat DROP COLUMN PERIODE_FOM; +ALTER TABLE periode_resultat DROP COLUMN PERIODE_TOM; +ALTER TABLE periode_resultat rename TO person_resultat; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V44__oppdater_steg.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V44__oppdater_steg.sql new file mode 100644 index 000000000..da39e70fb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V44__oppdater_steg.sql @@ -0,0 +1 @@ +UPDATE behandling SET steg='BESLUTTE_VEDTAK' WHERE steg='GODKJENNE_VEDTAK'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V45__slett_satstabell.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V45__slett_satstabell.sql new file mode 100644 index 000000000..526d7b1bf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V45__slett_satstabell.sql @@ -0,0 +1 @@ +DROP TABLE SATS; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V46__oppgave.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V46__oppgave.sql new file mode 100644 index 000000000..3bbf49d28 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V46__oppgave.sql @@ -0,0 +1,11 @@ +CREATE TABLE OPPGAVE ( + id bigint primary key, + fk_behandling_id bigint references behandling (id) not null, + gsak_id varchar not null, + type varchar not null, + ferdigstilt bool not null, + opprettet_tid timestamp not null +); + +CREATE SEQUENCE OPPGAVE_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V47__vedtak_beslutter.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V47__vedtak_beslutter.sql new file mode 100644 index 000000000..c9f634238 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V47__vedtak_beslutter.sql @@ -0,0 +1 @@ +ALTER TABLE VEDTAK ADD COLUMN ansvarlig_beslutter varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V48__vedtak_brev_html_og_journalpost_id.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V48__vedtak_brev_html_og_journalpost_id.sql new file mode 100644 index 000000000..cd7451bd1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V48__vedtak_brev_html_og_journalpost_id.sql @@ -0,0 +1,2 @@ +ALTER TABLE VEDTAK ADD COLUMN stonad_brev_pdf bytea; +ALTER TABLE VEDTAK ADD COLUMN ansvarlig_enhet varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V49__ident_tabell.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V49__ident_tabell.sql new file mode 100644 index 000000000..ec7e43371 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V49__ident_tabell.sql @@ -0,0 +1,10 @@ +CREATE TABLE FAGSAK_PERSON ( + ID BIGINT PRIMARY KEY, + FK_FAGSAK_ID BIGINT NOT NULL, + IDENT VARCHAR(50) NOT NULL, + OPPRETTET_AV VARCHAR(512) DEFAULT 'VL', + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp +); +CREATE SEQUENCE FAGSAK_PERSON_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +create index on FAGSAK_PERSON (FK_FAGSAK_ID); +create index on FAGSAK_PERSON (IDENT); diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V4__behandling_aktiv.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V4__behandling_aktiv.sql new file mode 100644 index 000000000..0ee604469 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V4__behandling_aktiv.sql @@ -0,0 +1,14 @@ +alter table BEHANDLING add column aktiv boolean default true; + +CREATE UNIQUE INDEX UIDX_BEHANDLING_01 + ON BEHANDLING + ( + (CASE + WHEN aktiv = true + THEN FK_FAGSAK_ID + ELSE NULL END), + (CASE + WHEN aktiv = true + THEN aktiv + ELSE NULL END) + ); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V50__migrer_ident_fra_fagsak_til_fagsak_person.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V50__migrer_ident_fra_fagsak_til_fagsak_person.sql new file mode 100644 index 000000000..03e5c65f2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V50__migrer_ident_fra_fagsak_til_fagsak_person.sql @@ -0,0 +1,5 @@ +INSERT INTO FAGSAK_PERSON (ID, FK_FAGSAK_ID, IDENT, OPPRETTET_AV, OPPRETTET_TID) +SELECT nextval('FAGSAK_PERSON_SEQ'), ID, PERSON_IDENT, OPPRETTET_AV, OPPRETTET_TID FROM FAGSAK; + +ALTER TABLE fagsak ALTER COLUMN person_ident DROP NOT NULL; +ALTER TABLE fagsak ALTER COLUMN aktoer_id DROP NOT NULL; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V51__behandling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V51__behandling.sql new file mode 100644 index 000000000..6d1de3cc4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V51__behandling.sql @@ -0,0 +1,2 @@ +ALTER TABLE behandling DROP COLUMN oppgave_id; +ALTER TABLE behandling ADD COLUMN behandling_opprinnelse varchar default 'MANUELL'; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V52__personident_andel.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V52__personident_andel.sql new file mode 100644 index 000000000..06d23f03d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V52__personident_andel.sql @@ -0,0 +1,11 @@ +ALTER TABLE ANDEL_TILKJENT_YTELSE + ADD COLUMN person_ident varchar; + +UPDATE ANDEL_TILKJENT_YTELSE aty +SET PERSON_IDENT = ( + SELECT P.person_ident + FROM po_person P + WHERE aty.fk_person_id = p.id limit 1); + +ALTER TABLE ANDEL_TILKJENT_YTELSE + ALTER COLUMN person_ident SET NOT NULL; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V53__totrinnskontroll_v2.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V53__totrinnskontroll_v2.sql new file mode 100644 index 000000000..013a0831f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V53__totrinnskontroll_v2.sql @@ -0,0 +1,31 @@ +CREATE TABLE TOTRINNSKONTROLL +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT NOT NULL, + VERSJON bigint default 0 not null, + OPPRETTET_AV VARCHAR default 'VL' not null, + OPPRETTET_TID TIMESTAMP(3) default localtimestamp not null, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3), + AKTIV BOOLEAN DEFAULT TRUE NOT NULL, + SAKSBEHANDLER VARCHAR NOT NULL, + BESLUTTER VARCHAR, + GODKJENT BOOLEAN DEFAULT TRUE +); + +CREATE SEQUENCE TOTRINNSKONTROLL_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON TOTRINNSKONTROLL (FK_BEHANDLING_ID); + +/* Forsikrer at kun en rad er aktiv */ +CREATE UNIQUE INDEX UIDX_TOTRINNSKONTROLL_01 + ON TOTRINNSKONTROLL + ( + (CASE + WHEN aktiv = true + THEN fk_behandling_id + END), + (CASE + WHEN aktiv = true + THEN aktiv + END) + ); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V54__drop_null_vilkarresultat.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V54__drop_null_vilkarresultat.sql new file mode 100644 index 000000000..689ed8207 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V54__drop_null_vilkarresultat.sql @@ -0,0 +1 @@ +ALTER TABLE VILKAR_RESULTAT ALTER COLUMN fk_periode_resultat_id DROP NOT NULL; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V55__legger_til_fg_constraint_paa_fagsak_person.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V55__legger_til_fg_constraint_paa_fagsak_person.sql new file mode 100644 index 000000000..123ea2ed2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V55__legger_til_fg_constraint_paa_fagsak_person.sql @@ -0,0 +1,2 @@ +ALTER TABLE FAGSAK_PERSON +ADD CONSTRAINT FK_FAGSAK_ID_FAGSAK_PERSON FOREIGN KEY (FK_FAGSAK_ID) REFERENCES FAGSAK (ID); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V56__drop_totrinn_fra_vedtak.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V56__drop_totrinn_fra_vedtak.sql new file mode 100644 index 000000000..ef00044c6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V56__drop_totrinn_fra_vedtak.sql @@ -0,0 +1,4 @@ +ALTER TABLE VEDTAK + DROP COLUMN ansvarlig_saksbehandler; +ALTER TABLE VEDTAK + DROP COLUMN ansvarlig_beslutter; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V57__bostedsadresse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V57__bostedsadresse.sql new file mode 100644 index 000000000..dfbfb6d2f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V57__bostedsadresse.sql @@ -0,0 +1,22 @@ +CREATE TABLE PO_BOSTEDSADRESSE +( + id bigint primary key, + type varchar(20) NOT NULL, + bostedskommune varchar(20), + husnummer varchar(4), + husbokstav varchar(2), + bruksenhetsnummer varchar(10), + adressenavn varchar(30), + kommunenummer varchar(10), + tilleggsnavn varchar(30), + postnummer varchar(5), + opprettet_av varchar(20) DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT current_timestamp NOT NULL, + endret_av varchar(20), + versjon bigint DEFAULT 0 NOT NULL, + endret_tid TIMESTAMP(3) +); + +CREATE SEQUENCE PO_BOSTEDSADRESSE_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; + +ALTER TABLE PO_PERSON ADD COLUMN bostedsadresse_id bigint REFERENCES PO_BOSTEDSADRESSE; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V58__altered_bostedsadresse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V58__altered_bostedsadresse.sql new file mode 100644 index 000000000..58a62ae9c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V58__altered_bostedsadresse.sql @@ -0,0 +1,10 @@ +ALTER TABLE PO_BOSTEDSADRESSE ADD COLUMN matrikkel_id bigint; +ALTER TABLE PO_BOSTEDSADRESSE ALTER COLUMN bostedskommune TYPE varchar; +ALTER TABLE PO_BOSTEDSADRESSE ALTER COLUMN husnummer TYPE varchar; +ALTER TABLE PO_BOSTEDSADRESSE ALTER COLUMN husbokstav TYPE varchar; +ALTER TABLE PO_BOSTEDSADRESSE ALTER COLUMN bruksenhetsnummer TYPE varchar; +ALTER TABLE PO_BOSTEDSADRESSE ALTER COLUMN adressenavn TYPE varchar; +ALTER TABLE PO_BOSTEDSADRESSE ALTER COLUMN kommunenummer TYPE varchar; +ALTER TABLE PO_BOSTEDSADRESSE ALTER COLUMN tilleggsnavn TYPE varchar; +ALTER TABLE PO_BOSTEDSADRESSE ALTER COLUMN postnummer TYPE varchar; +ALTER TABLE PO_BOSTEDSADRESSE ALTER COLUMN opprettet_av TYPE varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V59__journalpost.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V59__journalpost.sql new file mode 100644 index 000000000..d61dcac5b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V59__journalpost.sql @@ -0,0 +1,13 @@ +CREATE TABLE JOURNALPOST +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT REFERENCES BEHANDLING (id) NOT NULL, + JOURNALPOST_ID VARCHAR NOT NULL +); + +CREATE SEQUENCE JOURNALPOST_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +CREATE INDEX ON JOURNALPOST (FK_BEHANDLING_ID); + +INSERT INTO JOURNALPOST(id, FK_BEHANDLING_ID, JOURNALPOST_ID) +SELECT nextval('JOURNALPOST_SEQ'), B.id, B.journalpost_id FROM BEHANDLING B +WHERE journalpost_id IS NOT NULL; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V5__vedtak_aktiv.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V5__vedtak_aktiv.sql new file mode 100644 index 000000000..9c51641b2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V5__vedtak_aktiv.sql @@ -0,0 +1,14 @@ +alter table BEHANDLING_VEDTAK add column aktiv boolean default true; + +CREATE UNIQUE INDEX UIDX_BEHANDLING_VEDTAK_01 + ON BEHANDLING_VEDTAK + ( + (CASE + WHEN aktiv = true + THEN fk_behandling_id + ELSE NULL END), + (CASE + WHEN aktiv = true + THEN aktiv + ELSE NULL END) + ); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V60__vilkaar_resultat_behandling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V60__vilkaar_resultat_behandling.sql new file mode 100644 index 000000000..b27becfdd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V60__vilkaar_resultat_behandling.sql @@ -0,0 +1,2 @@ +ALTER TABLE VILKAR_RESULTAT ADD COLUMN BEHANDLING_ID BIGINT; + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V61__legger_til_constraint_paa_vilkaar_resultat.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V61__legger_til_constraint_paa_vilkaar_resultat.sql new file mode 100644 index 000000000..55517347c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V61__legger_til_constraint_paa_vilkaar_resultat.sql @@ -0,0 +1,5 @@ +ALTER TABLE VILKAR_RESULTAT +RENAME COLUMN BEHANDLING_ID TO FK_BEHANDLING_ID; + +ALTER TABLE VILKAR_RESULTAT +ADD CONSTRAINT FK_BEHANDLING_ID_VILKAR_RESULTAT FOREIGN KEY (FK_BEHANDLING_ID) REFERENCES BEHANDLING (ID); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V62__altered_bostedsadresse_endret_av.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V62__altered_bostedsadresse_endret_av.sql new file mode 100644 index 000000000..cba6ad9ec --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V62__altered_bostedsadresse_endret_av.sql @@ -0,0 +1 @@ +ALTER TABLE PO_BOSTEDSADRESSE ALTER COLUMN endret_av TYPE varchar; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V63__oppdatert_journalpost.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V63__oppdatert_journalpost.sql new file mode 100644 index 000000000..bcc4c3b18 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V63__oppdatert_journalpost.sql @@ -0,0 +1,12 @@ +ALTER TABLE JOURNALPOST + ADD COLUMN OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp, + ADD COLUMN OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL; + +UPDATE JOURNALPOST +SET OPPRETTET_TID = '2020-06-24 00:00:00-00'; + +ALTER TABLE JOURNALPOST + ALTER COLUMN OPPRETTET_TID SET NOT NULL; + +ALTER TABLE behandling + DROP COLUMN journalpost_id; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V64__altered_person_add_sivilstand.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V64__altered_person_add_sivilstand.sql new file mode 100644 index 000000000..ad19c367f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V64__altered_person_add_sivilstand.sql @@ -0,0 +1 @@ +ALTER TABLE PO_PERSON ADD COLUMN sivilstand varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V65__fjerne_aktorid_og_personident.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V65__fjerne_aktorid_og_personident.sql new file mode 100644 index 000000000..ecd5f1f67 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V65__fjerne_aktorid_og_personident.sql @@ -0,0 +1,2 @@ +ALTER TABLE FAGSAK DROP COLUMN AKTOER_ID; +ALTER TABLE FAGSAK DROP COLUMN PERSON_IDENT; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V66__altered_person_add_statsborgerskap_og_medlemskap.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V66__altered_person_add_statsborgerskap_og_medlemskap.sql new file mode 100644 index 000000000..0361cb20b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V66__altered_person_add_statsborgerskap_og_medlemskap.sql @@ -0,0 +1,18 @@ +ALTER TABLE PO_PERSON ADD COLUMN medlemskap varchar DEFAULT 'UKJENT' NOT NULL; + +CREATE TABLE PO_STATSBORGERSKAP +( + id bigint primary key, + fk_po_person_id bigint references PO_PERSON NOT NULL, + landkode VARCHAR(3) DEFAULT 'XUK' NOT NULL, + fom DATE, + tom DATE, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP DEFAULT current_timestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + versjon bigint DEFAULT 0 NOT NULL +); + +CREATE SEQUENCE PO_STATSBORGERSKAP_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +ALTER TABLE PO_PERSON ADD COLUMN statsborgerskap_id bigint REFERENCES PO_STATSBORGERSKAP; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V67__altered_statsborgerskap_oprettet_tid.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V67__altered_statsborgerskap_oprettet_tid.sql new file mode 100644 index 000000000..69e7846cf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V67__altered_statsborgerskap_oprettet_tid.sql @@ -0,0 +1,2 @@ +ALTER TABLE PO_STATSBORGERSKAP ALTER COLUMN opprettet_tid TYPE TIMESTAMP(3); +ALTER TABLE PO_STATSBORGERSKAP ALTER COLUMN opprettet_tid SET DEFAULT localtimestamp; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V68__altered_statsborgerskap_legg_til_medlemskap.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V68__altered_statsborgerskap_legg_til_medlemskap.sql new file mode 100644 index 000000000..a25772f36 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V68__altered_statsborgerskap_legg_til_medlemskap.sql @@ -0,0 +1,3 @@ +ALTER TABLE PO_PERSON DROP COLUMN medlemskap; +ALTER TABLE PO_PERSON DROP COLUMN statsborgerskap_id; +ALTER TABLE PO_STATSBORGERSKAP ADD COLUMN medlemskap varchar DEFAULT 'UKJENT' NOT NULL; \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V69__vilk\303\245r_not_null_behandlingid.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V69__vilk\303\245r_not_null_behandlingid.sql" new file mode 100644 index 000000000..1e326cb7d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V69__vilk\303\245r_not_null_behandlingid.sql" @@ -0,0 +1 @@ +ALTER TABLE VILKAR_RESULTAT ALTER COLUMN FK_BEHANDLING_ID SET NOT NULL; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V6__vedtak_barn.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V6__vedtak_barn.sql new file mode 100644 index 000000000..3a69ea763 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V6__vedtak_barn.sql @@ -0,0 +1,18 @@ +create table BEHANDLING_VEDTAK_BARN +( + id bigint primary key, + fk_behandling_vedtak_id bigint references BEHANDLING_VEDTAK (id) not null, + fk_person_id bigint references po_person (id) not null, + versjon bigint default 0 not null, + opprettet_av VARCHAR(20) default 'VL' not null, + opprettet_tid TIMESTAMP(3) default localtimestamp not null, + stonad_fom TIMESTAMP(3) not null, + stonad_tom TIMESTAMP(3) not null, + belop numeric, + endret_av VARCHAR(20), + endret_tid TIMESTAMP(3) +); + +CREATE SEQUENCE BEHANDLING_VEDTAK_BARN_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; +create index on BEHANDLING_VEDTAK_BARN (fk_behandling_vedtak_id); +create index on BEHANDLING_VEDTAK_BARN (fk_person_id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V70__opprett_opphold_og_relater_til_person.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V70__opprett_opphold_og_relater_til_person.sql new file mode 100644 index 000000000..c093be871 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V70__opprett_opphold_og_relater_til_person.sql @@ -0,0 +1,15 @@ +CREATE TABLE PO_OPPHOLD +( + id bigint primary key, + fk_po_person_id bigint references PO_PERSON NOT NULL, + type VARCHAR NOT NULL, + fom DATE, + tom DATE, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) default localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + versjon bigint DEFAULT 0 NOT NULL +); + +CREATE SEQUENCE PO_OPPHOLD_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V71__vedtak_st\303\270nad_brev_begrunnelser.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V71__vedtak_st\303\270nad_brev_begrunnelser.sql" new file mode 100644 index 000000000..3936de815 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V71__vedtak_st\303\270nad_brev_begrunnelser.sql" @@ -0,0 +1,2 @@ +ALTER TABLE VEDTAK + ADD COLUMN STONAD_BREV_METADATA TEXT; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V72__andel_tilkjent_ytelse_revurdering.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V72__andel_tilkjent_ytelse_revurdering.sql new file mode 100644 index 000000000..dd8b40836 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V72__andel_tilkjent_ytelse_revurdering.sql @@ -0,0 +1,2 @@ +ALTER TABLE ANDEL_TILKJENT_YTELSE ADD COLUMN periode_offset BIGINT; +ALTER TABLE ANDEL_TILKJENT_YTELSE DROP COLUMN fk_person_id; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V73__andel_tilkjent_ytelse_forrige_periode.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V73__andel_tilkjent_ytelse_forrige_periode.sql new file mode 100644 index 000000000..203123159 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V73__andel_tilkjent_ytelse_forrige_periode.sql @@ -0,0 +1 @@ +ALTER TABLE ANDEL_TILKJENT_YTELSE ADD COLUMN forrige_periode_offset BIGINT; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V74__lag_tabell_for_arbeidsforhold.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V74__lag_tabell_for_arbeidsforhold.sql new file mode 100644 index 000000000..d5f640f78 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V74__lag_tabell_for_arbeidsforhold.sql @@ -0,0 +1,16 @@ +CREATE TABLE PO_ARBEIDSFORHOLD +( + id bigint primary key, + fk_po_person_id bigint references PO_PERSON NOT NULL, + arbeidsgiver_id VARCHAR, + arbeidsgiver_type VARCHAR, + fom DATE, + tom DATE, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + versjon bigint DEFAULT 0 NOT NULL +); + +CREATE SEQUENCE PO_ARBEIDSFORHOLD_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V75__vedtaksdato_kan_v\303\246re_null.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V75__vedtaksdato_kan_v\303\246re_null.sql" new file mode 100644 index 000000000..05c1874d8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V75__vedtaksdato_kan_v\303\246re_null.sql" @@ -0,0 +1 @@ +alter table vedtak alter column vedtaksdato drop not null; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V76__foreign_key_fra_totrinnskontroll.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V76__foreign_key_fra_totrinnskontroll.sql new file mode 100644 index 000000000..407907d47 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V76__foreign_key_fra_totrinnskontroll.sql @@ -0,0 +1,2 @@ +ALTER TABLE TOTRINNSKONTROLL + ADD FOREIGN KEY (FK_BEHANDLING_ID) REFERENCES BEHANDLING (ID); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V77__lag_tabell_for_bostedsadresseperiode.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V77__lag_tabell_for_bostedsadresseperiode.sql new file mode 100644 index 000000000..7cd1ca744 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V77__lag_tabell_for_bostedsadresseperiode.sql @@ -0,0 +1,14 @@ +CREATE TABLE PO_BOSTEDSADRESSEPERIODE +( + id bigint primary key, + fk_po_person_id bigint references PO_PERSON NOT NULL, + fom DATE, + tom DATE, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) default localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + versjon bigint DEFAULT 0 NOT NULL +); + +CREATE SEQUENCE PO_BOSTEDSADRESSEPERIODE_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V78__utbetaling_begrunnelse_tabell.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V78__utbetaling_begrunnelse_tabell.sql new file mode 100644 index 000000000..23b03755d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V78__utbetaling_begrunnelse_tabell.sql @@ -0,0 +1,18 @@ +CREATE TABLE UTBETALING_BEGRUNNELSE +( + id bigint primary key NOT NULL, + fk_vedtak_id bigint references vedtak (id), + fom TIMESTAMP(3) NOT NULL, + tom TIMESTAMP(3) NOT NULL, + resultat VARCHAR, + vedtak_begrunnelse VARCHAR, + brev_begrunnelse TEXT +); + +CREATE SEQUENCE UTBETALING_BEGRUNNELSE_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; + +ALTER TABLE UTBETALING_BEGRUNNELSE + ADD CONSTRAINT FK_VEDTAK_ID_UTBETALING_BEGRUNNELSE_ID FOREIGN KEY (FK_VEDTAK_ID) REFERENCES VEDTAK (ID); + +ALTER TABLE VEDTAK + DROP COLUMN STONAD_BREV_METADATA; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V79__utbetaling_begrunnelse_utvidelser.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V79__utbetaling_begrunnelse_utvidelser.sql new file mode 100644 index 000000000..e677292a6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V79__utbetaling_begrunnelse_utvidelser.sql @@ -0,0 +1,14 @@ +ALTER TABLE UTBETALING_BEGRUNNELSE + ADD COLUMN OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL; + +ALTER TABLE UTBETALING_BEGRUNNELSE + ADD COLUMN OPPRETTET_TID TIMESTAMP(3) DEFAULT LOCALTIMESTAMP NOT NULL; + +ALTER TABLE UTBETALING_BEGRUNNELSE + ADD COLUMN ENDRET_AV VARCHAR; + +ALTER TABLE UTBETALING_BEGRUNNELSE + ADD COLUMN ENDRET_TID TIMESTAMP(3); + +ALTER TABLE UTBETALING_BEGRUNNELSE + ADD COLUMN VERSJON BIGINT DEFAULT 0 NOT NULL; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V7__iverksett_vedtak.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V7__iverksett_vedtak.sql new file mode 100644 index 000000000..b9c82d09d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V7__iverksett_vedtak.sql @@ -0,0 +1,3 @@ +alter table PO_PERSON add column foedselsdato TIMESTAMP(3) default CURRENT_TIMESTAMP; + +alter table BEHANDLING_VEDTAK add column status varchar(50) default 'OPPRETTET'; diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V80__person_m\303\245lform.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V80__person_m\303\245lform.sql" new file mode 100644 index 000000000..a1ff15cfd --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V80__person_m\303\245lform.sql" @@ -0,0 +1,3 @@ +ALTER TABLE PO_PERSON + ADD COLUMN MAALFORM VARCHAR(2) DEFAULT 'NB' NOT NULL; + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V81__behandling__og_fagsak_status.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V81__behandling__og_fagsak_status.sql new file mode 100644 index 000000000..b5995a20b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V81__behandling__og_fagsak_status.sql @@ -0,0 +1,31 @@ +UPDATE BEHANDLING +SET STATUS = 'UTREDES' +WHERE STATUS = 'OPPRETTET'; + +UPDATE BEHANDLING +SET STATUS = 'UTREDES' +WHERE STATUS = 'UNDERKJENT_AV_BESLUTTER'; + +UPDATE BEHANDLING +SET STATUS = 'FATTER_VEDTAK' +WHERE STATUS = 'SENDT_TIL_BESLUTTER'; + +UPDATE BEHANDLING +SET STATUS = 'IVERKSETTER_VEDTAK' +WHERE STATUS = 'GODKJENT'; + +UPDATE BEHANDLING +SET STATUS = 'IVERKSETTER_VEDTAK' +WHERE STATUS = 'SENDT_TIL_IVERKSETTING'; + +UPDATE BEHANDLING +SET STATUS = 'IVERKSETTER_VEDTAK' +WHERE STATUS = 'IVERKSATT'; + +UPDATE BEHANDLING +SET STATUS = 'AVSLUTTET' +WHERE STATUS = 'FERDIGSTILT'; + +UPDATE FAGSAK +SET STATUS = 'AVSLUTTET' +WHERE STATUS = 'STANSET'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V82__foedselshendese_pre_lansering.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V82__foedselshendese_pre_lansering.sql new file mode 100644 index 000000000..1c1137c0d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V82__foedselshendese_pre_lansering.sql @@ -0,0 +1,17 @@ +CREATE TABLE FOEDSELSHENDELSE_PRE_LANSERING +( + id BIGINT PRIMARY KEY, + fk_behandling_id BIGINT NOT NULL, + person_ident VARCHAR, + ny_behandling_hendelse TEXT, + filtreringsregler_input TEXT, + filtreringsregler_output TEXT, + vilkaarsvurderinger_for_foedselshendelse TEXT, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) default localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + versjon BIGINT DEFAULT 0 NOT NULL +); + +CREATE SEQUENCE FOEDSELSHENDELSE_PRE_LANSERING_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V83__foedselshendese_ny_behandling_not_null.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V83__foedselshendese_ny_behandling_not_null.sql new file mode 100644 index 000000000..884dc4344 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V83__foedselshendese_ny_behandling_not_null.sql @@ -0,0 +1 @@ +ALTER TABLE FOEDSELSHENDELSE_PRE_LANSERING ALTER COLUMN ny_behandling_hendelse SET NOT NULL; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V84__behandling_resultat_samlet_resultat.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V84__behandling_resultat_samlet_resultat.sql new file mode 100644 index 000000000..83c451665 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V84__behandling_resultat_samlet_resultat.sql @@ -0,0 +1,2 @@ +ALTER TABLE BEHANDLING_RESULTAT DROP COLUMN IF EXISTS forrige_samlede_resultat; +ALTER TABLE BEHANDLING_RESULTAT ADD COLUMN samlet_resultat VARCHAR; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V85__behandling_resultat_set_samlet_resultat.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V85__behandling_resultat_set_samlet_resultat.sql new file mode 100644 index 000000000..7f7e16fb8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V85__behandling_resultat_set_samlet_resultat.sql @@ -0,0 +1 @@ +update BEHANDLING_RESULTAT set samlet_resultat = 'INNVILGET' where samlet_resultat = '' or samlet_resultat is null; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V86__arbeidsfordeling_pa_behandling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V86__arbeidsfordeling_pa_behandling.sql new file mode 100644 index 000000000..657181051 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V86__arbeidsfordeling_pa_behandling.sql @@ -0,0 +1,10 @@ +CREATE TABLE ARBEIDSFORDELING_PA_BEHANDLING +( + id BIGINT PRIMARY KEY, + fk_behandling_id BIGINT UNIQUE NOT NULL, + behandlende_enhet_id VARCHAR NOT NULL, + behandlende_enhet_navn VARCHAR NOT NULL, + manuelt_overstyrt BOOLEAN NOT NULL +); + +CREATE SEQUENCE ARBEIDSFORDELING_PA_BEHANDLING_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V87__autovedtak_evaluering.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V87__autovedtak_evaluering.sql new file mode 100644 index 000000000..ef0e481fb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V87__autovedtak_evaluering.sql @@ -0,0 +1 @@ +ALTER TABLE VILKAR_RESULTAT ADD COLUMN evaluering_aarsak TEXT default ''; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V88__behandling_aarsak_og_automatisk.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V88__behandling_aarsak_og_automatisk.sql new file mode 100644 index 000000000..520a12715 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V88__behandling_aarsak_og_automatisk.sql @@ -0,0 +1,13 @@ +UPDATE BEHANDLING +SET behandling_opprinnelse = 'FØDSELSHENDELSE' +where behandling_opprinnelse = 'AUTOMATISK_VED_FØDSELSHENDELSE'; + +UPDATE BEHANDLING +SET behandling_opprinnelse = 'SØKNAD' +where behandling_opprinnelse = 'MANUELL' + OR behandling_opprinnelse = 'AUTOMATISK_VED_JOURNALFØRING'; + +ALTER TABLE BEHANDLING + RENAME COLUMN behandling_opprinnelse TO opprettet_aarsak; +ALTER TABLE BEHANDLING + ADD COLUMN skal_behandles_automatisk BOOLEAN default false; \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V89__begrunnelse_aareg_til_skj\303\270nnhetsvurdering.sql" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V89__begrunnelse_aareg_til_skj\303\270nnhetsvurdering.sql" new file mode 100644 index 000000000..33d4be0ed --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V89__begrunnelse_aareg_til_skj\303\270nnhetsvurdering.sql" @@ -0,0 +1,2 @@ +update utbetaling_begrunnelse set vedtak_begrunnelse = 'INNVILGET_LOVLIG_OPPHOLD_EØS_BORGER_SKJØNNSMESSIG_VURDERING' + where vedtak_begrunnelse = 'INNVILGET_LOVLIG_OPPHOLD_AAREG'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V8__fjern_periode_fra_vedtak.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V8__fjern_periode_fra_vedtak.sql new file mode 100644 index 000000000..c34f017c1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V8__fjern_periode_fra_vedtak.sql @@ -0,0 +1,2 @@ +alter table BEHANDLING_VEDTAK drop column stonad_fom; +alter table BEHANDLING_VEDTAK drop column stonad_tom; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V90__behandling_gjeldende_for_fremtidig_utbetaling.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V90__behandling_gjeldende_for_fremtidig_utbetaling.sql new file mode 100644 index 000000000..005880c7c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V90__behandling_gjeldende_for_fremtidig_utbetaling.sql @@ -0,0 +1,2 @@ +ALTER TABLE BEHANDLING + RENAME COLUMN gjeldende_for_utbetaling TO gjeldende_for_fremtidig_utbetaling; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V91__oppdater_utbetalingbegrunnelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V91__oppdater_utbetalingbegrunnelse.sql new file mode 100644 index 000000000..44fc11337 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V91__oppdater_utbetalingbegrunnelse.sql @@ -0,0 +1,3 @@ +ALTER TABLE UTBETALING_BEGRUNNELSE + RENAME COLUMN resultat TO begrunnelse_type; + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V92__oppdater_verdi_utbetalingbegrunnelse.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V92__oppdater_verdi_utbetalingbegrunnelse.sql new file mode 100644 index 000000000..56a9e58bd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V92__oppdater_verdi_utbetalingbegrunnelse.sql @@ -0,0 +1,3 @@ +UPDATE UTBETALING_BEGRUNNELSE +SET begrunnelse_type = 'INNVILGELSE' +WHERE begrunnelse_type = 'INNVILGET'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V93__sett_korekte_enheter.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V93__sett_korekte_enheter.sql new file mode 100644 index 000000000..b85b21a18 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V93__sett_korekte_enheter.sql @@ -0,0 +1,7 @@ +UPDATE arbeidsfordeling_pa_behandling +SET behandlende_enhet_id = '4833' +WHERE behandlende_enhet_id = '4817'; + +UPDATE arbeidsfordeling_pa_behandling +SET behandlende_enhet_navn = 'NAV Familie- og pensjonsytelser Oslo 1' +WHERE behandlende_enhet_navn = 'NAV Familie- og pensjonsytelser Steinkjer'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V94__fjern_forrige_vedtak_fra_vedtak.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V94__fjern_forrige_vedtak_fra_vedtak.sql new file mode 100644 index 000000000..8f5aff0f6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V94__fjern_forrige_vedtak_fra_vedtak.sql @@ -0,0 +1,8 @@ +ALTER TABLE VEDTAK + DROP COLUMN fk_forrige_vedtak_id; + +ALTER TABLE VEDTAK + DROP COLUMN ansvarlig_enhet; + +ALTER TABLE VEDTAK + DROP COLUMN stonad_brev_markdown; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V95__legg_til_opplysningsplikt.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V95__legg_til_opplysningsplikt.sql new file mode 100644 index 000000000..fd9a8f030 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V95__legg_til_opplysningsplikt.sql @@ -0,0 +1,14 @@ +CREATE TABLE OPPLYSNINGSPLIKT +( + ID BIGINT PRIMARY KEY, + FK_BEHANDLING_ID BIGINT REFERENCES behandling (id) NOT NULL, + STATUS VARCHAR NOT NULL, + BEGRUNNELSE TEXT, + VERSJON BIGINT DEFAULT 0 NOT NULL, + OPPRETTET_AV VARCHAR DEFAULT 'VL' NOT NULL, + OPPRETTET_TID TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + ENDRET_AV VARCHAR, + ENDRET_TID TIMESTAMP(3) +); + +CREATE SEQUENCE OPPLYSNINGSPLIKT_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V96__opprett_behandling_steg_tilstand.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V96__opprett_behandling_steg_tilstand.sql new file mode 100644 index 000000000..e4ceba1eb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V96__opprett_behandling_steg_tilstand.sql @@ -0,0 +1,27 @@ +DROP TABLE IF EXISTS BEHANDLING_STEG_TILSTAND; +CREATE TABLE BEHANDLING_STEG_TILSTAND +( + id BIGINT primary key, + fk_behandling_id BIGINT references BEHANDLING(id) not null, + behandling_steg VARCHAR not null, + behandling_steg_status VARCHAR default 'IKKE_UTFØRT' not null, + versjon BIGINT default 0 not null, + opprettet_av VARCHAR default 'VL' not null, + opprettet_tid TIMESTAMP(3) default localtimestamp not null, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +) ; + +DROP SEQUENCE IF EXISTS BEHANDLING_STEG_TILSTAND_SEQ; +CREATE SEQUENCE BEHANDLING_STEG_TILSTAND_SEQ INCREMENT BY 50 START WITH 1000000 NO CYCLE; + +INSERT INTO + BEHANDLING_STEG_TILSTAND(id, fk_behandling_id, behandling_steg) + ( + SELECT + nextval('BEHANDLING_STEG_TILSTAND_SEQ'), id, steg + FROM + BEHANDLING + WHERE + steg is not null + ); diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V97__behandling_slette_steg.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V97__behandling_slette_steg.sql new file mode 100644 index 000000000..d4165079c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V97__behandling_slette_steg.sql @@ -0,0 +1 @@ +ALTER TABLE behandling DROP COLUMN steg; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V98__behandlingstegtilstand_sett_avsluttet_til_utfort.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V98__behandlingstegtilstand_sett_avsluttet_til_utfort.sql new file mode 100644 index 000000000..8153c44bd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V98__behandlingstegtilstand_sett_avsluttet_til_utfort.sql @@ -0,0 +1,3 @@ +UPDATE behandling_steg_tilstand +SET behandling_steg_status = 'UTFØRT' +WHERE behandling_steg = 'BEHANDLING_AVSLUTTET'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V99__vilkaar_resultat_enum_endring.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V99__vilkaar_resultat_enum_endring.sql new file mode 100644 index 000000000..a1bb0aeed --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V99__vilkaar_resultat_enum_endring.sql @@ -0,0 +1,11 @@ +UPDATE VILKAR_RESULTAT +SET resultat = 'OPPFYLT' +WHERE resultat = 'JA'; + +UPDATE VILKAR_RESULTAT +SET resultat = 'IKKE_OPPFYLT' +WHERE resultat = 'NEI'; + +UPDATE VILKAR_RESULTAT +SET resultat = 'IKKE_VURDERT' +WHERE resultat = 'KANSKJE'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V9__prosessering.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V9__prosessering.sql new file mode 100644 index 000000000..9836739a9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/db/migration/V9__prosessering.sql @@ -0,0 +1,33 @@ +CREATE TABLE IF NOT EXISTS task ( + id bigint NOT NULL + CONSTRAINT henvendelse_pkey PRIMARY KEY, + payload text NOT NULL, + status varchar(15) DEFAULT 'UBEHANDLET'::character varying NOT NULL, + versjon bigint DEFAULT 0, + opprettet_tid timestamp(3) DEFAULT LOCALTIMESTAMP, + type varchar(100) NOT NULL, + metadata varchar(4000), + trigger_tid timestamp DEFAULT LOCALTIMESTAMP, + avvikstype varchar(50) +); + +CREATE INDEX IF NOT EXISTS henvendelse_status_idx + ON task (status); + +CREATE TABLE IF NOT EXISTS task_logg ( + id bigint NOT NULL + CONSTRAINT henvendelse_logg_pkey PRIMARY KEY, + task_id bigint NOT NULL + CONSTRAINT henvendelse_logg_henvendelse_id_fkey REFERENCES task, + type varchar(15) NOT NULL, + node varchar(100) NOT NULL, + opprettet_tid timestamp(3) DEFAULT LOCALTIMESTAMP, + melding text, + endret_av varchar(100) DEFAULT 'VL'::character varying +); + +CREATE INDEX IF NOT EXISTS henvendelse_logg_henvendelse_id_idx + ON task_logg (task_id); + +CREATE SEQUENCE task_seq INCREMENT BY 50; +CREATE SEQUENCE task_logg_seq INCREMENT BY 50; diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/dokumenter/NAV_33-0005bm-10.2016.pdf b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/dokumenter/NAV_33-0005bm-10.2016.pdf new file mode 100644 index 000000000..54743c0cb Binary files /dev/null and b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/dokumenter/NAV_33-0005bm-10.2016.pdf differ diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/kafka/topic-dev.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/kafka/topic-dev.json new file mode 100644 index 000000000..aa3d6e3ed --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/kafka/topic-dev.json @@ -0,0 +1,37 @@ +{ + "topics": [ + { + "configEntries": { + "retention.ms": -1 + }, + "members": [ + { + "member": "srvfamilie-ba-sak", + "role": "PRODUCER" + }, + { + "member": "srvfamilie-ba-mottak", + "role": "CONSUMER" + }, + { + "member": "S138604", + "role": "MANAGER" + }, + { + "member": "G131744", + "role": "MANAGER" + }, + { + "member": "S154134", + "role": "MANAGER" + }, + { + "member": "N153212", + "role": "MANAGER" + } + ], + "numPartitions": 1, + "topicName": "aapen-barnetrygd-vedtak-v1" + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/kafka/topic-prod.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/kafka/topic-prod.json new file mode 100644 index 000000000..0fec3e1ff --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/kafka/topic-prod.json @@ -0,0 +1,33 @@ +{ + "topics": [ + { + "configEntries": { + "retention.ms": -1 + }, + "members": [ + { + "member": "srvfamilie-ba-sak", + "role": "PRODUCER" + }, + { + "member": "S138604", + "role": "MANAGER" + }, + { + "member": "G131744", + "role": "MANAGER" + }, + { + "member": "S154134", + "role": "MANAGER" + }, + { + "member": "N153212", + "role": "MANAGER" + } + ], + "numPartitions": 1, + "topicName": "aapen-barnetrygd-vedtak-v1" + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/logback-spring.xml b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..bd38e6bc5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/logback-spring.xml @@ -0,0 +1,68 @@ + + + + + + + /secure-logs/secure.log + + /secure-logs/secure.log.%i + 1 + 1 + + + 50MB + + + + + + + + + + + %m%n%xEx + + + + + audit.nais + + 6514 + FAMILIE-BA-SAK + + 128000 + + + + + + + + + + + + + + + + https://dd9a6107bdda4edeb51ece7283f37af4@sentry.gc.nav.no/112 + + + + + + + + + + + + + + + + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/bostedsadresse-utenlandsk.graphql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/bostedsadresse-utenlandsk.graphql new file mode 100644 index 000000000..ce3592321 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/bostedsadresse-utenlandsk.graphql @@ -0,0 +1,13 @@ +query($ident: ID!) {person: hentPerson(ident: $ident) { + bostedsadresse(historikk: false) { + utenlandskAdresse { + adressenavnNummer + bygningEtasjeLeilighet + postboksNummerNavn + postkode + bySted + regionDistriktOmraade + landkode + } + } +}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/doedsfall.graphql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/doedsfall.graphql new file mode 100644 index 000000000..fe22a995e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/doedsfall.graphql @@ -0,0 +1,5 @@ +query($ident: ID!) {person: hentPerson(ident: $ident) { + doedsfall { + doedsdato + } +}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/graphql.config.yml b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/graphql.config.yml new file mode 100644 index 000000000..3044a954a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/graphql.config.yml @@ -0,0 +1,8 @@ +schema: pdl-api-schema.graphql +extensions: + endpoints: + PDL GraphQL Endpoint: + url: https://pdl-api.dev.adeo.no/graphql + headers: + user-agent: familie-ba-sak + introspect: false diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hent-adressebeskyttelse-bolk.graphql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hent-adressebeskyttelse-bolk.graphql new file mode 100644 index 000000000..042f3a3a6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hent-adressebeskyttelse-bolk.graphql @@ -0,0 +1,9 @@ +query($identer: [ID!]!) {personBolk: hentPersonBolk(identer: $identer) { + ident, + person { + adressebeskyttelse { + gradering + } + }, + code +}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hent-adressebeskyttelse.graphql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hent-adressebeskyttelse.graphql new file mode 100644 index 000000000..695f229ac --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hent-adressebeskyttelse.graphql @@ -0,0 +1,5 @@ +query($ident: ID!) {person: hentPerson(ident: $ident) { + adressebeskyttelse { + gradering + } +}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentIdenter.graphql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentIdenter.graphql new file mode 100644 index 000000000..305515968 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentIdenter.graphql @@ -0,0 +1,9 @@ +query($ident: ID!) { + pdlIdenter: hentIdenter(ident: $ident, historikk: true) { + identer{ + ident + historisk + gruppe + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentperson-enkel.graphql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentperson-enkel.graphql new file mode 100644 index 000000000..72f944d0c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentperson-enkel.graphql @@ -0,0 +1,59 @@ +query($ident: ID!) {person: hentPerson(ident: $ident) { + folkeregisteridentifikator { + identifikasjonsnummer + status + type + } + foedsel { + foedselsdato + } + navn { + fornavn + mellomnavn + etternavn + } + kjoenn { + kjoenn + } + adressebeskyttelse { + gradering + } + bostedsadresse(historikk: false) { + angittFlyttedato + gyldigTilOgMed + vegadresse { + matrikkelId + husnummer + husbokstav + bruksenhetsnummer + adressenavn + kommunenummer + tilleggsnavn + postnummer + } + matrikkeladresse { + matrikkelId + bruksenhetsnummer + tilleggsnavn + postnummer + kommunenummer + } + ukjentBosted { + bostedskommune + } + } + sivilstand(historikk: false) { + gyldigFraOgMed + type + } + doedsfall { + doedsdato + } + kontaktinformasjonForDoedsbo(historikk: false) { + adresse { + adresselinje1 + poststedsnavn + postnummer + } + } +}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentperson-med-relasjoner-og-registerinformasjon.graphql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentperson-med-relasjoner-og-registerinformasjon.graphql new file mode 100644 index 000000000..1a8d76d3e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentperson-med-relasjoner-og-registerinformasjon.graphql @@ -0,0 +1,73 @@ +query($ident: ID!) {person: hentPerson(ident: $ident) { + folkeregisteridentifikator { + identifikasjonsnummer + status + type + } + foedsel { + foedselsdato + } + navn { + fornavn + mellomnavn + etternavn + } + kjoenn { + kjoenn + } + forelderBarnRelasjon { + relatertPersonsIdent, + relatertPersonsRolle + } + adressebeskyttelse { + gradering + } + bostedsadresse(historikk: true) { + angittFlyttedato + gyldigTilOgMed + vegadresse { + matrikkelId + husnummer + husbokstav + bruksenhetsnummer + adressenavn + kommunenummer + tilleggsnavn + postnummer + } + matrikkeladresse { + matrikkelId + bruksenhetsnummer + tilleggsnavn + postnummer + kommunenummer + } + ukjentBosted { + bostedskommune + } + } + sivilstand(historikk: true) { + gyldigFraOgMed + type + } + opphold(historikk: true) { + type + oppholdFra + oppholdTil + } + statsborgerskap(historikk: true) { + land + gyldigFraOgMed + gyldigTilOgMed + } + doedsfall { + doedsdato + } + kontaktinformasjonForDoedsbo(historikk: false) { + adresse { + adresselinje1 + poststedsnavn + postnummer + } + } +}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentperson-navn-og-adresse.graphql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentperson-navn-og-adresse.graphql new file mode 100644 index 000000000..e9329c9a0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentperson-navn-og-adresse.graphql @@ -0,0 +1,26 @@ +query($ident: ID!) {person: hentPerson(ident: $ident) { + navn { + fornavn + mellomnavn + etternavn + } + bostedsadresse(historikk: false) { + gyldigTilOgMed + vegadresse { + husnummer + adressenavn + postnummer + } + matrikkeladresse { + postnummer + } + } + folkeregisteridentifikator { + identifikasjonsnummer + status + type + } + foedsel { + foedselsdato + } +}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentperson-relasjoner.graphql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentperson-relasjoner.graphql new file mode 100644 index 000000000..818269728 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/hentperson-relasjoner.graphql @@ -0,0 +1,6 @@ +query($ident: ID!) {person: hentPerson(ident: $ident) { + forelderBarnRelasjon { + relatertPersonsIdent, + relatertPersonsRolle + } +}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/opphold-uten-historikk.graphql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/opphold-uten-historikk.graphql new file mode 100644 index 000000000..12287e876 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/opphold-uten-historikk.graphql @@ -0,0 +1,9 @@ +query($ident: ID!) { + person: hentPerson(ident: $ident) { + opphold { + type + oppholdFra + oppholdTil + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/pdl-api-schema.graphql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/pdl-api-schema.graphql new file mode 100644 index 000000000..d54e5b37f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/pdl-api-schema.graphql @@ -0,0 +1,899 @@ +# This file was generated. Do not edit manually. + +schema { + query: Query +} + +type AdresseCompletionResult { + addressFound: CompletionAdresse + suggestions: [String!]! +} + +type AdresseSearchHit { + matrikkeladresse: MatrikkeladresseResult + score: Float + vegadresse: VegadresseResult +} + +type AdresseSearchResult { + hits: [AdresseSearchHit!]! + pageNumber: Int + totalHits: Int + totalPages: Int +} + +type Adressebeskyttelse { + folkeregistermetadata: Folkeregistermetadata + gradering: AdressebeskyttelseGradering! + metadata: Metadata! +} + +type Bostedsadresse { + angittFlyttedato: Date + coAdressenavn: String + folkeregistermetadata: Folkeregistermetadata + gyldigFraOgMed: DateTime + gyldigTilOgMed: DateTime + matrikkeladresse: Matrikkeladresse + metadata: Metadata! + ukjentBosted: UkjentBosted + utenlandskAdresse: UtenlandskAdresse + vegadresse: Vegadresse +} + +type CompletionAdresse { + matrikkeladresse: MatrikkeladresseResult + vegadresse: VegadresseResult +} + +type DeltBosted { + coAdressenavn: String + folkeregistermetadata: Folkeregistermetadata! + matrikkeladresse: Matrikkeladresse + metadata: Metadata! + sluttdatoForKontrakt: Date + startdatoForKontrakt: Date! + ukjentBosted: UkjentBosted + utenlandskAdresse: UtenlandskAdresse + vegadresse: Vegadresse +} + +type DoedfoedtBarn { + dato: Date + folkeregistermetadata: Folkeregistermetadata! + metadata: Metadata! +} + +type Doedsfall { + doedsdato: Date + folkeregistermetadata: Folkeregistermetadata + metadata: Metadata! +} + +" Endring som har blitt utført på opplysningen. F.eks: Opprett -> Korriger -> Korriger" +type Endring { + hendelseId: String! + """ + + Opphavet til informasjonen. I NAV blir dette satt i forbindelse med registrering (f.eks: Sykehuskassan). + Fra Folkeregisteret får vi opphaven til dems opplysning, altså NAV, UDI, Politiet, Skatteetaten o.l.. Fra Folkeregisteret kan det også være tekniske navn som: DSF_MIGRERING, m.m.. + """ + kilde: String! + " Tidspunktet for registrering." + registrert: DateTime! + " Hvem endringen har blitt utført av, ofte saksbehandler (f.eks Z990200), men kan også være system (f.eks srvXXXX). Denne blir satt til \"Folkeregisteret\" for det vi får fra dem." + registrertAv: String! + " Hvilke system endringen har kommet fra (f.eks srvXXX). Denne blir satt til \"FREG\" for det vi får fra Folkeregisteret." + systemkilde: String! + " Hvilke type endring som har blitt utført." + type: Endringstype! +} + +type FalskIdentitet { + erFalsk: Boolean! + folkeregistermetadata: Folkeregistermetadata + metadata: Metadata! + rettIdentitetErUkjent: Boolean + rettIdentitetVedIdentifikasjonsnummer: String + rettIdentitetVedOpplysninger: FalskIdentitetIdentifiserendeInformasjon +} + +type FalskIdentitetIdentifiserendeInformasjon { + foedselsdato: Date + kjoenn: KjoennType + personnavn: Personnavn! + statsborgerskap: [String!]! +} + +type Foedsel { + foedekommune: String + foedeland: String + foedested: String + foedselsaar: Int + foedselsdato: Date + folkeregistermetadata: Folkeregistermetadata + metadata: Metadata! +} + +type Folkeregisteridentifikator { + folkeregistermetadata: Folkeregistermetadata! + identifikasjonsnummer: String! + metadata: Metadata! + status: String! + type: String! +} + +type Folkeregistermetadata { + aarsak: String + ajourholdstidspunkt: DateTime + gyldighetstidspunkt: DateTime + kilde: String + opphoerstidspunkt: DateTime + sekvens: Int +} + +type Folkeregisterpersonstatus { + folkeregistermetadata: Folkeregistermetadata! + forenkletStatus: String! + metadata: Metadata! + status: String! +} + +type ForelderBarnRelasjon { + folkeregistermetadata: Folkeregistermetadata + metadata: Metadata! + minRolleForPerson: ForelderBarnRelasjonRolle + relatertPersonUtenFolkeregisteridentifikator: RelatertBiPerson + relatertPersonsIdent: String + relatertPersonsRolle: ForelderBarnRelasjonRolle! +} + +type Foreldreansvar { + ansvar: String + ansvarlig: String + ansvarligUtenIdentifikator: RelatertBiPerson + ansvarssubjekt: String + folkeregistermetadata: Folkeregistermetadata + metadata: Metadata! +} + +type Fullmakt { + gyldigFraOgMed: Date! + gyldigTilOgMed: Date! + metadata: Metadata! + motpartsPersonident: String! + motpartsRolle: FullmaktsRolle! + omraader: [String!]! +} + +type GeografiskTilknytning { + gtBydel: String + gtKommune: String + gtLand: String + gtType: GtType! + regel: String! +} + +type HentIdenterBolkResult { + code: String! + ident: String! + identer: [IdentInformasjon!] +} + +type HentPersonBolkResult { + code: String! + ident: String! + person: Person +} + +type IdentInformasjon { + gruppe: IdentGruppe! + historisk: Boolean! + ident: String! +} + +type Identitetsgrunnlag { + folkeregistermetadata: Folkeregistermetadata! + metadata: Metadata! + status: Identitetsgrunnlagsstatus! +} + +type Identliste { + identer: [IdentInformasjon!]! +} + +type InnflyttingTilNorge { + folkeregistermetadata: Folkeregistermetadata + fraflyttingsland: String + fraflyttingsstedIUtlandet: String + metadata: Metadata! +} + +type KartverketAdresse { + id: Long! + matrikkeladresse: KartverketMatrikkeladresse + vegadresse: KartverketVegadresse +} + +type KartverketBydel { + bydelsnavn: String + bydelsnummer: String +} + +type KartverketFylke { + navn: String + nummer: String +} + +type KartverketGrunnkrets { + grunnkretsnavn: String + grunnkretsnummer: String +} + +type KartverketKommune { + fylke: KartverketFylke + navn: String + nummer: String +} + +type KartverketMatrikkeladresse { + adressetilleggsnavn: String + bydel: KartverketBydel + grunnkrets: KartverketGrunnkrets + kortnavn: String + matrikkelnummer: KartverketMatrikkelnummer + postnummeromraade: KartverketPostnummeromraade + representasjonspunkt: KartverketRepresentasjonspunkt + undernummer: Int +} + +type KartverketMatrikkelnummer { + bruksnummer: Int + festenummer: Int + gaardsnummer: Int + kommunenummer: String + seksjonsnummer: Int +} + +type KartverketPostnummeromraade { + postnummer: String + poststed: String +} + +type KartverketRepresentasjonspunkt { + posisjonskvalitet: Int + x: Float + y: Float + z: Float +} + +type KartverketVeg { + adressekode: Int + adressenavn: String + kommune: KartverketKommune + kortnavn: String + stedsnummer: String +} + +type KartverketVegadresse { + adressetilleggsnavn: String + bokstav: String + bydel: KartverketBydel + grunnkrets: KartverketGrunnkrets + kortnavn: String + nummer: Int + postnummeromraade: KartverketPostnummeromraade + representasjonspunkt: KartverketRepresentasjonspunkt + veg: KartverketVeg +} + +type Kjoenn { + folkeregistermetadata: Folkeregistermetadata + kjoenn: KjoennType + metadata: Metadata! +} + +type Kontaktadresse { + coAdressenavn: String + folkeregistermetadata: Folkeregistermetadata + gyldigFraOgMed: DateTime + gyldigTilOgMed: DateTime + metadata: Metadata! + postadresseIFrittFormat: PostadresseIFrittFormat + postboksadresse: Postboksadresse + type: KontaktadresseType! + utenlandskAdresse: UtenlandskAdresse + utenlandskAdresseIFrittFormat: UtenlandskAdresseIFrittFormat + vegadresse: Vegadresse +} + +type KontaktinformasjonForDoedsbo { + adresse: KontaktinformasjonForDoedsboAdresse! + advokatSomKontakt: KontaktinformasjonForDoedsboAdvokatSomKontakt + attestutstedelsesdato: Date! + folkeregistermetadata: Folkeregistermetadata! + metadata: Metadata! + organisasjonSomKontakt: KontaktinformasjonForDoedsboOrganisasjonSomKontakt + personSomKontakt: KontaktinformasjonForDoedsboPersonSomKontakt + skifteform: KontaktinformasjonForDoedsboSkifteform! +} + +type KontaktinformasjonForDoedsboAdresse { + adresselinje1: String! + adresselinje2: String + landkode: String + postnummer: String! + poststedsnavn: String! +} + +type KontaktinformasjonForDoedsboAdvokatSomKontakt { + organisasjonsnavn: String + organisasjonsnummer: String + personnavn: Personnavn! +} + +type KontaktinformasjonForDoedsboOrganisasjonSomKontakt { + kontaktperson: Personnavn + organisasjonsnavn: String! + organisasjonsnummer: String +} + +type KontaktinformasjonForDoedsboPersonSomKontakt { + foedselsdato: Date + identifikasjonsnummer: String + personnavn: Personnavn +} + +type Koordinater { + kvalitet: Int + x: Float + y: Float + z: Float +} + +type Matrikkeladresse { + bruksenhetsnummer: String + kommunenummer: String + koordinater: Koordinater + matrikkelId: Long + postnummer: String + tilleggsnavn: String +} + +type MatrikkeladresseResult { + bruksnummer: String + gaardsnummer: String + kommunenummer: String + matrikkelId: String + postnummer: String + poststed: String + tilleggsnavn: String +} + +type Metadata { + """ + + En liste over alle endringer som har blitt utført over tid. + Vær obs på at denne kan endre seg og man burde takle at det finnes flere korrigeringer i listen, så dersom man ønsker å kun vise den siste, så må man selv filtrere ut dette. + Det kan også ved svært få tilfeller skje at opprett blir fjernet. F.eks ved splitt tilfeller av identer. Dette skal skje i svært få tilfeller. Dersom man ønsker å presentere opprettet tidspunktet, så blir det tidspunktet på den første endringen. + """ + endringer: [Endring!]! + """ + + Feltet betegner hvorvidt dette er en funksjonelt historisk opplysning, for eksempel en tidligere fraflyttet adresse eller et foreldreansvar som er utløpt fordi barnet har fylt 18 år. + I de fleste tilfeller kan dette utledes ved å se på de andre feltene i opplysningen. Dette er imidlertid ikke alltid tilfellet, blant annet for foreldreansvar. + Feltet bør brukes av konsumenter som henter informasjon fra GraphQL med historikk, men som også trenger å utlede gjeldende informasjon. + """ + historisk: Boolean! + " Master refererer til hvem som eier opplysningen, f.eks så har PDL en kopi av Folkeregisteret, da vil master være FREG og eventuelle endringer på dette må gå via Folkeregisteret (API mot dem eller andre rutiner)." + master: String! + """ + + I PDL så får alle forekomster av en opplysning en ID som representerer dens unike forekomst. + F.eks, så vil en Opprett ha ID X, korriger ID Y (der hvor den spesifiserer at den korrigerer X). + Dersom en opplysning ikke er lagret i PDL, så vil denne verdien ikke være utfylt. + """ + opplysningsId: String +} + +type Navn { + etternavn: String! + folkeregistermetadata: Folkeregistermetadata + forkortetNavn: String + fornavn: String! + gyldigFraOgMed: Date + mellomnavn: String + metadata: Metadata! + originaltNavn: OriginaltNavn +} + +type Opphold { + folkeregistermetadata: Folkeregistermetadata! + metadata: Metadata! + oppholdFra: Date + oppholdTil: Date + type: Oppholdstillatelse! +} + +type Oppholdsadresse { + coAdressenavn: String + folkeregistermetadata: Folkeregistermetadata + gyldigFraOgMed: DateTime + gyldigTilOgMed: DateTime + matrikkeladresse: Matrikkeladresse + metadata: Metadata! + oppholdAnnetSted: String + utenlandskAdresse: UtenlandskAdresse + vegadresse: Vegadresse +} + +type OriginaltNavn { + etternavn: String + fornavn: String + mellomnavn: String +} + +type Person { + adressebeskyttelse(historikk: Boolean = false): [Adressebeskyttelse!]! + bostedsadresse(historikk: Boolean = false): [Bostedsadresse!]! + deltBosted(historikk: Boolean = false): [DeltBosted!]! + doedfoedtBarn: [DoedfoedtBarn!]! + doedsfall: [Doedsfall!]! + falskIdentitet: FalskIdentitet + foedsel: [Foedsel!]! + folkeregisteridentifikator(historikk: Boolean = false): [Folkeregisteridentifikator!]! + folkeregisterpersonstatus(historikk: Boolean = false): [Folkeregisterpersonstatus!]! + forelderBarnRelasjon: [ForelderBarnRelasjon!]! + foreldreansvar(historikk: Boolean = false): [Foreldreansvar!]! + fullmakt(historikk: Boolean = false): [Fullmakt!]! + identitetsgrunnlag(historikk: Boolean = false): [Identitetsgrunnlag!]! + innflyttingTilNorge: [InnflyttingTilNorge!]! + kjoenn(historikk: Boolean = false): [Kjoenn!]! + kontaktadresse(historikk: Boolean = false): [Kontaktadresse!]! + kontaktinformasjonForDoedsbo(historikk: Boolean = false): [KontaktinformasjonForDoedsbo!]! + navn(historikk: Boolean = false): [Navn!]! + opphold(historikk: Boolean = false): [Opphold!]! + oppholdsadresse(historikk: Boolean = false): [Oppholdsadresse!]! + sikkerhetstiltak: [Sikkerhetstiltak!]! + sivilstand(historikk: Boolean = false): [Sivilstand!]! + statsborgerskap(historikk: Boolean = false): [Statsborgerskap!]! + telefonnummer: [Telefonnummer!]! + tilrettelagtKommunikasjon: [TilrettelagtKommunikasjon!]! + utenlandskIdentifikasjonsnummer(historikk: Boolean = false): [UtenlandskIdentifikasjonsnummer!]! + utflyttingFraNorge: [UtflyttingFraNorge!]! + vergemaalEllerFremtidsfullmakt(historikk: Boolean = false): [VergemaalEllerFremtidsfullmakt!]! +} + +type PersonSearchHighlight { + " Forteller hvorvidt opplysningen som ga treff er markert som historisk." + historisk: Boolean + """ + + liste med feltene og verdiene som ga treff. + Merk at for fritekst søk så vil disse kunne referere til hjelpe felter som ikke er synelig i resultatene. + """ + matches: [SearchMatch] + """ + + Navn/Sti til opplysningen som ga treff. Merk at dette ikke er feltet som ga treff men opplysningen. + F.eks. hvis du søker på person.navn.fornavn så vil opplysingen være person.navn. + """ + opplysning: String + """ + + Gitt att opplysningen som ga treff har en opplysningsId så vil den returneres her. + alle søk under person skal ha opplysningsId, men søk i identer vil kunne returnere treff uten opplysningsId. + """ + opplysningsId: String +} + +type PersonSearchHit { + " Infromasjon om hva som ga treff i søke resultatet." + highlights: [PersonSearchHighlight] + " forespurte data" + identer(historikk: Boolean = false): [IdentInformasjon!]! + " forespurte data" + person: Person + " Poengsummen elasticsearch har gitt dette resultatet (brukt til feilsøking, og tuning av søk)" + score: Float +} + +type PersonSearchResult { + " treff liste" + hits: [PersonSearchHit!]! + " Side nummer for siden som vises" + pageNumber: Int + " Totalt antall treff (øvre grense er satt til 10 000)" + totalHits: Int + " Totalt antall sider" + totalPages: Int +} + +type Personnavn { + etternavn: String! + fornavn: String! + mellomnavn: String +} + +type PostadresseIFrittFormat { + adresselinje1: String + adresselinje2: String + adresselinje3: String + postnummer: String +} + +type Postboksadresse { + postboks: String! + postbokseier: String + postnummer: String +} + +type Query { + forslagAdresse(parameters: CompletionParameters): AdresseCompletionResult + hentAdresse(matrikkelId: ID!): KartverketAdresse + hentGeografiskTilknytning(ident: ID!): GeografiskTilknytning + hentGeografiskTilknytningBolk(identer: [ID!]!): [hentGeografiskTilknytningBolkResult!]! + hentIdenter(grupper: [IdentGruppe!], historikk: Boolean = false, ident: ID!): Identliste + hentIdenterBolk(grupper: [IdentGruppe!], historikk: Boolean = false, identer: [ID!]!): [HentIdenterBolkResult!]! + hentPerson(ident: ID!): Person + hentPersonBolk(identer: [ID!]!): [HentPersonBolkResult!]! + sokAdresse(criteria: [Criterion], paging: Paging): AdresseSearchResult + sokPerson(criteria: [Criterion], paging: Paging): PersonSearchResult +} + +type RelatertBiPerson { + foedselsdato: Date + kjoenn: KjoennType + navn: Personnavn + statsborgerskap: String +} + +type SearchMatch { + " feltnavn med sti til feltet so ga treff." + field: String! + " Verdien som ga treff" + fragments: [String] + type: String +} + +type Sikkerhetstiltak { + beskrivelse: String! + gyldigFraOgMed: Date! + gyldigTilOgMed: Date! + kontaktperson: SikkerhetstiltakKontaktperson + metadata: Metadata! + tiltakstype: String! +} + +type SikkerhetstiltakKontaktperson { + enhet: String! + personident: String! +} + +type Sivilstand { + bekreftelsesdato: Date + folkeregistermetadata: Folkeregistermetadata + gyldigFraOgMed: Date + metadata: Metadata! + relatertVedSivilstand: String + type: Sivilstandstype! +} + +type Statsborgerskap { + bekreftelsesdato: Date + folkeregistermetadata: Folkeregistermetadata + gyldigFraOgMed: Date + gyldigTilOgMed: Date + land: String! + metadata: Metadata! +} + +type Telefonnummer { + landskode: String! + metadata: Metadata! + nummer: String! + prioritet: Int! +} + +type TilrettelagtKommunikasjon { + metadata: Metadata! + talespraaktolk: Tolk + tegnspraaktolk: Tolk +} + +type Tolk { + spraak: String +} + +type UkjentBosted { + bostedskommune: String +} + +type UtenlandskAdresse { + adressenavnNummer: String + bySted: String + bygningEtasjeLeilighet: String + landkode: String! + postboksNummerNavn: String + postkode: String + regionDistriktOmraade: String +} + +type UtenlandskAdresseIFrittFormat { + adresselinje1: String + adresselinje2: String + adresselinje3: String + byEllerStedsnavn: String + landkode: String! + postkode: String +} + +type UtenlandskIdentifikasjonsnummer { + folkeregistermetadata: Folkeregistermetadata + identifikasjonsnummer: String! + metadata: Metadata! + opphoert: Boolean! + utstederland: String! +} + +type UtflyttingFraNorge { + folkeregistermetadata: Folkeregistermetadata + metadata: Metadata! + tilflyttingsland: String + tilflyttingsstedIUtlandet: String + utflyttingsdato: Date +} + +type Vegadresse { + adressenavn: String + bruksenhetsnummer: String + bydelsnummer: String + husbokstav: String + husnummer: String + kommunenummer: String + koordinater: Koordinater + matrikkelId: Long + postnummer: String + tilleggsnavn: String +} + +type VegadresseResult { + adressekode: String + adressenavn: String + bydelsnavn: String + bydelsnummer: String + fylkesnavn: String + fylkesnummer: String + husbokstav: String + husnummer: Int + kommunenavn: String + kommunenummer: String + matrikkelId: String + postnummer: String + poststed: String + tilleggsnavn: String +} + +type VergeEllerFullmektig { + motpartsPersonident: String + navn: Personnavn + omfang: String + omfangetErInnenPersonligOmraade: Boolean! +} + +type VergemaalEllerFremtidsfullmakt { + embete: String + folkeregistermetadata: Folkeregistermetadata + metadata: Metadata! + type: String + vergeEllerFullmektig: VergeEllerFullmektig! +} + +type hentGeografiskTilknytningBolkResult { + code: String! + geografiskTilknytning: GeografiskTilknytning + ident: String! +} + +enum AdressebeskyttelseGradering { + FORTROLIG + STRENGT_FORTROLIG + STRENGT_FORTROLIG_UTLAND + UGRADERT +} + +enum Direction { + ASC + DESC +} + +enum Endringstype { + KORRIGER + OPPHOER + OPPRETT +} + +enum Familierelasjonsrolle { + BARN + FAR + MEDMOR + MOR +} + +enum ForelderBarnRelasjonRolle { + BARN + FAR + MEDMOR + MOR +} + +enum FullmaktsRolle { + FULLMAKTSGIVER + FULLMEKTIG +} + +enum GtType { + BYDEL + KOMMUNE + UDEFINERT + UTLAND +} + +enum IdentGruppe { + AKTORID + FOLKEREGISTERIDENT + NPID +} + +enum Identitetsgrunnlagsstatus { + IKKE_KONTROLLERT + INGEN_STATUS + KONTROLLERT +} + +enum KjoennType { + KVINNE + MANN + UKJENT +} + +enum KontaktadresseType { + Innland + Utland +} + +enum KontaktinformasjonForDoedsboSkifteform { + ANNET + OFFENTLIG +} + +enum Oppholdstillatelse { + MIDLERTIDIG + OPPLYSNING_MANGLER + PERMANENT +} + +enum Sivilstandstype { + ENKE_ELLER_ENKEMANN + GIFT + GJENLEVENDE_PARTNER + REGISTRERT_PARTNER + SEPARERT + SEPARERT_PARTNER + SKILT + SKILT_PARTNER + UGIFT + UOPPGITT +} + +"Format: YYYY-MM-DD (ISO-8601), example: 2017-11-24" +scalar Date + +"Format: YYYY-MM-DDTHH:mm:SS (ISO-8601), example: 2011-12-03T10:15:30" +scalar DateTime + +"Long type" +scalar Long + +input CompletionFieldValue { + fieldName: String! + fieldValue: String +} + +input CompletionParameters { + completionField: String! + fieldValues: [CompletionFieldValue]! + maxSuggestions: Int +} + +input Criterion { + " Feltnavn ikludert sti til ønsket felt (Eksempel: person.navn.fornavn)" + fieldName: String! + """ + + Søk i historiske data + true = søker kun i historiske data. + false = søker kun i gjeldende data. + null = søke i både historiske og gjeldende data. + """ + searchHistorical: Boolean + searchRule: SearchRule! +} + +input Paging { + " Hvilken side i resultatsettet man ønsker vist." + pageNumber: Int = 1 + " antall treff per side (maks 100)" + resultsPerPage: Int = 10 + """ + + Liste over felter man ønsker resultatene sortert etter + Standard er "score". Score er poengsummen Elasticsearch tildeler hvert resultat. + """ + sortBy: [SearchSorting] +} + +input SearchRule { + " Brukes til søke etter datoer som kommer etter opgitt dato." + after: String + " Brukes til søke etter datoer som kommer før opgitt dato." + before: String + " Boost brukes til å gi ett søkekriterie høyere eller lavere vektlegging en de andre søke kriteriene." + boost: Float + " [Flag] Kan brukes til å overstyre standard oppførsellen for søk i felter (standard er case insensitive)" + caseSensitive: Boolean + " Gir treff når opgitt felt inneholder en eller flere ord fra input verdien." + contains: String + " [Flag] Brukes til å deaktivere fonetisk søk feltene som har dette som standard (Navn)" + disablePhonetic: Boolean + " Begrenser treff til kun de hvor felt har input verdi" + equals: String + " Sjekker om feltet finnes / at det ikke har en null verdi." + exists: String + """ + + Søk fra og med (se fromExcluding for bare fra men ikke med) + kan benyttes på tall og dato + """ + from: String + """ + + Søk fra men ikke med oppgitt verdi + kan benyttes på tall og dato + """ + fromExcluding: String + " Søk som gir treff også for små variasjoner i skrivemåte" + fuzzy: String + " Brukes til å søke i tall og finner verdier som er størren en input verdi." + greaterThan: String + " Brukes til å søke i tall og finner verdier som er mindre en input verdi." + lessThan: String + " Filtrerer bort treff hvor felt inneholder input verdi" + notEquals: String + " Søk som gir tilfeldig poengsum til hvert treff (kun ment til generering av testdata)" + random: String + " Regex søk for spesielle situasjoner (Dette er en treg opprasjon og bør ikke brukes)" + regex: String + " Gir treff når opgitt feltstarter med opgitt verdi." + startsWith: String + """ + + Søk til og med (se toExcluding for bare til men ikke med) + kan benyttes på tall og dato + """ + to: String + """ + + Søk til men ikke med oppgitt verdi + kan benyttes på tall og dato + """ + toExcluding: String + " Bruk \"?\" som wildcard for enkelt tegn, og \"*\" som wildcard for 0 eller flere tegn." + wildcard: String +} + +input SearchSorting { + direction: Direction! + " Feltnavn ikludert sti til ønsket felt (eksepmel: person.navn.fornavn)" + fieldName: String! +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/statsborgerskap-uten-historikk.graphql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/statsborgerskap-uten-historikk.graphql new file mode 100644 index 000000000..2ff78ff5e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/statsborgerskap-uten-historikk.graphql @@ -0,0 +1,10 @@ +query($ident: ID!) { + person: hentPerson(ident: $ident) { + statsborgerskap { + bekreftelsesdato + land + gyldigFraOgMed + gyldigTilOgMed + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/verge.graphql b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/verge.graphql new file mode 100644 index 000000000..da8db23ec --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/main/resources/pdl/verge.graphql @@ -0,0 +1,5 @@ +query($ident: ID!) {person: hentPerson(ident: $ident) { + vergemaalEllerFremtidsfullmakt(historikk: false) { + type + } +}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/CacheTestUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/CacheTestUtil.kt new file mode 100644 index 000000000..884818f4e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/CacheTestUtil.kt @@ -0,0 +1,5 @@ +package no.nav.familie.ba.sak.common + +import org.springframework.cache.CacheManager + +fun CacheManager.clearAllCaches() = this.cacheNames.forEach { getCache(it)?.clear() } diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/DataGenerator.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/DataGenerator.kt new file mode 100644 index 000000000..be36b021d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/DataGenerator.kt @@ -0,0 +1,1350 @@ +package no.nav.familie.ba.sak.common + +import no.nav.commons.foedselsnummer.testutils.FoedselsnummerGenerator +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.datagenerator.vedtak.lagVedtaksbegrunnelse +import no.nav.familie.ba.sak.ekstern.restDomene.BarnMedOpplysninger +import no.nav.familie.ba.sak.ekstern.restDomene.InstitusjonInfo +import no.nav.familie.ba.sak.ekstern.restDomene.RestPerson +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerInstitusjonOgVerge +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.ekstern.restDomene.SøkerMedOpplysninger +import no.nav.familie.ba.sak.ekstern.restDomene.SøknadDTO +import no.nav.familie.ba.sak.ekstern.restDomene.VergeInfo +import no.nav.familie.ba.sak.ekstern.restDomene.tilDto +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPerson +import no.nav.familie.ba.sak.integrasjoner.økonomi.sats +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.initStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.EndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.brev.domene.EndretUtbetalingsperiodeDeltBostedTriggere +import no.nav.familie.ba.sak.kjerne.brev.domene.EndretUtbetalingsperiodeTrigger +import no.nav.familie.ba.sak.kjerne.brev.domene.RestSanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityPeriodeResultat +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityVilkår +import no.nav.familie.ba.sak.kjerne.brev.domene.Tema +import no.nav.familie.ba.sak.kjerne.brev.domene.Valgbarhet +import no.nav.familie.ba.sak.kjerne.brev.domene.VilkårRolle +import no.nav.familie.ba.sak.kjerne.brev.domene.VilkårTrigger +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.BrevPeriodeType +import no.nav.familie.ba.sak.kjerne.brev.domene.ØvrigTrigger +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårRegelverkResultat +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Dødsfall +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonEnkel +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrMatrikkeladresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.domene.PersonIdent +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.sivilstand.GrSivilstand +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.GrStatsborgerskap +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.Personident +import no.nav.familie.ba.sak.kjerne.steg.FØRSTE_STEG +import no.nav.familie.ba.sak.kjerne.steg.StatusFraOppdragMedTask +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.steg.domene.JournalførVedtaksbrevDTO +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.BarnetsBostedsland +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.TriggesAv +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.EØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.Vedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksbegrunnelseFritekst +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Utbetalingsperiode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.UtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.UtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurderingType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.gjelderAlltidFraBarnetsFødselsdato +import no.nav.familie.ba.sak.task.DistribuerDokumentDTO +import no.nav.familie.ba.sak.task.JournalførVedtaksbrevTask +import no.nav.familie.ba.sak.task.StatusFraOppdragTask +import no.nav.familie.ba.sak.task.dto.FAGSYSTEM +import no.nav.familie.ba.sak.task.dto.IverksettingTaskDTO +import no.nav.familie.ba.sak.task.dto.StatusFraOppdragDTO +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.prosessering.domene.Task +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.YearMonth +import java.util.Properties +import kotlin.math.abs +import kotlin.random.Random + +val fødselsnummerGenerator = FoedselsnummerGenerator() + +fun randomFnr(): String = fødselsnummerGenerator.foedselsnummer().asString +fun randomPersonident(aktør: Aktør, fnr: String = randomFnr()): Personident = + Personident(fødselsnummer = fnr, aktør = aktør) + +fun randomAktør(fnr: String = randomFnr()): Aktør = + Aktør(Random.nextLong(1000_000_000_000, 31_121_299_99999).toString()).also { + it.personidenter.add( + randomPersonident(it, fnr), + ) + } + +private var gjeldendeVedtakId: Long = abs(Random.nextLong(10000000)) +private var gjeldendeBehandlingId: Long = abs(Random.nextLong(10000000)) +private var gjeldendePersonId: Long = abs(Random.nextLong(10000000)) +private var gjeldendeUtvidetVedtaksperiodeId: Long = abs(Random.nextLong(10000000)) +private const val ID_INKREMENT = 50 + +fun nesteVedtakId(): Long { + gjeldendeVedtakId += ID_INKREMENT + return gjeldendeVedtakId +} + +fun nesteBehandlingId(): Long { + gjeldendeBehandlingId += ID_INKREMENT + return gjeldendeBehandlingId +} + +fun nestePersonId(): Long { + gjeldendePersonId += ID_INKREMENT + return gjeldendePersonId +} + +fun nesteUtvidetVedtaksperiodeId(): Long { + gjeldendeUtvidetVedtaksperiodeId += ID_INKREMENT + return gjeldendeUtvidetVedtaksperiodeId +} + +fun defaultFagsak(aktør: Aktør = tilAktør(randomFnr())) = Fagsak( + 1, + aktør = aktør, +) + +fun lagBehandling( + fagsak: Fagsak = defaultFagsak(), + behandlingKategori: BehandlingKategori = BehandlingKategori.NASJONAL, + behandlingType: BehandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak: BehandlingÅrsak = BehandlingÅrsak.SØKNAD, + skalBehandlesAutomatisk: Boolean = false, + førsteSteg: StegType = FØRSTE_STEG, + resultat: Behandlingsresultat = Behandlingsresultat.IKKE_VURDERT, + underkategori: BehandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + status: BehandlingStatus = initStatus(), + aktivertTid: LocalDateTime = LocalDateTime.now(), +) = + Behandling( + id = nesteBehandlingId(), + fagsak = fagsak, + skalBehandlesAutomatisk = skalBehandlesAutomatisk, + type = behandlingType, + kategori = behandlingKategori, + underkategori = underkategori, + opprettetÅrsak = årsak, + resultat = resultat, + status = status, + aktivertTidspunkt = aktivertTid, + ).also { + it.behandlingStegTilstand.add(BehandlingStegTilstand(0, it, førsteSteg)) + } + +fun tilfeldigPerson( + fødselsdato: LocalDate = LocalDate.now(), + personType: PersonType = PersonType.BARN, + kjønn: Kjønn = Kjønn.MANN, + aktør: Aktør = randomAktør(), + personId: Long = nestePersonId(), + dødsfall: Dødsfall? = null, +) = + Person( + id = personId, + aktør = aktør, + fødselsdato = fødselsdato, + type = personType, + personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 0), + navn = "", + kjønn = kjønn, + målform = Målform.NB, + dødsfall = dødsfall, + ).apply { sivilstander = mutableListOf(GrSivilstand(type = SIVILSTAND.UGIFT, person = this)) } + +fun Person.tilPersonEnkel() = + PersonEnkel(this.type, this.aktør, this.fødselsdato, this.dødsfall?.dødsfallDato, this.målform) + +fun tilfeldigSøker( + fødselsdato: LocalDate = LocalDate.now(), + personType: PersonType = PersonType.SØKER, + kjønn: Kjønn = Kjønn.MANN, + aktør: Aktør = randomAktør(), +) = + Person( + id = nestePersonId(), + aktør = aktør, + fødselsdato = fødselsdato, + type = personType, + personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 0), + navn = "", + kjønn = kjønn, + målform = Målform.NB, + ).apply { sivilstander = mutableListOf(GrSivilstand(type = SIVILSTAND.UGIFT, person = this)) } + +fun lagVedtak(behandling: Behandling = lagBehandling(), stønadBrevPdF: ByteArray? = null) = + Vedtak( + id = nesteVedtakId(), + behandling = behandling, + vedtaksdato = LocalDateTime.now(), + stønadBrevPdF = stønadBrevPdF, + ) + +fun lagAndelTilkjentYtelse( + fom: YearMonth, + tom: YearMonth, + ytelseType: YtelseType = YtelseType.ORDINÆR_BARNETRYGD, + beløp: Int = sats(ytelseType), + behandling: Behandling = lagBehandling(), + person: Person = tilfeldigPerson(), + aktør: Aktør = person.aktør, + periodeIdOffset: Long? = null, + forrigeperiodeIdOffset: Long? = null, + tilkjentYtelse: TilkjentYtelse? = null, + prosent: BigDecimal = BigDecimal(100), + kildeBehandlingId: Long? = behandling.id, + differanseberegnetPeriodebeløp: Int? = null, + id: Long = 0, + sats: Int = sats(ytelseType), +): AndelTilkjentYtelse { + return AndelTilkjentYtelse( + id = id, + aktør = aktør, + behandlingId = behandling.id, + tilkjentYtelse = tilkjentYtelse ?: lagInitiellTilkjentYtelse(behandling), + kalkulertUtbetalingsbeløp = beløp, + nasjonaltPeriodebeløp = beløp, + stønadFom = fom, + stønadTom = tom, + type = ytelseType, + periodeOffset = periodeIdOffset, + forrigePeriodeOffset = forrigeperiodeIdOffset, + sats = sats, + prosent = prosent, + kildeBehandlingId = kildeBehandlingId, + differanseberegnetPeriodebeløp = differanseberegnetPeriodebeløp, + ) +} + +fun lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom: YearMonth, + tom: YearMonth, + ytelseType: YtelseType = YtelseType.ORDINÆR_BARNETRYGD, + beløp: Int = sats(ytelseType), + behandling: Behandling = lagBehandling(), + person: Person = tilfeldigPerson(), + aktør: Aktør = person.aktør, + periodeIdOffset: Long? = null, + forrigeperiodeIdOffset: Long? = null, + tilkjentYtelse: TilkjentYtelse? = null, + prosent: BigDecimal = BigDecimal(100), + endretUtbetalingAndeler: List = emptyList(), + differanseberegnetPeriodebeløp: Int? = null, + sats: Int = beløp, +): AndelTilkjentYtelseMedEndreteUtbetalinger { + val aty = AndelTilkjentYtelse( + aktør = aktør, + behandlingId = behandling.id, + tilkjentYtelse = tilkjentYtelse ?: lagInitiellTilkjentYtelse(behandling), + kalkulertUtbetalingsbeløp = beløp, + nasjonaltPeriodebeløp = beløp, + stønadFom = fom, + stønadTom = tom, + type = ytelseType, + periodeOffset = periodeIdOffset, + forrigePeriodeOffset = forrigeperiodeIdOffset, + sats = sats, + prosent = prosent, + differanseberegnetPeriodebeløp = differanseberegnetPeriodebeløp, + ) + + return AndelTilkjentYtelseMedEndreteUtbetalinger(aty, endretUtbetalingAndeler) +} + +fun lagAndelTilkjentYtelseUtvidet( + fom: String, + tom: String, + ytelseType: YtelseType, + beløp: Int = sats(ytelseType), + behandling: Behandling = lagBehandling(), + person: Person = tilfeldigSøker(), + periodeIdOffset: Long? = null, + forrigeperiodeIdOffset: Long? = null, + tilkjentYtelse: TilkjentYtelse? = null, +): AndelTilkjentYtelse { + return AndelTilkjentYtelse( + aktør = person.aktør, + behandlingId = behandling.id, + tilkjentYtelse = tilkjentYtelse ?: lagInitiellTilkjentYtelse(behandling), + kalkulertUtbetalingsbeløp = beløp, + nasjonaltPeriodebeløp = beløp, + stønadFom = årMnd(fom), + stønadTom = årMnd(tom), + type = ytelseType, + periodeOffset = periodeIdOffset, + forrigePeriodeOffset = forrigeperiodeIdOffset, + sats = beløp, + prosent = BigDecimal(100), + ) +} + +fun lagInitiellTilkjentYtelse( + behandling: Behandling = lagBehandling(), + utbetalingsoppdrag: String? = null, +): TilkjentYtelse { + return TilkjentYtelse( + behandling = behandling, + opprettetDato = LocalDate.now(), + endretDato = LocalDate.now(), + utbetalingsoppdrag = utbetalingsoppdrag, + ) +} + +fun lagTestPersonopplysningGrunnlag( + behandlingId: Long, + vararg personer: Person, +): PersonopplysningGrunnlag { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandlingId) + + personopplysningGrunnlag.personer.addAll( + personer.map { it.copy(personopplysningGrunnlag = personopplysningGrunnlag) }, + ) + return personopplysningGrunnlag +} + +fun lagTestPersonopplysningGrunnlag( + behandlingId: Long, + søkerPersonIdent: String, + barnasIdenter: List, + barnasFødselsdatoer: List = barnasIdenter.map { LocalDate.of(2019, 1, 1) }, + søkerFødselsdato: LocalDate = LocalDate.of(1987, 1, 1), + søkerAktør: Aktør = tilAktør(søkerPersonIdent).also { + it.personidenter.add( + Personident( + fødselsnummer = søkerPersonIdent, + aktør = it, + aktiv = søkerPersonIdent == it.personidenter.first().fødselsnummer, + ), + ) + }, + barnAktør: List = barnasIdenter.map { fødselsnummer -> + tilAktør(fødselsnummer).also { + it.personidenter.add( + Personident( + fødselsnummer = fødselsnummer, + aktør = it, + aktiv = fødselsnummer == it.personidenter.first().fødselsnummer, + ), + ) + } + }, +): PersonopplysningGrunnlag { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandlingId) + val bostedsadresse = GrMatrikkeladresse( + matrikkelId = null, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ) + + val søker = Person( + aktør = søkerAktør, + type = PersonType.SØKER, + personopplysningGrunnlag = personopplysningGrunnlag, + fødselsdato = søkerFødselsdato, + navn = "", + kjønn = Kjønn.KVINNE, + ).also { søker -> + søker.statsborgerskap = + mutableListOf(GrStatsborgerskap(landkode = "NOR", medlemskap = Medlemskap.NORDEN, person = søker)) + søker.bostedsadresser = mutableListOf(bostedsadresse.apply { person = søker }) + søker.sivilstander = mutableListOf( + GrSivilstand( + type = SIVILSTAND.GIFT, + person = søker, + ), + ) + } + personopplysningGrunnlag.personer.add(søker) + + barnAktør.mapIndexed { index, aktør -> + personopplysningGrunnlag.personer.add( + Person( + aktør = aktør, + type = PersonType.BARN, + personopplysningGrunnlag = personopplysningGrunnlag, + fødselsdato = barnasFødselsdatoer.get(index), + navn = "", + kjønn = Kjønn.MANN, + ).also { barn -> + barn.statsborgerskap = + mutableListOf(GrStatsborgerskap(landkode = "NOR", medlemskap = Medlemskap.NORDEN, person = barn)) + barn.bostedsadresser = mutableListOf(bostedsadresse.apply { person = barn }) + barn.sivilstander = mutableListOf( + GrSivilstand( + type = SIVILSTAND.UGIFT, + person = barn, + ), + ) + }, + ) + } + return personopplysningGrunnlag +} + +fun PersonopplysningGrunnlag.tilPersonEnkelSøkerOgBarn() = + this.søkerOgBarn.map { it.tilPersonEnkel() } + +fun dato(s: String) = LocalDate.parse(s) +fun årMnd(s: String) = YearMonth.parse(s) + +fun nyOrdinærBehandling( + søkersIdent: String, + årsak: BehandlingÅrsak = BehandlingÅrsak.SØKNAD, + fagsakId: Long, +): NyBehandling = + NyBehandling( + søkersIdent = søkersIdent, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingÅrsak = årsak, + søknadMottattDato = if (årsak == BehandlingÅrsak.SØKNAD) LocalDate.now() else null, + fagsakId = fagsakId, + ) + +fun nyRevurdering(søkersIdent: String, fagsakId: Long): NyBehandling = NyBehandling( + søkersIdent = søkersIdent, + behandlingType = BehandlingType.REVURDERING, + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + søknadMottattDato = LocalDate.now(), + fagsakId = fagsakId, +) + +fun lagSøknadDTO( + søkerIdent: String, + barnasIdenter: List, + underkategori: BehandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, +): SøknadDTO { + return SøknadDTO( + underkategori = underkategori.tilDto(), + søkerMedOpplysninger = SøkerMedOpplysninger( + ident = søkerIdent, + ), + barnaMedOpplysninger = barnasIdenter.map { + BarnMedOpplysninger( + ident = it, + ) + }, + endringAvOpplysningerBegrunnelse = "", + ) +} + +fun lagPersonResultaterForSøkerOgToBarn( + vilkårsvurdering: Vilkårsvurdering, + søkerAktør: Aktør, + barn1Aktør: Aktør, + barn2Aktør: Aktør, + stønadFom: LocalDate, + stønadTom: LocalDate, + erDeltBosted: Boolean = false, +): Set { + return setOf( + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson(type = PersonType.SØKER, aktør = søkerAktør), + resultat = Resultat.OPPFYLT, + periodeFom = stønadFom, + periodeTom = stønadTom, + lagFullstendigVilkårResultat = true, + personType = PersonType.SØKER, + ), + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson( + type = PersonType.BARN, + aktør = barn1Aktør, + fødselsdato = stønadFom, + ), + resultat = Resultat.OPPFYLT, + periodeFom = stønadFom, + periodeTom = stønadTom, + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + erDeltBosted = erDeltBosted, + ), + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson(type = PersonType.BARN, aktør = barn2Aktør, fødselsdato = stønadFom), + resultat = Resultat.OPPFYLT, + periodeFom = stønadFom, + periodeTom = stønadTom, + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + erDeltBosted = erDeltBosted, + ), + ) +} + +fun lagPersonResultat( + vilkårsvurdering: Vilkårsvurdering, + person: Person, + resultat: Resultat, + periodeFom: LocalDate?, + periodeTom: LocalDate?, + lagFullstendigVilkårResultat: Boolean = false, + personType: PersonType = PersonType.BARN, + vilkårType: Vilkår = Vilkår.BOSATT_I_RIKET, + erDeltBosted: Boolean = false, + erDeltBostedSkalIkkeDeles: Boolean = false, + erEksplisittAvslagPåSøknad: Boolean? = null, +): PersonResultat { + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = person.aktør, + ) + + if (lagFullstendigVilkårResultat) { + personResultat.setSortedVilkårResultater( + Vilkår.hentVilkårFor( + personType = personType, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ).map { + VilkårResultat( + personResultat = personResultat, + periodeFom = if (it.gjelderAlltidFraBarnetsFødselsdato()) person.fødselsdato else periodeFom, + periodeTom = periodeTom, + vilkårType = it, + resultat = resultat, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOfNotNull( + when { + erDeltBosted && it == Vilkår.BOR_MED_SØKER -> UtdypendeVilkårsvurdering.DELT_BOSTED + erDeltBostedSkalIkkeDeles && it == Vilkår.BOR_MED_SØKER -> UtdypendeVilkårsvurdering.DELT_BOSTED_SKAL_IKKE_DELES + else -> null + }, + ), + erEksplisittAvslagPåSøknad = erEksplisittAvslagPåSøknad, + ) + }.toSet(), + ) + } else { + personResultat.setSortedVilkårResultater( + setOf( + VilkårResultat( + personResultat = personResultat, + periodeFom = periodeFom, + periodeTom = periodeTom, + vilkårType = vilkårType, + resultat = resultat, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + erEksplisittAvslagPåSøknad = erEksplisittAvslagPåSøknad, + ), + ), + ) + } + return personResultat +} + +fun vurderVilkårsvurderingTilInnvilget(vilkårsvurdering: Vilkårsvurdering, barn: Person) { + vilkårsvurdering.personResultater.filter { it.aktør == barn.aktør }.forEach { personResultat -> + personResultat.vilkårResultater.forEach { + if (it.vilkårType == Vilkår.UNDER_18_ÅR) { + it.resultat = Resultat.OPPFYLT + it.periodeFom = barn.fødselsdato + it.periodeTom = barn.fødselsdato.plusYears(18) + } else { + it.resultat = Resultat.OPPFYLT + it.periodeFom = LocalDate.now() + } + } + } +} + +fun lagVilkårsvurdering( + søkerAktør: Aktør, + behandling: Behandling, + resultat: Resultat, + søkerPeriodeFom: LocalDate? = LocalDate.now().minusMonths(1), + søkerPeriodeTom: LocalDate? = LocalDate.now().plusYears(2), +): Vilkårsvurdering { + val vilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + ) + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = søkerAktør, + ) + personResultat.setSortedVilkårResultater( + setOf( + VilkårResultat( + personResultat = personResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = resultat, + periodeFom = søkerPeriodeFom, + periodeTom = søkerPeriodeTom, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + VilkårResultat( + personResultat = personResultat, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = resultat, + periodeFom = søkerPeriodeFom, + periodeTom = søkerPeriodeTom, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + ), + ) + personResultat.andreVurderinger.add( + AnnenVurdering( + personResultat = personResultat, + resultat = resultat, + type = AnnenVurderingType.OPPLYSNINGSPLIKT, + begrunnelse = null, + ), + ) + + vilkårsvurdering.personResultater = setOf(personResultat) + return vilkårsvurdering +} + +/** + * Dette er en funksjon for å få en førstegangsbehandling til en ønsket tilstand ved test. + * Man sender inn steg man ønsker å komme til (tilSteg), personer på behandlingen (søkerFnr og barnasIdenter), + * og serviceinstanser som brukes i testen. + */ +fun kjørStegprosessForFGB( + tilSteg: StegType, + søkerFnr: String = randomFnr(), + barnasIdenter: List = listOf(ClientMocks.barnFnr[0]), + fagsakService: FagsakService, + vedtakService: VedtakService, + persongrunnlagService: PersongrunnlagService, + vilkårsvurderingService: VilkårsvurderingService, + stegService: StegService, + vedtaksperiodeService: VedtaksperiodeService, + behandlingUnderkategori: BehandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + institusjon: InstitusjonInfo? = null, + verge: VergeInfo? = null, + brevmalService: BrevmalService, + behandlingKategori: BehandlingKategori = BehandlingKategori.NASJONAL, +): Behandling { + val fagsakType = utledFagsaktype(institusjon, verge) + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent( + fødselsnummer = søkerFnr, + institusjon = institusjon, + fagsakType = fagsakType, + ) + val behandling = stegService.håndterNyBehandling( + NyBehandling( + kategori = behandlingKategori, + underkategori = behandlingUnderkategori, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + søkersIdent = søkerFnr, + barnasIdenter = barnasIdenter, + søknadMottattDato = LocalDate.now(), + fagsakId = fagsak.id, + ), + ) + + if (verge != null) { + stegService.håndterRegistrerVerge( + behandling, + RestRegistrerInstitusjonOgVerge(vergeInfo = verge, institusjonInfo = null), + ) + } + + val behandlingEtterPersongrunnlagSteg = stegService.håndterSøknad( + behandling = behandling, + restRegistrerSøknad = RestRegistrerSøknad( + søknad = lagSøknadDTO( + søkerIdent = søkerFnr, + barnasIdenter = barnasIdenter, + underkategori = behandlingUnderkategori, + ), + bekreftEndringerViaFrontend = true, + ), + ) + + if (tilSteg == StegType.REGISTRERE_PERSONGRUNNLAG || tilSteg == StegType.REGISTRERE_SØKNAD) { + return behandlingEtterPersongrunnlagSteg + } + + val vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandling(behandlingId = behandling.id)!! + persongrunnlagService.hentAktivThrows(behandlingId = behandling.id).personer.forEach { barn -> + vurderVilkårsvurderingTilInnvilget(vilkårsvurdering, barn) + } + vilkårsvurderingService.oppdater(vilkårsvurdering) + + val behandlingEtterVilkårsvurderingSteg = stegService.håndterVilkårsvurdering(behandlingEtterPersongrunnlagSteg) + + if (tilSteg == StegType.VILKÅRSVURDERING) return behandlingEtterVilkårsvurderingSteg + + val behandlingEtterBehandlingsresultat = stegService.håndterBehandlingsresultat(behandlingEtterVilkårsvurderingSteg) + + if (tilSteg == StegType.BEHANDLINGSRESULTAT) return behandlingEtterBehandlingsresultat + + val behandlingEtterVurderTilbakekrevingSteg = stegService.håndterVurderTilbakekreving( + behandlingEtterBehandlingsresultat, + RestTilbakekreving( + valg = Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, + begrunnelse = "Begrunnelse", + ), + ) + + leggTilBegrunnelsePåVedtaksperiodeIBehandling( + behandling = behandlingEtterVurderTilbakekrevingSteg, + vedtakService = vedtakService, + vedtaksperiodeService = vedtaksperiodeService, + ) + + if (tilSteg == StegType.VURDER_TILBAKEKREVING) return behandlingEtterVurderTilbakekrevingSteg + + val behandlingEtterSendTilBeslutter = + stegService.håndterSendTilBeslutter(behandlingEtterVurderTilbakekrevingSteg, "1234") + if (tilSteg == StegType.SEND_TIL_BESLUTTER) return behandlingEtterSendTilBeslutter + + val behandlingEtterBeslutteVedtak = + stegService.håndterBeslutningForVedtak( + behandlingEtterSendTilBeslutter, + RestBeslutningPåVedtak(beslutning = Beslutning.GODKJENT), + ) + if (tilSteg == StegType.BESLUTTE_VEDTAK) return behandlingEtterBeslutteVedtak + + val vedtak = vedtakService.hentAktivForBehandling(behandlingEtterBeslutteVedtak.id) + val behandlingEtterIverksetteVedtak = + stegService.håndterIverksettMotØkonomi( + behandlingEtterBeslutteVedtak, + IverksettingTaskDTO( + behandlingsId = behandlingEtterBeslutteVedtak.id, + vedtaksId = vedtak!!.id, + saksbehandlerId = "System", + personIdent = behandlingEtterBeslutteVedtak.fagsak.aktør.aktivFødselsnummer(), + ), + ) + if (tilSteg == StegType.IVERKSETT_MOT_OPPDRAG) return behandlingEtterIverksetteVedtak + + val behandlingEtterStatusFraOppdrag = + stegService.håndterStatusFraØkonomi( + behandlingEtterIverksetteVedtak, + StatusFraOppdragMedTask( + statusFraOppdragDTO = StatusFraOppdragDTO( + fagsystem = FAGSYSTEM, + personIdent = søkerFnr, + aktørId = behandlingEtterIverksetteVedtak.fagsak.aktør.aktivFødselsnummer(), + behandlingsId = behandlingEtterIverksetteVedtak.id, + vedtaksId = vedtak.id, + ), + task = Task(type = StatusFraOppdragTask.TASK_STEP_TYPE, payload = ""), + ), + ) + if (tilSteg == StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI) return behandlingEtterStatusFraOppdrag + + val behandlingEtterIverksetteMotTilbake = + stegService.håndterIverksettMotFamilieTilbake(behandlingEtterStatusFraOppdrag, Properties()) + if (tilSteg == StegType.IVERKSETT_MOT_FAMILIE_TILBAKE) return behandlingEtterIverksetteMotTilbake + + val behandlingEtterJournalførtVedtak = + stegService.håndterJournalførVedtaksbrev( + behandlingEtterIverksetteMotTilbake, + JournalførVedtaksbrevDTO( + vedtakId = vedtak.id, + task = Task(type = JournalførVedtaksbrevTask.TASK_STEP_TYPE, payload = ""), + ), + ) + if (tilSteg == StegType.JOURNALFØR_VEDTAKSBREV) return behandlingEtterJournalførtVedtak + + val behandlingEtterDistribuertVedtak = + stegService.håndterDistribuerVedtaksbrev( + behandlingEtterJournalførtVedtak, + DistribuerDokumentDTO( + behandlingId = behandlingEtterJournalførtVedtak.id, + journalpostId = "1234", + personEllerInstitusjonIdent = søkerFnr, + brevmal = brevmalService.hentBrevmal( + behandlingEtterJournalførtVedtak, + ), + erManueltSendt = false, + ), + ) + if (tilSteg == StegType.DISTRIBUER_VEDTAKSBREV) return behandlingEtterDistribuertVedtak + + return stegService.håndterFerdigstillBehandling(behandlingEtterDistribuertVedtak) +} + +private fun utledFagsaktype(institusjon: InstitusjonInfo?, verge: VergeInfo?): FagsakType { + return if (institusjon != null) { + FagsakType.INSTITUSJON + } else if (verge != null) { + FagsakType.BARN_ENSLIG_MINDREÅRIG + } else { + FagsakType.NORMAL + } +} + +/** + * Dette er en funksjon for å få en førstegangsbehandling til en ønsket tilstand ved test. + * Man sender inn steg man ønsker å komme til (tilSteg), personer på behandlingen (søkerFnr og barnasIdenter), + * og serviceinstanser som brukes i testen. + */ +fun kjørStegprosessForRevurderingÅrligKontroll( + tilSteg: StegType, + søkerFnr: String, + barnasIdenter: List, + vedtakService: VedtakService, + stegService: StegService, + fagsakId: Long, + brevmalService: BrevmalService, +): Behandling { + val behandling = stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingType = BehandlingType.REVURDERING, + behandlingÅrsak = BehandlingÅrsak.ÅRLIG_KONTROLL, + søkersIdent = søkerFnr, + barnasIdenter = barnasIdenter, + fagsakId = fagsakId, + ), + ) + + val behandlingEtterVilkårsvurderingSteg = stegService.håndterVilkårsvurdering(behandling) + + if (tilSteg == StegType.VILKÅRSVURDERING) return behandlingEtterVilkårsvurderingSteg + + val behandlingEtterBehandlingsresultat = stegService.håndterBehandlingsresultat(behandlingEtterVilkårsvurderingSteg) + + if (tilSteg == StegType.BEHANDLINGSRESULTAT) return behandlingEtterBehandlingsresultat + + val behandlingEtterSimuleringSteg = stegService.håndterVurderTilbakekreving( + behandlingEtterBehandlingsresultat, + if (behandlingEtterBehandlingsresultat.resultat != Behandlingsresultat.FORTSATT_INNVILGET) { + RestTilbakekreving( + valg = Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, + begrunnelse = "Begrunnelse", + ) + } else { + null + }, + ) + if (tilSteg == StegType.VURDER_TILBAKEKREVING) return behandlingEtterSimuleringSteg + + val behandlingEtterSendTilBeslutter = stegService.håndterSendTilBeslutter(behandlingEtterSimuleringSteg, "1234") + if (tilSteg == StegType.SEND_TIL_BESLUTTER) return behandlingEtterSendTilBeslutter + + val behandlingEtterBeslutteVedtak = + stegService.håndterBeslutningForVedtak( + behandlingEtterSendTilBeslutter, + RestBeslutningPåVedtak(beslutning = Beslutning.GODKJENT), + ) + if (tilSteg == StegType.BESLUTTE_VEDTAK) return behandlingEtterBeslutteVedtak + + val vedtak = vedtakService.hentAktivForBehandling(behandlingEtterBeslutteVedtak.id) + val behandlingEtterIverksetteVedtak = + stegService.håndterIverksettMotØkonomi( + behandlingEtterBeslutteVedtak, + IverksettingTaskDTO( + behandlingsId = behandlingEtterBeslutteVedtak.id, + vedtaksId = vedtak!!.id, + saksbehandlerId = "System", + personIdent = behandlingEtterBeslutteVedtak.fagsak.aktør.aktivFødselsnummer(), + ), + ) + if (tilSteg == StegType.IVERKSETT_MOT_OPPDRAG) return behandlingEtterIverksetteVedtak + + val behandlingEtterStatusFraOppdrag = + stegService.håndterStatusFraØkonomi( + behandlingEtterIverksetteVedtak, + StatusFraOppdragMedTask( + statusFraOppdragDTO = StatusFraOppdragDTO( + fagsystem = FAGSYSTEM, + personIdent = søkerFnr, + aktørId = behandlingEtterIverksetteVedtak.fagsak.aktør.aktørId, + behandlingsId = behandlingEtterIverksetteVedtak.id, + vedtaksId = vedtak.id, + ), + task = Task(type = StatusFraOppdragTask.TASK_STEP_TYPE, payload = ""), + ), + ) + if (tilSteg == StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI) return behandlingEtterStatusFraOppdrag + + val behandlingEtterIverksetteMotTilbake = + stegService.håndterIverksettMotFamilieTilbake(behandlingEtterStatusFraOppdrag, Properties()) + if (tilSteg == StegType.IVERKSETT_MOT_FAMILIE_TILBAKE) return behandlingEtterIverksetteMotTilbake + + val behandlingEtterJournalførtVedtak = + stegService.håndterJournalførVedtaksbrev( + behandlingEtterIverksetteMotTilbake, + JournalførVedtaksbrevDTO( + vedtakId = vedtak.id, + task = Task(type = JournalførVedtaksbrevTask.TASK_STEP_TYPE, payload = ""), + ), + ) + if (tilSteg == StegType.JOURNALFØR_VEDTAKSBREV) return behandlingEtterJournalførtVedtak + + val behandlingEtterDistribuertVedtak = + stegService.håndterDistribuerVedtaksbrev( + behandlingEtterJournalførtVedtak, + DistribuerDokumentDTO( + behandlingId = behandling.id, + journalpostId = "1234", + personEllerInstitusjonIdent = søkerFnr, + brevmal = brevmalService.hentBrevmal(behandling), + erManueltSendt = false, + ), + ) + if (tilSteg == StegType.DISTRIBUER_VEDTAKSBREV) return behandlingEtterDistribuertVedtak + + return stegService.håndterFerdigstillBehandling(behandlingEtterDistribuertVedtak) +} + +fun opprettRestTilbakekreving(): RestTilbakekreving = RestTilbakekreving( + valg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + varsel = "Varsel", + begrunnelse = "Begrunnelse", +) + +fun lagUtbetalingsperiode( + periodeFom: LocalDate = LocalDate.now().withDayOfMonth(1), + periodeTom: LocalDate = LocalDate.now().let { it.withDayOfMonth(it.lengthOfMonth()) }, + vedtaksperiodetype: Vedtaksperiodetype = Vedtaksperiodetype.UTBETALING, + utbetalingsperiodeDetaljer: List, + ytelseTyper: List = listOf(YtelseType.ORDINÆR_BARNETRYGD), + antallBarn: Int = 1, + utbetaltPerMnd: Int = sats(YtelseType.ORDINÆR_BARNETRYGD), +) = Utbetalingsperiode( + periodeFom, + periodeTom, + vedtaksperiodetype, + utbetalingsperiodeDetaljer, + ytelseTyper, + antallBarn, + utbetaltPerMnd, +) + +fun lagUtbetalingsperiodeDetalj( + person: RestPerson = tilfeldigSøker().tilRestPerson(), + ytelseType: YtelseType = YtelseType.ORDINÆR_BARNETRYGD, + utbetaltPerMnd: Int = sats(YtelseType.ORDINÆR_BARNETRYGD), + prosent: BigDecimal = BigDecimal.valueOf(100), +) = UtbetalingsperiodeDetalj( + person = person, + ytelseType = ytelseType, + utbetaltPerMnd = utbetaltPerMnd, + erPåvirketAvEndring = false, + endringsårsak = null, + prosent = prosent, +) + +fun lagVedtaksperiodeMedBegrunnelser( + vedtak: Vedtak = lagVedtak(), + fom: LocalDate? = LocalDate.now().withDayOfMonth(1), + tom: LocalDate? = LocalDate.now().let { it.withDayOfMonth(it.lengthOfMonth()) }, + type: Vedtaksperiodetype = Vedtaksperiodetype.FORTSATT_INNVILGET, + begrunnelser: MutableSet = mutableSetOf(lagVedtaksbegrunnelse()), + fritekster: MutableList = mutableListOf(), +) = VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = fom, + tom = tom, + type = type, + begrunnelser = begrunnelser, + fritekster = fritekster, +) + +fun lagUtvidetVedtaksperiodeMedBegrunnelser( + id: Long = nesteUtvidetVedtaksperiodeId(), + fom: LocalDate? = LocalDate.now().withDayOfMonth(1), + tom: LocalDate? = LocalDate.now().let { it.withDayOfMonth(it.lengthOfMonth()) }, + type: Vedtaksperiodetype = Vedtaksperiodetype.FORTSATT_INNVILGET, + begrunnelser: List = listOf(lagVedtaksbegrunnelse()), + fritekster: MutableList = mutableListOf(), + utbetalingsperiodeDetaljer: List = emptyList(), + eøsBegrunnelser: List = emptyList(), +) = UtvidetVedtaksperiodeMedBegrunnelser( + id = id, + fom = fom, + tom = tom, + type = type, + begrunnelser = begrunnelser, + fritekster = fritekster.map { it.fritekst }, + utbetalingsperiodeDetaljer = utbetalingsperiodeDetaljer, + eøsBegrunnelser = eøsBegrunnelser, +) + +fun leggTilBegrunnelsePåVedtaksperiodeIBehandling( + behandling: Behandling, + vedtakService: VedtakService, + vedtaksperiodeService: VedtaksperiodeService, +) { + val aktivtVedtak = vedtakService.hentAktivForBehandling(behandling.id)!! + + val perisisterteVedtaksperioder = + vedtaksperiodeService.hentPersisterteVedtaksperioder(aktivtVedtak) + + vedtaksperiodeService.oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = perisisterteVedtaksperioder.first { it.type == Vedtaksperiodetype.UTBETALING }.id, + standardbegrunnelserFraFrontend = listOf( + Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + ), + eøsStandardbegrunnelserFraFrontend = emptyList(), + ) +} + +fun lagVilkårResultat( + vilkår: Vilkår, + vilkårRegelverk: Regelverk? = null, + fom: YearMonth? = null, + tom: YearMonth? = null, + behandlingId: Long = 0, +) = VilkårResultat( + personResultat = null, + vilkårType = vilkår, + resultat = Resultat.OPPFYLT, + periodeFom = fom?.toLocalDate(), + periodeTom = tom?.toLocalDate(), + begrunnelse = "", + sistEndretIBehandlingId = behandlingId, + vurderesEtter = vilkårRegelverk, +) + +fun lagVilkårResultat( + personResultat: PersonResultat? = null, + vilkårType: Vilkår = Vilkår.BOSATT_I_RIKET, + resultat: Resultat = Resultat.OPPFYLT, + periodeFom: LocalDate? = LocalDate.of(2009, 12, 24), + periodeTom: LocalDate? = LocalDate.of(2010, 1, 31), + begrunnelse: String = "", + behandlingId: Long = lagBehandling().id, + utdypendeVilkårsvurderinger: List = emptyList(), + erEksplisittAvslagPåSøknad: Boolean = false, + standardbegrunnelser: List = emptyList(), +) = VilkårResultat( + personResultat = personResultat, + vilkårType = vilkårType, + resultat = resultat, + periodeFom = periodeFom, + periodeTom = periodeTom, + begrunnelse = begrunnelse, + sistEndretIBehandlingId = behandlingId, + utdypendeVilkårsvurderinger = utdypendeVilkårsvurderinger, + erEksplisittAvslagPåSøknad = erEksplisittAvslagPåSøknad, + standardbegrunnelser = standardbegrunnelser, +) + +val guttenBarnesenFødselsdato = LocalDate.now().withDayOfMonth(10).minusYears(6) + +fun lagEndretUtbetalingAndel(behandlingId: Long, barn: Person, fom: YearMonth, tom: YearMonth, prosent: Int) = + lagEndretUtbetalingAndel( + behandlingId = behandlingId, + person = barn, + fom = fom, + tom = tom, + prosent = BigDecimal(prosent), + ) + +fun lagEndretUtbetalingAndel( + id: Long = 0, + behandlingId: Long = 0, + person: Person, + prosent: BigDecimal = BigDecimal.valueOf(100), + fom: YearMonth = YearMonth.now().minusMonths(1), + tom: YearMonth? = YearMonth.now(), + årsak: Årsak = Årsak.DELT_BOSTED, + avtaletidspunktDeltBosted: LocalDate = LocalDate.now().minusMonths(1), + søknadstidspunkt: LocalDate = LocalDate.now().minusMonths(1), + standardbegrunnelser: List = emptyList(), +) = + EndretUtbetalingAndel( + id = id, + behandlingId = behandlingId, + person = person, + prosent = prosent, + fom = fom, + tom = tom, + årsak = årsak, + avtaletidspunktDeltBosted = avtaletidspunktDeltBosted, + søknadstidspunkt = søknadstidspunkt, + begrunnelse = "Test", + ) + +fun lagEndretUtbetalingAndelMedAndelerTilkjentYtelse( + behandlingId: Long, + barn: Person, + fom: YearMonth, + tom: YearMonth, + prosent: Int, +) = + lagEndretUtbetalingAndelMedAndelerTilkjentYtelse( + behandlingId = behandlingId, + person = barn, + fom = fom, + tom = tom, + prosent = BigDecimal(prosent), + ) + +fun lagEndretUtbetalingAndelMedAndelerTilkjentYtelse( + id: Long = 0, + behandlingId: Long = 0, + person: Person, + prosent: BigDecimal = BigDecimal.valueOf(100), + fom: YearMonth = YearMonth.now().minusMonths(1), + tom: YearMonth? = YearMonth.now(), + årsak: Årsak = Årsak.DELT_BOSTED, + avtaletidspunktDeltBosted: LocalDate = LocalDate.now().minusMonths(1), + søknadstidspunkt: LocalDate = LocalDate.now().minusMonths(1), + andelTilkjentYtelser: MutableList = mutableListOf(), +): EndretUtbetalingAndelMedAndelerTilkjentYtelse { + val eua = EndretUtbetalingAndel( + id = id, + behandlingId = behandlingId, + person = person, + prosent = prosent, + fom = fom, + tom = tom, + årsak = årsak, + avtaletidspunktDeltBosted = avtaletidspunktDeltBosted, + søknadstidspunkt = søknadstidspunkt, + begrunnelse = "Test", + ) + + return EndretUtbetalingAndelMedAndelerTilkjentYtelse(eua, andelTilkjentYtelser) +} + +fun lagPerson( + personIdent: PersonIdent = PersonIdent(randomFnr()), + aktør: Aktør = tilAktør(personIdent.ident), + type: PersonType = PersonType.SØKER, + personopplysningGrunnlag: PersonopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 0), + fødselsdato: LocalDate = LocalDate.now().minusYears(19), + kjønn: Kjønn = Kjønn.KVINNE, + dødsfall: Dødsfall? = null, + id: Long = 0, +) = Person( + aktør = aktør, + type = type, + personopplysningGrunnlag = personopplysningGrunnlag, + fødselsdato = fødselsdato, + navn = type.name, + kjønn = kjønn, + dødsfall = dødsfall, + id = id, +) + +fun lagRestSanityBegrunnelse( + apiNavn: String = "", + navnISystem: String = "", + vilkaar: List? = emptyList(), + rolle: List? = emptyList(), + lovligOppholdTriggere: List? = emptyList(), + bosattIRiketTriggere: List? = emptyList(), + giftPartnerskapTriggere: List? = emptyList(), + borMedSokerTriggere: List? = emptyList(), + ovrigeTriggere: List? = emptyList(), + endringsaarsaker: List? = emptyList(), + hjemler: List = emptyList(), + hjemlerFolketrygdloven: List = emptyList(), + endretUtbetalingsperiodeDeltBostedTriggere: String = "", + endretUtbetalingsperiodeTriggere: List? = emptyList(), + vedtakResultat: String? = null, + fagsakType: String? = null, + tema: String? = null, + periodeType: String? = null, +): RestSanityBegrunnelse = RestSanityBegrunnelse( + apiNavn = apiNavn, + navnISystem = navnISystem, + vilkaar = vilkaar, + rolle = rolle, + lovligOppholdTriggere = lovligOppholdTriggere, + bosattIRiketTriggere = bosattIRiketTriggere, + giftPartnerskapTriggere = giftPartnerskapTriggere, + borMedSokerTriggere = borMedSokerTriggere, + ovrigeTriggere = ovrigeTriggere, + endringsaarsaker = endringsaarsaker, + hjemler = hjemler, + hjemlerFolketrygdloven = hjemlerFolketrygdloven, + endretUtbetalingsperiodeDeltBostedUtbetalingTrigger = endretUtbetalingsperiodeDeltBostedTriggere, + endretUtbetalingsperiodeTriggere = endretUtbetalingsperiodeTriggere, + vedtakResultat = vedtakResultat, + fagsakType = fagsakType, + tema = tema, + periodeType = periodeType, +) + +fun lagSanityBegrunnelse( + apiNavn: String = "", + navnISystem: String = "", + vilkaar: List = emptyList(), + rolle: List = emptyList(), + lovligOppholdTriggere: List = emptyList(), + bosattIRiketTriggere: List = emptyList(), + giftPartnerskapTriggere: List = emptyList(), + borMedSokerTriggere: List = emptyList(), + ovrigeTriggere: List<ØvrigTrigger> = emptyList(), + endringsaarsaker: List<Årsak> = emptyList(), + hjemler: List = emptyList(), + hjemlerFolketrygdloven: List = emptyList(), + endretUtbetalingsperiodeDeltBostedTriggere: EndretUtbetalingsperiodeDeltBostedTriggere? = null, + endretUtbetalingsperiodeTriggere: List = emptyList(), + resultat: SanityPeriodeResultat? = null, + fagsakType: FagsakType? = null, + periodeType: BrevPeriodeType? = null, +): SanityBegrunnelse = SanityBegrunnelse( + apiNavn = apiNavn, + navnISystem = navnISystem, + vilkaar = vilkaar, + rolle = rolle, + lovligOppholdTriggere = lovligOppholdTriggere, + bosattIRiketTriggere = bosattIRiketTriggere, + giftPartnerskapTriggere = giftPartnerskapTriggere, + borMedSokerTriggere = borMedSokerTriggere, + ovrigeTriggere = ovrigeTriggere, + endringsaarsaker = endringsaarsaker, + hjemler = hjemler, + hjemlerFolketrygdloven = hjemlerFolketrygdloven, + endretUtbetalingsperiodeDeltBostedUtbetalingTrigger = endretUtbetalingsperiodeDeltBostedTriggere, + endretUtbetalingsperiodeTriggere = endretUtbetalingsperiodeTriggere, + periodeResultat = resultat, + fagsakType = fagsakType, + periodeType = periodeType, +) + +fun lagSanityEøsBegrunnelse( + apiNavn: String = "", + navnISystem: String = "", + annenForeldersAktivitet: List = emptyList(), + barnetsBostedsland: List = emptyList(), + kompetanseResultat: List = emptyList(), + hjemler: List = emptyList(), + hjemlerFolketrygdloven: List = emptyList(), + hjemlerEØSForordningen883: List = emptyList(), + hjemlerEØSForordningen987: List = emptyList(), + hjemlerSeperasjonsavtalenStorbritannina: List = emptyList(), + vilkår: List = emptyList(), + fagsakType: FagsakType? = null, + tema: Tema? = null, + periodeType: BrevPeriodeType? = null, +): SanityEØSBegrunnelse = SanityEØSBegrunnelse( + apiNavn = apiNavn, + navnISystem = navnISystem, + annenForeldersAktivitet = annenForeldersAktivitet, + barnetsBostedsland = barnetsBostedsland, + kompetanseResultat = kompetanseResultat, + hjemler = hjemler, + hjemlerFolketrygdloven = hjemlerFolketrygdloven, + hjemlerEØSForordningen883 = hjemlerEØSForordningen883, + hjemlerEØSForordningen987 = hjemlerEØSForordningen987, + hjemlerSeperasjonsavtalenStorbritannina = hjemlerSeperasjonsavtalenStorbritannina, + vilkår = vilkår.toSet(), + fagsakType = fagsakType, + tema = tema, + periodeType = periodeType, +) + +fun lagTriggesAv( + vilkår: Set = emptySet(), + personTyper: Set = setOf(PersonType.BARN, PersonType.SØKER), + personerManglerOpplysninger: Boolean = false, + satsendring: Boolean = false, + barnMedSeksårsdag: Boolean = false, + vurderingAnnetGrunnlag: Boolean = false, + medlemskap: Boolean = false, + deltbosted: Boolean = false, + valgbar: Boolean = true, + valgbarhet: Valgbarhet? = null, + endringsaarsaker: Set<Årsak> = emptySet(), + etterEndretUtbetaling: Boolean = false, + endretUtbetalingSkalUtbetales: EndretUtbetalingsperiodeDeltBostedTriggere = EndretUtbetalingsperiodeDeltBostedTriggere.UTBETALING_IKKE_RELEVANT, + småbarnstillegg: Boolean = false, +): TriggesAv = TriggesAv( + vilkår = vilkår, + personTyper = personTyper, + personerManglerOpplysninger = personerManglerOpplysninger, + satsendring = satsendring, + barnMedSeksårsdag = barnMedSeksårsdag, + vurderingAnnetGrunnlag = vurderingAnnetGrunnlag, + medlemskap = medlemskap, + deltbosted = deltbosted, + valgbar = valgbar, + endringsaarsaker = endringsaarsaker, + etterEndretUtbetaling = etterEndretUtbetaling, + endretUtbetalingSkalUtbetales = endretUtbetalingSkalUtbetales, + småbarnstillegg = småbarnstillegg, + barnDød = false, + deltBostedSkalIkkeDeles = false, + gjelderFraInnvilgelsestidspunkt = false, + gjelderFørstePeriode = false, + valgbarhet = valgbarhet, +) + +fun oppfyltVilkår(vilkår: Vilkår, regelverk: Regelverk? = null) = + VilkårRegelverkResultat( + vilkår = vilkår, + regelverkResultat = when (regelverk) { + Regelverk.NASJONALE_REGLER -> RegelverkResultat.OPPFYLT_NASJONALE_REGLER + Regelverk.EØS_FORORDNINGEN -> RegelverkResultat.OPPFYLT_EØS_FORORDNINGEN + else -> RegelverkResultat.OPPFYLT_REGELVERK_IKKE_SATT + }, + ) + +fun ikkeOppfyltVilkår(vilkår: Vilkår) = + VilkårRegelverkResultat( + vilkår = vilkår, + regelverkResultat = RegelverkResultat.IKKE_OPPFYLT, + ) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/DbContainerInitializer.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/DbContainerInitializer.kt new file mode 100644 index 000000000..f0965f74d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/DbContainerInitializer.kt @@ -0,0 +1,35 @@ +package no.nav.familie.ba.sak.common + +import org.springframework.boot.test.util.TestPropertyValues +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.core.env.Profiles +import org.testcontainers.containers.PostgreSQLContainer + +class DbContainerInitializer : ApplicationContextInitializer { + + override fun initialize(applicationContext: ConfigurableApplicationContext) { + // Only start Postgres when not running in CI + if (!applicationContext.environment.acceptsProfiles(Profiles.of("ci"))) { + postgres.start() + TestPropertyValues.of( + "spring.datasource.url=${postgres.jdbcUrl}", + "spring.datasource.username=${postgres.username}", + "spring.datasource.password=${postgres.password}", + ).applyTo(applicationContext.environment) + } + } + + companion object { + // Lazy because we only want it to be initialized when accessed + private val postgres: KPostgreSQLContainer by lazy { + KPostgreSQLContainer("postgres:15.4") + .withDatabaseName("databasename") + .withUsername("postgres") + .withPassword("test") + } + } +} + +// Hack needed because testcontainers use of generics confuses Kotlin +class KPostgreSQLContainer(imageName: String) : PostgreSQLContainer(imageName) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/TidKtTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/TidKtTest.kt new file mode 100644 index 000000000..775871f34 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/TidKtTest.kt @@ -0,0 +1,39 @@ +package no.nav.familie.ba.sak.common + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.time.YearMonth + +internal class TidKtTest { + + @Test + fun `Test YearMonth range over flere måneder`() { + assertEquals(4, (YearMonth.of(2021, 1)..YearMonth.of(2021, 4)).toList().size) + } + + @Test + fun `Test YearMonth range med én måned`() { + assertEquals(1, (YearMonth.of(2021, 1)..YearMonth.of(2021, 1)).toList().size) + } + + @Test + fun `Test YearMonth range med tidligere sluttmåned`() { + assertEquals(0, (YearMonth.of(2021, 1)..YearMonth.of(2020, 11)).toList().size) + } + + @Test + fun `Test YearMonth range med tidligere sluttmåned og negativt steg`() { + assertEquals(3, (YearMonth.of(2021, 1)..YearMonth.of(2020, 11) step -1).toList().size) + } + + @Test + fun `Test YearMonth range med senere sluttmåned og negativt steg`() { + assertEquals(0, (YearMonth.of(2021, 1)..YearMonth.of(2021, 11) step -1).toList().size) + } + + @Test + fun `Test YearMonth range med tidligere sluttmåned og null som steg`() { + assertThrows { (YearMonth.of(2021, 1)..YearMonth.of(2020, 11) step 0).toList() } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/TidTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/TidTest.kt new file mode 100644 index 000000000..4639c1d2b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/TidTest.kt @@ -0,0 +1,309 @@ +package no.nav.familie.ba.sak.common + +import io.mockk.mockk +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.YearMonth + +internal class TidTest { + + @Test + fun `skal finne siste dag i måneden før 2020-03-01`() { + assertEquals(dato("2020-02-29"), dato("2020-03-01").sisteDagIForrigeMåned()) + } + + @Test + fun `skal finne siste dag i måneden før 2021-03-01`() { + assertEquals(dato("2021-02-28"), dato("2021-03-01").sisteDagIForrigeMåned()) + } + + @Test + fun `skal finne siste dag i måneden forrige år`() { + assertEquals(dato("2019-12-31"), dato("2020-01-15").sisteDagIForrigeMåned()) + } + + @Test + fun `skal finne første dag neste år`() { + assertEquals(dato("2020-01-01"), dato("2019-12-03").førsteDagINesteMåned()) + } + + @Test + fun `skal finne første dag i måneden etter skuddårsdagen`() { + assertEquals(dato("2020-03-01"), dato("2020-02-29").førsteDagINesteMåned()) + } + + @Test + fun `skal finne siste dag i inneværende måned 2020-03-01`() { + assertEquals(dato("2020-03-31"), dato("2020-03-01").sisteDagIMåned()) + } + + @Test + fun `skal finne siste dag i inneværende måned 2020-02-01 skuddår`() { + assertEquals(dato("2020-02-29"), dato("2020-02-01").sisteDagIMåned()) + } + + @Test + fun `skal returnere seneste dato av 2020-01-01 og 2019-01-01`() { + assertEquals(dato("2020-01-01"), senesteDatoAv(dato("2020-01-01"), dato("2019-01-01"))) + } + + @Test + fun `skal returnere true for dato som er senere enn`() { + assertEquals(true, dato("2020-01-01").isSameOrAfter(dato("2019-01-01"))) + } + + @Test + fun `skal returnere false for dato som er tidligere`() { + assertEquals(false, dato("2019-01-01").isSameOrAfter(dato("2020-01-01"))) + } + + @Test + fun `skal returnere true for dato som er lik`() { + assertEquals(true, dato("2020-01-01").isSameOrAfter(dato("2020-01-01"))) + } + + @Test + fun `skal returnere true dersom dato er dagen før en annen dato`() { + assertTrue(dato("2020-04-30").erDagenFør(dato("2020-05-01"))) + assertFalse(dato("2020-04-30").erDagenFør(dato("2020-05-02"))) + assertFalse(dato("2020-05-01").erDagenFør(dato("2020-04-30"))) + assertFalse(dato("2020-04-30").erDagenFør(dato("2020-04-30"))) + assertFalse(dato("2020-04-30").erDagenFør(null)) + } + + @Test + fun `dato i inneværende eller forrige måned`() { + assertTrue(LocalDate.now().erFraInneværendeMåned()) + assertTrue(LocalDate.now().erFraInneværendeEllerForrigeMåned()) + assertFalse(LocalDate.now().minusMonths(1).erFraInneværendeMåned()) + assertTrue(LocalDate.now().minusMonths(1).erFraInneværendeEllerForrigeMåned()) + assertFalse(LocalDate.now().minusYears(1).erFraInneværendeMåned()) + assertFalse(LocalDate.now().minusYears(1).erFraInneværendeEllerForrigeMåned()) + } + + @Test + fun `skal bestemme om periode er etterfølgende periode`() { + val personAktørId = randomAktør() + val behandling = lagBehandling() + val resultat: Resultat = mockk() + val vilkår: Vilkår = mockk(relaxed = true) + val vilkårsvurdering = lagVilkårsvurdering(personAktørId, behandling, resultat) + + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = personAktørId, + ) + + val førsteVilkårResultat = VilkårResultat( + personResultat = personResultat, + resultat = resultat, + vilkårType = vilkår, + periodeFom = LocalDate.of(2020, 1, 1), + periodeTom = LocalDate.of(2020, 3, 25), + begrunnelse = "", + sistEndretIBehandlingId = personResultat.vilkårsvurdering.behandling.id, + ) + val etterfølgendeVilkårResultat = VilkårResultat( + personResultat = personResultat, + resultat = resultat, + vilkårType = vilkår, + periodeFom = LocalDate.of(2020, 3, 31), + periodeTom = LocalDate.of(2020, 6, 1), + begrunnelse = "", + sistEndretIBehandlingId = personResultat.vilkårsvurdering.behandling.id, + ) + val ikkeEtterfølgendeVilkårResultat = VilkårResultat( + personResultat = personResultat, + resultat = resultat, + vilkårType = vilkår, + periodeFom = LocalDate.of(2020, 5, 1), + periodeTom = LocalDate.of(2020, 6, 1), + begrunnelse = "", + sistEndretIBehandlingId = personResultat.vilkårsvurdering.behandling.id, + ) + + assertTrue(førsteVilkårResultat.erEtterfølgendePeriode(etterfølgendeVilkårResultat)) + assertFalse(førsteVilkårResultat.erEtterfølgendePeriode(ikkeEtterfølgendeVilkårResultat)) + } + + @Test + fun `skal slå sammen overlappende perioder til en periode og bruke laveste fom og beholde tom fra periode 3`() { + val periode1 = DatoIntervallEntitet(LocalDate.of(2004, 1, 1), LocalDate.of(2005, 9, 2)) + val periode2 = DatoIntervallEntitet(LocalDate.of(2005, 10, 1), LocalDate.of(2015, 5, 20)) + val periode3 = DatoIntervallEntitet(LocalDate.of(2014, 10, 1), LocalDate.of(2018, 5, 20)) + val currentPeriode = DatoIntervallEntitet(LocalDate.of(2018, 6, 1), null) + + val result = slåSammenOverlappendePerioder(listOf(periode2, periode3, periode1, currentPeriode)) + Assertions.assertThat(result) + .hasSize(3) + .contains(periode1) + .contains(DatoIntervallEntitet(LocalDate.of(2005, 10, 1), LocalDate.of(2018, 5, 20))) + .contains(currentPeriode) + } + + @Test + fun `skal slå sammen overlappende perioder til en periode og bruke laveste fom og beholde tom fra periode 2`() { + val periode1 = DatoIntervallEntitet(LocalDate.of(2004, 1, 1), LocalDate.of(2005, 9, 2)) + val periode2 = DatoIntervallEntitet(LocalDate.of(2005, 10, 1), LocalDate.of(2018, 5, 20)) + val periode3 = DatoIntervallEntitet(LocalDate.of(2014, 10, 1), LocalDate.of(2015, 5, 20)) + val currentPeriode = DatoIntervallEntitet(LocalDate.of(2018, 6, 1), null) + + val result = slåSammenOverlappendePerioder(listOf(periode2, periode3, periode1, currentPeriode)) + Assertions.assertThat(result) + .hasSize(3) + .contains(periode1) + .contains(DatoIntervallEntitet(LocalDate.of(2005, 10, 1), LocalDate.of(2018, 5, 20))) + .contains(currentPeriode) + } + + @Test + fun `skal slå sammen overlappende perioder med samme startdato`() { + val result = slåSammenOverlappendePerioder( + listOf( + DatoIntervallEntitet(LocalDate.of(2004, 1, 1), LocalDate.of(2004, 2, 1)), + DatoIntervallEntitet(LocalDate.of(2004, 1, 1), LocalDate.of(2004, 3, 1)), + DatoIntervallEntitet(LocalDate.of(2005, 1, 1), LocalDate.of(2005, 3, 1)), + DatoIntervallEntitet(LocalDate.of(2005, 1, 1), LocalDate.of(2005, 2, 1)), + ), + ) + + Assertions.assertThat(result) + .hasSize(2) + .contains(DatoIntervallEntitet(LocalDate.of(2004, 1, 1), LocalDate.of(2004, 3, 1))) + .contains(DatoIntervallEntitet(LocalDate.of(2005, 1, 1), LocalDate.of(2005, 3, 1))) + } + + @Test + fun `skal ikke slå sammen perioder som ligger inntil hverandre`() { + val result = slåSammenOverlappendePerioder( + listOf( + DatoIntervallEntitet(LocalDate.of(2004, 1, 1), LocalDate.of(2004, 1, 31)), + DatoIntervallEntitet(LocalDate.of(2004, 2, 1), LocalDate.of(2004, 2, 28)), + ), + ) + + Assertions.assertThat(result) + .hasSize(2) + .contains(DatoIntervallEntitet(LocalDate.of(2004, 1, 1), LocalDate.of(2004, 1, 31))) + .contains(DatoIntervallEntitet(LocalDate.of(2004, 2, 1), LocalDate.of(2004, 2, 28))) + } + + @Test + fun `skal slå sammen overlappende perioder til en periode og videreføre null i tom`() { + val periode1 = DatoIntervallEntitet(LocalDate.of(2004, 1, 1), LocalDate.of(2005, 9, 2)) + val periode2 = DatoIntervallEntitet(LocalDate.of(2005, 10, 1), LocalDate.of(2015, 5, 20)) + val periode3 = DatoIntervallEntitet(LocalDate.of(2014, 10, 1), LocalDate.of(2018, 5, 20)) + val currentPeriode = DatoIntervallEntitet(LocalDate.of(2008, 6, 1), null) + + val result = slåSammenOverlappendePerioder(listOf(periode2, periode3, periode1, currentPeriode)) + Assertions.assertThat(result) + .hasSize(2) + .contains(periode1) + .contains(DatoIntervallEntitet(LocalDate.of(2005, 10, 1), null)) + } + + @Test + fun `skal slå sammen perioder til én periode hvor første periode har tom som null`() { + val periode1 = DatoIntervallEntitet(LocalDate.of(2004, 1, 1), null) + val periode2 = DatoIntervallEntitet(LocalDate.of(2005, 10, 1), LocalDate.of(2015, 5, 20)) + + val result = slåSammenOverlappendePerioder(listOf(periode1, periode2)) + Assertions.assertThat(result) + .hasSize(1) + .contains(periode1) + .contains(DatoIntervallEntitet(LocalDate.of(2004, 1, 1), null)) + } + + @Test + fun `hopp over perioder som ikke har fra-dato`() { + val periode1 = DatoIntervallEntitet(null, null) + val periode2 = DatoIntervallEntitet(LocalDate.of(2004, 1, 1), LocalDate.of(2004, 1, 5)) + + val result = slåSammenOverlappendePerioder(listOf(periode1, periode2)) + Assertions.assertThat(result) + .hasSize(1) + } + + @Test + fun `det skal kun finnes en periode med tom = null etter sammenslåing`() { + val result = slåSammenOverlappendePerioder( + listOf( + DatoIntervallEntitet(LocalDate.of(2004, 1, 1), LocalDate.of(2004, 1, 1)), + DatoIntervallEntitet(LocalDate.of(2005, 1, 1), null), + DatoIntervallEntitet(LocalDate.of(2005, 5, 1), LocalDate.of(2005, 6, 1)), + DatoIntervallEntitet(LocalDate.of(2006, 1, 1), null), + DatoIntervallEntitet(LocalDate.of(2006, 5, 1), LocalDate.of(2006, 6, 1)), + ), + ) + Assertions.assertThat(result).hasSize(2) + Assertions.assertThat(result.filter { it.tom != null }).hasSize(1) + } + + @Test + fun `formatering gir forventet resultat`() { + assertEquals("31. desember 2020", dato("2020-12-31").tilDagMånedÅr()) + assertEquals("311220", dato("2020-12-31").tilddMMyy()) + assertEquals("31.12.20", dato("2020-12-31").tilKortString()) + assertEquals("desember 2020", dato("2020-12-31").tilMånedÅr()) + } + + @Test + fun `sjekk for om to måned perioder helt eller delvis er overlappende`() { + val jan2020_aug2020 = MånedPeriode(YearMonth.of(2020, 1), YearMonth.of(2020, 8)) + val jul2020_des2020 = MånedPeriode(YearMonth.of(2020, 7), YearMonth.of(2020, 12)) + val des2019_sep2021 = MånedPeriode(YearMonth.of(2019, 12), YearMonth.of(2020, 9)) + val jan2020 = MånedPeriode(YearMonth.of(2020, 1), YearMonth.of(2020, 1)) + val aug2020 = MånedPeriode(YearMonth.of(2020, 8), YearMonth.of(2020, 8)) + val des2019 = MånedPeriode(YearMonth.of(2019, 12), YearMonth.of(2019, 12)) + val sep2021 = MånedPeriode(YearMonth.of(2021, 9), YearMonth.of(2021, 9)) + + assertTrue(jan2020_aug2020.overlapperHeltEllerDelvisMed(jul2020_des2020)) + assertTrue(jul2020_des2020.overlapperHeltEllerDelvisMed(jan2020_aug2020)) + assertTrue(jan2020_aug2020.overlapperHeltEllerDelvisMed(des2019_sep2021)) + assertTrue(des2019_sep2021.overlapperHeltEllerDelvisMed(jan2020_aug2020)) + assertTrue(jan2020_aug2020.overlapperHeltEllerDelvisMed(jan2020)) + assertTrue(jan2020.overlapperHeltEllerDelvisMed(jan2020_aug2020)) + assertTrue(jan2020_aug2020.overlapperHeltEllerDelvisMed(aug2020)) + assertTrue(aug2020.overlapperHeltEllerDelvisMed(jan2020_aug2020)) + assertFalse(jan2020_aug2020.overlapperHeltEllerDelvisMed(des2019)) + assertFalse(des2019.overlapperHeltEllerDelvisMed(jan2020_aug2020)) + assertFalse(jan2020_aug2020.overlapperHeltEllerDelvisMed(sep2021)) + assertFalse(sep2021.overlapperHeltEllerDelvisMed(jan2020_aug2020)) + } + + @Test + fun `sjekk for om to perioder helt eller delvis er overlappende`() { + val jan2020_aug2020 = Periode(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 8, 1)) + val jul2020_des2020 = Periode(LocalDate.of(2020, 7, 1), LocalDate.of(2020, 12, 1)) + val des2019_sep2021 = Periode(LocalDate.of(2019, 12, 1), LocalDate.of(2020, 9, 1)) + val jan2020 = Periode(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 1, 1)) + val aug2020 = Periode(LocalDate.of(2020, 8, 1), LocalDate.of(2020, 8, 1)) + val des2019 = Periode(LocalDate.of(2019, 12, 1), LocalDate.of(2019, 12, 1)) + val sep2021 = Periode(LocalDate.of(2021, 9, 1), LocalDate.of(2021, 9, 1)) + + assertTrue(jan2020_aug2020.overlapperHeltEllerDelvisMed(jul2020_des2020)) + assertTrue(jul2020_des2020.overlapperHeltEllerDelvisMed(jan2020_aug2020)) + assertTrue(jan2020_aug2020.overlapperHeltEllerDelvisMed(des2019_sep2021)) + assertTrue(des2019_sep2021.overlapperHeltEllerDelvisMed(jan2020_aug2020)) + assertTrue(jan2020_aug2020.overlapperHeltEllerDelvisMed(jan2020)) + assertTrue(jan2020.overlapperHeltEllerDelvisMed(jan2020_aug2020)) + assertTrue(jan2020_aug2020.overlapperHeltEllerDelvisMed(aug2020)) + assertTrue(aug2020.overlapperHeltEllerDelvisMed(jan2020_aug2020)) + assertFalse(jan2020_aug2020.overlapperHeltEllerDelvisMed(des2019)) + assertFalse(des2019.overlapperHeltEllerDelvisMed(jan2020_aug2020)) + assertFalse(jan2020_aug2020.overlapperHeltEllerDelvisMed(sep2021)) + assertFalse(sep2021.overlapperHeltEllerDelvisMed(jan2020_aug2020)) + } + + private fun dato(s: String): LocalDate { + return LocalDate.parse(s) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/UtilsTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/UtilsTest.kt new file mode 100644 index 000000000..26ea80f18 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/common/UtilsTest.kt @@ -0,0 +1,105 @@ +package no.nav.familie.ba.sak.common + +import no.nav.familie.ba.sak.common.Utils.avrundetHeltallAvProsent +import no.nav.familie.ba.sak.common.Utils.hentPropertyFraMaven +import no.nav.familie.ba.sak.common.Utils.storForbokstavIAlleNavn +import no.nav.familie.ba.sak.common.Utils.storForbokstavIHvertOrd +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrVegadresse +import no.nav.familie.ba.sak.kjerne.personident.Identkonverterer.er11Siffer +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.tilBrevTekst +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate + +internal class UtilsTest { + + @Test + fun `skal regne ut prosent og gi heltall med riktig avrunding`() { + assertEquals(200, 200.toBigDecimal().avrundetHeltallAvProsent(100.toBigDecimal())) + assertEquals(100, 200.toBigDecimal().avrundetHeltallAvProsent(50.toBigDecimal())) + assertEquals(201, BigDecimal(201.4).avrundetHeltallAvProsent(100.toBigDecimal())) + assertEquals(202, BigDecimal(201.5).avrundetHeltallAvProsent(100.toBigDecimal())) + } + + @Test + fun `Navn i uppercase blir formatert korrekt`() = + assertEquals("Store Bokstaver Her", "STORE BOKSTAVER HER ".storForbokstavIHvertOrd()) + + @Test + fun `Navn i uppercase med mellomrom og bindestrek blir formatert korrekt`() = + assertEquals("Hense-Ravnen Hopp", "HENSE-RAVNEN HOPP".storForbokstavIAlleNavn()) + + @Test + fun `Nullable verdier blir tom string`() { + val adresse = GrVegadresse( + matrikkelId = null, + bruksenhetsnummer = null, + husnummer = "1", + kommunenummer = null, + tilleggsnavn = null, + adressenavn = "TEST", + husbokstav = null, + postnummer = "1234", + ) + + assertEquals("Test 1, 1234", adresse.tilFrontendString()) + } + + @Test + fun `hent property fra maven skal ikke være blank`() { + val result = hentPropertyFraMaven("java.version") + assertTrue(result?.isNotBlank() == true) + } + + @Test + fun `hent property som mangler skal returnere null`() { + val result = hentPropertyFraMaven("skalikkefinnes") + assertTrue(result.isNullOrEmpty()) + } + + @Test + fun `Test transformering av en personer til brevtekst`() { + val førsteBarn = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(6)) + + assertEquals( + førsteBarn.fødselsdato.tilKortString(), + listOf(førsteBarn.fødselsdato).tilBrevTekst(), + ) + } + + @Test + fun `Test transformering av to personer til brevtekst`() { + val førsteBarn = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(6)) + val andreBarn = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(6)) + + assertEquals( + "${førsteBarn.fødselsdato.tilKortString()} og ${andreBarn.fødselsdato.tilKortString()}", + listOf(førsteBarn.fødselsdato, andreBarn.fødselsdato).tilBrevTekst(), + ) + } + + @Test + fun `Test transformering av tre personer til brevtekst`() { + val førsteBarn = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(6)) + val andreBarn = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(6)) + val tredjeBarn = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(6)) + + assertEquals( + "${førsteBarn.fødselsdato.tilKortString()}, ${andreBarn.fødselsdato.tilKortString()} og ${tredjeBarn.fødselsdato.tilKortString()}", + listOf(førsteBarn.fødselsdato, andreBarn.fødselsdato, tredjeBarn.fødselsdato).tilBrevTekst(), + ) + } + + @Test + fun `Sjekker om ident er 11 siffer`() { + assertFalse(er11Siffer("abc")) + assertFalse(er11Siffer("")) + assertFalse(er11Siffer("12345")) + assertFalse(er11Siffer("1234567890A")) + assertFalse(er11Siffer("1234567890123")) + assertTrue(er11Siffer("12345678901")) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/AbstractMockkSpringRunner.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/AbstractMockkSpringRunner.kt new file mode 100644 index 000000000..93a28d697 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/AbstractMockkSpringRunner.kt @@ -0,0 +1,151 @@ +package no.nav.familie.ba.sak.config + +import io.mockk.isMockKMock +import io.mockk.unmockkAll +import no.nav.familie.ba.sak.common.LocalDateService +import no.nav.familie.ba.sak.integrasjoner.ef.EfSakRestClient +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollClient +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdBarnetrygdClient +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdBarnetrygdClientMock +import no.nav.familie.ba.sak.integrasjoner.pdl.PdlIdentRestClient +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiKlient +import no.nav.familie.ba.sak.kjerne.tilbakekreving.TilbakekrevingKlient +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.ba.sak.task.TaskRepositoryTestConfig +import no.nav.familie.unleash.UnleashService +import org.junit.jupiter.api.BeforeEach +import org.slf4j.MDC +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.cache.CacheManager +import org.springframework.context.ConfigurableApplicationContext + +abstract class AbstractMockkSpringRunner { + /** + * Tjenester vi mocker ved bruk av every + */ + @Autowired + private lateinit var mockPersonopplysningerService: PersonopplysningerService + + @Autowired + private lateinit var mockPdlIdentRestClient: PdlIdentRestClient + + @Autowired + private lateinit var mockIntegrasjonClient: IntegrasjonClient + + @Autowired + private lateinit var mockFamilieIntegrasjonerTilgangskontrollClient: FamilieIntegrasjonerTilgangskontrollClient + + @Autowired + private lateinit var mockEfSakRestClient: EfSakRestClient + + @Autowired + private lateinit var mockØkonomiKlient: ØkonomiKlient + + @Autowired + private lateinit var mockFeatureToggleService: FeatureToggleService + + @Autowired + private lateinit var mockUnleashService: UnleashService + + @Autowired + private lateinit var mockTilbakekrevingKlient: TilbakekrevingKlient + + @Autowired + private lateinit var mockLocalDateService: LocalDateService + + @Autowired + private lateinit var mockInfotrygdBarnetrygdClient: InfotrygdBarnetrygdClient + + @Autowired + private lateinit var mockTaskRepository: TaskRepositoryWrapper + + @Autowired + private lateinit var mockOpprettTaskService: OpprettTaskService + + @Autowired + private lateinit var applicationContext: ConfigurableApplicationContext + + /** + * Cachemanagere + */ + @Autowired + private lateinit var defaultCacheManager: CacheManager + + @Autowired + @Qualifier("dailyCache") + private lateinit var dailyCacheManager: CacheManager + + @Autowired + @Qualifier("shortCache") + private lateinit var shortCacheManager: CacheManager + + @BeforeEach + fun reset() { + clearCaches() + clearMocks() + } + + private fun clearMocks() { + unmockkAll() + if (isMockKMock(mockPersonopplysningerService)) { + ClientMocks.clearPdlMocks(mockPersonopplysningerService) + } + + if (isMockKMock(mockPdlIdentRestClient)) { + ClientMocks.clearPdlIdentRestClient(mockPdlIdentRestClient) + } + + IntegrasjonClientMock.clearIntegrasjonMocks(mockIntegrasjonClient) + IntegrasjonClientMock.clearMockFamilieIntegrasjonerTilgangskontrollClient( + mockFamilieIntegrasjonerTilgangskontrollClient, + ) + + if (isMockKMock(mockFeatureToggleService)) { + ClientMocks.clearFeatureToggleMocks(mockFeatureToggleService) + } + + if (isMockKMock(mockUnleashService)) { + ClientMocks.clearUnleashServiceMocks(mockUnleashService) + } + + if (isMockKMock(mockEfSakRestClient)) { + EfSakRestClientMock.clearEfSakRestMocks(mockEfSakRestClient) + } + + if (isMockKMock(mockØkonomiKlient)) { + ØkonomiTestConfig.clearØkonomiMocks(mockØkonomiKlient) + } + + if (isMockKMock(mockTilbakekrevingKlient)) { + TilbakekrevingKlientTestConfig.clearTilbakekrevingKlientMocks(mockTilbakekrevingKlient) + } + + if (isMockKMock(mockLocalDateService)) { + LocalDateServiceTestConfig.clearLocalDateServiceMocks(mockLocalDateService) + } + + if (isMockKMock(mockInfotrygdBarnetrygdClient)) { + InfotrygdBarnetrygdClientMock.clearInfotrygdBarnetrygdMocks(mockInfotrygdBarnetrygdClient) + } + + if (isMockKMock(mockTaskRepository)) { + TaskRepositoryTestConfig.clearMockTaskRepository(mockTaskRepository) + } + + if (isMockKMock(mockOpprettTaskService)) { + TaskRepositoryTestConfig.clearMockTaskService(mockOpprettTaskService) + } + + MDC.put("callId", "callId") + } + + private fun clearCaches() { + listOf(defaultCacheManager, shortCacheManager, dailyCacheManager).forEach { + it.cacheNames.mapNotNull { cacheName -> it.getCache(cacheName) } + .forEach { cache -> cache.clear() } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/BrevKlientMock.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/BrevKlientMock.kt new file mode 100644 index 000000000..d871a08f1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/BrevKlientMock.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.config + +import io.mockk.spyk +import no.nav.familie.ba.sak.kjerne.brev.BrevKlient +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brev +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BegrunnelseMedData +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Service +import org.springframework.web.client.RestTemplate + +@Service +class BrevKlientMock : BrevKlient( + familieBrevUri = "brev_uri_mock", + restTemplate = RestTemplate(), + sanityDataset = "", +) { + + override fun genererBrev(målform: String, brev: Brev): ByteArray { + return TEST_PDF + } + + override fun hentBegrunnelsestekst(begrunnelseData: BegrunnelseMedData): String { + return "Dummytekst for ${begrunnelseData.apiNavn}" + } +} + +@TestConfiguration +class BrevKlientTestFactory { + + @Bean + @Profile("mock-brev-klient") + @Primary + fun brevKlient() = spyk() +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/ClientMocks.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/ClientMocks.kt new file mode 100644 index 000000000..2a5d2ad24 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/ClientMocks.kt @@ -0,0 +1,661 @@ +package no.nav.familie.ba.sak.config + +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.slot +import no.nav.familie.ba.sak.common.EnvService +import no.nav.familie.ba.sak.common.guttenBarnesenFødselsdato +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.tilddMMyy +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonException +import no.nav.familie.ba.sak.integrasjoner.pdl.PdlIdentRestClient +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.VergeResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjonMaskert +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.IdentInformasjon +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.VergeData +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.mockBarnAutomatiskBehandling +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.mockBarnAutomatiskBehandling2 +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.mockBarnAutomatiskBehandling2Fnr +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.mockBarnAutomatiskBehandlingFnr +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.mockBarnAutomatiskBehandlingSkalFeile +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.mockBarnAutomatiskBehandlingSkalFeileFnr +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.mockSøkerAutomatiskBehandling +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.mockSøkerAutomatiskBehandlingAktør +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.mockSøkerAutomatiskBehandlingFnr +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.Personident +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import no.nav.familie.kontrakter.felles.personopplysning.Matrikkeladresse +import no.nav.familie.kontrakter.felles.personopplysning.OPPHOLDSTILLATELSE +import no.nav.familie.kontrakter.felles.personopplysning.Opphold +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import no.nav.familie.kontrakter.felles.personopplysning.Sivilstand +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap +import no.nav.familie.leader.LeaderClient +import no.nav.familie.unleash.UnleashService +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import org.springframework.http.HttpStatus +import org.springframework.web.client.HttpClientErrorException +import java.lang.Integer.min +import java.time.LocalDate + +@TestConfiguration +class ClientMocks { + + @Bean + @Profile("mock-pdl") + @Primary + fun mockPersonopplysningerService(): PersonopplysningerService { + val mockPersonopplysningerService = mockk(relaxed = false) + + clearPdlMocks(mockPersonopplysningerService) + + return mockPersonopplysningerService + } + + @Bean + @Profile("mock-ident-client") + @Primary + fun mockPdlIdentRestClient(): PdlIdentRestClient { + val mockPdlIdentRestClient = mockk(relaxed = false) + + clearPdlIdentRestClient(mockPdlIdentRestClient) + + return mockPdlIdentRestClient + } + + @Bean + @Primary + @Profile("mock-pdl-test-søk") + fun mockPDL(): PersonopplysningerService { + val mockPersonopplysningerService = mockk() + + val farId = "12345678910" + val morId = "21345678910" + val barnId = "31245678910" + + val farAktør = tilAktør(farId) + val morAktør = tilAktør(morId) + val barnAktør = tilAktør(barnId) + + every { + mockPersonopplysningerService.hentPersoninfoEnkel(any()) + } returns personInfo.getValue(INTEGRASJONER_FNR) + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(farAktør) + } returns PersonInfo(fødselsdato = LocalDate.of(1969, 5, 1), kjønn = Kjønn.MANN, navn = "Far Mocksen") + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(morAktør) + } returns PersonInfo(fødselsdato = LocalDate.of(1979, 5, 1), kjønn = Kjønn.KVINNE, navn = "Mor Mocksen") + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(barnAktør) + } returns PersonInfo( + fødselsdato = LocalDate.of(2009, 5, 1), + kjønn = Kjønn.MANN, + navn = "Barn Mocksen", + forelderBarnRelasjon = setOf( + ForelderBarnRelasjon( + farAktør, + FORELDERBARNRELASJONROLLE.FAR, + "Far Mocksen", + LocalDate.of(1969, 5, 1), + ), + ForelderBarnRelasjon( + morAktør, + FORELDERBARNRELASJONROLLE.MOR, + "Mor Mocksen", + LocalDate.of(1979, 5, 1), + ), + ), + ) + + every { + mockPersonopplysningerService.hentGjeldendeStatsborgerskap(any()) + } answers { + Statsborgerskap( + "NOR", + LocalDate.of(1990, 1, 25), + LocalDate.of(1990, 1, 25), + null, + ) + } + + every { + mockPersonopplysningerService.hentGjeldendeOpphold(any()) + } answers { + Opphold( + type = OPPHOLDSTILLATELSE.PERMANENT, + oppholdFra = LocalDate.of(1990, 1, 25), + oppholdTil = LocalDate.of(2499, 1, 1), + ) + } + + every { + mockPersonopplysningerService.hentLandkodeAlpha2UtenlandskBostedsadresse(any()) + } returns "NO" + + val ukjentId = "43125678910" + val ukjentAktør = tilAktør(ukjentId) + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(ukjentAktør) + } throws HttpClientErrorException(HttpStatus.NOT_FOUND, "ikke funnet") + + val feilId = "41235678910" + val feilIdAktør = tilAktør(feilId) + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(feilIdAktør) + } throws IntegrasjonException("feil id") + + return mockPersonopplysningerService + } + + @Bean + @Primary + fun mockEnvService(): EnvService { + val mockEnvService = mockk(relaxed = true) + + every { + mockEnvService.erProd() + } answers { + true + } + + every { + mockEnvService.erPreprod() + } answers { + true + } + + every { + mockEnvService.erDev() + } answers { + true + } + + return mockEnvService + } + + companion object { + fun clearFeatureToggleMocks( + mockFeatureToggleService: FeatureToggleService, + ) { + clearMocks(mockFeatureToggleService) + + val mockFeatureToggleServiceAnswer = System.getProperty("mockFeatureToggleAnswer")?.toBoolean() ?: true + + val featureSlot = slot() + every { + mockFeatureToggleService.isEnabled(capture(featureSlot)) + } answers { + System.getProperty(featureSlot.captured)?.toBoolean() ?: mockFeatureToggleServiceAnswer + } + every { + mockFeatureToggleService.isEnabled(capture(featureSlot), any()) + } answers { + System.getProperty(featureSlot.captured)?.toBoolean() ?: mockFeatureToggleServiceAnswer + } + } + + fun clearUnleashServiceMocks(mockUnleashService: UnleashService) { + val mockUnleashServiceAnswer = System.getProperty("mockFeatureToggleAnswer")?.toBoolean() ?: true + + val featureSlot = slot() + every { + mockUnleashService.isEnabled(toggleId = capture(featureSlot)) + } answers { + System.getProperty(featureSlot.captured)?.toBoolean() ?: mockUnleashServiceAnswer + } + every { + mockUnleashService.isEnabled(toggleId = capture(featureSlot), defaultValue = any()) + } answers { + System.getProperty(featureSlot.captured)?.toBoolean() ?: mockUnleashServiceAnswer + } + + every { + mockUnleashService.isEnabled(toggleId = capture(featureSlot), properties = any()) + } answers { + System.getProperty(featureSlot.captured)?.toBoolean() ?: mockUnleashServiceAnswer + } + } + + fun clearPdlIdentRestClient( + mockPdlIdentRestClient: PdlIdentRestClient, + ) { + clearMocks(mockPdlIdentRestClient) + + val identSlot = slot() + every { + mockPdlIdentRestClient.hentIdenter(capture(identSlot), true) + } answers { + listOf( + IdentInformasjon(identSlot.captured, false, "FOLKEREGISTERIDENT"), + IdentInformasjon(randomFnr(), true, "FOLKEREGISTERIDENT"), + ) + } + + val identSlot2 = slot() + every { + mockPdlIdentRestClient.hentIdenter(capture(identSlot2), false) + } answers { + listOf( + IdentInformasjon( + identSlot2.captured.substring(0, min(11, identSlot2.captured.length)), + false, + "FOLKEREGISTERIDENT", + ), + IdentInformasjon( + identSlot2.captured.substring(0, min(11, identSlot2.captured.length)) + "00", + false, + "AKTORID", + ), + ) + } + } + + fun clearPdlMocks( + mockPersonopplysningerService: PersonopplysningerService, + ) { + clearMocks(mockPersonopplysningerService) + + every { + mockPersonopplysningerService.hentGjeldendeStatsborgerskap(any()) + } answers { + Statsborgerskap( + "NOR", + LocalDate.of(1990, 1, 25), + LocalDate.of(1990, 1, 25), + null, + ) + } + + every { + mockPersonopplysningerService.hentGjeldendeOpphold(any()) + } answers { + Opphold( + type = OPPHOLDSTILLATELSE.PERMANENT, + oppholdFra = LocalDate.of(1990, 1, 25), + oppholdTil = LocalDate.of(2499, 1, 1), + ) + } + + every { + mockPersonopplysningerService.hentVergeData(any()) + } returns VergeData(false) + + every { + mockPersonopplysningerService.harVerge(any()) + } returns VergeResponse(false) + + every { + mockPersonopplysningerService.hentLandkodeAlpha2UtenlandskBostedsadresse(any()) + } returns "NO" + + val idSlotForHentPersoninfo = slot() + every { + mockPersonopplysningerService.hentPersoninfoEnkel(capture(idSlotForHentPersoninfo)) + } answers { + when (val id = idSlotForHentPersoninfo.captured.aktivFødselsnummer()) { + barnFnr[0], barnFnr[1] -> personInfo.getValue(id) + søkerFnr[0], søkerFnr[1] -> personInfo.getValue(id) + "09121079074" -> personInfo.getValue(id) + "10031000033" -> personInfo.getValue(id) + "04068203010" -> personInfo.getValue(id) + else -> personInfo.getValue(INTEGRASJONER_FNR) + } + } + + val idSlotPersoninfoNavnOgAdresse = slot() + every { + mockPersonopplysningerService.hentPersoninfoNavnOgAdresse(capture(idSlotPersoninfoNavnOgAdresse)) + } answers { + when (val id = idSlotPersoninfoNavnOgAdresse.captured.aktivFødselsnummer()) { + barnFnr[0], barnFnr[1] -> personInfo.getValue(id) + søkerFnr[0], søkerFnr[1] -> personInfo.getValue(id) + "09121079074" -> personInfo.getValue(id) + "10031000033" -> personInfo.getValue(id) + "04068203010" -> personInfo.getValue(id) + else -> personInfo.getValue(INTEGRASJONER_FNR) + } + } + + val idSlot = slot() + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(capture(idSlot)) + } answers { + when (val id = idSlot.captured.aktivFødselsnummer()) { + "00000000000" -> throw HttpClientErrorException( + HttpStatus.NOT_FOUND, + "Fant ikke forespurte data på person.", + ) + + barnFnr[0], barnFnr[1], "09121079074", "10031000033", "04068203010" -> personInfo.getValue(id) + + søkerFnr[0] -> personInfo.getValue(id).copy( + forelderBarnRelasjon = setOf( + ForelderBarnRelasjon( + aktør = tilAktør(barnFnr[0]), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + navn = personInfo.getValue(barnFnr[0]).navn, + fødselsdato = personInfo.getValue(barnFnr[0]).fødselsdato, + ), + ForelderBarnRelasjon( + aktør = tilAktør(barnFnr[1]), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + navn = personInfo.getValue(barnFnr[1]).navn, + fødselsdato = personInfo.getValue(barnFnr[1]).fødselsdato, + ), + ForelderBarnRelasjon( + aktør = tilAktør(søkerFnr[1]), + relasjonsrolle = FORELDERBARNRELASJONROLLE.MEDMOR, + ), + ), + ) + + søkerFnr[1] -> personInfo.getValue(id).copy( + forelderBarnRelasjon = setOf( + ForelderBarnRelasjon( + aktør = tilAktør(barnFnr[0]), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + navn = personInfo.getValue(barnFnr[0]).navn, + fødselsdato = personInfo.getValue(barnFnr[0]).fødselsdato, + ), + ForelderBarnRelasjon( + aktør = tilAktør(barnFnr[1]), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + navn = personInfo.getValue(barnFnr[1]).navn, + fødselsdato = personInfo.getValue(barnFnr[1]).fødselsdato, + ), + ForelderBarnRelasjon( + aktør = tilAktør(søkerFnr[0]), + relasjonsrolle = FORELDERBARNRELASJONROLLE.FAR, + ), + ), + ) + + søkerFnr[2] -> personInfo.getValue(id).copy( + forelderBarnRelasjon = setOf( + ForelderBarnRelasjon( + aktør = tilAktør(barnFnr[0]), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + navn = personInfo.getValue(barnFnr[0]).navn, + fødselsdato = personInfo.getValue(barnFnr[0]).fødselsdato, + ), + ForelderBarnRelasjon( + aktør = tilAktør(barnFnr[1]), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + navn = personInfo.getValue(barnFnr[1]).navn, + fødselsdato = personInfo.getValue(barnFnr[1]).fødselsdato, + adressebeskyttelseGradering = personInfo.getValue(barnFnr[1]).adressebeskyttelseGradering, + ), + ForelderBarnRelasjon( + aktør = tilAktør(søkerFnr[0]), + relasjonsrolle = FORELDERBARNRELASJONROLLE.FAR, + ), + ), + forelderBarnRelasjonMaskert = setOf( + ForelderBarnRelasjonMaskert( + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + adressebeskyttelseGradering = personInfo.getValue( + BARN_DET_IKKE_GIS_TILGANG_TIL_FNR, + ).adressebeskyttelseGradering!!, + ), + ), + ) + + INTEGRASJONER_FNR -> personInfo.getValue(id).copy( + forelderBarnRelasjon = setOf( + ForelderBarnRelasjon( + aktør = tilAktør(barnFnr[0]), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + navn = personInfo.getValue(barnFnr[0]).navn, + fødselsdato = personInfo.getValue(barnFnr[0]).fødselsdato, + ), + ForelderBarnRelasjon( + aktør = tilAktør(barnFnr[1]), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + navn = personInfo.getValue(barnFnr[1]).navn, + fødselsdato = personInfo.getValue(barnFnr[1]).fødselsdato, + ), + ForelderBarnRelasjon( + aktør = tilAktør(søkerFnr[1]), + relasjonsrolle = FORELDERBARNRELASJONROLLE.MEDMOR, + ), + ), + ) + + mockBarnAutomatiskBehandlingFnr -> personInfo.getValue(id) + mockBarnAutomatiskBehandling2Fnr -> personInfo.getValue(id) + mockSøkerAutomatiskBehandlingFnr -> personInfo.getValue(id) + mockBarnAutomatiskBehandlingSkalFeileFnr -> personInfo.getValue(id) + else -> personInfo.getValue(INTEGRASJONER_FNR) + } + } + + every { + mockPersonopplysningerService.hentAdressebeskyttelseSomSystembruker(capture(idSlot)) + } answers { + if (BARN_DET_IKKE_GIS_TILGANG_TIL_FNR == idSlot.captured.aktivFødselsnummer()) { + ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG + } else { + ADRESSEBESKYTTELSEGRADERING.UGRADERT + } + } + + every { mockPersonopplysningerService.harVerge(mockSøkerAutomatiskBehandlingAktør) } returns VergeResponse( + harVerge = false, + ) + } + + val søkerFnr = arrayOf("12345678910", "11223344556", "12345678911") + private val barnFødselsdatoer = arrayOf( + guttenBarnesenFødselsdato, + LocalDate.now().withDayOfMonth(18).minusYears(2), + ) + val barnFnr = arrayOf(barnFødselsdatoer[0].tilddMMyy() + "50033", barnFødselsdatoer[1].tilddMMyy() + "50033") + const val BARN_DET_IKKE_GIS_TILGANG_TIL_FNR = "12345678912" + const val INTEGRASJONER_FNR = "10000111111" + val bostedsadresse = Bostedsadresse( + matrikkeladresse = Matrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ), + ) + private val bostedsadresseHistorikk = mutableListOf( + Bostedsadresse( + angittFlyttedato = LocalDate.now().minusDays(15), + gyldigTilOgMed = null, + matrikkeladresse = Matrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ), + ), + Bostedsadresse( + angittFlyttedato = LocalDate.now().minusYears(1), + gyldigTilOgMed = LocalDate.now().minusDays(16), + matrikkeladresse = Matrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ), + ), + ) + + private val sivilstandHistorisk = listOf( + Sivilstand(type = SIVILSTAND.GIFT, gyldigFraOgMed = LocalDate.now().minusMonths(8)), + Sivilstand(type = SIVILSTAND.SKILT, gyldigFraOgMed = LocalDate.now().minusMonths(4)), + ) + + val personInfo = mapOf( + søkerFnr[0] to PersonInfo( + fødselsdato = LocalDate.of(1990, 2, 19), + kjønn = Kjønn.KVINNE, + navn = "Mor Moresen", + bostedsadresser = bostedsadresseHistorikk, + sivilstander = sivilstandHistorisk, + statsborgerskap = listOf( + Statsborgerskap( + land = "DNK", + bekreftelsesdato = LocalDate.now().minusYears(1), + gyldigFraOgMed = null, + gyldigTilOgMed = null, + ), + ), + ), + søkerFnr[1] to PersonInfo( + fødselsdato = LocalDate.of(1995, 2, 19), + bostedsadresser = mutableListOf(), + sivilstander = listOf( + Sivilstand( + type = SIVILSTAND.GIFT, + gyldigFraOgMed = LocalDate.now().minusMonths(8), + ), + ), + kjønn = Kjønn.MANN, + navn = "Far Faresen", + ), + søkerFnr[2] to PersonInfo( + fødselsdato = LocalDate.of(1985, 7, 10), + bostedsadresser = mutableListOf(), + sivilstander = listOf( + Sivilstand( + type = SIVILSTAND.GIFT, + gyldigFraOgMed = LocalDate.now().minusMonths(8), + ), + ), + kjønn = Kjønn.KVINNE, + navn = "Moder Jord", + adressebeskyttelseGradering = ADRESSEBESKYTTELSEGRADERING.UGRADERT, + ), + barnFnr[0] to PersonInfo( + fødselsdato = barnFødselsdatoer[0], + bostedsadresser = mutableListOf(bostedsadresse), + sivilstander = listOf( + Sivilstand( + type = SIVILSTAND.UOPPGITT, + gyldigFraOgMed = LocalDate.now().minusMonths(8), + ), + ), + kjønn = Kjønn.MANN, + navn = "Gutten Barnesen", + ), + barnFnr[1] to PersonInfo( + fødselsdato = barnFødselsdatoer[1], + bostedsadresser = mutableListOf(bostedsadresse), + sivilstander = listOf( + Sivilstand( + type = SIVILSTAND.GIFT, + gyldigFraOgMed = LocalDate.now().minusMonths(8), + ), + ), + kjønn = Kjønn.KVINNE, + navn = "Jenta Barnesen", + adressebeskyttelseGradering = ADRESSEBESKYTTELSEGRADERING.FORTROLIG, + ), + mockBarnAutomatiskBehandlingFnr to mockBarnAutomatiskBehandling, + mockBarnAutomatiskBehandling2Fnr to mockBarnAutomatiskBehandling2, + mockSøkerAutomatiskBehandlingFnr to mockSøkerAutomatiskBehandling, + mockBarnAutomatiskBehandlingSkalFeileFnr to mockBarnAutomatiskBehandlingSkalFeile, + "09121079074" to PersonInfo( + fødselsdato = LocalDate.of(2010, 12, 9), + bostedsadresser = mutableListOf(bostedsadresse), + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UGIFT)), + kjønn = Kjønn.KVINNE, + navn = "Litt eldre barn", + adressebeskyttelseGradering = ADRESSEBESKYTTELSEGRADERING.UGRADERT, + ), + "10031000033" to PersonInfo( + fødselsdato = LocalDate.of(2015, 2, 10), + bostedsadresser = mutableListOf(bostedsadresse), + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UGIFT)), + kjønn = Kjønn.KVINNE, + navn = "Jenten 2015", + adressebeskyttelseGradering = ADRESSEBESKYTTELSEGRADERING.UGRADERT, + ), + "04068203010" to PersonInfo( + fødselsdato = LocalDate.of(1982, 6, 4), + bostedsadresser = mutableListOf(), + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UGIFT)), + kjønn = Kjønn.KVINNE, + navn = "Moder Jord", + adressebeskyttelseGradering = ADRESSEBESKYTTELSEGRADERING.UGRADERT, + ), + INTEGRASJONER_FNR to PersonInfo( + fødselsdato = LocalDate.of(1965, 2, 19), + bostedsadresser = mutableListOf(bostedsadresse), + kjønn = Kjønn.KVINNE, + navn = "Mor Integrasjon person", + sivilstander = sivilstandHistorisk, + ), + BARN_DET_IKKE_GIS_TILGANG_TIL_FNR to PersonInfo( + fødselsdato = LocalDate.of(2019, 6, 22), + bostedsadresser = mutableListOf(bostedsadresse), + sivilstander = listOf( + Sivilstand( + type = SIVILSTAND.UGIFT, + gyldigFraOgMed = LocalDate.now().minusMonths(8), + ), + ), + kjønn = Kjønn.KVINNE, + navn = "Maskert Banditt", + adressebeskyttelseGradering = ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG, + ), + ) + } + + @Bean + @Profile("mock-leader-client") + @Primary + fun mockLeaderClient() { + mockkStatic(LeaderClient::class) + every { LeaderClient.isLeader() } returns true + } +} + +fun mockHentPersoninfoForMedIdenter( + mockPersonopplysningerService: PersonopplysningerService, + søkerFnr: String, + barnFnr: String, +) { + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(eq(tilAktør(barnFnr))) + } returns PersonInfo( + fødselsdato = LocalDate.of(2018, 5, 1), + kjønn = Kjønn.KVINNE, + navn = "Barn Barnesen", + sivilstander = listOf(Sivilstand(type = SIVILSTAND.GIFT, gyldigFraOgMed = LocalDate.now().minusMonths(8))), + ) + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(eq(tilAktør(søkerFnr))) + } returns PersonInfo(fødselsdato = LocalDate.of(1990, 2, 19), kjønn = Kjønn.KVINNE, navn = "Mor Moresen") +} + +fun tilAktør(fnr: String, toSisteSiffrer: String = "00") = Aktør(fnr + toSisteSiffrer).also { + it.personidenter.add(Personident(fnr, aktør = it)) +} + +val TEST_PDF = ClientMocks::class.java.getResource("/dokument/mockvedtak.pdf").readBytes() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/EfSakRestClientMock.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/EfSakRestClientMock.kt new file mode 100644 index 000000000..1d28e3ed1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/EfSakRestClientMock.kt @@ -0,0 +1,48 @@ +package no.nav.familie.ba.sak.config + +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import no.nav.familie.ba.sak.integrasjoner.ef.EfSakRestClient +import no.nav.familie.kontrakter.felles.ef.Datakilde +import no.nav.familie.kontrakter.felles.ef.EksternPeriode +import no.nav.familie.kontrakter.felles.ef.EksternePerioderResponse +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import java.time.LocalDate + +@TestConfiguration +class EfSakRestClientMock { + + @Bean + @Primary + fun mockEfSakRestClient(): EfSakRestClient { + val efSakRestClient = mockk() + + clearEfSakRestMocks(efSakRestClient) + + return efSakRestClient + } + + companion object { + fun clearEfSakRestMocks(efSakRestClient: EfSakRestClient) { + clearMocks(efSakRestClient) + + val hentPerioderMedFullOvergangsstønadSlot = slot() + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(capture(hentPerioderMedFullOvergangsstønadSlot)) } answers { + EksternePerioderResponse( + perioder = listOf( + EksternPeriode( + personIdent = hentPerioderMedFullOvergangsstønadSlot.captured, + fomDato = LocalDate.now().minusYears(2), + datakilde = Datakilde.EF, + tomDato = LocalDate.now().minusMonths(3), + ), + ), + ) + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/IntegrasjonClientMock.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/IntegrasjonClientMock.kt new file mode 100644 index 000000000..b2cea3a55 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/IntegrasjonClientMock.kt @@ -0,0 +1,252 @@ +package no.nav.familie.ba.sak.config + +import com.fasterxml.jackson.module.kotlin.readValue +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.isMockKMock +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import no.nav.familie.ba.sak.config.ClientMocks.Companion.BARN_DET_IKKE_GIS_TILGANG_TIL_FNR +import no.nav.familie.ba.sak.config.ClientMocks.Companion.søkerFnr +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollClient +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Arbeidsfordelingsenhet +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.LogiskVedleggResponse +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.OppdaterJournalpostResponse +import no.nav.familie.ba.sak.integrasjoner.lagTestJournalpost +import no.nav.familie.ba.sak.integrasjoner.lagTestOppgaveDTO +import no.nav.familie.kontrakter.felles.dokarkiv.ArkiverDokumentResponse +import no.nav.familie.kontrakter.felles.kodeverk.BeskrivelseDto +import no.nav.familie.kontrakter.felles.kodeverk.BetydningDto +import no.nav.familie.kontrakter.felles.kodeverk.KodeverkDto +import no.nav.familie.kontrakter.felles.kodeverk.KodeverkSpråk +import no.nav.familie.kontrakter.felles.navkontor.NavKontorEnhet +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveResponseDto +import no.nav.familie.kontrakter.felles.oppgave.OppgaveResponse +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.kontrakter.felles.organisasjon.Organisasjon +import no.nav.familie.kontrakter.felles.tilgangskontroll.Tilgang +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import org.springframework.core.io.ClassPathResource +import java.io.BufferedReader +import java.time.LocalDate +import java.time.Month +import java.util.UUID + +@TestConfiguration +@Profile("dev", "postgres") +class IntegrasjonClientMock { + + @Bean + @Primary + fun mockIntegrasjonClient(): IntegrasjonClient { + val mockIntegrasjonClient = mockk(relaxed = false) + + clearIntegrasjonMocks(mockIntegrasjonClient) + + return mockIntegrasjonClient + } + + @Bean + @Primary + fun mockFamilieIntegrasjonerTilgangskontrollClient(): FamilieIntegrasjonerTilgangskontrollClient { + val mockFamilieIntegrasjonerTilgangskontrollClient = + mockk(relaxed = false) + + clearMockFamilieIntegrasjonerTilgangskontrollClient(mockFamilieIntegrasjonerTilgangskontrollClient) + + return mockFamilieIntegrasjonerTilgangskontrollClient + } + + companion object { + fun clearIntegrasjonMocks(mockIntegrasjonClient: IntegrasjonClient) { + /** + * Mulig årsak til at appen må bruke dirties i testene. + * Denne bønna blir initialisert av mockk, men etter noen av testene + * er det ikke lenger en mockk bønne! + */ + if (isMockKMock(mockIntegrasjonClient)) { + clearMocks(mockIntegrasjonClient) + } else { + return + } + + every { mockIntegrasjonClient.hentJournalpost(any()) } returns lagTestJournalpost( + søkerFnr[0], + UUID.randomUUID().toString(), + ) + + every { mockIntegrasjonClient.hentJournalposterForBruker(any()) } returns listOf( + lagTestJournalpost( + søkerFnr[0], + UUID.randomUUID().toString(), + ), + lagTestJournalpost( + søkerFnr[0], + UUID.randomUUID().toString(), + ), + ) + + every { mockIntegrasjonClient.finnOppgaveMedId(any()) } returns + lagTestOppgaveDTO(1L) + + every { mockIntegrasjonClient.hentOppgaver(any()) } returns + FinnOppgaveResponseDto( + 2, + listOf(lagTestOppgaveDTO(1L), lagTestOppgaveDTO(2L, Oppgavetype.BehandleSak, "Z999999")), + ) + + every { mockIntegrasjonClient.opprettOppgave(any()) } returns + OppgaveResponse(12345678L) + + every { mockIntegrasjonClient.patchOppgave(any()) } returns + OppgaveResponse(12345678L) + + every { mockIntegrasjonClient.tilordneEnhetForOppgave(any(), any()) } returns + OppgaveResponse(12345678L) + + every { mockIntegrasjonClient.fordelOppgave(any(), any()) } returns + OppgaveResponse(12345678L) + + every { mockIntegrasjonClient.fjernBehandlesAvApplikasjon(any()) } returns + OppgaveResponse(12345678L) + + every { mockIntegrasjonClient.oppdaterJournalpost(any(), any()) } returns + OppdaterJournalpostResponse("1234567") + + every { + mockIntegrasjonClient.journalførDokument(any()) + } returns ArkiverDokumentResponse(ferdigstilt = true, journalpostId = "journalpostId") + + every { + mockIntegrasjonClient.leggTilLogiskVedlegg(any(), any()) + } returns LogiskVedleggResponse(12345678) + + every { + mockIntegrasjonClient.slettLogiskVedlegg(any(), any()) + } returns LogiskVedleggResponse(12345678) + + every { mockIntegrasjonClient.distribuerBrev(any()) } returns "bestillingsId" + + every { mockIntegrasjonClient.ferdigstillJournalpost(any(), any()) } just runs + + every { mockIntegrasjonClient.ferdigstillOppgave(any()) } just runs + + every { mockIntegrasjonClient.hentBehandlendeEnhet(any()) } returns + listOf(Arbeidsfordelingsenhet("4833", "NAV Familie- og pensjonsytelser Oslo 1")) + + every { mockIntegrasjonClient.hentDokument(any(), any()) } returns TEST_PDF + + every { mockIntegrasjonClient.hentArbeidsforhold(any(), any()) } returns emptyList() + + every { mockIntegrasjonClient.hentBehandlendeEnhet(any()) } returns listOf( + Arbeidsfordelingsenhet( + "100", + "NAV Familie- og pensjonsytelser Oslo 1", + ), + ) + + every { mockIntegrasjonClient.hentEnhet(any()) } returns NavKontorEnhet( + 101, + "NAV Familie- og pensjonsytelser Oslo 1", + "101", + "", + ) + + every { mockIntegrasjonClient.opprettSkyggesak(any(), any()) } just runs + + every { mockIntegrasjonClient.hentLand(any()) } returns "Testland" + every { mockIntegrasjonClient.hentLandkoderISO2() } returns hentLandkoderISO2() + + every { mockIntegrasjonClient.hentAlleEØSLand() } returns hentKodeverkLand() + + every { mockIntegrasjonClient.oppdaterOppgave(any(), any()) } just runs + + every { mockIntegrasjonClient.hentOrganisasjon(any()) } answers { + Organisasjon( + "998765432", + "Testinstitusjon", + ) + } + } + + fun clearMockFamilieIntegrasjonerTilgangskontrollClient(mockFamilieIntegrasjonerTilgangskontrollClient: FamilieIntegrasjonerTilgangskontrollClient) { + clearMocks(mockFamilieIntegrasjonerTilgangskontrollClient) + + every { + mockFamilieIntegrasjonerTilgangskontrollClient.sjekkTilgangTilPersoner(any()) + } answers { + val identer = firstArg>() + identer.map { Tilgang(personIdent = it, harTilgang = it != BARN_DET_IKKE_GIS_TILGANG_TIL_FNR) } + } + } + + fun FamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang( + map: Map, + slot: MutableList> = mutableListOf(), + ) { + every { sjekkTilgangTilPersoner(capture(slot)) } answers { + val arg = firstArg>() + map.entries.filter { arg.contains(it.key) }.map { Tilgang(personIdent = it.key, harTilgang = it.value) } + } + } + + fun FamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang( + harTilgang: Boolean = false, + slot: MutableList> = mutableListOf(), + ) { + every { sjekkTilgangTilPersoner(capture(slot)) } answers { + firstArg>().map { Tilgang(personIdent = it, harTilgang = harTilgang) } + } + } + + fun initEuKodeverk(integrasjonClient: IntegrasjonClient) { + every { integrasjonClient.hentAlleEØSLand() } returns hentKodeverkLand() + } + + internal fun hentKodeverkLand(): KodeverkDto { + val beskrivelsePolen = BeskrivelseDto("POL", "") + val betydningPolen = BetydningDto(FOM_2004, TOM_9999, mapOf(KodeverkSpråk.BOKMÅL.kode to beskrivelsePolen)) + val beskrivelseTyskland = BeskrivelseDto("DEU", "") + val betydningTyskland = + BetydningDto(FOM_1900, TOM_9999, mapOf(KodeverkSpråk.BOKMÅL.kode to beskrivelseTyskland)) + val beskrivelseDanmark = BeskrivelseDto("DNK", "") + val betydningDanmark = + BetydningDto(FOM_1990, TOM_9999, mapOf(KodeverkSpråk.BOKMÅL.kode to beskrivelseDanmark)) + val beskrivelseUK = BeskrivelseDto("GBR", "") + val betydningUK = BetydningDto(FOM_1900, TOM_2010, mapOf(KodeverkSpråk.BOKMÅL.kode to beskrivelseUK)) + + return KodeverkDto( + betydninger = mapOf( + "POL" to listOf(betydningPolen), + "DEU" to listOf(betydningTyskland), + "DNK" to listOf(betydningDanmark), + "GBR" to listOf(betydningUK), + ), + ) + } + + val FOM_1900 = LocalDate.of(1900, Month.JANUARY, 1) + val FOM_1990 = LocalDate.of(1990, Month.JANUARY, 1) + val FOM_2004 = LocalDate.of(2004, Month.JANUARY, 1) + val TOM_2010 = LocalDate.of(2009, Month.DECEMBER, 31) + val TOM_9999 = LocalDate.of(9999, Month.DECEMBER, 31) + + data class LandkodeISO2( + val code: String, + val name: String, + ) + + fun hentLandkoderISO2(): Map { + val landkoder = + ClassPathResource("landkoder/landkoder.json").inputStream.bufferedReader().use(BufferedReader::readText) + + return objectMapper.readValue>(landkoder).associate { it.code to it.name } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/KafkaAivenErrorHandlerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/KafkaAivenErrorHandlerTest.kt new file mode 100644 index 000000000..3c5dbfe48 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/KafkaAivenErrorHandlerTest.kt @@ -0,0 +1,79 @@ +package no.nav.familie.ba.sak.config + +import io.mockk.MockKAnnotations +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import org.apache.kafka.clients.consumer.Consumer +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.springframework.kafka.listener.MessageListenerContainer + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class KafkaAivenErrorHandlerTest { + + @MockK(relaxed = true) + lateinit var container: MessageListenerContainer + + @MockK(relaxed = true) + lateinit var consumer: Consumer<*, *> + + @InjectMockKs + lateinit var errorHandler: KafkaAivenErrorHandler + + @BeforeEach + internal fun setUp() { + MockKAnnotations.init(this) + } + + @Test + fun `handle skal stoppe container hvis man mottar feil med en tom liste med records`() { + assertThatThrownBy { + errorHandler.handleRemaining( + RuntimeException("Feil i test"), + emptyList(), + consumer, + container, + ) + } + .hasMessageNotContaining("Feil i test") + .hasMessageContaining("Stopped container") + .hasStackTraceContaining("Sjekk securelogs for mer info") + .hasCauseExactlyInstanceOf(Exception::class.java) + } + + @Test + fun `handle skal stoppe container hvis man mottar feil med en liste med records`() { + val consumerRecord = ConsumerRecord("topic", 1, 1, 1, "record") + assertThatThrownBy { + errorHandler.handleRemaining( + RuntimeException("Feil i test"), + listOf(consumerRecord), + consumer, + container, + ) + } + .hasMessageNotContaining("Feil i test") + .hasMessageContaining("Stopped container") + .hasStackTraceContaining("Sjekk securelogs for mer info") + .hasCauseExactlyInstanceOf(Exception::class.java) + } + + @Test + fun `handle skal stoppe container hvis man mottar feil hvor liste med records er empty`() { + assertThatThrownBy { + errorHandler.handleRemaining( + RuntimeException("Feil i test"), + emptyList(), + consumer, + container, + ) + } + .hasMessageNotContaining("Feil i test") + .hasMessageContaining("Stopped container") + .hasStackTraceContaining("Sjekk securelogs for mer info") + .hasCauseExactlyInstanceOf(Exception::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/LocalDateServiceTestConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/LocalDateServiceTestConfig.kt new file mode 100644 index 000000000..25713d2a9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/LocalDateServiceTestConfig.kt @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.config + +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.LocalDateService +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import java.time.LocalDate + +@TestConfiguration +class LocalDateServiceTestConfig { + + @Bean + @Profile("mock-localdate-service") + @Primary + fun mockLocalDateService(): LocalDateService { + val mockLocalDateService = mockk() + + clearLocalDateServiceMocks(mockLocalDateService) + + return mockLocalDateService + } + + companion object { + fun clearLocalDateServiceMocks(mockLocalDateService: LocalDateService) { + clearMocks(mockLocalDateService) + + every { mockLocalDateService.now() } returns LocalDate.now() + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/OAuth2AccessTokenTestConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/OAuth2AccessTokenTestConfig.kt new file mode 100644 index 000000000..ff2c62f30 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/OAuth2AccessTokenTestConfig.kt @@ -0,0 +1,28 @@ +package no.nav.familie.ba.sak.config + +import io.mockk.every +import io.mockk.mockk +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenService +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile + +@TestConfiguration +class OAuth2AccessTokenTestConfig { + + @Bean + @Primary + @Profile("mock-oauth") + fun oAuth2AccessTokenServiceMock(): OAuth2AccessTokenService { + val tokenMockService: OAuth2AccessTokenService = mockk() + every { tokenMockService.getAccessToken(any()) } returns OAuth2AccessTokenResponse( + "Mock-token-response", + 60, + 60, + null, + ) + return tokenMockService + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/PdlRestClientTestConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/PdlRestClientTestConfig.kt new file mode 100644 index 000000000..7d39efe3f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/PdlRestClientTestConfig.kt @@ -0,0 +1,46 @@ +package no.nav.familie.ba.sak.config + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.integrasjoner.pdl.PdlRestClient +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import no.nav.familie.kontrakter.felles.personopplysning.Sivilstand +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import java.time.LocalDate + +@TestConfiguration +class PdlRestClientTestConfig { + + @Bean + @Profile("mock-pdl-client") + @Primary + fun pdlRestClientMock(): PdlRestClient { + val klient = mockk(relaxed = true) + + every { + klient.hentPerson(any(), any()) + } returns PersonInfo( + fødselsdato = LocalDate.of(1980, 5, 12), + navn = "Kari Normann", + kjønn = Kjønn.KVINNE, + forelderBarnRelasjon = setOf( + ForelderBarnRelasjon( + aktør = tilAktør("12345678910"), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + ), + ), + adressebeskyttelseGradering = null, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UGIFT)), + dødsfall = null, + kontaktinformasjonForDoedsbo = null, + ) + return klient + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/RestTemplateTestConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/RestTemplateTestConfig.kt new file mode 100644 index 000000000..0a89fe813 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/RestTemplateTestConfig.kt @@ -0,0 +1,95 @@ +package no.nav.familie.ba.sak.config + +import no.nav.familie.http.interceptor.ConsumerIdClientInterceptor +import no.nav.familie.http.interceptor.MdcValuesPropagatingClientInterceptor +import no.nav.familie.kontrakter.felles.objectMapper +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Import +import org.springframework.context.annotation.Profile +import org.springframework.http.converter.ByteArrayHttpMessageConverter +import org.springframework.http.converter.StringHttpMessageConverter +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter +import org.springframework.web.client.RestOperations +import org.springframework.web.client.RestTemplate +import java.nio.charset.StandardCharsets +import java.time.Duration + +@TestConfiguration +@Import( + ConsumerIdClientInterceptor::class, + MdcValuesPropagatingClientInterceptor::class, +) +@Profile("mock-rest-template-config") +class RestTemplateTestConfig { + + @Bean + fun restTemplate(): RestTemplate { + return RestTemplate( + listOf( + StringHttpMessageConverter(StandardCharsets.UTF_8), + ByteArrayHttpMessageConverter(), + MappingJackson2HttpMessageConverter(objectMapper), + ), + ) + } + + @Bean + fun restOperations( + consumerIdClientInterceptor: ConsumerIdClientInterceptor, + mdcValuesPropagatingClientInterceptor: MdcValuesPropagatingClientInterceptor, + ): RestOperations { + return RestTemplateBuilder() + .interceptors(consumerIdClientInterceptor, mdcValuesPropagatingClientInterceptor) + .additionalMessageConverters( + ByteArrayHttpMessageConverter(), + MappingJackson2HttpMessageConverter(objectMapper), + ) + .build() + } + + @Bean("jwtBearerClientCredentials") + fun restTemplateJwtBearerClientCredentials( + consumerIdClientInterceptor: ConsumerIdClientInterceptor, + mdcValuesPropagatingClientInterceptor: MdcValuesPropagatingClientInterceptor, + ): RestOperations { + return RestTemplateBuilder() + .additionalInterceptors(consumerIdClientInterceptor, mdcValuesPropagatingClientInterceptor) + .additionalMessageConverters(MappingJackson2HttpMessageConverter(objectMapper)) + .build() + } + + @Bean("jwtBearer") + fun restTemplateJwtBearer( + consumerIdClientInterceptor: ConsumerIdClientInterceptor, + mdcValuesPropagatingClientInterceptor: MdcValuesPropagatingClientInterceptor, + ): RestOperations { + return RestTemplateBuilder() + .additionalInterceptors(consumerIdClientInterceptor, mdcValuesPropagatingClientInterceptor) + .additionalMessageConverters(MappingJackson2HttpMessageConverter(objectMapper)) + .build() + } + + @Bean("jwtBearerMedLangTimeout") + fun restTemplateJwtBearerMedLangTimeout( + consumerIdClientInterceptor: ConsumerIdClientInterceptor, + mdcValuesPropagatingClientInterceptor: MdcValuesPropagatingClientInterceptor, + ): RestOperations { + return RestTemplateBuilder() + .additionalInterceptors(consumerIdClientInterceptor, mdcValuesPropagatingClientInterceptor) + .additionalMessageConverters(MappingJackson2HttpMessageConverter(objectMapper)) + .build() + } + + @Bean + fun restTemplateBuilderMedProxy( + consumerIdClientInterceptor: ConsumerIdClientInterceptor, + mdcValuesPropagatingClientInterceptor: MdcValuesPropagatingClientInterceptor, + ): RestTemplateBuilder { + return RestTemplateBuilder() + .setConnectTimeout(Duration.ofSeconds(5)) + .additionalInterceptors(consumerIdClientInterceptor, mdcValuesPropagatingClientInterceptor) + .setReadTimeout(Duration.ofSeconds(5)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/SamhandlerTestConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/SamhandlerTestConfig.kt new file mode 100644 index 000000000..f8b31ac84 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/SamhandlerTestConfig.kt @@ -0,0 +1,57 @@ +package no.nav.familie.ba.sak.config + +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.integrasjoner.samhandler.SamhandlerKlient +import no.nav.familie.kontrakter.ba.tss.SamhandlerAdresse +import no.nav.familie.kontrakter.ba.tss.SamhandlerInfo +import no.nav.familie.kontrakter.ba.tss.SøkSamhandlerInfo +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile + +@TestConfiguration +class SamhandlerTestConfig { + + @Bean + @Profile("mock-økonomi") + @Primary + fun mockSamhandlerKlient(): SamhandlerKlient { + val mockSamhandlerKlient: SamhandlerKlient = mockk() + + clearSamhandlerKlient(mockSamhandlerKlient) + + return mockSamhandlerKlient + } + + companion object { + fun clearSamhandlerKlient(samhandlerKlient: SamhandlerKlient) { + clearMocks(samhandlerKlient) + every { samhandlerKlient.hentSamhandler(any()) } returns samhandlereInfoMock.first() + every { samhandlerKlient.søkSamhandlere(any(), any(), any(), any()) } returns SøkSamhandlerInfo( + false, + samhandlereInfoMock, + ) + } + } +} + +val samhandlereInfoMock = listOf( + SamhandlerInfo( + "80000999999", + "INSTUTISJON 1", + listOf( + SamhandlerAdresse(listOf("Instutisjonsnsveien 1"), "0110", "Oslo", "Arbeidsadresse"), + SamhandlerAdresse(listOf("Postboks 123"), "0110", "Oslo", "Postadresse"), + ), + orgNummer = "974652269", + ), + SamhandlerInfo( + "80000888888", + "INSTUTISJON 2", + listOf(SamhandlerAdresse(listOf("Instutisjonsnsveien 2"), "1892", "Degernes", "Arbeidsadresse")), + orgNummer = "974652269", + ), +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/SanityKlientMock.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/SanityKlientMock.kt new file mode 100644 index 000000000..c8a5f2033 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/SanityKlientMock.kt @@ -0,0 +1,62 @@ +package no.nav.familie.ba.sak.config + +import no.nav.familie.ba.sak.integrasjoner.sanity.SanityKlient +import no.nav.familie.ba.sak.kjerne.brev.domene.ISanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile + +@TestConfiguration +class SanityKlientMock { + @Bean + @Profile("mock-sanity-client") + @Primary + fun mockSanityClient(): SanityKlient { + return testSanityKlient + } +} + +val testSanityKlient = TestSantityKlient() + +class TestSantityKlient : SanityKlient("ba-brev", restTemplate) { + private val begrunnelser: List by lazy { + super.hentBegrunnelser() + } + private val eøsBegrunnelser: List by lazy { + super.hentEØSBegrunnelser() + } + + override fun hentBegrunnelser(): List { + return begrunnelser + } + + override fun hentEØSBegrunnelser(): List { + return eøsBegrunnelser + } + + fun hentBegrunnelserMap(): Map { + val enumVerdier = Standardbegrunnelse.values().associateBy { it.sanityApiNavn } + val begrunnelser = hentBegrunnelser() + return tilMap(begrunnelser, enumVerdier) + } + + fun hentEØSBegrunnelserMap(): Map { + val enumVerdier = EØSStandardbegrunnelse.values().associateBy { it.sanityApiNavn } + val begrunnelser = hentEØSBegrunnelser() + return tilMap(begrunnelser, enumVerdier) + } + + private fun tilMap( + begrunnelser: List, + enumVerdier: Map, + ): Map = + begrunnelser.mapNotNull { sanityBegrunnelse -> + enumVerdier[sanityBegrunnelse.apiNavn]?.let { it to sanityBegrunnelse } + }.toMap() +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/TilbakekrevingKlientTestConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/TilbakekrevingKlientTestConfig.kt new file mode 100644 index 000000000..9ea70bd1f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/TilbakekrevingKlientTestConfig.kt @@ -0,0 +1,39 @@ +package no.nav.familie.ba.sak.config + +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.kjerne.tilbakekreving.TilbakekrevingKlient +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile + +@TestConfiguration +class TilbakekrevingKlientTestConfig { + + @Bean + @Profile("mock-tilbakekreving-klient") + @Primary + fun mockTilbakekrevingKlient(): TilbakekrevingKlient { + val tilbakekrevingKlient: TilbakekrevingKlient = mockk() + + clearTilbakekrevingKlientMocks(tilbakekrevingKlient) + + return tilbakekrevingKlient + } + + companion object { + fun clearTilbakekrevingKlientMocks(mockTilbakekrevingKlient: TilbakekrevingKlient) { + clearMocks(mockTilbakekrevingKlient) + + every { mockTilbakekrevingKlient.hentForhåndsvisningVarselbrev(any()) } returns TEST_PDF + + every { mockTilbakekrevingKlient.opprettTilbakekrevingBehandling(any()) } returns "id1" + + every { mockTilbakekrevingKlient.harÅpenTilbakekrevingsbehandling(any()) } returns false + + every { mockTilbakekrevingKlient.hentTilbakekrevingsbehandlinger(any()) } returns emptyList() + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/\303\230konomiTestConfig.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/\303\230konomiTestConfig.kt" new file mode 100644 index 000000000..014cfb98a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/config/\303\230konomiTestConfig.kt" @@ -0,0 +1,181 @@ +package no.nav.familie.ba.sak.config + +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiKlient +import no.nav.familie.kontrakter.felles.oppdrag.OppdragStatus +import no.nav.familie.kontrakter.felles.simulering.BetalingType +import no.nav.familie.kontrakter.felles.simulering.DetaljertSimuleringResultat +import no.nav.familie.kontrakter.felles.simulering.FagOmrådeKode +import no.nav.familie.kontrakter.felles.simulering.MottakerType +import no.nav.familie.kontrakter.felles.simulering.PosteringType +import no.nav.familie.kontrakter.felles.simulering.SimuleringMottaker +import no.nav.familie.kontrakter.felles.simulering.SimulertPostering +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import java.time.LocalDate + +@TestConfiguration +class ØkonomiTestConfig { + + @Bean + @Profile("mock-økonomi") + @Primary + fun mockØkonomiKlient(): ØkonomiKlient { + val økonomiKlient: ØkonomiKlient = mockk() + + clearØkonomiMocks(økonomiKlient) + + return økonomiKlient + } + + companion object { + fun clearØkonomiMocks(økonomiKlient: ØkonomiKlient) { + clearMocks(økonomiKlient) + + val iverksettRespons = "Mocksvar fra Økonomi-klient" + every { økonomiKlient.iverksettOppdrag(any()) } returns iverksettRespons + + val hentStatusRespons = OppdragStatus.KVITTERT_OK + + every { økonomiKlient.hentStatus(any()) } returns hentStatusRespons + + every { økonomiKlient.hentSimulering(any()) } returns DetaljertSimuleringResultat(simuleringMottakerMock) + } + } +} + +val simulertPosteringMock = listOf( + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-09-01"), + tom = LocalDate.parse("2019-09-30"), + betalingType = BetalingType.DEBIT, + beløp = 50.0.toBigDecimal(), + posteringType = PosteringType.YTELSE, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-09-01"), + tom = LocalDate.parse("2019-09-30"), + betalingType = BetalingType.DEBIT, + beløp = 1004.0.toBigDecimal(), + posteringType = PosteringType.YTELSE, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-09-01"), + tom = LocalDate.parse("2019-09-30"), + betalingType = BetalingType.DEBIT, + beløp = 50.0.toBigDecimal(), + posteringType = PosteringType.FEILUTBETALING, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-09-01"), + tom = LocalDate.parse("2019-09-30"), + betalingType = BetalingType.KREDIT, + beløp = (-50.0).toBigDecimal(), + posteringType = PosteringType.MOTP, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-09-01"), + tom = LocalDate.parse("2019-09-30"), + betalingType = BetalingType.KREDIT, + beløp = (-1054.0).toBigDecimal(), + posteringType = PosteringType.YTELSE, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-10-01"), + tom = LocalDate.parse("2019-10-31"), + betalingType = BetalingType.DEBIT, + beløp = 50.0.toBigDecimal(), + posteringType = PosteringType.YTELSE, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-10-01"), + tom = LocalDate.parse("2019-10-31"), + betalingType = BetalingType.DEBIT, + beløp = 1004.0.toBigDecimal(), + posteringType = PosteringType.YTELSE, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-10-01"), + tom = LocalDate.parse("2019-10-31"), + betalingType = BetalingType.DEBIT, + beløp = 50.0.toBigDecimal(), + posteringType = PosteringType.FEILUTBETALING, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-10-01"), + tom = LocalDate.parse("2019-10-31"), + betalingType = BetalingType.KREDIT, + beløp = (-50.0).toBigDecimal(), + posteringType = PosteringType.MOTP, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-10-01"), + tom = LocalDate.parse("2019-10-31"), + betalingType = BetalingType.KREDIT, + beløp = (-1054.0).toBigDecimal(), + posteringType = PosteringType.YTELSE, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2021-04-01"), + tom = LocalDate.parse("2021-04-30"), + betalingType = BetalingType.DEBIT, + beløp = 1054.0.toBigDecimal(), + posteringType = PosteringType.YTELSE, + forfallsdato = LocalDate.parse("2024-05-10"), + utenInntrekk = false, + erFeilkonto = null, + ), +) + +val simuleringMottakerMock = listOf( + SimuleringMottaker( + simulertPostering = simulertPosteringMock, + mottakerType = MottakerType.BRUKER, + mottakerNummer = "12345678910", + ), +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/behandling/Kj\303\270rRevurdering.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/behandling/Kj\303\270rRevurdering.kt" new file mode 100644 index 000000000..cc812a80f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/behandling/Kj\303\270rRevurdering.kt" @@ -0,0 +1,405 @@ +package no.nav.familie.ba.sak.datagenerator.behandling + +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.config.testSanityKlient +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertPersonResultat +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.steg.StatusFraOppdragMedTask +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.steg.domene.JournalførVedtaksbrevDTO +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.tilMinimertVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.tilMinimertePersoner +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.tilUtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.erFørsteVedtaksperiodePåFagsak +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.hentGyldigeBegrunnelserForVedtaksperiodeMinimert +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.hentYtelserForSøkerForrigeMåned +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.ytelseErFraForrigePeriode +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.task.DistribuerDokumentDTO +import no.nav.familie.ba.sak.task.JournalførVedtaksbrevTask +import no.nav.familie.ba.sak.task.StatusFraOppdragTask +import no.nav.familie.ba.sak.task.dto.FAGSYSTEM +import no.nav.familie.ba.sak.task.dto.IverksettingTaskDTO +import no.nav.familie.ba.sak.task.dto.StatusFraOppdragDTO +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.prosessering.domene.Task +import java.time.LocalDate +import java.util.Properties + +fun kjørStegprosessForBehandling( + tilSteg: StegType = StegType.BEHANDLING_AVSLUTTET, + søkerFnr: String, + barnasIdenter: List, + vedtakService: VedtakService, + underkategori: BehandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + behandlingÅrsak: BehandlingÅrsak = BehandlingÅrsak.SØKNAD, + overstyrendeVilkårsvurdering: Vilkårsvurdering, + behandlingstype: BehandlingType, + + vilkårsvurderingService: VilkårsvurderingService, + stegService: StegService, + vedtaksperiodeService: VedtaksperiodeService, + endretUtbetalingAndelHentOgPersisterService: EndretUtbetalingAndelHentOgPersisterService, + fagsakService: FagsakService, + persongrunnlagService: PersongrunnlagService, + andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + brevmalService: BrevmalService, +): Behandling { + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + + val nyBehandling = NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = underkategori, + søkersIdent = søkerFnr, + behandlingType = behandlingstype, + behandlingÅrsak = behandlingÅrsak, + barnasIdenter = barnasIdenter, + søknadMottattDato = LocalDate.now(), + fagsakId = fagsak.id, + ) + + val behandling = stegService.håndterNyBehandling(nyBehandling) + + val behandlingEtterPersongrunnlagSteg = + if (behandlingÅrsak == BehandlingÅrsak.SØKNAD || behandlingÅrsak == BehandlingÅrsak.FØDSELSHENDELSE) { + håndterSøknadSteg(stegService, behandling, søkerFnr, barnasIdenter, underkategori) + } else { + behandling + } + + if (tilSteg == StegType.REGISTRERE_PERSONGRUNNLAG || tilSteg == StegType.REGISTRERE_SØKNAD) { + return behandlingEtterPersongrunnlagSteg + } + + val behandlingEtterVilkårsvurderingSteg = + håndterVilkårsvurderingSteg( + vilkårsvurderingService = vilkårsvurderingService, + behandling = behandlingEtterPersongrunnlagSteg, + nyVilkårsvurdering = overstyrendeVilkårsvurdering, + stegService = stegService, + ) + if (tilSteg == StegType.VILKÅRSVURDERING) return behandlingEtterVilkårsvurderingSteg + + val behandlingEtterBehandlingsresultat = stegService.håndterBehandlingsresultat(behandlingEtterVilkårsvurderingSteg) + if (tilSteg == StegType.BEHANDLINGSRESULTAT) return behandlingEtterBehandlingsresultat + + val behandlingEtterSimuleringSteg = hånderSilmuleringssteg(stegService, behandlingEtterBehandlingsresultat) + if (tilSteg == StegType.VURDER_TILBAKEKREVING) return behandlingEtterSimuleringSteg + + val behandlingEtterSendTilBeslutter = + håndterSendtTilBeslutterSteg( + behandlingEtterSimuleringSteg = behandlingEtterSimuleringSteg, + vedtakService = vedtakService, + vedtaksperiodeService = vedtaksperiodeService, + stegService = stegService, + persongrunnlagService = persongrunnlagService, + andelerTilkjentYtelseOgEndreteUtbetalingerService = andelerTilkjentYtelseOgEndreteUtbetalingerService, + endretUtbetalingAndelHentOgPersisterService = endretUtbetalingAndelHentOgPersisterService, + sanityBegrunnelser = testSanityKlient.hentBegrunnelserMap(), + vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandling(behandlingEtterSimuleringSteg.id)!!, + ) + if (tilSteg == StegType.SEND_TIL_BESLUTTER) return behandlingEtterSendTilBeslutter + + val behandlingEtterBeslutteVedtak = + håndterBeslutteVedtakSteg(stegService, behandlingEtterSendTilBeslutter) + if (tilSteg == StegType.BESLUTTE_VEDTAK) return behandlingEtterBeslutteVedtak + + val behandlingEtterIverksetteVedtak = + håndterIverksetteVedtakSteg(stegService, behandlingEtterBeslutteVedtak, vedtakService) + if (tilSteg == StegType.IVERKSETT_MOT_OPPDRAG) return behandlingEtterIverksetteVedtak + + val behandlingEtterStatusFraOppdrag = + håndterStatusFraOppdragSteg(stegService, behandlingEtterIverksetteVedtak, søkerFnr, vedtakService) + if (tilSteg == StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI) return behandlingEtterStatusFraOppdrag + + val behandlingEtterIverksetteMotTilbake = + stegService.håndterIverksettMotFamilieTilbake(behandlingEtterStatusFraOppdrag, Properties()) + if (tilSteg == StegType.IVERKSETT_MOT_FAMILIE_TILBAKE) return behandlingEtterIverksetteMotTilbake + + val behandlingEtterJournalførtVedtak = + håndterJournalførtVedtakSteg(stegService, behandlingEtterIverksetteMotTilbake, vedtakService) + if (tilSteg == StegType.JOURNALFØR_VEDTAKSBREV) return behandlingEtterJournalførtVedtak + + val behandlingEtterDistribuertVedtak = + håndterDistribuertVedtakSteg(stegService, behandlingEtterJournalførtVedtak, søkerFnr, brevmalService) + if (tilSteg == StegType.DISTRIBUER_VEDTAKSBREV) return behandlingEtterDistribuertVedtak + + return stegService.håndterFerdigstillBehandling(behandlingEtterDistribuertVedtak) +} + +private fun håndterSøknadSteg( + stegService: StegService, + behandling: Behandling, + søkerFnr: String, + barnasIdenter: List, + behandlingUnderkategori: BehandlingUnderkategori, +) = stegService.håndterSøknad( + behandling = behandling, + restRegistrerSøknad = RestRegistrerSøknad( + søknad = lagSøknadDTO( + søkerIdent = søkerFnr, + barnasIdenter = barnasIdenter, + underkategori = behandlingUnderkategori, + ), + bekreftEndringerViaFrontend = true, + ), +) + +private fun håndterSendtTilBeslutterSteg( + behandlingEtterSimuleringSteg: Behandling, + vedtakService: VedtakService, + vedtaksperiodeService: VedtaksperiodeService, + stegService: StegService, + persongrunnlagService: PersongrunnlagService, + andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + endretUtbetalingAndelHentOgPersisterService: EndretUtbetalingAndelHentOgPersisterService, + sanityBegrunnelser: Map, + vilkårsvurdering: Vilkårsvurdering, +): Behandling { + val andelerTilkjentYtelse = andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnAndelerTilkjentYtelseMedEndreteUtbetalinger(behandlingId = behandlingEtterSimuleringSteg.id) + + val persongrunnlag = + persongrunnlagService.hentAktivThrows(behandlingEtterSimuleringSteg.id) + + val endredeUtbetalingAndeler = endretUtbetalingAndelHentOgPersisterService.hentForBehandling( + behandlingEtterSimuleringSteg.id, + ) + leggTilAlleGyldigeBegrunnelserPåVedtaksperiodeIBehandling( + behandling = behandlingEtterSimuleringSteg, + vedtakService = vedtakService, + vedtaksperiodeService = vedtaksperiodeService, + personopplysningGrunnlag = persongrunnlag, + andelerTilkjentYtelse = andelerTilkjentYtelse, + endredeUtbetalingAndeler = endredeUtbetalingAndeler, + sanityBegrunnelser = sanityBegrunnelser, + vilkårsvurdering = vilkårsvurdering, + ) + val behandlingEtterSendTilBeslutter = stegService.håndterSendTilBeslutter(behandlingEtterSimuleringSteg, "1234") + return behandlingEtterSendTilBeslutter +} + +private fun håndterDistribuertVedtakSteg( + stegService: StegService, + behandling: Behandling, + søkerFnr: String, + brevmalService: BrevmalService, +): Behandling { + val behandlingEtterDistribuertVedtak = + stegService.håndterDistribuerVedtaksbrev( + behandling, + DistribuerDokumentDTO( + behandlingId = behandling.id, + journalpostId = "1234", + personEllerInstitusjonIdent = søkerFnr, + brevmal = brevmalService.hentBrevmal(behandling), + erManueltSendt = false, + ), + ) + return behandlingEtterDistribuertVedtak +} + +private fun håndterJournalførtVedtakSteg( + stegService: StegService, + behandlingEtterIverksetteMotTilbake: Behandling, + vedtakService: VedtakService, +): Behandling { + val vedtak = vedtakService.hentAktivForBehandling(behandlingEtterIverksetteMotTilbake.id) + return stegService.håndterJournalførVedtaksbrev( + behandlingEtterIverksetteMotTilbake, + JournalførVedtaksbrevDTO( + vedtakId = vedtak!!.id, + task = Task(type = JournalførVedtaksbrevTask.TASK_STEP_TYPE, payload = ""), + ), + ) +} + +private fun håndterStatusFraOppdragSteg( + stegService: StegService, + behandlingEtterIverksetteVedtak: Behandling, + søkerFnr: String, + vedtakService: VedtakService, +): Behandling { + val vedtak = vedtakService.hentAktivForBehandling(behandlingEtterIverksetteVedtak.id) + return stegService.håndterStatusFraØkonomi( + behandlingEtterIverksetteVedtak, + StatusFraOppdragMedTask( + statusFraOppdragDTO = StatusFraOppdragDTO( + fagsystem = FAGSYSTEM, + personIdent = søkerFnr, + aktørId = behandlingEtterIverksetteVedtak.fagsak.aktør.aktørId, + behandlingsId = behandlingEtterIverksetteVedtak.id, + vedtaksId = vedtak!!.id, + ), + task = Task(type = StatusFraOppdragTask.TASK_STEP_TYPE, payload = ""), + ), + ) +} + +private fun håndterIverksetteVedtakSteg( + stegService: StegService, + behandlingEtterBeslutteVedtak: Behandling, + vedtakService: VedtakService, +): Behandling { + val vedtak = vedtakService.hentAktivForBehandling(behandlingEtterBeslutteVedtak.id) + return stegService.håndterIverksettMotØkonomi( + behandlingEtterBeslutteVedtak, + IverksettingTaskDTO( + behandlingsId = behandlingEtterBeslutteVedtak.id, + vedtaksId = vedtak!!.id, + saksbehandlerId = "System", + personIdent = behandlingEtterBeslutteVedtak.fagsak.aktør.aktivFødselsnummer(), + ), + ) +} + +private fun håndterBeslutteVedtakSteg( + stegService: StegService, + behandlingEtterSendTilBeslutter: Behandling, +): Behandling { + val behandlingEtterBeslutteVedtak = + stegService.håndterBeslutningForVedtak( + behandlingEtterSendTilBeslutter, + RestBeslutningPåVedtak(beslutning = Beslutning.GODKJENT), + ) + return behandlingEtterBeslutteVedtak +} + +private fun hånderSilmuleringssteg( + stegService: StegService, + behandlingEtterBehandlingsresultat: Behandling, +): Behandling { + return stegService.håndterVurderTilbakekreving( + behandlingEtterBehandlingsresultat, + if (behandlingEtterBehandlingsresultat.resultat != Behandlingsresultat.FORTSATT_INNVILGET) { + RestTilbakekreving( + valg = Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, + begrunnelse = "Begrunnelse", + ) + } else { + null + }, + ) +} + +private fun håndterVilkårsvurderingSteg( + vilkårsvurderingService: VilkårsvurderingService, + behandling: Behandling, + nyVilkårsvurdering: Vilkårsvurdering, + stegService: StegService, +): Behandling { + val vilkårsvurderingForBehandling = vilkårsvurderingService.hentAktivForBehandling(behandlingId = behandling.id)!! + vilkårsvurderingForBehandling.oppdaterMedDataFra(nyVilkårsvurdering) + + vilkårsvurderingService.oppdater(vilkårsvurderingForBehandling) + + return stegService.håndterVilkårsvurdering(behandling) +} + +fun Vilkårsvurdering.oppdaterMedDataFra(vilkårsvurdering: Vilkårsvurdering) { + this.personResultater.forEach { personResultatSomSkalOppdateres -> + val nyttPersonresultat = + vilkårsvurdering.personResultater.find { it.aktør.aktørId == personResultatSomSkalOppdateres.aktør.aktørId }!! + + personResultatSomSkalOppdateres.vilkårResultater.forEach { vilkårResultatSomSkalOppdateres -> + val nyttVilkårResultat = nyttPersonresultat.vilkårResultater + .find { it.vilkårType == vilkårResultatSomSkalOppdateres.vilkårType } + ?: error( + "Fant ikke ${vilkårResultatSomSkalOppdateres.vilkårType} i vilkårene som ble sendt med for " + + "${personResultatSomSkalOppdateres.aktør.aktivFødselsnummer()}.", + ) + + vilkårResultatSomSkalOppdateres.resultat = nyttVilkårResultat.resultat + vilkårResultatSomSkalOppdateres.periodeFom = nyttVilkårResultat.periodeFom + vilkårResultatSomSkalOppdateres.periodeTom = nyttVilkårResultat.periodeTom + } + } +} + +fun leggTilAlleGyldigeBegrunnelserPåVedtaksperiodeIBehandling( + behandling: Behandling, + vedtakService: VedtakService, + vedtaksperiodeService: VedtaksperiodeService, + personopplysningGrunnlag: PersonopplysningGrunnlag, + andelerTilkjentYtelse: List, + endredeUtbetalingAndeler: List, + sanityBegrunnelser: Map, + vilkårsvurdering: Vilkårsvurdering, +) { + val aktivtVedtak = vedtakService.hentAktivForBehandling(behandling.id)!! + + val perisisterteVedtaksperioder = + vedtaksperiodeService.hentPersisterteVedtaksperioder(aktivtVedtak) + + val vedtaksperiode = perisisterteVedtaksperioder.first() + + val utvidetVedtaksperiodeMedBegrunnelser = vedtaksperiode.tilUtvidetVedtaksperiodeMedBegrunnelser( + personopplysningGrunnlag = personopplysningGrunnlag, + andelerTilkjentYtelse = andelerTilkjentYtelse, + ) + + val aktørerMedUtbetaling = + utvidetVedtaksperiodeMedBegrunnelser + .utbetalingsperiodeDetaljer + .map { personMedUtbetaling -> + personopplysningGrunnlag.søkerOgBarn.find { + it.aktør.aktivFødselsnummer() == personMedUtbetaling.person.personIdent + }!!.aktør + } + + val gyldigebegrunnelser = hentGyldigeBegrunnelserForVedtaksperiodeMinimert( + minimertVedtaksperiode = utvidetVedtaksperiodeMedBegrunnelser.tilMinimertVedtaksperiode(), + sanityBegrunnelser = sanityBegrunnelser, + minimertePersoner = personopplysningGrunnlag.tilMinimertePersoner(), + minimertePersonresultater = vilkårsvurdering.personResultater + .map { it.tilMinimertPersonResultat() }, + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + minimerteEndredeUtbetalingAndeler = endredeUtbetalingAndeler + .map { it.tilMinimertEndretUtbetalingAndel() }, + erFørsteVedtaksperiodePåFagsak = erFørsteVedtaksperiodePåFagsak( + andelerTilkjentYtelse, + utvidetVedtaksperiodeMedBegrunnelser.fom, + ), + ytelserForSøkerForrigeMåned = hentYtelserForSøkerForrigeMåned( + andelerTilkjentYtelse, + utvidetVedtaksperiodeMedBegrunnelser, + ), + ytelserForrigePerioder = andelerTilkjentYtelse.filter { + ytelseErFraForrigePeriode( + it, + utvidetVedtaksperiodeMedBegrunnelser, + ) + }, + ) + + vedtaksperiodeService.oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = vedtaksperiode.id, + standardbegrunnelserFraFrontend = gyldigebegrunnelser.toList(), + eøsStandardbegrunnelserFraFrontend = emptyList(), + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/brev/BrevBegrunnelseGrunnlagMedPersoner.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/brev/BrevBegrunnelseGrunnlagMedPersoner.kt new file mode 100644 index 000000000..8ed0c1a6d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/brev/BrevBegrunnelseGrunnlagMedPersoner.kt @@ -0,0 +1,21 @@ +package no.nav.familie.ba.sak.datagenerator.brev + +import no.nav.familie.ba.sak.common.lagTriggesAv +import no.nav.familie.ba.sak.kjerne.brev.domene.BrevBegrunnelseGrunnlagMedPersoner +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.TriggesAv +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType + +fun lagBrevBegrunnelseGrunnlagMedPersoner( + standardbegrunnelse: Standardbegrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + vedtakBegrunnelseType: VedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET, + triggesAv: TriggesAv = lagTriggesAv(), + personIdenter: List = emptyList(), +): BrevBegrunnelseGrunnlagMedPersoner { + return BrevBegrunnelseGrunnlagMedPersoner( + standardbegrunnelse = standardbegrunnelse, + vedtakBegrunnelseType = vedtakBegrunnelseType, + triggesAv = triggesAv, + personIdenter = personIdenter, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/brev/MinimertPerson.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/brev/MinimertPerson.kt new file mode 100644 index 000000000..754dca64f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/brev/MinimertPerson.kt @@ -0,0 +1,21 @@ +package no.nav.familie.ba.sak.datagenerator.brev + +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.MinimertPerson +import java.time.LocalDate + +fun lagMinimertPerson( + type: PersonType = PersonType.BARN, + fødselsdato: LocalDate = LocalDate.now().minusYears(if (type == PersonType.BARN) 2 else 30), + aktivPersonIdent: String = randomFnr(), + aktørId: String = randomAktør(aktivPersonIdent).aktørId, + dødsfallsdato: LocalDate? = null, +) = MinimertPerson( + type = type, + fødselsdato = fødselsdato, + aktivPersonIdent = aktivPersonIdent, + aktørId = aktørId, + dødsfallsdato = dødsfallsdato, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/brev/MinimertUtbetalingsperiodeDetalj.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/brev/MinimertUtbetalingsperiodeDetalj.kt new file mode 100644 index 000000000..ccd8fbca8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/brev/MinimertUtbetalingsperiodeDetalj.kt @@ -0,0 +1,20 @@ +package no.nav.familie.ba.sak.datagenerator.brev + +import no.nav.familie.ba.sak.common.tilfeldigSøker +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPerson +import no.nav.familie.ba.sak.integrasjoner.økonomi.sats +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertUtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.vedtak.domene.MinimertRestPerson +import no.nav.familie.ba.sak.kjerne.vedtak.domene.tilMinimertPerson +import java.math.BigDecimal + +fun lagMinimertUtbetalingsperiodeDetalj( + person: MinimertRestPerson = tilfeldigSøker().tilRestPerson().tilMinimertPerson(), + ytelseType: YtelseType = YtelseType.ORDINÆR_BARNETRYGD, + utbetaltPerMnd: Int = sats(YtelseType.ORDINÆR_BARNETRYGD), + prosent: BigDecimal = BigDecimal.valueOf(100), + erPåvirketAvEndring: Boolean = false, + endringsårsak: Årsak? = null, +) = MinimertUtbetalingsperiodeDetalj(person, ytelseType, utbetaltPerMnd, erPåvirketAvEndring, endringsårsak, prosent) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/endretUtbetaling/MinimertEndretUtbetaling.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/endretUtbetaling/MinimertEndretUtbetaling.kt new file mode 100644 index 000000000..0677ce5e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/endretUtbetaling/MinimertEndretUtbetaling.kt @@ -0,0 +1,22 @@ +package no.nav.familie.ba.sak.datagenerator.endretUtbetaling + +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertEndretAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import java.math.BigDecimal +import java.time.YearMonth + +fun lagMinimertEndretUtbetalingAndel( + aktørId: String = randomAktør(randomFnr()).aktørId, + fom: YearMonth? = YearMonth.now(), + tom: YearMonth? = YearMonth.now(), + årsak: Årsak? = Årsak.DELT_BOSTED, + prosent: BigDecimal? = BigDecimal.valueOf(100), +) = MinimertEndretAndel( + aktørId = aktørId, + fom = fom, + tom = tom, + årsak = årsak, + prosent = prosent, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/grunnlag/Adresser.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/grunnlag/Adresser.kt new file mode 100644 index 000000000..484646762 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/grunnlag/Adresser.kt @@ -0,0 +1,21 @@ +package no.nav.familie.ba.sak.datagenerator.grunnlag + +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrVegadresse + +fun opprettAdresse( + matrikkelId: Long? = null, + bruksenhetsnummer: String? = null, + adressenavn: String? = null, + husnummer: String? = null, + husbokstav: String? = null, + postnummer: String? = null, +) = GrVegadresse( + matrikkelId = matrikkelId, + husnummer = husnummer, + husbokstav = husbokstav, + bruksenhetsnummer = bruksenhetsnummer, + adressenavn = adressenavn, + kommunenummer = null, + tilleggsnavn = null, + postnummer = postnummer, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/settp\303\245vent/SettP\303\245Vent.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/settp\303\245vent/SettP\303\245Vent.kt" new file mode 100644 index 000000000..13b422e44 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/settp\303\245vent/SettP\303\245Vent.kt" @@ -0,0 +1,23 @@ +package no.nav.familie.ba.sak.datagenerator.settpåvent + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVent +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentÅrsak +import java.time.LocalDate + +fun lagSettPåVent( + behandling: Behandling = lagBehandling(), + frist: LocalDate = LocalDate.now(), + tidTattAvVent: LocalDate = LocalDate.now(), + tidSattPåVent: LocalDate = LocalDate.now(), + årsak: SettPåVentÅrsak = SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, + aktiv: Boolean = true, +) = SettPåVent( + behandling = behandling, + frist = frist, + tidTattAvVent = tidTattAvVent, + tidSattPåVent = tidSattPåVent, + årsak = årsak, + aktiv = aktiv, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/vedtak/Vedtaksbegrunnelse.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/vedtak/Vedtaksbegrunnelse.kt new file mode 100644 index 000000000..36828c32e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/vedtak/Vedtaksbegrunnelse.kt @@ -0,0 +1,15 @@ +package no.nav.familie.ba.sak.datagenerator.vedtak + +import io.mockk.mockk +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.Vedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser + +fun lagVedtaksbegrunnelse( + standardbegrunnelse: Standardbegrunnelse = + Standardbegrunnelse.FORTSATT_INNVILGET_SØKER_OG_BARN_BOSATT_I_RIKET, + vedtaksperiodeMedBegrunnelser: VedtaksperiodeMedBegrunnelser = mockk(), +) = Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, + standardbegrunnelse = standardbegrunnelse, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/vilk\303\245rsvurdering/PersonResultat.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/vilk\303\245rsvurdering/PersonResultat.kt" new file mode 100644 index 000000000..2a6f75dba --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/datagenerator/vilk\303\245rsvurdering/PersonResultat.kt" @@ -0,0 +1,65 @@ +package no.nav.familie.ba.sak.datagenerator.vilkårsvurdering + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import java.time.LocalDate + +fun lagPersonResultatAvOverstyrteResultater( + person: Person, + overstyrendeVilkårResultater: List, + vilkårsvurdering: Vilkårsvurdering, + id: Long = 0, + +): PersonResultat { + val personResultat = PersonResultat( + id = id, + vilkårsvurdering = vilkårsvurdering, + aktør = person.aktør, + ) + + val erUtvidet = overstyrendeVilkårResultater.any { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + + val vilkårResultater = Vilkår.hentVilkårFor( + personType = person.type, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = if (erUtvidet) BehandlingUnderkategori.UTVIDET else BehandlingUnderkategori.ORDINÆR, + ).foldIndexed(mutableListOf()) { index, acc, vilkårType -> + val overstyrteVilkårResultaterForVilkår: List = overstyrendeVilkårResultater + .filter { it.vilkårType == vilkårType } + if (overstyrteVilkårResultaterForVilkår.isNotEmpty()) { + acc.addAll(overstyrteVilkårResultaterForVilkår) + } else { + acc.add( + VilkårResultat( + id = if (id != 0L) index + 1L else 0L, + personResultat = personResultat, + periodeFom = if (vilkårType == Vilkår.UNDER_18_ÅR) { + person.fødselsdato + } else { + maxOf( + person.fødselsdato, + LocalDate.now().minusYears(3), + ) + }, + periodeTom = if (vilkårType == Vilkår.UNDER_18_ÅR) person.fødselsdato.plusYears(18) else null, + vilkårType = vilkårType, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ), + ) + } + acc + }.toSet() + + personResultat.setSortedVilkårResultater(vilkårResultater) + + return personResultat +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/RestMappingTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/RestMappingTest.kt new file mode 100644 index 000000000..590b3cc2a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/RestMappingTest.kt @@ -0,0 +1,89 @@ +package no.nav.familie.ba.sak.ekstern + +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegisteropplysning +import no.nav.familie.ba.sak.ekstern.restDomene.fyllInnTomDatoer +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrBostedsadresse.Companion.fregManglendeFlytteDato +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrVegadresse +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class RestMappingTest { + + @Test + fun `Manglende angitt flyttedato fra freg mappes som manglende dato`() { + val adresseUtenFlyttedato = GrVegadresse( + matrikkelId = 1234, + husnummer = "11", + husbokstav = "B", + bruksenhetsnummer = "H022", + adressenavn = "Adressenavn", + kommunenummer = "1232", + tilleggsnavn = "noe", + postnummer = "4322", + ) + .apply { periode = DatoIntervallEntitet(fom = fregManglendeFlytteDato) } + + val flyttedato = LocalDate.of(2000, 1, 1) + val adresseMedFlyttedato = GrVegadresse( + matrikkelId = 1234, + husnummer = "11", + husbokstav = "B", + bruksenhetsnummer = "H022", + adressenavn = "Adressenavn", + kommunenummer = "1232", + tilleggsnavn = "noe", + postnummer = "4322", + ) + .apply { periode = DatoIntervallEntitet(fom = flyttedato) } + + assertEquals(null, adresseUtenFlyttedato.tilRestRegisteropplysning().fom) + assertEquals(flyttedato, adresseMedFlyttedato.tilRestRegisteropplysning().fom) + } + + @Test + fun `Fyller ut og sorterer i rett rekkefølge`() { + val fomA = LocalDate.of(2001, 1, 1) + val fomB = LocalDate.of(2005, 1, 1) + val tomB = LocalDate.of(2006, 1, 1) + val fomC = LocalDate.of(2010, 1, 1) + + val manglerDatoer = RestRegisteropplysning(fom = null, tom = null, verdi = "") + val tidligereUtenTom = RestRegisteropplysning(fom = fomA, tom = null, verdi = "") + val tidligereMedTom = RestRegisteropplysning(fom = fomB, tom = tomB, verdi = "") + val nåværende = RestRegisteropplysning(fom = fomC, tom = null, verdi = "") + + val utfylteOpplysninger = + listOf(manglerDatoer, tidligereUtenTom, tidligereMedTom, nåværende).shuffled().fyllInnTomDatoer() + + assertEquals(null, utfylteOpplysninger[0].tom) + assertEquals(fomB.minusDays(1), utfylteOpplysninger[1].tom) + assertEquals(tomB, utfylteOpplysninger[2].tom) + assertEquals(null, utfylteOpplysninger[3].tom) + } + + @Test + fun `Fyller ut tom-dato når denne mangler og det er påfølgende periode`() { + val tidligereUtenTom = RestRegisteropplysning(fom = LocalDate.of(2001, 1, 1), tom = null, verdi = "") + val nåværendeFom = LocalDate.of(2005, 1, 1) + val nåværende = RestRegisteropplysning(fom = nåværendeFom, tom = null, verdi = "") + val utfylteOpplysninger = + listOf(tidligereUtenTom, nåværende).fyllInnTomDatoer() + assertEquals(nåværendeFom.minusDays(1), utfylteOpplysninger[0].tom) + } + + @Test + fun `Fyller ikke ut tom-dato når det ikke finnes påfølgende perioder`() = + assertEquals(null, listOf(RestRegisteropplysning(fom = null, tom = null, verdi = ""))[0].tom) + + @Test + fun `Fyller ikke ut tom-dato når fom-dato er kjent`() = + assertEquals(null, listOf(RestRegisteropplysning(fom = null, tom = null, verdi = ""))[0].tom) + + @Test + fun `Fyller ikke ut tom-dato når denne er kjent, ved utvandring`() { + val tomDato = LocalDate.of(2006, 1, 1) + assertEquals(tomDato, listOf(RestRegisteropplysning(fom = LocalDate.of(2005, 1, 1), tom = tomDato, verdi = ""))[0].tom) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/bisys/BisysServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/bisys/BisysServiceTest.kt new file mode 100644 index 000000000..99d5d22d8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/bisys/BisysServiceTest.kt @@ -0,0 +1,339 @@ +package no.nav.familie.ba.sak.ekstern.bisys + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelseUtvidet +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdBarnetrygdClient +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class BisysServiceTest { + + private lateinit var bisysService: BisysService + private val mockPersonidentService = mockk() + private val mockFagsakRepository = mockk() + private val mockBehandlingHentOgPersisterService = mockk() + private val mockTilkjentYtelseRepository = mockk() + private val mockInfotrygdClient = mockk() + + @BeforeAll + fun setUp() { + bisysService = BisysService( + mockBehandlingHentOgPersisterService, + mockInfotrygdClient, + mockFagsakRepository, + mockPersonidentService, + mockTilkjentYtelseRepository, + ) + } + + @Test + fun `Skal returnere tom liste siden person ikke har finens i infotrygd og barnetrygd`() { + val fnr = randomFnr() + val aktør = tilAktør(fnr) + + every { mockPersonidentService.hentAktør(any()) } answers { aktør } + every { mockPersonidentService.hentAlleFødselsnummerForEnAktør(any()) } answers { listOf(aktør.aktivFødselsnummer()) } + + every { mockInfotrygdClient.hentUtvidetBarnetrygd(fnr, any()) } returns BisysUtvidetBarnetrygdResponse( + perioder = emptyList(), + ) + + every { mockFagsakRepository.finnFagsakForAktør(aktør) } returns null + + val response = bisysService.hentUtvidetBarnetrygd(fnr, LocalDate.of(2021, 1, 1)) + + assertThat(response.perioder).hasSize(0) + } + + @Test + fun `Skal returnere periode kun fra infotrygd`() { + val fnr = randomFnr() + val aktør = tilAktør(fnr) + + every { mockPersonidentService.hentAktør(any()) } answers { aktør } + every { mockPersonidentService.hentAlleFødselsnummerForEnAktør(any()) } answers { listOf(aktør.aktivFødselsnummer()) } + + val periodeInfotrygd = UtvidetBarnetrygdPeriode( + BisysStønadstype.UTVIDET, + YearMonth.of(2019, 1), + YearMonth.now(), + 500.0, + manueltBeregnet = true, + ) + every { mockInfotrygdClient.hentUtvidetBarnetrygd(fnr, any()) } returns BisysUtvidetBarnetrygdResponse( + perioder = listOf(periodeInfotrygd), + ) + + every { mockFagsakRepository.finnFagsakForAktør(aktør) } returns null + + val response = bisysService.hentUtvidetBarnetrygd(fnr, LocalDate.of(2021, 1, 1)) + + assertThat(response.perioder).hasSize(1).contains(periodeInfotrygd) + } + + @Test + fun `Skal returnere utvidet barnetrygdperiode fra basak`() { + val behandling = lagBehandling() + + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling = behandling).copy(utbetalingsoppdrag = "utbetalt") + + val andelTilkjentYtelse = + lagAndelTilkjentYtelseUtvidet( + fom = "2020-01", + tom = "2040-01", + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + behandling = behandling, + tilkjentYtelse = tilkjentYtelse, + beløp = 660, + ) + tilkjentYtelse.andelerTilkjentYtelse.add(andelTilkjentYtelse) + + every { mockInfotrygdClient.hentUtvidetBarnetrygd(any(), any()) } returns BisysUtvidetBarnetrygdResponse( + perioder = emptyList(), + ) + + every { mockFagsakRepository.finnFagsakForAktør(any()) } returns behandling.fagsak + every { mockBehandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(behandling.fagsak.id) } returns behandling + every { mockTilkjentYtelseRepository.findByBehandlingAndHasUtbetalingsoppdrag(behandling.id) } returns andelTilkjentYtelse.tilkjentYtelse + every { mockPersonidentService.hentAktør(any()) } answers { behandling.fagsak.aktør } + every { mockPersonidentService.hentAlleFødselsnummerForEnAktør(any()) } answers { listOf(behandling.fagsak.aktør.aktivFødselsnummer()) } + + val response = + bisysService.hentUtvidetBarnetrygd(andelTilkjentYtelse.aktør.aktivFødselsnummer(), LocalDate.of(2021, 1, 1)) + + assertThat(response.perioder).hasSize(1) + assertThat(response.perioder.first().beløp).isEqualTo(660.0) + assertThat(response.perioder.first().fomMåned).isEqualTo(YearMonth.of(2020, 1)) + assertThat(response.perioder.first().tomMåned).isEqualTo(YearMonth.of(2040, 1)) + assertThat(response.perioder.first().manueltBeregnet).isFalse + } + + @Test + fun `Skal slå sammen resultat fra ba-sak og infotrygd`() { + val behandling = lagBehandling() + + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling = behandling).copy(utbetalingsoppdrag = "utbetalt") + + val kalkulertbeløp = 660 + val andelTilkjentYtelse = + lagAndelTilkjentYtelseUtvidet( + fom = "2020-01", + tom = "2040-01", + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + behandling = behandling, + tilkjentYtelse = tilkjentYtelse, + beløp = kalkulertbeløp, + ).copy(prosent = BigDecimal.valueOf(50), sats = 2 * kalkulertbeløp) + + tilkjentYtelse.andelerTilkjentYtelse.add(andelTilkjentYtelse) + every { mockPersonidentService.hentAktør(any()) } answers { andelTilkjentYtelse.aktør } + every { mockPersonidentService.hentAlleFødselsnummerForEnAktør(any()) } answers { listOf(andelTilkjentYtelse.aktør.aktivFødselsnummer()) } + + val periodeInfotrygd = UtvidetBarnetrygdPeriode( + BisysStønadstype.UTVIDET, + YearMonth.of(2019, 1), + YearMonth.of(2019, 12), + 660.0, + manueltBeregnet = false, + deltBosted = true, + ) + every { + mockInfotrygdClient.hentUtvidetBarnetrygd( + andelTilkjentYtelse.aktør.aktivFødselsnummer(), + any(), + ) + } returns BisysUtvidetBarnetrygdResponse( + perioder = listOf(periodeInfotrygd), + ) + + every { mockFagsakRepository.finnFagsakForAktør(any()) } returns behandling.fagsak + + every { mockBehandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(behandling.fagsak.id) } returns behandling + every { mockTilkjentYtelseRepository.findByBehandlingAndHasUtbetalingsoppdrag(behandling.id) } returns andelTilkjentYtelse.tilkjentYtelse + + val response = + bisysService.hentUtvidetBarnetrygd(andelTilkjentYtelse.aktør.aktivFødselsnummer(), LocalDate.of(2019, 1, 1)) + + assertThat(response.perioder).hasSize(1) + assertThat(response.perioder.first().beløp).isEqualTo(660.0) + assertThat(response.perioder.first().fomMåned).isEqualTo(YearMonth.of(2019, 1)) + assertThat(response.perioder.first().tomMåned).isEqualTo(YearMonth.of(2040, 1)) + assertThat(response.perioder.first().manueltBeregnet).isFalse + } + + @Test + fun `Skal slå sammen resultat fra ba-sak og infotrygd når periodene overlapper`() { + val behandling = lagBehandling() + + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling = behandling).copy(utbetalingsoppdrag = "utbetalt") + + val andelTilkjentYtelse = + lagAndelTilkjentYtelseUtvidet( + fom = "2021-08", + tom = "2029-02", + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + behandling = behandling, + tilkjentYtelse = tilkjentYtelse, + beløp = 1054, + ) + + tilkjentYtelse.andelerTilkjentYtelse.add(andelTilkjentYtelse) + every { mockPersonidentService.hentAktør(any()) } answers { andelTilkjentYtelse.aktør } + every { mockPersonidentService.hentAlleFødselsnummerForEnAktør(any()) } answers { listOf(andelTilkjentYtelse.aktør.aktivFødselsnummer()) } + + val periodeInfotrygd = UtvidetBarnetrygdPeriode( + BisysStønadstype.UTVIDET, + YearMonth.of(2020, 9), + YearMonth.of(2021, 12), + 1054.0, + manueltBeregnet = false, + deltBosted = false, + ) + every { + mockInfotrygdClient.hentUtvidetBarnetrygd( + andelTilkjentYtelse.aktør.aktivFødselsnummer(), + any(), + ) + } returns BisysUtvidetBarnetrygdResponse( + perioder = listOf(periodeInfotrygd), + ) + + every { mockFagsakRepository.finnFagsakForAktør(any()) } returns behandling.fagsak + + every { mockBehandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(behandling.fagsak.id) } returns behandling + every { mockTilkjentYtelseRepository.findByBehandlingAndHasUtbetalingsoppdrag(behandling.id) } returns andelTilkjentYtelse.tilkjentYtelse + + val response = + bisysService.hentUtvidetBarnetrygd(andelTilkjentYtelse.aktør.aktivFødselsnummer(), LocalDate.of(2021, 1, 1)) + + assertThat(response.perioder).hasSize(1) + assertThat(response.perioder.first().beløp).isEqualTo(1054.0) + assertThat(response.perioder.first().fomMåned).isEqualTo(YearMonth.of(2020, 9)) + assertThat(response.perioder.first().tomMåned).isEqualTo(YearMonth.of(2029, 2)) + assertThat(response.perioder.first().manueltBeregnet).isFalse + } + + @Test + fun `Skal slå sammen resultat fra ba-sak og infotrygd, typisk rett etter en migrering, hvor tomMåned i infotrygd er null`() { + val behandling = lagBehandling() + + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling = behandling).copy(utbetalingsoppdrag = "utbetalt") + + val andelTilkjentYtelse = + lagAndelTilkjentYtelseUtvidet( + fom = "2022-01", + tom = "2022-12", + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + behandling = behandling, + tilkjentYtelse = tilkjentYtelse, + beløp = 1054, + ) + + tilkjentYtelse.andelerTilkjentYtelse.add(andelTilkjentYtelse) + every { mockPersonidentService.hentAktør(any()) } answers { andelTilkjentYtelse.aktør } + every { mockPersonidentService.hentAlleFødselsnummerForEnAktør(any()) } answers { listOf(andelTilkjentYtelse.aktør.aktivFødselsnummer()) } + + val periodeInfotrygd = UtvidetBarnetrygdPeriode( + BisysStønadstype.UTVIDET, + YearMonth.of(2019, 3), + null, + 1054.0, + manueltBeregnet = false, + deltBosted = false, + ) + every { + mockInfotrygdClient.hentUtvidetBarnetrygd( + andelTilkjentYtelse.aktør.aktivFødselsnummer(), + any(), + ) + } returns BisysUtvidetBarnetrygdResponse( + perioder = listOf(periodeInfotrygd), + ) + + every { mockFagsakRepository.finnFagsakForAktør(any()) } returns behandling.fagsak + + every { mockBehandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(behandling.fagsak.id) } returns behandling + every { mockTilkjentYtelseRepository.findByBehandlingAndHasUtbetalingsoppdrag(behandling.id) } returns andelTilkjentYtelse.tilkjentYtelse + + val response = + bisysService.hentUtvidetBarnetrygd(andelTilkjentYtelse.aktør.aktivFødselsnummer(), LocalDate.of(2021, 1, 1)) + + assertThat(response.perioder).hasSize(1) + assertThat(response.perioder.first().beløp).isEqualTo(1054.0) + assertThat(response.perioder.first().fomMåned).isEqualTo(YearMonth.of(2019, 3)) + assertThat(response.perioder.first().tomMåned).isEqualTo(YearMonth.of(2022, 12)) + assertThat(response.perioder.first().manueltBeregnet).isFalse + } + + @Test + fun `Skal ikke slå sammen resultat fra ba-sak og infotrygd hvis periode er manuelt beregnet i infotrygd`() { + val behandling = lagBehandling() + + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling = behandling).copy(utbetalingsoppdrag = "utbetalt") + + val andelTilkjentYtelse = + lagAndelTilkjentYtelseUtvidet( + fom = "2020-01", + tom = "2040-01", + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + behandling = behandling, + tilkjentYtelse = tilkjentYtelse, + beløp = 660, + ) + tilkjentYtelse.andelerTilkjentYtelse.add(andelTilkjentYtelse) + + every { mockPersonidentService.hentAktør(any()) } answers { andelTilkjentYtelse.aktør } + every { mockPersonidentService.hentAlleFødselsnummerForEnAktør(any()) } answers { listOf(andelTilkjentYtelse.aktør.aktivFødselsnummer()) } + + val periodeInfotrygd = UtvidetBarnetrygdPeriode( + BisysStønadstype.UTVIDET, + YearMonth.of(2019, 1), + YearMonth.of(2019, 12), + 660.0, + manueltBeregnet = true, + deltBosted = false, + ) + every { + mockInfotrygdClient.hentUtvidetBarnetrygd( + andelTilkjentYtelse.aktør.aktivFødselsnummer(), + any(), + ) + } returns BisysUtvidetBarnetrygdResponse( + perioder = listOf(periodeInfotrygd), + ) + + every { mockFagsakRepository.finnFagsakForAktør(any()) } returns behandling.fagsak + every { mockBehandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(behandling.fagsak.id) } returns behandling + every { mockTilkjentYtelseRepository.findByBehandlingAndHasUtbetalingsoppdrag(behandling.id) } returns andelTilkjentYtelse.tilkjentYtelse + + val response = + bisysService.hentUtvidetBarnetrygd(andelTilkjentYtelse.aktør.aktivFødselsnummer(), LocalDate.of(2019, 1, 1)) + + assertThat(response.perioder).hasSize(2) + assertThat(response.perioder.first().beløp).isEqualTo(660.0) + assertThat(response.perioder.first().fomMåned).isEqualTo(YearMonth.of(2019, 1)) + assertThat(response.perioder.first().tomMåned).isEqualTo(YearMonth.of(2019, 12)) + assertThat(response.perioder.first().manueltBeregnet).isTrue + + assertThat(response.perioder.last().beløp).isEqualTo(660.0) + assertThat(response.perioder.last().fomMåned).isEqualTo(YearMonth.of(2020, 1)) + assertThat(response.perioder.last().tomMåned).isEqualTo(YearMonth.of(2040, 1)) + assertThat(response.perioder.last().manueltBeregnet).isFalse + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/bisys/SendMeldingTilBisysTaskTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/bisys/SendMeldingTilBisysTaskTest.kt new file mode 100644 index 000000000..3128d234a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/bisys/SendMeldingTilBisysTaskTest.kt @@ -0,0 +1,333 @@ +package no.nav.familie.ba.sak.ekstern.bisys + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.statistikk.producer.DefaultKafkaProducer +import no.nav.familie.ba.sak.statistikk.producer.DefaultKafkaProducer.Companion.OPPHOER_BARNETRYGD_BISYS_TOPIC +import no.nav.familie.ba.sak.task.SendMeldingTilBisysTask +import no.nav.familie.eksterne.kontrakter.bisys.BarnetrygdBisysMelding +import no.nav.familie.eksterne.kontrakter.bisys.BarnetrygdEndretType +import no.nav.familie.kontrakter.felles.objectMapper +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.springframework.kafka.support.SendResult +import java.math.BigDecimal +import java.time.YearMonth +import java.util.concurrent.CompletableFuture + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SendMeldingTilBisysTaskTest { + + data class Mocks( + val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + val kafkaProducer: DefaultKafkaProducer, + val tilkjentYtelseRepository: TilkjentYtelseRepository, + val kafkaResult: CompletableFuture>, + val behandling: List, + ) + + fun setupMocks(): Mocks { + val tilkjentYtelseRepositoryMock = mockk() + val kafkaProducer = DefaultKafkaProducer(mockk()) + val listenableFutureMock = mockk>>() + val behandlingHentOgPersisterServiceMock = mockk() + + val forrigeBehandling = lagBehandling(defaultFagsak(), førsteSteg = StegType.BEHANDLING_AVSLUTTET) + + val nyBehandling = lagBehandling( + forrigeBehandling.fagsak, + resultat = Behandlingsresultat.OPPHØRT, + førsteSteg = StegType.IVERKSETT_MOT_OPPDRAG, + ) + + every { behandlingHentOgPersisterServiceMock.hent(forrigeBehandling.id) } returns forrigeBehandling + every { behandlingHentOgPersisterServiceMock.hent(nyBehandling.id) } returns nyBehandling + + every { behandlingHentOgPersisterServiceMock.hentForrigeBehandlingSomErVedtatt(nyBehandling) } returns forrigeBehandling + + every { listenableFutureMock.thenAccept(any()) } returns CompletableFuture() + + kafkaProducer.kafkaAivenTemplate = mockk() + return Mocks( + behandlingHentOgPersisterServiceMock, + kafkaProducer, + tilkjentYtelseRepositoryMock, + listenableFutureMock, + listOf(forrigeBehandling, nyBehandling), + ) + } + + @Test + fun `Skal send riktig melding til Bisys hvis barnetrygd er opphørt`() { + val (behandlingRepository, kafkaProducer, tilkjentYtelseRepository, kafkaResult, behandling) = setupMocks() + val sendMeldingTilBisysTask = + SendMeldingTilBisysTask(kafkaProducer, tilkjentYtelseRepository, behandlingRepository) + + val barn1 = lagPerson(type = PersonType.BARN) + + every { tilkjentYtelseRepository.findByBehandling(behandling[0].id) } returns lagInitiellTilkjentYtelse().also { + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2037, 12), + prosent = BigDecimal(100), + person = barn1, + ), + ) + } + every { tilkjentYtelseRepository.findByBehandling(behandling[1].id) } returns lagInitiellTilkjentYtelse().also { + // Barn1 opphør fra 04/2022 + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2022, 3), + prosent = BigDecimal(100), + person = barn1, + ), + ) + } + + val meldingSlot = slot() + every { + kafkaProducer.kafkaAivenTemplate.send( + OPPHOER_BARNETRYGD_BISYS_TOPIC, + behandling[1].id.toString(), + capture(meldingSlot), + ) + } returns kafkaResult + + sendMeldingTilBisysTask.doTask(SendMeldingTilBisysTask.opprettTask(behandling[1].id)) + + verify(exactly = 1) { kafkaProducer.kafkaAivenTemplate.send(any(), any(), any()) } + val jsonMelding = objectMapper.readValue(meldingSlot.captured, BarnetrygdBisysMelding::class.java) + assertThat(jsonMelding.søker).isEqualTo(behandling[1].fagsak.aktør.aktivFødselsnummer()) + assertThat(jsonMelding.barn).hasSize(1) + assertThat(jsonMelding.barn[0].ident).isEqualTo(barn1.aktør.aktivFødselsnummer()) + assertThat(jsonMelding.barn[0].årsakskode.toString()).isEqualTo("RO") + assertThat(jsonMelding.barn[0].fom).isEqualTo(YearMonth.of(2022, 4)) + } + + @Test + fun `Skal send riktig melding til Bisys hvis barnetrygd er redusert`() { + val (behandlingRepository, kafkaProducer, tilkjentYtelseRepository, kafkaResult, behandling) = setupMocks() + val sendMeldingTilBisysTask = + SendMeldingTilBisysTask(kafkaProducer, tilkjentYtelseRepository, behandlingRepository) + + val barn1 = lagPerson(type = PersonType.BARN) + + every { tilkjentYtelseRepository.findByBehandling(behandling[0].id) } returns lagInitiellTilkjentYtelse().also { + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2037, 12), + prosent = BigDecimal(100), + person = barn1, + ), + ) + } + every { tilkjentYtelseRepository.findByBehandling(behandling[1].id) } returns lagInitiellTilkjentYtelse().also { + // Barn1 reduser fra 04/2022 + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2022, 3), + prosent = BigDecimal(100), + person = barn1, + ), + ) + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2022, 4), + tom = YearMonth.of(2037, 12), + prosent = BigDecimal(50), + person = barn1, + ), + ) + } + + val meldingSlot = slot() + every { + kafkaProducer.kafkaAivenTemplate.send( + OPPHOER_BARNETRYGD_BISYS_TOPIC, + behandling[1].id.toString(), + capture(meldingSlot), + ) + } returns kafkaResult + + sendMeldingTilBisysTask.doTask(SendMeldingTilBisysTask.opprettTask(behandling[1].id)) + + verify(exactly = 1) { kafkaProducer.kafkaAivenTemplate.send(any(), any(), any()) } + val jsonMelding = objectMapper.readValue(meldingSlot.captured, BarnetrygdBisysMelding::class.java) + assertThat(jsonMelding.søker).isEqualTo(behandling[1].fagsak.aktør.aktivFødselsnummer()) + assertThat(jsonMelding.barn).hasSize(1) + assertThat(jsonMelding.barn[0].ident).isEqualTo(barn1.aktør.aktivFødselsnummer()) + assertThat(jsonMelding.barn[0].årsakskode.toString()).isEqualTo("RR") + assertThat(jsonMelding.barn[0].fom).isEqualTo(YearMonth.of(2022, 4)) + } + + @Test + fun `finnBarnEndretOpplysning() skal return riktig endret opplysning for barn`() { + val (behandlingRepository, kafkaProducer, tilkjentYtelseRepository, _, behandling) = setupMocks() + + val sendMeldingTilBisysTask = + SendMeldingTilBisysTask(kafkaProducer, tilkjentYtelseRepository, behandlingRepository) + + val barn1 = lagPerson(type = PersonType.BARN) + val barn2 = lagPerson(type = PersonType.BARN) + val barn3 = lagPerson(type = PersonType.BARN) + + every { tilkjentYtelseRepository.findByBehandling(behandling[0].id) } returns lagInitiellTilkjentYtelse().also { + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2037, 12), + prosent = BigDecimal(100), + person = barn1, + ), + ) + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2037, 12), + prosent = BigDecimal(100), + person = barn2, + ), + ) + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2019, 1), + tom = YearMonth.of(2036, 12), + prosent = BigDecimal(100), + person = barn3, + ), + ) + } + every { tilkjentYtelseRepository.findByBehandling(behandling[1].id) } returns lagInitiellTilkjentYtelse().also { + // Barn1 opphør fra 04/2022 + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2022, 3), + prosent = BigDecimal(100), + person = barn1, + ), + ) + + // Barn2 redusert fra 02/2026 + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2026, 1), + prosent = BigDecimal(100), + person = barn2, + ), + ) + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2026, 2), + tom = YearMonth.of(2037, 12), + prosent = BigDecimal(50), + person = barn2, + ), + ) + + // Barn3 redusert fra 04/2019 og opphørt fra 10/2019 + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2019, 1), + tom = YearMonth.of(2019, 4), + prosent = BigDecimal(100), + person = barn3, + ), + ) + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2019, 5), + tom = YearMonth.of(2019, 9), + prosent = BigDecimal(50), + person = barn3, + ), + ) + } + + val endretPerioder = sendMeldingTilBisysTask.finnBarnEndretOpplysning(behandling[1]) + val barn1Perioder = endretPerioder[barn1.aktør.aktivFødselsnummer()] + val barn2Perioder = endretPerioder[barn2.aktør.aktivFødselsnummer()] + val barn3Perioder = endretPerioder[barn3.aktør.aktivFødselsnummer()] + + assertThat(barn1Perioder).hasSize(1) + assertThat(barn1Perioder!![0].årsakskode).isEqualTo(BarnetrygdEndretType.RO) + assertThat(barn1Perioder[0].fom).isEqualTo(YearMonth.of(2022, 4)) + + assertThat(barn2Perioder).hasSize(1) + assertThat(barn2Perioder!![0].årsakskode).isEqualTo(BarnetrygdEndretType.RR) + assertThat(barn2Perioder[0].fom).isEqualTo(YearMonth.of(2026, 2)) + + assertThat(barn3Perioder).hasSize(2) + + val barn3PeriodeOpphør = barn3Perioder!!.first { it.årsakskode == BarnetrygdEndretType.RO } + assertThat(barn3PeriodeOpphør.årsakskode).isEqualTo(BarnetrygdEndretType.RO) + assertThat(barn3PeriodeOpphør.fom).isEqualTo(YearMonth.of(2019, 10)) + + val barn3PeriodeReduser = barn3Perioder.first { it.årsakskode == BarnetrygdEndretType.RR } + assertThat(barn3PeriodeReduser.årsakskode).isEqualTo(BarnetrygdEndretType.RR) + assertThat(barn3PeriodeReduser.fom).isEqualTo(YearMonth.of(2019, 5)) + } + + @Test + fun `Skal ikke sende melding til bisys hvis endring ikke er reduksjon eller opphøring`() { + val (behandlingRepository, kafkaProducer, tilkjentYtelseRepository, _, behandling) = setupMocks() + val sendMeldingTilBisysTask = + SendMeldingTilBisysTask(kafkaProducer, tilkjentYtelseRepository, behandlingRepository) + + val barn1 = lagPerson(type = PersonType.BARN) + + every { tilkjentYtelseRepository.findByBehandling(behandling[0].id) } returns lagInitiellTilkjentYtelse().also { + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2021, 1), + prosent = BigDecimal(100), + person = barn1, + ), + ) + } + every { tilkjentYtelseRepository.findByBehandling(behandling[1].id) } returns lagInitiellTilkjentYtelse().also { + // Barn1 legger til period fra 04/2022 + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2022, 3), + prosent = BigDecimal(100), + person = barn1, + ), + ) + it.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2022, 4), + tom = YearMonth.of(2037, 12), + prosent = BigDecimal(100), + person = barn1, + ), + ) + } + + sendMeldingTilBisysTask.doTask(SendMeldingTilBisysTask.opprettTask(behandling[1].id)) + + verify(exactly = 0) { kafkaProducer.kafkaAivenTemplate.send(any(), any(), any()) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/SkatteetatenServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/SkatteetatenServiceTest.kt new file mode 100644 index 000000000..e06993252 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/SkatteetatenServiceTest.kt @@ -0,0 +1,142 @@ +package no.nav.familie.ba.sak.ekstern.skatteetaten + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdBarnetrygdClient +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPerson +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPersonerResponse +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.time.LocalDate + +internal class SkatteetatenServiceTest { + + private val infotrygdBarnetrygdClient: InfotrygdBarnetrygdClient = mockk() + private val fagsakRepository: FagsakRepository = mockk() + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository = mockk() + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService = mockk() + + @Test + fun `finnPersonerMedUtvidetBarnetrygd() skal returnere person fra fagsystem med nyeste vedtaksdato`() { + val fagsak = defaultFagsak() + val fagsak2 = defaultFagsak() + + val nyesteVedtaksdato = LocalDate.now() + every { fagsakRepository.finnFagsakerMedUtvidetBarnetrygdInnenfor(any(), any()) } returns listOf( + TestUtvidetSkatt(fagsak.aktør.aktivFødselsnummer(), nyesteVedtaksdato), + TestUtvidetSkatt(fagsak2.aktør.aktivFødselsnummer(), nyesteVedtaksdato.plusDays(2)), + ) + + every { infotrygdBarnetrygdClient.hentPersonerMedUtvidetBarnetrygd(any()) } returns SkatteetatenPersonerResponse( + listOf( + SkatteetatenPerson(fagsak.aktør.aktivFødselsnummer(), nyesteVedtaksdato.atStartOfDay().minusYears(1)), + ), + ) + + val skatteetatenService = + SkatteetatenService( + infotrygdBarnetrygdClient, + fagsakRepository, + andelTilkjentYtelseRepository, + behandlingHentOgPersisterService, + ) + + assertThat(skatteetatenService.finnPersonerMedUtvidetBarnetrygd(nyesteVedtaksdato.year.toString()).brukere).hasSize( + 2, + ) + + assertThat( + skatteetatenService.finnPersonerMedUtvidetBarnetrygd(nyesteVedtaksdato.year.toString()).brukere + .find { it.ident == fagsak.aktør.aktivFødselsnummer() }!!.sisteVedtakPaaIdent, + ) + .isEqualTo(nyesteVedtaksdato.atStartOfDay()) + + assertThat( + skatteetatenService.finnPersonerMedUtvidetBarnetrygd(nyesteVedtaksdato.year.toString()).brukere + .find { it.ident == fagsak2.aktør.aktivFødselsnummer() }!!.sisteVedtakPaaIdent, + ) + .isEqualTo(nyesteVedtaksdato.plusDays(2).atStartOfDay()) + } + + @Test + fun `finnPersonerMedUtvidetBarnetrygd() return kun resultat fra ba-sak når ingen treff i infotrygd`() { + every { infotrygdBarnetrygdClient.hentPersonerMedUtvidetBarnetrygd(any()) } returns + SkatteetatenPersonerResponse(brukere = emptyList()) + + val fagsak = defaultFagsak() + val fagsak2 = defaultFagsak() + + val vedtaksdato = LocalDate.now() + + every { fagsakRepository.finnFagsakerMedUtvidetBarnetrygdInnenfor(any(), any()) } returns listOf( + TestUtvidetSkatt(fagsak.aktør.aktivFødselsnummer(), vedtaksdato), + TestUtvidetSkatt(fagsak2.aktør.aktivFødselsnummer(), vedtaksdato.plusDays(2)), + ) + + val skatteetatenService = + SkatteetatenService( + infotrygdBarnetrygdClient, + fagsakRepository, + andelTilkjentYtelseRepository, + behandlingHentOgPersisterService, + ) + val personerMedUtvidetBarnetrygd = + skatteetatenService.finnPersonerMedUtvidetBarnetrygd(vedtaksdato.year.toString()) + + assertThat(personerMedUtvidetBarnetrygd.brukere).hasSize(2) + + assertThat( + personerMedUtvidetBarnetrygd.brukere + .find { it.ident == fagsak.aktør.aktivFødselsnummer() }!!.sisteVedtakPaaIdent, + ) + .isEqualTo(vedtaksdato.atStartOfDay()) + + assertThat( + personerMedUtvidetBarnetrygd.brukere + .find { it.ident == fagsak2.aktør.aktivFødselsnummer() }!!.sisteVedtakPaaIdent, + ) + .isEqualTo(vedtaksdato.plusDays(2).atStartOfDay()) + } + + @Test + fun `finnPersonerMedUtvidetBarnetrygd() skal return kun resultat fra infotrygd når ingen treff i ba-sak`() { + every { fagsakRepository.finnFagsakerMedUtvidetBarnetrygdInnenfor(any(), any()) } returns emptyList() + + val fagsak = defaultFagsak() + val vedtaksdato = LocalDate.now() + + every { infotrygdBarnetrygdClient.hentPersonerMedUtvidetBarnetrygd(any()) } returns + SkatteetatenPersonerResponse( + brukere = listOf( + SkatteetatenPerson( + fagsak.aktør.aktivFødselsnummer(), + vedtaksdato.atStartOfDay(), + ), + ), + ) + + val skatteetatenService = + SkatteetatenService( + infotrygdBarnetrygdClient, + fagsakRepository, + andelTilkjentYtelseRepository, + behandlingHentOgPersisterService, + ) + val personerMedUtvidetBarnetrygd = + skatteetatenService.finnPersonerMedUtvidetBarnetrygd(vedtaksdato.year.toString()) + + assertThat(personerMedUtvidetBarnetrygd.brukere).hasSize(1) + + assertThat( + personerMedUtvidetBarnetrygd.brukere + .find { it.ident == fagsak.aktør.aktivFødselsnummer() }!!.sisteVedtakPaaIdent, + ) + .isEqualTo(vedtaksdato.atStartOfDay()) + } + + class TestUtvidetSkatt(override val fnr: String, override val sisteVedtaksdato: LocalDate) : UtvidetSkatt +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/tilbakekreving/HentFagsystemsbehandlingRequestConsumerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/tilbakekreving/HentFagsystemsbehandlingRequestConsumerTest.kt new file mode 100644 index 000000000..7915be0b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/ekstern/tilbakekreving/HentFagsystemsbehandlingRequestConsumerTest.kt @@ -0,0 +1,144 @@ +package no.nav.familie.ba.sak.ekstern.tilbakekreving + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.domene.ArbeidsfordelingPåBehandling +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.tilbakekreving.TilbakekrevingService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.statistikk.producer.KafkaProducer +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRespons +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.springframework.kafka.support.Acknowledgment +import java.time.LocalDate +import java.time.LocalDateTime + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class HentFagsystemsbehandlingRequestConsumerTest { + + private lateinit var hentFagsystemsbehandlingRequestConsumer: HentFagsystemsbehandlingRequestConsumer + private lateinit var fagsystemsbehandlingService: FagsystemsbehandlingService + + private lateinit var acknowledgment: Acknowledgment + + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService = mockk(relaxed = true) + private val persongrunnlagService: PersongrunnlagService = mockk() + private val arbeidsfordelingService: ArbeidsfordelingService = mockk() + private val vedtakService: VedtakService = mockk() + private val tilbakekrevingService: TilbakekrevingService = mockk() + private val kafkaProducer: KafkaProducer = mockk() + + private val requestSlot = slot() + private val responsSlot = slot() + private val keySlot = slot() + private val behandlingIdSlot = slot() + + val behandling = lagBehandling(årsak = BehandlingÅrsak.FØDSELSHENDELSE, skalBehandlesAutomatisk = true).also { + it.resultat = Behandlingsresultat.INNVILGET + } + + @BeforeAll + fun init() { + fagsystemsbehandlingService = spyk( + FagsystemsbehandlingService( + behandlingHentOgPersisterService, + persongrunnlagService, + arbeidsfordelingService, + vedtakService, + tilbakekrevingService, + kafkaProducer, + ), + ) + hentFagsystemsbehandlingRequestConsumer = HentFagsystemsbehandlingRequestConsumer(fagsystemsbehandlingService) + + acknowledgment = mockk() + every { acknowledgment.acknowledge() } returns Unit + + every { behandlingHentOgPersisterService.hent(any()) } returns behandling + every { persongrunnlagService.hentAktivThrows(any()) } returns lagTestPersonopplysningGrunnlag( + behandling.id, + tilfeldigPerson(personType = PersonType.BARN), + tilfeldigPerson(personType = PersonType.SØKER), + ) + every { arbeidsfordelingService.hentArbeidsfordelingPåBehandling(any()) } returns ArbeidsfordelingPåBehandling( + behandlendeEnhetId = "4820", + behandlendeEnhetNavn = "Nav", + behandlingId = behandling.id, + ) + every { vedtakService.hentVedtaksdatoForBehandlingThrows(any()) } returns LocalDateTime.now() + every { tilbakekrevingService.hentTilbakekrevingsvalg(any()) } returns Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING + every { kafkaProducer.sendFagsystemsbehandlingResponsForTopicTilbakekreving(any(), any(), any()) } returns Unit + } + + @Test + fun `listen skal lytte request og opprette hentFagsystemsbehandlingRespons`() { + val consumerRecord = ConsumerRecord("testtopic", 1, 1, "1", lagRequest()) + hentFagsystemsbehandlingRequestConsumer.listen(consumerRecord, acknowledgment) + + verify { fagsystemsbehandlingService.hentFagsystemsbehandling(capture(requestSlot)) } + + val request = requestSlot.captured + assertEquals(behandling.fagsak.id.toString(), request.eksternFagsakId) + assertEquals(behandling.id.toString(), request.eksternId) + assertEquals(Ytelsestype.BARNETRYGD, request.ytelsestype) + + verify { + fagsystemsbehandlingService.sendFagsystemsbehandling( + capture(responsSlot), + capture(keySlot), + capture(behandlingIdSlot), + ) + } + + val respons = responsSlot.captured + assertNull(respons.feilMelding) + + val fagsystemsbehandling = respons.hentFagsystemsbehandling + assertNotNull(fagsystemsbehandling) + assertEquals(behandling.fagsak.id.toString(), fagsystemsbehandling!!.eksternFagsakId) + assertEquals(behandling.id.toString(), fagsystemsbehandling.eksternId) + assertEquals(Ytelsestype.BARNETRYGD, fagsystemsbehandling.ytelsestype) + assertEquals("4820", fagsystemsbehandling.enhetId) + assertEquals("Nav", fagsystemsbehandling.enhetsnavn) + assertEquals(Målform.NB.tilSpråkkode(), fagsystemsbehandling.språkkode) + assertEquals(LocalDate.now(), fagsystemsbehandling.revurderingsvedtaksdato) + assertEquals(behandling.resultat.displayName, fagsystemsbehandling.faktainfo.revurderingsresultat) + assertEquals(behandling.opprettetÅrsak.visningsnavn, fagsystemsbehandling.faktainfo.revurderingsårsak) + assertEquals(Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, fagsystemsbehandling.faktainfo.tilbakekrevingsvalg) + assertTrue(fagsystemsbehandling.faktainfo.konsekvensForYtelser.isEmpty()) + assertNull(fagsystemsbehandling.verge) + } + + private fun lagRequest(): String { + return objectMapper.writeValueAsString( + HentFagsystemsbehandlingRequest( + eksternFagsakId = behandling.fagsak.id.toString(), + eksternId = behandling.id.toString(), + ytelsestype = Ytelsestype.BARNETRYGD, + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/Datagenerator.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/Datagenerator.kt new file mode 100644 index 000000000..47aef5bef --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/Datagenerator.kt @@ -0,0 +1,123 @@ +package no.nav.familie.ba.sak.integrasjoner + +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.DEFAULT_JOURNALFØRENDE_ENHET +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.Sakstype +import no.nav.familie.ba.sak.task.dto.FAGSYSTEM +import no.nav.familie.kontrakter.felles.Behandlingstema +import no.nav.familie.kontrakter.felles.BrukerIdType +import no.nav.familie.kontrakter.felles.Tema +import no.nav.familie.kontrakter.felles.journalpost.AvsenderMottaker +import no.nav.familie.kontrakter.felles.journalpost.AvsenderMottakerIdType +import no.nav.familie.kontrakter.felles.journalpost.Bruker +import no.nav.familie.kontrakter.felles.journalpost.DokumentInfo +import no.nav.familie.kontrakter.felles.journalpost.Journalpost +import no.nav.familie.kontrakter.felles.journalpost.Journalposttype +import no.nav.familie.kontrakter.felles.journalpost.Journalstatus +import no.nav.familie.kontrakter.felles.journalpost.LogiskVedlegg +import no.nav.familie.kontrakter.felles.journalpost.RelevantDato +import no.nav.familie.kontrakter.felles.journalpost.Sak +import no.nav.familie.kontrakter.felles.oppgave.Behandlingstype +import no.nav.familie.kontrakter.felles.oppgave.IdentGruppe +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import no.nav.familie.kontrakter.felles.oppgave.OppgaveIdentV2 +import no.nav.familie.kontrakter.felles.oppgave.OppgavePrioritet +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.kontrakter.felles.oppgave.OpprettOppgaveRequest +import no.nav.familie.kontrakter.felles.oppgave.StatusEnum +import java.time.LocalDate +import java.time.LocalDateTime + +fun lagTestJournalpost(personIdent: String, journalpostId: String): Journalpost { + return Journalpost( + journalpostId = journalpostId, + journalposttype = Journalposttype.I, + journalstatus = Journalstatus.MOTTATT, + tema = Tema.BAR.name, + behandlingstema = "ab00001", + bruker = Bruker(personIdent, type = BrukerIdType.FNR), + avsenderMottaker = AvsenderMottaker( + navn = "BLÅØYD HEST", + erLikBruker = true, + id = personIdent, + land = "NO", + type = AvsenderMottakerIdType.FNR, + ), + journalforendeEnhet = DEFAULT_JOURNALFØRENDE_ENHET, + kanal = "NAV_NO", + dokumenter = listOf( + DokumentInfo( + tittel = "Søknad om barnetrygd", + brevkode = "NAV 33-00.07", + dokumentstatus = null, + dokumentvarianter = emptyList(), + dokumentInfoId = "1", + logiskeVedlegg = listOf(LogiskVedlegg("123", "Oppholdstillatelse")), + ), + DokumentInfo( + tittel = "Ekstra vedlegg", + brevkode = null, + dokumentstatus = null, + dokumentvarianter = emptyList(), + dokumentInfoId = "2", + logiskeVedlegg = listOf(LogiskVedlegg("123", "Pass")), + ), + ), + sak = Sak( + arkivsaksnummer = "", + arkivsaksystem = "GSAK", + sakstype = Sakstype.FAGSAK.name, + fagsakId = "10695768", + fagsaksystem = FAGSYSTEM, + ), + tittel = "Søknad om ordinær barnetrygd", + relevanteDatoer = listOf(RelevantDato(LocalDateTime.now(), "DATO_REGISTRERT")), + ) +} + +fun lagTestOppgave(): OpprettOppgaveRequest { + return OpprettOppgaveRequest( + ident = OppgaveIdentV2(ident = "test", gruppe = IdentGruppe.AKTOERID), + saksId = "123", + tema = Tema.BAR, + oppgavetype = Oppgavetype.BehandleSak, + fristFerdigstillelse = LocalDate.now(), + beskrivelse = "test", + enhetsnummer = "1234", + behandlingstema = "behandlingstema", + ) +} + +fun lagTestOppgaveDTO( + oppgaveId: Long, + oppgavetype: Oppgavetype = Oppgavetype.Journalføring, + tildeltRessurs: String? = null, + tildeltEnhetsnr: String? = "4820", +): Oppgave { + return Oppgave( + id = oppgaveId, + aktoerId = randomAktør().aktørId, + identer = listOf(OppgaveIdentV2("11111111111", IdentGruppe.FOLKEREGISTERIDENT)), + journalpostId = "1234", + tildeltEnhetsnr = tildeltEnhetsnr, + tilordnetRessurs = tildeltRessurs, + behandlesAvApplikasjon = "FS22", + beskrivelse = "Beskrivelse for oppgave", + tema = Tema.BAR, + oppgavetype = oppgavetype.value, + behandlingstema = Behandlingstema.OrdinærBarnetrygd.value, + behandlingstype = Behandlingstype.NASJONAL.value, + opprettetTidspunkt = LocalDate.of( + 2020, + 1, + 1, + ).toString(), + fristFerdigstillelse = LocalDate.of( + 2020, + 2, + 1, + ).toString(), + prioritet = OppgavePrioritet.NORM, + status = StatusEnum.OPPRETTET, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/DeserializeTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/DeserializeTest.kt new file mode 100644 index 000000000..6177ab924 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/DeserializeTest.kt @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.integrasjoner + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class DeserializeTest { + private val mapper = ObjectMapper() + .registerKotlinModule() + .registerModule(JavaTimeModule()) + + @Test + fun testDeserializaPersoninfo() { + assertThat(getPersoninfo("M").kjønn).isEqualTo(Kjønn.MANN) + assertThat(getPersoninfo("MANN").kjønn).isEqualTo(Kjønn.MANN) + assertThat(getPersoninfo("K").kjønn).isEqualTo(Kjønn.KVINNE) + assertThat(getPersoninfo("KVINNE").kjønn).isEqualTo(Kjønn.KVINNE) + assertThat(getPersoninfo("UKJENT").kjønn).isEqualTo(Kjønn.UKJENT) + } + + private fun getPersoninfo(kjønn: String): PersonInfo { + val json = """ + { + "kjønn": "$kjønn", + "fødselsdato": "1982-08-05" + } + """.trimIndent() + return mapper.readValue(json, PersonInfo::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/ecb/ECBServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/ecb/ECBServiceTest.kt new file mode 100644 index 000000000..304d08444 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/ecb/ECBServiceTest.kt @@ -0,0 +1,140 @@ +package no.nav.familie.ba.sak.integrasjoner.ecb + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.unmockkAll +import no.nav.familie.valutakurs.Frequency +import no.nav.familie.valutakurs.ValutakursRestClient +import no.nav.familie.valutakurs.domene.ECBExchangeRate +import no.nav.familie.valutakurs.domene.ECBExchangeRateDate +import no.nav.familie.valutakurs.domene.ECBExchangeRateKey +import no.nav.familie.valutakurs.domene.ECBExchangeRateValue +import no.nav.familie.valutakurs.domene.ECBExchangeRatesData +import no.nav.familie.valutakurs.domene.ECBExchangeRatesDataSet +import no.nav.familie.valutakurs.domene.ECBExchangeRatesForCurrency +import no.nav.familie.valutakurs.domene.toExchangeRates +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import java.math.BigDecimal +import java.time.LocalDate + +@ExtendWith(MockKExtension::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ECBServiceTest { + + @MockK + private lateinit var ecbClient: ValutakursRestClient + + @InjectMockKs + private lateinit var ecbService: ECBService + + @AfterAll + fun tearDown() { + unmockkAll() + } + + @Test + fun `Hent valutakurs for utenlandsk valuta til NOK og sjekk at beregning av kurs er riktig`() { + val valutakursDato = LocalDate.of(2022, 6, 28) + val ecbExchangeRatesData = createECBResponse( + Frequency.Daily, + listOf(Pair("NOK", BigDecimal.valueOf(10.337)), Pair("SEK", BigDecimal.valueOf(10.6543))), + valutakursDato.toString(), + ) + every { + ecbClient.hentValutakurs( + Frequency.Daily, + listOf("NOK", "SEK"), + valutakursDato, + ) + } returns ecbExchangeRatesData.toExchangeRates() + val SEKtilNOKValutakurs = ecbService.hentValutakurs("SEK", valutakursDato) + assertEquals(BigDecimal.valueOf(0.9702185972), SEKtilNOKValutakurs) + } + + @Test + fun `Test at ECBService kaster ESBServiceException dersom de returnerte kursene ikke inneholder kurs for forespurt valuta`() { + val valutakursDato = LocalDate.of(2022, 7, 22) + val ecbExchangeRatesData = createECBResponse( + Frequency.Daily, + listOf(Pair("NOK", BigDecimal.valueOf(10.337))), + valutakursDato.toString(), + ) + every { + ecbClient.hentValutakurs( + Frequency.Daily, + listOf("NOK", "SEK"), + valutakursDato, + ) + } returns ecbExchangeRatesData.toExchangeRates() + assertThrows { ecbService.hentValutakurs("SEK", valutakursDato) } + } + + @Test + fun `Test at ECBService kaster ESBServiceException dersom de returnerte kursene ikke inneholder kurser med forespurt dato`() { + val valutakursDato = LocalDate.of(2022, 7, 20) + val ecbExchangeRatesData = createECBResponse( + Frequency.Daily, + listOf(Pair("NOK", BigDecimal.valueOf(10.337)), Pair("SEK", BigDecimal.valueOf(10.6543))), + valutakursDato.minusDays(1).toString(), + ) + every { + ecbClient.hentValutakurs( + Frequency.Daily, + listOf("NOK", "SEK"), + valutakursDato, + ) + } returns ecbExchangeRatesData.toExchangeRates() + assertThrows { ecbService.hentValutakurs("SEK", valutakursDato) } + } + + @Test + fun `Test at ECBService returnerer NOK til EUR dersom den forespurte valutaen er EUR`() { + val nokTilEur = BigDecimal.valueOf(9.4567) + val valutakursDato = LocalDate.of(2022, 7, 20) + val ecbExchangeRatesData = createECBResponse( + Frequency.Daily, + listOf(Pair("NOK", BigDecimal.valueOf(9.4567))), + valutakursDato.toString(), + ) + every { + ecbClient.hentValutakurs( + Frequency.Daily, + listOf("NOK", "EUR"), + valutakursDato, + ) + } returns ecbExchangeRatesData.toExchangeRates() + assertEquals(nokTilEur, ecbService.hentValutakurs("EUR", valutakursDato)) + } + + private fun createECBResponse( + frequency: Frequency, + exchangeRates: List>, + exchangeRateDate: String, + ): ECBExchangeRatesData { + return ECBExchangeRatesData( + ECBExchangeRatesDataSet( + exchangeRates.map { + ECBExchangeRatesForCurrency( + listOf( + ECBExchangeRateKey("CURRENCY", it.first), + ECBExchangeRateKey("FREQ", frequency.toFrequencyParam()), + ), + listOf( + ECBExchangeRate( + ECBExchangeRateDate(exchangeRateDate), + ECBExchangeRateValue((it.second)), + ), + ), + ) + }, + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/FamilieIntegrasjonerTilgangskontrollServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/FamilieIntegrasjonerTilgangskontrollServiceTest.kt new file mode 100644 index 000000000..2352b4952 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/familieintegrasjoner/FamilieIntegrasjonerTilgangskontrollServiceTest.kt @@ -0,0 +1,90 @@ +package no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner + +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.clearAllCaches +import no.nav.familie.ba.sak.config.IntegrasjonClientMock.Companion.mockSjekkTilgang +import no.nav.familie.ba.sak.util.BrukerContextUtil.testWithBrukerContext +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.cache.concurrent.ConcurrentMapCacheManager + +class FamilieIntegrasjonerTilgangskontrollServiceTest { + + private val client = mockk() + + private val cacheManager = ConcurrentMapCacheManager() + + private val service = FamilieIntegrasjonerTilgangskontrollService(client, cacheManager, mockk()) + + private val slot = mutableListOf>() + + @BeforeEach + fun setUp() { + slot.clear() + cacheManager.clearAllCaches() + } + + @Test + fun `har tilgang skal cacheas`() { + client.mockSjekkTilgang(true, slot) + + assertThat(testWithBrukerContext { service.sjekkTilgangTilPerson("1") }.harTilgang).isTrue + assertThat(testWithBrukerContext { service.sjekkTilgangTilPerson("1") }.harTilgang).isTrue + verify(exactly = 1) { client.sjekkTilgangTilPersoner(any()) } + } + + @Test + fun `har ikke tilgang skal cacheas`() { + client.mockSjekkTilgang(false, slot) + + assertThat(testWithBrukerContext { service.sjekkTilgangTilPerson("1") }.harTilgang).isFalse + assertThat(testWithBrukerContext { service.sjekkTilgangTilPerson("1") }.harTilgang).isFalse + verify(exactly = 1) { client.sjekkTilgangTilPersoner(any()) } + } + + @Test + fun `cachear per saksbehandlere`() { + client.mockSjekkTilgang(false, slot) + + // Systemcontext + service.sjekkTilgangTilPerson("1") + val kall1 = testWithBrukerContext("saksbehandler1") { service.sjekkTilgangTilPerson("1") } + val kall2 = testWithBrukerContext("saksbehandler2") { service.sjekkTilgangTilPerson("1") } + assertThat(kall1.harTilgang).isFalse + assertThat(kall2.harTilgang).isFalse + verify(exactly = 3) { client.sjekkTilgangTilPersoner(any()) } + } + + @Test + fun `tilgangskontrollerer unike identer`() { + client.mockSjekkTilgang(false, slot) + + testWithBrukerContext("saksbehandler1") { service.sjekkTilgangTilPersoner(listOf("1", "1")) } + + verify(exactly = 1) { client.sjekkTilgangTilPersoner(listOf("1")) } + } + + @Test + fun `skal ikke hente identer som allerede finnes i cachen`() { + val tilgang = mapOf("1" to false, "2" to true, "3" to false) + client.mockSjekkTilgang(tilgang, slot) + + testWithBrukerContext { service.sjekkTilgangTilPerson("1") } + val sjekkTilgangTilPersoner = testWithBrukerContext { service.sjekkTilgangTilPersoner(listOf("2", "1", "3")) } + testWithBrukerContext { service.sjekkTilgangTilPersoner(listOf("2", "1", "3")) } + testWithBrukerContext { service.sjekkTilgangTilPersoner(listOf("3", "3", "3")) } + + assertThat(sjekkTilgangTilPersoner.all { it.key == it.value.personIdent }) + assertThat(sjekkTilgangTilPersoner.map { it.key to it.value.harTilgang }).containsExactlyInAnyOrderElementsOf( + tilgang.entries.map { Pair(it.key, it.value) }.toList(), + ) + + verify(exactly = 2) { client.sjekkTilgangTilPersoner(any()) } + + val forventetFørsteKall = listOf("1") + val forventetAndreKall = listOf("2", "3") + assertThat(slot).containsExactlyElementsOf(listOf(forventetFørsteKall, forventetAndreKall)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdBarnetrygdClientMock.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdBarnetrygdClientMock.kt new file mode 100644 index 000000000..55c84d039 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdBarnetrygdClientMock.kt @@ -0,0 +1,41 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd + +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.kontrakter.ba.infotrygd.InfotrygdSøkResponse +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile + +@TestConfiguration +class InfotrygdBarnetrygdClientMock { + + @Bean + @Profile("mock-infotrygd-barnetrygd") + @Primary + fun mockInfotrygdBarnetrygd(): InfotrygdBarnetrygdClient { + val mockInfotrygdBarnetrygdClient = mockk(relaxed = true) + + clearInfotrygdBarnetrygdMocks(mockInfotrygdBarnetrygdClient) + + return mockInfotrygdBarnetrygdClient + } + + companion object { + fun clearInfotrygdBarnetrygdMocks(mockInfotrygdBarnetrygdClient: InfotrygdBarnetrygdClient) { + clearMocks(mockInfotrygdBarnetrygdClient) + + every { mockInfotrygdBarnetrygdClient.harLøpendeSakIInfotrygd(any(), any()) } returns false + every { mockInfotrygdBarnetrygdClient.hentSaker(any(), any()) } returns InfotrygdSøkResponse( + emptyList(), + emptyList(), + ) + every { mockInfotrygdBarnetrygdClient.hentStønader(any(), any()) } returns InfotrygdSøkResponse( + emptyList(), + emptyList(), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdControllerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdControllerTest.kt new file mode 100644 index 000000000..515f9a7dc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdControllerTest.kt @@ -0,0 +1,102 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.SpyK +import io.mockk.junit5.MockKExtension +import no.nav.familie.ba.sak.common.clearAllCaches +import no.nav.familie.ba.sak.config.IntegrasjonClientMock.Companion.mockSjekkTilgang +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollClient +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollService +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.SystemOnlyPdlRestClient +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.kontrakter.ba.infotrygd.InfotrygdSøkResponse +import no.nav.familie.kontrakter.ba.infotrygd.Sak +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.familie.kontrakter.felles.personopplysning.Adressebeskyttelse +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +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.springframework.cache.concurrent.ConcurrentMapCacheManager +import org.springframework.http.HttpStatus + +@ExtendWith(MockKExtension::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class InfotrygdControllerTest { + @MockK + lateinit var personopplysningerService: PersonopplysningerService + + @MockK + lateinit var systemOnlyPdlRestClient: SystemOnlyPdlRestClient + + @SpyK + var cacheManager = ConcurrentMapCacheManager() + + @MockK + lateinit var familieIntegrasjonerTilgangskontrollClient: FamilieIntegrasjonerTilgangskontrollClient + + @InjectMockKs + lateinit var familieIntegrasjonerTilgangskontrollService: FamilieIntegrasjonerTilgangskontrollService + + @MockK + lateinit var infotrygdBarnetrygdClient: InfotrygdBarnetrygdClient + + @MockK + lateinit var personidentService: PersonidentService + + @InjectMockKs + lateinit var infotrygdService: InfotrygdService + + lateinit var infotrygdController: InfotrygdController + + @BeforeAll + fun init() { + infotrygdController = InfotrygdController(infotrygdBarnetrygdClient, personidentService, infotrygdService) + } + + @BeforeEach + fun setUp() { + cacheManager.clearAllCaches() + } + + @Test + fun `hentInfotrygdsakerForSøker skal returnere ok dersom saksbehandler har tilgang`() { + val fnr = "12345678910" + + every { personidentService.hentAktør(fnr) } returns tilAktør(fnr) + familieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(true) + every { + infotrygdBarnetrygdClient.hentSaker( + any(), + any(), + ) + } returns InfotrygdSøkResponse(listOf(Sak(status = "IP")), emptyList()) + val respons = infotrygdController.hentInfotrygdsakerForSøker(Personident(fnr)) + + Assertions.assertEquals(HttpStatus.OK, respons.statusCode) + Assertions.assertEquals(true, respons.body?.data?.harTilgang) + Assertions.assertEquals("IP", respons.body?.data?.saker!![0].status) + } + + @Test + fun `hentInfotrygdsakerForSøker skal returnere ok, men ha gradering satt, dersom saksbehandler ikke har tilgang`() { + val fnr = "12345678910" + + every { personidentService.hentAktør(fnr) } returns tilAktør(fnr) + familieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(false) + every { systemOnlyPdlRestClient.hentAdressebeskyttelse(any()) } returns + listOf(Adressebeskyttelse(ADRESSEBESKYTTELSEGRADERING.FORTROLIG)) + + val respons = infotrygdController.hentInfotrygdsakerForSøker(Personident(fnr)) + + Assertions.assertEquals(HttpStatus.OK, respons.statusCode) + Assertions.assertEquals(false, respons.body?.data?.harTilgang) + Assertions.assertEquals(ADRESSEBESKYTTELSEGRADERING.FORTROLIG, respons.body?.data?.adressebeskyttelsegradering) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedClientMock.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedClientMock.kt new file mode 100644 index 000000000..f28c6ce38 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedClientMock.kt @@ -0,0 +1,18 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd + +import io.mockk.mockk +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile + +@TestConfiguration +class InfotrygdFeedClientMock { + + @Bean + @Profile("mock-infotrygd-feed") + @Primary + fun mockInfotrygdFeed(): InfotrygdFeedClient { + return mockk(relaxed = true) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedServiceTest.kt new file mode 100644 index 000000000..886c64783 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedServiceTest.kt @@ -0,0 +1,31 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd + +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.task.OpprettTaskService +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class InfotrygdFeedServiceTest { + private val opprettTaskServiceMock = mockk() + + @Test + fun `Skal send riktig start behandling feed`() { + val ident = "12345678900" + val identSlot = slot() + every { opprettTaskServiceMock.opprettSendStartBehandlingTilInfotrygdTask(capture(identSlot)) } just runs + + val infotrygdFeedService = InfotrygdFeedService(opprettTaskServiceMock) + infotrygdFeedService.sendStartBehandlingTilInfotrygdFeed(tilAktør(ident)) + verify(exactly = 1) { + opprettTaskServiceMock.opprettSendStartBehandlingTilInfotrygdTask(any()) + } + assertThat(identSlot.captured.aktivFødselsnummer()).isEqualTo(ident) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveControllerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveControllerTest.kt new file mode 100644 index 000000000..994120cfe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveControllerTest.kt @@ -0,0 +1,117 @@ +package no.nav.familie.ba.sak.integrasjoner.oppgave + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.just +import io.mockk.runs +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonException +import no.nav.familie.ba.sak.integrasjoner.journalføring.InnkommendeJournalføringService +import no.nav.familie.ba.sak.integrasjoner.oppgave.domene.RestFinnOppgaveRequest +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Tema +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveResponseDto +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.http.HttpStatus + +@ExtendWith(MockKExtension::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class OppgaveControllerTest { + + @MockK + lateinit var oppgaveService: OppgaveService + + @MockK + lateinit var personopplysningerService: PersonopplysningerService + + @MockK + lateinit var personidentService: PersonidentService + + @MockK + lateinit var integrasjonClient: IntegrasjonClient + + @MockK + lateinit var fagsakService: FagsakService + + @MockK + lateinit var innkommendeJournalføringService: InnkommendeJournalføringService + + @MockK + lateinit var tilgangService: TilgangService + + @InjectMockKs + lateinit var oppgaveController: OppgaveController + + @BeforeAll + fun init() { + every { tilgangService.verifiserHarTilgangTilHandling(any(), any()) } just runs + } + + @Test + fun `Tildeling av oppgave til saksbehandler skal returnere OK og sende med OppgaveId i respons`() { + val OPPGAVE_ID = "1234" + val SAKSBEHANDLER_ID = "Z999999" + every { oppgaveService.fordelOppgave(any(), any()) } returns OPPGAVE_ID + + val respons = oppgaveController.fordelOppgave(OPPGAVE_ID.toLong(), SAKSBEHANDLER_ID) + + Assertions.assertEquals(HttpStatus.OK, respons.statusCode) + Assertions.assertEquals(OPPGAVE_ID, respons.body?.data) + } + + @Test + fun `Tilbakestilling av tildeling på oppgave skal returnere OK og sende med Oppgave i respons`() { + val oppgave = Oppgave( + id = 1234, + ) + every { oppgaveService.tilbakestillFordelingPåOppgave(oppgave.id!!) } returns oppgave + + val respons = oppgaveController.tilbakestillFordelingPåOppgave(oppgave.id!!) + + Assertions.assertEquals(HttpStatus.OK, respons.statusCode) + Assertions.assertEquals(oppgave, respons.body?.data) + } + + @Test + fun `Tildeling av oppgave skal returnere feil ved feil fra integrasjonsklienten`() { + val OPPGAVE_ID = "1234" + val SAKSBEHANDLER_ID = "Z999998" + every { + oppgaveService.fordelOppgave( + any(), + any(), + ) + } throws IntegrasjonException("Kall mot integrasjon feilet ved fordel oppgave") + + val exception = assertThrows { + oppgaveController.fordelOppgave( + OPPGAVE_ID.toLong(), + SAKSBEHANDLER_ID, + ) + } + + Assertions.assertEquals("Kall mot integrasjon feilet ved fordel oppgave", exception.message) + } + + @Test + fun `hentOppgaver via OppgaveController skal fungere`() { + every { + oppgaveService.hentOppgaver(any()) + } returns FinnOppgaveResponseDto(1, listOf(Oppgave(tema = Tema.BAR))) + val response = oppgaveController.hentOppgaver(RestFinnOppgaveRequest()) + val oppgaverOgAntall = response.body?.data as FinnOppgaveResponseDto + Assertions.assertEquals(1, oppgaverOgAntall.antallTreffTotalt) + Assertions.assertEquals(Tema.BAR, oppgaverOgAntall.oppgaver.first().tema) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveServiceTest.kt new file mode 100644 index 000000000..086d78b5e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveServiceTest.kt @@ -0,0 +1,309 @@ +package no.nav.familie.ba.sak.integrasjoner.oppgave + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.lagTestOppgaveDTO +import no.nav.familie.ba.sak.integrasjoner.oppgave.domene.DbOppgave +import no.nav.familie.ba.sak.integrasjoner.oppgave.domene.OppgaveRepository +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.domene.ArbeidsfordelingPåBehandling +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.domene.ArbeidsfordelingPåBehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.FØRSTE_STEG +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.ba.sak.task.dto.ManuellOppgaveType +import no.nav.familie.kontrakter.felles.Behandlingstema +import no.nav.familie.kontrakter.felles.Tema +import no.nav.familie.kontrakter.felles.oppgave.IdentGruppe +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import no.nav.familie.kontrakter.felles.oppgave.OppgaveIdentV2 +import no.nav.familie.kontrakter.felles.oppgave.OppgaveResponse +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.kontrakter.felles.oppgave.OpprettOppgaveRequest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import java.time.LocalDate + +@ExtendWith(MockKExtension::class) +class OppgaveServiceTest { + @MockK + lateinit var integrasjonClient: IntegrasjonClient + + @MockK + lateinit var personopplysningerService: PersonopplysningerService + + @MockK + lateinit var arbeidsfordelingPåBehandlingRepository: ArbeidsfordelingPåBehandlingRepository + + @MockK + lateinit var arbeidsfordelingService: ArbeidsfordelingService + + @MockK + lateinit var behandlingRepository: BehandlingRepository + + @MockK + lateinit var behandlingHentOgPersisterService: BehandlingHentOgPersisterService + + @MockK + lateinit var personidentService: PersonidentService + + @MockK + lateinit var oppgaveRepository: OppgaveRepository + + @MockK + lateinit var opprettTaskService: OpprettTaskService + + @MockK + lateinit var loggService: LoggService + + @InjectMockKs + lateinit var oppgaveService: OppgaveService + + @Test + fun `Opprett oppgave skal lage oppgave med enhetsnummer fra behandlingen`() { + every { behandlingHentOgPersisterService.hent(BEHANDLING_ID) } returns lagTestBehandling(aktørId = AKTØR_ID_FAGSAK) + every { behandlingHentOgPersisterService.lagreEllerOppdater(any()) } returns lagTestBehandling() + every { oppgaveRepository.save(any()) } returns lagTestOppgave() + every { + oppgaveRepository.findByOppgavetypeAndBehandlingAndIkkeFerdigstilt( + any(), + any(), + ) + } returns null + every { personidentService.hentAktør(any()) } returns Aktør(AKTØR_ID_FAGSAK) + + every { arbeidsfordelingService.hentArbeidsfordelingPåBehandling(any()) } returns ArbeidsfordelingPåBehandling( + behandlingId = 1, + behandlendeEnhetId = ENHETSNUMMER, + behandlendeEnhetNavn = "enhet", + ) + + every { arbeidsfordelingPåBehandlingRepository.finnArbeidsfordelingPåBehandling(any()) } returns ArbeidsfordelingPåBehandling( + behandlingId = 1, + behandlendeEnhetId = ENHETSNUMMER, + behandlendeEnhetNavn = "enhet", + ) + + val slot = slot() + every { integrasjonClient.opprettOppgave(capture(slot)) } returns OppgaveResponse(OPPGAVE_ID.toLong()) + + oppgaveService.opprettOppgave(BEHANDLING_ID, Oppgavetype.BehandleSak, FRIST_FERDIGSTILLELSE_BEH_SAK) + + assertThat(slot.captured.enhetsnummer).isEqualTo(ENHETSNUMMER) + assertThat(slot.captured.saksId).isEqualTo(FAGSAK_ID.toString()) + assertThat(slot.captured.ident).isEqualTo( + OppgaveIdentV2( + ident = AKTØR_ID_FAGSAK, + gruppe = IdentGruppe.AKTOERID, + ), + ) + assertThat(slot.captured.behandlingstema).isEqualTo(Behandlingstema.OrdinærBarnetrygd.value) + assertThat(slot.captured.fristFerdigstillelse).isEqualTo(LocalDate.now().plusDays(1)) + assertThat(slot.captured.aktivFra).isEqualTo(LocalDate.now()) + assertThat(slot.captured.tema).isEqualTo(Tema.BAR) + assertThat(slot.captured.beskrivelse).contains("https://barnetrygd.intern.nav.no/fagsak/$FAGSAK_ID") + assertThat(slot.captured.behandlesAvApplikasjon).isEqualTo("familie-ba-sak") + } + + @ParameterizedTest + @EnumSource(ManuellOppgaveType::class) + fun `Opprett oppgave med manuell oppgavetype skal lage oppgave med behandlesAvApplikasjon satt for småbarnstillegg og åpen behandling, men ikke fødselshendelse`(manuellOppgaveType: ManuellOppgaveType) { + every { behandlingHentOgPersisterService.hent(BEHANDLING_ID) } returns lagTestBehandling(aktørId = AKTØR_ID_FAGSAK) + every { behandlingHentOgPersisterService.lagreEllerOppdater(any()) } returns lagTestBehandling() + every { oppgaveRepository.save(any()) } returns lagTestOppgave() + every { + oppgaveRepository.findByOppgavetypeAndBehandlingAndIkkeFerdigstilt( + any(), + any(), + ) + } returns null + every { personidentService.hentAktør(any()) } returns Aktør(AKTØR_ID_FAGSAK) + + every { arbeidsfordelingService.hentArbeidsfordelingPåBehandling(any()) } returns ArbeidsfordelingPåBehandling( + behandlingId = 1, + behandlendeEnhetId = ENHETSNUMMER, + behandlendeEnhetNavn = "enhet", + ) + + every { arbeidsfordelingPåBehandlingRepository.finnArbeidsfordelingPåBehandling(any()) } returns ArbeidsfordelingPåBehandling( + behandlingId = 1, + behandlendeEnhetId = ENHETSNUMMER, + behandlendeEnhetNavn = "enhet", + ) + + val slot = slot() + every { integrasjonClient.opprettOppgave(capture(slot)) } returns OppgaveResponse(OPPGAVE_ID.toLong()) + + oppgaveService.opprettOppgave( + behandlingId = BEHANDLING_ID, + oppgavetype = Oppgavetype.VurderLivshendelse, + fristForFerdigstillelse = FRIST_FERDIGSTILLELSE_BEH_SAK, + manuellOppgaveType = manuellOppgaveType, + ) + + assertThat(slot.captured.enhetsnummer).isEqualTo(ENHETSNUMMER) + assertThat(slot.captured.saksId).isEqualTo(FAGSAK_ID.toString()) + assertThat(slot.captured.ident).isEqualTo( + OppgaveIdentV2( + ident = AKTØR_ID_FAGSAK, + gruppe = IdentGruppe.AKTOERID, + ), + ) + assertThat(slot.captured.behandlingstema).isEqualTo(Behandlingstema.OrdinærBarnetrygd.value) + assertThat(slot.captured.fristFerdigstillelse).isEqualTo(LocalDate.now().plusDays(1)) + assertThat(slot.captured.aktivFra).isEqualTo(LocalDate.now()) + assertThat(slot.captured.tema).isEqualTo(Tema.BAR) + assertThat(slot.captured.beskrivelse).contains("https://barnetrygd.intern.nav.no/fagsak/$FAGSAK_ID") + + when (manuellOppgaveType) { + ManuellOppgaveType.SMÅBARNSTILLEGG, ManuellOppgaveType.ÅPEN_BEHANDLING -> assertThat(slot.captured.behandlesAvApplikasjon).isEqualTo("familie-ba-sak") + ManuellOppgaveType.FØDSELSHENDELSE -> assertThat(slot.captured.behandlesAvApplikasjon).isNull() + } + } + + @Test + fun `Ferdigstill oppgave`() { + every { behandlingHentOgPersisterService.hent(BEHANDLING_ID) } returns mockk {} + every { + oppgaveRepository.finnOppgaverSomSkalFerdigstilles( + any(), + any(), + ) + } returns listOf(lagTestOppgave()) + every { oppgaveRepository.saveAndFlush(any()) } returns lagTestOppgave() + val slot = slot() + every { integrasjonClient.ferdigstillOppgave(capture(slot)) } just runs + every { integrasjonClient.finnOppgaveMedId(any()) } returns lagTestOppgaveDTO(0L) + + oppgaveService.ferdigstillOppgaver(BEHANDLING_ID, Oppgavetype.BehandleSak) + assertThat(slot.captured).isEqualTo(OPPGAVE_ID.toLong()) + } + + @Test + fun `Fordel oppgave skal tildele oppgave til saksbehandler`() { + val oppgaveSlot = slot() + val saksbehandlerSlot = slot() + every { + integrasjonClient.fordelOppgave( + capture(oppgaveSlot), + capture(saksbehandlerSlot), + ) + } returns OppgaveResponse(OPPGAVE_ID.toLong()) + every { integrasjonClient.finnOppgaveMedId(any()) } returns Oppgave() + + oppgaveService.fordelOppgave(OPPGAVE_ID.toLong(), SAKSBEHANDLER_ID) + + assertEquals(OPPGAVE_ID.toLong(), oppgaveSlot.captured) + assertEquals(SAKSBEHANDLER_ID, saksbehandlerSlot.captured) + } + + @Test + fun `Fordel oppgave skal feile når oppgave allerede er tildelt`() { + val oppgaveSlot = slot() + val saksbehandlerSlot = slot() + val saksbehandler = "Test Testersen" + every { + integrasjonClient.fordelOppgave( + capture(oppgaveSlot), + capture(saksbehandlerSlot), + ) + } returns OppgaveResponse(OPPGAVE_ID.toLong()) + every { integrasjonClient.finnOppgaveMedId(any()) } returns Oppgave(tilordnetRessurs = saksbehandler) + + val funksjonellFeil = + assertThrows { oppgaveService.fordelOppgave(OPPGAVE_ID.toLong(), SAKSBEHANDLER_ID) } + + assertEquals("Oppgaven er allerede fordelt til $saksbehandler", funksjonellFeil.frontendFeilmelding) + } + + @Test + fun `Tilbakestill oppgave skal nullstille tildeling på oppgave`() { + val fordelOppgaveSlot = slot() + val finnOppgaveSlot = slot() + every { + integrasjonClient.fordelOppgave( + capture(fordelOppgaveSlot), + any(), + ) + } returns OppgaveResponse(OPPGAVE_ID.toLong()) + every { integrasjonClient.finnOppgaveMedId(capture(finnOppgaveSlot)) } returns Oppgave() + + oppgaveService.tilbakestillFordelingPåOppgave(OPPGAVE_ID.toLong()) + + assertEquals(OPPGAVE_ID.toLong(), fordelOppgaveSlot.captured) + assertEquals(OPPGAVE_ID.toLong(), finnOppgaveSlot.captured) + verify(exactly = 1) { integrasjonClient.fordelOppgave(any(), null) } + } + + @Test + fun `hent oppgavefrister for åpne utvidtet barnetrygd behandlinger`() { + every { behandlingRepository.finnÅpneUtvidetBarnetrygdBehandlinger() } returns listOf( + lagTestBehandling().copy(underkategori = BehandlingUnderkategori.UTVIDET, id = 1002602L), + lagTestBehandling().copy(underkategori = BehandlingUnderkategori.UTVIDET, id = 1002602L), + ) + every { oppgaveRepository.findByOppgavetypeAndBehandlingAndIkkeFerdigstilt(any(), any()) } returns lagTestOppgave() + + every { integrasjonClient.finnOppgaveMedId(any()) } returns Oppgave(id = 10018798L, fristFerdigstillelse = "21.01.23") + + assertEquals( + "behandlingId;oppgaveId;frist\n" + + "1002602;10018798;21.01.23\n" + + "1002602;10018798;21.01.23\n", + oppgaveService.hentFristerForÅpneUtvidetBarnetrygdBehandlinger(), + ) + } + + private fun lagTestBehandling(aktørId: String = "1234567891000"): Behandling { + return Behandling( + fagsak = Fagsak(id = FAGSAK_ID, aktør = Aktør(aktørId)), + type = BehandlingType.FØRSTEGANGSBEHANDLING, + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + opprettetÅrsak = BehandlingÅrsak.SØKNAD, + ).also { + it.behandlingStegTilstand.add(BehandlingStegTilstand(0, it, FØRSTE_STEG)) + } + } + + private fun lagTestOppgave(): DbOppgave { + return DbOppgave(behandling = lagTestBehandling(), type = Oppgavetype.BehandleSak, gsakId = OPPGAVE_ID) + } + + companion object { + + private const val FAGSAK_ID = 10000000L + private const val BEHANDLING_ID = 20000000L + private const val OPPGAVE_ID = "42" + private const val FNR = "12345678910" + private const val ENHETSNUMMER = "enhet" + private const val AKTØR_ID_FAGSAK = "1234567891000" + private const val SAKSBEHANDLER_ID = "Z999999" + private val FRIST_FERDIGSTILLELSE_BEH_SAK = LocalDate.now().plusDays(1) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlGraphqlTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlGraphqlTest.kt new file mode 100644 index 000000000..4f9b1a14e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PdlGraphqlTest.kt @@ -0,0 +1,109 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl + +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlAdressebeskyttelseResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlBaseResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlHentPersonRelasjonerResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlHentPersonResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlNavn +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.io.File + +class PdlGraphqlTest { + + private val mapper = objectMapper + + @Test + fun testDeserialization() { + val resp = + mapper.readValue>(File(getFile("pdl/pdlOkResponse.json"))) + assertThat(resp.data.person!!.foedsel.first().foedselsdato).isEqualTo("1955-09-13") + assertThat(resp.data.person!!.navn.first().fornavn).isEqualTo("ENGASJERT") + assertThat(resp.data.person!!.kjoenn.first().kjoenn.toString()).isEqualTo("MANN") + assertThat(resp.data.person!!.forelderBarnRelasjon.first().relatertPersonsIdent).isEqualTo("12345678910") + assertThat(resp.data.person!!.forelderBarnRelasjon.first().relatertPersonsRolle.toString()).isEqualTo("BARN") + assertThat(resp.data.person!!.sivilstand.first().type).isEqualTo(SIVILSTAND.UGIFT) + assertThat(resp.data.person!!.bostedsadresse.first().vegadresse?.husnummer).isEqualTo("3") + assertThat(resp.data.person!!.bostedsadresse.first().vegadresse?.matrikkelId).isEqualTo(1234) + assertNull(resp.data.person!!.bostedsadresse.first().matrikkeladresse) + assertNull(resp.data.person!!.bostedsadresse.first().ukjentBosted) + assertThat(resp.errorMessages()).isEqualTo("") + } + + @Test + fun testTomAdresse() { + val resp = + mapper.readValue>(File(getFile("pdl/pdlTomAdresseOkResponse.json"))) + assertTrue(resp.data.person!!.bostedsadresse.isEmpty()) + } + + @Test + fun testForelderBarnRelasjon() { + val resp = mapper.readValue>( + File(getFile("pdl/pdlForelderBarnRelasjonResponse.json")), + ) + assertThat(resp.data.person!!.forelderBarnRelasjon.first().relatertPersonsRolle).isEqualTo( + FORELDERBARNRELASJONROLLE.BARN, + ) + assertThat(resp.data.person!!.forelderBarnRelasjon.first().relatertPersonsIdent).isEqualTo("32345678901") + } + + @Test + fun testMatrikkelAdresse() { + val resp = + mapper.readValue>(File(getFile("pdl/pdlMatrikkelAdresseOkResponse.json"))) + assertThat(resp.data.person!!.bostedsadresse.first().matrikkeladresse?.postnummer).isEqualTo("0274") + assertThat(resp.data.person!!.bostedsadresse.first().matrikkeladresse?.matrikkelId).isEqualTo(2147483649) + } + + @Test + fun testUkjentBostedAdresse() { + val resp = mapper.readValue>( + File(getFile("pdl/pdlUkjentBostedAdresseOkResponse.json")), + ) + assertThat(resp.data.person!!.bostedsadresse.first().ukjentBosted?.bostedskommune).isEqualTo("Oslo") + } + + @Test + fun testAdressebeskyttelse() { + val resp = mapper.readValue>( + File(getFile("pdl/pdlAdressebeskyttelseResponse.json")), + ) + assertThat(resp.data.person!!.adressebeskyttelse.first().gradering).isEqualTo(ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG) + } + + @Test + fun testDeserializationOfResponseWithErrors() { + val resp = + mapper.readValue>(File(getFile("pdl/pdlPersonIkkeFunnetResponse.json"))) + assertThat(resp.harFeil()).isTrue + assertThat(resp.errorMessages()).contains("Fant ikke person", "Ikke tilgang") + assertThat(resp.errors!!.any { it.extensions?.notFound() == true }).isTrue + } + + @Test + fun testDeserializationOfResponseWithoutFødselsdato() { + val resp = + mapper.readValue>(File(getFile("pdl/pdlManglerFoedselResponse.json"))) + assertThat(resp.data.person!!.foedsel.first().foedselsdato).isNull() + } + + @Test + fun testFulltNavn() { + assertThat(PdlNavn(fornavn = "For", mellomnavn = "Mellom", etternavn = "Etter").fulltNavn()) + .isEqualTo("For Mellom Etter") + assertThat(PdlNavn(fornavn = "For", etternavn = "Etter").fulltNavn()) + .isEqualTo("For Etter") + } + + private fun getFile(name: String): String { + return javaClass.classLoader?.getResource(name)?.file ?: error("Testkonfigurasjon feil") + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/sanity/SanityServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/sanity/SanityServiceTest.kt new file mode 100644 index 000000000..891767464 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/sanity/SanityServiceTest.kt @@ -0,0 +1,82 @@ +package no.nav.familie.ba.sak.integrasjoner.sanity + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +class SanityServiceTest { + + @MockK + private lateinit var sanityKlient: SanityKlient + + @MockK + private lateinit var featureToggleService: FeatureToggleService + + @InjectMockKs + private lateinit var sanityService: SanityService + + @Test + fun `hentSanityEØSBegrunnelser - skal filtrere bort nye begrunnelser tilknyttet EØS praksisendring dersom toggel er av`() { + every { featureToggleService.isEnabled(FeatureToggleConfig.EØS_PRAKSISENDRING_SEPTEMBER2023) } returns false + + every { sanityKlient.hentEØSBegrunnelser() } returns EØSStandardbegrunnelse.values().map { + SanityEØSBegrunnelse( + apiNavn = it.sanityApiNavn, + navnISystem = it.name, + fagsakType = null, + periodeType = null, + tema = null, + vilkår = emptySet(), + annenForeldersAktivitet = emptyList(), + barnetsBostedsland = emptyList(), + kompetanseResultat = emptyList(), + hjemler = emptyList(), + hjemlerFolketrygdloven = emptyList(), + hjemlerEØSForordningen883 = emptyList(), + hjemlerEØSForordningen987 = emptyList(), + hjemlerSeperasjonsavtalenStorbritannina = emptyList(), + ) + } + + val eøsBegrunnelser = sanityService.hentSanityEØSBegrunnelser() + + assertThat(eøsBegrunnelser.keys).doesNotContainAnyElementsOf(EØSStandardbegrunnelse.eøsPraksisendringBegrunnelser()) + } + + @Test + fun `hentSanityEØSBegrunnelser - skal ikke filtrere bort nye begrunnelser tilknyttet EØS praksisendring dersom toggel er på`() { + every { featureToggleService.isEnabled(FeatureToggleConfig.EØS_PRAKSISENDRING_SEPTEMBER2023) } returns true + + every { sanityKlient.hentEØSBegrunnelser() } returns EØSStandardbegrunnelse.values().map { + SanityEØSBegrunnelse( + apiNavn = it.sanityApiNavn, + navnISystem = it.name, + fagsakType = null, + periodeType = null, + tema = null, + vilkår = emptySet(), + annenForeldersAktivitet = emptyList(), + barnetsBostedsland = emptyList(), + kompetanseResultat = emptyList(), + hjemler = emptyList(), + hjemlerFolketrygdloven = emptyList(), + hjemlerEØSForordningen883 = emptyList(), + hjemlerEØSForordningen987 = emptyList(), + hjemlerSeperasjonsavtalenStorbritannina = emptyList(), + ) + } + + val eøsBegrunnelser = sanityService.hentSanityEØSBegrunnelser() + + assertThat(eøsBegrunnelser.keys).isEqualTo(EØSStandardbegrunnelse.values().toSet()) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/HentStatusTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/HentStatusTest.kt" new file mode 100644 index 000000000..c58959111 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/HentStatusTest.kt" @@ -0,0 +1,179 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.årMnd +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.simulering.KontrollerNyUtbetalingsgeneratorService +import no.nav.familie.ba.sak.kjerne.steg.StatusFraOppdrag +import no.nav.familie.ba.sak.kjerne.steg.StatusFraOppdragMedTask +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.task.StatusFraOppdragTask +import no.nav.familie.ba.sak.task.dto.StatusFraOppdragDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppdrag.OppdragStatus +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsperiode +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.unleash.UnleashService +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.time.Month + +class HentStatusTest { + + private val økonomiKlient = mockk<ØkonomiKlient>() + + private val beregningService: BeregningService = mockk() + + private val kontrollerNyUtbetalingsgeneratorService: KontrollerNyUtbetalingsgeneratorService = mockk() + + lateinit var statusFraOppdrag: StatusFraOppdrag + + private val tilkjentYtelseRepository = mockk() + + private val utbetalingsoppdragGeneratorService: UtbetalingsoppdragGeneratorService = mockk() + + private val unleashService: UnleashService = mockk() + + @BeforeEach + fun setUp() { + val økonomiService = ØkonomiService( + + økonomiKlient = økonomiKlient, + beregningService = beregningService, + tilkjentYtelseValideringService = mockk(), + tilkjentYtelseRepository = tilkjentYtelseRepository, + kontrollerNyUtbetalingsgeneratorService = kontrollerNyUtbetalingsgeneratorService, + utbetalingsoppdragGeneratorService = utbetalingsoppdragGeneratorService, + unleashService = unleashService, + ) + statusFraOppdrag = StatusFraOppdrag( + økonomiService = økonomiService, + taskRepository = mockk().also { every { it.save(any()) } returns mockk() }, + ) + + every { unleashService.isEnabled(toggleId = any(), properties = any()) } returns false + } + + @Test + fun `henter status fra økonomi for behandling der alle utbetalingene hører til denne behandlinga`() { + val tilfeldigPerson = tilfeldigPerson() + val nyBehandling = lagBehandling() + lagTilkjentYtelse(nyBehandling, listOf(lagUtbetalingsperiode(nyBehandling))) + + every { + økonomiKlient.hentStatus( + match { it.behandlingsId == nyBehandling.id.toString() }, + ) + } returns OppdragStatus.KVITTERT_OK + val andelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-03"), + YtelseType.ORDINÆR_BARNETRYGD, + 10, + behandling = nyBehandling, + person = tilfeldigPerson, + aktør = mockk(), + tilkjentYtelse = mockk(), + kildeBehandlingId = null, + ), + ) + + every { beregningService.hentAndelerTilkjentYtelseMedUtbetalingerForBehandling(any()) } returns andelerTilkjentYtelse + + val nesteSteg = + statusFraOppdrag.utførStegOgAngiNeste(nyBehandling, statusFraOppdragMedTask(tilfeldigPerson, nyBehandling)) + assertThat(nesteSteg).isEqualTo(StegType.IVERKSETT_MOT_FAMILIE_TILBAKE) + verify { økonomiKlient.hentStatus(match { it.behandlingsId == nyBehandling.id.toString() }) } + } + + @Test + fun `kan håndtere nullutbetaling uten tidligere historikk`() { + val tilfeldigPerson = tilfeldigPerson() + val nyBehandling = lagBehandling() + lagTilkjentYtelse(nyBehandling, listOf()) + + every { + økonomiKlient.hentStatus( + match { it.behandlingsId == nyBehandling.id.toString() }, + ) + } returns OppdragStatus.KVITTERT_OK + val andelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-03"), + YtelseType.ORDINÆR_BARNETRYGD, + 0, + behandling = nyBehandling, + person = tilfeldigPerson, + aktør = mockk(), + tilkjentYtelse = mockk(), + kildeBehandlingId = null, + ), + ) + + every { beregningService.hentAndelerTilkjentYtelseMedUtbetalingerForBehandling(any()) } returns andelerTilkjentYtelse + + val nesteSteg = + statusFraOppdrag.utførStegOgAngiNeste(nyBehandling, statusFraOppdragMedTask(tilfeldigPerson, nyBehandling)) + assertThat(nesteSteg).isEqualTo(StegType.IVERKSETT_MOT_FAMILIE_TILBAKE) + verify(exactly = 0) { økonomiKlient.hentStatus(any()) } + } + + private fun lagTilkjentYtelse(behandling: Behandling, utbetalingsperiode: List) { + val nyTilkjentYtelse = lagInitiellTilkjentYtelse( + behandling = behandling, + utbetalingsoppdrag = objectMapper.writeValueAsString(lagUtbetalingsoppdrag(utbetalingsperiode = utbetalingsperiode)), + ) + every { tilkjentYtelseRepository.findByBehandling(behandling.id) } returns nyTilkjentYtelse + } + + private fun statusFraOppdragMedTask( + tilfeldigPerson: Person, + nyBehandling: Behandling, + ) = StatusFraOppdragMedTask( + statusFraOppdragDTO = StatusFraOppdragDTO( + fagsystem = "BA", + personIdent = tilfeldigPerson.aktør.aktivFødselsnummer(), + aktørId = "Søker1", + behandlingsId = nyBehandling.id, + vedtaksId = 0L, + ), + task = Task( + type = StatusFraOppdragTask.TASK_STEP_TYPE, + payload = "", + ), + ) + + private fun lagUtbetalingsperiode(nyBehandling: Behandling) = + Utbetalingsperiode( + vedtakdatoFom = LocalDate.of( + 2019, + Month.APRIL, + 1, + ), + vedtakdatoTom = LocalDate.of(2020, Month.MARCH, 31), + erEndringPåEksisterendePeriode = false, + periodeId = 1L, + behandlingId = nyBehandling.id, + datoForVedtak = LocalDate.of(2020, Month.APRIL, 1), + klassifisering = "", + sats = BigDecimal.ONE, + satsType = Utbetalingsperiode.SatsType.MND, + utbetalesTil = "", + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingTest.kt" new file mode 100644 index 000000000..4598a3779 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingTest.kt" @@ -0,0 +1,528 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import io.mockk.CapturingSlot +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.task.KonsistensavstemMotOppdragAvsluttTask +import no.nav.familie.ba.sak.task.KonsistensavstemMotOppdragDataTask +import no.nav.familie.ba.sak.task.KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask +import no.nav.familie.ba.sak.task.KonsistensavstemMotOppdragStartTask +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingAvsluttTaskDTO +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingDataTaskDTO +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingStartTaskDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.math.BigInteger +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.YearMonth +import java.util.UUID + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class KonsistensavstemmingTest { + private val økonomiKlient = mockk<ØkonomiKlient>() + private val behandlingHentOgPersisterService = mockk() + private val beregningService = mockk() + private val taskService = mockk(relaxed = true) + private val batchRepository = mockk() + private val dataChunkRepository = mockk(relaxed = true) + + private val avstemmingService = AvstemmingService( + behandlingHentOgPersisterService, + økonomiKlient, + beregningService, + taskService, + batchRepository, + dataChunkRepository, + ) + + private val batchId = 1000000L + private val behandlingId = BigInteger.ONE + private val avstemmingsdato = LocalDateTime.now() + + private lateinit var konistensavstemmingStartTask: KonsistensavstemMotOppdragStartTask + private lateinit var konsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask: KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask + private lateinit var konsistensavstemMotOppdragDataTask: KonsistensavstemMotOppdragDataTask + private lateinit var konsistensavstemMotOppdragAvsluttTask: KonsistensavstemMotOppdragAvsluttTask + + @BeforeEach + fun setUp() { + every { taskService.save(any()) } returns Task(type = "dummy", payload = "") + konistensavstemmingStartTask = KonsistensavstemMotOppdragStartTask(avstemmingService) + konsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask = + KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask(avstemmingService, taskService) + konsistensavstemMotOppdragDataTask = KonsistensavstemMotOppdragDataTask(avstemmingService) + konsistensavstemMotOppdragAvsluttTask = + KonsistensavstemMotOppdragAvsluttTask(avstemmingService, dataChunkRepository, BatchService(batchRepository)) + } + + @Test + fun `Første gangs kjøring av start task - Verifiser at konsistensavstemOppdragStart oppretter finn perioder for relevante behandlinger task- og avslutt task og sender start melding hvis transaksjon ikke allerede kjørt`() { + val transaksjonsId = UUID.randomUUID() + val avstemmingsdatoSlot = lagMockForStartTaskHappCase(transaksjonsId) + konistensavstemmingStartTask.doTask( + Task( + payload = objectMapper.writeValueAsString( + KonsistensavstemmingStartTaskDTO( + batchId, + avstemmingsdato, + transaksjonsId, + ), + ), + type = KonsistensavstemMotOppdragStartTask.TASK_STEP_TYPE, + ), + ) + + val taskSlots = mutableListOf() + verify(atLeast = 1) { taskService.save(capture(taskSlots)) } + // sjekk at KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask er opprettet + val finnPerioderForRelevanteBehandlingerTask = + finnFørsteTaskAvTypePåTransaksjonsId( + taskSlots, + transaksjonsId, + KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.TASK_STEP_TYPE, + ) + assertThat(finnPerioderForRelevanteBehandlingerTask).isNotNull + val finnPerioderForRelevanteBehandlingerDto = + objectMapper.readValue( + finnPerioderForRelevanteBehandlingerTask!!.payload, + KonsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO::class.java, + ) + assertEquals(batchId, finnPerioderForRelevanteBehandlingerDto.batchId) + assertEquals(transaksjonsId, finnPerioderForRelevanteBehandlingerDto.transaksjonsId) + assertEquals(1, finnPerioderForRelevanteBehandlingerDto.chunkNr) + assertThat(finnPerioderForRelevanteBehandlingerDto.relevanteBehandlinger).hasSize(1).containsExactly(1) + + // sjekk at KonsistensavstemMotOppdragAvsluttTask er opprettet + val finnAvsluttTask = finnFørsteTaskAvTypePåTransaksjonsId( + taskSlots, + transaksjonsId, + KonsistensavstemMotOppdragAvsluttTask.TASK_STEP_TYPE, + ) + val finnAvsluttTaskDto = + objectMapper.readValue( + finnAvsluttTask!!.payload, + KonsistensavstemmingAvsluttTaskDTO::class.java, + ) + assertThat(finnAvsluttTask).isNotNull + assertEquals(batchId, finnAvsluttTaskDto.batchId) + assertEquals(transaksjonsId, finnAvsluttTaskDto.transaksjonsId) + + // sjekk at datachunk er opprettet + val dataChunkSlot = mutableListOf() + verify(atLeast = 1) { dataChunkRepository.save(capture(dataChunkSlot)) } + assertThat(dataChunkSlot.filter { it.transaksjonsId == transaksjonsId }).hasSize(1) + assertEquals(1, dataChunkSlot.find { it.transaksjonsId == transaksjonsId }?.chunkNr) + + // sjekk at det har blitt sendt startmelding + verify(exactly = 1) { + økonomiKlient.konsistensavstemOppdragStart( + avstemmingsdato = avstemmingsdatoSlot.captured, + transaksjonsId = transaksjonsId, + ) + } + } + + private fun finnFørsteTaskAvTypePåTransaksjonsId( + tasker: List, + transaksjonsId: UUID, + type: String, + ): Task? { + return tasker.find { it.payload.contains(transaksjonsId.toString()) && it.type == type } + } + + @Test + fun `Rekjøring av start task - Verifiser at konsistensavstemming ikke kjører hvis alle datachunker allerede er sendt til økonomi for transaksjonId`() { + every { batchRepository.getReferenceById(batchId) } returns Batch( + kjøreDato = LocalDate.now(), + status = KjøreStatus.FERDIG, + ) + val transaksjonsId = UUID.randomUUID() + + konistensavstemmingStartTask.doTask( + Task( + payload = objectMapper.writeValueAsString( + KonsistensavstemmingStartTaskDTO( + batchId, + avstemmingsdato, + transaksjonsId, + ), + ), + type = KonsistensavstemMotOppdragStartTask.TASK_STEP_TYPE, + ), + ) + + verify(exactly = 0) { avstemmingService.hentSisteIverksatteBehandlingerFraLøpendeFagsaker() } + } + + @Test + fun `Rekjøring av start task - Verifiser at konsistensavstemming kun rekjører chunker som ikke allerede er kjørt`() { + val transaksjonsId = UUID.randomUUID() + lagMockForStartTaskHappCase(transaksjonsId) + val datachunks = listOf( + DataChunk( + batch = Batch(kjøreDato = LocalDate.now()), + transaksjonsId = transaksjonsId, + erSendt = true, + chunkNr = 1, + ), + DataChunk( + batch = Batch(kjøreDato = LocalDate.now()), + transaksjonsId = transaksjonsId, + erSendt = false, + chunkNr = 2, + ), + ) + every { dataChunkRepository.findByTransaksjonsIdAndChunkNr(transaksjonsId, 1) } returns datachunks[0] + every { dataChunkRepository.findByTransaksjonsIdAndChunkNr(transaksjonsId, 2) } returns datachunks[1] + every { dataChunkRepository.findByTransaksjonsIdAndChunkNr(transaksjonsId, 3) } returns null + + every { behandlingHentOgPersisterService.hentSisteIverksatteBehandlingerFraLøpendeFagsaker() } returns (1..1450).toList() + .map { it.toLong() } + + konistensavstemmingStartTask.doTask( + Task( + payload = objectMapper.writeValueAsString( + KonsistensavstemmingStartTaskDTO( + batchId, + avstemmingsdato, + transaksjonsId, + ), + ), + type = KonsistensavstemMotOppdragStartTask.TASK_STEP_TYPE, + ), + ) + + verify(exactly = 1) { avstemmingService.hentSisteIverksatteBehandlingerFraLøpendeFagsaker() } + val taskSlots = mutableListOf() + verify(exactly = 2) { taskService.save(capture(taskSlots)) } + + assertEquals( + KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.TASK_STEP_TYPE, + taskSlots[0].type, + ) + val finnPerioderForRelevanteBehandlingerDto = + objectMapper.readValue( + taskSlots[0].payload, + KonsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO::class.java, + ) + assertEquals(batchId, finnPerioderForRelevanteBehandlingerDto.batchId) + assertEquals(transaksjonsId, finnPerioderForRelevanteBehandlingerDto.transaksjonsId) + assertEquals(3, finnPerioderForRelevanteBehandlingerDto.chunkNr) + assertThat(finnPerioderForRelevanteBehandlingerDto.relevanteBehandlinger).hasSize(450) + + assertEquals(KonsistensavstemMotOppdragAvsluttTask.TASK_STEP_TYPE, taskSlots[1].type) + } + + @Test + fun `Verifiser at konsistensavstemPeriodeFinnPerioderForRelevanteBehandlingerTask finner perioder for behandlinger og oppretter data task`() { + val transaksjonsId = UUID.randomUUID() + lagMockFinnPerioderForRelevanteBehandlingerHappeCase() + + konsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.doTask( + Task( + payload = objectMapper.writeValueAsString( + KonsistensavstemmingFinnPerioderForRelevanteBehandlingerDTO( + batchId, + transaksjonsId, + avstemmingsdato, + 1, + listOf(behandlingId.toLong()), + ), + ), + type = KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.TASK_STEP_TYPE, + ), + ) + + val taskSlots = mutableListOf() + verify(atLeast = 1) { taskService.save(capture(taskSlots)) } + val konsistensavstemmingDataDto = + objectMapper.readValue(taskSlots.last().payload, KonsistensavstemmingDataTaskDTO::class.java) + assertEquals(konsistensavstemmingDataDto.chunkNr, 1) + assertEquals(konsistensavstemmingDataDto.transaksjonsId, transaksjonsId) + assertThat(konsistensavstemmingDataDto.perioderForBehandling) + .hasSize(1).extracting("behandlingId").containsExactly(behandlingId.toString()) + } + + @Test + fun `Verifiser at konsistensavstemOppdragData sender data og oppdatere datachunk tabellen`() { + val transaksjonsId = UUID.randomUUID() + lagMockOppdragDataHappeCase(transaksjonsId) + every { dataChunkRepository.findByTransaksjonsIdAndChunkNr(transaksjonsId, 1) } returns + DataChunk( + batch = Batch(id = batchId, kjøreDato = LocalDate.now()), + transaksjonsId = transaksjonsId, + chunkNr = 1, + ) + every { + økonomiKlient.konsistensavstemOppdragData( + avstemmingsdato, + emptyList(), + transaksjonsId, + ) + } returns "" + + konsistensavstemMotOppdragDataTask.doTask( + Task( + payload = objectMapper.writeValueAsString( + KonsistensavstemmingDataTaskDTO( + transaksjonsId = transaksjonsId, + chunkNr = 1, + avstemmingdato = avstemmingsdato, + perioderForBehandling = emptyList(), + sendTilØkonomi = true, + ), + ), + type = KonsistensavstemMotOppdragDataTask.TASK_STEP_TYPE, + ), + + ) + + val dataChunkSlot = slot() + verify(exactly = 1) { dataChunkRepository.save(capture(dataChunkSlot)) } + assertThat(dataChunkSlot.captured.erSendt).isTrue() + + verify(exactly = 1) { + økonomiKlient.konsistensavstemOppdragData( + avstemmingsdato = avstemmingsdato, + perioderTilAvstemming = emptyList(), + transaksjonsId = transaksjonsId, + ) + } + + assertEquals(1, dataChunkSlot.captured.chunkNr) + assertEquals(transaksjonsId, dataChunkSlot.captured.transaksjonsId) + assertEquals(true, dataChunkSlot.captured.erSendt) + } + + @Test + fun `Kjør alle tasker med input generert fra task som oppretter tasken`() { + val transaksjonsId = UUID.randomUUID() + lagMockForStartTaskHappCase(transaksjonsId) + konistensavstemmingStartTask.doTask( + Task( + payload = objectMapper.writeValueAsString( + KonsistensavstemmingStartTaskDTO( + batchId, + avstemmingsdato, + transaksjonsId, + ), + ), + type = KonsistensavstemMotOppdragStartTask.TASK_STEP_TYPE, + ), + ) + val taskSlots = mutableListOf() + verify(atLeast = 2) { taskService.save(capture(taskSlots)) } + val finnPerioderTask = finnFørsteTaskAvTypePåTransaksjonsId( + taskSlots, + transaksjonsId, + KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.TASK_STEP_TYPE, + )!! + + lagMockFinnPerioderForRelevanteBehandlingerHappeCase() + konsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.doTask( + Task( + payload = finnPerioderTask.payload, + type = KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.TASK_STEP_TYPE, + ), + ) + verify(atLeast = 3) { taskService.save(capture(taskSlots)) } + + lagMockOppdragDataHappeCase(transaksjonsId) + val finnDataTask = finnFørsteTaskAvTypePåTransaksjonsId( + taskSlots, + transaksjonsId, + KonsistensavstemMotOppdragDataTask.TASK_STEP_TYPE, + )!! + konsistensavstemMotOppdragDataTask.doTask( + Task( + payload = finnDataTask.payload, + type = KonsistensavstemMotOppdragDataTask.TASK_STEP_TYPE, + ), + ) + verify(atLeast = 3) { taskService.save(capture(taskSlots)) } + val dataTask = finnFørsteTaskAvTypePåTransaksjonsId( + taskSlots, + transaksjonsId, + KonsistensavstemMotOppdragDataTask.TASK_STEP_TYPE, + )!! + val datachunksSlot = mutableListOf() + verify(atLeast = 2) { dataChunkRepository.save(capture(datachunksSlot)) } + assertThat(datachunksSlot.last { it.transaksjonsId == transaksjonsId }.erSendt).isTrue() + + val dataTaskDto = objectMapper.readValue(dataTask.payload, KonsistensavstemmingDataTaskDTO::class.java) + assertThat(dataTaskDto.chunkNr).isEqualTo(1) + assertThat(dataTaskDto.transaksjonsId).isEqualTo(transaksjonsId) + assertThat(dataTaskDto.perioderForBehandling).hasSize(1) + assertThat(dataTaskDto.sendTilØkonomi).isTrue() + + lagMockAvsluttHappyCase(transaksjonsId) + val avsluttTask = finnFørsteTaskAvTypePåTransaksjonsId( + taskSlots, + transaksjonsId, + KonsistensavstemMotOppdragAvsluttTask.TASK_STEP_TYPE, + )!! + konsistensavstemMotOppdragAvsluttTask.doTask( + Task( + payload = avsluttTask.payload, + type = KonsistensavstemMotOppdragAvsluttTask.TASK_STEP_TYPE, + ), + ) + + verify(exactly = 1) { økonomiKlient.konsistensavstemOppdragStart(any(), transaksjonsId) } + verify(exactly = 1) { økonomiKlient.konsistensavstemOppdragData(any(), any(), transaksjonsId) } + verify(exactly = 1) { økonomiKlient.konsistensavstemOppdragAvslutt(any(), transaksjonsId) } + } + + @Test + fun `Kjør alle tasker med input generert fra task som oppretter tasken og send til økonomi skrudd av`() { + val transaksjonsId = UUID.randomUUID() + lagMockForStartTaskHappCase(transaksjonsId) + konistensavstemmingStartTask.doTask( + Task( + payload = objectMapper.writeValueAsString( + KonsistensavstemmingStartTaskDTO( + batchId, + avstemmingsdato, + transaksjonsId, + false, + ), + ), + type = KonsistensavstemMotOppdragStartTask.TASK_STEP_TYPE, + ), + ) + val taskSlots = mutableListOf() + verify(atLeast = 2) { taskService.save(capture(taskSlots)) } + + lagMockFinnPerioderForRelevanteBehandlingerHappeCase() + val finnPerioderForRelevanteBehandlingerTask = finnFørsteTaskAvTypePåTransaksjonsId( + taskSlots, + transaksjonsId, + KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.TASK_STEP_TYPE, + )!! + konsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.doTask( + Task( + payload = finnPerioderForRelevanteBehandlingerTask.payload, + type = KonsistensavstemMotOppdragFinnPerioderForRelevanteBehandlingerTask.TASK_STEP_TYPE, + ), + ) + verify(atLeast = 3) { taskService.save(capture(taskSlots)) } + + lagMockOppdragDataHappeCase(transaksjonsId) + konsistensavstemMotOppdragDataTask.doTask( + Task( + payload = finnFørsteTaskAvTypePåTransaksjonsId( + taskSlots, + transaksjonsId, + KonsistensavstemMotOppdragDataTask.TASK_STEP_TYPE, + )!!.payload, + type = KonsistensavstemMotOppdragDataTask.TASK_STEP_TYPE, + ), + ) + verify(atLeast = 3) { taskService.save(capture(taskSlots)) } + + lagMockAvsluttHappyCase(transaksjonsId) + val avsluttTask = finnFørsteTaskAvTypePåTransaksjonsId( + taskSlots, + transaksjonsId, + KonsistensavstemMotOppdragAvsluttTask.TASK_STEP_TYPE, + )!! + konsistensavstemMotOppdragAvsluttTask.doTask( + Task( + payload = avsluttTask.payload, + type = KonsistensavstemMotOppdragAvsluttTask.TASK_STEP_TYPE, + ), + ) + + verify(exactly = 0) { økonomiKlient.konsistensavstemOppdragStart(any(), transaksjonsId) } + verify(exactly = 0) { økonomiKlient.konsistensavstemOppdragData(any(), any(), transaksjonsId) } + verify(exactly = 0) { økonomiKlient.konsistensavstemOppdragAvslutt(any(), transaksjonsId) } + } + + private fun lagMockForStartTaskHappCase(transaksjonsId: UUID): CapturingSlot { + val behandlingId = 1L + every { behandlingHentOgPersisterService.hentSisteIverksatteBehandlingerFraLøpendeFagsaker() } returns listOf( + behandlingId, + ) + + every { batchRepository.getReferenceById(batchId) } returns Batch(id = batchId, kjøreDato = LocalDate.now()) + every { dataChunkRepository.save(any()) } returns DataChunk( + batch = Batch(kjøreDato = LocalDate.now()), + chunkNr = 1, + transaksjonsId = transaksjonsId, + ) + + val avstemmingsdatoSlot = slot() + every { + økonomiKlient.konsistensavstemOppdragStart( + capture(avstemmingsdatoSlot), + transaksjonsId, + ) + } returns "" + + every { dataChunkRepository.findByTransaksjonsIdAndChunkNr(transaksjonsId, any()) } returns null + return avstemmingsdatoSlot + } + + private fun lagMockFinnPerioderForRelevanteBehandlingerHappeCase() { + every { + beregningService.hentLøpendeAndelerTilkjentYtelseMedUtbetalingerForBehandlinger( + any(), + any(), + ) + } returns listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.now().minusMonths(4), + tom = YearMonth.now(), + periodeIdOffset = 0, + ).also { it.kildeBehandlingId = behandlingId.toLong() }, + ) + val aktivFødselsnummere = mapOf(behandlingId.toLong() to "test") + every { behandlingHentOgPersisterService.hentAktivtFødselsnummerForBehandlinger(any()) } returns aktivFødselsnummere + every { behandlingHentOgPersisterService.hentTssEksternIdForBehandlinger(any()) } returns emptyMap() + } + + private fun lagMockOppdragDataHappeCase(transaksjonsId: UUID) { + every { + økonomiKlient.konsistensavstemOppdragData( + any(), + any(), + transaksjonsId, + ) + } returns "" + + every { dataChunkRepository.findByTransaksjonsIdAndChunkNr(transaksjonsId, 1) } returns DataChunk( + batch = Batch(kjøreDato = LocalDate.now()), + chunkNr = 1, + transaksjonsId = transaksjonsId, + ) + every { dataChunkRepository.save(any()) } returns DataChunk( + batch = Batch(kjøreDato = LocalDate.now()), + chunkNr = 1, + transaksjonsId = transaksjonsId, + ) + } + + private fun lagMockAvsluttHappyCase(transaksjonsId: UUID) { + every { + økonomiKlient.konsistensavstemOppdragAvslutt( + any(), + transaksjonsId, + ) + } returns "" + + every { batchRepository.saveAndFlush(any()) } returns Batch(kjøreDato = LocalDate.now()) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragGeneratorServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragGeneratorServiceTest.kt" new file mode 100644 index 000000000..dcf9ed0c0 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragGeneratorServiceTest.kt" @@ -0,0 +1,768 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import io.mockk.CapturingSlot +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.felles.utbetalingsgenerator.domain.BeregnetUtbetalingsoppdragLongId +import no.nav.familie.unleash.UnleashService +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.time.LocalDate +import java.time.YearMonth + +@ExtendWith(MockKExtension::class) +class UtbetalingsoppdragGeneratorServiceTest { + + @MockK + private lateinit var behandlingHentOgPersisterService: BehandlingHentOgPersisterService + + @MockK + private lateinit var behandlingService: BehandlingService + + @MockK + private lateinit var tilkjentYtelseRepository: TilkjentYtelseRepository + + @MockK + private lateinit var andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository + + @MockK + private lateinit var beregningService: BeregningService + + @MockK + private lateinit var unleashService: UnleashService + + @InjectMockKs + private lateinit var utbetalingsoppdragGenerator: UtbetalingsoppdragGenerator + + @InjectMockKs + private lateinit var utbetalingsoppdragGeneratorService: UtbetalingsoppdragGeneratorService + + @Test + fun `genererUtbetalingsoppdrag - skal generere nytt utbetalingsoppdrag og oppdatere andeler med offset når det ikke finnes en forrige behandling`() { + val vedtak = lagVedtak() + val tilkjentYtelse = lagInitiellTilkjentYtelse(vedtak.behandling) + val person = tilfeldigPerson() + tilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + id = 1, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 3), + beløp = 250, + person = person, + ), + lagAndelTilkjentYtelse( + id = 2, + fom = YearMonth.of(2023, 4), + tom = YearMonth.of(2023, 5), + beløp = 350, + person = person, + ), + lagAndelTilkjentYtelse( + id = 3, + fom = YearMonth.of(2023, 6), + tom = YearMonth.of(2023, 8), + beløp = 250, + person = person, + ), + ), + ) + val tilkjentYtelseSlot = slot() + setUpMocks( + behandling = vedtak.behandling, + tilkjentYtelse = tilkjentYtelse, + tilkjentYtelseSlot = tilkjentYtelseSlot, + ) + + val beregnetUtbetalingsoppdrag = + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = vedtak, + "abc123", + ) + + val lagredeAndeler = tilkjentYtelseSlot.captured.andelerTilkjentYtelse + + verify(exactly = 1) { tilkjentYtelseRepository.save(any()) } + + validerBeregnetUtbetalingsoppdragOgAndeler( + beregnetUtbetalingsoppdrag = beregnetUtbetalingsoppdrag, + andelerTilkjentYtelse = lagredeAndeler, + forventetAntallAndeler = 3, + forventetAntallUtbetalingsperioder = 3, + forventedeOffsets = listOf( + Pair(0L, null), + Pair(1L, 0L), + Pair(2L, 1L), + ), + ) + } + + @Test + fun `genererUtbetalingsoppdrag - skal generere nytt utbetalingsoppdrag og oppdatere andeler med offset når det finnes en forrige behandling`() { + val vedtak = lagVedtak() + val tilkjentYtelse = lagInitiellTilkjentYtelse(vedtak.behandling) + val person = tilfeldigPerson() + tilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + id = 4, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 8), + beløp = 300, + person = person, + ), + ), + ) + + val forrigeBehandling = lagBehandling() + val forrigeTilkjentYtelse = lagInitiellTilkjentYtelse(forrigeBehandling) + forrigeTilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + id = 1, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 3), + beløp = 250, + person = person, + periodeIdOffset = 0, + forrigeperiodeIdOffset = null, + ), + lagAndelTilkjentYtelse( + id = 2, + fom = YearMonth.of(2023, 4), + tom = YearMonth.of(2023, 5), + beløp = 350, + person = person, + periodeIdOffset = 1, + forrigeperiodeIdOffset = 0, + ), + lagAndelTilkjentYtelse( + id = 3, + fom = YearMonth.of(2023, 6), + tom = YearMonth.of(2023, 8), + beløp = 250, + person = person, + periodeIdOffset = 2, + forrigeperiodeIdOffset = 1, + ), + ), + ) + + val tilkjentYtelseSlot = slot() + setUpMocks( + behandling = vedtak.behandling, + tilkjentYtelse = tilkjentYtelse, + forrigeTilkjentYtelse = forrigeTilkjentYtelse, + tilkjentYtelseSlot = tilkjentYtelseSlot, + ) + + val beregnetUtbetalingsoppdrag = + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = vedtak, + "abc123", + ) + + val lagredeAndeler = tilkjentYtelseSlot.captured.andelerTilkjentYtelse + + verify(exactly = 1) { tilkjentYtelseRepository.save(any()) } + + validerBeregnetUtbetalingsoppdragOgAndeler( + beregnetUtbetalingsoppdrag = beregnetUtbetalingsoppdrag, + andelerTilkjentYtelse = lagredeAndeler, + forventetAntallAndeler = 1, + forventetAntallUtbetalingsperioder = 1, + forventedeOffsets = listOf( + Pair(3L, 2L), + ), + ) + } + + @Test + fun `genererUtbetalingsoppdrag - skal generere nytt utbetalingsoppdrag og oppdatere andeler med offset for 2 personer`() { + val vedtak = lagVedtak() + val tilkjentYtelse = lagInitiellTilkjentYtelse(vedtak.behandling) + val person = tilfeldigPerson() + val barn = tilfeldigPerson() + tilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + id = 1, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 3), + beløp = 250, + person = person, + ), + lagAndelTilkjentYtelse( + id = 2, + fom = YearMonth.of(2023, 4), + tom = YearMonth.of(2023, 5), + beløp = 350, + person = person, + ), + lagAndelTilkjentYtelse( + id = 3, + fom = YearMonth.of(2023, 6), + tom = YearMonth.of(2023, 8), + beløp = 250, + person = person, + ), + lagAndelTilkjentYtelse( + id = 4, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 3), + beløp = 250, + person = barn, + ), + lagAndelTilkjentYtelse( + id = 5, + fom = YearMonth.of(2023, 4), + tom = YearMonth.of(2023, 8), + beløp = 350, + person = barn, + ), + ), + ) + val tilkjentYtelseSlot = slot() + setUpMocks( + behandling = vedtak.behandling, + tilkjentYtelse = tilkjentYtelse, + tilkjentYtelseSlot = tilkjentYtelseSlot, + ) + + val beregnetUtbetalingsoppdrag = + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = vedtak, + "abc123", + ) + + val lagredeAndeler = tilkjentYtelseSlot.captured.andelerTilkjentYtelse + + verify(exactly = 1) { tilkjentYtelseRepository.save(any()) } + + validerBeregnetUtbetalingsoppdragOgAndeler( + beregnetUtbetalingsoppdrag = beregnetUtbetalingsoppdrag, + andelerTilkjentYtelse = lagredeAndeler, + forventetAntallAndeler = 5, + forventetAntallUtbetalingsperioder = 5, + forventedeOffsets = listOf( + Pair(0L, null), + Pair(1L, 0L), + Pair(2L, 1L), + Pair(3L, null), + Pair(4L, 3L), + ), + ) + } + + @Test + fun `genererUtbetalingsoppdrag - skal generere nytt utbetalingsoppdrag og oppdatere andeler med offset for 2 personer og tidligere behandling`() { + val vedtak = lagVedtak() + val tilkjentYtelse = lagInitiellTilkjentYtelse(vedtak.behandling) + val person = tilfeldigPerson() + val barn = tilfeldigPerson() + tilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + id = 6, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 8), + beløp = 250, + person = person, + ), + lagAndelTilkjentYtelse( + id = 7, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 8), + beløp = 350, + person = barn, + ), + ), + ) + + val forrigeBehandling = lagBehandling() + val forrigeTilkjentYtelse = lagInitiellTilkjentYtelse(forrigeBehandling) + forrigeTilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + id = 1, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 3), + beløp = 250, + person = person, + periodeIdOffset = 0L, + ), + lagAndelTilkjentYtelse( + id = 2, + fom = YearMonth.of(2023, 4), + tom = YearMonth.of(2023, 5), + beløp = 350, + person = person, + periodeIdOffset = 1L, + forrigeperiodeIdOffset = 0L, + ), + lagAndelTilkjentYtelse( + id = 3, + fom = YearMonth.of(2023, 6), + tom = YearMonth.of(2023, 8), + beløp = 250, + person = person, + periodeIdOffset = 2L, + forrigeperiodeIdOffset = 1L, + ), + lagAndelTilkjentYtelse( + id = 4, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 3), + beløp = 250, + person = barn, + periodeIdOffset = 3L, + ), + lagAndelTilkjentYtelse( + id = 5, + fom = YearMonth.of(2023, 4), + tom = YearMonth.of(2023, 8), + beløp = 350, + person = barn, + periodeIdOffset = 4L, + forrigeperiodeIdOffset = 3L, + ), + ), + ) + val tilkjentYtelseSlot = slot() + setUpMocks( + behandling = vedtak.behandling, + tilkjentYtelse = tilkjentYtelse, + tilkjentYtelseSlot = tilkjentYtelseSlot, + forrigeTilkjentYtelse = forrigeTilkjentYtelse, + ) + + val beregnetUtbetalingsoppdrag = + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = vedtak, + "abc123", + ) + + val lagredeAndeler = tilkjentYtelseSlot.captured.andelerTilkjentYtelse + + verify(exactly = 1) { tilkjentYtelseRepository.save(any()) } + + validerBeregnetUtbetalingsoppdragOgAndeler( + beregnetUtbetalingsoppdrag = beregnetUtbetalingsoppdrag, + andelerTilkjentYtelse = lagredeAndeler, + forventetAntallAndeler = 2, + forventetAntallUtbetalingsperioder = 2, + forventedeOffsets = listOf( + Pair(5L, 2L), + Pair(6L, 4L), + ), + ) + } + + @Test + fun `genererUtbetalingsoppdrag - skal generere nytt utbetalingsoppdrag med endret migreringsdato for en eksisterende kjede og en ny kjede`() { + val vedtak = lagVedtak(behandling = lagBehandling(behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD)) + val tilkjentYtelse = lagInitiellTilkjentYtelse(vedtak.behandling) + val person = tilfeldigPerson() + val barn = tilfeldigPerson() + tilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + id = 6, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 8), + beløp = 250, + person = person, + ), + lagAndelTilkjentYtelse( + id = 7, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 8), + beløp = 350, + person = barn, + ), + ), + ) + + val forrigeBehandling = lagBehandling() + val forrigeTilkjentYtelse = lagInitiellTilkjentYtelse(forrigeBehandling) + forrigeTilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + id = 1, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 3), + beløp = 250, + person = person, + periodeIdOffset = 0L, + ), + lagAndelTilkjentYtelse( + id = 2, + fom = YearMonth.of(2023, 4), + tom = YearMonth.of(2023, 5), + beløp = 350, + person = person, + periodeIdOffset = 1L, + forrigeperiodeIdOffset = 0L, + ), + lagAndelTilkjentYtelse( + id = 3, + fom = YearMonth.of(2023, 6), + tom = YearMonth.of(2023, 8), + beløp = 250, + person = person, + periodeIdOffset = 2L, + forrigeperiodeIdOffset = 1L, + ), + ), + ) + val tilkjentYtelseSlot = slot() + setUpMocks( + behandling = vedtak.behandling, + tilkjentYtelse = tilkjentYtelse, + tilkjentYtelseSlot = tilkjentYtelseSlot, + forrigeTilkjentYtelse = forrigeTilkjentYtelse, + migreringsdato = YearMonth.of(2022, 11).førsteDagIInneværendeMåned(), + ) + + val beregnetUtbetalingsoppdrag = + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = vedtak, + "abc123", + ) + + val lagredeAndeler = tilkjentYtelseSlot.captured.andelerTilkjentYtelse + + verify(exactly = 1) { tilkjentYtelseRepository.save(any()) } + + validerBeregnetUtbetalingsoppdragOgAndeler( + beregnetUtbetalingsoppdrag = beregnetUtbetalingsoppdrag, + andelerTilkjentYtelse = lagredeAndeler, + forventetAntallAndeler = 2, + forventetAntallUtbetalingsperioder = 3, + forventedeOffsets = listOf( + Pair(3L, 2L), + Pair(4L, null), + ), + ) + } + + @Test + fun `genererUtbetalingsoppdrag - skal generere nytt utbetalingsoppdrag for simulering med en eksisterende kjede og en ny kjede`() { + val vedtak = lagVedtak() + val tilkjentYtelse = lagInitiellTilkjentYtelse(vedtak.behandling) + val person = tilfeldigPerson() + val barn = tilfeldigPerson() + tilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + id = 6, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 8), + beløp = 250, + person = person, + ), + lagAndelTilkjentYtelse( + id = 7, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 8), + beløp = 350, + person = barn, + ), + ), + ) + + val forrigeBehandling = lagBehandling() + val forrigeTilkjentYtelse = lagInitiellTilkjentYtelse(forrigeBehandling) + forrigeTilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + id = 1, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 3), + beløp = 250, + person = person, + periodeIdOffset = 0L, + ), + lagAndelTilkjentYtelse( + id = 2, + fom = YearMonth.of(2023, 4), + tom = YearMonth.of(2023, 5), + beløp = 350, + person = person, + periodeIdOffset = 1L, + forrigeperiodeIdOffset = 0L, + ), + lagAndelTilkjentYtelse( + id = 3, + fom = YearMonth.of(2023, 6), + tom = YearMonth.of(2023, 8), + beløp = 250, + person = person, + periodeIdOffset = 2L, + forrigeperiodeIdOffset = 1L, + ), + ), + ) + val tilkjentYtelseSlot = slot() + setUpMocks( + behandling = vedtak.behandling, + tilkjentYtelse = tilkjentYtelse, + tilkjentYtelseSlot = tilkjentYtelseSlot, + forrigeTilkjentYtelse = forrigeTilkjentYtelse, + ) + + val beregnetUtbetalingsoppdrag = + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = vedtak, + saksbehandlerId = "abc123", + erSimulering = true, + ) + + verify(exactly = 0) { tilkjentYtelseRepository.save(any()) } + + validerBeregnetUtbetalingsoppdragOgAndeler( + beregnetUtbetalingsoppdrag = beregnetUtbetalingsoppdrag, + andelerTilkjentYtelse = emptySet(), + forventetAntallAndeler = 2, + forventetAntallUtbetalingsperioder = 3, + forventedeOffsets = listOf( + Pair(3L, 2L), + Pair(4L, null), + ), + ) + } + + @Test + fun `genererUtbetalingsoppdrag - revurdering hvor 0-utbetaling går til betaling skal ikke opprette noe opphør ved simulering`() { + val vedtak = lagVedtak() + val tilkjentYtelse = lagInitiellTilkjentYtelse(vedtak.behandling) + val person = tilfeldigPerson() + val barn = tilfeldigPerson() + tilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + id = 3, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 8), + beløp = 250, + person = person, + ), + lagAndelTilkjentYtelse( + id = 4, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 8), + beløp = 350, + person = barn, + ), + ), + ) + + val forrigeBehandling = lagBehandling() + val forrigeTilkjentYtelse = lagInitiellTilkjentYtelse(forrigeBehandling) + forrigeTilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + id = 1, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 8), + beløp = 0, + person = person, + ), + lagAndelTilkjentYtelse( + id = 2, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 8), + beløp = 0, + person = barn, + ), + ), + ) + val tilkjentYtelseSlot = slot() + setUpMocks( + behandling = vedtak.behandling, + tilkjentYtelse = tilkjentYtelse, + tilkjentYtelseSlot = tilkjentYtelseSlot, + forrigeTilkjentYtelse = forrigeTilkjentYtelse, + ) + + val beregnetUtbetalingsoppdrag = + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = vedtak, + saksbehandlerId = "abc123", + erSimulering = true, + ) + + verify(exactly = 0) { tilkjentYtelseRepository.save(any()) } + + validerBeregnetUtbetalingsoppdragOgAndeler( + beregnetUtbetalingsoppdrag = beregnetUtbetalingsoppdrag, + andelerTilkjentYtelse = emptySet(), + forventetAntallAndeler = 2, + forventetAntallUtbetalingsperioder = 2, + forventedeOffsets = listOf( + Pair(0L, null), + Pair(1L, null), + ), + ) + } + + @Test + fun `genererUtbetalingsoppdrag - skal generere nytt utbetalingsoppdrag men ikke oppdatere andeler med offset når toggel er av`() { + val vedtak = lagVedtak() + val tilkjentYtelse = lagInitiellTilkjentYtelse(vedtak.behandling) + val person = tilfeldigPerson() + tilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + id = 1, + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 3), + beløp = 250, + person = person, + ), + lagAndelTilkjentYtelse( + id = 2, + fom = YearMonth.of(2023, 4), + tom = YearMonth.of(2023, 5), + beløp = 350, + person = person, + ), + lagAndelTilkjentYtelse( + id = 3, + fom = YearMonth.of(2023, 6), + tom = YearMonth.of(2023, 8), + beløp = 250, + person = person, + ), + ), + ) + val tilkjentYtelseSlot = slot() + setUpMocks( + behandling = vedtak.behandling, + tilkjentYtelse = tilkjentYtelse, + tilkjentYtelseSlot = tilkjentYtelseSlot, + brukNyUtbetalingsgeneratorToggleErPå = false, + ) + + val beregnetUtbetalingsoppdrag = + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = vedtak, + "abc123", + ) + + verify(exactly = 0) { tilkjentYtelseRepository.save(any()) } + + validerBeregnetUtbetalingsoppdragOgAndeler( + beregnetUtbetalingsoppdrag = beregnetUtbetalingsoppdrag, + andelerTilkjentYtelse = tilkjentYtelse.andelerTilkjentYtelse, + forventetAntallAndeler = 3, + forventetAntallUtbetalingsperioder = 3, + forventedeOffsets = listOf( + Pair(null, null), + Pair(null, null), + Pair(null, null), + ), + ) + } + + private fun validerBeregnetUtbetalingsoppdragOgAndeler( + beregnetUtbetalingsoppdrag: BeregnetUtbetalingsoppdragLongId, + andelerTilkjentYtelse: Set, + forventedeOffsets: List>, + forventetAntallAndeler: Int, + forventetAntallUtbetalingsperioder: Int, + ) { + assertThat(beregnetUtbetalingsoppdrag.utbetalingsoppdrag).isNotNull + assertThat(beregnetUtbetalingsoppdrag.utbetalingsoppdrag.utbetalingsperiode.size).isEqualTo( + forventetAntallUtbetalingsperioder, + ) + + assertThat(beregnetUtbetalingsoppdrag.andeler).isNotEmpty + assertThat(beregnetUtbetalingsoppdrag.andeler.size).isEqualTo(forventetAntallAndeler) + + if (andelerTilkjentYtelse.isNotEmpty()) { + assertThat(andelerTilkjentYtelse.size).isEqualTo(forventetAntallAndeler) + assertThat( + andelerTilkjentYtelse.map { + Pair( + it.periodeOffset, + it.forrigePeriodeOffset, + ) + }, + ).isEqualTo( + forventedeOffsets, + ) + } else { + assertThat( + beregnetUtbetalingsoppdrag.andeler.map { + Pair( + it.periodeId, + it.forrigePeriodeId, + ) + }, + ).isEqualTo( + forventedeOffsets, + ) + } + } + + private fun setUpMocks( + behandling: Behandling, + tilkjentYtelse: TilkjentYtelse, + tilkjentYtelseSlot: CapturingSlot, + brukNyUtbetalingsgeneratorToggleErPå: Boolean = true, + forrigeTilkjentYtelse: TilkjentYtelse? = null, + migreringsdato: LocalDate? = null, + ) { + if (forrigeTilkjentYtelse == null) { + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(behandling) } returns null + every { andelTilkjentYtelseRepository.hentSisteAndelPerIdentOgType(behandling.fagsak.id) } returns emptyList() + } else { + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(behandling) } returns forrigeTilkjentYtelse.behandling + + every { tilkjentYtelseRepository.findByBehandlingAndHasUtbetalingsoppdrag(forrigeTilkjentYtelse.behandling.id) } returns forrigeTilkjentYtelse + + every { andelTilkjentYtelseRepository.hentSisteAndelPerIdentOgType(behandling.fagsak.id) } returns + forrigeTilkjentYtelse.andelerTilkjentYtelse.filter { it.erAndelSomSkalSendesTilOppdrag() } + .groupBy { it.aktør.aktivFødselsnummer() } + .mapValues { it.value.maxBy { it.periodeOffset!! } }.values.toList() + } + + every { tilkjentYtelseRepository.findByBehandling(behandling.id) } returns tilkjentYtelse + + every { behandlingHentOgPersisterService.hentBehandlinger(behandling.fagsak.id) } returns listOf(behandling) + + every { behandlingService.hentMigreringsdatoPåFagsak(behandling.fagsak.id) } returns migreringsdato + + every { + unleashService.isEnabled( + toggleId = FeatureToggleConfig.BRUK_NY_UTBETALINGSGENERATOR, + properties = any(), + ) + } returns brukNyUtbetalingsgeneratorToggleErPå + + every { tilkjentYtelseRepository.save(capture(tilkjentYtelseSlot)) } returns mockk() + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragValidatorTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragValidatorTest.kt" new file mode 100644 index 000000000..af1567780 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragValidatorTest.kt" @@ -0,0 +1,196 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.medDifferanseberegning +import no.nav.familie.kontrakter.felles.oppdrag.Opphør +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsperiode +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.UUID + +internal class UtbetalingsoppdragValidatorTest { + + @Test + fun `nasjonalt utbetalingsoppdrag må ha utbetalingsperiode`() { + val utbetalingsoppdrag = lagUtbetalingsoppdrag() + assertThrows { + utbetalingsoppdrag.validerNullutbetaling( + behandlingskategori = BehandlingKategori.NASJONAL, + andelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 1054, + ), + ), + ) + } + } + + @Test + fun `innvilget EØS-utbetalingsoppdrag hvor Norge er sekundærland kan mangle utbetalingsperiode`() { + val utbetalingsoppdrag = lagUtbetalingsoppdrag() + assertDoesNotThrow { + utbetalingsoppdrag.validerNullutbetaling( + behandlingskategori = BehandlingKategori.EØS, + andelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 0, + ).medDifferanseberegning(BigDecimal("10")), + ), + ) + } + } + + @Test + fun `innvilget EØS-utbetalingsoppdrag hvor Norge er Primærland kan ikke mangle utbetalingsperiode`() { + val utbetalingsoppdrag = lagUtbetalingsoppdrag() + assertThrows { + utbetalingsoppdrag.validerNullutbetaling( + behandlingskategori = BehandlingKategori.EØS, + andelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 1054, + ), + ), + ) + } + } + + @Test + fun `innvilget EØS-utbetalingsoppdrag hvor Norge er sekundærland ikke kaster feil når finnes utbetalingsperiode`() { + val utbetalingsoppdrag = lagUtbetalingsoppdrag( + utbetalingsperioder = listOf( + Utbetalingsperiode( + erEndringPåEksisterendePeriode = false, + periodeId = 0, + datoForVedtak = LocalDate.now(), + klassifisering = "", + vedtakdatoFom = inneværendeMåned().førsteDagIInneværendeMåned(), + vedtakdatoTom = inneværendeMåned().atEndOfMonth(), + sats = BigDecimal(100), + satsType = Utbetalingsperiode.SatsType.MND, + utbetalesTil = "", + behandlingId = 123, + ), + ), + ) + assertDoesNotThrow { + utbetalingsoppdrag.validerNullutbetaling( + behandlingskategori = BehandlingKategori.EØS, + andelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 1024, + ).medDifferanseberegning(BigDecimal("10")), + ), + ) + } + } + + @Test + fun `valider opphør med lovlige perioder`() { + val fom = LocalDate.now().førsteDagIInneværendeMåned() + val tom = LocalDate.now().sisteDagIMåned() + val opphørDato = LocalDate.now().sisteDagIMåned() + + val utbetalingsPeriode = listOf( + lagEksternUtbetalingsperiode( + opphør = Opphør(opphørDato), + fom = fom.minusMonths(10), + tom = tom.minusMonths(8), + ), + lagEksternUtbetalingsperiode( + fom = fom.minusMonths(7), + tom = tom.minusMonths(6), + ), + lagEksternUtbetalingsperiode( + fom = fom.minusMonths(5), + tom = tom.minusMonths(4), + ), + ) + + // Test at validering ikke feiler. + lagEksternUtbetalingsoppdrag(utbetalingsPeriode).validerOpphørsoppdrag() + } + + @Test + fun `valider opphør med løpende utbetalingsperioder som skal kaste feil`() { + val fom = LocalDate.now().førsteDagIInneværendeMåned() + val tom = LocalDate.now().sisteDagIMåned() + val opphørDato = LocalDate.now().sisteDagIMåned() + + val utbetalingsPeriode = listOf( + lagEksternUtbetalingsperiode( + opphør = Opphør(opphørDato), + fom = fom.minusMonths(10), + tom = tom.minusMonths(8), + ), + lagEksternUtbetalingsperiode( + fom = fom.minusMonths(7), + tom = tom.minusMonths(6), + ), + lagEksternUtbetalingsperiode( + fom = fom.minusMonths(5), + tom = tom.plusMonths(1), + ), + ) + assertThrows { + lagEksternUtbetalingsoppdrag(utbetalingsPeriode).validerOpphørsoppdrag() + } + } + + private fun lagEksternUtbetalingsoppdrag(utbetalingsPeriode: List) = + Utbetalingsoppdrag( + Utbetalingsoppdrag.KodeEndring.ENDR, + "BA", + "123", + "123", + "123", + avstemmingTidspunkt = LocalDateTime.now(), + utbetalingsperiode = utbetalingsPeriode, + ) + + private fun lagEksternUtbetalingsperiode(opphør: Opphør? = null, fom: LocalDate, tom: LocalDate) = + Utbetalingsperiode( + false, + opphør, + 1, + null, + LocalDate.now(), + "BATR", + fom, + tom, + BigDecimal(1054), + Utbetalingsperiode.SatsType.MND, + randomFnr(), + lagBehandling().id, + ) + + private fun lagUtbetalingsoppdrag(utbetalingsperioder: List = emptyList()) = Utbetalingsoppdrag( + kodeEndring = Utbetalingsoppdrag.KodeEndring.NY, + fagSystem = "BA", + saksnummer = "", + aktoer = UUID.randomUUID().toString(), + saksbehandlerId = "", + avstemmingTidspunkt = LocalDateTime.now(), + utbetalingsperiode = utbetalingsperioder, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/internkonsistensavstemming/InternKonsistensavstemmingUtilTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/internkonsistensavstemming/InternKonsistensavstemmingUtilTest.kt" new file mode 100644 index 000000000..358e9d58a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/internkonsistensavstemming/InternKonsistensavstemmingUtilTest.kt" @@ -0,0 +1,547 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi.internkonsistensavstemming + +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.YearMonth + +class InternKonsistensavstemmingUtilTest { + + @Test + fun `Skal ignorere forskjeller før første utbetalingsoppdragsperiode`() { + val andelerSisteVedtatteBehandling = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.parse("2021-12"), + tom = YearMonth.parse("2021-12"), + beløp = 1654, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.parse("2022-01"), + tom = YearMonth.parse("2023-02"), + beløp = 1676, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.parse("2023-03"), + tom = YearMonth.parse("2027-10"), + beløp = 1723, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.parse("2027-11"), + tom = YearMonth.parse("2039-10"), + beløp = 1083, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + ) + val utbetalingsoppdrag = objectMapper.readValue(mockUtbetalingsoppdrag) + + Assertions.assertFalse(erForskjellMellomAndelerOgOppdrag(andelerSisteVedtatteBehandling, utbetalingsoppdrag, 0L)) + } + + @Test + fun `skal se at vi mangler andel for oppdragsperiode`() { + val andelerSisteVedtatteBehandling = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.parse("2021-12"), + tom = YearMonth.parse("2021-12"), + beløp = 1654, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.parse("2022-01"), + tom = YearMonth.parse("2023-02"), + beløp = 1676, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.parse("2023-03"), + tom = YearMonth.parse("2027-10"), + beløp = 1723, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + ) + val utbetalingsoppdrag = objectMapper.readValue(mockUtbetalingsoppdrag) + + Assertions.assertTrue(erForskjellMellomAndelerOgOppdrag(andelerSisteVedtatteBehandling, utbetalingsoppdrag, 0L)) + } + + @Test + fun `skal se at andel og oppdragsperiode har forskjellig beløp`() { + val andelerSisteVedtatteBehandling = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.parse("2021-12"), + tom = YearMonth.parse("2021-12"), + beløp = 1654, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.parse("2022-01"), + tom = YearMonth.parse("2023-02"), + beløp = 1676, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.parse("2023-03"), + tom = YearMonth.parse("2027-10"), + beløp = 1723, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.parse("2027-11"), + tom = YearMonth.parse("2039-10"), + beløp = 9999, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + ) + val utbetalingsoppdrag = objectMapper.readValue(mockUtbetalingsoppdrag) + + Assertions.assertTrue(erForskjellMellomAndelerOgOppdrag(andelerSisteVedtatteBehandling, utbetalingsoppdrag, 0L)) + } + + @Test + fun `skal ikke si det er forskjell ved riktig utbetalingsoppdrag når det er flere kjeder`() { + val andelStringer = listOf( + "2021-05,2021-08,1354,ORDINÆR_BARNETRYGD", + "2021-09,2021-12,1654,ORDINÆR_BARNETRYGD", + + "2022-01,2023-02,1676,ORDINÆR_BARNETRYGD", + "2023-03,2024-11,1723,ORDINÆR_BARNETRYGD", + "2024-12,2036-11,1083,ORDINÆR_BARNETRYGD", + + "2021-05,2023-02,1054,UTVIDET_BARNETRYGD", + "2023-03,2036-11,2489,UTVIDET_BARNETRYGD", + ) + + val andelerSisteVedtatteBehandling = andelStringer.map { it.split(",") }.map { + lagAndelTilkjentYtelse( + fom = YearMonth.parse(it[0]), + tom = YearMonth.parse(it[1]), + beløp = it[2].toInt(), + ytelseType = YtelseType.valueOf(it[3]), + ) + } + + val utbetalingsoppdrag = objectMapper.readValue(utbetalingsoppdragMockMedUtvidet) + + Assertions.assertFalse(erForskjellMellomAndelerOgOppdrag(andelerSisteVedtatteBehandling, utbetalingsoppdrag, 0L)) + } + + @Test + fun `skal ikke si det er forskjell ved riktig utbetalingsoppdrag når kun ett barn ble endret i siste behandling som iverksatte`() { + val andelStringer = listOf( + "2022-05,2022-06,1676,ORDINÆR_BARNETRYGD,2554733867704", // barn 1, ble laget i siste behandling som iverksatte + "2022-07,2028-03,838,ORDINÆR_BARNETRYGD,2554733867704", // barn 1, ble laget i siste behandling som iverksatte + "2028-04,2040-03,527,ORDINÆR_BARNETRYGD,2554733867704", // barn 1, ble laget i siste behandling som iverksatte + + "2022-07,2028-05,1676,ORDINÆR_BARNETRYGD,2909658383415", // barn 2, ble laget før siste behandling som iverksatte + "2028-06,2040-05,1054,ORDINÆR_BARNETRYGD,2909658383415", // barn 2, ble laget før siste behandling som iverksatte + ) + + val andelerSisteVedtatteBehandling = andelStringer.map { it.split(",") }.map { + lagAndelTilkjentYtelse( + fom = YearMonth.parse(it[0]), + tom = YearMonth.parse(it[1]), + beløp = it[2].toInt(), + ytelseType = YtelseType.valueOf(it[3]), + aktør = Aktør(it[4]), + ) + } + + val utbetalingsoppdrag = objectMapper.readValue(utbetalingsoppdragMockEndringKunEttBarn) + + Assertions.assertFalse(erForskjellMellomAndelerOgOppdrag(andelerSisteVedtatteBehandling, utbetalingsoppdrag, 0L)) + } + + @Test + fun `skal ikke si det er forskjell ved opphør`() { + val andelStringer = listOf( + "2016-02,2019-02,970,ORDINÆR_BARNETRYGD,2193974415300", + "2019-03,2020-08,1054,ORDINÆR_BARNETRYGD,2193974415300", + "2020-09,2021-08,1354,ORDINÆR_BARNETRYGD,2193974415300", + "2021-09,2021-12,1654,ORDINÆR_BARNETRYGD,2193974415300", + "2022-01,2022-02,1054,ORDINÆR_BARNETRYGD,2193974415300", + "2012-06,2019-02,970,ORDINÆR_BARNETRYGD,2094407059820", + "2019-03,2022-01,1054,ORDINÆR_BARNETRYGD,2094407059820", + ) + + val andelerSisteVedtatteBehandling = andelStringer.map { it.split(",") }.map { + lagAndelTilkjentYtelse( + fom = YearMonth.parse(it[0]), + tom = YearMonth.parse(it[1]), + beløp = it[2].toInt(), + ytelseType = YtelseType.valueOf(it[3]), + aktør = Aktør(it[4]), + ) + } + + val utbetalingsoppdrag = objectMapper.readValue(utbetalingsoppdragMockOpphør) + + Assertions.assertFalse(erForskjellMellomAndelerOgOppdrag(andelerSisteVedtatteBehandling, utbetalingsoppdrag, 0L)) + } + + @Test + fun `skal si andelene og utbetalingene er like dersom andel blir splittet opp, men betaler ut det samme i periodene`() { + val andelStringer = listOf( + "2021-01,2022-03,1054,ORDINÆR_BARNETRYGD", + "2022-04,2022-06,1054,ORDINÆR_BARNETRYGD", + ) + + val aktør = randomAktør() + + val andelerSisteVedtatteBehandling = andelStringer.map { it.split(",") }.map { + lagAndelTilkjentYtelse( + fom = YearMonth.parse(it[0]), + tom = YearMonth.parse(it[1]), + beløp = it[2].toInt(), + ytelseType = YtelseType.valueOf(it[3]), + aktør = aktør, + ) + } + + val utbetalingsoppdrag = objectMapper.readValue(utbetalingsoppdragMockEnPeriode) + + Assertions.assertFalse(erForskjellMellomAndelerOgOppdrag(andelerSisteVedtatteBehandling, utbetalingsoppdrag, 0L)) + } +} + +private val mockUtbetalingsoppdrag = """ + { + "kodeEndring": "ENDR", + "fagSystem": "BA", + "saksnummer": "1", + "aktoer": "1", + "saksbehandlerId": "VL", + "avstemmingTidspunkt": "2023-02-08T16:12:56.200284803", + "utbetalingsperiode": [ + { + "erEndringPåEksisterendePeriode": true, + "opphør": { + "opphørDatoFom": "2022-01-01" + }, + "periodeId": 2, + "forrigePeriodeId": 1, + "datoForVedtak": "2023-02-08", + "klassifisering": "BATR", + "vedtakdatoFom": "2027-11-01", + "vedtakdatoTom": "2039-10-31", + "sats": 1054, + "satsType": "MND", + "utbetalesTil": "1", + "behandlingId": 1, + "utbetalingsgrad": null + }, + { + "erEndringPåEksisterendePeriode": false, + "opphør": null, + "periodeId": 3, + "forrigePeriodeId": 2, + "datoForVedtak": "2023-02-08", + "klassifisering": "BATR", + "vedtakdatoFom": "2022-01-01", + "vedtakdatoTom": "2023-02-28", + "sats": 1676, + "satsType": "MND", + "utbetalesTil": "1", + "behandlingId": 1, + "utbetalingsgrad": null + }, + { + "erEndringPåEksisterendePeriode": false, + "opphør": null, + "periodeId": 4, + "forrigePeriodeId": 3, + "datoForVedtak": "2023-02-08", + "klassifisering": "BATR", + "vedtakdatoFom": "2023-03-01", + "vedtakdatoTom": "2027-10-31", + "sats": 1723, + "satsType": "MND", + "utbetalesTil": "1", + "behandlingId": 1, + "utbetalingsgrad": null + }, + { + "erEndringPåEksisterendePeriode": false, + "opphør": null, + "periodeId": 5, + "forrigePeriodeId": 4, + "datoForVedtak": "2023-02-08", + "klassifisering": "BATR", + "vedtakdatoFom": "2027-11-01", + "vedtakdatoTom": "2039-10-31", + "sats": 1083, + "satsType": "MND", + "utbetalesTil": "1", + "behandlingId": 1, + "utbetalingsgrad": null + } + ], + "gOmregning": false + } +""".trimIndent() + +private val utbetalingsoppdragMockMedUtvidet = """ + { + "kodeEndring": "ENDR", + "fagSystem": "BA", + "saksnummer": "200028561", + "aktoer": "02416938515", + "saksbehandlerId": "VL", + "avstemmingTidspunkt": "2023-02-08T15:57:38.341011606", + "utbetalingsperiode": [ + { + "erEndringPåEksisterendePeriode": true, + "opphør": { + "opphørDatoFom": "2022-01-01" + }, + "periodeId": 3, + "forrigePeriodeId": 2, + "datoForVedtak": "2023-02-08", + "klassifisering": "BATR", + "vedtakdatoFom": "2024-12-01", + "vedtakdatoTom": "2036-11-30", + "sats": 1054, + "satsType": "MND", + "utbetalesTil": "02416938515", + "behandlingId": 100134370, + "utbetalingsgrad": null + }, + { + "erEndringPåEksisterendePeriode": true, + "opphør": { + "opphørDatoFom": "2021-05-01" + }, + "periodeId": 4, + "forrigePeriodeId": null, + "datoForVedtak": "2023-02-08", + "klassifisering": "BATR", + "vedtakdatoFom": "2021-05-01", + "vedtakdatoTom": "2036-11-30", + "sats": 1054, + "satsType": "MND", + "utbetalesTil": "02416938515", + "behandlingId": 100134370, + "utbetalingsgrad": null + }, + { + "erEndringPåEksisterendePeriode": false, + "opphør": null, + "periodeId": 5, + "forrigePeriodeId": 3, + "datoForVedtak": "2023-02-08", + "klassifisering": "BATR", + "vedtakdatoFom": "2022-01-01", + "vedtakdatoTom": "2023-02-28", + "sats": 1676, + "satsType": "MND", + "utbetalesTil": "02416938515", + "behandlingId": 100134370, + "utbetalingsgrad": null + }, + { + "erEndringPåEksisterendePeriode": false, + "opphør": null, + "periodeId": 6, + "forrigePeriodeId": 5, + "datoForVedtak": "2023-02-08", + "klassifisering": "BATR", + "vedtakdatoFom": "2023-03-01", + "vedtakdatoTom": "2024-11-30", + "sats": 1723, + "satsType": "MND", + "utbetalesTil": "02416938515", + "behandlingId": 100134370, + "utbetalingsgrad": null + }, + { + "erEndringPåEksisterendePeriode": false, + "opphør": null, + "periodeId": 7, + "forrigePeriodeId": 6, + "datoForVedtak": "2023-02-08", + "klassifisering": "BATR", + "vedtakdatoFom": "2024-12-01", + "vedtakdatoTom": "2036-11-30", + "sats": 1083, + "satsType": "MND", + "utbetalesTil": "02416938515", + "behandlingId": 100134370, + "utbetalingsgrad": null + }, + { + "erEndringPåEksisterendePeriode": false, + "opphør": null, + "periodeId": 8, + "forrigePeriodeId": 4, + "datoForVedtak": "2023-02-08", + "klassifisering": "BATR", + "vedtakdatoFom": "2021-05-01", + "vedtakdatoTom": "2023-02-28", + "sats": 1054, + "satsType": "MND", + "utbetalesTil": "02416938515", + "behandlingId": 100134370, + "utbetalingsgrad": null + }, + { + "erEndringPåEksisterendePeriode": false, + "opphør": null, + "periodeId": 9, + "forrigePeriodeId": 8, + "datoForVedtak": "2023-02-08", + "klassifisering": "BATR", + "vedtakdatoFom": "2023-03-01", + "vedtakdatoTom": "2036-11-30", + "sats": 2489, + "satsType": "MND", + "utbetalesTil": "02416938515", + "behandlingId": 100134370, + "utbetalingsgrad": null + } + ], + "gOmregning": false + } +""".trimIndent() + +val utbetalingsoppdragMockEndringKunEttBarn = """ + { + "kodeEndring": "ENDR", + "fagSystem": "BA", + "saksnummer": "200002102", + "aktoer": "07118905215", + "saksbehandlerId": "Z994623", + "avstemmingTidspunkt": "2022-08-16T08:49:13.565620862", + "utbetalingsperiode": [ + { + "erEndringPåEksisterendePeriode": true, + "opphør": { + "opphørDatoFom": "2022-05-01" + }, + "periodeId": 3, + "forrigePeriodeId": 2, + "datoForVedtak": "2022-08-16", + "klassifisering": "BATR", + "vedtakdatoFom": "2028-04-01", + "vedtakdatoTom": "2040-03-31", + "sats": 1054, + "satsType": "MND", + "utbetalesTil": "07118905215", + "behandlingId": 100098303, + "utbetalingsgrad": null + }, + { + "erEndringPåEksisterendePeriode": false, + "opphør": null, + "periodeId": 4, + "forrigePeriodeId": 3, + "datoForVedtak": "2022-08-16", + "klassifisering": "BATR", + "vedtakdatoFom": "2022-05-01", + "vedtakdatoTom": "2022-06-30", + "sats": 1676, + "satsType": "MND", + "utbetalesTil": "07118905215", + "behandlingId": 100098303, + "utbetalingsgrad": null + }, + { + "erEndringPåEksisterendePeriode": false, + "opphør": null, + "periodeId": 5, + "forrigePeriodeId": 4, + "datoForVedtak": "2022-08-16", + "klassifisering": "BATR", + "vedtakdatoFom": "2022-07-01", + "vedtakdatoTom": "2028-03-31", + "sats": 838, + "satsType": "MND", + "utbetalesTil": "07118905215", + "behandlingId": 100098303, + "utbetalingsgrad": null + }, + { + "erEndringPåEksisterendePeriode": false, + "opphør": null, + "periodeId": 6, + "forrigePeriodeId": 5, + "datoForVedtak": "2022-08-16", + "klassifisering": "BATR", + "vedtakdatoFom": "2028-04-01", + "vedtakdatoTom": "2040-03-31", + "sats": 527, + "satsType": "MND", + "utbetalesTil": "07118905215", + "behandlingId": 100098303, + "utbetalingsgrad": null + } + ], + "gOmregning": false + } +""".trimIndent() + +val utbetalingsoppdragMockOpphør = """ + { + "kodeEndring": "ENDR", + "fagSystem": "BA", + "saksnummer": "200001701", + "aktoer": "25118604604", + "saksbehandlerId": "Z991771", + "avstemmingTidspunkt": "2022-08-09T14:42:14.977345689", + "utbetalingsperiode": [ + { + "erEndringPåEksisterendePeriode": true, + "opphør": { + "opphørDatoFom": "2022-08-01" + }, + "periodeId": 10, + "forrigePeriodeId": 9, + "datoForVedtak": "2022-08-09", + "klassifisering": "BATR", + "vedtakdatoFom": "2025-02-01", + "vedtakdatoTom": "2037-01-31", + "sats": 1054, + "satsType": "MND", + "utbetalesTil": "25118604604", + "behandlingId": 100096955, + "utbetalingsgrad": null + } + ], + "gOmregning": false + } +""".trimIndent() + +val utbetalingsoppdragMockEnPeriode = """ + { + "kodeEndring": "NY", + "fagSystem": "BA", + "saksnummer": "1", + "aktoer": "1", + "saksbehandlerId": "", + "avstemmingTidspunkt": "2021-12-23T08:11:33.333476714", + "utbetalingsperiode": [ + { + "erEndringPåEksisterendePeriode": false, + "opphør": null, + "periodeId": 0, + "forrigePeriodeId": null, + "datoForVedtak": "2021-12-23", + "klassifisering": "BATR", + "vedtakdatoFom": "2021-01-01", + "vedtakdatoTom": "2022-06-30", + "sats": 1054, + "satsType": "MND", + "utbetalesTil": "1", + "behandlingId": 1, + "utbetalingsgrad": null + } + ] + } +""".trimIndent() diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiServiceTest.kt" new file mode 100644 index 000000000..7531e47d8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiServiceTest.kt" @@ -0,0 +1,163 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValideringService +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.simulering.KontrollerNyUtbetalingsgeneratorService +import no.nav.familie.felles.utbetalingsgenerator.domain.BeregnetUtbetalingsoppdragLongId +import no.nav.familie.felles.utbetalingsgenerator.domain.Utbetalingsperiode +import no.nav.familie.unleash.UnleashService +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.math.BigDecimal +import java.time.LocalDate + +@ExtendWith(MockKExtension::class) +internal class ØkonomiServiceTest { + @MockK + private lateinit var økonomiKlient: ØkonomiKlient + + @MockK + private lateinit var beregningService: BeregningService + + @MockK + private lateinit var tilkjentYtelseValideringService: TilkjentYtelseValideringService + + @MockK + private lateinit var tilkjentYtelseRepository: TilkjentYtelseRepository + + @MockK + private lateinit var kontrollerNyUtbetalingsgeneratorService: KontrollerNyUtbetalingsgeneratorService + + @MockK + private lateinit var utbetalingsoppdragGeneratorService: UtbetalingsoppdragGeneratorService + + @MockK + private lateinit var unleashService: UnleashService + + @InjectMockKs + private lateinit var økonomiService: ØkonomiService + + @Test + fun `oppdaterTilkjentYtelseMedUtbetalingsoppdragOgIverksett - skal bruke gammel utbetalingsgenerator når toggel er av`() { + setupMocks(toggelPå = false) + + økonomiService.oppdaterTilkjentYtelseMedUtbetalingsoppdragOgIverksett( + lagVedtak(), + "123abc", + AndelTilkjentYtelseForIverksettingFactory(), + ) + + verify(exactly = 1) { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + andelTilkjentYtelseForUtbetalingsoppdragFactory = any(), + ) + } + verify(exactly = 1) { beregningService.oppdaterTilkjentYtelseMedUtbetalingsoppdrag(any(), any()) } + verify(exactly = 0) { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + erSimulering = any(), + ) + } + } + + @Test + fun `oppdaterTilkjentYtelseMedUtbetalingsoppdragOgIverksett - skal bruke ny utbetalingsgenerator når toggel er på`() { + setupMocks(toggelPå = true) + + økonomiService.oppdaterTilkjentYtelseMedUtbetalingsoppdragOgIverksett( + lagVedtak(), + "123abc", + AndelTilkjentYtelseForIverksettingFactory(), + ) + + verify(exactly = 0) { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + andelTilkjentYtelseForUtbetalingsoppdragFactory = any(), + ) + } + verify(exactly = 0) { beregningService.oppdaterTilkjentYtelseMedUtbetalingsoppdrag(any(), any()) } + verify(exactly = 1) { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + erSimulering = any(), + ) + } + } + + private fun setupMocks(toggelPå: Boolean) { + val utbetalingsoppdrag = lagUtbetalingsoppdrag( + listOf( + Utbetalingsperiode( + erEndringPåEksisterendePeriode = false, + opphør = null, + periodeId = 1, + forrigePeriodeId = null, + datoForVedtak = LocalDate.now(), + klassifisering = "BATR", + vedtakdatoFom = inneværendeMåned().førsteDagIInneværendeMåned(), + vedtakdatoTom = inneværendeMåned().sisteDagIInneværendeMåned(), + sats = BigDecimal(1054), + satsType = Utbetalingsperiode.SatsType.MND, + utbetalesTil = "13455678910", + behandlingId = 1, + ), + ), + ) + every { + kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + any(), + any(), + ) + } returns emptyList() + every { + unleashService.isEnabled( + toggleId = FeatureToggleConfig.BRUK_NY_UTBETALINGSGENERATOR, + properties = any(), + ) + } returns toggelPå + + every { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + andelTilkjentYtelseForUtbetalingsoppdragFactory = any(), + ) + } returns utbetalingsoppdrag.tilRestUtbetalingsoppdrag() + every { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + erSimulering = any(), + ) + } returns BeregnetUtbetalingsoppdragLongId(utbetalingsoppdrag = utbetalingsoppdrag, andeler = emptyList()) + every { + beregningService.oppdaterTilkjentYtelseMedUtbetalingsoppdrag( + any(), + any(), + ) + } returns mockk() + every { tilkjentYtelseValideringService.validerIngenAndelerTilkjentYtelseMedSammeOffsetIBehandling(any()) } just runs + every { økonomiKlient.iverksettOppdrag(any()) } returns "" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiTestUtils.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiTestUtils.kt" new file mode 100644 index 000000000..845ae3e8d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiTestUtils.kt" @@ -0,0 +1,57 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsperiode +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.UUID + +fun sats(ytelseType: YtelseType) = + when (ytelseType) { + YtelseType.ORDINÆR_BARNETRYGD -> 1054 + YtelseType.UTVIDET_BARNETRYGD -> 1054 + YtelseType.SMÅBARNSTILLEGG -> 660 + } + +fun lagUtbetalingsoppdrag(utbetalingsperiode: List) = Utbetalingsoppdrag( + kodeEndring = Utbetalingsoppdrag.KodeEndring.NY, + fagSystem = "BA", + saksnummer = "", + aktoer = UUID.randomUUID().toString(), + saksbehandlerId = "", + avstemmingTidspunkt = LocalDateTime.now(), + utbetalingsperiode = utbetalingsperiode, +) + +fun lagUtbetalingsoppdrag( + utbetalingsperiode: List = listOf( + lagUtbetalingsperiode(), + ), +) = + no.nav.familie.felles.utbetalingsgenerator.domain.Utbetalingsoppdrag( + kodeEndring = no.nav.familie.felles.utbetalingsgenerator.domain.Utbetalingsoppdrag.KodeEndring.NY, + fagSystem = "BA", + saksnummer = "", + aktoer = UUID.randomUUID().toString(), + saksbehandlerId = "", + avstemmingTidspunkt = LocalDateTime.now(), + utbetalingsperiode = utbetalingsperiode, + ) + +fun lagUtbetalingsperiode() = no.nav.familie.felles.utbetalingsgenerator.domain.Utbetalingsperiode( + erEndringPåEksisterendePeriode = false, + opphør = null, + periodeId = 0, + forrigePeriodeId = null, + datoForVedtak = LocalDate.now(), + klassifisering = "", + vedtakdatoFom = LocalDate.now(), + vedtakdatoTom = LocalDate.now().plusMonths(1), + sats = BigDecimal(100), + satsType = no.nav.familie.felles.utbetalingsgenerator.domain.Utbetalingsperiode.SatsType.MND, + utbetalesTil = "", + behandlingId = 1, + utbetalingsgrad = 100, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiUtilsTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiUtilsTest.kt" new file mode 100644 index 000000000..2175eac3c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiUtilsTest.kt" @@ -0,0 +1,538 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.årMnd +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.andelerTilOpphørMedDato +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.andelerTilOpprettelse +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.grupperAndeler +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.oppdaterBeståendeAndelerMedOffset +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.sisteBeståendeAndelPerKjede +import no.nav.familie.ba.sak.kjerne.beregning.BeregningTestUtil.sisteAndelPerIdent +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType.ORDINÆR_BARNETRYGD +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType.SMÅBARNSTILLEGG +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.YearMonth + +internal class ØkonomiUtilsTest { + + @Test + fun `skal separere småbarnstillegg`() { + val person = tilfeldigPerson() + val kjederBehandling = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2023-10"), + årMnd("2025-01"), + SMÅBARNSTILLEGG, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + årMnd("2027-10"), + årMnd("2028-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + ).forIverksetting(), + ) + + assertEquals(2, kjederBehandling.size) + } + + @Test + fun `skal siste før første berørte andel i kjede`() { + val person = tilfeldigPerson() + val person2 = tilfeldigPerson() + + val kjederBehandling1 = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + årMnd("2022-01"), + årMnd("2023-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person2, + ), + lagAndelTilkjentYtelse( + årMnd("2022-01"), + årMnd("2023-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person2, + ), + ).forIverksetting(), + ) + val kjederBehandling2 = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + årMnd("2022-01"), + årMnd("2022-10"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person2, + ), + lagAndelTilkjentYtelse( + årMnd("2022-01"), + årMnd("2023-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person2, + ), + ).forIverksetting(), + ) + + val sisteBeståendePerKjede = + sisteBeståendeAndelPerKjede(forrigeKjeder = kjederBehandling1, oppdaterteKjeder = kjederBehandling2) + val identOgTypePerson = IdentOgYtelse(person.aktør.aktivFødselsnummer(), ORDINÆR_BARNETRYGD) + val identOgYtelsePerson2 = IdentOgYtelse(person2.aktør.aktivFødselsnummer(), ORDINÆR_BARNETRYGD) + assertEquals(årMnd("2019-04"), sisteBeståendePerKjede[identOgTypePerson]?.stønadFom) + assertEquals(årMnd("2022-01"), sisteBeståendePerKjede[identOgYtelsePerson2]?.stønadFom) + } + + @Test + fun `skal sette null som siste bestående for person med endring i første`() { + val person = tilfeldigPerson() + + val kjederBehandling1 = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + årMnd("2022-01"), + årMnd("2023-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + ).forIverksetting(), + ) + val kjederBehandling2 = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2018-04"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + årMnd("2022-01"), + årMnd("2023-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + ).forIverksetting(), + ) + + val sisteBeståendePerKjede = + sisteBeståendeAndelPerKjede(forrigeKjeder = kjederBehandling1, oppdaterteKjeder = kjederBehandling2) + assertEquals(null, sisteBeståendePerKjede[IdentOgYtelse(person.aktør.aktørId, ORDINÆR_BARNETRYGD)]) + } + + @Test + fun `skal sette null som siste bestående for ny person`() { + val person = tilfeldigPerson() + val kjederBehandling = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2023-10"), + årMnd("2025-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + årMnd("2027-10"), + årMnd("2028-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + ).forIverksetting(), + ) + + val sisteBeståendePerKjede = + sisteBeståendeAndelPerKjede(forrigeKjeder = emptyMap(), oppdaterteKjeder = kjederBehandling) + assertEquals(null, sisteBeståendePerKjede[IdentOgYtelse(person.aktør.aktørId, ORDINÆR_BARNETRYGD)]?.stønadFom) + } + + @Test + fun `skal settes null som siste bestående ved fullt opphørt person`() { + val person = tilfeldigPerson() + + val kjederBehandling = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2023-10"), + årMnd("2025-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + årMnd("2027-10"), + årMnd("2028-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + ).forIverksetting(), + ) + + val sisteBeståendePerKjede = + sisteBeståendeAndelPerKjede(forrigeKjeder = kjederBehandling, oppdaterteKjeder = emptyMap()) + assertEquals(null, sisteBeståendePerKjede[IdentOgYtelse(person.aktør.aktørId, ORDINÆR_BARNETRYGD)]?.stønadFom) + } + + @Test + fun `skal velge rette perioder til opphør og oppbygging fra endring`() { + val person = tilfeldigPerson() + + val datoSomSkalOppdateres = "2022-01" + val datoSomErOppdatert = "2021-01" + + val kjederBehandling1 = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + aktør = person.aktør, + periodeIdOffset = 0, + forrigeperiodeIdOffset = null, + ), + lagAndelTilkjentYtelse( + årMnd(datoSomSkalOppdateres), + årMnd("2023-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + aktør = person.aktør, + periodeIdOffset = 1, + forrigeperiodeIdOffset = 0, + ), + lagAndelTilkjentYtelse( + årMnd("2025-04"), + årMnd("2026-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + aktør = person.aktør, + periodeIdOffset = 2, + forrigeperiodeIdOffset = 1, + ), + ).forIverksetting(), + ) + val kjederBehandling2 = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + aktør = person.aktør, + ), + lagAndelTilkjentYtelse( + årMnd(datoSomErOppdatert), + årMnd("2023-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + aktør = person.aktør, + ), + lagAndelTilkjentYtelse( + årMnd("2025-04"), + årMnd("2026-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + aktør = person.aktør, + ), + ).forIverksetting(), + ) + + val sisteBeståendePerKjede = + sisteBeståendeAndelPerKjede( + forrigeKjeder = kjederBehandling1, + oppdaterteKjeder = kjederBehandling2, + ) + val andelerTilOpprettelse = + andelerTilOpprettelse( + oppdaterteKjeder = kjederBehandling2, + sisteBeståendeAndelIHverKjede = sisteBeståendePerKjede, + ) + val andelerTilOpphørMedDato = + andelerTilOpphørMedDato( + forrigeKjeder = kjederBehandling1, + sisteBeståendeAndelIHverKjede = sisteBeståendePerKjede, + sisteAndelPerIdent = sisteAndelPerIdent(kjederBehandling1.values.flatten()), + ) + + assertEquals(1, andelerTilOpprettelse.size) + assertEquals(2, andelerTilOpprettelse.first().size) + assertEquals(1, andelerTilOpphørMedDato.size) + assertEquals(årMnd(datoSomSkalOppdateres), andelerTilOpphørMedDato.first().second) + } + + @Test + fun `skal opphøre først barn helt og innvilge nytt barn når første barn ikke er innvilget i andre behandling`() { + val førsteBarn = tilfeldigPerson() + val andreBarn = tilfeldigPerson() + + val kjederBehandling1 = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = førsteBarn, + aktør = førsteBarn.aktør, + periodeIdOffset = 0, + forrigeperiodeIdOffset = null, + ), + lagAndelTilkjentYtelse( + årMnd("2020-02"), + årMnd("2023-01"), + ORDINÆR_BARNETRYGD, + 1345, + person = førsteBarn, + aktør = førsteBarn.aktør, + periodeIdOffset = 1, + forrigeperiodeIdOffset = 0, + ), + lagAndelTilkjentYtelse( + årMnd("2023-02"), + årMnd("2026-01"), + ORDINÆR_BARNETRYGD, + 1654, + person = førsteBarn, + aktør = førsteBarn.aktør, + periodeIdOffset = 2, + forrigeperiodeIdOffset = 1, + ), + ).forIverksetting(), + ) + val kjederBehandling2 = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2020-04"), + årMnd("2021-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = andreBarn, + aktør = andreBarn.aktør, + ), + lagAndelTilkjentYtelse( + årMnd("2021-02"), + årMnd("2023-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = andreBarn, + aktør = andreBarn.aktør, + ), + lagAndelTilkjentYtelse( + årMnd("2025-04"), + årMnd("2026-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = andreBarn, + aktør = andreBarn.aktør, + ), + ).forIverksetting(), + ) + + val sisteBeståendePerKjede = + sisteBeståendeAndelPerKjede( + forrigeKjeder = kjederBehandling1, + oppdaterteKjeder = kjederBehandling2, + ) + val andelerTilOpprettelse = + andelerTilOpprettelse( + oppdaterteKjeder = kjederBehandling2, + sisteBeståendeAndelIHverKjede = sisteBeståendePerKjede, + ) + val andelerTilOpphørMedDato = + andelerTilOpphørMedDato( + forrigeKjeder = kjederBehandling1, + sisteBeståendeAndelIHverKjede = sisteBeståendePerKjede, + sisteAndelPerIdent = sisteAndelPerIdent(kjederBehandling1.values.flatten()), + ) + + assertEquals(1, andelerTilOpphørMedDato.size) + assertEquals(YearMonth.of(2023, 2), andelerTilOpphørMedDato.first().first.stønadFom) + assertEquals(YearMonth.of(2019, 4), andelerTilOpphørMedDato.first().second) + assertEquals(3, andelerTilOpprettelse.first().size) + assertEquals(YearMonth.of(2020, 4), andelerTilOpprettelse.first().first().stønadFom) + assertEquals(YearMonth.of(2021, 1), andelerTilOpprettelse.first().first().stønadTom) + assertEquals(YearMonth.of(2025, 4), andelerTilOpprettelse.first().last().stønadFom) + assertEquals(YearMonth.of(2026, 1), andelerTilOpprettelse.first().last().stønadTom) + } + + @Test + fun `skal gjøre separate endringer på ordinær og småbarnstillegg`() { + val person = tilfeldigPerson() + + val kjederBehandling1 = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + ).forIverksetting(), + ) + val kjederBehandling2 = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2019-06"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + årMnd("2022-01"), + årMnd("2023-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-01"), + SMÅBARNSTILLEGG, + 1054, + person = person, + ), + ).forIverksetting(), + ) + + val sisteBeståendePerKjede = + sisteBeståendeAndelPerKjede( + forrigeKjeder = kjederBehandling1, + oppdaterteKjeder = kjederBehandling2, + ) + val andelerTilOpprettelse = + andelerTilOpprettelse( + oppdaterteKjeder = kjederBehandling2, + sisteBeståendeAndelIHverKjede = sisteBeståendePerKjede, + ) + val andelerTilOpphørMedDato = + andelerTilOpphørMedDato( + forrigeKjeder = kjederBehandling1, + sisteBeståendeAndelIHverKjede = sisteBeståendePerKjede, + sisteAndelPerIdent = sisteAndelPerIdent(kjederBehandling1.values.flatten()), + ) + + assertEquals(2, andelerTilOpprettelse.size) + assertEquals(1, andelerTilOpphørMedDato.size) + assertEquals(årMnd("2019-04"), andelerTilOpphørMedDato.first().second) + } + + @Test + fun `skal oppdatere offset på bestående behandler i oppdaterte kjeder`() { + val person = tilfeldigPerson() + val person2 = tilfeldigPerson() + + val kjederBehandling1 = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + periodeIdOffset = 1, + forrigeperiodeIdOffset = 0, + person = person, + ), + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + periodeIdOffset = 3, + forrigeperiodeIdOffset = 2, + person = person2, + ), + ).forIverksetting(), + ) + val kjederBehandling2 = grupperAndeler( + listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + årMnd("2019-12"), + årMnd("2020-01"), + ORDINÆR_BARNETRYGD, + 1054, + person = person2, + ), + ).forIverksetting(), + ) + + val oppdaterte = + oppdaterBeståendeAndelerMedOffset( + forrigeKjeder = kjederBehandling1, + oppdaterteKjeder = kjederBehandling2, + ) + + val identOgYtelse = IdentOgYtelse(person.aktør.aktivFødselsnummer(), ORDINÆR_BARNETRYGD) + val identOgYtelsePerson2 = IdentOgYtelse(person2.aktør.aktivFødselsnummer(), ORDINÆR_BARNETRYGD) + assertEquals(1, oppdaterte.getValue(identOgYtelse).first().periodeOffset) + assertEquals(0, oppdaterte.getValue(identOgYtelse).first().forrigePeriodeOffset) + assertEquals(null, oppdaterte.getValue(identOgYtelsePerson2).first().periodeOffset) + assertEquals(null, oppdaterte.getValue(identOgYtelsePerson2).first().forrigePeriodeOffset) + } +} + +fun Collection.forIverksetting() = + AndelTilkjentYtelseForIverksettingFactory().pakkInnForUtbetaling(this) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/DiskresjonskodeUtilsTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/DiskresjonskodeUtilsTest.kt new file mode 100644 index 000000000..92b8fed8f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/DiskresjonskodeUtilsTest.kt @@ -0,0 +1,69 @@ +package no.nav.familie.ba.sak.kjerne.arbeidsfordeling + +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService.IdentMedAdressebeskyttelse +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class DiskresjonskodeUtilsTest { + val personUtenDiskresjonskode = IdentMedAdressebeskyttelse( + ident = "IDENT_UTEN", + adressebeskyttelsegradering = null, + ) + val personFortrolig = IdentMedAdressebeskyttelse( + ident = "IDENT_FORTROLIG", + adressebeskyttelsegradering = ADRESSEBESKYTTELSEGRADERING.FORTROLIG, + ) // Kode 7 + val personStrengtFortrolig = IdentMedAdressebeskyttelse( + ident = "IDENT_STRENGT_FORTROLIG", + adressebeskyttelsegradering = ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG, + ) // Kode 6 + + @Test + fun `ingen har adressebeskyttelse - skal gi null`() { + assertEquals( + null, + finnPersonMedStrengesteAdressebeskyttelse( + listOf( + personUtenDiskresjonskode, + personUtenDiskresjonskode, + ), + ), + ) + } + + @Test + fun `en har adressebeskyttelse STRENGT_FORTROLIG, en har adressebeskyttelse FORTROLIG, en har ingen adressebeskyttelse - skal gi adressebeskyttelse STRENGT_FORTROLIG`() { + assertEquals( + "IDENT_STRENGT_FORTROLIG", + finnPersonMedStrengesteAdressebeskyttelse( + listOf( + personFortrolig, + personStrengtFortrolig, + personUtenDiskresjonskode, + ), + ), + ) + } + + @Test + fun `en har adressebeskyttelse FORTROLIG, en har ingen adressebeskyttelse - skal gi adressebeskyttelse FORTROLIG`() { + assertEquals( + "IDENT_FORTROLIG", + finnPersonMedStrengesteAdressebeskyttelse( + listOf( + personUtenDiskresjonskode, + personFortrolig, + ), + ), + ) + } + + @Test + fun `tom liste - skal gi null`() { + assertEquals( + null, + finnPersonMedStrengesteAdressebeskyttelse(listOf()), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/AutobrevStegServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/AutobrevStegServiceTest.kt new file mode 100644 index 000000000..5de3398ec --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/AutobrevStegServiceTest.kt @@ -0,0 +1,99 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.AutovedtakFødselshendelseService +import no.nav.familie.ba.sak.kjerne.autovedtak.omregning.AutovedtakBrevService +import no.nav.familie.ba.sak.kjerne.autovedtak.småbarnstillegg.AutovedtakSmåbarnstilleggService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.prosessering.error.RekjørSenereException +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class AutobrevStegServiceTest { + private val fagsakService = mockk() + private val behandlingHentOgPersisterService = mockk() + private val oppgaveService = mockk() + private val autovedtakFødselshendelseService = mockk() + private val autovedtakBrevService = mockk() + private val autovedtakSmåbarnstilleggService = mockk() + + val autovedtakStegService = AutovedtakStegService( + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + oppgaveService = oppgaveService, + autovedtakFødselshendelseService = autovedtakFødselshendelseService, + autovedtakBrevService = autovedtakBrevService, + autovedtakSmåbarnstilleggService = autovedtakSmåbarnstilleggService, + ) + + @Test + fun `Skal stoppe autovedtak og opprette oppgave ved åpen behandling som utredes`() { + val aktør = randomAktør() + val fagsak = defaultFagsak(aktør) + val behandling = lagBehandling(fagsak).also { + it.status = BehandlingStatus.UTREDES + } + + every { autovedtakSmåbarnstilleggService.skalAutovedtakBehandles(SmåbarnstilleggData(aktør)) } returns true + every { fagsakService.hentNormalFagsak(aktør) } returns fagsak + every { behandlingHentOgPersisterService.finnAktivOgÅpenForFagsak(fagsakId = fagsak.id) } returns behandling + every { oppgaveService.opprettOppgaveForManuellBehandling(any(), any(), any(), any()) } returns "" + + autovedtakStegService.kjørBehandlingSmåbarnstillegg( + mottakersAktør = aktør, + aktør = aktør, + ) + + verify(exactly = 1) { oppgaveService.opprettOppgaveForManuellBehandling(any(), any(), any(), any()) } + } + + @Test + fun `Skal stoppe autovedtak og opprette oppgave ved åpen behandling med status Fatter vedtak`() { + val aktør = randomAktør() + val fagsak = defaultFagsak(aktør) + val behandling = lagBehandling(fagsak).also { + it.status = BehandlingStatus.FATTER_VEDTAK + } + + every { autovedtakSmåbarnstilleggService.skalAutovedtakBehandles(SmåbarnstilleggData(aktør)) } returns true + every { fagsakService.hentNormalFagsak(aktør) } returns fagsak + every { behandlingHentOgPersisterService.finnAktivOgÅpenForFagsak(fagsakId = fagsak.id) } returns behandling + every { oppgaveService.opprettOppgaveForManuellBehandling(any(), any(), any(), any()) } returns "" + + autovedtakStegService.kjørBehandlingSmåbarnstillegg( + mottakersAktør = aktør, + aktør = aktør, + ) + + verify(exactly = 1) { oppgaveService.opprettOppgaveForManuellBehandling(any(), any(), any(), any()) } + } + + @Test + fun `Skal stoppe autovedtak ved å kaste feil ved åpen behandling som iverksettes`() { + val aktør = randomAktør() + val fagsak = defaultFagsak(aktør) + val behandling = lagBehandling(fagsak).also { + it.status = BehandlingStatus.IVERKSETTER_VEDTAK + } + + every { autovedtakSmåbarnstilleggService.skalAutovedtakBehandles(SmåbarnstilleggData(aktør)) } returns true + every { fagsakService.hentNormalFagsak(aktør) } returns fagsak + every { behandlingHentOgPersisterService.finnAktivOgÅpenForFagsak(fagsakId = fagsak.id) } returns behandling + every { oppgaveService.opprettOppgaveForManuellBehandling(any(), any(), any(), any()) } returns "" + + assertThrows { + autovedtakStegService.kjørBehandlingSmåbarnstillegg( + mottakersAktør = aktør, + aktør = aktør, + ) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/FlerlingUtilsTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/FlerlingUtilsTest.kt" new file mode 100644 index 000000000..2cbd545f7 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/FlerlingUtilsTest.kt" @@ -0,0 +1,307 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse + +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class FlerlingUtilsTest { + + @Test + fun `Skal behandle 1 barn når mor ikke har andre barn`() { + val morsIdent = randomFnr() + val barn = randomFnr() + val (barnSomSkalBehandlesForMor, _) = finnBarnSomSkalBehandlesForMor( + nyBehandlingHendelse = NyBehandlingHendelse(morsIdent = morsIdent, barnasIdenter = listOf(barn)), + barnaTilMor = listOf( + ForelderBarnRelasjon( + aktør = tilAktør(barn), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now(), + ), + ), + barnaSomHarBlittBehandlet = emptyList(), + ) + + assertEquals(1, barnSomSkalBehandlesForMor.size) + assertTrue(barnSomSkalBehandlesForMor.contains(barn)) + } + + @Test + fun `Skal behandle 1 barn når mor har andre barn`() { + val morsIdent = randomFnr() + val barn = randomFnr() + val barn2 = randomFnr() + val (barnSomSkalBehandlesForMor, _) = finnBarnSomSkalBehandlesForMor( + nyBehandlingHendelse = NyBehandlingHendelse(morsIdent = morsIdent, barnasIdenter = listOf(barn)), + barnaTilMor = listOf( + ForelderBarnRelasjon( + aktør = tilAktør(barn), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now(), + ), + ForelderBarnRelasjon( + aktør = tilAktør(barn2), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now().minusYears(2), + ), + ), + barnaSomHarBlittBehandlet = listOf(barn2), + ) + + assertEquals(1, barnSomSkalBehandlesForMor.size) + assertTrue(barnSomSkalBehandlesForMor.contains(barn)) + } + + @Test + fun `Skal behandle 0 barn når mor har tvillinger og de allerede er behandlet`() { + val morsIdent = randomFnr() + val barn = randomFnr() + val barn2 = randomFnr() + val (barnSomSkalBehandlesForMor, alleBarnSomKanBehandles) = finnBarnSomSkalBehandlesForMor( + nyBehandlingHendelse = NyBehandlingHendelse(morsIdent = morsIdent, barnasIdenter = listOf(barn2)), + barnaTilMor = listOf( + ForelderBarnRelasjon( + aktør = tilAktør(barn), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now(), + ), + ForelderBarnRelasjon( + aktør = tilAktør(barn2), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now().minusYears(2), + ), + ), + barnaSomHarBlittBehandlet = listOf(barn, barn2), + ) + + assertEquals(0, barnSomSkalBehandlesForMor.size) + assertEquals(1, alleBarnSomKanBehandles.size) + } + + @Test + fun `Skal behandle 2 barn når hendelse inneholder flere barn og mor har ikke andre barn selv om barn ikke er definert som flerling`() { + val morsIdent = randomFnr() + val barn = randomFnr() + val barn2 = randomFnr() + val (barnSomSkalBehandlesForMor, _) = finnBarnSomSkalBehandlesForMor( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = morsIdent, + barnasIdenter = listOf(barn, barn2), + ), + barnaTilMor = listOf( + ForelderBarnRelasjon( + aktør = tilAktør(barn), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now(), + ), + ForelderBarnRelasjon( + aktør = tilAktør(barn2), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now().minusYears(2), + ), + ), + barnaSomHarBlittBehandlet = emptyList(), + ) + + assertEquals(2, barnSomSkalBehandlesForMor.size) + assertTrue(barnSomSkalBehandlesForMor.contains(barn)) + assertTrue(barnSomSkalBehandlesForMor.contains(barn2)) + } + + @Test + fun `Skal behandle 2 barn når mor har fått tvilling på samme dag og hendelse inneholder 1 barn`() { + val morsIdent = randomFnr() + val barn = randomFnr() + val barn2 = randomFnr() + val (barnSomSkalBehandlesForMor, _) = finnBarnSomSkalBehandlesForMor( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = morsIdent, + barnasIdenter = listOf(barn), + ), + barnaTilMor = listOf( + ForelderBarnRelasjon( + aktør = tilAktør(barn), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now(), + ), + ForelderBarnRelasjon( + aktør = tilAktør(barn2), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now(), + ), + ), + barnaSomHarBlittBehandlet = emptyList(), + ) + + assertEquals(2, barnSomSkalBehandlesForMor.size) + assertTrue(barnSomSkalBehandlesForMor.contains(barn)) + assertTrue(barnSomSkalBehandlesForMor.contains(barn2)) + } + + @Test + fun `Skal behandle 4 barn når mor har fått firlinger på samme dag og hendelse inneholder 1 barn`() { + val morsIdent = randomFnr() + val barn = randomFnr() + val barn2 = randomFnr() + val barn3 = randomFnr() + val barn4 = randomFnr() + val barn5 = randomFnr() + val (barnSomSkalBehandlesForMor, _) = finnBarnSomSkalBehandlesForMor( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = morsIdent, + barnasIdenter = listOf(barn), + ), + barnaTilMor = listOf( + ForelderBarnRelasjon( + aktør = tilAktør(barn), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now(), + ), + ForelderBarnRelasjon( + aktør = tilAktør(barn2), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now(), + ), + ForelderBarnRelasjon( + aktør = tilAktør(barn3), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now(), + ), + ForelderBarnRelasjon( + aktør = tilAktør(barn4), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now(), + ), + ), + barnaSomHarBlittBehandlet = listOf(barn5), + ) + + assertEquals(4, barnSomSkalBehandlesForMor.size) + assertTrue(barnSomSkalBehandlesForMor.contains(barn)) + assertTrue(barnSomSkalBehandlesForMor.contains(barn2)) + assertTrue(barnSomSkalBehandlesForMor.contains(barn3)) + assertTrue(barnSomSkalBehandlesForMor.contains(barn4)) + } + + @Test + fun `Skal behandle 2 barn når mor har fått tvilling med 1 dag mellomrom og hendelse inneholder 1 barn født dagen etter tvilling`() { + val morsIdent = randomFnr() + val barn = randomFnr() + val barn2 = randomFnr() + val (barnSomSkalBehandlesForMor, _) = finnBarnSomSkalBehandlesForMor( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = morsIdent, + barnasIdenter = listOf(barn), + ), + barnaTilMor = listOf( + ForelderBarnRelasjon( + aktør = tilAktør(barn), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now(), + ), + ForelderBarnRelasjon( + aktør = tilAktør(barn2), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now().minusDays(1), + ), + ), + barnaSomHarBlittBehandlet = emptyList(), + ) + + assertEquals(2, barnSomSkalBehandlesForMor.size) + assertTrue(barnSomSkalBehandlesForMor.contains(barn)) + assertTrue(barnSomSkalBehandlesForMor.contains(barn2)) + } + + @Test + fun `Skal behandle 2 barn når mor har fått tvilling med 1 dag mellomrom og hendelse inneholder 1 barn født dagen før tvilling`() { + val morsIdent = randomFnr() + val barn = randomFnr() + val barn2 = randomFnr() + val (barnSomSkalBehandlesForMor, _) = finnBarnSomSkalBehandlesForMor( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = morsIdent, + barnasIdenter = listOf(barn), + ), + barnaTilMor = listOf( + ForelderBarnRelasjon( + aktør = tilAktør(barn), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now(), + ), + ForelderBarnRelasjon( + aktør = tilAktør(barn2), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + fødselsdato = LocalDate.now().plusDays(1), + ), + ), + barnaSomHarBlittBehandlet = emptyList(), + ) + + assertEquals(2, barnSomSkalBehandlesForMor.size) + assertTrue(barnSomSkalBehandlesForMor.contains(barn)) + assertTrue(barnSomSkalBehandlesForMor.contains(barn2)) + } + + @Test + fun `Skal lage oppgave fordi barnet i hendelse ikke behandles på åpen behandling`() { + val barn = randomAktør() + val barn2 = randomAktør() + + assertFalse( + barnPåHendelseBlirAlleredeBehandletIÅpenBehandling( + barnaPåHendelse = listOf(barn), + barnaPåÅpenBehandling = listOf(barn2), + ), + ) + } + + @Test + fun `Skal ignorere hendelse fordi barnet i hendelse behandles på åpen behandling`() { + val barn = randomAktør() + val barn2 = randomAktør() + + assertTrue( + barnPåHendelseBlirAlleredeBehandletIÅpenBehandling( + barnaPåHendelse = listOf(barn), + barnaPåÅpenBehandling = listOf(barn, barn2), + ), + ) + } + + @Test + fun `Skal ignorere hendelse fordi barna i hendelse behandles på åpen behandling`() { + val barn = randomAktør() + val barn2 = randomAktør() + val barn3 = randomAktør() + + assertTrue( + barnPåHendelseBlirAlleredeBehandletIÅpenBehandling( + barnaPåHendelse = listOf(barn, barn2), + barnaPåÅpenBehandling = listOf(barn, barn2, barn3), + ), + ) + } + + @Test + fun `Skal lage oppgave fordi kun 1 av barna i hendelse behandles på åpen behandling`() { + val barn = randomAktør() + val barn2 = randomAktør() + val barn3 = randomAktør() + + assertFalse( + barnPåHendelseBlirAlleredeBehandletIÅpenBehandling( + barnaPåHendelse = listOf(barn, barn2), + barnaPåÅpenBehandling = listOf(barn, barn3), + ), + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/F\303\270dselshendelseServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/F\303\270dselshendelseServiceTest.kt" new file mode 100644 index 000000000..557f05066 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/F\303\270dselshendelseServiceTest.kt" @@ -0,0 +1,248 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse + +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.common.tilPersonEnkel +import no.nav.familie.ba.sak.config.IntegrasjonClientMock +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagVilkårsvurderingMedOverstyrendeResultater +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakService +import no.nav.familie.ba.sak.kjerne.autovedtak.FødselshendelseData +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.FiltreringsreglerService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.StatsborgerskapService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.ba.sak.task.dto.ManuellOppgaveType +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.Month + +class FødselshendelseServiceTest { + val filtreringsreglerService = mockk() + val taskRepository = mockk() + val behandlingRepository = mockk() + val fagsakService = mockk() + val behandlingHentOgPersisterService = mockk() + val vilkårsvurderingRepository = mockk() + val persongrunnlagService = mockk() + val personidentService = mockk() + val stegService = mockk() + val vedtakService = mockk() + val vedtaksperiodeService = mockk() + val autovedtakService = mockk() + val personopplysningerService = mockk() + val opprettTaskService = mockk() + val oppgaveService = mockk() + + val integrasjonClient = mockk() + val statsborgerskapService = StatsborgerskapService( + integrasjonClient = integrasjonClient, + ) + + private val autovedtakFødselshendelseService = AutovedtakFødselshendelseService( + fagsakService, + behandlingHentOgPersisterService, + filtreringsreglerService, + taskRepository, + vilkårsvurderingRepository, + persongrunnlagService, + personidentService, + stegService, + vedtakService, + vedtaksperiodeService, + autovedtakService, + personopplysningerService, + statsborgerskapService, + opprettTaskService, + oppgaveService, + ) + + @Test + fun `Skal opprette fremleggsoppgave dersom søker er EØS medlem`() { + every { personopplysningerService.hentGjeldendeStatsborgerskap(any()) } returns Statsborgerskap( + land = "POL", + gyldigFraOgMed = LocalDate.now().minusMonths(2), + gyldigTilOgMed = null, + bekreftelsesdato = null, + ) + every { integrasjonClient.hentAlleEØSLand() } returns IntegrasjonClientMock.hentKodeverkLand() + every { opprettTaskService.opprettOppgaveTask(any(), any(), any(), any()) } just runs + + autovedtakFødselshendelseService.opprettFremleggsoppgaveDersomEØSMedlem(lagBehandling()) + + verify(exactly = 1) { + opprettTaskService.opprettOppgaveTask( + behandlingId = any(), + oppgavetype = Oppgavetype.Fremlegg, + beskrivelse = "Kontroller gyldig opphold", + fristForFerdigstillelse = LocalDate.now().plusYears(1), + ) + } + } + + @Test + fun `Skal ikke opprette fremleggsoppgave dersom søker er nordisk medlem`() { + every { personopplysningerService.hentGjeldendeStatsborgerskap(any()) } returns Statsborgerskap( + land = "DNK", + gyldigFraOgMed = LocalDate.now().minusMonths(2), + gyldigTilOgMed = null, + bekreftelsesdato = null, + ) + every { integrasjonClient.hentAlleEØSLand() } returns IntegrasjonClientMock.hentKodeverkLand() + every { opprettTaskService.opprettOppgaveTask(any(), any(), any(), any()) } just runs + + autovedtakFødselshendelseService.opprettFremleggsoppgaveDersomEØSMedlem(lagBehandling()) + + verify(exactly = 0) { + opprettTaskService.opprettOppgaveTask( + behandlingId = any(), + oppgavetype = Oppgavetype.Fremlegg, + beskrivelse = "Kontroller gyldig opphold", + fristForFerdigstillelse = LocalDate.now().plusYears(1), + ) + } + } + + @Test + fun `Skal opprette manuell oppgave hvis resultat av fødselshendelse blir INNVILGET_OG_ENDRET`() { + val søkerPerson = lagPerson(type = PersonType.SØKER) + val fagsak = defaultFagsak(aktør = søkerPerson.aktør) + + val søkerAktør = fagsak.aktør + val søker = søkerAktør.aktivFødselsnummer() + val barn1Person = lagPerson(type = PersonType.BARN) + val barn1 = barn1Person.aktør.aktivFødselsnummer() + val barn2Person = lagPerson(type = PersonType.BARN) + val barn2 = barn2Person.aktør.aktivFødselsnummer() + val nyBehandlingHendelse = NyBehandlingHendelse(søker, listOf(barn2)) + + every { fagsakService.hentNormalFagsak(søkerAktør) } returns fagsak + every { + fagsakService.hentEllerOpprettFagsakForPersonIdent( + søker, + true, + FagsakType.NORMAL, + null, + ) + } returns fagsak + + val forrigeBehandling = lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.REVURDERING, + årsak = BehandlingÅrsak.NYE_OPPLYSNINGER, + førsteSteg = StegType.BEHANDLING_AVSLUTTET, + resultat = Behandlingsresultat.OPPHØRT, + status = BehandlingStatus.AVSLUTTET, + ) + val nyBehandling = lagBehandling( + fagsak, + behandlingKategori = BehandlingKategori.NASJONAL, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak = BehandlingÅrsak.FØDSELSHENDELSE, + skalBehandlesAutomatisk = true, + ) + every { behandlingHentOgPersisterService.hent(forrigeBehandling.id) } returns forrigeBehandling + every { behandlingHentOgPersisterService.lagreEllerOppdater(forrigeBehandling) } returns forrigeBehandling + every { behandlingHentOgPersisterService.hentBehandlinger(fagsak.id) } returns listOf(forrigeBehandling) + every { behandlingHentOgPersisterService.hent(nyBehandling.id) } returns nyBehandling + every { stegService.opprettNyBehandlingOgRegistrerPersongrunnlagForFødselhendelse(nyBehandlingHendelse) } returns nyBehandling + every { + stegService.håndterFiltreringsreglerForFødselshendelser( + nyBehandling, + nyBehandlingHendelse, + ) + } returns nyBehandling.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING) + every { stegService.håndterVilkårsvurdering(nyBehandling) } returns nyBehandling.copy(resultat = Behandlingsresultat.INNVILGET_OG_ENDRET) + .leggTilBehandlingStegTilstand(StegType.IVERKSETT_MOT_OPPDRAG) + every { stegService.håndterHenleggBehandling(any(), any()) } returns nyBehandling + every { oppgaveService.opprettOppgaveForManuellBehandling(any(), any(), any(), any()) } returns "" + every { persongrunnlagService.hentSøker(nyBehandling.id) } returns søkerPerson + every { persongrunnlagService.hentBarna(nyBehandling) } returns listOf(barn1Person, barn2Person) + + every { persongrunnlagService.hentSøkerOgBarnPåBehandlingThrows(nyBehandling.id) } returns listOf( + barn1Person.tilPersonEnkel(), + barn2Person.tilPersonEnkel(), + søkerPerson.tilPersonEnkel(), + ) + + every { personidentService.hentAktør(søker) } returns søkerAktør + every { personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(søkerAktør) } returns PersonInfo( + fødselsdato = LocalDate.of(1990, Month.JANUARY, 5), + navn = "Mor Mocksen", + kjønn = Kjønn.KVINNE, + forelderBarnRelasjon = setOf( + ForelderBarnRelasjon(aktør = tilAktør(barn1), relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN), + ForelderBarnRelasjon(aktør = tilAktør(barn2), relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN), + ), + ) + every { persongrunnlagService.hentBarna(forrigeBehandling) } returns listOf( + barn1Person, + ) + every { persongrunnlagService.hentSøkerOgBarnPåBehandlingThrows(forrigeBehandling.id) } returns listOf( + barn1Person.tilPersonEnkel(), + ) + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(nyBehandling.id) } returns lagVilkårsvurderingMedOverstyrendeResultater( + søkerPerson, + listOf(barn1Person, barn2Person), + nyBehandling, + id = 1, + mapOf( + Pair( + barn1Person.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.IKKE_OPPFYLT, + behandlingId = nyBehandling.id, + ), + ), + ), + ), + ) + + autovedtakFødselshendelseService.kjørBehandling(FødselshendelseData(nyBehandlingHendelse)) + verify(exactly = 0) { + opprettTaskService.opprettOppgaveForManuellBehandlingTask( + behandlingId = any(), + beskrivelse = "Fødselshendelse: Barnet (fødselsdato: ${barn1Person.fødselsdato.tilKortString()}) er ikke bosatt med mor.", + manuellOppgaveType = ManuellOppgaveType.FØDSELSHENDELSE, + ) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/PdlDatagenerator.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/PdlDatagenerator.kt" new file mode 100644 index 000000000..e69de29bb diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/PdlDatageneratorForF\303\270dselshendelse.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/PdlDatageneratorForF\303\270dselshendelse.kt" new file mode 100644 index 000000000..c50a16be3 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/PdlDatageneratorForF\303\270dselshendelse.kt" @@ -0,0 +1,173 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse + +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrMatrikkeladresse +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import no.nav.familie.kontrakter.felles.personopplysning.Sivilstand +import no.nav.familie.kontrakter.felles.personopplysning.Vegadresse +import java.time.LocalDate + +val konstantAdresse: List = + listOf( + Bostedsadresse( + gyldigFraOgMed = null, + gyldigTilOgMed = null, + vegadresse = Vegadresse( + matrikkelId = 6367230663, + husnummer = "36", + husbokstav = "D", + bruksenhetsnummer = null, + adressenavn = "Arnulv Eide -veien", + kommunenummer = "5422", + tilleggsnavn = null, + postnummer = "9050", + ), + ), + ) + +val alternaltivAdresse: List = + listOf( + Bostedsadresse( + gyldigFraOgMed = null, + gyldigTilOgMed = null, + vegadresse = Vegadresse( + matrikkelId = 1111000000, + husnummer = "36", + husbokstav = "D", + bruksenhetsnummer = null, + adressenavn = "IkkeSamme-veien", + kommunenummer = "5423", + tilleggsnavn = null, + postnummer = "9050", + ), + matrikkeladresse = null, + ukjentBosted = null, + ), + ) + +val mockBarnAutomatiskBehandlingFnr = "21131777001" +val mockBarnAutomatiskBehandling = PersonInfo( + fødselsdato = LocalDate.now(), + navn = "ARTIG MIDTPUNKT", + kjønn = Kjønn.KVINNE, + forelderBarnRelasjon = emptySet(), + forelderBarnRelasjonMaskert = emptySet(), + adressebeskyttelseGradering = null, + bostedsadresser = konstantAdresse, + sivilstander = emptyList(), + opphold = emptyList(), + statsborgerskap = emptyList(), +) + +val mockBarnAutomatiskBehandling2Fnr = "21131777002" +val mockBarnAutomatiskBehandling2 = PersonInfo( + fødselsdato = LocalDate.now(), + navn = "ARTIG MIDTPUNKT 2", + kjønn = Kjønn.KVINNE, + forelderBarnRelasjon = emptySet(), + forelderBarnRelasjonMaskert = emptySet(), + adressebeskyttelseGradering = null, + bostedsadresser = konstantAdresse, + sivilstander = emptyList(), + opphold = emptyList(), + statsborgerskap = emptyList(), +) + +val mockBarnAutomatiskBehandlingSkalFeileFnr = "21131777003" +val mockBarnAutomatiskBehandlingSkalFeile = PersonInfo( + fødselsdato = LocalDate.now().minusMonths(2), + navn = "ARTIG MIDTPUNKT 3", + kjønn = Kjønn.KVINNE, + forelderBarnRelasjon = emptySet(), + forelderBarnRelasjonMaskert = emptySet(), + adressebeskyttelseGradering = null, + bostedsadresser = alternaltivAdresse, + sivilstander = emptyList(), + opphold = emptyList(), + statsborgerskap = emptyList(), +) + +val mockSøkerAutomatiskBehandlingFnr = "04136226623" +val mockSøkerAutomatiskBehandlingAktør = tilAktør(mockSøkerAutomatiskBehandlingFnr) + +val mockSøkerAutomatiskBehandling = PersonInfo( + fødselsdato = LocalDate.parse("1962-08-04"), + navn = "LEALAUS GYNGEHEST", + kjønn = Kjønn.KVINNE, + forelderBarnRelasjon = setOf( + ForelderBarnRelasjon( + aktør = tilAktør(mockBarnAutomatiskBehandlingFnr), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + navn = null, + fødselsdato = null, + adressebeskyttelseGradering = + null, + ), + ), + forelderBarnRelasjonMaskert = emptySet(), + adressebeskyttelseGradering = null, + bostedsadresser = konstantAdresse, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UGIFT, gyldigFraOgMed = null)), + opphold = emptyList(), + statsborgerskap = emptyList(), +) + +fun genererAutomatiskTestperson( + fødselsdato: LocalDate = LocalDate.parse("1998-10-10"), + forelderBarnRelasjon: Set = emptySet(), + sivilstander: List = emptyList(), + bostedsadresser: List = konstantAdresse, +): PersonInfo { + return PersonInfo( + fødselsdato = fødselsdato, + navn = "Autogenerert Navn $fødselsdato", + forelderBarnRelasjon = forelderBarnRelasjon.map { + ForelderBarnRelasjon( + aktør = tilAktør(it.aktør.personidenter.first().fødselsnummer), + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + navn = null, + fødselsdato = null, + adressebeskyttelseGradering = + null, + ) + }.toSet(), + sivilstander = sivilstander, + bostedsadresser = bostedsadresser, + ) +} + +val mockNåværendeBosted = GrMatrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", +).apply { + periode = DatoIntervallEntitet(fom = LocalDate.now().minusYears(1)) +} + +val mockAnnetNåværendeBosted = GrMatrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H501", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", +).apply { + periode = DatoIntervallEntitet(fom = LocalDate.now().minusYears(1)) +} + +val mockTidligereBosted = GrMatrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", +).apply { + periode = DatoIntervallEntitet(fom = LocalDate.now().minusYears(3), tom = LocalDate.now().minusYears(1)) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsregelForFlereBarnTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsregelForFlereBarnTest.kt" new file mode 100644 index 000000000..d345d9360 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsregelForFlereBarnTest.kt" @@ -0,0 +1,389 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import no.nav.familie.ba.sak.common.LocalDateService +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagVilkårsvurderingMedOverstyrendeResultater +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.VergeResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlKontaktinformasjonForDødsboAdresse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.erOppfylt +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.domene.FødselshendelsefiltreringResultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.domene.FødselshendelsefiltreringResultatRepository +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.domene.erOppfylt +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValideringService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrBostedsadresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.lagDødsfallFraPdl +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.sivilstand.GrSivilstand +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.tilPerson +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import no.nav.familie.kontrakter.felles.personopplysning.Sivilstand +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.YearMonth + +class FiltreringsregelForFlereBarnTest { + + val barnAktør0 = randomAktør() + val barnAktør1 = randomAktør() + val gyldigAktør = randomAktør() + + val personopplysningGrunnlagRepositoryMock = mockk() + val personopplysningerServiceMock = mockk() + val personidentService = mockk() + val localDateServiceMock = mockk() + val fødselshendelsefiltreringResultatRepository = mockk(relaxed = true) + val vilkårsvurderingRepository = mockk() + val behandlingServiceMock = mockk(relaxed = true) + val behandlingHentOgPersisterService = mockk() + val tilkjentYtelseValideringServiceMock = mockk() + val andelTilkjentYtelseRepository = mockk() + val filtreringsreglerService = FiltreringsreglerService( + personopplysningerService = personopplysningerServiceMock, + personidentService = personidentService, + personopplysningGrunnlagRepository = personopplysningGrunnlagRepositoryMock, + localDateService = localDateServiceMock, + fødselshendelsefiltreringResultatRepository = fødselshendelsefiltreringResultatRepository, + behandlingService = behandlingServiceMock, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + tilkjentYtelseValideringService = tilkjentYtelseValideringServiceMock, + vilkårsvurderingRepository = vilkårsvurderingRepository, + andelTilkjentYtelseRepository = andelTilkjentYtelseRepository, + ) + + init { + val fødselshendelsefiltreringResultatSlot = slot>() + every { fødselshendelsefiltreringResultatRepository.saveAll(capture(fødselshendelsefiltreringResultatSlot)) } answers { + fødselshendelsefiltreringResultatSlot.captured + } + } + + @Test + fun `Regelevaluering skal resultere i NEI når det har gått mellom fem dager og fem måneder siden forrige minst ett barn ble født`() { + val evalueringer = FiltreringsregelEvaluering.evaluerFiltreringsregler( + genererFaktaMedTidligereBarn(1, 3, 7, 0), + ) + + Assertions.assertThat(evalueringer.erOppfylt()).isFalse + Assertions.assertThat( + evalueringer + .filter { it.resultat == Resultat.IKKE_OPPFYLT } + .any { it.identifikator == Filtreringsregel.MER_ENN_5_MND_SIDEN_FORRIGE_BARN.name }, + ) + } + + @Test + fun `Regelevaluering skal resultere i JA når det har ikke gått mellom fem dager og fem måneder siden forrige minst ett barn ble født`() { + val evalueringer = FiltreringsregelEvaluering.evaluerFiltreringsregler( + genererFaktaMedTidligereBarn(0, 0, 0, 5), + ) + + Assertions.assertThat(evalueringer.erOppfylt()).isTrue + } + + @Test + fun `Regelevaluering skal resultere i NEI når det er registrert dødsfall på minst ett barn`() { + val behandling = lagBehandling() + val personInfo = generePersonInfoMedBarn(setOf(barnAktør0, barnAktør1)) + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id, aktiv = true).apply { + personer.addAll( + listOf( + genererPerson( + type = PersonType.SØKER, + personopplysningGrunnlag = this, + aktør = gyldigAktør, + ), + genererPerson( + type = PersonType.BARN, + personopplysningGrunnlag = this, + aktør = barnAktør0, + fødselsDato = LocalDate.now().minusMonths(1), + dødsfallDato = LocalDate.now().toString(), + ), + genererPerson( + type = PersonType.BARN, + personopplysningGrunnlag = this, + aktør = barnAktør1, + fødselsDato = LocalDate.now().minusMonths(1), + ), + ), + ) + } + + every { personopplysningGrunnlagRepositoryMock.findByBehandlingAndAktiv(any()) } returns personopplysningGrunnlag + + every { personopplysningerServiceMock.hentPersoninfoMedRelasjonerOgRegisterinformasjon(gyldigAktør) } returns personInfo + + every { personopplysningerServiceMock.harVerge(gyldigAktør) } returns VergeResponse(harVerge = false) + + every { localDateServiceMock.now() } returns LocalDate.now().withDayOfMonth(15) + + every { personidentService.hentAktør(gyldigAktør.aktivFødselsnummer()) } returns gyldigAktør + + val andelTilkjentytelse = listOf( + MånedPeriode(YearMonth.of(2018, 1), YearMonth.now().plusYears(1)), + ) + .map { + lagAndelTilkjentYtelse(it.fom, it.tom) + } + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(any()) } returns andelTilkjentytelse + + every { + personidentService.hentAktørIder( + listOf( + barnAktør0.aktivFødselsnummer(), + barnAktør1.aktivFødselsnummer(), + ), + ) + } returns listOf(barnAktør0, barnAktør1) + + every { tilkjentYtelseValideringServiceMock.barnetrygdLøperForAnnenForelder(any(), any()) } returns false + + val sisteVedtatteBehandling = lagBehandling() + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(behandling.fagsak.id) } returns sisteVedtatteBehandling + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(sisteVedtatteBehandling.id) } returns lagVilkårsvurderingMedOverstyrendeResultater( + gyldigAktør.tilPerson(personopplysningGrunnlag)!!, + listOf(barnAktør0.tilPerson(personopplysningGrunnlag)!!, barnAktør1.tilPerson(personopplysningGrunnlag)!!), + behandling, + overstyrendeVilkårResultater = mapOf( + Pair( + gyldigAktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2020, 11, 1), + periodeTom = LocalDate.of(2021, 2, 1), + ), + ), + ), + ), + ) + + val fødselshendelsefiltreringResultater = filtreringsreglerService.kjørFiltreringsregler( + NyBehandlingHendelse( + morsIdent = gyldigAktør.aktivFødselsnummer(), + barnasIdenter = listOf( + barnAktør0.aktivFødselsnummer(), + barnAktør1.aktivFødselsnummer(), + ), + ), + behandling, + ) + + Assertions.assertThat(fødselshendelsefiltreringResultater.erOppfylt()).isFalse + Assertions.assertThat( + fødselshendelsefiltreringResultater + .filter { it.resultat == Resultat.IKKE_OPPFYLT } + .any { it.filtreringsregel == Filtreringsregel.BARN_LEVER }, + ) + } + + @Test + fun `Regelevaluering skal resultere i JA når alle filtreringsregler er oppfylt`() { + val behandling = lagBehandling() + val personInfo = generePersonInfoMedBarn(setOf(barnAktør0, barnAktør1)) + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id, aktiv = true).apply { + personer.addAll( + listOf( + genererPerson( + type = PersonType.SØKER, + personopplysningGrunnlag = this, + aktør = gyldigAktør, + ), + genererPerson( + type = PersonType.BARN, + personopplysningGrunnlag = this, + aktør = barnAktør0, + fødselsDato = LocalDate.now().minusMonths(1), + ), + genererPerson( + type = PersonType.BARN, + personopplysningGrunnlag = this, + aktør = barnAktør1, + fødselsDato = LocalDate.now().minusMonths(1), + ), + ), + ) + } + + every { personopplysningGrunnlagRepositoryMock.findByBehandlingAndAktiv(any()) } returns personopplysningGrunnlag + + every { personopplysningerServiceMock.hentPersoninfoMedRelasjonerOgRegisterinformasjon(gyldigAktør) } returns personInfo + + every { personopplysningerServiceMock.harVerge(gyldigAktør) } returns VergeResponse(harVerge = false) + + every { localDateServiceMock.now() } returns LocalDate.now().withDayOfMonth(20) + + every { personidentService.hentAktør(gyldigAktør.aktivFødselsnummer()) } returns gyldigAktør + every { + personidentService.hentAktørIder( + listOf( + barnAktør0.aktivFødselsnummer(), + barnAktør1.aktivFødselsnummer(), + ), + ) + } returns listOf(barnAktør0, barnAktør1) + + every { tilkjentYtelseValideringServiceMock.barnetrygdLøperForAnnenForelder(any(), any()) } returns false + + val andelTilkjentytelse = listOf( + MånedPeriode(YearMonth.of(2018, 1), YearMonth.now().plusYears(1)), + ) + .map { + lagAndelTilkjentYtelse(it.fom, it.tom) + } + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(any()) } returns andelTilkjentytelse + + val sisteVedtatteBehandling = lagBehandling() + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(behandling.fagsak.id) } returns sisteVedtatteBehandling + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(sisteVedtatteBehandling.id) } returns lagVilkårsvurderingMedOverstyrendeResultater( + gyldigAktør.tilPerson(personopplysningGrunnlag)!!, + listOf(barnAktør0.tilPerson(personopplysningGrunnlag)!!, barnAktør1.tilPerson(personopplysningGrunnlag)!!), + behandling, + overstyrendeVilkårResultater = mapOf( + Pair( + gyldigAktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2020, 11, 1), + periodeTom = LocalDate.of(2021, 2, 1), + ), + ), + ), + ), + ) + + val fødselshendelsefiltreringResultater = filtreringsreglerService.kjørFiltreringsregler( + NyBehandlingHendelse( + morsIdent = gyldigAktør.aktivFødselsnummer(), + barnasIdenter = listOf( + barnAktør0.aktivFødselsnummer(), + barnAktør1.aktivFødselsnummer(), + ), + ), + behandling, + ) + + Assertions.assertThat(fødselshendelsefiltreringResultater.erOppfylt()).isTrue + } + + private fun genererPerson( + type: PersonType, + personopplysningGrunnlag: PersonopplysningGrunnlag, + aktør: Aktør, + fødselsDato: LocalDate? = null, + grBostedsadresse: GrBostedsadresse? = null, + kjønn: Kjønn = Kjønn.KVINNE, + sivilstand: SIVILSTAND = SIVILSTAND.UGIFT, + dødsfallDato: String? = null, + ): Person { + return Person( + aktør = aktør, + type = type, + personopplysningGrunnlag = personopplysningGrunnlag, + fødselsdato = fødselsDato ?: LocalDate.of(1991, 1, 1), + navn = "navn", + kjønn = kjønn, + bostedsadresser = grBostedsadresse?.let { mutableListOf(grBostedsadresse) } ?: mutableListOf(), + ) + .apply { + this.sivilstander = mutableListOf(GrSivilstand(type = sivilstand, person = this)) + if (dødsfallDato != null) { + this.dødsfall = lagDødsfallFraPdl( + person = this, + dødsfallDatoFraPdl = dødsfallDato, + dødsfallAdresseFraPdl = PdlKontaktinformasjonForDødsboAdresse( + adresselinje1 = "Gate 1", + postnummer = "1234", + poststedsnavn = "Oslo", + ), + ) + } + } + } + + private fun generePersonInfoMedBarn( + barn: Set? = null, + navn: String = "Noname", + fødselsDato: LocalDate? = null, + adressebeskyttelsegradering: ADRESSEBESKYTTELSEGRADERING = ADRESSEBESKYTTELSEGRADERING.UGRADERT, + bostedsadresse: Bostedsadresse? = null, + sivilstand: SIVILSTAND = SIVILSTAND.UGIFT, + ): PersonInfo { + return PersonInfo( + fødselsdato = fødselsDato ?: LocalDate.now().minusYears(20), + navn = navn, + adressebeskyttelseGradering = adressebeskyttelsegradering, + bostedsadresser = bostedsadresse?.let { mutableListOf(it) } ?: mutableListOf(Bostedsadresse()), + sivilstander = listOf(Sivilstand(type = sivilstand)), + forelderBarnRelasjon = barn?.map { + ForelderBarnRelasjon( + aktør = it, + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + navn = "navn $it", + ) + }?.toSet() ?: emptySet(), + ) + } + + private fun genererFaktaMedTidligereBarn( + manaderFodselEtt: Long, + manaderFodselTo: Long, + manaderFodselForrigeFodsel: Long, + dagerFodselForrigeFodsel: Long, + ): FiltreringsreglerFakta { + val mor = tilfeldigPerson(LocalDate.now().minusYears(20)).copy(aktør = gyldigAktør) + val barn = listOf( + tilfeldigPerson(LocalDate.now().minusMonths(manaderFodselEtt)).copy(aktør = barnAktør0), + tilfeldigPerson(LocalDate.now().minusMonths(manaderFodselTo)).copy(aktør = barnAktør1), + ) + + val restenAvBarna: List = listOf( + PersonInfo(LocalDate.now().minusMonths(manaderFodselForrigeFodsel).minusDays(dagerFodselForrigeFodsel)), + ) + + return FiltreringsreglerFakta( + mor = mor, + barnaFraHendelse = barn, + restenAvBarna = restenAvBarna, + morLever = true, + barnaLever = true, + morHarVerge = false, + dagensDato = LocalDate.now(), + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsregelTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsregelTest.kt" new file mode 100644 index 000000000..89acf871b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsregelTest.kt" @@ -0,0 +1,714 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler + +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.tilfeldigSøker +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Evaluering +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.erOppfylt +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate + +internal class FiltreringsregelTest { + + private val gyldigAktørId = randomAktør() + + @Test + fun `Regelevaluering skal resultere i Ja`() { + val mor = tilfeldigPerson(LocalDate.now().minusYears(20)).copy(aktør = gyldigAktørId) + val barnet = tilfeldigPerson(LocalDate.now()).copy(aktør = gyldigAktørId) + val restenAvBarna: List = listOf() + + val evalueringer = FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = mor, + barnaFraHendelse = listOf(barnet), + restenAvBarna = restenAvBarna, + morLever = true, + barnaLever = true, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + + assertThat(evalueringer.erOppfylt()).isTrue + } + + @Test + fun `Regelevaluering skal resultere i NEI når mor mottar utvidet barnetrygd`() { + val mor = tilfeldigPerson(LocalDate.now().minusYears(20)).copy(aktør = gyldigAktørId) + val barnet = tilfeldigPerson(LocalDate.now()).copy(aktør = gyldigAktørId) + val restenAvBarna: List = listOf() + + val evalueringer = FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = mor, + morMottarLøpendeUtvidet = true, + barnaFraHendelse = listOf(barnet), + restenAvBarna = restenAvBarna, + morLever = true, + barnaLever = true, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + + ), + ) + + assertThat(evalueringer.erOppfylt()).isFalse + assertEnesteRegelMedResultatNei(evalueringer, Filtreringsregel.MOR_MOTTAR_IKKE_LØPENDE_UTVIDET) + } + + @Test + fun `Regelevaluering skal gi resultat IKKE_OPPFYLT når mor har løpende EØS-barnetrygd`() { + val mor = tilfeldigPerson(LocalDate.now().minusYears(20)).copy(aktør = gyldigAktørId) + val barnet = tilfeldigPerson(LocalDate.now()).copy(aktør = gyldigAktørId) + val restenAvBarna: List = listOf() + + val evalueringer = FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = mor, + morMottarLøpendeUtvidet = false, + barnaFraHendelse = listOf(barnet), + restenAvBarna = restenAvBarna, + morLever = true, + barnaLever = true, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morMottarEøsBarnetrygd = true, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + + assertThat(evalueringer.erOppfylt()).isFalse + assertEnesteRegelMedResultatNei(evalueringer, Filtreringsregel.MOR_HAR_IKKE_LØPENDE_EØS_BARNETRYGD) + } + + @Test + fun `Regelevaluering skal resultere i NEI når mor er under 18 år`() { + val mor = tilfeldigPerson(LocalDate.now().minusYears(17)).copy(aktør = gyldigAktørId) + val barnet = tilfeldigPerson(LocalDate.now()).copy(aktør = gyldigAktørId) + val restenAvBarna: List = listOf() + + val evalueringer = FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = mor, + barnaFraHendelse = listOf(barnet), + restenAvBarna = restenAvBarna, + morLever = true, + barnaLever = true, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + + assertThat(evalueringer.erOppfylt()).isFalse + assertEnesteRegelMedResultatNei(evalueringer, Filtreringsregel.MOR_ER_OVER_18_ÅR) + } + + @Test + fun `Regelevaluering skal resultere i JA når det har gått mer enn 5 måneder siden forrige barn ble født`() { + val mor = tilfeldigPerson(LocalDate.now().minusYears(20)).copy(aktør = gyldigAktørId) + val barnet1 = tilfeldigPerson(LocalDate.now().plusMonths(0)).copy(aktør = gyldigAktørId) + val barnet2 = tilfeldigPerson(LocalDate.now().minusMonths(1)).copy(aktør = gyldigAktørId) + val restenAvBarna: List = listOf( + PersonInfo(LocalDate.now().minusMonths(8).minusDays(1)), + PersonInfo(LocalDate.now().minusMonths(8)), + ) + + val evaluering = Filtreringsregel.MER_ENN_5_MND_SIDEN_FORRIGE_BARN.vurder( + FiltreringsreglerFakta( + mor = mor, + barnaFraHendelse = listOf(barnet1, barnet2), + restenAvBarna = restenAvBarna, + morLever = true, + barnaLever = true, + morHarVerge = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + erFagsakenMigrertEtterBarnFødt = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + + ), + ) + assertThat(evaluering.resultat).isEqualTo(Resultat.OPPFYLT) + } + + @Test + fun `Regelevaluering skal resultere i NEI når det har gått mindre enn 5 måneder siden forrige barn ble født`() { + val mor = tilfeldigPerson(LocalDate.now().minusYears(20)).copy(aktør = gyldigAktørId) + val barnet1 = tilfeldigPerson(LocalDate.now()).copy(aktør = gyldigAktørId) + val barnet2 = tilfeldigPerson(LocalDate.now().minusMonths(1)).copy(aktør = gyldigAktørId) + val restenAvBarna: List = listOf( + PersonInfo(LocalDate.now().minusMonths(5).minusDays(1)), + PersonInfo(LocalDate.now().minusMonths(8)), + ) + + val evaluering = Filtreringsregel.MER_ENN_5_MND_SIDEN_FORRIGE_BARN.vurder( + FiltreringsreglerFakta( + mor = mor, + barnaFraHendelse = listOf(barnet1, barnet2), + restenAvBarna = restenAvBarna, + morLever = true, + barnaLever = true, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + + assertThat(evaluering.resultat).isEqualTo(Resultat.IKKE_OPPFYLT) + } + + @Test + fun `Regelevaluering skal resultere i NEI når det er registrert dødsfall på mor`() { + val mor = tilfeldigPerson(LocalDate.now().minusYears(20)).copy(aktør = gyldigAktørId) + val barnet = tilfeldigPerson(LocalDate.now()).copy(aktør = gyldigAktørId) + val restenAvBarna: List = listOf() + + val evalueringer = FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = mor, + barnaFraHendelse = listOf(barnet), + restenAvBarna = restenAvBarna, + morLever = false, + barnaLever = true, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + + assertThat(evalueringer.erOppfylt()).isFalse + assertEnesteRegelMedResultatNei(evalueringer, Filtreringsregel.MOR_LEVER) + } + + @Test + fun `Regelevaluering skal resultere i NEI når det er registrert dødsfall på barnet`() { + val mor = tilfeldigPerson(LocalDate.now().minusYears(20)).copy(aktør = gyldigAktørId) + val barnet = tilfeldigPerson(LocalDate.now()).copy(aktør = gyldigAktørId) + val restenAvBarna: List = listOf() + + val evalueringer = FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = mor, + barnaFraHendelse = listOf(barnet), + restenAvBarna = restenAvBarna, + morLever = true, + barnaLever = false, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + + assertThat(evalueringer.erOppfylt()).isFalse + assertEnesteRegelMedResultatNei(evalueringer, Filtreringsregel.BARN_LEVER) + } + + @Test + fun `Regelevaluering skal resultere i NEI når mor har verge`() { + val mor = tilfeldigPerson(LocalDate.now().minusYears(20)).copy(aktør = gyldigAktørId) + val barnet = tilfeldigPerson(LocalDate.now()).copy(aktør = gyldigAktørId) + val restenAvBarna: List = listOf() + + val evalueringer = FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = mor, + barnaFraHendelse = listOf(barnet), + restenAvBarna = restenAvBarna, + morLever = true, + barnaLever = true, + morHarVerge = true, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + + assertThat(evalueringer.erOppfylt()).isFalse + assertEnesteRegelMedResultatNei(evalueringer, Filtreringsregel.MOR_HAR_IKKE_VERGE) + } + + fun assertIkkeOppfyltFiltreringsregel(evalueringer: List, filtreringsregel: Filtreringsregel) { + evalueringer.forEach { + if (it.evalueringÅrsaker.first().hentIdentifikator() == filtreringsregel.name) { + Assertions.assertEquals(Resultat.IKKE_OPPFYLT, it.resultat) + return + } else { + Assertions.assertEquals(Resultat.OPPFYLT, it.resultat) + } + } + } + + @Test + fun `Mor er under 18`() { + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("2020-10-23"), aktør = tilAktør("04086226621")) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2019-10-23"), aktør = tilAktør("21111777001")) + val barn2PersonInfo = PersonInfo(fødselsdato = LocalDate.parse("2020-09-23")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(barn2PersonInfo), + morLever = true, + barnaLever = true, + morHarVerge = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + erFagsakenMigrertEtterBarnFødt = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + assertIkkeOppfyltFiltreringsregel(evalueringer, Filtreringsregel.MOR_ER_OVER_18_ÅR) + } + + @Test + fun `Barn med mindre mellomrom enn 5mnd`() { + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23"), aktør = tilAktør("04086226621")) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2020-10-23"), aktør = tilAktør("21111777001")) + val barn2PersonInfo = PersonInfo(fødselsdato = LocalDate.parse("2020-09-23")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(barn2PersonInfo), + morLever = true, + barnaLever = true, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + assertIkkeOppfyltFiltreringsregel(evalueringer, Filtreringsregel.MER_ENN_5_MND_SIDEN_FORRIGE_BARN) + } + + @Test + fun `Tvillinger født på samme dag skal gi oppfylt`() { + val mor = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23"), aktør = tilAktør(randomFnr())) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2020-10-23"), aktør = tilAktør(randomFnr())) + val barn2PersonInfo = PersonInfo(fødselsdato = LocalDate.parse("2020-10-23")) + + val evaluering = Filtreringsregel.MER_ENN_5_MND_SIDEN_FORRIGE_BARN.vurder( + FiltreringsreglerFakta( + mor = mor, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(barn2PersonInfo), + morLever = true, + barnaLever = true, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + assertThat(evaluering.resultat).isEqualTo(Resultat.OPPFYLT) + } + + @Test + fun `Mor lever ikke`() { + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23"), aktør = tilAktør("04086226621")) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2020-10-23"), aktør = tilAktør("21111777001")) + val barn2PersonInfo = PersonInfo(fødselsdato = LocalDate.parse("2018-09-23")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(barn2PersonInfo), + morLever = false, + barnaLever = true, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + assertIkkeOppfyltFiltreringsregel(evalueringer, Filtreringsregel.MOR_LEVER) + } + + @Test + fun `Barnet lever ikke`() { + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23"), aktør = tilAktør("04086226621")) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2020-10-23"), aktør = tilAktør("21111777001")) + val barn2PersonInfo = PersonInfo(fødselsdato = LocalDate.parse("2018-09-23")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(barn2PersonInfo), + morLever = true, + barnaLever = false, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + assertIkkeOppfyltFiltreringsregel(evalueringer, Filtreringsregel.BARN_LEVER) + } + + @Test + fun `Mor har verge`() { + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23"), aktør = tilAktør("04086226621")) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2020-10-23"), aktør = tilAktør("21111777001")) + val barn2PersonInfo = PersonInfo(fødselsdato = LocalDate.parse("2018-09-23")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(barn2PersonInfo), + morLever = true, + barnaLever = true, + morHarVerge = true, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + assertIkkeOppfyltFiltreringsregel(evalueringer, Filtreringsregel.MOR_HAR_IKKE_VERGE) + } + + @Test + fun `Mor er død og er under vergemål`() { + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23"), aktør = tilAktør("04086226621")) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2020-10-23"), aktør = tilAktør("21111777001")) + val barn2PersonInfo = PersonInfo(fødselsdato = LocalDate.parse("2018-09-23")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(barn2PersonInfo), + morLever = false, + barnaLever = true, + morHarVerge = true, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + assertIkkeOppfyltFiltreringsregel(evalueringer, Filtreringsregel.MOR_LEVER) + } + + @Test + fun `Flere barn født`() { + val nyligFødselsdato = LocalDate.now().minusDays(2) + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23"), aktør = tilAktør("04086226621")) + val barn1Person = + tilfeldigPerson(fødselsdato = nyligFødselsdato, aktør = tilAktør("21111777001")) + val barn2Person = + tilfeldigPerson(fødselsdato = nyligFødselsdato, aktør = tilAktør("23128438785")) + val barn3PersonInfo = PersonInfo(fødselsdato = LocalDate.parse("2018-09-23")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person, barn2Person), + restenAvBarna = listOf(barn3PersonInfo), + morLever = true, + barnaLever = true, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + Assertions.assertTrue(evalueringer.erOppfylt()) + } + + @Test + fun `Mor har ugyldig fødselsnummer`() { + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23"), aktør = tilAktør("23236789111")) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2020-10-23"), aktør = tilAktør("21111777001")) + val barn3PersonInfo = PersonInfo(fødselsdato = LocalDate.parse("2018-09-23")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(barn3PersonInfo), + morLever = true, + barnaLever = true, + morHarVerge = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + erFagsakenMigrertEtterBarnFødt = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + assertIkkeOppfyltFiltreringsregel(evalueringer, Filtreringsregel.MOR_GYLDIG_FNR) + } + + @Test + fun `Barn med ugyldig fødselsnummer`() { + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23"), aktør = tilAktør("04086226621")) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2020-10-23"), aktør = tilAktør("23102000000")) + val barn2Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2018-09-23"), aktør = tilAktør("23091823456")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person, barn2Person), + restenAvBarna = listOf(), + morLever = true, + barnaLever = true, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + assertIkkeOppfyltFiltreringsregel(evalueringer, Filtreringsregel.BARN_GYLDIG_FNR) + } + + @Test + fun `Fagsak migrert etter barn født`() { + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23"), aktør = tilAktør("04086226621")) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2020-09-23"), aktør = tilAktør("23092023456")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(), + morLever = true, + barnaLever = true, + morHarVerge = false, + erFagsakenMigrertEtterBarnFødt = true, + løperBarnetrygdForBarnetPåAnnenForelder = false, + dagensDato = LocalDate.parse("2020-10-23"), + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + assertIkkeOppfyltFiltreringsregel( + evalueringer, + Filtreringsregel.FAGSAK_IKKE_MIGRERT_UT_AV_INFOTRYGD_ETTER_BARN_FØDT, + ) + } + + @Test + fun `Saken er godkjent fordi barnet er født i denne måneden`() { + val fødselsdatoIDenneMåned = LocalDate.now().withDayOfMonth(1) + + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23"), aktør = tilAktør("04086226621")) + val barn1Person = + tilfeldigPerson(fødselsdato = fødselsdatoIDenneMåned, aktør = tilAktør("23091823456")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(), + morLever = true, + barnaLever = true, + morHarVerge = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + erFagsakenMigrertEtterBarnFødt = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + Assertions.assertTrue(evalueringer.erOppfylt()) + } + + @Test + fun `Skal returnere ikke oppfylt for regelevaluering når det allerede løper barnetrygd for barnet på annen forelder`() { + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23")) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2020-10-23")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(), + morLever = true, + barnaLever = true, + morHarVerge = false, + løperBarnetrygdForBarnetPåAnnenForelder = true, + erFagsakenMigrertEtterBarnFødt = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + Assertions.assertTrue(!evalueringer.erOppfylt()) + } + + @Test + fun `Skal returnere ikke oppfylt for regelevaluering når mor oppfyller vilkår for utvidet barnetrygd`() { + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23")) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2020-10-23")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(), + morLever = true, + barnaLever = true, + morHarVerge = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + erFagsakenMigrertEtterBarnFødt = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = true, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + assertThat(evalueringer.erOppfylt()).isFalse + } + + @Test + fun `Skal returnere oppfylt for regelevaluering når mor ikke oppfyller vilkår for utvidet barnetrygd`() { + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23")) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2020-10-23")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(), + morLever = true, + barnaLever = true, + morHarVerge = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + erFagsakenMigrertEtterBarnFødt = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = true, + ), + ) + assertThat(evalueringer.erOppfylt()).isTrue + } + + @Test + fun `Skal returnere ikke oppfylt for regelevaluering når mor har opphørt barnetrygd`() { + val søkerPerson = + tilfeldigSøker(fødselsdato = LocalDate.parse("1962-10-23")) + val barn1Person = + tilfeldigPerson(fødselsdato = LocalDate.parse("2020-10-23")) + + val evalueringer = + FiltreringsregelEvaluering.evaluerFiltreringsregler( + FiltreringsreglerFakta( + mor = søkerPerson, + barnaFraHendelse = listOf(barn1Person), + restenAvBarna = listOf(), + morLever = true, + barnaLever = true, + morHarVerge = false, + løperBarnetrygdForBarnetPåAnnenForelder = false, + erFagsakenMigrertEtterBarnFødt = false, + morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato = false, + morHarIkkeOpphørtBarnetrygd = false, + ), + ) + assertThat(evalueringer.erOppfylt()).isFalse + } + + private fun assertEnesteRegelMedResultatNei(evalueringer: List, filtreringsRegel: Filtreringsregel) { + assertThat(1).isEqualTo(evalueringer.filter { it.resultat == Resultat.IKKE_OPPFYLT }.size) + assertThat(filtreringsRegel.name) + .isEqualTo(evalueringer.filter { it.resultat == Resultat.IKKE_OPPFYLT }[0].identifikator) + } + + @Test + fun `Filtreringsreglene skal følge en fagbestemt rekkefølge`() { + val fagbestemtFiltreringsregelrekkefølge = listOf( + Filtreringsregel.MOR_GYLDIG_FNR, + Filtreringsregel.BARN_GYLDIG_FNR, + Filtreringsregel.MOR_LEVER, + Filtreringsregel.BARN_LEVER, + Filtreringsregel.MER_ENN_5_MND_SIDEN_FORRIGE_BARN, + Filtreringsregel.MOR_ER_OVER_18_ÅR, + Filtreringsregel.MOR_HAR_IKKE_VERGE, + Filtreringsregel.MOR_MOTTAR_IKKE_LØPENDE_UTVIDET, + Filtreringsregel.MOR_HAR_IKKE_LØPENDE_EØS_BARNETRYGD, + Filtreringsregel.FAGSAK_IKKE_MIGRERT_UT_AV_INFOTRYGD_ETTER_BARN_FØDT, + Filtreringsregel.LØPER_IKKE_BARNETRYGD_FOR_BARNET, + Filtreringsregel.MOR_HAR_IKKE_OPPFYLT_UTVIDET_VILKÅR_VED_FØDSELSDATO, + Filtreringsregel.MOR_HAR_IKKE_OPPHØRT_BARNETRYGD, + ) + assertThat(Filtreringsregel.values().size).isEqualTo(fagbestemtFiltreringsregelrekkefølge.size) + assertThat( + Filtreringsregel.values().zip(fagbestemtFiltreringsregelrekkefølge) + .all { (x, y) -> x == y }, + ).isTrue + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsreglerServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsreglerServiceTest.kt" new file mode 100644 index 000000000..3af12aa5b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/filtreringsregler/FiltreringsreglerServiceTest.kt" @@ -0,0 +1,542 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler + +import io.mockk.CapturingSlot +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.common.LocalDateService +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.tilfeldigSøker +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagVilkårsvurderingMedOverstyrendeResultater +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.VergeResponse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.domene.FødselshendelsefiltreringResultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.domene.FødselshendelsefiltreringResultatRepository +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.filtreringsregler.domene.erOppfylt +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValideringService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.time.LocalDate +import java.time.YearMonth + +@ExtendWith(MockKExtension::class) +class FiltreringsreglerServiceTest { + + @MockK + private lateinit var personopplysningerService: PersonopplysningerService + + @MockK + private lateinit var personidentService: PersonidentService + + @MockK + private lateinit var personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository + + @MockK + private lateinit var vilkårsvurderingRepository: VilkårsvurderingRepository + + @MockK + private lateinit var localDateService: LocalDateService + + @MockK + private lateinit var fødselshendelsefiltreringResultatRepository: FødselshendelsefiltreringResultatRepository + + @MockK + private lateinit var behandlingService: BehandlingService + + @MockK + private lateinit var behandlingHentOgPersisterService: BehandlingHentOgPersisterService + + @MockK + private lateinit var tilkjentYtelseValideringService: TilkjentYtelseValideringService + + @InjectMockKs + private lateinit var filtreringsreglerService: FiltreringsreglerService + + @MockK + private lateinit var andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository + + @Test + fun `kjørFiltreringsregler - skal gi resultat ikke oppfylt når mors vilkår om utvidet barnetrygd er oppfylt i tidsrommet barnet er mellom 0 og 18`() { + val mor = tilfeldigSøker(fødselsdato = LocalDate.of(1985, 1, 1)) + val barn = tilfeldigPerson(fødselsdato = LocalDate.of(2021, 1, 1)) + val nyBehandlingHendelse = NyBehandlingHendelse(mor.aktør.aktørId, listOf(barn.aktør.aktørId)) + val sisteVedtatteBehandling = lagBehandling() + val behandling = lagBehandling() + + val fødselshendelsefiltreringResultatSlot = + settOppMocksHvorAlleFiltreringsreglerBlirOppfylt(mor, listOf(barn), behandling, sisteVedtatteBehandling) + + clearMocks(vilkårsvurderingRepository) + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(sisteVedtatteBehandling.id) } returns lagVilkårsvurderingMedOverstyrendeResultater( + mor, + listOf(barn), + behandling, + overstyrendeVilkårResultater = mapOf( + Pair( + mor.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2020, 11, 1), + periodeTom = LocalDate.of(2021, 2, 1), + ), + ), + ), + ), + ) + + mockkObject(FiltreringsregelEvaluering) + val filtreringsreglerFaktaSlot = slot() + + filtreringsreglerService.kjørFiltreringsregler(nyBehandlingHendelse, behandling) + + verify { FiltreringsregelEvaluering.evaluerFiltreringsregler(capture(filtreringsreglerFaktaSlot)) } + + val fødselshendelsefiltreringResultat = fødselshendelsefiltreringResultatSlot.captured + val filtreringsreglerFakta = filtreringsreglerFaktaSlot.captured + + assertThat(filtreringsreglerFakta.morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato).isTrue + + assertThat(fødselshendelsefiltreringResultat.single { it.resultat == Resultat.IKKE_OPPFYLT }.filtreringsregel).isEqualTo( + Filtreringsregel.MOR_HAR_IKKE_OPPFYLT_UTVIDET_VILKÅR_VED_FØDSELSDATO, + ) + assertThat(fødselshendelsefiltreringResultat.erOppfylt()).isFalse + } + + @Test + fun `kjørFiltreringsregler - skal gi resultat oppfylt når mors vilkår om utvidet barnetrygd er oppfylt utenfor tidsrommet barnet er mellom 0 og 18`() { + val mor = tilfeldigSøker(fødselsdato = LocalDate.of(1985, 1, 1)) + val barn = tilfeldigPerson(fødselsdato = LocalDate.of(2021, 1, 1)) + val nyBehandlingHendelse = NyBehandlingHendelse(mor.aktør.aktørId, listOf(barn.aktør.aktørId)) + val behandling = lagBehandling() + val sisteVedtatteBehandling = lagBehandling() + + val fødselshendelsefiltreringResultatSlot = + settOppMocksHvorAlleFiltreringsreglerBlirOppfylt(mor, listOf(barn), behandling, sisteVedtatteBehandling) + + clearMocks(vilkårsvurderingRepository) + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(sisteVedtatteBehandling.id) } returns lagVilkårsvurderingMedOverstyrendeResultater( + mor, + listOf(barn), + behandling, + overstyrendeVilkårResultater = mapOf( + Pair( + mor.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2020, 11, 1), + periodeTom = LocalDate.of(2021, 1, 1), + ), + ), + ), + ), + ) + + mockkObject(FiltreringsregelEvaluering) + val filtreringsreglerFaktaSlot = slot() + + filtreringsreglerService.kjørFiltreringsregler(nyBehandlingHendelse, behandling) + + verify { FiltreringsregelEvaluering.evaluerFiltreringsregler(capture(filtreringsreglerFaktaSlot)) } + + val fødselshendelsefiltreringResultat = fødselshendelsefiltreringResultatSlot.captured + val filtreringsreglerFakta = filtreringsreglerFaktaSlot.captured + + assertThat(filtreringsreglerFakta.morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato).isFalse + + assertThat(fødselshendelsefiltreringResultat.single { it.filtreringsregel == Filtreringsregel.MOR_HAR_IKKE_OPPFYLT_UTVIDET_VILKÅR_VED_FØDSELSDATO }.resultat).isEqualTo( + Resultat.OPPFYLT, + ) + assertThat(fødselshendelsefiltreringResultat.erOppfylt()).isTrue + } + + @Test + fun `kjørFiltreringsregler - skal gi resultat ikke oppfylt når en vilkårsperiode for utvidet barnetrygd er oppfylt i tidsrommet barnet er mellom 0 og 18`() { + val mor = tilfeldigSøker(fødselsdato = LocalDate.of(1985, 1, 1)) + val barn = tilfeldigPerson(fødselsdato = LocalDate.of(2021, 1, 1)) + val nyBehandlingHendelse = NyBehandlingHendelse(mor.aktør.aktørId, listOf(barn.aktør.aktørId)) + val behandling = lagBehandling() + val sisteVedtatteBehandling = lagBehandling() + + val fødselshendelsefiltreringResultatSlot = + settOppMocksHvorAlleFiltreringsreglerBlirOppfylt(mor, listOf(barn), behandling, sisteVedtatteBehandling) + + clearMocks(vilkårsvurderingRepository) + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(sisteVedtatteBehandling.id) } returns lagVilkårsvurderingMedOverstyrendeResultater( + mor, + listOf(barn), + behandling, + overstyrendeVilkårResultater = mapOf( + Pair( + mor.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2020, 11, 1), + periodeTom = LocalDate.of(2021, 1, 1), + ), + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2022, 1, 1), + periodeTom = LocalDate.of(2023, 1, 1), + ), + ), + ), + ), + ) + + mockkObject(FiltreringsregelEvaluering) + val filtreringsreglerFaktaSlot = slot() + + filtreringsreglerService.kjørFiltreringsregler(nyBehandlingHendelse, behandling) + + verify { FiltreringsregelEvaluering.evaluerFiltreringsregler(capture(filtreringsreglerFaktaSlot)) } + + val fødselshendelsefiltreringResultat = fødselshendelsefiltreringResultatSlot.captured + val filtreringsreglerFakta = filtreringsreglerFaktaSlot.captured + + assertThat(filtreringsreglerFakta.morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato).isTrue + + assertThat(fødselshendelsefiltreringResultat.single { it.resultat == Resultat.IKKE_OPPFYLT }.filtreringsregel).isEqualTo( + Filtreringsregel.MOR_HAR_IKKE_OPPFYLT_UTVIDET_VILKÅR_VED_FØDSELSDATO, + ) + assertThat(fødselshendelsefiltreringResultat.erOppfylt()).isFalse + } + + @Test + fun `kjørFiltreringsregler - skal gi resultat ikke oppfylt når tom-dato er null på vilkåret utvidet barnetrygd`() { + val mor = tilfeldigSøker(fødselsdato = LocalDate.of(1985, 1, 1)) + val barn = tilfeldigPerson(fødselsdato = LocalDate.of(2021, 1, 1)) + val nyBehandlingHendelse = NyBehandlingHendelse(mor.aktør.aktørId, listOf(barn.aktør.aktørId)) + val behandling = lagBehandling() + val sisteVedtatteBehandling = lagBehandling() + + val fødselshendelsefiltreringResultatSlot = + settOppMocksHvorAlleFiltreringsreglerBlirOppfylt(mor, listOf(barn), behandling, sisteVedtatteBehandling) + + clearMocks(vilkårsvurderingRepository) + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(sisteVedtatteBehandling.id) } returns lagVilkårsvurderingMedOverstyrendeResultater( + mor, + listOf(barn), + behandling, + overstyrendeVilkårResultater = mapOf( + Pair( + mor.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2020, 11, 1), + periodeTom = LocalDate.of(2021, 1, 1), + ), + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2022, 1, 1), + periodeTom = null, + ), + ), + ), + ), + ) + + mockkObject(FiltreringsregelEvaluering) + val filtreringsreglerFaktaSlot = slot() + + filtreringsreglerService.kjørFiltreringsregler(nyBehandlingHendelse, behandling) + + verify { FiltreringsregelEvaluering.evaluerFiltreringsregler(capture(filtreringsreglerFaktaSlot)) } + + val fødselshendelsefiltreringResultat = fødselshendelsefiltreringResultatSlot.captured + val filtreringsreglerFakta = filtreringsreglerFaktaSlot.captured + + assertThat(filtreringsreglerFakta.morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato).isTrue + + assertThat(fødselshendelsefiltreringResultat.single { it.resultat == Resultat.IKKE_OPPFYLT }.filtreringsregel).isEqualTo( + Filtreringsregel.MOR_HAR_IKKE_OPPFYLT_UTVIDET_VILKÅR_VED_FØDSELSDATO, + ) + assertThat(fødselshendelsefiltreringResultat.erOppfylt()).isFalse + } + + @Test + fun `kjørFiltreringsregler - skal gi resultat oppfylt når begge barnas fødselsdatoer er etter tom på vilkåret utvidet barnetrygd`() { + val mor = tilfeldigSøker(fødselsdato = LocalDate.of(1985, 1, 1)) + val barn1 = tilfeldigPerson(fødselsdato = LocalDate.of(2021, 1, 1)) + val barn2 = tilfeldigPerson(fødselsdato = LocalDate.of(2020, 1, 1)) + + val nyBehandlingHendelse = + NyBehandlingHendelse(mor.aktør.aktørId, listOf(barn1.aktør.aktørId, barn2.aktør.aktørId)) + val behandling = lagBehandling() + val sisteVedtatteBehandling = lagBehandling() + + val fødselshendelsefiltreringResultatSlot = + settOppMocksHvorAlleFiltreringsreglerBlirOppfylt( + mor, + listOf(barn1, barn2), + behandling, + sisteVedtatteBehandling, + ) + + clearMocks(vilkårsvurderingRepository) + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(sisteVedtatteBehandling.id) } returns lagVilkårsvurderingMedOverstyrendeResultater( + mor, + listOf(barn1, barn2), + behandling, + overstyrendeVilkårResultater = mapOf( + Pair( + mor.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2019, 11, 1), + periodeTom = LocalDate.of(2020, 1, 1), + ), + ), + ), + ), + ) + + mockkObject(FiltreringsregelEvaluering) + val filtreringsreglerFaktaSlot = slot() + + filtreringsreglerService.kjørFiltreringsregler(nyBehandlingHendelse, behandling) + + verify { FiltreringsregelEvaluering.evaluerFiltreringsregler(capture(filtreringsreglerFaktaSlot)) } + + val fødselshendelsefiltreringResultat = fødselshendelsefiltreringResultatSlot.captured + val filtreringsreglerFakta = filtreringsreglerFaktaSlot.captured + + assertThat(filtreringsreglerFakta.morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato).isFalse + + assertThat(fødselshendelsefiltreringResultat.single { it.filtreringsregel == Filtreringsregel.MOR_HAR_IKKE_OPPFYLT_UTVIDET_VILKÅR_VED_FØDSELSDATO }.resultat).isEqualTo( + Resultat.OPPFYLT, + ) + assertThat(fødselshendelsefiltreringResultat.erOppfylt()).isTrue + } + + @Test + fun `kjørFiltreringsregler - skal gi resultat ikke oppfylt når mors vilkår om utvidet barnetrygd er oppfylt i tidsrommet et av barna er mellom 0 og 18`() { + val mor = tilfeldigSøker(fødselsdato = LocalDate.of(1985, 1, 1)) + val barn1 = tilfeldigPerson(fødselsdato = LocalDate.of(2021, 1, 1)) + val barn2 = tilfeldigPerson(fødselsdato = LocalDate.of(2020, 1, 1)) + + val nyBehandlingHendelse = + NyBehandlingHendelse(mor.aktør.aktørId, listOf(barn1.aktør.aktørId, barn2.aktør.aktørId)) + val behandling = lagBehandling() + val sisteVedtatteBehandling = lagBehandling() + + val fødselshendelsefiltreringResultatSlot = + settOppMocksHvorAlleFiltreringsreglerBlirOppfylt( + mor, + listOf(barn1, barn2), + behandling, + sisteVedtatteBehandling, + ) + + clearMocks(vilkårsvurderingRepository) + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(sisteVedtatteBehandling.id) } returns lagVilkårsvurderingMedOverstyrendeResultater( + mor, + listOf(barn1, barn2), + behandling, + overstyrendeVilkårResultater = mapOf( + Pair( + mor.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2019, 11, 1), + periodeTom = LocalDate.of(2021, 1, 1), + ), + ), + ), + ), + ) + + mockkObject(FiltreringsregelEvaluering) + val filtreringsreglerFaktaSlot = slot() + + filtreringsreglerService.kjørFiltreringsregler(nyBehandlingHendelse, behandling) + + verify { FiltreringsregelEvaluering.evaluerFiltreringsregler(capture(filtreringsreglerFaktaSlot)) } + + val fødselshendelsefiltreringResultat = fødselshendelsefiltreringResultatSlot.captured + val filtreringsreglerFakta = filtreringsreglerFaktaSlot.captured + + assertThat(filtreringsreglerFakta.morOppfyllerVilkårForUtvidetBarnetrygdVedFødselsdato).isTrue + + assertThat(fødselshendelsefiltreringResultat.single { it.resultat == Resultat.IKKE_OPPFYLT }.filtreringsregel).isEqualTo( + Filtreringsregel.MOR_HAR_IKKE_OPPFYLT_UTVIDET_VILKÅR_VED_FØDSELSDATO, + ) + assertThat(fødselshendelsefiltreringResultat.erOppfylt()).isFalse + } + + @Test + fun `kjørFiltreringsregler - skal gi resultat ikke oppfylt når mor har opphørt barnetrygd`() { + val mor = tilfeldigSøker(fødselsdato = LocalDate.of(1985, 1, 1)) + val barn = tilfeldigPerson(fødselsdato = LocalDate.of(2021, 1, 1)) + val nyBehandlingHendelse = NyBehandlingHendelse(mor.aktør.aktørId, listOf(barn.aktør.aktørId)) + val sisteVedtatteBehandling = lagBehandling() + val behandling = lagBehandling() + + val fødselshendelsefiltreringResultatSlot = + settOppMocksHvorAlleFiltreringsreglerBlirOppfylt(mor, listOf(barn), behandling, sisteVedtatteBehandling) + + clearMocks(andelTilkjentYtelseRepository) + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(sisteVedtatteBehandling.id) } returns listOf( + MånedPeriode(YearMonth.of(2018, 1), YearMonth.now()), + ) + .map { + lagAndelTilkjentYtelse(it.fom, it.tom) + } + + mockkObject(FiltreringsregelEvaluering) + val filtreringsreglerFaktaSlot = slot() + + filtreringsreglerService.kjørFiltreringsregler(nyBehandlingHendelse, behandling) + + verify { FiltreringsregelEvaluering.evaluerFiltreringsregler(capture(filtreringsreglerFaktaSlot)) } + + val fødselshendelsefiltreringResultat = fødselshendelsefiltreringResultatSlot.captured + val filtreringsreglerFakta = filtreringsreglerFaktaSlot.captured + + assertThat(filtreringsreglerFakta.morHarIkkeOpphørtBarnetrygd).isFalse + + assertThat(fødselshendelsefiltreringResultat.single { it.resultat == Resultat.IKKE_OPPFYLT }.filtreringsregel).isEqualTo( + Filtreringsregel.MOR_HAR_IKKE_OPPHØRT_BARNETRYGD, + ) + assertThat(fødselshendelsefiltreringResultat.erOppfylt()).isFalse + } + + @Test + fun `kjørFiltreringsregler - skal gi resultat oppfylt når vilkår er oppfylt og mor ikke har opphørt barnetrygd`() { + val mor = tilfeldigSøker(fødselsdato = LocalDate.of(1985, 1, 1)) + val barn = tilfeldigPerson(fødselsdato = LocalDate.of(2021, 1, 1)) + val nyBehandlingHendelse = NyBehandlingHendelse(mor.aktør.aktørId, listOf(barn.aktør.aktørId)) + val sisteVedtatteBehandling = lagBehandling() + val behandling = lagBehandling() + + val fødselshendelsefiltreringResultatSlot = + settOppMocksHvorAlleFiltreringsreglerBlirOppfylt(mor, listOf(barn), behandling, sisteVedtatteBehandling) + + clearMocks(andelTilkjentYtelseRepository) + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(sisteVedtatteBehandling.id) } returns emptyList() + + mockkObject(FiltreringsregelEvaluering) + val filtreringsreglerFaktaSlot = slot() + + filtreringsreglerService.kjørFiltreringsregler(nyBehandlingHendelse, behandling) + + verify { FiltreringsregelEvaluering.evaluerFiltreringsregler(capture(filtreringsreglerFaktaSlot)) } + + val fødselshendelsefiltreringResultat = fødselshendelsefiltreringResultatSlot.captured + val filtreringsreglerFakta = filtreringsreglerFaktaSlot.captured + + assertThat(filtreringsreglerFakta.morHarIkkeOpphørtBarnetrygd).isTrue + + assertThat(fødselshendelsefiltreringResultat.single { it.filtreringsregel == Filtreringsregel.MOR_HAR_IKKE_OPPHØRT_BARNETRYGD }.resultat).isEqualTo( + Resultat.OPPFYLT, + ) + assertThat(fødselshendelsefiltreringResultat.erOppfylt()).isTrue + } + + private fun settOppMocksHvorAlleFiltreringsreglerBlirOppfylt( + mor: Person, + barna: List, + behandling: Behandling, + sisteVedtatteBehandling: Behandling, + ): CapturingSlot> { + every { personidentService.hentAktør(mor.aktør.aktørId) } returns mor.aktør + every { personidentService.hentAktørIder(barna.map { it.aktør.aktørId }) } returns barna.map { it.aktør } + + every { personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandling.id) } returns lagTestPersonopplysningGrunnlag( + behandling.id, + mor, + *barna.toTypedArray(), + ) + every { behandlingService.hentMigreringsdatoPåFagsak(behandling.fagsak.id) } returns null + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(behandling.fagsak.id) } returns sisteVedtatteBehandling + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(sisteVedtatteBehandling.id) } returns lagVilkårsvurderingMedOverstyrendeResultater( + mor, + barna, + behandling, + overstyrendeVilkårResultater = mapOf( + Pair( + mor.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = barna.minOf { it.fødselsdato }.minusYears(1), + periodeTom = barna.minOf { it.fødselsdato }.minusYears(1).plusMonths(6), + ), + ), + ), + ), + ) + + every { personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(mor.aktør) } returns PersonInfo( + forelderBarnRelasjon = + barna.map { + ForelderBarnRelasjon( + aktør = it.aktør, + relasjonsrolle = FORELDERBARNRELASJONROLLE.BARN, + ) + }.toSet(), + fødselsdato = mor.fødselsdato, + ) + + every { personopplysningerService.harVerge(mor.aktør) } returns VergeResponse(false) + + every { localDateService.now() } returns LocalDate.now() + + every { + tilkjentYtelseValideringService.barnetrygdLøperForAnnenForelder( + behandling, + barna, + ) + } returns false + + val andelTilkjentytelse = listOf( + MånedPeriode(YearMonth.of(2018, 1), YearMonth.now().plusYears(1)), + ) + .map { + lagAndelTilkjentYtelse(it.fom, it.tom) + } + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(any()) } returns andelTilkjentytelse + + val fødselshendelsefiltreringResultatSlot = slot>() + + every { + fødselshendelsefiltreringResultatRepository.saveAll( + capture( + fødselshendelsefiltreringResultatSlot, + ), + ) + } returns mockk() + return fødselshendelsefiltreringResultatSlot + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/BarnBorMedS\303\270kerVilk\303\245rTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/BarnBorMedS\303\270kerVilk\303\245rTest.kt" new file mode 100644 index 000000000..3c958780a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/BarnBorMedS\303\270kerVilk\303\245rTest.kt" @@ -0,0 +1,89 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering + +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.datagenerator.grunnlag.opprettAdresse +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrBostedsadresse +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test + +class BarnBorMedSøkerVilkårTest { + + @Test + fun `Samme matrikkelId men ellers forskjellige adresser`() { + val faktaPerson = opprettFaktaPerson(adresseMatrikkelId1barn, adresseMatrikkelId1SøkerBruksenhetsnummer) + + val evaluering = vilkår.vurderVilkår(faktaPerson) + Assertions.assertThat(evaluering.resultat).isEqualTo(Resultat.OPPFYLT) + } + + @Test + fun `Forskjellige matrikkelId`() { + val faktaPerson = opprettFaktaPerson(adresseMatrikkelId1barn, adresseMatrikkelId2Søker) + + val evaluering = vilkår.vurderVilkår(faktaPerson) + Assertions.assertThat(evaluering.resultat).isEqualTo(Resultat.IKKE_OPPFYLT) + } + + @Test + fun `Address som mangler postnummer`() { + val faktaPerson = opprettFaktaPerson(adresseIkkePostnummerBarn, adresseIkkePostnummerSøker) + + val evaluering = vilkår.vurderVilkår(faktaPerson) + Assertions.assertThat(evaluering.resultat).isEqualTo(Resultat.IKKE_OPPFYLT) + } + + @Test + fun `Address som mangler matrikkelid`() { + val faktaPerson = opprettFaktaPerson(adresseAttrBarn, adresseAttrSøker) + + val evaluering = vilkår.vurderVilkår(faktaPerson) + Assertions.assertThat(evaluering.resultat).isEqualTo(Resultat.OPPFYLT) + } + + @Test + fun `To forskjellige address som begge mangler matrikkelid`() { + val faktaPerson = opprettFaktaPerson(adresseAttrBarn, adresseAttr2Søker) + + val evaluering = vilkår.vurderVilkår(faktaPerson) + Assertions.assertThat(evaluering.resultat).isEqualTo(Resultat.IKKE_OPPFYLT) + } + + private fun opprettFaktaPerson( + bostedsadresseSøker: GrBostedsadresse, + bostedsadresseBarn: GrBostedsadresse, + ): Person { + val barnMedAdresse = barn.copy(bostedsadresser = mutableListOf(bostedsadresseBarn)) + barnMedAdresse.personopplysningGrunnlag.personer.clear() + barnMedAdresse.personopplysningGrunnlag.personer.add( + søker.copy( + bostedsadresser = mutableListOf( + bostedsadresseSøker, + ), + ), + ) + + return barnMedAdresse + } + + companion object { + + val vilkår = Vilkår.BOR_MED_SØKER + val barn = tilfeldigPerson(personType = PersonType.BARN) + + val søker = tilfeldigPerson(personType = PersonType.SØKER, kjønn = Kjønn.KVINNE) + + val adresseMatrikkelId1barn = opprettAdresse(1234L) + val adresseMatrikkelId2Søker = opprettAdresse(4321L) + val adresseMatrikkelId1SøkerBruksenhetsnummer = opprettAdresse(matrikkelId = 1234L, bruksenhetsnummer = "123") + val adresseIkkePostnummerBarn = opprettAdresse(adressenavn = "Fågelveien", husnummer = "123") + val adresseIkkePostnummerSøker = opprettAdresse(adressenavn = "Fågelveien", husnummer = "123") + val adresseAttrBarn = opprettAdresse(adressenavn = "Fågelveien", husnummer = "123", postnummer = "0245") + val adresseAttrSøker = opprettAdresse(adressenavn = "Fågelveien", husnummer = "123", postnummer = "0245") + val adresseAttr2Søker = opprettAdresse(adressenavn = "Fågelveien", husnummer = "11", postnummer = "0245") + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/BosattIRiketVilk\303\245rTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/BosattIRiketVilk\303\245rTest.kt" new file mode 100644 index 000000000..f9c2520ac --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/BosattIRiketVilk\303\245rTest.kt" @@ -0,0 +1,305 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering + +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall.VilkårIkkeOppfyltÅrsak +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall.VilkårOppfyltÅrsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrBostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.Matrikkeladresse +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class BosattIRiketVilkårTest { + + private val defaultAdresse = Bostedsadresse( + angittFlyttedato = LocalDate.parse("2020-07-13"), + gyldigTilOgMed = null, + matrikkeladresse = Matrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ), + ) + + @Test + fun `Skal sjekke at person bor i riket dersom vedkommende har vært utvandret langt tilbake i tid`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + + søker.apply { + bostedsadresser = mutableListOf( + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.parse("2019-01-19"), + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.parse("2011-06-02"), + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.parse("1988-06-23"), + gyldigTilOgMed = LocalDate.parse("2006-01-01"), + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.parse("2013-09-22"), + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.parse("2016-10-01"), + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.parse("2012-07-11"), + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.parse("2006-06-04"), + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.parse("2011-06-01"), + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.parse("2020-07-13"), + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.parse("2020-06-08"), + ), + søker, + ), + ) + } + + val evaluering = VurderPersonErBosattIRiket( + adresser = søker.bostedsadresser, + vurderFra = LocalDate.now().minusDays(1), + ).vurder() + + assertEquals(Resultat.OPPFYLT, evaluering.resultat) + } + + @Test + fun `Skal sjekke at person ikke bor i riket`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + + søker.apply { + bostedsadresser = mutableListOf( + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.parse("2019-01-19"), + gyldigTilOgMed = LocalDate.parse("2021-05-01"), + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.parse("2011-06-02"), + ), + søker, + ), + ) + } + + val evaluering = VurderPersonErBosattIRiket( + adresser = søker.bostedsadresser, + vurderFra = LocalDate.now().minusDays(1), + ).vurder() + + assertEquals(Resultat.IKKE_OPPFYLT, evaluering.resultat) + } + + @Test + fun `Skal sjekke at person ikke bor i riket dersom vedkommende har vært utvandret`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + + søker.apply { + bostedsadresser = mutableListOf( + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.now().minusMonths(3), + gyldigTilOgMed = LocalDate.now().minusMonths(2), + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.now().minusMonths(1), + ), + søker, + ), + ) + } + + val evaluering = VurderPersonErBosattIRiket( + adresser = søker.bostedsadresser, + vurderFra = LocalDate.now().minusMonths(4), + ).vurder() + + assertEquals(Resultat.IKKE_OPPFYLT, evaluering.resultat) + } + + @Test + fun `Skal sjekke at person ikke bor i riket dersom vedkommende har vært utvandret først i perioden`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + + søker.apply { + bostedsadresser = mutableListOf( + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.now().minusMonths(3), + ), + søker, + ), + ) + } + + val evaluering = VurderPersonErBosattIRiket( + adresser = søker.bostedsadresser, + vurderFra = LocalDate.now().minusMonths(4), + ).vurder() + + assertEquals(Resultat.IKKE_OPPFYLT, evaluering.resultat) + } + + @Test + fun `Skal sjekke at person ikke bor i riket dersom vedkommende har vært utvandret sist i perioden`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + + søker.apply { + bostedsadresser = mutableListOf( + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.now().minusMonths(7), + gyldigTilOgMed = LocalDate.now().minusMonths(2), + ), + søker, + ), + ) + } + + val evaluering = VurderPersonErBosattIRiket( + adresser = søker.bostedsadresser, + vurderFra = LocalDate.now().minusMonths(4), + ).vurder() + + assertEquals(Resultat.IKKE_OPPFYLT, evaluering.resultat) + } + + @Test + fun `Skal sjekke at person bor i riket selv om hen har ekstra adresse uten fom`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + + søker.apply { + bostedsadresser = mutableListOf( + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = null, + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.now().minusMonths(7), + ), + søker, + ), + ) + } + + val evaluering = VurderPersonErBosattIRiket( + adresser = søker.bostedsadresser, + vurderFra = LocalDate.now().minusMonths(4), + ).vurder() + + assertEquals(Resultat.OPPFYLT, evaluering.resultat) + } + + @Test + fun `Skal sjekke at person bor i riket selv om hen kun har en adresse uten fom `() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + + søker.apply { + bostedsadresser = mutableListOf( + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = null, + ), + søker, + ), + ) + } + + val evaluering = VurderPersonErBosattIRiket( + adresser = søker.bostedsadresser, + vurderFra = LocalDate.now().minusMonths(4), + ).vurder() + + assertEquals(Resultat.OPPFYLT, evaluering.resultat) + assertEquals(VilkårOppfyltÅrsak.BOR_I_RIKET_KUN_ADRESSER_UTEN_FOM, evaluering.evalueringÅrsaker.single()) + } + + @Test + fun `Skal sjekke at person ikke bor i riket om hen har flere adresser uten fom `() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + + søker.apply { + bostedsadresser = mutableListOf( + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = null, + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = null, + ), + søker, + ), + GrBostedsadresse.fraBostedsadresse( + defaultAdresse.copy( + angittFlyttedato = LocalDate.now().minusMonths(3), + ), + søker, + ), + ) + } + + val evaluering = VurderPersonErBosattIRiket( + adresser = søker.bostedsadresser, + vurderFra = LocalDate.now().minusMonths(4), + ).vurder() + + assertEquals(Resultat.IKKE_OPPFYLT, evaluering.resultat) + assertEquals( + VilkårIkkeOppfyltÅrsak.BOR_IKKE_I_RIKET_FLERE_ADRESSER_UTEN_FOM, + evaluering.evalueringÅrsaker.single(), + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/GiftEllerPartnerskapVilk\303\245rTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/GiftEllerPartnerskapVilk\303\245rTest.kt" new file mode 100644 index 000000000..afba77a9b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/GiftEllerPartnerskapVilk\303\245rTest.kt" @@ -0,0 +1,28 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering + +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.sivilstand.GrSivilstand +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test + +class GiftEllerPartnerskapVilkårTest { + + @Test + fun `Gift-vilkår gir resultat JA for fødselshendelse når sivilstand er uoppgitt`() { + val evaluering = vilkår.vurderVilkår(barn) + Assertions.assertThat(evaluering.resultat).isEqualTo(Resultat.OPPFYLT) + } + + companion object { + + val vilkår = Vilkår.GIFT_PARTNERSKAP + val barn = + tilfeldigPerson(personType = PersonType.BARN).apply { + sivilstander = mutableListOf(GrSivilstand(type = SIVILSTAND.UOPPGITT, person = this)) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/LovligOppholdVilk\303\245rTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/LovligOppholdVilk\303\245rTest.kt" new file mode 100644 index 000000000..f28eaa9f9 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/vilk\303\245rsvurdering/LovligOppholdVilk\303\245rTest.kt" @@ -0,0 +1,476 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering + +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.datagenerator.grunnlag.opprettAdresse +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall.VilkårIkkeOppfyltÅrsak +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall.VilkårOppfyltÅrsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.arbeidsforhold.GrArbeidsforhold +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.opphold.GrOpphold +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.GrStatsborgerskap +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.kontrakter.felles.personopplysning.OPPHOLDSTILLATELSE +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class LovligOppholdVilkårTest { + + @Test + fun `Ikke lovlig opphold dersom søker ikke har noen gjeldende opphold registrert`() { + val evaluering = vilkår.vurderVilkår(tredjelandsborger).evaluering + assertThat(evaluering.resultat).isEqualTo(Resultat.IKKE_OPPFYLT) + } + + @Test + fun `Ikke lovlig opphold dersom søker er statsløs og ikke har noen gjeldende opphold registrert`() { + val statsløsEvaluering = vilkår.vurderVilkår(statsløsPerson).evaluering + assertThat(statsløsEvaluering.resultat).isEqualTo(Resultat.IKKE_OPPFYLT) + + val ukjentStatsborgerskapEvaluering = + vilkår.vurderVilkår(ukjentStatsborger).evaluering + assertThat(ukjentStatsborgerskapEvaluering.resultat).isEqualTo(Resultat.IKKE_OPPFYLT) + } + + @Test + fun `Lovlig opphold vurdert på bakgrunn av status`() { + var evaluering = vilkår.vurderVilkår(faktaPerson(OPPHOLDSTILLATELSE.MIDLERTIDIG, null)).evaluering + assertThat(evaluering.resultat).isEqualTo(Resultat.OPPFYLT) + evaluering = vilkår.vurderVilkår(faktaPerson(OPPHOLDSTILLATELSE.PERMANENT, null)).evaluering + assertThat(evaluering.resultat).isEqualTo(Resultat.OPPFYLT) + evaluering = vilkår.vurderVilkår(faktaPerson(OPPHOLDSTILLATELSE.OPPLYSNING_MANGLER, null)).evaluering + assertThat(evaluering.resultat).isEqualTo(Resultat.IKKE_OPPFYLT) + } + + @Test + fun `Lovlig opphold vurdert på bakgrunn av status for statsløs søker`() { + var evaluering = vilkår.vurderVilkår( + statsløsPerson.copy().apply { + opphold = + mutableListOf(GrOpphold(gyldigPeriode = null, type = OPPHOLDSTILLATELSE.MIDLERTIDIG, person = this)) + }, + ).evaluering + assertThat(evaluering.resultat).isEqualTo(Resultat.OPPFYLT) + + evaluering = vilkår.vurderVilkår( + ukjentStatsborger.copy().apply { + opphold = + mutableListOf(GrOpphold(gyldigPeriode = null, type = OPPHOLDSTILLATELSE.MIDLERTIDIG, person = this)) + }, + ).evaluering + assertThat(evaluering.resultat).isEqualTo(Resultat.OPPFYLT) + + evaluering = vilkår.vurderVilkår( + statsløsPerson.copy().apply { + opphold = + mutableListOf( + GrOpphold( + gyldigPeriode = null, + type = OPPHOLDSTILLATELSE.OPPLYSNING_MANGLER, + person = this, + ), + ) + }, + ).evaluering + assertThat(evaluering.resultat).isEqualTo(Resultat.IKKE_OPPFYLT) + } + + @Test + fun `Ikke lovlig opphold dersom utenfor gyldig periode`() { + var evaluering = vilkår.vurderVilkår( + tredjelandsborger.copy( + statsborgerskap = mutableListOf( + GrStatsborgerskap( + landkode = "ANG", + medlemskap = Medlemskap.TREDJELANDSBORGER, + person = tredjelandsborger, + ), + ), + opphold = mutableListOf( + GrOpphold( + gyldigPeriode = DatoIntervallEntitet( + fom = LocalDate.now().minusYears(10), + tom = LocalDate.now().minusYears(5), + ), + type = OPPHOLDSTILLATELSE.MIDLERTIDIG, + person = tredjelandsborger, + ), + ), + ), + ).evaluering + assertThat(evaluering.resultat).isEqualTo(Resultat.IKKE_OPPFYLT) + + evaluering = vilkår.vurderVilkår( + statsløsPerson.copy().apply { + opphold = mutableListOf( + GrOpphold( + gyldigPeriode = DatoIntervallEntitet( + fom = LocalDate.now().minusYears(10), + tom = LocalDate.now().minusYears(5), + ), + type = OPPHOLDSTILLATELSE.MIDLERTIDIG, + person = this, + ), + ) + }, + ).evaluering + assertThat(evaluering.resultat).isEqualTo(Resultat.IKKE_OPPFYLT) + } + + @Test + fun `Lovlig opphold dersom status med gjeldende periode`() { + var evaluering = vilkår.vurderVilkår( + tredjelandsborger.copy( + statsborgerskap = mutableListOf( + GrStatsborgerskap( + landkode = "ANG", + medlemskap = Medlemskap.TREDJELANDSBORGER, + person = tredjelandsborger, + ), + ), + opphold = mutableListOf( + GrOpphold( + gyldigPeriode = DatoIntervallEntitet( + fom = LocalDate.now().minusYears(10), + tom = LocalDate.now().minusYears(5), + ), + type = OPPHOLDSTILLATELSE.OPPLYSNING_MANGLER, + person = tredjelandsborger, + ), + GrOpphold( + gyldigPeriode = DatoIntervallEntitet( + fom = LocalDate.now().minusYears(5), + tom = null, + ), + type = OPPHOLDSTILLATELSE.MIDLERTIDIG, + person = tredjelandsborger, + ), + ), + ), + ).evaluering + assertThat(evaluering.resultat).isEqualTo(Resultat.OPPFYLT) + + evaluering = vilkår.vurderVilkår( + statsløsPerson.copy().apply { + opphold = mutableListOf( + GrOpphold( + gyldigPeriode = DatoIntervallEntitet( + fom = LocalDate.now().minusYears(10), + tom = LocalDate.now().minusYears(5), + ), + type = OPPHOLDSTILLATELSE.OPPLYSNING_MANGLER, + person = this, + ), + GrOpphold( + gyldigPeriode = DatoIntervallEntitet( + fom = LocalDate.now().minusYears(5), + tom = null, + ), + type = OPPHOLDSTILLATELSE.MIDLERTIDIG, + person = this, + ), + ) + }, + ).evaluering + assertThat(evaluering.resultat).isEqualTo(Resultat.OPPFYLT) + } + + @Test + fun `Lovlig opphold blir oppfylt for mor med EØS medlemskap og har løpende arbeidsforhold`() { + val evaluering = vilkår.vurderVilkår( + eøsBorger.copy().apply { + arbeidsforhold = mutableListOf( + GrArbeidsforhold( + periode = DatoIntervallEntitet(fom = null, tom = LocalDate.now().plusMonths(6)), + arbeidsgiverId = null, + arbeidsgiverType = null, + person = this, + ), + ) + }, + ).evaluering + assertEquals(Resultat.OPPFYLT, evaluering.resultat) + assertEquals(listOf(VilkårOppfyltÅrsak.EØS_MED_LØPENDE_ARBEIDSFORHOLD), evaluering.evalueringÅrsaker) + } + + @Test + fun `Lovlig opphold blir ikke oppfylt for mor med EØS medlemskap, uten løpende arbeidsforhold og annen forelder`() { + val evaluering = vilkår.vurderVilkår( + person = eøsBorger.copy().apply { + arbeidsforhold = mutableListOf() + }, + annenForelder = null, + ).evaluering + assertEquals(Resultat.IKKE_OPPFYLT, evaluering.resultat) + assertEquals(listOf(VilkårIkkeOppfyltÅrsak.EØS_STATSBORGERSKAP_ANNEN_FORELDER_UKLART), evaluering.evalueringÅrsaker) + } + + @Test + fun `Lovlig opphold blir ikke oppfylt for mor med EØS medlemskap, uten løpende arbeidsforhold og annen forelder bor ikke med mor`() { + val evaluering = vilkår.vurderVilkår( + person = eøsBorger.copy().apply { + arbeidsforhold = mutableListOf() + bostedsadresser = + mutableListOf(opprettAdresse(adressenavn = "Osloveien", husnummer = "123", postnummer = "0245")) + }, + annenForelder = annenForelderNordiskBorger.copy( + bostedsadresser = mutableListOf( + opprettAdresse( + adressenavn = "Fågelveien", + husnummer = "123", + postnummer = "0245", + ), + ), + ), + ).evaluering + assertEquals(Resultat.IKKE_OPPFYLT, evaluering.resultat) + assertEquals( + listOf(VilkårIkkeOppfyltÅrsak.EØS_BOR_IKKE_SAMMEN_MED_ANNEN_FORELDER), + evaluering.evalueringÅrsaker, + ) + } + + @Test + fun `Lovlig opphold blir ikke oppfylt for mor med EØS medlemskap, uten løpende arbeidsforhold og annen forelder har bodd med mor`() { + val tidligereAdresse = opprettAdresse(adressenavn = "Uteveien", husnummer = "123", postnummer = "0245").also { + it.periode = DatoIntervallEntitet( + fom = LocalDate.now().minusYears(2), + tom = LocalDate.now().minusYears(1), + ) + } + + val evaluering = vilkår.vurderVilkår( + person = eøsBorger.copy().apply { + arbeidsforhold = mutableListOf() + bostedsadresser = + mutableListOf( + tidligereAdresse, + opprettAdresse(adressenavn = "Osloveien", husnummer = "123", postnummer = "0245"), + ) + }, + annenForelder = annenForelderNordiskBorger.copy( + bostedsadresser = mutableListOf( + tidligereAdresse, + opprettAdresse( + adressenavn = "Fågelveien", + husnummer = "123", + postnummer = "0245", + ), + ), + ), + ).evaluering + assertEquals(Resultat.IKKE_OPPFYLT, evaluering.resultat) + assertEquals( + listOf(VilkårIkkeOppfyltÅrsak.EØS_BOR_IKKE_SAMMEN_MED_ANNEN_FORELDER), + evaluering.evalueringÅrsaker, + ) + } + + @Test + fun `Lovlig opphold blir oppfylt for mor med EØS medlemskap, uten løpende arbeidsforhold og annen forelder bor med mor og nordisk`() { + val adresse = opprettAdresse(adressenavn = "Osloveien", husnummer = "123", postnummer = "0245") + val evaluering = vilkår.vurderVilkår( + person = eøsBorger.copy().apply { + arbeidsforhold = mutableListOf() + bostedsadresser = + mutableListOf(adresse) + }, + annenForelder = annenForelderNordiskBorger.copy( + bostedsadresser = mutableListOf( + adresse, + ), + ), + ).evaluering + assertEquals(Resultat.OPPFYLT, evaluering.resultat) + assertEquals( + listOf(VilkårOppfyltÅrsak.ANNEN_FORELDER_NORDISK), + evaluering.evalueringÅrsaker, + ) + } + + @Test + fun `Lovlig opphold blir ikke oppfylt for mor med EØS medlemskap, annen forelder(EØS) ikke løpende arbeidsforhold`() { + val adresse = opprettAdresse(adressenavn = "Osloveien", husnummer = "123", postnummer = "0245") + val evaluering = vilkår.vurderVilkår( + person = eøsBorger.copy().apply { + arbeidsforhold = mutableListOf() + bostedsadresser = + mutableListOf(adresse) + }, + annenForelder = annenForelderEØS.copy( + bostedsadresser = mutableListOf( + adresse, + ), + arbeidsforhold = mutableListOf(), + ), + ).evaluering + assertEquals(Resultat.IKKE_OPPFYLT, evaluering.resultat) + assertEquals( + listOf(VilkårIkkeOppfyltÅrsak.EØS_ANNEN_FORELDER_EØS_MEN_IKKE_MED_LØPENDE_ARBEIDSFORHOLD), + evaluering.evalueringÅrsaker, + ) + } + + @Test + fun `Lovlig opphold blir oppfylt for mor med EØS medlemskap, annen forelder(EØS) har løpende arbeidsforhold`() { + val adresse = opprettAdresse(adressenavn = "Osloveien", husnummer = "123", postnummer = "0245") + val evaluering = vilkår.vurderVilkår( + person = eøsBorger.copy().apply { + arbeidsforhold = mutableListOf() + bostedsadresser = + mutableListOf(adresse) + }, + annenForelder = annenForelderEØS.copy().apply { + bostedsadresser = mutableListOf( + adresse, + ) + arbeidsforhold = mutableListOf( + GrArbeidsforhold( + periode = DatoIntervallEntitet(fom = null, tom = LocalDate.now().plusMonths(6)), + arbeidsgiverId = null, + arbeidsgiverType = null, + person = this, + ), + ) + }, + ).evaluering + assertEquals(Resultat.OPPFYLT, evaluering.resultat) + assertEquals( + listOf(VilkårOppfyltÅrsak.ANNEN_FORELDER_EØS_MEN_MED_LØPENDE_ARBEIDSFORHOLD), + evaluering.evalueringÅrsaker, + ) + } + + @Test + fun `Lovlig opphold blir ikke oppfylt for mor med EØS medlemskap, annen forelder er tredjelandsborger`() { + val adresse = opprettAdresse(adressenavn = "Osloveien", husnummer = "123", postnummer = "0245") + val evaluering = vilkår.vurderVilkår( + person = eøsBorger.copy().apply { + arbeidsforhold = mutableListOf() + bostedsadresser = + mutableListOf(adresse) + }, + annenForelder = tredjelandsborger.copy().apply { + bostedsadresser = + mutableListOf(adresse) + }, + ).evaluering + assertEquals(Resultat.IKKE_OPPFYLT, evaluering.resultat) + assertEquals( + listOf(VilkårIkkeOppfyltÅrsak.EØS_MEDFORELDER_TREDJELANDSBORGER), + evaluering.evalueringÅrsaker, + ) + } + + @Test + fun `Lovlig opphold blir ikke oppfylt for mor med EØS medlemskap, annen forelder er statsløs`() { + val adresse = opprettAdresse(adressenavn = "Osloveien", husnummer = "123", postnummer = "0245") + val evaluering = vilkår.vurderVilkår( + person = eøsBorger.copy().apply { + arbeidsforhold = mutableListOf() + bostedsadresser = + mutableListOf(adresse) + }, + annenForelder = statsløsPerson.copy().apply { + bostedsadresser = + mutableListOf(adresse) + }, + ).evaluering + assertEquals(Resultat.IKKE_OPPFYLT, evaluering.resultat) + assertEquals( + listOf(VilkårIkkeOppfyltÅrsak.EØS_MEDFORELDER_STATSLØS), + evaluering.evalueringÅrsaker, + ) + } + + @Test + fun `Lovlig opphold gir resultat JA for barn ved fødselshendelse`() { + val evaluering = vilkår.vurderVilkår(barn).evaluering + assertThat(evaluering.resultat).isEqualTo(Resultat.OPPFYLT) + } + + private fun faktaPerson(oppholdstillatelse: OPPHOLDSTILLATELSE, periode: DatoIntervallEntitet?): Person { + return tredjelandsborger.copy( + statsborgerskap = mutableListOf( + GrStatsborgerskap( + landkode = "ANG", + medlemskap = Medlemskap.TREDJELANDSBORGER, + person = tredjelandsborger, + ), + ), + opphold = mutableListOf( + GrOpphold( + gyldigPeriode = periode, + type = oppholdstillatelse, + person = tredjelandsborger, + ), + ), + ) + } + + companion object { + + val vilkår = Vilkår.LOVLIG_OPPHOLD + val tredjelandsborger = tilfeldigPerson(personType = PersonType.SØKER).apply { + statsborgerskap = mutableListOf( + GrStatsborgerskap( + landkode = "ANG", + medlemskap = Medlemskap.TREDJELANDSBORGER, + person = this, + ), + ) + } + val eøsBorger = tilfeldigPerson(personType = PersonType.SØKER).apply { + statsborgerskap = mutableListOf( + GrStatsborgerskap( + landkode = "POL", + medlemskap = Medlemskap.EØS, + person = this, + ), + ) + } + val annenForelderNordiskBorger = tilfeldigPerson(personType = PersonType.SØKER).apply { + statsborgerskap = mutableListOf( + GrStatsborgerskap( + landkode = "NOR", + medlemskap = Medlemskap.NORDEN, + person = this, + ), + ) + } + val annenForelderEØS = tilfeldigPerson(personType = PersonType.SØKER).apply { + statsborgerskap = mutableListOf( + GrStatsborgerskap( + landkode = "POL", + medlemskap = Medlemskap.EØS, + person = this, + ), + ) + } + val statsløsPerson = tilfeldigPerson(personType = PersonType.SØKER).apply { + statsborgerskap = mutableListOf( + GrStatsborgerskap( + landkode = "XXX", + medlemskap = Medlemskap.STATSLØS, + person = this, + ), + ) + } + val ukjentStatsborger = tilfeldigPerson(personType = PersonType.SØKER).apply { + statsborgerskap = mutableListOf( + GrStatsborgerskap( + landkode = "XUK", + medlemskap = Medlemskap.UKJENT, + person = this, + ), + ) + } + + val barn = tilfeldigPerson(personType = PersonType.BARN) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/Autobrev6og18\303\205rServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/Autobrev6og18\303\205rServiceTest.kt" new file mode 100644 index 000000000..e3d8ff5e8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/Autobrev6og18\303\205rServiceTest.kt" @@ -0,0 +1,397 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.omregning + +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.tilfeldigSøker +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakStegService +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.StartSatsendring +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndelRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.task.dto.Autobrev6og18ÅrDTO +import no.nav.familie.prosessering.domene.Task +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.YearMonth + +internal class Autobrev6og18ÅrServiceTest { + + private val autovedtakService = mockk() + private val autovedtakStegService = mockk() + private val personopplysningGrunnlagRepository = mockk() + private val behandlingService = mockk() + private val behandlingHentOgPersisterService = mockk() + private val fagsakService = mockk(relaxed = true) + private val infotrygdService = mockk(relaxed = true) + private val stegService = mockk() + private val vedtakService = mockk(relaxed = true) + private val taskRepository = mockk(relaxed = true) + private val vedtaksperiodeService = mockk() + private val andelTilkjentYtelseRepository = mockk() + private val endretUtbetalingAndelRepository = mockk(relaxed = true) + private val featureToggleService = mockk(relaxed = true) + private val startSatsendring = mockk(relaxed = true) + + private val autovedtakBrevService = AutovedtakBrevService( + behandlingService = behandlingService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + fagsakService = fagsakService, + autovedtakService = autovedtakService, + vedtakService = vedtakService, + infotrygdService = infotrygdService, + vedtaksperiodeService = vedtaksperiodeService, + taskRepository = taskRepository, + ) + + private val autobrev6og18ÅrService = Autobrev6og18ÅrService( + personopplysningGrunnlagRepository = personopplysningGrunnlagRepository, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + autovedtakBrevService = autovedtakBrevService, + autovedtakStegService = autovedtakStegService, + andelerTilkjentYtelseOgEndreteUtbetalingerService = AndelerTilkjentYtelseOgEndreteUtbetalingerService( + andelTilkjentYtelseRepository, + endretUtbetalingAndelRepository, + mockk(), + ), + startSatsendring = startSatsendring, + ) + + @Test + fun `Verifiser at løpende fagsak med avsluttede behandlinger og barn på 18 ikke oppretter en behandling for omregning`() { + val behandling = initMock(alder = 18, medSøsken = false).first + + val autobrev6og18ÅrDTO = Autobrev6og18ÅrDTO( + fagsakId = behandling.fagsak.id, + alder = Alder.ATTEN.år, + årMåned = inneværendeMåned(), + ) + + autobrev6og18ÅrService.opprettOmregningsoppgaveForBarnIBrytingsalder(autobrev6og18ÅrDTO) + + verify(exactly = 0) { stegService.håndterVilkårsvurdering(any()) } + } + + @Test + fun `Verifiser at behandling for omregning ikke opprettes om barn med angitt ålder ikke finnes`() { + val behandling = initMock(alder = 7, medSøsken = false).first + + val autobrev6og18ÅrDTO = Autobrev6og18ÅrDTO( + fagsakId = behandling.fagsak.id, + alder = Alder.SEKS.år, + årMåned = inneværendeMåned(), + ) + + autobrev6og18ÅrService.opprettOmregningsoppgaveForBarnIBrytingsalder(autobrev6og18ÅrDTO) + + verify(exactly = 0) { stegService.håndterVilkårsvurdering(any()) } + } + + @Test + fun `Verifiser at behandling for omregning ikke opprettes om fagsak ikke er løpende`() { + val behandling = initMock(fagsakStatus = FagsakStatus.OPPRETTET, alder = 6, medSøsken = false).first + + val autobrev6og18ÅrDTO = Autobrev6og18ÅrDTO( + fagsakId = behandling.fagsak.id, + alder = Alder.SEKS.år, + årMåned = inneværendeMåned(), + ) + + autobrev6og18ÅrService.opprettOmregningsoppgaveForBarnIBrytingsalder(autobrev6og18ÅrDTO) + + verify(exactly = 0) { stegService.håndterVilkårsvurdering(any()) } + } + + @Test + fun `Verifiser at behandling for omregning blir trigget for løpende fagsak med barn som fyller 6 år inneværende måned`() { + val behandling = initMock(alder = 6, medSøsken = false).first + + val autobrev6og18ÅrDTO = Autobrev6og18ÅrDTO( + fagsakId = behandling.fagsak.id, + alder = Alder.SEKS.år, + årMåned = inneværendeMåned(), + ) + + every { stegService.håndterVilkårsvurdering(any()) } returns behandling + every { stegService.håndterNyBehandling(any()) } returns behandling + every { vedtaksperiodeService.oppdaterFortsattInnvilgetPeriodeMedAutobrevBegrunnelse(any(), any()) } just runs + every { taskRepository.save(any()) } returns Task(type = "test", payload = "") + every { autovedtakStegService.kjørBehandlingOmregning(any(), any()) } returns "" + + autobrev6og18ÅrService.opprettOmregningsoppgaveForBarnIBrytingsalder(autobrev6og18ÅrDTO) + + verify(exactly = 1) { + autovedtakStegService.kjørBehandlingOmregning( + any(), + any(), + ) + } + } + + @Test + fun `Verifiser at behandling for omregning blir trigget for løpende fagsak med barn som fyller 18 år inneværende måned og som har søsken`() { + val behandling = initMock(alder = 18, medSøsken = true).first + + val autobrev6og18ÅrDTO = Autobrev6og18ÅrDTO( + fagsakId = behandling.fagsak.id, + alder = Alder.ATTEN.år, + årMåned = inneværendeMåned(), + ) + + every { stegService.håndterVilkårsvurdering(any()) } returns behandling + every { stegService.håndterNyBehandling(any()) } returns behandling + every { vedtaksperiodeService.oppdaterFortsattInnvilgetPeriodeMedAutobrevBegrunnelse(any(), any()) } just runs + every { taskRepository.save(any()) } returns Task(type = "test", payload = "") + every { autovedtakStegService.kjørBehandlingOmregning(any(), any()) } returns "" + + autobrev6og18ÅrService.opprettOmregningsoppgaveForBarnIBrytingsalder(autobrev6og18ÅrDTO) + + verify(exactly = 1) { + autovedtakStegService.kjørBehandlingOmregning( + any(), + any(), + ) + } + } + + @Test + fun `Verifiser at behandling for omregning ikke blir trigget for løpende fagsak med barn som fyller 6år inneværende måned, hvis barnet ikke har løpende andel tilkjent ytelse`() { + val behandling = initMock(alder = 6, medSøsken = false).first + + val autobrev6og18ÅrDTO = Autobrev6og18ÅrDTO( + fagsakId = behandling.fagsak.id, + alder = Alder.SEKS.år, + årMåned = inneværendeMåned(), + ) + + every { stegService.håndterVilkårsvurdering(any()) } returns behandling + every { stegService.håndterNyBehandling(any()) } returns behandling + + val barn6årUtenAktivTilkjentYtelse = + tilfeldigPerson(LocalDate.now().minusYears(Alder.SEKS.år.toLong()).minusMonths(1)) + val barn10årMedAktivTilkjentYtelse = tilfeldigPerson(LocalDate.now().minusYears(10)) + + every { + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling( + behandling.id, + ) + } returns listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = YearMonth.now().minusMonths(1), // en gammel ytelse + beløp = 1054, + person = barn6årUtenAktivTilkjentYtelse, + ), + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = YearMonth.now().plusYears(4), + beløp = 1054, + person = barn10årMedAktivTilkjentYtelse, // den aktive er på et annet barn + ), + ) + every { vedtaksperiodeService.oppdaterFortsattInnvilgetPeriodeMedAutobrevBegrunnelse(any(), any()) } just runs + every { taskRepository.save(any()) } returns Task(type = "test", payload = "") + every { autovedtakStegService.kjørBehandlingOmregning(any(), any()) } returns "" + + autobrev6og18ÅrService.opprettOmregningsoppgaveForBarnIBrytingsalder(autobrev6og18ÅrDTO) + + verify(exactly = 0) { + autovedtakStegService.kjørBehandlingOmregning( + any(), + any(), + ) + } + } + + @Test + fun `Verifiser at behandling for omregning ikke blir trigget for løpende fagsak med barn som fyller 18år inneværende måned, hvis barnet ikke har løpende andel tilkjent ytelse`() { + val (behandling, _, barnIBrytningsalder) = initMock(alder = 18, medSøsken = true) + + val autobrev6og18ÅrDTO = Autobrev6og18ÅrDTO( + fagsakId = behandling.fagsak.id, + alder = Alder.ATTEN.år, + årMåned = inneværendeMåned(), + ) + + every { stegService.håndterVilkårsvurdering(any()) } returns behandling + every { stegService.håndterNyBehandling(any()) } returns behandling + + val barn10årMedAktivTilkjentYtelse = tilfeldigPerson(LocalDate.now().minusYears(10)) + + every { + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling( + behandling.id, + ) + } returns listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = YearMonth.now().minusMonths(2), // en gammel ytelse + beløp = 1054, + person = barnIBrytningsalder, + ), + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = YearMonth.now().plusYears(4), + beløp = 1054, + person = barn10årMedAktivTilkjentYtelse, // den aktive er på et annet barn + ), + ) + every { vedtaksperiodeService.oppdaterFortsattInnvilgetPeriodeMedAutobrevBegrunnelse(any(), any()) } just runs + every { taskRepository.save(any()) } returns Task(type = "test", payload = "") + every { autovedtakStegService.kjørBehandlingOmregning(any(), any()) } returns "" + + autobrev6og18ÅrService.opprettOmregningsoppgaveForBarnIBrytingsalder(autobrev6og18ÅrDTO) + + verify(exactly = 0) { + autovedtakStegService.kjørBehandlingOmregning( + any(), + any(), + ) + } + } + + @Test + fun `Verifiser at vi ikke oppretter behandling hvis brev er sendt fra infotrygd`() { + val behandling = initMock(alder = 6, medSøsken = false).first + + val autobrev6og18ÅrDTO = Autobrev6og18ÅrDTO( + fagsakId = behandling.fagsak.id, + alder = Alder.SEKS.år, + årMåned = inneværendeMåned(), + ) + + every { infotrygdService.harSendtbrev(any(), any()) } returns true + every { stegService.håndterVilkårsvurdering(any()) } returns behandling + every { stegService.håndterNyBehandling(any()) } returns behandling + every { vedtaksperiodeService.oppdaterFortsattInnvilgetPeriodeMedAutobrevBegrunnelse(any(), any()) } just runs + every { taskRepository.save(any()) } returns Task(type = "test", payload = "") + autobrev6og18ÅrService.opprettOmregningsoppgaveForBarnIBrytingsalder(autobrev6og18ÅrDTO) + + verify(exactly = 0) { + autovedtakService.opprettAutomatiskBehandlingOgKjørTilBehandlingsresultat( + any(), + any(), + any(), + any(), + ) + } + verify(exactly = 0) { + vedtaksperiodeService.oppdaterFortsattInnvilgetPeriodeMedAutobrevBegrunnelse( + any(), + any(), + ) + } + verify(exactly = 0) { autovedtakService.opprettToTrinnskontrollOgVedtaksbrevForAutomatiskBehandling(any()) } + verify(exactly = 0) { taskRepository.save(any()) } + } + + private fun initMock( + behandlingStatus: BehandlingStatus = BehandlingStatus.AVSLUTTET, + fagsakStatus: FagsakStatus = FagsakStatus.LØPENDE, + alder: Long, + medSøsken: Boolean, + ): Triple { + val behandling = lagBehandling().also { + it.fagsak.status = fagsakStatus + it.status = behandlingStatus + } + + val søker = tilfeldigSøker() + var barnIBrytningsalder: Person = tilfeldigPerson(LocalDate.now().minusYears(alder)) + var søsken: Person = tilfeldigPerson(LocalDate.now().minusYears(3)) + + if (alder == 6L) { + val andelTilkjentYtelseSøsken = if (medSøsken) { + null + } else { + lagAndelTilkjentYtelse( + fom = søsken.fødselsdato.toYearMonth(), + tom = søsken.fødselsdato.plusYears(6).toYearMonth(), + beløp = 1676, + person = søsken, + ) + } + every { + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling( + behandling.id, + ) + } returns listOfNotNull( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusMonths(10), + tom = inneværendeMåned().minusMonths(1), + beløp = 1676, + person = barnIBrytningsalder, + ), + lagAndelTilkjentYtelse( + fom = inneværendeMåned(), + tom = YearMonth.now().plusYears(12), + beløp = 1054, + person = barnIBrytningsalder, + ), + andelTilkjentYtelseSøsken, + ) + } else if (alder == 18L) { + barnIBrytningsalder = tilfeldigPerson(LocalDate.now().minusYears(18)) + val andelTilkjentYtelseSøsken = if (medSøsken) { + null + } else { + lagAndelTilkjentYtelse( + fom = søsken.fødselsdato.toYearMonth(), + tom = søsken.fødselsdato.plusYears(6).toYearMonth(), + beløp = 1676, + person = søsken, + ) + } + every { + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling( + behandling.id, + ) + } returns listOfNotNull( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(12), + tom = inneværendeMåned().minusMonths(1), + beløp = 1054, + person = barnIBrytningsalder, + ), + andelTilkjentYtelseSøsken, + ) + } + + every { infotrygdService.harSendtbrev(any(), any()) } returns false + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(behandling.fagsak.id) } returns behandling + every { behandlingService.opprettBehandling(any()) } returns behandling + every { behandlingHentOgPersisterService.hentBehandlinger(any()) } returns emptyList() + every { behandlingService.harBehandlingsårsakAlleredeKjørt(any(), any(), any()) } returns false + + val personer = + if (medSøsken) arrayOf(søker, barnIBrytningsalder, søsken) else arrayOf(søker, barnIBrytningsalder) + every { personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = behandling.id) } returns lagTestPersonopplysningGrunnlag( + behandling.id, + *personer, + ) + return Triple(behandling, søker, barnIBrytningsalder) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevOpph\303\270rSm\303\245barnstilleggServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevOpph\303\270rSm\303\245barnstilleggServiceTest.kt" new file mode 100644 index 000000000..b90c75ccc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevOpph\303\270rSm\303\245barnstilleggServiceTest.kt" @@ -0,0 +1,316 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.omregning + +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.førsteDagINesteMåned +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakService +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakStegService +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.StartSatsendring +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.småbarnstillegg.PeriodeOvergangsstønadGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.småbarnstillegg.PeriodeOvergangsstønadGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.Vedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.kontrakter.felles.ef.Datakilde +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.time.LocalDate + +internal class AutobrevOpphørSmåbarnstilleggServiceTest { + private val autovedtakService = mockk() + private val autovedtakStegService = mockk() + private val fagsakService = mockk(relaxed = true) + private val persongrunnlagService = mockk() + private val behandlingService = mockk() + private val behandlingHentOgPersisterService = mockk() + private val infotrygdService = mockk(relaxed = true) + private val stegService = mockk() + private val vedtakService = mockk(relaxed = true) + private val taskRepository = mockk(relaxed = true) + private val vedtaksperiodeService = mockk() + private val periodeOvergangsstønadGrunnlagRepository = mockk() + private val startSatsendring = mockk(relaxed = true) + + private val autovedtakBrevService = AutovedtakBrevService( + fagsakService = fagsakService, + behandlingService = behandlingService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + infotrygdService = infotrygdService, + autovedtakService = autovedtakService, + vedtakService = vedtakService, + vedtaksperiodeService = vedtaksperiodeService, + taskRepository = taskRepository, + ) + + private val autobrevOpphørSmåbarnstilleggService = AutobrevOpphørSmåbarnstilleggService( + autovedtakBrevService = autovedtakBrevService, + persongrunnlagService = persongrunnlagService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + periodeOvergangsstønadGrunnlagRepository = periodeOvergangsstønadGrunnlagRepository, + autovedtakStegService = autovedtakStegService, + startSatsendring = startSatsendring, + ) + + @Test + fun `Verifiser at løpende fagsak med småbarnstillegg sender opphørsbrev måneden etter yngste barn ble 3 år`() { + val behandling = lagBehandling() + val barn3ÅrForrigeMåned = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(3).minusMonths(1)) + val personopplysningGrunnlag: PersonopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandlingId = behandlingId, barn3ÅrForrigeMåned) + + every { behandlingHentOgPersisterService.finnAktivForFagsak(any()) } returns behandling + every { behandlingHentOgPersisterService.hentBehandlinger(any()) } returns listOf(behandling) + every { behandlingService.harBehandlingsårsakAlleredeKjørt(any(), any(), any()) } returns false + every { persongrunnlagService.hentAktivThrows(any()) } returns personopplysningGrunnlag + every { periodeOvergangsstønadGrunnlagRepository.findByBehandlingId(any()) } returns emptyList() + every { stegService.håndterVilkårsvurdering(any()) } returns behandling + every { stegService.håndterNyBehandling(any()) } returns behandling + every { vedtaksperiodeService.oppdaterFortsattInnvilgetPeriodeMedAutobrevBegrunnelse(any(), any()) } just runs + every { autovedtakStegService.kjørBehandlingOmregning(any(), any()) } returns "" + + autobrevOpphørSmåbarnstilleggService + .kjørBehandlingOgSendBrevForOpphørAvSmåbarnstillegg(fagsakId = behandling.fagsak.id) + + verify(exactly = 1) { + autovedtakStegService.kjørBehandlingOmregning( + any(), + any(), + ) + } + } + + @Test + fun `Skal ikke sende lage autobrevbehandling om det i forrige måned ble vedtatt en reduksjon på småbarnstillegg`() { + val behandling = lagBehandling().apply { + status = BehandlingStatus.AVSLUTTET + } + val vedtak = lagVedtak(behandling = behandling) + + val barn3ÅrForrigeMåned = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(3).minusMonths(1)) + val personopplysningGrunnlag: PersonopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandlingId = behandlingId, barn3ÅrForrigeMåned) + + val vedtaksperioder = listOf( + VedtaksperiodeMedBegrunnelser( + fom = LocalDate.now().førsteDagIInneværendeMåned(), + vedtak = vedtak, + type = Vedtaksperiodetype.UTBETALING, + ).apply { + begrunnelser.addAll( + listOf( + Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_BARN_UNDER_TRE_ÅR, + ).map { begrunnelse -> + Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = this, + standardbegrunnelse = begrunnelse, + ) + }, + ) + }, + ) + + every { behandlingHentOgPersisterService.finnAktivForFagsak(any()) } returns behandling + every { behandlingHentOgPersisterService.hentBehandlinger(behandling.fagsak.id) } returns listOf(behandling) + every { vedtaksperiodeService.hentPersisterteVedtaksperioder(any()) } returns vedtaksperioder + every { behandlingService.harBehandlingsårsakAlleredeKjørt(any(), any(), any()) } returns false + every { persongrunnlagService.hentAktivThrows(any()) } returns personopplysningGrunnlag + every { periodeOvergangsstønadGrunnlagRepository.findByBehandlingId(any()) } returns listOf( + lagPeriodeOvergangsstønadGrunnlag(LocalDate.now().minusYears(1), LocalDate.now().plusYears(1)), + ) + + autobrevOpphørSmåbarnstilleggService + .kjørBehandlingOgSendBrevForOpphørAvSmåbarnstillegg(fagsakId = behandling.fagsak.id) + + verify(exactly = 0) { + autovedtakService.opprettAutomatiskBehandlingOgKjørTilBehandlingsresultat( + any(), + any(), + any(), + any(), + ) + } + + verify(exactly = 0) { taskRepository.save(any()) } + } + + @Test + fun `Verifiser at behandling ikke blir opprettet om behandling allerede har kjørt`() { + val behandling = lagBehandling() + val barn3ÅrForrigeMåned = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(3).minusMonths(1)) + val personopplysningGrunnlag: PersonopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandlingId = behandlingId, barn3ÅrForrigeMåned) + + every { behandlingHentOgPersisterService.finnAktivForFagsak(any()) } returns behandling + every { behandlingService.harBehandlingsårsakAlleredeKjørt(any(), any(), any()) } returns true + every { persongrunnlagService.hentAktivThrows(any()) } returns personopplysningGrunnlag + every { periodeOvergangsstønadGrunnlagRepository.findByBehandlingId(any()) } returns emptyList() + every { stegService.håndterVilkårsvurdering(any()) } returns behandling + every { stegService.håndterNyBehandling(any()) } returns behandling + every { vedtaksperiodeService.oppdaterFortsattInnvilgetPeriodeMedAutobrevBegrunnelse(any(), any()) } just runs + + autobrevOpphørSmåbarnstilleggService + .kjørBehandlingOgSendBrevForOpphørAvSmåbarnstillegg(fagsakId = behandling.fagsak.id) + + verify(exactly = 0) { + autovedtakService.opprettAutomatiskBehandlingOgKjørTilBehandlingsresultat( + any(), + any(), + any(), + any(), + ) + } + + verify(exactly = 0) { taskRepository.save(any()) } + } + + @Test + fun `overgangstønadOpphørteForrigeMåned - en periode med opphør denne måneden gir false`() { + val fom = LocalDate.now().minusYears(1) + val tom = LocalDate.now() + val input: List = listOf( + lagPeriodeOvergangsstønadGrunnlag(fom, tom), + ) + val overgangstønadOpphørteForrigeMåned = + autobrevOpphørSmåbarnstilleggService.overgangstønadOpphørteForrigeMåned(input) + assertFalse(overgangstønadOpphørteForrigeMåned) + } + + @Test + fun `overgangstønadOpphørteForrigeMåned - tom liste gir false`() { + val input: List = emptyList() + val overgangstønadOpphørteForrigeMåned = + autobrevOpphørSmåbarnstilleggService.overgangstønadOpphørteForrigeMåned(input) + assertFalse(overgangstønadOpphørteForrigeMåned) + } + + @Test + fun `overgangstønadOpphørteForrigeMåned - neste måned gir false`() { + val fom = LocalDate.now().minusYears(1) + val tom = LocalDate.now().førsteDagINesteMåned() + val input: List = listOf( + lagPeriodeOvergangsstønadGrunnlag(fom, tom), + ) + val overgangstønadOpphørteForrigeMåned = + autobrevOpphørSmåbarnstilleggService.overgangstønadOpphørteForrigeMåned(input) + assertFalse(overgangstønadOpphørteForrigeMåned) + } + + @Test + fun `overgangstønadOpphørteForrigeMåned - forrige måned gir true`() { + val fom = LocalDate.now().minusYears(1) + val tom = LocalDate.now().minusMonths(1) + val input: List = listOf( + lagPeriodeOvergangsstønadGrunnlag(fom, tom), + ) + val overgangstønadOpphørteForrigeMåned = + autobrevOpphørSmåbarnstilleggService.overgangstønadOpphørteForrigeMåned(input) + assertTrue(overgangstønadOpphørteForrigeMåned) + } + + @Test + fun `overgangstønadOpphørteForrigeMåned - ett år siden gir false`() { + val fom = LocalDate.now().minusYears(1) + val tom = LocalDate.now().minusYears(1) + val input: List = listOf( + lagPeriodeOvergangsstønadGrunnlag(fom, tom), + ) + val overgangstønadOpphørteForrigeMåned = + autobrevOpphørSmåbarnstilleggService.overgangstønadOpphørteForrigeMåned(input) + assertFalse(overgangstønadOpphørteForrigeMåned) + } + + val behandlingId: Long = 1 + + @Test + fun `minsteBarnFylteTreÅrForrigeMåned - et barn som fylte tre forrige måned gir true`() { + val barn3ForrigeMåned = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(3).minusMonths(1)) + val peronsopplysningGrunnalg: PersonopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandlingId = behandlingId, barn3ForrigeMåned) + + assertTrue(autobrevOpphørSmåbarnstilleggService.yngsteBarnFylteTreÅrForrigeMåned(peronsopplysningGrunnalg)) + } + + @Test + fun `minsteBarnFylteTreÅrForrigeMåned - et barn som fylte tre forrige måned og et eldre gir true`() { + val barn3ForrigeMåned = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(3).minusMonths(1)) + val barnOverTre = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(4)) + val peronsopplysningGrunnalg: PersonopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandlingId = behandlingId, barn3ForrigeMåned, barnOverTre) + + assertTrue(autobrevOpphørSmåbarnstilleggService.yngsteBarnFylteTreÅrForrigeMåned(peronsopplysningGrunnalg)) + } + + @Test + fun `minsteBarnFylteTreÅrForrigeMåned - to barn som fylte tre forrige måned gir true`() { + val barn3ForrigeMåned = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(3).minusMonths(1)) + val ekstraBarn3ForrigeMåned = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(3).minusMonths(1)) + val peronsopplysningGrunnalg: PersonopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandlingId = behandlingId, barn3ForrigeMåned, ekstraBarn3ForrigeMåned) + + assertTrue(autobrevOpphørSmåbarnstilleggService.yngsteBarnFylteTreÅrForrigeMåned(peronsopplysningGrunnalg)) + } + + @Test + fun `minsteBarnFylteTreÅrForrigeMåned - to barn over tre gir false`() { + val barnOverTre = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(4)) + val ekstraBarnOverTre = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(4)) + val peronsopplysningGrunnalg: PersonopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandlingId = behandlingId, barnOverTre, ekstraBarnOverTre) + + assertFalse(autobrevOpphørSmåbarnstilleggService.yngsteBarnFylteTreÅrForrigeMåned(peronsopplysningGrunnalg)) + } + + @Test + fun `minsteBarnFylteTreÅrForrigeMåned - to barn under tre gir false`() { + val barnUnderTre = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(2)) + val ekstraBarnUnderTre = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(3)) + val peronsopplysningGrunnalg: PersonopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandlingId = behandlingId, barnUnderTre, ekstraBarnUnderTre) + + assertFalse(autobrevOpphørSmåbarnstilleggService.yngsteBarnFylteTreÅrForrigeMåned(peronsopplysningGrunnalg)) + } + + @Test + fun `minsteBarnFylteTreÅrForrigeMåned - et barn under tre og et barn 3 forrige måned gir false`() { + val barnUnderTre = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(2)) + val barn3ForrigeMåned = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(3).minusMonths(1)) + val peronsopplysningGrunnalg: PersonopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandlingId = behandlingId, barnUnderTre, barn3ForrigeMåned) + + assertFalse(autobrevOpphørSmåbarnstilleggService.yngsteBarnFylteTreÅrForrigeMåned(peronsopplysningGrunnalg)) + } + + private fun lagPeriodeOvergangsstønadGrunnlag( + fom: LocalDate, + tom: LocalDate, + ) = PeriodeOvergangsstønadGrunnlag( + id = 1, + behandlingId = 1, + aktør = randomAktør(), + fom = fom, + tom = tom, + datakilde = Datakilde.EF, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevTaskTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevTaskTest.kt new file mode 100644 index 000000000..2fbe68c4b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevTaskTest.kt @@ -0,0 +1,61 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.omregning + +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.prosessering.domene.Task +import org.junit.jupiter.api.Test + +internal class AutobrevTaskTest { + + val fagsakRepository = mockk() + val opprettTaskService = mockk() + val behandlingService = mockk() + val behandlingHentOgPersisterService = mockk() + + private val autobrevTask = AutobrevTask( + fagsakRepository = fagsakRepository, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + opprettTaskService = opprettTaskService, + ) + + private val autoBrevTask = Task( + type = AutobrevTask.TASK_STEP_TYPE, + payload = "", + ) + + @Test + fun `oppretter autobrev tasker for 6 år, 2 for 18 år og 1 for småbarnstillegg`() { + val fagsaker = setOf( + Fagsak(1, aktør = tilAktør(randomFnr())), + Fagsak(2, aktør = tilAktør(randomFnr())), + ) + + every { fagsakRepository.finnLøpendeFagsakMedBarnMedFødselsdatoInnenfor(any(), any()) } answers { fagsaker } + every { fagsakRepository.finnAlleFagsakerMedOpphørSmåbarnstilleggIMåned(any()) } returns listOf(1L) + every { opprettTaskService.opprettAutovedtakFor6Og18ÅrBarn(any(), any()) } just runs + every { opprettTaskService.opprettAutovedtakForOpphørSmåbarnstilleggTask(any()) } just runs + every { behandlingHentOgPersisterService.partitionByIverksatteBehandlinger(any()) } returns listOf(1L) + + autobrevTask.doTask(autoBrevTask) + + verify(exactly = 2) { + opprettTaskService.opprettAutovedtakFor6Og18ÅrBarn(any(), 6) + } + verify(exactly = 2) { + opprettTaskService.opprettAutovedtakFor6Og18ÅrBarn(any(), 18) + } + verify(exactly = 1) { + opprettTaskService.opprettAutovedtakForOpphørSmåbarnstilleggTask(1L) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevUtilsTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevUtilsTest.kt new file mode 100644 index 000000000..e408ce4b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/omregning/AutobrevUtilsTest.kt @@ -0,0 +1,35 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.omregning + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class AutobrevUtilsTest { + + @Test + fun `Skal sjekke at historiske og gjeldene begrunnelser blir hentet for 6 år`() { + val begrunnelser = AutobrevUtils.hentStandardbegrunnelserReduksjonForAlder(alder = 6) + + assertEquals(listOf("REDUKSJON_UNDER_6_ÅR_AUTOVEDTAK", "REDUKSJON_UNDER_6_ÅR"), begrunnelser.map { it.name }) + } + + @Test + fun `Skal sjekke at historiske og gjeldene begrunnelser blir hentet for 18 år`() { + val begrunnelser = AutobrevUtils.hentStandardbegrunnelserReduksjonForAlder(alder = 18) + + assertEquals(listOf("REDUKSJON_UNDER_18_ÅR_AUTOVEDTAK", "REDUKSJON_UNDER_18_ÅR"), begrunnelser.map { it.name }) + } + + @Test + fun `Skal sjekke at gjeldende begrunnelse for autobrev er i listen over alle`() { + assertTrue( + AutobrevUtils.hentStandardbegrunnelserReduksjonForAlder(6) + .contains(AutobrevUtils.hentGjeldendeVedtakbegrunnelseReduksjonForAlder(6)), + ) + + assertTrue( + AutobrevUtils.hentStandardbegrunnelserReduksjonForAlder(18) + .contains(AutobrevUtils.hentGjeldendeVedtakbegrunnelseReduksjonForAlder(18)), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringUtilTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringUtilTest.kt new file mode 100644 index 000000000..f30d68a2f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/SatsendringUtilTest.kt @@ -0,0 +1,338 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.satsendring + +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.SatsService +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +class SatsendringUtilTest { + + private val UGYLDIG_SATS = 1000 + + @Test + fun `Skal returnere true dersom vi har siste sats`() { + val andelerMedSisteSats = SatsType.values() + .filter { it != SatsType.FINN_SVAL } + .map { + val sisteSats = SatsService.finnSisteSatsFor(it) + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = sisteSats.gyldigFom.toYearMonth(), + tom = sisteSats.gyldigTom.toYearMonth(), + sats = sisteSats.beløp, + ytelseType = it.tilYtelseType(), + ) + } + + assertTrue(andelerMedSisteSats.erOppdatertMedSisteSatser()) + } + + @Test + fun `Skal returnere true dersom vi har siste sats selv om alle perioder er fram i tid`() { + val andelerMedSisteSats = SatsType.values() + .filter { it != SatsType.FINN_SVAL } + .map { + val sisteSats = SatsService.finnSisteSatsFor(it) + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = sisteSats.gyldigFom.toYearMonth().plusYears(1), + tom = sisteSats.gyldigFom.toYearMonth().plusYears(1), + sats = sisteSats.beløp, + ytelseType = it.tilYtelseType(), + ) + } + + assertTrue(andelerMedSisteSats.erOppdatertMedSisteSatser()) + } + + @Test + fun `Skal returnere false dersom vi ikke har siste sats`() { + SatsType.values() + .filter { it != SatsType.FINN_SVAL } + .forEach { + val sisteSats = SatsService.finnSisteSatsFor(it) + val andelerMedFeilSats = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = sisteSats.gyldigFom.toYearMonth(), + tom = sisteSats.gyldigTom.toYearMonth(), + sats = sisteSats.beløp - 1, + ytelseType = it.tilYtelseType(), + ), + ) + + assertFalse(andelerMedFeilSats.erOppdatertMedSisteSatser()) + } + } + + @Test + fun `Skal ignorere andeler som kommer før siste sats`() { + SatsType.values() + .filter { it != SatsType.FINN_SVAL } + .forEach { + val sisteSats = SatsService.finnSisteSatsFor(it) + val andelerSomErFørSisteSats = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = sisteSats.gyldigFom.toYearMonth().minusMonths(100), + tom = sisteSats.gyldigFom.toYearMonth().minusMonths(1), + sats = sisteSats.beløp - 1, + ytelseType = it.tilYtelseType(), + ), + ) + + assertTrue(andelerSomErFørSisteSats.erOppdatertMedSisteSatser()) + } + } + + @Test + fun `Skal ikke returnere false dersom vi ikke har siste sats, men de er redusert til 0 prosent`() { + SatsType.values() + .filter { it != SatsType.FINN_SVAL } + .forEach { + val sisteSats = SatsService.finnSisteSatsFor(it) + val andelerMedFeilSats = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = sisteSats.gyldigFom.toYearMonth(), + tom = sisteSats.gyldigTom.toYearMonth(), + sats = sisteSats.beløp - 1, + prosent = BigDecimal.ZERO, + ytelseType = it.tilYtelseType(), + ), + ) + + assertTrue(andelerMedFeilSats.erOppdatertMedSisteSatser()) + } + } + + @Test + fun `harAlleredeSatsendring skal returnere true hvis den har siste satsendring`() { + val behandling = lagBehandling() + val atyMedBareSmåbarnstillegg = + lagAndelTilkjentYtelseMedEndreteUtbetalingerIPeriodenRundtSisteSatsenring( + SatsType.SMA, + behandling, + YtelseType.SMÅBARNSTILLEGG, + ) + + Assertions.assertThat(atyMedBareSmåbarnstillegg.erOppdatertMedSisteSatser()).isEqualTo(true) + + val atyMedBareUtvidet = + lagAndelTilkjentYtelseMedEndreteUtbetalingerIPeriodenRundtSisteSatsenring( + SatsType.UTVIDET_BARNETRYGD, + behandling, + YtelseType.UTVIDET_BARNETRYGD, + ) + + Assertions.assertThat(atyMedBareUtvidet.erOppdatertMedSisteSatser()).isEqualTo(true) + + val atyMedBareOrba = + lagAndelTilkjentYtelseMedEndreteUtbetalingerIPeriodenRundtSisteSatsenring( + SatsType.ORBA, + behandling, + YtelseType.ORDINÆR_BARNETRYGD, + ) + + Assertions.assertThat(atyMedBareOrba.erOppdatertMedSisteSatser()).isEqualTo(true) + + val atyMedBareTilleggOrba = + lagAndelTilkjentYtelseMedEndreteUtbetalingerIPeriodenRundtSisteSatsenring( + SatsType.TILLEGG_ORBA, + behandling, + YtelseType.ORDINÆR_BARNETRYGD, + ) + + Assertions.assertThat(atyMedBareTilleggOrba.erOppdatertMedSisteSatser()).isEqualTo(true) + + Assertions.assertThat( + (atyMedBareTilleggOrba + atyMedBareOrba + atyMedBareUtvidet + atyMedBareSmåbarnstillegg) + .erOppdatertMedSisteSatser(), + ).isEqualTo(true) + } + + @Test + fun `harAlleredeSatsendring skal returnere false hvis den har gammel satsendring`() { + val behandling = lagBehandling() + val atyMedUgyldigSatsSmåbarnstillegg = + lagAndelTilkjentYtelseMedEndreteUtbetalingerIPeriodenRundtSisteSatsenring( + SatsType.SMA, + behandling, + YtelseType.SMÅBARNSTILLEGG, + UGYLDIG_SATS, + ) + + Assertions.assertThat(atyMedUgyldigSatsSmåbarnstillegg.erOppdatertMedSisteSatser()).isEqualTo(false) + + val atyMedUglydligSatsUtvidet = + lagAndelTilkjentYtelseMedEndreteUtbetalingerIPeriodenRundtSisteSatsenring( + SatsType.UTVIDET_BARNETRYGD, + behandling, + YtelseType.UTVIDET_BARNETRYGD, + UGYLDIG_SATS, + ) + + Assertions.assertThat(atyMedUglydligSatsUtvidet.erOppdatertMedSisteSatser()).isEqualTo(false) + + val atyMedUgyldigSatsBareOrba = + lagAndelTilkjentYtelseMedEndreteUtbetalingerIPeriodenRundtSisteSatsenring( + SatsType.ORBA, + behandling, + YtelseType.ORDINÆR_BARNETRYGD, + UGYLDIG_SATS, + ) + + Assertions.assertThat(atyMedUgyldigSatsBareOrba.erOppdatertMedSisteSatser()).isEqualTo(false) + + val atyMedUgyldigSatsTilleggOrba = + lagAndelTilkjentYtelseMedEndreteUtbetalingerIPeriodenRundtSisteSatsenring( + SatsType.TILLEGG_ORBA, + behandling, + YtelseType.ORDINÆR_BARNETRYGD, + UGYLDIG_SATS, + ) + + Assertions.assertThat(atyMedUgyldigSatsTilleggOrba.erOppdatertMedSisteSatser()).isEqualTo(false) + } + + @Test + fun `harAlleredeSatsendring skal returnere false en av satsene ikke er ny`() { + val behandling = lagBehandling() + val atyMedUgyldigSatsSmåbarnstillegg = + lagAndelTilkjentYtelseMedEndreteUtbetalingerIPeriodenRundtSisteSatsenring( + SatsType.SMA, + behandling, + YtelseType.SMÅBARNSTILLEGG, + UGYLDIG_SATS, + ) + + val atyMedGyldigUtvidet = + lagAndelTilkjentYtelseMedEndreteUtbetalingerIPeriodenRundtSisteSatsenring( + SatsType.UTVIDET_BARNETRYGD, + behandling, + YtelseType.UTVIDET_BARNETRYGD, + ) + + val atyMedBGyldigOrba = + lagAndelTilkjentYtelseMedEndreteUtbetalingerIPeriodenRundtSisteSatsenring( + SatsType.ORBA, + behandling, + YtelseType.ORDINÆR_BARNETRYGD, + ) + + Assertions.assertThat( + (atyMedBGyldigOrba + atyMedGyldigUtvidet + atyMedUgyldigSatsSmåbarnstillegg).erOppdatertMedSisteSatser(), + ).isEqualTo(false) + } + + @Test + fun `harAlleredeSatsendring skal returnere true på ytelse med rett sats når tom dato er på samme dato som satstidspunkt`() { + val behandling = lagBehandling() + val atySomGårUtPåSatstidspunktGyldig = + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = datoForSisteSatsendringForSatsType(SatsType.ORBA).minusMonths(1), + tom = datoForSisteSatsendringForSatsType(SatsType.ORBA), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + behandling = behandling, + person = lagPerson(), + aktør = lagPerson().aktør, + periodeIdOffset = 1, + beløp = SatsService.finnSisteSatsFor(SatsType.ORBA).beløp, + ) + + Assertions.assertThat(listOf(atySomGårUtPåSatstidspunktGyldig).erOppdatertMedSisteSatser()).isEqualTo(true) + + val atySomGårUtPåSatstidspunktUgyldig = + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = datoForSisteSatsendringForSatsType(SatsType.ORBA).minusMonths(1), + tom = datoForSisteSatsendringForSatsType(SatsType.ORBA), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + behandling = behandling, + person = lagPerson(), + aktør = lagPerson().aktør, + periodeIdOffset = 1, + beløp = UGYLDIG_SATS, + ) + + Assertions.assertThat(listOf(atySomGårUtPåSatstidspunktUgyldig).erOppdatertMedSisteSatser()).isEqualTo(false) + } + + @Test + fun `harAlleredeSatsendring skal returnere true hvis ingen aktive andel tilkjent ytelser`() { + val behandling = lagBehandling() + val utgåttAndelTilkjentYtelse = + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = datoForSisteSatsendringForSatsType(SatsType.ORBA).minusMonths(10), + tom = datoForSisteSatsendringForSatsType(SatsType.ORBA).minusMonths(1), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + behandling = behandling, + person = lagPerson(), + aktør = lagPerson().aktør, + periodeIdOffset = 1, + beløp = SatsService.finnSisteSatsFor(SatsType.ORBA).beløp, + ) + + Assertions.assertThat(listOf(utgåttAndelTilkjentYtelse).erOppdatertMedSisteSatser()).isEqualTo(true) + } + + @Test + fun `harAlleredeSatsendring skal returnere true for ny sats når fom er på satstidspunktet`() { + val behandling = lagBehandling() + val utgåttAndelTilkjentYtelse = + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = datoForSisteSatsendringForSatsType(SatsType.ORBA), + tom = datoForSisteSatsendringForSatsType(SatsType.ORBA).plusYears(10), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + behandling = behandling, + person = lagPerson(), + aktør = lagPerson().aktør, + periodeIdOffset = 1, + beløp = SatsService.finnSisteSatsFor(SatsType.ORBA).beløp, + ) + + Assertions.assertThat(listOf(utgåttAndelTilkjentYtelse).erOppdatertMedSisteSatser()).isEqualTo(true) + } + + @Test + fun `harAlleredeSatsendring skal returnere false for gammel sats når fom er på satstidspunktet`() { + val behandling = lagBehandling() + val utgåttAndelTilkjentYtelse = + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = datoForSisteSatsendringForSatsType(SatsType.ORBA), + tom = datoForSisteSatsendringForSatsType(SatsType.ORBA).plusYears(10), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + behandling = behandling, + person = lagPerson(), + aktør = lagPerson().aktør, + periodeIdOffset = 1, + beløp = UGYLDIG_SATS, + ) + + Assertions.assertThat(listOf(utgåttAndelTilkjentYtelse).erOppdatertMedSisteSatser()).isEqualTo(false) + } + + private fun lagAndelTilkjentYtelseMedEndreteUtbetalingerIPeriodenRundtSisteSatsenring( + satsType: SatsType, + behandling: Behandling, + ytelseType: YtelseType, + beløp: Int? = null, + ) = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = SatsService.finnSisteSatsFor(satsType).gyldigFom.minusMonths(1).toYearMonth(), + tom = SatsService.finnSisteSatsFor(satsType).gyldigFom.plusMonths(1).toYearMonth(), + ytelseType = ytelseType, + behandling = behandling, + person = lagPerson(), + aktør = lagPerson().aktør, + periodeIdOffset = 1, + beløp = beløp ?: SatsService.finnSisteSatsFor(satsType).beløp, + ), + ) + + private fun datoForSisteSatsendringForSatsType(satsType: SatsType) = + SatsService.finnSisteSatsFor(satsType).gyldigFom.toYearMonth() +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/StartSatsendringTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/StartSatsendringTest.kt new file mode 100644 index 000000000..99614fb3d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/StartSatsendringTest.kt @@ -0,0 +1,189 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.satsendring + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.StartSatsendring.Companion.SATSENDRINGMÅNED_MARS_2023 +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene.Satskjøring +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene.SatskjøringRepository +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.prosessering.domene.Task +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable + +internal class StartSatsendringTest { + + private val fagsakRepository: FagsakRepository = mockk() + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService = mockk() + private val satskjøringRepository: SatskjøringRepository = mockk() + private val featureToggleService: FeatureToggleService = mockk() + private val personidentService: PersonidentService = mockk() + private val autovedtakSatsendringService: AutovedtakSatsendringService = mockk() + private val satsendringService: SatsendringService = mockk() + private val taskRepository: TaskRepositoryWrapper = mockk() + + private lateinit var startSatsendring: StartSatsendring + + @BeforeEach + fun setUp() { + val satsSlot = slot() + every { satskjøringRepository.save(capture(satsSlot)) } answers { satsSlot.captured } + val taskSlot = slot() + every { taskRepository.save(capture(taskSlot)) } answers { taskSlot.captured } + val opprettTaskService = OpprettTaskService(taskRepository, satskjøringRepository) + + every { satsendringService.erFagsakOppdatertMedSisteSatser(any()) } returns true + + startSatsendring = spyk( + StartSatsendring( + fagsakRepository = fagsakRepository, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + opprettTaskService = opprettTaskService, + satskjøringRepository = satskjøringRepository, + featureToggleService = featureToggleService, + personidentService = personidentService, + autovedtakSatsendringService = autovedtakSatsendringService, + satsendringService = satsendringService, + ), + ) + } + + @Test + fun `start satsendring og opprett satsendringtask på sak hvis toggler er på `() { + every { featureToggleService.isEnabled(FeatureToggleConfig.SATSENDRING_ENABLET, false) } returns true + + val behandling = lagBehandling() + + every { fagsakRepository.finnLøpendeFagsakerForSatsendring(any(), any()) } returns PageImpl( + listOf(behandling.fagsak.id), + Pageable.ofSize(5), + 0, + ) + + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(behandling.fagsak.id) } returns behandling + + startSatsendring.startSatsendring(5) + + verify(exactly = 1) { taskRepository.save(any()) } + } + + @Test + fun `finnLøpendeFagsaker har totalt antall sider 3, så den skal kalle finnLøpendeFagsaker 3 ganger for å få 5 satsendringer`() { + every { featureToggleService.isEnabled(any(), any()) } returns true + every { featureToggleService.isEnabled(any()) } returns true + + val behandling = lagBehandling() + + every { fagsakRepository.finnLøpendeFagsakerForSatsendring(any(), any()) } returns PageImpl( + listOf(behandling.fagsak.id, behandling.fagsak.id), + Pageable.ofSize(2), // 5/2 gir totalt 3 sider, så finnLøpendeFagsakerForSatsendring skal trigges 3 ganger + 5, + ) + + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(behandling.fagsak.id) } returns behandling + + startSatsendring.startSatsendring(5) + + verify(exactly = 5) { taskRepository.save(any()) } + verify(exactly = 3) { fagsakRepository.finnLøpendeFagsakerForSatsendring(any(), any()) } + } + + @Test + fun `kanStarteSatsendringPåFagsak gir false når vi ikke har noen tidligere behandling`() { + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(1L) } returns null + every { satskjøringRepository.findByFagsakIdAndSatsTidspunkt(1L, any()) } returns Satskjøring( + fagsakId = 1L, + satsTidspunkt = SATSENDRINGMÅNED_MARS_2023, + ) + + assertFalse(startSatsendring.kanStarteSatsendringPåFagsak(1L)) + } + + @Test + fun `kanStarteSatsendringPåFagsak gir false når vi har en satskjøring for fagsaken i satskjøringsrepoet`() { + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(1L) } returns lagBehandling() + every { satskjøringRepository.findByFagsakIdAndSatsTidspunkt(1L, any()) } returns Satskjøring( + fagsakId = 1L, + satsTidspunkt = SATSENDRINGMÅNED_MARS_2023, + ) + + assertFalse(startSatsendring.kanStarteSatsendringPåFagsak(1L)) + } + + @Test + fun `kanStarteSatsendringPåFagsak gir false når harSisteSats er true`() { + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(1L) } returns lagBehandling() + every { satskjøringRepository.findByFagsakIdAndSatsTidspunkt(1L, any()) } returns null + every { satsendringService.erFagsakOppdatertMedSisteSatser(any()) } returns true + + assertFalse(startSatsendring.kanStarteSatsendringPåFagsak(1L)) + } + + @Test + fun `kanStarteSatsendringPåFagsak gir true når harSisteSats er false`() { + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(1L) } returns lagBehandling() + every { satskjøringRepository.findByFagsakIdAndSatsTidspunkt(1L, any()) } returns null + every { satsendringService.erFagsakOppdatertMedSisteSatser(any()) } returns false + + assertTrue(startSatsendring.kanStarteSatsendringPåFagsak(1L)) + } + + @Test + fun `kanGjennomføreSatsendringManuelt gir false når vi ikke har noen tidligere behandling`() { + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(1L) } returns null + + assertFalse(startSatsendring.kanGjennomføreSatsendringManuelt(1L)) + } + + @Test + fun `kanGjennomføreSatsendringManuelt gir false når harSisteSats er true`() { + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(1L) } returns lagBehandling() + every { satskjøringRepository.findByFagsakIdAndSatsTidspunkt(1L, any()) } returns null + every { satsendringService.erFagsakOppdatertMedSisteSatser(any()) } returns true + + assertFalse(startSatsendring.kanGjennomføreSatsendringManuelt(1L)) + } + + @Test + fun `opprettSatsendringSynkrontVedGammelSats skal kaste dersom man ikke kan starte satsendring`() { + every { startSatsendring.kanStarteSatsendringPåFagsak(any()) } returns false + + assertThrows { + startSatsendring.gjennomførSatsendringManuelt(0L) + } + } + + @Test + fun `kanGjennomføreSatsendringManuelt skal kaste feil for alle andre resultater enn OK`() { + every { startSatsendring.kanGjennomføreSatsendringManuelt(any()) } returns true + + SatsendringSvar.entries.forEach { + every { autovedtakSatsendringService.kjørBehandling(any()) } returns it + + when (it) { + SatsendringSvar.SATSENDRING_KJØRT_OK -> assertDoesNotThrow { + startSatsendring.gjennomførSatsendringManuelt(0L) + } + + else -> assertThrows { + startSatsendring.gjennomførSatsendringManuelt(0L) + } + } + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/sm\303\245barnstillegg/RestartAvSm\303\245barnstilleggServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/sm\303\245barnstillegg/RestartAvSm\303\245barnstilleggServiceTest.kt" new file mode 100644 index 000000000..c9635649b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/sm\303\245barnstillegg/RestartAvSm\303\245barnstilleggServiceTest.kt" @@ -0,0 +1,51 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.småbarnstillegg + +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingMigreringsinfoRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class RestartAvSmåbarnstilleggServiceTest { + + private val fagsakRepository = mockk() + private val behandlingHentOgPersisterService = mockk() + private val behandlingMigreringsinfoRepository = mockk() + private val restartAvSmåbarnstilleggService = spyk( + RestartAvSmåbarnstilleggService( + fagsakRepository = fagsakRepository, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + opprettTaskService = mockk(), + vedtakService = mockk(), + vedtaksperiodeService = mockk(), + behandlingMigreringsinfoRepository = behandlingMigreringsinfoRepository, + andelerTilkjentYtelseRepository = mockk(), + ), + ) + + @Test + fun `Skal ikke inkludere saker som er migrert forrige måned ved opprettelse av restartet småbarnstillegg oppgave`() { + every { behandlingHentOgPersisterService.partitionByIverksatteBehandlinger(any()) } returns + listOf(0L, 1L, 2L) + + every { behandlingMigreringsinfoRepository.finnSisteMigreringsdatoPåFagsak(0L) } returns LocalDate.now() + + every { behandlingMigreringsinfoRepository.finnSisteMigreringsdatoPåFagsak(1L) } returns LocalDate.now() + .minusMonths(1) + every { behandlingMigreringsinfoRepository.finnSisteMigreringsdatoPåFagsak(2L) } returns LocalDate.now() + .minusMonths(2) + + every { + restartAvSmåbarnstilleggService.periodeMedRestartetSmåbarnstilleggErAlleredeBegrunnet(any(), any()) + } returns false + + Assertions.assertEquals( + listOf(0L, 2L), + restartAvSmåbarnstilleggService.finnAlleFagsakerMedRestartetSmåbarnstilleggIMåned(), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/AutomatiskBeslutningServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/AutomatiskBeslutningServiceTest.kt new file mode 100644 index 000000000..4e688593e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/AutomatiskBeslutningServiceTest.kt @@ -0,0 +1,101 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.simulering.SimuleringService +import no.nav.familie.ba.sak.kjerne.simulering.lagBehandling +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +@ExtendWith(MockKExtension::class) +class AutomatiskBeslutningServiceTest { + @MockK + private lateinit var simuleringService: SimuleringService + + @InjectMockKs + private lateinit var automatiskBeslutningService: AutomatiskBeslutningService + + @Test + fun `behandlingSkalAutomatiskBesluttes - skal returnere true dersom behandling er helmanuell migrering med avvik innenfor beløpsgrenser og det ikke finnes manuelle posteringer`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + ) + every { simuleringService.harMigreringsbehandlingAvvikInnenforBeløpsgrenser(behandling) } returns true + every { simuleringService.harMigreringsbehandlingManuellePosteringer(behandling) } returns false + + assertThat(automatiskBeslutningService.behandlingSkalAutomatiskBesluttes(behandling)).isTrue + } + + @Test + fun `behandlingSkalAutomatiskBesluttes - skal returnere true dersom behandling er endre migreringsdato behandling`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + ) + + assertThat(automatiskBeslutningService.behandlingSkalAutomatiskBesluttes(behandling)).isTrue + } + + @Test + fun `behandlingSkalAutomatiskBesluttes - skal returnere false dersom behandling er helmanuell migrering med avvik innenfor beløpsgrenser men det finnes manuelle posteringer`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + ) + every { simuleringService.harMigreringsbehandlingAvvikInnenforBeløpsgrenser(behandling) } returns true + every { simuleringService.harMigreringsbehandlingManuellePosteringer(behandling) } returns true + + assertThat(automatiskBeslutningService.behandlingSkalAutomatiskBesluttes(behandling)).isFalse + } + + @Test + fun `behandlingSkalAutomatiskBesluttes - skal returnere false dersom behandling er helmanuell migrering med avvik utenfor beløpsgrenser og det ikke finnes manuelle posteringer`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + ) + every { simuleringService.harMigreringsbehandlingAvvikInnenforBeløpsgrenser(behandling) } returns false + every { simuleringService.harMigreringsbehandlingManuellePosteringer(behandling) } returns false + + assertThat(automatiskBeslutningService.behandlingSkalAutomatiskBesluttes(behandling)).isFalse + } + + @Test + fun `behandlingSkalAutomatiskBesluttes - skal returnere false dersom behandling er helmanuell migrering med avvik utenfor beløpsgrenser og det finnes manuelle posteringer`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + ) + every { simuleringService.harMigreringsbehandlingAvvikInnenforBeløpsgrenser(behandling) } returns false + every { simuleringService.harMigreringsbehandlingManuellePosteringer(behandling) } returns true + + assertThat(automatiskBeslutningService.behandlingSkalAutomatiskBesluttes(behandling)).isFalse + } + + @ParameterizedTest + @EnumSource(value = BehandlingÅrsak::class, names = ["HELMANUELL_MIGRERING", "ENDRE_MIGRERINGSDATO"]) + fun `behandlingSkalAutomatiskBesluttes - skal returnere false dersom behandling ikke er migrering uavhengig av avvik og manuelle posteringer`( + behandlingÅrsak: BehandlingÅrsak, + ) { + BehandlingType.values().filter { + !listOf( + BehandlingType.MIGRERING_FRA_INFOTRYGD, + BehandlingType.MIGRERING_FRA_INFOTRYGD_OPPHØRT, + ).contains(it) + }.forEach { behandlingType -> + val behandling = lagBehandling( + behandlingType = behandlingType, + årsak = behandlingÅrsak, + ) + assertThat(automatiskBeslutningService.behandlingSkalAutomatiskBesluttes(behandling)).isFalse + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingServiceEnhetstest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingServiceEnhetstest.kt new file mode 100644 index 000000000..ed986e242 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingServiceEnhetstest.kt @@ -0,0 +1,113 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.BehandlingstemaService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingMigreringsinfoRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingSøknadsinfoService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.simulering.lagBehandling +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.statistikk.saksstatistikk.SaksstatistikkEventPublisher +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.time.YearMonth + +@ExtendWith(MockKExtension::class) +class BehandlingServiceEnhetstest { + + @MockK + private lateinit var behandlingHentOgPersisterService: BehandlingHentOgPersisterService + + @MockK + private lateinit var behandlingstemaService: BehandlingstemaService + + @MockK + private lateinit var behandlingSøknadsinfoService: BehandlingSøknadsinfoService + + @MockK + private lateinit var behandlingMigreringsinfoRepository: BehandlingMigreringsinfoRepository + + @MockK + private lateinit var behandlingMetrikker: BehandlingMetrikker + + @MockK + private lateinit var saksstatistikkEventPublisher: SaksstatistikkEventPublisher + + @MockK + private lateinit var fagsakRepository: FagsakRepository + + @MockK + private lateinit var vedtakRepository: VedtakRepository + + @MockK + private lateinit var andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository + + @MockK + private lateinit var loggService: LoggService + + @MockK + private lateinit var arbeidsfordelingService: ArbeidsfordelingService + + @MockK + private lateinit var infotrygdService: InfotrygdService + + @MockK + private lateinit var vedtaksperiodeService: VedtaksperiodeService + + @MockK + private lateinit var taskRepository: TaskRepositoryWrapper + + @MockK + private lateinit var vilkårsvurderingService: VilkårsvurderingService + + @InjectMockKs + private lateinit var behandlingService: BehandlingService + + @Test + fun `erLøpende - skal returnere true dersom det finnes andeler i en behandling hvor tom er etter YearMonth now`() { + val behandling = lagBehandling() + + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(any()) } returns listOf( + lagAndelTilkjentYtelse(YearMonth.now().minusYears(1), YearMonth.now().minusMonths(6)), + lagAndelTilkjentYtelse(YearMonth.now().minusMonths(6), YearMonth.now().minusMonths(3)), + lagAndelTilkjentYtelse(YearMonth.now().minusMonths(3), YearMonth.now().plusMonths(3)), + ) + assertThat(behandlingService.erLøpende(behandling)).isTrue + } + + @Test + fun `erLøpende - skal returnere false dersom det finnes andeler i en behandling hvor tom er det samme som YearMonth now`() { + val behandling = lagBehandling() + + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(any()) } returns listOf( + lagAndelTilkjentYtelse(YearMonth.now().minusYears(1), YearMonth.now().minusMonths(6)), + lagAndelTilkjentYtelse(YearMonth.now().minusMonths(6), YearMonth.now().minusMonths(3)), + lagAndelTilkjentYtelse(YearMonth.now().minusMonths(3), YearMonth.now()), + ) + assertThat(behandlingService.erLøpende(behandling)).isFalse + } + + @Test + fun `erLøpende - skal returnere false dersom alle andeler i en behandling har tom før YearMonth now`() { + val behandling = lagBehandling() + + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(any()) } returns listOf( + lagAndelTilkjentYtelse(YearMonth.now().minusYears(1), YearMonth.now().minusMonths(6)), + lagAndelTilkjentYtelse(YearMonth.now().minusMonths(6), YearMonth.now().minusMonths(3)), + lagAndelTilkjentYtelse(YearMonth.now().minusMonths(3), YearMonth.now().minusMonths(1)), + ) + assertThat(behandlingService.erLøpende(behandling)).isFalse + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingStegControllerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingStegControllerTest.kt new file mode 100644 index 000000000..03d01767f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingStegControllerTest.kt @@ -0,0 +1,52 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class BehandlingStegControllerTest { + + private val featureToggleServiceMock = mockk() + private val tilgangServiceMock = mockk() + private val behandlingHentOgPersisterServiceMock = mockk() + private val stegServiceMock = mockk() + private val utvidetBehandlingServiceMock = mockk() + private val behandlingStegController = BehandlingStegController( + behandlingHentOgPersisterService = behandlingHentOgPersisterServiceMock, + stegService = stegServiceMock, + tilgangService = tilgangServiceMock, + featureToggleService = featureToggleServiceMock, + utvidetBehandlingService = utvidetBehandlingServiceMock, + ) + + @Test + fun `Skal kaste feil hvis saksbehandler uten teknisk endring-tilgang prøver å henlegge en behandling med årsak=teknisk endring`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.TEKNISK_ENDRING) + + every { featureToggleServiceMock.isEnabled(FeatureToggleConfig.TEKNISK_ENDRING) } returns false + every { featureToggleServiceMock.isEnabled(FeatureToggleConfig.TEKNISK_VEDLIKEHOLD_HENLEGGELSE) } returns false + every { tilgangServiceMock.verifiserHarTilgangTilHandling(any(), any()) } just runs + every { behandlingHentOgPersisterServiceMock.hent(any()) } returns behandling + every { stegServiceMock.håndterHenleggBehandling(any(), any()) } returns behandling + + assertThrows { + behandlingStegController.henleggBehandlingOgSendBrev( + behandlingId = behandling.id, + henleggInfo = RestHenleggBehandlingInfo( + begrunnelse = "dette er en begrunnelse", + årsak = HenleggÅrsak.FEILAKTIG_OPPRETTET, + ), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingTest.kt new file mode 100644 index 000000000..90e6ea379 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingTest.kt @@ -0,0 +1,155 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows + +class BehandlingTest { + + @Test + fun `validerBehandling kaster feil hvis behandlingType og behandlingÅrsak ikke samsvarer ved teknisk endring`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.TEKNISK_ENDRING, + årsak = BehandlingÅrsak.SØKNAD, + ) + assertThrows { behandling.validerBehandlingstype() } + } + + @Test + fun `validerBehandling kaster feil hvis behandlingType er teknisk opphør`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.TEKNISK_OPPHØR, + årsak = BehandlingÅrsak.TEKNISK_OPPHØR, + ) + assertThrows { behandling.validerBehandlingstype() } + } + + @Test + fun `validerBehandling kaster feil hvis man prøver å opprette revurdering uten andre vedtatte behandlinger`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.REVURDERING, + årsak = BehandlingÅrsak.SØKNAD, + ) + assertThrows { behandling.validerBehandlingstype() } + } + + @Test + fun `validerBehandling kaster ikke feil hvis man prøver å opprette revurdering med andre vedtatte behandlinger`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.REVURDERING, + årsak = BehandlingÅrsak.SØKNAD, + ) + assertDoesNotThrow { + behandling.validerBehandlingstype( + sisteBehandlingSomErVedtatt = lagBehandling( + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak = BehandlingÅrsak.SØKNAD, + ), + ) + } + } + + @Test + fun `erRentTekniskOpphør kastet feil hvis behandlingType og behandlingÅrsak ikke samsvarer ved teknisk opphør`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.TEKNISK_OPPHØR, + årsak = BehandlingÅrsak.SØKNAD, + ) + assertThrows { behandling.erTekniskOpphør() } + } + + @Test + fun `erRentTekniskOpphør gir true når teknisk opphør`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.TEKNISK_OPPHØR, + årsak = BehandlingÅrsak.TEKNISK_OPPHØR, + ) + assertTrue(behandling.erTekniskOpphør()) + } + + @Test + fun `erRentTekniskOpphør gir false når ikke teknisk opphør`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.REVURDERING, + årsak = BehandlingÅrsak.SØKNAD, + ) + assertFalse(behandling.erTekniskOpphør()) + } + + @Test + fun `erBehandlingMedVedtaksbrevutsending kan sende vedtaksbrev for ordinær førstegangsbehandling`() { + val behandling = lagBehandling() + assertTrue { behandling.erBehandlingMedVedtaksbrevutsending() } + } + + @Test + fun `erBehandlingMedVedtaksbrevutsending kan sende vedtaksbrev for ordinær revurdering`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.REVURDERING, + årsak = BehandlingÅrsak.NYE_OPPLYSNINGER, + ) + assertTrue { behandling.erBehandlingMedVedtaksbrevutsending() } + } + + @Test + fun `erBehandlingMedVedtaksbrevutsending kan ikke sende vedtaksbrev for migrering med endre migreringsdato`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + ) + assertFalse { behandling.erBehandlingMedVedtaksbrevutsending() } + } + + @Test + fun `erBehandlingMedVedtaksbrevutsending kan ikke sende vedtaksbrev for helmanuell migrering`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + ) + assertFalse { behandling.erBehandlingMedVedtaksbrevutsending() } + } + + @Test + fun `erBehandlingMedVedtaksbrevutsending kan ikke sende vedtaksbrev for automatisk migrering`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.MIGRERING, + ) + assertFalse { behandling.erBehandlingMedVedtaksbrevutsending() } + } + + @Test + fun `erBehandlingMedVedtaksbrevutsending kan ikke sende vedtaksbrev for teknisk endring`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.TEKNISK_ENDRING, + ) + assertFalse { behandling.erBehandlingMedVedtaksbrevutsending() } + } + + @Test + fun `erBehandlingMedVedtaksbrevutsending kan ikke sende vedtaksbrev for revurdering med satsendring`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.REVURDERING, + årsak = BehandlingÅrsak.SATSENDRING, + ) + assertFalse { behandling.erBehandlingMedVedtaksbrevutsending() } + } + + @Test + fun `Skal svare med overstyrt dokumenttittel på alle behandlinger som er definert som omgjøringsårsaker`() { + BehandlingÅrsak.values().forEach { + if (it.erOmregningsårsak()) { + assertNotNull(it.hentOverstyrtDokumenttittelForOmregningsbehandling()) + } else { + assertNull(it.hentOverstyrtDokumenttittelForOmregningsbehandling()) + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingUtilsTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingUtilsTest.kt new file mode 100644 index 000000000..7a285e31d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingUtilsTest.kt @@ -0,0 +1,386 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.kjerne.behandling.Behandlingutils.validerBehandlingIkkeSendtTilEksterneTjenester +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.bestemKategoriVedOpprettelse +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.bestemUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.utledLøpendeUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.steg.BehandlingStegStatus +import no.nav.familie.ba.sak.kjerne.steg.StegType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class BehandlingUtilsTest { + @Test + fun `Skal velge ordinær ved FGB`() { + assertEquals( + BehandlingUnderkategori.ORDINÆR, + bestemUnderkategori( + overstyrtUnderkategori = BehandlingUnderkategori.ORDINÆR, + underkategoriFraLøpendeBehandling = null, + ), + ) + } + + @Test + fun `Skal velge utvidet ved FGB`() { + assertEquals( + BehandlingUnderkategori.UTVIDET, + bestemUnderkategori( + overstyrtUnderkategori = BehandlingUnderkategori.UTVIDET, + underkategoriFraLøpendeBehandling = null, + ), + ) + } + + @Test + fun `Skal velge utvidet når løpende er ordinær, og inneværende er utvidet`() { + assertEquals( + BehandlingUnderkategori.UTVIDET, + bestemUnderkategori( + overstyrtUnderkategori = null, + underkategoriFraLøpendeBehandling = BehandlingUnderkategori.ORDINÆR, + underkategoriFraInneværendeBehandling = BehandlingUnderkategori.UTVIDET, + ), + ) + } + + @Test + fun `Skal beholde utvidet når løpende er utvidet, og ny er ordinær`() { + assertEquals( + BehandlingUnderkategori.UTVIDET, + bestemUnderkategori( + overstyrtUnderkategori = BehandlingUnderkategori.ORDINÆR, + underkategoriFraLøpendeBehandling = BehandlingUnderkategori.UTVIDET, + underkategoriFraInneværendeBehandling = BehandlingUnderkategori.ORDINÆR, + ), + ) + } + + @Test + fun `Skal velge utvidet ved RV når FGB er utvidet`() { + assertEquals( + BehandlingUnderkategori.UTVIDET, + bestemUnderkategori( + overstyrtUnderkategori = BehandlingUnderkategori.ORDINÆR, + underkategoriFraLøpendeBehandling = BehandlingUnderkategori.UTVIDET, + ), + ) + } + + @Test + fun `Skal velge ordinær ved RV når FGB er ordinær`() { + assertEquals( + BehandlingUnderkategori.ORDINÆR, + bestemUnderkategori( + overstyrtUnderkategori = BehandlingUnderkategori.ORDINÆR, + underkategoriFraLøpendeBehandling = BehandlingUnderkategori.ORDINÆR, + ), + ) + } + + @Test + fun `Skal velge utvidet ved RV når FGB er ordinær`() { + assertEquals( + BehandlingUnderkategori.UTVIDET, + bestemUnderkategori( + overstyrtUnderkategori = BehandlingUnderkategori.UTVIDET, + underkategoriFraLøpendeBehandling = BehandlingUnderkategori.ORDINÆR, + ), + ) + } + + @Test + fun `Skal velge den løpende underkategorien ved 'endre migreringsdato'`() { + assertEquals( + BehandlingUnderkategori.UTVIDET, + bestemUnderkategori( + overstyrtUnderkategori = null, + underkategoriFraLøpendeBehandling = BehandlingUnderkategori.UTVIDET, + ), + ) + assertEquals( + BehandlingUnderkategori.ORDINÆR, + bestemUnderkategori( + overstyrtUnderkategori = null, + underkategoriFraLøpendeBehandling = BehandlingUnderkategori.ORDINÆR, + ), + ) + } + + @Test + fun `Skal returnere utvidet hvis det eksisterer en løpende utvidet-sak`() { + val søkerAktørId = randomAktør() + + val behandling = lagBehandling() + + val tilkjentYtelse = TilkjentYtelse( + behandling = behandling, + opprettetDato = LocalDate.now(), + endretDato = LocalDate.now(), + ) + + val andelTilkjentYtelse = listOf( + AndelTilkjentYtelse( + behandlingId = behandling.id, + type = YtelseType.UTVIDET_BARNETRYGD, + tilkjentYtelse = tilkjentYtelse, + aktør = søkerAktørId, + kalkulertUtbetalingsbeløp = 1054, + nasjonaltPeriodebeløp = 1054, + sats = 123, + stønadFom = YearMonth.of(2015, 6), + stønadTom = YearMonth.now().plusYears(5), + prosent = BigDecimal(2), + ), + ) + + val løpendeUndekategori = utledLøpendeUnderkategori(andelTilkjentYtelse) + + assertEquals(BehandlingUnderkategori.UTVIDET, løpendeUndekategori) + } + + @Test + fun `Skal returnere ordinær hvis det eksisterer en utvidet-sak som er avsluttet`() { + val søkerAktørId = randomAktør() + + val behandling = lagBehandling() + + val tilkjentYtelse = TilkjentYtelse( + behandling = behandling, + opprettetDato = LocalDate.now(), + endretDato = LocalDate.now(), + ) + + val andelTilkjentYtelse = listOf( + AndelTilkjentYtelse( + behandlingId = behandling.id, + type = YtelseType.UTVIDET_BARNETRYGD, + tilkjentYtelse = tilkjentYtelse, + aktør = søkerAktørId, + kalkulertUtbetalingsbeløp = 1054, + nasjonaltPeriodebeløp = 1054, + sats = 123, + stønadFom = YearMonth.of(2015, 6), + stønadTom = YearMonth.now().minusYears(1), + prosent = BigDecimal(2), + ), + ) + + val løpendeUndekategori = utledLøpendeUnderkategori(andelTilkjentYtelse) + + assertEquals(BehandlingUnderkategori.ORDINÆR, løpendeUndekategori) + } + + @Test + fun `Skal returnere ordinær hvis det eksisterer en løpende ordinær-sak`() { + val søkerAktørId = randomAktør() + + val behandling = lagBehandling() + + val tilkjentYtelse = TilkjentYtelse( + behandling = behandling, + opprettetDato = LocalDate.now(), + endretDato = LocalDate.now(), + ) + + val andelTilkjentYtelse = listOf( + AndelTilkjentYtelse( + behandlingId = behandling.id, + type = YtelseType.ORDINÆR_BARNETRYGD, + tilkjentYtelse = tilkjentYtelse, + aktør = søkerAktørId, + kalkulertUtbetalingsbeløp = 1054, + nasjonaltPeriodebeløp = 1054, + sats = 123, + stønadFom = YearMonth.of(2015, 6), + stønadTom = YearMonth.now().plusYears(2), + prosent = BigDecimal(2), + ), + ) + + val løpendeUndekategori = utledLøpendeUnderkategori(andelTilkjentYtelse) + + assertEquals(BehandlingUnderkategori.ORDINÆR, løpendeUndekategori) + } + + @Test + fun `Skal finne ut at omregningsbehandling allerede har kjørt inneværende måned`() { + val fgb = lagBehandling() + val omregning6År = lagBehandling( + årsak = BehandlingÅrsak.OMREGNING_6ÅR, + ) + val behandlingsårsakHarAlleredeKjørt = Behandlingutils.harBehandlingsårsakAlleredeKjørt( + behandlingÅrsak = BehandlingÅrsak.OMREGNING_6ÅR, + behandlinger = listOf(fgb, omregning6År), + måned = YearMonth.now(), + ) + + assertTrue(behandlingsårsakHarAlleredeKjørt) + } + + @Test + fun `Skal finne ut at omregningsbehandling ikke har kjørt inneværende måned`() { + val fgb = lagBehandling() + val behandlingsårsakHarAlleredeKjørt = Behandlingutils.harBehandlingsårsakAlleredeKjørt( + behandlingÅrsak = BehandlingÅrsak.OMREGNING_6ÅR, + behandlinger = listOf(fgb), + måned = YearMonth.now(), + ) + + assertFalse(behandlingsårsakHarAlleredeKjørt) + } + + @Test + fun `Skal kaste feil etter at vedtaksbrev er distribuert`() { + val fgb = lagBehandling() + fgb.behandlingStegTilstand.clear() + fgb.behandlingStegTilstand.add( + BehandlingStegTilstand( + behandlingSteg = StegType.DISTRIBUER_VEDTAKSBREV, + behandlingStegStatus = BehandlingStegStatus.UTFØRT, + behandling = fgb, + ), + ) + + assertThrows { validerBehandlingIkkeSendtTilEksterneTjenester(fgb) } + } + + @Test + fun `Skal kaste feil etter iverksetting mot økonomi`() { + val fgb = lagBehandling() + fgb.behandlingStegTilstand.clear() + fgb.behandlingStegTilstand.add( + BehandlingStegTilstand( + behandlingSteg = StegType.IVERKSETT_MOT_OPPDRAG, + behandlingStegStatus = BehandlingStegStatus.UTFØRT, + behandling = fgb, + ), + ) + + assertThrows { validerBehandlingIkkeSendtTilEksterneTjenester(fgb) } + } + + @Test + fun `Skal kaste feil etter at brev er journalført`() { + val fgb = lagBehandling() + fgb.behandlingStegTilstand.clear() + fgb.behandlingStegTilstand.add( + BehandlingStegTilstand( + behandlingSteg = StegType.JOURNALFØR_VEDTAKSBREV, + behandlingStegStatus = BehandlingStegStatus.UTFØRT, + behandling = fgb, + ), + ) + + assertThrows { validerBehandlingIkkeSendtTilEksterneTjenester(fgb) } + } + + @Test + fun `skal få gitt behandlingskategori ved opprettelse av FGB`() { + assertEquals( + BehandlingKategori.EØS, + bestemKategoriVedOpprettelse( + overstyrtKategori = BehandlingKategori.EØS, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + kategoriFraLøpendeBehandling = BehandlingKategori.NASJONAL, // default verdi + ), + ) + } + + @Test + fun `skal få gitt behandlingskategori ved opprettelse av Revurdering med søknad`() { + assertEquals( + BehandlingKategori.EØS, + bestemKategoriVedOpprettelse( + overstyrtKategori = BehandlingKategori.EØS, + behandlingType = BehandlingType.REVURDERING, + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + kategoriFraLøpendeBehandling = BehandlingKategori.NASJONAL, // default verdi + ), + ) + } + + @Test + fun `skal bruke overstyrt kategori ved opprettelse av migreringsbehandling med helmanuell migrering`() { + val overstyrtKategori = BehandlingKategori.EØS + assertEquals( + overstyrtKategori, + bestemKategoriVedOpprettelse( + overstyrtKategori = overstyrtKategori, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + behandlingÅrsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + kategoriFraLøpendeBehandling = BehandlingKategori.NASJONAL, // default verdi + ), + ) + } + + @Test + fun `skal få NASJONAL kategori ved opprettelse av automatisk migreringsbehandling `() { + assertEquals( + BehandlingKategori.NASJONAL, + bestemKategoriVedOpprettelse( + overstyrtKategori = BehandlingKategori.NASJONAL, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + behandlingÅrsak = BehandlingÅrsak.MIGRERING, + kategoriFraLøpendeBehandling = BehandlingKategori.NASJONAL, // default verdi + ), + ) + } + + @Test + fun `skal få EØS kategori ved opprettelse av automatisk migreringsbehandling `() { + assertEquals( + BehandlingKategori.EØS, + bestemKategoriVedOpprettelse( + overstyrtKategori = BehandlingKategori.EØS, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + behandlingÅrsak = BehandlingÅrsak.MIGRERING, + kategoriFraLøpendeBehandling = BehandlingKategori.NASJONAL, // default verdi + ), + ) + } + + @Test + fun `skal få EØS kategori ved opprettelse av revurdering når siste behandling er EØS og har løpende utbetaling `() { + assertEquals( + BehandlingKategori.EØS, + bestemKategoriVedOpprettelse( + overstyrtKategori = null, + behandlingType = BehandlingType.REVURDERING, + behandlingÅrsak = BehandlingÅrsak.NYE_OPPLYSNINGER, + kategoriFraLøpendeBehandling = BehandlingKategori.EØS, + ), + ) + } + + @Test + fun `skal få NASJONAL kategori ved opprettelse av revurdering når siste behandling er NASJONAL og har løpende utbetaling `() { + assertEquals( + BehandlingKategori.NASJONAL, + bestemKategoriVedOpprettelse( + overstyrtKategori = null, + behandlingType = BehandlingType.REVURDERING, + behandlingÅrsak = BehandlingÅrsak.NYE_OPPLYSNINGER, + kategoriFraLøpendeBehandling = BehandlingKategori.NASJONAL, + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/LagreMigreringsdatoTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/LagreMigreringsdatoTest.kt new file mode 100644 index 000000000..580c18769 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/LagreMigreringsdatoTest.kt @@ -0,0 +1,174 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.BehandlingstemaService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingMigreringsinfoRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingSøknadsinfoService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.simulering.SimuleringService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.statistikk.saksstatistikk.SaksstatistikkEventPublisher +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import java.time.LocalDate +import java.time.YearMonth + +class LagreMigreringsdatoTest { + val behandlingstemaService = mockk() + val behandlingHentOgPersisterService = mockk(relaxed = true) + val behandlingSøknadsinfoService = mockk() + val beregningService = mockk() + val personopplysningGrunnlagRepository = mockk() + val andelTilkjentYtelseRepository = mockk() + val behandlingMetrikker = mockk() + val fagsakRepository = mockk() + val vedtakRepository = mockk() + val loggService = mockk() + val arbeidsfordelingService = mockk() + val saksstatistikkEventPublisher = mockk() + val infotrygdService = mockk() + val vedtaksperiodeService = mockk() + val taskRepository = mockk() + val behandlingMigreringsinfoRepository = mockk() + val vilkårsvurderingService = mockk() + val simuleringService = mockk() + + private val behandlingService = BehandlingService( + behandlingHentOgPersisterService, + behandlingstemaService, + behandlingSøknadsinfoService, + behandlingMigreringsinfoRepository, + behandlingMetrikker, + saksstatistikkEventPublisher, + fagsakRepository, + vedtakRepository, + andelTilkjentYtelseRepository, + loggService, + arbeidsfordelingService, + infotrygdService, + vedtaksperiodeService, + taskRepository, + vilkårsvurderingService, + ) + + @Test + fun `Lagre første migreringstidspunkt skal ikke kaste feil`() { + every { behandlingMigreringsinfoRepository.finnSisteMigreringsdatoPåFagsak(any()) } returns null + every { behandlingMigreringsinfoRepository.save(any()) } returns mockk() + every { vilkårsvurderingService.hentTidligsteVilkårsvurderingKnyttetTilMigrering(any()) } returns YearMonth.now() + every { behandlingHentOgPersisterService.hentBehandlinger(any()) } returns emptyList() + + assertDoesNotThrow { + behandlingService.lagreNedMigreringsdato( + migreringsdato = LocalDate.now(), + behandling = lagBehandling(), + ) + } + } + + @Test + fun `Lagre likt migreringstidspunkt skal kaste feil`() { + every { behandlingMigreringsinfoRepository.finnSisteMigreringsdatoPåFagsak(any()) } returns LocalDate.now() + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(any()) } returns null + every { behandlingMigreringsinfoRepository.save(any()) } returns mockk() + + val feil = assertThrows { + behandlingService.lagreNedMigreringsdato( + migreringsdato = LocalDate.now(), + behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + ), + ) + } + assertEquals( + "Migreringsdatoen du har lagt inn er lik eller senere enn eksisterende migreringsdato. Du må velge en tidligere migreringsdato for å fortsette.", + feil.melding, + ) + } + + @Test + fun `Lagre tidligere migreringstidspunkt skal ikke kaste feil`() { + every { behandlingMigreringsinfoRepository.finnSisteMigreringsdatoPåFagsak(any()) } returns LocalDate.now() + every { behandlingHentOgPersisterService.hentBehandlinger(any()) } returns emptyList() + every { behandlingMigreringsinfoRepository.save(any()) } returns mockk() + + assertDoesNotThrow { + behandlingService.lagreNedMigreringsdato( + migreringsdato = LocalDate.now().minusMonths(1), + behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + ), + ) + } + } + + @Test + fun `Lagre tidligere migreringstidspunkt skal kaste feil dersom forrige behandling ikke har lagret migreringsdato`() { + every { behandlingMigreringsinfoRepository.finnSisteMigreringsdatoPåFagsak(any()) } returns null + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(any()) } returns + lagBehandling(behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD).also { + it.status = BehandlingStatus.AVSLUTTET + it.resultat = Behandlingsresultat.INNVILGET + } + every { vilkårsvurderingService.hentTidligsteVilkårsvurderingKnyttetTilMigrering(any()) } returns YearMonth.now() + + every { behandlingMigreringsinfoRepository.save(any()) } returns mockk() + + val feil = assertThrows { + behandlingService.lagreNedMigreringsdato( + migreringsdato = LocalDate.now(), + behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + ), + ) + } + assertEquals( + "Migreringsdatoen du har lagt inn er lik eller senere enn eksisterende migreringsdato. Du må velge en tidligere migreringsdato for å fortsette.", + feil.melding, + ) + } + + @Test + fun `Lagre tidligere migreringstidspunkt skal ikke kaste feil dersom forrige behandling ikke er migreringsbehandling`() { + every { behandlingMigreringsinfoRepository.finnSisteMigreringsdatoPåFagsak(any()) } returns null + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(any()) } returns + lagBehandling(behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING).also { + it.status = BehandlingStatus.AVSLUTTET + it.resultat = Behandlingsresultat.INNVILGET + } + every { vilkårsvurderingService.hentTidligsteVilkårsvurderingKnyttetTilMigrering(any()) } returns YearMonth.now() + + every { behandlingMigreringsinfoRepository.save(any()) } returns mockk() + + assertDoesNotThrow { + behandlingService.lagreNedMigreringsdato( + migreringsdato = LocalDate.now(), + behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + ), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/ValiderBrevmottakerServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/ValiderBrevmottakerServiceTest.kt new file mode 100644 index 000000000..359197250 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/ValiderBrevmottakerServiceTest.kt @@ -0,0 +1,128 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollService +import no.nav.familie.ba.sak.kjerne.brev.mottaker.Brevmottaker +import no.nav.familie.ba.sak.kjerne.brev.mottaker.BrevmottakerRepository +import no.nav.familie.ba.sak.kjerne.brev.mottaker.MottakerType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +class ValiderBrevmottakerServiceTest { + private val brevmottakerRepository = mockk() + private val persongrunnlagService = mockk() + private val familieIntegrasjonerTilgangskontrollService = mockk() + val validerBrevmottakerService = ValiderBrevmottakerService( + brevmottakerRepository, + persongrunnlagService, + familieIntegrasjonerTilgangskontrollService, + ) + + private val behandlingId = 0L + val brevmottaker = Brevmottaker( + behandlingId = behandlingId, + type = MottakerType.DØDSBO, + navn = "Donald Duck", + adresselinje1 = "Andebyveien 1", + postnummer = "0000", + poststed = "OSLO", + landkode = "NO", + ) + val søker = tilfeldigPerson(personType = PersonType.SØKER) + + @Test + fun `Skal ikke kaste funksjonell feil når en behandling ikke inneholder noen manuelle brevmottakere`() { + every { brevmottakerRepository.finnBrevMottakereForBehandling(behandlingId) } returns emptyList() + + validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligePersonerMedManuelleBrevmottakere( + behandlingId, + ) + + verify(exactly = 1) { brevmottakerRepository.finnBrevMottakereForBehandling(behandlingId) } + verify(exactly = 0) { persongrunnlagService.hentAktiv(any()) } + verify(exactly = 0) { + familieIntegrasjonerTilgangskontrollService.hentIdenterMedStrengtFortroligAdressebeskyttelse( + any(), + ) + } + } + + @Test + fun `Skal kaste en FunksjonellFeil exception når en behandling inneholder minst en strengt fortrolig person og minst en manuell brevmottaker`() { + every { brevmottakerRepository.finnBrevMottakereForBehandling(behandlingId) } returns listOf(brevmottaker) + every { persongrunnlagService.hentAktiv(behandlingId) } returns lagTestPersonopplysningGrunnlag( + behandlingId, + søker, + ) + every { familieIntegrasjonerTilgangskontrollService.hentIdenterMedStrengtFortroligAdressebeskyttelse(any()) } returns listOf( + søker.aktør.aktivFødselsnummer(), + ) + + assertThatThrownBy { + validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligePersonerMedManuelleBrevmottakere( + behandlingId, + ) + }.isInstanceOf(FunksjonellFeil::class.java).hasMessageContaining("strengt fortrolig adressebeskyttelse og kan ikke kombineres med manuelle brevmottakere") + } + + @Test + fun `Skal ikke kaste funksjonell feil når behandling ikke inneholder noen strengt fortrolige personer og inneholder minst en manuell brevmottaker`() { + every { brevmottakerRepository.finnBrevMottakereForBehandling(behandlingId) } returns listOf(brevmottaker) + every { persongrunnlagService.hentAktiv(behandlingId) } returns lagTestPersonopplysningGrunnlag( + behandlingId, + søker, + ) + every { familieIntegrasjonerTilgangskontrollService.hentIdenterMedStrengtFortroligAdressebeskyttelse(any()) } returns emptyList() + + validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligePersonerMedManuelleBrevmottakere( + behandlingId, + ) + verify(exactly = 1) { + familieIntegrasjonerTilgangskontrollService.hentIdenterMedStrengtFortroligAdressebeskyttelse( + any(), + ) + } + } + + @Test + fun `Skal ikke kaste en exception når en behandling inneholder minst en strengt fortrolig person og ingen manuelle brevmottakere`() { + every { brevmottakerRepository.finnBrevMottakereForBehandling(behandlingId) } returns emptyList() + every { persongrunnlagService.hentAktiv(behandlingId) } returns lagTestPersonopplysningGrunnlag( + behandlingId, + søker, + ) + every { familieIntegrasjonerTilgangskontrollService.hentIdenterMedStrengtFortroligAdressebeskyttelse(any()) } returns listOf( + søker.aktør.aktivFødselsnummer(), + ) + + validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligePersonerMedManuelleBrevmottakere( + behandlingId, + ) + } + + @Test + fun `Skal kaste en FunksjonellFeil exception når en behandling inneholder minst en strengt fortrolig person og det blir forsøkt lagt til en ny manuell brevmottaker`() { + every { brevmottakerRepository.finnBrevMottakereForBehandling(behandlingId) } returns emptyList() + every { persongrunnlagService.hentAktiv(behandlingId) } returns lagTestPersonopplysningGrunnlag( + behandlingId, + søker, + ) + every { familieIntegrasjonerTilgangskontrollService.hentIdenterMedStrengtFortroligAdressebeskyttelse(any()) } returns listOf( + søker.aktør.aktivFødselsnummer(), + ) + + assertThatThrownBy { + validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligePersonerMedManuelleBrevmottakere( + behandlingId, + brevmottaker, + ) + }.isInstanceOf(FunksjonellFeil::class.java).hasMessageContaining("strengt fortrolig adressebeskyttelse og kan ikke kombineres med manuelle brevmottakere") + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/behandlingstema/BehandlingstemaServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/behandlingstema/BehandlingstemaServiceTest.kt new file mode 100644 index 000000000..966808e66 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/behandlingstema/BehandlingstemaServiceTest.kt @@ -0,0 +1,240 @@ +package no.nav.familie.ba.sak.kjerne.behandling.behandlingstema + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPersonResultaterForSøkerOgToBarn +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.tilPersonEnkelSøkerOgBarn +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjeService +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjer +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.time.LocalDate + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class BehandlingstemaServiceTest { + private val behandlingHentOgPersisterService = mockk() + private val andelTilkjentYtelseRepository = mockk() + private val loggService = mockk() + private val oppgaveService = mockk() + private val tidslinjeService = mockk() + private val vilkårsvurderingRepository = mockk() + + val behandlingstemaService = BehandlingstemaService( + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + andelTilkjentYtelseRepository = andelTilkjentYtelseRepository, + loggService = loggService, + oppgaveService = oppgaveService, + vilkårsvurderingTidslinjeService = tidslinjeService, + vilkårsvurderingRepository = vilkårsvurderingRepository, + ) + val defaultFagsak = defaultFagsak() + val defaultBehandling = lagBehandling(defaultFagsak) + + @BeforeAll + fun init() { + every { behandlingHentOgPersisterService.finnAktivOgÅpenForFagsak(defaultFagsak.id) } returns defaultBehandling + } + + @Test + fun `Skal utlede EØS dersom minst ett vilkår i har blitt behandlet i inneværende behandling`() { + val barn = randomAktør() + val vilkårsvurdering = Vilkårsvurdering( + behandling = defaultBehandling, + ) + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn, + vilkårResultater = mutableSetOf( + lagVilkårResultat( + vilkår = Vilkår.BOSATT_I_RIKET, + vilkårRegelverk = Regelverk.NASJONALE_REGLER, + behandlingId = defaultBehandling.id, + ), + lagVilkårResultat( + vilkår = Vilkår.BOSATT_I_RIKET, + vilkårRegelverk = Regelverk.EØS_FORORDNINGEN, + behandlingId = defaultBehandling.id, + ), + ), + ) + vilkårsvurdering.personResultater = setOf(personResultat) + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(defaultBehandling.id) } returns vilkårsvurdering + + val kategori = behandlingstemaService.hentKategoriFraInneværendeBehandling(defaultFagsak.id) + + assertEquals(BehandlingKategori.EØS, kategori) + } + + @Test + fun `Skal utlede NASJONAL dersom EØS vilkåret ble behandlet i annen behandling`() { + val barn = randomAktør() + val vilkårsvurdering = Vilkårsvurdering( + behandling = defaultBehandling, + ) + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn, + vilkårResultater = mutableSetOf( + lagVilkårResultat( + vilkår = Vilkår.BOSATT_I_RIKET, + vilkårRegelverk = Regelverk.NASJONALE_REGLER, + behandlingId = defaultBehandling.id, + ), + lagVilkårResultat( + vilkår = Vilkår.BOSATT_I_RIKET, + vilkårRegelverk = Regelverk.EØS_FORORDNINGEN, + behandlingId = 0L, + ), + ), + ) + vilkårsvurdering.personResultater = setOf(personResultat) + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(defaultBehandling.id) } returns vilkårsvurdering + + val kategori = behandlingstemaService.hentKategoriFraInneværendeBehandling(defaultFagsak.id) + + assertEquals(BehandlingKategori.NASJONAL, kategori) + } + + @Test + fun `Skal utlede UTVIDET dersom minst ett vilkår i har blitt behandlet i inneværende behandling`() { + val vilkårsvurdering = Vilkårsvurdering( + behandling = defaultBehandling, + ) + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = defaultFagsak.aktør, + vilkårResultater = mutableSetOf( + lagVilkårResultat( + vilkår = Vilkår.UTVIDET_BARNETRYGD, + behandlingId = defaultBehandling.id, + ), + lagVilkårResultat( + vilkår = Vilkår.UTVIDET_BARNETRYGD, + behandlingId = defaultBehandling.id, + ), + ), + ) + vilkårsvurdering.personResultater = setOf(personResultat) + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(defaultBehandling.id) } returns vilkårsvurdering + + val underkategori = behandlingstemaService.hentUnderkategoriFraInneværendeBehandling(defaultFagsak.id) + + assertEquals(BehandlingUnderkategori.UTVIDET, underkategori) + } + + @Test + fun `Skal utlede ORDINÆR dersom UTVIDET vilkåret ble behandlet i annen behandling`() { + val barn = randomAktør() + val vilkårsvurdering = Vilkårsvurdering( + behandling = defaultBehandling, + ) + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn, + vilkårResultater = mutableSetOf( + lagVilkårResultat( + vilkår = Vilkår.UTVIDET_BARNETRYGD, + vilkårRegelverk = Regelverk.NASJONALE_REGLER, + behandlingId = 0L, + ), + ), + ) + vilkårsvurdering.personResultater = setOf(personResultat) + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(defaultBehandling.id) } returns vilkårsvurdering + + val underkategori = behandlingstemaService.hentUnderkategoriFraInneværendeBehandling(defaultFagsak.id) + + assertEquals(BehandlingUnderkategori.ORDINÆR, underkategori) + } + + @Test + fun `skal hente løpende kategori til NASJONAL når siste behandling er NASJONAL og har løpende utbetaling`() { + val søkerFnr = randomFnr() + val barnFnr = listOf(randomFnr()) + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(defaultFagsak.id) } returns defaultBehandling + every { tidslinjeService.hentTidslinjer(BehandlingId(defaultBehandling.id)) } returns + VilkårsvurderingTidslinjer( + vilkårsvurdering = lagVilkårsvurdering(tilAktør(søkerFnr), defaultBehandling, Resultat.OPPFYLT), + søkerOgBarn = lagTestPersonopplysningGrunnlag(defaultBehandling.id, randomFnr(), barnFnr) + .tilPersonEnkelSøkerOgBarn(), + ) + assertEquals(BehandlingKategori.NASJONAL, behandlingstemaService.hentLøpendeKategori(defaultFagsak.id)) + } + + @Test + fun `skal hente løpende kategori til EØS når siste behandling er EØS og har løpende utbetaling`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val barn2Fnr = randomFnr() + val barnaFnr = listOf(barnFnr, barn2Fnr) + val behandlingMedEøsRegelverk = defaultBehandling.copy(kategori = BehandlingKategori.EØS) + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(defaultFagsak.id) } returns + behandlingMedEøsRegelverk + val vilkårsvurdering = Vilkårsvurdering(behandling = behandlingMedEøsRegelverk) + vilkårsvurdering.personResultater = lagPersonResultaterForSøkerOgToBarn( + vilkårsvurdering, + tilAktør(søkerFnr), + tilAktør(barnFnr), + tilAktør(barn2Fnr), + LocalDate.now().minusMonths(1), + LocalDate.now().plusYears(2), + ) + every { tidslinjeService.hentTidslinjer(BehandlingId(defaultBehandling.id)) } returns + VilkårsvurderingTidslinjer( + vilkårsvurdering = vilkårsvurdering, + søkerOgBarn = lagTestPersonopplysningGrunnlag(defaultBehandling.id, søkerFnr, barnaFnr) + .tilPersonEnkelSøkerOgBarn(), + ) + assertEquals(BehandlingKategori.EØS, behandlingstemaService.hentLøpendeKategori(defaultFagsak.id)) + } + + @Test + fun `skal hente løpende kategori til NASJONAL når siste behandling er EØS opphørt`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val barn2Fnr = randomFnr() + val barnaFnr = listOf(barnFnr, barn2Fnr) + val behandlingMedEøsRegelverk = defaultBehandling.copy(kategori = BehandlingKategori.EØS) + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(defaultFagsak.id) } returns + behandlingMedEøsRegelverk + val vilkårsvurdering = Vilkårsvurdering(behandling = behandlingMedEøsRegelverk) + vilkårsvurdering.personResultater = lagPersonResultaterForSøkerOgToBarn( + vilkårsvurdering, + tilAktør(søkerFnr), + tilAktør(barnFnr), + tilAktør(barn2Fnr), + LocalDate.now().minusMonths(3), + LocalDate.now().minusMonths(2), + ) + every { tidslinjeService.hentTidslinjer(BehandlingId(defaultBehandling.id)) } returns + VilkårsvurderingTidslinjer( + vilkårsvurdering = vilkårsvurdering, + søkerOgBarn = lagTestPersonopplysningGrunnlag(defaultBehandling.id, søkerFnr, barnaFnr) + .tilPersonEnkelSøkerOgBarn(), + ) + assertEquals(BehandlingKategori.NASJONAL, behandlingstemaService.hentLøpendeKategori(defaultFagsak.id)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingStegTilstandTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingStegTilstandTest.kt new file mode 100644 index 000000000..309621a8d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingStegTilstandTest.kt @@ -0,0 +1,111 @@ +package no.nav.familie.ba.sak.kjerne.behandling.domene + +import io.mockk.mockk +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.steg.BehandlingStegStatus +import no.nav.familie.ba.sak.kjerne.steg.StegType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class BehandlingStegTilstandTest { + + @Test + fun `Verifiser at siste steg får status IKKE_UTFØRT`() { + val behandling = opprettBehandling() + behandling.leggTilBehandlingStegTilstand(StegType.REGISTRERE_PERSONGRUNNLAG) + behandling.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING) + + assertEquals( + BehandlingStegStatus.UTFØRT, + behandling.behandlingStegTilstand.first { it.behandlingSteg == StegType.REGISTRERE_SØKNAD }.behandlingStegStatus, + ) + assertEquals( + BehandlingStegStatus.UTFØRT, + behandling.behandlingStegTilstand.first { it.behandlingSteg == StegType.REGISTRERE_PERSONGRUNNLAG }.behandlingStegStatus, + ) + assertEquals( + BehandlingStegStatus.IKKE_UTFØRT, + behandling.behandlingStegTilstand.first { it.behandlingSteg == StegType.VILKÅRSVURDERING }.behandlingStegStatus, + ) + } + + @Test + fun `Verifiser maks et steg av hver type`() { + val behandling = opprettBehandling() + behandling.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING) + behandling.leggTilBehandlingStegTilstand(StegType.REGISTRERE_PERSONGRUNNLAG) + behandling.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING) + + assertEquals( + BehandlingStegStatus.IKKE_UTFØRT, + behandling.behandlingStegTilstand.single { it.behandlingSteg == StegType.VILKÅRSVURDERING }.behandlingStegStatus, + ) + } + + @Test + fun `Verifiser at alle steg med høyere rekkefølge enn siste fjernes`() { + val behandling = opprettBehandling() + behandling.leggTilBehandlingStegTilstand(StegType.REGISTRERE_PERSONGRUNNLAG) + behandling.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING) + behandling.leggTilBehandlingStegTilstand(StegType.SEND_TIL_BESLUTTER) + behandling.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING) + + assertEquals( + BehandlingStegStatus.UTFØRT, + behandling.behandlingStegTilstand.single { it.behandlingSteg == StegType.REGISTRERE_PERSONGRUNNLAG }.behandlingStegStatus, + ) + assertEquals( + BehandlingStegStatus.UTFØRT, + behandling.behandlingStegTilstand.single { it.behandlingSteg == StegType.REGISTRERE_SØKNAD }.behandlingStegStatus, + ) + assertEquals( + BehandlingStegStatus.IKKE_UTFØRT, + behandling.behandlingStegTilstand.single { it.behandlingSteg == StegType.VILKÅRSVURDERING }.behandlingStegStatus, + ) + assertTrue(behandling.behandlingStegTilstand.none { it.behandlingSteg == StegType.SEND_TIL_BESLUTTER }) + } + + @Test + fun `Verifiser henlegg søknad ikke endrer stegstatus`() { + val behandling = opprettBehandling() + behandling.leggTilBehandlingStegTilstand(StegType.REGISTRERE_PERSONGRUNNLAG) + behandling.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING) + behandling.leggTilBehandlingStegTilstand(StegType.SEND_TIL_BESLUTTER) + behandling.leggTilHenleggStegOmDetIkkeFinnesFraFør() + + assertEquals( + BehandlingStegStatus.UTFØRT, + behandling.behandlingStegTilstand.single { it.behandlingSteg == StegType.REGISTRERE_SØKNAD }.behandlingStegStatus, + ) + assertEquals( + BehandlingStegStatus.UTFØRT, + behandling.behandlingStegTilstand.single { it.behandlingSteg == StegType.REGISTRERE_PERSONGRUNNLAG }.behandlingStegStatus, + ) + assertEquals( + BehandlingStegStatus.UTFØRT, + behandling.behandlingStegTilstand.single { it.behandlingSteg == StegType.VILKÅRSVURDERING }.behandlingStegStatus, + ) + assertEquals( + BehandlingStegStatus.IKKE_UTFØRT, + behandling.behandlingStegTilstand.single { it.behandlingSteg == StegType.SEND_TIL_BESLUTTER }.behandlingStegStatus, + ) + assertEquals( + BehandlingStegStatus.IKKE_UTFØRT, + behandling.behandlingStegTilstand.single { it.behandlingSteg == StegType.HENLEGG_BEHANDLING }.behandlingStegStatus, + ) + } + + fun opprettBehandling(): Behandling { + return Behandling( + id = 1, + fagsak = mockk(), + kategori = BehandlingKategori.NASJONAL, + type = BehandlingType.FØRSTEGANGSBEHANDLING, + underkategori = BehandlingUnderkategori.ORDINÆR, + opprettetÅrsak = BehandlingÅrsak.SØKNAD, + ).also { + it.behandlingStegTilstand.add(BehandlingStegTilstand(0, it, StegType.REGISTRERE_SØKNAD)) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatEndringUtilsTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatEndringUtilsTest.kt new file mode 100644 index 000000000..1ed44758c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatEndringUtilsTest.kt @@ -0,0 +1,1246 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import io.mockk.mockk +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndel +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatEndringUtils.erEndringIBeløp +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatEndringUtils.erEndringIEndretUtbetalingAndeler +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatEndringUtils.erEndringIKompetanse +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatEndringUtils.erEndringIVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatEndringUtils.utledEndringsresultat +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.lagKompetanse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import org.hamcrest.MatcherAssert.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth +import org.hamcrest.CoreMatchers.`is` as Is + +class BehandlingsresultatEndringUtilsTest { + + val søker = tilfeldigPerson() + + private val barn1Aktør = randomAktør() + + val jan22 = YearMonth.of(2022, 1) + val feb22 = YearMonth.of(2022, 2) + val mai22 = YearMonth.of(2022, 5) + val aug22 = YearMonth.of(2022, 8) + val des22 = YearMonth.of(2022, 12) + + @Test + fun `utledEndringsresultat skal returnere INGEN_ENDRING dersom det ikke finnes noe endringer i behandling`() { + val endringsresultat = utledEndringsresultat( + nåværendeAndeler = emptyList(), + forrigeAndeler = emptyList(), + personerFremstiltKravFor = emptyList(), + nåværendeKompetanser = emptyList(), + forrigeKompetanser = emptyList(), + nåværendePersonResultat = emptySet(), + forrigePersonResultat = emptySet(), + nåværendeEndretAndeler = emptyList(), + forrigeEndretAndeler = emptyList(), + personerIBehandling = emptySet(), + personerIForrigeBehandling = emptySet(), + ) + + assertThat(endringsresultat, Is(Endringsresultat.INGEN_ENDRING)) + } + + @Test + fun `utledEndringsresultat skal returnere ENDRING dersom det finnes endringer i beløp`() { + val forrigeAndel = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ) + + val endringsresultat = utledEndringsresultat( + forrigeAndeler = listOf(forrigeAndel), + nåværendeAndeler = listOf(forrigeAndel.copy(kalkulertUtbetalingsbeløp = 40)), + personerFremstiltKravFor = emptyList(), + forrigeKompetanser = emptyList(), + nåværendeKompetanser = emptyList(), + nåværendePersonResultat = emptySet(), + forrigePersonResultat = emptySet(), + nåværendeEndretAndeler = emptyList(), + forrigeEndretAndeler = emptyList(), + personerIBehandling = emptySet(), + personerIForrigeBehandling = emptySet(), + ) + + assertThat(endringsresultat, Is(Endringsresultat.ENDRING)) + } + + @Test + fun `utledEndringsresultat skal returnere ENDRING dersom det finnes endringer i vilkårsvurderingen`() { + val fødselsdato = LocalDate.of(2015, 1, 1) + val barn = lagPerson(aktør = barn1Aktør, fødselsdato = fødselsdato, type = PersonType.BARN) + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2015, 2), + tom = YearMonth.of(2020, 1), + ), + ) + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2015, 2), + tom = YearMonth.of(2020, 1), + ), + ) + + val forrigeVilkårResultater = listOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = LocalDate.of(2020, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val nåværendeVilkårResultater = listOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = LocalDate.of(2020, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.EØS_FORORDNINGEN, + ), + ) + + val forrigePersonResultat = PersonResultat( + id = 0, + vilkårsvurdering = mockk(), + aktør = barn1Aktør, + vilkårResultater = forrigeVilkårResultater.toMutableSet(), + ) + + val nåværendePersonResultat = + PersonResultat( + id = 0, + vilkårsvurdering = mockk(), + aktør = barn1Aktør, + vilkårResultater = nåværendeVilkårResultater.toMutableSet(), + ) + + val endringsresultat = utledEndringsresultat( + forrigeAndeler = forrigeAndeler, + nåværendeAndeler = nåværendeAndeler, + personerFremstiltKravFor = emptyList(), + forrigeKompetanser = emptyList(), + nåværendeKompetanser = emptyList(), + forrigePersonResultat = setOf(forrigePersonResultat), + nåværendePersonResultat = setOf(nåværendePersonResultat), + nåværendeEndretAndeler = emptyList(), + forrigeEndretAndeler = emptyList(), + personerIBehandling = setOf(barn), + personerIForrigeBehandling = setOf(barn), + ) + + assertThat(endringsresultat, Is(Endringsresultat.ENDRING)) + } + + @Test + fun `utledEndringsresultat skal returnere ENDRING dersom det finnes endringer i kompetanse`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val forrigeKompetanse = + lagKompetanse( + behandlingId = forrigeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = "NO", + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + annenForeldersAktivitetsland = "PO", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = YearMonth.now().minusMonths(6), + tom = null, + ) + + val endringsresultat = utledEndringsresultat( + nåværendeAndeler = emptyList(), + forrigeAndeler = emptyList(), + personerFremstiltKravFor = emptyList(), + forrigeKompetanser = listOf(forrigeKompetanse), + nåværendeKompetanser = listOf( + forrigeKompetanse.copy(søkersAktivitet = KompetanseAktivitet.ARBEIDER_PÅ_NORSK_SOKKEL) + .apply { behandlingId = nåværendeBehandling.id }, + ), + nåværendePersonResultat = emptySet(), + forrigePersonResultat = emptySet(), + nåværendeEndretAndeler = emptyList(), + forrigeEndretAndeler = emptyList(), + personerIBehandling = emptySet(), + personerIForrigeBehandling = emptySet(), + ) + + assertThat(endringsresultat, Is(Endringsresultat.ENDRING)) + } + + @Test + fun `utledEndringsresultat skal returnere ENDRING dersom det finnes endringer i endret utbetaling andeler`() { + val barn = lagPerson(type = PersonType.BARN) + val forrigeEndretAndel = lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.ETTERBETALING_3ÅR, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + ) + + val endringsresultat = utledEndringsresultat( + nåværendeAndeler = emptyList(), + forrigeAndeler = emptyList(), + personerFremstiltKravFor = emptyList(), + forrigeKompetanser = emptyList(), + nåværendeKompetanser = emptyList(), + nåværendePersonResultat = emptySet(), + forrigePersonResultat = emptySet(), + forrigeEndretAndeler = listOf(forrigeEndretAndel), + nåværendeEndretAndeler = listOf(forrigeEndretAndel.copy(årsak = Årsak.ALLEREDE_UTBETALT)), + personerIBehandling = emptySet(), + personerIForrigeBehandling = emptySet(), + ) + + assertThat(endringsresultat, Is(Endringsresultat.ENDRING)) + } + + @Test + fun `Endring i beløp - Skal returnere false dersom eneste endring er opphør`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ), + ) + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = mai22, + beløp = 1054, + aktør = barn1Aktør, + ), + ) + + val erEndringIBeløp = erEndringIBeløp( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = emptyList(), + personerFremstiltKravFor = listOf(), + ) + + assertEquals(false, erEndringIBeløp) + } + + @Test + fun `Endring i beløp - Skal returnere true når beløp i periode har gått fra større enn 0 til null og det er søkt for person`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = mai22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val erEndringIBeløp = erEndringIBeløp( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = emptyList(), + personerFremstiltKravFor = listOf(barn1Aktør), + ) + + assertEquals(true, erEndringIBeløp) + } + + @Test + fun `Endring i beløp - Skal returnere false når beløp i periode har gått fra større enn 0 til at annet tall større enn 0 og det er søkt for person`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = mai22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = mai22.plusMonths(1), + tom = aug22, + beløp = 527, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val erEndringIBeløp = erEndringIBeløp( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = emptyList(), + personerFremstiltKravFor = listOf(barn1Aktør), + ) + + assertEquals(false, erEndringIBeløp) + } + + @Test + fun `Endring i beløp - Skal returnere true når beløp i periode har gått fra null til et tall større enn 0 og det ikke er søkt for person`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = des22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val erEndringIBeløp = erEndringIBeløp( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = emptyList(), + personerFremstiltKravFor = listOf(), + ) + + assertEquals(true, erEndringIBeløp) + } + + @Test + fun `Endring i beløp - Skal returnere false når beløp i periode har gått fra null til et tall større enn 0 og det er søkt for person`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = des22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val erEndringIBeløp = erEndringIBeløp( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = emptyList(), + personerFremstiltKravFor = listOf(barn1Aktør), + ) + + assertEquals(false, erEndringIBeløp) + } + + @Test + fun `Endring i beløp - Skal returnere true når beløp i periode har gått fra større enn 0 til at annet tall større enn 0 og det ikke er søkt for person`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = mai22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = mai22.plusMonths(1), + tom = aug22, + beløp = 527, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val erEndringIBeløp = erEndringIBeløp( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = emptyList(), + personerFremstiltKravFor = listOf(), + ) + + assertEquals(true, erEndringIBeløp) + } + + @Test + fun `Endring i beløp - Skal returnere true hvis utvidet ikke er endret men småbarnstillegg kun er lagt på`() { + val søker = lagPerson(type = PersonType.SØKER).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = søker, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = søker, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ), + lagAndelTilkjentYtelse( + fom = mai22, + tom = aug22, + beløp = 630, + aktør = søker, + ytelseType = YtelseType.SMÅBARNSTILLEGG, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val erEndringIBeløp = erEndringIBeløp( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = emptyList(), + personerFremstiltKravFor = listOf(), + ) + + assertEquals(true, erEndringIBeløp) + } + + @Test + fun `Endring i endret utbetaling andel - skal returnere true hvis årsak er endret`() { + val barn = lagPerson(type = PersonType.BARN) + val forrigeEndretAndel = lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.ETTERBETALING_3ÅR, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + ) + + val erEndringIEndretAndeler = erEndringIEndretUtbetalingAndeler( + forrigeEndretAndeler = listOf(forrigeEndretAndel), + nåværendeEndretAndeler = listOf(forrigeEndretAndel.copy(årsak = Årsak.ALLEREDE_UTBETALT)), + ) + + assertTrue(erEndringIEndretAndeler) + } + + @Test + fun `Endring i endret utbetaling andel - skal returnere true hvis avtaletidspunktDeltBosted er endret`() { + val barn = lagPerson(type = PersonType.BARN) + val forrigeEndretAndel = lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.DELT_BOSTED, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + avtaletidspunktDeltBosted = jan22.førsteDagIInneværendeMåned(), + ) + + val erEndringIEndretAndeler = erEndringIEndretUtbetalingAndeler( + forrigeEndretAndeler = listOf(forrigeEndretAndel), + nåværendeEndretAndeler = listOf(forrigeEndretAndel.copy(avtaletidspunktDeltBosted = feb22.førsteDagIInneværendeMåned())), + ) + + assertTrue(erEndringIEndretAndeler) + } + + @Test + fun `Endring i endret utbetaling andel - skal returnere true hvis søknadstidspunkt er endret`() { + val barn = lagPerson(type = PersonType.BARN) + val forrigeEndretAndel = lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.DELT_BOSTED, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + avtaletidspunktDeltBosted = jan22.førsteDagIInneværendeMåned(), + ) + + val erEndringIEndretAndeler = erEndringIEndretUtbetalingAndeler( + forrigeEndretAndeler = listOf(forrigeEndretAndel), + nåværendeEndretAndeler = listOf(forrigeEndretAndel.copy(søknadstidspunkt = feb22.førsteDagIInneværendeMåned())), + ) + + assertTrue(erEndringIEndretAndeler) + } + + @Test + fun `Endring i endret utbetaling andel - skal returnere false hvis prosent er endret`() { + val barn = lagPerson(type = PersonType.BARN) + val forrigeEndretAndel = lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.DELT_BOSTED, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + avtaletidspunktDeltBosted = jan22.førsteDagIInneværendeMåned(), + ) + + val erEndringIEndretAndeler = erEndringIEndretUtbetalingAndeler( + forrigeEndretAndeler = listOf(forrigeEndretAndel), + nåværendeEndretAndeler = listOf(forrigeEndretAndel.copy(prosent = BigDecimal(100))), + ) + + assertFalse(erEndringIEndretAndeler) + } + + @Test + fun `Endring i endret utbetaling andel - skal returnere false hvis eneste endring er at perioden blir lenger`() { + val barn = lagPerson(type = PersonType.BARN) + val forrigeEndretAndel = lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.DELT_BOSTED, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + avtaletidspunktDeltBosted = jan22.førsteDagIInneværendeMåned(), + ) + + val erEndringIEndretAndeler = erEndringIEndretUtbetalingAndeler( + forrigeEndretAndeler = listOf(forrigeEndretAndel), + nåværendeEndretAndeler = listOf(forrigeEndretAndel.copy(tom = des22)), + ) + + assertFalse(erEndringIEndretAndeler) + } + + @Test + fun `Endring i endret utbetaling andel - skal returnere false hvis endringsperiode oppstår i nåværende behandling`() { + val barn = lagPerson(type = PersonType.BARN) + val nåværendeEndretAndel = lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.DELT_BOSTED, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + avtaletidspunktDeltBosted = jan22.førsteDagIInneværendeMåned(), + ) + + val erEndringIEndretAndeler = erEndringIEndretUtbetalingAndeler( + forrigeEndretAndeler = emptyList(), + nåværendeEndretAndeler = listOf(nåværendeEndretAndel), + ) + + assertFalse(erEndringIEndretAndeler) + } + + @Test + fun `Endring i endret utbetaling andel - skal returnere true hvis et av to barn har endring på årsak`() { + val barn1 = lagPerson(type = PersonType.BARN) + val barn2 = lagPerson(type = PersonType.BARN) + + val forrigeEndretAndelBarn1 = lagEndretUtbetalingAndel( + person = barn1, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.DELT_BOSTED, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + avtaletidspunktDeltBosted = jan22.førsteDagIInneværendeMåned(), + ) + + val forrigeEndretAndelBarn2 = lagEndretUtbetalingAndel( + person = barn2, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.ETTERBETALING_3ÅR, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + ) + + val erEndringIEndretAndeler = erEndringIEndretUtbetalingAndeler( + forrigeEndretAndeler = listOf(forrigeEndretAndelBarn1, forrigeEndretAndelBarn2), + nåværendeEndretAndeler = listOf( + forrigeEndretAndelBarn1, + forrigeEndretAndelBarn2.copy(årsak = Årsak.ALLEREDE_UTBETALT), + ), + ) + + assertTrue(erEndringIEndretAndeler) + } + + @Test + fun `Endring i kompetanse - skal returnere false når ingenting endrer seg`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val forrigeKompetanse = + lagKompetanse( + behandlingId = forrigeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = "NO", + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + annenForeldersAktivitetsland = "PO", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = YearMonth.now().minusMonths(6), + tom = null, + ) + + val endring = erEndringIKompetanse( + nåværendeKompetanser = listOf(forrigeKompetanse.copy().apply { behandlingId = nåværendeBehandling.id }), + forrigeKompetanser = listOf(forrigeKompetanse), + ) + + assertEquals(false, endring) + } + + @Test + fun `Endring i kompetanse - skal returnere true når søkers aktivitetsland endrer seg`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val forrigeKompetanse = + lagKompetanse( + behandlingId = forrigeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = "NO", + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + annenForeldersAktivitetsland = "PO", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = YearMonth.now().minusMonths(6), + tom = null, + ) + + val endring = erEndringIKompetanse( + nåværendeKompetanser = listOf( + forrigeKompetanse.copy(søkersAktivitetsland = "DK").apply { behandlingId = nåværendeBehandling.id }, + ), + forrigeKompetanser = listOf(forrigeKompetanse), + ) + + assertEquals(true, endring) + } + + @Test + fun `Endring i kompetanse - skal returnere true når søkers aktivitet endrer seg`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val forrigeKompetanse = + lagKompetanse( + behandlingId = forrigeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = "NO", + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + annenForeldersAktivitetsland = "PO", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = YearMonth.now().minusMonths(6), + tom = null, + ) + + val endring = erEndringIKompetanse( + nåværendeKompetanser = listOf( + forrigeKompetanse.copy(søkersAktivitet = KompetanseAktivitet.ARBEIDER_PÅ_NORSK_SOKKEL) + .apply { behandlingId = nåværendeBehandling.id }, + ), + forrigeKompetanser = listOf(forrigeKompetanse), + ) + + assertEquals(true, endring) + } + + @Test + fun `Endring i kompetanse - skal returnere true når annen forelders aktivitetsland endrer seg`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val forrigeKompetanse = + lagKompetanse( + behandlingId = forrigeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = "NO", + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + annenForeldersAktivitetsland = "PO", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = YearMonth.now().minusMonths(6), + tom = null, + ) + + val endring = erEndringIKompetanse( + nåværendeKompetanser = listOf( + forrigeKompetanse.copy(annenForeldersAktivitetsland = "DK") + .apply { behandlingId = nåværendeBehandling.id }, + ), + forrigeKompetanser = listOf(forrigeKompetanse), + ) + + assertEquals(true, endring) + } + + @Test + fun `Endring i kompetanse - skal returnere true når annen forelders aktivitet endrer seg`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val forrigeKompetanse = + lagKompetanse( + behandlingId = forrigeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = "NO", + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + annenForeldersAktivitetsland = "PO", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = YearMonth.now().minusMonths(6), + tom = null, + ) + + val endring = erEndringIKompetanse( + nåværendeKompetanser = listOf( + forrigeKompetanse.copy(annenForeldersAktivitet = KompetanseAktivitet.FORSIKRET_I_BOSTEDSLAND) + .apply { behandlingId = nåværendeBehandling.id }, + ), + forrigeKompetanser = listOf(forrigeKompetanse), + ) + + assertEquals(true, endring) + } + + @Test + fun `Endring i kompetanse - skal returnere true når barnets bostedsland endrer seg`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val forrigeKompetanse = + lagKompetanse( + behandlingId = forrigeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = "NO", + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + annenForeldersAktivitetsland = "PO", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = YearMonth.now().minusMonths(6), + tom = null, + ) + + val endring = erEndringIKompetanse( + nåværendeKompetanser = listOf( + forrigeKompetanse.copy(barnetsBostedsland = "DK").apply { behandlingId = nåværendeBehandling.id }, + ), + forrigeKompetanser = listOf(forrigeKompetanse), + ) + + assertEquals(true, endring) + } + + @Test + fun `Endring i kompetanse - skal returnere true når resultat på kompetansen endrer seg`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val forrigeKompetanse = + lagKompetanse( + behandlingId = forrigeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = "NO", + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + annenForeldersAktivitetsland = "PO", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = YearMonth.now().minusMonths(6), + tom = null, + ) + + val endring = erEndringIKompetanse( + nåværendeKompetanser = listOf( + forrigeKompetanse.copy(resultat = KompetanseResultat.NORGE_ER_SEKUNDÆRLAND) + .apply { behandlingId = nåværendeBehandling.id }, + ), + forrigeKompetanser = listOf(forrigeKompetanse), + ) + + assertEquals(true, endring) + } + + @Test + fun `Endring i kompetanse - skal returnere false når det kun blir lagt på en ekstra kompetanseperiode`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val forrigeKompetanse = + lagKompetanse( + behandlingId = forrigeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = "NO", + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + annenForeldersAktivitetsland = "PO", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = YearMonth.now().minusMonths(6), + tom = null, + ) + + val endring = erEndringIKompetanse( + nåværendeKompetanser = listOf( + forrigeKompetanse.copy(fom = YearMonth.now().minusMonths(10)) + .apply { behandlingId = nåværendeBehandling.id }, + ), + forrigeKompetanser = listOf(forrigeKompetanse), + ) + + assertEquals(false, endring) + } + + @Test + fun `Endring i vilkårsvurdering - skal returnere false dersom vilkårresultatene er helt like`() { + val fødselsdato = LocalDate.of(2015, 1, 1) + val nåværendeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = LocalDate.of(2020, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.of(2020, 1, 2), + periodeTom = LocalDate.of(2022, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val forrigeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = LocalDate.of(2020, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.of(2020, 1, 2), + periodeTom = LocalDate.of(2022, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val aktør = randomAktør() + val barn = lagPerson(aktør = aktør, fødselsdato = fødselsdato, type = PersonType.BARN) + + val erEndringIVilkårvurderingForPerson = erEndringIVilkårsvurdering( + nåværendePersonResultat = setOf(lagPersonResultatFraVilkårResultater(nåværendeVilkårResultat, aktør)), + forrigePersonResultat = setOf(lagPersonResultatFraVilkårResultater(forrigeVilkårResultat, aktør)), + personerIBehandling = setOf(barn), + personerIForrigeBehandling = setOf(barn), + ) + + assertThat(erEndringIVilkårvurderingForPerson, Is(false)) + } + + @Test + fun `Endring i vilkårsvurdering - skal returnere true dersom det har vært endringer i regelverk`() { + val fødselsdato = LocalDate.of(2015, 1, 1) + val nåværendeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = LocalDate.of(2020, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val forrigeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = LocalDate.of(2020, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.EØS_FORORDNINGEN, + ), + ) + + val aktør = randomAktør() + val barn = lagPerson(aktør = aktør, fødselsdato = fødselsdato, type = PersonType.BARN) + + val erEndringIVilkårvurderingForPerson = erEndringIVilkårsvurdering( + nåværendePersonResultat = setOf(lagPersonResultatFraVilkårResultater(nåværendeVilkårResultat, aktør)), + forrigePersonResultat = setOf(lagPersonResultatFraVilkårResultater(forrigeVilkårResultat, aktør)), + personerIBehandling = setOf(barn), + personerIForrigeBehandling = setOf(barn), + ) + + assertThat(erEndringIVilkårvurderingForPerson, Is(true)) + } + + @Test + fun `Endring i vilkårsvurdering - skal returnere true dersom det har vært endringer i utdypendevilkårsvurdering`() { + val fødselsdato = LocalDate.of(2015, 1, 1) + val nåværendeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = LocalDate.of(2020, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.VURDERING_ANNET_GRUNNLAG, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val forrigeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = LocalDate.of(2020, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val aktør = randomAktør() + val barn = lagPerson(aktør = aktør, fødselsdato = fødselsdato, type = PersonType.BARN) + + val erEndringIVilkårvurderingForPerson = erEndringIVilkårsvurdering( + nåværendePersonResultat = setOf(lagPersonResultatFraVilkårResultater(nåværendeVilkårResultat, aktør)), + forrigePersonResultat = setOf(lagPersonResultatFraVilkårResultater(forrigeVilkårResultat, aktør)), + personerIBehandling = setOf(barn), + personerIForrigeBehandling = setOf(barn), + ) + + assertThat(erEndringIVilkårvurderingForPerson, Is(true)) + } + + @Test + fun `Endring i vilkårsvurdering - skal returnere true dersom det har oppstått splitt i vilkårsvurderingen`() { + val fødselsdato = jan22.førsteDagIInneværendeMåned() + val forrigeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = null, + begrunnelse = "", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf(), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val nåværendeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = mai22.atDay(7), + begrunnelse = "", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf(), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = mai22.atDay(8), + periodeTom = null, + begrunnelse = "", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf(), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val aktør = randomAktør() + val barn = lagPerson(aktør = aktør, fødselsdato = fødselsdato, type = PersonType.BARN) + + val erEndringIVilkårvurderingForPerson = erEndringIVilkårsvurdering( + nåværendePersonResultat = setOf(lagPersonResultatFraVilkårResultater(nåværendeVilkårResultat, aktør)), + forrigePersonResultat = setOf(lagPersonResultatFraVilkårResultater(forrigeVilkårResultat, aktør)), + personerIBehandling = setOf(barn), + personerIForrigeBehandling = setOf(barn), + ) + + assertThat(erEndringIVilkårvurderingForPerson, Is(true)) + } + + @Test + fun `Endring i vilkårsvurdering - skal returnere false hvis det kun er opphørt`() { + val fødselsdato = LocalDate.of(2015, 1, 1) + val nåværendeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = LocalDate.of(2020, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val forrigeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = null, + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val aktør = randomAktør() + val barn = lagPerson(aktør = aktør, fødselsdato = fødselsdato, type = PersonType.BARN) + + val erEndringIVilkårvurderingForPerson = erEndringIVilkårsvurdering( + nåværendePersonResultat = setOf(lagPersonResultatFraVilkårResultater(nåværendeVilkårResultat, aktør)), + forrigePersonResultat = setOf(lagPersonResultatFraVilkårResultater(forrigeVilkårResultat, aktør)), + personerIBehandling = setOf(barn), + personerIForrigeBehandling = setOf(barn), + ) + + assertThat(erEndringIVilkårvurderingForPerson, Is(false)) + } + + private fun lagPersonResultatFraVilkårResultater( + vilkårResultater: Set, + aktør: Aktør, + ): PersonResultat { + val vilkårsvurdering = + lagVilkårsvurdering(behandling = lagBehandling(), resultat = Resultat.OPPFYLT, søkerAktør = randomAktør()) + val personResultat = PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = aktør) + + personResultat.setSortedVilkårResultater(vilkårResultater) + + return personResultat + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatOpph\303\270rUtilsTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatOpph\303\270rUtilsTest.kt" new file mode 100644 index 000000000..3510403c1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatOpph\303\270rUtilsTest.kt" @@ -0,0 +1,444 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import io.mockk.clearStaticMockk +import io.mockk.every +import io.mockk.mockkStatic +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndel +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatOpphørUtils.filtrerBortIrrelevanteAndeler +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatOpphørUtils.hentOpphørsresultatPåBehandling +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import java.math.BigDecimal +import java.time.YearMonth + +class BehandlingsresultatOpphørUtilsTest { + + val søker = tilfeldigPerson() + + val jan22 = YearMonth.of(2022, 1) + val feb22 = YearMonth.of(2022, 2) + val mar22 = YearMonth.of(2022, 3) + val mai22 = YearMonth.of(2022, 5) + val aug22 = YearMonth.of(2022, 8) + + @BeforeEach + fun reset() { + clearStaticMockk(YearMonth::class) + } + + @Test + fun `hentOpphørsresultatPåBehandling skal returnere IKKE_OPPHØRT dersom nåværende andeler strekker seg lengre enn dagens dato`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + mockkStatic(YearMonth::class) + every { YearMonth.now() } returns YearMonth.of(2022, 4) + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = mai22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = mai22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = mai22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val opphørsresultat = hentOpphørsresultatPåBehandling( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = emptyList(), + forrigeEndretAndeler = emptyList(), + ) + + assertEquals(Opphørsresultat.IKKE_OPPHØRT, opphørsresultat) + } + + @Test + fun `hentOpphørsresultatPåBehandling skal returnere OPPHØRT dersom nåværende andeler opphører mens forrige andeler ikke opphører til og med dagens dato`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + mockkStatic(YearMonth::class) + every { YearMonth.now() } returns YearMonth.of(2022, 4) + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = feb22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = feb22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val opphørsresultat = hentOpphørsresultatPåBehandling( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = emptyList(), + forrigeEndretAndeler = emptyList(), + ) + + assertEquals(Opphørsresultat.OPPHØRT, opphørsresultat) + } + + @Test + fun `hentOpphørsresultatPåBehandling skal returnere OPPHØRT dersom nåværende andeler opphører tidligere enn forrige andeler og dagens dato`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + val apr22 = YearMonth.of(2022, 4) + + mockkStatic(YearMonth::class) + every { YearMonth.now() } returns apr22 + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = mar22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = mar22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = feb22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = feb22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val opphørsresultat = hentOpphørsresultatPåBehandling( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = emptyList(), + forrigeEndretAndeler = emptyList(), + ) + + assertEquals(Opphørsresultat.OPPHØRT, opphørsresultat) + } + + @Test + fun `hentOpphørsresultatPåBehandling skal returnere OPPHØRT dersom vi går fra andeler på person til fullt opphør på person`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val apr22 = YearMonth.of(2022, 4) + + mockkStatic(YearMonth::class) + every { YearMonth.now() } returns apr22 + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = mar22, + beløp = 1054, + aktør = barn1Aktør, + ), + ) + + val opphørsresultat = hentOpphørsresultatPåBehandling( + nåværendeAndeler = emptyList(), + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = emptyList(), + forrigeEndretAndeler = emptyList(), + ) + + assertEquals(Opphørsresultat.OPPHØRT, opphørsresultat) + } + + @Test + fun `hentOpphørsresultatPåBehandling skal returnere FORTSATT_OPPHØRT dersom nåværende andeler har lik opphørsdato som forrige andeler`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + val apr22 = YearMonth.of(2022, 4) + + mockkStatic(YearMonth::class) + every { YearMonth.now() } returns apr22 + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = mar22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = mar22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = mar22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = mar22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val opphørsresultat = hentOpphørsresultatPåBehandling( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = emptyList(), + forrigeEndretAndeler = emptyList(), + ) + + assertEquals(Opphørsresultat.FORTSATT_OPPHØRT, opphørsresultat) + } + + @Test + fun `hentOpphørsresultatPåBehandling skal returnere IKKE_OPPHØRT dersom nåværende andeler har lik opphørsdato som forrige andeler men det er i fremtiden`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + val apr22 = YearMonth.of(2022, 4) + + mockkStatic(YearMonth::class) + every { YearMonth.now() } returns apr22 + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = mar22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = mar22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val opphørsresultat = hentOpphørsresultatPåBehandling( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + nåværendeEndretAndeler = emptyList(), + forrigeEndretAndeler = emptyList(), + ) + + assertEquals(Opphørsresultat.IKKE_OPPHØRT, opphørsresultat) + } + + @ParameterizedTest + @EnumSource(Årsak::class, names = ["ALLEREDE_UTBETALT", "ENDRE_MOTTAKER", "ETTERBETALING_3ÅR"]) + internal fun `filtrerBortIrrelevanteAndeler - skal filtrere andeler som har 0 i beløp og endret utbetaling andel med årsak ALLEREDE_UTBETALT, ENDRE_MOTTAKER eller ETTERBETALING_3ÅR`(årsak: Årsak) { + val barn = lagPerson(type = PersonType.BARN) + val barnAktør = barn.aktør + + val andeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = feb22, + beløp = 0, + aktør = barnAktør, + ), + lagAndelTilkjentYtelse( + fom = mar22, + tom = mai22, + beløp = 1400, + aktør = barnAktør, + ), + lagAndelTilkjentYtelse( + fom = aug22, + tom = aug22, + beløp = 0, + aktør = barnAktør, + ), + ) + + val endretUtBetalingAndeler = + listOf( + lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = feb22, + årsak = årsak, + ), + lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = aug22, + tom = aug22, + årsak = årsak, + ), + ) + + val andelerEtterFiltrering = andeler.filtrerBortIrrelevanteAndeler(endretUtBetalingAndeler) + + assertEquals(andelerEtterFiltrering.minOf { it.stønadFom }, mar22) + assertEquals(andelerEtterFiltrering.maxOf { it.stønadTom }, mai22) + } + + @Test + internal fun `filtrerBortIrrelevanteAndeler - skal ikke filtrere andeler som har 0 i beløp og endret utbetaling andel med årsak DELT_BOSTED`() { + val barn = lagPerson(type = PersonType.BARN) + val barnAktør = barn.aktør + + val andeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = feb22, + beløp = 0, + aktør = barnAktør, + ), + lagAndelTilkjentYtelse( + fom = mar22, + tom = mai22, + beløp = 1400, + aktør = barnAktør, + ), + lagAndelTilkjentYtelse( + fom = aug22, + tom = aug22, + beløp = 0, + aktør = barnAktør, + ), + ) + + val endretUtBetalingAndeler = + listOf( + lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = feb22, + årsak = Årsak.DELT_BOSTED, + ), + lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = aug22, + tom = aug22, + årsak = Årsak.DELT_BOSTED, + ), + ) + + val andelerEtterFiltrering = andeler.filtrerBortIrrelevanteAndeler(endretUtBetalingAndeler) + + assertEquals(andelerEtterFiltrering.minOf { it.stønadFom }, jan22) + assertEquals(andelerEtterFiltrering.maxOf { it.stønadTom }, aug22) + } + + @Test + internal fun `filtrerBortIrrelevanteAndeler - skal ikke filtrere andeler som har 0 i beløp grunnet differanseberegning`() { + val barn = lagPerson(type = PersonType.BARN) + val barnAktør = barn.aktør + val søker = lagPerson(type = PersonType.SØKER) + val søkerAktør = søker.aktør + + val andeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = feb22, + beløp = 0, + differanseberegnetPeriodebeløp = 50, + aktør = søkerAktør, + ), + lagAndelTilkjentYtelse( + fom = mar22, + tom = mai22, + beløp = 0, + differanseberegnetPeriodebeløp = 50, + aktør = barnAktør, + ), + lagAndelTilkjentYtelse( + fom = aug22, + tom = aug22, + beløp = 0, + differanseberegnetPeriodebeløp = 50, + aktør = barnAktør, + ), + ) + + val andelerEtterFiltrering = andeler.filtrerBortIrrelevanteAndeler(endretAndeler = emptyList()) + + assertEquals(andelerEtterFiltrering.minOf { it.stønadFom }, jan22) + assertEquals(andelerEtterFiltrering.maxOf { it.stønadTom }, aug22) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatServiceTest.kt new file mode 100644 index 000000000..3514ebf1f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatServiceTest.kt @@ -0,0 +1,276 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.ekstern.restDomene.BarnMedOpplysninger +import no.nav.familie.ba.sak.ekstern.restDomene.BehandlingUnderkategoriDTO +import no.nav.familie.ba.sak.ekstern.restDomene.SøknadDTO +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.KompetanseService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.søknad.SøknadGrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.containsInAnyOrder +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.hamcrest.CoreMatchers.`is` as Is + +@ExtendWith(MockKExtension::class) +internal class BehandlingsresultatServiceTest { + + @MockK + private lateinit var behandlingHentOgPersisterService: BehandlingHentOgPersisterService + + @MockK + private lateinit var søknadGrunnlagService: SøknadGrunnlagService + + @MockK + private lateinit var personidentService: PersonidentService + + @MockK + private lateinit var persongrunnlagService: PersongrunnlagService + + @MockK + private lateinit var vilkårsvurderingService: VilkårsvurderingService + + @MockK + private lateinit var kompetanseService: KompetanseService + + @MockK + private lateinit var endretUtbetalingAndelHentOgPersisterService: EndretUtbetalingAndelHentOgPersisterService + + @MockK + private lateinit var andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository + + @InjectMockKs + private lateinit var behandlingsresultatService: BehandlingsresultatService + + @Test + fun `finnPersonerFremstiltKravFor skal returnere tom liste dersom behandlingen ikke er søknad, fødselshendelse eller manuell migrering`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.DØDSFALL_BRUKER) + + val personerFramstiltForKrav = behandlingsresultatService.finnPersonerFremstiltKravFor( + behandling = behandling, + søknadDTO = null, + forrigeBehandling = null, + ) + + assertThat(personerFramstiltForKrav, Is(emptyList())) + } + + @Test + fun `finnPersonerFremstiltKravFor skal returnere aktør som person framstilt krav for dersom det er søkt for utvidet barnetrygd`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.SØKNAD) + + val barnSomIkkeErKryssetAvFor = BarnMedOpplysninger( + ident = randomFnr(), + navn = "barn1", + inkludertISøknaden = false, + erFolkeregistrert = true, + ) + + val søknadDto = SøknadDTO( + underkategori = BehandlingUnderkategoriDTO.UTVIDET, + barnaMedOpplysninger = listOf(barnSomIkkeErKryssetAvFor), + søkerMedOpplysninger = mockk(), + endringAvOpplysningerBegrunnelse = "", + ) + + every { vilkårsvurderingService.hentAktivForBehandlingThrows(any()) } returns Vilkårsvurdering(behandling = behandling) + + val personerFramstiltForKrav = + behandlingsresultatService.finnPersonerFremstiltKravFor( + behandling = behandling, + søknadDTO = søknadDto, + forrigeBehandling = null, + ) + + assertThat(personerFramstiltForKrav.single(), Is(behandling.fagsak.aktør)) + } + + @Test + fun `finnPersonerFremstiltKravFor skal returnere aktør som person framstilt krav for dersom det er søkt for utvidet barnetrygd og barn som er krysset for`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.SØKNAD) + val barn = lagPerson(type = PersonType.BARN) + + val barnSomErKryssetAvFor = BarnMedOpplysninger( + ident = barn.aktør.aktivFødselsnummer(), + navn = "barn1", + inkludertISøknaden = true, + erFolkeregistrert = true, + ) + + val søknadDto = SøknadDTO( + underkategori = BehandlingUnderkategoriDTO.UTVIDET, + barnaMedOpplysninger = listOf(barnSomErKryssetAvFor), + søkerMedOpplysninger = mockk(), + endringAvOpplysningerBegrunnelse = "", + ) + + every { vilkårsvurderingService.hentAktivForBehandlingThrows(any()) } returns Vilkårsvurdering(behandling = behandling) + every { personidentService.hentAktør(barn.aktør.aktivFødselsnummer()) } returns barn.aktør + + val personerFramstiltForKrav = + behandlingsresultatService.finnPersonerFremstiltKravFor( + behandling = behandling, + søknadDTO = søknadDto, + forrigeBehandling = null, + ) + + assertThat(personerFramstiltForKrav.size, Is(2)) + assertThat(personerFramstiltForKrav, containsInAnyOrder(behandling.fagsak.aktør, barn.aktør)) + } + + @Test + fun `finnPersonerFremstiltKravFor skal bare returnere barn som er folkeregistret og krysset av på søknad`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.SØKNAD) + val barn1Fnr = randomFnr() + val mocketAktør = mockk() + + val barnSomErKryssetAvFor = BarnMedOpplysninger( + ident = barn1Fnr, + navn = "barn1", + inkludertISøknaden = true, + erFolkeregistrert = true, + ) + + val barnSomIkkeErKryssetAvFor = BarnMedOpplysninger( + ident = randomFnr(), + navn = "barn2", + inkludertISøknaden = false, + erFolkeregistrert = true, + ) + + val barnSomErKryssetAvForMenIkkeFolkeregistrert = BarnMedOpplysninger( + ident = randomFnr(), + navn = "barn3", + inkludertISøknaden = true, + erFolkeregistrert = false, + ) + + val søknadDto = SøknadDTO( + underkategori = BehandlingUnderkategoriDTO.ORDINÆR, + barnaMedOpplysninger = listOf( + barnSomErKryssetAvFor, + barnSomIkkeErKryssetAvFor, + barnSomErKryssetAvForMenIkkeFolkeregistrert, + ), + søkerMedOpplysninger = mockk(), + endringAvOpplysningerBegrunnelse = "", + ) + + every { personidentService.hentAktør(barn1Fnr) } returns mocketAktør + + val personerFramstiltForKrav = + behandlingsresultatService.finnPersonerFremstiltKravFor( + behandling = behandling, + søknadDTO = søknadDto, + forrigeBehandling = null, + ) + + assertThat(personerFramstiltForKrav.single(), Is(mocketAktør)) + + verify(exactly = 1) { personidentService.hentAktør(barn1Fnr) } + } + + @Test + fun `finnPersonerFremstiltKravFor skal returnere nye barn dersom behandlingen har fødselshendelse som årsak`() { + val forrigeBehandling = lagBehandling(årsak = BehandlingÅrsak.SØKNAD) + val behandling = lagBehandling(årsak = BehandlingÅrsak.FØDSELSHENDELSE) + val nyttBarn = lagPerson() + + every { persongrunnlagService.finnNyeBarn(behandling, forrigeBehandling) } returns listOf(nyttBarn) + + val personerFramstiltForKrav = + behandlingsresultatService.finnPersonerFremstiltKravFor( + behandling = behandling, + søknadDTO = null, + forrigeBehandling = forrigeBehandling, + ) + + assertThat(personerFramstiltForKrav.single(), Is(nyttBarn.aktør)) + + verify(exactly = 1) { persongrunnlagService.finnNyeBarn(behandling, forrigeBehandling) } + } + + @Test + fun `finnPersonerFremstiltKravFor skal returnere eksisterende personer fra persongrunnlaget dersom behandlingen er en manuell migrering`() { + val behandling = lagBehandling( + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + ) + val eksisterendeBarn = lagPerson() + val eksisterendePersonpplysningGrunnlag = + PersonopplysningGrunnlag(behandlingId = behandling.id, personer = mutableSetOf(eksisterendeBarn)) + + every { persongrunnlagService.hentAktivThrows(behandling.id) } returns eksisterendePersonpplysningGrunnlag + + val personerFramstiltForKrav = + behandlingsresultatService.finnPersonerFremstiltKravFor( + behandling = behandling, + søknadDTO = null, + forrigeBehandling = null, + ) + + assertThat(personerFramstiltForKrav.single(), Is(eksisterendeBarn.aktør)) + + verify(exactly = 1) { persongrunnlagService.hentAktivThrows(behandling.id) } + } + + @Test + fun `finnPersonerFremstiltKravFor skal ikke returnere duplikater av personer`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.SØKNAD) + val barn = lagPerson(type = PersonType.BARN) + + val barnSomErKryssetAvFor = BarnMedOpplysninger( + ident = barn.aktør.aktivFødselsnummer(), + navn = "barn1", + inkludertISøknaden = true, + erFolkeregistrert = true, + ) + + val duplikatBarnSomErKryssetAvFor = BarnMedOpplysninger( + ident = barn.aktør.aktivFødselsnummer(), + navn = "barn1", + inkludertISøknaden = true, + erFolkeregistrert = true, + ) + + val søknadDto = SøknadDTO( + underkategori = BehandlingUnderkategoriDTO.ORDINÆR, + barnaMedOpplysninger = listOf(barnSomErKryssetAvFor, duplikatBarnSomErKryssetAvFor), + søkerMedOpplysninger = mockk(), + endringAvOpplysningerBegrunnelse = "", + ) + + every { vilkårsvurderingService.hentAktivForBehandlingThrows(any()) } returns Vilkårsvurdering(behandling = behandling) + every { personidentService.hentAktør(barn.aktør.aktivFødselsnummer()) } returns barn.aktør + + val personerFramstiltForKrav = + behandlingsresultatService.finnPersonerFremstiltKravFor( + behandling = behandling, + søknadDTO = søknadDto, + forrigeBehandling = null, + ) + + assertThat(personerFramstiltForKrav.size, Is(1)) + assertThat(personerFramstiltForKrav.single(), Is(barn.aktør)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatStegTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatStegTest.kt new file mode 100644 index 000000000..59d28dcb7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatStegTest.kt @@ -0,0 +1,468 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.tilPersonEnkel +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.SatsService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.forrigebehandling.EndringIUtbetalingUtil +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.simulering.SimuleringService +import no.nav.familie.ba.sak.kjerne.steg.EndringerIUtbetalingForBehandlingSteg +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårService +import org.assertj.core.api.Assertions.assertThatCode +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import java.math.BigDecimal +import java.time.YearMonth + +class BehandlingsresultatStegTest { + + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService = mockk() + + private val behandlingService: BehandlingService = mockk() + + private val simuleringService: SimuleringService = mockk() + + private val vedtakService: VedtakService = mockk() + + private val vedtaksperiodeService: VedtaksperiodeService = mockk() + + private val mockBehandlingsresultatService: BehandlingsresultatService = mockk() + + private val vilkårService: VilkårService = mockk() + + private val persongrunnlagService: PersongrunnlagService = mockk() + + private val beregningService: BeregningService = mockk() + + private lateinit var behandlingsresultatSteg: BehandlingsresultatSteg + + private lateinit var behandling: Behandling + + private val andelerTilkjentYtelseOgEndreteUtbetalingerService = + mockk() + + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository = mockk() + + @BeforeEach + fun init() { + behandlingsresultatSteg = BehandlingsresultatSteg( + behandlingHentOgPersisterService, + behandlingService, + simuleringService, + vedtakService, + vedtaksperiodeService, + mockBehandlingsresultatService, + vilkårService, + persongrunnlagService, + beregningService, + andelerTilkjentYtelseOgEndreteUtbetalingerService, + andelTilkjentYtelseRepository, + ) + + behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + ) + } + + @Test + fun `skal kaste exception hvis behandlingsresultat er Avslått for en manuell migrering`() { + every { mockBehandlingsresultatService.utledBehandlingsresultat(any()) } returns Behandlingsresultat.AVSLÅTT + + every { + behandlingService.oppdaterBehandlingsresultat( + any(), + any(), + ) + } returns behandling.copy(resultat = Behandlingsresultat.AVSLÅTT) + + val exception = assertThrows { behandlingsresultatSteg.utførStegOgAngiNeste(behandling, "") } + assertEquals( + "Du har fått behandlingsresultatet Avslått. " + + "Dette er ikke støttet på migreringsbehandlinger. " + + "Meld sak i Porten om du er uenig i resultatet.", + exception.message, + ) + } + + @Test + fun `skal kaste exception hvis behandlingsresultat er Delvis Innvilget for en manuell migrering`() { + every { mockBehandlingsresultatService.utledBehandlingsresultat(any()) } returns Behandlingsresultat.DELVIS_INNVILGET + + every { + behandlingService.oppdaterBehandlingsresultat( + any(), + any(), + ) + } returns behandling.copy(resultat = Behandlingsresultat.DELVIS_INNVILGET) + + val exception = assertThrows { behandlingsresultatSteg.utførStegOgAngiNeste(behandling, "") } + assertEquals( + "Du har fått behandlingsresultatet Delvis innvilget. " + + "Dette er ikke støttet på migreringsbehandlinger. " + + "Meld sak i Porten om du er uenig i resultatet.", + exception.message, + ) + } + + @Test + fun `skal kaste exception hvis behandlingsresultat er Avslått,Endret og Opphørt for en manuell migrering`() { + every { mockBehandlingsresultatService.utledBehandlingsresultat(any()) } returns Behandlingsresultat.AVSLÅTT_ENDRET_OG_OPPHØRT + + every { + behandlingService.oppdaterBehandlingsresultat( + any(), + any(), + ) + } returns behandling.copy(resultat = Behandlingsresultat.AVSLÅTT_ENDRET_OG_OPPHØRT) + + val exception = assertThrows { behandlingsresultatSteg.utførStegOgAngiNeste(behandling, "") } + assertEquals( + "Du har fått behandlingsresultatet Avslått, endret og opphørt. " + + "Dette er ikke støttet på migreringsbehandlinger. " + + "Meld sak i Porten om du er uenig i resultatet.", + exception.message, + ) + } + + @Test + fun `skal kaste feil om det er endring etter migreringsdatoen til første behandling`() { + val startdato = YearMonth.of(2023, 2) + val endringTidslinje = "TTTFFFF".tilBoolskTidslinje( + startdato, + ) + + assertThrows { + endringTidslinje.kastFeilVedEndringEtter(startdato, lagBehandling()) + } + } + + @Test + fun `skal ikke kaste feil om det ikke er endring etter migreringsdatoen til første behandling`() { + val startdato = YearMonth.of(2023, 2) + val treMånederEtterStartdato = startdato.plusMonths(3) + + val endringTidslinje = "TTTFFFF".tilBoolskTidslinje( + startdato, + ) + + assertDoesNotThrow { + endringTidslinje.kastFeilVedEndringEtter(treMånederEtterStartdato, lagBehandling()) + } + } + + @Test + fun `preValiderSteg - skal validere andeler ved satsendring og ikke kaste feil når endringene i andeler kun er relatert til endring i sats`() { + val søker = lagPerson() + val barn = lagPerson(type = PersonType.BARN) + val forrigeBehandling = + lagBehandling(behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, årsak = BehandlingÅrsak.SØKNAD) + + val forrigeTilkjentYtelse = lagInitiellTilkjentYtelse(behandling = forrigeBehandling) + forrigeTilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 2), + behandling = forrigeBehandling, + tilkjentYtelse = forrigeTilkjentYtelse, + beløp = SatsService.finnGjeldendeSatsForDato( + SatsType.ORBA, + YearMonth.of(2023, 1).førsteDagIInneværendeMåned(), + ), + aktør = barn.aktør, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 3), + tom = YearMonth.of(2033, 1), + behandling = forrigeBehandling, + tilkjentYtelse = forrigeTilkjentYtelse, + beløp = SatsService.finnGjeldendeSatsForDato( + SatsType.ORBA, + YearMonth.of(2023, 3).førsteDagIInneværendeMåned(), + ), + aktør = barn.aktør, + ), + ), + ) + + val behandling = lagBehandling( + fagsak = forrigeBehandling.fagsak, + behandlingType = BehandlingType.REVURDERING, + årsak = BehandlingÅrsak.SATSENDRING, + ) + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling = behandling) + tilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 2), + behandling = forrigeBehandling, + tilkjentYtelse = forrigeTilkjentYtelse, + beløp = SatsService.finnGjeldendeSatsForDato( + SatsType.ORBA, + YearMonth.of(2023, 1).førsteDagIInneværendeMåned(), + ), + aktør = barn.aktør, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 3), + tom = YearMonth.of(2023, 6), + behandling = forrigeBehandling, + tilkjentYtelse = forrigeTilkjentYtelse, + beløp = SatsService.finnGjeldendeSatsForDato( + SatsType.ORBA, + YearMonth.of(2023, 3).førsteDagIInneværendeMåned(), + ), + aktør = barn.aktør, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 7), + tom = YearMonth.of(2033, 1), + behandling = forrigeBehandling, + tilkjentYtelse = forrigeTilkjentYtelse, + beløp = SatsService.finnGjeldendeSatsForDato( + SatsType.ORBA, + YearMonth.of(2023, 7).førsteDagIInneværendeMåned(), + ), + aktør = barn.aktør, + ), + ), + ) + lagMocksForPreValiderStegSatsendring( + behandling = behandling, + tilkjentYtelse = tilkjentYtelse, + forrigeBehandling = forrigeBehandling, + forrigeTilkjentYtelse = forrigeTilkjentYtelse, + søker = søker, + barn = listOf(barn), + ) + + behandlingsresultatSteg.preValiderSteg(behandling) + + assertThatCode { behandlingsresultatSteg.preValiderSteg(behandling) }.doesNotThrowAnyException() + } + + @Test + fun `preValiderSteg - skal validere andeler ved satsendring og kaste feil når endringene i andeler er relatert til noe annet enn endring i sats`() { + val søker = lagPerson() + val barn = lagPerson() + val forrigeBehandling = + lagBehandling(behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, årsak = BehandlingÅrsak.SØKNAD) + val forrigeTilkjentYtelse = lagInitiellTilkjentYtelse(behandling = forrigeBehandling) + forrigeTilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 2), + behandling = forrigeBehandling, + tilkjentYtelse = forrigeTilkjentYtelse, + beløp = SatsService.finnGjeldendeSatsForDato( + SatsType.ORBA, + YearMonth.of(2023, 1).førsteDagIInneværendeMåned(), + ), + aktør = barn.aktør, + prosent = BigDecimal(50), + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 3), + tom = YearMonth.of(2033, 1), + behandling = forrigeBehandling, + tilkjentYtelse = forrigeTilkjentYtelse, + beløp = SatsService.finnGjeldendeSatsForDato( + SatsType.ORBA, + YearMonth.of(2023, 3).førsteDagIInneværendeMåned(), + ), + aktør = barn.aktør, + prosent = BigDecimal(50), + ), + ), + ) + + val behandling = lagBehandling( + fagsak = forrigeBehandling.fagsak, + behandlingType = BehandlingType.REVURDERING, + årsak = BehandlingÅrsak.SATSENDRING, + ) + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling = behandling) + tilkjentYtelse.andelerTilkjentYtelse.addAll( + mutableSetOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 2), + behandling = forrigeBehandling, + tilkjentYtelse = forrigeTilkjentYtelse, + beløp = SatsService.finnGjeldendeSatsForDato( + SatsType.ORBA, + YearMonth.of(2023, 1).førsteDagIInneværendeMåned(), + ), + aktør = barn.aktør, + prosent = BigDecimal(50), + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 3), + tom = YearMonth.of(2023, 6), + behandling = forrigeBehandling, + tilkjentYtelse = forrigeTilkjentYtelse, + beløp = SatsService.finnGjeldendeSatsForDato( + SatsType.ORBA, + YearMonth.of(2023, 3).førsteDagIInneværendeMåned(), + ), + aktør = barn.aktør, + prosent = BigDecimal(100), + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 7), + tom = YearMonth.of(2033, 1), + behandling = forrigeBehandling, + tilkjentYtelse = forrigeTilkjentYtelse, + beløp = SatsService.finnGjeldendeSatsForDato( + SatsType.ORBA, + YearMonth.of(2023, 7).førsteDagIInneværendeMåned(), + ), + aktør = barn.aktør, + prosent = BigDecimal(50), + ), + ), + ) + lagMocksForPreValiderStegSatsendring( + behandling = behandling, + tilkjentYtelse = tilkjentYtelse, + forrigeBehandling = forrigeBehandling, + forrigeTilkjentYtelse = forrigeTilkjentYtelse, + søker = søker, + barn = listOf(barn), + ) + + assertThatThrownBy { behandlingsresultatSteg.preValiderSteg(behandling) }.isInstanceOf(Feil::class.java) + .hasMessage("Satsendring kan ikke endre på prosenten til en andel") + } + + private fun lagMocksForPreValiderStegSatsendring( + behandling: Behandling, + tilkjentYtelse: TilkjentYtelse, + forrigeBehandling: Behandling, + forrigeTilkjentYtelse: TilkjentYtelse, + søker: Person, + barn: List, + ) { + val personopplysningGrunnlag = mockk() + val vikårsvurderings = + lagVilkårsvurdering(søkerAktør = søker.aktør, behandling = behandling, resultat = Resultat.OPPFYLT) + every { vilkårService.hentVilkårsvurderingThrows(behandling.id) } returns vikårsvurderings + every { vilkårService.hentVilkårsvurdering(behandling.id) } returns vikårsvurderings + every { persongrunnlagService.hentBarna(any()) } returns emptyList() + every { beregningService.hentTilkjentYtelseForBehandling(behandling.id) } returns tilkjentYtelse + + every { persongrunnlagService.hentSøkerOgBarnPåBehandlingThrows(any()) } returns barn.map { it.tilPersonEnkel() } + søker.tilPersonEnkel() + + every { persongrunnlagService.hentAktivThrows(any()) } returns personopplysningGrunnlag + every { personopplysningGrunnlag.søker } returns søker + every { personopplysningGrunnlag.barna } returns barn + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling) } returns forrigeBehandling + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(forrigeBehandling.id) } returns forrigeTilkjentYtelse.andelerTilkjentYtelse.toList() + every { + andelerTilkjentYtelseOgEndreteUtbetalingerService + .finnEndreteUtbetalingerMedAndelerTilkjentYtelse(behandling.id) + } returns emptyList() + + every { beregningService.hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomiTidslinje(behandling) } returns EndringIUtbetalingUtil.lagEndringIUtbetalingTidslinje( + nåværendeAndeler = tilkjentYtelse.andelerTilkjentYtelse.toList(), + forrigeAndeler = forrigeTilkjentYtelse.andelerTilkjentYtelse.toList(), + ) + + every { + beregningService + .hentAndelerFraForrigeIverksattebehandling(behandling) + } returns emptyList() + } + + fun `skal gå rett fra behandlingsresultat til iverksetting for alle fødselshendelser`() { + val fødselshendelseBehandling = behandling.copy( + skalBehandlesAutomatisk = true, + opprettetÅrsak = BehandlingÅrsak.FØDSELSHENDELSE, + type = BehandlingType.FØRSTEGANGSBEHANDLING, + ) + val vedtak = lagVedtak( + fødselshendelseBehandling, + ) + every { mockBehandlingsresultatService.utledBehandlingsresultat(any()) } returns Behandlingsresultat.INNVILGET_OG_ENDRET + every { behandlingService.nullstillEndringstidspunkt(fødselshendelseBehandling.id) } just runs + every { + behandlingService.oppdaterBehandlingsresultat( + any(), + any(), + ) + } returns fødselshendelseBehandling.copy(resultat = Behandlingsresultat.INNVILGET_OG_ENDRET) + every { + behandlingService.oppdaterStatusPåBehandling( + fødselshendelseBehandling.id, + BehandlingStatus.IVERKSETTER_VEDTAK, + ) + } returns fødselshendelseBehandling.copy(status = BehandlingStatus.IVERKSETTER_VEDTAK) + every { vedtakService.hentAktivForBehandlingThrows(fødselshendelseBehandling.id) } returns vedtak + every { vedtaksperiodeService.oppdaterVedtakMedVedtaksperioder(vedtak) } just runs + every { beregningService.hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(fødselshendelseBehandling) } returns EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING + + assertEquals( + behandlingsresultatSteg.utførStegOgAngiNeste(fødselshendelseBehandling, ""), + StegType.IVERKSETT_MOT_OPPDRAG, + ) + } + + fun String.tilBoolskTidslinje(startdato: YearMonth): Tidslinje { + return tidslinje { + this.mapIndexed { index, it -> + Periode( + startdato.plusMonths(index.toLong()).tilTidspunkt(), + startdato.plusMonths(index.toLong()).tilTidspunkt(), + when (it) { + 'T' -> true + 'F' -> false + else -> throw Feil("Klarer ikke å konvertere \"$it\" til Boolean") + }, + ) + } + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatS\303\270knadUtilsTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatS\303\270knadUtilsTest.kt" new file mode 100644 index 000000000..9f4ee276e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatS\303\270knadUtilsTest.kt" @@ -0,0 +1,661 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndel +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatSøknadUtils.kombinerSøknadsresultater +import no.nav.familie.ba.sak.kjerne.behandlingsresultat.BehandlingsresultatSøknadUtils.utledSøknadResultatFraAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.containsInAnyOrder +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth +import org.hamcrest.CoreMatchers.`is` as Is + +internal class BehandlingsresultatSøknadUtilsTest { + + val søker = tilfeldigPerson() + + val des21 = LocalDate.of(2021, 12, 1) + val jan22 = YearMonth.of(2022, 1) + val aug22 = YearMonth.of(2022, 8) + + @Test + fun `utledSøknadResultatFraAndelerTilkjentYtelse skal bare utlede resultater for personer det er framstilt krav for`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndel = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ) + + val søknadsResultat = utledSøknadResultatFraAndelerTilkjentYtelse( + forrigeAndeler = listOf(forrigeAndel), + nåværendeAndeler = listOf(forrigeAndel.copy()), + personerFremstiltKravFor = emptyList(), + endretUtbetalingAndeler = emptyList(), + ) + + assertThat(søknadsResultat, Is(emptyList())) + } + + @Test + fun `utledSøknadResultatFraAndelerTilkjentYtelse skal returnere ingen relevante endringer dersom beløpene for periodene er lik forrige behandling`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndel = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ) + + val søknadsResultat = utledSøknadResultatFraAndelerTilkjentYtelse( + forrigeAndeler = listOf(forrigeAndel), + nåværendeAndeler = listOf(forrigeAndel.copy()), + personerFremstiltKravFor = listOf(barn1Aktør), + endretUtbetalingAndeler = emptyList(), + ) + + assertThat(søknadsResultat.size, Is(1)) + assertThat(søknadsResultat[0], Is(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER)) + } + + @Test + fun `utledSøknadResultatFraAndelerTilkjentYtelse skal returnere innvilget dersom det finnes beløp for perioder som er annerledes enn sist og større enn 0`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndel = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 0, + aktør = barn1Aktør, + ) + + val søknadsResultat = utledSøknadResultatFraAndelerTilkjentYtelse( + forrigeAndeler = listOf(forrigeAndel), + nåværendeAndeler = listOf( + forrigeAndel.copy(kalkulertUtbetalingsbeløp = 1054), + ), + personerFremstiltKravFor = listOf(barn1Aktør), + endretUtbetalingAndeler = emptyList(), + ) + + assertThat(søknadsResultat.size, Is(1)) + assertThat(søknadsResultat[0], Is(Søknadsresultat.INNVILGET)) + } + + @Test + fun `utledSøknadResultatFraAndelerTilkjentYtelse skal returnere ingen relevante endringer dersom beløp på nåværende andel er 0 og det ikke finnes noen endringsperioder eller differanse beregning`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndel = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ) + + val søknadsResultat = utledSøknadResultatFraAndelerTilkjentYtelse( + forrigeAndeler = listOf(forrigeAndel), + nåværendeAndeler = listOf( + forrigeAndel.copy(kalkulertUtbetalingsbeløp = 0), + ), + personerFremstiltKravFor = listOf(barn1Aktør), + endretUtbetalingAndeler = emptyList(), + ) + + assertThat(søknadsResultat.size, Is(1)) + assertThat(søknadsResultat[0], Is(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER)) + } + + @Test + fun `utledSøknadResultatFraAndelerTilkjentYtelse skal returnere INNVILGET dersom beløp på nåværende andel er 0 og det finnes endringsperiode som DELT_BOSTED`() { + val barn1Person = lagPerson(type = PersonType.BARN) + val barn1Aktør = barn1Person.aktør + + val andel = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 0, + prosent = BigDecimal.ZERO, + aktør = barn1Aktør, + ) + + val endretUtbetalingAndel = lagEndretUtbetalingAndel( + person = barn1Person, + fom = jan22, + tom = aug22, + prosent = BigDecimal(100), + behandlingId = 123L, + årsak = Årsak.DELT_BOSTED, + ) + + val søknadsResultat = utledSøknadResultatFraAndelerTilkjentYtelse( + forrigeAndeler = emptyList(), + nåværendeAndeler = listOf( + andel.copy(kalkulertUtbetalingsbeløp = 0), + ), + personerFremstiltKravFor = listOf(barn1Aktør), + endretUtbetalingAndeler = listOf(endretUtbetalingAndel), + ) + + assertThat(søknadsResultat.size, Is(1)) + assertThat(søknadsResultat[0], Is(Søknadsresultat.INNVILGET)) + } + + @ParameterizedTest + @EnumSource(value = Årsak::class, mode = EnumSource.Mode.EXCLUDE, names = ["DELT_BOSTED"]) + fun `utledSøknadResultatFraAndelerTilkjentYtelse skal returnere AVSLÅTT dersom beløp på nåværende andel er 0 og det finnes endringsperiode som ikke er DELT_BOSTED`( + årsak: Årsak, + ) { + val barn1Person = lagPerson(type = PersonType.BARN) + val barn1Aktør = barn1Person.aktør + + val andel = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 0, + prosent = BigDecimal.ZERO, + aktør = barn1Aktør, + ) + + val endretUtbetalingAndel = lagEndretUtbetalingAndel( + person = barn1Person, + fom = jan22, + tom = aug22, + prosent = BigDecimal(100), + behandlingId = 123L, + årsak = årsak, + ) + + val søknadsResultat = utledSøknadResultatFraAndelerTilkjentYtelse( + forrigeAndeler = emptyList(), + nåværendeAndeler = listOf( + andel, + ), + personerFremstiltKravFor = listOf(barn1Aktør), + endretUtbetalingAndeler = listOf(endretUtbetalingAndel), + ) + + assertThat(søknadsResultat.size, Is(1)) + assertThat(søknadsResultat[0], Is(Søknadsresultat.AVSLÅTT)) + } + + @ParameterizedTest + @EnumSource(value = Årsak::class) + fun `utledSøknadResultatFraAndelerTilkjentYtelse skal returnere INGEN_RELEVANTE_ENDRINGER dersom beløp på nåværende andel er 0 og andelen eksisterte forrige gang (beløp større eller lik 0)`( + årsak: Årsak, + ) { + val barn1Person = lagPerson(type = PersonType.BARN) + val barn1Aktør = barn1Person.aktør + + val forrigeAndel = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ) + + val endretUtbetalingAndel = lagEndretUtbetalingAndel( + person = barn1Person, + fom = jan22, + tom = aug22, + prosent = BigDecimal(100), + behandlingId = 123L, + årsak = årsak, + ) + + val søknadsResultat = utledSøknadResultatFraAndelerTilkjentYtelse( + forrigeAndeler = listOf(forrigeAndel), + nåværendeAndeler = listOf( + forrigeAndel.copy(kalkulertUtbetalingsbeløp = 0), + ), + personerFremstiltKravFor = listOf(barn1Aktør), + endretUtbetalingAndeler = listOf(endretUtbetalingAndel), + ) + + assertThat(søknadsResultat.size, Is(1)) + assertThat(søknadsResultat[0], Is(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER)) + } + + @Test + fun `utledSøknadResultatFraAndelerTilkjentYtelse skal returnere INNVILGET dersom beløpet på nåværende andel er 0 men er differanseberegnet`() { + val barn1Person = lagPerson(type = PersonType.BARN) + val barn1Aktør = barn1Person.aktør + + val andel = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 0, + prosent = BigDecimal.ZERO, + differanseberegnetPeriodebeløp = 0, + aktør = barn1Aktør, + ) + + val søknadsResultat = utledSøknadResultatFraAndelerTilkjentYtelse( + forrigeAndeler = emptyList(), + nåværendeAndeler = listOf( + andel.copy( + kalkulertUtbetalingsbeløp = 0, + differanseberegnetPeriodebeløp = 0, + ), + ), + personerFremstiltKravFor = listOf(barn1Aktør), + endretUtbetalingAndeler = emptyList(), + ) + + assertThat(søknadsResultat.size, Is(1)) + assertThat(søknadsResultat[0], Is(Søknadsresultat.INNVILGET)) + } + + @Test + fun `utledSøknadResultatFraAndelerTilkjentYtelse skal returnere INNVILGET OG AVSLÅTT dersom 1 barn får innvilget og 1 barn får avslått`() { + val barn1Person = lagPerson(type = PersonType.BARN) + val barn1Aktør = barn1Person.aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 0, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1060, + aktør = barn2Aktør, + ), + ) + + val endretUtbetalingAndel = lagEndretUtbetalingAndel( + person = barn1Person, + fom = jan22, + tom = aug22, + prosent = BigDecimal(100), + behandlingId = 123L, + årsak = Årsak.ALLEREDE_UTBETALT, + ) + + val søknadsResultat = utledSøknadResultatFraAndelerTilkjentYtelse( + forrigeAndeler = forrigeAndeler, + nåværendeAndeler = nåværendeAndeler, + personerFremstiltKravFor = listOf(barn1Aktør, barn2Aktør), + endretUtbetalingAndeler = listOf(endretUtbetalingAndel), + ) + + assertThat(søknadsResultat.size, Is(2)) + assertThat( + søknadsResultat, + containsInAnyOrder( + Søknadsresultat.AVSLÅTT, + Søknadsresultat.INNVILGET, + ), + ) + } + + @Test + fun `utledSøknadResultatFraAndelerTilkjentYtelse skal returnere INNVILGET dersom småbarnstillegg blir lagt til`() { + val barn1Person = lagPerson(type = PersonType.BARN) + val barn1Aktør = barn1Person.aktør + val søker = lagPerson(type = PersonType.SØKER) + + val forrigeAndelBarn = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ) + + val forrigeAndelUtvidet = lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = søker.aktør, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ) + + val søknadsResultat = utledSøknadResultatFraAndelerTilkjentYtelse( + forrigeAndeler = listOf(forrigeAndelBarn, forrigeAndelUtvidet), + nåværendeAndeler = listOf( + forrigeAndelBarn, + forrigeAndelUtvidet, + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 630, + aktør = søker.aktør, + ytelseType = YtelseType.SMÅBARNSTILLEGG, + ), + ), + personerFremstiltKravFor = listOf(søker.aktør), + endretUtbetalingAndeler = emptyList(), + ).filter { it != Søknadsresultat.INGEN_RELEVANTE_ENDRINGER } + + assertThat(søknadsResultat.size, Is(1)) + assertThat(søknadsResultat[0], Is(Søknadsresultat.INNVILGET)) + } + + @Test + fun `kombinerSøknadsresultater skal kaste feil dersom lista ikke inneholder noe som helst`() { + val listeMedIngenSøknadsresultat = listOf() + + val feil = assertThrows { listeMedIngenSøknadsresultat.kombinerSøknadsresultater(behandlingÅrsak = BehandlingÅrsak.SØKNAD) } + + assertThat(feil.message, Is("Klarer ikke utlede søknadsresultat. Finner ingen resultater.")) + } + + @ParameterizedTest + @EnumSource(value = Søknadsresultat::class) + internal fun `kombinerSøknadsresultater skal alltid returnere innholdet som det er hvis det bare 1 resultat i lista`( + søknadsresultat: Søknadsresultat, + ) { + val listeMedSøknadsresultat = listOf(søknadsresultat) + + val kombinertResultat = listeMedSøknadsresultat.kombinerSøknadsresultater(behandlingÅrsak = BehandlingÅrsak.SØKNAD) + + assertThat(kombinertResultat, Is(søknadsresultat)) + } + + @ParameterizedTest + @EnumSource(value = Søknadsresultat::class, names = ["INNVILGET", "AVSLÅTT"]) + internal fun `kombinerSøknadsresultater skal ignorere INGEN_RELEVANTE_ENDRINGER dersom den er paret opp med INNVILGET eller AVSLÅTT`( + søknadsresultat: Søknadsresultat, + ) { + val listeMedSøknadsresultat = + listOf(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, søknadsresultat) + + val kombinertResultat = listeMedSøknadsresultat.kombinerSøknadsresultater(behandlingÅrsak = BehandlingÅrsak.SØKNAD) + + assertThat(kombinertResultat, Is(søknadsresultat)) + } + + @Test + fun `kombinerSøknadsresultater skal returnere DELVIS_INNVILGET dersom lista består av INNVILGET, AVSLÅTT OG INGEN_RELEVANTE_ENDRINGER`() { + val listeMedSøknadsresultat = listOf( + Søknadsresultat.INNVILGET, + Søknadsresultat.AVSLÅTT, + Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, + ) + + val kombinertResultat = listeMedSøknadsresultat.kombinerSøknadsresultater(behandlingÅrsak = BehandlingÅrsak.SØKNAD) + + assertThat(kombinertResultat, Is(Søknadsresultat.DELVIS_INNVILGET)) + } + + @Test + fun `utledResultatPåSøknad - skal kaste feil dersom man har endt opp med ingen resultater`() { + assertThrows { + BehandlingsresultatSøknadUtils.utledResultatPåSøknad( + forrigeAndeler = emptyList(), + nåværendeAndeler = emptyList(), + nåværendePersonResultater = emptySet(), + personerFremstiltKravFor = emptyList(), + endretUtbetalingAndeler = emptyList(), + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + finnesUregistrerteBarn = false, + ) + } + } + + @Test + fun `utledResultatPåSøknad - skal returnere AVSLÅTT dersom det er søkt for barn som ikke er registrert`() { + val resultatPåSøknad = BehandlingsresultatSøknadUtils.utledResultatPåSøknad( + forrigeAndeler = emptyList(), + nåværendeAndeler = emptyList(), + nåværendePersonResultater = emptySet(), + personerFremstiltKravFor = emptyList(), + endretUtbetalingAndeler = emptyList(), + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + finnesUregistrerteBarn = true, + ) + + assertThat(resultatPåSøknad, Is(Søknadsresultat.AVSLÅTT)) + } + + @Test + fun `utledResultatPåSøknad - skal returnere AVSLÅTT dersom det er eksplisitt avslag på søker (uten at det er søkt om utvidet)`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.SØKNAD) + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + + val søker = lagPerson(type = PersonType.SØKER) + + val søkersPersonResultat = lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = søker, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = des21, + periodeTom = LocalDate.now(), + personType = PersonType.SØKER, + erEksplisittAvslagPåSøknad = true, + lagFullstendigVilkårResultat = true, + + ) + val resultatPåSøknad = BehandlingsresultatSøknadUtils.utledResultatPåSøknad( + forrigeAndeler = emptyList(), + nåværendeAndeler = emptyList(), + nåværendePersonResultater = setOf(søkersPersonResultat), + personerFremstiltKravFor = emptyList(), + endretUtbetalingAndeler = emptyList(), + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + finnesUregistrerteBarn = false, + ) + + assertThat(resultatPåSøknad, Is(Søknadsresultat.AVSLÅTT)) + } + + @ParameterizedTest + @EnumSource(value = Resultat::class, names = ["IKKE_OPPFYLT", "IKKE_VURDERT"]) + fun `utledResultatPåSøknad - skal returnere AVSLÅTT dersom behandlingen er en fødselshendelse og det finnes vilkårsvurdering som ikke er oppfylt eller vurdert`(resultat: Resultat) { + val behandling = lagBehandling(årsak = BehandlingÅrsak.FØDSELSHENDELSE) + val vikårsvurdering = Vilkårsvurdering(behandling = behandling) + + val barnPersonResultat = lagPersonResultat( + vilkårsvurdering = vikårsvurdering, + person = lagPerson(type = PersonType.BARN, fødselsdato = des21), + resultat = resultat, + periodeFom = des21, + periodeTom = LocalDate.now(), + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + ) + + val resultatPåSøknad = BehandlingsresultatSøknadUtils.utledResultatPåSøknad( + forrigeAndeler = emptyList(), + nåværendeAndeler = emptyList(), + nåværendePersonResultater = setOf(barnPersonResultat), + personerFremstiltKravFor = emptyList(), + endretUtbetalingAndeler = emptyList(), + behandlingÅrsak = BehandlingÅrsak.FØDSELSHENDELSE, + finnesUregistrerteBarn = false, + ) + + assertThat(resultatPåSøknad, Is(Søknadsresultat.AVSLÅTT)) + } + + @Test + fun `utledResultatPåSøknad - skal returnere AVSLÅTT dersom er eksplisitt avslag på minst en person det er framstilt krav for`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.SØKNAD) + val vikårsvurdering = Vilkårsvurdering(behandling = behandling) + + val barn = lagPerson(type = PersonType.BARN, fødselsdato = des21) + + val barnPersonResultat = lagPersonResultat( + vilkårsvurdering = vikårsvurdering, + person = barn, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = des21, + periodeTom = LocalDate.now(), + personType = PersonType.BARN, + erEksplisittAvslagPåSøknad = true, + lagFullstendigVilkårResultat = true, + + ) + + val resultatPåSøknad = BehandlingsresultatSøknadUtils.utledResultatPåSøknad( + forrigeAndeler = emptyList(), + nåværendeAndeler = emptyList(), + nåværendePersonResultater = setOf(barnPersonResultat), + personerFremstiltKravFor = listOf(barn.aktør), + endretUtbetalingAndeler = emptyList(), + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + finnesUregistrerteBarn = false, + ) + + assertThat(resultatPåSøknad, Is(Søknadsresultat.AVSLÅTT)) + } + + @Test + fun `utledResultatPåSøknad - skal returnere INNVILGET dersom barnet det er søkt for har fått andeler med positive beløp som er annerledes enn forrige gang`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.FØDSELSHENDELSE) + val vikårsvurdering = Vilkårsvurdering(behandling = behandling) + + val barn1Person = lagPerson(type = PersonType.BARN, fødselsdato = des21) + + val nåværendeAndeler = + listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Person.aktør, + ), + ) + + val barnPersonResultat = lagPersonResultat( + vilkårsvurdering = vikårsvurdering, + person = barn1Person, + resultat = Resultat.OPPFYLT, + periodeFom = des21, + periodeTom = LocalDate.now(), + personType = PersonType.BARN, + lagFullstendigVilkårResultat = true, + ) + + val resultatPåSøknad = BehandlingsresultatSøknadUtils.utledResultatPåSøknad( + forrigeAndeler = emptyList(), + nåværendeAndeler = nåværendeAndeler, + nåværendePersonResultater = setOf(barnPersonResultat), + personerFremstiltKravFor = listOf(barn1Person.aktør), + endretUtbetalingAndeler = emptyList(), + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + finnesUregistrerteBarn = false, + ) + + assertThat(resultatPåSøknad, Is(Søknadsresultat.INNVILGET)) + } + + @Test + fun `utledResultatPåSøknad - skal returnere DELVIS_INNVILGET dersom det finnes et barn som har fått innvilget men også et barn som ikke er registrert`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.FØDSELSHENDELSE) + val vikårsvurdering = Vilkårsvurdering(behandling = behandling) + + val barn1Person = lagPerson(type = PersonType.BARN) + + val nåværendeAndeler = + listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Person.aktør, + ), + ) + + val barnPersonResultat = lagPersonResultat( + vilkårsvurdering = vikårsvurdering, + person = barn1Person, + resultat = Resultat.OPPFYLT, + periodeFom = des21, + periodeTom = LocalDate.now(), + personType = PersonType.BARN, + lagFullstendigVilkårResultat = true, + ) + + val resultatPåSøknad = BehandlingsresultatSøknadUtils.utledResultatPåSøknad( + forrigeAndeler = emptyList(), + nåværendeAndeler = nåværendeAndeler, + nåværendePersonResultater = setOf(barnPersonResultat), + personerFremstiltKravFor = listOf(barn1Person.aktør), + endretUtbetalingAndeler = emptyList(), + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + finnesUregistrerteBarn = true, + ) + + assertThat(resultatPåSøknad, Is(Søknadsresultat.DELVIS_INNVILGET)) + } + + @Test + fun `utledResultatPåSøknad - skal returnere INGEN_RELEVANTE_ENDRINGER dersom barnet det er søkt for har fått helt lik andel som forrige behandling`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.FØDSELSHENDELSE) + val vikårsvurdering = Vilkårsvurdering(behandling = behandling) + + val barn1Person = lagPerson(type = PersonType.BARN, fødselsdato = des21) + + val andeler = + listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Person.aktør, + ), + ) + + val barnPersonResultat = lagPersonResultat( + vilkårsvurdering = vikårsvurdering, + person = barn1Person, + resultat = Resultat.OPPFYLT, + periodeFom = des21, + periodeTom = LocalDate.now(), + personType = PersonType.BARN, + lagFullstendigVilkårResultat = true, + ) + + val resultatPåSøknad = BehandlingsresultatSøknadUtils.utledResultatPåSøknad( + forrigeAndeler = andeler, + nåværendeAndeler = andeler, + nåværendePersonResultater = setOf(barnPersonResultat), + personerFremstiltKravFor = listOf(barn1Person.aktør), + endretUtbetalingAndeler = emptyList(), + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + finnesUregistrerteBarn = false, + ) + + assertThat(resultatPåSøknad, Is(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatUtilsTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatUtilsTest.kt new file mode 100644 index 000000000..dccabcd29 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatUtilsTest.kt @@ -0,0 +1,99 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import io.mockk.clearStaticMockk +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.time.YearMonth +import java.util.stream.Stream + +class BehandlingsresultatUtilsTest { + + val søker = tilfeldigPerson() + + @BeforeEach + fun reset() { + clearStaticMockk(YearMonth::class) + } + + @ParameterizedTest(name = "Søknadsresultat {0}, Endringsresultat {1} og Opphørsresultat {2} skal kombineres til behandlingsresultat {3}") + @MethodSource("hentKombinasjonerOgBehandlingsResultat") + internal fun `Kombiner resultater - skal kombinere til riktig behandlingsresultat gitt forskjellige kombinasjoner av resultater`( + søknadsresultat: Søknadsresultat?, + endringsresultat: Endringsresultat, + opphørsresultat: Opphørsresultat, + behandlingsresultat: Behandlingsresultat, + ) { + val kombinertResultat = BehandlingsresultatUtils.kombinerResultaterTilBehandlingsresultat( + søknadsresultat, + endringsresultat, + opphørsresultat, + ) + + assertEquals(kombinertResultat, behandlingsresultat) + } + + @ParameterizedTest(name = "Søknadsresultat {0}, Endringsresultat {1} og Opphørsresultat {2} skal kaste feil") + @MethodSource("hentUgyldigeKombinasjoner") + internal fun `Kombiner resultater - skal kaste feil ved ugyldige kombinasjoner av resultat`( + søknadsresultat: Søknadsresultat?, + endringsresultat: Endringsresultat, + opphørsresultat: Opphørsresultat, + ) { + assertThrows { + BehandlingsresultatUtils.kombinerResultaterTilBehandlingsresultat( + søknadsresultat, + endringsresultat, + opphørsresultat, + ) + } + } + + companion object { + @JvmStatic + fun hentKombinasjonerOgBehandlingsResultat() = + Stream.of( + Arguments.of(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, Endringsresultat.INGEN_ENDRING, Opphørsresultat.IKKE_OPPHØRT, Behandlingsresultat.FORTSATT_INNVILGET), + Arguments.of(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, Endringsresultat.ENDRING, Opphørsresultat.IKKE_OPPHØRT, Behandlingsresultat.ENDRET_OG_FORTSATT_INNVILGET), + Arguments.of(Søknadsresultat.INNVILGET, Endringsresultat.ENDRING, Opphørsresultat.OPPHØRT, Behandlingsresultat.INNVILGET_ENDRET_OG_OPPHØRT), + Arguments.of(Søknadsresultat.INNVILGET, Endringsresultat.ENDRING, Opphørsresultat.FORTSATT_OPPHØRT, Behandlingsresultat.INNVILGET_OG_ENDRET), + Arguments.of(Søknadsresultat.INNVILGET, Endringsresultat.ENDRING, Opphørsresultat.IKKE_OPPHØRT, Behandlingsresultat.INNVILGET_OG_ENDRET), + Arguments.of(Søknadsresultat.INNVILGET, Endringsresultat.INGEN_ENDRING, Opphørsresultat.OPPHØRT, Behandlingsresultat.INNVILGET_OG_OPPHØRT), + Arguments.of(Søknadsresultat.INNVILGET, Endringsresultat.INGEN_ENDRING, Opphørsresultat.FORTSATT_OPPHØRT, Behandlingsresultat.INNVILGET), + Arguments.of(Søknadsresultat.INNVILGET, Endringsresultat.INGEN_ENDRING, Opphørsresultat.IKKE_OPPHØRT, Behandlingsresultat.INNVILGET), + Arguments.of(Søknadsresultat.AVSLÅTT, Endringsresultat.ENDRING, Opphørsresultat.OPPHØRT, Behandlingsresultat.AVSLÅTT_ENDRET_OG_OPPHØRT), + Arguments.of(Søknadsresultat.AVSLÅTT, Endringsresultat.ENDRING, Opphørsresultat.FORTSATT_OPPHØRT, Behandlingsresultat.AVSLÅTT_OG_ENDRET), + Arguments.of(Søknadsresultat.AVSLÅTT, Endringsresultat.ENDRING, Opphørsresultat.IKKE_OPPHØRT, Behandlingsresultat.AVSLÅTT_OG_ENDRET), + Arguments.of(Søknadsresultat.AVSLÅTT, Endringsresultat.INGEN_ENDRING, Opphørsresultat.OPPHØRT, Behandlingsresultat.AVSLÅTT_OG_OPPHØRT), + Arguments.of(Søknadsresultat.AVSLÅTT, Endringsresultat.INGEN_ENDRING, Opphørsresultat.FORTSATT_OPPHØRT, Behandlingsresultat.AVSLÅTT), + Arguments.of(Søknadsresultat.AVSLÅTT, Endringsresultat.INGEN_ENDRING, Opphørsresultat.IKKE_OPPHØRT, Behandlingsresultat.AVSLÅTT), + Arguments.of(Søknadsresultat.DELVIS_INNVILGET, Endringsresultat.ENDRING, Opphørsresultat.OPPHØRT, Behandlingsresultat.DELVIS_INNVILGET_ENDRET_OG_OPPHØRT), + Arguments.of(Søknadsresultat.DELVIS_INNVILGET, Endringsresultat.ENDRING, Opphørsresultat.FORTSATT_OPPHØRT, Behandlingsresultat.DELVIS_INNVILGET_OG_ENDRET), + Arguments.of(Søknadsresultat.DELVIS_INNVILGET, Endringsresultat.ENDRING, Opphørsresultat.IKKE_OPPHØRT, Behandlingsresultat.DELVIS_INNVILGET_OG_ENDRET), + Arguments.of(Søknadsresultat.DELVIS_INNVILGET, Endringsresultat.INGEN_ENDRING, Opphørsresultat.OPPHØRT, Behandlingsresultat.DELVIS_INNVILGET_OG_OPPHØRT), + Arguments.of(Søknadsresultat.DELVIS_INNVILGET, Endringsresultat.INGEN_ENDRING, Opphørsresultat.FORTSATT_OPPHØRT, Behandlingsresultat.DELVIS_INNVILGET), + Arguments.of(Søknadsresultat.DELVIS_INNVILGET, Endringsresultat.INGEN_ENDRING, Opphørsresultat.IKKE_OPPHØRT, Behandlingsresultat.DELVIS_INNVILGET), + Arguments.of(null, Endringsresultat.ENDRING, Opphørsresultat.OPPHØRT, Behandlingsresultat.ENDRET_OG_OPPHØRT), + Arguments.of(null, Endringsresultat.ENDRING, Opphørsresultat.FORTSATT_OPPHØRT, Behandlingsresultat.ENDRET_UTBETALING), + Arguments.of(null, Endringsresultat.ENDRING, Opphørsresultat.IKKE_OPPHØRT, Behandlingsresultat.ENDRET_UTBETALING), + Arguments.of(null, Endringsresultat.INGEN_ENDRING, Opphørsresultat.OPPHØRT, Behandlingsresultat.OPPHØRT), + Arguments.of(null, Endringsresultat.INGEN_ENDRING, Opphørsresultat.FORTSATT_OPPHØRT, Behandlingsresultat.FORTSATT_OPPHØRT), + Arguments.of(null, Endringsresultat.INGEN_ENDRING, Opphørsresultat.IKKE_OPPHØRT, Behandlingsresultat.FORTSATT_INNVILGET), + ) + + @JvmStatic + fun hentUgyldigeKombinasjoner() = + Stream.of( + Arguments.of(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, Endringsresultat.INGEN_ENDRING, Opphørsresultat.OPPHØRT), + Arguments.of(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, Endringsresultat.INGEN_ENDRING, Opphørsresultat.FORTSATT_OPPHØRT), + Arguments.of(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, Endringsresultat.ENDRING, Opphørsresultat.OPPHØRT), + Arguments.of(Søknadsresultat.INGEN_RELEVANTE_ENDRINGER, Endringsresultat.ENDRING, Opphørsresultat.FORTSATT_OPPHØRT), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatValideringUtilsTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatValideringUtilsTest.kt new file mode 100644 index 000000000..c5bb0d941 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/behandlingsresultat/BehandlingsresultatValideringUtilsTest.kt @@ -0,0 +1,114 @@ +package no.nav.familie.ba.sak.kjerne.behandlingsresultat + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import java.time.LocalDate + +internal class BehandlingsresultatValideringUtilsTest { + + @Test + fun `Valider eksplisitt avlag - Skal kaste feil hvis eksplisitt avslått for barn det ikke er fremstilt krav for`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.SØKNAD) + val vikårsvurdering = Vilkårsvurdering(behandling = behandling) + val barn1 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(5)) + val barn2 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(7)) + + val barn1PersonResultat = lagPersonResultat( + vilkårsvurdering = vikårsvurdering, + person = barn1, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.now().minusMonths(5), + periodeTom = LocalDate.now(), + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + erEksplisittAvslagPåSøknad = true, + ) + val barn2PersonResultat = lagPersonResultat( + vilkårsvurdering = vikårsvurdering, + person = barn2, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.now().minusMonths(5), + periodeTom = LocalDate.now(), + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + erEksplisittAvslagPåSøknad = true, + ) + + assertThrows { + BehandlingsresultatValideringUtils.validerAtBarePersonerFremstiltKravForEllerSøkerHarFåttEksplisittAvslag( + personResultater = setOf(barn1PersonResultat, barn2PersonResultat), + personerFremstiltKravFor = listOf(barn2.aktør), + ) + } + } + + @Test + fun `Valider eksplisitt avslag - Skal ikke kaste feil hvis søker er eksplisitt avslått`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.SØKNAD) + val vikårsvurdering = Vilkårsvurdering(behandling = behandling) + val søker = lagPerson(type = PersonType.SØKER) + + val søkerPersonResultat = lagPersonResultat( + vilkårsvurdering = vikårsvurdering, + person = søker, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.now().minusMonths(5), + periodeTom = LocalDate.now(), + lagFullstendigVilkårResultat = true, + personType = PersonType.SØKER, + erEksplisittAvslagPåSøknad = true, + ) + + assertDoesNotThrow { + BehandlingsresultatValideringUtils.validerAtBarePersonerFremstiltKravForEllerSøkerHarFåttEksplisittAvslag( + personResultater = setOf(søkerPersonResultat), + personerFremstiltKravFor = emptyList(), + ) + } + } + + @Test + fun `Valider eksplisitt avslag - Skal ikke kaste feil hvis person med eksplsitt avslag er fremstilt krav for`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.SØKNAD) + val vikårsvurdering = Vilkårsvurdering(behandling = behandling) + val barn1 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(5)) + val barn2 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(7)) + + val barn1PersonResultat = lagPersonResultat( + vilkårsvurdering = vikårsvurdering, + person = barn1, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.now().minusMonths(5), + periodeTom = LocalDate.now(), + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + erEksplisittAvslagPåSøknad = true, + ) + val barn2PersonResultat = lagPersonResultat( + vilkårsvurdering = vikårsvurdering, + person = barn2, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now().minusMonths(5), + periodeTom = LocalDate.now(), + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + erEksplisittAvslagPåSøknad = false, + ) + + assertDoesNotThrow { + BehandlingsresultatValideringUtils.validerAtBarePersonerFremstiltKravForEllerSøkerHarFåttEksplisittAvslag( + personResultater = setOf(barn1PersonResultat, barn2PersonResultat), + personerFremstiltKravFor = listOf(barn1.aktør, barn2.aktør), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/AndelTilkjentYtelseUtledRegelverkTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/AndelTilkjentYtelseUtledRegelverkTest.kt new file mode 100644 index 000000000..112d91759 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/AndelTilkjentYtelseUtledRegelverkTest.kt @@ -0,0 +1,91 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.YearMonth + +class AndelTilkjentYtelseUtledRegelverkTest { + + val behandling = lagBehandling() + val barnPerson = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(1)) + + val andelTilkjentYtelse = lagAndelTilkjentYtelse( + behandling = behandling, + person = barnPerson, + fom = YearMonth.now().minusMonths(4), + tom = YearMonth.now().plusMonths(1), + ) + + val vilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + ) + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barnPerson.aktør, + ) + + @Test + fun `EØS-forordning om alle relevante vilkår er satt til regelverk EØS forordning`() { + val regelverk = andelTilkjentYtelse.vurdertEtter(setOf(genererPersonresultat())) + + assertEquals(Regelverk.EØS_FORORDNINGEN, regelverk) + } + + @Test + fun `Nasjonale regler om alle relevante vilkår er satt til regelverk nasonale regler`() { + val personResultat = + genererPersonresultat(Regelverk.NASJONALE_REGLER, Regelverk.NASJONALE_REGLER, Regelverk.NASJONALE_REGLER) + + val regelverk = andelTilkjentYtelse.vurdertEtter(setOf(personResultat)) + + assertEquals(Regelverk.NASJONALE_REGLER, regelverk) + } + + @Test + fun `Default til nasjonale regler om relevante vilkår er satt til forskjellig regelverk`() { + val personResultat = + genererPersonresultat(Regelverk.EØS_FORORDNINGEN, Regelverk.NASJONALE_REGLER, Regelverk.NASJONALE_REGLER) + + val regelverk = andelTilkjentYtelse.vurdertEtter(setOf(personResultat)) + + assertEquals(Regelverk.NASJONALE_REGLER, regelverk) + } + + private fun genererPersonresultat( + regelVerkBosattIRiket: Regelverk = Regelverk.EØS_FORORDNINGEN, + regelVerkLovligOpphold: Regelverk = Regelverk.EØS_FORORDNINGEN, + regelVerkBorMedSøker: Regelverk = Regelverk.EØS_FORORDNINGEN, + ): PersonResultat { + val vilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + ) + + val barnPersonResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barnPerson.aktør, + ) + + val vilkårResultat = listOf( + lagVilkårResultat(Vilkår.BOSATT_I_RIKET, regelVerkBosattIRiket, YearMonth.now().minusYears(1), null), + lagVilkårResultat(Vilkår.LOVLIG_OPPHOLD, regelVerkLovligOpphold, YearMonth.now().minusYears(1), null), + lagVilkårResultat(Vilkår.BOR_MED_SØKER, regelVerkBorMedSøker, YearMonth.now().minusYears(1), null), + lagVilkårResultat( + Vilkår.UNDER_18_ÅR, + Regelverk.NASJONALE_REGLER, + YearMonth.now().minusYears(1), + YearMonth.now().plusYears(17), + ), + ) + barnPersonResultat.vilkårResultater.addAll(vilkårResultat) + return barnPersonResultat + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregnAndelerTilkjentYtelseMedGjeldendeSatserTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregnAndelerTilkjentYtelseMedGjeldendeSatserTest.kt new file mode 100644 index 000000000..4812fcebe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregnAndelerTilkjentYtelseMedGjeldendeSatserTest.kt @@ -0,0 +1,366 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.kjerne.beregning.Prosent.alt +import no.nav.familie.ba.sak.kjerne.beregning.Prosent.halvparten +import no.nav.familie.ba.sak.kjerne.eøs.util.barn +import no.nav.familie.ba.sak.kjerne.eøs.util.der +import no.nav.familie.ba.sak.kjerne.eøs.util.død +import no.nav.familie.ba.sak.kjerne.eøs.util.etter +import no.nav.familie.ba.sak.kjerne.eøs.util.født +import no.nav.familie.ba.sak.kjerne.eøs.util.har +import no.nav.familie.ba.sak.kjerne.eøs.util.med +import no.nav.familie.ba.sak.kjerne.eøs.util.og +import no.nav.familie.ba.sak.kjerne.eøs.util.oppfylt +import no.nav.familie.ba.sak.kjerne.eøs.util.søker +import no.nav.familie.ba.sak.kjerne.eøs.util.uendelig +import no.nav.familie.ba.sak.kjerne.eøs.util.under18år +import no.nav.familie.ba.sak.kjerne.eøs.util.vilkårsvurdering +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo +import no.nav.familie.ba.sak.kjerne.tidslinje.util.VilkårsvurderingBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.apr +import no.nav.familie.ba.sak.kjerne.tidslinje.util.aug +import no.nav.familie.ba.sak.kjerne.tidslinje.util.des +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jul +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jun +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mai +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mar +import no.nav.familie.ba.sak.kjerne.tidslinje.util.nov +import no.nav.familie.ba.sak.kjerne.tidslinje.util.okt +import no.nav.familie.ba.sak.kjerne.tidslinje.util.sep +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk.EØS_FORORDNINGEN +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk.NASJONALE_REGLER +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering.DELT_BOSTED +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.BOR_MED_SØKER +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.BOSATT_I_RIKET +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.GIFT_PARTNERSKAP +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.LOVLIG_OPPHOLD +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.UNDER_18_ÅR +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.UTVIDET_BARNETRYGD +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class BeregnAndelerTilkjentYtelseMedGjeldendeSatserTest { + + @Test + fun `tom vilkårsvurdering gir ingen utbetalinger`() { + assertEquals(emptyList(), vilkårsvurdering.beregnAndelerTilkjentYtelseForBarna()) + } + + @Test + fun `minimal oppfylt vilkårsvurdering ett barn skal gi utbetalinger etter satsendringer`() { + val søker = søker født 19.nov(1995) + val barn = barn født 14.nov(2017) + + val vurdering = vilkårsvurdering der + søker har + (BOSATT_I_RIKET og LOVLIG_OPPHOLD oppfylt 26.jan(2018)..uendelig) der + barn har + (UNDER_18_ÅR oppfylt barn.under18år()) og + (GIFT_PARTNERSKAP og BOR_MED_SØKER og BOSATT_I_RIKET og LOVLIG_OPPHOLD oppfylt 26.jan(2018)..uendelig) + + val forventedeAndeler = listOf( + barn får alt av 970 i feb(2018)..feb(2019), + barn får alt av 1054 i mar(2019)..aug(2020), + barn får alt av 1354 i sep(2020)..aug(2021), + barn får alt av 1654 i sep(2021)..des(2021), + barn får alt av 1676 i jan(2022)..feb(2023), + barn får alt av 1723 i mar(2023)..jun(2023), + barn får alt av 1766 i jul(2023)..okt(2023), + barn får alt av 1310 i nov(2023)..okt(2035), + ) + + assertEquals(forventedeAndeler, vurdering.beregnAndelerTilkjentYtelseForBarna()) + } + + @Test + fun `minimal oppfylt vilkårsvurdering to barn skal gi utbetalinger etter satsendringer`() { + val søker = søker født 19.nov(1995) + val barn1 = barn født 14.nov(2017) + val barn2 = barn født 1.mai(2013) + + val vurdering = vilkårsvurdering der + søker har + (BOSATT_I_RIKET og LOVLIG_OPPHOLD oppfylt 26.jan(2011)..uendelig) der + barn1 har + (UNDER_18_ÅR oppfylt barn1.under18år()) og + (GIFT_PARTNERSKAP og BOR_MED_SØKER og BOSATT_I_RIKET og LOVLIG_OPPHOLD oppfylt 26.jan(2018)..uendelig) der + barn2 har + (UNDER_18_ÅR oppfylt barn2.under18år()) og + (GIFT_PARTNERSKAP og BOR_MED_SØKER og BOSATT_I_RIKET og LOVLIG_OPPHOLD oppfylt 1.jun(2013)..uendelig) + + val forventedeAndeler = listOf( + // barn 1 + barn1 får alt av 970 i feb(2018)..feb(2019), + barn1 får alt av 1054 i mar(2019)..aug(2020), + barn1 får alt av 1354 i sep(2020)..aug(2021), + barn1 får alt av 1654 i sep(2021)..des(2021), + barn1 får alt av 1676 i jan(2022)..feb(2023), + barn1 får alt av 1723 i mar(2023)..jun(2023), + barn1 får alt av 1766 i jul(2023)..okt(2023), + barn1 får alt av 1310 i nov(2023)..okt(2035), + // barn 2 + barn2 får alt av 970 i jun(2013)..feb(2019), + barn2 får alt av 1054 i mar(2019)..feb(2023), + barn2 får alt av 1083 i mar(2023)..jun(2023), + barn2 får alt av 1310 i jul(2023)..apr(2031), + ) + + assertEquals(forventedeAndeler, vurdering.beregnAndelerTilkjentYtelseForBarna()) + } + + @Test + fun `vilkårsvurdering med søker og ett barn der søker mangler vurdering av ett vilkår, skal ikke gi utbetalinger`() { + val søker = søker født 19.nov(1995) + val barn = barn født 14.des(2019) + + val vurdering = vilkårsvurdering der + søker har + (BOSATT_I_RIKET oppfylt 26.jan(2020)..uendelig) der + // mangler LOVLIG_OPPHOLD + barn har + (UNDER_18_ÅR oppfylt barn.under18år()) og + (GIFT_PARTNERSKAP og BOR_MED_SØKER og BOSATT_I_RIKET og LOVLIG_OPPHOLD oppfylt 26.jan(2020)..uendelig) + + assertEquals(emptyList(), vurdering.beregnAndelerTilkjentYtelseForBarna()) + } + + @Test + fun `vilkårsvurdering med søker og ett barn der barn mangler vurdering av ett vilkår, skal ikke gi utbetalinger`() { + val søker = søker født 19.nov(1995) + val barn = barn født 14.des(2019) + + val vurdering = vilkårsvurdering der + søker har + (BOSATT_I_RIKET og LOVLIG_OPPHOLD oppfylt 26.jan(2020)..uendelig) der + barn har + (UNDER_18_ÅR oppfylt barn.under18år()) og + // mangler LOVLIG_OPPHOLD + (GIFT_PARTNERSKAP og BOR_MED_SØKER og BOSATT_I_RIKET oppfylt 26.jan(2020)..uendelig) + + assertEquals(emptyList(), vurdering.beregnAndelerTilkjentYtelseForBarna()) + } + + @Test + fun `vilkårsvurdering der søkers vilkårsresultater ikke overlapper for noen vilkår, skal ikke gi utbetalinger`() { + val søker = søker født 19.nov(1995) + val barn = barn født 14.des(2019) + + val vurdering = vilkårsvurdering der + søker har + (BOSATT_I_RIKET oppfylt 26.jan(2020)..25.apr(2024)) og + (LOVLIG_OPPHOLD oppfylt 26.apr(2024)..uendelig) der + barn har + (UNDER_18_ÅR oppfylt barn.under18år()) og + (GIFT_PARTNERSKAP og BOR_MED_SØKER og BOSATT_I_RIKET og LOVLIG_OPPHOLD oppfylt 26.jan(2020)..uendelig) + + assertEquals(emptyList(), vurdering.beregnAndelerTilkjentYtelseForBarna()) + } + + @Test + fun `vilkårsvurdering der barnet vilkårsresultater ikke overlapper for noen vilkår, skal ikke gi utbetalinger`() { + val søker = søker født 19.nov(1995) + val barn = barn født 14.des(2019) + + val vurdering = vilkårsvurdering der + søker har + (BOSATT_I_RIKET og LOVLIG_OPPHOLD oppfylt 26.jan(2020)..uendelig) der + barn har + (UNDER_18_ÅR oppfylt 26.jan(2020)..30.apr(2022)) og + (GIFT_PARTNERSKAP oppfylt 1.mai(2022)..29.feb(2024)) og + (BOR_MED_SØKER oppfylt 1.mar(2024)..31.jul(2027)) og + (BOSATT_I_RIKET oppfylt 1.aug(2027)..31.des(2031)) og + (LOVLIG_OPPHOLD oppfylt 1.jan(2032)..uendelig) + + assertEquals(emptyList(), vurdering.beregnAndelerTilkjentYtelseForBarna()) + } + + @Test + fun `minimal vilkårsvurdering ett barn og delt bosted`() { + val søker = søker født 19.nov(1995) + val barn = barn født 14.des(2019) + + val vurdering = vilkårsvurdering der + søker har + (BOSATT_I_RIKET og LOVLIG_OPPHOLD oppfylt 26.jan(2020)..uendelig) der + barn har + (UNDER_18_ÅR oppfylt barn.under18år()) og + (BOR_MED_SØKER oppfylt 26.jan(2020)..13.feb(2024) med DELT_BOSTED) og + (BOR_MED_SØKER oppfylt 14.feb(2024)..uendelig) og + (BOSATT_I_RIKET og LOVLIG_OPPHOLD og GIFT_PARTNERSKAP oppfylt 26.jan(2020)..uendelig) + + val forventedeAndeler = listOf( + barn får halvparten av 1054 i feb(2020)..aug(2020), + barn får halvparten av 1354 i sep(2020)..aug(2021), + barn får halvparten av 1654 i sep(2021)..des(2021), + barn får halvparten av 1676 i jan(2022)..feb(2023), + barn får halvparten av 1723 i mar(2023)..jun(2023), + barn får halvparten av 1766 i jul(2023)..feb(2024), + + barn får alt av 1766 i mar(2024)..nov(2025), + barn får alt av 1310 i des(2025)..nov(2037), + ) + + assertEquals(forventedeAndeler, vurdering.beregnAndelerTilkjentYtelseForBarna()) + } + + @Test + fun `Sjekk overgang fra oppfylt nasjonalt til oppfylt EØS dagen andre dag i måneden`() { + val søker = søker født 19.nov(1995) + val barn = barn født 14.des(2019) + + val vurdering = vilkårsvurdering der + søker har + (BOSATT_I_RIKET oppfylt 26.jan(2020)..1.mai(2021) etter NASJONALE_REGLER) og + (BOSATT_I_RIKET oppfylt 2.mai(2021)..30.nov(2021) etter EØS_FORORDNINGEN) og + (LOVLIG_OPPHOLD oppfylt 26.jan(2020)..1.mai(2021) etter NASJONALE_REGLER) og + (LOVLIG_OPPHOLD oppfylt 2.mai(2021)..30.nov(2021) etter EØS_FORORDNINGEN) der + barn har + (UNDER_18_ÅR oppfylt 26.jan(2020)..30.nov(2021)) og + (GIFT_PARTNERSKAP oppfylt 26.jan(2020)..30.nov(2021)) og + (BOR_MED_SØKER oppfylt 26.jan(2020)..1.mai(2021) etter NASJONALE_REGLER) og + (BOR_MED_SØKER oppfylt 2.mai(2021)..30.nov(2021) etter EØS_FORORDNINGEN) og + (BOSATT_I_RIKET oppfylt 26.jan(2020)..1.mai(2021) etter NASJONALE_REGLER) og + (BOSATT_I_RIKET oppfylt 2.mai(2021)..30.nov(2021) etter EØS_FORORDNINGEN) og + (LOVLIG_OPPHOLD oppfylt 26.jan(2020)..1.mai(2021) etter NASJONALE_REGLER) og + (LOVLIG_OPPHOLD oppfylt 2.mai(2021)..30.nov(2021) etter EØS_FORORDNINGEN) + + val forventedeAndeler = listOf( + barn får alt av 1054 i feb(2020)..aug(2020), + barn får alt av 1354 i sep(2020)..aug(2021), + barn får alt av 1654 i sep(2021)..nov(2021), + ) + + assertEquals(forventedeAndeler, vurdering.beregnAndelerTilkjentYtelseForBarna()) + } + + @Test + fun `vilkårsvurdering ett barn som dør før fylte 18`() { + val søker = søker født 19.nov(1995) + val barn = barn født 14.des(2019) død 9.des(2024) + + val vurdering = vilkårsvurdering der + søker har + (BOSATT_I_RIKET oppfylt 26.jan(2020)..uendelig) og + (LOVLIG_OPPHOLD oppfylt 26.jan(2020)..uendelig) der + barn har + (UNDER_18_ÅR oppfylt 14.des(2019)..9.des(2024)) og + (GIFT_PARTNERSKAP oppfylt 26.jan(2020)..9.des(2024)) og + (BOR_MED_SØKER oppfylt 26.jan(2020)..9.des(2024)) og + (BOSATT_I_RIKET oppfylt 26.jan(2020)..9.des(2024)) og + (LOVLIG_OPPHOLD oppfylt 26.jan(2020)..9.des(2024)) + + val forventedeAndeler = listOf( + barn får alt av 1054 i feb(2020)..aug(2020), + barn får alt av 1354 i sep(2020)..aug(2021), + barn får alt av 1654 i sep(2021)..des(2021), + barn får alt av 1676 i jan(2022)..feb(2023), + barn får alt av 1723 i mar(2023)..jun(2023), + barn får alt av 1766 i jul(2023)..des(2024), + ) + + assertEquals(forventedeAndeler, vurdering.beregnAndelerTilkjentYtelseForBarna()) + } + + @Test + fun `vilkårsvurdering ett barn som dør samme måned som fylte 18`() { + val søker = søker født 19.nov(1995) + val barn = barn født 14.des(2019) død 9.des(2037) + + val vurdering = vilkårsvurdering der + søker har + (BOSATT_I_RIKET oppfylt 26.jan(2020)..uendelig) og + (LOVLIG_OPPHOLD oppfylt 26.jan(2020)..uendelig) der + barn har + (UNDER_18_ÅR oppfylt 14.des(2019)..9.des(2037)) og + (GIFT_PARTNERSKAP oppfylt 26.jan(2020)..9.des(2037)) og + (BOR_MED_SØKER oppfylt 26.jan(2020)..9.des(2037)) og + (BOSATT_I_RIKET oppfylt 26.jan(2020)..9.des(2037)) og + (LOVLIG_OPPHOLD oppfylt 26.jan(2020)..9.des(2037)) + + val forventedeAndeler = listOf( + barn får alt av 1054 i feb(2020)..aug(2020), + barn får alt av 1354 i sep(2020)..aug(2021), + barn får alt av 1654 i sep(2021)..des(2021), + barn får alt av 1676 i jan(2022)..feb(2023), + barn får alt av 1723 i mar(2023)..jun(2023), + barn får alt av 1766 i jul(2023)..nov(2025), + barn får alt av 1310 i des(2025)..nov(2037), + ) + + assertEquals(forventedeAndeler, vurdering.beregnAndelerTilkjentYtelseForBarna()) + } + + @Test + fun `skal opprette riktige satser for barn og søker ved utvidet barnetrygd`() { + val søker = PersonType.SØKER født 19.nov(1995) + val barn = PersonType.BARN født 14.des(2018) + + val vurdering = vilkårsvurdering der + søker har + (BOSATT_I_RIKET oppfylt 26.jan(2018)..uendelig) og + (UTVIDET_BARNETRYGD oppfylt 26.jan(2018)..uendelig) og + (LOVLIG_OPPHOLD oppfylt 26.jan(2018)..uendelig) der + barn har + (UNDER_18_ÅR oppfylt 14.des(2018)..14.des(2036)) og + (GIFT_PARTNERSKAP oppfylt 26.jan(2018)..14.des(2036)) og + (BOR_MED_SØKER oppfylt 26.jan(2018)..14.des(2036)) og + (BOSATT_I_RIKET oppfylt 26.jan(2018)..14.des(2036)) og + (LOVLIG_OPPHOLD oppfylt 26.jan(2018)..14.des(2036)) + + val forventedeAndeler = listOf( + barn får 970 i jan(2019)..feb(2019), + barn får 1054 i mar(2019)..aug(2020), + barn får 1354 i sep(2020)..aug(2021), + barn får 1654 i sep(2021)..des(2021), + barn får 1676 i jan(2022)..feb(2023), + barn får 1723 i mar(2023)..jun(2023), + barn får 1766 i jul(2023)..nov(2024), + barn får 1310 i des(2024)..nov(2036), + + søker får 970 i jan(2019)..feb(2019), + søker får 1054 i mar(2019)..feb(2023), + søker får 2489 i mar(2023)..jun(2023), + søker får 2516 i jul(2023)..nov(2036), + ) + + assertEquals(forventedeAndeler, vurdering.beregnAndelerTilkjentYteldse()) + } +} + +private fun VilkårsvurderingBuilder.PersonResultatBuilder.beregnAndelerTilkjentYteldse(): List { + val personopplysningGrunnlag = this.byggPersonopplysningGrunnlag() + return TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = this.byggVilkårsvurdering(), + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ).andelerTilkjentYtelse.map { + BeregnetAndel( + person = personopplysningGrunnlag.personer.first { person -> person.aktør == it.aktør }, + stønadFom = it.stønadFom, + stønadTom = it.stønadTom, + beløp = it.kalkulertUtbetalingsbeløp, + sats = it.sats, + prosent = it.prosent, + ) + } +} + +internal fun VilkårsvurderingBuilder.beregnAndelerTilkjentYtelseForBarna(): List = + OrdinærBarnetrygdUtil.beregnAndelerTilkjentYtelseForBarna( + personopplysningGrunnlag = this.byggPersonopplysningGrunnlag(), + personResultater = this.byggVilkårsvurdering().personResultater, + fagsakType = FagsakType.NORMAL, + ) + +internal fun VilkårsvurderingBuilder.PersonResultatBuilder.beregnAndelerTilkjentYtelseForBarna(): List = + OrdinærBarnetrygdUtil.beregnAndelerTilkjentYtelseForBarna( + personopplysningGrunnlag = this.byggPersonopplysningGrunnlag(), + personResultater = this.byggVilkårsvurdering().personResultater, + fagsakType = FagsakType.NORMAL, + ) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregnetAndelDsl.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregnetAndelDsl.kt new file mode 100644 index 000000000..f2851fc89 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregnetAndelDsl.kt @@ -0,0 +1,50 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.Utils.avrundetHeltallAvProsent +import no.nav.familie.ba.sak.kjerne.beregning.Prosent.alt +import no.nav.familie.ba.sak.kjerne.beregning.Prosent.halvparten +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.TidspunktClosedRange +import java.math.BigDecimal +import java.time.YearMonth + +internal infix fun Person.får(prosent: Prosent) = BeregnetAndel( + person = this, + prosent = when (prosent) { + alt -> BigDecimal.valueOf(100) + halvparten -> BigDecimal.valueOf(50) + Prosent.ingenting -> BigDecimal.ZERO + }, + stønadFom = YearMonth.now(), + stønadTom = YearMonth.now(), + beløp = 0, + sats = 0, +) + +internal infix fun Person.får(sats: Int) = BeregnetAndel( + person = this, + prosent = BigDecimal.valueOf(100), + stønadFom = YearMonth.now(), + stønadTom = YearMonth.now(), + beløp = sats, + sats = sats, +) + +@Suppress("ktlint:standard:enum-entry-name-case") +enum class Prosent { + alt, + halvparten, + ingenting, +} + +internal infix fun BeregnetAndel.av(sats: Int) = this.copy( + sats = sats, + beløp = sats.avrundetHeltallAvProsent(prosent), +) + +internal infix fun BeregnetAndel.i(tidsrom: TidspunktClosedRange) = this.copy( + stønadFom = tidsrom.start.tilYearMonth(), + stønadTom = tidsrom.endInclusive.tilYearMonth(), +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregnetAndelDslTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregnetAndelDslTest.kt new file mode 100644 index 000000000..4bf2037ec --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregnetAndelDslTest.kt @@ -0,0 +1,38 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.kjerne.beregning.Prosent.alt +import no.nav.familie.ba.sak.kjerne.beregning.Prosent.halvparten +import no.nav.familie.ba.sak.kjerne.eøs.util.barn +import no.nav.familie.ba.sak.kjerne.eøs.util.død +import no.nav.familie.ba.sak.kjerne.eøs.util.født +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo +import no.nav.familie.ba.sak.kjerne.tidslinje.util.aug +import no.nav.familie.ba.sak.kjerne.tidslinje.util.des +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.YearMonth +class BeregnetAndelDslTest { + + @Test + fun `sjekk at andel bygges riktig`() { + val barn = barn født 14.des(2019) død 9.des(2024) + val andel = barn får alt av 1054 i feb(2020)..aug(2020) + + Assertions.assertEquals(barn, andel.person) + Assertions.assertEquals(BigDecimal.valueOf(100), andel.prosent) + Assertions.assertEquals(1054, andel.beløp) + Assertions.assertEquals(1054, andel.sats) + Assertions.assertEquals(YearMonth.of(2020, 2), andel.stønadFom) + Assertions.assertEquals(YearMonth.of(2020, 8), andel.stønadTom) + } + + @Test + fun `sjekk at halvparten av oddetall rundes opp`() { + val barn = barn født 14.des(2019) + val andel = barn får halvparten av 1723 + Assertions.assertEquals(BigDecimal.valueOf(50), andel.prosent) + Assertions.assertEquals(862, andel.beløp) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningServiceTest.kt new file mode 100644 index 000000000..8e03b798e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningServiceTest.kt @@ -0,0 +1,1351 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.slot +import io.mockk.unmockkObject +import io.mockk.verify +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.forrigeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestBaseFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestFagsak +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndelRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.søknad.SøknadGrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.EndringerIUtbetalingForBehandlingSteg +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import no.nav.familie.kontrakter.felles.Ressurs +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class BeregningServiceTest { + + private val tilkjentYtelseRepository = mockk() + private val vilkårsvurderingRepository = mockk() + private val behandlingHentOgPersisterService = mockk() + private val andelTilkjentYtelseRepository = mockk() + private val behandlingRepository = mockk() + private val søknadGrunnlagService = mockk() + private val personopplysningGrunnlagRepository = mockk() + private val endretUtbetalingAndelRepository = mockk() + private val småbarnstilleggService = mockk() + private val featureToggleService = mockk(relaxed = true) + private val andelerTilkjentYtelseOgEndreteUtbetalingerService = AndelerTilkjentYtelseOgEndreteUtbetalingerService( + andelTilkjentYtelseRepository, + endretUtbetalingAndelRepository, + vilkårsvurderingRepository, + ) + + private lateinit var beregningService: BeregningService + + val jan22 = YearMonth.of(2022, 1) + val aug22 = YearMonth.of(2022, 8) + + @BeforeEach + fun førHverTest() { + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2022, 12, 31) + } + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + @BeforeEach + fun setUp() { + val fagsakService = mockk() + + beregningService = BeregningService( + andelTilkjentYtelseRepository = andelTilkjentYtelseRepository, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + tilkjentYtelseRepository = tilkjentYtelseRepository, + vilkårsvurderingRepository = vilkårsvurderingRepository, + behandlingRepository = behandlingRepository, + personopplysningGrunnlagRepository = personopplysningGrunnlagRepository, + småbarnstilleggService = småbarnstilleggService, + andelerTilkjentYtelseOgEndreteUtbetalingerService = andelerTilkjentYtelseOgEndreteUtbetalingerService, + featureToggleService = featureToggleService, + ) + + every { tilkjentYtelseRepository.slettTilkjentYtelseFor(any()) } just Runs + every { fagsakService.hentRestFagsak(any()) } answers { + Ressurs.success( + defaultFagsak().tilRestBaseFagsak(false, emptyList(), null, null) + .tilRestFagsak(emptyList(), emptyList()), + ) + } + every { endretUtbetalingAndelRepository.findByBehandlingId(any()) } answers { emptyList() } + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(any()) } answers { emptyList() } + every { endretUtbetalingAndelRepository.saveAllAndFlush(any>()) } answers { emptyList() } + every { andelTilkjentYtelseRepository.saveAllAndFlush(any>()) } answers { emptyList() } + } + + @Test + fun `Skal mappe perioderesultat til andel ytelser for innvilget vedtak med 18-års vilkår som sluttdato`() { + val behandling = lagBehandling() + + val barn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2002, 7, 1)) + val søker = lagPerson(type = PersonType.SØKER) + + val vilkårsvurdering = + Vilkårsvurdering(behandling = behandling) + + val periodeFom = LocalDate.of(2020, 1, 1) + val periodeTom = LocalDate.of(2020, 7, 1) + val personResultatBarn = lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = barn, + resultat = Resultat.OPPFYLT, + periodeFom = periodeFom, + periodeTom = periodeTom, + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + ) + + val personResultatSøker = lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = søker, + resultat = Resultat.OPPFYLT, + periodeFom = periodeFom, + periodeTom = periodeTom, + lagFullstendigVilkårResultat = true, + personType = PersonType.SØKER, + ) + vilkårsvurdering.personResultater = setOf(personResultatBarn, personResultatSøker) + + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søker.aktør.aktivFødselsnummer(), + barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), + barnasFødselsdatoer = listOf(barn.fødselsdato), + ) + val slot = slot() + + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(any()) } answers { vilkårsvurdering } + every { tilkjentYtelseRepository.save(any()) } returns lagInitiellTilkjentYtelse(behandling) + every { søknadGrunnlagService.hentAktiv(any())?.hentSøknadDto() } returns lagSøknadDTO( + søker.aktør.aktivFødselsnummer(), + listOf(barn.aktør.aktivFødselsnummer()), + ) + + beregningService.oppdaterBehandlingMedBeregning( + behandling = behandling, + personopplysningGrunnlag = personopplysningGrunnlag, + ) + + verify(exactly = 1) { tilkjentYtelseRepository.save(capture(slot)) } + + Assertions.assertEquals(1, slot.captured.andelerTilkjentYtelse.size) + Assertions.assertEquals(1054, slot.captured.andelerTilkjentYtelse.first().kalkulertUtbetalingsbeløp) + Assertions.assertEquals(periodeFom.nesteMåned(), slot.captured.andelerTilkjentYtelse.first().stønadFom) + Assertions.assertEquals(periodeTom.forrigeMåned(), slot.captured.andelerTilkjentYtelse.first().stønadTom) + } + + @Test + fun `Skal mappe perioderesultat til andel ytelser for innvilget vedtak som spenner over flere satsperioder`() { + val behandling = lagBehandling() + + val barn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2016, 5, 4)) + val søker = lagPerson(type = PersonType.SØKER) + + val vilkårsvurdering = + Vilkårsvurdering(behandling = behandling) + + val periodeFom = LocalDate.of(2018, 1, 1) + val periodeTom = LocalDate.of(2020, 7, 1) + val personResultatBarn = lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = barn, + resultat = Resultat.OPPFYLT, + periodeFom = periodeFom, + periodeTom = periodeTom, + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + ) + + val personResultatSøker = lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = søker, + resultat = Resultat.OPPFYLT, + periodeFom = periodeFom, + periodeTom = periodeTom, + lagFullstendigVilkårResultat = true, + personType = PersonType.SØKER, + ) + vilkårsvurdering.personResultater = setOf(personResultatBarn, personResultatSøker) + + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søker.aktør.aktivFødselsnummer(), + barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), + barnasFødselsdatoer = listOf(barn.fødselsdato), + ) + val slot = slot() + + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(any()) } answers { vilkårsvurdering } + every { tilkjentYtelseRepository.save(any()) } returns lagInitiellTilkjentYtelse(behandling) + every { søknadGrunnlagService.hentAktiv(any())?.hentSøknadDto() } returns lagSøknadDTO( + søker.aktør.aktivFødselsnummer(), + listOf(barn.aktør.aktivFødselsnummer()), + ) + + beregningService.oppdaterBehandlingMedBeregning( + behandling = behandling, + personopplysningGrunnlag = personopplysningGrunnlag, + ) + + verify(exactly = 1) { tilkjentYtelseRepository.save(capture(slot)) } + + Assertions.assertEquals(2, slot.captured.andelerTilkjentYtelse.size) + + val andelerTilkjentYtelse = slot.captured.andelerTilkjentYtelse.sortedBy { it.stønadFom } + val satsPeriode1Slutt = YearMonth.of(2019, 2) + val satsPeriode2Start = YearMonth.of(2019, 3) + + Assertions.assertEquals(970, andelerTilkjentYtelse.first().kalkulertUtbetalingsbeløp) + Assertions.assertEquals(periodeFom.nesteMåned(), andelerTilkjentYtelse.first().stønadFom) + Assertions.assertEquals(satsPeriode1Slutt, andelerTilkjentYtelse.first().stønadTom) + + Assertions.assertEquals(1054, andelerTilkjentYtelse.last().kalkulertUtbetalingsbeløp) + Assertions.assertEquals(satsPeriode2Start, andelerTilkjentYtelse.last().stønadFom) + Assertions.assertEquals(periodeTom.toYearMonth(), andelerTilkjentYtelse.last().stønadTom) + } + + @Test + fun `Skal verifisere at endret utbetaling andel appliseres på en innvilget utbetaling andel`() { + val behandling = lagBehandling() + val barn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2016, 4, 5)) + val søker = lagPerson(type = PersonType.SØKER) + val vilkårsvurdering = + Vilkårsvurdering(behandling = behandling) + + val periodeFom = LocalDate.of(2018, 1, 1) + val periodeTom = LocalDate.of(2018, 7, 1) + val avtaletidspunktDeltBosted = LocalDate.of(2018, 7, 1) + val søknadstidspunkt = LocalDate.of(2018, 9, 1) + + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søker.aktør.aktivFødselsnummer(), + barnasFødselsdatoer = listOf(barn.fødselsdato), + barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), + barnAktør = listOf(barn.aktør), + søkerAktør = søker.aktør, + ) + + val personResultatBarn = lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = barn, + resultat = Resultat.OPPFYLT, + periodeFom = periodeFom, + periodeTom = periodeTom, + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + erDeltBosted = true, + ) + + val personResultatSøker = lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = søker, + resultat = Resultat.OPPFYLT, + periodeFom = periodeFom, + periodeTom = periodeTom, + lagFullstendigVilkårResultat = true, + personType = PersonType.SØKER, + ) + vilkårsvurdering.personResultater = setOf(personResultatBarn, personResultatSøker) + + val slot = slot() + + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(any()) } answers { vilkårsvurdering } + every { tilkjentYtelseRepository.save(any()) } returns lagInitiellTilkjentYtelse(behandling) + every { søknadGrunnlagService.hentAktiv(any())?.hentSøknadDto() } returns lagSøknadDTO( + søker.aktør.aktivFødselsnummer(), + listOf(barn.aktør.aktivFødselsnummer()), + ) + + val andelTilkjentYtelser = mutableListOf( + lagAndelTilkjentYtelse( + fom = periodeFom.toYearMonth(), + tom = periodeTom.toYearMonth(), + person = barn, + ), + ) + every { endretUtbetalingAndelRepository.findByBehandlingId(behandlingId = behandling.id) } returns + listOf( + EndretUtbetalingAndel( + behandlingId = behandling.id, + person = barn, + prosent = BigDecimal(50), + fom = periodeFom.toYearMonth(), + tom = periodeTom.toYearMonth(), + avtaletidspunktDeltBosted = avtaletidspunktDeltBosted, + søknadstidspunkt = søknadstidspunkt, + årsak = Årsak.DELT_BOSTED, + begrunnelse = "En begrunnelse", + ), + ) + + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandling.id) } returns + andelTilkjentYtelser + + beregningService.oppdaterBehandlingMedBeregning( + behandling = behandling, + personopplysningGrunnlag = personopplysningGrunnlag, + ) + + verify(exactly = 1) { tilkjentYtelseRepository.save(capture(slot)) } + + Assertions.assertEquals(1, slot.captured.andelerTilkjentYtelse.size) + + val andelerTilkjentYtelse = slot.captured.andelerTilkjentYtelse.sortedBy { it.stønadFom } + + Assertions.assertEquals(970 / 2, andelerTilkjentYtelse.first().kalkulertUtbetalingsbeløp) + Assertions.assertEquals(periodeFom.nesteMåned(), andelerTilkjentYtelse.first().stønadFom) + Assertions.assertEquals(periodeTom.toYearMonth(), andelerTilkjentYtelse.first().stønadTom) + } + + @Test + fun `Skal mappe perioderesultat til andel ytelser for avslått vedtak`() { + val behandling = lagBehandling() + val barn = lagPerson(type = PersonType.BARN) + val søker = lagPerson(type = PersonType.SØKER) + val vilkårsvurdering = + Vilkårsvurdering(behandling = behandling) + + val periodeFom = LocalDate.of(2020, 1, 1) + val periodeTom = LocalDate.of(2020, 11, 1) + val personResultatBarn = lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = barn, + resultat = Resultat.OPPFYLT, + periodeFom = periodeFom, + periodeTom = periodeTom, + ) + + val personResultatSøker = lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = søker, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = periodeFom, + periodeTom = periodeTom, + ) + vilkårsvurdering.personResultater = setOf(personResultatBarn, personResultatSøker) + + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søker.aktør.aktivFødselsnummer(), + barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), + ) + val slot = slot() + + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(any()) } answers { vilkårsvurdering } + every { tilkjentYtelseRepository.save(any()) } returns lagInitiellTilkjentYtelse(behandling) + every { søknadGrunnlagService.hentAktiv(any())?.hentSøknadDto() } returns lagSøknadDTO( + søker.aktør.aktivFødselsnummer(), + listOf(barn.aktør.aktivFødselsnummer()), + ) + + beregningService.oppdaterBehandlingMedBeregning( + behandling = behandling, + personopplysningGrunnlag = personopplysningGrunnlag, + ) + + verify(exactly = 1) { tilkjentYtelseRepository.save(capture(slot)) } + + Assertions.assertTrue(slot.captured.andelerTilkjentYtelse.isEmpty()) + } + + @Test + fun `For flere barn med forskjellige perioderesultat skal perioderesultat mappes til andel ytelser`() { + val behandling = lagBehandling() + val barnFødselsdato = LocalDate.of(2019, 1, 1) + val barn1 = lagPerson(type = PersonType.BARN, fødselsdato = barnFødselsdato) + val barn2 = lagPerson(type = PersonType.BARN, fødselsdato = barnFødselsdato) + val søker = lagPerson(type = PersonType.SØKER) + val vilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + ) + + val periode1Fom = LocalDate.of(2020, 1, 1) + val periode1Tom = LocalDate.of(2020, 11, 13) + + val periode2Fom = LocalDate.of(2020, 12, 1) + val periode2Midt = LocalDate.of(2021, 6, 1) + val periode2Tom = LocalDate.of(2021, 12, 11) + + val periode3Fom = LocalDate.of(2022, 1, 12) + val periode3Midt = LocalDate.of(2023, 6, 10) + val periode3Tom = LocalDate.of(2028, 1, 1) + + val tilleggFom = SatsService.hentDatoForSatsendring(satstype = SatsType.TILLEGG_ORBA, oppdatertBeløp = 1354) + + val søkerVilkår = Vilkår.hentVilkårFor( + personType = PersonType.SØKER, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ) + val vilkårResultaterSøker = søkerVilkår.map { + lagVilkårResultat( + vilkårType = it, + periodeFom = periode1Fom, + periodeTom = periode1Tom, + ) + } + søkerVilkår.map { + lagVilkårResultat( + vilkårType = it, + periodeFom = periode2Fom, + periodeTom = periode2Tom, + resultat = Resultat.IKKE_OPPFYLT, + ) + } + søkerVilkår.map { lagVilkårResultat(vilkårType = it, periodeFom = periode3Fom, periodeTom = periode3Tom) } + + val personResultatSøker = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = søker.aktør, + ) + + personResultatSøker.setSortedVilkårResultater(vilkårResultaterSøker.toSet()) + + val personResultatBarna = mutableSetOf( + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = barn1, + resultat = Resultat.OPPFYLT, + periodeFom = periode1Fom.minusYears(1), + periodeTom = periode3Tom.plusYears(1), + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + ), + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = barn2, + resultat = Resultat.OPPFYLT, + periodeFom = periode2Midt, + periodeTom = periode3Midt, + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + ), + ) + + vilkårsvurdering.personResultater = personResultatBarna + personResultatSøker + + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søker.aktør.aktivFødselsnummer(), + barnasIdenter = listOf(barn1.aktør.aktivFødselsnummer(), barn2.aktør.aktivFødselsnummer()), + ) + val slot = slot() + + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(any()) } answers { vilkårsvurdering } + every { tilkjentYtelseRepository.save(any()) } returns lagInitiellTilkjentYtelse(behandling) + every { søknadGrunnlagService.hentAktiv(any())?.hentSøknadDto() } returns lagSøknadDTO( + søker.aktør.aktivFødselsnummer(), + listOf(barn1.aktør.aktivFødselsnummer(), barn2.aktør.aktivFødselsnummer()), + ) + + beregningService.oppdaterBehandlingMedBeregning( + behandling = behandling, + personopplysningGrunnlag = personopplysningGrunnlag, + ) + + verify(exactly = 1) { tilkjentYtelseRepository.save(capture(slot)) } + + Assertions.assertEquals(5, slot.captured.andelerTilkjentYtelse.size) + val andelerTilkjentYtelse = slot.captured.andelerTilkjentYtelse.sortedBy { it.stønadTom } + + val (andelerBarn1, andelerBarn2) = andelerTilkjentYtelse.partition { it.aktør.aktivFødselsnummer() == barn1.aktør.aktivFødselsnummer() } + + // Barn 1 - første periode (før satsendring) + Assertions.assertEquals(periode1Fom.nesteMåned(), andelerBarn1[0].stønadFom) + Assertions.assertEquals(tilleggFom!!.forrigeMåned(), andelerBarn1[0].stønadTom) + Assertions.assertEquals(1054, andelerBarn1[0].kalkulertUtbetalingsbeløp) + + // Barn 1 - første periode (etter sept 2020 satsendring og før sept 2021 satsendring, før fylte 6 år) + Assertions.assertEquals(tilleggFom.toYearMonth(), andelerBarn1[1].stønadFom) + Assertions.assertEquals(periode1Tom.toYearMonth(), andelerBarn1[1].stønadTom) + Assertions.assertEquals(1354, andelerBarn1[1].kalkulertUtbetalingsbeløp) + + // Barn 1 - andre periode (etter siste satsendring, før fylte 6 år) + Assertions.assertEquals(periode3Fom.nesteMåned(), andelerBarn1[2].stønadFom) + Assertions.assertEquals(barnFødselsdato.plusYears(6).forrigeMåned(), andelerBarn1[2].stønadTom) + Assertions.assertEquals(1676, andelerBarn1[2].kalkulertUtbetalingsbeløp) + + // Barn 1 - andre periode (etter fylte 6 år) + Assertions.assertEquals(barnFødselsdato.plusYears(6).toYearMonth(), andelerBarn1[3].stønadFom) + Assertions.assertEquals(periode3Tom.toYearMonth(), andelerBarn1[3].stønadTom) + Assertions.assertEquals(1054, andelerBarn1[3].kalkulertUtbetalingsbeløp) + + // Barn 2 sin eneste periode (etter siste satsendring) + Assertions.assertEquals(periode3Fom.nesteMåned(), andelerBarn2[0].stønadFom) + Assertions.assertEquals(periode3Midt.toYearMonth(), andelerBarn2[0].stønadTom) + Assertions.assertEquals(1676, andelerBarn2[0].kalkulertUtbetalingsbeløp) + } + + @Test + fun `Skal ikke oppdatere utvidet andeler basert på endringsperioder med årsak=delt bosted`() { + val barn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(2)) + val søker = lagPerson(type = PersonType.SØKER, fødselsdato = LocalDate.now().minusYears(31)) + + val fom = LocalDate.now().minusMonths(8) + val tom = LocalDate.now().plusYears(3) + + val utvidetFom = LocalDate.now().minusMonths(3) + val utvidetTom = LocalDate.now().plusMonths(5) + + val endretUtbetalingAndelFom = utvidetFom.minusMonths(2).toYearMonth() + val endretUtbetalingAndelTom = utvidetTom.minusMonths(2).toYearMonth() + + val andelerTilkjentYtelse = genererAndelerTilkjentYtelseForScenario( + endretUtbetalingÅrsak = Årsak.DELT_BOSTED, + endretUtbetalingProsent = BigDecimal.ZERO, + endretUtbetalingFom = endretUtbetalingAndelFom, + endretUtbetalingTom = endretUtbetalingAndelTom, + endretUtbetalingPerson = barn, + generellFom = fom, + generellTom = tom, + utvidetFom = utvidetFom, + utvidetTom = utvidetTom, + barna = listOf(barn), + søker = søker, + ) + + Assertions.assertEquals(5, andelerTilkjentYtelse.size) + + val (andelerSøker, andelerBarn) = andelerTilkjentYtelse.partition { it.aktør.aktivFødselsnummer() == søker.aktør.aktivFødselsnummer() } + + Assertions.assertEquals(3, andelerBarn.size) + // Barn før endringsperiode + Assertions.assertEquals(barn.aktør, andelerBarn[0].aktør) + Assertions.assertEquals(BigDecimal(50), andelerBarn[0].prosent) + Assertions.assertEquals(fom.plusMonths(1).toYearMonth(), andelerBarn[0].stønadFom) + Assertions.assertEquals(endretUtbetalingAndelFom.minusMonths(1), andelerBarn[0].stønadTom) + + // Barn under endringsperiode + Assertions.assertEquals(barn.aktør, andelerBarn[1].aktør) + Assertions.assertEquals(BigDecimal.ZERO, andelerBarn[1].prosent) + Assertions.assertEquals(endretUtbetalingAndelFom, andelerBarn[1].stønadFom) + Assertions.assertEquals(endretUtbetalingAndelTom, andelerBarn[1].stønadTom) + + // Barn etter endringsperiode + Assertions.assertEquals(barn.aktør, andelerBarn[2].aktør) + Assertions.assertEquals(BigDecimal(50), andelerBarn[2].prosent) + Assertions.assertEquals(endretUtbetalingAndelTom.plusMonths(1), andelerBarn[2].stønadFom) + Assertions.assertEquals(tom.toYearMonth(), andelerBarn[2].stønadTom) + + Assertions.assertEquals(2, andelerSøker.size) + + val (andelerUtvidet, andelerSmåbarnstillegg) = andelerSøker.partition { it.erUtvidet() } + + Assertions.assertEquals(1, andelerUtvidet.size) + // Søker - utvidet under og etter endringsperiode + Assertions.assertEquals(søker.aktør, andelerUtvidet[0].aktør) + Assertions.assertEquals(BigDecimal(50), andelerUtvidet[0].prosent) + Assertions.assertEquals(utvidetFom.plusMonths(1).toYearMonth(), andelerUtvidet[0].stønadFom) + Assertions.assertEquals(utvidetTom.toYearMonth(), andelerUtvidet[0].stønadTom) + + Assertions.assertEquals(1, andelerSmåbarnstillegg.size) + // Søker - småbarnstillegg + Assertions.assertEquals(søker.aktør, andelerSmåbarnstillegg[0].aktør) + Assertions.assertEquals(BigDecimal(100), andelerSmåbarnstillegg[0].prosent) + Assertions.assertEquals(utvidetFom.plusMonths(1).toYearMonth(), andelerSmåbarnstillegg[0].stønadFom) + Assertions.assertEquals(utvidetTom.toYearMonth(), andelerSmåbarnstillegg[0].stønadTom) + } + + @Test + fun `Skal oppdatere utvidet andeler og småbarnstillegg med riktig periode og sats ved endringsperiode med årsak=etterbetaling 3år`() { + val barn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(2)) + val søker = lagPerson(type = PersonType.SØKER, fødselsdato = LocalDate.now().minusYears(31)) + + val fom = LocalDate.now().minusMonths(8) + val tom = LocalDate.now().plusYears(3) + + val utvidetFom = LocalDate.now().minusMonths(3) + val utvidetTom = LocalDate.now().plusMonths(5) + + val endretUtbetalingAndelFom = utvidetFom.minusMonths(2).toYearMonth() + val endretUtbetalingAndelTom = utvidetTom.minusMonths(2).toYearMonth() + + val andelerTilkjentYtelse = genererAndelerTilkjentYtelseForScenario( + endretUtbetalingÅrsak = Årsak.ETTERBETALING_3ÅR, + endretUtbetalingProsent = BigDecimal.ZERO, + endretUtbetalingFom = endretUtbetalingAndelFom, + endretUtbetalingTom = endretUtbetalingAndelTom, + endretUtbetalingPerson = barn, + generellFom = fom, + generellTom = tom, + utvidetFom = utvidetFom, + utvidetTom = utvidetTom, + barna = listOf(barn), + søker = søker, + ) + + Assertions.assertEquals(7, andelerTilkjentYtelse.size) + + val (andelerSøker, andelerBarn) = andelerTilkjentYtelse.partition { it.aktør.aktivFødselsnummer() == søker.aktør.aktivFødselsnummer() } + + Assertions.assertEquals(3, andelerBarn.size) + // Barn før endringsperiode + Assertions.assertEquals(barn.aktør, andelerBarn[0].aktør) + Assertions.assertEquals(BigDecimal(100), andelerBarn[0].prosent) + Assertions.assertEquals(fom.plusMonths(1).toYearMonth(), andelerBarn[0].stønadFom) + Assertions.assertEquals(endretUtbetalingAndelFom.minusMonths(1), andelerBarn[0].stønadTom) + + // Barn under endringsperiode + Assertions.assertEquals(barn.aktør, andelerBarn[1].aktør) + Assertions.assertEquals(BigDecimal.ZERO, andelerBarn[1].prosent) + Assertions.assertEquals(endretUtbetalingAndelFom, andelerBarn[1].stønadFom) + Assertions.assertEquals(endretUtbetalingAndelTom, andelerBarn[1].stønadTom) + + // Barn etter endringsperiode + Assertions.assertEquals(barn.aktør, andelerBarn[2].aktør) + Assertions.assertEquals(BigDecimal(100), andelerBarn[2].prosent) + Assertions.assertEquals(endretUtbetalingAndelTom.plusMonths(1), andelerBarn[2].stønadFom) + Assertions.assertEquals(tom.toYearMonth(), andelerBarn[2].stønadTom) + + Assertions.assertEquals(4, andelerSøker.size) + + val (andelerUtvidet, andelerSmåbarnstillegg) = andelerSøker.partition { it.erUtvidet() } + + Assertions.assertEquals(2, andelerUtvidet.size) + // Søker - utvidet under endringsperiode + Assertions.assertEquals(søker.aktør, andelerUtvidet[0].aktør) + Assertions.assertEquals(BigDecimal.ZERO, andelerUtvidet[0].prosent) + Assertions.assertEquals(utvidetFom.plusMonths(1).toYearMonth(), andelerUtvidet[0].stønadFom) + Assertions.assertEquals(endretUtbetalingAndelTom, andelerUtvidet[0].stønadTom) + + // Søker - utvidet etter endringsperiode + Assertions.assertEquals(søker.aktør, andelerUtvidet[1].aktør) + Assertions.assertEquals(BigDecimal(100), andelerUtvidet[1].prosent) + Assertions.assertEquals(endretUtbetalingAndelTom.plusMonths(1), andelerUtvidet[1].stønadFom) + Assertions.assertEquals(utvidetTom.toYearMonth(), andelerUtvidet[1].stønadTom) + + Assertions.assertEquals(2, andelerSmåbarnstillegg.size) + // Søker - småbarnstillegg under endringsperiode + Assertions.assertEquals(søker.aktør, andelerSmåbarnstillegg[0].aktør) + Assertions.assertEquals(BigDecimal.ZERO, andelerSmåbarnstillegg[0].prosent) + Assertions.assertEquals(utvidetFom.plusMonths(1).toYearMonth(), andelerSmåbarnstillegg[0].stønadFom) + Assertions.assertEquals(endretUtbetalingAndelTom, andelerSmåbarnstillegg[0].stønadTom) + + // Søker - småbarnstillegg etter endringsperiode + Assertions.assertEquals(søker.aktør, andelerSmåbarnstillegg[1].aktør) + Assertions.assertEquals(BigDecimal(100), andelerSmåbarnstillegg[1].prosent) + Assertions.assertEquals(endretUtbetalingAndelTom.plusMonths(1), andelerSmåbarnstillegg[1].stønadFom) + Assertions.assertEquals(utvidetTom.toYearMonth(), andelerSmåbarnstillegg[1].stønadTom) + } + + @Test + fun `Skal få utvidet, men ikke småbarnstillegg hvis to barn, men barn under 3 år har endringsperiode med årsak=etterbetaling 3 år`() { + val barnUnder3År = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(2)) + val barnOver3År = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(7)) + val søker = lagPerson(type = PersonType.SØKER, fødselsdato = LocalDate.now().minusYears(31)) + + val fom = LocalDate.now().minusMonths(8) + val tom = LocalDate.now().plusYears(3) + + val utvidetFom = LocalDate.now().minusMonths(3) + val utvidetTom = LocalDate.now().plusMonths(5) + + val endretUtbetalingAndelFom = fom.plusMonths(1).toYearMonth() + val endretUtbetalingAndelTom = utvidetTom.minusMonths(2).toYearMonth() + + val andelerTilkjentYtelse = genererAndelerTilkjentYtelseForScenario( + endretUtbetalingÅrsak = Årsak.ETTERBETALING_3ÅR, + endretUtbetalingProsent = BigDecimal.ZERO, + endretUtbetalingFom = endretUtbetalingAndelFom, + endretUtbetalingTom = endretUtbetalingAndelTom, + endretUtbetalingPerson = barnUnder3År, + generellFom = fom, + generellTom = tom, + utvidetFom = utvidetFom, + utvidetTom = utvidetTom, + barna = listOf(barnUnder3År, barnOver3År), + søker = søker, + ) + + val (andelerSøker, andelerBarn) = andelerTilkjentYtelse.partition { it.aktør.aktivFødselsnummer() == søker.aktør.aktivFødselsnummer() } + + // BARNA + val (andelerBarnUnder3År, andelerBarnOver3År) = andelerBarn.partition { it.aktør.aktivFødselsnummer() == barnUnder3År.aktør.aktivFødselsnummer() } + + Assertions.assertEquals(2, andelerBarnUnder3År.size) + // Barn (under 3 år) under endringsperiode + Assertions.assertEquals(barnUnder3År.aktør, andelerBarnUnder3År[0].aktør) + Assertions.assertEquals(BigDecimal.ZERO, andelerBarnUnder3År[0].prosent) + Assertions.assertEquals(endretUtbetalingAndelFom, andelerBarnUnder3År[0].stønadFom) + Assertions.assertEquals(endretUtbetalingAndelTom, andelerBarnUnder3År[0].stønadTom) + + // Barn (under 3 år) etter endringsperiode + Assertions.assertEquals(barnUnder3År.aktør, andelerBarnUnder3År[1].aktør) + Assertions.assertEquals(BigDecimal(100), andelerBarnUnder3År[1].prosent) + Assertions.assertEquals(endretUtbetalingAndelTom.plusMonths(1), andelerBarnUnder3År[1].stønadFom) + Assertions.assertEquals(tom.toYearMonth(), andelerBarnUnder3År[1].stønadTom) + + val andelerBarnOver3ÅrEtterIRelevantPeriode = + andelerBarnOver3År.filter { it.stønadTom.isAfter(fom.toYearMonth()) } + + // Barn over 3 år har ingen endringer og derfor bare 1 andel i perioden vi ser på + Assertions.assertEquals(1, andelerBarnOver3ÅrEtterIRelevantPeriode.size) + + // SØKER + Assertions.assertEquals(3, andelerSøker.size) + + val (andelerUtvidet, andelerSmåbarnstillegg) = andelerSøker.partition { it.erUtvidet() } + + Assertions.assertEquals(1, andelerUtvidet.size) + + // Søker får utvidet hele perioden pga barn over 3 år + Assertions.assertEquals(søker.aktør, andelerUtvidet.single().aktør) + Assertions.assertEquals(BigDecimal(100), andelerUtvidet.single().prosent) + Assertions.assertEquals(utvidetFom.toYearMonth().plusMonths(1), andelerUtvidet.single().stønadFom) + Assertions.assertEquals(utvidetTom.toYearMonth(), andelerUtvidet.single().stønadTom) + + Assertions.assertEquals(2, andelerSmåbarnstillegg.size) + // Søker - småbarnstillegg under endringsperiode + Assertions.assertEquals(søker.aktør, andelerSmåbarnstillegg[0].aktør) + Assertions.assertEquals(BigDecimal.ZERO, andelerSmåbarnstillegg[0].prosent) + Assertions.assertEquals(utvidetFom.plusMonths(1).toYearMonth(), andelerSmåbarnstillegg[0].stønadFom) + Assertions.assertEquals(endretUtbetalingAndelTom, andelerSmåbarnstillegg[0].stønadTom) + + // Søker - småbarnstillegg etter endringsperiode + Assertions.assertEquals(søker.aktør, andelerSmåbarnstillegg[1].aktør) + Assertions.assertEquals(BigDecimal(100), andelerSmåbarnstillegg[1].prosent) + Assertions.assertEquals(endretUtbetalingAndelTom.plusMonths(1), andelerSmåbarnstillegg[1].stønadFom) + Assertions.assertEquals(utvidetTom.toYearMonth(), andelerSmåbarnstillegg[1].stønadTom) + } + + @Test + fun `Dersom barn har flere godkjente perioderesultat back2back skal det ikke bli glippe i andel ytelser`() { + val førstePeriodeTomForBarnet = LocalDate.of(2020, 11, 30) + kjørScenarioForBack2Backtester( + førstePeriodeTomForBarnet = førstePeriodeTomForBarnet, + andrePeriodeFomForBarnet = førstePeriodeTomForBarnet.plusDays(1), + forventetSluttForFørsteAndelsperiode = førstePeriodeTomForBarnet.plusMonths(1).toYearMonth(), + forventetStartForAndreAndelsperiode = førstePeriodeTomForBarnet.plusMonths(2).toYearMonth(), + skalLageSplitt = false, + ) + } + + @Test + fun `Dersom barn har flere godkjente perioderesultat som ikke følger back2back skal det bli glippe på en måned i andel ytelser`() { + val førstePeriodeTomForBarnet = LocalDate.of(2020, 11, 29) + kjørScenarioForBack2Backtester( + førstePeriodeTomForBarnet = førstePeriodeTomForBarnet, + andrePeriodeFomForBarnet = førstePeriodeTomForBarnet.plusDays(2), + forventetSluttForFørsteAndelsperiode = førstePeriodeTomForBarnet.toYearMonth(), + forventetStartForAndreAndelsperiode = førstePeriodeTomForBarnet.plusMonths(2).toYearMonth(), + skalLageSplitt = true, + ) + } + + @Test + fun `Dersom barn har flere godkjente perioderesultat back2back og delt bosted kun i første periode skal det ikke bli glippe i andel ytelser men beløpsendringen skal inntreffe som normalt neste måned`() { + val førstePeriodeTomForBarnet = LocalDate.of(2020, 11, 30) + kjørScenarioForBack2Backtester( + førstePeriodeTomForBarnet = førstePeriodeTomForBarnet, + andrePeriodeFomForBarnet = LocalDate.of(2020, 12, 1), + forventetSluttForFørsteAndelsperiode = førstePeriodeTomForBarnet.toYearMonth(), + forventetStartForAndreAndelsperiode = førstePeriodeTomForBarnet.plusMonths(1).toYearMonth(), + deltBostedForFørstePeriode = true, + skalLageSplitt = true, + ) + } + + @Test + fun `Dersom barn har flere godkjente perioderesultat back2back og delt bosted kun i andre periode skal det ikke bli glippe i andel ytelser men beløpsendringen skal inntreffe som normalt neste måned`() { + val førstePeriodeTomForBarnet = LocalDate.of(2020, 11, 30) + kjørScenarioForBack2Backtester( + førstePeriodeTomForBarnet = førstePeriodeTomForBarnet, + andrePeriodeFomForBarnet = LocalDate.of(2020, 12, 1), + forventetSluttForFørsteAndelsperiode = førstePeriodeTomForBarnet.plusMonths(1).toYearMonth(), + forventetStartForAndreAndelsperiode = førstePeriodeTomForBarnet.plusMonths(2).toYearMonth(), + deltBostedForAndrePeriode = true, + skalLageSplitt = true, + ) + } + + @Test + fun `Dersom barn har flere godkjente perioderesultat back2back der alle er delt bosted skal det ikke bli glippe i andel ytelser`() { + val førstePeriodeTomForBarnet = LocalDate.of(2020, 11, 30) + kjørScenarioForBack2Backtester( + førstePeriodeTomForBarnet = førstePeriodeTomForBarnet, + andrePeriodeFomForBarnet = førstePeriodeTomForBarnet.plusDays(1), + forventetSluttForFørsteAndelsperiode = førstePeriodeTomForBarnet.plusMonths(1).toYearMonth(), + forventetStartForAndreAndelsperiode = førstePeriodeTomForBarnet.plusMonths(2).toYearMonth(), + deltBostedForFørstePeriode = true, + deltBostedForAndrePeriode = true, + skalLageSplitt = false, + ) + } + + @Test + fun `erEndringerIUtbetalingMellomNåværendeOgForrigeBehandling skal returnere INGEN_ENDRING_I_UTBETALING dersom det utbetalingsbeløpene er like mellom nåværende og forrige behandling`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1000, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1000, + aktør = barn2Aktør, + ), + ) + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1000, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1000, + aktør = barn2Aktør, + ), + ) + + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(nåværendeBehandling) } returns forrigeBehandling + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(nåværendeBehandling.id) } returns nåværendeAndeler + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(forrigeBehandling.id) } returns forrigeAndeler + + val erEndringIUtbetaling = + beregningService.hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(nåværendeBehandling) + + Assertions.assertEquals(erEndringIUtbetaling, EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING) + + verify { behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(nåværendeBehandling) } + verify { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(nåværendeBehandling.id) } + verify { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(forrigeBehandling.id) } + } + + @Test + fun `erEndringerIUtbetalingMellomNåværendeOgForrigeBehandling skal returnere INGEN_ENDRING_I_UTBETALING dersom man har gått fra andeler med 0 i beløp til ingen andeler`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 0, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 0, + aktør = barn2Aktør, + ), + ) + + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(nåværendeBehandling) } returns forrigeBehandling + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(nåværendeBehandling.id) } returns emptyList() + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(forrigeBehandling.id) } returns forrigeAndeler + + val erEndringIUtbetaling = + beregningService.hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(nåværendeBehandling) + + Assertions.assertEquals(erEndringIUtbetaling, EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING) + + verify { behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(nåværendeBehandling) } + verify { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(nåværendeBehandling.id) } + verify { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(forrigeBehandling.id) } + } + + @Test + fun `erEndringerIUtbetalingMellomNåværendeOgForrigeBehandling skal returnere INGEN_ENDRING_I_UTBETALING dersom man har gått fra ingen andeler til andeler med 0 i beløp`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 0, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 0, + aktør = barn2Aktør, + ), + ) + + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(nåværendeBehandling) } returns forrigeBehandling + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(nåværendeBehandling.id) } returns nåværendeAndeler + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(forrigeBehandling.id) } returns emptyList() + + val erEndringIUtbetaling = + beregningService.hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(nåværendeBehandling) + + Assertions.assertEquals(erEndringIUtbetaling, EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING) + + verify { behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(nåværendeBehandling) } + verify { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(nåværendeBehandling.id) } + verify { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(forrigeBehandling.id) } + } + + @Test + fun `erEndringerIUtbetalingMellomNåværendeOgForrigeBehandling skal returnere ENDRING_I_UTBETALING dersom man har gått fra ingen andeler til andeler med over 0 i beløp`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1000, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1000, + aktør = barn2Aktør, + ), + ) + + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(nåværendeBehandling) } returns forrigeBehandling + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(nåværendeBehandling.id) } returns nåværendeAndeler + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(forrigeBehandling.id) } returns emptyList() + + val erEndringIUtbetaling = + beregningService.hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(nåværendeBehandling) + + Assertions.assertEquals(erEndringIUtbetaling, EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING) + + verify { behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(nåværendeBehandling) } + verify { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(nåværendeBehandling.id) } + verify { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(forrigeBehandling.id) } + } + + @Test + fun `erEndringerIUtbetalingMellomNåværendeOgForrigeBehandling skal returnere ENDRING_I_UTBETALING dersom man fikk beløp over 0 i forrige behandling og det har forandret på seg`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1000, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1000, + aktør = barn2Aktør, + ), + ) + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 500, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1000, + aktør = barn2Aktør, + ), + ) + + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(nåværendeBehandling) } returns forrigeBehandling + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(nåværendeBehandling.id) } returns nåværendeAndeler + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(forrigeBehandling.id) } returns forrigeAndeler + + val erEndringIUtbetaling = + beregningService.hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(nåværendeBehandling) + + Assertions.assertEquals(erEndringIUtbetaling, EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING) + + verify { behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(nåværendeBehandling) } + verify { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(nåværendeBehandling.id) } + verify { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(forrigeBehandling.id) } + } + + private fun kjørScenarioForBack2Backtester( + førstePeriodeTomForBarnet: LocalDate, + andrePeriodeFomForBarnet: LocalDate, + forventetSluttForFørsteAndelsperiode: YearMonth, + forventetStartForAndreAndelsperiode: YearMonth, + deltBostedForFørstePeriode: Boolean = false, + deltBostedForAndrePeriode: Boolean = false, + skalLageSplitt: Boolean, + ) { + val behandling = lagBehandling() + val barnFødselsdato = LocalDate.of(2019, 1, 1) + val barn = lagPerson(type = PersonType.BARN, fødselsdato = barnFødselsdato) + val søker = lagPerson(type = PersonType.SØKER) + val vilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + ) + + val førstePeriodeFomForBarnet = LocalDate.of(2020, 1, 1) + val andrePeriodeTomForBarnet = LocalDate.of(2021, 12, 11) + + // Den godkjente perioden for søker er ikke så relevant for testen annet enn at den settes før og etter periodene som brukes for barnet. + val periodeTomForSøker = andrePeriodeTomForBarnet.plusMonths(1) + + val førsteSatsendringFom = + SatsService.hentDatoForSatsendring(satstype = SatsType.TILLEGG_ORBA, oppdatertBeløp = 1354)!! + val andreSatsendringFom = + SatsService.hentDatoForSatsendring(satstype = SatsType.TILLEGG_ORBA, oppdatertBeløp = 1654)!! + + val personResultatSøker = + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = søker, + resultat = Resultat.OPPFYLT, + periodeFom = førstePeriodeFomForBarnet, + periodeTom = periodeTomForSøker, + lagFullstendigVilkårResultat = true, + personType = PersonType.SØKER, + ) + val personResultatBarn = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn.aktør, + ) + + val vilkårForBarn = Vilkår.hentVilkårFor( + personType = PersonType.BARN, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ) + val vilkårResultaterBarn = + vilkårForBarn.lagVilkårResultaterForPerson( + fom = førstePeriodeFomForBarnet, + tom = førstePeriodeTomForBarnet, + erDeltBosted = deltBostedForFørstePeriode, + ) + vilkårForBarn.lagVilkårResultaterForPerson( + fom = andrePeriodeFomForBarnet, + tom = andrePeriodeTomForBarnet, + erDeltBosted = deltBostedForAndrePeriode, + ) + + personResultatBarn.setSortedVilkårResultater(vilkårResultaterBarn.toSet()) + vilkårsvurdering.personResultater = setOf(personResultatSøker, personResultatBarn) + + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søker.aktør.aktivFødselsnummer(), + barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), + barnasFødselsdatoer = listOf(barnFødselsdato), + ) + val slot = slot() + + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(any()) } answers { vilkårsvurdering } + every { tilkjentYtelseRepository.save(any()) } returns lagInitiellTilkjentYtelse(behandling) + every { søknadGrunnlagService.hentAktiv(any())?.hentSøknadDto() } returns lagSøknadDTO( + søker.aktør.aktivFødselsnummer(), + listOf(barn.aktør.aktivFødselsnummer()), + ) + + beregningService.oppdaterBehandlingMedBeregning( + behandling = behandling, + personopplysningGrunnlag = personopplysningGrunnlag, + ) + + verify(exactly = 1) { tilkjentYtelseRepository.save(capture(slot)) } + + Assertions.assertEquals(if (skalLageSplitt) 4 else 3, slot.captured.andelerTilkjentYtelse.size) + val andelerTilkjentYtelse = slot.captured.andelerTilkjentYtelse.sortedBy { it.stønadTom } + + // Første periode (før satsendring) + Assertions.assertEquals(førstePeriodeFomForBarnet.nesteMåned(), andelerTilkjentYtelse[0].stønadFom) + Assertions.assertEquals(førsteSatsendringFom.forrigeMåned(), andelerTilkjentYtelse[0].stønadTom) + Assertions.assertEquals( + if (deltBostedForFørstePeriode) 527 else 1054, + andelerTilkjentYtelse[0].kalkulertUtbetalingsbeløp, + ) + if (skalLageSplitt) { + // Andre periode (fra første satsendring til slutt av første godkjente perioderesultat for barnet) + Assertions.assertEquals(førsteSatsendringFom.toYearMonth(), andelerTilkjentYtelse[1].stønadFom) + Assertions.assertEquals(forventetSluttForFørsteAndelsperiode, andelerTilkjentYtelse[1].stønadTom) + Assertions.assertEquals( + if (deltBostedForFørstePeriode) 677 else 1354, + andelerTilkjentYtelse[1].kalkulertUtbetalingsbeløp, + ) + + // Tredje periode (fra start av andre godkjente perioderesultat for barnet til neste satsendring). + // At denne perioden følger back2back med tom for forrige periode er primært det som testes her. + Assertions.assertEquals(forventetStartForAndreAndelsperiode, andelerTilkjentYtelse[2].stønadFom) + Assertions.assertEquals(andreSatsendringFom.forrigeMåned(), andelerTilkjentYtelse[2].stønadTom) + Assertions.assertEquals( + if (deltBostedForAndrePeriode) 677 else 1354, + andelerTilkjentYtelse[2].kalkulertUtbetalingsbeløp, + ) + } else { + Assertions.assertEquals(førsteSatsendringFom.toYearMonth(), andelerTilkjentYtelse[1].stønadFom) + Assertions.assertEquals(andreSatsendringFom.forrigeMåned(), andelerTilkjentYtelse[1].stønadTom) + Assertions.assertEquals( + if (deltBostedForFørstePeriode) 677 else 1354, + andelerTilkjentYtelse[1].kalkulertUtbetalingsbeløp, + ) + } + + val sisteAndel = if (skalLageSplitt) andelerTilkjentYtelse[3] else andelerTilkjentYtelse[2] + // Siste periode (fra siste satsendring til slutt av endre godkjente perioderesultat for barnet) + Assertions.assertEquals(andreSatsendringFom.toYearMonth(), sisteAndel.stønadFom) + Assertions.assertEquals(andrePeriodeTomForBarnet.toYearMonth(), sisteAndel.stønadTom) + Assertions.assertEquals( + if (deltBostedForAndrePeriode) 827 else 1654, + sisteAndel.kalkulertUtbetalingsbeløp, + ) + } + + private fun Set.lagVilkårResultaterForPerson( + fom: LocalDate, + tom: LocalDate, + erDeltBosted: Boolean, + ): List = + this.map { + lagVilkårResultat( + vilkårType = it, + periodeFom = fom, + periodeTom = tom, + utdypendeVilkårsvurderinger = listOfNotNull( + when { + erDeltBosted && it == Vilkår.BOR_MED_SØKER -> UtdypendeVilkårsvurdering.DELT_BOSTED + else -> null + }, + ), + ) + } + + private fun genererAndelerTilkjentYtelseForScenario( + endretUtbetalingÅrsak: Årsak, + endretUtbetalingProsent: BigDecimal, + endretUtbetalingFom: YearMonth, + endretUtbetalingTom: YearMonth, + endretUtbetalingPerson: Person, + generellFom: LocalDate, + generellTom: LocalDate, + utvidetFom: LocalDate, + utvidetTom: LocalDate, + barna: List, + søker: Person, + ): List { + val behandling = lagBehandling() + val vilkårsvurdering = + lagVilkårsvurdering(søkerAktør = søker.aktør, behandling = behandling, resultat = Resultat.OPPFYLT) + + val søkerPersonResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = søker.aktør, + ) + + søkerPersonResultat.setSortedVilkårResultater( + setOf( + VilkårResultat( + personResultat = søkerPersonResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = generellFom, + periodeTom = generellTom, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + VilkårResultat( + personResultat = søkerPersonResultat, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + periodeFom = generellFom, + periodeTom = generellTom, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + VilkårResultat( + personResultat = søkerPersonResultat, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + resultat = Resultat.OPPFYLT, + periodeFom = utvidetFom, + periodeTom = utvidetTom, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + ), + ) + + val personResultatBarna = barna.map { barn -> + val barnPersonResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn.aktør, + ) + + barnPersonResultat.setSortedVilkårResultater( + setOf( + VilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = generellFom, + periodeTom = generellTom, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + VilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + periodeFom = generellFom, + periodeTom = generellTom, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + VilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + periodeFom = generellFom, + periodeTom = generellTom, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + utdypendeVilkårsvurderinger = if (endretUtbetalingÅrsak == Årsak.DELT_BOSTED) { + listOf( + UtdypendeVilkårsvurdering.DELT_BOSTED, + ) + } else { + emptyList() + }, + ), + VilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.GIFT_PARTNERSKAP, + resultat = Resultat.OPPFYLT, + periodeFom = generellFom, + periodeTom = generellTom, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + VilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.UNDER_18_ÅR, + resultat = Resultat.OPPFYLT, + periodeFom = barn.fødselsdato, + periodeTom = barn.fødselsdato.plusYears(18), + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + ), + ) + barnPersonResultat + } + + vilkårsvurdering.personResultater = personResultatBarna.toSet() + setOf(søkerPersonResultat) + + val endretUtbetalingAndel = + EndretUtbetalingAndel( + behandlingId = behandling.id, + person = endretUtbetalingPerson, + fom = endretUtbetalingFom, + tom = endretUtbetalingTom, + årsak = endretUtbetalingÅrsak, + prosent = endretUtbetalingProsent, + avtaletidspunktDeltBosted = LocalDate.now().minusMonths(1), + søknadstidspunkt = LocalDate.now(), + begrunnelse = "Dette er en begrunnelse", + ) + + val personopplysningGrunnlag = PersonopplysningGrunnlag( + behandlingId = behandling.id, + personer = (barna + søker).toMutableSet(), + ) + + val slot = slot() + + every { endretUtbetalingAndelRepository.findByBehandlingId(behandlingId = behandling.id) } answers { + listOf( + endretUtbetalingAndel, + ) + } + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(behandlingId = behandling.id) } answers { vilkårsvurdering } + every { tilkjentYtelseRepository.save(any()) } returns lagInitiellTilkjentYtelse(behandling) + every { småbarnstilleggService.hentOgLagrePerioderMedOvergangsstønadForBehandling(any(), any()) } just Runs + every { småbarnstilleggService.hentPerioderMedFullOvergangsstønad(any()) } answers { + listOf( + InternPeriodeOvergangsstønad( + personIdent = søker.aktør.aktivFødselsnummer(), + fomDato = utvidetFom, + tomDato = utvidetTom, + ), + ) + } + + beregningService.oppdaterBehandlingMedBeregning( + behandling = behandling, + personopplysningGrunnlag = personopplysningGrunnlag, + nyEndretUtbetalingAndel = endretUtbetalingAndel, + ) + verify(exactly = 1) { tilkjentYtelseRepository.save(capture(slot)) } + return slot.captured.andelerTilkjentYtelse.sortedBy { it.stønadTom } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningTestUtils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningTestUtils.kt new file mode 100644 index 000000000..33b54abca --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningTestUtils.kt @@ -0,0 +1,193 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.kontrakter.felles.oppdrag.Opphør +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsperiode +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.UUID + +fun lagTestUtbetalingsoppdragForFGBMedToBarn( + personIdent: String, + fagsakId: String, + behandlingId: Long, + vedtakDato: LocalDate, + datoFomBarn1: LocalDate, + datoFomBarn2: LocalDate, + datoTomBarn1: LocalDate, + datoTomBarn2: LocalDate, +): Utbetalingsoppdrag { + return Utbetalingsoppdrag( + Utbetalingsoppdrag.KodeEndring.NY, + "BA", + fagsakId, + UUID.randomUUID().toString(), + "SAKSBEHANDLERID", + LocalDateTime.now(), + listOf( + Utbetalingsperiode( + false, + null, + 1, + null, + vedtakDato, + "BATR", + datoFomBarn1, + datoTomBarn1, + BigDecimal(1054), + Utbetalingsperiode.SatsType.MND, + personIdent, + behandlingId, + ), + Utbetalingsperiode( + false, + null, + 2, + null, + vedtakDato, + "BATR", + datoFomBarn2, + datoTomBarn2, + BigDecimal(1054), + Utbetalingsperiode.SatsType.MND, + personIdent, + behandlingId, + ), + ), + ) +} + +fun lagTestUtbetalingsoppdragForOpphørMedToBarn( + personIdent: String, + fagsakId: String, + behandlingId: Long, + vedtakDato: LocalDate, + datoFomBarn1: LocalDate, + datoFomBarn2: LocalDate, + datoTomBarn1: LocalDate, + datoTomBarn2: LocalDate, + opphørFom: LocalDate, +): Utbetalingsoppdrag { + return Utbetalingsoppdrag( + Utbetalingsoppdrag.KodeEndring.NY, + "BA", + fagsakId, + UUID.randomUUID().toString(), + "SAKSBEHANDLERID", + LocalDateTime.now(), + listOf( + Utbetalingsperiode( + true, + Opphør(opphørFom), + 1, + null, + vedtakDato, + "BATR", + datoFomBarn1, + datoTomBarn1, + BigDecimal(1054), + Utbetalingsperiode.SatsType.MND, + personIdent, + behandlingId, + ), + Utbetalingsperiode( + true, + Opphør(opphørFom), + 2, + null, + vedtakDato, + "BATR", + datoFomBarn2, + datoTomBarn2, + BigDecimal(1054), + Utbetalingsperiode.SatsType.MND, + personIdent, + behandlingId, + ), + ), + ) +} + +fun lagTestUtbetalingsoppdragForRevurderingMedToBarn( + personIdent: String, + fagsakId: String, + behandlingId: Long, + forrigeBehandlingId: Long, + vedtakDato: LocalDate, + opphørFomBarn1: LocalDate, + revurderingFomBarn1: LocalDate, + datoFomBarn1: LocalDate, + datoTomBarn1: LocalDate, + opphørFomBarn2: LocalDate, + revurderingFomBarn2: LocalDate, + datoFomBarn2: LocalDate, + datoTomBarn2: LocalDate, +): Utbetalingsoppdrag { + return Utbetalingsoppdrag( + Utbetalingsoppdrag.KodeEndring.NY, + "BA", + fagsakId, + UUID.randomUUID().toString(), + "SAKSBEHANDLERID", + LocalDateTime.now(), + listOf( + Utbetalingsperiode( + true, + Opphør(opphørFomBarn1), + 1, + null, + vedtakDato, + "BATR", + datoFomBarn1, + datoTomBarn1, + BigDecimal(1054), + Utbetalingsperiode.SatsType.MND, + personIdent, + forrigeBehandlingId, + ), + Utbetalingsperiode( + false, + null, + 3, + 1, + vedtakDato, + "BATR", + revurderingFomBarn1, + datoTomBarn1, + BigDecimal(1054), + Utbetalingsperiode.SatsType.MND, + personIdent, + behandlingId, + ), + Utbetalingsperiode( + true, + Opphør(opphørFomBarn2), + 2, + null, + vedtakDato, + "BATR", + datoFomBarn2, + datoTomBarn2, + BigDecimal(1054), + Utbetalingsperiode.SatsType.MND, + personIdent, + forrigeBehandlingId, + ), + Utbetalingsperiode( + false, + null, + 4, + 2, + vedtakDato, + "BATR", + revurderingFomBarn2, + datoTomBarn2, + BigDecimal(1054), + Utbetalingsperiode.SatsType.MND, + personIdent, + behandlingId, + ), + ), + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/InternPeriodeOvergangsst\303\270nadTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/InternPeriodeOvergangsst\303\270nadTest.kt" new file mode 100644 index 000000000..8de8928d1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/InternPeriodeOvergangsst\303\270nadTest.kt" @@ -0,0 +1,50 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.beregning.domene.slåSammenSammenhengendePerioder +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class InternPeriodeOvergangsstønadTest { + @Test + fun `Skal slå sammen perioder som er sammenhengende`() { + val personIdent = randomFnr() + val sammenslåttePerioder = listOf( + InternPeriodeOvergangsstønad( + fomDato = LocalDate.now().minusMonths(6).førsteDagIInneværendeMåned(), + tomDato = LocalDate.now().minusMonths(3).sisteDagIMåned(), + personIdent = personIdent, + ), + InternPeriodeOvergangsstønad( + fomDato = LocalDate.now().minusMonths(2).førsteDagIInneværendeMåned(), + tomDato = LocalDate.now().sisteDagIMåned(), + personIdent = personIdent, + ), + ).slåSammenSammenhengendePerioder() + + assertEquals(1, sammenslåttePerioder.size) + } + + @Test + fun `Skal ikke slå sammen perioder som ikke er sammenhengende`() { + val personIdent = randomFnr() + val sammenslåttePerioder = listOf( + InternPeriodeOvergangsstønad( + fomDato = LocalDate.now().minusMonths(6).førsteDagIInneværendeMåned(), + tomDato = LocalDate.now().minusMonths(4).sisteDagIMåned(), + personIdent = personIdent, + ), + InternPeriodeOvergangsstønad( + fomDato = LocalDate.now().minusMonths(2).førsteDagIInneværendeMåned(), + tomDato = LocalDate.now().sisteDagIMåned(), + personIdent = personIdent, + ), + ).slåSammenSammenhengendePerioder() + + assertEquals(2, sammenslåttePerioder.size) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Ordin\303\246rBarnetrygdUtilTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Ordin\303\246rBarnetrygdUtilTest.kt" new file mode 100644 index 000000000..5ea6569e1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Ordin\303\246rBarnetrygdUtilTest.kt" @@ -0,0 +1,234 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.forrigeMåned +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.OrdinærBarnetrygdUtil.mapTilProsentEllerNull +import no.nav.familie.ba.sak.kjerne.beregning.OrdinærBarnetrygdUtil.tilTidslinjeMedRettTilProsentForPerson +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.gjelderAlltidFraBarnetsFødselsdato +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class OrdinærBarnetrygdUtilTest { + + @Test + fun `Skal lage riktig tidslinje med rett til prosent for person med start og stopp av delt bosted`() { + val barn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(9)) + val vilkårsvurdering = Vilkårsvurdering(behandling = lagBehandling()) + + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn.aktør, + ) + + val generellVilkårFom = LocalDate.now().minusYears(3) + val borMedSøkerVilkårFom = LocalDate.now().minusYears(2) + val borMedSøkerVilkårTom = LocalDate.now() + val startPåYtelse = generellVilkårFom.plusMonths(1).toYearMonth() + val rettTilDeltFom = borMedSøkerVilkårFom.plusMonths(1).toYearMonth() + val rettTilDeltTom = borMedSøkerVilkårTom.toYearMonth() + val månedFørFylte18År = barn.fødselsdato.plusYears(18).forrigeMåned() + + val vilkårResulater = Vilkår.hentVilkårFor(personType = PersonType.BARN, fagsakType = FagsakType.NORMAL, behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR).mapNotNull { + if (it == Vilkår.BOR_MED_SØKER) { + null + } else { + lagVilkårResultat( + personResultat = personResultat, + periodeFom = if (it.gjelderAlltidFraBarnetsFødselsdato()) barn.fødselsdato else generellVilkårFom, + periodeTom = null, + resultat = Resultat.OPPFYLT, + vilkårType = it, + ) + } + }.toSet() + + val borMedSøkerVilkår = listOf( + lagVilkårResultat( + personResultat = personResultat, + periodeFom = generellVilkårFom, + periodeTom = borMedSøkerVilkårFom.minusMonths(1).sisteDagIMåned(), + resultat = Resultat.OPPFYLT, + vilkårType = Vilkår.BOR_MED_SØKER, + ), + lagVilkårResultat( + personResultat = personResultat, + periodeFom = borMedSøkerVilkårFom.førsteDagIInneværendeMåned(), + periodeTom = borMedSøkerVilkårTom.sisteDagIMåned(), + resultat = Resultat.OPPFYLT, + vilkårType = Vilkår.BOR_MED_SØKER, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + lagVilkårResultat( + personResultat = personResultat, + periodeFom = borMedSøkerVilkårTom.plusMonths(1).førsteDagIInneværendeMåned(), + periodeTom = null, + resultat = Resultat.OPPFYLT, + vilkårType = Vilkår.BOR_MED_SØKER, + ), + ) + + personResultat.setSortedVilkårResultater(vilkårResulater + borMedSøkerVilkår) + + val tidslinje = personResultat.tilTidslinjeMedRettTilProsentForPerson( + person = barn, + fagsakType = FagsakType.NORMAL, + ) + + val perioder = tidslinje.perioder().toList() + + assertEquals(3, perioder.size) + + val periode1 = perioder[0] + val periode2 = perioder[1] + val periode3 = perioder[2] + + assertProsentPeriode( + forventetFom = startPåYtelse, + forventetTom = rettTilDeltFom.minusMonths(1), + forventetProsent = BigDecimal(100), + faktisk = periode1, + ) + assertProsentPeriode( + forventetFom = rettTilDeltFom, + forventetTom = rettTilDeltTom, + forventetProsent = BigDecimal(50), + faktisk = periode2, + ) + assertProsentPeriode( + forventetFom = rettTilDeltTom.plusMonths(1), + forventetTom = månedFørFylte18År, + forventetProsent = BigDecimal(100), + faktisk = periode3, + ) + } + + @Test + fun `Skal returnere 50 prosent hvis vilkårsvurderingen har delt bosted i perioden`() { + val barn = lagPerson(type = PersonType.BARN) + val personResultat = PersonResultat( + vilkårsvurdering = Vilkårsvurdering(behandling = lagBehandling()), + aktør = barn.aktør, + ) + val vilkårResultater = Vilkår.hentVilkårFor(personType = PersonType.BARN, fagsakType = FagsakType.NORMAL, behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR).map { + lagVilkårResultat( + vilkårType = it, + periodeFom = LocalDate.now().minusMonths(5), + periodeTom = null, + resultat = Resultat.OPPFYLT, + utdypendeVilkårsvurderinger = if (it == Vilkår.BOR_MED_SØKER) listOf(UtdypendeVilkårsvurdering.DELT_BOSTED) else emptyList(), + personResultat = personResultat, + ) + } + + val prosent = vilkårResultater.mapTilProsentEllerNull(personType = PersonType.BARN, fagsakType = FagsakType.NORMAL) + + assertEquals(BigDecimal(50), prosent) + } + + @Test + fun `Skal returnere 100 prosent hvis vilkårsvurderingen ikke har delt bosted i perioden`() { + val barn = lagPerson(type = PersonType.BARN) + val personResultat = PersonResultat( + vilkårsvurdering = Vilkårsvurdering(behandling = lagBehandling()), + aktør = barn.aktør, + ) + val vilkårResultater = Vilkår.hentVilkårFor(personType = PersonType.BARN, fagsakType = FagsakType.NORMAL, behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR).map { + lagVilkårResultat( + vilkårType = it, + periodeFom = LocalDate.now().minusMonths(5), + periodeTom = null, + resultat = Resultat.OPPFYLT, + utdypendeVilkårsvurderinger = emptyList(), + personResultat = personResultat, + ) + } + + val prosent = vilkårResultater.mapTilProsentEllerNull(personType = PersonType.BARN, fagsakType = FagsakType.NORMAL) + + assertEquals(BigDecimal(100), prosent) + } + + @Test + fun `Skal returnere null hvis ikke alle vilkår for barn er oppfylt`() { + val barn = lagPerson(type = PersonType.BARN) + val personResultat = PersonResultat( + vilkårsvurdering = Vilkårsvurdering(behandling = lagBehandling()), + aktør = barn.aktør, + ) + val vilkårResultater = Vilkår.hentVilkårFor(personType = PersonType.BARN, fagsakType = FagsakType.NORMAL, behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR).mapNotNull { + if (it == Vilkår.LOVLIG_OPPHOLD) { + null + } else { + lagVilkårResultat( + vilkårType = it, + periodeFom = LocalDate.now().minusMonths(5), + periodeTom = null, + resultat = Resultat.OPPFYLT, + utdypendeVilkårsvurderinger = if (it == Vilkår.BOR_MED_SØKER) listOf(UtdypendeVilkårsvurdering.DELT_BOSTED) else emptyList(), + personResultat = personResultat, + ) + } + } + + val prosent = vilkårResultater.mapTilProsentEllerNull(personType = PersonType.BARN, fagsakType = FagsakType.NORMAL) + + assertEquals(null, prosent) + } + + @Test + fun `Skal returnere null hvis ikke alle vilkår for søker er oppfylt`() { + val søker = lagPerson(type = PersonType.SØKER) + val personResultat = PersonResultat( + vilkårsvurdering = Vilkårsvurdering(behandling = lagBehandling()), + aktør = søker.aktør, + ) + val vilkårResultater = Vilkår.hentVilkårFor(personType = PersonType.SØKER, fagsakType = FagsakType.NORMAL, behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR).mapNotNull { + if (it == Vilkår.LOVLIG_OPPHOLD) { + null + } else { + lagVilkårResultat( + vilkårType = it, + periodeFom = LocalDate.now().minusMonths(5), + periodeTom = null, + resultat = Resultat.OPPFYLT, + utdypendeVilkårsvurderinger = emptyList(), + personResultat = personResultat, + ) + } + } + + val prosent = vilkårResultater.mapTilProsentEllerNull(personType = PersonType.SØKER, fagsakType = FagsakType.NORMAL) + + assertEquals(null, prosent) + } + + private fun assertProsentPeriode( + forventetFom: YearMonth, + forventetTom: YearMonth, + forventetProsent: BigDecimal, + faktisk: Periode, + ) { + assertEquals(forventetFom, faktisk.fraOgMed.tilYearMonth()) + assertEquals(forventetTom, faktisk.tilOgMed.tilYearMonth()) + assertEquals(forventetProsent, faktisk.innhold) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/SatsServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/SatsServiceTest.kt new file mode 100644 index 000000000..323ce56cd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/SatsServiceTest.kt @@ -0,0 +1,55 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.årMnd +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonthEllerNull +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class SatsServiceTest { + + @Test + fun `Skal opprette korrekt tidslinje for ordinær barnetrygd for barn`() { + val barn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2017, 4, 6)) + + val ordinærTidslinje = lagOrdinærTidslinje(barn) + val ordinærePerioder = ordinærTidslinje.perioder().toList() + + Assertions.assertEquals(8, ordinærePerioder.size) + + assertPeriode(TestKrPeriode(beløp = 970, fom = "2017-04", tom = "2019-02"), ordinærePerioder[0]) + assertPeriode(TestKrPeriode(beløp = 1054, fom = "2019-03", tom = "2020-08"), ordinærePerioder[1]) + assertPeriode(TestKrPeriode(beløp = 1354, fom = "2020-09", tom = "2021-08"), ordinærePerioder[2]) + assertPeriode(TestKrPeriode(beløp = 1654, fom = "2021-09", tom = "2021-12"), ordinærePerioder[3]) + assertPeriode(TestKrPeriode(beløp = 1676, fom = "2022-01", tom = "2023-02"), ordinærePerioder[4]) + assertPeriode(TestKrPeriode(beløp = 1723, fom = "2023-03", tom = "2023-03"), ordinærePerioder[5]) + assertPeriode(TestKrPeriode(beløp = 1083, fom = "2023-04", tom = "2023-06"), ordinærePerioder[6]) + assertPeriode(TestKrPeriode(beløp = 1310, fom = "2023-07", tom = null), ordinærePerioder[7]) + } + + private fun assertPeriode( + forventet: TestKrPeriode, + faktisk: no.nav.familie.ba.sak.kjerne.tidslinje.Periode, + ) { + Assertions.assertEquals(forventet.beløp, faktisk.innhold, "Forskjell i beløp") + Assertions.assertEquals( + forventet.fom?.let { årMnd(it) }, + faktisk.fraOgMed.tilYearMonthEllerNull(), + "Forskjell i fra-og-med", + ) + Assertions.assertEquals( + forventet.tom?.let { årMnd(it) }, + faktisk.tilOgMed.tilYearMonthEllerNull(), + "Forskjell i til-og-med", + ) + } + + private data class TestKrPeriode( + val beløp: Int, + val fom: String?, + val tom: String?, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggBarnetrygdGeneratorTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggBarnetrygdGeneratorTest.kt" new file mode 100644 index 000000000..a599ca3b7 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggBarnetrygdGeneratorTest.kt" @@ -0,0 +1,279 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class SmåbarnstilleggBarnetrygdGeneratorTest { + + val søker = lagPerson(type = PersonType.SØKER) + val barn1 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(3).minusMonths(1)) + val barn2 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(2).minusMonths(1)) + val barn3 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(4).minusMonths(1)) + + val behandling = lagBehandling() + val tilkjentYtelse = + TilkjentYtelse(behandling = behandling, opprettetDato = LocalDate.now(), endretDato = LocalDate.now()) + + @BeforeEach + fun førHverTest() { + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2022, 12, 31) + } + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + @Test + fun `Skal kun få småbarnstillegg når alle tre krav er oppfylt i samme periode`() { + val overgangsstønadPerioder = listOf( + InternPeriodeOvergangsstønad( + personIdent = søker.aktør.aktivFødselsnummer(), + fomDato = LocalDate.now().minusYears(2), + tomDato = LocalDate.now(), + ), + ) + + val utvidetAndeler = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusYears(3), + tom = YearMonth.now().plusYears(1), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = søker, + ), + ) + + val barnasAndeler = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = barn3.fødselsdato.toYearMonth().plusMonths(1), + tom = YearMonth.now().plusYears(2), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn3, + ), + ) + + val småbarnstilleggAndeler = + SmåbarnstilleggBarnetrygdGenerator(behandlingId = behandling.id, tilkjentYtelse = tilkjentYtelse) + .lagSmåbarnstilleggAndeler( + perioderMedFullOvergangsstønad = overgangsstønadPerioder, + utvidetAndeler = utvidetAndeler, + barnasAndeler = barnasAndeler, + barnasAktørerOgFødselsdatoer = listOf(Pair(barn3.aktør, barn3.fødselsdato)), + ) + + Assertions.assertEquals(1, småbarnstilleggAndeler.size) + Assertions.assertEquals(YearMonth.now().minusYears(2), småbarnstilleggAndeler.single().stønadFom) + Assertions.assertEquals(barn3.fødselsdato.plusYears(3).toYearMonth(), småbarnstilleggAndeler.single().stønadTom) + Assertions.assertEquals(BigDecimal(100), småbarnstilleggAndeler.single().prosent) + } + + @Test + fun `Skal lage småbarnstillegg-andeler med 0kr når enten utvidet eller barnet under 3 år er overstyrt til 0kr`() { + val overgangsstønadPerioder = listOf( + InternPeriodeOvergangsstønad( + personIdent = søker.aktør.aktivFødselsnummer(), + fomDato = LocalDate.now().minusYears(4), + tomDato = LocalDate.now().plusYears(1), + ), + ) + + val utvidetAndeler = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusYears(4), + tom = YearMonth.now().minusYears(3), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = søker, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusYears(3).plusMonths(1), + tom = YearMonth.now().minusYears(2), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = søker, + prosent = BigDecimal.ZERO, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusYears(2).plusMonths(1), + tom = YearMonth.now(), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = søker, + ), + ) + + val barnasAndeler = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = barn3.fødselsdato.toYearMonth().plusMonths(1), + tom = YearMonth.now().minusYears(3), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn3, + prosent = BigDecimal.ZERO, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusYears(3).plusMonths(1), + tom = YearMonth.now().plusYears(2), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn3, + ), + ) + + val småbarnstilleggAndeler = + SmåbarnstilleggBarnetrygdGenerator(behandlingId = behandling.id, tilkjentYtelse = tilkjentYtelse) + .lagSmåbarnstilleggAndeler( + perioderMedFullOvergangsstønad = overgangsstønadPerioder, + utvidetAndeler = utvidetAndeler, + barnasAndeler = barnasAndeler, + barnasAktørerOgFødselsdatoer = listOf(Pair(barn3.aktør, barn3.fødselsdato)), + ) + + Assertions.assertEquals(2, småbarnstilleggAndeler.size) + Assertions.assertEquals(barn3.fødselsdato.plusMonths(1).toYearMonth(), småbarnstilleggAndeler.first().stønadFom) + Assertions.assertEquals(YearMonth.now().minusYears(2), småbarnstilleggAndeler.first().stønadTom) + Assertions.assertEquals(BigDecimal.ZERO, småbarnstilleggAndeler.first().prosent) + + Assertions.assertEquals(YearMonth.now().minusYears(2).plusMonths(1), småbarnstilleggAndeler.last().stønadFom) + Assertions.assertEquals(barn3.fødselsdato.plusYears(3).toYearMonth(), småbarnstilleggAndeler.last().stønadTom) + Assertions.assertEquals(BigDecimal(100), småbarnstilleggAndeler.last().prosent) + } + + @Test + fun `Skal lage småbarnstillegg-andeler med riktig prosent når vi har to barn hvor 1 av de har nullutbetaling`() { + val overgangsstønadPerioder = listOf( + InternPeriodeOvergangsstønad( + personIdent = søker.aktør.aktivFødselsnummer(), + fomDato = LocalDate.now().minusYears(3), + tomDato = LocalDate.now().plusYears(3), + ), + ) + + val utvidetAndeler = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusYears(3), + tom = YearMonth.now().plusYears(2), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = søker, + ), + ) + + val barnasAndeler = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = barn1.fødselsdato.toYearMonth().plusMonths(1), + tom = YearMonth.now().minusYears(1), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn1, + prosent = BigDecimal.ZERO, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusYears(1).plusMonths(1), + tom = YearMonth.now().plusYears(5), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn1, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = barn2.fødselsdato.toYearMonth().plusMonths(1), + tom = YearMonth.now().plusYears(6), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn2, + ), + ) + + val småbarnstilleggAndeler = + SmåbarnstilleggBarnetrygdGenerator(behandlingId = behandling.id, tilkjentYtelse = tilkjentYtelse) + .lagSmåbarnstilleggAndeler( + perioderMedFullOvergangsstønad = overgangsstønadPerioder, + utvidetAndeler = utvidetAndeler, + barnasAndeler = barnasAndeler, + barnasAktørerOgFødselsdatoer = listOf( + Pair(barn1.aktør, barn1.fødselsdato), + Pair(barn2.aktør, barn2.fødselsdato), + ), + ) + + Assertions.assertEquals(2, småbarnstilleggAndeler.size) + Assertions.assertEquals(barn1.fødselsdato.plusMonths(1).toYearMonth(), småbarnstilleggAndeler.first().stønadFom) + Assertions.assertEquals(barn2.fødselsdato.toYearMonth(), småbarnstilleggAndeler.first().stønadTom) + Assertions.assertEquals(BigDecimal.ZERO, småbarnstilleggAndeler.first().prosent) + + Assertions.assertEquals(barn2.fødselsdato.plusMonths(1).toYearMonth(), småbarnstilleggAndeler.last().stønadFom) + Assertions.assertEquals(barn2.fødselsdato.plusYears(3).toYearMonth(), småbarnstilleggAndeler.last().stønadTom) + Assertions.assertEquals(BigDecimal(100), småbarnstilleggAndeler.last().prosent) + } + + @Test + fun `Skal lage småbarnstillegg-andeler med 0kr for 2 barn når søker sin utvidet del er overstyrt til 0kr`() { + val overgangsstønadPerioder = listOf( + InternPeriodeOvergangsstønad( + personIdent = søker.aktør.aktivFødselsnummer(), + fomDato = LocalDate.now().minusYears(3), + tomDato = LocalDate.now().plusYears(3), + ), + ) + + val utvidetAndeler = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = barn1.fødselsdato.toYearMonth().plusMonths(1), + tom = YearMonth.now().minusYears(1), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = søker, + prosent = BigDecimal.ZERO, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusYears(1).plusMonths(1), + tom = YearMonth.now().plusYears(2), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = søker, + ), + ) + + val barnasAndeler = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = barn1.fødselsdato.toYearMonth().plusMonths(1), + tom = YearMonth.now().plusYears(5), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn1, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = barn2.fødselsdato.toYearMonth().plusMonths(1), + tom = YearMonth.now().plusYears(6), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn2, + ), + ) + + val småbarnstilleggAndeler = + SmåbarnstilleggBarnetrygdGenerator(behandlingId = behandling.id, tilkjentYtelse = tilkjentYtelse) + .lagSmåbarnstilleggAndeler( + perioderMedFullOvergangsstønad = overgangsstønadPerioder, + utvidetAndeler = utvidetAndeler, + barnasAndeler = barnasAndeler, + barnasAktørerOgFødselsdatoer = listOf( + Pair(barn1.aktør, barn1.fødselsdato), + Pair(barn2.aktør, barn2.fødselsdato), + ), + ) + + Assertions.assertEquals(2, småbarnstilleggAndeler.size) + Assertions.assertEquals(barn1.fødselsdato.plusMonths(1).toYearMonth(), småbarnstilleggAndeler.first().stønadFom) + Assertions.assertEquals(YearMonth.now().minusYears(1), småbarnstilleggAndeler.first().stønadTom) + Assertions.assertEquals(BigDecimal.ZERO, småbarnstilleggAndeler.first().prosent) + + Assertions.assertEquals(YearMonth.now().minusYears(1).plusMonths(1), småbarnstilleggAndeler.last().stønadFom) + Assertions.assertEquals(barn2.fødselsdato.plusYears(3).toYearMonth(), småbarnstilleggAndeler.last().stønadTom) + Assertions.assertEquals(BigDecimal(100), småbarnstilleggAndeler.last().prosent) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggServiceTest.kt" new file mode 100644 index 000000000..46d6ef825 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggServiceTest.kt" @@ -0,0 +1,140 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.integrasjoner.ef.EfSakRestClient +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.småbarnstillegg.PeriodeOvergangsstønadGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.småbarnstillegg.PeriodeOvergangsstønadGrunnlagRepository +import no.nav.familie.kontrakter.felles.ef.Datakilde +import no.nav.familie.kontrakter.felles.ef.EksternPeriode +import no.nav.familie.kontrakter.felles.ef.EksternePerioderResponse +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class SmåbarnstilleggServiceTest { + private val behandlingHentOgPersisterService = mockk() + private val efSakRestClient = mockk() + private val periodeOvergangsstønadGrunnlagRepository = mockk() + private val tilkjentYtelseRepository = mockk() + private val persongrunnlagService = mockk() + private val andelerTilkjentYtelseOgEndreteUtbetalingerService = + mockk() + + private lateinit var småbarnstilleggService: SmåbarnstilleggService + + @BeforeEach + fun setUp() { + småbarnstilleggService = SmåbarnstilleggService( + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + efSakRestClient = efSakRestClient, + periodeOvergangsstønadGrunnlagRepository = periodeOvergangsstønadGrunnlagRepository, + tilkjentYtelseRepository = tilkjentYtelseRepository, + persongrunnlagService = persongrunnlagService, + andelerTilkjentYtelseOgEndreteUtbetalingerService = andelerTilkjentYtelseOgEndreteUtbetalingerService, + ) + + every { periodeOvergangsstønadGrunnlagRepository.deleteByBehandlingId(any()) } just Runs + } + + @Test + fun `Ved satsendring skal gamle perioder kopieres`() { + val søker = lagPerson(type = PersonType.SØKER) + val fagsak = Fagsak(aktør = søker.aktør) + val forrigeBehandling = lagBehandling(årsak = BehandlingÅrsak.SØKNAD, fagsak = fagsak) + val behandling = lagBehandling(årsak = BehandlingÅrsak.SATSENDRING, fagsak = fagsak) + + val perioderForrigeBehandling = listOf( + PeriodeOvergangsstønadGrunnlag( + fom = LocalDate.now().minusYears(1), + tom = LocalDate.now().minusMonths(8), + aktør = søker.aktør, + datakilde = Datakilde.EF, + behandlingId = forrigeBehandling.id, + ), + PeriodeOvergangsstønadGrunnlag( + fom = LocalDate.now().minusMonths(7), + tom = LocalDate.now().minusMonths(1), + aktør = søker.aktør, + datakilde = Datakilde.EF, + behandlingId = forrigeBehandling.id, + ), + ) + + val forventetNyePerioder = perioderForrigeBehandling.map { + PeriodeOvergangsstønadGrunnlag( + fom = it.fom, + tom = it.tom, + aktør = it.aktør, + datakilde = it.datakilde, + behandlingId = behandling.id, + ) + } + + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling) } returns forrigeBehandling + every { periodeOvergangsstønadGrunnlagRepository.findByBehandlingId(forrigeBehandling.id) } returns perioderForrigeBehandling + + val slot = slot>() + every { periodeOvergangsstønadGrunnlagRepository.saveAll(capture(slot)) } returnsArgument 0 + + småbarnstilleggService.hentOgLagrePerioderMedOvergangsstønadForBehandling( + søkerAktør = søker.aktør, + behandling = behandling, + ) + + assertThat(slot.captured).containsAll(forventetNyePerioder) + verify(exactly = 1) { periodeOvergangsstønadGrunnlagRepository.saveAll(forventetNyePerioder) } + verify(exactly = 0) { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } + } + + @Test + fun `Vanlige behandlinger skal hente perioder fra EF`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.NYE_OPPLYSNINGER) + val søker = lagPerson(type = PersonType.SØKER) + + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = listOf( + EksternPeriode( + personIdent = søker.aktør.aktivFødselsnummer(), + fomDato = LocalDate.now().minusMonths(5), + tomDato = LocalDate.now().minusMonths(1), + datakilde = Datakilde.EF, + ), + ), + ) + val forventetPeriode = PeriodeOvergangsstønadGrunnlag( + behandlingId = behandling.id, + fom = LocalDate.now().minusMonths(5), + tom = LocalDate.now().minusMonths(1), + datakilde = Datakilde.EF, + aktør = søker.aktør, + ) + + val slot = slot>() + every { periodeOvergangsstønadGrunnlagRepository.saveAll(capture(slot)) } returnsArgument 0 + + småbarnstilleggService.hentOgLagrePerioderMedOvergangsstønadForBehandling( + søkerAktør = søker.aktør, + behandling = behandling, + ) + + verify(exactly = 1) { efSakRestClient.hentPerioderMedFullOvergangsstønad(søker.aktør.aktivFødselsnummer()) } + assertThat(slot.captured).containsExactly( + forventetPeriode, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggUtilsTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggUtilsTest.kt" new file mode 100644 index 000000000..e90efa0e1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Sm\303\245barnstilleggUtilsTest.kt" @@ -0,0 +1,771 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønadTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class SmåbarnstilleggUtilsTest { + + @BeforeEach + fun førHverTest() { + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2022, 12, 31) + } + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + @Test + fun `Skal generere tidslinje for barn med rett til småbarnstillegg kun hvor barn er under 3 år`() { + val barn = lagPerson(fødselsdato = LocalDate.now().minusYears(4), type = PersonType.BARN) + + val barnasAndeler = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = barn.fødselsdato.plusMonths(1).toYearMonth(), + tom = YearMonth.now(), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn, + ), + ) + + val generertePerioder = lagTidslinjeForPerioderMedBarnSomGirRettTilSmåbarnstillegg( + barnasAndeler = barnasAndeler, + barnasAktørerOgFødselsdatoer = listOf(Pair(barn.aktør, barn.fødselsdato)), + ).perioder() + + assertEquals(1, generertePerioder.size) + assertEquals(barn.fødselsdato.plusMonths(1).toYearMonth(), generertePerioder.single().fraOgMed.tilYearMonth()) + assertEquals(barn.fødselsdato.plusYears(3).toYearMonth(), generertePerioder.single().tilOgMed.tilYearMonth()) + assertEquals(BarnSinRettTilSmåbarnstillegg.UNDER_3_ÅR_UTBETALING, generertePerioder.single().innhold) + } + + @Test + fun `Skal generere tidslinje for barn med rett til småbarnstillegg med riktig utbetalings-info for ett barn`() { + val barn = lagPerson(fødselsdato = LocalDate.now().minusYears(4), type = PersonType.BARN) + + val brytningstidspunkt = LocalDate.now().minusYears(3) + + val barnasAndeler = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = barn.fødselsdato.plusMonths(1).toYearMonth(), + tom = brytningstidspunkt.toYearMonth(), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn, + prosent = BigDecimal.ZERO, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = brytningstidspunkt.plusMonths(1).toYearMonth(), + tom = YearMonth.now(), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn, + ), + ) + + val generertePerioder = lagTidslinjeForPerioderMedBarnSomGirRettTilSmåbarnstillegg( + barnasAndeler = barnasAndeler, + barnasAktørerOgFødselsdatoer = listOf(Pair(barn.aktør, barn.fødselsdato)), + ).perioder().sortedBy { it.fraOgMed } + + assertEquals(2, generertePerioder.size) + assertEquals(barn.fødselsdato.plusMonths(1).toYearMonth(), generertePerioder.first().fraOgMed.tilYearMonth()) + assertEquals(brytningstidspunkt.toYearMonth(), generertePerioder.first().tilOgMed.tilYearMonth()) + assertEquals(BarnSinRettTilSmåbarnstillegg.UNDER_3_ÅR_NULLUTBETALING, generertePerioder.first().innhold) + + assertEquals(brytningstidspunkt.plusMonths(1).toYearMonth(), generertePerioder.last().fraOgMed.tilYearMonth()) + assertEquals(barn.fødselsdato.plusYears(3).toYearMonth(), generertePerioder.last().tilOgMed.tilYearMonth()) + assertEquals(BarnSinRettTilSmåbarnstillegg.UNDER_3_ÅR_UTBETALING, generertePerioder.last().innhold) + } + + @Test + fun `Skal generere tidslinje for barn med rett til småbarnstillegg med riktig utbetalings-info når det er flere barn`() { + val barn1 = lagPerson(fødselsdato = LocalDate.now().minusYears(4), type = PersonType.BARN) + val barn2 = lagPerson(fødselsdato = LocalDate.now().minusYears(6), type = PersonType.BARN) + val barn3 = lagPerson(fødselsdato = LocalDate.now().minusYears(1), type = PersonType.BARN) + + val brytningstidspunkt1 = LocalDate.now().minusYears(3).minusMonths(6) + val brytningstidspunkt2 = LocalDate.now().minusYears(2) + + val barnasAndeler = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = barn1.fødselsdato.plusMonths(1).toYearMonth(), + tom = brytningstidspunkt1.toYearMonth(), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn1, + prosent = BigDecimal.ZERO, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = brytningstidspunkt1.plusMonths(1).toYearMonth(), + tom = brytningstidspunkt2.toYearMonth(), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn1, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = brytningstidspunkt2.plusMonths(1).toYearMonth(), + tom = YearMonth.now().plusYears(5), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn1, + prosent = BigDecimal.ZERO, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = barn2.fødselsdato.plusMonths(1).toYearMonth(), + tom = YearMonth.now().plusYears(5), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn2, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = barn3.fødselsdato.plusMonths(1).toYearMonth(), + tom = YearMonth.now(), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn3, + ), + ) + + val generertePerioder = lagTidslinjeForPerioderMedBarnSomGirRettTilSmåbarnstillegg( + barnasAndeler = barnasAndeler, + barnasAktørerOgFødselsdatoer = listOf( + Pair(barn1.aktør, barn1.fødselsdato), + Pair(barn2.aktør, barn2.fødselsdato), + Pair(barn3.aktør, barn3.fødselsdato), + ), + ).perioder().sortedBy { it.fraOgMed } + + assertEquals(3, generertePerioder.size) + assertEquals(barn2.fødselsdato.plusMonths(1).toYearMonth(), generertePerioder.first().fraOgMed.tilYearMonth()) + assertEquals(brytningstidspunkt2.toYearMonth(), generertePerioder.first().tilOgMed.tilYearMonth()) + assertEquals(BarnSinRettTilSmåbarnstillegg.UNDER_3_ÅR_UTBETALING, generertePerioder.first().innhold) + + assertEquals(brytningstidspunkt2.plusMonths(1).toYearMonth(), generertePerioder[1].fraOgMed.tilYearMonth()) + assertEquals(barn3.fødselsdato.toYearMonth(), generertePerioder[1].tilOgMed.tilYearMonth()) + assertEquals(BarnSinRettTilSmåbarnstillegg.UNDER_3_ÅR_NULLUTBETALING, generertePerioder[1].innhold) + + assertEquals(barn3.fødselsdato.plusMonths(1).toYearMonth(), generertePerioder[2].fraOgMed.tilYearMonth()) + assertEquals(LocalDate.now().toYearMonth(), generertePerioder[2].tilOgMed.tilYearMonth()) + assertEquals(BarnSinRettTilSmåbarnstillegg.UNDER_3_ÅR_UTBETALING, generertePerioder[2].innhold) + } + + @Test + fun `Skal kun få småbarnstillegg når alle tre tidslinjene har oppfylt kravene`() { + val søker = lagPerson(type = PersonType.SØKER) + val overgangsstønadPerioder = listOf( + InternPeriodeOvergangsstønad( + personIdent = søker.aktør.aktivFødselsnummer(), + fomDato = LocalDate.now().minusYears(2), + tomDato = LocalDate.now().plusYears(1), + ), + ) + + val utvidetAndeler = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusYears(3), + tom = YearMonth.now().plusYears(1), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = søker, + ), + ) + + val barnsSomGirRettTilSmåbarnstilleggTidslinje = lagBarnSomGirRettTilSmåbarnstilleggTidslinje( + listOf( + Periode( + fraOgMed = YearMonth.now().minusYears(4).tilTidspunkt(), + tilOgMed = YearMonth.now().minusYears(1).tilTidspunkt(), + innhold = BarnSinRettTilSmåbarnstillegg.UNDER_3_ÅR_UTBETALING, + ), + ), + ) + + val kombinertTidslinje = kombinerAlleTidslinjerTilProsentTidslinje( + perioderMedFullOvergangsstønadTidslinje = InternPeriodeOvergangsstønadTidslinje(overgangsstønadPerioder), + utvidetBarnetrygdTidslinje = AndelTilkjentYtelseMedEndreteUtbetalingerTidslinje(utvidetAndeler), + barnSomGirRettTilSmåbarnstilleggTidslinje = barnsSomGirRettTilSmåbarnstilleggTidslinje, + ) + + val perioderMedSmåbarnstillegg = kombinertTidslinje.perioder() + + assertEquals(1, perioderMedSmåbarnstillegg.size) + assertEquals(YearMonth.now().minusYears(2), perioderMedSmåbarnstillegg.single().fraOgMed.tilYearMonth()) + assertEquals(YearMonth.now().minusYears(1), perioderMedSmåbarnstillegg.single().tilOgMed.tilYearMonth()) + assertEquals(BigDecimal(100), perioderMedSmåbarnstillegg.single().innhold!!.prosent) + } + + @Test + fun `Skal få småbarnstillegg med nullutbetaling når utvidet eller barn er overstyrt til 0kr`() { + val søker = lagPerson(type = PersonType.SØKER) + val brytningstidspunkt1 = YearMonth.now().minusYears(3) + val brytningstidspunkt2 = YearMonth.now().minusYears(2) + + val overgangsstønadPerioder = listOf( + InternPeriodeOvergangsstønad( + personIdent = søker.aktør.aktivFødselsnummer(), + fomDato = LocalDate.now().minusYears(5), + tomDato = LocalDate.now().plusYears(1), + ), + ) + + val utvidetAndeler = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusYears(4), + tom = brytningstidspunkt1, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = søker, + prosent = BigDecimal.ZERO, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = brytningstidspunkt1.plusMonths(1), + tom = YearMonth.now(), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = søker, + ), + ) + + val barnsSomGirRettTilSmåbarnstilleggTidslinje = lagBarnSomGirRettTilSmåbarnstilleggTidslinje( + listOf( + Periode( + fraOgMed = YearMonth.now().minusYears(5).tilTidspunkt(), + tilOgMed = brytningstidspunkt2.tilTidspunkt(), + innhold = BarnSinRettTilSmåbarnstillegg.UNDER_3_ÅR_UTBETALING, + ), + Periode( + fraOgMed = brytningstidspunkt2.plusMonths(1).tilTidspunkt(), + tilOgMed = YearMonth.now().minusYears(1).tilTidspunkt(), + innhold = BarnSinRettTilSmåbarnstillegg.UNDER_3_ÅR_NULLUTBETALING, + ), + ), + ) + + val kombinertTidslinje = kombinerAlleTidslinjerTilProsentTidslinje( + perioderMedFullOvergangsstønadTidslinje = InternPeriodeOvergangsstønadTidslinje(overgangsstønadPerioder), + utvidetBarnetrygdTidslinje = AndelTilkjentYtelseMedEndreteUtbetalingerTidslinje(utvidetAndeler), + barnSomGirRettTilSmåbarnstilleggTidslinje = barnsSomGirRettTilSmåbarnstilleggTidslinje, + ) + + val perioderMedSmåbarnstillegg = kombinertTidslinje.perioder().toList() + + assertEquals(3, perioderMedSmåbarnstillegg.size) + + assertEquals(YearMonth.now().minusYears(4), perioderMedSmåbarnstillegg[0].fraOgMed.tilYearMonth()) + assertEquals(brytningstidspunkt1, perioderMedSmåbarnstillegg[0].tilOgMed.tilYearMonth()) + assertEquals(BigDecimal.ZERO, perioderMedSmåbarnstillegg[0].innhold!!.prosent) + + assertEquals(brytningstidspunkt1.plusMonths(1), perioderMedSmåbarnstillegg[1].fraOgMed.tilYearMonth()) + assertEquals(brytningstidspunkt2, perioderMedSmåbarnstillegg[1].tilOgMed.tilYearMonth()) + assertEquals(BigDecimal(100), perioderMedSmåbarnstillegg[1].innhold!!.prosent) + + assertEquals(brytningstidspunkt2.plusMonths(1), perioderMedSmåbarnstillegg[2].fraOgMed.tilYearMonth()) + assertEquals(YearMonth.now().minusYears(1), perioderMedSmåbarnstillegg[2].tilOgMed.tilYearMonth()) + assertEquals(BigDecimal.ZERO, perioderMedSmåbarnstillegg[2].innhold!!.prosent) + } + + @Test + fun `Skal svare true om at nye perioder med full OS påvirker behandling`() { + val personIdent = randomFnr() + val barnAktør = tilAktør(randomFnr()) + + val påvirkerFagsak = vedtakOmOvergangsstønadPåvirkerFagsak( + småbarnstilleggBarnetrygdGenerator = SmåbarnstilleggBarnetrygdGenerator( + behandlingId = 1L, + tilkjentYtelse = lagInitiellTilkjentYtelse(), + ), + nyePerioderMedFullOvergangsstønad = listOf( + InternPeriodeOvergangsstønad( + personIdent = personIdent, + fomDato = LocalDate.now().minusMonths(6), + tomDato = LocalDate.now().plusMonths(6), + ), + ), + forrigeAndelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(6), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = tilfeldigPerson(aktør = barnAktør), + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(6), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = tilfeldigPerson(aktør = tilAktør(personIdent)), + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(6), + ytelseType = YtelseType.SMÅBARNSTILLEGG, + person = tilfeldigPerson(aktør = tilAktør(personIdent)), + ), + ), + barnasAktørerOgFødselsdatoer = listOf(Pair(barnAktør, LocalDate.now().minusYears(2))), + + ) + + assertTrue(påvirkerFagsak) + } + + @Test + fun `Skal svare false om at nye perioder med full OS påvirker behandling`() { + val personIdent = randomAktør() + val barnIdent = randomAktør() + + val påvirkerFagsak = vedtakOmOvergangsstønadPåvirkerFagsak( + småbarnstilleggBarnetrygdGenerator = SmåbarnstilleggBarnetrygdGenerator( + behandlingId = 1L, + tilkjentYtelse = lagInitiellTilkjentYtelse(), + ), + nyePerioderMedFullOvergangsstønad = listOf( + InternPeriodeOvergangsstønad( + personIdent = personIdent.aktørId, + fomDato = LocalDate.now().minusMonths(10), + tomDato = LocalDate.now().plusMonths(6), + ), + ), + forrigeAndelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(6), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = tilfeldigPerson(aktør = barnIdent), + aktør = barnIdent, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(6), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = tilfeldigPerson(aktør = personIdent), + aktør = personIdent, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(6), + ytelseType = YtelseType.SMÅBARNSTILLEGG, + person = tilfeldigPerson(aktør = personIdent), + aktør = personIdent, + ), + ), + barnasAktørerOgFødselsdatoer = listOf(Pair(barnIdent, LocalDate.now().minusYears(2))), + + ) + + assertFalse(påvirkerFagsak) + } + + @Test + fun `Skal svare false om at nye perioder med full OS påvirker behandling ved flere perioder`() { + val personIdent = randomAktør() + val barnIdent = randomAktør() + + val påvirkerFagsak = vedtakOmOvergangsstønadPåvirkerFagsak( + småbarnstilleggBarnetrygdGenerator = SmåbarnstilleggBarnetrygdGenerator( + behandlingId = 1L, + tilkjentYtelse = lagInitiellTilkjentYtelse(), + ), + nyePerioderMedFullOvergangsstønad = listOf( + InternPeriodeOvergangsstønad( + personIdent = personIdent.aktørId, + fomDato = LocalDate.now().minusMonths(10), + tomDato = LocalDate.now().minusMonths(6), + ), + InternPeriodeOvergangsstønad( + personIdent = personIdent.aktørId, + fomDato = LocalDate.now().minusMonths(4), + tomDato = LocalDate.now().plusMonths(2), + ), + ), + forrigeAndelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(6), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = tilfeldigPerson(aktør = barnIdent), + aktør = barnIdent, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().minusMonths(6), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = tilfeldigPerson(aktør = personIdent), + aktør = personIdent, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().minusMonths(6), + ytelseType = YtelseType.SMÅBARNSTILLEGG, + person = tilfeldigPerson(aktør = personIdent), + aktør = personIdent, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(4), + tom = YearMonth.now().plusMonths(2), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = tilfeldigPerson(aktør = personIdent), + aktør = personIdent, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(4), + tom = YearMonth.now().plusMonths(2), + ytelseType = YtelseType.SMÅBARNSTILLEGG, + person = tilfeldigPerson(aktør = personIdent), + aktør = personIdent, + ), + ), + barnasAktørerOgFødselsdatoer = listOf(Pair(barnIdent, LocalDate.now().minusYears(2))), + + ) + + assertFalse(påvirkerFagsak) + } + + @Test + fun `skal ikke behandle vedtak om overgangsstønad når vedtaket ikke fører til endring i utbetaling`() { + val personIdent = randomAktør() + val barnIdent = randomAktør() + + val påvirkerFagsak = vedtakOmOvergangsstønadPåvirkerFagsak( + småbarnstilleggBarnetrygdGenerator = SmåbarnstilleggBarnetrygdGenerator( + behandlingId = 1L, + tilkjentYtelse = lagInitiellTilkjentYtelse(), + ), + nyePerioderMedFullOvergangsstønad = listOf( + InternPeriodeOvergangsstønad( + personIdent = personIdent.aktørId, + fomDato = LocalDate.now().minusMonths(10), + tomDato = LocalDate.now().minusMonths(1), + ), + InternPeriodeOvergangsstønad( + personIdent = personIdent.aktørId, + fomDato = LocalDate.now(), + tomDato = LocalDate.now().plusMonths(6), + ), + ), + forrigeAndelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(10), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = tilfeldigPerson(aktør = barnIdent), + aktør = barnIdent, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(10), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = tilfeldigPerson(aktør = personIdent), + aktør = personIdent, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(6), + ytelseType = YtelseType.SMÅBARNSTILLEGG, + person = tilfeldigPerson(aktør = personIdent), + aktør = personIdent, + ), + ), + barnasAktørerOgFødselsdatoer = listOf(Pair(barnIdent, LocalDate.now().minusYears(2))), + + ) + + assertFalse(påvirkerFagsak) + } + + @Test + fun `skal behandle vedtak om overgangsstønad når vedtaket fører til endring i utbetaling`() { + val personIdent = randomAktør() + val barnIdent = randomAktør() + + val påvirkerFagsak = vedtakOmOvergangsstønadPåvirkerFagsak( + småbarnstilleggBarnetrygdGenerator = SmåbarnstilleggBarnetrygdGenerator( + behandlingId = 1L, + tilkjentYtelse = lagInitiellTilkjentYtelse(), + ), + nyePerioderMedFullOvergangsstønad = listOf( + InternPeriodeOvergangsstønad( + personIdent = personIdent.aktørId, + fomDato = LocalDate.now().minusMonths(10), + tomDato = LocalDate.now().minusMonths(1), + ), + InternPeriodeOvergangsstønad( + personIdent = personIdent.aktørId, + fomDato = LocalDate.now(), + tomDato = LocalDate.now().plusMonths(8), + ), + ), + forrigeAndelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(10), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = tilfeldigPerson(aktør = barnIdent), + aktør = barnIdent, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(10), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = tilfeldigPerson(aktør = personIdent), + aktør = personIdent, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(6), + ytelseType = YtelseType.SMÅBARNSTILLEGG, + person = tilfeldigPerson(aktør = personIdent), + aktør = personIdent, + ), + ), + barnasAktørerOgFødselsdatoer = listOf(Pair(barnIdent, LocalDate.now().minusYears(2))), + + ) + + assertTrue(påvirkerFagsak) + } + + @Test + fun `Skal legge til innvilgelsesbegrunnelse for småbarnstillegg`() { + val vedtaksperiodeMedBegrunnelser = lagVedtaksperiodeMedBegrunnelser( + fom = LocalDate.now().førsteDagIInneværendeMåned(), + tom = LocalDate.now().plusMonths(3).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING, + ) + + val oppdatertVedtaksperiodeMedBegrunnelser = finnAktuellVedtaksperiodeOgLeggTilSmåbarnstilleggbegrunnelse( + vedtaksperioderMedBegrunnelser = listOf( + vedtaksperiodeMedBegrunnelser, + ), + innvilgetMånedPeriode = MånedPeriode( + fom = YearMonth.now(), + tom = vedtaksperiodeMedBegrunnelser.tom!!.toYearMonth(), + ), + redusertMånedPeriode = null, + ) + + assertNotNull(oppdatertVedtaksperiodeMedBegrunnelser) + assertTrue(oppdatertVedtaksperiodeMedBegrunnelser.begrunnelser.any { it.standardbegrunnelse == Standardbegrunnelse.INNVILGET_SMÅBARNSTILLEGG }) + assertTrue(oppdatertVedtaksperiodeMedBegrunnelser.begrunnelser.none { it.standardbegrunnelse == Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_FULL_OVERGANGSSTØNAD }) + } + + @Test + fun `Skal legge til reduksjonsbegrunnelse for småbarnstillegg`() { + val vedtaksperiodeMedBegrunnelser = lagVedtaksperiodeMedBegrunnelser( + fom = LocalDate.now().nesteMåned().førsteDagIInneværendeMåned(), + tom = LocalDate.now().plusMonths(3).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING, + ) + + val oppdatertVedtaksperiodeMedBegrunnelser = finnAktuellVedtaksperiodeOgLeggTilSmåbarnstilleggbegrunnelse( + vedtaksperioderMedBegrunnelser = listOf( + vedtaksperiodeMedBegrunnelser, + ), + innvilgetMånedPeriode = null, + redusertMånedPeriode = MånedPeriode( + fom = YearMonth.now().nesteMåned(), + tom = vedtaksperiodeMedBegrunnelser.tom!!.toYearMonth(), + ), + ) + + assertNotNull(oppdatertVedtaksperiodeMedBegrunnelser) + assertTrue(oppdatertVedtaksperiodeMedBegrunnelser.begrunnelser.none { it.standardbegrunnelse == Standardbegrunnelse.INNVILGET_SMÅBARNSTILLEGG }) + assertTrue(oppdatertVedtaksperiodeMedBegrunnelser.begrunnelser.any { it.standardbegrunnelse == Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_FULL_OVERGANGSSTØNAD }) + } + + @Test + fun `Skal legge til reduksjonsbegrunnelse fra inneværende måned for småbarnstillegg`() { + val vedtaksperiodeMedBegrunnelser = lagVedtaksperiodeMedBegrunnelser( + fom = LocalDate.now().førsteDagIInneværendeMåned(), + tom = LocalDate.now().plusMonths(3).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING, + ) + + val oppdatertVedtaksperiodeMedBegrunnelser = finnAktuellVedtaksperiodeOgLeggTilSmåbarnstilleggbegrunnelse( + vedtaksperioderMedBegrunnelser = listOf( + vedtaksperiodeMedBegrunnelser, + ), + innvilgetMånedPeriode = null, + redusertMånedPeriode = MånedPeriode( + fom = YearMonth.now(), + tom = vedtaksperiodeMedBegrunnelser.tom!!.toYearMonth(), + ), + ) + + assertNotNull(oppdatertVedtaksperiodeMedBegrunnelser) + assertTrue(oppdatertVedtaksperiodeMedBegrunnelser.begrunnelser.none { it.standardbegrunnelse == Standardbegrunnelse.INNVILGET_SMÅBARNSTILLEGG }) + assertTrue(oppdatertVedtaksperiodeMedBegrunnelser.begrunnelser.any { it.standardbegrunnelse == Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_FULL_OVERGANGSSTØNAD }) + } + + @Test + fun `Skal kaste feil om det ikke finnes innvilget eller redusert periode å begrunne`() { + val vedtaksperiodeMedBegrunnelser = lagVedtaksperiodeMedBegrunnelser( + fom = LocalDate.now().nesteMåned().førsteDagIInneværendeMåned(), + tom = LocalDate.now().plusMonths(3).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING, + ) + + assertThrows { + finnAktuellVedtaksperiodeOgLeggTilSmåbarnstilleggbegrunnelse( + vedtaksperioderMedBegrunnelser = listOf( + vedtaksperiodeMedBegrunnelser, + ), + innvilgetMånedPeriode = null, + redusertMånedPeriode = null, + ) + } + } + + @Test + fun `Skal kunne automatisk iverksette småbarnstillegg når endringer i OS kun er frem i tid`() { + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.now().minusYears(2), + tom = YearMonth.now().minusMonths(10), + ), + ) + + val nyeAndeler = forrigeAndeler + listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.now(), + tom = YearMonth.now().plusMonths(2), + ), + ) + + val (innvilgedeMånedPerioder, reduserteMånedPerioder) = hentInnvilgedeOgReduserteAndelerSmåbarnstillegg( + forrigeSmåbarnstilleggAndeler = forrigeAndeler, + nyeSmåbarnstilleggAndeler = nyeAndeler, + ) + + assertTrue( + kanAutomatiskIverksetteSmåbarnstillegg( + innvilgedeMånedPerioder = innvilgedeMånedPerioder, + reduserteMånedPerioder = reduserteMånedPerioder, + ), + ) + } + + @Test + fun `Skal ikke kunne automatisk iverksette småbarnstillegg når endringer i OS er tilbake og frem i tid`() { + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.now().minusYears(2), + tom = YearMonth.now().minusMonths(10), + ), + ) + + val nyeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.now().minusYears(2), + tom = YearMonth.now().minusMonths(5), + ), + lagAndelTilkjentYtelse( + fom = YearMonth.now(), + tom = YearMonth.now().plusMonths(2), + ), + ) + + val (innvilgedeMånedPerioder, reduserteMånedPerioder) = hentInnvilgedeOgReduserteAndelerSmåbarnstillegg( + forrigeSmåbarnstilleggAndeler = forrigeAndeler, + nyeSmåbarnstilleggAndeler = nyeAndeler, + ) + + assertFalse( + kanAutomatiskIverksetteSmåbarnstillegg( + innvilgedeMånedPerioder = innvilgedeMånedPerioder, + reduserteMånedPerioder = reduserteMånedPerioder, + ), + ) + } + + @Test + fun `Skal ikke kunne automatisk iverksette småbarnstillegg når endringer i OS er 2 måneder frem i tid`() { + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.now().minusYears(2), + tom = YearMonth.now().minusMonths(10), + ), + ) + + val nyeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.now().minusYears(2), + tom = YearMonth.now().minusMonths(5), + ), + lagAndelTilkjentYtelse( + fom = YearMonth.now().plusMonths(2), + tom = YearMonth.now().plusMonths(4), + ), + ) + + val (innvilgedeMånedPerioder, reduserteMånedPerioder) = hentInnvilgedeOgReduserteAndelerSmåbarnstillegg( + forrigeSmåbarnstilleggAndeler = forrigeAndeler, + nyeSmåbarnstilleggAndeler = nyeAndeler, + ) + + assertFalse( + kanAutomatiskIverksetteSmåbarnstillegg( + innvilgedeMånedPerioder = innvilgedeMånedPerioder, + reduserteMånedPerioder = reduserteMånedPerioder, + ), + ) + } + + @Test + fun `Skal ikke kunne automatisk iverksette småbarnstillegg når reduksjon i OS kun tilbake i tid`() { + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.now().minusYears(2), + tom = YearMonth.now().minusMonths(10), + ), + ) + + val nyeAndeler = emptyList() + + val (innvilgedeMånedPerioder, reduserteMånedPerioder) = hentInnvilgedeOgReduserteAndelerSmåbarnstillegg( + forrigeSmåbarnstilleggAndeler = forrigeAndeler, + nyeSmåbarnstilleggAndeler = nyeAndeler, + ) + + assertFalse( + kanAutomatiskIverksetteSmåbarnstillegg( + innvilgedeMånedPerioder = innvilgedeMånedPerioder, + reduserteMånedPerioder = reduserteMånedPerioder, + ), + ) + } + + private fun lagBarnSomGirRettTilSmåbarnstilleggTidslinje(perioder: List>): Tidslinje = + tidslinje { perioder } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseUtilsEndretUtbetalingAndelTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseUtilsEndretUtbetalingAndelTest.kt new file mode 100644 index 000000000..f7a7f7991 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseUtilsEndretUtbetalingAndelTest.kt @@ -0,0 +1,266 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +internal class TilkjentYtelseUtilsEndretUtbetalingAndelTest { + + val behandling = lagBehandling() + val tilkjentYtelse = + TilkjentYtelse(behandling = behandling, endretDato = LocalDate.now(), opprettetDato = LocalDate.now()) + val beløp = BigDecimal(100) + + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + val barn2 = tilfeldigPerson(personType = PersonType.BARN) + val søker = tilfeldigPerson(personType = PersonType.SØKER) + + @BeforeEach + fun setUp() { + } + + @Test + fun `teste nye andeler tilkjent ytelse for to barn med endrete utbetalingsandeler`() { + /** + * Tidslinjer barn 1: + * -------------[############]-----------[#########]---------- AndelTilkjentYtelse + * 0118 0418 1018 0821 + * ---[################]-------------------------------------- EndretUtbetalingYtelse + * 0115 0318 + * + * -------------[######][##]-------------[#########]---------- Nye AndelTilkjentYtelse + * + * Periodene for nye AndelTilkjentYtelse: 0118-0318, 0418-0418, 1018-0821 + * + * + * Tidslinjer barn 2: + * --------------[###################]--------[###########]------------ AndelTilkjentYtelse + * 0218 0818 1118 0921 + * ---------------------[####]----[#######################]---[####]--- EndretUtbetalingYtelse + * 0418 0518 0718 0921 1121-1221 + * + * --------------[#####][####][##][##]--------[###########]------------ Nye AndelTilkjentYtelse + * + * Periodene for nye AndelTilkjentYtelse: 0218-0318, 0418-0518, 0618-0618, 0718-0818, 1118-0921 + */ + + val andelTilkjentytelseForBarn1 = listOf( + MånedPeriode(YearMonth.of(2018, 1), YearMonth.of(2018, 4)), + MånedPeriode(YearMonth.of(2018, 10), YearMonth.of(2021, 8)), + ) + .map { + lagAndelTilkjentYtelse(barn1, it.fom, it.tom) + } + + val andelTilkjentytelseForBarn2 = listOf( + MånedPeriode(YearMonth.of(2018, 2), YearMonth.of(2018, 8)), + MånedPeriode(YearMonth.of(2018, 11), YearMonth.of(2021, 9)), + ) + .map { + lagAndelTilkjentYtelse(barn2, it.fom, it.tom) + } + + val endretUtbetalingerForBarn1 = listOf( + MånedPeriode(YearMonth.of(2015, 1), YearMonth.of(2018, 3)), + MånedPeriode(YearMonth.of(2018, 4), YearMonth.of(2018, 4)), + ) + .map { + lagEndretUtbetalingAndelMedAndelerTilkjentYtelse(behandling.id, barn1, it.fom, it.tom, 50) + } + + val endretUtbetalingerForBarn2 = listOf( + MånedPeriode(YearMonth.of(2018, 4), YearMonth.of(2018, 5)), + MånedPeriode(YearMonth.of(2018, 7), YearMonth.of(2021, 9)), + MånedPeriode(YearMonth.of(2021, 11), YearMonth.of(2021, 12)), + ) + .map { + lagEndretUtbetalingAndelMedAndelerTilkjentYtelse(behandling.id, barn2, it.fom, it.tom, 50) + } + + val andelerTilkjentYtelserEtterEUA = + TilkjentYtelseUtils.oppdaterTilkjentYtelseMedEndretUtbetalingAndeler( + (andelTilkjentytelseForBarn1 + andelTilkjentytelseForBarn2), + endretUtbetalingerForBarn1 + endretUtbetalingerForBarn2, + ) + + val andelerTilkjentYtelserEtterEUAList = andelerTilkjentYtelserEtterEUA.map { it.andel }.toList() + + assertEquals(8, andelerTilkjentYtelserEtterEUAList.size) + + verifiserAndelTilkjentYtelse( + andelerTilkjentYtelserEtterEUAList[0], + barn1.aktør.aktivFødselsnummer(), + beløp / BigDecimal(2), + YearMonth.of(2018, 1), + YearMonth.of(2018, 3), + ) + + verifiserAndelTilkjentYtelse( + andelerTilkjentYtelserEtterEUAList[1], + barn1.aktør.aktivFødselsnummer(), + beløp / BigDecimal(2), + YearMonth.of(2018, 4), + YearMonth.of(2018, 4), + ) + + verifiserAndelTilkjentYtelse( + andelerTilkjentYtelserEtterEUAList[2], + barn1.aktør.aktivFødselsnummer(), + beløp, + YearMonth.of(2018, 10), + YearMonth.of(2021, 8), + ) + + verifiserAndelTilkjentYtelse( + andelerTilkjentYtelserEtterEUAList[3], + barn2.aktør.aktivFødselsnummer(), + beløp, + YearMonth.of(2018, 2), + YearMonth.of(2018, 3), + ) + + verifiserAndelTilkjentYtelse( + andelerTilkjentYtelserEtterEUAList[4], + barn2.aktør.aktivFødselsnummer(), + beløp / BigDecimal(2), + YearMonth.of(2018, 4), + YearMonth.of(2018, 5), + ) + + verifiserAndelTilkjentYtelse( + andelerTilkjentYtelserEtterEUAList[5], + barn2.aktør.aktivFødselsnummer(), + beløp, + YearMonth.of(2018, 6), + YearMonth.of(2018, 6), + ) + + verifiserAndelTilkjentYtelse( + andelerTilkjentYtelserEtterEUAList[6], + barn2.aktør.aktivFødselsnummer(), + beløp / BigDecimal(2), + YearMonth.of(2018, 7), + YearMonth.of(2018, 8), + ) + + verifiserAndelTilkjentYtelse( + andelerTilkjentYtelserEtterEUAList[7], + barn2.aktør.aktivFødselsnummer(), + beløp / BigDecimal(2), + YearMonth.of(2018, 11), + YearMonth.of(2021, 9), + ) + } + + @Test + fun `En gitt MånedPeriode skal gi tilbake perioder med og uten overlapp den har mot en eller flere andre perioder`() { + val periode = MånedPeriode(YearMonth.of(2018, 1), YearMonth.of(2018, 12)) + + var perioder = periode.perioderMedOgUtenOverlapp(emptyList()) + var perioderMedOverlapp = perioder.first + var perioderUtenOverlapp = perioder.second + assertEquals(0, perioderMedOverlapp.size) + assertEquals(1, perioderUtenOverlapp.size) + assertEquals(periode, perioderUtenOverlapp[0]) + + perioder = periode.perioderMedOgUtenOverlapp( + listOf( + MånedPeriode(YearMonth.of(2015, 1), YearMonth.of(2016, 3)), + ), + ) + perioderMedOverlapp = perioder.first + perioderUtenOverlapp = perioder.second + assertEquals(0, perioderMedOverlapp.size) + assertEquals(1, perioderUtenOverlapp.size) + assertEquals(periode, perioderUtenOverlapp[0]) + + perioder = periode.perioderMedOgUtenOverlapp( + listOf( + periode, + ), + ) + perioderMedOverlapp = perioder.first + perioderUtenOverlapp = perioder.second + assertEquals(1, perioderMedOverlapp.size) + assertEquals(periode, perioderMedOverlapp[0]) + assertEquals(0, perioderUtenOverlapp.size) + + perioder = periode.perioderMedOgUtenOverlapp( + listOf( + MånedPeriode(YearMonth.of(2015, 1), YearMonth.of(2018, 3)), + ), + ) + perioderMedOverlapp = perioder.first + perioderUtenOverlapp = perioder.second + assertEquals(1, perioderMedOverlapp.size) + assertEquals(MånedPeriode(YearMonth.of(2018, 1), YearMonth.of(2018, 3)), perioderMedOverlapp[0]) + assertEquals(1, perioderUtenOverlapp.size) + assertEquals(MånedPeriode(YearMonth.of(2018, 4), YearMonth.of(2018, 12)), perioderUtenOverlapp[0]) + + perioder = periode.perioderMedOgUtenOverlapp( + listOf( + MånedPeriode(YearMonth.of(2015, 1), YearMonth.of(2018, 3)), + MånedPeriode(YearMonth.of(2018, 6), YearMonth.of(2018, 6)), + ), + ) + perioderMedOverlapp = perioder.first + perioderUtenOverlapp = perioder.second + assertEquals(2, perioderMedOverlapp.size) + assertEquals(MånedPeriode(YearMonth.of(2018, 1), YearMonth.of(2018, 3)), perioderMedOverlapp[0]) + assertEquals(MånedPeriode(YearMonth.of(2018, 6), YearMonth.of(2018, 6)), perioderMedOverlapp[1]) + assertEquals(2, perioderUtenOverlapp.size) + assertEquals(MånedPeriode(YearMonth.of(2018, 4), YearMonth.of(2018, 5)), perioderUtenOverlapp[0]) + assertEquals(MånedPeriode(YearMonth.of(2018, 7), YearMonth.of(2018, 12)), perioderUtenOverlapp[1]) + + perioder = periode.perioderMedOgUtenOverlapp( + listOf( + MånedPeriode(YearMonth.of(2018, 2), YearMonth.of(2018, 11)), + ), + ) + perioderMedOverlapp = perioder.first + perioderUtenOverlapp = perioder.second + assertEquals(1, perioderMedOverlapp.size) + assertEquals(MånedPeriode(YearMonth.of(2018, 2), YearMonth.of(2018, 11)), perioderMedOverlapp[0]) + assertEquals(2, perioderUtenOverlapp.size) + assertEquals(MånedPeriode(YearMonth.of(2018, 1), YearMonth.of(2018, 1)), perioderUtenOverlapp[0]) + assertEquals(MånedPeriode(YearMonth.of(2018, 12), YearMonth.of(2018, 12)), perioderUtenOverlapp[1]) + } + + private fun verifiserAndelTilkjentYtelse( + andelTilkjentYtelse: AndelTilkjentYtelse, + forventetBarnIdent: String, + forventetBeløp: BigDecimal, + forventetStønadFom: YearMonth, + forventetStønadTom: YearMonth, + ) { + assertEquals(forventetBarnIdent, andelTilkjentYtelse.aktør.aktivFødselsnummer()) + assertEquals(forventetBeløp, BigDecimal(andelTilkjentYtelse.kalkulertUtbetalingsbeløp)) + assertEquals(forventetStønadFom, andelTilkjentYtelse.stønadFom) + assertEquals(forventetStønadTom, andelTilkjentYtelse.stønadTom) + } + + private fun lagAndelTilkjentYtelse(barn: Person, fom: YearMonth, tom: YearMonth) = AndelTilkjentYtelse( + behandlingId = behandling.id, + tilkjentYtelse = tilkjentYtelse, + aktør = barn.aktør, + kalkulertUtbetalingsbeløp = beløp.toInt(), + nasjonaltPeriodebeløp = beløp.toInt(), + stønadFom = fom, + stønadTom = tom, + type = YtelseType.ORDINÆR_BARNETRYGD, + sats = beløp.toInt(), + prosent = BigDecimal(100), + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseUtilsTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseUtilsTest.kt new file mode 100644 index 000000000..f6d6f772c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseUtilsTest.kt @@ -0,0 +1,1410 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.forrigeMåned +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.isSameOrBefore +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.common.til18ÅrsVilkårsdato +import no.nav.familie.ba.sak.common.tilyyyyMMdd +import no.nav.familie.ba.sak.common.toLocalDate +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseUtils.beregnTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseUtils.oppdaterTilkjentYtelseMedEndretUtbetalingAndeler +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.lagDødsfallFraPdl +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.sivilstand.GrSivilstand +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import org.hamcrest.MatcherAssert.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth +import org.hamcrest.CoreMatchers.`is` as Is + +internal class TilkjentYtelseUtilsTest { + + @BeforeEach + fun førHverTest() { + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2022, 12, 31) + } + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + @Test + fun `Barn som fyller 6 år i det vilkårene er oppfylt får andel måneden etter`() { + val barnFødselsdato = LocalDate.of(2016, 2, 2) + val barnSeksårsdag = barnFødselsdato.plusYears(6) + + val (vilkårsvurdering, personopplysningGrunnlag) = + genererVilkårsvurderingOgPersonopplysningGrunnlag( + barnFødselsdato = barnFødselsdato, + vilkårOppfyltFom = barnSeksårsdag, + under18ÅrVilkårOppfyltTom = barnFødselsdato.plusYears(18), + ) + + val tilkjentYtelse = beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + + assertEquals(1, tilkjentYtelse.andelerTilkjentYtelse.size) + + val andelTilkjentYtelse = tilkjentYtelse.andelerTilkjentYtelse.first() + assertEquals( + MånedPeriode( + barnSeksårsdag.nesteMåned(), + barnFødselsdato.plusYears(18).forrigeMåned(), + ), + MånedPeriode(andelTilkjentYtelse.stønadFom, andelTilkjentYtelse.stønadTom), + ) + } + + @Test + fun `Barn som fyller 6 år i det vilkårene ikke lenger er oppfylt får andel den måneden også`() { + val barnSeksårsdag = LocalDate.of(2022, 2, 2) + val barnFødselsdato = barnSeksårsdag.minusYears(6) + + val vilkårOppfyltFom = barnSeksårsdag.minusMonths(2) + val vilkårOppfyltTom = barnSeksårsdag.plusDays(2) + val (vilkårsvurdering, personopplysningGrunnlag) = + genererVilkårsvurderingOgPersonopplysningGrunnlag( + barnFødselsdato = barnFødselsdato, + vilkårOppfyltFom = vilkårOppfyltFom, + vilkårOppfyltTom = vilkårOppfyltTom, + under18ÅrVilkårOppfyltTom = barnFødselsdato.plusYears(18), + ) + + val tilkjentYtelse = beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + + assertEquals(2, tilkjentYtelse.andelerTilkjentYtelse.size) + + val andelTilkjentYtelseFør6År = tilkjentYtelse.andelerTilkjentYtelse.first() + assertEquals( + MånedPeriode(vilkårOppfyltFom.nesteMåned(), barnSeksårsdag.forrigeMåned()), + MånedPeriode(andelTilkjentYtelseFør6År.stønadFom, andelTilkjentYtelseFør6År.stønadTom), + ) + assertEquals(1676, andelTilkjentYtelseFør6År.kalkulertUtbetalingsbeløp) + + val andelTilkjentYtelseEtter6År = tilkjentYtelse.andelerTilkjentYtelse.last() + assertEquals( + MånedPeriode(barnSeksårsdag.toYearMonth(), barnSeksårsdag.toYearMonth()), + MånedPeriode(andelTilkjentYtelseEtter6År.stønadFom, andelTilkjentYtelseEtter6År.stønadTom), + ) + assertEquals(1054, andelTilkjentYtelseEtter6År.kalkulertUtbetalingsbeløp) + } + + @Test + fun `Det skal utbetales for inneværende måned hvis barn dør minst 1 måned før 18 års datoen`() { + val barnFødselsDato = LocalDate.of(2012, 2, 2) + val barnDødsfallsDato = LocalDate.of(2030, 1, 1) + + val (vilkårsvurdering, personopplysningGrunnlag) = + genererVilkårsvurderingOgPersonopplysningGrunnlag( + barnFødselsdato = barnFødselsDato, + vilkårOppfyltFom = barnFødselsDato, + vilkårOppfyltTom = barnDødsfallsDato, + barnDødsfallDato = barnDødsfallsDato, + under18ÅrVilkårOppfyltTom = barnDødsfallsDato, + ) + + val tilkjentYtelse = beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + + assertEquals(2, tilkjentYtelse.andelerTilkjentYtelse.size) + + val andelFør6År = tilkjentYtelse.andelerTilkjentYtelse.first() + val andelEtter6År = tilkjentYtelse.andelerTilkjentYtelse.last() + + assertEquals(YearMonth.of(2014, 2), andelFør6År.stønadFom) + assertEquals(YearMonth.of(2019, 2), andelFør6År.stønadTom) + + assertEquals(YearMonth.of(2019, 3), andelEtter6År.stønadFom) + assertEquals(YearMonth.of(2030, 1), andelEtter6År.stønadTom) + } + + @Test + fun `Det skal ikke utbetales for inneværende måned hvis barn dør samme måned som 18 års datoen`() { + val barnFødselsDato = LocalDate.of(2012, 2, 20) + val barnDødsfallsDato = LocalDate.of(2030, 2, 2) + + val (vilkårsvurdering, personopplysningGrunnlag) = + genererVilkårsvurderingOgPersonopplysningGrunnlag( + barnFødselsdato = barnFødselsDato, + vilkårOppfyltFom = barnFødselsDato, + vilkårOppfyltTom = barnDødsfallsDato, + barnDødsfallDato = barnDødsfallsDato, + under18ÅrVilkårOppfyltTom = barnDødsfallsDato, + ) + + val tilkjentYtelse = beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + + assertEquals(2, tilkjentYtelse.andelerTilkjentYtelse.size) + + val andelFør6År = tilkjentYtelse.andelerTilkjentYtelse.first() + val andelEtter6År = tilkjentYtelse.andelerTilkjentYtelse.last() + + assertEquals(YearMonth.of(2014, 2), andelFør6År.stønadFom) + assertEquals(YearMonth.of(2019, 2), andelFør6År.stønadTom) + + assertEquals(YearMonth.of(2019, 3), andelEtter6År.stønadFom) + assertEquals(YearMonth.of(2030, 1), andelEtter6År.stønadTom) + } + + @Test + fun `1 barn får normal utbetaling med satsendring fra september 2020, september 2021 og januar 2022`() { + val barnFødselsdato = LocalDate.of(2021, 2, 2) + val barnSeksårsdag = barnFødselsdato.plusYears(6) + + val (vilkårsvurdering, personopplysningGrunnlag) = genererVilkårsvurderingOgPersonopplysningGrunnlag( + barnFødselsdato = barnFødselsdato, + vilkårOppfyltFom = barnFødselsdato, + under18ÅrVilkårOppfyltTom = barnFødselsdato.plusYears(18), + ) + + val andeler = beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + .andelerTilkjentYtelse + .toList() + .sortedBy { it.stønadFom } + + assertEquals(4, andeler.size) + + val andelTilkjentYtelseFør6ÅrSeptember2020 = andeler[0] + assertEquals( + MånedPeriode(barnFødselsdato.nesteMåned(), YearMonth.of(2021, 8)), + MånedPeriode( + andelTilkjentYtelseFør6ÅrSeptember2020.stønadFom, + andelTilkjentYtelseFør6ÅrSeptember2020.stønadTom, + ), + ) + assertEquals(1354, andelTilkjentYtelseFør6ÅrSeptember2020.kalkulertUtbetalingsbeløp) + + val andelTilkjentYtelseFør6ÅrSeptember2021 = andeler[1] + assertEquals( + MånedPeriode(YearMonth.of(2021, 9), YearMonth.of(2021, 12)), + MånedPeriode( + andelTilkjentYtelseFør6ÅrSeptember2021.stønadFom, + andelTilkjentYtelseFør6ÅrSeptember2021.stønadTom, + ), + ) + assertEquals(1654, andelTilkjentYtelseFør6ÅrSeptember2021.kalkulertUtbetalingsbeløp) + + val andelTilkjentYtelseFør6ÅrJanuar2022 = andeler[2] + assertEquals( + MånedPeriode(YearMonth.of(2022, 1), barnSeksårsdag.forrigeMåned()), + MånedPeriode( + andelTilkjentYtelseFør6ÅrJanuar2022.stønadFom, + andelTilkjentYtelseFør6ÅrJanuar2022.stønadTom, + ), + ) + assertEquals(1676, andelTilkjentYtelseFør6ÅrJanuar2022.kalkulertUtbetalingsbeløp) + + val andelTilkjentYtelseEtter6År = andeler[3] + assertEquals( + MånedPeriode(barnSeksårsdag.toYearMonth(), barnFødselsdato.plusYears(18).forrigeMåned()), + MånedPeriode(andelTilkjentYtelseEtter6År.stønadFom, andelTilkjentYtelseEtter6År.stønadTom), + ) + assertEquals(1054, andelTilkjentYtelseEtter6År.kalkulertUtbetalingsbeløp) + } + + @Test + fun `Halvt beløp av grunnsats utbetales ved delt bosted`() { + val barnFødselsdato = LocalDate.of(2021, 2, 2) + + val (vilkårsvurdering, personopplysningGrunnlag) = + genererVilkårsvurderingOgPersonopplysningGrunnlag( + barnFødselsdato = barnFødselsdato, + vilkårOppfyltFom = barnFødselsdato, + erDeltBosted = true, + under18ÅrVilkårOppfyltTom = barnFødselsdato.plusYears(18), + ) + + val andeler = beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + .andelerTilkjentYtelse.toList() + .sortedBy { it.stønadFom } + + val andelTilkjentYtelseFør6ÅrSeptember2020 = andeler[0] + assertEquals(677, andelTilkjentYtelseFør6ÅrSeptember2020.kalkulertUtbetalingsbeløp) + + val andelTilkjentYtelseFør6ÅrSeptember2021 = andeler[1] + assertEquals(827, andelTilkjentYtelseFør6ÅrSeptember2021.kalkulertUtbetalingsbeløp) + + val andelTilkjentYtelseFør6ÅrJanuar2022 = andeler[2] + assertEquals(838, andelTilkjentYtelseFør6ÅrJanuar2022.kalkulertUtbetalingsbeløp) + + val andelTilkjentYtelseEtter6År = andeler[3] + assertEquals(527, andelTilkjentYtelseEtter6År.kalkulertUtbetalingsbeløp) + } + + @Test + fun `Skal opprette riktig tilkjent ytelse-perioder for perioder på barn som ikke er back-to-back over månedskiftet`() { + val barnFødselsdato = LocalDate.of(2016, 2, 5) + + val (vilkårsvurdering, personopplysningGrunnlag) = + genererVilkårsvurderingOgPersonopplysningGrunnlag( + barnFødselsdato = barnFødselsdato, + vilkårOppfyltFom = barnFødselsdato, + erDeltBosted = true, + under18ÅrVilkårOppfyltTom = barnFødselsdato.plusYears(18), + ) + + val oppdatertVilkårsvurdering = oppdaterBosattIRiketMedBack2BackPerioder( + vilkårsvurdering = vilkårsvurdering, + personResultat = vilkårsvurdering.personResultater.find { !it.erSøkersResultater() }!!, + barnFødselsdato = barnFødselsdato, + backToBackTom = LocalDate.of(2019, 8, 31), + backToBackFom = LocalDate.of(2019, 9, 2), + ) + + val andeler = beregnTilkjentYtelse( + vilkårsvurdering = oppdatertVilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + .andelerTilkjentYtelse.toList() + .sortedBy { it.stønadFom } + + assertEquals(YearMonth.of(2019, 8), andeler[1].stønadTom) + assertEquals(YearMonth.of(2019, 10), andeler[2].stønadFom) + } + + @Test + fun `Skal opprette riktig tilkjent ytelse-perioder for perioder på søker som ikke er back-to-back over månedskiftet`() { + val barnFødselsdato = LocalDate.of(2016, 2, 5) + + val (vilkårsvurdering, personopplysningGrunnlag) = + genererVilkårsvurderingOgPersonopplysningGrunnlag( + barnFødselsdato = barnFødselsdato, + vilkårOppfyltFom = barnFødselsdato, + erDeltBosted = true, + under18ÅrVilkårOppfyltTom = barnFødselsdato.plusYears(18), + ) + + val oppdatertVilkårsvurdering = oppdaterBosattIRiketMedBack2BackPerioder( + vilkårsvurdering = vilkårsvurdering, + personResultat = vilkårsvurdering.personResultater.find { it.erSøkersResultater() }!!, + barnFødselsdato = barnFødselsdato, + backToBackTom = LocalDate.of(2019, 8, 31), + backToBackFom = LocalDate.of(2019, 9, 2), + ) + + val andeler = beregnTilkjentYtelse( + vilkårsvurdering = oppdatertVilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + .andelerTilkjentYtelse.toList() + .sortedBy { it.stønadFom } + + assertEquals(YearMonth.of(2019, 8), andeler[1].stønadTom) + assertEquals(YearMonth.of(2019, 10), andeler[2].stønadFom) + } + + private fun oppdaterBosattIRiketMedBack2BackPerioder( + vilkårsvurdering: Vilkårsvurdering, + personResultat: PersonResultat, + barnFødselsdato: LocalDate, + backToBackTom: LocalDate? = null, + backToBackFom: LocalDate? = null, + ): Vilkårsvurdering { + personResultat.setSortedVilkårResultater( + personResultat.vilkårResultater.filter { it.vilkårType != Vilkår.BOSATT_I_RIKET } + .toSet() + + setOf( + VilkårResultat( + personResultat = personResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = barnFødselsdato, + periodeTom = backToBackTom, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ), + VilkårResultat( + personResultat = personResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = backToBackFom, + periodeTom = null, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ), + ), + ) + + vilkårsvurdering.personResultater = + vilkårsvurdering.personResultater.filter { it.aktør != personResultat.aktør }.toSet() + setOf( + personResultat, + ) + + return vilkårsvurdering + } + + private fun genererVilkårsvurderingOgPersonopplysningGrunnlag( + barnFødselsdato: LocalDate, + vilkårOppfyltFom: LocalDate, + vilkårOppfyltTom: LocalDate? = barnFødselsdato.plusYears(18), + barnDødsfallDato: LocalDate? = null, + erDeltBosted: Boolean = false, + under18ÅrVilkårOppfyltTom: LocalDate?, + ): Pair { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val søkerAktørId = tilAktør(søkerFnr) + val barnAktørId = tilAktør(barnFnr) + + val behandling = lagBehandling() + + val vilkårsvurdering = lagVilkårsvurdering( + søkerAktør = søkerAktørId, + behandling = behandling, + resultat = Resultat.OPPFYLT, + søkerPeriodeFom = LocalDate.of(2014, 1, 1), + søkerPeriodeTom = null, + ) + + val barnResultat = + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = barnAktørId) + barnResultat.setSortedVilkårResultater( + setOf( + VilkårResultat( + personResultat = barnResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = vilkårOppfyltFom, + periodeTom = vilkårOppfyltTom, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + VilkårResultat( + personResultat = barnResultat, + vilkårType = Vilkår.UNDER_18_ÅR, + resultat = Resultat.OPPFYLT, + periodeFom = barnFødselsdato, + periodeTom = under18ÅrVilkårOppfyltTom, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + VilkårResultat( + personResultat = barnResultat, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + periodeFom = barnFødselsdato, + periodeTom = null, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + VilkårResultat( + personResultat = barnResultat, + vilkårType = Vilkår.GIFT_PARTNERSKAP, + resultat = Resultat.OPPFYLT, + periodeFom = barnFødselsdato, + periodeTom = null, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + VilkårResultat( + personResultat = barnResultat, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + periodeFom = barnFødselsdato, + periodeTom = null, + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + utdypendeVilkårsvurderinger = listOfNotNull( + if (erDeltBosted) UtdypendeVilkårsvurdering.DELT_BOSTED else null, + ), + ), + ), + ) + + vilkårsvurdering.personResultater = setOf(vilkårsvurdering.personResultater.first(), barnResultat) + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id) + + val barn = Person( + aktør = tilAktør(barnFnr), + type = PersonType.BARN, + personopplysningGrunnlag = personopplysningGrunnlag, + fødselsdato = barnFødselsdato, + navn = "Barn", + kjønn = Kjønn.MANN, + ) + .apply { + sivilstander = mutableListOf(GrSivilstand(type = SIVILSTAND.UGIFT, person = this)) + barnDødsfallDato?.let { dødsfall = lagDødsfallFraPdl(this, it.tilyyyyMMdd(), null) } + } + val søker = Person( + aktør = tilAktør(søkerFnr), + type = PersonType.SØKER, + personopplysningGrunnlag = personopplysningGrunnlag, + fødselsdato = barnFødselsdato.minusYears(20), + navn = "Barn", + kjønn = Kjønn.MANN, + ) + .apply { sivilstander = mutableListOf(GrSivilstand(type = SIVILSTAND.UGIFT, person = this)) } + personopplysningGrunnlag.personer.add(søker) + personopplysningGrunnlag.personer.add(barn) + + return Pair(vilkårsvurdering, personopplysningGrunnlag) + } + + @Test + fun `endret utbetalingsandel skal overstyre andel`() { + val person = lagPerson() + val behandling = lagBehandling() + val fom = YearMonth.of(2018, 1) + val tom = YearMonth.of(2019, 1) + val utbetalinsandeler = listOf( + lagAndelTilkjentYtelse( + fom = fom, + tom = tom, + person = person, + behandling = behandling, + ), + ) + + val endretProsent = BigDecimal.ZERO + + val endretUtbetalingAndeler = listOf( + lagEndretUtbetalingAndelMedAndelerTilkjentYtelse( + person = person, + fom = fom, + tom = tom, + prosent = endretProsent, + behandlingId = behandling.id, + ), + ) + + val andelerTIlkjentYtelse = oppdaterTilkjentYtelseMedEndretUtbetalingAndeler( + utbetalinsandeler, + endretUtbetalingAndeler, + ) + + assertEquals(1, andelerTIlkjentYtelse.size) + assertEquals(endretProsent, andelerTIlkjentYtelse.single().prosent) + assertEquals(1, andelerTIlkjentYtelse.single().endreteUtbetalinger.size) + } + + @Test + fun `endret utbetalingsandel koble endrede andeler til riktig endret utbetalingandel`() { + val person = lagPerson() + val behandling = lagBehandling() + val fom1 = YearMonth.of(2018, 1) + val tom1 = YearMonth.of(2018, 11) + + val fom2 = YearMonth.of(2019, 1) + val tom2 = YearMonth.of(2019, 11) + + val utbetalinsandeler = listOf( + lagAndelTilkjentYtelse( + fom = fom1, + tom = tom1, + person = person, + behandling = behandling, + ), + lagAndelTilkjentYtelse( + fom = fom2, + tom = tom2, + person = person, + behandling = behandling, + ), + ) + + val endretProsent = BigDecimal.ZERO + + val endretUtbetalingAndel = lagEndretUtbetalingAndelMedAndelerTilkjentYtelse( + person = person, + fom = fom1, + tom = tom2, + prosent = endretProsent, + behandlingId = behandling.id, + ) + + val endretUtbetalingAndeler = listOf( + endretUtbetalingAndel, + lagEndretUtbetalingAndelMedAndelerTilkjentYtelse( + person = person, + fom = tom2.nesteMåned(), + prosent = endretProsent, + behandlingId = behandling.id, + ), + ) + + val andelerTIlkjentYtelse = oppdaterTilkjentYtelseMedEndretUtbetalingAndeler( + utbetalinsandeler, + endretUtbetalingAndeler, + ) + + assertEquals(2, andelerTIlkjentYtelse.size) + andelerTIlkjentYtelse.forEach { assertEquals(endretProsent, it.prosent) } + andelerTIlkjentYtelse.forEach { assertEquals(1, it.endreteUtbetalinger.size) } + andelerTIlkjentYtelse.forEach { + assertEquals( + endretUtbetalingAndel.id, + it.endreteUtbetalinger.single().id, + ) + } + } + + val søker = lagPerson(type = PersonType.SØKER) + val januar2019 = YearMonth.of(2019, 1) + val februar2019 = YearMonth.of(2019, 2) + val mars2019 = YearMonth.of(2019, 3) + val april2019 = YearMonth.of(2019, 4) + val juli2019 = YearMonth.of(2019, 7) + val august2019 = YearMonth.of(2019, 8) + val november2019 = YearMonth.of(2019, 11) + val desember2019 = YearMonth.of(2019, 12) + val august2020 = YearMonth.of(2020, 8) + val januar2022 = YearMonth.of(2022, 1) + val februar2022 = YearMonth.of(2022, 2) + val mars2022 = YearMonth.of(2022, 3) + val april2022 = YearMonth.of(2022, 4) + val mai2022 = YearMonth.of(2022, 5) + val juni2022 = YearMonth.of(2022, 6) + val juli2022 = YearMonth.of(2022, 7) + val august2022 = YearMonth.of(2022, 8) + val november2022 = YearMonth.of(2022, 11) + val desember2022 = YearMonth.of(2022, 12) + + // src/test/resources/scenario/Far søker om delt bosted - Mor har tidligere mottatt fult utvidet og ordinær barnetrygd + @Test + fun `Skal støtte endret utbetaling som delvis overlapper delt bosted på søker og barn og småbarnstillegg på søker`() { + val barnFødtAugust2019 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2019, 8, 15)) + val månedFørBarnBlir18 = barnFødtAugust2019.fødselsdato.til18ÅrsVilkårsdato().minusMonths(1).toYearMonth() + val månedFørBarnBlir6 = barnFødtAugust2019.fødselsdato.plusYears(6).minusMonths(1).toYearMonth() + val barnFyller3ÅrDato = barnFødtAugust2019.fødselsdato.plusYears(3).toYearMonth() + + val tilkjentYtelse = settOppScenarioOgBeregnTilkjentYtelse( + endretAndeler = listOf( + EndretAndel( + person = søker, + skalUtbetales = false, + årsak = Årsak.DELT_BOSTED, + fom = april2022, + tom = juli2022, + ), + EndretAndel( + person = barnFødtAugust2019, + skalUtbetales = false, + årsak = Årsak.DELT_BOSTED, + fom = april2022, + tom = juli2022, + ), + ), + atypiskeVilkårBarna = listOf( + AtypiskVilkår( + fom = mars2022.toLocalDate().førsteDagIInneværendeMåned(), + vilkårType = Vilkår.BOR_MED_SØKER, + utdypendeVilkårsvurdering = UtdypendeVilkårsvurdering.DELT_BOSTED, + aktør = barnFødtAugust2019.aktør, + ), + ), + barna = listOf(barnFødtAugust2019), + overgangsstønadPerioder = listOf(MånedPeriode(januar2022, november2022)), + ) + + val andelerTilkjentYtelseITidsrom = + tilkjentYtelse.andelerTilkjentYtelse.filter { it.stønadFom.isSameOrBefore(desember2022) } + assertEquals(6, andelerTilkjentYtelseITidsrom.size) + + val (søkersAndeler, barnasAndeler) = andelerTilkjentYtelseITidsrom.partition { it.erSøkersAndel() } + + // SØKER + val (utvidetAndeler, småbarnstilleggAndeler) = søkersAndeler.partition { it.erUtvidet() } + + assertEquals(2, utvidetAndeler.size) + assertThat(utvidetAndeler[0].stønadFom, Is(april2022)) + assertThat(utvidetAndeler[0].stønadTom, Is(juli2022)) + assertThat(utvidetAndeler[0].prosent, Is(BigDecimal.ZERO)) + assertThat(utvidetAndeler[1].stønadFom, Is(august2022)) + assertThat(utvidetAndeler[1].stønadTom, Is(månedFørBarnBlir18)) + assertThat(utvidetAndeler[1].prosent, Is(BigDecimal(50))) + + assertEquals(2, småbarnstilleggAndeler.size) + assertThat(småbarnstilleggAndeler[0].stønadFom, Is(april2022)) + assertThat(småbarnstilleggAndeler[0].stønadTom, Is(juli2022)) + assertThat(småbarnstilleggAndeler[0].prosent, Is(BigDecimal.ZERO)) + assertThat(småbarnstilleggAndeler[1].stønadFom, Is(august2022)) + assertThat(småbarnstilleggAndeler[1].stønadTom, Is(barnFyller3ÅrDato)) + assertThat(småbarnstilleggAndeler[1].prosent, Is(BigDecimal(100))) + + assertEquals(2, barnasAndeler.size) + assertThat(barnasAndeler[0].stønadFom, Is(april2022)) + assertThat(barnasAndeler[0].stønadTom, Is(juli2022)) + assertThat(barnasAndeler[0].prosent, Is(BigDecimal.ZERO)) + assertThat(barnasAndeler[1].stønadFom, Is(august2022)) + assertThat(barnasAndeler[1].stønadTom, Is(månedFørBarnBlir6)) + assertThat(barnasAndeler[1].prosent, Is(BigDecimal(50))) + } + + // src/test/resources/scenario/Far søker om delt bosted - Mor har tidligere mottatt fult, men har ikke mottatt utvidet + @Test + fun `Skal støtte endret utbetaling som kun gjelder barn på delt bosted utbetaling`() { + val barnFødtAugust2019 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2019, 8, 15)) + val månedFørBarnBlir18 = barnFødtAugust2019.fødselsdato.til18ÅrsVilkårsdato().minusMonths(1).toYearMonth() + val månedFørBarnBlir6 = barnFødtAugust2019.fødselsdato.plusYears(6).minusMonths(1).toYearMonth() + val barnFyller3ÅrDato = barnFødtAugust2019.fødselsdato.plusYears(3).toYearMonth() + + val tilkjentYtelse = settOppScenarioOgBeregnTilkjentYtelse( + endretAndeler = listOf( + EndretAndel( + person = barnFødtAugust2019, + skalUtbetales = false, + årsak = Årsak.DELT_BOSTED, + fom = april2022, + tom = juli2022, + ), + ), + atypiskeVilkårBarna = listOf( + AtypiskVilkår( + fom = mars2022.toLocalDate().førsteDagIInneværendeMåned(), + vilkårType = Vilkår.BOR_MED_SØKER, + utdypendeVilkårsvurdering = UtdypendeVilkårsvurdering.DELT_BOSTED, + aktør = barnFødtAugust2019.aktør, + ), + ), + barna = listOf(barnFødtAugust2019), + overgangsstønadPerioder = listOf(MånedPeriode(januar2022, november2022)), + ) + + val andelerTilkjentYtelseITidsrom = + tilkjentYtelse.andelerTilkjentYtelse.filter { it.stønadFom.isSameOrBefore(desember2022) } + assertEquals(4, andelerTilkjentYtelseITidsrom.size) + + val (søkersAndeler, barnasAndeler) = andelerTilkjentYtelseITidsrom.partition { it.erSøkersAndel() } + + // SØKER + val (utvidetAndeler, småbarnstilleggAndeler) = søkersAndeler.partition { it.erUtvidet() } + + assertEquals(1, utvidetAndeler.size) + assertThat(utvidetAndeler[0].stønadFom, Is(april2022)) + assertThat(utvidetAndeler[0].stønadTom, Is(månedFørBarnBlir18)) + assertThat(utvidetAndeler[0].prosent, Is(BigDecimal(50))) + + assertEquals(1, småbarnstilleggAndeler.size) + assertThat(småbarnstilleggAndeler[0].stønadFom, Is(april2022)) + assertThat(småbarnstilleggAndeler[0].stønadTom, Is(barnFyller3ÅrDato)) + assertThat(småbarnstilleggAndeler[0].prosent, Is(BigDecimal(100))) + + // BARN + assertEquals(2, barnasAndeler.size) + assertThat(barnasAndeler[0].stønadFom, Is(april2022)) + assertThat(barnasAndeler[0].stønadTom, Is(juli2022)) + assertThat(barnasAndeler[0].prosent, Is(BigDecimal.ZERO)) + assertThat(barnasAndeler[1].stønadFom, Is(august2022)) + assertThat(barnasAndeler[1].stønadTom, Is(månedFørBarnBlir6)) + assertThat(barnasAndeler[1].prosent, Is(BigDecimal(50))) + } + + // src/test/resources/scenario/Mor har tidligere mottatt barnetrygden - Far har nå søkt om delt bosted og mors barnetrygd skal også deles + @Test + fun `Skal gi riktig resultat når barnetrygden går over til å være delt, kun småbarnstillegg og utvidet blir delt i første periode`() { + val barnFødtAugust2019 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2019, 8, 15)) + val månedFørBarnBlir18 = barnFødtAugust2019.fødselsdato.til18ÅrsVilkårsdato().minusMonths(1).toYearMonth() + val månedFørBarnBlir6 = barnFødtAugust2019.fødselsdato.plusYears(6).minusMonths(1).toYearMonth() + val barnFyller3ÅrDato = barnFødtAugust2019.fødselsdato.plusYears(3).toYearMonth() + + val tilkjentYtelse = settOppScenarioOgBeregnTilkjentYtelse( + endretAndeler = listOf( + EndretAndel( + person = barnFødtAugust2019, + skalUtbetales = true, + årsak = Årsak.DELT_BOSTED, + fom = juni2022, + tom = juli2022, + ), + ), + atypiskeVilkårBarna = listOf( + AtypiskVilkår( + fom = februar2022.toLocalDate().førsteDagIInneværendeMåned(), + tom = april2022.toLocalDate().sisteDagIMåned(), + vilkårType = Vilkår.BOR_MED_SØKER, + aktør = barnFødtAugust2019.aktør, + ), + AtypiskVilkår( + fom = mai2022.toLocalDate().førsteDagIInneværendeMåned(), + vilkårType = Vilkår.BOR_MED_SØKER, + aktør = barnFødtAugust2019.aktør, + utdypendeVilkårsvurdering = UtdypendeVilkårsvurdering.DELT_BOSTED, + ), + ), + atypiskeVilkårSøker = listOf( + AtypiskVilkår( + fom = mai2022.toLocalDate().førsteDagIInneværendeMåned(), + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + aktør = søker.aktør, + ), + ), + overgangsstønadPerioder = listOf(MånedPeriode(januar2022, november2022)), + barna = listOf(barnFødtAugust2019), + ) + + val andelerTilkjentYtelseITidsrom = + tilkjentYtelse.andelerTilkjentYtelse.filter { it.stønadFom.isSameOrBefore(desember2022) } + assertEquals(5, andelerTilkjentYtelseITidsrom.size) + + val (søkersAndeler, barnasAndeler) = andelerTilkjentYtelseITidsrom.partition { it.erSøkersAndel() } + + // SØKER + val (utvidetAndeler, småbarnstilleggAndeler) = søkersAndeler.partition { it.erUtvidet() } + + assertEquals(1, utvidetAndeler.size) + assertThat(utvidetAndeler[0].stønadFom, Is(juni2022)) + assertThat(utvidetAndeler[0].stønadTom, Is(månedFørBarnBlir18)) + assertThat(utvidetAndeler[0].prosent, Is(BigDecimal(50))) + + assertEquals(1, småbarnstilleggAndeler.size) + assertThat(småbarnstilleggAndeler[0].stønadFom, Is(juni2022)) + assertThat(småbarnstilleggAndeler[0].stønadTom, Is(barnFyller3ÅrDato)) + assertThat(småbarnstilleggAndeler[0].prosent, Is(BigDecimal(100))) + + // BARN + assertEquals(3, barnasAndeler.size) + assertThat(barnasAndeler[0].stønadFom, Is(mars2022)) + assertThat(barnasAndeler[0].stønadTom, Is(mai2022)) + assertThat(barnasAndeler[0].prosent, Is(BigDecimal(100))) + assertThat(barnasAndeler[1].stønadFom, Is(juni2022)) + assertThat(barnasAndeler[1].stønadTom, Is(juli2022)) + assertThat(barnasAndeler[1].prosent, Is(BigDecimal(100))) + assertThat(barnasAndeler[2].stønadFom, Is(august2022)) + assertThat(barnasAndeler[2].stønadTom, Is(månedFørBarnBlir6)) + assertThat(barnasAndeler[2].prosent, Is(BigDecimal(50))) + } + + // src/test/resources/scenario/Mor har tidligere mottatt barnetrygden - Far har nå søkt om delt bosted og mors barnetrygd skal også deles 2 + @Test + fun `Delt, utvidet og ordinær barnetrygd deles fra juni, men skal utbetales fult fra juni til og med juli - deles som vanlig fra August`() { + val barnFødtAugust2019 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2019, 8, 15)) + val månedFørBarnBlir18 = barnFødtAugust2019.fødselsdato.til18ÅrsVilkårsdato().minusMonths(1).toYearMonth() + val månedFørBarnBlir6 = barnFødtAugust2019.fødselsdato.plusYears(6).minusMonths(1).toYearMonth() + val barnFyller3ÅrDato = barnFødtAugust2019.fødselsdato.plusYears(3).toYearMonth() + + val tilkjentYtelse = settOppScenarioOgBeregnTilkjentYtelse( + endretAndeler = listOf( + EndretAndel( + person = barnFødtAugust2019, + skalUtbetales = true, + årsak = Årsak.DELT_BOSTED, + fom = juni2022, + tom = juli2022, + ), + EndretAndel( + person = søker, + skalUtbetales = true, + årsak = Årsak.DELT_BOSTED, + fom = juni2022, + tom = juli2022, + ), + ), + atypiskeVilkårBarna = listOf( + AtypiskVilkår( + fom = februar2022.toLocalDate().førsteDagIInneværendeMåned(), + tom = april2022.toLocalDate().sisteDagIMåned(), + vilkårType = Vilkår.BOR_MED_SØKER, + aktør = barnFødtAugust2019.aktør, + ), + AtypiskVilkår( + fom = mai2022.toLocalDate().førsteDagIInneværendeMåned(), + vilkårType = Vilkår.BOR_MED_SØKER, + aktør = barnFødtAugust2019.aktør, + utdypendeVilkårsvurdering = UtdypendeVilkårsvurdering.DELT_BOSTED, + ), + ), + atypiskeVilkårSøker = listOf( + AtypiskVilkår( + fom = februar2022.toLocalDate().førsteDagIInneværendeMåned(), + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + aktør = søker.aktør, + ), + ), + barna = listOf(barnFødtAugust2019), + overgangsstønadPerioder = listOf(MånedPeriode(januar2022, november2022)), + ) + + val andelerTilkjentYtelseITidsrom = + tilkjentYtelse.andelerTilkjentYtelse.filter { it.stønadFom.isSameOrBefore(desember2022) } + assertEquals(7, andelerTilkjentYtelseITidsrom.size) + + val (søkersAndeler, barnasAndeler) = andelerTilkjentYtelseITidsrom.partition { it.erSøkersAndel() } + val (utvidetAndeler, småbarnstilleggAndeler) = søkersAndeler.partition { it.erUtvidet() } + + assertEquals(3, utvidetAndeler.size) + assertThat(utvidetAndeler[0].stønadFom, Is(mars2022)) + assertThat(utvidetAndeler[0].stønadTom, Is(mai2022)) + assertThat(utvidetAndeler[0].prosent, Is(BigDecimal(100))) + assertThat(utvidetAndeler[1].stønadFom, Is(juni2022)) + assertThat(utvidetAndeler[1].stønadTom, Is(juli2022)) + assertThat(utvidetAndeler[1].prosent, Is(BigDecimal(100))) + assertThat(utvidetAndeler[2].stønadFom, Is(august2022)) + assertThat(utvidetAndeler[2].stønadTom, Is(månedFørBarnBlir18)) + assertThat(utvidetAndeler[2].prosent, Is(BigDecimal(50))) + + assertEquals(1, småbarnstilleggAndeler.size) + assertThat(småbarnstilleggAndeler[0].stønadFom, Is(mars2022)) + assertThat(småbarnstilleggAndeler[0].stønadTom, Is(barnFyller3ÅrDato)) + assertThat(småbarnstilleggAndeler[0].prosent, Is(BigDecimal(100))) + + assertEquals(3, barnasAndeler.size) + assertThat(barnasAndeler[0].stønadFom, Is(mars2022)) + assertThat(barnasAndeler[0].stønadTom, Is(mai2022)) + assertThat(barnasAndeler[0].prosent, Is(BigDecimal(100))) + assertThat(barnasAndeler[1].stønadFom, Is(juni2022)) + assertThat(barnasAndeler[1].stønadTom, Is(juli2022)) + assertThat(barnasAndeler[1].prosent, Is(BigDecimal(100))) + assertThat(barnasAndeler[2].stønadFom, Is(august2022)) + assertThat(barnasAndeler[2].stønadTom, Is(månedFørBarnBlir6)) + assertThat(barnasAndeler[2].prosent, Is(BigDecimal(50))) + } + + // src/test/resources/scenario/Far søker om utvidet barnetrygd - Har full overgangsstønad, men søker sent og får ikke etterbetalt mer enn 3år + @Test + fun `Småbarnstillleg, utvidet og ordinær barnetrygd fra april, men skal ikke utbetales før august på grunn av etterbetaling 3 år`() { + val barnFødtAugust2016 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2016, 8, 15)) + val månedFørBarnBlir18 = barnFødtAugust2016.fødselsdato.til18ÅrsVilkårsdato().minusMonths(1).toYearMonth() + + val tilkjentYtelse = settOppScenarioOgBeregnTilkjentYtelse( + endretAndeler = listOf( + EndretAndel( + person = barnFødtAugust2016, + skalUtbetales = false, + årsak = Årsak.ETTERBETALING_3ÅR, + fom = april2019, + tom = juli2019, + ), + EndretAndel( + person = søker, + skalUtbetales = false, + årsak = Årsak.ETTERBETALING_3ÅR, + fom = april2019, + tom = juli2019, + ), + ), + atypiskeVilkårBarna = listOf( + AtypiskVilkår( + fom = mars2019.toLocalDate().førsteDagIInneværendeMåned(), + tom = null, + vilkårType = Vilkår.BOR_MED_SØKER, + aktør = barnFødtAugust2016.aktør, + ), + ), + barna = listOf(barnFødtAugust2016), + overgangsstønadPerioder = listOf(MånedPeriode(januar2019, november2019)), + ) + + val andelerTilkjentYtelseITidsrom = + tilkjentYtelse.andelerTilkjentYtelse.filter { it.stønadFom.isSameOrBefore(desember2019) } + assertEquals(6, andelerTilkjentYtelseITidsrom.size) + + val (søkersAndeler, barnasAndeler) = andelerTilkjentYtelseITidsrom.partition { it.erSøkersAndel() } + val (utvidetAndeler, småbarnstilleggAndeler) = søkersAndeler.partition { it.erUtvidet() } + + assertEquals(2, utvidetAndeler.size) + assertThat(utvidetAndeler[0].stønadFom, Is(april2019)) + assertThat(utvidetAndeler[0].stønadTom, Is(juli2019)) + assertThat(utvidetAndeler[0].prosent, Is(BigDecimal(0))) + assertThat(utvidetAndeler[1].stønadFom, Is(august2019)) + assertThat(utvidetAndeler[1].stønadTom, Is(månedFørBarnBlir18)) + assertThat(utvidetAndeler[1].prosent, Is(BigDecimal(100))) + + assertEquals(2, småbarnstilleggAndeler.size) + assertThat(småbarnstilleggAndeler[0].stønadFom, Is(april2019)) + assertThat(småbarnstilleggAndeler[0].stønadTom, Is(juli2019)) + assertThat(småbarnstilleggAndeler[0].prosent, Is(BigDecimal(0))) + assertThat(småbarnstilleggAndeler[1].stønadFom, Is(august2019)) + assertThat(småbarnstilleggAndeler[1].stønadTom, Is(august2019)) + assertThat(småbarnstilleggAndeler[1].prosent, Is(BigDecimal(100))) + + // BARN + assertEquals(2, barnasAndeler.size) + assertThat(barnasAndeler[0].stønadFom, Is(april2019)) + assertThat(barnasAndeler[0].stønadTom, Is(juli2019)) + assertThat(barnasAndeler[0].prosent, Is(BigDecimal(0))) + assertThat(barnasAndeler[1].stønadFom, Is(august2019)) + assertThat(barnasAndeler[1].stønadTom, Is(august2020)) + assertThat(barnasAndeler[1].prosent, Is(BigDecimal(100))) + } + + // src/test/resources/scenario/Far har mottatt delt utvidet barnetrygd for barn 12år - Søker nå om barnetrygd for barn som flyttet til han for over 3 år siden + @Test + fun `Det er småbarnstillegg på søker og ordinær barnetrygd på barn 1 fra april, men det skal ikke utbetales før august på grunn av etterbetaling 3 år - Søker og barn 2 har utbetalinger fra tidligere behandlinger som ikke skal overstyres`() { + val barnFødtAugust2016 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2016, 8, 15)) + val månedFørBarnFødtAugust2016Blir18 = + barnFødtAugust2016.fødselsdato.til18ÅrsVilkårsdato().minusMonths(1).toYearMonth() + val barnFødtDesember2006 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2006, 12, 1)) + val månedFørBarnFødtDesember2006Blir18 = barnFødtDesember2006.fødselsdato.til18ÅrsVilkårsdato().toYearMonth() + + val tilkjentYtelse = settOppScenarioOgBeregnTilkjentYtelse( + endretAndeler = listOf( + EndretAndel( + person = barnFødtAugust2016, + skalUtbetales = false, + årsak = Årsak.ETTERBETALING_3ÅR, + fom = april2019, + tom = juli2019, + ), + ), + atypiskeVilkårBarna = listOf( + AtypiskVilkår( + fom = mars2019.toLocalDate().førsteDagIInneværendeMåned(), + tom = null, + vilkårType = Vilkår.BOR_MED_SØKER, + aktør = barnFødtAugust2016.aktør, + ), + AtypiskVilkår( + fom = januar2019.toLocalDate().førsteDagIInneværendeMåned(), + tom = null, + vilkårType = Vilkår.BOR_MED_SØKER, + aktør = barnFødtDesember2006.aktør, + utdypendeVilkårsvurdering = UtdypendeVilkårsvurdering.DELT_BOSTED, + ), + ), + atypiskeVilkårSøker = listOf( + AtypiskVilkår( + fom = februar2019.toLocalDate().førsteDagIInneværendeMåned(), + tom = null, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + aktør = søker.aktør, + ), + ), + barna = listOf(barnFødtAugust2016, barnFødtDesember2006), + overgangsstønadPerioder = listOf(MånedPeriode(januar2019, november2019)), + ) + + val andelerTilkjentYtelseITidsrom = + tilkjentYtelse.andelerTilkjentYtelse.filter { it.stønadFom.isSameOrBefore(desember2019) } + assertEquals(8, andelerTilkjentYtelseITidsrom.size) + + val (søkersAndeler, barnasAndeler) = andelerTilkjentYtelseITidsrom.partition { it.erSøkersAndel() } + + // SØKER + val (utvidetAndeler, småbarnstilleggAndeler) = søkersAndeler.partition { it.erUtvidet() } + assertEquals(2, utvidetAndeler.size) + assertThat(utvidetAndeler[0].stønadFom, Is(mars2019)) + assertThat(utvidetAndeler[0].stønadTom, Is(juli2019)) + assertThat(utvidetAndeler[0].prosent, Is(BigDecimal(50))) + assertThat(utvidetAndeler[1].stønadFom, Is(august2019)) + assertThat(utvidetAndeler[1].stønadTom, Is(månedFørBarnFødtAugust2016Blir18)) + assertThat(utvidetAndeler[1].prosent, Is(BigDecimal(100))) + + assertEquals(2, småbarnstilleggAndeler.size) + assertThat(småbarnstilleggAndeler[0].stønadFom, Is(april2019)) + assertThat(småbarnstilleggAndeler[0].stønadTom, Is(juli2019)) + assertThat(småbarnstilleggAndeler[0].prosent, Is(BigDecimal(0))) + assertThat(småbarnstilleggAndeler[1].stønadFom, Is(august2019)) + assertThat(småbarnstilleggAndeler[1].stønadTom, Is(august2019)) + assertThat(småbarnstilleggAndeler[1].prosent, Is(BigDecimal(100))) + + // BARN + val (barn1Andeler, barn2Andeler) = barnasAndeler.partition { it.aktør == barnFødtAugust2016.aktør } + assertEquals(2, barn1Andeler.size) + assertThat(barn1Andeler[0].stønadFom, Is(april2019)) + assertThat(barn1Andeler[0].stønadTom, Is(juli2019)) + assertThat(barn1Andeler[0].prosent, Is(BigDecimal(0))) + assertThat(barn1Andeler[1].stønadFom, Is(august2019)) + assertThat(barn1Andeler[1].stønadTom, Is(august2020)) + assertThat(barn1Andeler[1].prosent, Is(BigDecimal(100))) + + assertEquals(2, barn2Andeler.size) + assertThat(barn2Andeler[0].stønadFom, Is(februar2019)) + assertThat(barn2Andeler[0].stønadTom, Is(februar2019)) + assertThat(barn2Andeler[0].prosent, Is(BigDecimal(50))) + assertThat(barn2Andeler[1].stønadFom, Is(mars2019)) + assertThat(barn2Andeler[1].stønadTom, Is(månedFørBarnFødtDesember2006Blir18)) + assertThat(barn2Andeler[1].prosent, Is(BigDecimal(50))) + } + + // src/test/resources/scenario/Far søker om utvidet barnetrygd for barn under 3 år - han har full overgangsstlnad for bare deler av perioden + @Test + fun `Skal gi riktig resultat når det overgangsstønad i deler av utbetalingen`() { + val barnFødtAugust2019 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2019, 8, 15)) + val månedFørBarnBlir6 = barnFødtAugust2019.fødselsdato.plusYears(6).minusMonths(1).toYearMonth() + + val tilkjentYtelse = settOppScenarioOgBeregnTilkjentYtelse( + atypiskeVilkårBarna = listOf( + AtypiskVilkår( + fom = februar2022.toLocalDate().førsteDagIInneværendeMåned(), + tom = null, + vilkårType = Vilkår.BOR_MED_SØKER, + aktør = barnFødtAugust2019.aktør, + ), + ), + atypiskeVilkårSøker = listOf( + AtypiskVilkår( + fom = januar2022.toLocalDate().førsteDagIInneværendeMåned(), + tom = august2022.toLocalDate().sisteDagIMåned(), + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + aktør = søker.aktør, + ), + ), + barna = listOf(barnFødtAugust2019), + overgangsstønadPerioder = listOf(MånedPeriode(april2022, juni2022)), + ) + + val andelerTilkjentYtelseITidsrom = + tilkjentYtelse.andelerTilkjentYtelse.filter { it.stønadFom.isSameOrBefore(desember2022) } + assertEquals(3, andelerTilkjentYtelseITidsrom.size) + + val (søkersAndeler, barnasAndeler) = andelerTilkjentYtelseITidsrom.partition { it.erSøkersAndel() } + val (utvidetAndeler, småbarnstilleggAndeler) = søkersAndeler.partition { it.erUtvidet() } + + assertEquals(1, utvidetAndeler.size) + assertThat(utvidetAndeler[0].stønadFom, Is(mars2022)) + assertThat(utvidetAndeler[0].stønadTom, Is(august2022)) + assertThat(utvidetAndeler[0].prosent, Is(BigDecimal(100))) + + assertEquals(1, småbarnstilleggAndeler.size) + assertThat(småbarnstilleggAndeler[0].stønadFom, Is(april2022)) + assertThat(småbarnstilleggAndeler[0].stønadTom, Is(juni2022)) + assertThat(småbarnstilleggAndeler[0].prosent, Is(BigDecimal(100))) + + assertEquals(1, barnasAndeler.size) + assertThat(barnasAndeler[0].stønadFom, Is(mars2022)) + assertThat(barnasAndeler[0].stønadTom, Is(månedFørBarnBlir6)) + assertThat(barnasAndeler[0].prosent, Is(BigDecimal(100))) + } + + // src/test/resources/scenario/Far søker om utvidet barnetrygd for barn under 3år, men oppfyller vilkårene kun tilbake i tid + @Test + fun `Skal gi riktig resultat når det overgangsstønad i deler av utbetalingen - Overgangsstønaden stopper før barn fyller 3 år fordi søker ikke lenger har rett til utvidet barnetrygd`() { + val barnFødtAugust2019 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2019, 8, 15)) + val månedFørBarnBlir6 = barnFødtAugust2019.fødselsdato.plusYears(6).minusMonths(1).toYearMonth() + + val tilkjentYtelse = settOppScenarioOgBeregnTilkjentYtelse( + atypiskeVilkårBarna = listOf( + AtypiskVilkår( + fom = februar2022.toLocalDate().førsteDagIInneværendeMåned(), + tom = null, + vilkårType = Vilkår.BOR_MED_SØKER, + aktør = barnFødtAugust2019.aktør, + ), + ), + atypiskeVilkårSøker = listOf( + AtypiskVilkår( + fom = januar2022.toLocalDate().førsteDagIInneværendeMåned(), + tom = juni2022.toLocalDate().sisteDagIMåned(), + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + aktør = søker.aktør, + ), + ), + barna = listOf(barnFødtAugust2019), + overgangsstønadPerioder = listOf(MånedPeriode(april2022, august2022)), + ) + + val andelerTilkjentYtelseITidsrom = + tilkjentYtelse.andelerTilkjentYtelse.filter { it.stønadFom.isSameOrBefore(desember2022) } + assertEquals(3, andelerTilkjentYtelseITidsrom.size) + + val (søkersAndeler, barnasAndeler) = andelerTilkjentYtelseITidsrom.partition { it.erSøkersAndel() } + + val (utvidetAndeler, småbarnstilleggAndeler) = søkersAndeler.partition { it.erUtvidet() } + + assertEquals(1, utvidetAndeler.size) + assertThat(utvidetAndeler[0].stønadFom, Is(mars2022)) + assertThat(utvidetAndeler[0].stønadTom, Is(juni2022)) + assertThat(utvidetAndeler[0].prosent, Is(BigDecimal(100))) + + assertEquals(1, småbarnstilleggAndeler.size) + assertThat(småbarnstilleggAndeler[0].stønadFom, Is(april2022)) + assertThat(småbarnstilleggAndeler[0].stønadTom, Is(juni2022)) + assertThat(småbarnstilleggAndeler[0].prosent, Is(BigDecimal(100))) + + assertEquals(1, barnasAndeler.size) + assertThat(barnasAndeler[0].stønadFom, Is(mars2022)) + assertThat(barnasAndeler[0].stønadTom, Is(månedFørBarnBlir6)) + assertThat(barnasAndeler[0].prosent, Is(BigDecimal(100))) + } + + // src/test/resources/scenario/Far søker om utvidet barnetrygd for barn under 3 år - Har full overgangsstønad som opphører når barnet fyller 3 år + @Test + fun `Skal gi riktig resultat når søker har rett på ordinær og utvidet barnetrygd fra mars og rett på overgangsstønad fra April`() { + val barnFødtAugust2019 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2019, 8, 15)) + val månedFørBarnBlir18 = barnFødtAugust2019.fødselsdato.til18ÅrsVilkårsdato().minusMonths(1).toYearMonth() + val månedFørBarnBlir6 = barnFødtAugust2019.fødselsdato.plusYears(6).minusMonths(1).toYearMonth() + + val tilkjentYtelse = settOppScenarioOgBeregnTilkjentYtelse( + atypiskeVilkårBarna = listOf( + AtypiskVilkår( + fom = februar2022.toLocalDate().førsteDagIInneværendeMåned(), + tom = null, + vilkårType = Vilkår.BOR_MED_SØKER, + aktør = barnFødtAugust2019.aktør, + ), + ), + overgangsstønadPerioder = listOf(MånedPeriode(april2022, desember2022)), + barna = listOf(barnFødtAugust2019), + ) + + val andelerTilkjentYtelseITidsrom = + tilkjentYtelse.andelerTilkjentYtelse.filter { it.stønadFom.isSameOrBefore(desember2022) } + + assertEquals(3, andelerTilkjentYtelseITidsrom.size) + + val (søkersAndeler, barnasAndeler) = andelerTilkjentYtelseITidsrom.partition { it.erSøkersAndel() } + + // SØKER + val (utvidetAndeler, småbarnstilleggAndeler) = søkersAndeler.partition { it.erUtvidet() } + + assertEquals(1, utvidetAndeler.size) + assertThat(utvidetAndeler[0].stønadFom, Is(mars2022)) + assertThat(utvidetAndeler[0].stønadTom, Is(månedFørBarnBlir18)) + assertThat(utvidetAndeler[0].prosent, Is(BigDecimal(100))) + + assertEquals(1, småbarnstilleggAndeler.size) + assertThat(småbarnstilleggAndeler[0].stønadFom, Is(april2022)) + assertThat(småbarnstilleggAndeler[0].stønadTom, Is(august2022)) + assertThat(småbarnstilleggAndeler[0].prosent, Is(BigDecimal(100))) + + assertEquals(1, barnasAndeler.size) + assertThat(barnasAndeler[0].stønadFom, Is(mars2022)) + assertThat(barnasAndeler[0].stønadTom, Is(månedFørBarnBlir6)) + assertThat(barnasAndeler[0].prosent, Is(BigDecimal(100))) + } + + private data class EndretAndel( + val fom: YearMonth, + val tom: YearMonth, + val person: Person, + val årsak: Årsak, + val skalUtbetales: Boolean, + ) + + private fun settOppScenarioOgBeregnTilkjentYtelse( + endretAndeler: List = emptyList(), + atypiskeVilkårBarna: List = emptyList(), + atypiskeVilkårSøker: List = emptyList(), + barna: List, + overgangsstønadPerioder: List, + ): TilkjentYtelse { + val vilkårsvurdering = lagVilkårsvurdering( + søker = søker, + barn = barna, + atypiskeVilkårBarna = atypiskeVilkårBarna, + atypiskeVilkårSøker = atypiskeVilkårSøker, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + ) + + val endretUtbetalingAndeler = endretAndeler.map { + lagEndretUtbetalingAndelMedAndelerTilkjentYtelse( + behandlingId = vilkårsvurdering.behandling.id, + person = it.person, + prosent = if (it.skalUtbetales) BigDecimal(100) else BigDecimal.ZERO, + årsak = it.årsak, + fom = it.fom, + tom = it.tom, + ) + } + + val tilkjentYtelse = beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = lagPersonopplysningsgrunnlag( + personer = barna.plus(søker), + behandlingId = vilkårsvurdering.behandling.id, + ), + endretUtbetalingAndeler = endretUtbetalingAndeler, + fagsakType = FagsakType.NORMAL, + + ) { (_) -> + lagOvergangsstønadPerioder( + perioder = overgangsstønadPerioder, + søkerIdent = søker.aktør.aktivFødselsnummer(), + ) + } + + return tilkjentYtelse + } + + private fun lagPersonopplysningsgrunnlag(personer: List, behandlingId: Long): PersonopplysningGrunnlag { + return PersonopplysningGrunnlag( + personer = personer.toMutableSet(), + behandlingId = behandlingId, + ) + } + + private fun lagOvergangsstønadPerioder( + perioder: List, + søkerIdent: String, + ): List { + return perioder.map { + InternPeriodeOvergangsstønad( + søkerIdent, + it.fom.førsteDagIInneværendeMåned(), + it.tom.sisteDagIInneværendeMåned(), + ) + } + } + + private data class AtypiskVilkår( + val aktør: Aktør, + val fom: LocalDate, + val tom: LocalDate? = null, + val resultat: Resultat = Resultat.OPPFYLT, + val vilkårType: Vilkår, + val utdypendeVilkårsvurdering: UtdypendeVilkårsvurdering? = null, + ) + + private fun lagVilkårResultat( + personResultat: PersonResultat, + fom: LocalDate, + tom: LocalDate? = null, + resultat: Resultat = Resultat.OPPFYLT, + vilkårType: Vilkår, + utdypendeVilkårsvurderinger: List = emptyList(), + ): VilkårResultat { + return VilkårResultat( + personResultat = personResultat, + vilkårType = vilkårType, + resultat = resultat, + periodeFom = fom, + periodeTom = tom, + begrunnelse = "", + sistEndretIBehandlingId = personResultat.vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = utdypendeVilkårsvurderinger, + ) + } + + private fun lagVilkårsvurdering( + søker: Person, + barn: List, + behandlingUnderkategori: BehandlingUnderkategori, + atypiskeVilkårSøker: List = emptyList(), + atypiskeVilkårBarna: List = emptyList(), + ): Vilkårsvurdering { + val vilkårsvurdering = Vilkårsvurdering(behandling = lagBehandling()) + + val eldsteBarn = barn.minBy { it.fødselsdato } + + val søkerPersonResultat = lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = søker, + behandlingUnderkategori = behandlingUnderkategori, + standardFom = eldsteBarn.fødselsdato, + atypiskeVilkår = atypiskeVilkårSøker, + ) + + val barnasPersonResultater = barn.map { barnet -> + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = barnet, + behandlingUnderkategori = behandlingUnderkategori, + standardFom = barnet.fødselsdato, + atypiskeVilkår = atypiskeVilkårBarna.filter { it.aktør == barnet.aktør }, + ) + } + + vilkårsvurdering.personResultater = barnasPersonResultater.toSet().plus(søkerPersonResultat) + return vilkårsvurdering + } + + private fun lagPersonResultat( + vilkårsvurdering: Vilkårsvurdering, + person: Person, + behandlingUnderkategori: BehandlingUnderkategori, + standardFom: LocalDate, + atypiskeVilkår: List, + ): PersonResultat { + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = person.aktør, + ) + + val vilkårForPersonType = Vilkår.hentVilkårFor( + personType = person.type, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = behandlingUnderkategori, + ) + + val ordinæreVilkårResultater = vilkårForPersonType.map { vilkår -> + lagVilkårResultat( + personResultat = personResultat, + vilkårType = vilkår, + resultat = Resultat.OPPFYLT, + fom = standardFom, + tom = if (vilkår == Vilkår.UNDER_18_ÅR) person.fødselsdato.til18ÅrsVilkårsdato() else null, + ) + } + + val atypiskeVilkårTyper = atypiskeVilkår.map { it.vilkårType } + + val oppdaterteVilkårResultater = ordinæreVilkårResultater + .filter { it.vilkårType !in atypiskeVilkårTyper } + .plus( + atypiskeVilkår.map { + lagVilkårResultat( + personResultat = personResultat, + fom = it.fom, + tom = it.tom, + vilkårType = it.vilkårType, + resultat = it.resultat, + utdypendeVilkårsvurderinger = if (it.utdypendeVilkårsvurdering != null) listOf(it.utdypendeVilkårsvurdering) else emptyList(), + ) + }, + ) + + personResultat.setSortedVilkårResultater(oppdaterteVilkårResultater.toSet()) + return personResultat + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseValideringServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseValideringServiceTest.kt new file mode 100644 index 000000000..4c47a48aa --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseValideringServiceTest.kt @@ -0,0 +1,152 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollService +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class TilkjentYtelseValideringServiceTest { + + private val behandlingHentOgPersisterService = mockk() + private val beregningServiceMock = mockk() + private val totrinnskontrollServiceMock = mockk() + private val persongrunnlagServiceMock = mockk() + + private lateinit var tilkjentYtelseValideringService: TilkjentYtelseValideringService + + @BeforeEach + fun setUp() { + tilkjentYtelseValideringService = TilkjentYtelseValideringService( + beregningService = beregningServiceMock, + totrinnskontrollService = totrinnskontrollServiceMock, + persongrunnlagService = persongrunnlagServiceMock, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + ) + + every { + beregningServiceMock.hentRelevanteTilkjentYtelserForBarn( + barnAktør = barn1.aktør, + fagsakId = any(), + ) + } answers { emptyList() } + every { + beregningServiceMock.hentRelevanteTilkjentYtelserForBarn( + barnAktør = barn2.aktør, + fagsakId = any(), + ) + } answers { emptyList() } + every { + beregningServiceMock.hentRelevanteTilkjentYtelserForBarn( + barnAktør = barn3MedUtbetalinger.aktør, + fagsakId = any(), + ) + } answers { + listOf( + TilkjentYtelse( + behandling = lagBehandling(), + endretDato = LocalDate.now().minusYears(1), + opprettetDato = LocalDate.now().minusYears(1), + ), + ) + } + } + + @Test + fun `Skal returnere false hvis ingen barn allerede mottar barnetrygd`() { + Assertions.assertFalse( + tilkjentYtelseValideringService.barnetrygdLøperForAnnenForelder( + behandling = lagBehandling(), + barna = listOf(barn1, barn2), + ), + ) + } + + @Test + fun `Skal returnere true hvis det løper barnetrygd for minst ett barn`() { + Assertions.assertTrue( + tilkjentYtelseValideringService.barnetrygdLøperForAnnenForelder( + behandling = lagBehandling(), + barna = listOf(barn1, barn3MedUtbetalinger), + ), + ) + } + + @Test + fun `Skal returnere liste med personer som har etterbetaling som er mer enn 3 år tilbake i tid`() { + val behandling = lagBehandling() + val person1 = tilfeldigPerson() + val person2 = tilfeldigPerson() + + val tilkjentYtelse = TilkjentYtelse( + behandling = behandling, + opprettetDato = LocalDate.now(), + endretDato = LocalDate.now(), + andelerTilkjentYtelse = mutableSetOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 2108, + person = person1, + ), + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 2108, + person = person2, + ), + ), + ) + + val forrigeBehandling = lagBehandling() + + val forrigeTilkjentYtelse = TilkjentYtelse( + behandling = forrigeBehandling, + opprettetDato = LocalDate.now(), + endretDato = LocalDate.now(), + andelerTilkjentYtelse = mutableSetOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 2108, + person = person1, + ), + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 1054, + person = person2, + ), + ), + ) + + every { beregningServiceMock.hentTilkjentYtelseForBehandling(behandlingId = behandling.id) } answers { tilkjentYtelse } + every { behandlingHentOgPersisterService.hent(behandlingId = behandling.id) } answers { behandling } + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErIverksatt(behandling = behandling) } answers { forrigeBehandling } + every { beregningServiceMock.hentOptionalTilkjentYtelseForBehandling(behandlingId = forrigeBehandling.id) } answers { forrigeTilkjentYtelse } + + Assertions.assertTrue(tilkjentYtelseValideringService.finnAktørerMedUgyldigEtterbetalingsperiode(behandlingId = behandling.id).size == 1) + Assertions.assertEquals( + person2.aktør, + tilkjentYtelseValideringService.finnAktørerMedUgyldigEtterbetalingsperiode(behandlingId = behandling.id) + .single(), + ) + } + + companion object { + val barn1 = lagPerson(type = PersonType.BARN) + val barn2 = lagPerson(type = PersonType.BARN) + val barn3MedUtbetalinger = lagPerson(type = PersonType.BARN) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseValideringTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseValideringTest.kt new file mode 100644 index 000000000..ff18f3289 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/TilkjentYtelseValideringTest.kt @@ -0,0 +1,599 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class TilkjentYtelseValideringTest { + + val gyldigEtterbetalingFom = hentGyldigEtterbetalingFom(LocalDate.now()) + + @Test + fun `Skal returnere true når person har etterbetaling som er mer enn 3 år tilbake i tid`() { + val person1 = tilfeldigPerson() + val person2 = tilfeldigPerson() + + val andeler1 = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 2108, + person = person1, + ), + ) + val forrigeAndeler1 = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 2108, + person = person1, + ), + ) + + Assertions.assertFalse( + TilkjentYtelseValidering.erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson = forrigeAndeler1, + andelerForPerson = andeler1, + gyldigEtterbetalingFom = gyldigEtterbetalingFom, + ), + ) + Assertions.assertTrue( + TilkjentYtelseValidering.erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson = emptyList(), + andelerForPerson = andeler1, + gyldigEtterbetalingFom = gyldigEtterbetalingFom, + ), + ) + Assertions.assertTrue( + TilkjentYtelseValidering.erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson = emptyList(), + andelerForPerson = andeler1, + gyldigEtterbetalingFom = gyldigEtterbetalingFom, + ), + ) + + val forrigeAndeler2 = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 1054, + person = person2, + ), + ) + val andeler2 = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 2108, + person = person2, + ), + ) + Assertions.assertTrue( + TilkjentYtelseValidering.erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson = forrigeAndeler2, + andelerForPerson = andeler2, + gyldigEtterbetalingFom = gyldigEtterbetalingFom, + ), + ) + Assertions.assertTrue( + TilkjentYtelseValidering.erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson = emptyList(), + andelerForPerson = andeler2, + gyldigEtterbetalingFom = gyldigEtterbetalingFom, + ), + ) + Assertions.assertTrue( + TilkjentYtelseValidering.erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson = emptyList(), + andelerForPerson = andeler2, + gyldigEtterbetalingFom = gyldigEtterbetalingFom, + ), + ) + } + + @Test + fun `Skal returnere false ved uendret tilkjent ytelse andel mer enn 3 år tilbake`() { + val person1 = tilfeldigPerson() + + val andeler = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 2108, + person = person1, + ), + ) + + Assertions.assertFalse( + TilkjentYtelseValidering.erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson = andeler, + andelerForPerson = andeler, + gyldigEtterbetalingFom = gyldigEtterbetalingFom, + ), + ) + } + + @Test + fun `Skal returnere false ved reduksjon av beløp mer enn 3 år tilbake`() { + val person1 = tilfeldigPerson() + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 2108, + person = person1, + ), + ) + + val andeler = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned(), + beløp = 1054, + person = person1, + ), + ) + + Assertions.assertFalse( + TilkjentYtelseValidering.erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson = forrigeAndeler, + andelerForPerson = andeler, + gyldigEtterbetalingFom = gyldigEtterbetalingFom, + ), + ) + Assertions.assertFalse( + TilkjentYtelseValidering.erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson = forrigeAndeler, + andelerForPerson = emptyList(), + gyldigEtterbetalingFom = gyldigEtterbetalingFom, + ), + ) + } + + @Test + fun `Skal returnere false ved endring av tilkjent ytelse andel som er mindre enn 3 år tilbake i tid`() { + val person1 = tilfeldigPerson() + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned().minusYears(2), + beløp = 2108, + person = person1, + ), + ) + + val andeler = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(4), + tom = inneværendeMåned().minusYears(2), + beløp = 2108, + person = person1, + ), + ) + + Assertions.assertFalse( + TilkjentYtelseValidering.erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson = forrigeAndeler, + andelerForPerson = andeler, + gyldigEtterbetalingFom = hentGyldigEtterbetalingFom(LocalDate.now().minusYears(2)), + ), + ) + + Assertions.assertFalse( + TilkjentYtelseValidering.erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson = emptyList(), + andelerForPerson = andeler, + gyldigEtterbetalingFom = hentGyldigEtterbetalingFom(LocalDate.now().minusYears(2)), + ), + ) + + Assertions.assertFalse( + TilkjentYtelseValidering.erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson = emptyList(), + andelerForPerson = andeler, + gyldigEtterbetalingFom = hentGyldigEtterbetalingFom(LocalDate.now().minusYears(2)), + ), + ) + + Assertions.assertFalse( + TilkjentYtelseValidering.erUgyldigEtterbetalingPåPerson( + forrigeAndelerForPerson = forrigeAndeler, + andelerForPerson = emptyList(), + gyldigEtterbetalingFom = hentGyldigEtterbetalingFom(LocalDate.now().minusYears(2)), + ), + ) + } + + @Test + fun `Skal finne overlappende perioder og returnere periode med første fom og siste tom`() { + val barn = tilfeldigPerson() + val andeler = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(1), + tom = inneværendeMåned(), + beløp = 2108, + person = barn, + ), + lagAndelTilkjentYtelse( + fom = inneværendeMåned().plusMonths(1), + tom = inneværendeMåned().plusYears(1), + beløp = 2108, + person = barn, + ), + ) + val andelerFraTidligere = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(1).plusMonths(1), + tom = inneværendeMåned().plusMonths(2), + beløp = 2108, + person = barn, + ), + lagAndelTilkjentYtelse( + fom = inneværendeMåned().plusMonths(3), + tom = inneværendeMåned().plusYears(1).minusMonths(1), + beløp = 2108, + person = barn, + ), + ) + + Assertions.assertEquals( + listOf( + MånedPeriode( + inneværendeMåned().minusYears(1).plusMonths(1), + inneværendeMåned().plusYears(1).minusMonths(1), + ), + ), + TilkjentYtelseValidering.finnPeriodeMedOverlappAvAndeler(andeler, andelerFraTidligere), + ) + Assertions.assertEquals( + TilkjentYtelseValidering.finnPeriodeMedOverlappAvAndeler(andeler, emptyList()), + emptyList(), + ) + } + + @Test + fun `Skal håndtere overlappende tidligere andeler fra flere enn 1 behandling`() { + val barn = tilfeldigPerson() + val andeler = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(1), + tom = inneværendeMåned(), + beløp = 2108, + person = barn, + ), + lagAndelTilkjentYtelse( + fom = inneværendeMåned().plusMonths(1), + tom = inneværendeMåned().plusYears(1), + beløp = 2108, + person = barn, + ), + ) + + // 3 Behandlinger med identiske perioder. + val andelerFraTidligere = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(1).plusMonths(1), + tom = inneværendeMåned().plusMonths(2), + beløp = 2108, + person = barn, + ), + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(1).plusMonths(1), + tom = inneværendeMåned().plusMonths(2), + beløp = 2108, + person = barn, + ), + lagAndelTilkjentYtelse( + fom = inneværendeMåned().minusYears(1).plusMonths(1), + tom = inneværendeMåned().plusMonths(2), + beløp = 2108, + person = barn, + ), + ) + + Assertions.assertEquals( + listOf( + MånedPeriode( + inneværendeMåned().minusYears(1).plusMonths(1), + inneværendeMåned().plusMonths(2), + ), + ), + TilkjentYtelseValidering.finnPeriodeMedOverlappAvAndeler(andeler, andelerFraTidligere), + ) + Assertions.assertEquals( + TilkjentYtelseValidering.finnPeriodeMedOverlappAvAndeler(andeler, emptyList()), + emptyList(), + ) + } + + @Nested + inner class `Valider at satsendring kun oppdaterer sats på eksisterende perioder` { + @Test + fun `Skal kaste feil hvis person har lagt til andel som ikke hadde utbetaling i forrige behandling`() { + val person = lagPerson(type = PersonType.BARN) + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2022, 1), + tom = YearMonth.of(2023, 5), + beløp = 1054, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + ) + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2023, 2), + beløp = 1054, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 3), + tom = YearMonth.of(2023, 5), + beløp = 1083, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + ) + + assertThatThrownBy { + TilkjentYtelseValidering.validerAtSatsendringKunOppdatererSatsPåEksisterendePerioder( + andelerFraForrigeBehandling = forrigeAndeler, + andelerTilkjentYtelse = nåværendeAndeler, + ) + }.isInstanceOf(Feil::class.java) + .hasMessageContaining("Satsendring kan ikke legge til en andel som ikke var der i forrige behandling") + } + + @Test + fun `Skal kaste feil hvis person har fått fjernet andel i periode som hadde utbetaling før`() { + val person = lagPerson(type = PersonType.BARN) + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2023, 2), + beløp = 1054, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + ) + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2022, 1), + tom = YearMonth.of(2023, 2), + beløp = 1054, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + ) + + assertThatThrownBy { + TilkjentYtelseValidering.validerAtSatsendringKunOppdatererSatsPåEksisterendePerioder( + andelerFraForrigeBehandling = forrigeAndeler, + andelerTilkjentYtelse = nåværendeAndeler, + ) + }.isInstanceOf(Feil::class.java) + .hasMessageContaining("Satsendring kan ikke fjerne en andel som fantes i forrige behandling") + } + + @Test + fun `Skal kaste feil hvis person har fått endret prosent på andel`() { + val person = lagPerson(type = PersonType.BARN) + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2022, 1), + tom = YearMonth.of(2023, 2), + beløp = 1054, + prosent = BigDecimal(100), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + ) + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2022, 1), + tom = YearMonth.of(2023, 2), + beløp = 527, + prosent = BigDecimal(50), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + ) + + assertThatThrownBy { + TilkjentYtelseValidering.validerAtSatsendringKunOppdatererSatsPåEksisterendePerioder( + andelerFraForrigeBehandling = forrigeAndeler, + andelerTilkjentYtelse = nåværendeAndeler, + ) + }.isInstanceOf(Feil::class.java) + .hasMessageContaining("Satsendring kan ikke endre på prosenten til en andel") + } + + @Test + fun `Skal ikke kaste feil hvis det eneste som er gjort er å oppdatere sats`() { + val person = lagPerson(type = PersonType.BARN) + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2022, 1), + tom = YearMonth.of(2023, 5), + beløp = 1054, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + ) + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2022, 1), + tom = YearMonth.of(2023, 2), + beløp = 1054, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 3), + tom = YearMonth.of(2023, 5), + beløp = 1083, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + ) + + assertDoesNotThrow { + TilkjentYtelseValidering.validerAtSatsendringKunOppdatererSatsPåEksisterendePerioder( + andelerFraForrigeBehandling = forrigeAndeler, + andelerTilkjentYtelse = nåværendeAndeler, + ) + } + } + + @Test + fun `Skal kaste feil hvis det ikke eksisterte andeler forrige gang men gjør det nå`() { + val person = lagPerson(type = PersonType.BARN) + val forrigeAndeler = emptyList() + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2022, 1), + tom = YearMonth.of(2023, 2), + beløp = 1054, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 3), + tom = YearMonth.of(2023, 5), + beløp = 1083, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + ) + + assertThatThrownBy { + TilkjentYtelseValidering.validerAtSatsendringKunOppdatererSatsPåEksisterendePerioder( + andelerFraForrigeBehandling = forrigeAndeler, + andelerTilkjentYtelse = nåværendeAndeler, + ) + }.isInstanceOf(Feil::class.java) + .hasMessageContaining("Satsendring kan ikke legge til en andel som ikke var der i forrige behandling") + } + + @Test + fun `Skal kaste feil hvis det eksisterte andeler forrige gang men ikke gjør det nå`() { + val person = lagPerson(type = PersonType.BARN) + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2022, 1), + tom = YearMonth.of(2023, 2), + beløp = 1054, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 3), + tom = YearMonth.of(2023, 5), + beløp = 1083, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = person.aktør, + ), + ) + + val nåværendeAndeler = emptyList() + + assertThatThrownBy { + TilkjentYtelseValidering.validerAtSatsendringKunOppdatererSatsPåEksisterendePerioder( + andelerFraForrigeBehandling = forrigeAndeler, + andelerTilkjentYtelse = nåværendeAndeler, + ) + }.isInstanceOf(Feil::class.java) + .hasMessageContaining("Satsendring kan ikke fjerne en andel som fantes i forrige behandling") + } + + @Test + fun `Skal kaste feil hvis det eksisterer andeler i lik periode som forrige gang men av ulik type`() { + val barn = lagPerson(type = PersonType.BARN) + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 2), + beløp = 1054, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = barn.aktør, + ), + ) + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 2), + beløp = 1054, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, // barn kan ha utvidet på enslig mindreårig-saker + aktør = barn.aktør, + ), + ) + + assertThrows { + TilkjentYtelseValidering.validerAtSatsendringKunOppdatererSatsPåEksisterendePerioder( + andelerFraForrigeBehandling = forrigeAndeler, + andelerTilkjentYtelse = nåværendeAndeler, + ) + } + } + + @Test + fun `Skal kaste feil hvis det eksisterer andeler i lik periode som forrige gang men på forskjellige personer`() { + val barn1 = lagPerson(type = PersonType.BARN) + val barn2 = lagPerson(type = PersonType.BARN) + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 2), + beløp = 1054, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = barn1.aktør, + ), + ) + + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 1), + tom = YearMonth.of(2023, 2), + beløp = 1054, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + aktør = barn2.aktør, + ), + ) + + assertThrows { + TilkjentYtelseValidering.validerAtSatsendringKunOppdatererSatsPåEksisterendePerioder( + andelerFraForrigeBehandling = forrigeAndeler, + andelerTilkjentYtelse = nåværendeAndeler, + ) + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtbetalingssikkerhetTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtbetalingssikkerhetTest.kt new file mode 100644 index 000000000..1adfa5bdf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtbetalingssikkerhetTest.kt @@ -0,0 +1,411 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.UtbetalingsikkerhetFeil +import no.nav.familie.ba.sak.common.forrigeMåned +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.common.tilPersonEnkel +import no.nav.familie.ba.sak.common.tilPersonEnkelSøkerOgBarn +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import java.math.BigDecimal +import java.time.LocalDate + +class UtbetalingssikkerhetTest { + + @Test + fun `Skal kaste feil når en periode har flere andeler enn det som er tillatt`() { + val person = tilfeldigPerson(personType = PersonType.SØKER) + + val tilkjentYtelse = lagInitiellTilkjentYtelse() + + tilkjentYtelse.andelerTilkjentYtelse.addAll( + listOf( + lagAndelTilkjentYtelse( + inneværendeMåned().minusYears(1), + inneværendeMåned().minusMonths(6), + YtelseType.UTVIDET_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + inneværendeMåned().minusYears(1), + inneværendeMåned().minusMonths(6), + YtelseType.UTVIDET_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + inneværendeMåned().minusYears(1), + inneværendeMåned().minusMonths(6), + YtelseType.SMÅBARNSTILLEGG, + 660, + person = person, + ), + ), + ) + + val feil = assertThrows { + TilkjentYtelseValidering.validerAtTilkjentYtelseHarFornuftigePerioderOgBeløp( + tilkjentYtelse, + listOf(person.tilPersonEnkel()), + ) + } + + assertTrue(feil.message?.contains("Tillatte andeler")!!) + } + + @Test + fun `Skal ikke kaste feil når en periode har like mange andeler som er tillatt`() { + val person = tilfeldigPerson(personType = PersonType.SØKER) + + val tilkjentYtelse = lagInitiellTilkjentYtelse() + + tilkjentYtelse.andelerTilkjentYtelse.addAll( + listOf( + lagAndelTilkjentYtelse( + inneværendeMåned().minusYears(1), + inneværendeMåned().minusMonths(6), + YtelseType.UTVIDET_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + inneværendeMåned().minusYears(1), + inneværendeMåned().minusMonths(6), + YtelseType.SMÅBARNSTILLEGG, + 660, + person = person, + ), + ), + ) + + assertDoesNotThrow { + TilkjentYtelseValidering.validerAtTilkjentYtelseHarFornuftigePerioderOgBeløp( + tilkjentYtelse, + listOf(person.tilPersonEnkel()), + ) + } + } + + @Test + fun `Skal kaste feil når en periode har større totalbeløp enn det som er tillatt`() { + val person = tilfeldigPerson(personType = PersonType.SØKER) + + val tilkjentYtelse = lagInitiellTilkjentYtelse() + + tilkjentYtelse.andelerTilkjentYtelse.addAll( + listOf( + lagAndelTilkjentYtelse( + inneværendeMåned().minusYears(1), + inneværendeMåned().minusMonths(6), + YtelseType.ORDINÆR_BARNETRYGD, + SatsService.finnSisteSatsFor(SatsType.ORBA).beløp, + person = person, + ), + lagAndelTilkjentYtelse( + inneværendeMåned().minusYears(1), + inneværendeMåned().minusMonths(6), + YtelseType.UTVIDET_BARNETRYGD, + SatsService.finnSisteSatsFor(SatsType.UTVIDET_BARNETRYGD).beløp + 1, + person = person, + ), + ), + ) + + val feil = assertThrows { + TilkjentYtelseValidering.validerAtTilkjentYtelseHarFornuftigePerioderOgBeløp( + tilkjentYtelse, + listOf(person.tilPersonEnkel()), + ) + } + + assertTrue(feil.message?.contains("Tillatt totalbeløp")!!) + } + + @Test + fun `Skal ikke kaste feil når en periode har gyldig totalbeløp`() { + val person = tilfeldigPerson(personType = PersonType.SØKER) + + val tilkjentYtelse = lagInitiellTilkjentYtelse() + + tilkjentYtelse.andelerTilkjentYtelse.addAll( + listOf( + lagAndelTilkjentYtelse( + inneværendeMåned().minusYears(1), + inneværendeMåned().minusMonths(6), + YtelseType.UTVIDET_BARNETRYGD, + 1054, + person = person, + ), + lagAndelTilkjentYtelse( + inneværendeMåned().minusYears(1), + inneværendeMåned().minusMonths(6), + YtelseType.SMÅBARNSTILLEGG, + 660, + person = person, + ), + ), + ) + + assertDoesNotThrow { + TilkjentYtelseValidering.validerAtTilkjentYtelseHarFornuftigePerioderOgBeløp( + tilkjentYtelse, + listOf(person.tilPersonEnkel()), + ) + } + } + + @Test + fun `Skal kaste feil når barn får har over 100 prosent gradering for ytelsetype`() { + val barn1 = tilfeldigPerson(fødselsdato = LocalDate.now().minusYears(2).minusMonths(1)) + val barn2 = tilfeldigPerson() + val tilkjentYtelse = lagInitiellTilkjentYtelse() + + tilkjentYtelse.andelerTilkjentYtelse.addAll( + listOf( + lagAndelTilkjentYtelse( + barn1.fødselsdato.nesteMåned(), + barn1.fødselsdato.plusYears(18).forrigeMåned(), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + prosent = BigDecimal(100), + ), + lagAndelTilkjentYtelse( + barn2.fødselsdato.nesteMåned(), + barn2.fødselsdato.plusYears(18).forrigeMåned(), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn2, + prosent = BigDecimal(100), + ), + ), + ) + + val far = tilfeldigPerson(personType = PersonType.SØKER) + val personopplysningGrunnlag2 = PersonopplysningGrunnlag( + behandlingId = 1, + personer = mutableSetOf(far, barn1, barn2), + ) + + val tilkjentYtelse2 = lagInitiellTilkjentYtelse() + + tilkjentYtelse2.andelerTilkjentYtelse.addAll( + listOf( + lagAndelTilkjentYtelse( + barn1.fødselsdato.nesteMåned(), + barn1.fødselsdato.plusYears(18).forrigeMåned(), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + prosent = BigDecimal(100), + ), + lagAndelTilkjentYtelse( + barn1.fødselsdato.nesteMåned(), + barn1.fødselsdato.plusYears(18).forrigeMåned(), + YtelseType.SMÅBARNSTILLEGG, + 660, + person = barn1, + ), + lagAndelTilkjentYtelse( + barn2.fødselsdato.nesteMåned(), + barn2.fødselsdato.plusYears(18).forrigeMåned(), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn2, + prosent = BigDecimal(100), + ), + lagAndelTilkjentYtelse( + barn2.fødselsdato.nesteMåned(), + barn2.fødselsdato.plusYears(18).forrigeMåned(), + YtelseType.SMÅBARNSTILLEGG, + 660, + person = barn2, + ), + ), + ) + + val feil = assertThrows { + TilkjentYtelseValidering.validerAtBarnIkkeFårFlereUtbetalingerSammePeriode( + behandlendeBehandlingTilkjentYtelse = tilkjentYtelse2, + barnMedAndreRelevanteTilkjentYtelser = listOf( + Pair(barn1.tilPersonEnkel(), listOf(tilkjentYtelse)), + Pair(barn2.tilPersonEnkel(), listOf(tilkjentYtelse)), + ), + søkerOgBarn = personopplysningGrunnlag2.tilPersonEnkelSøkerOgBarn(), + ) + } + + assertTrue( + feil.frontendFeilmelding?.contains( + "Du kan ikke godkjenne dette vedtaket fordi det vil betales ut mer enn 100% for barn født ${ + barn1.fødselsdato.tilKortString() + } i perioden ${barn1.fødselsdato.nesteMåned()} til ${ + barn1.fødselsdato.plusYears(18).forrigeMåned() + } og ${ + barn2.fødselsdato.tilKortString() + } i perioden ${barn2.fødselsdato.nesteMåned()} til ${ + barn2.fødselsdato.plusYears(18).forrigeMåned() + }", + )!!, + ) + } + + @Test + fun `Skal ikke kaste feil når utbetalingsandeler for barn ikke overskrider 100 prosent for ytelsetype`() { + val barn = tilfeldigPerson() + val tilkjentYtelse = lagInitiellTilkjentYtelse() + + tilkjentYtelse.andelerTilkjentYtelse.addAll( + listOf( + lagAndelTilkjentYtelse( + barn.fødselsdato.nesteMåned(), + barn.fødselsdato.plusYears(18).forrigeMåned(), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn, + prosent = BigDecimal(50), + ), + ), + ) + + val far = tilfeldigPerson(personType = PersonType.SØKER) + val personopplysningGrunnlag2 = PersonopplysningGrunnlag( + behandlingId = 1, + personer = mutableSetOf(far, barn), + ) + + val tilkjentYtelse2 = lagInitiellTilkjentYtelse() + + tilkjentYtelse2.andelerTilkjentYtelse.addAll( + listOf( + lagAndelTilkjentYtelse( + barn.fødselsdato.nesteMåned(), + barn.fødselsdato.plusYears(18).forrigeMåned(), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn, + prosent = BigDecimal(50), + ), + lagAndelTilkjentYtelse( + barn.fødselsdato.nesteMåned(), + barn.fødselsdato.plusYears(18).forrigeMåned(), + YtelseType.SMÅBARNSTILLEGG, + 660, + person = barn, + ), + ), + ) + + TilkjentYtelseValidering.validerAtBarnIkkeFårFlereUtbetalingerSammePeriode( + behandlendeBehandlingTilkjentYtelse = tilkjentYtelse2, + barnMedAndreRelevanteTilkjentYtelser = listOf(Pair(barn.tilPersonEnkel(), listOf(tilkjentYtelse))), + søkerOgBarn = personopplysningGrunnlag2.tilPersonEnkelSøkerOgBarn(), + ) + } + + @Test + fun `Skal ikke kaste feil når man ser sender inn ulike barn`() { + val barn = tilfeldigPerson() + val tilkjentYtelse = lagInitiellTilkjentYtelse() + + tilkjentYtelse.andelerTilkjentYtelse.addAll( + listOf( + lagAndelTilkjentYtelse( + barn.fødselsdato.nesteMåned(), + barn.fødselsdato.plusYears(18).forrigeMåned(), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn, + ), + ), + ) + + val far = tilfeldigPerson(personType = PersonType.SØKER) + val barn2 = tilfeldigPerson() + val personopplysningGrunnlag2 = PersonopplysningGrunnlag( + behandlingId = 1, + personer = mutableSetOf(far, barn2), + ) + + val tilkjentYtelse2 = lagInitiellTilkjentYtelse() + + tilkjentYtelse2.andelerTilkjentYtelse.addAll( + listOf( + lagAndelTilkjentYtelse( + barn2.fødselsdato.nesteMåned(), + barn2.fødselsdato.plusYears(18).forrigeMåned(), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn2, + ), + ), + ) + + assertDoesNotThrow { + TilkjentYtelseValidering.validerAtBarnIkkeFårFlereUtbetalingerSammePeriode( + behandlendeBehandlingTilkjentYtelse = tilkjentYtelse2, + barnMedAndreRelevanteTilkjentYtelser = listOf(Pair(barn.tilPersonEnkel(), listOf(tilkjentYtelse))), + søkerOgBarn = personopplysningGrunnlag2.tilPersonEnkelSøkerOgBarn(), + ) + } + } + + @Test + fun `Korrekt maksbeløp gis for persontype`() { + val utvidetBarnetrygd = SatsService.finnSisteSatsFor(SatsType.UTVIDET_BARNETRYGD).beløp + val småbarnstillegg = SatsService.finnSisteSatsFor(SatsType.SMA).beløp + val tilleggOrdinærBarnetrygd = SatsService.finnSisteSatsFor(SatsType.TILLEGG_ORBA).beløp + + assertEquals( + utvidetBarnetrygd + småbarnstillegg, + TilkjentYtelseValidering.maksBeløp(personType = PersonType.SØKER, fagsakType = FagsakType.NORMAL), + ) + assertEquals( + tilleggOrdinærBarnetrygd, + TilkjentYtelseValidering.maksBeløp(personType = PersonType.BARN, fagsakType = FagsakType.NORMAL), + ) + assertEquals( + tilleggOrdinærBarnetrygd, + TilkjentYtelseValidering.maksBeløp(personType = PersonType.BARN, fagsakType = FagsakType.INSTITUSJON), + ) + assertEquals( + tilleggOrdinærBarnetrygd + utvidetBarnetrygd, + TilkjentYtelseValidering.maksBeløp( + personType = PersonType.BARN, + fagsakType = FagsakType.BARN_ENSLIG_MINDREÅRIG, + ), + ) + assertThrows { TilkjentYtelseValidering.maksBeløp(personType = PersonType.ANNENPART, FagsakType.NORMAL) } + } + + /** + * Kontroller og eventuelt oppdater TilkjentYtelseValidering.maksBeløp() dersom nye satstyper legges til + */ + @Test + fun `Alle satstyper er tatt hensyn til`() { + val støttedeSatstyper = setOf( + SatsType.SMA, + SatsType.TILLEGG_ORBA, + SatsType.FINN_SVAL, + SatsType.ORBA, + SatsType.UTVIDET_BARNETRYGD, + ) + assertTrue(støttedeSatstyper.containsAll(SatsType.values().toSet())) + assertEquals(støttedeSatstyper.size, SatsType.values().size) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtvidetBarnetrygdTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtvidetBarnetrygdTest.kt new file mode 100644 index 000000000..d9730c86a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtvidetBarnetrygdTest.kt @@ -0,0 +1,1139 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import hentPerioderMedUtbetaling +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.sivilstand.GrSivilstand +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +internal class UtvidetBarnetrygdTest { + + private val fødselsdatoOver6År = LocalDate.of(2014, 1, 1) + private val fødselsdatoUnder6År = LocalDate.of(2021, 1, 15) + + @Test + fun `Utvidet andeler får høyeste beløp når det utbetales til flere barn med ulike beløp`() { + val søker = + OppfyltPeriode(fom = LocalDate.of(2019, 4, 1), tom = LocalDate.of(2020, 6, 15)) + val barnA = + OppfyltPeriode( + fom = LocalDate.of(2019, 4, 1), + tom = LocalDate.of(2020, 6, 15), + rolle = PersonType.BARN, + erDeltBosted = true, + ) + val barnB = + OppfyltPeriode(fom = LocalDate.of(2019, 4, 1), tom = LocalDate.of(2020, 2, 15), rolle = PersonType.BARN) + + val behandling = lagBehandling() + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + + val søkerResultat = + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = søker.aktør) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søker.fom, + vilkårOppfyltTom = søker.tom, + personType = PersonType.SØKER, + ), + ) + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søker.fom, + vilkårOppfyltTom = søker.tom, + personType = PersonType.SØKER, + erUtvidet = true, + ), + ) + } + val barnResultater = listOf(barnA, barnB).map { + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = it.aktør) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = it.fom, + vilkårOppfyltTom = it.tom, + personType = PersonType.BARN, + erDeltBosted = it.erDeltBosted, + ), + ) + } + } + vilkårsvurdering.apply { personResultater = (listOf(søkerResultat) + barnResultater).toSet() } + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id) + .apply { + personer.addAll(listOf(søker, barnA, barnB).lagGrunnlagPersoner(this)) + } + + val andeler = TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + .andelerTilkjentYtelse.toList() + .sortedWith(compareBy({ it.stønadFom }, { it.type }, { it.kalkulertUtbetalingsbeløp })) + + assertEquals(4, andeler.size) + + val andelBarnA = andeler[0] + val andelBarnB = andeler[1] + val andelUtvidetA = andeler[2] + val andelUtvidetB = andeler[3] + + assertEquals(barnA.ident, andelBarnA.aktør.aktivFødselsnummer()) + assertEquals(barnA.fom.nesteMåned(), andelBarnA.stønadFom) + assertEquals(barnA.tom.toYearMonth(), andelBarnA.stønadTom) + assertEquals(527, andelBarnA.kalkulertUtbetalingsbeløp) + + assertEquals(barnB.ident, andelBarnB.aktør.aktivFødselsnummer()) + assertEquals(barnB.fom.nesteMåned(), andelBarnB.stønadFom) + assertEquals(barnB.tom.toYearMonth(), andelBarnB.stønadTom) + assertEquals(1054, andelBarnB.kalkulertUtbetalingsbeløp) + + assertEquals(søker.ident, andelUtvidetA.aktør.aktivFødselsnummer()) + assertEquals(søker.fom.nesteMåned(), andelUtvidetA.stønadFom) + assertEquals(barnB.tom.toYearMonth(), andelUtvidetA.stønadTom) + assertEquals(andelBarnB.kalkulertUtbetalingsbeløp, andelUtvidetA.kalkulertUtbetalingsbeløp) + + assertEquals(søker.ident, andelUtvidetB.aktør.aktivFødselsnummer()) + assertEquals(barnB.tom.nesteMåned(), andelUtvidetB.stønadFom) + assertEquals(søker.tom.toYearMonth(), andelUtvidetB.stønadTom) + assertEquals(andelBarnA.kalkulertUtbetalingsbeløp, andelUtvidetB.kalkulertUtbetalingsbeløp) + } + + @Test + fun `Utvidet andeler får høyeste ordinærsats når søker har tillegg for barn under 6 år`() { + val søker = + OppfyltPeriode(fom = fødselsdatoUnder6År, tom = LocalDate.of(2021, 6, 15)) + val oppfyltBarn = + OppfyltPeriode(fom = fødselsdatoUnder6År, tom = LocalDate.of(2021, 6, 15), rolle = PersonType.BARN) + + val behandling = lagBehandling() + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + + val søkerResultat = + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = søker.aktør) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søker.fom, + vilkårOppfyltTom = søker.tom, + personType = PersonType.SØKER, + ), + ) + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søker.fom, + vilkårOppfyltTom = søker.tom, + personType = PersonType.SØKER, + erUtvidet = true, + ), + ) + } + val barnResultater = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = oppfyltBarn.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = oppfyltBarn.fom, + vilkårOppfyltTom = oppfyltBarn.tom, + personType = PersonType.BARN, + fødselsdato = fødselsdatoUnder6År, + ), + ) + } + + vilkårsvurdering.apply { personResultater = (listOf(søkerResultat) + barnResultater).toSet() } + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id) + .apply { + personer.addAll(listOf(søker, oppfyltBarn).lagGrunnlagPersoner(this, fødselsdatoUnder6År)) + } + + val andeler = TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + .andelerTilkjentYtelse.toList() + .sortedWith(compareBy({ it.stønadFom }, { it.type }, { it.kalkulertUtbetalingsbeløp })) + + assertEquals(2, andeler.size) + + val andelBarn = andeler[0] + val andelUtvidet = andeler[1] + + assertEquals(oppfyltBarn.ident, andelBarn.aktør.aktivFødselsnummer()) + assertEquals(oppfyltBarn.fom.nesteMåned(), andelBarn.stønadFom) + assertEquals(oppfyltBarn.tom.toYearMonth(), andelBarn.stønadTom) + assertEquals(1354, andelBarn.kalkulertUtbetalingsbeløp) + + assertEquals(søker.ident, andelUtvidet.aktør.aktivFødselsnummer()) + assertEquals(søker.fom.nesteMåned(), andelUtvidet.stønadFom) + assertEquals(søker.tom.toYearMonth(), andelUtvidet.stønadTom) + assertEquals(1054, andelUtvidet.kalkulertUtbetalingsbeløp) + } + + @Test + fun `Utvidet andeler får største prosent funnet blant andelene til barna som bor med søker`() { + val behandling = lagBehandling() + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling = behandling) + val søkerAktør = randomAktør() + + val utvidetVilkår = lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2021, 10, 1), + periodeTom = LocalDate.of(2022, 2, 28), + personResultat = PersonResultat( + aktør = søkerAktør, + vilkårsvurdering = lagVilkårsvurdering( + søkerAktør = søkerAktør, + behandling = behandling, + resultat = Resultat.OPPFYLT, + ), + ), + ) + + val barnasAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2021, 10), + tom = YearMonth.of(2022, 2), + person = tilfeldigPerson(personType = PersonType.BARN), + prosent = BigDecimal(50), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2021, 10), + tom = YearMonth.of(2022, 2), + person = tilfeldigPerson(personType = PersonType.BARN), + prosent = BigDecimal(100), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + tilkjentYtelse = tilkjentYtelse, + ), + ) + + val utvidetAndelerNårBarnMed100ProsentBorMedSøker = UtvidetBarnetrygdGenerator( + behandlingId = behandling.id, + tilkjentYtelse = tilkjentYtelse, + ).lagUtvidetBarnetrygdAndeler( + utvidetVilkår = listOf(utvidetVilkår), + andelerBarna = barnasAndeler, + tidslinjerMedPerioderBarnaBorMedSøker = barnasAndeler + .tilSeparateTidslinjerForBarna().mapValues { it.value.map { true } }, + ) + + val utvidetAndelerNårKunBarnMed50ProsentBorMedSøker = UtvidetBarnetrygdGenerator( + behandlingId = behandling.id, + tilkjentYtelse = tilkjentYtelse, + ).lagUtvidetBarnetrygdAndeler( + utvidetVilkår = listOf(utvidetVilkår), + andelerBarna = barnasAndeler, + tidslinjerMedPerioderBarnaBorMedSøker = barnasAndeler + .tilSeparateTidslinjerForBarna().mapValues { it.value.map { andel -> andel?.prosent == BigDecimal(50) } }, + ) + + assertEquals(BigDecimal(100), utvidetAndelerNårBarnMed100ProsentBorMedSøker.minOf { it.prosent }) + assertEquals(BigDecimal(50), utvidetAndelerNårKunBarnMed50ProsentBorMedSøker.maxOf { it.prosent }) + } + + @Test + fun `Utvidet andeler lages kun når vilkåret er innfridd`() { + val søkerOrdinær = + OppfyltPeriode(fom = LocalDate.of(2019, 4, 1), tom = LocalDate.of(2020, 6, 15)) + val søkerUtvidet = + søkerOrdinær.copy(fom = LocalDate.of(2019, 6, 15), erUtvidet = true) + val barnOppfylt = + OppfyltPeriode(fom = LocalDate.of(2019, 4, 1), tom = LocalDate.of(2020, 6, 15), rolle = PersonType.BARN) + + val behandling = lagBehandling() + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + val søkerResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = søkerOrdinær.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søkerOrdinær.fom, + vilkårOppfyltTom = søkerOrdinær.tom, + personType = PersonType.SØKER, + ), + ) + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søkerUtvidet.fom, + vilkårOppfyltTom = søkerUtvidet.tom, + personType = PersonType.SØKER, + erUtvidet = søkerUtvidet.erUtvidet, + ), + ) + } + + val barnResultater = + PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barnOppfylt.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = barnOppfylt.fom, + vilkårOppfyltTom = barnOppfylt.tom, + personType = PersonType.BARN, + erDeltBosted = barnOppfylt.erDeltBosted, + ), + ) + } + vilkårsvurdering.apply { personResultater = listOf(søkerResultat, barnResultater).toSet() } + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id) + .apply { + personer.addAll(listOf(søkerOrdinær, barnOppfylt).lagGrunnlagPersoner(this)) + } + + val andeler = TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + .andelerTilkjentYtelse.toList().sortedBy { it.type } + + assertEquals(2, andeler.size) + + val andelBarn = andeler[0] + val andelUtvidet = andeler[1] + + assertEquals(barnOppfylt.ident, andelBarn.aktør.aktivFødselsnummer()) + assertEquals(barnOppfylt.fom.nesteMåned(), andelBarn.stønadFom) + assertEquals(barnOppfylt.tom.toYearMonth(), andelBarn.stønadTom) + + assertEquals(søkerUtvidet.ident, andelUtvidet.aktør.aktivFødselsnummer()) + assertEquals(søkerUtvidet.fom.nesteMåned(), andelUtvidet.stønadFom) + assertEquals(søkerUtvidet.tom.toYearMonth(), andelUtvidet.stønadTom) + } + + @Test + fun `Utvidet andeler lages kun når det finnes andel for barn`() { + val søkerOrdinær = + OppfyltPeriode(fom = LocalDate.of(2019, 4, 1), tom = LocalDate.of(2020, 6, 15)) + val søkerUtvidet = + søkerOrdinær.copy(erUtvidet = true) + val barnOppfylt = + OppfyltPeriode(fom = LocalDate.of(2019, 6, 1), tom = LocalDate.of(2019, 8, 15), rolle = PersonType.BARN) + + val behandling = lagBehandling() + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + val søkerResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = søkerOrdinær.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søkerOrdinær.fom, + vilkårOppfyltTom = søkerOrdinær.tom, + personType = PersonType.SØKER, + ), + ) + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søkerUtvidet.fom, + vilkårOppfyltTom = søkerUtvidet.tom, + personType = PersonType.SØKER, + erUtvidet = søkerUtvidet.erUtvidet, + ), + ) + } + + val barnResultater = + PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barnOppfylt.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = barnOppfylt.fom, + vilkårOppfyltTom = barnOppfylt.tom, + personType = PersonType.BARN, + erDeltBosted = barnOppfylt.erDeltBosted, + ), + ) + } + vilkårsvurdering.apply { personResultater = listOf(søkerResultat, barnResultater).toSet() } + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id) + .apply { + personer.addAll(listOf(søkerOrdinær, barnOppfylt).lagGrunnlagPersoner(this)) + } + + val andeler = TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + .andelerTilkjentYtelse.toList().sortedBy { it.type } + + assertEquals(2, andeler.size) + + val andelBarn = andeler[0] + val andelUtvidet = andeler[1] + + assertEquals(barnOppfylt.ident, andelBarn.aktør.aktivFødselsnummer()) + assertEquals(barnOppfylt.fom.nesteMåned(), andelBarn.stønadFom) + assertEquals(barnOppfylt.tom.toYearMonth(), andelBarn.stønadTom) + + assertEquals(søkerUtvidet.ident, andelUtvidet.aktør.aktivFødselsnummer()) + assertEquals(barnOppfylt.fom.nesteMåned(), andelUtvidet.stønadFom) + assertEquals(barnOppfylt.tom.toYearMonth(), andelUtvidet.stønadTom) + } + + @Test + fun `Utvidet andeler slutter siste dag i måneden som vilkår ikke er innfridd lenger`() { + val søkerOrdinær = + OppfyltPeriode(fom = LocalDate.of(2019, 4, 1), tom = LocalDate.of(2020, 6, 15)) + val søkerUtvidet = + søkerOrdinær.copy(tom = LocalDate.of(2020, 4, 15), erUtvidet = true) + val barnOppfylt = + OppfyltPeriode(fom = søkerOrdinær.fom, tom = søkerOrdinær.tom, rolle = PersonType.BARN) + + val behandling = lagBehandling() + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + val søkerResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = søkerOrdinær.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søkerOrdinær.fom, + vilkårOppfyltTom = søkerOrdinær.tom, + personType = PersonType.SØKER, + ), + ) + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søkerUtvidet.fom, + vilkårOppfyltTom = søkerUtvidet.tom, + personType = PersonType.SØKER, + erUtvidet = søkerUtvidet.erUtvidet, + ), + ) + } + + val barnResultater = + PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barnOppfylt.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = barnOppfylt.fom, + vilkårOppfyltTom = barnOppfylt.tom, + personType = PersonType.BARN, + erDeltBosted = barnOppfylt.erDeltBosted, + ), + ) + } + vilkårsvurdering.apply { personResultater = listOf(søkerResultat, barnResultater).toSet() } + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id) + .apply { + personer.addAll(listOf(søkerOrdinær, barnOppfylt).lagGrunnlagPersoner(this)) + } + + val andeler = TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + .andelerTilkjentYtelse.toList().sortedBy { it.type } + + assertEquals(2, andeler.size) + + val andelBarn = andeler[0] + val andelUtvidet = andeler[1] + + assertEquals(barnOppfylt.ident, andelBarn.aktør.aktivFødselsnummer()) + assertEquals(barnOppfylt.tom.toYearMonth(), andelBarn.stønadTom) + + assertEquals(søkerUtvidet.ident, andelUtvidet.aktør.aktivFødselsnummer()) + assertEquals(søkerUtvidet.tom.toYearMonth(), andelUtvidet.stønadTom) + } + + @Test + fun `Utvidet andel blir IKKE splittet opp på endring i utvidet vilkåret ved back-to-back, men utbetalingsperiodene blir det`() { + val søkerOrdinær = + OppfyltPeriode(fom = LocalDate.of(2019, 4, 1), tom = LocalDate.of(2020, 10, 15)) + val barnOppfylt = + OppfyltPeriode(fom = søkerOrdinær.fom, tom = søkerOrdinær.tom, rolle = PersonType.BARN) + val b2bTom = LocalDate.of(2020, 2, 29) + val b2bFom = LocalDate.of(2020, 3, 1) + val behandling = lagBehandling() + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + val søkerResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = søkerOrdinær.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søkerOrdinær.fom, + vilkårOppfyltTom = søkerOrdinær.tom, + personType = PersonType.SØKER, + ), + ) + vilkårResultater.addAll( + setOf( + VilkårResultat( + personResultat = this, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + resultat = Resultat.OPPFYLT, + periodeFom = søkerOrdinær.fom, + periodeTom = b2bTom, + begrunnelse = "", + sistEndretIBehandlingId = this.vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ), + VilkårResultat( + personResultat = this, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + resultat = Resultat.OPPFYLT, + periodeFom = b2bFom, + periodeTom = null, + begrunnelse = "", + sistEndretIBehandlingId = this.vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ), + ), + ) + } + val barnResultater = + PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barnOppfylt.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = barnOppfylt.fom, + vilkårOppfyltTom = barnOppfylt.tom, + personType = PersonType.BARN, + erDeltBosted = barnOppfylt.erDeltBosted, + ), + ) + } + vilkårsvurdering.apply { personResultater = listOf(søkerResultat, barnResultater).toSet() } + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id) + .apply { + personer.addAll(listOf(søkerOrdinær, barnOppfylt).lagGrunnlagPersoner(this)) + } + val andeler = TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ).andelerTilkjentYtelse.toList().sortedBy { it.type } + + val vedtaksperioderMedBegrunnelser = hentPerioderMedUtbetaling( + andelerTilkjentYtelse = andeler, + vedtak = lagVedtak(behandling), + personResultater = vilkårsvurdering.personResultater, + personerIPersongrunnlag = personopplysningGrunnlag.personer.toList(), + fagsakType = FagsakType.NORMAL, + ) + + // Én andel for barnet og én andel for utvidet barnetrygd. Utvidet-andelen splittes IKKE + assertEquals(2, andeler.size) + + val andelBarn = andeler[0] + val andelUtvidet = andeler[1] + + assertEquals(barnOppfylt.ident, andelBarn.aktør.aktivFødselsnummer()) + assertEquals(YearMonth.of(2019, 5), andelBarn.stønadFom) + assertEquals(YearMonth.of(2020, 10), andelBarn.stønadTom) + + assertEquals(søkerOrdinær.ident, andelUtvidet.aktør.aktivFødselsnummer()) + assertEquals(YearMonth.of(2019, 5), andelUtvidet.stønadFom) + assertEquals(YearMonth.of(2020, 10), andelUtvidet.stønadTom) + + // Én periode frem til og med 2020-02, og én fra og med 2020-03, der vilkåret er splittet + assertEquals(2, vedtaksperioderMedBegrunnelser.size) + + val vedtaksperiode1 = vedtaksperioderMedBegrunnelser[0] + val vedtaksperiode2 = vedtaksperioderMedBegrunnelser[1] + + assertEquals(LocalDate.of(2019, 5, 1), vedtaksperiode1.fom) + assertEquals(LocalDate.of(2020, 2, 29), vedtaksperiode1.tom) + assertEquals(LocalDate.of(2020, 3, 1), vedtaksperiode2.fom) + assertEquals(LocalDate.of(2020, 10, 31), vedtaksperiode2.tom) + } + + @Test + fun `Utvidet andel blir ikke splittet opp på endring i barnas vilkår som ikke er delt bosted`() { + val søkerOrdinær = + OppfyltPeriode(fom = LocalDate.of(2019, 4, 1), tom = LocalDate.of(2020, 10, 15)) + val barnOppfylt = + OppfyltPeriode(fom = søkerOrdinær.fom, tom = søkerOrdinær.tom, rolle = PersonType.BARN) + + val behandling = lagBehandling() + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + val søkerResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = søkerOrdinær.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søkerOrdinær.fom, + vilkårOppfyltTom = søkerOrdinær.tom, + personType = PersonType.SØKER, + ), + ) + vilkårResultater.addAll( + setOf( + VilkårResultat( + personResultat = this, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + resultat = Resultat.OPPFYLT, + periodeFom = søkerOrdinær.fom, + periodeTom = søkerOrdinær.tom, + begrunnelse = "", + sistEndretIBehandlingId = this.vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ), + ), + ) + } + + val b2bTom = LocalDate.of(2020, 2, 29) + val b2bFom = LocalDate.of(2020, 3, 1) + + val barnResultater = + PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barnOppfylt.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = barnOppfylt.fom, + vilkårOppfyltTom = barnOppfylt.tom, + personType = PersonType.BARN, + erDeltBosted = barnOppfylt.erDeltBosted, + ), + ) + + vilkårResultater.removeIf { it.vilkårType == Vilkår.BOR_MED_SØKER } + vilkårResultater.addAll( + setOf( + VilkårResultat( + personResultat = this, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + periodeFom = søkerOrdinær.fom, + periodeTom = b2bTom, + begrunnelse = "", + sistEndretIBehandlingId = this.vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ), + VilkårResultat( + personResultat = this, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + periodeFom = b2bFom, + periodeTom = søkerOrdinær.tom, + begrunnelse = "", + sistEndretIBehandlingId = this.vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ), + ), + ) + } + vilkårsvurdering.apply { personResultater = listOf(søkerResultat, barnResultater).toSet() } + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id) + .apply { + personer.addAll(listOf(søkerOrdinær, barnOppfylt).lagGrunnlagPersoner(this)) + } + + val andeler = TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + .andelerTilkjentYtelse.toList().sortedBy { it.type } + + assertEquals(2, andeler.size) + + val andelBarn = andeler[0] + val andelUtvidet = andeler[1] + + assertEquals(barnOppfylt.ident, andelBarn.aktør.aktivFødselsnummer()) + assertEquals(barnOppfylt.fom.plusMonths(1).toYearMonth(), andelBarn.stønadFom) + assertEquals(barnOppfylt.tom.toYearMonth(), andelBarn.stønadTom) + + assertEquals(søkerOrdinær.ident, andelUtvidet.aktør.aktivFødselsnummer()) + assertEquals(søkerOrdinær.fom.plusMonths(1).toYearMonth(), andelUtvidet.stønadFom) + assertEquals(søkerOrdinær.tom.toYearMonth(), andelUtvidet.stønadTom) + } + + @Test + fun `Utvidet andel blir splittet opp på endring i barnas vilkår som er delt bosted`() { + val søkerOrdinær = + OppfyltPeriode(fom = LocalDate.of(2019, 4, 1), tom = LocalDate.of(2020, 10, 15)) + val barnOppfylt = + OppfyltPeriode(fom = søkerOrdinær.fom, tom = søkerOrdinær.tom, rolle = PersonType.BARN) + + val behandling = lagBehandling() + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + val søkerResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = søkerOrdinær.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søkerOrdinær.fom, + vilkårOppfyltTom = søkerOrdinær.tom, + personType = PersonType.SØKER, + ), + ) + vilkårResultater.addAll( + setOf( + VilkårResultat( + personResultat = this, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + resultat = Resultat.OPPFYLT, + periodeFom = søkerOrdinær.fom, + periodeTom = søkerOrdinær.tom, + begrunnelse = "", + sistEndretIBehandlingId = this.vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ), + ), + ) + } + + val b2bTom = LocalDate.of(2020, 2, 29) + val b2bFom = LocalDate.of(2020, 3, 1) + + val barnResultater = + PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barnOppfylt.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = barnOppfylt.fom, + vilkårOppfyltTom = barnOppfylt.tom, + personType = PersonType.BARN, + erDeltBosted = barnOppfylt.erDeltBosted, + ), + ) + vilkårResultater.removeIf { it.vilkårType == Vilkår.BOR_MED_SØKER } + vilkårResultater.addAll( + setOf( + VilkårResultat( + personResultat = this, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + periodeFom = søkerOrdinær.fom, + periodeTom = b2bTom, + begrunnelse = "", + sistEndretIBehandlingId = this.vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + VilkårResultat( + personResultat = this, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + periodeFom = b2bFom, + periodeTom = søkerOrdinær.tom, + begrunnelse = "", + sistEndretIBehandlingId = this.vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ), + ), + ) + } + vilkårsvurdering.apply { personResultater = listOf(søkerResultat, barnResultater).toSet() } + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id) + .apply { + personer.addAll(listOf(søkerOrdinær, barnOppfylt).lagGrunnlagPersoner(this)) + } + + val andeler = TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + .andelerTilkjentYtelse.toList().sortedBy { it.type } + + assertEquals(4, andeler.size) + + val andelBarn1 = andeler[0] + val andelBarn2 = andeler[1] + val andelUtvidet1 = andeler[2] + val andelUtvidet2 = andeler[3] + + assertEquals(barnOppfylt.ident, andelBarn1.aktør.aktivFødselsnummer()) + assertEquals(barnOppfylt.fom.plusMonths(1).toYearMonth(), andelBarn1.stønadFom) + assertEquals(b2bTom.toYearMonth(), andelBarn1.stønadTom) + assertEquals(BigDecimal(50), andelBarn1.prosent) + + assertEquals(barnOppfylt.ident, andelBarn2.aktør.aktivFødselsnummer()) + assertEquals(b2bFom.toYearMonth(), andelBarn2.stønadFom) + assertEquals(barnOppfylt.tom.toYearMonth(), andelBarn2.stønadTom) + assertEquals(BigDecimal(100), andelBarn2.prosent) + + assertEquals(søkerOrdinær.ident, andelUtvidet1.aktør.aktivFødselsnummer()) + assertEquals(søkerOrdinær.fom.plusMonths(1).toYearMonth(), andelUtvidet1.stønadFom) + assertEquals(b2bTom.toYearMonth(), andelUtvidet1.stønadTom) + assertEquals(BigDecimal(50), andelUtvidet1.prosent) + + assertEquals(søkerOrdinær.ident, andelUtvidet2.aktør.aktivFødselsnummer()) + assertEquals(b2bFom.toYearMonth(), andelUtvidet2.stønadFom) + assertEquals(søkerOrdinær.tom.toYearMonth(), andelUtvidet2.stønadTom) + assertEquals(BigDecimal(100), andelUtvidet2.prosent) + } + + @Test + fun `Utvidet andel starter og opphører riktig når det er to perioder som ikke er back2back`() { + val søkerOrdinær = + OppfyltPeriode(fom = LocalDate.of(2019, 4, 1), tom = LocalDate.of(2020, 10, 15)) + val barnOppfylt = + OppfyltPeriode(fom = søkerOrdinær.fom, tom = søkerOrdinær.tom, rolle = PersonType.BARN) + + val utvidetFørstePeriodeTom = LocalDate.of(2020, 2, 20) + val utvidetAndrePeriodeFom = LocalDate.of(2020, 3, 15) + + val behandling = lagBehandling() + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + val søkerResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = søkerOrdinær.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = søkerOrdinær.fom, + vilkårOppfyltTom = søkerOrdinær.tom, + personType = PersonType.SØKER, + ), + ) + vilkårResultater.addAll( + setOf( + VilkårResultat( + personResultat = this, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + resultat = Resultat.OPPFYLT, + periodeFom = søkerOrdinær.fom, + periodeTom = utvidetFørstePeriodeTom, + begrunnelse = "", + sistEndretIBehandlingId = this.vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ), + VilkårResultat( + personResultat = this, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + resultat = Resultat.OPPFYLT, + periodeFom = utvidetAndrePeriodeFom, + periodeTom = null, + begrunnelse = "", + sistEndretIBehandlingId = this.vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ), + ), + ) + } + + val barnResultater = + PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barnOppfylt.aktør, + ) + .apply { + vilkårResultater.addAll( + oppfylteVilkårFor( + personResultat = this, + vilkårOppfyltFom = barnOppfylt.fom, + vilkårOppfyltTom = barnOppfylt.tom, + personType = PersonType.BARN, + erDeltBosted = barnOppfylt.erDeltBosted, + ), + ) + } + vilkårsvurdering.apply { personResultater = listOf(søkerResultat, barnResultater).toSet() } + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id) + .apply { + personer.addAll(listOf(søkerOrdinær, barnOppfylt).lagGrunnlagPersoner(this)) + } + + val andeler = TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + .andelerTilkjentYtelse.toList().sortedBy { it.type } + + assertEquals(3, andeler.size) + + val andelBarn = andeler[0] + val andelUtvidet1 = andeler[1] + val andelUtvidet2 = andeler[2] + + assertEquals(barnOppfylt.ident, andelBarn.aktør.aktivFødselsnummer()) + assertEquals(barnOppfylt.tom.toYearMonth(), andelBarn.stønadTom) + + assertEquals(søkerOrdinær.ident, andelUtvidet1.aktør.aktivFødselsnummer()) + assertEquals(utvidetFørstePeriodeTom.toYearMonth(), andelUtvidet1.stønadTom) + + assertEquals(søkerOrdinær.ident, andelUtvidet2.aktør.aktivFødselsnummer()) + assertEquals(utvidetAndrePeriodeFom.plusMonths(1).toYearMonth(), andelUtvidet2.stønadFom) + } + + @Test + fun `Skal kaste feil hvis utvidet-andeler ikke overlapper med noen av barnas andeler`() { + val behandling = lagBehandling() + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling = behandling) + val søkerAktør = randomAktør() + + val utvidetVilkår = lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2018, 2, 1), + periodeTom = LocalDate.of(2019, 2, 28), + personResultat = PersonResultat( + aktør = søkerAktør, + vilkårsvurdering = lagVilkårsvurdering( + søkerAktør = søkerAktør, + behandling = behandling, + resultat = Resultat.OPPFYLT, + ), + ), + ) + + val barnasAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2021, 10), + tom = YearMonth.of(2022, 2), + person = tilfeldigPerson(personType = PersonType.BARN), + prosent = BigDecimal(100), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2021, 10), + tom = YearMonth.of(2022, 1), + person = tilfeldigPerson(personType = PersonType.BARN), + prosent = BigDecimal(100), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + tilkjentYtelse = tilkjentYtelse, + ), + ) + + assertThrows { + UtvidetBarnetrygdGenerator( + behandlingId = behandling.id, + tilkjentYtelse = tilkjentYtelse, + ).lagUtvidetBarnetrygdAndeler( + utvidetVilkår = listOf(utvidetVilkår), + andelerBarna = barnasAndeler, + tidslinjerMedPerioderBarnaBorMedSøker = + barnasAndeler.tilSeparateTidslinjerForBarna().mapValues { it.value.map { true } }, + ) + } + } + + @Test + fun `Skal dele opp utvidet-segment ved endring i sats`() { + val behandling = lagBehandling() + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling = behandling) + val søkerAktør = randomAktør() + + val utvidetVilkår = lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2016, 2, 1), + periodeTom = LocalDate.of(2022, 2, 28), + personResultat = PersonResultat( + aktør = søkerAktør, + vilkårsvurdering = lagVilkårsvurdering( + søkerAktør = søkerAktør, + behandling = behandling, + resultat = Resultat.OPPFYLT, + ), + ), + ) + + val barnasAndeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2015, 10), + tom = YearMonth.of(2022, 2), + person = tilfeldigPerson(personType = PersonType.BARN), + prosent = BigDecimal(100), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + tilkjentYtelse = tilkjentYtelse, + ), + ) + + val utvidetAndeler = UtvidetBarnetrygdGenerator( + behandlingId = behandling.id, + tilkjentYtelse = tilkjentYtelse, + ).lagUtvidetBarnetrygdAndeler( + utvidetVilkår = listOf(utvidetVilkår), + andelerBarna = barnasAndeler, + tidslinjerMedPerioderBarnaBorMedSøker = barnasAndeler + .tilSeparateTidslinjerForBarna().mapValues { it.value.map { true } }, + ).sortedBy { it.stønadFom } + + assertEquals(2, utvidetAndeler.size) + + val andelFørSatsendring = utvidetAndeler[0] + val andelEtterSatsendring = utvidetAndeler[1] + + val datoForSatsendring = SatsService.hentDatoForSatsendring( + satstype = SatsType.UTVIDET_BARNETRYGD, + oppdatertBeløp = 1054, + ) + + assertEquals(970, andelFørSatsendring.sats) + assertEquals(datoForSatsendring?.minusDays(1)?.toYearMonth(), andelFørSatsendring.stønadTom) + + assertEquals(1054, andelEtterSatsendring.sats) + assertEquals(datoForSatsendring?.toYearMonth(), andelEtterSatsendring.stønadFom) + } + + private data class OppfyltPeriode( + val fom: LocalDate, + val tom: LocalDate, + val ident: String = randomFnr(), + val aktør: Aktør = tilAktør(ident), + val rolle: PersonType = PersonType.SØKER, + val erUtvidet: Boolean = false, + val erDeltBosted: Boolean = false, + ) + + private fun oppfylteVilkårFor( + personResultat: PersonResultat, + vilkårOppfyltFom: LocalDate?, + vilkårOppfyltTom: LocalDate?, + personType: PersonType, + erUtvidet: Boolean = false, + erDeltBosted: Boolean = false, + fødselsdato: LocalDate = fødselsdatoOver6År, + ): Set { + val vilkårSomSkalVurderes = if (erUtvidet) { + listOf(Vilkår.UTVIDET_BARNETRYGD) + } else { + Vilkår.hentVilkårFor( + personType = personType, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ) + } + + return vilkårSomSkalVurderes.map { + VilkårResultat( + personResultat = personResultat, + vilkårType = it, + resultat = Resultat.OPPFYLT, + periodeFom = if (it == Vilkår.UNDER_18_ÅR) fødselsdato else vilkårOppfyltFom, + periodeTom = if (it == Vilkår.UNDER_18_ÅR) fødselsdato.plusYears(18) else vilkårOppfyltTom, + begrunnelse = "", + sistEndretIBehandlingId = personResultat.vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOfNotNull( + if (erDeltBosted) UtdypendeVilkårsvurdering.DELT_BOSTED else null, + ), + ) + }.toSet() + } + + private fun List.lagGrunnlagPersoner( + personopplysningGrunnlag: PersonopplysningGrunnlag, + fødselsdato: LocalDate = fødselsdatoOver6År, + ): List = this.map { + Person( + aktør = tilAktør(it.ident), + type = it.rolle, + personopplysningGrunnlag = personopplysningGrunnlag, + fødselsdato = fødselsdato, + navn = "Test Testesen", + kjønn = Kjønn.KVINNE, + ) + .apply { + sivilstander = mutableListOf(GrSivilstand(type = SIVILSTAND.UGIFT, person = this)) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtvidetBarnetrygdUtilTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtvidetBarnetrygdUtilTest.kt new file mode 100644 index 000000000..a8e6bf368 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/UtvidetBarnetrygdUtilTest.kt @@ -0,0 +1,137 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.beregning.UtvidetBarnetrygdUtil.beregnTilkjentYtelseUtvidet +import no.nav.familie.ba.sak.kjerne.beregning.UtvidetBarnetrygdUtil.validerUtvidetVilkårsresultat +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import java.time.LocalDate +import java.time.YearMonth + +class UtvidetBarnetrygdUtilTest { + + @Test + fun `Valider utvidet vilkår - skal kaste feil hvis fom og tom er i samme kalendermåned uten etterfølgende periode`() { + val vilkårResultat = lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2022, 7, 1), + periodeTom = LocalDate.of(2022, 7, 31), + resultat = Resultat.OPPFYLT, + ) + + assertThrows { + validerUtvidetVilkårsresultat( + vilkårResultat = vilkårResultat, + utvidetVilkårResultater = listOf(vilkårResultat), + ) + } + } + + @Test + fun `Valider utvidet vilkår - skal ikke kaste feil hvis fom og tom er i samme kalendermåned og har etterfølgende periode`() { + val vilkårResultat = lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2022, 7, 1), + periodeTom = LocalDate.of(2022, 7, 31), + resultat = Resultat.OPPFYLT, + ) + + val etterfølgendeVilkårResultat = lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2022, 8, 1), + periodeTom = LocalDate.of(2022, 12, 10), + resultat = Resultat.OPPFYLT, + ) + + assertDoesNotThrow { + validerUtvidetVilkårsresultat( + vilkårResultat = vilkårResultat, + utvidetVilkårResultater = listOf( + vilkårResultat, + etterfølgendeVilkårResultat, + ), + ) + } + } + + @Test + fun `Beregn utvidet - skal ikke gi rett til utvidet for perioder der barnet ikke bor med søker`() { + val testBeregnUtvidet = TestBeregnTilkjentYtelseUtvidet() + + val feilmeldinger = assertThrows { + testBeregnUtvidet.med(UtdypendeVilkårsvurdering.BARN_BOR_I_EØS_MED_ANNEN_FORELDER) + }.melding to assertThrows { + testBeregnUtvidet.med(UtdypendeVilkårsvurdering.BARN_BOR_I_STORBRITANNIA_MED_ANNEN_FORELDER) + }.melding + + val forventetFeilmelding = "Du har lagt til utvidet barnetrygd for en periode der det ikke er rett til barnetrygd" + + assertTrue(forventetFeilmelding in feilmeldinger.first && forventetFeilmelding in feilmeldinger.second) + + assertDoesNotThrow { + testBeregnUtvidet.med(UtdypendeVilkårsvurdering.BARN_BOR_I_EØS_MED_SØKER) + } + } + + private class TestBeregnTilkjentYtelseUtvidet { + val tilkjentYtelse = lagInitiellTilkjentYtelse() + + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn = tilfeldigPerson() + + val personResultatSøker = lagPersonResultat( + vilkårsvurdering = Vilkårsvurdering(behandling = tilkjentYtelse.behandling), + person = søker, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2021, 10, 1), + periodeTom = LocalDate.of(2022, 2, 28), + ) + + val personResultatBarn = lagPersonResultat( + vilkårsvurdering = Vilkårsvurdering(behandling = tilkjentYtelse.behandling), + person = barn, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2021, 10, 1), + periodeTom = LocalDate.of(2022, 2, 28), + vilkårType = Vilkår.BOR_MED_SØKER, + ) + + val utvidetVilkår = lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2021, 10, 1), + periodeTom = LocalDate.of(2022, 2, 28), + resultat = Resultat.OPPFYLT, + personResultat = personResultatSøker, + ) + + fun med(utdypendeVilkårsvurdering: UtdypendeVilkårsvurdering) = utdypendeVilkårsvurdering.let { + personResultatBarn.vilkårResultater.first().utdypendeVilkårsvurderinger = listOf(utdypendeVilkårsvurdering) + + beregnTilkjentYtelseUtvidet( + utvidetVilkår = listOf(utvidetVilkår), + tilkjentYtelse = tilkjentYtelse, + andelerTilkjentYtelseBarnaMedEtterbetaling3ÅrEndringer = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + person = barn, + fom = YearMonth.of(2021, 10), + tom = YearMonth.of(2022, 2), + ), + ), + endretUtbetalingAndelerSøker = emptyList(), + personResultater = setOf(personResultatBarn), + ) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Vilk\303\245rTilTilkjentYtelseTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Vilk\303\245rTilTilkjentYtelseTest.kt" new file mode 100644 index 000000000..d0e02d562 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/Vilk\303\245rTilTilkjentYtelseTest.kt" @@ -0,0 +1,414 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.util.sisteSmåbarnstilleggSatsTilTester +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvFileSource +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class VilkårTilTilkjentYtelseTest { + + @BeforeEach + fun førHverTest() { + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2022, 12, 31) + } + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + @ParameterizedTest + @CsvFileSource( + resources = ["/beregning/vilkår_til_tilkjent_ytelse/søker_med_ett_barn_inntil_tre_perioder.csv"], + numLinesToSkip = 1, + delimiter = ';', + ) + fun `test søker med ett barn, inntil tre perioder`( + sakType: String, + søkerPeriode1: String?, + søkerVilkår1: String?, + søkerPeriode2: String?, + søkerVilkår2: String?, + barn1Periode1: String?, + barn1Vilkår1: String?, + barn1Andel1Beløp: Int?, + barn1Andel1Periode: String?, + barn1Andel1Type: String?, + barn1Andel2Beløp: Int?, + barn1Andel2Periode: String?, + barn1Andel2Type: String?, + barn1Andel3Beløp: Int?, + barn1Andel3Periode: String?, + barn1Andel3Type: String?, + erDeltBosted: Boolean?, + ) { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = LocalDate.of(2021, 9, 1)) + + val vilkårsvurdering = TestVilkårsvurderingBuilder(sakType) + .medPersonVilkårPeriode(søker, søkerVilkår1, søkerPeriode1, erDeltBosted) + .medPersonVilkårPeriode(søker, søkerVilkår2, søkerPeriode2, erDeltBosted) + .medPersonVilkårPeriode(barn1, barn1Vilkår1, barn1Periode1, erDeltBosted) + .bygg() + + val delBeløp = if (erDeltBosted != null && erDeltBosted) 2 else 1 + + val forventetTilkjentYtelse = TestTilkjentYtelseBuilder(vilkårsvurdering.behandling) + .medAndelTilkjentYtelse(barn1, barn1Andel1Beløp?.div(delBeløp), barn1Andel1Periode, barn1Andel1Type) + .medAndelTilkjentYtelse(barn1, barn1Andel2Beløp?.div(delBeløp), barn1Andel2Periode, barn1Andel2Type) + .medAndelTilkjentYtelse(barn1, barn1Andel3Beløp?.div(delBeløp), barn1Andel3Periode, barn1Andel3Type) + .bygg() + + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag(vilkårsvurdering.behandling.id, søker, barn1) + + val faktiskTilkjentYtelse = TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + + Assertions.assertEquals( + forventetTilkjentYtelse.andelerTilkjentYtelse, + faktiskTilkjentYtelse.andelerTilkjentYtelse, + ) + } + + @ParameterizedTest + @CsvFileSource( + resources = ["/beregning/vilkår_til_tilkjent_ytelse/søker_med_utvidet_og_ett_barn_inntil_to_perioder.csv"], + numLinesToSkip = 1, + delimiter = ';', + ) + fun `test søker med utvidet og ett barn, inntil to perioder`( + sakType: String, + søkerPeriode1: String, + søkerVilkår1: String, + søkerAndel1Beløp: Int, + søkerAndel1Periode: String, + søkerAndel1Type: String, + småbarnstilleggPeriode: String?, + barn1Periode1: String?, + barn1Vilkår1: String?, + barn1Andel1Beløp: Int?, + barn1Andel1Periode: String?, + barn1Andel1Type: String?, + ) { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = LocalDate.of(2021, 9, 1)) + + val vilkårsvurdering = TestVilkårsvurderingBuilder(sakType) + .medPersonVilkårPeriode(søker, søkerVilkår1, søkerPeriode1) + .medPersonVilkårPeriode(barn1, barn1Vilkår1, barn1Periode1) + .bygg() + + val småbarnstilleggTestPeriode: TestPeriode? = + if (småbarnstilleggPeriode != null) TestPeriode.parse(småbarnstilleggPeriode) else null + + val forventetTilkjentYtelse = if (småbarnstilleggTestPeriode != null) { + TestTilkjentYtelseBuilder(vilkårsvurdering.behandling) + .medAndelTilkjentYtelse(barn1, barn1Andel1Beløp, barn1Andel1Periode, barn1Andel1Type) + .medAndelTilkjentYtelse(søker, søkerAndel1Beløp, søkerAndel1Periode, søkerAndel1Type) + .medAndelTilkjentYtelse( + søker, + sisteSmåbarnstilleggSatsTilTester(), + småbarnstilleggPeriode, + YtelseType.SMÅBARNSTILLEGG.name, + ) + .bygg() + } else { + TestTilkjentYtelseBuilder(vilkårsvurdering.behandling) + .medAndelTilkjentYtelse(barn1, barn1Andel1Beløp, barn1Andel1Periode, barn1Andel1Type) + .medAndelTilkjentYtelse(søker, søkerAndel1Beløp, søkerAndel1Periode, søkerAndel1Type) + .bygg() + } + + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag(vilkårsvurdering.behandling.id, søker, barn1) + + val faktiskTilkjentYtelse = TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) { aktør -> + if (småbarnstilleggTestPeriode != null) { + listOf( + InternPeriodeOvergangsstønad( + personIdent = aktør.aktivFødselsnummer(), + fomDato = småbarnstilleggTestPeriode.fraOgMed, + tomDato = småbarnstilleggTestPeriode.tilOgMed!!, + ), + ) + } else { + emptyList() + } + } + + Assertions.assertEquals( + forventetTilkjentYtelse.andelerTilkjentYtelse, + faktiskTilkjentYtelse.andelerTilkjentYtelse, + ) + } + + @ParameterizedTest + @CsvFileSource( + resources = ["/beregning/vilkår_til_tilkjent_ytelse/søker_med_to_barn_inntil_to_perioder.csv"], + numLinesToSkip = 1, + delimiter = ';', + ) + fun `test søker med to barn, inntil to perioder`( + søkerPeriode1: String?, + søkerVilkår1: String?, + søkerPeriode2: String?, + søkerVilkår2: String?, + barn1Periode1: String?, + barn1Vilkår1: String?, + barn2Periode1: String?, + barn2Vilkår1: String?, + barn1Andel1Beløp: Int?, + barn1Andel1Periode: String?, + barn1Andel1Type: String?, + barn1Andel2Beløp: Int?, + barn1Andel2Periode: String?, + barn1Andel2Type: String?, + barn1Andel3Beløp: Int?, + barn1Andel3Periode: String?, + barn1Andel3Type: String?, + barn2Andel1Beløp: Int?, + barn2Andel1Periode: String?, + barn2Andel1Type: String?, + barn2Andel2Beløp: Int?, + barn2Andel2Periode: String?, + barn2Andel2Type: String?, + ) { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = LocalDate.of(2020, 2, 1)) + val barn2 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = LocalDate.of(2022, 4, 1)) + + val vilkårsvurdering = TestVilkårsvurderingBuilder("NASJONAL") + .medPersonVilkårPeriode(søker, søkerVilkår1, søkerPeriode1) + .medPersonVilkårPeriode(søker, søkerVilkår2, søkerPeriode2) + .medPersonVilkårPeriode(barn1, barn1Vilkår1, barn1Periode1) + .medPersonVilkårPeriode(barn2, barn2Vilkår1, barn2Periode1) + .bygg() + + val forventetTilkjentYtelse = TestTilkjentYtelseBuilder(vilkårsvurdering.behandling) + .medAndelTilkjentYtelse(barn1, barn1Andel1Beløp, barn1Andel1Periode, barn1Andel1Type) + .medAndelTilkjentYtelse(barn1, barn1Andel2Beløp, barn1Andel2Periode, barn1Andel2Type) + .medAndelTilkjentYtelse(barn1, barn1Andel3Beløp, barn1Andel3Periode, barn1Andel3Type) + .medAndelTilkjentYtelse(barn2, barn2Andel1Beløp, barn2Andel1Periode, barn2Andel1Type) + .medAndelTilkjentYtelse(barn2, barn2Andel2Beløp, barn2Andel2Periode, barn2Andel2Type) + .bygg() + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(vilkårsvurdering.behandling.id, søker, barn1, barn2) + + val faktiskTilkjentYtelse = TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = vilkårsvurdering, + personopplysningGrunnlag = personopplysningGrunnlag, + fagsakType = FagsakType.NORMAL, + + ) + + Assertions.assertEquals( + forventetTilkjentYtelse.andelerTilkjentYtelse, + faktiskTilkjentYtelse.andelerTilkjentYtelse, + ) + } +} + +class TestVilkårsvurderingBuilder(sakType: String) { + + private val identPersonResultatMap = mutableMapOf() + private val vilkårsvurdering = + Vilkårsvurdering( + behandling = lagBehandling( + behandlingKategori = BehandlingKategori.valueOf(sakType), + ), + ) + + fun medPersonVilkårPeriode( + person: Person, + vilkår: String?, + periode: String?, + erDeltBosted: Boolean? = null, + ): TestVilkårsvurderingBuilder { + if (vilkår.isNullOrEmpty() || periode.isNullOrEmpty()) { + return this + } + + val ident = person.aktør.aktivFødselsnummer() + val aktørId = person.aktør + val personResultat = + identPersonResultatMap.getOrPut(ident) { PersonResultat(0, vilkårsvurdering, aktørId) } + + val testperiode = TestPeriode.parse(periode) + + val vilkårsresultater = TestVilkårParser.parse(vilkår).map { + VilkårResultat( + personResultat = personResultat, + vilkårType = it, + resultat = Resultat.OPPFYLT, + periodeFom = testperiode.fraOgMed, + periodeTom = testperiode.tilOgMed, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOfNotNull( + if (erDeltBosted == true) UtdypendeVilkårsvurdering.DELT_BOSTED else null, + ), + ) + }.toSet() + + personResultat.setSortedVilkårResultater( + personResultat.vilkårResultater.plus(vilkårsresultater) + .toSet(), + ) + + return this + } + + fun bygg(): Vilkårsvurdering { + vilkårsvurdering.personResultater = identPersonResultatMap.values.toSet() + + return vilkårsvurdering + } +} + +class TestTilkjentYtelseBuilder(val behandling: Behandling) { + + private val tilkjentYtelse = TilkjentYtelse( + behandling = behandling, + opprettetDato = LocalDate.now(), + endretDato = LocalDate.now(), + ) + + fun medAndelTilkjentYtelse( + person: Person, + beløp: Int?, + periode: String?, + type: String?, + ): TestTilkjentYtelseBuilder { + if (beløp == null || periode.isNullOrEmpty() || type.isNullOrEmpty()) { + return this + } + + val stønadPeriode = TestPeriode.parse(periode) + + tilkjentYtelse.andelerTilkjentYtelse.add( + AndelTilkjentYtelse( + behandlingId = behandling.id, + tilkjentYtelse = tilkjentYtelse, + aktør = person.aktør, + stønadFom = stønadPeriode.fraOgMed.toYearMonth(), + stønadTom = stønadPeriode.tilOgMed!!.toYearMonth(), + kalkulertUtbetalingsbeløp = beløp.toInt(), + nasjonaltPeriodebeløp = beløp.toInt(), + type = YtelseType.valueOf(type), + sats = beløp.toInt(), + prosent = BigDecimal(100), + ), + ) + + return this + } + + fun bygg(): TilkjentYtelse { + return tilkjentYtelse + } +} + +data class TestPeriode(val fraOgMed: LocalDate, val tilOgMed: LocalDate?) { + + companion object { + + private val yearMonthRegex = """^(\d{4}-\d{2}).*?(\d{4}-\d{2})?$""".toRegex() + private val localDateRegex = """^(\d{4}-\d{2}-\d{2}).*?(\d{4}-\d{2}-\d{2})?$""".toRegex() + + fun parse(s: String): TestPeriode { + return prøvLocalDate(s) ?: prøvYearMonth(s) + ?: throw IllegalArgumentException("Kunne ikke parse periode '$s'") + } + + private fun prøvLocalDate(s: String): TestPeriode? { + val localDateMatch = localDateRegex.find(s) + + if (localDateMatch != null && localDateMatch.groupValues.size == 3) { + val fom = localDateMatch.groupValues[1].let { LocalDate.parse(it) } + val tom = + localDateMatch.groupValues[2].let { if (it.length == 10) LocalDate.parse(it) else null } + + return TestPeriode(fom!!, tom) + } + return null + } + + private fun prøvYearMonth(s: String): TestPeriode? { + val yearMonthMatch = yearMonthRegex.find(s) + + if (yearMonthMatch != null && yearMonthMatch.groupValues.size == 3) { + val fom = yearMonthMatch.groupValues[1].let { YearMonth.parse(it) } + val tom = + yearMonthMatch.groupValues[2].let { + if (it.length == 7) { + YearMonth.parse(it) + } else { + null + } + } + + return TestPeriode(fom!!.atDay(1), tom?.atEndOfMonth()) + } + return null + } + } +} + +object TestVilkårParser { + + fun parse(s: String): List { + return s.split(',') + .map { + when (it.replace("""\s*""".toRegex(), "").lowercase()) { + "opphold" -> Vilkår.LOVLIG_OPPHOLD + "<18" -> Vilkår.UNDER_18_ÅR + "<18år" -> Vilkår.UNDER_18_ÅR + "under18" -> Vilkår.UNDER_18_ÅR + "under18år" -> Vilkår.UNDER_18_ÅR + "bosatt" -> Vilkår.BOSATT_I_RIKET + "bormedsøker" -> Vilkår.BOR_MED_SØKER + "gift" -> Vilkår.GIFT_PARTNERSKAP + "partnerskap" -> Vilkår.GIFT_PARTNERSKAP + "utvidet" -> Vilkår.UTVIDET_BARNETRYGD + else -> throw IllegalArgumentException("Ukjent vilkår: $s") + } + }.toList() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevBegrunnelseTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevBegrunnelseTest.kt new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevPeriodeUtilTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevPeriodeUtilTest.kt new file mode 100644 index 000000000..6cc3f5eb0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevPeriodeUtilTest.kt @@ -0,0 +1,197 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.datagenerator.brev.lagMinimertPerson +import no.nav.familie.ba.sak.kjerne.brev.domene.BrevperiodeData +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertUregistrertBarn +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertVedtaksperiode +import no.nav.familie.ba.sak.kjerne.brev.domene.RestBehandlingsgrunnlagForBrev +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.lagKompetanse +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.YearMonth + +class BrevPeriodeUtilTest { + + @Test + fun `Skal sortere perioder kronologisk, med generelle avslag til slutt`() { + val liste = listOf( + lagBrevperiodeData( + fom = LocalDate.now().minusMonths(12), + tom = LocalDate.now().minusMonths(8), + type = Vedtaksperiodetype.UTBETALING, + + ), + lagBrevperiodeData( + fom = LocalDate.now().minusMonths(3), + tom = null, + type = Vedtaksperiodetype.AVSLAG, + + ), + lagBrevperiodeData( + fom = null, + tom = null, + type = Vedtaksperiodetype.AVSLAG, + + ), + lagBrevperiodeData( + fom = LocalDate.now().minusMonths(7), + tom = LocalDate.now().minusMonths(4), + type = Vedtaksperiodetype.OPPHØR, + + ), + lagBrevperiodeData( + fom = LocalDate.now().minusMonths(3), + tom = LocalDate.now(), + type = Vedtaksperiodetype.UTBETALING, + + ), + ) + + val sortertListe = liste.sorted() + + Assertions.assertTrue(sortertListe.size == 5) + val førstePeriode = sortertListe.first() + val andrePeriode = sortertListe[1] + val tredjePeriode = sortertListe[2] + val fjerdePeriode = sortertListe[3] + val sistePeriode = sortertListe.last() + + Assertions.assertEquals(Vedtaksperiodetype.UTBETALING, førstePeriode.minimertVedtaksperiode.type) + Assertions.assertEquals(LocalDate.now().minusMonths(12), førstePeriode.minimertVedtaksperiode.fom) + Assertions.assertEquals(Vedtaksperiodetype.OPPHØR, andrePeriode.minimertVedtaksperiode.type) + Assertions.assertEquals(LocalDate.now().minusMonths(7), andrePeriode.minimertVedtaksperiode.fom) + Assertions.assertEquals(Vedtaksperiodetype.UTBETALING, tredjePeriode.minimertVedtaksperiode.type) + Assertions.assertEquals(LocalDate.now().minusMonths(3), tredjePeriode.minimertVedtaksperiode.fom) + Assertions.assertEquals(Vedtaksperiodetype.AVSLAG, fjerdePeriode.minimertVedtaksperiode.type) + Assertions.assertEquals(LocalDate.now().minusMonths(3), fjerdePeriode.minimertVedtaksperiode.fom) + Assertions.assertEquals(Vedtaksperiodetype.AVSLAG, sistePeriode.minimertVedtaksperiode.type) + Assertions.assertNull(sistePeriode.minimertVedtaksperiode.fom) + } + + @Test + fun `Skal plukke ut kompetansene i perioden`() { + val barnAktør1 = Aktør(aktørId = "1111111111111") + val barnAktør2 = Aktør(aktørId = "2222222222222") + + val periode1 = MånedPeriode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)) + val periode2 = MånedPeriode(YearMonth.of(2021, 2), YearMonth.of(2021, 3)) + val periode3 = MånedPeriode(YearMonth.of(2021, 5), YearMonth.of(2021, 5)) + + val kompetanse1 = + lagKompetanse(fom = periode1.fom, tom = periode1.tom, barnAktører = setOf(barnAktør1)) + val kompetanse2 = + lagKompetanse( + fom = periode2.fom, + tom = periode2.tom, + barnAktører = setOf(barnAktør1), + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + ) + val kompetanse3 = + lagKompetanse( + fom = periode2.fom, + tom = periode3.tom, + barnAktører = setOf(barnAktør2), + søkersAktivitet = KompetanseAktivitet.INAKTIV, + ) + val kompetanse4 = + lagKompetanse(fom = periode3.fom, tom = periode3.tom, barnAktører = setOf(barnAktør1)) + + Assertions.assertEquals( + listOf(kompetanse1, kompetanse2, kompetanse3.copy(tom = periode2.tom)), + listOf(kompetanse1, kompetanse2, kompetanse3, kompetanse4) + .hentIPeriode(periode1.fom, periode2.tom), + ) + } + + @Test + fun `Skal kunne kombinere registrerte og uregistrerte barns fødselsdatoer til avslagsbegrunnelse`() { + val barnIBegrunnelse = listOf( + lagMinimertPerson(fødselsdato = LocalDate.of(2021, 1, 1), type = PersonType.BARN), + lagMinimertPerson(fødselsdato = LocalDate.of(2021, 2, 2), type = PersonType.BARN), + ).map { it.tilMinimertRestPerson() } + val barnPåBehandling = listOf( + lagMinimertPerson(fødselsdato = LocalDate.of(2021, 1, 1), type = PersonType.BARN), + lagMinimertPerson(fødselsdato = LocalDate.of(2021, 2, 2), type = PersonType.BARN), + lagMinimertPerson(fødselsdato = LocalDate.of(2021, 3, 3), type = PersonType.BARN), + ).map { it.tilMinimertRestPerson() } + val uregistrerteBarn = listOf( + MinimertUregistrertBarn(personIdent = "", navn = "Ole", fødselsdato = LocalDate.of(2021, 4, 4)), + MinimertUregistrertBarn(personIdent = "", navn = "Dole", fødselsdato = LocalDate.of(2021, 5, 5)), + MinimertUregistrertBarn(personIdent = "", navn = "Doffen", fødselsdato = LocalDate.of(2021, 6, 6)), + ) + + Assertions.assertEquals( + hentBarnasFødselsdatoerForAvslagsbegrunnelse( + barnIBegrunnelse, + barnPåBehandling, + uregistrerteBarn, + gjelderSøker = true, + ), + "01.01.21, 02.02.21, 03.03.21, 04.04.21, 05.05.21 og 06.06.21", + ) + Assertions.assertEquals( + hentBarnasFødselsdatoerForAvslagsbegrunnelse( + barnIBegrunnelse, + barnPåBehandling, + uregistrerteBarn, + gjelderSøker = false, + ), + "01.01.21, 02.02.21, 04.04.21, 05.05.21 og 06.06.21", + ) + Assertions.assertEquals( + hentBarnasFødselsdatoerForAvslagsbegrunnelse( + barnIBegrunnelse, + barnPåBehandling, + emptyList(), + gjelderSøker = true, + ), + "01.01.21, 02.02.21 og 03.03.21", + ) + Assertions.assertEquals( + hentBarnasFødselsdatoerForAvslagsbegrunnelse( + emptyList(), + emptyList(), + uregistrerteBarn, + gjelderSøker = true, + ), + "04.04.21, 05.05.21 og 06.06.21", + ) + } +} + +private fun lagBrevperiodeData( + fom: LocalDate?, + tom: LocalDate?, + type: Vedtaksperiodetype, +): BrevperiodeData { + val restBehandlingsgrunnlagForBrev = RestBehandlingsgrunnlagForBrev( + personerPåBehandling = emptyList(), + minimertePersonResultater = emptyList(), + minimerteEndredeUtbetalingAndeler = emptyList(), + fagsakType = FagsakType.NORMAL, + ) + return BrevperiodeData( + restBehandlingsgrunnlagForBrev = restBehandlingsgrunnlagForBrev, + erFørsteVedtaksperiodePåFagsak = false, + brevMålform = Målform.NB, + minimertVedtaksperiode = MinimertVedtaksperiode( + begrunnelser = emptyList(), + fom = fom, + tom = tom, + type = type, + eøsBegrunnelser = emptyList(), + ), + uregistrerteBarn = emptyList(), + minimerteKompetanserForPeriode = emptyList(), + minimerteKompetanserSomStopperRettFørPeriode = emptyList(), + dødeBarnForrigePeriode = emptyList(), + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevServiceTest.kt new file mode 100644 index 000000000..2ae17824d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevServiceTest.kt @@ -0,0 +1,108 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.domene.Totrinnskontroll +import no.nav.familie.ba.sak.sikkerhet.SaksbehandlerContext +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class BrevServiceTest { + val saksbehandlerContext = mockk() + val brevmalService = mockk() + val brevService = BrevService( + totrinnskontrollService = mockk(), + persongrunnlagService = mockk(), + arbeidsfordelingService = mockk(), + simuleringService = mockk(), + vedtaksperiodeService = mockk(), + brevPeriodeService = mockk(), + sanityService = mockk(), + vilkårsvurderingService = mockk(), + korrigertEtterbetalingService = mockk(), + organisasjonService = mockk(), + korrigertVedtakService = mockk(), + saksbehandlerContext = saksbehandlerContext, + brevmalService = brevmalService, + refusjonEøsRepository = mockk(), + unleashNext = mockk(), + integrasjonClient = mockk(), + ) + + @BeforeEach + fun setUp() { + every { saksbehandlerContext.hentSaksbehandlerSignaturTilBrev() } returns "saksbehandlerNavn" + } + + @Test + fun `Saksbehandler blir hentet fra sikkerhetscontext og beslutter viser placeholder tekst under behandling`() { + val behandling = lagBehandling() + + val (saksbehandler, beslutter) = brevService.hentSaksbehandlerOgBeslutter( + behandling = behandling, + totrinnskontroll = null, + ) + + Assertions.assertEquals("saksbehandlerNavn", saksbehandler) + Assertions.assertEquals("Beslutter", beslutter) + } + + @Test + fun `Saksbehandler blir hentet og beslutter er hentet fra sikkerhetscontext under beslutning`() { + val behandling = lagBehandling() + behandling.leggTilBehandlingStegTilstand(StegType.BESLUTTE_VEDTAK) + + val (saksbehandler, beslutter) = brevService.hentSaksbehandlerOgBeslutter( + behandling = behandling, + totrinnskontroll = Totrinnskontroll( + behandling = behandling, + saksbehandler = "Mock Saksbehandler", + saksbehandlerId = "mock.saksbehandler@nav.no", + ), + ) + + Assertions.assertEquals("Mock Saksbehandler", saksbehandler) + Assertions.assertEquals("saksbehandlerNavn", beslutter) + } + + @Test + fun `Saksbehandler blir hentet og beslutter viser placeholder tekst under beslutning`() { + val behandling = lagBehandling() + behandling.leggTilBehandlingStegTilstand(StegType.BESLUTTE_VEDTAK) + + val (saksbehandler, beslutter) = brevService.hentSaksbehandlerOgBeslutter( + behandling = behandling, + totrinnskontroll = Totrinnskontroll( + behandling = behandling, + saksbehandler = "System", + saksbehandlerId = "systembruker", + ), + ) + + Assertions.assertEquals("System", saksbehandler) + Assertions.assertEquals("saksbehandlerNavn", beslutter) + } + + @Test + fun `Saksbehandler og beslutter blir hentet etter at totrinnskontroll er besluttet`() { + val behandling = lagBehandling() + behandling.leggTilBehandlingStegTilstand(StegType.BESLUTTE_VEDTAK) + + val (saksbehandler, beslutter) = brevService.hentSaksbehandlerOgBeslutter( + behandling = behandling, + totrinnskontroll = Totrinnskontroll( + behandling = behandling, + saksbehandler = "Mock Saksbehandler", + saksbehandlerId = "mock.saksbehandler@nav.no", + beslutter = "Mock Beslutter", + beslutterId = "mock.beslutter@nav.no", + ), + ) + + Assertions.assertEquals("Mock Saksbehandler", saksbehandler) + Assertions.assertEquals("Mock Beslutter", beslutter) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevUtilsTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevUtilsTest.kt new file mode 100644 index 000000000..be991010b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevUtilsTest.kt @@ -0,0 +1,657 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import io.mockk.mockk +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagSanityBegrunnelse +import no.nav.familie.ba.sak.common.lagSanityEøsBegrunnelse +import no.nav.familie.ba.sak.common.lagUtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.common.lagUtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.common.lagVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.common.tilMånedÅr +import no.nav.familie.ba.sak.config.testSanityKlient +import no.nav.familie.ba.sak.datagenerator.vedtak.lagVedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertVedtaksperiode +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.EØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Opphørsperiode +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import java.time.LocalDate + +internal class BrevUtilsTest { + + @Test + fun `hent dokumenttittel dersom denne skal overstyres for behandlingen`() { + assertNull(hentOverstyrtDokumenttittel(lagBehandling().copy(type = BehandlingType.FØRSTEGANGSBEHANDLING))) + val revurdering = lagBehandling().copy(type = BehandlingType.REVURDERING) + assertNull(hentOverstyrtDokumenttittel(revurdering)) + Assertions.assertEquals( + "Vedtak om endret barnetrygd - barn 6 år", + hentOverstyrtDokumenttittel(revurdering.copy(opprettetÅrsak = BehandlingÅrsak.OMREGNING_6ÅR)), + ) + Assertions.assertEquals( + "Vedtak om endret barnetrygd - barn 18 år", + hentOverstyrtDokumenttittel(revurdering.copy(opprettetÅrsak = BehandlingÅrsak.OMREGNING_18ÅR)), + ) + Assertions.assertEquals( + "Vedtak om endret barnetrygd", + hentOverstyrtDokumenttittel(revurdering.copy(resultat = Behandlingsresultat.INNVILGET_OG_ENDRET)), + ) + Assertions.assertEquals( + "Vedtak om fortsatt barnetrygd", + hentOverstyrtDokumenttittel(revurdering.copy(resultat = Behandlingsresultat.FORTSATT_INNVILGET)), + ) + assertNull(hentOverstyrtDokumenttittel(revurdering.copy(resultat = Behandlingsresultat.OPPHØRT))) + } + + @Test + fun `hentHjemmeltekst skal returnere sorterte hjemler`() { + val utvidetVedtaksperioderMedBegrunnelser = listOf( + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + vedtaksperiodeMedBegrunnelser = lagVedtaksperiodeMedBegrunnelser(), + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_SATSENDRING, + vedtaksperiodeMedBegrunnelser = lagVedtaksperiodeMedBegrunnelser(), + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + ) + + Assertions.assertEquals( + "barnetrygdloven §§ 2, 4, 10 og 11", + hentHjemmeltekst( + minimerteVedtaksperioder = utvidetVedtaksperioderMedBegrunnelser.map { + it.tilMinimertVedtaksperiode( + testSanityKlient.hentBegrunnelserMap(), + emptyMap(), + ) + }, + sanityBegrunnelser = mapOf( + Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET.sanityApiNavn, + hjemler = listOf("11", "4", "2", "10"), + ), + Standardbegrunnelse.INNVILGET_SATSENDRING to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_SATSENDRING.sanityApiNavn, + hjemler = listOf("10"), + ), + ), + målform = Målform.NB, + refusjonEøsHjemmelSkalMedIBrev = false, + ), + ) + } + + @Test + fun `hentHjemmeltekst skal ikke inkludere hjemmel 17 og 18 hvis opplysningsplikt er oppfylt`() { + val utvidetVedtaksperioderMedBegrunnelser = listOf( + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_SATSENDRING, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + ) + + Assertions.assertEquals( + "barnetrygdloven §§ 2, 4, 10 og 11", + hentHjemmeltekst( + minimerteVedtaksperioder = utvidetVedtaksperioderMedBegrunnelser.map { + it.tilMinimertVedtaksperiode( + sanityBegrunnelser = testSanityKlient.hentBegrunnelserMap(), + sanityEØSBegrunnelser = emptyMap(), + ) + }, + sanityBegrunnelser = mapOf( + Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET.sanityApiNavn, + hjemler = listOf("11", "4", "2", "10"), + ), + Standardbegrunnelse.INNVILGET_SATSENDRING to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_SATSENDRING.sanityApiNavn, + hjemler = listOf("10"), + ), + ), + opplysningspliktHjemlerSkalMedIBrev = false, + målform = Målform.NB, + refusjonEøsHjemmelSkalMedIBrev = false, + ), + ) + } + + @Test + fun `hentHjemmeltekst skal inkludere hjemmel 17 og 18 hvis opplysningsplikt ikke er oppfylt`() { + val utvidetVedtaksperioderMedBegrunnelser = listOf( + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_SATSENDRING, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + ) + + Assertions.assertEquals( + "barnetrygdloven §§ 2, 4, 10, 11, 17 og 18", + hentHjemmeltekst( + minimerteVedtaksperioder = utvidetVedtaksperioderMedBegrunnelser.map { + it.tilMinimertVedtaksperiode( + sanityBegrunnelser = testSanityKlient.hentBegrunnelserMap(), + sanityEØSBegrunnelser = emptyMap(), + ) + }, + sanityBegrunnelser = mapOf( + Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET.sanityApiNavn, + hjemler = listOf("11", "4", "2", "10"), + ), + Standardbegrunnelse.INNVILGET_SATSENDRING to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_SATSENDRING.sanityApiNavn, + hjemler = listOf("10"), + ), + ), + opplysningspliktHjemlerSkalMedIBrev = true, + målform = Målform.NB, + refusjonEøsHjemmelSkalMedIBrev = false, + ), + ) + } + + @Test + fun `hentHjemmeltekst skal inkludere EØS-forordning 987 artikkel 60 hvis det eksisterer eøs refusjon på behandlingen`() { + Assertions.assertEquals( + "EØS-forordning 987/2009 artikkel 60", + hentHjemmeltekst( + minimerteVedtaksperioder = emptyList(), + sanityBegrunnelser = emptyMap(), + opplysningspliktHjemlerSkalMedIBrev = false, + målform = Målform.NB, + refusjonEøsHjemmelSkalMedIBrev = true, + ), + ) + } + + @Test + fun `Skal gi riktig hjemmeltekst ved hjemler både fra barnetrygdloven og folketrygdloven`() { + val utvidetVedtaksperioderMedBegrunnelser = listOf( + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_SØKER_OG_BARN_FRIVILLIG_MEDLEM, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_SATSENDRING, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + ) + + val sanityBegrunnelser = mapOf( + Standardbegrunnelse.INNVILGET_SØKER_OG_BARN_FRIVILLIG_MEDLEM to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_SØKER_OG_BARN_FRIVILLIG_MEDLEM.sanityApiNavn, + hjemler = listOf("11", "4"), + hjemlerFolketrygdloven = listOf("2-5", "2-8"), + ), + Standardbegrunnelse.INNVILGET_SATSENDRING to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_SATSENDRING.sanityApiNavn, + hjemler = listOf("10"), + ), + ) + + Assertions.assertEquals( + "barnetrygdloven §§ 4, 10 og 11 og folketrygdloven §§ 2-5 og 2-8", + hentHjemmeltekst( + minimerteVedtaksperioder = utvidetVedtaksperioderMedBegrunnelser.map { + it.tilMinimertVedtaksperiode( + sanityBegrunnelser = sanityBegrunnelser, + sanityEØSBegrunnelser = emptyMap(), + ) + }, + sanityBegrunnelser = sanityBegrunnelser, + opplysningspliktHjemlerSkalMedIBrev = false, + målform = Målform.NB, + refusjonEøsHjemmelSkalMedIBrev = false, + ), + ) + } + + @Test + fun `Skal gi riktig formattering ved hjemler fra barnetrygdloven og 2 EØS-forordninger`() { + val utvidetVedtaksperioderMedBegrunnelser = listOf( + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + ), + ), + eøsBegrunnelser = listOf( + EØSBegrunnelse( + vedtaksperiodeMedBegrunnelser = mockk(), + begrunnelse = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_SATSENDRING, + ), + ), + eøsBegrunnelser = listOf( + EØSBegrunnelse( + vedtaksperiodeMedBegrunnelser = mockk(), + begrunnelse = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + ) + + val sanityBegrunnelser = mapOf( + Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET.sanityApiNavn, + hjemler = listOf("11", "4"), + ), + Standardbegrunnelse.INNVILGET_SATSENDRING to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_SATSENDRING.sanityApiNavn, + hjemler = listOf("10"), + ), + ) + + val sanityEøsBegrunnelser = mapOf( + EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR to lagSanityEøsBegrunnelse( + apiNavn = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR.sanityApiNavn, + hjemler = listOf("4"), + hjemlerEØSForordningen883 = listOf("11-16"), + ), + EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE to lagSanityEøsBegrunnelse( + apiNavn = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE.sanityApiNavn, + hjemler = listOf("11"), + hjemlerEØSForordningen987 = listOf("58", "60"), + ), + ) + + Assertions.assertEquals( + "barnetrygdloven §§ 4, 10 og 11, EØS-forordning 883/2004 artikkel 11-16 og EØS-forordning 987/2009 artikkel 58 og 60", + hentHjemmeltekst( + minimerteVedtaksperioder = utvidetVedtaksperioderMedBegrunnelser.map { + it.tilMinimertVedtaksperiode( + sanityBegrunnelser = sanityBegrunnelser, + sanityEØSBegrunnelser = sanityEøsBegrunnelser, + ) + }, + sanityBegrunnelser = sanityBegrunnelser, + opplysningspliktHjemlerSkalMedIBrev = false, + målform = Målform.NB, + refusjonEøsHjemmelSkalMedIBrev = false, + ), + ) + } + + @Test + fun `Skal gi riktig formattering ved hjemler fra Separasjonsavtale og to EØS-forordninger`() { + val utvidetVedtaksperioderMedBegrunnelser = listOf( + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + ), + ), + eøsBegrunnelser = listOf( + EØSBegrunnelse( + vedtaksperiodeMedBegrunnelser = mockk(), + begrunnelse = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_SATSENDRING, + ), + ), + eøsBegrunnelser = listOf( + EØSBegrunnelse( + vedtaksperiodeMedBegrunnelser = mockk(), + begrunnelse = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + ) + + val sanityBegrunnelser = mapOf( + Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET.sanityApiNavn, + hjemler = listOf("11", "4"), + ), + Standardbegrunnelse.INNVILGET_SATSENDRING to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_SATSENDRING.sanityApiNavn, + hjemler = listOf("10"), + ), + ) + + val sanityEøsBegrunnelser = mapOf( + EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR to lagSanityEøsBegrunnelse( + apiNavn = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR.sanityApiNavn, + hjemler = listOf("4"), + hjemlerEØSForordningen883 = listOf("11-16"), + hjemlerSeperasjonsavtalenStorbritannina = listOf("29"), + ), + EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE to lagSanityEøsBegrunnelse( + apiNavn = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE.sanityApiNavn, + hjemler = listOf("11"), + hjemlerEØSForordningen987 = listOf("58", "60"), + ), + ) + + Assertions.assertEquals( + "Separasjonsavtalen mellom Storbritannia og Norge artikkel 29, barnetrygdloven §§ 4, 10 og 11, EØS-forordning 883/2004 artikkel 11-16 og EØS-forordning 987/2009 artikkel 58 og 60", + hentHjemmeltekst( + minimerteVedtaksperioder = utvidetVedtaksperioderMedBegrunnelser.map { + it.tilMinimertVedtaksperiode( + sanityBegrunnelser = sanityBegrunnelser, + sanityEØSBegrunnelser = sanityEøsBegrunnelser, + ) + }, + sanityBegrunnelser = sanityBegrunnelser, + opplysningspliktHjemlerSkalMedIBrev = false, + målform = Målform.NB, + refusjonEøsHjemmelSkalMedIBrev = false, + ), + ) + } + + @Test + fun `Skal gi riktig formattering ved nynorsk og hjemler fra Separasjonsavtale og to EØS-forordninger`() { + val utvidetVedtaksperioderMedBegrunnelser = listOf( + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + ), + ), + eøsBegrunnelser = listOf( + EØSBegrunnelse( + vedtaksperiodeMedBegrunnelser = mockk(), + begrunnelse = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_SATSENDRING, + ), + ), + eøsBegrunnelser = listOf( + EØSBegrunnelse( + vedtaksperiodeMedBegrunnelser = mockk(), + begrunnelse = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + ) + + val sanityBegrunnelser = mapOf( + Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET.sanityApiNavn, + hjemler = listOf("11", "4"), + ), + Standardbegrunnelse.INNVILGET_SATSENDRING to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_SATSENDRING.sanityApiNavn, + hjemler = listOf("10"), + ), + ) + + val sanityEøsBegrunnelser = mapOf( + EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR to lagSanityEøsBegrunnelse( + apiNavn = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR.sanityApiNavn, + hjemler = listOf("4"), + hjemlerEØSForordningen883 = listOf("11-16"), + hjemlerSeperasjonsavtalenStorbritannina = listOf("29"), + ), + EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE to lagSanityEøsBegrunnelse( + apiNavn = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE.sanityApiNavn, + hjemler = listOf("11"), + hjemlerEØSForordningen987 = listOf("58", "60"), + ), + ) + + Assertions.assertEquals( + "Separasjonsavtalen mellom Storbritannia og Noreg artikkel 29, barnetrygdlova §§ 4, 10 og 11, EØS-forordning 883/2004 artikkel 11-16 og EØS-forordning 987/2009 artikkel 58 og 60", + hentHjemmeltekst( + minimerteVedtaksperioder = utvidetVedtaksperioderMedBegrunnelser.map { + it.tilMinimertVedtaksperiode( + sanityBegrunnelser = sanityBegrunnelser, + sanityEØSBegrunnelser = sanityEøsBegrunnelser, + ) + }, + sanityBegrunnelser = sanityBegrunnelser, + opplysningspliktHjemlerSkalMedIBrev = false, + målform = Målform.NN, + refusjonEøsHjemmelSkalMedIBrev = false, + ), + ) + } + + @Test + fun `Skal slå sammen hjemlene riktig når det er 3 eller flere hjemler på 'siste' hjemmeltype`() { + val utvidetVedtaksperioderMedBegrunnelser = listOf( + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + ), + ), + eøsBegrunnelser = listOf( + EØSBegrunnelse( + vedtaksperiodeMedBegrunnelser = mockk(), + begrunnelse = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_SATSENDRING, + ), + ), + eøsBegrunnelser = listOf( + EØSBegrunnelse( + vedtaksperiodeMedBegrunnelser = mockk(), + begrunnelse = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + ) + + val sanityBegrunnelser = mapOf( + Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET.sanityApiNavn, + hjemler = listOf("11", "4"), + ), + Standardbegrunnelse.INNVILGET_SATSENDRING to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_SATSENDRING.sanityApiNavn, + hjemler = listOf("10"), + ), + ) + + val sanityEøsBegrunnelser = mapOf( + EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR to lagSanityEøsBegrunnelse( + apiNavn = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR.sanityApiNavn, + hjemler = listOf("4"), + hjemlerEØSForordningen883 = listOf("2", "11-16", "67", "68"), + hjemlerSeperasjonsavtalenStorbritannina = listOf("29"), + ), + EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE to lagSanityEøsBegrunnelse( + apiNavn = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE.sanityApiNavn, + hjemler = listOf("11"), + ), + ) + + Assertions.assertEquals( + "Separasjonsavtalen mellom Storbritannia og Noreg artikkel 29, barnetrygdlova §§ 4, 10 og 11 og EØS-forordning 883/2004 artikkel 2, 11-16, 67 og 68", + hentHjemmeltekst( + minimerteVedtaksperioder = utvidetVedtaksperioderMedBegrunnelser.map { + it.tilMinimertVedtaksperiode( + sanityBegrunnelser = sanityBegrunnelser, + sanityEØSBegrunnelser = sanityEøsBegrunnelser, + ) + }, + sanityBegrunnelser = sanityBegrunnelser, + opplysningspliktHjemlerSkalMedIBrev = false, + målform = Målform.NN, + refusjonEøsHjemmelSkalMedIBrev = false, + ), + ) + } + + @Test + fun `Skal kun ta med en hjemmel 1 gang hvis flere begrunnelser er knyttet til samme hjemmel`() { + val utvidetVedtaksperioderMedBegrunnelser = listOf( + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + ), + ), + eøsBegrunnelser = listOf( + EØSBegrunnelse( + vedtaksperiodeMedBegrunnelser = mockk(), + begrunnelse = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + lagUtvidetVedtaksperiodeMedBegrunnelser( + begrunnelser = listOf( + lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_SATSENDRING, + ), + ), + eøsBegrunnelser = listOf( + EØSBegrunnelse( + vedtaksperiodeMedBegrunnelser = mockk(), + begrunnelse = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE, + ), + ), + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ), + ) + + val sanityBegrunnelser = mapOf( + Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET.sanityApiNavn, + hjemler = listOf("11", "4"), + ), + Standardbegrunnelse.INNVILGET_SATSENDRING to lagSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_SATSENDRING.sanityApiNavn, + hjemler = listOf("10"), + ), + ) + + val sanityEøsBegrunnelser = mapOf( + EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR to lagSanityEøsBegrunnelse( + apiNavn = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_ALENEANSVAR.sanityApiNavn, + hjemler = listOf("4"), + hjemlerEØSForordningen883 = listOf("2", "11-16", "67", "68"), + hjemlerSeperasjonsavtalenStorbritannina = listOf("29"), + hjemlerEØSForordningen987 = listOf("58"), + ), + EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE to lagSanityEøsBegrunnelse( + apiNavn = EØSStandardbegrunnelse.INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE.sanityApiNavn, + hjemler = listOf("11"), + hjemlerEØSForordningen883 = listOf("2", "67", "68"), + hjemlerSeperasjonsavtalenStorbritannina = listOf("29"), + hjemlerEØSForordningen987 = listOf("58"), + + ), + ) + + Assertions.assertEquals( + "Separasjonsavtalen mellom Storbritannia og Noreg artikkel 29, barnetrygdlova §§ 4, 10 og 11, EØS-forordning 883/2004 artikkel 2, 11-16, 67 og 68 og EØS-forordning 987/2009 artikkel 58", + hentHjemmeltekst( + minimerteVedtaksperioder = utvidetVedtaksperioderMedBegrunnelser.map { + it.tilMinimertVedtaksperiode( + sanityBegrunnelser = sanityBegrunnelser, + sanityEØSBegrunnelser = sanityEøsBegrunnelser, + ) + }, + sanityBegrunnelser = sanityBegrunnelser, + opplysningspliktHjemlerSkalMedIBrev = false, + målform = Målform.NN, + refusjonEøsHjemmelSkalMedIBrev = false, + ), + ) + } + + @Test + fun `Skal gi riktig dato for opphørstester`() { + val sisteFom = LocalDate.now().minusMonths(2) + + val opphørsperioder = listOf( + Opphørsperiode( + periodeFom = LocalDate.now().minusYears(1), + periodeTom = LocalDate.now().minusYears(1).plusMonths(2), + ), + Opphørsperiode( + periodeFom = LocalDate.now().minusMonths(5), + periodeTom = LocalDate.now().minusMonths(4), + ), + Opphørsperiode( + periodeFom = sisteFom, + periodeTom = LocalDate.now(), + ), + ) + + Assertions.assertEquals(sisteFom.tilMånedÅr(), hentVirkningstidspunkt(opphørsperioder, 0L)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevmalServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevmalServiceTest.kt new file mode 100644 index 000000000..b746d6d2d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevmalServiceTest.kt @@ -0,0 +1,175 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import org.hamcrest.MatcherAssert.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import java.time.YearMonth +import org.hamcrest.CoreMatchers.`is` as Is + +@ExtendWith(MockKExtension::class) +internal class BrevmalServiceTest { + + @MockK + private lateinit var andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository + + @InjectMockKs + private lateinit var brevmalService: BrevmalService + + @Test + fun `hentBrevmal skal returnere VEDTAK_OPPHØR_DØDSFALL dersom behandlingårsak er DØDSFALL_BRUKER`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.DØDSFALL_BRUKER) + + assertThat(brevmalService.hentBrevmal(behandling), Is(Brevmal.VEDTAK_OPPHØR_DØDSFALL)) + } + + @Test + fun `hentBrevmal skal returnere VEDTAK_KORREKSJON_VEDTAKSBREV dersom behandlingårsak er KORREKSJON_VEDTAKSBREV`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.KORREKSJON_VEDTAKSBREV) + + assertThat(brevmalService.hentBrevmal(behandling), Is(Brevmal.VEDTAK_KORREKSJON_VEDTAKSBREV)) + } + + @Test + fun `hentVedtaksbrevmal skal kaste feil dersom behandling har status IKKE_VURDERT`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.KORREKSJON_VEDTAKSBREV, resultat = Behandlingsresultat.IKKE_VURDERT) + + assertThrows { + brevmalService.hentVedtaksbrevmal(behandling) + } + } + + @ParameterizedTest(name = "hentManuellVedtaksbrevtype skal returnere VEDTAK_FØRSTEGANGSVEDTAK_INSTITUSJON for førstegangsbehandling som er institusjon med behandlingsresultat {0}") + @EnumSource( + value = Behandlingsresultat::class, + names = ["INNVILGET", "INNVILGET_OG_ENDRET", "INNVILGET_OG_OPPHØRT", "INNVILGET_ENDRET_OG_OPPHØRT", "DELVIS_INNVILGET", "DELVIS_INNVILGET_OG_ENDRET", "DELVIS_INNVILGET_OG_OPPHØRT", "DELVIS_INNVILGET_ENDRET_OG_OPPHØRT", "AVSLÅTT_OG_ENDRET", "AVSLÅTT_OG_OPPHØRT", "AVSLÅTT_ENDRET_OG_OPPHØRT"], + ) + fun `hentManuellVedtaksbrevtype skal returnere VEDTAK_FØRSTEGANGSVEDTAK_INSTITUSJON for førstegangsbehandling som er institusjon med gitte typer behandlingsresultat `(behandlingsresultat: Behandlingsresultat) { + val behandling = mockk().apply { + every { resultat } returns behandlingsresultat + every { fagsak.institusjon } returns mockk() + every { type } returns BehandlingType.FØRSTEGANGSBEHANDLING + } + + assertThat(brevmalService.hentManuellVedtaksbrevtype(behandling), Is(Brevmal.VEDTAK_FØRSTEGANGSVEDTAK_INSTITUSJON)) + } + + @ParameterizedTest(name = "hentManuellVedtaksbrevtype skal returnere VEDTAK_FØRSTEGANGSVEDTAK for førstegangsbehandling med behandlingsresultat {0}") + @EnumSource( + value = Behandlingsresultat::class, + names = ["INNVILGET", "INNVILGET_OG_ENDRET", "INNVILGET_OG_OPPHØRT", "INNVILGET_ENDRET_OG_OPPHØRT", "DELVIS_INNVILGET", "DELVIS_INNVILGET_OG_ENDRET", "DELVIS_INNVILGET_OG_OPPHØRT", "DELVIS_INNVILGET_ENDRET_OG_OPPHØRT", "AVSLÅTT_OG_ENDRET", "AVSLÅTT_OG_OPPHØRT", "AVSLÅTT_ENDRET_OG_OPPHØRT"], + ) + fun `hentManuellVedtaksbrevtype skal returnere VEDTAK_FØRSTEGANGSVEDTAK for førstegangsbehandling med gitte behandlingsresultat `(behandlingsresultat: Behandlingsresultat) { + val behandling = mockk().apply { + every { resultat } returns behandlingsresultat + every { fagsak.institusjon } returns null + every { type } returns BehandlingType.FØRSTEGANGSBEHANDLING + } + + assertThat(brevmalService.hentManuellVedtaksbrevtype(behandling), Is(Brevmal.VEDTAK_FØRSTEGANGSVEDTAK)) + } + + @ParameterizedTest(name = "hentManuellVedtaksbrevtype skal returnere VEDTAK_OPPHØR_MED_ENDRING_INSTITUSJON for førstegangsbehandling som er institusjon med behandlingsresultat {0}") + @EnumSource( + value = Behandlingsresultat::class, + names = ["INNVILGET", "INNVILGET_OG_ENDRET", "INNVILGET_OG_OPPHØRT", "INNVILGET_ENDRET_OG_OPPHØRT", "DELVIS_INNVILGET", "DELVIS_INNVILGET_OG_ENDRET", "DELVIS_INNVILGET_OG_OPPHØRT", "DELVIS_INNVILGET_ENDRET_OG_OPPHØRT", "AVSLÅTT_OG_ENDRET", "AVSLÅTT_OG_OPPHØRT", "AVSLÅTT_ENDRET_OG_OPPHØRT"], + ) + fun `hentManuellVedtaksbrevtype skal returnere VEDTAK_OPPHØR_MED_ENDRING_INSTITUSJON for revurdering med ingen løpende ytelser som er institusjon med gitte behandlingsresultat `(behandlingsresultat: Behandlingsresultat) { + val behandling = mockk(relaxed = true).apply { + every { resultat } returns behandlingsresultat + every { fagsak.institusjon } returns mockk() + every { type } returns BehandlingType.REVURDERING + } + + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandling.id) } returns listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(1999, 1), + tom = YearMonth.of(1999, 2), + ), + ) + + assertThat(brevmalService.hentManuellVedtaksbrevtype(behandling), Is(Brevmal.VEDTAK_OPPHØR_MED_ENDRING_INSTITUSJON)) + } + + @ParameterizedTest(name = "hentManuellVedtaksbrevtype skal returnere VEDTAK_ENDRING_INSTITUSJON for førstegangsbehandling som er institusjon med behandlingsresultat {0}") + @EnumSource( + value = Behandlingsresultat::class, + names = ["INNVILGET", "INNVILGET_OG_ENDRET", "INNVILGET_OG_OPPHØRT", "INNVILGET_ENDRET_OG_OPPHØRT", "DELVIS_INNVILGET", "DELVIS_INNVILGET_OG_ENDRET", "DELVIS_INNVILGET_OG_OPPHØRT", "DELVIS_INNVILGET_ENDRET_OG_OPPHØRT", "AVSLÅTT_OG_ENDRET", "AVSLÅTT_OG_OPPHØRT", "AVSLÅTT_ENDRET_OG_OPPHØRT"], + ) + fun `hentManuellVedtaksbrevtype skal returnere VEDTAK_ENDRING_INSTITUSJON for revurdering med løpende ytelser som er institusjon med gitte typer behandlingsresultat `(behandlingsresultat: Behandlingsresultat) { + val behandling = mockk(relaxed = true).apply { + every { resultat } returns behandlingsresultat + every { fagsak.institusjon } returns mockk() + every { type } returns BehandlingType.REVURDERING + } + + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandling.id) } returns listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2015, 1), + tom = YearMonth.of(2037, 2), + ), + ) + + assertThat(brevmalService.hentManuellVedtaksbrevtype(behandling), Is(Brevmal.VEDTAK_ENDRING_INSTITUSJON)) + } + + @ParameterizedTest(name = "hentManuellVedtaksbrevtype skal returnere VEDTAK_ENDRING for førstegangsbehandling som er institusjon med behandlingsresultat {0}") + @EnumSource( + value = Behandlingsresultat::class, + names = ["INNVILGET", "INNVILGET_OG_ENDRET", "INNVILGET_OG_OPPHØRT", "INNVILGET_ENDRET_OG_OPPHØRT", "DELVIS_INNVILGET", "DELVIS_INNVILGET_OG_ENDRET", "DELVIS_INNVILGET_OG_OPPHØRT", "DELVIS_INNVILGET_ENDRET_OG_OPPHØRT", "AVSLÅTT_OG_ENDRET", "AVSLÅTT_OG_OPPHØRT", "AVSLÅTT_ENDRET_OG_OPPHØRT"], + ) + fun `hentManuellVedtaksbrevtype skal returnere VEDTAK_ENDRING for revurdering med løpende ytelser med gitte behandlingsresultat `(behandlingsresultat: Behandlingsresultat) { + val behandling = mockk(relaxed = true).apply { + every { resultat } returns behandlingsresultat + every { fagsak.institusjon } returns null + every { type } returns BehandlingType.REVURDERING + } + + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandling.id) } returns listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2015, 1), + tom = YearMonth.of(2037, 2), + ), + ) + + assertThat(brevmalService.hentManuellVedtaksbrevtype(behandling), Is(Brevmal.VEDTAK_ENDRING)) + } + + @ParameterizedTest(name = "hentManuellVedtaksbrevtype skal returnere VEDTAK_OPPHØR_MED_ENDRING for førstegangsbehandling som er institusjon med behandlingsresultat {0}") + @EnumSource( + value = Behandlingsresultat::class, + names = ["INNVILGET", "INNVILGET_OG_ENDRET", "INNVILGET_OG_OPPHØRT", "INNVILGET_ENDRET_OG_OPPHØRT", "DELVIS_INNVILGET", "DELVIS_INNVILGET_OG_ENDRET", "DELVIS_INNVILGET_OG_OPPHØRT", "DELVIS_INNVILGET_ENDRET_OG_OPPHØRT", "AVSLÅTT_OG_ENDRET", "AVSLÅTT_OG_OPPHØRT", "AVSLÅTT_ENDRET_OG_OPPHØRT"], + ) + fun `hentManuellVedtaksbrevtype skal returnere VEDTAK_OPPHØR_MED_ENDRING for revurdering med ingen løpende ytelser med gitte behandlingsresultat `(behandlingsresultat: Behandlingsresultat) { + val behandling = mockk(relaxed = true).apply { + every { resultat } returns behandlingsresultat + every { fagsak.institusjon } returns null + every { type } returns BehandlingType.REVURDERING + } + + every { andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandling.id) } returns listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(1999, 1), + tom = YearMonth.of(1999, 2), + ), + ) + + assertThat(brevmalService.hentManuellVedtaksbrevtype(behandling), Is(Brevmal.VEDTAK_OPPHØR_MED_ENDRING)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevperiodeTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevperiodeTest.kt new file mode 100644 index 000000000..ee4a61ed2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/BrevperiodeTest.kt @@ -0,0 +1,201 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import BegrunnelseDataTestConfig +import BrevPeriodeOutput +import BrevPeriodeTestConfig +import EØSBegrunnelseTestConfig +import FritekstBegrunnelseTestConfig +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.familie.ba.sak.common.Utils.formaterBeløp +import no.nav.familie.ba.sak.config.testSanityKlient +import no.nav.familie.ba.sak.kjerne.brev.domene.BegrunnelseMedTriggere +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertVedtaksperiode +import no.nav.familie.ba.sak.kjerne.brev.domene.RestBehandlingsgrunnlagForBrev +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.eøs.EØSBegrunnelseMedTriggere +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.FritekstBegrunnelse +import no.nav.familie.kontrakter.felles.objectMapper +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestReporter +import java.io.File + +class BrevperiodeTest { + + @Test + @Disabled("Må sees nøyere på i forbindelse med brevperioder") + fun test(testReporter: TestReporter) { + val testmappe = File("./src/test/resources/brevperiodeCaser") + + val sanityBegrunnelser = testSanityKlient.hentBegrunnelserMap() + val sanityEØSBegrunnelser = testSanityKlient.hentEØSBegrunnelserMap() + + val antallFeil = testmappe.list()?.fold(0) { acc, it -> + + val fil = File("$testmappe/$it") + + val behandlingsresultatPersonTestConfig = + try { + objectMapper.readValue(fil.readText()) + } catch (e: Exception) { + testReporter.publishEntry("Feil i fil: $it") + testReporter.publishEntry(e.message) + return@fold acc + 1 + } + + val minimertVedtaksperiode = + MinimertVedtaksperiode( + fom = behandlingsresultatPersonTestConfig.fom, + tom = behandlingsresultatPersonTestConfig.tom, + type = behandlingsresultatPersonTestConfig.vedtaksperiodetype, + begrunnelser = behandlingsresultatPersonTestConfig + .begrunnelser.map { it.tilBrevBegrunnelseGrunnlag(sanityBegrunnelser) }, + fritekster = behandlingsresultatPersonTestConfig.fritekster, + minimerteUtbetalingsperiodeDetaljer = behandlingsresultatPersonTestConfig + .personerPåBehandling + .flatMap { it.tilUtbetalingsperiodeDetaljer() }, + eøsBegrunnelser = behandlingsresultatPersonTestConfig.eøsBegrunnelser?.map { + EØSBegrunnelseMedTriggere( + eøsBegrunnelse = it, + sanityEØSBegrunnelse = sanityEØSBegrunnelser[it]!!, + ) + } ?: emptyList(), + ) + + val restBehandlingsgrunnlagForBrev = RestBehandlingsgrunnlagForBrev( + personerPåBehandling = behandlingsresultatPersonTestConfig.personerPåBehandling.map { it.tilMinimertPerson() }, + minimertePersonResultater = behandlingsresultatPersonTestConfig.personerPåBehandling.map { it.tilMinimertePersonResultater() }, + minimerteEndredeUtbetalingAndeler = behandlingsresultatPersonTestConfig.personerPåBehandling.flatMap { it.tilMinimerteEndredeUtbetalingAndeler() }, + fagsakType = FagsakType.NORMAL, + ) + + val brevperiode = try { + BrevPeriodeGenerator( + minimertVedtaksperiode = minimertVedtaksperiode, + restBehandlingsgrunnlagForBrev = restBehandlingsgrunnlagForBrev, + uregistrerteBarn = behandlingsresultatPersonTestConfig.uregistrerteBarn, + erFørsteVedtaksperiodePåFagsak = behandlingsresultatPersonTestConfig.erFørsteVedtaksperiodePåFagsak, + brevMålform = behandlingsresultatPersonTestConfig.brevMålform, + barnMedReduksjonFraForrigeBehandlingIdent = behandlingsresultatPersonTestConfig.hentBarnMedReduksjonFraForrigeBehandling() + .map { it.personIdent }, + minimerteKompetanserForPeriode = behandlingsresultatPersonTestConfig.kompetanser?.map { + it.tilMinimertKompetanse( + behandlingsresultatPersonTestConfig.personerPåBehandling, + ) + } ?: emptyList(), + minimerteKompetanserSomStopperRettFørPeriode = behandlingsresultatPersonTestConfig.kompetanserSomStopperRettFørPeriode?.map { + it.tilMinimertKompetanse( + behandlingsresultatPersonTestConfig.personerPåBehandling, + ) + } ?: emptyList(), + dødeBarnForrigePeriode = emptyList(), + ).genererBrevPeriode() + } catch (e: Exception) { + testReporter.publishEntry( + "Feil i test: $it" + + "\nFeilmelding: ${e.message}" + + "\nFil: ${e.stackTrace.first()}" + + "\n-----------------------------------\n", + ) + return@fold acc + 1 + } + + val feil = erLike( + forventetOutput = behandlingsresultatPersonTestConfig.forventetOutput, + output = brevperiode, + ) + + if (feil.isNotEmpty()) { + testReporter.publishEntry( + it, + "${behandlingsresultatPersonTestConfig.beskrivelse}\n\n" + + feil.joinToString("\n\n") + + "\n-----------------------------------\n", + ) + acc + 1 + } else { + acc + } + } + + assert(antallFeil == 0) + } + + private fun erLike( + forventetOutput: BrevPeriodeOutput?, + output: BrevPeriode?, + ): List { + val feil = mutableListOf() + + fun validerFelt(forventet: String?, faktisk: String?, variabelNavn: String) { + if (forventet != faktisk) { + feil.add( + "Forventet $variabelNavn var: '$forventet', men fikk '$faktisk'", + ) + } + } + + if (forventetOutput == null || output == null) { + if (forventetOutput != null) { + feil.add("Output er null, men forventet output er $forventetOutput.") + } + if (output != null) { + feil.add("Forventet output er null, men output er $output.") + } + } else { + validerFelt(forventetOutput.fom, output.fom?.single(), "fom") + validerFelt(forventetOutput.tom, output.tom?.single(), "tom") + validerFelt(forventetOutput.type, output.type?.single(), "type") + validerFelt(forventetOutput.barnasFodselsdager, output.barnasFodselsdager?.single(), "barnasFodselsdager") + validerFelt(forventetOutput.antallBarn, output.antallBarn?.single(), "antallBarn") + validerFelt( + if (forventetOutput.belop != null) { + formaterBeløp(forventetOutput.belop) + } else { + null + }, + output.belop?.single(), + "belop", + ) + + val forventedeBegrunnelser = forventetOutput.begrunnelser.map { + when (it) { + is BegrunnelseDataTestConfig -> it.tilBegrunnelseData() + is FritekstBegrunnelseTestConfig -> FritekstBegrunnelse(it.fritekst) + is EØSBegrunnelseTestConfig -> it.tilEØSBegrunnelseData() + else -> throw IllegalArgumentException("Ugyldig testconfig") + } + } + + if (forventedeBegrunnelser.size != output.begrunnelser.size) { + feil.add( + "Forventet antall begrunnelser var ${forventedeBegrunnelser.size} begrunnelser, " + + "men fikk ${output.begrunnelser.size}." + + "\nForventede begrunnelser: $forventedeBegrunnelser" + + "\nOutput: ${output.begrunnelser}", + ) + } else { + forventedeBegrunnelser.forEachIndexed { index, _ -> + if (forventedeBegrunnelser[index] != output.begrunnelser[index]) { + feil.add( + "Forventet begrunnelse nr. ${index + 1} var: " + + "\n'${forventedeBegrunnelser[index]}', " + + "\nmen fikk " + + "\n'${output.begrunnelser[index]}'", + ) + } + } + } + } + return feil + } + + private fun Standardbegrunnelse.tilBrevBegrunnelseGrunnlag(sanityBegrunnelser: Map) = + BegrunnelseMedTriggere( + standardbegrunnelse = this, + triggesAv = sanityBegrunnelser[this]!!.triggesAv, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentDistribueringServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentDistribueringServiceTest.kt new file mode 100644 index 000000000..3d5429f4d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentDistribueringServiceTest.kt @@ -0,0 +1,100 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.verify +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.task.DistribuerDokumentDTO +import no.nav.familie.http.client.RessursException +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.prosessering.internal.TaskService +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.http.HttpStatus +import org.springframework.web.client.RestClientResponseException + +@ExtendWith(MockKExtension::class) +internal class DokumentDistribueringServiceTest { + + @MockK(relaxed = true) + private lateinit var taskService: TaskService + + @MockK + private lateinit var integrasjonClient: IntegrasjonClient + + @MockK(relaxed = true) + private lateinit var loggService: LoggService + + @InjectMockKs + private lateinit var dokumentDistribueringService: DokumentDistribueringService + + @Test + fun `Skal kalle 'loggBrevIkkeDistribuertUkjentAdresse' ved 400 kode og 'Mottaker har ukjent adresse' melding`() { + every { + integrasjonClient.distribuerBrev(any()) + } throws RessursException( + httpStatus = HttpStatus.BAD_REQUEST, + ressurs = Ressurs.failure(), + cause = RestClientResponseException("Mottaker har ukjent adresse", 400, "", null, null, null), + ) + + dokumentDistribueringService.prøvDistribuerBrevOgLoggHendelseFraBehandling( + distribuerDokumentDTO = lagDistribuerDokumentDTO(), + loggBehandlerRolle = BehandlerRolle.BESLUTTER, + ) + + verify(exactly = 1) { loggService.opprettBrevIkkeDistribuertUkjentAdresseLogg(any(), any()) } + } + + @Test + fun `Skal kalle 'håndterMottakerDødIngenAdressePåBehandling' ved 410 Gone svar under distribuering`() { + every { + integrasjonClient.distribuerBrev(any()) + } throws RessursException( + httpStatus = HttpStatus.GONE, + ressurs = Ressurs.failure(), + cause = RestClientResponseException("", 410, "", null, null, null), + ) + + dokumentDistribueringService.prøvDistribuerBrevOgLoggHendelseFraBehandling( + distribuerDokumentDTO = lagDistribuerDokumentDTO(), + loggBehandlerRolle = BehandlerRolle.BESLUTTER, + ) + + verify(exactly = 1) { + loggService.opprettBrevIkkeDistribuertUkjentDødsboadresseLogg(any(), any()) + } + } + + @Test + fun `Skal hoppe over distribuering ved 409 Conflict mot dokdist`() { + every { + integrasjonClient.distribuerBrev(any()) + } throws RessursException( + httpStatus = HttpStatus.CONFLICT, + ressurs = Ressurs.failure(), + cause = RestClientResponseException("", 409, "", null, null, null), + ) + + assertDoesNotThrow { + dokumentDistribueringService.prøvDistribuerBrevOgLoggHendelseFraBehandling( + distribuerDokumentDTO = lagDistribuerDokumentDTO(), + loggBehandlerRolle = BehandlerRolle.BESLUTTER, + ) + } + } + + private fun lagDistribuerDokumentDTO() = DistribuerDokumentDTO( + journalpostId = "testId", + behandlingId = 1L, + brevmal = Brevmal.SVARTIDSBREV, + personEllerInstitusjonIdent = "test", + erManueltSendt = true, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentServiceTest.kt new file mode 100644 index 000000000..cb5fe6042 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentServiceTest.kt @@ -0,0 +1,341 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.journalføring.UtgåendeJournalføringService +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.DbJournalpost +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.JournalføringRepository +import no.nav.familie.ba.sak.integrasjoner.organisasjon.OrganisasjonService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.brev.domene.ManueltBrevRequest +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.brev.mottaker.Brevmottaker +import no.nav.familie.ba.sak.kjerne.brev.mottaker.BrevmottakerService +import no.nav.familie.ba.sak.kjerne.brev.mottaker.MottakerType +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.institusjon.Institusjon +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.VilkårsvurderingForNyBehandlingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurderingType +import no.nav.familie.kontrakter.felles.BrukerIdType +import no.nav.familie.kontrakter.felles.arbeidsfordeling.Enhet +import no.nav.familie.kontrakter.felles.dokarkiv.AvsenderMottaker +import no.nav.familie.kontrakter.felles.organisasjon.Organisasjon +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +internal class DokumentServiceTest { + val integrasjonClient = mockk(relaxed = true) + val vilkårsvurderingService = mockk(relaxed = true) + val vilkårsvurderingForNyBehandlingService = mockk(relaxed = true) + val utgåendeJournalføringService = mockk(relaxed = true) + val journalføringRepository = mockk(relaxed = true) + val taskRepository = mockk(relaxed = true) + val fagsakRepository = mockk(relaxed = true) + val organisasjonService = mockk(relaxed = true) + val behandlingHentOgPersisterService = mockk(relaxed = true) + val brevmottakerService = mockk(relaxed = true) + + private val dokumentService: DokumentService = spyk( + DokumentService( + journalføringRepository = journalføringRepository, + taskRepository = taskRepository, + vilkårsvurderingService = vilkårsvurderingService, + vilkårsvurderingForNyBehandlingService = vilkårsvurderingForNyBehandlingService, + rolleConfig = mockk(relaxed = true), + settPåVentService = mockk(relaxed = true), + utgåendeJournalføringService = utgåendeJournalføringService, + fagsakRepository = fagsakRepository, + organisasjonService = organisasjonService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + dokumentGenereringService = mockk(relaxed = true), + brevmottakerService = brevmottakerService, + validerBrevmottakerService = mockk(relaxed = true), + ), + ) + + @Test + fun `sendManueltBrev skal journalføre med brukerIdType ORGNR hvis brukers id er 9 siffer, og FNR ellers`() { + listOf("123456789", "12345678911").forEach { brukerId -> + val avsenderMottaker = slot() + val behandling = lagBehandling() + + val aktør = mockk() + every { aktør.aktivFødselsnummer() } returns "12345678911" + val fagsak = mockk() + every { fagsak.aktør } returns aktør + every { fagsakRepository.finnFagsak(any()) } returns fagsak + every { fagsak.institusjon } returns Institusjon(orgNummer = "123456789", tssEksternId = "xxx") + + every { + utgåendeJournalføringService.journalførManueltBrev( + fnr = any(), + fagsakId = any(), + journalførendeEnhet = any(), + brev = any(), + førsteside = any(), + dokumenttype = any(), + avsenderMottaker = capture(avsenderMottaker), + ) + } returns "mockJournalpostId" + every { journalføringRepository.save(any()) } returns DbJournalpost( + behandling = behandling, + journalpostId = "id", + ) + every { organisasjonService.hentOrganisasjon(any()) } returns Organisasjon( + organisasjonsnummer = brukerId, + navn = "Testinstitusjon", + ) + + runCatching { + dokumentService.sendManueltBrev( + ManueltBrevRequest( + brevmal = Brevmal.INNHENTE_OPPLYSNINGER, + mottakerIdent = brukerId, + enhet = Enhet("enhet", "enhetNavn"), + ), + behandling = behandling, + fagsakId = behandling.fagsak.id, + ) + } + when (brukerId.length) { + 9 -> { + assert(avsenderMottaker.isCaptured) { "AvsenderMottaker skal være fanget" } + assertThat(avsenderMottaker.captured.idType).isEqualTo(BrukerIdType.ORGNR) + assertThat(avsenderMottaker.captured.id).isEqualTo(brukerId) + assertThat(avsenderMottaker.captured.navn).isEqualTo("Testinstitusjon") + } + + else -> assert(!avsenderMottaker.isCaptured) { "AvsenderMottaker skal ikke være fanget" } + } + } + } + + @Test + fun `sendManueltBrev skal legge til opplysningspliktvilkåret når gjeldende og forrige vilkårsvurdering mangler`() { + val brevSomFørerTilOpplysningsplikt = Brevmal.values().filter { it.førerTilOpplysningsplikt() } + + brevSomFørerTilOpplysningsplikt.forEach { brevmal -> + val behandling = lagBehandling() + val vilkårsvurdering = lagVilkårsvurdering(lagPerson().aktør, behandling, Resultat.IKKE_VURDERT) + val personResultat = vilkårsvurdering.personResultater.find { it.erSøkersResultater() }!! + + // Scenario uten eksisterende vilkårsvurdering + every { vilkårsvurderingService.hentAktivForBehandling(any()) } returns null + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling) } returns null + every { + vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + any(), + any(), + null, + ) + } returns + vilkårsvurdering + + every { journalføringRepository.save(any()) } returns + DbJournalpost(behandling = behandling, journalpostId = "id") + + sendBrev(brevmal, behandling) + + assertThat(personResultat.andreVurderinger).extracting("type") + .containsExactly(AnnenVurderingType.OPPLYSNINGSPLIKT) + verify(exactly = 1) { + behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling) + } + verify(exactly = 1) { + vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling(behandling, any(), null) + } + } + } + + @Test + fun `sendManueltBrev skal legge til opplysningspliktvilkåret når gjeldende vilkårsvurdering mangler, men forrige finnes`() { + val brevSomFørerTilOpplysningsplikt = Brevmal.values().filter { it.førerTilOpplysningsplikt() } + + brevSomFørerTilOpplysningsplikt.forEach { brevmal -> + val behandling = lagBehandling() + val forrigeVedtatteBehandling = lagBehandling() + val vilkårsvurdering = lagVilkårsvurdering(lagPerson().aktør, behandling, Resultat.IKKE_VURDERT) + val personResultat = vilkårsvurdering.personResultater.find { it.erSøkersResultater() }!! + + // Scenario uten eksisterende vilkårsvurdering + every { vilkårsvurderingService.hentAktivForBehandling(any()) } returns null + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling) } returns forrigeVedtatteBehandling + every { + vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + any(), + any(), + forrigeVedtatteBehandling, + ) + } returns + vilkårsvurdering + + every { journalføringRepository.save(any()) } returns + DbJournalpost(behandling = behandling, journalpostId = "id") + + sendBrev(brevmal, behandling) + + assertThat(personResultat.andreVurderinger).extracting("type") + .containsExactly(AnnenVurderingType.OPPLYSNINGSPLIKT) + verify(exactly = 1) { + behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling) + } + verify(exactly = 1) { + vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling, + any(), + forrigeVedtatteBehandling, + ) + } + } + } + + @Test + fun `sendManueltBrev skal legge til opplysningspliktvilkåret når vilkårsvurderingen finnes`() { + val brevSomFørerTilOpplysningsplikt = Brevmal.values().filter { it.førerTilOpplysningsplikt() } + + brevSomFørerTilOpplysningsplikt.forEach { brevmal -> + val behandling = lagBehandling() + val vilkårsvurdering = lagVilkårsvurdering(lagPerson().aktør, behandling, Resultat.IKKE_VURDERT) + val personResultat = vilkårsvurdering.personResultater.find { it.erSøkersResultater() }!! + + // Scenario med eksisterende vilkårsvurdering + every { vilkårsvurderingService.hentAktivForBehandling(any()) } returns vilkårsvurdering + every { journalføringRepository.save(any()) } returns + DbJournalpost(behandling = behandling, journalpostId = "id") + + sendBrev(brevmal, behandling) + + assertThat(personResultat.andreVurderinger).extracting("type") + .containsExactly(AnnenVurderingType.OPPLYSNINGSPLIKT) + verify(exactly = 0) { + vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling(behandling, any(), null) + } + } + } + + @Test + @Disabled // Feiler kun pga en refaktorering (BrevmottakerService:97). Mulig det er 'callOriginal()' som er fragile ¯\_(ツ)_/¯ + fun `sendManueltBrev skal sende manuelt brev til FULLMEKTIG og bruker som har FULLMEKTIG manuelt brev mottaker`() { + val behandling = lagBehandling() + val søkersident = behandling.fagsak.aktør.aktivFødselsnummer() + val manueltBrevRequest = ManueltBrevRequest(mottakerIdent = søkersident, brevmal = Brevmal.SVARTIDSBREV) + val avsenderMottakere = mutableListOf() + + every { brevmottakerService.hentBrevmottakere(behandling.id) } returns listOf( + Brevmottaker( + behandlingId = behandling.id, + type = MottakerType.FULLMEKTIG, + navn = "Fullmektig navn", + adresselinje1 = "Test adresse", + postnummer = "0000", + poststed = "Oslo", + landkode = "NO", + ), + ) + every { brevmottakerService.lagMottakereFraBrevMottakere(any(), any(), any()) } answers { callOriginal() } + every { brevmottakerService.hentMottakerNavn(søkersident) } returns "søker" + every { + utgåendeJournalføringService.journalførManueltBrev( + fnr = any(), + fagsakId = any(), + journalførendeEnhet = any(), + brev = any(), + førsteside = any(), + dokumenttype = any(), + avsenderMottaker = capture(avsenderMottakere), + tilManuellMottakerEllerVerge = any(), + ) + } returns "mockJournalPostId" andThen "mockJournalPostId1" + + every { journalføringRepository.save(any()) } returns mockk() + every { taskRepository.save(any()) } returns mockk() + + dokumentService.sendManueltBrev(manueltBrevRequest, behandling, behandling.fagsak.id) + + verify(exactly = 2) { + utgåendeJournalføringService.journalførManueltBrev( + fnr = any(), + fagsakId = any(), + journalførendeEnhet = any(), + brev = any(), + førsteside = any(), + dokumenttype = any(), + avsenderMottaker = any(), + tilManuellMottakerEllerVerge = any(), + ) + } + verify(exactly = 2) { journalføringRepository.save(any()) } + verify(exactly = 2) { taskRepository.save(any()) } + + assertEquals(2, avsenderMottakere.size) + assertEquals("Fullmektig navn", avsenderMottakere.single { it.idType == null }.navn) + } + + @Test + fun `sendManueltBrev skal sende informasjonsbrev manuelt på fagsak`() { + val fagsak = defaultFagsak() + val søkersident = fagsak.aktør.aktivFødselsnummer() + val manueltBrevRequest = + ManueltBrevRequest(mottakerIdent = søkersident, brevmal = Brevmal.INFORMASJONSBREV_KAN_SØKE) + + every { + utgåendeJournalføringService.journalførManueltBrev( + fnr = any(), + fagsakId = any(), + journalførendeEnhet = any(), + brev = any(), + førsteside = any(), + dokumenttype = any(), + avsenderMottaker = any(), + tilManuellMottakerEllerVerge = any(), + ) + } returns "mockJournalPostId" + + every { taskRepository.save(any()) } returns mockk() + + dokumentService.sendManueltBrev(manueltBrevRequest, null, fagsak.id) + + verify(exactly = 1) { + utgåendeJournalføringService.journalførManueltBrev( + fnr = any(), + fagsakId = any(), + journalførendeEnhet = any(), + brev = any(), + førsteside = any(), + dokumenttype = any(), + avsenderMottaker = any(), + tilManuellMottakerEllerVerge = any(), + ) + } + verify(exactly = 0) { journalføringRepository.save(any()) } + verify(exactly = 1) { taskRepository.save(any()) } + } + + private fun sendBrev(brevmal: Brevmal, behandling: Behandling) { + dokumentService.sendManueltBrev( + ManueltBrevRequest( + brevmal = brevmal, + mottakerIdent = "123456789", + enhet = Enhet("enhet", "enhetNavn"), + ), + behandling = behandling, + fagsakId = behandling.fagsak.id, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/Landkoder.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/Landkoder.kt new file mode 100644 index 000000000..5606cfba6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/Landkoder.kt @@ -0,0 +1,21 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.familie.kontrakter.felles.objectMapper +import org.springframework.core.io.ClassPathResource +import java.io.BufferedReader + +data class LandkodeISO2( + val code: String, + val name: String, +) + +fun hentLandkoderISO2(): Map { + val landkoder = + ClassPathResource("landkoder/landkoder.json").inputStream.bufferedReader().use(BufferedReader::readText) + + return objectMapper.readValue>(landkoder) + .associate { it.code to it.name } +} + +val LANDKODER = hentLandkoderISO2() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/MalerServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/MalerServiceTest.kt new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/SanityKlientTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/SanityKlientTest.kt new file mode 100644 index 000000000..b14f731dd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/SanityKlientTest.kt @@ -0,0 +1,24 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import no.nav.familie.ba.sak.config.testSanityKlient +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class SanityKlientTest { + + @Test + fun `Skal teste at vi klarer å hente begrunnelser fra sanity-apiet`() { + val hentBegrunnelser = testSanityKlient.hentBegrunnelser() + val begrunnelserPåApiNavn = hentBegrunnelser.associateBy { it.apiNavn } + assertThat(hentBegrunnelser).hasSize(begrunnelserPåApiNavn.size) + assertThat(hentBegrunnelser).isNotEmpty + } + + @Test + fun `Skal teste at vi klarer å hente eøs-begrunnelser fra sanity-apiet`() { + val hentEØSBegrunnelser = testSanityKlient.hentEØSBegrunnelser() + val begrunnelserPåApiNavn = hentEØSBegrunnelser.associateBy { it.apiNavn } + assertThat(hentEØSBegrunnelser).hasSize(begrunnelserPåApiNavn.size) + assertThat(hentEØSBegrunnelser).isNotEmpty + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevBegrunnelserTestKlasser.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevBegrunnelserTestKlasser.kt new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevPeriodeTestKlasser.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevPeriodeTestKlasser.kt new file mode 100644 index 000000000..546141a9b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevPeriodeTestKlasser.kt @@ -0,0 +1,257 @@ +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.domene.LandNavn +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertAnnenVurdering +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertKompetanse +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertRestEndretAndel +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertRestPersonResultat +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertUregistrertBarn +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertUtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.kjerne.brev.domene.MinimertVilkårResultat +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BegrunnelseData +import no.nav.familie.ba.sak.kjerne.vedtak.domene.EØSBegrunnelseDataMedKompetanse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.MinimertRestPerson +import no.nav.familie.ba.sak.kjerne.vedtak.domene.SøkersRettTilUtvidet +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import java.math.BigDecimal +import java.time.LocalDate + +data class BrevPeriodeTestConfig( + val beskrivelse: String, + + val fom: LocalDate?, + val tom: LocalDate?, + val vedtaksperiodetype: Vedtaksperiodetype, + val begrunnelser: List, + val eøsBegrunnelser: List?, + val fritekster: List, + + val personerPåBehandling: List, + + val uregistrerteBarn: List, + val erFørsteVedtaksperiodePåFagsak: Boolean = false, + val brevMålform: Målform, + + val kompetanser: List? = null, + val kompetanserSomStopperRettFørPeriode: List? = null, + + val forventetOutput: BrevPeriodeOutput?, +) { + fun hentPersonerMedReduksjonFraForrigeBehandling(): List = + this.personerPåBehandling.filter { it.harReduksjonFraForrigeBehandling } + + fun hentBarnMedReduksjonFraForrigeBehandling() = + hentPersonerMedReduksjonFraForrigeBehandling().filter { it.type == PersonType.BARN } +} + +data class BrevPeriodeTestKompetanse( + val id: String, + val søkersAktivitet: KompetanseAktivitet, + val søkersAktivitetsland: String?, + val annenForeldersAktivitet: KompetanseAktivitet, + val annenForeldersAktivitetsland: String, + val barnetsBostedsland: String, + val resultat: KompetanseResultat, +) { + fun tilMinimertKompetanse(personer: List): MinimertKompetanse { + return MinimertKompetanse( + søkersAktivitet = this.søkersAktivitet, + annenForeldersAktivitet = this.annenForeldersAktivitet, + annenForeldersAktivitetslandNavn = LandNavn(this.annenForeldersAktivitetsland), + barnetsBostedslandNavn = LandNavn(this.barnetsBostedsland), + resultat = this.resultat, + personer = personer.filter { it.kompetanseIder?.contains(this.id) == true }.map { it.tilMinimertPerson() }, + søkersAktivitetsland = this.søkersAktivitetsland?.let { LandNavn(this.søkersAktivitetsland) }, + ) + } +} + +data class BrevPeriodeTestPerson( + val personIdent: String = randomFnr(), + val fødselsdato: LocalDate, + val type: PersonType, + val overstyrteVilkårresultater: List, + val andreVurderinger: List, + val endredeUtbetalinger: List, + val utbetalinger: List, + val harReduksjonFraForrigeBehandling: Boolean = false, + val kompetanseIder: List? = null, +) { + fun tilMinimertPerson() = MinimertRestPerson(personIdent = personIdent, fødselsdato = fødselsdato, type = type) + fun tilUtbetalingsperiodeDetaljer() = utbetalinger.map { + it.tilMinimertUtbetalingsperiodeDetalj(this.tilMinimertPerson()) + } + + fun tilMinimerteEndredeUtbetalingAndeler() = + endredeUtbetalinger.map { it.tilMinimertRestEndretUtbetalingAndel(this.personIdent) } + + fun tilMinimertePersonResultater(): MinimertRestPersonResultat { + return MinimertRestPersonResultat( + personIdent = this.personIdent, + minimerteVilkårResultater = hentVilkårForPerson(), + minimerteAndreVurderinger = this.andreVurderinger, + ) + } + + private fun hentVilkårForPerson() = + this.overstyrteVilkårresultater + + Vilkår.hentVilkårFor( + personType = this.type, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ) + .filter { vilkår -> !this.overstyrteVilkårresultater.any { it.vilkårType == vilkår } } + .map { vilkår -> + MinimertVilkårResultat( + vilkårType = vilkår, + periodeFom = this.fødselsdato, + periodeTom = null, + resultat = Resultat.OPPFYLT, + utdypendeVilkårsvurderinger = emptyList(), + erEksplisittAvslagPåSøknad = false, + standardbegrunnelser = emptyList(), + ) + } +} + +data class UtbetalingPåPerson( + val ytelseType: YtelseType, + val utbetaltPerMnd: Int, + val erPåvirketAvEndring: Boolean = false, + val prosent: BigDecimal = BigDecimal(100), + val endringsårsak: Årsak? = null, +) { + fun tilMinimertUtbetalingsperiodeDetalj(minimertRestPerson: MinimertRestPerson) = + MinimertUtbetalingsperiodeDetalj( + person = minimertRestPerson, + utbetaltPerMnd = this.utbetaltPerMnd, + prosent = this.prosent, + erPåvirketAvEndring = this.erPåvirketAvEndring, + ytelseType = this.ytelseType, + endringsårsak = endringsårsak, + ) +} + +data class EndretRestUtbetalingAndelPåPerson( + val periode: MånedPeriode, + val årsak: Årsak, + val søknadstidspunkt: LocalDate = LocalDate.now(), + val avtaletidspunktDeltBosted: LocalDate? = null, +) { + fun tilMinimertRestEndretUtbetalingAndel(personIdent: String) = + MinimertRestEndretAndel( + personIdent = personIdent, + periode = periode, + årsak = årsak, + søknadstidspunkt = søknadstidspunkt, + avtaletidspunktDeltBosted = avtaletidspunktDeltBosted, + ) +} + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type", + defaultImpl = BegrunnelseDataTestConfig::class, +) +@JsonSubTypes( + value = [ + JsonSubTypes.Type(value = FritekstBegrunnelseTestConfig::class, name = "fritekst"), + JsonSubTypes.Type(value = EØSBegrunnelseTestConfig::class, name = "eøsbegrunnelse"), + ], +) +interface TestBegrunnelse + +data class FritekstBegrunnelseTestConfig(val fritekst: String) : TestBegrunnelse + +data class BegrunnelseDataTestConfig( + val gjelderSoker: Boolean, + val barnasFodselsdatoer: String, + val fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling: String, + val fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling: String, + val antallBarn: Int, + val antallBarnOppfyllerTriggereOgHarUtbetaling: Int, + val antallBarnOppfyllerTriggereOgHarNullutbetaling: Int, + val maanedOgAarBegrunnelsenGjelderFor: String?, + val maalform: String, + val apiNavn: String, + val belop: Int, + val soknadstidspunkt: String?, + val avtaletidspunktDeltBosted: String?, + val sokersRettTilUtvidet: String?, +) : TestBegrunnelse { + + fun tilBegrunnelseData() = BegrunnelseData( + belop = Utils.formaterBeløp(this.belop), + gjelderSoker = this.gjelderSoker, + barnasFodselsdatoer = this.barnasFodselsdatoer, + fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling = this.fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling, + fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling = this.fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling, + antallBarn = this.antallBarn, + antallBarnOppfyllerTriggereOgHarUtbetaling = this.antallBarnOppfyllerTriggereOgHarUtbetaling, + antallBarnOppfyllerTriggereOgHarNullutbetaling = this.antallBarnOppfyllerTriggereOgHarNullutbetaling, + maanedOgAarBegrunnelsenGjelderFor = this.maanedOgAarBegrunnelsenGjelderFor, + maalform = this.maalform, + apiNavn = this.apiNavn, + soknadstidspunkt = this.soknadstidspunkt ?: "", + avtaletidspunktDeltBosted = this.avtaletidspunktDeltBosted ?: "", + sokersRettTilUtvidet = this.sokersRettTilUtvidet + ?: SøkersRettTilUtvidet.SØKER_HAR_IKKE_RETT.tilSanityFormat(), + vedtakBegrunnelseType = Standardbegrunnelse.values() + .find { it.sanityApiNavn == this.apiNavn }?.vedtakBegrunnelseType + ?: throw Feil("Fant ikke Standardbegrunnelse med apiNavn ${this.apiNavn}"), + ) +} + +data class EØSBegrunnelseTestConfig( + val apiNavn: String, + val annenForeldersAktivitet: KompetanseAktivitet, + val annenForeldersAktivitetsland: String, + val barnetsBostedsland: String, + val barnasFodselsdatoer: String, + val antallBarn: Int, + val maalform: String, + val sokersAktivitet: KompetanseAktivitet, + val sokersAktivitetsland: String?, +) : TestBegrunnelse { + fun tilEØSBegrunnelseData(): EØSBegrunnelseDataMedKompetanse = EØSBegrunnelseDataMedKompetanse( + apiNavn = this.apiNavn, + annenForeldersAktivitet = this.annenForeldersAktivitet, + annenForeldersAktivitetsland = this.annenForeldersAktivitetsland, + barnetsBostedsland = this.barnetsBostedsland, + barnasFodselsdatoer = this.barnasFodselsdatoer, + antallBarn = this.antallBarn, + maalform = this.maalform, + vedtakBegrunnelseType = EØSStandardbegrunnelse.values() + .find { it.sanityApiNavn == this.apiNavn }?.vedtakBegrunnelseType + ?: throw Feil("Fant ikke EØSStandardbegrunnelse med apiNavn ${this.apiNavn}"), + sokersAktivitet = this.sokersAktivitet, + sokersAktivitetsland = this.sokersAktivitetsland, + ) +} + +data class BrevPeriodeOutput( + val fom: String?, + val tom: String?, + val belop: Int?, + val antallBarn: String?, + val barnasFodselsdager: String?, + val begrunnelser: List, + val type: String, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevTypeTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevTypeTest.kt new file mode 100644 index 000000000..a5a0f8c2d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/BrevTypeTest.kt @@ -0,0 +1,97 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentÅrsak +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class BrevTypeTest { + + private val førerTilAvventerDokumentasjon = listOf( + Brevmal.INNHENTE_OPPLYSNINGER, + Brevmal.INNHENTE_OPPLYSNINGER_INSTITUSJON, + Brevmal.VARSEL_OM_REVURDERING, + Brevmal.VARSEL_OM_REVURDERING_INSTITUSJON, + Brevmal.VARSEL_OM_REVURDERING_DELT_BOSTED_PARAGRAF_14, + Brevmal.INNHENTE_OPPLYSNINGER_ETTER_SØKNAD_I_SED, + Brevmal.VARSEL_OM_REVURDERING_FRA_NASJONAL_TIL_EØS, + Brevmal.VARSEL_OM_VEDTAK_ETTER_SØKNAD_I_SED, + Brevmal.SVARTIDSBREV, + Brevmal.FORLENGET_SVARTIDSBREV, + Brevmal.SVARTIDSBREV_INSTITUSJON, + Brevmal.FORLENGET_SVARTIDSBREV_INSTITUSJON, + ) + + private val eøsDokumentMedAvventerDokumentasjon = listOf( + Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS, + Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER, + ) + + private val førerIkkeTilAvventingAvDokumentasjon = Brevmal.values() + .filter { + it !in førerTilAvventerDokumentasjon && it !in eøsDokumentMedAvventerDokumentasjon + } + + @Test + fun `Skal si om behandling settes på vent`() { + val setterIkkeBehandlingPåVent = Brevmal.values() + .filter { !førerTilAvventerDokumentasjon.contains(it) && it !in eøsDokumentMedAvventerDokumentasjon } + + setterIkkeBehandlingPåVent.forEach { + Assertions.assertFalse(it.setterBehandlingPåVent()) + } + + førerTilAvventerDokumentasjon.forEach { + Assertions.assertTrue(it.setterBehandlingPåVent()) + } + + eøsDokumentMedAvventerDokumentasjon.forEach { + Assertions.assertTrue(it.setterBehandlingPåVent()) + } + + førerIkkeTilAvventingAvDokumentasjon.forEach { + Assertions.assertFalse(it.setterBehandlingPåVent()) + } + } + + @Test + fun `Skal gi riktig ventefrist nasjonal`() { + førerTilAvventerDokumentasjon.forEach { + Assertions.assertEquals( + 21L, + it.ventefristDager(manuellFrist = 21L, behandlingKategori = BehandlingKategori.NASJONAL), + ) + } + + førerIkkeTilAvventingAvDokumentasjon.forEach { + assertThrows { it.ventefristDager(behandlingKategori = BehandlingKategori.NASJONAL) } + } + } + + @Test + fun `Skal gi riktig ventefrist eøs`() { + Assertions.assertEquals(90L, Brevmal.SVARTIDSBREV.ventefristDager(behandlingKategori = BehandlingKategori.EØS)) + Assertions.assertEquals( + 60L, + Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS.ventefristDager(behandlingKategori = BehandlingKategori.EØS), + ) + Assertions.assertEquals( + 60L, + Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER.ventefristDager(behandlingKategori = BehandlingKategori.EØS), + ) + } + + @Test + fun `Skal gi riktig venteårsak`() { + førerTilAvventerDokumentasjon.forEach { + Assertions.assertEquals(SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, it.venteårsak()) + } + + førerIkkeTilAvventingAvDokumentasjon.forEach { + Assertions.assertFalse(it.setterBehandlingPåVent()) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/ManueltBrevRequestTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/ManueltBrevRequestTest.kt new file mode 100644 index 000000000..312ee3039 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/ManueltBrevRequestTest.kt @@ -0,0 +1,154 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.ForlengetSvartidsbrev +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.VarselbrevÅrlegKontrollEøs +import no.nav.familie.kontrakter.felles.arbeidsfordeling.Enhet +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class ManueltBrevRequestTest { + private val årsaker = listOf("1", "2", "3") + private val baseRequest = ManueltBrevRequest( + brevmal = Brevmal.INNHENTE_OPPLYSNINGER, + multiselectVerdier = årsaker, + mottakerIdent = "testident", + mottakerNavn = "testnavn", + enhet = Enhet("testenhetId", "testenhet"), + antallUkerSvarfrist = 3, + ) + + @Test + fun `Forlenget svartidsbrev request skal gi forlenget svartid brevmal med riktig data`() { + val brev = + baseRequest.copy(brevmal = Brevmal.FORLENGET_SVARTIDSBREV).tilBrev("saksbehandlerNavn") { emptyMap() } + + assertThat(brev::class).isEqualTo(ForlengetSvartidsbrev::class) + brev as ForlengetSvartidsbrev + + assertThat(brev.mal).isEqualTo(Brevmal.FORLENGET_SVARTIDSBREV) + + assertThat(brev.data.flettefelter.antallUkerSvarfrist!!.single()).isEqualTo("3") + assertThat(brev.data.flettefelter.aarsakerSvartidsbrev!!).isEqualTo(årsaker) + } + + @Test + fun `Forlenget svartidsbrev institusjon request skal gi forlenget svartid brevmal med riktig data`() { + val brev = baseRequest.copy( + brevmal = Brevmal.FORLENGET_SVARTIDSBREV_INSTITUSJON, + mottakerIdent = "998765432", + mottakerNavn = "Testorganisasjon", + vedrørende = PersonITest( + fødselsnummer = "testident", + navn = "testnavn", + ), + ) + .tilBrev("saksbehandlerNavn") { emptyMap() } + + assertThat(brev::class).isEqualTo(ForlengetSvartidsbrev::class) + brev as ForlengetSvartidsbrev + + assertThat(brev.mal).isEqualTo(Brevmal.FORLENGET_SVARTIDSBREV_INSTITUSJON) + + assertThat(brev.data.flettefelter.antallUkerSvarfrist!!.single()).isEqualTo("3") + assertThat(brev.data.flettefelter.aarsakerSvartidsbrev!!).isEqualTo(årsaker) + assertThat(brev.data.flettefelter.organisasjonsnummer).containsExactly("998765432") + assertThat(brev.data.flettefelter.navn).containsExactly("Testorganisasjon") + assertThat(brev.data.flettefelter.fodselsnummer).containsExactly("testident") + assertThat(brev.data.flettefelter.gjelder).containsExactly("testnavn") + } + + @Test + fun `Innhente opplysninger brev til person og institusjon`() { + val fnr = "12345678910" + val orgnr = "123456789" + val brevRequestTilPerson = baseRequest.copy( + mottakerIdent = fnr, + ) + val brevRequestTilInstitusjon = baseRequest.copy( + brevmal = Brevmal.INNHENTE_OPPLYSNINGER_INSTITUSJON, + mottakerIdent = orgnr, + vedrørende = PersonITest( + fødselsnummer = fnr, + navn = "navn tilhørende $fnr", + ), + ) + brevRequestTilPerson.tilBrev("saksbehandlerNavn") { emptyMap() }.data.apply { + assertThat(flettefelter.fodselsnummer).containsExactly(brevRequestTilPerson.mottakerIdent) + assertThat(flettefelter.navn).containsExactly(brevRequestTilPerson.mottakerNavn) + assertThat(flettefelter.organisasjonsnummer).isNull() + assertThat(flettefelter.gjelder).isNull() + } + brevRequestTilInstitusjon.tilBrev("saksbehandlerNavn") { emptyMap() }.data.apply { + assertThat(flettefelter.organisasjonsnummer).containsExactly(brevRequestTilInstitusjon.mottakerIdent) + assertThat(flettefelter.fodselsnummer).containsExactly(brevRequestTilInstitusjon.vedrørende?.fødselsnummer) + assertThat(flettefelter.navn).containsExactly(brevRequestTilPerson.mottakerNavn) + assertThat(flettefelter.gjelder).containsExactly(brevRequestTilInstitusjon.vedrørende?.navn) + } + } + + @Test + fun `Varsel årleg kontroll eøs request skal gi varsel årleg kontroll eøs brevmal med riktig data`() { + val brev = baseRequest.copy(brevmal = Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS, mottakerlandSed = listOf("SE")) + .tilBrev("saksbehandlerNavn") { mapOf(Pair("SE", "Sverige")) } + + assertThat(brev::class).isEqualTo(VarselbrevÅrlegKontrollEøs::class) + brev as VarselbrevÅrlegKontrollEøs + + assertThat(brev.mal).isEqualTo(Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS) + + assertThat(brev.data.flettefelter.mottakerlandSed!!.single()).isEqualTo("Sverige") + assertThat(brev.data.flettefelter.dokumentliste!!.isEmpty()).isTrue + } + + @Test + fun `Varsel årleg kontroll eøs med innhenting av opplysninger request skal gi varsel årleg kontroll eøs brevmal med riktig data`() { + val dokumentliste = listOf("Dokument 1", "Dokument 2") + val brev = baseRequest.copy( + brevmal = Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER, + mottakerlandSed = listOf("SE"), + multiselectVerdier = dokumentliste, + ) + .tilBrev("saksbehandlerNavn") { mapOf(Pair("SE", "Sverige")) } + + assertThat(brev::class).isEqualTo(VarselbrevÅrlegKontrollEøs::class) + brev as VarselbrevÅrlegKontrollEøs + + assertThat(brev.mal).isEqualTo(Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER) + + assertThat(brev.data.flettefelter.mottakerlandSed!!.single()).isEqualTo("Sverige") + assertThat(brev.data.flettefelter.dokumentliste!!.isEmpty()).isFalse + assertThat(brev.data.flettefelter.dokumentliste).containsAll(dokumentliste) + } + + @Test + fun `Varsel årleg kontroll EØS request med flere mottakerland skal gi riktig brevdata`() { + val brev = + baseRequest.copy(brevmal = Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS, mottakerlandSed = listOf("SE", "DK")) + .tilBrev("saksbehandlerNavn") { mapOf(Pair("SE", "Sverige"), Pair("DK", "Danmark")) } + + assertThat(brev::class).isEqualTo(VarselbrevÅrlegKontrollEøs::class) + brev as VarselbrevÅrlegKontrollEøs + + assertThat(brev.mal).isEqualTo(Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS) + + assertThat(brev.data.flettefelter.mottakerlandSed!!.single()).isEqualTo("Sverige og Danmark") + } + + @Test + fun `Varsel årleg kontroll EØS request skal validere mottakerland`() { + val brevRequest = + baseRequest.copy( + brevmal = Brevmal.VARSEL_OM_ÅRLIG_REVURDERING_EØS, + mottakerlandSed = listOf("SE", "NO"), + ) + + assertThrows { + brevRequest.tilBrev("saksbehandlerNavn") { mapOf(Pair("SE", "Sverige"), Pair("NO", "Norge")) } + } + } + + class PersonITest(override val fødselsnummer: String, override val navn: String) : Person +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/SanityBegrunnelseTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/SanityBegrunnelseTest.kt new file mode 100644 index 000000000..63e970620 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/domene/SanityBegrunnelseTest.kt @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.kjerne.brev.domene + +import no.nav.familie.ba.sak.common.lagRestSanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class SanityBegrunnelseTest { + @Test + fun `skal fjerne ugyldige enumverdier`() { + val restSanityBegrunnelse = lagRestSanityBegrunnelse( + apiNavn = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET.sanityApiNavn, + ovrigeTriggere = listOf( + ØvrigTrigger.BARN_MED_6_ÅRS_DAG.name, + "IKKE_GYLDIG_ØVRIG_TRIGGER", + ), + ) + Assertions.assertEquals( + listOf( + ØvrigTrigger.BARN_MED_6_ÅRS_DAG, + ), + restSanityBegrunnelse.tilSanityBegrunnelse()!!.ovrigeTriggere?.toList(), + ) + } + + @Test + fun `skal konverdere string til enumverdi dersom det finnes og null ellers`() { + Assertions.assertEquals(null, "IKKE_GYLDIG_VERDI".finnEnumverdi<ØvrigTrigger>("")) + Assertions.assertEquals( + ØvrigTrigger.BARN_MED_6_ÅRS_DAG, + "BARN_MED_6_ÅRS_DAG".finnEnumverdi<ØvrigTrigger>(""), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerServiceTest.kt new file mode 100644 index 000000000..edc5f7908 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerServiceTest.kt @@ -0,0 +1,222 @@ +package no.nav.familie.ba.sak.kjerne.brev.mottaker + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.ekstern.restDomene.RestBrevmottaker +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.behandling.ValiderBrevmottakerService +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.data.repository.findByIdOrNull + +@ExtendWith(MockKExtension::class) +internal class BrevmottakerServiceTest { + + @MockK + private lateinit var brevmottakerRepository: BrevmottakerRepository + + @MockK + private lateinit var personidentService: PersonidentService + + @MockK + private lateinit var personopplysningerService: PersonopplysningerService + + @MockK + private lateinit var validerBrevmottakerService: ValiderBrevmottakerService + + @MockK + private lateinit var loggService: LoggService + + @InjectMockKs + private lateinit var brevmottakerService: BrevmottakerService + + private val søkersident = "123" + private val søkersnavn = "Test søker" + + @Test + fun `lagMottakereFraBrevMottakere skal lage mottakere når brevmottaker er FULLMEKTIG og bruker har norsk adresse`() { + val brevmottakere = listOf(lagBrevMottaker(mottakerType = MottakerType.FULLMEKTIG)) + every { brevmottakerRepository.finnBrevMottakereForBehandling(any()) } returns brevmottakere + + val mottakerInfo = brevmottakerService.lagMottakereFraBrevMottakere(brevmottakere, søkersident, søkersnavn) + assertTrue { mottakerInfo.size == 2 } + + assertEquals(søkersnavn, mottakerInfo.first().navn) + assertTrue { mottakerInfo.first().manuellAdresseInfo == null } + + assertEquals("John Doe", mottakerInfo.last().navn) + assertTrue { mottakerInfo.last().manuellAdresseInfo != null } + } + + @Test + fun `lagMottakereFraBrevMottakere skal lage mottakere når brevmottaker er FULLMEKTIG og bruker har utenlandsk adresse`() { + val brevmottakere = listOf( + lagBrevMottaker(mottakerType = MottakerType.FULLMEKTIG), + lagBrevMottaker( + mottakerType = MottakerType.BRUKER_MED_UTENLANDSK_ADRESSE, + poststed = "Munchen", + landkode = "DE", + ), + ) + every { brevmottakerRepository.finnBrevMottakereForBehandling(any()) } returns brevmottakere + + val mottakerInfo = brevmottakerService.lagMottakereFraBrevMottakere(brevmottakere, søkersident, søkersnavn) + assertTrue { mottakerInfo.size == 2 } + + assertEquals(søkersnavn, mottakerInfo.first().navn) + assertTrue { mottakerInfo.first().manuellAdresseInfo != null } + assertTrue { mottakerInfo.first().manuellAdresseInfo!!.landkode == "DE" } + + assertEquals("John Doe", mottakerInfo.last().navn) + assertTrue { mottakerInfo.last().manuellAdresseInfo != null } + } + + @Test + fun `lagMottakereFraBrevMottakere skal lage mottakere når brevmottaker er VERGE og bruker har utenlandsk adresse`() { + val brevmottakere = listOf( + lagBrevMottaker(mottakerType = MottakerType.VERGE), + lagBrevMottaker( + mottakerType = MottakerType.BRUKER_MED_UTENLANDSK_ADRESSE, + poststed = "Munchen", + landkode = "DE", + ), + ) + every { brevmottakerRepository.finnBrevMottakereForBehandling(any()) } returns brevmottakere + + val mottakerInfo = brevmottakerService.lagMottakereFraBrevMottakere(brevmottakere, søkersident, søkersnavn) + assertTrue { mottakerInfo.size == 2 } + + assertEquals(søkersnavn, mottakerInfo.first().navn) + assertTrue { mottakerInfo.first().manuellAdresseInfo != null } + assertTrue { mottakerInfo.first().manuellAdresseInfo!!.landkode == "DE" } + + assertEquals("John Doe", mottakerInfo.last().navn) + assertTrue { mottakerInfo.last().manuellAdresseInfo != null } + } + + @Test + fun `lagMottakereFraBrevMottakere skal lage mottakere når bruker har utenlandsk adresse`() { + val brevmottakere = listOf( + lagBrevMottaker( + mottakerType = MottakerType.BRUKER_MED_UTENLANDSK_ADRESSE, + poststed = "Munchen", + landkode = "DE", + ), + ) + every { brevmottakerRepository.finnBrevMottakereForBehandling(any()) } returns brevmottakere + + val mottakerInfo = brevmottakerService.lagMottakereFraBrevMottakere(brevmottakere, søkersident, søkersnavn) + assertTrue { mottakerInfo.size == 1 } + + assertEquals(søkersnavn, mottakerInfo.first().navn) + assertTrue { mottakerInfo.first().manuellAdresseInfo != null } + assertTrue { mottakerInfo.first().manuellAdresseInfo!!.landkode == "DE" } + } + + @Test + fun `lagMottakereFraBrevMottakere skal lage mottakere når bruker har dødsbo`() { + val brevmottakere = listOf( + lagBrevMottaker( + mottakerType = MottakerType.DØDSBO, + poststed = "Munchen", + landkode = "DE", + ), + ) + every { brevmottakerRepository.finnBrevMottakereForBehandling(any()) } returns brevmottakere + + val mottakerInfo = brevmottakerService.lagMottakereFraBrevMottakere(brevmottakere, søkersident, søkersnavn) + assertTrue { mottakerInfo.size == 1 } + + assertEquals(søkersnavn, mottakerInfo.first().navn) + assertTrue { mottakerInfo.first().manuellAdresseInfo != null } + assertTrue { mottakerInfo.first().manuellAdresseInfo!!.landkode == "DE" } + } + + @Test + fun `lagMottakereFraBrevMottakere skal kaste feil når brevmottakere inneholder ugyldig kombinasjon`() { + val brevmottakere = listOf( + lagBrevMottaker( + mottakerType = MottakerType.VERGE, + poststed = "Munchen", + landkode = "DE", + ), + lagBrevMottaker( + mottakerType = MottakerType.FULLMEKTIG, + poststed = "Munchen", + landkode = "DE", + ), + ) + every { brevmottakerRepository.finnBrevMottakereForBehandling(any()) } returns brevmottakere + + assertThrows { + brevmottakerService.lagMottakereFraBrevMottakere(brevmottakere, søkersident, søkersnavn) + }.also { + assertTrue(it.frontendFeilmelding!!.contains("kan ikke kombineres")) + } + } + + @Test + fun `leggTilBrevmottaker skal lagre logg på at brevmottaker legges til`() { + val restBrevmottaker = mockk(relaxed = true) + + every { validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligePersonerMedManuelleBrevmottakere(any(), any()) } just runs + every { loggService.opprettBrevmottakerLogg(any(), false) } just runs + every { brevmottakerRepository.save(any()) } returns mockk() + + brevmottakerService.leggTilBrevmottaker(restBrevmottaker, 200) + + verify { loggService.opprettBrevmottakerLogg(any(), false) } + verify { brevmottakerRepository.save(any()) } + } + + @Test + fun `fjernBrevmottaker skal kaste feil dersom brevmottakeren ikke finnes`() { + every { brevmottakerRepository.findByIdOrNull(404) } returns null + + assertThrows { + brevmottakerService.fjernBrevmottaker(404) + } + + verify { brevmottakerRepository.findByIdOrNull(404) } + } + + @Test + fun `fjernBrevmottaker skal lagre logg på at brevmottaker fjernes`() { + val mocketBrevmottaker = mockk() + + every { brevmottakerRepository.findByIdOrNull(200) } returns mocketBrevmottaker + every { loggService.opprettBrevmottakerLogg(mocketBrevmottaker, true) } just runs + every { brevmottakerRepository.deleteById(200) } just runs + + brevmottakerService.fjernBrevmottaker(200) + + verify { brevmottakerRepository.findByIdOrNull(200) } + verify { loggService.opprettBrevmottakerLogg(mocketBrevmottaker, true) } + verify { brevmottakerRepository.deleteById(200) } + } + + private fun lagBrevMottaker(mottakerType: MottakerType, poststed: String = "Oslo", landkode: String = "NO") = + Brevmottaker( + behandlingId = 1, + type = mottakerType, + navn = "John Doe", + adresselinje1 = "adresse 1", + adresselinje2 = "adresse 2", + postnummer = "000", + poststed = poststed, + landkode = landkode, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelServiceTest.kt new file mode 100644 index 000000000..f553e6566 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelServiceTest.kt @@ -0,0 +1,123 @@ +package no.nav.familie.ba.sak.kjerne.endretutbetaling + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndelRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.tilRestEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.time.LocalDate +import java.time.YearMonth + +class EndretUtbetalingAndelServiceTest { + + private val mockEndretUtbetalingAndelRepository = mockk() + private val mockPersongrunnlagService = mockk() + private val mockPersonopplysningGrunnlagRepository = mockk() + private val mockAndelTilkjentYtelseRepository = mockk() + private val mockVilkårsvurderingService = mockk() + private val mockEndretUtbetalingAndelHentOgPersisterService = mockk() + + private lateinit var endretUtbetalingAndelService: EndretUtbetalingAndelService + + @BeforeEach + fun setup() { + val beregningService = mockk() + endretUtbetalingAndelService = EndretUtbetalingAndelService( + endretUtbetalingAndelRepository = mockEndretUtbetalingAndelRepository, + personopplysningGrunnlagRepository = mockPersonopplysningGrunnlagRepository, + beregningService = beregningService, + persongrunnlagService = mockPersongrunnlagService, + andelTilkjentYtelseRepository = mockAndelTilkjentYtelseRepository, + vilkårsvurderingService = mockVilkårsvurderingService, + endretUtbetalingAndelHentOgPersisterService = mockEndretUtbetalingAndelHentOgPersisterService, + ) + } + + @Test + fun `Skal kaste feil hvis endringsperiode har årsak delt bosted, men ikke overlapper med delt bosted perioder`() { + val behandling = lagBehandling() + val barn = lagPerson(type = PersonType.BARN) + val endretUtbetalingAndel = lagEndretUtbetalingAndelMedAndelerTilkjentYtelse( + behandlingId = behandling.id, + person = barn, + årsak = Årsak.DELT_BOSTED, + fom = YearMonth.now().minusMonths(5), + tom = YearMonth.now().minusMonths(1), + ) + val restEndretUtbetalingAndel = endretUtbetalingAndel.tilRestEndretUtbetalingAndel() + + val andelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse( + person = barn, + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(5), + ), + lagAndelTilkjentYtelse( + person = barn, + fom = YearMonth.now().plusMonths(6), + tom = YearMonth.now().plusMonths(11), + ), + ) + + val vilkårsvurderingUtenDeltBosted = Vilkårsvurdering( + behandling = behandling, + ) + vilkårsvurderingUtenDeltBosted.personResultater = setOf( + lagPersonResultat( + vilkårsvurdering = vilkårsvurderingUtenDeltBosted, + person = barn, + resultat = Resultat.OPPFYLT, + periodeFom = endretUtbetalingAndel.fom?.minusMonths(1)?.førsteDagIInneværendeMåned(), + periodeTom = LocalDate.now(), + erDeltBosted = false, + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + vilkårType = Vilkår.BOR_MED_SØKER, + ), + ) + + every { mockEndretUtbetalingAndelRepository.getById(any()) } returns endretUtbetalingAndel.endretUtbetalingAndel + every { mockPersongrunnlagService.hentPersonerPåBehandling(any(), behandling) } returns listOf(barn) + every { mockPersonopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandling.id) } returns lagTestPersonopplysningGrunnlag( + behandling.id, + barn, + ) + every { mockAndelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId = behandling.id) } returns andelerTilkjentYtelse + every { mockEndretUtbetalingAndelHentOgPersisterService.hentForBehandling(behandlingId = behandling.id) } returns emptyList() + every { mockVilkårsvurderingService.hentAktivForBehandling(behandlingId = behandling.id) } returns vilkårsvurderingUtenDeltBosted + + val feil = assertThrows { + endretUtbetalingAndelService.oppdaterEndretUtbetalingAndelOgOppdaterTilkjentYtelse( + behandling = behandling, + endretUtbetalingAndelId = endretUtbetalingAndel.id, + restEndretUtbetalingAndel = restEndretUtbetalingAndel, + ) + } + Assertions.assertEquals( + "Du har valgt årsaken 'delt bosted', denne samstemmer ikke med vurderingene gjort på vilkårsvurderingssiden i perioden du har valgt.", + feil.frontendFeilmelding, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelValideringTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelValideringTest.kt new file mode 100644 index 000000000..5c1301bb9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/EndretUtbetalingAndelValideringTest.kt @@ -0,0 +1,925 @@ +package no.nav.familie.ba.sak.kjerne.endretutbetaling + +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndel +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.EndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerAtAlleOpprettedeEndringerErUtfylt +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerAtEndringerErTilknyttetAndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerDeltBosted +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerIngenOverlappendeEndring +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerPeriodeInnenforTilkjentytelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelValidering.validerÅrsak +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth +import kotlin.random.Random + +class EndretUtbetalingAndelValideringTest { + + val søker = lagPerson(type = PersonType.SØKER) + val barn = lagPerson(type = PersonType.BARN) + val endretUtbetalingAndelUtvidetNullutbetaling = + endretUtbetalingAndel(søker, YtelseType.UTVIDET_BARNETRYGD, BigDecimal.ZERO) + val endretUtbetalingAndelDeltBostedNullutbetaling = + endretUtbetalingAndel(barn, YtelseType.ORDINÆR_BARNETRYGD, BigDecimal.ZERO) + + @Test + fun `skal sjekke at en endret periode ikke overlapper med eksisterende endringsperioder`() { + val barn1 = tilfeldigPerson() + val barn2 = tilfeldigPerson() + val endretUtbetalingAndel = EndretUtbetalingAndel( + behandlingId = 1, + person = barn1, + fom = YearMonth.of(2020, 2), + tom = YearMonth.of(2020, 6), + årsak = Årsak.DELT_BOSTED, + begrunnelse = "begrunnelse", + prosent = BigDecimal(100), + søknadstidspunkt = LocalDate.now(), + avtaletidspunktDeltBosted = LocalDate.now(), + ) + + val feil = assertThrows { + validerIngenOverlappendeEndring( + endretUtbetalingAndel, + listOf( + endretUtbetalingAndel.copy( + fom = YearMonth.of(2018, 4), + tom = YearMonth.of(2019, 2), + ), + endretUtbetalingAndel.copy( + fom = YearMonth.of(2020, 4), + tom = YearMonth.of(2021, 2), + ), + ), + ) + } + assertEquals( + "Perioden som blir forsøkt lagt til overlapper med eksisterende periode på person.", + feil.melding, + ) + + // Resterende kall skal validere ok. + validerIngenOverlappendeEndring( + endretUtbetalingAndel, + listOf( + endretUtbetalingAndel.copy( + fom = endretUtbetalingAndel.tom!!.plusMonths(1), + tom = endretUtbetalingAndel.tom!!.plusMonths(10), + ), + ), + ) + validerIngenOverlappendeEndring( + endretUtbetalingAndel, + listOf(endretUtbetalingAndel.copy(person = barn2)), + ) + validerIngenOverlappendeEndring( + endretUtbetalingAndel, + listOf(endretUtbetalingAndel.copy(årsak = Årsak.ALLEREDE_UTBETALT)), + ) + } + + @Test + fun `skal sjekke at en endret periode ikke strekker seg utover ytterpunktene for tilkjent ytelse`() { + val barn1 = tilfeldigPerson() + val barn2 = tilfeldigPerson() + + val andelTilkjentYtelser = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 2), + tom = YearMonth.of(2020, 4), + person = barn1, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2020, 7), + tom = YearMonth.of(2020, 10), + person = barn1, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.of(2018, 10), + tom = YearMonth.of(2021, 10), + person = barn2, + ), + ) + + val endretUtbetalingAndel = EndretUtbetalingAndel( + behandlingId = 1, + person = barn1, + fom = YearMonth.of(2020, 2), + tom = YearMonth.of(2020, 6), + årsak = Årsak.DELT_BOSTED, + begrunnelse = "begrunnelse", + prosent = BigDecimal(100), + søknadstidspunkt = LocalDate.now(), + avtaletidspunktDeltBosted = LocalDate.now(), + ) + + var feil = assertThrows { + validerPeriodeInnenforTilkjentytelse(endretUtbetalingAndel, emptyList()) + } + assertEquals( + "Det er ingen tilkjent ytelse for personen det blir forsøkt lagt til en endret periode for.", + feil.melding, + ) + + val endretUtbetalingAndelerSomIkkeValiderer = listOf( + endretUtbetalingAndel.copy(fom = YearMonth.of(2020, 1), tom = YearMonth.of(2020, 11)), + endretUtbetalingAndel.copy(fom = YearMonth.of(2020, 1), tom = YearMonth.of(2020, 4)), + endretUtbetalingAndel.copy(fom = YearMonth.of(2020, 2), tom = YearMonth.of(2020, 11)), + ) + + endretUtbetalingAndelerSomIkkeValiderer.forEach { + feil = assertThrows { + validerPeriodeInnenforTilkjentytelse(it, andelTilkjentYtelser) + } + assertEquals( + "Det er ingen tilkjent ytelse for personen det blir forsøkt lagt til en endret periode for.", + feil.melding, + ) + } + + val endretUtbetalingAndelerSomValiderer = listOf( + endretUtbetalingAndel, + endretUtbetalingAndel.copy(fom = YearMonth.of(2020, 2), tom = YearMonth.of(2020, 10)), + endretUtbetalingAndel.copy(fom = YearMonth.of(2018, 10), tom = YearMonth.of(2021, 10), person = barn2), + ) + + endretUtbetalingAndelerSomValiderer.forEach { validerPeriodeInnenforTilkjentytelse(it, andelTilkjentYtelser) } + } + + @Test + fun `Skal kaste feil hvis endringsperiode med årsak delt bosted ikke overlapper helt med delt bosted periode`() { + val endretUtbetalingAndel = EndretUtbetalingAndel( + behandlingId = 1, + person = tilfeldigPerson(), + fom = YearMonth.of(2020, 2), + tom = YearMonth.of(2020, 6), + årsak = Årsak.DELT_BOSTED, + begrunnelse = "begrunnelse", + prosent = BigDecimal(100), + søknadstidspunkt = LocalDate.now(), + avtaletidspunktDeltBosted = LocalDate.now(), + ) + assertThrows { + validerDeltBosted( + endretUtbetalingAndel = endretUtbetalingAndel, + deltBostedPerioder = listOf( + MånedPeriode( + fom = YearMonth.of(2020, 2), + tom = YearMonth.of(2020, 4), + ), + ), + ) + } + + assertThrows { + validerDeltBosted( + endretUtbetalingAndel = endretUtbetalingAndel, + deltBostedPerioder = listOf( + MånedPeriode( + fom = YearMonth.of(2020, 7), + tom = YearMonth.of(2020, 10), + ), + ), + ) + } + } + + @Test + fun `Skal kaste feil hvis endringsårsak er delt bosted og det ikke eksisterer delt bosted perioder`() { + val endretUtbetalingAndel = EndretUtbetalingAndel( + behandlingId = 1, + person = tilfeldigPerson(), + fom = YearMonth.of(2020, 2), + tom = YearMonth.of(2020, 6), + årsak = Årsak.DELT_BOSTED, + begrunnelse = "begrunnelse", + prosent = BigDecimal(100), + søknadstidspunkt = LocalDate.now(), + avtaletidspunktDeltBosted = LocalDate.now(), + ) + assertThrows { + validerDeltBosted( + endretUtbetalingAndel = endretUtbetalingAndel, + deltBostedPerioder = emptyList(), + ) + } + } + + @Test + fun `Skal ikke kaste feil hvis endringsperiode med årsak delt bosted overlapper helt med delt bosted periode`() { + val endretUtbetalingAndel = EndretUtbetalingAndel( + behandlingId = 1, + person = tilfeldigPerson(), + fom = YearMonth.of(2020, 2), + tom = YearMonth.of(2020, 6), + årsak = Årsak.DELT_BOSTED, + begrunnelse = "begrunnelse", + prosent = BigDecimal(100), + søknadstidspunkt = LocalDate.now(), + avtaletidspunktDeltBosted = LocalDate.now(), + ) + assertDoesNotThrow { + validerDeltBosted( + endretUtbetalingAndel = endretUtbetalingAndel, + deltBostedPerioder = listOf( + MånedPeriode( + fom = YearMonth.of(2020, 2), + tom = YearMonth.of(2020, 6), + ), + ), + ) + } + assertDoesNotThrow { + validerDeltBosted( + endretUtbetalingAndel = endretUtbetalingAndel, + deltBostedPerioder = listOf( + MånedPeriode( + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2020, 7), + ), + ), + ) + } + } + + @Test + fun `sjekk at alle endrede utbetalingsandeler validerer`() { + val endretUtbetalingAndel1 = lagEndretUtbetalingAndel(person = tilfeldigPerson()) + val endretUtbetalingAndel2 = lagEndretUtbetalingAndel(person = tilfeldigPerson()) + validerAtAlleOpprettedeEndringerErUtfylt(listOf(endretUtbetalingAndel1, endretUtbetalingAndel2)) + + val feil = assertThrows { + validerAtAlleOpprettedeEndringerErUtfylt( + listOf( + endretUtbetalingAndel1, + endretUtbetalingAndel2.copy(fom = null), + ), + ) + } + assertEquals( + "Det er opprettet instanser av EndretUtbetalingandel som ikke er fylt ut før navigering til neste steg.", + feil.melding, + ) + } + + @Test + fun `sjekk at alle endrede utbetalingsandeler er tilknyttet andeltilkjentytelser`() { + val endretUtbetalingAndel1 = lagEndretUtbetalingAndelMedAndelerTilkjentYtelse(person = tilfeldigPerson()) + val feil = assertThrows { + validerAtEndringerErTilknyttetAndelTilkjentYtelse(listOf(endretUtbetalingAndel1)) + } + assertEquals( + "Det er opprettet instanser av EndretUtbetalingandel som ikke er tilknyttet noen andeler. De må enten lagres eller slettes av SB.", + feil.melding, + ) + + val andelTilkjentYtelse: AndelTilkjentYtelse = mockk() + validerAtEndringerErTilknyttetAndelTilkjentYtelse( + listOf( + lagEndretUtbetalingAndelMedAndelerTilkjentYtelse( + person = tilfeldigPerson(), + andelTilkjentYtelser = mutableListOf(andelTilkjentYtelse), + ), + ), + ) + } + + @Test + fun `Skal finne riktige delt bosted perioder for barn, og slå sammen de som er sammenhengende`() { + val behandling = lagBehandling() + + val fom = LocalDate.now().minusMonths(5) + val tom = LocalDate.now().plusMonths(7) + + val barn = lagPerson(type = PersonType.BARN, fødselsdato = fom) + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + + val personResultatForPerson = + PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn.aktør, + ) + + val vilkårResultaterForPerson = mutableSetOf() + Vilkår.hentVilkårFor( + personType = PersonType.BARN, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ).forEach { + if (it == Vilkår.BOR_MED_SØKER) { + vilkårResultaterForPerson.addAll( + listOf( + VilkårResultat( + personResultat = personResultatForPerson, + periodeFom = fom, + periodeTom = LocalDate.now().minusMonths(1).sisteDagIMåned(), + vilkårType = it, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + VilkårResultat( + personResultat = personResultatForPerson, + periodeFom = LocalDate.now().førsteDagIInneværendeMåned(), + periodeTom = tom, + vilkårType = it, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + ), + ) + } else { + VilkårResultat( + personResultat = personResultatForPerson, + periodeFom = fom, + periodeTom = tom, + vilkårType = it, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ) + } + } + personResultatForPerson.setSortedVilkårResultater(vilkårResultaterForPerson) + + vilkårsvurdering.personResultater = setOf( + personResultatForPerson, + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson(type = PersonType.BARN, fødselsdato = fom.minusYears(4)), + resultat = Resultat.OPPFYLT, + personType = PersonType.BARN, + periodeFom = fom.minusMonths(3), + periodeTom = tom.plusMonths(4), + erDeltBosted = true, + vilkårType = Vilkår.BOR_MED_SØKER, + ), + ) + + val deltBostedPerioder = finnDeltBostedPerioder(person = barn, vilkårsvurdering = vilkårsvurdering) + + assertTrue(deltBostedPerioder.size == 1) + assertEquals(fom.plusMonths(1).førsteDagIInneværendeMåned(), deltBostedPerioder.single().fom) + assertEquals(tom.sisteDagIMåned(), deltBostedPerioder.single().tom) + } + + @Test + fun `Skal finne riktige delt bosted perioder for barn og ikke slå de sammen når de ikke er sammenhengde`() { + val behandling = lagBehandling() + val barn = lagPerson(type = PersonType.BARN) + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + val fom1 = LocalDate.now().minusMonths(5) + val tom1 = LocalDate.now().minusMonths(2) + val fom2 = LocalDate.now() + val tom2 = LocalDate.now().plusMonths(7) + val personResultatForPerson = + PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn.aktør, + ) + + val vilkårResultaterForPerson = mutableSetOf() + Vilkår.hentVilkårFor( + personType = PersonType.BARN, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ).forEach { + if (it == Vilkår.BOR_MED_SØKER) { + vilkårResultaterForPerson.addAll( + listOf( + VilkårResultat( + personResultat = personResultatForPerson, + periodeFom = fom1, + periodeTom = tom1, + vilkårType = it, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + VilkårResultat( + personResultat = personResultatForPerson, + periodeFom = fom2, + periodeTom = tom2, + vilkårType = it, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + ), + ) + } else { + VilkårResultat( + personResultat = personResultatForPerson, + periodeFom = fom1, + periodeTom = tom2, + vilkårType = it, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ) + } + } + personResultatForPerson.setSortedVilkårResultater(vilkårResultaterForPerson) + + vilkårsvurdering.personResultater = setOf( + personResultatForPerson, + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson(type = PersonType.BARN, fødselsdato = fom1.minusYears(5)), + resultat = Resultat.OPPFYLT, + personType = PersonType.BARN, + periodeFom = fom1.minusMonths(3), + periodeTom = tom2.plusMonths(4), + erDeltBosted = true, + vilkårType = Vilkår.BOR_MED_SØKER, + lagFullstendigVilkårResultat = true, + ), + ) + + val deltBostedPerioder = finnDeltBostedPerioder(person = barn, vilkårsvurdering = vilkårsvurdering) + + assertTrue(deltBostedPerioder.size == 2) + + val førstePeriode = deltBostedPerioder.get(0) + val andrePeriode = deltBostedPerioder.get(1) + + assertEquals(fom1.plusMonths(1).førsteDagIInneværendeMåned(), førstePeriode.fom) + assertEquals(tom1.sisteDagIMåned(), førstePeriode.tom) + assertEquals(fom2.plusMonths(1).førsteDagIInneværendeMåned(), andrePeriode.fom) + assertEquals(tom2.sisteDagIMåned(), andrePeriode.tom) + } + + @Test + fun `Skal finne riktige delt bosted perioder for søker, og slå sammen de som er sammenhengende`() { + val behandling = lagBehandling() + val barn = lagPerson(type = PersonType.BARN) + val søker = lagPerson(type = PersonType.SØKER) + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + val fomBarn1 = LocalDate.now().minusMonths(5) + val tomBarn1 = LocalDate.now().plusMonths(7) + val fomBarn2 = fomBarn1.minusMonths(5) + val personResultatForPerson = + PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn.aktør, + ) + + val vilkårResultaterForPerson = mutableSetOf() + Vilkår.hentVilkårFor( + personType = PersonType.BARN, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ).forEach { + if (it == Vilkår.BOR_MED_SØKER) { + vilkårResultaterForPerson.addAll( + listOf( + VilkårResultat( + personResultat = personResultatForPerson, + periodeFom = fomBarn1, + periodeTom = LocalDate.now().minusMonths(1).sisteDagIMåned(), + vilkårType = it, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + VilkårResultat( + personResultat = personResultatForPerson, + periodeFom = LocalDate.now().førsteDagIInneværendeMåned(), + periodeTom = tomBarn1, + vilkårType = it, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + ), + ) + } else { + VilkårResultat( + personResultat = personResultatForPerson, + periodeFom = fomBarn1, + periodeTom = tomBarn1, + vilkårType = it, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ) + } + } + personResultatForPerson.setSortedVilkårResultater(vilkårResultaterForPerson) + + vilkårsvurdering.personResultater = setOf( + personResultatForPerson, + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson(type = PersonType.BARN, fødselsdato = fomBarn2), + resultat = Resultat.OPPFYLT, + personType = PersonType.BARN, + periodeFom = fomBarn2, + periodeTom = fomBarn1, + erDeltBosted = true, + vilkårType = Vilkår.BOR_MED_SØKER, + lagFullstendigVilkårResultat = true, + ), + ) + + val deltBostedPerioder = finnDeltBostedPerioder(person = søker, vilkårsvurdering = vilkårsvurdering) + + assertTrue(deltBostedPerioder.size == 1) + assertEquals(fomBarn2.plusMonths(1).førsteDagIInneværendeMåned(), deltBostedPerioder.single().fom) + assertEquals(tomBarn1.sisteDagIMåned(), deltBostedPerioder.single().tom) + } + + @Test + fun `Skal finne riktige delt bosted perioder for søker, og slå sammen de som overlapper`() { + val behandling = lagBehandling() + val barn = lagPerson(type = PersonType.BARN) + val søker = lagPerson(type = PersonType.SØKER) + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + val fomBarn1 = LocalDate.now().minusMonths(5) + val tomBarn1 = LocalDate.now().plusMonths(7) + val fomBarn2 = fomBarn1.minusMonths(5) + val personResultatForPerson = + PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn.aktør, + ) + + val vilkårResultaterForPerson = mutableSetOf() + Vilkår.hentVilkårFor( + personType = PersonType.BARN, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ).forEach { + if (it == Vilkår.BOR_MED_SØKER) { + vilkårResultaterForPerson.addAll( + listOf( + VilkårResultat( + personResultat = personResultatForPerson, + periodeFom = fomBarn1, + periodeTom = LocalDate.now().minusMonths(1).sisteDagIMåned(), + vilkårType = it, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + VilkårResultat( + personResultat = personResultatForPerson, + periodeFom = LocalDate.now().førsteDagIInneværendeMåned(), + periodeTom = tomBarn1, + vilkårType = it, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + ), + ) + } else { + VilkårResultat( + personResultat = personResultatForPerson, + periodeFom = fomBarn1, + periodeTom = tomBarn1, + vilkårType = it, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ) + } + } + personResultatForPerson.setSortedVilkårResultater(vilkårResultaterForPerson) + + vilkårsvurdering.personResultater = setOf( + personResultatForPerson, + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson(type = PersonType.BARN, fødselsdato = fomBarn2), + resultat = Resultat.OPPFYLT, + personType = PersonType.BARN, + periodeFom = fomBarn2, + periodeTom = tomBarn1, + erDeltBosted = true, + vilkårType = Vilkår.BOR_MED_SØKER, + lagFullstendigVilkårResultat = true, + ), + ) + + val deltBostedPerioder = finnDeltBostedPerioder(person = søker, vilkårsvurdering = vilkårsvurdering) + + assertTrue(deltBostedPerioder.size == 1) + assertEquals(fomBarn2.plusMonths(1).førsteDagIInneværendeMåned(), deltBostedPerioder.single().fom) + assertEquals(tomBarn1.sisteDagIMåned(), deltBostedPerioder.single().tom) + } + + @Test + fun `Skal returnere tom liste hvis det ikke finnes noen delt bosted perioder på person`() { + val behandling = lagBehandling() + val barn1 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(5)) + val barn2 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(4)) + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + + vilkårsvurdering.personResultater = setOf( + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = barn1, + periodeFom = LocalDate.now().minusMonths(7), + periodeTom = LocalDate.now(), + resultat = Resultat.OPPFYLT, + ), + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = barn2, + periodeFom = LocalDate.now().minusMonths(4), + periodeTom = LocalDate.now(), + resultat = Resultat.OPPFYLT, + ), + ) + + val deltBostedPerioder = finnDeltBostedPerioder(person = barn1, vilkårsvurdering = vilkårsvurdering) + + assertTrue(deltBostedPerioder.isEmpty()) + } + + @Test + fun `skal ikke feile dersom de er en utvidet endring og delt bosted endring med samme periode og prosent`() { + validerAtDetFinnesDeltBostedEndringerMedSammeProsentForUtvidedeEndringer( + listOf(endretUtbetalingAndelUtvidetNullutbetaling, endretUtbetalingAndelDeltBostedNullutbetaling), + ) + Assertions.assertDoesNotThrow { + validerAtDetFinnesDeltBostedEndringerMedSammeProsentForUtvidedeEndringer( + listOf(endretUtbetalingAndelUtvidetNullutbetaling, endretUtbetalingAndelDeltBostedNullutbetaling), + ) + } + } + + @Test + fun `skal kaste feil dersom det er en endring på utvidet ytelse uten en endring på delt bosted i samme periode`() { + Assertions.assertThrows(FunksjonellFeil::class.java) { + validerAtDetFinnesDeltBostedEndringerMedSammeProsentForUtvidedeEndringer( + listOf(endretUtbetalingAndelUtvidetNullutbetaling), + ) + } + } + + private fun endretUtbetalingAndel( + person: Person, + ytelsestype: YtelseType, + prosent: BigDecimal, + fomUtvidet: YearMonth = inneværendeMåned().minusMonths(1), + tomUtvidet: YearMonth = inneværendeMåned().minusMonths(1), + ): EndretUtbetalingAndelMedAndelerTilkjentYtelse { + return lagEndretUtbetalingAndelMedAndelerTilkjentYtelse( + id = Random.nextLong(), + fom = fomUtvidet, + tom = tomUtvidet, + person = person, + årsak = Årsak.DELT_BOSTED, + prosent = prosent, + andelTilkjentYtelser = mutableListOf( + lagAndelTilkjentYtelse( + fom = fomUtvidet, + tom = tomUtvidet, + ytelseType = ytelsestype, + ), + ), + ) + } + + @Test + fun `Skal kaste feil hvis endringsårsak=allerede utbetalt og tom-dato er i fremtiden selv om tom er samme som gyldigTomIFremtiden`() { + assertThrows { + validerTomDato( + tomDato = YearMonth.now().plusMonths(3), + årsak = Årsak.ALLEREDE_UTBETALT, + gyldigTomEtterDagensDato = YearMonth.now().plusMonths(3), + ) + } + } + + @Test + fun `Skal ikke kaste feil hvis tom-dato er i fremtiden, men lik gyldig dato i fremtiden`() { + val tom = YearMonth.now().plusMonths(4) + assertDoesNotThrow { validerTomDato(tomDato = tom, gyldigTomEtterDagensDato = tom, årsak = Årsak.DELT_BOSTED) } + } + + @Test + fun `Skal kaste feil hvis tom-dato er i fremtiden, men ikke lik gyldig dato i fremtiden`() { + assertThrows { + validerTomDato( + tomDato = YearMonth.now().plusMonths(6), + gyldigTomEtterDagensDato = YearMonth.now().plusMonths(9), + årsak = Årsak.ENDRE_MOTTAKER, + ) + } + } + + @Test + fun `Skal kaste feil hvis perioden skal utbetales, men årsak er 'endre mottaker' eller 'allerede utbetalt'`() { + assertThrows { + validerUtbetalingMotÅrsak( + årsak = Årsak.ALLEREDE_UTBETALT, + skalUtbetales = true, + ) + } + assertThrows { validerUtbetalingMotÅrsak(årsak = Årsak.ENDRE_MOTTAKER, skalUtbetales = true) } + } + + @Test + fun `Skal ikke kaste feil hvis perioden skal utbetales, men årsak er 'delt bosted'`() { + assertDoesNotThrow { validerUtbetalingMotÅrsak(årsak = Årsak.DELT_BOSTED, skalUtbetales = true) } + } + + @Test + fun `Skal kaste feil dersom endringsårsak er 'Allerede utbetalt' men tom dato er satt til etter inneværende måned`() { + val innværendeMåned = YearMonth.of(2022, 5) + mockkStatic(YearMonth::class) + every { YearMonth.now() } returns innværendeMåned + + val endretUtbetalingAndel = EndretUtbetalingAndel( + behandlingId = 1, + person = tilfeldigPerson(), + fom = YearMonth.of(2022, 2), + tom = YearMonth.of(2022, 6), + årsak = Årsak.ALLEREDE_UTBETALT, + begrunnelse = "begrunnelse", + prosent = BigDecimal(100), + søknadstidspunkt = LocalDate.now(), + avtaletidspunktDeltBosted = LocalDate.now(), + ) + + val feilmelding = assertThrows { + validerÅrsak( + endretUtbetalingAndel = endretUtbetalingAndel, + vilkårsvurdering = mockk(), + ) + }.frontendFeilmelding + + assertEquals( + "Du har valgt årsaken allerede utbetalt. Du kan ikke velge denne årsaken og en til og med dato frem i tid. Ta kontakt med superbruker om du er usikker på hva du skal gjøre.", + feilmelding, + ) + } + + @Test + fun `Skal kaste ikke feil dersom endringsårsak er 'Allerede utbetalt' og tom dato er satt til å være lik eller før inneværende måned`() { + val innværendeMåned = YearMonth.of(2022, 6) + + mockkStatic(YearMonth::class) + every { YearMonth.now() } returns innværendeMåned + + val endretUtbetalingAndel = EndretUtbetalingAndel( + behandlingId = 1, + person = tilfeldigPerson(), + fom = YearMonth.of(2022, 2), + tom = YearMonth.of(2022, 6), + årsak = Årsak.ALLEREDE_UTBETALT, + begrunnelse = "begrunnelse", + prosent = BigDecimal(100), + søknadstidspunkt = LocalDate.now(), + avtaletidspunktDeltBosted = LocalDate.now(), + ) + + assertDoesNotThrow { + validerÅrsak( + endretUtbetalingAndel = endretUtbetalingAndel, + vilkårsvurdering = mockk(), + ) + } + } + + @Test + fun `Skal kaste feil dersom endringsårsak er 'Endre mottaker' og fom dato er satt til å være før inneværende måned`() { + val innværendeMåned = YearMonth.of(2022, 3) + + mockkStatic(YearMonth::class) + every { YearMonth.now() } returns innværendeMåned + + val endretUtbetalingAndel = EndretUtbetalingAndel( + behandlingId = 1, + person = tilfeldigPerson(), + fom = YearMonth.of(2022, 2), + tom = YearMonth.of(2022, 6), + årsak = Årsak.ENDRE_MOTTAKER, + begrunnelse = "begrunnelse", + prosent = BigDecimal(100), + søknadstidspunkt = LocalDate.now(), + avtaletidspunktDeltBosted = LocalDate.now(), + ) + + val feilmelding = assertThrows { + validerÅrsak( + endretUtbetalingAndel = endretUtbetalingAndel, + vilkårsvurdering = mockk(), + ) + }.frontendFeilmelding + + assertEquals( + "Du har valgt årsaken Foreldre bor sammen, endre mottaker. Du kan ikke velge denne årsaken og en fra og med dato tilbake i tid. Ta kontakt med superbruker om du er usikker på hva du skal gjøre.", + feilmelding, + ) + } + + @Test + fun `Skal kaste ikke feil dersom endringsårsak er 'Endre mottaker' og fom dato er satt til å være lik eller etter inneværende måned`() { + val innværendeMåned = YearMonth.of(2022, 2) + + mockkStatic(YearMonth::class) + every { YearMonth.now() } returns innværendeMåned + + val endretUtbetalingAndel = EndretUtbetalingAndel( + behandlingId = 1, + person = tilfeldigPerson(), + fom = YearMonth.of(2022, 2), + tom = YearMonth.of(2022, 6), + årsak = Årsak.ENDRE_MOTTAKER, + begrunnelse = "begrunnelse", + prosent = BigDecimal(100), + søknadstidspunkt = LocalDate.now(), + avtaletidspunktDeltBosted = LocalDate.now(), + ) + + assertDoesNotThrow { + validerÅrsak( + endretUtbetalingAndel = endretUtbetalingAndel, + vilkårsvurdering = mockk(), + ) + } + } + + @Test + fun `Skal kaste feil dersom endringsårsak er 'Endre mottaker' og tom dato er satt til å være lik eller før inneværende måned`() { + val innværendeMåned = YearMonth.of(2022, 2) + + mockkStatic(YearMonth::class) + every { YearMonth.now() } returns innværendeMåned + + val endretUtbetalingAndel = EndretUtbetalingAndel( + behandlingId = 1, + person = tilfeldigPerson(), + fom = YearMonth.of(2022, 2), + tom = YearMonth.of(2022, 2), + årsak = Årsak.ENDRE_MOTTAKER, + begrunnelse = "begrunnelse", + prosent = BigDecimal(100), + søknadstidspunkt = LocalDate.now(), + avtaletidspunktDeltBosted = LocalDate.now(), + ) + + val feilmelding = assertThrows { + validerÅrsak( + endretUtbetalingAndel = endretUtbetalingAndel, + vilkårsvurdering = mockk(), + ) + }.frontendFeilmelding + + assertEquals( + "Du har valgt årsaken Foreldre bor sammen, endre mottaker. Du kan ikke velge denne årsaken og en til og med dato tilbake i tid. Ta kontakt med superbruker om du er usikker på hva du skal gjøre.", + feilmelding, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/domene/EndretUtbetalingAndelTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/domene/EndretUtbetalingAndelTest.kt new file mode 100644 index 000000000..280378ae2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/endretutbetaling/domene/EndretUtbetalingAndelTest.kt @@ -0,0 +1,152 @@ +package no.nav.familie.ba.sak.kjerne.endretutbetaling.domene + +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndel +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.endretutbetaling.beregnGyldigTomIFremtiden +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +internal class EndretUtbetalingAndelTest { + + @Test + fun `Sjekk validering med tomme felt`() { + val behandling = lagBehandling() + val endretUtbetalingAndel = EndretUtbetalingAndel(behandlingId = behandling.id) + endretUtbetalingAndel.begrunnelse = "" + + assertThrows { + endretUtbetalingAndel.validerUtfyltEndring() + } + } + + @Test + fun `Sjekk validering for delt bosted med tomt felt avtaletidpunkt`() { + val behandling = lagBehandling() + val endretUtbetalingAndel = EndretUtbetalingAndel(behandlingId = behandling.id) + + endretUtbetalingAndel.person = tilfeldigPerson() + endretUtbetalingAndel.prosent = BigDecimal(0) + endretUtbetalingAndel.fom = YearMonth.of(2020, 10) + endretUtbetalingAndel.tom = YearMonth.of(2020, 10) + endretUtbetalingAndel.årsak = Årsak.DELT_BOSTED + endretUtbetalingAndel.søknadstidspunkt = LocalDate.now() + endretUtbetalingAndel.begrunnelse = "begrunnelse" + + assertThrows { + endretUtbetalingAndel.validerUtfyltEndring() + } + } + + @Test + fun `Sjekk validering for delt bosted med ikke tomt felt avtaletidpunkt`() { + val behandling = lagBehandling() + val endretUtbetalingAndel = EndretUtbetalingAndel(behandlingId = behandling.id) + + endretUtbetalingAndel.person = tilfeldigPerson() + endretUtbetalingAndel.prosent = BigDecimal(0) + endretUtbetalingAndel.fom = YearMonth.of(2020, 10) + endretUtbetalingAndel.tom = YearMonth.of(2020, 10) + endretUtbetalingAndel.årsak = Årsak.DELT_BOSTED + endretUtbetalingAndel.søknadstidspunkt = LocalDate.now() + endretUtbetalingAndel.avtaletidspunktDeltBosted = LocalDate.now() + endretUtbetalingAndel.begrunnelse = "begrunnelse" + + assertTrue(endretUtbetalingAndel.validerUtfyltEndring()) + } + + @Test + fun `Skal sette tom til siste måned med andel tilkjent ytelse hvis tom er null og det ikke finnes noen andre endringsperioder`() { + val behandling = lagBehandling() + val barn = lagPerson(type = PersonType.BARN) + val endretUtbetalingAndel = lagEndretUtbetalingAndel( + behandlingId = behandling.id, + person = barn, + fom = YearMonth.now(), + tom = null, + årsak = Årsak.DELT_BOSTED, + ) + + val sisteTomPåAndeler = YearMonth.now().plusMonths(10) + val andelTilkjentYtelser = listOf( + lagAndelTilkjentYtelse( + person = barn, + fom = YearMonth.now().minusYears(2), + tom = YearMonth.now().minusMonths(5), + ), + lagAndelTilkjentYtelse( + person = barn, + fom = YearMonth.now().minusMonths(4), + tom = YearMonth.now().plusMonths(4), + ), + lagAndelTilkjentYtelse( + person = barn, + fom = YearMonth.now().plusMonths(5), + tom = sisteTomPåAndeler, + ), + ) + + val nyTom = beregnGyldigTomIFremtiden( + andelTilkjentYtelser = andelTilkjentYtelser, + endretUtbetalingAndel = endretUtbetalingAndel, + andreEndredeAndelerPåBehandling = emptyList(), + ) + + assertEquals(sisteTomPåAndeler, nyTom) + } + + @Test + fun `Skal sette tom til måneden før neste endringsperiode`() { + val behandling = lagBehandling() + val barn = lagPerson(type = PersonType.BARN) + val endretUtbetalingAndel = lagEndretUtbetalingAndel( + behandlingId = behandling.id, + person = barn, + fom = YearMonth.now(), + tom = null, + årsak = Årsak.DELT_BOSTED, + ) + + val annenEndretAndel = lagEndretUtbetalingAndel( + behandlingId = behandling.id, + person = barn, + fom = YearMonth.now().plusMonths(5), + tom = YearMonth.now().plusMonths(8), + årsak = Årsak.DELT_BOSTED, + ) + + val andelTilkjentYtelser = listOf( + lagAndelTilkjentYtelse( + person = barn, + fom = YearMonth.now().minusYears(2), + tom = YearMonth.now().minusMonths(5), + ), + lagAndelTilkjentYtelse( + person = barn, + fom = YearMonth.now().minusMonths(4), + tom = YearMonth.now().plusMonths(4), + ), + lagAndelTilkjentYtelse( + person = barn, + fom = YearMonth.now().plusMonths(5), + tom = YearMonth.now().plusMonths(10), + ), + ) + + val nyTom = beregnGyldigTomIFremtiden( + andelTilkjentYtelser = andelTilkjentYtelser, + endretUtbetalingAndel = endretUtbetalingAndel, + andreEndredeAndelerPåBehandling = listOf(annenEndretAndel), + ) + + assertEquals(annenEndretAndel.fom!!.minusMonths(1), nyTom) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/DifferanseberegningS\303\270kersYtelserTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/DifferanseberegningS\303\270kersYtelserTest.kt" new file mode 100644 index 000000000..a4c850f6b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/DifferanseberegningS\303\270kersYtelserTest.kt" @@ -0,0 +1,378 @@ +package no.nav.familie.ba.sak.kjerne.eøs.differanseberegning + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.util.TilkjentYtelseBuilder +import no.nav.familie.ba.sak.kjerne.eøs.util.barn +import no.nav.familie.ba.sak.kjerne.eøs.util.født +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.util.KompetanseBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.des +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jul +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class DifferanseberegningSøkersYtelserTest { + + @Test + fun `skal håndtere tre barn og utvidet barnetrygd og småbarnstillegg, der alle barna har underskudd i differanseberegning`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = barn født 13.des(2016) + val barn2 = barn født 15.des(2017) + val barn3 = barn født 9.des(2018) + val barna = listOf(barn1, barn2, barn3) + val behandling = lagBehandling() + + val kompetanser = KompetanseBuilder(jan(2017)) + // |1 stk <3 år|2 stk <3 år|3 stk <3 år|2 stk <3 år|1 stk <3 år| + // |01-17 |01-18 |01-19 |01-20 |01-21 |01-22 + .medKompetanse("PPPPPPSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSPPPSSS", barn1) + .medKompetanse(" SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSPPPPPPSSSSSSSSSSSSSSSSSSSSSSSS>", barn2) + .medKompetanse(" SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS>", barn3) + .byggKompetanser() + + val tilkjentYtelse = TilkjentYtelseBuilder(jan(2017), behandling) + .forPersoner(søker) + // |01-17 |01-18 |01-19 |01-20 |01-21 |01-22 + .medUtvidet("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") { 1000 } + .medSmåbarn("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") { 1000 } + .forPersoner(barn1) + .medOrdinær("$$$$$$") { 1000 } + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", 100, { 1000 }, { -700 }) { 0 } + .medOrdinær(" $$$") { 1000 } + .medOrdinær(" $$$", 100, { 1000 }, { -700 }) { 0 } + .forPersoner(barn2) + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", 100, { 1000 }, { -700 }) { 0 } + .medOrdinær(" $$$$$$") { 1000 } + .medOrdinær(" $$$>", 100, { 1000 }, { -700 }) { 0 } + .forPersoner(barn3) + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>", 100, { 1000 }, { -700 }) { 0 } + .bygg() + + val nyeAndeler = + tilkjentYtelse.andelerTilkjentYtelse.differanseberegnSøkersYtelser(barna, kompetanser) + + val forventet = TilkjentYtelseBuilder(jan(2017), behandling) + .forPersoner(søker) + // |01-17 |01-18 |01-19 |01-20 |01-21 |01-22 + .medUtvidet("$$$$$$") { 1000 } + .medUtvidet(" $$$$$$", { 1000 }, { 300 }) { 300 } + .medUtvidet(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", { 1000 }, { 0 }) { 0 } + .medUtvidet(" $$$$$$") { 1000 } + .medUtvidet(" $$$", { 1000 }, { 0 }) { 0 } + .medUtvidet(" $$$") { 1000 } + .medUtvidet(" $$$$$$$$$$", { 1000 }, { 0 }) { 0 } + .medSmåbarn("$$$$$$$$$$$$") { 1000 } + .medSmåbarn(" $$$$$$$$$$$$", { 1000 }, { 600 }) { 600 } + .medSmåbarn(" $$$$$$$$$$$$", { 1000 }, { 0 }) { 0 } + .medSmåbarn(" $$$$$$", { 1000 }, { 267 }) { 267 } + .medSmåbarn(" $$$$$$") { 1000 } + .medSmåbarn(" $$$", { 1000 }, { 633 }) { 633 } + .medSmåbarn(" $$$", { 1000 }, { 300 }) { 300 } + .medSmåbarn(" $$$", { 1000 }, { 633 }) { 633 } + .medSmåbarn(" $$$", { 1000 }, { 800 }) { 800 } + .forPersoner(barn1) + .medOrdinær("$$$$$$") { 1000 } + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", 100, { 1000 }, { -700 }) { 0 } + .medOrdinær(" $$$") { 1000 } + .medOrdinær(" $$$", 100, { 1000 }, { -700 }) { 0 } + .forPersoner(barn2) + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", 100, { 1000 }, { -700 }) { 0 } + .medOrdinær(" $$$$$$") { 1000 } + .medOrdinær(" $$$>", 100, { 1000 }, { -700 }) { 0 } + .forPersoner(barn3) + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>", 100, { 1000 }, { -700 }) { 0 } + .bygg() + + assertEquals(forventet.andelerTilkjentYtelse.sortert(), nyeAndeler.sortert()) + } + + @Test + fun `differanseberegnet ordinær barnetrygd uten at søker har ytelser, skal gi uendrete andeler for barna`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = barn født 13.des(2016) + val barn2 = barn født 15.des(2017) + val barna = listOf(barn1, barn2) + val behandling = lagBehandling() + + val kompetanser = KompetanseBuilder(jan(2017)) + // |1 stk <3 år|2 stk <3 år|3 stk <3 år|2 stk <3 år|1 stk <3 år| + // |01-17 |01-18 |01-19 |01-20 |01-21 |01-22 + .medKompetanse("PPPPPPSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSPPPSSS", barn1) + .medKompetanse(" SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSPPPPPPSSSSSSSSSSSSSSSSSSSSSSSS>", barn2) + .byggKompetanser() + + val tilkjentYtelse = TilkjentYtelseBuilder(jan(2017), behandling) + .forPersoner(søker) + // |01-17 |01-18 |01-19 |01-20 |01-21 |01-22 + .forPersoner(barn1) + .medOrdinær("$$$$$$") { 1000 } + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", 100, { 1000 }, { -700 }) { 0 } + .medOrdinær(" $$$") { 1000 } + .medOrdinær(" $$$", 100, { 1000 }, { -700 }) { 0 } + .forPersoner(barn2) + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", 100, { 1000 }, { -700 }) { 0 } + .medOrdinær(" $$$$$$") { 1000 } + .medOrdinær(" $$$>", 100, { 1000 }, { -700 }) { 0 } + .bygg() + + val nyeAndeler = + tilkjentYtelse.andelerTilkjentYtelse.differanseberegnSøkersYtelser(barna, kompetanser) + + assertEquals(tilkjentYtelse.andelerTilkjentYtelse.sortert(), nyeAndeler.sortert()) + } + + @Test + fun `Ingen differranseberegning skal gi uendrete andeler`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = barn født 13.des(2016) + val barn2 = barn født 15.des(2017) + val barna = listOf(barn1, barn2) + val behandling = lagBehandling() + + val kompetanser = emptyList() + + val tilkjentYtelse = TilkjentYtelseBuilder(jan(2017), behandling) + .forPersoner(søker) + // |01-17 |01-18 |01-19 |01-20 |01-21 |01-22 + .medUtvidet("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") { 1000 } + .medSmåbarn("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") { 1000 } + .forPersoner(barn1) + .medOrdinær("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") { 1000 } + .forPersoner(barn2) + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>") { 1000 } + .bygg() + + val nyeAndeler = + tilkjentYtelse.andelerTilkjentYtelse.differanseberegnSøkersYtelser(barna, kompetanser) + + assertEquals(tilkjentYtelse.andelerTilkjentYtelse.sortert(), nyeAndeler.sortert()) + } + + @Test + fun `Tom tilkjent ytelse og ingen barn skal ikke gi feil`() { + val tilkjentYtelse = lagInitiellTilkjentYtelse() + + val nyeAndeler = tilkjentYtelse.andelerTilkjentYtelse + .differanseberegnSøkersYtelser(emptyList(), emptyList()) + + assertEquals(emptyList(), nyeAndeler) + } + + @Test + fun `Søkers andel som har hatt differanseberegning, men ikke skal ha det lenger, skal fjerne differanseberegningen og slå sammen perioder som med sikkerhet kan slås sammen`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = barn født 13.des(2016) + val barn2 = barn født 15.des(2017) + val barn3 = barn født 9.des(2018) + val barna = listOf(barn1, barn2, barn3) + val behandling = lagBehandling() + + val kompetanser = emptyList() + + val tilkjentYtelse = TilkjentYtelseBuilder(jan(2017), behandling) + .forPersoner(søker) + // |01-17 |01-18 |01-19 |01-20 |01-21 |01-22 + .medUtvidet("$$$$$$") { 1000 } + .medUtvidet(" $$$$$$", { 1000 }, { 300 }) { 300 } + .medUtvidet(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", { 1000 }, { 0 }) { 0 } + .medUtvidet(" $$$$$$") { 1000 } + .medUtvidet(" $$$", { 1000 }, { 0 }) { 0 } + .medUtvidet(" $$$") { 1000 } + .medUtvidet(" $$$$$$$$$$", { 1000 }, { 0 }) { 0 } + .medSmåbarn("$$$$$$$$$$$$") { 1000 } + .medSmåbarn(" $$$$$$$$$$$$", { 1000 }, { 600 }) { 600 } + .medSmåbarn(" $$$$$$$$$$$$", { 1000 }, { 0 }) { 0 } + .medSmåbarn(" $$$$$$", { 1000 }, { 267 }) { 267 } + .medSmåbarn(" $$$$$$") { 1000 } + .medSmåbarn(" $$$", { 1000 }, { 633 }) { 633 } + .medSmåbarn(" $$$") { 1000 } + .medSmåbarn(" $$$", { 1000 }, { 633 }) { 633 } + .medSmåbarn(" $$$", { 1000 }, { 800 }) { 800 } + .forPersoner(barn1) + .medOrdinær("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") { 1000 } + .forPersoner(barn2) + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>") { 1000 } + .forPersoner(barn3) + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>") { 1000 } + .bygg() + + val nyeAndeler = + tilkjentYtelse.andelerTilkjentYtelse.differanseberegnSøkersYtelser(barna, kompetanser) + + // Dette er litt trist. Men selv om andelene er identiske, kan de ikke slås sammen fordi + // de er til forveksling like som andeler som har en funksjonell årsak til å være splittet + // Påfølgende andeler som begge har differanseberegning, KAN slås sammen + val forventet = TilkjentYtelseBuilder(jan(2017), behandling) + .forPersoner(søker) + // |01-17 |01-18 |01-19 |01-20 |01-21 |01-22 + .medUtvidet("$$$$$$") { 1000 } + .medUtvidet(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") { 1000 } + .medUtvidet(" $$$$$$") { 1000 } + .medUtvidet(" $$$") { 1000 } + .medUtvidet(" $$$") { 1000 } + .medUtvidet(" $$$$$$$$$$") { 1000 } + .medSmåbarn("$$$$$$$$$$$$") { 1000 } + .medSmåbarn(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") { 1000 } + .medSmåbarn(" $$$$$$") { 1000 } + .medSmåbarn(" $$$") { 1000 } + .medSmåbarn(" $$$") { 1000 } + .medSmåbarn(" $$$$$$") { 1000 } + .forPersoner(barn1) + .medOrdinær("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") { 1000 } + .forPersoner(barn2) + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>") { 1000 } + .forPersoner(barn3) + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>") { 1000 } + .bygg() + + assertEquals(forventet.andelerTilkjentYtelse.sortert(), nyeAndeler.sortert()) + } + + @Test + fun `Søkers andel som har differanseberegning, men underskuddet reduseres, skal få oppdatert differanseberegningen`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = barn født 13.des(2016) + val barna = listOf(barn1) + val behandling = lagBehandling() + + val kompetanser = KompetanseBuilder(jan(2017)) + .medKompetanse("S>", barn1) + .byggKompetanser() + + val tilkjentYtelse = TilkjentYtelseBuilder(jan(2017), behandling) + .forPersoner(søker) + .medUtvidet("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", { 1000 }, { 0 }) { 0 } + .medSmåbarn("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", { 1000 }, { 300 }) { 300 } + .forPersoner(barn1) + .medOrdinær("$>", 100, { 1000 }, { -650 }) { 0 } + .bygg() + + val nyeAndeler = + tilkjentYtelse.andelerTilkjentYtelse.differanseberegnSøkersYtelser(barna, kompetanser) + + val forventet = TilkjentYtelseBuilder(jan(2017), behandling) + .forPersoner(søker) + .medUtvidet("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", { 1000 }, { 350 }) { 350 } + .medSmåbarn("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") { 1000 } + .forPersoner(barn1) + .medOrdinær("$>", 100, { 1000 }, { -650 }) { 0 } + .bygg() + + assertEquals(forventet.andelerTilkjentYtelse.sortert(), nyeAndeler.sortert()) + } + + @Test + fun `Skal tåle perioder der underskuddet på differanseberegning er større enn alle tilkjente ytelser`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = barn født 13.des(2016) + val barna = listOf(barn1) + val behandling = lagBehandling() + + val kompetanser = KompetanseBuilder(jan(2017)) + .medKompetanse("S>", barn1) + .byggKompetanser() + + val tilkjentYtelse = TilkjentYtelseBuilder(jan(2017), behandling) + .forPersoner(søker) + .medUtvidet("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") { 1000 } + .medSmåbarn("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$") { 1000 } + .forPersoner(barn1) + .medOrdinær("$>", 100, { 1000 }, { -2650 }) { 0 } + .bygg() + + val nyeAndeler = + tilkjentYtelse.andelerTilkjentYtelse.differanseberegnSøkersYtelser(barna, kompetanser) + + val forventet = TilkjentYtelseBuilder(jan(2017), behandling) + .forPersoner(søker) + .medUtvidet("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", { 1000 }, { 0 }) { 0 } + .medSmåbarn("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$", { 1000 }, { 0 }) { 0 } + .forPersoner(barn1) + .medOrdinær("$>", 100, { 1000 }, { -2650 }) { 0 } + .bygg() + + assertEquals(forventet.andelerTilkjentYtelse.sortert(), nyeAndeler.sortert()) + } + + @Test + fun `skal illustrere avrundingssproblematikk, der søker tjener`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = barn født 13.des(2016) + val barn2 = barn født 15.des(2017) + val barn3 = barn født 9.des(2018) + val barna = listOf(barn1, barn2, barn3) + val behandling = lagBehandling() + + val kompetanser = KompetanseBuilder(jul(2020)) + .medKompetanse("SSSSSS", barn1, barn2, barn3) + .byggKompetanser() + + val tilkjentYtelse = TilkjentYtelseBuilder(jul(2020), behandling) + .forPersoner(søker) + .medUtvidet("$$$$$$") { 1054 } + .forPersoner(barn1) + .medOrdinær("$$$$$$", 100, { 1054 }, { -400 }) { 0 } + .forPersoner(barn2, barn3) + .medOrdinær("$$$$$$", 100, { 1054 }, { 554 }) { 554 } + .bygg() + + val nyeAndeler = + tilkjentYtelse.andelerTilkjentYtelse.differanseberegnSøkersYtelser(barna, kompetanser) + + val forventet = TilkjentYtelseBuilder(jul(2020), behandling) + .forPersoner(søker) + .medUtvidet("$$$$$$", { 1054 }, { 703 }) { 703 } // Egentlig 702,67 + .forPersoner(barn1) + .medOrdinær("$$$$$$", 100, { 1054 }, { -400 }) { 0 } + .forPersoner(barn2, barn3) + .medOrdinær("$$$$$$", 100, { 1054 }, { 554 }) { 554 } + .bygg() + + assertEquals(forventet.andelerTilkjentYtelse.sortert(), nyeAndeler.sortert()) + } + + @Test + fun `skal illustrere avrundingssproblematikk, der søker taper`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = barn født 13.des(2016) + val barn2 = barn født 15.des(2017) + val barn3 = barn født 9.des(2018) + val barna = listOf(barn1, barn2, barn3) + val behandling = lagBehandling() + + val kompetanser = KompetanseBuilder(jul(2020)) + .medKompetanse("SSSSSS", barn1, barn2, barn3) + .byggKompetanser() + + val tilkjentYtelse = TilkjentYtelseBuilder(jul(2020), behandling) + .forPersoner(søker) + .medUtvidet("$$$$$$") { 1054 } + .forPersoner(barn1, barn2) + .medOrdinær("$$$$$$", 100, { 1054 }, { -400 }) { 0 } + .forPersoner(barn3) + .medOrdinær("$$$$$$", 100, { 1054 }, { 554 }) { 554 } + .bygg() + + val nyeAndeler = + tilkjentYtelse.andelerTilkjentYtelse.differanseberegnSøkersYtelser(barna, kompetanser) + + val forventet = TilkjentYtelseBuilder(jul(2020), behandling) + .forPersoner(søker) + .medUtvidet("$$$$$$", { 1054 }, { 351 }) { 351 } // Egentlig 351,33 + .forPersoner(barn1, barn2) + .medOrdinær("$$$$$$", 100, { 1054 }, { -400 }) { 0 } + .forPersoner(barn3) + .medOrdinær("$$$$$$", 100, { 1054 }, { 554 }) { 554 } + .bygg() + + assertEquals(forventet.andelerTilkjentYtelse.sortert(), nyeAndeler.sortert()) + } +} + +private fun Collection.sortert() = + this.sortedWith(compareBy({ it.aktør.aktørId }, { it.type }, { it.stønadFom })) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/DifferanseberegningsUtilsTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/DifferanseberegningsUtilsTest.kt" new file mode 100644 index 000000000..9f5cdb255 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/DifferanseberegningsUtilsTest.kt" @@ -0,0 +1,156 @@ +package no.nav.familie.ba.sak.kjerne.eøs.differanseberegning + +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Intervall +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Intervall.KVARTALSVIS +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Intervall.MÅNEDLIG +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Intervall.UKENTLIG +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Intervall.ÅRLIG +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.KronerPerValutaenhet +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Valutabeløp +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.tilMånedligValutabeløp +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.times +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløp +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.math.MathContext +import java.math.RoundingMode +import java.time.YearMonth + +class DifferanseberegningsUtilsTest { + val utbetalingsbeløpNorge = 2000 + + @Test + fun `Skal multiplisere valutabeløp med valutakurs`() { + val valutabeløp = 1200.i("EUR") + val kurs = 9.731.kronerPer("EUR") + + Assertions.assertEquals(11_677.toBigDecimal(), (valutabeløp * kurs)?.round(MathContext(5))) + } + + @Test + fun `Skal ikke multiplisere valutabeløp med valutakurs når valuta er forskjellig, men returnere null`() { + val valutabeløp = 1200.i("EUR") + val kurs = 9.73.kronerPer("DKK") + + Assertions.assertNull(valutabeløp * kurs) + } + + @Test + fun `Skal konvertere årlig utenlandsk periodebeløp til månedlig`() { + val månedligValutabeløp = 1200.i("EUR").somUtenlandskPeriodebeløp(ÅRLIG) + .tilMånedligValutabeløp() + + Assertions.assertEquals(100.i("EUR"), månedligValutabeløp) + } + + @Test + fun `Skal konvertere kvartalsvis utenlandsk periodebeløp til månedlig`() { + val månedligValutabeløp = 300.i("EUR").somUtenlandskPeriodebeløp(KVARTALSVIS) + .tilMånedligValutabeløp() + + Assertions.assertEquals(100.i("EUR"), månedligValutabeløp) + } + + @Test + fun `Månedlig utenlandsk periodebeløp skal ikke endres`() { + val månedligValutabeløp = 100.i("EUR").somUtenlandskPeriodebeløp(MÅNEDLIG) + .tilMånedligValutabeløp() + + Assertions.assertEquals(100.i("EUR"), månedligValutabeløp) + } + + @Test + fun `Skal konvertere ukentlig utenlandsk periodebeløp til månedlig`() { + val månedligValutabeløp = 25.i("EUR").somUtenlandskPeriodebeløp(UKENTLIG) + .tilMånedligValutabeløp() + + Assertions.assertEquals(108.75.i("EUR"), månedligValutabeløp) + } + + @Test + fun `Skal ha presisjon i kronekonverteringen til norske kroner`() { + val månedligValutabeløp = 0.0123767453453.i("EUR").somUtenlandskPeriodebeløp(ÅRLIG) + .tilMånedligValutabeløp() + + Assertions.assertEquals(0.0010313954.i("EUR"), månedligValutabeløp) + } + + @Test + fun `Skal håndtere gjentakende endring og differanseberegning på andel tilkjent ytelse`() { + val aty1 = lagAndelTilkjentYtelse(beløp = 50).oppdaterDifferanseberegning( + 100.toBigDecimal(), + ) + + Assertions.assertEquals(0, aty1?.kalkulertUtbetalingsbeløp) + Assertions.assertEquals(-50, aty1?.differanseberegnetPeriodebeløp) + Assertions.assertEquals(50, aty1?.nasjonaltPeriodebeløp) + + val aty2 = aty1?.copy(nasjonaltPeriodebeløp = 1).oppdaterDifferanseberegning( + 75.toBigDecimal(), + ) + + Assertions.assertEquals(0, aty2?.kalkulertUtbetalingsbeløp) + Assertions.assertEquals(-74, aty2?.differanseberegnetPeriodebeløp) + Assertions.assertEquals(1, aty2?.nasjonaltPeriodebeløp) + + val aty3 = aty2?.copy(nasjonaltPeriodebeløp = 250).oppdaterDifferanseberegning( + 75.toBigDecimal(), + ) + + Assertions.assertEquals(175, aty3?.kalkulertUtbetalingsbeløp) + Assertions.assertEquals(175, aty3?.differanseberegnetPeriodebeløp) + Assertions.assertEquals(250, aty3?.nasjonaltPeriodebeløp) + } + + @Test + fun `Skal fjerne desimaler i utenlandskperiodebeløp, effektivt øke den norske ytelsen med inntil én krone`() { + val aty1 = lagAndelTilkjentYtelse(beløp = 50).oppdaterDifferanseberegning( + 100.987654.toBigDecimal(), + ) // Blir til rundet til 100 + + Assertions.assertEquals(0, aty1?.kalkulertUtbetalingsbeløp) + Assertions.assertEquals(-50, aty1?.differanseberegnetPeriodebeløp) + Assertions.assertEquals(50, aty1?.nasjonaltPeriodebeløp) + } + + @Test + fun `Skal beholde originalt nasjonaltPeriodebeløp når vi oppdatererDifferanseberegning gjentatte ganger`() { + var aty1 = lagAndelTilkjentYtelse(beløp = 50).oppdaterDifferanseberegning( + 100.987654.toBigDecimal(), + ) + + Assertions.assertEquals(0, aty1?.kalkulertUtbetalingsbeløp) + aty1 = aty1.oppdaterDifferanseberegning(13.6.toBigDecimal()) + Assertions.assertEquals(37, aty1?.kalkulertUtbetalingsbeløp) + aty1 = aty1.oppdaterDifferanseberegning(49.2.toBigDecimal()) + Assertions.assertEquals(1, aty1?.kalkulertUtbetalingsbeløp) + } +} + +fun lagAndelTilkjentYtelse(beløp: Int) = lagAndelTilkjentYtelse( + fom = YearMonth.now(), + tom = YearMonth.now().plusYears(1), + beløp = beløp, +) + +fun Double.kronerPer(valuta: String) = KronerPerValutaenhet( + valutakode = valuta, + kronerPerValutaenhet = this.toBigDecimal(), +) + +fun Double.i(valuta: String) = Valutabeløp(this.toBigDecimal(), valuta) +fun Int.i(valuta: String) = Valutabeløp(this.toBigDecimal(), valuta) + +fun Valutabeløp.somUtenlandskPeriodebeløp(intervall: Intervall): UtenlandskPeriodebeløp = + UtenlandskPeriodebeløp( + fom = null, + tom = null, + beløp = this.beløp, + valutakode = this.valutakode, + intervall = intervall, + kalkulertMånedligBeløp = intervall.konverterBeløpTilMånedlig(this.beløp), + ) + +fun Valutabeløp.rundNed(presisjon: Int) = + Valutabeløp(this.beløp.round(MathContext(presisjon, RoundingMode.DOWN)), this.valutakode) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/TilkjentYtelseDifferanseberegningTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/TilkjentYtelseDifferanseberegningTest.kt" new file mode 100644 index 000000000..8229dd944 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/TilkjentYtelseDifferanseberegningTest.kt" @@ -0,0 +1,184 @@ +package no.nav.familie.ba.sak.kjerne.eøs.differanseberegning + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.eøs.assertEqualsUnordered +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.util.DeltBostedBuilder +import no.nav.familie.ba.sak.kjerne.eøs.util.TilkjentYtelseBuilder +import no.nav.familie.ba.sak.kjerne.eøs.util.UtenlandskPeriodebeløpBuilder +import no.nav.familie.ba.sak.kjerne.eøs.util.ValutakursBuilder +import no.nav.familie.ba.sak.kjerne.eøs.util.oppdaterTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilInneværendeMåned +import no.nav.familie.ba.sak.kjerne.tidslinje.util.VilkårsvurderingBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.byggTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.BOR_MED_SØKER +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.BOSATT_I_RIKET +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.GIFT_PARTNERSKAP +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.LOVLIG_OPPHOLD +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.UNDER_18_ÅR +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +/** + * Merk at operasjoner som tilsynelatende lager en ny instans av TilkjentYtelse, faktisk returner samme. + * Det skyldes at JPA krever muterbare objekter. + * Ikke-muterbarhet krever en omskrivning av koden. F.eks å koble vekk EndretUtbetalingPeriode fra AndelTilkjentYtelse + */ +class TilkjentYtelseDifferanseberegningTest { + + @Test + fun `skal gjøre differanseberegning på en tilkjent ytelse med endringsperioder`() { + val barnsFødselsdato = 13.jan(2020) + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = barnsFødselsdato.tilLocalDate()) + val barn2 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = barnsFødselsdato.tilLocalDate()) + + val behandling = lagBehandling() + val behandlingId = BehandlingId(behandling.id) + val startMåned = barnsFødselsdato.tilInneværendeMåned() + + val vilkårsvurderingBygger = VilkårsvurderingBuilder(behandling) + .forPerson(søker, startMåned) + .medVilkår("EEEEEEEEEEEEEEEEEEEEEEE", BOSATT_I_RIKET) + .medVilkår("EEEEEEEEEEEEEEEEEEEEEEE", LOVLIG_OPPHOLD) + .forPerson(barn1, startMåned) + .medVilkår("+>", UNDER_18_ÅR, GIFT_PARTNERSKAP) + .medVilkår("E>", BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER) + .forPerson(barn2, startMåned) + .medVilkår("+>", UNDER_18_ÅR, GIFT_PARTNERSKAP) + .medVilkår("E>", BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER) + .byggPerson() + + val tilkjentYtelse = vilkårsvurderingBygger.byggTilkjentYtelse() + + assertEquals(6, tilkjentYtelse.andelerTilkjentYtelse.size) + + DeltBostedBuilder(startMåned, tilkjentYtelse) + .medDeltBosted(" //////000000000011111>", barn1, barn2) + .oppdaterTilkjentYtelse() + + val forventetTilkjentYtelseMedDelt = TilkjentYtelseBuilder(startMåned, behandling) + .forPersoner(barn1, barn2) + .medOrdinær(" $$$$$$", prosent = 50) { it / 2 } + .medOrdinær(" $$$$$$$$$$", prosent = 0) { 0 } + .medOrdinær(" $$$$$$", prosent = 100) { it } + .bygg() + + assertEquals(8, tilkjentYtelse.andelerTilkjentYtelse.size) + assertEqualsUnordered( + forventetTilkjentYtelseMedDelt.andelerTilkjentYtelse, + tilkjentYtelse.andelerTilkjentYtelse, + ) + + val utenlandskePeriodebeløp = UtenlandskPeriodebeløpBuilder(startMåned, behandlingId) + .medBeløp(" 44555666>", "EUR", "fr", barn1, barn2) + .bygg() + + val valutakurser = ValutakursBuilder(startMåned, behandlingId) + .medKurs(" 888899999>", "EUR", barn1, barn2) + .bygg() + + val forventetTilkjentYtelseMedDiff = TilkjentYtelseBuilder(startMåned, behandling) + .forPersoner(barn1, barn2) + .medOrdinær(" $$", 50, nasjonalt = { it / 2 }, differanse = { it / 2 - 32 }) { it / 2 - 32 } + .medOrdinær(" $$", 50, nasjonalt = { it / 2 }, differanse = { it / 2 - 40 }) { it / 2 - 40 } + .medOrdinær(" $", 50, nasjonalt = { it / 2 }, differanse = { it / 2 - 45 }) { it / 2 - 45 } + .medOrdinær(" $", 50, nasjonalt = { it / 2 }, differanse = { it / 2 - 54 }) { it / 2 - 54 } + .medOrdinær(" $$$$$$$$$$", 0, nasjonalt = { 0 }, differanse = { -54 }) { 0 } + .medOrdinær(" $$$$$$", 100, nasjonalt = { it }, differanse = { it - 54 }) { it - 54 } + .bygg() + + val andelerMedDifferanse = + beregnDifferanse(tilkjentYtelse.andelerTilkjentYtelse, utenlandskePeriodebeløp, valutakurser) + + assertEquals(14, andelerMedDifferanse.size) + assertEqualsUnordered( + forventetTilkjentYtelseMedDiff.andelerTilkjentYtelse, + andelerMedDifferanse, + ) + } + + @Test + fun `skal fjerne differanseberegning når utenlandsk periodebeløp eller valutakurs nullstilles`() { + val barnsFødselsdato = 13.jan(2020) + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = barnsFødselsdato.tilLocalDate()) + + val behandling = lagBehandling() + val behandlingId = BehandlingId(behandling.id) + val startMåned = barnsFødselsdato.tilInneværendeMåned() + + val vilkårsvurderingBygger = VilkårsvurderingBuilder(behandling) + .forPerson(søker, startMåned) + .medVilkår("EEEEEEEEEEEEEEEEEEEEEEE", BOSATT_I_RIKET) + .medVilkår("EEEEEEEEEEEEEEEEEEEEEEE", LOVLIG_OPPHOLD) + .forPerson(barn1, startMåned) + .medVilkår("+>", UNDER_18_ÅR, GIFT_PARTNERSKAP) + .medVilkår("E>", BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER) + .byggPerson() + + val tilkjentYtelse = vilkårsvurderingBygger.byggTilkjentYtelse() + + val forventetTilkjentYtelseKunSats = TilkjentYtelseBuilder(startMåned, behandling) + .forPersoner(barn1) + .medOrdinær(" $$$$$$$$$$$$$$$$$$$$$$", nasjonalt = { null }, differanse = { null }) + .bygg() + + assertEquals(3, tilkjentYtelse.andelerTilkjentYtelse.size) + assertEqualsUnordered( + forventetTilkjentYtelseKunSats.andelerTilkjentYtelse, + tilkjentYtelse.andelerTilkjentYtelse, + ) + + val utenlandskePeriodebeløp = UtenlandskPeriodebeløpBuilder(startMåned, behandlingId) + .medBeløp(" 44555666>", "EUR", "fr", barn1) + .bygg() + + val valutakurser = ValutakursBuilder(startMåned, behandlingId) + .medKurs(" 888899999>", "EUR", barn1) + .bygg() + + val forventetTilkjentYtelseMedDiff = TilkjentYtelseBuilder(startMåned, behandling) + .forPersoner(barn1) + .medOrdinær(" $$ ", nasjonalt = { it }, differanse = { it - 32 }) { it - 32 } + .medOrdinær(" $$ ", nasjonalt = { it }, differanse = { it - 40 }) { it - 40 } + .medOrdinær(" $ ", nasjonalt = { it }, differanse = { it - 45 }) { it - 45 } + .medOrdinær(" $$$$$$$$$$$$$$$$$", nasjonalt = { it }, differanse = { it - 54 }) { it - 54 } + .bygg() + + val andelerMedDiff = + beregnDifferanse(tilkjentYtelse.andelerTilkjentYtelse, utenlandskePeriodebeløp, valutakurser) + + assertEquals(6, andelerMedDiff.size) + assertEqualsUnordered( + forventetTilkjentYtelseMedDiff.andelerTilkjentYtelse, + andelerMedDiff, + ) + + val blanktUtenlandskPeridebeløp = UtenlandskPeriodebeløpBuilder(startMåned, behandlingId) + .medBeløp(" >", null, null, barn1) + .bygg() + + val andelerUtenDiff = + beregnDifferanse(tilkjentYtelse.andelerTilkjentYtelse, blanktUtenlandskPeridebeløp, valutakurser) + + assertEquals(3, andelerUtenDiff.size) + assertEqualsUnordered( + forventetTilkjentYtelseKunSats.andelerTilkjentYtelse, + andelerUtenDiff, + ) + + val andelerMedDiffIgjen = + beregnDifferanse(tilkjentYtelse.andelerTilkjentYtelse, utenlandskePeriodebeløp, valutakurser) + + assertEquals(6, andelerMedDiffIgjen.size) + assertEqualsUnordered( + forventetTilkjentYtelseMedDiff.andelerTilkjentYtelse, + andelerMedDiffIgjen, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/TilkjentYtelseRepositoryOppdaterTilkjentYtelseTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/TilkjentYtelseRepositoryOppdaterTilkjentYtelseTest.kt" new file mode 100644 index 000000000..6b4299dbc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/TilkjentYtelseRepositoryOppdaterTilkjentYtelseTest.kt" @@ -0,0 +1,90 @@ +package no.nav.familie.ba.sak.kjerne.eøs.differanseberegning + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.oppdaterTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.eøs.util.TilkjentYtelseBuilder +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilInneværendeMåned +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class TilkjentYtelseRepositoryOppdaterTilkjentYtelseTest { + + val barnsFødselsdato = 13.jan(2020) + val startMåned = barnsFødselsdato.tilInneværendeMåned() + + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = barnsFødselsdato.tilLocalDate()) + + val tilkjentYtelseRepository: TilkjentYtelseRepository = mockk(relaxed = true) + + @Test + fun `skal kaste exception hvis tilkjent ytelse oppdateres med overlappende andel tilkjent ytelse for et barn`() { + val behandling = lagBehandling() + + val forrigeTilkjentYtelse = TilkjentYtelseBuilder(startMåned, behandling).bygg() + + val nyTilkjentYtelse = TilkjentYtelseBuilder(startMåned, behandling) + .forPersoner(barn1) + .medOrdinær(" $$$$$$") + .medOrdinær(" $$$$$") + .bygg() + + assertThrows { + tilkjentYtelseRepository.oppdaterTilkjentYtelse( + forrigeTilkjentYtelse, + nyTilkjentYtelse.andelerTilkjentYtelse, + ) + } + } + + @Test + fun `skal kaste exception hvis tilkjent ytelse oppdateres med overlappende andel tilkjent ytelse for søker`() { + val behandling = lagBehandling() + + val forrigeTilkjentYtelse = TilkjentYtelseBuilder(startMåned, behandling).bygg() + + val nyTilkjentYtelse = TilkjentYtelseBuilder(startMåned, behandling) + .forPersoner(søker) + .medUtvidet(" $$$$$$") + .medUtvidet(" $$$$$") + .bygg() + + assertThrows { + tilkjentYtelseRepository.oppdaterTilkjentYtelse( + forrigeTilkjentYtelse, + nyTilkjentYtelse.andelerTilkjentYtelse, + ) + } + } + + @Test + fun `skal ikke kaste exception hvis tilkjent ytelse oppdateres med gyldige andeler`() { + val behandling = lagBehandling() + + val forrigeTilkjentYtelse = TilkjentYtelseBuilder(startMåned, behandling) + .bygg() + + val nyTilkjentYtelse = TilkjentYtelseBuilder(startMåned, behandling) + .forPersoner(søker) + .medUtvidet(" $$$$$$$$$$") + .forPersoner(barn1) + .medOrdinær("$$$$$$$$$") + .bygg() + + every { tilkjentYtelseRepository.saveAndFlush(any()) } returns nyTilkjentYtelse + + tilkjentYtelseRepository.oppdaterTilkjentYtelse( + forrigeTilkjentYtelse, + nyTilkjentYtelse.andelerTilkjentYtelse, + ) + + verify(exactly = 1) { tilkjentYtelseRepository.saveAndFlush(any()) } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/OppdaterSkjemaTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/OppdaterSkjemaTest.kt" new file mode 100644 index 000000000..3110fe1e1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/OppdaterSkjemaTest.kt" @@ -0,0 +1,79 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles.beregning + +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.eøs.assertEqualsUnordered +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.kompetanse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.util.KompetanseBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import org.junit.jupiter.api.Test + +internal class OppdaterSkjemaTest { + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + val barn2 = tilfeldigPerson(personType = PersonType.BARN) + val barn3 = tilfeldigPerson(personType = PersonType.BARN) + + val kompetanser = KompetanseBuilder(jan(2020)) + .medKompetanse(" SSSSSPP", barn1) + .medKompetanse(" ---------", barn2, barn3) + .medKompetanse(" PPPP", barn1, barn2, barn3) + .byggKompetanser() + + @Test + fun `oppdatere med tom kompetanse skal ikke har noen effekt`() { + val tomKompetanse = Kompetanse(null, null) + + val faktiskeKompetanser = oppdaterSkjemaerRekursivt(kompetanser, tomKompetanse) + assertEqualsUnordered(kompetanser, faktiskeKompetanser) + } + + @Test + fun `oppdatere tom liste av kompetansr med en gyldig kompetanse skal gi tom liste`() { + val kompetanse = kompetanse(jan(2020), "------", barn1, barn2, barn3) + + val faktiskeKompetanser = oppdaterSkjemaerRekursivt(emptyList(), kompetanse) + assertEqualsUnordered(emptyList(), faktiskeKompetanser) + } + + @Test + fun `oppdatere utenfor gjeldende kompetanser skal ikke ha effekt`() { + val kompetanse = kompetanse( + jan(2019), + "---SSS PPP------", + barn1, + barn2, + barn3, + ) + + val faktiskeKompetanser = oppdaterSkjemaerRekursivt(kompetanser, kompetanse) + assertEqualsUnordered(kompetanser, faktiskeKompetanser) + } + + @Test + fun `oppdatere mer enn gjeldende kompetanser skal bare påvirke eksisterende tidsperioder`() { + val kompetanse = kompetanse(jan(2020), "PPPPPPPPPPPPPPPPPPPPPP", barn1, barn2, barn3) + + val forventedeKompetanser = KompetanseBuilder(jan(2020)) + .medKompetanse(" PPPPPPPPPPP", barn1, barn2, barn3) + .medKompetanse(" PP", barn2, barn3) + .byggKompetanser() + + val faktiskeKompetanser = oppdaterSkjemaerRekursivt(kompetanser, kompetanse) + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun `oppdatere kompetanser som begynner uendret, skal likevel bli endret`() { + val kompetanse = kompetanse(jan(2020), " SSSSSSSSS", barn1) + + val forventedeKompetanser = KompetanseBuilder(jan(2020)) + .medKompetanse(" SSSSSSSSS", barn1) + .medKompetanse(" ---------PP", barn2, barn3) + .medKompetanse(" PP", barn1, barn2, barn3) + .byggKompetanser() + + val faktiskeKompetanser = oppdaterSkjemaerRekursivt(kompetanser, kompetanse) + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/SkjemaTidslinjeTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/SkjemaTidslinjeTest.kt" new file mode 100644 index 000000000..05d5ada94 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/SkjemaTidslinjeTest.kt" @@ -0,0 +1,53 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles.beregning + +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.fraOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.tilOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jul +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.YearMonth + +internal class SkjemaTidslinjeTest { + + @Test + fun `skal håndtere to påfølgende perioder i fremtiden, men de komprimeres ikke`() { + val barn = lagPerson(type = PersonType.BARN) + val kompetanse1 = Kompetanse( + fom = YearMonth.of(2437, 2), + tom = YearMonth.of(2438, 6), + barnAktører = setOf(barn.aktør), + ) + val kompetanse2 = Kompetanse( + fom = YearMonth.of(2438, 7), + tom = null, + barnAktører = setOf(barn.aktør), + ) + + val kompetanseTidslinje = listOf(kompetanse1, kompetanse2).tilTidslinje() + assertEquals(2, kompetanseTidslinje.perioder().size) + assertEquals(feb(2437), kompetanseTidslinje.fraOgMed()) + assertEquals(jul(2438).somUendeligLengeTil(), kompetanseTidslinje.tilOgMed()) + } + + @Test + fun `skal håndtere kompetanse som mangler både fom og tom`() { + val barn = lagPerson(type = PersonType.BARN) + val kompetanse = Kompetanse( + fom = null, + tom = null, + barnAktører = setOf(barn.aktør), + ) + + val kompetanseTidslinje = kompetanse.tilTidslinje() + assertEquals(1, kompetanseTidslinje.perioder().size) + assertEquals(MånedTidspunkt.nå().somUendeligLengeSiden(), kompetanseTidslinje.fraOgMed()) + assertEquals(MånedTidspunkt.nå().somUendeligLengeTil(), kompetanseTidslinje.tilOgMed()) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/Sl\303\245SammenSkjemaTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/Sl\303\245SammenSkjemaTest.kt" new file mode 100644 index 000000000..a00399e49 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/Sl\303\245SammenSkjemaTest.kt" @@ -0,0 +1,134 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles.beregning + +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.eøs.assertEqualsUnordered +import no.nav.familie.ba.sak.kjerne.tidslinje.util.KompetanseBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class SlåSammenSkjemaTest { + val jan2020 = jan(2020) + val barn1 = tilfeldigPerson() + val barn2 = tilfeldigPerson() + val barn3 = tilfeldigPerson() + + @Test + fun testSlåSammenPåfølgendePerioder() { + val kompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SSS", barn1) + .medKompetanse(" SSS", barn1) + .byggKompetanser() + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SSSSSS", barn1) + .byggKompetanser() + + val faktiskeKompetanser = kompetanser.slåSammen() + Assertions.assertEquals(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun testSlåSammenForPerioderMedMellomrom() { + val kompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SSS", barn1, barn2, barn3) + .medKompetanse(" SSS", barn1, barn2, barn3) + .byggKompetanser() + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SSS SSS", barn1, barn2, barn3) + .byggKompetanser() + + val faktiskeKompetanser = kompetanser.slåSammen() + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun testSlåSammenForPerioderDerTidligstePeriodeHarÅpemTOM() { + val kompetanser = KompetanseBuilder(jan2020) + .medKompetanse("->", barn1, barn2, barn3) + .medKompetanse(" -----", barn1, barn2, barn3) + .byggKompetanser() + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse("->", barn1, barn2, barn3) + .byggKompetanser() + + val faktiskeKompetanser = kompetanser.slåSammen() + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + Assertions.assertEquals(null, faktiskeKompetanser.first().tom) + } + + @Test + fun testSlåSammenForPerioderMedOverlapp() { + val kompetanser = KompetanseBuilder(jan2020) + .medKompetanse("-----", barn1, barn2, barn3) + .medKompetanse(" -----", barn1, barn2, barn3) + .byggKompetanser() + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse("--------", barn1, barn2, barn3) + .byggKompetanser() + + val faktiskeKompetanser = kompetanser.slåSammen() + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun testSlåSammneForPerioderDerSenestePeriodeHarÅpemTOM() { + val kompetanser = KompetanseBuilder(jan2020) + .medKompetanse("------", barn1, barn2, barn3) + .medKompetanse(" ----->", barn1, barn2, barn3) + .byggKompetanser() + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse("->", barn1, barn2, barn3) + .byggKompetanser() + + val faktiskeKompetanser = kompetanser.slåSammen() + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + Assertions.assertEquals(jan2020.tilYearMonth(), faktiskeKompetanser.first().fom) + Assertions.assertEquals(null, faktiskeKompetanser.first().tom) + } + + @Test + fun komplekseSlåSammenKommpetanserTest() { + val kompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SSSSSSS", barn1) + .medKompetanse("SSSPPSS", barn2) + .medKompetanse("-SSSSSS", barn3) + .byggKompetanser() + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse(" SS SS", barn1, barn2, barn3) + .medKompetanse("S ", barn1, barn2) + .medKompetanse(" SS ", barn1, barn3) + .medKompetanse(" ", barn2, barn3) + .medKompetanse(" ", barn1) + .medKompetanse(" PP ", barn2) + .medKompetanse("- ", barn3) + .byggKompetanser() + + val faktiskeKompetanser = kompetanser.slåSammen() + Assertions.assertEquals(6, faktiskeKompetanser.size) + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun slåSammenEnkeltBarnSomSkillerSegHeltUt() { + val kompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SSS", barn1) + .medKompetanse("---------", barn2, barn3) + .medKompetanse(" SSSS", barn1) + .byggKompetanser() + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SSSSSSS", barn1) + .medKompetanse("---------", barn2, barn3) + .byggKompetanser() + + val faktiskeKompetanser = kompetanser.slåSammen() + Assertions.assertEquals(2, faktiskeKompetanser.size) + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/TrekkFraSkjemaTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/TrekkFraSkjemaTest.kt" new file mode 100644 index 000000000..acb725055 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/felles/beregning/TrekkFraSkjemaTest.kt" @@ -0,0 +1,57 @@ +package no.nav.familie.ba.sak.kjerne.eøs.felles.beregning + +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.eøs.assertEqualsUnordered +import no.nav.familie.ba.sak.kjerne.tidslinje.util.KompetanseBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class TrekkFraSkjemaTest { + + val jan2020 = jan(2020) + val barn1 = tilfeldigPerson() + val barn2 = tilfeldigPerson() + val barn3 = tilfeldigPerson() + + @Test + fun testRestSomIntrodusererHull() { + val kompetanse = KompetanseBuilder(jan2020) + .medKompetanse("------", barn1, barn2, barn3) + .byggKompetanser().first() + + val oppdatertKompetanse = KompetanseBuilder(jan2020) + .medKompetanse(" SS ", barn1) + .byggKompetanser().first() + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse("------", barn2, barn3) + .medKompetanse("-- --", barn1) + .byggKompetanser() + + val restKompetanser = kompetanse.trekkFra(oppdatertKompetanse) + + Assertions.assertEquals(3, restKompetanser.size) + assertEqualsUnordered(forventedeKompetanser, restKompetanser) + } + + @Test + fun testRestMedPeriodeOverEnEnkeltMåned() { + val kompetanse = KompetanseBuilder(jan2020) + .medKompetanse(" --", barn1, barn2) + .byggKompetanser().first() + + val fjernKompetanse = KompetanseBuilder(jan2020) + .medKompetanse(" S", barn1) + .byggKompetanser().first() + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse(" - ", barn1) + .medKompetanse(" --", barn2) + .byggKompetanser() + + val restKompetanser = kompetanse.trekkFra(fjernKompetanse) + + assertEqualsUnordered(forventedeKompetanser, restKompetanser) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseServiceTest.kt" new file mode 100644 index 000000000..7019aabaf --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseServiceTest.kt" @@ -0,0 +1,480 @@ +package no.nav.familie.ba.sak.kjerne.eøs.kompetanse + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.tilPersonEnkelSøkerOgBarn +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.eøs.assertEqualsUnordered +import no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement.TilpassKompetanserTilRegelverkService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.util.mockPeriodeBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.EndretUtbetalingAndelTidslinjeService +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjeService +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjer +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.neste +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilLocalDate +import no.nav.familie.ba.sak.kjerne.tidslinje.util.KompetanseBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.VilkårsvurderingBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mar +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.unleash.UnleashService +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class KompetanseServiceTest { + + val mockKompetanseRepository: PeriodeOgBarnSkjemaRepository = mockPeriodeBarnSkjemaRepository() + val vilkårsvurderingTidslinjeService: VilkårsvurderingTidslinjeService = mockk() + val endretUtbetalingAndelTidslinjeService: EndretUtbetalingAndelTidslinjeService = mockk() + val unleashService: UnleashService = mockk() + + val kompetanseService = KompetanseService( + mockKompetanseRepository, + emptyList(), + ) + + val tilpassKompetanserTilRegelverkService = TilpassKompetanserTilRegelverkService( + vilkårsvurderingTidslinjeService, + endretUtbetalingAndelTidslinjeService, + unleashService, + mockKompetanseRepository, + emptyList(), + ) + + @BeforeEach + fun init() { + mockKompetanseRepository.deleteAll() + every { unleashService.isEnabled(any()) } returns true + } + + @Test + fun `bare reduksjon av periode skal ikke føre til endring i kompetansen`() { + val behandlingId = BehandlingId(10L) + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + + val lagretKompetanse = kompetanse(jan(2020), behandlingId, "SSSSSSSS", barn1) + .lagreTil(mockKompetanseRepository) + + val oppdatertKompetanse = kompetanse(jan(2020), " SSSSS ", barn1) + kompetanseService.oppdaterKompetanse(behandlingId, oppdatertKompetanse) + + val forventedeKompetanser = listOf(lagretKompetanse) + + assertEqualsUnordered(forventedeKompetanser, kompetanseService.hentKompetanser(behandlingId)) + } + + @Test + fun `oppdatering som splitter kompetanse fulgt av sletting skal returnere til utgangspunktet`() { + val behandlingId = BehandlingId(10L) + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + val barn2 = tilfeldigPerson(personType = PersonType.BARN) + val barn3 = tilfeldigPerson(personType = PersonType.BARN) + + val lagretKompetanse = kompetanse(jan(2020), behandlingId, "---------", barn1, barn2, barn3) + .lagreTil(mockKompetanseRepository) + + val oppdatertKompetanse = kompetanse(jan(2020), " PP", barn2, barn3) + + kompetanseService.oppdaterKompetanse(behandlingId, oppdatertKompetanse) + + val forventedeKompetanser = KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse("--", barn1, barn2, barn3) + .medKompetanse(" --", barn1) + .medKompetanse(" PP", barn2, barn3) + .medKompetanse(" -----", barn1, barn2, barn3) + .byggKompetanser() + + assertEqualsUnordered(forventedeKompetanser, kompetanseService.hentKompetanser(behandlingId)) + + val kompetanseSomSkalSlettes = kompetanseService.finnKompetanse(behandlingId, oppdatertKompetanse) + kompetanseService.slettKompetanse(behandlingId, kompetanseSomSkalSlettes.id) + + assertEqualsUnordered(listOf(lagretKompetanse), kompetanseService.hentKompetanser(behandlingId)) + } + + @Test + fun `oppdatering som endrer deler av en kompetanse, skal resultarere i en splitt`() { + val behandlingId = BehandlingId(10L) + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + val barn2 = tilfeldigPerson(personType = PersonType.BARN) + val barn3 = tilfeldigPerson(personType = PersonType.BARN) + + KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse("SSS", barn1) + .medKompetanse("---------", barn2, barn3) + .medKompetanse(" SSSS", barn1) + .lagreTil(mockKompetanseRepository) + + val oppdatertKompetanse = kompetanse(jan(2020), "PP", barn1) + kompetanseService.oppdaterKompetanse(behandlingId, oppdatertKompetanse) + + val forventedeKompetanser = KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse("PP", barn1) + .medKompetanse(" SSSSS", barn1) + .medKompetanse("---------", barn2, barn3) + .byggKompetanser() + + assertEqualsUnordered(forventedeKompetanser, kompetanseService.hentKompetanser(behandlingId)) + } + + @Test + fun `skal kunne sende inn oppdatering som overlapper flere kompetanser`() { + val behandlingId = BehandlingId(10L) + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + val barn2 = tilfeldigPerson(personType = PersonType.BARN) + val barn3 = tilfeldigPerson(personType = PersonType.BARN) + + KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse("SSS", barn1) + .medKompetanse("---------", barn2, barn3) + .medKompetanse(" SSSS", barn1) + .lagreTil(mockKompetanseRepository) + + val oppdatertKompetanse = kompetanse(mar(2020), "PPP", barn1, barn2, barn3) + kompetanseService.oppdaterKompetanse(behandlingId, oppdatertKompetanse) + + val forventedeKompetanser = KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse("SS SS", barn1) + .medKompetanse(" PPP", barn1, barn2, barn3) + .medKompetanse("-- ----", barn2, barn3) + .byggKompetanser() + + val faktiskeKompetanser = kompetanseService.hentKompetanser(behandlingId) + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun `skal kunne lukke åpen kompetanse ved å sende inn identisk skjema med til-og-med-dato`() { + val behandlingId = BehandlingId(10L) + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + val barn2 = tilfeldigPerson(personType = PersonType.BARN) + val barn3 = tilfeldigPerson(personType = PersonType.BARN) + + // Åpen (til-og-med er null) kompetanse med sekundærland for tre barn + KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse("S>", barn1, barn2, barn3) + .lagreTil(mockKompetanseRepository) + + // Endrer kun til-og-med dato fra uendelig (null) til en gitt dato + val oppdatertKompetanse = kompetanse(jan(2020), "SSS", barn1, barn2, barn3) + kompetanseService.oppdaterKompetanse(behandlingId, oppdatertKompetanse) + + // Forventer tomt skjema fra oppdatert dato og fremover + val forventedeKompetanser = KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse("SSS->", barn1, barn2, barn3) + .byggKompetanser() + + val faktiskeKompetanser = kompetanseService.hentKompetanser(behandlingId) + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun `skal kunne forkorte til-og-med ved å sende inn identisk skjema med tidligere til-og-med-dato`() { + val behandlingId = BehandlingId(10L) + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + val barn2 = tilfeldigPerson(personType = PersonType.BARN) + val barn3 = tilfeldigPerson(personType = PersonType.BARN) + + // Kompetanse med sekundærland for tre barn med til-og-med-dato + KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse("SSSSSSS", barn1, barn2, barn3) + .lagreTil(mockKompetanseRepository) + + // Endrer kun til-og-med dato til tidligere tidspunkt + val oppdatertKompetanse = kompetanse(jan(2020), "SSS", barn1, barn2, barn3) + kompetanseService.oppdaterKompetanse(behandlingId, oppdatertKompetanse) + + // Forventer tomt skjema fra oppdatert dato og fremover til orignal til-og-med + val forventedeKompetanser = KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse("SSS----", barn1, barn2, barn3) + .byggKompetanser() + + val faktiskeKompetanser = kompetanseService.hentKompetanser(behandlingId) + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun `skal opprette tomt skjema for barn som fjernes fra ellers uendret skjema`() { + val behandlingId = BehandlingId(10L) + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + val barn2 = tilfeldigPerson(personType = PersonType.BARN) + val barn3 = tilfeldigPerson(personType = PersonType.BARN) + + // Åpen (til-og-med er null) kompetanse med sekundærland for tre barn + KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse("S>", barn1, barn2, barn3) + .lagreTil(mockKompetanseRepository) + + // Fjerner ett barn fra gjeldende skjema, ellers likt + val oppdatertKompetanse = kompetanse(jan(2020), "S>", barn1, barn2) + kompetanseService.oppdaterKompetanse(behandlingId, oppdatertKompetanse) + + // Forventer tomt skjema for samme periode for barnet som ble fjernet + val forventedeKompetanser = KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse("S>", barn1, barn2) + .medKompetanse("->", barn3) + .byggKompetanser() + + val faktiskeKompetanser = kompetanseService.hentKompetanser(behandlingId) + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun `kompetanse skal vare uendelig når til regelverk-tidslinjer fortsetter etter nåtidspunktet`() { + val behandlingId = BehandlingId(10L) + + val treMånederSiden = MånedTidspunkt.nå().flytt(-3) + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = treMånederSiden.tilLocalDate()) + val barn2 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = treMånederSiden.tilLocalDate()) + + val vilkårsvurderingBygger = VilkårsvurderingBuilder() + .forPerson(søker, treMånederSiden) // Regelverk-tidslinje avslutter ETTER nå-tidspunkt + .medVilkår("EEEEEEEEEEE", Vilkår.BOSATT_I_RIKET) + .medVilkår("EEEEEEEEEEE", Vilkår.LOVLIG_OPPHOLD) + .forPerson(barn1, treMånederSiden) // Regelverk-tidslinje avslutter ETTER nå-tidspunkt + .medVilkår("+++++++++++", Vilkår.UNDER_18_ÅR) + .medVilkår("EEEEEEEEEEE", Vilkår.BOSATT_I_RIKET) + .medVilkår("EEEEEEEEEEE", Vilkår.LOVLIG_OPPHOLD) + .medVilkår("EEEEEEEEEEE", Vilkår.BOR_MED_SØKER) + .medVilkår("+++++++++++", Vilkår.GIFT_PARTNERSKAP) + .forPerson(barn2, treMånederSiden) // Regelverk-tidslinje avslutter ETTER nå-tidspunkt + .medVilkår("+++++++", Vilkår.UNDER_18_ÅR) + .medVilkår("EEEEEEE", Vilkår.BOSATT_I_RIKET) + .medVilkår("EEEEEEE", Vilkår.LOVLIG_OPPHOLD) + .medVilkår("EEEEEEE", Vilkår.BOR_MED_SØKER) + .medVilkår("+++++++", Vilkår.GIFT_PARTNERSKAP) + .byggPerson() + + val forventedeKompetanser = KompetanseBuilder(treMånederSiden.neste(), behandlingId) + .medKompetanse("->", barn1, barn2) + .byggKompetanser() + + val vilkårsvurderingTidslinjer = VilkårsvurderingTidslinjer( + vilkårsvurdering = vilkårsvurderingBygger.byggVilkårsvurdering(), + søkerOgBarn = lagTestPersonopplysningGrunnlag(behandlingId.id, søker, barn1, barn2) + .tilPersonEnkelSøkerOgBarn(), + ) + + every { vilkårsvurderingTidslinjeService.hentTidslinjerThrows(behandlingId) } returns vilkårsvurderingTidslinjer + every { vilkårsvurderingTidslinjeService.hentAnnenForelderOmfattetAvNorskLovgivningTidslinje(behandlingId) } returns TomTidslinje() + every { endretUtbetalingAndelTidslinjeService.hentBarnasSkalIkkeUtbetalesTidslinjer(behandlingId) } returns emptyMap() + + tilpassKompetanserTilRegelverkService.tilpassKompetanserTilRegelverk(behandlingId) + + val faktiskeKompetanser = kompetanseService.hentKompetanser(behandlingId) + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun `kompetanse skal ha sluttdato når til regelverk-tidslinjer avsluttes før nåtidspunktet`() { + val behandlingId = BehandlingId(10L) + + val seksMånederSiden = MånedTidspunkt.nå().flytt(-6) + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = seksMånederSiden.tilLocalDate()) + val barn2 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = seksMånederSiden.tilLocalDate()) + + val vilkårsvurderingBygger = VilkårsvurderingBuilder() + .forPerson(søker, seksMånederSiden) // Regelverk-tidslinje avslutter ETTER nå-tidspunkt + .medVilkår("EEEEEEEEEEE", Vilkår.BOSATT_I_RIKET) + .medVilkår("EEEEEEEEEEE", Vilkår.LOVLIG_OPPHOLD) + .forPerson(barn1, seksMånederSiden) // Regelverk-tidslinje avslutter ETTER nå-tidspunkt + .medVilkår("+++++++++++", Vilkår.UNDER_18_ÅR) + .medVilkår("EEEEEEEEEEE", Vilkår.BOSATT_I_RIKET) + .medVilkår("EEEEEEEEEEE", Vilkår.LOVLIG_OPPHOLD) + .medVilkår("EEEEEEEEEEE", Vilkår.BOR_MED_SØKER) + .medVilkår("+++++++++++", Vilkår.GIFT_PARTNERSKAP) + .forPerson(barn2, seksMånederSiden) // Regelverk-tidslinje avslutter FØR nå-tidspunkt + .medVilkår("+++", Vilkår.UNDER_18_ÅR) + .medVilkår("EEE", Vilkår.BOSATT_I_RIKET) + .medVilkår("EEE", Vilkår.LOVLIG_OPPHOLD) + .medVilkår("EEE", Vilkår.BOR_MED_SØKER) + .medVilkår("+++", Vilkår.GIFT_PARTNERSKAP) + .byggPerson() + + val forventedeKompetanser = KompetanseBuilder(seksMånederSiden.neste(), behandlingId) + .medKompetanse("--", barn1, barn2) // Begge barna har 3 mnd EØS-regelverk før nå-tidspunktet + .medKompetanse(" ->", barn1) // Bare barn 1 har EØS-regelverk etter nå-tidspunktet + .byggKompetanser() + + val vilkårsvurderingTidslinjer = VilkårsvurderingTidslinjer( + vilkårsvurdering = vilkårsvurderingBygger.byggVilkårsvurdering(), + søkerOgBarn = lagTestPersonopplysningGrunnlag(behandlingId.id, søker, barn1, barn2) + .tilPersonEnkelSøkerOgBarn(), + ) + + every { vilkårsvurderingTidslinjeService.hentTidslinjerThrows(behandlingId) } returns vilkårsvurderingTidslinjer + every { endretUtbetalingAndelTidslinjeService.hentBarnasSkalIkkeUtbetalesTidslinjer(behandlingId) } returns emptyMap() + every { vilkårsvurderingTidslinjeService.hentAnnenForelderOmfattetAvNorskLovgivningTidslinje(behandlingId) } returns TomTidslinje() + + tilpassKompetanserTilRegelverkService.tilpassKompetanserTilRegelverk(behandlingId) + + val faktiskeKompetanser = kompetanseService.hentKompetanser(behandlingId) + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun `skal tilpasse kompetanser til endrede regelverk-tidslinjer`() { + val behandlingId = BehandlingId(10L) + + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = jan(2020).tilLocalDate()) + val barn2 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = jan(2020).tilLocalDate()) + val barn3 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = jan(2020).tilLocalDate()) + + KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse("SS SS", barn1) + .medKompetanse(" PPP", barn1, barn2, barn3) + .medKompetanse("-- ----", barn2, barn3) + .lagreTil(mockKompetanseRepository) + + val vilkårsvurderingBygger = VilkårsvurderingBuilder() + .forPerson(søker, jan(2020)) + .medVilkår("EEEEEEEEEEE", Vilkår.BOSATT_I_RIKET, Vilkår.LOVLIG_OPPHOLD) + .forPerson(barn1, jan(2020)) + .medVilkår("+++++++++++", Vilkår.UNDER_18_ÅR, Vilkår.GIFT_PARTNERSKAP) + .medVilkår("EEEEEEEEEEE", Vilkår.BOSATT_I_RIKET, Vilkår.LOVLIG_OPPHOLD, Vilkår.BOR_MED_SØKER) + .forPerson(barn2, jan(2020)) + .medVilkår(" ++++", Vilkår.UNDER_18_ÅR, Vilkår.GIFT_PARTNERSKAP) + .medVilkår(" EEEE", Vilkår.BOSATT_I_RIKET, Vilkår.LOVLIG_OPPHOLD, Vilkår.BOR_MED_SØKER) + .forPerson(barn3, jan(2020)) + .medVilkår("+>", Vilkår.UNDER_18_ÅR, Vilkår.GIFT_PARTNERSKAP) + .medVilkår("N>", Vilkår.BOSATT_I_RIKET, Vilkår.LOVLIG_OPPHOLD, Vilkår.BOR_MED_SØKER) + + val vilkårsvurdering = vilkårsvurderingBygger.byggVilkårsvurdering() + val vilkårsvurderingTidslinjer = VilkårsvurderingTidslinjer( + vilkårsvurdering = vilkårsvurdering, + søkerOgBarn = lagTestPersonopplysningGrunnlag(behandlingId.id, søker, barn1, barn2, barn3) + .tilPersonEnkelSøkerOgBarn(), + ) + + every { vilkårsvurderingTidslinjeService.hentTidslinjerThrows(behandlingId) } returns vilkårsvurderingTidslinjer + every { vilkårsvurderingTidslinjeService.hentAnnenForelderOmfattetAvNorskLovgivningTidslinje(behandlingId) } returns TomTidslinje() + every { endretUtbetalingAndelTidslinjeService.hentBarnasSkalIkkeUtbetalesTidslinjer(behandlingId) } returns emptyMap() + + tilpassKompetanserTilRegelverkService.tilpassKompetanserTilRegelverk(behandlingId) + + val faktiskeKompetanser = kompetanseService.hentKompetanser(behandlingId) + + val forventedeKompetanser = KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse(" SP SS----", barn1) + .medKompetanse(" -", barn2) + .medKompetanse(" PP ", barn1, barn2) + .byggKompetanser() + + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun `skal kopiere over kompetanse-skjema fra forrige behandling til ny behandling`() { + val behandlingId1 = BehandlingId(10L) + val behandlingId2 = BehandlingId(11L) + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + val barn2 = tilfeldigPerson(personType = PersonType.BARN) + val barn3 = tilfeldigPerson(personType = PersonType.BARN) + + val kompetanser = KompetanseBuilder(jan(2020), behandlingId1) + .medKompetanse( + "SSS", + barn1, + annenForeldersAktivitetsland = null, + erAnnenForelderOmfattetAvNorskLovgivning = true, + ) + .medKompetanse( + "---------", + barn2, + barn3, + annenForeldersAktivitetsland = null, + erAnnenForelderOmfattetAvNorskLovgivning = false, + ) + .medKompetanse( + " SSSS", + barn1, + annenForeldersAktivitetsland = null, + erAnnenForelderOmfattetAvNorskLovgivning = true, + ) + .lagreTil(mockKompetanseRepository) + + kompetanseService.kopierOgErstattKompetanser(behandlingId1, behandlingId2) + + val kompetanserBehandling2 = kompetanseService.hentKompetanser(behandlingId2) + + assertEqualsUnordered(kompetanser, kompetanserBehandling2) + + kompetanserBehandling2.forEach { + assertEquals(behandlingId2.id, it.behandlingId) + } + + val kompetanserBehandling1 = kompetanseService.hentKompetanser(behandlingId1) + + kompetanserBehandling1.forEach { + assertEquals(behandlingId1.id, it.behandlingId) + } + + assertEqualsUnordered(kompetanser, kompetanserBehandling1) + } + + @Test + fun `skal kopiere kompetanser fra en behandling til en annen behandling, og overskrive eksisterende`() { + val behandlingId1 = BehandlingId(10L) + val behandlingId2 = BehandlingId(22L) + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + val barn2 = tilfeldigPerson(personType = PersonType.BARN) + val barn3 = tilfeldigPerson(personType = PersonType.BARN) + + val kompetanser1 = KompetanseBuilder(jan(2020), behandlingId1) + .medKompetanse("SS SS", barn1) + .medKompetanse(" PPP", barn1, barn2, barn3) + .medKompetanse("-- ----", barn2, barn3) + .lagreTil(mockKompetanseRepository) + + KompetanseBuilder(jan(2020), behandlingId2) + .medKompetanse("PPPSSSPPPPPPP", barn1, barn2, barn3) + .lagreTil(mockKompetanseRepository) + + kompetanseService.kopierOgErstattKompetanser(behandlingId1, behandlingId2) + + val kompetanserBehandling2EtterEndring = kompetanseService.hentKompetanser(behandlingId2) + + assertEqualsUnordered(kompetanser1, kompetanserBehandling2EtterEndring) + + kompetanserBehandling2EtterEndring.forEach { + assertEquals(behandlingId2.id, it.behandlingId) + } + + val kompetanserBehandling1EtterEndring = kompetanseService.hentKompetanser(behandlingId1) + + assertEqualsUnordered(kompetanser1, kompetanserBehandling1EtterEndring) + + kompetanserBehandling1EtterEndring.forEach { + assertEquals(behandlingId1.id, it.behandlingId) + } + } +} + +fun kompetanse(tidspunkt: Tidspunkt, behandlingId: BehandlingId, s: String, vararg barn: Person) = + KompetanseBuilder(tidspunkt, behandlingId).medKompetanse(s, *barn).byggKompetanser().first() + +fun kompetanse(tidspunkt: Tidspunkt, s: String, vararg barn: Person) = + KompetanseBuilder(tidspunkt).medKompetanse(s, *barn).byggKompetanser().first() + +private fun KompetanseService.finnKompetanse(behandlingId: BehandlingId, kompetanse: Kompetanse): Kompetanse { + return this.hentKompetanser(behandlingId) + .first { it == kompetanse } +} + +fun Kompetanse.lagreTil(kompetanseRepository: PeriodeOgBarnSkjemaRepository): Kompetanse { + return kompetanseRepository.saveAll(listOf(this)).first() +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/TilpassKompetanserTilRegelverkTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/TilpassKompetanserTilRegelverkTest.kt" new file mode 100644 index 000000000..09d5a3a76 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/TilpassKompetanserTilRegelverkTest.kt" @@ -0,0 +1,259 @@ +package no.nav.familie.ba.sak.kjerne.eøs.kompetanse + +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.eøs.assertEqualsUnordered +import no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement.tilpassKompetanserTilRegelverk +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.KombinertRegelverkResultat +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.util.KompetanseBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.somBoolskTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.util.tilAnnenForelderOmfattetAvNorskLovgivningTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.util.tilRegelverkResultatTidslinje +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class TilpassKompetanserTilRegelverkTest { + val jan2020 = jan(2020) + val barn1 = tilfeldigPerson() + val barn2 = tilfeldigPerson() + val barn3 = tilfeldigPerson() + + @Test + fun testTilpassKompetanserUtenKompetanser() { + val kompetanser: List = emptyList() + + val eøsPerioder = mapOf( + barn1.aktør to "EEENNEEEE".tilRegelverkResultatTidslinje(jan2020).kombinertSøkersResultatTidslinje(), + ) + val annenForelderOmfattetTidslinje = + "++++-----++++++".tilAnnenForelderOmfattetAvNorskLovgivningTidslinje(jan2020) + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse( + "--- ", + barn1, + annenForeldersAktivitetsland = null, + erAnnenForelderOmfattetAvNorskLovgivning = true, + ) + .medKompetanse( + " ----", + barn1, + annenForeldersAktivitetsland = null, + erAnnenForelderOmfattetAvNorskLovgivning = false, + ) + .byggKompetanser() + + val faktiskeKompetanser = + tilpassKompetanserTilRegelverk(kompetanser, eøsPerioder, emptyMap(), annenForelderOmfattetTidslinje) + + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun testTilpassKompetanserUtenEøsPerioder() { + val kompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SSSSSSS", barn1) + .byggKompetanser() + + val eøsPerioder = emptyMap>() + + val forventedeKompetanser = emptyList() + + val faktiskeKompetanser = tilpassKompetanserTilRegelverk(kompetanser, eøsPerioder, emptyMap()) + + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun testTilpassKompetanserMotEøsEttBarn() { + val kompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SSSSSSS", barn1) + .byggKompetanser() + + val barnasRegelverkResultatTidslinjer = mapOf( + barn1.aktør to "EEENNEEEE".tilRegelverkResultatTidslinje(jan2020).kombinertSøkersResultatTidslinje(), + ) + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SSS SS--", barn1) + .byggKompetanser() + + val faktiskeKompetanser = tilpassKompetanserTilRegelverk( + kompetanser, + barnasRegelverkResultatTidslinjer, + emptyMap(), + ) + + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun testTilpassKompetanserMotEøsToBarn() { + val kompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SS--SSSS", barn1, barn2) + .byggKompetanser() + + val barnasEgneRegelverkResultatTidslinjer = mapOf( + barn1.aktør to "EEENNEEEE".tilRegelverkResultatTidslinje(jan2020), + barn2.aktør to "EEEENNEEE".tilRegelverkResultatTidslinje(jan2020), + ) + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SS- SS-", barn1, barn2) + .medKompetanse(" S", barn1) + .medKompetanse(" - ", barn2) + .byggKompetanser().sortedBy { it.fom } + + val faktiskeKompetanser = tilpassKompetanserTilRegelverk( + kompetanser, + barnasEgneRegelverkResultatTidslinjer.mapValues { it.value.kombinertSøkersResultatTidslinje() }, + emptyMap(), + ).sortedBy { it.fom } + + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun testTilpassKompetanserMotEøsForFlereBarn() { + // "SSSSSSS", barn1 + // "SSSPPSS", barn2 + // "-SSSSSS", barn3 + + val kompetanser = KompetanseBuilder(jan2020) + .medKompetanse(" SS SS", barn1, barn2, barn3) + .medKompetanse("S ", barn1, barn2) + .medKompetanse(" SS ", barn1, barn3) + .medKompetanse(" PP ", barn2) + .medKompetanse("- ", barn3) + .byggKompetanser() + + val barnasEgneRegelverkResultatTidslinjer = mapOf( + barn1.aktør to "EEENNEEEE".tilRegelverkResultatTidslinje(jan2020), + barn2.aktør to "EEE--NNNN".tilRegelverkResultatTidslinje(jan2020), + barn3.aktør to "EEEEEEEEE".tilRegelverkResultatTidslinje(jan2020), + ) + + // SSS SS--, barn1 + // SSS , barn2 + // -SSSSSS--, barn3 + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse(" SS ", barn1, barn2, barn3) + .medKompetanse("S ", barn1, barn2) + .medKompetanse(" SS--", barn1, barn3) + .medKompetanse("- SS ", barn3) + .byggKompetanser().sortedBy { it.fom } + + val faktiskeKompetanser = tilpassKompetanserTilRegelverk( + kompetanser, + barnasEgneRegelverkResultatTidslinjer.mapValues { it.value.kombinertSøkersResultatTidslinje() }, + emptyMap(), + ).sortedBy { it.fom } + + Assertions.assertEquals(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun `tilpass kompetanser til barn med åpne regelverkstidslinjer`() { + val kompetanser: List = emptyList() + + val barnasRegelverkResultatTidslinjer = mapOf( + barn1.aktør to "EEEEEEEEE>".tilRegelverkResultatTidslinje(jan2020), + barn2.aktør to " EEEEEEEEE>".tilRegelverkResultatTidslinje(jan2020), + ) + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse("--", barn1) + .medKompetanse(" ->", barn1, barn2) + .byggKompetanser().sortedBy { it.fom } + + val faktiskeKompetanser = tilpassKompetanserTilRegelverk( + kompetanser, + barnasRegelverkResultatTidslinjer.mapValues { it.value.kombinertSøkersResultatTidslinje() }, + emptyMap(), + ).sortedBy { it.fom } + + Assertions.assertEquals(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun `tilpass kompetanser til barn som ikke lenger har EØS-perioder`() { + // "SSSSSSS", barn1 + // "SSSPPSS", barn2 + // "-SSSSSS", barn3 + + val kompetanser = KompetanseBuilder(jan2020) + .medKompetanse(" SS SS", barn1, barn2, barn3) + .medKompetanse("S ", barn1, barn2) + .medKompetanse(" SS ", barn1, barn3) + .medKompetanse(" PP ", barn2) + .medKompetanse("- ", barn3) + .byggKompetanser() + + val barnasRegelverkResultatTidslinjer = mapOf( + barn1.aktør to "EEENNEEEE".tilRegelverkResultatTidslinje(jan2020), + barn2.aktør to "EEE--NNNN".tilRegelverkResultatTidslinje(jan2020), + barn3.aktør to "NNNN-----".tilRegelverkResultatTidslinje(jan2020), + ) + + // SSS SS--, barn1 + // SSS , barn2 + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SSS ", barn1, barn2) + .medKompetanse(" SS--", barn1) + .byggKompetanser().sortedBy { it.fom } + + val faktiskeKompetanser = tilpassKompetanserTilRegelverk( + kompetanser, + barnasRegelverkResultatTidslinjer.mapValues { it.value.kombinertSøkersResultatTidslinje() }, + emptyMap(), + ).sortedBy { it.fom } + + Assertions.assertEquals(forventedeKompetanser, faktiskeKompetanser) + } + + @Test + fun `tilpass kompetanser mot eøs for to barn, der ett barn har etterbetaling 3 år`() { + val kompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SS--SSSS", barn1, barn2) + .byggKompetanser() + + val barnasRegelverkResultatTidslinjer = mapOf( + barn1.aktør to "EEENNEEEE".tilRegelverkResultatTidslinje(jan2020), + barn2.aktør to "EEEENNEEE".tilRegelverkResultatTidslinje(jan2020), + ) + + val barnasHarEtterbetaling3År = mapOf( + barn1.aktør to "TTT ".somBoolskTidslinje(jan2020), + ) + + val forventedeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse(" SS-", barn1, barn2) + .medKompetanse(" S", barn1) + .medKompetanse("SS-- ", barn2) + .byggKompetanser().sortedBy { it.fom } + + val faktiskeKompetanser = tilpassKompetanserTilRegelverk( + kompetanser, + barnasRegelverkResultatTidslinjer.mapValues { it.value.kombinertSøkersResultatTidslinje() }, + barnasHarEtterbetaling3År, + ).sortedBy { it.fom } + + assertEqualsUnordered(forventedeKompetanser, faktiskeKompetanser) + } +} + +private fun Tidslinje.kombinertSøkersResultatTidslinje( + søkersTidslinje: Tidslinje? = null, +): Tidslinje { + return this.kombinerMed(søkersTidslinje ?: this) { barnetsResultat: RegelverkResultat?, søkersResultat: RegelverkResultat? -> + KombinertRegelverkResultat(barnetsResultat, søkersResultat) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/domene/Datagenerator.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/domene/Datagenerator.kt" new file mode 100644 index 000000000..340698845 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/domene/Datagenerator.kt" @@ -0,0 +1,69 @@ +package no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Intervall +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløp +import no.nav.familie.ba.sak.kjerne.eøs.valutakurs.Valutakurs +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +fun lagKompetanse( + behandlingId: Long = lagBehandling().id, + fom: YearMonth? = YearMonth.now(), + tom: YearMonth? = null, + barnAktører: Set = emptySet(), + søkersAktivitet: KompetanseAktivitet? = null, + annenForeldersAktivitet: KompetanseAktivitet? = null, + annenForeldersAktivitetsland: String? = null, + barnetsBostedsland: String? = null, + kompetanseResultat: KompetanseResultat? = null, + søkersAktivitetsland: String? = null, +) = Kompetanse( + fom = fom, + tom = tom, + barnAktører = barnAktører, + søkersAktivitet = søkersAktivitet, + annenForeldersAktivitet = annenForeldersAktivitet, + annenForeldersAktivitetsland = annenForeldersAktivitetsland, + barnetsBostedsland = barnetsBostedsland, + resultat = kompetanseResultat, + søkersAktivitetsland = søkersAktivitetsland, +).also { it.behandlingId = behandlingId } + +fun lagValutakurs( + behandlingId: Long = lagBehandling().id, + fom: YearMonth? = null, + tom: YearMonth? = null, + barnAktører: Set = emptySet(), + valutakursdato: LocalDate? = null, + valutakode: String? = null, + kurs: BigDecimal? = null, +) = Valutakurs( + fom = fom, + tom = tom, + barnAktører = barnAktører, + valutakursdato = valutakursdato, + valutakode = valutakode, + kurs = kurs, +).also { it.behandlingId = behandlingId } + +fun lagUtenlandskPeriodebeløp( + behandlingId: Long = lagBehandling().id, + fom: YearMonth? = null, + tom: YearMonth? = null, + barnAktører: Set = emptySet(), + beløp: BigDecimal? = null, + valutakode: String? = null, + intervall: Intervall? = null, + utbetalingsland: String = "", +) = UtenlandskPeriodebeløp( + fom = fom, + tom = tom, + barnAktører = barnAktører, + valutakode = valutakode, + beløp = beløp, + intervall = intervall, + utbetalingsland = utbetalingsland, +).also { it.behandlingId = behandlingId } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/domene/KompetanseMappingTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/domene/KompetanseMappingTest.kt" new file mode 100644 index 000000000..9a22fa4dd --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/domene/KompetanseMappingTest.kt" @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene + +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.ekstern.restDomene.tilKompetanse +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestKompetanse +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.YearMonth + +internal class KompetanseMappingTest { + + val barn1 = tilfeldigPerson() + val barn2 = tilfeldigPerson() + val barn3 = tilfeldigPerson() + + @Test + fun sjekkAtMappingFremOgTilbakeGirSammeResultat() { + val barnAktører = setOf(barn1.aktør, barn2.aktør, barn3.aktør) + val kompetanse = Kompetanse( + fom = YearMonth.of(2019, 4), + tom = YearMonth.of(2037, 3), + barnAktører = barnAktører, + søkersAktivitet = KompetanseAktivitet.MOTTAR_PENSJON, + annenForeldersAktivitet = KompetanseAktivitet.FORSIKRET_I_BOSTEDSLAND, + annenForeldersAktivitetsland = "pl", + barnetsBostedsland = "dk", + resultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + ) + + val restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(kompetanse, restKompetanse.tilKompetanse(barnAktører.toList())) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/TilpassUtenlandskePeriodebel\303\270pTilKompetanserTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/TilpassUtenlandskePeriodebel\303\270pTilKompetanserTest.kt" new file mode 100644 index 000000000..bd26e45b4 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/TilpassUtenlandskePeriodebel\303\270pTilKompetanserTest.kt" @@ -0,0 +1,67 @@ +package no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp + +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.eøs.assertEqualsUnordered +import no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement.tilpassUtenlandskePeriodebeløpTilKompetanser +import no.nav.familie.ba.sak.kjerne.eøs.util.UtenlandskPeriodebeløpBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.KompetanseBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import org.junit.jupiter.api.Test + +/** + * Syntaks: + * ' ' (blank): Skjema finnes ikke for perioden + * '-': Skjema finnes, men alle felter er null + * '$': Skjema finnes, valutakode er satt, men ellers null-felter + * '': Skjema har oppgitt beløp og valutakode + */ +class TilpassUtenlandskePeriodebeløpTilKompetanserTest { + val jan2020 = jan(2020) + val barn1 = tilfeldigPerson() + val barn2 = tilfeldigPerson() + val barn3 = tilfeldigPerson() + + @Test + fun `test tilpasning av utenlandske periodebeløp mot kompleks endring av kompetanse`() { + val forrigeUtenlandskePeriodebeløp = UtenlandskPeriodebeløpBuilder(jan2020) + .medBeløp("--3456789-----", "EUR", "N", barn1, barn2) + .bygg() + + val gjeldendeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SSSSPPPSSS", barn1, annenForeldersAktivitetsland = "N") + .medKompetanse("PP--PP--PP", barn2, annenForeldersAktivitetsland = "N") + .medKompetanse("-SSS-PP-S-", barn3, annenForeldersAktivitetsland = "N") + .byggKompetanser() + + val forventedeUtenlandskePeriodebeløp = UtenlandskPeriodebeløpBuilder(jan2020) + .medBeløp("- 34 89-", "EUR", "N", barn1) + .medBeløp(" -- - ", null, "N", barn3) + .medBeløp(" - ", null, "N", barn1, barn3) + .bygg() + + val faktiskeUtenlandskePeriodebeløp = + tilpassUtenlandskePeriodebeløpTilKompetanser(forrigeUtenlandskePeriodebeløp, gjeldendeKompetanser) + + assertEqualsUnordered(forventedeUtenlandskePeriodebeløp, faktiskeUtenlandskePeriodebeløp) + } + + @Test + fun `test at endret annennForeldersAktivitetsland i kompetanse fører til endring i utenlandsk periodebeløp`() { + val forrigeUtenlandskePeriodebeløp = UtenlandskPeriodebeløpBuilder(jan2020) + .medBeløp("555555", "EUR", "N", barn1) + .bygg() + + val gjeldendeKompetanser = KompetanseBuilder(jan2020) + .medKompetanse("SSSSSS", barn1, annenForeldersAktivitetsland = "S") + .byggKompetanser() + + val faktiskeUtenlandskePeriodebeløp = + tilpassUtenlandskePeriodebeløpTilKompetanser(forrigeUtenlandskePeriodebeløp, gjeldendeKompetanser) + + val forventedeUtenlandskePeriodebeløp = UtenlandskPeriodebeløpBuilder(jan2020) + .medBeløp("------", null, "S", barn1) + .bygg() + + assertEqualsUnordered(forventedeUtenlandskePeriodebeløp, faktiskeUtenlandskePeriodebeløp) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pServiceTest.kt" new file mode 100644 index 000000000..35b455e16 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pServiceTest.kt" @@ -0,0 +1,132 @@ +package no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.eøs.assertEqualsUnordered +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Intervall +import no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement.TilpassUtenlandskePeriodebeløpTilKompetanserService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.KompetanseRepository +import no.nav.familie.ba.sak.kjerne.eøs.util.UtenlandskPeriodebeløpBuilder +import no.nav.familie.ba.sak.kjerne.eøs.util.mockPeriodeBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilLocalDate +import no.nav.familie.ba.sak.kjerne.tidslinje.util.KompetanseBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class UtenlandskPeriodebeløpServiceTest { + + val utenlandskPeriodebeløpRepository: PeriodeOgBarnSkjemaRepository = + mockPeriodeBarnSkjemaRepository() + val kompetanseRepository: KompetanseRepository = mockk() + + val utenlandskPeriodebeløpService = UtenlandskPeriodebeløpService( + utenlandskPeriodebeløpRepository, + emptyList(), + ) + + val tilpassUtenlandskePeriodebeløpTilKompetanserService = TilpassUtenlandskePeriodebeløpTilKompetanserService( + utenlandskPeriodebeløpRepository, + emptyList(), + kompetanseRepository, + ) + + @BeforeEach + fun init() { + utenlandskPeriodebeløpRepository.deleteAll() + } + + @Test + fun `skal tilpasse utenlandsk periodebeløp til endrede kompetanser`() { + val behandlingId = BehandlingId(10L) + + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = jan(2020).tilLocalDate()) + val barn2 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = jan(2020).tilLocalDate()) + val barn3 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = jan(2020).tilLocalDate()) + + UtenlandskPeriodebeløpBuilder(jan(2020), behandlingId) + .medBeløp("4444 555 666", "EUR", "N", barn1, barn2, barn3) + .lagreTil(utenlandskPeriodebeløpRepository) + + val kompetanser = KompetanseBuilder(jan(2020), behandlingId) + .medKompetanse("SS SSSSS", barn1, annenForeldersAktivitetsland = "N") + .medKompetanse(" PPP", barn1, barn2, barn3, annenForeldersAktivitetsland = "N") + .medKompetanse("-- ----", barn2, barn3, annenForeldersAktivitetsland = "N") + .byggKompetanser() + + every { kompetanseRepository.finnFraBehandlingId(behandlingId.id) } returns kompetanser + + tilpassUtenlandskePeriodebeløpTilKompetanserService + .tilpassUtenlandskPeriodebeløpTilKompetanser(behandlingId) + + val faktiskeUtenlandskePeriodebeløp = utenlandskPeriodebeløpService.hentUtenlandskePeriodebeløp(behandlingId) + + val forventedeUtenlandskePeriodebeløp = UtenlandskPeriodebeløpBuilder(jan(2020), behandlingId) + .medBeløp("44 --555", "EUR", "N", barn1) + .bygg() + + assertEqualsUnordered(forventedeUtenlandskePeriodebeløp, faktiskeUtenlandskePeriodebeløp) + } + + @Test + fun `Slette et utenlandskPeriodebeløp-skjema skal resultere i et skjema uten innhold, men som fortsatt har utbetalingsland`() { + val behandlingId = BehandlingId(10L) + + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = jan(2020).tilLocalDate()) + + val lagretUtenlandskPeriodebeløp = UtenlandskPeriodebeløpBuilder(jan(2020), behandlingId) + .medBeløp("44444444", "EUR", "SE", barn1) + .lagreTil(utenlandskPeriodebeløpRepository).single() + + utenlandskPeriodebeløpService.slettUtenlandskPeriodebeløp(behandlingId, lagretUtenlandskPeriodebeløp.id) + + val faktiskUtenlandskPeriodebeløp = + utenlandskPeriodebeløpService.hentUtenlandskePeriodebeløp(behandlingId).single() + + assertEquals("SE", faktiskUtenlandskPeriodebeløp.utbetalingsland) + assertNull(faktiskUtenlandskPeriodebeløp.beløp) + assertNull(faktiskUtenlandskPeriodebeløp.valutakode) + assertNull(faktiskUtenlandskPeriodebeløp.intervall) + assertNull(faktiskUtenlandskPeriodebeløp.kalkulertMånedligBeløp) + assertEquals(lagretUtenlandskPeriodebeløp.fom, faktiskUtenlandskPeriodebeløp.fom) + assertEquals(lagretUtenlandskPeriodebeløp.tom, faktiskUtenlandskPeriodebeløp.tom) + assertEquals(lagretUtenlandskPeriodebeløp.barnAktører, faktiskUtenlandskPeriodebeløp.barnAktører) + } + + @Test + fun `Skal kunne lukke åpen utenlandskPeriodebeløp-skjema ved å sende inn identisk skjema med satt tom-dato`() { + val behandlingId = BehandlingId(10L) + + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = jan(2020).tilLocalDate()) + + UtenlandskPeriodebeløpBuilder(jan(2020), behandlingId) + .medBeløp("4>", "EUR", "SE", barn1) + .medIntervall(Intervall.UKENTLIG) + .lagreTil(utenlandskPeriodebeløpRepository).single() + + // Oppdaterer UtenlandskPeriodeBeløp med identisk innhold, men med lukket tom for andre mnd. + val oppdatertUtenlandskPeriodebeløp = + UtenlandskPeriodebeløpBuilder(jan(2020)).medBeløp("44", "EUR", "SE", barn1).medIntervall(Intervall.UKENTLIG) + .bygg().first() + utenlandskPeriodebeløpService.oppdaterUtenlandskPeriodebeløp(behandlingId, oppdatertUtenlandskPeriodebeløp) + + // Forventer en liste på 2 elementer hvor det første dekker 2 mnd og det andre dekker fra mnd 3 og til uendelig (null). Det siste elementet skal ha beløp, valutakode og intervall satt til null, mens utbetalingsland skal være "SE". + val faktiskUtenlandskPeriodebeløp = utenlandskPeriodebeløpService.hentUtenlandskePeriodebeløp(behandlingId) + + assertNotNull(faktiskUtenlandskPeriodebeløp) + + assertEquals(2, faktiskUtenlandskPeriodebeløp.size) + assertNull(faktiskUtenlandskPeriodebeløp.elementAt(1).beløp) + assertNull(faktiskUtenlandskPeriodebeløp.elementAt(1).valutakode) + assertNull(faktiskUtenlandskPeriodebeløp.elementAt(1).intervall) + assertNull(faktiskUtenlandskPeriodebeløp.elementAt(1).kalkulertMånedligBeløp) + assertEquals("SE", faktiskUtenlandskPeriodebeløp.elementAt(1).utbetalingsland) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/AssertUtil.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/AssertUtil.kt" new file mode 100644 index 000000000..e37a4c998 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/AssertUtil.kt" @@ -0,0 +1,22 @@ +package no.nav.familie.ba.sak.kjerne.eøs + +import org.junit.jupiter.api.Assertions + +fun assertEqualsUnordered( + expected: Collection, + actual: Collection, +) { + Assertions.assertEquals( + expected.size, + actual.size, + "Forskjellig antall. Forventet ${expected.size} men fikk ${actual.size}", + ) + Assertions.assertTrue( + expected.containsAll(actual), + "Forvantet liste inneholder ikke alle elementene fra faktisk liste", + ) + Assertions.assertTrue( + actual.containsAll(expected), + "Faktisk liste inneholder ikke alle elementene fra forventet liste", + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/DeltBostedBuilder.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/DeltBostedBuilder.kt" new file mode 100644 index 000000000..90d6f75ae --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/DeltBostedBuilder.kt" @@ -0,0 +1,81 @@ +package no.nav.familie.ba.sak.kjerne.eøs.util + +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseUtils +import no.nav.familie.ba.sak.kjerne.beregning.domene.EndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEntitet +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import java.time.YearMonth + +class DeltBostedBuilder( + startMåned: Tidspunkt = jan(2020), + internal val tilkjentYtelse: TilkjentYtelse, +) : SkjemaBuilder(startMåned, BehandlingId(tilkjentYtelse.behandling.id)) { + + fun medDeltBosted(k: String, vararg barn: Person) = medSkjema(k, barn.toList()) { + when (it) { + '0' -> DeltBosted(prosent = 0, barnPersoner = barn.toList()) + '/' -> DeltBosted(prosent = 50, barnPersoner = barn.toList()) + '1' -> DeltBosted(prosent = 100, barnPersoner = barn.toList()) + else -> null + } + } +} + +data class DeltBosted( + override val fom: YearMonth? = null, + override val tom: YearMonth? = null, + override val barnAktører: Set = emptySet(), + val prosent: Int?, + internal val barnPersoner: List = emptyList(), +) : PeriodeOgBarnSkjemaEntitet() { + override fun utenInnhold() = copy(prosent = null) + override fun kopier(fom: YearMonth?, tom: YearMonth?, barnAktører: Set) = + copy( + fom = fom, + tom = tom, + barnAktører = barnAktører.map { it.copy() }.toSet(), + barnPersoner = this.barnPersoner.filter { barnAktører.contains(it.aktør) }, + ).also { + if (barnAktører.size != barnPersoner.size) { + throw Error("Ikke samsvar mellom antall aktører og barn lenger") + } + } + + override var id: Long = 0 + override var behandlingId: Long = 0 +} + +fun DeltBostedBuilder.oppdaterTilkjentYtelse(): TilkjentYtelse { + val andelerTilkjentYtelserEtterEUA = + TilkjentYtelseUtils.oppdaterTilkjentYtelseMedEndretUtbetalingAndeler( + tilkjentYtelse.andelerTilkjentYtelse.toList(), + bygg().tilEndreteUtebetalingAndeler(), + ) + + tilkjentYtelse.andelerTilkjentYtelse.clear() + tilkjentYtelse.andelerTilkjentYtelse.addAll(andelerTilkjentYtelserEtterEUA.map { it.andel }) + return tilkjentYtelse +} + +fun Iterable.tilEndreteUtebetalingAndeler(): List { + return this + .filter { deltBosted -> deltBosted.fom != null && deltBosted.tom != null && deltBosted.prosent != null } + .flatMap { deltBosted -> + deltBosted.barnPersoner.map { + lagEndretUtbetalingAndelMedAndelerTilkjentYtelse( + deltBosted.behandlingId, + it, + deltBosted.fom!!, + deltBosted.tom!!, + deltBosted.prosent!!, + ) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/KompetanseBuilder.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/KompetanseBuilder.kt" new file mode 100644 index 000000000..926d682ae --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/KompetanseBuilder.kt" @@ -0,0 +1,56 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.util + +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.eøs.util.SkjemaBuilder +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt + +class KompetanseBuilder( + startMåned: Tidspunkt = jan(2020), + behandlingId: BehandlingId = BehandlingId(1), +) : SkjemaBuilder(startMåned, behandlingId) { + + fun medKompetanse( + k: String, + vararg barn: Person, + annenForeldersAktivitetsland: String? = null, + erAnnenForelderOmfattetAvNorskLovgivning: Boolean? = false, + ) = + medSkjema(k, barn.toList()) { + when (it) { + '-' -> Kompetanse.NULL.copy( + annenForeldersAktivitetsland = annenForeldersAktivitetsland, + erAnnenForelderOmfattetAvNorskLovgivning = erAnnenForelderOmfattetAvNorskLovgivning, + ) + + 'S' -> Kompetanse.NULL.copy( + resultat = KompetanseResultat.NORGE_ER_SEKUNDÆRLAND, + annenForeldersAktivitetsland = annenForeldersAktivitetsland, + erAnnenForelderOmfattetAvNorskLovgivning = erAnnenForelderOmfattetAvNorskLovgivning, + ).fyllUt() + + 'P' -> Kompetanse.NULL.copy( + resultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + annenForeldersAktivitetsland = annenForeldersAktivitetsland, + erAnnenForelderOmfattetAvNorskLovgivning = erAnnenForelderOmfattetAvNorskLovgivning, + ).fyllUt() + + else -> null + } + } + + fun byggKompetanser(): Collection = bygg() +} + +fun Kompetanse.fyllUt() = this.copy( + erAnnenForelderOmfattetAvNorskLovgivning = erAnnenForelderOmfattetAvNorskLovgivning ?: false, + resultat = resultat ?: KompetanseResultat.NORGE_ER_SEKUNDÆRLAND, + annenForeldersAktivitetsland = annenForeldersAktivitetsland ?: "DK", + barnetsBostedsland = barnetsBostedsland ?: "NO", + søkersAktivitet = søkersAktivitet, + annenForeldersAktivitet = annenForeldersAktivitet, + søkersAktivitetsland = søkersAktivitetsland ?: "SE", +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/MockPeriodeBarnSkjemaRepository.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/MockPeriodeBarnSkjemaRepository.kt" new file mode 100644 index 000000000..436394138 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/MockPeriodeBarnSkjemaRepository.kt" @@ -0,0 +1,73 @@ +package no.nav.familie.ba.sak.kjerne.eøs.util + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEntitet +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import java.util.concurrent.atomic.AtomicLong + +fun > mockPeriodeBarnSkjemaRepository(): PeriodeOgBarnSkjemaRepository { + val minnebasertSkjemaRepository = MinnebasertSkjemaRepository() + val mockSkjemaRepository = mockk>() + + val idSlot = slot() + val skjemaListeSlot = slot>() + + every { mockSkjemaRepository.finnFraBehandlingId(capture(idSlot)) } answers { + minnebasertSkjemaRepository.hentSkjemaer(idSlot.captured) + } + + every { mockSkjemaRepository.getById(capture(idSlot)) } answers { + minnebasertSkjemaRepository.hentSkjema(idSlot.captured) + } + + every { mockSkjemaRepository.saveAll(capture(skjemaListeSlot)) } answers { + minnebasertSkjemaRepository.save(skjemaListeSlot.captured) + } + + every { mockSkjemaRepository.deleteAll(capture(skjemaListeSlot)) } answers { + minnebasertSkjemaRepository.delete(skjemaListeSlot.captured) + } + + every { mockSkjemaRepository.deleteAll() } answers { + minnebasertSkjemaRepository.deleteAll() + } + + return mockSkjemaRepository +} + +private class MinnebasertSkjemaRepository where S : PeriodeOgBarnSkjemaEntitet { + + private val løpenummer = AtomicLong() + private fun AtomicLong.neste() = this.addAndGet(1) + + private val skjemaer = mutableMapOf() + + fun hentSkjemaer(behandlingId: Long): List { + return skjemaer.values + .filter { it.behandlingId == behandlingId } + } + + fun hentSkjema(skjemaId: Long): S = + skjemaer[skjemaId] ?: throw IllegalArgumentException("Finner ikke skjema for id $skjemaId") + + fun save(skjemaer: Iterable) = skjemaer.map { save(it) } + + private fun save(skjema: S): S { + if (skjema.id == 0L) { + skjema.id = løpenummer.neste() + } + + skjemaer[skjema.id] = skjema + return skjema + } + + fun delete(tilSletting: Iterable) { + tilSletting.forEach { skjemaer.remove(it.id) } + } + + fun deleteAll() { + skjemaer.clear() + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/SkjemaBuilder.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/SkjemaBuilder.kt" new file mode 100644 index 000000000..35d347134 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/SkjemaBuilder.kt" @@ -0,0 +1,56 @@ +package no.nav.familie.ba.sak.kjerne.eøs.util + +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaEntitet +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.slåSammenLike +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonthEllerNull +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.tilCharTidslinje + +abstract class SkjemaBuilder( + private val startMåned: Tidspunkt = jan(2020), + private val behandlingId: BehandlingId, +) where S : PeriodeOgBarnSkjemaEntitet, B : SkjemaBuilder { + private val skjemaer: MutableList = mutableListOf() + + protected fun medSkjema(k: String, barn: List, mapChar: (Char?) -> S?): B { + val tidslinje = k.tilCharTidslinje(startMåned) + .map(mapChar) + .slåSammenLike() + + tidslinje.perioder() + .filter { it.innhold != null } + .map { + it.innhold!!.kopier( + fom = it.fraOgMed.tilYearMonthEllerNull(), + tom = it.tilOgMed.tilYearMonthEllerNull(), + barnAktører = barn.map { person -> person.aktør }.toSet(), + ) + } + .all { skjemaer.add(it) } + + @Suppress("UNCHECKED_CAST") + return this as B + } + + protected fun medTransformasjon(transformasjon: (S) -> S): B { + val transformerteSkjemaer = skjemaer.map { skjema -> transformasjon(skjema) } + skjemaer.clear() + skjemaer.addAll(transformerteSkjemaer) + + @Suppress("UNCHECKED_CAST") + return this as B + } + + fun bygg(): Collection = skjemaer + .map { skjema -> skjema.also { it.behandlingId = behandlingId.id } } + + fun lagreTil(repository: PeriodeOgBarnSkjemaRepository): List { + return repository.saveAll(bygg()) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/StringTilTidslinje.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/StringTilTidslinje.kt" new file mode 100644 index 000000000..5feca0794 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/StringTilTidslinje.kt" @@ -0,0 +1,47 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.util + +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat.IKKE_OPPFYLT +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat.OPPFYLT +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.RegelverkResultat +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk.EØS_FORORDNINGEN +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk.NASJONALE_REGLER +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering.DELT_BOSTED +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår + +fun String.tilRegelverkResultatTidslinje(start: Tidspunkt) = + this.tilCharTidslinje(start).map { + when (it?.lowercaseChar()) { + 'e' -> RegelverkResultat.OPPFYLT_EØS_FORORDNINGEN + 'n' -> RegelverkResultat.OPPFYLT_NASJONALE_REGLER + '!' -> RegelverkResultat.OPPFYLT_BLANDET_REGELVERK + '+' -> RegelverkResultat.OPPFYLT_REGELVERK_IKKE_SATT + '?' -> RegelverkResultat.IKKE_FULLT_VURDERT + 'x' -> RegelverkResultat.IKKE_OPPFYLT + else -> null + } + } + +fun String.tilUtdypendeVilkårRegelverkResultatTidslinje(vilkår: Vilkår, start: Tidspunkt) = + this.tilCharTidslinje(start).map { + when (it?.lowercaseChar()) { + '+' -> UtdypendeVilkårRegelverkResultat(vilkår, OPPFYLT, null) + 'n' -> UtdypendeVilkårRegelverkResultat(vilkår, OPPFYLT, NASJONALE_REGLER) + 'x' -> UtdypendeVilkårRegelverkResultat(vilkår, IKKE_OPPFYLT, null) + 'e' -> UtdypendeVilkårRegelverkResultat(vilkår, OPPFYLT, EØS_FORORDNINGEN) + 'é' -> UtdypendeVilkårRegelverkResultat(vilkår, OPPFYLT, EØS_FORORDNINGEN, DELT_BOSTED) + 'd' -> UtdypendeVilkårRegelverkResultat(vilkår, OPPFYLT, null, DELT_BOSTED) + else -> null + } + } + +fun String.tilAnnenForelderOmfattetAvNorskLovgivningTidslinje(start: Tidspunkt) = + this.tilCharTidslinje(start).map { + when (it?.lowercaseChar()) { + '+' -> true + '-' -> false + else -> null + } + } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/TidspunktTilTidslinje.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/TidspunktTilTidslinje.kt" new file mode 100644 index 000000000..84780269a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/TidspunktTilTidslinje.kt" @@ -0,0 +1,23 @@ +package no.nav.familie.ba.sak.kjerne.eøs.util + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.TidspunktClosedRange +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo + +fun TidspunktClosedRange.tilTidslinje(innhold: () -> I): Tidslinje { + val fom = this.start + val tom = this.endInclusive + return object : Tidslinje() { + override fun lagPerioder(): Collection> { + return listOf(Periode(fom, tom, innhold())) + } + } +} + +infix fun TidspunktClosedRange.med(innhold: () -> I): Tidslinje = this.tilTidslinje(innhold) + +fun Tidspunkt.tilTidslinje(innhold: () -> I): Tidslinje = + this.rangeTo(this).tilTidslinje(innhold) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/TilkjentYtelseBuilder.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/TilkjentYtelseBuilder.kt" new file mode 100644 index 000000000..4f82710b6 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/TilkjentYtelseBuilder.kt" @@ -0,0 +1,150 @@ +package no.nav.familie.ba.sak.kjerne.eøs.util + +import no.nav.familie.ba.sak.common.erUnder18ÅrVilkårTidslinje +import no.nav.familie.ba.sak.common.erUnder6ÅrTidslinje +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.beregning.satstypeTidslinje +import no.nav.familie.ba.sak.kjerne.beregning.tilAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.eøs.felles.util.MAX_MÅNED +import no.nav.familie.ba.sak.kjerne.eøs.felles.util.MIN_MÅNED +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrer +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNullMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.beskjærEtter +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map +import no.nav.familie.ba.sak.kjerne.tidslinje.util.tilCharTidslinje +import java.math.BigDecimal +import java.time.LocalDate + +class TilkjentYtelseBuilder( + private val startMåned: Tidspunkt, + private val behandling: Behandling = lagBehandling(), +) { + private val tilkjentYtelse = TilkjentYtelse( + behandling = behandling, + opprettetDato = LocalDate.now(), + endretDato = LocalDate.now(), + ) + + var gjeldendePersoner: List = emptyList() + + fun forPersoner(vararg personer: Person): TilkjentYtelseBuilder { + gjeldendePersoner = personer.toList() + return this + } + + fun medSmåbarn( + s: String, + nasjonalt: (Int) -> Int? = { null }, + differanse: (Int) -> Int? = { null }, + kalkulert: (Int) -> Int = { it }, + ) = medYtelse( + s = s, + type = YtelseType.SMÅBARNSTILLEGG, + kalkulert = kalkulert, + differanse = differanse, + nasjonalt = nasjonalt, + ) { + satstypeTidslinje(SatsType.SMA) + } + .also { gjeldendePersoner.single { it.type == PersonType.SØKER } } + + fun medUtvidet( + s: String, + nasjonalt: (Int) -> Int? = { null }, + differanse: (Int) -> Int? = { null }, + kalkulert: (Int) -> Int = { it }, + ) = medYtelse( + s = s, + type = YtelseType.UTVIDET_BARNETRYGD, + kalkulert = kalkulert, + nasjonalt = nasjonalt, + differanse = differanse, + ) { + satstypeTidslinje(SatsType.UTVIDET_BARNETRYGD) + } + .also { gjeldendePersoner.single { it.type == PersonType.SØKER } } + + fun medOrdinær( + s: String, + prosent: Long = 100, + nasjonalt: (Int) -> Int? = { null }, + differanse: (Int) -> Int? = { null }, + kalkulert: (Int) -> Int = { it }, + ) = medYtelse( + s, + YtelseType.ORDINÆR_BARNETRYGD, + prosent, + nasjonalt, + differanse, + kalkulert, + ) { + val orbaTidslinje = satstypeTidslinje(SatsType.ORBA) + val tilleggOrbaTidslinje = satstypeTidslinje(SatsType.TILLEGG_ORBA) + .filtrerMed(erUnder6ÅrTidslinje(it)) + orbaTidslinje.kombinerMed(tilleggOrbaTidslinje) { orba, tillegg -> tillegg ?: orba } + } + + private fun medYtelse( + s: String, + type: YtelseType, + prosent: Long = 100, + nasjonalt: (Int) -> Int? = { null }, + differanse: (Int) -> Int? = { null }, + kalkulert: (Int) -> Int = { it }, + satsTidslinje: (Person) -> Tidslinje, + ): TilkjentYtelseBuilder { + val andeler = gjeldendePersoner + .map { person -> + val andelTilkjentYtelseTidslinje = s.tilCharTidslinje(startMåned) + .filtrer { char -> char?.let { !it.isWhitespace() } ?: false } + .map { + AndelTilkjentYtelse( + behandlingId = behandling.id, + tilkjentYtelse = tilkjentYtelse, + aktør = person.aktør, + stønadFom = MIN_MÅNED, + stønadTom = MAX_MÅNED, + kalkulertUtbetalingsbeløp = 0, // Overskrives under + nasjonaltPeriodebeløp = 0, // Overskrives under + differanseberegnetPeriodebeløp = null, // Overskrives under + prosent = BigDecimal.valueOf(prosent), + sats = 0, // Overskrives under + type = type, + ) + } + + val begrensetAndelTilkjentYtelseTidslinje = when (type) { + YtelseType.ORDINÆR_BARNETRYGD -> andelTilkjentYtelseTidslinje.beskjærEtter( + erUnder18ÅrVilkårTidslinje(person.fødselsdato), + ) + else -> andelTilkjentYtelseTidslinje + } + + begrensetAndelTilkjentYtelseTidslinje.kombinerUtenNullMed(satsTidslinje(person)) { aty, sats -> + aty.copy( + sats = nasjonalt(sats) ?: kalkulert(sats), + kalkulertUtbetalingsbeløp = kalkulert(sats), + nasjonaltPeriodebeløp = nasjonalt(sats) ?: kalkulert(sats), + differanseberegnetPeriodebeløp = differanse(sats), + ) + } + }.tilAndelerTilkjentYtelse() + + tilkjentYtelse.andelerTilkjentYtelse.addAll(andeler) + return this + } + + fun bygg(): TilkjentYtelse = tilkjentYtelse +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/TilkjentYtelseDsl.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/TilkjentYtelseDsl.kt" new file mode 100644 index 000000000..db54180a1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/TilkjentYtelseDsl.kt" @@ -0,0 +1,62 @@ +package no.nav.familie.ba.sak.kjerne.eøs.util + +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import java.math.BigDecimal +import java.time.YearMonth + +/** + * Enkel DSL for å bygge TilkjentYtelse. Eksempel på bruk er: + * + * val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling) der + * (søker har 1054 i UTVIDET_BARNETRYGD fom jun(2018) tom jul(2024)) og + * (søker har 660 i SMÅBARNSTILLEGG fom aug(2018) tom des(2019)) og + * (søker har 660 i SMÅBARNSTILLEGG fom jul(2020) tom mai(2023)) og + * (barn1 har 1054 i ORDINÆR_BARNETRYGD fom aug(2019) tom jul(2022)) og + * (barn1 har 1054 i ORDINÆR_BARNETRYGD fom aug(2022) tom jul(2024)) og + * (barn2 har 1054 i ORDINÆR_BARNETRYGD fom jul(2020) tom mai(2038)) + * + * Utenlandsk beløp som gir differanseberegning kan introduseres med eller , f.eks: + * (barn1 har 1054 og 756 i ORDINÆR_BARNETRYGD fom aug(2019) tom jul(2022)) + * (barn1 har 1054 minus 756 i ORDINÆR_BARNETRYGD fom aug(2019) tom jul(2022)) + */ +infix fun TilkjentYtelse.der(andelTilkjentYtelse: AndelTilkjentYtelse): TilkjentYtelse { + this.andelerTilkjentYtelse.add( + andelTilkjentYtelse.copy( + tilkjentYtelse = this, + behandlingId = this.behandling.id, + ), + ) + return this +} + +infix fun TilkjentYtelse.og(andelTilkjentYtelse: AndelTilkjentYtelse) = this.der(andelTilkjentYtelse) + +infix fun Person.har(sats: Int) = AndelTilkjentYtelse( + aktør = this.aktør, + sats = sats, + kalkulertUtbetalingsbeløp = sats, + behandlingId = 0, + tilkjentYtelse = lagInitiellTilkjentYtelse(), + stønadFom = YearMonth.now(), + stønadTom = YearMonth.now(), + type = YtelseType.ORDINÆR_BARNETRYGD, + prosent = BigDecimal.valueOf(100), + nasjonaltPeriodebeløp = sats, +) + +infix fun AndelTilkjentYtelse.fom(tidspunkt: Tidspunkt) = this.copy(stønadFom = tidspunkt.tilYearMonth()) +infix fun AndelTilkjentYtelse.tom(tidspunkt: Tidspunkt) = this.copy(stønadTom = tidspunkt.tilYearMonth()) +infix fun AndelTilkjentYtelse.i(ytelseType: YtelseType) = this.copy(type = ytelseType) +infix fun AndelTilkjentYtelse.og(utenlandskBeløp: Int) = this.copy( + differanseberegnetPeriodebeløp = sats - utenlandskBeløp, + kalkulertUtbetalingsbeløp = maxOf(sats - utenlandskBeløp, 0), +) + +infix fun AndelTilkjentYtelse.minus(utenlandskBeløp: Int) = this.og(utenlandskBeløp) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/UtenlandskPeriodebel\303\270pBuilder.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/UtenlandskPeriodebel\303\270pBuilder.kt" new file mode 100644 index 000000000..3c88f4bf4 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/UtenlandskPeriodebel\303\270pBuilder.kt" @@ -0,0 +1,43 @@ +package no.nav.familie.ba.sak.kjerne.eøs.util + +import no.nav.familie.ba.sak.ekstern.restDomene.tilKalkulertMånedligBeløp +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Intervall +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløp +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan + +class UtenlandskPeriodebeløpBuilder( + startMåned: Tidspunkt = jan(2020), + behandlingId: BehandlingId = BehandlingId(1), +) : SkjemaBuilder(startMåned, behandlingId) { + fun medBeløp(k: String, valutakode: String?, utbetalingsland: String?, vararg barn: Person) = + medSkjema(k, barn.toList()) { + when { + it == '-' -> UtenlandskPeriodebeløp.NULL.copy(utbetalingsland = utbetalingsland) + it == '$' -> UtenlandskPeriodebeløp.NULL.copy( + valutakode = valutakode, + utbetalingsland = utbetalingsland, + ) + it?.isDigit() ?: false -> { + UtenlandskPeriodebeløp.NULL.copy( + beløp = it?.digitToInt()?.toBigDecimal(), + valutakode = valutakode, + intervall = Intervall.MÅNEDLIG, + utbetalingsland = utbetalingsland, + kalkulertMånedligBeløp = it?.digitToInt()?.toBigDecimal(), + ) + } + else -> null + } + } + + fun medIntervall(intervall: Intervall) = + medTransformasjon { utenlandskPeriodebeløp -> utenlandskPeriodebeløp.copy(intervall = intervall) }.medTransformasjon { utenlandskPeriodebeløp -> + utenlandskPeriodebeløp.copy( + kalkulertMånedligBeløp = utenlandskPeriodebeløp.tilKalkulertMånedligBeløp(), + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/ValutakursBuilder.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/ValutakursBuilder.kt" new file mode 100644 index 000000000..e8a1420ef --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/ValutakursBuilder.kt" @@ -0,0 +1,28 @@ +package no.nav.familie.ba.sak.kjerne.eøs.util + +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.valutakurs.Valutakurs +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan + +class ValutakursBuilder( + startMåned: Tidspunkt = jan(2020), + behandlingId: BehandlingId = BehandlingId(1), +) : SkjemaBuilder(startMåned, behandlingId) { + fun medKurs(k: String, valutakode: String?, vararg barn: Person) = medSkjema(k, barn.toList()) { + when { + it == '-' -> Valutakurs.NULL + it == '$' -> Valutakurs.NULL.copy(valutakode = valutakode) + it?.isDigit() ?: false -> { + Valutakurs.NULL.copy( + kurs = it?.digitToInt()?.toBigDecimal(), + valutakode = valutakode, + valutakursdato = null, + ) + } + else -> null + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/Vilk\303\245rVurderingBuilder.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/Vilk\303\245rVurderingBuilder.kt" new file mode 100644 index 000000000..89346359c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/Vilk\303\245rVurderingBuilder.kt" @@ -0,0 +1,144 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.util + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.tilPersonEnkelSøkerOgBarn +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseUtils +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårRegelverkResultat +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjer +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilDagEllerFørsteDagIPerioden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilDagEllerSisteDagIPerioden +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.mapIkkeNull +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering + +data class VilkårsvurderingBuilder( + val behandling: Behandling = lagBehandling(), + private val vilkårsvurdering: Vilkårsvurdering = Vilkårsvurdering(behandling = behandling), +) { + val personresultater: MutableSet = mutableSetOf() + val personer: MutableSet = mutableSetOf() + + fun forPerson(person: Person, startTidspunkt: Tidspunkt): PersonResultatBuilder { + return PersonResultatBuilder(this, startTidspunkt, person) + } + + fun byggVilkårsvurdering(): Vilkårsvurdering { + vilkårsvurdering.personResultater = personresultater + return vilkårsvurdering + } + + fun byggPersonopplysningGrunnlag(): PersonopplysningGrunnlag { + return lagTestPersonopplysningGrunnlag(behandling.id, *personer.toTypedArray()) + } + + data class PersonResultatBuilder( + val vilkårsvurderingBuilder: VilkårsvurderingBuilder, + val startTidspunkt: Tidspunkt, + private val person: Person = tilfeldigPerson(), + private val vilkårsresultatTidslinjer: MutableList> = mutableListOf(), + ) { + fun medVilkår(v: String, vararg vilkår: Vilkår): PersonResultatBuilder { + vilkårsresultatTidslinjer.addAll( + vilkår.map { v.tilUtdypendeVilkårRegelverkResultatTidslinje(it, startTidspunkt) }, + ) + return this + } + + fun medVilkår(tidslinje: Tidslinje): PersonResultatBuilder { + vilkårsresultatTidslinjer.add( + tidslinje.mapIkkeNull { UtdypendeVilkårRegelverkResultat(it.vilkår, it.resultat, it.regelverk) }, + ) + return this + } + + fun medUtdypendeVilkår(tidslinje: Tidslinje): PersonResultatBuilder { + vilkårsresultatTidslinjer.add(tidslinje) + return this + } + + fun forPerson(person: Person, startTidspunkt: Tidspunkt): PersonResultatBuilder { + return byggPerson().forPerson(person, startTidspunkt) + } + + fun byggVilkårsvurdering(): Vilkårsvurdering = byggPerson().byggVilkårsvurdering() + fun byggPersonopplysningGrunnlag(): PersonopplysningGrunnlag = byggPerson().byggPersonopplysningGrunnlag() + + fun byggPerson(): VilkårsvurderingBuilder { + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurderingBuilder.vilkårsvurdering, + aktør = person.aktør, + ) + + val vilkårresultater = vilkårsresultatTidslinjer.flatMap { + it.perioder() + .filter { it.innhold != null } + .flatMap { periode -> periode.tilVilkårResultater(personResultat) } + } + + personResultat.vilkårResultater.addAll(vilkårresultater) + vilkårsvurderingBuilder.personresultater.add(personResultat) + vilkårsvurderingBuilder.personer.add(person) + + return vilkårsvurderingBuilder + } + } +} + +internal fun Periode.tilVilkårResultater(personResultat: PersonResultat): Collection { + return listOf( + VilkårResultat( + personResultat = personResultat, + vilkårType = this.innhold?.vilkår!!, + resultat = this.innhold?.resultat!!, + vurderesEtter = this.innhold?.regelverk, + periodeFom = this.fraOgMed.tilDagEllerFørsteDagIPerioden().tilLocalDateEllerNull(), + periodeTom = this.tilOgMed.tilDagEllerSisteDagIPerioden().tilLocalDateEllerNull(), + begrunnelse = "En begrunnelse", + sistEndretIBehandlingId = personResultat.vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = this.innhold?.utdypendeVilkårsvurderinger ?: emptyList(), + ), + ) +} + +fun VilkårsvurderingBuilder.byggVilkårsvurderingTidslinjer() = + VilkårsvurderingTidslinjer(this.byggVilkårsvurdering(), this.byggPersonopplysningGrunnlag().tilPersonEnkelSøkerOgBarn()) + +fun VilkårsvurderingBuilder.PersonResultatBuilder.byggVilkårsvurderingTidslinjer() = + this.byggPerson().byggVilkårsvurderingTidslinjer() + +fun VilkårsvurderingBuilder.byggTilkjentYtelse() = + TilkjentYtelseUtils.beregnTilkjentYtelse( + vilkårsvurdering = this.byggVilkårsvurdering(), + personopplysningGrunnlag = this.byggPersonopplysningGrunnlag(), + fagsakType = FagsakType.NORMAL, + + ) + +data class UtdypendeVilkårRegelverkResultat( + val vilkår: Vilkår, + val resultat: Resultat?, + val regelverk: Regelverk?, + val utdypendeVilkårsvurderinger: List = emptyList(), +) { + constructor( + vilkår: Vilkår, + resultat: Resultat?, + regelverk: Regelverk?, + vararg utdypendeVilkårsvurdering: UtdypendeVilkårsvurdering, + ) : this(vilkår, resultat, regelverk, utdypendeVilkårsvurdering.toList()) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/Vilk\303\245rsvurderingBuilderDsl.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/Vilk\303\245rsvurderingBuilderDsl.kt" new file mode 100644 index 000000000..2fae7c7fa --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/util/Vilk\303\245rsvurderingBuilderDsl.kt" @@ -0,0 +1,85 @@ +package no.nav.familie.ba.sak.kjerne.eøs.util + +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Dødsfall +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Uendelighet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilLocalDate +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.TidspunktClosedRange +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.mapIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.util.UtdypendeVilkårRegelverkResultat +import no.nav.familie.ba.sak.kjerne.tidslinje.util.VilkårsvurderingBuilder +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import java.time.LocalDate + +val barn get() = PersonType.BARN +val søker get() = PersonType.SØKER +infix fun PersonType.født(tidspunkt: Tidspunkt) = + tilfeldigPerson(personType = this, fødselsdato = tidspunkt.tilLocalDate()) + +internal infix fun Person.død(tidspunkt: Tidspunkt) = this.copy( + dødsfall = Dødsfall( + person = this, + dødsfallDato = tidspunkt.tilLocalDate(), + dødsfallAdresse = null, + dødsfallPostnummer = null, + dødsfallPoststed = null, + ), +) + +internal val uendelig: Tidspunkt = DagTidspunkt(LocalDate.now(), Uendelighet.FREMTID) +internal fun Person.under18år() = DagTidspunkt.med(this.fødselsdato) + .rangeTo(DagTidspunkt.med(this.fødselsdato.plusYears(18).minusDays(1))) + +val vilkårsvurdering get() = VilkårsvurderingBuilder() +infix fun Vilkår.og(vilkår: Vilkår) = listOf(this, vilkår) +infix fun List.og(vilkår: Vilkår) = this + vilkår +infix fun Vilkår.i(tidsrom: TidspunktClosedRange) = oppfyltUtdypendeVilkår(this, null) i tidsrom +infix fun UtdypendeVilkårRegelverkResultat.i(tidsrom: TidspunktClosedRange) = + tidsrom.tilTidslinje { this } + +infix fun List.oppfylt(tidsrom: TidspunktClosedRange) = this.map { + oppfyltUtdypendeVilkår(it, null) i tidsrom +} + +infix fun Vilkår.oppfylt(tidsrom: TidspunktClosedRange) = + oppfyltUtdypendeVilkår(this, null) i tidsrom + +infix fun Tidslinje.etter(regelverk: Regelverk) = + this.mapIkkeNull { it.copy(regelverk = regelverk) } + +infix fun Tidslinje.med(utdypendeVilkår: UtdypendeVilkårsvurdering) = + this.mapIkkeNull { it.copy(utdypendeVilkårsvurderinger = it.utdypendeVilkårsvurderinger + utdypendeVilkår) } + +infix fun VilkårsvurderingBuilder.der(person: Person) = this.forPerson(person, DagTidspunkt.nå()) +infix fun VilkårsvurderingBuilder.PersonResultatBuilder.har(vilkår: Tidslinje) = + this.medUtdypendeVilkår(vilkår) + +infix fun VilkårsvurderingBuilder.PersonResultatBuilder.har(vilkår: Iterable>) = + vilkår.map { this.medUtdypendeVilkår(it) }.last() + +infix fun VilkårsvurderingBuilder.PersonResultatBuilder.og(vilkår: Tidslinje) = + har(vilkår) + +infix fun VilkårsvurderingBuilder.PersonResultatBuilder.og(vilkår: Iterable>) = + har(vilkår) + +infix fun VilkårsvurderingBuilder.PersonResultatBuilder.der(person: Person) = + this.forPerson(person, DagTidspunkt.nå()) + +fun oppfyltUtdypendeVilkår(vilkår: Vilkår, regelverk: Regelverk? = null) = + UtdypendeVilkårRegelverkResultat( + vilkår = vilkår, + resultat = Resultat.OPPFYLT, + regelverk = regelverk, + ) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/TilpassValutakursTilUtenlandskePeridebel\303\270pTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/TilpassValutakursTilUtenlandskePeridebel\303\270pTest.kt" new file mode 100644 index 000000000..804d1240b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/TilpassValutakursTilUtenlandskePeridebel\303\270pTest.kt" @@ -0,0 +1,68 @@ +package no.nav.familie.ba.sak.kjerne.eøs.valutakurs + +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.eøs.assertEqualsUnordered +import no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement.tilpassValutakurserTilUtenlandskePeriodebeløp +import no.nav.familie.ba.sak.kjerne.eøs.util.UtenlandskPeriodebeløpBuilder +import no.nav.familie.ba.sak.kjerne.eøs.util.ValutakursBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import org.junit.jupiter.api.Test + +/** + * Syntaks: + * ' ' (blank): Skjema finnes ikke for perioden + * '-': Skjema finnes, men alle felter er null + * '$': Skjema finnes, valutakode er satt, men ellers null-felter + * '': Skjema har oppgitt kurs og valutakode + */ +class TilpassValutakursTilUtenlandskePeridebeløpTest { + val jan2020 = jan(2020) + val barn1 = tilfeldigPerson() + val barn2 = tilfeldigPerson() + val barn3 = tilfeldigPerson() + + @Test + fun `test tilpasning av valutakurser mot kompleks endring av utenlandsk valutabeløp`() { + val gjeldendeValutakurser = ValutakursBuilder(jan2020) + .medKurs("--3456789-----", "EUR", barn1, barn2) + .bygg() + + val utenlandskePeriodebeløp = UtenlandskPeriodebeløpBuilder(jan2020) + .medBeløp("2222 --333-", "EUR", "N", barn1) + .medBeløp("2223333444 ", "SEK", "N", barn2) + .medBeløp("-$$$- -23- ", "DKK", "N", barn3) + .bygg() + + val forventedeValutakurser = ValutakursBuilder(jan2020) + .medKurs("$$34 - 89$-", "EUR", barn1) + .medKurs("$$$$$$$$$$ ", "SEK", barn2) + .medKurs("-$$$- $$- ", "DKK", barn3) + .medKurs(" - ", "DKK", barn1, barn3) + .bygg() + + val faktiskeValutakurser = + tilpassValutakurserTilUtenlandskePeriodebeløp(gjeldendeValutakurser, utenlandskePeriodebeløp) + + assertEqualsUnordered(forventedeValutakurser, faktiskeValutakurser) + } + + @Test + fun `test at endret valuta i utenlandsk periodebeløp fører til endring i valutakurs`() { + val gjeldendeValutakurser = ValutakursBuilder(jan2020) + .medKurs("333333", "EUR", barn1) + .bygg() + + val utenlandskePeriodebeløp = UtenlandskPeriodebeløpBuilder(jan2020) + .medBeløp("222222", "DKK", "DK", barn1) + .bygg() + + val forventedeValutakurser = ValutakursBuilder(jan2020) + .medKurs("$$$$$$", "DKK", barn1) + .bygg() + + val faktiskeValutakurser = + tilpassValutakurserTilUtenlandskePeriodebeløp(gjeldendeValutakurser, utenlandskePeriodebeløp) + + assertEqualsUnordered(forventedeValutakurser, faktiskeValutakurser) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursControllerTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursControllerTest.kt" new file mode 100644 index 000000000..a27600904 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursControllerTest.kt" @@ -0,0 +1,127 @@ +package no.nav.familie.ba.sak.kjerne.eøs.valutakurs + +import io.mockk.MockKException +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.justRun +import io.mockk.verify +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.ekstern.restDomene.RestValutakurs +import no.nav.familie.ba.sak.ekstern.restDomene.UtfyltStatus +import no.nav.familie.ba.sak.ekstern.restDomene.tilValutakurs +import no.nav.familie.ba.sak.integrasjoner.ecb.ECBService +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +@ExtendWith(MockKExtension::class) +class ValutakursControllerTest { + + @MockK + private lateinit var valutakursService: ValutakursService + + @MockK + private lateinit var personidentService: PersonidentService + + @MockK + private lateinit var utvidetBehandlingService: UtvidetBehandlingService + + @MockK + private lateinit var ecbService: ECBService + + @MockK + private lateinit var tilgangService: TilgangService + + @InjectMockKs + private lateinit var valutakursController: ValutakursController + + private val barnId = "12345678910" + + private val restValutakurs: RestValutakurs = + RestValutakurs(1, YearMonth.of(2020, 1), null, listOf(barnId), null, null, null, UtfyltStatus.OK) + + @BeforeEach + fun setup() { + every { personidentService.hentAktør(any()) } returns tilAktør(barnId) + every { valutakursService.hentValutakurs(any()) } returns restValutakurs.tilValutakurs(listOf(tilAktør(barnId))) + every { ecbService.hentValutakurs(any(), any()) } returns BigDecimal.valueOf(0.95) + justRun { tilgangService.validerTilgangTilBehandling(any(), any()) } + justRun { tilgangService.verifiserHarTilgangTilHandling(any(), any()) } + justRun { tilgangService.validerKanRedigereBehandling(any()) } + } + + @Test + fun `Test at valutakurs hentes fra ECB dersom dato og valuta er satt`() { + val valutakursDato = LocalDate.of(2022, 1, 1) + val valuta = "SEK" + assertThrows { + valutakursController.oppdaterValutakurs( + 1, + restValutakurs.copy(valutakursdato = valutakursDato, valutakode = valuta), + ) + } + verify(exactly = 1) { ecbService.hentValutakurs("SEK", valutakursDato) } + verify(exactly = 1) { valutakursService.oppdaterValutakurs(any(), any()) } + } + + @Test + fun `Test at valutakurs ikke hentes fra ECB dersom dato ikke er satt`() { + val valutakursDato = LocalDate.of(2022, 1, 1) + assertThrows { + valutakursController.oppdaterValutakurs( + 1, + restValutakurs.copy(valutakode = "SEK"), + ) + } + verify(exactly = 0) { ecbService.hentValutakurs("SEK", valutakursDato) } + verify(exactly = 1) { valutakursService.oppdaterValutakurs(any(), any()) } + } + + @Test + fun `Test at valutakurs ikke hentes fra ECB dersom valuta ikke er satt`() { + val valutakursDato = LocalDate.of(2022, 1, 1) + assertThrows { + valutakursController.oppdaterValutakurs( + 1, + restValutakurs.copy(valutakursdato = valutakursDato), + ) + } + verify(exactly = 0) { ecbService.hentValutakurs("SEK", valutakursDato) } + verify(exactly = 1) { valutakursService.oppdaterValutakurs(any(), any()) } + } + + @Test + fun `Test at valutakurs ikke hentes fra ECB dersom ISK og før 1 feb 2018`() { + val valutakursDato = LocalDate.of(2018, 1, 31) + assertThrows { + valutakursController.oppdaterValutakurs( + 1, + restValutakurs.copy(valutakursdato = valutakursDato, valutakode = "ISK"), + ) + } + verify(exactly = 0) { ecbService.hentValutakurs("ISK", valutakursDato) } + verify(exactly = 1) { valutakursService.oppdaterValutakurs(any(), any()) } + } + + @Test + fun `Test at valutakurs hentes fra ECB dersom ISK og etter 1 feb 2018`() { + val valutakursDato = LocalDate.of(2018, 2, 1) + assertThrows { + valutakursController.oppdaterValutakurs( + 1, + restValutakurs.copy(valutakursdato = valutakursDato, valutakode = "ISK"), + ) + } + verify(exactly = 1) { ecbService.hentValutakurs("ISK", valutakursDato) } + verify(exactly = 1) { valutakursService.oppdaterValutakurs(any(), any()) } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursServiceTest.kt" new file mode 100644 index 000000000..6c8d22671 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursServiceTest.kt" @@ -0,0 +1,133 @@ +package no.nav.familie.ba.sak.kjerne.eøs.valutakurs + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.eøs.assertEqualsUnordered +import no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement.TilpassValutakurserTilUtenlandskePeriodebeløpService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.PeriodeOgBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.medBehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløpRepository +import no.nav.familie.ba.sak.kjerne.eøs.util.UtenlandskPeriodebeløpBuilder +import no.nav.familie.ba.sak.kjerne.eøs.util.ValutakursBuilder +import no.nav.familie.ba.sak.kjerne.eøs.util.mockPeriodeBarnSkjemaRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilLocalDate +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +internal class ValutakursServiceTest { + val valutakursRepository: PeriodeOgBarnSkjemaRepository = mockPeriodeBarnSkjemaRepository() + val utenlandskPeriodebeløpRepository: UtenlandskPeriodebeløpRepository = mockk() + + val valutakursService = ValutakursService( + valutakursRepository, + emptyList(), + ) + + val tilpassValutakurserTilUtenlandskePeriodebeløpService = TilpassValutakurserTilUtenlandskePeriodebeløpService( + valutakursRepository, + utenlandskPeriodebeløpRepository, + emptyList(), + ) + + @BeforeEach + fun init() { + valutakursRepository.deleteAll() + } + + @Test + fun `skal tilpasse utenlandsk periodebeløp til endrede kompetanser`() { + val behandlingId = BehandlingId(10L) + + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = jan(2020).tilLocalDate()) + val barn2 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = jan(2020).tilLocalDate()) + val barn3 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = jan(2020).tilLocalDate()) + + ValutakursBuilder(jan(2020), behandlingId) + .medKurs("4444 555 666", "EUR", barn1, barn2, barn3) + .lagreTil(valutakursRepository) + + val utenlandskePeriodebeløp = UtenlandskPeriodebeløpBuilder(jan(2020), behandlingId) + .medBeløp(" 777777777", "EUR", "N", barn1) + .bygg() + + every { utenlandskPeriodebeløpRepository.finnFraBehandlingId(behandlingId.id) } returns utenlandskePeriodebeløp + + tilpassValutakurserTilUtenlandskePeriodebeløpService.tilpassValutakursTilUtenlandskPeriodebeløp(behandlingId) + + val faktiskeValutakurser = valutakursService.hentValutakurser(behandlingId) + + val forventedeValutakurser = ValutakursBuilder(jan(2020), behandlingId) + .medKurs(" 44$$$555$", "EUR", barn1) + .bygg() + + assertEqualsUnordered(forventedeValutakurser, faktiskeValutakurser) + } + + @Test + fun `slette et valutakurs-skjema skal resultere i et skjema uten innhold, men som fortsatt har valutakoden`() { + val behandlingId = BehandlingId(10L) + + val lagretValutakurs = valutakursRepository.saveAll( + listOf( + Valutakurs( + fom = YearMonth.now(), + tom = YearMonth.now(), + barnAktører = setOf(tilfeldigPerson().aktør), + valutakursdato = LocalDate.now(), + valutakode = "EUR", + kurs = BigDecimal.TEN, + ), + ).medBehandlingId(behandlingId), + ).single() + + valutakursService.slettValutakurs(behandlingId, lagretValutakurs.id) + + val faktiskValutakurs = valutakursService.hentValutakurser(behandlingId).single() + + assertEquals("EUR", faktiskValutakurs.valutakode) + assertNull(faktiskValutakurs.valutakursdato) + assertNull(faktiskValutakurs.kurs) + + assertEquals(lagretValutakurs.fom, faktiskValutakurs.fom) + assertEquals(lagretValutakurs.tom, faktiskValutakurs.tom) + assertEquals(lagretValutakurs.barnAktører, faktiskValutakurs.barnAktører) + } + + @Test + fun `skal kunne lukke åpen valutakurs ved å sende inn identisk skjema med til-og-med-dato`() { + val behandlingId = BehandlingId(10L) + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + + // Åpen (til-og-med er null) valutakurs for ett barn + ValutakursBuilder(jan(2020), behandlingId) + .medKurs("4>", "EUR", barn1) + .lagreTil(valutakursRepository) + + // Endrer kun til-og-med dato fra uendelig (null) til en gitt dato + val oppdatertKompetanse = valutakurs(jan(2020), "444", "EUR", barn1) + valutakursService.oppdaterValutakurs(behandlingId, oppdatertKompetanse) + + // Forventer skjema uten innhold (MEN MED VALUTAKODE) fra oppdatert dato og fremover + val forventedeValutakurser = ValutakursBuilder(jan(2020), behandlingId) + .medKurs("444$>", "EUR", barn1) + .bygg() + + val faktiskeValutakurser = valutakursService.hentValutakurser(behandlingId) + assertEqualsUnordered(forventedeValutakurser, faktiskeValutakurser) + } +} + +fun valutakurs(tidspunkt: Tidspunkt, s: String, valutakode: String, vararg barn: Person) = + ValutakursBuilder(tidspunkt).medKurs(s, valutakode, *barn).bygg().first() diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/EndretUtbetalingAndelTidslinjeServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/EndretUtbetalingAndelTidslinjeServiceTest.kt" new file mode 100644 index 000000000..ab4f00c31 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/EndretUtbetalingAndelTidslinjeServiceTest.kt" @@ -0,0 +1,141 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering + +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndel +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mar +import no.nav.familie.ba.sak.kjerne.tidslinje.util.nov +import no.nav.familie.ba.sak.kjerne.tidslinje.util.somBoolskTidslinje +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.YearMonth + +internal class EndretUtbetalingAndelTidslinjeServiceTest { + + @Test + fun `lager tidslinje for ett barn med én etterbetaling`() { + val person = tilfeldigPerson() + val endringer = listOf( + lagEndretUtbetalingAndel( + person = person, + årsak = Årsak.ETTERBETALING_3ÅR, + fom = YearMonth.of(2020, 3), + tom = YearMonth.of(2020, 7), + prosent = BigDecimal.ZERO, + ), + ) + + val forventet = mapOf( + person.aktør to "TTTTT".somBoolskTidslinje(mar(2020)), + ) + + val faktisk = endringer.tilBarnasSkalIkkeUtbetalesTidslinjer() + + assertEquals(forventet, faktisk) + } + + @Test + fun `lager tidslinje for to barn med flere etterbetalinger`() { + val person1 = tilfeldigPerson() + val person2 = tilfeldigPerson() + + val endringer = listOf( + lagEndretUtbetalingAndel( + person = person1, + årsak = Årsak.ETTERBETALING_3ÅR, + fom = YearMonth.of(2020, 3), + tom = YearMonth.of(2020, 7), + prosent = BigDecimal.ZERO, + ), + lagEndretUtbetalingAndel( + person = person2, + årsak = Årsak.ETTERBETALING_3ÅR, + fom = YearMonth.of(2019, 11), + tom = YearMonth.of(2021, 3), + prosent = BigDecimal.ZERO, + ), + lagEndretUtbetalingAndel( + person = person1, + årsak = Årsak.ETTERBETALING_3ÅR, + fom = YearMonth.of(2021, 1), + tom = YearMonth.of(2021, 5), + prosent = BigDecimal.ZERO, + ), + ) + + val forventet = mapOf( + person1.aktør to "TTTTT TTTTT".somBoolskTidslinje(mar(2020)).filtrerIkkeNull(), + person2.aktør to "TTTTTTTTTTTTTTTTT".somBoolskTidslinje(nov(2019)).filtrerIkkeNull(), + ) + + val faktisk = endringer.tilBarnasSkalIkkeUtbetalesTidslinjer() + + assertEquals(forventet, faktisk) + } + + @Test + fun `lager tidslinje for ett barn med allerede utbetalt`() { + val person = tilfeldigPerson() + val endringer = listOf( + lagEndretUtbetalingAndel( + person = person, + årsak = Årsak.ALLEREDE_UTBETALT, + fom = YearMonth.of(2020, 3), + tom = YearMonth.of(2020, 7), + prosent = BigDecimal.ZERO, + ), + ) + + val forventet = mapOf( + person.aktør to "TTTTT".somBoolskTidslinje(mar(2020)), + ) + + val faktisk = endringer.tilBarnasSkalIkkeUtbetalesTidslinjer() + + assertEquals(forventet, faktisk) + } + + @Test + fun `lager tidslinje for ett barn med endre mottaker`() { + val person = tilfeldigPerson() + val endringer = listOf( + lagEndretUtbetalingAndel( + person = person, + årsak = Årsak.ENDRE_MOTTAKER, + fom = YearMonth.of(2020, 3), + tom = YearMonth.of(2020, 7), + prosent = BigDecimal.ZERO, + ), + ) + + val forventet = mapOf( + person.aktør to "TTTTT".somBoolskTidslinje(mar(2020)), + ) + + val faktisk = endringer.tilBarnasSkalIkkeUtbetalesTidslinjer() + + assertEquals(forventet, faktisk) + } + + @Test + fun `ikke lag tidslinje hvis årsaken ikke er etterbetaling 3 år, allerede utbetalt eller endre mottaker`() { + val person = tilfeldigPerson() + val endringer = listOf( + lagEndretUtbetalingAndel( + person = person, + årsak = Årsak.DELT_BOSTED, + fom = YearMonth.of(2020, 3), + tom = YearMonth.of(2020, 7), + ), + ) + + val faktisk = endringer.tilBarnasSkalIkkeUtbetalesTidslinjer() + + assertEquals(emptyMap>(), faktisk) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/TidslinjerTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/TidslinjerTest.kt" new file mode 100644 index 000000000..012c56b58 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/TidslinjerTest.kt" @@ -0,0 +1,362 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.oppfyltVilkår +import no.nav.familie.ba.sak.common.til18ÅrsVilkårsdato +import no.nav.familie.ba.sak.common.tilPersonEnkelSøkerOgBarn +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement.tilpassKompetanserTilRegelverk +import no.nav.familie.ba.sak.kjerne.eøs.util.tilTidslinje +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.konkatenerTidslinjer +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.ogSenere +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilInneværendeMåned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map +import no.nav.familie.ba.sak.kjerne.tidslinje.util.VilkårsvurderingBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.apr +import no.nav.familie.ba.sak.kjerne.tidslinje.util.byggVilkårsvurderingTidslinjer +import no.nav.familie.ba.sak.kjerne.tidslinje.util.des +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mai +import no.nav.familie.ba.sak.kjerne.tidslinje.util.nov +import no.nav.familie.ba.sak.kjerne.tidslinje.util.tilRegelverkResultatTidslinje +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk.EØS_FORORDNINGEN +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk.NASJONALE_REGLER +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.BOR_MED_SØKER +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.BOSATT_I_RIKET +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.GIFT_PARTNERSKAP +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.LOVLIG_OPPHOLD +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.UNDER_18_ÅR +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import java.time.YearMonth + +internal class TidslinjerTest { + + @Test + fun `lag en søker med to barn og mye kompleksitet i vilkårsvurderingen`() { + val barnsFødselsdato = 13.jan(2020) + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = barnsFødselsdato.tilLocalDate()) + val barn2 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = barnsFødselsdato.tilLocalDate()) + + val behandling = lagBehandling() + val startMåned = barnsFødselsdato.tilInneværendeMåned() + + val vilkårsvurderingBygger = VilkårsvurderingBuilder(behandling) + .forPerson(søker, startMåned) + .medVilkår("EEEEEEEENNEEEEEEEEEEE", BOSATT_I_RIKET) + .medVilkår("EEEEEEEENNEEEEEEEEEEE", LOVLIG_OPPHOLD) + .byggPerson() + val søkerResult = " EEEEEEENNEEEEEEEEEEE".tilRegelverkResultatTidslinje(startMåned).filtrerIkkeNull() + + vilkårsvurderingBygger.forPerson(barn1, startMåned) + .medVilkår("++++++++++++++++ ", UNDER_18_ÅR) + .medVilkår(" EEE NNNN EEEE+++ ", BOSATT_I_RIKET) + .medVilkår(" EEENNEEEEEEEEE ", LOVLIG_OPPHOLD) + .medVilkår("NNNNNNNNNNEEEEEEEEEEE", BOR_MED_SØKER) + .medVilkår("+++++++++++++++++++++", GIFT_PARTNERSKAP) + .byggPerson() + val barn1Result = " ???????NN!???EE?????".tilRegelverkResultatTidslinje(startMåned).filtrerIkkeNull() + + vilkårsvurderingBygger.forPerson(barn2, startMåned) + .medVilkår("+++++++++>", UNDER_18_ÅR) + .medVilkår(" EEEE++EE>", BOSATT_I_RIKET) + .medVilkår("EEEEEEEEE>", LOVLIG_OPPHOLD) + .medVilkår("EEEENNEEE>", BOR_MED_SØKER) + .medVilkår("+++++++++>", GIFT_PARTNERSKAP) + .byggPerson() + val barn2Result = " ?EE!!!E!!EEEEEEEEEEE".tilRegelverkResultatTidslinje(startMåned).filtrerIkkeNull() + + val vilkårsvurderingTidslinjer = VilkårsvurderingTidslinjer( + vilkårsvurdering = vilkårsvurderingBygger.byggVilkårsvurdering(), + søkerOgBarn = lagTestPersonopplysningGrunnlag(behandling.id, søker, barn1, barn2) + .tilPersonEnkelSøkerOgBarn(), + ) + + assertEquals(søkerResult, vilkårsvurderingTidslinjer.søkersTidslinjer().regelverkResultatTidslinje) + assertEquals(barn1Result, vilkårsvurderingTidslinjer.forBarn(barn1).regelverkResultatTidslinje.kombinertResultat) + assertEquals(barn2Result, vilkårsvurderingTidslinjer.forBarn(barn2).regelverkResultatTidslinje.kombinertResultat) + } + + @Test + fun `lag en søker med ett barn og søker går fra EØS-regelverk til nasjonalt`() { + val barnsFødselsdato = 13.jan(2020) + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = barnsFødselsdato.tilLocalDate()) + + val behandling = lagBehandling() + val startMåned = barnsFødselsdato.tilInneværendeMåned() + + val vilkårsvurderingBygger = VilkårsvurderingBuilder(behandling) + .forPerson(søker, startMåned) + .medVilkår("EEEEEEEEEEEEENNNNNNNN", BOSATT_I_RIKET) + .medVilkår("EEEEEEEEEEEEENNNNNNNN", LOVLIG_OPPHOLD) + .byggPerson() + val søkerResult = " EEEEEEEEEEEENNNNNNNN".tilRegelverkResultatTidslinje(startMåned).filtrerIkkeNull() + + vilkårsvurderingBygger.forPerson(barn1, startMåned) + .medVilkår("++++++++++++++++ ", UNDER_18_ÅR) + .medVilkår(" EEEENNNNEEEEEEEE ", BOSATT_I_RIKET) + .medVilkår(" EEENNEEEEEEEEE ", LOVLIG_OPPHOLD) + .medVilkår("NNNNNNNNNNEEEEEEEEEEE", BOR_MED_SØKER) + .medVilkår("+++++++++++++++++++++", GIFT_PARTNERSKAP) + .byggPerson() + val barn1Result = " ?????!!!!!EE!!!?????".tilRegelverkResultatTidslinje(startMåned).filtrerIkkeNull() + + val vilkårsvurderingTidslinjer = VilkårsvurderingTidslinjer( + vilkårsvurdering = vilkårsvurderingBygger.byggVilkårsvurdering(), + søkerOgBarn = lagTestPersonopplysningGrunnlag(behandling.id, søker, barn1) + .tilPersonEnkelSøkerOgBarn(), + ) + + assertEquals(søkerResult, vilkårsvurderingTidslinjer.søkersTidslinjer().regelverkResultatTidslinje) + assertEquals(barn1Result, vilkårsvurderingTidslinjer.forBarn(barn1).regelverkResultatTidslinje.kombinertResultat) + } + + @Test + fun `Virkningstidspunkt for vilkårsvurdering varer frem til måneden før barnet fyller 18 år`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = 14.des(2019).tilLocalDate()) + + val behandling = lagBehandling() + + val vilkårsvurderingBygger = VilkårsvurderingBuilder(behandling) + .forPerson(søker, jan(2020)) + .medVilkår("+>", BOSATT_I_RIKET) + .medVilkår("+>", LOVLIG_OPPHOLD) + .forPerson(barn1, jan(2020)) + .medVilkår("+>", UNDER_18_ÅR) + .medVilkår("+>", BOSATT_I_RIKET) + .medVilkår("+>", LOVLIG_OPPHOLD) + .medVilkår("+>", BOR_MED_SØKER) + .medVilkår("+>", GIFT_PARTNERSKAP) + .byggPerson() + + val vilkårsvurderingTidslinjer = VilkårsvurderingTidslinjer( + vilkårsvurdering = vilkårsvurderingBygger.byggVilkårsvurdering(), + søkerOgBarn = lagTestPersonopplysningGrunnlag(behandling.id, søker, barn1) + .tilPersonEnkelSøkerOgBarn(), + ) + + assertEquals( + barn1.fødselsdato.til18ÅrsVilkårsdato().minusMonths(1).toYearMonth(), + vilkårsvurderingTidslinjer.forBarn(barn1).egetRegelverkResultatTidslinje.filtrerIkkeNull() + .perioder().maxOf { it.tilOgMed.tilYearMonth() }, + ) + } + + @Test + fun `Sjekk overgang fra oppfylt nasjonalt til oppfylt EØS i månedsskiftet`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = 14.des(2019).tilLocalDate()) + + val vilkårsvurderingTidslinjer = VilkårsvurderingBuilder() + .forPerson(søker, 30.apr(2020)) + .medVilkår("NE", BOSATT_I_RIKET, LOVLIG_OPPHOLD) + .forPerson(barn1, 30.apr(2020)) + .medVilkår("++", UNDER_18_ÅR, GIFT_PARTNERSKAP) + .medVilkår("NE", BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER) + .byggVilkårsvurderingTidslinjer() + + val barn1Result = "E".tilRegelverkResultatTidslinje(mai(2020)) + + assertEquals(barn1Result, vilkårsvurderingTidslinjer.forBarn(barn1).regelverkResultatTidslinje.kombinertResultat) + } + + @Test + fun `Sjekk overgang fra oppfylt EØS til oppfylt nasjonalt i månedsskiftet`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = 14.des(2019).tilLocalDate()) + + val vilkårsvurderingTidslinjer = VilkårsvurderingBuilder() + .forPerson(søker, 30.apr(2020)) + .medVilkår("EN", BOSATT_I_RIKET, LOVLIG_OPPHOLD) + .forPerson(barn1, 30.apr(2020)) + .medVilkår("++", UNDER_18_ÅR, GIFT_PARTNERSKAP) + .medVilkår("EN", BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER) + .byggVilkårsvurderingTidslinjer() + + val barn1Result = "N".tilRegelverkResultatTidslinje(mai(2020)) + + assertEquals(barn1Result, vilkårsvurderingTidslinjer.forBarn(barn1).regelverkResultatTidslinje.kombinertResultat) + } + + @Test + fun `Sjekk overgang fra oppfylt EØS til oppfylt blandet i månedsskiftet`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = 14.des(2019).tilLocalDate()) + + val vilkårsvurderingTidslinjer = VilkårsvurderingBuilder() + .forPerson(søker, 30.apr(2020)) + .medVilkår("EE", BOSATT_I_RIKET, LOVLIG_OPPHOLD) + .forPerson(barn1, 30.apr(2020)) + .medVilkår("++", UNDER_18_ÅR, GIFT_PARTNERSKAP) + .medVilkår("EE", BOSATT_I_RIKET) + .medVilkår("EE", LOVLIG_OPPHOLD) + .medVilkår("EN", BOR_MED_SØKER) + .byggVilkårsvurderingTidslinjer() + + val barn1Result = "!".tilRegelverkResultatTidslinje(mai(2020)) + + assertEquals(barn1Result, vilkårsvurderingTidslinjer.forBarn(barn1).regelverkResultatTidslinje.kombinertResultat) + } + + @Test + fun `Sjekk overgang fra oppfylt nasjonalt til oppfylt EØS dagen før siste dag i måneden`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = 14.des(2019).tilLocalDate()) + + val giftPartnerskap = + (26.jan(2020)..30.nov(2021)).tilTidslinje { oppfyltVilkår(GIFT_PARTNERSKAP) } + val under18 = + (26.jan(2020)..30.nov(2021)).tilTidslinje { oppfyltVilkår(UNDER_18_ÅR) } + val bosattBarnOgSøker = konkatenerTidslinjer( + (26.jan(2020)..29.apr(2021)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, NASJONALE_REGLER) }, + (30.apr(2021)..30.nov(2021)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, EØS_FORORDNINGEN) }, + ) + val lovligOppholdBarnOgSøker = konkatenerTidslinjer( + (26.jan(2020)..29.apr(2021)).tilTidslinje { oppfyltVilkår(LOVLIG_OPPHOLD, NASJONALE_REGLER) }, + (30.apr(2021)..30.nov(2021)).tilTidslinje { oppfyltVilkår(LOVLIG_OPPHOLD, EØS_FORORDNINGEN) }, + ) + val borMedSøker = konkatenerTidslinjer( + (26.jan(2020)..29.apr(2021)).tilTidslinje { oppfyltVilkår(BOR_MED_SØKER, NASJONALE_REGLER) }, + (30.apr(2021)..30.nov(2021)).tilTidslinje { oppfyltVilkår(BOR_MED_SØKER, EØS_FORORDNINGEN) }, + ) + + val barnaRegelverkTidslinjer = VilkårsvurderingBuilder() + .forPerson(søker, 26.jan(2020)) + .medVilkår(bosattBarnOgSøker) + .medVilkår(lovligOppholdBarnOgSøker) + .forPerson(barn, 26.jan(2020)) + .medVilkår(bosattBarnOgSøker) + .medVilkår(lovligOppholdBarnOgSøker) + .medVilkår(giftPartnerskap) + .medVilkår(under18) + .medVilkår(borMedSøker) + .byggVilkårsvurderingTidslinjer() + .barnasRegelverkResultatTidslinjer() + + val kompetanser = tilpassKompetanserTilRegelverk( + emptyList(), + barnaRegelverkTidslinjer, + emptyMap(), + ) + + assertEquals(1, kompetanser.size) + assertEquals(YearMonth.of(2021, 5), kompetanser.first().fom) + assertEquals(YearMonth.of(2021, 11), kompetanser.first().tom) + } + + @Test + fun `Sjekk overgang fra oppfylt nasjonalt til oppfylt EØS dagen andre dag i måneden`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = 14.des(2019).tilLocalDate()) + + val giftPartnerskap = + (26.jan(2020)..30.nov(2021)).tilTidslinje { oppfyltVilkår(GIFT_PARTNERSKAP) } + val under18 = + (26.jan(2020)..30.nov(2021)).tilTidslinje { oppfyltVilkår(UNDER_18_ÅR) } + val bosattBarnOgSøker = konkatenerTidslinjer( + (26.jan(2020)..1.mai(2021)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, NASJONALE_REGLER) }, + (2.mai(2021)..30.nov(2021)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, EØS_FORORDNINGEN) }, + ) + val lovligOppholdBarnOgSøker = konkatenerTidslinjer( + (26.jan(2020)..1.mai(2021)).tilTidslinje { oppfyltVilkår(LOVLIG_OPPHOLD, NASJONALE_REGLER) }, + (2.mai(2021)..30.nov(2021)).tilTidslinje { oppfyltVilkår(LOVLIG_OPPHOLD, EØS_FORORDNINGEN) }, + ) + val borMedSøker = konkatenerTidslinjer( + (26.jan(2020)..1.mai(2021)).tilTidslinje { oppfyltVilkår(BOR_MED_SØKER, NASJONALE_REGLER) }, + (2.mai(2021)..30.nov(2021)).tilTidslinje { oppfyltVilkår(BOR_MED_SØKER, EØS_FORORDNINGEN) }, + ) + + val barnaRegelverkTidslinjer = VilkårsvurderingBuilder() + .forPerson(søker, 26.jan(2020)) + .medVilkår(bosattBarnOgSøker) + .medVilkår(lovligOppholdBarnOgSøker) + .forPerson(barn, 26.jan(2020)) + .medVilkår(bosattBarnOgSøker) + .medVilkår(lovligOppholdBarnOgSøker) + .medVilkår(giftPartnerskap) + .medVilkår(under18) + .medVilkår(borMedSøker) + .byggVilkårsvurderingTidslinjer() + .barnasRegelverkResultatTidslinjer() + + val kompetanser = tilpassKompetanserTilRegelverk( + emptyList(), + barnaRegelverkTidslinjer, + emptyMap(), + ) + + val forventetRegelverkResultat = + "NNNNNNNNNNNNNNNNEEEEEE".tilRegelverkResultatTidslinje(feb(2020)) + + assertEquals(forventetRegelverkResultat, barnaRegelverkTidslinjer[barn.aktør]?.kombinertResultat) + assertEquals(1, kompetanser.size) + assertEquals(YearMonth.of(2021, 6), kompetanser.first().fom) + assertEquals(YearMonth.of(2021, 11), kompetanser.first().tom) + } + + @Test + fun `Sjekk overgang fra oppfylt nasjonalt til oppfylt EØS dagen før siste dag i måneden, der siste periode er uendelig`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = 14.des(2019).tilLocalDate()) + + val giftPartnerskap = + 26.jan(2020).ogSenere().tilTidslinje { oppfyltVilkår(GIFT_PARTNERSKAP) } + val under18 = + 26.jan(2020).ogSenere().tilTidslinje { oppfyltVilkår(UNDER_18_ÅR) } + val bosattBarnOgSøker = konkatenerTidslinjer( + (26.jan(2020)..29.apr(2021)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, NASJONALE_REGLER) }, + 30.apr(2021).ogSenere().tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, EØS_FORORDNINGEN) }, + ) + val lovligOppholdBarnOgSøker = konkatenerTidslinjer( + (26.jan(2020)..29.apr(2021)).tilTidslinje { oppfyltVilkår(LOVLIG_OPPHOLD, NASJONALE_REGLER) }, + 30.apr(2021).ogSenere().tilTidslinje { oppfyltVilkår(LOVLIG_OPPHOLD, EØS_FORORDNINGEN) }, + ) + val borMedSøker = konkatenerTidslinjer( + (26.jan(2020)..29.apr(2021)).tilTidslinje { oppfyltVilkår(BOR_MED_SØKER, NASJONALE_REGLER) }, + 30.apr(2021).ogSenere().tilTidslinje { oppfyltVilkår(BOR_MED_SØKER, EØS_FORORDNINGEN) }, + ) + + val barnaRegelverkTidslinjer = VilkårsvurderingBuilder() + .forPerson(søker, 26.jan(2020)) + .medVilkår(bosattBarnOgSøker) + .medVilkår(lovligOppholdBarnOgSøker) + .forPerson(barn, 26.jan(2020)) + .medVilkår(bosattBarnOgSøker) + .medVilkår(lovligOppholdBarnOgSøker) + .medVilkår(giftPartnerskap) + .medVilkår(under18) + .medVilkår(borMedSøker) + .byggVilkårsvurderingTidslinjer() + .barnasRegelverkResultatTidslinjer() + + val kompetanser = tilpassKompetanserTilRegelverk( + emptyList(), + barnaRegelverkTidslinjer, + emptyMap(), + ) + + assertEquals(1, kompetanser.size) + assertEquals(YearMonth.of(2021, 5), kompetanser.first().fom) + assertNull(kompetanser.first().tom) + } +} + +fun VilkårsvurderingTidslinjer.barnasRegelverkResultatTidslinjer() = this.barnasTidslinjer() + .mapValues { (_, barnetsTidslinjer) -> barnetsTidslinjer.regelverkResultatTidslinje } + +private val Tidslinje.kombinertResultat: Tidslinje + get() = map { it?.kombinertResultat } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsresultatM\303\245nedTidslinjeTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsresultatM\303\245nedTidslinjeTest.kt" new file mode 100644 index 000000000..4ef33b367 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsresultatM\303\245nedTidslinjeTest.kt" @@ -0,0 +1,155 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering + +import no.nav.familie.ba.sak.common.ikkeOppfyltVilkår +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.oppfyltVilkår +import no.nav.familie.ba.sak.kjerne.eøs.util.tilTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.konkatenerTidslinjer +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.ogSenere +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.ogTidligere +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo +import no.nav.familie.ba.sak.kjerne.tidslinje.util.apr +import no.nav.familie.ba.sak.kjerne.tidslinje.util.aug +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mai +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mar +import no.nav.familie.ba.sak.kjerne.tidslinje.util.nov +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk.EØS_FORORDNINGEN +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk.NASJONALE_REGLER +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.BOSATT_I_RIKET +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class VilkårsresultatMånedTidslinjeTest { + + @Test + fun `Virkningstidspunkt fra vilkårsvurdering er måneden etter at normalt vilkår er oppfylt`() { + val dagTidslinje = (15.apr(2022)..14.apr(2040)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET) } + val faktiskMånedTidslinje = dagTidslinje.tilMånedsbasertTidslinjeForVilkårRegelverkResultat() + val forventetMånedTidslinje = (mai(2022)..apr(2040)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET) } + + assertEquals( + forventetMånedTidslinje, + faktiskMånedTidslinje, + ) + } + + @Test + fun `Back to back perioder i månedsskiftet gir sammenhengende perioder`() { + val periodeFom = LocalDate.of(2022, 4, 15) + val periodeFom2 = LocalDate.of(2022, 7, 1) + val vilkårsresultatMånedTidslinje = + listOf( + lagVilkårResultat( + vilkårType = BOSATT_I_RIKET, + periodeFom = periodeFom, + periodeTom = periodeFom2.minusDays(1), + ), + lagVilkårResultat( + vilkårType = BOSATT_I_RIKET, + periodeFom = periodeFom2, + periodeTom = null, + ), + ) + .tilVilkårRegelverkResultatTidslinje() + .tilMånedsbasertTidslinjeForVilkårRegelverkResultat() + + val forventetMånedstidslinje: Tidslinje = + (mai(2022)..aug(2022).somUendeligLengeTil()).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET) } + + assertEquals(forventetMånedstidslinje, vilkårsresultatMånedTidslinje) + } + + @Test + fun `Siste dag fom-måned og første dag i tom-måned gir oppfylt fra neste måned`() { + val dagvilkårtidslinje: Tidslinje = + (29.feb(2020)..1.mai(2020)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET) } + + val forventetMånedstidslinje: Tidslinje = + (mar(2020)..mai(2020)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET) } + + val faktiskMånedstidslinje = dagvilkårtidslinje.tilMånedsbasertTidslinjeForVilkårRegelverkResultat() + assertEquals(forventetMånedstidslinje, faktiskMånedstidslinje) + } + + @Test + fun `Bytte av regelverk innen en måned skal gi kontinuerlig oppfylt tidslinje`() { + val dagvilkårtidslinje = konkatenerTidslinjer( + (26.feb(2020)..7.mar(2020)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, EØS_FORORDNINGEN) }, + (21.mar(2020)..13.mai(2020)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, NASJONALE_REGLER) }, + ) + + val forventetMånedstidslinje = konkatenerTidslinjer( + mar(2020).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, EØS_FORORDNINGEN) }, + (apr(2020)..mai(2020)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, NASJONALE_REGLER) }, + ) + + val faktiskMånedstidslinje = + dagvilkårtidslinje.tilMånedsbasertTidslinjeForVilkårRegelverkResultat() + assertEquals(forventetMånedstidslinje, faktiskMånedstidslinje) + } + + @Test + fun `Hvis vilkåret er oppfylt siste dag i måneden, skal kun gi oppfylt frem til og med den måneden`() { + val dagTidslinje = (15.apr(2022)..30.nov(2022)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET) } + val faktiskMånedTidslinje = dagTidslinje.tilMånedsbasertTidslinjeForVilkårRegelverkResultat() + val forventetMånedTidslinje = (mai(2022)..nov(2022)).tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET) } + + assertEquals( + forventetMånedTidslinje, + faktiskMånedTidslinje, + ) + } + + @Test + fun `Hvis regelverk byttes i månedskiftet, skal det være kontinuerlig oppfylt vilkår`() { + val dagvilkårtidslinje = konkatenerTidslinjer( + 31.mar(2020).ogTidligere().tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, EØS_FORORDNINGEN) }, + 1.apr(2020).ogSenere().tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, NASJONALE_REGLER) }, + ) + + val forventetMånedstidslinje = konkatenerTidslinjer( + mar(2020).ogTidligere().tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, EØS_FORORDNINGEN) }, + apr(2020).ogSenere().tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, NASJONALE_REGLER) }, + ) + + val faktiskMånedstidslinje = dagvilkårtidslinje.tilMånedsbasertTidslinjeForVilkårRegelverkResultat() + assertEquals(forventetMånedstidslinje, faktiskMånedstidslinje) + } + + @Test + fun `Hvis det byttes fra oppfylt til ikke oppfylt i månedskiftet, skal kun gi oppfylt til og med denne måneden`() { + val dagvilkårtidslinje = konkatenerTidslinjer( + 31.mar(2020).ogTidligere().tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, EØS_FORORDNINGEN) }, + 1.apr(2020).ogSenere().tilTidslinje { ikkeOppfyltVilkår(BOSATT_I_RIKET) }, + ) + + val forventetMånedstidslinje = konkatenerTidslinjer( + mar(2020).ogTidligere().tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, EØS_FORORDNINGEN) }, + ) + + val faktiskMånedstidslinje = dagvilkårtidslinje.tilMånedsbasertTidslinjeForVilkårRegelverkResultat() + assertEquals(forventetMånedstidslinje, faktiskMånedstidslinje) + } + + @Test + fun `Hvis regelverk byttes dagen før månedskiftet, skal det være kontinuerlig oppfylt vilkår`() { + val dagvilkårtidslinje = konkatenerTidslinjer( + 29.apr(2020).ogTidligere().tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, NASJONALE_REGLER) }, + 30.apr(2020).ogSenere().tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, EØS_FORORDNINGEN) }, + ) + + val forventetMånedstidslinje = konkatenerTidslinjer( + apr(2020).ogTidligere().tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, NASJONALE_REGLER) }, + mai(2020).ogSenere().tilTidslinje { oppfyltVilkår(BOSATT_I_RIKET, EØS_FORORDNINGEN) }, + ) + + val faktiskMånedstidslinje = dagvilkårtidslinje.tilMånedsbasertTidslinjeForVilkårRegelverkResultat() + assertEquals(forventetMånedstidslinje, faktiskMånedstidslinje) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTidslinjeServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTidslinjeServiceTest.kt" new file mode 100644 index 000000000..613f7e7f6 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTidslinjeServiceTest.kt" @@ -0,0 +1,168 @@ +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagVilkårsvurderingMedOverstyrendeResultater +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjeService +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.simulering.lagBehandling +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.filtrerIkkeNull +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.erTom +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import no.nav.familie.ba.sak.kjerne.tidslinje.util.tilAnnenForelderOmfattetAvNorskLovgivningTidslinje +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.LocalDate + +internal class VilkårsvurderingTidslinjeServiceTest { + val persongrunnlagService = mockk() + val vilkårsvurderingService = mockk() + val vilkårsvurderingRepository = mockk() + + private lateinit var vilkårsvurderingTidslinjeService: VilkårsvurderingTidslinjeService + + @BeforeEach + fun setUp() { + vilkårsvurderingTidslinjeService = VilkårsvurderingTidslinjeService( + vilkårsvurderingRepository = vilkårsvurderingRepository, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + ) + } + + @Test + fun `skal forskyve fom med 1 mnd for periode med erAnnenForelderOmfattetAvNorskLovgivning`() { + val søker = lagPerson(type = PersonType.SØKER) + val barn = lagPerson(type = PersonType.BARN) + val fagsak = Fagsak(aktør = søker.aktør) + val behandling = lagBehandling(fagsak = fagsak, behandlingKategori = BehandlingKategori.EØS) + val vilkårsvurdering = lagVilkårsvurderingMedOverstyrendeResultater( + søker = søker, + barna = listOf(barn), + overstyrendeVilkårResultater = mapOf( + Pair( + søker.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2023, 1, 2), + periodeTom = LocalDate.of(2023, 3, 4), + behandlingId = behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.ANNEN_FORELDER_OMFATTET_AV_NORSK_LOVGIVNING), + ), + ), + ), + ), + ) + every { persongrunnlagService.hentAktivThrows(behandlingId = behandling.id) } returns lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søker.aktør.aktivFødselsnummer(), + barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), + ) + every { vilkårsvurderingService.hentAktivForBehandlingThrows(behandlingId = behandling.id) } returns vilkårsvurdering + + val faktiskTidslinje = vilkårsvurderingTidslinjeService.hentAnnenForelderOmfattetAvNorskLovgivningTidslinje( + behandlingId = BehandlingId(behandling.id), + ) + val forventetTidslinje = "++".tilAnnenForelderOmfattetAvNorskLovgivningTidslinje(feb(2023)) + assertThat(faktiskTidslinje).isEqualTo(forventetTidslinje) + } + + @Test + fun `skal ikke gi noen oppfylte perioder hvis vilkår kun oppfylt innenfor én måned`() { + val søker = lagPerson(type = PersonType.SØKER) + val barn = lagPerson(type = PersonType.BARN) + val fagsak = Fagsak(aktør = søker.aktør) + val behandling = lagBehandling(fagsak = fagsak, behandlingKategori = BehandlingKategori.EØS) + val vilkårsvurdering = lagVilkårsvurderingMedOverstyrendeResultater( + søker = søker, + barna = listOf(barn), + overstyrendeVilkårResultater = mapOf( + Pair( + søker.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2021, 12, 1), + periodeTom = LocalDate.of(2021, 12, 31), + behandlingId = behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.ANNEN_FORELDER_OMFATTET_AV_NORSK_LOVGIVNING), + ), + ), + ), + ), + ) + every { persongrunnlagService.hentAktivThrows(behandlingId = behandling.id) } returns lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søker.aktør.aktivFødselsnummer(), + barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), + ) + every { vilkårsvurderingService.hentAktivForBehandlingThrows(behandlingId = behandling.id) } returns vilkårsvurdering + + val faktiskTidslinje = vilkårsvurderingTidslinjeService.hentAnnenForelderOmfattetAvNorskLovgivningTidslinje( + behandlingId = BehandlingId(behandling.id), + ) + + assertThat(faktiskTidslinje.erTom()).isTrue + } + + @Test + fun `skal forskyve fom med 1 mnd for flere perioder med erAnnenForelderOmfattetAvNorskLovgivning`() { + val søker = lagPerson(type = PersonType.SØKER) + val barn = lagPerson(type = PersonType.BARN) + val fagsak = Fagsak(aktør = søker.aktør) + val behandling = lagBehandling(fagsak = fagsak, behandlingKategori = BehandlingKategori.EØS) + val vilkårsvurdering = lagVilkårsvurderingMedOverstyrendeResultater( + søker = søker, + barna = listOf(barn), + overstyrendeVilkårResultater = mapOf( + Pair( + søker.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2023, 1, 2), + periodeTom = LocalDate.of(2023, 3, 4), + behandlingId = behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.ANNEN_FORELDER_OMFATTET_AV_NORSK_LOVGIVNING), + ), + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2023, 4, 30), + periodeTom = LocalDate.of(2023, 7, 1), + behandlingId = behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.ANNEN_FORELDER_OMFATTET_AV_NORSK_LOVGIVNING), + ), + ), + ), + ), + ) + every { persongrunnlagService.hentAktivThrows(behandlingId = behandling.id) } returns lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søker.aktør.aktivFødselsnummer(), + barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), + ) + every { vilkårsvurderingService.hentAktivForBehandlingThrows(behandlingId = behandling.id) } returns vilkårsvurdering + + val faktiskTidslinje = vilkårsvurderingTidslinjeService.hentAnnenForelderOmfattetAvNorskLovgivningTidslinje( + behandlingId = BehandlingId(behandling.id), + ) + val forventetTidslinje = "++ +++".tilAnnenForelderOmfattetAvNorskLovgivningTidslinje(feb(2023)).filtrerIkkeNull() + assertThat(faktiskTidslinje).isEqualTo(forventetTidslinje) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTidslinjerTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTidslinjerTest.kt" new file mode 100644 index 000000000..dcf9156a8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTidslinjerTest.kt" @@ -0,0 +1,134 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering + +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPersonResultaterForSøkerOgToBarn +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.tilPersonEnkelSøkerOgBarn +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import java.time.LocalDate + +internal class VilkårsvurderingTidslinjerTest { + + @Test + fun `et vilkår kan ha overlappende vilkårsresultater hvis bare ett er oppfylt`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val barn2Fnr = randomFnr() + val barnaFnr = listOf(barnFnr, barn2Fnr) + + val defaultBehandling = lagBehandling(defaultFagsak()) + val vilkårsvurdering = Vilkårsvurdering( + behandling = defaultBehandling, + ).also { + it.personResultater = lagPersonResultaterForSøkerOgToBarn( + it, + tilAktør(søkerFnr), + tilAktør(barnFnr), + tilAktør(barn2Fnr), + LocalDate.now().minusMonths(3), + LocalDate.now().minusMonths(2), + ) + } + + // Legg på et overlappende vilkårsresultat som IKKE er oppfylt + vilkårsvurdering.personResultater.filter { it.aktør.aktivFødselsnummer() == barnFnr }.forEach { + it.vilkårResultater.add( + lagVilkårResultat( + id = 1000, + personResultat = it, + vilkårType = Vilkår.BOR_MED_SØKER, + behandlingId = defaultBehandling.id, + periodeFom = null, // uendelig lenge siden + periodeTom = null, // uendelig lenge til + resultat = Resultat.IKKE_OPPFYLT, + ), + ) + } + + assertDoesNotThrow { + VilkårsvurderingTidslinjer( + vilkårsvurdering = vilkårsvurdering, + søkerOgBarn = lagTestPersonopplysningGrunnlag(defaultBehandling.id, søkerFnr, barnaFnr) + .tilPersonEnkelSøkerOgBarn(), + ) + } + } + + @Test + fun `kan ikke ha to overlappende vilkårsresultater hvis begge er oppfylt`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val barn2Fnr = randomFnr() + val barnaFnr = listOf(barnFnr, barn2Fnr) + + val defaultBehandling = lagBehandling(defaultFagsak()) + val vilkårsvurdering = Vilkårsvurdering( + behandling = defaultBehandling, + ).also { + it.personResultater = lagPersonResultaterForSøkerOgToBarn( + it, + tilAktør(søkerFnr), + tilAktør(barnFnr), + tilAktør(barn2Fnr), + LocalDate.now().minusMonths(3), + LocalDate.now().minusMonths(2), + ) + } + + // Legg på et overlappende vilkårsresultat som ER oppfylt + vilkårsvurdering.personResultater.filter { it.aktør.aktivFødselsnummer() == barnFnr }.forEach { + it.vilkårResultater.add( + lagVilkårResultat( + id = 500, + personResultat = it, + vilkårType = Vilkår.BOSATT_I_RIKET, + behandlingId = defaultBehandling.id, + periodeTom = null, + resultat = Resultat.OPPFYLT, + ), + ) + } + + assertThrows { + VilkårsvurderingTidslinjer( + vilkårsvurdering = vilkårsvurdering, + søkerOgBarn = lagTestPersonopplysningGrunnlag(defaultBehandling.id, søkerFnr, barnaFnr) + .tilPersonEnkelSøkerOgBarn(), + ) + } + } + + private fun lagVilkårResultat( + id: Long = 0, + personResultat: PersonResultat? = null, + vilkårType: Vilkår = Vilkår.BOSATT_I_RIKET, + resultat: Resultat = Resultat.OPPFYLT, + periodeFom: LocalDate? = LocalDate.of(2009, 12, 24), + periodeTom: LocalDate? = LocalDate.of(2010, 1, 31), + begrunnelse: String = "", + behandlingId: Long = lagBehandling().id, + utdypendeVilkårsvurderinger: List = emptyList(), + ) = VilkårResultat( + id = id, + personResultat = personResultat, + vilkårType = vilkårType, + resultat = resultat, + periodeFom = periodeFom, + periodeTom = periodeTom, + begrunnelse = begrunnelse, + sistEndretIBehandlingId = behandlingId, + utdypendeVilkårsvurderinger = utdypendeVilkårsvurderinger, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/rest/RestTidslinjerTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/rest/RestTidslinjerTest.kt" new file mode 100644 index 000000000..000f8a38c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/vilk\303\245rsvurdering/rest/RestTidslinjerTest.kt" @@ -0,0 +1,120 @@ +package no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.rest + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.tilPersonEnkelSøkerOgBarn +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjer +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilInneværendeMåned +import no.nav.familie.ba.sak.kjerne.tidslinje.util.VilkårsvurderingBuilder +import no.nav.familie.ba.sak.kjerne.tidslinje.util.des +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.nov +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class RestTidslinjerTest { + + @Test + fun `når barnet har løpende vilkår, skal likevel rest-tidslinjene for regelverk og oppfylt vilkår være avsluttet ved 18 år`() { + val barnsFødselsdato = 13.jan(2020) + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = barnsFødselsdato.tilLocalDate()) + + val behandling = lagBehandling() + val startMåned = barnsFødselsdato.tilInneværendeMåned() + + val vilkårsvurderingBygger = VilkårsvurderingBuilder(behandling) + .forPerson(søker, startMåned) + .medVilkår("EEEEEEEEEEEEEEEEEEEEEEEEE", Vilkår.BOSATT_I_RIKET) + .medVilkår("EEEEEEEEEEEEEEEEEEEEEEEEE", Vilkår.LOVLIG_OPPHOLD) + .forPerson(barn1, startMåned) + .medVilkår("+++++++++>", Vilkår.UNDER_18_ÅR) + .medVilkår(" EEEE++EE>", Vilkår.BOSATT_I_RIKET) + .medVilkår("EEEEEEEEE>", Vilkår.LOVLIG_OPPHOLD) + .medVilkår("EEEENNEEE>", Vilkår.BOR_MED_SØKER) + .medVilkår("+++++++++>", Vilkår.GIFT_PARTNERSKAP) + + val vilkårsvurderingTidslinjer = VilkårsvurderingTidslinjer( + vilkårsvurdering = vilkårsvurderingBygger.byggVilkårsvurdering(), + søkerOgBarn = lagTestPersonopplysningGrunnlag(behandling.id, søker, barn1) + .tilPersonEnkelSøkerOgBarn(), + ) + + val restTidslinjer = vilkårsvurderingTidslinjer.tilRestTidslinjer() + val barnetsTidslinjer = restTidslinjer.barnasTidslinjer[barn1.aktør.aktivFødselsnummer()]!! + + // Stopper ved søkers siste til-og-med-dato fordi Regelverk er etter det, som filtreres bort + assertEquals( + 31.jan(2022).tilLocalDate(), + barnetsTidslinjer.regelverkTidslinje.last().tilOgMed, + ) + assertEquals( + 31.jan(2022).tilLocalDate(), + barnetsTidslinjer.oppfyllerEgneVilkårIKombinasjonMedSøkerTidslinje.last().tilOgMed, + ) + + // Alle vilkårene til barnet kuttes ved siste dag i måneden før barnet fyller 18 år + barnetsTidslinjer.vilkårTidslinjer.forEach { + assertEquals( + 31.des(2037).tilLocalDate(), + it.last().tilOgMed, + ) + } + } + + @Test + fun `søkers rest-tidslinjene for oppfylt vilkår skal begrenses av barnas 18-års-perioder`() { + val søkersFødselsdato = 3.feb(1995) + val barn1Fødselsdato = 13.jan(2020) + val barn2Fødselsdato = 27.des(2021) + val søker = tilfeldigPerson(personType = PersonType.SØKER, fødselsdato = søkersFødselsdato.tilLocalDate()) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = barn1Fødselsdato.tilLocalDate()) + val barn2 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = barn2Fødselsdato.tilLocalDate()) + + val behandling = lagBehandling() + + val vilkårsvurderingBygger = VilkårsvurderingBuilder(behandling) + .forPerson(søker, søkersFødselsdato.tilInneværendeMåned()) + .medVilkår("E>", Vilkår.BOSATT_I_RIKET) + .medVilkår("E>", Vilkår.LOVLIG_OPPHOLD) + .forPerson(barn1, barn1Fødselsdato.tilInneværendeMåned()) + .medVilkår("+>", Vilkår.UNDER_18_ÅR) + .medVilkår("E>", Vilkår.BOSATT_I_RIKET) + .medVilkår("E>", Vilkår.LOVLIG_OPPHOLD) + .medVilkår("E>", Vilkår.BOR_MED_SØKER) + .medVilkår("+>", Vilkår.GIFT_PARTNERSKAP) + .forPerson(barn2, barn2Fødselsdato.tilInneværendeMåned()) + .medVilkår("+>", Vilkår.UNDER_18_ÅR) + .medVilkår("E>", Vilkår.BOSATT_I_RIKET) + .medVilkår("E>", Vilkår.LOVLIG_OPPHOLD) + .medVilkår("E>", Vilkår.BOR_MED_SØKER) + .medVilkår("+>", Vilkår.GIFT_PARTNERSKAP) + + val vilkårsvurderingTidslinjer = VilkårsvurderingTidslinjer( + vilkårsvurdering = vilkårsvurderingBygger.byggVilkårsvurdering(), + søkerOgBarn = lagTestPersonopplysningGrunnlag(behandling.id, søker, barn1, barn2) + .tilPersonEnkelSøkerOgBarn(), + ) + + val restTidslinjer = vilkårsvurderingTidslinjer.tilRestTidslinjer() + val søkersTidslinjer = restTidslinjer.søkersTidslinjer + + // Stopper ved siste dag i måneden før yngste barn fyller 18 år + søkersTidslinjer.vilkårTidslinjer.forEach { + assertEquals( + 30.nov(2039).tilLocalDate(), + it.last().tilOgMed, + ) + } + + assertEquals( + 30.nov(2039).tilLocalDate(), + søkersTidslinjer.oppfyllerEgneVilkårTidslinje.last().tilOgMed, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIEndretUtbetalingAndelUtilTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIEndretUtbetalingAndelUtilTest.kt new file mode 100644 index 000000000..1a4c02d79 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIEndretUtbetalingAndelUtilTest.kt @@ -0,0 +1,187 @@ +package no.nav.familie.ba.sak.kjerne.forrigebehandling + +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndel +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.YearMonth + +class EndringIEndretUtbetalingAndelUtilTest { + + val jan22 = YearMonth.of(2022, 1) + val aug22 = YearMonth.of(2022, 8) + val des22 = YearMonth.of(2022, 12) + + @Test + fun `Endring i endret utbetaling andel - skal ha endret periode hvis årsak er endret`() { + val barn = lagPerson(type = PersonType.BARN) + val forrigeEndretAndel = lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.ETTERBETALING_3ÅR, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + ) + + val nåværendeEndretAndel = forrigeEndretAndel.copy(årsak = Årsak.ALLEREDE_UTBETALT) + + val perioderMedEndring = EndringIEndretUtbetalingAndelUtil.lagEndringIEndretUtbetalingAndelTidslinje( + forrigeEndretAndeler = listOf(forrigeEndretAndel), + nåværendeEndretAndeler = listOf(nåværendeEndretAndel), + ).perioder().filter { it.innhold == true } + + assertEquals(1, perioderMedEndring.size) + assertEquals(jan22, perioderMedEndring.single().fraOgMed.tilYearMonth()) + assertEquals(aug22, perioderMedEndring.single().tilOgMed.tilYearMonth()) + + val endringstidspunkt = EndringIEndretUtbetalingAndelUtil.utledEndringstidspunktForEndretUtbetalingAndel( + forrigeEndretAndeler = listOf(forrigeEndretAndel), + nåværendeEndretAndeler = listOf(nåværendeEndretAndel), + ) + + assertEquals(jan22, endringstidspunkt) + } + + @Test + fun `Endring i endret utbetaling andel - skal ikke ha noen endrede perioder hvis kun prosent er endret`() { + val barn = lagPerson(type = PersonType.BARN) + val forrigeEndretAndel = lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.DELT_BOSTED, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + avtaletidspunktDeltBosted = jan22.førsteDagIInneværendeMåned(), + ) + + val nåværendeEndretAndel = forrigeEndretAndel.copy(prosent = BigDecimal(100)) + + val perioderMedEndring = EndringIEndretUtbetalingAndelUtil.lagEndringIEndretUtbetalingAndelTidslinje( + forrigeEndretAndeler = listOf(forrigeEndretAndel), + nåværendeEndretAndeler = listOf(nåværendeEndretAndel), + ).perioder().filter { it.innhold == true } + + assertTrue(perioderMedEndring.isEmpty()) + + val endringstidspunkt = EndringIEndretUtbetalingAndelUtil.utledEndringstidspunktForEndretUtbetalingAndel( + forrigeEndretAndeler = listOf(forrigeEndretAndel), + nåværendeEndretAndeler = listOf(nåværendeEndretAndel), + ) + + Assertions.assertNull(endringstidspunkt) + } + + @Test + fun `Endring i endret utbetaling andel - skal ikke ha noen endrede perioder hvis eneste endring er at perioden blir lenger`() { + val barn = lagPerson(type = PersonType.BARN) + val forrigeEndretAndel = lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.DELT_BOSTED, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + avtaletidspunktDeltBosted = jan22.førsteDagIInneværendeMåned(), + ) + + val nåværendeEndretAndel = forrigeEndretAndel.copy(tom = des22) + + val perioderMedEndring = EndringIEndretUtbetalingAndelUtil.lagEndringIEndretUtbetalingAndelTidslinje( + forrigeEndretAndeler = listOf(forrigeEndretAndel), + nåværendeEndretAndeler = listOf(nåværendeEndretAndel), + ).perioder().filter { it.innhold == true } + + assertTrue(perioderMedEndring.isEmpty()) + + val endringstidspunkt = EndringIEndretUtbetalingAndelUtil.utledEndringstidspunktForEndretUtbetalingAndel( + forrigeEndretAndeler = listOf(forrigeEndretAndel), + nåværendeEndretAndeler = listOf(nåværendeEndretAndel), + ) + + Assertions.assertNull(endringstidspunkt) + } + + @Test + fun `Endring i endret utbetaling andel - skal ikke ha noen endrede perioder hvis endringsperiode oppstår i nåværende behandling`() { + val barn = lagPerson(type = PersonType.BARN) + val nåværendeEndretAndel = lagEndretUtbetalingAndel( + person = barn, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.DELT_BOSTED, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + avtaletidspunktDeltBosted = jan22.førsteDagIInneværendeMåned(), + ) + + val perioderMedEndring = EndringIEndretUtbetalingAndelUtil.lagEndringIEndretUtbetalingAndelTidslinje( + forrigeEndretAndeler = emptyList(), + nåværendeEndretAndeler = listOf(nåværendeEndretAndel), + ).perioder().filter { it.innhold == true } + + assertTrue(perioderMedEndring.isEmpty()) + + val endringstidspunkt = EndringIEndretUtbetalingAndelUtil.utledEndringstidspunktForEndretUtbetalingAndel( + forrigeEndretAndeler = emptyList(), + nåværendeEndretAndeler = listOf(nåværendeEndretAndel), + ) + + Assertions.assertNull(endringstidspunkt) + } + + @Test + fun `Endring i endret utbetaling andel - skal returnere endret periode hvis et av to barn har endring på årsak`() { + val barn1 = lagPerson(type = PersonType.BARN) + val barn2 = lagPerson(type = PersonType.BARN) + + val forrigeEndretAndelBarn1 = lagEndretUtbetalingAndel( + person = barn1, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.DELT_BOSTED, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + avtaletidspunktDeltBosted = jan22.førsteDagIInneværendeMåned(), + ) + + val forrigeEndretAndelBarn2 = lagEndretUtbetalingAndel( + person = barn2, + prosent = BigDecimal.ZERO, + fom = jan22, + tom = aug22, + årsak = Årsak.ETTERBETALING_3ÅR, + søknadstidspunkt = des22.førsteDagIInneværendeMåned(), + ) + + val perioderMedEndring = EndringIEndretUtbetalingAndelUtil.lagEndringIEndretUtbetalingAndelTidslinje( + forrigeEndretAndeler = listOf(forrigeEndretAndelBarn1, forrigeEndretAndelBarn2), + nåværendeEndretAndeler = listOf( + forrigeEndretAndelBarn1, + forrigeEndretAndelBarn2.copy(årsak = Årsak.ALLEREDE_UTBETALT), + ), + ).perioder().filter { it.innhold == true } + + assertEquals(1, perioderMedEndring.size) + assertEquals(jan22, perioderMedEndring.single().fraOgMed.tilYearMonth()) + assertEquals(aug22, perioderMedEndring.single().tilOgMed.tilYearMonth()) + + val endringstidspunkt = EndringIEndretUtbetalingAndelUtil.utledEndringstidspunktForEndretUtbetalingAndel( + forrigeEndretAndeler = listOf(forrigeEndretAndelBarn1, forrigeEndretAndelBarn2), + nåværendeEndretAndeler = listOf( + forrigeEndretAndelBarn1, + forrigeEndretAndelBarn2.copy(årsak = Årsak.ALLEREDE_UTBETALT), + ), + ) + + assertEquals(jan22, endringstidspunkt) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIKompetanseUtilTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIKompetanseUtilTest.kt new file mode 100644 index 000000000..2144550ae --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIKompetanseUtilTest.kt @@ -0,0 +1,186 @@ +package no.nav.familie.ba.sak.kjerne.forrigebehandling + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.lagKompetanse +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.YearMonth + +class EndringIKompetanseUtilTest { + + private val barn1Aktør = randomAktør() + val jan22 = YearMonth.of(2022, 1) + val mai22 = YearMonth.of(2022, 5) + + @Test + fun `Endring i kompetanse - skal ikke returnere noen endrede perioder når ingenting endrer seg`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val forrigeKompetanse = + lagKompetanse( + behandlingId = forrigeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = "NO", + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + annenForeldersAktivitetsland = "PO", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = jan22, + tom = mai22, + ) + + val nåværendeKompetanse = forrigeKompetanse.copy().apply { behandlingId = nåværendeBehandling.id } + + val perioderMedEndring = EndringIKompetanseUtil.lagEndringIKompetanseTidslinje( + nåværendeKompetanser = listOf(nåværendeKompetanse), + forrigeKompetanser = listOf(forrigeKompetanse), + ).perioder().filter { it.innhold == true } + + Assertions.assertTrue(perioderMedEndring.isEmpty()) + + val endringstidspunkt = EndringIKompetanseUtil.utledEndringstidspunktForKompetanse( + nåværendeKompetanser = listOf(nåværendeKompetanse), + forrigeKompetanser = listOf(forrigeKompetanse), + ) + + Assertions.assertNull(endringstidspunkt) + } + + @Test + fun `Endring i kompetanse - skal returnere endret periode når søkers aktivitetsland endrer seg`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val forrigeKompetanse = + lagKompetanse( + behandlingId = forrigeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = "NO", + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + annenForeldersAktivitetsland = "PO", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = jan22, + tom = mai22, + ) + + val nåværendeKompetanse = + forrigeKompetanse.copy(søkersAktivitetsland = "DK").apply { behandlingId = nåværendeBehandling.id } + + val perioderMedEndring = EndringIKompetanseUtil.lagEndringIKompetanseTidslinje( + nåværendeKompetanser = listOf( + nåværendeKompetanse, + ), + forrigeKompetanser = listOf(forrigeKompetanse), + ).perioder().filter { it.innhold == true } + + Assertions.assertEquals(1, perioderMedEndring.size) + Assertions.assertEquals(jan22, perioderMedEndring.single().fraOgMed.tilYearMonth()) + Assertions.assertEquals(mai22, perioderMedEndring.single().tilOgMed.tilYearMonth()) + + val endringstidspunkt = EndringIKompetanseUtil.utledEndringstidspunktForKompetanse( + nåværendeKompetanser = listOf( + nåværendeKompetanse, + ), + forrigeKompetanser = listOf(forrigeKompetanse), + ) + + Assertions.assertEquals(jan22, endringstidspunkt) + } + + @Test + fun `Endring i kompetanse - skal ikke lage endret periode når det kun blir lagt på en ekstra kompetanseperiode`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val forrigeKompetanse = + lagKompetanse( + behandlingId = forrigeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = "NO", + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + annenForeldersAktivitetsland = "PO", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = YearMonth.now().minusMonths(6), + tom = null, + ) + + val nåværendeKompetanse = forrigeKompetanse.copy(fom = YearMonth.now().minusMonths(10)) + .apply { behandlingId = nåværendeBehandling.id } + + val perioderMedEndring = EndringIKompetanseUtil.lagEndringIKompetanseTidslinje( + nåværendeKompetanser = listOf( + nåværendeKompetanse, + ), + forrigeKompetanser = listOf(forrigeKompetanse), + ).perioder().filter { it.innhold == true } + + Assertions.assertTrue(perioderMedEndring.isEmpty()) + + val endringstidspunkt = EndringIKompetanseUtil.utledEndringstidspunktForKompetanse( + nåværendeKompetanser = listOf( + nåværendeKompetanse, + ), + forrigeKompetanser = listOf(forrigeKompetanse), + ) + + Assertions.assertNull(endringstidspunkt) + } + + @Test + fun `Endring i kompetanse - skal ikke lage endret periode når forrige kompetanse ikke er utfylt (pga migrering+ evt autovedtak)`() { + val forrigeBehandling = lagBehandling() + val nåværendeBehandling = lagBehandling() + val forrigeKompetanse = + lagKompetanse( + behandlingId = forrigeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = null, + søkersAktivitet = null, + søkersAktivitetsland = null, + annenForeldersAktivitet = null, + annenForeldersAktivitetsland = null, + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = YearMonth.now().minusMonths(6), + tom = null, + ) + + val nåværendeKompetanse = + lagKompetanse( + behandlingId = nåværendeBehandling.id, + barnAktører = setOf(barn1Aktør), + barnetsBostedsland = "NO", + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + annenForeldersAktivitetsland = "PO", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + fom = YearMonth.now().minusMonths(6), + tom = null, + ) + + val perioderMedEndring = EndringIKompetanseUtil.lagEndringIKompetanseTidslinje( + nåværendeKompetanser = listOf( + nåværendeKompetanse, + ), + forrigeKompetanser = listOf(forrigeKompetanse), + ).perioder().filter { it.innhold == true } + + Assertions.assertTrue(perioderMedEndring.isEmpty()) + + val endringstidspunkt = EndringIKompetanseUtil.utledEndringstidspunktForKompetanse( + nåværendeKompetanser = listOf( + nåværendeKompetanse, + ), + forrigeKompetanser = listOf(forrigeKompetanse), + ) + + Assertions.assertNull(endringstidspunkt) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIUtbetalingUtilTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIUtbetalingUtilTest.kt new file mode 100644 index 000000000..c1b656921 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIUtbetalingUtilTest.kt @@ -0,0 +1,237 @@ +package no.nav.familie.ba.sak.kjerne.forrigebehandling + +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.YearMonth + +class EndringIUtbetalingUtilTest { + + val jan22 = YearMonth.of(2022, 1) + val mai22 = YearMonth.of(2022, 5) + val aug22 = YearMonth.of(2022, 8) + val sep22 = YearMonth.of(2022, 9) + val des22 = YearMonth.of(2022, 12) + + @Test + fun `Endring i beløp - Skal returnere periode med endring når ny andel med beløp større enn 0 er lagt til`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = des22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val perioderMedEndring = EndringIUtbetalingUtil.lagEndringIUtbetalingTidslinje( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + ).perioder().filter { it.innhold == true } + + Assertions.assertEquals(1, perioderMedEndring.size) + Assertions.assertEquals(sep22, perioderMedEndring.single().fraOgMed.tilYearMonth()) + Assertions.assertEquals(des22, perioderMedEndring.single().tilOgMed.tilYearMonth()) + + val endringstidspunkt = EndringIUtbetalingUtil.utledEndringstidspunktForUtbetalingsbeløp( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + ) + + Assertions.assertEquals(sep22, endringstidspunkt) + } + + @Test + fun `Endring i beløp - Skal ikke gi noen perioder med endring hvis andelene er helt like forrige behandling og nå`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val andeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val perioderMedEndring = EndringIUtbetalingUtil.lagEndringIUtbetalingTidslinje( + nåværendeAndeler = andeler, + forrigeAndeler = andeler, + ).perioder().filter { it.innhold == true } + + Assertions.assertTrue(perioderMedEndring.isEmpty()) + + val endringstidspunkt = EndringIUtbetalingUtil.utledEndringstidspunktForUtbetalingsbeløp( + nåværendeAndeler = andeler, + forrigeAndeler = andeler, + ) + + Assertions.assertNull(endringstidspunkt) + } + + @Test + fun `Endring i beløp - Skal returnere periode med endring hvis utvidet ikke er endret men småbarnstillegg kun er lagt på`() { + val søker = lagPerson(type = PersonType.SØKER).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val forrigeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = søker, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + val nåværendeAndeler = listOf( + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = søker, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ), + lagAndelTilkjentYtelse( + fom = mai22, + tom = aug22, + beløp = 630, + aktør = søker, + ytelseType = YtelseType.SMÅBARNSTILLEGG, + ), + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ), + ) + + val perioderMedEndring = EndringIUtbetalingUtil.lagEndringIUtbetalingTidslinje( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + ).perioder().filter { it.innhold == true } + + Assertions.assertEquals(1, perioderMedEndring.size) + Assertions.assertEquals(mai22, perioderMedEndring.single().fraOgMed.tilYearMonth()) + Assertions.assertEquals(aug22, perioderMedEndring.single().tilOgMed.tilYearMonth()) + + val endringstidspunkt = EndringIUtbetalingUtil.utledEndringstidspunktForUtbetalingsbeløp( + nåværendeAndeler = nåværendeAndeler, + forrigeAndeler = forrigeAndeler, + ) + + Assertions.assertEquals(mai22, endringstidspunkt) + } + + @Test + fun `Endring i beløp - Skal returnere periode med endring hvis andel med beløp større enn 0 er fjernet`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val andelBarn1 = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn1Aktør, + ) + val andelBarn2 = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ) + + val perioderMedEndring = EndringIUtbetalingUtil.lagEndringIUtbetalingTidslinje( + nåværendeAndeler = listOf(andelBarn2), + forrigeAndeler = listOf(andelBarn2, andelBarn1), + ).perioder().filter { it.innhold == true } + + Assertions.assertEquals(1, perioderMedEndring.size) + Assertions.assertEquals(jan22, perioderMedEndring.single().fraOgMed.tilYearMonth()) + Assertions.assertEquals(aug22, perioderMedEndring.single().tilOgMed.tilYearMonth()) + + val endringstidspunkt = EndringIUtbetalingUtil.utledEndringstidspunktForUtbetalingsbeløp( + nåværendeAndeler = listOf(andelBarn2), + forrigeAndeler = listOf(andelBarn2, andelBarn1), + ) + + Assertions.assertEquals(jan22, endringstidspunkt) + } + + @Test + fun `Endring i beløp - Skal ikke returnere periode med endring hvis andel med 0 i beløp er fjernet`() { + val barn1Aktør = lagPerson(type = PersonType.BARN).aktør + val barn2Aktør = lagPerson(type = PersonType.BARN).aktør + + val andelBarn1 = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 0, + aktør = barn1Aktør, + ) + val andelBarn2 = + lagAndelTilkjentYtelse( + fom = jan22, + tom = aug22, + beløp = 1054, + aktør = barn2Aktør, + ) + + val perioderMedEndring = EndringIUtbetalingUtil.lagEndringIUtbetalingTidslinje( + nåværendeAndeler = listOf(andelBarn2), + forrigeAndeler = listOf(andelBarn2, andelBarn1), + ).perioder().filter { it.innhold == true } + + Assertions.assertTrue(perioderMedEndring.isEmpty()) + + val endringstidspunkt = EndringIUtbetalingUtil.utledEndringstidspunktForUtbetalingsbeløp( + nåværendeAndeler = listOf(andelBarn2), + forrigeAndeler = listOf(andelBarn2, andelBarn1), + ) + + Assertions.assertNull(endringstidspunkt) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIVilk\303\245rsvurderingUtilTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIVilk\303\245rsvurderingUtilTest.kt" new file mode 100644 index 000000000..b72921fd8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/forrigebehandling/EndringIVilk\303\245rsvurderingUtilTest.kt" @@ -0,0 +1,337 @@ +package no.nav.familie.ba.sak.kjerne.forrigebehandling + +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Uendelighet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.YearMonth + +class EndringIVilkårsvurderingUtilTest { + + private val jan22 = YearMonth.of(2022, 1) + private val feb22 = YearMonth.of(2022, 2) + private val mai22 = YearMonth.of(2022, 5) + private val jun22 = YearMonth.of(2022, 6) + + @Test + fun `Endring i vilkårsvurdering - skal ikke lage periode med endring dersom vilkårresultatene er helt like`() { + val fødselsdato = LocalDate.of(2015, 1, 1) + val vilkårResultater = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = LocalDate.of(2020, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.of(2020, 1, 2), + periodeTom = LocalDate.of(2022, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val aktør = randomAktør() + + val person = lagPerson(aktør = aktør, type = PersonType.BARN, fødselsdato = fødselsdato) + + val perioderMedEndring = EndringIVilkårsvurderingUtil.lagEndringIVilkårsvurderingTidslinje( + nåværendePersonResultater = setOf(lagPersonResultatFraVilkårResultater(vilkårResultater, aktør)), + forrigePersonResultater = setOf(lagPersonResultatFraVilkårResultater(vilkårResultater, aktør)), + personerIBehandling = setOf(person), + personerIForrigeBehandling = setOf(person), + ).perioder().filter { it.innhold == true } + + Assertions.assertTrue(perioderMedEndring.isEmpty()) + + val endringstidspunkt = EndringIVilkårsvurderingUtil.utledEndringstidspunktForVilkårsvurdering( + nåværendePersonResultat = setOf(lagPersonResultatFraVilkårResultater(vilkårResultater, aktør)), + forrigePersonResultat = setOf(lagPersonResultatFraVilkårResultater(vilkårResultater, aktør)), + personerIBehandling = setOf(person), + personerIForrigeBehandling = setOf(person), + ) + + Assertions.assertNull(endringstidspunkt) + } + + @Test + fun `Endring i vilkårsvurdering - skal returnere periode med endring dersom det har vært endringer i regelverk`() { + val fødselsdato = jan22.førsteDagIInneværendeMåned() + val nåværendeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = mai22.sisteDagIInneværendeMåned(), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val forrigeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = mai22.sisteDagIInneværendeMåned(), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.EØS_FORORDNINGEN, + ), + ) + + val aktør = randomAktør() + val person = lagPerson(aktør = aktør, type = PersonType.BARN, fødselsdato = fødselsdato) + + val perioderMedEndring = EndringIVilkårsvurderingUtil.lagEndringIVilkårsvurderingTidslinje( + nåværendePersonResultater = setOf(lagPersonResultatFraVilkårResultater(nåværendeVilkårResultat, aktør)), + forrigePersonResultater = setOf(lagPersonResultatFraVilkårResultater(forrigeVilkårResultat, aktør)), + personerIBehandling = setOf(person), + personerIForrigeBehandling = setOf(person), + ).perioder().filter { it.innhold == true } + + Assertions.assertEquals(1, perioderMedEndring.size) + Assertions.assertEquals(feb22, perioderMedEndring.single().fraOgMed.tilYearMonth()) + Assertions.assertEquals(mai22, perioderMedEndring.single().tilOgMed.tilYearMonth()) + + val endringstidspunkt = EndringIVilkårsvurderingUtil.utledEndringstidspunktForVilkårsvurdering( + nåværendePersonResultat = setOf(lagPersonResultatFraVilkårResultater(nåværendeVilkårResultat, aktør)), + forrigePersonResultat = setOf(lagPersonResultatFraVilkårResultater(forrigeVilkårResultat, aktør)), + personerIBehandling = setOf(person), + personerIForrigeBehandling = setOf(person), + ) + + Assertions.assertEquals(feb22, endringstidspunkt) + } + + @Test + fun `Endring i vilkårsvurdering - skal returnere periode med endring dersom det har oppstått splitt i vilkårsvurderingen`() { + val fødselsdato = jan22.førsteDagIInneværendeMåned() + val forrigeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = null, + begrunnelse = "", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf(), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val nåværendeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = mai22.atDay(7), + begrunnelse = "", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf(), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = mai22.atDay(8), + periodeTom = null, + begrunnelse = "", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf(), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val aktør = randomAktør() + val person = lagPerson(aktør = aktør, type = PersonType.BARN, fødselsdato = fødselsdato) + + val perioderMedEndring = EndringIVilkårsvurderingUtil.lagEndringIVilkårsvurderingTidslinje( + nåværendePersonResultater = setOf(lagPersonResultatFraVilkårResultater(nåværendeVilkårResultat, aktør)), + forrigePersonResultater = setOf(lagPersonResultatFraVilkårResultater(forrigeVilkårResultat, aktør)), + personerIBehandling = setOf(person), + personerIForrigeBehandling = setOf(person), + ).perioder().filter { it.innhold == true } + + Assertions.assertEquals(1, perioderMedEndring.size) + Assertions.assertEquals(jun22, perioderMedEndring.single().fraOgMed.tilYearMonth()) + Assertions.assertEquals(Uendelighet.FREMTID, perioderMedEndring.single().tilOgMed.uendelighet) + + val endringstidspunkt = EndringIVilkårsvurderingUtil.utledEndringstidspunktForVilkårsvurdering( + nåværendePersonResultat = setOf(lagPersonResultatFraVilkårResultater(nåværendeVilkårResultat, aktør)), + forrigePersonResultat = setOf(lagPersonResultatFraVilkårResultater(forrigeVilkårResultat, aktør)), + personerIBehandling = setOf(person), + personerIForrigeBehandling = setOf(person), + ) + + Assertions.assertEquals(jun22, endringstidspunkt) + } + + @Test + fun `Endring i vilkårsvurdering - skal ikke lage periode med endring hvis det kun er opphørt`() { + val fødselsdato = LocalDate.of(2015, 1, 1) + val nåværendeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = LocalDate.of(2020, 1, 1), + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val forrigeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = null, + begrunnelse = "begrunnelse", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ), + ) + + val aktør = randomAktør() + val person = lagPerson(aktør = aktør, type = PersonType.BARN, fødselsdato = fødselsdato) + + val perioderMedEndring = EndringIVilkårsvurderingUtil.lagEndringIVilkårsvurderingTidslinje( + nåværendePersonResultater = setOf(lagPersonResultatFraVilkårResultater(nåværendeVilkårResultat, aktør)), + forrigePersonResultater = setOf(lagPersonResultatFraVilkårResultater(forrigeVilkårResultat, aktør)), + personerIBehandling = setOf(person), + personerIForrigeBehandling = setOf(person), + ).perioder().filter { it.innhold == true } + + Assertions.assertTrue(perioderMedEndring.isEmpty()) + + val endringstidspunkt = EndringIVilkårsvurderingUtil.utledEndringstidspunktForVilkårsvurdering( + nåværendePersonResultat = setOf(lagPersonResultatFraVilkårResultater(nåværendeVilkårResultat, aktør)), + forrigePersonResultat = setOf(lagPersonResultatFraVilkårResultater(forrigeVilkårResultat, aktør)), + personerIBehandling = setOf(person), + personerIForrigeBehandling = setOf(person), + ) + + Assertions.assertNull(endringstidspunkt) + } + + @Test + fun `Endring i vilkårsvurdering - skal ikke lage periode med endring hvis eneste endring er å sette obligatoriske utdypende vilkårsvurderinger`() { + val fødselsdato = LocalDate.of(2015, 1, 1) + val forrigeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = null, + begrunnelse = "migrering", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf(), + vurderesEtter = Regelverk.EØS_FORORDNINGEN, + ), + ) + + val nåværendeVilkårResultat = setOf( + VilkårResultat( + personResultat = null, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = fødselsdato, + periodeTom = null, + begrunnelse = "migrering", + sistEndretIBehandlingId = 0, + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE, + ), + vurderesEtter = Regelverk.EØS_FORORDNINGEN, + ), + ) + + val aktør = randomAktør() + val person = lagPerson(aktør = aktør, type = PersonType.BARN, fødselsdato = fødselsdato) + + val perioderMedEndring = EndringIVilkårsvurderingUtil.lagEndringIVilkårsvurderingTidslinje( + nåværendePersonResultater = setOf(lagPersonResultatFraVilkårResultater(nåværendeVilkårResultat, aktør)), + forrigePersonResultater = setOf(lagPersonResultatFraVilkårResultater(forrigeVilkårResultat, aktør)), + personerIBehandling = setOf(person), + personerIForrigeBehandling = setOf(person), + ).perioder().filter { it.innhold == true } + + Assertions.assertTrue(perioderMedEndring.isEmpty()) + + val endringstidspunkt = EndringIVilkårsvurderingUtil.utledEndringstidspunktForVilkårsvurdering( + nåværendePersonResultat = setOf(lagPersonResultatFraVilkårResultater(nåværendeVilkårResultat, aktør)), + forrigePersonResultat = setOf(lagPersonResultatFraVilkårResultater(forrigeVilkårResultat, aktør)), + personerIBehandling = setOf(person), + personerIForrigeBehandling = setOf(person), + ) + + Assertions.assertNull(endringstidspunkt) + } + + private fun lagPersonResultatFraVilkårResultater(vilkårResultater: Set, aktør: Aktør): PersonResultat { + val vilkårsvurdering = lagVilkårsvurdering(behandling = lagBehandling(), resultat = Resultat.OPPFYLT, søkerAktør = randomAktør()) + val personResultat = PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = aktør) + + personResultat.setSortedVilkårResultater(vilkårResultater) + + return personResultat + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/BostedsadresseTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/BostedsadresseTest.kt new file mode 100644 index 000000000..2559fc519 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/BostedsadresseTest.kt @@ -0,0 +1,141 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrBostedsadresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrBostedsadresse.Companion.sisteAdresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrMatrikkeladresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.filtrerGjeldendeNå +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.vurderOmPersonerBorSammen +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.Matrikkeladresse +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.time.LocalDate + +internal class BostedsadresseTest { + + @Test + fun `Skal adresse med mest nylig fom-dato`() { + val adresse = GrMatrikkeladresse( + matrikkelId = null, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ) + val adresseMedNullFom = adresse.copy().apply { periode = DatoIntervallEntitet(fom = null) } + val adresseMedEldreDato = adresse.copy().apply { periode = DatoIntervallEntitet(fom = LocalDate.now().minusYears(3)) } + val adresseMedNyereDato = adresse.copy().apply { periode = DatoIntervallEntitet(fom = LocalDate.now().minusYears(1)) } + val adresserTilSortering = mutableListOf(adresseMedEldreDato, adresseMedNyereDato, adresseMedNullFom) + assertEquals(adresseMedNyereDato, adresserTilSortering.sisteAdresse()) + } + + @Test + fun `Skal returnere adresse uten datoer når dette er eneste`() { + val adresse = GrMatrikkeladresse( + matrikkelId = null, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ).apply { + periode = DatoIntervallEntitet(fom = null) + } as GrBostedsadresse + assertEquals(adresse, mutableListOf(adresse).sisteAdresse()) + } + + @Test + fun `Skal kaste feil hvis det finnes flere adresser uten datoer`() { + val adresse1 = GrMatrikkeladresse( + matrikkelId = null, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ).apply { + periode = DatoIntervallEntitet(fom = null) + } as GrBostedsadresse + val adresse2 = GrMatrikkeladresse( + matrikkelId = null, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ).apply { + periode = DatoIntervallEntitet(fom = null) + } as GrBostedsadresse + assertThrows { mutableListOf(adresse1, adresse2).sisteAdresse() } + } + + @Test + fun `Skal returnere at personer bor sammen når begge kun har felles adresse`() { + val p1 = lagPerson() + val p2 = lagPerson() + val fellesAdresse = Bostedsadresse( + angittFlyttedato = LocalDate.parse("2020-07-13"), + gyldigTilOgMed = null, + matrikkeladresse = Matrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ), + ) + val p1Adresser = listOf( + GrBostedsadresse.fraBostedsadresse( + person = p1, + bostedsadresse = fellesAdresse, + ), + ) + val p2Adresser = listOf( + GrBostedsadresse.fraBostedsadresse(person = p2, bostedsadresse = fellesAdresse), + ) + Assertions.assertTrue(vurderOmPersonerBorSammen(p1Adresser, p2Adresser)) + } + + @Test + fun `Skal returnere at personer bor sammen når en av personene har flere adresser`() { + val p1 = lagPerson() + val p2 = lagPerson() + val fellesAdresse = Bostedsadresse( + angittFlyttedato = LocalDate.parse("2020-07-13"), + gyldigTilOgMed = null, + matrikkeladresse = Matrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ), + ) + val p1Adresser = listOf( + GrBostedsadresse.fraBostedsadresse( + person = p1, + bostedsadresse = fellesAdresse, + ), + ) + val p2Adresser = listOf( + GrBostedsadresse.fraBostedsadresse(person = p2, bostedsadresse = fellesAdresse), + GrBostedsadresse.fraBostedsadresse( + person = p2, + bostedsadresse = Bostedsadresse( + angittFlyttedato = LocalDate.parse("2021-08-09"), + gyldigTilOgMed = null, + matrikkeladresse = Matrikkeladresse( + matrikkelId = 145L, + bruksenhetsnummer = "H402", + tilleggsnavn = "ekstra", + postnummer = "0333", + kommunenummer = "3456", + ), + ), + ), + ) + Assertions.assertTrue(vurderOmPersonerBorSammen(p1Adresser.filtrerGjeldendeNå(), p2Adresser.filtrerGjeldendeNå())) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersongrunnlagServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersongrunnlagServiceTest.kt new file mode 100644 index 000000000..9192871d9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersongrunnlagServiceTest.kt @@ -0,0 +1,243 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.kontrakter.felles.PersonIdent +import org.assertj.core.api.Assertions +import org.hamcrest.MatcherAssert.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.time.LocalDate +import java.time.YearMonth +import org.hamcrest.CoreMatchers.`is` as Is + +class PersongrunnlagServiceTest { + val personidentService = mockk() + val andelTilkjentYtelseRepository = mockk() + val personopplysningerService = mockk() + val personopplysningGrunnlagRepository = mockk() + val loggService = mockk() + val vilkårsvurderingService = mockk() + + val persongrunnlagService = spyk( + PersongrunnlagService( + personopplysningGrunnlagRepository = personopplysningGrunnlagRepository, + statsborgerskapService = mockk(), + arbeidsfordelingService = mockk(relaxed = true), + personopplysningerService = personopplysningerService, + personidentService = personidentService, + saksstatistikkEventPublisher = mockk(relaxed = true), + behandlingHentOgPersisterService = mockk(), + andelTilkjentYtelseRepository = andelTilkjentYtelseRepository, + loggService = loggService, + arbeidsforholdService = mockk(), + vilkårsvurderingService = vilkårsvurderingService, + ), + ) + + @Test + fun `Skal sende med barna fra forrige behandling ved førstegangsbehandling nummer to`() { + val søker = lagPerson() + val barnFraForrigeBehandling = lagPerson(type = PersonType.BARN) + val barn = lagPerson(type = PersonType.BARN) + + val barnFnr = barn.aktør.aktivFødselsnummer() + val søkerFnr = søker.aktør.aktivFødselsnummer() + + val forrigeBehandling = lagBehandling(behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING) + val behandling = lagBehandling(behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING) + + val forrigeBehandlingPersongrunnlag = + lagTestPersonopplysningGrunnlag( + behandlingId = forrigeBehandling.id, + personer = arrayOf(søker, barnFraForrigeBehandling), + ) + + val søknadDTO = lagSøknadDTO( + søkerIdent = søkerFnr, + barnasIdenter = listOf(barnFnr), + ) + + every { personidentService.hentOgLagreAktør(søkerFnr, true) } returns søker.aktør + every { personidentService.hentOgLagreAktør(barnFnr, true) } returns barn.aktør + + every { persongrunnlagService.hentAktiv(forrigeBehandling.id) } returns forrigeBehandlingPersongrunnlag + + every { + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandlingOgBarn( + forrigeBehandling.id, + barnFraForrigeBehandling.aktør, + ) + } returns listOf(lagAndelTilkjentYtelse(fom = YearMonth.now(), tom = YearMonth.now())) + + every { + persongrunnlagService.hentOgLagreSøkerOgBarnINyttGrunnlag( + any(), + any(), + any(), + any(), + any(), + ) + } returns PersonopplysningGrunnlag(behandlingId = behandling.id) + + persongrunnlagService.registrerBarnFraSøknad( + søknadDTO = søknadDTO, + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandling, + ) + verify(exactly = 1) { + persongrunnlagService.hentOgLagreSøkerOgBarnINyttGrunnlag( + aktør = søker.aktør, + barnFraInneværendeBehandling = listOf(barn.aktør), + barnFraForrigeBehandling = listOf(barnFraForrigeBehandling.aktør), + behandling = behandling, + målform = søknadDTO.søkerMedOpplysninger.målform, + ) + } + } + + @Test + fun `hentOgLagreSøkerOgBarnINyttGrunnlag skal på inst- og EM-saker kun lagre èn instans av barnet, med personType BARN`() { + val barnet = lagPerson() + val behandlinger = listOf(FagsakType.INSTITUSJON, FagsakType.BARN_ENSLIG_MINDREÅRIG).map { fagsakType -> + lagBehandling(fagsak = defaultFagsak().copy(type = fagsakType)) + } + behandlinger.forEach { behandling -> + val nyttGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id) + + every { + persongrunnlagService.lagreOgDeaktiverGammel(any()) + } returns nyttGrunnlag + + every { + personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(barnet.aktør) + } returns PersonInfo(barnet.fødselsdato, barnet.navn, barnet.kjønn) + + every { personopplysningGrunnlagRepository.save(nyttGrunnlag) } returns nyttGrunnlag + + persongrunnlagService.hentOgLagreSøkerOgBarnINyttGrunnlag( + aktør = barnet.aktør, + barnFraInneværendeBehandling = listOf(barnet.aktør), + barnFraForrigeBehandling = listOf(barnet.aktør), + behandling = behandling, + målform = Målform.NB, + ).apply { + Assertions.assertThat(this.personer) + .hasSize(1) + .extracting("type") + .containsExactly(PersonType.BARN) + } + } + } + + @Test + fun `registrerManuellDødsfallPåPerson skal kaste feil dersom man registrer dødsfall dato før personen er født`() { + val dødsfallsDato = LocalDate.of(2020, 10, 10) + val person = lagPerson(fødselsdato = dødsfallsDato.plusMonths(10)) + val personFnr = person.aktør.aktivFødselsnummer() + val behandling = lagBehandling(behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + personer = arrayOf(person), + ) + + every { personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandling.id) } returns personopplysningGrunnlag + every { personidentService.hentAktør(personFnr) } returns person.aktør + + val funksjonellFeil = assertThrows { + persongrunnlagService.registrerManuellDødsfallPåPerson( + behandlingId = BehandlingId(behandling.id), + personIdent = PersonIdent(personFnr), + dødsfallDato = dødsfallsDato, + begrunnelse = "test", + ) + } + + assertThat(funksjonellFeil.melding, Is("Du kan ikke sette dødsfall dato til en dato som er før SØKER sin fødselsdato")) + } + + @Test + fun `registrerManuellDødsfallPåPerson skal kaste feil dersom man registrer dødsfall dato når personen allerede har dødsfallsdato registrert`() { + val dødsfallsDato = LocalDate.of(2020, 10, 10) + val person = lagPerson(fødselsdato = dødsfallsDato.minusMonths(10)).also { + it.dødsfall = Dødsfall(person = it, dødsfallDato = dødsfallsDato) + } + + val personFnr = person.aktør.aktivFødselsnummer() + val behandling = lagBehandling(behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + personer = arrayOf(person), + ) + + every { personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandling.id) } returns personopplysningGrunnlag + every { personidentService.hentAktør(personFnr) } returns person.aktør + + val funksjonellFeil = assertThrows { + persongrunnlagService.registrerManuellDødsfallPåPerson( + behandlingId = BehandlingId(behandling.id), + personIdent = PersonIdent(personFnr), + dødsfallDato = dødsfallsDato, + begrunnelse = "test", + ) + } + + assertThat(funksjonellFeil.melding, Is("Dødsfall dato er allerede registrert på person med navn ${person.navn}")) + } + + @Test + fun `registrerManuellDødsfallPåPerson skal endre på vilkår og logge at manuelt dødsfalldato er registrert`() { + val dødsfallsDato = LocalDate.of(2020, 10, 10) + val person = lagPerson(fødselsdato = dødsfallsDato.minusMonths(10)) + + val personFnr = person.aktør.aktivFødselsnummer() + val behandling = lagBehandling(behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + personer = arrayOf(person), + ) + + every { personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandling.id) } returns personopplysningGrunnlag + every { personidentService.hentAktør(personFnr) } returns person.aktør + every { loggService.loggManueltRegistrertDødsfallDato(any(), any(), "test") } returns mockk() + every { vilkårsvurderingService.oppdaterVilkårVedDødsfall(any(), any(), any()) } just runs + + persongrunnlagService.registrerManuellDødsfallPåPerson( + behandlingId = BehandlingId(behandling.id), + personIdent = PersonIdent(personFnr), + dødsfallDato = dødsfallsDato, + begrunnelse = "test", + ) + + verify(exactly = 1) { personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandling.id) } + verify(exactly = 1) { personidentService.hentAktør(personFnr) } + verify(exactly = 1) { loggService.loggManueltRegistrertDødsfallDato(any(), any(), "test") } + verify(exactly = 1) { vilkårsvurderingService.oppdaterVilkårVedDødsfall(any(), any(), any()) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersongrunnlagTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersongrunnlagTest.kt new file mode 100644 index 000000000..e22354bee --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersongrunnlagTest.kt @@ -0,0 +1,50 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.randomFnr +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class PersongrunnlagTest { + + val persongrunnlagService = mockk() + + @Test + fun `Returnerer nytt barn fra personopplysningsgrunnlag`() { + val søker = randomFnr() + val barn = randomFnr() + val nyttbarn = randomFnr() + + val forrigeBehandling = lagBehandling() + val forrigeGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = forrigeBehandling.id, + søkerPersonIdent = søker, + barnasIdenter = listOf(barn), + ) + + val behandling = lagBehandling() + val grunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søker, + barnasIdenter = listOf(barn, nyttbarn), + ) + + every { persongrunnlagService.hentAktiv(forrigeBehandling.id) } returns forrigeGrunnlag + every { persongrunnlagService.hentAktivThrows(behandling.id) } returns grunnlag + every { persongrunnlagService.finnNyeBarn(any(), any()) } answers { callOriginal() } + + val nye = persongrunnlagService.finnNyeBarn(forrigeBehandling = forrigeBehandling, behandling = behandling) + Assertions.assertEquals(nyttbarn, nye.singleOrNull()!!.aktør.aktivFødselsnummer()) + } + + @Test + fun `Returnerer barnet som 'søker' når grunnlag kun består av ett barn (enslig mindreårig eller institusjonsbarn)`() { + val barnet = lagPerson(type = PersonType.BARN) + val persongrunnlag = lagTestPersonopplysningGrunnlag(1L, barnet) + Assertions.assertEquals(barnet, persongrunnlag.søker) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/StatsborgerskapServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/StatsborgerskapServiceTest.kt new file mode 100644 index 000000000..59c3ecbcb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/StatsborgerskapServiceTest.kt @@ -0,0 +1,245 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import io.mockk.mockk +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.config.IntegrasjonClientMock +import no.nav.familie.ba.sak.config.IntegrasjonClientMock.Companion.FOM_1990 +import no.nav.familie.ba.sak.config.IntegrasjonClientMock.Companion.FOM_2004 +import no.nav.familie.ba.sak.config.IntegrasjonClientMock.Companion.TOM_2010 +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.GrStatsborgerskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.StatsborgerskapService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.finnNåværendeMedlemskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.finnSterkesteMedlemskap +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.LocalDate + +internal class StatsborgerskapServiceTest { + + private val integrasjonClient = mockk() + + private lateinit var statsborgerskapService: StatsborgerskapService + + @BeforeEach + fun setUp() { + statsborgerskapService = StatsborgerskapService(integrasjonClient) + IntegrasjonClientMock.initEuKodeverk(integrasjonClient) + } + + @Test + fun `Skal generere GrStatsborgerskap med flere perioder fordi Polen ble medlem av EØS`() { + val statsborgerskapMedGyldigFom = Statsborgerskap( + "POL", + bekreftelsesdato = null, + gyldigFraOgMed = FOM_1990, + gyldigTilOgMed = TOM_2010, + ) + + val grStatsborgerskap = statsborgerskapService.hentStatsborgerskapMedMedlemskap( + statsborgerskap = statsborgerskapMedGyldigFom, + person = lagPerson(), + ) + + assertEquals(2, grStatsborgerskap.size) + assertEquals(FOM_1990, grStatsborgerskap.sortedBy { it.gyldigPeriode?.fom }.first().gyldigPeriode?.fom) + val dagenFørPolenBleMedlemAvEØS = FOM_2004.minusDays(1) + assertEquals( + dagenFørPolenBleMedlemAvEØS, + grStatsborgerskap.sortedBy { it.gyldigPeriode?.fom }.first().gyldigPeriode?.tom, + ) + assertEquals( + Medlemskap.TREDJELANDSBORGER, + grStatsborgerskap.sortedBy { it.gyldigPeriode?.fom }.first().medlemskap, + ) + assertEquals(FOM_2004, grStatsborgerskap.sortedBy { it.gyldigPeriode?.fom }.last().gyldigPeriode?.fom) + assertEquals(Medlemskap.EØS, grStatsborgerskap.sortedBy { it.gyldigPeriode?.fom }.last().medlemskap) + } + + @Test + fun `Skal evaluere polske statsborgere med ukjent periode som EØS-borgere`() { + val statsborgerPolenUtenPeriode = Statsborgerskap( + "POL", + gyldigFraOgMed = null, + gyldigTilOgMed = null, + bekreftelsesdato = null, + ) + + val grStatsborgerskapUtenPeriode = statsborgerskapService.hentStatsborgerskapMedMedlemskap( + statsborgerskap = statsborgerPolenUtenPeriode, + person = lagPerson(), + ) + assertEquals(1, grStatsborgerskapUtenPeriode.size) + assertEquals(Medlemskap.EØS, grStatsborgerskapUtenPeriode.single().medlemskap) + assertTrue(grStatsborgerskapUtenPeriode.single().gjeldendeNå()) + } + + @Test + fun `Lovlig opphold - valider at alle gjeldende medlemskap blir returnert`() { + val person = lagPerson() + .also { + it.statsborgerskap = + mutableListOf( + GrStatsborgerskap( + gyldigPeriode = DatoIntervallEntitet(tom = null, fom = null), + landkode = "DNK", + medlemskap = Medlemskap.NORDEN, + person = it, + ), + GrStatsborgerskap( + gyldigPeriode = DatoIntervallEntitet( + tom = null, + fom = LocalDate.now().minusYears(1), + ), + landkode = "DEU", + medlemskap = Medlemskap.EØS, + person = it, + ), + GrStatsborgerskap( + gyldigPeriode = DatoIntervallEntitet( + tom = LocalDate.now().minusYears(2), + fom = LocalDate.now().minusYears(2), + ), + landkode = "POL", + medlemskap = Medlemskap.EØS, + person = it, + ), + ) + } + + val medlemskap = finnNåværendeMedlemskap(person.statsborgerskap) + + assertEquals(2, medlemskap.size) + assertEquals(Medlemskap.NORDEN, medlemskap[0]) + assertEquals(Medlemskap.EØS, medlemskap[1]) + } + + @Test + fun `Lovlig opphold - valider at sterkeste medlemskap blir returnert`() { + val medlemskapNorden = listOf(Medlemskap.TREDJELANDSBORGER, Medlemskap.NORDEN, Medlemskap.UKJENT) + val medlemskapUkjent = listOf(Medlemskap.UKJENT) + val medlemskapIngen = emptyList() + + assertEquals(Medlemskap.NORDEN, finnSterkesteMedlemskap(medlemskapNorden)) + assertEquals(Medlemskap.UKJENT, finnSterkesteMedlemskap(medlemskapUkjent)) + assertEquals(null, finnSterkesteMedlemskap(medlemskapIngen)) + } + + @Test + fun `Skal evaluere britiske statsborgere med ukjent periode som tredjelandsborgere`() { + val statsborgerStorbritanniaUtenPeriode = Statsborgerskap( + "GBR", + gyldigFraOgMed = null, + gyldigTilOgMed = null, + bekreftelsesdato = null, + ) + + val grStatsborgerskapUtenPeriode = statsborgerskapService.hentStatsborgerskapMedMedlemskap( + statsborgerskap = statsborgerStorbritanniaUtenPeriode, + person = lagPerson(), + ) + assertEquals(1, grStatsborgerskapUtenPeriode.size) + assertEquals(Medlemskap.TREDJELANDSBORGER, grStatsborgerskapUtenPeriode.single().medlemskap) + assertTrue(grStatsborgerskapUtenPeriode.single().gjeldendeNå()) + } + + @Test + fun `Skal evaluere britiske statsborgere etter brexit som tredjelandsborgere`() { + val statsborgerStorbritanniaMedPeriodeEtterBrexit = Statsborgerskap( + "GBR", + gyldigFraOgMed = LocalDate.of(2022, 3, 1), + gyldigTilOgMed = LocalDate.now(), + bekreftelsesdato = null, + ) + val grStatsborgerskapEtterBrexit = statsborgerskapService.hentStatsborgerskapMedMedlemskap( + statsborgerskap = statsborgerStorbritanniaMedPeriodeEtterBrexit, + person = lagPerson(), + ) + assertEquals(1, grStatsborgerskapEtterBrexit.size) + assertEquals(Medlemskap.TREDJELANDSBORGER, grStatsborgerskapEtterBrexit.single().medlemskap) + assertTrue(grStatsborgerskapEtterBrexit.single().gjeldendeNå()) + } + + @Test + fun `Skal evaluere britiske statsborgere under Brexit som først EØS, nå tredjelandsborgere`() { + val datoFørBrexit = LocalDate.of(1989, 3, 1) + val datoEtterBrexit = LocalDate.of(2020, 5, 1) + + val statsborgerStorbritanniaMedPeriodeUnderBrexit = Statsborgerskap( + "GBR", + gyldigFraOgMed = datoFørBrexit, + gyldigTilOgMed = datoEtterBrexit, + bekreftelsesdato = null, + ) + val grStatsborgerskapUnderBrexit = statsborgerskapService.hentStatsborgerskapMedMedlemskap( + statsborgerskap = statsborgerStorbritanniaMedPeriodeUnderBrexit, + person = lagPerson(), + ) + assertEquals(2, grStatsborgerskapUnderBrexit.size) + assertEquals(datoFørBrexit, grStatsborgerskapUnderBrexit.first().gyldigPeriode?.fom) + assertEquals(TOM_2010, grStatsborgerskapUnderBrexit.first().gyldigPeriode?.tom) + assertEquals(Medlemskap.EØS, grStatsborgerskapUnderBrexit.sortedBy { it.gyldigPeriode?.fom }.first().medlemskap) + assertEquals( + Medlemskap.TREDJELANDSBORGER, + grStatsborgerskapUnderBrexit.sortedBy { it.gyldigPeriode?.fom }.last().medlemskap, + ) + } + + @Test + fun `hentSterkesteMedlemskap - skal finne sterkeste medlemskap i statsborgerperioden`() { + val statsborgerStorbritannia = Statsborgerskap( + "GBR", + gyldigFraOgMed = LocalDate.of(1990, 4, 1), + gyldigTilOgMed = null, + bekreftelsesdato = null, + ) + val statsborgerPolen = Statsborgerskap( + "POL", + gyldigFraOgMed = LocalDate.of(1990, 4, 1), + gyldigTilOgMed = null, + bekreftelsesdato = null, + ) + val statsborgerSerbia = Statsborgerskap( + "SRB", + gyldigFraOgMed = LocalDate.of(1990, 4, 1), + gyldigTilOgMed = null, + bekreftelsesdato = null, + ) + val statsborgerNorge = Statsborgerskap( + "NOR", + gyldigFraOgMed = LocalDate.of(1990, 4, 1), + gyldigTilOgMed = null, + bekreftelsesdato = null, + ) + + assertEquals(Medlemskap.EØS, statsborgerskapService.hentSterkesteMedlemskap(statsborgerStorbritannia)) + assertEquals(Medlemskap.EØS, statsborgerskapService.hentSterkesteMedlemskap(statsborgerPolen)) + assertEquals(Medlemskap.TREDJELANDSBORGER, statsborgerskapService.hentSterkesteMedlemskap(statsborgerSerbia)) + assertEquals(Medlemskap.NORDEN, statsborgerskapService.hentSterkesteMedlemskap(statsborgerNorge)) + } + + @Test + fun `hentSterkesteMedlemskap - om statsborgerperiode er ukjent vurderer vi basert på dagens medlemskap`() { + val statsborgerStorbritanniaMedNullDatoer = Statsborgerskap( + "GBR", + gyldigFraOgMed = null, + gyldigTilOgMed = null, + bekreftelsesdato = null, + ) + val statsborgerPolenMedNullDatoer = Statsborgerskap( + "POL", + gyldigFraOgMed = null, + gyldigTilOgMed = null, + bekreftelsesdato = null, + ) + assertEquals( + Medlemskap.TREDJELANDSBORGER, + statsborgerskapService.hentSterkesteMedlemskap(statsborgerStorbritanniaMedNullDatoer), + ) + assertEquals(Medlemskap.EØS, statsborgerskapService.hentSterkesteMedlemskap(statsborgerPolenMedNullDatoer)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/SamhandlerControllerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/SamhandlerControllerTest.kt new file mode 100644 index 000000000..e26f38cbd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/institusjon/SamhandlerControllerTest.kt @@ -0,0 +1,85 @@ +package no.nav.familie.ba.sak.kjerne.institusjon + +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.config.samhandlereInfoMock +import no.nav.familie.ba.sak.integrasjoner.samhandler.SamhandlerKlient +import no.nav.familie.kontrakter.ba.tss.SøkSamhandlerInfo +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.http.HttpStatus +import org.springframework.web.client.HttpClientErrorException + +internal class SamhandlerControllerTest { + + lateinit var samhandlerController: SamhandlerController + private val samhandlerKlientMock: SamhandlerKlient = mockk() + private val institusjonRepository: InstitusjonRepository = mockk() + + @BeforeEach + fun setUp() { + val institusjonService = InstitusjonService(mockk(), samhandlerKlientMock, institusjonRepository) + samhandlerController = SamhandlerController(institusjonService = institusjonService) + clearMocks(samhandlerKlientMock) + } + + @Test + fun `Skal hente samhandlerinformasjon fra orgnr `() { + every { samhandlerKlientMock.hentSamhandler(any()) } returns samhandlereInfoMock.first() + + val samhandlerInfo = samhandlerController.hentSamhandlerDataForOrganisasjon("ORGNR") + assertThat(samhandlerInfo.data).isNotNull() + assertThat(samhandlerInfo.data!!.tssEksternId).isEqualTo("80000999999") + } + + @Test + fun `Kaster feilmelding hvis det ikke fins organisasjon med gitt orgnr`() { + every { samhandlerKlientMock.hentSamhandler(any()) } throws HttpClientErrorException(HttpStatus.NOT_FOUND) + val feil = assertThrows { + samhandlerController.hentSamhandlerDataForOrganisasjon("123456789") + } + assertThat(feil.message).isEqualTo("Finner ikke institusjon. Kontakt NØS for å opprette TSS-ident.") + } + + @Test + fun `Søk etter samhandlere skal returnere samhandlere på navn og ikke hente flere hvis det ikke finnes flere samhandlere`() { + every { samhandlerKlientMock.søkSamhandlere("BUFETAT", null, null, 0) } returns SøkSamhandlerInfo( + false, + samhandlereInfoMock, + ) + + val samhandlerInfo = + samhandlerController.søkSamhandlerinfoFraNavn(SøkSamhandlerInfoRequest("Bufetat", null, null)) + assertThat(samhandlerInfo.data).isNotNull() + assertThat(samhandlerInfo.data).hasSize(2) + assertThat(samhandlerInfo.data?.get(0)?.tssEksternId).isEqualTo("80000999999") + assertThat(samhandlerInfo.data?.get(1)?.tssEksternId).isEqualTo("80000888888") + verify(exactly = 1) { samhandlerKlientMock.søkSamhandlere(any(), any(), any(), any()) } + } + + @Test + fun `Søk etter samhandlere skal returnere samhandlere på navn og slå sammen resultatene fra alle sidene ved mer enn 1 side`() { + every { samhandlerKlientMock.søkSamhandlere("BUFETAT", null, null, 0) } returns SøkSamhandlerInfo( + true, + listOf(samhandlereInfoMock.get(0)), + ) + + every { samhandlerKlientMock.søkSamhandlere("BUFETAT", null, null, 1) } returns SøkSamhandlerInfo( + false, + listOf(samhandlereInfoMock.get(1)), + ) + + val samhandlerInfo = + samhandlerController.søkSamhandlerinfoFraNavn(SøkSamhandlerInfoRequest("Bufetat", null, null)) + assertThat(samhandlerInfo.data).isNotNull() + assertThat(samhandlerInfo.data).hasSize(2) + assertThat(samhandlerInfo.data?.get(0)?.tssEksternId).isEqualTo("80000999999") + assertThat(samhandlerInfo.data?.get(1)?.tssEksternId).isEqualTo("80000888888") + verify(exactly = 2) { samhandlerKlientMock.søkSamhandlere(any(), any(), any(), any()) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageServiceTest.kt new file mode 100644 index 000000000..3115db7fd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/klage/KlageServiceTest.kt @@ -0,0 +1,143 @@ +package no.nav.familie.ba.sak.kjerne.klage + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.kontrakter.felles.klage.KanIkkeOppretteRevurderingÅrsak +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class KlageServiceTest { + val fagsakService = mockk() + val behandlingHentOgPersisterService = mockk() + val stegService = mockk() + val klageService = KlageService( + fagsakService = fagsakService, + klageClient = mockk(), + integrasjonClient = mockk(), + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + stegService = stegService, + vedtakService = mockk(), + tilbakekrevingKlient = mockk(), + + ) + + @Nested + inner class KanOppretteRevurdering { + + @Test + internal fun `kan opprette revurdering hvis det finnes en ferdigstilt behandling`() { + every { fagsakService.hentPåFagsakId(any()) } returns Fagsak(aktør = mockk()) + every { behandlingHentOgPersisterService.erÅpenBehandlingPåFagsak(any()) } returns false + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(any()) } returns lagBehandling( + status = BehandlingStatus.AVSLUTTET, + ) + + val result = klageService.kanOppretteRevurdering(0L) + + Assertions.assertTrue(result.kanOpprettes) + Assertions.assertEquals(result.årsak, null) + } + + @Test + internal fun `kan ikke opprette revurdering hvis det finnes åpen behandling`() { + every { fagsakService.hentPåFagsakId(any()) } returns Fagsak(aktør = mockk()) + every { behandlingHentOgPersisterService.erÅpenBehandlingPåFagsak(any()) } returns true + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(any()) } returns lagBehandling( + status = BehandlingStatus.UTREDES, + ) + + val result = klageService.kanOppretteRevurdering(0L) + + Assertions.assertFalse(result.kanOpprettes) + Assertions.assertEquals(result.årsak, KanIkkeOppretteRevurderingÅrsak.ÅPEN_BEHANDLING) + } + + @Test + internal fun `kan ikke opprette revurdering hvis det ikke finnes noen behandlinger`() { + every { fagsakService.hentPåFagsakId(any()) } returns Fagsak(aktør = mockk()) + every { behandlingHentOgPersisterService.erÅpenBehandlingPåFagsak(any()) } returns false + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(any()) } returns null + + val result = klageService.kanOppretteRevurdering(0L) + + Assertions.assertFalse(result.kanOpprettes) + Assertions.assertEquals(result.årsak, KanIkkeOppretteRevurderingÅrsak.INGEN_BEHANDLING) + } + } + + @Nested + inner class OpprettRevurderingKlage { + + @Test + internal fun `kan opprette revurdering hvis det finnes en ferdigstilt behandling`() { + val aktør = randomAktør() + val fagsak = Fagsak(aktør = aktør) + val forrigeBehandling = lagBehandling( + behandlingKategori = BehandlingKategori.EØS, + underkategori = BehandlingUnderkategori.UTVIDET, + fagsak = fagsak, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak = BehandlingÅrsak.OMREGNING_SMÅBARNSTILLEGG, + status = BehandlingStatus.AVSLUTTET, + ) + + every { fagsakService.hentPåFagsakId(any()) } returns fagsak + every { behandlingHentOgPersisterService.erÅpenBehandlingPåFagsak(any()) } returns false + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(any()) } returns forrigeBehandling + + val nyBehandling = NyBehandling( + kategori = forrigeBehandling.kategori, + underkategori = forrigeBehandling.underkategori, + søkersIdent = forrigeBehandling.fagsak.aktør.aktivFødselsnummer(), + behandlingType = BehandlingType.REVURDERING, + behandlingÅrsak = BehandlingÅrsak.KLAGE, + navIdent = SikkerhetContext.hentSaksbehandler(), + barnasIdenter = emptyList(), + fagsakId = forrigeBehandling.fagsak.id, + ) + + klageService.validerOgOpprettRevurderingKlage(fagsak.id) + + verify { stegService.håndterNyBehandling(nyBehandling) } + } + + @Test + internal fun `kan ikke opprette revurdering hvis det finnes åpen behandling`() { + every { fagsakService.hentPåFagsakId(any()) } returns Fagsak(aktør = mockk()) + every { behandlingHentOgPersisterService.erÅpenBehandlingPåFagsak(any()) } returns true + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(any()) } returns lagBehandling( + status = BehandlingStatus.UTREDES, + ) + + val result = klageService.validerOgOpprettRevurderingKlage(0L) + + Assertions.assertFalse(result.opprettetBehandling) + } + + @Test + internal fun `kan ikke opprette revurdering hvis det ikke finnes noen behandlinger`() { + every { fagsakService.hentPåFagsakId(any()) } returns Fagsak(aktør = mockk()) + every { behandlingHentOgPersisterService.erÅpenBehandlingPåFagsak(any()) } returns false + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(any()) } returns null + + val result = klageService.validerOgOpprettRevurderingKlage(0L) + + Assertions.assertFalse(result.opprettetBehandling) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingServiceTest.kt new file mode 100644 index 000000000..c0e814df8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingServiceTest.kt @@ -0,0 +1,141 @@ +package no.nav.familie.ba.sak.kjerne.korrigertetterbetaling + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import org.hamcrest.MatcherAssert.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.fail +import org.hamcrest.CoreMatchers.`is` as Is + +@ExtendWith(MockKExtension::class) +internal class KorrigertEtterbetalingServiceTest { + + @MockK + private lateinit var korrigertEtterbetalingRepository: KorrigertEtterbetalingRepository + + @MockK + private lateinit var loggService: LoggService + + @InjectMockKs + private lateinit var korrigertEtterbetalingService: KorrigertEtterbetalingService + + @Test + fun `finnAktivtKorrigeringPåBehandling skal hente aktivt korrigering fra repository hvis det finnes`() { + val behandling = lagBehandling() + val korrigertEtterbetaling = lagKorrigertEtterbetaling(behandling) + + every { korrigertEtterbetalingRepository.finnAktivtKorrigeringPåBehandling(behandling.id) } returns korrigertEtterbetaling + + val hentetKorrigertEtterbetaling = + korrigertEtterbetalingService.finnAktivtKorrigeringPåBehandling(behandling.id) + ?: fail("etterbetaling korrigering ikke hentet riktig") + + assertThat(hentetKorrigertEtterbetaling.behandling.id, Is(behandling.id)) + assertThat(hentetKorrigertEtterbetaling.aktiv, Is(true)) + + verify(exactly = 1) { korrigertEtterbetalingRepository.finnAktivtKorrigeringPåBehandling(behandling.id) } + } + + @Test + fun `finnAlleKorrigeringerPåBehandling skal hente alle korrigering fra repository hvis de finnes`() { + val behandling = lagBehandling() + val korrigertEtterbetaling = lagKorrigertEtterbetaling(behandling) + + every { korrigertEtterbetalingRepository.finnAlleKorrigeringerPåBehandling(behandling.id) } returns listOf( + korrigertEtterbetaling, + korrigertEtterbetaling, + ) + + val hentetKorrigertEtterbetaling = + korrigertEtterbetalingService.finnAlleKorrigeringerPåBehandling(behandling.id) + + assertThat(hentetKorrigertEtterbetaling.size, Is(2)) + + verify(exactly = 1) { korrigertEtterbetalingRepository.finnAlleKorrigeringerPåBehandling(behandling.id) } + } + + @Test + fun `lagreKorrigertEtterbetaling skal lagre korrigering på behandling og logg på dette`() { + val behandling = lagBehandling() + val korrigertEtterbetaling = lagKorrigertEtterbetaling(behandling) + + every { korrigertEtterbetalingRepository.finnAktivtKorrigeringPåBehandling(behandling.id) } returns null + every { korrigertEtterbetalingRepository.save(korrigertEtterbetaling) } returns korrigertEtterbetaling + every { loggService.opprettKorrigertEtterbetalingLogg(behandling, any()) } returns Unit + + val lagretKorrigertEtterbetaling = + korrigertEtterbetalingService.lagreKorrigertEtterbetaling(korrigertEtterbetaling) + + assertThat(lagretKorrigertEtterbetaling.behandling.id, Is(behandling.id)) + + verify(exactly = 1) { korrigertEtterbetalingRepository.finnAktivtKorrigeringPåBehandling(behandling.id) } + verify(exactly = 1) { korrigertEtterbetalingRepository.save(korrigertEtterbetaling) } + verify(exactly = 1) { + loggService.opprettKorrigertEtterbetalingLogg( + behandling, + korrigertEtterbetaling, + ) + } + } + + @Test + fun `lagreKorrigertEtterbetaling skal sette og lagre forrige korrigering til inaktivt hvis det finnes tidligere korrigering`() { + val behandling = lagBehandling() + val forrigeKorrigering = mockk(relaxed = true) + val korrigertEtterbetaling = lagKorrigertEtterbetaling(behandling) + + every { korrigertEtterbetalingRepository.finnAktivtKorrigeringPåBehandling(any()) } returns forrigeKorrigering + every { korrigertEtterbetalingRepository.saveAndFlush(forrigeKorrigering) } returns korrigertEtterbetaling + every { korrigertEtterbetalingRepository.save(korrigertEtterbetaling) } returns korrigertEtterbetaling + every { loggService.opprettKorrigertEtterbetalingLogg(any(), any()) } returns Unit + + korrigertEtterbetalingService.lagreKorrigertEtterbetaling(korrigertEtterbetaling) + + verify(exactly = 1) { korrigertEtterbetalingRepository.finnAktivtKorrigeringPåBehandling(any()) } + verify(exactly = 1) { forrigeKorrigering setProperty "aktiv" value false } + verify(exactly = 1) { korrigertEtterbetalingRepository.saveAndFlush(forrigeKorrigering) } + verify(exactly = 1) { korrigertEtterbetalingRepository.save(korrigertEtterbetaling) } + } + + @Test + fun `settKorrigeringPåBehandlingTilInaktiv skal sette korrigering til inaktivt hvis det finnes`() { + val behandling = lagBehandling() + val korrigertEtterbetaling = mockk(relaxed = true) + + every { korrigertEtterbetalingRepository.finnAktivtKorrigeringPåBehandling(any()) } returns korrigertEtterbetaling + every { loggService.opprettKorrigertEtterbetalingLogg(any(), any()) } returns Unit + + korrigertEtterbetalingService.settKorrigeringPåBehandlingTilInaktiv(behandling) + + verify(exactly = 1) { korrigertEtterbetaling setProperty "aktiv" value false } + verify(exactly = 1) { + loggService.opprettKorrigertEtterbetalingLogg( + any(), + korrigertEtterbetaling, + ) + } + } +} + +fun lagKorrigertEtterbetaling( + behandling: Behandling, + årsak: KorrigertEtterbetalingÅrsak = KorrigertEtterbetalingÅrsak.FEIL_TIDLIGERE_UTBETALT_BELØP, + begrunnelse: String? = null, + beløp: Int = 2000, + aktiv: Boolean = true, +) = + KorrigertEtterbetaling( + behandling = behandling, + årsak = årsak, + begrunnelse = begrunnelse, + aktiv = aktiv, + beløp = beløp, + ) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakServiceTest.kt new file mode 100644 index 000000000..f5baaded5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakServiceTest.kt @@ -0,0 +1,122 @@ +package no.nav.familie.ba.sak.kjerne.korrigertvedtak + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.fail +import java.time.LocalDate + +@ExtendWith(MockKExtension::class) +internal class KorrigertVedtakServiceTest { + + @MockK + private lateinit var korrigertVedtakRepository: KorrigertVedtakRepository + + @MockK + private lateinit var loggService: LoggService + + @InjectMockKs + private lateinit var korrigertVedtakService: KorrigertVedtakService + + @Test + fun `finnAktivtKorrigertVedtakPåBehandling skal hente aktivt korrigert vedtak fra repository hvis det finnes`() { + val behandling = lagBehandling() + val korrigertVedtak = lagKorrigertVedtak(behandling) + + every { korrigertVedtakRepository.finnAktivtKorrigertVedtakPåBehandling(behandling.id) } returns korrigertVedtak + + val hentetKorrigertVedtak = + korrigertVedtakService.finnAktivtKorrigertVedtakPåBehandling(behandling.id) + ?: fail("korrigert vedtak ikke hentet riktig") + + MatcherAssert.assertThat(hentetKorrigertVedtak.behandling.id, CoreMatchers.`is`(behandling.id)) + MatcherAssert.assertThat(hentetKorrigertVedtak.aktiv, CoreMatchers.`is`(true)) + + verify(exactly = 1) { korrigertVedtakRepository.finnAktivtKorrigertVedtakPåBehandling(behandling.id) } + } + + @Test + fun `lagreKorrigertVedtak skal lagre korrigert vedtak på behandling og logg på dette`() { + val behandling = lagBehandling() + val korrigertVedtak = lagKorrigertVedtak(behandling) + + every { korrigertVedtakRepository.finnAktivtKorrigertVedtakPåBehandling(behandling.id) } returns null + every { korrigertVedtakRepository.save(korrigertVedtak) } returns korrigertVedtak + every { loggService.opprettKorrigertVedtakLogg(behandling, any()) } returns Unit + + val lagretKorrigertVedtak = + korrigertVedtakService.lagreKorrigertVedtak(korrigertVedtak) + + MatcherAssert.assertThat(lagretKorrigertVedtak.behandling.id, CoreMatchers.`is`(behandling.id)) + + verify(exactly = 1) { korrigertVedtakRepository.finnAktivtKorrigertVedtakPåBehandling(behandling.id) } + verify(exactly = 1) { korrigertVedtakRepository.save(korrigertVedtak) } + verify(exactly = 1) { + loggService.opprettKorrigertVedtakLogg( + behandling, + korrigertVedtak, + ) + } + } + + @Test + fun `lagreKorrigertVedtak skal sette og lagre forrige korrigert vedtak til inaktivt hvis det finnes tidligere korrigering`() { + val behandling = lagBehandling() + val forrigeKorrigering = mockk(relaxed = true) + val korrigertVedtak = lagKorrigertVedtak(behandling, vedtaksdato = LocalDate.now().minusDays(3)) + + every { korrigertVedtakRepository.finnAktivtKorrigertVedtakPåBehandling(any()) } returns forrigeKorrigering + every { korrigertVedtakRepository.saveAndFlush(forrigeKorrigering) } returns korrigertVedtak + every { korrigertVedtakRepository.save(korrigertVedtak) } returns korrigertVedtak + every { loggService.opprettKorrigertVedtakLogg(any(), any()) } returns Unit + + korrigertVedtakService.lagreKorrigertVedtak(korrigertVedtak) + + verify(exactly = 1) { korrigertVedtakRepository.finnAktivtKorrigertVedtakPåBehandling(any()) } + verify(exactly = 1) { forrigeKorrigering setProperty "aktiv" value false } + verify(exactly = 1) { korrigertVedtakRepository.saveAndFlush(forrigeKorrigering) } + verify(exactly = 1) { korrigertVedtakRepository.save(korrigertVedtak) } + } + + @Test + fun `settKorrigertVedtakPåBehandlingTilInaktiv skal sette korrigert vedtak til inaktivt hvis det finnes`() { + val behandling = lagBehandling() + val korrigertVedtak = mockk(relaxed = true) + + every { korrigertVedtakRepository.finnAktivtKorrigertVedtakPåBehandling(any()) } returns korrigertVedtak + every { loggService.opprettKorrigertVedtakLogg(any(), any()) } returns Unit + + korrigertVedtakService.settKorrigertVedtakPåBehandlingTilInaktiv(behandling) + + verify(exactly = 1) { korrigertVedtak setProperty "aktiv" value false } + verify(exactly = 1) { + loggService.opprettKorrigertVedtakLogg( + any(), + korrigertVedtak, + ) + } + } + + fun lagKorrigertVedtak( + behandling: Behandling, + vedtaksdato: LocalDate = LocalDate.now().minusDays(6), + begrunnelse: String? = null, + aktiv: Boolean = true, + ) = + KorrigertVedtak( + behandling = behandling, + vedtaksdato = vedtaksdato, + begrunnelse = begrunnelse, + aktiv = aktiv, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/personident/IdentHendelseTaskTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/personident/IdentHendelseTaskTest.kt new file mode 100644 index 000000000..d3839f590 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/personident/IdentHendelseTaskTest.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.kjerne.personident + +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.kontrakter.felles.PersonIdent +import no.nav.familie.kontrakter.felles.objectMapper +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +internal class IdentHendelseTaskTest { + + @MockK(relaxed = true) + private lateinit var personidentService: PersonidentService + + @InjectMockKs + private lateinit var identHendelseTask: IdentHendelseTask + + @Test + fun opprettTask() { + val nyPersonIdent = PersonIdent("123") + val task = IdentHendelseTask.opprettTask(nyPersonIdent) + assertEquals(nyPersonIdent, objectMapper.readValue(task.payload, PersonIdent::class.java)) + assertEquals("123", task.metadata["nyPersonIdent"]) + assertEquals("IdentHendelseTask", task.type) + + identHendelseTask.doTask(task) + + val slot = slot() + verify(exactly = 1) { personidentService.håndterNyIdent(capture(slot)) } + assertEquals(nyPersonIdent, slot.captured) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/personident/PersonidentServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/personident/PersonidentServiceTest.kt new file mode 100644 index 000000000..e54199d2f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/personident/PersonidentServiceTest.kt @@ -0,0 +1,493 @@ +package no.nav.familie.ba.sak.kjerne.personident + +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.secureLogger +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.integrasjoner.pdl.PdlIdentRestClient +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.IdentInformasjon +import no.nav.familie.kontrakter.felles.PersonIdent +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.domene.Task +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.assertThrows +import java.time.LocalDateTime + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class PersonidentServiceTest { + private val personidentAleredePersistert = randomFnr() + private val aktørIdAleredePersistert = tilAktør(personidentAleredePersistert) + private val personidentAktiv = randomFnr() + private val aktørIdAktiv = tilAktør(personidentAktiv) + private val personidentHistorisk = randomFnr() + + private val pdlIdentRestClient: PdlIdentRestClient = mockk(relaxed = true) + private val personidentRepository: PersonidentRepository = mockk() + private val aktørIdRepository: AktørIdRepository = mockk() + private val personIdentSlot = slot() + private val aktørSlot = slot() + private val personidentService = PersonidentService( + personidentRepository, + aktørIdRepository, + pdlIdentRestClient, + mockk(), + ) + + @BeforeAll + fun init() { + every { pdlIdentRestClient.hentIdenter(personidentAktiv, false) } answers { + listOf( + IdentInformasjon(aktørIdAktiv.aktørId, false, "AKTORID"), + IdentInformasjon(personidentAktiv, false, "FOLKEREGISTERIDENT"), + ) + } + every { pdlIdentRestClient.hentIdenter(personidentHistorisk, false) } answers { + listOf( + IdentInformasjon(aktørIdAktiv.aktørId, false, "AKTORID"), + IdentInformasjon(personidentAktiv, false, "FOLKEREGISTERIDENT"), + ) + } + } + + @BeforeEach + fun byggRepositoryMocks() { + clearMocks(answers = true, firstMock = aktørIdRepository) + clearMocks(answers = true, firstMock = personidentRepository) + + every { personidentRepository.saveAndFlush(capture(personIdentSlot)) } answers { + personIdentSlot.captured + } + + every { aktørIdRepository.saveAndFlush(capture(aktørSlot)) } answers { + aktørSlot.captured + } + } + + @Nested + inner class HåndterNyIdentTest { + @Test + fun `Skal legge til ny ident på aktør som finnes i systemet`() { + val personIdentSomFinnes = randomFnr() + val personIdentSomSkalLeggesTil = randomFnr() + val historiskIdent = randomFnr() + val historiskAktør = tilAktør(historiskIdent) + val aktørIdSomFinnes = tilAktør(personIdentSomFinnes) + + every { pdlIdentRestClient.hentIdenter(personIdentSomFinnes, false) } answers { + listOf( + IdentInformasjon(aktørIdSomFinnes.aktørId, false, "AKTORID"), + IdentInformasjon(personIdentSomFinnes, false, "FOLKEREGISTERIDENT"), + ) + } + + every { pdlIdentRestClient.hentIdenter(personIdentSomSkalLeggesTil, true) } answers { + listOf( + IdentInformasjon(aktørIdSomFinnes.aktørId, false, "AKTORID"), + IdentInformasjon(personIdentSomSkalLeggesTil, false, "FOLKEREGISTERIDENT"), + IdentInformasjon(historiskAktør.aktørId, true, "AKTORID"), + IdentInformasjon(historiskIdent, true, "FOLKEREGISTERIDENT"), + ) + } + + every { personidentRepository.findByFødselsnummerOrNull(personIdentSomFinnes) }.answers { + Personident(fødselsnummer = personidentAktiv, aktør = aktørIdSomFinnes, aktiv = true) + } + + every { aktørIdRepository.findByAktørIdOrNull(aktørIdSomFinnes.aktørId) }.answers { + aktørIdSomFinnes + } + + every { aktørIdRepository.findByAktørIdOrNull(historiskAktør.aktørId) }.answers { + null + } + every { personidentRepository.findByFødselsnummerOrNull(personIdentSomSkalLeggesTil) }.answers { + null + } + + val personidentService = PersonidentService( + personidentRepository, + aktørIdRepository, + pdlIdentRestClient, + mockk(), + ) + + val aktør = personidentService.håndterNyIdent(nyIdent = PersonIdent(personIdentSomSkalLeggesTil)) + + assertEquals(2, aktør?.personidenter?.size) + assertEquals(personIdentSomSkalLeggesTil, aktør!!.aktivFødselsnummer()) + assertTrue(aktør.personidenter.first { !it.aktiv }.gjelderTil!!.isBefore(LocalDateTime.now())) + verify(exactly = 2) { aktørIdRepository.saveAndFlush(any()) } + verify(exactly = 0) { personidentRepository.saveAndFlush(any()) } + } + + @Test + fun `Skal ikke legge til ny ident på aktør som allerede har denne identen registert i systemet`() { + val personIdentSomFinnes = randomFnr() + val aktørIdSomFinnes = tilAktør(personIdentSomFinnes) + + every { pdlIdentRestClient.hentIdenter(personIdentSomFinnes, true) } answers { + listOf( + IdentInformasjon(aktørIdSomFinnes.aktørId, false, "AKTORID"), + IdentInformasjon(personIdentSomFinnes, false, "FOLKEREGISTERIDENT"), + ) + } + + every { aktørIdRepository.findByAktørIdOrNull(aktørIdSomFinnes.aktørId) }.answers { aktørIdSomFinnes } + every { personidentRepository.findByFødselsnummerOrNull(personIdentSomFinnes) }.answers { + tilAktør( + personIdentSomFinnes, + ).personidenter.first() + } + + val personidentService = PersonidentService( + personidentRepository, + aktørIdRepository, + pdlIdentRestClient, + mockk(), + ) + + val aktør = personidentService.håndterNyIdent(nyIdent = PersonIdent(personIdentSomFinnes)) + + assertEquals(aktørIdSomFinnes.aktørId, aktør?.aktørId) + assertEquals(1, aktør?.personidenter?.size) + assertEquals(personIdentSomFinnes, aktør?.personidenter?.single()?.fødselsnummer) + verify(exactly = 0) { aktørIdRepository.saveAndFlush(any()) } + verify(exactly = 0) { personidentRepository.saveAndFlush(any()) } + } + + @Test + fun `Hendelse på en ident hvor gammel ident1 er merget med ny ident2 skal kaste feil når bruker har en sak på gammel aktør`() { + val fnrIdent1 = randomFnr() + val aktørIdent1 = tilAktør(fnrIdent1) + val aktivFnrIdent2 = randomFnr() + val aktivAktørIdent2 = tilAktør(aktivFnrIdent2) + + secureLogger.info("gammelIdent=$fnrIdent1,${aktørIdent1.aktørId} nyIdent=$aktivFnrIdent2,${aktivAktørIdent2.aktørId}") + + every { pdlIdentRestClient.hentIdenter(aktivFnrIdent2, true) } answers { + listOf( + IdentInformasjon(aktivAktørIdent2.aktørId, false, "AKTORID"), + IdentInformasjon(aktivFnrIdent2, false, "FOLKEREGISTERIDENT"), + IdentInformasjon(aktørIdent1.aktørId, true, "AKTORID"), + IdentInformasjon(fnrIdent1, true, "FOLKEREGISTERIDENT"), + ) + } + + every { aktørIdRepository.findByAktørIdOrNull(aktivAktørIdent2.aktørId) }.answers { + null + } + every { aktørIdRepository.findByAktørIdOrNull(aktørIdent1.aktørId) }.answers { + aktørIdent1 + } + + val personidentService = PersonidentService( + personidentRepository, + aktørIdRepository, + pdlIdentRestClient, + mockk(), + ) + + val feil = assertThrows { personidentService.håndterNyIdent(nyIdent = PersonIdent(aktivFnrIdent2)) } + assertThat(feil.message).contains("Mottok potensielt en hendelse på en merget ident for aktørId=${aktivAktørIdent2.aktørId}. Sjekk securelogger for liste med identer. Sjekk om identen har flere saker. Disse må løses manuelt") + } + + @Test + fun `Hendelse på en ident hvor gammel ident1 er merget med ny ident2 skal ikke kaste feil når bruker har alt bruker ny ident`() { + val fnrIdent1 = randomFnr() + val aktørIdent1 = tilAktør(fnrIdent1) + val aktivFnrIdent2 = randomFnr() + val aktivAktørIdent2 = tilAktør(aktivFnrIdent2) + + secureLogger.info("gammelIdent=$fnrIdent1,${aktørIdent1.aktørId} nyIdent=$aktivFnrIdent2,${aktivAktørIdent2.aktørId}") + + every { pdlIdentRestClient.hentIdenter(aktivFnrIdent2, true) } answers { + listOf( + IdentInformasjon(aktivAktørIdent2.aktørId, false, "AKTORID"), + IdentInformasjon(aktivFnrIdent2, false, "FOLKEREGISTERIDENT"), + IdentInformasjon(aktørIdent1.aktørId, true, "AKTORID"), + IdentInformasjon(fnrIdent1, true, "FOLKEREGISTERIDENT"), + ) + } + + every { aktørIdRepository.findByAktørIdOrNull(aktivAktørIdent2.aktørId) }.answers { + aktivAktørIdent2 + } + every { aktørIdRepository.findByAktørIdOrNull(aktørIdent1.aktørId) }.answers { + null + } + + val personidentService = PersonidentService( + personidentRepository, + aktørIdRepository, + pdlIdentRestClient, + mockk(), + ) + + val aktør = personidentService.håndterNyIdent(nyIdent = PersonIdent(aktivFnrIdent2)) + assertEquals(aktivAktørIdent2.aktørId, aktør?.aktørId) + assertEquals(1, aktør?.personidenter?.size) + assertEquals(aktivFnrIdent2, aktør?.personidenter?.single()?.fødselsnummer) + verify(exactly = 0) { aktørIdRepository.saveAndFlush(any()) } + verify(exactly = 0) { personidentRepository.saveAndFlush(any()) } + } + } + + @Nested + inner class HentAktørTest { + @Test + fun `Test aktør id som som er persistert fra før`() { + every { personidentRepository.findByFødselsnummerOrNull(aktørIdAleredePersistert.aktørId) } answers { null } + every { aktørIdRepository.findByAktørIdOrNull(aktørIdAleredePersistert.aktørId) } answers { aktørIdAleredePersistert } + + val aktør = personidentService.hentAktør(aktørIdAleredePersistert.aktørId) + + verify(exactly = 0) { aktørIdRepository.saveAndFlush(any()) } + verify(exactly = 0) { personidentRepository.saveAndFlush(any()) } + assertEquals(aktørIdAleredePersistert.aktørId, aktør.aktørId) + assertEquals(personidentAleredePersistert, aktør.personidenter.single().fødselsnummer) + } + + @Test + fun `Test personident som er persistert fra før`() { + every { personidentRepository.findByFødselsnummerOrNull(personidentAleredePersistert) } answers { aktørIdAleredePersistert.personidenter.first() } + + val aktør = personidentService.hentAktør(personidentAleredePersistert) + + verify(exactly = 0) { aktørIdRepository.saveAndFlush(any()) } + verify(exactly = 0) { personidentRepository.saveAndFlush(any()) } + assertEquals(aktørIdAleredePersistert.aktørId, aktør.aktørId) + assertEquals(personidentAleredePersistert, aktør.personidenter.single().fødselsnummer) + } + + @Test + fun `Test aktiv personident som er persistert fra før`() { + every { personidentRepository.findByFødselsnummerOrNull(personidentAktiv) } answers { null } + every { aktørIdRepository.findByAktørIdOrNull(personidentAktiv) } answers { null } + + every { personidentRepository.findByFødselsnummerOrNull(personidentAktiv) } answers { + Personident( + personidentAktiv, + aktørIdAktiv, + ) + } + + val aktør = personidentService.hentOgLagreAktør(personidentAktiv, false) + + verify(exactly = 0) { aktørIdRepository.saveAndFlush(any()) } + verify(exactly = 0) { personidentRepository.saveAndFlush(any()) } + assertEquals(aktørIdAktiv.aktørId, aktør.aktørId) + assertEquals(personidentAktiv, aktør.personidenter.single().fødselsnummer) + } + + @Test + fun `Test aktør id som som er persistert fra før men aktiv personident som ikke er persistert`() { + every { personidentRepository.findByFødselsnummerOrNull(personidentHistorisk) } answers { null } + every { aktørIdRepository.findByAktørIdOrNull(personidentHistorisk) } answers { null } + every { personidentRepository.findByFødselsnummerOrNull(personidentAktiv) } answers { null } + + every { aktørIdRepository.findByAktørIdOrNull(aktørIdAktiv.aktørId) } answers { aktørIdAktiv } + + val aktør = personidentService.hentOgLagreAktør(personidentHistorisk, true) + + verify(exactly = 0) { aktørIdRepository.saveAndFlush(any()) } + verify(exactly = 0) { personidentRepository.saveAndFlush(any()) } + assertEquals(aktørIdAktiv.aktørId, aktør.aktørId) + assertEquals(personidentAktiv, aktør.personidenter.single().fødselsnummer) + } + + @Test + fun `Test hverken aktør id eller aktiv personident som er persistert fra før`() { + every { personidentRepository.findByFødselsnummerOrNull(personidentAktiv) } answers { null } + every { aktørIdRepository.findByAktørIdOrNull(personidentAktiv) } answers { null } + every { personidentRepository.findByFødselsnummerOrNull(personidentAktiv) } answers { null } + + every { aktørIdRepository.findByAktørIdOrNull(aktørIdAktiv.aktørId) } answers { null } + + val aktør = personidentService.hentOgLagreAktør(personidentAktiv, true) + + verify(exactly = 1) { aktørIdRepository.saveAndFlush(any()) } + verify(exactly = 0) { personidentRepository.saveAndFlush(any()) } + assertEquals(aktørIdAktiv.aktørId, aktør.aktørId) + assertEquals(personidentAktiv, aktør.personidenter.single().fødselsnummer) + } + + @Test + fun `Test hverken aktør id eller aktiv personident som er persistert fra før men som ikke skal persisteres`() { + every { personidentRepository.findByFødselsnummerOrNull(personidentAktiv) } answers { null } + every { aktørIdRepository.findByAktørIdOrNull(personidentAktiv) } answers { null } + every { personidentRepository.findByFødselsnummerOrNull(personidentAktiv) } answers { null } + + every { aktørIdRepository.findByAktørIdOrNull(aktørIdAktiv.aktørId) } answers { null } + + val aktør = personidentService.hentOgLagreAktør(personidentAktiv, false) + + verify(exactly = 0) { aktørIdRepository.saveAndFlush(any()) } + verify(exactly = 0) { personidentRepository.saveAndFlush(any()) } + assertEquals(aktørIdAktiv.aktørId, aktør.aktørId) + assertEquals(personidentAktiv, aktør.personidenter.single().fødselsnummer) + } + } + + @Nested + inner class OpprettTaskForIdentHendelseTest { + @Test + fun `Skal opprette task for håndtering av ny ident ved ny fnr men samme aktør`() { + val personIdentSomFinnes = randomFnr() + val personIdentSomSkalLeggesTil = randomFnr() + val aktørIdSomFinnes = tilAktør(personIdentSomFinnes) + aktørIdSomFinnes.personidenter.add( + Personident( + fødselsnummer = personIdentSomFinnes, + aktør = aktørIdSomFinnes, + ), + ) + + val taskRepositoryMock = mockk(relaxed = true) + val personidentService = PersonidentService( + personidentRepository, + aktørIdRepository, + pdlIdentRestClient, + taskRepositoryMock, + ) + + every { pdlIdentRestClient.hentIdenter(personIdentSomFinnes, false) } answers { + listOf( + IdentInformasjon(aktørIdSomFinnes.aktørId, false, "AKTORID"), + IdentInformasjon(personIdentSomFinnes, false, "FOLKEREGISTERIDENT"), + ) + } + + every { pdlIdentRestClient.hentIdenter(personIdentSomSkalLeggesTil, true) } answers { + listOf( + IdentInformasjon(aktørIdSomFinnes.aktørId, false, "AKTORID"), + IdentInformasjon(personIdentSomSkalLeggesTil, false, "FOLKEREGISTERIDENT"), + IdentInformasjon(personIdentSomFinnes, true, "FOLKEREGISTERIDENT"), + ) + } + every { aktørIdRepository.findByAktørIdOrNull(aktørIdSomFinnes.aktørId) }.answers { + aktørIdSomFinnes + } + + val slot = slot() + every { taskRepositoryMock.save(capture(slot)) } answers { slot.captured } + + val ident = PersonIdent(personIdentSomSkalLeggesTil) + personidentService.opprettTaskForIdentHendelse(ident) + + verify(exactly = 1) { taskRepositoryMock.save(any()) } + assertEquals(ident, objectMapper.readValue(slot.captured.payload, PersonIdent::class.java)) + } + + @Test + fun `Skal opprette task for håndtering av ny ident ved ny fnr og ny aktør`() { + val personIdentSomFinnes = randomFnr() + val personIdentSomSkalLeggesTil = randomFnr() + val aktørIdGammel = tilAktør(personIdentSomFinnes) + val aktørIdNy = tilAktør(personIdentSomSkalLeggesTil) + aktørIdGammel.personidenter.add( + Personident( + fødselsnummer = personIdentSomFinnes, + aktør = aktørIdGammel, + ), + ) + + val taskRepositoryMock = mockk(relaxed = true) + val personidentService = PersonidentService( + personidentRepository, + aktørIdRepository, + pdlIdentRestClient, + taskRepositoryMock, + ) + + every { pdlIdentRestClient.hentIdenter(personIdentSomFinnes, false) } answers { + listOf( + IdentInformasjon(aktørIdGammel.aktørId, false, "AKTORID"), + IdentInformasjon(personIdentSomFinnes, false, "FOLKEREGISTERIDENT"), + ) + } + + every { pdlIdentRestClient.hentIdenter(personIdentSomSkalLeggesTil, true) } answers { + listOf( + IdentInformasjon(aktørIdGammel.aktørId, false, "AKTORID"), + IdentInformasjon(personIdentSomSkalLeggesTil, false, "FOLKEREGISTERIDENT"), + IdentInformasjon(aktørIdNy.aktørId, true, "AKTORID"), + IdentInformasjon(personIdentSomFinnes, true, "FOLKEREGISTERIDENT"), + ) + } + every { aktørIdRepository.findByAktørIdOrNull(aktørIdGammel.aktørId) }.answers { + aktørIdGammel + } + + every { aktørIdRepository.findByAktørIdOrNull(aktørIdNy.aktørId) } returns null + + val slot = slot() + every { taskRepositoryMock.save(capture(slot)) } answers { slot.captured } + + val ident = PersonIdent(personIdentSomSkalLeggesTil) + personidentService.opprettTaskForIdentHendelse(ident) + + verify(exactly = 1) { taskRepositoryMock.save(any()) } + assertEquals(ident, objectMapper.readValue(slot.captured.payload, PersonIdent::class.java)) + } + + @Test + fun `Skal ikke opprette task for håndtering av ny ident når ident ikke er tilknyttet noen aktører i systemet`() { + val personIdentSomFinnes = randomFnr() + val personIdentSomSkalLeggesTil = randomFnr() + val aktørIdIkkeIBaSak = tilAktør(personIdentSomSkalLeggesTil) + val aktørIdSomFinnes = tilAktør(personIdentSomFinnes) + aktørIdSomFinnes.personidenter.add( + Personident( + fødselsnummer = personIdentSomFinnes, + aktør = aktørIdSomFinnes, + ), + ) + + val taskRepositoryMock = mockk(relaxed = true) + val personidentService = PersonidentService( + personidentRepository, + aktørIdRepository, + pdlIdentRestClient, + taskRepositoryMock, + ) + + every { pdlIdentRestClient.hentIdenter(personIdentSomFinnes, false) } answers { + listOf( + IdentInformasjon(aktørIdSomFinnes.aktørId, false, "AKTORID"), + IdentInformasjon(personIdentSomFinnes, false, "FOLKEREGISTERIDENT"), + ) + } + + every { pdlIdentRestClient.hentIdenter(personIdentSomSkalLeggesTil, false) } answers { + listOf( + IdentInformasjon(aktørIdIkkeIBaSak.aktørId, false, "AKTORID"), + IdentInformasjon(personIdentSomSkalLeggesTil, false, "FOLKEREGISTERIDENT"), + ) + } + every { aktørIdRepository.findByAktørIdOrNull(aktørIdIkkeIBaSak.aktørId) }.answers { + aktørIdIkkeIBaSak + } + + val slot = slot() + every { taskRepositoryMock.save(capture(slot)) } answers { slot.captured } + + val ident = PersonIdent(personIdentSomSkalLeggesTil) + personidentService.opprettTaskForIdentHendelse(ident) + + verify(exactly = 0) { taskRepositoryMock.save(any()) } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/personident/PersonidentTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/personident/PersonidentTest.kt new file mode 100644 index 000000000..da4a12480 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/personident/PersonidentTest.kt @@ -0,0 +1,40 @@ +package no.nav.familie.ba.sak.kjerne.personident + +import io.mockk.mockk +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Test + +internal class PersonidentTest { + + val fnr1 = "12345678903" + val fnr2 = "23345678903" + + @Test + fun `To personidenter er like hvis de har samme fødselsnummer og aktiv`() { + val p1 = Personident(fnr1, aktiv = true, aktør = mockk()) + val p2 = Personident(fnr1, aktiv = true, aktør = mockk()) + assertEquals(p1, p2) + } + + @Test + fun `To personidenter er ulike hvis de har samme fødselsnummer, men kun en er aktiv`() { + val p1 = Personident(fnr1, aktiv = true, aktør = mockk()) + val p2 = Personident(fnr1, aktiv = false, aktør = mockk()) + assertNotEquals(p1, p2) + } + + @Test + fun `To personidenter er like hvis de har samme fødselsnummer og begge er inaktive`() { + val p1 = Personident(fnr1, aktiv = false, aktør = mockk()) + val p2 = Personident(fnr1, aktiv = false, aktør = mockk()) + assertEquals(p1, p2) + } + + @Test + fun `To personidenter er ulike hvis de har forskjellige fødselsnummer og begge er aktive`() { + val p1 = Personident(fnr1, aktiv = true, aktør = mockk()) + val p2 = Personident(fnr2, aktiv = true, aktør = mockk()) + assertNotEquals(p1, p2) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/simulering/KontrollerNyUtbetalingsgeneratorServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/simulering/KontrollerNyUtbetalingsgeneratorServiceTest.kt new file mode 100644 index 000000000..3d217dab6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/simulering/KontrollerNyUtbetalingsgeneratorServiceTest.kt @@ -0,0 +1,575 @@ +package no.nav.familie.ba.sak.kjerne.simulering + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.integrasjoner.økonomi.UtbetalingsoppdragGeneratorService +import no.nav.familie.ba.sak.integrasjoner.økonomi.lagUtbetalingsoppdrag +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiKlient +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilLocalDate +import no.nav.familie.ba.sak.kjerne.tidslinje.util.apr +import no.nav.familie.ba.sak.kjerne.tidslinje.util.aug +import no.nav.familie.ba.sak.kjerne.tidslinje.util.des +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jul +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jun +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mai +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mar +import no.nav.familie.ba.sak.kjerne.tidslinje.util.sep +import no.nav.familie.felles.utbetalingsgenerator.domain.AndelMedPeriodeIdLongId +import no.nav.familie.felles.utbetalingsgenerator.domain.BeregnetUtbetalingsoppdragLongId +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsperiode +import no.nav.familie.kontrakter.felles.simulering.BetalingType +import no.nav.familie.kontrakter.felles.simulering.DetaljertSimuleringResultat +import no.nav.familie.kontrakter.felles.simulering.FagOmrådeKode +import no.nav.familie.kontrakter.felles.simulering.MottakerType +import no.nav.familie.kontrakter.felles.simulering.PosteringType +import no.nav.familie.kontrakter.felles.simulering.SimuleringMottaker +import no.nav.familie.kontrakter.felles.simulering.SimulertPostering +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.math.BigDecimal + +@ExtendWith(MockKExtension::class) +class KontrollerNyUtbetalingsgeneratorServiceTest { + + @MockK + private lateinit var featureToggleService: FeatureToggleService + + @MockK + private lateinit var utbetalingsoppdragGeneratorService: UtbetalingsoppdragGeneratorService + + @MockK + private lateinit var økonomiKlient: ØkonomiKlient + + @MockK + private lateinit var tikjentYtelseRepository: TilkjentYtelseRepository + + @InjectMockKs + private lateinit var kontrollerNyUtbetalingsgeneratorService: KontrollerNyUtbetalingsgeneratorService + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal fange opp at gammel simulering har perioder med endring før ny simulering og ulikt resultat i samme perioder`() { + setupMocks() + val simuleringBasertPåGammelGenerator = lagDetaljertSimuleringsResultat( + listOf( + Periode(jan(2023), mar(2023), 100), + Periode(apr(2023), mai(2023), 200), + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns lagDetaljertSimuleringsResultat( + listOf( + Periode(apr(2023), mai(2023), 250), + ), + ) + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + gammeltSimuleringResultat = simuleringBasertPåGammelGenerator, + gammeltUtbetalingsoppdrag = mockk(), + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(2) + assertThat( + simuleringsPeriodeDiffFeil.containsAll( + listOf( + DiffFeilType.TidligerePerioderIGammelUlik0, + DiffFeilType.UliktResultatISammePeriode, + ), + ), + ).isTrue + } + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal ikke gi feil dersom gammel simulering har perioder uten endring før ny simulering og resultatene er like i øvrige perioder`() { + setupMocks() + val simuleringBasertPåGammelGenerator = lagDetaljertSimuleringsResultat( + listOf( + Periode(jan(2023), mar(2023), 0), + Periode(apr(2023), mai(2023), 200), + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns lagDetaljertSimuleringsResultat( + listOf( + Periode(apr(2023), mai(2023), 200), + ), + ) + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + gammeltSimuleringResultat = simuleringBasertPåGammelGenerator, + gammeltUtbetalingsoppdrag = mockk(), + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(0) + } + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal ikke gi feil dersom gammel simulering og ny simulering er helt like`() { + setupMocks() + val simuleringBasertPåGammelGenerator = lagDetaljertSimuleringsResultat( + listOf( + Periode(jan(2023), mar(2023), 100), + Periode(apr(2023), mai(2023), 200), + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns lagDetaljertSimuleringsResultat( + listOf( + Periode(jan(2023), mar(2023), 100), + Periode(apr(2023), mai(2023), 200), + ), + ) + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + gammeltSimuleringResultat = simuleringBasertPåGammelGenerator, + gammeltUtbetalingsoppdrag = mockk(), + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(0) + } + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal gi feil dersom gammel simulering og ny simulering har et ulikt resultat`() { + setupMocks() + val simuleringBasertPåGammelGenerator = lagDetaljertSimuleringsResultat( + listOf( + Periode(jan(2023), feb(2023), 100), + Periode(mar(2023), apr(2023), 200), + Periode(mai(2023), jun(2023), 300), + Periode(jul(2023), aug(2023), 200), + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns lagDetaljertSimuleringsResultat( + listOf( + Periode(jan(2023), feb(2023), 100), + Periode(mar(2023), apr(2023), 200), + Periode(mai(2023), jun(2023), 320), + Periode(jul(2023), aug(2023), 200), + ), + ) + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + gammeltSimuleringResultat = simuleringBasertPåGammelGenerator, + gammeltUtbetalingsoppdrag = mockk(), + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(1) + assertThat( + simuleringsPeriodeDiffFeil.first(), + ).isEqualTo(DiffFeilType.UliktResultatISammePeriode) + } + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal gi feil dersom gammel simulering har endring før ny simulering`() { + setupMocks() + val simuleringBasertPåGammelGenerator = lagDetaljertSimuleringsResultat( + listOf( + Periode(jan(2023), feb(2023), 100), + Periode(mar(2023), apr(2023), 200), + Periode(mai(2023), jun(2023), 320), + Periode(jul(2023), aug(2023), 200), + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns lagDetaljertSimuleringsResultat( + listOf( + Periode(mar(2023), apr(2023), 200), + Periode(mai(2023), jun(2023), 320), + Periode(jul(2023), aug(2023), 200), + ), + ) + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + gammeltSimuleringResultat = simuleringBasertPåGammelGenerator, + gammeltUtbetalingsoppdrag = mockk(), + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(1) + assertThat( + simuleringsPeriodeDiffFeil.first(), + ).isEqualTo(DiffFeilType.TidligerePerioderIGammelUlik0) + } + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal ikke gi feil dersom gammel simulering og ny simulering er like og har hull i periodene`() { + setupMocks() + val simuleringBasertPåGammelGenerator = lagDetaljertSimuleringsResultat( + listOf( + Periode(jan(2023), feb(2023), 100), + Periode(mai(2023), jun(2023), 300), + Periode(aug(2023), sep(2023), 200), + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns lagDetaljertSimuleringsResultat( + listOf( + Periode(jan(2023), feb(2023), 100), + Periode(mai(2023), jun(2023), 300), + Periode(aug(2023), sep(2023), 200), + ), + ) + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + gammeltSimuleringResultat = simuleringBasertPåGammelGenerator, + gammeltUtbetalingsoppdrag = mockk(), + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(0) + } + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal ikke kjøre sammenligning dersom det ikke finnes noen utbetalingsperioder i utbetalingsoppdraget fra gammel`() { + setupMocks() + every { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + andelTilkjentYtelseForUtbetalingsoppdragFactory = any(), + ) + } returns lagUtbetalingsoppdrag(emptyList()) + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + saksbehandlerId = "12345", + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(0) + } + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal ikke kjøre sammenligning dersom det ikke finnes noen utbetalingsperioder i utbetalingsoppdraget fra ny`() { + setupMocks() + every { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + erSimulering = any(), + ) + } returns BeregnetUtbetalingsoppdragLongId( + lagUtbetalingsoppdrag(emptyList()), + emptyList(), + ) + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + gammeltSimuleringResultat = mockk(), + gammeltUtbetalingsoppdrag = mockk(), + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(0) + } + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal gi feil dersom ett av simuleringsresultatene ikke er tomt men det andre er det`() { + setupMocks() + val simuleringBasertPåGammelGenerator = lagDetaljertSimuleringsResultat( + listOf( + Periode(jan(2023), feb(2023), 100), + Periode(mai(2023), jun(2023), 300), + Periode(aug(2023), sep(2023), 200), + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns lagDetaljertSimuleringsResultat( + emptyList(), + ) + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + gammeltSimuleringResultat = simuleringBasertPåGammelGenerator, + gammeltUtbetalingsoppdrag = mockk(), + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(1) + assertThat( + simuleringsPeriodeDiffFeil.first(), + ).isEqualTo(DiffFeilType.DetEneSimuleringsresultatetErTomt) + } + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal ikke kjøre sammenligning dersom begge simuleringsresultatene er tomme`() { + setupMocks() + val simuleringBasertPåGammelGenerator = lagDetaljertSimuleringsResultat( + emptyList(), + ) + + every { økonomiKlient.hentSimulering(any()) } returns lagDetaljertSimuleringsResultat( + emptyList(), + ) + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + gammeltSimuleringResultat = simuleringBasertPåGammelGenerator, + gammeltUtbetalingsoppdrag = mockk(), + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(0) + } + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal fange opp feil som kastes`() { + setupMocks() + every { + økonomiKlient.hentSimulering(any()) + } throws Exception("Test") + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + gammeltSimuleringResultat = mockk(), + gammeltUtbetalingsoppdrag = mockk(), + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(1) + assertThat( + simuleringsPeriodeDiffFeil.first(), + ).isEqualTo(DiffFeilType.UventetFeil) + } + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal gi feil dersom antall andeler fra ny generator er ulikt andeler med utbetaling`() { + setupMocks( + overstyrteAndelerFraGenerator = listOf( + AndelMedPeriodeIdLongId(0, 1, null, 1), + AndelMedPeriodeIdLongId(1, 2, 1, 1), + AndelMedPeriodeIdLongId(2, 3, 2, 1), + ), + overstyrteAndeler = listOf( + lagAndelTilkjentYtelse( + fom = inneværendeMåned(), + tom = inneværendeMåned().plusMonths(1), + ), + lagAndelTilkjentYtelse( + fom = inneværendeMåned().plusMonths(2), + tom = inneværendeMåned().plusMonths(3), + ), + ), + ) + + val simuleringBasertPåGammelGenerator = lagDetaljertSimuleringsResultat( + emptyList(), + ) + + every { økonomiKlient.hentSimulering(any()) } returns lagDetaljertSimuleringsResultat( + emptyList(), + ) + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + gammeltSimuleringResultat = simuleringBasertPåGammelGenerator, + gammeltUtbetalingsoppdrag = mockk(), + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(1) + assertThat( + simuleringsPeriodeDiffFeil.first(), + ).isEqualTo(DiffFeilType.FeilAntallAndeler) + } + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal gi feil dersom id'ene til andeler fra ny generator ikke matcher id'ene til andeler med utbetaling`() { + setupMocks( + overstyrteAndelerFraGenerator = listOf( + AndelMedPeriodeIdLongId(0, 1, null, 1), + AndelMedPeriodeIdLongId(1, 2, 1, 1), + AndelMedPeriodeIdLongId(2, 3, 2, 1), + ), + overstyrteAndeler = listOf( + lagAndelTilkjentYtelse( + id = 1, + fom = inneværendeMåned(), + tom = inneværendeMåned().plusMonths(1), + ), + lagAndelTilkjentYtelse( + id = 2, + fom = inneværendeMåned().plusMonths(2), + tom = inneværendeMåned().plusMonths(3), + ), + lagAndelTilkjentYtelse( + id = 3, + fom = inneværendeMåned().plusMonths(4), + tom = inneværendeMåned().plusMonths(5), + ), + ), + ) + + val simuleringBasertPåGammelGenerator = lagDetaljertSimuleringsResultat( + emptyList(), + ) + + every { økonomiKlient.hentSimulering(any()) } returns lagDetaljertSimuleringsResultat( + emptyList(), + ) + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + gammeltSimuleringResultat = simuleringBasertPåGammelGenerator, + gammeltUtbetalingsoppdrag = mockk(), + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(1) + assertThat( + simuleringsPeriodeDiffFeil.first(), + ).isEqualTo(DiffFeilType.AndelerMatcherIkke) + } + + @Test + fun `kontrollerNyUtbetalingsgenerator - skal ikke gi feil dersom andeler fra ny generator matcher andeler med utbetaling`() { + setupMocks( + overstyrteAndelerFraGenerator = listOf( + AndelMedPeriodeIdLongId(0, 1, null, 1), + AndelMedPeriodeIdLongId(1, 2, 1, 1), + AndelMedPeriodeIdLongId(2, 3, 2, 1), + ), + overstyrteAndeler = listOf( + lagAndelTilkjentYtelse( + id = 0, + fom = inneværendeMåned(), + tom = inneværendeMåned().plusMonths(1), + ), + lagAndelTilkjentYtelse( + id = 1, + fom = inneværendeMåned().plusMonths(2), + tom = inneværendeMåned().plusMonths(3), + ), + lagAndelTilkjentYtelse( + id = 2, + fom = inneværendeMåned().plusMonths(4), + tom = inneværendeMåned().plusMonths(5), + ), + ), + ) + + val simuleringBasertPåGammelGenerator = lagDetaljertSimuleringsResultat( + emptyList(), + ) + + every { økonomiKlient.hentSimulering(any()) } returns lagDetaljertSimuleringsResultat( + emptyList(), + ) + + val simuleringsPeriodeDiffFeil = kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = lagVedtak(), + gammeltSimuleringResultat = simuleringBasertPåGammelGenerator, + gammeltUtbetalingsoppdrag = mockk(), + ) + + assertThat(simuleringsPeriodeDiffFeil.size).isEqualTo(0) + } + + private fun setupMocks( + overstyrteAndelerFraGenerator: List? = null, + overstyrteAndeler: List? = null, + ) { + every { + featureToggleService.isEnabled( + FeatureToggleConfig.KONTROLLER_NY_UTBETALINGSGENERATOR, + false, + ) + } returns true + + val beregnetUtbetalingsoppdragMock = mockk() + val utbetalingsoppdrag = lagUtbetalingsoppdrag() + + // every { utbetalingsoppdrag.kodeEndring } returns Utbetalingsoppdrag.KodeEndring.ENDR + // + // every { utbetalingsoppdrag.fagSystem } returns FagsystemBA.BARNETRYGD + // + // every { utbetalingsoppdrag.utbetalingsperiode } returns listOf(mockk()) + + every { beregnetUtbetalingsoppdragMock.utbetalingsoppdrag } returns utbetalingsoppdrag + + every { beregnetUtbetalingsoppdragMock.andeler } returns (overstyrteAndelerFraGenerator ?: emptyList()) + + every { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + erSimulering = any(), + ) + } returns beregnetUtbetalingsoppdragMock + + every { tikjentYtelseRepository.findByBehandling(any()) } returns lagInitiellTilkjentYtelse().also { + it.andelerTilkjentYtelse.addAll( + overstyrteAndeler ?: emptyList(), + ) + } + } + + fun lagDetaljertSimuleringsResultat(perioder: List>) = + if (perioder.isEmpty()) { + DetaljertSimuleringResultat(emptyList()) + } else { + DetaljertSimuleringResultat( + simuleringMottaker = + listOf( + SimuleringMottaker( + simulertPostering = perioder.fold(mutableListOf()) { acc, periode -> + if (periode.innhold!! == 0) { + acc.addAll( + listOf( + lagSimulertPostering(periode, overstyrtBeløp = 1000), + lagSimulertPostering( + periode = periode, + overstyrtBeløp = 1000, + negativtFortegn = true, + ), + ), + ) + acc + } else { + acc.add( + lagSimulertPostering(periode), + ) + acc + } + }, + mottakerType = MottakerType.BRUKER, + ), + ), + ) + } + + fun lagSimulertPostering( + periode: Periode, + overstyrtBeløp: Int? = null, + negativtFortegn: Boolean = false, + ) = + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = periode.fraOgMed.tilLocalDate(), + tom = periode.tilOgMed.tilLocalDate(), + betalingType = BetalingType.DEBIT, + beløp = if (negativtFortegn) { + -BigDecimal(overstyrtBeløp ?: periode.innhold!!).setScale(10) + } else { + BigDecimal(overstyrtBeløp ?: periode.innhold!!).setScale( + 10, + ) + }, + posteringType = PosteringType.YTELSE, + forfallsdato = des(2023).tilLocalDate(), + utenInntrekk = true, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringServiceEnhetTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringServiceEnhetTest.kt new file mode 100644 index 000000000..30a391594 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringServiceEnhetTest.kt @@ -0,0 +1,438 @@ +package no.nav.familie.ba.sak.kjerne.simulering + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.tilPersonEnkel +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.integrasjoner.økonomi.UtbetalingsoppdragGeneratorService +import no.nav.familie.ba.sak.integrasjoner.økonomi.lagUtbetalingsoppdrag +import no.nav.familie.ba.sak.integrasjoner.økonomi.tilRestUtbetalingsoppdrag +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiKlient +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.simulering.domene.ØkonomiSimuleringMottaker +import no.nav.familie.ba.sak.kjerne.simulering.domene.ØkonomiSimuleringMottakerRepository +import no.nav.familie.ba.sak.kjerne.simulering.domene.ØkonomiSimuleringPostering +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.felles.utbetalingsgenerator.domain.BeregnetUtbetalingsoppdragLongId +import no.nav.familie.felles.utbetalingsgenerator.domain.Utbetalingsperiode +import no.nav.familie.kontrakter.felles.simulering.BetalingType +import no.nav.familie.kontrakter.felles.simulering.FagOmrådeKode +import no.nav.familie.kontrakter.felles.simulering.MottakerType +import no.nav.familie.kontrakter.felles.simulering.PosteringType +import no.nav.familie.unleash.UnleashService +import org.hamcrest.MatcherAssert.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import java.math.BigDecimal +import java.time.LocalDate +import org.hamcrest.CoreMatchers.`is` as Is + +internal class SimuleringServiceEnhetTest { + + private val økonomiKlient: ØkonomiKlient = mockk() + private val økonomiService: ØkonomiService = mockk() + private val beregningService: BeregningService = mockk() + private val økonomiSimuleringMottakerRepository: ØkonomiSimuleringMottakerRepository = mockk() + private val tilgangService: TilgangService = mockk() + private val featureToggleService: FeatureToggleService = mockk() + private val vedtakRepository: VedtakRepository = mockk() + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService = mockk() + private val persongrunnlagService: PersongrunnlagService = mockk() + private val kontrollerNyUtbetalingsgeneratorService: KontrollerNyUtbetalingsgeneratorService = mockk() + private val utbetalingsoppdragGeneratorService: UtbetalingsoppdragGeneratorService = mockk() + private val unleashService: UnleashService = mockk() + + private val simuleringService: SimuleringService = SimuleringService( + økonomiKlient, + beregningService, + økonomiSimuleringMottakerRepository, + tilgangService, + featureToggleService, + unleashService, + vedtakRepository, + utbetalingsoppdragGeneratorService, + behandlingHentOgPersisterService, + persongrunnlagService, + kontrollerNyUtbetalingsgeneratorService, + ) + + val februar2023 = LocalDate.of(2023, 2, 1) + + @ParameterizedTest + @EnumSource(value = BehandlingÅrsak::class, names = ["HELMANUELL_MIGRERING", "ENDRE_MIGRERINGSDATO"]) + fun `harMigreringsbehandlingAvvikInnenforBeløpsgrenser skal returnere true dersom det finnes avvik i form av etterbetaling som er innenfor beløpsgrense`( + behandlingÅrsak: BehandlingÅrsak, + ) { + val behandling: Behandling = no.nav.familie.ba.sak.common.lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = behandlingÅrsak, + førsteSteg = StegType.VURDER_TILBAKEKREVING, + ) + + // etterbetaling 4 KR pga. avrundingsfeil. 1 KR per barn i hver periode. + val posteringer = listOf( + mockVedtakSimuleringPostering(fom = februar2023, beløp = 2, betalingType = BetalingType.DEBIT), + mockVedtakSimuleringPostering(fom = februar2023, beløp = -2, betalingType = BetalingType.KREDIT), + mockVedtakSimuleringPostering(fom = februar2023, beløp = 2, betalingType = BetalingType.DEBIT), + mockVedtakSimuleringPostering(beløp = 2, betalingType = BetalingType.DEBIT), + mockVedtakSimuleringPostering(beløp = -2, betalingType = BetalingType.KREDIT), + mockVedtakSimuleringPostering(beløp = 2, betalingType = BetalingType.DEBIT), + ) + val simuleringMottaker = + listOf(mockØkonomiSimuleringMottaker(behandling = behandling, økonomiSimuleringPostering = posteringer)) + + every { økonomiSimuleringMottakerRepository.findByBehandlingId(behandling.id) } returns simuleringMottaker + every { persongrunnlagService.hentSøkerOgBarnPåBehandling(behandling.id) } returns listOf( + lagPerson(type = PersonType.BARN).tilPersonEnkel(), + lagPerson(type = PersonType.BARN).tilPersonEnkel(), + ) + every { featureToggleService.isEnabled(FeatureToggleConfig.ER_MANUEL_POSTERING_TOGGLE_PÅ) } returns true + + val behandlingHarAvvikInnenforBeløpsgrenser = + simuleringService.harMigreringsbehandlingAvvikInnenforBeløpsgrenser(behandling) + + assertThat(behandlingHarAvvikInnenforBeløpsgrenser, Is(true)) + } + + @ParameterizedTest + @EnumSource(value = BehandlingÅrsak::class, names = ["HELMANUELL_MIGRERING", "ENDRE_MIGRERINGSDATO"]) + fun `harMigreringsbehandlingAvvikInnenforBeløpsgrenser skal returnere true dersom det finnes avvik i form av feilutbetaling som er innenfor beløpsgrense`( + behandlingÅrsak: BehandlingÅrsak, + ) { + val behandling: Behandling = no.nav.familie.ba.sak.common.lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = behandlingÅrsak, + førsteSteg = StegType.VURDER_TILBAKEKREVING, + ) + every { featureToggleService.isEnabled(FeatureToggleConfig.IKKE_STOPP_MIGRERINGSBEHANDLING) } returns false + every { simuleringService.hentFeilutbetaling(behandling.id) } returns BigDecimal(4) + + val fom = LocalDate.of(2021, 1, 1) + val tom = LocalDate.of(2021, 1, 31) + val fom2 = LocalDate.of(2021, 2, 1) + val tom2 = LocalDate.of(2021, 2, 28) + + // feilutbetaling 1 KR per barn i hver periode + val posteringer = listOf( + mockVedtakSimuleringPostering( + fom = fom, + tom = tom, + beløp = 2, + posteringType = PosteringType.FEILUTBETALING, + ), + mockVedtakSimuleringPostering( + fom = fom2, + tom = tom2, + beløp = 2, + posteringType = PosteringType.FEILUTBETALING, + ), + ) + + val simuleringMottaker = + listOf(mockØkonomiSimuleringMottaker(behandling = behandling, økonomiSimuleringPostering = posteringer)) + + every { økonomiSimuleringMottakerRepository.findByBehandlingId(behandling.id) } returns simuleringMottaker + every { persongrunnlagService.hentSøkerOgBarnPåBehandling(behandling.id) } returns listOf( + lagPerson(type = PersonType.BARN).tilPersonEnkel(), + lagPerson(type = PersonType.BARN).tilPersonEnkel(), + ) + every { featureToggleService.isEnabled(FeatureToggleConfig.ER_MANUEL_POSTERING_TOGGLE_PÅ) } returns true + + val behandlingHarAvvikInnenforBeløpsgrenser = + simuleringService.harMigreringsbehandlingAvvikInnenforBeløpsgrenser(behandling) + + assertThat(behandlingHarAvvikInnenforBeløpsgrenser, Is(true)) + } + + @ParameterizedTest + @EnumSource(value = BehandlingÅrsak::class, names = ["HELMANUELL_MIGRERING", "ENDRE_MIGRERINGSDATO"]) + fun `harMigreringsbehandlingAvvikInnenforBeløpsgrenser skal returnere false dersom det finnes avvik i form av feilutbetaling som er utenfor beløpsgrense`( + behandlingÅrsak: BehandlingÅrsak, + ) { + val behandling: Behandling = no.nav.familie.ba.sak.common.lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = behandlingÅrsak, + førsteSteg = StegType.VURDER_TILBAKEKREVING, + ) + every { featureToggleService.isEnabled(FeatureToggleConfig.IKKE_STOPP_MIGRERINGSBEHANDLING) } returns false + every { simuleringService.hentFeilutbetaling(behandling.id) } returns BigDecimal.ZERO + + // etterbetaling 200 KR + val posteringer = listOf( + mockVedtakSimuleringPostering(beløp = 200, betalingType = BetalingType.DEBIT), + mockVedtakSimuleringPostering(beløp = -200, betalingType = BetalingType.KREDIT), + mockVedtakSimuleringPostering(beløp = 200, betalingType = BetalingType.DEBIT), + ) + val simuleringMottaker = + listOf(mockØkonomiSimuleringMottaker(behandling = behandling, økonomiSimuleringPostering = posteringer)) + + every { økonomiSimuleringMottakerRepository.findByBehandlingId(behandling.id) } returns simuleringMottaker + every { persongrunnlagService.hentSøkerOgBarnPåBehandling(behandling.id) } returns listOf( + lagPerson(type = PersonType.BARN).tilPersonEnkel(), + lagPerson(type = PersonType.BARN).tilPersonEnkel(), + ) + every { featureToggleService.isEnabled(FeatureToggleConfig.ER_MANUEL_POSTERING_TOGGLE_PÅ) } returns true + + val behandlingHarAvvikInnenforBeløpsgrenser = + simuleringService.harMigreringsbehandlingAvvikInnenforBeløpsgrenser(behandling) + + assertThat(behandlingHarAvvikInnenforBeløpsgrenser, Is(false)) + } + + @ParameterizedTest + @EnumSource( + value = BehandlingÅrsak::class, + mode = EnumSource.Mode.EXCLUDE, + names = ["HELMANUELL_MIGRERING", "ENDRE_MIGRERINGSDATO"], + ) + fun `harMigreringsbehandlingAvvikInnenforBeløpsgrenser skal kaste feil dersom behandlingen ikke er en manuell migrering`( + behandlingÅrsak: BehandlingÅrsak, + ) { + val behandling: Behandling = no.nav.familie.ba.sak.common.lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = behandlingÅrsak, + førsteSteg = StegType.VURDER_TILBAKEKREVING, + ) + + assertThrows { simuleringService.harMigreringsbehandlingAvvikInnenforBeløpsgrenser(behandling) } + } + + @ParameterizedTest + @EnumSource(value = BehandlingÅrsak::class, names = ["HELMANUELL_MIGRERING", "ENDRE_MIGRERINGSDATO"]) + fun `harMigreringsbehandlingManuellePosteringerFørMars2023 skal returnere true dersom det finnes manuelle posteringer i simuleringsresultat før mars 2023`( + behandlingÅrsak: BehandlingÅrsak, + ) { + val behandling: Behandling = no.nav.familie.ba.sak.common.lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = behandlingÅrsak, + førsteSteg = StegType.VURDER_TILBAKEKREVING, + ) + every { featureToggleService.isEnabled(FeatureToggleConfig.IKKE_STOPP_MIGRERINGSBEHANDLING) } returns false + every { simuleringService.hentFeilutbetaling(behandling.id) } returns BigDecimal.ZERO + + // etterbetaling 200 KR + val posteringer = listOf( + mockVedtakSimuleringPostering(beløp = 200, betalingType = BetalingType.DEBIT), + mockVedtakSimuleringPostering(beløp = -200, betalingType = BetalingType.KREDIT), + mockVedtakSimuleringPostering( + beløp = 200, + betalingType = BetalingType.DEBIT, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD_MANUELT, + ), + ) + val simuleringMottaker = + listOf(mockØkonomiSimuleringMottaker(behandling = behandling, økonomiSimuleringPostering = posteringer)) + + every { økonomiSimuleringMottakerRepository.findByBehandlingId(behandling.id) } returns simuleringMottaker + + val behandlingHarManuellePosteringerFørMars2023 = + simuleringService.harMigreringsbehandlingManuellePosteringer(behandling) + + assertThat(behandlingHarManuellePosteringerFørMars2023, Is(true)) + } + + @ParameterizedTest + @EnumSource( + value = BehandlingÅrsak::class, + names = ["HELMANUELL_MIGRERING", "ENDRE_MIGRERINGSDATO"], + ) + fun `harMigreringsbehandlingManuellePosteringerFørMars2023 skal returnere false dersom det ikke finnes manuelle posteringer i simuleringsresultat før mars 2023`( + behandlingÅrsak: BehandlingÅrsak, + ) { + val behandling: Behandling = no.nav.familie.ba.sak.common.lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = behandlingÅrsak, + førsteSteg = StegType.VURDER_TILBAKEKREVING, + ) + every { featureToggleService.isEnabled(FeatureToggleConfig.IKKE_STOPP_MIGRERINGSBEHANDLING) } returns false + every { simuleringService.hentFeilutbetaling(behandling.id) } returns BigDecimal.ZERO + + // etterbetaling 200 KR + val posteringer = listOf( + mockVedtakSimuleringPostering(beløp = 200, betalingType = BetalingType.DEBIT), + mockVedtakSimuleringPostering(beløp = -200, betalingType = BetalingType.KREDIT), + mockVedtakSimuleringPostering(beløp = 200, betalingType = BetalingType.DEBIT), + ) + val simuleringMottaker = + listOf(mockØkonomiSimuleringMottaker(behandling = behandling, økonomiSimuleringPostering = posteringer)) + + every { økonomiSimuleringMottakerRepository.findByBehandlingId(behandling.id) } returns simuleringMottaker + + val behandlingHarManuellePosteringerFørMars2023 = + simuleringService.harMigreringsbehandlingManuellePosteringer(behandling) + + assertThat(behandlingHarManuellePosteringerFørMars2023, Is(false)) + } + + @ParameterizedTest + @EnumSource( + value = BehandlingÅrsak::class, + mode = EnumSource.Mode.EXCLUDE, + names = ["HELMANUELL_MIGRERING", "ENDRE_MIGRERINGSDATO"], + ) + fun `harMigreringsbehandlingManuellePosteringerFørMars2023 skal kaste feil dersom behandlingen ikke er en manuell migrering`( + behandlingÅrsak: BehandlingÅrsak, + ) { + val behandling: Behandling = no.nav.familie.ba.sak.common.lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = behandlingÅrsak, + førsteSteg = StegType.VURDER_TILBAKEKREVING, + ) + + assertThrows { simuleringService.harMigreringsbehandlingManuellePosteringer(behandling) } + } + + @Test + fun `hentSimuleringFraFamilieOppdrag - skal bruke gammel utbetalingsgenerator når toggel er av`() { + setupMocksForFeatureToggleTests(false) + simuleringService.hentSimuleringFraFamilieOppdrag(lagVedtak()) + + verify(exactly = 1) { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + andelTilkjentYtelseForUtbetalingsoppdragFactory = any(), + erSimulering = any(), + ) + } + + verify(exactly = 0) { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + erSimulering = any(), + ) + } + } + + @Test + fun `hentSimuleringFraFamilieOppdrag - skal bruke ny utbetalingsgenerator når toggel er på`() { + setupMocksForFeatureToggleTests(true) + simuleringService.hentSimuleringFraFamilieOppdrag(lagVedtak()) + + verify(exactly = 0) { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + andelTilkjentYtelseForUtbetalingsoppdragFactory = any(), + erSimulering = any(), + ) + } + + verify(exactly = 1) { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + erSimulering = any(), + ) + } + } + + private fun setupMocksForFeatureToggleTests(togglePå: Boolean) { + every { beregningService.erEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(any()) } returns true + + every { + unleashService.isEnabled( + toggleId = any(), + properties = any(), + ) + } returns togglePå + + val utbetalingsoppdrag = lagUtbetalingsoppdrag( + listOf( + Utbetalingsperiode( + erEndringPåEksisterendePeriode = false, + opphør = null, + periodeId = 1, + forrigePeriodeId = null, + datoForVedtak = LocalDate.now(), + klassifisering = "BATR", + vedtakdatoFom = inneværendeMåned().førsteDagIInneværendeMåned(), + vedtakdatoTom = inneværendeMåned().sisteDagIInneværendeMåned(), + sats = BigDecimal(1054), + satsType = Utbetalingsperiode.SatsType.MND, + utbetalesTil = "13455678910", + behandlingId = 1, + ), + ), + ) + + every { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + andelTilkjentYtelseForUtbetalingsoppdragFactory = any(), + erSimulering = any(), + ) + } returns utbetalingsoppdrag.tilRestUtbetalingsoppdrag() + + every { + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak = any(), + saksbehandlerId = any(), + erSimulering = any(), + ) + } returns BeregnetUtbetalingsoppdragLongId(utbetalingsoppdrag = utbetalingsoppdrag, andeler = emptyList()) + + every { økonomiKlient.hentSimulering(utbetalingsoppdrag.tilRestUtbetalingsoppdrag()) } returns mockk() + + every { + kontrollerNyUtbetalingsgeneratorService.kontrollerNyUtbetalingsgenerator( + vedtak = any(), + gammeltSimuleringResultat = any(), + gammeltUtbetalingsoppdrag = any(), + erSimulering = any(), + ) + } returns mockk() + } + + private fun mockØkonomiSimuleringMottaker( + id: Long = 0, + mottakerNummer: String? = randomFnr(), + mottakerType: MottakerType = MottakerType.BRUKER, + behandling: Behandling = mockk(relaxed = true), + økonomiSimuleringPostering: List<ØkonomiSimuleringPostering> = listOf(mockVedtakSimuleringPostering()), + ) = ØkonomiSimuleringMottaker(id, mottakerNummer, mottakerType, behandling, økonomiSimuleringPostering) + + private fun mockVedtakSimuleringPostering( + økonomiSimuleringMottaker: ØkonomiSimuleringMottaker = mockk(relaxed = true), + beløp: Int = 0, + fagOmrådeKode: FagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom: LocalDate = LocalDate.of(2023, 1, 1), + tom: LocalDate = LocalDate.of(2023, 1, 1), + betalingType: BetalingType = BetalingType.DEBIT, + posteringType: PosteringType = PosteringType.YTELSE, + forfallsdato: LocalDate = LocalDate.of(2023, 1, 1), + utenInntrekk: Boolean = false, + ) = ØkonomiSimuleringPostering( + økonomiSimuleringMottaker = økonomiSimuleringMottaker, + fagOmrådeKode = fagOmrådeKode, + fom = fom, + tom = tom, + betalingType = betalingType, + beløp = beløp.toBigDecimal(), + posteringType = posteringType, + forfallsdato = forfallsdato, + utenInntrekk = utenInntrekk, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringUtilTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringUtilTest.kt new file mode 100644 index 000000000..5c86a1669 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringUtilTest.kt @@ -0,0 +1,695 @@ +package no.nav.familie.ba.sak.kjerne.simulering + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.readValue +import io.mockk.mockk +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.nesteBehandlingId +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.initStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.simulering.domene.ØkonomiSimuleringMottaker +import no.nav.familie.ba.sak.kjerne.simulering.domene.ØkonomiSimuleringPostering +import no.nav.familie.ba.sak.kjerne.steg.FØRSTE_STEG +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.simulering.BetalingType +import no.nav.familie.kontrakter.felles.simulering.DetaljertSimuleringResultat +import no.nav.familie.kontrakter.felles.simulering.FagOmrådeKode +import no.nav.familie.kontrakter.felles.simulering.MottakerType +import no.nav.familie.kontrakter.felles.simulering.PosteringType +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.io.File +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class SimuleringUtilTest { + + private fun mockØkonomiSimuleringMottaker( + id: Long = 0, + mottakerNummer: String? = randomFnr(), + mottakerType: MottakerType = MottakerType.BRUKER, + behandling: Behandling = mockk(relaxed = true), + økonomiSimuleringPostering: List<ØkonomiSimuleringPostering> = listOf(mockVedtakSimuleringPostering()), + ) = ØkonomiSimuleringMottaker(id, mottakerNummer, mottakerType, behandling, økonomiSimuleringPostering) + + private fun mockVedtakSimuleringPostering( + økonomiSimuleringMottaker: ØkonomiSimuleringMottaker = mockk(relaxed = true), + beløp: Int = 0, + fagOmrådeKode: FagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom: LocalDate = LocalDate.now().minusMonths(1), + tom: LocalDate = LocalDate.now().minusMonths(1), + betalingType: BetalingType = if (beløp >= 0) BetalingType.DEBIT else BetalingType.KREDIT, + posteringType: PosteringType = PosteringType.YTELSE, + forfallsdato: LocalDate = LocalDate.now().minusMonths(1), + utenInntrekk: Boolean = false, + ) = ØkonomiSimuleringPostering( + økonomiSimuleringMottaker = økonomiSimuleringMottaker, + fagOmrådeKode = fagOmrådeKode, + fom = fom, + tom = tom, + betalingType = betalingType, + beløp = beløp.toBigDecimal(), + posteringType = posteringType, + forfallsdato = forfallsdato, + utenInntrekk = utenInntrekk, + ) + + fun mockVedtakSimuleringPosteringer( + måned: YearMonth = YearMonth.of(2021, 1), + antallMåneder: Int = 1, + beløp: Int = 5000, + posteringstype: PosteringType = PosteringType.YTELSE, + betalingstype: BetalingType = if (beløp >= 0) BetalingType.DEBIT else BetalingType.KREDIT, + + ): List<ØkonomiSimuleringPostering> = MutableList(antallMåneder) { index -> + ØkonomiSimuleringPostering( + økonomiSimuleringMottaker = mockk(relaxed = true), + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = måned.plusMonths(index.toLong()).atDay(1), + tom = måned.plusMonths(index.toLong()).atEndOfMonth(), + betalingType = betalingstype, + beløp = beløp.toBigDecimal(), + posteringType = posteringstype, + forfallsdato = måned.plusMonths(index.toLong()).atEndOfMonth(), + utenInntrekk = false, + ) + } + + @Test + fun `Test henting av 'nytt beløp ', 'tidligere utbetalt ' og 'resultat ' for simuleringsperiode uten feilutbetaling`() { + val vedtaksimuleringPosteringer = listOf( + mockVedtakSimuleringPostering(beløp = 100, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = 100, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = -99, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = -99, posteringType = PosteringType.YTELSE), + ) + + Assertions.assertEquals(BigDecimal.valueOf(200), hentNyttBeløpIPeriode(vedtaksimuleringPosteringer)) + Assertions.assertEquals(BigDecimal.valueOf(198), hentTidligereUtbetaltIPeriode(vedtaksimuleringPosteringer)) + Assertions.assertEquals(BigDecimal.valueOf(2), hentResultatIPeriode(vedtaksimuleringPosteringer)) + } + + @Test + fun `Test henting av 'nytt beløp', 'tidligere utbetalt' og 'resultat' for simuleringsperiode med feilutbetaling`() { + val økonomiSimuleringPosteringer = listOf( + mockVedtakSimuleringPostering(beløp = 100, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = 100, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = -99, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = -99, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = 98, posteringType = PosteringType.FEILUTBETALING), + mockVedtakSimuleringPostering(beløp = 98, posteringType = PosteringType.FEILUTBETALING), + ) + + Assertions.assertEquals(BigDecimal.valueOf(4), hentNyttBeløpIPeriode(økonomiSimuleringPosteringer)) + Assertions.assertEquals(BigDecimal.valueOf(198), hentTidligereUtbetaltIPeriode(økonomiSimuleringPosteringer)) + Assertions.assertEquals(BigDecimal.valueOf(-196), hentResultatIPeriode(økonomiSimuleringPosteringer)) + } + + @Test + fun `Test 'nytt beløp', 'tidligere utbetalt' og 'resultat' for simuleringsperiode med reduksjon i feilutbetaling`() { + val økonomiSimuleringPosteringer = listOf( + mockVedtakSimuleringPostering(beløp = 100, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = 100, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = -99, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = -99, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = 98, posteringType = PosteringType.FEILUTBETALING), + mockVedtakSimuleringPostering(beløp = -99, posteringType = PosteringType.FEILUTBETALING), + ) + + Assertions.assertEquals(BigDecimal.valueOf(200), hentNyttBeløpIPeriode(økonomiSimuleringPosteringer)) + Assertions.assertEquals(BigDecimal.valueOf(197), hentTidligereUtbetaltIPeriode(økonomiSimuleringPosteringer)) + Assertions.assertEquals(BigDecimal.valueOf(3), hentResultatIPeriode(økonomiSimuleringPosteringer)) + } + + private val økonomiSimuleringPosteringerMedNegativFeilutbetaling = listOf( + mockVedtakSimuleringPostering(beløp = -500, posteringType = PosteringType.FEILUTBETALING), + mockVedtakSimuleringPostering(beløp = -2000, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = 3000, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = -500, posteringType = PosteringType.YTELSE), + ) + + @Test + fun `Total etterbetaling skal bli summen av ytelsene i periode med negativ feilutbetaling`() { + val økonomiSimuleringMottaker = + mockØkonomiSimuleringMottaker(økonomiSimuleringPostering = økonomiSimuleringPosteringerMedNegativFeilutbetaling) + val restSimulering = vedtakSimuleringMottakereTilRestSimulering(listOf(økonomiSimuleringMottaker), false) + + Assertions.assertEquals(BigDecimal.valueOf(500), restSimulering.etterbetaling) + } + + @Test + fun `Total feilutbetaling skal bli 0 i periode med negativ feilutbetaling`() { + val økonomiSimuleringMottaker = + mockØkonomiSimuleringMottaker(økonomiSimuleringPostering = økonomiSimuleringPosteringerMedNegativFeilutbetaling) + val restSimulering = vedtakSimuleringMottakereTilRestSimulering(listOf(økonomiSimuleringMottaker), false) + + Assertions.assertEquals(BigDecimal.valueOf(0), restSimulering.feilutbetaling) + } + + @Test + fun `Skal gi 0 etterbetaling og sum feilutbetaling ved positiv feilutbetaling`() { + val økonomiSimuleringPosteringerMedPositivFeilutbetaling = listOf( + mockVedtakSimuleringPostering(beløp = 500, posteringType = PosteringType.FEILUTBETALING), + mockVedtakSimuleringPostering(beløp = -2000, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = 3000, posteringType = PosteringType.YTELSE), + mockVedtakSimuleringPostering(beløp = -500, posteringType = PosteringType.YTELSE), + ) + + val økonomiSimuleringMottaker = + mockØkonomiSimuleringMottaker(økonomiSimuleringPostering = økonomiSimuleringPosteringerMedPositivFeilutbetaling) + val restSimulering = vedtakSimuleringMottakereTilRestSimulering(listOf(økonomiSimuleringMottaker), false) + + Assertions.assertEquals(BigDecimal.valueOf(0), restSimulering.etterbetaling) + Assertions.assertEquals(BigDecimal.valueOf(500), restSimulering.feilutbetaling) + } + + @Test + fun `Test at bare perioder med passert forfalldato blir inludert i summeringen av etterbetaling`() { + val vedtaksimuleringPosteringer = listOf( + mockVedtakSimuleringPostering( + beløp = 100, + posteringType = PosteringType.YTELSE, + forfallsdato = LocalDate.now().plusDays(1), + ), + mockVedtakSimuleringPostering( + beløp = 200, + posteringType = PosteringType.YTELSE, + forfallsdato = LocalDate.now().minusDays(1), + ), + ) + + Assertions.assertEquals( + BigDecimal.valueOf(200), + hentEtterbetalingIPeriode( + vedtaksimuleringPosteringer, + LocalDate.now(), + ), + ) + } + + /* + De neste testene antar at brukeren går gjennom følgende for ÉN periode: + - Førstegangsbehandling gir ytelse på kr 10 000 + - Revurdering reduserer ytelse fra kr 10 000 til kr 2 000, dvs kr 8 000 feilutbetalt + - Revurdering øker ytelse fra kr 2 000 til kr 3 000, dvs feilutbetaling reduseres + - Revurdering øker ytelse fra kr 3 000 tik kr 12 000, dvs feilutbetaling nulles ut, og etterbetaling skjer + */ + @Test + fun `ytelse på 10000 korrigert til 2000`() { + val redusertYtelseTil2_000 = listOf( + mockVedtakSimuleringPostering( + beløp = -10_000, + posteringType = PosteringType.YTELSE, + betalingType = BetalingType.KREDIT, + ), // Forrige + mockVedtakSimuleringPostering( + beløp = 2_000, + posteringType = PosteringType.YTELSE, + betalingType = BetalingType.DEBIT, + ), // Ny + mockVedtakSimuleringPostering( + beløp = 8_000, + posteringType = PosteringType.FEILUTBETALING, + betalingType = BetalingType.DEBIT, + ), // Feilutbetaling + mockVedtakSimuleringPostering( + beløp = -8_000, + posteringType = PosteringType.MOTP, + betalingType = BetalingType.KREDIT, + ), // "Nuller ut" Feilutbetalingen + mockVedtakSimuleringPostering( + beløp = 8_000, + posteringType = PosteringType.YTELSE, + betalingType = BetalingType.DEBIT, + ), // "Nuller ut" forrige og ny + ) + + val økonomiSimuleringMottakere = + listOf(mockØkonomiSimuleringMottaker(økonomiSimuleringPostering = redusertYtelseTil2_000)) + val simuleringsperioder = vedtakSimuleringMottakereTilSimuleringPerioder(økonomiSimuleringMottakere, true) + val oppsummering = vedtakSimuleringMottakereTilRestSimulering(økonomiSimuleringMottakere, true) + + assertThat(simuleringsperioder.size).isEqualTo(1) + assertThat(simuleringsperioder[0].tidligereUtbetalt).isEqualTo(10_000.toBigDecimal()) + assertThat(simuleringsperioder[0].nyttBeløp).isEqualTo(2_000.toBigDecimal()) + assertThat(simuleringsperioder[0].resultat).isEqualTo(-8_000.toBigDecimal()) + assertThat(simuleringsperioder[0].feilutbetaling).isEqualTo(8_000.toBigDecimal()) + assertThat(oppsummering.etterbetaling).isEqualTo(0.toBigDecimal()) + } + + @Test + fun `ytelse med manuelle posteringer på trekk av 770 over 3 mnd`() { + val fil = File("./src/test/resources/kjerne/simulering/simulering_med_manuell_postering.json") + + val ytelseMedManuellePosteringer = + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true) + .readValue(fil) + + val vedtakSimuleringMottakere = ytelseMedManuellePosteringer.simuleringMottaker.map { + it.tilBehandlingSimuleringMottaker( + lagBehandling(), + ) + } + + val simuleringsperioder = vedtakSimuleringMottakereTilSimuleringPerioder(vedtakSimuleringMottakere, true) + val oppsummering = vedtakSimuleringMottakereTilRestSimulering(vedtakSimuleringMottakere, true) + + val simuleringJanuar22 = simuleringsperioder.single { it.fom == LocalDate.of(2022, 1, 1) } + val simuleringFebruar22 = simuleringsperioder.single { it.fom == LocalDate.of(2022, 2, 1) } + val simuleringMars22 = simuleringsperioder.single { it.fom == LocalDate.of(2022, 3, 1) } + val simuleringApril22 = simuleringsperioder.single { it.fom == LocalDate.of(2022, 4, 1) } + + assertThat(simuleringJanuar22.tidligereUtbetalt).isEqualTo(305.toBigDecimal()) + assertThat(simuleringJanuar22.resultat).isEqualTo(0.toBigDecimal()) + assertThat(simuleringJanuar22.manuellPostering).isEqualTo(0.toBigDecimal()) + + assertThat(simuleringFebruar22.tidligereUtbetalt).isEqualTo(0.toBigDecimal()) + assertThat(simuleringFebruar22.resultat).isEqualTo(305.toBigDecimal()) + assertThat(simuleringFebruar22.manuellPostering).isEqualTo(305.toBigDecimal()) + + assertThat(simuleringMars22.tidligereUtbetalt).isEqualTo(0.toBigDecimal()) + assertThat(simuleringMars22.resultat).isEqualTo(305.toBigDecimal()) + assertThat(simuleringMars22.manuellPostering).isEqualTo(305.toBigDecimal()) + + assertThat(simuleringApril22.tidligereUtbetalt).isEqualTo(140.toBigDecimal()) + assertThat(simuleringApril22.resultat).isEqualTo(165.toBigDecimal()) + assertThat(simuleringApril22.manuellPostering).isEqualTo(165.toBigDecimal()) + + assertThat(simuleringsperioder.sumOf { it.manuellPostering }).isEqualTo(775.toBigDecimal()) + assertThat(oppsummering.etterbetaling).isEqualTo(775.toBigDecimal()) + } + + @Test + fun `ytelse på 2000 korrigert til 3000`() { + val øktYtelseFra2_000Til3_000 = listOf( + mockVedtakSimuleringPostering( + beløp = -2_000, + posteringType = PosteringType.YTELSE, + betalingType = BetalingType.KREDIT, + ), + mockVedtakSimuleringPostering( + beløp = 3_000, + posteringType = PosteringType.YTELSE, + betalingType = BetalingType.DEBIT, + ), + mockVedtakSimuleringPostering( + beløp = -1_000, + posteringType = PosteringType.FEILUTBETALING, + betalingType = BetalingType.KREDIT, + ), // Reduser feilutbetaling + mockVedtakSimuleringPostering( + beløp = 1_000, + posteringType = PosteringType.MOTP, + betalingType = BetalingType.DEBIT, + ), + mockVedtakSimuleringPostering( + beløp = -1_000, + posteringType = PosteringType.YTELSE, + betalingType = BetalingType.KREDIT, + ), + ) + + val økonomiSimuleringMottakere = + listOf(mockØkonomiSimuleringMottaker(økonomiSimuleringPostering = øktYtelseFra2_000Til3_000)) + val simuleringsperioder = vedtakSimuleringMottakereTilSimuleringPerioder(økonomiSimuleringMottakere, false) + val oppsummering = vedtakSimuleringMottakereTilRestSimulering(økonomiSimuleringMottakere, false) + + assertThat(simuleringsperioder.size).isEqualTo(1) + assertThat(simuleringsperioder[0].tidligereUtbetalt).isEqualTo(2_000.toBigDecimal()) + assertThat(simuleringsperioder[0].nyttBeløp).isEqualTo(3_000.toBigDecimal()) + assertThat(simuleringsperioder[0].resultat).isEqualTo(1_000.toBigDecimal()) + assertThat(simuleringsperioder[0].feilutbetaling).isEqualTo(0.toBigDecimal()) + assertThat(oppsummering.etterbetaling).isEqualTo(0.toBigDecimal()) + } + + @Test + fun `ytelse med manuellt trekk av valutajustering deler er trukket`() { + val YtelsefraBA = listOf( + mockVedtakSimuleringPostering( + beløp = 305, + posteringType = PosteringType.YTELSE, + ), + mockVedtakSimuleringPostering( + beløp = -198, + posteringType = PosteringType.JUSTERING, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + ), + mockVedtakSimuleringPostering( + beløp = 305, + posteringType = PosteringType.JUSTERING, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD, + ), + mockVedtakSimuleringPostering( + beløp = -305, + posteringType = PosteringType.YTELSE, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD, + ), + mockVedtakSimuleringPostering( + beløp = -107, + posteringType = PosteringType.JUSTERING, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD_MANUELT, + ), + mockVedtakSimuleringPostering( + beløp = 165, + posteringType = PosteringType.YTELSE, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD_MANUELT, + ), + ) + + val økonomiSimuleringMottakere = + listOf(mockØkonomiSimuleringMottaker(økonomiSimuleringPostering = YtelsefraBA)) + val simuleringsperioder = vedtakSimuleringMottakereTilSimuleringPerioder(økonomiSimuleringMottakere, true) + val oppsummering = vedtakSimuleringMottakereTilRestSimulering(økonomiSimuleringMottakere, true) + + assertThat(simuleringsperioder.size).isEqualTo(1) + assertThat(simuleringsperioder[0].nyttBeløp).isEqualTo(305.toBigDecimal()) + assertThat(simuleringsperioder[0].manuellPostering).isEqualTo(165.toBigDecimal()) + assertThat(simuleringsperioder[0].tidligereUtbetalt).isEqualTo(140.toBigDecimal()) + assertThat(simuleringsperioder[0].resultat).isEqualTo(165.toBigDecimal()) + assertThat(simuleringsperioder[0].feilutbetaling).isEqualTo(0.toBigDecimal()) + assertThat(oppsummering.etterbetaling).isEqualTo(165.toBigDecimal()) + } + + @Test + fun `ytelse med manuellt trekk av valutajustering trukket på fagområdekode MBA`() { + val YtelsefraBA = listOf( + mockVedtakSimuleringPostering( + beløp = 305, + posteringType = PosteringType.YTELSE, + ), + mockVedtakSimuleringPostering( + beløp = -198, + posteringType = PosteringType.JUSTERING, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + ), + mockVedtakSimuleringPostering( + beløp = 305, + posteringType = PosteringType.JUSTERING, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_MANUELT, + ), + mockVedtakSimuleringPostering( + beløp = -305, + posteringType = PosteringType.YTELSE, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD, + ), + mockVedtakSimuleringPostering( + beløp = -107, + posteringType = PosteringType.JUSTERING, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_MANUELT, + ), + mockVedtakSimuleringPostering( + beløp = 165, + posteringType = PosteringType.YTELSE, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_MANUELT, + ), + ) + + val økonomiSimuleringMottakere = + listOf(mockØkonomiSimuleringMottaker(økonomiSimuleringPostering = YtelsefraBA)) + val simuleringsperioder = vedtakSimuleringMottakereTilSimuleringPerioder(økonomiSimuleringMottakere, true) + val oppsummering = vedtakSimuleringMottakereTilRestSimulering(økonomiSimuleringMottakere, true) + + assertThat(simuleringsperioder.size).isEqualTo(1) + assertThat(simuleringsperioder[0].nyttBeløp).isEqualTo(305.toBigDecimal()) + assertThat(simuleringsperioder[0].manuellPostering).isEqualTo((165).toBigDecimal()) + assertThat(simuleringsperioder[0].tidligereUtbetalt).isEqualTo(140.toBigDecimal()) + assertThat(simuleringsperioder[0].resultat).isEqualTo(165.toBigDecimal()) + assertThat(simuleringsperioder[0].feilutbetaling).isEqualTo(0.toBigDecimal()) + assertThat(oppsummering.etterbetaling).isEqualTo(165.toBigDecimal()) + } + + @Test + fun `ytelse med manuellt trekk av valutajustering alt er trukket`() { + val YtelsefraBA = listOf( + mockVedtakSimuleringPostering( + beløp = 305, + posteringType = PosteringType.YTELSE, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + ), + mockVedtakSimuleringPostering( + beløp = -153, + posteringType = PosteringType.JUSTERING, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + ), + mockVedtakSimuleringPostering( + beløp = 306, + posteringType = PosteringType.JUSTERING, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD, + ), + mockVedtakSimuleringPostering( + beløp = -305, + posteringType = PosteringType.YTELSE, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD, + ), + mockVedtakSimuleringPostering( + beløp = -153, + posteringType = PosteringType.JUSTERING, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD_MANUELT, + ), + mockVedtakSimuleringPostering( + beløp = 305, + posteringType = PosteringType.YTELSE, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD_MANUELT, + ), + ) + + val økonomiSimuleringMottakere = + listOf(mockØkonomiSimuleringMottaker(økonomiSimuleringPostering = YtelsefraBA)) + val simuleringsperioder = vedtakSimuleringMottakereTilSimuleringPerioder(økonomiSimuleringMottakere, true) + val oppsummering = vedtakSimuleringMottakereTilRestSimulering(økonomiSimuleringMottakere, true) + + val simuleringsperiode = simuleringsperioder.single() + + assertThat(simuleringsperiode.nyttBeløp).isEqualTo(305.toBigDecimal()) + assertThat(simuleringsperiode.manuellPostering).isEqualTo(305.toBigDecimal()) + assertThat(simuleringsperiode.tidligereUtbetalt).isEqualTo(0.toBigDecimal()) + assertThat(simuleringsperiode.resultat).isEqualTo(305.toBigDecimal()) + assertThat(simuleringsperiode.feilutbetaling).isEqualTo(0.toBigDecimal()) + assertThat(oppsummering.etterbetaling).isEqualTo(305.toBigDecimal()) + } + + @Test + fun `ytelse på 3000 korrigert til 12000`() { + val øktYtelseFra3_000Til12_000 = listOf( + mockVedtakSimuleringPostering( + beløp = -3_000, + posteringType = PosteringType.YTELSE, + betalingType = BetalingType.KREDIT, + ), + mockVedtakSimuleringPostering( + beløp = 12_000, + posteringType = PosteringType.YTELSE, + betalingType = BetalingType.DEBIT, + ), + mockVedtakSimuleringPostering( + beløp = -7_000, + posteringType = PosteringType.FEILUTBETALING, + betalingType = BetalingType.KREDIT, + ), // Reduser feilutb + mockVedtakSimuleringPostering( + beløp = 7_000, + posteringType = PosteringType.MOTP, + betalingType = BetalingType.DEBIT, + ), + mockVedtakSimuleringPostering( + beløp = -7_000, + posteringType = PosteringType.YTELSE, + betalingType = BetalingType.KREDIT, + ), + ) + + val økonomiSimuleringMottakere = + listOf(mockØkonomiSimuleringMottaker(økonomiSimuleringPostering = øktYtelseFra3_000Til12_000)) + val simuleringsperioder = vedtakSimuleringMottakereTilSimuleringPerioder(økonomiSimuleringMottakere, true) + val oppsummering = vedtakSimuleringMottakereTilRestSimulering(økonomiSimuleringMottakere, true) + + assertThat(simuleringsperioder.size).isEqualTo(1) + assertThat(simuleringsperioder[0].tidligereUtbetalt).isEqualTo(3_000.toBigDecimal()) + assertThat(simuleringsperioder[0].nyttBeløp).isEqualTo(12_000.toBigDecimal()) + assertThat(simuleringsperioder[0].resultat).isEqualTo(9_000.toBigDecimal()) + assertThat(simuleringsperioder[0].feilutbetaling).isEqualTo(0.toBigDecimal()) + assertThat(oppsummering.etterbetaling).isEqualTo(2_000.toBigDecimal()) + } + + /* + De neste testene antar at brukeren går gjennom følgende førstegangsbehandling og revurderinger i november 2021: + 2021 Feb Mar Apr Mai Jun Jul Aug Sep Okt Nov + 18/11 17153 17153 17153 18195 18195 18195 18195 18195 18195 + 22/11 17153 17153 17153 17257 17257 17257 17257 18195 18195 + 23/11 17341 17341 17341 18382 18382 18382 18382 18382 18382 18382 + */ + + @Test + fun `førstegangsbehandling 18 nov`() { + val førstegangsbehandling_18_nov = + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 2), 3, 17_153, PosteringType.YTELSE) + + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 5), 6, 18_195, PosteringType.YTELSE) + + val økonomiSimuleringMottakere = + listOf(mockØkonomiSimuleringMottaker(økonomiSimuleringPostering = førstegangsbehandling_18_nov)) + val oppsummering = vedtakSimuleringMottakereTilRestSimulering(økonomiSimuleringMottakere, false) + + assertThat(oppsummering.feilutbetaling).isEqualTo(0.toBigDecimal()) + assertThat(oppsummering.etterbetaling).isEqualTo(160_629.toBigDecimal()) + } + + @Test + fun `revurdering 22 nov`() { + val revurering_22_nov = + // Forrige ytelse + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 2), 3, -17_153, PosteringType.YTELSE) + + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 5), 6, -18_195, PosteringType.YTELSE) + + // Ny ytelse + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 2), 3, 17_153, PosteringType.YTELSE) + + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 5), 4, 17_257, PosteringType.YTELSE) + + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 9), 2, 18_195, PosteringType.YTELSE) + + // Feilutbetaling + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 5), 4, 938, PosteringType.FEILUTBETALING) + + // Motpost feilutbetaling + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 5), 4, -938, PosteringType.MOTP) + + // Teknisk postering + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 5), 4, 938, PosteringType.YTELSE) + + val økonomiSimuleringMottakere = + listOf(mockØkonomiSimuleringMottaker(økonomiSimuleringPostering = revurering_22_nov)) + val oppsummering = vedtakSimuleringMottakereTilRestSimulering(økonomiSimuleringMottakere, false) + + assertThat(oppsummering.feilutbetaling).isEqualTo(3_752.toBigDecimal()) + assertThat(oppsummering.etterbetaling).isEqualTo(0.toBigDecimal()) + } + + @Test + fun `revurdering 23 nov`() { + val revurdering_23_nov = + // Forrige ytelse + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 2), 3, -17_153, PosteringType.YTELSE) + + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 5), 4, -17_257, PosteringType.YTELSE) + + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 9), 2, -18_195, PosteringType.YTELSE) + + // Ny ytelse + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 2), 3, 17_341, PosteringType.YTELSE) + + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 5), 7, 18_382, PosteringType.YTELSE) + + // Teknisk postering + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 5), 4, -938, PosteringType.YTELSE) + + // Reduser feilutbetaling til null + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 5), 4, -938, PosteringType.FEILUTBETALING) + + // Motpost feilutbetaling + mockVedtakSimuleringPosteringer(YearMonth.of(2021, 5), 4, 938, PosteringType.MOTP) + + val økonomiSimuleringMottakere = + listOf(mockØkonomiSimuleringMottaker(økonomiSimuleringPostering = revurdering_23_nov)) + val simuleringsperioder = vedtakSimuleringMottakereTilSimuleringPerioder(økonomiSimuleringMottakere, false) + val oppsummering = vedtakSimuleringMottakereTilRestSimulering(økonomiSimuleringMottakere, false) + + (3..6).forEach { + assertThat(simuleringsperioder[it].tidligereUtbetalt).isEqualTo(17_257.toBigDecimal()) + assertThat(simuleringsperioder[it].resultat).isEqualTo(1_125.toBigDecimal()) + } + + assertThat(oppsummering.feilutbetaling).isEqualTo(0.toBigDecimal()) + assertThat(oppsummering.etterbetaling).isEqualTo(20_068.toBigDecimal()) // 1 686 hvis revurderingen ble gjort nov 2021, ikke "i dag" + } + + @Test + fun `ytelse med ikke reelle feilutbetalinger skal gi riktig resultat`() { + val ytelseMetMotposteringerOgManuellePosteringer = listOf( + mockVedtakSimuleringPostering( + beløp = 658, + posteringType = PosteringType.YTELSE, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + ), + mockVedtakSimuleringPostering( + beløp = -657, + posteringType = PosteringType.YTELSE, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD, + ), + mockVedtakSimuleringPostering( + beløp = -50, + posteringType = PosteringType.YTELSE, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD_MANUELT, + ), + + mockVedtakSimuleringPostering( + beløp = 46, + posteringType = PosteringType.YTELSE, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD, + ), + mockVedtakSimuleringPostering( + beløp = 46, + posteringType = PosteringType.FEILUTBETALING, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD, + ), + mockVedtakSimuleringPostering( + beløp = -46, + posteringType = PosteringType.MOTP, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD, + ), + + mockVedtakSimuleringPostering( + beløp = 3, + posteringType = PosteringType.YTELSE, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD_MANUELT, + ), + mockVedtakSimuleringPostering( + beløp = 3, + posteringType = PosteringType.FEILUTBETALING, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD_MANUELT, + ), + mockVedtakSimuleringPostering( + beløp = -3, + posteringType = PosteringType.MOTP, + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD_MANUELT, + ), + ) + + val økonomiSimuleringMottakere = + listOf(mockØkonomiSimuleringMottaker(økonomiSimuleringPostering = ytelseMetMotposteringerOgManuellePosteringer)) + val simuleringsperioder = vedtakSimuleringMottakereTilSimuleringPerioder(økonomiSimuleringMottakere, true) + + val simuleringsperiode = simuleringsperioder.single() + + assertThat(simuleringsperiode.nyttBeløp).isEqualTo(658.toBigDecimal()) + assertThat(simuleringsperiode.manuellPostering).isEqualTo((-50).toBigDecimal()) + assertThat(simuleringsperiode.tidligereUtbetalt).isEqualTo(707.toBigDecimal()) + assertThat(simuleringsperiode.feilutbetaling).isEqualTo((49).toBigDecimal()) + assertThat(simuleringsperiode.resultat).isEqualTo((-49).toBigDecimal()) + assertThat(simuleringsperiode.etterbetaling).isEqualTo((0).toBigDecimal()) + } +} + +fun lagBehandling( + fagsak: Fagsak = defaultFagsak(), + behandlingKategori: BehandlingKategori = BehandlingKategori.NASJONAL, + behandlingType: BehandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak: BehandlingÅrsak = BehandlingÅrsak.SØKNAD, + skalBehandlesAutomatisk: Boolean = false, + førsteSteg: StegType = FØRSTE_STEG, + resultat: Behandlingsresultat = Behandlingsresultat.IKKE_VURDERT, + underkategori: BehandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + status: BehandlingStatus = initStatus(), +) = + Behandling( + id = nesteBehandlingId(), + fagsak = fagsak, + skalBehandlesAutomatisk = skalBehandlesAutomatisk, + type = behandlingType, + kategori = behandlingKategori, + underkategori = underkategori, + opprettetÅrsak = årsak, + resultat = resultat, + status = status, + ).also { + it.behandlingStegTilstand.add(BehandlingStegTilstand(0, it, førsteSteg)) + } diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/sm\303\245barnstilleggkorrigering/Sm\303\245barnstilleggKorrigeringServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/sm\303\245barnstilleggkorrigering/Sm\303\245barnstilleggKorrigeringServiceTest.kt" new file mode 100644 index 000000000..646115d23 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/sm\303\245barnstilleggkorrigering/Sm\303\245barnstilleggKorrigeringServiceTest.kt" @@ -0,0 +1,143 @@ +package no.nav.familie.ba.sak.kjerne.småbarnstilleggkorrigering + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import org.hamcrest.MatcherAssert.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import java.time.YearMonth +import org.hamcrest.CoreMatchers.`is` as Is + +@ExtendWith(MockKExtension::class) +internal class SmåbarnstilleggKorrigeringServiceTest { + + @MockK + private lateinit var tilkjentYtelseRepository: TilkjentYtelseRepository + + @MockK(relaxed = true) + private lateinit var loggService: LoggService + + @InjectMockKs + private lateinit var småbarnstilleggKorrigeringService: SmåbarnstilleggKorrigeringService + + @Test + fun `leggTilSmåbarnstilleggPåBehandling skal legge til småbarnstillegg på behandling som en AndelTilkjentYtelse`() { + val behandling = lagBehandling() + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling = behandling) + + every { tilkjentYtelseRepository.findByBehandling(behandling.id) } returns tilkjentYtelse + + val småbarnsTillegg = + småbarnstilleggKorrigeringService.leggTilSmåbarnstilleggPåBehandling(YearMonth.of(2020, 10), behandling) + + verify(exactly = 1) { tilkjentYtelseRepository.findByBehandling(behandling.id) } + verify(exactly = 1) { + loggService.opprettSmåbarnstilleggLogg( + behandling, + "Småbarnstillegg for oktober 2020 lagt til", + ) + } + + assertThat(småbarnsTillegg.size, Is(1)) + assertThat(småbarnsTillegg[0].type, Is(YtelseType.SMÅBARNSTILLEGG)) + assertThat(småbarnsTillegg[0].stønadFom, Is(YearMonth.of(2020, 10))) + assertThat(småbarnsTillegg[0].stønadTom, Is(YearMonth.of(2020, 10))) + } + + @Test + fun `leggTilSmåbarnstilleggPåBehandling skal kaste feil hvis småbarnstillegg allerede finnes for periode`() { + val behandling = lagBehandling() + val tilkjentYtelseMock = mockk() + + val andelTilkjentYtelse = lagAndelTilkjentYtelse( + fom = YearMonth.of(2010, 10), + tom = YearMonth.of(2020, 10), + ytelseType = YtelseType.SMÅBARNSTILLEGG, + ) + + every { tilkjentYtelseRepository.findByBehandling(behandling.id) } returns tilkjentYtelseMock + every { tilkjentYtelseMock.andelerTilkjentYtelse } returns mutableSetOf(andelTilkjentYtelse) + + val feil = assertThrows { + småbarnstilleggKorrigeringService.leggTilSmåbarnstilleggPåBehandling(YearMonth.of(2020, 10), behandling) + } + + assertThat( + feil.melding, + Is("Det er ikke mulig å legge til småbarnstillegg for oktober 2020 fordi det allerede finnes småbarnstillegg for denne perioden"), + ) + } + + @Test + fun `fjernSmåbarnstilleggPåBehandling skal splitte eksisterende overlappende småbarnstilleggsperiode`() { + val behandling = lagBehandling() + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling = behandling) + + tilkjentYtelse.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2010, 10), + tom = YearMonth.of(2020, 10), + ytelseType = YtelseType.SMÅBARNSTILLEGG, + ), + ) + + every { tilkjentYtelseRepository.findByBehandling(behandling.id) } returns tilkjentYtelse + every { tilkjentYtelseRepository.saveAndFlush(any()) } returns tilkjentYtelse + + val oppsplittetSmåbarnstillegg = + småbarnstilleggKorrigeringService.fjernSmåbarnstilleggPåBehandling(YearMonth.of(2020, 5), behandling) + + verify(exactly = 1) { + loggService.opprettSmåbarnstilleggLogg( + behandling, + "Småbarnstillegg for mai 2020 fjernet", + ) + } + + assertThat(oppsplittetSmåbarnstillegg.size, Is(2)) + + assertThat(oppsplittetSmåbarnstillegg[0].stønadFom, Is(YearMonth.of(2010, 10))) + assertThat(oppsplittetSmåbarnstillegg[0].stønadTom, Is(YearMonth.of(2020, 4))) + + assertThat(oppsplittetSmåbarnstillegg[1].stønadFom, Is(YearMonth.of(2020, 6))) + assertThat(oppsplittetSmåbarnstillegg[1].stønadTom, Is(YearMonth.of(2020, 10))) + } + + @Test + fun `fjernSmåbarnstilleggPåBehandling skal kaste feil hvis småbarnstillegg ikke finnes for periode`() { + val behandling = lagBehandling() + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling = behandling) + + tilkjentYtelse.andelerTilkjentYtelse.add( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2010, 10), + tom = YearMonth.of(2020, 10), + ytelseType = YtelseType.SMÅBARNSTILLEGG, + ), + ) + + every { tilkjentYtelseRepository.findByBehandling(behandling.id) } returns tilkjentYtelse + + val feil = assertThrows { + småbarnstilleggKorrigeringService.fjernSmåbarnstilleggPåBehandling(YearMonth.of(2025, 5), behandling) + } + + assertThat( + feil.melding, + Is("Det er ikke mulig å fjerne småbarnstillegg for mai 2025 fordi det ikke finnes småbarnstillegg for denne perioden"), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/BehandlingStegTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/BehandlingStegTest.kt new file mode 100644 index 000000000..197f6aa63 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/BehandlingStegTest.kt @@ -0,0 +1,413 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class BehandlingStegTest { + + @Test + fun `Tester rekkefølgen på behandling ENDRE_MIGRERINGSDATO`() { + var steg = FØRSTE_STEG + + listOf( + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + StegType.VURDER_TILBAKEKREVING, + StegType.SEND_TIL_BESLUTTER, + StegType.BESLUTTE_VEDTAK, + StegType.FERDIGSTILLE_BEHANDLING, + StegType.BEHANDLING_AVSLUTTET, + ).forEach { + assertEquals(steg, it) + steg = hentNesteSteg( + behandling = lagBehandling( + årsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + ), + utførendeStegType = it, + ) + } + } + + @Test + fun `Tester rekkefølgen på behandling av søknad ved endringer i utbetaling`() { + var steg = FØRSTE_STEG + + listOf( + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.REGISTRERE_SØKNAD, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + StegType.VURDER_TILBAKEKREVING, + StegType.SEND_TIL_BESLUTTER, + StegType.BESLUTTE_VEDTAK, + StegType.IVERKSETT_MOT_OPPDRAG, + StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI, + StegType.IVERKSETT_MOT_FAMILIE_TILBAKE, + StegType.JOURNALFØR_VEDTAKSBREV, + StegType.DISTRIBUER_VEDTAKSBREV, + StegType.FERDIGSTILLE_BEHANDLING, + StegType.BEHANDLING_AVSLUTTET, + ).forEach { + assertEquals(steg, it) + steg = hentNesteSteg( + behandling = lagBehandling( + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak = BehandlingÅrsak.SØKNAD, + ), + utførendeStegType = it, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING, + ) + } + } + + @Test + fun `Tester rekkefølgen på behandling av søknad ved ingen endringer i utbetaling`() { + var steg = FØRSTE_STEG + + listOf( + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.REGISTRERE_SØKNAD, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + StegType.VURDER_TILBAKEKREVING, + StegType.SEND_TIL_BESLUTTER, + StegType.BESLUTTE_VEDTAK, + StegType.JOURNALFØR_VEDTAKSBREV, + StegType.DISTRIBUER_VEDTAKSBREV, + StegType.FERDIGSTILLE_BEHANDLING, + StegType.BEHANDLING_AVSLUTTET, + ).forEach { + assertEquals(steg, it) + steg = hentNesteSteg( + behandling = lagBehandling( + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak = BehandlingÅrsak.SØKNAD, + ), + utførendeStegType = it, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING, + ) + } + } + + @Test + fun `Tester rekkefølgen på behandling av fødselshendelser ved endringer i utbetaling`() { + var steg = FØRSTE_STEG + + listOf( + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.FILTRERING_FØDSELSHENDELSER, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + StegType.IVERKSETT_MOT_OPPDRAG, + StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI, + StegType.JOURNALFØR_VEDTAKSBREV, + StegType.DISTRIBUER_VEDTAKSBREV, + StegType.FERDIGSTILLE_BEHANDLING, + StegType.BEHANDLING_AVSLUTTET, + ).forEach { + assertEquals(steg, it) + steg = hentNesteSteg( + behandling = lagBehandling( + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak = BehandlingÅrsak.FØDSELSHENDELSE, + ), + utførendeStegType = it, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING, + ) + } + } + + @Test + fun `Tester rekkefølgen på behandling av fødselshendelser ved ingen endringer i utbetaling`() { + var steg = FØRSTE_STEG + + listOf( + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.FILTRERING_FØDSELSHENDELSER, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + StegType.HENLEGG_BEHANDLING, + ).forEach { + assertEquals(steg, it) + steg = hentNesteSteg( + behandling = lagBehandling( + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak = BehandlingÅrsak.FØDSELSHENDELSE, + ), + utførendeStegType = it, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING, + ) + } + } + + @Test + fun `Tester at neste steg for migrering fra infotrygd kaster feil at denne ikke er mulig å behandle lenger`() { + val feil = assertThrows { + hentNesteSteg( + behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.MIGRERING, + ), + utførendeStegType = FØRSTE_STEG, + ) + } + assertEquals("Maskinell migrering er ikke mulig å behandle lenger", feil.message) + } + + @Test + fun `Tester rekkefølgen på behandling av type teknisk endring ved endringer i utbetaling`() { + var steg = FØRSTE_STEG + + listOf( + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + StegType.VURDER_TILBAKEKREVING, + StegType.SEND_TIL_BESLUTTER, + StegType.BESLUTTE_VEDTAK, + StegType.IVERKSETT_MOT_OPPDRAG, + StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI, + StegType.FERDIGSTILLE_BEHANDLING, + StegType.BEHANDLING_AVSLUTTET, + ).forEach { + assertEquals(steg, it) + assertNotEquals(StegType.JOURNALFØR_VEDTAKSBREV, it) + steg = hentNesteSteg( + behandling = lagBehandling( + behandlingType = BehandlingType.TEKNISK_ENDRING, + årsak = BehandlingÅrsak.TEKNISK_ENDRING, + ), + utførendeStegType = it, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING, + ) + } + } + + @Test + fun `teknisk endring skal ha riktig seg ved behandlingsresultat fortsatt innvilget ved ingen endringer i utbetaling`() { + var steg = FØRSTE_STEG + + listOf( + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + StegType.VURDER_TILBAKEKREVING, + StegType.SEND_TIL_BESLUTTER, + StegType.BESLUTTE_VEDTAK, + StegType.FERDIGSTILLE_BEHANDLING, + StegType.BEHANDLING_AVSLUTTET, + ).forEach { + assertEquals(steg, it) + assertNotEquals(StegType.JOURNALFØR_VEDTAKSBREV, it) + steg = hentNesteSteg( + behandling = lagBehandling( + behandlingType = BehandlingType.TEKNISK_ENDRING, + årsak = BehandlingÅrsak.TEKNISK_ENDRING, + ), + utførendeStegType = it, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING, + ) + } + } + + @Test + fun `Tester rekkefølgen på behandling av omregn 18 år`() { + var steg = FØRSTE_STEG + + listOf( + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + StegType.JOURNALFØR_VEDTAKSBREV, + StegType.DISTRIBUER_VEDTAKSBREV, + StegType.FERDIGSTILLE_BEHANDLING, + StegType.BEHANDLING_AVSLUTTET, + ).forEach { + assertEquals(it, steg) + steg = hentNesteSteg( + behandling = lagBehandling( + årsak = BehandlingÅrsak.OMREGNING_18ÅR, + ), + utførendeStegType = it, + ) + } + } + + @Test + fun `Tester rekkefølgen til manuell behandling med årsak småbarnstillegg ved endringer i utbetaling`() { + var steg = FØRSTE_STEG + + listOf( + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + StegType.VURDER_TILBAKEKREVING, + StegType.SEND_TIL_BESLUTTER, + StegType.BESLUTTE_VEDTAK, + StegType.IVERKSETT_MOT_OPPDRAG, + StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI, + StegType.IVERKSETT_MOT_FAMILIE_TILBAKE, + StegType.JOURNALFØR_VEDTAKSBREV, + StegType.DISTRIBUER_VEDTAKSBREV, + StegType.FERDIGSTILLE_BEHANDLING, + StegType.BEHANDLING_AVSLUTTET, + ).forEach { + assertEquals(it, steg) + steg = hentNesteSteg( + behandling = lagBehandling( + årsak = BehandlingÅrsak.SMÅBARNSTILLEGG, + ), + utførendeStegType = it, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING, + ) + } + } + + @Test + fun `Tester rekkefølgen på behandling av ÅRLIG_KONTROLL, som er test av else gren ved endring i utbetaling`() { + var steg = FØRSTE_STEG + + listOf( + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + StegType.VURDER_TILBAKEKREVING, + StegType.SEND_TIL_BESLUTTER, + StegType.BESLUTTE_VEDTAK, + StegType.IVERKSETT_MOT_OPPDRAG, + StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI, + StegType.IVERKSETT_MOT_FAMILIE_TILBAKE, + StegType.JOURNALFØR_VEDTAKSBREV, + StegType.DISTRIBUER_VEDTAKSBREV, + StegType.FERDIGSTILLE_BEHANDLING, + StegType.BEHANDLING_AVSLUTTET, + ).forEach { + assertEquals(it, steg) + steg = hentNesteSteg( + behandling = lagBehandling( + årsak = BehandlingÅrsak.ÅRLIG_KONTROLL, + ), + utførendeStegType = it, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING, + ) + } + } + + @Test + fun `Tester rekkefølgen på behandling av satsendring ved endringer i utbetaling`() { + var steg = FØRSTE_STEG + + listOf( + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + StegType.IVERKSETT_MOT_OPPDRAG, + StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI, + StegType.FERDIGSTILLE_BEHANDLING, + StegType.BEHANDLING_AVSLUTTET, + ).forEach { + assertEquals(it, steg) + steg = hentNesteSteg( + behandling = lagBehandling( + årsak = BehandlingÅrsak.SATSENDRING, + ), + utførendeStegType = it, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING, + ) + } + } + + @Test + fun `Tester at man ikke får lov til å komme videre etter behandlingsresultat dersom det er ingen endringer i utbetaling`() { + var steg = FØRSTE_STEG + + listOf( + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + ).forEach { + assertEquals(it, steg) + if (it == StegType.BEHANDLINGSRESULTAT) { + assertThrows { + hentNesteSteg( + behandling = lagBehandling( + årsak = BehandlingÅrsak.SATSENDRING, + ), + utførendeStegType = it, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING, + ) + } + } else { + steg = hentNesteSteg( + behandling = lagBehandling( + årsak = BehandlingÅrsak.SATSENDRING, + ), + utførendeStegType = it, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING, + ) + } + } + } + + @Test + fun `Tester rekkefølgen på behandling av ÅRLIG_KONTROLL, som er test av else gren, med FORTSATT_INNVILGET ved ingen endringer i utbetaling`() { + var steg = FØRSTE_STEG + + listOf( + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + StegType.VURDER_TILBAKEKREVING, + StegType.SEND_TIL_BESLUTTER, + StegType.BESLUTTE_VEDTAK, + StegType.JOURNALFØR_VEDTAKSBREV, + StegType.DISTRIBUER_VEDTAKSBREV, + StegType.FERDIGSTILLE_BEHANDLING, + StegType.BEHANDLING_AVSLUTTET, + ).forEach { + assertEquals(it, steg) + steg = hentNesteSteg( + behandling = lagBehandling( + årsak = BehandlingÅrsak.ÅRLIG_KONTROLL, + ), + utførendeStegType = it, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING, + ) + } + } + + @Test + fun `Skal kaste feil dersom det er en søknad og det forsøkes å gå videre fra beslutt vedtak uten at det har vært sjekk om det finnes endringer i utbetaling`() { + assertThrows { + hentNesteSteg( + lagBehandling( + årsak = BehandlingÅrsak.SØKNAD, + ), + utførendeStegType = StegType.BESLUTTE_VEDTAK, + endringerIUtbetaling = EndringerIUtbetalingForBehandlingSteg.IKKE_RELEVANT, + ) + } + } + + @Test + fun testDisplayName() { + assertEquals("Send til beslutter", StegType.SEND_TIL_BESLUTTER.displayName()) + } + + @Test + fun testErKompatibelMed() { + assertTrue(StegType.REGISTRERE_SØKNAD.erGyldigIKombinasjonMedStatus(BehandlingStatus.UTREDES)) + assertFalse(StegType.REGISTRERE_SØKNAD.erGyldigIKombinasjonMedStatus(BehandlingStatus.IVERKSETTER_VEDTAK)) + assertFalse(StegType.BEHANDLING_AVSLUTTET.erGyldigIKombinasjonMedStatus(BehandlingStatus.FATTER_VEDTAK)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/BeslutteVedtakTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/BeslutteVedtakTest.kt new file mode 100644 index 000000000..2dd665cdd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/BeslutteVedtakTest.kt @@ -0,0 +1,267 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.runs +import io.mockk.verify +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.behandling.AutomatiskBeslutningService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValideringService +import no.nav.familie.ba.sak.kjerne.brev.DokumentService +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.simulering.SimuleringService +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollService +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.domene.Totrinnskontroll +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.sikkerhet.SaksbehandlerContext +import no.nav.familie.ba.sak.task.FerdigstillOppgaver +import no.nav.familie.ba.sak.task.JournalførVedtaksbrevTask +import no.nav.familie.ba.sak.task.OpprettOppgaveTask +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.prosessering.domene.Task +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class BeslutteVedtakTest { + + private lateinit var beslutteVedtak: BeslutteVedtak + private lateinit var vedtakService: VedtakService + private lateinit var behandlingService: BehandlingService + private lateinit var beregningService: BeregningService + private lateinit var taskRepository: TaskRepositoryWrapper + private lateinit var dokumentService: DokumentService + private lateinit var vilkårsvurderingService: VilkårsvurderingService + private lateinit var featureToggleService: FeatureToggleService + private lateinit var tilkjentYtelseValideringService: TilkjentYtelseValideringService + private lateinit var simuleringService: SimuleringService + private lateinit var automatiskBeslutningService: AutomatiskBeslutningService + + private val saksbehandlerContext = mockk() + + private val randomVilkårsvurdering = Vilkårsvurdering(behandling = lagBehandling()) + + @BeforeEach + fun setUp() { + val toTrinnKontrollService = mockk() + vedtakService = mockk() + taskRepository = mockk() + dokumentService = mockk() + behandlingService = mockk() + beregningService = mockk() + vilkårsvurderingService = mockk() + featureToggleService = mockk() + tilkjentYtelseValideringService = mockk() + simuleringService = mockk() + automatiskBeslutningService = mockk() + + val loggService = mockk() + + every { taskRepository.save(any()) } returns Task(OpprettOppgaveTask.TASK_STEP_TYPE, "") + every { + toTrinnKontrollService.besluttTotrinnskontroll( + any(), + any(), + any(), + any(), + any(), + ) + } returns Totrinnskontroll( + behandling = lagBehandling(), + saksbehandler = "Mock Saksbehandler", + saksbehandlerId = "Mock.Saksbehandler", + ) + every { loggService.opprettBeslutningOmVedtakLogg(any(), any(), any(), any()) } just Runs + every { vedtakService.oppdaterVedtaksdatoOgBrev(any()) } just runs + every { behandlingService.opprettOgInitierNyttVedtakForBehandling(any(), any(), any()) } just runs + every { vilkårsvurderingService.hentAktivForBehandling(any()) } returns randomVilkårsvurdering + every { vilkårsvurderingService.lagreNyOgDeaktiverGammel(any()) } returns randomVilkårsvurdering + every { saksbehandlerContext.hentSaksbehandlerSignaturTilBrev() } returns "saksbehandlerNavn" + + beslutteVedtak = BeslutteVedtak( + toTrinnKontrollService, + vedtakService, + behandlingService, + beregningService, + taskRepository, + loggService, + vilkårsvurderingService, + featureToggleService, + tilkjentYtelseValideringService, + saksbehandlerContext, + automatiskBeslutningService, + ) + } + + @Test + fun `Skal ferdigstille Godkjenne vedtak-oppgave ved Godkjent vedtak`() { + val behandling = lagBehandling() + behandling.status = BehandlingStatus.FATTER_VEDTAK + behandling.behandlingStegTilstand.add(BehandlingStegTilstand(0, behandling, StegType.BESLUTTE_VEDTAK)) + val restBeslutningPåVedtak = RestBeslutningPåVedtak(Beslutning.GODKJENT) + + every { vedtakService.hentAktivForBehandling(any()) } returns lagVedtak(behandling) + every { beregningService.hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(behandling) } returns EndringerIUtbetalingForBehandlingSteg.ENDRING_I_UTBETALING + mockkObject(FerdigstillOppgaver.Companion) + every { FerdigstillOppgaver.opprettTask(any(), any()) } returns Task(FerdigstillOppgaver.TASK_STEP_TYPE, "") + every { FerdigstillOppgaver.opprettTask(any(), any()) } returns Task(FerdigstillOppgaver.TASK_STEP_TYPE, "") + every { automatiskBeslutningService.behandlingSkalAutomatiskBesluttes(any()) } returns false + + val nesteSteg = beslutteVedtak.utførStegOgAngiNeste(behandling, restBeslutningPåVedtak) + + verify(exactly = 1) { FerdigstillOppgaver.opprettTask(behandling.id, Oppgavetype.GodkjenneVedtak) } + Assertions.assertEquals(StegType.IVERKSETT_MOT_OPPDRAG, nesteSteg) + } + + @Test + fun `Skal ferdigstille Godkjenne vedtak-oppgave og opprette Behandle Underkjent Vedtak-oppgave ved Underkjent vedtak`() { + val behandling = lagBehandling() + behandling.status = BehandlingStatus.FATTER_VEDTAK + behandling.behandlingStegTilstand.add(BehandlingStegTilstand(0, behandling, StegType.BESLUTTE_VEDTAK)) + val restBeslutningPåVedtak = RestBeslutningPåVedtak(Beslutning.UNDERKJENT) + + every { vedtakService.hentAktivForBehandling(any()) } returns lagVedtak(behandling) + mockkObject(FerdigstillOppgaver.Companion) + mockkObject(OpprettOppgaveTask.Companion) + every { FerdigstillOppgaver.opprettTask(any(), any()) } returns Task(FerdigstillOppgaver.TASK_STEP_TYPE, "") + every { + OpprettOppgaveTask.opprettTask( + any(), + any(), + any(), + any(), + any(), + ) + } returns Task(OpprettOppgaveTask.TASK_STEP_TYPE, "") + + every { automatiskBeslutningService.behandlingSkalAutomatiskBesluttes(any()) } returns false + + val nesteSteg = beslutteVedtak.utførStegOgAngiNeste(behandling, restBeslutningPåVedtak) + + verify(exactly = 1) { FerdigstillOppgaver.opprettTask(behandling.id, Oppgavetype.GodkjenneVedtak) } + verify(exactly = 1) { + OpprettOppgaveTask.opprettTask( + behandling.id, + Oppgavetype.BehandleUnderkjentVedtak, + any(), + any(), + any(), + ) + } + Assertions.assertEquals(StegType.SEND_TIL_BESLUTTER, nesteSteg) + } + + @Test + fun `Skal ikke iverksette hvis det ikke er forskjell i utbetaling mellom nåværende og forrige andeler`() { + val behandling = lagBehandling() + val vedtak = lagVedtak(behandling) + behandling.status = BehandlingStatus.FATTER_VEDTAK + behandling.behandlingStegTilstand.add(BehandlingStegTilstand(0, behandling, StegType.BESLUTTE_VEDTAK)) + val restBeslutningPåVedtak = RestBeslutningPåVedtak(Beslutning.GODKJENT) + + every { vedtakService.hentAktivForBehandling(any()) } returns vedtak + every { beregningService.hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(behandling) } returns EndringerIUtbetalingForBehandlingSteg.INGEN_ENDRING_I_UTBETALING + + mockkObject(JournalførVedtaksbrevTask.Companion) + every { + JournalførVedtaksbrevTask.opprettTaskJournalførVedtaksbrev( + any(), + any(), + any(), + ) + } returns Task(OpprettOppgaveTask.TASK_STEP_TYPE, "") + + every { automatiskBeslutningService.behandlingSkalAutomatiskBesluttes(any()) } returns false + + val nesteSteg = beslutteVedtak.utførStegOgAngiNeste(behandling, restBeslutningPåVedtak) + + verify(exactly = 1) { beregningService.hentEndringerIUtbetalingFraForrigeBehandlingSendtTilØkonomi(behandling) } + + verify(exactly = 1) { + JournalførVedtaksbrevTask.opprettTaskJournalførVedtaksbrev( + personIdent = behandling.fagsak.aktør.aktivFødselsnummer(), + behandlingId = behandling.id, + vedtakId = vedtak.id, + ) + } + Assertions.assertEquals(StegType.JOURNALFØR_VEDTAKSBREV, nesteSteg) + } + + @Test + fun `Skal initiere nytt vedtak når vedtak ikke er godkjent`() { + val behandling = lagBehandling() + behandling.status = BehandlingStatus.FATTER_VEDTAK + behandling.behandlingStegTilstand.add(BehandlingStegTilstand(0, behandling, StegType.BESLUTTE_VEDTAK)) + val restBeslutningPåVedtak = RestBeslutningPåVedtak(Beslutning.UNDERKJENT) + + every { vedtakService.hentAktivForBehandling(any()) } returns lagVedtak(behandling) + mockkObject(FerdigstillOppgaver.Companion) + mockkObject(OpprettOppgaveTask.Companion) + every { FerdigstillOppgaver.opprettTask(any(), any()) } returns Task(FerdigstillOppgaver.TASK_STEP_TYPE, "") + every { OpprettOppgaveTask.opprettTask(any(), any(), any()) } returns Task( + OpprettOppgaveTask.TASK_STEP_TYPE, + "", + ) + + every { automatiskBeslutningService.behandlingSkalAutomatiskBesluttes(any()) } returns false + + beslutteVedtak.utførStegOgAngiNeste(behandling, restBeslutningPåVedtak) + verify(exactly = 1) { behandlingService.opprettOgInitierNyttVedtakForBehandling(behandling, true, emptyList()) } + } + + @Test + fun `Skal kaste feil dersom toggle ikke er enabled og årsak er korreksjon vedtaksbrev`() { + every { featureToggleService.isEnabled(FeatureToggleConfig.KAN_MANUELT_KORRIGERE_MED_VEDTAKSBREV) } returns false + + val behandling = lagBehandling(årsak = BehandlingÅrsak.KORREKSJON_VEDTAKSBREV) + behandling.status = BehandlingStatus.FATTER_VEDTAK + behandling.behandlingStegTilstand.add(BehandlingStegTilstand(0, behandling, StegType.BESLUTTE_VEDTAK)) + val restBeslutningPåVedtak = RestBeslutningPåVedtak(Beslutning.GODKJENT) + + every { vedtakService.hentAktivForBehandling(any()) } returns lagVedtak(behandling) + mockkObject(FerdigstillOppgaver.Companion) + every { FerdigstillOppgaver.opprettTask(any(), any()) } returns Task( + type = FerdigstillOppgaver.TASK_STEP_TYPE, + payload = "", + ) + + assertThrows { beslutteVedtak.utførStegOgAngiNeste(behandling, restBeslutningPåVedtak) } + } + + @Test + fun `Skal kaste feil dersom saksbehandler uten tilgang til teknisk endring prøve å godkjenne en behandling med årsak=teknisk endring`() { + every { featureToggleService.isEnabled(FeatureToggleConfig.TEKNISK_ENDRING) } returns false + + val behandling = lagBehandling(årsak = BehandlingÅrsak.TEKNISK_ENDRING) + behandling.status = BehandlingStatus.FATTER_VEDTAK + behandling.behandlingStegTilstand.add(BehandlingStegTilstand(0, behandling, StegType.BESLUTTE_VEDTAK)) + val restBeslutningPåVedtak = RestBeslutningPåVedtak(Beslutning.GODKJENT) + + every { vedtakService.hentAktivForBehandling(any()) } returns lagVedtak(behandling) + mockkObject(FerdigstillOppgaver.Companion) + every { FerdigstillOppgaver.opprettTask(any(), any()) } returns Task( + type = FerdigstillOppgaver.TASK_STEP_TYPE, + payload = "", + ) + + assertThrows { beslutteVedtak.utførStegOgAngiNeste(behandling, restBeslutningPåVedtak) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerInstitusjonOgVergeStegTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerInstitusjonOgVergeStegTest.kt new file mode 100644 index 000000000..9c708562c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerInstitusjonOgVergeStegTest.kt @@ -0,0 +1,153 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.ekstern.restDomene.InstitusjonInfo +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerInstitusjonOgVerge +import no.nav.familie.ba.sak.ekstern.restDomene.VergeInfo +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.institusjon.Institusjon +import no.nav.familie.ba.sak.kjerne.institusjon.InstitusjonRepository +import no.nav.familie.ba.sak.kjerne.institusjon.InstitusjonService +import no.nav.familie.ba.sak.kjerne.logg.Logg +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.logg.LoggType +import no.nav.familie.ba.sak.kjerne.verge.Verge +import no.nav.familie.ba.sak.kjerne.verge.VergeRepository +import no.nav.familie.ba.sak.kjerne.verge.VergeService +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class RegistrerInstitusjonOgVergeStegTest { + + private val vergeRepositoryMock: VergeRepository = mockk() + private val fagsakRepositoryMock: FagsakRepository = mockk() + private val loggServiceMock: LoggService = mockk() + private val behandlingHentOgPersisterServiceMock: BehandlingHentOgPersisterService = mockk() + private val fagsakServiceMock: FagsakService = mockk(relaxed = true) + private val institusjonRepositoryMock: InstitusjonRepository = mockk() + + private lateinit var institusjonService: InstitusjonService + private lateinit var vergeService: VergeService + private lateinit var registrerInstitusjonOgVerge: RegistrerInstitusjonOgVerge + + @BeforeAll + fun setUp() { + institusjonService = + InstitusjonService( + fagsakRepository = fagsakRepositoryMock, + samhandlerKlient = mockk(relaxed = true), + institusjonRepository = institusjonRepositoryMock, + ) + vergeService = VergeService(vergeRepositoryMock) + registrerInstitusjonOgVerge = + RegistrerInstitusjonOgVerge( + institusjonService, + vergeService, + loggServiceMock, + behandlingHentOgPersisterServiceMock, + fagsakServiceMock, + ) + } + + @BeforeEach + fun init() { + clearMocks(loggServiceMock) + } + + @Test + fun `utførStegOgAngiNeste() skal lagre institusjon og verge`() { + val behandling = lagBehandling(fagsak = defaultFagsak().copy(type = FagsakType.INSTITUSJON)) + val fagsakSlot = slot() + val vergeSlot = slot() + every { fagsakRepositoryMock.finnFagsak(any()) } returns behandling.fagsak + every { fagsakServiceMock.lagre(capture(fagsakSlot)) } returns behandling.fagsak + every { vergeRepositoryMock.findByBehandling(any()) } returns null + every { vergeRepositoryMock.save(capture(vergeSlot)) } returns Verge(1L, "", behandling) + every { loggServiceMock.opprettRegistrerVergeLogg(any()) } just runs + every { loggServiceMock.opprettRegistrerInstitusjonLogg(any()) } just runs + every { institusjonRepositoryMock.findByOrgNummer(any()) } returns Institusjon( + orgNummer = "12345", + tssEksternId = "cool tsr", + ) + every { loggServiceMock.lagre(any()) } returns Logg( + behandlingId = behandling.id, + type = LoggType.VERGE_REGISTRERT, + tittel = "tittel", + rolle = BehandlerRolle.SYSTEM, + tekst = "", + ) + every { behandlingHentOgPersisterServiceMock.hent(any()) } returns behandling + val restRegistrerInstitusjonOgVerge = RestRegistrerInstitusjonOgVerge( + vergeInfo = VergeInfo( + "12345678910", + ), + institusjonInfo = InstitusjonInfo("12345", "cool tsr"), + ) + + registrerInstitusjonOgVerge.utførStegOgAngiNeste( + behandling, + restRegistrerInstitusjonOgVerge, + ) + + assertThat(fagsakSlot.captured.institusjon!!.orgNummer).isEqualTo(restRegistrerInstitusjonOgVerge.institusjonInfo!!.orgNummer) + assertThat(vergeSlot.captured.ident).isEqualTo(restRegistrerInstitusjonOgVerge.vergeInfo!!.ident) + verify(exactly = 1) { + loggServiceMock.opprettRegistrerVergeLogg(any()) + } + verify(exactly = 1) { + loggServiceMock.opprettRegistrerInstitusjonLogg(any()) + } + } + + @Test + fun `utførStegOgAngiNeste() skal returnere REGISTRERE_SØKNAD som neste steg`() { + val behandling = lagBehandling( + fagsak = defaultFagsak().copy( + type = FagsakType.INSTITUSJON, + institusjon = Institusjon(orgNummer = "12345", tssEksternId = "tss"), + ), + ) + every { fagsakRepositoryMock.finnFagsak(any()) } returns behandling.fagsak + every { fagsakRepositoryMock.save(any()) } returns behandling.fagsak + every { vergeRepositoryMock.findByBehandling(any()) } returns null + every { vergeRepositoryMock.save(any()) } returns Verge(1L, "", behandling) + every { loggServiceMock.opprettRegistrerVergeLogg(any()) } just runs + every { loggServiceMock.opprettRegistrerInstitusjonLogg(any()) } just runs + every { institusjonRepositoryMock.findByOrgNummer("12345") } returns behandling.fagsak.institusjon + every { loggServiceMock.lagre(any()) } returns Logg( + behandlingId = behandling.id, + type = LoggType.VERGE_REGISTRERT, + tittel = "tittel", + rolle = BehandlerRolle.SYSTEM, + tekst = "", + ) + every { behandlingHentOgPersisterServiceMock.hent(any()) } returns behandling + val restRegistrerInstitusjonOgVerge = RestRegistrerInstitusjonOgVerge( + vergeInfo = VergeInfo("12345678910"), + institusjonInfo = InstitusjonInfo("12345", "cool tsr"), + ) + + val nesteSteg = registrerInstitusjonOgVerge.utførStegOgAngiNeste( + behandling, + restRegistrerInstitusjonOgVerge, + ) + + assertThat(nesteSteg).isEqualTo(StegType.REGISTRERE_SØKNAD) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerPersongrunnlagEnhetTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerPersongrunnlagEnhetTest.kt new file mode 100644 index 000000000..c31567e8b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerPersongrunnlagEnhetTest.kt @@ -0,0 +1,110 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.KompetanseService +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløpService +import no.nav.familie.ba.sak.kjerne.eøs.valutakurs.ValutakursService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.EøsSkjemaerForNyBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.PersonopplysningGrunnlagForNyBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.VilkårsvurderingForNyBehandlingService +import org.junit.jupiter.api.Test + +class RegistrerPersongrunnlagEnhetTest { + + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService = mockk() + private val personopplysningGrunnlagForNyBehandlingService: PersonopplysningGrunnlagForNyBehandlingService = mockk() + private val vilkårsvurderingForNyBehandlingService: VilkårsvurderingForNyBehandlingService = mockk() + private val kompetanseService: KompetanseService = mockk() + private val valutakursService: ValutakursService = mockk() + private val utenlandskPeriodebeløpService: UtenlandskPeriodebeløpService = mockk() + + private val registrerPersongrunnlagSteg = RegistrerPersongrunnlag( + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + vilkårsvurderingForNyBehandlingService = vilkårsvurderingForNyBehandlingService, + personopplysningGrunnlagForNyBehandlingService = personopplysningGrunnlagForNyBehandlingService, + eøsSkjemaerForNyBehandlingService = EøsSkjemaerForNyBehandlingService( + kompetanseService = kompetanseService, + utenlandskPeriodebeløpService = utenlandskPeriodebeløpService, + valutakursService = valutakursService, + ), + ) + + @Test + fun `Kopierer kompetanser, valutakurser og utenlandsk periodebeløp til ny behandling`() { + val mor = lagPerson(type = PersonType.SØKER) + val barn1 = lagPerson(type = PersonType.BARN) + val barn2 = lagPerson(type = PersonType.BARN) + + val behandling1 = lagBehandling() + val behandling2 = lagBehandling() + + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling2) } returns behandling1 + + every { + personopplysningGrunnlagForNyBehandlingService.opprettKopiEllerNyttPersonopplysningGrunnlag( + behandling = behandling2, + forrigeBehandlingSomErVedtatt = behandling1, + søkerIdent = mor.aktør.aktivFødselsnummer(), + barnasIdenter = listOf(barn1.aktør.aktivFødselsnummer(), barn2.aktør.aktivFødselsnummer()), + ) + } just runs + + every { + vilkårsvurderingForNyBehandlingService.opprettVilkårsvurderingUtenomHovedflyt( + behandling = behandling2, + forrigeBehandlingSomErVedtatt = behandling1, + ) + } just runs + + every { + kompetanseService.kopierOgErstattKompetanser( + BehandlingId(behandling1.id), + BehandlingId(behandling2.id), + ) + } just runs + every { + valutakursService.kopierOgErstattValutakurser( + BehandlingId(behandling1.id), + BehandlingId(behandling2.id), + ) + } just runs + every { + utenlandskPeriodebeløpService.kopierOgErstattUtenlandskPeriodebeløp( + BehandlingId(behandling1.id), + BehandlingId(behandling2.id), + ) + } just runs + + registrerPersongrunnlagSteg.utførStegOgAngiNeste( + behandling = behandling2, + data = RegistrerPersongrunnlagDTO( + ident = mor.aktør.aktivFødselsnummer(), + barnasIdenter = listOf(barn1.aktør.aktivFødselsnummer(), barn2.aktør.aktivFødselsnummer()), + ), + ) + + verify(exactly = 1) { + kompetanseService.kopierOgErstattKompetanser( + BehandlingId(behandling1.id), + BehandlingId(behandling2.id), + ) + valutakursService.kopierOgErstattValutakurser( + BehandlingId(behandling1.id), + BehandlingId(behandling2.id), + ) + utenlandskPeriodebeløpService.kopierOgErstattUtenlandskPeriodebeløp( + BehandlingId(behandling1.id), + BehandlingId(behandling2.id), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/SendTilBeslutterTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/SendTilBeslutterTest.kt new file mode 100644 index 000000000..768790a30 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/SendTilBeslutterTest.kt @@ -0,0 +1,87 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class SendTilBeslutterTest { + + @Test + fun `Sjekk at validering er bakoverkompatibel med endring i stegrekkefølge`() { + val behandling = lagBehandling(førsteSteg = StegType.REGISTRERE_SØKNAD) + behandling.leggTilBehandlingStegTilstand(StegType.REGISTRERE_PERSONGRUNNLAG) + behandling.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING) + behandling.leggTilBehandlingStegTilstand(StegType.SEND_TIL_BESLUTTER) + + assertTrue(behandling.validerRekkefølgeOgUnikhetPåSteg()) + } + + @Test + fun `Sjekk validering med gyldig stegrekkefølge`() { + val behandling = lagBehandling() + behandling.leggTilBehandlingStegTilstand(StegType.REGISTRERE_SØKNAD) + behandling.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING) + behandling.leggTilBehandlingStegTilstand(StegType.SEND_TIL_BESLUTTER) + + assertTrue(behandling.validerRekkefølgeOgUnikhetPåSteg()) + } + + @Test + fun `Sjekk validering med ugyldig flere steg av samme type`() { + val behandling = lagBehandling() + behandling.leggTilBehandlingStegTilstand(StegType.REGISTRERE_SØKNAD) + behandling.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING) + behandling.behandlingStegTilstand.add( + BehandlingStegTilstand( + behandling = behandling, + behandlingSteg = StegType.VILKÅRSVURDERING, + ), + ) + + assertThrows { + behandling.validerRekkefølgeOgUnikhetPåSteg() + } + } + + @Test + fun `Sjekk validering med ugyldig stegrekkefølge`() { + val behandling = lagBehandling() + behandling.leggTilBehandlingStegTilstand(StegType.REGISTRERE_SØKNAD) + behandling.leggTilBehandlingStegTilstand(StegType.SEND_TIL_BESLUTTER) + behandling.behandlingStegTilstand.add( + BehandlingStegTilstand( + behandling = behandling, + behandlingSteg = StegType.VILKÅRSVURDERING, + ), + ) + + assertThrows { + behandling.validerRekkefølgeOgUnikhetPåSteg() + } + } + + @Test + fun `Sjekk validering som inneholder annen vurdering som ikke er vurdert`() { + val vilkårsvurdering = + lagVilkårsvurdering(randomAktør(), lagBehandling(), Resultat.IKKE_VURDERT) + + assertThrows { + vilkårsvurdering.validerAtAlleAnndreVurderingerErVurdert() + } + } + + @Test + fun `Sjekk validering som inneholder annen vurdering hvor alle er vurdert`() { + val vilkårsvurdering = + lagVilkårsvurdering(randomAktør(), lagBehandling(), Resultat.IKKE_OPPFYLT) + + vilkårsvurdering.validerAtAlleAnndreVurderingerErVurdert() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/StegServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/StegServiceTest.kt new file mode 100644 index 000000000..e89d49aa6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/StegServiceTest.kt @@ -0,0 +1,134 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.SatsendringService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import java.time.LocalDate + +internal class StegServiceTest { + + private val behandlingService: BehandlingService = mockk() + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService = mockk() + private val satsendringService: SatsendringService = mockk() + + private val stegService = StegService( + steg = listOf(mockRegistrerPersongrunnlag()), + fagsakService = mockk(), + behandlingService = behandlingService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + beregningService = mockk(), + søknadGrunnlagService = mockk(), + tilgangService = mockk(relaxed = true), + infotrygdFeedService = mockk(), + satsendringService = satsendringService, + personopplysningerService = mockk(), + automatiskBeslutningService = mockk(), + ) + + @BeforeEach + fun setup() { + val behandling = lagBehandling() + every { behandlingService.opprettBehandling(any()) } returns behandling + every { behandlingService.leggTilStegPåBehandlingOgSettTidligereStegSomUtført(any(), any()) } returns behandling + every { behandlingHentOgPersisterService.hent(any()) } returns behandling + } + + @Test + fun `skal IKKE feile validering av helmanuell migrering når fagsak har aktivt vedtak som er et opphør`() { + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = any()) } returns + lagBehandling() + + every { behandlingService.erLøpende(any()) } returns false + + assertDoesNotThrow { + stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + behandlingÅrsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + søkersIdent = randomFnr(), + barnasIdenter = listOf(randomFnr()), + nyMigreringsdato = LocalDate.now().minusMonths(6), + fagsakId = 1L, + ), + ) + } + } + + @Test + fun `skal feile validering av helmanuell migrering når fagsak har aktivt vedtak med løpende utbetalinger`() { + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId = any()) } returns + lagBehandling() + + every { behandlingService.erLøpende(any()) } returns true + + assertThrows { + stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + behandlingÅrsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + søkersIdent = randomFnr(), + barnasIdenter = listOf(randomFnr()), + nyMigreringsdato = LocalDate.now().minusMonths(6), + fagsakId = 1L, + ), + ) + } + } + + @Test + fun `Skal feile dersom vi har en gammel sats på forrige iverksatte behandling på endre migreringsdato behandling`() { + every { satsendringService.erFagsakOppdatertMedSisteSatser(any()) } returns false + + val nyBehandling = NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingType = BehandlingType.REVURDERING, + behandlingÅrsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + søkersIdent = randomFnr(), + barnasIdenter = listOf(randomFnr()), + nyMigreringsdato = LocalDate.now().minusMonths(6), + fagsakId = 1L, + ) + + assertThrows { stegService.håndterNyBehandling(nyBehandling) } + } + + @Test + fun `Skal feile dersom behandlingen er satt på vent`() { + val behandling = lagBehandling(status = BehandlingStatus.SATT_PÅ_VENT) + val grunnlag = RegistrerPersongrunnlagDTO("", emptyList()) + assertThatThrownBy { stegService.håndterPersongrunnlag(behandling, grunnlag) } + .hasMessageContaining("er på vent") + } + + private fun mockRegistrerPersongrunnlag() = object : RegistrerPersongrunnlag(mockk(), mockk(), mockk(), mockk()) { + override fun utførStegOgAngiNeste(behandling: Behandling, data: RegistrerPersongrunnlagDTO): StegType { + return StegType.VILKÅRSVURDERING + } + + override fun stegType(): StegType { + return StegType.REGISTRERE_PERSONGRUNNLAG + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/Vilk\303\245rsvurderingForNyBehandlingUtilsTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/Vilk\303\245rsvurderingForNyBehandlingUtilsTest.kt" new file mode 100644 index 000000000..6fe78ebeb --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/Vilk\303\245rsvurderingForNyBehandlingUtilsTest.kt" @@ -0,0 +1,121 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagBarnVilkårResultat +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagSøkerVilkårResultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Dødsfall +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.VilkårsvurderingForNyBehandlingUtils +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.finnAktørerMedUtvidetFraAndeler +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.YearMonth + +class VilkårsvurderingForNyBehandlingUtilsTest { + + @Test + fun `Skal kun ta med aktører som hadde andeler med utvidet barnetrygd`() { + val søker = lagPerson(type = PersonType.SØKER) + val barn = lagPerson(type = PersonType.BARN) + + val andeler = listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.now().minusYears(2).plusMonths(1), + tom = YearMonth.now(), + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + person = barn, + ), + lagAndelTilkjentYtelse( + fom = YearMonth.now().minusYears(1), + tom = YearMonth.now(), + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + person = søker, + ), + ) + + val aktørerMedUtvidet = finnAktørerMedUtvidetFraAndeler( + andeler = andeler, + ) + + Assertions.assertThat(aktørerMedUtvidet).containsExactly(søker.aktør) + } + + @Test + fun `Skal lage vilkårsvurdering med søkers vilkår satt med tom=dødsdato`() { + val søker = lagPerson(type = PersonType.SØKER).also { it.dødsfall = Dødsfall(person = it, dødsfallDato = LocalDate.now(), dødsfallAdresse = "Adresse 1", dødsfallPostnummer = "1234", dødsfallPoststed = "Oslo") } + val barn = lagPerson(type = PersonType.BARN) + val behandling = lagBehandling() + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + + val tomPåFørsteUtvidetVilkår = LocalDate.now().minusMonths(8) + + val søkerPersonResultat = PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = søker.aktør) + val søkerVilkårResultater = lagSøkerVilkårResultat(søkerPersonResultat = søkerPersonResultat, periodeFom = LocalDate.now().minusYears(2), periodeTom = null, behandlingId = behandling.id) + setOf( + VilkårResultat( + personResultat = søkerPersonResultat, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now().minusYears(2), + periodeTom = tomPåFørsteUtvidetVilkår, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ), + VilkårResultat( + personResultat = søkerPersonResultat, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + resultat = Resultat.OPPFYLT, + periodeFom = tomPåFørsteUtvidetVilkår.plusMonths(1), + periodeTom = null, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ), + ) + + val barnPersonResultat = PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = barn.aktør) + val barnVilkårResultater = lagBarnVilkårResultat( + barnPersonResultat = barnPersonResultat, + barnetsFødselsdato = barn.fødselsdato, + periodeFom = LocalDate.now().minusYears(2), + behandlingId = behandling.id, + ) + + søkerPersonResultat.setSortedVilkårResultater(søkerVilkårResultater) + barnPersonResultat.setSortedVilkårResultater(barnVilkårResultater) + + vilkårsvurdering.personResultater = setOf(søkerPersonResultat, barnPersonResultat) + + val nyVilkårsvurdering = VilkårsvurderingForNyBehandlingUtils(personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandling.id, personer = mutableSetOf(barn, søker))).hentVilkårsvurderingMedDødsdatoSomTomDato( + vilkårsvurdering = vilkårsvurdering, + ) + val søkersVilkårResultater = nyVilkårsvurdering.personResultater.find { it.erSøkersResultater() }?.vilkårResultater + val søkersUtvidetVilkår = søkersVilkårResultater?.filter { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + + Assertions.assertThat(søkersUtvidetVilkår).hasSize(2) + + val utvidetVilkårSortert = søkersUtvidetVilkår?.sortedBy { it.periodeTom } + + Assertions.assertThat(utvidetVilkårSortert?.first()?.periodeTom).isEqualTo(tomPåFørsteUtvidetVilkår) + Assertions.assertThat(utvidetVilkårSortert?.first()?.periodeFom).isEqualTo(LocalDate.now().minusYears(2)) + + Assertions.assertThat(utvidetVilkårSortert?.last()?.periodeTom).isEqualTo(søker.dødsfall?.dødsfallDato) + Assertions.assertThat(utvidetVilkårSortert?.last()?.periodeFom).isEqualTo(tomPåFørsteUtvidetVilkår.plusMonths(1)) + + Assertions.assertThat(søkerVilkårResultater.filter { it.vilkårType == Vilkår.LOVLIG_OPPHOLD }).hasSize(1) + Assertions.assertThat(søkerVilkårResultater.first { it.vilkårType == Vilkår.LOVLIG_OPPHOLD }.periodeTom).isEqualTo(søker.dødsfall?.dødsfallDato) + + Assertions.assertThat(søkerVilkårResultater.filter { it.vilkårType == Vilkår.BOSATT_I_RIKET }).hasSize(1) + Assertions.assertThat(søkerVilkårResultater.first { it.vilkårType == Vilkår.BOSATT_I_RIKET }.periodeTom).isEqualTo(søker.dødsfall?.dødsfallDato) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/Vilk\303\245rsvurderingStegTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/Vilk\303\245rsvurderingStegTest.kt" new file mode 100644 index 000000000..f43efd119 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/Vilk\303\245rsvurderingStegTest.kt" @@ -0,0 +1,166 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.tilPersonEnkel +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.BehandlingstemaService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement.TilpassKompetanserTilRegelverkService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.VilkårsvurderingForNyBehandlingService +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.util.VilkårsvurderingBuilder +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import java.time.LocalDate + +class VilkårsvurderingStegTest { + + private val vilkårService: VilkårService = mockk() + private val beregningService: BeregningService = mockk() + private val persongrunnlagService: PersongrunnlagService = mockk() + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService = mockk(relaxed = true) + private val behandlingstemaService: BehandlingstemaService = mockk(relaxed = true) + private val tilbakestillBehandlingService: TilbakestillBehandlingService = mockk() + private val tilpassKompetanserTilRegelverkService: TilpassKompetanserTilRegelverkService = mockk() + private val vilkårsvurderingForNyBehandlingService: VilkårsvurderingForNyBehandlingService = mockk() + + private val vilkårsvurderingSteg: VilkårsvurderingSteg = VilkårsvurderingSteg( + behandlingHentOgPersisterService, + behandlingstemaService, + vilkårService, + beregningService, + persongrunnlagService, + tilbakestillBehandlingService, + tilpassKompetanserTilRegelverkService, + vilkårsvurderingForNyBehandlingService, + ) + + val behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + ) + val søker = lagPerson(type = PersonType.SØKER, fødselsdato = LocalDate.of(1984, 1, 1)) + val barn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2019, 1, 1)) + + @BeforeEach + fun setup() { + every { persongrunnlagService.hentAktivThrows(behandling.id) } returns lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søker.aktør.aktivFødselsnummer(), + barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), + ) + every { tilbakestillBehandlingService.tilbakestillDataTilVilkårsvurderingssteg(behandling) } returns Unit + every { beregningService.genererTilkjentYtelseFraVilkårsvurdering(any(), any()) } returns lagInitiellTilkjentYtelse( + behandling, + ) + + every { tilpassKompetanserTilRegelverkService.tilpassKompetanserTilRegelverk(BehandlingId(behandling.id)) } just Runs + } + + @Test + fun `skal fortsette til neste steg når helmanuell migreringsbehandling har del bosted`() { + val vikårsvurdering = Vilkårsvurdering(behandling = behandling) + val søkerPersonResultat = lagPersonResultat( + vilkårsvurdering = vikårsvurdering, + person = søker, + resultat = Resultat.OPPFYLT, + periodeFom = søker.fødselsdato, + periodeTom = LocalDate.now(), + lagFullstendigVilkårResultat = true, + personType = PersonType.SØKER, + erDeltBosted = false, + ) + val barnPersonResultat = lagPersonResultat( + vilkårsvurdering = vikårsvurdering, + person = barn, + resultat = Resultat.OPPFYLT, + periodeFom = barn.fødselsdato, + periodeTom = LocalDate.now(), + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + erDeltBosted = true, + ) + vikårsvurdering.personResultater = setOf(søkerPersonResultat, barnPersonResultat) + every { vilkårService.hentVilkårsvurderingThrows(behandling.id) } returns vikårsvurdering + + assertDoesNotThrow { vilkårsvurderingSteg.utførStegOgAngiNeste(behandling, "") } + } + + @Test + fun `skal validere når regelverk er konsistent`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN, fødselsdato = LocalDate.now().minusMonths(2).withDayOfMonth(1)) + + val behandling = lagBehandling() + + val vilkårsvurderingBygger = VilkårsvurderingBuilder(behandling) + .forPerson(søker, MånedTidspunkt.nå()) + .medVilkår("N>", Vilkår.BOSATT_I_RIKET, Vilkår.LOVLIG_OPPHOLD) + .forPerson(barn1, barn1.fødselsdato.tilMånedTidspunkt()) + .medVilkår("+>", Vilkår.UNDER_18_ÅR, Vilkår.GIFT_PARTNERSKAP) + .medVilkår("N>", Vilkår.BOSATT_I_RIKET, Vilkår.LOVLIG_OPPHOLD, Vilkår.BOR_MED_SØKER) + .byggPerson() + + val vilkårsvurdering = vilkårsvurderingBygger.byggVilkårsvurdering() + val søkerOgBarnPåBehandling = listOf(søker.tilPersonEnkel(), barn1.tilPersonEnkel()) + + every { vilkårService.hentVilkårsvurdering(behandling.id) } returns vilkårsvurdering + every { persongrunnlagService.hentSøkerOgBarnPåBehandlingThrows(behandling.id) } returns søkerOgBarnPåBehandling + + assertDoesNotThrow { vilkårsvurderingSteg.preValiderSteg(behandling, null) } + } + + @Test + fun `validering skal feile når det er blanding av regelverk på vilkårene for barnet`() { + val søker = tilfeldigPerson(personType = PersonType.SØKER) + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + + val behandling = lagBehandling() + + val vilkårsvurderingBygger = VilkårsvurderingBuilder(behandling) + .forPerson(søker, MånedTidspunkt.nå()) + .medVilkår("EEEEEEEEEEEEE", Vilkår.BOSATT_I_RIKET, Vilkår.LOVLIG_OPPHOLD) + .forPerson(barn1, MånedTidspunkt.nå()) + .medVilkår("+++++++++++++", Vilkår.UNDER_18_ÅR, Vilkår.GIFT_PARTNERSKAP) + .medVilkår(" EEEENNNNEE", Vilkår.BOSATT_I_RIKET) + .medVilkår(" EEENNEEE", Vilkår.LOVLIG_OPPHOLD) + .medVilkår("NNNNNNNNNNEEE", Vilkår.BOR_MED_SØKER) + .byggPerson() + + val vilkårsvurdering = vilkårsvurderingBygger.byggVilkårsvurdering() + val søkerOgBarnPåBehandling = listOf(søker.tilPersonEnkel(), barn1.tilPersonEnkel()) + + every { vilkårService.hentVilkårsvurdering(behandling.id) } returns vilkårsvurdering + every { persongrunnlagService.hentSøkerOgBarnPåBehandlingThrows(behandling.id) } returns søkerOgBarnPåBehandling + + val exception = assertThrows { vilkårsvurderingSteg.preValiderSteg(behandling, null) } + assertEquals( + "Det er forskjellig regelverk for en eller flere perioder for søker eller barna", + exception.message, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/VurderTilbakekrevingStegTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/VurderTilbakekrevingStegTest.kt new file mode 100644 index 000000000..ea689b744 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/VurderTilbakekrevingStegTest.kt @@ -0,0 +1,75 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.simulering.SimuleringService +import no.nav.familie.ba.sak.kjerne.tilbakekreving.TilbakekrevingService +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow + +class VurderTilbakekrevingStegTest { + + private val tilbakekrevingService: TilbakekrevingService = mockk() + private val simuleringService: SimuleringService = mockk() + private val featureToggleService: FeatureToggleService = mockk() + + private val vurderTilbakekrevingSteg: VurderTilbakekrevingSteg = + VurderTilbakekrevingSteg(featureToggleService, tilbakekrevingService, simuleringService) + + private val behandling: Behandling = lagBehandling( + behandlingType = BehandlingType.REVURDERING, + årsak = BehandlingÅrsak.NYE_OPPLYSNINGER, + førsteSteg = StegType.VURDER_TILBAKEKREVING, + ) + private val restTilbakekreving: RestTilbakekreving = RestTilbakekreving( + valg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + varsel = "testverdi", + begrunnelse = "testverdi", + ) + + @BeforeEach + fun setup() { + every { tilbakekrevingService.søkerHarÅpenTilbakekreving(any()) } returns false + every { tilbakekrevingService.validerRestTilbakekreving(any(), any()) } returns Unit + every { tilbakekrevingService.lagreTilbakekreving(any(), any()) } returns null + every { featureToggleService.isEnabled(FeatureToggleConfig.ER_MANUEL_POSTERING_TOGGLE_PÅ) } returns true + } + + @Test + fun `skal utføre steg for vanlig behandling uten åpen tilbakekreving`() { + val stegType = assertDoesNotThrow { + vurderTilbakekrevingSteg.utførStegOgAngiNeste( + behandling, + restTilbakekreving, + ) + } + assertTrue { stegType == StegType.SEND_TIL_BESLUTTER } + verify(exactly = 1) { tilbakekrevingService.validerRestTilbakekreving(restTilbakekreving, behandling.id) } + verify(exactly = 1) { tilbakekrevingService.lagreTilbakekreving(restTilbakekreving, behandling.id) } + } + + @Test + fun `skal utføre steg for vanlig behandling med åpen tilbakekreving`() { + every { tilbakekrevingService.søkerHarÅpenTilbakekreving(any()) } returns true + val stegType = assertDoesNotThrow { + vurderTilbakekrevingSteg.utførStegOgAngiNeste( + behandling, + restTilbakekreving, + ) + } + assertTrue { stegType == StegType.SEND_TIL_BESLUTTER } + verify(exactly = 0) { tilbakekrevingService.validerRestTilbakekreving(restTilbakekreving, behandling.id) } + verify(exactly = 0) { tilbakekrevingService.lagreTilbakekreving(restTilbakekreving, behandling.id) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/PersonopplysningGrunnlagForNyBehandlingServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/PersonopplysningGrunnlagForNyBehandlingServiceTest.kt new file mode 100644 index 000000000..600863806 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/PersonopplysningGrunnlagForNyBehandlingServiceTest.kt @@ -0,0 +1,325 @@ +package no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Dødsfall +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagForNyBehandlingServiceTest.Companion.validerAtPersonerIGrunnlagErLike +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.arbeidsforhold.GrArbeidsforhold +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrBostedsadresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrMatrikkeladresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrUkjentBosted +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrVegadresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.domene.PersonIdent +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.opphold.GrOpphold +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.sivilstand.GrSivilstand +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.GrStatsborgerskap +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.kontrakter.felles.personopplysning.OPPHOLDSTILLATELSE +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class PersonopplysningGrunnlagForNyBehandlingServiceTest { + val personidentService = mockk() + val beregningService = mockk() + val persongrunnlagService = mockk() + val featureToggleService = mockk() + + private val personopplysningGrunnlagForNyBehandlingService = spyk( + PersonopplysningGrunnlagForNyBehandlingService( + personidentService = personidentService, + beregningService = beregningService, + persongrunnlagService = persongrunnlagService, + ), + ) + + @Test + fun `Skal sende med barna fra forrige behandling ved førstegangsbehandling nummer to`() { + val søker = lagPerson() + val barnFraForrigeBehandling = lagPerson(type = PersonType.BARN) + val barn = lagPerson(type = PersonType.BARN) + + val barnFnr = barn.aktør.aktivFødselsnummer() + val søkerFnr = søker.aktør.aktivFødselsnummer() + + val forrigeBehandling = lagBehandling(behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING) + val behandling = lagBehandling(behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING) + + every { personidentService.hentOgLagreAktør(søkerFnr, true) } returns søker.aktør + every { personidentService.hentOgLagreAktørIder(listOf(barnFnr), true) } returns listOf(barn.aktør) + + every { beregningService.finnBarnFraBehandlingMedTilkjentYtelse(forrigeBehandling.id) } returns + listOf(barnFraForrigeBehandling.aktør) + + every { persongrunnlagService.hentSøkersMålform(forrigeBehandling.id) } returns søker.målform + + every { + persongrunnlagService.hentOgLagreSøkerOgBarnINyttGrunnlag( + any(), + any(), + any(), + any(), + any(), + ) + } returns PersonopplysningGrunnlag(behandlingId = behandling.id) + + personopplysningGrunnlagForNyBehandlingService.opprettKopiEllerNyttPersonopplysningGrunnlag( + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandling, + søkerIdent = søkerFnr, + barnasIdenter = listOf(barnFnr), + ) + verify(exactly = 1) { + persongrunnlagService.hentOgLagreSøkerOgBarnINyttGrunnlag( + aktør = søker.aktør, + barnFraInneværendeBehandling = listOf(barn.aktør), + barnFraForrigeBehandling = listOf(barnFraForrigeBehandling.aktør), + behandling = behandling, + målform = søker.målform, + ) + } + } + + @Test + fun `hentOgLagrePersonopplysningGrunnlag - skal kopiere persongrunnlaget fra forrige behandling ved satsendring`() { + val forrigeBehandling = lagBehandling() + val nyBehandling = lagBehandling(årsak = BehandlingÅrsak.SATSENDRING) + val søker = PersonIdent(randomFnr()) + val barn = PersonIdent(randomFnr()) + val søkerPerson = lagPerson(personIdent = søker, id = 1) + val barnPerson = lagPerson(personIdent = barn, id = 2) + + val periode = DatoIntervallEntitet(LocalDate.now(), LocalDate.now().plusMonths(4)) + + val kopiertPersonopplysningGrunnlag = slot() + + val grVegadresse = + GrVegadresse(1, "2", null, "123", "Testgate", "23", null, "0682").medPeriodeOgPerson(periode, søkerPerson) + val grUkjentBosted = GrUkjentBosted("Oslo").medPeriodeOgPerson(periode, søkerPerson) + val grMatrikkeladresse = GrMatrikkeladresse(1, "2", null, "0682", "23").medPeriodeOgPerson(periode, søkerPerson) + + val statsborgerskap = GrStatsborgerskap( + id = 1, + gyldigPeriode = periode, + landkode = "N", + medlemskap = Medlemskap.EØS, + person = søkerPerson, + ) + val opphold = + GrOpphold(id = 1, gyldigPeriode = periode, type = OPPHOLDSTILLATELSE.PERMANENT, person = søkerPerson) + val arbeidsforhold = + GrArbeidsforhold( + id = 1, + periode = periode, + arbeidsgiverId = "1", + arbeidsgiverType = "AS", + person = søkerPerson, + ) + val sivilstand = + GrSivilstand(id = 1, fom = LocalDate.now(), type = SIVILSTAND.REGISTRERT_PARTNER, person = søkerPerson) + val dødsfall = Dødsfall( + id = 1, + person = søkerPerson, + dødsfallDato = LocalDate.now(), + dødsfallAdresse = "Adresse", + dødsfallPostnummer = "1234", + dødsfallPoststed = "Oslo", + ) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(forrigeBehandling.id, søkerPerson, barnPerson).also { + it.personer.map { person -> + if (person.aktør.aktivFødselsnummer() == søker.ident) { + person.bostedsadresser.addAll( + listOf( + grVegadresse, + grUkjentBosted, + grMatrikkeladresse, + ), + ) + person.statsborgerskap.addAll(listOf(statsborgerskap)) + person.opphold.addAll(listOf(opphold)) + person.arbeidsforhold.addAll(listOf(arbeidsforhold)) + person.sivilstander.addAll(listOf(sivilstand)) + person.dødsfall = dødsfall + } + } + } + every { featureToggleService.isEnabled(any(), any()) } returns true + every { persongrunnlagService.hentAktivThrows(forrigeBehandling.id) } returns personopplysningGrunnlag + every { persongrunnlagService.lagreOgDeaktiverGammel(capture(kopiertPersonopplysningGrunnlag)) } returns mockk() + every { personidentService.hentOgLagreAktør(any(), any()) } returns tilAktør(søker.ident) + every { beregningService.finnBarnFraBehandlingMedTilkjentYtelse(forrigeBehandling.id) } returns listOf( + tilAktør( + barn.ident, + ), + ) + personopplysningGrunnlagForNyBehandlingService.opprettKopiEllerNyttPersonopplysningGrunnlag( + nyBehandling, + forrigeBehandling, + søker.ident, + listOf(barn.ident), + ) + + assertThat(kopiertPersonopplysningGrunnlag.captured.behandlingId).isEqualTo(nyBehandling.id) + assertThat(kopiertPersonopplysningGrunnlag.captured.personer.size).isEqualTo(2) + + validerAtPersonerIGrunnlagErLike(personopplysningGrunnlag, kopiertPersonopplysningGrunnlag.captured, true) + } + + @Test + fun `hentOgLagrePersonopplysningGrunnlag - skal kopiere søker og barn med tilkjent ytelse fra persongrunnlaget fra forrige behandling ved satsendring`() { + val forrigeBehandling = lagBehandling() + val nyBehandling = lagBehandling(årsak = BehandlingÅrsak.SATSENDRING) + val søker = PersonIdent(randomFnr()) + val barn1 = PersonIdent(randomFnr()) + val barn2 = PersonIdent(randomFnr()) + val søkerPerson = lagPerson(personIdent = søker, id = 1) + val barnPerson1 = lagPerson(personIdent = barn1, id = 2) + val barnPerson2 = lagPerson(personIdent = barn2, id = 3) + + val periode = DatoIntervallEntitet(LocalDate.now(), LocalDate.now().plusMonths(4)) + + val kopiertPersonopplysningGrunnlag = slot() + + val grVegadresse = + GrVegadresse(1, "2", null, "123", "Testgate", "23", null, "0682").medPeriodeOgPerson(periode, søkerPerson) + val grUkjentBosted = GrUkjentBosted("Oslo").medPeriodeOgPerson(periode, søkerPerson) + val grMatrikkeladresse = GrMatrikkeladresse(1, "2", null, "0682", "23").medPeriodeOgPerson(periode, søkerPerson) + + val statsborgerskap = GrStatsborgerskap( + id = 1, + gyldigPeriode = periode, + landkode = "N", + medlemskap = Medlemskap.EØS, + person = søkerPerson, + ) + val opphold = + GrOpphold(id = 1, gyldigPeriode = periode, type = OPPHOLDSTILLATELSE.PERMANENT, person = søkerPerson) + val arbeidsforhold = + GrArbeidsforhold( + id = 1, + periode = periode, + arbeidsgiverId = "1", + arbeidsgiverType = "AS", + person = søkerPerson, + ) + val sivilstand = + GrSivilstand(id = 1, fom = LocalDate.now(), type = SIVILSTAND.REGISTRERT_PARTNER, person = søkerPerson) + val dødsfall = Dødsfall( + id = 1, + person = søkerPerson, + dødsfallDato = LocalDate.now(), + dødsfallAdresse = "Adresse", + dødsfallPostnummer = "1234", + dødsfallPoststed = "Oslo", + ) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(forrigeBehandling.id, søkerPerson, barnPerson1, barnPerson2).also { + it.personer.map { person -> + if (person.aktør.aktivFødselsnummer() == søker.ident) { + person.bostedsadresser.addAll( + listOf( + grVegadresse, + grUkjentBosted, + grMatrikkeladresse, + ), + ) + person.statsborgerskap.addAll(listOf(statsborgerskap)) + person.opphold.addAll(listOf(opphold)) + person.arbeidsforhold.addAll(listOf(arbeidsforhold)) + person.sivilstander.addAll(listOf(sivilstand)) + person.dødsfall = dødsfall + } + } + } + + val forventetPersonopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(forrigeBehandling.id, søkerPerson, barnPerson1).also { + it.personer.map { person -> + if (person.aktør.aktivFødselsnummer() == søker.ident) { + person.bostedsadresser.addAll( + listOf( + grVegadresse, + grUkjentBosted, + grMatrikkeladresse, + ), + ) + person.statsborgerskap.addAll(listOf(statsborgerskap)) + person.opphold.addAll(listOf(opphold)) + person.arbeidsforhold.addAll(listOf(arbeidsforhold)) + person.sivilstander.addAll(listOf(sivilstand)) + person.dødsfall = dødsfall + } + } + } + every { featureToggleService.isEnabled(any(), any()) } returns true + every { persongrunnlagService.hentAktivThrows(forrigeBehandling.id) } returns personopplysningGrunnlag + every { persongrunnlagService.lagreOgDeaktiverGammel(capture(kopiertPersonopplysningGrunnlag)) } returns mockk() + every { personidentService.hentOgLagreAktør(any(), any()) } returns tilAktør(søker.ident) + every { beregningService.finnBarnFraBehandlingMedTilkjentYtelse(forrigeBehandling.id) } returns listOf( + tilAktør( + barn1.ident, + ), + ) + personopplysningGrunnlagForNyBehandlingService.opprettKopiEllerNyttPersonopplysningGrunnlag( + nyBehandling, + forrigeBehandling, + søker.ident, + listOf(barn1.ident), + ) + + assertThat(kopiertPersonopplysningGrunnlag.captured.behandlingId).isEqualTo(nyBehandling.id) + assertThat(kopiertPersonopplysningGrunnlag.captured.personer.size).isEqualTo(2) + + validerAtPersonerIGrunnlagErLike( + forventetPersonopplysningGrunnlag, + kopiertPersonopplysningGrunnlag.captured, + true, + ) + } + + @Test + fun `hentOgLagrePersonopplysningGrunnlag - skal kaste feil dersom behandling er satsendring og forrige behandling er null`() { + val nyBehandling = lagBehandling(årsak = BehandlingÅrsak.SATSENDRING) + val søker = PersonIdent(randomFnr()) + val barn = PersonIdent(randomFnr()) + every { featureToggleService.isEnabled(any(), any()) } returns true + assertThatThrownBy { + personopplysningGrunnlagForNyBehandlingService.opprettKopiEllerNyttPersonopplysningGrunnlag( + behandling = nyBehandling, + forrigeBehandlingSomErVedtatt = null, + søker.ident, + listOf(barn.ident), + ) + }.isInstanceOf(Feil::class.java) + } + + private fun GrBostedsadresse.medPeriodeOgPerson(periode: DatoIntervallEntitet, person: Person): GrBostedsadresse = + this.also { + it.periode = periode + it.person = person + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/Vilk\303\245rsvurderingForNyBehandlingServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/Vilk\303\245rsvurderingForNyBehandlingServiceTest.kt" new file mode 100644 index 000000000..2d45948f5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/grunnlagForNyBehandling/Vilk\303\245rsvurderingForNyBehandlingServiceTest.kt" @@ -0,0 +1,305 @@ +package no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling + +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagVilkårsvurderingMedOverstyrendeResultater +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.BehandlingstemaService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelService +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingMetrics +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.LocalDate +import kotlin.reflect.full.declaredMemberProperties + +class VilkårsvurderingForNyBehandlingServiceTest { + + private val vilkårsvurderingService = mockk() + private val behandlingService = mockk() + private val persongrunnlagService = mockk() + private val behandlingstemaService = mockk() + private val endretUtbetalingAndelService = mockk() + private val vilkårsvurderingMetrics = mockk() + private val andelTilkjentYtelseRepository = mockk() + + private lateinit var vilkårsvurderingForNyBehandlingService: VilkårsvurderingForNyBehandlingService + + @BeforeEach + fun setUp() { + vilkårsvurderingForNyBehandlingService = VilkårsvurderingForNyBehandlingService( + vilkårsvurderingService = vilkårsvurderingService, + behandlingService = behandlingService, + persongrunnlagService = persongrunnlagService, + behandlingstemaService = behandlingstemaService, + endretUtbetalingAndelService = endretUtbetalingAndelService, + vilkårsvurderingMetrics = vilkårsvurderingMetrics, + andelerTilkjentYtelseRepository = andelTilkjentYtelseRepository, + ) + } + + @Test + fun `skal kopiere vilkårsvurdering fra forrige behandling ved satsendring - alle vilkår for alle personer er oppfylt`() { + val søker = lagPerson(type = PersonType.SØKER) + val barn = lagPerson(type = PersonType.BARN) + val fagsak = Fagsak(aktør = søker.aktør) + val behandling = lagBehandling(fagsak = fagsak, årsak = BehandlingÅrsak.SATSENDRING) + val forrigeBehandling = lagBehandling(fagsak = fagsak, årsak = BehandlingÅrsak.SØKNAD) + + val forrigeVilkårsvurdering = lagVilkårsvurderingMedOverstyrendeResultater( + søker = søker, + barna = listOf(barn), + behandling = forrigeBehandling, + overstyrendeVilkårResultater = emptyMap(), + id = 1, + ) + val forventetNåværendeVilkårsvurdering = lagVilkårsvurderingMedOverstyrendeResultater( + søker = søker, + barna = listOf(barn), + behandling = behandling, + overstyrendeVilkårResultater = emptyMap(), + ) + + every { vilkårsvurderingService.hentAktivForBehandling(behandlingId = forrigeBehandling.id) } returns forrigeVilkårsvurdering + + val slot = slot() + + every { vilkårsvurderingService.lagreNyOgDeaktiverGammel(capture(slot)) } returnsArgument 0 + + every { persongrunnlagService.hentAktivThrows(behandling.id) } returns lagTestPersonopplysningGrunnlag( + forrigeBehandling.id, + søker, + barn, + ) + + every { + endretUtbetalingAndelService.kopierEndretUtbetalingAndelFraForrigeBehandling( + behandling, + forrigeBehandling, + ) + } just runs + + vilkårsvurderingForNyBehandlingService.opprettVilkårsvurderingUtenomHovedflyt( + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandling, + ) + + verify(exactly = 1) { vilkårsvurderingService.lagreNyOgDeaktiverGammel(any()) } + + validerKopiertVilkårsvurdering(slot.captured, forrigeVilkårsvurdering, forventetNåværendeVilkårsvurdering) + } + + @Test + fun `skal kopiere vilkårsvurdering fra forrige behandling ved satsendring - alle VilkårResultater er ikke oppfylt`() { + val søker = lagPerson(type = PersonType.SØKER) + val barn = lagPerson(type = PersonType.BARN) + val fagsak = Fagsak(aktør = søker.aktør) + val behandling = lagBehandling(fagsak = fagsak, årsak = BehandlingÅrsak.SATSENDRING) + val forrigeBehandling = lagBehandling(fagsak = fagsak, årsak = BehandlingÅrsak.SØKNAD) + + val forrigeVilkårsvurdering = lagVilkårsvurderingMedOverstyrendeResultater( + søker = søker, + barna = listOf(barn), + behandling = forrigeBehandling, + overstyrendeVilkårResultater = mapOf( + Pair( + barn.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeTom = LocalDate.now().minusMonths(4), + behandlingId = forrigeBehandling.id, + ), + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = null, + periodeTom = null, + behandlingId = forrigeBehandling.id, + ), + ), + ), + ), + id = 1, + ) + val forventetNåværendeVilkårsvurdering = lagVilkårsvurderingMedOverstyrendeResultater( + søker = søker, + barna = listOf(barn), + behandling = behandling, + overstyrendeVilkårResultater = mapOf( + Pair( + barn.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeTom = LocalDate.now().minusMonths(4), + behandlingId = behandling.id, + ), + ), + ), + ), + ) + + every { vilkårsvurderingService.hentAktivForBehandling(behandlingId = forrigeBehandling.id) } returns forrigeVilkårsvurdering + + val slot = slot() + + every { vilkårsvurderingService.lagreNyOgDeaktiverGammel(capture(slot)) } returnsArgument 0 + + every { persongrunnlagService.hentAktivThrows(behandling.id) } returns lagTestPersonopplysningGrunnlag( + forrigeBehandling.id, + søker, + barn, + ) + + every { + endretUtbetalingAndelService.kopierEndretUtbetalingAndelFraForrigeBehandling( + behandling, + forrigeBehandling, + ) + } just runs + + vilkårsvurderingForNyBehandlingService.opprettVilkårsvurderingUtenomHovedflyt( + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandling, + ) + + verify(exactly = 1) { vilkårsvurderingService.lagreNyOgDeaktiverGammel(any()) } + + validerKopiertVilkårsvurdering(slot.captured, forrigeVilkårsvurdering, forventetNåværendeVilkårsvurdering) + } + + @Test + fun `skal kopiere vilkårsvurdering fra forrige behandling ved satsendring - ett barn har ikke oppfylt alle vilkår og har ingen tilkjent ytelse fra forrige behandling`() { + val søker = lagPerson(type = PersonType.SØKER) + val barn1 = lagPerson(type = PersonType.BARN) + val barn2 = lagPerson(type = PersonType.BARN) + val barna = listOf(barn1, barn2) + val fagsak = Fagsak(aktør = søker.aktør) + val behandling = lagBehandling(fagsak = fagsak, årsak = BehandlingÅrsak.SATSENDRING) + val forrigeBehandling = lagBehandling(fagsak = fagsak, årsak = BehandlingÅrsak.SØKNAD) + + val forrigeVilkårsvurdering = lagVilkårsvurderingMedOverstyrendeResultater( + søker = søker, + barna = barna, + behandling = forrigeBehandling, + overstyrendeVilkårResultater = mapOf( + Pair( + barn1.aktør.aktørId, + listOf( + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = null, + periodeTom = null, + behandlingId = forrigeBehandling.id, + ), + ), + ), + ), + id = 1, + ) + val forventetNåværendeVilkårsvurdering = lagVilkårsvurderingMedOverstyrendeResultater( + søker = søker, + barna = listOf(barn2), + behandling = behandling, + overstyrendeVilkårResultater = emptyMap(), + ) + + every { vilkårsvurderingService.hentAktivForBehandling(behandlingId = forrigeBehandling.id) } returns forrigeVilkårsvurdering + + val slot = slot() + + every { vilkårsvurderingService.lagreNyOgDeaktiverGammel(capture(slot)) } returnsArgument 0 + + every { persongrunnlagService.hentAktivThrows(behandling.id) } returns lagTestPersonopplysningGrunnlag( + forrigeBehandling.id, + søker, + barn2, + ) + + every { + endretUtbetalingAndelService.kopierEndretUtbetalingAndelFraForrigeBehandling( + behandling, + forrigeBehandling, + ) + } just runs + + vilkårsvurderingForNyBehandlingService.opprettVilkårsvurderingUtenomHovedflyt( + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandling, + ) + + verify(exactly = 1) { vilkårsvurderingService.lagreNyOgDeaktiverGammel(any()) } + + validerKopiertVilkårsvurdering(slot.captured, forrigeVilkårsvurdering, forventetNåværendeVilkårsvurdering) + } + + companion object { + fun validerKopiertVilkårsvurdering( + kopiertVilkårsvurdering: Vilkårsvurdering, + forrigeVilkårsvurdering: Vilkårsvurdering, + forventetNåværendeVilkårsvurdering: Vilkårsvurdering, + ) { + assertThat(kopiertVilkårsvurdering.id).isNotEqualTo(forrigeVilkårsvurdering.id) + assertThat(kopiertVilkårsvurdering.behandling.id).isNotEqualTo(forrigeVilkårsvurdering.behandling.id) + + kopiertVilkårsvurdering.personResultater.forEach { + assertThat(it.aktør).isEqualTo(forventetNåværendeVilkårsvurdering.personResultater.first { personResultat -> personResultat.aktør.aktivFødselsnummer() == it.aktør.aktivFødselsnummer() }.aktør) + } + + assertThat(kopiertVilkårsvurdering.personResultater.flatMap { it.vilkårResultater }.size).isEqualTo( + forventetNåværendeVilkårsvurdering.personResultater.flatMap { it.vilkårResultater }.size, + ) + + val kopierteOgForrigeVilkårResultaterGruppertEtterVilkårType = + kopiertVilkårsvurdering.personResultater.fold(mutableListOf, List>>()) { acc, personResultat -> + val vilkårResultaterForrigeBehandlingForPerson = + forventetNåværendeVilkårsvurdering.personResultater.filter { it.aktør.aktivFødselsnummer() == personResultat.aktør.aktivFødselsnummer() } + .flatMap { it.vilkårResultater } + acc.addAll( + personResultat.vilkårResultater.groupBy { it.vilkårType } + .map { (vilkårType, vilkårResultater) -> + Pair( + vilkårResultater, + vilkårResultaterForrigeBehandlingForPerson.filter { forrigeVilkårResultat -> forrigeVilkårResultat.vilkårType == vilkårType }, + ) + }, + ) + acc + } + + val baseEntitetFelter = + BaseEntitet::class.declaredMemberProperties.map { it.name }.toTypedArray() + kopierteOgForrigeVilkårResultaterGruppertEtterVilkårType.forEach { + assertThat(it.first).usingRecursiveFieldByFieldElementComparatorIgnoringFields( + "id", + "personResultat", + *baseEntitetFelter, + ) + .isEqualTo(it.second) + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/TidslinjeFraTidspunktTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/TidslinjeFraTidspunktTest.kt new file mode 100644 index 000000000..92aadce3b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/TidslinjeFraTidspunktTest.kt @@ -0,0 +1,65 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje + +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.Innhold +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.innholdForTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.tidslinjeFraTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.util.apr +import no.nav.familie.ba.sak.kjerne.tidslinje.util.des +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jul +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jun +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mai +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mar +import no.nav.familie.ba.sak.kjerne.tidslinje.util.nov +import no.nav.familie.ba.sak.kjerne.tidslinje.util.okt +import no.nav.familie.ba.sak.kjerne.tidslinje.util.sep +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class TidslinjeFraTidspunktTest { + val tidslinje = tidslinje { + listOf( + Periode(mar(2018), mar(2018), null), + Periode(mai(2018), mai(2018), "A"), + Periode(jun(2018), sep(2018), "B"), + Periode(des(2018), feb(2019), "C"), + Periode(apr(2019), jul(2019), null), + Periode(sep(2019), jan(2020), "D"), + Periode(feb(2020), okt(2020), null), + Periode(nov(2020), feb(2021), "e"), + Periode(apr(2021), apr(2021), null), + ) + } + + @Test + fun `skal gjenskape underliggende tidslinje dersom innholdet returneres uendret `() { + val resultat = tidslinje.tidsrom().tidslinjeFraTidspunkt { tidspunkt -> + tidslinje.innholdForTidspunkt(tidspunkt) + } + + assertEquals(tidslinje, resultat) + } + + @Test + fun `skal skape sammenhengende tidslinje i samme tidsrom hvis alt innhold er identisk`() { + val resultat = tidslinje.tidsrom().tidslinjeFraTidspunkt { tidspunkt -> + Innhold("A") + } + + val forventet = tidslinje { listOf(Periode(mar(2018), apr(2021), "A")) } + + assertEquals(forventet, resultat) + } + + @Test + fun `skal skape tom tidslinje dersom alt innhold mangler`() { + val resultat = tidslinje.tidsrom().tidslinjeFraTidspunkt { tidspunkt -> + Innhold.utenInnhold() + } + + assertEquals(TomTidslinje(), resultat) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/TidslinjeKombinasjonTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/TidslinjeKombinasjonTest.kt new file mode 100644 index 000000000..b22186477 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/TidslinjeKombinasjonTest.kt @@ -0,0 +1,78 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje + +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.slåSammenLike +import no.nav.familie.ba.sak.kjerne.tidslinje.util.StringTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.tilCharTidslinje +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class TidslinjeKombinasjonTest { + + val kombinator = { venstre: Char?, høyre: Char? -> + (venstre?.toString() ?: "").trim() + (høyre?.toString() ?: "").trim() + } + + @Test + fun testEndeligeLikeLangTidslinjer() { + assertTidslinjer( + linje1 = "abcdef", + linje2 = "fedcba", + "af", + "be", + "cd", + "dc", + "eb", + "fa", + ) + } + + @Test + fun testEndeligeTidslinjerMedForskjelligLengde() { + assertTidslinjer( + linje1 = " ab", + linje2 = "fedcba", + "f", + "e", + "ad", + "bc", + "b", + "a", + ) + } + + @Test + fun testUendeligeTidslinjerFremover() { + assertTidslinjer( + linje1 = "abc>", + linje2 = "abacd>", + "aa", + "bb", + "ca", + "cc", + "cd", + ">", + ) + } + + @Test + fun testUendeligeTidslinjerBeggeVeier() { + assertTidslinjer( + linje1 = "", + ) + } + + private fun assertTidslinjer(linje1: String, linje2: String, vararg forventet: String) { + val fom = jan(2020) + val char1 = linje1.tilCharTidslinje(fom) + val char2 = linje2.tilCharTidslinje(fom) + + val k1 = char1.kombinerMed(char2, kombinator) + val f = StringTidslinje(fom, forventet.toList()).slåSammenLike() + + Assertions.assertEquals(f, k1) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/TidslinjeTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/TidslinjeTest.kt new file mode 100644 index 000000000..afedfa1f2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/TidslinjeTest.kt @@ -0,0 +1,133 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje + +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje.Companion.TidslinjeFeilException +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.util.apr +import no.nav.familie.ba.sak.kjerne.tidslinje.util.des +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mai +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mar +import no.nav.familie.ba.sak.kjerne.tidslinje.util.nov +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow + +internal class TidslinjeTest { + + @Test + fun `skal validere at perioder ikke kan ha fra-og-med etter til-og-med`() { + assertThatExceptionOfType(TidslinjeFeilException::class.java).isThrownBy { + TestTidslinje( + Periode(15.jan(2020), 14.jan(2020), 'A'), + ).perioder() + } + } + + @Test + fun `skal validere at perioder som overlapper med kun én dag ikke er lov`() { + assertThatExceptionOfType(TidslinjeFeilException::class.java).isThrownBy { + TestTidslinje( + Periode(1.jan(2020), 31.mar(2020), 'A'), + Periode(31.mar(2020), 31.mai(2020), 'B'), + ).perioder() + } + } + + @Test + fun `skal validere at periode som ligger inni en annen ikke er lov`() { + assertThatExceptionOfType(TidslinjeFeilException::class.java).isThrownBy { + TestTidslinje( + Periode(1.jan(2020), 31.mai(2020), 'A'), + Periode(1.mar(2020), 30.apr(2020), 'B'), + ).perioder() + } + } + + @Test + fun `skal validere at uendelig i begge ender av tidslinjen er lov`() { + assertDoesNotThrow { + TestTidslinje( + Periode(1.jan(2020).somUendeligLengeSiden(), 1.jan(2020).somUendeligLengeTil(), 'A'), + ).perioder() + } + + assertDoesNotThrow { + TestTidslinje( + Periode(1.jan(2020).somUendeligLengeSiden(), 29.feb(2020), 'A'), + Periode(1.mar(2020), 30.apr(2020).somUendeligLengeTil(), 'B'), + ).perioder() + } + } + + @Test + fun `skal validere at uendelige perioder inni en tidslinje ikke er lov`() { + assertThatExceptionOfType(TidslinjeFeilException::class.java).isThrownBy { + TestTidslinje( + Periode(1.jan(2020), 31.jan(2020), 'A'), + Periode(1.feb(2020).somUendeligLengeSiden(), 29.feb(2020), 'A'), + Periode(1.mar(2020), 30.apr(2020), 'B'), + ).perioder() + } + + assertThatExceptionOfType(TidslinjeFeilException::class.java).isThrownBy { + TestTidslinje( + Periode(1.jan(2020), 31.jan(2020), 'A'), + Periode(1.feb(2020), 29.feb(2020).somUendeligLengeTil(), 'A'), + Periode(1.mar(2020), 30.apr(2020), 'B'), + ).perioder() + } + } + + @Test + fun `skal presentere tidslinjefeil på et forstålig format`() { + assertThatExceptionOfType(TidslinjeFeilException::class.java).isThrownBy { + TestTidslinje( + Periode(1.jan(2020), 31.jan(2020), 'A'), + Periode(1.feb(2020), 29.feb(2020).somUendeligLengeTil(), 'A'), + Periode(1.mar(2020), 30.apr(2020), 'B'), + ).perioder() + }.withMessage( + "[TidslinjeFeil(type=UENDELIG_FREMTID_FØR_SISTE_PERIODE, periode=2020-02-01 - 2020-02-29-->: A, tidslinje=2020-01-01 - 2020-01-31: A | 2020-02-01 - 2020-02-29-->: A | 2020-03-01 - 2020-04-30: B)]", + ) + } + + @Test + fun `Skal kunne kombinere tidslinje med uendelighet der det uendelige tidspunktet er satt tilbake i tid`() { + val tidslinjeMedUendelighet = listOf( + Periode(jan(2020), des(2020), 'A'), + Periode(jan(2021), feb(1999).somUendeligLengeTil(), 'B'), + ).tilTidslinje() + + val kombinertMedSegSelv = tidslinjeMedUendelighet.kombinerMed(tidslinjeMedUendelighet) { v, h -> + "$v$h" + } + + Assertions.assertThat(kombinertMedSegSelv).isEqualTo( + listOf( + Periode(jan(2020), des(2020), "AA"), + Periode(jan(2021), feb(1999).somUendeligLengeTil(), "BB"), + ).tilTidslinje(), + ) + } + + @Test + fun `tidsrom skal lage liste med alle tidspunkter opp til uendelig tidspunk`() { + val tidslinjeMedUendelighet = listOf( + Periode(nov(2020), des(2020), 'A'), + Periode(jan(2021), jan(1999).somUendeligLengeTil(), 'B'), + ).tilTidslinje() + + Assertions.assertThat(tidsrom(tidslinjeMedUendelighet).toList()).isEqualTo( + listOf(nov(2020), des(2020), jan(1999).somUendeligLengeTil()), + ) + } +} + +internal class TestTidslinje(vararg val perioder: Periode) : Tidslinje() { + override fun lagPerioder() = perioder.toList() +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/TomTidslinjeTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/TomTidslinjeTest.kt new file mode 100644 index 000000000..d4bd544ab --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/TomTidslinjeTest.kt @@ -0,0 +1,58 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje + +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Dag +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.somBoolskTidslinje +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +internal class TomTidslinjeTest { + + @Test + fun `test at fra-og-med og til-og-med er uendelige`() { + val fom = TomTidslinje().fraOgMed() + val tom = TomTidslinje().tilOgMed() + + assertNull(fom) + assertNull(tom) + } + + @Test + fun `test at tidsrommet mellom fra-og-med og til-og-med er tomt`() { + assertTrue(TomTidslinje().tidsrom().toList().isEmpty()) + } + + @Test + fun `test at listen av perioder er tom`() { + assertTrue(TomTidslinje().perioder().isEmpty()) + } + + @Test + fun `test kombinering av to tomme tidslinjer`() { + val resultat = + TomTidslinje().kombinerMed(TomTidslinje()) { v, h -> v ?: h } + + assertEquals(TomTidslinje(), resultat) + } + + @Test + fun `test kombinering fra tom tidslinje til tidslinje med innhold`() { + val boolskTidslinje = "tftftftftftft".somBoolskTidslinje(jan(2020)) + val resultat = TomTidslinje().kombinerMed(boolskTidslinje) { v, h -> v ?: h } + + assertEquals(boolskTidslinje, resultat) + } + + @Test + fun `test kombinering fra tidslinje med innhold til tom tidslinje`() { + val boolskTidslinje = "tftft ftft".somBoolskTidslinje(jan(2020)) + val resultat = boolskTidslinje.kombinerMed(TomTidslinje()) { v, h -> v ?: h } + + assertEquals(boolskTidslinje, resultat) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/eksperimentelt/TidslinjeKonkatenering.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/eksperimentelt/TidslinjeKonkatenering.kt new file mode 100644 index 000000000..7b2f46ab4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/eksperimentelt/TidslinjeKonkatenering.kt @@ -0,0 +1,16 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt + +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.kombinerUtenNullOgIkkeTom +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet + +/** + * Funksjon for å kjede sammen tidslinjer + * Vil krasje med exception fra Iterable.single() hvis to eller flere tidslinjer overlapper + */ +fun konkatenerTidslinjer(vararg tidslinje: Tidslinje): Tidslinje { + return tidslinje.toList().kombinerUtenNullOgIkkeTom { it.single() } +} + +operator fun Tidslinje.plus(tidslinje: Tidslinje): Tidslinje = + konkatenerTidslinjer(this, tidslinje) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/eksperimentelt/TidslinjeTransformasjon.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/eksperimentelt/TidslinjeTransformasjon.kt new file mode 100644 index 000000000..3f9bc5315 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/eksperimentelt/TidslinjeTransformasjon.kt @@ -0,0 +1,36 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.fraOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tilOgMed + +/** + * Extension-metode for å kombinere et "vindu" med størrelse size i hver periode + * Fungerer helt likt som (og bruker) Iterable.windowed + * mapper-nmetoden må passe på ikke å skape overlapp mellom perioder (gir exception) + * Forsøk på å flytte perioder utenfor tidslinjen, vil gi exception + */ +fun Tidslinje.windowed( + size: Int, + step: Int = 1, + partialWindows: Boolean = false, + mapper: (List>) -> Periode, +): Tidslinje { + val tidslinje = this + + return object : Tidslinje() { + val fraOgMed = tidslinje.fraOgMed() + val tilOgMed = tidslinje.tilOgMed() + + override fun lagPerioder(): Collection> = + tidslinje.perioder().windowed(size, step, partialWindows) { perioder -> + val periode = mapper(perioder) + if (periode.fraOgMed < fraOgMed!! || periode.tilOgMed > tilOgMed!!) { + throw IllegalArgumentException("Forsøk på å flytte perioden utenfor grensene for tidslinjen") + } + periode + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/eksperimentelt/TidspunktClosedRangeUtvidelser.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/eksperimentelt/TidspunktClosedRangeUtvidelser.kt new file mode 100644 index 000000000..0ecd3ec7f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/eksperimentelt/TidspunktClosedRangeUtvidelser.kt @@ -0,0 +1,10 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt + +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo + +fun Tidspunkt.ogSenere() = this.somUendeligLengeTil().rangeTo(this.somUendeligLengeTil()) +fun Tidspunkt.ogTidligere() = this.somUendeligLengeSiden().rangeTo(this.somUendeligLengeSiden()) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidsrom/TidspunktClosedRangeTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidsrom/TidspunktClosedRangeTest.kt new file mode 100644 index 000000000..51d2b387c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidsrom/TidspunktClosedRangeTest.kt @@ -0,0 +1,222 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom + +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erEndelig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.neste +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somEndelig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeTil +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.YearMonth + +class TidspunktClosedRangeTest { + val A = YearMonth.of(2020, 1).tilTidspunkt() + val B = A.neste() + val C = B.neste() + val D = C.neste() + val E = D.neste() + val F = E.neste() + + val tomListe = emptyList>() + + @Test + fun `A til A`() { + val tidsrom = A..A + assertEquals(listOf(A), tidsrom.toList()) + } + + @Test + fun `A til B`() { + val tidsrom = A..B + assertEquals(listOf(A, B), tidsrom.toList()) + } + + @Test + fun `A til C`() { + val tidsrom = A..C + assertEquals(listOf(A, B, C), tidsrom.toList()) + } + + @Test + fun `B til A`() { + val tidsrom = B..A + assertEquals(tomListe, tidsrom.toList()) + } + + @Test + fun `←A til A`() { + val tidspunkter = (A.somUendeligLengeSiden()..A).toList() + assertEquals(listOf(A), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter.first().erUendeligLengeSiden()) + } + + @Test + fun `←A til B`() { + val tidspunkter = (A.somUendeligLengeSiden()..B).toList() + assertEquals(listOf(A, B), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter.first().erUendeligLengeSiden()) + assertTrue(tidspunkter.last().erEndelig()) + } + + @Test + fun `←A til ←A`() { + val tidspunkter = (A.somUendeligLengeSiden()..A.somUendeligLengeSiden()).toList() + assertEquals(listOf(A), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter[0].erUendeligLengeSiden()) + } + + @Test + fun `←A til ←C`() { + val tidspunkter = (A.somUendeligLengeSiden()..C.somUendeligLengeSiden()).toList() + assertEquals(listOf(A, B, C), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter[0].erUendeligLengeSiden()) + assertTrue(tidspunkter[1].erEndelig()) + assertTrue(tidspunkter[2].erEndelig()) + } + + @Test + fun `←B til A`() { + val tidspunkter = (B.somUendeligLengeSiden()..A).toList() + assertEquals(listOf(A), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter[0].erUendeligLengeSiden()) + } + + @Test + fun `←B til ←A`() { + val tidspunkter = (B.somUendeligLengeSiden()..A.somUendeligLengeSiden()).toList() + assertEquals(listOf(A), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter[0].erUendeligLengeSiden()) + } + + @Test + fun `A til A→`() { + val tidspunkter = (A..A.somUendeligLengeTil()).toList() + assertEquals(listOf(A), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter[0].erUendeligLengeTil()) + } + + @Test + fun `A→ til A→`() { + val tidspunkter = (A.somUendeligLengeTil()..A.somUendeligLengeTil()).toList() + assertEquals(listOf(A), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter[0].erUendeligLengeTil()) + } + + @Test + fun `A→ til C→`() { + val tidspunkter = (A.somUendeligLengeTil()..C.somUendeligLengeTil()).toList() + assertEquals(listOf(A, B, C), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter[0].erEndelig()) + assertTrue(tidspunkter[1].erEndelig()) + assertTrue(tidspunkter[2].erUendeligLengeTil()) + } + + @Test + fun `B til A→`() { + val tidspunkter = (B..A.somUendeligLengeTil()).toList() + assertEquals(listOf(B), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter[0].erUendeligLengeTil()) + } + + @Test + fun `B→ til A→`() { + val tidspunkter = (B.somUendeligLengeTil()..A.somUendeligLengeTil()).toList() + assertEquals(listOf(B), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter[0].erUendeligLengeTil()) + } + + @Test + fun `←A til A→`() { + val tidspunkter = (A.somUendeligLengeSiden()..A.somUendeligLengeTil()).toList() + assertEquals(listOf(A, B), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter.first().erUendeligLengeSiden()) + assertTrue(tidspunkter.last().erUendeligLengeTil()) + } + + @Test + fun `←A til B→`() { + val tidspunkter = (A.somUendeligLengeSiden()..B.somUendeligLengeTil()).toList() + assertEquals(listOf(A, B), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter.first().erUendeligLengeSiden()) + assertTrue(tidspunkter.last().erUendeligLengeTil()) + } + + @Test + fun `←A til C→`() { + val tidspunkter = (A.somUendeligLengeSiden()..C.somUendeligLengeTil()).toList() + assertEquals(listOf(A, B, C), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter.first().erUendeligLengeSiden()) + assertTrue(tidspunkter.last().erUendeligLengeTil()) + } + + @Test + fun `←B til A→`() { + val tidspunkter = (B.somUendeligLengeSiden()..A.somUendeligLengeTil()).toList() + assertEquals(listOf(A, B), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter.first().erUendeligLengeSiden()) + assertTrue(tidspunkter.last().erUendeligLengeTil()) + } + + @Test + fun `←E til A→`() { + val tidspunkter = (E.somUendeligLengeSiden()..A.somUendeligLengeTil()).toList() + assertEquals(listOf(A, B, C, D, E), tidspunkter.map { it.somEndelig() }) + assertTrue(tidspunkter.first().erUendeligLengeSiden()) + assertTrue(tidspunkter.last().erUendeligLengeTil()) + } + + @Test + fun `A→ til ←A`() { + val tidsrom = A.somUendeligLengeTil()..A.somUendeligLengeSiden() + assertEquals(listOf(A), tidsrom.toList()) + } + + @Test + fun `A→ til ←B`() { + val tidsrom = A.somUendeligLengeTil()..B.somUendeligLengeSiden() + assertEquals(listOf(A, B), tidsrom.toList()) + } + + @Test + fun `A→ til ←E`() { + val tidsrom = A.somUendeligLengeTil()..E.somUendeligLengeSiden() + assertEquals(listOf(A, B, C, D, E), tidsrom.toList()) + } + + @Test + fun `B→ til ←A`() { + val tidsrom = B.somUendeligLengeTil()..A.somUendeligLengeSiden() + assertEquals(tomListe, tidsrom.toList()) + } + + @Test + fun testTidsromMedMåneder() { + val fom = MånedTidspunkt.uendeligLengeSiden(YearMonth.of(2020, 1)) + val tom = MånedTidspunkt.uendeligLengeTil(YearMonth.of(2020, 10)) + val tidsrom = fom..tom + + assertEquals(10, tidsrom.count()) + assertEquals(fom, tidsrom.first()) + assertEquals(tom, tidsrom.last()) + } + + @Test + fun testTidsromMedDager() { + val fom = DagTidspunkt.uendeligLengeSiden(LocalDate.of(2020, 1, 1)) + val tom = DagTidspunkt.uendeligLengeTil(LocalDate.of(2020, 10, 31)) + val tidsrom = fom..tom + + assertEquals(305, tidsrom.count()) + assertEquals(fom, tidsrom.first()) + assertEquals(tom, tidsrom.last()) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidsrom/TidsromTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidsrom/TidsromTest.kt new file mode 100644 index 000000000..c78eef858 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/tidsrom/TidsromTest.kt @@ -0,0 +1,87 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom + +import no.nav.familie.ba.sak.kjerne.tidslinje.minsteAv +import no.nav.familie.ba.sak.kjerne.tidslinje.størsteAv +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.util.apr +import no.nav.familie.ba.sak.kjerne.tidslinje.util.des +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jun +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mai +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mar +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Test + +class TidsromTest { + @Test + fun testStørsteAv() { + assertEquals( + feb(2020), + størsteAv(jan(2020), feb(2020)), + ) + assertEquals( + feb(2020).somUendeligLengeTil(), + størsteAv(jan(2020).somUendeligLengeTil(), jan(2020)), + ) + assertEquals( + jun(2020).somUendeligLengeTil(), + størsteAv(jan(2020).somUendeligLengeTil(), mai(2020)), + ) + assertEquals( + jan(2020).somUendeligLengeTil(), + størsteAv(jan(2020).somUendeligLengeTil(), des(2019)), + ) + assertEquals( + feb(2020).somUendeligLengeTil(), + størsteAv(jan(2020).somUendeligLengeTil(), feb(2020).somUendeligLengeTil()), + ) + } + + @Test + fun testMinsteAv() { + assertEquals( + des(2019).somUendeligLengeSiden(), + minsteAv(jan(2020).somUendeligLengeSiden(), jan(2020)), + ) + assertEquals( + apr(2019).somUendeligLengeSiden(), + minsteAv(jan(2020).somUendeligLengeSiden(), mai(2019)), + ) + assertEquals( + jan(2020).somUendeligLengeSiden(), + minsteAv(jan(2020).somUendeligLengeSiden(), feb(2020)), + ) + assertEquals( + feb(2020).somUendeligLengeSiden(), + størsteAv(feb(2020).somUendeligLengeSiden(), mar(2020).somUendeligLengeSiden()), + ) + } + + @Test + fun `Equals på ulike tidsenheter skal være forskjellig`() { + assertEquals(feb(2020), feb(2020)) + assertEquals(1.feb(2020), 1.feb(2020)) + assertEquals(31.jan(2020), 31.jan(2020)) + + assertNotEquals(1.jan(2020), jan(2020)) + assertNotEquals(31.jan(2020), jan(2020)) + } + + @Test + fun `Equals på samme uendelig skal være lik`() { + assertEquals(feb(2020).somUendeligLengeTil(), mar(2020).somUendeligLengeTil()) + assertEquals(feb(2020).somUendeligLengeSiden(), mar(2020).somUendeligLengeSiden()) + + assertEquals(1.feb(2020).somUendeligLengeTil(), 2.feb(2020).somUendeligLengeTil()) + assertEquals(1.feb(2020).somUendeligLengeSiden(), 2.feb(2020).somUendeligLengeSiden()) + + assertEquals(1.feb(2020).somUendeligLengeSiden(), mar(2020).somUendeligLengeSiden()) + assertEquals(feb(2020).somUendeligLengeTil(), 1.jan(2020).somUendeligLengeTil()) + + assertNotEquals(feb(2020).somUendeligLengeSiden(), feb(2020).somUendeligLengeTil()) + assertNotEquals(5.feb(2020).somUendeligLengeTil(), 5.feb(2020).somUendeligLengeSiden()) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/Beskj\303\246reTidslinjeTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/Beskj\303\246reTidslinjeTest.kt" new file mode 100644 index 000000000..535097b99 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/Beskj\303\246reTidslinjeTest.kt" @@ -0,0 +1,148 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.fraOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Uendelighet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.tilOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.util.des +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mai +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mar +import no.nav.familie.ba.sak.kjerne.tidslinje.util.tilCharTidslinje +import org.junit.Assert.assertEquals +import org.junit.jupiter.api.Test + +internal class BeskjæreTidslinjeTest { + + @Test + fun `skal beskjære endelig tidslinje på begge sider`() { + val hovedlinje = "aaaaaa".tilCharTidslinje(des(2001)) + val beskjæring = "bbb".tilCharTidslinje(feb(2002)) + + val faktisk = hovedlinje.beskjærEtter(beskjæring) + val forventet = "aaa".tilCharTidslinje(feb(2002)) + + assertEquals(forventet, faktisk) + } + + @Test + fun `skal beholde tidslinje som allerede er innenfor beskjæring`() { + val hovedlinje = "aaa".tilCharTidslinje(feb(2002)) + val beskjæring = "bbbbbbbbb".tilCharTidslinje(des(2001)) + + val faktisk = hovedlinje.beskjærEtter(beskjæring) + val forventet = "aaa".tilCharTidslinje(feb(2002)) + + assertEquals(forventet, faktisk) + } + + @Test + fun `skal beholde tidslinje som er innenfor en uendelig beskjæring`() { + val hovedlinje = "aaa".tilCharTidslinje(feb(2002)) + val beskjæring = "".tilCharTidslinje(mar(2002)) + + val faktisk = hovedlinje.beskjærEtter(beskjæring) + val forventet = "aaa".tilCharTidslinje(feb(2002)) + + assertEquals(forventet, faktisk) + } + + @Test + fun `beskjæring utenfor tidslinjen skal gi tom tidslinje`() { + val hovedlinje = "aaaaaa".tilCharTidslinje(des(2001)) + val beskjæring = "bbb".tilCharTidslinje(feb(2009)) + + val faktisk = hovedlinje.beskjærEtter(beskjæring) + + assertEquals(TomTidslinje(), faktisk) + } + + @Test + fun `skal beskjære uendelig tidslinje begge veier mot endelig tidsline`() { + val hovedlinje = "".tilCharTidslinje(des(2002)) + val beskjæring = "bbb".tilCharTidslinje(feb(2002)) + + val faktisk = hovedlinje.beskjærEtter(beskjæring) + val forventet = "aaa".tilCharTidslinje(feb(2002)) + + assertEquals(forventet, faktisk) + assertEquals(forventet.somEndelig(), faktisk.somEndelig()) + } + + @Test + fun `skal beskjære tidslinje som går fra uendelig lenge siden til et endelig tidspunkt i fremtiden`() { + val hovedlinje = "()) + val forventet = TomTidslinje() + + assertEquals(forventet, faktisk) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/MapTidslinjeTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/MapTidslinjeTest.kt new file mode 100644 index 000000000..a9639c476 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/MapTidslinjeTest.kt @@ -0,0 +1,83 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.fraOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.Innhold +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.innholdForTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo +import no.nav.familie.ba.sak.kjerne.tidslinje.tilOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.util.apr +import no.nav.familie.ba.sak.kjerne.tidslinje.util.aug +import no.nav.familie.ba.sak.kjerne.tidslinje.util.des +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jul +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jun +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mai +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mar +import no.nav.familie.ba.sak.kjerne.tidslinje.util.nov +import no.nav.familie.ba.sak.kjerne.tidslinje.util.okt +import no.nav.familie.ba.sak.kjerne.tidslinje.util.sep +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class MapTidslinjeTest { + + val tidslinje = tidslinje { + listOf( + Periode(aug(2019), nov(2019), null), + Periode(jan(2020), mar(2020), "A"), + Periode(apr(2020), jun(2020), null), + Periode(jul(2020), aug(2020), "B"), + Periode(mar(2021), okt(2021), "C"), + Periode(jan(2022), mai(2022), null), + ) + } + + @Test + fun `skal mappe innhold og ivareta null`() { + val faktisk = tidslinje.map { it?.lowercase() } + + val forventet = tidslinje { + listOf( + Periode(aug(2019), nov(2019), null), + Periode(jan(2020), mar(2020), "a"), + Periode(apr(2020), jun(2020), null), + Periode(jul(2020), aug(2020), "b"), + Periode(mar(2021), okt(2021), "c"), + Periode(jan(2022), mai(2022), null), + ) + } + + assertEquals(forventet, faktisk) + assertEquals(aug(2019), faktisk.fraOgMed()) + assertEquals(mai(2022), faktisk.tilOgMed()) + } + + @Test + fun `skal mappe innhold og fjerne null`() { + val faktisk = tidslinje.mapIkkeNull { it.lowercase() } + + val forventet = tidslinje { + listOf( + Periode(jan(2020), mar(2020), "a"), + Periode(jul(2020), aug(2020), "b"), + Periode(mar(2021), okt(2021), "c"), + ) + } + + assertEquals(forventet, faktisk) + assertEquals(jan(2020), faktisk.fraOgMed()) + assertEquals(okt(2021), faktisk.tilOgMed()) + + ( + (aug(2019)..des(2019)) + .plus(apr(2020)..jun(2020)) + .plus(sep(2020)..feb(2021)) + .plus(nov(2021)..mai(2022)) + ).forEach { + assertEquals(Innhold.utenInnhold(), faktisk.innholdForTidspunkt(it)) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/M\303\245nedFraM\303\245nedsskifteTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/M\303\245nedFraM\303\245nedsskifteTest.kt" new file mode 100644 index 000000000..6e841da6d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/M\303\245nedFraM\303\245nedsskifteTest.kt" @@ -0,0 +1,75 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.TomTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.util.des +import no.nav.familie.ba.sak.kjerne.tidslinje.util.nov +import no.nav.familie.ba.sak.kjerne.tidslinje.util.tilCharTidslinje +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class MånedFraMånedsskifteTest { + + @Test + fun `skal gi tom tidslinje hvis alle dager er inni én måned`() { + val daglinje = "aaaaaa".tilCharTidslinje(7.des(2021)) + val månedtidsline = daglinje + .tilMånedFraMånedsskifteIkkeNull { verdiSisteDagForrigeMåned, verdiFørsteDagDenneMåned -> 'b' } + + assertEquals(TomTidslinje(), månedtidsline) + } + + @Test + fun `skal gi én måned ved ett månedsskifte`() { + val daglinje = "abcdefg".tilCharTidslinje(28.nov(2021)) + val månedtidsline = daglinje + .tilMånedFraMånedsskifteIkkeNull { verdiSisteDagForrigeMåned, verdiFørsteDagDenneMåned -> + verdiFørsteDagDenneMåned + } + + assertEquals("d".tilCharTidslinje(des(2021)), månedtidsline) + } + + @Test + fun `skal gi to måneder ved to månedsskifter`() { + val daglinje = "abcdefghijklmnopqrstuvwxyzæøå0123456789".tilCharTidslinje(28.nov(2021)) + val månedtidsline = daglinje + .tilMånedFraMånedsskifteIkkeNull { verdiSisteDagForrigeMåned, verdiFørsteDagDenneMåned -> + verdiSisteDagForrigeMåned + } + + assertEquals("c4".tilCharTidslinje(des(2021)), månedtidsline) + } + + @Test + fun `skal gi tom tidslinje hvis månedsskiftet mangler verdi på begge sider`() { + val daglinje = "abcdefghijklmnopqrstuvwxyzæøå0123456789".tilCharTidslinje(28.nov(2021)) + .mapIkkeNull { + when (it) { + 'c', 'd', '4', '5' -> null // 30/11, 1/12, 31/12 og 1/1 mangler verdi + else -> it + } + } + + val månedtidsline = daglinje + .tilMånedFraMånedsskifteIkkeNull { verdiSisteDagForrigeMåned, verdiFørsteDagDenneMåned -> 'A' } + + assertEquals(TomTidslinje(), månedtidsline) + } + + @Test + fun `skal gi tom tidslinje hvis månedsskiftet mangler verdi på begge én av sidene`() { + val daglinje = "abcdefghijklmnopqrstuvwxyzæøå0123456789".tilCharTidslinje(28.nov(2021)) + .mapIkkeNull { + when (it) { + 'c', '5' -> null // 30/11 og 1/1 mangler verdi + else -> it + } + } + + val månedtidsline = daglinje + .tilMånedFraMånedsskifteIkkeNull { verdiSisteDagForrigeMåned, verdiFørsteDagDenneMåned -> 'A' } + + assertEquals(TomTidslinje(), månedtidsline) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/ZipTidslinjeTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/ZipTidslinjeTest.kt new file mode 100644 index 000000000..69a7a9f65 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/transformasjon/ZipTidslinjeTest.kt @@ -0,0 +1,23 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon + +import no.nav.familie.ba.sak.kjerne.tidslinje.util.tilCharTidslinje +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.time.YearMonth + +class ZipTidslinjeTest { + @Test + fun testZipMedNesteTidslinje() { + val aTilF = ('a'..'f').toList().joinToString("") + val bokstavTidslinje = aTilF.tilCharTidslinje(YearMonth.now()) + val bokstavParTidslinje = bokstavTidslinje.zipMedNeste(ZipPadding.FØR) + + assertThat(aTilF).isEqualTo("abcdef") + + assertThat(bokstavParTidslinje.perioder().map { it.innhold }).isEqualTo( + listOf(Pair(null, 'a'), Pair('a', 'b'), Pair('b', 'c'), Pair('c', 'd'), Pair('d', 'e'), Pair('e', 'f')), + ) + + println(listOf(Pair(null, 'a'), Pair('a', 'b'), Pair('b', 'c'), Pair('c', 'd'), Pair('d', 'e'), Pair('e', 'f'))) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/BooleanCharTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/BooleanCharTidslinje.kt new file mode 100644 index 000000000..51d0bf198 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/BooleanCharTidslinje.kt @@ -0,0 +1,16 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.util + +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.transformasjon.map + +fun String.somBoolskTidslinje(t: Tidspunkt) = this.tilCharTidslinje(t).somBoolsk() + +fun Tidslinje.somBoolsk() = this.map { + when (it?.lowercaseChar()) { + 't' -> true + 'f' -> false + else -> null + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/CharTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/CharTidslinje.kt new file mode 100644 index 000000000..bd37a6c65 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/CharTidslinje.kt @@ -0,0 +1,52 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.util + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.slåSammenLike +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somFraOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somTilOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo +import java.time.YearMonth + +class CharTidslinje(private val tegn: String, private val startTidspunkt: Tidspunkt) : + Tidslinje() { + + val fraOgMed = when (tegn.first()) { + '<' -> startTidspunkt.somUendeligLengeSiden() + else -> startTidspunkt + } + + val tilOgMed: Tidspunkt + get() { + val sluttMåned = startTidspunkt.flytt(tegn.length.toLong() - 1) + return when (tegn.last()) { + '>' -> sluttMåned.somUendeligLengeTil() + else -> sluttMåned + } + } + + override fun lagPerioder(): Collection> { + val tidspunkter = fraOgMed..tilOgMed + + return tidspunkter.mapIndexed { index, tidspunkt -> + val c = when (index) { + 0 -> if (tegn[index] == '<') tegn[index + 1] else tegn[index] + tegn.length - 1 -> if (tegn[index] == '>') tegn[index - 1] else tegn[index] + else -> tegn[index] + } + Periode(tidspunkt.somFraOgMed(), tidspunkt.somTilOgMed(), c) + } + } +} + +fun String.tilCharTidslinje(fom: YearMonth): Tidslinje = + CharTidslinje(this, MånedTidspunkt.med(fom)).slåSammenLike() + +fun String.tilCharTidslinje(fom: Tidspunkt): Tidslinje = + CharTidslinje(this, fom).slåSammenLike() diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/CharTidslinjeTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/CharTidslinjeTest.kt new file mode 100644 index 000000000..96341920e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/CharTidslinjeTest.kt @@ -0,0 +1,58 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.util + +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.slåSammenLike +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.erUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somEndelig +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class CharTidslinjeTest { + @Test + fun testEnkelCharTidsline() { + val tegn = "---------------" + val charTidslinje = CharTidslinje(tegn, jan(2020)) + Assertions.assertEquals(tegn.length, charTidslinje.perioder().size) + + val perioder = charTidslinje.slåSammenLike().perioder() + Assertions.assertEquals(1, perioder.size) + + val periode = perioder.first() + Assertions.assertEquals(jan(2020), periode.fraOgMed) + Assertions.assertEquals(mar(2021), periode.tilOgMed) + Assertions.assertEquals('-', periode.innhold) + } + + @Test + fun testUendeligCharTidslinje() { + val tegn = "<--->" + val charTidslinje = CharTidslinje(tegn, jan(2020)) + + Assertions.assertEquals(tegn.length, charTidslinje.perioder().size) + + val perioder = charTidslinje.slåSammenLike().perioder() + + Assertions.assertEquals(1, perioder.size) + val periode = perioder.first() + Assertions.assertTrue(periode.fraOgMed.erUendeligLengeSiden()) + Assertions.assertTrue(periode.tilOgMed.erUendeligLengeTil()) + Assertions.assertEquals(jan(2020), periode.fraOgMed.somEndelig()) + Assertions.assertEquals(mai(2020), periode.tilOgMed.somEndelig()) + Assertions.assertEquals('-', periode.innhold) + } + + @Test + fun testSammensattTidsline() { + val tegn = "aabbbbcdddddda" + val charTidslinje = CharTidslinje(tegn, jan(2020)) + Assertions.assertEquals(tegn.length, charTidslinje.perioder().size) + val perioder = charTidslinje.slåSammenLike().perioder().toList() + Assertions.assertEquals(5, perioder.size) + Assertions.assertEquals((jan(2020)..feb(2020)).med('a'), perioder[0]) + Assertions.assertEquals((mar(2020)..jun(2020)).med('b'), perioder[1]) + Assertions.assertEquals((jul(2020)..jul(2020)).med('c'), perioder[2]) + Assertions.assertEquals((aug(2020)..jan(2021)).med('d'), perioder[3]) + Assertions.assertEquals((feb(2021)..feb(2021)).med('a'), perioder[4]) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/StringTidslinje.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/StringTidslinje.kt new file mode 100644 index 000000000..2f07709fe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/StringTidslinje.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.util + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somFraOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somTilOgMed +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeSiden +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.somUendeligLengeTil +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo + +class StringTidslinje( + val start: Tidspunkt, + val s: List, +) : Tidslinje() { + + val fraOgMed = if (s.firstOrNull() == "<") start.somUendeligLengeSiden() else start + + val tilOgMed: Tidspunkt + get() { + val slutt = start.flytt(s.size.toLong() - 1) + return if (s.lastOrNull() == ">") slutt.somUendeligLengeTil() else slutt + } + + override fun lagPerioder(): Collection> { + val tidspunkter = fraOgMed..tilOgMed + return tidspunkter.mapIndexed { index, tidspunkt -> + val c = when (index) { + 0 -> if (s[index] == "<") s[index + 1] else s[index] + s.size - 1 -> if (s[index] == ">") s[index - 1] else s[index] + else -> s[index] + } + Periode(tidspunkt.somFraOgMed(), tidspunkt.somTilOgMed(), c) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/Tid.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/Tid.kt new file mode 100644 index 000000000..580472b3f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/Tid.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.util + +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.TidspunktClosedRange +import java.time.LocalDate +import java.time.YearMonth + +fun jan(år: Int) = MånedTidspunkt.med(YearMonth.of(år, 1)) +fun feb(år: Int) = MånedTidspunkt.med(YearMonth.of(år, 2)) +fun mar(år: Int) = MånedTidspunkt.med(YearMonth.of(år, 3)) +fun apr(år: Int) = MånedTidspunkt.med(YearMonth.of(år, 4)) +fun mai(år: Int) = MånedTidspunkt.med(YearMonth.of(år, 5)) +fun jun(år: Int) = MånedTidspunkt.med(YearMonth.of(år, 6)) +fun jul(år: Int) = MånedTidspunkt.med(YearMonth.of(år, 7)) +fun aug(år: Int) = MånedTidspunkt.med(YearMonth.of(år, 8)) +fun sep(år: Int) = MånedTidspunkt.med(YearMonth.of(år, 9)) +fun okt(år: Int) = MånedTidspunkt.med(YearMonth.of(år, 10)) +fun nov(år: Int) = MånedTidspunkt.med(YearMonth.of(år, 11)) +fun des(år: Int) = MånedTidspunkt.med(YearMonth.of(år, 12)) + +fun Int.jan(år: Int) = DagTidspunkt.med(LocalDate.of(år, 1, this)) +fun Int.feb(år: Int) = DagTidspunkt.med(LocalDate.of(år, 2, this)) +fun Int.mar(år: Int) = DagTidspunkt.med(LocalDate.of(år, 3, this)) +fun Int.apr(år: Int) = DagTidspunkt.med(LocalDate.of(år, 4, this)) +fun Int.mai(år: Int) = DagTidspunkt.med(LocalDate.of(år, 5, this)) +fun Int.jun(år: Int) = DagTidspunkt.med(LocalDate.of(år, 5, this)) +fun Int.jul(år: Int) = DagTidspunkt.med(LocalDate.of(år, 5, this)) +fun Int.aug(år: Int) = DagTidspunkt.med(LocalDate.of(år, 5, this)) +fun Int.sep(år: Int) = DagTidspunkt.med(LocalDate.of(år, 5, this)) +fun Int.okt(år: Int) = DagTidspunkt.med(LocalDate.of(år, 5, this)) +fun Int.nov(år: Int) = DagTidspunkt.med(LocalDate.of(år, 11, this)) +fun Int.des(år: Int) = DagTidspunkt.med(LocalDate.of(år, 12, this)) + +fun TidspunktClosedRange.med(t: T) = Periode(this, t) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/TidslinjePrint.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/TidslinjePrint.kt new file mode 100644 index 000000000..de65ca93c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tidslinje/util/TidslinjePrint.kt @@ -0,0 +1,11 @@ +package no.nav.familie.ba.sak.kjerne.tidslinje.util + +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Tidsenhet +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom + +fun Iterable>.print() = this.forEach { it.print() } +fun Tidslinje<*, T>.print() { + println("${this.tidsrom()} ${this.javaClass.name}") + this.perioder().forEach { println(it) } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingUtilTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingUtilTest.kt new file mode 100644 index 000000000..8d798c1f7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingUtilTest.kt @@ -0,0 +1,128 @@ +package no.nav.familie.ba.sak.kjerne.tilbakekreving + +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.kjerne.simulering.domene.SimuleringsPeriode +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.math.BigDecimal +import java.time.LocalDate + +class TilbakekrevingUtilTest { + + private val fom1 = LocalDate.of(2020, 1, 1) + private val tom1 = LocalDate.of(2020, 1, 31) + private val tom2 = LocalDate.of(2020, 2, 28) + private val fom3 = LocalDate.of(2020, 4, 1) + private val tom3 = LocalDate.of(2020, 4, 30) + private val fom4 = LocalDate.of(2020, 5, 1) + private val tom4 = LocalDate.of(2020, 5, 31) + + @Test + fun `test validerVerdierPåRestTilbakekreving kaster feil ved tilbakekreving uten feilutbetaling`() { + assertThrows { + validerVerdierPåRestTilbakekreving( + restTilbakekreving = RestTilbakekreving( + valg = Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, + begrunnelse = "", + ), + feilutbetaling = BigDecimal.ZERO, + ) + } + } + + @Test + fun `test validerVerdierPåRestTilbakekreving kaster feil ved ingen tilbakekreving når det er en feilutbetaling`() { + assertThrows { + validerVerdierPåRestTilbakekreving( + restTilbakekreving = null, + feilutbetaling = BigDecimal.ONE, + ) + } + } + + @Test + fun `test sammenslåing av feilutbetalingsperioder med ensom siste periode`() { + val simuleringsPerioder = listOf( + opprettSimuleringsPeriode( + fom = fom1, + tom = tom1, + feilUtbetaling = BigDecimal.ONE, + ), + opprettSimuleringsPeriode( + fom = LocalDate.of(2020, 2, 1), + tom = tom2, + feilUtbetaling = BigDecimal.ONE, + ), + opprettSimuleringsPeriode( + fom = LocalDate.of(2020, 3, 1), + tom = LocalDate.of(2020, 3, 31), + feilUtbetaling = BigDecimal.ZERO, + ), + opprettSimuleringsPeriode( + fom = fom4, + tom = tom4, + feilUtbetaling = BigDecimal.ONE, + ), + ) + + val sammenslåttePerioder = slåsammenNærliggendeFeilutbtalingPerioder(simuleringsPerioder) + + Assertions.assertEquals(2, sammenslåttePerioder.size) + Assertions.assertEquals(fom1, sammenslåttePerioder[0].fom) + Assertions.assertEquals(tom2, sammenslåttePerioder[0].tom) + Assertions.assertEquals(fom4, sammenslåttePerioder[1].fom) + Assertions.assertEquals(tom4, sammenslåttePerioder[1].tom) + } + + @Test + fun `test sammenslåing av feilutbetalingsperioder med ensom første periode`() { + val simuleringsPerioder = listOf( + opprettSimuleringsPeriode( + fom = fom1, + tom = tom1, + feilUtbetaling = BigDecimal.ONE, + ), + opprettSimuleringsPeriode( + fom = LocalDate.of(2020, 2, 1), + tom = tom2, + feilUtbetaling = BigDecimal.ZERO, + ), + opprettSimuleringsPeriode( + fom = fom3, + tom = tom3, + feilUtbetaling = BigDecimal.ONE, + ), + opprettSimuleringsPeriode( + fom = fom4, + tom = tom4, + feilUtbetaling = BigDecimal.ONE, + ), + ) + + val sammenslåttePerioder = slåsammenNærliggendeFeilutbtalingPerioder(simuleringsPerioder) + + Assertions.assertEquals(2, sammenslåttePerioder.size) + Assertions.assertEquals(fom1, sammenslåttePerioder[0].fom) + Assertions.assertEquals(tom1, sammenslåttePerioder[0].tom) + Assertions.assertEquals(fom3, sammenslåttePerioder[1].fom) + Assertions.assertEquals(tom4, sammenslåttePerioder[1].tom) + } + + private fun opprettSimuleringsPeriode( + fom: LocalDate, + tom: LocalDate, + feilUtbetaling: BigDecimal, + ): SimuleringsPeriode = SimuleringsPeriode( + fom = fom, + tom = tom, + feilutbetaling = feilUtbetaling, + forfallsdato = LocalDate.now(), + manuellPostering = BigDecimal.ZERO, + nyttBeløp = BigDecimal.ZERO, + tidligereUtbetalt = BigDecimal.ZERO, + resultat = BigDecimal.ZERO, + etterbetaling = BigDecimal.ZERO, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/Utgj\303\270rendePersonerTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/Utgj\303\270rendePersonerTest.kt" new file mode 100644 index 000000000..99946b01f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/Utgj\303\270rendePersonerTest.kt" @@ -0,0 +1,473 @@ +package no.nav.familie.ba.sak.kjerne.vedtak + +import io.mockk.mockk +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagTriggesAv +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertPersonResultat +import no.nav.familie.ba.sak.kjerne.brev.hentPersonerForAlleUtgjørendeVilkår +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vedtak.domene.tilMinimertPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class UtgjørendePersonerTest { + + private val featureToggleService: FeatureToggleService = mockk() + + @Test + fun `Skal hente riktige personer fra vilkårsvurderingen basert på innvilgelsesbegrunnelse`() { + val søkerFnr = randomFnr() + val barn1Fnr = randomFnr() + val barn2Fnr = randomFnr() + + val søkerAktørId = tilAktør(søkerFnr) + val barn1AktørId = tilAktør(barn1Fnr) + + val behandling = lagBehandling() + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandling.id, søkerFnr, listOf(barn1Fnr, barn2Fnr)) + + val vilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + ) + + val søkerPersonResultat = + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = søkerAktørId) + søkerPersonResultat.setSortedVilkårResultater( + setOf( + VilkårResultat( + personResultat = søkerPersonResultat, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2009, 12, 24), + periodeTom = LocalDate.of(2010, 6, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ), + VilkårResultat( + personResultat = søkerPersonResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2008, 12, 24), + periodeTom = LocalDate.of(2010, 6, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ), + ), + ) + + val barn1PersonResultat = + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = barn1AktørId) + + barn1PersonResultat.setSortedVilkårResultater( + setOf( + VilkårResultat( + personResultat = barn1PersonResultat, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2009, 12, 24), + periodeTom = LocalDate.of(2010, 6, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ), + VilkårResultat( + personResultat = barn1PersonResultat, + vilkårType = Vilkår.GIFT_PARTNERSKAP, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2009, 11, 24), + periodeTom = LocalDate.of(2010, 6, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ), + VilkårResultat( + personResultat = søkerPersonResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2009, 12, 24), + periodeTom = LocalDate.of(2010, 6, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ), + ), + ) + + val barn2PersonResultat = + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = barn1AktørId) + + barn2PersonResultat.setSortedVilkårResultater( + setOf( + VilkårResultat( + personResultat = barn1PersonResultat, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2010, 2, 24), + periodeTom = LocalDate.of(2010, 6, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ), + VilkårResultat( + personResultat = barn1PersonResultat, + vilkårType = Vilkår.GIFT_PARTNERSKAP, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2009, 11, 24), + periodeTom = LocalDate.of(2010, 6, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ), + ), + ) + + vilkårsvurdering.personResultater = setOf(søkerPersonResultat, barn1PersonResultat, barn2PersonResultat) + + val personerMedUtgjørendeVilkårLovligOpphold = hentPersonerForAlleUtgjørendeVilkår( + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + vedtaksperiode = Periode( + fom = LocalDate.of(2010, 1, 1), + tom = LocalDate.of(2010, 6, 1), + ), + oppdatertBegrunnelseType = VedtakBegrunnelseType.INNVILGET, + triggesAv = lagTriggesAv(vilkår = setOf(Vilkår.LOVLIG_OPPHOLD)), + aktuellePersonerForVedtaksperiode = personopplysningGrunnlag.personer.toList() + .map { it.tilMinimertPerson() }, + erFørsteVedtaksperiodePåFagsak = false, + + begrunnelse = Standardbegrunnelse.INNVILGET_LOVLIG_OPPHOLD_OPPHOLDSTILLATELSE, + ) + + assertEquals(2, personerMedUtgjørendeVilkårLovligOpphold.size) + assertEquals( + listOf(søkerFnr, barn1Fnr).sorted(), + personerMedUtgjørendeVilkårLovligOpphold.map { it.personIdent }.sorted(), + ) + + val personerMedUtgjørendeVilkårBosattIRiket = hentPersonerForAlleUtgjørendeVilkår( + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + vedtaksperiode = Periode( + fom = LocalDate.of(2010, 1, 1), + tom = LocalDate.of(2010, 6, 1), + ), + oppdatertBegrunnelseType = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET.vedtakBegrunnelseType, + triggesAv = lagTriggesAv(vilkår = setOf(Vilkår.BOSATT_I_RIKET)), + aktuellePersonerForVedtaksperiode = personopplysningGrunnlag.personer.toList() + .map { it.tilMinimertPerson() }, + erFørsteVedtaksperiodePåFagsak = false, + + begrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + ) + + assertEquals(1, personerMedUtgjørendeVilkårBosattIRiket.size) + assertEquals(barn1Fnr, personerMedUtgjørendeVilkårBosattIRiket.first().personIdent) + } + + @Test + fun `Skal hente riktige personer fra vilkårsvurderingen basert på reduksjon og opphørsbegrunnelser`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val barn2Fnr = randomFnr() + + val barnAktørId = tilAktør(barnFnr) + val barn2AktørId = tilAktør(barn2Fnr) + + val behandling = lagBehandling() + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + søkerFnr, + listOf(barnFnr, barn2Fnr), + barnasFødselsdatoer = listOf(LocalDate.of(2010, 12, 24), LocalDate.of(2010, 12, 24)), + ) + + val vilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + ) + + val barnPersonResultat = + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = barnAktørId) + + barnPersonResultat.setSortedVilkårResultater( + setOf( + VilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2010, 12, 24), + periodeTom = LocalDate.of(2021, 3, 31), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ), + ), + ) + + val barn2PersonResultat = + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = barn2AktørId) + + barn2PersonResultat.setSortedVilkårResultater( + setOf( + VilkårResultat( + personResultat = barn2PersonResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2010, 12, 24), + periodeTom = LocalDate.of(2021, 1, 31), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ), + ), + ) + + vilkårsvurdering.personResultater = setOf(barnPersonResultat, barn2PersonResultat) + + val personerMedUtgjørendeVilkårBosattIRiket = hentPersonerForAlleUtgjørendeVilkår( + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + vedtaksperiode = Periode( + fom = LocalDate.of(2021, 2, 1), + tom = TIDENES_ENDE, + ), + oppdatertBegrunnelseType = Standardbegrunnelse.REDUKSJON_BOSATT_I_RIKTET.vedtakBegrunnelseType, + triggesAv = lagTriggesAv(vilkår = setOf(Vilkår.BOSATT_I_RIKET)), + aktuellePersonerForVedtaksperiode = personopplysningGrunnlag.personer.toList() + .map { it.tilMinimertPerson() }, + erFørsteVedtaksperiodePåFagsak = false, + + begrunnelse = Standardbegrunnelse.REDUKSJON_BOSATT_I_RIKTET, + ) + + assertEquals(1, personerMedUtgjørendeVilkårBosattIRiket.size) + assertEquals( + barn2Fnr, + personerMedUtgjørendeVilkårBosattIRiket.first().personIdent, + ) + + val personerMedUtgjørendeVilkårBarnUtvandret = hentPersonerForAlleUtgjørendeVilkår( + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + vedtaksperiode = Periode( + fom = LocalDate.of(2021, 4, 1), + tom = TIDENES_ENDE, + ), + oppdatertBegrunnelseType = Standardbegrunnelse.OPPHØR_UTVANDRET.vedtakBegrunnelseType, + triggesAv = lagTriggesAv(vilkår = setOf(Vilkår.BOSATT_I_RIKET)), + aktuellePersonerForVedtaksperiode = personopplysningGrunnlag.personer.toList() + .map { it.tilMinimertPerson() }, + erFørsteVedtaksperiodePåFagsak = false, + + begrunnelse = Standardbegrunnelse.OPPHØR_UTVANDRET, + ) + + assertEquals(1, personerMedUtgjørendeVilkårBarnUtvandret.size) + assertEquals( + barnFnr, + personerMedUtgjørendeVilkårBarnUtvandret.first().personIdent, + ) + } + + @Test + fun `Skal kun hente medlemskapsbegrunnelser ved medlemskap og ikke hente medlemskapsbegrunnelser ellers`() { + val søkerFnr = randomFnr() + val barn1Fnr = randomFnr() + val barn2Fnr = randomFnr() + + val barn1AktørId = tilAktør(barn1Fnr) + val barn2AktørId = tilAktør(barn2Fnr) + + val behandling = lagBehandling() + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + søkerFnr, + listOf(barn1Fnr, barn2Fnr), + ) + + val vilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + ) + + val barn1PersonResultat = + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = barn1AktørId) + val barn2PersonResultat = + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = barn2AktørId) + + barn1PersonResultat.setSortedVilkårResultater( + setOf( + lagVilkårResultat( + barn1PersonResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + periodeFom = LocalDate.of(2021, 11, 1), + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP), + ), + ), + ) + barn2PersonResultat.setSortedVilkårResultater( + setOf( + lagVilkårResultat( + barn2PersonResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + periodeFom = LocalDate.of(2021, 11, 1), + utdypendeVilkårsvurderinger = emptyList(), + ), + ), + ) + + vilkårsvurdering.personResultater = + setOf(barn1PersonResultat, barn2PersonResultat) + + val personerMedUtgjørendeVilkårBosattIRiketMedlemskap = hentPersonerForAlleUtgjørendeVilkår( + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + vedtaksperiode = Periode( + fom = LocalDate.of(2021, 12, 1), + tom = TIDENES_ENDE, + ), + oppdatertBegrunnelseType = VedtakBegrunnelseType.INNVILGET, + triggesAv = lagTriggesAv(vilkår = setOf(Vilkår.BOSATT_I_RIKET), medlemskap = true), + aktuellePersonerForVedtaksperiode = personopplysningGrunnlag.personer.toList() + .map { it.tilMinimertPerson() }, + erFørsteVedtaksperiodePåFagsak = false, + + begrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + ) + + val personerMedUtgjørendeVilkårBosattIRiket = hentPersonerForAlleUtgjørendeVilkår( + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + vedtaksperiode = Periode( + fom = LocalDate.of(2021, 12, 1), + tom = TIDENES_ENDE, + ), + oppdatertBegrunnelseType = VedtakBegrunnelseType.INNVILGET, + triggesAv = lagTriggesAv(vilkår = setOf(Vilkår.BOSATT_I_RIKET)), + aktuellePersonerForVedtaksperiode = personopplysningGrunnlag.personer.toList() + .map { it.tilMinimertPerson() }, + erFørsteVedtaksperiodePåFagsak = false, + + begrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + ) + + assertEquals(1, personerMedUtgjørendeVilkårBosattIRiketMedlemskap.size) + assertEquals( + barn1Fnr, + personerMedUtgjørendeVilkårBosattIRiketMedlemskap.first().personIdent, + ) + + assertEquals(1, personerMedUtgjørendeVilkårBosattIRiket.size) + assertEquals( + barn2Fnr, + personerMedUtgjørendeVilkårBosattIRiket.first().personIdent, + ) + } + + @Test + fun `Skal ta med riktig personer på avslag som er samtidige`() { + val søkerFnr = randomFnr() + val barn1Fnr = randomFnr() + val barn2Fnr = randomFnr() + + val barn1AktørId = tilAktør(barn1Fnr) + val barn2AktørId = tilAktør(barn2Fnr) + + val behandling = lagBehandling() + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + søkerFnr, + listOf(barn1Fnr, barn2Fnr), + ) + + val vilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + ) + + val barn1PersonResultat = + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = barn1AktørId) + val barn2PersonResultat = + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = barn2AktørId) + + val avslagBegrunnelse1 = Standardbegrunnelse.AVSLAG_IKKE_AVTALE_OM_DELT_BOSTED + barn1PersonResultat.setSortedVilkårResultater( + setOf( + lagVilkårResultat( + barn1PersonResultat, + vilkårType = Vilkår.BOR_MED_SØKER, + periodeFom = null, + periodeTom = null, + resultat = Resultat.IKKE_OPPFYLT, + erEksplisittAvslagPåSøknad = true, + standardbegrunnelser = listOf(avslagBegrunnelse1), + ), + ), + ) + + val avslagBegrunnelse2 = Standardbegrunnelse.AVSLAG_BOR_HOS_SØKER + barn2PersonResultat.setSortedVilkårResultater( + setOf( + lagVilkårResultat( + barn2PersonResultat, + vilkårType = Vilkår.BOR_MED_SØKER, + periodeFom = null, + periodeTom = null, + resultat = Resultat.IKKE_OPPFYLT, + erEksplisittAvslagPåSøknad = true, + standardbegrunnelser = listOf(avslagBegrunnelse2), + ), + ), + ) + + vilkårsvurdering.personResultater = + setOf(barn1PersonResultat, barn2PersonResultat) + + val personerMedUtgjørendeVilkårAvslag1 = hentPersonerForAlleUtgjørendeVilkår( + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + vedtaksperiode = Periode( + fom = TIDENES_MORGEN, + tom = TIDENES_ENDE, + ), + oppdatertBegrunnelseType = VedtakBegrunnelseType.AVSLAG, + triggesAv = lagTriggesAv(vilkår = setOf(Vilkår.BOR_MED_SØKER), medlemskap = true), + aktuellePersonerForVedtaksperiode = personopplysningGrunnlag.personer.toList() + .map { it.tilMinimertPerson() }, + erFørsteVedtaksperiodePåFagsak = false, + + begrunnelse = avslagBegrunnelse1, + ) + + val personerMedUtgjørendeVilkårAvslag2 = hentPersonerForAlleUtgjørendeVilkår( + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + vedtaksperiode = Periode( + fom = TIDENES_MORGEN, + tom = TIDENES_ENDE, + ), + oppdatertBegrunnelseType = VedtakBegrunnelseType.AVSLAG, + triggesAv = lagTriggesAv(vilkår = setOf(Vilkår.BOR_MED_SØKER)), + aktuellePersonerForVedtaksperiode = personopplysningGrunnlag.personer.toList() + .map { it.tilMinimertPerson() }, + erFørsteVedtaksperiodePåFagsak = false, + + begrunnelse = avslagBegrunnelse2, + ) + + assertEquals(1, personerMedUtgjørendeVilkårAvslag1.size) + assertEquals( + barn1Fnr, + personerMedUtgjørendeVilkårAvslag1.first().personIdent, + ) + + assertEquals(1, personerMedUtgjørendeVilkårAvslag2.size) + assertEquals( + barn2Fnr, + personerMedUtgjørendeVilkårAvslag2.first().personIdent, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/Vilk\303\245rUtilsTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/Vilk\303\245rUtilsTest.kt" new file mode 100644 index 000000000..19ec6c495 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/Vilk\303\245rUtilsTest.kt" @@ -0,0 +1,73 @@ +package no.nav.familie.ba.sak.kjerne.vedtak + +import no.nav.familie.ba.sak.common.lagUtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.common.lagUtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.sorter +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class VilkårUtilsTest { + + /** + * Korrekt rekkefølge: + * 1. Utbetalings-, opphørs- og avslagsperioder sortert på fom-dato + * 2. Avslagsperioder uten datoer + */ + @Test + fun `vedtaksperioder sorteres korrekt til brev`() { + val avslagMedTomDatoInneværendeMåned = lagUtvidetVedtaksperiodeMedBegrunnelser( + fom = LocalDate.now().minusMonths(6), + tom = LocalDate.now(), + type = Vedtaksperiodetype.AVSLAG, + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + + ) + val avslagUtenTomDato = + lagUtvidetVedtaksperiodeMedBegrunnelser( + fom = LocalDate.now().minusMonths(5), + tom = null, + type = Vedtaksperiodetype.AVSLAG, + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ) + val opphørsperiode = lagUtvidetVedtaksperiodeMedBegrunnelser( + fom = LocalDate.now().minusMonths(4), + tom = LocalDate.now().minusMonths(1), + type = Vedtaksperiodetype.OPPHØR, + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ) + + val utbetalingsperiode = lagUtvidetVedtaksperiodeMedBegrunnelser( + fom = LocalDate.now().minusMonths(3), + tom = LocalDate.now().minusMonths(1), + type = Vedtaksperiodetype.UTBETALING, + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ) + + val avslagUtenDatoer = lagUtvidetVedtaksperiodeMedBegrunnelser( + fom = null, + tom = null, + type = Vedtaksperiodetype.AVSLAG, + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ) + + val sorterteVedtaksperioder = + listOf( + utbetalingsperiode, + opphørsperiode, + avslagMedTomDatoInneværendeMåned, + avslagUtenDatoer, + avslagUtenTomDato, + ).shuffled().sorter() + + // Utbetalingsperiode, opphørspersiode og avslagsperiode med fom-dato sorteres kronologisk + Assertions.assertEquals(avslagMedTomDatoInneværendeMåned, sorterteVedtaksperioder[0]) + Assertions.assertEquals(avslagUtenTomDato, sorterteVedtaksperioder[1]) + Assertions.assertEquals(opphørsperiode, sorterteVedtaksperioder[2]) + Assertions.assertEquals(utbetalingsperiode, sorterteVedtaksperioder[3]) + + // Avslag uten datoer legger seg til slutt + Assertions.assertEquals(avslagUtenDatoer, sorterteVedtaksperioder[4]) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/IVedtakBegrunnelseTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/IVedtakBegrunnelseTest.kt new file mode 100644 index 000000000..563559238 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/IVedtakBegrunnelseTest.kt @@ -0,0 +1,24 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser + +import no.nav.familie.kontrakter.felles.objectMapper +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +internal class IVedtakBegrunnelseTest { + @Test + fun `Skal serialiseres med prefix`() { + val serialisertStandardbegrunnelse = + objectMapper.writeValueAsString(Standardbegrunnelse.INNVILGET_BOR_HOS_SØKER) + Assertions.assertEquals( + objectMapper.readValue(serialisertStandardbegrunnelse, Standardbegrunnelse::class.java), + Standardbegrunnelse.INNVILGET_BOR_HOS_SØKER, + ) + + val serialisertEØSStandardbegrunnelse = + objectMapper.writeValueAsString(EØSStandardbegrunnelse.AVSLAG_EØS_IKKE_EØS_BORGER) + Assertions.assertEquals( + objectMapper.readValue(serialisertEØSStandardbegrunnelse, EØSStandardbegrunnelse::class.java), + EØSStandardbegrunnelse.AVSLAG_EØS_IKKE_EØS_BORGER, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/StandardbegrunnelseTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/StandardbegrunnelseTest.kt new file mode 100644 index 000000000..67f80a2fe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/StandardbegrunnelseTest.kt @@ -0,0 +1,561 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser + +import io.mockk.mockk +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndel +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagTriggesAv +import no.nav.familie.ba.sak.common.lagUtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.common.lagUtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.config.testSanityKlient +import no.nav.familie.ba.sak.datagenerator.brev.lagMinimertPerson +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.brev.domene.EndretUtbetalingsperiodeDeltBostedTriggere +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertPersonResultat +import no.nav.familie.ba.sak.kjerne.brev.domene.ØvrigTrigger +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.domene.PersonIdent +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.lagDødsfallFraPdl +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.Personident +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.tilMinimertVedtaksperiode +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.tilMinimertePersoner +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class StandardbegrunnelseTest { + + private val behandling = lagBehandling() + private val søker = tilfeldigPerson(personType = PersonType.SØKER) + private val barn = tilfeldigPerson(personType = PersonType.BARN) + private val utvidetVedtaksperiodeMedBegrunnelser = lagUtvidetVedtaksperiodeMedBegrunnelser( + type = Vedtaksperiodetype.UTBETALING, + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ) + private val vilkårsvurdering = + lagVilkårsvurdering(søker.aktør, lagBehandling(), Resultat.OPPFYLT) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag(behandling.id, søker, barn) + + private val aktørerMedUtbetaling = listOf(søker.aktør, barn.aktør) + + private val sanityBegrunnelser = testSanityKlient.hentBegrunnelserMap() + private val featureToggleService: FeatureToggleService = mockk() + + @Test + fun `Oppfyller vilkår skal gi true`() { + assertTrue( + Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET + .triggesForPeriode( + + sanityBegrunnelser = sanityBegrunnelser, + minimertVedtaksperiode = utvidetVedtaksperiodeMedBegrunnelser.tilMinimertVedtaksperiode(), + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimertePersoner = personopplysningGrunnlag.tilMinimertePersoner(), + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + erFørsteVedtaksperiodePåFagsak = false, + ytelserForSøkerForrigeMåned = emptyList(), + ytelserForrigePeriode = emptyList(), + ), + ) + } + + @Test + fun `Annen periode type skal gi false`() { + assertFalse( + Standardbegrunnelse.OPPHØR_UTVANDRET + .triggesForPeriode( + + sanityBegrunnelser = sanityBegrunnelser, + minimertVedtaksperiode = utvidetVedtaksperiodeMedBegrunnelser.tilMinimertVedtaksperiode(), + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimertePersoner = personopplysningGrunnlag.tilMinimertePersoner(), + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + erFørsteVedtaksperiodePåFagsak = false, + ytelserForSøkerForrigeMåned = emptyList(), + ytelserForrigePeriode = emptyList(), + ), + ) + } + + @Test + fun `Har ikke barn med seksårsdag skal gi false`() { + assertFalse( + Standardbegrunnelse.REDUKSJON_UNDER_6_ÅR + .triggesForPeriode( + + sanityBegrunnelser = sanityBegrunnelser, + minimertVedtaksperiode = utvidetVedtaksperiodeMedBegrunnelser.tilMinimertVedtaksperiode(), + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimertePersoner = personopplysningGrunnlag.tilMinimertePersoner(), + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + erFørsteVedtaksperiodePåFagsak = false, + ytelserForSøkerForrigeMåned = emptyList(), + ytelserForrigePeriode = emptyList(), + ), + ) + } + + @Test + fun `Har barn med seksårsdag skal gi true`() { + val minimertePersoner = + listOf( + lagMinimertPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(6)), + lagMinimertPerson(type = PersonType.SØKER), + ) + + assertTrue( + Standardbegrunnelse.REDUKSJON_UNDER_6_ÅR + .triggesForPeriode( + + sanityBegrunnelser = sanityBegrunnelser, + minimertVedtaksperiode = utvidetVedtaksperiodeMedBegrunnelser.tilMinimertVedtaksperiode(), + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimertePersoner = minimertePersoner, + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + erFørsteVedtaksperiodePåFagsak = false, + ytelserForSøkerForrigeMåned = emptyList(), + ytelserForrigePeriode = emptyList(), + ), + ) + } + + @Test + fun `Har sats endring skal gi true`() { + val vedtaksperiodeMedBegrunnelserSatsEndring = lagUtvidetVedtaksperiodeMedBegrunnelser( + fom = LocalDate.of(2021, 9, 1), + type = Vedtaksperiodetype.UTBETALING, + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ) + + assertTrue( + Standardbegrunnelse.INNVILGET_SATSENDRING + .triggesForPeriode( + + sanityBegrunnelser = sanityBegrunnelser, + minimertVedtaksperiode = vedtaksperiodeMedBegrunnelserSatsEndring.tilMinimertVedtaksperiode(), + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimertePersoner = personopplysningGrunnlag.tilMinimertePersoner(), + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + erFørsteVedtaksperiodePåFagsak = false, + ytelserForSøkerForrigeMåned = emptyList(), + ytelserForrigePeriode = emptyList(), + ), + ) + } + + @Test + fun `Har ikke sats endring skal gi false`() { + val vedtaksperiodeMedBegrunnelserSatsEndring = lagUtvidetVedtaksperiodeMedBegrunnelser( + fom = LocalDate.of(2021, 8, 1), + type = Vedtaksperiodetype.UTBETALING, + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj()), + ) + + assertFalse( + Standardbegrunnelse.INNVILGET_SATSENDRING + .triggesForPeriode( + + sanityBegrunnelser = sanityBegrunnelser, + minimertVedtaksperiode = vedtaksperiodeMedBegrunnelserSatsEndring.tilMinimertVedtaksperiode(), + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimertePersoner = personopplysningGrunnlag.tilMinimertePersoner(), + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + erFørsteVedtaksperiodePåFagsak = false, + ytelserForSøkerForrigeMåned = emptyList(), + ytelserForrigePeriode = emptyList(), + ), + ) + } + + @Test + fun `Oppfyller ikke vilkår for person skal gi false`() { + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag(behandling.id, barn) + + assertFalse( + Standardbegrunnelse.INNVILGET_LOVLIG_OPPHOLD_EØS_BORGER + .triggesForPeriode( + + sanityBegrunnelser = sanityBegrunnelser, + minimertVedtaksperiode = utvidetVedtaksperiodeMedBegrunnelser.tilMinimertVedtaksperiode(), + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimertePersoner = personopplysningGrunnlag.tilMinimertePersoner(), + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + erFørsteVedtaksperiodePåFagsak = false, + ytelserForSøkerForrigeMåned = emptyList(), + ytelserForrigePeriode = emptyList(), + ), + ) + } + + @Test + fun `Oppfyller vilkår for person skal gi true`() { + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag(behandling.id, søker) + + assertTrue( + Standardbegrunnelse.INNVILGET_LOVLIG_OPPHOLD_EØS_BORGER + .triggesForPeriode( + + sanityBegrunnelser = sanityBegrunnelser, + minimertVedtaksperiode = utvidetVedtaksperiodeMedBegrunnelser.tilMinimertVedtaksperiode(), + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimertePersoner = personopplysningGrunnlag.tilMinimertePersoner(), + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + erFørsteVedtaksperiodePåFagsak = false, + ytelserForSøkerForrigeMåned = emptyList(), + ytelserForrigePeriode = emptyList(), + ), + ) + } + + @Test + fun `Oppfyller etter endringsperiode skal gi true`() { + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag(behandling.id, barn) + + assertTrue( + Standardbegrunnelse.ETTER_ENDRET_UTBETALING_AVTALE_DELT_BOSTED_FØLGES + .triggesForPeriode( + + sanityBegrunnelser = sanityBegrunnelser, + minimertePersoner = personopplysningGrunnlag.tilMinimertePersoner(), + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + minimertVedtaksperiode = lagUtvidetVedtaksperiodeMedBegrunnelser( + type = Vedtaksperiodetype.UTBETALING, + fom = LocalDate.of(2021, 10, 1), + tom = LocalDate.of(2021, 10, 31), + ).tilMinimertVedtaksperiode(), + minimerteEndredeUtbetalingAndeler = listOf( + lagEndretUtbetalingAndel( + prosent = BigDecimal.ZERO, + behandlingId = behandling.id, + person = barn, + fom = YearMonth.of(2021, 6), + tom = YearMonth.of(2021, 9), + årsak = Årsak.DELT_BOSTED, + ), + ).map { it.tilMinimertEndretUtbetalingAndel() }, + erFørsteVedtaksperiodePåFagsak = false, + ytelserForSøkerForrigeMåned = emptyList(), + ytelserForrigePeriode = emptyList(), + ), + ) + } + + @Test + fun `Oppfyller ikke etter endringsperiode skal gi false`() { + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag(behandling.id, barn) + + assertFalse( + Standardbegrunnelse.ETTER_ENDRET_UTBETALING_AVTALE_DELT_BOSTED_FØLGES + .triggesForPeriode( + + sanityBegrunnelser = sanityBegrunnelser, + minimertePersoner = personopplysningGrunnlag.tilMinimertePersoner(), + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + minimertVedtaksperiode = lagUtvidetVedtaksperiodeMedBegrunnelser( + type = Vedtaksperiodetype.UTBETALING, + fom = LocalDate.of(2021, 10, 1), + tom = LocalDate.of(2021, 10, 31), + ).tilMinimertVedtaksperiode(), + minimerteEndredeUtbetalingAndeler = listOf( + lagEndretUtbetalingAndel( + prosent = BigDecimal.ZERO, + behandlingId = behandling.id, + person = barn, + fom = YearMonth.of(2021, 10), + tom = YearMonth.of(2021, 10), + årsak = Årsak.DELT_BOSTED, + ), + ).map { it.tilMinimertEndretUtbetalingAndel() }, + erFørsteVedtaksperiodePåFagsak = false, + ytelserForSøkerForrigeMåned = emptyList(), + ytelserForrigePeriode = emptyList(), + ), + ) + } + + @Test + fun `Oppfyller skal utbetales gir false`() { + assertFalse( + lagEndretUtbetalingAndel(prosent = BigDecimal.ZERO, person = barn) + .tilMinimertEndretUtbetalingAndel() + .oppfyllerSkalUtbetalesTrigger( + triggesAv = lagTriggesAv(endretUtbetalingSkalUtbetales = EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_UTBETALES), + ), + ) + + assertFalse( + lagEndretUtbetalingAndel(prosent = BigDecimal.valueOf(100), person = barn) + .tilMinimertEndretUtbetalingAndel() + .oppfyllerSkalUtbetalesTrigger( + triggesAv = lagTriggesAv(endretUtbetalingSkalUtbetales = EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_IKKE_UTBETALES), + ), + ) + } + + @Test + fun `Oppfyller skal utbetales gir true`() { + assertTrue( + lagEndretUtbetalingAndel(prosent = BigDecimal.ZERO, person = barn) + .tilMinimertEndretUtbetalingAndel() + .oppfyllerSkalUtbetalesTrigger( + triggesAv = lagTriggesAv(endretUtbetalingSkalUtbetales = EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_IKKE_UTBETALES), + ), + ) + + assertTrue( + lagEndretUtbetalingAndel(prosent = BigDecimal.valueOf(100), person = barn) + .tilMinimertEndretUtbetalingAndel() + .oppfyllerSkalUtbetalesTrigger( + triggesAv = lagTriggesAv(endretUtbetalingSkalUtbetales = EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_UTBETALES), + ), + ) + } + + @Test + fun `Alle begrunnelser er unike`() { + val vedtakBegrunnelser = Standardbegrunnelse.values().groupBy { it.sanityApiNavn } + assertEquals(vedtakBegrunnelser.size, Standardbegrunnelse.values().size) + } + + private fun String.startsWithUppercaseLetter(): Boolean { + return this.matches(Regex("[A-Z]{1}.*")) + } + + @Test + fun `Dersom dødsfalldato ligger i forrige ytelse-periode skal begrunnelsen begrunnelser med trigger BARN_DØD trigges`() { + val fnr = "12345678910" + val dødtBarn = lagPerson(personIdent = PersonIdent(fnr), type = PersonType.BARN) + dødtBarn.dødsfall = lagDødsfallFraPdl( + dødtBarn, + dødsfallDatoFraPdl = LocalDate.now().minusMonths(1).withDayOfMonth(15).toString(), + dødsfallAdresseFraPdl = null, + ) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag(behandling.id, dødtBarn) + + val reduksjonBarnDødBegrunnelse = mapOf( + Standardbegrunnelse.REDUKSJON_BARN_DØD to SanityBegrunnelse( + apiNavn = "reduksjonBarnDod", + navnISystem = "barnDød", + ovrigeTriggere = listOf(ØvrigTrigger.BARN_DØD), + ), + ) + + val ytelserForrigeMåned = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of( + LocalDate.now().minusMonths(1).year, + LocalDate.now().minusMonths(1).month, + ), + tom = YearMonth.of(LocalDate.now().year, LocalDate.now().month), + aktør = Aktør(fnr + "00").also { it.personidenter.add(Personident(fnr, it)) }, + ), + ) + + assertTrue( + Standardbegrunnelse.REDUKSJON_BARN_DØD + .triggesForPeriode( + sanityBegrunnelser = reduksjonBarnDødBegrunnelse, + minimertVedtaksperiode = utvidetVedtaksperiodeMedBegrunnelser.tilMinimertVedtaksperiode(), + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimertePersoner = personopplysningGrunnlag.tilMinimertePersoner(), + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + erFørsteVedtaksperiodePåFagsak = false, + ytelserForSøkerForrigeMåned = emptyList(), + ytelserForrigePeriode = ytelserForrigeMåned, + ), + ) + } + + @Test + fun `Dersom dødsfalldato ligger etter en ytelse-periode skal ikke begrunnelser med trigger BARN_DØD trigges`() { + val fnr = "12345678910" + val dødtBarn = lagPerson(personIdent = PersonIdent(fnr), type = PersonType.BARN) + val dødsfallDato = LocalDate.now().minusMonths(1).withDayOfMonth(15) + dødtBarn.dødsfall = + lagDødsfallFraPdl(dødtBarn, dødsfallDatoFraPdl = dødsfallDato.toString(), dødsfallAdresseFraPdl = null) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag(behandling.id, dødtBarn) + + val reduksjonBarnDødBegrunnelse = mapOf( + Standardbegrunnelse.REDUKSJON_BARN_DØD to SanityBegrunnelse( + apiNavn = "reduksjonBarnDod", + navnISystem = "barnDød", + ovrigeTriggere = listOf(ØvrigTrigger.BARN_DØD), + ), + ) + + val ytelserForrigeMåned = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of( + dødsfallDato.minusMonths(5).year, + dødsfallDato.minusMonths(5).month, + ), + tom = YearMonth.of(dødsfallDato.minusMonths(1).year, dødsfallDato.minusMonths(1).month), + aktør = Aktør(fnr + "00").also { it.personidenter.add(Personident(fnr, it)) }, + ), + ) + + assertFalse( + Standardbegrunnelse.REDUKSJON_BARN_DØD + .triggesForPeriode( + sanityBegrunnelser = reduksjonBarnDødBegrunnelse, + minimertVedtaksperiode = utvidetVedtaksperiodeMedBegrunnelser.tilMinimertVedtaksperiode(), + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimertePersoner = personopplysningGrunnlag.tilMinimertePersoner(), + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + erFørsteVedtaksperiodePåFagsak = false, + ytelserForSøkerForrigeMåned = emptyList(), + ytelserForrigePeriode = ytelserForrigeMåned, + ), + ) + } + + @Test + fun `Dersom dødsfalldato ligger før en ytelse-periode skal ikke begrunnelser med trigger BARN_DØD trigges`() { + val fnr = "12345678910" + val dødtBarn = lagPerson(personIdent = PersonIdent(fnr), type = PersonType.BARN) + val dødsfallDato = LocalDate.now().minusMonths(1).withDayOfMonth(15) + dødtBarn.dødsfall = + lagDødsfallFraPdl(dødtBarn, dødsfallDatoFraPdl = dødsfallDato.toString(), dødsfallAdresseFraPdl = null) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag(behandling.id, dødtBarn) + + val reduksjonBarnDødBegrunnelse = mapOf( + Standardbegrunnelse.REDUKSJON_BARN_DØD to SanityBegrunnelse( + apiNavn = "reduksjonBarnDod", + navnISystem = "barnDød", + ovrigeTriggere = listOf(ØvrigTrigger.BARN_DØD), + ), + ) + + val ytelserForrigeMåned = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of( + dødsfallDato.plusMonths(5).year, + dødsfallDato.plusMonths(5).month, + ), + tom = YearMonth.of(dødsfallDato.plusMonths(6).year, dødsfallDato.plusMonths(6).month), + aktør = Aktør(fnr + "00").also { it.personidenter.add(Personident(fnr, it)) }, + ), + ) + + assertFalse( + Standardbegrunnelse.REDUKSJON_BARN_DØD + .triggesForPeriode( + sanityBegrunnelser = reduksjonBarnDødBegrunnelse, + minimertVedtaksperiode = utvidetVedtaksperiodeMedBegrunnelser.tilMinimertVedtaksperiode(), + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimertePersoner = personopplysningGrunnlag.tilMinimertePersoner(), + aktørIderMedUtbetaling = aktørerMedUtbetaling.map { it.aktørId }, + erFørsteVedtaksperiodePåFagsak = false, + ytelserForSøkerForrigeMåned = emptyList(), + ytelserForrigePeriode = ytelserForrigeMåned, + ), + ) + } + + @Test + fun `dødeBarnForrigePeriode() skal returnere barn som døde i forrige periode og som er tilknyttet ytelsen`() { + val barn1Fnr = "12345678910" + val barn2Fnr = "12345678911" + + // Barn1 dør før Barn2. + var dødsfallDatoBarn1 = LocalDate.of(2022, 5, 12) + var dødsfallDatoBarn2 = LocalDate.of(2022, 7, 2) + var barnIBehandling = listOf( + lagMinimertPerson(dødsfallsdato = dødsfallDatoBarn1, aktivPersonIdent = barn1Fnr), + lagMinimertPerson(dødsfallsdato = dødsfallDatoBarn2, aktivPersonIdent = barn2Fnr), + ) + var ytelserForrigePeriode = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of( + dødsfallDatoBarn1.minusMonths(1).year, + dødsfallDatoBarn1.minusMonths(1).month, + ), + tom = YearMonth.of(dødsfallDatoBarn1.year, dødsfallDatoBarn1.month), + aktør = Aktør(barn1Fnr + "00").also { it.personidenter.add(Personident(barn1Fnr, it)) }, + ), + ) + + var dødeBarnForrigePeriode = dødeBarnForrigePeriode(ytelserForrigePeriode, barnIBehandling) + assertEquals( + 1, + dødeBarnForrigePeriode.size, + ) + assertEquals( + barn1Fnr, + dødeBarnForrigePeriode[0], + ) + + ytelserForrigePeriode = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of( + dødsfallDatoBarn1.minusMonths(1).year, + dødsfallDatoBarn1.minusMonths(1).month, + ), + tom = YearMonth.of(dødsfallDatoBarn2.year, dødsfallDatoBarn2.month), + aktør = Aktør(barn2Fnr + "00").also { it.personidenter.add(Personident(barn2Fnr, it)) }, + ), + ) + + dødeBarnForrigePeriode = dødeBarnForrigePeriode(ytelserForrigePeriode, barnIBehandling) + assertEquals( + 1, + dødeBarnForrigePeriode.size, + ) + assertEquals( + barn2Fnr, + dødeBarnForrigePeriode[0], + ) + + // Barn1 og Barn2 dør i samme måned + dødsfallDatoBarn1 = LocalDate.of(2022, 5, 12) + dødsfallDatoBarn2 = LocalDate.of(2022, 5, 2) + + barnIBehandling = listOf( + lagMinimertPerson(dødsfallsdato = dødsfallDatoBarn1, aktivPersonIdent = barn1Fnr), + lagMinimertPerson(dødsfallsdato = dødsfallDatoBarn2, aktivPersonIdent = barn2Fnr), + ) + + ytelserForrigePeriode = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of( + dødsfallDatoBarn1.minusMonths(1).year, + dødsfallDatoBarn1.minusMonths(1).month, + ), + tom = YearMonth.of(dødsfallDatoBarn1.year, dødsfallDatoBarn1.month), + aktør = Aktør(barn1Fnr + "00").also { it.personidenter.add(Personident(barn1Fnr, it)) }, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(dødsfallDatoBarn2.minusMonths(1).year, dødsfallDatoBarn2.minusMonths(1).month), + tom = YearMonth.of(dødsfallDatoBarn2.year, dødsfallDatoBarn2.month), + aktør = Aktør(barn2Fnr + "00").also { it.personidenter.add(Personident(barn2Fnr, it)) }, + ), + ) + + dødeBarnForrigePeriode = dødeBarnForrigePeriode(ytelserForrigePeriode, barnIBehandling) + assertEquals( + 2, + dødeBarnForrigePeriode.size, + ) + assertTrue( + dødeBarnForrigePeriode.containsAll(barnIBehandling.map { it.aktivPersonIdent }), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/TriggesAvTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/TriggesAvTest.kt new file mode 100644 index 000000000..9163b5346 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/begrunnelser/TriggesAvTest.kt @@ -0,0 +1,247 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser + +import no.nav.familie.ba.sak.common.lagTriggesAv +import no.nav.familie.ba.sak.datagenerator.brev.lagMinimertUtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.datagenerator.endretUtbetaling.lagMinimertEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.domene.EndretUtbetalingsperiodeDeltBostedTriggere +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +class TriggesAvTest { + + val vilkårUtenUtvidetBarnetrygd: Set = emptySet() + val vilkårMedUtvidetBarnetrygd: Set = setOf(Vilkår.UTVIDET_BARNETRYGD) + + val endretUtbetalingAndelNull = + lagMinimertEndretUtbetalingAndel( + prosent = BigDecimal.ZERO, + årsak = Årsak.DELT_BOSTED, + ) + val endretUtbetalingAndelIkkeNull = + lagMinimertEndretUtbetalingAndel( + prosent = BigDecimal.ONE, + årsak = Årsak.DELT_BOSTED, + ) + + val triggesAvEtterEndretUtbetaling = lagTriggesAv( + etterEndretUtbetaling = true, + endretUtbetalingSkalUtbetales = EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_UTBETALES, + endringsaarsaker = setOf( + Årsak.DELT_BOSTED, + ), + vilkår = vilkårMedUtvidetBarnetrygd, + ) + + val triggesIkkeAvSkalUtbetalesMedUtvidetVilkår = + lagTriggesAv( + endretUtbetalingSkalUtbetales = EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_IKKE_UTBETALES, + etterEndretUtbetaling = false, + endringsaarsaker = setOf( + Årsak.DELT_BOSTED, + ), + vilkår = vilkårMedUtvidetBarnetrygd, + ) + + val triggesIkkeAvSkalUtbetalesUtenUtvidetVilkår = + lagTriggesAv( + endretUtbetalingSkalUtbetales = EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_IKKE_UTBETALES, + etterEndretUtbetaling = false, + endringsaarsaker = setOf( + Årsak.DELT_BOSTED, + ), + vilkår = vilkårUtenUtvidetBarnetrygd, + ) + + val triggesAvSkalUtbetalesMedUtvidetVilkår = lagTriggesAv( + endretUtbetalingSkalUtbetales = EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_UTBETALES, + etterEndretUtbetaling = false, + endringsaarsaker = setOf( + Årsak.DELT_BOSTED, + ), + vilkår = vilkårMedUtvidetBarnetrygd, + ) + + val triggesAvSkalUtbetalesUtenUtvidetVilkår = lagTriggesAv( + endretUtbetalingSkalUtbetales = EndretUtbetalingsperiodeDeltBostedTriggere.SKAL_UTBETALES, + etterEndretUtbetaling = false, + endringsaarsaker = setOf( + Årsak.DELT_BOSTED, + ), + vilkår = vilkårUtenUtvidetBarnetrygd, + ) + + @Test + fun `Skal gi false dersom er etter endret utbetaling`() { + val erEtterEndretUbetaling = triggesAvEtterEndretUtbetaling.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndelIkkeNull, + minimerteUtbetalingsperiodeDetaljer = emptyList(), + ) + + Assertions.assertFalse(erEtterEndretUbetaling) + + val erEtterEndretUbetalingMedToggle = triggesAvEtterEndretUtbetaling.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndelIkkeNull, + minimerteUtbetalingsperiodeDetaljer = listOf( + lagMinimertUtbetalingsperiodeDetalj( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ), + ), + + ) + Assertions.assertFalse(erEtterEndretUbetalingMedToggle) + } + + @Test + fun `Triggere for endret utbetaling-begrunnelser skal bli true ved riktig utbetalingsandel`() { + val skalUtbetalesMedUtbetaling = + triggesAvSkalUtbetalesMedUtvidetVilkår.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndelIkkeNull, + minimerteUtbetalingsperiodeDetaljer = listOf( + lagMinimertUtbetalingsperiodeDetalj( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + erPåvirketAvEndring = true, + ), + ), + ) + + val skalUtbetalesUtenUtbetaling = + triggesAvSkalUtbetalesMedUtvidetVilkår.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndelNull, + minimerteUtbetalingsperiodeDetaljer = listOf( + lagMinimertUtbetalingsperiodeDetalj( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + erPåvirketAvEndring = true, + ), + ), + ) + + val skalIkkeUtbetalesUtenUtbetaling = + triggesIkkeAvSkalUtbetalesMedUtvidetVilkår.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndelNull, + minimerteUtbetalingsperiodeDetaljer = listOf( + lagMinimertUtbetalingsperiodeDetalj( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + erPåvirketAvEndring = true, + ), + ), + ) + + val skalIkkeUtbetalesMedUtbetaling = + triggesIkkeAvSkalUtbetalesMedUtvidetVilkår.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndelIkkeNull, + minimerteUtbetalingsperiodeDetaljer = listOf( + lagMinimertUtbetalingsperiodeDetalj( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + erPåvirketAvEndring = true, + ), + ), + ) + + Assertions.assertTrue(skalUtbetalesMedUtbetaling) + Assertions.assertFalse(skalUtbetalesUtenUtbetaling) + Assertions.assertTrue(skalIkkeUtbetalesUtenUtbetaling) + Assertions.assertFalse(skalIkkeUtbetalesMedUtbetaling) + } + + @Test + fun `Skal gi riktig resultat for om endret utbetaling begrunnelse trigges ved utvidetScenario`() { + Assertions.assertTrue( + triggesAvSkalUtbetalesMedUtvidetVilkår.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndelIkkeNull, + minimerteUtbetalingsperiodeDetaljer = listOf( + lagMinimertUtbetalingsperiodeDetalj( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + erPåvirketAvEndring = true, + ), + ), + ), + ) + + Assertions.assertFalse( + triggesAvSkalUtbetalesMedUtvidetVilkår.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndelIkkeNull, + minimerteUtbetalingsperiodeDetaljer = listOf( + lagMinimertUtbetalingsperiodeDetalj(ytelseType = YtelseType.ORDINÆR_BARNETRYGD), + ), + ), + ) + + Assertions.assertTrue( + triggesAvSkalUtbetalesUtenUtvidetVilkår.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndelIkkeNull, + minimerteUtbetalingsperiodeDetaljer = listOf( + lagMinimertUtbetalingsperiodeDetalj( + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + ), + ), + ) + + Assertions.assertFalse( + triggesAvSkalUtbetalesUtenUtvidetVilkår.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndelIkkeNull, + minimerteUtbetalingsperiodeDetaljer = listOf( + lagMinimertUtbetalingsperiodeDetalj( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + erPåvirketAvEndring = true, + ), + ), + ), + ) + + Assertions.assertFalse( + triggesAvSkalUtbetalesMedUtvidetVilkår.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndelIkkeNull, + minimerteUtbetalingsperiodeDetaljer = listOf( + lagMinimertUtbetalingsperiodeDetalj( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + erPåvirketAvEndring = false, + ), + ), + ), + ) + + Assertions.assertTrue( + triggesAvSkalUtbetalesUtenUtvidetVilkår.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndelIkkeNull, + minimerteUtbetalingsperiodeDetaljer = listOf( + lagMinimertUtbetalingsperiodeDetalj( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + erPåvirketAvEndring = false, + ), + ), + ), + ) + } + + @Test + fun `Skal ikke være oppfylt hvis endringsperiode og triggesav ulik årsak`() { + val endretUtbetalingAndel = lagMinimertEndretUtbetalingAndel( + prosent = BigDecimal.ZERO, + årsak = Årsak.ALLEREDE_UTBETALT, + ) + + Assertions.assertFalse( + triggesIkkeAvSkalUtbetalesUtenUtvidetVilkår.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndel, + minimerteUtbetalingsperiodeDetaljer = emptyList(), + ), + ) + + Assertions.assertFalse( + triggesIkkeAvSkalUtbetalesUtenUtvidetVilkår.erTriggereOppfyltForEndretUtbetaling( + minimertEndretAndel = endretUtbetalingAndel, + minimerteUtbetalingsperiodeDetaljer = listOf( + lagMinimertUtbetalingsperiodeDetalj( + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + ), + + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksbegrunnelseTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksbegrunnelseTest.kt new file mode 100644 index 000000000..d2a9797c3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksbegrunnelseTest.kt @@ -0,0 +1,100 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.domene + +import no.nav.familie.ba.sak.common.NullablePeriode +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.datagenerator.brev.lagBrevBegrunnelseGrunnlagMedPersoner +import no.nav.familie.ba.sak.datagenerator.vedtak.lagVedtaksbegrunnelse +import no.nav.familie.ba.sak.ekstern.restDomene.BarnMedOpplysninger +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertUregistrertBarn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.tilBrevTekst +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class VedtaksbegrunnelseTest { + val søker = lagPerson(type = PersonType.SØKER) + val barn1 = lagPerson(type = PersonType.BARN) + val barn2 = lagPerson(type = PersonType.BARN) + val barn3 = lagPerson(type = PersonType.BARN) + + val restVedtaksbegrunnelse = lagVedtaksbegrunnelse( + standardbegrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET, + ) + + val vedtaksperiode = NullablePeriode(LocalDate.now().minusMonths(1), LocalDate.now()) + + val personerIPersongrunnlag = listOf(søker, barn1, barn2, barn3).map { it.tilMinimertPerson() } + + val målform = Målform.NB + + val beløp = "1234" + + @Test + fun `skal ta med alle barnas fødselsdatoer ved avslag på søker, men ikke inkludere dem i antall barn`() { + val brevBegrunnelseGrunnlagMedPersoner = lagBrevBegrunnelseGrunnlagMedPersoner( + standardbegrunnelse = Standardbegrunnelse.AVSLAG_BOR_HOS_SØKER, + personIdenter = listOf(søker).map { it.aktør.aktivFødselsnummer() }, + vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG, + ) + + val brevbegrunnelse = brevBegrunnelseGrunnlagMedPersoner.tilBrevBegrunnelse( + vedtaksperiode = vedtaksperiode, + personerIPersongrunnlag = personerIPersongrunnlag, + brevMålform = målform, + uregistrerteBarn = emptyList(), + minimerteUtbetalingsperiodeDetaljer = emptyList(), + minimerteRestEndredeAndeler = emptyList(), + ) as BegrunnelseData + + Assertions.assertEquals(true, brevbegrunnelse.gjelderSoker) + Assertions.assertEquals( + listOf(barn1, barn2, barn3).map { it.fødselsdato }.tilBrevTekst(), + brevbegrunnelse.barnasFodselsdatoer, + ) + Assertions.assertEquals(0, brevbegrunnelse.antallBarn) + Assertions.assertEquals(målform.tilSanityFormat(), brevbegrunnelse.maalform) + Assertions.assertEquals(Utils.formaterBeløp(0), brevbegrunnelse.belop) + } + + @Test + fun `skal ta med uregistrerte barn`() { + val uregistrerteBarn = listOf( + lagPerson(type = PersonType.BARN), + lagPerson(type = PersonType.BARN), + ).map { + BarnMedOpplysninger( + ident = it.aktør.aktivFødselsnummer(), + fødselsdato = it.fødselsdato, + ).tilMinimertUregistrertBarn() + } + + val brevBegrunnelseGrunnlagMedPersoner = lagBrevBegrunnelseGrunnlagMedPersoner( + standardbegrunnelse = Standardbegrunnelse.AVSLAG_UREGISTRERT_BARN, + personIdenter = emptyList(), + vedtakBegrunnelseType = VedtakBegrunnelseType.AVSLAG, + ) + + val brevbegrunnelse = brevBegrunnelseGrunnlagMedPersoner.tilBrevBegrunnelse( + vedtaksperiode = vedtaksperiode, + personerIPersongrunnlag = personerIPersongrunnlag, + brevMålform = målform, + uregistrerteBarn = uregistrerteBarn, + minimerteUtbetalingsperiodeDetaljer = emptyList(), + minimerteRestEndredeAndeler = emptyList(), + ) as BegrunnelseData + + Assertions.assertEquals(false, brevbegrunnelse.gjelderSoker) + Assertions.assertEquals( + uregistrerteBarn.map { it.fødselsdato!! }.tilBrevTekst(), + brevbegrunnelse.barnasFodselsdatoer, + ) + Assertions.assertEquals(2, brevbegrunnelse.antallBarn) + Assertions.assertEquals(målform.tilSanityFormat(), brevbegrunnelse.maalform) + Assertions.assertEquals(Utils.formaterBeløp(0), brevbegrunnelse.belop) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Opph\303\270rsperiodeTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Opph\303\270rsperiodeTest.kt" new file mode 100644 index 000000000..57f0865b1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Opph\303\270rsperiodeTest.kt" @@ -0,0 +1,339 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.forrigeMåned +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.toLocalDate +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.YearMonth + +class OpphørsperiodeTest { + + val januar2023 = YearMonth.of(2023, 1) + val februar2023 = YearMonth.of(2023, 2) + val mars2023 = YearMonth.of(2023, 3) + val april2023 = YearMonth.of(2023, 4) + val mai2023 = YearMonth.of(2023, 5) + val mai2024 = YearMonth.of(2024, 5) + + val søker = tilfeldigPerson() + val barn1 = tilfeldigPerson() + val barn2 = tilfeldigPerson() + + val personopplysningGrunnlag = PersonopplysningGrunnlag( + behandlingId = 0L, + personer = mutableSetOf(søker, barn1, barn2), + ) + + @Test + fun `Skal utlede opphørsperiode mellom oppfylte perioder`() { + val periodeTomFørsteAndel = inneværendeMåned().minusYears(2) + val periodeFomAndreAndel = inneværendeMåned().minusYears(1) + val periodeTomAndreAndel = inneværendeMåned().minusMonths(10) + val periodeFomSisteAndel = inneværendeMåned().minusMonths(4) + val andelBarn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + inneværendeMåned().minusYears(4), + periodeTomFørsteAndel, + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val andel2Barn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + periodeFomAndreAndel, + periodeTomAndreAndel, + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val andel3Barn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + periodeFomSisteAndel, + inneværendeMåned().plusMonths(12), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val opphørsperioder = mapTilOpphørsperioder( + andelerTilkjentYtelse = listOf(andelBarn1, andel2Barn1, andel3Barn1), + personopplysningGrunnlag = personopplysningGrunnlag, + ) + + assertEquals(2, opphørsperioder.size) + assertEquals(periodeTomFørsteAndel.nesteMåned(), opphørsperioder[0].periodeFom.toYearMonth()) + assertEquals(periodeFomAndreAndel.forrigeMåned(), opphørsperioder[0].periodeTom?.toYearMonth()) + + assertEquals(periodeTomAndreAndel.nesteMåned(), opphørsperioder[1].periodeFom.toYearMonth()) + assertEquals(periodeFomSisteAndel.forrigeMåned(), opphørsperioder[1].periodeTom?.toYearMonth()) + } + + @Test + fun `Skal utlede opphørsperiode når siste utbetalingsperiode er før neste måned`() { + val periodeTomFørsteAndel = inneværendeMåned().minusYears(1) + val andelBarn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + inneværendeMåned().minusYears(4), + periodeTomFørsteAndel, + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val opphørsperioder = mapTilOpphørsperioder( + andelerTilkjentYtelse = listOf(andelBarn1), + personopplysningGrunnlag = personopplysningGrunnlag, + ) + + assertEquals(1, opphørsperioder.size) + assertEquals(periodeTomFørsteAndel.nesteMåned(), opphørsperioder[0].periodeFom.toYearMonth()) + assertEquals(null, opphørsperioder[0].periodeTom) + } + + @Test + fun `Skal utlede opphørsperiode fra neste måned når siste utbetalingsperiode er inneværende måned`() { + val periodeTomFørsteAndel = inneværendeMåned() + val andelBarn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + inneværendeMåned().minusYears(4), + periodeTomFørsteAndel, + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val opphørsperioder = mapTilOpphørsperioder( + andelerTilkjentYtelse = listOf(andelBarn1), + personopplysningGrunnlag = personopplysningGrunnlag, + ) + + assertEquals(1, opphørsperioder.size) + assertEquals(periodeTomFørsteAndel.nesteMåned(), opphørsperioder[0].periodeFom.toYearMonth()) + assertEquals(null, opphørsperioder[0].periodeTom) + } + + @Test + fun `Skal utlede opphørsperiode når ytelsen reduseres i revurdering`() { + val reduksjonFom = inneværendeMåned().minusYears(5) + val reduksjonTom = inneværendeMåned().minusYears(3) + val forrigeAndelBarn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + inneværendeMåned().minusYears(5), + inneværendeMåned().plusMonths(12), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val andelBarn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + reduksjonTom, + inneværendeMåned().plusMonths(12), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val opphørsperioder = mapTilOpphørsperioder( + forrigeAndelerTilkjentYtelse = listOf(forrigeAndelBarn1), + andelerTilkjentYtelse = listOf(andelBarn1), + personopplysningGrunnlag = personopplysningGrunnlag, + forrigePersonopplysningGrunnlag = personopplysningGrunnlag, + ) + + assertEquals(1, opphørsperioder.size) + assertEquals(reduksjonFom, opphørsperioder[0].periodeFom.toYearMonth()) + assertEquals(reduksjonTom.forrigeMåned(), opphørsperioder[0].periodeTom?.toYearMonth()) + } + + @Test + fun `Skal utlede opphørsperiode når ytelsen reduseres i revurdering og to inntilliggende perioder opphøres`() { + val forrigeAndel1Barn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + januar2023, + februar2023, + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val forrigeAndel2Barn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + mars2023, + april2023, + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val andelBarn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + mai2023, + mai2024, + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val opphørsperioder = mapTilOpphørsperioder( + forrigeAndelerTilkjentYtelse = listOf(forrigeAndel1Barn1, forrigeAndel2Barn1), + andelerTilkjentYtelse = listOf(andelBarn1), + personopplysningGrunnlag = personopplysningGrunnlag, + forrigePersonopplysningGrunnlag = personopplysningGrunnlag, + ) + + assertEquals(1, opphørsperioder.size) + assertEquals(januar2023, opphørsperioder[0].periodeFom.toYearMonth()) + assertEquals(april2023, opphørsperioder[0].periodeTom?.toYearMonth()) + } + + @Test + fun `Skal utlede opphørsperiode når ytelsen reduseres i revurdering og ytelsen ikke lenger er løpende`() { + val reduksjonFom = inneværendeMåned() + val forrigeAndelBarn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + inneværendeMåned().minusYears(5), + inneværendeMåned().plusMonths(12), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val andelBarn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + inneværendeMåned().minusYears(5), + reduksjonFom, + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val opphørsperioder = mapTilOpphørsperioder( + forrigeAndelerTilkjentYtelse = listOf(forrigeAndelBarn1), + andelerTilkjentYtelse = listOf(andelBarn1), + personopplysningGrunnlag = personopplysningGrunnlag, + forrigePersonopplysningGrunnlag = personopplysningGrunnlag, + ).run(::slåSammenOpphørsperioder) + + assertEquals(1, opphørsperioder.size) + assertEquals(reduksjonFom.nesteMåned(), opphørsperioder[0].periodeFom.toYearMonth()) + assertEquals(null, opphørsperioder[0].periodeTom?.toYearMonth()) + } + + @Test + fun `Skal slå sammen to like opphørsperioder`() { + val periode12MånederFraInneværendeMåned = inneværendeMåned().minusMonths(12).toLocalDate() + + val toLikePerioder = listOf( + Opphørsperiode( + periodeFom = periode12MånederFraInneværendeMåned, + periodeTom = inneværendeMåned().toLocalDate(), + ), + Opphørsperiode( + periodeFom = periode12MånederFraInneværendeMåned, + periodeTom = inneværendeMåned().toLocalDate(), + ), + ) + + assertEquals(1, slåSammenOpphørsperioder(toLikePerioder).size) + } + + @Test + fun `Skal slå sammen to opphørsperioder med ulik sluttdato`() { + val toPerioderMedUlikSluttdato = listOf( + Opphørsperiode( + periodeFom = inneværendeMåned().minusMonths(12).toLocalDate(), + periodeTom = inneværendeMåned().toLocalDate(), + ), + Opphørsperiode( + periodeFom = inneværendeMåned().minusMonths(12).toLocalDate(), + periodeTom = inneværendeMåned().nesteMåned().toLocalDate(), + ), + ) + val enPeriodeMedSluttDatoNesteMåned = slåSammenOpphørsperioder(toPerioderMedUlikSluttdato) + + assertEquals(1, enPeriodeMedSluttDatoNesteMåned.size) + assertEquals(inneværendeMåned().nesteMåned().toLocalDate(), enPeriodeMedSluttDatoNesteMåned.first().periodeTom) + } + + @Test + fun `Skal slå sammen to opphørsperioder med ulik startdato`() { + val toPerioderMedUlikStartdato = listOf( + Opphørsperiode( + periodeFom = inneværendeMåned().minusMonths(12).toLocalDate(), + periodeTom = inneværendeMåned().toLocalDate(), + ), + Opphørsperiode( + periodeFom = inneværendeMåned().minusMonths(13).toLocalDate(), + periodeTom = inneværendeMåned().toLocalDate(), + ), + ) + val enPeriodeMedStartDato13MånederTilbake = slåSammenOpphørsperioder(toPerioderMedUlikStartdato) + + assertEquals(1, enPeriodeMedStartDato13MånederTilbake.size) + assertEquals( + inneværendeMåned().minusMonths(13).toLocalDate(), + enPeriodeMedStartDato13MånederTilbake.first().periodeFom, + ) + } + + @Test + fun `Skal slå sammen to opphørsperioder som overlapper`() { + val førsteOpphørsperiodeFom = inneværendeMåned().minusMonths(12).toLocalDate() + val sisteOpphørsperiodeTom = inneværendeMåned().plusMonths(1).toLocalDate() + val toPerioderMedUlikStartdato = listOf( + Opphørsperiode( + periodeFom = førsteOpphørsperiodeFom, + periodeTom = inneværendeMåned().minusMonths(2).toLocalDate(), + ), + Opphørsperiode( + periodeFom = inneværendeMåned().minusMonths(6).toLocalDate(), + periodeTom = sisteOpphørsperiodeTom, + ), + ) + val enOpphørsperiodeMedFørsteFomOgSisteTom = slåSammenOpphørsperioder(toPerioderMedUlikStartdato) + + assertEquals(1, enOpphørsperiodeMedFørsteFomOgSisteTom.size) + assertEquals(førsteOpphørsperiodeFom, enOpphørsperiodeMedFørsteFomOgSisteTom.first().periodeFom) + assertEquals(sisteOpphørsperiodeTom, enOpphørsperiodeMedFørsteFomOgSisteTom.first().periodeTom) + } + + @Test + fun `Skal håndtere at det ikke er noen andeler i denne behandlingen`() { + val forrigeAndel1Barn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + januar2023, + februar2023, + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val forrigeAndel2Barn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + mars2023, + april2023, + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + person = barn1, + ) + + val opphørsperioder = mapTilOpphørsperioder( + forrigeAndelerTilkjentYtelse = listOf(forrigeAndel1Barn1, forrigeAndel2Barn1), + andelerTilkjentYtelse = emptyList(), + personopplysningGrunnlag = personopplysningGrunnlag, + forrigePersonopplysningGrunnlag = personopplysningGrunnlag, + ) + + assertEquals(1, opphørsperioder.size) + assertEquals(januar2023, opphørsperioder[0].periodeFom.toYearMonth()) + assertEquals(april2023, opphørsperioder[0].periodeTom?.toYearMonth()) + } + + @Test + fun `Skal håndtere at det ikke er noen andeler i denne eller forrige behandling`() { + val opphørsperioder = mapTilOpphørsperioder( + forrigeAndelerTilkjentYtelse = emptyList(), + andelerTilkjentYtelse = emptyList(), + personopplysningGrunnlag = personopplysningGrunnlag, + forrigePersonopplysningGrunnlag = personopplysningGrunnlag, + ) + + assertEquals(0, opphørsperioder.size) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeMedBegrunnelser/UtbetalingsperiodeMedBegrunnelserUtilsTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeMedBegrunnelser/UtbetalingsperiodeMedBegrunnelserUtilsTest.kt new file mode 100644 index 000000000..752c05e01 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeMedBegrunnelser/UtbetalingsperiodeMedBegrunnelserUtilsTest.kt @@ -0,0 +1,131 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.utbetalingsperiodemedbegrunnelser + +import no.nav.familie.ba.sak.common.MånedPeriode +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.lagKompetanse +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.YearMonth + +class UtbetalingsperiodeMedBegrunnelserUtilsTest { + + val barnAktør1 = Aktør(aktørId = "1111111111111") + val barnAktør2 = Aktør(aktørId = "2222222222222") + + @Test + fun `skal splitte opp utbetalingsperioder når det er overlappende kompetanse`() { + val periode1 = MånedPeriode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)) + val periode2 = MånedPeriode(YearMonth.of(2021, 2), YearMonth.of(2021, 3)) + val periode3 = MånedPeriode(YearMonth.of(2021, 5), YearMonth.of(2021, 5)) + val periode4 = MånedPeriode(YearMonth.of(2021, 6), YearMonth.of(2021, 7)) + val periode5 = MånedPeriode(YearMonth.of(2021, 8), YearMonth.of(2021, 10)) + + val utbetalingsperiode1 = lagVedtaksperiodeMedBegrunnelser( + type = Vedtaksperiodetype.UTBETALING, + fom = periode1.fom.førsteDagIInneværendeMåned(), + tom = periode2.tom.sisteDagIInneværendeMåned(), + ) + + val kompetanse1 = lagKompetanse(fom = periode2.fom, tom = periode2.tom, barnAktører = setOf(barnAktør1)) + + val utbetalingsperiode2 = lagVedtaksperiodeMedBegrunnelser( + type = Vedtaksperiodetype.UTBETALING, + fom = periode3.fom.førsteDagIInneværendeMåned(), + tom = periode4.tom.sisteDagIInneværendeMåned(), + ) + + val kompetanse2 = lagKompetanse(fom = periode4.fom, tom = periode5.tom, barnAktører = setOf(barnAktør1)) + + val utbetalingsperiodeMedReduksjonFraSistIverksatteBehandling = lagVedtaksperiodeMedBegrunnelser( + type = Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + fom = periode5.fom.førsteDagIInneværendeMåned(), + tom = periode5.tom.sisteDagIInneværendeMåned(), + ) + + val utbetalingsperiodeMedKompetanseSplitter = splittUtbetalingsperioderPåKompetanser( + utbetalingsperioder = listOf( + utbetalingsperiode1, + utbetalingsperiode2, + utbetalingsperiodeMedReduksjonFraSistIverksatteBehandling, + ), + kompetanser = listOf(kompetanse1, kompetanse2), + ) + + val forventedePerioder = + listOf(periode1, periode2, periode3, periode4, periode5) + + Assertions.assertEquals(5, utbetalingsperiodeMedKompetanseSplitter.size) + utbetalingsperiodeMedKompetanseSplitter + .zip(forventedePerioder) + .forEach { (utbetalingsperiode, forventetPeriode) -> + Assertions.assertEquals(forventetPeriode.fom.førsteDagIInneværendeMåned(), utbetalingsperiode.fom) + Assertions.assertEquals(forventetPeriode.tom.sisteDagIInneværendeMåned(), utbetalingsperiode.tom) + } + } + + @Test + fun `Skal splitte opp utbetalingsperioder riktig når kompetanse strekker seg over flere perioder`() { + val utbetalingsperioder = listOf( + MånedPeriode(YearMonth.of(2020, 5), YearMonth.of(2020, 8)), + MånedPeriode(YearMonth.of(2020, 9), YearMonth.of(2021, 4)), + MånedPeriode(YearMonth.of(2021, 5), YearMonth.of(2021, 8)), + MånedPeriode(YearMonth.of(2021, 9), YearMonth.of(2021, 12)), + MånedPeriode(YearMonth.of(2022, 1), YearMonth.of(2022, 3)), + MånedPeriode(YearMonth.of(2022, 4), YearMonth.of(2038, 3)), + ).mapIndexed { _, it -> + lagVedtaksperiodeMedBegrunnelser( + type = Vedtaksperiodetype.UTBETALING, + fom = it.fom.førsteDagIInneværendeMåned(), + tom = it.tom.sisteDagIInneværendeMåned(), + ) + } + + val kompetanse = + lagKompetanse(fom = YearMonth.of(2020, 5), tom = YearMonth.of(2021, 5), barnAktører = setOf(barnAktør1)) + + val utbetalingsperiodeMedKompetanseSplitter = splittUtbetalingsperioderPåKompetanser( + utbetalingsperioder = utbetalingsperioder, + kompetanser = listOf(kompetanse), + ) + + Assertions.assertEquals(7, utbetalingsperiodeMedKompetanseSplitter.size) + } + + @Test + fun `Skal splitte opp utbetalingsperiodene riktig når det er kompetanse på forskjellige personer`() { + val periode1 = MånedPeriode(YearMonth.of(2021, 1), YearMonth.of(2021, 4)) + val periode2 = MånedPeriode(YearMonth.of(2021, 5), YearMonth.of(2021, 6)) + val periode3 = MånedPeriode(YearMonth.of(2021, 7), YearMonth.of(2022, 1)) + + val utbetalingsperiode1 = lagVedtaksperiodeMedBegrunnelser( + type = Vedtaksperiodetype.UTBETALING, + fom = periode1.fom.førsteDagIInneværendeMåned(), + tom = periode3.tom.sisteDagIInneværendeMåned(), + ) + + val kompetanse1 = + lagKompetanse(fom = periode1.fom, tom = periode2.tom, barnAktører = setOf(barnAktør1)) + val kompetanse2 = + lagKompetanse(fom = periode2.fom, tom = periode3.tom, barnAktører = setOf(barnAktør2)) + + val forventedePerioder = + listOf(periode1, periode2, periode3) + + val utbetalingsperiodeMedKompetanseSplitter = splittUtbetalingsperioderPåKompetanser( + utbetalingsperioder = listOf(utbetalingsperiode1), + kompetanser = listOf(kompetanse1, kompetanse2), + ) + + Assertions.assertEquals(3, utbetalingsperiodeMedKompetanseSplitter.size) + utbetalingsperiodeMedKompetanseSplitter + .zip(forventedePerioder) + .forEach { (utbetalingsperiode, forventetPeriode) -> + Assertions.assertEquals(forventetPeriode.fom.førsteDagIInneværendeMåned(), utbetalingsperiode.fom) + Assertions.assertEquals(forventetPeriode.tom.sisteDagIInneværendeMåned(), utbetalingsperiode.tom) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeMedBegrunnelser/UtbetalingsperiodeUtilTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeMedBegrunnelser/UtbetalingsperiodeUtilTest.kt new file mode 100644 index 000000000..eb06c6760 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeMedBegrunnelser/UtbetalingsperiodeUtilTest.kt @@ -0,0 +1,496 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.utbetalingsperiodemedbegrunnelser + +import hentPerioderMedUtbetaling +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.TIDENES_ENDE +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.lagVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.til18ÅrsVilkårsdato +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.YearMonth + +class UtbetalingsperiodeUtilTest { + + @Test + fun `Skal beholde split i andel tilkjent ytelse`() { + val mars2020 = YearMonth.of(2020, 3) + val april2020 = YearMonth.of(2020, 4) + val mai2020 = YearMonth.of(2020, 5) + val juli2020 = YearMonth.of(2020, 7) + + val person1 = lagPerson() + val person2 = lagPerson() + + val vedtak = lagVedtak() + + val andelPerson1MarsTilApril = lagAndelTilkjentYtelse( + fom = mars2020, + tom = april2020, + beløp = 1000, + person = person1, + ) + + val andelPerson1MaiTilJuli = lagAndelTilkjentYtelse( + fom = mai2020, + tom = juli2020, + beløp = 1000, + person = person1, + ) + + val andelPerson2MarsTilJuli = lagAndelTilkjentYtelse( + fom = mars2020, + tom = juli2020, + beløp = 1000, + person = person2, + ) + + val forventetResultat = listOf( + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = mars2020.førsteDagIInneværendeMåned(), + tom = april2020.sisteDagIInneværendeMåned(), + type = Vedtaksperiodetype.UTBETALING, + begrunnelser = mutableSetOf(), + ), + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = mai2020.førsteDagIInneværendeMåned(), + tom = juli2020.sisteDagIInneværendeMåned(), + type = Vedtaksperiodetype.UTBETALING, + begrunnelser = mutableSetOf(), + ), + ) + + val vilkårsvurdering = Vilkårsvurdering(behandling = lagBehandling()) + val personResultater = setOf( + vilkårsvurdering.lagGodkjentPersonResultatForBarn(person1), + vilkårsvurdering.lagGodkjentPersonResultatForBarn(person2), + ) + + val faktiskResultat = hentPerioderMedUtbetaling( + andelerTilkjentYtelse = listOf(andelPerson1MarsTilApril, andelPerson1MaiTilJuli, andelPerson2MarsTilJuli), + vedtak = vedtak, + personResultater = personResultater, + personerIPersongrunnlag = listOf(person1, person2), + fagsakType = FagsakType.NORMAL, + ) + + Assertions.assertEquals( + forventetResultat.map { Periode(it.fom ?: TIDENES_MORGEN, it.tom ?: TIDENES_ENDE) }, + faktiskResultat.map { Periode(it.fom ?: TIDENES_MORGEN, it.tom ?: TIDENES_ENDE) }, + ) + + Assertions.assertEquals( + forventetResultat.map { it.type }.toSet(), + faktiskResultat.map { it.type }.toSet(), + ) + } + + @Test + fun `Skal splitte på forskjellige personer`() { + val mars2020 = YearMonth.of(2020, 3) + val april2020 = YearMonth.of(2020, 4) + val mai2020 = YearMonth.of(2020, 5) + val juni2020 = YearMonth.of(2020, 6) + val juli2020 = YearMonth.of(2020, 7) + + val person1 = lagPerson(type = PersonType.BARN) + val person2 = lagPerson(type = PersonType.BARN) + + val vedtak = lagVedtak() + + val andelPerson1MarsTilMai = lagAndelTilkjentYtelse( + fom = mars2020, + tom = mai2020, + beløp = 1000, + person = person1, + ) + + val andelPerson2MaiTilJuli = lagAndelTilkjentYtelse( + fom = mai2020, + tom = juli2020, + beløp = 1000, + person = person2, + ) + + val forventetResultat = listOf( + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = mars2020.førsteDagIInneværendeMåned(), + tom = april2020.sisteDagIInneværendeMåned(), + type = Vedtaksperiodetype.UTBETALING, + begrunnelser = mutableSetOf(), + ), + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = mai2020.førsteDagIInneværendeMåned(), + tom = mai2020.sisteDagIInneværendeMåned(), + type = Vedtaksperiodetype.UTBETALING, + begrunnelser = mutableSetOf(), + ), + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = juni2020.førsteDagIInneværendeMåned(), + tom = juli2020.sisteDagIInneværendeMåned(), + type = Vedtaksperiodetype.UTBETALING, + begrunnelser = mutableSetOf(), + ), + ) + + val vilkårsvurdering = Vilkårsvurdering(behandling = lagBehandling()) + val personResultater = setOf( + vilkårsvurdering.lagGodkjentPersonResultatForBarn(person1), + vilkårsvurdering.lagGodkjentPersonResultatForBarn(person2), + ) + + val faktiskResultat = hentPerioderMedUtbetaling( + andelerTilkjentYtelse = listOf(andelPerson1MarsTilMai, andelPerson2MaiTilJuli), + vedtak = vedtak, + personResultater = personResultater, + personerIPersongrunnlag = listOf(person1, person2), + fagsakType = FagsakType.NORMAL, + ) + + Assertions.assertEquals( + forventetResultat.map { Periode(it.fom ?: TIDENES_MORGEN, it.tom ?: TIDENES_ENDE) }, + faktiskResultat.map { Periode(it.fom ?: TIDENES_MORGEN, it.tom ?: TIDENES_ENDE) }, + ) + + Assertions.assertEquals( + forventetResultat.map { it.type }.toSet(), + faktiskResultat.map { it.type }.toSet(), + ) + } + + @Test + fun `Skal splitte på utdypende vilkårsvurdering når det flytter seg fra ett barn til et annet`() { + val mars2020 = YearMonth.of(2020, 3) + val april2020 = YearMonth.of(2020, 4) + val mai2020 = YearMonth.of(2020, 5) + val juli2020 = YearMonth.of(2020, 7) + + val søker = lagPerson(type = PersonType.SØKER) + val barn1 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(16)) + val barn2 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(7)) + + val vedtak = lagVedtak() + + val andelBarn1MarsTilJuli = lagAndelTilkjentYtelse( + fom = mars2020, + tom = juli2020, + beløp = 1000, + person = barn1, + ) + + val andelBarn2MarsTilJuli = lagAndelTilkjentYtelse( + fom = mars2020, + tom = juli2020, + beløp = 2000, + person = barn2, + ) + + val vilkårsvurdering = lagVilkårsvurdering( + søkerAktør = søker.aktør, + behandling = lagBehandling(), + resultat = Resultat.OPPFYLT, + ) + + val personResultatBarn1 = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn1.aktør, + ) + + val vilkårResultatBorMedSøkerMedUtdypendeVilkårsvurderingBarn1 = VilkårResultat( + personResultat = personResultatBarn1, + periodeFom = mars2020.minusMonths(1).førsteDagIInneværendeMåned(), + periodeTom = april2020.sisteDagIInneværendeMåned(), + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.BARN_BOR_I_STORBRITANNIA_MED_SØKER), + ) + val vilkårResultatBorMedSøkerUtenUtdypendeVilkårsvurderingBarn1 = VilkårResultat( + personResultat = personResultatBarn1, + periodeFom = mai2020.førsteDagIInneværendeMåned(), + periodeTom = juli2020.sisteDagIInneværendeMåned(), + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ) + + val resterendeVilkårForBarn = Vilkår.hentVilkårFor(PersonType.BARN, fagsakType = FagsakType.NORMAL, behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR).mapNotNull { + if (it == Vilkår.BOR_MED_SØKER) null else lagVilkårResultat(vilkår = it, fom = mars2020.minusMonths(1), tom = juli2020) + } + + val vilkårResultaterBarn1 = listOf( + vilkårResultatBorMedSøkerMedUtdypendeVilkårsvurderingBarn1, + vilkårResultatBorMedSøkerUtenUtdypendeVilkårsvurderingBarn1, + ) + resterendeVilkårForBarn + + personResultatBarn1.setSortedVilkårResultater( + vilkårResultaterBarn1.toSet(), + ) + + val personResultatBarn2 = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn2.aktør, + ) + + val vilkårResultatBorMedSøkerMedUtdypendeVilkårsvurderingBarn2 = VilkårResultat( + personResultat = personResultatBarn2, + periodeFom = mars2020.minusMonths(1).førsteDagIInneværendeMåned(), + periodeTom = april2020.sisteDagIInneværendeMåned(), + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + ) + val vilkårResultatBorMedSøkerUtenUtdypendeVilkårsvurderingBarn2 = VilkårResultat( + personResultat = personResultatBarn2, + periodeFom = mai2020.førsteDagIInneværendeMåned(), + periodeTom = juli2020.sisteDagIInneværendeMåned(), + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.BARN_BOR_I_STORBRITANNIA_MED_SØKER), + ) + + val vilkårResultaterBarn2 = listOf( + vilkårResultatBorMedSøkerMedUtdypendeVilkårsvurderingBarn2, + vilkårResultatBorMedSøkerUtenUtdypendeVilkårsvurderingBarn2, + ) + resterendeVilkårForBarn + + personResultatBarn2.setSortedVilkårResultater( + vilkårResultaterBarn2.toSet(), + ) + + val forventetResultat = listOf( + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = mars2020.førsteDagIInneværendeMåned(), + tom = april2020.sisteDagIInneværendeMåned(), + type = Vedtaksperiodetype.UTBETALING, + begrunnelser = mutableSetOf(), + ), + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = mai2020.førsteDagIInneværendeMåned(), + tom = juli2020.sisteDagIInneværendeMåned(), + type = Vedtaksperiodetype.UTBETALING, + begrunnelser = mutableSetOf(), + ), + ) + + val faktiskResultat = hentPerioderMedUtbetaling( + andelerTilkjentYtelse = listOf(andelBarn1MarsTilJuli, andelBarn2MarsTilJuli), + vedtak = vedtak, + personResultater = setOf(personResultatBarn1, personResultatBarn2), + personerIPersongrunnlag = listOf(søker, barn1, barn2), + fagsakType = FagsakType.NORMAL, + ) + + Assertions.assertEquals( + forventetResultat.map { Periode(it.fom ?: TIDENES_MORGEN, it.tom ?: TIDENES_ENDE) }, + faktiskResultat.map { Periode(it.fom ?: TIDENES_MORGEN, it.tom ?: TIDENES_ENDE) }, + ) + + Assertions.assertEquals( + forventetResultat.map { it.type }.toSet(), + faktiskResultat.map { it.type }.toSet(), + ) + } + + @Test + fun `Skal få med opphør i andel tilkjent ytelse`() { + val mars2020 = YearMonth.of(2020, 3) + val april2020 = YearMonth.of(2020, 4) + val juli2020 = YearMonth.of(2020, 7) + val barn1 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(9)) + val vedtak = lagVedtak() + + val andelBarn1MarsTilApril = lagAndelTilkjentYtelse( + fom = mars2020, + tom = april2020, + beløp = 1000, + person = barn1, + ) + val andelBarn1JuliTilJuli = lagAndelTilkjentYtelse( + fom = juli2020, + tom = juli2020, + beløp = 1000, + person = barn1, + ) + + val faktiskResultat = hentPerioderMedUtbetaling( + andelerTilkjentYtelse = listOf(andelBarn1MarsTilApril, andelBarn1JuliTilJuli), + vedtak = vedtak, + personResultater = emptySet(), + personerIPersongrunnlag = listOf(barn1), + fagsakType = FagsakType.NORMAL, + ) + + val forventetResultat = listOf( + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = mars2020.førsteDagIInneværendeMåned(), + tom = april2020.sisteDagIInneværendeMåned(), + type = Vedtaksperiodetype.UTBETALING, + begrunnelser = mutableSetOf(), + ), + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = juli2020.førsteDagIInneværendeMåned(), + tom = juli2020.sisteDagIInneværendeMåned(), + type = Vedtaksperiodetype.UTBETALING, + begrunnelser = mutableSetOf(), + ), + ) + + Assertions.assertEquals( + forventetResultat.map { Periode(it.fom ?: TIDENES_MORGEN, it.tom ?: TIDENES_ENDE) }, + faktiskResultat.map { Periode(it.fom ?: TIDENES_MORGEN, it.tom ?: TIDENES_ENDE) }, + ) + + Assertions.assertEquals( + forventetResultat.map { it.type }.toSet(), + faktiskResultat.map { it.type }.toSet(), + ) + } + + @Test + fun `Skal lage splitt i vedtaksperioder med der ulikt regelverk er brukt`() { + val mars2020 = YearMonth.of(2020, 3) + val april2020 = YearMonth.of(2020, 4) + val mai2020 = YearMonth.of(2020, 5) + val juli2020 = YearMonth.of(2020, 7) + + val søker = lagPerson(type = PersonType.SØKER) + val barn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.now().minusYears(8)) + + val vedtak = lagVedtak() + + val andelBarnMarsTilJuli = lagAndelTilkjentYtelse( + fom = mars2020, + tom = juli2020, + beløp = 1000, + person = barn, + ) + + val vilkårsvurdering = lagVilkårsvurdering( + søkerAktør = søker.aktør, + behandling = lagBehandling(), + resultat = Resultat.OPPFYLT, + ) + + val personResultatBarn = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn.aktør, + ) + + fun nasjonaltVilkår(vilkårType: Vilkår): VilkårResultat = VilkårResultat( + personResultat = personResultatBarn, + periodeFom = mars2020.minusMonths(1).førsteDagIInneværendeMåned(), + periodeTom = april2020.sisteDagIInneværendeMåned(), + vilkårType = vilkårType, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + vurderesEtter = Regelverk.NASJONALE_REGLER, + ) + + fun eøsVilkår(vilkårType: Vilkår): VilkårResultat = + VilkårResultat( + personResultat = personResultatBarn, + periodeFom = mai2020.førsteDagIInneværendeMåned(), + periodeTom = juli2020.sisteDagIInneværendeMåned(), + vilkårType = vilkårType, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + utdypendeVilkårsvurderinger = emptyList(), + vurderesEtter = Regelverk.EØS_FORORDNINGEN, + ) + + val vilkårForBarn = Vilkår.hentVilkårFor(personType = PersonType.BARN, fagsakType = FagsakType.NORMAL, behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR) + + val vilkårResultaterBarn1 = vilkårForBarn.map { nasjonaltVilkår(it) } + vilkårForBarn.map { eøsVilkår(it) } + + personResultatBarn.setSortedVilkårResultater( + vilkårResultaterBarn1.toSet(), + ) + + val forventetResultat = listOf( + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = mars2020.førsteDagIInneværendeMåned(), + tom = april2020.sisteDagIInneværendeMåned(), + type = Vedtaksperiodetype.UTBETALING, + begrunnelser = mutableSetOf(), + ), + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = mai2020.førsteDagIInneværendeMåned(), + tom = juli2020.sisteDagIInneværendeMåned(), + type = Vedtaksperiodetype.UTBETALING, + begrunnelser = mutableSetOf(), + ), + ) + + val faktiskResultat = hentPerioderMedUtbetaling( + andelerTilkjentYtelse = listOf(andelBarnMarsTilJuli), + vedtak = vedtak, + personResultater = setOf(personResultatBarn), + personerIPersongrunnlag = listOf(søker, barn), + fagsakType = FagsakType.NORMAL, + ) + + Assertions.assertEquals( + forventetResultat.map { Periode(it.fom ?: TIDENES_MORGEN, it.tom ?: TIDENES_ENDE) }, + faktiskResultat.map { Periode(it.fom ?: TIDENES_MORGEN, it.tom ?: TIDENES_ENDE) }, + ) + + Assertions.assertEquals( + forventetResultat.map { it.type }.toSet(), + faktiskResultat.map { it.type }.toSet(), + ) + } + + private fun Vilkårsvurdering.lagGodkjentPersonResultatForBarn(person: Person) = lagPersonResultat( + vilkårsvurdering = this, + person = person, + resultat = Resultat.OPPFYLT, + periodeFom = person.fødselsdato, + periodeTom = person.fødselsdato.til18ÅrsVilkårsdato(), + lagFullstendigVilkårResultat = true, + personType = person.type, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeTest.kt new file mode 100644 index 000000000..2353ceab0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtbetalingsperiodeTest.kt @@ -0,0 +1,77 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.lagUtbetalingsperiode +import no.nav.familie.ba.sak.common.lagUtbetalingsperiodeDetalj +import no.nav.familie.ba.sak.common.tilfeldigSøker +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPerson +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class UtbetalingsperiodeTest { + + val søker = tilfeldigSøker() + val fomDato1 = LocalDate.now().minusMonths(2).withDayOfMonth(1) + val fomDato2 = LocalDate.now().minusMonths(1).withDayOfMonth(1) + val fomDato3 = LocalDate.now().withDayOfMonth(1) + val utbetalingsperiode1 = lagUtbetalingsperiode( + periodeFom = fomDato1, + periodeTom = fomDato1.let { it.withDayOfMonth(it.lengthOfMonth()) }, + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj(person = søker.tilRestPerson())), + ) + val utbetalingsperiode2 = lagUtbetalingsperiode( + periodeFom = fomDato2, + periodeTom = fomDato2.let { it.withDayOfMonth(it.lengthOfMonth()) }, + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj(person = søker.tilRestPerson())), + ) + val utbetalingsperiode3 = lagUtbetalingsperiode( + periodeFom = fomDato3, + periodeTom = fomDato3.let { it.withDayOfMonth(it.lengthOfMonth()) }, + utbetalingsperiodeDetaljer = listOf(lagUtbetalingsperiodeDetalj(person = søker.tilRestPerson())), + ) + val utbetalingsperioder = listOf( + utbetalingsperiode1, + utbetalingsperiode2, + utbetalingsperiode3, + ) + + @Test + fun `Skal gi riktig siste utbetalingsperiode som er tidligere eller samme måned som inneværende måned`() { + val utbetalingsperiodeForVedtaksperiode = + hentUtbetalingsperiodeForVedtaksperiode( + utbetalingsperioder, + fomDato2, + ) + + Assertions.assertEquals(utbetalingsperiode2.periodeFom, utbetalingsperiodeForVedtaksperiode.periodeFom) + Assertions.assertEquals(utbetalingsperiode2.periodeTom, utbetalingsperiodeForVedtaksperiode.periodeTom) + Assertions.assertEquals(utbetalingsperiode2.vedtaksperiodetype, utbetalingsperiodeForVedtaksperiode.vedtaksperiodetype) + Assertions.assertEquals( + utbetalingsperiode2.utbetalingsperiodeDetaljer, + utbetalingsperiodeForVedtaksperiode.utbetalingsperiodeDetaljer, + ) + Assertions.assertEquals(utbetalingsperiode2.ytelseTyper, utbetalingsperiodeForVedtaksperiode.ytelseTyper) + Assertions.assertEquals(utbetalingsperiode2.antallBarn, utbetalingsperiodeForVedtaksperiode.antallBarn) + Assertions.assertEquals(utbetalingsperiode2.utbetaltPerMnd, utbetalingsperiodeForVedtaksperiode.utbetaltPerMnd) + } + + @Test + fun `Skal gi utbetalingsperiode i inneværende måned dersom fom er null`() { + val utbetalingsperiodeForVedtaksperiode = + hentUtbetalingsperiodeForVedtaksperiode( + utbetalingsperioder, + null, + ) + + Assertions.assertEquals(utbetalingsperiode3.periodeFom, utbetalingsperiodeForVedtaksperiode.periodeFom) + Assertions.assertEquals(utbetalingsperiode3.periodeTom, utbetalingsperiodeForVedtaksperiode.periodeTom) + Assertions.assertEquals(utbetalingsperiode3.vedtaksperiodetype, utbetalingsperiodeForVedtaksperiode.vedtaksperiodetype) + Assertions.assertEquals( + utbetalingsperiode3.utbetalingsperiodeDetaljer, + utbetalingsperiodeForVedtaksperiode.utbetalingsperiodeDetaljer, + ) + Assertions.assertEquals(utbetalingsperiode3.ytelseTyper, utbetalingsperiodeForVedtaksperiode.ytelseTyper) + Assertions.assertEquals(utbetalingsperiode3.antallBarn, utbetalingsperiodeForVedtaksperiode.antallBarn) + Assertions.assertEquals(utbetalingsperiode3.utbetaltPerMnd, utbetalingsperiodeForVedtaksperiode.utbetaltPerMnd) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtvidetVedtaksperiodeMedBegrunnelserTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtvidetVedtaksperiodeMedBegrunnelserTest.kt new file mode 100644 index 000000000..4ff1ae10c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/UtvidetVedtaksperiodeMedBegrunnelserTest.kt @@ -0,0 +1,91 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndel +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.tilfeldigSøker +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPerson +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.tilUtvidetVedtaksperiodeMedBegrunnelser +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.time.YearMonth + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class UtvidetVedtaksperiodeMedBegrunnelserTest { + + val barn1 = tilfeldigPerson(personType = PersonType.BARN) + val barn2 = tilfeldigPerson(personType = PersonType.BARN) + val barn3 = tilfeldigPerson(personType = PersonType.BARN) + val søker = tilfeldigSøker() + + @Test + fun `Skal kun legge på utbetalingsdetaljer som gjelder riktig andeler tilkjent ytelse for fortsatt innvilget`() { + val behandling = lagBehandling() + + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + barnasIdenter = listOf(barn1.aktør.aktivFødselsnummer(), barn2.aktør.aktivFødselsnummer()), + søkerPersonIdent = søker.aktør.aktivFødselsnummer(), + søkerAktør = søker.aktør, + barnAktør = listOf(barn1.aktør, barn2.aktør), + ) + + val fom = YearMonth.of(2018, 6) + val tom = YearMonth.of(2018, 8) + + val endretUtbetalingAndel = lagEndretUtbetalingAndel( + behandlingId = behandling.id, + fom = fom, + tom = tom, + person = barn2, + ) + + val andelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + behandling = behandling, + endretUtbetalingAndeler = emptyList(), + fom = fom.minusMonths(2), + tom = tom, + person = barn1, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + behandling = behandling, + endretUtbetalingAndeler = listOf(endretUtbetalingAndel), + fom = fom, + tom = tom, + person = barn2, + ), + lagAndelTilkjentYtelseMedEndreteUtbetalinger( + behandling = behandling, + endretUtbetalingAndeler = emptyList(), + fom = tom.plusMonths(1), + tom = tom.plusMonths(3), + person = barn1, + ), + ) + + val vedtaksperiodeMedBegrunnelser = lagVedtaksperiodeMedBegrunnelser( + fom = null, + tom = null, + type = Vedtaksperiodetype.FORTSATT_INNVILGET, + ) + + val utvidetVedtaksperiodeMedBegrunnelser = + vedtaksperiodeMedBegrunnelser.tilUtvidetVedtaksperiodeMedBegrunnelser( + personopplysningGrunnlag = personopplysningGrunnlag, + andelerTilkjentYtelse = andelerTilkjentYtelse, + ) + + Assertions.assertEquals(1, utvidetVedtaksperiodeMedBegrunnelser.utbetalingsperiodeDetaljer.size) + Assertions.assertEquals( + barn1.tilRestPerson().personIdent, + utvidetVedtaksperiodeMedBegrunnelser.utbetalingsperiodeDetaljer.single().person.personIdent, + ) + Assertions.assertFalse(utvidetVedtaksperiodeMedBegrunnelser.utbetalingsperiodeDetaljer.single().erPåvirketAvEndring) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelseTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelseTest.kt new file mode 100644 index 000000000..b8609a8b6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelseTest.kt @@ -0,0 +1,210 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.Vedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.domene.erAlleredeBegrunnetMedBegrunnelse +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.YearMonth + +class VedtaksperiodeMedBegrunnelseTest { + + @Test + fun `Skal finne begrunnelse på tidligere vedtaksperiode for samme fra- og med dato`() { + val fom = YearMonth.now().minusMonths(3) + val vedtak = lagVedtak() + + val vedtaksperioder = listOf( + VedtaksperiodeMedBegrunnelser( + fom = fom.førsteDagIInneværendeMåned(), + vedtak = vedtak, + type = Vedtaksperiodetype.UTBETALING, + ).apply { + begrunnelser.addAll( + listOf( + Standardbegrunnelse.INNVILGET_FØDSELSHENDELSE_NYFØDT_BARN, + Standardbegrunnelse.REDUKSJON_UNDER_6_ÅR, + ).map { begrunnelse -> + Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = this, + standardbegrunnelse = begrunnelse, + ) + }, + ) + }, + ) + + val fagsakErBegrunnet = vedtaksperioder.erAlleredeBegrunnetMedBegrunnelse( + standardbegrunnelser = listOf( + Standardbegrunnelse.REDUKSJON_UNDER_6_ÅR, + Standardbegrunnelse.REDUKSJON_UNDER_6_ÅR_AUTOVEDTAK, + ), + måned = fom, + ) + + assertTrue(fagsakErBegrunnet) + } + + @Test + fun `Skal ikke finne begrunnelse på tidligere vedtaksperiode når den er begrunnet for 1 år siden`() { + val fom = YearMonth.now().minusMonths(3) + val vedtak = lagVedtak() + + val vedtaksperioder = listOf( + VedtaksperiodeMedBegrunnelser( + fom = fom.minusYears(1).førsteDagIInneværendeMåned(), + vedtak = vedtak, + type = Vedtaksperiodetype.UTBETALING, + ).apply { + begrunnelser.addAll( + listOf( + Standardbegrunnelse.INNVILGET_FØDSELSHENDELSE_NYFØDT_BARN, + Standardbegrunnelse.REDUKSJON_UNDER_6_ÅR_AUTOVEDTAK, + ).map { begrunnelse -> + Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = this, + standardbegrunnelse = begrunnelse, + ) + }, + ) + }, + ) + + val fagsakErBegrunnet = vedtaksperioder.erAlleredeBegrunnetMedBegrunnelse( + standardbegrunnelser = listOf(Standardbegrunnelse.REDUKSJON_UNDER_6_ÅR_AUTOVEDTAK), + måned = fom, + ) + + assertFalse(fagsakErBegrunnet) + } + + @Test + fun `Skal ikke finne begrunnelse på tidligere vedtaksperiode når den ikke har begrunnelsen i det hele tatt`() { + val fom = YearMonth.now().minusMonths(3) + val vedtak = lagVedtak() + + val vedtaksperioder = listOf( + VedtaksperiodeMedBegrunnelser( + fom = fom.minusYears(1).førsteDagIInneværendeMåned(), + vedtak = vedtak, + type = Vedtaksperiodetype.UTBETALING, + ).apply { + begrunnelser.addAll( + listOf( + Standardbegrunnelse.INNVILGET_FØDSELSHENDELSE_NYFØDT_BARN, + ).map { begrunnelse -> + Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = this, + standardbegrunnelse = begrunnelse, + ) + }, + ) + }, + ) + + val fagsakErBegrunnet = vedtaksperioder.erAlleredeBegrunnetMedBegrunnelse( + standardbegrunnelser = listOf(Standardbegrunnelse.REDUKSJON_UNDER_6_ÅR_AUTOVEDTAK), + måned = fom, + ) + + assertFalse(fagsakErBegrunnet) + } + + @Test + fun `Skal være begrunnet med reduksjon 3 år småbarnstillegg fra før`() { + val vedtak = lagVedtak() + + val vedtaksperioder = listOf( + VedtaksperiodeMedBegrunnelser( + fom = LocalDate.now().førsteDagIInneværendeMåned(), + vedtak = vedtak, + type = Vedtaksperiodetype.UTBETALING, + ).apply { + begrunnelser.addAll( + listOf( + Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_BARN_UNDER_TRE_ÅR, + ).map { begrunnelse -> + Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = this, + standardbegrunnelse = begrunnelse, + ) + }, + ) + }, + ) + + val fagsakErBegrunnet = vedtaksperioder.erAlleredeBegrunnetMedBegrunnelse( + standardbegrunnelser = listOf(Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_BARN_UNDER_TRE_ÅR), + måned = YearMonth.now(), + ) + + assertTrue(fagsakErBegrunnet) + } + + @Test + fun `Skal være begrunnet med reduksjon 3 år småbarnstillegg for 1 år siden, men sende ut brev for neste barn som fyller 3 år`() { + val vedtak = lagVedtak() + + val vedtaksperioder = listOf( + VedtaksperiodeMedBegrunnelser( + fom = LocalDate.now().minusYears(1).førsteDagIInneværendeMåned(), + vedtak = vedtak, + type = Vedtaksperiodetype.UTBETALING, + ).apply { + begrunnelser.addAll( + listOf( + Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_BARN_UNDER_TRE_ÅR, + ).map { begrunnelse -> + Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = this, + standardbegrunnelse = begrunnelse, + ) + }, + ) + }, + ) + + val fagsakErBegrunnet = vedtaksperioder.erAlleredeBegrunnetMedBegrunnelse( + standardbegrunnelser = listOf(Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_BARN_UNDER_TRE_ÅR), + måned = YearMonth.now(), + ) + + assertFalse(fagsakErBegrunnet) + } + + @Test + fun `Skal være begrunnet med reduksjon ikke lenger full OS`() { + val vedtak = lagVedtak() + + val vedtaksperioder = listOf( + VedtaksperiodeMedBegrunnelser( + fom = LocalDate.now().førsteDagIInneværendeMåned(), + vedtak = vedtak, + type = Vedtaksperiodetype.UTBETALING, + ).apply { + begrunnelser.addAll( + listOf( + Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_FULL_OVERGANGSSTØNAD, + ).map { begrunnelse -> + Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = this, + standardbegrunnelse = begrunnelse, + ) + }, + ) + }, + ) + + val fagsakErBegrunnet = vedtaksperioder.erAlleredeBegrunnetMedBegrunnelse( + standardbegrunnelser = listOf(Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_FULL_OVERGANGSSTØNAD), + måned = YearMonth.now(), + ) + + assertTrue(fagsakErBegrunnet) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelserTidslinjeTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelserTidslinjeTest.kt new file mode 100644 index 000000000..223bb6abf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeMedBegrunnelserTidslinjeTest.kt @@ -0,0 +1,56 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.DagTidspunkt +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class VedtaksperiodeMedBegrunnelserTidslinjeTest { + + val utbetalingsperioder = + listOf( + VedtaksperiodeMedBegrunnelser( + vedtak = lagVedtak(), + type = Vedtaksperiodetype.UTBETALING, + fom = null, + tom = LocalDate.now(), + ), + VedtaksperiodeMedBegrunnelser( + vedtak = lagVedtak(), + type = Vedtaksperiodetype.UTBETALING, + fom = LocalDate.now().plusDays(1), + tom = null, + ), + ) + + @Test + fun `Skal returnere tidslinje hvor null-datoer er gjort om til uendelighet`() { + val tidslinje = VedtaksperiodeMedBegrunnelserTidslinje(utbetalingsperioder) + val perioderFraTidslinje = tidslinje.perioder() + + Assertions.assertEquals(2, perioderFraTidslinje.size) + + val periode1 = perioderFraTidslinje.first() + val periode2 = perioderFraTidslinje.last() + + Assertions.assertEquals(DagTidspunkt.uendeligLengeSiden(), periode1.fraOgMed) + Assertions.assertEquals(DagTidspunkt.uendeligLengeTil(), periode2.tilOgMed) + } + + @Test + fun `Skal gjøre om uendelighet til null-datoer`() { + val tidslinje = VedtaksperiodeMedBegrunnelserTidslinje(utbetalingsperioder) + + val vedtaksperioderMedBegrunnelser = tidslinje.lagVedtaksperioderMedBegrunnelser() + + Assertions.assertEquals(2, vedtaksperioderMedBegrunnelser.size) + + val periode1 = vedtaksperioderMedBegrunnelser.first() + val periode2 = vedtaksperioderMedBegrunnelser.last() + + Assertions.assertEquals(null, periode1.fom) + Assertions.assertEquals(null, periode2.tom) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeServiceTest.kt new file mode 100644 index 000000000..6f69e85ba --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeServiceTest.kt @@ -0,0 +1,248 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.lagVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.beregning.SmåbarnstilleggService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.feilutbetaltValuta.FeilutbetaltValuta +import no.nav.familie.ba.sak.kjerne.vedtak.feilutbetaltValuta.FeilutbetaltValutaRepository +import no.nav.familie.ba.sak.kjerne.vedtak.refusjonEøs.RefusjonEøs +import no.nav.familie.ba.sak.kjerne.vedtak.refusjonEøs.RefusjonEøsRepository +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.YearMonth + +class VedtaksperiodeServiceTest { + private val persongrunnlagService: PersongrunnlagService = mockk() + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService = + mockk() + private val vedtaksperiodeHentOgPersisterService: VedtaksperiodeHentOgPersisterService = mockk() + private val featureToggleService: FeatureToggleService = mockk() + private val feilutbetaltValutaRepository: FeilutbetaltValutaRepository = mockk() + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService = mockk() + private val småbarnstilleggService: SmåbarnstilleggService = mockk() + private val refusjonEøsRepository = mockk() + private val integrasjonClient = mockk() + + private val vedtaksperiodeService = spyk( + VedtaksperiodeService( + personidentService = mockk(), + persongrunnlagService = persongrunnlagService, + andelTilkjentYtelseRepository = mockk(), + vedtaksperiodeHentOgPersisterService = vedtaksperiodeHentOgPersisterService, + vedtakRepository = mockk(), + vilkårsvurderingService = mockk(relaxed = true), + sanityService = mockk(), + søknadGrunnlagService = mockk(relaxed = true), + endretUtbetalingAndelRepository = mockk(), + kompetanseRepository = mockk(), + andelerTilkjentYtelseOgEndreteUtbetalingerService = andelerTilkjentYtelseOgEndreteUtbetalingerService, + featureToggleService = featureToggleService, + feilutbetaltValutaRepository = feilutbetaltValutaRepository, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + småbarnstilleggService = småbarnstilleggService, + refusjonEøsRepository = refusjonEøsRepository, + integrasjonClient = integrasjonClient, + ), + ) + + private val person = lagPerson() + private val forrigeBehandling = lagBehandling().also { + it.behandlingStegTilstand.add(BehandlingStegTilstand(0, it, StegType.BEHANDLING_AVSLUTTET)) + } + private val behandling = lagBehandling(fagsak = forrigeBehandling.fagsak) + private val vedtak = lagVedtak(behandling) + + private val endringstidspunkt = LocalDate.of(2022, 11, 1) + private val ytelseOpphørtFørEndringstidspunkt = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.from(endringstidspunkt).minusMonths(3), + tom = YearMonth.from(endringstidspunkt).minusMonths(2), + person = person, + ) + private val ytelseOpphørtSammeMåned = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.from(endringstidspunkt), + tom = YearMonth.from(endringstidspunkt).plusYears(18), + person = person, + ) + + @BeforeEach + fun init() { + every { behandlingHentOgPersisterService.hentSisteBehandlingSomErVedtatt(any()) } returns forrigeBehandling + every { behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(any()) } returns forrigeBehandling + every { behandlingHentOgPersisterService.hent(forrigeBehandling.id) } returns forrigeBehandling + every { behandlingHentOgPersisterService.hent(behandling.id) } returns behandling + every { vedtaksperiodeService.finnEndringstidspunktForBehandling(vedtak.behandling.id) } returns endringstidspunkt + every { persongrunnlagService.hentAktiv(any()) } returns + lagTestPersonopplysningGrunnlag(vedtak.behandling.id, person) + every { persongrunnlagService.hentAktivThrows(any()) } returns + lagTestPersonopplysningGrunnlag(vedtak.behandling.id, person) + every { + andelerTilkjentYtelseOgEndreteUtbetalingerService.finnAndelerTilkjentYtelseMedEndreteUtbetalinger( + forrigeBehandling.id, + ) + } returns listOf(ytelseOpphørtSammeMåned) + + every { + andelerTilkjentYtelseOgEndreteUtbetalingerService.finnAndelerTilkjentYtelseMedEndreteUtbetalinger(behandling.id) + } returns listOf(ytelseOpphørtFørEndringstidspunkt) + every { + featureToggleService.isEnabled( + FeatureToggleConfig.EØS_INFORMASJON_OM_ÅRLIG_KONTROLL, + any(), + ) + } returns true + every { + featureToggleService.isEnabled( + FeatureToggleConfig.FEILUTBETALT_VALUTA_PR_MND, + any(), + ) + } returns true + every { feilutbetaltValutaRepository.finnFeilutbetaltValutaForBehandling(any()) } returns emptyList() + every { småbarnstilleggService.hentPerioderMedFullOvergangsstønad(any()) } returns emptyList() + every { refusjonEøsRepository.finnRefusjonEøsForBehandling(any()) } returns emptyList() + every { integrasjonClient.hentLandkoderISO2() } returns mapOf(Pair("NO", "NORGE")) + } + + @Test + fun `nasjonal skal ikke ha årlig kontroll`() { + val behandling = lagBehandling(behandlingKategori = BehandlingKategori.NASJONAL) + val vedtak = Vedtak(behandling = behandling) + assertFalse { vedtaksperiodeService.skalHaÅrligKontroll(vedtak) } + } + + @Test + fun `EØS med periode med utløpt tom skal ikke ha årlig kontroll`() { + val vedtak = Vedtak(behandling = lagBehandling(behandlingKategori = BehandlingKategori.EØS)) + every { vedtaksperiodeHentOgPersisterService.finnVedtaksperioderFor(any()) } returns listOf( + lagVedtaksperiodeMedBegrunnelser(vedtak = vedtak, tom = LocalDate.now()), + ) + assertFalse { vedtaksperiodeService.skalHaÅrligKontroll(vedtak) } + } + + @Test + fun `EØS med periode med løpende tom skal ha årlig kontroll`() { + val vedtak = Vedtak(behandling = lagBehandling(behandlingKategori = BehandlingKategori.EØS)) + every { vedtaksperiodeHentOgPersisterService.finnVedtaksperioderFor(any()) } returns listOf( + lagVedtaksperiodeMedBegrunnelser(vedtak = vedtak, tom = LocalDate.now().plusMonths(1)), + ) + assertTrue { vedtaksperiodeService.skalHaÅrligKontroll(vedtak) } + } + + @Test + fun `EØS med periode uten tom skal ha årlig kontroll`() { + val vedtak = Vedtak(behandling = lagBehandling(behandlingKategori = BehandlingKategori.EØS)) + every { vedtaksperiodeHentOgPersisterService.finnVedtaksperioderFor(any()) } returns listOf( + lagVedtaksperiodeMedBegrunnelser(vedtak = vedtak, tom = null), + ) + assertTrue { vedtaksperiodeService.skalHaÅrligKontroll(vedtak) } + } + + @Test + fun `EØS skal ikke ha årlig kontroll når feature toggle er skrudd av`() { + every { + featureToggleService.isEnabled(FeatureToggleConfig.EØS_INFORMASJON_OM_ÅRLIG_KONTROLL, any()) + } returns false + + val vedtak = Vedtak(behandling = lagBehandling(behandlingKategori = BehandlingKategori.EØS)) + every { vedtaksperiodeHentOgPersisterService.finnVedtaksperioderFor(any()) } returns listOf( + lagVedtaksperiodeMedBegrunnelser(vedtak = vedtak, tom = null), + ) + assertFalse { vedtaksperiodeService.skalHaÅrligKontroll(vedtak) } + } + + @Test + fun `skal beskrive perioder med for mye utbetalt for behandling med feilutbetalt valuta`() { + val vedtak = Vedtak(behandling = lagBehandling(behandlingKategori = BehandlingKategori.EØS)) + val perioder = listOf( + LocalDate.now() to LocalDate.now().plusYears(1), + LocalDate.now().plusYears(2) to LocalDate.now().plusYears(3), + ) + assertThat(vedtaksperiodeService.beskrivPerioderMedFeilutbetaltValuta(vedtak)).isNull() + + every { + feilutbetaltValutaRepository.finnFeilutbetaltValutaForBehandling(vedtak.behandling.id) + } returns perioder.map { + FeilutbetaltValuta(1L, fom = it.first, tom = it.second, 200, true) + } + val periodebeskrivelser = vedtaksperiodeService.beskrivPerioderMedFeilutbetaltValuta(vedtak) + + perioder.forEach { periode -> + assertThat(periodebeskrivelser!!.find { it.contains("${periode.first.year}") }) + .contains("Fra", "til", "${periode.second.year}", "er det utbetalt 200 kroner for mye per måned.") + } + } + + @Test + fun `skal beskrive perioder med eøs refusjoner for behandlinger med avklarte refusjon eøs`() { + val behandling = lagBehandling(behandlingKategori = BehandlingKategori.EØS) + assertThat( + vedtaksperiodeService.beskrivPerioderMedRefusjonEøs( + behandling = behandling, + avklart = true, + ), + ).isNull() + + every { refusjonEøsRepository.finnRefusjonEøsForBehandling(behandling.id) } returns listOf( + RefusjonEøs( + behandlingId = 1L, + fom = LocalDate.of(2020, 1, 1), + tom = LocalDate.of(2022, 1, 1), + refusjonsbeløp = 200, + land = "NO", + refusjonAvklart = true, + ), + ) + + val perioder = vedtaksperiodeService.beskrivPerioderMedRefusjonEøs(behandling = behandling, avklart = true) + + assertThat(perioder?.size).isEqualTo(1) + assertThat(perioder?.single()).isEqualTo("Fra januar 2020 til januar 2022 blir etterbetaling på 200 kroner per måned utbetalt til myndighetene i Norge.") + } + + @Test + fun `skal beskrive perioder med eøs refusjoner for behandlinger med uavklarte refusjon eøs`() { + val behandling = lagBehandling(behandlingKategori = BehandlingKategori.EØS) + assertThat( + vedtaksperiodeService.beskrivPerioderMedRefusjonEøs( + behandling = behandling, + avklart = false, + ), + ).isNull() + + every { refusjonEøsRepository.finnRefusjonEøsForBehandling(behandling.id) } returns listOf( + RefusjonEøs( + behandlingId = 1L, + fom = LocalDate.of(2020, 1, 1), + tom = LocalDate.of(2022, 1, 1), + refusjonsbeløp = 200, + land = "NO", + refusjonAvklart = false, + ), + ) + + val perioder = vedtaksperiodeService.beskrivPerioderMedRefusjonEøs(behandling = behandling, avklart = false) + + assertThat(perioder?.size).isEqualTo(1) + assertThat(perioder?.single()).isEqualTo("Fra januar 2020 til januar 2022 blir ikke etterbetaling på 200 kroner per måned utbetalt nå siden det er utbetalt barnetrygd i Norge.") + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeServiceUtilsTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeServiceUtilsTest.kt new file mode 100644 index 000000000..6f60d7f01 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeServiceUtilsTest.kt @@ -0,0 +1,263 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.NullablePeriode +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagTriggesAv +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.domene.RestBehandlingsgrunnlagForBrev +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertPersonResultat +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertRestEndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.brev.hentPersonidenterGjeldendeForBegrunnelse +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.periodeErOppyltForYtelseType +import no.nav.familie.ba.sak.kjerne.vedtak.domene.tilMinimertPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class VedtaksperiodeServiceUtilsTest { + + @Test + fun `Skal legge til alle barn med utbetaling ved utvidet barnetrygd`() { + val behandling = lagBehandling() + val søker = lagPerson(type = PersonType.SØKER) + val barn = lagPerson(type = PersonType.BARN) + + val persongrunnlag = + lagTestPersonopplysningGrunnlag(behandlingId = behandling.id, personer = arrayOf(søker, barn)) + val triggesAv = lagTriggesAv(vilkår = setOf(Vilkår.UTVIDET_BARNETRYGD)) + val vilkårsvurdering = lagVilkårsvurdering( + søkerAktør = søker.aktør, + behandling = behandling, + resultat = Resultat.OPPFYLT, + ) + val identerMedUtbetaling = listOf(barn.aktør.aktivFødselsnummer(), søker.aktør.aktivFødselsnummer()) + + val personidenterForBegrunnelse = hentPersonidenterGjeldendeForBegrunnelse( + triggesAv = triggesAv, + vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET, + vedtaksperiodetype = Vedtaksperiodetype.UTBETALING, + periode = NullablePeriode(LocalDate.now().minusMonths(1), null), + restBehandlingsgrunnlagForBrev = RestBehandlingsgrunnlagForBrev( + personerPåBehandling = persongrunnlag.personer.map { it.tilMinimertPerson() }, + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimerteEndredeUtbetalingAndeler = emptyList(), + fagsakType = FagsakType.NORMAL, + ), + identerMedUtbetalingPåPeriode = identerMedUtbetaling, + erFørsteVedtaksperiodePåFagsak = false, + minimerteUtbetalingsperiodeDetaljer = listOf(), + dødeBarnForrigePeriode = emptyList(), + begrunnelse = Standardbegrunnelse.INNVILGET_BOR_ALENE_MED_BARN, + ) + + Assertions.assertEquals( + setOf(barn.aktør.aktivFødselsnummer(), søker.aktør.aktivFødselsnummer()), + personidenterForBegrunnelse, + ) + } + + @Test + fun `Skal legge til alle barn fra endret utbetaling ved utvidet barnetrygd og endret utbetaling`() { + val behandling = lagBehandling() + val søker = lagPerson(type = PersonType.SØKER) + val barn1 = lagPerson(type = PersonType.BARN) + val barn2 = lagPerson(type = PersonType.BARN) + + val fom = LocalDate.now().withDayOfMonth(1) + val tom = LocalDate.now().let { + it.withDayOfMonth(it.lengthOfMonth()) + } + + val persongrunnlag = + lagTestPersonopplysningGrunnlag(behandlingId = behandling.id, personer = arrayOf(søker, barn2)) + val triggesAv = lagTriggesAv(vilkår = setOf(Vilkår.UTVIDET_BARNETRYGD)) + val vilkårsvurdering = lagVilkårsvurdering( + søkerAktør = søker.aktør, + behandling = behandling, + resultat = Resultat.OPPFYLT, + ) + + val identerMedUtbetaling = listOf(barn1.aktør.aktivFødselsnummer(), søker.aktør.aktivFødselsnummer()) + val endredeUtbetalingAndeler = listOf( + lagEndretUtbetalingAndelMedAndelerTilkjentYtelse( + person = barn2, + fom = fom.toYearMonth(), + tom = tom.toYearMonth(), + ), + ) + + val personidenterForBegrunnelse = hentPersonidenterGjeldendeForBegrunnelse( + triggesAv = triggesAv, + periode = NullablePeriode(fom, tom), + vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET, + vedtaksperiodetype = Vedtaksperiodetype.UTBETALING, + restBehandlingsgrunnlagForBrev = RestBehandlingsgrunnlagForBrev( + personerPåBehandling = persongrunnlag.personer.map { it.tilMinimertPerson() }, + minimertePersonResultater = vilkårsvurdering.personResultater.map { it.tilMinimertPersonResultat() }, + minimerteEndredeUtbetalingAndeler = endredeUtbetalingAndeler + .map { it.tilMinimertRestEndretUtbetalingAndel() }, + fagsakType = FagsakType.NORMAL, + ), + identerMedUtbetalingPåPeriode = identerMedUtbetaling, + erFørsteVedtaksperiodePåFagsak = false, + minimerteUtbetalingsperiodeDetaljer = listOf(), + dødeBarnForrigePeriode = emptyList(), + begrunnelse = Standardbegrunnelse.INNVILGET_BOR_ALENE_MED_BARN, + ) + + Assertions.assertEquals( + setOf(barn1.aktør.aktivFødselsnummer(), barn2.aktør.aktivFødselsnummer(), søker.aktør.aktivFødselsnummer()), + personidenterForBegrunnelse.toSet(), + ) + } + + val ytelseTyperSmåbarnstillegg = + setOf(YtelseType.SMÅBARNSTILLEGG, YtelseType.UTVIDET_BARNETRYGD, YtelseType.ORDINÆR_BARNETRYGD) + val ytelseTyperUtvidetOgOrdinær = + setOf(YtelseType.UTVIDET_BARNETRYGD, YtelseType.ORDINÆR_BARNETRYGD) + val ytelseTyperOrdinær = + setOf(YtelseType.ORDINÆR_BARNETRYGD) + + @Test + fun `Skal gi riktig svar for småbarnstillegg-trigger ved innvilget VedtakBegrunnelseType`() { + Assertions.assertEquals( + true, + VedtakBegrunnelseType.INNVILGET.periodeErOppyltForYtelseType( + ytelseType = YtelseType.SMÅBARNSTILLEGG, + ytelseTyperForPeriode = ytelseTyperSmåbarnstillegg, + ytelserGjeldeneForSøkerForrigeMåned = emptyList(), + ), + ) + + Assertions.assertEquals( + false, + VedtakBegrunnelseType.INNVILGET.periodeErOppyltForYtelseType( + ytelseType = YtelseType.SMÅBARNSTILLEGG, + ytelseTyperForPeriode = ytelseTyperUtvidetOgOrdinær, + ytelserGjeldeneForSøkerForrigeMåned = emptyList(), + ), + ) + } + + @Test + fun `Skal gi riktig svar for småbarnstillegg-trigger når VedtakBegrunnelseType er reduksjon`() { + Assertions.assertEquals( + true, + VedtakBegrunnelseType.REDUKSJON.periodeErOppyltForYtelseType( + ytelseType = YtelseType.SMÅBARNSTILLEGG, + ytelseTyperForPeriode = ytelseTyperUtvidetOgOrdinær, + ytelserGjeldeneForSøkerForrigeMåned = listOf(YtelseType.SMÅBARNSTILLEGG), + ), + ) + + Assertions.assertEquals( + false, + VedtakBegrunnelseType.REDUKSJON.periodeErOppyltForYtelseType( + ytelseType = YtelseType.SMÅBARNSTILLEGG, + ytelseTyperForPeriode = ytelseTyperSmåbarnstillegg, + ytelserGjeldeneForSøkerForrigeMåned = listOf(YtelseType.SMÅBARNSTILLEGG), + ), + ) + + Assertions.assertEquals( + false, + VedtakBegrunnelseType.REDUKSJON.periodeErOppyltForYtelseType( + ytelseType = YtelseType.SMÅBARNSTILLEGG, + ytelseTyperForPeriode = ytelseTyperUtvidetOgOrdinær, + ytelserGjeldeneForSøkerForrigeMåned = listOf(YtelseType.ORDINÆR_BARNETRYGD), + ), + ) + + Assertions.assertEquals( + false, + VedtakBegrunnelseType.REDUKSJON.periodeErOppyltForYtelseType( + ytelseType = YtelseType.SMÅBARNSTILLEGG, + ytelseTyperForPeriode = ytelseTyperUtvidetOgOrdinær, + ytelserGjeldeneForSøkerForrigeMåned = listOf(), + ), + ) + } + + @Test + fun `Skal gi false når VedtakBegrunnelseType ikke er innvilget eller reduksjon `() { + Assertions.assertEquals( + false, + VedtakBegrunnelseType.AVSLAG.periodeErOppyltForYtelseType( + ytelseType = YtelseType.SMÅBARNSTILLEGG, + ytelseTyperForPeriode = ytelseTyperSmåbarnstillegg, + ytelserGjeldeneForSøkerForrigeMåned = emptyList(), + ), + ) + } + + @Test + fun `Skal gi riktig svar for utvidet-trigger ved innvilget`() { + Assertions.assertEquals( + true, + VedtakBegrunnelseType.INNVILGET.periodeErOppyltForYtelseType( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ytelseTyperForPeriode = ytelseTyperUtvidetOgOrdinær, + ytelserGjeldeneForSøkerForrigeMåned = emptyList(), + ), + ) + + Assertions.assertEquals( + false, + VedtakBegrunnelseType.INNVILGET.periodeErOppyltForYtelseType( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ytelseTyperForPeriode = ytelseTyperOrdinær, + ytelserGjeldeneForSøkerForrigeMåned = emptyList(), + ), + ) + } + + @Test + fun `Skal gi riktig svar for utvidet barnetrygd-trigger når VedtakBegrunnelseType er reduksjon`() { + Assertions.assertEquals( + true, + VedtakBegrunnelseType.REDUKSJON.periodeErOppyltForYtelseType( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ytelseTyperForPeriode = ytelseTyperOrdinær, + ytelserGjeldeneForSøkerForrigeMåned = listOf(YtelseType.UTVIDET_BARNETRYGD), + ), + ) + + Assertions.assertEquals( + false, + VedtakBegrunnelseType.REDUKSJON.periodeErOppyltForYtelseType( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ytelseTyperForPeriode = ytelseTyperUtvidetOgOrdinær, + ytelserGjeldeneForSøkerForrigeMåned = listOf(YtelseType.UTVIDET_BARNETRYGD), + ), + ) + + Assertions.assertEquals( + false, + VedtakBegrunnelseType.REDUKSJON.periodeErOppyltForYtelseType( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ytelseTyperForPeriode = ytelseTyperOrdinær, + ytelserGjeldeneForSøkerForrigeMåned = listOf(YtelseType.ORDINÆR_BARNETRYGD), + ), + ) + + Assertions.assertEquals( + false, + VedtakBegrunnelseType.REDUKSJON.periodeErOppyltForYtelseType( + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ytelseTyperForPeriode = ytelseTyperOrdinær, + ytelserGjeldeneForSøkerForrigeMåned = listOf(), + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeUtilTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeUtilTest.kt new file mode 100644 index 000000000..57016995e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeUtilTest.kt @@ -0,0 +1,253 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.config.testSanityKlient +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class VedtaksperiodeUtilTest { + + private val sanityEØSBegrunnelser = testSanityKlient.hentEØSBegrunnelserMap() + + @Test + fun `Skal ikke endre på utbetalingsperioder hvis det ikke finnes reduksjonsperioder`() { + val vedtak = lagVedtak() + val utbetalingsperioder = listOf( + VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.now().minusYears(2).førsteDagIInneværendeMåned(), + tom = LocalDate.now().minusYears(1).minusMonths(1).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING, + ), + VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.now().minusYears(1).førsteDagIInneværendeMåned(), + tom = LocalDate.now().sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING, + ), + VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.now().plusMonths(1).førsteDagIInneværendeMåned(), + tom = LocalDate.now().plusYears(3).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING, + ), + ) + + val oppdaterteUtbetalingsperioder = oppdaterUtbetalingsperioderMedReduksjonFraForrigeBehandling( + utbetalingsperioder = utbetalingsperioder, + reduksjonsperioder = emptyList(), + ) + + Assertions.assertEquals(3, oppdaterteUtbetalingsperioder.size) + Assertions.assertEquals(utbetalingsperioder, oppdaterteUtbetalingsperioder) + } + + @Test + fun `Skal oppdatere utbetalingsperiodene med reduksjonsperioder`() { + val vedtak = lagVedtak() + + val utbetalingsperiode = VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.now().minusYears(1).førsteDagIInneværendeMåned(), + tom = LocalDate.now().plusYears(1).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING, + ) + val reduksjonsperiode = VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.now().minusYears(1).plusMonths(1).førsteDagIInneværendeMåned(), + tom = LocalDate.now().minusYears(1).plusMonths(6).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + ) + + val oppdaterteUtbetalingsperioder = oppdaterUtbetalingsperioderMedReduksjonFraForrigeBehandling( + utbetalingsperioder = listOf(utbetalingsperiode), + reduksjonsperioder = listOf(reduksjonsperiode), + ) + + Assertions.assertEquals(3, oppdaterteUtbetalingsperioder.size) + + val periode1 = oppdaterteUtbetalingsperioder[0] + val periode2 = oppdaterteUtbetalingsperioder[1] + val periode3 = oppdaterteUtbetalingsperioder[2] + + Assertions.assertEquals(Vedtaksperiodetype.UTBETALING, periode1.type) + Assertions.assertEquals(utbetalingsperiode.fom, periode1.fom) + Assertions.assertEquals(reduksjonsperiode.fom?.minusMonths(1)?.sisteDagIMåned(), periode1.tom) + + Assertions.assertEquals( + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + periode2.type, + ) + Assertions.assertEquals(reduksjonsperiode.fom, periode2.fom) + Assertions.assertEquals(reduksjonsperiode.tom, periode2.tom) + + Assertions.assertEquals(Vedtaksperiodetype.UTBETALING, periode3.type) + Assertions.assertEquals(reduksjonsperiode.tom?.plusMonths(1)?.førsteDagIInneværendeMåned(), periode3.fom) + Assertions.assertEquals(utbetalingsperiode.tom, periode3.tom) + } + + @Test + fun `Skal kun lage en vedtaksperiode med type UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING hvis reduksjonsperiodene er sammenhengende og ikke krysser utbetalingsperiode-splitt`() { + val vedtak = lagVedtak() + + val b2bFomUtbetalingsperiode = LocalDate.now().minusYears(2).førsteDagIInneværendeMåned() + val utbetalingsperiode1 = VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.now().minusYears(3).førsteDagIInneværendeMåned(), + tom = b2bFomUtbetalingsperiode.minusMonths(1).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING, + ) + val utbetalingsperiode2 = VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = b2bFomUtbetalingsperiode, + tom = LocalDate.now().plusYears(1).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING, + ) + + val b2bFom1 = LocalDate.now().minusMonths(6).førsteDagIInneværendeMåned() + val b2bFom2 = LocalDate.now().førsteDagIInneværendeMåned() + + val reduksjonsperiode1 = VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.now().minusYears(1).plusMonths(1).førsteDagIInneværendeMåned(), + tom = b2bFom1.minusMonths(1).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + ) + val reduksjonsperiode2 = VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = b2bFom1, + tom = b2bFom2.minusMonths(1).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + ) + val reduksjonsperiode3 = VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = b2bFom2, + tom = LocalDate.now().plusMonths(6).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + ) + + val oppdaterteUtbetalingsperioder = oppdaterUtbetalingsperioderMedReduksjonFraForrigeBehandling( + utbetalingsperioder = listOf(utbetalingsperiode1, utbetalingsperiode2), + reduksjonsperioder = listOf(reduksjonsperiode1, reduksjonsperiode2, reduksjonsperiode3), + ) + + Assertions.assertEquals(4, oppdaterteUtbetalingsperioder.size) + + val periode1 = oppdaterteUtbetalingsperioder[0] + val periode2 = oppdaterteUtbetalingsperioder[1] + val periode3 = oppdaterteUtbetalingsperioder[2] + val periode4 = oppdaterteUtbetalingsperioder[3] + + Assertions.assertEquals(Vedtaksperiodetype.UTBETALING, periode1.type) + Assertions.assertEquals(utbetalingsperiode1.fom, periode1.fom) + Assertions.assertEquals(b2bFomUtbetalingsperiode.minusMonths(1)?.sisteDagIMåned(), periode1.tom) + + Assertions.assertEquals(Vedtaksperiodetype.UTBETALING, periode2.type) + Assertions.assertEquals(b2bFomUtbetalingsperiode, periode2.fom) + Assertions.assertEquals(reduksjonsperiode1.fom?.minusMonths(1)?.sisteDagIMåned(), periode2.tom) + + Assertions.assertEquals( + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + periode3.type, + ) + Assertions.assertEquals(reduksjonsperiode1.fom, periode3.fom) + Assertions.assertEquals(reduksjonsperiode3.tom, periode3.tom) + + Assertions.assertEquals(Vedtaksperiodetype.UTBETALING, periode4.type) + Assertions.assertEquals(reduksjonsperiode3.tom?.plusMonths(1)?.førsteDagIInneværendeMåned(), periode4.fom) + Assertions.assertEquals(utbetalingsperiode2.tom, periode4.tom) + } + + @Test + fun `Skal ikke slå sammen reduksjonsperioder som overlapper med utbetalingsperioder`() { + val vedtak = lagVedtak() + + val fom1 = LocalDate.now().minusYears(1).førsteDagIInneværendeMåned() + val fomReduksjon = fom1.plusMonths(1).førsteDagIInneværendeMåned() + val fom2 = fomReduksjon.plusMonths(2).førsteDagIInneværendeMåned() + val fom3 = fom2.plusMonths(3).førsteDagIInneværendeMåned() + val sisteTom = fom3.plusMonths(5).sisteDagIMåned() + + val utbetalingsperioder = listOf( + VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = fom1, + tom = fom2.minusMonths(1).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING, + ), + VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = fom2, + tom = fom3.minusMonths(1).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING, + ), + VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = fom3, + tom = sisteTom, + type = Vedtaksperiodetype.UTBETALING, + ), + ) + val reduksjonsperioder = listOf( + VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = fomReduksjon, + tom = fom2.minusMonths(1).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + ), + VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = fom2, + tom = fom3.minusMonths(1).sisteDagIMåned(), + type = Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + ), + VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = fom3, + tom = sisteTom, + type = Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + ), + ) + + val oppdaterteUtbetalingsperioder = oppdaterUtbetalingsperioderMedReduksjonFraForrigeBehandling( + utbetalingsperioder = utbetalingsperioder, + reduksjonsperioder = reduksjonsperioder, + ) + + Assertions.assertEquals(4, oppdaterteUtbetalingsperioder.size) + + val periode1 = oppdaterteUtbetalingsperioder[0] + val periode2 = oppdaterteUtbetalingsperioder[1] + val periode3 = oppdaterteUtbetalingsperioder[2] + val periode4 = oppdaterteUtbetalingsperioder[3] + + Assertions.assertEquals(Vedtaksperiodetype.UTBETALING, periode1.type) + Assertions.assertEquals(fom1, periode1.fom) + Assertions.assertEquals(fomReduksjon.minusDays(1), periode1.tom) + + Assertions.assertEquals( + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + periode2.type, + ) + Assertions.assertEquals(fomReduksjon, periode2.fom) + Assertions.assertEquals(fom2.minusDays(1), periode2.tom) + + Assertions.assertEquals( + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + periode3.type, + ) + Assertions.assertEquals(fom2, periode3.fom) + Assertions.assertEquals(fom3.minusDays(1), periode3.tom) + + Assertions.assertEquals( + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + periode4.type, + ) + Assertions.assertEquals(fom3, periode4.fom) + Assertions.assertEquals(sisteTom, periode4.tom) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Vilk\303\245rUtilsTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Vilk\303\245rUtilsTest.kt" new file mode 100644 index 000000000..068f56a38 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/Vilk\303\245rUtilsTest.kt" @@ -0,0 +1,109 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import io.mockk.mockk +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.lagTriggesAv +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.brev.domene.tilMinimertVilkårResultat +import no.nav.familie.ba.sak.kjerne.brev.erFørstePeriodeOgVilkårIkkeOppfylt +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class VilkårUtilsTest { + + val vedtaksperiode: Periode = Periode( + fom = LocalDate.now().minusMonths(2), + tom = LocalDate.now().plusMonths(4), + ) + + val triggesAv = lagTriggesAv(deltbosted = false, vurderingAnnetGrunnlag = false, medlemskap = false) + val vilkårResultatIkkeOppfylt: VilkårResultat = lagVilkårResultat( + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = vedtaksperiode.fom, + periodeTom = vedtaksperiode.tom, + personResultat = mockk(relaxed = true), + ) + val vilkårResultatIkkeOppfyltDelvisOverlapp: VilkårResultat = lagVilkårResultat( + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = vedtaksperiode.fom.minusMonths(1), + periodeTom = vedtaksperiode.tom.plusMonths(1), + personResultat = mockk(relaxed = true), + ) + val vilkårResultatUtenforPeriode: VilkårResultat = lagVilkårResultat( + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = vedtaksperiode.tom.plusMonths(1), + periodeTom = vedtaksperiode.tom.plusMonths(3), + personResultat = mockk(relaxed = true), + ) + + val vilkårResultatOppfylt: VilkårResultat = lagVilkårResultat( + resultat = Resultat.OPPFYLT, + periodeFom = vedtaksperiode.fom, + periodeTom = vedtaksperiode.tom, + personResultat = mockk(relaxed = true), + ) + + @Test + fun `Er førte periode dersom resultat ikke er godkjent og det ikke er noen andeler tilkjent ytelse før perioden`() { + Assertions.assertTrue( + erFørstePeriodeOgVilkårIkkeOppfylt( + erFørsteVedtaksperiodePåFagsak = true, + vilkårResultat = vilkårResultatIkkeOppfylt.tilMinimertVilkårResultat(), + vedtaksperiode = vedtaksperiode, + triggesAv = triggesAv, + + ), + ) + Assertions.assertTrue( + erFørstePeriodeOgVilkårIkkeOppfylt( + erFørsteVedtaksperiodePåFagsak = true, + vilkårResultat = vilkårResultatIkkeOppfyltDelvisOverlapp.tilMinimertVilkårResultat(), + vedtaksperiode = vedtaksperiode, + triggesAv = triggesAv, + + ), + ) + } + + @Test + fun `Er ikke førte periode dersom det er en andel tilkjent ytelse før perioden`() { + Assertions.assertFalse( + erFørstePeriodeOgVilkårIkkeOppfylt( + erFørsteVedtaksperiodePåFagsak = false, + vilkårResultat = vilkårResultatIkkeOppfylt.tilMinimertVilkårResultat(), + vedtaksperiode = vedtaksperiode, + triggesAv = triggesAv, + + ), + ) + } + + @Test + fun `Er ikke førte periode dersom vilkårResultatet er oppfylt`() { + Assertions.assertFalse( + erFørstePeriodeOgVilkårIkkeOppfylt( + erFørsteVedtaksperiodePåFagsak = true, + vilkårResultat = vilkårResultatOppfylt.tilMinimertVilkårResultat(), + vedtaksperiode = vedtaksperiode, + triggesAv = triggesAv, + + ), + ) + } + + @Test + fun `Er ikke førte periode dersom vilkårResultatet ikke overlapper med periode`() { + Assertions.assertFalse( + erFørstePeriodeOgVilkårIkkeOppfylt( + erFørsteVedtaksperiodePåFagsak = true, + vilkårResultat = vilkårResultatUtenforPeriode.tilMinimertVilkårResultat(), + vedtaksperiode = vedtaksperiode, + triggesAv = triggesAv, + + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/verge/VergeServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/verge/VergeServiceTest.kt new file mode 100644 index 000000000..3c50a1c7c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/verge/VergeServiceTest.kt @@ -0,0 +1,30 @@ +package no.nav.familie.ba.sak.kjerne.verge + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import no.nav.familie.ba.sak.common.lagBehandling +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class VergeServiceTest { + + private val vergeRepositoryMock: VergeRepository = mockk() + + @Test + fun `RegistrerVerge() skal lagre verge og oppdater behandling`() { + val behandling = lagBehandling() + val vergeSlot = slot() + every { vergeRepositoryMock.findByBehandling(any()) } returns null + every { vergeRepositoryMock.save(capture(vergeSlot)) } returns Verge(1L, "", behandling) + val vergeService = VergeService(vergeRepositoryMock) + val verge = Verge(1L, "verge 1", behandling) + vergeService.oppdaterVergeForBehandling(behandling, verge) + val vergeCaptured = vergeSlot.captured + assertThat(vergeCaptured.id).isEqualTo(verge.id) + assertThat(vergeCaptured.ident).isEqualTo(verge.ident) + assertThat(vergeCaptured.behandling.id).isEqualTo(behandling.id) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/AnnenVurderingServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/AnnenVurderingServiceTest.kt" new file mode 100644 index 000000000..aa7774ef0 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/AnnenVurderingServiceTest.kt" @@ -0,0 +1,76 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestAnnenVurdering +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurderingRepository +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurderingType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.util.Optional + +class AnnenVurderingServiceTest { + + private val annenVurderingRepository = mockk(relaxed = true) + + private lateinit var annenVurderingService: AnnenVurderingService + private lateinit var personResultat: PersonResultat + + @BeforeEach + fun setUp() { + annenVurderingService = AnnenVurderingService(annenVurderingRepository = annenVurderingRepository) + + personResultat = lagPersonResultat( + vilkårsvurdering = Vilkårsvurdering(behandling = lagBehandling()), + person = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2019, 1, 1)), + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2020, 1, 1), + periodeTom = LocalDate.of(2020, 7, 1), + ) + } + + @Test + fun `Verifiser endreAnnenVurdering`() { + every { annenVurderingRepository.findById(any()) } returns Optional.of( + AnnenVurdering( + resultat = Resultat.OPPFYLT, + type = AnnenVurderingType.OPPLYSNINGSPLIKT, + begrunnelse = "begrunnelse", + personResultat = personResultat, + ), + ) + val nyAnnenVurering = AnnenVurdering( + resultat = Resultat.IKKE_OPPFYLT, + type = AnnenVurderingType.OPPLYSNINGSPLIKT, + begrunnelse = "begrunnelse to", + personResultat = personResultat, + ) + + every { annenVurderingRepository.save(any()) } returns nyAnnenVurering + + annenVurderingService.endreAnnenVurdering( + personResultat.vilkårsvurdering.behandling.id, + 123L, + RestAnnenVurdering( + 123L, + Resultat.IKKE_OPPFYLT, + type = AnnenVurderingType.OPPLYSNINGSPLIKT, + begrunnelse = "begrunnelse to", + ), + ) + + verify(exactly = 1) { + annenVurderingRepository.save(any()) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/OppdaterVilk\303\245rsvurderingTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/OppdaterVilk\303\245rsvurderingTest.kt" new file mode 100644 index 000000000..85eaa46a2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/OppdaterVilk\303\245rsvurderingTest.kt" @@ -0,0 +1,607 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingUtils.flyttResultaterTilInitielt +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingUtils.lagFjernAdvarsel +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurderingType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class OppdaterVilkårsvurderingTest { + + @Test + fun `Skal legge til nytt vilkår`() { + val fnr1 = randomFnr() + val aktørId1 = randomAktør() + val behandling = lagBehandling() + val resA = lagVilkårsvurdering(behandling = behandling, fnrAktør = listOf(Pair(fnr1, aktørId1))) + val resB = lagVilkårsvurderingResultatB(behandling = behandling, fnrAktør = listOf(Pair(fnr1, aktørId1))) + + val (oppdatert, gammelt) = flyttResultaterTilInitielt(resB, resA) + Assertions.assertEquals(3, oppdatert.personResultater.first().vilkårResultater.size) + Assertions.assertEquals( + Resultat.OPPFYLT, + oppdatert.personResultater.first() + .vilkårResultater.find { it.vilkårType == Vilkår.BOSATT_I_RIKET }?.resultat, + ) + Assertions.assertTrue(gammelt.personResultater.isEmpty()) + } + + @Test + fun `Skal fjerne vilkår`() { + val fnr1 = randomFnr() + val aktørId1 = randomAktør() + val behandling = lagBehandling() + val resA = lagVilkårsvurderingResultatB(behandling = behandling, fnrAktør = listOf(Pair(fnr1, aktørId1))) + val resB = lagVilkårsvurdering(behandling = behandling, fnrAktør = listOf(Pair(fnr1, aktørId1))) + + val (oppdatert, gammelt) = flyttResultaterTilInitielt(resB, resA) + Assertions.assertEquals(2, oppdatert.personResultater.first().vilkårResultater.size) + Assertions.assertEquals( + Resultat.OPPFYLT, + oppdatert.personResultater.first() + .vilkårResultater.find { it.vilkårType == Vilkår.BOSATT_I_RIKET }?.resultat, + ) + Assertions.assertEquals(1, gammelt.personResultater.size) + Assertions.assertEquals(1, gammelt.personResultater.first().vilkårResultater.size) + } + + @Test + fun `Skal legge til person på vilkårsvurdering`() { + val fnr1 = randomFnr() + val fnr2 = randomFnr() + val aktørId1 = randomAktør() + val aktørId2 = randomAktør() + val behandling = lagBehandling() + val resA = lagVilkårsvurdering(behandling = behandling, fnrAktør = listOf(Pair(fnr1, aktørId1))) + val resB = lagVilkårsvurdering( + behandling = behandling, + fnrAktør = listOf(Pair(fnr1, aktørId1), Pair(fnr2, aktørId2)), + ) + + val (oppdatert, gammelt) = flyttResultaterTilInitielt(resB, resA) + Assertions.assertEquals(2, oppdatert.personResultater.size) + Assertions.assertEquals(0, gammelt.personResultater.size) + } + + @Test + fun `Skal fjerne person på vilkårsvurdering`() { + val fnr1 = randomFnr() + val fnr2 = randomFnr() + val aktørId1 = randomAktør() + val aktørId2 = randomAktør() + val behandling = lagBehandling() + val resA = lagVilkårsvurdering( + behandling = behandling, + fnrAktør = listOf(Pair(fnr1, aktørId1), Pair(fnr2, aktørId2)), + ) + val resB = lagVilkårsvurdering(behandling = behandling, fnrAktør = listOf(Pair(fnr1, aktørId1))) + + val (oppdatert, gammelt) = flyttResultaterTilInitielt(resB, resA) + Assertions.assertEquals(1, oppdatert.personResultater.size) + Assertions.assertEquals(1, gammelt.personResultater.size) + } + + @Test + fun `Skal lage advarsel tekst`() { + val fnr1 = randomFnr() + val fnr2 = randomFnr() + val aktørId1 = tilAktør(fnr1) + val aktørId2 = tilAktør(fnr2) + val behandling = lagBehandling() + val resultat1 = lagVilkårsvurdering( + behandling = behandling, + fnrAktør = listOf(Pair(fnr1, aktørId1), Pair(fnr2, aktørId2)), + ) + val resultat2 = lagVilkårsvurdering(behandling = behandling, fnrAktør = listOf(Pair(fnr2, aktørId2))) + + val resterende = flyttResultaterTilInitielt(resultat2, resultat1).second + val fjernedeVilkår = resultat1.personResultater.first().vilkårResultater.toList() + val generertAdvarsel = lagFjernAdvarsel(resterende.personResultater) + + Assertions.assertEquals( + "Du har gjort endringer i behandlingsgrunnlaget. Dersom du går videre vil vilkår for følgende personer fjernes:\n" + + fnr1 + ":\n" + + " - " + fjernedeVilkår[0].vilkårType.beskrivelse + "\n" + + " - " + fjernedeVilkår[1].vilkårType.beskrivelse + "\n", + generertAdvarsel, + ) + } + + @Test + fun `Skal ha med tomt vilkår på person hvis vilkåret ble avslått forrige behandling`() { + val søkerAktørId = randomAktør() + val nyBehandling = lagBehandling() + val forrigeBehandling = lagBehandling() + + val init = + lagBasicVilkårsvurdering( + behandling = nyBehandling, + personer = listOf( + lagPerson(type = PersonType.SØKER, aktør = søkerAktørId), + lagPerson(type = PersonType.BARN), + ), + ) + val aktivMedBosattIRiketIkkeOppfylt = Vilkårsvurdering(behandling = forrigeBehandling) + val personResultat = + PersonResultat( + vilkårsvurdering = aktivMedBosattIRiketIkkeOppfylt, + aktør = søkerAktørId, + ) + val bosattIRiketVilkårResultater = + setOf( + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + personResultat = personResultat, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.now().minusYears(2), + periodeTom = LocalDate.now().minusYears(1), + ), + ) + personResultat.setSortedVilkårResultater(bosattIRiketVilkårResultater) + aktivMedBosattIRiketIkkeOppfylt.personResultater = setOf(personResultat) + + val (nyInit, nyAktiv) = flyttResultaterTilInitielt( + initiellVilkårsvurdering = init, + aktivVilkårsvurdering = aktivMedBosattIRiketIkkeOppfylt, + ) + + val nyInitBosattIRiketVilkår = + nyInit.personResultater.find { it.aktør == søkerAktørId }?.vilkårResultater?.filter { it.vilkårType == Vilkår.BOSATT_I_RIKET } + ?: emptyList() + + Assertions.assertTrue(nyInitBosattIRiketVilkår.isNotEmpty()) + Assertions.assertTrue(nyInitBosattIRiketVilkår.single().resultat == Resultat.IKKE_VURDERT) + Assertions.assertTrue(nyAktiv.personResultater.isEmpty()) + } + + @Test + fun `Skal ha med oppfylte perioder fra vilkår på person hvis vilkåret ble både avslått og innvilget forrige behandling`() { + val søkerAktørId = randomAktør() + val nyBehandling = lagBehandling() + val forrigeBehandling = lagBehandling() + + val init = + lagBasicVilkårsvurdering( + behandling = nyBehandling, + personer = listOf( + lagPerson(type = PersonType.SØKER, aktør = søkerAktørId), + lagPerson(type = PersonType.BARN), + ), + ) + val aktivMedBosattIRiketDelvisIkkeOppfylt = Vilkårsvurdering(behandling = forrigeBehandling) + val personResultat = + PersonResultat( + vilkårsvurdering = aktivMedBosattIRiketDelvisIkkeOppfylt, + aktør = søkerAktørId, + ) + val bosattIRiketVilkårResultater = + setOf( + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + personResultat = personResultat, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.now().minusYears(2), + periodeTom = LocalDate.now().minusYears(1), + ), + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + personResultat = personResultat, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now(), + periodeTom = LocalDate.now().plusYears(1), + ), + ) + personResultat.setSortedVilkårResultater(bosattIRiketVilkårResultater) + aktivMedBosattIRiketDelvisIkkeOppfylt.personResultater = setOf(personResultat) + + val (nyInit, nyAktiv) = flyttResultaterTilInitielt( + initiellVilkårsvurdering = init, + aktivVilkårsvurdering = aktivMedBosattIRiketDelvisIkkeOppfylt, + ) + + val nyInitBosattIRiketVilkår = + nyInit.personResultater.find { it.aktør == søkerAktørId }?.vilkårResultater?.filter { it.vilkårType == Vilkår.BOSATT_I_RIKET } + ?: emptyList() + + Assertions.assertTrue(nyInitBosattIRiketVilkår.isNotEmpty()) + Assertions.assertTrue(nyInitBosattIRiketVilkår.single().resultat == Resultat.OPPFYLT) + Assertions.assertTrue(nyAktiv.personResultater.isEmpty()) + } + + @Test + fun `Skal beholde vilkår om utvidet barnetrygd når forrige behandling inneholdt utvidet-vilkåret, men inneværende behandling er ordinær`() { + val søkerAktørId = randomAktør() + val behandling = lagBehandling() + + val initUtenUtvidetVilkår = + lagVilkårsvurderingMedForskjelligeTyperVilkår(søkerAktørId, behandling, listOf()) + val aktivMedUtvidetVilkår = + lagVilkårsvurderingMedForskjelligeTyperVilkår( + søkerAktørId, + behandling, + listOf(Vilkår.UTVIDET_BARNETRYGD), + ) + + val (nyInit, nyAktiv) = flyttResultaterTilInitielt( + initiellVilkårsvurdering = initUtenUtvidetVilkår, + aktivVilkårsvurdering = aktivMedUtvidetVilkår, + aktørerMedUtvidetAndelerIForrigeBehandling = listOf(søkerAktørId), + løpendeUnderkategori = BehandlingUnderkategori.UTVIDET, + ) + + val nyInitInnholderUtvidetVilkår = + nyInit.personResultater.first().vilkårResultater.any { vilkårResultat -> vilkårResultat.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + + Assertions.assertTrue(nyInitInnholderUtvidetVilkår) + Assertions.assertTrue(nyAktiv.personResultater.isEmpty()) + } + + @Test + fun `Skal beholde vilkår om utvidet barnetrygd når det eksisterer løpende sak med utvidet, men inneværende behandling er ordinær`() { + val søkerAktørId = randomAktør() + val behandling = lagBehandling() + + val initUtenUtvidetVilkår = + lagVilkårsvurderingMedForskjelligeTyperVilkår(søkerAktørId, behandling, listOf()) + val aktivMedUtvidetVilkår = + lagVilkårsvurderingMedForskjelligeTyperVilkår( + søkerAktørId, + behandling, + listOf(Vilkår.UTVIDET_BARNETRYGD), + ) + + val (nyInit, nyAktiv) = flyttResultaterTilInitielt( + initiellVilkårsvurdering = initUtenUtvidetVilkår, + aktivVilkårsvurdering = aktivMedUtvidetVilkår, + løpendeUnderkategori = BehandlingUnderkategori.UTVIDET, + ) + + val nyInitInnholderUtvidetVilkår = + nyInit.personResultater.first().vilkårResultater.any { vilkårResultat -> vilkårResultat.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + + Assertions.assertTrue(nyInitInnholderUtvidetVilkår) + Assertions.assertTrue(nyAktiv.personResultater.isEmpty()) + } + + @Test + fun `Skal fjerne vilkår om utvidet barnetrygd når den inneværende behandlingen gjelder ordinær, og det ikke eksisterer løpende sak med utvidet, eller utvidet-vilkåret var på forrige behandling`() { + val søkerAktørId = randomAktør() + val behandling = lagBehandling() + + val initUtenUtvidetVilkår = + lagVilkårsvurderingMedForskjelligeTyperVilkår(søkerAktørId, behandling, listOf()) + val aktivMedUtvidetVilkår = + lagVilkårsvurderingMedForskjelligeTyperVilkår( + søkerAktørId, + behandling, + listOf(Vilkår.UTVIDET_BARNETRYGD), + ) + + val (nyInit, nyAktiv) = flyttResultaterTilInitielt( + initiellVilkårsvurdering = initUtenUtvidetVilkår, + aktivVilkårsvurdering = aktivMedUtvidetVilkår, + løpendeUnderkategori = BehandlingUnderkategori.ORDINÆR, + aktørerMedUtvidetAndelerIForrigeBehandling = emptyList(), + ) + + val nyInitInnholderIkkeUtvidetVilkår = + nyInit.personResultater.first().vilkårResultater.none { vilkårResultat -> vilkårResultat.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + val nyAktivInneholderUtvidetVilkår = + nyAktiv.personResultater.first().vilkårResultater.any { vilkårResultat -> vilkårResultat.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + + Assertions.assertTrue(nyInitInnholderIkkeUtvidetVilkår) + Assertions.assertTrue(nyAktivInneholderUtvidetVilkår) + } + + @Test + fun `Skal kun kopiere over oppfylte utvidet-vilkår ved opprettelse av ny behandling, men slette alle fra aktiv`() { + val søkerAktørId = randomAktør() + val nyBehandling = lagBehandling() + val forrigeBehandling = lagBehandling() + + val initUtenUtvidetVilkår = + lagVilkårsvurderingMedForskjelligeTyperVilkår(søkerAktørId, nyBehandling, listOf()) + + val aktivVilkårsvurderingMedUtvidet = Vilkårsvurdering(behandling = forrigeBehandling) + val personResultat = + PersonResultat( + vilkårsvurdering = aktivVilkårsvurderingMedUtvidet, + aktør = søkerAktørId, + ) + val utvidetVilkårResultater = + setOf( + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + personResultat = personResultat, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.now().minusYears(2), + periodeTom = LocalDate.now().minusYears(1), + ), + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + personResultat = personResultat, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now().minusYears(1), + periodeTom = LocalDate.now(), + ), + ) + personResultat.setSortedVilkårResultater(utvidetVilkårResultater) + aktivVilkårsvurderingMedUtvidet.personResultater = setOf(personResultat) + + val (nyInit, nyAktiv) = flyttResultaterTilInitielt( + initiellVilkårsvurdering = initUtenUtvidetVilkår, + aktivVilkårsvurdering = aktivVilkårsvurderingMedUtvidet, + løpendeUnderkategori = BehandlingUnderkategori.UTVIDET, + aktørerMedUtvidetAndelerIForrigeBehandling = listOf(søkerAktørId), + ) + + val nyInitUtvidetVilkår = + nyInit.personResultater.first().vilkårResultater.single { vilkårResultat -> vilkårResultat.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + + Assertions.assertTrue(nyInitUtvidetVilkår.resultat == Resultat.OPPFYLT) + Assertions.assertTrue(nyAktiv.personResultater.isEmpty()) + } + + @Test + fun `Skal kopiere over alle utvidet-vilkår fra aktiv vilkårsvurdering hvis den aktive vilkårsvurderingen er fra den inneværende behandlingen`() { + val søkerAktørId = randomAktør() + val behandling = lagBehandling() + + val initUtenUtvidetVilkår = + lagVilkårsvurderingMedForskjelligeTyperVilkår(søkerAktørId, behandling, listOf()) + + val aktivVilkårsvurderingMedUtvidet = Vilkårsvurdering(behandling = behandling) + val personResultat = + PersonResultat( + vilkårsvurdering = aktivVilkårsvurderingMedUtvidet, + aktør = søkerAktørId, + ) + val utvidetVilkårResultater = + setOf( + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + personResultat = personResultat, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.now().minusYears(2), + periodeTom = LocalDate.now().minusYears(1), + ), + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + personResultat = personResultat, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now().minusYears(1), + periodeTom = LocalDate.now(), + ), + ) + personResultat.setSortedVilkårResultater(utvidetVilkårResultater) + aktivVilkårsvurderingMedUtvidet.personResultater = setOf(personResultat) + + val (nyInit, nyAktiv) = flyttResultaterTilInitielt( + initiellVilkårsvurdering = initUtenUtvidetVilkår, + aktivVilkårsvurdering = aktivVilkårsvurderingMedUtvidet, + løpendeUnderkategori = BehandlingUnderkategori.UTVIDET, + aktørerMedUtvidetAndelerIForrigeBehandling = listOf(søkerAktørId), + ) + + val nyInitUtvidetVilkår = + nyInit.personResultater.first().vilkårResultater.filter { vilkårResultat -> vilkårResultat.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + + Assertions.assertTrue(nyInitUtvidetVilkår.size == 2) + Assertions.assertTrue(nyAktiv.personResultater.isEmpty()) + } + + @Test + fun `Skal ikke legge til utvidet vilkåret hvis det kun eksisterer ikke-oppfylte perioder, men fortsatt slette fra aktiv`() { + val søkerAktørId = randomAktør() + val nyBehandling = lagBehandling() + val forrigeBehandling = lagBehandling() + + val initUtenUtvidetVilkår = + lagVilkårsvurderingMedForskjelligeTyperVilkår(søkerAktørId, nyBehandling, listOf()) + + val aktivVilkårsvurderingMedUtvidetIkkeOppfylt = Vilkårsvurdering(behandling = forrigeBehandling) + val personResultat = + PersonResultat( + vilkårsvurdering = aktivVilkårsvurderingMedUtvidetIkkeOppfylt, + aktør = søkerAktørId, + ) + val utvidetVilkårResultater = + setOf( + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + personResultat = personResultat, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.now().minusYears(2), + periodeTom = LocalDate.now().minusYears(1), + ), + lagVilkårResultat( + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + personResultat = personResultat, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.now().minusYears(1), + periodeTom = LocalDate.now(), + ), + ) + personResultat.setSortedVilkårResultater(utvidetVilkårResultater) + aktivVilkårsvurderingMedUtvidetIkkeOppfylt.personResultater = setOf(personResultat) + + val (nyInit, nyAktiv) = flyttResultaterTilInitielt( + initiellVilkårsvurdering = initUtenUtvidetVilkår, + aktivVilkårsvurdering = aktivVilkårsvurderingMedUtvidetIkkeOppfylt, + løpendeUnderkategori = BehandlingUnderkategori.UTVIDET, + aktørerMedUtvidetAndelerIForrigeBehandling = emptyList(), + ) + + val nyInitInneholderIkkeUtvidetVilkår = + nyInit.personResultater.first().vilkårResultater.none { vilkårResultat -> vilkårResultat.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + + Assertions.assertTrue(nyInitInneholderIkkeUtvidetVilkår) + Assertions.assertTrue(nyAktiv.personResultater.isEmpty()) + } + + @Test + fun `Skal beholde andreVurderinger lagt til på inneværende behandling`() { + val søkerAktørId = randomAktør() + val nyBehandling = lagBehandling() + + val initiellVilkårsvurderingUtenAndreVurderinger = + lagBasicVilkårsvurdering( + behandling = nyBehandling, + personer = listOf( + lagPerson(type = PersonType.SØKER, aktør = søkerAktørId), + lagPerson(type = PersonType.BARN), + ), + ) + val aktivVilkårsvurdering = initiellVilkårsvurderingUtenAndreVurderinger.copy() + aktivVilkårsvurdering.personResultater.find { it.erSøkersResultater() }!! + .leggTilBlankAnnenVurdering(AnnenVurderingType.OPPLYSNINGSPLIKT) + + val (nyInit, nyAktiv) = flyttResultaterTilInitielt( + initiellVilkårsvurdering = initiellVilkårsvurderingUtenAndreVurderinger, + aktivVilkårsvurdering = aktivVilkårsvurdering, + ) + + val nyInitInnholderOpplysningspliktVilkår = nyInit.personResultater.find { it.erSøkersResultater() }!!.andreVurderinger + .any { it.type == AnnenVurderingType.OPPLYSNINGSPLIKT } + + Assertions.assertTrue(nyInitInnholderOpplysningspliktVilkår) + Assertions.assertTrue(nyAktiv.personResultater.isEmpty()) + } + + fun lagVilkårsvurderingMedForskjelligeTyperVilkår( + søkerAktør: Aktør, + behandling: Behandling, + vilkår: List, + ): Vilkårsvurdering { + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + val personResultat = + PersonResultat(vilkårsvurdering = vilkårsvurdering, aktør = søkerAktør) + val vilkårResultater = + vilkår.map { lagVilkårResultat(vilkårType = it, personResultat = personResultat) }.toSet() + personResultat.setSortedVilkårResultater(vilkårResultater) + vilkårsvurdering.personResultater = setOf(personResultat) + return vilkårsvurdering + } + + fun lagVilkårsvurdering(fnrAktør: List>, behandling: Behandling): Vilkårsvurdering { + val vilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + ) + + vilkårsvurdering.personResultater = fnrAktør.map { + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = it.second, + ) + + personResultat.setSortedVilkårResultater( + setOf( + VilkårResultat( + personResultat = personResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now(), + periodeTom = LocalDate.now(), + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + VilkårResultat( + personResultat = personResultat, + vilkårType = Vilkår.GIFT_PARTNERSKAP, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now(), + periodeTom = LocalDate.now(), + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + ), + ) + + personResultat + }.toSet() + + return vilkårsvurdering + } + + fun lagVilkårsvurderingResultatB(fnrAktør: List>, behandling: Behandling): Vilkårsvurdering { + val vilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + ) + + vilkårsvurdering.personResultater = fnrAktør.map { + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = it.second, + ) + + personResultat.setSortedVilkårResultater( + setOf( + VilkårResultat( + personResultat = personResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now(), + periodeTom = LocalDate.now(), + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + VilkårResultat( + personResultat = personResultat, + vilkårType = Vilkår.GIFT_PARTNERSKAP, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now(), + periodeTom = LocalDate.now(), + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + VilkårResultat( + personResultat = personResultat, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now(), + periodeTom = LocalDate.now(), + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ), + ), + ) + personResultat + }.toSet() + + return vilkårsvurdering + } + + fun lagBasicVilkårsvurdering(behandling: Behandling, personer: List): Vilkårsvurdering { + val vilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + ) + + val personResultater = personer.map { person -> + genererPersonResultatForPerson( + vilkårsvurdering = vilkårsvurdering, + person = person, + ) + }.toSet() + + vilkårsvurdering.personResultater = personResultater + + return vilkårsvurdering + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rTest.kt" new file mode 100644 index 000000000..6e3ad0802 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rTest.kt" @@ -0,0 +1,194 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class VilkårTest { + + @Nested + inner class `Hent relevante vilkår for persontype BARN` { + @Test + fun `For ordinær nasjonal sak`() { + val relevanteVilkår = Vilkår.hentVilkårFor( + personType = PersonType.BARN, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ) + val vilkårForBarn = setOf( + Vilkår.UNDER_18_ÅR, + Vilkår.BOR_MED_SØKER, + Vilkår.GIFT_PARTNERSKAP, + Vilkår.BOSATT_I_RIKET, + Vilkår.LOVLIG_OPPHOLD, + ) + Assertions.assertEquals(vilkårForBarn, relevanteVilkår) + } + + @Test + fun `For utvidet nasjonal sak`() { + val relevanteVilkår = Vilkår.hentVilkårFor( + personType = PersonType.BARN, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + ) + val vilkårForBarn = setOf( + Vilkår.UNDER_18_ÅR, + Vilkår.BOR_MED_SØKER, + Vilkår.GIFT_PARTNERSKAP, + Vilkår.BOSATT_I_RIKET, + Vilkår.LOVLIG_OPPHOLD, + ) + Assertions.assertEquals(vilkårForBarn, relevanteVilkår) + } + + @Test + fun `For ordinær institusjonssak`() { + val relevanteVilkår = Vilkår.hentVilkårFor( + personType = PersonType.BARN, + fagsakType = FagsakType.INSTITUSJON, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ) + val vilkårForBarn = setOf( + Vilkår.UNDER_18_ÅR, + Vilkår.BOR_MED_SØKER, + Vilkår.GIFT_PARTNERSKAP, + Vilkår.BOSATT_I_RIKET, + Vilkår.LOVLIG_OPPHOLD, + ) + Assertions.assertEquals(vilkårForBarn, relevanteVilkår) + } + + @Test + fun `For utvidet institusjonssak`() { + val relevanteVilkår = Vilkår.hentVilkårFor( + personType = PersonType.BARN, + fagsakType = FagsakType.INSTITUSJON, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + ) + val vilkårForBarn = setOf( + Vilkår.UNDER_18_ÅR, + Vilkår.BOR_MED_SØKER, + Vilkår.GIFT_PARTNERSKAP, + Vilkår.BOSATT_I_RIKET, + Vilkår.LOVLIG_OPPHOLD, + ) + Assertions.assertEquals(vilkårForBarn, relevanteVilkår) + } + + @Test + fun `For ordinær enslig mindreårig sak`() { + val relevanteVilkår = Vilkår.hentVilkårFor( + personType = PersonType.BARN, + fagsakType = FagsakType.BARN_ENSLIG_MINDREÅRIG, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ) + val vilkårForBarn = setOf( + Vilkår.UNDER_18_ÅR, + Vilkår.BOR_MED_SØKER, + Vilkår.GIFT_PARTNERSKAP, + Vilkår.BOSATT_I_RIKET, + Vilkår.LOVLIG_OPPHOLD, + ) + Assertions.assertEquals(vilkårForBarn, relevanteVilkår) + } + + @Test + fun `For utvidet enslig mindreårig sak`() { + val relevanteVilkår = Vilkår.hentVilkårFor( + personType = PersonType.BARN, + fagsakType = FagsakType.BARN_ENSLIG_MINDREÅRIG, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + ) + val vilkårForBarn = setOf( + Vilkår.UNDER_18_ÅR, + Vilkår.BOR_MED_SØKER, + Vilkår.GIFT_PARTNERSKAP, + Vilkår.BOSATT_I_RIKET, + Vilkår.LOVLIG_OPPHOLD, + Vilkår.UTVIDET_BARNETRYGD, + ) + Assertions.assertEquals(vilkårForBarn, relevanteVilkår) + } + } + + @Nested + inner class `Hent relevante vilkår for persontype SØKER` { + @Test + fun `For ordinær nasjonal sak`() { + val relevanteVilkår = Vilkår.hentVilkårFor( + personType = PersonType.SØKER, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ) + val vilkårForBarn = setOf( + Vilkår.BOSATT_I_RIKET, + Vilkår.LOVLIG_OPPHOLD, + ) + Assertions.assertEquals(vilkårForBarn, relevanteVilkår) + } + + @Test + fun `For utvidet nasjonal sak`() { + val relevanteVilkår = Vilkår.hentVilkårFor( + personType = PersonType.SØKER, + fagsakType = FagsakType.NORMAL, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + ) + val vilkårForBarn = setOf( + Vilkår.BOSATT_I_RIKET, + Vilkår.LOVLIG_OPPHOLD, + Vilkår.UTVIDET_BARNETRYGD, + ) + Assertions.assertEquals(vilkårForBarn, relevanteVilkår) + } + + @Test + fun `For ordinær institusjonssak`() { + val relevanteVilkår = Vilkår.hentVilkårFor( + personType = PersonType.SØKER, + fagsakType = FagsakType.INSTITUSJON, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ) + val vilkårForBarn = emptySet() + Assertions.assertEquals(vilkårForBarn, relevanteVilkår) + } + + @Test + fun `For utvidet institusjonssak`() { + val relevanteVilkår = Vilkår.hentVilkårFor( + personType = PersonType.SØKER, + fagsakType = FagsakType.INSTITUSJON, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + ) + val vilkårForBarn = emptySet() + Assertions.assertEquals(vilkårForBarn, relevanteVilkår) + } + + @Test + fun `For ordinær enslig mindreårig sak`() { + val relevanteVilkår = Vilkår.hentVilkårFor( + personType = PersonType.SØKER, + fagsakType = FagsakType.BARN_ENSLIG_MINDREÅRIG, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + ) + val vilkårForBarn = emptySet() + Assertions.assertEquals(vilkårForBarn, relevanteVilkår) + } + + @Test + fun `For utvidet enslig mindreårig sak`() { + val relevanteVilkår = Vilkår.hentVilkårFor( + personType = PersonType.SØKER, + fagsakType = FagsakType.BARN_ENSLIG_MINDREÅRIG, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + ) + val vilkårForBarn = emptySet() + Assertions.assertEquals(vilkårForBarn, relevanteVilkår) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rVurderingMatcherTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rVurderingMatcherTest.kt" new file mode 100644 index 000000000..018d76b36 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rVurderingMatcherTest.kt" @@ -0,0 +1,44 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class VilkårVurderingMatcherTest { + + private val randomBehandling = lagBehandling() + private val søkerFnr = randomFnr() + private val søkerAktørId = randomAktør() + + private fun likeVilkårResultater(a: VilkårResultat?, b: VilkårResultat?): Boolean = + a?.vilkårType == b?.vilkårType && + a?.resultat == b?.resultat && + a?.periodeFom == b?.periodeFom && + a?.periodeTom == b?.periodeTom && + a?.begrunnelse == b?.begrunnelse && + a?.erEksplisittAvslagPåSøknad == b?.erEksplisittAvslagPåSøknad + + @Test + fun `Kopierte vilkår matches og returneres som par`() { + val vilkårsvurderingOriginal = lagVilkårsvurdering(søkerAktørId, randomBehandling, Resultat.OPPFYLT) + val vilkårsvurderingKopi = vilkårsvurderingOriginal.kopier() + val vilkårPar = VilkårsvurderingService.matchVilkårResultater(vilkårsvurderingOriginal, vilkårsvurderingKopi) + assertTrue(vilkårPar.all { likeVilkårResultater(it.first, it.second) }) + } + + @Test + fun `Vilkår som kun finnes i den ene vilkårsvurderingen returneres alene`() { + val vilkårsvurderingOriginal = lagVilkårsvurdering(søkerAktørId, randomBehandling, Resultat.OPPFYLT) + val vilkårsvurderingUlik = lagVilkårsvurdering(søkerAktørId, randomBehandling, Resultat.IKKE_OPPFYLT) + val vilkårPar = VilkårsvurderingService.matchVilkårResultater(vilkårsvurderingOriginal, vilkårsvurderingUlik) + assertTrue(vilkårPar.any { it.first == null && it.second != null }) + assertTrue(vilkårPar.any { it.first != null && it.second == null }) + assertTrue(vilkårPar.none { it.first == null && it.second == null }) + assertTrue(vilkårPar.none { likeVilkårResultater(it.first, it.second) }) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingForskyvningUtilsTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingForskyvningUtilsTest.kt" new file mode 100644 index 000000000..95bcfa70a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingForskyvningUtilsTest.kt" @@ -0,0 +1,469 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.isSameOrBefore +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.common.til18ÅrsVilkårsdato +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.Tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.neste +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonth +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.tilYearMonthEllerNull +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.beskjærPå18År +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.tilForskjøvetTidslinje +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.tilForskjøvetTidslinjeForOppfyltVilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.tilForskjøvetTidslinjerForHvertOppfylteVilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingForskyvningUtils.tilTidslinjeForSplitt +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.Month +import java.time.YearMonth + +class VilkårsvurderingForskyvningUtilsTest { + + @Test + fun `Skal lage riktig splitt når bor med søker går fra delt bosted til fullt`() { + val fom = LocalDate.now().minusMonths(7).førsteDagIInneværendeMåned() + val deltBostedTom = LocalDate.now().minusMonths(1).sisteDagIMåned() + val barnets18årsdag = LocalDate.now().plusYears(14) + + val vilkårResultater = lagVilkårForPerson( + fom = fom, + tom = null, + spesielleVilkår = setOf( + lagVilkårResultat( + periodeFom = fom, + periodeTom = deltBostedTom, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + lagVilkårResultat( + periodeFom = deltBostedTom.plusMonths(1).førsteDagIInneværendeMåned(), + periodeTom = null, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + ), + ), + maksTom = barnets18årsdag, + ) + + val tidslinjer = vilkårResultater.tilForskjøvetTidslinjerForHvertOppfylteVilkår(barnets18årsdag.minusYears(18)) + + Assertions.assertEquals(5, tidslinjer.size) + + val borMedSøkerTidslinje = tidslinjer.first() + val borMedSøkerPerioder = borMedSøkerTidslinje.perioder() + + Assertions.assertEquals(2, borMedSøkerPerioder.size) + + val deltBostedPeriode = borMedSøkerPerioder.first() + val fullPeriode = borMedSøkerPerioder.last() + + Assertions.assertEquals(fom.plusMonths(1).toYearMonth(), deltBostedPeriode.fraOgMed.tilYearMonth()) + Assertions.assertEquals(deltBostedTom.toYearMonth(), deltBostedPeriode.tilOgMed.tilYearMonth()) + Assertions.assertEquals(deltBostedTom.plusMonths(1).toYearMonth(), fullPeriode.fraOgMed.tilYearMonth()) + Assertions.assertNull(fullPeriode.tilOgMed.tilYearMonthEllerNull()) + + tidslinjer.subList(1, tidslinjer.size).forEach { + Assertions.assertEquals(1, it.perioder().size) + val periode = it.perioder().first() + Assertions.assertEquals(fom.plusMonths(1).toYearMonth(), periode.fraOgMed.tilYearMonth()) + Assertions.assertNull(fullPeriode.tilOgMed.tilYearMonthEllerNull()) + } + } + + @Test + fun `Skal lage riktig splitt når bor med søker går fra fullt til delt bosted`() { + val fom = LocalDate.now().minusMonths(7).førsteDagIInneværendeMåned() + val deltBostedTom = LocalDate.now().minusMonths(1).sisteDagIMåned() + val barnets18årsdag = LocalDate.now().plusYears(14) + + val vilkårResultater = lagVilkårForPerson( + fom = fom, + tom = null, + spesielleVilkår = setOf( + lagVilkårResultat( + periodeFom = fom, + periodeTom = deltBostedTom, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + ), + lagVilkårResultat( + periodeFom = deltBostedTom.plusMonths(1).førsteDagIInneværendeMåned(), + periodeTom = null, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + ), + maksTom = barnets18årsdag, + ) + + val tidslinjer = vilkårResultater.tilForskjøvetTidslinjerForHvertOppfylteVilkår(barnets18årsdag.minusYears(18)) + + Assertions.assertEquals(5, tidslinjer.size) + + val borMedSøkerTidslinje = tidslinjer.first() + val borMedSøkerPerioder = borMedSøkerTidslinje.perioder() + + Assertions.assertEquals(2, borMedSøkerPerioder.size) + + val deltBostedPeriode = borMedSøkerPerioder.first() + val fullPeriode = borMedSøkerPerioder.last() + + Assertions.assertEquals(fom.plusMonths(1).toYearMonth(), deltBostedPeriode.fraOgMed.tilYearMonth()) + Assertions.assertEquals(deltBostedTom.plusMonths(1).toYearMonth(), deltBostedPeriode.tilOgMed.tilYearMonth()) + Assertions.assertEquals(deltBostedTom.plusMonths(2).toYearMonth(), fullPeriode.fraOgMed.tilYearMonth()) + Assertions.assertNull(fullPeriode.tilOgMed.tilYearMonthEllerNull()) + + tidslinjer.subList(1, tidslinjer.size).forEach { + Assertions.assertEquals(1, it.perioder().size) + val periode = it.perioder().first() + Assertions.assertEquals(fom.plusMonths(1).toYearMonth(), periode.fraOgMed.tilYearMonth()) + Assertions.assertNull(fullPeriode.tilOgMed.tilYearMonthEllerNull()) + } + } + + @Test + fun `Skal lage riktig splitt når bor med søker er oppfylt i to back2back-perioder uten utdypende vilkårsvurdering`() { + val fom = LocalDate.now().minusMonths(7).førsteDagIInneværendeMåned() + val b2bTom = LocalDate.now().minusMonths(1).sisteDagIMåned() + val barnets18årsdag = LocalDate.now().plusYears(14) + + val vilkårResultater = lagVilkårForPerson( + fom = fom, + tom = null, + spesielleVilkår = setOf( + lagVilkårResultat( + periodeFom = fom, + periodeTom = b2bTom, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + ), + lagVilkårResultat( + periodeFom = b2bTom.plusMonths(1).førsteDagIInneværendeMåned(), + periodeTom = null, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + ), + ), + maksTom = barnets18årsdag, + ) + + val tidslinjer = vilkårResultater.tilForskjøvetTidslinjerForHvertOppfylteVilkår(barnets18årsdag.minusYears(18)) + + Assertions.assertEquals(5, tidslinjer.size) + + val borMedSøkerTidslinje = tidslinjer.first() + val borMedSøkerPerioder = borMedSøkerTidslinje.perioder() + + Assertions.assertEquals(2, borMedSøkerPerioder.size) + + val førstePeriode = borMedSøkerPerioder.first() + val andrePeriode = borMedSøkerPerioder.last() + + Assertions.assertEquals(fom.plusMonths(1).toYearMonth(), førstePeriode.fraOgMed.tilYearMonth()) + Assertions.assertEquals(b2bTom.toYearMonth(), førstePeriode.tilOgMed.tilYearMonth()) + Assertions.assertEquals(b2bTom.plusMonths(1).toYearMonth(), andrePeriode.fraOgMed.tilYearMonth()) + Assertions.assertNull(andrePeriode.tilOgMed.tilYearMonthEllerNull()) + + tidslinjer.subList(1, tidslinjer.size).forEach { + Assertions.assertEquals(1, it.perioder().size) + val periode = it.perioder().first() + Assertions.assertEquals(fom.plusMonths(1).toYearMonth(), periode.fraOgMed.tilYearMonth()) + Assertions.assertNull(andrePeriode.tilOgMed.tilYearMonthEllerNull()) + } + } + + @Test + fun `Skal forskyve UNDER_18 tidslinjen og beskjære den måneden før 18-årsdag`() { + val barn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2022, Month.DECEMBER, 1).minusYears(18)) + + val under18VilkårResultat = listOf( + lagVilkårResultat( + periodeFom = barn.fødselsdato, + periodeTom = barn.fødselsdato.plusYears(18).minusDays(1), + vilkårType = Vilkår.UNDER_18_ÅR, + resultat = Resultat.OPPFYLT, + ), + ) + + val forskjøvetTidslinje = + under18VilkårResultat.tilForskjøvetTidslinjeForOppfyltVilkår(vilkår = Vilkår.UNDER_18_ÅR, barn.fødselsdato) + + val forskjøvedePerioder = forskjøvetTidslinje.perioder() + + Assertions.assertEquals(1, forskjøvedePerioder.size) + + val forskjøvetPeriode = forskjøvedePerioder.single() + + Assertions.assertEquals(barn.fødselsdato.plusMonths(1).toYearMonth(), forskjøvetPeriode.fraOgMed.tilYearMonth()) + Assertions.assertEquals( + barn.fødselsdato.plusYears(18).minusMonths(1).toYearMonth(), + forskjøvetPeriode.tilOgMed.tilYearMonth(), + ) + } + + @Test + fun `Skal forskyve UNDER_18 tidslinjen, men ikke kutte den måneden før hvis til og med dato ikke er i måneden hen fyller 18`() { + val barn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2022, Month.DECEMBER, 1).minusYears(18)) + + val tomDato = barn.fødselsdato.plusYears(7) + + val under18VilkårResultat = listOf( + lagVilkårResultat( + periodeFom = barn.fødselsdato, + periodeTom = tomDato, + vilkårType = Vilkår.UNDER_18_ÅR, + resultat = Resultat.OPPFYLT, + ), + ) + + val forskjøvetTidslinje = + under18VilkårResultat.tilForskjøvetTidslinjeForOppfyltVilkår(vilkår = Vilkår.UNDER_18_ÅR, barn.fødselsdato) + + val forskjøvedePerioder = forskjøvetTidslinje.perioder() + + Assertions.assertEquals(1, forskjøvedePerioder.size) + + val forskjøvetPeriode = forskjøvedePerioder.single() + + Assertions.assertEquals(barn.fødselsdato.plusMonths(1).toYearMonth(), forskjøvetPeriode.fraOgMed.tilYearMonth()) + Assertions.assertEquals(tomDato.toYearMonth(), forskjøvetPeriode.tilOgMed.tilYearMonth()) + } + + @Test + fun `Skal gi tom liste og ikke kaste feil dersom vi ikke sender med noen vilkårresultater`() { + val barn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2022, Month.DECEMBER, 1).minusYears(18)) + + val forskjøvedeOppfylteVilkårTomListe = emptyList().tilForskjøvetTidslinjeForOppfyltVilkår( + vilkår = Vilkår.UNDER_18_ÅR, + fødselsdato = barn.fødselsdato, + ).perioder() + Assertions.assertEquals(forskjøvedeOppfylteVilkårTomListe.size, 0) + + val forskjøvedeVilkårTomListe = emptyList().tilForskjøvetTidslinje( + vilkår = Vilkår.UNDER_18_ÅR, + fødselsdato = barn.fødselsdato, + ).perioder() + Assertions.assertEquals(forskjøvedeVilkårTomListe.size, 0) + } + + @Test + fun `Skal kutte UNDER_18 tidslinjen måneden før 18-årsdag`() { + val barn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2022, Month.DECEMBER, 1).minusYears(18)) + + val under18VilkårResultat = listOf( + lagVilkårResultat( + periodeFom = barn.fødselsdato, + periodeTom = barn.fødselsdato.plusYears(18).minusDays(1), + vilkårType = Vilkår.UNDER_18_ÅR, + resultat = Resultat.OPPFYLT, + ), + ) + + val under18årVilkårTidslinje: Tidslinje = tidslinje { + under18VilkårResultat.map { + Periode( + fraOgMed = it.periodeFom!!.tilMånedTidspunkt().neste(), + tilOgMed = it.periodeTom!!.tilMånedTidspunkt(), + innhold = it, + ) + } + } + + val under18PerioderFørBeskjæring = under18årVilkårTidslinje.perioder() + + Assertions.assertEquals(1, under18PerioderFørBeskjæring.size) + Assertions.assertEquals( + barn.fødselsdato.plusMonths(1).toYearMonth(), + under18PerioderFørBeskjæring.first().fraOgMed.tilYearMonth(), + ) + Assertions.assertEquals( + YearMonth.of(2022, Month.NOVEMBER), + under18PerioderFørBeskjæring.first().tilOgMed.tilYearMonth(), + ) + + val tidslinjeBeskåret = under18årVilkårTidslinje.beskjærPå18År(barn.fødselsdato) + + val under18PerioderEtterBeskjæring = tidslinjeBeskåret.perioder() + + Assertions.assertEquals(1, under18PerioderEtterBeskjæring.size) + Assertions.assertEquals( + barn.fødselsdato.plusMonths(1).toYearMonth(), + under18PerioderEtterBeskjæring.first().fraOgMed.tilYearMonth(), + ) + Assertions.assertEquals( + barn.fødselsdato.plusYears(18).minusMonths(1).toYearMonth(), + under18PerioderEtterBeskjæring.first().tilOgMed.tilYearMonth(), + ) + } + + @Test + fun `Skal lage korrekt tidslinje for splitting av vedtaksperioder`() { + val februar2020 = YearMonth.of(2020, 2) + val oktober2020 = YearMonth.of(2020, 10) + val mars2021 = YearMonth.of(2021, 3) + val desember2021 = YearMonth.of(2021, 12) + val mai2022 = YearMonth.of(2022, 5) + + val søker = lagPerson(type = PersonType.SØKER) + val barn1 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2015, 5, 6)) + val barn2 = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2019, 9, 7)) + + val vilkårsvurdering = lagVilkårsvurdering( + søkerAktør = søker.aktør, + behandling = lagBehandling(), + resultat = Resultat.OPPFYLT, + ) + + val personResultatSøker = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = søker.aktør, + ) + + personResultatSøker.setSortedVilkårResultater( + lagVilkårForPerson( + personResultat = personResultatSøker, + fom = februar2020.førsteDagIInneværendeMåned(), + tom = null, + generiskeVilkår = listOf(Vilkår.LOVLIG_OPPHOLD, Vilkår.BOSATT_I_RIKET), + ), + ) + + val personResultatBarn1 = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn1.aktør, + ) + + personResultatBarn1.setSortedVilkårResultater( + lagVilkårForPerson( + fom = oktober2020.førsteDagIInneværendeMåned(), + tom = null, + maksTom = barn1.fødselsdato.til18ÅrsVilkårsdato(), + spesielleVilkår = setOf( + lagVilkårResultat( + periodeFom = oktober2020.førsteDagIInneværendeMåned(), + periodeTom = desember2021.sisteDagIInneværendeMåned(), + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + lagVilkårResultat( + periodeFom = desember2021.plusMonths(1).førsteDagIInneværendeMåned(), + periodeTom = null, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + ), + ), + ), + ) + + val personResultatBarn2 = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = barn2.aktør, + ) + + personResultatBarn2.setSortedVilkårResultater( + lagVilkårForPerson( + fom = oktober2020.førsteDagIInneværendeMåned(), + tom = null, + maksTom = barn1.fødselsdato.til18ÅrsVilkårsdato(), + generiskeVilkår = listOf( + Vilkår.BOSATT_I_RIKET, + Vilkår.BOR_MED_SØKER, + Vilkår.UNDER_18_ÅR, + Vilkår.GIFT_PARTNERSKAP, + ), + spesielleVilkår = setOf( + lagVilkårResultat( + periodeFom = mars2021.førsteDagIInneværendeMåned(), + periodeTom = mai2022.sisteDagIInneværendeMåned(), + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + ), + lagVilkårResultat( + periodeFom = mai2022.plusMonths(1).førsteDagIInneværendeMåned(), + periodeTom = null, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + ), + ), + ), + ) + + val personResultater = setOf(personResultatSøker, personResultatBarn1, personResultatBarn2) + + val tidslinje = personResultater.tilTidslinjeForSplitt( + personerIPersongrunnlag = listOf(søker, barn1, barn2), + fagsakType = FagsakType.NORMAL, + ) + + val perioder = tidslinje.perioder() + + val perioderRelevantForTesting = perioder.filter { it.fraOgMed.tilYearMonth().isSameOrBefore(mai2022) } + + Assertions.assertEquals(4, perioderRelevantForTesting.size) + + val periode1 = perioderRelevantForTesting[0] + val periode2 = perioderRelevantForTesting[1] + val periode3 = perioderRelevantForTesting[2] + val periode4 = perioderRelevantForTesting[3] + + assertPeriode(periode = periode1, forventetFom = februar2020.plusMonths(1), forventetTom = oktober2020) + assertPeriode(periode = periode2, forventetFom = oktober2020.plusMonths(1), forventetTom = mars2021) + assertPeriode(periode = periode3, forventetFom = mars2021.plusMonths(1), forventetTom = desember2021) + assertPeriode(periode = periode4, forventetFom = desember2021.plusMonths(1), forventetTom = mai2022) + } + + private fun assertPeriode( + periode: Periode, Måned>, + forventetFom: YearMonth, + forventetTom: YearMonth, + ) { + Assertions.assertEquals(forventetFom, periode.fraOgMed.tilYearMonth()) + Assertions.assertEquals(forventetTom, periode.tilOgMed.tilYearMonth()) + } + + private fun lagVilkårForPerson( + fom: LocalDate, + tom: LocalDate? = null, + spesielleVilkår: Set = emptySet(), + generiskeVilkår: List = listOf( + Vilkår.BOSATT_I_RIKET, + Vilkår.LOVLIG_OPPHOLD, + Vilkår.UNDER_18_ÅR, + Vilkår.GIFT_PARTNERSKAP, + ), + maksTom: LocalDate? = null, + personResultat: PersonResultat? = null, + ): Set { + return spesielleVilkår + generiskeVilkår.map { + lagVilkårResultat( + periodeFom = fom, + periodeTom = if (it == Vilkår.UNDER_18_ÅR) maksTom else tom, + vilkårType = it, + resultat = Resultat.OPPFYLT, + personResultat = personResultat, + ) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingServiceTest.kt" new file mode 100644 index 000000000..62f5d3344 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingServiceTest.kt" @@ -0,0 +1,106 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.integrasjoner.sanity.SanityService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import org.hamcrest.MatcherAssert.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.time.LocalDate +import org.hamcrest.CoreMatchers.`is` as Is + +@ExtendWith(MockKExtension::class) +internal class VilkårsvurderingServiceTest { + + @MockK + private lateinit var sanityService: SanityService + + @MockK + private lateinit var vilkårsvurderingRepository: VilkårsvurderingRepository + + @InjectMockKs + private lateinit var vilkårsvurderingService: VilkårsvurderingService + + @Test + fun `oppdaterVilkårVedDødsfall skal sette tom dato til dødsfallsdato dersom dødsfallsdato er tidligere enn nåværende tom`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.DØDSFALL_BRUKER) + val aktør = randomAktør() + val vilkårFomDato = LocalDate.of(2000, 1, 1) + val vilkårTomDato = LocalDate.of(2020, 1, 1) + val dødsfallsDato = LocalDate.of(2015, 1, 1) + + val vilkårsvurdering = lagVilkårsvurdering( + søkerAktør = aktør, + behandling = behandling, + resultat = Resultat.IKKE_VURDERT, + søkerPeriodeFom = vilkårFomDato, + søkerPeriodeTom = vilkårTomDato, + ) + + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(behandlingId = behandling.id) } returns vilkårsvurdering + + vilkårsvurderingService.oppdaterVilkårVedDødsfall(behandlingId = BehandlingId(behandling.id), dødsfallsDato, aktør) + + val vilkårResultater = vilkårsvurdering.personResultater.single().vilkårResultater + + assertThat(vilkårResultater.all { it.periodeTom == dødsfallsDato }, Is(true)) + } + + @Test + fun `oppdaterVilkårVedDødsfall skal ikke sette tom dato til dødsfallsdato dersom dødsfallsdato er senere enn nåværende tom`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.DØDSFALL_BRUKER) + val aktør = randomAktør() + val vilkårFomDato = LocalDate.of(2000, 1, 1) + val vilkårTomDato = LocalDate.of(2020, 1, 1) + val dødsfallsDato = LocalDate.of(2022, 1, 1) + + val vilkårsvurdering = lagVilkårsvurdering( + søkerAktør = aktør, + behandling = behandling, + resultat = Resultat.IKKE_VURDERT, + søkerPeriodeFom = vilkårFomDato, + søkerPeriodeTom = vilkårTomDato, + ) + + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(behandlingId = behandling.id) } returns vilkårsvurdering + + vilkårsvurderingService.oppdaterVilkårVedDødsfall(behandlingId = BehandlingId(behandling.id), dødsfallsDato, aktør) + + val vilkårResultater = vilkårsvurdering.personResultater.single().vilkårResultater + + assertThat(vilkårResultater.all { it.periodeTom == vilkårTomDato }, Is(true)) + } + + @Test + fun `oppdaterVilkårVedDødsfall skal ikke sette tom dato til dødsfallsdato dersom tom dato ikke allerede er satt`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.DØDSFALL_BRUKER) + val aktør = randomAktør() + val vilkårFomDato = LocalDate.of(2000, 1, 1) + val dødsfallsDato = LocalDate.of(2022, 1, 1) + + val vilkårsvurdering = lagVilkårsvurdering( + søkerAktør = aktør, + behandling = behandling, + resultat = Resultat.IKKE_VURDERT, + søkerPeriodeFom = vilkårFomDato, + søkerPeriodeTom = null, + ) + + every { vilkårsvurderingRepository.findByBehandlingAndAktiv(behandlingId = behandling.id) } returns vilkårsvurdering + + vilkårsvurderingService.oppdaterVilkårVedDødsfall(behandlingId = BehandlingId(behandling.id), dødsfallsDato, aktør) + + val vilkårResultater = vilkårsvurdering.personResultater.single().vilkårResultater + + assertThat(vilkårResultater.all { it.periodeTom == null }, Is(true)) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingStegUtilsTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingStegUtilsTest.kt" new file mode 100644 index 000000000..4b3b39ef0 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingStegUtilsTest.kt" @@ -0,0 +1,512 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.Periode +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.toPeriode +import no.nav.familie.ba.sak.ekstern.restDomene.RestVilkårResultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat.Companion.VilkårResultatComparator +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.LocalDateTime + +class VilkårsvurderingStegUtilsTest { + + lateinit var vilkårResultat1: VilkårResultat + lateinit var vilkårResultat2: VilkårResultat + lateinit var vilkårResultat3: VilkårResultat + lateinit var vilkårsvurdering: Vilkårsvurdering + lateinit var personResultat: PersonResultat + lateinit var vilkår: Vilkår + lateinit var resultat: Resultat + lateinit var behandling: Behandling + + @BeforeEach + fun init() { + val personAktørId = randomAktør() + + behandling = lagBehandling() + + vilkår = Vilkår.BOR_MED_SØKER + resultat = Resultat.OPPFYLT + + vilkårsvurdering = lagVilkårsvurdering(personAktørId, behandling, resultat) + + personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = personAktørId, + ) + + vilkårResultat1 = VilkårResultat( + id = 1, + personResultat = personResultat, + vilkårType = vilkår, + resultat = resultat, + resultatBegrunnelse = null, + periodeFom = LocalDate.of(2010, 1, 1), + periodeTom = LocalDate.of(2010, 6, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ) + vilkårResultat2 = VilkårResultat( + id = 2, + personResultat = personResultat, + vilkårType = vilkår, + resultat = resultat, + resultatBegrunnelse = null, + periodeFom = LocalDate.of(2010, 6, 2), + periodeTom = LocalDate.of(2010, 8, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ) + vilkårResultat3 = VilkårResultat( + id = 3, + personResultat = personResultat, + vilkårType = vilkår, + resultat = resultat, + resultatBegrunnelse = null, + periodeFom = LocalDate.of(2010, 8, 2), + periodeTom = LocalDate.of(2010, 12, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ) + personResultat.setSortedVilkårResultater( + setOf( + vilkårResultat1, + vilkårResultat2, + vilkårResultat3, + ).toSortedSet(VilkårResultatComparator), + ) + } + + private fun assertPeriode(expected: Periode, actual: Periode) { + assertEquals(expected.fom, actual.fom) + assertEquals(expected.tom, actual.tom) + } + + @Test + fun `periode erstattes dersom en periode med overlappende tidsintervall legges til`() { + val restVilkårResultat = RestVilkårResultat( + 2, vilkår, resultat, + LocalDate.of(2010, 6, 2), LocalDate.of(2011, 9, 1), + "", + "", + LocalDateTime.now(), + behandling.id, + ) + VilkårsvurderingUtils.muterPersonVilkårResultaterPut( + personResultat, + restVilkårResultat, + ) + + assertEquals(2, personResultat.vilkårResultater.size) + assertPeriode( + Periode( + LocalDate.of(2010, 1, 1), + LocalDate.of(2010, 6, 1), + ), + personResultat.getSortedVilkårResultat(0)!!.toPeriode(), + ) + + assertPeriode( + Periode( + LocalDate.of(2010, 6, 2), + LocalDate.of(2011, 9, 1), + ), + personResultat.getSortedVilkårResultat(1)!!.toPeriode(), + ) + } + + @Test + fun `periode splittes dersom en periode med inneklemt tidsintervall legges til`() { + val restVilkårResultat = RestVilkårResultat( + 2, vilkår, resultat, + LocalDate.of(2010, 3, 5), LocalDate.of(2010, 5, 20), + "", + "", + LocalDateTime.now(), + behandling.id, + ) + + VilkårsvurderingUtils.muterPersonVilkårResultaterPut( + personResultat, + restVilkårResultat, + ) + + assertEquals(4, personResultat.vilkårResultater.size) + assertPeriode( + Periode( + LocalDate.of(2010, 1, 1), + LocalDate.of(2010, 3, 4), + ), + personResultat.getSortedVilkårResultat(0)!!.toPeriode(), + ) + assertPeriode( + Periode( + LocalDate.of(2010, 3, 5), + LocalDate.of(2010, 5, 20), + ), + personResultat.getSortedVilkårResultat(1)!!.toPeriode(), + ) + assertPeriode( + Periode( + LocalDate.of(2010, 5, 21), + LocalDate.of(2010, 6, 1), + ), + personResultat.getSortedVilkårResultat(2)!!.toPeriode(), + ) + assertPeriode( + Periode( + LocalDate.of(2010, 8, 2), + LocalDate.of(2010, 12, 1), + ), + personResultat.getSortedVilkårResultat(3)!!.toPeriode(), + ) + } + + @Test + fun `fom-dato flyttes korrekt`() { + val restVilkårResultat = RestVilkårResultat( + 2, vilkår, resultat, + LocalDate.of(2010, 4, 2), LocalDate.of(2010, 8, 1), + "", + "", + LocalDateTime.now(), + behandling.id, + ) + + VilkårsvurderingUtils.muterPersonVilkårResultaterPut( + personResultat, + restVilkårResultat, + ) + + assertEquals(3, personResultat.vilkårResultater.size) + assertPeriode( + Periode( + LocalDate.of(2010, 1, 1), + LocalDate.of(2010, 4, 1), + ), + personResultat.getSortedVilkårResultat(0)!!.toPeriode(), + ) + assertPeriode( + Periode( + LocalDate.of(2010, 4, 2), + LocalDate.of(2010, 8, 1), + ), + personResultat.getSortedVilkårResultat(1)!!.toPeriode(), + ) + assertPeriode( + Periode( + LocalDate.of(2010, 8, 2), + LocalDate.of(2010, 12, 1), + ), + personResultat.getSortedVilkårResultat(2)!!.toPeriode(), + ) + } + + @Test + fun `tom-dato flyttes korrekt`() { + val restVilkårResultat = RestVilkårResultat( + 2, vilkår, resultat, + LocalDate.of(2010, 6, 2), LocalDate.of(2010, 9, 1), + "", + "", + LocalDateTime.now(), + behandling.id, + ) + + VilkårsvurderingUtils.muterPersonVilkårResultaterPut( + personResultat, + restVilkårResultat, + ) + + assertEquals(3, personResultat.vilkårResultater.size) + assertPeriode( + Periode( + LocalDate.of(2010, 1, 1), + LocalDate.of(2010, 6, 1), + ), + personResultat.getSortedVilkårResultat(0)!!.toPeriode(), + ) + assertPeriode( + Periode( + LocalDate.of(2010, 6, 2), + LocalDate.of(2010, 9, 1), + ), + personResultat.getSortedVilkårResultat(1)!!.toPeriode(), + ) + assertPeriode( + Periode( + LocalDate.of(2010, 9, 2), + LocalDate.of(2010, 12, 1), + ), + personResultat.getSortedVilkårResultat(2)!!.toPeriode(), + ) + } + + @Test + fun `Skal fjerne og ikke fylle inn tom periode i midten`() { + VilkårsvurderingUtils.muterPersonResultatDelete(personResultat, 2) + + assertEquals(2, personResultat.vilkårResultater.size) + assertPeriode( + Periode( + LocalDate.of(2010, 1, 1), + LocalDate.of(2010, 6, 1), + ), + personResultat.getSortedVilkårResultat(0)!!.toPeriode(), + ) + assertPeriode( + Periode( + LocalDate.of(2010, 8, 2), + LocalDate.of(2010, 12, 1), + ), + personResultat.getSortedVilkårResultat(1)!!.toPeriode(), + ) + } + + @Test + fun `Skal fjerne første periode`() { + VilkårsvurderingUtils.muterPersonResultatDelete( + personResultat, + 1, + ) + + assertEquals(2, personResultat.vilkårResultater.size) + assertPeriode( + Periode( + LocalDate.of(2010, 6, 2), + LocalDate.of(2010, 8, 1), + ), + personResultat.getSortedVilkårResultat(0)!!.toPeriode(), + ) + assertPeriode( + Periode( + LocalDate.of(2010, 8, 2), + LocalDate.of(2010, 12, 1), + ), + personResultat.getSortedVilkårResultat(1)!!.toPeriode(), + ) + } + + @Test + fun `Skal fjerne siste periode`() { + VilkårsvurderingUtils.muterPersonResultatDelete( + personResultat, + 3, + ) + + assertEquals(2, personResultat.vilkårResultater.size) + assertPeriode( + Periode( + LocalDate.of(2010, 1, 1), + LocalDate.of(2010, 6, 1), + ), + personResultat.getSortedVilkårResultat(0)!!.toPeriode(), + ) + assertPeriode( + Periode( + LocalDate.of(2010, 6, 2), + LocalDate.of(2010, 8, 1), + ), + personResultat.getSortedVilkårResultat(1)!!.toPeriode(), + ) + } + + @Test + fun `Skal nullstille periode hvis det kun finnes en periode`() { + val mockPersonResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = randomAktør(), + ) + + val mockVilkårResultat = VilkårResultat( + id = 1, + personResultat = mockPersonResultat, + vilkårType = vilkår, + resultat = resultat, + resultatBegrunnelse = null, + periodeFom = LocalDate.of(2010, 1, 1), + periodeTom = LocalDate.of(2010, 6, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ) + mockPersonResultat.setSortedVilkårResultater(setOf(mockVilkårResultat)) + + VilkårsvurderingUtils.muterPersonResultatDelete( + mockPersonResultat, + 1, + ) + + assertEquals(1, mockPersonResultat.vilkårResultater.size) + assertEquals(Resultat.IKKE_VURDERT, mockPersonResultat.getSortedVilkårResultat(0)!!.resultat) + } + + @Test + fun `Skal legge til periode`() { + assertEquals(3, personResultat.vilkårResultater.size) + VilkårsvurderingUtils.muterPersonResultatPost(personResultat, Vilkår.BOR_MED_SØKER) + assertEquals(4, personResultat.vilkårResultater.size) + } + + @Test + fun `Skal kaste feil når det legges til periode i en vilkårtype der det allerede finnes en uvurdert periode`() { + VilkårsvurderingUtils.muterPersonResultatPost(personResultat, Vilkår.BOR_MED_SØKER) + assertThrows(FunksjonellFeil::class.java) { + VilkårsvurderingUtils.muterPersonResultatPost(personResultat, Vilkår.BOR_MED_SØKER) + } + } + + @Test + fun `Skal tilpasse vilkår for endret vilkår når begge mangler tom-dato`() { + val vilkårResultat = VilkårResultat( + personResultat = personResultat, + vilkårType = vilkår, + resultat = resultat, + periodeFom = LocalDate.of(2020, 1, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ) + val restVilkårResultat = RestVilkårResultat( + id = 1, + vilkårType = vilkår, + resultat = resultat, + periodeFom = LocalDate.of(2020, 6, 1), + periodeTom = null, + begrunnelse = "", + endretAv = "", + endretTidspunkt = LocalDateTime.now(), + behandlingId = behandling.id, + ) + + VilkårsvurderingUtils.tilpassVilkårForEndretVilkår(personResultat, vilkårResultat, restVilkårResultat) + + assertEquals(LocalDate.of(2020, 1, 1), vilkårResultat.periodeFom) + assertEquals(LocalDate.of(2020, 5, 31), vilkårResultat.periodeTom) + } + + @Test + fun `flyttResultaterTilInitielt filtrer ikke bort ikke oppfylte perioder når det gjelder samme behandling`() { + val søkerAktørId = randomAktør() + val behandling = lagBehandling() + + val initiellVilkårvurdering = + lagVilkårsvurderingMedForskelligeResultat(søkerAktørId, behandling, listOf(Resultat.OPPFYLT)) + val aktivVilkårsvurdering = + lagVilkårsvurderingMedForskelligeResultat( + søkerAktørId, + behandling, + listOf(Resultat.IKKE_OPPFYLT, Resultat.OPPFYLT), + ) + + val (initiell, _) = VilkårsvurderingUtils.flyttResultaterTilInitielt( + initiellVilkårsvurdering = initiellVilkårvurdering, + aktivVilkårsvurdering = aktivVilkårsvurdering, + ) + + val opprettetBosattIRiket = + initiell.personResultater.flatMap { it.vilkårResultater }.filter { it.vilkårType == Vilkår.BOSATT_I_RIKET } + + assertEquals(2, opprettetBosattIRiket.size) + assertEquals( + listOf(Resultat.IKKE_OPPFYLT, Resultat.OPPFYLT).sorted(), + opprettetBosattIRiket.map { it.resultat }.sorted(), + ) + } + + @Test + fun `flyttResultaterTilInitielt filtrer ikke oppfylt om oppfylt finnes ved kopiering fra forrige behandling`() { + val søkerAktørId = randomAktør() + val behandling = lagBehandling() + val behandling2 = lagBehandling() + + val initiellVilkårvurdering = + lagVilkårsvurderingMedForskelligeResultat(søkerAktørId, behandling, listOf(Resultat.OPPFYLT)) + val aktivVilkårsvurdering = + lagVilkårsvurderingMedForskelligeResultat( + søkerAktørId, + behandling2, + listOf(Resultat.IKKE_OPPFYLT, Resultat.OPPFYLT), + ) + + val (initiell, _) = VilkårsvurderingUtils.flyttResultaterTilInitielt( + initiellVilkårsvurdering = initiellVilkårvurdering, + aktivVilkårsvurdering = aktivVilkårsvurdering, + ) + + val opprettetBosattIRiket = + initiell.personResultater.flatMap { it.vilkårResultater }.filter { it.vilkårType == Vilkår.BOSATT_I_RIKET } + + assertEquals(1, opprettetBosattIRiket.size) + assertEquals(Resultat.OPPFYLT, opprettetBosattIRiket.first().resultat) + } + + @Test + fun `flyttResultaterTilInitielt filtrer ikke ikke oppfylt om oppfylt ikke finnes`() { + val søkerAktørId = randomAktør() + val behandling = lagBehandling() + + val initiellVilkårsvurdering = + lagVilkårsvurderingMedForskelligeResultat(søkerAktørId, behandling, listOf(Resultat.OPPFYLT)) + val activeVilkårvurdering = + lagVilkårsvurderingMedForskelligeResultat( + søkerAktørId, + behandling, + listOf(Resultat.IKKE_OPPFYLT, Resultat.IKKE_OPPFYLT), + ) + + val (initiell, _) = VilkårsvurderingUtils.flyttResultaterTilInitielt( + initiellVilkårsvurdering = initiellVilkårsvurdering, + aktivVilkårsvurdering = activeVilkårvurdering, + ) + + val opprettetBosattIRiket = + initiell.personResultater.flatMap { it.vilkårResultater }.filter { it.vilkårType == Vilkår.BOSATT_I_RIKET } + + assertEquals(2, opprettetBosattIRiket.size) + assertTrue(opprettetBosattIRiket.none { it.resultat == Resultat.OPPFYLT }) + } + + fun lagVilkårsvurderingMedForskelligeResultat( + søkerAktør: Aktør, + behandling: Behandling, + resultater: List, + ): Vilkårsvurdering { + val vilkårsvurdering = Vilkårsvurdering( + behandling = behandling, + ) + var månedsteller = 0L + val personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = søkerAktør, + ) + personResultat.setSortedVilkårResultater( + resultater.map { + VilkårResultat( + personResultat = personResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = it, + periodeFom = LocalDate.now().plusMonths(månedsteller++), + periodeTom = LocalDate.now().plusMonths(månedsteller++), + begrunnelse = "", + sistEndretIBehandlingId = behandling.id, + ) + }.toSet(), + ) + vilkårsvurdering.personResultater = setOf(personResultat) + return vilkårsvurdering + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingUtilsTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingUtilsTest.kt" new file mode 100644 index 000000000..cd7102da8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingUtilsTest.kt" @@ -0,0 +1,256 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.til18ÅrsVilkårsdato +import no.nav.familie.ba.sak.ekstern.restDomene.RestVilkårResultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityVilkår +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.lagDødsfallFraPdl +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import java.time.LocalDate +import java.time.LocalDateTime + +class VilkårsvurderingUtilsTest { + + private val uvesentligVilkårsvurdering = + lagVilkårsvurdering(randomAktør(), lagBehandling(), Resultat.IKKE_VURDERT) + + @Test + fun `feil kastes når det finnes løpende oppfylt ved forsøk på å legge til avslag uten periode`() { + val personResultat = PersonResultat( + vilkårsvurdering = uvesentligVilkårsvurdering, + aktør = randomAktør(), + ) + val løpendeOppfylt = VilkårResultat( + personResultat = personResultat, + periodeFom = LocalDate.of(2020, 1, 1), + periodeTom = null, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = 0, + ) + personResultat.vilkårResultater.add(løpendeOppfylt) + + val avslagUtenPeriode = RestVilkårResultat( + id = 123, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = null, + periodeTom = null, + begrunnelse = "", + endretAv = "", + endretTidspunkt = LocalDateTime.now(), + behandlingId = 0, + erEksplisittAvslagPåSøknad = true, + ) + + assertThrows { + VilkårsvurderingUtils.validerAvslagUtenPeriodeMedLøpende( + personSomEndres = personResultat, + vilkårSomEndres = avslagUtenPeriode, + ) + } + } + + @Test + fun `feil kastes når det finnes avslag uten periode ved forsøk på å legge til løpende oppfylt`() { + val personResultat = PersonResultat( + vilkårsvurdering = uvesentligVilkårsvurdering, + aktør = randomAktør(), + ) + val avslagUtenPeriode = VilkårResultat( + personResultat = personResultat, + periodeFom = null, + periodeTom = null, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.IKKE_OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = 0, + erEksplisittAvslagPåSøknad = true, + ) + personResultat.vilkårResultater.add(avslagUtenPeriode) + + val løpendeOppfylt = RestVilkårResultat( + id = 123, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2020, 1, 1), + periodeTom = null, + begrunnelse = "", + endretAv = "", + endretTidspunkt = LocalDateTime.now(), + behandlingId = 0, + ) + + assertThrows { + VilkårsvurderingUtils.validerAvslagUtenPeriodeMedLøpende( + personSomEndres = personResultat, + vilkårSomEndres = løpendeOppfylt, + ) + } + } + + @Test + fun `skal ikke kaste feil hvis vilkåret er bor med søker`() { + val personResultat = PersonResultat( + vilkårsvurdering = uvesentligVilkårsvurdering, + aktør = randomAktør(), + ) + val løpendeOppfylt = VilkårResultat( + personResultat = personResultat, + periodeFom = LocalDate.of(2020, 1, 1), + periodeTom = null, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = 0, + ) + personResultat.vilkårResultater.add(løpendeOppfylt) + + val avslagUtenPeriode = RestVilkårResultat( + id = 123, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = null, + periodeTom = null, + begrunnelse = "", + endretAv = "", + endretTidspunkt = LocalDateTime.now(), + behandlingId = 0, + erEksplisittAvslagPåSøknad = true, + ) + + assertDoesNotThrow { + VilkårsvurderingUtils.validerAvslagUtenPeriodeMedLøpende( + personSomEndres = personResultat, + vilkårSomEndres = avslagUtenPeriode, + ) + } + } + + @Test + fun `feil kastes ikke når når ingen periode er løpende`() { + val personResultat = PersonResultat( + vilkårsvurdering = uvesentligVilkårsvurdering, + aktør = randomAktør(), + ) + val avslagUtenPeriode = VilkårResultat( + personResultat = personResultat, + periodeFom = null, + periodeTom = null, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.IKKE_OPPFYLT, + begrunnelse = "", + sistEndretIBehandlingId = 0, + erEksplisittAvslagPåSøknad = true, + ) + personResultat.vilkårResultater.add(avslagUtenPeriode) + + val løpendeOppfylt = RestVilkårResultat( + id = 123, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2020, 1, 1), + periodeTom = LocalDate.of(2020, 6, 1), + begrunnelse = "", + endretAv = "", + endretTidspunkt = LocalDateTime.now(), + behandlingId = 0, + ) + + assertDoesNotThrow { + VilkårsvurderingUtils.validerAvslagUtenPeriodeMedLøpende( + personSomEndres = personResultat, + vilkårSomEndres = løpendeOppfylt, + ) + } + } + + @Test + fun `skal liste opp begrunnelser uten vilkår`() { + val sanityBegrunnelser = mapOf( + Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET to SanityBegrunnelse( + vilkaar = emptyList(), + apiNavn = "innvilgetBosattIRiket", + navnISystem = "", + ), + ) + val vedtakBegrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET + + val restVedtakBegrunnelserTilknyttetVilkår = + vedtakBegrunnelseTilRestVedtakBegrunnelseTilknyttetVilkår(sanityBegrunnelser, vedtakBegrunnelse) + + Assertions.assertEquals(1, restVedtakBegrunnelserTilknyttetVilkår.size) + } + + @Test + fun `skal liste opp begrunnelsene en gang per vilkår`() { + val sanityBegrunnelser = mapOf( + Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET to SanityBegrunnelse( + vilkaar = listOf(SanityVilkår.BOSATT_I_RIKET, SanityVilkår.LOVLIG_OPPHOLD), + apiNavn = "innvilgetBosattIRiket", + navnISystem = "", + ), + ) + val vedtakBegrunnelse = Standardbegrunnelse.INNVILGET_BOSATT_I_RIKTET + + val restVedtakBegrunnelserTilknyttetVilkår = + vedtakBegrunnelseTilRestVedtakBegrunnelseTilknyttetVilkår(sanityBegrunnelser, vedtakBegrunnelse) + + Assertions.assertEquals(2, restVedtakBegrunnelserTilknyttetVilkår.size) + } + + @Test + fun `genererPersonResultatForPerson skal sette til-og-med dato på alle vilkår til dødsfallsdato og begrunnelse til dødsfall hvis barn er død`() { + val nyBehandling = lagBehandling() + + val vilkårsvurdering = Vilkårsvurdering(behandling = nyBehandling) + val dødtBarn = lagPerson(type = PersonType.BARN).apply { dødsfall = lagDødsfallFraPdl(this, "2012-12-12", null) } + + val personResultatForDødtBarn = genererPersonResultatForPerson( + vilkårsvurdering = vilkårsvurdering, + person = dødtBarn, + ) + + Assertions.assertTrue(personResultatForDødtBarn.vilkårResultater.all { it.begrunnelse == "Dødsfall" }) + Assertions.assertTrue( + personResultatForDødtBarn.vilkårResultater.all { + it.periodeTom == LocalDate.of(2012, 12, 12) + }, + ) + } + + @Test + fun `genererPersonResultatForPerson skal sette til-og-med dato på under-18-årsvilkår til 18 års datoen hvis barn ikke er død`() { + val nyBehandling = lagBehandling() + + val vilkårsvurdering = Vilkårsvurdering(behandling = nyBehandling) + val levendeBarn = lagPerson(type = PersonType.BARN, fødselsdato = LocalDate.of(2020, 10, 10)) + + val personResultatForLevendeBarn = genererPersonResultatForPerson( + vilkårsvurdering = vilkårsvurdering, + person = levendeBarn, + ) + + val under18ÅrVilkår = + personResultatForLevendeBarn.vilkårResultater.find { it.vilkårType == Vilkår.UNDER_18_ÅR }!! + + Assertions.assertEquals(under18ÅrVilkår.begrunnelse, "Vurdert og satt automatisk") + Assertions.assertEquals(under18ÅrVilkår.periodeTom, levendeBarn.fødselsdato.til18ÅrsVilkårsdato()) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/sikkerhet/AuditLoggerMock.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/sikkerhet/AuditLoggerMock.kt new file mode 100644 index 000000000..006d9ac8f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/sikkerhet/AuditLoggerMock.kt @@ -0,0 +1,16 @@ +package no.nav.familie.ba.sak.sikkerhet + +import io.mockk.mockk +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile + +@Configuration +@Profile("dev") +class AuditLoggerMock { + + @Bean + @Primary + fun auditLogger(): AuditLogger = mockk(relaxed = true) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/sikkerhet/AuditLoggerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/sikkerhet/AuditLoggerTest.kt new file mode 100644 index 000000000..9e8337d2c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/sikkerhet/AuditLoggerTest.kt @@ -0,0 +1,75 @@ +package no.nav.familie.ba.sak.sikkerhet + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.util.BrukerContextUtil +import no.nav.familie.log.mdc.MDCConstants +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.mock.web.MockHttpServletRequest + +internal class AuditLoggerTest { + + private val auditLogger = AuditLogger("familie-ba-sak") + private val navIdent = "Z1234567" + private val method = "POST" + private val requestUri = "/api/test/123" + + private lateinit var logger: Logger + private lateinit var listAppender: ListAppender + + @BeforeEach + internal fun setUp() { + MDC.put(MDCConstants.MDC_CALL_ID, "00001111") + val servletRequest = MockHttpServletRequest(method, requestUri) + BrukerContextUtil.mockBrukerContext(preferredUsername = navIdent, servletRequest = servletRequest) + logger = LoggerFactory.getLogger("auditLogger") as Logger + listAppender = ListAppender() + listAppender.start() + logger.addAppender(listAppender) + } + + @AfterEach + internal fun tearDown() { + BrukerContextUtil.clearBrukerContext() + MDC.remove(MDCConstants.MDC_CALL_ID) + } + + @Test + internal fun `logger melding uten custom strings`() { + auditLogger.log(Sporingsdata(AuditLoggerEvent.ACCESS, "12345678901")) + assertThat(listAppender.list).hasSize(1) + assertThat(getMessage()).isEqualTo(expectedBaseLog) + } + + @Test + internal fun `logger melding med custom strings`() { + auditLogger.log( + Sporingsdata( + event = AuditLoggerEvent.ACCESS, + personIdent = "12345678901", + custom1 = CustomKeyValue("k", "v"), + custom2 = CustomKeyValue("k2", "v2"), + custom3 = CustomKeyValue("k3", "v3"), + ), + ) + assertThat(listAppender.list).hasSize(1) + assertThat(getMessage()) + .isEqualTo("${expectedBaseLog}cs3Label=k cs3=v cs5Label=k2 cs5=v2 cs6Label=k3 cs6=v3") + } + + private fun getMessage() = listAppender.list[0].message.replace("""end=\d+""".toRegex(), "end=123") + + private val expectedBaseLog = "CEF:0|Familie|familie-ba-sak|1.0|audit:access|Saksbehandling|INFO|end=123 " + + "suid=Z1234567 " + + "duid=12345678901 " + + "sproc=00001111 " + + "requestMethod=POST " + + "request=/api/test/123 " +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/sikkerhet/RolletilgangDatabaseTestController.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/sikkerhet/RolletilgangDatabaseTestController.kt new file mode 100644 index 000000000..21fa280fb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/sikkerhet/RolletilgangDatabaseTestController.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.sikkerhet + +import no.nav.familie.ba.sak.config.featureToggle.miljø.Profil +import no.nav.familie.ba.sak.config.featureToggle.miljø.erAktiv +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.core.env.Environment +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/rolletilgang") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class RolletilgangDatabaseTestController( + private val behandlingService: BehandlingService, + private val environment: Environment, +) { + + @PostMapping(path = ["test-behandlinger"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun opprettBehandling(@RequestBody nyBehandling: NyBehandling): ResponseEntity> { + if (environment.erAktiv(Profil.Prod) || environment.erAktiv(Profil.Preprod)) { + error("Controller feilaktig aktivert i miljø") + } + + return ResponseEntity.status(HttpStatus.CREATED).body(Ressurs.success(behandlingService.opprettBehandling(nyBehandling))) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/sikkerhet/TilgangServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/sikkerhet/TilgangServiceTest.kt new file mode 100644 index 000000000..060fc817a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/sikkerhet/TilgangServiceTest.kt @@ -0,0 +1,228 @@ +package no.nav.familie.ba.sak.sikkerhet + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.RolleTilgangskontrollFeil +import no.nav.familie.ba.sak.common.clearAllCaches +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.tilPersonEnkelSøkerOgBarn +import no.nav.familie.ba.sak.config.AuditLoggerEvent +import no.nav.familie.ba.sak.config.IntegrasjonClientMock.Companion.mockSjekkTilgang +import no.nav.familie.ba.sak.config.RolleConfig +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollClient +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonEnkel +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.util.BrukerContextUtil.clearBrukerContext +import no.nav.familie.ba.sak.util.BrukerContextUtil.mockBrukerContext +import no.nav.familie.log.mdc.MDCConstants +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.slf4j.MDC +import org.springframework.cache.concurrent.ConcurrentMapCacheManager +import java.time.LocalDate + +class TilgangServiceTest { + + private val mockFamilieIntegrasjonerTilgangskontrollClient: FamilieIntegrasjonerTilgangskontrollClient = mockk() + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService = mockk() + private val fagsakService: FagsakService = mockk() + private val persongrunnlagService: PersongrunnlagService = mockk() + private val cacheManager = ConcurrentMapCacheManager() + private val kode6Gruppe = "kode6" + private val kode7Gruppe = "kode7" + private val rolleConfig = RolleConfig("", "", "", FORVALTER_ROLLE = "", KODE6 = kode6Gruppe, KODE7 = kode7Gruppe) + private val auditLogger = AuditLogger("familie-ba-sak") + private val familieIntegrasjonerTilgangskontrollService = FamilieIntegrasjonerTilgangskontrollService( + mockFamilieIntegrasjonerTilgangskontrollClient, + cacheManager, + mockk(), + ) + private val tilgangService = + TilgangService( + familieIntegrasjonerTilgangskontrollService = familieIntegrasjonerTilgangskontrollService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + persongrunnlagService = persongrunnlagService, + fagsakService = fagsakService, + rolleConfig = rolleConfig, + cacheManager = cacheManager, + auditLogger = auditLogger, + ) + + private val fagsak = defaultFagsak() + private val behandling = lagBehandling(fagsak) + private val aktør = fagsak.aktør + private val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = aktør.aktivFødselsnummer(), + barnasIdenter = emptyList(), + ) + private val olaIdent = "4567" + + @BeforeEach + internal fun setUp() { + MDC.put(MDCConstants.MDC_CALL_ID, "00001111") + mockBrukerContext() + every { fagsakService.hentAktør(fagsak.id) } returns fagsak.aktør + every { behandlingHentOgPersisterService.hent(any()) } returns behandling + every { persongrunnlagService.hentSøkerOgBarnPåBehandling(behandling.id) } returns + personopplysningGrunnlag.tilPersonEnkelSøkerOgBarn() + cacheManager.clearAllCaches() + } + + @AfterEach + internal fun tearDown() { + clearBrukerContext() + } + + @Test + internal fun `skal kaste RolleTilgangskontrollFeil dersom saksbehandler ikke har tilgang til person eller dets barn`() { + mockFamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(false) + + assertThrows { + tilgangService.validerTilgangTilPersoner( + listOf(aktør.aktivFødselsnummer()), + AuditLoggerEvent.ACCESS, + ) + } + } + + @Test + internal fun `skal ikke feile når saksbehandler har tilgang til person og dets barn`() { + mockFamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(true) + + tilgangService.validerTilgangTilPersoner(listOf(aktør.aktivFødselsnummer()), AuditLoggerEvent.ACCESS) + } + + @Test + internal fun `skal kaste RolleTilgangskontrollFeil dersom saksbehandler ikke har tilgang til behandling`() { + mockFamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(false) + + assertThrows { + tilgangService.validerTilgangTilBehandling( + behandling.id, + AuditLoggerEvent.ACCESS, + ) + } + } + + @Test + internal fun `skal ikke feile når saksbehandler har tilgang til behandling`() { + mockFamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(true) + + tilgangService.validerTilgangTilBehandling(behandling.id, AuditLoggerEvent.ACCESS) + } + + @Test + internal fun `validerTilgangTilPersoner - hvis samme saksbehandler kaller skal den ha cachet`() { + mockFamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(true) + + mockBrukerContext("A") + tilgangService.validerTilgangTilPersoner(listOf(olaIdent), AuditLoggerEvent.ACCESS) + tilgangService.validerTilgangTilPersoner(listOf(olaIdent), AuditLoggerEvent.ACCESS) + verify(exactly = 1) { + mockFamilieIntegrasjonerTilgangskontrollClient.sjekkTilgangTilPersoner(any()) + } + } + + @Test + internal fun `validerTilgangTilPersoner - hvis to ulike saksbehandler kaller skal den sjekke tilgang på nytt`() { + mockFamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(true) + + mockBrukerContext("A") + tilgangService.validerTilgangTilPersoner(listOf(olaIdent), AuditLoggerEvent.ACCESS) + mockBrukerContext("B") + tilgangService.validerTilgangTilPersoner(listOf(olaIdent), AuditLoggerEvent.ACCESS) + + verify(exactly = 2) { + mockFamilieIntegrasjonerTilgangskontrollClient.sjekkTilgangTilPersoner(any()) + } + } + + @Test + internal fun `validerTilgangTilBehandling - hvis samme saksbehandler kaller skal den ha cachet`() { + mockFamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(true) + + mockBrukerContext("A") + + tilgangService.validerTilgangTilBehandling(behandling.id, AuditLoggerEvent.ACCESS) + tilgangService.validerTilgangTilBehandling(behandling.id, AuditLoggerEvent.ACCESS) + + verify(exactly = 1) { + mockFamilieIntegrasjonerTilgangskontrollClient.sjekkTilgangTilPersoner(any()) + } + } + + @Test + internal fun `validerTilgangTilBehandling - hvis to ulike saksbehandler kaller skal den sjekke tilgang på nytt`() { + mockFamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(true) + + mockBrukerContext("A") + tilgangService.validerTilgangTilBehandling(behandling.id, AuditLoggerEvent.ACCESS) + mockBrukerContext("B") + tilgangService.validerTilgangTilBehandling(behandling.id, AuditLoggerEvent.ACCESS) + + verify(exactly = 2) { + mockFamilieIntegrasjonerTilgangskontrollClient.sjekkTilgangTilPersoner(any()) + } + } + + @Test + fun `validerTilgangTilFagsak - skal kaste feil dersom søker eller et eller flere av barna har diskresjonskode og saksbehandler mangler tilgang`() { + val søkerAktør = randomAktør("65434563721") + val barnAktør = randomAktør("12345678910") + every { fagsakService.hentAktør(fagsak.id) }.returns(aktør) + every { behandlingHentOgPersisterService.hentBehandlinger(fagsak.id) }.returns(listOf(behandling)) + every { persongrunnlagService.hentSøkerOgBarnPåFagsak(fagsak.id) }.returns( + setOf( + PersonEnkel( + aktør = søkerAktør, + type = PersonType.SØKER, + fødselsdato = LocalDate.now(), + dødsfallDato = null, + målform = Målform.NB, + ), + PersonEnkel( + aktør = barnAktør, + type = PersonType.BARN, + fødselsdato = LocalDate.now(), + dødsfallDato = null, + målform = Målform.NB, + ), + ), + ) + mockFamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(false) + mockBrukerContext("A") + assertThrows { + tilgangService.validerTilgangTilFagsak( + fagsak.id, + AuditLoggerEvent.ACCESS, + ) + } + } + + @Test + fun `skal feile hvis man mangler tilgang til en ident`() { + val fnr = randomFnr() + val fnr2 = randomFnr() + val fnr3 = randomFnr() + mockFamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(mapOf(fnr to true, fnr2 to false, fnr3 to true)) + assertThrows { + tilgangService.validerTilgangTilPersoner( + listOf(fnr, fnr2, fnr3), + AuditLoggerEvent.ACCESS, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkControllerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkControllerTest.kt new file mode 100644 index 000000000..f8f8d22c8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkControllerTest.kt @@ -0,0 +1,76 @@ +package no.nav.familie.ba.sak.statistikk.saksstatistikk + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagring +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringRepository +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringType +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.within +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +class SaksstatistikkControllerTest { + + val saksstatistikkMellomlagringRepository: SaksstatistikkMellomlagringRepository = mockk() + val saksstatistikkService: SaksstatistikkService = mockk() + + val controller: SaksstatistikkController = + SaksstatistikkController(saksstatistikkService, saksstatistikkMellomlagringRepository) + + @BeforeEach + fun init() { + } + + @Test + fun `Skal lagre saksstatistikk sak til repository med sendttidspunkt`() { + val request = SaksstatistikkController.SaksstatistikkSendtRequest( + offset = 45635, + type = SaksstatistikkMellomlagringType.SAK, + sendtTidspunkt = LocalDateTime.now(), + json = """{"sakId": 123456789, "versjon": "1.0", "funksjonellId": "aaa-bbb-ccc"}""", + ) + + val slot = slot() + every { saksstatistikkMellomlagringRepository.saveAndFlush(capture(slot)) } returns mockk() + controller.registrerSendtFraStatistikk(request) + + assertThat(slot.captured.offsetVerdiOnPrem).isEqualTo(request.offset) + assertThat(slot.captured.type).isEqualTo(SaksstatistikkMellomlagringType.SAK) + assertThat(slot.captured.sendtTidspunkt).isCloseTo( + LocalDateTime.now(), + within(10, ChronoUnit.SECONDS), + ) + assertThat(slot.captured.funksjonellId).isEqualTo("aaa-bbb-ccc") + assertThat(slot.captured.typeId).isEqualTo(123456789) + assertThat(slot.captured.kontraktVersjon).isEqualTo("1.0") + } + + @Test + fun `Skal lagre saksstatistikk behandling til repository med sendttidspunkt`() { + val request = SaksstatistikkController.SaksstatistikkSendtRequest( + offset = 45635, + type = SaksstatistikkMellomlagringType.BEHANDLING, + sendtTidspunkt = LocalDateTime.now(), + json = """{"behandlingId": 123456789, "versjon": "1.0", "funksjonellId": "aaa-bbb-ccc"}""", + ) + + val slot = slot() + every { saksstatistikkMellomlagringRepository.saveAndFlush(capture(slot)) } returns mockk() + + controller.registrerSendtFraStatistikk(request) + + assertThat(slot.captured.offsetVerdiOnPrem).isEqualTo(request.offset) + assertThat(slot.captured.type).isEqualTo(SaksstatistikkMellomlagringType.BEHANDLING) + assertThat(slot.captured.sendtTidspunkt).isCloseTo( + LocalDateTime.now(), + within(10, ChronoUnit.SECONDS), + ) + assertThat(slot.captured.funksjonellId).isEqualTo("aaa-bbb-ccc") + assertThat(slot.captured.typeId).isEqualTo(123456789) + assertThat(slot.captured.kontraktVersjon).isEqualTo("1.0") + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkEventPublisherFailSafe.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkEventPublisherFailSafe.kt new file mode 100644 index 000000000..3133252ac --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkEventPublisherFailSafe.kt @@ -0,0 +1,27 @@ +package no.nav.familie.ba.sak.statistikk.saksstatistikk + +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary + +@TestConfiguration +class SaksstatistikkEventPublisherFailSafe : SaksstatistikkEventPublisher() { + + @Bean + @Primary + fun safeSaksstatistikkEventPublisher(): SaksstatistikkEventPublisher { + return this + } + + override fun publiserBehandlingsstatistikk(behandlingId: Long) { + runCatching { + super.publiserBehandlingsstatistikk(behandlingId) + }.onFailure { it.printStackTrace() } + } + + override fun publiserSaksstatistikk(fagsakId: Long) { + runCatching { + super.publiserSaksstatistikk(fagsakId) + }.onFailure { it.printStackTrace() } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkServiceTest.kt new file mode 100644 index 000000000..aa2825683 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkServiceTest.kt @@ -0,0 +1,543 @@ +package no.nav.familie.ba.sak.statistikk.saksstatistikk + +import com.fasterxml.jackson.core.JsonFactory +import com.worldturner.medeia.api.UrlSchemaSource +import com.worldturner.medeia.api.jackson.MedeiaJacksonApi +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.unmockkAll +import no.nav.familie.ba.sak.common.Utils +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.lagVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.tilfeldigSøker +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.datagenerator.settpåvent.lagSettPåVent +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Arbeidsfordelingsenhet +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.domene.ArbeidsfordelingPåBehandling +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingSøknadsinfoService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentService +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentÅrsak +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollService +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.domene.Totrinnskontroll +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext.SYSTEM_FORKORTELSE +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext.SYSTEM_NAVN +import no.nav.familie.eksterne.kontrakter.saksstatistikk.AktørDVH +import no.nav.familie.eksterne.kontrakter.saksstatistikk.BehandlingDVH +import no.nav.familie.eksterne.kontrakter.saksstatistikk.ResultatBegrunnelseDVH +import no.nav.familie.eksterne.kontrakter.saksstatistikk.SakDVH +import no.nav.familie.eksterne.kontrakter.saksstatistikk.SettPåVent +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.Vegadresse +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.within +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.extension.ExtendWith +import java.nio.charset.Charset +import java.time.LocalDate +import java.time.LocalDate.now +import java.time.LocalDateTime +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import java.util.UUID +import kotlin.random.Random.Default.nextBoolean +import kotlin.random.Random.Default.nextInt + +@ExtendWith(MockKExtension::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class SaksstatistikkServiceTest( + @MockK(relaxed = true) + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @MockK(relaxed = true) + private val behandlingSøknadsinfoService: BehandlingSøknadsinfoService, + + @MockK + private val arbeidsfordelingService: ArbeidsfordelingService, + + @MockK + private val totrinnskontrollService: TotrinnskontrollService, + + @MockK + private val fagsakService: FagsakService, + + @MockK + private val personopplysningerService: PersonopplysningerService, + + @MockK + private val personidentService: PersonidentService, + + @MockK + private val persongrunnlagService: PersongrunnlagService, + + @MockK + private val vedtakService: VedtakService, + + @MockK + private val vedtaksperiodeService: VedtaksperiodeService, + + @MockK + private val settPåVentService: SettPåVentService, + +) { + + private val sakstatistikkService = SaksstatistikkService( + behandlingHentOgPersisterService, + behandlingSøknadsinfoService, + arbeidsfordelingService, + totrinnskontrollService, + vedtakService, + fagsakService, + personopplysningerService, + persongrunnlagService, + vedtaksperiodeService, + settPåVentService, + ) + + @BeforeAll + fun init() { + MockKAnnotations.init() + + every { arbeidsfordelingService.hentArbeidsfordelingPåBehandling(any()) } returns ArbeidsfordelingPåBehandling( + behandlendeEnhetId = "4820", + behandlendeEnhetNavn = "Nav", + behandlingId = 1, + ) + every { arbeidsfordelingService.hentArbeidsfordelingsenhet(any()) } returns Arbeidsfordelingsenhet( + "4821", + "NAV", + ) + + every { settPåVentService.finnAktivSettPåVentPåBehandling(any()) } returns lagSettPåVent() + } + + @AfterAll + fun tearDown() { + unmockkAll() + } + + @Test + fun `Skal mappe henleggelsesårsak til behandlingDVH for henlagt behandling`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.FØDSELSHENDELSE).also { + it.resultat = Behandlingsresultat.HENLAGT_FEILAKTIG_OPPRETTET + } + + every { behandlingHentOgPersisterService.hent(any()) } returns behandling + every { totrinnskontrollService.hentAktivForBehandling(any()) } returns null + every { vedtakService.hentAktivForBehandling(any()) } returns null + + val behandlingDvh = sakstatistikkService.mapTilBehandlingDVH(2) + println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(behandlingDvh)) + + assertThat(behandlingDvh?.resultat).isEqualTo("HENLAGT_FEILAKTIG_OPPRETTET") + assertThat(behandlingDvh?.resultatBegrunnelser).hasSize(0) + } + + @Test + fun `Skal mappe til behandlingDVH for Automatisk rute`() { + val behandling = lagBehandling(årsak = BehandlingÅrsak.FØDSELSHENDELSE, skalBehandlesAutomatisk = true).also { + it.resultat = Behandlingsresultat.INNVILGET + } + + val vedtak = lagVedtak(behandling) + val vedtaksperiodeMedBegrunnelser = + lagVedtaksperiodeMedBegrunnelser() + + every { behandlingHentOgPersisterService.hent(any()) } returns behandling + every { vedtakService.hentAktivForBehandling(any()) } returns vedtak + every { vedtaksperiodeService.hentPersisterteVedtaksperioder(any()) } returns listOf( + vedtaksperiodeMedBegrunnelser, + ) + every { totrinnskontrollService.hentAktivForBehandling(any()) } returns Totrinnskontroll( + saksbehandler = SYSTEM_NAVN, + saksbehandlerId = SYSTEM_FORKORTELSE, + beslutter = SYSTEM_NAVN, + beslutterId = SYSTEM_FORKORTELSE, + godkjent = true, + behandling = behandling, + ) + + val behandlingDvh = sakstatistikkService.mapTilBehandlingDVH(2) + println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(behandlingDvh)) + + assertThat(behandlingDvh?.funksjonellTid).isCloseTo(ZonedDateTime.now(), within(1, ChronoUnit.MINUTES)) + assertThat(behandlingDvh?.tekniskTid).isCloseTo(ZonedDateTime.now(), within(1, ChronoUnit.MINUTES)) + assertThat(behandlingDvh?.mottattDato).isEqualTo( + ZonedDateTime.of( + behandling.opprettetTidspunkt, + SaksstatistikkService.TIMEZONE, + ), + ) + assertThat(behandlingDvh?.registrertDato).isEqualTo( + ZonedDateTime.of( + behandling.opprettetTidspunkt, + SaksstatistikkService.TIMEZONE, + ), + ) + assertThat(behandlingDvh?.vedtaksDato).isEqualTo(vedtak.vedtaksdato?.toLocalDate()) + assertThat(behandlingDvh?.behandlingId).isEqualTo(behandling.id.toString()) + assertThat(behandlingDvh?.relatertBehandlingId).isNull() + assertThat(behandlingDvh?.sakId).isEqualTo(behandling.fagsak.id.toString()) + assertThat(behandlingDvh?.vedtakId).isEqualTo(vedtak.id.toString()) + assertThat(behandlingDvh?.behandlingType).isEqualTo(behandling.type.name) + assertThat(behandlingDvh?.utenlandstilsnitt).isEqualTo(behandling.kategori.name) + assertThat(behandlingDvh?.behandlingKategori).isEqualTo(behandling.underkategori.name) + assertThat(behandlingDvh?.behandlingUnderkategori).isNull() + assertThat(behandlingDvh?.behandlingStatus).isEqualTo(behandling.status.name) + assertThat(behandlingDvh?.totrinnsbehandling).isFalse + assertThat(behandlingDvh?.saksbehandler).isEqualTo(SYSTEM_FORKORTELSE) + assertThat(behandlingDvh?.beslutter).isEqualTo(SYSTEM_FORKORTELSE) + assertThat(behandlingDvh?.avsender).isEqualTo("familie-ba-sak") + assertThat(behandlingDvh?.versjon).isNotEmpty + assertThat(behandlingDvh?.resultat).isEqualTo(behandling.resultat.name) + } + + @Test + fun `Skal mappe til behandlingDVH for manuell rute`() { + val behandling = + lagBehandling(årsak = BehandlingÅrsak.SØKNAD).also { it.resultat = Behandlingsresultat.AVSLÅTT } + + every { totrinnskontrollService.hentAktivForBehandling(any()) } returns Totrinnskontroll( + saksbehandler = "Saksbehandler", + saksbehandlerId = "saksbehandlerId", + beslutter = "Beslutter", + beslutterId = "beslutterId", + godkjent = true, + behandling = behandling, + ) + + val vedtak = lagVedtak(behandling) + + val vedtaksperiodeFom = LocalDate.of(2021, 3, 11) + val vedtaksperiodeTom = LocalDate.of(21, 4, 11) + val vedtaksperiodeMedBegrunnelser = + lagVedtaksperiodeMedBegrunnelser(vedtak = vedtak, fom = vedtaksperiodeFom, tom = vedtaksperiodeTom) + + every { behandlingHentOgPersisterService.hent(any()) } returns behandling + every { persongrunnlagService.hentSøker(any()) } returns tilfeldigSøker() + every { persongrunnlagService.hentBarna(any()) } returns listOf( + tilfeldigPerson() + .copy(aktør = randomAktør("01010000001")), + ) + + every { vedtakService.hentAktivForBehandling(any()) } returns vedtak + every { vedtaksperiodeService.hentPersisterteVedtaksperioder(any()) } returns listOf( + vedtaksperiodeMedBegrunnelser, + ) + + val mottattDato = LocalDateTime.now() + every { behandlingSøknadsinfoService.hentSøknadMottattDato(any()) } returns mottattDato + + val tidSattPåVent = now() + val frist = now().plusWeeks(3) + every { settPåVentService.finnAktivSettPåVentPåBehandling(any()) } returns lagSettPåVent( + tidSattPåVent = tidSattPåVent, + årsak = SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, + frist = frist, + ) + + val behandlingDvh = sakstatistikkService.mapTilBehandlingDVH(2) + println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(behandlingDvh)) + + assertThat(behandlingDvh?.funksjonellTid).isCloseTo(ZonedDateTime.now(), within(1, ChronoUnit.MINUTES)) + assertThat(behandlingDvh?.tekniskTid).isCloseTo(ZonedDateTime.now(), within(1, ChronoUnit.MINUTES)) + assertThat(behandlingDvh?.mottattDato).isEqualTo( + mottattDato.atZone(SaksstatistikkService.TIMEZONE), + ) + assertThat(behandlingDvh?.registrertDato).isEqualTo( + behandling.opprettetTidspunkt.atZone(SaksstatistikkService.TIMEZONE), + ) + assertThat(behandlingDvh?.vedtaksDato).isEqualTo(vedtak.vedtaksdato?.toLocalDate()) + assertThat(behandlingDvh?.behandlingId).isEqualTo(behandling.id.toString()) + assertThat(behandlingDvh?.relatertBehandlingId).isNull() + assertThat(behandlingDvh?.sakId).isEqualTo(behandling.fagsak.id.toString()) + assertThat(behandlingDvh?.vedtakId).isEqualTo(vedtak.id.toString()) + assertThat(behandlingDvh?.behandlingType).isEqualTo(behandling.type.name) + assertThat(behandlingDvh?.behandlingStatus).isEqualTo(behandling.status.name) + assertThat(behandlingDvh?.totrinnsbehandling).isTrue + assertThat(behandlingDvh?.saksbehandler).isEqualTo("saksbehandlerId") + assertThat(behandlingDvh?.beslutter).isEqualTo("beslutterId") + assertThat(behandlingDvh?.avsender).isEqualTo("familie-ba-sak") + assertThat(behandlingDvh?.versjon).isNotEmpty + assertThat(behandlingDvh?.settPaaVent?.tidSattPaaVent) + .isEqualTo(tidSattPåVent.atStartOfDay(SaksstatistikkService.TIMEZONE)) + assertThat(behandlingDvh?.settPaaVent?.aarsak).isEqualTo(SettPåVentÅrsak.AVVENTER_DOKUMENTASJON.name) + assertThat(behandlingDvh?.settPaaVent?.frist) + .isEqualTo(frist.atStartOfDay(SaksstatistikkService.TIMEZONE)) + } + + @Test + fun `skal levere dvh-kodene for sakstype (institusjon, enslig_mindreårig) i behandlingUnderkategori`() { + every { totrinnskontrollService.hentAktivForBehandling(any()) } returns null + every { vedtakService.hentAktivForBehandling(any()) } returns null + + listOf(FagsakType.INSTITUSJON, FagsakType.BARN_ENSLIG_MINDREÅRIG, null) + .forEach { optionalSakstype -> + val fagsak = defaultFagsak().copy(type = optionalSakstype ?: FagsakType.NORMAL) + val behandling = lagBehandling(årsak = BehandlingÅrsak.SØKNAD, fagsak = fagsak) + + every { behandlingHentOgPersisterService.hent(any()) } returns behandling + + val behandlingDvh = sakstatistikkService.mapTilBehandlingDVH(2) + assertThat("${behandlingDvh?.behandlingUnderkategori}").isSubstringOf("${optionalSakstype?.name}") + } + } + + @Test + fun `Skal mappe til sakDVH, ingen aktiv behandling, så kun aktør SØKER, bostedsadresse i Norge`() { + every { fagsakService.hentPåFagsakId(any()) } answers { + Fagsak(status = FagsakStatus.OPPRETTET, aktør = tilAktør("12345678910")) + } + + every { personidentService.hentAktør("12345678910") } returns Aktør("1234567891000") + every { personidentService.hentAktør("12345678911") } returns Aktør("1234567891100") + every { personopplysningerService.hentPersoninfoEnkel(tilAktør("12345678910")) } returns PersonInfo( + fødselsdato = LocalDate.of( + 2017, + 3, + 1, + ), + bostedsadresser = mutableListOf( + Bostedsadresse( + vegadresse = Vegadresse( + matrikkelId = 1111, + husnummer = null, + husbokstav = null, + bruksenhetsnummer = null, + adressenavn = null, + kommunenummer = null, + tilleggsnavn = null, + postnummer = "2222", + ), + ), + ), + ) + + every { behandlingHentOgPersisterService.finnAktivForFagsak(any()) } returns null + + val sakDvh = sakstatistikkService.mapTilSakDvh(1) + println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(sakDvh)) + + assertThat(sakDvh?.aktorId).isEqualTo(1234567891000) + assertThat(sakDvh?.aktorer).hasSize(1).extracting("rolle").contains("SØKER") + assertThat(sakDvh?.sakStatus).isEqualTo(FagsakStatus.OPPRETTET.name) + assertThat(sakDvh?.avsender).isEqualTo("familie-ba-sak") + assertThat(sakDvh?.bostedsland).isEqualTo("NO") + } + + @Test + fun `Skal mappe til sakDVH, ingen aktiv behandling, så kun aktør SØKER, bostedsadresse i Utland`() { + every { fagsakService.hentPåFagsakId(any()) } answers { + Fagsak(status = FagsakStatus.OPPRETTET, aktør = tilAktør("12345678910")) + } + + every { personidentService.hentAktør("12345678910") } returns Aktør("1234567891000") + every { personidentService.hentAktør("12345678911") } returns Aktør("1234567891100") + + every { personopplysningerService.hentPersoninfoEnkel(tilAktør("12345678910")) } returns PersonInfo( + fødselsdato = LocalDate.of( + 2017, + 3, + 1, + ), + ) + every { personopplysningerService.hentLandkodeAlpha2UtenlandskBostedsadresse(tilAktør("12345678910")) } returns "SE" + + every { behandlingHentOgPersisterService.finnAktivForFagsak(any()) } returns null + + val sakDvh = sakstatistikkService.mapTilSakDvh(1) + println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(sakDvh)) + + assertThat(sakDvh?.aktorId).isEqualTo(1234567891000) + assertThat(sakDvh?.aktorer).hasSize(1).extracting("rolle").contains("SØKER") + assertThat(sakDvh?.sakStatus).isEqualTo(FagsakStatus.OPPRETTET.name) + assertThat(sakDvh?.avsender).isEqualTo("familie-ba-sak") + assertThat(sakDvh?.bostedsland).isEqualTo("SE") + } + + @Test + fun `Skal mappe til sakDVH, aktører har SØKER og BARN`() { + val randomAktørId = randomAktør() + val fagsak = Fagsak(status = FagsakStatus.OPPRETTET, aktør = randomAktørId) + every { fagsakService.hentPåFagsakId(any()) } answers { + fagsak + } + every { personidentService.hentAktør(any()) } returns randomAktørId + every { personopplysningerService.hentLandkodeAlpha2UtenlandskBostedsadresse(any()) } returns "SE" + + every { persongrunnlagService.hentAktiv(any()) } returns lagTestPersonopplysningGrunnlag( + 1, + tilfeldigPerson(personType = PersonType.BARN), + tilfeldigPerson(personType = PersonType.SØKER), + ) + + every { behandlingHentOgPersisterService.finnAktivForFagsak(any()) } returns lagBehandling(fagsak) + + val sakDvh = sakstatistikkService.mapTilSakDvh(1) + println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(sakDvh)) + + assertThat(sakDvh?.aktorId).isEqualTo(randomAktørId.aktørId.toLong()) + assertThat(sakDvh?.aktorer).hasSize(2).extracting("rolle").containsOnly("SØKER", "BARN") + assertThat(sakDvh?.sakStatus).isEqualTo(FagsakStatus.OPPRETTET.name) + assertThat(sakDvh?.avsender).isEqualTo("familie-ba-sak") + assertThat(sakDvh?.bostedsland).isEqualTo("SE") + } + + @Test + fun `Enum-verdier brukt i behandlingDVH skal validere mot json schema`() { + val enumVerdier = listOf( + BehandlingType.values(), + BehandlingStatus.values(), + BehandlingUnderkategori.values(), + BehandlingÅrsak.values(), + BehandlingKategori.values(), + Behandlingsresultat.values(), + SettPåVentÅrsak.values(), + ) + + val alleMuligeResultatBegrunnelser = Standardbegrunnelse.values().map { + ResultatBegrunnelseDVH(now(), now(), it.vedtakBegrunnelseType.name, it.name) + } + + for (i in 0..enumVerdier.maxOf { it.size }) { + val enumI = enumVerdier.map { it.getOrElse(i) { _ -> it.first() } } + val behandlingType = enumI[0] as BehandlingType + val behandlingStatus = enumI[1].name + val behandlingUnderkategori = enumI[2].name + val behandlingAarsak = enumI[3].name + val behandlingKategori = enumI[4].name + val behandlingsresultat = enumI[5].name + val settPåVentÅrsak = enumI[6].name + + val behandlingDVH = BehandlingDVH( + funksjonellTid = ZonedDateTime.now(), + tekniskTid = ZonedDateTime.now(), + mottattDato = LocalDateTime.now().atZone(SaksstatistikkService.TIMEZONE), + registrertDato = LocalDateTime.now().atZone(SaksstatistikkService.TIMEZONE), + behandlingId = nextInt(100000000, 999999999).toString(), + funksjonellId = UUID.randomUUID().toString(), + sakId = nextInt(100000000, 999999999).toString(), + behandlingType = behandlingType.name, + behandlingStatus = behandlingStatus, + behandlingKategori = behandlingUnderkategori, + behandlingAarsak = behandlingAarsak, + automatiskBehandlet = nextBoolean(), + utenlandstilsnitt = behandlingKategori, + ansvarligEnhetKode = "EnhetKodeA", + behandlendeEnhetKode = "EnhetKodeB", + ansvarligEnhetType = "NORG", + behandlendeEnhetType = "NORG", + totrinnsbehandling = nextBoolean(), + avsender = "familie-ba-sak", + versjon = Utils.hentPropertyFraMaven("familie.kontrakter.saksstatistikk") ?: "2", + // Ikke påkrevde felt + vedtaksDato = now(), + relatertBehandlingId = nextInt(100000000, 999999999).toString(), + vedtakId = nextInt(100000000, 999999999).toString(), + resultat = behandlingsresultat, + behandlingTypeBeskrivelse = behandlingType.visningsnavn, + resultatBegrunnelser = alleMuligeResultatBegrunnelser, + behandlingOpprettetAv = "behandling.opprettetAv", + behandlingOpprettetType = "saksbehandlerId", + behandlingOpprettetTypeBeskrivelse = "saksbehandlerId. VL ved automatisk behandling", + beslutter = "beslutterId", + saksbehandler = "saksbehandlerId", + settPaaVent = SettPåVent( + frist = now().atStartOfDay(SaksstatistikkService.TIMEZONE), + tidSattPaaVent = now().atStartOfDay(SaksstatistikkService.TIMEZONE), + aarsak = settPåVentÅrsak, + ), + ) + try { + validerJsonMotSchema( + sakstatistikkObjectMapper.writeValueAsString(behandlingDVH), + "/schema/behandling-schema.json", + ) + } catch (e: Exception) { + throw IllegalStateException( + "Skjema til saksstatistikk validerer ikke etter endringer blant enum-verdier. Sjekk feilmelding og oppdater enten enum til å passe skjema eller skjema til å passe enum.", + e, + ) + } + } + } + + @Test + fun `Enum-verdier brukt i sakDvh skal validere mot json schema`() { + val deltagere = + PersonType.values().map { personType -> AktørDVH(randomAktør().aktørId.toLong(), personType.name) } + + FagsakStatus.values().forEach { + val sakDvh = SakDVH( + funksjonellTid = ZonedDateTime.now(), + tekniskTid = ZonedDateTime.now(), + opprettetDato = now(), + funksjonellId = UUID.randomUUID().toString(), + sakId = nextInt(100000000, 999999999).toString(), + aktorId = deltagere.first().aktorId, + aktorer = deltagere, + sakStatus = it.name, + avsender = "familie-ba-sak", + versjon = Utils.hentPropertyFraMaven("familie.kontrakter.saksstatistikk") ?: "2", + bostedsland = "NO", + ) + try { + validerJsonMotSchema( + sakstatistikkObjectMapper.writeValueAsString(sakDvh), + "/schema/sak-schema.json", + ) + } catch (e: Exception) { + throw IllegalStateException( + "Skjema til saksstatistikk validerer ikke etter endringer blant enum-verdier. Sjekk feilmelding og oppdater enten enum til å passe skjema eller skjema til å passe enum.", + e, + ) + } + } + } + + fun validerJsonMotSchema(json: String, schemaPath: String) { + val api = MedeiaJacksonApi() + val behandlingSchemaValidator = api.loadSchema( + UrlSchemaSource( + object {}::class.java.getResource(schemaPath)!!, + ), + ) + val validatedParser = api.decorateJsonParser( + behandlingSchemaValidator, + JsonFactory().createParser(json.toByteArray(Charset.defaultCharset())), + ) + api.parseAll(validatedParser) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/statistikk/st\303\270nadsstatistikk/St\303\270nadsstatistikkServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/statistikk/st\303\270nadsstatistikk/St\303\270nadsstatistikkServiceTest.kt" new file mode 100644 index 000000000..5b0e2d595 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/statistikk/st\303\270nadsstatistikk/St\303\270nadsstatistikkServiceTest.kt" @@ -0,0 +1,327 @@ +package no.nav.familie.ba.sak.statistikk.stønadsstatistikk + +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import no.nav.familie.ba.sak.common.forrigeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelseUtvidet +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.ClientMocks.Companion.barnFnr +import no.nav.familie.ba.sak.config.ClientMocks.Companion.søkerFnr +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.økonomi.sats +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.KompetanseService +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.eksterne.kontrakter.BehandlingTypeV2 +import no.nav.familie.eksterne.kontrakter.BehandlingÅrsakV2 +import no.nav.familie.eksterne.kontrakter.FagsakType +import no.nav.familie.eksterne.kontrakter.Kompetanse +import no.nav.familie.kontrakter.felles.objectMapper +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.extension.ExtendWith +import java.lang.reflect.Field +import java.math.BigDecimal +import java.time.YearMonth + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@ExtendWith(MockKExtension::class) +internal class StønadsstatistikkServiceTest( + @MockK(relaxed = true) + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @MockK + private val persongrunnlagService: PersongrunnlagService, + + @MockK + private val vedtakService: VedtakService, + + @MockK + private val personopplysningerService: PersonopplysningerService, + + @MockK + private val kompetanseService: KompetanseService, + + @MockK + private val vedtakRepository: VedtakRepository, + + @MockK + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, +) { + + private val stønadsstatistikkService = + StønadsstatistikkService( + behandlingHentOgPersisterService, + persongrunnlagService, + vedtakService, + personopplysningerService, + vedtakRepository, + kompetanseService, + andelerTilkjentYtelseOgEndreteUtbetalingerService, + ) + private val behandling = lagBehandling() + private val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag(behandling.id, søkerFnr[0], barnFnr.toList()) + private val barn1 = personopplysningGrunnlag.barna.first() + private val barn2 = personopplysningGrunnlag.barna.last() + + @BeforeAll + fun init() { + MockKAnnotations.init(this) + + val vedtak = lagVedtak(behandling) + + val andelTilkjentYtelseBarn1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + barn1.fødselsdato.nesteMåned(), + barn1.fødselsdato.plusYears(3).toYearMonth(), + YtelseType.ORDINÆR_BARNETRYGD, + behandling = behandling, + person = barn1, + aktør = barn1.aktør, + periodeIdOffset = 1, + + ) + val andelTilkjentYtelseBarn2PeriodeMed0Beløp = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + barn2.fødselsdato.nesteMåned(), + barn2.fødselsdato.plusYears(18).forrigeMåned(), + YtelseType.ORDINÆR_BARNETRYGD, + behandling = behandling, + person = barn2, + beløp = 0, + aktør = barn2.aktør, + prosent = BigDecimal(0), + periodeIdOffset = null, + ) + + val kompetanseperioder = setOf( + no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse( + fom = YearMonth.now(), + tom = null, + barnAktører = setOf(barn1.aktør), + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + annenForeldersAktivitet = KompetanseAktivitet.I_ARBEID, + annenForeldersAktivitetsland = "PL", + barnetsBostedsland = "PL", + resultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + ), + no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse( + fom = null, + tom = null, + barnAktører = emptySet(), + søkersAktivitet = null, + annenForeldersAktivitet = null, + annenForeldersAktivitetsland = null, + barnetsBostedsland = null, + resultat = null, + ), + ) + + val andelTilkjentYtelseSøker = lagAndelTilkjentYtelseUtvidet( + barn2.fødselsdato.nesteMåned().toString(), + barn2.fødselsdato.plusYears(2).toYearMonth().toString(), + YtelseType.UTVIDET_BARNETRYGD, + behandling = behandling, + person = personopplysningGrunnlag.søker, + periodeIdOffset = 3, + ) + + val andelerTilkjentYtelse = listOf( + andelTilkjentYtelseBarn1, + andelTilkjentYtelseBarn2PeriodeMed0Beløp, + AndelTilkjentYtelseMedEndreteUtbetalinger.utenEndringer(andelTilkjentYtelseSøker), + ) + + every { behandlingHentOgPersisterService.hent(any()) } returns behandling + every { kompetanseService.hentKompetanser(any()) } returns kompetanseperioder + every { persongrunnlagService.hentAktivThrows(any()) } returns personopplysningGrunnlag + every { vedtakService.hentAktivForBehandling(any()) } returns vedtak + every { personopplysningerService.hentLandkodeAlpha2UtenlandskBostedsadresse(any()) } returns "DK" + every { andelerTilkjentYtelseOgEndreteUtbetalingerService.finnAndelerTilkjentYtelseMedEndreteUtbetalinger(any()) } returns + andelerTilkjentYtelse + } + + @Test + fun hentVedtakV2() { + val vedtak = stønadsstatistikkService.hentVedtakV2(1L) + println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(vedtak)) + + assertEquals(2, vedtak.utbetalingsperioderV2[0].utbetalingsDetaljer.size) + assertEquals( + 1 * sats(YtelseType.ORDINÆR_BARNETRYGD) + sats(YtelseType.UTVIDET_BARNETRYGD), + vedtak.utbetalingsperioderV2[0].utbetaltPerMnd, + ) + + assertThat(vedtak.kompetanseperioder).hasSize(1).contains( + Kompetanse( + fom = YearMonth.now(), + tom = null, + barnsIdenter = listOf(barn1.aktør.aktivFødselsnummer()), + sokersaktivitet = no.nav.familie.eksterne.kontrakter.KompetanseAktivitet.ARBEIDER, + annenForeldersAktivitet = no.nav.familie.eksterne.kontrakter.KompetanseAktivitet.I_ARBEID, + annenForeldersAktivitetsland = "PL", + barnetsBostedsland = "PL", + resultat = no.nav.familie.eksterne.kontrakter.KompetanseResultat.NORGE_ER_PRIMÆRLAND, + ), + ) + + vedtak.utbetalingsperioderV2 + .flatMap { it.utbetalingsDetaljer.map { ud -> ud.person } } + .filter { it.personIdent != søkerFnr[0] } + .forEach { + assertEquals(0, it.delingsprosentYtelse) + } + } + + /** + * Nye årsaker må legges til VedtakDVH i familie-eksterne-kontrakter når det legges til i Behandling + * + * Endringenen MÅ være BAKOVERKOMPATIBEL. Hvis man f.eks. endrer navn på en BehandlingÅrsak, så må man være sikker på at det ikke er sendt + * et slik vedtak til stønaddstatistikk. Den nye kontrakten skal kunne brukes til å lese ALLE meldinger på topic + * + * Hvis det er sendt et slik vedtak, så legger man heller til den nye verdien i VedtakDVH og ikke slette gamle + * + */ + @Test + fun `Skal gi feil hvis det kommer en ny BehandlingÅrsak som det ikke er tatt høyde for mot stønaddstatistkk - Man trenger å oppdatere schema og varsle stønaddstatistikk - Tips i javadoc`() { + val behandlingsÅrsakIBASak = + enumValues() + .filter { it != BehandlingÅrsak.TEKNISK_OPPHØR } // IKke i bruk lenger + .map { it.name } + val behandlingsÅrsakFraEksternKontrakt = + ikkeAvvikleteEnumverdier() + + assertThat(behandlingsÅrsakIBASak) + .hasSize(behandlingsÅrsakFraEksternKontrakt.size) + .containsAll(behandlingsÅrsakFraEksternKontrakt) + } + + /** + * Nye annenForeldersAktivitet må legges til VedtakDVHV2 i familie-eksterne-kontrakter når det legges til i Behandling + * + * Endringenen MÅ være BAKOVERKOMPATIBEL. Hvis man f.eks. endrer navn på en KompetanseAktivitet, så må man være sikker på at det ikke er sendt + * et slik vedtak til stønaddstatistikk. Den nye kontrakten skal kunne brukes til å lese ALLE meldinger på topic + * + * Hvis det er sendt et slik vedtak, så legger man heller til den nye verdien i VedtakDVHV2 og ikke slette gamle + * + */ + @Test + fun `Skal gi feil hvis det kommer en ny KompetanseAktivitet for annenForelder som det ikke er tatt høyde for mot stønaddstatistkk - Man trenger å oppdatere schema og varsle stønaddstatistikk - Tips i javadoc`() { + val annenForeldersAktivitet = enumValues().map { it.name } + val annenForeldersAktivitetFraEksternKontrakt = + ikkeAvvikleteEnumverdier() + + assertThat(annenForeldersAktivitet) + .hasSize(annenForeldersAktivitetFraEksternKontrakt.size) + .containsAll(annenForeldersAktivitetFraEksternKontrakt) + } + + /** + * Nye søkersAktivitet må legges til VedtakDVHV2 i familie-eksterne-kontrakter når det legges til i Behandling + * + * Endringenen MÅ være BAKOVERKOMPATIBEL. Hvis man f.eks. endrer navn på en KompetanseAktivitet, så må man være sikker på at det ikke er sendt + * et slik vedtak til stønaddstatistikk. Den nye kontrakten skal kunne brukes til å lese ALLE meldinger på topic + * + * Hvis det er sendt et slik vedtak, så legger man heller til den nye verdien i VedtakDVHV2 og ikke slette gamle + * + */ + @Test + fun `Skal gi feil hvis det kommer en ny KompetanseAktivitet for søkersAktivitet som det ikke er tatt høyde for mot stønaddstatistkk - Man trenger å oppdatere schema og varsle stønaddstatistikk - Tips i javadoc`() { + val søkersAktivitet = enumValues().map { it.name } + val søkersAktivitetFraEksternKontrakt = + ikkeAvvikleteEnumverdier() + + assertThat(søkersAktivitetFraEksternKontrakt) + .hasSize(søkersAktivitet.size) + .containsAll(søkersAktivitetFraEksternKontrakt) + } + + /** + * Nye KompetanseResultat må legges til VedtakDVHV2 i familie-eksterne-kontrakter når det legges til i Behandling + * + * Endringenen MÅ være BAKOVERKOMPATIBEL. Hvis man f.eks. endrer navn på en KompetanseResultat, så må man være sikker på at det ikke er sendt + * et slik vedtak til stønaddstatistikk. Den nye kontrakten skal kunne brukes til å lese ALLE meldinger på topic + * + * Hvis det er sendt et slik vedtak, så legger man heller til den nye verdien i VedtakDVHV2 og ikke slette gamle + * + */ + @Test + fun `Skal gi feil hvis det kommer en ny KompetanseResultat som det ikke er tatt høyde for mot stønaddstatistkk - Man trenger å oppdatere schema og varsle stønaddstatistikk - Tips i javadoc`() { + val kompetanseResultat = enumValues().map { it.name } + val kompetanseResultatFraEksternKontrakt = + ikkeAvvikleteEnumverdier() + + assertThat(kompetanseResultat) + .hasSize(kompetanseResultatFraEksternKontrakt.size) + .containsAll(kompetanseResultatFraEksternKontrakt) + } + + /** + * Nye behandlingstyper må legges til VedtakDVH2 i familie-eksterne-kontrakter når det legges til i Behandling + * + * Endringenen MÅ være BAKOVERKOMPATIBEL. Hvis man f.eks. endrer navn på en BehandlingType, så må man være sikker på at det ikke er sendt + * et slik vedtak til stønaddstatistikk. Den nye kontrakten skal kunne brukes til å lese ALLE meldinger på topic + * + * Hvis det er sendt et slik vedtak, så legger man heller til den nye verdien i VedtakDVH og ikke slette gamle + * + */ + @Test + fun `Skal gi feil hvis det kommer en ny BehandlingType som det ikke er tatt høyde for mot stønaddstatistkk - Man trenger å oppdatere schema og varsle stønaddstatistikk - Tips i javadoc`() { + val behandlingsTypeIBasak = enumValues().map { it.name } + .filter { it != BehandlingType.TEKNISK_OPPHØR.name } // TEKNISK_OPPHØR er ikke i bruk + val behandlingsTypeFraStønadskontrakt = ikkeAvvikleteEnumverdier() + + assertThat(behandlingsTypeIBasak) + .hasSize(behandlingsTypeFraStønadskontrakt.size) + .containsAll(behandlingsTypeFraStønadskontrakt) + } + + /** + * Nye fagsaktyper må legges til VedtakDVH2 i familie-eksterne-kontrakter når det legges til i Behandling + * + * Endringenen MÅ være BAKOVERKOMPATIBEL. Hvis man f.eks. endrer navn på en FagsakType, så må man være sikker på at det ikke er sendt + * et slik vedtak til stønaddstatistikk. Den nye kontrakten skal kunne brukes til å lese ALLE meldinger på topic + * + * Hvis det er sendt et slik vedtak, så legger man heller til den nye verdien i VedtakDVH og ikke slette gamle + * + */ + @Test + fun `Skal gi feil hvis det kommer en ny FagsakType som det ikke er tatt høyde for mot stønaddstatistkk - Man trenger å oppdatere schema og varsle stønaddstatistikk - Tips i javadoc`() { + val behandlingsTypeIBasak = enumValues().map { it.name } + val behandlingsTypeFraStønadskontrakt = ikkeAvvikleteEnumverdier() + + assertThat(behandlingsTypeIBasak) + .hasSize(behandlingsTypeFraStønadskontrakt.size) + .containsAll(behandlingsTypeFraStønadskontrakt) + } + + inline fun > ikkeAvvikleteEnumverdier(): List { + return enumValues().filter { value -> + try { + val field: Field = T::class.java.getField(value.name) + return@filter !field.isAnnotationPresent(Deprecated::class.java) + } catch (e: NoSuchFieldException) { + return@filter false + } catch (e: SecurityException) { + return@filter false + } + }.map { it.name } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/BehandleF\303\270dselshendelseTaskTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/BehandleF\303\270dselshendelseTaskTest.kt" new file mode 100644 index 000000000..c893662ac --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/BehandleF\303\270dselshendelseTaskTest.kt" @@ -0,0 +1,166 @@ +package no.nav.familie.ba.sak.task + +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.verify +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakStegService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.FagsystemRegelVurdering +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.FagsystemUtfall +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.VelgFagSystemService +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.StartSatsendring +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.task.dto.BehandleFødselshendelseTaskDTO +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.prosessering.error.RekjørSenereException +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +internal class BehandleFødselshendelseTaskTest { + + @Test + fun `håndterer syntetisk fødselsnummer`() { + val autovedtakStegService = + mockk().apply { every { kjørBehandlingFødselshendelse(any(), any()) } returns "" } + settOppBehandleFødselshendelseTask(autovedtakStegService).doTask( + BehandleFødselshendelseTask.opprettTask( + BehandleFødselshendelseTaskDTO( + nyBehandling = NyBehandlingHendelse( + morsIdent = randomFnr(), + barnasIdenter = listOf("61031999277"), + ), + ), + ), + ) + verify { autovedtakStegService.kjørBehandlingFødselshendelse(any(), any()) } + } + + @Test + fun `håndterer vanlig fødselsnummer`() { + val autovedtakStegService = + mockk().apply { every { kjørBehandlingFødselshendelse(any(), any()) } returns "" } + settOppBehandleFødselshendelseTask(autovedtakStegService).doTask( + BehandleFødselshendelseTask.opprettTask( + BehandleFødselshendelseTaskDTO( + nyBehandling = NyBehandlingHendelse( + morsIdent = randomFnr(), + barnasIdenter = listOf("31018721832"), + ), + ), + ), + ) + verify { autovedtakStegService.kjørBehandlingFødselshendelse(any(), any()) } + } + + @Test + fun `skal kaste rekjør senere exception hvis det opprettes satsendring task`() { + assertThrows { + BehandleFødselshendelseTask( + taskRepositoryWrapper = mockk(), + autovedtakStegService = mockk().apply { + every { + kjørBehandlingFødselshendelse( + any(), + any(), + ) + } returns "" + }, + velgFagsystemService = mockk().apply { + every> { velgFagsystem(any()) } returns Pair( + FagsystemRegelVurdering.SEND_TIL_BA, + FagsystemUtfall.IVERKSATTE_BEHANDLINGER_I_BA_SAK, + ) + }, + infotrygdFeedService = mockk(), + personidentService = mockk().apply { every { hentAktør(any()) } returns mockk() }, + startSatsendring = mockk().apply { + every { + sjekkOgOpprettSatsendringVedGammelSats( + any(), + ) + } returns true + }, + ).doTask( + BehandleFødselshendelseTask.opprettTask( + BehandleFødselshendelseTaskDTO( + nyBehandling = NyBehandlingHendelse( + morsIdent = randomFnr(), + barnasIdenter = listOf("31018721832"), + ), + ), + ), + ) + } + } + + @Test + fun `skal opprette oppggavetask dersom det oppstår en funksjonell feil ved fødselshendelse`() { + val taskRepositoryWrapper = mockk().also { every { it.save(any()) } returns mockk() } + val randomAktør = randomAktør() + mockkObject(OpprettVurderFødselshendelseKonsekvensForYtelseOppgave) + + BehandleFødselshendelseTask( + taskRepositoryWrapper = taskRepositoryWrapper, + personidentService = mockk().apply { every { hentAktør(any()) } returns randomAktør }, + autovedtakStegService = mockk(), + velgFagsystemService = mockk().apply { + every> { velgFagsystem(any()) } returns Pair( + FagsystemRegelVurdering.SEND_TIL_BA, + FagsystemUtfall.IVERKSATTE_BEHANDLINGER_I_BA_SAK, + ) + }, + infotrygdFeedService = mockk(), + startSatsendring = mockk().apply { + every { + sjekkOgOpprettSatsendringVedGammelSats( + any(), + ) + }.throws(FunksjonellFeil("funksjonell feil")) + }, + ).doTask( + BehandleFødselshendelseTask.opprettTask( + BehandleFødselshendelseTaskDTO( + nyBehandling = NyBehandlingHendelse( + morsIdent = randomFnr(), + barnasIdenter = listOf("31018721832"), + ), + ), + ), + ) + verify(exactly = 1) { + OpprettVurderFødselshendelseKonsekvensForYtelseOppgave.opprettTask( + ident = randomAktør.aktørId, + oppgavetype = Oppgavetype.VurderLivshendelse, + beskrivelse = "Saksbehandler må vurdere konsekvens for ytelse fordi fødselshendelsen ikke kunne håndteres automatisk", + ) + } + } + + private fun settOppBehandleFødselshendelseTask( + autovedtakStegService: AutovedtakStegService, + ): BehandleFødselshendelseTask = + BehandleFødselshendelseTask( + taskRepositoryWrapper = mockk(), + autovedtakStegService = autovedtakStegService, + velgFagsystemService = mockk().apply { + every> { velgFagsystem(any()) } returns Pair( + FagsystemRegelVurdering.SEND_TIL_BA, + FagsystemUtfall.IVERKSATTE_BEHANDLINGER_I_BA_SAK, + ) + }, + infotrygdFeedService = mockk(), + personidentService = mockk().apply { every { hentAktør(any()) } returns mockk() }, + startSatsendring = mockk().apply { + every { + sjekkOgOpprettSatsendringVedGammelSats( + any(), + ) + } returns false + }, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/GrensesnittavstemMotOppdragTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/GrensesnittavstemMotOppdragTest.kt new file mode 100644 index 000000000..de2b71069 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/GrensesnittavstemMotOppdragTest.kt @@ -0,0 +1,132 @@ +package no.nav.familie.ba.sak.task + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.økonomi.AvstemmingService +import no.nav.familie.ba.sak.task.dto.GrensesnittavstemmingTaskDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.domene.Task +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import java.time.LocalDate +import java.util.Properties + +class GrensesnittavstemMotOppdragTest { + + private lateinit var grensesnittavstemMotOppdrag: GrensesnittavstemMotOppdrag + private lateinit var taskRepositoryMock: TaskRepositoryWrapper + + @BeforeEach + fun setUp() { + val avstemmingServiceMock = mockk() + taskRepositoryMock = mockk() + grensesnittavstemMotOppdrag = GrensesnittavstemMotOppdrag(avstemmingServiceMock, taskRepositoryMock) + } + + @ParameterizedTest + @CsvSource( + "2020-01-06, 2020-01-07, task som kjører en mandag og oppretter task på en tirsdag", + "2020-01-07, 2020-01-08, task som kjører en tirsdag og oppretter task på en onsdag", + "2020-01-08, 2020-01-09, task som kjører en onsdag og oppretter task på en torsdag", + "2020-01-09, 2020-01-10, task som kjører en torsdag og oppretter task på en fredag", + "2020-01-10, 2020-01-13, task som kjører en fredag og oppretter task på en mandag", + ) + fun `Skal opprette task for neste arbeidsdag`(triggerDato: LocalDate, nesteTriggerDato: LocalDate, denneTester: String) { + val slot = slot() + every { taskRepositoryMock.save(capture(slot)) } answers { slot.captured } + + grensesnittavstemMotOppdrag.onCompletion( + Task( + payload = objectMapper.writeValueAsString( + GrensesnittavstemmingTaskDTO( + fomDato = triggerDato.minusDays(1).atStartOfDay(), + tomDato = triggerDato.atStartOfDay(), + ), + ), + properties = Properties(), + type = GrensesnittavstemMotOppdrag.TASK_STEP_TYPE, + + ).medTriggerTid(triggerDato.atTime(8, 0, 0)), + ) + + val lagretTask = slot.captured + val testDto = objectMapper.readValue(lagretTask.payload, GrensesnittavstemmingTaskDTO::class.java) + + assertEquals(triggerDato.atStartOfDay(), testDto.fomDato) + assertEquals(nesteTriggerDato.atStartOfDay(), testDto.tomDato) + assertEquals(nesteTriggerDato.atTime(8, 0, 0), lagretTask.triggerTid) + } + + @Test + fun skalBeregneNesteAvstemmingForSammenhengendeHelligdag() { + val juledagen = LocalDate.of(2019, 12, 24) + + val testDto = grensesnittavstemMotOppdrag.nesteAvstemmingDTO(juledagen) + + assertEquals(LocalDate.of(2019, 12, 27).atStartOfDay(), testDto.tomDato) + assertEquals(LocalDate.of(2019, 12, 24).atStartOfDay(), testDto.fomDato) + } + + @Test + fun skalBeregneNesteAvstemmingForEnkeltHelligdag() { + val nyttårsdag = LocalDate.of(2019, 12, 31) + + val testDto = grensesnittavstemMotOppdrag.nesteAvstemmingDTO(nyttårsdag) + + assertEquals(LocalDate.of(2020, 1, 2).atStartOfDay(), testDto.tomDato) + assertEquals(LocalDate.of(2019, 12, 31).atStartOfDay(), testDto.fomDato) + } + + @Test + fun skalBeregneNesteAvstemmingForLanghelg() { + val valborg = LocalDate.of(2020, 4, 30) + + val testDto = grensesnittavstemMotOppdrag.nesteAvstemmingDTO(valborg) + + assertEquals(LocalDate.of(2020, 5, 4).atStartOfDay(), testDto.tomDato) + assertEquals(LocalDate.of(2020, 4, 30).atStartOfDay(), testDto.fomDato) + } + + @Test + fun skalBeregneNesteAvstemmingForUkedag() { + val enTirsdag = LocalDate.of(2020, 1, 14) + + val testDto = grensesnittavstemMotOppdrag.nesteAvstemmingDTO(enTirsdag) + + assertEquals(LocalDate.of(2020, 1, 15).atStartOfDay(), testDto.tomDato) + assertEquals(LocalDate.of(2020, 1, 14).atStartOfDay(), testDto.fomDato) + } + + @Test + fun skalLageNyAvstemmingstaskEtterJobb() { + val iDag = LocalDate.of(2020, 1, 15).atStartOfDay() + val testTask = Task( + type = GrensesnittavstemMotOppdrag.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + GrensesnittavstemmingTaskDTO( + iDag.minusDays(1), + iDag, + ), + ), + ).medTriggerTid( + iDag.toLocalDate().atTime(8, 0), + ) + val slot = slot() + every { taskRepositoryMock.save(any()) } returns testTask + + grensesnittavstemMotOppdrag.onCompletion(testTask) + + verify(exactly = 1) { taskRepositoryMock.save(capture(slot)) } + assertEquals(GrensesnittavstemMotOppdrag.TASK_STEP_TYPE, slot.captured.type) + assertEquals(iDag.plusDays(1).toLocalDate().atTime(8, 0), slot.captured.triggerTid) + val taskDTO = objectMapper.readValue(slot.captured.payload, GrensesnittavstemmingTaskDTO::class.java) + assertEquals(taskDTO.fomDato, iDag) + assertEquals(taskDTO.tomDato, iDag.plusDays(1)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/HenleggBehandlingTaskTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/HenleggBehandlingTaskTest.kt new file mode 100644 index 000000000..eaecfffb0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/HenleggBehandlingTaskTest.kt @@ -0,0 +1,97 @@ +package no.nav.familie.ba.sak.task + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.integrasjoner.lagTestOppgaveDTO +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.integrasjoner.oppgave.domene.DbOppgave +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.HenleggÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.RestHenleggBehandlingInfo +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.prosessering.domene.Task +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.time.LocalDate + +internal class HenleggBehandlingTaskTest { + + val oppgaveService: OppgaveService = mockk() + val behandlingHentOgPersisterService: BehandlingHentOgPersisterService = mockk() + val stegService: StegService = mockk(relaxed = true) + private val henleggBehandlingTask = HenleggBehandlingTask( + arbeidsfordelingService = mockk(relaxed = true), + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + stegService = stegService, + oppgaveService = oppgaveService, + ) + + @Test + fun `skal ikke henlegge dersom behandlingen allerede er avsluttet`() { + every { behandlingHentOgPersisterService.hent(any()) } returns lagBehandling(status = BehandlingStatus.AVSLUTTET) + val task = opprettTekniskHenleggelseGrunnetSatsendringTask() + henleggBehandlingTask.doTask(task) + verify(exactly = 0) { + stegService.håndterHenleggBehandling(any(), any()) + } + assertThat(task.metadata["Resultat"]).isEqualTo("Behandlingen er allerede avsluttet") + } + + @Test + fun `skal ikke henlegge dersom behandlingsfristen ikke er etter angitt valideringsdato`() { + val behandling = lagBehandling() + every { behandlingHentOgPersisterService.hent(any()) } returns behandling + every { oppgaveService.hentOppgaverSomIkkeErFerdigstilt(any(), any()) } returns + listOf(DbOppgave(behandling = behandling, gsakId = "1", type = Oppgavetype.BehandleSak)) + every { oppgaveService.hentOppgave(any()) } returns lagTestOppgaveDTO(1, Oppgavetype.BehandleSak) + + val task = opprettTekniskHenleggelseGrunnetSatsendringTask() + henleggBehandlingTask.doTask(task) + verify(exactly = 0) { + stegService.håndterHenleggBehandling(any(), any()) + } + assertThat(task.metadata["Resultat"] as String).contains("frist", "Må være etter 2023-04-01") + } + + @Test + fun `skal henlegge med årsak TEKNISK_VEDLIKEHOLD og begrunnelse Satsendring`() { + val behandling = lagBehandling() + every { behandlingHentOgPersisterService.hent(any()) } returns behandling + every { oppgaveService.hentOppgaverSomIkkeErFerdigstilt(any(), any()) } returns + listOf(DbOppgave(behandling = behandling, gsakId = "1", type = Oppgavetype.BehandleSak)) + every { oppgaveService.hentOppgave(any()) } returns lagTestOppgaveDTO(1, Oppgavetype.BehandleSak).copy( + fristFerdigstillelse = LocalDate.of(2023, 4, 2).toString(), + ) + + val task = opprettTekniskHenleggelseGrunnetSatsendringTask() + henleggBehandlingTask.doTask(task) + + val henleggBehandlingInfo = slot() + verify(exactly = 1) { + stegService.håndterHenleggBehandling(behandling, capture(henleggBehandlingInfo)) + } + assertThat(henleggBehandlingInfo.captured.årsak).isEqualTo(HenleggÅrsak.TEKNISK_VEDLIKEHOLD) + assertThat(henleggBehandlingInfo.captured.begrunnelse).isEqualTo("Satsendring") + assertThat(task.metadata["Resultat"]).isEqualTo("Henleggelse kjørt OK") + } + + private fun opprettTekniskHenleggelseGrunnetSatsendringTask(): Task { + return Task( + type = HenleggBehandlingTask.TASK_STEP_TYPE, + payload = objectMapper.writeValueAsString( + HenleggBehandlingTaskDTO( + behandlingId = 1, + årsak = HenleggÅrsak.TEKNISK_VEDLIKEHOLD, + begrunnelse = "Satsendring", + validerOppgavefristErEtterDato = LocalDate.of(2023, 4, 1), + ), + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragStartTaskTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragStartTaskTest.kt new file mode 100644 index 000000000..3989ab724 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/KonsistensavstemMotOppdragStartTaskTest.kt @@ -0,0 +1,130 @@ +package no.nav.familie.ba.sak.task + +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.integrasjoner.økonomi.AvstemmingService +import no.nav.familie.ba.sak.task.dto.KonsistensavstemmingStartTaskDTO +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.domene.Task +import org.junit.jupiter.api.Test +import java.time.LocalDateTime +import java.util.UUID + +internal class KonsistensavstemMotOppdragStartTaskTest { + private val avstemmingService = mockk() + private val startTask = KonsistensavstemMotOppdragStartTask(avstemmingService) + + @Test + fun `Ved kjøring av task første gang, så skal den sende start til økonomi, opprette finn perioder til avstemming task og og sende avslutt til økonomi`() { + val (transaksjonsId, task) = opprettStartTask() + + every { avstemmingService.harBatchStatusFerdig(123L) } returns false + every { avstemmingService.erKonsistensavstemmingStartet(transaksjonsId) } returns false + every { + avstemmingService.skalOppretteFinnPerioderForRelevanteBehandlingerTask( + transaksjonsId, + any(), + ) + } returns true + every { + avstemmingService.erKonsistensavstemmingKjørtForTransaksjonsidOgChunk( + transaksjonsId, + range(1, 3), + ) + } returns false + justRun { avstemmingService.sendKonsistensavstemmingStart(any(), transaksjonsId) } + mockTreSiderMedSisteBehandlinger() + justRun { + avstemmingService.opprettKonsistensavstemmingFinnPerioderForRelevanteBehandlingerTask(any(), any()) + } + justRun { avstemmingService.opprettKonsistensavstemmingAvsluttTask(any()) } + + startTask.doTask(task) + + verify(exactly = 1) { avstemmingService.sendKonsistensavstemmingStart(any(), transaksjonsId) } + verify(exactly = 3) { + avstemmingService.opprettKonsistensavstemmingFinnPerioderForRelevanteBehandlingerTask( + any(), + any(), + ) + } + verify(exactly = 1) { avstemmingService.opprettKonsistensavstemmingAvsluttTask(any()) } + } + + @Test + fun `Ved rekjøring av task som er alt kjørt, så skal den avslutte uten å sende meldinger eller generere perioder`() { + val (transaksjonsId, task) = opprettStartTask() + + every { avstemmingService.harBatchStatusFerdig(123L) } returns true + + startTask.doTask(task) + + verify(exactly = 0) { avstemmingService.sendKonsistensavstemmingStart(any(), transaksjonsId) } + verify(exactly = 0) { + avstemmingService.opprettKonsistensavstemmingFinnPerioderForRelevanteBehandlingerTask( + any(), + any(), + ) + } + verify(exactly = 0) { avstemmingService.opprettKonsistensavstemmingAvsluttTask(any()) } + } + + @Test + fun `Ved rekjøring av task som er delvis kjørt, så skal den ikke sende start melding, men opprette finn perioder til avstemminger som det ikke er sendt og sende avslutt melding til økonomi`() { + val (transaksjonsId, task) = opprettStartTask() + + every { avstemmingService.harBatchStatusFerdig(123L) } returns false + every { avstemmingService.erKonsistensavstemmingStartet(transaksjonsId) } returns true + every { + avstemmingService.skalOppretteFinnPerioderForRelevanteBehandlingerTask( + transaksjonsId, + range(1, 2), + ) + } returns false + every { + avstemmingService.skalOppretteFinnPerioderForRelevanteBehandlingerTask( + transaksjonsId, + 3, + ) + } returns true + + justRun { avstemmingService.sendKonsistensavstemmingStart(any(), transaksjonsId) } + mockTreSiderMedSisteBehandlinger() + justRun { + avstemmingService.opprettKonsistensavstemmingFinnPerioderForRelevanteBehandlingerTask(any(), any()) + } + justRun { avstemmingService.opprettKonsistensavstemmingAvsluttTask(any()) } + + startTask.doTask(task) + + verify(exactly = 0) { avstemmingService.sendKonsistensavstemmingStart(any(), transaksjonsId) } + verify(exactly = 1) { + avstemmingService.opprettKonsistensavstemmingFinnPerioderForRelevanteBehandlingerTask( + any(), + any(), + ) + } + verify(exactly = 1) { avstemmingService.opprettKonsistensavstemmingAvsluttTask(any()) } + } + + private fun mockTreSiderMedSisteBehandlinger() { + every { avstemmingService.hentSisteIverksatteBehandlingerFraLøpendeFagsaker() } returns (1L..1450).toList() + } + + private fun opprettStartTask(): Pair { + val avstemmingdato = LocalDateTime.of(2022, 4, 1, 0, 0) + val batchId = 123L + val transaksjonsId = UUID.randomUUID() + val payload = objectMapper.writeValueAsString( + KonsistensavstemmingStartTaskDTO( + batchId = batchId, + avstemmingdato = avstemmingdato, + transaksjonsId = transaksjonsId, + ), + ) + val task = Task(payload = payload, type = KonsistensavstemMotOppdragStartTask.TASK_STEP_TYPE) + return Pair(transaksjonsId, task) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/PubliserVedtakV2TaskTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/PubliserVedtakV2TaskTest.kt new file mode 100644 index 000000000..f81cedd5c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/PubliserVedtakV2TaskTest.kt @@ -0,0 +1,65 @@ +package no.nav.familie.ba.sak.task + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.common.EnvService +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.statistikk.producer.KafkaProducer +import no.nav.familie.ba.sak.statistikk.stønadsstatistikk.StønadsstatistikkService +import no.nav.familie.eksterne.kontrakter.VedtakDVHV2 +import no.nav.familie.prosessering.domene.Task +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +class PubliserVedtakV2TaskTest { + + @MockK(relaxed = true) + private lateinit var taskRepositoryMock: TaskRepositoryWrapper + + @MockK(relaxed = true) + private lateinit var kafkaProducerMock: KafkaProducer + + @MockK(relaxed = true) + private lateinit var stønadsstatistikkService: StønadsstatistikkService + + @MockK + private lateinit var env: EnvService + + @InjectMockKs + private lateinit var publiserVedtakV2Task: PubliserVedtakV2Task + + @BeforeEach + fun initMocks() { + every { env.erProd() } returns false + } + + @Test + fun skalOppretteTask() { + val task = PubliserVedtakV2Task.opprettTask("ident", 42) + + Assertions.assertThat(task.payload).isEqualTo("42") + Assertions.assertThat(task.metadata["personIdent"]).isEqualTo("ident") + Assertions.assertThat(task.type).isEqualTo("publiserVedtakV2Task") + } + + @Test + fun `skal kjøre task`() { + every { kafkaProducerMock.sendMessageForTopicVedtakV2(ofType(VedtakDVHV2::class)) }.returns(100) + every { taskRepositoryMock.save(any()) } returns Task(type = "test", payload = "") + + val task = PubliserVedtakV2Task.opprettTask("ident", 42) + publiserVedtakV2Task.doTask(task) + taskRepositoryMock.save(task) + + val slot = slot() + verify(exactly = 1) { taskRepositoryMock.save(capture(slot)) } + Assertions.assertThat(slot.captured.metadata["offset"]).isEqualTo("100") + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/SendVedtakTilInfotrygdTaskTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/SendVedtakTilInfotrygdTaskTest.kt new file mode 100644 index 000000000..192d05624 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/SendVedtakTilInfotrygdTaskTest.kt @@ -0,0 +1,63 @@ +package no.nav.familie.ba.sak.task + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelseUtvidet +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdFeedClient +import no.nav.familie.ba.sak.integrasjoner.infotrygd.domene.InfotrygdVedtakFeedDto +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.time.YearMonth + +internal class SendVedtakTilInfotrygdTaskTest { + + val infotrygdFeedClient: InfotrygdFeedClient = mockk() + val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService = mockk() + private val sendVedtakTilInfotrygdTask = SendVedtakTilInfotrygdTask( + infotrygdFeedClient, + andelerTilkjentYtelseOgEndreteUtbetalingerService, + ) + + @Test + fun `skal sende vedtak til infotrygd ved første gang behandling`() { + val behandling = lagBehandling(status = BehandlingStatus.AVSLUTTET) + val fom = YearMonth.now().minusMonths(2) + every { andelerTilkjentYtelseOgEndreteUtbetalingerService.finnAndelerTilkjentYtelseMedEndreteUtbetalinger(behandling.id) } returns lagAndelerMedFom(behandling, fom) + val slot = slot() + every { infotrygdFeedClient.sendVedtakFeedTilInfotrygd(capture(slot)) } returns Unit + + sendVedtakTilInfotrygdTask.doTask(SendVedtakTilInfotrygdTask.opprettTask(behandling.fagsak.aktør.aktivFødselsnummer(), behandling.id)) + + assertThat(slot.captured.fnrStoenadsmottaker).isEqualTo(behandling.fagsak.aktør.aktivFødselsnummer()) + assertThat(slot.captured.datoStartNyBa).isEqualTo(fom.atDay(1)) + } + + private fun lagAndelerMedFom(behandling: Behandling, fom: YearMonth): List { + val andel1 = lagAndelTilkjentYtelseUtvidet( + fom.toString(), + fom.plusYears(6).toString(), + YtelseType.ORDINÆR_BARNETRYGD, + behandling = behandling, + person = lagPerson(), + periodeIdOffset = 1, + ) + + val andel2 = lagAndelTilkjentYtelseUtvidet( + fom.plusYears(6).toString(), + fom.plusYears(12).toString(), + YtelseType.ORDINÆR_BARNETRYGD, + behandling = behandling, + person = lagPerson(), + periodeIdOffset = 2, + ) + return listOf(AndelTilkjentYtelseMedEndreteUtbetalinger.utenEndringer(andel1), AndelTilkjentYtelseMedEndreteUtbetalinger.utenEndringer(andel2)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/TaskRepositoryTestConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/TaskRepositoryTestConfig.kt new file mode 100644 index 000000000..ae2586a76 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/TaskRepositoryTestConfig.kt @@ -0,0 +1,40 @@ +package no.nav.familie.ba.sak.task + +import io.mockk.clearMocks +import io.mockk.mockk +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile + +@TestConfiguration +class TaskRepositoryTestConfig { + + @Bean + @Profile("mock-task-repository") + fun mockTaskRepository(): TaskRepositoryWrapper { + return clearMockTaskRepository(mockk(relaxed = true)) + } + + @Bean + @Profile("mock-task-service") + @Primary + fun mockTaskService(): OpprettTaskService { + return clearMockTaskService(mockk(relaxed = true)) + } + + companion object { + fun clearMockTaskRepository(mockTaskRepository: TaskRepositoryWrapper): TaskRepositoryWrapper { + clearMocks(mockTaskRepository) + + return mockTaskRepository + } + + fun clearMockTaskService(mockTaskService: OpprettTaskService): OpprettTaskService { + clearMocks(mockTaskService) + + return mockTaskService + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/TaskUtilsTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/TaskUtilsTest.kt new file mode 100644 index 000000000..676a20245 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/task/TaskUtilsTest.kt @@ -0,0 +1,38 @@ +package no.nav.familie.ba.sak.task + +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.springframework.core.env.Environment +import java.time.LocalDateTime + +class TaskUtilsTest { + + @ParameterizedTest + @CsvSource( + "2020-06-09T13:37:00, 2020-06-09T14:37:00", // Innenfor dagtid + "2020-06-09T21:37:00, 2020-06-10T06:37:00", // Utenfor dagtid, men på en hverdag. Venter til dagen etter klokken 6 + "2020-06-12T19:37:00, 2020-06-12T20:37:00", // Innenfor dagtid på en fredag + "2020-06-12T21:37:00, 2020-06-15T06:37:00", // Utenfor dagtid på en fredag. Venter til mandag morgen + "2020-06-13T09:37:00, 2020-06-15T06:37:00", // Lørdag morgen. Venter til mandag morgen + "2020-06-13T19:37:00, 2020-06-15T06:37:00", // Lørdag kveld. Venter til mandag morgen + "2020-06-14T09:37:00, 2020-06-15T06:37:00", // Søndag morgen. Venter til mandag morgen + "2020-06-14T19:37:00, 2020-06-15T06:37:00", // Søndag kveld. Venter til mandag morgen + "2020-06-14T21:37:00, 2020-06-15T06:37:00", // Søndag etter 21. Venter til mandag morgen + "2020-05-17T15:37:00, 2020-05-18T06:37:00", // Innenfor dagtid 17 mai. Venter til morgenen etter + "2020-05-17T05:37:00, 2020-05-18T06:37:00", // Før dagtid 17 mai. Venter til morgenen etter + "2020-05-17T22:37:00, 2020-05-18T06:37:00", // Etter dagtid 17 mai. Venter til morgenen etter + "2021-05-14T21:30:00, 2021-05-18T06:30:00", // 14 mai er fredag, 17 mai er mandag og fridag. Venter til 18 mai klokken 6 + ) + fun `skal returnere neste arbeidsdag `(input: LocalDateTime, expected: LocalDateTime) { + mockkStatic(LocalDateTime::class) + mockk(relaxed = true) + + every { LocalDateTime.now() } returns input + + assertEquals(expected, nesteGyldigeTriggertidForBehandlingIHverdager(60)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/util/BrukerContextUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/util/BrukerContextUtil.kt new file mode 100644 index 000000000..9bea105cd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/util/BrukerContextUtil.kt @@ -0,0 +1,52 @@ +package no.nav.familie.ba.sak.util + +import io.mockk.every +import io.mockk.mockk +import jakarta.servlet.http.HttpServletRequest +import no.nav.security.token.support.core.context.TokenValidationContext +import no.nav.security.token.support.core.jwt.JwtTokenClaims +import no.nav.security.token.support.spring.SpringTokenValidationContextHolder +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.web.context.request.RequestAttributes +import org.springframework.web.context.request.RequestContextHolder +import org.springframework.web.context.request.ServletRequestAttributes +import java.util.UUID + +object BrukerContextUtil { + + fun clearBrukerContext() { + RequestContextHolder.resetRequestAttributes() + } + + fun mockBrukerContext( + preferredUsername: String = "A", + groups: List = emptyList(), + servletRequest: HttpServletRequest = MockHttpServletRequest(), + ) { + val tokenValidationContext = mockk() + val jwtTokenClaims = mockk() + val requestAttributes = ServletRequestAttributes(servletRequest) + RequestContextHolder.setRequestAttributes(requestAttributes) + requestAttributes.setAttribute( + SpringTokenValidationContextHolder::class.java.name, + tokenValidationContext, + RequestAttributes.SCOPE_REQUEST, + ) + every { tokenValidationContext.getClaims("azuread") } returns jwtTokenClaims + every { jwtTokenClaims.get("preferred_username") } returns preferredUsername + every { jwtTokenClaims.get("NAVident") } returns preferredUsername + every { jwtTokenClaims.get("name") } returns preferredUsername + every { jwtTokenClaims.get("groups") } returns groups + every { jwtTokenClaims.get("oid") } returns UUID.randomUUID().toString() + every { jwtTokenClaims.get("sub") } returns UUID.randomUUID().toString() + } + + fun testWithBrukerContext(preferredUsername: String = "A", groups: List = emptyList(), fn: () -> T): T { + try { + mockBrukerContext(preferredUsername, groups) + return fn() + } finally { + clearBrukerContext() + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/util/SatserForTester.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/util/SatserForTester.kt new file mode 100644 index 000000000..05353fac2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/enhetstester/kotlin/no/nav/familie/ba/sak/util/SatserForTester.kt @@ -0,0 +1,24 @@ +package no.nav.familie.ba.sak.util + +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.beregning.SatsService +import no.nav.familie.ba.sak.kjerne.beregning.domene.Sats +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import java.time.LocalDate + +fun tilleggOrdinærSatsTilTester(): Int = + SatsService.finnAlleSatserFor(SatsType.TILLEGG_ORBA).findLast { + it.gyldigFom <= LocalDate.now().plusDays(1) + }!!.beløp + +fun sisteUtvidetSatsTilTester(): Int = SatsService.finnSisteSatsFor(SatsType.UTVIDET_BARNETRYGD).beløp + +fun sisteSmåbarnstilleggSatsTilTester(): Int = SatsService.finnSisteSatsFor(SatsType.SMA).beløp + +fun sisteTilleggOrdinærSats(): Double = SatsService.finnSisteSatsFor(SatsType.TILLEGG_ORBA).beløp.toDouble() + +fun tilleggOrdinærSatsNesteMånedTilTester(): Sats = + SatsService.finnAlleSatserFor(SatsType.TILLEGG_ORBA).findLast { + it.gyldigFom.toYearMonth() <= LocalDate.now().nesteMåned() + }!! diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/DevLauncher.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/DevLauncher.kt new file mode 100644 index 000000000..d52fe4441 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/DevLauncher.kt @@ -0,0 +1,24 @@ +import no.nav.familie.ba.sak.config.ApplicationConfig +import org.springframework.boot.builder.SpringApplicationBuilder + +object DevLauncher { + + @JvmStatic + fun main(args: Array) { + System.setProperty("spring.profiles.active", "dev") + System.setProperty("prosessering.enabled", "false") + val app = SpringApplicationBuilder(ApplicationConfig::class.java) + .profiles( + "dev", + "mock-brev-klient", + "mock-økonomi", + "mock-infotrygd-feed", + "mock-infotrygd-barnetrygd", + "mock-pdl", + "mock-ident-client", + "mock-tilbakekreving-klient", + "task-scheduling", + ) + app.run(*args) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/DevLauncherPostgres.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/DevLauncherPostgres.kt new file mode 100644 index 000000000..0b20f787b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/DevLauncherPostgres.kt @@ -0,0 +1,25 @@ +import no.nav.familie.ba.sak.common.DbContainerInitializer +import no.nav.familie.ba.sak.config.ApplicationConfig +import org.springframework.boot.builder.SpringApplicationBuilder + +fun main(args: Array) { + System.setProperty("spring.profiles.active", "postgres") + val springBuilder = SpringApplicationBuilder(ApplicationConfig::class.java).profiles( + "dev", + "postgres", + "mock-brev-klient", + "mock-økonomi", + "mock-infotrygd-feed", + "mock-infotrygd-barnetrygd", + "mock-pdl", + "mock-ident-client", + "mock-tilbakekreving-klient", + "task-scheduling", + ) + + if (args.contains("--dbcontainer")) { + springBuilder.initializers(DbContainerInitializer()) + } + + springBuilder.run(*args) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/DevLauncherPostgresPreprod.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/DevLauncherPostgresPreprod.kt new file mode 100644 index 000000000..03b1aee3d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/DevLauncherPostgresPreprod.kt @@ -0,0 +1,45 @@ +import no.nav.familie.ba.sak.common.DbContainerInitializer +import no.nav.familie.ba.sak.config.ApplicationConfig +import no.nav.familie.ba.sak.config.featureToggle.miljø.Profil +import org.springframework.boot.builder.SpringApplicationBuilder +import java.io.BufferedReader +import java.io.InputStreamReader + +fun main(args: Array) { + System.setProperty("spring.profiles.active", Profil.DevPostgresPreprod.navn) + val springBuilder = SpringApplicationBuilder(ApplicationConfig::class.java).profiles( + "mock-økonomi", + "mock-infotrygd-feed", + "mock-tilbakekreving-klient", + "task-scheduling", + "mock-infotrygd-barnetrygd", + "mock-leader-client", + ) + + if (args.contains("--dbcontainer")) { + springBuilder.initializers(DbContainerInitializer()) + } + + if (!args.contains("--manuellMiljø")) { + settClientIdOgSecret() + } + + springBuilder.run(* args) +} + +private fun settClientIdOgSecret() { + val cmd = "src/test/resources/hentMiljøvariabler.sh" + + val process = ProcessBuilder(cmd).start() + + if (process.waitFor() == 1) { + error("Klarte ikke hente variabler fra Nais. Er du logget på Naisdevice og gcloud?") + } + + val inputStream = BufferedReader(InputStreamReader(process.inputStream)) + inputStream.readLine() // "Switched to context dev-gcp" + inputStream.readLine().split(";") + .map { it.split("=") } + .map { System.setProperty(it[0], it[1]) } + inputStream.close() +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/RunCucumberTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/RunCucumberTest.kt new file mode 100644 index 000000000..efe48ec80 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/RunCucumberTest.kt @@ -0,0 +1,11 @@ +import io.cucumber.core.options.Constants.PLUGIN_PROPERTY_NAME +import org.junit.platform.suite.api.ConfigurationParameter +import org.junit.platform.suite.api.IncludeEngines +import org.junit.platform.suite.api.SelectClasspathResource +import org.junit.platform.suite.api.Suite + +@Suite(failIfNoTests = false) +@IncludeEngines("cucumber") +@SelectClasspathResource("no/nav/familie/ba/sak/cucumber") +@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty") +class RunCucumberTest diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ApplicationTests.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ApplicationTests.kt new file mode 100644 index 000000000..c1897bbba --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ApplicationTests.kt @@ -0,0 +1,11 @@ +package no.nav.familie.ba.sak + +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import org.junit.jupiter.api.Test + +class ApplicationTests : AbstractSpringIntegrationTest() { + + @Test + fun contextLoads() { + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/WebSpringAuthTestRunner.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/WebSpringAuthTestRunner.kt new file mode 100644 index 000000000..35e381cb1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/WebSpringAuthTestRunner.kt @@ -0,0 +1,115 @@ +package no.nav.familie.ba.sak + +import no.nav.familie.ba.sak.common.DbContainerInitializer +import no.nav.familie.ba.sak.config.AbstractMockkSpringRunner +import no.nav.familie.ba.sak.config.ApplicationConfig +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext.SYSTEM_FORKORTELSE +import no.nav.security.mock.oauth2.MockOAuth2Server +import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback +import no.nav.security.token.support.spring.test.EnableMockOAuth2Server +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.web.client.RestTemplate + +@SpringBootTest( + classes = [ApplicationConfig::class], + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = [ + "no.nav.security.jwt.issuer.azuread.discoveryUrl: http://localhost:\${mock-oauth2-server.port}/azuread/.well-known/openid-configuration", + "no.nav.security.jwt.issuer.azuread.accepted_audience: some-audience", + "rolle.veileder: VEILDER", + "rolle.saksbehandler: SAKSBEHANDLER", + "rolle.beslutter: BESLUTTER", + ], +) +@ExtendWith(SpringExtension::class) +@EnableMockOAuth2Server +@ContextConfiguration(initializers = [DbContainerInitializer::class]) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Tag("integration") +abstract class WebSpringAuthTestRunner : AbstractMockkSpringRunner() { + + @Autowired + lateinit var databaseCleanupService: DatabaseCleanupService + + @Autowired + lateinit var restTemplate: RestTemplate + + @Autowired + lateinit var mockOAuth2Server: MockOAuth2Server + + @LocalServerPort + private val port = 0 + + fun hentUrl(path: String) = "http://localhost:$port$path" + + fun token( + claims: Map, + subject: String = DEFAULT_SUBJECT, + audience: String = DEFAULT_AUDIENCE, + issuerId: String = DEFAULT_ISSUER_ID, + clientId: String = DEFAULT_CLIENT_ID, + ): String { + return mockOAuth2Server.issueToken( + issuerId, + clientId, + DefaultOAuth2TokenCallback( + issuerId = issuerId, + subject = subject, + audience = listOf(audience), + claims = claims, + expiry = 3600, + ), + ).serialize() + } + + fun hentHeaders(groups: List? = null): HttpHeaders { + val httpHeaders = HttpHeaders() + httpHeaders.contentType = MediaType.APPLICATION_JSON + httpHeaders.setBearerAuth( + token( + mapOf( + "groups" to (groups ?: listOf(BehandlerRolle.SAKSBEHANDLER.name)), + "azp" to "azp-test", + "name" to "Mock McMockface", + "NAVident" to "Z0000", + ), + ), + ) + return httpHeaders + } + + fun hentHeadersForSystembruker(groups: List? = null): HttpHeaders { + val httpHeaders = HttpHeaders() + httpHeaders.contentType = MediaType.APPLICATION_JSON + httpHeaders.setBearerAuth( + token( + mapOf( + "groups" to (groups ?: listOf(BehandlerRolle.SYSTEM.name)), + "azp" to "azp-test", + "name" to SYSTEM_FORKORTELSE, + "preferred_username" to SYSTEM_FORKORTELSE, + ), + ), + ) + return httpHeaders + } + + companion object { + + const val DEFAULT_ISSUER_ID = "azuread" + const val DEFAULT_SUBJECT = "subject" + const val DEFAULT_AUDIENCE = "some-audience" + const val DEFAULT_CLIENT_ID = "theclientid" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/common/EksternTjenesteKallerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/common/EksternTjenesteKallerTest.kt new file mode 100644 index 000000000..d1ca4d44f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/common/EksternTjenesteKallerTest.kt @@ -0,0 +1,109 @@ +package no.nav.familie.ba.sak.common + +import com.github.tomakehurst.wiremock.client.WireMock.aResponse +import com.github.tomakehurst.wiremock.client.WireMock.post +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonException +import no.nav.familie.ba.sak.integrasjoner.lagTestOppgave +import no.nav.familie.http.client.RessursException +import no.nav.familie.kontrakter.felles.Ressurs.Companion.failure +import no.nav.familie.kontrakter.felles.Ressurs.Companion.ikkeTilgang +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppgave.OppgaveResponse +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.slf4j.MDC +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.http.HttpStatus +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.RestOperations +import java.net.URI + +class EksternTjenesteKallerTest : AbstractSpringIntegrationTest() { + + @Autowired + @Qualifier("jwtBearer") + lateinit var restOperations: RestOperations + + lateinit var integrasjonClient: IntegrasjonClient + + @BeforeEach + fun setUp() { + integrasjonClient = IntegrasjonClient( + URI.create(wireMockServer.baseUrl() + "/api"), + restOperations, + ) + } + + @AfterEach + fun clearTest() { + MDC.clear() + wireMockServer.resetAll() + } + + @Test + @Tag("integration") + fun `Tjeneste svarer med 200 OK og feilet ressurs`() { + wireMockServer.stubFor( + post("/api/oppgave/opprett").willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(failure("Opprett oppgave feilet"))), + ), + ) + + assertThrows { integrasjonClient.opprettOppgave(lagTestOppgave()) } + } + + @Test + @Tag("integration") + fun `Tjeneste svarer med 500 og skal feile`() { + wireMockServer.stubFor( + post("/api/oppgave/opprett").willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json"), + ), + ) + + assertThrows { integrasjonClient.opprettOppgave(lagTestOppgave()) } + } + + @Test + @Tag("integration") + fun `Tjeneste svarer med forbidden og skal kaste feil videre`() { + wireMockServer.stubFor( + post("/api/oppgave/opprett").willReturn( + aResponse() + .withStatus(403) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(ikkeTilgang("Ikke tilgang til å opprett oppgave"))), + ), + ) + + val feil = + assertThrows { integrasjonClient.opprettOppgave(lagTestOppgave()) } + assertTrue(feil.httpStatus == HttpStatus.FORBIDDEN) + assertTrue(feil.message?.contains("Ikke tilgang til å opprett oppgave") == true) + } + + @Test + @Tag("integration") + fun `Tjeneste svarer med 404 og not found skal ligge på integrasjon exception`() { + wireMockServer.stubFor( + post("/api/oppgave/opprett").willReturn( + aResponse() + .withStatus(404), + ), + ) + + assertThrows { integrasjonClient.opprettOppgave(lagTestOppgave()) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/common/EnvServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/common/EnvServiceTest.kt new file mode 100644 index 000000000..85be27a0a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/common/EnvServiceTest.kt @@ -0,0 +1,23 @@ +package no.nav.familie.ba.sak.common + +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.core.env.Environment + +class EnvServiceTest( + @Autowired + private val environment: Environment, +) : AbstractSpringIntegrationTest() { + + @Test + fun `erDev skal returnere true dersom appen er startet med dev-profil`() { + val envService = EnvService(environment) + + assertTrue(envService.erDev()) + assertFalse(envService.erProd()) + assertFalse(envService.erPreprod()) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/common/http/interceptor/RolletilgangInterceptorTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/common/http/interceptor/RolletilgangInterceptorTest.kt new file mode 100644 index 000000000..cf3e2ff88 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/common/http/interceptor/RolletilgangInterceptorTest.kt @@ -0,0 +1,25 @@ +package no.nav.familie.ba.sak.common.http.interceptor + +import io.mockk.mockk +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.RolleConfig +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class RolletilgangInterceptorTest : AbstractSpringIntegrationTest() { + + @Autowired + lateinit var rolleConfig: RolleConfig + + val request = mockk() + val response = mockk() + val handler = mockk() + + @Test + fun `Verifiser at systembruker har tilgang`() { + assertTrue(RolletilgangInterceptor(rolleConfig).preHandle(request, response, handler)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/AbstractSpringIntegrationTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/AbstractSpringIntegrationTest.kt new file mode 100644 index 000000000..fb615b089 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/AbstractSpringIntegrationTest.kt @@ -0,0 +1,44 @@ +package no.nav.familie.ba.sak.config + +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import no.nav.familie.ba.sak.common.DbContainerInitializer +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.TestInstance +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration + +@SpringBootTest +@ActiveProfiles( + "postgres", + "integrasjonstest", + "mock-økonomi", + "mock-pdl", + "mock-ident-client", + "mock-task-repository", + "mock-infotrygd-barnetrygd", + "mock-tilbakekreving-klient", + "mock-brev-klient", + "mock-infotrygd-feed", + "mock-oauth", + "mock-rest-template-config", + "mock-localdate-service", + "mock-sanity-client", +) +@ContextConfiguration(initializers = [DbContainerInitializer::class]) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Tag("integration") +abstract class AbstractSpringIntegrationTest : AbstractMockkSpringRunner() { + protected final val wireMockServer = WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()) + + init { + wireMockServer.start() + } + + @AfterAll + fun stopWiremockServer() { + wireMockServer.stop() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/DatabaseCleanupService.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/DatabaseCleanupService.kt new file mode 100644 index 000000000..de01ba3d9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/DatabaseCleanupService.kt @@ -0,0 +1,79 @@ +package no.nav.familie.ba.sak.config + +import jakarta.persistence.EntityManager +import jakarta.persistence.Table +import jakarta.persistence.metamodel.Metamodel +import jakarta.transaction.Transactional +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.Profile +import org.springframework.core.env.Environment +import org.springframework.data.relational.core.mapping.RelationalMappingContext +import org.springframework.data.relational.core.sql.IdentifierProcessing +import org.springframework.stereotype.Service +import kotlin.reflect.full.findAnnotation +import org.springframework.data.relational.core.mapping.Table as JdbcTable + +/** + * Test utility service that allows to truncate all tables in the test database. + * Inspired by: http://www.greggbolinger.com/truncate-all-tables-in-spring-boot-jpa-app/ + * @author Sebastien Dubois + */ +@Service +@Profile("dev", "postgres") +class DatabaseCleanupService( + private val entityManager: EntityManager, + private val environment: Environment, + private val relationalMappingContext: RelationalMappingContext, +) { + + private val logger = LoggerFactory.getLogger(DatabaseCleanupService::class.java) + + private var tableNames: List? = null + /** + * Uses the JPA metamodel to find all managed types then try to get the [Table] annotation's from each (if present) to discover the table name. + * If the [Table] annotation is not defined then we skip that entity (oops :p) + * JDBC tables must be found out in another way + */ + get() { + if (field == null) { + val metaModel: Metamodel = entityManager.metamodel + field = metaModel.managedTypes + .filter { + it.javaType.kotlin.findAnnotation() != null || it.javaType.kotlin.findAnnotation() != null + } + .map { + val tableAnnotation: Table? = it.javaType.kotlin.findAnnotation() + val jdbcTableAnnotation: JdbcTable? = it.javaType.kotlin.findAnnotation() + tableAnnotation?.name ?: jdbcTableAnnotation?.value + ?: throw IllegalStateException("should never get here") + } + getJdbcTableNames() + } + return field + } + + private fun getJdbcTableNames(): List { + return relationalMappingContext.persistentEntities.map { entity -> + entity.tableName.toSql(IdentifierProcessing.NONE) + } + } + + /** + * Utility method that truncates all identified tables + */ + @Transactional + fun truncate() { + logger.info("Truncating tables: $tableNames") + entityManager.flush() + if (environment.activeProfiles.contains("postgres")) { + tableNames?.forEach { tableName -> + entityManager.createNativeQuery("TRUNCATE TABLE $tableName CASCADE").executeUpdate() + } + } else { + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TO FALSE").executeUpdate() + tableNames?.forEach { tableName -> + entityManager.createNativeQuery("TRUNCATE TABLE $tableName").executeUpdate() + } + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TO TRUE").executeUpdate() + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/FeatureToggleMockConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/FeatureToggleMockConfig.kt new file mode 100644 index 000000000..c85513d47 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/FeatureToggleMockConfig.kt @@ -0,0 +1,31 @@ +package no.nav.familie.ba.sak.config + +import io.mockk.mockk +import no.nav.familie.ba.sak.config.featureToggle.FeatureToggleInitializer +import no.nav.familie.ba.sak.config.featureToggle.miljø.Profil +import no.nav.familie.ba.sak.config.featureToggle.miljø.erAktiv +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.core.env.Environment + +@TestConfiguration +class FeatureToggleMockConfig( + @Autowired val featureToggleInitializer: FeatureToggleInitializer, + @Autowired private val environment: Environment, +) { + + @Bean + @Primary + fun mockFeatureToggleService(): FeatureToggleService { + if (environment.erAktiv(Profil.Integrasjonstest)) { + val mockFeatureToggleService = mockk(relaxed = true) + + ClientMocks.clearFeatureToggleMocks(mockFeatureToggleService) + + return mockFeatureToggleService + } + return featureToggleInitializer.featureToggle() + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/UnleashServiceMockConfig.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/UnleashServiceMockConfig.kt new file mode 100644 index 000000000..6942fc23c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/UnleashServiceMockConfig.kt @@ -0,0 +1,30 @@ +package no.nav.familie.ba.sak.config + +import io.mockk.mockk +import no.nav.familie.ba.sak.config.featureToggle.miljø.Profil +import no.nav.familie.ba.sak.config.featureToggle.miljø.erAktiv +import no.nav.familie.unleash.UnleashService +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.core.env.Environment + +@TestConfiguration +class UnleashServiceMockConfig( + private val unleashService: UnleashService, + private val environment: Environment, +) { + + @Bean + @Primary + fun mockUnleashService(): UnleashService { + if (environment.erAktiv(Profil.Integrasjonstest)) { + val mockUnleashService = mockk(relaxed = true) + + ClientMocks.clearUnleashServiceMocks(mockUnleashService) + + return mockUnleashService + } + return unleashService + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleServiceTest.kt new file mode 100644 index 000000000..8724c3e73 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/featureToggle/FeatureToggleServiceTest.kt @@ -0,0 +1,20 @@ +package no.nav.familie.ba.sak.config.featureToggle + +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.FeatureToggleService +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +@Tag("integration") +class FeatureToggleServiceTest( + @Autowired + private val featureToggleService: FeatureToggleService, +) : AbstractSpringIntegrationTest() { + + @Test + fun `skal svare true ved dummy impl`() { + Assertions.assertEquals(true, featureToggleService.isEnabled("sull-bala-tull")) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/featureToggle/milj\303\270/EnvironmentConfigTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/featureToggle/milj\303\270/EnvironmentConfigTest.kt" new file mode 100644 index 000000000..663a80138 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/config/featureToggle/milj\303\270/EnvironmentConfigTest.kt" @@ -0,0 +1,22 @@ +package no.nav.familie.ba.sak.config.featureToggle.miljø + +import io.mockk.every +import io.mockk.mockk +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.core.env.Environment + +class EnvironmentConfigTest { + + @Test + fun `aktiv profil skal være aktiv`() { + val env = mockk().also { every { it.activeProfiles } returns arrayOf("dev") } + assertThat(env.erAktiv(Profil.Dev)).isTrue + } + + @Test + fun `profil som ikke er lista som aktiv skal ikke være aktiv`() { + val env = mockk().also { every { it.activeProfiles } returns arrayOf("prod") } + assertThat(env.erAktiv(Profil.Dev)).isFalse + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/BegrunnelseTeksterStepDefinition.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/BegrunnelseTeksterStepDefinition.kt new file mode 100644 index 000000000..43bb3c963 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/BegrunnelseTeksterStepDefinition.kt @@ -0,0 +1,408 @@ +package no.nav.familie.ba.sak.cucumber + +import io.cucumber.datatable.DataTable +import io.cucumber.java.no.Gitt +import io.cucumber.java.no.Når +import io.cucumber.java.no.Og +import io.cucumber.java.no.Så +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.cucumber.domeneparser.BrevBegrunnelseParser.mapBegrunnelser +import no.nav.familie.ba.sak.cucumber.domeneparser.VedtaksperiodeMedBegrunnelserParser +import no.nav.familie.ba.sak.cucumber.domeneparser.parseDato +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.brev.LANDKODER +import no.nav.familie.ba.sak.kjerne.brev.brevBegrunnelseProdusent.GrunnlagForBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.brevPeriodeProdusent.lagBrevPeriode +import no.nav.familie.ba.sak.kjerne.brev.domene.RestSanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.SanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.RestSanityEØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BegrunnelseMedData +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.UtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.tilUtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.tilVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtakBegrunnelseProdusent.hentGyldigeBegrunnelserForPeriode +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.BehandlingsGrunnlagForVedtaksperioder +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.genererVedtaksperioder +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.kontrakter.felles.objectMapper +import org.assertj.core.api.Assertions.assertThat +import java.time.LocalDate + +class BegrunnelseTeksterStepDefinition { + + private var fagsaker: Map = emptyMap() + private var behandlinger = mutableMapOf() + private var behandlingTilForrigeBehandling = mutableMapOf() + private var vedtaksliste = mutableListOf() + private var persongrunnlag = mutableMapOf() + private var personResultater = mutableMapOf>() + private var vedtaksperioderMedBegrunnelser = listOf() + private var kompetanser = mutableMapOf>() + private var endredeUtbetalinger = mutableMapOf>() + private var andelerTilkjentYtelse = mutableMapOf>() + private var overstyrteEndringstidspunkt = mutableMapOf() + private var overgangsstønadForVedtaksperiode = mapOf>() + private var dagensDato: LocalDate = LocalDate.now() + + private var gjeldendeBehandlingId: Long? = null + + private var utvidetVedtaksperiodeMedBegrunnelser = listOf() + + private var målform: Målform = Målform.NB + private var søknadstidspunkt: LocalDate? = null + + /** + * Mulige verdier: | FagsakId | Fagsaktype | + */ + @Gitt("følgende fagsaker for begrunnelse") + fun `følgende fagsaker for begrunnelse`(dataTable: DataTable) { + fagsaker = lagFagsaker(dataTable) + } + + /** + * Mulige felter: + * | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + */ + @Gitt("følgende behandling") + fun `følgende behandling`(dataTable: DataTable) { + lagVedtak( + dataTable = dataTable, + behandlinger = behandlinger, + behandlingTilForrigeBehandling = behandlingTilForrigeBehandling, + vedtaksListe = vedtaksliste, + fagsaker = fagsaker, + ) + } + + /** + * Mulige verdier: | BehandlingId | AktørId | Persontype | Fødselsdato | + */ + @Og("følgende persongrunnlag for begrunnelse") + fun `følgende persongrunnlag for begrunnelse`(dataTable: DataTable) { + persongrunnlag.putAll(lagPersonGrunnlag(dataTable)) + } + + @Og("følgende dagens dato {}") + fun `følgende dagens dato`(dagensDatoString: String) { + dagensDato = parseDato(dagensDatoString) + } + + @Og("lag personresultater for begrunnelse for behandling {}") + fun `lag personresultater for begrunnelse`(behandlingId: Long) { + val persongrunnlagForBehandling = persongrunnlag.finnPersonGrunnlagForBehandling(behandlingId) + val behandling = behandlinger.finnBehandling(behandlingId) + personResultater[behandlingId] = lagPersonresultater(persongrunnlagForBehandling, behandling) + } + + /** + * Mulige verdier: | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | Vurderes etter | + */ + @Og("legg til nye vilkårresultater for begrunnelse for behandling {}") + fun `legg til nye vilkårresultater for behandling`(behandlingId: Long, dataTable: DataTable) { + val vilkårResultaterPerPerson = + dataTable.asMaps().groupBy { VedtaksperiodeMedBegrunnelserParser.parseAktørId(it) } + val personResultatForBehandling = personResultater[behandlingId] + ?: error("Finner ikke personresultater for behandling med id $behandlingId") + + personResultater[behandlingId] = + leggTilVilkårResultatPåPersonResultat(personResultatForBehandling, vilkårResultaterPerPerson, behandlingId) + } + + /** + * Mulige felt: + * | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland | + */ + @Og("med kompetanser for begrunnelse") + fun `med kompetanser for begrunnelse`(dataTable: DataTable) { + val nyeKompetanserPerBarn = dataTable.asMaps() + kompetanser = lagKompetanser(nyeKompetanserPerBarn, persongrunnlag) + } + + /** + * Mulige verdier: | AktørId | Fra dato | Til dato | BehandlingId | Årsak | Prosent | + */ + @Og("med endrede utbetalinger for begrunnelse") + fun `med endrede utbetalinger for begrunnelse`(dataTable: DataTable) { + val nyeEndredeUtbetalingAndeler = dataTable.asMaps() + endredeUtbetalinger = lagEndredeUtbetalinger(nyeEndredeUtbetalingAndeler, persongrunnlag) + } + + /** + * Mulige verdier: | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + */ + @Og("med andeler tilkjent ytelse for begrunnelse") + fun `med andeler tilkjent ytelse for begrunnelse`(dataTable: DataTable) { + andelerTilkjentYtelse = lagAndelerTilkjentYtelse(dataTable, behandlinger, persongrunnlag) + } + + /** + * Mulige verdier: | BehandlingId | AktørId | Fra dato | Til dato | + */ + @Og("med overgangsstønad for begrunnelse") + fun `med overgangsstønad for begrunnelse`(dataTable: DataTable) { + overgangsstønadForVedtaksperiode = lagOvergangsstønad( + dataTable = dataTable, + persongrunnlag = persongrunnlag, + tidligereBehandlinger = behandlingTilForrigeBehandling, + dagensDato = dagensDato, + ) + } + + /** + * Mulige verdier: | Fra dato | Til dato | Standardbegrunnelser | Eøsbegrunnelser | Fritekster | + */ + @Og("med vedtaksperioder for behandling {}") + fun `med vedtaksperioder`(behandlingId: Long, dataTable: DataTable) { + val vedtaksperioder = genererVedtaksperioderForBehandling(behandlingId) + + vedtaksperioderMedBegrunnelser = leggBegrunnelserIVedtaksperiodene( + dataTable, + vedtaksperioder, + vedtaksliste.single { it.behandling.id == behandlingId }, + ) + } + + @Når("begrunnelsetekster genereres for behandling {}") + fun `generer begrunnelsetekst for `(behandlingId: Long) { + utvidetVedtaksperiodeMedBegrunnelser = genererVedtaksperioderForBehandling(behandlingId) + } + + private fun genererVedtaksperioderForBehandling(behandlingId: Long): List { + gjeldendeBehandlingId = behandlingId + val behandling = behandlinger.finnBehandling(behandlingId) + + val vedtak = vedtaksliste.find { it.behandling.id == behandlingId && it.aktiv } ?: error("Finner ikke vedtak") + + vedtak.behandling.overstyrtEndringstidspunkt = overstyrteEndringstidspunkt[behandlingId] + + val forrigeBehandlingId = behandlingTilForrigeBehandling[behandlingId] + + val grunnlagForBegrunnelser = hentGrunnlagForBegrunnelser(behandlingId, vedtak, forrigeBehandlingId) + + vedtaksperioderMedBegrunnelser = genererVedtaksperioder( + vedtak = vedtak, + grunnlagForVedtakPerioder = grunnlagForBegrunnelser.behandlingsGrunnlagForVedtaksperioder, + grunnlagForVedtakPerioderForrigeBehandling = grunnlagForBegrunnelser.behandlingsGrunnlagForVedtaksperioderForrigeBehandling, + nåDato = dagensDato, + ) + + val utvidedeVedtaksperioderMedBegrunnelser = vedtaksperioderMedBegrunnelser.map { + it.tilUtvidetVedtaksperiodeMedBegrunnelser( + personopplysningGrunnlag = persongrunnlag.finnPersonGrunnlagForBehandling(behandlingId), + andelerTilkjentYtelse = andelerTilkjentYtelse[behandlingId]?.map { + AndelTilkjentYtelseMedEndreteUtbetalinger( + it, + endredeUtbetalinger[behandlingId] ?: emptySet(), + ) + } ?: emptyList(), + ) + } + + return utvidedeVedtaksperioderMedBegrunnelser.map { + it.copy( + gyldigeBegrunnelser = it.tilVedtaksperiodeMedBegrunnelser(vedtak) + .hentGyldigeBegrunnelserForPeriode(grunnlagForBegrunnelser).toList(), + ) + } + } + + private fun hentGrunnlagForBegrunnelser( + behandlingId: Long, + vedtak: Vedtak, + forrigeBehandlingId: Long?, + ): GrunnlagForBegrunnelse { + val grunnlagForVedtaksperiode = BehandlingsGrunnlagForVedtaksperioder( + persongrunnlag = persongrunnlag.finnPersonGrunnlagForBehandling(behandlingId), + personResultater = personResultater[behandlingId] ?: error("Finner ikke personresultater"), + fagsakType = vedtak.behandling.fagsak.type, + kompetanser = kompetanser[behandlingId] ?: emptyList(), + endredeUtbetalinger = endredeUtbetalinger[behandlingId] ?: emptyList(), + andelerTilkjentYtelse = andelerTilkjentYtelse[behandlingId] ?: emptyList(), + perioderOvergangsstønad = overgangsstønadForVedtaksperiode[behandlingId] ?: emptyList(), + uregistrerteBarn = emptyList(), + ) + + val grunnlagForVedtaksperiodeForrigeBehandling = forrigeBehandlingId?.let { + val forrigeVedtak = + vedtaksliste.find { it.behandling.id == forrigeBehandlingId && it.aktiv } ?: error("Finner ikke vedtak") + BehandlingsGrunnlagForVedtaksperioder( + persongrunnlag = persongrunnlag.finnPersonGrunnlagForBehandling(forrigeBehandlingId), + personResultater = personResultater[forrigeBehandlingId] ?: error("Finner ikke personresultater"), + fagsakType = forrigeVedtak.behandling.fagsak.type, + kompetanser = kompetanser[forrigeBehandlingId] ?: emptyList(), + endredeUtbetalinger = endredeUtbetalinger[forrigeBehandlingId] ?: emptyList(), + andelerTilkjentYtelse = andelerTilkjentYtelse[forrigeBehandlingId] ?: emptyList(), + perioderOvergangsstønad = overgangsstønadForVedtaksperiode[forrigeBehandlingId] ?: emptyList(), + uregistrerteBarn = emptyList(), + ) + } + + val grunnlagForBegrunnelse = GrunnlagForBegrunnelse( + behandlingsGrunnlagForVedtaksperioder = grunnlagForVedtaksperiode, + behandlingsGrunnlagForVedtaksperioderForrigeBehandling = grunnlagForVedtaksperiodeForrigeBehandling, + sanityBegrunnelser = mockHentSanityBegrunnelser(), + sanityEØSBegrunnelser = mockHentSanityEØSBegrunnelser(), + nåDato = dagensDato, + ) + return grunnlagForBegrunnelse + } + + /** + * Mulige verdier: | Fra dato | Til dato | VedtaksperiodeType | Regelverk Inkluderte Begrunnelser | Inkluderte Begrunnelser | Regelverk Ekskluderte Begrunnelser | Ekskluderte Begrunnelser | + */ + @Så("forvent følgende standardBegrunnelser") + fun `forvent følgende standardBegrunnelser`(dataTable: DataTable) { + val forventedeStandardBegrunnelser = mapBegrunnelser(dataTable).toSet() + + forventedeStandardBegrunnelser.forEach { forventet -> + val faktisk = + utvidetVedtaksperiodeMedBegrunnelser.find { it.fom == forventet.fom && it.tom == forventet.tom } + ?: throw Feil( + "Forventet å finne en vedtaksperiode med \n" + + " Fom: ${forventet.fom} og Tom: ${forventet.tom}. \n" + + "Faktiske vedtaksperioder var \n${ + utvidetVedtaksperiodeMedBegrunnelser.joinToString("\n") { + " Fom: ${it.fom}, Tom: ${it.tom}" + } + }", + ) + assertThat(faktisk.type) + .`as`("For periode: ${forventet.fom} til ${forventet.tom}") + .isEqualTo(forventet.type) + assertThat(faktisk.gyldigeBegrunnelser) + .`as`("For periode: ${forventet.fom} til ${forventet.tom}") + .containsAll(forventet.inkluderteStandardBegrunnelser) + + if (faktisk.gyldigeBegrunnelser.isNotEmpty() && forventet.ekskluderteStandardBegrunnelser.isNotEmpty()) { + assertThat(faktisk.gyldigeBegrunnelser).doesNotContainAnyElementsOf(forventet.ekskluderteStandardBegrunnelser) + } + } + } + + /** + * Mulige verdier: | Begrunnelse | Type | Gjelder søker | Barnas fødselsdatoer | Antall barn | Måned og år begrunnelsen gjelder for | Målform | Beløp | Søknadstidspunkt | Avtale tidspunkt delt bosted | Søkers rett til utvidet | + */ + @Så("forvent følgende brevbegrunnelser for behandling {} i periode {} til {}") + fun `forvent følgende brevbegrunnelser for behandling i periode`( + behandlingId: Long, + periodeFom: String, + periodeTom: String, + dataTable: DataTable, + ) { + val forrigeBehandlingId = behandlingTilForrigeBehandling[behandlingId] + val vedtak = vedtaksliste.find { it.behandling.id == behandlingId && it.aktiv } ?: error("Finner ikke vedtak") + val grunnlagForBegrunnelse = hentGrunnlagForBegrunnelser(behandlingId, vedtak, forrigeBehandlingId) + + val faktiskeBegrunnelser: List = + vedtaksperioderMedBegrunnelser.single { + it.fom == parseNullableDato(periodeFom) && it.tom == parseNullableDato(periodeTom) + }.lagBrevPeriode(grunnlagForBegrunnelse, LANDKODER)!! + .begrunnelser + .filterIsInstance() + + val forvendtedeBegrunnelser = parseBegrunnelser(dataTable) + + assertThat(faktiskeBegrunnelser.sortedBy { it.apiNavn }) + .usingRecursiveComparison() + .isEqualTo(forvendtedeBegrunnelser.sortedBy { it.apiNavn }) + } + + /** + * Mulige verdier: | Brevperiodetype | Fra dato | Til dato | Beløp | Antall barn med utbetaling | Barnas fødselsdager | Du eller institusjonen | + */ + @Så("forvent følgende brevperioder for behandling {}") + fun `forvent følgende brevperioder for behandling i periode`( + behandlingId: Long, + dataTable: DataTable, + ) { + val forrigeBehandlingId = behandlingTilForrigeBehandling[behandlingId] + val vedtak = vedtaksliste.find { it.behandling.id == behandlingId && it.aktiv } ?: error("Finner ikke vedtak") + val grunnlagForBegrunnelse = hentGrunnlagForBegrunnelser(behandlingId, vedtak, forrigeBehandlingId) + + val faktiskeBrevperioder: List = + vedtaksperioderMedBegrunnelser.sortedBy { it.fom }.mapNotNull { + it.lagBrevPeriode(grunnlagForBegrunnelse, LANDKODER) + } + + val forvendtedeBrevperioder = parseBrevPerioder(dataTable) + + assertThat(faktiskeBrevperioder) + .usingRecursiveComparison() + .ignoringFields("begrunnelser") + .isEqualTo(forvendtedeBrevperioder) + } + + // For å laste ned begrunnelsene på nytt anbefales https://familie-brev.sanity.studio/ba-test/vision med query fra SanityQueries.kt . + // Kopier URL fra resultatet og kjør + // curl -XGET | jq '.result' > /familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/restSanityTestBegrunnelser + private fun mockHentSanityBegrunnelser(): Map { + val restSanityBegrunnelserJson = + this::class.java.getResource("/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/restSanityBegrunnelser")!! + + val restSanityBegrunnelser = + objectMapper.readValue(restSanityBegrunnelserJson.readText(), Array::class.java) + .toList() + + val enumPåApiNavn = Standardbegrunnelse.values().associateBy { it.sanityApiNavn } + val sanityBegrunnelser = restSanityBegrunnelser.mapNotNull { it.tilSanityBegrunnelse() } + + return sanityBegrunnelser + .mapNotNull { + val begrunnelseEnum = enumPåApiNavn[it.apiNavn] + if (begrunnelseEnum == null) { + null + } else { + begrunnelseEnum to it + } + }.toMap() + } + + private fun mockHentSanityEØSBegrunnelser(): Map { + val restSanityEØSBegrunnelserJson = + this::class.java.getResource("/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/restSanityEØSBegrunnelser")!! + + val restSanityEØSBegrunnelser = + objectMapper.readValue( + restSanityEØSBegrunnelserJson.readText(), + Array::class.java, + ) + .toList() + + val enumPåApiNavn = EØSStandardbegrunnelse.entries.associateBy { it.sanityApiNavn } + val sanityEØSBegrunnelser = restSanityEØSBegrunnelser.mapNotNull { it.tilSanityEØSBegrunnelse() } + + return sanityEØSBegrunnelser + .mapNotNull { + val begrunnelseEnum = enumPåApiNavn[it.apiNavn] + if (begrunnelseEnum == null) { + null + } else { + begrunnelseEnum to it + } + }.toMap() + } +} + +data class SammenlignbarBegrunnelse( + val fom: LocalDate?, + val tom: LocalDate?, + val type: Vedtaksperiodetype, + val inkluderteStandardBegrunnelser: Set, + val ekskluderteStandardBegrunnelser: Set = emptySet(), +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/BrevbegrunnelseUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/BrevbegrunnelseUtil.kt new file mode 100644 index 000000000..2934da904 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/BrevbegrunnelseUtil.kt @@ -0,0 +1,174 @@ +package no.nav.familie.ba.sak.cucumber + +import io.cucumber.datatable.DataTable +import no.nav.familie.ba.sak.cucumber.domeneparser.BrevPeriodeParser +import no.nav.familie.ba.sak.cucumber.domeneparser.VedtaksperiodeMedBegrunnelserParser +import no.nav.familie.ba.sak.cucumber.domeneparser.norskDatoFormatter +import no.nav.familie.ba.sak.cucumber.domeneparser.parseBoolean +import no.nav.familie.ba.sak.cucumber.domeneparser.parseEnum +import no.nav.familie.ba.sak.cucumber.domeneparser.parseInt +import no.nav.familie.ba.sak.cucumber.domeneparser.parseString +import no.nav.familie.ba.sak.cucumber.domeneparser.parseValgfriBoolean +import no.nav.familie.ba.sak.cucumber.domeneparser.parseValgfriEnum +import no.nav.familie.ba.sak.cucumber.domeneparser.parseValgfriString +import no.nav.familie.ba.sak.kjerne.brev.brevBegrunnelseProdusent.SøkersRettTilUtvidet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.VedtakBegrunnelseType +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BegrunnelseData +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BegrunnelseMedData +import no.nav.familie.ba.sak.kjerne.vedtak.domene.EØSBegrunnelseData +import no.nav.familie.ba.sak.kjerne.vedtak.domene.EØSBegrunnelseDataMedKompetanse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.EØSBegrunnelseDataUtenKompetanse +import java.time.LocalDate + +typealias Tabellrad = Map + +enum class Begrunnelsetype { + EØS, + STANDARD, +} + +fun parseBegrunnelser(dataTable: DataTable): List { + return dataTable.asMaps().map { rad: Tabellrad -> + + val type = parseValgfriEnum( + BrevPeriodeParser.DomenebegrepBrevBegrunnelse.TYPE, + rad, + ) ?: Begrunnelsetype.STANDARD + + when (type) { + Begrunnelsetype.STANDARD -> parseStandardBegrunnelse(rad) + Begrunnelsetype.EØS -> parseEøsBegrunnelse(rad) + } + } +} + +fun parseStandardBegrunnelse(rad: Tabellrad) = + BegrunnelseData( + vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET, + apiNavn = parseEnum( + BrevPeriodeParser.DomenebegrepBrevBegrunnelse.BEGRUNNELSE, + rad, + ).sanityApiNavn, + + gjelderSoker = parseBoolean(BrevPeriodeParser.DomenebegrepBrevBegrunnelse.GJELDER_SØKER, rad), + barnasFodselsdatoer = parseValgfriString( + BrevPeriodeParser.DomenebegrepBrevBegrunnelse.BARNAS_FØDSELSDATOER, + rad, + ) ?: "", + + fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling = "", + fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling = "", + + antallBarn = parseInt(BrevPeriodeParser.DomenebegrepBrevBegrunnelse.ANTALL_BARN, rad), + + antallBarnOppfyllerTriggereOgHarUtbetaling = 0, + antallBarnOppfyllerTriggereOgHarNullutbetaling = 0, + + maanedOgAarBegrunnelsenGjelderFor = parseString( + BrevPeriodeParser.DomenebegrepBrevBegrunnelse.MÅNED_OG_ÅR_BEGRUNNELSEN_GJELDER_FOR, + rad, + ), + maalform = parseEnum(BrevPeriodeParser.DomenebegrepBrevBegrunnelse.MÅLFORM, rad).tilSanityFormat(), + belop = parseString(BrevPeriodeParser.DomenebegrepBrevBegrunnelse.BELØP, rad).replace(' ', ' '), + soknadstidspunkt = parseValgfriString( + BrevPeriodeParser.DomenebegrepBrevBegrunnelse.SØKNADSTIDSPUNKT, + rad, + ) ?: "", + avtaletidspunktDeltBosted = parseValgfriString( + BrevPeriodeParser.DomenebegrepBrevBegrunnelse.AVTALETIDSPUNKT_DELT_BOSTED, + rad, + ) ?: "", + sokersRettTilUtvidet = parseValgfriEnum( + BrevPeriodeParser.DomenebegrepBrevBegrunnelse.SØKERS_RETT_TIL_UTVIDET, + rad, + )?.tilSanityFormat() ?: SøkersRettTilUtvidet.SØKER_HAR_IKKE_RETT.tilSanityFormat(), + ) + +fun parseEøsBegrunnelse(rad: Tabellrad): EØSBegrunnelseData { + val gjelderSoker = parseValgfriBoolean(BrevPeriodeParser.DomenebegrepBrevBegrunnelse.GJELDER_SØKER, rad) + + val annenForeldersAktivitet = parseValgfriEnum( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepKompetanse.ANNEN_FORELDERS_AKTIVITET, + rad, + ) + val annenForeldersAktivitetsland = parseValgfriString( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepKompetanse.ANNEN_FORELDERS_AKTIVITETSLAND, + rad, + ) + val barnetsBostedsland = parseValgfriString( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepKompetanse.BARNETS_BOSTEDSLAND, + rad, + ) + val søkersAktivitet = parseValgfriEnum( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepKompetanse.SØKERS_AKTIVITET, + rad, + ) + val søkersAktivitetsland = parseValgfriString( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepKompetanse.SØKERS_AKTIVITETSLAND, + rad, + ) + + val vedtakBegrunnelseType = VedtakBegrunnelseType.INNVILGET + + val apiNavn = parseEnum( + BrevPeriodeParser.DomenebegrepBrevBegrunnelse.BEGRUNNELSE, + rad, + ).sanityApiNavn + + val barnasFodselsdatoer = parseString( + BrevPeriodeParser.DomenebegrepBrevBegrunnelse.BARNAS_FØDSELSDATOER, + rad, + ) + + val antallBarn = parseInt(BrevPeriodeParser.DomenebegrepBrevBegrunnelse.ANTALL_BARN, rad) + + val målform = parseEnum(BrevPeriodeParser.DomenebegrepBrevBegrunnelse.MÅLFORM, rad).tilSanityFormat() + + return if (gjelderSoker == null) { + if (annenForeldersAktivitet == null || + annenForeldersAktivitetsland == null || + barnetsBostedsland == null || + søkersAktivitet == null || + søkersAktivitetsland == null + ) { + error("For EØS-begrunnelser må enten 'Gjelder søker' eller kompetansefeltene settes") + } + + EØSBegrunnelseDataMedKompetanse( + vedtakBegrunnelseType = VedtakBegrunnelseType.EØS_INNVILGET, + apiNavn = apiNavn, + barnasFodselsdatoer = barnasFodselsdatoer, + antallBarn = antallBarn, + maalform = målform, + + annenForeldersAktivitet = annenForeldersAktivitet, + annenForeldersAktivitetsland = annenForeldersAktivitetsland, + barnetsBostedsland = barnetsBostedsland, + sokersAktivitet = søkersAktivitet, + sokersAktivitetsland = søkersAktivitetsland, + ) + } else { + EØSBegrunnelseDataUtenKompetanse( + vedtakBegrunnelseType = vedtakBegrunnelseType, + apiNavn = apiNavn, + barnasFodselsdatoer = barnasFodselsdatoer, + + antallBarn = antallBarn, + maalform = målform, + gjelderSoker = gjelderSoker, + ) + } +} + +fun parseNullableDato(fom: String) = if (fom.uppercase() in listOf("NULL", "-", "")) { + null +} else { + LocalDate.parse( + fom, + norskDatoFormatter, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/BrevperiodeUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/BrevperiodeUtil.kt new file mode 100644 index 000000000..7ae52b432 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/BrevperiodeUtil.kt @@ -0,0 +1,37 @@ +package no.nav.familie.ba.sak.cucumber + +import io.cucumber.datatable.DataTable +import no.nav.familie.ba.sak.cucumber.domeneparser.BrevPeriodeParser +import no.nav.familie.ba.sak.cucumber.domeneparser.Domenebegrep +import no.nav.familie.ba.sak.cucumber.domeneparser.parseEnum +import no.nav.familie.ba.sak.cucumber.domeneparser.parseInt +import no.nav.familie.ba.sak.cucumber.domeneparser.parseString +import no.nav.familie.ba.sak.cucumber.domeneparser.parseValgfriString +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.brevperioder.BrevPeriode +import no.nav.familie.ba.sak.kjerne.vedtak.domene.BrevBegrunnelse + +fun parseBrevPerioder(dataTable: DataTable): List { + return dataTable.asMaps().map { rad: Tabellrad -> + + val beløp = parseString(BrevPeriodeParser.DomenebegrepBrevPeriode.BELØP, rad) + val antallBarn = parseInt(BrevPeriodeParser.DomenebegrepBrevPeriode.ANTALL_BARN, rad) + val barnasFodselsdager = parseValgfriString( + BrevPeriodeParser.DomenebegrepBrevPeriode.BARNAS_FØDSELSDAGER, + rad, + ) ?: "" + val duEllerInstitusjonen = + parseString(BrevPeriodeParser.DomenebegrepBrevPeriode.DU_ELLER_INSTITUSJONEN, rad) + + BrevPeriode( + fom = parseValgfriString(Domenebegrep.FRA_DATO, rad) ?: "", + tom = parseValgfriString(Domenebegrep.TIL_DATO, rad) ?: "", + beløp = beløp, + // egen test for dette. Se `forvent følgende brevbegrunnelser for behandling i periode`() + begrunnelser = emptyList(), + brevPeriodeType = parseEnum(BrevPeriodeParser.DomenebegrepBrevPeriode.TYPE, rad), + antallBarn = antallBarn.toString(), + barnasFodselsdager = barnasFodselsdager, + duEllerInstitusjonen = duEllerInstitusjonen, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/OppdragSteg.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/OppdragSteg.kt new file mode 100644 index 000000000..78281e84b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/OppdragSteg.kt @@ -0,0 +1,306 @@ +package no.nav.familie.ba.sak.cucumber + +import io.cucumber.datatable.DataTable +import io.cucumber.java.no.Gitt +import io.cucumber.java.no.Når +import io.cucumber.java.no.Så +import io.mockk.mockk +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.cucumber.ValideringUtil.assertSjekkBehandlingIder +import no.nav.familie.ba.sak.cucumber.domeneparser.Domenebegrep +import no.nav.familie.ba.sak.cucumber.domeneparser.DomeneparserUtil.groupByBehandlingId +import no.nav.familie.ba.sak.cucumber.domeneparser.ForventetUtbetalingsoppdrag +import no.nav.familie.ba.sak.cucumber.domeneparser.ForventetUtbetalingsperiode +import no.nav.familie.ba.sak.cucumber.domeneparser.OppdragParser +import no.nav.familie.ba.sak.cucumber.domeneparser.OppdragParser.mapTilkjentYtelse +import no.nav.familie.ba.sak.cucumber.domeneparser.parseÅrMåned +import no.nav.familie.ba.sak.integrasjoner.økonomi.AndelTilkjentYtelseForIverksettingFactory +import no.nav.familie.ba.sak.integrasjoner.økonomi.AndelTilkjentYtelseForSimuleringFactory +import no.nav.familie.ba.sak.integrasjoner.økonomi.AndelTilkjentYtelseForUtbetalingsoppdrag +import no.nav.familie.ba.sak.integrasjoner.økonomi.IdentOgYtelse +import no.nav.familie.ba.sak.integrasjoner.økonomi.UtbetalingsoppdragGenerator +import no.nav.familie.ba.sak.integrasjoner.økonomi.pakkInnForUtbetaling +import no.nav.familie.ba.sak.integrasjoner.økonomi.tilRestUtbetalingsoppdrag +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.grupperAndeler +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiUtils.oppdaterBeståendeAndelerMedOffset +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.BeregningTestUtil.sisteAndelPerIdent +import no.nav.familie.ba.sak.kjerne.beregning.BeregningTestUtil.sisteAndelPerIdentNy +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.felles.utbetalingsgenerator.domain.BeregnetUtbetalingsoppdragLongId +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsperiode +import org.assertj.core.api.Assertions.assertThat +import org.slf4j.LoggerFactory +import java.time.YearMonth + +class OppdragSteg { + + private val utbetalingsoppdragGenerator = UtbetalingsoppdragGenerator(mockk(relaxed = true)) + private var behandlinger = mapOf() + private var tilkjenteYtelser = listOf() + private var tilkjenteYtelserNy = listOf() + private var beregnetUtbetalingsoppdrag = mutableMapOf() + private var beregnetUtbetalingsoppdragNy = mutableMapOf() + private var beregnetUtbetalingsoppdragSimulering = mutableMapOf() + private var beregnetUtbetalingsoppdragSimuleringNy = mutableMapOf() + private var endretMigreringsdatoMap = mutableMapOf() + private var kastedeFeil = mutableMapOf() + + private val logger = LoggerFactory.getLogger(javaClass) + + @Gitt("følgende tilkjente ytelser") + fun følgendeTilkjenteYtelser(dataTable: DataTable) { + genererBehandlinger(dataTable) + tilkjenteYtelser = mapTilkjentYtelse(dataTable, behandlinger) + tilkjenteYtelserNy = mapTilkjentYtelse(dataTable, behandlinger, tilkjenteYtelser.size.toLong()) + if (tilkjenteYtelser.flatMap { it.andelerTilkjentYtelse }.any { it.kildeBehandlingId != null }) { + error("Kildebehandling skal ikke settes på input, denne settes fra utbetalingsgeneratorn") + } + } + + @Gitt("følgende behandlingsinformasjon") + fun `følgendeBehandlingsinformasjon`(dataTable: DataTable) { + endretMigreringsdatoMap = dataTable.groupByBehandlingId() + .mapValues { + it.value.map { entry: Map -> parseÅrMåned(entry[Domenebegrep.ENDRET_MIGRERINGSDATO.nøkkel]!!) } + .single() + }.toMutableMap() + } + + @Når("beregner utbetalingsoppdrag") + fun `beregner utbetalingsoppdrag`() { + tilkjenteYtelser.fold(emptyList()) { acc, tilkjentYtelse -> + val behandlingId = tilkjentYtelse.behandling.id + try { + beregnetUtbetalingsoppdragSimulering[behandlingId] = + beregnUtbetalingsoppdrag(acc, tilkjentYtelse, erSimulering = true) + beregnetUtbetalingsoppdrag[behandlingId] = beregnUtbetalingsoppdrag(acc, tilkjentYtelse) + } catch (e: Exception) { + logger.error("Feilet beregning av oppdrag for behandling=$behandlingId") + kastedeFeil[behandlingId] = e + } + acc + tilkjentYtelse + } + tilkjenteYtelserNy.fold(emptyList()) { acc, tilkjentYtelse -> + val behandlingId = tilkjentYtelse.behandling.id + try { + genererUtbetalingsoppdragForSimuleringNy(behandlingId, acc, tilkjentYtelse) + beregnetUtbetalingsoppdragNy[behandlingId] = beregnUtbetalingsoppdragNy(acc, tilkjentYtelse) + oppdaterTilkjentYtelseMedUtbetalingsoppdrag( + beregnetUtbetalingsoppdragNy[behandlingId]!!, + tilkjentYtelse, + ) + } catch (e: Exception) { + logger.error("Feilet beregning av oppdrag for behandling=$behandlingId") + kastedeFeil[behandlingId] = e + } + acc + tilkjentYtelse + } + } + + private fun genererUtbetalingsoppdragForSimuleringNy( + behandlingId: Long, + tilkjenteYtelser: List, + tilkjentYtelse: TilkjentYtelse, + ) { + try { + beregnetUtbetalingsoppdragSimuleringNy[behandlingId] = + beregnUtbetalingsoppdragNy(tilkjenteYtelser, tilkjentYtelse, erSimulering = true) + } catch (e: Exception) { + logger.error("Feilet beregning av oppdrag ved simulering for behandling=$behandlingId") + kastedeFeil[behandlingId] = e + } + } + + private fun oppdaterTilkjentYtelseMedUtbetalingsoppdrag( + beregnetUtbetalingsoppdragLongId: BeregnetUtbetalingsoppdragLongId, + tilkjentYtelse: TilkjentYtelse, + ) { + tilkjentYtelse.andelerTilkjentYtelse.forEach { andel -> + val andelMedOppdatertOffset = beregnetUtbetalingsoppdragLongId.andeler.find { it.id == andel.id } + if (andelMedOppdatertOffset != null) { + andel.periodeOffset = andelMedOppdatertOffset.periodeId + andel.forrigePeriodeOffset = andelMedOppdatertOffset.forrigePeriodeId + andel.kildeBehandlingId = andelMedOppdatertOffset.kildeBehandlingId + } + } + } + + private fun beregnUtbetalingsoppdragNy( + acc: List, + tilkjentYtelse: TilkjentYtelse, + erSimulering: Boolean = false, + ): BeregnetUtbetalingsoppdragLongId { + val forrigeTilkjentYtelse = acc.lastOrNull() + + val vedtak = lagVedtak(behandling = tilkjentYtelse.behandling) + val sisteAndelPerIdent = sisteAndelPerIdentNy(acc) + return utbetalingsoppdragGenerator.lagUtbetalingsoppdrag( + saksbehandlerId = "saksbehandlerId", + vedtak = vedtak, + forrigeTilkjentYtelse = forrigeTilkjentYtelse, + sisteAndelPerKjede = sisteAndelPerIdent, + nyTilkjentYtelse = tilkjentYtelse, + erSimulering = erSimulering, + endretMigreringsDato = endretMigreringsdatoMap[tilkjentYtelse.behandling.id], + ) + } + + private fun beregnUtbetalingsoppdrag( + acc: List, + tilkjentYtelse: TilkjentYtelse, + erSimulering: Boolean = false, + ): Utbetalingsoppdrag { + val forrigeTilkjentYtelse = acc.lastOrNull() + + val vedtak = lagVedtak(behandling = tilkjentYtelse.behandling) + val forrigeKjeder = tilKjeder(forrigeTilkjentYtelse, erSimulering) + val oppdaterteKjeder = tilKjeder(tilkjentYtelse, erSimulering) + val sisteAndelPerIdent = sisteAndelPerIdent(acc) + oppdaterBeståendeAndelerMedOffset(oppdaterteKjeder, forrigeKjeder) + return utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + saksbehandlerId = "saksbehandlerId", + vedtak = vedtak, + erFørsteBehandlingPåFagsak = forrigeTilkjentYtelse == null, + forrigeKjeder = forrigeKjeder, + sisteAndelPerIdent = sisteAndelPerIdent, + oppdaterteKjeder = oppdaterteKjeder, + erSimulering = erSimulering, + endretMigreringsDato = endretMigreringsdatoMap[tilkjentYtelse.behandling.id], + ) + } + + @Så("forvent at en exception kastes for behandling {long}") + fun `forvent at en exception kastes for behandling`(behandlingId: Long) { + assertThat(kastedeFeil).isNotEmpty + assertThat(kastedeFeil[behandlingId]).isNotNull + } + + @Så("forvent følgende utbetalingsoppdrag") + fun `forvent følgende utbetalingsoppdrag`(dataTable: DataTable) { + validerForventetUtbetalingsoppdrag(dataTable, beregnetUtbetalingsoppdrag) + assertSjekkBehandlingIder(dataTable, beregnetUtbetalingsoppdrag) + } + + @Så("forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator") + fun `forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator`(dataTable: DataTable) { + validerForventetUtbetalingsoppdrag( + dataTable, + beregnetUtbetalingsoppdragNy.mapValues { it.value.utbetalingsoppdrag.tilRestUtbetalingsoppdrag() } + .toMutableMap(), + ) + assertSjekkBehandlingIder( + dataTable, + beregnetUtbetalingsoppdragNy.mapValues { it.value.utbetalingsoppdrag.tilRestUtbetalingsoppdrag() } + .toMutableMap(), + ) + } + + @Så("forvent følgende simulering") + fun `forvent følgende simulering`(dataTable: DataTable) { + validerForventetUtbetalingsoppdrag(dataTable, beregnetUtbetalingsoppdragSimulering) + assertSjekkBehandlingIder(dataTable, beregnetUtbetalingsoppdragSimulering) + } + + @Så("forvent følgende simulering med ny utbetalingsgenerator") + fun `forvent følgende simulering med ny utbetalingsgenerator`(dataTable: DataTable) { + validerForventetUtbetalingsoppdrag( + dataTable, + beregnetUtbetalingsoppdragSimuleringNy.mapValues { it.value.utbetalingsoppdrag.tilRestUtbetalingsoppdrag() } + .toMutableMap(), + ) + assertSjekkBehandlingIder( + dataTable, + beregnetUtbetalingsoppdragSimuleringNy.mapValues { it.value.utbetalingsoppdrag.tilRestUtbetalingsoppdrag() } + .toMutableMap(), + ) + } + + private fun validerForventetUtbetalingsoppdrag( + dataTable: DataTable, + beregnetUtbetalingsoppdrag: MutableMap, + ) { + val medUtbetalingsperiode = true // TODO? Burde denne kunne sendes med som et flagg? Hva gjør den? + val forventedeUtbetalingsoppdrag = OppdragParser.mapForventetUtbetalingsoppdrag( + dataTable, + ) + forventedeUtbetalingsoppdrag.forEach { forventetUtbetalingsoppdrag -> + val behandlingId = forventetUtbetalingsoppdrag.behandlingId + val utbetalingsoppdrag = beregnetUtbetalingsoppdrag[behandlingId] + ?: error("Mangler utbetalingsoppdrag for $behandlingId") + try { + assertUtbetalingsoppdrag(forventetUtbetalingsoppdrag, utbetalingsoppdrag, medUtbetalingsperiode) + } catch (e: Throwable) { + logger.error("Feilet validering av behandling $behandlingId") + throw e + } + } + } + + private fun tilKjeder( + tilkjentYtelse: TilkjentYtelse?, + erSimulering: Boolean = false, + ): Map> { + val andelFactory = if (erSimulering) { + AndelTilkjentYtelseForSimuleringFactory() + } else { + AndelTilkjentYtelseForIverksettingFactory() + } + + return (tilkjentYtelse?.andelerTilkjentYtelse ?: emptyList()) + .filter { it.erAndelSomSkalSendesTilOppdrag() } + .pakkInnForUtbetaling(andelFactory) + .let { grupperAndeler(it) } + } + + private fun genererBehandlinger(dataTable: DataTable) { + val fagsak = defaultFagsak() + behandlinger = dataTable.groupByBehandlingId() + .map { lagBehandling(fagsak = fagsak).copy(id = it.key) } + .associateBy { it.id } + } + + // @Gitt("følgende tilkjente ytelser uten andel for {}") + + private fun assertUtbetalingsoppdrag( + forventetUtbetalingsoppdrag: ForventetUtbetalingsoppdrag, + utbetalingsoppdrag: Utbetalingsoppdrag, + medUtbetalingsperiode: Boolean = true, + ) { + assertThat(utbetalingsoppdrag.kodeEndring).isEqualTo(forventetUtbetalingsoppdrag.kodeEndring) + assertThat(utbetalingsoppdrag.utbetalingsperiode).hasSize(forventetUtbetalingsoppdrag.utbetalingsperiode.size) + if (medUtbetalingsperiode) { + forventetUtbetalingsoppdrag.utbetalingsperiode.forEachIndexed { index, forventetUtbetalingsperiode -> + val utbetalingsperiode = utbetalingsoppdrag.utbetalingsperiode[index] + try { + assertUtbetalingsperiode(utbetalingsperiode, forventetUtbetalingsperiode) + } catch (e: Throwable) { + logger.error("Feilet validering av rad $index for oppdrag=${forventetUtbetalingsoppdrag.behandlingId}") + throw e + } + } + } + } +} + +private fun assertUtbetalingsperiode( + utbetalingsperiode: Utbetalingsperiode, + forventetUtbetalingsperiode: ForventetUtbetalingsperiode, +) { + assertThat(utbetalingsperiode.erEndringPåEksisterendePeriode) + .isEqualTo(forventetUtbetalingsperiode.erEndringPåEksisterendePeriode) + assertThat(utbetalingsperiode.klassifisering).isEqualTo(forventetUtbetalingsperiode.ytelse.klassifisering) + assertThat(utbetalingsperiode.periodeId).isEqualTo(forventetUtbetalingsperiode.periodeId) + assertThat(utbetalingsperiode.forrigePeriodeId).isEqualTo(forventetUtbetalingsperiode.forrigePeriodeId) + assertThat(utbetalingsperiode.sats.toInt()).isEqualTo(forventetUtbetalingsperiode.sats) + assertThat(utbetalingsperiode.satsType).isEqualTo(Utbetalingsperiode.SatsType.MND) + assertThat(utbetalingsperiode.vedtakdatoFom).isEqualTo(forventetUtbetalingsperiode.fom) + assertThat(utbetalingsperiode.vedtakdatoTom).isEqualTo(forventetUtbetalingsperiode.tom) + assertThat(utbetalingsperiode.opphør?.opphørDatoFom).isEqualTo(forventetUtbetalingsperiode.opphør) + forventetUtbetalingsperiode.kildebehandlingId?.let { + assertThat(utbetalingsperiode.behandlingId).isEqualTo(forventetUtbetalingsperiode.kildebehandlingId) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/ValideringUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/ValideringUtil.kt new file mode 100644 index 000000000..08b83df53 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/ValideringUtil.kt @@ -0,0 +1,34 @@ +package no.nav.familie.ba.sak.cucumber + +import io.cucumber.datatable.DataTable +import no.nav.familie.ba.sak.cucumber.domeneparser.Domenebegrep +import no.nav.familie.ba.sak.cucumber.domeneparser.parseLong +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag + +object ValideringUtil { + + fun assertSjekkBehandlingIder(dataTable: DataTable, utbetalingsoppdrag: Map) { + val eksisterendeBehandlingId = utbetalingsoppdrag.filter { + it.value.utbetalingsperiode.isNotEmpty() + }.keys + val forventedeBehandlingId = dataTable.asMaps().map { parseLong(Domenebegrep.BEHANDLING_ID, it) }.toSet() + val ukontrollerteBehandlingId = eksisterendeBehandlingId.filterNot { forventedeBehandlingId.contains(it) } + if (ukontrollerteBehandlingId.isNotEmpty() && erUkontrollerteUtbetalingsoppdragTomme( + ukontrollerteBehandlingId, + utbetalingsoppdrag, + ) + ) { + error("Har ikke kontrollert behandlingene:$ukontrollerteBehandlingId") + } + } + + private fun erUkontrollerteUtbetalingsoppdragTomme( + ukontrollerteBehandlingId: List, + utbetalingsoppdrag: Map, + ): Boolean = + utbetalingsoppdrag + .filterKeys { + ukontrollerteBehandlingId.contains(it) + } + .any { it.value.utbetalingsperiode.isNotEmpty() } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/VedtaksperiodeMedBegrunnelserStepDefinition.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/VedtaksperiodeMedBegrunnelserStepDefinition.kt new file mode 100644 index 000000000..51156c7b0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/VedtaksperiodeMedBegrunnelserStepDefinition.kt @@ -0,0 +1,186 @@ +package no.nav.familie.ba.sak.cucumber + +import io.cucumber.datatable.DataTable +import io.cucumber.java.no.Gitt +import io.cucumber.java.no.Når +import io.cucumber.java.no.Og +import io.cucumber.java.no.Så +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.cucumber.domeneparser.Domenebegrep +import no.nav.familie.ba.sak.cucumber.domeneparser.VedtaksperiodeMedBegrunnelserParser.DomenebegrepVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.cucumber.domeneparser.VedtaksperiodeMedBegrunnelserParser.mapForventetVedtaksperioderMedBegrunnelser +import no.nav.familie.ba.sak.cucumber.domeneparser.VedtaksperiodeMedBegrunnelserParser.parseAktørId +import no.nav.familie.ba.sak.cucumber.domeneparser.parseDato +import no.nav.familie.ba.sak.cucumber.domeneparser.parseLong +import no.nav.familie.ba.sak.ekstern.restDomene.BarnMedOpplysninger +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import org.assertj.core.api.Assertions +import java.time.LocalDate + +class VedtaksperiodeMedBegrunnelserStepDefinition { + + private var fagsaker: Map = emptyMap() + private var behandlinger = mutableMapOf() + private var behandlingTilForrigeBehandling = mutableMapOf() + private var vedtaksliste = mutableListOf() + private var persongrunnlag = mapOf() + private var personResultater = mutableMapOf>() + private var vedtaksperioderMedBegrunnelser = listOf() + private var kompetanser = mutableMapOf>() + private var endredeUtbetalinger = mutableMapOf>() + private var andelerTilkjentYtelse = mutableMapOf>() + private var overstyrteEndringstidspunkt = mapOf() + private var overgangsstønad = mapOf>() + private var uregistrerteBarn = listOf() + private var dagensDato: LocalDate = LocalDate.now() + + private var gjeldendeBehandlingId: Long? = null + + /** + * Mulige verdier: | FagsakId | Fagsaktype | + */ + @Gitt("følgende fagsaker") + fun `følgende fagsaker`(dataTable: DataTable) { + fagsaker = lagFagsaker(dataTable) + } + + /** + * Mulige verdier: + * | BehandlingId | ForrigeBehandlingId | FagsakId | Behandlingsresultat | Behandlingsårsak | + */ + @Gitt("følgende vedtak") + fun `følgende vedtak`(dataTable: DataTable) { + lagVedtak(dataTable, behandlinger, behandlingTilForrigeBehandling, vedtaksliste, fagsaker) + } + + @Og("dagens dato er {}") + fun `dagens dato er`(dagensDatoString: String) { + dagensDato = parseDato(dagensDatoString) + } + + /** + * Mulige verdier: | BehandlingId | AktørId | Persontype | Fødselsdato | + */ + @Og("følgende persongrunnlag") + fun `følgende persongrunnlag`(dataTable: DataTable) { + persongrunnlag = lagPersonGrunnlag(dataTable) + } + + @Og("lag personresultater for behandling {}") + fun `lag personresultater`(behandlingId: Long) { + val persongrunnlagForBehandling = persongrunnlag.finnPersonGrunnlagForBehandling(behandlingId) + val behandling = behandlinger.finnBehandling(behandlingId) + personResultater[behandlingId] = lagPersonresultater(persongrunnlagForBehandling, behandling) + } + + /** + * Mulige verdier: | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + */ + @Og("legg til nye vilkårresultater for behandling {}") + fun `legg til nye vilkårresultater for behandling`(behandlingId: Long, dataTable: DataTable) { + val vilkårResultaterPerPerson = dataTable.asMaps().groupBy { parseAktørId(it) } + val personResultatForBehandling = personResultater[behandlingId] + ?: error("Finner ikke personresultater for behandling med id $behandlingId") + + personResultater[behandlingId] = + leggTilVilkårResultatPåPersonResultat(personResultatForBehandling, vilkårResultaterPerPerson, behandlingId) + } + + /** + * Mulige verdier: | BehandlingId | Endringstidspunkt | + */ + @Og("med overstyrt endringstidspunkt") + fun settEndringstidspunkt(dataTable: DataTable) { + overstyrteEndringstidspunkt = dataTable.asMaps().associate { rad -> + parseLong(Domenebegrep.BEHANDLING_ID, rad) to + parseDato(DomenebegrepVedtaksperiodeMedBegrunnelser.ENDRINGSTIDSPUNKT, rad) + } + } + + /** + * Mulige verdier: | AktørId | Fra dato | Til dato | Resultat | BehandlingId | + */ + @Og("med kompetanser") + fun `med kompetanser`(dataTable: DataTable) { + val nyeKompetanserPerBarn = dataTable.asMaps() + kompetanser = lagKompetanser(nyeKompetanserPerBarn, persongrunnlag) + } + + /** + * Mulige verdier: | AktørId | Fra dato | Til dato | BehandlingId | Årsak | Prosent | + */ + @Og("med endrede utbetalinger") + fun `med endrede utbetalinger`(dataTable: DataTable) { + val nyeEndredeUtbetalingAndeler = dataTable.asMaps() + endredeUtbetalinger = lagEndredeUtbetalinger(nyeEndredeUtbetalingAndeler, persongrunnlag) + } + + /** + * Mulige verdier: | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + */ + @Og("med andeler tilkjent ytelse") + fun `med andeler tilkjent ytelse`(dataTable: DataTable) { + andelerTilkjentYtelse = lagAndelerTilkjentYtelse(dataTable, behandlinger, persongrunnlag) + } + + /** + * Mulige verdier: | BehandlingId | AktørId | Fra dato | Til dato | + */ + @Og("med overgangsstønad") + fun `med overgangsstønad`(dataTable: DataTable) { + overgangsstønad = lagOvergangsstønad( + dataTable = dataTable, + persongrunnlag = persongrunnlag, + tidligereBehandlinger = behandlingTilForrigeBehandling, + dagensDato = LocalDate.now(), + ) + } + + @Og("med uregistrerte barn") + fun `med uregistrerte barn`() { + uregistrerteBarn = listOf(BarnMedOpplysninger(ident = "")) + } + + @Når("vedtaksperioder med begrunnelser genereres for behandling {}") + fun `generer vedtaksperiode med begrunnelse`(behandlingId: Long) { + gjeldendeBehandlingId = behandlingId + + vedtaksperioderMedBegrunnelser = lagVedtaksPerioder( + behandlingId = behandlingId, + vedtaksListe = vedtaksliste, + behandlingTilForrigeBehandling = behandlingTilForrigeBehandling, + personGrunnlag = persongrunnlag, + personResultater = personResultater, + kompetanser = kompetanser, + endredeUtbetalinger = endredeUtbetalinger, + andelerTilkjentYtelse = andelerTilkjentYtelse, + overstyrteEndringstidspunkt = overstyrteEndringstidspunkt, + overgangsstønad = overgangsstønad, + uregistrerteBarn = uregistrerteBarn, + nåDato = dagensDato, + ) + } + + @Så("forvent følgende vedtaksperioder med begrunnelser") + fun `forvent følgende vedtaksperioder med begrunnelser`(dataTable: DataTable) { + val forventedeVedtaksperioder = mapForventetVedtaksperioderMedBegrunnelser( + dataTable = dataTable, + vedtak = vedtaksliste.find { it.behandling.id == gjeldendeBehandlingId } + ?: throw Feil("Fant ingen vedtak for behandling $gjeldendeBehandlingId"), + ) + + val vedtaksperioderComparator = compareBy({ it.type }, { it.fom }, { it.tom }) + Assertions.assertThat(vedtaksperioderMedBegrunnelser.sortedWith(vedtaksperioderComparator)) + .usingRecursiveComparison().ignoringFieldsMatchingRegexes(".*endretTidspunkt", ".*opprettetTidspunkt") + .isEqualTo(forventedeVedtaksperioder.sortedWith(vedtaksperioderComparator)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/VedtaksperiodeUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/VedtaksperiodeUtil.kt new file mode 100644 index 000000000..2d472cf76 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/VedtaksperiodeUtil.kt @@ -0,0 +1,441 @@ +package no.nav.familie.ba.sak.cucumber + +import io.cucumber.datatable.DataTable +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.tilddMMyyyy +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.cucumber.domeneparser.Domenebegrep +import no.nav.familie.ba.sak.cucumber.domeneparser.DomeneparserUtil.groupByBehandlingId +import no.nav.familie.ba.sak.cucumber.domeneparser.VedtaksperiodeMedBegrunnelserParser +import no.nav.familie.ba.sak.cucumber.domeneparser.parseDato +import no.nav.familie.ba.sak.cucumber.domeneparser.parseEnum +import no.nav.familie.ba.sak.cucumber.domeneparser.parseEnumListe +import no.nav.familie.ba.sak.cucumber.domeneparser.parseInt +import no.nav.familie.ba.sak.cucumber.domeneparser.parseList +import no.nav.familie.ba.sak.cucumber.domeneparser.parseLong +import no.nav.familie.ba.sak.cucumber.domeneparser.parseValgfriBoolean +import no.nav.familie.ba.sak.cucumber.domeneparser.parseValgfriDato +import no.nav.familie.ba.sak.cucumber.domeneparser.parseValgfriEnum +import no.nav.familie.ba.sak.cucumber.domeneparser.parseValgfriInt +import no.nav.familie.ba.sak.cucumber.domeneparser.parseValgfriLong +import no.nav.familie.ba.sak.cucumber.domeneparser.parseValgfriString +import no.nav.familie.ba.sak.cucumber.domeneparser.parseValgfriStringList +import no.nav.familie.ba.sak.ekstern.restDomene.BarnMedOpplysninger +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.InternPeriodeOvergangsstønad +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.beregning.domene.slåSammenTidligerePerioder +import no.nav.familie.ba.sak.kjerne.beregning.splittOgSlåSammen +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndel +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.lagDødsfall +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.domene.EØSBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.Vedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksbegrunnelseFritekst +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.UtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.tilVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.BehandlingsGrunnlagForVedtaksperioder +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.vedtaksperiodeProdusent.genererVedtaksperioder +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import java.math.BigDecimal +import java.time.LocalDate + +fun Map.finnBehandling(behandlingId: Long) = + this[behandlingId] ?: error("Finner ikke behandling med id $behandlingId") + +fun Map.finnPersonGrunnlagForBehandling(behandlingId: Long): PersonopplysningGrunnlag = + this[behandlingId] ?: error("Finner ikke persongrunnlag for behandling med id $behandlingId") + +fun lagFagsaker(dataTable: DataTable) = dataTable.asMaps().map { rad -> + Fagsak( + id = parseLong(Domenebegrep.FAGSAK_ID, rad), + type = parseValgfriEnum(Domenebegrep.FAGSAK_TYPE, rad) ?: FagsakType.NORMAL, + aktør = randomAktør(), + ) +}.associateBy { it.id } + +fun lagVedtak( + dataTable: DataTable, + behandlinger: MutableMap, + behandlingTilForrigeBehandling: MutableMap, + vedtaksListe: MutableList, + fagsaker: Map, +) { + behandlinger.putAll( + dataTable.asMaps().map { rad -> + val behandlingId = parseLong(Domenebegrep.BEHANDLING_ID, rad) + val fagsakId = parseValgfriLong(Domenebegrep.FAGSAK_ID, rad) + val fagsak = fagsaker[fagsakId] ?: defaultFagsak() + val behandlingÅrsak = parseValgfriEnum(Domenebegrep.BEHANDLINGSÅRSAK, rad) + val behandlingResultat = parseValgfriEnum(Domenebegrep.BEHANDLINGSRESULTAT, rad) + + lagBehandling( + fagsak = fagsak, + årsak = behandlingÅrsak ?: BehandlingÅrsak.SØKNAD, + resultat = behandlingResultat ?: Behandlingsresultat.IKKE_VURDERT, + ).copy(id = behandlingId) + }.associateBy { it.id }, + ) + behandlingTilForrigeBehandling.putAll( + dataTable.asMaps().associate { rad -> + parseLong(Domenebegrep.BEHANDLING_ID, rad) to parseValgfriLong(Domenebegrep.FORRIGE_BEHANDLING_ID, rad) + }, + ) + vedtaksListe.addAll( + dataTable.groupByBehandlingId() + .map { no.nav.familie.ba.sak.common.lagVedtak(behandlinger[it.key] ?: error("Finner ikke behandling")) }, + ) +} + +fun lagPersonresultater( + persongrunnlagForBehandling: PersonopplysningGrunnlag, + behandling: Behandling, +) = persongrunnlagForBehandling.personer.map { person -> + lagPersonResultat( + vilkårsvurdering = lagVilkårsvurdering(person.aktør, behandling, Resultat.OPPFYLT), + person = person, + resultat = Resultat.OPPFYLT, + personType = person.type, + lagFullstendigVilkårResultat = true, + periodeFom = null, + periodeTom = null, + ) +}.toSet() + +fun leggTilVilkårResultatPåPersonResultat( + personResultatForBehandling: Set, + vilkårResultaterPerPerson: Map>>, + behandlingId: Long, +) = personResultatForBehandling.map { personResultat -> + personResultat.vilkårResultater.clear() + + vilkårResultaterPerPerson[personResultat.aktør.aktørId]?.forEach { rad -> + val vilkårForÉnRad = parseEnumListe( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepVedtaksperiodeMedBegrunnelser.VILKÅR, + rad, + ) + + val utdypendeVilkårsvurderingForÉnRad = parseEnumListe( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepVedtaksperiodeMedBegrunnelser.UTDYPENDE_VILKÅR, + rad, + ) + + val vurderesEtterForEnRad = parseValgfriEnum( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepVedtaksperiodeMedBegrunnelser.VURDERES_ETTER, + rad, + ) ?: Regelverk.NASJONALE_REGLER + + val vilkårResultaterForÉnRad = vilkårForÉnRad.map { vilkår -> + VilkårResultat( + sistEndretIBehandlingId = behandlingId, + personResultat = personResultat, + vilkårType = vilkår, + resultat = parseEnum( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepVedtaksperiodeMedBegrunnelser.RESULTAT, + rad, + ), + periodeFom = parseValgfriDato(Domenebegrep.FRA_DATO, rad), + periodeTom = parseValgfriDato(Domenebegrep.TIL_DATO, rad), + erEksplisittAvslagPåSøknad = parseValgfriBoolean( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepVedtaksperiodeMedBegrunnelser.ER_EKSPLISITT_AVSLAG, + rad, + ), + begrunnelse = "", + utdypendeVilkårsvurderinger = utdypendeVilkårsvurderingForÉnRad, + vurderesEtter = vurderesEtterForEnRad, + ) + } + personResultat.vilkårResultater.addAll(vilkårResultaterForÉnRad) + } + personResultat +}.toSet() + +fun lagKompetanser( + nyeKompetanserPerBarn: MutableList>, + personopplysningGrunnlag: Map, +) = + nyeKompetanserPerBarn.map { rad -> + val aktørerForKompetanse = VedtaksperiodeMedBegrunnelserParser.parseAktørIdListe(rad) + val behandlingId = parseLong(Domenebegrep.BEHANDLING_ID, rad) + Kompetanse( + fom = parseValgfriDato(Domenebegrep.FRA_DATO, rad)?.toYearMonth(), + tom = parseValgfriDato(Domenebegrep.TIL_DATO, rad)?.toYearMonth(), + barnAktører = personopplysningGrunnlag.finnPersonGrunnlagForBehandling(behandlingId).personer + .filter { aktørerForKompetanse.contains(it.aktør.aktørId) } + .map { it.aktør } + .toSet(), + søkersAktivitet = parseValgfriEnum( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepKompetanse.SØKERS_AKTIVITET, + rad, + ) + ?: KompetanseAktivitet.ARBEIDER, + annenForeldersAktivitet = + parseValgfriEnum( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepKompetanse.ANNEN_FORELDERS_AKTIVITET, + rad, + ) + ?: KompetanseAktivitet.I_ARBEID, + søkersAktivitetsland = parseValgfriString( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepKompetanse.SØKERS_AKTIVITETSLAND, + rad, + )?.also { validerErLandkode(it) } ?: "PL", + annenForeldersAktivitetsland = parseValgfriString( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepKompetanse.ANNEN_FORELDERS_AKTIVITETSLAND, + rad, + )?.also { validerErLandkode(it) } ?: "NO", + barnetsBostedsland = parseValgfriString( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepKompetanse.BARNETS_BOSTEDSLAND, + rad, + )?.also { validerErLandkode(it) } ?: "NO", + resultat = parseEnum( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepKompetanse.RESULTAT, + rad, + ), + ).also { it.behandlingId = behandlingId } + }.groupBy { it.behandlingId } + .toMutableMap() + +private fun validerErLandkode(it: String) { + if (it.length != 2) { + error("$it er ikke en landkode") + } +} + +fun lagEndredeUtbetalinger( + nyeEndredeUtbetalingAndeler: MutableList>, + persongrunnlag: Map, +) = + nyeEndredeUtbetalingAndeler.map { rad -> + val aktørId = VedtaksperiodeMedBegrunnelserParser.parseAktørId(rad) + val behandlingId = parseLong(Domenebegrep.BEHANDLING_ID, rad) + EndretUtbetalingAndel( + behandlingId = behandlingId, + fom = parseValgfriDato(Domenebegrep.FRA_DATO, rad)?.toYearMonth(), + tom = parseValgfriDato(Domenebegrep.TIL_DATO, rad)?.toYearMonth(), + person = persongrunnlag.finnPersonGrunnlagForBehandling(behandlingId).personer.find { aktørId == it.aktør.aktørId }, + prosent = parseValgfriLong( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepEndretUtbetaling.PROSENT, + rad, + )?.toBigDecimal() ?: BigDecimal.valueOf(100), + årsak = parseValgfriEnum<Årsak>(VedtaksperiodeMedBegrunnelserParser.DomenebegrepEndretUtbetaling.ÅRSAK, rad) + ?: Årsak.ALLEREDE_UTBETALT, + søknadstidspunkt = LocalDate.now(), + begrunnelse = "Fordi at...", + avtaletidspunktDeltBosted = LocalDate.now(), + ) + }.groupBy { it.behandlingId } + .toMutableMap() + +fun lagPersonGrunnlag(dataTable: DataTable): Map { + return dataTable.asMaps().map { rad -> + val behandlingsIder = parseList(Domenebegrep.BEHANDLING_ID, rad) + behandlingsIder.map { id -> + id to tilfeldigPerson( + personType = parseEnum( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepPersongrunnlag.PERSON_TYPE, + rad, + ), + fødselsdato = parseDato( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepPersongrunnlag.FØDSELSDATO, + rad, + ), + aktør = randomAktør().copy(aktørId = VedtaksperiodeMedBegrunnelserParser.parseAktørId(rad)), + ).also { person -> + parseValgfriDato( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepPersongrunnlag.DØDSFALLDATO, + rad, + )?.let { person.dødsfall = lagDødsfall(person = person, dødsfallDato = it) } + } + } + }.flatten() + .groupBy({ it.first }, { it.second }) + .map { (behandlingId, personer) -> + PersonopplysningGrunnlag( + behandlingId = behandlingId, + personer = personer.toMutableSet(), + ) + }.associateBy { it.behandlingId } +} + +fun lagAndelerTilkjentYtelse( + dataTable: DataTable, + behandlinger: MutableMap, + personGrunnlag: Map, +) = dataTable.asMaps().map { rad -> + val aktørId = VedtaksperiodeMedBegrunnelserParser.parseAktørId(rad) + val behandlingId = parseLong(Domenebegrep.BEHANDLING_ID, rad) + val beløp = parseInt(VedtaksperiodeMedBegrunnelserParser.DomenebegrepVedtaksperiodeMedBegrunnelser.BELØP, rad) + lagAndelTilkjentYtelse( + fom = parseDato(Domenebegrep.FRA_DATO, rad).toYearMonth(), + tom = parseDato(Domenebegrep.TIL_DATO, rad).toYearMonth(), + behandling = behandlinger.finnBehandling(behandlingId), + person = personGrunnlag.finnPersonGrunnlagForBehandling(behandlingId).personer.find { aktørId == it.aktør.aktørId }!!, + beløp = beløp, + ytelseType = parseValgfriEnum( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepAndelTilkjentYtelse.YTELSE_TYPE, + rad, + ) ?: YtelseType.ORDINÆR_BARNETRYGD, + prosent = parseValgfriLong( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepEndretUtbetaling.PROSENT, + rad, + )?.toBigDecimal() ?: BigDecimal(100), + sats = parseValgfriInt( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepVedtaksperiodeMedBegrunnelser.SATS, + rad, + ) ?: beløp, + ) +}.groupBy { it.behandlingId } + .toMutableMap() + +fun lagOvergangsstønad( + dataTable: DataTable, + persongrunnlag: Map, + tidligereBehandlinger: Map, + dagensDato: LocalDate, +): Map> { + val overgangsstønadPeriodePåBehandlinger = dataTable.asMaps() + .groupBy({ rad -> parseLong(Domenebegrep.BEHANDLING_ID, rad) }, { rad -> + val behandlingId = parseLong(Domenebegrep.BEHANDLING_ID, rad) + val aktørId = VedtaksperiodeMedBegrunnelserParser.parseAktørId(rad) + + InternPeriodeOvergangsstønad( + fomDato = parseDato(Domenebegrep.FRA_DATO, rad), + tomDato = parseDato(Domenebegrep.TIL_DATO, rad), + personIdent = persongrunnlag[behandlingId]!!.personer.single { it.aktør.aktørId == aktørId }.aktør.aktivFødselsnummer(), + ) + }) + + return overgangsstønadPeriodePåBehandlinger.mapValues { (behandlingId, overgangsstønad) -> + overgangsstønad.splittOgSlåSammen( + overgangsstønadPeriodePåBehandlinger[tidligereBehandlinger[behandlingId]]?.slåSammenTidligerePerioder( + dagensDato, + ) ?: emptyList(), + dagensDato, + ) + } +} + +fun lagVedtaksPerioder( + behandlingId: Long, + vedtaksListe: List, + behandlingTilForrigeBehandling: MutableMap, + personGrunnlag: Map, + personResultater: Map>, + kompetanser: Map>, + endredeUtbetalinger: Map>, + andelerTilkjentYtelse: Map>, + overstyrteEndringstidspunkt: Map, + overgangsstønad: Map?>, + uregistrerteBarn: List, + nåDato: LocalDate, +): List { + val vedtak = vedtaksListe.find { it.behandling.id == behandlingId && it.aktiv } + ?: error("Finner ikke vedtak") + + vedtak.behandling.overstyrtEndringstidspunkt = overstyrteEndringstidspunkt[behandlingId] + val grunnlagForVedtaksperiode = BehandlingsGrunnlagForVedtaksperioder( + persongrunnlag = personGrunnlag.finnPersonGrunnlagForBehandling(behandlingId), + personResultater = personResultater[behandlingId] ?: error("Finner ikke personresultater"), + fagsakType = vedtak.behandling.fagsak.type, + kompetanser = kompetanser[behandlingId] ?: emptyList(), + endredeUtbetalinger = endredeUtbetalinger[behandlingId] ?: emptyList(), + andelerTilkjentYtelse = andelerTilkjentYtelse[behandlingId] ?: emptyList(), + perioderOvergangsstønad = overgangsstønad[behandlingId] ?: emptyList(), + uregistrerteBarn = uregistrerteBarn, + ) + + val forrigeBehandlingId = behandlingTilForrigeBehandling[behandlingId] + + val grunnlagForVedtaksperiodeForrigeBehandling = forrigeBehandlingId?.let { + val forrigeVedtak = vedtaksListe.find { it.behandling.id == forrigeBehandlingId && it.aktiv } + ?: error("Finner ikke vedtak") + BehandlingsGrunnlagForVedtaksperioder( + persongrunnlag = personGrunnlag.finnPersonGrunnlagForBehandling(forrigeBehandlingId), + personResultater = personResultater[forrigeBehandlingId] ?: error("Finner ikke personresultater"), + fagsakType = forrigeVedtak.behandling.fagsak.type, + kompetanser = kompetanser[forrigeBehandlingId] ?: emptyList(), + endredeUtbetalinger = endredeUtbetalinger[forrigeBehandlingId] ?: emptyList(), + andelerTilkjentYtelse = andelerTilkjentYtelse[forrigeBehandlingId] ?: emptyList(), + perioderOvergangsstønad = overgangsstønad[behandlingId] ?: emptyList(), + uregistrerteBarn = emptyList(), + ) + } + + return genererVedtaksperioder( + vedtak = vedtak, + grunnlagForVedtakPerioder = grunnlagForVedtaksperiode, + grunnlagForVedtakPerioderForrigeBehandling = grunnlagForVedtaksperiodeForrigeBehandling, + nåDato = nåDato, + ) +} + +fun leggBegrunnelserIVedtaksperiodene( + dataTable: DataTable, + vedtaksperioder: List, + vedtak: Vedtak, +) = dataTable.asMaps().map { rad -> + val fom = parseValgfriDato(Domenebegrep.FRA_DATO, rad) + val tom = parseValgfriDato(Domenebegrep.TIL_DATO, rad) + + val vedtaksperiode = + vedtaksperioder.find { it.fom == fom && it.tom == tom } + ?: throw Feil( + "Ingen vedtaksperioder med Fom=$fom og Tom=$tom. " + + "Vedtaksperiodene var ${vedtaksperioder.map { "\n${it.fom?.tilddMMyyyy()} til ${it.tom?.tilddMMyyyy()}" }}", + ) + val vedtaksperiodeMedBegrunnelser = vedtaksperiode.tilVedtaksperiodeMedBegrunnelser( + vedtak, + ) + + val standardbegrunnelser = parseEnumListe( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepVedtaksperiodeMedBegrunnelser.STANDARDBEGRUNNELSER, + rad, + ).map { + Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, + standardbegrunnelse = it, + ) + }.toMutableSet() + val eøsBegrunnelser = parseEnumListe( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepVedtaksperiodeMedBegrunnelser.EØSBEGRUNNELSER, + rad, + ).map { EØSBegrunnelse(vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, begrunnelse = it) } + .toMutableSet() + val fritekster = parseValgfriStringList( + VedtaksperiodeMedBegrunnelserParser.DomenebegrepVedtaksperiodeMedBegrunnelser.FRITEKSTER, + rad, + ).map { + VedtaksbegrunnelseFritekst( + vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, + fritekst = it, + ) + }.toMutableList() + + vedtaksperiodeMedBegrunnelser + .copy(begrunnelser = standardbegrunnelser, eøsBegrunnelser = eøsBegrunnelser, fritekster = fritekster) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/BasisDomeneParser.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/BasisDomeneParser.kt new file mode 100644 index 000000000..ec2607c4b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/BasisDomeneParser.kt @@ -0,0 +1,239 @@ +package no.nav.familie.ba.sak.cucumber.domeneparser + +import no.nav.familie.ba.sak.common.nbLocale +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth +import java.time.format.DateTimeFormatter + +val norskDatoFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") +val norskDatoFormatterKort = DateTimeFormatter.ofPattern("dd.MM.yy", nbLocale) +val norskÅrMånedFormatter = DateTimeFormatter.ofPattern("MM.yyyy") +val isoDatoFormatter = DateTimeFormatter.ISO_LOCAL_DATE +val isoÅrMånedFormatter = DateTimeFormatter.ofPattern("yyyy-MM") + +fun parseValgfriDatoListe(domenebegrep: Domenenøkkel, rad: Map): List { + val stringVerdier = parseValgfriString(domenebegrep, rad)?.split(",")?.map { it.trim() } ?: emptyList() + return stringVerdier.map { + parseDato(it) + } +} + +fun parseDatoListe(domenebegrep: Domenenøkkel, rad: Map): List { + val stringVerdier = parseString(domenebegrep, rad).split(",").map { it.trim() } + return stringVerdier.map { + parseDato(it) + } +} + +fun parseDato(domenebegrep: Domenenøkkel, rad: Map): LocalDate { + return parseDato(domenebegrep.nøkkel, rad) +} + +fun parseValgfriDato(domenebegrep: Domenenøkkel, rad: Map): LocalDate? { + return parseValgfriDato(domenebegrep.nøkkel, rad) +} + +fun parseÅrMåned(domenebegrep: Domenenøkkel, rad: Map): YearMonth { + return parseValgfriÅrMåned(domenebegrep.nøkkel, rad)!! +} + +fun parseValgfriÅrMåned(domenebegrep: Domenenøkkel, rad: Map): YearMonth? { + return parseValgfriÅrMåned(domenebegrep.nøkkel, rad) +} + +fun parseString(domenebegrep: Domenenøkkel, rad: Map): String { + return verdi(domenebegrep.nøkkel, rad) +} + +fun parseValgfriString(domenebegrep: Domenenøkkel, rad: Map): String? { + return valgfriVerdi(domenebegrep.nøkkel, rad) +} + +fun parseBooleanMedBooleanVerdi(domenebegrep: Domenenøkkel, rad: Map): Boolean { + val verdi = verdi(domenebegrep.nøkkel, rad) + + return when (verdi) { + "true" -> true + else -> false + } +} + +fun parseBooleanJaIsTrue(domenebegrep: Domenenøkkel, rad: Map): Boolean { + return when (valgfriVerdi(domenebegrep.nøkkel, rad)) { + "Ja" -> true + else -> false + } +} + +fun parseBoolean(domenebegrep: Domenenøkkel, rad: Map): Boolean { + val verdi = verdi(domenebegrep.nøkkel, rad) + + return when (verdi) { + "Ja" -> true + else -> false + } +} + +fun parseBoolean(verdi: String): Boolean { + return when (verdi) { + "Ja" -> true + else -> false + } +} + +fun parseValgfriBoolean(domenebegrep: Domenenøkkel, rad: Map): Boolean? { + val verdi = rad[domenebegrep.nøkkel] + if (verdi == null || verdi == "") { + return null + } + + return when (verdi.uppercase()) { + "JA" -> true + "NEI" -> false + else -> null + } +} + +fun parseDato(domenebegrep: String, rad: Map): LocalDate { + val dato = rad[domenebegrep]!! + + return parseDato(dato) +} + +fun parseDato(dato: String): LocalDate { + return if (dato.contains(".")) { + LocalDate.parse(dato, norskDatoFormatter) + } else { + LocalDate.parse(dato, isoDatoFormatter) + } +} + +fun parseValgfriDato(domenebegrep: String, rad: Map): LocalDate? { + val verdi = rad[domenebegrep] + if (verdi == null || verdi == "") { + return null + } + + return if (verdi.contains(".")) { + LocalDate.parse(verdi, norskDatoFormatter) + } else { + LocalDate.parse(verdi, isoDatoFormatter) + } +} + +fun parseValgfriÅrMåned(domenebegrep: String, rad: Map): YearMonth? { + val verdi = rad[domenebegrep] + if (verdi == null || verdi == "") { + return null + } + + return parseÅrMåned(verdi) +} + +fun parseÅrMåned(verdi: String): YearMonth { + return if (verdi.contains(".")) { + YearMonth.parse(verdi, norskÅrMånedFormatter) + } else { + YearMonth.parse(verdi, isoÅrMånedFormatter) + } +} + +fun parseValgfriÅrMånedEllerDato(domenebegrep: Domenenøkkel, rad: Map): ÅrMånedEllerDato? { + val verdi = rad[domenebegrep.nøkkel] + if (verdi == null || verdi == "") { + return null + } + val dato = when (verdi.toList().count { it == '.' || it == '-' }) { + 2 -> parseDato(verdi) + 1 -> parseÅrMåned(verdi) + else -> error("Er datoet=$verdi riktigt formatert? Trenger å være på norskt eller iso-format") + } + return ÅrMånedEllerDato(dato) +} + +fun verdi(nøkkel: String, rad: Map): String { + val verdi = rad[nøkkel] + + if (verdi == null || verdi == "") { + throw java.lang.RuntimeException("Fant ingen verdi for $nøkkel") + } + + return verdi +} + +fun valgfriVerdi(nøkkel: String, rad: Map): String? { + return rad[nøkkel] +} + +fun parseInt(domenebegrep: Domenenøkkel, rad: Map): Int { + val verdi = verdi(domenebegrep.nøkkel, rad).replace("_", "") + + return Integer.parseInt(verdi) +} + +fun parseLong(domenebegrep: Domenenøkkel, rad: Map): Long { + val verdi = verdi(domenebegrep.nøkkel, rad).replace("_", "") + + return verdi.toLong() +} + +fun parseList(domenebegrep: Domenenøkkel, rad: Map): List { + return verdi(domenebegrep.nøkkel, rad).split(",").map { it.trim().toLong() } +} + +fun parseStringList(domenebegrep: Domenenøkkel, rad: Map): List { + return verdi(domenebegrep.nøkkel, rad).split(",").map { it.trim() } +} + +fun parseValgfriStringList(domenebegrep: Domenenøkkel, rad: Map): List { + return valgfriVerdi(domenebegrep.nøkkel, rad)?.split(",")?.map { it.trim() } ?: emptyList() +} + +fun parseBigDecimal(domenebegrep: Domenenøkkel, rad: Map): BigDecimal { + val verdi = verdi(domenebegrep.nøkkel, rad) + return verdi.toBigDecimal() +} + +fun parseDouble(domenebegrep: Domenenøkkel, rad: Map): Double { + val verdi = verdi(domenebegrep.nøkkel, rad) + return verdi.toDouble() +} + +fun parseValgfriDouble(domenebegrep: Domenenøkkel, rad: Map): Double? { + return valgfriVerdi(domenebegrep.nøkkel, rad)?.toDouble() ?: return null +} + +fun parseValgfriLong(domenebegrep: Domenenøkkel, rad: Map): Long? = + parseValgfriInt(domenebegrep, rad)?.toLong() + +fun parseValgfriInt(domenebegrep: Domenenøkkel, rad: Map): Int? { + valgfriVerdi(domenebegrep.nøkkel, rad) ?: return null + + return parseInt(domenebegrep, rad) +} + +fun parseValgfriIntRange(domenebegrep: Domenenøkkel, rad: Map): Pair? { + val verdi = valgfriVerdi(domenebegrep.nøkkel, rad) ?: return null + + return Pair( + Integer.parseInt(verdi.split("-").first()), + Integer.parseInt(verdi.split("-").last()), + ) +} + +inline fun > parseValgfriEnum(domenebegrep: Domenenøkkel, rad: Map): T? { + val verdi = valgfriVerdi(domenebegrep.nøkkel, rad) ?: return null + return enumValueOf(verdi.uppercase()) +} + +inline fun > parseEnum(domenebegrep: Domenenøkkel, rad: Map): T { + return parseValgfriEnum(domenebegrep, rad)!! +} + +inline fun > parseEnumListe(domenebegrep: Domenenøkkel, rad: Map): List { + val stringVerdier = valgfriVerdi(domenebegrep.nøkkel, rad)?.split(",")?.map { it.trim() } ?: return emptyList() + return stringVerdier.map { + enumValueOf(it.uppercase()) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/BrevBegrunnelseParser.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/BrevBegrunnelseParser.kt new file mode 100644 index 000000000..4061cd209 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/BrevBegrunnelseParser.kt @@ -0,0 +1,72 @@ +package no.nav.familie.ba.sak.cucumber.domeneparser + +import io.cucumber.datatable.DataTable +import no.nav.familie.ba.sak.cucumber.SammenlignbarBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.IVedtakBegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk + +object BrevBegrunnelseParser { + + fun mapBegrunnelser(dataTable: DataTable): List { + return dataTable.asMaps().map { rad -> + val regelverkForInkluderteBegrunnelser = + parseValgfriEnum(DomenebegrepUtvidetVedtaksperiodeMedBegrunnelser.REGELVERK_INKLUDERTE_BEGRUNNELSER, rad) + ?: Regelverk.NASJONALE_REGLER + + val regelverkForEkskluderteBegrunnelser = + parseValgfriEnum(DomenebegrepUtvidetVedtaksperiodeMedBegrunnelser.REGELVERK_EKSKLUDERTE_BEGRUNNELSER, rad) + ?: regelverkForInkluderteBegrunnelser + + val inkluderteStandardBegrunnelser = hentForventedeBegrunnelser( + regelverkForInkluderteBegrunnelser, + DomenebegrepUtvidetVedtaksperiodeMedBegrunnelser.INKLUDERTE_BEGRUNNELSER, + rad, + ) + val ekskluderteStandardBegrunnelser = hentForventedeBegrunnelser( + regelverkForEkskluderteBegrunnelser, + DomenebegrepUtvidetVedtaksperiodeMedBegrunnelser.EKSKLUDERTE_BEGRUNNELSER, + rad, + ) + + SammenlignbarBegrunnelse( + fom = parseValgfriDato(Domenebegrep.FRA_DATO, rad), + tom = parseValgfriDato(Domenebegrep.TIL_DATO, rad), + type = parseEnum(DomenebegrepUtvidetVedtaksperiodeMedBegrunnelser.VEDTAKSPERIODE_TYPE, rad), + inkluderteStandardBegrunnelser = inkluderteStandardBegrunnelser, + ekskluderteStandardBegrunnelser = ekskluderteStandardBegrunnelser, + ) + } + } + + private fun hentForventedeBegrunnelser( + vurderesEtter: Regelverk, + inkludertEllerEkskludert: DomenebegrepUtvidetVedtaksperiodeMedBegrunnelser, + rad: Map, + ): Set { + return when (vurderesEtter) { + Regelverk.NASJONALE_REGLER -> { + parseEnumListe( + inkludertEllerEkskludert, + rad, + ).toSet() + } + + Regelverk.EØS_FORORDNINGEN -> { + parseEnumListe( + inkludertEllerEkskludert, + rad, + ).toSet() + } + } + } + + enum class DomenebegrepUtvidetVedtaksperiodeMedBegrunnelser(override val nøkkel: String) : Domenenøkkel { + VEDTAKSPERIODE_TYPE("VedtaksperiodeType"), + INKLUDERTE_BEGRUNNELSER("Inkluderte Begrunnelser"), + EKSKLUDERTE_BEGRUNNELSER("Ekskluderte Begrunnelser"), + REGELVERK_INKLUDERTE_BEGRUNNELSER("Regelverk Inkluderte Begrunnelser"), + REGELVERK_EKSKLUDERTE_BEGRUNNELSER("Regelverk Ekskluderte Begrunnelser"), + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/BrevPeriodeParser.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/BrevPeriodeParser.kt new file mode 100644 index 000000000..7d7c43650 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/BrevPeriodeParser.kt @@ -0,0 +1,26 @@ +package no.nav.familie.ba.sak.cucumber.domeneparser + +object BrevPeriodeParser { + + enum class DomenebegrepBrevBegrunnelse(override val nøkkel: String) : Domenenøkkel { + BEGRUNNELSE("Begrunnelse"), + GJELDER_SØKER("Gjelder søker"), + BARNAS_FØDSELSDATOER("Barnas fødselsdatoer"), + ANTALL_BARN("Antall barn"), + MÅNED_OG_ÅR_BEGRUNNELSEN_GJELDER_FOR("Måned og år begrunnelsen gjelder for"), + MÅLFORM("Målform"), + BELØP("Beløp"), + SØKNADSTIDSPUNKT("Søknadstidspunkt"), + AVTALETIDSPUNKT_DELT_BOSTED("Avtaletidspunkt delt bosted"), + SØKERS_RETT_TIL_UTVIDET("Søkers rett til utvidet"), + TYPE("Type"), + } + + enum class DomenebegrepBrevPeriode(override val nøkkel: String) : Domenenøkkel { + BARNAS_FØDSELSDAGER("Barnas fødselsdager"), + ANTALL_BARN("Antall barn med utbetaling"), + TYPE("Brevperiodetype"), + BELØP("Beløp"), + DU_ELLER_INSTITUSJONEN("Du eller institusjonen"), + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/DomeneparserUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/DomeneparserUtil.kt new file mode 100644 index 000000000..36a1015db --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/DomeneparserUtil.kt @@ -0,0 +1,25 @@ +package no.nav.familie.ba.sak.cucumber.domeneparser + +import io.cucumber.datatable.DataTable + +interface Domenenøkkel { + val nøkkel: String +} + +enum class Domenebegrep(override val nøkkel: String) : Domenenøkkel { + ID("Id"), + FAGSAK_ID("FagsakId"), + FAGSAK_TYPE("Fagsaktype"), + BEHANDLING_ID("BehandlingId"), + FORRIGE_BEHANDLING_ID("ForrigeBehandlingId"), + FRA_DATO("Fra dato"), + TIL_DATO("Til dato"), + ENDRET_MIGRERINGSDATO("Endret migreringsdato"), + BEHANDLINGSÅRSAK("Behandlingsårsak"), + BEHANDLINGSRESULTAT("Behandlingsresultat"), +} + +object DomeneparserUtil { + fun DataTable.groupByBehandlingId(): Map>> = + this.asMaps().groupBy { rad -> parseLong(Domenebegrep.BEHANDLING_ID, rad) } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/OppdragParser.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/OppdragParser.kt new file mode 100644 index 000000000..4b21f11b3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/OppdragParser.kt @@ -0,0 +1,187 @@ +package no.nav.familie.ba.sak.cucumber.domeneparser + +import io.cucumber.datatable.DataTable +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.cucumber.domeneparser.DomeneparserUtil.groupByBehandlingId +import no.nav.familie.ba.sak.integrasjoner.økonomi.YtelsetypeBA +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.Personident +import no.nav.familie.felles.utbetalingsgenerator.domain.AndelDataLongId +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import org.assertj.core.api.Assertions.assertThat +import java.time.LocalDate + +object OppdragParser { + + fun mapTilkjentYtelse( + dataTable: DataTable, + behandlinger: Map, + tilkjentYtelseId: Long = 0, + ): List { + var index = 0 + var tilkjentYtelseId = tilkjentYtelseId + return dataTable.groupByBehandlingId().map { (behandlingId, rader) -> + + val behandling = behandlinger.getValue(behandlingId) + val andeler = parseAndelder(behandling, rader, index) + index += andeler.size + + val tilkjentYtelse = TilkjentYtelse( + id = tilkjentYtelseId++, + behandling = behandling, + stønadFom = null, + stønadTom = null, + opphørFom = null, + opprettetDato = LocalDate.now(), + endretDato = LocalDate.now(), + utbetalingsoppdrag = null, + andelerTilkjentYtelse = andeler, + ) + andeler.forEach { it.tilkjentYtelse = tilkjentYtelse } + + tilkjentYtelse + } + } + + private fun parseAndelder( + behandling: Behandling, + rader: List>, + forrigeAndelId: Int, + ): MutableSet { + val erUtenAndeler = (parseValgfriBoolean(DomenebegrepTilkjentYtelse.UTEN_ANDELER, rader.first()) ?: false) + var andelId = forrigeAndelId + return if (erUtenAndeler) { + emptySet().toMutableSet() + } else { + rader.map { mapAndelTilkjentYtelse(it, behandling, andelId++) }.toMutableSet() + } + } + + fun mapForventetUtbetalingsoppdrag( + dataTable: DataTable, + ): List { + return dataTable.groupByBehandlingId().map { (behandlingId, rader) -> + val rad = rader.first() + validerAlleKodeEndringerLike(rader) + ForventetUtbetalingsoppdrag( + behandlingId = behandlingId, + kodeEndring = parseEnum(DomenebegrepUtbetalingsoppdrag.KODE_ENDRING, rad), + utbetalingsperiode = rader.map { mapForventetUtbetalingsperiode(it) }, + ) + } + } + + fun mapForventetBeståendeAndeler(dataTable: DataTable): List { + return dataTable.asMaps().map { rad -> + AndelDataLongId( + id = parseLong(Domenebegrep.ID, rad), + fom = parseÅrMåned(Domenebegrep.FRA_DATO, rad), + tom = parseÅrMåned(Domenebegrep.TIL_DATO, rad), + beløp = parseInt(DomenebegrepTilkjentYtelse.BELØP, rad), + personIdent = lagFødselsnummer("1"), + type = YtelsetypeBA.ORDINÆR_BARNETRYGD, + periodeId = parseValgfriLong(DomenebegrepUtbetalingsoppdrag.PERIODE_ID, rad), + forrigePeriodeId = parseValgfriLong(DomenebegrepUtbetalingsoppdrag.FORRIGE_PERIODE_ID, rad), + kildeBehandlingId = parseValgfriLong(DomenebegrepTilkjentYtelse.KILDEBEHANDLING_ID, rad), + ) + } + } + + private fun mapForventetUtbetalingsperiode(it: Map) = + ForventetUtbetalingsperiode( + erEndringPåEksisterendePeriode = parseBoolean(DomenebegrepUtbetalingsoppdrag.ER_ENDRING, it), + periodeId = parseLong(DomenebegrepUtbetalingsoppdrag.PERIODE_ID, it), + forrigePeriodeId = parseValgfriLong(DomenebegrepUtbetalingsoppdrag.FORRIGE_PERIODE_ID, it), + sats = parseInt(DomenebegrepUtbetalingsoppdrag.BELØP, it), + ytelse = parseValgfriEnum(DomenebegrepUtbetalingsoppdrag.YTELSE_TYPE, it) + ?: YtelseType.ORDINÆR_BARNETRYGD, + fom = parseÅrMåned(Domenebegrep.FRA_DATO, it).atDay(1), + tom = parseÅrMåned(Domenebegrep.TIL_DATO, it).atEndOfMonth(), + opphør = parseValgfriÅrMåned(DomenebegrepUtbetalingsoppdrag.OPPHØRSDATO, it)?.atDay(1), + kildebehandlingId = parseValgfriLong(DomenebegrepTilkjentYtelse.KILDEBEHANDLING_ID, it), + ) + + private fun validerAlleKodeEndringerLike(rader: List>) { + rader.map { parseEnum(DomenebegrepUtbetalingsoppdrag.KODE_ENDRING, it) } + .zipWithNext().forEach { + assertThat(it.first).isEqualTo(it.second) + .withFailMessage("Alle kodeendringer for en og samme oppdrag må være lik ${it.first} -> ${it.second}") + } + } + + private fun mapAndelTilkjentYtelse( + rad: Map, + behandling: Behandling, + andelId: Int, + ): AndelTilkjentYtelse { + val ytelseType = + parseValgfriEnum(DomenebegrepTilkjentYtelse.YTELSE_TYPE, rad) ?: YtelseType.ORDINÆR_BARNETRYGD + return lagAndelTilkjentYtelse( + fom = parseÅrMåned(Domenebegrep.FRA_DATO, rad), + tom = parseÅrMåned(Domenebegrep.TIL_DATO, rad), + ytelseType = ytelseType, + beløp = parseInt(DomenebegrepTilkjentYtelse.BELØP, rad), + behandling = behandling, + tilkjentYtelse = null, + kildeBehandlingId = parseValgfriLong(DomenebegrepTilkjentYtelse.KILDEBEHANDLING_ID, rad), + aktør = parseAktør(rad), + periodeIdOffset = parseValgfriLong(DomenebegrepUtbetalingsoppdrag.PERIODE_ID, rad), + forrigeperiodeIdOffset = parseValgfriLong(DomenebegrepUtbetalingsoppdrag.FORRIGE_PERIODE_ID, rad), + ).copy(id = parseValgfriLong(Domenebegrep.ID, rad) ?: andelId.toLong()) + } + + private fun parseAktør(rad: Map): Aktør { + val id = (parseValgfriInt(DomenebegrepTilkjentYtelse.IDENT, rad) ?: 1).toString() + val aktørId = id.padStart(13, '0') + val fødselsnummer = lagFødselsnummer(id) + return Aktør(aktørId).also { + it.personidenter.add(Personident(fødselsnummer, it)) + } + } + + private fun lagFødselsnummer(id: String) = id.padStart(11, '0') +} + +enum class DomenebegrepBehandlingsinformasjon(override val nøkkel: String) : Domenenøkkel { + ENDRET_MIGRERINGSDATO("Endret migreringsdato"), +} + +enum class DomenebegrepTilkjentYtelse(override val nøkkel: String) : Domenenøkkel { + YTELSE_TYPE("Ytelse"), + UTEN_ANDELER("Uten andeler"), + BELØP("Beløp"), + KILDEBEHANDLING_ID("Kildebehandling"), + IDENT("Ident"), +} + +enum class DomenebegrepUtbetalingsoppdrag(override val nøkkel: String) : Domenenøkkel { + KODE_ENDRING("Kode endring"), + ER_ENDRING("Er endring"), + PERIODE_ID("Periode id"), + FORRIGE_PERIODE_ID("Forrige periode id"), + BELØP("Beløp"), + YTELSE_TYPE("Ytelse"), + OPPHØRSDATO("Opphørsdato"), +} + +data class ForventetUtbetalingsoppdrag( + val behandlingId: Long, + val kodeEndring: Utbetalingsoppdrag.KodeEndring, + val utbetalingsperiode: List, +) + +data class ForventetUtbetalingsperiode( + val erEndringPåEksisterendePeriode: Boolean, + val periodeId: Long, + val forrigePeriodeId: Long?, + val sats: Int, + val ytelse: YtelseType, + val fom: LocalDate, + val tom: LocalDate, + val opphør: LocalDate?, + val kildebehandlingId: Long?, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/VedtaksperiodeMedBegrunnelserParser.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/VedtaksperiodeMedBegrunnelserParser.kt new file mode 100644 index 000000000..b43090447 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/VedtaksperiodeMedBegrunnelserParser.kt @@ -0,0 +1,83 @@ +package no.nav.familie.ba.sak.cucumber.domeneparser + +import io.cucumber.datatable.DataTable +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.Vedtaksbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser + +object VedtaksperiodeMedBegrunnelserParser { + + fun mapForventetVedtaksperioderMedBegrunnelser( + dataTable: DataTable, + vedtak: Vedtak, + ): List { + return dataTable.asMaps().map { rad -> + VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = parseValgfriDato(Domenebegrep.FRA_DATO, rad), + tom = parseValgfriDato(Domenebegrep.TIL_DATO, rad), + type = parseEnum(DomenebegrepVedtaksperiodeMedBegrunnelser.VEDTAKSPERIODE_TYPE, rad), + ).also { vedtaksperiodeMedBegrunnelser -> + val begrunnelser = + parseEnumListe(DomenebegrepVedtaksperiodeMedBegrunnelser.BEGRUNNELSER, rad) + + vedtaksperiodeMedBegrunnelser.begrunnelser.addAll( + begrunnelser.map { + Vedtaksbegrunnelse( + vedtaksperiodeMedBegrunnelser = vedtaksperiodeMedBegrunnelser, + standardbegrunnelse = it, + ) + }, + ) + } + } + } + + fun parseAktørId(rad: MutableMap) = + parseString(DomenebegrepPersongrunnlag.AKTØR_ID, rad).padEnd(13, '0') + + fun parseAktørIdListe(rad: MutableMap) = + parseStringList(DomenebegrepPersongrunnlag.AKTØR_ID, rad).map { it.padEnd(13, '0') } + + enum class DomenebegrepPersongrunnlag(override val nøkkel: String) : Domenenøkkel { + PERSON_TYPE("Persontype"), + FØDSELSDATO("Fødselsdato"), + DØDSFALLDATO("Dødsfalldato"), + AKTØR_ID("AktørId"), + } + + enum class DomenebegrepVedtaksperiodeMedBegrunnelser(override val nøkkel: String) : Domenenøkkel { + VEDTAKSPERIODE_TYPE("Vedtaksperiodetype"), + VILKÅR("Vilkår"), + UTDYPENDE_VILKÅR("Utdypende vilkår"), + RESULTAT("Resultat"), + VURDERES_ETTER("Vurderes etter"), + BELØP("Beløp"), + SATS("Sats"), + ER_EKSPLISITT_AVSLAG("Er eksplisitt avslag"), + ENDRINGSTIDSPUNKT("Endringstidspunkt"), + BEGRUNNELSER("Begrunnelser"), + STANDARDBEGRUNNELSER("Standardbegrunnelser"), + EØSBEGRUNNELSER("Eøsbegrunnelser"), + FRITEKSTER("Fritekster"), + } + + enum class DomenebegrepKompetanse(override val nøkkel: String) : Domenenøkkel { + SØKERS_AKTIVITET("Søkers aktivitet"), + ANNEN_FORELDERS_AKTIVITET("Annen forelders aktivitet"), + SØKERS_AKTIVITETSLAND("Søkers aktivitetsland"), + ANNEN_FORELDERS_AKTIVITETSLAND("Annen forelders aktivitetsland"), + BARNETS_BOSTEDSLAND("Barnets bostedsland"), + RESULTAT("Resultat"), + } + + enum class DomenebegrepEndretUtbetaling(override val nøkkel: String) : Domenenøkkel { + PROSENT("Prosent"), + ÅRSAK("Årsak"), + } + + enum class DomenebegrepAndelTilkjentYtelse(override val nøkkel: String) : Domenenøkkel { + YTELSE_TYPE("Ytelse type"), + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/\303\205rM\303\245nedEllerDato.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/\303\205rM\303\245nedEllerDato.kt" new file mode 100644 index 000000000..baf371f68 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/cucumber/domeneparser/\303\205rM\303\245nedEllerDato.kt" @@ -0,0 +1,35 @@ +package no.nav.familie.ba.sak.cucumber.domeneparser + +import java.time.LocalDate +import java.time.YearMonth + +data class ÅrMånedEllerDato(val dato: Any) { + + fun førsteDagenIMåneden(): LocalDate { + return if (dato is LocalDate) { + require(dato.dayOfMonth != 1) { "Må være første dagen i måneden - $dato" } + dato + } else if (dato is YearMonth) { + dato.atDay(1) + } else { + error("Typen er feil - ${dato::class.java.simpleName}") + } + } + + fun sisteDagenIMåneden(): LocalDate { + return if (dato is LocalDate) { + require(dato != YearMonth.from(dato).atEndOfMonth()) { "Må være siste dagen i måneden - $dato" } + dato + } else if (dato is YearMonth) { + dato.atEndOfMonth() + } else { + error("Typen er feil - ${dato::class.java.simpleName}") + } + } +} + +fun ÅrMånedEllerDato?.førsteDagenIMånedenEllerDefault(dato: LocalDate = YearMonth.now().atDay(1)) = + this?.førsteDagenIMåneden() ?: dato + +fun ÅrMånedEllerDato?.sisteDagenIMånedenEllerDefault(dato: LocalDate = YearMonth.now().atEndOfMonth()) = + this?.sisteDagenIMåneden() ?: dato diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/datagenerator/vilk\303\245rsvurdering/Vilk\303\245rsvurdering.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/datagenerator/vilk\303\245rsvurdering/Vilk\303\245rsvurdering.kt" new file mode 100644 index 000000000..e7de29d27 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/datagenerator/vilk\303\245rsvurdering/Vilk\303\245rsvurdering.kt" @@ -0,0 +1,160 @@ +package no.nav.familie.ba.sak.datagenerator.vilkårsvurdering + +import io.mockk.mockk +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.Personident +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import java.time.LocalDate + +typealias AktørId = String + +/** + * Setter vilkår som ikke er overstyrte til oppfylt fra det seneste av + * fødselsdato eller tre år tilbake i tid for personen som vurderes. + * Dersom personen er et barn settes også tom-datoen til barnets attenårsdag. + * Om du vil ha med utvidet vilkår og delt bosted må det sendes med uansett. + **/ +fun lagVilkårsvurderingMedOverstyrendeResultater( + søker: Person, + barna: List, + behandling: Behandling? = null, + id: Long = 0, + overstyrendeVilkårResultater: Map>, +): Vilkårsvurdering { + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling ?: mockk(relaxed = true), id = id) + + val søkerPersonResultater = lagPersonResultatAvOverstyrteResultater( + person = søker, + overstyrendeVilkårResultater = overstyrendeVilkårResultater[søker.aktør.aktørId] ?: emptyList(), + vilkårsvurdering = vilkårsvurdering, + id = id, + ) + + val barnaPersonResultater = barna.map { + lagPersonResultatAvOverstyrteResultater( + person = it, + overstyrendeVilkårResultater = overstyrendeVilkårResultater[it.aktør.aktørId] ?: emptyList(), + vilkårsvurdering = vilkårsvurdering, + ) + } + + vilkårsvurdering.personResultater = barnaPersonResultater.toSet() + søkerPersonResultater + return vilkårsvurdering +} + +fun lagVilkårsvurderingFraRestScenario( + scenario: RestScenario, + overstyrendeVilkårResultater: Map>, +): Vilkårsvurdering { + fun RestScenarioPerson.TilAktør() = Aktør( + this.aktørId!!, + mutableSetOf(Personident(this.ident!!, mockk(relaxed = true))), + ) + + val søker = + lagPerson( + aktør = scenario.søker.TilAktør(), + fødselsdato = LocalDate.parse(scenario.søker.fødselsdato), + type = PersonType.SØKER, + ) + val barna = scenario.barna.map { + lagPerson( + aktør = it.TilAktør(), + fødselsdato = LocalDate.parse(it.fødselsdato), + type = PersonType.BARN, + ) + } + return lagVilkårsvurderingMedOverstyrendeResultater( + søker = søker, + barna = barna, + overstyrendeVilkårResultater = overstyrendeVilkårResultater, + ) +} + +fun lagSøkerVilkårResultat( + søkerPersonResultat: PersonResultat, + periodeFom: LocalDate, + periodeTom: LocalDate? = null, + behandlingId: Long, +): Set { + return setOf( + lagVilkårResultat( + personResultat = søkerPersonResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = periodeFom, + periodeTom = periodeTom, + behandlingId = behandlingId, + ), + lagVilkårResultat( + personResultat = søkerPersonResultat, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + periodeFom = periodeFom, + periodeTom = periodeTom, + behandlingId = behandlingId, + ), + ) +} + +fun lagBarnVilkårResultat( + barnPersonResultat: PersonResultat, + barnetsFødselsdato: LocalDate, + behandlingId: Long, + periodeFom: LocalDate, + flytteSak: Boolean = false, +): Set { + return setOf( + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.UNDER_18_ÅR, + resultat = Resultat.OPPFYLT, + periodeFom = barnetsFødselsdato, + periodeTom = barnetsFødselsdato.plusYears(18).minusMonths(1), + behandlingId = behandlingId, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.GIFT_PARTNERSKAP, + resultat = Resultat.OPPFYLT, + periodeFom = barnetsFødselsdato, + periodeTom = null, + behandlingId = behandlingId, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + periodeFom = periodeFom, + periodeTom = null, + behandlingId = behandlingId, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = if (flytteSak) barnetsFødselsdato else periodeFom, + periodeTom = null, + behandlingId = behandlingId, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + periodeFom = if (flytteSak) barnetsFødselsdato else periodeFom, + periodeTom = null, + behandlingId = behandlingId, + ), + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/bisys/BisysControllerIntegrasjonsTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/bisys/BisysControllerIntegrasjonsTest.kt new file mode 100644 index 000000000..1c7047c05 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/bisys/BisysControllerIntegrasjonsTest.kt @@ -0,0 +1,340 @@ +package no.nav.familie.ba.sak.ekstern.bisys + +import com.fasterxml.jackson.module.kotlin.readValue +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock.aResponse +import com.github.tomakehurst.wiremock.client.WireMock.equalToJson +import com.github.tomakehurst.wiremock.client.WireMock.post +import com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor +import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo +import no.nav.familie.ba.sak.WebSpringAuthTestRunner +import no.nav.familie.ba.sak.common.EksternTjenesteFeil +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.kontrakter.felles.objectMapper +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.within +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.core.io.ClassPathResource +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.test.context.ActiveProfiles +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.HttpServerErrorException +import org.springframework.web.client.postForEntity +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.YearMonth +import java.time.temporal.ChronoUnit + +@ActiveProfiles("postgres", "integrasjonstest", "mock-pdl", "mock-ident-client", "mock-oauth", "mock-brev-klient") +class BisysControllerIntegrasjonsTest : WebSpringAuthTestRunner() { + + // Trenger fast port for at klienten i ba-sak kan kalle wiremock'en + private val wireMockServer = WireMockServer(28085) + + @BeforeEach + fun setUp() { + wireMockServer.start() + } + + @AfterEach + fun after() { + wireMockServer.resetAll() + wireMockServer.stop() + } + + @Test + fun `Skal kaste feil når fraDato er mer enn 5 år tilbake i tid`() { + val fnr = randomFnr() + + val requestEntity = byggRequestEntity( + BisysUtvidetBarnetrygdRequest( + fnr, + LocalDate.now().minusYears(5).minusDays(1), + ), + ) + + val error = assertThrows { + restTemplate.postForEntity( + hentUrl("/api/bisys/hent-utvidet-barnetrygd"), + requestEntity, + ) + } + + assertThat(error.statusCode).isEqualTo(HttpStatus.BAD_REQUEST) + val errorObject = objectMapper.readValue(error.responseBodyAsByteArray) + assertThat(errorObject.melding).isEqualTo("fraDato kan ikke være lenger enn 5 år tilbake i tid") + assertThat(errorObject.path).isEqualTo("/api/bisys/hent-utvidet-barnetrygd") + assertThat(errorObject.timestamp).isCloseTo(LocalDateTime.now(), within(10, ChronoUnit.SECONDS)) + } + + @Test + fun `Skal kaste gode feilmeldinger ved feil mot infotrygd-barnetrygd`() { + val fnr = randomFnr() + + wireMockServer.stubFor( + post(urlEqualTo("/infotrygd/barnetrygd/utvidet")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("foobar"), + ), + ) + + val requestEntity = byggRequestEntity( + BisysUtvidetBarnetrygdRequest( + fnr, + LocalDate.now(), + ), + ) + + val error = assertThrows { + restTemplate.postForEntity( + hentUrl("/api/bisys/hent-utvidet-barnetrygd"), + requestEntity, + ) + } + + assertThat(error.statusCode).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + val errorObject = objectMapper.readValue(error.responseBodyAsByteArray) + assertThat(errorObject.melding).isEqualTo("Henting av utvidet barnetrygd feilet. Gav feil: 500 Server Error: \"foobar\"") + assertThat(errorObject.path).isEqualTo("/api/bisys/hent-utvidet-barnetrygd") + assertThat(errorObject.timestamp).isCloseTo(LocalDateTime.now(), within(10, ChronoUnit.SECONDS)) + assertThat(errorObject.exception).contains("HttpServerErrorException") + assertThat(errorObject.stackTrace).isNotEmpty + } + + @Test + fun `Skal returnere tom periode hvis det ikke er noen utbetalinger i infotrygd-barnetrygd`() { + val fnr = randomFnr() + + wireMockServer.stubFor( + post(urlEqualTo("/infotrygd/barnetrygd/utvidet")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(gyldigOppgaveResponse("tom")), + ), + ) + + val requestEntity = byggRequestEntity( + BisysUtvidetBarnetrygdRequest( + fnr, + LocalDate.now().minusYears(4), + ), + ) + + val responseEntity = restTemplate.postForEntity( + hentUrl("/api/bisys/hent-utvidet-barnetrygd"), + requestEntity, + ) + wireMockServer.verify( + postRequestedFor(urlEqualTo("/infotrygd/barnetrygd/utvidet")) + .withRequestBody( + equalToJson( + """{"personIdent":"$fnr", "fraDato":"${ + YearMonth.now().minusYears(4) + }" }""", + ), + ), + + ) + + assertThat(responseEntity.statusCode).isEqualTo(HttpStatus.OK) + assertThat(responseEntity.body).isNotNull + assertThat(responseEntity.body!!.perioder).isEmpty() + } + + @Test + fun `Skal returnere perioder hvis det er noen utbetalinger i infotrygd-barnetrygd`() { + val fnr = randomFnr() + + wireMockServer.stubFor( + post(urlEqualTo("/infotrygd/barnetrygd/utvidet")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(gyldigOppgaveResponse("tom")), + ), + ) + + wireMockServer.stubFor( + post(urlEqualTo("/infotrygd/barnetrygd/utvidet")) + .withRequestBody( + equalToJson( + """{"personIdent":"$fnr", "fraDato":"${ + YearMonth.now().minusYears(4) + }" }""", + ), + ) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(gyldigOppgaveResponse("med-perioder")), + ), + ) + + val requestEntity = byggRequestEntity( + BisysUtvidetBarnetrygdRequest( + fnr, + LocalDate.now().minusYears(4), + ), + ) + + val responseEntity = restTemplate.postForEntity( + hentUrl("/api/bisys/hent-utvidet-barnetrygd"), + requestEntity, + ) + + assertThat(responseEntity.statusCode).isEqualTo(HttpStatus.OK) + assertThat(responseEntity.body).isNotNull + assertThat(responseEntity.body!!.perioder) + .hasSize(2) + assertThat(responseEntity.body!!.perioder) + .contains( + UtvidetBarnetrygdPeriode( + BisysStønadstype.SMÅBARNSTILLEGG, + YearMonth.of(2019, 12), + null, + 660.0, + false, + ), + ) + assertThat(responseEntity.body!!.perioder) + .contains(UtvidetBarnetrygdPeriode(BisysStønadstype.UTVIDET, YearMonth.of(2019, 12), null, 1054.0, false)) + } + + @Test + fun `Skal også returnere gamle perioder hvis det er noen utbetalinger i infotrygd-barnetrygd`() { + val fnr = randomFnr() + + wireMockServer.stubFor( + post(urlEqualTo("/infotrygd/barnetrygd/utvidet")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(gyldigOppgaveResponse("gammel-periode")), + ), + ) + + wireMockServer.stubFor( + post(urlEqualTo("/infotrygd/barnetrygd/utvidet")) + .withRequestBody( + equalToJson( + """{"personIdent":"$fnr", "fraDato":"${ + YearMonth.now().minusYears(4) + }" }""", + ), + ) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(gyldigOppgaveResponse("med-perioder")), + ), + ) + + val requestEntity = byggRequestEntity( + BisysUtvidetBarnetrygdRequest( + fnr, + LocalDate.now().minusYears(4), + ), + ) + + val responseEntity = restTemplate.postForEntity( + hentUrl("/api/bisys/hent-utvidet-barnetrygd"), + requestEntity, + ) + + assertThat(responseEntity.statusCode).isEqualTo(HttpStatus.OK) + assertThat(responseEntity.body).isNotNull + assertThat(responseEntity.body!!.perioder) + .hasSize(3) + assertThat(responseEntity.body!!.perioder) + .contains( + UtvidetBarnetrygdPeriode( + BisysStønadstype.SMÅBARNSTILLEGG, + YearMonth.of(2019, 12), + null, + 660.0, + false, + ), + ) + assertThat(responseEntity.body!!.perioder) + .contains(UtvidetBarnetrygdPeriode(BisysStønadstype.UTVIDET, YearMonth.of(2019, 12), null, 1054.0, false)) + assertThat(responseEntity.body!!.perioder) + .contains( + UtvidetBarnetrygdPeriode( + BisysStønadstype.UTVIDET, + YearMonth.of(2017, 1), + YearMonth.of(2018, 12), + 970.0, + false, + ), + ) + } + + @Test + fun `Skal kaste feil tilgang når bisys kaller tjenste som ikke er bisys-relatert`() { + val header = HttpHeaders() + header.contentType = MediaType.APPLICATION_JSON + header.setBearerAuth( + hentTokenForBisys(), + ) + val ikkeBisysTjeneste = HttpEntity( + "tullball", + header, + ) + + val error = assertThrows { + restTemplate.postForEntity( + hentUrl("/api/tullballtjeneste"), + ikkeBisysTjeneste, + ) + } + + assertThat(error.statusCode).isEqualTo(HttpStatus.UNAUTHORIZED) + } + + private fun byggRequestEntity(request: BisysUtvidetBarnetrygdRequest): HttpEntity { + val header = HttpHeaders() + header.contentType = MediaType.APPLICATION_JSON + header.setBearerAuth( + hentTokenForBisys(), + ) + return HttpEntity( + objectMapper.writeValueAsString( + request, + ), + header, + ) + } + + private fun hentTokenForBisys() = token( + mapOf( + "groups" to listOf("SAKSBEHANDLER"), + "name" to "Mock McMockface", + "NAVident" to "Z0000", + ), + clientId = "dummy", + ) + + private fun gyldigOppgaveResponse(filnavn: String): String { + return Files.readString( + ClassPathResource("ekstern/bisys-$filnavn.json").file.toPath(), + StandardCharsets.UTF_8, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/FinnIdenterMedL\303\270pendeBarnetrygdForGitt\303\205rTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/FinnIdenterMedL\303\270pendeBarnetrygdForGitt\303\205rTest.kt" new file mode 100644 index 000000000..e9cfa60a9 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/FinnIdenterMedL\303\270pendeBarnetrygdForGitt\303\205rTest.kt" @@ -0,0 +1,117 @@ +package no.nav.familie.ba.sak.ekstern.pensjon + +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.årMnd +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +class FinnIdenterMedLøpendeBarnetrygdForGittÅrTest() : AbstractSpringIntegrationTest() { + + @Autowired + lateinit var databaseCleanupService: DatabaseCleanupService + + @Autowired + lateinit var fagsakRepository: FagsakRepository + + @Autowired + lateinit var personidentService: PersonidentService + + @Autowired + lateinit var behandlingService: BehandlingService + + @Autowired + lateinit var fagsakService: FagsakService + + @Autowired + lateinit var andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository + + @Autowired + lateinit var tilkjentYtelseRepository: TilkjentYtelseRepository + + @Autowired + lateinit var behandlingHentOgPersisterService: BehandlingHentOgPersisterService + + @Test + fun `Skal plukke riktig identer som har hatt barnetryd i løpet av gitt år`() { + val søker = tilfeldigPerson() + val barn1 = tilfeldigPerson() + personidentService.hentOgLagreAktør(søker.aktør.aktivFødselsnummer(), true) + val barnAktør = personidentService.hentOgLagreAktør(barn1.aktør.aktivFødselsnummer(), true) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søker.aktør.aktivFødselsnummer()) + with(behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak))) { + val behandling = this + with(lagInitiellTilkjentYtelse(behandling, "utbetalingsoppdrag")) { + val andel = lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2023-03"), + no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType.ORDINÆR_BARNETRYGD, + 660, + behandling, + person = barn1, + aktør = barnAktør, + tilkjentYtelse = this, + ) + andelerTilkjentYtelse.add(andel) + tilkjentYtelseRepository.save(this) + } + avsluttOgLagreBehandling(behandling) + } + val identer = andelTilkjentYtelseRepository.finnIdenterMedLøpendeBarnetrygdForGittÅr(2023) + Assertions.assertTrue(identer.isNotEmpty()) + Assertions.assertTrue(identer.contains(søker.aktør.aktivFødselsnummer())) + } + + @Test + fun `Verifiser at ingen har barnetrygd for angitt år `() { + val søker = tilfeldigPerson() + val barn1 = tilfeldigPerson() + personidentService.hentOgLagreAktør(søker.aktør.aktivFødselsnummer(), true) + val barnAktør = personidentService.hentOgLagreAktør(barn1.aktør.aktivFødselsnummer(), true) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søker.aktør.aktivFødselsnummer()) + with(behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak))) { + val behandling = this + with(lagInitiellTilkjentYtelse(behandling, "utbetalingsoppdrag")) { + val andel = lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2023-03"), + no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType.ORDINÆR_BARNETRYGD, + 660, + behandling, + person = barn1, + aktør = barnAktør, + tilkjentYtelse = this, + ) + andelerTilkjentYtelse.add(andel) + tilkjentYtelseRepository.save(this) + } + avsluttOgLagreBehandling(behandling) + } + + val identer = andelTilkjentYtelseRepository.finnIdenterMedLøpendeBarnetrygdForGittÅr(2018) + Assertions.assertFalse(identer.contains(søker.aktør.aktivFødselsnummer())) + } + + private fun avsluttOgLagreBehandling(behandling: Behandling) { + behandling.status = BehandlingStatus.AVSLUTTET + behandling.leggTilBehandlingStegTilstand(StegType.BEHANDLING_AVSLUTTET) + behandlingHentOgPersisterService.lagreEllerOppdater(behandling, false) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/PensjonControllerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/PensjonControllerTest.kt new file mode 100644 index 000000000..0f116975a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/PensjonControllerTest.kt @@ -0,0 +1,43 @@ +package no.nav.familie.ba.sak.ekstern.pensjon + +import no.nav.familie.ba.sak.WebSpringAuthTestRunner +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.test.context.ActiveProfiles +import java.util.Arrays +import java.util.UUID + +@ActiveProfiles("postgres", "integrasjonstest", "mock-pdl", "mock-ident-client", "mock-oauth", "mock-brev-klient") +class PensjonControllerTest : WebSpringAuthTestRunner() { + + @Test + fun `Verifiser at pensjon-endepunkt - bestillPersonerMedBarnetrygdForGittÅrPåKafka - for henting av identer med barnetrygd - returnerer en gyldig UUID som string`() { + val headers = HttpHeaders() + headers.accept = Arrays.asList(MediaType.TEXT_PLAIN) + headers.setBearerAuth( + hentTokenForPsys(), + ) + val entity: HttpEntity = HttpEntity(headers) + val responseEntity: ResponseEntity = restTemplate.exchange( + hentUrl("/api/ekstern/pensjon/bestill-personer-med-barnetrygd/2023"), + HttpMethod.GET, + entity, + String::class.java, + ) + assertEquals(UUID.fromString(responseEntity.body.toString()).toString(), responseEntity.body.toString()) + } + + private fun hentTokenForPsys() = token( + mapOf( + "groups" to listOf("SAKSBEHANDLER"), + "name" to "Mock McMockface", + "NAVident" to "Z0000", + ), + clientId = "omsorgsopptjening", + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/PensjonServiceIntegrationTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/PensjonServiceIntegrationTest.kt new file mode 100644 index 000000000..cb48f753f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/pensjon/PensjonServiceIntegrationTest.kt @@ -0,0 +1,105 @@ +package no.nav.familie.ba.sak.ekstern.pensjon + +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.årMnd +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class PensjonServiceIntegrationTest : AbstractSpringIntegrationTest() { + @Autowired + lateinit var databaseCleanupService: DatabaseCleanupService + + @Autowired + lateinit var fagsakRepository: FagsakRepository + + @Autowired + lateinit var personidentService: PersonidentService + + @Autowired + lateinit var behandlingService: BehandlingService + + @Autowired + lateinit var fagsakService: FagsakService + + @Autowired + lateinit var pensjonService: PensjonService + + @Autowired + lateinit var tilkjentYtelseRepository: TilkjentYtelseRepository + + @Autowired + lateinit var behandlingHentOgPersisterService: BehandlingHentOgPersisterService + + @Test + fun `skal finne en relaterte fagsaker per barn`() { + val søker = tilfeldigPerson() + val barn1 = tilfeldigPerson() + val søkerAktør = personidentService.hentOgLagreAktør(søker.aktør.aktivFødselsnummer(), true) + val barnAktør = personidentService.hentOgLagreAktør(barn1.aktør.aktivFødselsnummer(), true) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søker.aktør.aktivFødselsnummer()) + with(behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak))) { + val behandling = this + with(lagInitiellTilkjentYtelse(behandling, "utbetalingsoppdrag")) { + val andel = lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2023-03"), + no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType.ORDINÆR_BARNETRYGD, + 660, + behandling, + person = barn1, + aktør = barnAktør, + tilkjentYtelse = this, + ) + andelerTilkjentYtelse.add(andel) + tilkjentYtelseRepository.save(this) + } + avsluttOgLagreBehandling(behandling) + } + + val fagsak2 = fagsakService.hentEllerOpprettFagsakForPersonIdent(barn1.aktør.aktivFødselsnummer()) + with(behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak2))) { + val behandling = this + with(lagInitiellTilkjentYtelse(behandling, "utbetalingsoppdrag")) { + val andel = lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2023-03"), + no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType.ORDINÆR_BARNETRYGD, + 660, + behandling, + person = barn1, + aktør = barnAktør, + tilkjentYtelse = this, + ) + andelerTilkjentYtelse.add(andel) + tilkjentYtelseRepository.save(this) + } + avsluttOgLagreBehandling(behandling) + } + + val barnetrygdTilPensjon = pensjonService.hentBarnetrygd(søkerAktør.aktivFødselsnummer(), LocalDate.of(2023, 1, 1)) + assertThat(barnetrygdTilPensjon).hasSize(2) + } + + private fun avsluttOgLagreBehandling(behandling: Behandling) { + behandling.status = BehandlingStatus.AVSLUTTET + behandling.leggTilBehandlingStegTilstand(StegType.BEHANDLING_AVSLUTTET) + behandlingHentOgPersisterService.lagreEllerOppdater(behandling, false) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/SkatteetatenServiceIntegrationTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/SkatteetatenServiceIntegrationTest.kt new file mode 100644 index 000000000..6d192100b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/ekstern/skatteetaten/SkatteetatenServiceIntegrationTest.kt @@ -0,0 +1,749 @@ +package no.nav.familie.ba.sak.ekstern.skatteetaten + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.nesteBehandlingId +import no.nav.familie.ba.sak.common.nesteVedtakId +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdBarnetrygdClient +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.Personident +import no.nav.familie.ba.sak.kjerne.personident.PersonidentRepository +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPeriode +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPerioder +import no.nav.familie.eksterne.kontrakter.skatteetaten.SkatteetatenPersonerResponse +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.YearMonth +import java.time.format.DateTimeFormatter + +class SkatteetatenServiceIntegrationTest : AbstractSpringIntegrationTest() { + + @Autowired + lateinit var databaseCleanupService: DatabaseCleanupService + + @Autowired + lateinit var fagsakRepository: FagsakRepository + + @Autowired + lateinit var personidentService: PersonidentService + + @Autowired + lateinit var personidentRepository: PersonidentRepository + + @Autowired + lateinit var andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository + + @Autowired + lateinit var tilkjentYtelseRepository: TilkjentYtelseRepository + + @Autowired + lateinit var behandlingHentOgPersisterService: BehandlingHentOgPersisterService + + val infotrygdBarnetrygdClientMock = mockk() + + lateinit var skatteetatenService: SkatteetatenService + + @BeforeEach + fun cleanUp() { + databaseCleanupService.truncate() + } + + @BeforeAll + fun init() { + skatteetatenService = + SkatteetatenService( + infotrygdBarnetrygdClientMock, + fagsakRepository, + andelTilkjentYtelseRepository, + behandlingHentOgPersisterService, + ) + } + + data class PerioderTestData( + val fnr: String, + val aktør: Aktør, + val endretDato: LocalDateTime, + val perioder: List>, + ) + + @Test + fun `finnPerioderMedUtvidetBarnetrygd() skal return riktig data`() { + val duplicatedFnr = "00000000001" + val excludedFnr = "10000000004" + val duplicatedAktørId = tilAktør(duplicatedFnr) + val excludedAktørId = tilAktør(excludedFnr) + + // Result from ba-sak + val testDataBaSak = arrayOf( + // Excluded because of the vedtak is older + PerioderTestData( + fnr = duplicatedFnr, + aktør = duplicatedAktørId, + endretDato = LocalDateTime.of(2020, 11, 5, 12, 0), + perioder = listOf( + Triple( + LocalDateTime.of(2020, 9, 1, 12, 0), + LocalDateTime.of(2020, 10, 8, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ), + // Included + PerioderTestData( + fnr = duplicatedFnr, + aktør = duplicatedAktørId, + endretDato = LocalDateTime.of(2020, 11, 6, 12, 0), + perioder = listOf( + Triple( + LocalDateTime.of(2019, 9, 1, 12, 0), + LocalDateTime.of(2020, 7, 31, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + Triple( + LocalDateTime.of(2020, 8, 1, 12, 0), + LocalDateTime.of(2020, 12, 8, 12, 0), + SkatteetatenPeriode.Delingsprosent._50, + ), + ), + ), + // Excluded because the stonad period is earlier than the specified year + PerioderTestData( + fnr = "00000000002", + aktør = tilAktør("00000000002"), + endretDato = LocalDateTime.of(2020, 8, 5, 12, 0), + perioder = listOf( + Triple( + LocalDateTime.of(2019, 3, 1, 12, 0), + LocalDateTime.of(2019, 12, 31, 23, 59), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ), + // Excluded because the stonad period is later than the specified year + PerioderTestData( + fnr = "00000000003", + aktør = tilAktør("00000000003"), + endretDato = LocalDateTime.of(2020, 8, 5, 12, 0), + perioder = listOf( + Triple( + LocalDateTime.of(2021, 1, 1, 1, 0), + LocalDateTime.of(2022, 12, 31, 23, 59), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ), + // Excluded because the person ident is not in the provided list + PerioderTestData( + fnr = excludedFnr, + aktør = excludedAktørId, + endretDato = LocalDateTime.of(2020, 8, 5, 12, 0), + perioder = listOf( + Triple( + LocalDateTime.of(2020, 1, 1, 1, 0), + LocalDateTime.of(2022, 12, 31, 23, 59), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ), + ) + + // result from Infotrygd + val testDataInfotrygd = arrayOf( + // Excluded because the person ident can be found in ba-sak + PerioderTestData( + fnr = duplicatedFnr, + aktør = duplicatedAktørId, + endretDato = LocalDateTime.of(2020, 9, 5, 12, 0), + perioder = listOf( + Triple( + LocalDateTime.of(2020, 8, 1, 12, 0), + LocalDateTime.of(2020, 9, 8, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ), + // Included + PerioderTestData( + fnr = "00000000010", + aktør = tilAktør("00000000010"), + endretDato = LocalDateTime.of(2020, 8, 5, 12, 0), + perioder = listOf( + Triple( + LocalDateTime.of(2020, 3, 1, 12, 0), + LocalDateTime.of(2020, 4, 8, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ), + ) + + testDataBaSak.forEach { + lagerTilkjentYtelse(it) + } + + val result = testDataInfotrygd.flatMap { + listOf( + SkatteetatenPerioder( + it.fnr, + it.endretDato, + it.perioder.map { p -> + SkatteetatenPeriode( + fraMaaned = p.first.tilMaaned(), + tomMaaned = p.second?.tilMaaned(), + delingsprosent = p.third, + ) + }, + ), + ) + } + + every { + infotrygdBarnetrygdClientMock.hentPerioderMedUtvidetBarnetrygdForPersoner( + eq(listOf("00000000001", "00000000002", "00000000003", "00000000010")), + any(), + ) + } returns result + + val samletResultat = + skatteetatenService.finnPerioderMedUtvidetBarnetrygd( + testDataBaSak.filter { it.fnr != excludedFnr } + .map { it.fnr } + + testDataInfotrygd.map { it.fnr }, + "2020", + ) + + assertThat(samletResultat.brukere).hasSize(2) + assertThat(samletResultat.brukere.find { it.ident == duplicatedFnr }!!.perioder).hasSize(2) + assertThat( + samletResultat.brukere.find { it.ident == duplicatedFnr }!!.perioder.find { + it.fraMaaned == "2020-08" + }!!.delingsprosent, + ).isEqualTo( + SkatteetatenPeriode.Delingsprosent._50, + ) + assertThat( + samletResultat.brukere.find { it.ident == duplicatedFnr }!!.perioder.find { + it.tomMaaned == "2020-09" + }!!.delingsprosent, + ).isEqualTo( + SkatteetatenPeriode.Delingsprosent._0, + ) + assertThat(samletResultat.brukere.find { it.ident == testDataInfotrygd[1].fnr }!!.perioder).hasSize(1) + } + + @Test + fun `finnPerioderMedUtvidetBarnetrygd() skal slå sammen data fra infotrygd og ba-sak når overlappende periode`() { + val fnr = "00000000001" + val aktør = tilAktør(fnr) + + // Result from ba-sak + val testDataBaSak = arrayOf( + // Included + PerioderTestData( + fnr = fnr, + aktør = aktør, + endretDato = LocalDate.of(2022, 2, 6).atStartOfDay(), + perioder = listOf( + Triple( + LocalDateTime.of(2022, 3, 1, 12, 0), + LocalDateTime.of(2027, 7, 31, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ), + ) + + // result from Infotrygd + val testDataInfotrygd = arrayOf( + // Excluded because the person ident can be found in ba-sak + PerioderTestData( + fnr = fnr, + aktør = aktør, + endretDato = LocalDate.of(2020, 9, 5).atStartOfDay(), + perioder = listOf( + Triple( + LocalDateTime.of(2019, 9, 1, 12, 0), + LocalDateTime.of(2022, 2, 8, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ), + ) + + testDataBaSak.forEach { + lagerTilkjentYtelse(it) + } + + every { + infotrygdBarnetrygdClientMock.hentPerioderMedUtvidetBarnetrygdForPersoner( + eq(listOf("00000000001")), + any(), + ) + } returns testDataInfotrygd.flatMap { + listOf( + SkatteetatenPerioder( + it.fnr, + it.endretDato, + it.perioder.map { p -> + SkatteetatenPeriode( + fraMaaned = p.first.tilMaaned(), + tomMaaned = p.second?.tilMaaned(), + delingsprosent = p.third, + ) + }, + ), + ) + } + + val resultat = skatteetatenService.finnPerioderMedUtvidetBarnetrygd(listOf((fnr)), "2022") + + assertThat(resultat.brukere).hasSize(1) + assertThat(resultat.brukere.first().perioder).hasSize(1) + assertThat(resultat.brukere.first().perioder.first().fraMaaned).isEqualTo("2019-09") + assertThat(resultat.brukere.first().perioder.first().tomMaaned).isEqualTo("2027-07") + assertThat(resultat.brukere.first().perioder.first().delingsprosent).isEqualTo(SkatteetatenPeriode.Delingsprosent._0) + assertThat(resultat.brukere.first().ident).isEqualTo(fnr) + assertThat(resultat.brukere.first().sisteVedtakPaaIdent).isEqualTo(LocalDate.of(2022, 2, 6).atStartOfDay()) + } + + @Test + fun `finnPerioderMedUtvidetBarnetrygd() skal slå sammen data fra infotrygd og ba-sak når infotrygdperioden slutter med null fordi den ikke er ferdig opphørt`() { + val fnr = "00000000001" + val aktør = tilAktør(fnr) + + // Result from ba-sak + val testDataBaSak = arrayOf( + // Included + PerioderTestData( + fnr = fnr, + aktør = aktør, + endretDato = LocalDate.of(2022, 2, 6).atStartOfDay(), + perioder = listOf( + Triple( + LocalDateTime.of(2022, 3, 1, 12, 0), + LocalDateTime.of(2027, 7, 31, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ), + ) + + // result from Infotrygd + val testDataInfotrygd = arrayOf( + // Excluded because the person ident can be found in ba-sak + PerioderTestData( + fnr = fnr, + aktør = aktør, + endretDato = LocalDate.of(2020, 9, 5).atStartOfDay(), + perioder = listOf( + Triple( + LocalDateTime.of(2019, 9, 1, 12, 0), + null, + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ), + ) + + testDataBaSak.forEach { + lagerTilkjentYtelse(it) + } + + every { + infotrygdBarnetrygdClientMock.hentPerioderMedUtvidetBarnetrygdForPersoner( + eq(listOf("00000000001")), + any(), + ) + } returns testDataInfotrygd.flatMap { + listOf( + SkatteetatenPerioder( + it.fnr, + it.endretDato, + it.perioder.map { p -> + SkatteetatenPeriode( + fraMaaned = p.first.tilMaaned(), + tomMaaned = p.second?.tilMaaned(), + delingsprosent = p.third, + ) + }, + ), + ) + } + + val resultat = skatteetatenService.finnPerioderMedUtvidetBarnetrygd(listOf((fnr)), "2022") + + assertThat(resultat.brukere).hasSize(1) + assertThat(resultat.brukere.first().perioder).hasSize(1) + assertThat(resultat.brukere.first().perioder.first().fraMaaned).isEqualTo("2019-09") + assertThat(resultat.brukere.first().perioder.first().tomMaaned).isEqualTo("2027-07") + assertThat(resultat.brukere.first().perioder.first().delingsprosent).isEqualTo(SkatteetatenPeriode.Delingsprosent._0) + assertThat(resultat.brukere.first().ident).isEqualTo(fnr) + assertThat(resultat.brukere.first().sisteVedtakPaaIdent).isEqualTo(LocalDate.of(2022, 2, 6).atStartOfDay()) + } + + @Test + fun `finnPerioderMedUtvidetBarnetrygd() skal slå sammen data fra infotrygd og ba-sak når overlappende periode mellom ba-sak og infotrygd, noe som typisk skjer ved endret migreringsdato`() { + val fnr = "00000000001" + val aktør = tilAktør(fnr) + + // Result from ba-sak + val testDataBaSak = arrayOf( + // Included + PerioderTestData( + fnr = fnr, + aktør = aktør, + endretDato = LocalDate.of(2022, 2, 6).atStartOfDay(), + perioder = listOf( + Triple( + LocalDateTime.of(2021, 9, 1, 12, 0), + LocalDateTime.of(2027, 7, 31, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ), + ) + + // result from Infotrygd + val testDataInfotrygd = arrayOf( + // Excluded because the person ident can be found in ba-sak + PerioderTestData( + fnr = fnr, + aktør = aktør, + endretDato = LocalDate.of(2020, 9, 5).atStartOfDay(), + perioder = listOf( + Triple( + LocalDateTime.of(2019, 9, 1, 12, 0), + LocalDateTime.of(2022, 3, 1, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ), + ) + + testDataBaSak.forEach { + lagerTilkjentYtelse(it) + } + + every { + infotrygdBarnetrygdClientMock.hentPerioderMedUtvidetBarnetrygdForPersoner( + eq(listOf("00000000001")), + any(), + ) + } returns testDataInfotrygd.flatMap { + listOf( + SkatteetatenPerioder( + it.fnr, + it.endretDato, + it.perioder.map { p -> + SkatteetatenPeriode( + fraMaaned = p.first.tilMaaned(), + tomMaaned = p.second?.tilMaaned(), + delingsprosent = p.third, + ) + }, + ), + ) + } + + val resultat = skatteetatenService.finnPerioderMedUtvidetBarnetrygd(listOf((fnr)), "2022") + + assertThat(resultat.brukere).hasSize(1) + assertThat(resultat.brukere.first().perioder).hasSize(1) + assertThat(resultat.brukere.first().perioder.first().fraMaaned).isEqualTo("2019-09") + assertThat(resultat.brukere.first().perioder.first().tomMaaned).isEqualTo("2027-07") + assertThat(resultat.brukere.first().perioder.first().delingsprosent).isEqualTo(SkatteetatenPeriode.Delingsprosent._0) + assertThat(resultat.brukere.first().ident).isEqualTo(fnr) + assertThat(resultat.brukere.first().sisteVedtakPaaIdent).isEqualTo(LocalDate.of(2022, 2, 6).atStartOfDay()) + } + + @Test + fun `finnPerioderMedUtvidetBarnetrygd() skal slå sammen perioder basert på prosent`() { + val fnr = "00000000001" + val aktørId = tilAktør("00000000001") + val excludedFnr = "10000000004" + + // Result from ba-sak + val testDataBaSak = arrayOf( + PerioderTestData( + fnr = fnr, + aktør = aktørId, + endretDato = LocalDateTime.of(2020, 11, 6, 12, 0), + perioder = listOf( + Triple( + LocalDateTime.of(2019, 9, 1, 12, 0), + LocalDateTime.of(2020, 2, 11, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + Triple( + LocalDateTime.of(2020, 3, 1, 12, 0), + LocalDateTime.of(2020, 4, 8, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + Triple( + LocalDateTime.of(2020, 5, 1, 12, 0), + LocalDateTime.of(2020, 6, 8, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + Triple( + LocalDateTime.of(2020, 7, 1, 12, 0), + LocalDateTime.of(2020, 8, 8, 12, 0), + SkatteetatenPeriode.Delingsprosent._50, + ), + Triple( + LocalDateTime.of(2020, 9, 1, 12, 0), + LocalDateTime.of(2020, 11, 8, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ), + ) + + testDataBaSak.forEach { + every { + infotrygdBarnetrygdClientMock.hentPerioderMedUtvidetBarnetrygdForPersoner( + eq(listOf(it.fnr)), + any(), + ) + } returns emptyList() + + lagerTilkjentYtelse(it) + } + + val samletResultat = + skatteetatenService.finnPerioderMedUtvidetBarnetrygd( + testDataBaSak.filter { it.fnr != excludedFnr } + .map { it.fnr }, + "2020", + ) + + assertThat(samletResultat.brukere).hasSize(1) + assertThat(samletResultat.brukere.find { it.ident == fnr }!!.perioder).hasSize(3) + val sortertePerioder = samletResultat.brukere.find { it.ident == fnr }!!.perioder.sortedBy { it.fraMaaned } + assertThat(sortertePerioder[0].delingsprosent).isEqualTo( + SkatteetatenPeriode.Delingsprosent._0, + ) + assertThat(sortertePerioder[0].fraMaaned).isEqualTo( + "2019-09", + ) + assertThat(sortertePerioder[0].tomMaaned).isEqualTo( + "2020-06", + ) + + assertThat(sortertePerioder[1].delingsprosent).isEqualTo( + SkatteetatenPeriode.Delingsprosent._50, + ) + assertThat(sortertePerioder[1].fraMaaned).isEqualTo("2020-07") + assertThat(sortertePerioder[1].tomMaaned).isEqualTo( + "2020-08", + ) + + assertThat(sortertePerioder[2].delingsprosent).isEqualTo( + SkatteetatenPeriode.Delingsprosent._0, + ) + assertThat(sortertePerioder[2].fraMaaned).isEqualTo( + "2020-09", + ) + assertThat(sortertePerioder[2].tomMaaned).isEqualTo( + "2020-11", + ) + } + + @Test + fun `finnPerioderMedUtvidetBarnetrygd() skal IKKE finne perioder for år 2021 etter en revurdering med ny stønadTom 2020`() { + val fnr = "00000000001" + + // Result from ba-sak + val testDataBaSak = + PerioderTestData( + fnr = fnr, + aktør = tilAktør(fnr), + endretDato = LocalDateTime.of(2020, 11, 6, 12, 0), + perioder = listOf( + Triple( + LocalDateTime.of(2019, 9, 1, 12, 0), + LocalDateTime.of(2029, 7, 31, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ) + + lagerTilkjentYtelse(testDataBaSak) + + every { + infotrygdBarnetrygdClientMock.hentPerioderMedUtvidetBarnetrygdForPersoner( + eq(listOf(fnr)), + any(), + ) + } returns emptyList() + + var resultat = + skatteetatenService.finnPerioderMedUtvidetBarnetrygd( + listOf(testDataBaSak.fnr), + "2021", + ) + + assertThat(resultat.brukere).hasSize(1) + + lagRevurderingMedNyStonadTom(testDataBaSak, YearMonth.of(2020, 12)) + + resultat = + skatteetatenService.finnPerioderMedUtvidetBarnetrygd( + listOf(testDataBaSak.fnr), + "2021", + ) + + assertThat(resultat.brukere).hasSize(0) + } + + @Test + fun `finnPersonerMedUtvidetBarnetrygd() skal IKKE ta med historisk ident som en ekstra person`() { + val fnr = "00000000002" + val historiskIdent = "00000000001" + + // Result from ba-sak + val testDataBaSak = + PerioderTestData( + fnr = fnr, + aktør = tilAktør(fnr).also { it.personidenter.add(Personident(historiskIdent, aktiv = false, aktør = it)) }, + endretDato = LocalDateTime.of(2020, 11, 6, 12, 0), + perioder = listOf( + Triple( + LocalDateTime.of(2019, 9, 1, 12, 0), + LocalDateTime.of(2029, 7, 31, 12, 0), + SkatteetatenPeriode.Delingsprosent._0, + ), + ), + ) + + lagerTilkjentYtelse(testDataBaSak) + + every { + infotrygdBarnetrygdClientMock.hentPersonerMedUtvidetBarnetrygd( + any(), + ) + } returns SkatteetatenPersonerResponse() + + val resultat = + skatteetatenService.finnPersonerMedUtvidetBarnetrygd( + "2021", + ) + + assertThat(resultat.brukere).hasSize(1) + assertThat(resultat.brukere.first().ident == fnr) + } + + fun lagerTilkjentYtelse(perioderTestData: PerioderTestData) { + val fødselsnummer = perioderTestData.aktør.aktivFødselsnummer() + val aktør = perioderTestData.aktør + personidentService.hentOgLagreAktør(fødselsnummer, true).also { + personidentRepository.saveAll(aktør.personidenter.filter { !it.aktiv }) + } + + val fagsak = fagsakRepository.finnFagsakForAktør(aktør) ?: fagsakRepository.saveAndFlush(Fagsak(aktør = aktør)) + + val behandling = Behandling( + fagsak = fagsak, + type = BehandlingType.FØRSTEGANGSBEHANDLING, + opprettetÅrsak = BehandlingÅrsak.MIGRERING, + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.UTVIDET, + status = BehandlingStatus.AVSLUTTET, + aktiv = false, + ) + behandlingHentOgPersisterService.lagreOgFlush(behandling) + + val ty = TilkjentYtelse( + behandling = behandling, + opprettetDato = perioderTestData.endretDato.toLocalDate(), + endretDato = perioderTestData.endretDato.toLocalDate(), + utbetalingsoppdrag = "utbetalt", + ).also { + it.andelerTilkjentYtelse.addAll( + perioderTestData.perioder.map { p -> + AndelTilkjentYtelse( + behandlingId = it.behandling.id, + tilkjentYtelse = it, + aktør = perioderTestData.aktør, + kalkulertUtbetalingsbeløp = 1000, + nasjonaltPeriodebeløp = 1000, + stønadFom = YearMonth.of(p.first.year, p.first.month), + stønadTom = YearMonth.of(p.second!!.year, p.second!!.month), + type = YtelseType.UTVIDET_BARNETRYGD, + sats = 1, + prosent = p.third.tilBigDecimal(), + ) + }.toMutableSet(), + ) + } + tilkjentYtelseRepository.saveAndFlush(ty) + } + + fun lagRevurderingMedNyStonadTom(perioderTestData: PerioderTestData, stønadTom: YearMonth) { + val fødselsnummer = perioderTestData.aktør.aktivFødselsnummer() + val aktør = personidentService.hentOgLagreAktør(fødselsnummer, false) + + val fagsak = fagsakRepository.finnFagsakForAktør(aktør)!! + + val behandling = behandlingHentOgPersisterService.lagreOgFlush( + Behandling( + id = nesteBehandlingId(), + fagsak = fagsak, + type = BehandlingType.REVURDERING, + opprettetÅrsak = BehandlingÅrsak.NYE_OPPLYSNINGER, + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.UTVIDET, + ), + ) + + val ty = TilkjentYtelse( + id = nesteVedtakId(), + behandling = behandling, + opprettetDato = perioderTestData.endretDato.toLocalDate(), + endretDato = perioderTestData.endretDato.toLocalDate(), + utbetalingsoppdrag = "utbetalt", + ).also { + it.andelerTilkjentYtelse.addAll( + perioderTestData.perioder.map { p -> + AndelTilkjentYtelse( + behandlingId = it.behandling.id, + tilkjentYtelse = it, + aktør = perioderTestData.aktør, + kalkulertUtbetalingsbeløp = 1000, + nasjonaltPeriodebeløp = 1000, + stønadFom = YearMonth.of(p.first.year, p.first.month), + stønadTom = stønadTom, + type = YtelseType.UTVIDET_BARNETRYGD, + sats = 1, + prosent = p.third.tilBigDecimal(), + ) + }.toMutableSet(), + ) + } + tilkjentYtelseRepository.saveAndFlush(ty) + } +} + +fun LocalDateTime.tilMaaned(): String = this.format(DateTimeFormatter.ofPattern("yyyy-MM")) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/IntergrasjonTjenesteTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/IntergrasjonTjenesteTest.kt new file mode 100644 index 000000000..1175dca30 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/IntergrasjonTjenesteTest.kt @@ -0,0 +1,528 @@ +package no.nav.familie.ba.sak.integrasjoner + +import com.github.tomakehurst.wiremock.client.WireMock.aResponse +import com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor +import com.github.tomakehurst.wiremock.client.WireMock.anyUrl +import com.github.tomakehurst.wiremock.client.WireMock.containing +import com.github.tomakehurst.wiremock.client.WireMock.equalTo +import com.github.tomakehurst.wiremock.client.WireMock.equalToJson +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor +import com.github.tomakehurst.wiremock.client.WireMock.okJson +import com.github.tomakehurst.wiremock.client.WireMock.patch +import com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor +import com.github.tomakehurst.wiremock.client.WireMock.post +import com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor +import com.github.tomakehurst.wiremock.client.WireMock.status +import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo +import no.nav.familie.ba.sak.common.MDCOperations +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient.Companion.VEDTAK_VEDLEGG_FILNAVN +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient.Companion.VEDTAK_VEDLEGG_TITTEL +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient.Companion.hentVedlegg +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonException +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Ansettelsesperiode +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Arbeidsfordelingsenhet +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Arbeidsforhold +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Arbeidsgiver +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.ArbeidsgiverType +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Arbeidstaker +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Periode +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Skyggesak +import no.nav.familie.ba.sak.integrasjoner.journalføring.UtgåendeJournalføringService +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.task.DistribuerDokumentDTO +import no.nav.familie.http.client.RessursException +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.Ressurs.Companion.failure +import no.nav.familie.kontrakter.felles.Ressurs.Companion.success +import no.nav.familie.kontrakter.felles.Tema +import no.nav.familie.kontrakter.felles.dokarkiv.ArkiverDokumentResponse +import no.nav.familie.kontrakter.felles.dokarkiv.Dokumenttype +import no.nav.familie.kontrakter.felles.dokarkiv.v2.ArkiverDokumentRequest +import no.nav.familie.kontrakter.felles.dokarkiv.v2.Dokument +import no.nav.familie.kontrakter.felles.dokarkiv.v2.Filtype +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveRequest +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveResponseDto +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import no.nav.familie.kontrakter.felles.oppgave.OppgaveResponse +import no.nav.familie.log.NavHttpHeaders +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.slf4j.MDC +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.RestOperations +import java.net.URI +import java.time.LocalDate +import kotlin.random.Random + +class IntergrasjonTjenesteTest : AbstractSpringIntegrationTest() { + + @Autowired + @Qualifier("jwtBearer") + lateinit var restOperations: RestOperations + + lateinit var integrasjonClient: IntegrasjonClient + lateinit var utgåendeJournalføringService: UtgåendeJournalføringService + + @BeforeEach + fun setUp() { + integrasjonClient = IntegrasjonClient( + URI.create(wireMockServer.baseUrl() + "/api"), + restOperations, + ) + utgåendeJournalføringService = UtgåendeJournalføringService( + integrasjonClient = integrasjonClient, + ) + } + + @AfterEach + fun clearTest() { + MDC.clear() + wireMockServer.resetAll() + } + + @Test + @Tag("integration") + fun `Opprett oppgave skal returnere oppgave id`() { + MDC.put("callId", "opprettOppgave") + wireMockServer.stubFor( + post("/api/oppgave/opprett").willReturn( + okJson(objectMapper.writeValueAsString(success(OppgaveResponse(oppgaveId = 1234)))), + ), + ) + + val request = lagTestOppgave() + + val opprettOppgaveResponse = integrasjonClient.opprettOppgave(request).oppgaveId.toString() + + assertThat(opprettOppgaveResponse).isEqualTo("1234") + wireMockServer.verify( + anyRequestedFor(anyUrl()) + .withHeader(NavHttpHeaders.NAV_CALL_ID.asString(), equalTo("opprettOppgave")) + .withHeader(NavHttpHeaders.NAV_CONSUMER_ID.asString(), equalTo("srvfamilie-ba-sak")) + .withRequestBody(equalToJson(objectMapper.writeValueAsString(request))), + ) + } + + @Test + @Tag("integration") + fun `Opprett oppgave skal kaste feil hvis response er ugyldig`() { + wireMockServer.stubFor( + post("/api/oppgave/opprett").willReturn( + aResponse() + .withStatus(500) + .withBody(objectMapper.writeValueAsString(failure("test"))), + ), + ) + + val feil = assertThrows { integrasjonClient.opprettOppgave(lagTestOppgave()) } + assertEquals("test", feil.ressurs.melding) + } + + @Test + @Tag("integration") + fun `hentOppgaver skal returnere en liste av oppgaver og antallet oppgaver`() { + val oppgave = Oppgave() + wireMockServer.stubFor( + post("/api/oppgave/v4").willReturn( + okJson( + objectMapper.writeValueAsString( + success(FinnOppgaveResponseDto(1, listOf(oppgave))), + ), + ), + ), + ) + + val oppgaverOgAntall = integrasjonClient.hentOppgaver(FinnOppgaveRequest(tema = Tema.BAR)) + assertThat(oppgaverOgAntall.oppgaver).hasSize(1) + } + + @Test + @Tag("integration") + fun `Journalfør vedtaksbrev skal journalføre dokument, returnere 201 og journalpostId`() { + MDC.put("callId", "journalfør") + wireMockServer.stubFor( + post("/api/arkiv/v4") + .withHeader("Accept", containing("json")) + .willReturn( + aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(journalpostOkResponse())), + ), + ) + + val vedtak = lagVedtak(lagBehandling()) + vedtak.stønadBrevPdF = mockPdf + + val journalPostId = + utgåendeJournalføringService.journalførDokument( + fnr = MOCK_FNR, + fagsakId = vedtak.behandling.fagsak.id.toString(), + brev = listOf( + Dokument( + dokument = mockPdf, + filtype = Filtype.PDFA, + dokumenttype = Dokumenttype.BARNETRYGD_VEDTAK_INNVILGELSE, + ), + ), + journalførendeEnhet = "1", + vedlegg = listOf( + Dokument( + dokument = hentVedlegg(VEDTAK_VEDLEGG_FILNAVN)!!, + filtype = Filtype.PDFA, + dokumenttype = Dokumenttype.BARNETRYGD_VEDLEGG, + tittel = VEDTAK_VEDLEGG_TITTEL, + ), + ), + behandlingId = vedtak.behandling.id, + ) + + assertThat(journalPostId).isEqualTo(MOCK_JOURNALPOST_FOR_VEDTAK_ID) + wireMockServer.verify( + anyRequestedFor(anyUrl()) + .withHeader(NavHttpHeaders.NAV_CALL_ID.asString(), equalTo("journalfør")) + .withHeader(NavHttpHeaders.NAV_CONSUMER_ID.asString(), equalTo("srvfamilie-ba-sak")) + .withRequestBody( + equalToJson( + objectMapper.writeValueAsString( + forventetRequestArkiverDokument( + fagsakId = vedtak.behandling.fagsak.id, + behandlingId = vedtak.behandling.id, + ), + ), + ), + ), + ) + } + + @Test + @Tag("integration") + fun `distribuerVedtaksbrev returnerer normalt ved vellykket integrasjonskall`() { + MDC.put("callId", "distribuerVedtaksbrev") + wireMockServer.stubFor( + post("/api/dist/v1") + .withHeader("Accept", containing("json")) + .willReturn(okJson(objectMapper.writeValueAsString(success("1234567")))), + ) + + assertDoesNotThrow { integrasjonClient.distribuerBrev(lagDistribuerDokumentDTO()) } + wireMockServer.verify( + postRequestedFor(anyUrl()) + .withHeader(NavHttpHeaders.NAV_CALL_ID.asString(), equalTo("distribuerVedtaksbrev")) + .withHeader(NavHttpHeaders.NAV_CONSUMER_ID.asString(), equalTo("srvfamilie-ba-sak")) + .withRequestBody( + equalToJson( + "{\"journalpostId\":\"123456789\"," + + "\"bestillendeFagsystem\":\"BA\"," + + "\"dokumentProdApp\":\"FAMILIE_BA_SAK\"," + + "\"distribusjonstype\" : \"VIKTIG\"," + + "\"distribusjonstidspunkt\" : \"KJERNETID\"}", + false, + true, + ), + ), + ) + } + + @Test + @Tag("integration") + fun `distribuerVedtaksbrev kaster exception hvis integrasjoner gir blank response`() { + wireMockServer.stubFor( + post("/api/dist/v1") + .withHeader("Accept", containing("json")) + .willReturn(okJson(objectMapper.writeValueAsString(success("")))), + ) + + assertThrows { integrasjonClient.distribuerBrev(lagDistribuerDokumentDTO()) } + } + + @Test + @Tag("integration") + fun `distribuerVedtaksbrev kaster exception hvis integrasjoner gir failure response`() { + wireMockServer.stubFor( + post("/api/dist/v1") + .withHeader("Accept", containing("json")) + .willReturn(okJson(objectMapper.writeValueAsString(failure("")))), + ) + + val feil = assertThrows { integrasjonClient.distribuerBrev(lagDistribuerDokumentDTO()) } + assertTrue(feil.message?.contains("dokdist") == true) + } + + @Test + @Tag("integration") + fun `distribuerVedtaksbrev kaster exception hvis responsekoden ikke er 2xx`() { + wireMockServer.stubFor( + post("/api/dist/v1") + .withHeader("Accept", containing("json")) + .willReturn( + aResponse() + .withStatus(400) + .withHeader("Content-Type", "application/json"), + ), + ) + + assertThrows { integrasjonClient.distribuerBrev(lagDistribuerDokumentDTO()) } + } + + @Test + @Tag("integration") + fun `Ferdigstill oppgave returnerer OK`() { + MDC.put("callId", "ferdigstillOppgave") + wireMockServer.stubFor( + patch(urlEqualTo("/api/oppgave/123/ferdigstill")) + .withHeader("Accept", containing("json")) + .willReturn(okJson(objectMapper.writeValueAsString(success(OppgaveResponse(1))))), + ) + + integrasjonClient.ferdigstillOppgave(123) + + wireMockServer.verify( + patchRequestedFor(urlEqualTo("/api/oppgave/123/ferdigstill")) + .withHeader(NavHttpHeaders.NAV_CALL_ID.asString(), equalTo("ferdigstillOppgave")) + .withHeader(NavHttpHeaders.NAV_CONSUMER_ID.asString(), equalTo("srvfamilie-ba-sak")), + ) + } + + @Test + @Tag("integration") + fun `Ferdigstill oppgave returnerer feil `() { + MDC.put("callId", "ferdigstillOppgave") + wireMockServer.stubFor( + patch(urlEqualTo("/api/oppgave/123/ferdigstill")) + .withHeader("Accept", containing("json")) + .willReturn( + aResponse() + .withStatus(400) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(failure("test"))), + ), + ) + + val feil = + assertThrows { integrasjonClient.ferdigstillOppgave(123) } + assertEquals("test", feil.ressurs.melding) + } + + @Test + @Tag("integration") + fun `hentBehandlendeEnhet returnerer OK`() { + wireMockServer.stubFor( + post("/api/arbeidsfordeling/enhet/BAR") + .withHeader("Accept", containing("json")) + .willReturn( + okJson( + objectMapper.writeValueAsString( + success( + listOf( + Arbeidsfordelingsenhet("2", "foo"), + ), + ), + ), + ), + ), + ) + + val enhet = integrasjonClient.hentBehandlendeEnhet("1") + assertThat(enhet).isNotEmpty + assertThat(enhet.first().enhetId).isEqualTo("2") + } + + @Test + @Tag("integration") + fun `finnOppgaveMedId returnerer OK`() { + val oppgaveId = 1234L + wireMockServer.stubFor( + get("/api/oppgave/$oppgaveId").willReturn( + okJson( + objectMapper.writeValueAsString( + success( + lagTestOppgaveDTO( + oppgaveId, + ), + ), + ), + ), + ), + ) + + val oppgave = integrasjonClient.finnOppgaveMedId(oppgaveId) + assertThat(oppgave.id).isEqualTo(oppgaveId) + + wireMockServer.verify(getRequestedFor(urlEqualTo("/api/oppgave/$oppgaveId"))) + } + + @Test + @Tag("integration") + fun `hentJournalpost returnerer OK`() { + val journalpostId = "1234" + val fnr = randomFnr() + wireMockServer.stubFor( + get("/api/journalpost?journalpostId=$journalpostId").willReturn( + okJson( + objectMapper.writeValueAsString( + success( + lagTestJournalpost(fnr, journalpostId), + ), + ), + ), + ), + ) + + val oppgave = integrasjonClient.hentJournalpost(journalpostId) + assertThat(oppgave).isNotNull + assertThat(oppgave.journalpostId).isEqualTo(journalpostId) + assertThat(oppgave.bruker?.id).isEqualTo(fnr) + + wireMockServer.verify(getRequestedFor(urlEqualTo("/api/journalpost?journalpostId=$journalpostId"))) + } + + @Test + @Tag("integration") + fun `skal hente arbeidsforhold for person`() { + val fnr = randomFnr() + + val arbeidsforhold = listOf( + Arbeidsforhold( + navArbeidsforholdId = Random.nextLong(), + arbeidstaker = Arbeidstaker("Person", fnr), + arbeidsgiver = Arbeidsgiver(ArbeidsgiverType.Organisasjon, "998877665"), + ansettelsesperiode = Ansettelsesperiode(Periode(fom = LocalDate.now().minusYears(1))), + ), + ) + + wireMockServer.stubFor( + post("/api/aareg/arbeidsforhold").willReturn( + okJson( + objectMapper.writeValueAsString( + success( + arbeidsforhold, + ), + ), + ), + ), + ) + + val response = integrasjonClient.hentArbeidsforhold(fnr, LocalDate.now()) + + assertThat(response).hasSize(1) + assertThat(response.first().arbeidstaker?.offentligIdent).isEqualTo(fnr) + assertThat(response.first().arbeidsgiver?.organisasjonsnummer).isEqualTo("998877665") + assertThat(response.first().ansettelsesperiode?.periode?.fom).isEqualTo(LocalDate.now().minusYears(1)) + } + + @Test + @Tag("integration") + fun `skal kaste integrasjonsfeil mot arbeidsforhold`() { + val fnr = randomFnr() + + wireMockServer.stubFor(post("/api/aareg/arbeidsforhold").willReturn(status(500))) + + val feil = assertThrows { integrasjonClient.hentArbeidsforhold(fnr, LocalDate.now()) } + assertTrue(feil.message?.contains("aareg") == true) + } + + @Test + @Tag("integration") + fun `skal opprette skyggesak for Sak`() { + val aktørId = randomAktør() + + wireMockServer.stubFor(post("/api/skyggesak/v1").willReturn(okJson(objectMapper.writeValueAsString(success(null))))) + + integrasjonClient.opprettSkyggesak(aktørId, MOCK_FAGSAK_ID.toLong()) + + wireMockServer.verify( + postRequestedFor(urlEqualTo("/api/skyggesak/v1")) + .withRequestBody( + equalToJson( + objectMapper.writeValueAsString( + Skyggesak( + aktoerId = aktørId.aktørId, + MOCK_FAGSAK_ID, + "BAR", + "BA", + ), + ), + ), + ), + ) + } + + @Test + @Tag("integration") + fun `skal kaste integrasjonsfeil ved oppretting av skyggesak`() { + val aktørId = randomAktør() + + wireMockServer.stubFor(post("/api/skyggesak/v1").willReturn(status(500))) + + val feil = + assertThrows { integrasjonClient.opprettSkyggesak(aktørId, MOCK_FAGSAK_ID.toLong()) } + assertTrue(feil.message?.contains("skyggesak") == true) + } + + private fun journalpostOkResponse(): Ressurs { + return success(ArkiverDokumentResponse(MOCK_JOURNALPOST_FOR_VEDTAK_ID, true)) + } + + private fun forventetRequestArkiverDokument(fagsakId: Long, behandlingId: Long): ArkiverDokumentRequest { + val vedleggPdf = hentVedlegg(VEDTAK_VEDLEGG_FILNAVN) + val brev = listOf( + Dokument( + dokument = mockPdf, + filtype = Filtype.PDFA, + dokumenttype = Dokumenttype.BARNETRYGD_VEDTAK_INNVILGELSE, + ), + ) + val vedlegg = listOf( + Dokument( + dokument = vedleggPdf!!, + filtype = Filtype.PDFA, + dokumenttype = Dokumenttype.BARNETRYGD_VEDLEGG, + tittel = VEDTAK_VEDLEGG_TITTEL, + ), + ) + + return ArkiverDokumentRequest( + fnr = MOCK_FNR, + forsøkFerdigstill = true, + fagsakId = fagsakId.toString(), + journalførendeEnhet = "1", + hoveddokumentvarianter = brev, + vedleggsdokumenter = vedlegg, + eksternReferanseId = "${fagsakId}_${behandlingId}_${MDCOperations.getCallId()}", + ) + } + + private fun lagDistribuerDokumentDTO() = DistribuerDokumentDTO( + journalpostId = "123456789", + behandlingId = 1L, + brevmal = Brevmal.VARSEL_OM_REVURDERING, + personEllerInstitusjonIdent = "test", + erManueltSendt = true, + ) + + companion object { + + const val MOCK_JOURNALPOST_FOR_VEDTAK_ID = "453491843" + const val MOCK_FNR = "12345678910" + val mockPdf = "mock data".toByteArray() + const val MOCK_FAGSAK_ID = "140258931" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdBarnetrygdClientTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdBarnetrygdClientTest.kt new file mode 100644 index 000000000..f6cfe55fe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdBarnetrygdClientTest.kt @@ -0,0 +1,182 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd + +import com.github.tomakehurst.wiremock.client.WireMock.aResponse +import com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor +import com.github.tomakehurst.wiremock.client.WireMock.equalToJson +import com.github.tomakehurst.wiremock.client.WireMock.okJson +import com.github.tomakehurst.wiremock.client.WireMock.post +import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.kontrakter.ba.infotrygd.InfotrygdSøkRequest +import no.nav.familie.kontrakter.ba.infotrygd.InfotrygdSøkResponse +import no.nav.familie.kontrakter.ba.infotrygd.Sak +import no.nav.familie.kontrakter.ba.infotrygd.Stønad +import no.nav.familie.kontrakter.felles.objectMapper +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.core.env.Environment +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.RestOperations +import java.net.URI + +class InfotrygdBarnetrygdClientTest : AbstractSpringIntegrationTest() { + + val løpendeBarnetrygdURL = "/api/infotrygd/barnetrygd/lopende-barnetrygd" + val sakerURL = "/api/infotrygd/barnetrygd/saker" + val stønaderURL = "/api/infotrygd/barnetrygd/stonad?historikk=false" + val brevURL = "/api/infotrygd/barnetrygd/brev" + + @Autowired + @Qualifier("jwtBearer") + lateinit var restOperations: RestOperations + + @Autowired + lateinit var environment: Environment + + lateinit var client: InfotrygdBarnetrygdClient + + @BeforeEach + fun setUp() { + client = InfotrygdBarnetrygdClient( + URI.create(wireMockServer.baseUrl() + "/api"), + restOperations, + ) + } + + @AfterEach + fun clearTest() { + wireMockServer.resetAll() + } + + @Test + fun `Skal lage InfotrygdBarnetrygdRequest basert på lister med fnr og barns fødselsnummer`() { + wireMockServer.stubFor( + post(løpendeBarnetrygdURL).willReturn( + okJson( + objectMapper.writeValueAsString( + InfotrygdLøpendeBarnetrygdResponse(false), + ), + ), + ), + ) + wireMockServer.stubFor( + post(sakerURL).willReturn( + okJson( + objectMapper.writeValueAsString( + InfotrygdSøkResponse(listOf(Sak(status = "IP")), emptyList()), + ), + ), + ), + ) + wireMockServer.stubFor( + post(stønaderURL).willReturn( + okJson( + objectMapper.writeValueAsString( + InfotrygdSøkResponse(listOf(Stønad()), emptyList()), + ), + ), + ), + ) + + val søkersIdenter = ClientMocks.søkerFnr.toList() + val barnasIdenter = ClientMocks.barnFnr.toList() + val infotrygdSøkRequest = InfotrygdSøkRequest(søkersIdenter, barnasIdenter) + + val finnesIkkeHosInfotrygd = client.harLøpendeSakIInfotrygd(søkersIdenter, barnasIdenter) + val hentsakerResponse = client.hentSaker(søkersIdenter, barnasIdenter) + val hentstønaderResponse = client.hentStønader(søkersIdenter, barnasIdenter) + + wireMockServer.verify( + anyRequestedFor(urlEqualTo(løpendeBarnetrygdURL)).withRequestBody( + equalToJson( + objectMapper.writeValueAsString( + infotrygdSøkRequest, + ), + ), + ), + ) + wireMockServer.verify( + anyRequestedFor(urlEqualTo(sakerURL)).withRequestBody( + equalToJson( + objectMapper.writeValueAsString( + infotrygdSøkRequest, + ), + ), + ), + ) + wireMockServer.verify( + anyRequestedFor(urlEqualTo(stønaderURL)).withRequestBody( + equalToJson( + objectMapper.writeValueAsString( + infotrygdSøkRequest, + ), + ), + ), + ) + Assertions.assertEquals(false, finnesIkkeHosInfotrygd) + Assertions.assertEquals(hentsakerResponse.bruker[0].status, "IP") + Assertions.assertEquals(hentstønaderResponse.bruker.size, 1) + } + + @Test + fun `Skal lage InfotrygdBarnetrygdRequest basert på lister med fnr`() { + wireMockServer.stubFor( + post(løpendeBarnetrygdURL).willReturn( + okJson( + objectMapper.writeValueAsString( + InfotrygdLøpendeBarnetrygdResponse(false), + ), + ), + ), + ) + + val søkersIdenter = ClientMocks.søkerFnr.toList() + val infotrygdSøkRequest = InfotrygdSøkRequest(søkersIdenter, emptyList()) + + val finnesIkkeHosInfotrygd = client.harLøpendeSakIInfotrygd(søkersIdenter, emptyList()) + + wireMockServer.verify( + anyRequestedFor(urlEqualTo(løpendeBarnetrygdURL)).withRequestBody( + equalToJson( + objectMapper.writeValueAsString( + infotrygdSøkRequest, + ), + ), + ), + ) + Assertions.assertEquals(false, finnesIkkeHosInfotrygd) + } + + @Test + fun `Invokering av Infotrygd-Barnetrygd genererer http feil`() { + wireMockServer.stubFor(post(løpendeBarnetrygdURL).willReturn(aResponse().withStatus(401))) + + assertThrows { + client.harLøpendeSakIInfotrygd(ClientMocks.søkerFnr.toList(), ClientMocks.barnFnr.toList()) + } + } + + @Test + fun `harNyligSendtBrevFor skal returnerer true for personIdent`() { + wireMockServer.stubFor( + post(brevURL).willReturn( + okJson(objectMapper.writeValueAsString(InfotrygdBarnetrygdClient.SendtBrevResponse(true, emptyList()))), + ), + ) + + val søkersIdenter = ClientMocks.søkerFnr.toList() + + val harNyligSendtBrev = client.harNyligSendtBrevFor( + søkersIdenter, + listOf(InfotrygdBrevkode.BREV_BATCH_INNVILGET_SMÅBARNSTILLEGG), + ) + + Assertions.assertEquals(true, harNyligSendtBrev.harSendtBrev) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedClientTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedClientTest.kt new file mode 100644 index 000000000..ea0f20314 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/InfotrygdFeedClientTest.kt @@ -0,0 +1,92 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd + +import com.github.tomakehurst.wiremock.client.WireMock.aResponse +import com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor +import com.github.tomakehurst.wiremock.client.WireMock.anyUrl +import com.github.tomakehurst.wiremock.client.WireMock.equalTo +import com.github.tomakehurst.wiremock.client.WireMock.equalToJson +import com.github.tomakehurst.wiremock.client.WireMock.okJson +import com.github.tomakehurst.wiremock.client.WireMock.post +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.integrasjoner.infotrygd.domene.InfotrygdFødselhendelsesFeedDto +import no.nav.familie.ba.sak.integrasjoner.infotrygd.domene.InfotrygdFødselhendelsesFeedTaskDto +import no.nav.familie.kontrakter.felles.Ressurs.Companion.success +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.log.NavHttpHeaders +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.RestOperations +import java.net.URI + +class InfotrygdFeedClientTest : AbstractSpringIntegrationTest() { + + lateinit var client: InfotrygdFeedClient + + @Autowired + @Qualifier("jwtBearer") + lateinit var restOperations: RestOperations + + @BeforeEach + fun setUp() { + client = InfotrygdFeedClient( + URI.create(wireMockServer.baseUrl() + "/api"), + restOperations, + ) + } + + @AfterEach + fun clearTest() { + wireMockServer.resetAll() + } + + @Test + @Tag("integration") + fun `skal legge til fødselsnummer i infotrygd feed`() { + wireMockServer.stubFor( + post("/api/barnetrygd/v1/feed/foedselsmelding").willReturn( + okJson(objectMapper.writeValueAsString(success("Create"))), + ), + ) + val request = InfotrygdFødselhendelsesFeedTaskDto(listOf("fnr")) + + request.fnrBarn.forEach { + client.sendFødselhendelsesFeedTilInfotrygd(InfotrygdFødselhendelsesFeedDto(fnrBarn = it)) + } + + wireMockServer.verify( + anyRequestedFor(anyUrl()) + .withHeader(NavHttpHeaders.NAV_CONSUMER_ID.asString(), equalTo("srvfamilie-ba-sak")) + .withRequestBody( + equalToJson( + objectMapper.writeValueAsString(InfotrygdFødselhendelsesFeedDto(fnrBarn = request.fnrBarn.first())), + ), + ), + ) + } + + @Test + @Tag("integration") + fun `Invokering av Infotrygd feed genererer http feil`() { + wireMockServer.stubFor(post("/api/barnetrygd/v1/feed/foedselsmelding").willReturn(aResponse().withStatus(401))) + + assertThrows { + client.sendFødselhendelsesFeedTilInfotrygd(InfotrygdFødselhendelsesFeedDto("fnr")) + } + } + + @Test + @Tag("integration") + fun `Invokering av Infotrygd returnerer ulovlig response format`() { + wireMockServer.stubFor(post("/api/barnetrygd/v1/feed/foedselsmelding").willReturn(aResponse().withBody("Create"))) + + assertThrows { + client.sendFødselhendelsesFeedTilInfotrygd(InfotrygdFødselhendelsesFeedDto("fnr")) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/SendF\303\270dselsmeldingTilInfotrygdTaskTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/SendF\303\270dselsmeldingTilInfotrygdTaskTest.kt" new file mode 100644 index 000000000..e530f6466 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/infotrygd/SendF\303\270dselsmeldingTilInfotrygdTaskTest.kt" @@ -0,0 +1,21 @@ +package no.nav.familie.ba.sak.integrasjoner.infotrygd + +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.integrasjoner.infotrygd.domene.InfotrygdFødselhendelsesFeedTaskDto +import no.nav.familie.ba.sak.task.SendFødselsmeldingTilInfotrygdTask +import no.nav.familie.kontrakter.felles.objectMapper +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class SendFødselsmeldingTilInfotrygdTaskTest : AbstractSpringIntegrationTest() { + + @Test + fun `Legg til fødselsmelding til task`() { + val fnrBarn = "12345678910" + val testTask = SendFødselsmeldingTilInfotrygdTask.opprettTask(listOf(fnrBarn)) + + val infotrygdFeedDto = objectMapper.readValue(testTask.payload, InfotrygdFødselhendelsesFeedTaskDto::class.java) + + Assertions.assertEquals(listOf(fnrBarn), infotrygdFeedDto.fnrBarn) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/InnkommendeJournalf\303\270ringServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/InnkommendeJournalf\303\270ringServiceTest.kt" new file mode 100644 index 000000000..804c511cf --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/InnkommendeJournalf\303\270ringServiceTest.kt" @@ -0,0 +1,161 @@ +package no.nav.familie.ba.sak.integrasjoner.journalføring + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.ekstern.restDomene.InstitusjonInfo +import no.nav.familie.ba.sak.ekstern.restDomene.NavnOgIdent +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.DbJournalpostType +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.JournalføringRepository +import no.nav.familie.ba.sak.integrasjoner.journalføring.domene.Sakstype +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingSøknadsinfoRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingSøknadsinfoService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.lagMockRestJournalføring +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired + +class InnkommendeJournalføringServiceTest( + + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @Autowired + private val behandlingSøknadsinfoService: BehandlingSøknadsinfoService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val innkommendeJournalføringService: InnkommendeJournalføringService, + + @Autowired + private val journalføringRepository: JournalføringRepository, + + @Autowired + private val behandlingSøknadsinfoRepository: BehandlingSøknadsinfoRepository, + +) : AbstractSpringIntegrationTest() { + + @Test + fun `lagrer journalpostreferanse til behandling og fagsak til journalpost`() { + val søkerFnr = randomFnr() + val søkerAktør = personidentService.hentAktør(søkerFnr) + + val fagsak = fagsakService.hentEllerOpprettFagsak(søkerAktør.aktivFødselsnummer()) + val behandling = behandlingHentOgPersisterService.lagreEllerOppdater(lagBehandling(fagsak)) + + val (sak, behandlinger) = innkommendeJournalføringService + .lagreJournalpostOgKnyttFagsakTilJournalpost(listOf(behandling.id.toString()), "12345") + + val journalposter = journalføringRepository.findByBehandlingId(behandlingId = behandling.id) + + assertEquals(1, journalposter.size) + assertEquals(DbJournalpostType.I, journalposter.first().type) + assertEquals(fagsak.id.toString(), sak.fagsakId) + assertEquals(1, behandlinger.size) + } + + @Test + fun `ferdigstill skal oppdatere journalpost med GENERELL_SAKSTYPE hvis knyttTilFagsak er false`() { + val søkerFnr = randomFnr() + val søkerAktør = personidentService.hentAktør(søkerFnr) + + val fagsak = fagsakService.hentEllerOpprettFagsak(søkerAktør.aktivFødselsnummer()) + behandlingHentOgPersisterService.lagreEllerOppdater(lagBehandling(fagsak)) + + val (sak, behandlinger) = innkommendeJournalføringService + .lagreJournalpostOgKnyttFagsakTilJournalpost(listOf(), "12345") + + assertNull(sak.fagsakId) + assertEquals(Sakstype.GENERELL_SAK.type, sak.sakstype) + assertEquals(0, behandlinger.size) + } + + @Test + fun `journalfør skal opprette en førstegangsbehandling fra journalføring og lagre ned søknadsinfo`() { + val søkerFnr = randomFnr() + val request = lagMockRestJournalføring(bruker = NavnOgIdent("Mock", søkerFnr)) + val fagsakId = innkommendeJournalføringService.journalfør(request, "123", "mockEnhet", "1") + + val behandling = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId.toLong()) + assertNotNull(behandling) + assertEquals(request.nyBehandlingstype, behandling!!.type) + assertEquals(request.nyBehandlingsårsak, behandling.opprettetÅrsak) + + val søknadMottattDato = behandlingSøknadsinfoService.hentSøknadMottattDato(behandling.id) + assertNotNull(søknadMottattDato) + assertEquals(request.datoMottatt!!.toLocalDate(), søknadMottattDato!!.toLocalDate()) + + val søknadsinfo = behandlingSøknadsinfoRepository.findByBehandlingId(behandling.id).single() + assertEquals(true, søknadsinfo.erDigital) + } + + @Test + fun `journalfør skal lagre ned søknadsinfo tilknyttet en tidligere behandling`() { + val søkerFnr = randomFnr() + val førsteSøknad = lagMockRestJournalføring(bruker = NavnOgIdent("Mock", søkerFnr)) + val fagsakId = innkommendeJournalføringService.journalfør(førsteSøknad, "123", "mockEnhet", "1") + + val behandling = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId.toLong()) + + val nySøknad2DagerSenere = førsteSøknad.copy( + datoMottatt = førsteSøknad.datoMottatt!!.plusDays(2), + opprettOgKnyttTilNyBehandling = false, + tilknyttedeBehandlingIder = listOf(behandling!!.id.toString()), + ) + + innkommendeJournalføringService.journalfør(nySøknad2DagerSenere, "124", "mockEnhet", "2") + + val søknadsinfo = behandlingSøknadsinfoRepository.findByBehandlingId(behandling.id) + assertEquals(2, søknadsinfo.size) + + val søknadMottattDato = behandlingSøknadsinfoService.hentSøknadMottattDato(behandling.id) + assertEquals(førsteSøknad.datoMottatt!!.toLocalDate(), søknadMottattDato!!.toLocalDate()) + } + + @Test + fun `journalfør skal opprette behandling på fagsak som har BARN som eier hvis enslig mindreårig eller institusjon`() { + val request = lagMockRestJournalføring(bruker = NavnOgIdent("Mock", randomFnr())) + .copy(fagsakType = FagsakType.BARN_ENSLIG_MINDREÅRIG) + val fagsakId = innkommendeJournalføringService.journalfør(request, "123", "mockEnhet", "1") + val behandling = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId.toLong()) + + assertNotNull(behandling) + assertEquals(FagsakType.BARN_ENSLIG_MINDREÅRIG, behandling!!.fagsak.type) + + val request2 = lagMockRestJournalføring(bruker = NavnOgIdent("Mock", randomFnr())) + .copy(fagsakType = FagsakType.INSTITUSJON, institusjon = InstitusjonInfo("orgnr", tssEksternId = "tss")) + val fagsakId2 = innkommendeJournalføringService.journalfør(request2, "1234", "mockEnhet", "2") + val behandling2 = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId2.toLong()) + + assertNotNull(behandling2) + assertEquals(FagsakType.INSTITUSJON, behandling2!!.fagsak.type) + } + + @Test + fun `journalfør skal ikke opprette en førstegangsbehandling fra journalføring med manglende mottatt dato`() { + val søkerFnr = randomFnr() + val request = lagMockRestJournalføring(bruker = NavnOgIdent("Mock", søkerFnr)).copy(datoMottatt = null) + + val exception = assertThrows { + innkommendeJournalføringService.journalfør( + request, + "123", + "mockEnhet", + "1", + ) + } + assertEquals("Du må sette søknads mottatt dato før du kan fortsette videre", exception.message) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/Journalf\303\270ringUtilsTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/Journalf\303\270ringUtilsTest.kt" new file mode 100644 index 000000000..113065bf6 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/journalf\303\270ring/Journalf\303\270ringUtilsTest.kt" @@ -0,0 +1,99 @@ +package no.nav.familie.ba.sak.integrasjoner.journalføring + +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.ekstern.restDomene.NavnOgIdent +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.verdikjedetester.lagMockRestJournalføring +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class JournalføringUtilsTest { + + val ordinærJournalpostTittel = "Søknad om ordinær barnetrygd" + val utvidetJournalpostTittel = "Søknad om utvidet barnetrygd" + + @Test + fun `Skal utlede ordinær når søknad om ordinær journalføres`() { + val søkerFnr = randomFnr() + assertEquals( + BehandlingUnderkategori.ORDINÆR, + lagMockRestJournalføring( + bruker = NavnOgIdent("Mock", søkerFnr), + ).copy( + journalpostTittel = "Søknad om ordinær barnetrygd", + opprettOgKnyttTilNyBehandling = true, + ).hentUnderkategori(), + ) + } + + @Test + fun `Skal utlede utvidet når søknad om utvidet journalføres`() { + val søkerFnr = randomFnr() + assertEquals( + BehandlingUnderkategori.UTVIDET, + lagMockRestJournalføring( + bruker = NavnOgIdent("Mock", søkerFnr), + ).copy( + journalpostTittel = utvidetJournalpostTittel, + underkategori = BehandlingUnderkategori.UTVIDET, + opprettOgKnyttTilNyBehandling = true, + ).hentUnderkategori(), + ) + } + + @Test + fun `Skal utlede ordinær når søknad om ordinær journalføres, men underkategori ikke er satt`() { + val søkerFnr = randomFnr() + val underkategori: BehandlingUnderkategori = + lagMockRestJournalføring(bruker = NavnOgIdent(navn = "Mock", søkerFnr)) + .copy( + journalpostTittel = ordinærJournalpostTittel, + kategori = null, + underkategori = null, + opprettOgKnyttTilNyBehandling = true, + ).hentUnderkategori() + assertEquals(BehandlingUnderkategori.ORDINÆR, underkategori) + } + + @Test + fun `Skal utlede ordinær når søknad om ordinær journalføres, og underkategori er satt til ordinær`() { + val søkerFnr = randomFnr() + val underkategori: BehandlingUnderkategori = + lagMockRestJournalføring(bruker = NavnOgIdent(navn = "Mock", søkerFnr)) + .copy( + journalpostTittel = ordinærJournalpostTittel, + kategori = null, + underkategori = BehandlingUnderkategori.ORDINÆR, + opprettOgKnyttTilNyBehandling = true, + ).hentUnderkategori() + assertEquals(BehandlingUnderkategori.ORDINÆR, underkategori) + } + + @Test + fun `Skal utlede utvidet når søknad om utvidet journalføres, men underkategori ikke er satt`() { + val søkerFnr = randomFnr() + val underkategori: BehandlingUnderkategori = + lagMockRestJournalføring(bruker = NavnOgIdent(navn = "Mock", søkerFnr)) + .copy( + journalpostTittel = utvidetJournalpostTittel, + kategori = null, + underkategori = null, + opprettOgKnyttTilNyBehandling = true, + ).hentUnderkategori() + assertEquals(BehandlingUnderkategori.UTVIDET, underkategori) + } + + @Test + fun `Skal utlede utvidet når søknad om utvidet journalføres, og underkategori er satt til utvidet`() { + val søkerFnr = randomFnr() + val underkategori: BehandlingUnderkategori = + lagMockRestJournalføring(bruker = NavnOgIdent(navn = "Mock", søkerFnr)) + .copy( + journalpostTittel = utvidetJournalpostTittel, + kategori = null, + underkategori = BehandlingUnderkategori.UTVIDET, + opprettOgKnyttTilNyBehandling = true, + ).hentUnderkategori() + assertEquals(BehandlingUnderkategori.UTVIDET, underkategori) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveIntegrationTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveIntegrationTest.kt new file mode 100644 index 000000000..dd6150865 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/oppgave/OppgaveIntegrationTest.kt @@ -0,0 +1,163 @@ +package no.nav.familie.ba.sak.integrasjoner.oppgave + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.integrasjoner.oppgave.domene.OppgaveRepository +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class OppgaveIntegrationTest : AbstractSpringIntegrationTest() { + + @Autowired + private lateinit var fagsakService: FagsakService + + @Autowired + private lateinit var behandlingService: BehandlingService + + @Autowired + private lateinit var oppgaveService: OppgaveService + + @Autowired + private lateinit var oppgaveRepository: OppgaveRepository + + @Autowired + private lateinit var personidentService: PersonidentService + + @Autowired + private lateinit var personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository + + @Autowired + private lateinit var databaseCleanupService: DatabaseCleanupService + + @BeforeEach + fun setUp() { + databaseCleanupService.truncate() + } + + @Test + fun `Skal opprette oppgave og ferdigstille oppgave for behandling`() { + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(SØKER_FNR) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val barnAktør = personidentService.hentOgLagreAktørIder(listOf(BARN_FNR), true) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandling.id, + SØKER_FNR, + listOf(BARN_FNR), + søkerAktør = fagsak.aktør, + barnAktør = barnAktør, + ) + + personopplysningGrunnlagRepository.save(personopplysningGrunnlag) + + val godkjenneVedtakOppgaveId = + oppgaveService.opprettOppgave(behandling.id, Oppgavetype.GodkjenneVedtak, LocalDate.now()) + + val opprettetOppgave = + oppgaveRepository.findByOppgavetypeAndBehandlingAndIkkeFerdigstilt(Oppgavetype.GodkjenneVedtak, behandling) + + Assertions.assertNotNull(opprettetOppgave) + Assertions.assertEquals(Oppgavetype.GodkjenneVedtak, opprettetOppgave!!.type) + Assertions.assertEquals(behandling.id, opprettetOppgave.behandling.id) + Assertions.assertEquals(behandling.status, opprettetOppgave.behandling.status) + Assertions.assertEquals( + behandling.behandlingStegTilstand.first().behandlingSteg, + opprettetOppgave.behandling.behandlingStegTilstand.first().behandlingSteg, + ) + Assertions.assertEquals( + behandling.behandlingStegTilstand.first().behandlingStegStatus, + opprettetOppgave.behandling.behandlingStegTilstand.first().behandlingStegStatus, + ) + Assertions.assertFalse(opprettetOppgave.erFerdigstilt) + Assertions.assertEquals(godkjenneVedtakOppgaveId, opprettetOppgave.gsakId) + + oppgaveService.ferdigstillOppgaver(behandling.id, Oppgavetype.GodkjenneVedtak) + + Assertions.assertNull( + oppgaveRepository.findByOppgavetypeAndBehandlingAndIkkeFerdigstilt( + Oppgavetype.GodkjenneVedtak, + behandling, + ), + ) + } + + @Test + fun `Skal logge feil ved opprettelse av oppgave på type som ikke er ferdigstilt`() { + val logger: Logger = LoggerFactory.getLogger(OppgaveService::class.java) as Logger + + val listAppender: ListAppender = initLoggingEventListAppender() + logger.addAppender(listAppender) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(SØKER_FNR) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val barnAktør = personidentService.hentOgLagreAktørIder(listOf(BARN_FNR), true) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandling.id, + SØKER_FNR, + listOf(BARN_FNR), + søkerAktør = fagsak.aktør, + barnAktør = barnAktør, + ) + + personopplysningGrunnlagRepository.save(personopplysningGrunnlag) + + oppgaveService.opprettOppgave(behandling.id, Oppgavetype.GodkjenneVedtak, LocalDate.now()) + oppgaveService.opprettOppgave(behandling.id, Oppgavetype.GodkjenneVedtak, LocalDate.now()) + + val loggingEvents = listAppender.list + + assertThat(loggingEvents) + .extracting { obj: ILoggingEvent -> obj.formattedMessage } + .anyMatch { message -> message.contains("Fant eksisterende oppgave med samme oppgavetype") } + } + + @Test + fun `Skal fjerne behandlesAvApplikasjon på liste med oppgaver som finnes i ba-ak`() { + databaseCleanupService.truncate() + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(SØKER_FNR) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val barnAktør = personidentService.hentOgLagreAktørIder(listOf(BARN_FNR), true) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandling.id, + SØKER_FNR, + listOf(BARN_FNR), + søkerAktør = fagsak.aktør, + barnAktør = barnAktør, + ) + + personopplysningGrunnlagRepository.save(personopplysningGrunnlag) + + val oppgave1 = + oppgaveService.opprettOppgave(behandling.id, Oppgavetype.GodkjenneVedtak, LocalDate.now()).toLong() + + val response = oppgaveService.fjernBehandlesAvApplikasjon(listOf(oppgave1, 123456L)) + assertThat(response.toList()).hasSize(1).containsOnly(oppgave1) + } + + protected fun initLoggingEventListAppender(): ListAppender { + val listAppender = ListAppender() + listAppender.start() + return listAppender + } + + companion object { + private val SØKER_FNR = randomFnr() + private val BARN_FNR = ClientMocks.barnFnr[0] + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PersonopplysningerServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PersonopplysningerServiceTest.kt new file mode 100644 index 000000000..2f001ac6f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/pdl/PersonopplysningerServiceTest.kt @@ -0,0 +1,285 @@ +package no.nav.familie.ba.sak.integrasjoner.pdl + +import com.github.tomakehurst.wiremock.client.WireMock +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.IntegrasjonClientMock.Companion.mockSjekkTilgang +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollClient +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.familie.kontrakter.felles.personopplysning.OPPHOLDSTILLATELSE +import org.apache.commons.lang3.StringUtils +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.RestOperations +import java.net.URI +import java.time.LocalDate + +internal class PersonopplysningerServiceTest( + @Autowired + @Qualifier("jwtBearer") + private val restTemplate: RestOperations, + + @Autowired + private val mockFamilieIntegrasjonerTilgangskontrollClient: FamilieIntegrasjonerTilgangskontrollClient, + + @Autowired + private val familieIntegrasjonerTilgangskontrollService: FamilieIntegrasjonerTilgangskontrollService, + + @Autowired + private val mockPersonidentService: PersonidentService, + +) : AbstractSpringIntegrationTest() { + + lateinit var personopplysningerService: PersonopplysningerService + + @BeforeEach + fun setUp() { + personopplysningerService = + PersonopplysningerService( + PdlRestClient(URI.create(wireMockServer.baseUrl() + "/api"), restTemplate, mockPersonidentService), + SystemOnlyPdlRestClient( + URI.create(wireMockServer.baseUrl() + "/api"), + restTemplate, + mockPersonidentService, + ), + familieIntegrasjonerTilgangskontrollService, + ) + lagMockForPersoner() + } + + @Test + fun `hentPersoninfoMedRelasjonerOgRegisterinformasjon() skal return riktig personinfo`() { + mockFamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(mapOf(ID_BARN_1 to true, ID_BARN_2 to false)) + + val personInfo = personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(ID_MOR)) + + assert(LocalDate.of(1955, 9, 13) == personInfo.fødselsdato) + assertThat(personInfo.adressebeskyttelseGradering).isEqualTo(ADRESSEBESKYTTELSEGRADERING.UGRADERT) + assertThat(personInfo.forelderBarnRelasjon.size).isEqualTo(1) + assertThat(personInfo.forelderBarnRelasjonMaskert.size).isEqualTo(1) + assertThat(personInfo.kontaktinformasjonForDoedsbo).isNull() + assertThat(personInfo.dødsfall).isNull() + } + + @Test + fun `hentPersoninfoMedRelasjonerOgRegisterinformasjon() skal returnere riktig personinfo for død person`() { + mockFamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(mapOf(ID_BARN_1 to true, ID_BARN_2 to false)) + + val personInfo = personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon( + tilAktør( + ID_DØD_MOR, + ), + ) + + assertThat(personInfo.dødsfall?.erDød).isTrue + assertThat(personInfo.dødsfall?.dødsdato).isEqualTo("2020-04-04") + assertThat(personInfo.kontaktinformasjonForDoedsbo?.adresse?.postnummer).isEqualTo("1234") + } + + @Test + fun `hentPersoninfoMedRelasjonerOgRegisterinformasjon() skal filtrere bort relasjoner med opphørte folkreregisteridenter eller uten fødselsdato`() { + mockFamilieIntegrasjonerTilgangskontrollClient.mockSjekkTilgang(true) + + val personInfo = personopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon( + tilAktør( + ID_MOR_3BARN_1OPPHØRT_1UTENFØDSELSDATO, + ), + ) + + assertEquals(1, personInfo.forelderBarnRelasjon.size) + assertEquals(ID_BARN_1, personInfo.forelderBarnRelasjon.single().aktør.aktivFødselsnummer()) + } + + @Test + fun `hentStatsborgerskap() skal return riktig statsborgerskap`() { + val statsborgerskap = personopplysningerService.hentGjeldendeStatsborgerskap(tilAktør(ID_MOR)) + assert(statsborgerskap.land == "XXX") + } + + @Test + fun `hentOpphold() skal returnere riktig opphold`() { + val opphold = personopplysningerService.hentGjeldendeOpphold(tilAktør(ID_MOR)) + assert(opphold.type == OPPHOLDSTILLATELSE.MIDLERTIDIG) + } + + @Test + fun `hentLandkodeUtenlandskAdresse() skal returnere landkode `() { + val landkode = personopplysningerService.hentLandkodeAlpha2UtenlandskBostedsadresse(tilAktør(ID_MOR)) + assertThat(landkode).isEqualTo("GB") + } + + @Test + fun `hentLandkodeUtenlandskAdresse() skal returnere ZZ hvis ingen landkode `() { + val landkode = personopplysningerService.hentLandkodeAlpha2UtenlandskBostedsadresse(tilAktør(ID_BARN_1)) + assertThat(landkode).isEqualTo("ZZ") + } + + @Test + fun `hentLandkodeUtenlandskAdresse() skal returnere ZZ hvis ingen bostedsadresse `() { + val landkode = + personopplysningerService.hentLandkodeAlpha2UtenlandskBostedsadresse(tilAktør(ID_MOR_MED_TOM_BOSTEDSADRESSE)) + assertThat(landkode).isEqualTo("ZZ") + } + + @Test + fun `hentadressebeskyttelse skal returnere gradering`() { + val gradering = personopplysningerService.hentAdressebeskyttelseSomSystembruker(tilAktør(ID_BARN_1)) + assertThat(gradering).isEqualTo(ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG) + } + + @Test + fun `hentadressebeskyttelse skal returnere ugradert ved tom liste fra pdl`() { + val gradering = personopplysningerService.hentAdressebeskyttelseSomSystembruker(tilAktør(ID_UGRADERT_PERSON)) + assertThat(gradering).isEqualTo(ADRESSEBESKYTTELSEGRADERING.UGRADERT) + } + + @Test + fun `hentadressebeskyttelse feiler`() { + assertThrows { + personopplysningerService.hentAdressebeskyttelseSomSystembruker( + tilAktør( + ID_MOR, + ), + ) + } + } + + companion object { + + const val ID_MOR = "22345678901" + const val ID_MOR_MED_TOM_BOSTEDSADRESSE = "22345678903" + const val ID_DØD_MOR = "44556612345" + const val ID_MOR_3BARN_1OPPHØRT_1UTENFØDSELSDATO = "94556612349" + const val ID_BARN_1 = "32345678901" + const val ID_BARN_2 = "32345678902" + const val ID_UGRADERT_PERSON = "32345678903" + } + + private fun gyldigRequest(queryFilnavn: String, requestFilnavn: String): String { + return readfile(requestFilnavn) + .replace( + "GRAPHQL-PLACEHOLDER", + readfile(queryFilnavn).graphqlCompatible(), + ) + } + + private fun readfile(filnavn: String): String { + return this::class.java.getResource("/pdl/$filnavn")!!.readText() + } + + private fun String.graphqlCompatible(): String { + return StringUtils.normalizeSpace(this.replace("\n", "")) + } + + private fun lagMockForPdl(graphqlQueryFilnavn: String, requestFilnavn: String, mockResponse: String) { + wireMockServer.stubFor( + WireMock.post(WireMock.urlEqualTo("/api/graphql")) + .withRequestBody(WireMock.equalToJson(gyldigRequest(graphqlQueryFilnavn, requestFilnavn))) + .willReturn( + WireMock.aResponse() + .withHeader("Content-Type", "application/json") + .withBody(mockResponse), + ), + ) + } + + private fun lagMockForPersoner() { + lagMockForPdl( + "hentperson-med-relasjoner-og-registerinformasjon.graphql", + "PdlIntegrasjon/gyldigRequestForMor3Barn1Opphørt1UtenFødselsdato.json", + readfile("PdlIntegrasjon/personinfoResponseForMor3Barn1Opphørt1UtenFødselsdato.json"), + ) + + lagMockForPdl( + "hentperson-med-relasjoner-og-registerinformasjon.graphql", + "PdlIntegrasjon/gyldigRequestForMorMedXXXStatsborgerskap.json", + readfile("PdlIntegrasjon/personinfoResponseForMorMedXXXStatsborgerskap.json"), + ) + + lagMockForPdl( + "hentperson-enkel.graphql", + "PdlIntegrasjon/gyldigRequestForBarn.json", + readfile("PdlIntegrasjon/personinfoResponseForBarn.json"), + ) + + lagMockForPdl( + "hentperson-enkel.graphql", + "PdlIntegrasjon/gyldigRequestForBarnUtenFødselsdato.json", + readfile("PdlIntegrasjon/personinfoResponseForBarnUtenFødselsdato.json"), + ) + + lagMockForPdl( + "hentperson-enkel.graphql", + "PdlIntegrasjon/gyldigRequestForBarnMedOpphørtStatus.json", + readfile("PdlIntegrasjon/personinfoResponseForBarnMedOpphørtStatus.json"), + ) + + lagMockForPdl( + "hentperson-enkel.graphql", + "PdlIntegrasjon/gyldigRequestForBarn2.json", + readfile("PdlIntegrasjon/personinfoResponseForBarnMedAdressebeskyttelse.json"), + ) + + lagMockForPdl( + "hentperson-med-relasjoner-og-registerinformasjon.graphql", + "PdlIntegrasjon/gyldigRequestForDødMor.json", + readfile("PdlIntegrasjon/personinfoResponseForDødMor.json"), + ) + + lagMockForPdl( + "statsborgerskap-uten-historikk.graphql", + "PdlIntegrasjon/gyldigRequestForMorMedXXXStatsborgerskap.json", + readfile("PdlIntegrasjon/personinfoResponseForMorMedXXXStatsborgerskap.json"), + ) + + lagMockForPdl( + "opphold-uten-historikk.graphql", + "PdlIntegrasjon/gyldigRequestForMorMedXXXStatsborgerskap.json", + readfile("PdlIntegrasjon/personinfoResponseForMorMedXXXStatsborgerskap.json"), + ) + + lagMockForPdl( + "bostedsadresse-utenlandsk.graphql", + "PdlIntegrasjon/gyldigRequestForBostedsadresseperioder.json", + readfile("PdlIntegrasjon/utenlandskAdresseResponse.json"), + ) + + lagMockForPdl( + "bostedsadresse-utenlandsk.graphql", + "PdlIntegrasjon/gyldigRequestForBarn.json", + readfile("PdlIntegrasjon/personinfoResponseForBarn.json"), + ) + + lagMockForPdl( + "bostedsadresse-utenlandsk.graphql", + "PdlIntegrasjon/gyldigRequestForMorMedTomBostedsadresse.json", + readfile("PdlIntegrasjon/tomBostedsadresseResponse.json"), + ) + + lagMockForPdl( + "hent-adressebeskyttelse.graphql", + "PdlIntegrasjon/gyldigRequestForAdressebeskyttelse.json", + readfile("pdlAdressebeskyttelseResponse.json"), + ) + + lagMockForPdl( + "hent-adressebeskyttelse.graphql", + "PdlIntegrasjon/gyldigRequestForAdressebeskyttelse2.json", + readfile("pdlAdressebeskyttelseResponse.json"), + ) + + lagMockForPdl( + "hent-adressebeskyttelse.graphql", + "PdlIntegrasjon/gyldigRequestForAdressebeskyttelse3.json", + readfile("PdlIntegrasjon/pdlAdressebeskyttelseMedTomListeResponse.json"), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/SkyggesakSchedulerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/SkyggesakSchedulerTest.kt new file mode 100644 index 000000000..66a332857 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/skyggesak/SkyggesakSchedulerTest.kt @@ -0,0 +1,79 @@ +package no.nav.familie.ba.sak.integrasjoner.skyggesak + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.ba.sak.common.DbContainerInitializer +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.domain.Pageable +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit.jupiter.SpringExtension +import java.time.LocalDateTime + +@SpringBootTest +@ExtendWith(SpringExtension::class) +@ContextConfiguration(initializers = [DbContainerInitializer::class]) +@ActiveProfiles("postgres", "integrasjonstest") +@Tag("integration") +class SkyggesakSchedulerTest { + + @Autowired + lateinit var skyggesakRepository: SkyggesakRepository + + lateinit var skyggesakScheduler: SkyggesakScheduler + + @BeforeEach + fun init() { + skyggesakScheduler = SkyggesakScheduler(skyggesakRepository, mockk(), mockk(relaxed = true)) + skyggesakRepository.deleteAll() + } + + @Test + fun `Skal sende skyggesak for fagsak med sendtTidspunkt null`() { + val sendtTidspunkt = listOf(null, LocalDateTime.now()) + skyggesakRepository.saveAll( + sendtTidspunkt.mapIndexed { i, tid -> Skyggesak(i.toLong(), fagsakId = i.toLong(), sendtTidspunkt = tid) }, + ) + + every { skyggesakScheduler.fagsakRepository.finnFagsak(any()) } returns Fagsak(aktør = Aktør("1234567890123")) + + skyggesakScheduler.sendSkyggesaker() + + verify(exactly = 1) { + skyggesakScheduler.integrasjonClient.opprettSkyggesak(Aktør("1234567890123"), 0) + } + Assertions.assertEquals(0, skyggesakRepository.finnSkyggesakerKlareForSending(Pageable.unpaged()).size) + } + + @Test + fun `Skal slette skyggesaker eldre enn 14 dager`() { + val now = LocalDateTime.now() + val sendtTidspunkt = listOf(now.minusDays(13), now.minusDays(14), null) + + skyggesakRepository.saveAll( + sendtTidspunkt.mapIndexed { i, tid -> Skyggesak(i.toLong(), fagsakId = i.toLong(), sendtTidspunkt = tid) }, + ) + + Assertions.assertEquals(2, skyggesakRepository.finnSkyggesakerSomErSendt().size) + skyggesakScheduler.fjernGamleSkyggesakInnslag() + + // Sjekker at usendt skyggesak ikke er slettet, samt skyggesak sendt for mindre enn 14 dager siden + Assertions.assertEquals( + null, + skyggesakRepository.finnSkyggesakerKlareForSending(Pageable.unpaged()).single().sendtTidspunkt, + ) + Assertions.assertEquals( + now.minusDays(13).toLocalDate(), + skyggesakRepository.finnSkyggesakerSomErSendt().single().sendtTidspunkt?.toLocalDate(), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/statistikk/StatistikkClientTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/statistikk/StatistikkClientTest.kt new file mode 100644 index 000000000..896321774 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/statistikk/StatistikkClientTest.kt @@ -0,0 +1,53 @@ +package no.nav.familie.ba.sak.integrasjoner.statistikk + +import com.github.tomakehurst.wiremock.client.WireMock +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.objectMapper +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.web.client.RestOperations +import java.net.URI + +internal class StatistikkClientTest : AbstractSpringIntegrationTest() { + lateinit var client: StatistikkClient + + @Autowired + @Qualifier("jwtBearer") + lateinit var restOperations: RestOperations + + @BeforeEach + fun setUp() { + client = StatistikkClient( + URI.create(wireMockServer.baseUrl() + "/api"), + restOperations, + ) + } + + @AfterEach + fun clearTest() { + wireMockServer.resetAll() + } + + @Test + fun harSendtVedtaksmeldingForBehandling() { + wireMockServer.stubFor( + WireMock.get("/api/vedtak/123").willReturn( + WireMock.okJson( + objectMapper.writeValueAsString( + Ressurs.success(true), + ), + ), + ), + ) + + assertEquals( + client.harSendtVedtaksmeldingForBehandling(123), + true, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/FagsakStatusOppdatererIntegrasjonTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/FagsakStatusOppdatererIntegrasjonTest.kt" new file mode 100644 index 000000000..82cb0f1c2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/FagsakStatusOppdatererIntegrasjonTest.kt" @@ -0,0 +1,156 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.nyOrdinærBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate + +class FagsakStatusOppdatererIntegrasjonTest : AbstractSpringIntegrationTest() { + + @Autowired + private lateinit var fagsakService: FagsakService + + @Autowired + private lateinit var behandlingService: BehandlingService + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var tilkjentYtelseRepository: TilkjentYtelseRepository + + @Autowired + private lateinit var andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository + + @Autowired + private lateinit var databaseCleanupService: DatabaseCleanupService + + @BeforeEach + fun cleanUp() { + databaseCleanupService.truncate() + } + + @Test + fun `ikke oppdater status på fagsaker som er løpende og har løpende utbetalinger`() { + val forelderIdent = randomFnr() + + val fagsakOriginal = fagsakService.hentEllerOpprettFagsakForPersonIdent(forelderIdent).also { + fagsakService.oppdaterStatus(it, FagsakStatus.LØPENDE) + } + opprettOgLagreBehandlingMedAndeler( + personIdent = forelderIdent, + offsetPåAndeler = listOf(1L), + fagsakId = fagsakOriginal.id, + ) + + val fagsak = fagsakService.hentLøpendeFagsaker() + + Assertions.assertTrue(fagsak.any { it.id == fagsakOriginal.id }) + + fagsakService.oppdaterLøpendeStatusPåFagsaker() + val fagsak2 = fagsakService.hentLøpendeFagsaker() + + Assertions.assertTrue(fagsak2.any { it.id == fagsakOriginal.id }) + } + + @Test + fun `skal sette status til avsluttet hvis ingen løpende utbetalinger`() { + val forelderIdent = randomFnr() + + val fagsakOriginal = fagsakService.hentEllerOpprettFagsakForPersonIdent(forelderIdent).also { + fagsakService.oppdaterStatus(it, FagsakStatus.LØPENDE) + } + val førstegangsbehandling = + opprettOgLagreBehandlingMedAndeler( + personIdent = forelderIdent, + offsetPåAndeler = listOf(1L), + fagsakId = fagsakOriginal.id, + ) + + val tilkjentYtelse = tilkjentYtelseRepository.findByBehandling(førstegangsbehandling.id) + + tilkjentYtelse.stønadTom = LocalDate.now().minusMonths(1).toYearMonth() + tilkjentYtelseRepository.save(tilkjentYtelse) + + fagsakService.oppdaterLøpendeStatusPåFagsaker() + val fagsak = fagsakService.hentLøpendeFagsaker() + + Assertions.assertFalse(fagsak.any { it.id == fagsakOriginal.id }) + } + + private fun opprettOgLagreBehandlingMedAndeler( + personIdent: String, + offsetPåAndeler: List = emptyList(), + erIverksatt: Boolean = true, + medStatus: BehandlingStatus = BehandlingStatus.UTREDES, + fagsakId: Long, + ): Behandling { + val behandling = + behandlingService.opprettBehandling(nyOrdinærBehandling(søkersIdent = personIdent, fagsakId = fagsakId)) + behandling.status = medStatus + behandlingRepository.save(behandling) + val tilkjentYtelse = tilkjentYtelse(behandling = behandling, erIverksatt = erIverksatt) + tilkjentYtelseRepository.save(tilkjentYtelse) + offsetPåAndeler.forEach { + andelTilkjentYtelseRepository.save( + andelPåTilkjentYtelse( + tilkjentYtelse = tilkjentYtelse, + periodeOffset = it, + aktør = behandling.fagsak.aktør, + ), + ) + } + return behandling + } + + private fun tilkjentYtelse(behandling: Behandling, erIverksatt: Boolean) = TilkjentYtelse( + behandling = behandling, + opprettetDato = LocalDate.now(), + endretDato = LocalDate.now(), + utbetalingsoppdrag = if (erIverksatt) "Skal ikke være null" else null, + ) + + // Kun offset og kobling til behandling/tilkjent ytelse som er relevant når man skal plukke ut til konsistensavstemming + private fun andelPåTilkjentYtelse( + tilkjentYtelse: TilkjentYtelse, + periodeOffset: Long, + aktør: Aktør = randomAktør(), + ) = AndelTilkjentYtelse( + aktør = aktør, + behandlingId = tilkjentYtelse.behandling.id, + tilkjentYtelse = tilkjentYtelse, + kalkulertUtbetalingsbeløp = 1054, + nasjonaltPeriodebeløp = 1054, + stønadFom = LocalDate.now() + .minusMonths(12) + .toYearMonth(), + stønadTom = LocalDate.now() + .plusMonths(12) + .toYearMonth(), + type = YtelseType.ORDINÆR_BARNETRYGD, + periodeOffset = periodeOffset, + forrigePeriodeOffset = null, + sats = 1054, + prosent = BigDecimal(100), + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingSchedulerTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingSchedulerTest.kt" new file mode 100644 index 000000000..f661a9327 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingSchedulerTest.kt" @@ -0,0 +1,106 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import io.mockk.called +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.ba.sak.common.DbContainerInitializer +import no.nav.familie.ba.sak.config.AbstractMockkSpringRunner +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.prosessering.domene.Status +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit.jupiter.SpringExtension +import java.time.LocalDate + +@SpringBootTest +@ExtendWith(SpringExtension::class) +@ContextConfiguration(initializers = [DbContainerInitializer::class]) +@ActiveProfiles("postgres", "mock-brev-klient", "integrasjonstest") +@Tag("integration") +class KonsistensavstemmingSchedulerTest : AbstractMockkSpringRunner() { + + @Autowired + lateinit var taskRepository: TaskRepositoryWrapper + + @Autowired + lateinit var batchService: BatchService + + @Autowired + lateinit var behandlingService: BehandlingService + + @Autowired + lateinit var fagsakService: FagsakService + + @Autowired + lateinit var konsistensavstemmingScheduler: KonsistensavstemmingScheduler + + @Autowired + private lateinit var databaseCleanupService: DatabaseCleanupService + + @Autowired + private lateinit var featureToggleService: FeatureToggleService + + @BeforeEach + fun setUp() { + databaseCleanupService.truncate() + konsistensavstemmingScheduler = + KonsistensavstemmingScheduler( + batchService, + behandlingService, + fagsakService, + taskRepository, + featureToggleService, + ) + taskRepository = spyk(taskRepository) + } + + @Test + fun `Skal ikke trigge avstemming når det ikke er noen ledige batchkjøringer for dato`() { + val dagensDato = LocalDate.now() + val nyBatch = Batch(kjøreDato = dagensDato, status = KjøreStatus.TATT) + batchService.lagreNyStatus(nyBatch, KjøreStatus.TATT) + + konsistensavstemmingScheduler.utførKonsistensavstemming() + + verify { taskRepository wasNot called } + } + + @Test + fun `Skal ikke trigge avstemming når det ikke finnes batchkjøringer for dato`() { + val imorgen = LocalDate.now().plusDays(1) + val nyBatch = Batch(kjøreDato = imorgen) + batchService.lagreNyStatus(nyBatch, KjøreStatus.LEDIG) + + konsistensavstemmingScheduler.utførKonsistensavstemming() + + verify { taskRepository wasNot called } + } + + @Test + fun `Skal trigge en avstemming når det er ledig batchkjøring for dato`() { + val dagensDato = LocalDate.now() + val nyBatch = Batch(kjøreDato = dagensDato) + batchService.lagreNyStatus(nyBatch, KjøreStatus.LEDIG) + fagsakService.hentLøpendeFagsaker().forEach { fagsakService.oppdaterStatus(it, FagsakStatus.AVSLUTTET) } + + konsistensavstemmingScheduler.utførKonsistensavstemming() + + val tasks = taskRepository.findByStatus(Status.UBEHANDLET) + Assertions.assertEquals(1, tasks.size) + + // Setter task til Ferdig for å unngå at den kjøres fra andre tester. + taskRepository.save(tasks[0].copy(status = Status.FERDIG)) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingUtplukkingIntegrationTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingUtplukkingIntegrationTest.kt" new file mode 100644 index 000000000..a3748b443 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/KonsistensavstemmingUtplukkingIntegrationTest.kt" @@ -0,0 +1,308 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.nyOrdinærBehandling +import no.nav.familie.ba.sak.common.nyRevurdering +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate + +@TestMethodOrder(MethodOrderer.MethodName::class) +class KonsistensavstemmingUtplukkingIntegrationTest : AbstractSpringIntegrationTest() { + + @Autowired + private lateinit var fagsakService: FagsakService + + @Autowired + private lateinit var avstemmingService: AvstemmingService + + @Autowired + private lateinit var behandlingService: BehandlingService + + @Autowired + private lateinit var personidentService: PersonidentService + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var tilkjentYtelseRepository: TilkjentYtelseRepository + + @Autowired + private lateinit var andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository + + @Autowired + private lateinit var databaseCleanupService: DatabaseCleanupService + + @BeforeEach + fun cleanUp() { + databaseCleanupService.truncate() + } + + @Test + fun `Skal plukke iverksatt FGB`() { + val forelderIdent = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(forelderIdent).also { + fagsakService.oppdaterStatus(it, FagsakStatus.LØPENDE) + } + val førstegangsbehandling = + opprettOgLagreBehandlingMedAndeler( + personIdent = forelderIdent, + kildeOgOffsetPåAndeler = listOf(KildeOgOffsetPåAndel(null, 1L)), + fagsakId = fagsak.id, + ) + val iverksattOgLøpendeBehandlinger = avstemmingService.hentSisteIverksatteBehandlingerFraLøpendeFagsaker() + + val behandlingerMedRelevanteAndeler = + andelTilkjentYtelseRepository + .finnAndelerTilkjentYtelseForBehandlinger(iverksattOgLøpendeBehandlinger) + .map { it.kildeBehandlingId } + .distinct() + + Assertions.assertTrue(behandlingerMedRelevanteAndeler.any { it == førstegangsbehandling.id }) + } + + @Test + fun `Skal plukke både iverksatt FGB og revurdering når periode legges til`() { + val forelderIdent = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(forelderIdent).also { + fagsakService.oppdaterStatus(it, FagsakStatus.LØPENDE) + } + + val førstegangsbehandling = + opprettOgLagreBehandlingMedAndeler( + personIdent = forelderIdent, + kildeOgOffsetPåAndeler = listOf(KildeOgOffsetPåAndel(null, 1L)), + medStatus = BehandlingStatus.AVSLUTTET, + fagsakId = fagsak.id, + ) + val revurdering = + opprettOgLagreRevurderingMedAndeler( + personIdent = forelderIdent, + kildeOgOffsetPåAndeler = listOf( + KildeOgOffsetPåAndel(førstegangsbehandling.id, 1L), + KildeOgOffsetPåAndel(null, 2L), + ), + fagsakId = fagsak.id, + ) + + val iverksattOgLøpendeBehandlinger = avstemmingService.hentSisteIverksatteBehandlingerFraLøpendeFagsaker() + val behandlingerMedRelevanteAndeler = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandlinger(iverksattOgLøpendeBehandlinger) + .map { it.kildeBehandlingId } + .sortedBy { it } + .distinct() + + Assertions.assertEquals(2, behandlingerMedRelevanteAndeler.size) + Assertions.assertEquals(førstegangsbehandling.id, behandlingerMedRelevanteAndeler[0]) + Assertions.assertEquals(revurdering.id, behandlingerMedRelevanteAndeler[1]) + } + + @Test + fun `Skal kun plukke revurdering når periode på førstegangsbehandling blir erstattet`() { + val forelderIdent = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(forelderIdent).also { + fagsakService.oppdaterStatus(it, FagsakStatus.LØPENDE) + } + opprettOgLagreBehandlingMedAndeler( + personIdent = forelderIdent, + kildeOgOffsetPåAndeler = listOf(KildeOgOffsetPåAndel(null, 1L)), + medStatus = BehandlingStatus.AVSLUTTET, + fagsakId = fagsak.id, + ) + val revurdering = + opprettOgLagreRevurderingMedAndeler( + personIdent = forelderIdent, + kildeOgOffsetPåAndeler = listOf(KildeOgOffsetPåAndel(null, 2L)), + fagsakId = fagsak.id, + ) + + val iverksattOgLøpendeBehandlinger = avstemmingService.hentSisteIverksatteBehandlingerFraLøpendeFagsaker() + + val behandlingerMedRelevanteAndeler = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandlinger(iverksattOgLøpendeBehandlinger) + .map { it.kildeBehandlingId } + .distinct() + + Assertions.assertEquals(1, behandlingerMedRelevanteAndeler.size) + Assertions.assertEquals(revurdering.id, behandlingerMedRelevanteAndeler[0]) + } + + @Test + fun `Skal ikke plukke noe ved opphør`() { + val forelderIdent = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(forelderIdent).also { + fagsakService.oppdaterStatus(it, FagsakStatus.LØPENDE) + } + + opprettOgLagreBehandlingMedAndeler( + personIdent = forelderIdent, + kildeOgOffsetPåAndeler = listOf(KildeOgOffsetPåAndel(null, 1L)), + medStatus = BehandlingStatus.AVSLUTTET, + fagsakId = fagsak.id, + ) + opprettOgLagreRevurderingMedAndeler( + personIdent = forelderIdent, + kildeOgOffsetPåAndeler = emptyList(), + fagsakId = fagsak.id, + ) + + val iverksattOgLøpendeBehandlinger = avstemmingService.hentSisteIverksatteBehandlingerFraLøpendeFagsaker() + + val behandlingerMedRelevanteAndeler = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandlinger(iverksattOgLøpendeBehandlinger) + .map { it.kildeBehandlingId } + .distinct() + + Assertions.assertTrue(behandlingerMedRelevanteAndeler.isEmpty()) + } + + @Test + fun `Skal ikke plukke behandling som ikke er iverksatt`() { + val forelderIdent = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(forelderIdent).also { + fagsakService.oppdaterStatus(it, FagsakStatus.LØPENDE) + } + val iverksattBehandling = + opprettOgLagreBehandlingMedAndeler( + personIdent = forelderIdent, + kildeOgOffsetPåAndeler = listOf(KildeOgOffsetPåAndel(null, 1L)), + medStatus = BehandlingStatus.AVSLUTTET, + fagsakId = fagsak.id, + ) + + opprettOgLagreRevurderingMedAndeler( + personIdent = forelderIdent, + kildeOgOffsetPåAndeler = listOf(KildeOgOffsetPåAndel(null, 2L)), + erIverksatt = false, + fagsakId = fagsak.id, + ) + + val iverksattOgLøpendeBehandlinger = avstemmingService.hentSisteIverksatteBehandlingerFraLøpendeFagsaker() + val behandlingerMedRelevanteAndeler = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandlinger(iverksattOgLøpendeBehandlinger) + .map { it.kildeBehandlingId } + .distinct() + + Assertions.assertEquals(1, behandlingerMedRelevanteAndeler.size) + Assertions.assertEquals(iverksattBehandling.id, behandlingerMedRelevanteAndeler[0]) + } + + private fun opprettOgLagreBehandlingMedAndeler( + personIdent: String, + kildeOgOffsetPåAndeler: List = emptyList(), + erIverksatt: Boolean = true, + medStatus: BehandlingStatus = BehandlingStatus.UTREDES, + fagsakId: Long, + ): Behandling { + val behandling = + behandlingService.opprettBehandling(nyOrdinærBehandling(søkersIdent = personIdent, fagsakId = fagsakId)) + behandling.status = medStatus + behandlingRepository.save(behandling) + val tilkjentYtelse = tilkjentYtelse(behandling = behandling, erIverksatt = erIverksatt) + tilkjentYtelseRepository.save(tilkjentYtelse) + val personFnr = randomFnr() + val aktør = personidentService.hentOgLagreAktør(personFnr, true) + kildeOgOffsetPåAndeler.forEach { + andelTilkjentYtelseRepository.save( + andelPåTilkjentYtelse( + tilkjentYtelse = tilkjentYtelse, + kildeBehandlingId = it.kilde ?: behandling.id, + periodeOffset = it.offset, + aktør = aktør, + ), + ) + } + return behandling + } + + private fun opprettOgLagreRevurderingMedAndeler( + personIdent: String, + kildeOgOffsetPåAndeler: List = emptyList(), + erIverksatt: Boolean = true, + fagsakId: Long, + ): Behandling { + val behandling = + behandlingService.opprettBehandling(nyRevurdering(søkersIdent = personIdent, fagsakId = fagsakId)) + val tilkjentYtelse = tilkjentYtelse(behandling = behandling, erIverksatt = erIverksatt) + tilkjentYtelseRepository.save(tilkjentYtelse) + val personFnr = randomFnr() + val aktør = personidentService.hentOgLagreAktør(personFnr, true) + kildeOgOffsetPåAndeler.forEach { + andelTilkjentYtelseRepository.save( + andelPåTilkjentYtelse( + tilkjentYtelse = tilkjentYtelse, + kildeBehandlingId = it.kilde ?: behandling.id, + periodeOffset = it.offset, + aktør = aktør, + ), + ) + } + return behandling + } + + private fun tilkjentYtelse(behandling: Behandling, erIverksatt: Boolean) = TilkjentYtelse( + behandling = behandling, + opprettetDato = LocalDate.now(), + endretDato = LocalDate.now(), + utbetalingsoppdrag = if (erIverksatt) "Skal ikke være null" else null, + ) + + // Kun offset og kobling til behandling/tilkjent ytelse som er relevant når man skal plukke ut til konsistensavstemming + private fun andelPåTilkjentYtelse( + tilkjentYtelse: TilkjentYtelse, + kildeBehandlingId: Long, + periodeOffset: Long, + aktør: Aktør = randomAktør(), + ) = AndelTilkjentYtelse( + behandlingId = tilkjentYtelse.behandling.id, + tilkjentYtelse = tilkjentYtelse, + kalkulertUtbetalingsbeløp = 1054, + nasjonaltPeriodebeløp = 1054, + stønadFom = LocalDate.now() + .minusMonths(12) + .toYearMonth(), + stønadTom = LocalDate.now() + .plusMonths(12) + .toYearMonth(), + type = YtelseType.ORDINÆR_BARNETRYGD, + kildeBehandlingId = kildeBehandlingId, + periodeOffset = periodeOffset, + forrigePeriodeOffset = null, + sats = 1054, + prosent = BigDecimal(100), + aktør = aktør, + ) +} + +data class KildeOgOffsetPåAndel( + val kilde: Long?, // Hvis denne er null setter vi til behandling som opprettes, for å unngå loop-avhengighet + val offset: Long, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/PeriodeOffsetIntegrasjonTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/PeriodeOffsetIntegrasjonTest.kt" new file mode 100644 index 000000000..af4c6e608 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/PeriodeOffsetIntegrasjonTest.kt" @@ -0,0 +1,218 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.simulering.SimuleringService +import no.nav.familie.ba.sak.kjerne.steg.IverksettMotOppdrag +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.task.dto.IverksettingTaskDTO +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.LocalDateTime + +class PeriodeOffsetIntegrasjonTest( + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val vilkårsvurderingService: VilkårsvurderingService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + + @Autowired + private val beregningService: BeregningService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val vedtakService: VedtakService, + + @Autowired + private val simuleringService: SimuleringService, + + @Autowired + private val iverksettMotOppdrag: IverksettMotOppdrag, + + @Autowired + private val featureToggleService: FeatureToggleService, + +) : AbstractSpringIntegrationTest() { + + @Test + @Tag("integration") + fun `Sjekk at offset settes på andel tilkjent ytelse når behandlingen iverksettes`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val stønadFom = LocalDate.now() + val stønadTom = stønadFom.plusYears(17) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val barnAktørId = personidentService.hentOgLagreAktør(barnFnr, true) + + val vilkårsvurdering = + lagVilkårsvurdering(behandling, fagsak.aktør, barnAktørId, stønadFom, stønadTom) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = vilkårsvurdering) + Assertions.assertNotNull(behandling.fagsak.id) + + val barnAktør = personidentService.hentAktørIder(listOf(barnFnr)) + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = fagsak.aktør, + barnAktør = barnAktør, + ) + personopplysningGrunnlagRepository.save(personopplysningGrunnlag) + + behandlingService.opprettOgInitierNyttVedtakForBehandling(behandling = behandling) + + val vedtak = vedtakService.hentAktivForBehandling(behandlingId = behandling.id) + Assertions.assertNotNull(vedtak) + vedtak!!.vedtaksdato = LocalDateTime.now() + vedtakService.oppdater(vedtak) + + beregningService.oppdaterBehandlingMedBeregning(behandling, personopplysningGrunnlag) + + beregningService.hentAndelerTilkjentYtelseForBehandling(behandling.id) + .forEach { + Assertions.assertNull(it.periodeOffset) + Assertions.assertNull(it.forrigePeriodeOffset) + Assertions.assertNull(it.kildeBehandlingId) + } + + assertDoesNotThrow { + iverksettMotOppdrag.utførStegOgAngiNeste( + behandling, + IverksettingTaskDTO( + behandlingsId = behandling.id, + vedtaksId = vedtak.id, + saksbehandlerId = "ansvarligSaksbehandler", + personIdent = fagsak.aktør.aktivFødselsnummer(), + ), + ) + } + + beregningService.hentAndelerTilkjentYtelseForBehandling(behandling.id) + .forEach { + Assertions.assertNotNull(it.periodeOffset) + Assertions.assertNotNull(it.kildeBehandlingId) + } + } + + @Test + @Tag("integration") + fun `Sjekk at offset IKKE settes på andel tilkjent ytelse når behandlingen simuleres`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val stønadFom = LocalDate.now() + val stønadTom = stønadFom.plusYears(17) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val barnAktørId = personidentService.hentOgLagreAktør(barnFnr, true) + + val vilkårsvurdering = + lagVilkårsvurdering(behandling, fagsak.aktør, barnAktørId, stønadFom, stønadTom) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = vilkårsvurdering) + Assertions.assertNotNull(behandling.fagsak.id) + + val barnAktør = personidentService.hentAktørIder(listOf(barnFnr)) + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = fagsak.aktør, + barnAktør = barnAktør, + ) + personopplysningGrunnlagRepository.save(personopplysningGrunnlag) + + behandlingService.opprettOgInitierNyttVedtakForBehandling(behandling = behandling) + + val vedtak = vedtakService.hentAktivForBehandling(behandlingId = behandling.id) + Assertions.assertNotNull(vedtak) + vedtak!!.vedtaksdato = LocalDateTime.now() + vedtakService.oppdater(vedtak) + + beregningService.oppdaterBehandlingMedBeregning(behandling, personopplysningGrunnlag) + + beregningService.hentAndelerTilkjentYtelseForBehandling(behandling.id) + .forEach { + Assertions.assertNull(it.periodeOffset) + Assertions.assertNull(it.forrigePeriodeOffset) + Assertions.assertNull(it.kildeBehandlingId) + } + + assertDoesNotThrow { + simuleringService.oppdaterSimuleringPåBehandling(behandling) + } + + beregningService.hentAndelerTilkjentYtelseForBehandling(behandling.id) + .forEach { + Assertions.assertNull(it.periodeOffset) + Assertions.assertNull(it.forrigePeriodeOffset) + Assertions.assertNull(it.kildeBehandlingId) + } + } + + private fun lagVilkårsvurdering( + behandling: Behandling, + søkerAktør: Aktør, + barnAktør: Aktør, + stønadFom: LocalDate, + stønadTom: LocalDate, + ): Vilkårsvurdering { + val vilkårsvurdering = + Vilkårsvurdering(behandling = behandling) + vilkårsvurdering.personResultater = setOf( + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson(type = PersonType.SØKER, aktør = søkerAktør), + resultat = Resultat.OPPFYLT, + periodeFom = stønadFom, + periodeTom = stønadTom, + lagFullstendigVilkårResultat = true, + personType = PersonType.SØKER, + ), + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson(type = PersonType.BARN, aktør = barnAktør, fødselsdato = stønadFom), + resultat = Resultat.OPPFYLT, + periodeFom = stønadFom, + periodeTom = stønadTom, + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + ), + ) + return vilkårsvurdering + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragIntegrasjonTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragIntegrasjonTest.kt" new file mode 100644 index 000000000..4bfd482df --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/UtbetalingsoppdragIntegrasjonTest.kt" @@ -0,0 +1,1563 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.dato +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.toLocalDate +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.common.årMnd +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.ekstern.restDomene.InstitusjonInfo +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.BeregningTestUtil.sisteAndelPerIdent +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.kontrakter.felles.oppdrag.Opphør +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsoppdrag +import no.nav.familie.kontrakter.felles.oppdrag.Utbetalingsperiode +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.YearMonth + +class UtbetalingsoppdragIntegrasjonTest( + @Autowired + private val beregningService: BeregningService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val utbetalingsoppdragGeneratorService: UtbetalingsoppdragGeneratorService, + + @Autowired + private val tilkjentYtelseRepository: TilkjentYtelseRepository, +) : AbstractSpringIntegrationTest() { + + lateinit var utbetalingsoppdragGenerator: UtbetalingsoppdragGenerator + + @BeforeEach + fun setUp() { + databaseCleanupService.truncate() + utbetalingsoppdragGenerator = UtbetalingsoppdragGenerator(beregningService) + } + + @Test + fun `skal opprette et nytt utbetalingsoppdrag med felles løpende periodeId og separat kjeding på to personer`() { + val personMedFlerePerioder = tilfeldigPerson() + val tilfeldigPerson = tilfeldigPerson() + val fagsak = + fagsakService.hentEllerOpprettFagsakForPersonIdent(personMedFlerePerioder.aktør.aktivFødselsnummer()) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val vedtak = lagVedtak(behandling = behandling) + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling) + val andelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2023-03"), + YtelseType.SMÅBARNSTILLEGG, + 660, + behandling, + person = personMedFlerePerioder, + aktør = personidentService.hentOgLagreAktør(personMedFlerePerioder.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + årMnd("2026-05"), + årMnd("2027-06"), + YtelseType.SMÅBARNSTILLEGG, + 660, + behandling, + person = personMedFlerePerioder, + aktør = personidentService.hentOgLagreAktør(personMedFlerePerioder.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + årMnd("2019-03"), + årMnd("2037-02"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + tilkjentYtelse = tilkjentYtelse, + person = tilfeldigPerson, + aktør = personidentService.hentOgLagreAktør(tilfeldigPerson.aktør.aktivFødselsnummer(), true), + ), + ) + tilkjentYtelse.andelerTilkjentYtelse.addAll(andelerTilkjentYtelse) + + val utbetalingsoppdrag = + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak, + true, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerTilkjentYtelse.forIverksetting(), + ), + ) + + assertEquals(Utbetalingsoppdrag.KodeEndring.NY, utbetalingsoppdrag.kodeEndring) + assertEquals(3, utbetalingsoppdrag.utbetalingsperiode.size) + + val utbetalingsperioderPerKlasse = utbetalingsoppdrag.utbetalingsperiode.groupBy { it.klassifisering } + assertUtbetalingsperiode( + utbetalingsperioderPerKlasse.getValue("BATR")[0], + 2, + null, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2019-03-01", + "2037-02-28", + ) + assertUtbetalingsperiode( + utbetalingsperioderPerKlasse.getValue("BATRSMA")[0], + 0, + null, + fagsak.aktør.aktivFødselsnummer(), + 660, + "2019-04-01", + "2023-03-31", + ) + assertUtbetalingsperiode( + utbetalingsperioderPerKlasse.getValue("BATRSMA")[1], + 1, + 0, + fagsak.aktør.aktivFødselsnummer(), + 660, + "2026-05-01", + "2027-06-30", + ) + } + + @Test + fun `skal opprette et fullstendig opphør for to personer, hvor opphørsdatoer blir første dato i hver kjede`() { + val personMedFlerePerioder = tilfeldigPerson() + val fagsak = + fagsakService.hentEllerOpprettFagsakForPersonIdent(personMedFlerePerioder.aktør.aktivFødselsnummer()) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + val førsteDatoKjede1 = årMnd("2019-04") + val førsteDatoKjede2 = årMnd("2019-03") + val andelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse( + førsteDatoKjede1, + årMnd("2023-03"), + YtelseType.SMÅBARNSTILLEGG, + 660, + behandling, + person = personMedFlerePerioder, + aktør = personidentService.hentOgLagreAktør(personMedFlerePerioder.aktør.aktivFødselsnummer(), true), + periodeIdOffset = 0, + ), + lagAndelTilkjentYtelse( + årMnd("2026-05"), + årMnd("2027-06"), + YtelseType.SMÅBARNSTILLEGG, + 660, + behandling, + person = personMedFlerePerioder, + aktør = personidentService.hentOgLagreAktør(personMedFlerePerioder.aktør.aktivFødselsnummer(), true), + periodeIdOffset = 1, + ), + lagAndelTilkjentYtelse( + førsteDatoKjede2, + årMnd("2037-02"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + periodeIdOffset = 2, + ), + ) + + val vedtak = lagVedtak(behandling = behandling) + + val utbetalingsoppdrag = + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + saksbehandlerId = "saksbehandler", + vedtak = vedtak, + erFørsteBehandlingPåFagsak = false, + forrigeKjeder = ØkonomiUtils.grupperAndeler( + andelerTilkjentYtelse.forIverksetting(), + ), + sisteAndelPerIdent = sisteAndelPerIdent(andelerTilkjentYtelse), + ) + + assertEquals(Utbetalingsoppdrag.KodeEndring.ENDR, utbetalingsoppdrag.kodeEndring) + assertEquals(2, utbetalingsoppdrag.utbetalingsperiode.size) + + val utbetalingsperioderPerKlasse = utbetalingsoppdrag.utbetalingsperiode.groupBy { it.klassifisering } + assertUtbetalingsperiode( + utbetalingsperioderPerKlasse.getValue("BATRSMA")[0], + 1, + null, + fagsak.aktør.aktivFødselsnummer(), + 660, + "2026-05-01", + "2027-06-30", + førsteDatoKjede1.førsteDagIInneværendeMåned(), + ) + assertUtbetalingsperiode( + utbetalingsperioderPerKlasse.getValue("BATR")[0], + 2, + null, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2019-03-01", + "2037-02-28", + førsteDatoKjede2.førsteDagIInneværendeMåned(), + ) + } + + @Test + fun `skal opprette revurdering med endring på eksisterende periode`() { + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(randomFnr()) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak, + førsteSteg = StegType.BEHANDLING_AVSLUTTET, + ), + ) + + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling) + val person = tilfeldigPerson() + val vedtak = lagVedtak(behandling) + val fomDatoSomEndres = "2033-01-01" + val andelerFørstegangsbehandling = listOf( + lagAndelTilkjentYtelse( + årMnd("2020-01"), + årMnd("2029-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + periodeIdOffset = 0, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + dato(fomDatoSomEndres).toYearMonth(), + årMnd("2034-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + periodeIdOffset = 1, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + årMnd("2037-01"), + årMnd("2039-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + periodeIdOffset = 2, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + ) + tilkjentYtelse.andelerTilkjentYtelse.addAll(andelerFørstegangsbehandling) + tilkjentYtelse.utbetalingsoppdrag = "Oppdrag" + + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak, + true, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerFørstegangsbehandling.forIverksetting(), + ), + ) + + avsluttOgLagreBehandling(behandling) + val behandling2 = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val tilkjentYtelse2 = lagInitiellTilkjentYtelse(behandling2) + val vedtak2 = lagVedtak(behandling2) + val andelerRevurdering = listOf( + lagAndelTilkjentYtelse( + årMnd("2020-01"), + årMnd("2029-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling2, + periodeIdOffset = 0, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + årMnd("2034-01"), + årMnd("2034-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling2, + periodeIdOffset = 3, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse2, + ), + lagAndelTilkjentYtelse( + årMnd("2037-01"), + årMnd("2039-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling2, + periodeIdOffset = 4, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse2, + ), + ) + tilkjentYtelse2.andelerTilkjentYtelse.addAll(andelerRevurdering) + val sisteAndelPerIdent = beregningService.hentSisteAndelPerIdent(behandling.fagsak.id) + + val utbetalingsoppdrag = + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak2, + false, + forrigeKjeder = ØkonomiUtils.grupperAndeler( + andelerFørstegangsbehandling.forIverksetting(), + ), + sisteAndelPerIdent = sisteAndelPerIdent, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerRevurdering.forIverksetting(), + ), + ) + avsluttOgLagreBehandling(behandling2) + + assertEquals(Utbetalingsoppdrag.KodeEndring.ENDR, utbetalingsoppdrag.kodeEndring) + assertEquals(3, utbetalingsoppdrag.utbetalingsperiode.size) + + val opphørsperiode = utbetalingsoppdrag.utbetalingsperiode.find { it.opphør != null } + assertNotNull(opphørsperiode) + val nyeUtbetalingsPerioderSortert = + utbetalingsoppdrag.utbetalingsperiode.filter { it.opphør == null }.sortedBy { it.vedtakdatoFom } + assertEquals(2, nyeUtbetalingsPerioderSortert.size) + + assertUtbetalingsperiode( + opphørsperiode!!, + 2, + 1, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2037-01-01", + "2039-12-31", + dato(fomDatoSomEndres), + ) + assertUtbetalingsperiode( + nyeUtbetalingsPerioderSortert.first(), + 3, + 2, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2034-01-01", + "2034-12-31", + ) + assertUtbetalingsperiode( + nyeUtbetalingsPerioderSortert.last(), + 4, + 3, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2037-01-01", + "2039-12-31", + ) + } + + @Test + fun `Skal opprette revurdering med nytt barn`() { + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(randomFnr()) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak, + førsteSteg = StegType.BEHANDLING_AVSLUTTET, + ), + ) + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling) + val aktør = personidentService.hentOgLagreAktør(randomFnr(), true) + val person = tilfeldigPerson(aktør = aktør) + val vedtak = lagVedtak(behandling) + val andelerFørstegangsbehandling = listOf( + lagAndelTilkjentYtelse( + årMnd("2020-01"), + årMnd("2029-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + periodeIdOffset = 0, + person = person, + aktør = aktør, + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + årMnd("2033-01"), + årMnd("2034-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + periodeIdOffset = 1, + person = person, + aktør = aktør, + tilkjentYtelse = tilkjentYtelse, + ), + ) + tilkjentYtelse.andelerTilkjentYtelse.addAll(andelerFørstegangsbehandling) + tilkjentYtelse.utbetalingsoppdrag = "Oppdrag" + + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak, + true, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerFørstegangsbehandling.forIverksetting(), + ), + ) + + avsluttOgLagreBehandling(behandling) + val behandling2 = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val tilkjentYtelse2 = lagInitiellTilkjentYtelse(behandling2) + val nyAktør = personidentService.hentOgLagreAktør(randomFnr(), true) + val nyPerson = tilfeldigPerson(aktør = nyAktør) + val vedtak2 = lagVedtak(behandling2) + val andelerRevurdering = listOf( + lagAndelTilkjentYtelse( + årMnd("2022-01"), + årMnd("2034-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling2, + periodeIdOffset = 2, + person = nyPerson, + aktør = nyAktør, + tilkjentYtelse = tilkjentYtelse2, + ), + lagAndelTilkjentYtelse( + årMnd("2037-01"), + årMnd("2039-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling2, + periodeIdOffset = 3, + person = nyPerson, + aktør = personidentService.hentOgLagreAktør(nyPerson.aktør.aktørId, true), + tilkjentYtelse = tilkjentYtelse2, + ), + ) + tilkjentYtelse2.andelerTilkjentYtelse.addAll(andelerRevurdering) + val sisteAndelPerIdent = beregningService.hentSisteAndelPerIdent(behandling2.fagsak.id) + + val utbetalingsoppdrag = + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak2, + false, + forrigeKjeder = ØkonomiUtils.grupperAndeler( + andelerFørstegangsbehandling.forIverksetting(), + ), + sisteAndelPerIdent = sisteAndelPerIdent, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerRevurdering.forIverksetting(), + ), + ) + + assertEquals(Utbetalingsoppdrag.KodeEndring.ENDR, utbetalingsoppdrag.kodeEndring) + assertEquals(3, utbetalingsoppdrag.utbetalingsperiode.size) + val sorterteUtbetalingsperioder = utbetalingsoppdrag.utbetalingsperiode.sortedBy { it.periodeId } + assertUtbetalingsperiode( + sorterteUtbetalingsperioder[0], + 1, + 0, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2033-01-01", + "2034-12-31", + ) + assertUtbetalingsperiode( + sorterteUtbetalingsperioder[1], + 2, + null, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2022-01-01", + "2034-12-31", + ) + assertUtbetalingsperiode( + sorterteUtbetalingsperioder[2], + 3, + 2, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2037-01-01", + "2039-12-31", + ) + } + + @Test + fun `skal opprette et nytt utbetalingsoppdrag med to andeler på samme person og separat kjeding for småbarnstillegg`() { + val personMedFlerePerioder = tilfeldigPerson() + val fagsak = + fagsakService.hentEllerOpprettFagsakForPersonIdent(personMedFlerePerioder.aktør.aktivFødselsnummer()) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val vedtak = lagVedtak(behandling = behandling) + val andelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2023-03"), + YtelseType.SMÅBARNSTILLEGG, + 660, + behandling, + person = personMedFlerePerioder, + aktør = personidentService.hentOgLagreAktør(personMedFlerePerioder.aktør.aktørId, true), + ), + lagAndelTilkjentYtelse( + årMnd("2026-05"), + årMnd("2027-06"), + YtelseType.SMÅBARNSTILLEGG, + 660, + behandling, + person = personMedFlerePerioder, + aktør = personidentService.hentOgLagreAktør(personMedFlerePerioder.aktør.aktørId, true), + ), + lagAndelTilkjentYtelse( + årMnd("2019-03"), + årMnd("2037-02"), + YtelseType.UTVIDET_BARNETRYGD, + 1054, + behandling, + person = personMedFlerePerioder, + aktør = personidentService.hentOgLagreAktør(personMedFlerePerioder.aktør.aktørId, true), + ), + ) + + val utbetalingsoppdrag = utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak, + true, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerTilkjentYtelse.forIverksetting(), + ), + ) + + assertEquals(Utbetalingsoppdrag.KodeEndring.NY, utbetalingsoppdrag.kodeEndring) + assertEquals(3, utbetalingsoppdrag.utbetalingsperiode.size) + + val utbetalingsperioderPerKlasse = utbetalingsoppdrag.utbetalingsperiode.groupBy { it.klassifisering } + assertUtbetalingsperiode( + utbetalingsperioderPerKlasse.getValue("BATR")[0], + 2, + null, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2019-03-01", + "2037-02-28", + ) + assertUtbetalingsperiode( + utbetalingsperioderPerKlasse.getValue("BATRSMA")[0], + 0, + null, + fagsak.aktør.aktivFødselsnummer(), + 660, + "2019-04-01", + "2023-03-31", + ) + assertUtbetalingsperiode( + utbetalingsperioderPerKlasse.getValue("BATRSMA")[1], + 1, + 0, + fagsak.aktør.aktivFødselsnummer(), + 660, + "2026-05-01", + "2027-06-30", + ) + } + + @Test + fun `opprettelse av utbetalingsoppdrag hvor flere har småbarnstillegg kaster feil`() { + val behandling = lagBehandling() + val vedtak = lagVedtak(behandling = behandling) + val andelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse(årMnd("2019-04"), årMnd("2023-03"), YtelseType.SMÅBARNSTILLEGG, 660, behandling), + lagAndelTilkjentYtelse(årMnd("2026-05"), årMnd("2027-06"), YtelseType.SMÅBARNSTILLEGG, 660, behandling), + ) + + assertThrows { + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak, + true, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerTilkjentYtelse.forIverksetting(), + ), + ) + } + } + + @Test + fun `Ved full betalingsoppdrag skal komplett utbetalinsoppdrag genereres også når ingen endring blitt gjort`() { + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(randomFnr()) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak, + førsteSteg = StegType.BEHANDLING_AVSLUTTET, + ), + ) + + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling) + val person = tilfeldigPerson() + val vedtak = lagVedtak(behandling) + val andelerFørstegangsbehandling = listOf( + lagAndelTilkjentYtelse( + årMnd("2020-01"), + årMnd("2029-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + periodeIdOffset = 0, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + årMnd("2030-01"), + årMnd("2034-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + periodeIdOffset = 1, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + årMnd("2035-01"), + årMnd("2039-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + periodeIdOffset = 2, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + ) + tilkjentYtelse.andelerTilkjentYtelse.addAll(andelerFørstegangsbehandling) + + tilkjentYtelse.utbetalingsoppdrag = "Oppdrag" + + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak, + true, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerFørstegangsbehandling.forIverksetting(), + ), + ) + + avsluttOgLagreBehandling(behandling) + val behandling2 = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val tilkjentYtelse2 = lagInitiellTilkjentYtelse(behandling2) + val vedtak2 = lagVedtak(behandling2) + val andelerRevurdering = listOf( + lagAndelTilkjentYtelse( + årMnd("2020-01"), + årMnd("2029-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling2, + periodeIdOffset = 0, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + årMnd("2030-01"), + årMnd("2034-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling2, + periodeIdOffset = 3, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse2, + ), + lagAndelTilkjentYtelse( + årMnd("2035-01"), + årMnd("2039-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling2, + periodeIdOffset = 4, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse2, + ), + ) + tilkjentYtelse2.andelerTilkjentYtelse.addAll(andelerRevurdering) + val sisteAndelPerIdent = beregningService.hentSisteAndelPerIdent(behandling2.fagsak.id) + + val utbetalingsoppdrag = + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak2, + false, + forrigeKjeder = ØkonomiUtils.grupperAndeler( + andelerFørstegangsbehandling.forIverksetting(), + ), + sisteAndelPerIdent = sisteAndelPerIdent, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerRevurdering.forIverksetting(), + ), + erSimulering = true, + ) + + assertEquals(Utbetalingsoppdrag.KodeEndring.ENDR, utbetalingsoppdrag.kodeEndring) + assertEquals(4, utbetalingsoppdrag.utbetalingsperiode.size) + + val opphørsperiode = utbetalingsoppdrag.utbetalingsperiode.find { it.opphør != null } + assertNotNull(opphørsperiode) + val nyeUtbetalingsPerioderSortert = + utbetalingsoppdrag.utbetalingsperiode.filter { it.opphør == null }.sortedBy { it.vedtakdatoFom } + assertEquals(3, nyeUtbetalingsPerioderSortert.size) + + assertUtbetalingsperiode( + opphørsperiode!!, + 2, + 1, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2035-01-01", + "2039-12-31", + dato("2020-01-01"), + ) + assertUtbetalingsperiode( + nyeUtbetalingsPerioderSortert.first(), + 3, + 2, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2020-01-01", + "2029-12-31", + ) + assertUtbetalingsperiode( + nyeUtbetalingsPerioderSortert[1], + 4, + 3, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2030-01-01", + "2034-12-31", + ) + assertUtbetalingsperiode( + nyeUtbetalingsPerioderSortert.last(), + 5, + 4, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2035-01-01", + "2039-12-31", + ) + } + + @Test + fun `Ved full betalingsoppdrag skal komplett utbetalinsoppdrag genereres også når bare siste periode blitt endrett`() { + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(randomFnr()) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak, + førsteSteg = StegType.BEHANDLING_AVSLUTTET, + ), + ) + + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling) + val person = tilfeldigPerson() + val vedtak = lagVedtak(behandling) + val andelerFørstegangsbehandling = listOf( + lagAndelTilkjentYtelse( + årMnd("2020-01"), + årMnd("2029-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + periodeIdOffset = 0, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + årMnd("2030-01"), + årMnd("2034-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + periodeIdOffset = 1, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + årMnd("2035-01"), + årMnd("2039-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + periodeIdOffset = 2, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + ) + tilkjentYtelse.andelerTilkjentYtelse.addAll(andelerFørstegangsbehandling) + tilkjentYtelse.utbetalingsoppdrag = "Oppdrag" + + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak, + true, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerFørstegangsbehandling.forIverksetting(), + ), + ) + avsluttOgLagreBehandling(behandling) + val behandling2 = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val tilkjentYtelse2 = lagInitiellTilkjentYtelse(behandling2) + val vedtak2 = lagVedtak(behandling2) + val andelerRevurdering = listOf( + lagAndelTilkjentYtelse( + årMnd("2020-01"), + årMnd("2029-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling2, + periodeIdOffset = 0, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + lagAndelTilkjentYtelse( + årMnd("2030-01"), + årMnd("2034-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling2, + periodeIdOffset = 3, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse2, + ), + lagAndelTilkjentYtelse( + årMnd("2035-01"), + årMnd("2038-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling2, + periodeIdOffset = 4, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse2, + ), + ) + tilkjentYtelse2.andelerTilkjentYtelse.addAll(andelerRevurdering) + + val sisteAndelPerIdent = beregningService.hentSisteAndelPerIdent(behandling2.fagsak.id) + + val utbetalingsoppdrag = + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak2, + false, + forrigeKjeder = ØkonomiUtils.grupperAndeler( + andelerFørstegangsbehandling.forIverksetting(), + ), + sisteAndelPerIdent = sisteAndelPerIdent, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerRevurdering.forIverksetting(), + ), + erSimulering = true, + ) + + assertEquals(Utbetalingsoppdrag.KodeEndring.ENDR, utbetalingsoppdrag.kodeEndring) + assertEquals(4, utbetalingsoppdrag.utbetalingsperiode.size) + + val opphørsperiode = utbetalingsoppdrag.utbetalingsperiode.find { it.opphør != null } + assertNotNull(opphørsperiode) + val nyeUtbetalingsPerioderSortert = + utbetalingsoppdrag.utbetalingsperiode.filter { it.opphør == null }.sortedBy { it.vedtakdatoFom } + assertEquals(3, nyeUtbetalingsPerioderSortert.size) + + assertUtbetalingsperiode( + opphørsperiode!!, + 2, + 1, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2035-01-01", + "2039-12-31", + dato("2020-01-01"), + ) + assertUtbetalingsperiode( + nyeUtbetalingsPerioderSortert.first(), + 3, + 2, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2020-01-01", + "2029-12-31", + ) + assertUtbetalingsperiode( + nyeUtbetalingsPerioderSortert[1], + 4, + 3, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2030-01-01", + "2034-12-31", + ) + assertUtbetalingsperiode( + nyeUtbetalingsPerioderSortert.last(), + 5, + 4, + fagsak.aktør.aktivFødselsnummer(), + 1054, + "2035-01-01", + "2038-12-31", + ) + } + + @Test + fun `Skal teste uthenting av offset på revurderinger`() { + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(randomFnr()) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak, + førsteSteg = StegType.BEHANDLING_AVSLUTTET, + ), + ) + + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling) + val person = tilfeldigPerson() + val vedtak = lagVedtak(behandling) + val andelerFørstegangsbehandling = listOf( + lagAndelTilkjentYtelse( + årMnd("2020-01"), + årMnd("2029-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + periodeIdOffset = 0, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + ) + tilkjentYtelse.andelerTilkjentYtelse.addAll(andelerFørstegangsbehandling) + tilkjentYtelse.utbetalingsoppdrag = "Oppdrag" + + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak, + true, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerFørstegangsbehandling.forIverksetting(), + ), + ) + + avsluttOgLagreBehandling(behandling) + val behandling2 = behandlingService.lagreNyOgDeaktiverGammelBehandling( + ( + lagBehandling( + fagsak, + førsteSteg = StegType.BEHANDLING_AVSLUTTET, + ) + ), + ) + val tilkjentYtelse2 = lagInitiellTilkjentYtelse(behandling2) + val andelerRevurdering = emptyList() + tilkjentYtelse2.andelerTilkjentYtelse.addAll(andelerRevurdering) + tilkjentYtelse2.utbetalingsoppdrag = "Oppdrag" + + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak, + false, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerRevurdering.forIverksetting(), + ), + ) + + avsluttOgLagreBehandling(behandling2) + val behandling3 = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val tilkjentYtelse3 = lagInitiellTilkjentYtelse(behandling3) + val andelerRevurdering2 = listOf( + lagAndelTilkjentYtelse( + årMnd("2020-01"), + årMnd("2029-12"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling3, + periodeIdOffset = 0, + person = person, + aktør = personidentService.hentOgLagreAktør(person.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = tilkjentYtelse, + ), + ) + tilkjentYtelse3.andelerTilkjentYtelse.addAll(andelerRevurdering2) + + assertEquals( + 0, + beregningService.hentSisteAndelPerIdent(behandling3.fagsak.id).maxOf { it.value.periodeOffset!! }, + ) + } + + @Test + fun `Skal opphøre tideligere utbetaling hvis barnet ikke har utbetaling i den nye behandlingen`() { + val søker = tilfeldigPerson() + val førsteBarnet = tilfeldigPerson() + val andreBarnet = tilfeldigPerson() + + val fagsak = + fagsakService.hentEllerOpprettFagsakForPersonIdent(søker.aktør.aktivFødselsnummer()) + val førsteBehandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val førsteVedtak = lagVedtak(behandling = førsteBehandling) + + val førsteTilkjentYtelse = lagInitiellTilkjentYtelse(førsteBehandling) + val førsteAndelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2023-03"), + YtelseType.ORDINÆR_BARNETRYGD, + 1345, + førsteBehandling, + person = førsteBarnet, + aktør = personidentService.hentOgLagreAktør(førsteBarnet.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = førsteTilkjentYtelse, + ), + lagAndelTilkjentYtelse( + årMnd("2023-04"), + årMnd("2027-06"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + førsteBehandling, + person = førsteBarnet, + aktør = personidentService.hentOgLagreAktør(førsteBarnet.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = førsteTilkjentYtelse, + ), + ) + førsteTilkjentYtelse.andelerTilkjentYtelse.addAll(førsteAndelerTilkjentYtelse) + førsteTilkjentYtelse.utbetalingsoppdrag = "utbetalingsoppdrg" + tilkjentYtelseRepository.saveAndFlush(førsteTilkjentYtelse) + + utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + førsteVedtak, + "Z123", + AndelTilkjentYtelseForIverksettingFactory(), + ) + avsluttOgLagreBehandling(førsteBehandling) + + val andreBehandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak, + behandlingType = BehandlingType.REVURDERING, + ), + ) + val andreVedtak = lagVedtak(behandling = andreBehandling) + + val andreTilkjentYtelse = lagInitiellTilkjentYtelse(andreBehandling) + val andreAndelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse( + årMnd("2019-04"), + årMnd("2023-03"), + YtelseType.ORDINÆR_BARNETRYGD, + 1345, + andreBehandling, + person = andreBarnet, + aktør = personidentService.hentOgLagreAktør(andreBarnet.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = andreTilkjentYtelse, + ), + lagAndelTilkjentYtelse( + årMnd("2023-04"), + årMnd("2027-06"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + andreBehandling, + person = andreBarnet, + aktør = personidentService.hentOgLagreAktør(andreBarnet.aktør.aktivFødselsnummer(), true), + tilkjentYtelse = andreTilkjentYtelse, + ), + ) + andreTilkjentYtelse.andelerTilkjentYtelse.addAll(andreAndelerTilkjentYtelse) + tilkjentYtelseRepository.saveAndFlush(andreTilkjentYtelse) + + val utbetalingsoppdrag = utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + andreVedtak, + "Z123", + AndelTilkjentYtelseForIverksettingFactory(), + ) + assertEquals(Utbetalingsoppdrag.KodeEndring.ENDR, utbetalingsoppdrag.kodeEndring) + assertEquals(3, utbetalingsoppdrag.utbetalingsperiode.size) + assertEquals(true, utbetalingsoppdrag.utbetalingsperiode.first().erEndringPåEksisterendePeriode) + assertEquals(Opphør(YearMonth.of(2019, 4).toLocalDate()), utbetalingsoppdrag.utbetalingsperiode.first().opphør) + assertEquals(0, utbetalingsoppdrag.utbetalingsperiode.first().forrigePeriodeId) + assertEquals(false, utbetalingsoppdrag.utbetalingsperiode[1].erEndringPåEksisterendePeriode) + assertNull(utbetalingsoppdrag.utbetalingsperiode[1].opphør) + assertNull(utbetalingsoppdrag.utbetalingsperiode[1].forrigePeriodeId) + } + + @Test + fun `skal opprette et nytt utbetalingsoppdrag for institusjon`() { + val tilfeldigPerson = tilfeldigPerson() + val fagsak = + fagsakService.hentEllerOpprettFagsakForPersonIdent( + tilfeldigPerson.aktør.aktivFødselsnummer(), + fagsakType = FagsakType.INSTITUSJON, + institusjon = InstitusjonInfo(ORGNUMMER, TSS_ID_INSTITUSJON), + ) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val vedtak = lagVedtak(behandling = behandling) + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling) + val andelerTilkjentYtelse = listOf( + lagAndelTilkjentYtelse( + årMnd("2019-03"), + årMnd("2037-02"), + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + tilkjentYtelse = tilkjentYtelse, + person = tilfeldigPerson, + aktør = personidentService.hentOgLagreAktør(tilfeldigPerson.aktør.aktivFødselsnummer(), true), + ), + ) + tilkjentYtelse.andelerTilkjentYtelse.addAll(andelerTilkjentYtelse) + + val utbetalingsoppdrag = + utbetalingsoppdragGenerator.lagUtbetalingsoppdragOgOppdaterTilkjentYtelse( + "saksbehandler", + vedtak, + true, + oppdaterteKjeder = ØkonomiUtils.grupperAndeler( + andelerTilkjentYtelse.forIverksetting(), + ), + ) + + assertEquals(Utbetalingsoppdrag.KodeEndring.NY, utbetalingsoppdrag.kodeEndring) + assertEquals(1, utbetalingsoppdrag.utbetalingsperiode.size) + + val utbetalingsperioderPerKlasse = utbetalingsoppdrag.utbetalingsperiode.groupBy { it.klassifisering } + assertUtbetalingsperiode( + utbetalingsperioderPerKlasse.getValue("BATR")[0], + 0, + null, + TSS_ID_INSTITUSJON, + 1054, + "2019-03-01", + "2037-02-28", + ) + } + + @Nested + inner class SisteAndelIKjeden { + + val søker = tilfeldigPerson() + + lateinit var fagsak: Fagsak + lateinit var førsteBehandling: Behandling + lateinit var førsteVedtak: Vedtak + lateinit var aktørSøker: Aktør + + val fom = årMnd("2019-04") + val tom = årMnd("2019-05") + val fom2 = årMnd("2019-06") + val tom2 = årMnd("2020-05") + + @BeforeEach + fun setUp() { + fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søker.aktør.aktivFødselsnummer()) + førsteBehandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + førsteVedtak = lagVedtak(behandling = førsteBehandling) + aktørSøker = personidentService.hentOgLagreAktør(søker.aktør.aktivFødselsnummer(), true) + } + + @Test + fun `skal hente siste andelene per ident og ytelsestype`() { + with(lagInitiellTilkjentYtelse(førsteBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndel(this, fom, tom), + lagAndel(this, fom, tom, YtelseType.UTVIDET_BARNETRYGD, 1054), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + + genererUtbetalingsoppdragOgOppdaterTilkjentYtelse(førsteVedtak) + avsluttOgLagreBehandling(førsteBehandling) + + val andreBehandling = opprettRevurdering() + val andreVedtak = lagVedtak(behandling = andreBehandling) + + with(lagInitiellTilkjentYtelse(andreBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndel(this, fom, tom), + lagAndel(this, fom2, tom2), + lagAndel(this, fom, tom, YtelseType.UTVIDET_BARNETRYGD, 1054), + lagAndel(this, fom2, tom2, YtelseType.UTVIDET_BARNETRYGD, 1054), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + + val utbetalingsoppdrag = genererUtbetalingsoppdragOgOppdaterTilkjentYtelse(andreVedtak) + assertThat(utbetalingsoppdrag.kodeEndring).isEqualTo(Utbetalingsoppdrag.KodeEndring.ENDR) + assertThat(utbetalingsoppdrag.utbetalingsperiode).hasSize(2) + assertThat(utbetalingsoppdrag.utbetalingsperiode.map { it.erEndringPåEksisterendePeriode }) + .containsOnly(false) + with(utbetalingsoppdrag.utbetalingsperiode[0]) { + assertThat(periodeId).isEqualTo(2) + assertThat(forrigePeriodeId).isEqualTo(0) + assertThat(sats.toInt()).isEqualTo(1345) + } + with(utbetalingsoppdrag.utbetalingsperiode[1]) { + assertThat(periodeId).isEqualTo(3) + assertThat(forrigePeriodeId).isEqualTo(1) + assertThat(sats.toInt()).isEqualTo(1054) + } + } + + @Test + fun `flere ytelestyper per person`() { + val barn = tilfeldigPerson() + val aktørBarn = personidentService.hentOgLagreAktør(barn.aktør.aktivFødselsnummer(), true) + + with(lagInitiellTilkjentYtelse(førsteBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndelTilkjentYtelse( + fom, + tom, + YtelseType.SMÅBARNSTILLEGG, + 1, + behandling, + søker, + aktørSøker, + tilkjentYtelse = this, + ), + lagAndelTilkjentYtelse( + fom, + tom, + YtelseType.UTVIDET_BARNETRYGD, + 2, + behandling, + søker, + aktørSøker, + tilkjentYtelse = this, + ), + lagAndelTilkjentYtelse( + fom, + tom, + YtelseType.ORDINÆR_BARNETRYGD, + 3, + behandling, + barn, + aktørBarn, + tilkjentYtelse = this, + ), + lagAndelTilkjentYtelse( + fom, + tom, + YtelseType.UTVIDET_BARNETRYGD, + 4, + behandling, + barn, + aktørBarn, + tilkjentYtelse = this, + ), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + val utbetalingsoppdrag = genererUtbetalingsoppdragOgOppdaterTilkjentYtelse(førsteVedtak) + avsluttOgLagreBehandling(førsteBehandling) + assertThat(utbetalingsoppdrag.utbetalingsperiode).hasSize(4) + assertThat(utbetalingsoppdrag.utbetalingsperiode.map { it.forrigePeriodeId }) + .`as`("Alle utbetalingsperioder skal peke mot null i forrigePeriodeId") + .containsOnly(null) + + val revurdering = opprettRevurdering() + + with(lagInitiellTilkjentYtelse(revurdering, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndelTilkjentYtelse( + fom, + tom, + YtelseType.SMÅBARNSTILLEGG, + 2, + revurdering, + søker, + aktørSøker, + tilkjentYtelse = this, + ), + lagAndelTilkjentYtelse( + fom, + tom, + YtelseType.UTVIDET_BARNETRYGD, + 3, + revurdering, + søker, + aktørSøker, + tilkjentYtelse = this, + ), + lagAndelTilkjentYtelse( + fom, + tom, + YtelseType.ORDINÆR_BARNETRYGD, + 4, + revurdering, + barn, + aktørBarn, + tilkjentYtelse = this, + ), + lagAndelTilkjentYtelse( + fom, + tom, + YtelseType.UTVIDET_BARNETRYGD, + 5, + revurdering, + barn, + aktørBarn, + tilkjentYtelse = this, + ), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + val utbetalingsoppdrag2 = genererUtbetalingsoppdragOgOppdaterTilkjentYtelse(lagVedtak(revurdering)) + val opphørsperioder = utbetalingsoppdrag2.utbetalingsperiode.filter { it.erEndringPåEksisterendePeriode } + val nyePerioder = utbetalingsoppdrag2.utbetalingsperiode.filterNot { it.erEndringPåEksisterendePeriode } + assertThat(opphørsperioder).hasSize(4) + assertThat(nyePerioder).hasSize(4) + assertUtbetalingsperiode(nyePerioder[0], 4, 0, aktørSøker, 2, fom, tom) + assertUtbetalingsperiode(nyePerioder[1], 5, 1, aktørSøker, 3, fom, tom) + assertUtbetalingsperiode(nyePerioder[2], 6, 2, aktørSøker, 4, fom, tom) + assertUtbetalingsperiode(nyePerioder[3], 7, 3, aktørSøker, 5, fom, tom) + } + + @Test + fun `skal alltid peke til siste andelen i kjeden ved opphør, selv opphør etter opphør`() { + fun assertHarKunOpphør(utbetalingsoppdrag: Utbetalingsoppdrag, opphørFom: YearMonth) { + assertThat(utbetalingsoppdrag.kodeEndring).isEqualTo(Utbetalingsoppdrag.KodeEndring.ENDR) + assertThat(utbetalingsoppdrag.utbetalingsperiode).hasSize(1) + with(utbetalingsoppdrag.utbetalingsperiode[0]) { + assertThat(erEndringPåEksisterendePeriode).isTrue() + assertThat(opphør!!.opphørDatoFom).isEqualTo(opphørFom.atDay(1)) + assertThat(periodeId).isEqualTo(1L) + assertThat(forrigePeriodeId).isEqualTo(0L) + assertThat(vedtakdatoFom).isEqualTo(fom2.atDay(1)) + assertThat(vedtakdatoTom).isEqualTo(tom2.atEndOfMonth()) + assertThat(sats.toInt()).isEqualTo(1345) + } + } + + with(lagInitiellTilkjentYtelse(førsteBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndel(this, fom = fom, tom = tom), + lagAndel(this, fom = fom2, tom = tom2), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + + genererUtbetalingsoppdragOgOppdaterTilkjentYtelse(lagVedtak(behandling = førsteBehandling)) + avsluttOgLagreBehandling(førsteBehandling) + + val andreBehandling = opprettRevurdering() + + with(lagInitiellTilkjentYtelse(andreBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + andelerTilkjentYtelse.add(lagAndel(this, fom = fom, tom = tom)) + tilkjentYtelseRepository.saveAndFlush(this) + } + + val utbetalingsoppdrag = genererUtbetalingsoppdragOgOppdaterTilkjentYtelse(lagVedtak(andreBehandling)) + assertHarKunOpphør(utbetalingsoppdrag, fom2) + + avsluttOgLagreBehandling(andreBehandling) + val tredjeBehandling = opprettRevurdering() + with(lagInitiellTilkjentYtelse(tredjeBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + tilkjentYtelseRepository.saveAndFlush(this) + } + with(genererUtbetalingsoppdragOgOppdaterTilkjentYtelse(lagVedtak(behandling = tredjeBehandling))) { + assertHarKunOpphør(this, fom) + } + } + + @Test + fun `ny andel etter opphør skal peke til siste andelen`() { + with(lagInitiellTilkjentYtelse(førsteBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndel(this, fom = fom, tom = tom), + lagAndel(this, fom = fom2, tom = tom2), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + + genererUtbetalingsoppdragOgOppdaterTilkjentYtelse(lagVedtak(behandling = førsteBehandling)) + avsluttOgLagreBehandling(førsteBehandling) + + val andreBehandling = opprettRevurdering() + + with(lagInitiellTilkjentYtelse(andreBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + andelerTilkjentYtelse.add(lagAndel(this, fom = fom, tom = tom)) + tilkjentYtelseRepository.saveAndFlush(this) + } + genererUtbetalingsoppdragOgOppdaterTilkjentYtelse(lagVedtak(behandling = andreBehandling)) + + avsluttOgLagreBehandling(andreBehandling) + val tredjeBehandling = opprettRevurdering() + with(lagInitiellTilkjentYtelse(tredjeBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndel(this, fom = fom, tom = tom), + lagAndel(this, fom = fom2, tom = tom2), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + with(genererUtbetalingsoppdragOgOppdaterTilkjentYtelse(lagVedtak(behandling = tredjeBehandling))) { + assertThat(kodeEndring).isEqualTo(Utbetalingsoppdrag.KodeEndring.ENDR) + assertThat(utbetalingsperiode).hasSize(1) + with(utbetalingsperiode[0]) { + assertThat(erEndringPåEksisterendePeriode).isFalse() + assertThat(opphør).isNull() + assertThat(periodeId).isEqualTo(2L) + assertThat(forrigePeriodeId).isEqualTo(1L) + assertThat(vedtakdatoFom).isEqualTo(fom2.atDay(1)) + assertThat(vedtakdatoTom).isEqualTo(tom2.atEndOfMonth()) + assertThat(sats.toInt()).isEqualTo(1345) + } + } + } + + fun lagAndel( + tilkjentYtelse: TilkjentYtelse, + fom: YearMonth, + tom: YearMonth, + type: YtelseType = YtelseType.SMÅBARNSTILLEGG, + beløp: Int = 1345, + aktør: Aktør? = null, + person: Person? = null, + ): AndelTilkjentYtelse = + lagAndelTilkjentYtelse( + fom = fom, + tom = tom, + ytelseType = type, + beløp = beløp, + behandling = tilkjentYtelse.behandling, + person = person ?: søker, + aktør = aktør ?: aktørSøker, + tilkjentYtelse = tilkjentYtelse, + ) + + private fun opprettRevurdering() = opprettRevurdering(fagsak) + } + + private fun opprettRevurdering(fagsak: Fagsak) = + behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling(fagsak, behandlingType = BehandlingType.REVURDERING), + ) + + private fun genererUtbetalingsoppdragOgOppdaterTilkjentYtelse(vedtak: Vedtak): Utbetalingsoppdrag { + return utbetalingsoppdragGeneratorService.genererUtbetalingsoppdragOgOppdaterTilkjentYtelse( + vedtak, + "Z123", + AndelTilkjentYtelseForIverksettingFactory(), + ) + } + + private fun avsluttOgLagreBehandling(behandling: Behandling) { + behandling.status = BehandlingStatus.AVSLUTTET + behandling.leggTilBehandlingStegTilstand(StegType.BEHANDLING_AVSLUTTET) + behandlingHentOgPersisterService.lagreEllerOppdater(behandling, false) + } + + private fun assertUtbetalingsperiode( + utbetalingsperiode: Utbetalingsperiode, + periodeId: Long, + forrigePeriodeId: Long?, + utbetalesTil: Aktør, + sats: Int, + fom: YearMonth, + tom: YearMonth, + opphørFom: LocalDate? = null, + ) = assertUtbetalingsperiode( + utbetalingsperiode, + periodeId, + forrigePeriodeId, + utbetalesTil.aktivFødselsnummer(), + sats, + fom.atDay(1).toString(), + tom.atEndOfMonth().toString(), + opphørFom, + ) + + private fun assertUtbetalingsperiode( + utbetalingsperiode: Utbetalingsperiode, + periodeId: Long, + forrigePeriodeId: Long?, + utbetalesTil: String, + sats: Int, + fom: String, + tom: String, + opphørFom: LocalDate? = null, + ) { + assertEquals(periodeId, utbetalingsperiode.periodeId) + assertEquals(forrigePeriodeId, utbetalingsperiode.forrigePeriodeId) + assertEquals(sats, utbetalingsperiode.sats.toInt()) + assertEquals(dato(fom), utbetalingsperiode.vedtakdatoFom) + assertEquals(dato(tom), utbetalingsperiode.vedtakdatoTom) + if (opphørFom != null) { + assertEquals(opphørFom, utbetalingsperiode.opphør?.opphørDatoFom) + } + assertEquals(utbetalesTil, utbetalingsperiode.utbetalesTil) + } + + companion object { + private const val TSS_ID_INSTITUSJON = "80000" + private const val ORGNUMMER = "987654321" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiIntegrasjonTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiIntegrasjonTest.kt" new file mode 100644 index 000000000..a778770cd --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/integrasjoner/\303\270konomi/\303\230konomiIntegrasjonTest.kt" @@ -0,0 +1,196 @@ +package no.nav.familie.ba.sak.integrasjoner.økonomi + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.simulering.SimuleringService +import no.nav.familie.ba.sak.kjerne.vedtak.Vedtak +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.LocalDateTime + +class ØkonomiIntegrasjonTest( + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @Autowired + private val vilkårsvurderingService: VilkårsvurderingService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + + @Autowired + private val økonomiService: ØkonomiService, + + @Autowired + private val beregningService: BeregningService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val vedtakService: VedtakService, + + @Autowired + private val simuleringService: SimuleringService, +) : AbstractSpringIntegrationTest() { + + @Test + @Tag("integration") + fun `Iverksett vedtak på aktiv behandling`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val stønadFom = LocalDate.now() + val stønadTom = stønadFom.plusYears(17) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val barnAktørId = personidentService.hentOgLagreAktør(barnFnr, true) + + val vilkårsvurdering = + lagVilkårsvurdering(behandling, fagsak.aktør, barnAktørId, stønadFom, stønadTom) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = vilkårsvurdering) + Assertions.assertNotNull(behandling.fagsak.id) + + val barnAktør = personidentService.hentAktørIder(listOf(barnFnr)) + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = fagsak.aktør, + barnAktør = barnAktør, + ) + personopplysningGrunnlagRepository.save(personopplysningGrunnlag) + + behandlingService.opprettOgInitierNyttVedtakForBehandling(behandling = behandling) + + val vedtak = vedtakService.hentAktivForBehandling(behandlingId = behandling.id) + Assertions.assertNotNull(vedtak) + vedtak!!.vedtaksdato = LocalDateTime.now() + vedtakService.oppdater(vedtak) + + beregningService.oppdaterBehandlingMedBeregning(behandling, personopplysningGrunnlag) + + assertDoesNotThrow { + økonomiService.oppdaterTilkjentYtelseMedUtbetalingsoppdragOgIverksett( + vedtak, + "ansvarligSaksbehandler", + AndelTilkjentYtelseForIverksettingFactory(), + ) + } + } + + @Test + @Tag("integration") + fun `Hent behandlinger for løpende fagsaker til konsistensavstemming mot økonomi`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val stønadFom = LocalDate.now() + val stønadTom = stønadFom.plusYears(17) + + // Lag fagsak med behandling og personopplysningsgrunnlag og Iverksett. + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val barnAktørId = personidentService.hentAktør(barnFnr) + + val vedtak = Vedtak( + behandling = behandling, + vedtaksdato = LocalDateTime.of(2020, 1, 1, 4, 35), + ) + + val barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true) + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = fagsak.aktør, + barnAktør = barnAktør, + ) + personopplysningGrunnlagRepository.save(personopplysningGrunnlag) + behandlingService.opprettOgInitierNyttVedtakForBehandling(behandling) + + val vilkårsvurdering = + lagVilkårsvurdering(behandling, fagsak.aktør, barnAktørId, stønadFom, stønadTom) + vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = vilkårsvurdering) + + beregningService.oppdaterBehandlingMedBeregning(behandling, personopplysningGrunnlag) + + økonomiService.oppdaterTilkjentYtelseMedUtbetalingsoppdragOgIverksett( + vedtak, + "ansvarligSaksbehandler", + AndelTilkjentYtelseForIverksettingFactory(), + ) + behandlingService.oppdaterStatusPåBehandling(behandling.id, BehandlingStatus.AVSLUTTET) + + fagsak.status = FagsakStatus.LØPENDE + fagsakService.lagre(fagsak) + + val behandlingerMedAndelerTilAvstemming = + behandlingHentOgPersisterService.hentSisteIverksatteBehandlingerFraLøpendeFagsaker() + + Assertions.assertTrue(behandlingerMedAndelerTilAvstemming.contains(behandling.id)) + } + + private fun lagVilkårsvurdering( + behandling: Behandling, + søkerAktør: Aktør, + barnAktør: Aktør, + stønadFom: LocalDate, + stønadTom: LocalDate, + ): Vilkårsvurdering { + val vilkårsvurdering = + Vilkårsvurdering(behandling = behandling) + vilkårsvurdering.personResultater = setOf( + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson(type = PersonType.SØKER, aktør = søkerAktør), + resultat = Resultat.OPPFYLT, + periodeFom = stønadFom, + periodeTom = stønadTom, + lagFullstendigVilkårResultat = true, + personType = PersonType.SØKER, + ), + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson(type = PersonType.BARN, aktør = barnAktør, fødselsdato = stønadFom), + resultat = Resultat.OPPFYLT, + periodeFom = stønadFom, + periodeTom = stønadTom, + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + ), + ) + return vilkårsvurdering + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/ArbeidsfordelingIntegrationTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/ArbeidsfordelingIntegrationTest.kt new file mode 100644 index 000000000..18bd2b4f9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/arbeidsfordeling/ArbeidsfordelingIntegrationTest.kt @@ -0,0 +1,409 @@ +package no.nav.familie.ba.sak.kjerne.arbeidsfordeling + +import io.mockk.every +import io.mockk.slot +import io.mockk.verify +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.domene.Arbeidsfordelingsenhet +import no.nav.familie.ba.sak.integrasjoner.oppgave.OppgaveService +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.kontrakter.felles.navkontor.NavKontorEnhet +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import no.nav.familie.kontrakter.felles.personopplysning.Sivilstand +import no.nav.familie.kontrakter.felles.personopplysning.Vegadresse +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate.now + +class ArbeidsfordelingIntegrationTest( + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val stegService: StegService, + + @Autowired + private val arbeidsfordelingService: ArbeidsfordelingService, + + @Autowired + private val integrasjonClient: IntegrasjonClient, + + @Autowired + private val oppgaveService: OppgaveService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val mockPersonopplysningerService: PersonopplysningerService, +) : AbstractSpringIntegrationTest() { + + @BeforeEach + fun init() { + databaseCleanupService.truncate() + + val now = now() + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(SØKER_FNR)) + } returns PersonInfo( + fødselsdato = now.minusYears(20), + navn = "Mor Søker", + kjønn = Kjønn.KVINNE, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UGIFT)), + adressebeskyttelseGradering = ADRESSEBESKYTTELSEGRADERING.UGRADERT, + bostedsadresser = mutableListOf(søkerBostedsadresse), + ) + + every { + mockPersonopplysningerService.hentPersoninfoEnkel(tilAktør(SØKER_FNR)) + } returns PersonInfo( + fødselsdato = now.minusYears(20), + navn = "Mor Søker", + kjønn = Kjønn.KVINNE, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UGIFT)), + adressebeskyttelseGradering = ADRESSEBESKYTTELSEGRADERING.UGRADERT, + bostedsadresser = mutableListOf(søkerBostedsadresse), + ) + + every { + mockPersonopplysningerService.hentPersoninfoEnkel(tilAktør(BARN_UTEN_DISKRESJONSKODE)) + } returns PersonInfo( + fødselsdato = now.førsteDagIInneværendeMåned(), + navn = "Gutt Barn", + kjønn = Kjønn.MANN, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UGIFT)), + adressebeskyttelseGradering = ADRESSEBESKYTTELSEGRADERING.UGRADERT, + bostedsadresser = mutableListOf(søkerBostedsadresse), + ) + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon( + tilAktør( + BARN_UTEN_DISKRESJONSKODE, + ), + ) + } returns PersonInfo( + fødselsdato = now.førsteDagIInneværendeMåned(), + navn = "Gutt Barn", + kjønn = Kjønn.MANN, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UGIFT)), + forelderBarnRelasjon = setOf( + ForelderBarnRelasjon( + aktør = tilAktør(SØKER_FNR), + relasjonsrolle = FORELDERBARNRELASJONROLLE.MOR, + ), + ), + adressebeskyttelseGradering = ADRESSEBESKYTTELSEGRADERING.UGRADERT, + bostedsadresser = mutableListOf(søkerBostedsadresse), + ) + + every { + mockPersonopplysningerService.hentPersoninfoEnkel(tilAktør(BARN_MED_DISKRESJONSKODE)) + } returns PersonInfo( + fødselsdato = now.førsteDagIInneværendeMåned(), + navn = "Gutt Barn fortrolig", + kjønn = Kjønn.MANN, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UGIFT)), + adressebeskyttelseGradering = ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG, + bostedsadresser = mutableListOf(søkerBostedsadresse), + ) + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon( + tilAktør( + BARN_MED_DISKRESJONSKODE, + ), + ) + } returns PersonInfo( + fødselsdato = now.førsteDagIInneværendeMåned(), + navn = "Gutt Barn fortrolig", + kjønn = Kjønn.MANN, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UGIFT)), + forelderBarnRelasjon = setOf( + ForelderBarnRelasjon( + aktør = tilAktør(SØKER_FNR), + relasjonsrolle = FORELDERBARNRELASJONROLLE.MOR, + ), + ), + adressebeskyttelseGradering = ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG, + bostedsadresser = mutableListOf(søkerBostedsadresse), + ) + + every { integrasjonClient.hentBehandlendeEnhet(eq(SØKER_FNR)) } returns listOf( + Arbeidsfordelingsenhet( + enhetId = IKKE_FORTROLIG_ENHET, + enhetNavn = "vanlig enhet", + ), + ) + + every { integrasjonClient.hentBehandlendeEnhet(eq(BARN_MED_DISKRESJONSKODE)) } returns listOf( + Arbeidsfordelingsenhet( + enhetId = FORTROLIG_ENHET, + enhetNavn = "Diskresjonsenhet", + ), + ) + val hentEnhetSlot = slot() + every { integrasjonClient.hentEnhet(capture(hentEnhetSlot)) } answers { + NavKontorEnhet( + enhetId = hentEnhetSlot.captured.toInt(), + navn = "${hentEnhetSlot.captured}, NAV Familie- og pensjonsytelser Oslo 1", + enhetNr = "4848", + status = "aktiv", + ) + } + } + + @Test + fun `Skal fastsette behandlende enhet ved opprettelse av behandling`() { + val søkerAktør = personidentService.hentAktør(SØKER_FNR) + val fagsak = fagsakService.hentEllerOpprettFagsak(søkerAktør.aktivFødselsnummer()) + val behandling = stegService.håndterNyBehandling( + lagNyBehandling(fagsak.id), + ) + + val arbeidsfordelingPåBehandling = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id) + + assertEquals(IKKE_FORTROLIG_ENHET, arbeidsfordelingPåBehandling.behandlendeEnhetId) + } + + @Test + fun `Skal ikke fastsette ny behandlende enhet ved registrering av søknad`() { + val søkerAktør = personidentService.hentAktør(SØKER_FNR) + val fagsak = fagsakService.hentEllerOpprettFagsak(søkerAktør.aktivFødselsnummer()) + val behandling = stegService.håndterNyBehandling( + lagNyBehandling(fagsak.id), + ) + + val arbeidsfordelingPåBehandling = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id) + assertEquals(IKKE_FORTROLIG_ENHET, arbeidsfordelingPåBehandling.behandlendeEnhetId) + + stegService.håndterSøknad( + behandling, + RestRegistrerSøknad( + søknad = lagSøknadDTO( + SØKER_FNR, + listOf(BARN_UTEN_DISKRESJONSKODE), + ), + bekreftEndringerViaFrontend = false, + ), + ) + + val arbeidsfordelingPåBehandlingEtterSøknadsregistrering = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id) + assertEquals(IKKE_FORTROLIG_ENHET, arbeidsfordelingPåBehandlingEtterSøknadsregistrering.behandlendeEnhetId) + } + + @Test + fun `Skal fastsette ny behandlende enhet ved registrering av søknad`() { + val søkerAktør = personidentService.hentAktør(SØKER_FNR) + val fagsak = fagsakService.hentEllerOpprettFagsak(søkerAktør.aktivFødselsnummer()) + val behandling = stegService.håndterNyBehandling( + lagNyBehandling(fagsak.id), + ) + + val arbeidsfordelingPåBehandling = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id) + assertEquals(IKKE_FORTROLIG_ENHET, arbeidsfordelingPåBehandling.behandlendeEnhetId) + + stegService.håndterSøknad( + behandling, + RestRegistrerSøknad( + søknad = lagSøknadDTO( + SØKER_FNR, + listOf(BARN_MED_DISKRESJONSKODE), + ), + bekreftEndringerViaFrontend = false, + ), + ) + + val arbeidsfordelingPåBehandlingEtterSøknadsregistrering = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id) + assertEquals(FORTROLIG_ENHET, arbeidsfordelingPåBehandlingEtterSøknadsregistrering.behandlendeEnhetId) + } + + @Test + fun `Skal fastsette ny behandlende enhet når man legger til nytt barn ved endring på søknadsgrunnlag`() { + val søkerAktør = personidentService.hentAktør(SØKER_FNR) + val fagsak = fagsakService.hentEllerOpprettFagsak(søkerAktør.aktivFødselsnummer()) + val behandling = stegService.håndterNyBehandling( + lagNyBehandling(fagsak.id), + ) + + val arbeidsfordelingPåBehandling = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id) + assertEquals(IKKE_FORTROLIG_ENHET, arbeidsfordelingPåBehandling.behandlendeEnhetId) + + stegService.håndterSøknad( + behandling, + RestRegistrerSøknad( + søknad = lagSøknadDTO( + SØKER_FNR, + listOf(BARN_UTEN_DISKRESJONSKODE), + ), + bekreftEndringerViaFrontend = false, + ), + ) + + val arbeidsfordelingPåBehandlingEtterSøknadsregistreringUtenDiskresjonskode = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id) + assertEquals( + IKKE_FORTROLIG_ENHET, + arbeidsfordelingPåBehandlingEtterSøknadsregistreringUtenDiskresjonskode.behandlendeEnhetId, + ) + + stegService.håndterSøknad( + behandling, + RestRegistrerSøknad( + søknad = lagSøknadDTO( + SØKER_FNR, + listOf( + BARN_UTEN_DISKRESJONSKODE, + BARN_MED_DISKRESJONSKODE, + ), + ), + bekreftEndringerViaFrontend = false, + ), + ) + + val arbeidsfordelingPåBehandlingEtterSøknadsregistreringMedDiskresjonskode = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id) + assertEquals( + FORTROLIG_ENHET, + arbeidsfordelingPåBehandlingEtterSøknadsregistreringMedDiskresjonskode.behandlendeEnhetId, + ) + } + + @Test + fun `Skal ikke fastsette ny behandlende enhet ved registrering av søknad når enhet er manuelt satt`() { + val søkerAktør = personidentService.hentAktør(SØKER_FNR) + val fagsak = fagsakService.hentEllerOpprettFagsak(søkerAktør.aktivFødselsnummer()) + val behandling = stegService.håndterNyBehandling( + lagNyBehandling(fagsak.id), + ) + + val arbeidsfordelingPåBehandling = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id) + assertEquals(IKKE_FORTROLIG_ENHET, arbeidsfordelingPåBehandling.behandlendeEnhetId) + + arbeidsfordelingService.manueltOppdaterBehandlendeEnhet( + behandling, + RestEndreBehandlendeEnhet( + enhetId = MANUELT_OVERSTYRT_ENHET, + begrunnelse = "", + ), + ) + + stegService.håndterSøknad( + behandling, + RestRegistrerSøknad( + søknad = lagSøknadDTO( + SØKER_FNR, + listOf(BARN_UTEN_DISKRESJONSKODE), + ), + bekreftEndringerViaFrontend = false, + ), + ) + + val arbeidsfordelingPåBehandlingEtterSøknadsregistrering = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id) + assertEquals(MANUELT_OVERSTYRT_ENHET, arbeidsfordelingPåBehandlingEtterSøknadsregistrering.behandlendeEnhetId) + } + + @Test + fun `Skal fastsette ny behandlende enhet og oppdatere eksisterende oppgave ved registrering av søknad`() { + val søkerAktør = personidentService.hentAktør(SØKER_FNR) + val fagsak = fagsakService.hentEllerOpprettFagsak(søkerAktør.aktivFødselsnummer()) + val behandling = stegService.håndterNyBehandling( + lagNyBehandling(fagsak.id), + ) + + val arbeidsfordelingPåBehandling = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id) + assertEquals(IKKE_FORTROLIG_ENHET, arbeidsfordelingPåBehandling.behandlendeEnhetId) + + oppgaveService.opprettOppgave(behandling.id, Oppgavetype.BehandleSak, now()) + + stegService.håndterSøknad( + behandling, + RestRegistrerSøknad( + søknad = lagSøknadDTO( + SØKER_FNR, + listOf(BARN_MED_DISKRESJONSKODE), + ), + bekreftEndringerViaFrontend = false, + ), + ) + + verify(exactly = 1) { + integrasjonClient.tilordneEnhetForOppgave(any(), any()) + } + + val arbeidsfordelingPåBehandlingEtterSøknadsregistreringUtenDiskresjonskode = + arbeidsfordelingService.hentArbeidsfordelingPåBehandling(behandlingId = behandling.id) + assertEquals( + FORTROLIG_ENHET, + arbeidsfordelingPåBehandlingEtterSøknadsregistreringUtenDiskresjonskode.behandlendeEnhetId, + ) + } + + private fun lagNyBehandling(fagsakId: Long) = NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + søkersIdent = SØKER_FNR, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + søknadMottattDato = now(), + fagsakId = fagsakId, + ) + + companion object { + + const val MANUELT_OVERSTYRT_ENHET = "1234" + const val IKKE_FORTROLIG_ENHET = "4820" + const val FORTROLIG_ENHET = "1122" + const val SØKER_FNR = "12445678910" + const val BARN_UTEN_DISKRESJONSKODE = "12345678911" + const val BARN_MED_DISKRESJONSKODE = "12345678912" + + val søkerBostedsadresse = Bostedsadresse( + vegadresse = Vegadresse( + matrikkelId = 1111, + husnummer = null, + husbokstav = null, + bruksenhetsnummer = null, + adressenavn = null, + kommunenummer = null, + tilleggsnavn = null, + postnummer = "2222", + ), + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/AutomatiskVilk\303\245rsvurderingIntegrasjonTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/AutomatiskVilk\303\245rsvurderingIntegrasjonTest.kt" new file mode 100644 index 000000000..70db3973c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/AutomatiskVilk\303\245rsvurderingIntegrasjonTest.kt" @@ -0,0 +1,144 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse + +import io.mockk.every +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import no.nav.familie.kontrakter.felles.personopplysning.Sivilstand +import no.nav.familie.kontrakter.felles.personopplysning.Vegadresse +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class AutomatiskVilkårsvurderingIntegrasjonTest( + @Autowired val stegService: StegService, + @Autowired val mockPersonopplysningerService: PersonopplysningerService, + @Autowired val persongrunnlagService: PersongrunnlagService, + @Autowired val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + @Autowired val databaseCleanupService: DatabaseCleanupService, +) : AbstractSpringIntegrationTest() { + + @BeforeEach + fun truncate() { + databaseCleanupService.truncate() + } + + @Test + fun `Ikke bosatt i riket, skal ikke passere vilkår`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val mockSøkerUtenHjem = genererAutomatiskTestperson(bostedsadresser = emptyList()) + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(søkerFnr)) } returns mockSøkerUtenHjem + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(barnFnr)) } returns mockBarnAutomatiskBehandling + + val nyBehandling = NyBehandlingHendelse(søkerFnr, listOf(barnFnr)) + val behandlingFørVilkår = + stegService.opprettNyBehandlingOgRegistrerPersongrunnlagForFødselhendelse(nyBehandling) + val behandlingEtterVilkår = + stegService.håndterVilkårsvurdering(behandlingFørVilkår.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING)) + Assertions.assertEquals(Behandlingsresultat.AVSLÅTT, behandlingEtterVilkår.resultat) + } + + @Test + fun `Barnet er gift, skal ikke passere vilkår`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val mockBarnGift = genererAutomatiskTestperson(sivilstander = listOf(Sivilstand(SIVILSTAND.GIFT))) + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(søkerFnr)) } returns mockSøkerAutomatiskBehandling + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(barnFnr)) } returns mockBarnGift + + val nyBehandling = NyBehandlingHendelse(søkerFnr, listOf(barnFnr)) + val behandlingFørVilkår = + stegService.opprettNyBehandlingOgRegistrerPersongrunnlagForFødselhendelse(nyBehandling) + val behandlingEtterVilkår = + stegService.håndterVilkårsvurdering(behandlingFørVilkår.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING)) + Assertions.assertEquals(Behandlingsresultat.AVSLÅTT, behandlingEtterVilkår.resultat) + } + + @Test + fun `Skal ikke passere vilkårsvurdering dersom barn er over 18`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val barn = genererAutomatiskTestperson(LocalDate.parse("1999-10-10"), emptySet(), emptyList()) + val søker = genererAutomatiskTestperson( + LocalDate.parse("1998-10-10"), + setOf( + ForelderBarnRelasjon( + tilAktør(barnFnr), + FORELDERBARNRELASJONROLLE.BARN, + ), + ), + emptyList(), + ) + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(barnFnr)) } returns barn + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(søkerFnr)) } returns søker + + val nyBehandling = NyBehandlingHendelse(søkerFnr, listOf(barnFnr)) + val behandlingFørVilkår = + stegService.opprettNyBehandlingOgRegistrerPersongrunnlagForFødselhendelse(nyBehandling) + val behandlingEtterVilkår = + stegService.håndterVilkårsvurdering(behandlingFørVilkår.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING)) + Assertions.assertEquals(Behandlingsresultat.AVSLÅTT, behandlingEtterVilkår.resultat) + } + + @Test + fun `Skal ikke passere vilkårsvurdering dersom barn ikke bor med mor`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val barn = genererAutomatiskTestperson(LocalDate.now(), emptySet(), emptyList()) + val søker = genererAutomatiskTestperson( + LocalDate.parse("1998-10-10"), + setOf( + ForelderBarnRelasjon( + tilAktør(barnFnr), + FORELDERBARNRELASJONROLLE.BARN, + ), + ), + emptyList(), + bostedsadresser = listOf( + Bostedsadresse( + gyldigFraOgMed = null, + gyldigTilOgMed = null, + vegadresse = Vegadresse( + matrikkelId = 1111111111, + husnummer = "36", + husbokstav = "D", + bruksenhetsnummer = null, + adressenavn = "IkkeSamme -veien", + kommunenummer = "5423", + tilleggsnavn = null, + postnummer = "9050", + ), + matrikkeladresse = null, + ukjentBosted = null, + ), + ), + ) + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(barnFnr)) } returns barn + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(søkerFnr)) } returns søker + + val nyBehandling = NyBehandlingHendelse(søkerFnr, listOf(barnFnr)) + val behandlingFørVilkår = + stegService.opprettNyBehandlingOgRegistrerPersongrunnlagForFødselhendelse(nyBehandling) + val behandlingEtterVilkår = + stegService.håndterVilkårsvurdering(behandlingFørVilkår.leggTilBehandlingStegTilstand(StegType.VILKÅRSVURDERING)) + Assertions.assertEquals(Behandlingsresultat.AVSLÅTT, behandlingEtterVilkår.resultat) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/VelgFagsystemIntegrasjonTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/VelgFagsystemIntegrasjonTest.kt" new file mode 100644 index 000000000..02ae3ad28 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/VelgFagsystemIntegrasjonTest.kt" @@ -0,0 +1,97 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse + +import io.mockk.every +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdBarnetrygdClient +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdService +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.kontrakter.ba.infotrygd.InfotrygdSøkResponse +import no.nav.familie.kontrakter.ba.infotrygd.Stønad +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class VelgFagsystemIntegrasjonTest( + @Autowired val stegService: StegService, + @Autowired val mockPersonopplysningerService: PersonopplysningerService, + @Autowired val persongrunnlagService: PersongrunnlagService, + @Autowired val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + @Autowired val velgFagSystemService: VelgFagSystemService, + @Autowired val fagSakService: FagsakService, + @Autowired val infotrygdService: InfotrygdService, + @Autowired val infotrygdBarnetrygdClient: InfotrygdBarnetrygdClient, + @Autowired val databaseCleanupService: DatabaseCleanupService, + @Autowired val featureToggleService: FeatureToggleService, +) : AbstractSpringIntegrationTest() { + + val søkerFnr = randomFnr() + + @BeforeEach + fun init() { + databaseCleanupService.truncate() + } + + @Test + fun `sjekk om mor har løpende utbetalinger i infotrygd`() { + every { infotrygdService.harLøpendeSakIInfotrygd(any(), any()) } returns true andThen false + + assertEquals(true, velgFagSystemService.morEllerBarnHarLøpendeSakIInfotrygd(søkerFnr, emptyList())) + } + + @Test + fun `skal IKKE velge ba-sak når mor har stønadhistorikk i Infotrygd`() { + val nyBehandling = NyBehandlingHendelse(søkerFnr, listOf(søkerFnr)) + val fagsystemUtfall = FagsystemUtfall.SAKER_I_INFOTRYGD_MEN_IKKE_LØPENDE_UTBETALINGER + + every { infotrygdBarnetrygdClient.hentStønader(any(), any(), any()) } returns InfotrygdSøkResponse( + listOf(Stønad(opphørtFom = "012020")), + emptyList(), + ) andThen InfotrygdSøkResponse(emptyList(), emptyList()) + + val (fagsystemRegelVurdering, faktiskFagsystemUtfall) = velgFagSystemService.velgFagsystem(nyBehandling) + assertEquals(FagsystemRegelVurdering.SEND_TIL_INFOTRYGD, fagsystemRegelVurdering) + assertEquals(fagsystemUtfall, faktiskFagsystemUtfall) + } + + @Test + fun `skal IKKE velge ba-sak når mor er EØS borger`() { + val nyBehandling = NyBehandlingHendelse(søkerFnr, listOf(søkerFnr)) + + every { mockPersonopplysningerService.hentGjeldendeStatsborgerskap(any()) } returns Statsborgerskap( + land = "POL", + gyldigFraOgMed = LocalDate.now().minusYears(2), + gyldigTilOgMed = null, + bekreftelsesdato = null, + ) + val (fagsystemRegelVurdering, faktiskFagsystemUtfall) = velgFagSystemService.velgFagsystem(nyBehandling) + assertEquals(FagsystemRegelVurdering.SEND_TIL_BA, fagsystemRegelVurdering) + assertEquals(FagsystemUtfall.STØTTET_I_BA_SAK, faktiskFagsystemUtfall) + } + + @Test + fun `skal velge ba-sak når mor er tredjelandsborger`() { + val nyBehandling = NyBehandlingHendelse(søkerFnr, listOf(søkerFnr)) + val fagsystemUtfall = FagsystemUtfall.STØTTET_I_BA_SAK + + every { mockPersonopplysningerService.hentGjeldendeStatsborgerskap(any()) } returns Statsborgerskap( + land = "USA", + gyldigFraOgMed = LocalDate.now().minusYears(2), + gyldigTilOgMed = null, + bekreftelsesdato = null, + ) + val (fagsystemRegelVurdering, faktiskFagsystemUtfall) = velgFagSystemService.velgFagsystem(nyBehandling) + assertEquals(FagsystemRegelVurdering.SEND_TIL_BA, fagsystemRegelVurdering) + assertEquals(fagsystemUtfall, faktiskFagsystemUtfall) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/Vilk\303\245rVurderingTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/Vilk\303\245rVurderingTest.kt" new file mode 100644 index 000000000..d7390e04e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/f\303\270dselshendelse/Vilk\303\245rVurderingTest.kt" @@ -0,0 +1,653 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse + +import no.nav.familie.ba.sak.common.DatoIntervallEntitet +import no.nav.familie.ba.sak.common.TIDENES_MORGEN +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.arbeidsforhold.GrArbeidsforhold +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrBostedsadresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrUkjentBosted +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrVegadresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.sivilstand.GrSivilstand +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.GrStatsborgerskap +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.VilkårsvurderingForNyBehandlingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.GyldigVilkårsperiode +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import no.nav.familie.kontrakter.felles.personopplysning.Vegadresse +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class VilkårVurderingTest( + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val vilkårsvurderingForNyBehandlingService: VilkårsvurderingForNyBehandlingService, + +) : AbstractSpringIntegrationTest() { + + @BeforeAll + fun init() { + databaseCleanupService.truncate() + } + + @Test + fun `Henting og evaluering av oppfylte vilkår gir rett antall samlede resultater`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling(fagsak, årsak = BehandlingÅrsak.FØDSELSHENDELSE, skalBehandlesAutomatisk = true), + ) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + personopplysningGrunnlagRepository.save(personopplysningGrunnlag) + + val vilkårsvurdering = + vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling(behandling, false, null) + + val forventetAntallVurderteVilkår = + Vilkår.hentVilkårFor(personType = PersonType.BARN, fagsakType = FagsakType.NORMAL, behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR).size + + Vilkår.hentVilkårFor(personType = PersonType.SØKER, fagsakType = FagsakType.NORMAL, behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR).size + assertEquals( + forventetAntallVurderteVilkår, + vilkårsvurdering.personResultater.flatMap { personResultat -> personResultat.vilkårResultater }.size, + ) + } + + @Test + fun `Sjekk gyldig vilkårsperiode`() { + val ubegrensetGyldigVilkårsperiode = GyldigVilkårsperiode() + assertTrue(ubegrensetGyldigVilkårsperiode.gyldigFor(LocalDate.now())) + + val begrensetGyldigVilkårsperiode = GyldigVilkårsperiode( + gyldigFom = LocalDate.now().minusDays(5), + gyldigTom = LocalDate.now().plusDays(5), + ) + assertTrue(begrensetGyldigVilkårsperiode.gyldigFor(LocalDate.now())) + assertTrue(begrensetGyldigVilkårsperiode.gyldigFor(LocalDate.now().minusDays(5))) + assertFalse(begrensetGyldigVilkårsperiode.gyldigFor(LocalDate.now().minusDays(6))) + assertTrue(begrensetGyldigVilkårsperiode.gyldigFor(LocalDate.now().plusDays(5))) + assertFalse(begrensetGyldigVilkårsperiode.gyldigFor(LocalDate.now().plusDays(6))) + } + + private fun genererPerson( + type: PersonType, + personopplysningGrunnlag: PersonopplysningGrunnlag, + grBostedsadresse: GrBostedsadresse? = null, + kjønn: Kjønn = Kjønn.KVINNE, + sivilstand: SIVILSTAND = SIVILSTAND.UGIFT, + ): Person { + val fnr = randomFnr() + return Person( + aktør = randomAktør(fnr), + type = type, + personopplysningGrunnlag = personopplysningGrunnlag, + fødselsdato = LocalDate.of(1991, 1, 1), + navn = "navn", + kjønn = kjønn, + bostedsadresser = grBostedsadresse?.let { mutableListOf(grBostedsadresse) } ?: mutableListOf(), + ) + .apply { + this.sivilstander = + mutableListOf(GrSivilstand(type = sivilstand, person = this, fom = LocalDate.of(1991, 1, 1))) + } + } + + @Test + fun `Sjekk barn bor med søker`() { + val søkerAddress = GrVegadresse( + 1234, + "11", + "B", + "H022", + "St. Olavsvegen", + "1232", + "whatever", + "4322", + ) + val barnAddress = GrVegadresse( + 1235, + "11", + "B", + "H024", + "St. Olavsvegen", + "1232", + "whatever", + "4322", + ) + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 1) + + val søker = genererPerson(PersonType.SØKER, personopplysningGrunnlag, søkerAddress) + personopplysningGrunnlag.personer.add(søker) + + val barn1 = genererPerson(PersonType.BARN, personopplysningGrunnlag, søkerAddress, Kjønn.MANN) + personopplysningGrunnlag.personer.add(barn1) + + val barn2 = genererPerson(PersonType.BARN, personopplysningGrunnlag, barnAddress, Kjønn.MANN) + personopplysningGrunnlag.personer.add(barn2) + + val barn3 = genererPerson(PersonType.BARN, personopplysningGrunnlag, null, Kjønn.MANN) + personopplysningGrunnlag.personer.add(barn3) + + assertEquals(Resultat.OPPFYLT, Vilkår.BOR_MED_SØKER.vurderVilkår(barn1, LocalDate.now()).resultat) + assertEquals(Resultat.IKKE_OPPFYLT, Vilkår.BOR_MED_SØKER.vurderVilkår(barn2, LocalDate.now()).resultat) + assertEquals(Resultat.IKKE_OPPFYLT, Vilkår.BOR_MED_SØKER.vurderVilkår(barn3, LocalDate.now()).resultat) + } + + @Test + fun `Sjekk barn bor med mor når mor har bodd på adressen lengre enn barn`() { + val søkerAddress = GrVegadresse( + 1234, + "11", + "B", + "H022", + "St. Olavsvegen", + "1232", + "whatever", + "4322", + ) + .apply { + periode = DatoIntervallEntitet(LocalDate.now().minusYears(10)) + } + + val barnAddress = GrVegadresse( + 1234, + "11", + "B", + "H024", + "St. Olavsvegen", + "1232", + "whatever", + "4322", + ) + .apply { + periode = DatoIntervallEntitet(LocalDate.now().minusMonths(1)) + } + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 1) + + val søker = genererPerson(PersonType.SØKER, personopplysningGrunnlag, søkerAddress) + personopplysningGrunnlag.personer.add(søker) + + val barn1 = genererPerson(PersonType.BARN, personopplysningGrunnlag, barnAddress, Kjønn.MANN) + personopplysningGrunnlag.personer.add(barn1) + + assertEquals(Resultat.OPPFYLT, Vilkår.BOR_MED_SØKER.vurderVilkår(barn1, LocalDate.now()).resultat) + } + + @Test + fun `Negativ vurdering - Barn og søker har ikke adresse angitt`() { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 2) + val søker = genererPerson(PersonType.SØKER, personopplysningGrunnlag, null) + personopplysningGrunnlag.personer.add(søker) + + val barn = genererPerson(PersonType.BARN, personopplysningGrunnlag, null) + personopplysningGrunnlag.personer.add(barn) + + assertEquals(Resultat.IKKE_OPPFYLT, Vilkår.BOR_MED_SØKER.vurderVilkår(barn, barn.fødselsdato).resultat) + } + + @Test + fun `Skal kaste exception - ingen søker`() { + val søkerAddress = GrVegadresse( + 1234, + "11", + "B", + "H022", + "St. Olavsvegen", + "1232", + "whatever", + "4322", + ) + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 4) + val barn = genererPerson(PersonType.BARN, personopplysningGrunnlag, søkerAddress, Kjønn.MANN) + val feilregistrertSøker = genererPerson(PersonType.BARN, personopplysningGrunnlag, søkerAddress, Kjønn.KVINNE) + personopplysningGrunnlag.personer.add(barn) + personopplysningGrunnlag.personer.add(feilregistrertSøker) + + assertThrows(IllegalStateException::class.java) { + Vilkår.BOR_MED_SØKER.vurderVilkår(barn, LocalDate.now()).resultat + } + } + + @Test + fun `Negativ vurdering - søker har ukjentadresse`() { + val ukjentbosted = GrUkjentBosted("Oslo") + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val søker = genererPerson(PersonType.SØKER, personopplysningGrunnlag, ukjentbosted) + personopplysningGrunnlag.personer.add(søker) + val barn = genererPerson(PersonType.BARN, personopplysningGrunnlag, ukjentbosted) + personopplysningGrunnlag.personer.add(barn) + + assertEquals(Resultat.IKKE_OPPFYLT, Vilkår.BOR_MED_SØKER.vurderVilkår(barn, LocalDate.now()).resultat) + } + + @Test + fun `Sjekk at barn er ugift`() { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val barn = genererPerson(PersonType.BARN, personopplysningGrunnlag) + personopplysningGrunnlag.personer.add(barn) + + assertEquals(Resultat.OPPFYLT, Vilkår.GIFT_PARTNERSKAP.vurderVilkår(barn, LocalDate.now()).resultat) + } + + @Test + fun `Negativ vurdering - barn er gift`() { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val barn = genererPerson(PersonType.BARN, personopplysningGrunnlag, sivilstand = SIVILSTAND.GIFT) + personopplysningGrunnlag.personer.add(barn) + + assertEquals( + Resultat.IKKE_OPPFYLT, + Vilkår.GIFT_PARTNERSKAP.vurderVilkår(barn, LocalDate.now()).resultat, + ) + } + + @Test + fun `Negativ vurdering - barn har vært gift`() { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val barn = genererPerson(PersonType.BARN, personopplysningGrunnlag, sivilstand = SIVILSTAND.GIFT).apply { + sivilstander = mutableListOf( + GrSivilstand(fom = LocalDate.now().minusMonths(2), type = SIVILSTAND.GIFT, person = this), + GrSivilstand(fom = LocalDate.now().minusMonths(1), type = SIVILSTAND.UGIFT, person = this), + ) + } + personopplysningGrunnlag.personer.add(barn) + + assertEquals( + Resultat.IKKE_OPPFYLT, + Vilkår.GIFT_PARTNERSKAP.vurderVilkår(barn).resultat, + ) + } + + @Test + fun `Negativ vurdering - søker er ikke bosatt i norge`() { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val søker = genererPerson(PersonType.SØKER, personopplysningGrunnlag, sivilstand = SIVILSTAND.GIFT) + personopplysningGrunnlag.personer.add(søker) + + assertEquals(Resultat.IKKE_OPPFYLT, Vilkår.BOSATT_I_RIKET.vurderVilkår(søker, LocalDate.now()).resultat) + } + + @Test + fun `Negativ vurdering - søker har ikke vært bosatt i norge siden barnets fødselsdato`() { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val søker = genererPerson(PersonType.SØKER, personopplysningGrunnlag, sivilstand = SIVILSTAND.GIFT).apply { + bostedsadresser = mutableListOf( + GrVegadresse( + 1234, + "11", + "B", + "H022", + "St. Olavsvegen", + "1232", + "whatever", + "4322", + ) + .apply { + periode = DatoIntervallEntitet(LocalDate.now().minusDays(10)) + }, + ) + } + personopplysningGrunnlag.personer.add(søker) + + assertEquals( + Resultat.IKKE_OPPFYLT, + Vilkår.BOSATT_I_RIKET.vurderVilkår(søker, LocalDate.now().minusMonths(1)).resultat, + ) + } + + @Test + fun `Sjekk at mor er bosatt i norge`() { + val vegadresse = GrVegadresse( + 1234, + "11", + "B", + "H022", + "St. Olavsvegen", + "1232", + "whatever", + "4322", + ).apply { + periode = DatoIntervallEntitet(TIDENES_MORGEN) + } + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val mor = genererPerson(PersonType.SØKER, personopplysningGrunnlag, vegadresse) + personopplysningGrunnlag.personer.add(mor) + + assertEquals(Resultat.OPPFYLT, Vilkår.BOSATT_I_RIKET.vurderVilkår(mor, LocalDate.now()).resultat) + } + + @Test + fun `Sjekk at mor har vært bosatt i norge siden barnet ble født`() { + val vegadresse = GrVegadresse( + matrikkelId = 1234, + husnummer = "11", + husbokstav = "B", + bruksenhetsnummer = "H022", + adressenavn = "St. Olavsvegen", + kommunenummer = "1232", + tilleggsnavn = "whatever", + postnummer = "4322", + ).apply { + periode = DatoIntervallEntitet(LocalDate.now().minusMonths(10)) + } + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val mor = genererPerson(PersonType.SØKER, personopplysningGrunnlag, vegadresse) + personopplysningGrunnlag.personer.add(mor) + + assertEquals( + Resultat.OPPFYLT, + Vilkår.BOSATT_I_RIKET.vurderVilkår(mor, LocalDate.now().minusMonths(3)).resultat, + ) + } + + @Test + fun `Negativ vurdering - mor har bare adresse deler av perioden siden barnet ble født`() { + val vegadresser = listOf( + DatoIntervallEntitet(LocalDate.now().minusMonths(7), LocalDate.now().minusMonths(4)), + DatoIntervallEntitet(LocalDate.now().minusMonths(2)), + ).map { + GrVegadresse( + matrikkelId = 1234, + husnummer = "11", + husbokstav = "B", + bruksenhetsnummer = "H022", + adressenavn = "St. Olavsvegen", + kommunenummer = "1232", + tilleggsnavn = "whatever", + postnummer = "4322", + ).apply { + periode = it + } + } + + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val mor = genererPerson(PersonType.SØKER, personopplysningGrunnlag).apply { + bostedsadresser = vegadresser.toMutableList() + } + personopplysningGrunnlag.personer.add(mor) + + assertEquals( + Resultat.IKKE_OPPFYLT, + Vilkår.BOSATT_I_RIKET.vurderVilkår(mor, LocalDate.now().minusMonths(6)).resultat, + ) + } + + @Test + fun `Negativ vurdering - mor er ikke bosatt i norge`() { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val mor = genererPerson(PersonType.SØKER, personopplysningGrunnlag, sivilstand = SIVILSTAND.GIFT) + personopplysningGrunnlag.personer.add(mor) + + assertEquals(Resultat.IKKE_OPPFYLT, Vilkår.BOSATT_I_RIKET.vurderVilkår(mor, LocalDate.now()).resultat) + } + + @Test + fun `Lovlig opphold - nordisk statsborger`() { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val person = genererPerson(PersonType.BARN, personopplysningGrunnlag, sivilstand = SIVILSTAND.GIFT) + .also { + it.statsborgerskap = + mutableListOf( + GrStatsborgerskap( + gyldigPeriode = DatoIntervallEntitet( + tom = null, + fom = LocalDate.now().minusYears(1), + ), + landkode = "DNK", + medlemskap = Medlemskap.NORDEN, + person = it, + ), + ) + } + + assertEquals(Resultat.OPPFYLT, Vilkår.LOVLIG_OPPHOLD.vurderVilkår(person, LocalDate.now()).resultat) + } + + @Test + @Disabled + fun `Mor er fra EØS og har et løpende arbeidsforhold - lovlig opphold, skal evalueres til Ja`() { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val person = genererPerson(PersonType.SØKER, personopplysningGrunnlag, sivilstand = SIVILSTAND.GIFT) + .also { + it.statsborgerskap = mutableListOf( + GrStatsborgerskap( + gyldigPeriode = DatoIntervallEntitet(LocalDate.now().minusYears(1)), + landkode = "BEL", + medlemskap = Medlemskap.EØS, + person = it, + ), + ) + it.arbeidsforhold = løpendeArbeidsforhold(it) + } + + assertEquals(Resultat.OPPFYLT, Vilkår.LOVLIG_OPPHOLD.vurderVilkår(person, LocalDate.now()).resultat) + assertEquals( + "Mor er EØS-borger, men har et løpende arbeidsforhold i Norge.", + Vilkår.LOVLIG_OPPHOLD.vurderVilkår(person, LocalDate.now()).evaluering.begrunnelse, + ) + } + + @Test + @Disabled + fun `Mor er fra EØS og har ikke et løpende arbeidsforhold, bor sammen med annen forelder som er fra norden - lovlig opphold, skal evalueres til Ja`() { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val bostedsadresse = Bostedsadresse(vegadresse = Vegadresse(0, null, null, "32E", null, null, null, null)) + val person = genererPerson( + PersonType.SØKER, + personopplysningGrunnlag, + sivilstand = SIVILSTAND.GIFT, + ) + .also { + it.statsborgerskap = mutableListOf( + GrStatsborgerskap( + gyldigPeriode = DatoIntervallEntitet(LocalDate.now().minusYears(1)), + landkode = "BEL", + medlemskap = Medlemskap.EØS, + person = it, + ), + ) + it.bostedsadresser = + mutableListOf(GrBostedsadresse.fraBostedsadresse(bostedsadresse, it)) + } + val annenForelder = opprettAnnenForelder(personopplysningGrunnlag, bostedsadresse, Medlemskap.NORDEN) + person.personopplysningGrunnlag.personer.add(annenForelder) + + assertEquals(Resultat.OPPFYLT, Vilkår.LOVLIG_OPPHOLD.vurderVilkår(person, LocalDate.now()).resultat) + assertEquals( + "Annen forelder er norsk eller nordisk statsborger.", + Vilkår.LOVLIG_OPPHOLD.vurderVilkår(person, LocalDate.now()).evaluering.begrunnelse, + ) + } + + @Test + @Disabled + fun `Mor er fra EØS og har ikke et løpende arbeidsforhold, bor sammen med annen forelder som er tredjelandsborger - lovlig opphold, skal evalueres til Nei`() { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val bostedsadresse = Bostedsadresse(vegadresse = Vegadresse(0, null, null, "32E", null, null, null, null)) + val person = genererPerson( + PersonType.SØKER, + personopplysningGrunnlag, + sivilstand = SIVILSTAND.GIFT, + ) + .also { + it.statsborgerskap = mutableListOf( + GrStatsborgerskap( + gyldigPeriode = DatoIntervallEntitet(LocalDate.now().minusYears(1)), + landkode = "BEL", + medlemskap = Medlemskap.EØS, + person = it, + ), + ) + it.bostedsadresser = + mutableListOf(GrBostedsadresse.fraBostedsadresse(bostedsadresse, it)) + } + val annenForelder = opprettAnnenForelder(personopplysningGrunnlag, bostedsadresse, Medlemskap.TREDJELANDSBORGER) + + person.personopplysningGrunnlag.personer.add(annenForelder) + + assertEquals( + Resultat.IKKE_OPPFYLT, + Vilkår.LOVLIG_OPPHOLD.vurderVilkår(person, LocalDate.now()).resultat, + ) + assertEquals( + "Mor har ikke lovlig opphold - EØS borger. Mor er ikke registrert med arbeidsforhold. Medforelder er tredjelandsborger.", + Vilkår.LOVLIG_OPPHOLD.vurderVilkår(person, LocalDate.now()).evaluering.begrunnelse, + ) + } + + @Test + @Disabled + fun `Mor er fra EØS og har ikke et løpende arbeidsforhold, bor sammen med annen forelder som er statsløs - lovlig opphold, skal evalueres til Nei`() { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val bostedsadresse = Bostedsadresse(vegadresse = Vegadresse(0, null, null, "32E", null, null, null, null)) + val person = genererPerson( + PersonType.SØKER, + personopplysningGrunnlag, + sivilstand = SIVILSTAND.GIFT, + ) + .also { + it.statsborgerskap = mutableListOf( + GrStatsborgerskap( + gyldigPeriode = DatoIntervallEntitet(LocalDate.now().minusYears(1)), + landkode = "BEL", + medlemskap = Medlemskap.EØS, + person = it, + ), + ) + it.bostedsadresser = + mutableListOf(GrBostedsadresse.fraBostedsadresse(bostedsadresse, it)) + } + val annenForelder = opprettAnnenForelder(personopplysningGrunnlag, bostedsadresse, Medlemskap.UKJENT) + person.personopplysningGrunnlag.personer.add(annenForelder) + + assertEquals( + Resultat.IKKE_OPPFYLT, + Vilkår.LOVLIG_OPPHOLD.vurderVilkår(person, LocalDate.now()).resultat, + ) + assertEquals( + "Mor har ikke lovlig opphold - EØS borger. Mor er ikke registrert med arbeidsforhold. Medforelder er statsløs.", + Vilkår.LOVLIG_OPPHOLD.vurderVilkår(person, LocalDate.now()).evaluering.begrunnelse, + ) + } + + @Test + @Disabled + fun `Mor er fra EØS og har ikke et løpende arbeidsforhold, bor sammen med annen forelder fra EØS som har løpende arbeidsforhold - lovlig opphold, skal evalueres til Ja`() { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = 6) + val bostedsadresse = Bostedsadresse(vegadresse = Vegadresse(0, null, null, "32E", null, null, null, null)) + val person = genererPerson( + PersonType.SØKER, + personopplysningGrunnlag, + sivilstand = SIVILSTAND.GIFT, + ) + .also { + it.statsborgerskap = mutableListOf( + GrStatsborgerskap( + gyldigPeriode = DatoIntervallEntitet(LocalDate.now().minusYears(1)), + landkode = "BEL", + medlemskap = Medlemskap.EØS, + person = it, + ), + ) + it.bostedsadresser = + mutableListOf(GrBostedsadresse.fraBostedsadresse(bostedsadresse, it)) + } + val annenForelder = opprettAnnenForelder(personopplysningGrunnlag, bostedsadresse, Medlemskap.EØS) + .also { it.arbeidsforhold = løpendeArbeidsforhold(it) } + person.personopplysningGrunnlag.personer.add(annenForelder) + + assertEquals(Resultat.OPPFYLT, Vilkår.LOVLIG_OPPHOLD.vurderVilkår(person, LocalDate.now()).resultat) + assertEquals( + "Annen forelder er fra EØS, men har et løpende arbeidsforhold i Norge.", + Vilkår.LOVLIG_OPPHOLD.vurderVilkår(person, LocalDate.now()).evaluering.begrunnelse, + ) + } + + private fun opprettAnnenForelder( + personopplysningGrunnlag: PersonopplysningGrunnlag, + bostedsadresse: Bostedsadresse, + medlemskap: Medlemskap, + ): Person { + return genererPerson( + PersonType.ANNENPART, + personopplysningGrunnlag, + sivilstand = SIVILSTAND.GIFT, + ) + .also { + it.statsborgerskap = mutableListOf( + GrStatsborgerskap( + gyldigPeriode = DatoIntervallEntitet(LocalDate.now().minusYears(1)), + landkode = "LOL", + medlemskap = medlemskap, + person = it, + ), + ) + it.bostedsadresser = + mutableListOf(GrBostedsadresse.fraBostedsadresse(bostedsadresse, it)) + } + } + + private fun løpendeArbeidsforhold(person: Person) = mutableListOf( + GrArbeidsforhold( + periode = DatoIntervallEntitet( + LocalDate.now() + .minusYears( + 1, + ), + ), + arbeidsgiverId = "998877665", + arbeidsgiverType = "Organisasjon", + person = person, + ), + ) +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/AutovedtakSatsendringServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/AutovedtakSatsendringServiceTest.kt new file mode 100644 index 000000000..926d041ba --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/autovedtak/satsendring/AutovedtakSatsendringServiceTest.kt @@ -0,0 +1,280 @@ +package no.nav.familie.ba.sak.kjerne.autovedtak.satsendring + +import io.mockk.every +import io.mockk.just +import io.mockk.mockkObject +import io.mockk.runs +import io.mockk.unmockkObject +import no.nav.familie.ba.sak.common.LocalDateService +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene.Satskjøring +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene.SatskjøringRepository +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentService +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.SatsTidspunkt +import no.nav.familie.ba.sak.kjerne.beregning.TilkjentYtelseValidering +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.RegistrerPersongrunnlag +import no.nav.familie.ba.sak.kjerne.steg.RegistrerPersongrunnlagDTO +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.task.SatsendringTaskDto +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.jdbc.core.JdbcTemplate +import java.time.LocalDate +import java.time.YearMonth + +class AutovedtakSatsendringServiceTest( + @Autowired private val jdbcTemplate: JdbcTemplate, + @Autowired private val mockLocalDateService: LocalDateService, + @Autowired private val featureToggleService: FeatureToggleService, + @Autowired private val databaseCleanupService: DatabaseCleanupService, + + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingService: BehandlingService, + @Autowired private val behandlingRepository: BehandlingRepository, + @Autowired private val autovedtakSatsendringService: AutovedtakSatsendringService, + @Autowired private val satskjøringRepository: SatskjøringRepository, + @Autowired private val settPåVentService: SettPåVentService, + @Autowired private val tilkjentYtelseRepository: TilkjentYtelseRepository, + @Autowired private val personidentService: PersonidentService, + @Autowired private val registrerPersongrunnlag: RegistrerPersongrunnlag, +) : AbstractSpringIntegrationTest() { + + private lateinit var fagsak: Fagsak + private lateinit var aktørBarn: Aktør + + @BeforeEach + fun setUp() { + databaseCleanupService.truncate() + + // Vilkårsvurdering og andeler tilkjent ytelse blir ikke generert i disse testene. Validering av andeler ved satsendring vil derfor kaste feil. For at testene for sett på vent skal fungere skrur vi her av denne valideringen. + mockkObject(TilkjentYtelseValidering) + every { + TilkjentYtelseValidering.validerAtSatsendringKunOppdatererSatsPåEksisterendePerioder( + any(), + any(), + ) + } just runs + + mockkObject(SatsTidspunkt) + // Grunnen til at denne mockes er egentlig at den indirekte påvirker hva SatsService.hentGyldigSatsFor + // returnerer. Det vi ønsker er at den sist tillagte satsendringen ikke kommer med slik at selve + // satsendringen som skal kjøres senere faktisk utgjør en endring (slik at behandlingsresultatet blir ENDRET). + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2023, 2, 1) + + every { mockLocalDateService.now() } returns LocalDate.now().minusYears(6) andThen LocalDate.now() + every { featureToggleService.isEnabled(FeatureToggleConfig.SATSENDRING_SNIKE_I_KØEN) } returns true + fagsak = opprettLøpendeFagsak() + aktørBarn = personidentService.hentOgLagreAktør(randomFnr(), true) + } + + @AfterEach + fun tearDown() { + unmockkObject(SatsTidspunkt) + unmockkObject(TilkjentYtelseValidering) + } + + @Nested + inner class ÅpenBehandling { + + @Test + fun `Kan ikke sette åpen behandling på vent når behandlingen akkurat er opprettet`() { + val behandling = opprettBehandling() + lagTilkjentAndelOgFerdigstillBehandling(behandling) + satskjøringRepository.saveAndFlush( + Satskjøring( + fagsakId = behandling.fagsak.id, + satsTidspunkt = StartSatsendring.SATSENDRINGMÅNED_MARS_2023, + ), + ) + + // Opprett revurdering som blir liggende igjen som åpen og på behandlingsresultatsteget + opprettBehandling() + + // Fjerner mocking slik at den siste satsendringen vi fjernet via mocking nå skal komme med. + unmockkObject(SatsTidspunkt) + + val satsendringTaskDto = SatsendringTaskDto( + behandling.fagsak.id, + YearMonth.of(2023, 3), + ) + val satsendringResultat = autovedtakSatsendringService.kjørBehandling(satsendringTaskDto) + + assertThat(satsendringResultat).isEqualTo(SatsendringSvar.BEHANDLING_KAN_IKKE_SETTES_PÅ_VENT) + } + + @Test + fun `Skal sette åpen behandling på maskinell vent hvis den er satt på vent av saksbehandler ved kjøring av satsendring`() { + val behandling = opprettBehandling() + lagTilkjentAndelOgFerdigstillBehandling(behandling) + satskjøringRepository.saveAndFlush( + Satskjøring( + fagsakId = behandling.fagsak.id, + satsTidspunkt = StartSatsendring.SATSENDRINGMÅNED_MARS_2023, + ), + ) + + // Opprett revurdering som blir liggende igjen som åpen og på behandlingsresultatsteget + val revurdering = opprettBehandling() + justerLoggTidspunktForÅKunneSatsendreNårDetFinnesÅpenBehandling(revurdering) + + // Fjerner mocking slik at den siste satsendringen vi fjernet via mocking nå skal komme med. + unmockkObject(SatsTidspunkt) + + val satsendringTaskDto = SatsendringTaskDto( + behandling.fagsak.id, + YearMonth.of(2023, 3), + ) + val satsendringResultat = autovedtakSatsendringService.kjørBehandling(satsendringTaskDto) + + assertThat(satsendringResultat).isEqualTo(SatsendringSvar.SATSENDRING_KJØRT_OK) + } + + // Kan fjernes når feature toggle er fjernet + @Test + fun `Skal ikke sette behandling på vent hvis feature toggle er slått av`() { + every { featureToggleService.isEnabled(FeatureToggleConfig.SATSENDRING_SNIKE_I_KØEN) } returns false + val behandling = opprettBehandling() + lagTilkjentAndelOgFerdigstillBehandling(behandling) + satskjøringRepository.saveAndFlush( + Satskjøring( + fagsakId = behandling.fagsak.id, + satsTidspunkt = StartSatsendring.SATSENDRINGMÅNED_MARS_2023, + ), + ) + + // Opprett revurdering som blir liggende igjen som åpen og på behandlingsresultatsteget + val revurdering = opprettBehandling() + justerLoggTidspunktForÅKunneSatsendreNårDetFinnesÅpenBehandling(revurdering) + + // Fjerner mocking slik at den siste satsendringen vi fjernet via mocking nå skal komme med. + unmockkObject(SatsTidspunkt) + + val satsendringTaskDto = SatsendringTaskDto( + behandling.fagsak.id, + YearMonth.of(2023, 3), + ) + val satsendringResultat = autovedtakSatsendringService.kjørBehandling(satsendringTaskDto) + + assertThat(satsendringResultat).isEqualTo(SatsendringSvar.BEHANDLING_KAN_SNIKES_FORBI) + } + + @Test + fun `Skal sette behandling på vent på maskinell vent hvis den er satt på vent av saksbehandler ved kjøring av satsendring`() { + val behandling = opprettBehandling() + lagTilkjentAndelOgFerdigstillBehandling(behandling) + satskjøringRepository.saveAndFlush( + Satskjøring( + fagsakId = behandling.fagsak.id, + satsTidspunkt = StartSatsendring.SATSENDRINGMÅNED_MARS_2023, + ), + ) + + // Opprett revurdering som blir liggende igjen som åpen og på behandlingsresultatsteget + val revurdering = opprettBehandling() + + settPåVentService.settBehandlingPåVent( + revurdering.id, + LocalDate.now(), + SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, + ) + justerLoggTidspunktForÅKunneSatsendreNårDetFinnesÅpenBehandling(revurdering) + + // Fjerner mocking slik at den siste satsendringen vi fjernet via mocking nå skal komme med. + unmockkObject(SatsTidspunkt) + + val satsendringTaskDto = SatsendringTaskDto( + behandling.fagsak.id, + YearMonth.of(2023, 3), + ) + val satsendringResultat = autovedtakSatsendringService.kjørBehandling(satsendringTaskDto) + + assertThat(satsendringResultat).isEqualTo(SatsendringSvar.SATSENDRING_KJØRT_OK) + } + } + + private fun justerLoggTidspunktForÅKunneSatsendreNårDetFinnesÅpenBehandling(behandling: Behandling) { + jdbcTemplate.update( + "UPDATE logg SET opprettet_tid = opprettet_tid - interval '12 hours' WHERE fk_behandling_id = ?", + behandling.id, + ) + jdbcTemplate.update( + "UPDATE behandling SET endret_tid = endret_tid - interval '12 hours' WHERE id = ?", + behandling.id, + ) + } + + private fun opprettBehandling(): Behandling { + val behandling = behandlingService.opprettBehandling( + NyBehandling( + fagsakId = fagsak.id, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + behandlingÅrsak = BehandlingÅrsak.NYE_OPPLYSNINGER, + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + søkersIdent = fagsak.aktør.aktivFødselsnummer(), + ), + ) + registrerPersongrunnlag.utførStegOgAngiNeste( + behandling, + RegistrerPersongrunnlagDTO(fagsak.aktør.aktivFødselsnummer(), listOf(aktørBarn.aktivFødselsnummer())), + ) + return behandling + } + + private fun lagTilkjentAndelOgFerdigstillBehandling(behandling: Behandling) { + behandling.status = BehandlingStatus.AVSLUTTET + val avsluttetSteg = BehandlingStegTilstand( + behandling = behandling, + behandlingSteg = StegType.BEHANDLING_AVSLUTTET, + ) + behandling.behandlingStegTilstand.add(avsluttetSteg) + with(lagInitiellTilkjentYtelse(behandling, "utbetalingsoppdrag")) { + val andel = lagAndelTilkjentYtelse( + fom = YearMonth.of(2021, 1), // Tidspunkt før siste satsendring + tom = YearMonth.of( + 2026, + 5, + ), // Tidspunkt etter siste satsendring. Dersom tom er før siste satsendring vil alle testene feile. + behandling = behandling, + beløp = 10, + aktør = aktørBarn, + tilkjentYtelse = this, + ) + andelerTilkjentYtelse.add(andel) + tilkjentYtelseRepository.saveAndFlush(this) + } + behandlingRepository.saveAndFlush(behandling) + } + + private fun opprettLøpendeFagsak(): Fagsak { + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(randomFnr()) + return fagsakService.lagre(fagsak.copy(status = FagsakStatus.LØPENDE)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingHentOgPersisterServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingHentOgPersisterServiceTest.kt new file mode 100644 index 000000000..9f80766a0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingHentOgPersisterServiceTest.kt @@ -0,0 +1,46 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +class BehandlingHentOgPersisterServiceTest( + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val fagsakService: FagsakService, +) : AbstractSpringIntegrationTest() { + + @Test + fun `skal hente aktiv fødselsnummere`() { + val fødselsnummere = listOf(randomFnr(), randomFnr()) + val fagsak1 = fagsakService.hentEllerOpprettFagsakForPersonIdent(fødselsnummere[0]) + fagsakService.oppdaterStatus(fagsak1, FagsakStatus.LØPENDE) + val behandling1 = behandlingHentOgPersisterService.lagreEllerOppdater(lagBehandling(fagsak1), false) + + val fagsak2 = fagsakService.hentEllerOpprettFagsakForPersonIdent(fødselsnummere[1]) + fagsakService.oppdaterStatus(fagsak2, FagsakStatus.LØPENDE) + val behandling2 = behandlingHentOgPersisterService.lagreEllerOppdater(lagBehandling(fagsak2), false) + + val aktivFødselsnummere = behandlingHentOgPersisterService.hentAktivtFødselsnummerForBehandlinger( + listOf( + behandling1.id, + behandling2.id, + ), + ) + assertEquals(fødselsnummere[0], aktivFødselsnummere[behandling1.id]) + assertEquals(fødselsnummere[1], aktivFødselsnummere[behandling2.id]) + } + + @Test + fun `skal hente status på behandling`() { + val fnr = randomFnr() + val fagsak1 = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling1 = behandlingHentOgPersisterService.lagreEllerOppdater(lagBehandling(fagsak1), false) + assertThat(behandlingHentOgPersisterService.hentStatus(behandling1.id)).isEqualTo(behandling1.status) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingIntegrationTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingIntegrationTest.kt new file mode 100644 index 000000000..479b9e575 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingIntegrationTest.kt @@ -0,0 +1,753 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import io.mockk.verify +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.common.lagPersonResultaterForSøkerOgToBarn +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.nyOrdinærBehandling +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.toLocalDate +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPersonerMedAndeler +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdBarnetrygdClient +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.SatsService +import no.nav.familie.ba.sak.kjerne.beregning.SatsTidspunkt +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRequest +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrBostedsadresse.Companion.sisteAdresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrMatrikkeladresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrUkjentBosted +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrVegadresse +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.BehandlingStegStatus +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårsvurderingRepository +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringRepository +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringType +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.Matrikkeladresse +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import no.nav.familie.kontrakter.felles.personopplysning.Sivilstand +import no.nav.familie.kontrakter.felles.personopplysning.UkjentBosted +import no.nav.familie.kontrakter.felles.personopplysning.Vegadresse +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.YearMonth + +class BehandlingIntegrationTest( + @Autowired + private val behandlingRepository: BehandlingRepository, + + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @Autowired + private val personRepository: PersonRepository, + + @Autowired + private val vedtakService: VedtakService, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val beregningService: BeregningService, + + @Autowired + private val vilkårsvurderingRepository: VilkårsvurderingRepository, + + @Autowired + private val vilkårsvurderingService: VilkårsvurderingService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val mockPersonopplysningerService: PersonopplysningerService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val saksstatistikkMellomlagringRepository: SaksstatistikkMellomlagringRepository, + + @Autowired + private val infotrygdBarnetrygdClient: InfotrygdBarnetrygdClient, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val taskRepository: TaskRepositoryWrapper, + + @Autowired + private val vedtaksperiodeService: VedtaksperiodeService, +) : AbstractSpringIntegrationTest() { + + @BeforeEach + fun truncate() { + databaseCleanupService.truncate() + } + + @BeforeEach + fun førHverTest() { + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2022, 12, 31) + } + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + @Test + fun `Kjør flyway migreringer og sjekk at behandlingslagerservice klarer å lese å skrive til postgresql`() { + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + behandlingService.opprettBehandling( + nyOrdinærBehandling( + søkersIdent = fnr, + fagsakId = fagsak.id, + ), + ) + assertEquals(1, behandlingHentOgPersisterService.hentBehandlinger(fagsak.id).size) + } + + @Test + fun `Test at opprettEllerOppdaterBehandling kjører uten feil`() { + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + behandlingService.opprettBehandling( + nyOrdinærBehandling( + søkersIdent = fnr, + fagsakId = fagsak.id, + ), + ) + assertEquals( + 1, + behandlingHentOgPersisterService.hentBehandlinger(fagsak.id).size, + ) + } + + @Test + fun `Opprett behandling`() { + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + assertEquals(fagsak.id, behandling.fagsak.id) + } + + @Test + fun `Kast feil ved opprettelse av behandling for ny person med åpen sak i Infotrygd`() { + val fnr = randomFnr() + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + + every { infotrygdBarnetrygdClient.harÅpenSakIInfotrygd(listOf(fnr)) } returns true + + assertThatThrownBy { behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) } + .hasMessageContaining("sak i Infotrygd") + } + + @Test + fun `Kast feil ved opprettelse av behandling for ny person med løpende sak i Infotrygd, utenom migrering`() { + val fnr = randomFnr() + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + + every { infotrygdBarnetrygdClient.harLøpendeSakIInfotrygd(listOf(fnr)) } returns true + + assertThatThrownBy { behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) } + .hasMessageContaining("sak i Infotrygd") + + val behandling = behandlingService.opprettBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + skalBehandlesAutomatisk = true, + søkersIdent = fnr, + fagsakId = fagsak.id, + ), + ) + assertNotNull(vedtakService.hentAktivForBehandling(behandlingId = behandling.id)) + markerBehandlingSomAvsluttet(behandling) + assertDoesNotThrow { + behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak, + behandlingType = BehandlingType.REVURDERING, + ), + ) + } + } + + @Test + fun `Opprett behandle sak oppgave ved opprettelse av førstegangsbehandling`() { + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + behandlingService.opprettBehandling(nyOrdinærBehandling(søkersIdent = fnr, fagsakId = fagsak.id)) + + verify(exactly = 1) { + taskRepository.save(any()) + } + } + + @Test + fun `Opprett aktivt vedtak ved opprettelse av behandling`() { + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = + behandlingService.opprettBehandling(nyOrdinærBehandling(søkersIdent = fnr, fagsakId = fagsak.id)) + + assertNotNull(vedtakService.hentAktivForBehandling(behandlingId = behandling.id)) + } + + @Test + fun `Ikke opprett behandle sak oppgave ved opprettelse av fødselshendelsebehandling`() { + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + behandlingService.opprettBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + søkersIdent = fnr, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + skalBehandlesAutomatisk = true, + søknadMottattDato = LocalDate.now(), + fagsakId = fagsak.id, + ), + ) + + verify(exactly = 0) { + taskRepository.save(any()) + } + } + + @Test + fun `Kast feil om man lager ny behandling på fagsak som har behandling som skal godkjennes`() { + val morId = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsak(FagsakRequest(personIdent = morId)) + val behandling = + behandlingService.opprettBehandling(nyOrdinærBehandling(søkersIdent = morId, fagsakId = fagsak.data!!.id)) + behandling.behandlingStegTilstand.forEach { it.behandlingStegStatus = BehandlingStegStatus.UTFØRT } + behandling.behandlingStegTilstand.add( + BehandlingStegTilstand( + behandling = behandling, + behandlingSteg = StegType.BESLUTTE_VEDTAK, + ), + ) + behandlingRepository.saveAndFlush(behandling) + + assertThrows(Exception::class.java) { + behandlingService.opprettBehandling( + NyBehandling( + BehandlingKategori.NASJONAL, + BehandlingUnderkategori.ORDINÆR, + morId, + BehandlingType.REVURDERING, + BehandlingÅrsak.SØKNAD, + fagsakId = fagsak.data!!.id, + ), + ) + } + } + + @Test + fun `Bruk samme behandling hvis nytt barn kommer på fagsak med aktiv behandling`() { + val morId = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(morId) + behandlingService.opprettBehandling(nyOrdinærBehandling(søkersIdent = morId, fagsakId = fagsak.id)) + + assertEquals(1, behandlingHentOgPersisterService.hentBehandlinger(fagsakId = fagsak.id).size) + + behandlingService.opprettBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + søkersIdent = morId, + behandlingType = BehandlingType.REVURDERING, + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + søknadMottattDato = LocalDate.now(), + fagsakId = fagsak.id, + ), + ) + + val behandlinger = behandlingHentOgPersisterService.hentBehandlinger(fagsakId = fagsak.id) + assertEquals(1, behandlinger.size) + } + + @Test + fun `Opprett barnas beregning på vedtak`() { + val søkerFnr = randomFnr() + val barn1Fnr = randomFnr() + val barn2Fnr = randomFnr() + + val søkerAktørId = personidentService.hentAktør(søkerFnr) + val barn1AktørId = personidentService.hentAktør(barn1Fnr) + val barn2AktørId = personidentService.hentAktør(barn2Fnr) + + val januar2020 = YearMonth.of(2020, 1) + val oktober2020 = YearMonth.of(2020, 10) + val stønadTom = januar2020.plusYears(17) + + val fagsak = fagsakService.hentEllerOpprettFagsak(FagsakRequest(personIdent = søkerFnr)) + val behandling = behandlingService.opprettBehandling( + nyOrdinærBehandling( + søkersIdent = søkerFnr, + fagsakId = fagsak.data!!.id, + ), + ) + + val barnAktør = personidentService.hentOgLagreAktørIder(listOf(barn1Fnr, barn2Fnr), true) + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + søkerFnr, + listOf(barn1Fnr, barn2Fnr), + søkerAktør = behandling.fagsak.aktør, + barnAktør = barnAktør, + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + behandlingService.opprettOgInitierNyttVedtakForBehandling(behandling = behandling) + + val vilkårsvurdering = + Vilkårsvurdering(behandling = behandling) + vilkårsvurdering.personResultater = setOf( + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson(type = PersonType.SØKER, aktør = søkerAktørId), + resultat = Resultat.OPPFYLT, + periodeFom = januar2020.minusMonths(1).toLocalDate(), + periodeTom = stønadTom.toLocalDate(), + lagFullstendigVilkårResultat = true, + personType = PersonType.SØKER, + ), + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson(type = PersonType.BARN, aktør = barn1AktørId, fødselsdato = januar2020.minusYears(2).førsteDagIInneværendeMåned()), + resultat = Resultat.OPPFYLT, + periodeFom = januar2020.minusMonths(1).toLocalDate(), + periodeTom = stønadTom.toLocalDate(), + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + ), + lagPersonResultat( + vilkårsvurdering = vilkårsvurdering, + person = lagPerson(type = PersonType.BARN, aktør = barn2AktørId, fødselsdato = januar2020.førsteDagIInneværendeMåned()), + resultat = Resultat.OPPFYLT, + periodeFom = oktober2020.minusMonths(1).toLocalDate(), + periodeTom = stønadTom.toLocalDate(), + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + ), + ) + vilkårsvurderingRepository.save(vilkårsvurdering) + + val tilkjentYtelse = beregningService.oppdaterBehandlingMedBeregning(behandling, personopplysningGrunnlag) + val restVedtakBarnMap = + personopplysningGrunnlag.tilRestPersonerMedAndeler(andelerKnyttetTilPersoner = tilkjentYtelse.andelerTilkjentYtelse.toList()) + .associateBy( + { it.personIdent }, + { restPersonMedAndeler -> restPersonMedAndeler.ytelsePerioder.sortedBy { it.stønadFom } }, + ) + + val satsEndringDatoSeptember2020 = + SatsService.hentDatoForSatsendring(SatsType.TILLEGG_ORBA, 1354)!!.toYearMonth() + val satsEndringDatoSeptember2021 = + SatsService.hentDatoForSatsendring(SatsType.TILLEGG_ORBA, 1654)!!.toYearMonth() + val satsEndringDatoJanuar2022 = SatsService.hentDatoForSatsendring(SatsType.TILLEGG_ORBA, 1676)!!.toYearMonth() + assertEquals(2, restVedtakBarnMap.size) + + // Barn 1 + val barn1Perioder = restVedtakBarnMap[barn1Fnr]!!.sortedBy { it.stønadFom } + assertEquals(1054, barn1Perioder[0].beløp) + assertEquals(januar2020, barn1Perioder[0].stønadFom) + assertEquals(satsEndringDatoSeptember2020.minusMonths(1), barn1Perioder[0].stønadTom) + assertEquals(YtelseType.ORDINÆR_BARNETRYGD, barn1Perioder[0].ytelseType) + assertEquals(1354, barn1Perioder[1].beløp) + assertEquals(satsEndringDatoSeptember2020, barn1Perioder[1].stønadFom) + assertEquals(satsEndringDatoSeptember2021.minusMonths(1), barn1Perioder[1].stønadTom) + assertEquals(YtelseType.ORDINÆR_BARNETRYGD, barn1Perioder[1].ytelseType) + assertEquals(1654, barn1Perioder[2].beløp) + assertEquals(satsEndringDatoSeptember2021, barn1Perioder[2].stønadFom) + assertTrue(januar2020 < barn1Perioder[2].stønadTom) + assertEquals(YtelseType.ORDINÆR_BARNETRYGD, barn1Perioder[2].ytelseType) + assertEquals(1676, barn1Perioder[3].beløp) + assertEquals(satsEndringDatoJanuar2022, barn1Perioder[3].stønadFom) + assertTrue(januar2020 < barn1Perioder[3].stønadTom) + assertEquals(YtelseType.ORDINÆR_BARNETRYGD, barn1Perioder[3].ytelseType) + assertEquals(1054, barn1Perioder[4].beløp) + + // Barn 2 + val barn2Perioder = restVedtakBarnMap[barn2Fnr]!!.sortedBy { it.stønadFom } + assertEquals(1354, barn2Perioder[0].beløp) + assertEquals(oktober2020, barn2Perioder[0].stønadFom) + assertTrue(oktober2020 < barn2Perioder[0].stønadTom) + assertEquals(YtelseType.ORDINÆR_BARNETRYGD, barn2Perioder[0].ytelseType) + assertEquals(1654, barn2Perioder[1].beløp) + assertEquals(satsEndringDatoSeptember2021, barn2Perioder[1].stønadFom) + assertTrue(januar2020 < barn2Perioder[1].stønadTom) + assertEquals(YtelseType.ORDINÆR_BARNETRYGD, barn2Perioder[1].ytelseType) + assertEquals(1676, barn2Perioder[2].beløp) + assertEquals(satsEndringDatoJanuar2022, barn2Perioder[2].stønadFom) + assertTrue(januar2020 < barn2Perioder[2].stønadTom) + assertEquals(YtelseType.ORDINÆR_BARNETRYGD, barn2Perioder[2].ytelseType) + assertEquals(1054, barn2Perioder[3].beløp) + } + + @Test + fun `Endre barnas beregning på vedtak`() { + val søkerFnr = randomFnr() + val barn1Fnr = randomFnr() + val barn2Fnr = randomFnr() + val barn3Fnr = randomFnr() + + val søkerAktørId = personidentService.hentOgLagreAktør(søkerFnr, true) + val barn1AktørId = personidentService.hentOgLagreAktør(barn1Fnr, true) + val barn2AktørId = personidentService.hentOgLagreAktør(barn2Fnr, true) + val barn3AktørId = personidentService.hentOgLagreAktør(barn3Fnr, true) + + val januar2020 = YearMonth.of(2020, 1) + val januar2021 = YearMonth.of(2021, 1) + val stønadTom = januar2020.plusYears(17) + + val fagsak = fagsakService.hentEllerOpprettFagsak(FagsakRequest(personIdent = søkerFnr)) + val behandling = behandlingService.opprettBehandling( + nyOrdinærBehandling( + søkersIdent = søkerFnr, + fagsakId = fagsak.data!!.id, + ), + ) + + val barnAktør = personidentService.hentOgLagreAktørIder(listOf(barn1Fnr, barn2Fnr, barn3Fnr), true) + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + søkerFnr, + listOf(barn1Fnr, barn2Fnr, barn3Fnr), + søkerAktør = behandling.fagsak.aktør, + barnAktør = barnAktør, + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + assertNotNull(personopplysningGrunnlag) + + behandlingService.opprettOgInitierNyttVedtakForBehandling(behandling = behandling) + + val vilkårsvurdering = + Vilkårsvurdering(behandling = behandling) + vilkårsvurdering.personResultater = lagPersonResultaterForSøkerOgToBarn( + vilkårsvurdering, + søkerAktørId, + barn1AktørId, + barn2AktørId, + januar2020.minusMonths(1).toLocalDate(), + stønadTom.toLocalDate(), + ) + vilkårsvurderingRepository.save(vilkårsvurdering) + + beregningService.oppdaterBehandlingMedBeregning(behandling, personopplysningGrunnlag) + + val vilkårsvurdering2 = + Vilkårsvurdering(behandling = behandling) + vilkårsvurdering2.personResultater = lagPersonResultaterForSøkerOgToBarn( + vilkårsvurdering2, + søkerAktørId, + barn1AktørId, + barn3AktørId, + januar2021.minusMonths(1).toLocalDate(), + stønadTom.toLocalDate(), + ) + vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = vilkårsvurdering2) + + val satsEndringDatoSeptember2021 = + SatsService.hentDatoForSatsendring(SatsType.TILLEGG_ORBA, 1654)!!.toYearMonth() + val satsEndringDatoJanuar2022 = SatsService.hentDatoForSatsendring(SatsType.TILLEGG_ORBA, 1676)!!.toYearMonth() + + val tilkjentYtelse = beregningService.oppdaterBehandlingMedBeregning(behandling, personopplysningGrunnlag) + val restVedtakBarnMap = + personopplysningGrunnlag.tilRestPersonerMedAndeler(andelerKnyttetTilPersoner = tilkjentYtelse.andelerTilkjentYtelse.toList()) + .associateBy( + { it.personIdent }, + { restPersonMedAndeler -> restPersonMedAndeler.ytelsePerioder.sortedBy { it.stønadFom } }, + ) + + assertEquals(2, restVedtakBarnMap.size) + + // Barn 1 + val barn1Perioder = restVedtakBarnMap[barn1Fnr]!!.sortedBy { it.stønadFom } + assertEquals(4, barn1Perioder.size) + assertEquals(1354, barn1Perioder[0].beløp) + assertEquals(januar2021, barn1Perioder[0].stønadFom) + assertEquals(satsEndringDatoSeptember2021.minusMonths(1), barn1Perioder[0].stønadTom) + assertEquals(1654, barn1Perioder[1].beløp) + assertEquals(satsEndringDatoSeptember2021, barn1Perioder[1].stønadFom) + assertEquals(1676, barn1Perioder[2].beløp) + assertEquals(satsEndringDatoJanuar2022, barn1Perioder[2].stønadFom) + assertEquals(1054, barn1Perioder[3].beløp) + assertTrue(stønadTom >= barn1Perioder[3].stønadTom) + + // Barn 3 + val barn3perioder = restVedtakBarnMap[barn3Fnr]!!.sortedBy { it.stønadFom } + assertEquals(4, barn3perioder.size) + assertEquals(1354, barn3perioder[0].beløp) + assertEquals(januar2021, barn3perioder[0].stønadFom) + assertEquals(satsEndringDatoSeptember2021.minusMonths(1), barn3perioder[0].stønadTom) + assertEquals(1654, barn3perioder[1].beløp) + assertEquals(satsEndringDatoSeptember2021, barn3perioder[1].stønadFom) + assertEquals(1676, barn3perioder[2].beløp) + assertEquals(satsEndringDatoJanuar2022, barn3perioder[2].stønadFom) + assertEquals(1054, barn3perioder[3].beløp) + assertTrue(stønadTom >= barn3perioder[3].stønadTom) + } + + @Test + fun `Hent en persons bostedsadresse fra PDL og lagre den i database`() { + val søkerFnr = randomFnr() + val barn1Fnr = randomFnr() + val barn2Fnr = randomFnr() + + val matrikkelId = 123456L + val søkerHusnummer = "12" + val søkerHusbokstav = "A" + val søkerBruksenhetsnummer = "H012" + val søkerAdressnavn = "Sannergate" + val søkerKommunenummer = "1234" + val søkerTilleggsnavn = "whatever" + val søkerPostnummer = "2222" + + val barn1Bruksenhetsnummer = "H201" + val barn1Tilleggsnavn = "whoknows" + val barn1Postnummer = "3333" + val barn1Kommunenummer = "3233" + val barn2BostedKommune = "Oslo" + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(søkerFnr)) } returns PersonInfo( + fødselsdato = LocalDate.of(1990, 1, 1), + adressebeskyttelseGradering = null, + navn = "Mor", + kjønn = Kjønn.KVINNE, + forelderBarnRelasjon = emptySet(), + bostedsadresser = mutableListOf( + Bostedsadresse( + vegadresse = Vegadresse( + matrikkelId, + søkerHusnummer, + søkerHusbokstav, + søkerBruksenhetsnummer, + søkerAdressnavn, + søkerKommunenummer, + søkerTilleggsnavn, + søkerPostnummer, + ), + ), + ), + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UOPPGITT)), + ) + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(barn1Fnr)) } returns PersonInfo( + fødselsdato = LocalDate.of(2009, 1, 1), + adressebeskyttelseGradering = null, + navn = "Gutt", + kjønn = Kjønn.MANN, + forelderBarnRelasjon = emptySet(), + bostedsadresser = mutableListOf( + Bostedsadresse( + matrikkeladresse = Matrikkeladresse( + matrikkelId, + barn1Bruksenhetsnummer, + barn1Tilleggsnavn, + barn1Postnummer, + barn1Kommunenummer, + ), + ), + ), + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UOPPGITT)), + ) + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(barn2Fnr)) } returns PersonInfo( + fødselsdato = LocalDate.of(2012, 1, 1), + adressebeskyttelseGradering = null, + navn = "Jente", + kjønn = Kjønn.KVINNE, + forelderBarnRelasjon = emptySet(), + bostedsadresser = mutableListOf(Bostedsadresse(ukjentBosted = UkjentBosted(barn2BostedKommune))), + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UOPPGITT)), + ) + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(søkerFnr)) } returns PersonInfo( + fødselsdato = LocalDate.of(1990, 1, 1), + bostedsadresser = mutableListOf( + Bostedsadresse( + vegadresse = Vegadresse( + matrikkelId, + søkerHusnummer, + søkerHusbokstav, + søkerBruksenhetsnummer, + søkerAdressnavn, + søkerKommunenummer, + søkerTilleggsnavn, + søkerPostnummer, + ), + ), + ), + ) + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(barn1Fnr)) } returns PersonInfo( + fødselsdato = LocalDate.of(2009, 1, 1), + bostedsadresser = mutableListOf( + Bostedsadresse( + matrikkeladresse = Matrikkeladresse( + matrikkelId, + barn1Bruksenhetsnummer, + barn1Tilleggsnavn, + barn1Postnummer, + barn1Kommunenummer, + ), + ), + ), + ) + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(tilAktør(barn2Fnr)) } returns PersonInfo( + fødselsdato = LocalDate.of(1990, 1, 1), + bostedsadresser = mutableListOf(Bostedsadresse(ukjentBosted = UkjentBosted(barn2BostedKommune))), + ) + + val fagsak = fagsakService.hentEllerOpprettFagsak(FagsakRequest(personIdent = søkerFnr)) + val behandling = behandlingService.opprettBehandling( + nyOrdinærBehandling( + søkersIdent = søkerFnr, + fagsakId = fagsak.data!!.id, + ), + ) + + val søkerAktør = personidentService.hentOgLagreAktør(søkerFnr, true) + val barn1Aktør = personidentService.hentOgLagreAktør(barn1Fnr, true) + val barn2Aktør = personidentService.hentOgLagreAktør(barn2Fnr, true) + + persongrunnlagService.hentOgLagreSøkerOgBarnINyttGrunnlag( + søkerAktør, + listOf(barn1Aktør, barn2Aktør), + behandling, + Målform.NB, + ) + + val søker = personRepository.findByAktør(søkerAktør).first() + val vegadresse = søker.bostedsadresser.sisteAdresse() as GrVegadresse + assertEquals(søkerAdressnavn, vegadresse.adressenavn) + assertEquals(matrikkelId, vegadresse.matrikkelId) + assertEquals(søkerBruksenhetsnummer, vegadresse.bruksenhetsnummer) + assertEquals(søkerHusbokstav, vegadresse.husbokstav) + assertEquals(søkerHusnummer, vegadresse.husnummer) + assertEquals(søkerKommunenummer, vegadresse.kommunenummer) + assertEquals(søkerPostnummer, vegadresse.postnummer) + assertEquals(søkerTilleggsnavn, vegadresse.tilleggsnavn) + + assertEquals(3, søker.personopplysningGrunnlag.personer.size) + + søker.personopplysningGrunnlag.barna.forEach { + when (it.aktør.aktivFødselsnummer()) { + barn1Fnr -> { + val matrikkeladresse = it.bostedsadresser.sisteAdresse() as GrMatrikkeladresse + assertEquals(barn1Bruksenhetsnummer, matrikkeladresse.bruksenhetsnummer) + assertEquals(barn1Kommunenummer, matrikkeladresse.kommunenummer) + assertEquals(barn1Postnummer, matrikkeladresse.postnummer) + assertEquals(barn1Tilleggsnavn, matrikkeladresse.tilleggsnavn) + } + + barn2Fnr -> { + val ukjentBosted = it.bostedsadresser.sisteAdresse() as GrUkjentBosted + assertEquals(barn2BostedKommune, ukjentBosted.bostedskommune) + } + + else -> { + throw RuntimeException("Ujent barn fnr") + } + } + } + } + + @Test + fun `Skal lagre og sende korrekt sakstatistikk for behandlingsresultat`() { + val fnr = "12345678910" + val fagsak = fagsakService.hentEllerOpprettFagsak(FagsakRequest(personIdent = fnr)) + val behandling = + behandlingService.opprettBehandling(nyOrdinærBehandling(søkersIdent = fnr, fagsakId = fagsak.data!!.id)) + behandlingService.opprettOgInitierNyttVedtakForBehandling(behandling = behandling) + val vedtak = vedtakService.hentAktivForBehandling(behandling.id) + + vedtakService.oppdater(vedtak!!) + + behandlingHentOgPersisterService.lagreEllerOppdater( + behandling.also { + it.resultat = Behandlingsresultat.AVSLÅTT + }, + ) + + val behandlingDvhMeldinger = saksstatistikkMellomlagringRepository.finnMeldingerKlarForSending() + .filter { it.type == SaksstatistikkMellomlagringType.BEHANDLING } + .map { it.jsonToBehandlingDVH() } + + assertEquals(2, behandlingDvhMeldinger.size) + assertThat(behandlingDvhMeldinger.last().resultat).isEqualTo("AVSLÅTT") + } + + private fun markerBehandlingSomAvsluttet(behandling: Behandling): Behandling { + behandling.status = BehandlingStatus.AVSLUTTET + behandling.leggTilBehandlingStegTilstand(StegType.BEHANDLING_AVSLUTTET) + return behandlingHentOgPersisterService.lagreOgFlush(behandling) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingServiceTest.kt new file mode 100644 index 000000000..818e836d0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingServiceTest.kt @@ -0,0 +1,188 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import io.mockk.every +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.nyOrdinærBehandling +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.LocalDateTime + +@Tag("integration") +class BehandlingServiceTest( + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @Autowired + private val beregningService: BeregningService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + + @Autowired + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val behandlingRepository: BehandlingRepository, + + @Autowired + private val stegService: StegService, + + @Autowired + private val mockPersonopplysningerService: PersonopplysningerService, +) : AbstractSpringIntegrationTest() { + + @BeforeAll + fun init() { + databaseCleanupService.truncate() + } + + @Test + fun `Skal rulle tilbake behandling om noe feiler etter opprettelse`() { + databaseCleanupService.truncate() + + val feilmelding = "Feil ved henting av personinformasjon" + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(any()) } answers { + throw Feil( + feilmelding, + ) + } + + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val error = assertThrows { + stegService.håndterNyBehandlingOgSendInfotrygdFeed( + nyOrdinærBehandling( + søkersIdent = fnr, + fagsakId = fagsak.id, + ), + ) + } + + assertEquals(feilmelding, error.message) + + val behandlinger = behandlingRepository.finnBehandlinger(fagsakId = fagsak.id) + assertEquals(0, behandlinger.size) + } + + @Test + fun `Skal svare med behandling som er opprettet før X tid`() { + databaseCleanupService.truncate() + + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val behandlingerSomErOpprettetFørIMorgen = + behandlingRepository.finnÅpneBehandlinger(LocalDateTime.now().plusDays(1)) + + assertEquals(1, behandlingerSomErOpprettetFørIMorgen.size) + assertEquals(behandling.id, behandlingerSomErOpprettetFørIMorgen.single().id) + } + + @Test + fun `Skal hente forrige behandling`() { + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + tilkjentYtelseRepository.save( + lagInitiellTilkjentYtelse(behandling).also { + it.utbetalingsoppdrag = "Utbetalingsoppdrag()" + }, + ) + ferdigstillBehandling(behandling) + + val revurderingInnvilgetBehandling = + behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.REVURDERING, + ), + ) + + val forrigeBehandling = + behandlingHentOgPersisterService.hentForrigeBehandlingSomErVedtatt(behandling = revurderingInnvilgetBehandling) + Assertions.assertNotNull(forrigeBehandling) + assertEquals(behandling.id, forrigeBehandling?.id) + } + + @Test + fun `Skal bare hente barn med tilkjentytelse for den aktuelle behandlingen`() { + val søker = randomFnr() + val barn = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søker) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + tilkjentYtelseRepository.save( + TilkjentYtelse( + behandling = behandling, + opprettetDato = LocalDate.now(), + endretDato = LocalDate.now(), + andelerTilkjentYtelse = mutableSetOf(), + ), + ) + + val barnAktør = personidentService.hentOgLagreAktørIder(listOf(barn), true) + val testPersonopplysningsGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søker, + barnasIdenter = listOf(barn), + søkerAktør = fagsak.aktør, + barnAktør = barnAktør, + ) + personopplysningGrunnlagRepository.save(testPersonopplysningsGrunnlag) + + assertEquals( + 0, + beregningService.finnBarnFraBehandlingMedTilkjentYtelse(behandlingId = behandling.id).size, + ) + } + + private fun ferdigstillBehandling(behandling: Behandling) { + behandlingService.oppdaterStatusPåBehandling(behandling.id, BehandlingStatus.AVSLUTTET) + behandlingService.leggTilStegPåBehandlingOgSettTidligereStegSomUtført( + behandling.id, + StegType.BEHANDLING_AVSLUTTET, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingStateTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingStateTest.kt new file mode 100644 index 000000000..b0bfd60c0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/BehandlingStateTest.kt @@ -0,0 +1,111 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +class BehandlingStateTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingRepository: BehandlingRepository, + @Autowired private val databaseCleanupService: DatabaseCleanupService, +) : AbstractSpringIntegrationTest() { + + private lateinit var fagsak: Fagsak + + @BeforeEach + fun setUp() { + databaseCleanupService.truncate() + fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(tilfeldigPerson().aktør.aktivFødselsnummer()) + } + + @Nested + inner class AktivBehandling { + @Test + fun `kan ikke ha flere behandlinger med aktiv true`() { + opprettBehandling(status = BehandlingStatus.AVSLUTTET, aktiv = true) + assertThatThrownBy { + opprettBehandling(status = BehandlingStatus.AVSLUTTET, aktiv = true) + }.hasMessageContaining("uidx_behandling_01") + } + + @Test + fun `skal kunne ha aktiv tvers ulike fagsaker`() { + opprettBehandling(status = BehandlingStatus.AVSLUTTET, aktiv = true) + val annenFagsak = + fagsakService.hentEllerOpprettFagsakForPersonIdent(tilfeldigPerson().aktør.aktivFødselsnummer()) + opprettBehandling(annenFagsak, status = BehandlingStatus.AVSLUTTET, aktiv = true) + } + } + + @Nested + inner class BehandlingStatuser { + + @Test + fun `kan ha flere behandlinger som er avsluttet`() { + opprettBehandling(status = BehandlingStatus.AVSLUTTET, aktiv = false) + opprettBehandling(status = BehandlingStatus.AVSLUTTET, aktiv = true) + } + + @Test + fun `kan ha en behandling på maskinell vent og en med status utredes`() { + opprettBehandling(status = BehandlingStatus.AVSLUTTET, aktiv = false) + opprettBehandling(status = BehandlingStatus.SATT_PÅ_MASKINELL_VENT, aktiv = false) + opprettBehandling(status = BehandlingStatus.UTREDES, aktiv = true) + } + + @Test + fun `kan ikke ha 2 behandlinger med status SATT_PÅ_VENTSATT_PÅ_MASKINELL_VENT`() { + opprettBehandling(status = BehandlingStatus.SATT_PÅ_MASKINELL_VENT, aktiv = false) + assertThatThrownBy { + opprettBehandling(status = BehandlingStatus.SATT_PÅ_MASKINELL_VENT, aktiv = true) + }.hasMessageContaining("uidx_behandling_03") + } + + @Test + fun `kan maks ha en parallell behandling i arbeid`() { + BehandlingStatus.values().filter { it != BehandlingStatus.AVSLUTTET && it != BehandlingStatus.SATT_PÅ_MASKINELL_VENT } + .forEach { + behandlingRepository.deleteAll() + opprettBehandling(status = it, aktiv = false) + assertThatThrownBy { + opprettBehandling(status = it, aktiv = true) + }.hasMessageContaining("uidx_behandling_02") + } + } + } + + private fun opprettBehandling(status: BehandlingStatus, aktiv: Boolean): Behandling { + return opprettBehandling(fagsak, status, aktiv) + } + + private fun opprettBehandling( + fagsak: Fagsak, + status: BehandlingStatus, + aktiv: Boolean, + ): Behandling { + val behandling = Behandling( + fagsak = fagsak, + opprettetÅrsak = BehandlingÅrsak.NYE_OPPLYSNINGER, + type = BehandlingType.REVURDERING, + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + status = status, + aktiv = aktiv, + ).initBehandlingStegTilstand() + return behandlingRepository.saveAndFlush(behandling) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/SnikeIK\303\270enServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/SnikeIK\303\270enServiceTest.kt" new file mode 100644 index 000000000..24f5abb02 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/SnikeIK\303\270enServiceTest.kt" @@ -0,0 +1,312 @@ +package no.nav.familie.ba.sak.kjerne.behandling + +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.domene.ArbeidsfordelingPåBehandling +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.domene.ArbeidsfordelingPåBehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentService +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class SnikeIKøenServiceTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingRepository: BehandlingRepository, + @Autowired private val tilkjentYtelseRepository: TilkjentYtelseRepository, + @Autowired private val hentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val databaseCleanupService: DatabaseCleanupService, + @Autowired private val snikeIKøenService: SnikeIKøenService, + @Autowired private val settPåVentService: SettPåVentService, + @Autowired private val vedtakRepository: VedtakRepository, + @Autowired private val vedtaksperiodeHentOgPersisterService: VedtaksperiodeHentOgPersisterService, + @Autowired private val arbeidsfordelingPåBehandlingRepository: ArbeidsfordelingPåBehandlingRepository, +) : AbstractSpringIntegrationTest() { + + private lateinit var fagsak: Fagsak + private var skalVenteLitt = false // for å unngå at behandlingen opprettes med samme tidspunkt + + @BeforeEach + fun setUp() { + skalVenteLitt = false + databaseCleanupService.truncate() + fagsak = opprettLøpendeFagsak() + } + + private fun opprettLøpendeFagsak(): Fagsak { + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(tilfeldigPerson().aktør.aktivFødselsnummer()) + return fagsakService.lagre(fagsak.copy(status = FagsakStatus.LØPENDE)) + } + + @ParameterizedTest + @EnumSource(BehandlingStatus::class, names = ["UTREDES", "SATT_PÅ_VENT"], mode = EnumSource.Mode.INCLUDE) + fun `skal kunne sette en behandling med status UTREDES eller SATT_PÅ_VENT på maskinell vent`(status: BehandlingStatus) { + val behandling = opprettBehandling(status = status) + + settAktivBehandlingTilPåMaskinellVent(behandling) + + val oppdatertBehandling = behandlingRepository.finnBehandling(behandling.id) + assertThat(behandling.status).isEqualTo(status) + assertThat(behandling.aktiv).isTrue() + assertThat(oppdatertBehandling.status).isEqualTo(BehandlingStatus.SATT_PÅ_MASKINELL_VENT) + assertThat(oppdatertBehandling.aktiv).isFalse() + } + + @Test + fun `reaktivering av behandling skal sette status tilbake til UTREDES`() { + val behandling1 = opprettBehandling() + settAktivBehandlingTilPåMaskinellVent(behandling1) + val behandling2 = opprettBehandling(status = BehandlingStatus.AVSLUTTET, aktiv = true) + + snikeIKøenService.reaktiverBehandlingPåMaskinellVent(behandling2) + + val oppdatertBehandling = behandlingRepository.finnBehandling(behandling1.id) + assertThat(oppdatertBehandling.status).isEqualTo(BehandlingStatus.UTREDES) + assertThat(oppdatertBehandling.aktiv).isTrue() + } + + @Test + fun `reaktivering av behandling som er på vent skal sette status tilbake til SATT_PÅ_VENT`() { + val behandling1 = opprettBehandling() + lagreArbeidsfordeling(behandling1) + settPåVentService.settBehandlingPåVent(behandling1.id, LocalDate.now(), SettPåVentÅrsak.AVVENTER_DOKUMENTASJON) + settAktivBehandlingTilPåMaskinellVent(behandling1) + val behandling2 = opprettBehandling(status = BehandlingStatus.AVSLUTTET, aktiv = true) + + snikeIKøenService.reaktiverBehandlingPåMaskinellVent(behandling2) + + val oppdatertBehandling = behandlingRepository.finnBehandling(behandling1.id) + assertThat(oppdatertBehandling.status).isEqualTo(BehandlingStatus.SATT_PÅ_VENT) + assertThat(oppdatertBehandling.aktiv).isTrue() + } + + @Test + fun `siste behandlingen er den som er aktiv til at behandlingen som er satt på vent er aktivert på nytt`() { + opprettBehandling(status = BehandlingStatus.SATT_PÅ_MASKINELL_VENT, aktiv = false) + val behandling2 = opprettBehandling(status = BehandlingStatus.AVSLUTTET, aktiv = true) + lagUtbetalingsoppdragOgAvslutt(behandling2) + + validerSisteBehandling(behandling2) + validerErAktivBehandling(behandling2) + } + + @Test + fun `behandling som er satt på vent blir aktivert, men ennå ikke iverksatt, og er då siste aktive behandlingen`() { + val behandling1 = opprettBehandling(status = BehandlingStatus.SATT_PÅ_MASKINELL_VENT, aktiv = false) + val behandling2 = opprettBehandling(status = BehandlingStatus.AVSLUTTET, aktiv = true) + lagUtbetalingsoppdragOgAvslutt(behandling2) + + snikeIKøenService.reaktiverBehandlingPåMaskinellVent(behandling2) + + validerSisteBehandling(behandling2) + validerErAktivBehandling(behandling1) + } + + @Test + fun `behandling som er satt på vent blir aktivert og iverksatt, og er då siste aktive behandlingen`() { + val behandling1 = opprettBehandling(status = BehandlingStatus.SATT_PÅ_MASKINELL_VENT, aktiv = false) + val behandling2 = opprettBehandling(status = BehandlingStatus.AVSLUTTET, aktiv = true) + lagUtbetalingsoppdragOgAvslutt(behandling2) + + snikeIKøenService.reaktiverBehandlingPåMaskinellVent(behandling2) + lagUtbetalingsoppdragOgAvslutt(behandling1) + + validerSisteBehandling(behandling1) + validerErAktivBehandling(behandling1) + } + + @Test + fun `reaktivering skal tilbakestille behandling på vent`() { + val behandling1 = opprettBehandling(status = BehandlingStatus.SATT_PÅ_MASKINELL_VENT, aktiv = false).let { + leggTilSteg(it, StegType.VURDER_TILBAKEKREVING) + behandlingRepository.saveAndFlush(it) + } + val vedtak = vedtakRepository.saveAndFlush(lagVedtak(behandling = behandling1)) + vedtaksperiodeHentOgPersisterService.lagre( + VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + type = Vedtaksperiodetype.FORTSATT_INNVILGET, + ), + ) + val behandling2 = opprettBehandling(status = BehandlingStatus.AVSLUTTET, aktiv = true) + lagUtbetalingsoppdragOgAvslutt(behandling2) + + snikeIKøenService.reaktiverBehandlingPåMaskinellVent(behandling2) + + assertThat(vedtaksperiodeHentOgPersisterService.finnVedtaksperioderFor(vedtak.id)).isEmpty() + } + + @Test + fun `skal ikke reaktivere noe hvis det ikke finnes en behandling som er på maskinell vent`() { + val behandling2 = opprettBehandling( + status = BehandlingStatus.AVSLUTTET, + aktiv = true, + ) + + assertThat(snikeIKøenService.reaktiverBehandlingPåMaskinellVent(behandling2)).isFalse() + assertThat(behandlingRepository.finnBehandling(behandling2.id).aktiv).isTrue() + } + + @Test + fun `skal kunne reaktivere en behandling selv om det ikke finnes en annen behandling som er aktiv, eks henlagt`() { + val behandling1 = opprettBehandling(status = BehandlingStatus.SATT_PÅ_MASKINELL_VENT, aktiv = false) + val behandling2 = opprettBehandling( + status = BehandlingStatus.AVSLUTTET, + aktiv = false, + resultat = Behandlingsresultat.HENLAGT_FEILAKTIG_OPPRETTET, + ) + + snikeIKøenService.reaktiverBehandlingPåMaskinellVent(behandling2) + + assertThat(behandlingRepository.finnBehandling(behandling1.id).aktiv).isTrue() + assertThat(behandlingRepository.finnBehandling(behandling2.id).aktiv).isFalse() + } + + @Nested + inner class ValideringAvSettPåVent { + + @Test + fun `kan ikke sette en inaktiv behandling på vent`() { + val behandling = opprettBehandling(aktiv = false) + + assertThatThrownBy { + settAktivBehandlingTilPåMaskinellVent(behandling) + }.hasMessageContaining("er ikke aktiv") + } + + @ParameterizedTest + @EnumSource(BehandlingStatus::class, names = ["UTREDES", "SATT_PÅ_VENT"], mode = EnumSource.Mode.EXCLUDE) + fun `kan ikke sette en behandling på vent med annen status enn UTREDES eller SATT_PÅ_VENT`(status: BehandlingStatus) { + val behandling = opprettBehandling(status = status) + + assertThatThrownBy { + settAktivBehandlingTilPåMaskinellVent(behandling) + }.hasMessageContaining("kan ikke settes på maskinell vent då status") + } + } + + @Nested + inner class ValideringAvReaktiverBehandling { + + @Test + fun `skal feile når åpen behandling er aktiv`() { + val behandlingPåVent = opprettBehandling(status = BehandlingStatus.SATT_PÅ_MASKINELL_VENT, aktiv = true) + val behandlingSomSnekIKøen = opprettBehandling(status = BehandlingStatus.AVSLUTTET, aktiv = false) + + assertThatThrownBy { snikeIKøenService.reaktiverBehandlingPåMaskinellVent(behandlingSomSnekIKøen) } + .hasMessageContaining("Åpen behandling har feil tilstand") + } + + @Test + fun `skal feile når behandling som snek i køen har status satt på vent`() { + opprettBehandling(status = BehandlingStatus.SATT_PÅ_MASKINELL_VENT, aktiv = false) + val behandlingSomSnekIKøen = opprettBehandling(status = BehandlingStatus.UTREDES, aktiv = true) + + assertThatThrownBy { snikeIKøenService.reaktiverBehandlingPåMaskinellVent(behandlingSomSnekIKøen) } + .hasMessageContaining("er ikke avsluttet") + } + } + + private fun settAktivBehandlingTilPåMaskinellVent(behandling: Behandling) { + snikeIKøenService.settAktivBehandlingTilPåMaskinellVent(behandling.id, SettPåMaskinellVentÅrsak.SATSENDRING) + } + + private fun validerSisteBehandling(behandling: Behandling) { + val fagsakId = fagsak.id + assertThat(behandlingRepository.finnSisteIverksatteBehandling(fagsakId)!!.id).isEqualTo(behandling.id) + assertThat(behandlingRepository.finnSisteIverksatteBehandlingFraLøpendeFagsaker()).containsExactly(behandling.id) + assertThat(behandlingRepository.finnSisteIverksatteBehandling(fagsakId)!!.id).isEqualTo(behandling.id) + + assertThat(hentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId)!!.id).isEqualTo(behandling.id) + assertThat(hentOgPersisterService.hentSisteBehandlingSomErVedtatt(fagsakId)!!.id).isEqualTo(behandling.id) + assertThat( + hentOgPersisterService.hentSisteBehandlingSomErSendtTilØkonomiPerFagsak(setOf(fagsakId)).single().id, + ).isEqualTo(behandling.id) + } + + private fun validerErAktivBehandling(behandling: Behandling) { + assertThat(hentOgPersisterService.finnAktivForFagsak(behandling.fagsak.id)!!.id) + .isEqualTo(behandling.id) + } + + private fun lagUtbetalingsoppdragOgAvslutt(behandling: Behandling) { + val tilkjentYtelse = lagInitiellTilkjentYtelse(behandling, utbetalingsoppdrag = "utbetalingsoppdrag") + tilkjentYtelseRepository.saveAndFlush(tilkjentYtelse) + behandlingRepository.finnBehandling(behandling.id).let { behandling -> + leggTilSteg(behandling, StegType.BEHANDLING_AVSLUTTET) + behandling.status = BehandlingStatus.AVSLUTTET + behandlingRepository.saveAndFlush(behandling) + } + } + + private fun leggTilSteg(behandling: Behandling, stegType: StegType) { + val stegTilstand = BehandlingStegTilstand(behandling = behandling, behandlingSteg = stegType) + behandling.behandlingStegTilstand.add(stegTilstand) + } + + private fun opprettBehandling( + status: BehandlingStatus = BehandlingStatus.UTREDES, + resultat: Behandlingsresultat = Behandlingsresultat.INNVILGET, + aktiv: Boolean = true, + ): Behandling = opprettBehandling(fagsak, status, resultat, aktiv) + + private fun opprettBehandling( + fagsak: Fagsak, + status: BehandlingStatus, + resultat: Behandlingsresultat, + aktiv: Boolean, + ): Behandling { + if (skalVenteLitt) { + Thread.sleep(10) + } else { + skalVenteLitt = true + } + val behandling = Behandling( + fagsak = fagsak, + opprettetÅrsak = BehandlingÅrsak.NYE_OPPLYSNINGER, + type = BehandlingType.REVURDERING, + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + status = status, + aktiv = aktiv, + resultat = resultat, + ).initBehandlingStegTilstand() + return behandlingRepository.saveAndFlush(behandling) + } + + private fun lagreArbeidsfordeling(behandling1: Behandling) { + val arbeidsfordelingPåBehandling = ArbeidsfordelingPåBehandling( + behandlingId = behandling1.id, + behandlendeEnhetId = "4820", + behandlendeEnhetNavn = "Enhet", + ) + arbeidsfordelingPåBehandlingRepository.saveAndFlush(arbeidsfordelingPåBehandling) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingRepositoryTest.kt new file mode 100644 index 000000000..1cb618afe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/domene/BehandlingRepositoryTest.kt @@ -0,0 +1,104 @@ +package no.nav.familie.ba.sak.kjerne.behandling.domene + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus.AVSLUTTET +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus.IVERKSETTER_VEDTAK +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDateTime + +class BehandlingRepositoryTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingRepository: BehandlingRepository, + @Autowired private val tilkjentRepository: TilkjentYtelseRepository, + @Autowired private val databaseCleanupService: DatabaseCleanupService, +) : AbstractSpringIntegrationTest() { + + @BeforeEach + fun cleanUp() { + databaseCleanupService.truncate() + } + + @Nested + inner class FinnSisteIverksatteBehandling { + + val tilfeldigPerson = tilfeldigPerson() + val tilfeldigPerson2 = tilfeldigPerson() + lateinit var fagsak: Fagsak + lateinit var fagsak2: Fagsak + + @BeforeEach + fun setUp() { + fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(tilfeldigPerson.aktør.aktivFødselsnummer()) + fagsak2 = fagsakService.hentEllerOpprettFagsakForPersonIdent(tilfeldigPerson2.aktør.aktivFødselsnummer()) + } + + @Test + fun `skal finne siste iverksatte behandlingen som har utbetalingsoppdrag, som er avsluttet`() { + opprettBehandling(fagsak, AVSLUTTET, LocalDateTime.now().minusDays(3)) + .medTilkjentYtelse(true) + val behandling2 = opprettBehandling(fagsak, AVSLUTTET, LocalDateTime.now().minusDays(2)) + .medTilkjentYtelse(true) + opprettBehandling(fagsak, IVERKSETTER_VEDTAK, LocalDateTime.now().minusDays(1)) + .medTilkjentYtelse(true) + + val behandling4 = opprettBehandling(fagsak2, AVSLUTTET, LocalDateTime.now()) + .medTilkjentYtelse(true) + + assertThat(behandlingRepository.finnSisteIverksatteBehandling(fagsak.id)!!).isEqualTo(behandling2) + assertThat(behandlingRepository.finnSisteIverksatteBehandling(fagsak2.id)!!).isEqualTo(behandling4) + } + + @Test + fun `skal finne siste iverksatte behandlingen som har utbetalingsoppdrag`() { + opprettBehandling(fagsak, AVSLUTTET, LocalDateTime.now().minusDays(3)) + .medTilkjentYtelse(true) + val behandling3 = opprettBehandling(fagsak, AVSLUTTET, LocalDateTime.now().minusDays(1)) + .medTilkjentYtelse(true) + + opprettBehandling(fagsak, AVSLUTTET).medTilkjentYtelse() + opprettBehandling(fagsak, IVERKSETTER_VEDTAK).medTilkjentYtelse() + + assertThat(behandlingRepository.finnSisteIverksatteBehandling(fagsak.id)!!).isEqualTo(behandling3) + } + } + + private fun opprettBehandling( + fagsak: Fagsak, + behandlingStatus: BehandlingStatus, + aktivertTidspunkt: LocalDateTime = LocalDateTime.now(), + aktiv: Boolean = false, + ): Behandling { + val behandling = lagBehandling(fagsak = fagsak, status = behandlingStatus) + .copy( + id = 0, + aktiv = aktiv, + aktivertTidspunkt = aktivertTidspunkt, + ) + val oppdaterteSteg = behandling.behandlingStegTilstand.map { it.copy(behandling = behandling) } + behandling.behandlingStegTilstand.clear() + behandling.behandlingStegTilstand.addAll(oppdaterteSteg) + return behandlingRepository.saveAndFlush(behandling).let { + behandlingRepository.finnBehandling(it.id) + } + } + + private fun Behandling.medTilkjentYtelse(medUtbetalingsoppdrag: Boolean = false) = + this.also { + val tilkjentYtelse = lagInitiellTilkjentYtelse( + behandling = it, + utbetalingsoppdrag = if (medUtbetalingsoppdrag) "~" else null, + ) + tilkjentRepository.saveAndFlush(tilkjentYtelse) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentServiceTest.kt" new file mode 100644 index 000000000..4d7316ab1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/behandling/settp\303\245vent/SettP\303\245VentServiceTest.kt" @@ -0,0 +1,319 @@ +package no.nav.familie.ba.sak.kjerne.behandling.settpåvent + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.kjørStegprosessForFGB +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.SettPåMaskinellVentÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.SnikeIKøenService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.task.TaBehandlingerEtterVentefristAvVentTask +import no.nav.familie.prosessering.domene.Task +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.assertj.core.api.Assertions.catchThrowable +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.repository.findByIdOrNull +import java.time.LocalDate + +@Tag("integration") +class SettPåVentServiceTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingService: BehandlingService, + @Autowired private val behandlingRepository: BehandlingRepository, + @Autowired private val databaseCleanupService: DatabaseCleanupService, + @Autowired private val stegService: StegService, + @Autowired private val settPåVentService: SettPåVentService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val persongrunnlagService: PersongrunnlagService, + @Autowired private val vilkårsvurderingService: VilkårsvurderingService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, + @Autowired private val settPåVentRepository: SettPåVentRepository, + @Autowired private val taBehandlingerEtterVentefristAvVentTask: TaBehandlingerEtterVentefristAvVentTask, + @Autowired private val brevmalService: BrevmalService, + @Autowired private val snikeIKøenService: SnikeIKøenService, +) : AbstractSpringIntegrationTest() { + + @BeforeAll + fun init() { + databaseCleanupService.truncate() + } + + @Test + fun `Kan sette en behandling på vent hvis statusen er utredes`() { + val behandling = opprettBehandling() + val frist = LocalDate.now().plusDays(3) + + val settBehandlingPåVent = settPåVentService.settBehandlingPåVent( + behandling.id, + frist, + SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, + ) + + assertThat(settBehandlingPåVent.behandling.id).isEqualTo(behandling.id) + assertThat(settBehandlingPåVent.frist).isEqualTo(frist) + assertThat(settBehandlingPåVent.årsak).isEqualTo(SettPåVentÅrsak.AVVENTER_DOKUMENTASJON) + assertThat(settBehandlingPåVent.aktiv).isTrue() + + assertThat(behandlingRepository.finnBehandling(behandling.id).status).isEqualTo(BehandlingStatus.SATT_PÅ_VENT) + } + + @Test + fun `gjenopprett behandling skal sette status til utredes på nytt`() { + val behandling = opprettBehandling() + val frist = LocalDate.now().plusDays(3) + + settPåVentService.settBehandlingPåVent( + behandling.id, + frist, + SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, + ) + val behandlingEtterSattPåVent = behandlingRepository.finnBehandling(behandling.id).status + val gjenopptattSettPåVent = settPåVentService.gjenopptaBehandling(behandling.id) + + assertThat(gjenopptattSettPåVent.aktiv).isFalse() + assertThat(behandling.status).isEqualTo(BehandlingStatus.UTREDES) + assertThat(behandlingEtterSattPåVent).isEqualTo(BehandlingStatus.SATT_PÅ_VENT) + assertThat(behandlingRepository.finnBehandling(behandling.id).status).isEqualTo(BehandlingStatus.UTREDES) + } + + @ParameterizedTest + @EnumSource( + value = BehandlingStatus::class, + names = ["UTREDES"], + mode = EnumSource.Mode.EXCLUDE, + ) + fun `Skal ikke kunne sette en behandling på vent hvis den ikke har status utredes`(status: BehandlingStatus) { + assertThatThrownBy { + settPåVentService.settBehandlingPåVent( + opprettBehandling(status).id, + LocalDate.now().plusDays(3), + SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, + ) + }.hasMessageContaining("har status=$status og kan ikke settes på vent") + } + + @Test + fun `Kan ikke endre på behandling etter at den er satt på vent`() { + val behandlingEtterVilkårsvurderingSteg = kjørStegprosessForFGB( + tilSteg = StegType.VILKÅRSVURDERING, + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val behandlingId = behandlingEtterVilkårsvurderingSteg.id + settPåVentService.settBehandlingPåVent( + behandlingId = behandlingId, + frist = LocalDate.now().plusDays(21), + årsak = SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, + ) + + assertThrows { + stegService.håndterBehandlingsresultat(behandlingRepository.finnBehandling(behandlingId)) + } + } + + @Test + fun `Kan endre på behandling etter venting er deaktivert`() { + val behandlingEtterVilkårsvurderingSteg = kjørStegprosessForFGB( + tilSteg = StegType.VILKÅRSVURDERING, + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + settPåVentService.settBehandlingPåVent( + behandlingId = behandlingEtterVilkårsvurderingSteg.id, + frist = LocalDate.now().plusDays(21), + årsak = SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, + ) + + val nå = LocalDate.now() + + val settPåVent = settPåVentService.gjenopptaBehandling( + behandlingId = behandlingEtterVilkårsvurderingSteg.id, + nå = nå, + ) + + Assertions.assertEquals( + nå, + settPåVentRepository.findByIdOrNull(settPåVent.id)!!.tidTattAvVent, + ) + + assertDoesNotThrow { + stegService.håndterBehandlingsresultat(behandlingEtterVilkårsvurderingSteg) + } + } + + @Test + fun `Kan ikke sette ventefrist til før dagens dato`() { + val behandlingEtterVilkårsvurderingSteg = kjørStegprosessForFGB( + tilSteg = StegType.VILKÅRSVURDERING, + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + assertThrows { + settPåVentService.settBehandlingPåVent( + behandlingId = behandlingEtterVilkårsvurderingSteg.id, + frist = LocalDate.now().minusDays(1), + årsak = SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, + ) + } + } + + @Test + fun `Kan oppdatare set på vent på behandling`() { + val behandlingEtterVilkårsvurderingSteg = kjørStegprosessForFGB( + tilSteg = StegType.VILKÅRSVURDERING, + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val behandlingId = behandlingEtterVilkårsvurderingSteg.id + + val frist1 = LocalDate.now().plusDays(21) + + val settPåVent = settPåVentRepository.save( + SettPåVent( + behandling = behandlingEtterVilkårsvurderingSteg, + frist = frist1, + årsak = SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, + ), + ) + + Assertions.assertEquals(frist1, settPåVentService.finnAktivSettPåVentPåBehandling(behandlingId)!!.frist) + + val frist2 = LocalDate.now().plusDays(9) + + settPåVent.frist = frist2 + settPåVentRepository.save(settPåVent) + + Assertions.assertEquals(frist2, settPåVentService.finnAktivSettPåVentPåBehandling(behandlingId)!!.frist) + } + + @Test + fun `Skal gjennopta behandlinger etter ventefristen`() { + val behandling1 = kjørStegprosessForFGB( + tilSteg = StegType.VILKÅRSVURDERING, + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val behandling2 = kjørStegprosessForFGB( + tilSteg = StegType.VILKÅRSVURDERING, + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + settPåVentService.settBehandlingPåVent( + behandlingId = behandling1.id, + frist = LocalDate.now(), + årsak = SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, + ).let { + settPåVentRepository.save(it.copy(frist = LocalDate.now().minusDays(1))) + } + + settPåVentService.settBehandlingPåVent( + behandlingId = behandling2.id, + frist = LocalDate.now().plusDays(21), + årsak = SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, + ) + + taBehandlingerEtterVentefristAvVentTask.doTask( + Task( + type = TaBehandlingerEtterVentefristAvVentTask.TASK_STEP_TYPE, + payload = "", + ), + ) + + Assertions.assertNull(settPåVentRepository.findByBehandlingIdAndAktiv(behandling1.id, true)) + Assertions.assertNotNull(settPåVentRepository.findByBehandlingIdAndAktiv(behandling2.id, true)) + } + + @Test + fun `Skal ikke kunne gjenoppta behandlingen hvis den er satt på maskinell vent`() { + val behandling = opprettBehandling() + val frist = LocalDate.now().plusDays(3) + + settPåVentService.settBehandlingPåVent( + behandling.id, + frist, + SettPåVentÅrsak.AVVENTER_DOKUMENTASJON, + ) + snikeIKøenService.settAktivBehandlingTilPåMaskinellVent(behandling.id, SettPåMaskinellVentÅrsak.SATSENDRING) + + val throwable = catchThrowable { + settPåVentService.gjenopptaBehandling(behandling.id) + } + assertThat(throwable).isInstanceOf(FunksjonellFeil::class.java) + assertThat((throwable as FunksjonellFeil).frontendFeilmelding) + .isEqualTo("Behandlingen er under maskinell vent, og kan gjenopptas senere.") + } + + private fun opprettBehandling(status: BehandlingStatus = BehandlingStatus.UTREDES): Behandling { + val fagsak = fagsakService.hentEllerOpprettFagsak(randomFnr()) + val behandling = Behandling( + fagsak = fagsak, + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + type = BehandlingType.REVURDERING, + opprettetÅrsak = BehandlingÅrsak.NYE_OPPLYSNINGER, + status = status, + ).initBehandlingStegTilstand() + return behandlingService.lagreNyOgDeaktiverGammelBehandling(behandling) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningServiceIntegrationTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningServiceIntegrationTest.kt new file mode 100644 index 000000000..418a4800d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningServiceIntegrationTest.kt @@ -0,0 +1,613 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPersonResultaterForSøkerOgToBarn +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.sisteDagIForrigeMåned +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.integrasjoner.økonomi.AndelTilkjentYtelseForUtbetalingsoppdrag +import no.nav.familie.ba.sak.integrasjoner.økonomi.IdentOgYtelse +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.YearMonth + +class BeregningServiceIntegrationTest : AbstractSpringIntegrationTest() { + + @Autowired + private lateinit var beregningService: BeregningService + + @Autowired + private lateinit var fagsakService: FagsakService + + @Autowired + private lateinit var tilkjentYtelseRepository: TilkjentYtelseRepository + + @Autowired + private lateinit var personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository + + @Autowired + private lateinit var behandlingService: BehandlingService + + @Autowired + private lateinit var vilkårsvurderingService: VilkårsvurderingService + + @Autowired + private lateinit var personidentService: PersonidentService + + @Autowired + private lateinit var aktørIdRepository: AktørIdRepository + + @BeforeEach + fun førHverTest() { + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2022, 12, 31) + } + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + @Test + fun skalLagreRiktigTilkjentYtelseForFGBMedToBarn() { + val fnr = randomFnr() + val dagensDato = LocalDate.now() + val fomBarn1 = dagensDato.withDayOfMonth(1) + val fomBarn2 = fomBarn1.plusYears(2) + val tomBarn1 = fomBarn1.plusYears(18).sisteDagIMåned() + val tomBarn2 = fomBarn2.plusYears(18).sisteDagIMåned() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + opprettTilkjentYtelse(behandling) + val utbetalingsoppdrag = lagTestUtbetalingsoppdragForFGBMedToBarn( + fnr, + fagsak.id.toString(), + behandling.id, + dagensDato, + fomBarn1, + tomBarn1, + fomBarn2, + tomBarn2, + ) + + leggTilAndelTilkjentYtelsePåTilkjentYtelse( + behandling, + fomBarn1.toYearMonth(), + tomBarn1.toYearMonth(), + ) + + leggTilAndelTilkjentYtelsePåTilkjentYtelse( + behandling, + fomBarn2.toYearMonth(), + tomBarn2.toYearMonth(), + ) + + val tilkjentYtelse = + beregningService.oppdaterTilkjentYtelseMedUtbetalingsoppdrag(behandling, utbetalingsoppdrag) + + Assertions.assertNotNull(tilkjentYtelse) + Assertions.assertEquals(fomBarn1.toYearMonth(), tilkjentYtelse.stønadFom) + Assertions.assertEquals(tomBarn2.toYearMonth(), tilkjentYtelse.stønadTom) + Assertions.assertNull(tilkjentYtelse.opphørFom) + } + + @Test + fun skalLagreRiktigTilkjentYtelseForOpphørMedToBarn() { + val fnr = randomFnr() + val dagensDato = LocalDate.now() + val fomBarn1 = dagensDato.withDayOfMonth(1) + val fomBarn2 = fomBarn1.plusYears(2) + val tomBarn1 = fomBarn1.plusYears(18).sisteDagIMåned() + val tomBarn2 = fomBarn2.plusYears(18).sisteDagIMåned() + val opphørsDato = fomBarn1.plusYears(5).withDayOfMonth(1) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + opprettTilkjentYtelse(behandling) + val utbetalingsoppdrag = lagTestUtbetalingsoppdragForOpphørMedToBarn( + fnr, + fagsak.id.toString(), + behandling.id, + dagensDato, + fomBarn1, + tomBarn1, + fomBarn2, + tomBarn2, + opphørsDato, + ) + + leggTilAndelTilkjentYtelsePåTilkjentYtelse( + behandling, + fomBarn1.toYearMonth(), + tomBarn1.toYearMonth(), + ) + + leggTilAndelTilkjentYtelsePåTilkjentYtelse( + behandling, + fomBarn2.toYearMonth(), + tomBarn2.toYearMonth(), + ) + + val tilkjentYtelse = + beregningService.oppdaterTilkjentYtelseMedUtbetalingsoppdrag(behandling, utbetalingsoppdrag) + + Assertions.assertNotNull(tilkjentYtelse) + Assertions.assertNull(tilkjentYtelse.stønadFom) + Assertions.assertEquals(tomBarn2.toYearMonth(), tilkjentYtelse.stønadTom) + Assertions.assertNotNull(tilkjentYtelse.opphørFom) + Assertions.assertEquals(opphørsDato.toYearMonth(), tilkjentYtelse.opphørFom) + } + + @Test + fun skalLagreRiktigTilkjentYtelseForRevurderingMedToBarn() { + val fnr = randomFnr() + val dagensDato = LocalDate.now() + val opphørFomBarn1 = LocalDate.of(2020, 5, 1) + val revurderingFomBarn1 = LocalDate.of(2020, 7, 1) + val fomDatoBarn1 = LocalDate.of(2020, 1, 1) + val tomDatoBarn1 = fomDatoBarn1.plusYears(18).sisteDagIForrigeMåned() + + val opphørFomBarn2 = LocalDate.of(2020, 8, 1) + val revurderingFomBarn2 = LocalDate.of(2020, 10, 1) + val fomDatoBarn2 = LocalDate.of(2019, 10, 1) + val tomDatoBarn2 = fomDatoBarn2.plusYears(18).sisteDagIForrigeMåned() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling(fagsak = fagsak, behandlingType = BehandlingType.REVURDERING), + ) + opprettTilkjentYtelse(behandling) + val utbetalingsoppdrag = lagTestUtbetalingsoppdragForRevurderingMedToBarn( + fnr, + fagsak.id.toString(), + behandling.id, + behandling.id - 1, + dagensDato, + opphørFomBarn1, + revurderingFomBarn1, + fomDatoBarn1, + tomDatoBarn1, + opphørFomBarn2, + revurderingFomBarn2, + fomDatoBarn2, + tomDatoBarn2, + ) + + leggTilAndelTilkjentYtelsePåTilkjentYtelse( + behandling, + revurderingFomBarn1.toYearMonth(), + tomDatoBarn1.toYearMonth(), + ) + + leggTilAndelTilkjentYtelsePåTilkjentYtelse( + behandling, + revurderingFomBarn2.toYearMonth(), + tomDatoBarn2.toYearMonth(), + ) + + val tilkjentYtelse = + beregningService.oppdaterTilkjentYtelseMedUtbetalingsoppdrag(behandling, utbetalingsoppdrag) + + Assertions.assertNotNull(tilkjentYtelse) + Assertions.assertEquals(revurderingFomBarn1.toYearMonth(), tilkjentYtelse.stønadFom) + Assertions.assertEquals(tomDatoBarn1.toYearMonth(), tilkjentYtelse.stønadTom) + Assertions.assertEquals(opphørFomBarn2.toYearMonth(), tilkjentYtelse.opphørFom) + } + + @Test + fun `Skal lagre andelerTilkjentYtelse med kobling til TilkjentYtelse`() { + val søkerFnr = randomFnr() + val barn1Fnr = randomFnr() + val barn2Fnr = randomFnr() + val søkerAktørId = personidentService.hentOgLagreAktør(søkerFnr, true) + val barn1AktørId = personidentService.hentOgLagreAktør(barn1Fnr, true) + val barn2AktørId = personidentService.hentOgLagreAktør(barn2Fnr, true) + val dato_2021_11_01 = LocalDate.of(2021, 11, 1) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + val barnAktør = personidentService.hentOgLagreAktørIder(listOf(barn1Fnr, barn2Fnr), true) + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + søkerFnr, + listOf(barn1Fnr, barn2Fnr), + søkerAktør = fagsak.aktør, + barnAktør = barnAktør, + ) + personopplysningGrunnlagRepository.save(personopplysningGrunnlag) + + val barn1Id = + personopplysningGrunnlag.barna.find { it.aktør.aktivFødselsnummer() == barn1Fnr }!!.aktør.aktivFødselsnummer() + val barn2Id = + personopplysningGrunnlag.barna.find { it.aktør.aktivFødselsnummer() == barn2Fnr }!!.aktør.aktivFødselsnummer() + + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling) + vilkårsvurdering.personResultater = lagPersonResultaterForSøkerOgToBarn( + vilkårsvurdering, + søkerAktørId, + barn1AktørId, + barn2AktørId, + dato_2021_11_01, + dato_2021_11_01.plusYears(17), + ) + vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = vilkårsvurdering) + + beregningService.oppdaterBehandlingMedBeregning(behandling, personopplysningGrunnlag) + + val tilkjentYtelse = tilkjentYtelseRepository.findByBehandling(behandling.id) + val andelBarn1 = tilkjentYtelse.andelerTilkjentYtelse.filter { it.aktør.aktivFødselsnummer() == barn1Id } + val andelBarn2 = tilkjentYtelse.andelerTilkjentYtelse.filter { it.aktør.aktivFødselsnummer() == barn2Id } + + Assertions.assertNotNull(tilkjentYtelse) + Assertions.assertTrue(tilkjentYtelse.andelerTilkjentYtelse.isNotEmpty()) + Assertions.assertEquals(3, andelBarn1.size) + Assertions.assertEquals(3, andelBarn2.size) + tilkjentYtelse.andelerTilkjentYtelse.forEach { + Assertions.assertEquals(tilkjentYtelse, it.tilkjentYtelse) + } + Assertions.assertEquals(1, andelBarn1.filter { it.kalkulertUtbetalingsbeløp == 1054 }.size) + Assertions.assertEquals(1, andelBarn1.filter { it.kalkulertUtbetalingsbeløp == 1654 }.size) + Assertions.assertEquals(1, andelBarn1.filter { it.kalkulertUtbetalingsbeløp == 1676 }.size) + Assertions.assertEquals(1, andelBarn2.filter { it.kalkulertUtbetalingsbeløp == 1054 }.size) + Assertions.assertEquals(1, andelBarn2.filter { it.kalkulertUtbetalingsbeløp == 1654 }.size) + Assertions.assertEquals(1, andelBarn2.filter { it.kalkulertUtbetalingsbeløp == 1676 }.size) + } + + @Nested + inner class HentSisteAndelPerIdent { + + val søker = tilfeldigPerson() + val barn1 = tilfeldigPerson() + val barn2 = tilfeldigPerson() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søker.aktør.aktivFødselsnummer()) + val aktørSøker = personidentService.hentOgLagreAktør(søker.aktør.aktivFødselsnummer(), true) + val aktørBarn1 = personidentService.hentOgLagreAktør(barn1.aktør.aktivFødselsnummer(), true) + val aktørBarn2 = personidentService.hentOgLagreAktør(barn2.aktør.aktivFødselsnummer(), true) + + lateinit var førsteBehandling: Behandling + + @BeforeEach + fun setUp() { + førsteBehandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + } + + @Test + fun `ingen andeler`() { + assertThat(hentSisteAndelPerIdent()).isEmpty() + } + + @Test + fun `uten utbetalingsoppdrag`() { + with(lagInitiellTilkjentYtelse(førsteBehandling, utbetalingsoppdrag = null)) { + val andeler = listOf( + lagAndel( + tilkjentYtelse = this, + aktør = aktørBarn1, + person = barn1, + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2020, 2), + offset = 0, + ), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + avsluttOgLagreBehandling(førsteBehandling) + val sisteAndelPerIdent = hentSisteAndelPerIdent() + assertThat(sisteAndelPerIdent).isEmpty() + } + + @Test + fun `behandling er ikke avsluttet`() { + with(lagInitiellTilkjentYtelse(førsteBehandling, utbetalingsoppdrag = "")) { + val andeler = listOf( + lagAndel( + tilkjentYtelse = this, + aktør = aktørBarn1, + person = barn1, + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2020, 2), + offset = 0, + ), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + val sisteAndelPerIdent = hentSisteAndelPerIdent() + assertThat(sisteAndelPerIdent).isEmpty() + } + + @Test + fun `gitt fagsak har ikke noen andeler`() { + with(lagInitiellTilkjentYtelse(førsteBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndel( + tilkjentYtelse = this, + aktør = aktørBarn1, + person = barn1, + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2020, 2), + offset = 0, + ), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + avsluttOgLagreBehandling(førsteBehandling) + assertThat(beregningService.hentSisteAndelPerIdent(fagsak.id + 1)).isEmpty() + } + + @Test + fun `2 ulike personer med samme type`() { + with(lagInitiellTilkjentYtelse(førsteBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndel( + tilkjentYtelse = this, + aktør = aktørBarn1, + person = barn1, + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2020, 2), + offset = 0, + ), + lagAndel( + tilkjentYtelse = this, + aktør = aktørBarn2, + person = barn2, + fom = YearMonth.of(2020, 3), + tom = YearMonth.of(2020, 5), + offset = 1, + ), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + avsluttOgLagreBehandling(førsteBehandling) + val sisteAndelPerIdent = hentSisteAndelPerIdent() + assertThat(sisteAndelPerIdent).hasSize(2) + with(sisteAndelPerIdent[IdentOgYtelse(barn1.aktør.aktivFødselsnummer(), YtelseType.SMÅBARNSTILLEGG)]!!) { + assertThat(periodeOffset).isEqualTo(0L) + assertThat(forrigePeriodeOffset).isNull() + assertThat(stønadFom).isEqualTo(YearMonth.of(2020, 1)) + assertThat(stønadTom).isEqualTo(YearMonth.of(2020, 2)) + } + with(sisteAndelPerIdent[IdentOgYtelse(barn2.aktør.aktivFødselsnummer(), YtelseType.SMÅBARNSTILLEGG)]!!) { + assertThat(periodeOffset).isEqualTo(1L) + assertThat(forrigePeriodeOffset).isNull() + assertThat(stønadFom).isEqualTo(YearMonth.of(2020, 3)) + assertThat(stønadTom).isEqualTo(YearMonth.of(2020, 5)) + } + } + + @Test + fun `førstegångsbehandling med flere andeler per person`() { + with(lagInitiellTilkjentYtelse(førsteBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndel( + tilkjentYtelse = this, + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2020, 2), + offset = 0, + ), + lagAndel( + tilkjentYtelse = this, + fom = YearMonth.of(2020, 3), + tom = YearMonth.of(2020, 5), + offset = 1, + forrigeOffset = 0, + ), + lagAndel( + tilkjentYtelse = this, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + fom = YearMonth.of(2020, 3), + tom = YearMonth.of(2020, 3), + offset = 2, + forrigeOffset = null, + ), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + avsluttOgLagreBehandling(førsteBehandling) + val sisteAndelPerIdent = hentSisteAndelPerIdent() + assertThat(sisteAndelPerIdent).hasSize(2) + val fødselsnummer = aktørSøker.aktivFødselsnummer() + with(sisteAndelPerIdent[IdentOgYtelse(fødselsnummer, YtelseType.SMÅBARNSTILLEGG)]!!) { + assertThat(periodeOffset).isEqualTo(1L) + assertThat(forrigePeriodeOffset).isEqualTo(0L) + assertThat(stønadFom).isEqualTo(YearMonth.of(2020, 3)) + assertThat(stønadTom).isEqualTo(YearMonth.of(2020, 5)) + } + with(sisteAndelPerIdent[IdentOgYtelse(fødselsnummer, YtelseType.UTVIDET_BARNETRYGD)]!!) { + assertThat(periodeOffset).isEqualTo(2L) + assertThat(forrigePeriodeOffset).isNull() + assertThat(stønadFom).isEqualTo(YearMonth.of(2020, 3)) + assertThat(stønadTom).isEqualTo(YearMonth.of(2020, 3)) + } + } + + @Test + fun `siste andelen kommer fra revurderingen`() { + with(lagInitiellTilkjentYtelse(førsteBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndel( + tilkjentYtelse = this, + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2020, 2), + offset = 0, + ), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + avsluttOgLagreBehandling(førsteBehandling) + val revurdering = lagRevurdering() + with(lagInitiellTilkjentYtelse(revurdering, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndel( + tilkjentYtelse = this, + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2020, 3), + offset = 1, + forrigeOffset = 0, + ), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + avsluttOgLagreBehandling(revurdering) + val sisteAndelPerIdent = hentSisteAndelPerIdent() + assertThat(sisteAndelPerIdent).hasSize(1) + val fødselsnummer = aktørSøker.aktivFødselsnummer() + with(sisteAndelPerIdent[IdentOgYtelse(fødselsnummer, YtelseType.SMÅBARNSTILLEGG)]!!) { + assertThat(periodeOffset).isEqualTo(1L) + assertThat(forrigePeriodeOffset).isEqualTo(0L) + assertThat(stønadFom).isEqualTo(YearMonth.of(2020, 1)) + assertThat(stønadTom).isEqualTo(YearMonth.of(2020, 3)) + assertThat(kildeBehandlingId).isEqualTo(revurdering.id) + } + } + + @Test + fun `en revurdering opphører en andel, sånn at siste andelen finnes i en tidligere behandling`() { + with(lagInitiellTilkjentYtelse(førsteBehandling, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndel( + tilkjentYtelse = this, + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2020, 3), + offset = 0, + ), + lagAndel( + tilkjentYtelse = this, + fom = YearMonth.of(2020, 4), + tom = YearMonth.of(2020, 5), + offset = 1, + forrigeOffset = 0, + ), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + avsluttOgLagreBehandling(førsteBehandling) + val revurdering = lagRevurdering() + with(lagInitiellTilkjentYtelse(revurdering, utbetalingsoppdrag = "utbetalingsoppdrag")) { + val andeler = listOf( + lagAndel( + tilkjentYtelse = this, + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2020, 3), + offset = 0, + ), + ) + andelerTilkjentYtelse.addAll(andeler) + tilkjentYtelseRepository.saveAndFlush(this) + } + avsluttOgLagreBehandling(revurdering) + val sisteAndelPerIdent = hentSisteAndelPerIdent() + assertThat(sisteAndelPerIdent).hasSize(1) + val fødselsnummer = aktørSøker.aktivFødselsnummer() + with(sisteAndelPerIdent[IdentOgYtelse(fødselsnummer, YtelseType.SMÅBARNSTILLEGG)]!!) { + assertThat(periodeOffset).isEqualTo(1L) + assertThat(forrigePeriodeOffset).isEqualTo(0L) + assertThat(stønadFom).isEqualTo(YearMonth.of(2020, 4)) + assertThat(stønadTom).isEqualTo(YearMonth.of(2020, 5)) + assertThat(kildeBehandlingId).isEqualTo(førsteBehandling.id) + } + } + + fun hentSisteAndelPerIdent(): Map { + return beregningService.hentSisteAndelPerIdent(fagsak.id) + } + + fun lagAndel( + tilkjentYtelse: TilkjentYtelse, + ytelseType: YtelseType = YtelseType.SMÅBARNSTILLEGG, + person: Person? = null, + aktør: Aktør? = null, + fom: YearMonth, + tom: YearMonth, + offset: Long, + forrigeOffset: Long? = null, + ): AndelTilkjentYtelse = + lagAndelTilkjentYtelse( + fom, + tom, + ytelseType, + 1345, + tilkjentYtelse.behandling, + person = person ?: søker, + aktør = aktør ?: aktørSøker, + tilkjentYtelse = tilkjentYtelse, + periodeIdOffset = offset, + forrigeperiodeIdOffset = forrigeOffset, + ) + + private fun lagRevurdering() = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling(fagsak, behandlingType = BehandlingType.REVURDERING), + ) + } + + private fun avsluttOgLagreBehandling(behandling: Behandling) { + behandling.status = BehandlingStatus.AVSLUTTET + behandlingService.oppdaterStatusPåBehandling(behandlingId = behandling.id, BehandlingStatus.AVSLUTTET) + } + + private fun opprettTilkjentYtelse(behandling: Behandling) { + tilkjentYtelseRepository.saveAndFlush(lagInitiellTilkjentYtelse(behandling)) + } + + private fun leggTilAndelTilkjentYtelsePåTilkjentYtelse(behandling: Behandling, fom: YearMonth, tom: YearMonth) { + val tilkjentYtelse = tilkjentYtelseRepository.findByBehandling(behandling.id) + val tilfeldigperson = tilfeldigPerson(aktør = tilAktør(randomFnr())) + aktørIdRepository.saveAndFlush(tilfeldigperson.aktør) + + val andelTilkjentYtelse = lagAndelTilkjentYtelse( + fom, + tom, + YtelseType.ORDINÆR_BARNETRYGD, + 1054, + behandling, + tilkjentYtelse = tilkjentYtelse, + aktør = tilfeldigperson.aktør, + ) + + tilkjentYtelse.andelerTilkjentYtelse.add(andelTilkjentYtelse) + tilkjentYtelseRepository.saveAndFlush(tilkjentYtelse) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningTestUtil.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningTestUtil.kt new file mode 100644 index 000000000..aba76cad2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/BeregningTestUtil.kt @@ -0,0 +1,39 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.integrasjoner.økonomi.AndelTilkjentYtelseForSimuleringFactory +import no.nav.familie.ba.sak.integrasjoner.økonomi.AndelTilkjentYtelseForUtbetalingsoppdrag +import no.nav.familie.ba.sak.integrasjoner.økonomi.IdentOgYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.felles.utbetalingsgenerator.domain.IdentOgType + +object BeregningTestUtil { + + /** + * Denne erstatter det som [no.nav.familie.ba.sak.kjerne.beregning.BeregningService.hentSisteAndelPerIdent] gjør + * Pga at det ikke er den samme implementasjonen burde bruket av denne minimeres + */ + fun sisteAndelPerIdent(tilkjenteYtelser: List): Map { + val andeler = tilkjenteYtelser.flatMap { it.andelerTilkjentYtelse } + return sisteAndelPerIdent(andeler) + } + + fun sisteAndelPerIdentNy(tilkjenteYtelser: List): Map { + return tilkjenteYtelser.flatMap { it.andelerTilkjentYtelse } + .groupBy { IdentOgType(it.aktør.aktivFødselsnummer(), it.type.tilYtelseType()) } + .mapValues { it.value.maxBy { it.periodeOffset ?: 0 } } + } + + @JvmName("sisteAndelTilkjentYtelsePerIdent") + fun sisteAndelPerIdent(andeler: List): Map { + val factory = AndelTilkjentYtelseForSimuleringFactory() + return sisteAndelPerIdent(factory.pakkInnForUtbetaling(andeler)) + } + + @JvmName("sisteAndelPerIdentAndelUtbetalingsoppdrag") + fun sisteAndelPerIdent(andeler: List): Map { + return andeler + .groupBy { IdentOgYtelse(it.aktør.aktivFødselsnummer(), it.type) } + .mapValues { it.value.maxBy { it.periodeOffset ?: 0 } } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/SatsServiceIntegrationTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/SatsServiceIntegrationTest.kt new file mode 100644 index 000000000..31c3ecd37 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/beregning/SatsServiceIntegrationTest.kt @@ -0,0 +1,79 @@ +package no.nav.familie.ba.sak.kjerne.beregning + +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType.ORBA +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType.SMA +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType.TILLEGG_ORBA +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType.UTVIDET_BARNETRYGD +import no.nav.familie.ba.sak.kjerne.eøs.util.tilTidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.ogSenere +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.ogTidligere +import no.nav.familie.ba.sak.kjerne.tidslinje.eksperimentelt.plus +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom.rangeTo +import no.nav.familie.ba.sak.kjerne.tidslinje.util.aug +import no.nav.familie.ba.sak.kjerne.tidslinje.util.des +import no.nav.familie.ba.sak.kjerne.tidslinje.util.feb +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jul +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jun +import no.nav.familie.ba.sak.kjerne.tidslinje.util.mar +import no.nav.familie.ba.sak.kjerne.tidslinje.util.sep +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class SatsServiceIntegrationTest : AbstractSpringIntegrationTest() { + + @Test + fun `Skal gi riktig sats for ordinær barnetrygd, 6 til 18 år`() { + val forventet = + feb(2019).ogTidligere().tilTidslinje { 970 } + + (mar(2019)..feb(2023)).tilTidslinje { 1054 } + + (mar(2023)..jun(2023)).tilTidslinje { 1083 } + + jul(2023).ogSenere().tilTidslinje { 1310 } + + val faktisk = satstypeTidslinje(ORBA) + + assertEquals(forventet, faktisk) + } + + @Test + fun `Skal gi riktig sats for tillegg ordinær barnetrygd, 0 til 6 år`() { + val forventet = + feb(2019).ogTidligere().tilTidslinje { 970 } + + (mar(2019)..aug(2020)).tilTidslinje { 1054 } + + (sep(2020)..aug(2021)).tilTidslinje { 1354 } + + (sep(2021)..des(2021)).tilTidslinje { 1654 } + + (jan(2022)..feb(2023)).tilTidslinje { 1676 } + + (mar(2023)..jun(2023)).tilTidslinje { 1723 } + + jul(2023).ogSenere().tilTidslinje { 1766 } + + val faktisk = satstypeTidslinje(TILLEGG_ORBA) + + assertEquals(forventet, faktisk) + } + + @Test + fun `Skal gi riktig sats for småbarnstillegg`() { + val forventet = + feb(2023).ogTidligere().tilTidslinje { 660 } + + (mar(2023)..jun(2023)).tilTidslinje { 678 } + + jul(2023).ogSenere().tilTidslinje { 696 } + + val faktisk = satstypeTidslinje(SMA) + + assertEquals(forventet, faktisk) + } + + @Test + fun `Skal gi riktig sats for utvidet barnetrygd`() { + val forventet = + feb(2019).ogTidligere().tilTidslinje { 970 } + + (mar(2019)..feb(2023)).tilTidslinje { 1054 } + + (mar(2023)..jun(2023)).tilTidslinje { 2489 } + + jul(2023).ogSenere().tilTidslinje { 2516 } + + val faktisk = satstypeTidslinje(UTVIDET_BARNETRYGD) + + assertEquals(forventet, faktisk) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentControllerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentControllerTest.kt new file mode 100644 index 000000000..a9bab4bd6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentControllerTest.kt @@ -0,0 +1,70 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import no.nav.familie.kontrakter.felles.Ressurs +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired + +class DokumentControllerTest( + @Autowired + private val dokumentService: DokumentService, + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, +) : AbstractSpringIntegrationTest() { + + private val mockDokumentGenereringService: DokumentGenereringService = mockk() + private val mockDokumentService: DokumentService = mockk() + private val vedtakService: VedtakService = mockk(relaxed = true) + private val fagsakService: FagsakService = mockk() + private val tilgangService: TilgangService = mockk(relaxed = true) + val mockDokumentController = + DokumentController( + dokumentGenereringService = mockDokumentGenereringService, + dokumentService = mockDokumentService, + vedtakService = vedtakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + fagsakService = fagsakService, + tilgangService = tilgangService, + persongrunnlagService = mockk(relaxed = true), + arbeidsfordelingService = mockk(relaxed = true), + utvidetBehandlingService = mockk(relaxed = true), + ) + + @Test + @Tag("integration") + fun `Test generer vedtaksbrev`() { + every { vedtakService.hent(any()) } returns lagVedtak() + every { mockDokumentGenereringService.genererBrevForVedtak(any()) } returns "pdf".toByteArray() + + val response = mockDokumentController.genererVedtaksbrev(1) + assert(response.status == Ressurs.Status.SUKSESS) + } + + @Test + @Tag("integration") + fun `Test hent pdf vedtak`() { + every { vedtakService.hent(any()) } returns lagVedtak(stønadBrevPdF = "pdf".toByteArray()) + every { mockDokumentService.hentBrevForVedtak(any()) } returns Ressurs.success("pdf".toByteArray()) + + val response = mockDokumentController.hentVedtaksbrev(1) + assert(response.status == Ressurs.Status.SUKSESS) + } + + @Test + @Tag("integration") + fun `Kast feil ved hent av vedtaksbrev når det ikke er generert brev`() { + assertThrows { + dokumentService.hentBrevForVedtak(lagVedtak()) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentServiceIntegrationTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentServiceIntegrationTest.kt new file mode 100644 index 000000000..5f2f4aae6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/DokumentServiceIntegrationTest.kt @@ -0,0 +1,544 @@ +package no.nav.familie.ba.sak.kjerne.brev + +import io.mockk.verify +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.kjørStegprosessForFGB +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.TEST_PDF +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.ekstern.restDomene.InstitusjonInfo +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.brev.domene.ManueltBrevRequest +import no.nav.familie.ba.sak.kjerne.brev.domene.byggMottakerdata +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Medlemskap +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.bostedsadresse.GrMatrikkeladresse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.sivilstand.GrSivilstand +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.statsborgerskap.GrStatsborgerskap +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.Personident +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class DokumentServiceIntegrationTest( + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val vilkårsvurderingService: VilkårsvurderingService, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val vedtakService: VedtakService, + + @Autowired + private val dokumentService: DokumentService, + + @Autowired + private val totrinnskontrollService: TotrinnskontrollService, + + @Autowired + private val stegService: StegService, + + @Autowired + private val brevService: BrevService, + + @Autowired + private val integrasjonClient: IntegrasjonClient, + + @Autowired + private val arbeidsfordelingService: ArbeidsfordelingService, + + @Autowired + private val vedtaksperiodeService: VedtaksperiodeService, + + @Autowired + private val brevKlient: BrevKlient, + + @Autowired + private val dokumentGenereringService: DokumentGenereringService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val brevmalService: BrevmalService, +) : AbstractSpringIntegrationTest() { + + @BeforeEach + fun setup() { + databaseCleanupService.truncate() + } + + @Test + fun `Hent vedtaksbrev`() { + val behandlingEtterVilkårsvurderingSteg = kjørStegprosessForFGB( + tilSteg = StegType.VURDER_TILBAKEKREVING, + søkerFnr = randomFnr(), + barnasIdenter = listOf(ClientMocks.barnFnr[0]), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + totrinnskontrollService.opprettTotrinnskontrollMedSaksbehandler( + behandlingEtterVilkårsvurderingSteg, + "ansvarligSaksbehandler", + "saksbehandlerId", + ) + totrinnskontrollService.besluttTotrinnskontroll( + behandlingEtterVilkårsvurderingSteg, + "ansvarligBeslutter", + "beslutterId", + Beslutning.GODKJENT, + ) + val vedtak = vedtakService.hentAktivForBehandling(behandlingId = behandlingEtterVilkårsvurderingSteg.id) + + vedtakService.oppdaterVedtakMedStønadsbrev(vedtak!!) + + val pdfvedtaksbrevRess = dokumentService.hentBrevForVedtak(vedtak) + assertEquals(Ressurs.Status.SUKSESS, pdfvedtaksbrevRess.status) + assert(pdfvedtaksbrevRess.data!!.contentEquals(TEST_PDF)) + } + + @Test + fun `Skal generere vedtaksbrev`() { + val behandlingEtterVilkårsvurderingSteg = kjørStegprosessForFGB( + tilSteg = StegType.VURDER_TILBAKEKREVING, + søkerFnr = randomFnr(), + barnasIdenter = listOf(ClientMocks.barnFnr[0]), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + totrinnskontrollService.opprettTotrinnskontrollMedSaksbehandler( + behandlingEtterVilkårsvurderingSteg, + "ansvarligSaksbehandler", + "saksbehandlerId", + ) + totrinnskontrollService.besluttTotrinnskontroll( + behandlingEtterVilkårsvurderingSteg, + "ansvarligBeslutter", + "beslutterId", + Beslutning.GODKJENT, + ) + + val vedtak = vedtakService.hentAktivForBehandling(behandlingId = behandlingEtterVilkårsvurderingSteg.id) + vedtakService.oppdaterVedtakMedStønadsbrev(vedtak!!) + + val pdfvedtaksbrev = dokumentGenereringService.genererBrevForVedtak(vedtak) + assert(pdfvedtaksbrev.contentEquals(TEST_PDF)) + } + + @Test + fun `Skal verifisere at brev får riktig signatur ved alle steg i behandling`() { + val mockSaksbehandler = "Mock Saksbehandler" + val mockSaksbehandlerId = "mock.saksbehandler@nav.no" + val mockBeslutter = "Mock Beslutter" + val mockBeslutterId = "mock.beslutter@nav.no" + + val behandlingEtterVilkårsvurderingSteg = kjørStegprosessForFGB( + tilSteg = StegType.VURDER_TILBAKEKREVING, + søkerFnr = randomFnr(), + barnasIdenter = listOf(ClientMocks.barnFnr[0]), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + val vedtak = vedtakService.hentAktivForBehandling(behandlingId = behandlingEtterVilkårsvurderingSteg.id)!! + + val vedtaksbrevFellesFelter = brevService.lagVedtaksbrevFellesfelter(vedtak) + + assertEquals("NAV Familie- og pensjonsytelser Oslo 1", vedtaksbrevFellesFelter.enhet) + assertEquals("System", vedtaksbrevFellesFelter.saksbehandler) + assertEquals("Beslutter", vedtaksbrevFellesFelter.beslutter) + + totrinnskontrollService.opprettTotrinnskontrollMedSaksbehandler( + behandlingEtterVilkårsvurderingSteg, + mockSaksbehandler, + mockSaksbehandlerId, + ) + val behandlingEtterSendTilBeslutter = + behandlingEtterVilkårsvurderingSteg.leggTilBehandlingStegTilstand(StegType.BESLUTTE_VEDTAK) + behandlingHentOgPersisterService.lagreEllerOppdater(behandlingEtterSendTilBeslutter) + + val vedtakEtterSendTilBeslutter = + vedtakService.hentAktivForBehandling(behandlingId = behandlingEtterSendTilBeslutter.id)!! + + val vedtaksbrevFellesFelterEtterSendTilBeslutter = + brevService.lagVedtaksbrevFellesfelter(vedtakEtterSendTilBeslutter) + + assertEquals(mockSaksbehandler, vedtaksbrevFellesFelterEtterSendTilBeslutter.saksbehandler) + assertEquals("System", vedtaksbrevFellesFelterEtterSendTilBeslutter.beslutter) + + totrinnskontrollService.besluttTotrinnskontroll( + behandling = behandlingEtterSendTilBeslutter, + beslutter = mockBeslutter, + beslutterId = mockBeslutterId, + beslutning = Beslutning.GODKJENT, + ) + val behandlingEtterVedtakBesluttet = + behandlingEtterVilkårsvurderingSteg.leggTilBehandlingStegTilstand(StegType.IVERKSETT_MOT_OPPDRAG) + + val vedtakEtterVedtakBesluttet = + vedtakService.hentAktivForBehandling(behandlingId = behandlingEtterVedtakBesluttet.id)!! + + val vedtaksbrevFellesFelterEtterVedtakBesluttet = + brevService.lagVedtaksbrevFellesfelter(vedtakEtterVedtakBesluttet) + + assertEquals(mockSaksbehandler, vedtaksbrevFellesFelterEtterVedtakBesluttet.saksbehandler) + assertEquals(mockBeslutter, vedtaksbrevFellesFelterEtterVedtakBesluttet.beslutter) + } + + @Test + fun `Skal verifisere at man ikke får generert brev etter at behandlingen er sendt fra beslutter`() { + val behandlingEtterVedtakBesluttet = kjørStegprosessForFGB( + tilSteg = StegType.BESLUTTE_VEDTAK, + søkerFnr = randomFnr(), + barnasIdenter = listOf(ClientMocks.barnFnr[0]), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val vedtak = vedtakService.hentAktivForBehandling(behandlingId = behandlingEtterVedtakBesluttet.id)!! + val feil = assertThrows { + dokumentGenereringService.genererBrevForVedtak(vedtak) + } + + assert( + feil.message!!.contains("Ikke tillatt å generere brev etter at behandlingen er sendt fra beslutter"), + ) + } + + @Test + fun `Sjekk at send brev for trukket søknad ikke genererer forside`() { + val fnr = randomFnr() + val barn1Fnr = randomFnr() + val barn2Fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + val barnAktør = personidentService.hentOgLagreAktørIder(listOf(barn1Fnr, barn2Fnr), true) + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barn1Fnr, barn2Fnr), + søkerAktør = behandling.fagsak.aktør, + barnAktør = barnAktør, + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val manueltBrevRequest = ManueltBrevRequest( + brevmal = Brevmal.HENLEGGE_TRUKKET_SØKNAD, + mottakerIdent = fnr, + ).byggMottakerdata( + behandling, + persongrunnlagService, + arbeidsfordelingService, + ) + dokumentService.sendManueltBrev(manueltBrevRequest, behandling, behandling.fagsak.id) + + verify(exactly = 1) { + integrasjonClient.journalførDokument(match { it.fnr == fnr }) + } + } + + @Test + fun `Test sending varsel om revurdering til institusjon`() { + val fnr = "09121079074" + val orgNummer = "998765432" + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent( + fødselsnummer = fnr, + fagsakType = FagsakType.INSTITUSJON, + institusjon = InstitusjonInfo(orgNummer = orgNummer, tssEksternId = "8000000"), + ) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlagForInstitusjon( + behandlingId = behandling.id, + barnasIdenter = listOf(fnr), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel( + lagVilkårsvurdering( + behandling.fagsak.aktør, + behandling, + resultat = Resultat.IKKE_VURDERT, + ), + ) + + val manueltBrevRequest = ManueltBrevRequest( + brevmal = Brevmal.VARSEL_OM_REVURDERING_INSTITUSJON, + mottakerIdent = orgNummer, + ).byggMottakerdata( + behandling, + persongrunnlagService, + arbeidsfordelingService, + ) + dokumentService.sendManueltBrev(manueltBrevRequest, behandling, behandling.fagsak.id) + + verify(exactly = 1) { + integrasjonClient.journalførDokument(match { it.fnr == fnr && it.avsenderMottaker?.id == orgNummer && it.avsenderMottaker?.navn == "Testinstitusjon" }) + } + assertEquals(fnr, manueltBrevRequest.vedrørende?.fødselsnummer) + assertEquals("institusjonsbarnets navn", manueltBrevRequest.vedrørende?.navn) + verify { + brevKlient.genererBrev( + "bokmaal", + match { + it.mal == Brevmal.VARSEL_OM_REVURDERING_INSTITUSJON && it.data.flettefelter.gjelder!!.first() == "institusjonsbarnets navn" && + it.data.flettefelter.organisasjonsnummer!!.first() == orgNummer + }, + ) + } + } + + @Test + fun `Test sending innhent dokumentasjon til institusjon`() { + val fnr = randomFnr() + val orgNummer = "998765432" + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent( + fødselsnummer = fnr, + fagsakType = FagsakType.INSTITUSJON, + institusjon = InstitusjonInfo(orgNummer = orgNummer, tssEksternId = "8000000"), + ) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlagForInstitusjon( + behandlingId = behandling.id, + barnasIdenter = listOf(fnr), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel( + lagVilkårsvurdering( + behandling.fagsak.aktør, + behandling, + resultat = Resultat.IKKE_VURDERT, + ), + ) + + val manueltBrevRequest = ManueltBrevRequest( + brevmal = Brevmal.INNHENTE_OPPLYSNINGER_INSTITUSJON, + mottakerIdent = orgNummer, + ).byggMottakerdata( + behandling, + persongrunnlagService, + arbeidsfordelingService, + ) + dokumentService.sendManueltBrev(manueltBrevRequest, behandling, behandling.fagsak.id) + + verify(exactly = 1) { + integrasjonClient.journalførDokument(match { it.fnr == fnr && it.avsenderMottaker?.id == orgNummer && it.avsenderMottaker?.navn == "Testinstitusjon" }) + } + assertEquals(fnr, manueltBrevRequest.vedrørende?.fødselsnummer) + assertEquals("institusjonsbarnets navn", manueltBrevRequest.vedrørende?.navn) + } + + @Test + fun `Test sending svartidsbrev til institusjon`() { + val fnr = "10121079074" + val orgNummer = "998765432" + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent( + fødselsnummer = fnr, + fagsakType = FagsakType.INSTITUSJON, + institusjon = InstitusjonInfo(orgNummer = orgNummer, tssEksternId = "8000000"), + ) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlagForInstitusjon( + behandlingId = behandling.id, + barnasIdenter = listOf(fnr), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel( + lagVilkårsvurdering( + behandling.fagsak.aktør, + behandling, + resultat = Resultat.IKKE_VURDERT, + ), + ) + + val manueltBrevRequest = ManueltBrevRequest( + brevmal = Brevmal.SVARTIDSBREV_INSTITUSJON, + mottakerIdent = orgNummer, + ).byggMottakerdata( + behandling, + persongrunnlagService, + arbeidsfordelingService, + ) + dokumentService.sendManueltBrev(manueltBrevRequest, behandling, behandling.fagsak.id) + + verify(exactly = 1) { + integrasjonClient.journalførDokument(match { it.fnr == fnr && it.avsenderMottaker?.id == orgNummer && it.avsenderMottaker?.navn == "Testinstitusjon" }) + } + assertEquals(fnr, manueltBrevRequest.vedrørende?.fødselsnummer) + assertEquals("institusjonsbarnets navn", manueltBrevRequest.vedrørende?.navn) + } + + @Test + fun `Test sending forlenget svartidsbrev til institusjon`() { + val fnr = "11121079074" + val orgNummer = "998765432" + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent( + fødselsnummer = fnr, + fagsakType = FagsakType.INSTITUSJON, + institusjon = InstitusjonInfo(orgNummer = orgNummer, tssEksternId = "8000000"), + ) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlagForInstitusjon( + behandlingId = behandling.id, + barnasIdenter = listOf(fnr), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel( + lagVilkårsvurdering( + behandling.fagsak.aktør, + behandling, + resultat = Resultat.IKKE_VURDERT, + ), + ) + + val manueltBrevRequest = ManueltBrevRequest( + brevmal = Brevmal.FORLENGET_SVARTIDSBREV_INSTITUSJON, + mottakerIdent = orgNummer, + antallUkerSvarfrist = 3, + ).byggMottakerdata( + behandling, + persongrunnlagService, + arbeidsfordelingService, + ) + dokumentService.sendManueltBrev(manueltBrevRequest, behandling, behandling.fagsak.id) + + verify(exactly = 1) { + integrasjonClient.journalførDokument(match { it.fnr == fnr && it.avsenderMottaker?.id == orgNummer && it.avsenderMottaker?.navn == "Testinstitusjon" }) + } + assertEquals(fnr, manueltBrevRequest.vedrørende?.fødselsnummer) + assertEquals("institusjonsbarnets navn", manueltBrevRequest.vedrørende?.navn) + } + + fun lagTestPersonopplysningGrunnlagForInstitusjon( + behandlingId: Long, + barnasIdenter: List, + barnasFødselsdatoer: List = barnasIdenter.map { LocalDate.of(2019, 1, 1) }, + barnAktør: List = barnasIdenter.map { fødselsnummer -> + tilAktør(fødselsnummer).also { + it.personidenter.add( + Personident( + fødselsnummer = fødselsnummer, + aktør = it, + aktiv = fødselsnummer == it.personidenter.first().fødselsnummer, + ), + ) + } + }, + ): PersonopplysningGrunnlag { + val personopplysningGrunnlag = PersonopplysningGrunnlag(behandlingId = behandlingId) + val bostedsadresse = GrMatrikkeladresse( + matrikkelId = null, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ) + + barnAktør.mapIndexed { index, aktør -> + personopplysningGrunnlag.personer.add( + Person( + aktør = aktør, + type = PersonType.BARN, + personopplysningGrunnlag = personopplysningGrunnlag, + fødselsdato = barnasFødselsdatoer[index], + navn = "institusjonsbarnets navn", + kjønn = Kjønn.MANN, + ).also { barn -> + barn.statsborgerskap = mutableListOf( + GrStatsborgerskap( + landkode = "NOR", + medlemskap = Medlemskap.NORDEN, + person = barn, + ), + ) + barn.bostedsadresser = mutableListOf(bostedsadresse.apply { person = barn }) + barn.sivilstander = mutableListOf( + GrSivilstand( + type = SIVILSTAND.UGIFT, + person = barn, + ), + ) + }, + ) + } + return personopplysningGrunnlag + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerControllerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerControllerTest.kt new file mode 100644 index 000000000..debcf7277 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/brev/mottaker/BrevmottakerControllerTest.kt @@ -0,0 +1,59 @@ +package no.nav.familie.ba.sak.kjerne.brev.mottaker + +import io.mockk.mockk +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.ekstern.restDomene.RestBrevmottaker +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class BrevmottakerControllerTest( + @Autowired private val brevmottakerService: BrevmottakerService, + @Autowired private val aktørIdRepository: AktørIdRepository, + @Autowired private val fagsakRepository: FagsakRepository, + @Autowired private val behandlingRepository: BehandlingRepository, +) : AbstractSpringIntegrationTest() { + + val brevmottakerController = BrevmottakerController( + brevmottakerService = brevmottakerService, + tilgangService = mockk(relaxed = true), + utvidetBehandlingService = mockk(relaxed = true), + ) + + @Test + @Tag("integration") + fun kanLagreOgSlette() { + val fagsak = + defaultFagsak(aktør = randomAktør().also { aktørIdRepository.save(it) }).let { fagsakRepository.save(it) } + val behandling = lagBehandling(fagsak = fagsak).let { behandlingRepository.save(it) } + + val brevmottaker = RestBrevmottaker( + null, + MottakerType.FULLMEKTIG, + "navn", + "adresse", + null, + "postnummer", + "poststed", + "NO", + + ) + brevmottakerController.leggTilBrevmottaker(behandling.id, brevmottaker) + + brevmottakerController.leggTilBrevmottaker(behandling.id, brevmottaker.copy(type = MottakerType.VERGE)) + brevmottakerController.hentBrevmottakere(behandling.id).body?.data!!.apply { + Assertions.assertThat(this).hasSize(2) + forEach { lagretBrevmottaker -> + brevmottakerController.fjernBrevmottaker(behandling.id, lagretBrevmottaker.id!!) + } + } + Assertions.assertThat(brevmottakerController.hentBrevmottakere(behandling.id).body?.data!!).hasSize(0) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/Vilk\303\245rsvurderingTilValutakursIntegrasjonTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/Vilk\303\245rsvurderingTilValutakursIntegrasjonTest.kt" new file mode 100644 index 000000000..c4d80b4df --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/Vilk\303\245rsvurderingTilValutakursIntegrasjonTest.kt" @@ -0,0 +1,55 @@ +package no.nav.familie.ba.sak.kjerne.eøs + +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.KompetanseTestController +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingTestController +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +class VilkårsvurderingTilValutakursIntegrasjonTest : AbstractSpringIntegrationTest() { + + @Autowired + lateinit var vilkårsvurderingTestController: VilkårsvurderingTestController + + @Autowired + lateinit var kompetanseTestController: KompetanseTestController + + @Test + fun `vilkårsvurdering med EØS-perioder + kompetanser med sekundærland fører til skjemaer med valutakurser`() { + val søkerStartdato = 1.jan(2020).tilLocalDate() + val barnStartdato = 2.jan(2020).tilLocalDate() + + val vilkårsvurderingRequest = mapOf( + søkerStartdato to mapOf( + Vilkår.BOSATT_I_RIKET to "EEEEEEEEEEEEEEEE", + Vilkår.LOVLIG_OPPHOLD to "EEEEEEEEEEEEEEEE", + ), + barnStartdato to mapOf( + Vilkår.UNDER_18_ÅR to "++++++++++++++++", + Vilkår.GIFT_PARTNERSKAP to "++++++++++++++++", + Vilkår.BOSATT_I_RIKET to "EEEEEEEEEEEEEEEE", + Vilkår.LOVLIG_OPPHOLD to "EEEEEEEEEEEEEEEE", + Vilkår.BOR_MED_SØKER to "EEEEEEEEEEEEEEEE", + ), + ) + + val kompetanseRequest = mapOf( + barnStartdato to "PPPSSSSSSPPSSS--", + ) + + val utvidetBehandlingFør = + vilkårsvurderingTestController.opprettBehandlingMedVilkårsvurdering(vilkårsvurderingRequest) + .body?.data!! + + assertTrue(utvidetBehandlingFør.valutakurser.isEmpty()) + + val utvidetBehandlingEtter = + kompetanseTestController.endreKompetanser(utvidetBehandlingFør.behandlingId, kompetanseRequest) + .body?.data!! + + assertTrue(utvidetBehandlingEtter.valutakurser.isNotEmpty()) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/DifferanseberegningIntegrasjonTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/DifferanseberegningIntegrasjonTest.kt" new file mode 100644 index 000000000..a7f580e31 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/DifferanseberegningIntegrasjonTest.kt" @@ -0,0 +1,157 @@ +package no.nav.familie.ba.sak.kjerne.eøs.differanseberegning + +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.KompetanseTestController +import no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp.UtenlandskPeriodebeløpTestController +import no.nav.familie.ba.sak.kjerne.eøs.valutakurs.ValutakursTestController +import no.nav.familie.ba.sak.kjerne.tidslinje.Periode +import no.nav.familie.ba.sak.kjerne.tidslinje.komposisjon.innholdForTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidslinje +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.tidsrom +import no.nav.familie.ba.sak.kjerne.tidslinje.util.jan +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Utbetalingsperiode +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingTestController +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.unleash.UnleashService +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +class DifferanseberegningIntegrasjonTest : AbstractSpringIntegrationTest() { + + @Autowired + lateinit var vilkårsvurderingTestController: VilkårsvurderingTestController + + @Autowired + lateinit var tilkjentYtelseTestController: TilkjentYtelseTestController + + @Autowired + lateinit var kompetanseTestController: KompetanseTestController + + @Autowired + lateinit var utenlandskPeriodebeløpTestController: UtenlandskPeriodebeløpTestController + + @Autowired + lateinit var valutakursTestController: ValutakursTestController + + @Autowired + lateinit var unleashService: UnleashService + + @Test + fun `vilkårsvurdering med EØS-perioder + kompetanser med sekundærland fører til skjemaer med valutakurser`() { + val søkerStartdato = 1.jan(2020).tilLocalDate() + val barnStartdato = 2.jan(2020).tilLocalDate() + + val vilkårsvurderingRequest = mapOf( + søkerStartdato to mapOf( + Vilkår.BOSATT_I_RIKET to "EEEEEEEEEEEEEEEE", + Vilkår.LOVLIG_OPPHOLD to "EEEEEEEEEEEEEEEE", + ), + barnStartdato to mapOf( + Vilkår.UNDER_18_ÅR to "++++++++++++++++", + Vilkår.GIFT_PARTNERSKAP to "++++++++++++++++", + Vilkår.BOSATT_I_RIKET to "EEEEEEEEEEEEEEEE", + Vilkår.LOVLIG_OPPHOLD to "EEEEEEEEEEEEEEEE", + Vilkår.BOR_MED_SØKER to "ÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉ", + ), + ) + + val deltBosteRequest = mapOf( + barnStartdato to "/////00000011111", + ) + + val kompetanseRequest = mapOf( + barnStartdato to "PPPSSSSSSPPSSS--", + ) + + val utenlandskPeriodebeløpRequest = mapOf( + barnStartdato to "3333444566677777", + ) + + val valutakursRequest = mapOf( + barnStartdato to "5555566644234489", + ) + + val utvidetBehandlingFørsteGang = + vilkårsvurderingTestController.opprettBehandlingMedVilkårsvurdering(vilkårsvurderingRequest) + .body?.data!! + + val sumUtbetalingFørsteGang = utvidetBehandlingFørsteGang.utbetalingsperioder.sumUtbetaling() + + // tilkjentYtelseTestController.lagInitiellTilkjentYtelse(utvidetBehandling1.behandlingId) + val sumUtbetalingDelt = tilkjentYtelseTestController + .oppdaterEndretUtebetalingAndeler(utvidetBehandlingFørsteGang.behandlingId, deltBosteRequest) + .body?.data!!.utbetalingsperioder.sumUtbetaling() + + kompetanseTestController.endreKompetanser(utvidetBehandlingFørsteGang.behandlingId, kompetanseRequest) + utenlandskPeriodebeløpTestController.endreUtenlandskePeriodebeløp( + utvidetBehandlingFørsteGang.behandlingId, + utenlandskPeriodebeløpRequest, + ) + + val utvidetbehandlingDifferanseberegnet = valutakursTestController + .endreValutakurser(utvidetBehandlingFørsteGang.behandlingId, valutakursRequest) + .body?.data!! + + val sumUtbetalingDifferanseberegnet = + utvidetbehandlingDifferanseberegnet.utbetalingsperioder.sumUtbetaling() + + Assertions.assertEquals(3, utvidetbehandlingDifferanseberegnet.endretUtbetalingAndeler.size) + Assertions.assertEquals(10, utvidetbehandlingDifferanseberegnet.utbetalingsperioder.size) + + val vilkårsvurderingRequest2 = mapOf( + søkerStartdato to mapOf( + Vilkår.BOSATT_I_RIKET to "NNNNNNNNNNNNNNNN", + Vilkår.LOVLIG_OPPHOLD to "EEEEEEEEEEEEEEEE", + ), + barnStartdato to mapOf( + Vilkår.UNDER_18_ÅR to "++++++++++++++++", + Vilkår.GIFT_PARTNERSKAP to "++++++++++++++++", + Vilkår.BOSATT_I_RIKET to "EEEEEEEEEEEEEEEE", + Vilkår.LOVLIG_OPPHOLD to "EEEEEEEEEEEEEEEE", + // TODO: Fiks test slik at den fungerer når toggle er skrudd på og av uten dette hacket + if (unleashService.isEnabled(FeatureToggleConfig.ENDRET_EØS_REGELVERKFILTER_FOR_BARN)) { + Vilkår.BOR_MED_SØKER to "DDDDDDDDDDDDDDDD" + } else { + Vilkår.BOR_MED_SØKER to "ÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉÉ" + }, + ), + ) + + val utvidetBehandlingTilbakestilt = vilkårsvurderingTestController + .oppdaterVilkårsvurderingIBehandling( + utvidetbehandlingDifferanseberegnet.behandlingId, + vilkårsvurderingRequest2, + ) + .body?.data!! + + val sumUtbetalingTilbakestilt = + utvidetBehandlingTilbakestilt.utbetalingsperioder.sumUtbetaling() + + Assertions.assertEquals(3, utvidetBehandlingTilbakestilt.endretUtbetalingAndeler.size) + Assertions.assertEquals(3, utvidetBehandlingTilbakestilt.utbetalingsperioder.size) + + Assertions.assertTrue(sumUtbetalingFørsteGang > 0) + Assertions.assertTrue(sumUtbetalingDelt < sumUtbetalingFørsteGang) + Assertions.assertTrue(sumUtbetalingDifferanseberegnet < sumUtbetalingDelt) + Assertions.assertTrue(sumUtbetalingTilbakestilt == sumUtbetalingDelt) + } +} + +fun Iterable.sumUtbetaling(): Int { + val tidslinje = tidslinje { + this.map { + Periode( + it.periodeFom.tilMånedTidspunkt(), + it.periodeTom.tilMånedTidspunkt(), + it.utbetaltPerMnd, + ) + } + } + + return (tidslinje.tidsrom()).fold(0) { sum, tidspunkt -> + sum + (tidslinje.innholdForTidspunkt(tidspunkt).innhold ?: 0) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/TilkjentYtelseTestController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/TilkjentYtelseTestController.kt" new file mode 100644 index 000000000..66cc9f788 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/differanseberegning/TilkjentYtelseTestController.kt" @@ -0,0 +1,74 @@ +package no.nav.familie.ba.sak.kjerne.eøs.differanseberegning + +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.domene.EndretUtbetalingAndelMedAndelerTilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndelRepository +import no.nav.familie.ba.sak.kjerne.eøs.util.DeltBostedBuilder +import no.nav.familie.ba.sak.kjerne.eøs.util.tilEndreteUtebetalingAndeler +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.context.annotation.Profile +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate + +@RestController +@RequestMapping("/api/test/tilkjentytelse") +@ProtectedWithClaims(issuer = "azuread") +@Validated +@Profile("!prod") +class TilkjentYtelseTestController( + private val utvidetBehandlingService: UtvidetBehandlingService, + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + private val beregningService: BeregningService, + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + private val endretUtbetalingAndelRepository: EndretUtbetalingAndelRepository, +) { + @PutMapping(path = ["{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun oppdaterEndretUtebetalingAndeler( + @PathVariable behandlingId: Long, + @RequestBody restDeltBosted: Map, + ): ResponseEntity> { + val behandling = behandlingHentOgPersisterService.hent(behandlingId) + val personopplysningGrunnlag = personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId)!! + + val tilkjentYtelse = beregningService.oppdaterBehandlingMedBeregning(behandling, personopplysningGrunnlag) + + restDeltBosted.tilEndretUtbetalingAndeler(personopplysningGrunnlag, tilkjentYtelse).forEach { + val lagretEndretUtbetalingAndel = endretUtbetalingAndelRepository.saveAndFlush(it.endretUtbetalingAndel) + + beregningService.oppdaterBehandlingMedBeregning( + behandling, + personopplysningGrunnlag, + lagretEndretUtbetalingAndel, + ) + } + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } +} + +private fun Map.tilEndretUtbetalingAndeler( + personopplysningGrunnlag: PersonopplysningGrunnlag, + tilkjentYtelse: TilkjentYtelse, +): Collection { + return this.map { (dato, tidslinje) -> + val person = personopplysningGrunnlag.personer.first { it.fødselsdato == dato } + DeltBostedBuilder(dato.tilMånedTidspunkt(), tilkjentYtelse) + .medDeltBosted(tidslinje, person) + .bygg().tilEndreteUtebetalingAndeler() + }.flatten() +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseRepositoryTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseRepositoryTest.kt" new file mode 100644 index 000000000..ea64f83d4 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseRepositoryTest.kt" @@ -0,0 +1,74 @@ +package no.nav.familie.ba.sak.kjerne.eøs.kompetanse + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.lagKompetanse +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.YearMonth + +class KompetanseRepositoryTest( + @Autowired private val aktørIdRepository: AktørIdRepository, + @Autowired private val fagsakRepository: FagsakRepository, + @Autowired private val behandlingRepository: BehandlingRepository, + @Autowired private val kompetanseRepository: KompetanseRepository, +) : AbstractSpringIntegrationTest() { + + @Test + fun `Skal lagre flere kompetanser med gjenbruk av flere aktører`() { + val søker = aktørIdRepository.save(randomAktør()) + val barn1 = aktørIdRepository.save(randomAktør()) + val barn2 = aktørIdRepository.save(randomAktør()) + + val fagsak = fagsakRepository.save(Fagsak(aktør = søker)) + val behandling = behandlingRepository.save(lagBehandling(fagsak)) + + val kompetanse = kompetanseRepository.save( + lagKompetanse( + barnAktører = setOf(barn1, barn2), + ).also { it.behandlingId = behandling.id }, + ) + + val kompetanse2 = kompetanseRepository.save( + lagKompetanse( + barnAktører = setOf(barn1, barn2), + ).also { it.behandlingId = behandling.id }, + ) + + assertEquals(kompetanse.barnAktører, kompetanse2.barnAktører) + } + + @Test + fun `Skal lagre skjema-feltene`() { + val søker = aktørIdRepository.save(randomAktør()) + val barn1 = aktørIdRepository.save(randomAktør()) + + val fagsak = fagsakRepository.save(Fagsak(aktør = søker)) + val behandling = behandlingRepository.save(lagBehandling(fagsak)) + + val kompetanse = kompetanseRepository.save( + lagKompetanse( + behandlingId = behandling.id, + barnAktører = setOf(barn1), + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2021, 12), + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + annenForeldersAktivitet = KompetanseAktivitet.MOTTAR_PENSJON, + annenForeldersAktivitetsland = "pl", + barnetsBostedsland = "sl", + ), + ) + + val hentedeKompetanser = kompetanseRepository.finnFraBehandlingId(behandlingId = behandling.id) + + assertEquals(1, hentedeKompetanser.size) + assertEquals(kompetanse, hentedeKompetanser.first()) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseTestController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseTestController.kt" new file mode 100644 index 000000000..97c5673fb --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseTestController.kt" @@ -0,0 +1,61 @@ +package no.nav.familie.ba.sak.kjerne.eøs.kompetanse + +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.slåSammen +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.Kompetanse +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.util.KompetanseBuilder +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.context.annotation.Profile +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate + +@RestController +@RequestMapping("/api/test/kompetanser") +@ProtectedWithClaims(issuer = "azuread") +@Validated +@Profile("!prod") +class KompetanseTestController( + private val utvidetBehandlingService: UtvidetBehandlingService, + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + private val kompetanseService: KompetanseService, +) { + + @PutMapping(path = ["{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun endreKompetanser( + @PathVariable behandlingId: Long, + @RequestBody restKompetanser: Map, + ): ResponseEntity> { + val behandlingIdObjekt = BehandlingId(behandlingId) + val personopplysningGrunnlag = personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingIdObjekt.id)!! + restKompetanser.tilKompetanser(behandlingIdObjekt, personopplysningGrunnlag).forEach { + kompetanseService.oppdaterKompetanse(behandlingIdObjekt, it) + } + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingIdObjekt.id))) + } +} + +private fun Map.tilKompetanser( + behandlingId: BehandlingId, + personopplysningGrunnlag: PersonopplysningGrunnlag, +): Collection { + return this.map { (dato, tidslinje) -> + val person = personopplysningGrunnlag.personer.first { it.fødselsdato == dato } + KompetanseBuilder(dato.tilMånedTidspunkt(), behandlingId) + .medKompetanse(tidslinje, person) + .byggKompetanser() + }.flatten().slåSammen() +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseUtfyltTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseUtfyltTest.kt" new file mode 100644 index 000000000..3b5d7565f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/kompetanse/KompetanseUtfyltTest.kt" @@ -0,0 +1,168 @@ +package no.nav.familie.ba.sak.kjerne.eøs.kompetanse + +import no.nav.familie.ba.sak.ekstern.restDomene.UtfyltStatus +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestKompetanse +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseAktivitet +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.KompetanseResultat +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.lagKompetanse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.BarnetsBostedsland +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class KompetanseUtfyltTest { + + @Test + fun `Skal sette UtfyltStatus til OK når alle felter i skjema er fylt ut`() { + val kompetanse = lagKompetanse( + annenForeldersAktivitet = KompetanseAktivitet.I_ARBEID, + barnetsBostedsland = BarnetsBostedsland.NORGE.name, + annenForeldersAktivitetsland = "NORGE", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + ) + + val restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(UtfyltStatus.OK, restKompetanse.status) + } + + @Test + fun `Skal sette UtfyltStatus til OK dersom alle felter unntatt annenForeldersAktivitetsland er fylt ut og annenForeldersAktivitet er IKKE_AKTUELT eller INAKTIV`() { + var kompetanse = lagKompetanse( + annenForeldersAktivitet = KompetanseAktivitet.IKKE_AKTUELT, + barnetsBostedsland = BarnetsBostedsland.NORGE.name, + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + ) + + var restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(UtfyltStatus.OK, restKompetanse.status) + + kompetanse = lagKompetanse( + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + barnetsBostedsland = BarnetsBostedsland.NORGE.name, + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + søkersAktivitetsland = "NO", + ) + + restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(UtfyltStatus.OK, restKompetanse.status) + } + + @Test + fun `Skal sette UtfyltStatus til UFULLSTENDIG dersom alle felter unntatt annenForeldersAktivitetsland er fylt ut og annenForeldersAktivitet ikke er IKKE_AKTUELT eller INAKTIV`() { + var kompetanse = lagKompetanse( + annenForeldersAktivitet = KompetanseAktivitet.I_ARBEID, + barnetsBostedsland = BarnetsBostedsland.NORGE.name, + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + ) + + var restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(UtfyltStatus.UFULLSTENDIG, restKompetanse.status) + + kompetanse = lagKompetanse( + annenForeldersAktivitet = KompetanseAktivitet.MOTTAR_PENSJON, + barnetsBostedsland = BarnetsBostedsland.NORGE.name, + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + ) + + restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(UtfyltStatus.UFULLSTENDIG, restKompetanse.status) + + kompetanse = lagKompetanse( + annenForeldersAktivitet = KompetanseAktivitet.FORSIKRET_I_BOSTEDSLAND, + barnetsBostedsland = BarnetsBostedsland.NORGE.name, + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + ) + + restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(UtfyltStatus.UFULLSTENDIG, restKompetanse.status) + + kompetanse = lagKompetanse( + annenForeldersAktivitet = KompetanseAktivitet.MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN, + barnetsBostedsland = BarnetsBostedsland.NORGE.name, + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + ) + + restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(UtfyltStatus.UFULLSTENDIG, restKompetanse.status) + } + + @Test + fun `Skal sette UtfyltStatus til UFULLSTENDIG dersom 1 til 4 felter er satt med unntak av regel om annenForeldersAktivitet`() { + var kompetanse = lagKompetanse( + annenForeldersAktivitet = KompetanseAktivitet.IKKE_AKTUELT, + ) + + var restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(UtfyltStatus.UFULLSTENDIG, restKompetanse.status) + + kompetanse = lagKompetanse( + annenForeldersAktivitet = KompetanseAktivitet.INAKTIV, + barnetsBostedsland = BarnetsBostedsland.NORGE.name, + ) + + restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(UtfyltStatus.UFULLSTENDIG, restKompetanse.status) + + kompetanse = lagKompetanse( + annenForeldersAktivitet = KompetanseAktivitet.FORSIKRET_I_BOSTEDSLAND, + barnetsBostedsland = BarnetsBostedsland.NORGE.name, + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + ) + + restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(UtfyltStatus.UFULLSTENDIG, restKompetanse.status) + + kompetanse = lagKompetanse( + annenForeldersAktivitet = KompetanseAktivitet.MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN, + barnetsBostedsland = BarnetsBostedsland.NORGE.name, + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + ) + + restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(UtfyltStatus.UFULLSTENDIG, restKompetanse.status) + } + + @Test + fun `Skal sette UtfyltStatus til IKKE_UTFYLT dersom ingen av feltene er utfylt`() { + val kompetanse = lagKompetanse() + + val restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(UtfyltStatus.IKKE_UTFYLT, restKompetanse.status) + } + + @Test + fun `Skal sette UtfyltStatus til UFULLSTENDIG dersom alle felter unntatt søkersAktivitetsland er fylt ut`() { + val kompetanse = lagKompetanse( + annenForeldersAktivitet = KompetanseAktivitet.I_ARBEID, + barnetsBostedsland = BarnetsBostedsland.NORGE.name, + annenForeldersAktivitetsland = "NORGE", + kompetanseResultat = KompetanseResultat.NORGE_ER_PRIMÆRLAND, + søkersAktivitet = KompetanseAktivitet.ARBEIDER, + ) + + val restKompetanse = kompetanse.tilRestKompetanse() + + assertEquals(UtfyltStatus.UFULLSTENDIG, restKompetanse.status) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pControllerTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pControllerTest.kt" new file mode 100644 index 000000000..260edc355 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pControllerTest.kt" @@ -0,0 +1,97 @@ +package no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp + +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import jakarta.validation.ConstraintViolationException +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtenlandskPeriodebeløp +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.eøs.assertEqualsUnordered +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.sikkerhet.TilgangService +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpStatus +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration + +@SpringBootTest +@ContextConfiguration(classes = [TestConfig::class, UtenlandskPeriodebeløpController::class, ValidationAutoConfiguration::class]) +@ActiveProfiles("postgres", "integrasjonstest") +class UtenlandskPeriodebeløpControllerTest { + + @Autowired + lateinit var featureToggleService: FeatureToggleService + + @Autowired + lateinit var utenlandskPeriodebeløpController: UtenlandskPeriodebeløpController + + @Autowired + lateinit var utenlandskPeriodebeløpRepository: UtenlandskPeriodebeløpRepository + + @Autowired + lateinit var utenlandskPeriodebeløpService: UtenlandskPeriodebeløpService + + @Autowired + lateinit var utvidetBehandlingService: UtvidetBehandlingService + + @Test + fun `Skal kaste feil dersom validering av input feiler`() { + val exception = assertThrows { + utenlandskPeriodebeløpController.oppdaterUtenlandskPeriodebeløp( + 1, + RestUtenlandskPeriodebeløp(1, null, null, emptyList(), beløp = (-1.0).toBigDecimal(), null, null, null), + ) + } + + val forventedeFelterMedFeil = listOf("beløp") + val faktiskeFelterMedFeil = exception.constraintViolations.map { constraintViolation -> + constraintViolation.propertyPath.toString().split(".").last() + } + + assertEqualsUnordered(forventedeFelterMedFeil, faktiskeFelterMedFeil) + + println(faktiskeFelterMedFeil) + } + + @Test + fun `Skal ikke kaste feil dersom validering av input går bra`() { + every { utenlandskPeriodebeløpRepository.getById(any()) } returns UtenlandskPeriodebeløp.NULL + every { utenlandskPeriodebeløpService.oppdaterUtenlandskPeriodebeløp(any(), any()) } just runs + + val response = utenlandskPeriodebeløpController.oppdaterUtenlandskPeriodebeløp( + 1, + RestUtenlandskPeriodebeløp(1, null, null, emptyList(), beløp = 1.0.toBigDecimal(), null, null, null), + ) + + assertEquals(HttpStatus.OK, response.statusCode) + } +} + +class TestConfig { + + @Bean + fun featureToggleService(): FeatureToggleService = mockk() + + @Bean + fun utenlandskPeriodebeløpService(): UtenlandskPeriodebeløpService = mockk() + + @Bean + fun utenlandskPeriodebeløpRepository(): UtenlandskPeriodebeløpRepository = mockk() + + @Bean + fun personidentService(): PersonidentService = mockk() + + @Bean + fun utvidetBehandlingService(): UtvidetBehandlingService = mockk(relaxed = true) + + @Bean + fun tilgangService(): TilgangService = mockk(relaxed = true) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pRepositoryTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pRepositoryTest.kt" new file mode 100644 index 000000000..55295ac2b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pRepositoryTest.kt" @@ -0,0 +1,75 @@ +package no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Intervall +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.lagUtenlandskPeriodebeløp +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.YearMonth + +class UtenlandskPeriodebeløpRepositoryTest( + @Autowired private val aktørIdRepository: AktørIdRepository, + @Autowired private val fagsakRepository: FagsakRepository, + @Autowired private val behandlingRepository: BehandlingRepository, + @Autowired private val utenlandskPeriodebeløpRepository: UtenlandskPeriodebeløpRepository, +) : AbstractSpringIntegrationTest() { + + @Test + fun `Skal lagre flere utenlandske periodebeløp med gjenbruk av flere aktører`() { + val søker = aktørIdRepository.save(randomAktør()) + val barn1 = aktørIdRepository.save(randomAktør()) + val barn2 = aktørIdRepository.save(randomAktør()) + + val fagsak = fagsakRepository.save(Fagsak(aktør = søker)) + val behandling = behandlingRepository.save(lagBehandling(fagsak)) + + val utenlandskPeriodebeløp = utenlandskPeriodebeløpRepository.save( + lagUtenlandskPeriodebeløp( + barnAktører = setOf(barn1, barn2), + ).also { it.behandlingId = behandling.id }, + ) + + val utenlandskPeriodebeløp2 = utenlandskPeriodebeløpRepository.save( + lagUtenlandskPeriodebeløp( + barnAktører = setOf(barn1, barn2), + ).also { it.behandlingId = behandling.id }, + ) + + assertEquals(utenlandskPeriodebeløp.barnAktører, utenlandskPeriodebeløp2.barnAktører) + } + + @Test + fun `Skal lagre skjema-feltene`() { + val søker = aktørIdRepository.save(randomAktør()) + val barn1 = aktørIdRepository.save(randomAktør()) + + val fagsak = fagsakRepository.save(Fagsak(aktør = søker)) + val behandling = behandlingRepository.save(lagBehandling(fagsak)) + + val utenlandskPeriodebeløp = utenlandskPeriodebeløpRepository.save( + lagUtenlandskPeriodebeløp( + behandlingId = behandling.id, + barnAktører = setOf(barn1), + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2021, 12), + beløp = BigDecimal.valueOf(1_234), + valutakode = "EUR", + intervall = Intervall.UKENTLIG, + ), + ) + + val hentedeUtenlandskePeriodebeløp = + utenlandskPeriodebeløpRepository.finnFraBehandlingId(behandlingId = behandling.id) + + assertEquals(1, hentedeUtenlandskePeriodebeløp.size) + assertEquals(utenlandskPeriodebeløp, hentedeUtenlandskePeriodebeløp.first()) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pTestController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pTestController.kt" new file mode 100644 index 000000000..ef700c96d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskPeriodebel\303\270pTestController.kt" @@ -0,0 +1,60 @@ +package no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp + +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.slåSammen +import no.nav.familie.ba.sak.kjerne.eøs.util.UtenlandskPeriodebeløpBuilder +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.context.annotation.Profile +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate + +@RestController +@RequestMapping("/api/test/utenlandskeperiodebeløp") +@ProtectedWithClaims(issuer = "azuread") +@Validated +@Profile("!prod") +class UtenlandskPeriodebeløpTestController( + private val utvidetBehandlingService: UtvidetBehandlingService, + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + private val utenlandskPeriodebeløpService: UtenlandskPeriodebeløpService, +) { + + @PutMapping(path = ["{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun endreUtenlandskePeriodebeløp( + @PathVariable behandlingId: Long, + @RequestBody restUtenlandskePeriodebeløp: Map, + ): ResponseEntity> { + val behandlingIdObjekt = BehandlingId(behandlingId) + val personopplysningGrunnlag = personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingIdObjekt.id)!! + restUtenlandskePeriodebeløp.tilUtenlandskePeriodebeløp(behandlingIdObjekt, personopplysningGrunnlag).forEach { + utenlandskPeriodebeløpService.oppdaterUtenlandskPeriodebeløp(behandlingIdObjekt, it) + } + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingIdObjekt.id))) + } +} + +private fun Map.tilUtenlandskePeriodebeløp( + behandlingId: BehandlingId, + personopplysningGrunnlag: PersonopplysningGrunnlag, +): Collection { + return this.map { (dato, tidslinje) -> + val person = personopplysningGrunnlag.personer.first { it.fødselsdato == dato } + UtenlandskPeriodebeløpBuilder(dato.tilMånedTidspunkt(), behandlingId) + .medBeløp(tidslinje, "EUR", "fr", person) + .bygg() + }.flatten().slåSammen() +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskePeriodebel\303\270pUtfyltTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskePeriodebel\303\270pUtfyltTest.kt" new file mode 100644 index 000000000..f1820e79d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/utenlandskperiodebel\303\270p/UtenlandskePeriodebel\303\270pUtfyltTest.kt" @@ -0,0 +1,54 @@ +package no.nav.familie.ba.sak.kjerne.eøs.utenlandskperiodebeløp + +import no.nav.familie.ba.sak.ekstern.restDomene.UtfyltStatus +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestUtenlandskPeriodebeløp +import no.nav.familie.ba.sak.kjerne.eøs.differanseberegning.domene.Intervall +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.lagUtenlandskPeriodebeløp +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +class UtenlandskePeriodebeløpUtfyltTest { + + @Test + fun `Skal sette UtfyltStatus til OK når alle felter er utfylt`() { + val utenlandskPeriodebeløp = lagUtenlandskPeriodebeløp( + beløp = BigDecimal.valueOf(500), + valutakode = "NOK", + intervall = Intervall.MÅNEDLIG, + ) + + val restUtenlandskPeriodebeløp = utenlandskPeriodebeløp.tilRestUtenlandskPeriodebeløp() + + assertEquals(UtfyltStatus.OK, restUtenlandskPeriodebeløp.status) + } + + @Test + fun `Skal sette UtfyltStatus til UFULLSTENDIG når ett eller to felter er utfylt`() { + var utenlandskPeriodebeløp = lagUtenlandskPeriodebeløp( + beløp = BigDecimal.valueOf(500), + ) + + var restUtenlandskPeriodebeløp = utenlandskPeriodebeløp.tilRestUtenlandskPeriodebeløp() + + assertEquals(UtfyltStatus.UFULLSTENDIG, restUtenlandskPeriodebeløp.status) + + utenlandskPeriodebeløp = lagUtenlandskPeriodebeløp( + beløp = BigDecimal.valueOf(500), + valutakode = "NOK", + ) + + restUtenlandskPeriodebeløp = utenlandskPeriodebeløp.tilRestUtenlandskPeriodebeløp() + + assertEquals(UtfyltStatus.UFULLSTENDIG, restUtenlandskPeriodebeløp.status) + } + + @Test + fun `Skal sette UtfyltStatus til IKKE_UTFYLT når ingen felter er utfylt`() { + val utenlandskPeriodebeløp = lagUtenlandskPeriodebeløp() + + val restUtenlandskPeriodebeløp = utenlandskPeriodebeløp.tilRestUtenlandskPeriodebeløp() + + assertEquals(UtfyltStatus.IKKE_UTFYLT, restUtenlandskPeriodebeløp.status) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursRepositoryTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursRepositoryTest.kt" new file mode 100644 index 000000000..51bc08bcd --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursRepositoryTest.kt" @@ -0,0 +1,75 @@ +package no.nav.familie.ba.sak.kjerne.eøs.valutakurs + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.lagValutakurs +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class ValutakursRepositoryTest( + @Autowired private val aktørIdRepository: AktørIdRepository, + @Autowired private val fagsakRepository: FagsakRepository, + @Autowired private val behandlingRepository: BehandlingRepository, + @Autowired private val valutakursRepository: ValutakursRepository, +) : AbstractSpringIntegrationTest() { + + @Test + fun `Skal lagre flere valutakurser med gjenbruk av flere aktører`() { + val søker = aktørIdRepository.save(randomAktør()) + val barn1 = aktørIdRepository.save(randomAktør()) + val barn2 = aktørIdRepository.save(randomAktør()) + + val fagsak = fagsakRepository.save(Fagsak(aktør = søker)) + val behandling = behandlingRepository.save(lagBehandling(fagsak)) + + val valutakurs = valutakursRepository.save( + lagValutakurs( + barnAktører = setOf(barn1, barn2), + ).also { it.behandlingId = behandling.id }, + ) + + val valutakurs2 = valutakursRepository.save( + lagValutakurs( + barnAktører = setOf(barn1, barn2), + ).also { it.behandlingId = behandling.id }, + ) + + assertEquals(valutakurs.barnAktører, valutakurs2.barnAktører) + } + + @Test + fun `Skal lagre skjema-feltene`() { + val søker = aktørIdRepository.save(randomAktør()) + val barn1 = aktørIdRepository.save(randomAktør()) + + val fagsak = fagsakRepository.save(Fagsak(aktør = søker)) + val behandling = behandlingRepository.save(lagBehandling(fagsak)) + + val valutakurs = valutakursRepository.save( + lagValutakurs( + behandlingId = behandling.id, + barnAktører = setOf(barn1), + fom = YearMonth.of(2020, 1), + tom = YearMonth.of(2021, 12), + valutakode = "EUR", + valutakursdato = LocalDate.of(2020, 2, 17), + kurs = BigDecimal.valueOf(10.453), + ), + ) + + val hentedeValutakurser = + valutakursRepository.finnFraBehandlingId(behandlingId = behandling.id) + + assertEquals(1, hentedeValutakurser.size) + assertEquals(valutakurs, hentedeValutakurser.first()) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursTestController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursTestController.kt" new file mode 100644 index 000000000..cbf196b71 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursTestController.kt" @@ -0,0 +1,60 @@ +package no.nav.familie.ba.sak.kjerne.eøs.valutakurs + +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.eøs.felles.beregning.slåSammen +import no.nav.familie.ba.sak.kjerne.eøs.util.ValutakursBuilder +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.context.annotation.Profile +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate + +@RestController +@RequestMapping("/api/test/valutakurser") +@ProtectedWithClaims(issuer = "azuread") +@Validated +@Profile("!prod") +class ValutakursTestController( + private val utvidetBehandlingService: UtvidetBehandlingService, + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + private val valutakursService: ValutakursService, +) { + + @PutMapping(path = ["{behandlingId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun endreValutakurser( + @PathVariable behandlingId: Long, + @RequestBody restValutakurser: Map, + ): ResponseEntity> { + val behandlingIdObjekt = BehandlingId(behandlingId) + val personopplysningGrunnlag = personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingIdObjekt.id)!! + restValutakurser.tilValutakurser(behandlingIdObjekt, personopplysningGrunnlag).forEach { + valutakursService.oppdaterValutakurs(behandlingIdObjekt, it) + } + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingIdObjekt.id))) + } +} + +private fun Map.tilValutakurser( + behandlingId: BehandlingId, + personopplysningGrunnlag: PersonopplysningGrunnlag, +): Collection { + return this.map { (dato, tidslinje) -> + val person = personopplysningGrunnlag.personer.first { it.fødselsdato == dato } + ValutakursBuilder(dato.tilMånedTidspunkt(), behandlingId) + .medKurs(tidslinje, "EUR", person) + .bygg() + }.flatten().slåSammen() +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursUtfyltTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursUtfyltTest.kt" new file mode 100644 index 000000000..cbc7a8f6e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/e\303\270s/valutakurs/ValutakursUtfyltTest.kt" @@ -0,0 +1,52 @@ +package no.nav.familie.ba.sak.kjerne.eøs.valutakurs + +import no.nav.familie.ba.sak.ekstern.restDomene.UtfyltStatus +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestValutakurs +import no.nav.familie.ba.sak.kjerne.eøs.kompetanse.domene.lagValutakurs +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate + +class ValutakursUtfyltTest { + + @Test + fun `Skal sette UtfyltStatus til OK når alle felter er utfylt`() { + val valutakurs = lagValutakurs( + valutakursdato = LocalDate.now(), + kurs = BigDecimal.valueOf(10), + ) + + val restValutakurs = valutakurs.tilRestValutakurs() + + Assertions.assertEquals(UtfyltStatus.OK, restValutakurs.status) + } + + @Test + fun `Skal sette UtfyltStatus til UFULLSTENDIG når ett felt er utfylt`() { + var valutakurs = lagValutakurs( + valutakursdato = LocalDate.now(), + ) + + var restValutakurs = valutakurs.tilRestValutakurs() + + Assertions.assertEquals(UtfyltStatus.UFULLSTENDIG, restValutakurs.status) + + valutakurs = lagValutakurs( + kurs = BigDecimal.valueOf(10), + ) + + restValutakurs = valutakurs.tilRestValutakurs() + + Assertions.assertEquals(UtfyltStatus.UFULLSTENDIG, restValutakurs.status) + } + + @Test + fun `Skal sette UtfyltStatus til IKKE_UTFYLT når ingen felter er utfylt`() { + val valutakurs = lagValutakurs() + + val restValutakurs = valutakurs.tilRestValutakurs() + + Assertions.assertEquals(UtfyltStatus.IKKE_UTFYLT, restValutakurs.status) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakControllerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakControllerTest.kt new file mode 100644 index 000000000..05fab14d3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakControllerTest.kt @@ -0,0 +1,261 @@ +package no.nav.familie.ba.sak.kjerne.fagsak + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.nyOrdinærBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.ekstern.restDomene.FagsakDeltagerRolle +import no.nav.familie.ba.sak.ekstern.restDomene.InstitusjonInfo +import no.nav.familie.ba.sak.ekstern.restDomene.RestSøkParam +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.skyggesak.SkyggesakRepository +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Målform +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.institusjon.InstitusjonService +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import no.nav.familie.ba.sak.kjerne.personident.Personident +import no.nav.familie.ba.sak.kjerne.personident.PersonidentRepository +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.util.BrukerContextUtil +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.log.mdc.MDCConstants +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.slf4j.MDC +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.domain.Pageable + +class FagsakControllerTest( + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val fagsakController: FagsakController, + + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val mockIntegrasjonClient: IntegrasjonClient, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val mockPersonidentService: PersonidentService, + + @Autowired + private val aktørIdRepository: AktørIdRepository, + + @Autowired + private val personidentRepository: PersonidentRepository, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val skyggesakRepository: SkyggesakRepository, + + @Autowired + private val institusjonService: InstitusjonService, +) : AbstractSpringIntegrationTest() { + + @BeforeEach + fun init() { + MDC.put(MDCConstants.MDC_CALL_ID, "00001111") + BrukerContextUtil.mockBrukerContext(SikkerhetContext.SYSTEM_FORKORTELSE) + databaseCleanupService.truncate() + } + + @AfterEach + fun tearDown() { + BrukerContextUtil.clearBrukerContext() + } + + @Test + @Tag("integration") + fun `Skal opprette fagsak av type NORMAL`() { + val fnr = randomFnr() + + fagsakController.hentEllerOpprettFagsak(FagsakRequest(personIdent = fnr)) + val fagsak = fagsakService.hentNormalFagsak(tilAktør(fnr)) + assertEquals(fnr, fagsak?.aktør?.aktivFødselsnummer()) + assertEquals(FagsakType.NORMAL, fagsak?.type) + assertNull(fagsak?.institusjon) + } + + @Test + @Tag("integration") + fun `Skal opprette skyggesak i Sak`() { + val fnr = randomFnr() + + val fagsak = fagsakController.hentEllerOpprettFagsak(FagsakRequest(personIdent = fnr)).body?.data + + val skyggesak = skyggesakRepository.finnSkyggesakerKlareForSending(Pageable.unpaged()) + assertEquals(1, skyggesak.filter { it.fagsakId == fagsak?.id }.size) + } + + @Test + @Tag("integration") + fun `Skal returnere eksisterende fagsak på person som allerede finnes`() { + val fnr = randomFnr() + + val nyRestFagsak = fagsakController.hentEllerOpprettFagsak(FagsakRequest(personIdent = fnr)) + assertEquals(Ressurs.Status.SUKSESS, nyRestFagsak.body?.status) + assertEquals(fnr, fagsakService.hentNormalFagsak(tilAktør(fnr))?.aktør?.aktivFødselsnummer()) + + val eksisterendeRestFagsak = fagsakController.hentEllerOpprettFagsak( + FagsakRequest( + personIdent = fnr, + ), + ) + assertEquals(Ressurs.Status.SUKSESS, eksisterendeRestFagsak.body?.status) + assertEquals(eksisterendeRestFagsak.body!!.data!!.id, nyRestFagsak.body!!.data!!.id) + } + + @Test + @Tag("integration") + fun `Skal returnere eksisterende fagsak på person som allerede finnes med gammel ident`() { + val fnr = randomFnr() + val nyttFnr = randomFnr() + val aktørId = randomAktør().aktørId + + // Får ikke mockPersonopplysningerService til å virke riktig derfor oppdateres db direkte. + val aktør = aktørIdRepository.save(Aktør(aktørId)) + personidentRepository.save(Personident(fødselsnummer = fnr, aktør = aktør, aktiv = true)) + + val nyRestFagsak = fagsakController.hentEllerOpprettFagsak(FagsakRequest(personIdent = fnr)) + assertEquals(Ressurs.Status.SUKSESS, nyRestFagsak.body?.status) + assertEquals(fnr, fagsakService.hentNormalFagsak(aktør)?.aktør?.aktivFødselsnummer()) + + personidentRepository.save( + personidentRepository.getReferenceById(fnr).also { it.aktiv = false }, + ) + personidentRepository.save(Personident(fødselsnummer = nyttFnr, aktør = aktør, aktiv = true)) + + val eksisterendeRestFagsak = fagsakController.hentEllerOpprettFagsak( + FagsakRequest( + personIdent = nyttFnr, + ), + ) + assertEquals(Ressurs.Status.SUKSESS, eksisterendeRestFagsak.body?.status) + assertEquals(eksisterendeRestFagsak.body!!.data!!.id, nyRestFagsak.body!!.data!!.id) + assertEquals(nyttFnr, eksisterendeRestFagsak.body!!.data?.søkerFødselsnummer) + } + + @Test + @Tag("integration") + fun `Skal returnere eksisterende fagsak på person som allerede finnes basert på aktørid`() { + val aktørId = randomAktør() + val fagsakRequest = FagsakRequest(personIdent = aktørId.aktivFødselsnummer()) + val nyRestFagsak = fagsakController.hentEllerOpprettFagsak( + fagsakRequest, + ) + assertEquals(Ressurs.Status.SUKSESS, nyRestFagsak.body?.status) + + val eksisterendeRestFagsak = fagsakController.hentEllerOpprettFagsak(fagsakRequest) + assertEquals(Ressurs.Status.SUKSESS, eksisterendeRestFagsak.body?.status) + assertEquals(eksisterendeRestFagsak.body!!.data!!.id, nyRestFagsak.body!!.data!!.id) + } + + @Test + fun `Skal oppgi person med fagsak som fagsakdeltaker`() { + val personAktør = mockPersonidentService.hentAktør(randomFnr()) + + fagsakService.hentEllerOpprettFagsak(personAktør.aktivFødselsnummer()) + .also { fagsakService.oppdaterStatus(it, FagsakStatus.LØPENDE) } + + fagsakController.oppgiFagsakdeltagere(RestSøkParam(personAktør.aktivFødselsnummer(), emptyList())).apply { + assertEquals(personAktør.aktivFødselsnummer(), body!!.data!!.first().ident) + assertEquals(FagsakDeltagerRolle.FORELDER, body!!.data!!.first().rolle) + } + } + + @Test + fun `Skal oppgi det første barnet i listen som fagsakdeltaker`() { + val personAktør = mockPersonidentService.hentOgLagreAktør(randomFnr(), true) + val søkerAktør = mockPersonidentService.hentOgLagreAktør(ClientMocks.søkerFnr[0], true) + val barnaAktør = mockPersonidentService.hentOgLagreAktørIder(ClientMocks.barnFnr.toList().subList(0, 1), true) + + val fagsak = fagsakService.hentEllerOpprettFagsak(søkerAktør.aktivFødselsnummer()) + + val behandling = + behandlingService.opprettBehandling( + nyOrdinærBehandling( + søkersIdent = ClientMocks.søkerFnr[0], + fagsakId = fagsak.id, + ), + ) + persongrunnlagService.hentOgLagreSøkerOgBarnINyttGrunnlag( + personAktør, + barnaAktør, + behandling, + Målform.NB, + ) + + fagsakController.oppgiFagsakdeltagere( + RestSøkParam( + personAktør.aktivFødselsnummer(), + ClientMocks.barnFnr.toList(), + ), + ) + .apply { + assertEquals(ClientMocks.barnFnr.toList().subList(0, 1), body!!.data!!.map { it.ident }) + assertEquals(listOf(FagsakDeltagerRolle.BARN), body!!.data!!.map { it.rolle }) + } + } + + @Test + @Tag("integration") + fun `Skal få valideringsfeil ved oppretting av fagsak av type INSTITUSJON uten FagsakInstitusjon satt`() { + val fnr = randomFnr() + + val exception = assertThrows { + fagsakController.hentEllerOpprettFagsak( + FagsakRequest( + personIdent = fnr, + fagsakType = FagsakType.INSTITUSJON, + ), + ) + } + val fagsaker = fagsakService.hentMinimalFagsakerForPerson(tilAktør(fnr)) + assert(fagsaker.status == Ressurs.Status.FEILET) + assertEquals("Mangler påkrevd variabel orgnummer for institusjon", exception.message) + } + + @Test + @Tag("integration") + fun `Skal opprette fagsak av type INSTITUSJON hvor FagsakInstitusjon er satt`() { + val fnr = randomFnr() + + fagsakController.hentEllerOpprettFagsak( + FagsakRequest( + personIdent = fnr, + fagsakType = FagsakType.INSTITUSJON, + institusjon = InstitusjonInfo("orgnr", "tss-id"), + ), + ) + val fagsakerRessurs = fagsakService.hentMinimalFagsakerForPerson(tilAktør(fnr)) + assert(fagsakerRessurs.status == Ressurs.Status.SUKSESS) + val fagsaker = fagsakerRessurs.data!! + assert(fagsaker.isNotEmpty()) { "Fagsak skulle ha blitt opprettet" } + assertEquals(fnr, fagsaker[0].søkerFødselsnummer) + assertEquals(FagsakType.INSTITUSJON, fagsaker[0].fagsakType) + assertNotNull(fagsaker[0].institusjon) + assertEquals("orgnr", fagsaker[0].institusjon?.orgNummer) + assertEquals("tss-id", fagsaker[0].institusjon?.tssEksternId) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakIntegrationTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakIntegrationTest.kt new file mode 100644 index 000000000..28c0bcbe1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakIntegrationTest.kt @@ -0,0 +1,62 @@ +package no.nav.familie.ba.sak.kjerne.fagsak + +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.ekstern.restDomene.InstitusjonInfo +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +class FagsakIntegrationTest( + @Autowired + val fagsakService: FagsakService, +) : AbstractSpringIntegrationTest() { + + @Test + fun `hentMinimalFagsakerForPerson() skal return begge fagsaker for en person`() { + val personFnr = randomFnr() + val fagsakOmsorgperson = fagsakService.hentEllerOpprettFagsak(personFnr) + val fagsakInstitusjon = fagsakService.hentEllerOpprettFagsak( + personFnr, + false, + FagsakType.INSTITUSJON, + InstitusjonInfo("orgnr", null), + ) + val fagsakEnsligMindreÅrig = + fagsakService.hentEllerOpprettFagsak(personFnr, false, FagsakType.BARN_ENSLIG_MINDREÅRIG) + + val minimalFagsakList = fagsakService.hentMinimalFagsakerForPerson(fagsakOmsorgperson.aktør) + + assertThat(minimalFagsakList.data).hasSize(3).extracting("id") + .contains(fagsakInstitusjon.id, fagsakOmsorgperson.id, fagsakEnsligMindreÅrig.id) + } + + @Test + fun `hentMinimalFagsakForPerson() skal return riktig fagsak for en person`() { + val personFnr = randomFnr() + val fagsakOmsorgperson = fagsakService.hentEllerOpprettFagsak(personFnr) + val fagsakInstitusjon = fagsakService.hentEllerOpprettFagsak( + personFnr, + false, + FagsakType.INSTITUSJON, + InstitusjonInfo("orgnr", null), + ) + val fagsakEnsligMindreÅrig = + fagsakService.hentEllerOpprettFagsak(personFnr, false, FagsakType.BARN_ENSLIG_MINDREÅRIG) + + val defaultMinimalFagsak = fagsakService.hentMinimalFagsakForPerson(fagsakOmsorgperson.aktør) + assertThat(defaultMinimalFagsak.data!!.id).isEqualTo(fagsakOmsorgperson.id) + + val omsorgpersonMinimalFagsak = + fagsakService.hentMinimalFagsakForPerson(fagsakOmsorgperson.aktør, FagsakType.NORMAL) + assertThat(omsorgpersonMinimalFagsak.data!!.id).isEqualTo(fagsakOmsorgperson.id) + + val institusjonMinimalFagsak = + fagsakService.hentMinimalFagsakForPerson(fagsakOmsorgperson.aktør, FagsakType.INSTITUSJON) + assertThat(institusjonMinimalFagsak.data!!.id).isEqualTo(fagsakInstitusjon.id) + + val ensligMindreÅrigMinimalFagsak = + fagsakService.hentMinimalFagsakForPerson(fagsakOmsorgperson.aktør, FagsakType.BARN_ENSLIG_MINDREÅRIG) + assertThat(ensligMindreÅrigMinimalFagsak.data!!.id).isEqualTo(fagsakEnsligMindreÅrig.id) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakServiceTest.kt new file mode 100644 index 000000000..ab052ed10 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/FagsakServiceTest.kt @@ -0,0 +1,725 @@ +package no.nav.familie.ba.sak.kjerne.fagsak + +import io.mockk.every +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.ekstern.restDomene.InstitusjonInfo +import no.nav.familie.ba.sak.ekstern.restDomene.RestFagsakDeltager +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollClient +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.ForelderBarnRelasjon +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelse +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Kjønn +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.RegistrerPersongrunnlagDTO +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringRepository +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringType.SAK +import no.nav.familie.kontrakter.felles.personopplysning.FORELDERBARNRELASJONROLLE +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus +import org.springframework.web.client.HttpServerErrorException +import java.time.LocalDate +import java.time.YearMonth + +class FagsakServiceTest( + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val stegService: StegService, + + @Autowired + private val mockPersonopplysningerService: PersonopplysningerService, + + @Autowired + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + + @Autowired + private val persongrunnlagRepository: PersonopplysningGrunnlagRepository, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val saksstatistikkMellomlagringRepository: SaksstatistikkMellomlagringRepository, + + @Autowired + private val mockFamilieIntegrasjonerTilgangskontrollClient: FamilieIntegrasjonerTilgangskontrollClient, +) : AbstractSpringIntegrationTest() { + + @BeforeEach + fun init() { + databaseCleanupService.truncate() + } + + /* + This is a complicated test against following family relationship: + søker3----------- + (no case) | (medmor) + barn2 + | (medmor) + søker1----------- + | (mor) + barn1 + | (far) + søker2----------- + | (far) + barn3 + + We tests three search: + 1) search for søker1, one participant (søker1) should be returned + 2) search for barn1, three participants (barn1, søker1, søker2) should be returned + 3) search for barn2, three participants (barn2, søker3, søker1) should be returned, where fagsakId of søker3 is null + */ + @Test + fun `test å søke fagsak med fnr`() { + val søker1Fnr = randomFnr() + val søker2Fnr = randomFnr() + val søker3Fnr = randomFnr() + val barn1Fnr = randomFnr() + val barn2Fnr = randomFnr() + val barn3Fnr = randomFnr() + + val søker1Aktør = personidentService.hentAktør(søker1Fnr) + val søker2Aktør = personidentService.hentAktør(søker2Fnr) + val søker3Aktør = personidentService.hentAktør(søker3Fnr) + val barn1Aktør = personidentService.hentAktør(barn1Fnr) + val barn2Aktør = personidentService.hentAktør(barn2Fnr) + val barn3Aktør = personidentService.hentAktør(barn3Fnr) + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(eq(barn1Aktør)) + } returns PersonInfo(fødselsdato = LocalDate.of(2018, 5, 1), kjønn = Kjønn.KVINNE, navn = "barn1") + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(eq(barn2Aktør)) + } returns PersonInfo( + fødselsdato = LocalDate.of(2019, 5, 1), + kjønn = Kjønn.MANN, + navn = "barn2", + forelderBarnRelasjon = setOf( + ForelderBarnRelasjon( + søker1Aktør, + FORELDERBARNRELASJONROLLE.MEDMOR, + "søker1", + LocalDate.of(1990, 2, 19), + ), + ForelderBarnRelasjon( + søker3Aktør, + FORELDERBARNRELASJONROLLE.MEDMOR, + "søker3", + LocalDate.of(1990, 1, 10), + ), + ), + ) + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(eq(søker1Aktør)) + } returns PersonInfo(fødselsdato = LocalDate.of(1990, 2, 19), kjønn = Kjønn.KVINNE, navn = "søker1") + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(eq(søker2Aktør)) + } returns PersonInfo(fødselsdato = LocalDate.of(1991, 2, 20), kjønn = Kjønn.MANN, navn = "søker2") + + every { + mockPersonopplysningerService.hentPersoninfoEnkel(eq(barn2Aktør)) + } returns PersonInfo(fødselsdato = LocalDate.of(2019, 5, 1), kjønn = Kjønn.MANN, navn = "barn2") + + every { + mockPersonopplysningerService.hentPersoninfoEnkel(eq(barn3Aktør)) + } returns PersonInfo(fødselsdato = LocalDate.of(2017, 3, 1), kjønn = Kjønn.KVINNE, navn = "barn3") + + every { + mockPersonopplysningerService.hentPersoninfoEnkel(eq(søker1Aktør)) + } returns PersonInfo(fødselsdato = LocalDate.of(1990, 2, 19), kjønn = Kjønn.KVINNE, navn = "søker1") + + every { + mockPersonopplysningerService.hentPersoninfoEnkel(eq(søker2Aktør)) + } returns PersonInfo(fødselsdato = LocalDate.of(1991, 2, 20), kjønn = Kjønn.MANN, navn = "søker2") + + every { + mockPersonopplysningerService.hentPersoninfoEnkel(eq(søker3Aktør)) + } returns PersonInfo(fødselsdato = LocalDate.of(1990, 1, 10), kjønn = Kjønn.KVINNE, navn = "søker3") + + val fagsak0 = fagsakService.hentEllerOpprettFagsak( + FagsakRequest( + søker1Fnr, + ), + ) + + val fagsak1 = fagsakService.hentEllerOpprettFagsak( + FagsakRequest( + søker2Fnr, + ), + ) + + val førsteBehandling = stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + søkersIdent = søker1Fnr, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + søknadMottattDato = LocalDate.now(), + fagsakId = fagsak0.data!!.id, + ), + ) + stegService.håndterPersongrunnlag( + førsteBehandling, + RegistrerPersongrunnlagDTO(ident = søker1Fnr, barnasIdenter = listOf(barn1Fnr)), + ) + + behandlingService.oppdaterStatusPåBehandling(førsteBehandling.id, BehandlingStatus.AVSLUTTET) + + val andreBehandling = stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + søkersIdent = søker1Fnr, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + søknadMottattDato = LocalDate.now(), + fagsakId = fagsak0.data!!.id, + ), + ) + stegService.håndterPersongrunnlag( + andreBehandling, + RegistrerPersongrunnlagDTO( + ident = søker1Fnr, + barnasIdenter = listOf(barn1Fnr, barn2Fnr), + ), + ) + + val tredjeBehandling = stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + søkersIdent = søker2Fnr, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + søknadMottattDato = LocalDate.now(), + fagsakId = fagsak1.data!!.id, + ), + ) + stegService.håndterPersongrunnlag( + tredjeBehandling, + RegistrerPersongrunnlagDTO(ident = søker2Fnr, barnasIdenter = listOf(barn1Fnr)), + ) + + val søkeresultat1 = fagsakService.hentFagsakDeltager(søker1Fnr) + assertEquals(1, søkeresultat1.size) + assertEquals(Kjønn.KVINNE, søkeresultat1[0].kjønn) + assertEquals("søker1", søkeresultat1[0].navn) + assertEquals(fagsak0.data!!.id, søkeresultat1[0].fagsakId) + + val søkeresultat2 = fagsakService.hentFagsakDeltager(barn1Fnr) + assertEquals(3, søkeresultat2.size) + var matching = 0 + søkeresultat2.forEach { + matching += if (it.fagsakId == fagsak0.data!!.id) 1 else if (it.fagsakId == fagsak1.data!!.id) 10 else 0 + } + assertEquals(11, matching) + assertEquals(1, søkeresultat2.filter { it.ident == barn1Fnr }.size) + + val søkeresultat3 = fagsakService.hentFagsakDeltager(barn2Fnr) + assertEquals(3, søkeresultat3.size) + assertEquals(1, søkeresultat3.filter { it.ident == barn2Fnr }.size) + assertNull(søkeresultat3.find { it.ident == barn2Fnr }!!.fagsakId) + assertEquals(fagsak0.data!!.id, søkeresultat3.find { it.ident == søker1Fnr }!!.fagsakId) + assertEquals(1, søkeresultat3.filter { it.ident == søker3Fnr }.size) + assertEquals("søker3", søkeresultat3.filter { it.ident == søker3Fnr }[0].navn) + assertNull(søkeresultat3.find { it.ident == søker3Fnr }!!.fagsakId) + + val fagsak = fagsakService.hentNormalFagsak(søker1Aktør)!! + + assertEquals( + FagsakStatus.OPPRETTET.name, + saksstatistikkMellomlagringRepository.findByTypeAndTypeId(SAK, fagsak.id) + .last().jsonToSakDVH().sakStatus, + ) + } + + @Test + fun `Skal teste at arkiverte fagsaker med behandling ikke blir funnet ved søk`() { + val søker1Fnr = randomFnr() + val søker1Aktør = tilAktør(søker1Fnr) + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(eq(søker1Aktør)) + } returns PersonInfo(fødselsdato = LocalDate.of(1991, 2, 19), kjønn = Kjønn.KVINNE, navn = "søker1") + + every { + mockPersonopplysningerService.hentPersoninfoEnkel(eq(søker1Aktør)) + } returns PersonInfo(fødselsdato = LocalDate.of(1991, 2, 19), kjønn = Kjønn.KVINNE, navn = "søker1") + + val fagsak = fagsakService.hentEllerOpprettFagsak( + FagsakRequest( + søker1Fnr, + ), + ) + + stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + søkersIdent = søker1Fnr, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + søknadMottattDato = LocalDate.now(), + fagsakId = fagsak.data!!.id, + ), + ) + + fagsakService.lagre( + fagsakService.hentFagsakPåPerson(søker1Aktør).also { it?.arkivert = true }!!, + ) + + val søkeresultat1 = fagsakService.hentFagsakDeltager(søker1Fnr) + + assertEquals(1, søkeresultat1.size) + assertNull(søkeresultat1.first().fagsakId) + } + + @Test + fun `Skal teste at arkiverte fagsaker uten behandling ikke blir funnet ved søk`() { + val søker1Fnr = randomFnr() + val søker1Aktør = tilAktør(søker1Fnr) + + every { + mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(eq(søker1Aktør)) + } returns PersonInfo(fødselsdato = LocalDate.of(1992, 2, 19), kjønn = Kjønn.KVINNE, navn = "søker1") + + every { + mockPersonopplysningerService.hentPersoninfoEnkel(eq(søker1Aktør)) + } returns PersonInfo(fødselsdato = LocalDate.of(1992, 2, 19), kjønn = Kjønn.KVINNE, navn = "søker1") + + fagsakService.hentEllerOpprettFagsak( + FagsakRequest( + søker1Fnr, + ), + ) + + fagsakService.lagre( + fagsakService.hentFagsakPåPerson(søker1Aktør).also { it?.arkivert = true }!!, + ) + + val søkeresultat1 = fagsakService.hentFagsakDeltager(søker1Fnr) + + assertEquals(1, søkeresultat1.size) + assertNull(søkeresultat1.first().fagsakId) + } + + @Test + fun `Skal teste at man henter alle fagsakene til barnet`() { + val barnFnr = randomFnr() + + val barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true) + fun opprettGrunnlag(behandling: Behandling) = lagTestPersonopplysningGrunnlag( + behandling.id, + behandling.fagsak.aktør.aktivFødselsnummer(), + listOf(barnFnr), + søkerAktør = behandling.fagsak.aktør, + barnAktør = barnAktør, + ) + + val fagsakMor = fagsakService.hentEllerOpprettFagsakForPersonIdent(randomFnr()) + val behandlingMor = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsakMor)) + persongrunnlagService.lagreOgDeaktiverGammel(opprettGrunnlag(behandlingMor)) + persongrunnlagService.lagreOgDeaktiverGammel(opprettGrunnlag(behandlingMor)) + behandlingService.oppdaterStatusPåBehandling(behandlingMor.id, BehandlingStatus.AVSLUTTET) + val behandlingMor2 = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsakMor)) + persongrunnlagService.lagreOgDeaktiverGammel(opprettGrunnlag(behandlingMor2)) + + val fagsakFar = fagsakService.hentEllerOpprettFagsakForPersonIdent(randomFnr()) + val behandlingFar = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsakFar)) + persongrunnlagService.lagreOgDeaktiverGammel(opprettGrunnlag(behandlingFar)) + + val fagsaker = fagsakService.hentFagsakerPåPerson(barnAktør.first()) + assertEquals(2, fagsaker.size) + assertThat(persongrunnlagRepository.findAll()).hasSize(4) + } + + // Satte XX for at dette testet skal kjøre sist. + @Test + fun `XX Søk på fnr som ikke finnes i PDL skal vi tom liste`() { + every { + mockFamilieIntegrasjonerTilgangskontrollClient.sjekkTilgangTilPersoner(any()) + } answers { + throw HttpServerErrorException( + HttpStatus.INTERNAL_SERVER_ERROR, + "[PdlRestClient][Feil ved oppslag på person: Fant ikke person]", + ) + } + assertEquals(emptyList(), fagsakService.hentFagsakDeltager(randomFnr())) + } + + @Test + fun `Skal kun hente løpende fagsak for søker`() { + val søker = lagPerson(type = PersonType.SØKER) + + val normalFagsakForSøker = opprettFagsakForPersonMedStatus(personIdent = søker.aktør.aktivFødselsnummer(), fagsakStatus = FagsakStatus.LØPENDE) + opprettFagsakForPersonMedStatus(personIdent = randomFnr(), fagsakStatus = FagsakStatus.LØPENDE) + opprettFagsakForPersonMedStatus(personIdent = randomFnr(), fagsakStatus = FagsakStatus.AVSLUTTET) + opprettFagsakForPersonMedStatus(personIdent = randomFnr(), fagsakStatus = FagsakStatus.OPPRETTET) + + val fagsakerMedSøkerSomDeltaker = fagsakService.finnAlleFagsakerHvorAktørErSøkerEllerMottarLøpendeOrdinær(søker.aktør) + + assertEquals(1, fagsakerMedSøkerSomDeltaker.size) + assertEquals(normalFagsakForSøker, fagsakerMedSøkerSomDeltaker.single()) + } + + @Test + fun `Skal hente løpende institusjonsfagsak for søker`() { + val barn = lagPerson(type = PersonType.BARN) + + val normalFagsakForSøker = opprettFagsakForPersonMedStatus(personIdent = barn.aktør.aktivFødselsnummer(), fagsakStatus = FagsakStatus.LØPENDE, fagsakType = FagsakType.INSTITUSJON) + opprettFagsakForPersonMedStatus(personIdent = randomFnr(), fagsakStatus = FagsakStatus.LØPENDE) + opprettFagsakForPersonMedStatus(personIdent = randomFnr(), fagsakStatus = FagsakStatus.AVSLUTTET) + opprettFagsakForPersonMedStatus(personIdent = randomFnr(), fagsakStatus = FagsakStatus.OPPRETTET) + + val fagsakerMedSøkerSomDeltaker = fagsakService.finnAlleFagsakerHvorAktørErSøkerEllerMottarLøpendeOrdinær(barn.aktør) + + assertEquals(1, fagsakerMedSøkerSomDeltaker.size) + assertEquals(normalFagsakForSøker, fagsakerMedSøkerSomDeltaker.single()) + } + + @Test + fun `Skal hente fagsak hvor barn har løpende andel`() { + val barn = lagPerson(type = PersonType.BARN) + personidentService.hentOgLagreAktørIder(listOf(barn.aktør.aktivFødselsnummer()), lagre = true) + + val fagsak = opprettFagsakForPersonMedStatus(randomFnr(), FagsakStatus.LØPENDE) + opprettFagsakForPersonMedStatus(personIdent = randomFnr(), fagsakStatus = FagsakStatus.LØPENDE) // Lager en ekstre fagsak for å teste at denne ikke kommer med + + val perioderTilAndeler = listOf( + PeriodeForAktør( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().minusMonths(3), + aktør = barn.aktør, + ), + PeriodeForAktør( + fom = YearMonth.now().minusMonths(2), + tom = YearMonth.now().plusMonths(6), + aktør = barn.aktør, + ), + ) + + opprettAndelerOgBehandling(fagsak = fagsak, barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), perioderTilAndeler = perioderTilAndeler) + + val fagsakerMedSøkerSomDeltaker = fagsakService.finnAlleFagsakerHvorAktørErSøkerEllerMottarLøpendeOrdinær(barn.aktør) + + assertEquals(1, fagsakerMedSøkerSomDeltaker.size) + assertEquals(fagsak, fagsakerMedSøkerSomDeltaker.single()) + } + + @Test + fun `Skal ikke hente fagsak hvor barn har andel som ikke er løpende`() { + val barn = lagPerson(type = PersonType.BARN) + personidentService.hentOgLagreAktørIder(listOf(barn.aktør.aktivFødselsnummer()), lagre = true) + + val fagsak = opprettFagsakForPersonMedStatus(randomFnr(), FagsakStatus.LØPENDE) + opprettFagsakForPersonMedStatus(personIdent = randomFnr(), fagsakStatus = FagsakStatus.LØPENDE) // Lager en ekstre fagsak for å teste at denne ikke kommer med + + val perioderTilAndeler = listOf( + PeriodeForAktør( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().minusMonths(3), + aktør = barn.aktør, + ), + ) + opprettAndelerOgBehandling(fagsak = fagsak, barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), perioderTilAndeler = perioderTilAndeler) + + val fagsakerMedSøkerSomDeltaker = fagsakService.finnAlleFagsakerHvorAktørErSøkerEllerMottarLøpendeOrdinær(barn.aktør) + + assertEquals(0, fagsakerMedSøkerSomDeltaker.size) + } + + @Test + fun `Skal hente to fagsaker hvis aktør er søker i en sak og blir mottatt barnetrygd for i en annen`() { + val person = lagPerson(type = PersonType.BARN) + personidentService.hentOgLagreAktørIder(listOf(person.aktør.aktivFødselsnummer()), lagre = true) + + val fagsakHvorPersonErBarn = opprettFagsakForPersonMedStatus(randomFnr(), FagsakStatus.LØPENDE) + val fagsakHvorPersonErSøker = opprettFagsakForPersonMedStatus(person.aktør.aktivFødselsnummer(), FagsakStatus.LØPENDE) + opprettFagsakForPersonMedStatus(personIdent = randomFnr(), fagsakStatus = FagsakStatus.LØPENDE) // Lager en ekstre fagsak for å teste at denne ikke kommer med + + val perioderTilAndeler = listOf( + PeriodeForAktør( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().minusMonths(3), + aktør = person.aktør, + ), + PeriodeForAktør( + fom = YearMonth.now().minusMonths(2), + tom = YearMonth.now().plusMonths(6), + aktør = person.aktør, + ), + ) + + opprettAndelerOgBehandling(fagsak = fagsakHvorPersonErBarn, barnasIdenter = listOf(person.aktør.aktivFødselsnummer()), perioderTilAndeler = perioderTilAndeler) + + val fagsakerMedSøkerSomDeltaker = fagsakService.finnAlleFagsakerHvorAktørErSøkerEllerMottarLøpendeOrdinær(person.aktør) + + assertEquals(2, fagsakerMedSøkerSomDeltaker.size) + assertEquals(fagsakHvorPersonErSøker, fagsakerMedSøkerSomDeltaker.first()) + assertEquals(fagsakHvorPersonErBarn, fagsakerMedSøkerSomDeltaker.last()) + } + + @Test + fun `Skal ikke hente fagsak hvis barn kun har løpende andeler i en gammel behandling som senere er opphørt`() { + val person = lagPerson(type = PersonType.BARN) + personidentService.hentOgLagreAktørIder(listOf(person.aktør.aktivFødselsnummer()), lagre = true) + + val fagsakHvorPersonErBarn = opprettFagsakForPersonMedStatus(randomFnr(), FagsakStatus.LØPENDE) + opprettFagsakForPersonMedStatus(personIdent = randomFnr(), fagsakStatus = FagsakStatus.LØPENDE) // Lager en ekstre fagsak for å teste at denne ikke kommer med + + val gamlePerioder = listOf( + PeriodeForAktør( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().minusMonths(3), + aktør = person.aktør, + ), + PeriodeForAktør( + fom = YearMonth.now().minusMonths(2), + tom = YearMonth.now().plusMonths(6), + aktør = person.aktør, + ), + ) + + val nyePerioder = listOf( + PeriodeForAktør( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().minusMonths(3), + aktør = person.aktør, + ), + ) + + opprettAndelerOgBehandling(fagsak = fagsakHvorPersonErBarn, barnasIdenter = listOf(person.aktør.aktivFødselsnummer()), perioderTilAndeler = gamlePerioder) // gammel behandling + opprettAndelerOgBehandling(fagsak = fagsakHvorPersonErBarn, barnasIdenter = listOf(person.aktør.aktivFødselsnummer()), perioderTilAndeler = nyePerioder) // ny behandling + + val fagsakerMedSøkerSomDeltaker = fagsakService.finnAlleFagsakerHvorAktørErSøkerEllerMottarLøpendeOrdinær(person.aktør) + + assertEquals(0, fagsakerMedSøkerSomDeltaker.size) + } + + @Test + fun `Skal returnere fagsak hvor person mottar løpende utvidet`() { + val person = lagPerson(type = PersonType.SØKER) + val barn = lagPerson(type = PersonType.BARN) + personidentService.hentOgLagreAktørIder(listOf(person.aktør.aktivFødselsnummer(), barn.aktør.aktivFødselsnummer()), lagre = true) + + val fagsakHvorPersonErSøker = opprettFagsakForPersonMedStatus(randomFnr(), FagsakStatus.LØPENDE) + + val perioder = listOf( + PeriodeForAktør( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().minusMonths(3), + aktør = person.aktør, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ), + PeriodeForAktør( + fom = YearMonth.now().minusMonths(2), + tom = YearMonth.now().plusMonths(6), + aktør = person.aktør, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ), + PeriodeForAktør( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(6), + aktør = barn.aktør, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + ) + + opprettAndelerOgBehandling(fagsak = fagsakHvorPersonErSøker, barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), perioderTilAndeler = perioder) + + val fagsakerMedPersonSomFårUtvidetEllerOrdinær = fagsakService.finnAlleFagsakerHvorAktørHarLøpendeYtelseAvType(aktør = person.aktør, ytelseTyper = listOf(YtelseType.ORDINÆR_BARNETRYGD, YtelseType.UTVIDET_BARNETRYGD)) + + assertEquals(1, fagsakerMedPersonSomFårUtvidetEllerOrdinær.size) + assertEquals(fagsakHvorPersonErSøker, fagsakerMedPersonSomFårUtvidetEllerOrdinær.single()) + } + + @Test + fun `Skal returnere ikke fagsak hvor person mottok utvidet som ikke er løpende lenger`() { + val person = lagPerson(type = PersonType.SØKER) + val barn = lagPerson(type = PersonType.BARN) + personidentService.hentOgLagreAktørIder(listOf(person.aktør.aktivFødselsnummer(), barn.aktør.aktivFødselsnummer()), lagre = true) + + val fagsakHvorPersonErSøker = opprettFagsakForPersonMedStatus(randomFnr(), FagsakStatus.LØPENDE) + + val perioder = listOf( + PeriodeForAktør( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().minusMonths(3), + aktør = person.aktør, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ), + PeriodeForAktør( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().plusMonths(6), + aktør = barn.aktør, + ytelseType = YtelseType.ORDINÆR_BARNETRYGD, + ), + ) + + opprettAndelerOgBehandling(fagsak = fagsakHvorPersonErSøker, barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), perioderTilAndeler = perioder) + + val fagsakerMedPersonSomFårUtvidetEllerOrdinær = fagsakService.finnAlleFagsakerHvorAktørHarLøpendeYtelseAvType(aktør = person.aktør, ytelseTyper = listOf(YtelseType.ORDINÆR_BARNETRYGD, YtelseType.UTVIDET_BARNETRYGD)) + + assertEquals(0, fagsakerMedPersonSomFårUtvidetEllerOrdinær.size) + } + + @Test + fun `Skal kun hente én fagsak hvis aktør er søker i en sak (uten løpende utvidet) og blir mottatt barnetrygd for i en annen`() { + val person = lagPerson(type = PersonType.BARN) + personidentService.hentOgLagreAktørIder(listOf(person.aktør.aktivFødselsnummer()), lagre = true) + + val fagsakHvorPersonErBarn = opprettFagsakForPersonMedStatus(randomFnr(), FagsakStatus.LØPENDE) + opprettFagsakForPersonMedStatus(person.aktør.aktivFødselsnummer(), FagsakStatus.LØPENDE) // Fagsak hvor person er søker, men ikke har noen løpende utvidet-andeler + opprettFagsakForPersonMedStatus(personIdent = randomFnr(), fagsakStatus = FagsakStatus.LØPENDE) // Lager en ekstre fagsak for å teste at denne ikke kommer med + + val perioderTilAndeler = listOf( + PeriodeForAktør( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().minusMonths(3), + aktør = person.aktør, + ), + PeriodeForAktør( + fom = YearMonth.now().minusMonths(2), + tom = YearMonth.now().plusMonths(6), + aktør = person.aktør, + ), + ) + + opprettAndelerOgBehandling(fagsak = fagsakHvorPersonErBarn, barnasIdenter = listOf(person.aktør.aktivFødselsnummer()), perioderTilAndeler = perioderTilAndeler) + + val fagsakerMedSøkerSomDeltaker = fagsakService.finnAlleFagsakerHvorAktørHarLøpendeYtelseAvType(aktør = person.aktør, ytelseTyper = listOf(YtelseType.ORDINÆR_BARNETRYGD, YtelseType.UTVIDET_BARNETRYGD)) + + assertEquals(1, fagsakerMedSøkerSomDeltaker.size) + assertEquals(fagsakHvorPersonErBarn, fagsakerMedSøkerSomDeltaker.single()) + } + + @Test + fun `Skal hente to fagsaker hvor person mottar løpende utvidet i en behandling og blir mottatt løpende ordinær for i en annen`() { + val person = lagPerson(type = PersonType.BARN) + val barn = lagPerson(type = PersonType.BARN) + personidentService.hentOgLagreAktørIder(listOf(person.aktør.aktivFødselsnummer(), barn.aktør.aktivFødselsnummer()), lagre = true) + + val fagsakHvorPersonErBarn = opprettFagsakForPersonMedStatus(randomFnr(), FagsakStatus.LØPENDE) + val fagsakHvorPersonErSøker = opprettFagsakForPersonMedStatus(person.aktør.aktivFødselsnummer(), FagsakStatus.LØPENDE) + opprettFagsakForPersonMedStatus(personIdent = randomFnr(), fagsakStatus = FagsakStatus.LØPENDE) // Lager en ekstre fagsak for å teste at denne ikke kommer med + + val perioderTilFagsakBarn = listOf( + PeriodeForAktør( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().minusMonths(3), + aktør = person.aktør, + ), + PeriodeForAktør( + fom = YearMonth.now().minusMonths(2), + tom = YearMonth.now().plusMonths(6), + aktør = person.aktør, + ), + ) + + val perioderTilFagsakSøker = listOf( + PeriodeForAktør( + fom = YearMonth.now().minusMonths(10), + tom = YearMonth.now().minusMonths(3), + aktør = person.aktør, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ), + PeriodeForAktør( + fom = YearMonth.now().minusMonths(2), + tom = YearMonth.now().plusMonths(6), + aktør = person.aktør, + ytelseType = YtelseType.UTVIDET_BARNETRYGD, + ), + ) + + opprettAndelerOgBehandling(fagsak = fagsakHvorPersonErBarn, barnasIdenter = listOf(person.aktør.aktivFødselsnummer()), perioderTilAndeler = perioderTilFagsakBarn) + opprettAndelerOgBehandling(fagsak = fagsakHvorPersonErSøker, barnasIdenter = listOf(barn.aktør.aktivFødselsnummer()), perioderTilAndeler = perioderTilFagsakSøker) + + val fagsakerMedSøkerSomDeltaker = fagsakService.finnAlleFagsakerHvorAktørHarLøpendeYtelseAvType(aktør = person.aktør, ytelseTyper = listOf(YtelseType.ORDINÆR_BARNETRYGD, YtelseType.UTVIDET_BARNETRYGD)) + + assertEquals(2, fagsakerMedSøkerSomDeltaker.size) + assertEquals(fagsakHvorPersonErBarn, fagsakerMedSøkerSomDeltaker.first()) + assertEquals(fagsakHvorPersonErSøker, fagsakerMedSøkerSomDeltaker.last()) + } + + private data class PeriodeForAktør( + val fom: YearMonth, + val tom: YearMonth, + val aktør: Aktør, + val ytelseType: YtelseType = YtelseType.ORDINÆR_BARNETRYGD, + ) + + private fun opprettFagsakForPersonMedStatus(personIdent: String, fagsakStatus: FagsakStatus, fagsakType: FagsakType = FagsakType.NORMAL): Fagsak { + val institusjon = InstitusjonInfo(orgNummer = "123456789", tssEksternId = "testid") + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fødselsnummer = personIdent, fagsakType = fagsakType, institusjon = if (fagsakType == FagsakType.INSTITUSJON) institusjon else null) + return fagsakService.oppdaterStatus(fagsak, fagsakStatus) + } + + private fun opprettAndelerOgBehandling(fagsak: Fagsak, barnasIdenter: List, perioderTilAndeler: List) { + val nyBehandling = NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + søkersIdent = fagsak.aktør.aktivFødselsnummer(), + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + navIdent = randomFnr(), + barnasIdenter = barnasIdenter, + søknadMottattDato = LocalDate.now().minusMonths(1), + fagsakId = fagsak.id, + ) + val behandling = behandlingService.opprettBehandling(nyBehandling = nyBehandling) + val tilkjentYtelse = TilkjentYtelse(behandling = behandling, endretDato = LocalDate.now(), opprettetDato = LocalDate.now()) + val andelerTilkjentYtelse = perioderTilAndeler.map { + lagAndelTilkjentYtelse( + fom = it.fom, + tom = it.tom, + aktør = it.aktør, + behandling = behandling, + tilkjentYtelse = tilkjentYtelse, + ytelseType = it.ytelseType, + ) + } + + tilkjentYtelse.andelerTilkjentYtelse.addAll(andelerTilkjentYtelse) + tilkjentYtelseRepository.save(tilkjentYtelse) + + behandlingService.oppdaterStatusPåBehandling(behandlingId = behandling.id, status = BehandlingStatus.AVSLUTTET) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/RestFagsakTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/RestFagsakTest.kt new file mode 100644 index 000000000..a455f1aef --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/RestFagsakTest.kt @@ -0,0 +1,111 @@ +package no.nav.familie.ba.sak.kjerne.fagsak + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import no.nav.familie.ba.sak.common.kjørStegprosessForFGB +import no.nav.familie.ba.sak.common.kjørStegprosessForRevurderingÅrligKontroll +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.kjerne.beregning.SatsTidspunkt +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.tilbakekreving.TilbakekrevingService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class RestFagsakTest( + @Autowired + private val stegService: StegService, + + @Autowired + private val vedtakService: VedtakService, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val vilkårsvurderingService: VilkårsvurderingService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val tilbakekrevingService: TilbakekrevingService, + + @Autowired + private val vedtaksperiodeService: VedtaksperiodeService, + + @Autowired + private val brevmalService: BrevmalService, + + @Autowired + private val featureToggleService: FeatureToggleService, + +) : AbstractSpringIntegrationTest() { + + @BeforeAll + fun init() { + databaseCleanupService.truncate() + } + + @BeforeEach + fun førHverTest() { + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2022, 12, 31) + } + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + @Test + fun `Skal sjekke at gjeldende utbetalingsperioder kommer med i restfagsak`() { + val søkerFnr = randomFnr() + val barnFnr = ClientMocks.barnFnr[0] + + val førstegangsbehandling = kjørStegprosessForFGB( + tilSteg = StegType.BEHANDLING_AVSLUTTET, + søkerFnr = søkerFnr, + barnasIdenter = listOf(barnFnr), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService, + brevmalService = brevmalService, + ) + + kjørStegprosessForRevurderingÅrligKontroll( + tilSteg = StegType.BEHANDLINGSRESULTAT, + søkerFnr = søkerFnr, + barnasIdenter = listOf(barnFnr), + vedtakService = vedtakService, + stegService = stegService, + fagsakId = førstegangsbehandling.fagsak.id, + brevmalService = brevmalService, + ) + + val restfagsak = fagsakService.hentRestFagsak(fagsakId = førstegangsbehandling.fagsak.id) + + assertEquals(1, restfagsak.data?.gjeldendeUtbetalingsperioder?.size) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/S\303\270kFagsakNegativeTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/S\303\270kFagsakNegativeTest.kt" new file mode 100644 index 000000000..76afb401b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/fagsak/S\303\270kFagsakNegativeTest.kt" @@ -0,0 +1,63 @@ +package no.nav.familie.ba.sak.kjerne.fagsak + +import no.nav.familie.ba.sak.common.DbContainerInitializer +import no.nav.familie.ba.sak.ekstern.restDomene.RestFagsakDeltager +import no.nav.familie.ba.sak.ekstern.restDomene.RestSøkParam +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonException +import no.nav.familie.kontrakter.felles.Ressurs +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.HttpStatus +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration + +@SpringBootTest +@ActiveProfiles( + "postgres", + "integrasjonstest", + "mock-oauth", + "mock-pdl-test-søk", + "mock-ident-client", + "mock-infotrygd-barnetrygd", + "mock-brev-klient", +) +@ContextConfiguration(initializers = [DbContainerInitializer::class]) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Tag("integration") +class SøkFagsakNegativeTest { + + @Autowired + lateinit var fagsakService: FagsakService + + @Autowired + lateinit var fagsakController: FagsakController + + @Test + fun `test å søke fagsak deltager med ugyldig fnr`() { + val feilId = "41235678910" + assertThrows { + fagsakService.hentFagsakDeltager(feilId) + } + } + + @Test + fun `test generer riktig ressur ved feil`() { + val ukjentId = "43125678910" + val feilId = "41235678910" + + val resEntity1 = fagsakController.søkFagsak(RestSøkParam(ukjentId)) + assertThat(HttpStatus.OK).isEqualTo(resEntity1.statusCode) + val ress = resEntity1.body as Ressurs> + assertThat(Ressurs.Status.SUKSESS).isEqualTo(ress.status) + assertThat(ress.data).isEqualTo(emptyList()) + + assertThrows { + fagsakController.søkFagsak(RestSøkParam(feilId)) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersongrunnlagIntegrationTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersongrunnlagIntegrationTest.kt new file mode 100644 index 000000000..bc4a65aaf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersongrunnlagIntegrationTest.kt @@ -0,0 +1,281 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import io.mockk.every +import io.mockk.verify +import no.nav.familie.ba.sak.common.nyOrdinærBehandling +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.IntegrasjonClient +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.DødsfallData +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlKontaktinformasjonForDødsbo +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlKontaktinformasjonForDødsboAdresse +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PersonInfo +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRequest +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.defaultBostedsadresseHistorikk +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.SIVILSTAND +import no.nav.familie.kontrakter.felles.personopplysning.Sivilstand +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class PersongrunnlagIntegrationTest( + @Autowired + private val mockIntegrasjonClient: IntegrasjonClient, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val mockPersonopplysningerService: PersonopplysningerService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val behandlingService: BehandlingService, +) : AbstractSpringIntegrationTest() { + + @Test + fun `Skal lagre dødsfall på person når person er død`() { + val søkerAktør = personidentService.hentOgLagreAktør(randomFnr(), true) + val barn1Aktør = personidentService.hentOgLagreAktør(randomFnr(), true) + + val dødsdato = "2020-04-04" + val adresselinje1 = "Gatenavn 1" + val poststedsnavn = "Oslo" + val postnummer = "1234" + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(søkerAktør) } returns PersonInfo( + fødselsdato = LocalDate.of(1990, 1, 1), + adressebeskyttelseGradering = null, + navn = "Mor", + kjønn = Kjønn.KVINNE, + forelderBarnRelasjon = emptySet(), + bostedsadresser = mutableListOf(Bostedsadresse()) + defaultBostedsadresseHistorikk, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UOPPGITT)), + dødsfall = DødsfallData(erDød = true, dødsdato = dødsdato), + kontaktinformasjonForDoedsbo = PdlKontaktinformasjonForDødsbo( + adresse = PdlKontaktinformasjonForDødsboAdresse( + adresselinje1 = adresselinje1, + poststedsnavn = poststedsnavn, + postnummer = postnummer, + ), + ), + ) + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(barn1Aktør) } returns PersonInfo( + fødselsdato = LocalDate.of(2009, 1, 1), + adressebeskyttelseGradering = null, + navn = "Gutt", + kjønn = Kjønn.MANN, + forelderBarnRelasjon = emptySet(), + bostedsadresser = mutableListOf(Bostedsadresse()) + defaultBostedsadresseHistorikk, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UOPPGITT)), + dødsfall = null, + kontaktinformasjonForDoedsbo = null, + ) + + val fagsak = fagsakService.hentEllerOpprettFagsak(FagsakRequest(personIdent = søkerAktør.aktivFødselsnummer())) + val behandling = behandlingService.opprettBehandling( + nyOrdinærBehandling( + søkersIdent = søkerAktør.aktivFødselsnummer(), + fagsakId = fagsak.data!!.id, + ), + ) + + val personopplysningGrunnlag = persongrunnlagService.hentOgLagreSøkerOgBarnINyttGrunnlag( + aktør = søkerAktør, + barnFraInneværendeBehandling = listOf(barn1Aktør), + behandling = behandling, + målform = Målform.NB, + ) + + Assertions.assertTrue(personopplysningGrunnlag.søker.erDød()) + assertEquals(LocalDate.parse(dødsdato), personopplysningGrunnlag.søker.dødsfall?.dødsfallDato) + assertEquals(adresselinje1, personopplysningGrunnlag.søker.dødsfall?.dødsfallAdresse) + assertEquals(postnummer, personopplysningGrunnlag.søker.dødsfall?.dødsfallPostnummer) + assertEquals(poststedsnavn, personopplysningGrunnlag.søker.dødsfall?.dødsfallPoststed) + + Assertions.assertFalse(personopplysningGrunnlag.barna.single().erDød()) + assertEquals(null, personopplysningGrunnlag.barna.single().dødsfall) + } + + @Test + fun `Skal hente arbeidsforhold for mor når hun er EØS-borger og det er en automatisk behandling`() { + val morAktør = personidentService.hentOgLagreAktør(randomFnr(), true) + val barn1Aktør = personidentService.hentOgLagreAktør(randomFnr(), true) + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(morAktør) } returns PersonInfo( + fødselsdato = LocalDate.of(1990, 1, 1), + adressebeskyttelseGradering = null, + navn = "Mor", + kjønn = Kjønn.KVINNE, + forelderBarnRelasjon = emptySet(), + bostedsadresser = mutableListOf(Bostedsadresse()) + defaultBostedsadresseHistorikk, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UOPPGITT)), + statsborgerskap = listOf( + Statsborgerskap( + land = "POL", + gyldigFraOgMed = null, + gyldigTilOgMed = null, + bekreftelsesdato = null, + ), + ), + dødsfall = null, + kontaktinformasjonForDoedsbo = null, + ) + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(barn1Aktør) } returns PersonInfo( + fødselsdato = LocalDate.of(2009, 1, 1), + adressebeskyttelseGradering = null, + navn = "Gutt", + kjønn = Kjønn.MANN, + forelderBarnRelasjon = emptySet(), + bostedsadresser = mutableListOf(Bostedsadresse()) + defaultBostedsadresseHistorikk, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UOPPGITT)), + dødsfall = null, + kontaktinformasjonForDoedsbo = null, + ) + val fagsak = fagsakService.hentEllerOpprettFagsak(FagsakRequest(personIdent = morAktør.aktivFødselsnummer())) + val behandling = behandlingService.opprettBehandling( + NyBehandling( + skalBehandlesAutomatisk = true, + søkersIdent = morAktør.aktivFødselsnummer(), + behandlingÅrsak = BehandlingÅrsak.FØDSELSHENDELSE, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + kategori = BehandlingKategori.NASJONAL, // alltid NASJONAL for fødselshendelse + underkategori = BehandlingUnderkategori.ORDINÆR, + fagsakId = fagsak.data!!.id, + ), + ) + + persongrunnlagService.hentOgLagreSøkerOgBarnINyttGrunnlag( + aktør = morAktør, + barnFraInneværendeBehandling = listOf(barn1Aktør), + behandling = behandling, + målform = Målform.NB, + ) + + verify(exactly = 1) { mockIntegrasjonClient.hentArbeidsforhold(any(), any()) } + } + + @Test + fun `Skal ikke hente arbeidsforhold for mor når det er en automatisk behandling, men hun er norsk statsborger`() { + val morAktør = personidentService.hentOgLagreAktør(randomFnr(), true) + val barn1Aktør = personidentService.hentOgLagreAktør(randomFnr(), true) + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(morAktør) } returns PersonInfo( + fødselsdato = LocalDate.of(1990, 1, 1), + adressebeskyttelseGradering = null, + navn = "Mor", + kjønn = Kjønn.KVINNE, + forelderBarnRelasjon = emptySet(), + bostedsadresser = mutableListOf(Bostedsadresse()) + defaultBostedsadresseHistorikk, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UOPPGITT)), + statsborgerskap = listOf( + Statsborgerskap( + land = "NOR", + gyldigFraOgMed = null, + gyldigTilOgMed = null, + bekreftelsesdato = null, + ), + ), + dødsfall = null, + kontaktinformasjonForDoedsbo = null, + ) + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(barn1Aktør) } returns PersonInfo( + fødselsdato = LocalDate.of(2009, 1, 1), + adressebeskyttelseGradering = null, + navn = "Gutt", + kjønn = Kjønn.MANN, + forelderBarnRelasjon = emptySet(), + bostedsadresser = mutableListOf(Bostedsadresse()) + defaultBostedsadresseHistorikk, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UOPPGITT)), + dødsfall = null, + kontaktinformasjonForDoedsbo = null, + ) + val fagsak = fagsakService.hentEllerOpprettFagsak(FagsakRequest(personIdent = morAktør.aktivFødselsnummer())) + val behandling = behandlingService.opprettBehandling( + NyBehandling( + skalBehandlesAutomatisk = true, + søkersIdent = morAktør.aktivFødselsnummer(), + behandlingÅrsak = BehandlingÅrsak.FØDSELSHENDELSE, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + fagsakId = fagsak.data!!.id, + ), + ) + + persongrunnlagService.hentOgLagreSøkerOgBarnINyttGrunnlag( + aktør = morAktør, + barnFraInneværendeBehandling = listOf(barn1Aktør), + behandling = behandling, + målform = Målform.NB, + ) + + verify(exactly = 0) { mockIntegrasjonClient.hentArbeidsforhold(any(), any()) } + } + + @Test + fun `Skal filtrere ut bostedsadresse uten verdier når de mappes inn`() { + val søkerAktør = personidentService.hentOgLagreAktør(randomFnr(), true) + val barn1Aktør = personidentService.hentOgLagreAktør(randomFnr(), true) + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(søkerAktør) } returns PersonInfo( + fødselsdato = LocalDate.of(1990, 1, 1), + adressebeskyttelseGradering = null, + navn = "Mor", + kjønn = Kjønn.KVINNE, + forelderBarnRelasjon = emptySet(), + bostedsadresser = mutableListOf(Bostedsadresse()) + defaultBostedsadresseHistorikk, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UOPPGITT)), + ) + + every { mockPersonopplysningerService.hentPersoninfoMedRelasjonerOgRegisterinformasjon(barn1Aktør) } returns PersonInfo( + fødselsdato = LocalDate.of(2009, 1, 1), + adressebeskyttelseGradering = null, + navn = "Gutt", + kjønn = Kjønn.MANN, + forelderBarnRelasjon = emptySet(), + bostedsadresser = mutableListOf(Bostedsadresse()) + defaultBostedsadresseHistorikk, + sivilstander = listOf(Sivilstand(type = SIVILSTAND.UOPPGITT)), + ) + + val fagsak = fagsakService.hentEllerOpprettFagsak(FagsakRequest(personIdent = søkerAktør.aktivFødselsnummer())) + val behandling = behandlingService.opprettBehandling( + nyOrdinærBehandling( + søkersIdent = søkerAktør.aktivFødselsnummer(), + fagsakId = fagsak.data!!.id, + ), + ) + + val personopplysningGrunnlag = persongrunnlagService.hentOgLagreSøkerOgBarnINyttGrunnlag( + søkerAktør, + listOf(barn1Aktør), + behandling, + Målform.NB, + ) + + personopplysningGrunnlag.personer.forEach { + assertEquals(defaultBostedsadresseHistorikk.size, it.bostedsadresser.size) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonopplysningGrunnlagForNyBehandlingServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonopplysningGrunnlagForNyBehandlingServiceTest.kt new file mode 100644 index 000000000..a8820f780 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/personopplysninger/PersonopplysningGrunnlagForNyBehandlingServiceTest.kt @@ -0,0 +1,302 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger + +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelse +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.PersonopplysningGrunnlagForNyBehandlingService +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.YearMonth +import kotlin.reflect.full.declaredMemberProperties + +class PersonopplysningGrunnlagForNyBehandlingServiceTest( + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @Autowired + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + + @Autowired + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + + @Autowired + private val personopplysningGrunnlagForNyBehandlingService: PersonopplysningGrunnlagForNyBehandlingService, +) : AbstractSpringIntegrationTest() { + + @Test + fun `opprettKopiEllerNyttPersonopplysningGrunnlag - skal opprette nytt PersonopplysningGrunnlag som kopi av personopplysningsgrunnlag fra forrige behandling ved satsendring`() { + val morId = randomFnr() + val barnId = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(morId) + val førsteBehandling = + behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + personopplysningGrunnlagForNyBehandlingService.opprettKopiEllerNyttPersonopplysningGrunnlag( + førsteBehandling, + null, + morId, + listOf(barnId), + ) + + val grunnlagFraFørsteBehandling = + personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = førsteBehandling.id) + + // Legger til andel tilkjent ytelse på barn + tilkjentYtelseRepository.saveAndFlush( + lagInitiellTilkjentYtelse(førsteBehandling, "").also { + it.andelerTilkjentYtelse.addAll( + listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 5), + tom = YearMonth.of(2025, 5), + person = grunnlagFraFørsteBehandling!!.personer.first { person -> person.aktør.aktivFødselsnummer() == barnId }, + behandling = førsteBehandling, + tilkjentYtelse = it, + ), + ), + ) + }, + ) + + avsluttOgLagreBehandling(førsteBehandling) + + assertThat(grunnlagFraFørsteBehandling!!.personer.size).isEqualTo(2) + assertThat(grunnlagFraFørsteBehandling.personer.any { it.aktør.aktivFødselsnummer() == morId }) + assertThat(grunnlagFraFørsteBehandling.personer.any { it.aktør.aktivFødselsnummer() == barnId }) + + val satsendring = lagBehandling( + fagsak, + behandlingType = BehandlingType.REVURDERING, + årsak = BehandlingÅrsak.SATSENDRING, + ) + + val satsendringBehandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + satsendring, + ) + + personopplysningGrunnlagForNyBehandlingService.opprettKopiEllerNyttPersonopplysningGrunnlag( + satsendringBehandling, + førsteBehandling, + morId, + listOf(barnId), + ) + + val grunnlagFraSatsendringBehandling = + personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = satsendringBehandling.id) + + assertThat(grunnlagFraSatsendringBehandling!!.personer.size).isEqualTo(2) + assertThat(grunnlagFraSatsendringBehandling.personer.any { it.aktør.aktivFødselsnummer() == morId }) + assertThat(grunnlagFraSatsendringBehandling.personer.any { it.aktør.aktivFødselsnummer() == barnId }) + assertThat(grunnlagFraSatsendringBehandling.id) + .isNotEqualTo(grunnlagFraFørsteBehandling.id) + assertThat(grunnlagFraSatsendringBehandling.behandlingId).isNotEqualTo(grunnlagFraFørsteBehandling.behandlingId) + validerAtPersonerIGrunnlagErLike(grunnlagFraFørsteBehandling, grunnlagFraSatsendringBehandling, false) + } + + @Test + fun `opprettKopiEllerNyttPersonopplysningGrunnlag - skal opprette nytt PersonopplysningGrunnlag som kopi av personopplysningsgrunnlag fra forrige behandling med barn som hadde andeler tilkjent ytelse`() { + val morId = randomFnr() + val barn1Id = randomFnr() + val barn2Id = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(morId) + val førsteBehandling = + behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + personopplysningGrunnlagForNyBehandlingService.opprettKopiEllerNyttPersonopplysningGrunnlag( + førsteBehandling, + null, + morId, + listOf(barn1Id, barn2Id), + ) + + val grunnlagFraFørsteBehandling = + personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = førsteBehandling.id) + + // Legger til andel tilkjent ytelse på barn + tilkjentYtelseRepository.saveAndFlush( + lagInitiellTilkjentYtelse(førsteBehandling, "").also { + it.andelerTilkjentYtelse.addAll( + listOf( + lagAndelTilkjentYtelse( + fom = YearMonth.of(2023, 5), + tom = YearMonth.of(2025, 5), + person = grunnlagFraFørsteBehandling!!.personer.first { person -> person.aktør.aktivFødselsnummer() == barn2Id }, + behandling = førsteBehandling, + tilkjentYtelse = it, + ), + ), + ) + }, + ) + + avsluttOgLagreBehandling(førsteBehandling) + + assertThat(grunnlagFraFørsteBehandling!!.personer.size).isEqualTo(3) + assertThat(grunnlagFraFørsteBehandling.personer.any { it.aktør.aktivFødselsnummer() == morId }).isTrue + assertThat(grunnlagFraFørsteBehandling.personer.any { it.aktør.aktivFødselsnummer() == barn1Id }).isTrue + assertThat(grunnlagFraFørsteBehandling.personer.any { it.aktør.aktivFødselsnummer() == barn2Id }).isTrue + + val satsendring = lagBehandling( + fagsak, + behandlingType = BehandlingType.REVURDERING, + årsak = BehandlingÅrsak.SATSENDRING, + ) + + val satsendringBehandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + satsendring, + ) + + personopplysningGrunnlagForNyBehandlingService.opprettKopiEllerNyttPersonopplysningGrunnlag( + satsendringBehandling, + førsteBehandling, + morId, + listOf(barn1Id, barn2Id), + ) + + val grunnlagFraSatsendringBehandling = + personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = satsendringBehandling.id) + + assertThat(grunnlagFraSatsendringBehandling!!.personer.size).isEqualTo(2) + assertThat(grunnlagFraSatsendringBehandling.personer.any { it.aktør.aktivFødselsnummer() == morId }).isTrue + assertThat(grunnlagFraSatsendringBehandling.personer.any { it.aktør.aktivFødselsnummer() == barn2Id }).isTrue + assertThat(grunnlagFraSatsendringBehandling.personer.any { it.aktør.aktivFødselsnummer() == barn1Id }).isFalse + + assertThat(grunnlagFraSatsendringBehandling.id) + .isNotEqualTo(grunnlagFraFørsteBehandling.id) + assertThat(grunnlagFraSatsendringBehandling.behandlingId).isNotEqualTo(grunnlagFraFørsteBehandling.behandlingId) + + grunnlagFraFørsteBehandling.personer.removeAll { it.aktør.aktivFødselsnummer() == barn1Id } + validerAtPersonerIGrunnlagErLike(grunnlagFraFørsteBehandling, grunnlagFraSatsendringBehandling, false) + } + + private fun avsluttOgLagreBehandling(behandling: Behandling) { + behandling.status = BehandlingStatus.AVSLUTTET + behandling.leggTilBehandlingStegTilstand(StegType.BEHANDLING_AVSLUTTET) + behandlingHentOgPersisterService.lagreEllerOppdater(behandling) + } + + companion object { + fun validerAtPersonerIGrunnlagErLike( + personopplysningGrunnlagFørsteBehandling: PersonopplysningGrunnlag, + personopplysningGrunnlagSatsendringBehandling: PersonopplysningGrunnlag, + erEnhetstest: Boolean, + ) { + personopplysningGrunnlagFørsteBehandling.personer.fold(mutableListOf>()) { acc, person -> + acc.add( + Pair( + person, + personopplysningGrunnlagSatsendringBehandling.personer.first { it.aktør.aktivFødselsnummer() == person.aktør.aktivFødselsnummer() }, + ), + ) + acc + }.forEach { + validerAtSubEntiteterAvPersonErLike( + it.first.bostedsadresser, + it.second.bostedsadresser, + it.first.bostedsadresser.firstOrNull()?.person, + it.second.bostedsadresser.firstOrNull()?.person, + erEnhetstest, + ) + validerAtSubEntiteterAvPersonErLike( + it.first.sivilstander, + it.second.sivilstander, + it.first.sivilstander.firstOrNull()?.person, + it.second.sivilstander.firstOrNull()?.person, + ) + + assertThat(it.first.sivilstander).containsExactlyInAnyOrderElementsOf(it.second.sivilstander) + + validerAtSubEntiteterAvPersonErLike( + it.first.statsborgerskap, + it.second.statsborgerskap, + it.first.statsborgerskap.firstOrNull()?.person, + it.second.statsborgerskap.firstOrNull()?.person, + ) + + validerAtSubEntiteterAvPersonErLike( + it.first.opphold, + it.second.opphold, + it.first.opphold.firstOrNull()?.person, + it.second.opphold.firstOrNull()?.person, + ) + + validerAtSubEntiteterAvPersonErLike( + it.first.arbeidsforhold, + it.second.arbeidsforhold, + it.first.arbeidsforhold.firstOrNull()?.person, + it.second.arbeidsforhold.firstOrNull()?.person, + ) + + if (it.first.dødsfall != null) { + validerAtSubEntiteterAvPersonErLike( + listOf(it.first.dødsfall), + listOf(it.second.dødsfall), + it.first.dødsfall?.person, + it.second.dødsfall?.person, + ) + } + } + } + + fun validerAtSubEntiteterAvPersonErLike( + forrige: List, + kopiert: List, + forrigePerson: Person?, + kopiertPerson: Person?, + erEnhetstestOgBostedsadresse: Boolean = false, + ) { + val baseEntitetFelter = + BaseEntitet::class.declaredMemberProperties.map { it.name }.toTypedArray() + + // Sammenligner ikke id, person og BaseEntitet-felter. id skal være ulik, person sjekkes separat og likhet med BaseEntitet-felter bryr vi oss ikke om. + assertThat(kopiert).usingRecursiveFieldByFieldElementComparatorIgnoringFields( + "id", + "person", + *baseEntitetFelter, + ).isEqualTo(forrige) + + // Id skal alltid være ulik. Har ikke mulighet til å sette id til bostedsadresser i enhetstester + if (kopiert.isNotEmpty() && !erEnhetstestOgBostedsadresse) { + assertThat(kopiert).usingRecursiveFieldByFieldElementComparatorOnFields("id").isNotEqualTo(forrige) + } + + if (kopiertPerson != null) { + // Ignorerer sub-entiteter i sjekk da disse sjekkes hver for seg. + assertThat(kopiertPerson).usingRecursiveComparison().ignoringFields( + "id", + "personopplysningGrunnlag", + "bostedsadresser", + "statsborgerskap", + "opphold", + "arbeidsforhold", + "sivilstander", + "dødsfall", + *baseEntitetFelter, + ).isEqualTo(forrigePerson) + assertThat(kopiertPerson.id).isNotEqualTo(forrigePerson?.id) + } + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/S\303\270knadGrunnlagTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/S\303\270knadGrunnlagTest.kt" new file mode 100644 index 000000000..d7c894baf --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/grunnlag/s\303\270knad/S\303\270knadGrunnlagTest.kt" @@ -0,0 +1,309 @@ +package no.nav.familie.ba.sak.kjerne.grunnlag.søknad + +import no.nav.familie.ba.sak.common.kjørStegprosessForFGB +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.ekstern.restDomene.BarnMedOpplysninger +import no.nav.familie.ba.sak.ekstern.restDomene.BehandlingUnderkategoriDTO +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.SøkerMedOpplysninger +import no.nav.familie.ba.sak.ekstern.restDomene.SøknadDTO +import no.nav.familie.ba.sak.ekstern.restDomene.writeValueAsString +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.dao.EmptyResultDataAccessException +import java.time.LocalDate + +class SøknadGrunnlagTest( + @Autowired + private val søknadGrunnlagService: SøknadGrunnlagService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val stegService: StegService, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val vedtakService: VedtakService, + + @Autowired + private val vilkårsvurderingService: VilkårsvurderingService, + + @Autowired + private val beregningService: BeregningService, + + @Autowired + private val utvidetBehandlingService: UtvidetBehandlingService, + + @Autowired + private val vedtaksperiodeService: VedtaksperiodeService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val brevmalService: BrevmalService, +) : AbstractSpringIntegrationTest() { + + @BeforeAll + fun init() { + databaseCleanupService.truncate() + } + + @Test + fun `Skal lagre ned og hente søknadsgrunnlag`() { + val søkerIdent = randomFnr() + val barnIdent = randomFnr() + val søkerAktør = personidentService.hentAktør(søkerIdent) + + val fagsak = fagsakService.hentEllerOpprettFagsak(søkerAktør.aktivFødselsnummer()) + val behandling = stegService.håndterNyBehandling( + lagNyBehandling(søkerIdent, fagsak.id), + ) + + val søknadDTO = lagSøknadDTO(søkerIdent = søkerIdent, barnasIdenter = listOf(barnIdent)) + søknadGrunnlagService.lagreOgDeaktiverGammel( + SøknadGrunnlag( + behandlingId = behandling.id, + søknad = søknadDTO.writeValueAsString(), + ), + ) + + val søknadGrunnlag = søknadGrunnlagService.hentAktiv(behandling.id) + assertNotNull(søknadGrunnlag) + assertEquals(behandling.id, søknadGrunnlag?.behandlingId) + assertEquals(true, søknadGrunnlag?.aktiv) + assertEquals(søkerIdent, søknadGrunnlag?.hentSøknadDto()?.søkerMedOpplysninger?.ident) + } + + @Test + fun `Skal sjekke at det kun kan være et aktivt grunnlag for en behandling`() { + val søkerIdent = randomFnr() + val barnIdent = randomFnr() + val søkerAktør = personidentService.hentAktør(søkerIdent) + + val fagsak = fagsakService.hentEllerOpprettFagsak(søkerAktør.aktivFødselsnummer()) + val behandling = stegService.håndterNyBehandling( + lagNyBehandling(søkerIdent, fagsak.id), + ) + val søknadDTO = lagSøknadDTO(søkerIdent = søkerIdent, barnasIdenter = listOf(barnIdent)) + + val barnIdent2 = randomFnr() + val søknadDTO2 = lagSøknadDTO(søkerIdent = søkerIdent, barnasIdenter = listOf(barnIdent2)) + + søknadGrunnlagService.lagreOgDeaktiverGammel( + SøknadGrunnlag( + behandlingId = behandling.id, + søknad = søknadDTO.writeValueAsString(), + ), + ) + + søknadGrunnlagService.lagreOgDeaktiverGammel( + SøknadGrunnlag( + behandlingId = behandling.id, + søknad = søknadDTO2.writeValueAsString(), + ), + ) + val søknadsGrunnlag = søknadGrunnlagService.hentAlle(behandling.id) + assertEquals(2, søknadsGrunnlag.size) + + val aktivSøknadGrunnlag = søknadGrunnlagService.hentAktiv(behandling.id) + assertNotNull(aktivSøknadGrunnlag) + } + + @Test + fun `Skal registrere søknad med uregistrerte barn og disse skal ikke komme med i persongrunnlaget`() { + val søkerIdent = randomFnr() + val folkeregistrertBarn = ClientMocks.barnFnr[0] + val uregistrertBarn = randomFnr() + + val søkerAktør = personidentService.hentAktør(søkerIdent) + + val søknadDTO = SøknadDTO( + underkategori = BehandlingUnderkategoriDTO.ORDINÆR, + søkerMedOpplysninger = SøkerMedOpplysninger( + ident = søkerIdent, + ), + barnaMedOpplysninger = listOf( + BarnMedOpplysninger( + ident = folkeregistrertBarn, + ), + BarnMedOpplysninger( + ident = uregistrertBarn, + erFolkeregistrert = false, + ), + ), + endringAvOpplysningerBegrunnelse = "", + ) + + val fagsak = fagsakService.hentEllerOpprettFagsak(søkerAktør.aktivFødselsnummer()) + val behandling = stegService.håndterNyBehandling( + lagNyBehandling(søkerIdent, fagsak.id), + ) + + stegService.håndterSøknad( + behandling = behandling, + restRegistrerSøknad = RestRegistrerSøknad( + søknad = søknadDTO, + bekreftEndringerViaFrontend = false, + ), + ) + + val persongrunnlag = persongrunnlagService.hentAktiv(behandlingId = behandling.id) + + assertEquals(1, persongrunnlag!!.barna.size) + assertTrue(persongrunnlag.barna.any { it.aktør.aktivFødselsnummer() == folkeregistrertBarn }) + assertTrue(persongrunnlag.barna.none { it.aktør.aktivFødselsnummer() == uregistrertBarn }) + } + + private fun lagNyBehandling(søkerIdent: String, fagsakId: Long) = NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + søkersIdent = søkerIdent, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + søknadMottattDato = LocalDate.now(), + fagsakId = fagsakId, + ) + + @Test + fun `Skal tilbakestille behandling ved endring på søknadsregistrering`() { + val søkerFnr = randomFnr() + val barn1Fnr = ClientMocks.barnFnr[0] + val barn2Fnr = ClientMocks.barnFnr[1] + val behandlingEtterVilkårsvurderingSteg = kjørStegprosessForFGB( + tilSteg = StegType.VILKÅRSVURDERING, + søkerFnr = søkerFnr, + barnasIdenter = listOf(barn1Fnr), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val tilkjentYtelse = + beregningService.hentTilkjentYtelseForBehandling(behandlingId = behandlingEtterVilkårsvurderingSteg.id) + val steg = behandlingEtterVilkårsvurderingSteg.behandlingStegTilstand.map { it.behandlingSteg }.toSet() + assertEquals( + setOf( + StegType.REGISTRERE_SØKNAD, + StegType.REGISTRERE_PERSONGRUNNLAG, + StegType.VILKÅRSVURDERING, + StegType.BEHANDLINGSRESULTAT, + ), + steg, + ) + assertNotNull(tilkjentYtelse) + assertTrue(tilkjentYtelse.andelerTilkjentYtelse.size > 0) + + val behandlingEtterNyRegistrering = stegService.håndterSøknad( + behandling = behandlingEtterVilkårsvurderingSteg, + restRegistrerSøknad = RestRegistrerSøknad( + søknad = SøknadDTO( + underkategori = BehandlingUnderkategoriDTO.ORDINÆR, + søkerMedOpplysninger = SøkerMedOpplysninger( + ident = søkerFnr, + ), + barnaMedOpplysninger = listOf( + BarnMedOpplysninger( + ident = barn1Fnr, + inkludertISøknaden = false, + ), + BarnMedOpplysninger( + ident = barn2Fnr, + inkludertISøknaden = true, + ), + ), + endringAvOpplysningerBegrunnelse = "", + ), + bekreftEndringerViaFrontend = true, + ), + ) + + assertThrows { beregningService.hentTilkjentYtelseForBehandling(behandlingId = behandlingEtterNyRegistrering.id) } + val stegEtterNyRegistrering = + behandlingEtterNyRegistrering.behandlingStegTilstand.map { it.behandlingSteg }.toSet() + assertEquals( + setOf(StegType.REGISTRERE_SØKNAD, StegType.REGISTRERE_PERSONGRUNNLAG, StegType.VILKÅRSVURDERING), + stegEtterNyRegistrering, + ) + } + + @Test + fun `Skal fjerne barn og mapping til restbehandling skal kjøre ok`() { + val søkerFnr = randomFnr() + val barn1Fnr = ClientMocks.barnFnr[0] + val barn2Fnr = ClientMocks.barnFnr[1] + val behandlingEtterVilkårsvurderingSteg = kjørStegprosessForFGB( + tilSteg = StegType.VILKÅRSVURDERING, + søkerFnr = søkerFnr, + barnasIdenter = listOf(barn1Fnr), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val behandlingEtterNyRegistrering = stegService.håndterSøknad( + behandling = behandlingEtterVilkårsvurderingSteg, + restRegistrerSøknad = RestRegistrerSøknad( + søknad = SøknadDTO( + underkategori = BehandlingUnderkategoriDTO.ORDINÆR, + søkerMedOpplysninger = SøkerMedOpplysninger( + ident = søkerFnr, + ), + barnaMedOpplysninger = listOf( + BarnMedOpplysninger( + ident = barn1Fnr, + inkludertISøknaden = false, + ), + BarnMedOpplysninger( + ident = barn2Fnr, + inkludertISøknaden = true, + ), + ), + endringAvOpplysningerBegrunnelse = "", + ), + bekreftEndringerViaFrontend = true, + ), + ) + + assertDoesNotThrow { utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingEtterNyRegistrering.id) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingRepositoryTest.kt new file mode 100644 index 000000000..792a4cf8e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/korrigertetterbetaling/KorrigertEtterbetalingRepositoryTest.kt @@ -0,0 +1,137 @@ +package no.nav.familie.ba.sak.kjerne.korrigertetterbetaling + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import org.hamcrest.CoreMatchers.nullValue +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.containsInAnyOrder +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.dao.DataIntegrityViolationException +import org.hamcrest.CoreMatchers.`is` as Is + +class KorrigertEtterbetalingRepositoryTest( + @Autowired private val aktørIdRepository: AktørIdRepository, + @Autowired private val fagsakRepository: FagsakRepository, + @Autowired private val behandlingRepository: BehandlingRepository, + @Autowired private val korrigertEtterbetalingRepository: KorrigertEtterbetalingRepository, +) : AbstractSpringIntegrationTest() { + + @Test + fun `finnAktivtKorrigeringPåBehandling skal returnere null dersom det ikke eksisterer en aktiv etterbetaling korrigering på behandling`() { + val behandling = opprettBehandling() + + val inaktivKorrigertEtterbetaling = KorrigertEtterbetaling( + id = 10000001, + årsak = KorrigertEtterbetalingÅrsak.REFUSJON_FRA_ANDRE_MYNDIGHETER, + begrunnelse = "Test på inaktiv korrigering", + beløp = 1000, + behandling = behandling, + aktiv = false, + ) + + korrigertEtterbetalingRepository.saveAndFlush(inaktivKorrigertEtterbetaling) + + val ikkeEksisterendeKorrigertEtterbetaling = + korrigertEtterbetalingRepository.finnAktivtKorrigeringPåBehandling(behandling.id) + + assertThat(ikkeEksisterendeKorrigertEtterbetaling, Is(nullValue())) + } + + @Test + fun `finnAktivtKorrigeringPåBehandling skal returnere aktiv korrigering på behandling dersom det finnes`() { + val behandling = opprettBehandling() + + val aktivKorrigertEtterbetaling = KorrigertEtterbetaling( + id = 10000002, + årsak = KorrigertEtterbetalingÅrsak.REFUSJON_FRA_ANDRE_MYNDIGHETER, + begrunnelse = "Test på aktiv korrigering", + beløp = 1000, + behandling = behandling, + aktiv = true, + ) + + korrigertEtterbetalingRepository.saveAndFlush(aktivKorrigertEtterbetaling) + + val eksisterendeKorrigertEtterbetaling = + korrigertEtterbetalingRepository.finnAktivtKorrigeringPåBehandling(behandling.id)!! + + assertThat(eksisterendeKorrigertEtterbetaling.begrunnelse, Is("Test på aktiv korrigering")) + assertThat(eksisterendeKorrigertEtterbetaling.beløp, Is(1000)) + } + + @Test + fun `Det skal kastes DataIntegrityViolationException dersom det forsøkes å lagre aktivt korrigering når det allerede finnes en`() { + val behandling = opprettBehandling() + + val aktivKorrigertEtterbetaling = KorrigertEtterbetaling( + id = 10000007, + årsak = KorrigertEtterbetalingÅrsak.REFUSJON_FRA_ANDRE_MYNDIGHETER, + begrunnelse = "Test på aktiv korrigering", + beløp = 1000, + behandling = behandling, + aktiv = true, + ) + + val aktivKorrigertEtterbetaling2 = KorrigertEtterbetaling( + id = 10000008, + årsak = KorrigertEtterbetalingÅrsak.REFUSJON_FRA_ANDRE_MYNDIGHETER, + begrunnelse = "Test på aktiv korrigering", + beløp = 1000, + behandling = behandling, + aktiv = true, + ) + + korrigertEtterbetalingRepository.saveAndFlush(aktivKorrigertEtterbetaling) + + assertThrows { + korrigertEtterbetalingRepository.saveAndFlush(aktivKorrigertEtterbetaling2) + } + } + + @Test + fun `hentAlleKorrigeringPåBehandling skal returnere alle KorrigertEtterbetaling på behandling`() { + val behandling = opprettBehandling() + + val aktivKorrigertEtterbetaling = KorrigertEtterbetaling( + id = 10000003, + årsak = KorrigertEtterbetalingÅrsak.REFUSJON_FRA_ANDRE_MYNDIGHETER, + begrunnelse = "1", + beløp = 1000, + behandling = behandling, + aktiv = true, + ) + + val inaktivKorrigertEtterbetaling = KorrigertEtterbetaling( + id = 10000004, + årsak = KorrigertEtterbetalingÅrsak.REFUSJON_FRA_ANDRE_MYNDIGHETER, + begrunnelse = "2", + beløp = 1000, + behandling = behandling, + aktiv = false, + ) + + korrigertEtterbetalingRepository.saveAndFlush(aktivKorrigertEtterbetaling) + korrigertEtterbetalingRepository.saveAndFlush(inaktivKorrigertEtterbetaling) + + val eksisterendeKorrigertEtterbetaling = + korrigertEtterbetalingRepository.finnAlleKorrigeringerPåBehandling(behandling.id) + + assertThat(eksisterendeKorrigertEtterbetaling.size, Is(2)) + assertThat(eksisterendeKorrigertEtterbetaling.map { it.begrunnelse }, containsInAnyOrder("1", "2")) + } + + private fun opprettBehandling(): Behandling { + val søker = aktørIdRepository.save(randomAktør()) + val fagsak = fagsakRepository.save(Fagsak(aktør = søker)) + + return behandlingRepository.save(lagBehandling(fagsak)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakRepositoryTest.kt new file mode 100644 index 000000000..242935c9d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/korrigertvedtak/KorrigertVedtakRepositoryTest.kt @@ -0,0 +1,101 @@ +package no.nav.familie.ba.sak.kjerne.korrigertvedtak + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.dao.DataIntegrityViolationException +import java.time.LocalDate + +class KorrigertVedtakRepositoryTest( + @Autowired private val aktørIdRepository: AktørIdRepository, + @Autowired private val fagsakRepository: FagsakRepository, + @Autowired private val behandlingRepository: BehandlingRepository, + @Autowired private val korrigertVedtakRepository: KorrigertVedtakRepository, +) : AbstractSpringIntegrationTest() { + + @Test + fun `finnAktivtKorrigertVedtakPåBehandling skal returnere null dersom det ikke eksisterer en aktiv korrigering av vedtak på behandling`() { + val behandling = opprettBehandling() + + val inaktivKorrigertVedtak = KorrigertVedtak( + id = 10000001, + vedtaksdato = LocalDate.now().minusDays(6), + begrunnelse = "Test på inaktiv korrigering", + behandling = behandling, + aktiv = false, + ) + + korrigertVedtakRepository.saveAndFlush(inaktivKorrigertVedtak) + + val ikkeEksisterendeKorrigertVedtak = + korrigertVedtakRepository.finnAktivtKorrigertVedtakPåBehandling(behandling.id) + + Assertions.assertNull(ikkeEksisterendeKorrigertVedtak, "Skal ikke finnes aktiv korrigert vedtak på behandling") + } + + @Test + fun `finnAktivtKorrigertVedtakPåBehandling skal returnere aktiv korrigert vedtak når det eksisterer en aktiv korrigering av vedtak på behandling`() { + val behandling = opprettBehandling() + + val aktivKorrigertVedtak = KorrigertVedtak( + id = 10000001, + vedtaksdato = LocalDate.now().minusDays(6), + begrunnelse = "Test på aktiv korrigering", + behandling = behandling, + aktiv = true, + ) + + korrigertVedtakRepository.saveAndFlush(aktivKorrigertVedtak) + + val eksisterendeKorrigertVedtak = + korrigertVedtakRepository.finnAktivtKorrigertVedtakPåBehandling(behandling.id) + + Assertions.assertNotNull( + eksisterendeKorrigertVedtak, + "Skal finnes aktiv korrigert vedtak på behandling", + ) + } + + @Test + fun `Det skal kastes DataIntegrityViolationException dersom det forsøkes å lagre aktivt korrigert vedtak når det allerede finnes en`() { + val behandling = opprettBehandling() + + val aktivKorrigertVedtak1 = KorrigertVedtak( + id = 10000007, + begrunnelse = "Test på aktiv korrigering", + vedtaksdato = LocalDate.now().minusDays(6), + behandling = behandling, + aktiv = true, + ) + + val aktivKorrigertVedtak2 = KorrigertVedtak( + id = 10000008, + begrunnelse = "Test på aktiv korrigering", + vedtaksdato = LocalDate.now().minusDays(3), + behandling = behandling, + aktiv = true, + ) + + korrigertVedtakRepository.saveAndFlush(aktivKorrigertVedtak1) + + assertThrows { + korrigertVedtakRepository.saveAndFlush(aktivKorrigertVedtak2) + } + } + + private fun opprettBehandling(): Behandling { + val søker = aktørIdRepository.save(randomAktør()) + val fagsak = fagsakRepository.save(Fagsak(aktør = søker)) + + return behandlingRepository.save(lagBehandling(fagsak)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/logg/LoggServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/logg/LoggServiceTest.kt new file mode 100644 index 000000000..ccf1a44ef --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/logg/LoggServiceTest.kt @@ -0,0 +1,342 @@ +package no.nav.familie.ba.sak.kjerne.logg + +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.mockHentPersoninfoForMedIdenter +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import no.nav.familie.ba.sak.kjerne.steg.BehandlerRolle +import no.nav.familie.ba.sak.kjerne.steg.FØRSTE_STEG +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDateTime + +class LoggServiceTest( + @Autowired + private val loggService: LoggService, + + @Autowired + private val stegService: StegService, + + @Autowired + private val mockPersonopplysningerService: PersonopplysningerService, + + @Autowired + private val behandlingRepository: BehandlingRepository, + + @Autowired + private val aktørIdRepository: AktørIdRepository, + + @Autowired + private val fagsakRepository: FagsakRepository, +) : AbstractSpringIntegrationTest() { + + @Test + fun `Skal lage noen logginnslag på forskjellige behandlinger og hente dem fra databasen`() { + val behandling: Behandling = lagBehandling() + val behandling1: Behandling = lagBehandling() + + val logg1 = Logg( + behandlingId = behandling.id, + type = LoggType.BEHANDLING_OPPRETTET, + tittel = "Førstegangsbehandling opprettet", + rolle = BehandlerRolle.SYSTEM, + tekst = "", + ) + loggService.lagre(logg1) + + val logg2 = Logg( + behandlingId = behandling.id, + type = LoggType.BEHANDLING_OPPRETTET, + tittel = "Revurdering opprettet", + rolle = BehandlerRolle.SYSTEM, + tekst = "", + ) + loggService.lagre(logg2) + + val logg3 = Logg( + behandlingId = behandling1.id, + type = LoggType.BEHANDLING_OPPRETTET, + tittel = "Førstegangsbehandling opprettet", + rolle = BehandlerRolle.SYSTEM, + tekst = "", + ) + loggService.lagre(logg3) + + val loggForBehandling = loggService.hentLoggForBehandling(behandling.id) + assertEquals(2, loggForBehandling.size) + + val loggForBehandling1 = loggService.hentLoggForBehandling(behandling1.id) + assertEquals(1, loggForBehandling1.size) + } + + @Test + fun `Skal lage logginnslag ved stegflyt for automatisk behandling`() { + val morsIdent = randomFnr() + val barnetsIdent = "29422059278" + + mockHentPersoninfoForMedIdenter(mockPersonopplysningerService, morsIdent, barnetsIdent) + + val behandling = stegService.opprettNyBehandlingOgRegistrerPersongrunnlagForFødselhendelse( + NyBehandlingHendelse( + morsIdent = morsIdent, + barnasIdenter = listOf(barnetsIdent), + ), + ) + + val loggForBehandling = loggService.hentLoggForBehandling(behandlingId = behandling.id) + assertEquals(2, loggForBehandling.size) + assertTrue(loggForBehandling.any { it.type == LoggType.LIVSHENDELSE && it.tekst == "Gjelder barn 29.02.20" }) + assertTrue(loggForBehandling.any { it.type == LoggType.BEHANDLING_OPPRETTET }) + assertTrue(loggForBehandling.none { it.rolle != BehandlerRolle.SYSTEM }) + } + + @Test + fun `Skal lage nye vilkårslogger og endringer`() { + val behandling = lagBehandling() + val vilkårsvurderingLogg = loggService.opprettVilkårsvurderingLogg( + behandling = behandling, + forrigeBehandlingsresultat = behandling.resultat, + nyttBehandlingsresultat = Behandlingsresultat.INNVILGET, + ) + + assertNotNull(vilkårsvurderingLogg) + assertEquals("Vilkårsvurdering gjennomført", vilkårsvurderingLogg!!.tittel) + + behandling.resultat = Behandlingsresultat.INNVILGET + val nyVilkårsvurderingLogg = + loggService.opprettVilkårsvurderingLogg( + behandling = behandling, + forrigeBehandlingsresultat = behandling.resultat, + nyttBehandlingsresultat = Behandlingsresultat.AVSLÅTT, + ) + + assertNotNull(nyVilkårsvurderingLogg) + assertEquals("Vilkårsvurdering endret", nyVilkårsvurderingLogg!!.tittel) + + val logger = loggService.hentLoggForBehandling(behandlingId = behandling.id) + assertEquals(2, logger.size) + } + + @Test + fun `Skal ikke logge ved uforandret behandlingsresultat`() { + val vilkårsvurderingLogg = loggService.opprettVilkårsvurderingLogg( + behandling = lagBehandling(), + forrigeBehandlingsresultat = Behandlingsresultat.FORTSATT_INNVILGET, + nyttBehandlingsresultat = Behandlingsresultat.FORTSATT_INNVILGET, + ) + + assertNull(vilkårsvurderingLogg) + } + + @Test + fun `Skal lage noen logginnslag på helmanuell migrering ved avvik innenfor beløpsgrenser`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + ) + loggService.opprettBehandlingLogg(BehandlingLoggRequest(behandling)) + loggService.opprettVilkårsvurderingLogg(behandling, behandling.resultat, Behandlingsresultat.INNVILGET) + loggService.opprettSendTilBeslutterLogg(behandling = behandling, skalAutomatiskBesluttes = true) + loggService.opprettBeslutningOmVedtakLogg( + behandling = behandling, + beslutning = Beslutning.GODKJENT, + begrunnelse = "begrunnelse", + behandlingErAutomatiskBesluttet = true, + ) + loggService.opprettFerdigstillBehandling(behandling) + + val logger = loggService.hentLoggForBehandling(behandling.id) + assertEquals(5, logger.size) + assertTrue { + logger.any { + it.type == LoggType.BEHANDLING_OPPRETTET && it.tittel == "Migrering fra infotrygd opprettet" + } + } + assertTrue { + logger.any { + it.type == LoggType.VILKÅRSVURDERING && + it.tittel == "Vilkårsvurdering gjennomført" && it.tekst == "Resultat ble innvilget" + } + } + assertTrue { + logger.any { + it.type == LoggType.SEND_TIL_SYSTEM && + it.tittel == "Sendt til system" + } + } + assertTrue { + logger.any { + it.type == LoggType.MIGRERING_BEKREFTET && + it.tittel == "Migrering bekreftet" && + it.opprettetAv == SikkerhetContext.SYSTEM_NAVN + } + } + assertTrue { + logger.any { + it.type == LoggType.FERDIGSTILLE_BEHANDLING && + it.tittel == "Ferdigstilt behandling" + } + } + } + + @Test + fun `Skal lage noen logginnslag på helmanuell migrering ved avvik utenfor beløpsgrenser`() { + val behandling = lagBehandling( + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + ) + loggService.opprettBehandlingLogg(BehandlingLoggRequest(behandling)) + loggService.opprettVilkårsvurderingLogg(behandling, behandling.resultat, Behandlingsresultat.INNVILGET) + loggService.opprettSendTilBeslutterLogg(behandling = behandling, skalAutomatiskBesluttes = false) + loggService.opprettBeslutningOmVedtakLogg( + behandling = behandling, + beslutning = Beslutning.GODKJENT, + begrunnelse = "begrunnelse", + behandlingErAutomatiskBesluttet = false, + ) + loggService.opprettFerdigstillBehandling(behandling) + + val logger = loggService.hentLoggForBehandling(behandling.id) + assertEquals(5, logger.size) + assertTrue { + logger.any { + it.type == LoggType.BEHANDLING_OPPRETTET && it.tittel == "Migrering fra infotrygd opprettet" + } + } + assertTrue { + logger.any { + it.type == LoggType.VILKÅRSVURDERING && + it.tittel == "Vilkårsvurdering gjennomført" && it.tekst == "Resultat ble innvilget" + } + } + assertTrue { + logger.any { + it.type == LoggType.SEND_TIL_BESLUTTER && + it.tittel == "Sendt til beslutter" + } + } + assertTrue { + logger.any { + it.type == LoggType.GODKJENNE_VEDTAK && + it.tittel == "Vedtak godkjent" && + it.opprettetAv == SikkerhetContext.SYSTEM_NAVN + } + } + assertTrue { + logger.any { + it.type == LoggType.FERDIGSTILLE_BEHANDLING && + it.tittel == "Ferdigstilt behandling" + } + } + } + + @Test + fun `Om to logginnslag blir oppretta i samme sekund skal vi sortere med den første først`() { + val behandlingId = lagBehandling( + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak = BehandlingÅrsak.SØKNAD, + ).id + val tidspunkt = LocalDateTime.now() + val logg1 = loggService.lagre( + Logg( + behandlingId = behandlingId, + type = LoggType.LIVSHENDELSE, + tittel = "Mottok fødselshendelse", + rolle = BehandlerRolle.SAKSBEHANDLER, + tekst = "", + opprettetTidspunkt = tidspunkt, + ), + ) + val logg2 = loggService.lagre( + Logg( + behandlingId = behandlingId, + type = LoggType.BEHANDLING_OPPRETTET, + tittel = "Førstegangbehandling opprettet", + rolle = BehandlerRolle.SAKSBEHANDLER, + tekst = "", + opprettetTidspunkt = tidspunkt, + ), + ) + + val logginnslag = loggService.hentLoggForBehandling(behandlingId) + assertEquals(listOf(logg2.id, logg1.id), logginnslag.map { it.id }) + } + + @Test + fun `eldste logginnslag skal komme sist og nyaste først`() { + val behandlingId = lagBehandling( + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak = BehandlingÅrsak.SØKNAD, + ).id + val eldst = loggService.lagre( + Logg( + behandlingId = behandlingId, + type = LoggType.BEHANDLING_OPPRETTET, + tittel = "Førstegangbehandling opprettet", + rolle = BehandlerRolle.SAKSBEHANDLER, + tekst = "", + ), + ) + val mellomst = loggService.lagre( + Logg( + behandlingId = behandlingId, + type = LoggType.LIVSHENDELSE, + tittel = "Søknaden ble registrert", + rolle = BehandlerRolle.SAKSBEHANDLER, + tekst = "", + ), + ) + val nyast = loggService.lagre( + Logg( + behandlingId = behandlingId, + type = LoggType.LIVSHENDELSE, + tittel = "Vilkårsvurdering gjennomført", + rolle = BehandlerRolle.SAKSBEHANDLER, + tekst = "", + ), + ) + + val logginnslag = loggService.hentLoggForBehandling(behandlingId) + assertEquals(listOf(nyast.id, mellomst.id, eldst.id), logginnslag.map { it.id }) + } + + fun lagBehandling( + behandlingType: BehandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak: BehandlingÅrsak = BehandlingÅrsak.SØKNAD, + ): Behandling { + val aktør = randomAktør().also { aktørIdRepository.save(it) } + val fagsak = Fagsak(aktør = aktør).also { fagsakRepository.save(it) } + + return Behandling( + fagsak = fagsak, + skalBehandlesAutomatisk = false, + type = behandlingType, + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + opprettetÅrsak = årsak, + resultat = Behandlingsresultat.IKKE_VURDERT, + ).also { + it.behandlingStegTilstand.add(BehandlingStegTilstand(0, it, FØRSTE_STEG)) + }.also { behandlingRepository.save(it) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringServiceTest.kt new file mode 100644 index 000000000..4831943eb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/simulering/SimuleringServiceTest.kt @@ -0,0 +1,69 @@ +package no.nav.familie.ba.sak.kjerne.simulering + +import no.nav.familie.ba.sak.common.kjørStegprosessForFGB +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.simuleringMottakerMock +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +@Tag("integration") +class SimuleringServiceTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val vilkårsvurderingService: VilkårsvurderingService, + @Autowired private val persongrunnlagService: PersongrunnlagService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val stegService: StegService, + @Autowired private val simuleringService: SimuleringService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, + @Autowired private val databaseCleanupService: DatabaseCleanupService, + @Autowired private val brevmalService: BrevmalService, +) : AbstractSpringIntegrationTest() { + + @BeforeAll + fun init() { + databaseCleanupService.truncate() + } + + @Test + fun `Skal verifisere at simulering blir lagert og oppdatert`() { + val behandlingEtterVilkårsvurderingSteg = kjørStegprosessForFGB( + tilSteg = StegType.VURDER_TILBAKEKREVING, + søkerFnr = randomFnr(), + barnasIdenter = listOf(ClientMocks.barnFnr[0]), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val vedtakSimuleringMottakerMock = + simuleringMottakerMock.map { it.tilBehandlingSimuleringMottaker(behandlingEtterVilkårsvurderingSteg) } + + assertEquals( + vedtakSimuleringMottakerMock.size, + simuleringService.oppdaterSimuleringPåBehandlingVedBehov(behandlingEtterVilkårsvurderingSteg.id).size, + ) + + assertEquals( + vedtakSimuleringMottakerMock.size, + simuleringService.oppdaterSimuleringPåBehandling(behandlingEtterVilkårsvurderingSteg).size, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerPersongrunnlagTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerPersongrunnlagTest.kt new file mode 100644 index 000000000..69b3de66b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/RegistrerPersongrunnlagTest.kt @@ -0,0 +1,132 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +class RegistrerPersongrunnlagTest( + @Autowired + private val stegService: StegService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + + @Autowired + private val mockPersonopplysningerService: PersonopplysningerService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, +) : AbstractSpringIntegrationTest() { + + @BeforeAll + fun truncate() { + databaseCleanupService.truncate() + } + + @Test + @Tag("integration") + fun `Legg til personer på behandling`() { + val morId = randomFnr() + val barn1Id = randomFnr() + val barn2Id = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(morId) + val behandling1 = + behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + stegService.håndterPersongrunnlag( + behandling = behandling1, + registrerPersongrunnlagDTO = RegistrerPersongrunnlagDTO( + ident = morId, + barnasIdenter = listOf(barn1Id, barn2Id), + ), + ) + + val grunnlag1 = personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = behandling1.id) + + Assertions.assertEquals(3, grunnlag1!!.personer.size) + Assertions.assertTrue(grunnlag1.personer.any { it.aktør.aktivFødselsnummer() == morId }) + Assertions.assertTrue(grunnlag1.personer.any { it.aktør.aktivFødselsnummer() == barn1Id }) + Assertions.assertTrue(grunnlag1.personer.any { it.aktør.aktivFødselsnummer() == barn2Id }) + Assertions.assertEquals(2, grunnlag1.personer.first { it.type == PersonType.SØKER }.sivilstander.size) + + Assertions.assertTrue(grunnlag1.personer.any { it.aktør.aktivFødselsnummer() == barn1Id }) + } + + @Test + @Tag("integration") + fun `Legg til barn på eksisterende behandling`() { + val morId = randomFnr() + val barn1Id = randomFnr() + val barn2Id = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(morId) + val behandling1 = + behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + stegService.håndterPersongrunnlag( + behandling = behandling1, + registrerPersongrunnlagDTO = RegistrerPersongrunnlagDTO( + ident = morId, + barnasIdenter = listOf(barn1Id), + ), + ) + + val grunnlag1 = personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = behandling1.id) + + Assertions.assertEquals(2, grunnlag1!!.personer.size) + Assertions.assertTrue(grunnlag1.personer.any { it.aktør.aktivFødselsnummer() == morId }) + Assertions.assertTrue(grunnlag1.personer.any { it.aktør.aktivFødselsnummer() == barn1Id }) + + stegService.håndterPersongrunnlag( + behandling = behandling1, + registrerPersongrunnlagDTO = RegistrerPersongrunnlagDTO( + ident = morId, + barnasIdenter = listOf( + barn1Id, + barn2Id, + ), + ), + ) + val grunnlag2 = personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = behandling1.id) + + Assertions.assertEquals(3, grunnlag2!!.personer.size) + Assertions.assertTrue(grunnlag2.personer.any { it.aktør.aktivFødselsnummer() == morId }) + Assertions.assertTrue(grunnlag2.personer.any { it.aktør.aktivFødselsnummer() == barn1Id }) + Assertions.assertTrue(grunnlag2.personer.any { it.aktør.aktivFødselsnummer() == barn2Id }) + + // Skal ikke føre til flere personer på persongrunnlaget + stegService.håndterPersongrunnlag( + behandling = behandling1, + registrerPersongrunnlagDTO = RegistrerPersongrunnlagDTO( + ident = morId, + barnasIdenter = listOf( + barn1Id, + barn2Id, + ), + ), + ) + + val grunnlag3 = personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId = behandling1.id) + + Assertions.assertEquals(3, grunnlag3!!.personer.size) + Assertions.assertTrue(grunnlag3.personer.any { it.aktør.aktivFødselsnummer() == morId }) + Assertions.assertTrue(grunnlag3.personer.any { it.aktør.aktivFødselsnummer() == barn1Id }) + Assertions.assertTrue(grunnlag3.personer.any { it.aktør.aktivFødselsnummer() == barn2Id }) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/StegServiceIntegrationTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/StegServiceIntegrationTest.kt new file mode 100644 index 000000000..0555466bd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/StegServiceIntegrationTest.kt @@ -0,0 +1,1192 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import io.mockk.every +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.kjørStegprosessForFGB +import no.nav.familie.ba.sak.common.kjørStegprosessForRevurderingÅrligKontroll +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.mockHentPersoninfoForMedIdenter +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.integrasjoner.oppgave.domene.DbOppgave +import no.nav.familie.ba.sak.integrasjoner.oppgave.domene.OppgaveRepository +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.ba.sak.integrasjoner.økonomi.ØkonomiKlient +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.HenleggÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.RestHenleggBehandlingInfo +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.kontrakter.felles.simulering.BetalingType +import no.nav.familie.kontrakter.felles.simulering.DetaljertSimuleringResultat +import no.nav.familie.kontrakter.felles.simulering.FagOmrådeKode +import no.nav.familie.kontrakter.felles.simulering.MottakerType +import no.nav.familie.kontrakter.felles.simulering.PosteringType +import no.nav.familie.kontrakter.felles.simulering.SimuleringMottaker +import no.nav.familie.kontrakter.felles.simulering.SimulertPostering +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class StegServiceIntegrationTest( + @Autowired + private val stegService: StegService, + + @Autowired + private val vedtakService: VedtakService, + + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val mockPersonopplysningerService: PersonopplysningerService, + + @Autowired + private val vilkårsvurderingService: VilkårsvurderingService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val totrinnskontrollService: TotrinnskontrollService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val vedtaksperiodeService: VedtaksperiodeService, + + @Autowired + private val oppgaveRepository: OppgaveRepository, + + @Autowired + private val brevmalService: BrevmalService, + + @Autowired + private val økonomiKlient: ØkonomiKlient, + +) : AbstractSpringIntegrationTest() { + + @BeforeEach + fun init() { + databaseCleanupService.truncate() + ClientMocks.clearPdlMocks(mockPersonopplysningerService) + } + + @Test + fun `Skal sette default-verdier på gift-vilkår for barn`() { + val søkerFnr = randomFnr() + val barnFnr1 = ClientMocks.barnFnr[0] + val barnFnr2 = ClientMocks.barnFnr[1] + + val behandling = kjørStegprosessForFGB( + tilSteg = StegType.REGISTRERE_SØKNAD, + søkerFnr = søkerFnr, + barnasIdenter = listOf(barnFnr1, barnFnr2), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandling(behandlingId = behandling.id)!! + assertEquals( + Resultat.OPPFYLT, + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr1 }.vilkårResultater + .single { it.vilkårType == Vilkår.GIFT_PARTNERSKAP }.resultat, + ) + assertEquals( + Resultat.IKKE_VURDERT, + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr2 }.vilkårResultater + .single { it.vilkårType == Vilkår.GIFT_PARTNERSKAP }.resultat, + ) + } + + @Test + fun `Skal kjøre gjennom alle steg med datageneratoren`() { + val søkerFnr = randomFnr() + val behandling = kjørStegprosessForFGB( + tilSteg = StegType.BEHANDLING_AVSLUTTET, + søkerFnr = søkerFnr, + barnasIdenter = listOf(ClientMocks.barnFnr[0]), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + // Venter med å kjøre gjennom til avsluttet til brev er støttet for fortsatt innvilget. + kjørStegprosessForRevurderingÅrligKontroll( + tilSteg = StegType.SEND_TIL_BESLUTTER, + søkerFnr = søkerFnr, + barnasIdenter = listOf(ClientMocks.barnFnr[0]), + vedtakService = vedtakService, + stegService = stegService, + fagsakId = behandling.fagsak.id, + brevmalService = brevmalService, + ) + } + + @Test + fun `Skal feile når man prøver å håndtere feil steg`() { + val søkerFnr = randomFnr() + + mockHentPersoninfoForMedIdenter(mockPersonopplysningerService, søkerFnr, "98765432110") + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + assertEquals(FØRSTE_STEG, behandling.steg) + + assertThrows { + stegService.håndterVilkårsvurdering(behandling) + } + } + + @Test + fun `Skal feile når man prøver å endre en avsluttet behandling`() { + val søkerFnr = randomFnr() + + mockHentPersoninfoForMedIdenter(mockPersonopplysningerService, søkerFnr, "98765432110") + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling, aktiv = true) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = vilkårsvurdering) + + behandling.behandlingStegTilstand.add(BehandlingStegTilstand(0, behandling, StegType.BEHANDLING_AVSLUTTET)) + behandling.status = BehandlingStatus.AVSLUTTET + val feil = assertThrows { + stegService.håndterSendTilBeslutter(behandling, "1234") + } + assertEquals( + "Behandling med id ${behandling.id} er avsluttet og stegprosessen kan ikke gjenåpnes", + feil.message, + ) + } + + @Test + fun `Skal feile når man prøver å noe annet enn å beslutte behandling når den er på dette steget`() { + val søkerFnr = randomFnr() + + mockHentPersoninfoForMedIdenter(mockPersonopplysningerService, søkerFnr, "98765432110") + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val vilkårsvurdering = Vilkårsvurdering(behandling = behandling, aktiv = true) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = vilkårsvurdering) + + behandling.behandlingStegTilstand.add(BehandlingStegTilstand(0, behandling, StegType.BESLUTTE_VEDTAK)) + behandling.status = BehandlingStatus.FATTER_VEDTAK + assertThrows { + stegService.håndterSendTilBeslutter(behandling, "1234") + } + } + + @Test + fun `Skal feile når man prøver å kalle beslutning-steget med feil status på behandling`() { + val søkerFnr = randomFnr() + + mockHentPersoninfoForMedIdenter(mockPersonopplysningerService, søkerFnr, "98765432110") + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + behandling.behandlingStegTilstand.add(BehandlingStegTilstand(0, behandling, StegType.BESLUTTE_VEDTAK)) + behandling.status = BehandlingStatus.IVERKSETTER_VEDTAK + assertThrows { + stegService.håndterBeslutningForVedtak( + behandling, + RestBeslutningPåVedtak(beslutning = Beslutning.GODKJENT, begrunnelse = null), + ) + } + } + + @Test + fun `Underkjent beslutning setter steg tilbake til send til beslutter`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + + val søkerAktørId = personidentService.hentAktør(søkerFnr) + + mockHentPersoninfoForMedIdenter(mockPersonopplysningerService, søkerFnr, barnFnr) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + vilkårsvurderingService.lagreNyOgDeaktiverGammel( + lagVilkårsvurdering( + søkerAktørId, + behandling, + Resultat.OPPFYLT, + ), + ) + behandling.endretAv = "1234" + assertEquals(FØRSTE_STEG, behandling.steg) + + totrinnskontrollService.opprettTotrinnskontrollMedSaksbehandler(behandling = behandling) + behandling.behandlingStegTilstand.forEach { it.behandlingStegStatus = BehandlingStegStatus.UTFØRT } + behandling.behandlingStegTilstand.add(BehandlingStegTilstand(0, behandling, StegType.BESLUTTE_VEDTAK)) + behandling.status = BehandlingStatus.FATTER_VEDTAK + stegService.håndterBeslutningForVedtak( + behandling, + RestBeslutningPåVedtak(beslutning = Beslutning.UNDERKJENT, begrunnelse = "Feil"), + ) + + val behandlingEtterPersongrunnlagSteg = behandlingHentOgPersisterService.hent(behandlingId = behandling.id) + assertEquals(StegType.SEND_TIL_BESLUTTER, behandlingEtterPersongrunnlagSteg.steg) + } + + @Test + fun `Henlegge før behandling er sendt til beslutter`() { + val vilkårsvurdertBehandling = kjørGjennomStegInkludertVurderTilbakekreving() + + val henlagtBehandling = stegService.håndterHenleggBehandling( + vilkårsvurdertBehandling, + RestHenleggBehandlingInfo( + årsak = HenleggÅrsak.FEILAKTIG_OPPRETTET, + begrunnelse = "", + ), + ) + assertTrue( + henlagtBehandling.behandlingStegTilstand.firstOrNull { + it.behandlingSteg == StegType.HENLEGG_BEHANDLING && it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } != null, + ) + assertTrue( + henlagtBehandling.behandlingStegTilstand.firstOrNull { + it.behandlingSteg == StegType.FERDIGSTILLE_BEHANDLING && it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } != null, + ) + + assertEquals(StegType.BEHANDLING_AVSLUTTET, henlagtBehandling.steg) + } + + @Test + fun `Teknisk henleggelse med begrunnelse Satsendring skal beholde behandleSak-oppgaven åpen`() { + val behandling = kjørGjennomStegInkludertVurderTilbakekreving() + oppgaveRepository.saveAll( + listOf( + DbOppgave(behandling = behandling, type = Oppgavetype.Journalføring, gsakId = "1"), + DbOppgave(behandling = behandling, type = Oppgavetype.BehandleSak, gsakId = "2"), + DbOppgave(behandling = behandling, type = Oppgavetype.BehandleUnderkjentVedtak, gsakId = "3"), + ), + ) + val henlagtBehandling = stegService.håndterHenleggBehandling( + behandling, + RestHenleggBehandlingInfo( + årsak = HenleggÅrsak.TEKNISK_VEDLIKEHOLD, + begrunnelse = "Satsendring", + ), + ) + assertEquals(StegType.BEHANDLING_AVSLUTTET, henlagtBehandling.steg) + assertTrue { + oppgaveRepository.findByBehandlingAndIkkeFerdigstilt(henlagtBehandling) + .filter { it.type == Oppgavetype.BehandleSak }.isNotEmpty() + } + assertTrue { + oppgaveRepository.findByBehandlingAndIkkeFerdigstilt(henlagtBehandling) + .filter { it.type == Oppgavetype.BehandleUnderkjentVedtak }.isNotEmpty() + } + assertTrue { + oppgaveRepository.findByBehandlingAndIkkeFerdigstilt(henlagtBehandling) + .filter { it.type == Oppgavetype.Journalføring }.isEmpty() + } + } + + @Test + fun `Henlegge etter behandling er sendt til beslutter`() { + val vilkårsvurdertBehandling = kjørGjennomStegInkludertVurderTilbakekreving() + stegService.håndterSendTilBeslutter(vilkårsvurdertBehandling, "1234") + + val behandlingEtterSendTilBeslutter = + behandlingHentOgPersisterService.hent(behandlingId = vilkårsvurdertBehandling.id) + + assertThrows { + stegService.håndterHenleggBehandling( + behandlingEtterSendTilBeslutter, + RestHenleggBehandlingInfo( + årsak = HenleggÅrsak.FEILAKTIG_OPPRETTET, + begrunnelse = "", + ), + ) + } + } + + // I de fleste tilfeller vil det ikke være mulig å henlegge en behandling som har kommet forbi iverksett steget. + // Disse vil bli stoppet i BehandlingStegController. + @Test + fun `Henlegge dersom behandling står på FERDIGSTILLE_BEHANDLING steget`() { + val søkerFnr = randomFnr() + + mockHentPersoninfoForMedIdenter(mockPersonopplysningerService, søkerFnr, "98765432110") + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + behandling.behandlingStegTilstand.add( + BehandlingStegTilstand( + 0, + behandling, + StegType.FERDIGSTILLE_BEHANDLING, + BehandlingStegStatus.IKKE_UTFØRT, + ), + ) + + val behandlingEtterHenleggelse = stegService.håndterHenleggBehandling( + behandling, + RestHenleggBehandlingInfo(årsak = HenleggÅrsak.FEILAKTIG_OPPRETTET, begrunnelse = ""), + ) + + assertThat(behandlingEtterHenleggelse.steg).isEqualTo(StegType.BEHANDLING_AVSLUTTET) + assertThat(behandlingEtterHenleggelse.status).isEqualTo(BehandlingStatus.AVSLUTTET) + assertThat(behandlingEtterHenleggelse.behandlingStegTilstand.any { it.behandlingSteg == StegType.FERDIGSTILLE_BEHANDLING && it.behandlingStegStatus == BehandlingStegStatus.UTFØRT }) + } + + @Test + fun `skal kjøre gjennom steg for migreringsbehandling med årsak endre migreringsdato og avvik i simulering innenfor beløpsgrenser`() { + val simulertPosteringMock = listOf( + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-09-01"), + tom = LocalDate.parse("2019-09-30"), + betalingType = BetalingType.DEBIT, + beløp = 1.toBigDecimal(), + posteringType = PosteringType.FEILUTBETALING, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + ) + + val simuleringMottakerMock = listOf( + SimuleringMottaker( + simulertPostering = simulertPosteringMock, + mottakerType = MottakerType.BRUKER, + mottakerNummer = "12345678910", + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns DetaljertSimuleringResultat(simuleringMottakerMock) + + val søkerFnr = randomFnr() + val barnFnr = ClientMocks.barnFnr[0] + val barnasIdenter = listOf(barnFnr) + + kjørStegprosessForFGB( + tilSteg = StegType.BEHANDLING_AVSLUTTET, + søkerFnr = søkerFnr, + barnasIdenter = barnasIdenter, + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val nyMigreringsdato = LocalDate.now().minusMonths(6) + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val behandling = stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + behandlingÅrsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + søkersIdent = søkerFnr, + barnasIdenter = barnasIdenter, + nyMigreringsdato = nyMigreringsdato, + fagsakId = fagsak.id, + ), + ) + assertEquals(StegType.VILKÅRSVURDERING, behandling.steg) + assertTrue { + behandling.behandlingStegTilstand.any { + it.behandlingSteg == StegType.REGISTRERE_PERSONGRUNNLAG && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + assertMigreringsdato(nyMigreringsdato, behandling) + assertNotNull(vilkårsvurderingService.hentAktivForBehandling(behandling.id)) + + val behandlingEtterVilkårsvurdering = stegService.håndterVilkårsvurdering(behandling) + assertEquals(StegType.BEHANDLINGSRESULTAT, behandlingEtterVilkårsvurdering.steg) + + val behandlingEtterBehandlingsresultatSteg = + stegService.håndterBehandlingsresultat(behandlingEtterVilkårsvurdering) + assertEquals(StegType.VURDER_TILBAKEKREVING, behandlingEtterBehandlingsresultatSteg.steg) + + val behandlingEtterTilbakekrevingSteg = stegService.håndterVurderTilbakekreving( + behandlingEtterBehandlingsresultatSteg, + RestTilbakekreving( + valg = Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, + begrunnelse = "ignorer tilbakekreving", + ), + ) + assertEquals(StegType.SEND_TIL_BESLUTTER, behandlingEtterTilbakekrevingSteg.steg) + + val behandlingEtterBeslutterSteg = stegService.håndterSendTilBeslutter( + behandlingEtterTilbakekrevingSteg, + "1234", + ) + assertEquals(StegType.FERDIGSTILLE_BEHANDLING, behandlingEtterBeslutterSteg.steg) + assertTrue { + behandlingEtterBeslutterSteg.behandlingStegTilstand.any { + it.behandlingSteg == StegType.SEND_TIL_BESLUTTER && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + assertTrue { + behandlingEtterBeslutterSteg.behandlingStegTilstand.any { + it.behandlingSteg == StegType.BESLUTTE_VEDTAK && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + val totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(behandling.id) + assertNotNull(totrinnskontroll) + assertEquals(true, totrinnskontroll!!.godkjent) + assertEquals(SikkerhetContext.hentSaksbehandlerNavn(), totrinnskontroll.saksbehandler) + assertEquals(SikkerhetContext.hentSaksbehandler(), totrinnskontroll.saksbehandlerId) + assertEquals(SikkerhetContext.SYSTEM_NAVN, totrinnskontroll.beslutter) + assertEquals(SikkerhetContext.SYSTEM_FORKORTELSE, totrinnskontroll.beslutterId) + } + + @Test + fun `skal kjøre gjennom steg for migreringsbehandling med årsak endre migreringsdato og avvik i simulering utenefor beløpsgrenser`() { + val simulertPosteringMock = listOf( + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-09-01"), + tom = LocalDate.parse("2019-09-30"), + betalingType = BetalingType.DEBIT, + beløp = 600.toBigDecimal(), + posteringType = PosteringType.FEILUTBETALING, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + ) + + val simuleringMottakerMock = listOf( + SimuleringMottaker( + simulertPostering = simulertPosteringMock, + mottakerType = MottakerType.BRUKER, + mottakerNummer = "12345678910", + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns DetaljertSimuleringResultat(simuleringMottakerMock) + + val søkerFnr = randomFnr() + val barnFnr = ClientMocks.barnFnr[0] + val barnasIdenter = listOf(barnFnr) + + kjørStegprosessForFGB( + tilSteg = StegType.BEHANDLING_AVSLUTTET, + søkerFnr = søkerFnr, + barnasIdenter = barnasIdenter, + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val nyMigreringsdato = LocalDate.now().minusMonths(6) + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val behandling = stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + behandlingÅrsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + søkersIdent = søkerFnr, + barnasIdenter = barnasIdenter, + nyMigreringsdato = nyMigreringsdato, + fagsakId = fagsak.id, + ), + ) + assertEquals(StegType.VILKÅRSVURDERING, behandling.steg) + assertTrue { + behandling.behandlingStegTilstand.any { + it.behandlingSteg == StegType.REGISTRERE_PERSONGRUNNLAG && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + assertMigreringsdato(nyMigreringsdato, behandling) + assertNotNull(vilkårsvurderingService.hentAktivForBehandling(behandling.id)) + + val behandlingEtterVilkårsvurdering = stegService.håndterVilkårsvurdering(behandling) + assertEquals(StegType.BEHANDLINGSRESULTAT, behandlingEtterVilkårsvurdering.steg) + + val behandlingEtterBehandlingsresultatSteg = + stegService.håndterBehandlingsresultat(behandlingEtterVilkårsvurdering) + assertEquals(StegType.VURDER_TILBAKEKREVING, behandlingEtterBehandlingsresultatSteg.steg) + + val behandlingEtterTilbakekrevingSteg = stegService.håndterVurderTilbakekreving( + behandlingEtterBehandlingsresultatSteg, + RestTilbakekreving( + valg = Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, + begrunnelse = "ignorer tilbakekreving", + ), + ) + assertEquals(StegType.SEND_TIL_BESLUTTER, behandlingEtterTilbakekrevingSteg.steg) + + val behandlingEtterBeslutterSteg = stegService.håndterSendTilBeslutter( + behandlingEtterTilbakekrevingSteg, + "1234", + ) + assertEquals(StegType.FERDIGSTILLE_BEHANDLING, behandlingEtterBeslutterSteg.steg) + assertTrue { + behandlingEtterBeslutterSteg.behandlingStegTilstand.any { + it.behandlingSteg == StegType.SEND_TIL_BESLUTTER && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + assertTrue { + behandlingEtterBeslutterSteg.behandlingStegTilstand.any { + it.behandlingSteg == StegType.BESLUTTE_VEDTAK && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + val totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(behandling.id) + assertNotNull(totrinnskontroll) + assertEquals(true, totrinnskontroll!!.godkjent) + assertEquals(SikkerhetContext.hentSaksbehandlerNavn(), totrinnskontroll.saksbehandler) + assertEquals(SikkerhetContext.hentSaksbehandler(), totrinnskontroll.saksbehandlerId) + assertEquals(SikkerhetContext.SYSTEM_NAVN, totrinnskontroll.beslutter) + assertEquals(SikkerhetContext.SYSTEM_FORKORTELSE, totrinnskontroll.beslutterId) + } + + @Test + fun `skal kjøre gjennom steg for migreringsbehandling med årsak endre migreringsdato og avvik i simulering utenfor beløpsgrenser`() { + val simulertPosteringMock = listOf( + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-09-01"), + tom = LocalDate.parse("2019-09-30"), + betalingType = BetalingType.DEBIT, + beløp = 500.toBigDecimal(), + posteringType = PosteringType.FEILUTBETALING, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + ) + + val simuleringMottakerMock = listOf( + SimuleringMottaker( + simulertPostering = simulertPosteringMock, + mottakerType = MottakerType.BRUKER, + mottakerNummer = "12345678910", + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns DetaljertSimuleringResultat(simuleringMottakerMock) + + val søkerFnr = randomFnr() + val barnFnr = ClientMocks.barnFnr[0] + val barnasIdenter = listOf(barnFnr) + + kjørStegprosessForFGB( + tilSteg = StegType.BEHANDLING_AVSLUTTET, + søkerFnr = søkerFnr, + barnasIdenter = barnasIdenter, + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val nyMigreringsdato = LocalDate.now().minusMonths(6) + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val behandling = stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + behandlingÅrsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + søkersIdent = søkerFnr, + barnasIdenter = barnasIdenter, + nyMigreringsdato = nyMigreringsdato, + fagsakId = fagsak.id, + ), + ) + assertEquals(StegType.VILKÅRSVURDERING, behandling.steg) + assertTrue { + behandling.behandlingStegTilstand.any { + it.behandlingSteg == StegType.REGISTRERE_PERSONGRUNNLAG && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + assertMigreringsdato(nyMigreringsdato, behandling) + assertNotNull(vilkårsvurderingService.hentAktivForBehandling(behandling.id)) + + val behandlingEtterVilkårsvurdering = stegService.håndterVilkårsvurdering(behandling) + assertEquals(StegType.BEHANDLINGSRESULTAT, behandlingEtterVilkårsvurdering.steg) + + val behandlingEtterBehandlingsresultatSteg = + stegService.håndterBehandlingsresultat(behandlingEtterVilkårsvurdering) + assertEquals(StegType.VURDER_TILBAKEKREVING, behandlingEtterBehandlingsresultatSteg.steg) + + val behandlingEtterTilbakekrevingSteg = stegService.håndterVurderTilbakekreving( + behandlingEtterBehandlingsresultatSteg, + RestTilbakekreving( + valg = Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, + begrunnelse = "ignorer tilbakekreving", + ), + ) + + assertEquals(StegType.SEND_TIL_BESLUTTER, behandlingEtterTilbakekrevingSteg.steg) + + val behandlingEtterSendTilBeslutterSteg = stegService.håndterSendTilBeslutter( + behandlingEtterTilbakekrevingSteg, + "1234", + ) + + assertEquals(StegType.FERDIGSTILLE_BEHANDLING, behandlingEtterSendTilBeslutterSteg.steg) + + val totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(behandling.id) + assertNotNull(totrinnskontroll) + assertEquals(true, totrinnskontroll!!.godkjent) + assertEquals(SikkerhetContext.hentSaksbehandlerNavn(), totrinnskontroll.saksbehandler) + assertEquals(SikkerhetContext.hentSaksbehandler(), totrinnskontroll.saksbehandlerId) + assertEquals(SikkerhetContext.SYSTEM_NAVN, totrinnskontroll.beslutter) + assertEquals(SikkerhetContext.SYSTEM_FORKORTELSE, totrinnskontroll.beslutterId) + } + + @Test + fun `skal kjøre gjennom steg for helmanuell migrering med avvik i simulering innenfor beløpsgrenser`() { + val simulertPosteringMock = listOf( + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-09-01"), + tom = LocalDate.parse("2019-09-30"), + betalingType = BetalingType.DEBIT, + beløp = 1.toBigDecimal(), + posteringType = PosteringType.FEILUTBETALING, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + ) + + val simuleringMottakerMock = listOf( + SimuleringMottaker( + simulertPostering = simulertPosteringMock, + mottakerType = MottakerType.BRUKER, + mottakerNummer = "12345678910", + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns DetaljertSimuleringResultat(simuleringMottakerMock) + + val søkerFnr = randomFnr() + val barnFnr = ClientMocks.barnFnr[0] + val barnasIdenter = listOf(barnFnr) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val migreringsdato = LocalDate.now().minusMonths(6) + val behandling = stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + behandlingÅrsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + søkersIdent = søkerFnr, + barnasIdenter = barnasIdenter, + nyMigreringsdato = migreringsdato, + fagsakId = fagsak.id, + ), + ) + assertEquals(StegType.VILKÅRSVURDERING, behandling.steg) + assertTrue { + behandling.behandlingStegTilstand.any { + it.behandlingSteg == StegType.REGISTRERE_PERSONGRUNNLAG && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + assertMigreringsdato(migreringsdato, behandling) + assertNotNull(vilkårsvurderingService.hentAktivForBehandling(behandling.id)) + val vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandling(behandling.id)!! + val barnPersonResultat = + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr }.apply { + vilkårResultater.first { it.vilkårType == Vilkår.BOR_MED_SØKER } + .apply { utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED) } + } + val søkerPersonResultat = vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == søkerFnr } + vilkårsvurdering.personResultater = setOf(søkerPersonResultat, barnPersonResultat) + vilkårsvurderingService.oppdater(vilkårsvurdering) + + val behandlingEtterVilkårsvurdering = stegService.håndterVilkårsvurdering(behandling) + assertEquals(StegType.BEHANDLINGSRESULTAT, behandlingEtterVilkårsvurdering.steg) + + val behandlingEtterBehandlingsresultatSteg = + stegService.håndterBehandlingsresultat(behandlingEtterVilkårsvurdering) + assertEquals(StegType.VURDER_TILBAKEKREVING, behandlingEtterBehandlingsresultatSteg.steg) + + val behandlingEtterTilbakekrevingSteg = stegService.håndterVurderTilbakekreving( + behandlingEtterBehandlingsresultatSteg, + RestTilbakekreving( + valg = Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, + begrunnelse = "ignorer tilbakekreving", + ), + ) + assertEquals(StegType.SEND_TIL_BESLUTTER, behandlingEtterTilbakekrevingSteg.steg) + + val behandlingEtterBesultterSteg = stegService.håndterSendTilBeslutter( + behandlingEtterTilbakekrevingSteg, + "1234", + ) + assertEquals(StegType.IVERKSETT_MOT_OPPDRAG, behandlingEtterBesultterSteg.steg) + assertTrue { + behandlingEtterBesultterSteg.behandlingStegTilstand.any { + it.behandlingSteg == StegType.SEND_TIL_BESLUTTER && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + assertTrue { + behandlingEtterBesultterSteg.behandlingStegTilstand.any { + it.behandlingSteg == StegType.BESLUTTE_VEDTAK && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + val totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(behandling.id) + assertNotNull(totrinnskontroll) + assertEquals(true, totrinnskontroll!!.godkjent) + assertEquals(SikkerhetContext.hentSaksbehandlerNavn(), totrinnskontroll.saksbehandler) + assertEquals(SikkerhetContext.hentSaksbehandler(), totrinnskontroll.saksbehandlerId) + assertEquals(SikkerhetContext.SYSTEM_NAVN, totrinnskontroll.beslutter) + assertEquals(SikkerhetContext.SYSTEM_FORKORTELSE, totrinnskontroll.beslutterId) + } + + @Test + fun `skal kjøre gjennom steg for helmanuell migrering med avvik i simulering utenfor beløpsgrenser`() { + val simulertPosteringMock = listOf( + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD, + fom = LocalDate.parse("2019-09-01"), + tom = LocalDate.parse("2019-09-30"), + betalingType = BetalingType.DEBIT, + beløp = 300.toBigDecimal(), + posteringType = PosteringType.FEILUTBETALING, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + ) + + val simuleringMottakerMock = listOf( + SimuleringMottaker( + simulertPostering = simulertPosteringMock, + mottakerType = MottakerType.BRUKER, + mottakerNummer = "12345678910", + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns DetaljertSimuleringResultat(simuleringMottakerMock) + + val søkerFnr = randomFnr() + val barnFnr = ClientMocks.barnFnr[0] + val barnasIdenter = listOf(barnFnr) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val migreringsdato = LocalDate.now().minusMonths(6) + val behandling = stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + behandlingÅrsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + søkersIdent = søkerFnr, + barnasIdenter = barnasIdenter, + nyMigreringsdato = migreringsdato, + fagsakId = fagsak.id, + ), + ) + assertEquals(StegType.VILKÅRSVURDERING, behandling.steg) + assertTrue { + behandling.behandlingStegTilstand.any { + it.behandlingSteg == StegType.REGISTRERE_PERSONGRUNNLAG && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + assertMigreringsdato(migreringsdato, behandling) + assertNotNull(vilkårsvurderingService.hentAktivForBehandling(behandling.id)) + val vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandling(behandling.id)!! + val barnPersonResultat = + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr }.apply { + vilkårResultater.first { it.vilkårType == Vilkår.BOR_MED_SØKER } + .apply { utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED) } + } + val søkerPersonResultat = vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == søkerFnr } + vilkårsvurdering.personResultater = setOf(søkerPersonResultat, barnPersonResultat) + vilkårsvurderingService.oppdater(vilkårsvurdering) + + val behandlingEtterVilkårsvurdering = stegService.håndterVilkårsvurdering(behandling) + assertEquals(StegType.BEHANDLINGSRESULTAT, behandlingEtterVilkårsvurdering.steg) + + val behandlingEtterBehandlingsresultatSteg = + stegService.håndterBehandlingsresultat(behandlingEtterVilkårsvurdering) + assertEquals(StegType.VURDER_TILBAKEKREVING, behandlingEtterBehandlingsresultatSteg.steg) + + val behandlingEtterTilbakekrevingSteg = stegService.håndterVurderTilbakekreving( + behandlingEtterBehandlingsresultatSteg, + RestTilbakekreving( + valg = Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, + begrunnelse = "ignorer tilbakekreving", + ), + ) + assertEquals(StegType.SEND_TIL_BESLUTTER, behandlingEtterTilbakekrevingSteg.steg) + + val behandlingEtterSendTilBeslutterSteg = stegService.håndterSendTilBeslutter( + behandlingEtterTilbakekrevingSteg, + "1234", + ) + + assertEquals(StegType.BESLUTTE_VEDTAK, behandlingEtterSendTilBeslutterSteg.steg) + + val behandlingEtterBesluttVedtakSteg = stegService.håndterBeslutningForVedtak( + behandlingEtterSendTilBeslutterSteg, + RestBeslutningPåVedtak( + Beslutning.GODKJENT, + "godkjent manuelt", + ), + ) + + assertEquals(StegType.IVERKSETT_MOT_OPPDRAG, behandlingEtterBesluttVedtakSteg.steg) + + assertTrue { + behandlingEtterBesluttVedtakSteg.behandlingStegTilstand.any { + it.behandlingSteg == StegType.SEND_TIL_BESLUTTER && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + + assertTrue { + behandlingEtterBesluttVedtakSteg.behandlingStegTilstand.any { + it.behandlingSteg == StegType.BESLUTTE_VEDTAK && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + val totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(behandling.id) + assertNotNull(totrinnskontroll) + assertEquals(true, totrinnskontroll!!.godkjent) + assertEquals(SikkerhetContext.hentSaksbehandlerNavn(), totrinnskontroll.saksbehandler) + assertEquals(SikkerhetContext.hentSaksbehandler(), totrinnskontroll.saksbehandlerId) + assertEquals(SikkerhetContext.SYSTEM_NAVN, totrinnskontroll.beslutter) + assertEquals(SikkerhetContext.SYSTEM_FORKORTELSE, totrinnskontroll.beslutterId) + } + + @Test + fun `skal kjøre gjennom steg for helmanuell migrering med manuelle posteringer med avvik innenfor beløpsgrenser`() { + val simulertPosteringMock = listOf( + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD_MANUELT, + fom = LocalDate.parse("2019-09-01"), + tom = LocalDate.parse("2019-09-30"), + betalingType = BetalingType.DEBIT, + beløp = 1.toBigDecimal(), + posteringType = PosteringType.FEILUTBETALING, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + ) + + val simuleringMottakerMock = listOf( + SimuleringMottaker( + simulertPostering = simulertPosteringMock, + mottakerType = MottakerType.BRUKER, + mottakerNummer = "12345678910", + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns DetaljertSimuleringResultat(simuleringMottakerMock) + + val søkerFnr = randomFnr() + val barnFnr = ClientMocks.barnFnr[0] + val barnasIdenter = listOf(barnFnr) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val migreringsdato = LocalDate.now().minusMonths(6) + val behandling = stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + behandlingÅrsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + søkersIdent = søkerFnr, + barnasIdenter = barnasIdenter, + nyMigreringsdato = migreringsdato, + fagsakId = fagsak.id, + ), + ) + assertEquals(StegType.VILKÅRSVURDERING, behandling.steg) + assertTrue { + behandling.behandlingStegTilstand.any { + it.behandlingSteg == StegType.REGISTRERE_PERSONGRUNNLAG && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + assertMigreringsdato(migreringsdato, behandling) + assertNotNull(vilkårsvurderingService.hentAktivForBehandling(behandling.id)) + val vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandling(behandling.id)!! + val barnPersonResultat = + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr }.apply { + vilkårResultater.first { it.vilkårType == Vilkår.BOR_MED_SØKER } + .apply { utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED) } + } + val søkerPersonResultat = vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == søkerFnr } + vilkårsvurdering.personResultater = setOf(søkerPersonResultat, barnPersonResultat) + vilkårsvurderingService.oppdater(vilkårsvurdering) + + val behandlingEtterVilkårsvurdering = stegService.håndterVilkårsvurdering(behandling) + assertEquals(StegType.BEHANDLINGSRESULTAT, behandlingEtterVilkårsvurdering.steg) + + val behandlingEtterBehandlingsresultatSteg = + stegService.håndterBehandlingsresultat(behandlingEtterVilkårsvurdering) + assertEquals(StegType.VURDER_TILBAKEKREVING, behandlingEtterBehandlingsresultatSteg.steg) + + val behandlingEtterTilbakekrevingSteg = stegService.håndterVurderTilbakekreving( + behandlingEtterBehandlingsresultatSteg, + RestTilbakekreving( + valg = Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, + begrunnelse = "ignorer tilbakekreving", + ), + ) + assertEquals(StegType.SEND_TIL_BESLUTTER, behandlingEtterTilbakekrevingSteg.steg) + + val behandlingEtterSendTilBeslutterSteg = stegService.håndterSendTilBeslutter( + behandlingEtterTilbakekrevingSteg, + "1234", + ) + + assertEquals(StegType.BESLUTTE_VEDTAK, behandlingEtterSendTilBeslutterSteg.steg) + + // Må manuelt godkjenne vedtak + val behandlingEtterBesluttVedtakSteg = stegService.håndterBeslutningForVedtak( + behandlingEtterSendTilBeslutterSteg, + RestBeslutningPåVedtak( + Beslutning.GODKJENT, + "godkjent manuelt", + ), + ) + + assertEquals(StegType.IVERKSETT_MOT_OPPDRAG, behandlingEtterBesluttVedtakSteg.steg) + + assertTrue { + behandlingEtterBesluttVedtakSteg.behandlingStegTilstand.any { + it.behandlingSteg == StegType.SEND_TIL_BESLUTTER && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + + assertTrue { + behandlingEtterBesluttVedtakSteg.behandlingStegTilstand.any { + it.behandlingSteg == StegType.BESLUTTE_VEDTAK && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + val totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(behandling.id) + assertNotNull(totrinnskontroll) + assertEquals(true, totrinnskontroll!!.godkjent) + assertEquals(SikkerhetContext.hentSaksbehandlerNavn(), totrinnskontroll.saksbehandler) + assertEquals(SikkerhetContext.hentSaksbehandler(), totrinnskontroll.saksbehandlerId) + assertEquals(SikkerhetContext.SYSTEM_NAVN, totrinnskontroll.beslutter) + assertEquals(SikkerhetContext.SYSTEM_FORKORTELSE, totrinnskontroll.beslutterId) + } + + @Test + fun `skal kjøre gjennom steg for endre migreringsdato behandling og automatisk godkjenne totrinnskontroll`() { + val simulertPosteringMock = listOf( + SimulertPostering( + fagOmrådeKode = FagOmrådeKode.BARNETRYGD_INFOTRYGD_MANUELT, + fom = LocalDate.parse("2019-09-01"), + tom = LocalDate.parse("2019-09-30"), + betalingType = BetalingType.DEBIT, + beløp = 1.toBigDecimal(), + posteringType = PosteringType.FEILUTBETALING, + forfallsdato = LocalDate.parse("2021-02-23"), + utenInntrekk = false, + erFeilkonto = null, + ), + ) + + val simuleringMottakerMock = listOf( + SimuleringMottaker( + simulertPostering = simulertPosteringMock, + mottakerType = MottakerType.BRUKER, + mottakerNummer = "12345678910", + ), + ) + + every { økonomiKlient.hentSimulering(any()) } returns DetaljertSimuleringResultat(simuleringMottakerMock) + + val søkerFnr = randomFnr() + val barnFnr = ClientMocks.barnFnr[0] + val barnasIdenter = listOf(barnFnr) + + kjørStegprosessForFGB( + tilSteg = StegType.BEHANDLING_AVSLUTTET, + søkerFnr = søkerFnr, + barnasIdenter = barnasIdenter, + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val migreringsdato = LocalDate.now().minusMonths(6) + val behandling = stegService.håndterNyBehandling( + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + behandlingÅrsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + søkersIdent = søkerFnr, + barnasIdenter = barnasIdenter, + nyMigreringsdato = migreringsdato, + fagsakId = fagsak.id, + ), + ) + assertEquals(StegType.VILKÅRSVURDERING, behandling.steg) + assertTrue { + behandling.behandlingStegTilstand.any { + it.behandlingSteg == StegType.REGISTRERE_PERSONGRUNNLAG && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + assertMigreringsdato(migreringsdato, behandling) + assertNotNull(vilkårsvurderingService.hentAktivForBehandling(behandling.id)) + val vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandling(behandling.id)!! + val barnPersonResultat = vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr } + val søkerPersonResultat = vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == søkerFnr } + vilkårsvurdering.personResultater = setOf(søkerPersonResultat, barnPersonResultat) + vilkårsvurderingService.oppdater(vilkårsvurdering) + + val behandlingEtterVilkårsvurdering = stegService.håndterVilkårsvurdering(behandling) + assertEquals(StegType.BEHANDLINGSRESULTAT, behandlingEtterVilkårsvurdering.steg) + + val behandlingEtterBehandlingsresultatSteg = + stegService.håndterBehandlingsresultat(behandlingEtterVilkårsvurdering) + assertEquals(StegType.VURDER_TILBAKEKREVING, behandlingEtterBehandlingsresultatSteg.steg) + + val behandlingEtterTilbakekrevingSteg = stegService.håndterVurderTilbakekreving( + behandlingEtterBehandlingsresultatSteg, + RestTilbakekreving( + valg = Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, + begrunnelse = "ignorer tilbakekreving", + ), + ) + assertEquals(StegType.SEND_TIL_BESLUTTER, behandlingEtterTilbakekrevingSteg.steg) + + val behandlingEtterSendTilBeslutterSteg = stegService.håndterSendTilBeslutter( + behandlingEtterTilbakekrevingSteg, + "1234", + ) + + assertEquals(StegType.FERDIGSTILLE_BEHANDLING, behandlingEtterSendTilBeslutterSteg.steg) + + assertTrue { + behandlingEtterSendTilBeslutterSteg.behandlingStegTilstand.any { + it.behandlingSteg == StegType.SEND_TIL_BESLUTTER && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + + assertTrue { + behandlingEtterSendTilBeslutterSteg.behandlingStegTilstand.any { + it.behandlingSteg == StegType.BESLUTTE_VEDTAK && + it.behandlingStegStatus == BehandlingStegStatus.UTFØRT + } + } + val totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(behandling.id) + assertNotNull(totrinnskontroll) + assertEquals(true, totrinnskontroll!!.godkjent) + assertEquals(SikkerhetContext.hentSaksbehandlerNavn(), totrinnskontroll.saksbehandler) + assertEquals(SikkerhetContext.hentSaksbehandler(), totrinnskontroll.saksbehandlerId) + assertEquals(SikkerhetContext.SYSTEM_NAVN, totrinnskontroll.beslutter) + assertEquals(SikkerhetContext.SYSTEM_FORKORTELSE, totrinnskontroll.beslutterId) + } + + private fun kjørGjennomStegInkludertVurderTilbakekreving(): Behandling { + val søkerFnr = randomFnr() + val barnFnr = ClientMocks.barnFnr[0] + + return kjørStegprosessForFGB( + tilSteg = StegType.VURDER_TILBAKEKREVING, + søkerFnr = søkerFnr, + barnasIdenter = listOf(barnFnr), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + } + + private fun assertMigreringsdato(migreringsdato: LocalDate, behandling: Behandling) { + assertEquals(migreringsdato, behandlingService.hentMigreringsdatoIBehandling(behandling.id)) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/Vilk\303\245rsvurderingForNyBehandlingServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/Vilk\303\245rsvurderingForNyBehandlingServiceTest.kt" new file mode 100644 index 000000000..23d5ab31b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/steg/Vilk\303\245rsvurderingForNyBehandlingServiceTest.kt" @@ -0,0 +1,1072 @@ +package no.nav.familie.ba.sak.kjerne.steg + +import no.nav.familie.ba.sak.common.BaseEntitet +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagEndretUtbetalingAndel +import no.nav.familie.ba.sak.common.lagInitiellTilkjentYtelse +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.til18ÅrsVilkårsdato +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagBarnVilkårResultat +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagSøkerVilkårResultat +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagVilkårsvurderingMedOverstyrendeResultater +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.TilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.EndretUtbetalingAndelRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.domene.PersonIdent +import no.nav.familie.ba.sak.kjerne.personident.Aktør +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import no.nav.familie.ba.sak.kjerne.personident.Personident +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.VilkårsvurderingForNyBehandlingService +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.VilkårsvurderingForNyBehandlingServiceTest.Companion.validerKopiertVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.gjelderAlltidFraBarnetsFødselsdato +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.YearMonth +import kotlin.reflect.full.declaredMemberProperties + +class VilkårsvurderingForNyBehandlingServiceTest( + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + + @Autowired + private val vilkårsvurderingService: VilkårsvurderingService, + + @Autowired + private val vilkårsvurderingForNyBehandlingService: VilkårsvurderingForNyBehandlingService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val tilkjentYtelseRepository: TilkjentYtelseRepository, + + @Autowired + private val endretUtbetalingAndelRepository: EndretUtbetalingAndelRepository, + + @Autowired + private val personRepository: PersonRepository, + + @Autowired + private val aktørIdRepository: AktørIdRepository, + +) : AbstractSpringIntegrationTest() { + @BeforeAll + fun init() { + databaseCleanupService.truncate() + } + + @Test + fun `skal lage vilkårsvurderingsperiode for migrering ved flyttesak`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val søkerFødselsdato = LocalDate.of(1984, 1, 14) + val barnetsFødselsdato = LocalDate.now().minusMonths(6) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val forrigeBehandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.MIGRERING, + ), + ) + val forrigePersonopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = forrigeBehandling.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(forrigePersonopplysningGrunnlag) + + var forrigeVilkårsvurdering = Vilkårsvurdering(behandling = forrigeBehandling) + val søkerPersonResultat = PersonResultat( + vilkårsvurdering = forrigeVilkårsvurdering, + aktør = personidentService.hentOgLagreAktør(fnr, true), + ) + søkerPersonResultat.setSortedVilkårResultater( + lagSøkerVilkårResultat( + søkerPersonResultat = søkerPersonResultat, + periodeFom = søkerFødselsdato, + behandlingId = forrigeBehandling.id, + ), + ) + val barnPersonResultat = PersonResultat( + vilkårsvurdering = forrigeVilkårsvurdering, + aktør = personidentService.hentOgLagreAktør(barnFnr, true), + ) + barnPersonResultat.setSortedVilkårResultater( + lagBarnVilkårResultat( + barnPersonResultat = barnPersonResultat, + barnetsFødselsdato = barnetsFødselsdato, + behandlingId = forrigeBehandling.id, + periodeFom = LocalDate.now().minusMonths(1), + flytteSak = true, + ), + ) + + forrigeVilkårsvurdering = forrigeVilkårsvurdering.apply { + personResultater = setOf( + søkerPersonResultat, + barnPersonResultat, + ) + } + vilkårsvurderingService.lagreNyOgDeaktiverGammel(forrigeVilkårsvurdering) + + markerBehandlingSomAvsluttet(forrigeBehandling) + + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + ), + ) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val nyMigreringsdato = LocalDate.now().minusMonths(5) + val vilkårsvurdering = + vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandling, + nyMigreringsdato = nyMigreringsdato, + ) + Assertions.assertTrue { vilkårsvurdering.personResultater.isNotEmpty() } + val søkerVilkårResultat = + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == fnr }.vilkårResultater + Assertions.assertTrue { søkerVilkårResultat.size == 2 } + Assertions.assertTrue { + søkerVilkårResultat.all { + it.periodeFom == søkerFødselsdato && + it.periodeTom == null + } + } + + val barnVilkårResultat = + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr }.vilkårResultater + Assertions.assertTrue { barnVilkårResultat.size == 5 } + Assertions.assertTrue { + barnVilkårResultat.filter { it.vilkårType == Vilkår.BOR_MED_SØKER }.all { + it.periodeFom == nyMigreringsdato && + it.periodeTom == null + } + } + Assertions.assertTrue { + barnVilkårResultat.filter { it.vilkårType == Vilkår.UNDER_18_ÅR }.all { + it.periodeFom == barnetsFødselsdato && + it.periodeTom == barnetsFødselsdato.til18ÅrsVilkårsdato() + } + } + + Assertions.assertTrue { + barnVilkårResultat.filter { it.vilkårType !in listOf(Vilkår.BOR_MED_SØKER, Vilkår.UNDER_18_ÅR) }.all { + it.periodeFom == barnetsFødselsdato && + it.periodeTom == null + } + } + } + + @Test + fun `skal lage vilkårsvurderingsperiode for migrering ved flere perioder`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val søkerFødselsdato = LocalDate.of(1984, 8, 1) + val barnetsFødselsdato = LocalDate.now().minusYears(10) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val forrigeBehandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.MIGRERING, + ), + ) + val forrigePersonopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = forrigeBehandling.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(forrigePersonopplysningGrunnlag) + + var forrigeVilkårsvurdering = Vilkårsvurdering(behandling = forrigeBehandling) + val søkerPersonResultat = PersonResultat( + vilkårsvurdering = forrigeVilkårsvurdering, + aktør = personidentService.hentOgLagreAktør(fnr, true), + ) + søkerPersonResultat.setSortedVilkårResultater( + lagSøkerVilkårResultat( + søkerPersonResultat = søkerPersonResultat, + periodeFom = søkerFødselsdato, + behandlingId = forrigeBehandling.id, + ), + ) + val barnPersonResultat = PersonResultat( + vilkårsvurdering = forrigeVilkårsvurdering, + aktør = personidentService.hentOgLagreAktør(barnFnr, true), + ) + barnPersonResultat.setSortedVilkårResultater( + setOf( + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.UNDER_18_ÅR, + resultat = Resultat.OPPFYLT, + periodeFom = barnetsFødselsdato, + periodeTom = barnetsFødselsdato.plusYears(18).minusMonths(1), + behandlingId = forrigeBehandling.id, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.GIFT_PARTNERSKAP, + resultat = Resultat.OPPFYLT, + periodeFom = barnetsFødselsdato, + periodeTom = null, + behandlingId = forrigeBehandling.id, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2021, 4, 14), + periodeTom = LocalDate.of(2021, 8, 16), + behandlingId = forrigeBehandling.id, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2021, 10, 5), + periodeTom = null, + behandlingId = forrigeBehandling.id, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = barnetsFødselsdato, + periodeTom = null, + behandlingId = forrigeBehandling.id, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + periodeFom = barnetsFødselsdato, + periodeTom = null, + behandlingId = forrigeBehandling.id, + ), + ), + ) + forrigeVilkårsvurdering = forrigeVilkårsvurdering.apply { + personResultater = setOf( + søkerPersonResultat, + barnPersonResultat, + ) + } + vilkårsvurderingService.lagreNyOgDeaktiverGammel(forrigeVilkårsvurdering) + + markerBehandlingSomAvsluttet(forrigeBehandling) + + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + ), + ) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val nyMigreringsdato = LocalDate.of(2021, 1, 1) + val vilkårsvurdering = + vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandling, + nyMigreringsdato = nyMigreringsdato, + ) + Assertions.assertTrue { vilkårsvurdering.personResultater.isNotEmpty() } + val søkerVilkårResultat = + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == fnr }.vilkårResultater + Assertions.assertTrue { søkerVilkårResultat.size == 2 } + Assertions.assertTrue { + søkerVilkårResultat.all { + it.periodeFom == søkerFødselsdato && + it.periodeTom == null + } + } + + val barnVilkårResultat = + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr }.vilkårResultater + Assertions.assertTrue { barnVilkårResultat.size == 6 } + Assertions.assertTrue { + barnVilkårResultat.filter { it.vilkårType == Vilkår.BOR_MED_SØKER }.any { + it.periodeFom == nyMigreringsdato && + it.periodeTom == LocalDate.of(2021, 8, 16) + } + } + Assertions.assertTrue { + barnVilkårResultat.filter { it.vilkårType == Vilkår.BOR_MED_SØKER }.any { + it.periodeFom == LocalDate.of(2021, 10, 5) && + it.periodeTom == null + } + } + Assertions.assertTrue { + barnVilkårResultat.filter { it.vilkårType == Vilkår.UNDER_18_ÅR }.all { + it.periodeFom == barnetsFødselsdato && + it.periodeTom == barnetsFødselsdato.til18ÅrsVilkårsdato() + } + } + Assertions.assertTrue { + barnVilkårResultat.filter { it.vilkårType !in listOf(Vilkår.BOR_MED_SØKER, Vilkår.UNDER_18_ÅR) }.all { + it.periodeFom == barnetsFødselsdato && + it.periodeTom == null + } + } + } + + @Test + fun `genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato - skal ikke vurdere VilkårResultater som ikke er oppfylt fra forrige behandling ved kopiering til nye VilkårResultater`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val søkerFødselsdato = LocalDate.of(1984, 8, 1) + val barnetsFødselsdato = LocalDate.now().minusYears(10) + val nyMigreringsdato = LocalDate.now().minusYears(5) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val forrigeBehandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.MIGRERING, + ), + ) + val forrigePersonopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = forrigeBehandling.id, + søkerPersonIdent = søkerFnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerFødselsdato = søkerFødselsdato, + søkerAktør = personidentService.hentOgLagreAktør(søkerFnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(forrigePersonopplysningGrunnlag) + + var forrigeVilkårsvurdering = Vilkårsvurdering(behandling = forrigeBehandling) + val søkerPersonResultat = PersonResultat( + vilkårsvurdering = forrigeVilkårsvurdering, + aktør = personidentService.hentOgLagreAktør(søkerFnr, true), + ) + søkerPersonResultat.setSortedVilkårResultater( + lagSøkerVilkårResultat( + søkerPersonResultat = søkerPersonResultat, + periodeFom = nyMigreringsdato.plusYears(1), + behandlingId = forrigeBehandling.id, + ), + ) + val barnPersonResultat = PersonResultat( + vilkårsvurdering = forrigeVilkårsvurdering, + aktør = personidentService.hentOgLagreAktør(barnFnr, true), + ) + barnPersonResultat.setSortedVilkårResultater( + setOf( + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.UNDER_18_ÅR, + resultat = Resultat.OPPFYLT, + periodeFom = barnetsFødselsdato, + periodeTom = barnetsFødselsdato.plusYears(18).minusMonths(1), + behandlingId = forrigeBehandling.id, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.GIFT_PARTNERSKAP, + resultat = Resultat.OPPFYLT, + periodeFom = nyMigreringsdato.plusYears(1), + periodeTom = null, + behandlingId = forrigeBehandling.id, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + periodeFom = nyMigreringsdato.plusYears(1), + periodeTom = nyMigreringsdato.plusYears(1).plusMonths(4), + behandlingId = forrigeBehandling.id, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = null, + periodeTom = null, + behandlingId = forrigeBehandling.id, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.BOSATT_I_RIKET, + resultat = Resultat.OPPFYLT, + periodeFom = nyMigreringsdato.plusYears(1), + periodeTom = null, + behandlingId = forrigeBehandling.id, + ), + lagVilkårResultat( + personResultat = barnPersonResultat, + vilkårType = Vilkår.LOVLIG_OPPHOLD, + resultat = Resultat.OPPFYLT, + periodeFom = nyMigreringsdato.plusYears(1), + periodeTom = null, + behandlingId = forrigeBehandling.id, + ), + ), + ) + forrigeVilkårsvurdering = forrigeVilkårsvurdering.apply { + personResultater = setOf( + søkerPersonResultat, + barnPersonResultat, + ) + } + vilkårsvurderingService.lagreNyOgDeaktiverGammel(forrigeVilkårsvurdering) + + markerBehandlingSomAvsluttet(forrigeBehandling) + + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + ), + ) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søkerFnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerFødselsdato = søkerFødselsdato, + søkerAktør = personidentService.hentOgLagreAktør(søkerFnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val vilkårsvurdering = + vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandling, + nyMigreringsdato = nyMigreringsdato, + ) + Assertions.assertTrue { vilkårsvurdering.personResultater.isNotEmpty() } + val søkerVilkårResultat = + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == søkerFnr }.vilkårResultater + Assertions.assertTrue { søkerVilkårResultat.size == 2 } + Assertions.assertTrue { + søkerVilkårResultat.all { + it.periodeFom == nyMigreringsdato && + it.periodeTom == null + } + } + + val barnVilkårResultat = + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr }.vilkårResultater + Assertions.assertTrue { barnVilkårResultat.size == 5 } // IKKE_OPPFYLT vilkår skal ikke kopieres over + Assertions.assertTrue { + barnVilkårResultat.filter { it.vilkårType == Vilkår.BOR_MED_SØKER }.any { + it.periodeFom == nyMigreringsdato && + it.periodeTom == nyMigreringsdato.plusYears(1).plusMonths(4) + } + } + Assertions.assertTrue { + barnVilkårResultat.filter { it.vilkårType == Vilkår.UNDER_18_ÅR }.all { + it.periodeFom == barnetsFødselsdato && + it.periodeTom == barnetsFødselsdato.til18ÅrsVilkårsdato() + } + } + Assertions.assertTrue { + barnVilkårResultat.filter { it.vilkårType == Vilkår.GIFT_PARTNERSKAP }.all { + it.periodeFom == barnetsFødselsdato && + it.periodeTom == null + } + } + Assertions.assertTrue { + barnVilkårResultat.filter { + it.vilkårType !in listOf( + Vilkår.GIFT_PARTNERSKAP, + Vilkår.UNDER_18_ÅR, + Vilkår.BOR_MED_SØKER, + ) + }.all { + it.periodeFom == nyMigreringsdato && + it.periodeTom == null + } + } + } + + @Test + fun `genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato - skal kopiere over alle felter, inkludert UtdypendeVilkårsvurdering, med unntak av fom og tom for VilkårResultatene som blir forskjøvet av ny migreringsdato`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val søkerFødselsdato = LocalDate.of(1984, 8, 1) + val barnetsFødselsdato = LocalDate.now().minusYears(10) + val nyMigreringsdato = LocalDate.now().minusYears(5) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val forrigeBehandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.MIGRERING, + ), + ) + val forrigePersonopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = forrigeBehandling.id, + søkerPersonIdent = søkerFnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerFødselsdato = søkerFødselsdato, + søkerAktør = personidentService.hentOgLagreAktør(søkerFnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(forrigePersonopplysningGrunnlag) + + var forrigeVilkårsvurdering = Vilkårsvurdering(behandling = forrigeBehandling) + val søkerPersonResultat = PersonResultat( + vilkårsvurdering = forrigeVilkårsvurdering, + aktør = personidentService.hentOgLagreAktør(søkerFnr, true), + ) + val deltBostedTom = nyMigreringsdato.plusYears(2) + val deltBostedBegrunnelse = "Dette er en kopiert begrunnelse" + søkerPersonResultat.setSortedVilkårResultater( + lagSøkerVilkårResultat( + søkerPersonResultat = søkerPersonResultat, + periodeFom = nyMigreringsdato.plusYears(1), + behandlingId = forrigeBehandling.id, + ).plus( + lagVilkårResultat( + personResultat = søkerPersonResultat, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = nyMigreringsdato.plusMonths(5), + periodeTom = deltBostedTom, + behandlingId = forrigeBehandling.id, + begrunnelse = deltBostedBegrunnelse, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + ), + ) + val barnPersonResultat = PersonResultat( + vilkårsvurdering = forrigeVilkårsvurdering, + aktør = personidentService.hentOgLagreAktør(barnFnr, true), + ) + barnPersonResultat.setSortedVilkårResultater( + lagBarnVilkårResultat( + barnPersonResultat = barnPersonResultat, + barnetsFødselsdato = barnetsFødselsdato, + behandlingId = forrigeBehandling.id, + periodeFom = nyMigreringsdato.plusMonths(5), + ), + ) + forrigeVilkårsvurdering = forrigeVilkårsvurdering.apply { + personResultater = setOf( + søkerPersonResultat, + barnPersonResultat, + ) + } + vilkårsvurderingService.lagreNyOgDeaktiverGammel(forrigeVilkårsvurdering) + + markerBehandlingSomAvsluttet(forrigeBehandling) + + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + ), + ) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søkerFnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerFødselsdato = søkerFødselsdato, + søkerAktør = personidentService.hentOgLagreAktør(søkerFnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val vilkårsvurdering = + vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandling, + nyMigreringsdato = nyMigreringsdato, + ) + assertThat(vilkårsvurdering.personResultater).isNotEmpty + val søkerVilkårResultat = + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == søkerFnr }.vilkårResultater + assertThat(søkerVilkårResultat).hasSize(3) + val utvidetBarnetrygdVilkår = søkerVilkårResultat.single { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + assertThat(utvidetBarnetrygdVilkår.utdypendeVilkårsvurderinger.single()).isEqualTo( + UtdypendeVilkårsvurdering.DELT_BOSTED, + ) + assertThat(utvidetBarnetrygdVilkår.periodeFom).isEqualTo(nyMigreringsdato) + assertThat(utvidetBarnetrygdVilkår.periodeTom).isEqualTo(deltBostedTom) + assertThat(utvidetBarnetrygdVilkår.begrunnelse).isEqualTo(deltBostedBegrunnelse) + } + + @Test + fun `genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato - skal kunne identifisere og kopiere forkjøvede vilkår som alltid starter fra fødselsdato når aktør har fått ny fødselsdato siden forrige behandling`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val søkerFødselsdato = LocalDate.of(1984, 8, 1) + val barnetsFødselsdato = LocalDate.now().minusYears(10) + val nyMigreringsdato = LocalDate.now().minusYears(5) + + val søkerAktør = personidentService.hentOgLagreAktør(søkerFnr, true) + val barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val forrigeBehandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.MIGRERING, + ), + ) + val forrigePersonopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = forrigeBehandling.id, + søkerPersonIdent = søkerFnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerFødselsdato = søkerFødselsdato, + søkerAktør = søkerAktør, + barnAktør = barnAktør, + ) + persongrunnlagService.lagreOgDeaktiverGammel(forrigePersonopplysningGrunnlag) + + var forrigeVilkårsvurdering = Vilkårsvurdering(behandling = forrigeBehandling) + val søkerPersonResultat = PersonResultat( + vilkårsvurdering = forrigeVilkårsvurdering, + aktør = personidentService.hentOgLagreAktør(søkerFnr, true), + ) + val deltBostedTom = nyMigreringsdato.plusYears(2) + val deltBostedBegrunnelse = "Dette er en kopiert begrunnelse" + søkerPersonResultat.setSortedVilkårResultater( + lagSøkerVilkårResultat( + søkerPersonResultat = søkerPersonResultat, + periodeFom = nyMigreringsdato.plusYears(1), + behandlingId = forrigeBehandling.id, + ).plus( + lagVilkårResultat( + personResultat = søkerPersonResultat, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = nyMigreringsdato.plusMonths(5), + periodeTom = deltBostedTom, + behandlingId = forrigeBehandling.id, + begrunnelse = deltBostedBegrunnelse, + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + ), + ) + val barnPersonResultat = PersonResultat( + vilkårsvurdering = forrigeVilkårsvurdering, + aktør = personidentService.hentOgLagreAktør(barnFnr, true), + ) + barnPersonResultat.setSortedVilkårResultater( + lagBarnVilkårResultat( + barnPersonResultat = barnPersonResultat, + barnetsFødselsdato = barnetsFødselsdato, + behandlingId = forrigeBehandling.id, + periodeFom = nyMigreringsdato.plusMonths(5), + ), + ) + forrigeVilkårsvurdering = forrigeVilkårsvurdering.apply { + personResultater = setOf( + søkerPersonResultat, + barnPersonResultat, + ) + } + vilkårsvurderingService.lagreNyOgDeaktiverGammel(forrigeVilkårsvurdering) + + markerBehandlingSomAvsluttet(forrigeBehandling) + + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + ), + ) + // val oppdatertBarnAktør = listOf(leggTilNyIdentPåAktør(barnAktør.first(), randomFnr())) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = søkerFnr, + barnasIdenter = listOf(barnFnr), + // Justerer barnets fødselsdato slik at eksisterende vilkår for UNDER_18 og GIFT_PARNTERSKAP får "feil" fom og tom + barnasFødselsdatoer = listOf(barnetsFødselsdato.minusMonths(2)), + søkerFødselsdato = søkerFødselsdato, + søkerAktør = søkerAktør, + barnAktør = barnAktør, + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val vilkårsvurdering = + vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandling, + nyMigreringsdato = nyMigreringsdato, + ) + assertThat(vilkårsvurdering.personResultater).isNotEmpty + val søkerVilkårResultat = + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == søkerFnr }.vilkårResultater + assertThat(søkerVilkårResultat).hasSize(3) + val barnVilkårResultat = + vilkårsvurdering.personResultater.first { + it.aktør.aktivFødselsnummer() == barnAktør.first().aktivFødselsnummer() + }.vilkårResultater + assertThat(barnVilkårResultat).hasSize(5) + assertThat( + barnVilkårResultat.filter { it.vilkårType.gjelderAlltidFraBarnetsFødselsdato() } + .all { it.periodeFom == personopplysningGrunnlag.barna.first().fødselsdato }, + ) + } + + @Test + fun `skal lage vilkårsvurderingsperiode for helmanuell migrering`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val barnetsFødselsdato = LocalDate.of(2020, 8, 15) + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + ), + ) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val nyMigreringsdato = LocalDate.of(2021, 1, 1) + val vilkårsvurdering = vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForHelmanuellMigrering( + behandling, + nyMigreringsdato, + ) + + Assertions.assertTrue { vilkårsvurdering.personResultater.isNotEmpty() } + Assertions.assertTrue { vilkårsvurdering.personResultater.size == 2 } + + val søkerPersonResultat = vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == fnr } + Assertions.assertTrue { søkerPersonResultat.vilkårResultater.isNotEmpty() } + Assertions.assertTrue { søkerPersonResultat.vilkårResultater.size == 2 } + Assertions.assertTrue { + søkerPersonResultat.vilkårResultater.all { + it.periodeTom == null && + it.periodeFom == nyMigreringsdato + } + } + + val barnPersonResultat = vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr } + Assertions.assertTrue { barnPersonResultat.vilkårResultater.isNotEmpty() } + Assertions.assertTrue { barnPersonResultat.vilkårResultater.size == 5 } + Assertions.assertTrue { + barnPersonResultat.vilkårResultater.any { + it.vilkårType == Vilkår.UNDER_18_ÅR && + it.periodeTom == barnetsFødselsdato.til18ÅrsVilkårsdato() && + it.periodeFom == barnetsFødselsdato + } + } + Assertions.assertTrue { + barnPersonResultat.vilkårResultater.any { + it.vilkårType == Vilkår.GIFT_PARTNERSKAP && + it.periodeTom == null && + it.periodeFom == barnetsFødselsdato + } + } + Assertions.assertTrue { + barnPersonResultat.vilkårResultater.filter { !it.vilkårType.gjelderAlltidFraBarnetsFødselsdato() }.all { + it.periodeTom == null && + it.periodeFom == nyMigreringsdato + } + } + } + + @Test + fun `skal lage vilkårsvurderingsperiode for helmanuell migrering med migreringsdato før barnetsfødselsdato`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val barnetsFødselsdato = LocalDate.of(2021, 8, 15) + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + ), + ) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val nyMigreringsdato = LocalDate.of(2021, 1, 1) + val vilkårsvurdering = vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForHelmanuellMigrering( + behandling, + nyMigreringsdato, + ) + + Assertions.assertTrue { vilkårsvurdering.personResultater.isNotEmpty() } + Assertions.assertTrue { vilkårsvurdering.personResultater.size == 2 } + + val søkerPersonResultat = vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == fnr } + Assertions.assertTrue { søkerPersonResultat.vilkårResultater.isNotEmpty() } + Assertions.assertTrue { søkerPersonResultat.vilkårResultater.size == 2 } + Assertions.assertTrue { + søkerPersonResultat.vilkårResultater.all { + it.periodeTom == null && + it.periodeFom == nyMigreringsdato + } + } + + val barnPersonResultat = vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr } + Assertions.assertTrue { barnPersonResultat.vilkårResultater.isNotEmpty() } + Assertions.assertTrue { barnPersonResultat.vilkårResultater.size == 5 } + Assertions.assertTrue { + barnPersonResultat.vilkårResultater.any { + it.vilkårType == Vilkår.UNDER_18_ÅR && + it.periodeTom == barnetsFødselsdato.til18ÅrsVilkårsdato() && + it.periodeFom == barnetsFødselsdato + } + } + Assertions.assertTrue { + barnPersonResultat.vilkårResultater.any { + it.vilkårType == Vilkår.GIFT_PARTNERSKAP && + it.periodeTom == null && + it.periodeFom == barnetsFødselsdato + } + } + Assertions.assertTrue { + barnPersonResultat.vilkårResultater.filter { !it.vilkårType.gjelderAlltidFraBarnetsFødselsdato() }.all { + it.periodeTom == null && + it.periodeFom == barnetsFødselsdato + } + } + } + + @Test + fun `skal kopiere vilkårsvurdering og endrede utbetalingsandeler fra forrige behandling ved satsendring`() { + val søkerFnr = randomFnr() + val barnFnr = randomFnr() + val søkerAktør = personidentService.hentOgLagreAktør(søkerFnr, true) + val barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true) + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(søkerFnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak = BehandlingÅrsak.SØKNAD, + ), + ) + + val personopplysningGrunnlag = persongrunnlagService.lagreOgDeaktiverGammel( + PersonopplysningGrunnlag( + behandlingId = behandling.id, + ), + ) + + val søker = personRepository.saveAndFlush( + lagPerson( + personIdent = PersonIdent(søkerFnr), + type = PersonType.SØKER, + aktør = søkerAktør, + personopplysningGrunnlag = personopplysningGrunnlag, + ), + ) + val barn = personRepository.saveAndFlush( + lagPerson( + personIdent = PersonIdent(barnFnr), + type = PersonType.BARN, + aktør = barnAktør[0], + personopplysningGrunnlag = personopplysningGrunnlag, + ), + ) + + val vilkårsvurdering = + lagVilkårsvurderingMedOverstyrendeResultater( + søker = søker, + barna = listOf(barn), + behandling = behandling, + overstyrendeVilkårResultater = emptyMap(), + ) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering) + + val endredeUtbetalingsAndeler = listOf( + lagEndretUtbetalingAndel( + behandlingId = behandling.id, + barn = barn, + fom = YearMonth.now(), + tom = YearMonth.now().plusMonths(6), + prosent = 50, + ), + lagEndretUtbetalingAndel( + behandlingId = behandling.id, + barn = barn, + fom = YearMonth.now().plusMonths(7), + tom = YearMonth.now().plusMonths(12), + prosent = 100, + ), + ) + + endretUtbetalingAndelRepository.saveAllAndFlush(endredeUtbetalingsAndeler) + + tilkjentYtelseRepository.saveAndFlush(lagInitiellTilkjentYtelse(behandling, "")) + + markerBehandlingSomAvsluttet(behandling) + + val behandling2 = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.REVURDERING, + årsak = BehandlingÅrsak.SATSENDRING, + ), + ) + + val personopplysningGrunnlagB2 = persongrunnlagService.lagreOgDeaktiverGammel( + PersonopplysningGrunnlag( + behandlingId = behandling2.id, + ), + ) + val søkerB2 = lagPerson( + personIdent = PersonIdent(søkerFnr), + type = PersonType.SØKER, + aktør = søkerAktør, + personopplysningGrunnlag = personopplysningGrunnlagB2, + ) + val barnB2 = lagPerson( + personIdent = PersonIdent(barnFnr), + type = PersonType.BARN, + aktør = barnAktør[0], + personopplysningGrunnlag = personopplysningGrunnlagB2, + ) + + personopplysningGrunnlagB2.personer.addAll(listOf(søkerB2, barnB2)) + personopplysningGrunnlagRepository.save(personopplysningGrunnlagB2) + + val forventetVilkårsvurdering = + lagVilkårsvurderingMedOverstyrendeResultater( + søker = søkerB2, + barna = listOf(barnB2), + behandling = behandling2, + overstyrendeVilkårResultater = emptyMap(), + ) + + vilkårsvurderingForNyBehandlingService.opprettVilkårsvurderingUtenomHovedflyt(behandling2, behandling) + + val kopiertVilkårsvurdering = vilkårsvurderingForNyBehandlingService.hentVilkårsvurderingThrows(behandling2.id) + + val kopiertEndredeUtbetalingsandeler = endretUtbetalingAndelRepository.findByBehandlingId(behandling2.id) + + val baseEntitetFelter = + BaseEntitet::class.declaredMemberProperties.map { it.name }.toTypedArray() + + assertThat(kopiertEndredeUtbetalingsandeler.size).isEqualTo(endredeUtbetalingsAndeler.size) + assertThat(kopiertEndredeUtbetalingsandeler).usingRecursiveFieldByFieldElementComparatorIgnoringFields( + "id", + "behandlingId", + "person", + *baseEntitetFelter, + ) + .isEqualTo(endredeUtbetalingsAndeler) + + assertThat(kopiertEndredeUtbetalingsandeler.all { kopiertEua -> endredeUtbetalingsAndeler.any { eua -> eua.person!!.aktør.aktørId == kopiertEua.person!!.aktør.aktørId } }).isTrue + + validerKopiertVilkårsvurdering(kopiertVilkårsvurdering, vilkårsvurdering, forventetVilkårsvurdering) + } + + private fun markerBehandlingSomAvsluttet(behandling: Behandling): Behandling { + behandling.status = BehandlingStatus.AVSLUTTET + behandling.leggTilBehandlingStegTilstand(StegType.BEHANDLING_AVSLUTTET) + return behandlingHentOgPersisterService.lagreOgFlush(behandling) + } + + private fun leggTilNyIdentPåAktør(aktør: Aktør, nyttFnr: String): Aktør { + aktør.personidenter.filter { it.aktiv }.map { + it.aktiv = false + it.gjelderTil = LocalDateTime.now() + } + val oppdatertAktør = aktørIdRepository.saveAndFlush(aktør) + oppdatertAktør.personidenter.add( + Personident(fødselsnummer = nyttFnr, aktør = oppdatertAktør), + ) + return aktørIdRepository.saveAndFlush(oppdatertAktør) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingServiceTest.kt new file mode 100644 index 000000000..6e63d033f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/tilbakekreving/TilbakekrevingServiceTest.kt @@ -0,0 +1,204 @@ +package no.nav.familie.ba.sak.kjerne.tilbakekreving + +import no.nav.familie.ba.sak.common.kjørStegprosessForFGB +import no.nav.familie.ba.sak.common.opprettRestTilbakekreving +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.ekstern.restDomene.InstitusjonInfo +import no.nav.familie.ba.sak.ekstern.restDomene.VergeInfo +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.brev.mottaker.Brevmottaker +import no.nav.familie.ba.sak.kjerne.brev.mottaker.BrevmottakerRepository +import no.nav.familie.ba.sak.kjerne.brev.mottaker.MottakerType +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.tilbakekreving.domene.TilbakekrevingRepository +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Vergetype +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import org.junit.jupiter.params.provider.ArgumentsSource +import org.springframework.beans.factory.annotation.Autowired +import java.util.Properties +import java.util.stream.Stream +import no.nav.familie.kontrakter.felles.tilbakekreving.Brevmottaker as TilbakekrevingBrevmottaker + +class TilbakekrevingServiceTest( + @Autowired private val vilkårsvurderingService: VilkårsvurderingService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val persongrunnlagService: PersongrunnlagService, + @Autowired private val fagsakService: FagsakService, + @Autowired private val stegService: StegService, + @Autowired private val tilbakekrevingService: TilbakekrevingService, + @Autowired private val tilbakekrevingRepository: TilbakekrevingRepository, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, + @Autowired private val databaseCleanupService: DatabaseCleanupService, + @Autowired private val brevmalService: BrevmalService, + @Autowired private val brevmottakerRepository: BrevmottakerRepository, +) : AbstractSpringIntegrationTest() { + + @BeforeEach + fun init() { + databaseCleanupService.truncate() + } + + @Test + @Tag("integration") + fun `tilbakekreving skal bli OPPRETT_TILBAKEKREVING_MED_VARSEL når man oppretter tilbakekreving med varsel`() { + val behandling = kjørStegprosessForFGB( + tilSteg = StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI, + søkerFnr = randomFnr(), + barnasIdenter = listOf(ClientMocks.barnFnr[0]), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val restTilbakekreving = opprettRestTilbakekreving() + tilbakekrevingService.validerRestTilbakekreving(restTilbakekreving, behandling.id) + tilbakekrevingService.lagreTilbakekreving(restTilbakekreving, behandling.id) + + stegService.håndterIverksettMotFamilieTilbake(behandling, Properties()) + + val tilbakekreving = tilbakekrevingRepository.findByBehandlingId(behandling.id) + + assertEquals(Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, tilbakekreving?.valg) + assertEquals("id1", tilbakekreving?.tilbakekrevingsbehandlingId) + assertEquals("Varsel", tilbakekreving?.varsel) + } + + @Test + @Tag("integration") + fun `tilbakekreving skal bli OPPRETT_TILBAKEKREVING_MED_VARSEL når man oppretter tilbakekreving med varsel for institusjon`() { + val behandling = kjørStegprosessForFGB( + tilSteg = StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI, + søkerFnr = "09121079074", + barnasIdenter = listOf("09121079074"), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + institusjon = InstitusjonInfo(orgNummer = "998765432", tssEksternId = "8000000"), + brevmalService = brevmalService, + ) + + val restTilbakekreving = opprettRestTilbakekreving() + tilbakekrevingService.validerRestTilbakekreving(restTilbakekreving, behandling.id) + tilbakekrevingService.lagreTilbakekreving(restTilbakekreving, behandling.id) + + stegService.håndterIverksettMotFamilieTilbake(behandling, Properties()) + + val tilbakekreving = tilbakekrevingRepository.findByBehandlingId(behandling.id) + + assertEquals(Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, tilbakekreving?.valg) + assertEquals("id1", tilbakekreving?.tilbakekrevingsbehandlingId) + assertEquals("Varsel", tilbakekreving?.varsel) + } + + @Test + @Tag("integration") + fun `tilbakekreving skal bli OPPRETT_TILBAKEKREVING_MED_VARSEL når man oppretter tilbakekreving med varsel for mindreårig med verge`() { + val behandling = kjørStegprosessForFGB( + tilSteg = StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI, + søkerFnr = "10031000033", + barnasIdenter = listOf("10031000033"), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + verge = VergeInfo("04068203010"), + brevmalService = brevmalService, + ) + + val restTilbakekreving = opprettRestTilbakekreving() + tilbakekrevingService.validerRestTilbakekreving(restTilbakekreving, behandling.id) + tilbakekrevingService.lagreTilbakekreving(restTilbakekreving, behandling.id) + + stegService.håndterIverksettMotFamilieTilbake(behandling, Properties()) + + val tilbakekreving = tilbakekrevingRepository.findByBehandlingId(behandling.id) + + assertEquals(Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, tilbakekreving?.valg) + assertEquals("id1", tilbakekreving?.tilbakekrevingsbehandlingId) + assertEquals("Varsel", tilbakekreving?.varsel) + } + + @Tag("integration") + @ParameterizedTest + @ArgumentsSource(TestProvider::class) + @Suppress("SENSELESS_COMPARISON") + fun `lagOpprettTilbakekrevingRequest sender brevmottakere i kall mot familie-tilbake`(arguments: Triple) { + val behandling = kjørStegprosessForFGB( + tilSteg = StegType.VENTE_PÅ_STATUS_FRA_ØKONOMI, + søkerFnr = if (arguments.first != null) ClientMocks.barnFnr[0] else randomFnr(), + barnasIdenter = listOf(ClientMocks.barnFnr[0]), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + verge = arguments.first, + ) + + val brevmottaker = Brevmottaker( + behandlingId = behandling.id, + type = arguments.second, + navn = "Donald Duck", + adresselinje1 = "Andebyveien 1", + postnummer = "0000", + poststed = "OSLO", + landkode = "NO", + ) + brevmottakerRepository.saveAndFlush(brevmottaker) + + val opprettTilbakekrevingRequest = tilbakekrevingService.lagOpprettTilbakekrevingRequest(behandling) + assertEquals(1, opprettTilbakekrevingRequest.manuelleBrevmottakere.size) + val actualBrevmottaker = opprettTilbakekrevingRequest.manuelleBrevmottakere.first() + + assertBrevmottakerEquals(brevmottaker, actualBrevmottaker) + assertEquals(arguments.third, actualBrevmottaker.vergetype) + } + + private fun assertBrevmottakerEquals(expected: Brevmottaker, actual: TilbakekrevingBrevmottaker) { + assertEquals(expected.navn, actual.navn) + assertEquals(expected.type.name, actual.type.name) + assertEquals(expected.adresselinje1, actual.manuellAdresseInfo?.adresselinje1) + assertEquals(expected.postnummer, actual.manuellAdresseInfo?.postnummer) + assertEquals(expected.poststed, actual.manuellAdresseInfo?.poststed) + assertEquals(expected.landkode, actual.manuellAdresseInfo?.landkode) + } + + private class TestProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream { + return Stream.of( + Arguments.of(Triple(null, MottakerType.FULLMEKTIG, Vergetype.ANNEN_FULLMEKTIG)), + Arguments.of(Triple(null, MottakerType.VERGE, Vergetype.VERGE_FOR_VOKSEN)), + Arguments.of(Triple(VergeInfo("12345678910"), MottakerType.VERGE, Vergetype.VERGE_FOR_BARN)), + Arguments.of(Triple(null, MottakerType.BRUKER_MED_UTENLANDSK_ADRESSE, null)), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/totrinnskontroll/TotrinnskontrollTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/totrinnskontroll/TotrinnskontrollTest.kt new file mode 100644 index 000000000..40f8c8ec2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/totrinnskontroll/TotrinnskontrollTest.kt @@ -0,0 +1,162 @@ +package no.nav.familie.ba.sak.kjerne.totrinnskontroll + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.nyOrdinærBehandling +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.domene.Totrinnskontroll +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringRepository +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringType +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +class TotrinnskontrollTest( + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @Autowired + private val totrinnskontrollService: TotrinnskontrollService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val saksstatistikkMellomlagringRepository: SaksstatistikkMellomlagringRepository, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, +) : AbstractSpringIntegrationTest() { + + @BeforeAll + fun init() { + databaseCleanupService.truncate() + } + + @Test + @Tag("integration") + fun `Skal godkjenne 2 trinnskontroll`() { + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = + behandlingService.opprettBehandling(nyOrdinærBehandling(søkersIdent = fnr, fagsakId = fagsak.id)) + + behandlingService.sendBehandlingTilBeslutter(behandling) + assertEquals(BehandlingStatus.FATTER_VEDTAK, behandlingHentOgPersisterService.hent(behandling.id).status) + assertThat( + saksstatistikkMellomlagringRepository.findByTypeAndTypeId( + SaksstatistikkMellomlagringType.BEHANDLING, + behandling.id, + ), + ) + .hasSize(2) + assertThat( + saksstatistikkMellomlagringRepository.findByTypeAndTypeId( + SaksstatistikkMellomlagringType.BEHANDLING, + behandling.id, + ) + .last().jsonToBehandlingDVH().behandlingStatus, + ).isEqualTo(BehandlingStatus.FATTER_VEDTAK.name) + + totrinnskontrollService.opprettTotrinnskontrollMedSaksbehandler(behandling = behandling) + + totrinnskontrollService.besluttTotrinnskontroll(behandling, "Beslutter", "beslutterId", Beslutning.GODKJENT) + + assertEquals(BehandlingStatus.IVERKSETTER_VEDTAK, behandlingHentOgPersisterService.hent(behandling.id).status) + + assertThat( + saksstatistikkMellomlagringRepository.findByTypeAndTypeId( + SaksstatistikkMellomlagringType.BEHANDLING, + behandling.id, + ), + ) + .hasSize(3) + assertThat( + saksstatistikkMellomlagringRepository.findByTypeAndTypeId( + SaksstatistikkMellomlagringType.BEHANDLING, + behandling.id, + ) + .last().jsonToBehandlingDVH().behandlingStatus, + ).isEqualTo(BehandlingStatus.IVERKSETTER_VEDTAK.name) + + val totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(behandlingId = behandling.id)!! + assertTrue(totrinnskontroll.godkjent) + } + + @Test + @Tag("integration") + fun `Skal underkjenne 2 trinnskontroll`() { + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = + behandlingService.opprettBehandling(nyOrdinærBehandling(søkersIdent = fnr, fagsakId = fagsak.id)) + + behandlingService.sendBehandlingTilBeslutter(behandling) + assertEquals(BehandlingStatus.FATTER_VEDTAK, behandlingHentOgPersisterService.hent(behandling.id).status) + + totrinnskontrollService.opprettTotrinnskontrollMedSaksbehandler(behandling = behandling) + totrinnskontrollService.besluttTotrinnskontroll(behandling, "Beslutter", "beslutterId", Beslutning.UNDERKJENT) + assertEquals(BehandlingStatus.UTREDES, behandlingHentOgPersisterService.hent(behandling.id).status) + assertThat( + saksstatistikkMellomlagringRepository.findByTypeAndTypeId( + SaksstatistikkMellomlagringType.BEHANDLING, + behandling.id, + ), + ) + .hasSize(3) + assertThat( + saksstatistikkMellomlagringRepository.findByTypeAndTypeId( + SaksstatistikkMellomlagringType.BEHANDLING, + behandling.id, + ) + .last().jsonToBehandlingDVH().behandlingStatus, + ).isEqualTo(BehandlingStatus.UTREDES.name) + + val totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(behandlingId = behandling.id)!! + assertFalse(totrinnskontroll.godkjent) + } + + @Test + fun `Skal ikke kunne godkjenne eget vedtak`() { + val totrinnskontroll = Totrinnskontroll( + behandling = lagBehandling(), + saksbehandler = "Mock Saksbehandler", + saksbehandlerId = "Mock.Saksbehandler", + beslutter = "Mock Saksbehandler", + beslutterId = "Mock.Saksbehandler", + godkjent = true, + ) + + assertTrue(totrinnskontroll.erUgyldig()) + } + + @Test + fun `Skal kunne underkjenne eget vedtak`() { + val totrinnskontroll = Totrinnskontroll( + behandling = lagBehandling(), + saksbehandler = "Mock Saksbehandler", + saksbehandlerId = "Mock.Saksbehandler", + beslutter = "Mock Saksbehandler", + beslutterId = "Mock.Saksbehandler", + godkjent = false, + ) + + assertFalse(totrinnskontroll.erUgyldig()) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/VedtakServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/VedtakServiceTest.kt new file mode 100644 index 000000000..27101dfde --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/VedtakServiceTest.kt @@ -0,0 +1,281 @@ +package no.nav.familie.ba.sak.kjerne.vedtak + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.TaskRepositoryWrapper +import no.nav.familie.ba.sak.integrasjoner.infotrygd.InfotrygdService +import no.nav.familie.ba.sak.kjerne.arbeidsfordeling.ArbeidsfordelingService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingMetrikker +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.BehandlingstemaService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingMigreringsinfoRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingSøknadsinfoRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingSøknadsinfoService +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.eøs.vilkårsvurdering.VilkårsvurderingTidslinjeService +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.logg.LoggService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.totrinnskontroll.TotrinnskontrollService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat.Companion.VilkårResultatComparator +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.ba.sak.statistikk.saksstatistikk.SaksstatistikkEventPublisher +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.LocalDateTime + +class VedtakServiceTest( + @Autowired + private val behandlingRepository: BehandlingRepository, + + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @Autowired + private val behandlingstemaService: BehandlingstemaService, + + @Autowired + private val behandlingSøknadsinfoService: BehandlingSøknadsinfoService, + + @Autowired + private val vedtakRepository: VedtakRepository, + + @Autowired + private val behandlingMetrikker: BehandlingMetrikker, + + @Autowired + private val vilkårsvurderingService: VilkårsvurderingService, + + @Autowired + private val arbeidsfordelingService: ArbeidsfordelingService, + + @Autowired + private val vedtakService: VedtakService, + + @Autowired + private val vedtaksperiodeService: VedtaksperiodeService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val fagsakRepository: FagsakRepository, + + @Autowired + private val totrinnskontrollService: TotrinnskontrollService, + + @Autowired + private val loggService: LoggService, + + @Autowired + private val saksstatistikkEventPublisher: SaksstatistikkEventPublisher, + + @Autowired + private val infotrygdService: InfotrygdService, + + @Autowired + private val beregningService: BeregningService, + + @Autowired + private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + + @Autowired + private val taskRepository: TaskRepositoryWrapper, + + @Autowired + private val behandlingMigreringsinfoRepository: BehandlingMigreringsinfoRepository, + + @Autowired + private val behandlingSøknadsinfoRepository: BehandlingSøknadsinfoRepository, + + @Autowired + private val vilkårsvurderingTidslinjeService: VilkårsvurderingTidslinjeService, + +) : AbstractSpringIntegrationTest() { + + lateinit var behandlingService: BehandlingService + lateinit var vilkårResultat1: VilkårResultat + lateinit var vilkårResultat2: VilkårResultat + lateinit var vilkårResultat3: VilkårResultat + lateinit var vilkårsvurdering: Vilkårsvurdering + lateinit var personResultat: PersonResultat + lateinit var vilkår: Vilkår + lateinit var resultat: Resultat + lateinit var behandling: Behandling + + @BeforeEach + fun setup() { + behandlingService = BehandlingService( + behandlingHentOgPersisterService, + behandlingstemaService, + behandlingSøknadsinfoService, + behandlingMigreringsinfoRepository, + behandlingMetrikker, + saksstatistikkEventPublisher, + fagsakRepository, + vedtakRepository, + andelTilkjentYtelseRepository, + loggService, + arbeidsfordelingService, + infotrygdService, + vedtaksperiodeService, + taskRepository, + vilkårsvurderingService, + ) + + val personAktørId = randomAktør() + + behandling = lagBehandling() + + vilkår = Vilkår.LOVLIG_OPPHOLD + resultat = Resultat.OPPFYLT + + vilkårsvurdering = lagVilkårsvurdering(personAktørId, behandling, resultat) + + personResultat = PersonResultat( + vilkårsvurdering = vilkårsvurdering, + aktør = personAktørId, + ) + + vilkårResultat1 = VilkårResultat( + id = 1, + personResultat = personResultat, + vilkårType = vilkår, + resultat = resultat, + resultatBegrunnelse = null, + periodeFom = LocalDate.of(2010, 1, 1), + periodeTom = LocalDate.of(2010, 6, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ) + vilkårResultat2 = VilkårResultat( + id = 2, + personResultat = personResultat, + vilkårType = vilkår, + resultat = resultat, + resultatBegrunnelse = null, + periodeFom = LocalDate.of(2010, 6, 2), + periodeTom = LocalDate.of(2010, 8, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ) + vilkårResultat3 = VilkårResultat( + id = 3, + personResultat = personResultat, + vilkårType = vilkår, + resultat = resultat, + resultatBegrunnelse = null, + periodeFom = LocalDate.of(2010, 8, 2), + periodeTom = LocalDate.of(2010, 12, 1), + begrunnelse = "", + sistEndretIBehandlingId = vilkårsvurdering.behandling.id, + ) + personResultat.setSortedVilkårResultater( + setOf( + vilkårResultat1, + vilkårResultat2, + vilkårResultat3, + ).toSortedSet(VilkårResultatComparator), + ) + } + + @Test + @Tag("integration") + fun `Opprett behandling med vedtak`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + + val fnrAktørNr = personidentService.hentAktør(fnr) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + val vilkårsvurdering = lagVilkårsvurdering(fnrAktørNr, behandling, Resultat.OPPFYLT) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = vilkårsvurdering) + + val barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true) + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = fagsak.aktør, + barnAktør = barnAktør, + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + behandlingService.opprettOgInitierNyttVedtakForBehandling(behandling = behandling) + + totrinnskontrollService.opprettTotrinnskontrollMedSaksbehandler( + behandling, + "ansvarligSaksbehandler", + "saksbehandlerId", + ) + totrinnskontrollService.besluttTotrinnskontroll( + behandling, + "ansvarligBeslutter", + "beslutterId", + Beslutning.GODKJENT, + ) + + val hentetVedtak = vedtakService.hentAktivForBehandling(behandling.id) + Assertions.assertNotNull(hentetVedtak) + Assertions.assertNull(hentetVedtak!!.vedtaksdato) + Assertions.assertEquals(null, hentetVedtak.stønadBrevPdF) + + val totrinnskontroll = totrinnskontrollService.hentAktivForBehandling(behandlingId = behandling.id) + Assertions.assertNotNull(totrinnskontroll) + Assertions.assertEquals("ansvarligSaksbehandler", totrinnskontroll!!.saksbehandler) + Assertions.assertEquals("saksbehandlerId", totrinnskontroll.saksbehandlerId) + Assertions.assertEquals("ansvarligBeslutter", totrinnskontroll.beslutter) + Assertions.assertEquals("beslutterId", totrinnskontroll.beslutterId) + } + + @Test + @Tag("integration") + fun `Kast feil når det forsøkes å oppdatere et vedtak som ikke er lagret`() { + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + assertThrows { + vedtakService.oppdater( + Vedtak( + behandling = behandling, + vedtaksdato = LocalDateTime.now(), + ), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksperiodeRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksperiodeRepositoryTest.kt new file mode 100644 index 000000000..a5ee82a07 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/domene/VedtaksperiodeRepositoryTest.kt @@ -0,0 +1,50 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.domene + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.lagVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakRepository +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.dao.EmptyResultDataAccessException + +class VedtaksperiodeRepositoryTest( + @Autowired private val aktørIdRepository: AktørIdRepository, + @Autowired private val fagsakRepository: FagsakRepository, + @Autowired private val behandlingRepository: BehandlingRepository, + @Autowired private val vedtakRepository: VedtakRepository, + @Autowired private val vedtaksperiodeRepository: VedtaksperiodeRepository, +) : AbstractSpringIntegrationTest() { + + @Nested + inner class FinnBehandlingIdForVedtaksperiode { + @Test + fun `skal kunne hente behandlingId til en vedtaksperiode`() { + val søker = aktørIdRepository.save(randomAktør()) + val fagsak = fagsakRepository.save(Fagsak(aktør = søker)) + val behandling = behandlingRepository.save(lagBehandling(fagsak)) + val vedtak = vedtakRepository.save(lagVedtak(behandling)) + val lagVedtaksperiodeMedBegrunnelser = lagVedtaksperiodeMedBegrunnelser(vedtak = vedtak) + lagVedtaksperiodeMedBegrunnelser.begrunnelser.clear() + val vedtaksperiode = vedtaksperiodeRepository.save(lagVedtaksperiodeMedBegrunnelser) + + assertThat(vedtaksperiodeRepository.finnBehandlingIdForVedtaksperiode(vedtaksperiode.id)) + .isEqualTo(behandling.id) + } + + @Test + fun `skal kaste feil hvis ikke vedtaksperiode finnes `() { + assertThatThrownBy { vedtaksperiodeRepository.finnBehandlingIdForVedtaksperiode(1L) } + .isInstanceOf(EmptyResultDataAccessException::class.java) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValutaServiceTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValutaServiceTest.kt new file mode 100644 index 000000000..2857b8718 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/feilutbetaltValuta/FeilutbetaltValutaServiceTest.kt @@ -0,0 +1,76 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.feilutbetaltValuta + +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.ekstern.restDomene.RestFeilutbetaltValuta +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.Month + +class FeilutbetaltValutaServiceTest( + @Autowired val feilutbetaltValutaService: FeilutbetaltValutaService, + @Autowired val aktørIdRepository: AktørIdRepository, + @Autowired val fagsakRepository: FagsakRepository, + @Autowired val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, +) : AbstractSpringIntegrationTest() { + + @Test + fun kanLagreEndreOgSlette() { + val fagsak = + defaultFagsak(aktør = randomAktør().also { aktørIdRepository.save(it) }).let { fagsakRepository.save(it) } + val behandling = lagBehandling(fagsak = fagsak).let { behandlingHentOgPersisterService.lagreEllerOppdater(it, false) } + val feilutbetaltValuta = RestFeilutbetaltValuta( + id = 0, + fom = LocalDate.of(2020, Month.JANUARY, 1), + tom = LocalDate.of(2021, Month.MAY, 31), + feilutbetaltBeløp = 1234, + ) + + val id = feilutbetaltValutaService.leggTilFeilutbetaltValutaPeriode(feilutbetaltValuta = feilutbetaltValuta, behandlingId = behandling.id) + + feilutbetaltValutaService.hentFeilutbetaltValutaPerioder(behandlingId = behandling.id) + .also { Assertions.assertThat(it[0].id).isEqualTo(id) } + .also { Assertions.assertThat(it[0].fom).isNotNull() } + .also { Assertions.assertThat(it[0].tom).isNotNull() } + + feilutbetaltValutaService.oppdatertFeilutbetaltValutaPeriode( + feilutbetaltValuta = RestFeilutbetaltValuta( + id = id, + fom = LocalDate.of(2020, Month.JANUARY, 1), + tom = LocalDate.of(2020, Month.MAY, 31), + feilutbetaltBeløp = 1, + ), + id = id, + ) + + feilutbetaltValutaService.hentFeilutbetaltValutaPerioder(behandlingId = behandling.id) + .also { Assertions.assertThat(it.get(0).id).isEqualTo(id) } + .also { Assertions.assertThat(it.get(0).tom).isEqualTo("2020-05-31") } + + val feilutbetaltValuta2 = RestFeilutbetaltValuta( + id = 0, + fom = LocalDate.of(2019, Month.DECEMBER, 1), + tom = LocalDate.of(2019, Month.DECEMBER, 31), + feilutbetaltBeløp = 100, + ) + + val id2 = feilutbetaltValutaService.leggTilFeilutbetaltValutaPeriode(feilutbetaltValuta = feilutbetaltValuta2, behandlingId = behandling.id) + + feilutbetaltValutaService.hentFeilutbetaltValutaPerioder(behandlingId = behandling.id) + .also { Assertions.assertThat(it.size).isEqualTo(2) } + .also { Assertions.assertThat(it.get(0).id).isEqualTo(id2) } + + feilutbetaltValutaService.fjernFeilutbetaltValutaPeriode(id = id, behandlingId = behandling.id) + + feilutbetaltValutaService.hentFeilutbetaltValutaPerioder(behandlingId = behandling.id) + .also { Assertions.assertThat(it.size).isEqualTo(1) } + .also { Assertions.assertThat(it[0].id).isEqualTo(id2) } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270sServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270sServiceTest.kt" new file mode 100644 index 000000000..795d6e9a0 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/refusjonE\303\270s/RefusjonE\303\270sServiceTest.kt" @@ -0,0 +1,86 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.refusjonEøs + +import no.nav.familie.ba.sak.common.defaultFagsak +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.randomAktør +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.ekstern.restDomene.RestRefusjonEøs +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.Month + +class RefusjonEøsServiceTest( + @Autowired val refusjonEøsService: RefusjonEøsService, + @Autowired val aktørIdRepository: AktørIdRepository, + @Autowired val fagsakRepository: FagsakRepository, + @Autowired val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, +) : AbstractSpringIntegrationTest() { + + @Test + fun kanLagreEndreOgSlette() { + val fagsak = + defaultFagsak(aktør = randomAktør().also { aktørIdRepository.save(it) }).let { fagsakRepository.save(it) } + val behandling = + lagBehandling(fagsak = fagsak).let { behandlingHentOgPersisterService.lagreEllerOppdater(it, false) } + val refusjonEøs = RestRefusjonEøs( + id = 0, + fom = LocalDate.of(2020, Month.JANUARY, 1), + tom = LocalDate.of(2021, Month.MAY, 31), + refusjonsbeløp = 1234, + land = "SE", + refusjonAvklart = true, + ) + + val id = refusjonEøsService.leggTilRefusjonEøsPeriode(refusjonEøs = refusjonEøs, behandlingId = behandling.id) + + refusjonEøsService.hentRefusjonEøsPerioder(behandlingId = behandling.id) + .also { Assertions.assertThat(it[0].id).isEqualTo(id) } + .also { Assertions.assertThat(it[0].fom).isEqualTo("2020-01-01") } + .also { Assertions.assertThat(it[0].tom).isEqualTo("2021-05-31") } + + refusjonEøsService.oppdaterRefusjonEøsPeriode( + restRefusjonEøs = RestRefusjonEøs( + id = id, + fom = LocalDate.of(2020, Month.JANUARY, 1), + tom = LocalDate.of(2020, Month.MAY, 31), + refusjonsbeløp = 1, + land = "NL", + refusjonAvklart = false, + ), + id = id, + ) + + refusjonEøsService.hentRefusjonEøsPerioder(behandlingId = behandling.id) + .also { Assertions.assertThat(it[0].id).isEqualTo(id) } + .also { Assertions.assertThat(it[0].tom).isEqualTo("2020-05-31") } + .also { Assertions.assertThat(it[0].refusjonsbeløp).isEqualTo(1) } + .also { Assertions.assertThat(it[0].land).isEqualTo("NL") } + .also { Assertions.assertThat(it[0].refusjonAvklart).isEqualTo(false) } + + val refusjonEøs2 = RestRefusjonEøs( + id = 0, + fom = LocalDate.of(2019, Month.DECEMBER, 1), + tom = LocalDate.of(2019, Month.DECEMBER, 31), + refusjonsbeløp = 100, + land = "DK", + refusjonAvklart = false, + ) + + val id2 = refusjonEøsService.leggTilRefusjonEøsPeriode(refusjonEøs = refusjonEøs2, behandlingId = behandling.id) + + refusjonEøsService.hentRefusjonEøsPerioder(behandlingId = behandling.id) + .also { Assertions.assertThat(it.size).isEqualTo(2) } + .also { Assertions.assertThat(it[0].id).isEqualTo(id2) } + + refusjonEøsService.fjernRefusjonEøsPeriode(id = id, behandlingId = behandling.id) + + refusjonEøsService.hentRefusjonEøsPerioder(behandlingId = behandling.id) + .also { Assertions.assertThat(it.size).isEqualTo(1) } + .also { Assertions.assertThat(it[0].id).isEqualTo(id2) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeServiceIntegrationTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeServiceIntegrationTest.kt new file mode 100644 index 000000000..5c89cb302 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vedtak/vedtaksperiode/VedtaksperiodeServiceIntegrationTest.kt @@ -0,0 +1,668 @@ +package no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode + +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.inneværendeMåned +import no.nav.familie.ba.sak.common.kjørStegprosessForFGB +import no.nav.familie.ba.sak.common.kjørStegprosessForRevurderingÅrligKontroll +import no.nav.familie.ba.sak.common.lagAndelTilkjentYtelseMedEndreteUtbetalinger +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVedtak +import no.nav.familie.ba.sak.common.lagVedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.tilAktør +import no.nav.familie.ba.sak.ekstern.restDomene.BarnMedOpplysninger +import no.nav.familie.ba.sak.ekstern.restDomene.BehandlingUnderkategoriDTO +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.SøkerMedOpplysninger +import no.nav.familie.ba.sak.ekstern.restDomene.SøknadDTO +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.beregning.endringstidspunkt.filtrerLikEllerEtterEndringstidspunkt +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.EØSStandardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeMedBegrunnelser +import no.nav.familie.ba.sak.kjerne.vedtak.domene.VedtaksperiodeRepository +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.YearMonth + +class VedtaksperiodeServiceIntegrationTest( + @Autowired + private val stegService: StegService, + + @Autowired + private val vedtakService: VedtakService, + + @Autowired + private val vedtaksperiodeRepository: VedtaksperiodeRepository, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val vilkårsvurderingService: VilkårsvurderingService, + + @Autowired + private val vedtaksperiodeService: VedtaksperiodeService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val brevmalService: BrevmalService, + +) : AbstractSpringIntegrationTest() { + + val søkerFnr = randomFnr() + val barnFnr = ClientMocks.barnFnr[0] + val barn2Fnr = ClientMocks.barnFnr[1] + + @BeforeEach + fun init() { + databaseCleanupService.truncate() + } + + private fun kjørFørstegangsbehandlingOgRevurderingÅrligKontroll(): Behandling { + val førstegangsbehandling = kjørStegprosessForFGB( + tilSteg = StegType.BEHANDLING_AVSLUTTET, + søkerFnr = søkerFnr, + barnasIdenter = listOf(barnFnr, barn2Fnr), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + return kjørStegprosessForRevurderingÅrligKontroll( + tilSteg = StegType.BEHANDLINGSRESULTAT, + søkerFnr = søkerFnr, + barnasIdenter = listOf(barnFnr, barn2Fnr), + vedtakService = vedtakService, + stegService = stegService, + fagsakId = førstegangsbehandling.fagsak.id, + brevmalService = brevmalService, + ) + } + + @Test + fun `Skal lage og populere avslagsperiode for uregistrert barn`() { + val søkerFnr = randomFnr() + val behandling = kjørStegprosessForFGB( + tilSteg = StegType.REGISTRERE_SØKNAD, + søkerFnr = søkerFnr, + barnasIdenter = listOf(barnFnr), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + val behandlingEtterNySøknadsregistrering = stegService.håndterSøknad( + behandling = behandling, + restRegistrerSøknad = RestRegistrerSøknad( + søknad = SøknadDTO( + underkategori = BehandlingUnderkategoriDTO.ORDINÆR, + søkerMedOpplysninger = SøkerMedOpplysninger( + ident = søkerFnr, + ), + barnaMedOpplysninger = listOf( + BarnMedOpplysninger( + ident = "", + erFolkeregistrert = false, + inkludertISøknaden = true, + ), + ), + endringAvOpplysningerBegrunnelse = "", + ), + bekreftEndringerViaFrontend = true, + ), + ) + + val vedtaksperioder = + vedtaksperiodeService.finnVedtaksperioderForBehandling(behandlingEtterNySøknadsregistrering.id) + + assertEquals(1, vedtaksperioder.size) + assertEquals(1, vedtaksperioder.flatMap { it.begrunnelser }.size) + assertEquals( + Standardbegrunnelse.AVSLAG_UREGISTRERT_BARN, + vedtaksperioder.flatMap { it.begrunnelser }.first().standardbegrunnelse, + ) + } + + @Test + fun `Skal lage og populere avslagsperiode for uregistrert barn med eøs begrunnelse dersom behandling sin kategori er EØS`() { + val søkerFnr = randomFnr() + val behandling = kjørStegprosessForFGB( + tilSteg = StegType.REGISTRERE_SØKNAD, + søkerFnr = søkerFnr, + barnasIdenter = listOf(barnFnr), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + behandlingKategori = BehandlingKategori.EØS, + ) + + val behandlingEtterNySøknadsregistrering = stegService.håndterSøknad( + behandling = behandling, + restRegistrerSøknad = RestRegistrerSøknad( + søknad = SøknadDTO( + underkategori = BehandlingUnderkategoriDTO.ORDINÆR, + søkerMedOpplysninger = SøkerMedOpplysninger( + ident = søkerFnr, + ), + barnaMedOpplysninger = listOf( + BarnMedOpplysninger( + ident = "", + erFolkeregistrert = false, + inkludertISøknaden = true, + ), + ), + endringAvOpplysningerBegrunnelse = "", + ), + bekreftEndringerViaFrontend = true, + ), + ) + + val vedtaksperioder = + vedtaksperiodeService.finnVedtaksperioderForBehandling(behandlingEtterNySøknadsregistrering.id) + + assertEquals(1, vedtaksperioder.size) + assertEquals(1, vedtaksperioder.flatMap { it.eøsBegrunnelser }.size) + assertEquals( + EØSStandardbegrunnelse.AVSLAG_EØS_UREGISTRERT_BARN, + vedtaksperioder.flatMap { it.eøsBegrunnelser }.first().begrunnelse, + ) + } + + @Test + fun `Skal kunne lagre flere vedtaksperioder av typen endret utbetaling med samme periode`() { + val behandling = kjørStegprosessForFGB( + tilSteg = StegType.REGISTRERE_SØKNAD, + søkerFnr = randomFnr(), + barnasIdenter = listOf(barnFnr), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + val vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingId = behandling.id) + + val fom = inneværendeMåned().minusMonths(12).førsteDagIInneværendeMåned() + val tom = inneværendeMåned().sisteDagIInneværendeMåned() + val type = Vedtaksperiodetype.UTBETALING + val vedtaksperiode = VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = fom, + tom = tom, + type = type, + ) + vedtaksperiodeRepository.save(vedtaksperiode) + + val vedtaksperiodeMedSammePeriode = VedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = fom, + tom = tom, + type = type, + ) + + Assertions.assertDoesNotThrow { + vedtaksperiodeRepository.save(vedtaksperiodeMedSammePeriode) + } + } + + @Test + fun `Skal validere at vedtaksperioder blir lagret ved fortsatt innvilget som resultat`() { + val revurdering = kjørFørstegangsbehandlingOgRevurderingÅrligKontroll() + assertEquals(Behandlingsresultat.FORTSATT_INNVILGET, revurdering.resultat) + + val vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingId = revurdering.id) + val vedtaksperioder = vedtaksperiodeService.hentPersisterteVedtaksperioder(vedtak) + + assertEquals(1, vedtaksperioder.size) + assertEquals(Vedtaksperiodetype.FORTSATT_INNVILGET, vedtaksperioder.first().type) + } + + @Test + fun `Skal legge til og overskrive begrunnelser og fritekst på vedtaksperiode`() { + val revurdering = kjørFørstegangsbehandlingOgRevurderingÅrligKontroll() + val vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingId = revurdering.id) + val vedtaksperioder = vedtaksperiodeService.hentPersisterteVedtaksperioder(vedtak) + + vedtaksperiodeService.oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = vedtaksperioder.first().id, + standardbegrunnelserFraFrontend = listOf(Standardbegrunnelse.FORTSATT_INNVILGET_BARN_OG_SØKER_LOVLIG_OPPHOLD_OPPHOLDSTILLATELSE), + eøsStandardbegrunnelserFraFrontend = emptyList(), + ) + + val vedtaksperioderMedUtfylteBegrunnelser = vedtaksperiodeService.hentPersisterteVedtaksperioder(vedtak) + assertEquals(1, vedtaksperioderMedUtfylteBegrunnelser.size) + assertEquals(1, vedtaksperioderMedUtfylteBegrunnelser.first().begrunnelser.size) + assertEquals( + Standardbegrunnelse.FORTSATT_INNVILGET_BARN_OG_SØKER_LOVLIG_OPPHOLD_OPPHOLDSTILLATELSE, + vedtaksperioderMedUtfylteBegrunnelser.first().begrunnelser.first().standardbegrunnelse, + ) + + vedtaksperiodeService.oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = vedtaksperioder.first().id, + standardbegrunnelserFraFrontend = listOf(Standardbegrunnelse.FORTSATT_INNVILGET_FAST_OMSORG), + eøsStandardbegrunnelserFraFrontend = emptyList(), + ) + + val vedtaksperioderMedOverskrevneBegrunnelser = vedtaksperiodeService.hentPersisterteVedtaksperioder(vedtak) + assertEquals(1, vedtaksperioderMedOverskrevneBegrunnelser.size) + assertEquals(1, vedtaksperioderMedOverskrevneBegrunnelser.first().begrunnelser.size) + assertEquals( + Standardbegrunnelse.FORTSATT_INNVILGET_FAST_OMSORG, + vedtaksperioderMedOverskrevneBegrunnelser.first().begrunnelser.first().standardbegrunnelse, + ) + assertEquals(0, vedtaksperioderMedOverskrevneBegrunnelser.first().fritekster.size) + } + + @Test + fun `Skal kaste feil når feil type blir valgt`() { + val revurdering = kjørFørstegangsbehandlingOgRevurderingÅrligKontroll() + val vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingId = revurdering.id) + val vedtaksperioder = vedtaksperiodeService.hentPersisterteVedtaksperioder(vedtak) + + val feil = assertThrows { + vedtaksperiodeService.oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = vedtaksperioder.first().id, + standardbegrunnelserFraFrontend = listOf(Standardbegrunnelse.INNVILGET_BARN_BOR_SAMMEN_MED_MOTTAKER), + eøsStandardbegrunnelserFraFrontend = emptyList(), + ) + } + + assertEquals( + "Begrunnelsestype INNVILGET passer ikke med typen 'FORTSATT_INNVILGET' som er satt på perioden.", + feil.message, + ) + } + + @Test + fun `skal identifisere reduserte perioder i begynnelsen`() { + val barn1 = tilAktør(barnFnr) + val barn2 = tilAktør(barn2Fnr) + val behandling = lagBehandling() + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandling.id, søkerFnr, listOf(barnFnr, barn2Fnr)) + + val forrigeAndelTilkjentYtelse1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 8), + aktør = barn1, + ) + val forrigeAndelTilkjentYtelse2 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 8), + aktør = barn2, + ) + + val andelTilkjentYtelse1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 5), + tom = YearMonth.of(2021, 8), + behandling = behandling, + aktør = barn1, + ) + val andelTilkjentYtelse2 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 8), + behandling = behandling, + aktør = barn2, + ) + val vedtak = lagVedtak() + val utbetalingsperioder = listOf( + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.of(2021, 4, 1), + tom = LocalDate.of(2021, 4, 30), + type = Vedtaksperiodetype.UTBETALING, + ), + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.of(2021, 5, 1), + tom = LocalDate.of(2021, 8, 31), + type = Vedtaksperiodetype.UTBETALING, + ), + ) + + val redusertePerioder = identifiserReduksjonsperioderFraSistIverksatteBehandling( + forrigeAndelerTilkjentYtelse = listOf(forrigeAndelTilkjentYtelse1, forrigeAndelTilkjentYtelse2), + andelerTilkjentYtelse = listOf(andelTilkjentYtelse1, andelTilkjentYtelse2), + vedtak = vedtak, + utbetalingsperioder = utbetalingsperioder, + personopplysningGrunnlag = personopplysningGrunnlag, + opphørsperioder = emptyList(), + aktørerIForrigePersonopplysningGrunnlag = listOf(barn1, barn2), + ) + assertTrue { redusertePerioder.isNotEmpty() } + assertEquals(1, redusertePerioder.size) + assertEquals( + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + redusertePerioder.first().type, + ) + assertEquals(LocalDate.of(2021, 4, 1), redusertePerioder.first().fom) + assertEquals(LocalDate.of(2021, 4, 30), redusertePerioder.first().tom) + } + + @Test + fun `skal ikke identifisere reduserte perioder midt i utbetalingsperiode når forrige måned har utbetaling`() { + val barn1 = tilAktør(barnFnr) + val barn2 = tilAktør(barn2Fnr) + val behandling = lagBehandling() + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandling.id, søkerFnr, listOf(barnFnr, barn2Fnr)) + + val forrigeAndelTilkjentYtelse1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 8), + aktør = barn1, + ) + val forrigeAndelTilkjentYtelse2 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 8), + aktør = barn2, + ) + + val andelTilkjentYtelse1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 6), + behandling = behandling, + aktør = barn1, + ) + val andelTilkjentYtelse2 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 8), + tom = YearMonth.of(2021, 8), + behandling = behandling, + aktør = barn1, + ) + val andelTilkjentYtelse3 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 8), + behandling = behandling, + aktør = barn2, + ) + + val vedtak = lagVedtak() + val utbetalingsperioder = listOf( + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.of(2021, 4, 1), + tom = LocalDate.of(2021, 6, 30), + type = Vedtaksperiodetype.UTBETALING, + ), + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.of(2021, 7, 1), + tom = LocalDate.of(2021, 8, 31), + type = Vedtaksperiodetype.UTBETALING, + ), + ) + + val redusertePerioder = identifiserReduksjonsperioderFraSistIverksatteBehandling( + forrigeAndelerTilkjentYtelse = listOf(forrigeAndelTilkjentYtelse1, forrigeAndelTilkjentYtelse2), + andelerTilkjentYtelse = listOf(andelTilkjentYtelse1, andelTilkjentYtelse2, andelTilkjentYtelse3), + vedtak = vedtak, + utbetalingsperioder = utbetalingsperioder, + personopplysningGrunnlag = personopplysningGrunnlag, + opphørsperioder = emptyList(), + aktørerIForrigePersonopplysningGrunnlag = listOf(barn1, barn2), + ) + assertTrue { redusertePerioder.isEmpty() } + } + + @Test + fun `skal identifisere reduserte perioder midt i utbetalingsperiode når forrige måned ikke har utbetaling`() { + val barn1 = tilAktør(barnFnr) + val barn2 = tilAktør(barn2Fnr) + val behandling = lagBehandling() + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandling.id, søkerFnr, listOf(barnFnr, barn2Fnr)) + + val forrigeAndelTilkjentYtelse1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 8), + aktør = barn1, + ) + val forrigeAndelTilkjentYtelse2 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 8), + aktør = barn2, + ) + + val andelTilkjentYtelse1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 4), + behandling = behandling, + aktør = barn1, + ) + val andelTilkjentYtelse2 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 8), + tom = YearMonth.of(2021, 8), + behandling = behandling, + aktør = barn1, + ) + val andelTilkjentYtelse3 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 4), + behandling = behandling, + aktør = barn2, + ) + val andelTilkjentYtelse4 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 6), + tom = YearMonth.of(2021, 8), + behandling = behandling, + aktør = barn2, + ) + + val vedtak = lagVedtak() + val opphørsperiode = lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.of(2021, 5, 1), + tom = LocalDate.of(2021, 5, 31), + type = Vedtaksperiodetype.OPPHØR, + ) + val utbetalingsperioder = listOf( + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.of(2021, 4, 1), + tom = LocalDate.of(2021, 4, 30), + type = Vedtaksperiodetype.UTBETALING, + ), + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.of(2021, 6, 1), + tom = LocalDate.of(2021, 8, 31), + type = Vedtaksperiodetype.UTBETALING, + ), + ) + + val redusertePerioder = identifiserReduksjonsperioderFraSistIverksatteBehandling( + forrigeAndelerTilkjentYtelse = listOf(forrigeAndelTilkjentYtelse1, forrigeAndelTilkjentYtelse2), + andelerTilkjentYtelse = listOf( + andelTilkjentYtelse1, + andelTilkjentYtelse2, + andelTilkjentYtelse3, + andelTilkjentYtelse4, + ), + vedtak = vedtak, + utbetalingsperioder = utbetalingsperioder, + personopplysningGrunnlag = personopplysningGrunnlag, + opphørsperioder = listOf(opphørsperiode), + aktørerIForrigePersonopplysningGrunnlag = listOf(barn1, barn2), + ) + assertTrue { redusertePerioder.isNotEmpty() } + assertEquals(1, redusertePerioder.size) + assertEquals( + Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING, + redusertePerioder.first().type, + ) + assertEquals(LocalDate.of(2021, 6, 1), redusertePerioder.first().fom) + assertEquals(LocalDate.of(2021, 7, 31), redusertePerioder.first().tom) + } + + @Test + fun `skal identifisere flere reduserte perioder`() { + val barn1 = tilAktør(barnFnr) + val barn2 = tilAktør(barn2Fnr) + val behandling = lagBehandling() + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag(behandling.id, søkerFnr, listOf(barnFnr, barn2Fnr)) + + val forrigeAndelTilkjentYtelse1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 8), + aktør = barn1, + ) + val forrigeAndelTilkjentYtelse2 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 8), + aktør = barn2, + ) + + val andelTilkjentYtelse1 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 4), + tom = YearMonth.of(2021, 4), + behandling = behandling, + aktør = barn1, + ) + val andelTilkjentYtelse2 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 8), + tom = YearMonth.of(2021, 8), + behandling = behandling, + aktør = barn1, + ) + val andelTilkjentYtelse3 = lagAndelTilkjentYtelseMedEndreteUtbetalinger( + fom = YearMonth.of(2021, 6), + tom = YearMonth.of(2021, 8), + behandling = behandling, + aktør = barn2, + ) + + val vedtak = lagVedtak() + val opphørsperiode = lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.of(2021, 5, 1), + tom = LocalDate.of(2021, 5, 31), + type = Vedtaksperiodetype.OPPHØR, + ) + val utbetalingsperioder = listOf( + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.of(2021, 4, 1), + tom = LocalDate.of(2021, 4, 30), + type = Vedtaksperiodetype.UTBETALING, + ), + lagVedtaksperiodeMedBegrunnelser( + vedtak = vedtak, + fom = LocalDate.of(2021, 6, 1), + tom = LocalDate.of(2021, 8, 31), + type = Vedtaksperiodetype.UTBETALING, + ), + ) + + val redusertePerioder = identifiserReduksjonsperioderFraSistIverksatteBehandling( + forrigeAndelerTilkjentYtelse = listOf(forrigeAndelTilkjentYtelse1, forrigeAndelTilkjentYtelse2), + andelerTilkjentYtelse = listOf( + andelTilkjentYtelse1, + andelTilkjentYtelse2, + andelTilkjentYtelse3, + ), + vedtak = vedtak, + utbetalingsperioder = utbetalingsperioder, + personopplysningGrunnlag = personopplysningGrunnlag, + opphørsperioder = listOf(opphørsperiode), + aktørerIForrigePersonopplysningGrunnlag = listOf(barn1, barn2), + ) + assertTrue { redusertePerioder.isNotEmpty() } + assertEquals(2, redusertePerioder.size) + assertTrue { redusertePerioder.all { Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING == it.type } } + assertEquals(LocalDate.of(2021, 4, 1), redusertePerioder.first().fom) + assertEquals(LocalDate.of(2021, 4, 30), redusertePerioder.first().tom) + assertEquals(LocalDate.of(2021, 6, 1), redusertePerioder.last().fom) + assertEquals(LocalDate.of(2021, 7, 31), redusertePerioder.last().tom) + } + + @Test + fun `generere vedtaksperioder basert på manuelt overstyrt endringstidspunkt`() { + val vedtak = lagVedtak() + val avslagsperioder = listOf( + lagVedtaksperiodeMedBegrunnelser( + vedtak, + LocalDate.of(2021, 1, 1), + LocalDate.of(2021, 4, 30), + Vedtaksperiodetype.AVSLAG, + ), + ) + val utbetalingsperioder = listOf( + lagVedtaksperiodeMedBegrunnelser( + vedtak, + LocalDate.of(2021, 1, 1), + LocalDate.of(2021, 2, 28), + Vedtaksperiodetype.UTBETALING, + ), + lagVedtaksperiodeMedBegrunnelser( + vedtak, + LocalDate.of(2021, 3, 1), + LocalDate.of(2021, 7, 31), + Vedtaksperiodetype.UTBETALING, + ), + ) + val vedtaksperioder = utbetalingsperioder.filtrerLikEllerEtterEndringstidspunkt( + endringstidspunkt = LocalDate.of(2021, 3, 1), + ) + avslagsperioder + + assertNotNull(vedtaksperioder) + assertEquals(2, vedtaksperioder.size) + assertTrue { + vedtaksperioder.any { + it.fom == LocalDate.of(2021, 1, 1) && + it.tom == LocalDate.of(2021, 4, 30) && + it.type == Vedtaksperiodetype.AVSLAG + } + } + assertTrue { + vedtaksperioder.any { + it.fom == LocalDate.of(2021, 3, 1) && + it.tom == LocalDate.of(2021, 7, 31) && + it.type == Vedtaksperiodetype.UTBETALING + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/AbstractVerdikjedetest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/AbstractVerdikjedetest.kt new file mode 100644 index 000000000..6d54669ef --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/AbstractVerdikjedetest.kt @@ -0,0 +1,79 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import no.nav.familie.ba.sak.WebSpringAuthTestRunner +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.MockserverKlient +import org.junit.jupiter.api.Tag +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.support.TestPropertySourceUtils +import org.springframework.web.client.RestOperations +import org.testcontainers.containers.FixedHostPortGenericContainer +import org.testcontainers.images.PullPolicy + +val MOCK_SERVER_IMAGE = "ghcr.io/navikt/familie-mock-server/familie-mock-server:latest" + +class VerdikjedetesterPropertyOverrideContextInitializer : + ApplicationContextInitializer { + + override fun initialize(configurableApplicationContext: ConfigurableApplicationContext) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment( + configurableApplicationContext, + "FAMILIE_BA_INFOTRYGD_API_URL: http://localhost:1337/rest/api/infotrygd/ba", + "PDL_URL: http://localhost:1337/rest/api/pdl", + ) + mockServer.start() + } + + companion object { + // Lazy because we only want it to be initialized when accessed + val mockServer: KMockServerContainer by lazy { + val mockServer = KMockServerContainer(MOCK_SERVER_IMAGE) + mockServer.withExposedPorts(1337) + mockServer.withFixedExposedPort(1337, 1337) + mockServer.withImagePullPolicy(PullPolicy.alwaysPull()) + mockServer + } + } +} + +@ActiveProfiles( + "postgres", + "integrasjonstest", + "mock-oauth", + "mock-localdate-service", + "mock-tilbakekreving-klient", + "mock-brev-klient", + "mock-økonomi", + "mock-infotrygd-feed", + "mock-rest-template-config", + "mock-task-repository", + "mock-task-service", + "mock-sanity-client", +) +@ContextConfiguration(initializers = [VerdikjedetesterPropertyOverrideContextInitializer::class]) +@Tag("verdikjedetest") +abstract class AbstractVerdikjedetest : WebSpringAuthTestRunner() { + + @Autowired + lateinit var restOperations: RestOperations + + fun familieBaSakKlient(): FamilieBaSakKlient = FamilieBaSakKlient( + baSakUrl = hentUrl(""), + restOperations = restOperations, + headers = hentHeadersForSystembruker(), + ) + + fun mockServerKlient(): MockserverKlient = MockserverKlient( + mockServerUrl = "http://localhost:1337", + restOperations = restOperations, + ) +} + +/** + * Hack needed because testcontainers use of generics confuses Kotlin. + * Må bruke fixed host port for at klientene våres kan konfigureres med fast port. + */ +class KMockServerContainer(imageName: String) : FixedHostPortGenericContainer(imageName) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/AndelTilkjentYtelseOffsetTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/AndelTilkjentYtelseOffsetTest.kt new file mode 100644 index 000000000..febaad8cb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/AndelTilkjentYtelseOffsetTest.kt @@ -0,0 +1,203 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.RestMinimalFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.integrasjoner.ef.EfSakRestClient +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.beregning.SatsTidspunkt +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.ef.Datakilde +import no.nav.familie.kontrakter.felles.ef.EksternPeriode +import no.nav.familie.kontrakter.felles.ef.EksternePerioderResponse +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class AndelTilkjentYtelseOffsetTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, + @Autowired private val stegService: StegService, + @Autowired private val efSakRestClient: EfSakRestClient, + @Autowired private val beregningService: BeregningService, + @Autowired private val brevmalService: BrevmalService, + @Autowired private val featureToggleService: FeatureToggleService, +) : AbstractVerdikjedetest() { + private val barnFødselsdato: LocalDate = LocalDate.now().minusYears(2) + + @BeforeEach + fun førHverTest() { + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2022, 12, 31) + } + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + @Test + fun `Skal ha riktig offset for andeler når man legger til ny andel`() { + val personScenario1: RestScenario = lagScenario(barnFødselsdato) + val fagsak1: RestMinimalFagsak = lagFagsak(personScenario = personScenario1) + val behandling1 = fullførBehandling( + fagsak = fagsak1, + personScenario = personScenario1, + barnFødselsdato = barnFødselsdato, + ) + + // Legger til småbarnstillegg på søker + val behandling2: Behandling = fullførRevurderingMedOvergangstonad( + fagsak = fagsak1, + personScenario = personScenario1, + barnFødselsdato = barnFødselsdato, + ) + + val andelerBehandling1 = beregningService.hentAndelerTilkjentYtelseForBehandling(behandlingId = behandling1.id) + val offsetBehandling1 = andelerBehandling1.mapNotNull { it.periodeOffset }.map { it.toInt() }.sorted() + + val andelerBehandling2 = beregningService.hentAndelerTilkjentYtelseForBehandling(behandlingId = behandling2.id) + val offsetBehandling2 = andelerBehandling2.mapNotNull { it.periodeOffset }.map { it.toInt() }.sorted() + + val nyAndelIBehandling2 = andelerBehandling2.single { it.erSmåbarnstillegg() } + val forventetOffsetNyAndel = offsetBehandling1.max() + 1 + + Assertions.assertEquals(forventetOffsetNyAndel, nyAndelIBehandling2.periodeOffset?.toInt()) + + // Ønsker at uendrede andeler skal beholde samme offset + Assertions.assertEquals(offsetBehandling1 + forventetOffsetNyAndel, offsetBehandling2) + } + + fun lagScenario(barnFødselsdato: LocalDate): RestScenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1996-01-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = barnFødselsdato.toString(), + fornavn = "Barn", + etternavn = "Barnesen", + bostedsadresser = emptyList(), + ), + ), + ), + ) + + fun lagFagsak(personScenario: RestScenario): RestMinimalFagsak { + return familieBaSakKlient().opprettFagsak(søkersIdent = personScenario.søker.ident!!).data!! + } + + fun fullførBehandling( + fagsak: RestMinimalFagsak, + personScenario: RestScenario, + barnFødselsdato: LocalDate, + ): Behandling { + val behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = emptyList(), + ) + + val restBehandling: Ressurs = + familieBaSakKlient().opprettBehandling( + søkersIdent = fagsak.søkerFødselsnummer, + behandlingType = behandlingType, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + fagsakId = fagsak.id, + ) + val behandling = behandlingHentOgPersisterService.hent(restBehandling.data!!.behandlingId) + val restRegistrerSøknad = + RestRegistrerSøknad( + søknad = lagSøknadDTO( + søkerIdent = fagsak.søkerFødselsnummer, + barnasIdenter = personScenario.barna.map { it.ident!! }, + underkategori = BehandlingUnderkategori.UTVIDET, + ), + bekreftEndringerViaFrontend = false, + ) + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().registrererSøknad( + behandlingId = behandling.id, + restRegistrerSøknad = restRegistrerSøknad, + ) + + return fullførBehandlingFraVilkårsvurderingAlleVilkårOppfylt( + restUtvidetBehandling = restUtvidetBehandling.data!!, + personScenario = personScenario, + fagsak = fagsak, + familieBaSakKlient = familieBaSakKlient(), + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + lagToken = ::token, + brevmalService = brevmalService, + vedtaksperiodeService = vedtaksperiodeService, + + ) + } + + fun fullførRevurderingMedOvergangstonad( + fagsak: RestMinimalFagsak, + personScenario: RestScenario, + barnFødselsdato: LocalDate, + ): Behandling { + val behandlingType = BehandlingType.REVURDERING + val behandlingÅrsak = BehandlingÅrsak.SMÅBARNSTILLEGG + + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = listOf( + EksternPeriode( + personIdent = personScenario.søker.ident!!, + fomDato = barnFødselsdato.plusYears(1), + tomDato = LocalDate.now().minusMonths(1).førsteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + ), + ) + + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().opprettBehandling( + søkersIdent = fagsak.søkerFødselsnummer, + behandlingType = behandlingType, + behandlingÅrsak = behandlingÅrsak, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + fagsakId = fagsak.id, + ) + + return fullførBehandlingFraVilkårsvurderingAlleVilkårOppfylt( + restUtvidetBehandling = restUtvidetBehandling.data!!, + personScenario = personScenario, + fagsak = fagsak, + familieBaSakKlient = familieBaSakKlient(), + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + lagToken = ::token, + brevmalService = brevmalService, + vedtaksperiodeService = vedtaksperiodeService, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/AutobrevSm\303\245barnstilleggOpph\303\270rTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/AutobrevSm\303\245barnstilleggOpph\303\270rTest.kt" new file mode 100644 index 000000000..caadb0c7e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/AutobrevSm\303\245barnstilleggOpph\303\270rTest.kt" @@ -0,0 +1,242 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import io.mockk.every +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.RestMinimalFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.integrasjoner.ef.EfSakRestClient +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRepository +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.ef.Datakilde +import no.nav.familie.kontrakter.felles.ef.EksternPeriode +import no.nav.familie.kontrakter.felles.ef.EksternePerioderResponse +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.YearMonth + +class AutobrevSmåbarnstilleggOpphørTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val fagsakRepository: FagsakRepository, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, + @Autowired private val stegService: StegService, + @Autowired private val efSakRestClient: EfSakRestClient, + @Autowired private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + @Autowired private val brevmalService: BrevmalService, + @Autowired private val featureToggleService: FeatureToggleService, +) : AbstractVerdikjedetest() { + + private val barnFødselsdato: LocalDate = LocalDate.now().minusYears(2) + + @Test + fun `Plukk riktige behandlinger - skal være nyeste, løpende med opphør i småbarnstillegg for valgt måned`() { + val personScenario1: RestScenario = lagScenario(barnFødselsdato) + val fagsak1: RestMinimalFagsak = lagFagsak(personScenario = personScenario1) + fullførBehandling( + fagsak = fagsak1, + personScenario = personScenario1, + barnFødselsdato = barnFødselsdato, + ) + val fagsak1behandling2: Behandling = fullførRevurderingMedOvergangstonad( + fagsak = fagsak1, + personScenario = personScenario1, + barnFødselsdato = barnFødselsdato, + ) + startEnRevurderingNyeOpplysningerMenIkkeFullfør( + fagsak = fagsak1, + personScenario = personScenario1, + barnFødselsdato = barnFødselsdato, + ) + + val personScenario2: RestScenario = lagScenario(barnFødselsdato) + val fagsak2: RestMinimalFagsak = lagFagsak(personScenario = personScenario2) + fullførBehandling( + fagsak = fagsak2, + personScenario = personScenario2, + barnFødselsdato = barnFødselsdato, + ) + val fagsak2behandling2: Behandling = fullførRevurderingMedOvergangstonad( + fagsak = fagsak2, + personScenario = personScenario2, + barnFødselsdato = barnFødselsdato, + ) + + val andelerForSmåbarnstilleggFagsak1Behandling2 = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId = fagsak2behandling2.id) + val førsteDagIStønadTomMåned = YearMonth.now().minusMonths(1) + assertEquals( + førsteDagIStønadTomMåned, + andelerForSmåbarnstilleggFagsak1Behandling2.maxByOrNull { + it.stønadTom == YearMonth.now().minusMonths(1) && it.erSmåbarnstillegg() + }?.stønadTom, + ) + + val fagsaker: List = + fagsakRepository.finnAlleFagsakerMedOpphørSmåbarnstilleggIMåned( + iverksatteLøpendeBehandlinger = listOf(fagsak1behandling2.id, fagsak2behandling2.id), + ) + + assertTrue(fagsaker.containsAll(listOf(fagsak2.id))) + assertFalse(fagsaker.contains(fagsak1.id)) + } + + fun lagScenario(barnFødselsdato: LocalDate): RestScenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1996-01-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = barnFødselsdato.toString(), + fornavn = "Barn", + etternavn = "Barnesen", + bostedsadresser = emptyList(), + ), + ), + ), + ) + + fun lagFagsak(personScenario: RestScenario): RestMinimalFagsak { + return familieBaSakKlient().opprettFagsak(søkersIdent = personScenario.søker.ident!!).data!! + } + + fun fullførBehandling( + fagsak: RestMinimalFagsak, + personScenario: RestScenario, + barnFødselsdato: LocalDate, + ): Behandling { + val behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = emptyList(), + ) + + val restBehandling: Ressurs = + familieBaSakKlient().opprettBehandling( + søkersIdent = fagsak.søkerFødselsnummer, + behandlingType = behandlingType, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + fagsakId = fagsak.id, + ) + val behandling = behandlingHentOgPersisterService.hent(restBehandling.data!!.behandlingId) + val restRegistrerSøknad = + RestRegistrerSøknad( + søknad = lagSøknadDTO( + søkerIdent = fagsak.søkerFødselsnummer, + barnasIdenter = personScenario.barna.map { it.ident!! }, + underkategori = BehandlingUnderkategori.UTVIDET, + ), + bekreftEndringerViaFrontend = false, + ) + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().registrererSøknad( + behandlingId = behandling.id, + restRegistrerSøknad = restRegistrerSøknad, + ) + + return fullførBehandlingFraVilkårsvurderingAlleVilkårOppfylt( + restUtvidetBehandling = restUtvidetBehandling.data!!, + personScenario = personScenario, + fagsak = fagsak, + familieBaSakKlient = familieBaSakKlient(), + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + lagToken = ::token, + brevmalService = brevmalService, + vedtaksperiodeService = vedtaksperiodeService, + ) + } + + fun fullførRevurderingMedOvergangstonad( + fagsak: RestMinimalFagsak, + personScenario: RestScenario, + barnFødselsdato: LocalDate, + ): Behandling { + val behandlingType = BehandlingType.REVURDERING + val behandlingÅrsak = BehandlingÅrsak.SMÅBARNSTILLEGG + + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = listOf( + EksternPeriode( + personIdent = personScenario.søker.ident!!, + fomDato = barnFødselsdato.plusYears(1), + tomDato = LocalDate.now().minusMonths(1).førsteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + ), + ) + + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().opprettBehandling( + søkersIdent = fagsak.søkerFødselsnummer, + behandlingType = behandlingType, + behandlingÅrsak = behandlingÅrsak, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + fagsakId = fagsak.id, + ) + + return fullførBehandlingFraVilkårsvurderingAlleVilkårOppfylt( + restUtvidetBehandling = restUtvidetBehandling.data!!, + personScenario = personScenario, + fagsak = fagsak, + familieBaSakKlient = familieBaSakKlient(), + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + lagToken = ::token, + brevmalService = brevmalService, + vedtaksperiodeService = vedtaksperiodeService, + ) + } + + private fun startEnRevurderingNyeOpplysningerMenIkkeFullfør( + fagsak: RestMinimalFagsak, + personScenario: RestScenario, + barnFødselsdato: LocalDate, + ): Behandling { + val behandlingType = BehandlingType.REVURDERING + val behandlingÅrsak = BehandlingÅrsak.SMÅBARNSTILLEGG + + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = listOf( + EksternPeriode( + personIdent = personScenario.søker.ident!!, + fomDato = barnFødselsdato.plusYears(1), + tomDato = LocalDate.now().minusMonths(1).førsteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + ), + ) + + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().opprettBehandling( + søkersIdent = fagsak.søkerFødselsnummer, + behandlingType = behandlingType, + behandlingÅrsak = behandlingÅrsak, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + fagsakId = fagsak.id, + ) + return behandlingHentOgPersisterService.hent(restUtvidetBehandling.data!!.behandlingId) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/BehandleSm\303\245barnstilleggTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/BehandleSm\303\245barnstilleggTest.kt" new file mode 100644 index 000000000..d8a17827e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/BehandleSm\303\245barnstilleggTest.kt" @@ -0,0 +1,423 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import io.mockk.verify +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.EfSakRestClientMock +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestPutVedtaksperiodeMedStandardbegrunnelser +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.integrasjoner.ef.EfSakRestClient +import no.nav.familie.ba.sak.kjerne.autovedtak.AutovedtakStegService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.HenleggÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.RestHenleggBehandlingInfo +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.SatsTidspunkt +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.ba.sak.task.dto.ManuellOppgaveType +import no.nav.familie.ba.sak.util.sisteSmåbarnstilleggSatsTilTester +import no.nav.familie.ba.sak.util.sisteUtvidetSatsTilTester +import no.nav.familie.ba.sak.util.tilleggOrdinærSatsTilTester +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.ef.Datakilde +import no.nav.familie.kontrakter.felles.ef.EksternPeriode +import no.nav.familie.kontrakter.felles.ef.EksternePerioderResponse +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import java.time.LocalDate +import java.time.YearMonth + +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class BehandleSmåbarnstilleggTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val stegService: StegService, + @Autowired private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + @Autowired private val personidentService: PersonidentService, + @Autowired private val efSakRestClient: EfSakRestClient, + @Autowired private val autovedtakStegService: AutovedtakStegService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, + @Autowired private val opprettTaskService: OpprettTaskService, + @Autowired private val brevmalService: BrevmalService, +) : AbstractVerdikjedetest() { + + private val barnFødselsdato = LocalDate.now().minusYears(2) + private val periodeMedFullOvergangsstønadFom = barnFødselsdato.plusYears(1) + + val restScenario = RestScenario( + søker = RestScenarioPerson(fødselsdato = "1996-01-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = barnFødselsdato.toString(), + fornavn = "Barn", + etternavn = "Barnesen", + bostedsadresser = emptyList(), + ), + ), + ) + + lateinit var scenario: RestScenario + + @BeforeAll + fun init() { + scenario = mockServerKlient().lagScenario(restScenario) + } + + @BeforeEach + fun førHverTest() { + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2022, 12, 31) + } + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + private fun settOppefSakMockForDeFørste2Testene(søkersIdent: String) { + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = listOf( + EksternPeriode( + personIdent = søkersIdent, + fomDato = periodeMedFullOvergangsstønadFom, + tomDato = barnFødselsdato.plusYears(18), + datakilde = Datakilde.EF, + ), + ), + ) + } + + @Test + @Order(1) + fun `Skal behandle utvidet nasjonal sak med småbarnstillegg`() { + val søkersIdent = scenario.søker.ident!! + settOppefSakMockForDeFørste2Testene(søkersIdent) + + val fagsak = familieBaSakKlient().opprettFagsak(søkersIdent = søkersIdent) + val restBehandling = familieBaSakKlient().opprettBehandling( + søkersIdent = søkersIdent, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + fagsakId = fagsak.data!!.id, + ) + + val behandling = behandlingHentOgPersisterService.hent(restBehandling.data!!.behandlingId) + val restRegistrerSøknad = + RestRegistrerSøknad( + søknad = lagSøknadDTO( + søkerIdent = søkersIdent, + barnasIdenter = scenario.barna.map { it.ident!! }, + underkategori = BehandlingUnderkategori.UTVIDET, + ), + bekreftEndringerViaFrontend = false, + ) + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().registrererSøknad( + behandlingId = behandling.id, + restRegistrerSøknad = restRegistrerSøknad, + ) + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandling, + behandlingStatus = BehandlingStatus.UTREDES, + behandlingStegType = StegType.VILKÅRSVURDERING, + ) + + restUtvidetBehandling.data!!.personResultater.forEach { restPersonResultat -> + restPersonResultat.vilkårResultater.filter { it.resultat == Resultat.IKKE_VURDERT }.forEach { + familieBaSakKlient().putVilkår( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + vilkårId = it.id, + restPersonResultat = + RestPersonResultat( + personIdent = restPersonResultat.personIdent, + vilkårResultater = listOf( + it.copy( + resultat = Resultat.OPPFYLT, + periodeFom = barnFødselsdato, + ), + ), + ), + ) + } + } + + familieBaSakKlient().validerVilkårsvurdering( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + ) + + val restUtvidetBehandlingEtterBehandlingsResultat = + familieBaSakKlient().behandlingsresultatStegOgGåVidereTilNesteSteg( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + ) + + assertEquals( + tilleggOrdinærSatsTilTester() + sisteUtvidetSatsTilTester() + sisteSmåbarnstilleggSatsTilTester(), + hentNåværendeEllerNesteMånedsUtbetaling( + behandling = restUtvidetBehandlingEtterBehandlingsResultat.data!!, + ), + ) + + val andelerTilkjentYtelse = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling( + behandlingId = restUtvidetBehandlingEtterBehandlingsResultat.data!!.behandlingId, + ) + val utvidedeAndeler = andelerTilkjentYtelse.filter { it.type == YtelseType.UTVIDET_BARNETRYGD } + val småbarnstilleggAndel = andelerTilkjentYtelse.single { it.type == YtelseType.SMÅBARNSTILLEGG } + + assertEquals( + barnFødselsdato.plusMonths(1).toYearMonth(), + utvidedeAndeler.minByOrNull { it.stønadFom }?.stønadFom, + ) + assertEquals( + periodeMedFullOvergangsstønadFom.toYearMonth(), + småbarnstilleggAndel.stønadFom, + ) + assertEquals( + barnFødselsdato.plusYears(3).toYearMonth(), + småbarnstilleggAndel.stønadTom, + ) + + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterBehandlingsResultat, + behandlingStatus = BehandlingStatus.UTREDES, + behandlingStegType = StegType.VURDER_TILBAKEKREVING, + ) + + val restUtvidetBehandlingEtterVurderTilbakekreving = + familieBaSakKlient().lagreTilbakekrevingOgGåVidereTilNesteSteg( + restUtvidetBehandlingEtterBehandlingsResultat.data!!.behandlingId, + RestTilbakekreving(Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, begrunnelse = "begrunnelse"), + ) + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterVurderTilbakekreving, + behandlingStatus = BehandlingStatus.UTREDES, + behandlingStegType = StegType.SEND_TIL_BESLUTTER, + ) + + val vedtaksperioderMedBegrunnelser = vedtaksperiodeService.hentRestUtvidetVedtaksperiodeMedBegrunnelser( + restUtvidetBehandlingEtterVurderTilbakekreving.data!!.behandlingId, + ) + + val vedtaksperiode = vedtaksperioderMedBegrunnelser.sortedBy { it.fom }.first() + familieBaSakKlient().oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = vedtaksperiode.id, + restPutVedtaksperiodeMedStandardbegrunnelser = RestPutVedtaksperiodeMedStandardbegrunnelser( + standardbegrunnelser = listOf( + Standardbegrunnelse.INNVILGET_BOR_HOS_SØKER.enumnavnTilString(), + ), + ), + ) + + val restUtvidetBehandlingEtterSendTilBeslutter = + familieBaSakKlient().sendTilBeslutter(behandlingId = restUtvidetBehandlingEtterVurderTilbakekreving.data!!.behandlingId) + + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterSendTilBeslutter, + behandlingStatus = BehandlingStatus.FATTER_VEDTAK, + behandlingStegType = StegType.BESLUTTE_VEDTAK, + ) + + val restUtvidetBehandlingEtterIverksetting = + familieBaSakKlient().iverksettVedtak( + behandlingId = restUtvidetBehandlingEtterSendTilBeslutter.data!!.behandlingId, + restBeslutningPåVedtak = RestBeslutningPåVedtak( + Beslutning.GODKJENT, + ), + beslutterHeaders = HttpHeaders().apply { + setBearerAuth( + token( + mapOf( + "groups" to listOf("SAKSBEHANDLER", "BESLUTTER"), + "azp" to "azp-test", + "name" to "Mock McMockface Beslutter", + "NAVident" to "Z0000", + ), + ), + ) + }, + ) + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterIverksetting, + behandlingStatus = BehandlingStatus.IVERKSETTER_VEDTAK, + behandlingStegType = StegType.IVERKSETT_MOT_OPPDRAG, + ) + + håndterIverksettingAvBehandling( + behandlingEtterVurdering = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = fagsak.data!!.id)!!, + søkerFnr = søkersIdent, + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + ) + } + + @Test + @Order(2) + fun `Skal ikke opprette behandling når det ikke finnes endringer på perioder med full overgangsstønad`() { + val søkersIdent = scenario.søker.ident!! + settOppefSakMockForDeFørste2Testene(søkersIdent) + + val søkersAktør = personidentService.hentAktør(søkersIdent) + autovedtakStegService.kjørBehandlingSmåbarnstillegg( + mottakersAktør = søkersAktør, + aktør = søkersAktør, + ) + val fagsak = fagsakService.hentFagsakPåPerson(aktør = søkersAktør) + val aktivBehandling = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = fagsak!!.id)!! + + assertEquals(BehandlingStatus.AVSLUTTET, aktivBehandling.status) + assertNotEquals(BehandlingÅrsak.SMÅBARNSTILLEGG, aktivBehandling.opprettetÅrsak) + } + + @Test + @Order(3) + fun `Skal stoppe automatisk behandling som må fortsette manuelt pga tilbakekreving`() { + EfSakRestClientMock.clearEfSakRestMocks(efSakRestClient) + + val søkersAktør = personidentService.hentAktør(scenario.søker.aktørId!!) + + val periodeOvergangsstønadTom = LocalDate.now().minusMonths(3) + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = listOf( + EksternPeriode( + personIdent = søkersAktør.aktivFødselsnummer(), + fomDato = periodeMedFullOvergangsstønadFom, + tomDato = periodeOvergangsstønadTom, + datakilde = Datakilde.EF, + ), + ), + ) + autovedtakStegService.kjørBehandlingSmåbarnstillegg( + mottakersAktør = søkersAktør, + aktør = søkersAktør, + ) + + val fagsak = fagsakService.hentFagsakPåPerson(aktør = søkersAktør) + val aktivBehandling = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = fagsak!!.id)!! + + // Vedtaksperioder skal være slettet etter at den er blitt omgjort til manuell behandling + assertEquals( + 0, + vedtaksperiodeService.hentPersisterteVedtaksperioder( + vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingId = aktivBehandling.id), + ).size, + ) + + verify(exactly = 1) { + opprettTaskService.opprettOppgaveForManuellBehandlingTask( + behandlingId = aktivBehandling.id, + beskrivelse = "Småbarnstillegg: endring i overgangsstønad må behandles manuelt", + manuellOppgaveType = ManuellOppgaveType.SMÅBARNSTILLEGG, + ) + } + + assertEquals(StegType.BEHANDLINGSRESULTAT, aktivBehandling.steg) + assertEquals(BehandlingStatus.UTREDES, aktivBehandling.status) + + val behandlingEtterHenleggelse = stegService.håndterHenleggBehandling( + behandling = aktivBehandling, + henleggBehandlingInfo = RestHenleggBehandlingInfo( + årsak = HenleggÅrsak.FEILAKTIG_OPPRETTET, + begrunnelse = "", + ), + ) + assertEquals(false, behandlingEtterHenleggelse.aktiv) + } + + @Test + @Order(4) + fun `Skal automatisk endre småbarnstilleggperioder`() { + EfSakRestClientMock.clearEfSakRestMocks(efSakRestClient) + + val søkersIdent = scenario.søker.ident!! + val søkersAktør = personidentService.hentAktør(søkersIdent) + + val periodeOvergangsstønadTom = LocalDate.now() + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = listOf( + EksternPeriode( + personIdent = søkersIdent, + fomDato = periodeMedFullOvergangsstønadFom, + tomDato = periodeOvergangsstønadTom, + datakilde = Datakilde.EF, + ), + ), + ) + autovedtakStegService.kjørBehandlingSmåbarnstillegg( + mottakersAktør = søkersAktør, + aktør = søkersAktør, + ) + + val fagsak = fagsakService.hentFagsakPåPerson(aktør = søkersAktør) + val aktivBehandling = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = fagsak!!.id)!! + + val andelerTilkjentYtelse = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling( + behandlingId = aktivBehandling.id, + ) + val småbarnstilleggAndel = andelerTilkjentYtelse.single { it.type == YtelseType.SMÅBARNSTILLEGG } + assertEquals( + periodeMedFullOvergangsstønadFom.toYearMonth(), + småbarnstilleggAndel.stønadFom, + ) + assertEquals( + periodeOvergangsstønadTom.toYearMonth(), + småbarnstilleggAndel.stønadTom, + ) + + val vedtaksperioderMedBegrunnelser = vedtaksperiodeService.hentPersisterteVedtaksperioder( + vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingId = aktivBehandling.id), + ) + + val aktuellVedtaksperiode = + vedtaksperioderMedBegrunnelser.find { it.fom?.toYearMonth() == YearMonth.now().nesteMåned() } + assertNotNull(aktuellVedtaksperiode) + assertTrue(aktuellVedtaksperiode?.begrunnelser?.any { it.standardbegrunnelse == Standardbegrunnelse.REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_FULL_OVERGANGSSTØNAD } == true) + + håndterIverksettingAvBehandling( + behandlingEtterVurdering = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = fagsak.id)!!, + søkerFnr = søkersIdent, + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/BehandlingSatsendringTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/BehandlingSatsendringTest.kt new file mode 100644 index 000000000..ff71870b2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/BehandlingSatsendringTest.kt @@ -0,0 +1,179 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import no.nav.familie.ba.sak.common.LocalDateService +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.AutovedtakSatsendringService +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.SatsendringSvar +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.StartSatsendring.Companion.SATSENDRINGMÅNED_MARS_2023 +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene.Satskjøring +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.domene.SatskjøringRepository +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.beregning.SatsService +import no.nav.familie.ba.sak.kjerne.beregning.SatsTidspunkt +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.task.BehandleFødselshendelseTask +import no.nav.familie.ba.sak.task.SatsendringTaskDto +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.Matrikkeladresse +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.YearMonth +import java.time.temporal.ChronoUnit + +class BehandlingSatsendringTest( + @Autowired private val mockLocalDateService: LocalDateService, + @Autowired private val behandleFødselshendelseTask: BehandleFødselshendelseTask, + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val personidentService: PersonidentService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val stegService: StegService, + @Autowired private val autovedtakSatsendringService: AutovedtakSatsendringService, + @Autowired private val andelTilkjentYtelseMedEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + @Autowired private val satskjøringRepository: SatskjøringRepository, + @Autowired private val brevmalService: BrevmalService, +) : AbstractVerdikjedetest() { + + @BeforeEach + fun setUp() { + mockkObject(SatsTidspunkt) + // Grunnen til at denne mockes er egentlig at den indirekte påvirker hva SatsService.hentGyldigSatsFor + // returnerer. Det vi ønsker er at den sist tillagte satsendringen ikke kommer med slik at selve + // satsendringen som skal kjøres senere faktisk utgjør en endring (slik at behandlingsresultatet blir ENDRET). + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2023, 2, 1) + + every { mockLocalDateService.now() } returns LocalDate.now().minusYears(6) andThen LocalDate.now() + } + + @AfterEach + fun tearDown() { + unmockkObject(SatsTidspunkt) + } + + @Test + fun `Skal kjøre satsendring på løpende fagsak hvor brukeren har barnetrygd under 6 år`() { + val scenario = mockServerKlient().lagScenario(restScenario) + val behandling = opprettBehandling(scenario) + satskjøringRepository.saveAndFlush(Satskjøring(fagsakId = behandling.fagsak.id, satsTidspunkt = SATSENDRINGMÅNED_MARS_2023)) + + // Fjerner mocking slik at den siste satsendringen vi fjernet via mocking nå skal komme med. + unmockkObject(SatsTidspunkt) + + val satsendringResultat = + autovedtakSatsendringService.kjørBehandling(SatsendringTaskDto(behandling.fagsak.id, YearMonth.of(2023, 3))) + + assertEquals(SatsendringSvar.SATSENDRING_KJØRT_OK, satsendringResultat) + + val satsendringBehandling = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = behandling.fagsak.id) + assertEquals(Behandlingsresultat.ENDRET_UTBETALING, satsendringBehandling?.resultat) + assertEquals(StegType.IVERKSETT_MOT_OPPDRAG, satsendringBehandling?.steg) + + val satsendingsvedtak = vedtakService.hentAktivForBehandling(behandlingId = satsendringBehandling!!.id) + assertNull(satsendingsvedtak!!.stønadBrevPdF) + + val aty = andelTilkjentYtelseMedEndreteUtbetalingerService.finnAndelerTilkjentYtelseMedEndreteUtbetalinger( + satsendringBehandling.id, + ) + + val atyMedSenesteTilleggOrbaSats = + aty.first { it.type == YtelseType.ORDINÆR_BARNETRYGD && it.stønadFom == YearMonth.of(2023, 7) } + val atyMedVanligOrbaSats = + aty.first { it.type == YtelseType.ORDINÆR_BARNETRYGD && it.stønadFom == YearMonth.of(2029, 1) } + assertThat(atyMedSenesteTilleggOrbaSats.sats).isEqualTo(SatsService.finnSisteSatsFor(SatsType.TILLEGG_ORBA).beløp) + assertThat(atyMedVanligOrbaSats.sats).isEqualTo(SatsService.finnSisteSatsFor(SatsType.ORBA).beløp) + + val satskjøring = satskjøringRepository.findByFagsakIdAndSatsTidspunkt(behandling.fagsak.id, satsTidspunkt = SATSENDRINGMÅNED_MARS_2023) + assertThat(satskjøring?.ferdigTidspunkt) + .isCloseTo(LocalDateTime.now(), Assertions.within(30, ChronoUnit.SECONDS)) + } + + @Test + fun `Skal ignorere satsendring hvis siste sats alt er satt`() { + // Fjerner mocking slik at den siste satsendringen vi fjernet via mocking nå skal komme med. + unmockkObject(SatsTidspunkt) + + val scenario = mockServerKlient().lagScenario(restScenario) + val behandling = opprettBehandling(scenario) + satskjøringRepository.saveAndFlush(Satskjøring(fagsakId = behandling.fagsak.id, satsTidspunkt = SATSENDRINGMÅNED_MARS_2023)) + + val satsendringResultat = + autovedtakSatsendringService.kjørBehandling(SatsendringTaskDto(behandling.fagsak.id, YearMonth.of(2023, 3))) + + assertEquals(SatsendringSvar.SATSENDRING_ER_ALLEREDE_UTFØRT, satsendringResultat) + + val satskjøring = satskjøringRepository.findByFagsakIdAndSatsTidspunkt(behandling.fagsak.id, satsTidspunkt = SATSENDRINGMÅNED_MARS_2023) + assertThat(satskjøring?.ferdigTidspunkt) + .isCloseTo(LocalDateTime.now(), Assertions.within(30, ChronoUnit.SECONDS)) + } + + private val matrikkeladresse = Matrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ) + private val restScenario = RestScenario( + søker = RestScenarioPerson(fødselsdato = "1993-01-12", fornavn = "Mor", etternavn = "Søker").copy( + bostedsadresser = mutableListOf( + Bostedsadresse( + angittFlyttedato = LocalDate.now().minusYears(10), + gyldigTilOgMed = null, + matrikkeladresse = matrikkeladresse, + ), + ), + ), + barna = listOf( + RestScenarioPerson( + fødselsdato = LocalDate.of(2023, 1, 1).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ).copy( + bostedsadresser = mutableListOf( + Bostedsadresse( + angittFlyttedato = LocalDate.now().minusYears(6), + gyldigTilOgMed = null, + matrikkeladresse = matrikkeladresse, + ), + ), + ), + ), + ) + + private fun opprettBehandling(scenario: RestScenario) = + behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.first().ident!!), + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + vedtakService = vedtakService, + stegService = stegService, + personidentService = personidentService, + brevmalService = brevmalService, + )!! +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/Datagenerator.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/Datagenerator.kt new file mode 100644 index 000000000..b0a6f614f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/Datagenerator.kt @@ -0,0 +1,195 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import no.nav.familie.ba.sak.ekstern.restDomene.NavnOgIdent +import no.nav.familie.ba.sak.ekstern.restDomene.RestJournalføring +import no.nav.familie.ba.sak.ekstern.restDomene.RestJournalpostDokument +import no.nav.familie.ba.sak.ekstern.restDomene.RestMinimalFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestPutVedtaksperiodeMedStandardbegrunnelser +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakType +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.kontrakter.ba.infotrygd.Barn +import no.nav.familie.kontrakter.ba.infotrygd.Delytelse +import no.nav.familie.kontrakter.ba.infotrygd.Sak +import no.nav.familie.kontrakter.ba.infotrygd.Stønad +import no.nav.familie.kontrakter.felles.journalpost.LogiskVedlegg +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import org.springframework.http.HttpHeaders +import java.time.LocalDate +import java.time.LocalDateTime + +fun lagMockRestJournalføring(bruker: NavnOgIdent): RestJournalføring = RestJournalføring( + avsender = bruker, + bruker = bruker, + datoMottatt = LocalDateTime.now().minusDays(10), + journalpostTittel = "Søknad om ordinær barnetrygd", + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + knyttTilFagsak = true, + opprettOgKnyttTilNyBehandling = true, + tilknyttedeBehandlingIder = emptyList(), + dokumenter = listOf( + RestJournalpostDokument( + dokumentTittel = "Søknad om barnetrygd", + brevkode = "mock", + dokumentInfoId = "1", + logiskeVedlegg = listOf(LogiskVedlegg("123", "Oppholdstillatelse")), + eksisterendeLogiskeVedlegg = emptyList(), + ), + RestJournalpostDokument( + dokumentTittel = "Ekstra vedlegg", + brevkode = "mock", + dokumentInfoId = "2", + logiskeVedlegg = listOf(LogiskVedlegg("123", "Pass")), + eksisterendeLogiskeVedlegg = emptyList(), + ), + ), + navIdent = "09123", + nyBehandlingstype = BehandlingType.FØRSTEGANGSBEHANDLING, + nyBehandlingsårsak = BehandlingÅrsak.SØKNAD, + fagsakType = FagsakType.NORMAL, +) + +fun lagInfotrygdSak(beløp: Double, identBarn: List, valg: String? = "OR", undervalg: String? = "OS"): Sak { + return Sak( + stønad = Stønad( + barn = identBarn.map { Barn(it, barnetrygdTom = "000000") }, + delytelse = listOf( + Delytelse( + fom = LocalDate.now(), + tom = null, + beløp = beløp, + typeDelytelse = "MS", + typeUtbetaling = "J", + ), + ), + opphørsgrunn = "0", + antallBarn = identBarn.size, + mottakerNummer = 80000123456, + status = "04", + virkningFom = "797790", + ), + status = "FB", + valg = valg, + undervalg = undervalg, + ) +} + +fun fullførBehandlingFraVilkårsvurderingAlleVilkårOppfylt( + restUtvidetBehandling: RestUtvidetBehandling, + personScenario: RestScenario, + fagsak: RestMinimalFagsak, + familieBaSakKlient: FamilieBaSakKlient, + lagToken: (Map) -> String, + behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + fagsakService: FagsakService, + vedtakService: VedtakService, + stegService: StegService, + brevmalService: BrevmalService, + vedtaksperiodeService: VedtaksperiodeService, +): Behandling { + settAlleVilkårTilOppfylt( + restUtvidetBehandling = restUtvidetBehandling, + barnFødselsdato = personScenario.barna.maxOf { LocalDate.parse(it.fødselsdato) }, + familieBaSakKlient = familieBaSakKlient, + ) + + familieBaSakKlient.validerVilkårsvurdering( + behandlingId = restUtvidetBehandling.behandlingId, + ) + + val restUtvidetBehandlingEtterBehandlingsResultat = + familieBaSakKlient.behandlingsresultatStegOgGåVidereTilNesteSteg( + behandlingId = restUtvidetBehandling.behandlingId, + ) + + val restUtvidetBehandlingEtterVurderTilbakekreving = + familieBaSakKlient.lagreTilbakekrevingOgGåVidereTilNesteSteg( + restUtvidetBehandlingEtterBehandlingsResultat.data!!.behandlingId, + RestTilbakekreving(Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, begrunnelse = "begrunnelse"), + ) + + val vedtaksperioderMedBegrunnelser = vedtaksperiodeService.hentRestUtvidetVedtaksperiodeMedBegrunnelser( + restUtvidetBehandlingEtterVurderTilbakekreving.data!!.behandlingId, + ) + + val utvidetVedtaksperiodeMedBegrunnelser = + vedtaksperioderMedBegrunnelser.sortedBy { it.fom }.first() + + familieBaSakKlient.oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = utvidetVedtaksperiodeMedBegrunnelser.id, + restPutVedtaksperiodeMedStandardbegrunnelser = RestPutVedtaksperiodeMedStandardbegrunnelser( + standardbegrunnelser = utvidetVedtaksperiodeMedBegrunnelser.gyldigeBegrunnelser.filter(String::isNotEmpty), + ), + ) + val restUtvidetBehandlingEtterSendTilBeslutter = + familieBaSakKlient.sendTilBeslutter(behandlingId = restUtvidetBehandlingEtterVurderTilbakekreving.data!!.behandlingId) + + familieBaSakKlient.iverksettVedtak( + behandlingId = restUtvidetBehandlingEtterSendTilBeslutter.data!!.behandlingId, + restBeslutningPåVedtak = RestBeslutningPåVedtak( + Beslutning.GODKJENT, + ), + beslutterHeaders = HttpHeaders().apply { + setBearerAuth( + lagToken( + mapOf( + "groups" to listOf("SAKSBEHANDLER", "BESLUTTER"), + "azp" to "azp-test", + "name" to "Mock McMockface Beslutter", + "NAVident" to "Z0000", + ), + ), + ) + }, + ) + return håndterIverksettingAvBehandling( + behandlingEtterVurdering = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = fagsak.id)!!, + søkerFnr = personScenario.søker.ident!!, + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + ) +} + +fun settAlleVilkårTilOppfylt( + restUtvidetBehandling: RestUtvidetBehandling, + barnFødselsdato: LocalDate, + familieBaSakKlient: FamilieBaSakKlient, +) { + restUtvidetBehandling.personResultater.forEach { restPersonResultat -> + restPersonResultat.vilkårResultater.filter { it.resultat == Resultat.IKKE_VURDERT }.forEach { + familieBaSakKlient.putVilkår( + behandlingId = restUtvidetBehandling.behandlingId, + vilkårId = it.id, + restPersonResultat = + RestPersonResultat( + personIdent = restPersonResultat.personIdent, + vilkårResultater = listOf( + it.copy( + resultat = Resultat.OPPFYLT, + periodeFom = barnFødselsdato, + ), + ), + ), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/EndretUtbetalingAndelMedUtvidetAndelTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/EndretUtbetalingAndelMedUtvidetAndelTest.kt new file mode 100644 index 000000000..200dd2ecf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/EndretUtbetalingAndelMedUtvidetAndelTest.kt @@ -0,0 +1,165 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.ekstern.restDomene.RestEndretUtbetalingAndel +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.kontrakter.felles.Ressurs +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate + +class EndretUtbetalingAndelMedUtvidetAndelTest( + @Autowired private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, +) : AbstractVerdikjedetest() { + + @Test + fun `Skal teste at endret utbetalingsandeler for ordinær og utvidet endrer utbetaling for søker og barn`() { + val barnFødselsdato = LocalDate.now().minusYears(3) + + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1996-01-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = barnFødselsdato.toString(), + fornavn = "Barn", + etternavn = "Barnesen", + bostedsadresser = emptyList(), + ), + ), + ), + ) + + val søkersIdent = scenario.søker.ident!! + + val fagsak = familieBaSakKlient().opprettFagsak(søkersIdent = søkersIdent) + val restUtvidetBehandling = familieBaSakKlient().opprettBehandling( + søkersIdent = søkersIdent, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + fagsakId = fagsak.data!!.id, + ).data!! + + val restRegistrerSøknad = + RestRegistrerSøknad( + søknad = lagSøknadDTO( + søkerIdent = scenario.søker.ident, + barnasIdenter = scenario.barna.map { it.ident!! }, + underkategori = BehandlingUnderkategori.UTVIDET, + ), + bekreftEndringerViaFrontend = false, + ) + val restBehandlingEtterRegistrertSøknad: Ressurs = + familieBaSakKlient().registrererSøknad( + behandlingId = restUtvidetBehandling.behandlingId, + restRegistrerSøknad = restRegistrerSøknad, + ) + + restBehandlingEtterRegistrertSøknad.data!!.personResultater.forEach { restPersonResultat -> + restPersonResultat.vilkårResultater.filter { it.resultat == Resultat.IKKE_VURDERT }.forEach { + familieBaSakKlient().putVilkår( + behandlingId = restBehandlingEtterRegistrertSøknad.data?.behandlingId!!, + vilkårId = it.id, + restPersonResultat = + RestPersonResultat( + personIdent = restPersonResultat.personIdent, + vilkårResultater = listOf( + it.copy( + resultat = Resultat.OPPFYLT, + periodeFom = barnFødselsdato, + utdypendeVilkårsvurderinger = listOfNotNull( + if (it.vilkårType == Vilkår.BOR_MED_SØKER) UtdypendeVilkårsvurdering.DELT_BOSTED else null, + ), + ), + ), + ), + ) + } + } + + val restBehandlingEtterBehandlingsresultat = familieBaSakKlient().validerVilkårsvurdering( + behandlingId = restBehandlingEtterRegistrertSøknad.data?.behandlingId!!, + ).data!! + + val endretFom = barnFødselsdato.nesteMåned() + val endretTom = endretFom.plusMonths(2) + + val restEndretUtbetalingAndelUtvidetBarnetrygd = RestEndretUtbetalingAndel( + id = null, + personIdent = scenario.søker.ident, + prosent = BigDecimal(0), + fom = endretFom, + tom = endretTom, + årsak = Årsak.DELT_BOSTED, + avtaletidspunktDeltBosted = LocalDate.now(), + søknadstidspunkt = LocalDate.now(), + begrunnelse = "begrunnelse", + erTilknyttetAndeler = true, + ) + + familieBaSakKlient().leggTilEndretUtbetalingAndel( + restBehandlingEtterBehandlingsresultat.behandlingId, + restEndretUtbetalingAndelUtvidetBarnetrygd, + ) + + val restEndretUtbetalingAndelOrdinærBarnetrygd = RestEndretUtbetalingAndel( + id = null, + personIdent = scenario.barna.first().ident, + prosent = BigDecimal(0), + fom = endretFom, + tom = endretTom, + årsak = Årsak.DELT_BOSTED, + avtaletidspunktDeltBosted = LocalDate.now(), + søknadstidspunkt = LocalDate.now(), + begrunnelse = "begrunnelse", + erTilknyttetAndeler = true, + ) + + familieBaSakKlient().leggTilEndretUtbetalingAndel( + restBehandlingEtterBehandlingsresultat.behandlingId, + restEndretUtbetalingAndelOrdinærBarnetrygd, + ) + + familieBaSakKlient().behandlingsresultatStegOgGåVidereTilNesteSteg( + behandlingId = restBehandlingEtterBehandlingsresultat.behandlingId, + ) + + val andelerTilkjentYtelseMedEndretPeriode = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId = restBehandlingEtterBehandlingsresultat.behandlingId) + + val endredeAndelerTilkjentYtelse = + andelerTilkjentYtelseMedEndretPeriode.filter { it.kalkulertUtbetalingsbeløp == 0 } + + Assertions.assertEquals( + endredeAndelerTilkjentYtelse.single { it.aktør.aktivFødselsnummer() == scenario.barna.first().ident }.stønadFom, + endretFom, + ) + + Assertions.assertEquals( + endredeAndelerTilkjentYtelse.single { it.aktør.aktivFødselsnummer() == scenario.barna.first().ident }.stønadTom, + endretTom, + ) + + Assertions.assertEquals( + endredeAndelerTilkjentYtelse.single { it.aktør.aktivFødselsnummer() == scenario.søker.ident }.stønadFom, + endretFom, + ) + + Assertions.assertEquals( + endredeAndelerTilkjentYtelse.single { it.aktør.aktivFødselsnummer() == scenario.søker.ident }.stønadTom, + endretTom, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/EndretUtbetalingAndelTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/EndretUtbetalingAndelTest.kt new file mode 100644 index 000000000..72905f728 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/EndretUtbetalingAndelTest.kt @@ -0,0 +1,201 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.ekstern.restDomene.RestEndretUtbetalingAndel +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.kontrakter.felles.Ressurs +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class EndretUtbetalingAndelTest( + @Autowired private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, +) : AbstractVerdikjedetest() { + + @Test + fun `Skal teste at endret utbetalingsandel overskriver eksisterende utbetalingsandel`() { + val (scenario, restUtvidetBehandling) = genererBehandlingsresultat() + + val endretFom = YearMonth.of(2021, 9) + val endretTom = YearMonth.of(2021, 11) + + val restEndretUtbetalingAndel = RestEndretUtbetalingAndel( + id = null, + personIdent = scenario.barna.first().ident, + prosent = BigDecimal(0), + fom = endretFom, + tom = endretTom, + årsak = Årsak.DELT_BOSTED, avtaletidspunktDeltBosted = LocalDate.now(), + søknadstidspunkt = LocalDate.now(), + begrunnelse = "begrunnelse", + erTilknyttetAndeler = true, + ) + + familieBaSakKlient().leggTilEndretUtbetalingAndel( + restUtvidetBehandling.data!!.behandlingId, + restEndretUtbetalingAndel, + ) + + val andelerTilkjentYtelseMedEndretPeriode = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId = restUtvidetBehandling.data!!.behandlingId) + + val endretAndeleTilkjentYtelse = + andelerTilkjentYtelseMedEndretPeriode.single { it.kalkulertUtbetalingsbeløp == 0 } + + Assertions.assertEquals( + endretFom, + endretAndeleTilkjentYtelse.stønadFom, + ) + + Assertions.assertEquals( + endretTom, + endretAndeleTilkjentYtelse.stønadTom, + ) + + val utbetalingAndeleTilkjentYtelse = + andelerTilkjentYtelseMedEndretPeriode.filter { it.kalkulertUtbetalingsbeløp != 0 } + + Assertions.assertNotNull( + utbetalingAndeleTilkjentYtelse.firstOrNull { it.stønadTom == endretFom.minusMonths(1) }, + ) + + Assertions.assertNotNull( + utbetalingAndeleTilkjentYtelse.firstOrNull { it.stønadFom == endretTom.plusMonths(1) }, + ) + } + + @Test + fun `Skal teste at fjernet endret utbetalingsandel oppretter tidligere eksisterende utbetalingsandel`() { + val (scenario, restUtvidetBehandling) = genererBehandlingsresultat() + + val endretFom = YearMonth.of(2021, 9) + val endretTom = YearMonth.of(2021, 11) + + val restEndretUtbetalingAndel = RestEndretUtbetalingAndel( + id = null, + personIdent = scenario.barna.first().ident, + prosent = BigDecimal(0), + fom = endretFom, + tom = endretTom, + årsak = Årsak.DELT_BOSTED, + avtaletidspunktDeltBosted = LocalDate.now(), + søknadstidspunkt = LocalDate.now(), + begrunnelse = "begrunnelse", + erTilknyttetAndeler = true, + ) + + val restUtvidetBehandlingEtterEndretPeriode = familieBaSakKlient().leggTilEndretUtbetalingAndel( + restUtvidetBehandling.data!!.behandlingId, + restEndretUtbetalingAndel, + ) + + val endretUtbetalingAndelId = + restUtvidetBehandlingEtterEndretPeriode.data!!.endretUtbetalingAndeler.first().id + + familieBaSakKlient().fjernEndretUtbetalingAndel( + restUtvidetBehandling.data!!.behandlingId, + endretUtbetalingAndelId!!, + ) + + val andelerTilkjentYtelseEtterFjeringAvEndretUtbetaling = + andelTilkjentYtelseRepository.finnAndelerTilkjentYtelseForBehandling(behandlingId = restUtvidetBehandling.data!!.behandlingId) + + Assertions.assertNotNull( + andelerTilkjentYtelseEtterFjeringAvEndretUtbetaling.firstOrNull { it.stønadFom == endretFom }, + ) + + Assertions.assertNotNull( + andelerTilkjentYtelseEtterFjeringAvEndretUtbetaling.firstOrNull { it.stønadTom == YearMonth.of(2021, 12) }, + ) + } + + private fun genererBehandlingsresultat(): Pair> { + val barnFødselsdato = LocalDate.of(2020, 1, 3) + + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1996-01-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = barnFødselsdato.toString(), + fornavn = "Barn", + etternavn = "Barnesen", + bostedsadresser = emptyList(), + ), + ), + ), + ) + + val søkersIdent = scenario.søker.ident!! + + val fagsak = familieBaSakKlient().opprettFagsak(søkersIdent = søkersIdent) + val restFagsakMedBehandling = familieBaSakKlient().opprettBehandling( + søkersIdent = søkersIdent, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + fagsakId = fagsak.data!!.id, + ) + + val behandling = behandlingHentOgPersisterService.hent(restFagsakMedBehandling.data!!.behandlingId) + val restRegistrerSøknad = + RestRegistrerSøknad( + søknad = lagSøknadDTO( + søkerIdent = scenario.søker.ident, + barnasIdenter = scenario.barna.map { it.ident!! }, + underkategori = BehandlingUnderkategori.ORDINÆR, + ), + bekreftEndringerViaFrontend = false, + ) + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().registrererSøknad( + behandlingId = behandling.id, + restRegistrerSøknad = restRegistrerSøknad, + ) + + restUtvidetBehandling.data!!.personResultater.forEach { restPersonResultat -> + restPersonResultat.vilkårResultater.filter { it.resultat == Resultat.IKKE_VURDERT }.forEach { + familieBaSakKlient().putVilkår( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + vilkårId = it.id, + restPersonResultat = + RestPersonResultat( + personIdent = restPersonResultat.personIdent, + vilkårResultater = listOf( + it.copy( + resultat = Resultat.OPPFYLT, + periodeFom = barnFødselsdato, + utdypendeVilkårsvurderinger = listOfNotNull( + if (it.vilkårType == Vilkår.BOR_MED_SØKER) UtdypendeVilkårsvurdering.DELT_BOSTED else null, + ), + ), + ), + ), + ) + } + } + + familieBaSakKlient().validerVilkårsvurdering( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + ) + + val restFagsakEtterBehandlingsresultat = + familieBaSakKlient().behandlingsresultatStegOgGåVidereTilNesteSteg( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + ) + return Pair(scenario, restFagsakEtterBehandlingsresultat) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/EndringstidspunktTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/EndringstidspunktTest.kt new file mode 100644 index 000000000..b41fb840e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/EndringstidspunktTest.kt @@ -0,0 +1,160 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import io.mockk.mockk +import no.nav.familie.ba.sak.common.Feil +import no.nav.familie.ba.sak.common.førsteDagINesteMåned +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.datagenerator.behandling.kjørStegprosessForBehandling +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagVilkårsvurderingFraRestScenario +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class EndringstidspunktTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val stegService: StegService, + @Autowired private val persongrunnlagService: PersongrunnlagService, + @Autowired private val vilkårsvurderingService: VilkårsvurderingService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, + @Autowired private val endretUtbetalingAndelHentOgPersisterService: EndretUtbetalingAndelHentOgPersisterService, + @Autowired private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + @Autowired private val brevmalService: BrevmalService, +) : AbstractVerdikjedetest() { + + @Test + fun `Skal filtrere bort alle vedtaksperioder før endringstidspunktet`() { + val barnFødselsdato = LocalDate.now().minusYears(2) + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson( + fødselsdato = "1982-01-12", + fornavn = "Mor", + etternavn = "Søker", + ), + barna = listOf( + RestScenarioPerson( + fødselsdato = barnFødselsdato.toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + ), + ), + ) + + val overstyrendeVilkårResultaterFGB = + scenario.barna.associate { it.aktørId!! to emptyList() }.toMutableMap() + + overstyrendeVilkårResultaterFGB[scenario.søker.aktørId!!] = emptyList() + + kjørStegprosessForBehandling( + tilSteg = StegType.BEHANDLING_AVSLUTTET, + søkerFnr = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.first().ident!!), + vedtakService = vedtakService, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + overstyrendeVilkårsvurdering = lagVilkårsvurderingFraRestScenario( + scenario, + overstyrendeVilkårResultaterFGB, + ), + + behandlingstype = BehandlingType.FØRSTEGANGSBEHANDLING, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + endretUtbetalingAndelHentOgPersisterService = endretUtbetalingAndelHentOgPersisterService, + fagsakService = fagsakService, + persongrunnlagService = persongrunnlagService, + andelerTilkjentYtelseOgEndreteUtbetalingerService = andelerTilkjentYtelseOgEndreteUtbetalingerService, + brevmalService = brevmalService, + ) + + val sisteDagUtenDeltBostedOppfylt = barnFødselsdato.plusYears(1).sisteDagIMåned() + val førsteDagMedDeltBostedOppfylt = sisteDagUtenDeltBostedOppfylt.førsteDagINesteMåned() + val sisteDagMedDeltBosdet = sisteDagUtenDeltBostedOppfylt.plusMonths(2).sisteDagIMåned() + + val overstyrendeVilkårResultaterRevurdering = + scenario.barna.associate { + it.aktørId!! to listOf( + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + periodeFom = barnFødselsdato, + periodeTom = førsteDagMedDeltBostedOppfylt, + personResultat = mockk(relaxed = true), + ), + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + periodeFom = førsteDagMedDeltBostedOppfylt, + periodeTom = sisteDagMedDeltBosdet, + personResultat = mockk(relaxed = true), + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + periodeFom = førsteDagMedDeltBostedOppfylt, + periodeTom = sisteDagMedDeltBosdet.førsteDagINesteMåned(), + personResultat = mockk(relaxed = true), + utdypendeVilkårsvurderinger = listOf(UtdypendeVilkårsvurdering.DELT_BOSTED), + ), + ) + }.toMutableMap() + + overstyrendeVilkårResultaterRevurdering[scenario.søker.aktørId] = emptyList() + + val revurdering = kjørStegprosessForBehandling( + tilSteg = StegType.BEHANDLING_AVSLUTTET, + søkerFnr = scenario.søker.ident, + barnasIdenter = listOf(scenario.barna.first().ident!!), + vedtakService = vedtakService, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingÅrsak = BehandlingÅrsak.NYE_OPPLYSNINGER, + overstyrendeVilkårsvurdering = lagVilkårsvurderingFraRestScenario( + scenario, + overstyrendeVilkårResultaterRevurdering, + ), + + behandlingstype = BehandlingType.REVURDERING, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + endretUtbetalingAndelHentOgPersisterService = endretUtbetalingAndelHentOgPersisterService, + fagsakService = fagsakService, + persongrunnlagService = persongrunnlagService, + andelerTilkjentYtelseOgEndreteUtbetalingerService = andelerTilkjentYtelseOgEndreteUtbetalingerService, + brevmalService = brevmalService, + ) + + val vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingId = revurdering.id) + val vedtaksperioder = vedtaksperiodeService.hentPersisterteVedtaksperioder(vedtak) + + val førsteFomDatoIVedtaksperiodene = + vedtaksperioder.minOf { it.fom ?: throw Feil("Ingen fom-dato") } + + assertTrue( + førsteFomDatoIVedtaksperiodene.isEqual( + førsteDagMedDeltBostedOppfylt.førsteDagINesteMåned(), + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/FamilieBaSakKlient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/FamilieBaSakKlient.kt new file mode 100644 index 000000000..00dfd48b2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/FamilieBaSakKlient.kt @@ -0,0 +1,219 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import no.nav.familie.ba.sak.ekstern.restDomene.RestEndretUtbetalingAndel +import no.nav.familie.ba.sak.ekstern.restDomene.RestFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestHentFagsakForPerson +import no.nav.familie.ba.sak.ekstern.restDomene.RestJournalføring +import no.nav.familie.ba.sak.ekstern.restDomene.RestMinimalFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestPutVedtaksperiodeMedStandardbegrunnelser +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.DEFAULT_JOURNALFØRENDE_ENHET +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.RestHenleggBehandlingInfo +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.brev.domene.ManueltBrevRequest +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRequest +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.logg.Logg +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.domene.RestUtvidetVedtaksperiodeMedBegrunnelser +import no.nav.familie.http.client.AbstractRestClient +import no.nav.familie.kontrakter.felles.Ressurs +import org.springframework.http.HttpHeaders +import org.springframework.web.client.RestOperations +import org.springframework.web.util.UriUtils.encodePath +import java.net.URI +import java.time.LocalDate + +class FamilieBaSakKlient( + private val baSakUrl: String, + restOperations: RestOperations, + private val headers: HttpHeaders, +) : AbstractRestClient(restOperations, "familie-ba-sak") { + + fun opprettFagsak(søkersIdent: String): Ressurs { + val uri = URI.create("$baSakUrl/api/fagsaker") + + return postForEntity( + uri, + FagsakRequest( + personIdent = søkersIdent, + ), + headers, + ) + } + + fun hentFagsak(fagsakId: Long): Ressurs { + val uri = URI.create("$baSakUrl/api/fagsaker/$fagsakId") + + return getForEntity( + uri, + headers, + ) + } + + fun hentMinimalFagsakPåPerson(personIdent: String): Ressurs { + val uri = URI.create("$baSakUrl/api/fagsaker/hent-fagsak-paa-person") + + return postForEntity( + uri, + RestHentFagsakForPerson(personIdent), + headers, + ) + } + + fun journalfør( + journalpostId: String, + oppgaveId: String, + journalførendeEnhet: String, + restJournalføring: RestJournalføring, + ): Ressurs { + val uri = + URI.create(encodePath("$baSakUrl/api/journalpost/$journalpostId/journalfør/$oppgaveId") + "?journalfoerendeEnhet=$journalførendeEnhet") + return postForEntity( + uri, + restJournalføring, + headers, + ) + } + + fun behandlingsresultatStegOgGåVidereTilNesteSteg(behandlingId: Long): Ressurs { + val uri = URI.create("$baSakUrl/api/behandlinger/$behandlingId/steg/behandlingsresultat") + + return postForEntity(uri, "", headers) + } + + fun henleggSøknad( + behandlingId: Long, + restHenleggBehandlingInfo: RestHenleggBehandlingInfo, + ): Ressurs { + val uri = URI.create("$baSakUrl/api/behandlinger/$behandlingId/steg/henlegg") + return putForEntity(uri, restHenleggBehandlingInfo, headers) + } + + fun hentBehandlingslogg(behandlingId: Long): Ressurs> { + val uri = URI.create("$baSakUrl/api/logg/$behandlingId") + return getForEntity(uri, headers) + } + + fun opprettBehandling( + søkersIdent: String, + behandlingType: BehandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + behandlingÅrsak: BehandlingÅrsak = BehandlingÅrsak.SØKNAD, + behandlingUnderkategori: BehandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + fagsakId: Long, + ): Ressurs { + val uri = URI.create("$baSakUrl/api/behandlinger") + + return postForEntity( + uri, + NyBehandling( + kategori = BehandlingKategori.NASJONAL, + underkategori = behandlingUnderkategori, + søkersIdent = søkersIdent, + behandlingType = behandlingType, + behandlingÅrsak = behandlingÅrsak, + søknadMottattDato = if (behandlingÅrsak == BehandlingÅrsak.SØKNAD) LocalDate.now() else null, + fagsakId = fagsakId, + ), + headers, + ) + } + + fun registrererSøknad( + behandlingId: Long, + restRegistrerSøknad: RestRegistrerSøknad, + ): Ressurs { + val uri = + URI.create(encodePath("$baSakUrl/api/behandlinger/$behandlingId/steg/registrer-søknad", "UTF-8")) + + return postForEntity(uri, restRegistrerSøknad, headers) + } + + fun putVilkår( + behandlingId: Long, + vilkårId: Long, + restPersonResultat: RestPersonResultat, + ): Ressurs { + val uri = URI.create(encodePath("$baSakUrl/api/vilkaarsvurdering/$behandlingId/$vilkårId")) + + return putForEntity(uri, restPersonResultat, headers) + } + + fun validerVilkårsvurdering(behandlingId: Long): Ressurs { + val uri = URI.create(encodePath("$baSakUrl/api/behandlinger/$behandlingId/steg/vilkårsvurdering")) + return postForEntity(uri, "", headers) + } + + fun lagreTilbakekrevingOgGåVidereTilNesteSteg( + behandlingId: Long, + restTilbakekreving: RestTilbakekreving, + ): Ressurs { + val uri = URI.create("$baSakUrl/api/behandlinger/$behandlingId/steg/tilbakekreving") + + return postForEntity(uri, restTilbakekreving, headers) + } + + fun oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId: Long, + restPutVedtaksperiodeMedStandardbegrunnelser: RestPutVedtaksperiodeMedStandardbegrunnelser, + ): Ressurs> { + val uri = URI.create("$baSakUrl/api/vedtaksperioder/standardbegrunnelser/$vedtaksperiodeId") + + return putForEntity(uri, restPutVedtaksperiodeMedStandardbegrunnelser, headers) + } + + fun sendTilBeslutter(behandlingId: Long): Ressurs { + val uri = + URI.create("$baSakUrl/api/behandlinger/$behandlingId/steg/send-til-beslutter?behandlendeEnhet=$DEFAULT_JOURNALFØRENDE_ENHET") + + return postForEntity(uri, "", headers) + } + + fun leggTilEndretUtbetalingAndel( + behandlingId: Long, + restEndretUtbetalingAndel: RestEndretUtbetalingAndel, + ): Ressurs { + val uriPost = URI.create("$baSakUrl/api/endretutbetalingandel/$behandlingId") + val restUtvidetBehandling = postForEntity>(uriPost, "", headers) + + val endretUtbetalingAndelId = + restUtvidetBehandling.data!!.endretUtbetalingAndeler.first { it.tom == null && it.fom == null }.id + val uriPut = URI.create("$baSakUrl/api/endretutbetalingandel/$behandlingId/$endretUtbetalingAndelId") + + return putForEntity(uriPut, restEndretUtbetalingAndel, headers) + } + + fun fjernEndretUtbetalingAndel( + behandlingId: Long, + endretUtbetalingAndelId: Long, + ): Ressurs { + val uri = URI.create("$baSakUrl/api/endretutbetalingandel/$behandlingId/$endretUtbetalingAndelId") + + return deleteForEntity(uri, "", headers) + } + + fun iverksettVedtak( + behandlingId: Long, + restBeslutningPåVedtak: RestBeslutningPåVedtak, + beslutterHeaders: HttpHeaders, + ): Ressurs { + val uri = URI.create("$baSakUrl/api/behandlinger/$behandlingId/steg/iverksett-vedtak") + + return postForEntity(uri, restBeslutningPåVedtak, beslutterHeaders) + } + + fun forhaandsvisHenleggelseBrev(behandlingId: Long, manueltBrevRequest: ManueltBrevRequest): Ressurs { + val uri = URI.create("$baSakUrl/api/dokument/forhaandsvis-brev/$behandlingId") + return postForEntity(uri, manueltBrevRequest, headers) + } + + fun encodePath(path: String): String { + return encodePath(path, "UTF-8") + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/F\303\270dselshendelseF\303\270rstegangsbehandlingTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/F\303\270dselshendelseF\303\270rstegangsbehandlingTest.kt" new file mode 100644 index 000000000..8aba1484e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/F\303\270dselshendelseF\303\270rstegangsbehandlingTest.kt" @@ -0,0 +1,174 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import io.mockk.every +import no.nav.familie.ba.sak.common.LocalDateService +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.totaltUtbetalt +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.task.BehandleFødselshendelseTask +import no.nav.familie.ba.sak.util.tilleggOrdinærSatsNesteMånedTilTester +import no.nav.familie.kontrakter.felles.getDataOrThrow +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class FødselshendelseFørstegangsbehandlingTest( + @Autowired private val behandleFødselshendelseTask: BehandleFødselshendelseTask, + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val personidentService: PersonidentService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val stegService: StegService, + @Autowired private val mockLocalDateService: LocalDateService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, + @Autowired private val brevmalService: BrevmalService, +) : AbstractVerdikjedetest() { + + @Test + fun `Skal innvilge fødselshendelse på mor med 1 barn født november 2021 og behandles desember 2021 uten utbetalinger`() { + // Behandler desember 2021 for å få med automatisk begrunnelse av satsendring januar 2022 + every { mockLocalDateService.now() } returns LocalDate.of(2021, 12, 12) andThen LocalDate.now() + + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1996-01-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = LocalDate.of(2021, 11, 18).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + ), + ), + ) + val behandling = behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.first().ident!!), + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + vedtakService = vedtakService, + stegService = stegService, + personidentService = personidentService, + brevmalService = brevmalService, + + ) + + val restFagsakEtterBehandlingAvsluttet = + familieBaSakKlient().hentFagsak(fagsakId = behandling!!.fagsak.id) + generellAssertFagsak( + restFagsak = restFagsakEtterBehandlingAvsluttet, + fagsakStatus = FagsakStatus.LØPENDE, + behandlingStegType = StegType.BEHANDLING_AVSLUTTET, + ) + + val aktivBehandling = restFagsakEtterBehandlingAvsluttet.getDataOrThrow().behandlinger.single() + assertEquals(Behandlingsresultat.INNVILGET, aktivBehandling.resultat) + + val vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingId = aktivBehandling.behandlingId) + val vedtaksperioder = vedtaksperiodeService.hentUtvidetVedtaksperiodeMedBegrunnelser(vedtak = vedtak) + + val desember2021Vedtaksperiode = vedtaksperioder.find { it.fom == LocalDate.of(2021, 12, 1) } + val januar2022Vedtaksperiode = vedtaksperioder.find { it.fom == LocalDate.of(2022, 1, 1) } + + assertEquals( + 0, + vedtaksperioder + .filter { it != desember2021Vedtaksperiode && it != januar2022Vedtaksperiode } + .flatMap { it.begrunnelser } + .size, + ) + + assertEquals( + 1654, + desember2021Vedtaksperiode?.utbetalingsperiodeDetaljer?.totaltUtbetalt(), + ) + assertEquals( + Standardbegrunnelse.INNVILGET_FØDSELSHENDELSE_NYFØDT_BARN_FØRSTE, + desember2021Vedtaksperiode?.begrunnelser?.first()?.standardbegrunnelse, + ) + + assertEquals( + 1676, + januar2022Vedtaksperiode?.utbetalingsperiodeDetaljer?.totaltUtbetalt(), + ) + assertEquals( + Standardbegrunnelse.INNVILGET_SATSENDRING, + januar2022Vedtaksperiode?.begrunnelser?.first()?.standardbegrunnelse, + ) + } + + @Test + fun `Skal innvilge fødselshendelse på mor med 2 barn uten utbetalinger`() { + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1996-01-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = LocalDate.now().minusDays(2).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + RestScenarioPerson( + fødselsdato = LocalDate.now().minusDays(2).toString(), + fornavn = "Barn", + etternavn = "Barnesen 2", + ), + ), + ), + ) + val behandling = behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident!!, + barnasIdenter = scenario.barna.map { it.ident!! }, + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + personidentService = personidentService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + + ) + + val restFagsakEtterBehandlingAvsluttet = + familieBaSakKlient().hentFagsak(fagsakId = behandling!!.fagsak.id) + generellAssertFagsak( + restFagsak = restFagsakEtterBehandlingAvsluttet, + fagsakStatus = FagsakStatus.LØPENDE, + behandlingStegType = StegType.BEHANDLING_AVSLUTTET, + ) + + val aktivBehandling = restFagsakEtterBehandlingAvsluttet.getDataOrThrow().behandlinger.single() + assertEquals(Behandlingsresultat.INNVILGET, aktivBehandling.resultat) + + val utbetalingsperioder = aktivBehandling.utbetalingsperioder + val gjeldendeUtbetalingsperiode = utbetalingsperioder.find { + it.periodeFom.toYearMonth() >= tilleggOrdinærSatsNesteMånedTilTester().gyldigFom.toYearMonth() && + it.periodeFom.toYearMonth() <= tilleggOrdinærSatsNesteMånedTilTester().gyldigTom.toYearMonth() + }!! + + assertUtbetalingsperiode( + gjeldendeUtbetalingsperiode, + 2, + tilleggOrdinærSatsNesteMånedTilTester().beløp * 2, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/F\303\270dselshendelseHenleggelseTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/F\303\270dselshendelseHenleggelseTest.kt" new file mode 100644 index 000000000..7014314a6 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/F\303\270dselshendelseHenleggelseTest.kt" @@ -0,0 +1,540 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import io.mockk.verify +import no.nav.familie.ba.sak.common.førsteDagINesteMåned +import no.nav.familie.ba.sak.common.kjørStegprosessForFGB +import no.nav.familie.ba.sak.common.tilKortString +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.vilkårsvurdering.utfall.VilkårKanskjeOppfyltÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.beregning.SatsTidspunkt +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.defaultBostedsadresseHistorikk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.task.BehandleFødselshendelseTask +import no.nav.familie.ba.sak.task.OpprettOppgaveTask +import no.nav.familie.ba.sak.task.OpprettTaskService +import no.nav.familie.ba.sak.task.dto.ManuellOppgaveType +import no.nav.familie.ba.sak.util.sisteTilleggOrdinærSats +import no.nav.familie.ba.sak.util.sisteUtvidetSatsTilTester +import no.nav.familie.ba.sak.util.tilleggOrdinærSatsNesteMånedTilTester +import no.nav.familie.kontrakter.ba.infotrygd.InfotrygdSøkResponse +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.Matrikkeladresse +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.LocalDate.now + +class FødselshendelseHenleggelseTest( + @Autowired private val opprettTaskService: OpprettTaskService, + @Autowired private val behandleFødselshendelseTask: BehandleFødselshendelseTask, + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val personidentService: PersonidentService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val stegService: StegService, + @Autowired private val persongrunnlagService: PersongrunnlagService, + @Autowired private val vilkårsvurderingService: VilkårsvurderingService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, + @Autowired private val utvidetBehandlingService: UtvidetBehandlingService, + @Autowired private val brevmalService: BrevmalService, + @Autowired private val opprettOppgaveForManuellBehandlingTask: OpprettOppgaveTask, +) : AbstractVerdikjedetest() { + + @BeforeEach + fun førHverTest() { + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2022, 12, 31) + } + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + @Test + fun `Skal ikke starte behandling i ba-sak fordi det finnes saker i infotrygd (velg fagsystem)`() { + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson( + fødselsdato = "1982-01-12", + fornavn = "Mor", + etternavn = "Søker", + infotrygdSaker = InfotrygdSøkResponse( + bruker = listOf( + lagInfotrygdSak( + sisteTilleggOrdinærSats(), + listOf("1234"), + "OR", + "OS", + ), + ), + barn = emptyList(), + ), + ), + barna = listOf( + RestScenarioPerson( + fødselsdato = now().minusMonths(2).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + ), + ), + ) + + val behandling = behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.first().ident!!), + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + vedtakService = vedtakService, + stegService = stegService, + personidentService = personidentService, + brevmalService = brevmalService, + + ) + assertNull(behandling) + + verify(exactly = 1) { + opprettTaskService.opprettSendFeedTilInfotrygdTask(scenario.barna.map { it.ident!! }) + } + } + + @Test + fun `Skal henlegge fødselshendelse på grunn av at søker er under 18 (filtreringsregel)`() { + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson( + fødselsdato = now().minusYears(16).toString(), + fornavn = "Mor", + etternavn = "Søker", + ), + barna = listOf( + RestScenarioPerson( + fødselsdato = now().minusMonths(2).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + ), + ), + ) + + val behandling = behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.first().ident!!), + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + personidentService = personidentService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + + ) + + assertEquals(Behandlingsresultat.HENLAGT_AUTOMATISK_FØDSELSHENDELSE, behandling?.resultat) + assertEquals(StegType.BEHANDLING_AVSLUTTET, behandling?.steg) + + verify(exactly = 1) { + opprettTaskService.opprettOppgaveForManuellBehandlingTask( + behandlingId = behandling!!.id, + beskrivelse = "Fødselshendelse: Mor er under 18 år.", + manuellOppgaveType = ManuellOppgaveType.FØDSELSHENDELSE, + ) + } + + val fagsak = + familieBaSakKlient().hentFagsak(fagsakId = behandling!!.fagsak.id).data + + val automatiskVurdertBehandling = fagsak?.behandlinger?.first { it.skalBehandlesAutomatisk }!! + assertEquals(0, automatiskVurdertBehandling.personResultater.size) + } + + @Test + fun `Skal henlegge fødselshendelse på grunn av at søker har flere adresser uten fom-dato (vilkårsvurdering)`() { + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson( + fødselsdato = "1993-01-12", + fornavn = "Mor", + etternavn = "Søker", + bostedsadresser = defaultBostedsadresseHistorikk + listOf( + Bostedsadresse( + angittFlyttedato = null, + gyldigTilOgMed = null, + matrikkeladresse = Matrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ), + ), + Bostedsadresse( + angittFlyttedato = null, + gyldigTilOgMed = null, + matrikkeladresse = Matrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ), + ), + Bostedsadresse( + angittFlyttedato = now(), + gyldigTilOgMed = null, + matrikkeladresse = Matrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ), + ), + ), + ), + barna = listOf( + RestScenarioPerson( + fødselsdato = now().toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + ), + ), + ) + + val behandling = behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.first().ident!!), + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + personidentService = personidentService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + + ) + + assertEquals(Behandlingsresultat.HENLAGT_AUTOMATISK_FØDSELSHENDELSE, behandling?.resultat) + assertEquals(StegType.BEHANDLING_AVSLUTTET, behandling?.steg) + + verify(exactly = 1) { + opprettTaskService.opprettOppgaveForManuellBehandlingTask( + behandlingId = behandling!!.id, + beskrivelse = "Fødselshendelse: Mor har flere bostedsadresser uten fra- og med dato", + manuellOppgaveType = ManuellOppgaveType.FØDSELSHENDELSE, + ) + } + } + + @Test + fun `Skal henlegge fødselshendelse på grunn av at barn ikke er bosatt i riket og bor ikke med mor (vilkårsvurdering)`() { + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1993-01-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = now().toString(), + fornavn = "Barn", + etternavn = "Barnesen", + bostedsadresser = emptyList(), + ), + ), + ), + ) + + val barnIdent = scenario.barna.first().ident!! + val behandling = behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.first().ident!!), + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + personidentService = personidentService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + + ) + + assertEquals(Behandlingsresultat.HENLAGT_AUTOMATISK_FØDSELSHENDELSE, behandling?.resultat) + assertEquals(StegType.BEHANDLING_AVSLUTTET, behandling?.steg) + + verify(exactly = 1) { + opprettTaskService.opprettOppgaveForManuellBehandlingTask( + behandlingId = behandling!!.id, + beskrivelse = "Fødselshendelse: Barnet (fødselsdato: ${ + LocalDate.parse(scenario.barna.first().fødselsdato) + .tilKortString() + }) er ikke bosatt med mor.", + manuellOppgaveType = ManuellOppgaveType.FØDSELSHENDELSE, + ) + } + + val fagsak = + familieBaSakKlient().hentFagsak(fagsakId = behandling!!.fagsak.id).data + + val automatiskVurdertBehandling = fagsak?.behandlinger?.first { it.skalBehandlesAutomatisk }!! + val borMedSøkerVikårForbarn = + automatiskVurdertBehandling.personResultater.firstOrNull { it.personIdent == barnIdent }?.vilkårResultater?.firstOrNull { it.vilkårType == Vilkår.BOR_MED_SØKER } + val bosattIRiketVikårForbarn = + automatiskVurdertBehandling.personResultater.firstOrNull { it.personIdent == barnIdent }?.vilkårResultater?.firstOrNull { it.vilkårType == Vilkår.BOSATT_I_RIKET } + + assertEquals(Resultat.IKKE_OPPFYLT, borMedSøkerVikårForbarn?.resultat) + assertEquals(Resultat.IKKE_OPPFYLT, bosattIRiketVikårForbarn?.resultat) + } + + @Test + fun `Skal henlegge fødselshendelse på grunn av at mor mottar utvidet barnetrygd (filtreringsregel)`() { + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson( + fødselsdato = now().minusYears(26).førsteDagINesteMåned().plusDays(6).toString(), + fornavn = "Mor", + etternavn = "Søker", + ), + barna = listOf( + RestScenarioPerson( + fødselsdato = now().minusMonths(2).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + RestScenarioPerson( + fødselsdato = now().minusYears(2).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + ), + ), + ) + + val behandling = kjørStegprosessForFGB( + tilSteg = StegType.BEHANDLING_AVSLUTTET, + søkerFnr = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.last().ident!!), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + brevmalService = brevmalService, + + ) + + assertEquals(BehandlingUnderkategori.UTVIDET, behandling.underkategori) + assertEquals( + tilleggOrdinærSatsNesteMånedTilTester().beløp + sisteUtvidetSatsTilTester(), + hentNåværendeEllerNesteMånedsUtbetaling( + behandling = utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandling.id), + ), + ) + + val revurdering = behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident, + barnasIdenter = listOf(scenario.barna.first().ident!!), + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + personidentService = personidentService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + + ) + + assertEquals(BehandlingUnderkategori.UTVIDET, revurdering?.underkategori) + assertEquals(Behandlingsresultat.HENLAGT_AUTOMATISK_FØDSELSHENDELSE, revurdering?.resultat) + assertEquals(StegType.BEHANDLING_AVSLUTTET, revurdering?.steg) + + verify(exactly = 1) { + opprettTaskService.opprettOppgaveForManuellBehandlingTask( + behandlingId = revurdering!!.id, + beskrivelse = "Fødselshendelse: Mor mottar utvidet barnetrygd.", + manuellOppgaveType = ManuellOppgaveType.FØDSELSHENDELSE, + ) + } + } + + @Test + fun `Skal henlegge fødselshendelse på grunn av at mor mottar EØS-barnetrygd (filtreringsregel)`() { + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson( + fødselsdato = now().minusYears(26).førsteDagINesteMåned().plusDays(6).toString(), + fornavn = "Mor", + etternavn = "Søker", + ), + barna = listOf( + RestScenarioPerson( + fødselsdato = now().minusMonths(2).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + RestScenarioPerson( + fødselsdato = now().minusYears(2).toString(), + fornavn = "Barn2", + etternavn = "Barnesen", + ), + ), + ), + ) + + val behandling = kjørStegprosessForFGB( + tilSteg = StegType.BEHANDLING_AVSLUTTET, + søkerFnr = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.last().ident!!), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + behandlingUnderkategori = BehandlingUnderkategori.ORDINÆR, + brevmalService = brevmalService, + + ) + + oppdaterRegelverkTilEøs(behandling) + + val revurdering = behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident, + barnasIdenter = listOf(scenario.barna.first().ident!!), + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + personidentService = personidentService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + + ) + + assertEquals(BehandlingKategori.EØS, revurdering?.kategori) + assertEquals(Behandlingsresultat.HENLAGT_AUTOMATISK_FØDSELSHENDELSE, revurdering?.resultat) + assertEquals(StegType.BEHANDLING_AVSLUTTET, revurdering?.steg) + + verify(exactly = 1) { + opprettTaskService.opprettOppgaveForManuellBehandlingTask( + behandlingId = revurdering!!.id, + beskrivelse = "Fødselshendelse: Mor har EØS-barnetrygd", + manuellOppgaveType = ManuellOppgaveType.FØDSELSHENDELSE, + ) + } + } + + @Test + fun `Skal sende tredjelandsborgere fra Ukraina til manuel oppfølging (midlertidig regel for ukrainakonflikten)`() { + val fødselsdato = "1993-01-12" + val barnFødselsdato = LocalDate.now() + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = fødselsdato, fornavn = "Mor", etternavn = "Søker").copy( + statsborgerskap = listOf( + Statsborgerskap( + land = "UKR", + gyldigFraOgMed = LocalDate.parse(fødselsdato), + bekreftelsesdato = LocalDate.parse(fødselsdato), + gyldigTilOgMed = null, + ), + ), + ), + barna = listOf( + RestScenarioPerson( + fødselsdato = barnFødselsdato.toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ).copy( + statsborgerskap = listOf( + Statsborgerskap( + land = "UKR", + gyldigFraOgMed = barnFødselsdato, + bekreftelsesdato = barnFødselsdato, + gyldigTilOgMed = null, + ), + ), + ), + ), + ), + ) + val behandling = behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.first().ident!!), + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + vedtakService = vedtakService, + stegService = stegService, + personidentService = personidentService, + brevmalService = brevmalService, + + )!! + + assertEquals(Behandlingsresultat.HENLAGT_AUTOMATISK_FØDSELSHENDELSE, behandling.resultat) + assertEquals(StegType.BEHANDLING_AVSLUTTET, behandling.steg) + + verify(exactly = 1) { + opprettTaskService.opprettOppgaveForManuellBehandlingTask( + behandlingId = behandling.id, + beskrivelse = "Fødselshendelse: ${VilkårKanskjeOppfyltÅrsak.LOVLIG_OPPHOLD_MÅ_VURDERE_LENGDEN_PÅ_OPPHOLDSTILLATELSEN.beskrivelse}", + manuellOppgaveType = ManuellOppgaveType.FØDSELSHENDELSE, + ) + } + } + + private fun oppdaterRegelverkTilEøs(behandling: Behandling) { + vilkårsvurderingService.hentAktivForBehandling(behandling.id)!!.apply { + personResultater.first { !it.erSøkersResultater() }.apply { + vilkårResultater.forEach { + it.vurderesEtter = Regelverk.EØS_FORORDNINGEN + it.periodeFom = now().minusMonths(1) + } + } + vilkårsvurderingService.oppdater(this) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/F\303\270dselshendelseRevurderingTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/F\303\270dselshendelseRevurderingTest.kt" new file mode 100644 index 000000000..61fdec679 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/F\303\270dselshendelseRevurderingTest.kt" @@ -0,0 +1,126 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import no.nav.familie.ba.sak.common.nesteMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelTilkjentYtelseRepository +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.task.BehandleFødselshendelseTask +import no.nav.familie.ba.sak.util.tilleggOrdinærSatsNesteMånedTilTester +import no.nav.familie.kontrakter.felles.getDataOrThrow +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate.now + +class FødselshendelseRevurderingTest( + @Autowired private val behandleFødselshendelseTask: BehandleFødselshendelseTask, + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val personidentService: PersonidentService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val stegService: StegService, + @Autowired private val andelTilkjentYtelseRepository: AndelTilkjentYtelseRepository, + @Autowired private val brevmalService: BrevmalService, +) : AbstractVerdikjedetest() { + + @Test + fun `Skal innvilge fødselshendelse på mor med 1 barn med eksisterende utbetalinger`() { + val revurderingsbarnSinFødselsdato = now().minusMonths(3) + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1993-01-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = now().minusMonths(12).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + RestScenarioPerson( + fødselsdato = revurderingsbarnSinFødselsdato.toString(), + fornavn = "Barn2", + etternavn = "Barnesen2", + ), + ), + ), + ) + + behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.minByOrNull { it.fødselsdato }!!.ident!!), + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + vedtakService = vedtakService, + stegService = stegService, + personidentService = personidentService, + brevmalService = brevmalService, + + ) + + val søkerIdent = scenario.søker.ident + val vurdertBarn = scenario.barna.maxByOrNull { it.fødselsdato }!!.ident!! + val behandling = behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = søkerIdent, + barnasIdenter = listOf(vurdertBarn), + ), + fagsakStatusEtterVurdering = FagsakStatus.LØPENDE, + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + personidentService = personidentService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + + ) + + val restFagsakEtterBehandlingAvsluttet = + familieBaSakKlient().hentFagsak(fagsakId = behandling!!.fagsak.id) + + generellAssertFagsak( + restFagsak = restFagsakEtterBehandlingAvsluttet, + fagsakStatus = FagsakStatus.LØPENDE, + behandlingStegType = StegType.BEHANDLING_AVSLUTTET, + aktivBehandlingId = behandling.id, + ) + + val aktivBehandling = + restFagsakEtterBehandlingAvsluttet.getDataOrThrow().behandlinger + .single { + it.behandlingId == behandlingHentOgPersisterService.finnAktivForFagsak( + restFagsakEtterBehandlingAvsluttet.data!!.id, + )?.id + } + + val vurderteVilkårIDenneBehandlingen = aktivBehandling.personResultater.flatMap { it.vilkårResultater } + .filter { it.behandlingId == aktivBehandling.behandlingId } + assertEquals(Behandlingsresultat.INNVILGET, aktivBehandling.resultat) + assertEquals(5, vurderteVilkårIDenneBehandlingen.size) + vurderteVilkårIDenneBehandlingen.forEach { assertEquals(revurderingsbarnSinFødselsdato, it.periodeFom) } + + val utbetalingsperioder = aktivBehandling.utbetalingsperioder + val nesteMånedUtbetalingsperiode = utbetalingsperioder.sortedBy { it.periodeFom }.first { + it.periodeFom.toYearMonth() <= now().nesteMåned() && it.periodeTom.toYearMonth() >= now().nesteMåned() + } + + assertUtbetalingsperiode( + nesteMånedUtbetalingsperiode, + 2, + tilleggOrdinærSatsNesteMånedTilTester().beløp * 2, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/HenleggelseTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/HenleggelseTest.kt new file mode 100644 index 000000000..0a746dd8b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/HenleggelseTest.kt @@ -0,0 +1,140 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.HenleggÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.RestHenleggBehandlingInfo +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.brev.domene.ManueltBrevRequest +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.logg.LoggType +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.kontrakter.felles.Ressurs +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class HenleggelseTest( + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, +) : AbstractVerdikjedetest() { + + val restScenario = RestScenario( + søker = RestScenarioPerson(fødselsdato = "1990-04-20", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = LocalDate.now().minusMonths(2).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + ), + ) + + @Test + fun `Opprett behandling, henlegg behandling feilaktig opprettet og opprett behandling på nytt`() { + val scenario = mockServerKlient().lagScenario(restScenario) + + val førsteBehandling = opprettBehandlingOgRegistrerSøknad(scenario) + + val responseHenlagtSøknad = familieBaSakKlient().henleggSøknad( + førsteBehandling.behandlingId, + RestHenleggBehandlingInfo( + årsak = HenleggÅrsak.FEILAKTIG_OPPRETTET, + begrunnelse = "feilaktig opprettet", + ), + ) + + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = responseHenlagtSøknad, + behandlingStatus = BehandlingStatus.AVSLUTTET, + behandlingStegType = StegType.BEHANDLING_AVSLUTTET, + ) + + val ferdigstiltBehandling = + behandlingHentOgPersisterService.hent(behandlingId = responseHenlagtSøknad.data!!.behandlingId) + + assertThat(!ferdigstiltBehandling.aktiv) + assertThat(ferdigstiltBehandling.resultat == Behandlingsresultat.HENLAGT_FEILAKTIG_OPPRETTET) + + val behandlingslogg = familieBaSakKlient().hentBehandlingslogg(responseHenlagtSøknad.data!!.behandlingId) + assertEquals(Ressurs.Status.SUKSESS, behandlingslogg.status) + assertThat(behandlingslogg.data?.filter { it.type == LoggType.HENLEGG_BEHANDLING }?.size == 1) + assertThat(behandlingslogg.data?.filter { it.type == LoggType.DISTRIBUERE_BREV }?.size == 0) + + val andreBehandling = opprettBehandlingOgRegistrerSøknad(scenario) + assertEquals(andreBehandling.status, BehandlingStatus.UTREDES) + } + + @Test + fun `Opprett behandling, hent forhåndsvising av brev, henlegg behandling søknad trukket`() { + val scenario = mockServerKlient().lagScenario(restScenario) + val førsteBehandling = opprettBehandlingOgRegistrerSøknad(scenario) + + /** + * Denne forhåndsvisningen går ikke til sanity for øyeblikket, men det er en mulighet å legge til + * familie-brev som docker-container og mocke ut pdf-generering for å teste mapping mot sanity. + */ + val responseForhandsvis = familieBaSakKlient().forhaandsvisHenleggelseBrev( + behandlingId = førsteBehandling.behandlingId, + manueltBrevRequest = ManueltBrevRequest( + mottakerIdent = scenario.søker.ident!!, + brevmal = Brevmal.HENLEGGE_TRUKKET_SØKNAD, + ), + ) + assertThat(responseForhandsvis.status == Ressurs.Status.SUKSESS) + + val responseHenlagtSøknad = familieBaSakKlient().henleggSøknad( + førsteBehandling.behandlingId, + RestHenleggBehandlingInfo( + årsak = HenleggÅrsak.SØKNAD_TRUKKET, + begrunnelse = "Søknad trukket", + ), + ) + + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = responseHenlagtSøknad, + behandlingStatus = BehandlingStatus.AVSLUTTET, + behandlingStegType = StegType.BEHANDLING_AVSLUTTET, + ) + + val ferdigstiltBehandling = + behandlingHentOgPersisterService.hent(behandlingId = responseHenlagtSøknad.data!!.behandlingId) + + assertThat(!ferdigstiltBehandling.aktiv) + assertThat(ferdigstiltBehandling.resultat == Behandlingsresultat.HENLAGT_SØKNAD_TRUKKET) + + val behandlingslogg = familieBaSakKlient().hentBehandlingslogg(responseHenlagtSøknad.data!!.behandlingId) + assertEquals(Ressurs.Status.SUKSESS, behandlingslogg.status) + assertThat(behandlingslogg.data?.filter { it.type == LoggType.HENLEGG_BEHANDLING }?.size == 1) + assertThat(behandlingslogg.data?.filter { it.type == LoggType.DISTRIBUERE_BREV }?.size == 1) + } + + private fun opprettBehandlingOgRegistrerSøknad(scenario: RestScenario): RestUtvidetBehandling { + val søkersIdent = scenario.søker.ident!! + val barn1 = scenario.barna[0].ident!! + val fagsak = familieBaSakKlient().opprettFagsak(søkersIdent = søkersIdent) + val restFagsakMedBehandling = familieBaSakKlient().opprettBehandling( + søkersIdent = søkersIdent, + fagsakId = fagsak.data!!.id, + ) + + val behandling = behandlingHentOgPersisterService.hent(restFagsakMedBehandling.data!!.behandlingId) + val restRegistrerSøknad = RestRegistrerSøknad( + søknad = lagSøknadDTO( + søkerIdent = søkersIdent, + barnasIdenter = listOf(barn1), + ), + bekreftEndringerViaFrontend = false, + ) + return familieBaSakKlient().registrererSøknad( + behandlingId = behandling.id, + restRegistrerSøknad = restRegistrerSøknad, + ).data!! + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/Journalf\303\270rOgBehandleF\303\270rstegangss\303\270knadNasjonalTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/Journalf\303\270rOgBehandleF\303\270rstegangss\303\270knadNasjonalTest.kt" new file mode 100644 index 000000000..e2c2b4ef9 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/Journalf\303\270rOgBehandleF\303\270rstegangss\303\270knadNasjonalTest.kt" @@ -0,0 +1,418 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.BehandlingUnderkategoriDTO +import no.nav.familie.ba.sak.ekstern.restDomene.NavnOgIdent +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestPutVedtaksperiodeMedStandardbegrunnelser +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.SatsTidspunkt +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.util.sisteUtvidetSatsTilTester +import no.nav.familie.ba.sak.util.tilleggOrdinærSatsTilTester +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import java.time.LocalDate + +class JournalførOgBehandleFørstegangssøknadNasjonalTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val stegService: StegService, + @Autowired private val featureToggleService: FeatureToggleService, + @Autowired private val brevmalService: BrevmalService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, +) : AbstractVerdikjedetest() { + + @BeforeEach + fun førHverTest() { + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2022, 12, 31) + } + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + @Test + fun `Skal journalføre og behandle ordinær nasjonal sak`() { + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1996-11-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = LocalDate.now().minusMonths(6).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + bostedsadresser = emptyList(), + ), + ), + ), + ) + + val fagsakId: Ressurs = familieBaSakKlient().journalfør( + journalpostId = "1234", + oppgaveId = "5678", + journalførendeEnhet = "4833", + restJournalføring = lagMockRestJournalføring( + bruker = NavnOgIdent( + navn = scenario.søker.navn, + id = scenario.søker.ident!!, + ), + ), + ) + + assertEquals(Ressurs.Status.SUKSESS, fagsakId.status) + + val restFagsakEtterJournalføring = familieBaSakKlient().hentFagsak(fagsakId = fagsakId.data?.toLong()!!) + generellAssertFagsak( + restFagsak = restFagsakEtterJournalføring, + fagsakStatus = FagsakStatus.OPPRETTET, + behandlingStegType = StegType.REGISTRERE_SØKNAD, + ) + + val aktivBehandling = hentAktivBehandling(restFagsak = restFagsakEtterJournalføring.data!!) + val restRegistrerSøknad = + RestRegistrerSøknad( + søknad = lagSøknadDTO( + søkerIdent = scenario.søker.ident, + barnasIdenter = scenario.barna.map { it.ident!! }, + ), + bekreftEndringerViaFrontend = false, + ) + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().registrererSøknad( + behandlingId = aktivBehandling.behandlingId, + restRegistrerSøknad = restRegistrerSøknad, + ) + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandling, + behandlingStatus = BehandlingStatus.UTREDES, + behandlingStegType = StegType.VILKÅRSVURDERING, + ) + + // Godkjenner alle vilkår på førstegangsbehandling. + restUtvidetBehandling.data!!.personResultater.forEach { restPersonResultat -> + restPersonResultat.vilkårResultater.filter { it.resultat == Resultat.IKKE_VURDERT }.forEach { + familieBaSakKlient().putVilkår( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + vilkårId = it.id, + restPersonResultat = + RestPersonResultat( + personIdent = restPersonResultat.personIdent, + vilkårResultater = listOf( + it.copy( + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now().minusMonths(2), + ), + ), + ), + ) + } + } + + familieBaSakKlient().validerVilkårsvurdering( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + ) + + val restUtvidetBehandlingEtterBehandlingsresultat = + familieBaSakKlient().behandlingsresultatStegOgGåVidereTilNesteSteg( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + ) + + assertEquals( + tilleggOrdinærSatsTilTester(), + hentNåværendeEllerNesteMånedsUtbetaling( + behandling = restUtvidetBehandlingEtterBehandlingsresultat.data!!, + ), + ) + + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterBehandlingsresultat, + behandlingStatus = BehandlingStatus.UTREDES, + behandlingStegType = StegType.VURDER_TILBAKEKREVING, + ) + + val restUtvidetBehandlingEtterVurderTilbakekreving = + familieBaSakKlient().lagreTilbakekrevingOgGåVidereTilNesteSteg( + restUtvidetBehandlingEtterBehandlingsresultat.data!!.behandlingId, + RestTilbakekreving(Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, begrunnelse = "begrunnelse"), + ) + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterVurderTilbakekreving, + behandlingStatus = BehandlingStatus.UTREDES, + behandlingStegType = StegType.SEND_TIL_BESLUTTER, + ) + + val vedtaksperioderMedBegrunnelser = vedtaksperiodeService.hentRestUtvidetVedtaksperiodeMedBegrunnelser( + restUtvidetBehandlingEtterVurderTilbakekreving.data!!.behandlingId, + ) + + val vedtaksperiode = vedtaksperioderMedBegrunnelser.sortedBy { it.fom }.first() + familieBaSakKlient().oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = vedtaksperiode.id, + restPutVedtaksperiodeMedStandardbegrunnelser = RestPutVedtaksperiodeMedStandardbegrunnelser( + standardbegrunnelser = listOf( + Standardbegrunnelse.INNVILGET_BOR_HOS_SØKER.enumnavnTilString(), + ), + ), + ) + + val restUtvidetBehandlingEtterSendTilBeslutter = + familieBaSakKlient().sendTilBeslutter(behandlingId = restUtvidetBehandlingEtterVurderTilbakekreving.data!!.behandlingId) + + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterSendTilBeslutter, + behandlingStatus = BehandlingStatus.FATTER_VEDTAK, + behandlingStegType = StegType.BESLUTTE_VEDTAK, + ) + + val restUtvidetBehandlingEtterIverksetting = + familieBaSakKlient().iverksettVedtak( + behandlingId = restUtvidetBehandlingEtterSendTilBeslutter.data!!.behandlingId, + restBeslutningPåVedtak = RestBeslutningPåVedtak( + Beslutning.GODKJENT, + ), + beslutterHeaders = HttpHeaders().apply { + setBearerAuth( + token( + mapOf( + "groups" to listOf("SAKSBEHANDLER", "BESLUTTER"), + "azp" to "azp-test", + "name" to "Mock McMockface Beslutter", + "NAVident" to "Z0000", + ), + ), + ) + }, + ) + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterIverksetting, + behandlingStatus = BehandlingStatus.IVERKSETTER_VEDTAK, + behandlingStegType = StegType.IVERKSETT_MOT_OPPDRAG, + ) + + håndterIverksettingAvBehandling( + behandlingEtterVurdering = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = fagsakId.data!!.toLong())!!, + søkerFnr = scenario.søker.ident, + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + + ) + } + + @Test + fun `Skal journalføre og behandle utvidet nasjonal sak`() { + every { featureToggleService.isEnabled(FeatureToggleConfig.TEKNISK_ENDRING) } returns true + + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1996-12-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = LocalDate.now().minusMonths(6).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + bostedsadresser = emptyList(), + ), + ), + ), + ) + + val fagsakId: Ressurs = familieBaSakKlient().journalfør( + journalpostId = "1234", + oppgaveId = "5678", + journalførendeEnhet = "4833", + restJournalføring = lagMockRestJournalføring( + bruker = NavnOgIdent( + navn = scenario.søker.navn, + id = scenario.søker.ident!!, + ), + ).copy( + journalpostTittel = "Søknad om utvidet barnetrygd", + underkategori = BehandlingUnderkategori.UTVIDET, + ), + ) + + assertEquals(Ressurs.Status.SUKSESS, fagsakId.status) + + val restFagsakEtterJournalføring = familieBaSakKlient().hentFagsak(fagsakId = fagsakId.data?.toLong()!!) + generellAssertFagsak( + restFagsak = restFagsakEtterJournalføring, + fagsakStatus = FagsakStatus.OPPRETTET, + behandlingStegType = StegType.REGISTRERE_SØKNAD, + ) + + val aktivBehandling = hentAktivBehandling(restFagsak = restFagsakEtterJournalføring.data!!) + + assertEquals(BehandlingUnderkategoriDTO.UTVIDET, aktivBehandling.underkategori) + + val restRegistrerSøknad = + RestRegistrerSøknad( + søknad = lagSøknadDTO( + søkerIdent = scenario.søker.ident, + barnasIdenter = scenario.barna.map { it.ident!! }, + underkategori = BehandlingUnderkategori.UTVIDET, + ), + bekreftEndringerViaFrontend = false, + ) + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().registrererSøknad( + behandlingId = aktivBehandling.behandlingId, + restRegistrerSøknad = restRegistrerSøknad, + ) + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandling, + behandlingStatus = BehandlingStatus.UTREDES, + behandlingStegType = StegType.VILKÅRSVURDERING, + ) + + // Godkjenner alle vilkår på førstegangsbehandling. + assertEquals( + 3, + restUtvidetBehandling.data!!.personResultater.find { it.personIdent == scenario.søker.ident }?.vilkårResultater?.size, + ) + + restUtvidetBehandling.data!!.personResultater.forEach { restPersonResultat -> + restPersonResultat.vilkårResultater.filter { it.resultat == Resultat.IKKE_VURDERT }.forEach { + familieBaSakKlient().putVilkår( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + vilkårId = it.id, + restPersonResultat = + RestPersonResultat( + personIdent = restPersonResultat.personIdent, + vilkårResultater = listOf( + it.copy( + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now().minusMonths(2), + ), + ), + ), + ) + } + } + + familieBaSakKlient().validerVilkårsvurdering( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + ) + + val restUtvidetBehandlingEtterBehandlingsresultat = + familieBaSakKlient().behandlingsresultatStegOgGåVidereTilNesteSteg( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + ) + + assertEquals( + tilleggOrdinærSatsTilTester() + sisteUtvidetSatsTilTester(), + hentNåværendeEllerNesteMånedsUtbetaling( + behandling = restUtvidetBehandlingEtterBehandlingsresultat.data!!, + ), + ) + + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterBehandlingsresultat, + behandlingStatus = BehandlingStatus.UTREDES, + behandlingStegType = StegType.VURDER_TILBAKEKREVING, + ) + + val restUtvidetBehandlingEtterVurderTilbakekreving = + familieBaSakKlient().lagreTilbakekrevingOgGåVidereTilNesteSteg( + restUtvidetBehandlingEtterBehandlingsresultat.data!!.behandlingId, + RestTilbakekreving(Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, begrunnelse = "begrunnelse"), + ) + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterVurderTilbakekreving, + behandlingStatus = BehandlingStatus.UTREDES, + behandlingStegType = StegType.SEND_TIL_BESLUTTER, + ) + + val vedtaksperioderMedBegrunnelser = vedtaksperiodeService.hentRestUtvidetVedtaksperiodeMedBegrunnelser( + restUtvidetBehandlingEtterVurderTilbakekreving.data!!.behandlingId, + ) + + val vedtaksperiode = vedtaksperioderMedBegrunnelser.sortedBy { it.fom }.first() + + familieBaSakKlient().oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = vedtaksperiode.id, + restPutVedtaksperiodeMedStandardbegrunnelser = RestPutVedtaksperiodeMedStandardbegrunnelser( + standardbegrunnelser = listOf( + Standardbegrunnelse.INNVILGET_BOR_HOS_SØKER.enumnavnTilString(), + ), + ), + ) + + val restUtvidetBehandlingEtterSendTilBeslutter = + familieBaSakKlient().sendTilBeslutter(behandlingId = restUtvidetBehandlingEtterVurderTilbakekreving.data!!.behandlingId) + + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterSendTilBeslutter, + behandlingStatus = BehandlingStatus.FATTER_VEDTAK, + behandlingStegType = StegType.BESLUTTE_VEDTAK, + ) + + val restUtvidetBehandlingEtterIverksetting = + familieBaSakKlient().iverksettVedtak( + behandlingId = restUtvidetBehandlingEtterSendTilBeslutter.data!!.behandlingId, + restBeslutningPåVedtak = RestBeslutningPåVedtak( + Beslutning.GODKJENT, + ), + beslutterHeaders = HttpHeaders().apply { + setBearerAuth( + token( + mapOf( + "groups" to listOf("SAKSBEHANDLER", "BESLUTTER"), + "azp" to "azp-test", + "name" to "Mock McMockface Beslutter", + "NAVident" to "Z0000", + ), + ), + ) + }, + ) + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterIverksetting, + behandlingStatus = BehandlingStatus.IVERKSETTER_VEDTAK, + behandlingStegType = StegType.IVERKSETT_MOT_OPPDRAG, + ) + + håndterIverksettingAvBehandling( + behandlingEtterVurdering = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = fagsakId.data!!.toLong())!!, + søkerFnr = scenario.søker.ident, + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/OpplysningspliktTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/OpplysningspliktTest.kt new file mode 100644 index 000000000..bc4b623b4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/OpplysningspliktTest.kt @@ -0,0 +1,147 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import no.nav.familie.ba.sak.common.kjørStegprosessForFGB +import no.nav.familie.ba.sak.ekstern.restDomene.RestPutVedtaksperiodeMedStandardbegrunnelser +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.settpåvent.SettPåVentService +import no.nav.familie.ba.sak.kjerne.brev.BrevService +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.brev.DokumentService +import no.nav.familie.ba.sak.kjerne.brev.domene.ManueltBrevRequest +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Brevmal +import no.nav.familie.ba.sak.kjerne.brev.domene.maler.Førstegangsvedtak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurderingType +import no.nav.familie.kontrakter.felles.arbeidsfordeling.Enhet +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class OpplysningspliktTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val stegService: StegService, + @Autowired private val vilkårsvurderingService: VilkårsvurderingService, + @Autowired private val persongrunnlagService: PersongrunnlagService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, + @Autowired private val vedtaksperiodeHentOgPersisterService: VedtaksperiodeHentOgPersisterService, + @Autowired private val dokumentService: DokumentService, + @Autowired private val brevService: BrevService, + @Autowired private val settPåVentService: SettPåVentService, + @Autowired private val brevmalService: BrevmalService, +) : AbstractVerdikjedetest() { + + @Test + fun `Skal opprette opplysningsplikt-vilkår på søker når 'innhente opplysninger'-brev sendes ut og ta med hjemmel 17 og 18 i vedtaksbrev når opplysningsplikt-vilkåret ikke er oppfylt`() { + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1990-04-20", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = LocalDate.now().minusMonths(2).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + ), + ), + ) + + val behandling = kjørStegprosessForFGB( + tilSteg = StegType.VILKÅRSVURDERING, + søkerFnr = scenario.søker.ident!!, + barnasIdenter = scenario.barna.map { it.ident!! }, + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + + ) + + // Send "innhente opplysninger"-brev og sjekk at opplysningsplikt vilkåret dukker opp på _kun_ søker + dokumentService.sendManueltBrev( + fagsakId = behandling.fagsak.id, + manueltBrevRequest = ManueltBrevRequest( + brevmal = Brevmal.INNHENTE_OPPLYSNINGER, + mottakerIdent = scenario.søker.ident, + enhet = Enhet(enhetId = "1234", enhetNavn = "Enhet Enhetesen"), + ), + behandling = behandling, + ) + + settPåVentService.gjenopptaBehandling(behandling.id) + + val vilkårsvurdering = vilkårsvurderingService.hentAktivForBehandling(behandlingId = behandling.id) + + val opplysningspliktVilkårPåSøker = + vilkårsvurdering?.personResultater?.single { it.erSøkersResultater() }?.andreVurderinger?.singleOrNull { it.type == AnnenVurderingType.OPPLYSNINGSPLIKT } + + val opplysningspliktVilkårPåBarna = vilkårsvurdering?.personResultater?.filter { !it.erSøkersResultater() } + ?.flatMap { it.andreVurderinger.filter { it.type == AnnenVurderingType.OPPLYSNINGSPLIKT } } ?: emptyList() + + Assertions.assertTrue(opplysningspliktVilkårPåSøker != null) + Assertions.assertTrue(opplysningspliktVilkårPåBarna.isEmpty()) + + // Sette opplysningsplikt vilkåret til ikke oppfylt og sjekke at hjemlene blir riktig + opplysningspliktVilkårPåSøker?.resultat = Resultat.IKKE_OPPFYLT + vilkårsvurderingService.oppdater(vilkårsvurdering = vilkårsvurdering!!) + + val vilkårsvurderingOppdatert = vilkårsvurderingService.hentAktivForBehandling(behandlingId = behandling.id) + + Assertions.assertTrue(vilkårsvurderingOppdatert?.personResultater?.single { it.erSøkersResultater() }?.andreVurderinger?.single { it.type == AnnenVurderingType.OPPLYSNINGSPLIKT }?.resultat == Resultat.IKKE_OPPFYLT) + + familieBaSakKlient().validerVilkårsvurdering( + behandlingId = behandling.id, + ) + + val behandlingEtterBehandlingsResultat = + familieBaSakKlient().behandlingsresultatStegOgGåVidereTilNesteSteg( + behandlingId = behandling.id, + ) + + val behandlingEtterVurderTilbakekreving = + familieBaSakKlient().lagreTilbakekrevingOgGåVidereTilNesteSteg( + behandlingEtterBehandlingsResultat.data!!.behandlingId, + RestTilbakekreving(Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, begrunnelse = "begrunnelse"), + ) + + val vedtaksperioderMedBegrunnelser = vedtaksperiodeService.hentRestUtvidetVedtaksperiodeMedBegrunnelser( + behandlingEtterVurderTilbakekreving.data!!.behandlingId, + ) + + val vedtaksperiode = vedtaksperioderMedBegrunnelser.sortedBy { it.fom }.first() + familieBaSakKlient().oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = vedtaksperiode.id, + restPutVedtaksperiodeMedStandardbegrunnelser = RestPutVedtaksperiodeMedStandardbegrunnelser( + standardbegrunnelser = listOf( + Standardbegrunnelse.INNVILGET_BOR_HOS_SØKER.enumnavnTilString(), + ), + ), + ) + + val vedtak = + vedtaksperiodeHentOgPersisterService.hentVedtaksperiodeThrows(vedtaksperiodeId = vedtaksperiode.id).vedtak + + val vedtaksbrev = brevService.hentVedtaksbrevData(vedtak) + + val hjemmeltekst = (vedtaksbrev as Førstegangsvedtak).data.delmalData.hjemmeltekst.hjemler!!.first() + + Assertions.assertTrue(hjemmeltekst.contains("17")) + Assertions.assertTrue(hjemmeltekst.contains("18")) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/ReduksjonFraForrigeIverksatteBehandlingTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/ReduksjonFraForrigeIverksatteBehandlingTest.kt new file mode 100644 index 000000000..1abd78e7f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/ReduksjonFraForrigeIverksatteBehandlingTest.kt @@ -0,0 +1,221 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.common.sisteDagIMåned +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.RestMinimalFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.integrasjoner.ef.EfSakRestClient +import no.nav.familie.ba.sak.internal.TestVerktøyService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.SatsTidspunkt +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.ef.Datakilde +import no.nav.familie.kontrakter.felles.ef.EksternPeriode +import no.nav.familie.kontrakter.felles.ef.EksternePerioderResponse +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class ReduksjonFraForrigeIverksatteBehandlingTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, + @Autowired private val stegService: StegService, + @Autowired private val efSakRestClient: EfSakRestClient, + @Autowired private val brevmalService: BrevmalService, + @Autowired private val featureToggleService: FeatureToggleService, + @Autowired private val testVerktøyService: TestVerktøyService, +) : AbstractVerdikjedetest() { + + private val barnFødselsdato: LocalDate = LocalDate.now().minusYears(2) + + @BeforeEach + fun førHverTest() { + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of(2022, 12, 31) + } + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + @Test + @Disabled("Utsatt, mulig vi bør se nøyere på denne når BEGRUNNELSER_NY togglen kan fjernes.") + fun `Skal lage reduksjon fra sist iverksatte behandling-periode når småbarnstillegg blir borte`() { + val personScenario: RestScenario = lagScenario(barnFødselsdato) + val fagsak: RestMinimalFagsak = lagFagsak(personScenario) + + val osFom = LocalDate.now().førsteDagIInneværendeMåned() + val osTom = LocalDate.now().plusMonths(2).sisteDagIMåned() + + val behandling1 = fullførBehandlingMedOvergangsstønad( + fagsak = fagsak, + personScenario = personScenario, + barnFødselsdato = barnFødselsdato, + overgangsstønadPerioder = listOf( + EksternPeriode( + personIdent = personScenario.søker.ident!!, + fomDato = osFom, + tomDato = osTom, + datakilde = Datakilde.EF, + ), + ), + ) + val perioderBehandling1 = vedtaksperiodeService.hentUtvidetVedtaksperiodeMedBegrunnelser( + vedtak = vedtakService.hentAktivForBehandling(behandling1.id)!!, + ) + + Assertions.assertEquals( + 1, + perioderBehandling1.filter { it.utbetalingsperiodeDetaljer.any { it.ytelseType == YtelseType.SMÅBARNSTILLEGG } }.size, + ) + + val behandling2 = fullførRevurderingUtenOvergangstonad( + fagsak = fagsak, + personScenario = personScenario, + barnFødselsdato = barnFødselsdato, + ) + + val perioderBehandling2 = vedtaksperiodeService.hentUtvidetVedtaksperiodeMedBegrunnelser( + vedtak = vedtakService.hentAktivForBehandling(behandling2.id)!!, + ) + val periodeMedReduksjon = + perioderBehandling2.singleOrNull { it.type == Vedtaksperiodetype.UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING } + + Assertions.assertEquals( + 0, + perioderBehandling2.filter { it.utbetalingsperiodeDetaljer.any { it.ytelseType == YtelseType.SMÅBARNSTILLEGG } }.size, + ) + Assertions.assertNotNull(periodeMedReduksjon) + Assertions.assertEquals(osFom, periodeMedReduksjon!!.fom) + Assertions.assertEquals(osTom, periodeMedReduksjon.tom) + } + + fun lagScenario(barnFødselsdato: LocalDate): RestScenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1996-01-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = barnFødselsdato.toString(), + fornavn = "Barn", + etternavn = "Barnesen", + bostedsadresser = emptyList(), + ), + ), + ), + ) + + fun lagFagsak(personScenario: RestScenario): RestMinimalFagsak { + return familieBaSakKlient().opprettFagsak(søkersIdent = personScenario.søker.ident!!).data!! + } + + fun fullførBehandlingMedOvergangsstønad( + fagsak: RestMinimalFagsak, + personScenario: RestScenario, + barnFødselsdato: LocalDate, + overgangsstønadPerioder: List, + ): Behandling { + val behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = overgangsstønadPerioder, + ) + + val restBehandling: Ressurs = + familieBaSakKlient().opprettBehandling( + søkersIdent = fagsak.søkerFødselsnummer, + behandlingType = behandlingType, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + fagsakId = fagsak.id, + ) + val behandling = behandlingHentOgPersisterService.hent(restBehandling.data!!.behandlingId) + val restRegistrerSøknad = + RestRegistrerSøknad( + søknad = lagSøknadDTO( + søkerIdent = fagsak.søkerFødselsnummer, + barnasIdenter = personScenario.barna.map { it.ident!! }, + underkategori = BehandlingUnderkategori.UTVIDET, + ), + bekreftEndringerViaFrontend = false, + ) + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().registrererSøknad( + behandlingId = behandling.id, + restRegistrerSøknad = restRegistrerSøknad, + ) + + return fullførBehandlingFraVilkårsvurderingAlleVilkårOppfylt( + restUtvidetBehandling = restUtvidetBehandling.data!!, + personScenario = personScenario, + fagsak = fagsak, + familieBaSakKlient = familieBaSakKlient(), + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + lagToken = ::token, + brevmalService = brevmalService, + vedtaksperiodeService = vedtaksperiodeService, + ) + } + + fun fullførRevurderingUtenOvergangstonad( + fagsak: RestMinimalFagsak, + personScenario: RestScenario, + barnFødselsdato: LocalDate, + ): Behandling { + val behandlingType = BehandlingType.REVURDERING + val behandlingÅrsak = BehandlingÅrsak.SMÅBARNSTILLEGG + + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = emptyList(), + ) + + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().opprettBehandling( + søkersIdent = fagsak.søkerFødselsnummer, + behandlingType = behandlingType, + behandlingÅrsak = behandlingÅrsak, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + fagsakId = fagsak.id, + ) + + return fullførBehandlingFraVilkårsvurderingAlleVilkårOppfylt( + restUtvidetBehandling = restUtvidetBehandling.data!!, + personScenario = personScenario, + fagsak = fagsak, + familieBaSakKlient = familieBaSakKlient(), + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + lagToken = ::token, + brevmalService = brevmalService, + vedtaksperiodeService = vedtaksperiodeService, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/RestartAvSm\303\245barnstilleggTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/RestartAvSm\303\245barnstilleggTest.kt" new file mode 100644 index 000000000..a1e3d4d98 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/RestartAvSm\303\245barnstilleggTest.kt" @@ -0,0 +1,523 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.common.sisteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.toYearMonth +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.RestMinimalFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestPutVedtaksperiodeMedStandardbegrunnelser +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.integrasjoner.ef.EfSakRestClient +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.satsendring.AutovedtakSatsendringService +import no.nav.familie.ba.sak.kjerne.autovedtak.småbarnstillegg.RestartAvSmåbarnstilleggService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.SatsService +import no.nav.familie.ba.sak.kjerne.beregning.SatsTidspunkt +import no.nav.familie.ba.sak.kjerne.beregning.domene.SatsType +import no.nav.familie.ba.sak.kjerne.beregning.domene.YtelseType +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.task.SatsendringTaskDto +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.ef.Datakilde +import no.nav.familie.kontrakter.felles.ef.EksternPeriode +import no.nav.familie.kontrakter.felles.ef.EksternePerioderResponse +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import java.time.LocalDate +import java.time.YearMonth + +class RestartAvSmåbarnstilleggTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val stegService: StegService, + @Autowired private val efSakRestClient: EfSakRestClient, + @Autowired private val restartAvSmåbarnstilleggService: RestartAvSmåbarnstilleggService, + @Autowired private val brevmalService: BrevmalService, + @Autowired private val autovedtakSatsendringService: AutovedtakSatsendringService, + @Autowired private val featureToggleService: FeatureToggleService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, +) : AbstractVerdikjedetest() { + + private val barnFødselsdato: LocalDate = LocalDate.now().minusYears(2) + + @AfterEach + fun etterHverTest() { + unmockkObject(SatsTidspunkt) + } + + @Test + fun `Skal finne alle fagsaker hvor småbarnstillegg starter opp igjen inneværende måned, og ikke er begrunnet`() { + val restartSmåbarnstilleggMåned = LocalDate.now().plusMonths(4) + + // Fagsak 1 - har åpen behandling og skal ikke tas med + val personScenario1: RestScenario = lagScenario(barnFødselsdato) + val fagsak1: RestMinimalFagsak = lagFagsak(personScenario = personScenario1) + fullførBehandling( + fagsak = fagsak1, + personScenario = personScenario1, + barnFødselsdato = barnFødselsdato, + ) + + fullførRevurderingMedOvergangstonad( + fagsak = fagsak1, + personScenario = personScenario1, + barnFødselsdato = barnFødselsdato, + mockPerioderMedOvergangsstønad = listOf( + EksternPeriode( + personIdent = personScenario1.søker.ident!!, + fomDato = barnFødselsdato.plusYears(1), + tomDato = LocalDate.now().minusMonths(1).førsteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + EksternPeriode( + personIdent = personScenario1.søker.ident, + fomDato = restartSmåbarnstilleggMåned.førsteDagIInneværendeMåned(), + tomDato = LocalDate.now().plusYears(3).førsteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + ), + ) + startEnRevurderingNyeOpplysningerMenIkkeFullfør( + fagsak = fagsak1, + personScenario = personScenario1, + barnFødselsdato = barnFødselsdato, + ) + + // Fagsak 2 - har restart av småbarnstillegg som ikke er begrunnet og skal være med i listen + val personScenario2: RestScenario = lagScenario(barnFødselsdato) + val fagsak2: RestMinimalFagsak = lagFagsak(personScenario = personScenario2) + fullførBehandling( + fagsak = fagsak2, + personScenario = personScenario2, + barnFødselsdato = barnFødselsdato, + ) + fullførRevurderingMedOvergangstonad( + fagsak = fagsak2, + personScenario = personScenario2, + barnFødselsdato = barnFødselsdato, + mockPerioderMedOvergangsstønad = listOf( + EksternPeriode( + personIdent = personScenario2.søker.ident!!, + fomDato = barnFødselsdato.plusYears(1), + tomDato = LocalDate.now().minusMonths(1).førsteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + EksternPeriode( + personIdent = personScenario2.søker.ident, + fomDato = restartSmåbarnstilleggMåned.førsteDagIInneværendeMåned(), + tomDato = LocalDate.now().plusYears(3).førsteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + ), + ) + + // Fagsak 3 - har restart av småbarnstillegg som allerede er begrunnet, skal ikke være med i listen + val personScenario3: RestScenario = lagScenario(barnFødselsdato) + val fagsak3: RestMinimalFagsak = lagFagsak(personScenario = personScenario3) + fullførBehandling( + fagsak = fagsak3, + personScenario = personScenario3, + barnFødselsdato = barnFødselsdato, + ) + fullførRevurderingMedOvergangstonad( + fagsak = fagsak3, + personScenario = personScenario3, + barnFødselsdato = barnFødselsdato, + skalBegrunneSmåbarnstillegg = true, + mockPerioderMedOvergangsstønad = listOf( + EksternPeriode( + personIdent = personScenario3.søker.ident!!, + fomDato = barnFødselsdato.plusYears(1), + tomDato = LocalDate.now().minusMonths(1).førsteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + EksternPeriode( + personIdent = personScenario3.søker.ident, + fomDato = restartSmåbarnstilleggMåned.førsteDagIInneværendeMåned(), + tomDato = LocalDate.now().plusYears(3).førsteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + ), + ) + + val fagsaker: List = + restartAvSmåbarnstilleggService.finnAlleFagsakerMedRestartetSmåbarnstilleggIMåned(måned = restartSmåbarnstilleggMåned.toYearMonth()) + + Assertions.assertTrue(fagsaker.containsAll(listOf(fagsak2.id))) + Assertions.assertFalse(fagsaker.contains(fagsak1.id)) + Assertions.assertFalse(fagsaker.contains(fagsak3.id)) + } + + @Test + fun `Skal finne en fagsak hvor småbarnstillegg starter opp igjen inneværende måned selv om det er utført satsendring`() { + val satsendringDato = SatsService.finnSisteSatsFor(SatsType.SMA).gyldigFom.toYearMonth() + + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of( + 2022, + 12, + 1, + ) // Mocker slik at behandling får gammel sats + + // Fagsak - har restart dato på samme dato som satsendringen + val personScenario: RestScenario = lagScenario(barnFødselsdato) + val fagsakMedSatsendringOgSmåbarnstilleggSomSkalRestartes: RestMinimalFagsak = + lagFagsak(personScenario = personScenario) + fullførBehandling( + fagsak = fagsakMedSatsendringOgSmåbarnstilleggSomSkalRestartes, + personScenario = personScenario, + barnFødselsdato = barnFødselsdato, + ) + fullførRevurderingMedOvergangstonad( + fagsak = fagsakMedSatsendringOgSmåbarnstilleggSomSkalRestartes, + personScenario = personScenario, + barnFødselsdato = barnFødselsdato, + mockPerioderMedOvergangsstønad = listOf( + EksternPeriode( + personIdent = personScenario.søker.ident!!, + fomDato = barnFødselsdato.plusYears(1), + tomDato = satsendringDato.minusMonths(2).sisteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + EksternPeriode( + personIdent = personScenario.søker.ident, + fomDato = satsendringDato.førsteDagIInneværendeMåned(), + tomDato = LocalDate.now().plusYears(3).førsteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + ), + ) + + unmockkObject(SatsTidspunkt) + + // satsendring gjør at den får en ny andel på småbarnstillegg med gyldig fom satsendringsdatoen + fullførSatsendring(fagsakMedSatsendringOgSmåbarnstilleggSomSkalRestartes.id, satsendringDato) + + val fagsaker: List = + restartAvSmåbarnstilleggService.finnAlleFagsakerMedRestartetSmåbarnstilleggIMåned(måned = satsendringDato) + + Assertions.assertTrue(fagsaker.contains(fagsakMedSatsendringOgSmåbarnstilleggSomSkalRestartes.id)) + } + + @Test + fun `Satsendring skal ikke restarte småbarnstillegg på allerede løpende småbarnstillegg`() { + val satsendringDato = SatsService.finnSisteSatsFor(SatsType.SMA).gyldigFom.toYearMonth() + + mockkObject(SatsTidspunkt) + every { SatsTidspunkt.senesteSatsTidspunkt } returns LocalDate.of( + 2022, + 12, + 1, + ) // Mocker slik at behandling får gammel sats + + // Fagsak - har løpende fagsak med småbarnstillegg og skal ikke restartes + val personScenario2: RestScenario = lagScenario(barnFødselsdato) + val fagsakMedSatsendringOgSmåbarnstilleggSomIkkeSkalRestartes: RestMinimalFagsak = + lagFagsak(personScenario = personScenario2) + fullførBehandling( + fagsak = fagsakMedSatsendringOgSmåbarnstilleggSomIkkeSkalRestartes, + personScenario = personScenario2, + barnFødselsdato = barnFødselsdato, + ) + fullførRevurderingMedOvergangstonad( + fagsak = fagsakMedSatsendringOgSmåbarnstilleggSomIkkeSkalRestartes, + personScenario = personScenario2, + barnFødselsdato = barnFødselsdato, + mockPerioderMedOvergangsstønad = listOf( + EksternPeriode( + personIdent = personScenario2.søker.ident!!, + fomDato = barnFødselsdato.plusYears(1), + tomDato = LocalDate.now().plusYears(3).førsteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + ), + ) + + // satsendring gjør at den får en ny andel på småbarnstillegg med gyldig fom satsendringsdatoen + fullførSatsendring(fagsakMedSatsendringOgSmåbarnstilleggSomIkkeSkalRestartes.id, satsendringDato) + + val fagsaker: List = + restartAvSmåbarnstilleggService.finnAlleFagsakerMedRestartetSmåbarnstilleggIMåned(måned = satsendringDato) + + Assertions.assertFalse(fagsaker.contains(fagsakMedSatsendringOgSmåbarnstilleggSomIkkeSkalRestartes.id)) + } + + fun lagScenario(barnFødselsdato: LocalDate): RestScenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1996-01-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = barnFødselsdato.toString(), + fornavn = "Barn", + etternavn = "Barnesen", + bostedsadresser = emptyList(), + ), + ), + ), + ) + + fun lagFagsak(personScenario: RestScenario): RestMinimalFagsak { + return familieBaSakKlient().opprettFagsak(søkersIdent = personScenario.søker.ident!!).data!! + } + + fun fullførBehandling( + fagsak: RestMinimalFagsak, + personScenario: RestScenario, + barnFødselsdato: LocalDate, + ): Behandling { + val behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = emptyList(), + ) + + val restBehandling: Ressurs = + familieBaSakKlient().opprettBehandling( + søkersIdent = fagsak.søkerFødselsnummer, + behandlingType = behandlingType, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + fagsakId = fagsak.id, + ) + val behandling = behandlingHentOgPersisterService.hent(restBehandling.data!!.behandlingId) + val restRegistrerSøknad = + RestRegistrerSøknad( + søknad = lagSøknadDTO( + søkerIdent = fagsak.søkerFødselsnummer, + barnasIdenter = personScenario.barna.map { it.ident!! }, + underkategori = BehandlingUnderkategori.UTVIDET, + ), + bekreftEndringerViaFrontend = false, + ) + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().registrererSøknad( + behandlingId = behandling.id, + restRegistrerSøknad = restRegistrerSøknad, + ) + + return fullførRestenAvBehandlingen( + restUtvidetBehandling = restUtvidetBehandling.data!!, + personScenario = personScenario, + fagsak = fagsak, + ) + } + + fun fullførRevurderingMedOvergangstonad( + fagsak: RestMinimalFagsak, + personScenario: RestScenario, + barnFødselsdato: LocalDate, + mockPerioderMedOvergangsstønad: List = listOf( + EksternPeriode( + personIdent = personScenario.søker.ident!!, + fomDato = barnFødselsdato.plusYears(1), + tomDato = LocalDate.now().minusMonths(1).førsteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + ), + skalBegrunneSmåbarnstillegg: Boolean = false, + ): Behandling { + val behandlingType = BehandlingType.REVURDERING + val behandlingÅrsak = BehandlingÅrsak.SMÅBARNSTILLEGG + + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = mockPerioderMedOvergangsstønad, + ) + + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().opprettBehandling( + søkersIdent = fagsak.søkerFødselsnummer, + behandlingType = behandlingType, + behandlingÅrsak = behandlingÅrsak, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + fagsakId = fagsak.id, + ) + + return fullførRestenAvBehandlingen( + restUtvidetBehandling = restUtvidetBehandling.data!!, + personScenario = personScenario, + fagsak = fagsak, + skalBegrunneSmåbarnstillegg = skalBegrunneSmåbarnstillegg, + ) + } + + private fun fullførSatsendring(fagsakId: Long, satsendringsTidspunkt: YearMonth) { + unmockkObject(SatsTidspunkt) + autovedtakSatsendringService.kjørBehandling(SatsendringTaskDto(fagsakId, satsendringsTidspunkt)) + val satsendring = behandlingHentOgPersisterService.hentBehandlinger(fagsakId).first { it.erSatsendring() } + + val iverksattBehandling = håndterIverksettingAvBehandling( + behandlingEtterVurdering = satsendring, + søkerFnr = satsendring.fagsak.aktør.aktivFødselsnummer(), + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + ) + + if (!iverksattBehandling.erVedtatt() && iverksattBehandling.aktiv) error("Satsendringen er ikke utført $iverksattBehandling") + } + + fun settAlleVilkårTilOppfylt(restUtvidetBehandling: RestUtvidetBehandling, barnFødselsdato: LocalDate) { + restUtvidetBehandling.personResultater.forEach { restPersonResultat -> + restPersonResultat.vilkårResultater.filter { it.resultat == Resultat.IKKE_VURDERT }.forEach { + familieBaSakKlient().putVilkår( + behandlingId = restUtvidetBehandling.behandlingId, + vilkårId = it.id, + restPersonResultat = + RestPersonResultat( + personIdent = restPersonResultat.personIdent, + vilkårResultater = listOf( + it.copy( + resultat = Resultat.OPPFYLT, + periodeFom = barnFødselsdato, + ), + ), + ), + ) + } + } + } + + private fun startEnRevurderingNyeOpplysningerMenIkkeFullfør( + fagsak: RestMinimalFagsak, + personScenario: RestScenario, + barnFødselsdato: LocalDate, + ): Behandling { + val behandlingType = BehandlingType.REVURDERING + val behandlingÅrsak = BehandlingÅrsak.SMÅBARNSTILLEGG + + every { efSakRestClient.hentPerioderMedFullOvergangsstønad(any()) } returns EksternePerioderResponse( + perioder = listOf( + EksternPeriode( + personIdent = personScenario.søker.ident!!, + fomDato = barnFødselsdato.plusYears(1), + tomDato = LocalDate.now().minusMonths(1).førsteDagIInneværendeMåned(), + datakilde = Datakilde.EF, + ), + ), + ) + + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().opprettBehandling( + søkersIdent = fagsak.søkerFødselsnummer, + behandlingType = behandlingType, + behandlingÅrsak = behandlingÅrsak, + behandlingUnderkategori = BehandlingUnderkategori.UTVIDET, + fagsakId = fagsak.id, + ) + return behandlingHentOgPersisterService.hent(restUtvidetBehandling.data!!.behandlingId) + } + + fun fullførRestenAvBehandlingen( + restUtvidetBehandling: RestUtvidetBehandling, + personScenario: RestScenario, + fagsak: RestMinimalFagsak, + skalBegrunneSmåbarnstillegg: Boolean = false, + ): Behandling { + settAlleVilkårTilOppfylt( + restUtvidetBehandling = restUtvidetBehandling, + barnFødselsdato = barnFødselsdato, + ) + + familieBaSakKlient().validerVilkårsvurdering( + behandlingId = restUtvidetBehandling.behandlingId, + ) + + val restUtvidetBehandlingEtterBehandlingsResultat = + familieBaSakKlient().behandlingsresultatStegOgGåVidereTilNesteSteg( + behandlingId = restUtvidetBehandling.behandlingId, + ) + + val restUtvidetBehandlingEtterVurderTilbakekreving = + familieBaSakKlient().lagreTilbakekrevingOgGåVidereTilNesteSteg( + restUtvidetBehandlingEtterBehandlingsResultat.data!!.behandlingId, + RestTilbakekreving(Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, begrunnelse = "begrunnelse"), + ) + + val vedtaksperioderMedBegrunnelser = vedtaksperiodeService.hentRestUtvidetVedtaksperiodeMedBegrunnelser( + restUtvidetBehandlingEtterVurderTilbakekreving.data!!.behandlingId, + ) + + val utvidetVedtaksperiodeMedBegrunnelser = vedtaksperioderMedBegrunnelser.sortedBy { it.fom }.first() + + familieBaSakKlient().oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = utvidetVedtaksperiodeMedBegrunnelser.id, + restPutVedtaksperiodeMedStandardbegrunnelser = RestPutVedtaksperiodeMedStandardbegrunnelser( + standardbegrunnelser = utvidetVedtaksperiodeMedBegrunnelser.gyldigeBegrunnelser.filter(String::isNotEmpty), + ), + ) + if (skalBegrunneSmåbarnstillegg) { + val småbarnstilleggVedtaksperioder = + vedtaksperioderMedBegrunnelser.filter { + it.utbetalingsperiodeDetaljer.filter { utbetalingsperiodeDetalj -> utbetalingsperiodeDetalj.ytelseType == YtelseType.SMÅBARNSTILLEGG } + .isNotEmpty() + } + + småbarnstilleggVedtaksperioder.forEach { periode -> + familieBaSakKlient().oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = periode.id, + restPutVedtaksperiodeMedStandardbegrunnelser = RestPutVedtaksperiodeMedStandardbegrunnelser( + standardbegrunnelser = listOf( + Standardbegrunnelse.INNVILGET_SMÅBARNSTILLEGG.enumnavnTilString(), + ), + ), + ) + } + } + + val restUtvidetBehandlingEtterSendTilBeslutter = + familieBaSakKlient().sendTilBeslutter(behandlingId = restUtvidetBehandlingEtterVurderTilbakekreving.data!!.behandlingId) + + familieBaSakKlient().iverksettVedtak( + behandlingId = restUtvidetBehandlingEtterSendTilBeslutter.data!!.behandlingId, + restBeslutningPåVedtak = RestBeslutningPåVedtak( + Beslutning.GODKJENT, + ), + beslutterHeaders = HttpHeaders().apply { + setBearerAuth( + token( + mapOf( + "groups" to listOf("SAKSBEHANDLER", "BESLUTTER"), + "azp" to "azp-test", + "name" to "Mock McMockface Beslutter", + "NAVident" to "Z0000", + ), + ), + ) + }, + ) + return håndterIverksettingAvBehandling( + behandlingEtterVurdering = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = fagsak.id)!!, + søkerFnr = personScenario.søker.ident!!, + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/RevurderingD\303\270dsfallTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/RevurderingD\303\270dsfallTest.kt" new file mode 100644 index 000000000..935e8b460 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/RevurderingD\303\270dsfallTest.kt" @@ -0,0 +1,196 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import io.mockk.mockk +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.datagenerator.behandling.kjørStegprosessForBehandling +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagVilkårsvurderingFraRestScenario +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.VilkårResultat +import no.nav.familie.ba.sak.task.BehandleFødselshendelseTask +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class RevurderingDødsfallTest( + @Autowired private val behandleFødselshendelseTask: BehandleFødselshendelseTask, + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val personidentService: PersonidentService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val stegService: StegService, + @Autowired private val persongrunnlagService: PersongrunnlagService, + @Autowired private val vilkårsvurderingService: VilkårsvurderingService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, + @Autowired private val endretUtbetalingAndelHentOgPersisterService: EndretUtbetalingAndelHentOgPersisterService, + @Autowired private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + @Autowired private val brevmalService: BrevmalService, +) : AbstractVerdikjedetest() { + + @Test + fun `Dødsfall bruker skal kjøre gjennom`() { + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson( + fødselsdato = "1982-01-12", + fornavn = "Mor", + etternavn = "Søker", + ), + barna = listOf( + RestScenarioPerson( + fødselsdato = LocalDate.now().minusMonths(2).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + ), + ), + ) + + behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.first().ident!!), + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + vedtakService = vedtakService, + stegService = stegService, + personidentService = personidentService, + brevmalService = brevmalService, + + ) + + val overstyrendeVilkårResultater = + scenario.barna.associate { it.aktørId!! to emptyList() }.toMutableMap() + + // Ved søkers dødsfall settes tomdatoen for "bosatt i riket"-vilkåret til dagen søker døde. + overstyrendeVilkårResultater[scenario.søker.aktørId!!] = listOf( + lagVilkårResultat( + vilkårType = Vilkår.BOSATT_I_RIKET, + periodeFom = LocalDate.parse(scenario.søker.fødselsdato), + periodeTom = LocalDate.now().minusMonths(1), + personResultat = mockk(relaxed = true), + ), + lagVilkårResultat( + vilkårType = Vilkår.LOVLIG_OPPHOLD, + periodeFom = LocalDate.parse(scenario.søker.fødselsdato), + periodeTom = LocalDate.now().minusMonths(1), + personResultat = mockk(relaxed = true), + ), + ) + + val behandlingDødsfall = kjørStegprosessForBehandling( + tilSteg = StegType.BEHANDLING_AVSLUTTET, + søkerFnr = scenario.søker.ident, + barnasIdenter = listOf(scenario.barna.first().ident!!), + vedtakService = vedtakService, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingÅrsak = BehandlingÅrsak.DØDSFALL_BRUKER, + overstyrendeVilkårsvurdering = lagVilkårsvurderingFraRestScenario(scenario, overstyrendeVilkårResultater), + + behandlingstype = BehandlingType.REVURDERING, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + endretUtbetalingAndelHentOgPersisterService = endretUtbetalingAndelHentOgPersisterService, + fagsakService = fagsakService, + persongrunnlagService = persongrunnlagService, + andelerTilkjentYtelseOgEndreteUtbetalingerService = andelerTilkjentYtelseOgEndreteUtbetalingerService, + brevmalService = brevmalService, + ) + + val restFagsakEtterBehandlingAvsluttet = + familieBaSakKlient().hentFagsak(fagsakId = behandlingDødsfall.fagsak.id) + + generellAssertFagsak( + restFagsak = restFagsakEtterBehandlingAvsluttet, + fagsakStatus = FagsakStatus.AVSLUTTET, + behandlingStegType = StegType.BEHANDLING_AVSLUTTET, + aktivBehandlingId = behandlingDødsfall.id, + ) + } + + @Test + fun `Dødsfall bruker skal stoppes dersom ikke bosatt i riket er stoppet før dagens dato`() { + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson( + fødselsdato = "1982-01-12", + fornavn = "Mor", + etternavn = "Søker", + ), + barna = listOf( + RestScenarioPerson( + fødselsdato = LocalDate.now().minusMonths(2).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + ), + ), + ) + + behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.first().ident!!), + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + vedtakService = vedtakService, + stegService = stegService, + personidentService = personidentService, + brevmalService = brevmalService, + + ) + + val overstyrendeVilkårResultater = + (scenario.barna + scenario.søker).associate { it.aktørId!! to emptyList() }.toMutableMap() + + assertThrows { + kjørStegprosessForBehandling( + tilSteg = StegType.BEHANDLINGSRESULTAT, + søkerFnr = scenario.søker.ident, + barnasIdenter = listOf(scenario.barna.first().ident!!), + vedtakService = vedtakService, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingÅrsak = BehandlingÅrsak.DØDSFALL_BRUKER, + overstyrendeVilkårsvurdering = lagVilkårsvurderingFraRestScenario( + scenario, + overstyrendeVilkårResultater, + ), + + behandlingstype = BehandlingType.REVURDERING, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + endretUtbetalingAndelHentOgPersisterService = endretUtbetalingAndelHentOgPersisterService, + fagsakService = fagsakService, + persongrunnlagService = persongrunnlagService, + andelerTilkjentYtelseOgEndreteUtbetalingerService = andelerTilkjentYtelseOgEndreteUtbetalingerService, + brevmalService = brevmalService, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/RevurderingMedEndredeUtbetalingandelerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/RevurderingMedEndredeUtbetalingandelerTest.kt new file mode 100644 index 000000000..176ec927e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/RevurderingMedEndredeUtbetalingandelerTest.kt @@ -0,0 +1,283 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.nyOrdinærBehandling +import no.nav.familie.ba.sak.common.nyRevurdering +import no.nav.familie.ba.sak.ekstern.restDomene.RestEndretUtbetalingAndel +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.writeValueAsString +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelService +import no.nav.familie.ba.sak.kjerne.endretutbetaling.domene.Årsak +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.søknad.SøknadGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.søknad.SøknadGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.VilkårsvurderingForNyBehandlingService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class RevurderingMedEndredeUtbetalingandelerTest( + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val vilkårService: VilkårService, + + @Autowired + private val stegService: StegService, + + @Autowired + private val endretUtbetalingAndelHentOgPersisterService: EndretUtbetalingAndelHentOgPersisterService, + + @Autowired + private val endretUtbetalingAndelService: EndretUtbetalingAndelService, + + @Autowired + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val vilkårsvurderingForNyBehandlingService: VilkårsvurderingForNyBehandlingService, + + @Autowired + private val søknadGrunnlagRepository: SøknadGrunnlagRepository, + +) : AbstractVerdikjedetest() { + @Test + fun `Endrede utbetalingsandeler fra forrige behandling kopieres riktig og oppdaterer andel med riktig beløp`() { + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson( + fødselsdato = "${LocalDate.now().minusYears(28)}", + fornavn = "Mor", + etternavn = "Søker", + ), + barna = listOf( + RestScenarioPerson( + fødselsdato = LocalDate.now().minusYears(4).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + ), + ), + ) + val fnr = scenario.søker.ident!! + val barnFnr = scenario.barna[0].ident!! + val barnetsFødselsdato = LocalDate.parse(scenario.barna[0].fødselsdato) + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + + val endretAndelFom = YearMonth.now().minusYears(3).minusMonths(4) + val endretAndelTom = YearMonth.now().minusYears(3) + + // Behandling 1 - førstegangsbehandling + val iverksattFørstegangsbehandling = + lagFørstegangsbehandlingMedEndretUtbetalingAndel( + endretAndelFom = endretAndelFom, + endretAndelTom = endretAndelTom, + søkersIdent = fnr, + barnFnr = barnFnr, + fagsak = fagsak, + barnetsFødselsdato = barnetsFødselsdato, + ) + + // Behandling 2 - revurdering + val behandlingRevurdering = + stegService.håndterNyBehandling(nyRevurdering(søkersIdent = fnr, fagsakId = fagsak.id)) + + persongrunnlagService.lagreOgDeaktiverGammel( + lagTestPersonopplysningGrunnlag( + behandlingId = behandlingRevurdering.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + ), + ) + + val vilkårsvurderingRevurdering = vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandlingRevurdering, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = iverksattFørstegangsbehandling, + ) + + gjennomførVilkårsvurdering( + vilkårsvurdering = vilkårsvurderingRevurdering, + behandling = behandlingRevurdering, + barnetsFødselsdato = barnetsFødselsdato, + ) + + val kopierteEndredeUtbetalingAndeler = + endretUtbetalingAndelHentOgPersisterService.hentForBehandling(behandlingRevurdering.id) + val andelerTilkjentYtelse = + andelerTilkjentYtelseOgEndreteUtbetalingerService.finnAndelerTilkjentYtelseMedEndreteUtbetalinger( + behandlingRevurdering.id, + ) + val andelPåvirketAvEndringer = andelerTilkjentYtelse.first() + + assertEquals(1, kopierteEndredeUtbetalingAndeler.size) + + assertEquals(BigDecimal.ZERO, andelPåvirketAvEndringer.prosent) + assertEquals(endretAndelFom, andelPåvirketAvEndringer.stønadFom) + assertEquals(endretAndelTom, andelPåvirketAvEndringer.stønadTom) + assertTrue(andelPåvirketAvEndringer.endreteUtbetalinger.any { it.id == kopierteEndredeUtbetalingAndeler.single().id }) + } + + private fun gjennomførVilkårsvurdering( + vilkårsvurdering: Vilkårsvurdering, + behandling: Behandling, + barnetsFødselsdato: LocalDate, + ) { + vilkårsvurdering.personResultater.map { personResultat -> + personResultat.tilRestPersonResultat().vilkårResultater.map { + vilkårService.endreVilkår( + behandlingId = behandling.id, + vilkårId = it.id, + restPersonResultat = + RestPersonResultat( + personIdent = personResultat.aktør.aktivFødselsnummer(), + vilkårResultater = listOf( + it.copy( + resultat = Resultat.OPPFYLT, + periodeFom = if (it.vilkårType == Vilkår.UNDER_18_ÅR) { + barnetsFødselsdato + } else { + LocalDate.now().minusYears(3).minusMonths(5).withDayOfMonth(8) + }, + utdypendeVilkårsvurderinger = listOfNotNull( + if (it.vilkårType == Vilkår.BOR_MED_SØKER) UtdypendeVilkårsvurdering.DELT_BOSTED else null, + ), + ), + ), + ), + ) + } + } + behandling.behandlingStegTilstand.add( + BehandlingStegTilstand(behandling = behandling, behandlingSteg = StegType.VILKÅRSVURDERING), + ) + + stegService.håndterVilkårsvurdering(behandling) + } + + private fun lagFørstegangsbehandlingMedEndretUtbetalingAndel( + endretAndelFom: YearMonth, + endretAndelTom: YearMonth, + søkersIdent: String, + barnFnr: String, + fagsak: Fagsak, + barnetsFødselsdato: LocalDate, + ): Behandling { + val førstegangsbehandling = + stegService.håndterNyBehandling(nyOrdinærBehandling(søkersIdent = søkersIdent, fagsakId = fagsak.id)) + + val søknadGrunnlag = SøknadGrunnlag( + behandlingId = førstegangsbehandling.id, + aktiv = true, + søknad = lagSøknadDTO( + søkersIdent, + barnasIdenter = listOf(barnFnr), + underkategori = BehandlingUnderkategori.ORDINÆR, + ).writeValueAsString(), + ) + + søknadGrunnlagRepository.save(søknadGrunnlag) + + persongrunnlagService.lagreOgDeaktiverGammel( + lagTestPersonopplysningGrunnlag( + behandlingId = førstegangsbehandling.id, + søkerPersonIdent = søkersIdent, + barnasIdenter = listOf(barnFnr), + søkerAktør = personidentService.hentOgLagreAktør(søkersIdent, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + ), + ) + + val vilkårsvurdering = vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = førstegangsbehandling, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = null, + ) + + gjennomførVilkårsvurdering( + vilkårsvurdering = vilkårsvurdering, + behandling = førstegangsbehandling, + barnetsFødselsdato = barnetsFødselsdato, + ) + + val endretUtbetalingAndel = + endretUtbetalingAndelService.opprettTomEndretUtbetalingAndelOgOppdaterTilkjentYtelse(førstegangsbehandling) + + val restEndretUtbetalingAndel = RestEndretUtbetalingAndel( + id = endretUtbetalingAndel.id, + fom = endretAndelFom, + tom = endretAndelTom, + avtaletidspunktDeltBosted = LocalDate.now().minusYears(3).withDayOfMonth(8), + søknadstidspunkt = LocalDate.now().minusYears(3).withDayOfMonth(8), + begrunnelse = "begrunnelse", + personIdent = barnFnr, + årsak = Årsak.DELT_BOSTED, + prosent = BigDecimal.ZERO, + erTilknyttetAndeler = false, + ) + + endretUtbetalingAndelService.oppdaterEndretUtbetalingAndelOgOppdaterTilkjentYtelse( + førstegangsbehandling, + endretUtbetalingAndel.id, + restEndretUtbetalingAndel, + ) + + førstegangsbehandling.behandlingStegTilstand.add( + BehandlingStegTilstand(behandling = førstegangsbehandling, behandlingSteg = StegType.BEHANDLINGSRESULTAT), + ) + val behandlingEtterHåndterBehandlingsresultat = stegService.håndterBehandlingsresultat(førstegangsbehandling) + + behandlingEtterHåndterBehandlingsresultat.behandlingStegTilstand.add( + BehandlingStegTilstand( + behandling = førstegangsbehandling, + behandlingSteg = StegType.BEHANDLING_AVSLUTTET, + ), + ) + behandlingEtterHåndterBehandlingsresultat.status = BehandlingStatus.AVSLUTTET + + val iverksattBehandling = + behandlingHentOgPersisterService.lagreEllerOppdater(behandlingEtterHåndterBehandlingsresultat) + + return iverksattBehandling + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/TekniskEndringAvF\303\270dselshendelseTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/TekniskEndringAvF\303\270dselshendelseTest.kt" new file mode 100644 index 000000000..30607385a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/TekniskEndringAvF\303\270dselshendelseTest.kt" @@ -0,0 +1,183 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import io.mockk.every +import no.nav.familie.ba.sak.config.FeatureToggleConfig +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.task.BehandleFødselshendelseTask +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import java.time.LocalDate + +class TekniskEndringAvFødselshendelseTest( + @Autowired private val behandleFødselshendelseTask: BehandleFødselshendelseTask, + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val personidentService: PersonidentService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val stegService: StegService, + @Autowired private val featureToggleService: FeatureToggleService, + @Autowired private val brevmalService: BrevmalService, +) : AbstractVerdikjedetest() { + + @Test + fun `Skal teknisk opphøre fødselshendelse`() { + every { featureToggleService.isEnabled(FeatureToggleConfig.TEKNISK_ENDRING) } returns true + + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1998-01-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = LocalDate.now().minusDays(2).toString(), + fornavn = "Barn", + etternavn = "Barnesen", + ), + ), + ), + ) + val behandling = behandleFødselshendelse( + nyBehandlingHendelse = NyBehandlingHendelse( + morsIdent = scenario.søker.ident!!, + barnasIdenter = listOf(scenario.barna.first().ident!!), + ), + behandleFødselshendelseTask = behandleFødselshendelseTask, + fagsakService = fagsakService, + behandlingHentOgPersisterService = behandlingHentOgPersisterService, + personidentService = personidentService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + + )!! + + val restUtvidetBehandling = familieBaSakKlient().opprettBehandling( + søkersIdent = scenario.søker.ident, + behandlingType = BehandlingType.TEKNISK_ENDRING, + behandlingÅrsak = BehandlingÅrsak.TEKNISK_ENDRING, + fagsakId = behandling.fagsak.id, + ) + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandling, + behandlingStatus = BehandlingStatus.UTREDES, + behandlingStegType = StegType.VILKÅRSVURDERING, + ) + + val minimalFagsak = familieBaSakKlient().hentMinimalFagsakPåPerson(personIdent = scenario.søker.ident) + assertEquals(2, minimalFagsak.data?.behandlinger?.size) + + // Setter alle vilkår til ikke-oppfylt på løpende førstegangsbehandling + restUtvidetBehandling.data!!.personResultater.forEach { restPersonResultat -> + restPersonResultat.vilkårResultater.forEach { + familieBaSakKlient().putVilkår( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + vilkårId = it.id, + restPersonResultat = + RestPersonResultat( + personIdent = restPersonResultat.personIdent, + vilkårResultater = listOf( + it.copy( + resultat = Resultat.IKKE_OPPFYLT, + ), + ), + ), + ) + } + } + + familieBaSakKlient().validerVilkårsvurdering( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + ) + + val restUtvidetBehandlingEtterBehandlingsresultat = + familieBaSakKlient().behandlingsresultatStegOgGåVidereTilNesteSteg( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + ) + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterBehandlingsresultat, + behandlingStatus = BehandlingStatus.UTREDES, + behandlingStegType = StegType.VURDER_TILBAKEKREVING, + behandlingsresultat = Behandlingsresultat.OPPHØRT, + ) + + val restUtvidetBehandlingEtterVurderTilbakekreving = + familieBaSakKlient().lagreTilbakekrevingOgGåVidereTilNesteSteg( + restUtvidetBehandlingEtterBehandlingsresultat.data!!.behandlingId, + RestTilbakekreving(Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, begrunnelse = "begrunnelse"), + ) + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterVurderTilbakekreving, + behandlingStatus = BehandlingStatus.UTREDES, + behandlingStegType = StegType.SEND_TIL_BESLUTTER, + behandlingsresultat = Behandlingsresultat.OPPHØRT, + ) + + val restUtvidetBehandlingEtterSendTilBeslutter = + familieBaSakKlient().sendTilBeslutter(behandlingId = restUtvidetBehandlingEtterVurderTilbakekreving.data?.behandlingId!!) + + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterSendTilBeslutter, + behandlingStatus = BehandlingStatus.FATTER_VEDTAK, + behandlingStegType = StegType.BESLUTTE_VEDTAK, + ) + + val restUtvidetBehandlingEtterIverksetting = + familieBaSakKlient().iverksettVedtak( + behandlingId = restUtvidetBehandlingEtterSendTilBeslutter.data!!.behandlingId, + restBeslutningPåVedtak = RestBeslutningPåVedtak( + Beslutning.GODKJENT, + ), + beslutterHeaders = HttpHeaders().apply { + setBearerAuth( + token( + mapOf( + "groups" to listOf("SAKSBEHANDLER", "BESLUTTER"), + "azp" to "azp-test", + "name" to "Mock McMockface Beslutter", + "NAVident" to "Z0000", + ), + ), + ) + }, + ) + + generellAssertRestUtvidetBehandling( + restUtvidetBehandling = restUtvidetBehandlingEtterIverksetting, + behandlingStatus = BehandlingStatus.IVERKSETTER_VEDTAK, + behandlingStegType = StegType.IVERKSETT_MOT_OPPDRAG, + ) + + håndterIverksettingAvBehandling( + behandlingEtterVurdering = behandlingHentOgPersisterService.hent(behandlingId = restUtvidetBehandlingEtterIverksetting.data?.behandlingId!!), + søkerFnr = scenario.søker.ident, + fagsakStatusEtterIverksetting = FagsakStatus.AVSLUTTET, + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/TriggingAvAutobrev6og18\303\205rTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/TriggingAvAutobrev6og18\303\205rTest.kt" new file mode 100644 index 000000000..123198205 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/TriggingAvAutobrev6og18\303\205rTest.kt" @@ -0,0 +1,242 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import no.nav.familie.ba.sak.common.førsteDagIInneværendeMåned +import no.nav.familie.ba.sak.common.lagSøknadDTO +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestPutVedtaksperiodeMedStandardbegrunnelser +import no.nav.familie.ba.sak.ekstern.restDomene.RestRegistrerSøknad +import no.nav.familie.ba.sak.ekstern.restDomene.RestTilbakekreving +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.autovedtak.omregning.Autobrev6og18ÅrService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.Beslutning +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.RestBeslutningPåVedtak +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.begrunnelser.Standardbegrunnelse +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Vedtaksperiodetype +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenarioPerson +import no.nav.familie.ba.sak.task.dto.Autobrev6og18ÅrDTO +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpHeaders +import java.time.LocalDate +import java.time.YearMonth + +class TriggingAvAutobrev6og18ÅrTest( + @Autowired private val fagsakService: FagsakService, + @Autowired private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + @Autowired private val vedtakService: VedtakService, + @Autowired private val stegService: StegService, + @Autowired private val autobrev6og18ÅrService: Autobrev6og18ÅrService, + @Autowired private val brevmalService: BrevmalService, + @Autowired private val vedtaksperiodeService: VedtaksperiodeService, +) : AbstractVerdikjedetest() { + + @Test + fun `Omregning og autobrev skal kjøres for 18 år og ikke 6 år`() { + kjørFørstegangsbehandlingOgTriggAutobrev(6) + } + + @Test + fun `Omregning og autobrev skal kjøres for 6 år og ikke 18 år`() { + kjørFørstegangsbehandlingOgTriggAutobrev(18) + } + + fun kjørFørstegangsbehandlingOgTriggAutobrev(årMedReduksjonsbegrunnelse: Int) { + val reduksjonsbegrunnelse = if (årMedReduksjonsbegrunnelse == 6) { + Standardbegrunnelse.REDUKSJON_UNDER_6_ÅR + } else { + Standardbegrunnelse.REDUKSJON_UNDER_18_ÅR + } + + val scenario = mockServerKlient().lagScenario( + RestScenario( + søker = RestScenarioPerson(fødselsdato = "1996-11-12", fornavn = "Mor", etternavn = "Søker"), + barna = listOf( + RestScenarioPerson( + fødselsdato = LocalDate.now().minusYears(2).toString(), + fornavn = "Toåringen", + etternavn = "Barnesen", + bostedsadresser = emptyList(), + ), + RestScenarioPerson( + fødselsdato = LocalDate.now().minusYears(6).toString(), + fornavn = "Seksåringen", + etternavn = "Barnesen", + bostedsadresser = emptyList(), + ), + RestScenarioPerson( + fødselsdato = LocalDate.now().minusYears(18).toString(), + fornavn = "Attenåringen", + etternavn = "Barnesen", + bostedsadresser = emptyList(), + ), + ), + ), + ) + + val fagsakId = familieBaSakKlient().opprettFagsak(søkersIdent = scenario.søker.ident!!).data?.id!! + familieBaSakKlient().opprettBehandling(søkersIdent = scenario.søker.ident, fagsakId = fagsakId) + + val restFagsakEtterOpprettelse = familieBaSakKlient().hentFagsak(fagsakId = fagsakId) + + val aktivBehandling = hentAktivBehandling(restFagsak = restFagsakEtterOpprettelse.data!!) + val restRegistrerSøknad = + RestRegistrerSøknad( + søknad = lagSøknadDTO( + søkerIdent = scenario.søker.ident, + barnasIdenter = scenario.barna.map { it.ident!! }, + ), + bekreftEndringerViaFrontend = false, + ) + val restUtvidetBehandling: Ressurs = + familieBaSakKlient().registrererSøknad( + behandlingId = aktivBehandling.behandlingId, + restRegistrerSøknad = restRegistrerSøknad, + ) + + // Godkjenner alle vilkår på førstegangsbehandling. + restUtvidetBehandling.data!!.personResultater.forEach { restPersonResultat -> + restPersonResultat.vilkårResultater.filter { it.resultat == Resultat.IKKE_VURDERT }.forEach { + familieBaSakKlient().putVilkår( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + vilkårId = it.id, + restPersonResultat = RestPersonResultat( + personIdent = restPersonResultat.personIdent, + vilkårResultater = listOf( + it.copy( + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now().minusMonths(2), + ), + ), + ), + ) + } + } + + familieBaSakKlient().validerVilkårsvurdering( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + ) + + val restUtvidetBehandlingEtterBehandlingsresultat = + familieBaSakKlient().behandlingsresultatStegOgGåVidereTilNesteSteg( + behandlingId = restUtvidetBehandling.data!!.behandlingId, + ) + + val restUtvidetBehandlingEtterVurderTilbakekreving = + familieBaSakKlient().lagreTilbakekrevingOgGåVidereTilNesteSteg( + restUtvidetBehandlingEtterBehandlingsresultat.data!!.behandlingId, + RestTilbakekreving(Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, begrunnelse = "begrunnelse"), + ) + + val vedtaksperioderMedBegrunnelser = vedtaksperiodeService.hentRestUtvidetVedtaksperiodeMedBegrunnelser( + restUtvidetBehandlingEtterVurderTilbakekreving.data!!.behandlingId, + ) + + val førsteVedtaksperiode = vedtaksperioderMedBegrunnelser.sortedBy { it.fom }.first() + familieBaSakKlient().oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = førsteVedtaksperiode.id, + restPutVedtaksperiodeMedStandardbegrunnelser = RestPutVedtaksperiodeMedStandardbegrunnelser( + standardbegrunnelser = listOf( + Standardbegrunnelse.INNVILGET_BOR_HOS_SØKER.enumnavnTilString(), + ), + ), + ) + val reduksjonVedtaksperiodeId = + vedtaksperioderMedBegrunnelser.single { + it.fom!!.isEqual( + LocalDate.now().førsteDagIInneværendeMåned(), + ) && it.type == Vedtaksperiodetype.UTBETALING + } + familieBaSakKlient().oppdaterVedtaksperiodeMedStandardbegrunnelser( + vedtaksperiodeId = reduksjonVedtaksperiodeId.id, + restPutVedtaksperiodeMedStandardbegrunnelser = RestPutVedtaksperiodeMedStandardbegrunnelser( + standardbegrunnelser = listOf(reduksjonsbegrunnelse.enumnavnTilString()), + ), + ) + + val restUtvidetBehandlingEtterSendTilBeslutter = + familieBaSakKlient().sendTilBeslutter(behandlingId = restUtvidetBehandlingEtterVurderTilbakekreving.data!!.behandlingId) + + familieBaSakKlient().iverksettVedtak( + behandlingId = restUtvidetBehandlingEtterSendTilBeslutter.data!!.behandlingId, + restBeslutningPåVedtak = RestBeslutningPåVedtak( + Beslutning.GODKJENT, + ), + beslutterHeaders = HttpHeaders().apply { + setBearerAuth( + token( + mapOf( + "groups" to listOf("SAKSBEHANDLER", "BESLUTTER"), + "azp" to "azp-test", + "name" to "Mock McMockface Beslutter", + "NAVident" to "Z0000", + ), + ), + ) + }, + ) + + håndterIverksettingAvBehandling( + behandlingEtterVurdering = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId = fagsakId)!!, + søkerFnr = scenario.søker.ident, + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + + ) + + autobrev6og18ÅrService.opprettOmregningsoppgaveForBarnIBrytingsalder( + autobrev6og18ÅrDTO = Autobrev6og18ÅrDTO( + fagsakId = fagsakId, + alder = årMedReduksjonsbegrunnelse, + årMåned = YearMonth.now(), + ), + ) + + var behandlinger = behandlingHentOgPersisterService.hentBehandlinger(fagsakId) + // Her forventer vi ikke at autobrev skal trigges pga vedtaksbegrunnelsen som er satt på FGB. + assertEquals(1, behandlinger.size) + + autobrev6og18ÅrService.opprettOmregningsoppgaveForBarnIBrytingsalder( + autobrev6og18ÅrDTO = Autobrev6og18ÅrDTO( + fagsakId = fagsakId, + alder = if (årMedReduksjonsbegrunnelse == 6) 18 else 6, + årMåned = YearMonth.now(), + ), + ) + + behandlinger = behandlingHentOgPersisterService.hentBehandlinger(fagsakId) + // Her forventer vi at autobrev skal trigges pga manglende vedtaksbegrunnelse for denne alderen på FGB. + assertEquals(2, behandlinger.size) + + // Skal nye autobrev trigges må aktiv behandling være avsluttet. Gjør dette eksplisitt (og utenfor normal + // flyt) ettersom dette skjer via en task når autobrev-koden kjøres. + val revurderingMedAutobrev = behandlingHentOgPersisterService.finnAktivForFagsak(fagsakId)!! + revurderingMedAutobrev.status = BehandlingStatus.AVSLUTTET + behandlingHentOgPersisterService.lagreEllerOppdater(revurderingMedAutobrev) + + autobrev6og18ÅrService.opprettOmregningsoppgaveForBarnIBrytingsalder( + autobrev6og18ÅrDTO = Autobrev6og18ÅrDTO( + fagsakId = fagsakId, + alder = if (årMedReduksjonsbegrunnelse == 6) 18 else 6, + årMåned = YearMonth.now(), + ), + ) + + behandlinger = behandlingHentOgPersisterService.hentBehandlinger(fagsakId) + // Her forventer vi ikke at autobrev skal trigges fordi det har blitt kjørt. + assertEquals(2, behandlinger.size) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/Utils.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/Utils.kt new file mode 100644 index 000000000..b6457b5d9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/Utils.kt @@ -0,0 +1,246 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester + +import no.nav.familie.ba.sak.common.isSameOrBefore +import no.nav.familie.ba.sak.ekstern.restDomene.RestFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestMinimalFagsak +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.ekstern.restDomene.RestVisningBehandling +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandlingHendelse +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StatusFraOppdragMedTask +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.steg.domene.JournalførVedtaksbrevDTO +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.Utbetalingsperiode +import no.nav.familie.ba.sak.task.BehandleFødselshendelseTask +import no.nav.familie.ba.sak.task.DistribuerDokumentDTO +import no.nav.familie.ba.sak.task.JournalførVedtaksbrevTask +import no.nav.familie.ba.sak.task.StatusFraOppdragTask +import no.nav.familie.ba.sak.task.dto.BehandleFødselshendelseTaskDTO +import no.nav.familie.ba.sak.task.dto.FAGSYSTEM +import no.nav.familie.ba.sak.task.dto.IverksettingTaskDTO +import no.nav.familie.ba.sak.task.dto.StatusFraOppdragDTO +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.prosessering.domene.Task +import org.junit.jupiter.api.Assertions.assertEquals +import java.time.LocalDate +import java.util.Properties + +fun generellAssertRestUtvidetBehandling( + restUtvidetBehandling: Ressurs, + behandlingStatus: BehandlingStatus, + behandlingStegType: StegType? = null, + behandlingsresultat: Behandlingsresultat? = null, +) { + if (restUtvidetBehandling.status != Ressurs.Status.SUKSESS) { + throw IllegalStateException("generellAssertRestUtvidetBehandling feilet. status: ${restUtvidetBehandling.status.name}, melding: ${restUtvidetBehandling.melding}") + } + + assertEquals(behandlingStatus, restUtvidetBehandling.data?.status) + + if (behandlingStegType != null) { + assertEquals(behandlingStegType, restUtvidetBehandling.data?.steg) + } + + if (behandlingsresultat != null) { + assertEquals(behandlingsresultat, restUtvidetBehandling.data?.resultat) + } +} + +fun generellAssertFagsak( + restFagsak: Ressurs, + fagsakStatus: FagsakStatus, + behandlingStegType: StegType? = null, + behandlingsresultat: Behandlingsresultat? = null, + aktivBehandlingId: Long? = null, +) { + if (restFagsak.status != Ressurs.Status.SUKSESS) throw IllegalStateException("generellAssertFagsak feilet. status: ${restFagsak.status.name}, melding: ${restFagsak.melding}") + assertEquals(fagsakStatus, restFagsak.data?.status) + + val aktivBehandling = if (aktivBehandlingId == null) { + hentAktivBehandling(restFagsak = restFagsak.data!!) + } else { + restFagsak.data!!.behandlinger.single { it.behandlingId == aktivBehandlingId } + } + + if (behandlingStegType != null) { + assertEquals(behandlingStegType, aktivBehandling.steg) + } + if (behandlingsresultat != null) { + assertEquals(behandlingsresultat, aktivBehandling.resultat) + } +} + +fun assertUtbetalingsperiode(utbetalingsperiode: Utbetalingsperiode, antallBarn: Int, utbetaltPerMnd: Int) { + assertEquals(antallBarn, utbetalingsperiode.utbetalingsperiodeDetaljer.size) + assertEquals(utbetaltPerMnd, utbetalingsperiode.utbetaltPerMnd) +} + +fun hentNåværendeEllerNesteMånedsUtbetaling(behandling: RestUtvidetBehandling): Int { + val utbetalingsperioder = + behandling.utbetalingsperioder.sortedBy { it.periodeFom } + val nåværendeUtbetalingsperiode = utbetalingsperioder + .firstOrNull { it.periodeFom.isSameOrBefore(LocalDate.now()) && it.periodeTom.isAfter(LocalDate.now()) } + + val nesteUtbetalingsperiode = utbetalingsperioder.firstOrNull { it.periodeFom.isAfter(LocalDate.now()) } + + return nåværendeUtbetalingsperiode?.utbetaltPerMnd ?: nesteUtbetalingsperiode?.utbetaltPerMnd ?: 0 +} + +fun hentAktivBehandling(restFagsak: RestFagsak): RestUtvidetBehandling { + return restFagsak.behandlinger.single() +} + +fun hentAktivBehandling(restMinimalFagsak: RestMinimalFagsak): RestVisningBehandling { + return restMinimalFagsak.behandlinger.single { it.aktiv } +} + +fun behandleFødselshendelse( + nyBehandlingHendelse: NyBehandlingHendelse, + fagsakStatusEtterVurdering: FagsakStatus = FagsakStatus.OPPRETTET, + behandleFødselshendelseTask: BehandleFødselshendelseTask, + fagsakService: FagsakService, + behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + personidentService: PersonidentService, + vedtakService: VedtakService, + stegService: StegService, + brevmalService: BrevmalService, +): Behandling? { + val søkerFnr = nyBehandlingHendelse.morsIdent + val søkerAktør = personidentService.hentAktør(søkerFnr) + + behandleFødselshendelseTask.doTask( + BehandleFødselshendelseTask.opprettTask( + BehandleFødselshendelseTaskDTO( + nyBehandling = nyBehandlingHendelse, + ), + ), + ) + + val restMinimalFagsakEtterVurdering = fagsakService.hentMinimalFagsakForPerson(aktør = søkerAktør) + if (restMinimalFagsakEtterVurdering.status != Ressurs.Status.SUKSESS) { + return null + } + + val behandlingEtterVurdering = + behandlingHentOgPersisterService.hentBehandlinger(fagsakId = restMinimalFagsakEtterVurdering.data!!.id) + .maxByOrNull { it.opprettetTidspunkt }!! + if (behandlingEtterVurdering.erHenlagt()) { + return behandlingEtterVurdering + } + + generellAssertFagsak( + restFagsak = fagsakService.hentRestFagsak(restMinimalFagsakEtterVurdering.data!!.id), + fagsakStatus = fagsakStatusEtterVurdering, + behandlingStegType = StegType.IVERKSETT_MOT_OPPDRAG, + aktivBehandlingId = behandlingEtterVurdering.id, + ) + + return håndterIverksettingAvBehandling( + behandlingEtterVurdering = behandlingEtterVurdering, + søkerFnr = søkerFnr, + fagsakService = fagsakService, + vedtakService = vedtakService, + stegService = stegService, + brevmalService = brevmalService, + ) +} + +fun håndterIverksettingAvBehandling( + behandlingEtterVurdering: Behandling, + søkerFnr: String, + fagsakStatusEtterIverksetting: FagsakStatus = FagsakStatus.LØPENDE, + fagsakService: FagsakService, + vedtakService: VedtakService, + stegService: StegService, + brevmalService: BrevmalService, +): Behandling { + val vedtak = vedtakService.hentAktivForBehandlingThrows(behandlingId = behandlingEtterVurdering.id) + val behandlingEtterIverksetteVedtak = + stegService.håndterIverksettMotØkonomi( + behandlingEtterVurdering, + IverksettingTaskDTO( + behandlingsId = behandlingEtterVurdering.id, + vedtaksId = vedtak.id, + saksbehandlerId = "System", + personIdent = behandlingEtterVurdering.fagsak.aktør.aktivFødselsnummer(), + ), + ) + + val behandlingEtterStatusFraOppdrag = + stegService.håndterStatusFraØkonomi( + behandlingEtterIverksetteVedtak, + StatusFraOppdragMedTask( + statusFraOppdragDTO = StatusFraOppdragDTO( + fagsystem = FAGSYSTEM, + personIdent = søkerFnr, + aktørId = behandlingEtterVurdering.fagsak.aktør.aktørId, + behandlingsId = behandlingEtterIverksetteVedtak.id, + vedtaksId = vedtak.id, + ), + task = Task(type = StatusFraOppdragTask.TASK_STEP_TYPE, payload = ""), + ), + ) + + val behandlingEtterIverksettTilbakekreving = + if (behandlingEtterStatusFraOppdrag.steg == StegType.IVERKSETT_MOT_FAMILIE_TILBAKE) { + stegService.håndterIverksettMotFamilieTilbake( + behandling = behandlingEtterStatusFraOppdrag, + metadata = Properties(), + ) + } else { + behandlingEtterStatusFraOppdrag + } + + val behandlingSomSkalFerdigstilles = + if (behandlingEtterIverksettTilbakekreving.steg == StegType.JOURNALFØR_VEDTAKSBREV) { + val behandlingEtterJournalførtVedtak = + stegService.håndterJournalførVedtaksbrev( + behandlingEtterStatusFraOppdrag, + JournalførVedtaksbrevDTO( + vedtakId = vedtak.id, + task = Task(type = JournalførVedtaksbrevTask.TASK_STEP_TYPE, payload = ""), + ), + ) + + val behandlingEtterDistribuertVedtak = + stegService.håndterDistribuerVedtaksbrev( + behandlingEtterJournalførtVedtak, + DistribuerDokumentDTO( + behandlingId = behandlingEtterJournalførtVedtak.id, + journalpostId = "1234", + personEllerInstitusjonIdent = søkerFnr, + brevmal = brevmalService.hentBrevmal( + behandlingEtterJournalførtVedtak, + ), + erManueltSendt = false, + ), + ) + behandlingEtterDistribuertVedtak + } else { + behandlingEtterStatusFraOppdrag + } + + val ferdigstiltBehandling = stegService.håndterFerdigstillBehandling(behandlingSomSkalFerdigstilles) + + val restMinimalFagsakEtterAvsluttetBehandling = + fagsakService.hentMinimalFagsakForPerson(aktør = ferdigstiltBehandling.fagsak.aktør) + generellAssertFagsak( + restFagsak = fagsakService.hentRestFagsak(restMinimalFagsakEtterAvsluttetBehandling.data!!.id), + fagsakStatus = fagsakStatusEtterIverksetting, + behandlingStegType = StegType.BEHANDLING_AVSLUTTET, + aktivBehandlingId = hentAktivBehandling( + restMinimalFagsakEtterAvsluttetBehandling.data!!, + ).behandlingId, + ) + + return ferdigstiltBehandling +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/mockserver/MockserverKlient.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/mockserver/MockserverKlient.kt new file mode 100644 index 000000000..4570b84b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/mockserver/MockserverKlient.kt @@ -0,0 +1,25 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver + +import no.nav.familie.ba.sak.common.convertDataClassToJson +import no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene.RestScenario +import org.slf4j.LoggerFactory +import org.springframework.web.client.RestOperations +import org.springframework.web.client.postForEntity + +class MockserverKlient( + private val mockServerUrl: String, + private val restOperations: RestOperations, +) { + fun lagScenario(restScenario: RestScenario): RestScenario { + val scenario = restOperations.postForEntity("$mockServerUrl/rest/scenario", restScenario).body + ?: error("Klarte ikke lage scenario med data $restScenario") + logger.info("Laget scenario: ${scenario.convertDataClassToJson()}") + + return scenario + } + + companion object { + + val logger = LoggerFactory.getLogger(MockserverKlient::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/mockserver/domene/RestScenario.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/mockserver/domene/RestScenario.kt new file mode 100644 index 000000000..43fc5b59a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/mockserver/domene/RestScenario.kt @@ -0,0 +1,6 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene + +data class RestScenario( + val søker: RestScenarioPerson, + val barna: List, +) diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/mockserver/domene/RestScenarioPerson.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/mockserver/domene/RestScenarioPerson.kt new file mode 100644 index 000000000..839dfadaa --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/verdikjedetester/mockserver/domene/RestScenarioPerson.kt @@ -0,0 +1,66 @@ +package no.nav.familie.ba.sak.kjerne.verdikjedetester.mockserver.domene + +import no.nav.familie.ba.sak.integrasjoner.pdl.domene.PdlFolkeregisteridentifikator +import no.nav.familie.kontrakter.ba.infotrygd.InfotrygdSøkResponse +import no.nav.familie.kontrakter.ba.infotrygd.Sak +import no.nav.familie.kontrakter.felles.personopplysning.Bostedsadresse +import no.nav.familie.kontrakter.felles.personopplysning.Matrikkeladresse +import no.nav.familie.kontrakter.felles.personopplysning.Statsborgerskap +import java.time.LocalDate +import java.time.Month +import java.time.Period + +data class RestScenarioPerson( + val ident: String? = null, // Settes av mock-server + val aktørId: String? = null, // Settes av mock-server + val forelderBarnRelasjon: List = emptyList(), // Settes av mock-server + val folkeregisteridentifikator: List = emptyList(), // Settes av mock-server + val fødselsdato: String, // yyyy-mm-dd + val fornavn: String, + val etternavn: String, + val infotrygdSaker: InfotrygdSøkResponse? = null, + val statsborgerskap: List = listOf( + Statsborgerskap( + land = "NOR", + gyldigFraOgMed = LocalDate.parse(fødselsdato), + bekreftelsesdato = LocalDate.parse(fødselsdato), + gyldigTilOgMed = null, + ), + ), + val bostedsadresser: List = defaultBostedsadresseHistorikk, +) { + + val navn = "$fornavn $etternavn" + + val alder = Period.between(LocalDate.parse(fødselsdato), LocalDate.now()).years +} + +val defaultBostedsadresseHistorikk = mutableListOf( + Bostedsadresse( + angittFlyttedato = LocalDate.now().minusDays(15), + gyldigTilOgMed = null, + matrikkeladresse = Matrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ), + ), + Bostedsadresse( + angittFlyttedato = LocalDate.of(2018, Month.JANUARY, 1), + gyldigTilOgMed = LocalDate.now().minusDays(16), + matrikkeladresse = Matrikkeladresse( + matrikkelId = 123L, + bruksenhetsnummer = "H301", + tilleggsnavn = "navn", + postnummer = "0202", + kommunenummer = "2231", + ), + ), +) + +data class ForelderBarnRelasjon( + val relatertPersonsIdent: String, + val relatertPersonsRolle: String, +) diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rServiceTest.kt" new file mode 100644 index 000000000..f41ffeb6f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rServiceTest.kt" @@ -0,0 +1,1392 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.FunksjonellFeil +import no.nav.familie.ba.sak.common.kjørStegprosessForFGB +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.common.nyOrdinærBehandling +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.common.til18ÅrsVilkårsdato +import no.nav.familie.ba.sak.common.vurderVilkårsvurderingTilInnvilget +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagBarnVilkårResultat +import no.nav.familie.ba.sak.datagenerator.vilkårsvurdering.lagSøkerVilkårResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestNyttVilkår +import no.nav.familie.ba.sak.ekstern.restDomene.RestPersonResultat +import no.nav.familie.ba.sak.ekstern.restDomene.RestSlettVilkår +import no.nav.familie.ba.sak.ekstern.restDomene.RestVilkårResultat +import no.nav.familie.ba.sak.ekstern.restDomene.tilRestPersonResultat +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.behandlingstema.BehandlingstemaService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.BehandlingStegStatus +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.steg.grunnlagForNyBehandling.VilkårsvurderingForNyBehandlingService +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.AnnenVurderingType +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.PersonResultat +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.ResultatBegrunnelse +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.LocalDateTime + +class VilkårServiceTest( + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val behandlingHentOgPersisterService: BehandlingHentOgPersisterService, + + @Autowired + private val vilkårsvurderingService: VilkårsvurderingService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val vilkårService: VilkårService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val behandlingstemaService: BehandlingstemaService, + + @Autowired + private val vedtakService: VedtakService, + + @Autowired + private val vedtaksperiodeService: VedtaksperiodeService, + + @Autowired + private val stegService: StegService, + + @Autowired + private val vilkårsvurderingForNyBehandlingService: VilkårsvurderingForNyBehandlingService, + + @Autowired + private val brevmalService: BrevmalService, + +) : AbstractSpringIntegrationTest() { + + @BeforeAll + fun init() { + databaseCleanupService.truncate() + } + + @Test + fun `Manuell vilkårsvurdering skal få erAutomatiskVurdert på enkelte vilkår`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val forrigeBehandlingSomErIverksatt = + behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId = behandling.fagsak.id) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val vilkårsvurdering = vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErIverksatt, + ) + vilkårsvurdering.personResultater.forEach { personResultat -> + personResultat.vilkårResultater.forEach { vilkårResultat -> + when (vilkårResultat.vilkårType) { + Vilkår.UNDER_18_ÅR, Vilkår.GIFT_PARTNERSKAP -> assertTrue(vilkårResultat.erAutomatiskVurdert) + else -> assertFalse(vilkårResultat.erAutomatiskVurdert) + } + } + } + } + + @Test + fun `Endring på automatisk vurderte vilkår(manuell vilkårsvurdering) skal settes til manuell ved endring`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = + behandlingService.opprettBehandling(nyOrdinærBehandling(søkersIdent = fnr, fagsakId = fagsak.id)) + val forrigeBehandlingSomErIverksatt = + behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId = behandling.fagsak.id) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val vilkårsvurdering = vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErIverksatt, + ) + val under18ÅrVilkårForBarn = + vilkårsvurdering.personResultater.find { it.aktør.aktivFødselsnummer() == barnFnr } + ?.tilRestPersonResultat()?.vilkårResultater?.find { it.vilkårType == Vilkår.UNDER_18_ÅR } + + val endretVilkårsvurdering: List = + vilkårService.endreVilkår( + behandlingId = behandling.id, + vilkårId = under18ÅrVilkårForBarn!!.id, + restPersonResultat = + RestPersonResultat( + personIdent = barnFnr, + vilkårResultater = listOf( + under18ÅrVilkårForBarn.copy( + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2019, 5, 8), + ), + ), + ), + ) + + val endretUnder18ÅrVilkårForBarn = + endretVilkårsvurdering.find { it.personIdent == barnFnr } + ?.vilkårResultater?.find { it.vilkårType == Vilkår.UNDER_18_ÅR } + assertFalse(endretUnder18ÅrVilkårForBarn!!.erAutomatiskVurdert) + } + + @Test + fun `Skal automatisk lagre ny vilkårsvurdering over den gamle`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val barnFnr2 = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val forrigeBehandlingSomErIverksatt = + behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId = behandling.fagsak.id) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val vilkårsvurdering = vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErIverksatt, + ) + assertEquals(2, vilkårsvurdering.personResultater.size) + + val personopplysningGrunnlagMedEkstraBarn = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr, barnFnr2), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr, barnFnr2), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlagMedEkstraBarn) + + val vilkårsvurderingMedEkstraBarn = vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErIverksatt, + ) + assertEquals(3, vilkårsvurderingMedEkstraBarn.personResultater.size) + } + + @Test + fun `vurder ugyldig vilkårsvurdering`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + } + + @Test + fun `Vilkårsvurdering kopieres riktig`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val forrigeBehandlingSomErIverksatt = + behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId = behandling.fagsak.id) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val vilkårsvurdering = vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErIverksatt, + ) + .also { + it.personResultater + .forEach { personResultat -> + personResultat.leggTilBlankAnnenVurdering(AnnenVurderingType.OPPLYSNINGSPLIKT) + } + } + + val kopiertVilkårsvurdering = vilkårsvurdering.kopier(inkluderAndreVurderinger = true) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = kopiertVilkårsvurdering) + val personResultater = vilkårsvurderingService + .hentAktivForBehandling(behandlingId = behandling.id)!!.personResultater + + assertEquals(2, personResultater.size) + Assertions.assertNotEquals(vilkårsvurdering.id, kopiertVilkårsvurdering.id) + assertEquals(1, kopiertVilkårsvurdering.personResultater.first().andreVurderinger.size) + assertEquals( + AnnenVurderingType.OPPLYSNINGSPLIKT, + kopiertVilkårsvurdering.personResultater.first().andreVurderinger.first().type, + ) + } + + @Test + fun `Resultatbegrunnelse kan ikke settes i kombinasjon med ugyldig vilkår`() { + val vilkårsvurdering = lagVilkårsvurderingForEnSøkerMedEttBarn() + + val enPersonIBehandlingen = vilkårsvurdering.personResultater.elementAt(0) + val bosattVilkårForEnPersonIBehandlingen = + enPersonIBehandlingen.tilRestPersonResultat().vilkårResultater.find { it.vilkårType === Vilkår.BOSATT_I_RIKET } + + assertThrows { + vilkårService.endreVilkår( + behandlingId = vilkårsvurdering.behandling.id, + vilkårId = bosattVilkårForEnPersonIBehandlingen!!.id, + restPersonResultat = + RestPersonResultat( + personIdent = enPersonIBehandlingen.aktør.aktivFødselsnummer(), + vilkårResultater = listOf( + bosattVilkårForEnPersonIBehandlingen.copy( + resultat = Resultat.OPPFYLT, + resultatBegrunnelse = ResultatBegrunnelse.IKKE_AKTUELT, + vurderesEtter = Regelverk.EØS_FORORDNINGEN, + periodeFom = LocalDate.of(2019, 5, 8), + ), + ), + ), + ) + } + } + + @Test + fun `Resultatbegrunnelse kan ikke settes i kombinasjon med ugyldig resultat`() { + val vilkårsvurdering = lagVilkårsvurderingForEnSøkerMedEttBarn() + + val enPersonIBehandlingen = vilkårsvurdering.personResultater.elementAt(0) + val oppholdVilkårForEnPersonIBehandlingen = + enPersonIBehandlingen.tilRestPersonResultat().vilkårResultater.find { it.vilkårType === Vilkår.LOVLIG_OPPHOLD } + + assertThrows { + vilkårService.endreVilkår( + behandlingId = vilkårsvurdering.behandling.id, + vilkårId = oppholdVilkårForEnPersonIBehandlingen!!.id, + restPersonResultat = + RestPersonResultat( + personIdent = enPersonIBehandlingen.aktør.aktivFødselsnummer(), + vilkårResultater = listOf( + oppholdVilkårForEnPersonIBehandlingen.copy( + resultat = Resultat.IKKE_OPPFYLT, + resultatBegrunnelse = ResultatBegrunnelse.IKKE_AKTUELT, + vurderesEtter = Regelverk.EØS_FORORDNINGEN, + periodeFom = LocalDate.of(2019, 5, 8), + ), + ), + ), + ) + } + } + + @Test + fun `Resultatbegrunnelse kan ikke settes i kombinasjon med ugyldig regelverk`() { + val vilkårsvurdering = lagVilkårsvurderingForEnSøkerMedEttBarn() + + val enPersonIBehandlingen = vilkårsvurdering.personResultater.elementAt(0) + val oppholdVilkårForEnPersonIBehandlingen = + enPersonIBehandlingen.tilRestPersonResultat().vilkårResultater.find { it.vilkårType === Vilkår.LOVLIG_OPPHOLD } + + assertThrows { + vilkårService.endreVilkår( + behandlingId = vilkårsvurdering.behandling.id, + vilkårId = oppholdVilkårForEnPersonIBehandlingen!!.id, + restPersonResultat = + RestPersonResultat( + personIdent = enPersonIBehandlingen.aktør.aktivFødselsnummer(), + vilkårResultater = listOf( + oppholdVilkårForEnPersonIBehandlingen.copy( + resultat = Resultat.OPPFYLT, + resultatBegrunnelse = ResultatBegrunnelse.IKKE_AKTUELT, + vurderesEtter = Regelverk.NASJONALE_REGLER, + periodeFom = LocalDate.of(2019, 5, 8), + ), + ), + ), + ) + } + } + + @Test + fun `Resultatbegrunnelse kaster ikke feil når brukt i kombinasjon med gyldig vilkår, resultat og regelverk`() { + val vilkårsvurdering = lagVilkårsvurderingForEnSøkerMedEttBarn() + + val enPersonIBehandlingen = vilkårsvurdering.personResultater.elementAt(0) + val oppholdVilkårForEnPersonIBehandlingen = + enPersonIBehandlingen.tilRestPersonResultat().vilkårResultater.find { it.vilkårType === Vilkår.LOVLIG_OPPHOLD } + + assertDoesNotThrow { + vilkårService.endreVilkår( + behandlingId = vilkårsvurdering.behandling.id, + vilkårId = oppholdVilkårForEnPersonIBehandlingen!!.id, + restPersonResultat = + RestPersonResultat( + personIdent = enPersonIBehandlingen.aktør.aktivFødselsnummer(), + vilkårResultater = listOf( + oppholdVilkårForEnPersonIBehandlingen.copy( + resultat = Resultat.OPPFYLT, + resultatBegrunnelse = ResultatBegrunnelse.IKKE_AKTUELT, + vurderesEtter = Regelverk.EØS_FORORDNINGEN, + periodeFom = LocalDate.of(2019, 5, 8), + ), + ), + ), + ) + } + } + + @Test + fun `Vilkårsvurdering fra forrige behandling kopieres riktig`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + var behandling = + behandlingService.opprettBehandling(nyOrdinærBehandling(søkersIdent = fnr, fagsakId = fagsak.id)) + val forrigeBehandlingSomErIverksatt = + behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId = behandling.fagsak.id) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val vilkårsvurdering = vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErIverksatt, + ) + assertEquals(2, vilkårsvurdering.personResultater.size) + + vilkårsvurdering.personResultater.map { personResultat -> + personResultat.tilRestPersonResultat().vilkårResultater.map { + vilkårService.endreVilkår( + behandlingId = behandling.id, + vilkårId = it.id, + restPersonResultat = + RestPersonResultat( + personIdent = personResultat.aktør.aktivFødselsnummer(), + vilkårResultater = listOf( + it.copy( + resultat = Resultat.OPPFYLT, + resultatBegrunnelse = if (it.vilkårType === Vilkår.LOVLIG_OPPHOLD) ResultatBegrunnelse.IKKE_AKTUELT else null, + vurderesEtter = Regelverk.EØS_FORORDNINGEN, + periodeFom = LocalDate.of(2019, 5, 8), + ), + ), + ), + ) + } + } + + behandling = markerBehandlingSomAvsluttet(behandling) + + val barnFnr2 = randomFnr() + + val behandling2 = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + val personopplysningGrunnlag2 = + lagTestPersonopplysningGrunnlag( + behandling2.id, + fnr, + listOf(barnFnr, barnFnr2), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr, barnFnr2), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag2) + + val vilkårsvurdering2 = vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling2, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = behandling, + ) + + assertEquals(3, vilkårsvurdering2.personResultater.size) + + vilkårsvurdering2.personResultater.forEach { personResultat -> + personResultat.vilkårResultater.forEach { vilkårResultat -> + if (personResultat.aktør.aktivFødselsnummer() == barnFnr2) { + assertEquals(behandling2.id, vilkårResultat.sistEndretIBehandlingId) + } else { + if (vilkårResultat.vilkårType === Vilkår.LOVLIG_OPPHOLD) { + assertEquals(vilkårResultat.resultatBegrunnelse, vilkårResultat.resultatBegrunnelse) + } else { + assertEquals(null, vilkårResultat.resultatBegrunnelse) + } + + assertEquals(Resultat.OPPFYLT, vilkårResultat.resultat) + assertEquals(behandling.id, vilkårResultat.sistEndretIBehandlingId) + } + } + } + } + + @Test + fun `Peker til behandling oppdateres ved vurdering av revurdering`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + var behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + val forrigeBehandlingSomErIverksatt = + behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId = behandling.fagsak.id) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val vilkårsvurdering = vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErIverksatt, + ) + + val barn: Person = personopplysningGrunnlag.barna.find { it.aktør.aktivFødselsnummer() == barnFnr }!! + vurderVilkårsvurderingTilInnvilget(vilkårsvurdering, barn) + + vilkårsvurderingService.oppdater(vilkårsvurdering) + behandling = markerBehandlingSomAvsluttet(behandling) + + val barnFnr2 = randomFnr() + + val behandling2 = behandlingService.lagreNyOgDeaktiverGammelBehandling(lagBehandling(fagsak)) + + val personopplysningGrunnlag2 = + lagTestPersonopplysningGrunnlag( + behandling2.id, + fnr, + listOf(barnFnr, barnFnr2), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr, barnFnr2), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag2) + + val vilkårsvurdering1 = vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling2, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = behandling, + ) + + assertEquals(3, vilkårsvurdering1.personResultater.size) + + val personResultat = vilkårsvurdering1.personResultater.find { it.aktør.aktivFødselsnummer() == barnFnr }!! + val borMedSøkerVilkår = personResultat.vilkårResultater.find { it.vilkårType == Vilkår.BOR_MED_SØKER }!! + assertEquals(behandling.id, borMedSøkerVilkår.sistEndretIBehandlingId) + + VilkårsvurderingUtils.muterPersonVilkårResultaterPut( + personResultat, + RestVilkårResultat( + borMedSøkerVilkår.id, + Vilkår.BOR_MED_SØKER, + Resultat.OPPFYLT, + LocalDate.of(2010, 6, 2), + LocalDate.of(2011, 9, 1), + "", + "", + LocalDateTime.now(), + behandling.id, + ), + ) + + val vilkårsvurderingEtterEndring = vilkårsvurderingService.oppdater(vilkårsvurdering1) + val personResultatEtterEndring = + vilkårsvurderingEtterEndring.personResultater.find { it.aktør.aktivFødselsnummer() == barnFnr }!! + val borMedSøkerVilkårEtterEndring = + personResultatEtterEndring.vilkårResultater.find { it.vilkårType == Vilkår.BOR_MED_SØKER }!! + assertEquals(behandling2.id, borMedSøkerVilkårEtterEndring.sistEndretIBehandlingId) + } + + @Test + fun `Skal legge til både VURDERING_ANNET_GRUNNLAG og VURDERT_MEDLEMSKAP i utdypendeVilkårsvurderinger liste`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = + behandlingService.opprettBehandling(nyOrdinærBehandling(søkersIdent = fnr, fagsakId = fagsak.id)) + val forrigeBehandlingSomErIverksatt = + behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId = behandling.fagsak.id) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val vilkårsvurdering = vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErIverksatt, + ) + val under18ÅrVilkårForBarn = + vilkårsvurdering.personResultater.find { it.aktør.aktivFødselsnummer() == barnFnr } + ?.tilRestPersonResultat()?.vilkårResultater?.find { it.vilkårType == Vilkår.UNDER_18_ÅR } + + val endretVilkårsvurdering: List = + vilkårService.endreVilkår( + behandlingId = behandling.id, + vilkårId = under18ÅrVilkårForBarn!!.id, + restPersonResultat = + RestPersonResultat( + personIdent = barnFnr, + vilkårResultater = listOf( + under18ÅrVilkårForBarn.copy( + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2019, 5, 8), + utdypendeVilkårsvurderinger = listOf( + UtdypendeVilkårsvurdering.VURDERING_ANNET_GRUNNLAG, + UtdypendeVilkårsvurdering.VURDERT_MEDLEMSKAP, + ), + ), + ), + ), + ) + + val endretUnder18ÅrVilkårForBarn = + endretVilkårsvurdering.find { it.personIdent == barnFnr } + ?.vilkårResultater?.find { it.vilkårType == Vilkår.UNDER_18_ÅR } + + assertEquals( + 2, + endretUnder18ÅrVilkårForBarn!!.utdypendeVilkårsvurderinger.size, + ) + } + + @Test + fun `skal lage vilkårsvurderingsperiode for vanlig migrering tilbake i tid`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val forrigeVilkårsdato = LocalDate.of(2021, 8, 1) + val nyMigreringsdato = LocalDate.of(2021, 1, 1) + val barnetsFødselsdato = LocalDate.now().minusYears((LocalDate.now().year - nyMigreringsdato.year + 1).toLong()) + + val behandlinger = lagMigreringsbehandling(fnr, barnFnr, barnetsFødselsdato, forrigeVilkårsdato) + val behandling = behandlinger.second + + val vilkårsvurdering = + vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + behandling = behandling, + forrigeBehandlingSomErVedtatt = behandlinger.first, + nyMigreringsdato = nyMigreringsdato, + ) + assertTrue { vilkårsvurdering.personResultater.isNotEmpty() } + val søkerVilkårResultat = + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == fnr }.vilkårResultater + assertTrue { søkerVilkårResultat.size == 2 } + assertTrue { + søkerVilkårResultat.all { + it.periodeFom == nyMigreringsdato && + it.periodeTom == null + } + } + + val barnVilkårResultat = + vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr }.vilkårResultater + assertTrue { barnVilkårResultat.size == 5 } + assertTrue { + barnVilkårResultat.filter { it.vilkårType.påvirketVilkårForEndreMigreringsdato() }.all { + it.periodeFom == nyMigreringsdato && + it.periodeTom == null + } + } + assertTrue { + barnVilkårResultat.filter { it.vilkårType == Vilkår.UNDER_18_ÅR }.all { + it.periodeFom == barnetsFødselsdato && + it.periodeTom == barnetsFødselsdato.til18ÅrsVilkårsdato() + } + } + assertTrue { + barnVilkårResultat.filter { it.vilkårType == Vilkår.GIFT_PARTNERSKAP }.all { + it.periodeFom == barnetsFødselsdato && + it.periodeTom == null + } + } + } + + @Test + fun `skal lage utvidet barnetrygd vilkår for migreringsbehandling`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val forrigeVilkårsdato = LocalDate.of(2021, 8, 1) + val barnetsFødselsdato = LocalDate.now().minusYears(1) + + val behandlinger = lagMigreringsbehandling(fnr, barnFnr, barnetsFødselsdato, forrigeVilkårsdato) + val nåVærendeBehandling = behandlinger.second + + val nyMigreringsdato = LocalDate.of(2021, 1, 1) + var vilkårsvurdering = + vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + behandling = nåVærendeBehandling, + forrigeBehandlingSomErVedtatt = behandlinger.first, + nyMigreringsdato = nyMigreringsdato, + ) + assertTrue { vilkårsvurdering.personResultater.isNotEmpty() } + assertTrue { vilkårsvurdering.personResultater.any { it.aktør.aktivFødselsnummer() == fnr } } + assertTrue { vilkårsvurdering.personResultater.find { it.aktør.aktivFødselsnummer() == fnr }!!.vilkårResultater.size == 2 } + vilkårService.postVilkår( + nåVærendeBehandling.id, + RestNyttVilkår( + personIdent = fnr, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + ), + ) + vilkårsvurdering = vilkårService.hentVilkårsvurdering(nåVærendeBehandling.id)!! + assertEquals(BehandlingUnderkategori.UTVIDET, vilkårsvurdering.behandling.underkategori) + assertTrue { vilkårsvurdering.personResultater.find { it.aktør.aktivFødselsnummer() == fnr }!!.vilkårResultater.size == 3 } + val personResultat = vilkårsvurdering.personResultater.find { it.aktør.aktivFødselsnummer() == fnr }!! + assertTrue { personResultat.vilkårResultater.any { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } } + val utvidetBarnetrygdVilkår = + personResultat.vilkårResultater.first { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + assertEquals(Resultat.IKKE_VURDERT, utvidetBarnetrygdVilkår.resultat) + assertNull(utvidetBarnetrygdVilkår.periodeFom) + assertNull(utvidetBarnetrygdVilkår.periodeTom) + } + + @Test + fun `skal ikke lage utvidet barnetrygd vilkår for ordinær behandling`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak = BehandlingÅrsak.SØKNAD, + ), + ) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(LocalDate.now().minusYears(1)), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling(behandling, false, null) + val exception = assertThrows { + vilkårService.postVilkår( + behandling.id, + RestNyttVilkår( + personIdent = fnr, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + ), + ) + } + assertEquals( + "${Vilkår.UTVIDET_BARNETRYGD.beskrivelse} kan ikke legges til for behandling " + + "${behandling.id} med behandlingType ${behandling.type.visningsnavn}", + exception.message, + ) + } + + @Test + fun `skal kunne legge til utvidet barnetrygd vilkår for ordinær behandling dersom det er utvidet på vilkårsvurderingen allerede`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + underkategori = BehandlingUnderkategori.UTVIDET, + årsak = BehandlingÅrsak.SØKNAD, + ), + ) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(LocalDate.now().minusYears(1)), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + val vilkårsvurdering = + vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling(behandling, false, null) + assertEquals( + 1, + vilkårsvurdering.personResultater.find { it.erSøkersResultater() }?.vilkårResultater?.filter { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD }?.size, + ) + + val utvidetVilkår = + vilkårsvurdering.personResultater.find { it.erSøkersResultater() }?.vilkårResultater?.single { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + vilkårService.endreVilkår( + behandlingId = behandling.id, + vilkårId = utvidetVilkår!!.id, + restPersonResultat = RestPersonResultat( + personIdent = fnr, + vilkårResultater = listOf( + RestVilkårResultat( + id = utvidetVilkår.id, + vilkårType = utvidetVilkår.vilkårType, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.now().minusYears(2), + periodeTom = null, + begrunnelse = "", + endretAv = "", + endretTidspunkt = LocalDateTime.now(), + behandlingId = behandling.id, + ), + ), + ), + ) + + assertDoesNotThrow { + vilkårService.postVilkår( + behandling.id, + RestNyttVilkår( + personIdent = fnr, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + ), + ) + } + } + + @Test + fun `skal ikke lage utvidet barnetrygd vilkår for barn i migreringsbehandling`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val forrigeVilkårsdato = LocalDate.of(2021, 8, 1) + val barnetsFødselsdato = LocalDate.now().minusYears(1) + + val behandlinger = lagMigreringsbehandling(fnr, barnFnr, barnetsFødselsdato, forrigeVilkårsdato) + val nåVærendeBehandling = behandlinger.second + + val nyMigreringsdato = LocalDate.of(2021, 1, 1) + val vilkårsvurdering = + vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + behandling = nåVærendeBehandling, + forrigeBehandlingSomErVedtatt = behandlinger.first, + nyMigreringsdato = nyMigreringsdato, + ) + assertTrue { vilkårsvurdering.personResultater.isNotEmpty() } + assertTrue { vilkårsvurdering.personResultater.any { it.aktør.aktivFødselsnummer() == fnr } } + assertTrue { vilkårsvurdering.personResultater.find { it.aktør.aktivFødselsnummer() == fnr }!!.vilkårResultater.size == 2 } + val exception = assertThrows { + vilkårService.postVilkår( + nåVærendeBehandling.id, + RestNyttVilkår( + personIdent = barnFnr, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + ), + ) + } + assertEquals("${Vilkår.UTVIDET_BARNETRYGD.beskrivelse} kan ikke legges til for BARN", exception.message) + } + + @Test + fun `skal ikke slette bor med søker vilkår for migreringsbehandling`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val forrigeVilkårsdato = LocalDate.of(2021, 8, 1) + val barnetsFødselsdato = LocalDate.now().minusYears(1) + + val behandlinger = lagMigreringsbehandling(fnr, barnFnr, barnetsFødselsdato, forrigeVilkårsdato) + val behandling = behandlinger.second + vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + behandling = behandling, + forrigeBehandlingSomErVedtatt = behandlinger.first, + nyMigreringsdato = LocalDate.of(2021, 1, 1), + ) + + val exception = assertThrows { + vilkårService.deleteVilkår( + behandling.id, + RestSlettVilkår( + personIdent = fnr, + vilkårType = Vilkår.BOR_MED_SØKER, + ), + ) + } + assertEquals( + "Vilkår ${Vilkår.BOR_MED_SØKER.beskrivelse} kan ikke slettes " + + "for behandling ${behandling.id}", + exception.message, + ) + } + + @Test + fun `skal ikke slette utvidet barnetrygd vilkår for førstegangsbehandling`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + årsak = BehandlingÅrsak.SØKNAD, + ), + ) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(LocalDate.now().minusYears(1)), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling(behandling, false, null) + + val exception = assertThrows { + vilkårService.deleteVilkår( + behandling.id, + RestSlettVilkår( + personIdent = fnr, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + ), + ) + } + assertEquals( + "Vilkår ${Vilkår.UTVIDET_BARNETRYGD.beskrivelse} kan ikke slettes " + + "for behandling ${behandling.id}", + exception.message, + ) + } + + @Test + fun `skal ikke slette utvidet barnetrygd vilkår for migreringsbehandling når det finnes i forrige behandling`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val forrigeVilkårsdato = LocalDate.of(2021, 8, 1) + val barnetsFødselsdato = LocalDate.now().minusYears(1) + + val behandlinger = lagMigreringsbehandling(fnr, barnFnr, barnetsFødselsdato, forrigeVilkårsdato) + var forrigeBehandling = behandlinger.first + val behandling = behandlinger.second + + val forrigeVilkårvurdering = vilkårService.hentVilkårsvurdering(forrigeBehandling.id)!! + val forrigeSøkerPersonResultat = + forrigeVilkårvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == fnr } + val forrigeBarnPersonResultat = + forrigeVilkårvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr } + val forrigeVilkårResultat = forrigeSøkerPersonResultat.vilkårResultater + forrigeVilkårResultat.add( + lagVilkårResultat( + personResultat = forrigeSøkerPersonResultat, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + periodeFom = LocalDate.of(2021, 5, 1), + periodeTom = LocalDate.of(2021, 5, 31), + behandlingId = forrigeBehandling.id, + ), + ) + vilkårsvurderingService.oppdater( + forrigeVilkårvurdering.copy( + personResultater = setOf( + forrigeSøkerPersonResultat, + forrigeBarnPersonResultat, + ), + ), + ) + forrigeBehandling = behandlingHentOgPersisterService.hent(forrigeBehandling.id) + forrigeBehandling.behandlingStegTilstand.add( + BehandlingStegTilstand( + behandling = forrigeBehandling, + behandlingSteg = StegType.BEHANDLING_AVSLUTTET, + behandlingStegStatus = BehandlingStegStatus.UTFØRT, + ), + ) + behandlingHentOgPersisterService.lagreEllerOppdater(forrigeBehandling) + + val vilkårsvurdering = + vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + behandling = behandling, + forrigeBehandlingSomErVedtatt = forrigeBehandling, + nyMigreringsdato = LocalDate.of(2021, 1, 1), + ) + assertTrue { vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == fnr }.vilkårResultater.size == 3 } + val exception = assertThrows { + vilkårService.deleteVilkår( + behandling.id, + RestSlettVilkår( + personIdent = fnr, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + ), + ) + } + assertEquals( + "Vilkår ${Vilkår.UTVIDET_BARNETRYGD.beskrivelse} kan ikke slettes " + + "for behandling ${behandling.id}", + exception.message, + ) + } + + @Test + fun `skal slette utvidet barnetrygd vilkår for migreringsbehandling`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val forrigeVilkårsdato = LocalDate.of(2021, 8, 1) + val barnetsFødselsdato = LocalDate.now().minusYears(1) + + val behandlinger = lagMigreringsbehandling(fnr, barnFnr, barnetsFødselsdato, forrigeVilkårsdato) + val behandling = behandlinger.second + vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForMigreringsbehandlingMedÅrsakEndreMigreringsdato( + behandling = behandling, + forrigeBehandlingSomErVedtatt = behandlinger.first, + nyMigreringsdato = LocalDate.of(2021, 1, 1), + ) + + vilkårService.postVilkår( + behandling.id, + RestNyttVilkår(personIdent = fnr, vilkårType = Vilkår.UTVIDET_BARNETRYGD), + ) + + val vilkårsvurderingFørSlett = vilkårService.hentVilkårsvurdering(behandling.id)!! + + assertEquals(BehandlingUnderkategori.UTVIDET, vilkårsvurderingFørSlett.behandling.underkategori) + assertTrue { + vilkårsvurderingFørSlett + .personResultater.first { it.aktør.aktivFødselsnummer() == fnr }.vilkårResultater.size == 3 + } + + assertTrue { + vilkårsvurderingFørSlett + .personResultater.first { it.aktør.aktivFødselsnummer() == fnr } + .vilkårResultater.any { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + } + + vilkårService.deleteVilkår( + behandling.id, + RestSlettVilkår( + personIdent = fnr, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + ), + ) + + val vilkårsvurderingEtterSlett = vilkårService.hentVilkårsvurdering(behandling.id)!! + + assertEquals(BehandlingUnderkategori.ORDINÆR, vilkårsvurderingEtterSlett.behandling.underkategori) + assertTrue { + vilkårsvurderingEtterSlett + .personResultater.first { it.aktør.aktivFødselsnummer() == fnr }.vilkårResultater.size == 2 + } + + assertTrue { + vilkårsvurderingEtterSlett + .personResultater.first { it.aktør.aktivFødselsnummer() == fnr } + .vilkårResultater.none { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + } + } + + @Test + fun `skal slette utvidet barnetrygd vilkår for helmanuell migrering`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val barnetsFødselsdato = LocalDate.of(2020, 8, 15) + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + ), + ) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val nyMigreringsdato = LocalDate.of(2021, 1, 1) + vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForHelmanuellMigrering( + behandling, + nyMigreringsdato, + ) + + vilkårService.postVilkår( + behandling.id, + RestNyttVilkår(personIdent = fnr, vilkårType = Vilkår.UTVIDET_BARNETRYGD), + ) + + val vilkårsvurderingFørSlett = vilkårService.hentVilkårsvurdering(behandling.id)!! + + assertEquals(BehandlingUnderkategori.UTVIDET, vilkårsvurderingFørSlett.behandling.underkategori) + assertTrue { + vilkårsvurderingFørSlett + .personResultater.first { it.aktør.aktivFødselsnummer() == fnr }.vilkårResultater.size == 3 + } + + assertTrue { + vilkårsvurderingFørSlett + .personResultater.first { it.aktør.aktivFødselsnummer() == fnr } + .vilkårResultater.any { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + } + + vilkårService.deleteVilkår( + behandling.id, + RestSlettVilkår( + personIdent = fnr, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + ), + ) + + val vilkårsvurderingEtterSlett = vilkårService.hentVilkårsvurdering(behandling.id)!! + + assertEquals(BehandlingUnderkategori.ORDINÆR, vilkårsvurderingEtterSlett.behandling.underkategori) + assertTrue { + vilkårsvurderingEtterSlett + .personResultater.first { it.aktør.aktivFødselsnummer() == fnr }.vilkårResultater.size == 2 + } + + assertTrue { + vilkårsvurderingEtterSlett + .personResultater.first { it.aktør.aktivFødselsnummer() == fnr } + .vilkårResultater.none { it.vilkårType == Vilkår.UTVIDET_BARNETRYGD } + } + } + + @Test + fun `skal ikke endre vilkårsvurderingsperiode før migreringsdato for migreringsbehandling`() { + val fnr = randomFnr() + val barnFnr = randomFnr() + val barnetsFødselsdato = LocalDate.of(2020, 8, 1) + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.HELMANUELL_MIGRERING, + ), + ) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + val nyMigreringsdato = LocalDate.of(2021, 1, 1) + behandlingService.lagreNedMigreringsdato(nyMigreringsdato, behandling) + val vilkårsvurdering = vilkårsvurderingForNyBehandlingService.genererVilkårsvurderingForHelmanuellMigrering( + behandling, + nyMigreringsdato, + ) + + assertTrue { vilkårsvurdering.personResultater.isNotEmpty() } + assertTrue { vilkårsvurdering.personResultater.size == 2 } + + val søkerPersonResultat = vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == fnr } + assertTrue { søkerPersonResultat.vilkårResultater.isNotEmpty() } + assertTrue { søkerPersonResultat.vilkårResultater.size == 2 } + assertTrue { + søkerPersonResultat.vilkårResultater.all { + it.periodeTom == null && + it.periodeFom == nyMigreringsdato + } + } + + val barnPersonResultat = vilkårsvurdering.personResultater.first { it.aktør.aktivFødselsnummer() == barnFnr } + assertTrue { barnPersonResultat.vilkårResultater.isNotEmpty() } + assertTrue { barnPersonResultat.vilkårResultater.size == 5 } + assertTrue { + barnPersonResultat.vilkårResultater.filter { !it.vilkårType.gjelderAlltidFraBarnetsFødselsdato() }.all { + it.periodeTom == null && + it.periodeFom == nyMigreringsdato + } + } + + val vilkårId = barnPersonResultat.vilkårResultater.single { it.vilkårType == Vilkår.BOR_MED_SØKER }.id + val restVilkårResultat = RestVilkårResultat( + id = vilkårId, + vilkårType = Vilkår.BOR_MED_SØKER, + resultat = Resultat.OPPFYLT, + periodeFom = LocalDate.of(2020, 10, 1), + periodeTom = null, + begrunnelse = "Migrering", + endretAv = "", + endretTidspunkt = LocalDateTime.now(), + behandlingId = behandling.id, + ) + val exception = assertThrows { + vilkårService.endreVilkår( + behandling.id, + vilkårId, + RestPersonResultat( + barnFnr, + listOf(restVilkårResultat), + ), + ) + } + assertEquals( + "${Vilkår.BOR_MED_SØKER} kan ikke endres før $nyMigreringsdato " + + "for fagsak=${behandling.fagsak.id}", + exception.message, + ) + } + + @Test + fun `skal sette vurderes etter basert på behandlingstema`() { + val behandling = kjørStegprosessForFGB( + tilSteg = StegType.BEHANDLINGSRESULTAT, + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + + ) + var vilkårsvurdering = vilkårService.hentVilkårsvurderingThrows(behandling.id) + assertTrue { + vilkårsvurdering.personResultater.all { personResultat -> + personResultat.vilkårResultater.filter { + it.vilkårType !in listOf(Vilkår.UNDER_18_ÅR, Vilkår.GIFT_PARTNERSKAP) + }.all { it.vurderesEtter == Regelverk.NASJONALE_REGLER } + } + } + + behandlingstemaService.oppdaterBehandlingstema( + behandling, + BehandlingKategori.EØS, + BehandlingUnderkategori.ORDINÆR, + ) + vilkårsvurdering = vilkårService.hentVilkårsvurderingThrows(behandling.id) + assertTrue { + vilkårsvurdering.personResultater.all { personResultat -> + personResultat.vilkårResultater.filter { + it.vilkårType !in listOf(Vilkår.UNDER_18_ÅR, Vilkår.GIFT_PARTNERSKAP) + }.all { it.vurderesEtter == Regelverk.NASJONALE_REGLER } + } + } + + vilkårService.postVilkår(behandling.id, RestNyttVilkår(ClientMocks.barnFnr[0], Vilkår.BOR_MED_SØKER)) + + vilkårsvurdering = vilkårService.hentVilkårsvurderingThrows(behandling.id) + assertTrue { + vilkårsvurdering.personResultater.all { personResultat -> + personResultat.vilkårResultater.filter { + it.vilkårType == Vilkår.BOR_MED_SØKER && it.resultat == Resultat.IKKE_VURDERT + }.all { it.vurderesEtter == Regelverk.EØS_FORORDNINGEN } + } + } + } + + private fun lagMigreringsbehandling( + fnr: String, + barnFnr: String, + barnetsFødselsdato: LocalDate, + forrigeVilkårsdato: LocalDate, + ): Pair { + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + var forrigeBehandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.MIGRERING, + ), + ) + val forrigePersonopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = forrigeBehandling.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(forrigePersonopplysningGrunnlag) + + var forrigeVilkårsvurdering = Vilkårsvurdering(behandling = forrigeBehandling) + val søkerPersonResultat = PersonResultat( + vilkårsvurdering = forrigeVilkårsvurdering, + aktør = personidentService.hentOgLagreAktør(fnr, true), + ) + søkerPersonResultat.setSortedVilkårResultater( + lagSøkerVilkårResultat( + søkerPersonResultat = søkerPersonResultat, + periodeFom = forrigeVilkårsdato, + behandlingId = forrigeBehandling.id, + ), + ) + val barnPersonResultat = PersonResultat( + vilkårsvurdering = forrigeVilkårsvurdering, + aktør = personidentService.hentOgLagreAktør(barnFnr, true), + ) + barnPersonResultat.setSortedVilkårResultater( + lagBarnVilkårResultat( + barnPersonResultat = barnPersonResultat, + barnetsFødselsdato = barnetsFødselsdato, + periodeFom = forrigeVilkårsdato, + behandlingId = forrigeBehandling.id, + flytteSak = false, + ), + ) + forrigeVilkårsvurdering = forrigeVilkårsvurdering.apply { + personResultater = setOf( + søkerPersonResultat, + barnPersonResultat, + ) + } + vilkårsvurderingService.lagreNyOgDeaktiverGammel(forrigeVilkårsvurdering) + + forrigeBehandling = markerBehandlingSomAvsluttet(forrigeBehandling) + + val behandling = behandlingService.lagreNyOgDeaktiverGammelBehandling( + lagBehandling( + fagsak = fagsak, + behandlingType = BehandlingType.MIGRERING_FRA_INFOTRYGD, + årsak = BehandlingÅrsak.ENDRE_MIGRERINGSDATO, + ), + ) + val personopplysningGrunnlag = lagTestPersonopplysningGrunnlag( + behandlingId = behandling.id, + søkerPersonIdent = fnr, + barnasIdenter = listOf(barnFnr), + barnasFødselsdatoer = listOf(barnetsFødselsdato), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + return Pair(forrigeBehandling, behandling) + } + + private fun lagVilkårsvurderingForEnSøkerMedEttBarn(): Vilkårsvurdering { + val fnr = randomFnr() + val barnFnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr) + var behandling = + behandlingService.opprettBehandling(nyOrdinærBehandling(søkersIdent = fnr, fagsakId = fagsak.id)) + + val forrigeBehandlingSomErIverksatt = + behandlingHentOgPersisterService.hentSisteBehandlingSomErIverksatt(fagsakId = behandling.fagsak.id) + + val personopplysningGrunnlag = + lagTestPersonopplysningGrunnlag( + behandling.id, + fnr, + listOf(barnFnr), + søkerAktør = personidentService.hentOgLagreAktør(fnr, true), + barnAktør = personidentService.hentOgLagreAktørIder(listOf(barnFnr), true), + ) + persongrunnlagService.lagreOgDeaktiverGammel(personopplysningGrunnlag) + + return vilkårsvurderingForNyBehandlingService.initierVilkårsvurderingForBehandling( + behandling = behandling, + bekreftEndringerViaFrontend = true, + forrigeBehandlingSomErVedtatt = forrigeBehandlingSomErIverksatt, + ) + } + + private fun markerBehandlingSomAvsluttet(behandling: Behandling): Behandling { + behandling.status = BehandlingStatus.AVSLUTTET + behandling.leggTilBehandlingStegTilstand(StegType.BEHANDLING_AVSLUTTET) + return behandlingHentOgPersisterService.lagreOgFlush(behandling) + } + + fun Vilkår.påvirketVilkårForEndreMigreringsdato() = this in listOf( + Vilkår.BOSATT_I_RIKET, + Vilkår.LOVLIG_OPPHOLD, + Vilkår.BOR_MED_SØKER, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingFlyttResultaterTest.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingFlyttResultaterTest.kt" new file mode 100644 index 000000000..d6e86f67f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingFlyttResultaterTest.kt" @@ -0,0 +1,212 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.lagBehandling +import no.nav.familie.ba.sak.common.lagPerson +import no.nav.familie.ba.sak.common.lagPersonResultat +import no.nav.familie.ba.sak.common.lagVilkårResultat +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.config.FeatureToggleService +import no.nav.familie.ba.sak.datagenerator.behandling.kjørStegprosessForBehandling +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.beregning.domene.AndelerTilkjentYtelseOgEndreteUtbetalingerService +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.endretutbetaling.EndretUtbetalingAndelHentOgPersisterService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class VilkårsvurderingFlyttResultaterTest( + @Autowired + private val vilkårsvurderingService: VilkårsvurderingService, + + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val personidentService: PersonidentService, + + @Autowired + private val persongrunnlagService: PersongrunnlagService, + + @Autowired + private val vedtakService: VedtakService, + + @Autowired + private val stegService: StegService, + + @Autowired + private val vedtaksperiodeService: VedtaksperiodeService, + + @Autowired + private val endretUtbetalingAndelHentOgPersisterService: EndretUtbetalingAndelHentOgPersisterService, + + @Autowired + private val andelerTilkjentYtelseOgEndreteUtbetalingerService: AndelerTilkjentYtelseOgEndreteUtbetalingerService, + + @Autowired + private val brevmalService: BrevmalService, + + @Autowired + private val featureToggleService: FeatureToggleService, + +) : AbstractSpringIntegrationTest() { + + @BeforeAll + fun init() { + databaseCleanupService.truncate() + } + + @Test + fun `Skal ikke endre på forrige behandling sin vilkårsvurdering ved flytting av resultater`() { + val søker = lagPerson(type = PersonType.SØKER) + + val barn1Fnr = ClientMocks.barnFnr[0] + val barn1Aktør = personidentService.hentAktør(barn1Fnr) + + val barn2Fnr = ClientMocks.barnFnr[1] + val barn2Aktør = personidentService.hentAktør(barn2Fnr) + + // Lager førstegangsbehandling med utvidet vilkåret avslått + val vilkårsvurderingMedUtvidetAvslått = Vilkårsvurdering(behandling = lagBehandling()) + val søkerPersonResultat = lagPersonResultat( + vilkårsvurdering = vilkårsvurderingMedUtvidetAvslått, + person = søker, + periodeFom = LocalDate.now().minusMonths(8), + periodeTom = LocalDate.now().plusYears(2), + lagFullstendigVilkårResultat = true, + personType = PersonType.SØKER, + resultat = Resultat.OPPFYLT, + ) + + søkerPersonResultat.addVilkårResultat( + lagVilkårResultat( + personResultat = søkerPersonResultat, + vilkårType = Vilkår.UTVIDET_BARNETRYGD, + resultat = Resultat.IKKE_OPPFYLT, + periodeFom = LocalDate.now().minusMonths(8), + periodeTom = LocalDate.now().plusYears(2), + behandlingId = vilkårsvurderingMedUtvidetAvslått.behandling.id, + ), + ) + + val førstegangsbehandlingPersonResultater = setOf( + søkerPersonResultat, + lagPersonResultat( + vilkårsvurdering = vilkårsvurderingMedUtvidetAvslått, + person = lagPerson(type = PersonType.BARN, aktør = barn1Aktør, fødselsdato = ClientMocks.personInfo[barn1Fnr]!!.fødselsdato), + periodeFom = LocalDate.now().minusMonths(8), + periodeTom = LocalDate.now().plusYears(2), + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + resultat = Resultat.OPPFYLT, + ), + lagPersonResultat( + vilkårsvurdering = vilkårsvurderingMedUtvidetAvslått, + person = lagPerson(type = PersonType.BARN, aktør = barn2Aktør, fødselsdato = ClientMocks.personInfo[barn2Fnr]!!.fødselsdato), + periodeFom = LocalDate.now().minusMonths(8), + periodeTom = LocalDate.now().plusYears(2), + lagFullstendigVilkårResultat = true, + personType = PersonType.BARN, + resultat = Resultat.OPPFYLT, + ), + ) + + vilkårsvurderingMedUtvidetAvslått.personResultater = førstegangsbehandlingPersonResultater + + val førstegangsbehandling = kjørStegprosessForBehandling( + søkerFnr = søker.aktør.aktivFødselsnummer(), + barnasIdenter = listOf(barn1Fnr, barn2Fnr), + vedtakService = vedtakService, + underkategori = BehandlingUnderkategori.UTVIDET, + behandlingÅrsak = BehandlingÅrsak.SØKNAD, + overstyrendeVilkårsvurdering = vilkårsvurderingMedUtvidetAvslått, + behandlingstype = BehandlingType.FØRSTEGANGSBEHANDLING, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + endretUtbetalingAndelHentOgPersisterService = endretUtbetalingAndelHentOgPersisterService, + fagsakService = fagsakService, + persongrunnlagService = persongrunnlagService, + andelerTilkjentYtelseOgEndreteUtbetalingerService = andelerTilkjentYtelseOgEndreteUtbetalingerService, + brevmalService = brevmalService, + ) + + val vilkårsvurderingFraForrigeBehandlingFørNyRevurdering = + vilkårsvurderingService.hentAktivForBehandling(behandlingId = førstegangsbehandling.id) + + // Lager revurdering når utvidet ikke løper, så underkategorien er ordinær + kjørStegprosessForBehandling( + tilSteg = StegType.REGISTRERE_PERSONGRUNNLAG, + søkerFnr = søker.aktør.aktivFødselsnummer(), + barnasIdenter = listOf(barn1Fnr, barn2Fnr), + vedtakService = vedtakService, + underkategori = BehandlingUnderkategori.ORDINÆR, + behandlingÅrsak = BehandlingÅrsak.NYE_OPPLYSNINGER, + overstyrendeVilkårsvurdering = Vilkårsvurdering(behandling = lagBehandling()), + behandlingstype = BehandlingType.REVURDERING, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + endretUtbetalingAndelHentOgPersisterService = endretUtbetalingAndelHentOgPersisterService, + fagsakService = fagsakService, + persongrunnlagService = persongrunnlagService, + andelerTilkjentYtelseOgEndreteUtbetalingerService = andelerTilkjentYtelseOgEndreteUtbetalingerService, + brevmalService = brevmalService, + ) + + // Sjekker at vilkårsvurderingen fra forrige behandling ikke er endret + val vilkårsvurderingFraForrigeBehandlingEtterNyRevurdering = + vilkårsvurderingService.hentAktivForBehandling(behandlingId = førstegangsbehandling.id) + + val søkersVilkår = + vilkårsvurderingFraForrigeBehandlingEtterNyRevurdering?.personResultater?.find { it.erSøkersResultater() }?.vilkårResultater + Assertions.assertEquals( + 3, + søkersVilkår?.size, + ) + Assertions.assertEquals(søkerPersonResultat.vilkårResultater, søkersVilkår) + + Assertions.assertEquals( + 5, + vilkårsvurderingFraForrigeBehandlingEtterNyRevurdering?.personResultater?.find { it.aktør.aktivFødselsnummer() == barn1Fnr }?.vilkårResultater?.size, + ) + Assertions.assertEquals( + vilkårsvurderingMedUtvidetAvslått.personResultater.find { it.aktør.aktivFødselsnummer() == barn1Fnr }?.vilkårResultater, + vilkårsvurderingFraForrigeBehandlingEtterNyRevurdering?.personResultater?.find { it.aktør.aktivFødselsnummer() == barn1Fnr }?.vilkårResultater, + ) + + Assertions.assertEquals( + 5, + vilkårsvurderingFraForrigeBehandlingEtterNyRevurdering?.personResultater?.find { it.aktør.aktivFødselsnummer() == barn2Fnr }?.vilkårResultater?.size, + ) + Assertions.assertEquals( + vilkårsvurderingMedUtvidetAvslått.personResultater.find { it.aktør.aktivFødselsnummer() == barn2Fnr }?.vilkårResultater, + vilkårsvurderingFraForrigeBehandlingEtterNyRevurdering?.personResultater?.find { it.aktør.aktivFødselsnummer() == barn2Fnr }?.vilkårResultater, + ) + + Assertions.assertEquals( + vilkårsvurderingFraForrigeBehandlingFørNyRevurdering?.id, + vilkårsvurderingFraForrigeBehandlingEtterNyRevurdering?.id, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTestController.kt" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTestController.kt" new file mode 100644 index 000000000..72366a145 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/kjerne/vilk\303\245rsvurdering/Vilk\303\245rsvurderingTestController.kt" @@ -0,0 +1,174 @@ +package no.nav.familie.ba.sak.kjerne.vilkårsvurdering + +import no.nav.familie.ba.sak.common.lagTestPersonopplysningGrunnlag +import no.nav.familie.ba.sak.common.tilfeldigPerson +import no.nav.familie.ba.sak.ekstern.restDomene.RestUtvidetBehandling +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.NyBehandling +import no.nav.familie.ba.sak.kjerne.behandling.UtvidetBehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.beregning.BeregningService +import no.nav.familie.ba.sak.kjerne.eøs.endringsabonnement.TilpassKompetanserTilRegelverkService +import no.nav.familie.ba.sak.kjerne.eøs.felles.BehandlingId +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.Person +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonType +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlag +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.personident.AktørIdRepository +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.Måned +import no.nav.familie.ba.sak.kjerne.tidslinje.tidspunkt.MånedTidspunkt.Companion.tilMånedTidspunkt +import no.nav.familie.ba.sak.kjerne.tidslinje.util.VilkårsvurderingBuilder +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Regelverk.EØS_FORORDNINGEN +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering.BARN_BOR_I_NORGE_MED_SØKER +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.UtdypendeVilkårsvurdering.OMFATTET_AV_NORSK_LOVGIVNING +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.BOR_MED_SØKER +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkår.BOSATT_I_RIKET +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.domene.Vilkårsvurdering +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.context.annotation.Profile +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate + +@RestController +@RequestMapping("/api/test/vilkaarsvurdering") +@ProtectedWithClaims(issuer = "azuread") +@Validated +@Profile("!prod") +class VilkårsvurderingTestController( + private val utvidetBehandlingService: UtvidetBehandlingService, + private val fagsakService: FagsakService, + private val behandlingService: BehandlingService, + private val beregningService: BeregningService, + private val personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository, + private val behandlingRepository: BehandlingRepository, + private val vilkårsvurderingService: VilkårsvurderingService, + private val aktørIdRepository: AktørIdRepository, + private val tilpassKompetanserTilRegelverkService: TilpassKompetanserTilRegelverkService, +) { + + @PostMapping() + fun opprettBehandlingMedVilkårsvurdering( + @RequestBody personresultater: Map>, + ): ResponseEntity> { + val personer = personresultater.tilPersoner() + .map { it.copy(aktør = aktørIdRepository.saveAndFlush(it.aktør)) } + + val søker = personer.first { it.type == PersonType.SØKER } + val barn = personer.filter { it.type == PersonType.BARN } + + val fagsak = fagsakService.hentEllerOpprettFagsak(søker.aktør.aktivFødselsnummer()) + + val behandling = behandlingService.opprettBehandling( + NyBehandling( + kategori = BehandlingKategori.EØS, + underkategori = BehandlingUnderkategori.ORDINÆR, + søkersIdent = søker.aktør.aktivFødselsnummer(), + behandlingType = BehandlingType.FØRSTEGANGSBEHANDLING, + søknadMottattDato = LocalDate.now(), + barnasIdenter = barn.map { it.aktør.aktivFødselsnummer() }, + fagsakId = fagsak.id, + ), + ) + + // Opprett persongrunnlag + val personopplysningGrunnlag = personopplysningGrunnlagRepository.save( + lagTestPersonopplysningGrunnlag(behandling.id, *personer.toTypedArray()), + ) + + // Opprett og lagre vilkårsvurdering + val vilkårsvurdering = personresultater.tilVilkårsvurdering( + behandling, + personopplysningGrunnlag, + ) + + vilkårsvurderingService.lagreInitielt( + vilkårsvurdering, + ) + + beregningService.oppdaterBehandlingMedBeregning(behandling, personopplysningGrunnlag) + tilpassKompetanserTilRegelverkService.tilpassKompetanserTilRegelverk(BehandlingId(behandling.id)) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandling.id))) + } + + @PostMapping("/{behandlingId}") + fun oppdaterVilkårsvurderingIBehandling( + @PathVariable behandlingId: Long, + @RequestBody personresultater: Map>, + ): ResponseEntity> { + val personopplysningGrunnlag = personopplysningGrunnlagRepository.findByBehandlingAndAktiv(behandlingId) + val behandling = behandlingRepository.finnBehandling(behandlingId) + + val nyVilkårsvurdering = personresultater.tilVilkårsvurdering( + behandling, + personopplysningGrunnlag!!, + ) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel(nyVilkårsvurdering) + + beregningService.oppdaterBehandlingMedBeregning(behandling, personopplysningGrunnlag) + tilpassKompetanserTilRegelverkService.tilpassKompetanserTilRegelverk(BehandlingId(behandling.id)) + + return ResponseEntity.ok(Ressurs.success(utvidetBehandlingService.lagRestUtvidetBehandling(behandlingId = behandlingId))) + } +} + +private fun Map>.tilPersoner(): List { + return this.keys.mapIndexed { indeks, startTidspunkt -> + when (indeks) { + 0 -> tilfeldigPerson(personType = PersonType.SØKER, fødselsdato = startTidspunkt) + else -> tilfeldigPerson(personType = PersonType.BARN, fødselsdato = startTidspunkt) + } + }.map { + it.copy(id = 0).also { it.sivilstander.clear() } + } // tilfeldigPerson inneholder litt for mye, så fjerner det +} + +fun Map>.tilVilkårsvurdering( + behandling: Behandling, + personopplysningGrunnlag: PersonopplysningGrunnlag, +): Vilkårsvurdering { + val builder = VilkårsvurderingBuilder(behandling) + + this.entries.forEach { (startTidspunkt, vilkårsresultater) -> + val person = personopplysningGrunnlag.personer.first { it.fødselsdato == startTidspunkt } + + val personBuilder = builder.forPerson(person, startTidspunkt.tilMånedTidspunkt()) + vilkårsresultater.forEach { (vilkår, tidslinje) -> personBuilder.medVilkår(tidslinje, vilkår) } + personBuilder.byggPerson() + } + + return builder.byggVilkårsvurdering().leggPåUtdypendeVilkår() +} + +private fun Vilkårsvurdering.leggPåUtdypendeVilkår(): Vilkårsvurdering { + this.personResultater.forEach { personresultat -> + personresultat.vilkårResultater.forEach { + when { + it.vilkårType == BOSATT_I_RIKET && personresultat.erSøkersResultater() && it.vurderesEtter == EØS_FORORDNINGEN -> + it.utdypendeVilkårsvurderinger = it.utdypendeVilkårsvurderinger + OMFATTET_AV_NORSK_LOVGIVNING + it.vilkårType == BOSATT_I_RIKET && !personresultat.erSøkersResultater() && it.vurderesEtter == EØS_FORORDNINGEN -> + it.utdypendeVilkårsvurderinger = it.utdypendeVilkårsvurderinger + BARN_BOR_I_NORGE + it.vilkårType == BOR_MED_SØKER && !personresultat.erSøkersResultater() && it.vurderesEtter == EØS_FORORDNINGEN -> + it.utdypendeVilkårsvurderinger = it.utdypendeVilkårsvurderinger + BARN_BOR_I_NORGE_MED_SØKER + else -> Any() + } + } + } + + return this +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/sikkerhet/RolletilgangTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/sikkerhet/RolletilgangTest.kt new file mode 100644 index 000000000..28c76535d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/sikkerhet/RolletilgangTest.kt @@ -0,0 +1,154 @@ +package no.nav.familie.ba.sak.sikkerhet + +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.familie.ba.sak.WebSpringAuthTestRunner +import no.nav.familie.ba.sak.common.nyOrdinærBehandling +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.fagsak.Fagsak +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRequest +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.objectMapper +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.test.context.ActiveProfiles +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.postForEntity + +@ActiveProfiles( + "postgres", + "integrasjonstest", + "mock-pdl", + "mock-ident-client", + "mock-infotrygd-barnetrygd", + "mock-tilbakekreving-klient", + "mock-brev-klient", + "mock-økonomi", + "mock-infotrygd-feed", + "mock-rest-template-config", + "mock-task-repository", + "mock-task-service", +) +class RolletilgangTest( + @Autowired + private val fagsakService: FagsakService, +) : WebSpringAuthTestRunner() { + + @Test + fun `Skal kaste feil når innlogget veileder prøver å opprette fagsak gjennom rest-endepunkt`() { + val fnr = randomFnr() + + val header = HttpHeaders() + header.contentType = MediaType.APPLICATION_JSON + header.setBearerAuth( + token( + mapOf( + "groups" to listOf("VEILDER"), + "name" to "Mock McMockface", + "NAVident" to "Z0000", + ), + ), + ) + val requestEntity = HttpEntity( + objectMapper.writeValueAsString( + FagsakRequest( + personIdent = fnr, + ), + ), + header, + ) + + val error = assertThrows { + restTemplate.postForEntity>( + hentUrl("/api/fagsaker"), + requestEntity, + ) + } + + val ressurs: Ressurs = objectMapper.readValue(error.responseBodyAsString) + + assertEquals(HttpStatus.FORBIDDEN, error.statusCode) + assertEquals(Ressurs.Status.IKKE_TILGANG, ressurs.status) + assertEquals( + "Mock McMockface med rolle VEILEDER har ikke tilgang til å opprette fagsak. Krever SAKSBEHANDLER.", + ressurs.melding, + ) + } + + @Test + fun `Skal få 201 når innlogget saksbehandler prøver å opprette fagsak gjennom rest-endepunkt, tester også db-tilgangskontroll`() { + val fnr = randomFnr() + + val header = HttpHeaders() + header.contentType = MediaType.APPLICATION_JSON + header.setBearerAuth( + token( + mapOf( + "groups" to listOf("VEILDER", "SAKSBEHANDLER"), + "name" to "Mock McMockface", + "NAVident" to "Z0000", + ), + ), + ) + val requestEntity = HttpEntity( + objectMapper.writeValueAsString( + FagsakRequest( + personIdent = fnr, + ), + ), + header, + ) + + val response = restTemplate.postForEntity>(hentUrl("/api/fagsaker"), requestEntity) + val ressurs = response.body + + assertEquals(HttpStatus.CREATED, response.statusCode) + assertEquals(Ressurs.Status.SUKSESS, ressurs?.status) + } + + @Test + fun `Skal kaste feil når innlogget veileder prøver å opprette behandling gjennom test-rest-endepunkt som validerer på db-nivå`() { + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsak(FagsakRequest(personIdent = fnr)) + + val header = HttpHeaders() + header.contentType = MediaType.APPLICATION_JSON + header.setBearerAuth( + token( + mapOf( + "groups" to listOf("VEILDER"), + "name" to "Mock McMockface", + "NAVident" to "Z0000", + ), + ), + ) + val requestEntity = HttpEntity( + objectMapper.writeValueAsString(nyOrdinærBehandling(søkersIdent = fnr, fagsakId = fagsak.data!!.id)), + header, + ) + + val error = assertThrows { + restTemplate.postForEntity>( + hentUrl("/rolletilgang/test-behandlinger"), + requestEntity, + ) + } + + val ressurs: Ressurs = objectMapper.readValue(error.responseBodyAsString) + + assertEquals(HttpStatus.FORBIDDEN, error.statusCode) + assertEquals(Ressurs.Status.IKKE_TILGANG, ressurs.status) + assertEquals( + "Mock McMockface med rolle VEILEDER har ikke skrivetilgang til databasen.", + ressurs.melding, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/sikkerhet/TilgangControllerTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/sikkerhet/TilgangControllerTest.kt new file mode 100644 index 000000000..efee92590 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/sikkerhet/TilgangControllerTest.kt @@ -0,0 +1,40 @@ +package no.nav.familie.ba.sak.sikkerhet + +import io.mockk.every +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.integrasjoner.familieintegrasjoner.FamilieIntegrasjonerTilgangskontrollClient +import no.nav.familie.ba.sak.integrasjoner.pdl.PersonopplysningerService +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.familie.kontrakter.felles.tilgangskontroll.Tilgang +import no.nav.familie.util.FnrGenerator +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +class TilgangControllerTest( + @Autowired + private val tilgangController: TilgangController, + + @Autowired + private val mockPersonopplysningerService: PersonopplysningerService, + + @Autowired + private val mockFamilieIntegrasjonerTilgangskontrollClient: FamilieIntegrasjonerTilgangskontrollClient, +) : AbstractSpringIntegrationTest() { + + @Test + fun testHarTilgangTilKode6Person() { + val fnr = FnrGenerator.generer() + every { + mockPersonopplysningerService.hentAdressebeskyttelseSomSystembruker(any()) + } returns ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG + every { + mockFamilieIntegrasjonerTilgangskontrollClient.sjekkTilgangTilPersoner(listOf(fnr)) + } answers { firstArg>().map { Tilgang(it, true) } } + + val response = tilgangController.hentTilgangOgDiskresjonskode(TilgangRequestDTO(fnr)) + val tilgangDTO = response.body?.data ?: error("Fikk ikke forventet respons") + assertThat(tilgangDTO.adressebeskyttelsegradering).isEqualTo(ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG) + assertThat(tilgangDTO.saksbehandlerHarTilgang).isEqualTo(true) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkTest.kt new file mode 100644 index 000000000..8ef841f82 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/statistikk/saksstatistikk/SaksstatistikkTest.kt @@ -0,0 +1,133 @@ +package no.nav.familie.ba.sak.statistikk.saksstatistikk + +import no.nav.familie.ba.sak.common.Utils.hentPropertyFraMaven +import no.nav.familie.ba.sak.common.nyOrdinærBehandling +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakController +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakRequest +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.sikkerhet.SikkerhetContext +import no.nav.familie.ba.sak.statistikk.producer.MockKafkaProducer +import no.nav.familie.ba.sak.statistikk.producer.MockKafkaProducer.Companion.sendteMeldinger +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringRepository +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringType.BEHANDLING +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringType.SAK +import no.nav.familie.ba.sak.util.BrukerContextUtil +import no.nav.familie.eksterne.kontrakter.saksstatistikk.BehandlingDVH +import no.nav.familie.eksterne.kontrakter.saksstatistikk.SakDVH +import no.nav.familie.log.mdc.MDCConstants +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.slf4j.MDC +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.repository.findByIdOrNull + +class SaksstatistikkTest( + @Autowired + private val fagsakService: FagsakService, + + @Autowired + private val fagsakController: FagsakController, + + @Autowired + private val behandlingService: BehandlingService, + + @Autowired + private val databaseCleanupService: DatabaseCleanupService, + + @Autowired + private val saksstatistikkMellomlagringRepository: SaksstatistikkMellomlagringRepository, +) : AbstractSpringIntegrationTest() { + + private lateinit var saksstatistikkScheduler: SaksstatistikkScheduler + + @BeforeEach + fun init() { + MDC.put(MDCConstants.MDC_CALL_ID, "00001111") + BrukerContextUtil.mockBrukerContext(SikkerhetContext.SYSTEM_FORKORTELSE) + + val kafkaProducer = MockKafkaProducer(saksstatistikkMellomlagringRepository) + saksstatistikkScheduler = SaksstatistikkScheduler(saksstatistikkMellomlagringRepository, kafkaProducer) + databaseCleanupService.truncate() + } + + @AfterEach + fun tearDown() { + BrukerContextUtil.clearBrukerContext() + } + + @Test + @Tag("integration") + fun `Skal lagre saksstatistikk sak til repository og sende meldinger`() { + val fnr = randomFnr() + val fagsakId = fagsakController.hentEllerOpprettFagsak(FagsakRequest(personIdent = fnr)).body!!.data!!.id + + val mellomlagredeStatistikkHendelser = saksstatistikkMellomlagringRepository.findByTypeAndTypeId(SAK, fagsakId) + + assertEquals(1, mellomlagredeStatistikkHendelser.size) + assertEquals(SAK, mellomlagredeStatistikkHendelser.first().type) + assertNull(mellomlagredeStatistikkHendelser.first().konvertertTidspunkt) + assertNull(mellomlagredeStatistikkHendelser.first().sendtTidspunkt) + assertEquals( + hentPropertyFraMaven("familie.kontrakter.saksstatistikk"), + mellomlagredeStatistikkHendelser.first().kontraktVersjon, + ) + + val lagretJsonSomSakDVH: SakDVH = + sakstatistikkObjectMapper.readValue(mellomlagredeStatistikkHendelser.first().json, SakDVH::class.java) + + saksstatistikkScheduler.sendSaksstatistikk() + val oppdatertMellomlagretSaksstatistikkHendelse = + saksstatistikkMellomlagringRepository.findByIdOrNull(mellomlagredeStatistikkHendelser.first().id) + + assertNotNull(oppdatertMellomlagretSaksstatistikkHendelse!!.sendtTidspunkt) + assertEquals(lagretJsonSomSakDVH, sendteMeldinger["sak-$fagsakId"] as SakDVH) + } + + @Test + @Tag("integration") + fun `Skal lagre saksstatistikk behandling til repository og sende meldinger`() { + val fnr = randomFnr() + + val fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr, false) + val behandling = behandlingService.opprettBehandling( + nyOrdinærBehandling( + søkersIdent = fnr, + fagsakId = fagsak.id, + ), + ) + + behandlingService.oppdaterStatusPåBehandling(behandlingId = behandling.id, BehandlingStatus.AVSLUTTET) + + val mellomlagretBehandling = + saksstatistikkMellomlagringRepository.findByTypeAndTypeId(BEHANDLING, behandling.id) + assertEquals(2, mellomlagretBehandling.size) + assertNull(mellomlagretBehandling.first().konvertertTidspunkt) + assertNull(mellomlagretBehandling.first().sendtTidspunkt) + assertEquals( + hentPropertyFraMaven("familie.kontrakter.saksstatistikk"), + mellomlagretBehandling.first().kontraktVersjon, + ) + assertEquals("UTREDES", mellomlagretBehandling.first().jsonToBehandlingDVH().behandlingStatus) + assertEquals("AVSLUTTET", mellomlagretBehandling.last().jsonToBehandlingDVH().behandlingStatus) + + val lagretJsonSomSakDVH: BehandlingDVH = + sakstatistikkObjectMapper.readValue(mellomlagretBehandling.last().json, BehandlingDVH::class.java) + + saksstatistikkScheduler.sendSaksstatistikk() + val oppdatertMellomlagretSaksstatistikkHendelse = + saksstatistikkMellomlagringRepository.findByIdOrNull(mellomlagretBehandling.first().id) + + assertNotNull(oppdatertMellomlagretSaksstatistikkHendelse!!.sendtTidspunkt) + assertEquals(lagretJsonSomSakDVH, sendteMeldinger["behandling-${behandling.id}"] as BehandlingDVH) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/task/FerdigstillBehandlingTaskTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/task/FerdigstillBehandlingTaskTest.kt new file mode 100644 index 000000000..f6a6e035b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/task/FerdigstillBehandlingTaskTest.kt @@ -0,0 +1,251 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.common.kjørStegprosessForFGB +import no.nav.familie.ba.sak.common.lagVilkårsvurdering +import no.nav.familie.ba.sak.common.randomFnr +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.ba.sak.config.ClientMocks +import no.nav.familie.ba.sak.config.DatabaseCleanupService +import no.nav.familie.ba.sak.kjerne.autovedtak.fødselshendelse.Resultat +import no.nav.familie.ba.sak.kjerne.behandling.BehandlingService +import no.nav.familie.ba.sak.kjerne.behandling.SettPåMaskinellVentÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.SnikeIKøenService +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandling +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingKategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingRepository +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingStatus +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingType +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingUnderkategori +import no.nav.familie.ba.sak.kjerne.behandling.domene.Behandlingsresultat +import no.nav.familie.ba.sak.kjerne.behandling.domene.BehandlingÅrsak +import no.nav.familie.ba.sak.kjerne.behandling.domene.tilstand.BehandlingStegTilstand +import no.nav.familie.ba.sak.kjerne.brev.BrevmalService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakService +import no.nav.familie.ba.sak.kjerne.fagsak.FagsakStatus +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersongrunnlagService +import no.nav.familie.ba.sak.kjerne.grunnlag.personopplysninger.PersonopplysningGrunnlagRepository +import no.nav.familie.ba.sak.kjerne.personident.PersonidentService +import no.nav.familie.ba.sak.kjerne.steg.StegService +import no.nav.familie.ba.sak.kjerne.steg.StegType +import no.nav.familie.ba.sak.kjerne.vedtak.VedtakService +import no.nav.familie.ba.sak.kjerne.vedtak.vedtaksperiode.VedtaksperiodeService +import no.nav.familie.ba.sak.kjerne.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringRepository +import no.nav.familie.ba.sak.statistikk.saksstatistikk.domene.SaksstatistikkMellomlagringType +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +class FerdigstillBehandlingTaskTest : AbstractSpringIntegrationTest() { + + @Autowired + private lateinit var vedtakService: VedtakService + + @Autowired + private lateinit var fagsakService: FagsakService + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var persongrunnlagService: PersongrunnlagService + + @Autowired + lateinit var behandlingService: BehandlingService + + @Autowired + lateinit var stegService: StegService + + @Autowired + lateinit var vilkårsvurderingService: VilkårsvurderingService + + @Autowired + lateinit var databaseCleanupService: DatabaseCleanupService + + @Autowired + lateinit var personopplysningGrunnlagRepository: PersonopplysningGrunnlagRepository + + @Autowired + lateinit var saksstatistikkMellomlagringRepository: SaksstatistikkMellomlagringRepository + + @Autowired + lateinit var vedtaksperiodeService: VedtaksperiodeService + + @Autowired + lateinit var personidentService: PersonidentService + + @Autowired + lateinit var brevmalService: BrevmalService + + @Autowired + lateinit var snikeIKøenService: SnikeIKøenService + + private val fnr = randomFnr() + + @BeforeEach + fun init() { + databaseCleanupService.truncate() + } + + private fun kjørSteg(resultat: Resultat): Behandling { + val aktørId = personidentService.hentAktør(fnr) + val fnrBarn = ClientMocks.barnFnr[0] + + val behandling = kjørStegprosessForFGB( + tilSteg = if (resultat == Resultat.OPPFYLT) StegType.DISTRIBUER_VEDTAKSBREV else StegType.REGISTRERE_SØKNAD, + søkerFnr = fnr, + barnasIdenter = listOf(fnrBarn), + fagsakService = fagsakService, + vedtakService = vedtakService, + persongrunnlagService = persongrunnlagService, + vilkårsvurderingService = vilkårsvurderingService, + stegService = stegService, + vedtaksperiodeService = vedtaksperiodeService, + brevmalService = brevmalService, + ) + + return if (resultat == Resultat.IKKE_OPPFYLT) { + val vilkårsvurdering = lagVilkårsvurdering(aktørId, behandling, resultat) + + vilkårsvurderingService.lagreNyOgDeaktiverGammel(vilkårsvurdering = vilkårsvurdering) + val behandlingEtterVilkårsvurdering = stegService.håndterVilkårsvurdering(behandling) + + behandlingService.oppdaterStatusPåBehandling( + behandlingEtterVilkårsvurdering.id, + BehandlingStatus.IVERKSETTER_VEDTAK, + ) + behandlingService.leggTilStegPåBehandlingOgSettTidligereStegSomUtført( + behandlingId = behandlingEtterVilkårsvurdering.id, + steg = StegType.FERDIGSTILLE_BEHANDLING, + ) + } else { + behandling + } + } + + @Test + fun `Skal ferdigstille behandling og fagsak blir til løpende`() { + val behandling = kjørSteg(Resultat.OPPFYLT) + + val ferdigstiltBehandling = stegService.håndterFerdigstillBehandling(behandling) + + assertEquals(BehandlingStatus.AVSLUTTET, ferdigstiltBehandling.status) + assertEquals( + FagsakStatus.AVSLUTTET.name, + saksstatistikkMellomlagringRepository.findByTypeAndTypeId( + SaksstatistikkMellomlagringType.BEHANDLING, + ferdigstiltBehandling.id, + ) + .last().jsonToBehandlingDVH().behandlingStatus, + ) + + val ferdigstiltFagsak = ferdigstiltBehandling.fagsak + assertEquals(FagsakStatus.LØPENDE, ferdigstiltFagsak.status) + + assertEquals( + FagsakStatus.LØPENDE.name, + saksstatistikkMellomlagringRepository.findByTypeAndTypeId( + SaksstatistikkMellomlagringType.SAK, + ferdigstiltFagsak.id, + ) + .last().jsonToSakDVH().sakStatus, + ) + } + + @Test + fun `Skal ferdigstille behandling og sette fagsak til stanset`() { + val behandling = kjørSteg(Resultat.IKKE_OPPFYLT) + + val ferdigstiltBehandling = stegService.håndterFerdigstillBehandling(behandling) + assertEquals(BehandlingStatus.AVSLUTTET, ferdigstiltBehandling.status) + + val ferdigstiltFagsak = ferdigstiltBehandling.fagsak + assertEquals(FagsakStatus.AVSLUTTET, ferdigstiltFagsak.status) + assertEquals( + FagsakStatus.AVSLUTTET.name, + saksstatistikkMellomlagringRepository.findByTypeAndTypeId( + SaksstatistikkMellomlagringType.SAK, + ferdigstiltFagsak.id, + ) + .last().jsonToSakDVH().sakStatus, + ) + } + + @Nested + inner class BehandlingPåMaskinellVent { + + @Test + fun `skal reaktivere en behandling som er på maskinell vent`() { + val behandling1 = opprettBehandling(status = BehandlingStatus.UTREDES) + settPåMaskinellVent(behandling1) + + val behandling2 = kjørSteg(Resultat.OPPFYLT) + stegService.håndterFerdigstillBehandling(behandling2) + + assertThat(behandlingRepository.finnBehandling(behandling2.id).aktiv).isFalse() + assertThat(behandlingRepository.finnBehandling(behandling1.id).aktiv).isTrue() + } + + @Test + fun `skal reaktivere en behandling etter ferdigstilling av henlagt behandling`() { + val behandling1 = opprettBehandling(status = BehandlingStatus.UTREDES) + settPåMaskinellVent(behandling1) + + val behandling2 = kjørSteg(Resultat.OPPFYLT) + behandling2.resultat = Behandlingsresultat.HENLAGT_FEILAKTIG_OPPRETTET + stegService.håndterFerdigstillBehandling(behandling2) + + assertThat(behandlingRepository.finnBehandling(behandling2.id).aktiv).isFalse() + assertThat(behandlingRepository.finnBehandling(behandling1.id).aktiv).isTrue() + } + + @Test + fun `skal reaktivere en behandling etter ferdigstilling av henlagt behandling som har en tidligere iverksatt behandling`() { + val behandling = kjørSteg(Resultat.OPPFYLT) + stegService.håndterFerdigstillBehandling(behandling) + assertThat(behandlingRepository.finnBehandling(behandling.id).aktiv).isTrue() + + val behandling2 = opprettBehandling(status = BehandlingStatus.UTREDES) + settPåMaskinellVent(behandling2) + + val behandling3 = opprettBehandling( + status = BehandlingStatus.FATTER_VEDTAK, + resultat = Behandlingsresultat.HENLAGT_FEILAKTIG_OPPRETTET, + ) + stegService.håndterFerdigstillBehandling(behandling3) + + assertThat(behandlingRepository.finnBehandling(behandling2.id).aktiv).isTrue() + } + + private fun opprettBehandling( + status: BehandlingStatus = BehandlingStatus.IVERKSETTER_VEDTAK, + resultat: Behandlingsresultat = Behandlingsresultat.INNVILGET, + ): Behandling { + val behandling = Behandling( + fagsak = fagsakService.hentEllerOpprettFagsakForPersonIdent(fnr), + opprettetÅrsak = BehandlingÅrsak.NYE_OPPLYSNINGER, + type = BehandlingType.REVURDERING, + kategori = BehandlingKategori.NASJONAL, + underkategori = BehandlingUnderkategori.ORDINÆR, + status = status, + resultat = resultat, + ).initBehandlingStegTilstand() + val ferdigstillSteg = BehandlingStegTilstand( + behandling = behandling, + behandlingSteg = StegType.FERDIGSTILLE_BEHANDLING, + ) + behandling.behandlingStegTilstand.add(ferdigstillSteg) + return behandlingService.lagreNyOgDeaktiverGammelBehandling(behandling) + } + + private fun settPåMaskinellVent(behandling: Behandling) { + snikeIKøenService.settAktivBehandlingTilPåMaskinellVent( + behandling.id, + SettPåMaskinellVentÅrsak.SATSENDRING, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/task/TaskTest.kt b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/task/TaskTest.kt new file mode 100644 index 000000000..ba82ec8fe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/integrasjonstester/kotlin/no/nav/familie/ba/sak/task/TaskTest.kt @@ -0,0 +1,60 @@ +package no.nav.familie.ba.sak.task + +import no.nav.familie.ba.sak.config.AbstractSpringIntegrationTest +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.springframework.aop.framework.AopProxyUtils +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.core.annotation.AnnotationUtils +import java.util.stream.Collectors + +@Tag("integration") +class TaskTest : AbstractSpringIntegrationTest() { + + @Autowired + lateinit var tasker: List + + @Test + fun `Tasker skal ha unikt navn`() { + val taskTyper: List = tasker.stream() + .map { task: AsyncTaskStep -> + finnAnnotasjon(task) + } + .map { it?.taskStepType } + .collect( + Collectors.toList(), + ) + + Assertions.assertEquals(tasker.size, taskTyper.distinct().size) + } + + @Test + fun `Tasker skal ha annotasjon`() { + Assertions.assertEquals( + false, + tasker.stream().anyMatch { + harIkkePåkrevdAnnotasjon( + it!!, + ) + }, + ) + } + + private fun harIkkePåkrevdAnnotasjon(it: AsyncTaskStep): Boolean { + return !AnnotationUtils.isAnnotationDeclaredLocally( + TaskStepBeskrivelse::class.java, + it.javaClass, + ) + } + + private fun finnAnnotasjon(task: AsyncTaskStep): TaskStepBeskrivelse? { + val aClass = AopProxyUtils.ultimateTargetClass(task) + return AnnotationUtils.findAnnotation( + aClass, + TaskStepBeskrivelse::class.java, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/application-dev-postgres-preprod.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/application-dev-postgres-preprod.yaml new file mode 100644 index 000000000..ff3b56d7c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/application-dev-postgres-preprod.yaml @@ -0,0 +1,205 @@ +no.nav.security.jwt: + issuer.azuread: + discoveryurl: https://login.microsoftonline.com/navq.onmicrosoft.com/v2.0/.well-known/openid-configuration + accepted_audience: ${AZURE_APP_CLIENT_ID} + cookie_name: azure_token + client: + registration: + familie-integrasjoner-onbehalfof: + resource-url: ${FAMILIE_INTEGRASJONER_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-integrasjoner-clientcredentials: + resource-url: ${FAMILIE_INTEGRASJONER_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl-onbehalfof: + resource-url: ${PDL_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${PDL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl-clientcredentials: + resource-url: ${PDL_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${PDL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag-clientcredentials: + resource-url: ${FAMILIE_OPPDRAG_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-tilbake-onbehalfof: + resource-url: ${FAMILIE_TILBAKE_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_TILBAKE_API_URL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-tilbake-clientcredentials: + resource-url: ${FAMILIE_TILBAKE_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${FAMILIE_TILBAKE_API_URL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-onbehalfof: + resource-url: ${FAMILIE_BA_INFOTRYGD_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_BA_INFOTRYGD_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-feed-clientcredentials: + resource-url: ${FAMILIE_BA_INFOTRYGD_FEED_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${FAMILIE_BA_INFOTRYGD_FEED_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-clientcredentials: + resource-url: ${FAMILIE_BA_INFOTRYGD_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${FAMILIE_BA_INFOTRYGD_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag-onbehalfof: + resource-url: ${FAMILIE_OPPDRAG_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ef-sak-onbehalfof: + resource-url: ${FAMILIE_EF_SAK_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_EF_SAK_API_URL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ef-sak-clientcredentials: + resource-url: ${FAMILIE_EF_SAK_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${FAMILIE_EF_SAK_API_URL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-statistikk-clientcredentials: + resource-url: ${FAMILIE_STATISTIKK_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: api://dev-fss.teamfamilie.familie-ba-statistikk/.default + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + +prosessering.fixedDelayString.in.milliseconds: 15000 +prosessering.rolle: "928636f4-fd0d-4149-978e-a6fb68bb19de" # 0000-GA-STDAPPS + +credential: + username: "srvfamilie-ba-sak" + password: "not-a-real-password" + +logging: + config: "classpath:logback-test.xml" +sentry.environment: local +sentry.logging.enabled: false + +funksjonsbrytere: + enabled: false + unleash: + cluster: localhost + applicationName: familie-ba-sak + kafka: + producer: + enabled: false + +spring: + datasource: + url: jdbc:postgresql://localhost:5432/familie-ba-sak + password: test + username: postgres + flyway: + enabled: true + jpa: + show-sql: false + properties: + hibernate: + format_sql=false + hibernate: + ddl-auto: none + + + +SANITY_DATASET: "ba-brev" + +FAMILIE_INTEGRASJONER_API_URL: https://familie-integrasjoner.dev.intern.nav.no/api +FAMILIE_INTEGRASJONER_SCOPE: api://dev-fss.teamfamilie.familie-integrasjoner/.default +PDL_URL: https://pdl-api.dev.intern.nav.no/ +PDL_SCOPE: api://dev-fss.pdl.pdl-api/.default +FAMILIE_OPPDRAG_API_URL: https://familie-oppdrag.dev.intern.nav.no/api +FAMILIE_OPPDRAG_SCOPE: api://dev-fss.teamfamilie.familie-oppdrag/.default +FAMILIE_BREV_API_URL: https://familie-brev.intern.dev.nav.no +FAMILIE_TILBAKE_API_URL: https://familie-tilbake.intern.dev.nav.no +FAMILIE_TILBAKE_API_URL_SCOPE: api://dev-gcp.teamfamilie.familie-tilbake/.default +FAMILIE_KLAGE_URL: https://familie-klage-backend.intern.dev.nav.no +FAMILIE_BA_SAK_API_URL: http://localhost:8086/api +FAMILIE_BA_INFOTRYGD_API_URL: https://familie-ba-infotrygd.dev.intern.nav.no +FAMILIE_BA_INFOTRYGD_SCOPE: api://dev-fss.teamfamilie.familie-ba-infotrygd/.default +FAMILIE_STATISTIKK_URL: https://familie-ba-statistikk.dev.intern.nav.no + +FAMILIE_EF_SAK_API_URL: https://familie-ef-sak.intern.dev.nav.no +FAMILIE_EF_SAK_API_URL_SCOPE: api://dev-gcp.teamfamilie.familie-ef-sak/.default +FAMILIE_BA_INFOTRYGD_FEED_API_URL: https://familie-ba-infotrygd-feed.dev.intern.nav.no +FAMILIE_BA_INFOTRYGD_FEED_SCOPE: api://dev-fss.teamfamilie.familie-ba-infotrygd.feed/.default + + +CREDENTIAL_USERNAME: not-a-real-srvuser +CREDENTIAL_PASSWORD: not-a-real-pw + +KAFKA_BROKERS: http://localhost:9092 + +retry.backoff.delay: 5 +NAIS_APP_NAME: familie-ba-sak + +AZURE_OPENID_CONFIG_TOKEN_ENDPOINT: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token +DEPLOY_ENV: dev \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/application-integrasjonstest.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/application-integrasjonstest.yaml new file mode 100644 index 000000000..4b9623abd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/application-integrasjonstest.yaml @@ -0,0 +1,17 @@ +spring: + flyway: + locations: filesystem:./src/test/resources/db/migration-tests,filesystem:./src/main/resources/db/migration + +AZURE_OPENID_CONFIG_TOKEN_ENDPOINT: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token +AZURE_APP_CLIENT_ID: dummy + +# Kreves for at unleash mock skal fungere: +UNLEASH_SERVER_API_URL: http://dummy/api/ +UNLEASH_SERVER_API_TOKEN: dummy-token +NAIS_APP_NAME: familie-ba-sak + +# Disabler unleash her for å unngå feilmeldinger tilknyttet oppkobling når vi uansett mocker alle unleash-kall i testene. +unleash: + enabled: false + +prosessering.rolle: "928636f4-fd0d-4149-978e-a6fb68bb19de" # 0000-GA-STDAPPS \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/application-postgres.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/application-postgres.yaml new file mode 100644 index 000000000..ce72eae9a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/application-postgres.yaml @@ -0,0 +1,186 @@ +no.nav.security.jwt: + issuer.azuread: + discoveryurl: https://login.microsoftonline.com/navq.onmicrosoft.com/v2.0/.well-known/openid-configuration + accepted_audience: ${BA_SAK_CLIENT_ID} + cookie_name: azure_token + client: + registration: + familie-integrasjoner-onbehalfof: + resource-url: ${FAMILIE_INTEGRASJONER_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-integrasjoner-clientcredentials: + resource-url: ${FAMILIE_INTEGRASJONER_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl-onbehalfof: + resource-url: ${PDL_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${PDL_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl-clientcredentials: + resource-url: ${PDL_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${PDL_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag-clientcredentials: + resource-url: ${FAMILIE_OPPDRAG_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-tilbake-onbehalfof: + resource-url: ${FAMILIE_TILBAKE_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_TILBAKE_API_URL_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-tilbake-clientcredentials: + resource-url: ${FAMILIE_TILBAKE_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${FAMILIE_TILBAKE_API_URL_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-onbehalfof: + resource-url: ${FAMILIE_BA_INFOTRYGD_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_BA_INFOTRYGD_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-feed-clientcredentials: + resource-url: ${FAMILIE_BA_INFOTRYGD_FEED_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${FAMILIE_BA_INFOTRYGD_FEED_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ba-infotrygd-clientcredentials: + resource-url: ${FAMILIE_BA_INFOTRYGD_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${FAMILIE_BA_INFOTRYGD_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag-onbehalfof: + resource-url: ${FAMILIE_OPPDRAG_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ef-sak-onbehalfof: + resource-url: ${FAMILIE_EF_SAK_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_EF_SAK_API_URL_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-ef-sak-clientcredentials: + resource-url: ${FAMILIE_EF_SAK_API_URL} + token-endpoint-url: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + grant-type: client_credentials + scope: ${FAMILIE_EF_SAK_API_URL_SCOPE} + authentication: + client-id: ${BA_SAK_CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-auth-method: client_secret_basic + +prosessering.fixedDelayString.in.milliseconds: 2000 +prosessering.rolle: "928636f4-fd0d-4149-978e-a6fb68bb19de" # 0000-GA-STDAPPS + +credential: + username: "srvfamilie-ba-sak" + password: "not-a-real-password" + +logging: + config: "classpath:logback-test.xml" +sentry.environment: local +sentry.logging.enabled: false + +funksjonsbrytere: + enabled: false + unleash: + uri: http://dummy/api/ + cluster: localhost + applicationName: familie-ba-sak + kafka: + producer: + enabled: false + +spring: + datasource: + url: jdbc:postgresql://localhost:5432/familie-ba-sak + password: test + username: postgres + flyway: + enabled: true + jpa: + show-sql: false + properties: + hibernate: + format_sql=false + hibernate: + ddl-auto: none + + +FAMILIE_INTEGRASJONER_SCOPE: "dummy" +SANITY_DATASET: "ba-test" + +FAMILIE_BREV_API_URL: http://localhost:8001 +FAMILIE_OPPDRAG_API_URL: http://localhost:8087/api +FAMILIE_INTEGRASJONER_API_URL: http://localhost:28085/api +FAMILIE_TILBAKE_API_URL: http://localhost:8030/api +FAMILIE_BA_SAK_API_URL: http://localhost:8086/api +FAMILIE_BA_INFOTRYGD_API_URL: http://localhost:28085 +FAMILIE_STATISTIKK_URL: dummy +CREDENTIAL_USERNAME: not-a-real-srvuser +CREDENTIAL_PASSWORD: not-a-real-pw + +KAFKA_BROKERS: http://localhost:9092 + +retry.backoff.delay: 5 +NAIS_APP_NAME: familie-ba-sak +CRON_FAGSAKSTATUS_SCHEDULER: "0 0/10 * ? * *" + +AZURE_OPENID_CONFIG_TOKEN_ENDPOINT: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token + +UNLEASH_SERVER_API_URL: http://dummy/api/ +UNLEASH_SERVER_API_TOKEN: dummy \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/beregning/vilk\303\245r_til_tilkjent_ytelse/s\303\270ker_med_ett_barn_inntil_tre_perioder.csv" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/beregning/vilk\303\245r_til_tilkjent_ytelse/s\303\270ker_med_ett_barn_inntil_tre_perioder.csv" new file mode 100644 index 000000000..cee44ea4b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/beregning/vilk\303\245r_til_tilkjent_ytelse/s\303\270ker_med_ett_barn_inntil_tre_perioder.csv" @@ -0,0 +1,11 @@ +sakType;søkerPeriode1;søkerVilkår1;søkerPeriode2;søkerVilkår2;barn1Periode1;barn1Vilkår1;barn1Andel1Beløp;barn1Andel1Periode;barn1Andel1Type;barn1Andel2Beløp;barn1Andel2Periode;barn1Andel2Type;barn1Andel3Beløp;barn1Andel3Periode;barn1Andel3Type;deltBosted +NASJONAL;;;;;;;;;;;;;;;; +NASJONAL;;;;;2021-09 – 2034-05;Under 18;;;;;;;;;; +NASJONAL;2020-09 – 2022-05;Gift;;;;;;;;;;;;;; +NASJONAL;2020-09 – 2022-05;Bosatt, opphold;;;2021-09 – 2034-05;Gift, bosatt, opphold, under 18, bor med søker;1654;2021-10-01 - 2021-12-31;ORDINÆR_BARNETRYGD;1676;2022-01-01 - 2022-05-31;ORDINÆR_BARNETRYGD;;;;;; +NASJONAL;2021-12 – 2022-05;Bosatt, opphold;2022-06 – 2024-11;Bosatt, opphold;2021-09 – 2039-09;Gift, bosatt, opphold, under 18, bor med søker;1676;2022-01-01 – 2024-11-30;ORDINÆR_BARNETRYGD;;;;;;; +NASJONAL;2020-09 – 2034-05;Bosatt, opphold;;;2021-09 – 2039-09;Gift, bosatt, opphold, under 18, bor med søker;1654;2021-10-01 – 2021-12-31;ORDINÆR_BARNETRYGD;1676;2022-01-01 – 2027-08-31;ORDINÆR_BARNETRYGD;1054;2027-09-01 - 2034-05-31;ORDINÆR_BARNETRYGD; +NASJONAL;2020-09 – 2022-05;Bosatt, opphold;2022-06 – 2024-11;Bosatt, opphold;2021-09 – 2039-09;Gift, bosatt, opphold, under 18, bor med søker;1654;2021-10-01 – 2021-12-31;ORDINÆR_BARNETRYGD;1676;2022-01-01 – 2024-11-30;ORDINÆR_BARNETRYGD;;;;true +NASJONAL;2020-09 – 2034-05;Bosatt, opphold;;;2021-09 – 2039-09;Gift, bosatt, opphold, under 18, bor med søker;1654;2021-10-01 – 2021-12-31;ORDINÆR_BARNETRYGD;1676;2022-01-01 – 2027-08-31;ORDINÆR_BARNETRYGD;1054;2027-09-01 - 2034-05-31;ORDINÆR_BARNETRYGD;true +EØS;2020-09 – 2034-05;Bosatt;;;2020-09 – 2034-05;Gift, bosatt, under 18, bor med søker;;;;;;;;;; +EØS;2021-09 – 2034-05;Bosatt, opphold;;;2021-09 – 2034-04;Gift, bosatt, opphold, under 18, bor med søker;1654;2021-10-01 - 2021-12-31;ORDINÆR_BARNETRYGD;1676;2022-01-01 - 2027-08-31;ORDINÆR_BARNETRYGD;1054;2027-09-01 - 2034-04-30;ORDINÆR_BARNETRYGD; \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/beregning/vilk\303\245r_til_tilkjent_ytelse/s\303\270ker_med_to_barn_inntil_to_perioder.csv" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/beregning/vilk\303\245r_til_tilkjent_ytelse/s\303\270ker_med_to_barn_inntil_to_perioder.csv" new file mode 100644 index 000000000..9d4da5d39 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/beregning/vilk\303\245r_til_tilkjent_ytelse/s\303\270ker_med_to_barn_inntil_to_perioder.csv" @@ -0,0 +1,2 @@ +søkerPeriode1;søkerVilkår1;søkerPeriode2;søkerVilkår2;barn1Periode1;barn1Vilkår1;barn2Periode1;barn2Vilkår1;barn1Andel1Beløp;barn1Andel1Periode;barn1Andel1Type;barn1Andel2Beløp;barn1Andel2Periode;barn1Andel2Type;barn1Andel3Beløp;barn1Andel3Periode;barn1Andel3Type;barn2Andel1Beløp;barn2Andel1Periode;barn2Andel1Type;barn2Andel2Beløp;barn2Andel2Periode;barn2Andel2Type +2010-10 - ;Bosatt, opphold;;;2021-08 - 2038-01;Gift, bosatt, opphold, under 18, bor med søker;2022-04 - 2040-03;Gift, bosatt, opphold, under 18, bor med søker;1654;2021-09-01 - 2021-12-31;ORDINÆR_BARNETRYGD;1676;2022-01-01 - 2026-01-31;ORDINÆR_BARNETRYGD;1054;2026-02-01 - 2038-01-31;ORDINÆR_BARNETRYGD;1676;2022-05-01 - 2028-03-31;ORDINÆR_BARNETRYGD;1054;2028-04-01 - 2040-03-31;ORDINÆR_BARNETRYGD \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/beregning/vilk\303\245r_til_tilkjent_ytelse/s\303\270ker_med_utvidet_og_ett_barn_inntil_to_perioder.csv" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/beregning/vilk\303\245r_til_tilkjent_ytelse/s\303\270ker_med_utvidet_og_ett_barn_inntil_to_perioder.csv" new file mode 100644 index 000000000..4d0cf877a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/beregning/vilk\303\245r_til_tilkjent_ytelse/s\303\270ker_med_utvidet_og_ett_barn_inntil_to_perioder.csv" @@ -0,0 +1,3 @@ +sakType;søkerPeriode1;søkerVilkår1;søkerAndel1Beløp;søkerAndel1Periode;søkerAndel1Type;småbarnstilleggPeriode;barn1Periode1;barn1Vilkår1;barn1Andel1Beløp;barn1Andel1Periode;barn1Andel1Type +NASJONAL;2020-09 – 2022-05;Bosatt, opphold, utvidet;1054;2022-01 – 2022-05;UTVIDET_BARNETRYGD;2022-01 – 2022-05;2021-12 – 2034-05;Gift, bosatt, opphold, under 18, bor med søker;1676;2022-01-01 - 2022-05-31;ORDINÆR_BARNETRYGD +NASJONAL;2020-09 – 2022-05;Bosatt, opphold, utvidet;1054;2022-01 – 2022-05;UTVIDET_BARNETRYGD;;2021-12 – 2034-05;Gift, bosatt, opphold, under 18, bor med søker;1676;2022-01-01 - 2022-05-31;ORDINÆR_BARNETRYGD diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/bootstrap-postgres.yaml b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/bootstrap-postgres.yaml new file mode 100644 index 000000000..e702582f2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/bootstrap-postgres.yaml @@ -0,0 +1,6 @@ +spring: + cloud: + discovery: + client: + composite-indicator: + enabled: false \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AUTOBREV_18_\303\205R.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AUTOBREV_18_\303\205R.json" new file mode 100644 index 000000000..e7581a901 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AUTOBREV_18_\303\205R.json" @@ -0,0 +1,79 @@ +{ + "beskrivelse": "Reduksjon autobrev 18 år", + "fom": "2021-12-01", + "tom": null, + "vedtaksperiodetype": "FORTSATT_INNVILGET", + "begrunnelser": [ + "Standardbegrunnelse$REDUKSJON_UNDER_18_ÅR_AUTOVEDTAK" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2003-12-10", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "UNDER_18_ÅR", + "resultat": "OPPFYLT", + "periodeFom": "2003-12-10", + "periodeTom": "2021-12-10", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "2011-09-14", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "1973-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "Fra 1. desember 2021 får du:", + "tom": "", + "belop": 1054, + "antallBarn": 1, + "barnasFodselsdager": "14.09.11", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "10.12.03", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "november 2021", + "maalform": "bokmaal", + "apiNavn": "reduksjonAutovedtakBarn18Aar", + "belop": 0 + } + ], + "type": "fortsattInnvilget" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AUTOBREV_6_\303\205R.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AUTOBREV_6_\303\205R.json" new file mode 100644 index 000000000..04eddd109 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AUTOBREV_6_\303\205R.json" @@ -0,0 +1,61 @@ +{ + "beskrivelse": "Reduksjon autobrev 6 år", + "fom": "2021-09-01", + "tom": null, + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$REDUKSJON_UNDER_6_ÅR_AUTOVEDTAK" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2015-09-03", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1354, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": "", + "belop": 1354, + "antallBarn": 1, + "barnasFodselsdager": "03.09.15", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "03.09.15", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "03.09.15", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 1, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "reduksjonAutovedtakBarn6Aar", + "belop": 1354 + } + ], + "type": "innvilgelse" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG.json new file mode 100644 index 000000000..b89242e61 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG.json @@ -0,0 +1,66 @@ +{ + "beskrivelse": "Avslag 1 barn bor ikke hos søker", + "fom": "2021-01-01", + "tom": null, + "vedtaksperiodetype": "AVSLAG", + "begrunnelser": [ + "Standardbegrunnelse$AVSLAG_BOR_HOS_SØKER" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2013-12-10", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "resultat": "IKKE_OPPFYLT", + "periodeFom": "2021-01-01", + "periodeTom": null, + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": true, + "standardbegrunnelser": [ + "Standardbegrunnelse$AVSLAG_BOR_HOS_SØKER" + ] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "1973-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. januar 2021", + "tom": "", + "belop": 0, + "antallBarn": 0, + "barnasFodselsdager": "", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "10.12.13", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "januar 2021", + "maalform": "bokmaal", + "apiNavn": "avslagBorHosSoker", + "belop": 0 + } + ], + "type": "avslag" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_FOR_S\303\230KER_OG_ETT_BARN.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_FOR_S\303\230KER_OG_ETT_BARN.json" new file mode 100644 index 000000000..2b730a21c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_FOR_S\303\230KER_OG_ETT_BARN.json" @@ -0,0 +1,102 @@ +{ + "beskrivelse": "Avslag. Fire barn, men kun søker og ett barn som har utgjørende vilkår", + "fom": "2021-10-01", + "tom": null, + "vedtaksperiodetype": "AVSLAG", + "begrunnelser": [ + "Standardbegrunnelse$AVSLAG_BOSATT_I_RIKET" + ], + "fritekster": [], + "personerPåBehandling": [ + { + "fødselsdato": "2009-04-15", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOSATT_I_RIKET", + "periodeFom": "2021-10-10", + "periodeTom": null, + "resultat": "IKKE_OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": true, + "standardbegrunnelser": [ + "Standardbegrunnelse$AVSLAG_BOSATT_I_RIKET" + ] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "1969-07-29", + "type": "SØKER", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOSATT_I_RIKET", + "periodeFom": "2021-10-10", + "periodeTom": null, + "resultat": "IKKE_OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": true, + "standardbegrunnelser": [ + "Standardbegrunnelse$AVSLAG_BOSATT_I_RIKET" + ] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "2003-03-16", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "2010-09-06", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "2004-03-10", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "forventetOutput": { + "fom": "1. oktober 2021", + "tom": "", + "belop": 0, + "antallBarn": 0, + "barnasFodselsdager": "", + "begrunnelser": [ + { + "gjelderSoker": true, + "barnasFodselsdatoer": "16.03.03, 10.03.04, 15.04.09 og 06.09.10", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 0, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "oktober 2021", + "maalform": "bokmaal", + "apiNavn": "avslagBosattIRiket", + "belop": null + } + ], + "type": "avslag" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_FRITEKST.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_FRITEKST.json new file mode 100644 index 000000000..38e6b9700 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_FRITEKST.json @@ -0,0 +1,65 @@ +{ + "beskrivelse": "Avslag på behandling med 1 barn med fritekst.", + "fom": "2021-09-01", + "tom": null, + "vedtaksperiodetype": "AVSLAG", + "begrunnelser": [], + "fritekster": [ + "Du får ikke barnetrygd fordi test 1", + "Du får ikke barnetrygd fordi test 2", + "Du får ikke barnetrygd fordi test 3" + ], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2020-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [ + { + "vilkårType": "LOVLIG_OPPHOLD", + "resultat": "IKKE_OPPFYLT", + "periodeFom": "1993-08-10", + "periodeTom": null, + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": "", + "belop": 0, + "antallBarn": 0, + "barnasFodselsdager": "", + "begrunnelser": [ + { + "fritekst": "Du får ikke barnetrygd fordi test 1", + "type": "fritekst" + }, + { + "fritekst": "Du får ikke barnetrygd fordi test 2", + "type": "fritekst" + }, + { + "fritekst": "Du får ikke barnetrygd fordi test 3", + "type": "fritekst" + } + ], + "type": "avslag" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_MANGLER_OPPLYSNINGER.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_MANGLER_OPPLYSNINGER.json new file mode 100644 index 000000000..d48676228 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_MANGLER_OPPLYSNINGER.json @@ -0,0 +1,91 @@ +{ + "beskrivelse": "Avslag 1 barn. Søkt om utvidet, men bor med ektefelle. Bor ikke med barn.", + "fom": null, + "tom": null, + "vedtaksperiodetype": "AVSLAG", + "begrunnelser": [ + "Standardbegrunnelse$AVSLAG_IKKE_FLYTTET_FRA_EKTEFELLE", + "Standardbegrunnelse$AVSLAG_FORELDRENE_BOR_SAMMEN" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": true, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2010-12-13", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "periodeFom": null, + "periodeTom": null, + "resultat": "IKKE_OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": true, + "standardbegrunnelser": [ + "Standardbegrunnelse$AVSLAG_IKKE_FLYTTET_FRA_EKTEFELLE", + "Standardbegrunnelse$AVSLAG_FORELDRENE_BOR_SAMMEN" + ] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "1980-01-01", + "type": "SØKER", + "overstyrteVilkårresultater": [ + { + "vilkårType": "UTVIDET_BARNETRYGD", + "periodeFom": null, + "periodeTom": null, + "resultat": "IKKE_OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": true, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "", + "tom": "", + "belop": 0, + "antallBarn": 0, + "barnasFodselsdager": "", + "begrunnelser": [ + { + "gjelderSoker": true, + "barnasFodselsdatoer": "13.12.10", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 0, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": null, + "maalform": "bokmaal", + "apiNavn": "avslagIkkeFlyttetFraEktefelle", + "belop": 0 + }, + { + "gjelderSoker": false, + "barnasFodselsdatoer": "13.12.10", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": null, + "maalform": "bokmaal", + "apiNavn": "avslagForeldreneBorSammen", + "belop": 0 + } + ], + "type": "avslagUtenPeriode" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_UREGISTRERT_BARN.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_UREGISTRERT_BARN.json new file mode 100644 index 000000000..4d2ae7a1b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_UREGISTRERT_BARN.json @@ -0,0 +1,77 @@ +{ + "beskrivelse": "Avslag uregistrert barn", + "fom": null, + "tom": null, + "vedtaksperiodetype": "AVSLAG", + "begrunnelser": [ + "Standardbegrunnelse$AVSLAG_UREGISTRERT_BARN" + ], + "fritekster": [ + "Du har fått avslag fordi barnet ikke er registrert på samme adresse som deg og barnet ikke har fått norsk fødselsnummer ennå." + ], + "personerPåBehandling": [ + { + "fødselsdato": "1992-03-15", + "type": "SØKER", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOSATT_I_RIKET", + "periodeFom": "2021-03-11", + "periodeTom": null, + "resultat": "OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + }, + { + "vilkårType": "LOVLIG_OPPHOLD", + "periodeFom": "2021-03-11", + "periodeTom": null, + "resultat": "OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "uregistrerteBarn": [ + { + "personIdent": "", + "navn": "Barn Barnesen", + "fødselsdato": "2017-12-27" + } + ], + "erFørsteVedtaksperiodePåFagsak": true, + "brevMålform": "NB", + "forventetOutput": { + "fom": "", + "tom": "", + "barnasFodselsdager": "", + "antallBarn": 0, + "belop": 0, + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "27.12.17", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": null, + "maalform": "bokmaal", + "apiNavn": "avslagUregistrertBarn", + "belop": 0 + }, + { + "fritekst": "Du har fått avslag fordi barnet ikke er registrert på samme adresse som deg og barnet ikke har fått norsk fødselsnummer ennå.", + "type": "fritekst" + } + ], + "type": "avslagUtenPeriode" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_UTVIDET_INGEN_PERIODE.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_UTVIDET_INGEN_PERIODE.json new file mode 100644 index 000000000..dd2751333 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/AVSLAG_UTVIDET_INGEN_PERIODE.json @@ -0,0 +1,92 @@ +{ + "beskrivelse": "Avslag uten periode på utvidet barnetrygd med endret utbetaling", + "fom": null, + "tom": null, + "vedtaksperiodetype": "AVSLAG", + "begrunnelser": [ + "Standardbegrunnelse$AVSLAG_IKKE_MEKLINGSATTEST" + ], + "fritekster": [], + "personerPåBehandling": [ + { + "fødselsdato": "2017-09-26", + "type": "BARN", + "overstyrteVilkårresultater": [ + ], + "andreVurderinger": [], + "endredeUtbetalinger": [ + { + "periode": { + "fom": "2021-09", + "tom": "2022-01" + }, + "årsak": "DELT_BOSTED" + } + ], + "utbetalinger": [] + }, + { + "fødselsdato": "2013-06-02", + "type": "BARN", + "overstyrteVilkårresultater": [ + ], + "andreVurderinger": [], + "endredeUtbetalinger": [ + { + "periode": { + "fom": "2021-09", + "tom": "2022-01" + }, + "årsak": "DELT_BOSTED" + } + ], + "utbetalinger": [] + }, + { + "fødselsdato": "1993-02-18", + "type": "SØKER", + "overstyrteVilkårresultater": [ + { + "vilkårType": "UTVIDET_BARNETRYGD", + "periodeFom": null, + "periodeTom": null, + "resultat": "IKKE_OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": true, + "standardbegrunnelser": [ + "Standardbegrunnelse$AVSLAG_IKKE_MEKLINGSATTEST" + ] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "forventetOutput": { + "fom": "", + "tom": "", + "belop": 0, + "antallBarn": 0, + "barnasFodselsdager": "", + "begrunnelser": [ + { + "gjelderSoker": true, + "barnasFodselsdatoer": "02.06.13 og 26.09.17", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 0, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": null, + "maalform": "bokmaal", + "apiNavn": "avslagIkkeMeklingsattest", + "belop": 0 + } + ], + "type": "avslagUtenPeriode" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ENDRET_UTBETALING.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ENDRET_UTBETALING.json new file mode 100644 index 000000000..6c2a0a0ee --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ENDRET_UTBETALING.json @@ -0,0 +1,83 @@ +{ + "beskrivelse": "Innvilget 1 barn bor med søker fra august 2021 på behandling med 1 barn", + "fom": "2021-09-01", + "tom": "2021-12-31", + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_INGEN_UTBETALING_NY" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2019-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "resultat": "OPPFYLT", + "periodeFom": "2021-05-01", + "periodeTom": null, + "utdypendeVilkårsvurderinger": [ + "DELT_BOSTED" + ], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [ + { + "periode": { + "fom": "2021-09", + "tom": "2021-12" + }, + "årsak": "DELT_BOSTED", + "søknadstidspunkt": "2021-01-31" + } + ], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 0, + "erPåvirketAvEndring": true, + "prosent": 0 + } + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": " til 31. desember 2021", + "belop": 0, + "antallBarn": 1, + "barnasFodselsdager": "20.01.19", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "20.01.19", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "20.01.19", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 1, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "endretUtbetalingDeltBostedIngenUtbetaling", + "belop": 0, + "soknadstidspunkt": "31.01.21" + } + ], + "type": "innvilgelseIngenUtbetaling" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ENDRET_UTBETALING_DELT_BOSTED_AVTALEDATO.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ENDRET_UTBETALING_DELT_BOSTED_AVTALEDATO.json new file mode 100644 index 000000000..4f01f234f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ENDRET_UTBETALING_DELT_BOSTED_AVTALEDATO.json @@ -0,0 +1,86 @@ +{ + "beskrivelse": "Innvilget 1 barn med endringsperiode", + "fom": "2021-09-01", + "tom": "2021-12-31", + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_ENDRET_UTBETALING" + ], + "fritekster": [], + "utvidetScenario": "IKKE_UTVIDET_YTELSE", + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2019-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "resultat": "OPPFYLT", + "periodeFom": "2021-05-01", + "periodeTom": null, + "utdypendeVilkårsvurderinger": [ + "DELT_BOSTED" + ], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [ + { + "periode": { + "fom": "2021-09", + "tom": "2021-12" + }, + "årsak": "DELT_BOSTED", + "søknadstidspunkt": "2021-01-31", + "avtaletidspunktDeltBosted": "2021-04-02" + } + ], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 0, + "erPåvirketAvEndring": true, + "prosent": 0 + } + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": " til 31. desember 2021", + "belop": 0, + "antallBarn": 1, + "barnasFodselsdager": "20.01.19", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "20.01.19", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "20.01.19", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 1, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "endretUtbetalingDeltBostedEndretUtbetaling", + "belop": 0, + "soknadstidspunkt": "31.01.21", + "avtaletidspunktDeltBosted": "02.04.21" + } + ], + "type": "innvilgelseIngenUtbetaling" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ENDRET_UTBETALING_DELT_BOSTED_FLERE_AVTALEDATOER.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ENDRET_UTBETALING_DELT_BOSTED_FLERE_AVTALEDATOER.json new file mode 100644 index 000000000..58531144f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ENDRET_UTBETALING_DELT_BOSTED_FLERE_AVTALEDATOER.json @@ -0,0 +1,138 @@ +{ + "beskrivelse": "Innvilget 2 barn med endringsperioder med ulike avtaledatoer", + "fom": "2021-09-01", + "tom": "2021-12-31", + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_ENDRET_UTBETALING" + ], + "fritekster": [], + "utvidetScenario": "IKKE_UTVIDET_YTELSE", + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2019-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "resultat": "OPPFYLT", + "periodeFom": "2021-05-01", + "periodeTom": null, + "utdypendeVilkårsvurderinger": [ + "DELT_BOSTED" + ], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [ + { + "periode": { + "fom": "2021-09", + "tom": "2021-12" + }, + "årsak": "DELT_BOSTED", + "søknadstidspunkt": "2021-01-31", + "avtaletidspunktDeltBosted": "2021-04-02" + } + ], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 0, + "erPåvirketAvEndring": true, + "prosent": 0 + } + ] + }, + { + "fødselsdato": "2017-06-14", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "resultat": "OPPFYLT", + "periodeFom": "2021-05-01", + "periodeTom": null, + "utdypendeVilkårsvurderinger": [ + "DELT_BOSTED" + ], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [ + { + "periode": { + "fom": "2021-09", + "tom": "2021-12" + }, + "årsak": "DELT_BOSTED", + "søknadstidspunkt": "2021-01-31", + "avtaletidspunktDeltBosted": "2021-10-07" + } + ], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 0, + "erPåvirketAvEndring": true, + "prosent": 0 + } + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": " til 31. desember 2021", + "belop": 0, + "antallBarn": 2, + "barnasFodselsdager": "14.06.17 og 20.01.19", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "20.01.19", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "20.01.19", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 1, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "endretUtbetalingDeltBostedEndretUtbetaling", + "belop": 0, + "soknadstidspunkt": "31.01.21", + "avtaletidspunktDeltBosted": "02.04.21" + }, + { + "gjelderSoker": false, + "barnasFodselsdatoer": "14.06.17", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "14.06.17", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 1, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "endretUtbetalingDeltBostedEndretUtbetaling", + "belop": 0, + "soknadstidspunkt": "31.01.21", + "avtaletidspunktDeltBosted": "07.10.21" + } + ], + "type": "innvilgelseIngenUtbetaling" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ENDRET_UTBETALING_ETTERBETALING.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ENDRET_UTBETALING_ETTERBETALING.json new file mode 100644 index 000000000..518b63de3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ENDRET_UTBETALING_ETTERBETALING.json @@ -0,0 +1,71 @@ +{ + "beskrivelse": "Innvilget 1 barn fra august 2021 på behandling med 1 barn, med overstyrt etterbetalingsperiode ", + "fom": "2021-09-01", + "tom": "2021-12-31", + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$ETTER_ENDRET_UTBETALING_ETTERBETALING" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2019-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [ + { + "periode": { + "fom": "2017-09", + "tom": "2021-08" + }, + "årsak": "ETTERBETALING_3ÅR", + "søknadstidspunkt": "2021-01-31" + } + ], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": "til 31. desember 2021 ", + "belop": 1054, + "antallBarn": 1, + "barnasFodselsdager": "20.01.19", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "20.01.19", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "20.01.19", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 1, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "etterEndretUtbetalingEtterbetalingTreAarTilbakeITid", + "belop": 1054, + "soknadstidspunkt": "31.01.21" + } + ], + "type": "innvilgelse" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ETTER_ENDRET_UTBETALING.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ETTER_ENDRET_UTBETALING.json new file mode 100644 index 000000000..c33ca018f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/ETTER_ENDRET_UTBETALING.json @@ -0,0 +1,118 @@ +{ + "beskrivelse": "Innvilget delt bosted barn og utvidet. Barn og utvidet endret utbetaling i forrige periode.", + "fom": "2019-07-01", + "tom": "2020-02-29", + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$ETTER_ENDRET_UTBETALING_HAR_AVTALE_DELT_BOSTED" + ], + "fritekster": [], + "utvidetScenario": "IKKE_UTVIDET_YTELSE", + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2015-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "resultat": "OPPFYLT", + "periodeFom": "2019-05-01", + "periodeTom": null, + "utdypendeVilkårsvurderinger": [ + "DELT_BOSTED" + ], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [ + { + "periode": { + "fom": "2019-06", + "tom": "2019-06" + }, + "årsak": "DELT_BOSTED", + "søknadstidspunkt": "2021-01-31", + "avtaletidspunktDeltBosted": "2021-04-02" + } + ], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 527, + "erPåvirketAvEndring": false, + "prosent": 50 + } + ] + }, + { + "fødselsdato": "2012-06-13", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [ + { + "periode": { + "fom": "2019-06", + "tom": "2019-06" + }, + "årsak": "DELT_BOSTED", + "søknadstidspunkt": "2021-01-31", + "avtaletidspunktDeltBosted": "2021-04-02" + } + ], + "utbetalinger": [ + { + "ytelseType": "UTVIDET_BARNETRYGD", + "utbetaltPerMnd": 527, + "erPåvirketAvEndring": false, + "prosent": 50 + } + ] + } + ], + "forventetOutput": { + "fom": "1. juli 2019", + "tom": "til 29. februar 2020 ", + "belop": 2108, + "antallBarn": 2, + "barnasFodselsdager": "13.06.12 og 20.01.15", + "begrunnelser": [ + { + "gjelderSoker": true, + "barnasFodselsdatoer": "20.01.15", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "20.01.15", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 1, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "juni 2019", + "maalform": "bokmaal", + "apiNavn": "etterEndretUtbetalingAvtaleDeltBosted", + "belop": 2108, + "soknadstidspunkt": "31.01.21", + "sokersRettTilUtvidet": "sokerFaarUtvidet" + } + ], + "type": "innvilgelse" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/E\303\230S_INNVILGET.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/E\303\230S_INNVILGET.json" new file mode 100644 index 000000000..b39b4d9f2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/E\303\230S_INNVILGET.json" @@ -0,0 +1,73 @@ +{ + "beskrivelse": "Innvilget EØS primærland", + "fom": "2021-09-01", + "tom": "2021-12-31", + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [], + "eøsBegrunnelser": [ + "EØSStandardbegrunnelse$INNVILGET_PRIMÆRLAND_STANDARD" + ], + "kompetanser": [ + { + "id": "1", + "søkersAktivitet": "ARBEIDER", + "annenForeldersAktivitet": "INAKTIV", + "annenForeldersAktivitetsland": "Sverige", + "barnetsBostedsland": "Sverige", + "resultat": "NORGE_ER_PRIMÆRLAND" + } + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2020-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ], + "kompetanseIder": [ + "1" + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": "til 31. desember 2021 ", + "belop": 1054, + "antallBarn": 1, + "barnasFodselsdager": "20.01.20", + "begrunnelser": [ + { + "apiNavn": "innvilgetPrimarlandStandard", + "annenForeldersAktivitet": "INAKTIV", + "annenForeldersAktivitetsland": "Sverige", + "barnetsBostedsland": "Sverige", + "barnasFodselsdatoer": "20.01.20", + "antallBarn": 1, + "maalform": "bokmaal", + "sokersAktivitet": "ARBEIDER", + "type": "eøsbegrunnelse" + } + ], + "type": "innvilgelse" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/E\303\230S_OPPH\303\230R.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/E\303\230S_OPPH\303\230R.json" new file mode 100644 index 000000000..47e085a42 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/E\303\230S_OPPH\303\230R.json" @@ -0,0 +1,66 @@ +{ + "beskrivelse": "Opphør EØS primærland", + "fom": "2021-09-01", + "tom": "2021-12-31", + "vedtaksperiodetype": "OPPHØR", + "begrunnelser": [], + "eøsBegrunnelser": [ + "EØSStandardbegrunnelse$OPPHØR_EØS_STANDARD" + ], + "kompetanserSomStopperRettFørPeriode": [ + { + "id": "1", + "søkersAktivitet": "ARBEIDER", + "annenForeldersAktivitet": "IKKE_AKTUELT", + "annenForeldersAktivitetsland": "Sverige", + "barnetsBostedsland": "Sverige", + "resultat": "NORGE_ER_PRIMÆRLAND" + } + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2020-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [], + "kompetanseIder": [ + "1" + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": "til 31. desember 2021 ", + "belop": 0, + "antallBarn": 0, + "barnasFodselsdager": "", + "begrunnelser": [ + { + "apiNavn": "opphorEosStandard", + "annenForeldersAktivitet": "IKKE_AKTUELT", + "annenForeldersAktivitetsland": "Sverige", + "barnetsBostedsland": "Sverige", + "barnasFodselsdatoer": "20.01.20", + "antallBarn": 1, + "maalform": "bokmaal", + "sokersAktivitet": "ARBEIDER", + "type": "eøsbegrunnelse" + } + ], + "type": "opphor" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/FORTSATT_INNVILGET.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/FORTSATT_INNVILGET.json new file mode 100644 index 000000000..b11118372 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/FORTSATT_INNVILGET.json @@ -0,0 +1,90 @@ +{ + "beskrivelse": "Fortsatt innvilget 2 barn fra august 2021", + "fom": "2021-09-01", + "tom": "2021-12-31", + "vedtaksperiodetype": "FORTSATT_INNVILGET", + "begrunnelser": [ + "Standardbegrunnelse$FORTSATT_INNVILGET_SØKER_OG_BARN_BOSATT_I_RIKET", + "Standardbegrunnelse$FORTSATT_INNVILGET_BOR_MED_SØKER" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2020-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2020-12-20", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "Du får:", + "tom": "", + "belop": 2108, + "antallBarn": 2, + "barnasFodselsdager": "20.01.20 og 20.12.20", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "20.01.20 og 20.12.20", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "20.01.20 og 20.12.20", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 2, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 2, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "fortsattInnvilgetSokerOgBarnBosattIRiket", + "belop": 2108 + }, + { + "gjelderSoker": false, + "barnasFodselsdatoer": "20.01.20 og 20.12.20", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "20.01.20 og 20.12.20", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 2, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 2, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "fortsattInnvilgetBorMedSoker", + "belop": 2108 + } + ], + "type": "fortsattInnvilget" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/F\303\230DSELSHENDELSE.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/F\303\230DSELSHENDELSE.json" new file mode 100644 index 000000000..442b9e59d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/F\303\230DSELSHENDELSE.json" @@ -0,0 +1,61 @@ +{ + "beskrivelse": "Reduksjon autobrev 6 år", + "fom": "2021-09-01", + "tom": null, + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$INNVILGET_FØDSELSHENDELSE_NYFØDT_BARN_FØRSTE" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2021-08-01", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1354, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": "", + "belop": 1354, + "antallBarn": 1, + "barnasFodselsdager": "01.08.21", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "01.08.21", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "01.08.21", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 1, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "innvilgetFodselshendelseNyfodtBarnForste", + "belop": 1354 + } + ], + "type": "innvilgelse" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INGEN_BEGRUNNELSER.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INGEN_BEGRUNNELSER.json new file mode 100644 index 000000000..54a38aa6c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INGEN_BEGRUNNELSER.json @@ -0,0 +1,37 @@ +{ + "beskrivelse": "Ingen begrunnelser 2 barn fra august 2021", + "fom": "2021-09-01", + "tom": "2021-12-31", + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2020-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": null +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_BOR_HOS_S\303\230KER.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_BOR_HOS_S\303\230KER.json" new file mode 100644 index 000000000..f09b1c3a9 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_BOR_HOS_S\303\230KER.json" @@ -0,0 +1,71 @@ +{ + "beskrivelse": "Innvilget 1 barn bor med søker fra august 2021 på behandling med 1 barn", + "fom": "2021-09-01", + "tom": "2021-12-31", + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$INNVILGET_BOR_HOS_SØKER" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2020-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "resultat": "OPPFYLT", + "periodeFom": "2021-08-01", + "periodeTom": null, + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": "til 31. desember 2021 ", + "belop": 1054, + "antallBarn": 1, + "barnasFodselsdager": "20.01.20", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "20.01.20", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "20.01.20", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 1, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "innvilgetBorHosSoker", + "belop": 1054 + } + ], + "type": "innvilgelse" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_DELT_BOSTED_OG_BOR_HOS_S\303\230KER.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_DELT_BOSTED_OG_BOR_HOS_S\303\230KER.json" new file mode 100644 index 000000000..dd0b77658 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_DELT_BOSTED_OG_BOR_HOS_S\303\230KER.json" @@ -0,0 +1,112 @@ +{ + "beskrivelse": "Innvilget 2 barn bor med søker. Ett barn delt bosted.", + "fom": "2021-09-01", + "tom": "2021-12-31", + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$INNVILGET_BOR_HOS_SØKER", + "Standardbegrunnelse$INNVILGET_AVTALE_DELT_BOSTED_FÅR_FRA_FLYTTETIDSPUNKT" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2020-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "resultat": "OPPFYLT", + "periodeFom": "2021-08-01", + "periodeTom": null, + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2021-02-13", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "resultat": "OPPFYLT", + "periodeFom": "2021-08-01", + "periodeTom": null, + "utdypendeVilkårsvurderinger": [ + "DELT_BOSTED" + ], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 527, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": "til 31. desember 2021 ", + "belop": 1581, + "antallBarn": 2, + "barnasFodselsdager": "20.01.20 og 13.02.21", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "20.01.20", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "20.01.20", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 1, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "innvilgetBorHosSoker", + "belop": 1054 + }, + { + "gjelderSoker": false, + "barnasFodselsdatoer": "13.02.21", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "13.02.21", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 1, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "innvilgetAvtaleDeltBostedFaarFraFlyttetidspunkt", + "belop": 527 + } + ], + "type": "innvilgelse" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_FOR_S\303\230KER.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_FOR_S\303\230KER.json" new file mode 100644 index 000000000..7b14687f9 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_FOR_S\303\230KER.json" @@ -0,0 +1,118 @@ +{ + "beskrivelse": "Innvilget. Fire barn, men kun søker som har utgjørende vilkår", + "fom": "2021-11-01", + "tom": "2022-02-28", + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$INNVILGET_TREDJELANDSBORGER_LOVLIG_OPPHOLD_FOR_BOSATT_I_NORGE" + ], + "fritekster": [], + "personerPåBehandling": [ + { + "fødselsdato": "1969-07-29", + "type": "SØKER", + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [], + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOSATT_I_RIKET", + "periodeFom": "2021-10-10", + "periodeTom": null, + "resultat": "OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + }, + { + "vilkårType": "LOVLIG_OPPHOLD", + "periodeFom": "2016-07-01", + "periodeTom": null, + "resultat": "OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ] + }, + { + "fødselsdato": "2009-04-15", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2003-03-16", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "2010-09-06", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2004-03-10", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + } + ], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "forventetOutput": { + "fom": "1. november 2021", + "tom": "til 28. februar 2022 ", + "belop": 3162, + "antallBarn": 3, + "barnasFodselsdager": "10.03.04, 15.04.09 og 06.09.10", + "begrunnelser": [ + { + "gjelderSoker": true, + "barnasFodselsdatoer": "10.03.04, 15.04.09 og 06.09.10", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 3, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "oktober 2021", + "maalform": "bokmaal", + "apiNavn": "innvilgetTredjelandsborgerLovligOppholdForBosattINorge", + "belop": 3162 + } + ], + "type": "innvilgelse" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_FOR_S\303\230KER_OG_ETT_BARN.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_FOR_S\303\230KER_OG_ETT_BARN.json" new file mode 100644 index 000000000..5c43ef7a8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_FOR_S\303\230KER_OG_ETT_BARN.json" @@ -0,0 +1,119 @@ +{ + "beskrivelse": "Innvilget. Fire barn, men kun søker og ett barn som har utgjørende vilkår", + "fom": "2021-11-01", + "tom": "2022-02-28", + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$INNVILGET_TREDJELANDSBORGER_LOVLIG_OPPHOLD_FOR_BOSATT_I_NORGE" + ], + "fritekster": [], + "personerPåBehandling": [ + { + "fødselsdato": "2009-04-15", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOSATT_I_RIKET", + "periodeFom": "2021-10-10", + "periodeTom": null, + "resultat": "OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2003-03-16", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "2010-09-06", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2004-03-10", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "1969-07-29", + "type": "SØKER", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOSATT_I_RIKET", + "periodeFom": "2021-10-10", + "periodeTom": null, + "resultat": "OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "forventetOutput": { + "fom": "1. november 2021", + "tom": "til 28. februar 2022 ", + "belop": 3162, + "antallBarn": 3, + "barnasFodselsdager": "10.03.04, 15.04.09 og 06.09.10", + "begrunnelser": [ + { + "gjelderSoker": true, + "barnasFodselsdatoer": "10.03.04, 15.04.09 og 06.09.10", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "15.04.09", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 3, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 1, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "oktober 2021", + "maalform": "bokmaal", + "apiNavn": "innvilgetTredjelandsborgerLovligOppholdForBosattINorge", + "belop": 3162 + } + ], + "type": "innvilgelse" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_MED_REDUKSJON.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_MED_REDUKSJON.json new file mode 100644 index 000000000..fd7cfe523 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/INNVILGET_MED_REDUKSJON.json @@ -0,0 +1,103 @@ +{ + "beskrivelse": "Innvilget med reduksjon 2 barn fra august 2021. Begrunnelsene skal bli sortert.", + "fom": "2021-09-01", + "tom": null, + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$INNVILGET_BOR_HOS_SØKER", + "Standardbegrunnelse$REDUKSJON_FLYTTET_BARN" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2019-03-14", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "resultat": "OPPFYLT", + "periodeFom": "2021-08-01", + "periodeTom": null, + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2020-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "resultat": "OPPFYLT", + "periodeFom": "2020-01-20", + "periodeTom": "2021-08-01", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": "", + "belop": 1054, + "antallBarn": 1, + "barnasFodselsdager": "14.03.19", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "20.01.20", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "reduksjonFlyttetBarn", + "belop": 0 + }, + { + "gjelderSoker": false, + "barnasFodselsdatoer": "14.03.19", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "14.03.19", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 1, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "innvilgetBorHosSoker", + "belop": 1054 + } + ], + "type": "innvilgelse" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/OPPH\303\230R.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/OPPH\303\230R.json" new file mode 100644 index 000000000..6a2eb3535 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/OPPH\303\230R.json" @@ -0,0 +1,64 @@ +{ + "beskrivelse": "Opphør 1 barn bor med søker til august 2021 på behandling med 1 barn", + "fom": "2021-09-01", + "tom": null, + "vedtaksperiodetype": "OPPHØR", + "begrunnelser": [ + "Standardbegrunnelse$OPPHØR_BARN_FLYTTET_FRA_SØKER" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2020-01-20", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "resultat": "OPPFYLT", + "periodeFom": "2020-01-20", + "periodeTom": "2021-08-01", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": "", + "belop": 0, + "antallBarn": 0, + "barnasFodselsdager": "", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "20.01.20", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "opphorBarnBorIkkeMedSoker", + "belop": 0 + } + ], + "type": "opphor" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/REDUKSJON_BOR_MED_S\303\230KER_OG_DELT_BOSTED.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/REDUKSJON_BOR_MED_S\303\230KER_OG_DELT_BOSTED.json" new file mode 100644 index 000000000..6e5ac84e9 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/REDUKSJON_BOR_MED_S\303\230KER_OG_DELT_BOSTED.json" @@ -0,0 +1,148 @@ +{ + "beskrivelse": "Reduksjon start delt bosted og reduksjon barn flytter fra søker", + "fom": "2021-11-01", + "tom": "2021-12-31", + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$REDUKSJON_FLYTTET_BARN", + "Standardbegrunnelse$REDUKSJON_DELT_BARNETRYGD_ANNEN_FORELDER_SØKT" + ], + "fritekster": [], + "personerPåBehandling": [ + { + "fødselsdato": "1968-12-30", + "type": "SØKER", + "overstyrteVilkårresultater": [ + { + "vilkårType": "UTVIDET_BARNETRYGD", + "periodeFom": "2021-10-12", + "periodeTom": null, + "resultat": "OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "UTVIDET_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2011-07-28", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2020-10-30", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "periodeFom": "2021-03-01", + "periodeTom": "2021-10-10", + "resultat": "OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "2018-09-06", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "periodeFom": "2021-03-01", + "periodeTom": "2021-10-12", + "resultat": "OPPFYLT", + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + }, + { + "vilkårType": "BOR_MED_SØKER", + "periodeFom": "2021-10-13", + "periodeTom": null, + "resultat": "OPPFYLT", + "utdypendeVilkårsvurderinger": [ + "DELT_BOSTED" + ], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 827, + "erPåvirketAvEndring": false, + "prosent": 50 + } + ] + } + ], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "forventetOutput": { + "fom": "1. november 2021", + "tom": "til 31. desember 2021 ", + "belop": 2935, + "antallBarn": 2, + "barnasFodselsdager": "28.07.11 og 06.09.18", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "30.10.20", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "oktober 2021", + "maalform": "bokmaal", + "apiNavn": "reduksjonFlyttetBarn", + "belop": 0, + "sokersRettTilUtvidet": "sokerFaarUtvidet" + }, + { + "gjelderSoker": false, + "barnasFodselsdatoer": "06.09.18", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "06.09.18", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 1, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "oktober 2021", + "maalform": "bokmaal", + "apiNavn": "reduksjonDeltBarnetrygdAnnenForelderSokt", + "belop": 827, + "sokersRettTilUtvidet": "sokerFaarUtvidet" + } + ], + "type": "innvilgelse" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/REDUKSJON_FRA_FORRIGE_IVERKSATTE_BEHANDLING.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/REDUKSJON_FRA_FORRIGE_IVERKSATTE_BEHANDLING.json new file mode 100644 index 000000000..27bcb17a8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/REDUKSJON_FRA_FORRIGE_IVERKSATTE_BEHANDLING.json @@ -0,0 +1,118 @@ +{ + "beskrivelse": "Utbetalingsperiode. Reduksjon fra forrige iverksatte behandling. Ett barn bor ikke med søker og skal ikke med i periodeteksten.", + "fom": "2021-03-01", + "tom": "2021-08-31", + "vedtaksperiodetype": "UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING", + "begrunnelser": [ + "Standardbegrunnelse$REDUKSJON_FORELDRENE_BODDE_SAMMEN" + ], + "fritekster": [], + "personerPåBehandling": [ + { + "fødselsdato": "1985-05-24", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + }, + { + "fødselsdato": "2015-10-05", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1354, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2019-09-25", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1354, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2014-04-14", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2021-02-28", + "type": "BARN", + "harReduksjonFraForrigeBehandling": true, + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "periodeFom": "2021-02-28", + "periodeTom": null, + "resultat": "IKKE_OPPFYLT", + "utdypendeVilkårsvurderinger": [ + "VURDERING_ANNET_GRUNNLAG" + ], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "forventetOutput": { + "fom": "1. mars 2021", + "tom": "til 31. august 2021 ", + "belop": 3762, + "antallBarn": 3, + "barnasFodselsdager": "14.04.14, 05.10.15 og 25.09.19", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "28.02.21", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 1, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "februar 2021", + "maalform": "bokmaal", + "apiNavn": "reduksjonForeldreneBoddeSammen", + "belop": 0, + "soknadstidspunkt": "", + "avtaletidspunktDeltBosted": "" + } + ], + "type": "innvilgelse", + "antallBarnMedUtbetaling": 3, + "antallBarnMedNullutbetaling": 0, + "fodselsdagerBarnMedUtbetaling": "14.04.14, 05.10.15 og 25.09.19", + "fodselsdagerBarnMedNullutbetaling": "" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/SATSENDRING.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/SATSENDRING.json new file mode 100644 index 000000000..8e425b997 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/SATSENDRING.json @@ -0,0 +1,76 @@ +{ + "beskrivelse": "Satsendring ", + "fom": "2020-09-01", + "tom": "2020-12-31", + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$INNVILGET_SATSENDRING" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2019-11-30", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1354, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2011-09-06", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [] + } + ], + "forventetOutput": { + "fom": "1. september 2020", + "tom": "til 31. desember 2020 ", + "belop": 2408, + "antallBarn": 2, + "barnasFodselsdager": "06.09.11 og 30.11.19", + "begrunnelser": [ + { + "gjelderSoker": false, + "barnasFodselsdatoer": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 0, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 0, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "august 2020", + "maalform": "bokmaal", + "apiNavn": "innvilgetSatsendring", + "belop": 0 + } + ], + "type": "innvilgelse" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/SM\303\205BARNSTILLEGG.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/SM\303\205BARNSTILLEGG.json" new file mode 100644 index 000000000..cc50d6cd0 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/brevperiodeCaser/SM\303\205BARNSTILLEGG.json" @@ -0,0 +1,109 @@ +{ + "beskrivelse": "Reduksjon småbarnstillegg samtidig som endret utbetaling", + "_kommentar": "Småbarnstillegg-begrunnelsene bruker ikke... ", + "fom": "2021-09-01", + "tom": null, + "vedtaksperiodetype": "UTBETALING", + "begrunnelser": [ + "Standardbegrunnelse$REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_FULL_OVERGANGSSTØNAD" + ], + "fritekster": [], + "uregistrerteBarn": [], + "erFørsteVedtaksperiodePåFagsak": false, + "brevMålform": "NB", + "personerPåBehandling": [ + { + "fødselsdato": "2020-09-03", + "type": "BARN", + "overstyrteVilkårresultater": [], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "ORDINÆR_BARNETRYGD", + "utbetaltPerMnd": 1654, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + }, + { + "fødselsdato": "2021-09-03", + "type": "BARN", + "overstyrteVilkårresultater": [ + { + "vilkårType": "BOR_MED_SØKER", + "resultat": "OPPFYLT", + "periodeFom": "2021-09-03", + "periodeTom": null, + "utdypendeVilkårsvurderinger": [ + "DELT_BOSTED" + ], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [ + { + "periode": { + "fom": "2021-09", + "tom": "2021-09" + }, + "årsak": "DELT_BOSTED" + } + ], + "utbetalinger": [ + ] + }, + { + "fødselsdato": "1993-08-10", + "type": "SØKER", + "overstyrteVilkårresultater": [ + { + "vilkårType": "UTVIDET_BARNETRYGD", + "resultat": "OPPFYLT", + "periodeFom": "2020-08-10", + "periodeTom": null, + "utdypendeVilkårsvurderinger": [], + "erEksplisittAvslagPåSøknad": false, + "standardbegrunnelser": [] + } + ], + "andreVurderinger": [], + "endredeUtbetalinger": [], + "utbetalinger": [ + { + "ytelseType": "UTVIDET_BARNETRYGD", + "utbetaltPerMnd": 1054, + "erPåvirketAvEndring": false, + "prosent": 100 + } + ] + } + ], + "forventetOutput": { + "fom": "1. september 2021", + "tom": "", + "belop": 2708, + "antallBarn": 1, + "barnasFodselsdager": "03.09.20", + "begrunnelser": [ + { + "gjelderSoker": true, + "barnasFodselsdatoer": "03.09.20 og 03.09.21", + "fodselsdatoerBarnOppfyllerTriggereOgHarUtbetaling": "03.09.20", + "fodselsdatoerBarnOppfyllerTriggereOgHarNullutbetaling": "", + "antallBarn": 2, + "antallBarnOppfyllerTriggereOgHarUtbetaling": 1, + "antallBarnOppfyllerTriggereOgHarNullutbetaling": 0, + "maanedOgAarBegrunnelsenGjelderFor": "august 2021", + "maalform": "bokmaal", + "apiNavn": "reduksjonSmaabarnstilleggIkkeLengerFullOvergangsstonad", + "belop": 2708, + "sokersRettTilUtvidet": "sokerFaarUtvidet" + } + ], + "type": "innvilgelse" + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/db/migration-tests/Readme.md b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/db/migration-tests/Readme.md new file mode 100644 index 000000000..fe72f3a7d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/db/migration-tests/Readme.md @@ -0,0 +1,13 @@ +Skriptet som ligg her er ei samanslåing av alle skripta frå main/resources/db/migration, per skrivande stund t.o.m. 247. + +Viss du vil gjenskape det som er gjort, køyr skriptet _lagBaseline.sh_. + +Merk at skriptet fjernar dei første n radene i flyway_schema_history-tabellen. +Dette er ein workaround som primært for å ta bort dei to første innslaga i tabellen, som er dei to skripta som ligg i db\init-mappa og dermed ikkje køyrast i test. + +Merk at du først må +1. Starte databasen i ein postgres-container etter oppskrifta i readme +1. Starte applikasjonen og dermed få køyrd flyway-migreringa med desse +1. Pass på at du har postgresql-kommandoar i _path_ eller at du står i postgresql\bin-mappa + +Merk at du heilt fint kan leggje inn nye skript i src/db/migration på vanleg måte, men desse vil da bli køyrd som separate steg i kvar test (altså tilsvarande som alle vart før denne endringa). \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/db/migration-tests/V1__create_table.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/db/migration-tests/V1__create_table.sql new file mode 100644 index 000000000..5a6b61dfc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/db/migration-tests/V1__create_table.sql @@ -0,0 +1,4455 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 14.5 (Debian 14.5-1.pgdg110+1) +-- Dumped by pg_dump version 14.6 (Homebrew) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: aktoer; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aktoer ( + aktoer_id character varying NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: aktoer_til_kompetanse; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aktoer_til_kompetanse ( + fk_kompetanse_id bigint NOT NULL, + fk_aktoer_id character varying NOT NULL +); + + +-- +-- Name: aktoer_til_utenlandsk_periodebeloep; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aktoer_til_utenlandsk_periodebeloep ( + fk_utenlandsk_periodebeloep_id bigint NOT NULL, + fk_aktoer_id character varying NOT NULL +); + + +-- +-- Name: aktoer_til_valutakurs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aktoer_til_valutakurs ( + fk_valutakurs_id bigint NOT NULL, + fk_aktoer_id character varying NOT NULL +); + + +-- +-- Name: andel_tilkjent_ytelse; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.andel_tilkjent_ytelse ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying(512) DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + stonad_fom timestamp(3) without time zone NOT NULL, + stonad_tom timestamp(3) without time zone NOT NULL, + type character varying(50) NOT NULL, + kalkulert_utbetalingsbelop numeric, + endret_av character varying(512), + endret_tid timestamp(3) without time zone, + tilkjent_ytelse_id bigint, + periode_offset bigint, + forrige_periode_offset bigint, + kilde_behandling_id bigint, + prosent numeric NOT NULL, + sats bigint NOT NULL, + fk_aktoer_id character varying, + nasjonalt_periodebelop numeric, + differanseberegnet_periodebelop numeric +); + + +-- +-- Name: andel_tilkjent_ytelse_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.andel_tilkjent_ytelse_seq + START WITH 2000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: annen_vurdering; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.annen_vurdering ( + id bigint NOT NULL, + fk_person_resultat_id bigint NOT NULL, + resultat character varying NOT NULL, + type character varying NOT NULL, + begrunnelse text, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: annen_vurdering_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.annen_vurdering_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: arbeidsfordeling_pa_behandling; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.arbeidsfordeling_pa_behandling ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + behandlende_enhet_id character varying NOT NULL, + behandlende_enhet_navn character varying NOT NULL, + manuelt_overstyrt boolean NOT NULL +); + + +-- +-- Name: arbeidsfordeling_pa_behandling_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.arbeidsfordeling_pa_behandling_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: batch; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.batch ( + id bigint NOT NULL, + kjoredato timestamp(3) without time zone NOT NULL, + status character varying(50) DEFAULT 'LEDIG'::character varying NOT NULL +); + + +-- +-- Name: batch_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.batch_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: behandling; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.behandling ( + id bigint NOT NULL, + fk_fagsak_id bigint, + versjon bigint DEFAULT 0, + opprettet_av character varying(512) DEFAULT 'VL'::character varying, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP, + endret_av character varying(512), + endret_tid timestamp(3) without time zone, + behandling_type character varying(50), + aktiv boolean DEFAULT true, + status character varying(50) DEFAULT 'OPPRETTET'::character varying, + kategori character varying(50) DEFAULT 'NATIONAL'::character varying, + underkategori character varying(50) DEFAULT 'ORDINÆR'::character varying, + opprettet_aarsak character varying DEFAULT 'MANUELL'::character varying, + skal_behandles_automatisk boolean DEFAULT false, + resultat character varying DEFAULT 'IKKE_VURDERT'::character varying NOT NULL, + overstyrt_endringstidspunkt timestamp(3) without time zone, + aktivert_tid timestamp(3) without time zone NOT NULL +); + + +-- +-- Name: behandling_migreringsinfo; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.behandling_migreringsinfo ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + migreringsdato date NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: behandling_migreringsinfo_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.behandling_migreringsinfo_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: behandling_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.behandling_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: behandling_soknadsinfo; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.behandling_soknadsinfo ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + mottatt_dato timestamp(3) without time zone NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + er_digital boolean, + journalpost_id character varying, + brevkode character varying +); + + +-- +-- Name: behandling_soknadsinfo_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.behandling_soknadsinfo_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: behandling_steg_tilstand; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.behandling_steg_tilstand ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + behandling_steg character varying NOT NULL, + behandling_steg_status character varying DEFAULT 'IKKE_UTFØRT'::character varying NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: behandling_steg_tilstand_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.behandling_steg_tilstand_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: brevmottaker; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.brevmottaker ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + type character varying(50) NOT NULL, + navn character varying NOT NULL, + adresselinje_1 character varying NOT NULL, + adresselinje_2 character varying, + postnummer character varying NOT NULL, + poststed character varying NOT NULL, + landkode character varying(2) NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: brevmottaker_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.brevmottaker_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: data_chunk; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.data_chunk ( + id bigint NOT NULL, + fk_batch_id bigint NOT NULL, + transaksjons_id uuid NOT NULL, + chunk_nr bigint NOT NULL, + er_sendt boolean NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: data_chunk_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.data_chunk_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: endret_utbetaling_andel; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.endret_utbetaling_andel ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + fk_po_person_id bigint, + fom timestamp(3) without time zone, + tom timestamp(3) without time zone, + prosent numeric, + aarsak character varying, + begrunnelse text, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + vedtak_begrunnelse_spesifikasjoner text DEFAULT ''::text, + avtaletidspunkt_delt_bosted timestamp(3) without time zone, + soknadstidspunkt timestamp(3) without time zone +); + + +-- +-- Name: endret_utbetaling_andel_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.endret_utbetaling_andel_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: eos_begrunnelse; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.eos_begrunnelse ( + id bigint NOT NULL, + fk_vedtaksperiode_id bigint, + begrunnelse character varying NOT NULL +); + + +-- +-- Name: eos_begrunnelse_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.eos_begrunnelse_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: fagsak; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.fagsak ( + id bigint NOT NULL, + versjon bigint DEFAULT 0, + opprettet_av character varying(512) DEFAULT 'VL'::character varying, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP, + endret_av character varying(512), + endret_tid timestamp(3) without time zone, + status character varying(50) DEFAULT 'OPPRETTET'::character varying, + arkivert boolean DEFAULT false NOT NULL, + fk_aktoer_id character varying, + type character varying(50) DEFAULT 'NORMAL'::character varying NOT NULL, + fk_institusjon_id bigint +); + + +-- +-- Name: fagsak_person_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.fagsak_person_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: fagsak_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.fagsak_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: feilutbetalt_valuta; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.feilutbetalt_valuta ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + fom timestamp(3) without time zone NOT NULL, + tom timestamp(3) without time zone NOT NULL, + feilutbetalt_beloep numeric, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + er_per_maaned boolean DEFAULT false NOT NULL +); + + +-- +-- Name: feilutbetalt_valuta_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.feilutbetalt_valuta_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: foedselshendelse_pre_lansering; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.foedselshendelse_pre_lansering ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + ny_behandling_hendelse text NOT NULL, + filtreringsregler_input text, + filtreringsregler_output text, + vilkaarsvurderinger_for_foedselshendelse text, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + versjon bigint DEFAULT 0 NOT NULL, + fk_aktoer_id character varying +); + + +-- +-- Name: foedselshendelse_pre_lansering_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.foedselshendelse_pre_lansering_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: foedselshendelsefiltrering_resultat; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.foedselshendelsefiltrering_resultat ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + filtreringsregel character varying NOT NULL, + resultat character varying NOT NULL, + begrunnelse text NOT NULL, + evalueringsaarsaker text NOT NULL, + regel_input text, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: foedselshendelsefiltrering_resultat_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.foedselshendelsefiltrering_resultat_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: gr_periode_overgangsstonad; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.gr_periode_overgangsstonad ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + fom timestamp(3) without time zone NOT NULL, + tom timestamp(3) without time zone NOT NULL, + datakilde character varying NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + fk_aktoer_id character varying +); + + +-- +-- Name: gr_periode_overgangsstonad_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.gr_periode_overgangsstonad_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: gr_personopplysninger; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.gr_personopplysninger ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying(512) DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying(512), + endret_tid timestamp(3) without time zone, + aktiv boolean DEFAULT true NOT NULL +); + + +-- +-- Name: gr_personopplysninger_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.gr_personopplysninger_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: gr_soknad; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.gr_soknad ( + id bigint NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + fk_behandling_id bigint NOT NULL, + soknad text NOT NULL, + aktiv boolean DEFAULT true NOT NULL +); + + +-- +-- Name: gr_soknad_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.gr_soknad_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: institusjon; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.institusjon ( + id bigint NOT NULL, + org_nummer character varying NOT NULL, + tss_ekstern_id character varying, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying(20) DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying(20), + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: institusjon_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.institusjon_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: journalpost; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.journalpost ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + journalpost_id character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + type character varying +); + + +-- +-- Name: journalpost_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.journalpost_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: kompetanse; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.kompetanse ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + fom timestamp(3) without time zone, + tom timestamp(3) without time zone, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + soekers_aktivitet character varying, + annen_forelderes_aktivitet character varying, + annen_forelderes_aktivitetsland character varying, + barnets_bostedsland character varying, + resultat character varying, + sokers_aktivitetsland text +); + + +-- +-- Name: kompetanse_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.kompetanse_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: korrigert_etterbetaling; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.korrigert_etterbetaling ( + id bigint NOT NULL, + aarsak character varying NOT NULL, + begrunnelse character varying, + belop bigint NOT NULL, + aktiv boolean NOT NULL, + fk_behandling_id bigint NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: korrigert_etterbetaling_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.korrigert_etterbetaling_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: korrigert_vedtak; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.korrigert_vedtak ( + id bigint NOT NULL, + begrunnelse character varying, + vedtaksdato timestamp(3) without time zone DEFAULT NULL::timestamp without time zone, + aktiv boolean NOT NULL, + fk_behandling_id bigint NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: korrigert_vedtak_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.korrigert_vedtak_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: logg; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.logg ( + id bigint NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + fk_behandling_id bigint NOT NULL, + type character varying NOT NULL, + tittel character varying NOT NULL, + rolle character varying NOT NULL, + tekst text NOT NULL +); + + +-- +-- Name: logg_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.logg_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: okonomi_simulering_mottaker; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.okonomi_simulering_mottaker ( + id bigint NOT NULL, + mottaker_nummer character varying(50), + mottaker_type character varying(50), + opprettet_av character varying(512) DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying(512), + endret_tid timestamp(3) without time zone, + versjon bigint DEFAULT 0, + fk_behandling_id bigint +); + + +-- +-- Name: okonomi_simulering_mottaker_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.okonomi_simulering_mottaker_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: okonomi_simulering_postering; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.okonomi_simulering_postering ( + id bigint NOT NULL, + fk_okonomi_simulering_mottaker_id bigint, + fag_omraade_kode character varying(50), + fom timestamp(3) without time zone, + tom timestamp(3) without time zone, + betaling_type character varying(50), + belop bigint, + postering_type character varying(50), + forfallsdato timestamp(3) without time zone, + uten_inntrekk boolean, + opprettet_av character varying(512) DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying(512), + endret_tid timestamp(3) without time zone, + versjon bigint DEFAULT 0, + er_feilkonto boolean +); + + +-- +-- Name: okonomi_simulering_postering_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.okonomi_simulering_postering_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: oppgave; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.oppgave ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + gsak_id character varying NOT NULL, + type character varying NOT NULL, + ferdigstilt boolean NOT NULL, + opprettet_tid timestamp without time zone NOT NULL +); + + +-- +-- Name: oppgave_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.oppgave_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: periode_resultat_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.periode_resultat_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: person_resultat; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.person_resultat ( + id bigint NOT NULL, + fk_vilkaarsvurdering_id bigint NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + fk_aktoer_id character varying +); + + +-- +-- Name: personident; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.personident ( + fk_aktoer_id character varying NOT NULL, + foedselsnummer character varying NOT NULL, + aktiv boolean DEFAULT false NOT NULL, + gjelder_til timestamp(3) without time zone, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: po_arbeidsforhold; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.po_arbeidsforhold ( + id bigint NOT NULL, + fk_po_person_id bigint NOT NULL, + arbeidsgiver_id character varying, + arbeidsgiver_type character varying, + fom date, + tom date, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + versjon bigint DEFAULT 0 NOT NULL +); + + +-- +-- Name: po_arbeidsforhold_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.po_arbeidsforhold_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: po_bostedsadresse; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.po_bostedsadresse ( + id bigint NOT NULL, + type character varying(20) NOT NULL, + bostedskommune character varying, + husnummer character varying, + husbokstav character varying, + bruksenhetsnummer character varying, + adressenavn character varying, + kommunenummer character varying, + tilleggsnavn character varying, + postnummer character varying, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + endret_av character varying, + versjon bigint DEFAULT 0 NOT NULL, + endret_tid timestamp(3) without time zone, + matrikkel_id bigint, + fom date, + tom date, + fk_po_person_id bigint +); + + +-- +-- Name: po_bostedsadresse_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.po_bostedsadresse_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: po_bostedsadresseperiode; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.po_bostedsadresseperiode ( + id bigint NOT NULL, + fk_po_person_id bigint NOT NULL, + fom date, + tom date, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + versjon bigint DEFAULT 0 NOT NULL +); + + +-- +-- Name: po_bostedsadresseperiode_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.po_bostedsadresseperiode_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: po_doedsfall; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.po_doedsfall ( + id bigint NOT NULL, + fk_po_person_id bigint NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + doedsfall_dato timestamp(3) without time zone NOT NULL, + doedsfall_adresse character varying, + doedsfall_postnummer character varying, + doedsfall_poststed character varying, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: po_doedsfall_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.po_doedsfall_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: po_opphold; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.po_opphold ( + id bigint NOT NULL, + fk_po_person_id bigint NOT NULL, + type character varying NOT NULL, + fom date, + tom date, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + versjon bigint DEFAULT 0 NOT NULL +); + + +-- +-- Name: po_opphold_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.po_opphold_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: po_person; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.po_person ( + id bigint NOT NULL, + fk_gr_personopplysninger_id bigint NOT NULL, + type character varying(10) NOT NULL, + opprettet_av character varying(512) DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + endret_av character varying(512), + versjon bigint DEFAULT 0 NOT NULL, + endret_tid timestamp(3) without time zone, + foedselsdato timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP, + fk_aktoer_id character varying(50), + navn character varying DEFAULT ''::character varying, + kjoenn character varying DEFAULT 'UKJENT'::character varying, + maalform character varying(2) DEFAULT 'NB'::character varying NOT NULL +); + + +-- +-- Name: po_person_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.po_person_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: po_sivilstand; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.po_sivilstand ( + id bigint NOT NULL, + fk_po_person_id bigint NOT NULL, + fom date, + type character varying NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + versjon bigint DEFAULT 0 NOT NULL +); + + +-- +-- Name: po_sivilstand_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.po_sivilstand_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: po_statsborgerskap; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.po_statsborgerskap ( + id bigint NOT NULL, + fk_po_person_id bigint NOT NULL, + landkode character varying(3) DEFAULT 'XUK'::character varying NOT NULL, + fom date, + tom date, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + versjon bigint DEFAULT 0 NOT NULL, + medlemskap character varying DEFAULT 'UKJENT'::character varying NOT NULL +); + + +-- +-- Name: po_statsborgerskap_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.po_statsborgerskap_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: refusjon_eos; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.refusjon_eos ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + fom timestamp(3) without time zone NOT NULL, + tom timestamp(3) without time zone NOT NULL, + refusjonsbeloep numeric NOT NULL, + land character varying NOT NULL, + refusjon_avklart boolean NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: refusjon_eos_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.refusjon_eos_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: saksstatistikk_mellomlagring; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.saksstatistikk_mellomlagring ( + id bigint NOT NULL, + offset_verdi bigint, + funksjonell_id character varying NOT NULL, + type character varying NOT NULL, + kontrakt_versjon character varying NOT NULL, + json text NOT NULL, + konvertert_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + sendt_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP, + type_id bigint, + offset_aiven bigint +); + + +-- +-- Name: saksstatistikk_mellomlagring_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.saksstatistikk_mellomlagring_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: sats_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.sats_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: satskjoering; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.satskjoering ( + id bigint NOT NULL, + fk_fagsak_id bigint NOT NULL, + start_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + ferdig_tid timestamp(3) without time zone, + feiltype character varying, + sats_tid timestamp(3) without time zone DEFAULT to_timestamp('01-03-2023'::text, 'DD-MM-YYYY SS:MS'::text) NOT NULL +); + + +-- +-- Name: satskjoering_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.satskjoering_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: sett_paa_vent; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.sett_paa_vent ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + frist timestamp(3) without time zone NOT NULL, + aktiv boolean DEFAULT false NOT NULL, + aarsak character varying NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + tid_tatt_av_vent timestamp(3) without time zone, + tid_satt_paa_vent timestamp(3) without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: sett_paa_vent_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.sett_paa_vent_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: skyggesak; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.skyggesak ( + id bigint NOT NULL, + fk_fagsak_id bigint NOT NULL, + sendt_tid timestamp(3) without time zone +); + + +-- +-- Name: skyggesak_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.skyggesak_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: task; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.task ( + id bigint NOT NULL, + payload text NOT NULL, + status character varying(50) DEFAULT 'UBEHANDLET'::character varying NOT NULL, + versjon bigint DEFAULT 0, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP, + type character varying(100) NOT NULL, + metadata character varying(4000), + trigger_tid timestamp without time zone DEFAULT LOCALTIMESTAMP, + avvikstype character varying(50) +); + + +-- +-- Name: task_logg; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.task_logg ( + id bigint NOT NULL, + task_id bigint NOT NULL, + type character varying(50) NOT NULL, + node character varying(100) NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP, + melding text, + endret_av character varying(100) DEFAULT 'VL'::character varying +); + + +-- +-- Name: task_logg_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.task_logg_seq + START WITH 1 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: task_logg_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.task_logg_seq OWNED BY public.task_logg.id; + + +-- +-- Name: task_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.task_seq + START WITH 1 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: task_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.task_seq OWNED BY public.task.id; + + +-- +-- Name: tilbakekreving; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.tilbakekreving ( + id bigint NOT NULL, + valg character varying NOT NULL, + varsel text, + begrunnelse text NOT NULL, + tilbakekrevingsbehandling_id text, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + versjon bigint DEFAULT 0, + fk_behandling_id bigint +); + + +-- +-- Name: tilbakekreving_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.tilbakekreving_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: tilkjent_ytelse; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.tilkjent_ytelse ( + id bigint NOT NULL, + fk_behandling_id bigint, + stonad_fom timestamp without time zone, + stonad_tom timestamp without time zone, + opprettet_dato timestamp without time zone NOT NULL, + opphor_fom timestamp without time zone, + utbetalingsoppdrag text, + endret_dato timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: tilkjent_ytelse_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.tilkjent_ytelse_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: totrinnskontroll; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.totrinnskontroll ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + aktiv boolean DEFAULT true NOT NULL, + saksbehandler character varying NOT NULL, + beslutter character varying, + godkjent boolean DEFAULT true, + saksbehandler_id character varying DEFAULT 'ukjent'::character varying NOT NULL, + beslutter_id character varying, + kontrollerte_sider text DEFAULT ''::text +); + + +-- +-- Name: totrinnskontroll_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.totrinnskontroll_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: utenlandsk_periodebeloep; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.utenlandsk_periodebeloep ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + fom timestamp(3) without time zone, + tom timestamp(3) without time zone, + intervall character varying, + valutakode character varying, + beloep numeric, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + utbetalingsland character varying, + kalkulert_maanedlig_beloep numeric +); + + +-- +-- Name: utenlandsk_periodebeloep_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.utenlandsk_periodebeloep_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: valutakurs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.valutakurs ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + fom timestamp(3) without time zone, + tom timestamp(3) without time zone, + valutakursdato timestamp(3) without time zone DEFAULT NULL::timestamp without time zone, + valutakode character varying, + kurs numeric, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: valutakurs_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.valutakurs_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: vedtak; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.vedtak ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying(512) DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + vedtaksdato timestamp(3) without time zone DEFAULT LOCALTIMESTAMP, + endret_av character varying(512), + endret_tid timestamp(3) without time zone, + aktiv boolean DEFAULT true, + stonad_brev_pdf bytea +); + + +-- +-- Name: vedtak_begrunnelse_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.vedtak_begrunnelse_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: vedtak_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.vedtak_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: vedtaksbegrunnelse; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.vedtaksbegrunnelse ( + id bigint NOT NULL, + fk_vedtaksperiode_id bigint, + vedtak_begrunnelse_spesifikasjon character varying NOT NULL +); + + +-- +-- Name: vedtaksbegrunnelse_fritekst; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.vedtaksbegrunnelse_fritekst ( + id bigint NOT NULL, + fk_vedtaksperiode_id bigint, + fritekst text DEFAULT ''::text NOT NULL +); + + +-- +-- Name: vedtaksbegrunnelse_fritekst_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.vedtaksbegrunnelse_fritekst_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: vedtaksbegrunnelse_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.vedtaksbegrunnelse_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: vedtaksperiode; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.vedtaksperiode ( + id bigint NOT NULL, + fk_vedtak_id bigint, + fom timestamp without time zone, + tom timestamp without time zone, + type character varying NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + versjon bigint DEFAULT 0 +); + + +-- +-- Name: vedtaksperiode_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.vedtaksperiode_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: verge; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.verge ( + id bigint NOT NULL, + ident character varying, + fk_behandling_id bigint NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying(20) DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying(20), + endret_tid timestamp(3) without time zone +); + + +-- +-- Name: verge_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.verge_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: vilkaarsvurdering; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.vilkaarsvurdering ( + id bigint NOT NULL, + fk_behandling_id bigint NOT NULL, + aktiv boolean DEFAULT true NOT NULL, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying, + endret_tid timestamp(3) without time zone, + samlet_resultat character varying, + ytelse_personer text DEFAULT ''::text +); + + +-- +-- Name: vilkaarsvurdering_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.vilkaarsvurdering_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: vilkar_resultat; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.vilkar_resultat ( + id bigint NOT NULL, + vilkar character varying(50) NOT NULL, + resultat character varying(50) NOT NULL, + regel_input text, + regel_output text, + versjon bigint DEFAULT 0 NOT NULL, + opprettet_av character varying(512) DEFAULT 'VL'::character varying NOT NULL, + opprettet_tid timestamp(3) without time zone DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av character varying(512), + endret_tid timestamp(3) without time zone, + fk_person_resultat_id bigint, + begrunnelse text, + periode_fom timestamp(3) without time zone DEFAULT NULL::timestamp without time zone, + periode_tom timestamp(3) without time zone DEFAULT NULL::timestamp without time zone, + fk_behandling_id bigint NOT NULL, + evaluering_aarsak text DEFAULT ''::text, + er_automatisk_vurdert boolean DEFAULT false NOT NULL, + er_eksplisitt_avslag_paa_soknad boolean, + vedtak_begrunnelse_spesifikasjoner text DEFAULT ''::text, + vurderes_etter character varying, + utdypende_vilkarsvurderinger character varying, + resultat_begrunnelse character varying +); + + +-- +-- Name: vilkar_resultat_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.vilkar_resultat_seq + START WITH 1000000 + INCREMENT BY 50 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: task id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.task ALTER COLUMN id SET DEFAULT nextval('public.task_seq'::regclass); + + +-- +-- Name: task_logg id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.task_logg ALTER COLUMN id SET DEFAULT nextval('public.task_logg_seq'::regclass); + + +-- +-- Data for Name: aktoer; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.aktoer (aktoer_id, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: aktoer_til_kompetanse; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.aktoer_til_kompetanse (fk_kompetanse_id, fk_aktoer_id) FROM stdin; +\. + + +-- +-- Data for Name: aktoer_til_utenlandsk_periodebeloep; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.aktoer_til_utenlandsk_periodebeloep (fk_utenlandsk_periodebeloep_id, fk_aktoer_id) FROM stdin; +\. + + +-- +-- Data for Name: aktoer_til_valutakurs; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.aktoer_til_valutakurs (fk_valutakurs_id, fk_aktoer_id) FROM stdin; +\. + + +-- +-- Data for Name: andel_tilkjent_ytelse; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.andel_tilkjent_ytelse (id, fk_behandling_id, versjon, opprettet_av, opprettet_tid, stonad_fom, stonad_tom, type, kalkulert_utbetalingsbelop, endret_av, endret_tid, tilkjent_ytelse_id, periode_offset, forrige_periode_offset, kilde_behandling_id, prosent, sats, fk_aktoer_id, nasjonalt_periodebelop, differanseberegnet_periodebelop) FROM stdin; +\. + + +-- +-- Data for Name: annen_vurdering; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.annen_vurdering (id, fk_person_resultat_id, resultat, type, begrunnelse, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: arbeidsfordeling_pa_behandling; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.arbeidsfordeling_pa_behandling (id, fk_behandling_id, behandlende_enhet_id, behandlende_enhet_navn, manuelt_overstyrt) FROM stdin; +\. + + +-- +-- Data for Name: batch; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.batch (id, kjoredato, status) FROM stdin; +1000000 2021-01-06 00:00:00 LEDIG +1000050 2021-01-29 00:00:00 LEDIG +1000100 2021-02-26 00:00:00 LEDIG +1000150 2021-03-31 00:00:00 LEDIG +1000200 2021-04-26 00:00:00 LEDIG +1000250 2021-05-28 00:00:00 LEDIG +1000300 2021-06-29 00:00:00 LEDIG +1000350 2021-07-30 00:00:00 LEDIG +1000400 2021-08-30 00:00:00 LEDIG +1000450 2021-09-27 00:00:00 LEDIG +1000500 2021-10-29 00:00:00 LEDIG +1000550 2021-11-22 00:00:00 LEDIG +1000600 2022-01-05 00:00:00 LEDIG +1000650 2022-01-28 00:00:00 LEDIG +1000700 2022-02-25 00:00:00 LEDIG +1000750 2022-03-25 00:00:00 LEDIG +1000800 2022-04-26 00:00:00 LEDIG +1000850 2022-05-27 00:00:00 LEDIG +1000900 2022-06-29 00:00:00 LEDIG +1000950 2022-07-29 00:00:00 LEDIG +1001000 2022-08-30 00:00:00 LEDIG +1001050 2022-09-29 00:00:00 LEDIG +1001100 2022-10-28 00:00:00 LEDIG +1001150 2022-11-21 00:00:00 LEDIG +1001200 2023-01-05 00:00:00 LEDIG +1001250 2023-01-30 00:00:00 LEDIG +1001300 2023-02-27 00:00:00 LEDIG +1001350 2023-03-28 00:00:00 LEDIG +1001400 2023-04-25 00:00:00 LEDIG +1001450 2023-05-30 00:00:00 LEDIG +1001500 2023-06-29 00:00:00 LEDIG +1001550 2023-07-28 00:00:00 LEDIG +1001600 2023-08-30 00:00:00 LEDIG +1001650 2023-09-29 00:00:00 LEDIG +1001700 2023-10-30 00:00:00 LEDIG +1001750 2023-11-22 00:00:00 LEDIG +\. + + +-- +-- Data for Name: behandling; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.behandling (id, fk_fagsak_id, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid, behandling_type, aktiv, status, kategori, underkategori, opprettet_aarsak, skal_behandles_automatisk, resultat, overstyrt_endringstidspunkt, aktivert_tid) FROM stdin; +\. + + +-- +-- Data for Name: behandling_migreringsinfo; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.behandling_migreringsinfo (id, fk_behandling_id, migreringsdato, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: behandling_soknadsinfo; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.behandling_soknadsinfo (id, fk_behandling_id, mottatt_dato, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid, er_digital, journalpost_id, brevkode) FROM stdin; +\. + + +-- +-- Data for Name: behandling_steg_tilstand; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.behandling_steg_tilstand (id, fk_behandling_id, behandling_steg, behandling_steg_status, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: brevmottaker; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.brevmottaker (id, fk_behandling_id, type, navn, adresselinje_1, adresselinje_2, postnummer, poststed, landkode, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: data_chunk; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.data_chunk (id, fk_batch_id, transaksjons_id, chunk_nr, er_sendt, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: endret_utbetaling_andel; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.endret_utbetaling_andel (id, fk_behandling_id, fk_po_person_id, fom, tom, prosent, aarsak, begrunnelse, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid, vedtak_begrunnelse_spesifikasjoner, avtaletidspunkt_delt_bosted, soknadstidspunkt) FROM stdin; +\. + + +-- +-- Data for Name: eos_begrunnelse; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.eos_begrunnelse (id, fk_vedtaksperiode_id, begrunnelse) FROM stdin; +\. + + +-- +-- Data for Name: fagsak; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.fagsak (id, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid, status, arkivert, fk_aktoer_id, type, fk_institusjon_id) FROM stdin; +\. + + +-- +-- Data for Name: feilutbetalt_valuta; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.feilutbetalt_valuta (id, fk_behandling_id, fom, tom, feilutbetalt_beloep, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid, er_per_maaned) FROM stdin; +\. + + +-- +-- Data for Name: foedselshendelse_pre_lansering; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.foedselshendelse_pre_lansering (id, fk_behandling_id, ny_behandling_hendelse, filtreringsregler_input, filtreringsregler_output, vilkaarsvurderinger_for_foedselshendelse, opprettet_av, opprettet_tid, endret_av, endret_tid, versjon, fk_aktoer_id) FROM stdin; +\. + + +-- +-- Data for Name: foedselshendelsefiltrering_resultat; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.foedselshendelsefiltrering_resultat (id, fk_behandling_id, filtreringsregel, resultat, begrunnelse, evalueringsaarsaker, regel_input, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: gr_periode_overgangsstonad; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.gr_periode_overgangsstonad (id, fk_behandling_id, fom, tom, datakilde, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid, fk_aktoer_id) FROM stdin; +\. + + +-- +-- Data for Name: gr_personopplysninger; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.gr_personopplysninger (id, fk_behandling_id, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid, aktiv) FROM stdin; +\. + + +-- +-- Data for Name: gr_soknad; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.gr_soknad (id, opprettet_av, opprettet_tid, fk_behandling_id, soknad, aktiv) FROM stdin; +\. + + +-- +-- Data for Name: institusjon; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.institusjon (id, org_nummer, tss_ekstern_id, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: journalpost; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.journalpost (id, fk_behandling_id, journalpost_id, opprettet_tid, opprettet_av, type) FROM stdin; +\. + + +-- +-- Data for Name: kompetanse; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.kompetanse (id, fk_behandling_id, fom, tom, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid, soekers_aktivitet, annen_forelderes_aktivitet, annen_forelderes_aktivitetsland, barnets_bostedsland, resultat, sokers_aktivitetsland) FROM stdin; +\. + + +-- +-- Data for Name: korrigert_etterbetaling; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.korrigert_etterbetaling (id, aarsak, begrunnelse, belop, aktiv, fk_behandling_id, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: korrigert_vedtak; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.korrigert_vedtak (id, begrunnelse, vedtaksdato, aktiv, fk_behandling_id, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: logg; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.logg (id, opprettet_av, opprettet_tid, fk_behandling_id, type, tittel, rolle, tekst) FROM stdin; +\. + + +-- +-- Data for Name: okonomi_simulering_mottaker; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.okonomi_simulering_mottaker (id, mottaker_nummer, mottaker_type, opprettet_av, opprettet_tid, endret_av, endret_tid, versjon, fk_behandling_id) FROM stdin; +\. + + +-- +-- Data for Name: okonomi_simulering_postering; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.okonomi_simulering_postering (id, fk_okonomi_simulering_mottaker_id, fag_omraade_kode, fom, tom, betaling_type, belop, postering_type, forfallsdato, uten_inntrekk, opprettet_av, opprettet_tid, endret_av, endret_tid, versjon, er_feilkonto) FROM stdin; +\. + + +-- +-- Data for Name: oppgave; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.oppgave (id, fk_behandling_id, gsak_id, type, ferdigstilt, opprettet_tid) FROM stdin; +\. + + +-- +-- Data for Name: person_resultat; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.person_resultat (id, fk_vilkaarsvurdering_id, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid, fk_aktoer_id) FROM stdin; +\. + + +-- +-- Data for Name: personident; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.personident (fk_aktoer_id, foedselsnummer, aktiv, gjelder_til, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: po_arbeidsforhold; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.po_arbeidsforhold (id, fk_po_person_id, arbeidsgiver_id, arbeidsgiver_type, fom, tom, opprettet_av, opprettet_tid, endret_av, endret_tid, versjon) FROM stdin; +\. + + +-- +-- Data for Name: po_bostedsadresse; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.po_bostedsadresse (id, type, bostedskommune, husnummer, husbokstav, bruksenhetsnummer, adressenavn, kommunenummer, tilleggsnavn, postnummer, opprettet_av, opprettet_tid, endret_av, versjon, endret_tid, matrikkel_id, fom, tom, fk_po_person_id) FROM stdin; +\. + + +-- +-- Data for Name: po_bostedsadresseperiode; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.po_bostedsadresseperiode (id, fk_po_person_id, fom, tom, opprettet_av, opprettet_tid, endret_av, endret_tid, versjon) FROM stdin; +\. + + +-- +-- Data for Name: po_doedsfall; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.po_doedsfall (id, fk_po_person_id, versjon, doedsfall_dato, doedsfall_adresse, doedsfall_postnummer, doedsfall_poststed, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: po_opphold; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.po_opphold (id, fk_po_person_id, type, fom, tom, opprettet_av, opprettet_tid, endret_av, endret_tid, versjon) FROM stdin; +\. + + +-- +-- Data for Name: po_person; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.po_person (id, fk_gr_personopplysninger_id, type, opprettet_av, opprettet_tid, endret_av, versjon, endret_tid, foedselsdato, fk_aktoer_id, navn, kjoenn, maalform) FROM stdin; +\. + + +-- +-- Data for Name: po_sivilstand; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.po_sivilstand (id, fk_po_person_id, fom, type, opprettet_av, opprettet_tid, endret_av, endret_tid, versjon) FROM stdin; +\. + + +-- +-- Data for Name: po_statsborgerskap; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.po_statsborgerskap (id, fk_po_person_id, landkode, fom, tom, opprettet_av, opprettet_tid, endret_av, endret_tid, versjon, medlemskap) FROM stdin; +\. + + +-- +-- Data for Name: refusjon_eos; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.refusjon_eos (id, fk_behandling_id, fom, tom, refusjonsbeloep, land, refusjon_avklart, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: saksstatistikk_mellomlagring; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.saksstatistikk_mellomlagring (id, offset_verdi, funksjonell_id, type, kontrakt_versjon, json, konvertert_tid, opprettet_tid, sendt_tid, type_id, offset_aiven) FROM stdin; +\. + + +-- +-- Data for Name: satskjoering; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.satskjoering (id, fk_fagsak_id, start_tid, ferdig_tid, feiltype, sats_tid) FROM stdin; +\. + + +-- +-- Data for Name: sett_paa_vent; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.sett_paa_vent (id, fk_behandling_id, versjon, opprettet_av, opprettet_tid, frist, aktiv, aarsak, endret_av, endret_tid, tid_tatt_av_vent, tid_satt_paa_vent) FROM stdin; +\. + + +-- +-- Data for Name: skyggesak; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.skyggesak (id, fk_fagsak_id, sendt_tid) FROM stdin; +\. + + +-- +-- Data for Name: task; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.task (id, payload, status, versjon, opprettet_tid, type, metadata, trigger_tid, avvikstype) FROM stdin; +\. + + +-- +-- Data for Name: task_logg; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.task_logg (id, task_id, type, node, opprettet_tid, melding, endret_av) FROM stdin; +\. + + +-- +-- Data for Name: tilbakekreving; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.tilbakekreving (id, valg, varsel, begrunnelse, tilbakekrevingsbehandling_id, opprettet_av, opprettet_tid, endret_av, endret_tid, versjon, fk_behandling_id) FROM stdin; +\. + + +-- +-- Data for Name: tilkjent_ytelse; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.tilkjent_ytelse (id, fk_behandling_id, stonad_fom, stonad_tom, opprettet_dato, opphor_fom, utbetalingsoppdrag, endret_dato) FROM stdin; +\. + + +-- +-- Data for Name: totrinnskontroll; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.totrinnskontroll (id, fk_behandling_id, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid, aktiv, saksbehandler, beslutter, godkjent, saksbehandler_id, beslutter_id, kontrollerte_sider) FROM stdin; +\. + + +-- +-- Data for Name: utenlandsk_periodebeloep; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.utenlandsk_periodebeloep (id, fk_behandling_id, fom, tom, intervall, valutakode, beloep, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid, utbetalingsland, kalkulert_maanedlig_beloep) FROM stdin; +\. + + +-- +-- Data for Name: valutakurs; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.valutakurs (id, fk_behandling_id, fom, tom, valutakursdato, valutakode, kurs, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: vedtak; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.vedtak (id, fk_behandling_id, versjon, opprettet_av, opprettet_tid, vedtaksdato, endret_av, endret_tid, aktiv, stonad_brev_pdf) FROM stdin; +\. + + +-- +-- Data for Name: vedtaksbegrunnelse; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.vedtaksbegrunnelse (id, fk_vedtaksperiode_id, vedtak_begrunnelse_spesifikasjon) FROM stdin; +\. + + +-- +-- Data for Name: vedtaksbegrunnelse_fritekst; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.vedtaksbegrunnelse_fritekst (id, fk_vedtaksperiode_id, fritekst) FROM stdin; +\. + + +-- +-- Data for Name: vedtaksperiode; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.vedtaksperiode (id, fk_vedtak_id, fom, tom, type, opprettet_av, opprettet_tid, endret_av, endret_tid, versjon) FROM stdin; +\. + + +-- +-- Data for Name: verge; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.verge (id, ident, fk_behandling_id, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid) FROM stdin; +\. + + +-- +-- Data for Name: vilkaarsvurdering; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.vilkaarsvurdering (id, fk_behandling_id, aktiv, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid, samlet_resultat, ytelse_personer) FROM stdin; +\. + + +-- +-- Data for Name: vilkar_resultat; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.vilkar_resultat (id, vilkar, resultat, regel_input, regel_output, versjon, opprettet_av, opprettet_tid, endret_av, endret_tid, fk_person_resultat_id, begrunnelse, periode_fom, periode_tom, fk_behandling_id, evaluering_aarsak, er_automatisk_vurdert, er_eksplisitt_avslag_paa_soknad, vedtak_begrunnelse_spesifikasjoner, vurderes_etter, utdypende_vilkarsvurderinger, resultat_begrunnelse) FROM stdin; +\. + + +-- +-- Name: andel_tilkjent_ytelse_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.andel_tilkjent_ytelse_seq', 2000000, true); + + +-- +-- Name: annen_vurdering_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.annen_vurdering_seq', 1000000, false); + + +-- +-- Name: arbeidsfordeling_pa_behandling_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.arbeidsfordeling_pa_behandling_seq', 1000000, false); + + +-- +-- Name: batch_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.batch_seq', 1001750, true); + + +-- +-- Name: behandling_migreringsinfo_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.behandling_migreringsinfo_seq', 1000000, false); + + +-- +-- Name: behandling_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.behandling_seq', 1000000, false); + + +-- +-- Name: behandling_soknadsinfo_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.behandling_soknadsinfo_seq', 1000000, false); + + +-- +-- Name: behandling_steg_tilstand_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.behandling_steg_tilstand_seq', 1000000, false); + + +-- +-- Name: brevmottaker_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.brevmottaker_seq', 1000000, false); + + +-- +-- Name: data_chunk_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.data_chunk_seq', 1000000, false); + + +-- +-- Name: endret_utbetaling_andel_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.endret_utbetaling_andel_seq', 1000000, false); + + +-- +-- Name: eos_begrunnelse_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.eos_begrunnelse_seq', 1000000, false); + + +-- +-- Name: fagsak_person_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.fagsak_person_seq', 1000000, false); + + +-- +-- Name: fagsak_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.fagsak_seq', 1000000, false); + + +-- +-- Name: feilutbetalt_valuta_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.feilutbetalt_valuta_seq', 1000000, false); + + +-- +-- Name: foedselshendelse_pre_lansering_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.foedselshendelse_pre_lansering_seq', 1000000, false); + + +-- +-- Name: foedselshendelsefiltrering_resultat_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.foedselshendelsefiltrering_resultat_seq', 1000000, false); + + +-- +-- Name: gr_periode_overgangsstonad_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.gr_periode_overgangsstonad_seq', 1000000, false); + + +-- +-- Name: gr_personopplysninger_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.gr_personopplysninger_seq', 1000000, false); + + +-- +-- Name: gr_soknad_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.gr_soknad_seq', 1000000, false); + + +-- +-- Name: institusjon_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.institusjon_seq', 1000000, false); + + +-- +-- Name: journalpost_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.journalpost_seq', 1000000, false); + + +-- +-- Name: kompetanse_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.kompetanse_seq', 1000000, false); + + +-- +-- Name: korrigert_etterbetaling_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.korrigert_etterbetaling_seq', 1000000, false); + + +-- +-- Name: korrigert_vedtak_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.korrigert_vedtak_seq', 1000000, false); + + +-- +-- Name: logg_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.logg_seq', 1000000, false); + + +-- +-- Name: okonomi_simulering_mottaker_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.okonomi_simulering_mottaker_seq', 1000000, false); + + +-- +-- Name: okonomi_simulering_postering_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.okonomi_simulering_postering_seq', 1000000, false); + + +-- +-- Name: oppgave_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.oppgave_seq', 1000000, false); + + +-- +-- Name: periode_resultat_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.periode_resultat_seq', 1000000, false); + + +-- +-- Name: po_arbeidsforhold_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.po_arbeidsforhold_seq', 1000000, false); + + +-- +-- Name: po_bostedsadresse_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.po_bostedsadresse_seq', 1000000, false); + + +-- +-- Name: po_bostedsadresseperiode_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.po_bostedsadresseperiode_seq', 1000000, false); + + +-- +-- Name: po_doedsfall_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.po_doedsfall_seq', 1000000, false); + + +-- +-- Name: po_opphold_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.po_opphold_seq', 1000000, false); + + +-- +-- Name: po_person_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.po_person_seq', 1000000, false); + + +-- +-- Name: po_sivilstand_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.po_sivilstand_seq', 1000000, false); + + +-- +-- Name: po_statsborgerskap_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.po_statsborgerskap_seq', 1000000, false); + + +-- +-- Name: refusjon_eos_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.refusjon_eos_seq', 1000000, false); + + +-- +-- Name: saksstatistikk_mellomlagring_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.saksstatistikk_mellomlagring_seq', 1000000, false); + + +-- +-- Name: sats_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.sats_seq', 1000000, false); + + +-- +-- Name: satskjoering_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.satskjoering_seq', 1000000, false); + + +-- +-- Name: sett_paa_vent_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.sett_paa_vent_seq', 1000000, false); + + +-- +-- Name: skyggesak_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.skyggesak_seq', 1000000, false); + + +-- +-- Name: task_logg_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.task_logg_seq', 1, false); + + +-- +-- Name: task_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.task_seq', 51, true); + + +-- +-- Name: tilbakekreving_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.tilbakekreving_seq', 1000000, false); + + +-- +-- Name: tilkjent_ytelse_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.tilkjent_ytelse_seq', 1000000, false); + + +-- +-- Name: totrinnskontroll_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.totrinnskontroll_seq', 1000000, false); + + +-- +-- Name: utenlandsk_periodebeloep_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.utenlandsk_periodebeloep_seq', 1000000, false); + + +-- +-- Name: valutakurs_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.valutakurs_seq', 1000000, false); + + +-- +-- Name: vedtak_begrunnelse_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.vedtak_begrunnelse_seq', 1000000, false); + + +-- +-- Name: vedtak_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.vedtak_seq', 1000000, false); + + +-- +-- Name: vedtaksbegrunnelse_fritekst_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.vedtaksbegrunnelse_fritekst_seq', 1000000, false); + + +-- +-- Name: vedtaksbegrunnelse_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.vedtaksbegrunnelse_seq', 1000000, false); + + +-- +-- Name: vedtaksperiode_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.vedtaksperiode_seq', 1000000, false); + + +-- +-- Name: verge_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.verge_seq', 1000000, false); + + +-- +-- Name: vilkaarsvurdering_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.vilkaarsvurdering_seq', 1000000, false); + + +-- +-- Name: vilkar_resultat_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.vilkar_resultat_seq', 1000000, false); + + +-- +-- Name: aktoer aktoer_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aktoer + ADD CONSTRAINT aktoer_pkey PRIMARY KEY (aktoer_id); + + +-- +-- Name: aktoer_til_kompetanse aktoer_til_kompetanse_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aktoer_til_kompetanse + ADD CONSTRAINT aktoer_til_kompetanse_pkey PRIMARY KEY (fk_kompetanse_id, fk_aktoer_id); + + +-- +-- Name: aktoer_til_utenlandsk_periodebeloep aktoer_til_utenlandsk_periodebeloep_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aktoer_til_utenlandsk_periodebeloep + ADD CONSTRAINT aktoer_til_utenlandsk_periodebeloep_pkey PRIMARY KEY (fk_utenlandsk_periodebeloep_id, fk_aktoer_id); + + +-- +-- Name: aktoer_til_valutakurs aktoer_til_valutakurs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aktoer_til_valutakurs + ADD CONSTRAINT aktoer_til_valutakurs_pkey PRIMARY KEY (fk_valutakurs_id, fk_aktoer_id); + + +-- +-- Name: andel_tilkjent_ytelse andel_tilkjent_ytelse_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.andel_tilkjent_ytelse + ADD CONSTRAINT andel_tilkjent_ytelse_pkey PRIMARY KEY (id); + + +-- +-- Name: annen_vurdering annen_vurdering_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.annen_vurdering + ADD CONSTRAINT annen_vurdering_pkey PRIMARY KEY (id); + + +-- +-- Name: arbeidsfordeling_pa_behandling arbeidsfordeling_pa_behandling_fk_behandling_id_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.arbeidsfordeling_pa_behandling + ADD CONSTRAINT arbeidsfordeling_pa_behandling_fk_behandling_id_key UNIQUE (fk_behandling_id); + + +-- +-- Name: arbeidsfordeling_pa_behandling arbeidsfordeling_pa_behandling_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.arbeidsfordeling_pa_behandling + ADD CONSTRAINT arbeidsfordeling_pa_behandling_pkey PRIMARY KEY (id); + + +-- +-- Name: batch batch_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.batch + ADD CONSTRAINT batch_pkey PRIMARY KEY (id); + + +-- +-- Name: behandling_migreringsinfo behandling_migreringsinfo_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.behandling_migreringsinfo + ADD CONSTRAINT behandling_migreringsinfo_pkey PRIMARY KEY (id); + + +-- +-- Name: behandling behandling_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.behandling + ADD CONSTRAINT behandling_pkey PRIMARY KEY (id); + + +-- +-- Name: vilkaarsvurdering behandling_resultat_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vilkaarsvurdering + ADD CONSTRAINT behandling_resultat_pkey PRIMARY KEY (id); + + +-- +-- Name: behandling_soknadsinfo behandling_soknadsinfo_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.behandling_soknadsinfo + ADD CONSTRAINT behandling_soknadsinfo_pkey PRIMARY KEY (id); + + +-- +-- Name: behandling_steg_tilstand behandling_steg_tilstand_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.behandling_steg_tilstand + ADD CONSTRAINT behandling_steg_tilstand_pkey PRIMARY KEY (id); + + +-- +-- Name: vedtak behandling_vedtak_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vedtak + ADD CONSTRAINT behandling_vedtak_pkey PRIMARY KEY (id); + + +-- +-- Name: tilkjent_ytelse beregning_resultat_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tilkjent_ytelse + ADD CONSTRAINT beregning_resultat_pkey PRIMARY KEY (id); + + +-- +-- Name: brevmottaker brevmottaker_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.brevmottaker + ADD CONSTRAINT brevmottaker_pkey PRIMARY KEY (id); + + +-- +-- Name: data_chunk data_chunk_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.data_chunk + ADD CONSTRAINT data_chunk_pkey PRIMARY KEY (id); + + +-- +-- Name: endret_utbetaling_andel endret_utbetaling_andel_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.endret_utbetaling_andel + ADD CONSTRAINT endret_utbetaling_andel_pkey PRIMARY KEY (id); + + +-- +-- Name: eos_begrunnelse eos_begrunnelse_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.eos_begrunnelse + ADD CONSTRAINT eos_begrunnelse_pkey PRIMARY KEY (id); + + +-- +-- Name: fagsak fagsak_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.fagsak + ADD CONSTRAINT fagsak_pkey PRIMARY KEY (id); + + +-- +-- Name: foedselshendelse_pre_lansering foedselshendelse_pre_lansering_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.foedselshendelse_pre_lansering + ADD CONSTRAINT foedselshendelse_pre_lansering_pkey PRIMARY KEY (id); + + +-- +-- Name: foedselshendelsefiltrering_resultat foedselshendelsefiltrering_resultat_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.foedselshendelsefiltrering_resultat + ADD CONSTRAINT foedselshendelsefiltrering_resultat_pkey PRIMARY KEY (id); + + +-- +-- Name: gr_periode_overgangsstonad gr_periode_overgangsstonad_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.gr_periode_overgangsstonad + ADD CONSTRAINT gr_periode_overgangsstonad_pkey PRIMARY KEY (id); + + +-- +-- Name: gr_personopplysninger gr_personopplysninger_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.gr_personopplysninger + ADD CONSTRAINT gr_personopplysninger_pkey PRIMARY KEY (id); + + +-- +-- Name: gr_soknad gr_soknad_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.gr_soknad + ADD CONSTRAINT gr_soknad_pkey PRIMARY KEY (id); + + +-- +-- Name: task_logg henvendelse_logg_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.task_logg + ADD CONSTRAINT henvendelse_logg_pkey PRIMARY KEY (id); + + +-- +-- Name: task henvendelse_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.task + ADD CONSTRAINT henvendelse_pkey PRIMARY KEY (id); + + +-- +-- Name: institusjon institusjon_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.institusjon + ADD CONSTRAINT institusjon_pkey PRIMARY KEY (id); + + +-- +-- Name: journalpost journalpost_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.journalpost + ADD CONSTRAINT journalpost_pkey PRIMARY KEY (id); + + +-- +-- Name: kompetanse kompetanse_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.kompetanse + ADD CONSTRAINT kompetanse_pkey PRIMARY KEY (id); + + +-- +-- Name: korrigert_etterbetaling korrigert_etterbetaling_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.korrigert_etterbetaling + ADD CONSTRAINT korrigert_etterbetaling_pkey PRIMARY KEY (id); + + +-- +-- Name: korrigert_vedtak korrigert_vedtak_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.korrigert_vedtak + ADD CONSTRAINT korrigert_vedtak_pkey PRIMARY KEY (id); + + +-- +-- Name: logg logg_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.logg + ADD CONSTRAINT logg_pkey PRIMARY KEY (id); + + +-- +-- Name: oppgave oppgave_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oppgave + ADD CONSTRAINT oppgave_pkey PRIMARY KEY (id); + + +-- +-- Name: person_resultat periode_resultat_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.person_resultat + ADD CONSTRAINT periode_resultat_pkey PRIMARY KEY (id); + + +-- +-- Name: personident personident_foedselsnummer_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.personident + ADD CONSTRAINT personident_foedselsnummer_key UNIQUE (foedselsnummer); + + +-- +-- Name: personident personident_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.personident + ADD CONSTRAINT personident_pkey PRIMARY KEY (foedselsnummer); + + +-- +-- Name: po_arbeidsforhold po_arbeidsforhold_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_arbeidsforhold + ADD CONSTRAINT po_arbeidsforhold_pkey PRIMARY KEY (id); + + +-- +-- Name: po_bostedsadresse po_bostedsadresse_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_bostedsadresse + ADD CONSTRAINT po_bostedsadresse_pkey PRIMARY KEY (id); + + +-- +-- Name: po_bostedsadresseperiode po_bostedsadresseperiode_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_bostedsadresseperiode + ADD CONSTRAINT po_bostedsadresseperiode_pkey PRIMARY KEY (id); + + +-- +-- Name: po_doedsfall po_doedsfall_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_doedsfall + ADD CONSTRAINT po_doedsfall_pkey PRIMARY KEY (id); + + +-- +-- Name: po_opphold po_opphold_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_opphold + ADD CONSTRAINT po_opphold_pkey PRIMARY KEY (id); + + +-- +-- Name: po_person po_person_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_person + ADD CONSTRAINT po_person_pkey PRIMARY KEY (id); + + +-- +-- Name: po_sivilstand po_sivilstand_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_sivilstand + ADD CONSTRAINT po_sivilstand_pkey PRIMARY KEY (id); + + +-- +-- Name: po_statsborgerskap po_statsborgerskap_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_statsborgerskap + ADD CONSTRAINT po_statsborgerskap_pkey PRIMARY KEY (id); + + +-- +-- Name: refusjon_eos refusjon_eos_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.refusjon_eos + ADD CONSTRAINT refusjon_eos_pkey PRIMARY KEY (id); + + +-- +-- Name: saksstatistikk_mellomlagring saksstatistikk_mellomlagring_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.saksstatistikk_mellomlagring + ADD CONSTRAINT saksstatistikk_mellomlagring_pkey PRIMARY KEY (id); + + +-- +-- Name: satskjoering satskjoering_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.satskjoering + ADD CONSTRAINT satskjoering_pkey PRIMARY KEY (id); + + +-- +-- Name: sett_paa_vent sett_paa_vent_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sett_paa_vent + ADD CONSTRAINT sett_paa_vent_pkey PRIMARY KEY (id); + + +-- +-- Name: skyggesak skyggesak_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.skyggesak + ADD CONSTRAINT skyggesak_pkey PRIMARY KEY (id); + + +-- +-- Name: tilbakekreving tilbakekreving_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tilbakekreving + ADD CONSTRAINT tilbakekreving_pkey PRIMARY KEY (id); + + +-- +-- Name: totrinnskontroll totrinnskontroll_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.totrinnskontroll + ADD CONSTRAINT totrinnskontroll_pkey PRIMARY KEY (id); + + +-- +-- Name: feilutbetalt_valuta trekk_i_loepende_utbetaling_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.feilutbetalt_valuta + ADD CONSTRAINT trekk_i_loepende_utbetaling_pkey PRIMARY KEY (id); + + +-- +-- Name: behandling_migreringsinfo unik_behandling_id; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.behandling_migreringsinfo + ADD CONSTRAINT unik_behandling_id UNIQUE (fk_behandling_id); + + +-- +-- Name: utenlandsk_periodebeloep utenlandsk_periodebeloep_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.utenlandsk_periodebeloep + ADD CONSTRAINT utenlandsk_periodebeloep_pkey PRIMARY KEY (id); + + +-- +-- Name: valutakurs valutakurs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.valutakurs + ADD CONSTRAINT valutakurs_pkey PRIMARY KEY (id); + + +-- +-- Name: okonomi_simulering_mottaker vedtak_simulering_mottaker_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.okonomi_simulering_mottaker + ADD CONSTRAINT vedtak_simulering_mottaker_pkey PRIMARY KEY (id); + + +-- +-- Name: okonomi_simulering_postering vedtak_simulering_postering_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.okonomi_simulering_postering + ADD CONSTRAINT vedtak_simulering_postering_pkey PRIMARY KEY (id); + + +-- +-- Name: vedtaksbegrunnelse_fritekst vedtaksbegrunnelse_fritekst_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vedtaksbegrunnelse_fritekst + ADD CONSTRAINT vedtaksbegrunnelse_fritekst_pkey PRIMARY KEY (id); + + +-- +-- Name: vedtaksbegrunnelse vedtaksbegrunnelse_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vedtaksbegrunnelse + ADD CONSTRAINT vedtaksbegrunnelse_pkey PRIMARY KEY (id); + + +-- +-- Name: vedtaksperiode vedtaksperiode_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vedtaksperiode + ADD CONSTRAINT vedtaksperiode_pkey PRIMARY KEY (id); + + +-- +-- Name: verge verge_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.verge + ADD CONSTRAINT verge_pkey PRIMARY KEY (id); + + +-- +-- Name: vilkar_resultat vilkar_resultat_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vilkar_resultat + ADD CONSTRAINT vilkar_resultat_pkey PRIMARY KEY (id); + + +-- +-- Name: andel_tilkjent_ytelse_fk_aktoer_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX andel_tilkjent_ytelse_fk_aktoer_idx ON public.andel_tilkjent_ytelse USING btree (fk_aktoer_id); + + +-- +-- Name: andel_tilkjent_ytelse_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX andel_tilkjent_ytelse_fk_behandling_id_idx ON public.andel_tilkjent_ytelse USING btree (fk_behandling_id); + + +-- +-- Name: andel_tilkjent_ytelse_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX andel_tilkjent_ytelse_fk_idx ON public.andel_tilkjent_ytelse USING btree (kilde_behandling_id); + + +-- +-- Name: andel_tilkjent_ytelse_fk_tilkjent_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX andel_tilkjent_ytelse_fk_tilkjent_idx ON public.andel_tilkjent_ytelse USING btree (tilkjent_ytelse_id); + + +-- +-- Name: annen_vurdering_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX annen_vurdering_fk_idx ON public.annen_vurdering USING btree (fk_person_resultat_id); + + +-- +-- Name: aty_type_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX aty_type_idx ON public.andel_tilkjent_ytelse USING btree (type); + + +-- +-- Name: behandling_fk_fagsak_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX behandling_fk_fagsak_id_idx ON public.behandling USING btree (fk_fagsak_id); + + +-- +-- Name: behandling_migreringsinfo_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX behandling_migreringsinfo_fk_behandling_id_idx ON public.behandling_migreringsinfo USING btree (fk_behandling_id); + + +-- +-- Name: behandling_opprettet_tid_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX behandling_opprettet_tid_idx ON public.behandling USING btree (opprettet_tid); + + +-- +-- Name: behandling_soknadsinfo_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX behandling_soknadsinfo_fk_behandling_id_idx ON public.behandling_soknadsinfo USING btree (fk_behandling_id); + + +-- +-- Name: behandling_steg_tilstand_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX behandling_steg_tilstand_fk_idx ON public.behandling_steg_tilstand USING btree (fk_behandling_id); + + +-- +-- Name: behandling_vedtak_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX behandling_vedtak_fk_behandling_id_idx ON public.vedtak USING btree (fk_behandling_id); + + +-- +-- Name: beregning_resultat_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX beregning_resultat_fk_behandling_id_idx ON public.tilkjent_ytelse USING btree (fk_behandling_id); + + +-- +-- Name: brevmottaker_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX brevmottaker_fk_behandling_id_idx ON public.brevmottaker USING btree (fk_behandling_id); + + +-- +-- Name: data_chunk_transaksjons_id_chunk_nr_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX data_chunk_transaksjons_id_chunk_nr_idx ON public.data_chunk USING btree (transaksjons_id, chunk_nr); + + +-- +-- Name: data_chunk_transaksjons_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX data_chunk_transaksjons_id_idx ON public.data_chunk USING btree (transaksjons_id); + + +-- +-- Name: endret_utbetaling_andel_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX endret_utbetaling_andel_fk_behandling_id_idx ON public.endret_utbetaling_andel USING btree (fk_behandling_id); + + +-- +-- Name: endret_utbetaling_andel_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX endret_utbetaling_andel_fk_idx ON public.endret_utbetaling_andel USING btree (fk_po_person_id); + + +-- +-- Name: eos_begrunnelse_fk_vedtaksperiode_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX eos_begrunnelse_fk_vedtaksperiode_id_idx ON public.eos_begrunnelse USING btree (fk_vedtaksperiode_id); + + +-- +-- Name: fagsak_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX fagsak_fk_idx ON public.fagsak USING btree (fk_aktoer_id); + + +-- +-- Name: fagsak_status_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX fagsak_status_idx ON public.fagsak USING btree (status); + + +-- +-- Name: feilutbetalt_valuta_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX feilutbetalt_valuta_fk_behandling_id_idx ON public.feilutbetalt_valuta USING btree (fk_behandling_id); + + +-- +-- Name: foedselshendelse_pre_lansering_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX foedselshendelse_pre_lansering_fk_idx ON public.foedselshendelse_pre_lansering USING btree (fk_aktoer_id); + + +-- +-- Name: foedselshendelsefiltrering_resultat_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX foedselshendelsefiltrering_resultat_fk_behandling_id_idx ON public.foedselshendelsefiltrering_resultat USING btree (fk_behandling_id); + + +-- +-- Name: gr_periode_overgangsstonad_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX gr_periode_overgangsstonad_fk_behandling_id_idx ON public.gr_periode_overgangsstonad USING btree (fk_behandling_id); + + +-- +-- Name: gr_periode_overgangsstonad_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX gr_periode_overgangsstonad_fk_idx ON public.gr_periode_overgangsstonad USING btree (fk_aktoer_id); + + +-- +-- Name: gr_personopplysninger_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX gr_personopplysninger_fk_behandling_id_idx ON public.gr_personopplysninger USING btree (fk_behandling_id); + + +-- +-- Name: gr_soknad_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX gr_soknad_fk_behandling_id_idx ON public.gr_soknad USING btree (fk_behandling_id); + + +-- +-- Name: henvendelse_logg_henvendelse_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX henvendelse_logg_henvendelse_id_idx ON public.task_logg USING btree (task_id); + + +-- +-- Name: henvendelse_status_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX henvendelse_status_idx ON public.task USING btree (status); + + +-- +-- Name: journalpost_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX journalpost_fk_behandling_id_idx ON public.journalpost USING btree (fk_behandling_id); + + +-- +-- Name: journalpost_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX journalpost_id_idx ON public.behandling_soknadsinfo USING btree (journalpost_id); + + +-- +-- Name: kompetanse_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX kompetanse_fk_behandling_id_idx ON public.kompetanse USING btree (fk_behandling_id); + + +-- +-- Name: korrigert_etterbetaling_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX korrigert_etterbetaling_fk_behandling_id_idx ON public.korrigert_etterbetaling USING btree (fk_behandling_id); + + +-- +-- Name: korrigert_vedtak_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX korrigert_vedtak_fk_behandling_id_idx ON public.korrigert_vedtak USING btree (fk_behandling_id); + + +-- +-- Name: logg_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX logg_fk_behandling_id_idx ON public.logg USING btree (fk_behandling_id); + + +-- +-- Name: okonomi_simulering_mottaker_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX okonomi_simulering_mottaker_fk_idx ON public.okonomi_simulering_mottaker USING btree (fk_behandling_id); + + +-- +-- Name: oppgave_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX oppgave_fk_idx ON public.oppgave USING btree (fk_behandling_id); + + +-- +-- Name: person_resultat_fk_aktoer_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX person_resultat_fk_aktoer_idx ON public.person_resultat USING btree (fk_aktoer_id); + + +-- +-- Name: person_resultat_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX person_resultat_fk_idx ON public.person_resultat USING btree (fk_vilkaarsvurdering_id); + + +-- +-- Name: personident_aktoer_id_alle_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX personident_aktoer_id_alle_idx ON public.personident USING btree (fk_aktoer_id); + + +-- +-- Name: po_arbeidsforhold_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX po_arbeidsforhold_fk_idx ON public.po_arbeidsforhold USING btree (fk_po_person_id); + + +-- +-- Name: po_bostedsadresse_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX po_bostedsadresse_fk_idx ON public.po_bostedsadresse USING btree (fk_po_person_id); + + +-- +-- Name: po_bostedsadresseperiode_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX po_bostedsadresseperiode_fk_idx ON public.po_bostedsadresseperiode USING btree (fk_po_person_id); + + +-- +-- Name: po_doedsfall_fk_po_person_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX po_doedsfall_fk_po_person_id_idx ON public.po_doedsfall USING btree (fk_po_person_id); + + +-- +-- Name: po_opphold_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX po_opphold_fk_idx ON public.po_opphold USING btree (fk_po_person_id); + + +-- +-- Name: po_person_fk_gr_personopplysninger_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX po_person_fk_gr_personopplysninger_id_idx ON public.po_person USING btree (fk_gr_personopplysninger_id); + + +-- +-- Name: po_person_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX po_person_fk_idx ON public.po_person USING btree (fk_aktoer_id); + + +-- +-- Name: po_sivilstand_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX po_sivilstand_fk_idx ON public.po_sivilstand USING btree (fk_po_person_id); + + +-- +-- Name: po_statsborgerskap_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX po_statsborgerskap_fk_idx ON public.po_statsborgerskap USING btree (fk_po_person_id); + + +-- +-- Name: refusjon_eos_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX refusjon_eos_fk_behandling_id_idx ON public.refusjon_eos USING btree (fk_behandling_id); + + +-- +-- Name: saksstatistikk_mellomlagring_sendt_tid_null_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX saksstatistikk_mellomlagring_sendt_tid_null_idx ON public.saksstatistikk_mellomlagring USING btree (sendt_tid) WHERE (sendt_tid IS NULL); + + +-- +-- Name: saksstatistikk_mellomlagring_type_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX saksstatistikk_mellomlagring_type_id_idx ON public.saksstatistikk_mellomlagring USING btree (type_id); + + +-- +-- Name: satskjoering_fagsak_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX satskjoering_fagsak_id_idx ON public.satskjoering USING btree (fk_fagsak_id); + + +-- +-- Name: sett_paa_vent_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX sett_paa_vent_fk_behandling_id_idx ON public.sett_paa_vent USING btree (fk_behandling_id); + + +-- +-- Name: skyggesak_fagsak_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX skyggesak_fagsak_id_idx ON public.skyggesak USING btree (fk_fagsak_id); + + +-- +-- Name: tilbakekreving_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX tilbakekreving_fk_idx ON public.tilbakekreving USING btree (fk_behandling_id); + + +-- +-- Name: tilkjent_ytelse_utbetalingsoppdrag_not_null_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX tilkjent_ytelse_utbetalingsoppdrag_not_null_idx ON public.tilkjent_ytelse USING btree (utbetalingsoppdrag) WHERE (utbetalingsoppdrag IS NOT NULL); + + +-- +-- Name: totrinnskontroll_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX totrinnskontroll_fk_behandling_id_idx ON public.totrinnskontroll USING btree (fk_behandling_id); + + +-- +-- Name: uidx_behandling_01; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_behandling_01 ON public.behandling USING btree (( +CASE + WHEN (aktiv = true) THEN fk_fagsak_id + ELSE NULL::bigint +END), ( +CASE + WHEN (aktiv = true) THEN aktiv + ELSE NULL::boolean +END)); + + +-- +-- Name: uidx_behandling_02; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_behandling_02 ON public.behandling USING btree (fk_fagsak_id) WHERE (((status)::text <> 'AVSLUTTET'::text) AND ((status)::text <> 'SATT_PÅ_MASKINELL_VENT'::text)); + + +-- +-- Name: uidx_behandling_03; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_behandling_03 ON public.behandling USING btree (fk_fagsak_id) WHERE ((status)::text = 'SATT_PÅ_MASKINELL_VENT'::text); + + +-- +-- Name: uidx_behandling_vedtak_01; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_behandling_vedtak_01 ON public.vedtak USING btree (( +CASE + WHEN (aktiv = true) THEN fk_behandling_id + ELSE NULL::bigint +END), ( +CASE + WHEN (aktiv = true) THEN aktiv + ELSE NULL::boolean +END)); + + +-- +-- Name: uidx_fagsak_type_aktoer_ikke_arkivert; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_fagsak_type_aktoer_ikke_arkivert ON public.fagsak USING btree (type, fk_aktoer_id) WHERE ((fk_institusjon_id IS NULL) AND (arkivert = false)); + + +-- +-- Name: uidx_fagsak_type_aktoer_institusjon_ikke_arkivert; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_fagsak_type_aktoer_institusjon_ikke_arkivert ON public.fagsak USING btree (type, fk_aktoer_id, fk_institusjon_id) WHERE ((fk_institusjon_id IS NOT NULL) AND (arkivert = false)); + + +-- +-- Name: uidx_gr_personopplysninger_01; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_gr_personopplysninger_01 ON public.gr_personopplysninger USING btree (( +CASE + WHEN (aktiv = true) THEN fk_behandling_id + ELSE NULL::bigint +END), ( +CASE + WHEN (aktiv = true) THEN aktiv + ELSE NULL::boolean +END)); + + +-- +-- Name: uidx_gr_soknad_01; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_gr_soknad_01 ON public.gr_soknad USING btree (( +CASE + WHEN (aktiv = true) THEN fk_behandling_id + ELSE NULL::bigint +END), ( +CASE + WHEN (aktiv = true) THEN aktiv + ELSE NULL::boolean +END)); + + +-- +-- Name: uidx_institusjon_org_nummer; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_institusjon_org_nummer ON public.institusjon USING btree (org_nummer); + + +-- +-- Name: uidx_institusjon_tss_ekstern_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_institusjon_tss_ekstern_id ON public.institusjon USING btree (tss_ekstern_id); + + +-- +-- Name: uidx_korrigert_etterbetaling_fk_behandling_id_aktiv; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_korrigert_etterbetaling_fk_behandling_id_aktiv ON public.korrigert_etterbetaling USING btree (fk_behandling_id) WHERE (aktiv = true); + + +-- +-- Name: uidx_korrigert_vedtak_fk_behandling_id_aktiv; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_korrigert_vedtak_fk_behandling_id_aktiv ON public.korrigert_vedtak USING btree (fk_behandling_id) WHERE (aktiv = true); + + +-- +-- Name: uidx_personident_aktoer_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_personident_aktoer_id ON public.personident USING btree (fk_aktoer_id) WHERE (aktiv = true); + + +-- +-- Name: uidx_personident_foedselsnummer_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_personident_foedselsnummer_id ON public.personident USING btree (foedselsnummer); + + +-- +-- Name: uidx_sett_paa_vent_aktiv; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_sett_paa_vent_aktiv ON public.sett_paa_vent USING btree (fk_behandling_id, aktiv) WHERE (aktiv = true); + + +-- +-- Name: uidx_totrinnskontroll_01; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_totrinnskontroll_01 ON public.totrinnskontroll USING btree (( +CASE + WHEN (aktiv = true) THEN fk_behandling_id + ELSE NULL::bigint +END), ( +CASE + WHEN (aktiv = true) THEN aktiv + ELSE NULL::boolean +END)); + + +-- +-- Name: uidx_verge_behandling_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX uidx_verge_behandling_id ON public.verge USING btree (fk_behandling_id); + + +-- +-- Name: utenlandsk_periodebeloep_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX utenlandsk_periodebeloep_fk_behandling_id_idx ON public.utenlandsk_periodebeloep USING btree (fk_behandling_id); + + +-- +-- Name: valutakurs_fk_behandling_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX valutakurs_fk_behandling_id_idx ON public.valutakurs USING btree (fk_behandling_id); + + +-- +-- Name: vedtak_simulering_postering_fk_vedtak_simulering_mottaker_i_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX vedtak_simulering_postering_fk_vedtak_simulering_mottaker_i_idx ON public.okonomi_simulering_postering USING btree (fk_okonomi_simulering_mottaker_id); + + +-- +-- Name: vedtaksbegrunnelse_fk_vedtaksperiode_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX vedtaksbegrunnelse_fk_vedtaksperiode_id_idx ON public.vedtaksbegrunnelse USING btree (fk_vedtaksperiode_id); + + +-- +-- Name: vedtaksbegrunnelse_fritekst_fk_vedtaksperiode_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX vedtaksbegrunnelse_fritekst_fk_vedtaksperiode_id_idx ON public.vedtaksbegrunnelse_fritekst USING btree (fk_vedtaksperiode_id); + + +-- +-- Name: vedtaksperiode_fk_vedtak_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX vedtaksperiode_fk_vedtak_id_idx ON public.vedtaksperiode USING btree (fk_vedtak_id); + + +-- +-- Name: vilkaarsvurdering_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX vilkaarsvurdering_fk_idx ON public.vilkaarsvurdering USING btree (fk_behandling_id); + + +-- +-- Name: vilkar_resultat_fk_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX vilkar_resultat_fk_idx ON public.vilkar_resultat USING btree (fk_behandling_id); + + +-- +-- Name: vilkar_resultat_fk_personr_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX vilkar_resultat_fk_personr_idx ON public.vilkar_resultat USING btree (fk_person_resultat_id); + + +-- +-- Name: aktoer_til_kompetanse aktoer_til_kompetanse_fk_aktoer_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aktoer_til_kompetanse + ADD CONSTRAINT aktoer_til_kompetanse_fk_aktoer_id_fkey FOREIGN KEY (fk_aktoer_id) REFERENCES public.aktoer(aktoer_id) ON UPDATE CASCADE; + + +-- +-- Name: aktoer_til_kompetanse aktoer_til_kompetanse_fk_kompetanse_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aktoer_til_kompetanse + ADD CONSTRAINT aktoer_til_kompetanse_fk_kompetanse_id_fkey FOREIGN KEY (fk_kompetanse_id) REFERENCES public.kompetanse(id); + + +-- +-- Name: aktoer_til_utenlandsk_periodebeloep aktoer_til_utenlandsk_periode_fk_utenlandsk_periodebeloep__fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aktoer_til_utenlandsk_periodebeloep + ADD CONSTRAINT aktoer_til_utenlandsk_periode_fk_utenlandsk_periodebeloep__fkey FOREIGN KEY (fk_utenlandsk_periodebeloep_id) REFERENCES public.utenlandsk_periodebeloep(id); + + +-- +-- Name: aktoer_til_utenlandsk_periodebeloep aktoer_til_utenlandsk_periodebeloep_fk_aktoer_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aktoer_til_utenlandsk_periodebeloep + ADD CONSTRAINT aktoer_til_utenlandsk_periodebeloep_fk_aktoer_id_fkey FOREIGN KEY (fk_aktoer_id) REFERENCES public.aktoer(aktoer_id) ON UPDATE CASCADE; + + +-- +-- Name: aktoer_til_valutakurs aktoer_til_valutakurs_fk_aktoer_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aktoer_til_valutakurs + ADD CONSTRAINT aktoer_til_valutakurs_fk_aktoer_id_fkey FOREIGN KEY (fk_aktoer_id) REFERENCES public.aktoer(aktoer_id) ON UPDATE CASCADE; + + +-- +-- Name: aktoer_til_valutakurs aktoer_til_valutakurs_fk_valutakurs_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aktoer_til_valutakurs + ADD CONSTRAINT aktoer_til_valutakurs_fk_valutakurs_id_fkey FOREIGN KEY (fk_valutakurs_id) REFERENCES public.valutakurs(id); + + +-- +-- Name: andel_tilkjent_ytelse andel_tilkjent_ytelse_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.andel_tilkjent_ytelse + ADD CONSTRAINT andel_tilkjent_ytelse_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: andel_tilkjent_ytelse andel_tilkjent_ytelse_kilde_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.andel_tilkjent_ytelse + ADD CONSTRAINT andel_tilkjent_ytelse_kilde_behandling_id_fkey FOREIGN KEY (kilde_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: andel_tilkjent_ytelse andel_tilkjent_ytelse_tilkjent_ytelse_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.andel_tilkjent_ytelse + ADD CONSTRAINT andel_tilkjent_ytelse_tilkjent_ytelse_id_fkey FOREIGN KEY (tilkjent_ytelse_id) REFERENCES public.tilkjent_ytelse(id) ON DELETE CASCADE; + + +-- +-- Name: annen_vurdering annen_vurdering_fk_person_resultat_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.annen_vurdering + ADD CONSTRAINT annen_vurdering_fk_person_resultat_id_fkey FOREIGN KEY (fk_person_resultat_id) REFERENCES public.person_resultat(id); + + +-- +-- Name: behandling behandling_fk_fagsak_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.behandling + ADD CONSTRAINT behandling_fk_fagsak_id_fkey FOREIGN KEY (fk_fagsak_id) REFERENCES public.fagsak(id); + + +-- +-- Name: behandling_migreringsinfo behandling_migreringsinfo_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.behandling_migreringsinfo + ADD CONSTRAINT behandling_migreringsinfo_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: vilkaarsvurdering behandling_resultat_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vilkaarsvurdering + ADD CONSTRAINT behandling_resultat_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: behandling_soknadsinfo behandling_soknadsinfo_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.behandling_soknadsinfo + ADD CONSTRAINT behandling_soknadsinfo_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: behandling_steg_tilstand behandling_steg_tilstand_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.behandling_steg_tilstand + ADD CONSTRAINT behandling_steg_tilstand_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: vedtak behandling_vedtak_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vedtak + ADD CONSTRAINT behandling_vedtak_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: tilkjent_ytelse beregning_resultat_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tilkjent_ytelse + ADD CONSTRAINT beregning_resultat_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: brevmottaker brevmottaker_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.brevmottaker + ADD CONSTRAINT brevmottaker_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id) ON DELETE CASCADE; + + +-- +-- Name: data_chunk data_chunk_fk_batch_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.data_chunk + ADD CONSTRAINT data_chunk_fk_batch_id_fkey FOREIGN KEY (fk_batch_id) REFERENCES public.batch(id); + + +-- +-- Name: endret_utbetaling_andel endret_utbetaling_andel_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.endret_utbetaling_andel + ADD CONSTRAINT endret_utbetaling_andel_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: endret_utbetaling_andel endret_utbetaling_andel_fk_po_person_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.endret_utbetaling_andel + ADD CONSTRAINT endret_utbetaling_andel_fk_po_person_id_fkey FOREIGN KEY (fk_po_person_id) REFERENCES public.po_person(id); + + +-- +-- Name: eos_begrunnelse eos_begrunnelse_fk_vedtaksperiode_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.eos_begrunnelse + ADD CONSTRAINT eos_begrunnelse_fk_vedtaksperiode_id_fkey FOREIGN KEY (fk_vedtaksperiode_id) REFERENCES public.vedtaksperiode(id) ON DELETE CASCADE; + + +-- +-- Name: fagsak fagsak; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.fagsak + ADD CONSTRAINT fagsak FOREIGN KEY (fk_aktoer_id) REFERENCES public.aktoer(aktoer_id) ON UPDATE CASCADE; + + +-- +-- Name: fagsak fagsak_fk_institusjon_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.fagsak + ADD CONSTRAINT fagsak_fk_institusjon_id_fkey FOREIGN KEY (fk_institusjon_id) REFERENCES public.institusjon(id); + + +-- +-- Name: andel_tilkjent_ytelse fk_andel_tilkjent_ytelse; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.andel_tilkjent_ytelse + ADD CONSTRAINT fk_andel_tilkjent_ytelse FOREIGN KEY (fk_aktoer_id) REFERENCES public.aktoer(aktoer_id) ON UPDATE CASCADE; + + +-- +-- Name: vilkar_resultat fk_behandling_id_vilkar_resultat; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vilkar_resultat + ADD CONSTRAINT fk_behandling_id_vilkar_resultat FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: foedselshendelse_pre_lansering fk_foedselshendelse_pre_lansering; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.foedselshendelse_pre_lansering + ADD CONSTRAINT fk_foedselshendelse_pre_lansering FOREIGN KEY (fk_aktoer_id) REFERENCES public.aktoer(aktoer_id) ON UPDATE CASCADE; + + +-- +-- Name: gr_periode_overgangsstonad fk_gr_periode_overgangsstonad; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.gr_periode_overgangsstonad + ADD CONSTRAINT fk_gr_periode_overgangsstonad FOREIGN KEY (fk_aktoer_id) REFERENCES public.aktoer(aktoer_id) ON UPDATE CASCADE; + + +-- +-- Name: person_resultat fk_person_resultat; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.person_resultat + ADD CONSTRAINT fk_person_resultat FOREIGN KEY (fk_aktoer_id) REFERENCES public.aktoer(aktoer_id) ON UPDATE CASCADE; + + +-- +-- Name: personident fk_personident; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.personident + ADD CONSTRAINT fk_personident FOREIGN KEY (fk_aktoer_id) REFERENCES public.aktoer(aktoer_id) ON UPDATE CASCADE; + + +-- +-- Name: po_person fk_po_person; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_person + ADD CONSTRAINT fk_po_person FOREIGN KEY (fk_aktoer_id) REFERENCES public.aktoer(aktoer_id) ON UPDATE CASCADE; + + +-- +-- Name: foedselshendelsefiltrering_resultat foedselshendelsefiltrering_resultat_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.foedselshendelsefiltrering_resultat + ADD CONSTRAINT foedselshendelsefiltrering_resultat_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: gr_periode_overgangsstonad gr_periode_overgangsstonad_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.gr_periode_overgangsstonad + ADD CONSTRAINT gr_periode_overgangsstonad_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: gr_personopplysninger gr_personopplysninger_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.gr_personopplysninger + ADD CONSTRAINT gr_personopplysninger_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: gr_soknad gr_soknad_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.gr_soknad + ADD CONSTRAINT gr_soknad_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: task_logg henvendelse_logg_henvendelse_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.task_logg + ADD CONSTRAINT henvendelse_logg_henvendelse_id_fkey FOREIGN KEY (task_id) REFERENCES public.task(id); + + +-- +-- Name: journalpost journalpost_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.journalpost + ADD CONSTRAINT journalpost_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: kompetanse kompetanse_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.kompetanse + ADD CONSTRAINT kompetanse_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: korrigert_etterbetaling korrigert_etterbetaling_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.korrigert_etterbetaling + ADD CONSTRAINT korrigert_etterbetaling_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: korrigert_vedtak korrigert_vedtak_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.korrigert_vedtak + ADD CONSTRAINT korrigert_vedtak_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: logg logg_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.logg + ADD CONSTRAINT logg_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: okonomi_simulering_mottaker okonomi_simulering_mottaker_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.okonomi_simulering_mottaker + ADD CONSTRAINT okonomi_simulering_mottaker_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: oppgave oppgave_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.oppgave + ADD CONSTRAINT oppgave_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: person_resultat periode_resultat_fk_behandling_resultat_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.person_resultat + ADD CONSTRAINT periode_resultat_fk_behandling_resultat_id_fkey FOREIGN KEY (fk_vilkaarsvurdering_id) REFERENCES public.vilkaarsvurdering(id); + + +-- +-- Name: po_arbeidsforhold po_arbeidsforhold_fk_po_person_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_arbeidsforhold + ADD CONSTRAINT po_arbeidsforhold_fk_po_person_id_fkey FOREIGN KEY (fk_po_person_id) REFERENCES public.po_person(id); + + +-- +-- Name: po_bostedsadresse po_bostedsadresse_fk_po_person_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_bostedsadresse + ADD CONSTRAINT po_bostedsadresse_fk_po_person_id_fkey FOREIGN KEY (fk_po_person_id) REFERENCES public.po_person(id); + + +-- +-- Name: po_bostedsadresseperiode po_bostedsadresseperiode_fk_po_person_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_bostedsadresseperiode + ADD CONSTRAINT po_bostedsadresseperiode_fk_po_person_id_fkey FOREIGN KEY (fk_po_person_id) REFERENCES public.po_person(id); + + +-- +-- Name: po_doedsfall po_doedsfall_fk_po_person_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_doedsfall + ADD CONSTRAINT po_doedsfall_fk_po_person_id_fkey FOREIGN KEY (fk_po_person_id) REFERENCES public.po_person(id); + + +-- +-- Name: po_opphold po_opphold_fk_po_person_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_opphold + ADD CONSTRAINT po_opphold_fk_po_person_id_fkey FOREIGN KEY (fk_po_person_id) REFERENCES public.po_person(id); + + +-- +-- Name: po_person po_person_fk_gr_personopplysninger_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_person + ADD CONSTRAINT po_person_fk_gr_personopplysninger_id_fkey FOREIGN KEY (fk_gr_personopplysninger_id) REFERENCES public.gr_personopplysninger(id); + + +-- +-- Name: po_sivilstand po_sivilstand_fk_po_person_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_sivilstand + ADD CONSTRAINT po_sivilstand_fk_po_person_id_fkey FOREIGN KEY (fk_po_person_id) REFERENCES public.po_person(id); + + +-- +-- Name: po_statsborgerskap po_statsborgerskap_fk_po_person_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.po_statsborgerskap + ADD CONSTRAINT po_statsborgerskap_fk_po_person_id_fkey FOREIGN KEY (fk_po_person_id) REFERENCES public.po_person(id); + + +-- +-- Name: refusjon_eos refusjon_eos_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.refusjon_eos + ADD CONSTRAINT refusjon_eos_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: satskjoering satskjoering_fk_fagsak_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.satskjoering + ADD CONSTRAINT satskjoering_fk_fagsak_id_fkey FOREIGN KEY (fk_fagsak_id) REFERENCES public.fagsak(id) ON DELETE CASCADE; + + +-- +-- Name: sett_paa_vent sett_paa_vent_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sett_paa_vent + ADD CONSTRAINT sett_paa_vent_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: tilbakekreving tilbakekreving_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tilbakekreving + ADD CONSTRAINT tilbakekreving_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: totrinnskontroll totrinnskontroll_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.totrinnskontroll + ADD CONSTRAINT totrinnskontroll_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: feilutbetalt_valuta trekk_i_loepende_utbetaling_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.feilutbetalt_valuta + ADD CONSTRAINT trekk_i_loepende_utbetaling_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: utenlandsk_periodebeloep utenlandsk_periodebeloep_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.utenlandsk_periodebeloep + ADD CONSTRAINT utenlandsk_periodebeloep_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: valutakurs valutakurs_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.valutakurs + ADD CONSTRAINT valutakurs_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: okonomi_simulering_postering vedtak_simulering_postering_fk_vedtak_simulering_mottaker__fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.okonomi_simulering_postering + ADD CONSTRAINT vedtak_simulering_postering_fk_vedtak_simulering_mottaker__fkey FOREIGN KEY (fk_okonomi_simulering_mottaker_id) REFERENCES public.okonomi_simulering_mottaker(id) ON DELETE CASCADE; + + +-- +-- Name: vedtaksbegrunnelse vedtaksbegrunnelse_fk_vedtaksperiode_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vedtaksbegrunnelse + ADD CONSTRAINT vedtaksbegrunnelse_fk_vedtaksperiode_id_fkey FOREIGN KEY (fk_vedtaksperiode_id) REFERENCES public.vedtaksperiode(id) ON DELETE CASCADE; + + +-- +-- Name: vedtaksbegrunnelse_fritekst vedtaksbegrunnelse_fritekst_fk_vedtaksperiode_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vedtaksbegrunnelse_fritekst + ADD CONSTRAINT vedtaksbegrunnelse_fritekst_fk_vedtaksperiode_id_fkey FOREIGN KEY (fk_vedtaksperiode_id) REFERENCES public.vedtaksperiode(id) ON DELETE CASCADE; + + +-- +-- Name: vedtaksperiode vedtaksperiode_fk_vedtak_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vedtaksperiode + ADD CONSTRAINT vedtaksperiode_fk_vedtak_id_fkey FOREIGN KEY (fk_vedtak_id) REFERENCES public.vedtak(id); + + +-- +-- Name: verge verge_fk_behandling_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.verge + ADD CONSTRAINT verge_fk_behandling_id_fkey FOREIGN KEY (fk_behandling_id) REFERENCES public.behandling(id); + + +-- +-- Name: vilkar_resultat vilkar_resultat_fk_person_resultat_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.vilkar_resultat + ADD CONSTRAINT vilkar_resultat_fk_person_resultat_id_fkey FOREIGN KEY (fk_person_resultat_id) REFERENCES public.person_resultat(id); + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/db/migration-tests/V2__FyllFlywaySchemaHistory.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/db/migration-tests/V2__FyllFlywaySchemaHistory.sql new file mode 100644 index 000000000..356de473a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/db/migration-tests/V2__FyllFlywaySchemaHistory.sql @@ -0,0 +1,251 @@ +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (3, '3', 'vedtak', 'SQL', 'V3__vedtak.sql', -117979632, 'postgres', '2023-08-18 00:01:12.400387', 17, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (4, '4', 'behandling aktiv', 'SQL', 'V4__behandling_aktiv.sql', -910119557, 'postgres', '2023-08-18 00:01:12.446011', 13, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (5, '5', 'vedtak aktiv', 'SQL', 'V5__vedtak_aktiv.sql', -1803372790, 'postgres', '2023-08-18 00:01:12.494088', 12, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (6, '6', 'vedtak barn', 'SQL', 'V6__vedtak_barn.sql', -460268051, 'postgres', '2023-08-18 00:01:12.528084', 23, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (7, '7', 'iverksett vedtak', 'SQL', 'V7__iverksett_vedtak.sql', 346013391, 'postgres', '2023-08-18 00:01:12.577085', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (8, '8', 'fjern periode fra vedtak', 'SQL', 'V8__fjern_periode_fra_vedtak.sql', -388408157, 'postgres', '2023-08-18 00:01:12.619388', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (9, '9', 'prosessering', 'SQL', 'V9__prosessering.sql', 960629163, 'postgres', '2023-08-18 00:01:12.649894', 27, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (10, '10', 'unique fagsak for person', 'SQL', 'V10__unique_fagsak_for_person.sql', 1265256368, 'postgres', '2023-08-18 00:01:12.696792', 15, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (11, '11', 'lenger status task', 'SQL', 'V11__lenger_status_task.sql', 2091820971, 'postgres', '2023-08-18 00:01:12.740161', 13, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (12, '12', 'modell justering', 'SQL', 'V12__modell_justering.sql', 1650665626, 'postgres', '2023-08-18 00:01:12.771685', 13, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (13, '13', 'vedtak resultat', 'SQL', 'V13__vedtak_resultat.sql', -2047826069, 'postgres', '2023-08-18 00:01:12.808271', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (14, '14', 'vilkaar', 'SQL', 'V14__vilkaar.sql', 1083348811, 'postgres', '2023-08-18 00:01:12.834293', 22, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (15, '15', 'legger til oppgave til behandling', 'SQL', 'V15__legger_til_oppgave_til_behandling.sql', -1251512771, 'postgres', '2023-08-18 00:01:12.880839', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (16, '16', 'behandling kategorier', 'SQL', 'V16__behandling_kategorier.sql', 1850678695, 'postgres', '2023-08-18 00:01:12.910972', 14, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (17, '17', 'batch tabell', 'SQL', 'V17__batch_tabell.sql', -1759912139, 'postgres', '2023-08-18 00:01:12.947277', 14, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (18, '18', 'samlet vilkaar resultat behandling', 'SQL', 'V18__samlet_vilkaar_resultat_behandling.sql', -1204988852, 'postgres', '2023-08-18 00:01:12.982276', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (19, '19', 'vedtak begrunnelse', 'SQL', 'V19__vedtak_begrunnelse.sql', 1245138194, 'postgres', '2023-08-18 00:01:13.015385', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (20, '20', 'typo kategori', 'SQL', 'V20__typo_kategori.sql', -721050255, 'postgres', '2023-08-18 00:01:13.040554', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (21, '21', 'opphør vedtak', 'SQL', 'V21__opphør_vedtak.sql', 444010285, 'postgres', '2023-08-18 00:01:13.069303', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (22, '22', 'vedtak barn til vedtak person', 'SQL', 'V22__vedtak_barn_til_vedtak_person.sql', 1968450114, 'postgres', '2023-08-18 00:01:13.101386', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (23, '23', 'drop saksnummer', 'SQL', 'V23__drop_saksnummer.sql', -1928719954, 'postgres', '2023-08-18 00:01:13.127309', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (24, '24', 'base entitet oke felter', 'SQL', 'V24__base_entitet_oke_felter.sql', -2123655257, 'postgres', '2023-08-18 00:01:13.152645', 42, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (25, '25', 'aktør id person', 'SQL', 'V25__aktør_id_person.sql', 1420465049, 'postgres', '2023-08-18 00:01:13.21745', 12, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (26, '26', 'berik behandling', 'SQL', 'V26__berik_behandling.sql', -898245000, 'postgres', '2023-08-18 00:01:13.247849', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (27, '27', 'satstabell', 'SQL', 'V27__satstabell.sql', -766284868, 'postgres', '2023-08-18 00:01:13.277868', 18, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (28, '28', 'steg', 'SQL', 'V28__steg.sql', -1358191091, 'postgres', '2023-08-18 00:01:13.315471', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (29, '29', 'logg', 'SQL', 'V29__logg.sql', -247427736, 'postgres', '2023-08-18 00:01:13.347575', 13, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (30, '30', 'navn og kjonn', 'SQL', 'V30__navn_og_kjonn.sql', 257679508, 'postgres', '2023-08-18 00:01:13.379106', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (31, '31', 'endre navn loggtype', 'SQL', 'V31__endre_navn_loggtype.sql', 1109501216, 'postgres', '2023-08-18 00:01:13.398516', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (32, '32', 'endre utfall til resultat', 'SQL', 'V32__endre_utfall_til_resultat.sql', 5629839, 'postgres', '2023-08-18 00:01:13.416188', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (33, '33', 'soknad', 'SQL', 'V33__soknad.sql', -535802207, 'postgres', '2023-08-18 00:01:13.437738', 19, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (34, '34', 'gjeldende behandling utbetaling', 'SQL', 'V34__gjeldende_behandling_utbetaling.sql', 619575558, 'postgres', '2023-08-18 00:01:13.472406', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (35, '35', 'beregning resultat', 'SQL', 'V35__beregning_resultat.sql', 353349592, 'postgres', '2023-08-18 00:01:13.502578', 24, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (36, '36', 'vedtak person til andel tilkjent ytelse', 'SQL', 'V36__vedtak_person_til_andel_tilkjent_ytelse.sql', -1893741699, 'postgres', '2023-08-18 00:01:13.550993', 32, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (37, '37', 'fjern vedtak person', 'SQL', 'V37__fjern_vedtak_person.sql', 1261997160, 'postgres', '2023-08-18 00:01:13.619679', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (38, '38', 'periodisert vilkaarsvurdering', 'SQL', 'V38__periodisert_vilkaarsvurdering.sql', -13899829, 'postgres', '2023-08-18 00:01:13.645669', 39, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (39, '39', 'rename beregningsresultat', 'SQL', 'V39__rename_beregningsresultat.sql', 19150761, 'postgres', '2023-08-18 00:01:13.708703', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (40, '40', 'andel tilkjent ytelse', 'SQL', 'V40__andel_tilkjent_ytelse.sql', 506156552, 'postgres', '2023-08-18 00:01:13.734742', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (41, '41', 'flytt begrunnelse', 'SQL', 'V41__flytt_begrunnelse.sql', 2098536317, 'postgres', '2023-08-18 00:01:13.758573', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (42, '42', 'endringer vilkar', 'SQL', 'V42__endringer_vilkar.sql', -1795479413, 'postgres', '2023-08-18 00:01:13.780471', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (43, '43', 'flytte-perioder-til-vilkårvurdering', 'SQL', 'V43__flytte-perioder-til-vilkårvurdering.sql', -1788978407, 'postgres', '2023-08-18 00:01:13.803718', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (44, '44', 'oppdater steg', 'SQL', 'V44__oppdater_steg.sql', -1483454613, 'postgres', '2023-08-18 00:01:13.831868', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (45, '45', 'slett satstabell', 'SQL', 'V45__slett_satstabell.sql', 127435114, 'postgres', '2023-08-18 00:01:13.848625', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (46, '46', 'oppgave', 'SQL', 'V46__oppgave.sql', -1915918880, 'postgres', '2023-08-18 00:01:13.869343', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (47, '47', 'vedtak beslutter', 'SQL', 'V47__vedtak_beslutter.sql', -532567907, 'postgres', '2023-08-18 00:01:13.898766', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (48, '48', 'vedtak brev html og journalpost id', 'SQL', 'V48__vedtak_brev_html_og_journalpost_id.sql', 1382101441, 'postgres', '2023-08-18 00:01:13.919646', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (49, '49', 'ident tabell', 'SQL', 'V49__ident_tabell.sql', -685289502, 'postgres', '2023-08-18 00:01:13.942791', 16, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (50, '50', 'migrer ident fra fagsak til fagsak person', 'SQL', 'V50__migrer_ident_fra_fagsak_til_fagsak_person.sql', 1767543507, 'postgres', '2023-08-18 00:01:13.973747', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (51, '51', 'behandling', 'SQL', 'V51__behandling.sql', 377729761, 'postgres', '2023-08-18 00:01:13.997574', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (52, '52', 'personident andel', 'SQL', 'V52__personident_andel.sql', -1254721435, 'postgres', '2023-08-18 00:01:14.015659', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (53, '53', 'totrinnskontroll v2', 'SQL', 'V53__totrinnskontroll_v2.sql', 55067334, 'postgres', '2023-08-18 00:01:14.042891', 17, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (54, '54', 'drop null vilkarresultat', 'SQL', 'V54__drop_null_vilkarresultat.sql', 1364982728, 'postgres', '2023-08-18 00:01:14.074905', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (55, '55', 'legger til fg constraint paa fagsak person', 'SQL', 'V55__legger_til_fg_constraint_paa_fagsak_person.sql', 1061229666, 'postgres', '2023-08-18 00:01:14.095063', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (56, '56', 'drop totrinn fra vedtak', 'SQL', 'V56__drop_totrinn_fra_vedtak.sql', 1983726081, 'postgres', '2023-08-18 00:01:14.118194', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (57, '57', 'bostedsadresse', 'SQL', 'V57__bostedsadresse.sql', 1652513567, 'postgres', '2023-08-18 00:01:14.141235', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (58, '58', 'altered bostedsadresse', 'SQL', 'V58__altered_bostedsadresse.sql', -1922676921, 'postgres', '2023-08-18 00:01:14.167787', 16, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (59, '59', 'journalpost', 'SQL', 'V59__journalpost.sql', 2052030655, 'postgres', '2023-08-18 00:01:14.198355', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (60, '60', 'vilkaar resultat behandling', 'SQL', 'V60__vilkaar_resultat_behandling.sql', -507614896, 'postgres', '2023-08-18 00:01:14.221528', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (61, '61', 'legger til constraint paa vilkaar resultat', 'SQL', 'V61__legger_til_constraint_paa_vilkaar_resultat.sql', -904418652, 'postgres', '2023-08-18 00:01:14.235874', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (62, '62', 'altered bostedsadresse endret av', 'SQL', 'V62__altered_bostedsadresse_endret_av.sql', -2074154819, 'postgres', '2023-08-18 00:01:14.252517', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (63, '63', 'oppdatert journalpost', 'SQL', 'V63__oppdatert_journalpost.sql', -1677312120, 'postgres', '2023-08-18 00:01:14.270196', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (64, '64', 'altered person add sivilstand', 'SQL', 'V64__altered_person_add_sivilstand.sql', -38711458, 'postgres', '2023-08-18 00:01:14.297689', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (65, '65', 'fjerne aktorid og personident', 'SQL', 'V65__fjerne_aktorid_og_personident.sql', 1126550431, 'postgres', '2023-08-18 00:01:14.316464', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (66, '66', 'altered person add statsborgerskap og medlemskap', 'SQL', 'V66__altered_person_add_statsborgerskap_og_medlemskap.sql', -1107222800, 'postgres', '2023-08-18 00:01:14.33804', 12, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (67, '67', 'altered statsborgerskap oprettet tid', 'SQL', 'V67__altered_statsborgerskap_oprettet_tid.sql', -1256904310, 'postgres', '2023-08-18 00:01:14.365551', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (68, '68', 'altered statsborgerskap legg til medlemskap', 'SQL', 'V68__altered_statsborgerskap_legg_til_medlemskap.sql', -6891128, 'postgres', '2023-08-18 00:01:14.386822', 25, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (69, '69', 'vilkår not null behandlingid', 'SQL', 'V69__vilkår_not_null_behandlingid.sql', -2045937695, 'postgres', '2023-08-18 00:01:14.435667', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (70, '70', 'opprett opphold og relater til person', 'SQL', 'V70__opprett_opphold_og_relater_til_person.sql', -1142453931, 'postgres', '2023-08-18 00:01:14.456474', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (71, '71', 'vedtak stønad brev begrunnelser', 'SQL', 'V71__vedtak_stønad_brev_begrunnelser.sql', 1616418529, 'postgres', '2023-08-18 00:01:14.478368', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (72, '72', 'andel tilkjent ytelse revurdering', 'SQL', 'V72__andel_tilkjent_ytelse_revurdering.sql', -172226446, 'postgres', '2023-08-18 00:01:14.496085', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (73, '73', 'andel tilkjent ytelse forrige periode', 'SQL', 'V73__andel_tilkjent_ytelse_forrige_periode.sql', -182711922, 'postgres', '2023-08-18 00:01:14.514362', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (74, '74', 'lag tabell for arbeidsforhold', 'SQL', 'V74__lag_tabell_for_arbeidsforhold.sql', -1201091909, 'postgres', '2023-08-18 00:01:14.532012', 12, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (75, '75', 'vedtaksdato kan være null', 'SQL', 'V75__vedtaksdato_kan_være_null.sql', 225452020, 'postgres', '2023-08-18 00:01:14.557479', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (76, '76', 'foreign key fra totrinnskontroll', 'SQL', 'V76__foreign_key_fra_totrinnskontroll.sql', -955497424, 'postgres', '2023-08-18 00:01:14.576094', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (77, '77', 'lag tabell for bostedsadresseperiode', 'SQL', 'V77__lag_tabell_for_bostedsadresseperiode.sql', 1562922787, 'postgres', '2023-08-18 00:01:14.593198', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (78, '78', 'utbetaling begrunnelse tabell', 'SQL', 'V78__utbetaling_begrunnelse_tabell.sql', -1416311026, 'postgres', '2023-08-18 00:01:14.616156', 13, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (79, '79', 'utbetaling begrunnelse utvidelser', 'SQL', 'V79__utbetaling_begrunnelse_utvidelser.sql', -1081586372, 'postgres', '2023-08-18 00:01:14.64299', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (80, '80', 'person målform', 'SQL', 'V80__person_målform.sql', -166725326, 'postgres', '2023-08-18 00:01:14.66704', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (81, '81', 'behandling og fagsak status', 'SQL', 'V81__behandling__og_fagsak_status.sql', 430862658, 'postgres', '2023-08-18 00:01:14.682192', 15, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (82, '82', 'foedselshendese pre lansering', 'SQL', 'V82__foedselshendese_pre_lansering.sql', -1557146084, 'postgres', '2023-08-18 00:01:14.710208', 15, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (83, '83', 'foedselshendese ny behandling not null', 'SQL', 'V83__foedselshendese_ny_behandling_not_null.sql', -236203810, 'postgres', '2023-08-18 00:01:14.74402', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (84, '84', 'behandling resultat samlet resultat', 'SQL', 'V84__behandling_resultat_samlet_resultat.sql', -395896913, 'postgres', '2023-08-18 00:01:14.781417', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (85, '85', 'behandling resultat set samlet resultat', 'SQL', 'V85__behandling_resultat_set_samlet_resultat.sql', -1892717389, 'postgres', '2023-08-18 00:01:14.808758', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (86, '86', 'arbeidsfordeling pa behandling', 'SQL', 'V86__arbeidsfordeling_pa_behandling.sql', 1146974519, 'postgres', '2023-08-18 00:01:14.834589', 15, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (87, '87', 'autovedtak evaluering', 'SQL', 'V87__autovedtak_evaluering.sql', -1009788449, 'postgres', '2023-08-18 00:01:14.878616', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (88, '88', 'behandling aarsak og automatisk', 'SQL', 'V88__behandling_aarsak_og_automatisk.sql', -370962295, 'postgres', '2023-08-18 00:01:14.894265', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (89, '89', 'begrunnelse aareg til skjønnhetsvurdering', 'SQL', 'V89__begrunnelse_aareg_til_skjønnhetsvurdering.sql', -1364870645, 'postgres', '2023-08-18 00:01:14.915904', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (90, '90', 'behandling gjeldende for fremtidig utbetaling', 'SQL', 'V90__behandling_gjeldende_for_fremtidig_utbetaling.sql', 1525707757, 'postgres', '2023-08-18 00:01:14.929349', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (91, '91', 'oppdater utbetalingbegrunnelse', 'SQL', 'V91__oppdater_utbetalingbegrunnelse.sql', -1285357547, 'postgres', '2023-08-18 00:01:14.943218', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (92, '92', 'oppdater verdi utbetalingbegrunnelse', 'SQL', 'V92__oppdater_verdi_utbetalingbegrunnelse.sql', 1888195125, 'postgres', '2023-08-18 00:01:14.95823', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (93, '93', 'sett korekte enheter', 'SQL', 'V93__sett_korekte_enheter.sql', 387961620, 'postgres', '2023-08-18 00:01:14.976057', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (94, '94', 'fjern forrige vedtak fra vedtak', 'SQL', 'V94__fjern_forrige_vedtak_fra_vedtak.sql', 694967074, 'postgres', '2023-08-18 00:01:14.99067', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (95, '95', 'legg til opplysningsplikt', 'SQL', 'V95__legg_til_opplysningsplikt.sql', 1028115718, 'postgres', '2023-08-18 00:01:15.008492', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (96, '96', 'opprett behandling steg tilstand', 'SQL', 'V96__opprett_behandling_steg_tilstand.sql', -15839118, 'postgres', '2023-08-18 00:01:15.030624', 13, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (97, '97', 'behandling slette steg', 'SQL', 'V97__behandling_slette_steg.sql', 24281615, 'postgres', '2023-08-18 00:01:15.057994', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (98, '98', 'behandlingstegtilstand sett avsluttet til utfort', 'SQL', 'V98__behandlingstegtilstand_sett_avsluttet_til_utfort.sql', 802442809, 'postgres', '2023-08-18 00:01:15.074171', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (99, '99', 'vilkaar resultat enum endring', 'SQL', 'V99__vilkaar_resultat_enum_endring.sql', -1234431366, 'postgres', '2023-08-18 00:01:15.089706', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (100, '100', 'fjern behandling gjeldende for fremtidig utbetaling', 'SQL', 'V100__fjern_behandling_gjeldende_for_fremtidig_utbetaling.sql', -330201101, 'postgres', '2023-08-18 00:01:15.106239', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (101, '101', 'legg til behandling oppsto i', 'SQL', 'V101__legg_til_behandling_oppsto_i.sql', 263034285, 'postgres', '2023-08-18 00:01:15.123656', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (102, '102', 'manuelt oppdater fagsakstatus', 'SQL', 'V102__manuelt_oppdater_fagsakstatus.sql', 843110549, 'postgres', '2023-08-18 00:01:15.14514', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (103, '103', 'behandling resultat til vilkårsvurdering', 'SQL', 'V103__behandling_resultat_til_vilkårsvurdering.sql', -2007969993, 'postgres', '2023-08-18 00:01:15.162581', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (104, '104', 'resultat paa behandling', 'SQL', 'V104__resultat_paa_behandling.sql', 1080576696, 'postgres', '2023-08-18 00:01:15.181814', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (105, '105', 'dupliser behandlingresultat', 'SQL', 'V105__dupliser_behandlingresultat.sql', 1052594989, 'postgres', '2023-08-18 00:01:15.205788', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (106, '106', 'konsistensavstemming datoer 2021', 'SQL', 'V106__konsistensavstemming_datoer_2021.sql', -475303639, 'postgres', '2023-08-18 00:01:15.221874', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (107, '107', 'vilkår resultat er automatisk vurdert', 'SQL', 'V107__vilkår_resultat_er_automatisk_vurdert.sql', 867636024, 'postgres', '2023-08-18 00:01:15.237974', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (108, '108', 'utbetaling begrunnelse til vedtak begrunnelse', 'SQL', 'V108__utbetaling_begrunnelse_til_vedtak_begrunnelse.sql', -1397007051, 'postgres', '2023-08-18 00:01:15.25598', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (109, '109', 'oppdatere behandlingsresultat', 'SQL', 'V109__oppdatere_behandlingsresultat.sql', 1660331179, 'postgres', '2023-08-18 00:01:15.279045', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (110, '110', 'slett tomme vedtakbegrunnelser', 'SQL', 'V110__slett_tomme_vedtakbegrunnelser.sql', -1342811604, 'postgres', '2023-08-18 00:01:15.301344', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (111, '111', 'saksstatistikk mellomlagring', 'SQL', 'V111__saksstatistikk_mellomlagring.sql', 44129733, 'postgres', '2023-08-18 00:01:15.319047', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (112, '112', 'legg til avslag flagg', 'SQL', 'V112__legg_til_avslag_flagg.sql', -1080739677, 'postgres', '2023-08-18 00:01:15.344643', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (113, '113', 'altered totrinnskontroll legg til saksbehandler id', 'SQL', 'V113__altered_totrinnskontroll_legg_til_saksbehandler_id.sql', -1315474895, 'postgres', '2023-08-18 00:01:15.364217', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (114, '114', 'opprett andre vurderinger', 'SQL', 'V114__opprett_andre_vurderinger.sql', -829428929, 'postgres', '2023-08-18 00:01:15.382898', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (115, '115', 'saksstatistikk mellomlagring', 'SQL', 'V115__saksstatistikk_mellomlagring.sql', -468298486, 'postgres', '2023-08-18 00:01:15.413994', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (116, '116', 'periode resultat fk til person resultat fk', 'SQL', 'V116__periode_resultat_fk_til_person_resultat_fk.sql', -596848892, 'postgres', '2023-08-18 00:01:15.436271', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (117, '117', 'bugfix periode resultat fk til person resultat fk', 'SQL', 'V117__bugfix_periode_resultat_fk_til_person_resultat_fk.sql', 2046339591, 'postgres', '2023-08-18 00:01:15.455189', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (118, '118', 'replikerte fra opplysningsplikt til andre vurderinger', 'SQL', 'V118__replikerte_fra_opplysningsplikt_til_andre_vurderinger.sql', -1490156450, 'postgres', '2023-08-18 00:01:15.470772', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (119, '119', 'drop nullable tom vedtakbegrunnelse', 'SQL', 'V119__drop_nullable_tom_vedtakbegrunnelse.sql', 291343170, 'postgres', '2023-08-18 00:01:15.489027', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (120, '120', 'drop opplysningsplikt', 'SQL', 'V120__drop_opplysningsplikt.sql', 1677565755, 'postgres', '2023-08-18 00:01:15.505823', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (121, '121', 'migrer gammelt behandlingresultat', 'SQL', 'V121__migrer_gammelt_behandlingresultat.sql', 601595219, 'postgres', '2023-08-18 00:01:15.524376', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (122, '122', 'migrer gammelt behandlingresultat preprod', 'SQL', 'V122__migrer_gammelt_behandlingresultat_preprod.sql', 114794316, 'postgres', '2023-08-18 00:01:15.540113', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (123, '123', 'simulering', 'SQL', 'V123__simulering.sql', 159901940, 'postgres', '2023-08-18 00:01:15.555059', 15, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (124, '124', 'nullable fom og vilkaarkobling', 'SQL', 'V124__nullable_fom_og_vilkaarkobling.sql', -1130948673, 'postgres', '2023-08-18 00:01:15.584939', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (125, '125', 'legg baseentitet til simulering', 'SQL', 'V125__legg_baseentitet_til_simulering.sql', -1431267811, 'postgres', '2023-08-18 00:01:15.605287', 13, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (126, '126', 'legg versjonsnummer til simulering', 'SQL', 'V126__legg_versjonsnummer_til_simulering.sql', 1228570394, 'postgres', '2023-08-18 00:01:15.62954', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (127, '127', 'tilbakekreving', 'SQL', 'V127__tilbakekreving.sql', 542806616, 'postgres', '2023-08-18 00:01:15.654157', 15, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (128, '128', 'slett manuell opphorsdato fra vedtak', 'SQL', 'V128__slett_manuell_opphorsdato_fra_vedtak.sql', -1130791904, 'postgres', '2023-08-18 00:01:15.688191', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (129, '129', 'flytt tilbakekreving til behandling', 'SQL', 'V129__flytt_tilbakekreving_til_behandling.sql', -329149591, 'postgres', '2023-08-18 00:01:15.709379', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (130, '130', 'flytt simulering til behandling', 'SQL', 'V130__flytt_simulering_til_behandling.sql', 386450865, 'postgres', '2023-08-18 00:01:15.74319', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (131, '131', 'rename steg simulering til vurder tilbakekreving', 'SQL', 'V131__rename_steg_simulering_til_vurder_tilbakekreving.sql', -1694556105, 'postgres', '2023-08-18 00:01:15.768262', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (132, '132', 'legger til skjønnsmessig vurdering på vilkår resultat', 'SQL', 'V132__legger_til_skjønnsmessig_vurdering_på_vilkår_resultat.sql', -1130914241, 'postgres', '2023-08-18 00:01:15.784483', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (133, '133', 'vedtaksperiode og begrunnelser', 'SQL', 'V133__vedtaksperiode_og_begrunnelser.sql', 1752676846, 'postgres', '2023-08-18 00:01:15.800981', 28, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (134, '134', 'periode paa bostedsadresser og flere bostedsadresser', 'SQL', 'V134__periode_paa_bostedsadresser_og_flere_bostedsadresser.sql', -2057614828, 'postgres', '2023-08-18 00:01:15.842875', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (135, '135', 'jdbjournalpost utvidelser', 'SQL', 'V135__jdbjournalpost_utvidelser.sql', 2086441591, 'postgres', '2023-08-18 00:01:15.868053', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (136, '136', 'legg til sivilstad', 'SQL', 'V136__legg_til_sivilstad.sql', 246829226, 'postgres', '2023-08-18 00:01:15.88539', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (137, '137', 'legger til medlemskapsvurdering på vilkår resultat', 'SQL', 'V137__legger_til_medlemskapsvurdering_på_vilkår_resultat.sql', -689033237, 'postgres', '2023-08-18 00:01:15.914198', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (138, '138', 'rename dodsfall enum', 'SQL', 'V138__rename_dodsfall_enum.sql', -2084864536, 'postgres', '2023-08-18 00:01:15.941465', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (139, '139', 'cascade delete foreign keys', 'SQL', 'V139__cascade_delete_foreign_keys.sql', -1360795391, 'postgres', '2023-08-18 00:01:15.959335', 13, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (140, '140', 'fjern deprecated bostedsadresse sivilstand', 'SQL', 'V140__fjern_deprecated_bostedsadresse_sivilstand.sql', -367895975, 'postgres', '2023-08-18 00:01:15.985987', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (141, '141', 'legg til begrunnelser paa vilkaar', 'SQL', 'V141__legg_til_begrunnelser_paa_vilkaar.sql', -2039499837, 'postgres', '2023-08-18 00:01:16.004614', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (142, '142', 'legger til del bosted på vilkår resultat', 'SQL', 'V142__legger_til_del_bosted_på_vilkår_resultat.sql', -1603607341, 'postgres', '2023-08-18 00:01:16.021986', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (143, '143', 'cascade delete foreign keys fritekst', 'SQL', 'V143__cascade_delete_foreign_keys_fritekst.sql', 1926554209, 'postgres', '2023-08-18 00:01:16.036766', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (144, '144', 'oppdater enums', 'SQL', 'V144__oppdater_enums.sql', -376126292, 'postgres', '2023-08-18 00:01:16.051801', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (145, '145', 'migrer vadtaksbegrunnelse til veddtaksperiode og begrunnelse', 'SQL', 'V145__migrer_vadtaksbegrunnelse_til_veddtaksperiode_og_begrunnelse.sql', 2002119898, 'postgres', '2023-08-18 00:01:16.068767', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (146, '146', 'opprett fødselshendelsefiltrering resultat', 'SQL', 'V146__opprett_fødselshendelsefiltrering_resultat.sql', 131620641, 'postgres', '2023-08-18 00:01:16.090866', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (147, '147', 'dropp etterbetaling filtreringsresultat', 'SQL', 'V147__dropp_etterbetaling_filtreringsresultat.sql', -355132040, 'postgres', '2023-08-18 00:01:16.115306', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (148, '148', 'totrinnskontroll kontrollerte sider', 'SQL', 'V148__totrinnskontroll_kontrollerte_sider.sql', 1387811696, 'postgres', '2023-08-18 00:01:16.130934', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (149, '149', 'oppdater opphorsbegrunnelse enum', 'SQL', 'V149__oppdater_opphorsbegrunnelse_enum.sql', -1252199194, 'postgres', '2023-08-18 00:01:16.148395', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (150, '150', 'opprett overstyrt utbetaling', 'SQL', 'V150__opprett_overstyrt_utbetaling.sql', -441372945, 'postgres', '2023-08-18 00:01:16.170424', 14, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (151, '151', 'legg til ytelse personer', 'SQL', 'V151__legg_til_ytelse_personer.sql', 899116586, 'postgres', '2023-08-18 00:01:16.200167', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (152, '152', 'prosessering jdbc patch', 'SQL', 'V152__prosessering_jdbc_patch.sql', 1173753268, 'postgres', '2023-08-18 00:01:16.218973', 12, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (153, '153', 'patch task jdbc versjon', 'SQL', 'V153__patch_task_jdbc_versjon.sql', -573903784, 'postgres', '2023-08-18 00:01:16.242972', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (154, '154', 'utvid andel tilkjent ytelse', 'SQL', 'V154__utvid_andel_tilkjent_ytelse.sql', -202654425, 'postgres', '2023-08-18 00:01:16.25912', 19, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (155, '155', 'fag sak legg til arkivert og constraint', 'SQL', 'V155__fag_sak_legg_til_arkivert_og_constraint.sql', -762481641, 'postgres', '2023-08-18 00:01:16.290841', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (156, '156', 'arkiver feilende fag sak i prod', 'SQL', 'V156__arkiver_feilende_fag_sak_i_prod.sql', -1207572492, 'postgres', '2023-08-18 00:01:16.307303', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (157, '157', 'fagsak legg til unique index på arkivert', 'SQL', 'V157__fagsak_legg_til_unique_index_på_arkivert.sql', 466352695, 'postgres', '2023-08-18 00:01:16.324271', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (158, '158', 'legg til begrunnelse paa endringer', 'SQL', 'V158__legg_til_begrunnelse_paa_endringer.sql', 988377595, 'postgres', '2023-08-18 00:01:16.34269', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (159, '159', 'endret utbetaling andel definer felt nullable', 'SQL', 'V159__endret_utbetaling_andel_definer_felt_nullable.sql', -134113579, 'postgres', '2023-08-18 00:01:16.357451', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (160, '160', 'endret utbetaling andel legg til to nye felt', 'SQL', 'V160__endret_utbetaling_andel_legg_til_to_nye_felt.sql', -1568363481, 'postgres', '2023-08-18 00:01:16.37498', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (161, '161', 'opprett periode overgangsstønad', 'SQL', 'V161__opprett_periode_overgangsstønad.sql', 19331404, 'postgres', '2023-08-18 00:01:16.391936', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (162, '162', 'cascade slett endret andel tilkjent ytelse relasjon', 'SQL', 'V162__cascade_slett_endret_andel_tilkjent_ytelse_relasjon.sql', 540817621, 'postgres', '2023-08-18 00:01:16.414239', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (163, '163', 'legge til nytt felt vurderes etter', 'SQL', 'V163__legge_til_nytt_felt_vurderes_etter.sql', -847190144, 'postgres', '2023-08-18 00:01:16.432276', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (164, '164', 'konsistensavstemming datoer 2022', 'SQL', 'V164__konsistensavstemming_datoer_2022.sql', -964172920, 'postgres', '2023-08-18 00:01:16.449669', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (165, '165', 'standardbegrunnelser baseline', 'SQL', 'V165__standardbegrunnelser_baseline.sql', 555174593, 'postgres', '2023-08-18 00:01:16.468661', 24, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (166, '166', 'drop vedtak begrunnelse', 'SQL', 'V166__drop_vedtak_begrunnelse.sql', 476787405, 'postgres', '2023-08-18 00:01:16.50764', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (167, '167', 'opprett person ident og legg til kolonne aktorid', 'SQL', 'V167__opprett_person_ident_og_legg_til_kolonne_aktorid.sql', 577789640, 'postgres', '2023-08-18 00:01:16.528961', 21, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (168, '168', 'utdypende vilkarsvurderinger', 'SQL', 'V168__utdypende_vilkarsvurderinger.sql', -1671070398, 'postgres', '2023-08-18 00:01:16.567479', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (169, '169', 'oppret tabell aktørid', 'SQL', 'V169__oppret_tabell_aktørid.sql', 1838065429, 'postgres', '2023-08-18 00:01:16.590071', 19, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (170, '170', 'populer kolonner for aktorid', 'SQL', 'V170__populer_kolonner_for_aktorid.sql', -1262529325, 'postgres', '2023-08-18 00:01:16.624618', 31, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (171, '171', 'utdypende vilkarsvurderinger opprydding', 'SQL', 'V171__utdypende_vilkarsvurderinger_opprydding.sql', -1362678244, 'postgres', '2023-08-18 00:01:16.671172', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (172, '172', 'opprydning behandling 1106052', 'SQL', 'V172__opprydning_behandling_1106052.sql', -444498172, 'postgres', '2023-08-18 00:01:16.697799', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (173, '173', 'oppdater tilkjent ytelse stonad fom fra andel tilkjent ytelse', 'SQL', 'V173__oppdater_tilkjent_ytelse_stonad_fom_fra_andel_tilkjent_ytelse.sql', -1621089185, 'postgres', '2023-08-18 00:01:16.719189', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (174, '174', 'kickstart satsendring 06 01 2022', 'SQL', 'V174__kickstart_satsendring_06_01_2022.sql', -765880505, 'postgres', '2023-08-18 00:01:16.742578', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (175, '175', 'kickstart satsendring 07 01 2022', 'SQL', 'V175__kickstart_satsendring_07_01_2022.sql', 2031026047, 'postgres', '2023-08-18 00:01:16.762056', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (176, '176', 'fjern kolonn person ident', 'SQL', 'V176__fjern_kolonn_person_ident.sql', -1563244892, 'postgres', '2023-08-18 00:01:16.780478', 13, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (177, '177', 'personident constraint foedselsnummerog aktiv', 'SQL', 'V177__personident_constraint_foedselsnummerog_aktiv.sql', 1399297868, 'postgres', '2023-08-18 00:01:16.812217', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (178, '178', 'legg til inde på fk', 'SQL', 'V178__legg_til_inde_på_fk.sql', -1938224015, 'postgres', '2023-08-18 00:01:16.834676', 64, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (179, '179', 'korreksjon av versjon satsendring 06 01 2022', 'SQL', 'V179__korreksjon_av_versjon_satsendring_06_01_2022.sql', 1916719776, 'postgres', '2023-08-18 00:01:16.912861', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (180, '180', 'endre constraint vedtaksperiode', 'SQL', 'V180__endre_constraint_vedtaksperiode.sql', -1991194511, 'postgres', '2023-08-18 00:01:16.933321', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (181, '181', 'legg til index på personident', 'SQL', 'V181__legg_til_index_på_personident.sql', 939302647, 'postgres', '2023-08-18 00:01:16.952372', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (182, '182', 'opprett tabell sett paa vent', 'SQL', 'V182__opprett_tabell_sett_paa_vent.sql', -783184981, 'postgres', '2023-08-18 00:01:16.974616', 17, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (183, '183', 'migrerte behandlinger sett behandlingstema', 'SQL', 'V183__migrerte_behandlinger_sett_behandlingstema.sql', -124249885, 'postgres', '2023-08-18 00:01:17.0061', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (184, '184', 'opprett tabell po doedsfall', 'SQL', 'V184__opprett_tabell_po_doedsfall.sql', -1777273981, 'postgres', '2023-08-18 00:01:17.03786', 15, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (185, '185', 'legg kolonner til sett paa vent', 'SQL', 'V185__legg_kolonner_til_sett_paa_vent.sql', 1415307236, 'postgres', '2023-08-18 00:01:17.069473', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (186, '186', 'legg kolonner til po doedsfall', 'SQL', 'V186__legg_kolonner_til_po_doedsfall.sql', -415915075, 'postgres', '2023-08-18 00:01:17.089832', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (187, '187', 'skyggesak', 'SQL', 'V187__skyggesak.sql', 935309413, 'postgres', '2023-08-18 00:01:17.109766', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (188, '188', 'opprett skyggesaker', 'SQL', 'V188__opprett_skyggesaker.sql', 991403047, 'postgres', '2023-08-18 00:01:17.134227', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (189, '189', 'behandling migreringsinfo', 'SQL', 'V189__behandling_migreringsinfo.sql', 1879892420, 'postgres', '2023-08-18 00:01:17.154039', 15, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (190, '190', 'index saksstatistikkmellomlagring', 'SQL', 'V190__index_saksstatistikkmellomlagring.sql', 1432737148, 'postgres', '2023-08-18 00:01:17.183483', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (191, '191', 'opprett tabell data chunk', 'SQL', 'V191__opprett_tabell_data_chunk.sql', -501963181, 'postgres', '2023-08-18 00:01:17.205624', 13, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (192, '192', 'delete begrunnelse opphor ikke mottat opplysninger', 'SQL', 'V192__delete_begrunnelse_opphor_ikke_mottat_opplysninger.sql', -530907212, 'postgres', '2023-08-18 00:01:17.235076', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (193, '193', 'behandling søknadsinfo', 'SQL', 'V193__behandling_søknadsinfo.sql', -774483787, 'postgres', '2023-08-18 00:01:17.253374', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (194, '194', 'unik behandling i behandling migreringsinfo', 'SQL', 'V194__unik_behandling_i_behandling_migreringsinfo.sql', 613282642, 'postgres', '2023-08-18 00:01:17.27705', 7, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (195, '195', 'set fagsaker til løpende', 'SQL', 'V195__set_fagsaker_til_løpende.sql', 604700882, 'postgres', '2023-08-18 00:01:17.296455', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (196, '196', 'migrerte behandlinger sett behandlingstema', 'SQL', 'V196__migrerte_behandlinger_sett_behandlingstema.sql', 1080065086, 'postgres', '2023-08-18 00:01:17.313148', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (197, '197', 'fjern personidenter fra vedtaksbegrunnelse', 'SQL', 'V197__fjern_personidenter_fra_vedtaksbegrunnelse.sql', 1169273254, 'postgres', '2023-08-18 00:01:17.329906', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (198, '198', 'kompetanse', 'SQL', 'V198__kompetanse.sql', -23154794, 'postgres', '2023-08-18 00:01:17.346757', 18, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (199, '199', 'endre enum vedtaksperiode reduksjon til utbetaling med redukjson ', 'SQL', 'V199__endre_enum_vedtaksperiode_reduksjon_til_utbetaling_med_redukjson_.sql', 312584051, 'postgres', '2023-08-18 00:01:17.379598', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (200, '200', 'behandlingsmetadata ', 'SQL', 'V200__behandlingsmetadata_.sql', 800399767, 'postgres', '2023-08-18 00:01:17.400874', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (201, '201', 'sakkstatistikk mellomlagring index', 'SQL', 'V201__sakkstatistikk_mellomlagring_index.sql', -898582773, 'postgres', '2023-08-18 00:01:17.422095', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (202, '202', 'legg paa kolonner for kompetanseskjema', 'SQL', 'V202__legg_paa_kolonner_for_kompetanseskjema.sql', -912693613, 'postgres', '2023-08-18 00:01:17.4441', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (203, '203', 'oppdater tilkjent ytelse stonad tom fra andel tilkjent ytelse', 'SQL', 'V203__oppdater_tilkjent_ytelse_stonad_tom_fra_andel_tilkjent_ytelse.sql', -1242982512, 'postgres', '2023-08-18 00:01:17.461954', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (204, '204', 'oppdater behandlingsresultat endret til endret utbetaling', 'SQL', 'V204__oppdater_behandlingsresultat_endret_til_endret_utbetaling.sql', 1794950328, 'postgres', '2023-08-18 00:01:17.480577', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (205, '205', 'index tilkjent ytelse andel tilkjent ytelse', 'SQL', 'V205__index_tilkjent_ytelse_andel_tilkjent_ytelse.sql', -1810356772, 'postgres', '2023-08-18 00:01:17.497317', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (206, '206', 'valutakurs', 'SQL', 'V206__valutakurs.sql', -1449708213, 'postgres', '2023-08-18 00:01:17.518756', 15, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (207, '207', 'utenlandsk periodebeløp', 'SQL', 'V207__utenlandsk_periodebeløp.sql', -852394420, 'postgres', '2023-08-18 00:01:17.547006', 16, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (208, '208', 'legg til eos begrunnelse tabell', 'SQL', 'V208__legg_til_eos_begrunnelse_tabell.sql', -542589479, 'postgres', '2023-08-18 00:01:17.581511', 13, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (209, '209', 'fagsak eier', 'SQL', 'V209__fagsak_eier.sql', -269183142, 'postgres', '2023-08-18 00:01:17.60771', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (210, '210', 'upb utbetalingsland', 'SQL', 'V210__upb_utbetalingsland.sql', -129612747, 'postgres', '2023-08-18 00:01:17.627967', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (211, '211', 'kolonner for differanseberegning paa andel tilkjent ytelse', 'SQL', 'V211__kolonner_for_differanseberegning_paa_andel_tilkjent_ytelse.sql', 327297892, 'postgres', '2023-08-18 00:01:17.644411', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (212, '212', 'aktoerId splitt update cascade', 'SQL', 'V212__aktoerId_splitt_update_cascade.sql', 1565102752, 'postgres', '2023-08-18 00:01:17.660809', 28, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (213, '213', 'kalkulert maanedlig belop upb', 'SQL', 'V213__kalkulert_maanedlig_belop_upb.sql', -324759753, 'postgres', '2023-08-18 00:01:17.70367', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (214, '214', 'institusjon', 'SQL', 'V214__institusjon.sql', -1001992131, 'postgres', '2023-08-18 00:01:17.721389', 12, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (215, '215', 'fagsak type', 'SQL', 'V215__fagsak_type.sql', 654631204, 'postgres', '2023-08-18 00:01:17.746913', 14, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (216, '216', 'verge', 'SQL', 'V216__verge.sql', -914830156, 'postgres', '2023-08-18 00:01:17.77517', 17, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (217, '217', 'fikser skrivefeil i fagsak type', 'SQL', 'V217__fikser_skrivefeil_i_fagsak_type.sql', -1540504487, 'postgres', '2023-08-18 00:01:17.797249', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (218, '218', 'fjerner fagsak eier index', 'SQL', 'V218__fjerner_fagsak_eier_index.sql', 1640054597, 'postgres', '2023-08-18 00:01:17.825402', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (219, '219', 'fjerner fagsak eier', 'SQL', 'V219__fjerner_fagsak_eier.sql', -10090284, 'postgres', '2023-08-18 00:01:17.849178', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (220, '220', 'korrigert etterbetaling', 'SQL', 'V220__korrigert_etterbetaling.sql', -985428108, 'postgres', '2023-08-18 00:01:17.866804', 14, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (221, '221', 'drop verge navn adresse og unique index verge ident', 'SQL', 'V221__drop_verge_navn_adresse_og_unique_index_verge_ident.sql', 697030984, 'postgres', '2023-08-18 00:01:17.896337', 13, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (222, '222', 'sokers aktivitetsland kolonne paa kompetanse', 'SQL', 'V222__sokers_aktivitetsland_kolonne_paa_kompetanse.sql', -624384792, 'postgres', '2023-08-18 00:01:17.924409', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (223, '223', 'sokers aktivitet endre enumverdier', 'SQL', 'V223__sokers_aktivitet_endre_enumverdier.sql', 408370487, 'postgres', '2023-08-18 00:01:17.942515', 13, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (224, '224', 'notnull institusjon schema', 'SQL', 'V224__notnull_institusjon_schema.sql', -760022071, 'postgres', '2023-08-18 00:01:17.968037', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (225, '225', 'sokers aktivitet endre enumverdier paa nytt', 'SQL', 'V225__sokers_aktivitet_endre_enumverdier_paa_nytt.sql', 1840876643, 'postgres', '2023-08-18 00:01:17.987363', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (226, '226', 'vurderes etter null for utvidet', 'SQL', 'V226__vurderes_etter_null_for_utvidet.sql', -1461681488, 'postgres', '2023-08-18 00:01:18.010512', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (227, '227', 'offset aiven sakstatistikk mellomlagring', 'SQL', 'V227__offset_aiven_sakstatistikk_mellomlagring.sql', 1328972106, 'postgres', '2023-08-18 00:01:18.03004', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (228, '228', 'index konsistensavstemming', 'SQL', 'V228__index_konsistensavstemming.sql', 927080313, 'postgres', '2023-08-18 00:01:18.052057', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (229, '229', 'konsistensavstemming datoer 2023', 'SQL', 'V229__konsistensavstemming_datoer_2023.sql', 1407899542, 'postgres', '2023-08-18 00:01:18.0745', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (230, '230', 'korrigert vedtak', 'SQL', 'V230__korrigert_vedtak.sql', -1461351762, 'postgres', '2023-08-18 00:01:18.095345', 14, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (231, '231', 'opprett trekk i loepende utbetaling', 'SQL', 'V231__opprett_trekk_i_loepende_utbetaling.sql', 1580330894, 'postgres', '2023-08-18 00:01:18.123601', 12, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (232, '232', 'prefixer standardbegrunnelser', 'SQL', 'V232__prefixer_standardbegrunnelser.sql', -1221364442, 'postgres', '2023-08-18 00:01:18.148331', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (233, '233', 'notnull fom og tom trekk i loepende utbetaling', 'SQL', 'V233__notnull_fom_og_tom_trekk_i_loepende_utbetaling.sql', -1629542661, 'postgres', '2023-08-18 00:01:18.165582', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (234, '234', 'navnendring feilutbetalt valuta', 'SQL', 'V234__navnendring_feilutbetalt_valuta.sql', -867916140, 'postgres', '2023-08-18 00:01:18.184719', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (235, '235', 'okonomi simulering mottaker legg til er feilutbetaling', 'SQL', 'V235__okonomi_simulering_mottaker_legg_til_er_feilutbetaling.sql', -1595977433, 'postgres', '2023-08-18 00:01:18.201413', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (236, '236', 'brevmottaker', 'SQL', 'V236__brevmottaker.sql', 1832422079, 'postgres', '2023-08-18 00:01:18.216081', 11, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (237, '237', 'satskjoering', 'SQL', 'V237__satskjoering.sql', -918064663, 'postgres', '2023-08-18 00:01:18.239377', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (238, '238', 'satskjoering feiltype', 'SQL', 'V238__satskjoering_feiltype.sql', 944428520, 'postgres', '2023-08-18 00:01:18.262086', 8, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (239, '239', 'slett andel til endret andel tabell', 'SQL', 'V239__slett_andel_til_endret_andel_tabell.sql', 658890531, 'postgres', '2023-08-18 00:01:18.290314', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (240, '240', 'opprett refusjon eos', 'SQL', 'V240__opprett_refusjon_eos.sql', -1921965128, 'postgres', '2023-08-18 00:01:18.309033', 10, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (241, '241', 'oppdater behandlinger satt på vent', 'SQL', 'V241__oppdater_behandlinger_satt_på_vent.sql', 554802892, 'postgres', '2023-08-18 00:01:18.331611', 6, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (242, '242', 'behandling aktivert tid', 'SQL', 'V242__behandling_aktivert_tid.sql', 271395237, 'postgres', '2023-08-18 00:01:18.349232', 5, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (243, '243', 'flere aktive behandlinger', 'SQL', 'V243__flere_aktive_behandlinger.sql', 2060629810, 'postgres', '2023-08-18 00:01:18.366748', 9, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (244, '244', 'satstid satskjoring', 'SQL', 'V244__satstid_satskjoring.sql', 361012185, 'postgres', '2023-08-18 00:01:18.388139', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (245, '245', 'feilutbetalt valuta per mnd', 'SQL', 'V245__feilutbetalt_valuta_per_mnd.sql', 328930134, 'postgres', '2023-08-18 00:01:18.40311', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (246, '246', 'vilkar resultat begrunnelse', 'SQL', 'V246__vilkar_resultat_begrunnelse.sql', -449828735, 'postgres', '2023-08-18 00:01:18.417773', 4, true); +INSERT INTO public.flyway_schema_history (installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success) VALUES (247, '247', 'søknad digitaliseringsgrad data', 'SQL', 'V247__søknad_digitaliseringsgrad_data.sql', 705682480, 'postgres', '2023-08-18 00:01:18.432666', 6, true); + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/dokument/mockvedtak.pdf b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/dokument/mockvedtak.pdf new file mode 100644 index 000000000..97f7a444f Binary files /dev/null and b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/dokument/mockvedtak.pdf differ diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/ekstern/bisys-gammel-periode.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/ekstern/bisys-gammel-periode.json new file mode 100644 index 000000000..95723db31 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/ekstern/bisys-gammel-periode.json @@ -0,0 +1,11 @@ +{ + "perioder": [ + { + "stønadstype": "UTVIDET", + "fomMåned": "2017-01", + "tomMåned": "2018-12", + "beløp": 970, + "manueltBeregnet": false + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/ekstern/bisys-med-perioder.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/ekstern/bisys-med-perioder.json new file mode 100644 index 000000000..fd0b1179f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/ekstern/bisys-med-perioder.json @@ -0,0 +1,18 @@ +{ + "perioder": [ + { + "stønadstype": "UTVIDET", + "fomMåned": "2019-12", + "tomMåned": null, + "beløp": 1054, + "manueltBeregnet": false + }, + { + "stønadstype": "SMÅBARNSTILLEGG", + "fomMåned": "2019-12", + "tomMåned": null, + "beløp": 660, + "manueltBeregnet": false + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/ekstern/bisys-tom.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/ekstern/bisys-tom.json new file mode 100644 index 000000000..23fd58b41 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/ekstern/bisys-tom.json @@ -0,0 +1,3 @@ +{ + "perioder": [] +} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/hentMilj\303\270variabler.sh" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/hentMilj\303\270variabler.sh" new file mode 100644 index 000000000..ae199a3ed --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/hentMilj\303\270variabler.sh" @@ -0,0 +1,18 @@ +kubectl config use-context dev-gcp +PODNAVN=$(kubectl -n teamfamilie get pods --field-selector=status.phase==Running -o name | grep familie-ba-sak | grep -v "frontend" | sed "s/^.\{4\}//" | head -n 1); + +PODVARIABLER="$(kubectl -n teamfamilie exec -c familie-ba-sak -it "$PODNAVN" -- env)" +UNLEASH_VARIABLER="$(kubectl -n teamfamilie get secret familie-ba-sak-unleash-api-token -o json | jq '.data | map_values(@base64d)')" + +AZURE_APP_CLIENT_ID="$(echo "$PODVARIABLER" | grep "AZURE_APP_CLIENT_ID" | tr -d '\r' )" +AZURE_APP_CLIENT_SECRET="$(echo "$PODVARIABLER" | grep "AZURE_APP_CLIENT_SECRET" | tr -d '\r' )"; + +UNLEASH_SERVER_API_URL="$(echo "$UNLEASH_VARIABLER" | grep "UNLEASH_SERVER_API_URL" | sed 's/:/=/1' | tr -d ' "')" +UNLEASH_SERVER_API_TOKEN="$(echo "$UNLEASH_VARIABLER" | grep "UNLEASH_SERVER_API_TOKEN" | sed 's/:/=/1' | tr -d ' ,"')" + +if [ -z "$AZURE_APP_CLIENT_ID" ] +then + return 1 +else + printf "%s;%s;%s;%s" "$AZURE_APP_CLIENT_ID" "$AZURE_APP_CLIENT_SECRET" "$UNLEASH_SERVER_API_URL" "$UNLEASH_SERVER_API_TOKEN" +fi \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kalkulator/request.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kalkulator/request.json new file mode 100644 index 000000000..3dfbfe187 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kalkulator/request.json @@ -0,0 +1,36 @@ +[ + { + "personident": "1111111111111", + "ytelsetype": "SMÅBARNSTILLEGG", + "stønadFom": "2020-06", + "stønadTom": "2026-05" + }, + { + "personident": "1111111111111", + "ytelsetype": "UTVIDET_BARNETRYGD", + "stønadFom": "2020-10", + "stønadTom": "2022-03", + "halvYtelse": false + }, + { + "personident": "2222222222222", + "ytelsetype": "ORDINÆR_BARNETRYGD", + "stønadFom": "2020-04", + "stønadTom": "2031-01", + "halvYtelse": true + }, + { + "personident": "333333333333333", + "ytelsetype": "ORDINÆR_BARNETRYGD", + "stønadFom": "2020-04", + "stønadTom": "2038-03", + "halvYtelse": false + }, + { + "personident": "44444444444444", + "ytelsetype": "EØS", + "stønadFom": "2021-04", + "stønadTom": "2021-08", + "beløp":936 + } +] \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kalkulator/response.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kalkulator/response.json new file mode 100644 index 000000000..a43f68144 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kalkulator/response.json @@ -0,0 +1,57 @@ +{ + "totaler": { + "totalbeløp": 531420, + "personligeYtelserMedBeløp": [ + { + "personident": "1111111111111", + "type": "ORDINÆR_BARNETRYGD", + "fullYtelse": true, + "beløp": 231000 + }, + { + "personident": "222222222222", + "type": "SMÅBARNSTILLEGG", + "fullYtelse": false, + "beløp": 18420 + } + ] + }, + "perioder": [ + { + "måned": "2019-01", + "totaltBeløp": 1384, + "personligeYtelserMedBeløp": [ + { + "personident": "1111111111111", + "type": "ORDINÆR_BARNETRYGD", + "fullYtelse": true, + "beløp": 1054 + }, + { + "personident": "222222222222", + "type": "SMÅBARNSTILLEGG", + "fullYtelse": false, + "beløp": 330 + } + ] + }, + { + "måned": "2019-02", + "totalbeløp": 1384, + "personligeYtelserMedBeløp": [ + { + "personident": "1111111111111", + "type": "ORDINÆR_BARNETRYGD", + "fullYtelse": true, + "beløp": 1054 + }, + { + "personident": "222222222222", + "type": "SMÅBARNSTILLEGG", + "fullYtelse": false, + "beløp": 330 + } + ] + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kjerne/simulering/simulering_med_manuell_postering.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kjerne/simulering/simulering_med_manuell_postering.json new file mode 100644 index 000000000..67193c086 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kjerne/simulering/simulering_med_manuell_postering.json @@ -0,0 +1,928 @@ +{ + "simuleringMottaker": [ + { + "mottakerNummer": "01234567890", + "mottakerType": "BRUKER", + "simulertPostering": [ + { + "id": 5661414, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-01-01", + "tom": "2022-01-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661415, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-01-01", + "tom": "2022-01-31", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661416, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-01-01", + "tom": "2022-01-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661417, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-01-01", + "tom": "2022-01-31", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661418, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661419, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "KREDIT", + "beløp": -153, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661420, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "DEBIT", + "beløp": 306, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661421, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661422, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "KREDIT", + "beløp": -153, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661423, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661424, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661425, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "KREDIT", + "beløp": -153, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661426, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "DEBIT", + "beløp": 306, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661427, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661428, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "KREDIT", + "beløp": -153, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661429, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661430, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-04-01", + "tom": "2022-04-30", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661431, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-04-01", + "tom": "2022-04-30", + "betalingType": "KREDIT", + "beløp": -198, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661432, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-04-01", + "tom": "2022-04-30", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661433, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-04-01", + "tom": "2022-04-30", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661434, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-04-01", + "tom": "2022-04-30", + "betalingType": "KREDIT", + "beløp": -107, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661435, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-04-01", + "tom": "2022-04-30", + "betalingType": "DEBIT", + "beløp": 165, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661436, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-05-01", + "tom": "2022-05-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661437, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-05-01", + "tom": "2022-05-31", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661438, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-05-01", + "tom": "2022-05-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661439, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-05-01", + "tom": "2022-05-31", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661440, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-06-01", + "tom": "2022-06-30", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661441, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-06-01", + "tom": "2022-06-30", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661442, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-06-01", + "tom": "2022-06-30", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661443, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-06-01", + "tom": "2022-06-30", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661444, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-07-01", + "tom": "2022-07-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661445, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-07-01", + "tom": "2022-07-31", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661446, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-07-01", + "tom": "2022-07-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661447, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-07-01", + "tom": "2022-07-31", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661448, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-08-01", + "tom": "2022-08-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661449, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-08-01", + "tom": "2022-08-31", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661450, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-08-01", + "tom": "2022-08-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661751, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-08-01", + "tom": "2022-08-31", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661752, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-09-01", + "tom": "2022-09-30", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661753, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-09-01", + "tom": "2022-09-30", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661754, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-09-01", + "tom": "2022-09-30", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661755, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-09-01", + "tom": "2022-09-30", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661756, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-10-01", + "tom": "2022-10-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661757, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-10-01", + "tom": "2022-10-31", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661758, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-10-01", + "tom": "2022-10-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661759, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-10-01", + "tom": "2022-10-31", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661760, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-11-01", + "tom": "2022-11-30", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661761, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-11-01", + "tom": "2022-11-30", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661762, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-11-01", + "tom": "2022-11-30", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661763, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-11-01", + "tom": "2022-11-30", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661764, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-12-01", + "tom": "2022-12-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661765, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-12-01", + "tom": "2022-12-31", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661766, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-12-01", + "tom": "2022-12-31", + "betalingType": "DEBIT", + "beløp": 305, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + }, + { + "id": 5661767, + "fk_okonomi_simulering_mottaker_id": 1228768, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-12-01", + "tom": "2022-12-31", + "betalingType": "KREDIT", + "beløp": -305, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "utenInntrekk": false, + "opprettet_av": "T126296", + "opprettet_tid": "2023-01-06 09:55:37.633", + "endret_av": "T126296", + "endret_tid": "2023-01-06 09:55:37.633", + "versjon": 0 + } + ] + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kjerne/simulering/simulering_med_mottrekk.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kjerne/simulering/simulering_med_mottrekk.json new file mode 100644 index 000000000..7c2aa0f4e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kjerne/simulering/simulering_med_mottrekk.json @@ -0,0 +1,1136 @@ +{ + "simuleringMottaker": [ + { + "mottakerNummer": "01234567890", + "mottakerType": "BRUKER", + "simulertPostering": [ + { + "id": 5670526, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "DEBIT", + "beløp": 658, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670527, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "KREDIT", + "beløp": -658, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670528, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "DEBIT", + "beløp": 46, + "posteringType": "FEILUTBETALING", + "erFeilkonto": false, + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670529, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "DEBIT", + "beløp": 46, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670530, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "DEBIT", + "beløp": 611, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670531, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "KREDIT", + "beløp": -46, + "posteringType": "MOTP", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670532, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670533, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "DEBIT", + "beløp": 3, + "posteringType": "FEILUTBETALING", + "erFeilkonto": false, + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670534, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "DEBIT", + "beløp": 3, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670535, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "DEBIT", + "beløp": 47, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670536, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "KREDIT", + "beløp": -3, + "posteringType": "MOTP", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670537, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-02-01", + "tom": "2022-02-28", + "betalingType": "KREDIT", + "beløp": -50, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670538, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "DEBIT", + "beløp": 658, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670539, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "KREDIT", + "beløp": -658, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670540, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "DEBIT", + "beløp": 46, + "posteringType": "FEILUTBETALING", + "erFeilkonto": false, + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670541, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "DEBIT", + "beløp": 46, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670542, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "DEBIT", + "beløp": 611, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670543, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "KREDIT", + "beløp": -46, + "posteringType": "MOTP", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670544, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670545, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "DEBIT", + "beløp": 3, + "posteringType": "FEILUTBETALING", + "erFeilkonto": false, + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670546, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "DEBIT", + "beløp": 3, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670547, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "DEBIT", + "beløp": 47, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670548, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "KREDIT", + "beløp": -3, + "posteringType": "MOTP", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670549, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-03-01", + "tom": "2022-03-31", + "betalingType": "KREDIT", + "beløp": -50, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670550, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-04-01", + "tom": "2022-04-30", + "betalingType": "DEBIT", + "beløp": 658, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670651, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-04-01", + "tom": "2022-04-30", + "betalingType": "KREDIT", + "beløp": -329, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670652, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-04-01", + "tom": "2022-04-30", + "betalingType": "DEBIT", + "beløp": 657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670653, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-04-01", + "tom": "2022-04-30", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670654, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-04-01", + "tom": "2022-04-30", + "betalingType": "KREDIT", + "beløp": -328, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670655, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-04-01", + "tom": "2022-04-30", + "betalingType": "DEBIT", + "beløp": 657, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670656, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-05-01", + "tom": "2022-05-31", + "betalingType": "DEBIT", + "beløp": 658, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670657, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-05-01", + "tom": "2022-05-31", + "betalingType": "KREDIT", + "beløp": -329, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670658, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-05-01", + "tom": "2022-05-31", + "betalingType": "DEBIT", + "beløp": 657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670659, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-05-01", + "tom": "2022-05-31", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670660, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-05-01", + "tom": "2022-05-31", + "betalingType": "KREDIT", + "beløp": -328, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670661, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-05-01", + "tom": "2022-05-31", + "betalingType": "DEBIT", + "beløp": 657, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670662, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-06-01", + "tom": "2022-06-30", + "betalingType": "DEBIT", + "beløp": 658, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670663, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-06-01", + "tom": "2022-06-30", + "betalingType": "KREDIT", + "beløp": -544, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670664, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-06-01", + "tom": "2022-06-30", + "betalingType": "DEBIT", + "beløp": 657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670665, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-06-01", + "tom": "2022-06-30", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670666, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-06-01", + "tom": "2022-06-30", + "betalingType": "KREDIT", + "beløp": -113, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670667, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD_MANUELT", + "fom": "2022-06-01", + "tom": "2022-06-30", + "betalingType": "DEBIT", + "beløp": 136, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670668, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-07-01", + "tom": "2022-07-31", + "betalingType": "DEBIT", + "beløp": 658, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670669, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-07-01", + "tom": "2022-07-31", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670670, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-07-01", + "tom": "2022-07-31", + "betalingType": "DEBIT", + "beløp": 657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670671, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-07-01", + "tom": "2022-07-31", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670672, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-08-01", + "tom": "2022-08-31", + "betalingType": "DEBIT", + "beløp": 658, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670673, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-08-01", + "tom": "2022-08-31", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670674, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-08-01", + "tom": "2022-08-31", + "betalingType": "DEBIT", + "beløp": 657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670675, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-08-01", + "tom": "2022-08-31", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670676, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-09-01", + "tom": "2022-09-30", + "betalingType": "DEBIT", + "beløp": 658, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670677, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-09-01", + "tom": "2022-09-30", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670678, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-09-01", + "tom": "2022-09-30", + "betalingType": "DEBIT", + "beløp": 657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670679, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-09-01", + "tom": "2022-09-30", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670680, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-10-01", + "tom": "2022-10-31", + "betalingType": "DEBIT", + "beløp": 658, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670681, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-10-01", + "tom": "2022-10-31", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670682, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-10-01", + "tom": "2022-10-31", + "betalingType": "DEBIT", + "beløp": 657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670683, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-10-01", + "tom": "2022-10-31", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670684, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-11-01", + "tom": "2022-11-30", + "betalingType": "DEBIT", + "beløp": 658, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670685, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-11-01", + "tom": "2022-11-30", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670686, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-11-01", + "tom": "2022-11-30", + "betalingType": "DEBIT", + "beløp": 657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670687, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-11-01", + "tom": "2022-11-30", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670688, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-12-01", + "tom": "2022-12-31", + "betalingType": "DEBIT", + "beløp": 658, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670689, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD", + "fom": "2022-12-01", + "tom": "2022-12-31", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670690, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-12-01", + "tom": "2022-12-31", + "betalingType": "DEBIT", + "beløp": 657, + "posteringType": "JUSTERING", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + }, + { + "id": 5670691, + "fk_okonomi_simulering_mottaker_id": 1229110, + "fagOmrådeKode": "BARNETRYGD_INFOTRYGD", + "fom": "2022-12-01", + "tom": "2022-12-31", + "betalingType": "KREDIT", + "beløp": -657, + "posteringType": "YTELSE", + "forfallsdato": "2023-01-06", + "uten_inntrekk": false, + "opprettetAv": "V142809", + "opprettetTid": "2023-01-06 13:11:12.551", + "endretAv": "V142809", + "endretTid": "2023-01-06 13:11:12.551", + "versjon": 0 + } + ] + } + ] +} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kjerne/vedtak.vedtaksperiode/Vilk\303\245rPerBarnBlirSl\303\245ttSammenTilPerioderSomSkalBegrunnesIVedtak.png" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kjerne/vedtak.vedtaksperiode/Vilk\303\245rPerBarnBlirSl\303\245ttSammenTilPerioderSomSkalBegrunnesIVedtak.png" new file mode 100644 index 000000000..32efb879e Binary files /dev/null and "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/kjerne/vedtak.vedtaksperiode/Vilk\303\245rPerBarnBlirSl\303\245ttSammenTilPerioderSomSkalBegrunnesIVedtak.png" differ diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/landkoder/landkoder.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/landkoder/landkoder.json new file mode 100644 index 000000000..3e0eec5c2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/landkoder/landkoder.json @@ -0,0 +1,1006 @@ +[ + { + "code": "NO", + "name": "Norge" + }, + { + "code": "AD", + "name": "Andorra" + }, + { + "code": "AE", + "name": "De forente arabiske emirater" + }, + { + "code": "AF", + "name": "Afghanistan" + }, + { + "code": "AG", + "name": "Antigua og Barbuda" + }, + { + "code": "AI", + "name": "Anguilla" + }, + { + "code": "AL", + "name": "Albania" + }, + { + "code": "AM", + "name": "Armenia" + }, + { + "code": "AO", + "name": "Angola" + }, + { + "code": "AQ", + "name": "Antarktis" + }, + { + "code": "AR", + "name": "Argentina" + }, + { + "code": "AS", + "name": "Amerikansk Samoa" + }, + { + "code": "AT", + "name": "Østerrike" + }, + { + "code": "AU", + "name": "Australia" + }, + { + "code": "AW", + "name": "Aruba" + }, + { + "code": "AX", + "name": "Åland" + }, + { + "code": "AZ", + "name": "Aserbajdsjan" + }, + { + "code": "BA", + "name": "Bosnia-Hercegovina" + }, + { + "code": "BB", + "name": "Barbados" + }, + { + "code": "BD", + "name": "Bangladesh" + }, + { + "code": "BE", + "name": "Belgia" + }, + { + "code": "BF", + "name": "Burkina Faso" + }, + { + "code": "BG", + "name": "Bulgaria" + }, + { + "code": "BH", + "name": "Bahrain" + }, + { + "code": "BI", + "name": "Burundi" + }, + { + "code": "BJ", + "name": "Benin" + }, + { + "code": "BL", + "name": "Saint Barthelemy" + }, + { + "code": "BM", + "name": "Bermuda" + }, + { + "code": "BN", + "name": "Brunei Darussalam" + }, + { + "code": "BO", + "name": "Bolivia" + }, + { + "code": "BQ", + "name": "Bonaire, Sint Eustatius og Saba" + }, + { + "code": "BR", + "name": "Brasil" + }, + { + "code": "BS", + "name": "Bahamas" + }, + { + "code": "BT", + "name": "Bhutan" + }, + { + "code": "BV", + "name": "Bouvetøya" + }, + { + "code": "BW", + "name": "Botswana" + }, + { + "code": "BY", + "name": "Hviterussland" + }, + { + "code": "BZ", + "name": "Belize" + }, + { + "code": "CA", + "name": "Canada" + }, + { + "code": "CC", + "name": "Kokosøyene (Keelingøyene)" + }, + { + "code": "CD", + "name": "Kongo" + }, + { + "code": "CF", + "name": "Den sentralafrikanske republikk" + }, + { + "code": "CG", + "name": "Kongo-Brazzaville" + }, + { + "code": "CH", + "name": "Sveits" + }, + { + "code": "CI", + "name": "Elfenbenskysten" + }, + { + "code": "CK", + "name": "Cookøyene" + }, + { + "code": "CL", + "name": "Chile" + }, + { + "code": "CM", + "name": "Kamerun" + }, + { + "code": "CN", + "name": "Kina" + }, + { + "code": "CO", + "name": "Colombia" + }, + { + "code": "CR", + "name": "Costa Rica" + }, + { + "code": "CU", + "name": "Cuba" + }, + { + "code": "CV", + "name": "Kapp Verde" + }, + { + "code": "CW", + "name": "Curacao" + }, + { + "code": "CX", + "name": "Christmasøya" + }, + { + "code": "CY", + "name": "Kypros" + }, + { + "code": "CZ", + "name": "Tsjekkia" + }, + { + "code": "DE", + "name": "Tyskland" + }, + { + "code": "DJ", + "name": "Djibouti" + }, + { + "code": "DK", + "name": "Danmark" + }, + { + "code": "DM", + "name": "Dominica" + }, + { + "code": "DO", + "name": "Den dominikanske republikk" + }, + { + "code": "DZ", + "name": "Algerie" + }, + { + "code": "EC", + "name": "Ecuador" + }, + { + "code": "EE", + "name": "Estland" + }, + { + "code": "EG", + "name": "Egypt" + }, + { + "code": "EH", + "name": "Vest-Sahara" + }, + { + "code": "ER", + "name": "Eritrea" + }, + { + "code": "ES", + "name": "Spania" + }, + { + "code": "ET", + "name": "Etiopia" + }, + { + "code": "FI", + "name": "Finland" + }, + { + "code": "FJ", + "name": "Fiji" + }, + { + "code": "FK", + "name": "Falklandsøyene (Malvinas)" + }, + { + "code": "FM", + "name": "Mikronesiaføderasjonen" + }, + { + "code": "FO", + "name": "Færøyene" + }, + { + "code": "FR", + "name": "Frankrike" + }, + { + "code": "GA", + "name": "Gabon" + }, + { + "code": "GB", + "name": "Storbritannia" + }, + { + "code": "GD", + "name": "Grenada" + }, + { + "code": "GE", + "name": "Georgia" + }, + { + "code": "GF", + "name": "Fransk Guyana" + }, + { + "code": "GG", + "name": "Guernsey" + }, + { + "code": "GH", + "name": "Ghana" + }, + { + "code": "GI", + "name": "Gibraltar" + }, + { + "code": "GL", + "name": "Grønland" + }, + { + "code": "GM", + "name": "Gambia" + }, + { + "code": "GN", + "name": "Guinea" + }, + { + "code": "GP", + "name": "Guadeloupe" + }, + { + "code": "GQ", + "name": "Ekvatorial-Guinea" + }, + { + "code": "GR", + "name": "Hellas" + }, + { + "code": "GS", + "name": "Sør-Georgia/Sør-Sandwichøyene" + }, + { + "code": "GT", + "name": "Guatemala" + }, + { + "code": "GU", + "name": "Guam" + }, + { + "code": "GW", + "name": "Guinea-Bissau" + }, + { + "code": "GY", + "name": "Guyana" + }, + { + "code": "HK", + "name": "Hongkong" + }, + { + "code": "HM", + "name": "Heard- og McDonaldøyene" + }, + { + "code": "HN", + "name": "Honduras" + }, + { + "code": "HR", + "name": "Kroatia" + }, + { + "code": "HT", + "name": "Haiti" + }, + { + "code": "HU", + "name": "Ungarn" + }, + { + "code": "ID", + "name": "Indonesia" + }, + { + "code": "IE", + "name": "Irland" + }, + { + "code": "IL", + "name": "Israel" + }, + { + "code": "IM", + "name": "Isle of Man" + }, + { + "code": "IN", + "name": "India" + }, + { + "code": "IO", + "name": "Det Britiske terr. i Indiahavet" + }, + { + "code": "IQ", + "name": "Irak" + }, + { + "code": "IR", + "name": "Iran" + }, + { + "code": "IS", + "name": "Island" + }, + { + "code": "IT", + "name": "Italia" + }, + { + "code": "JE", + "name": "Jersey" + }, + { + "code": "JM", + "name": "Jamaica" + }, + { + "code": "JO", + "name": "Jordan" + }, + { + "code": "JP", + "name": "Japan" + }, + { + "code": "KE", + "name": "Kenya" + }, + { + "code": "KG", + "name": "Kirgisistan" + }, + { + "code": "KH", + "name": "Kambodsja" + }, + { + "code": "KI", + "name": "Kiribati" + }, + { + "code": "KM", + "name": "Komorene" + }, + { + "code": "KN", + "name": "Saint Kitts og Nevis" + }, + { + "code": "KP", + "name": "Nord-Korea" + }, + { + "code": "KR", + "name": "Sør-Korea" + }, + { + "code": "KW", + "name": "Kuwait" + }, + { + "code": "KY", + "name": "Caymanøyene" + }, + { + "code": "KZ", + "name": "Kasakhstan" + }, + { + "code": "LA", + "name": "Laos" + }, + { + "code": "LB", + "name": "Libanon" + }, + { + "code": "LC", + "name": "Saint Lucia" + }, + { + "code": "LI", + "name": "Liechtenstein" + }, + { + "code": "LK", + "name": "Sri Lanka" + }, + { + "code": "LR", + "name": "Liberia" + }, + { + "code": "LS", + "name": "Lesotho" + }, + { + "code": "LT", + "name": "Litauen" + }, + { + "code": "LU", + "name": "Luxembourg" + }, + { + "code": "LV", + "name": "Latvia" + }, + { + "code": "LY", + "name": "Libya" + }, + { + "code": "MA", + "name": "Marokko" + }, + { + "code": "MC", + "name": "Monaco" + }, + { + "code": "MD", + "name": "Moldova" + }, + { + "code": "ME", + "name": "Montenegro" + }, + { + "code": "MF", + "name": "Saint-Martin, FR" + }, + { + "code": "MG", + "name": "Madagaskar" + }, + { + "code": "MH", + "name": "Marshalløyene" + }, + { + "code": "MK", + "name": "Nord-Makedonia" + }, + { + "code": "ML", + "name": "Mali" + }, + { + "code": "MM", + "name": "Myanmar/Burma" + }, + { + "code": "MN", + "name": "Mongolia" + }, + { + "code": "MO", + "name": "Macao" + }, + { + "code": "MP", + "name": "Nord-Marianene" + }, + { + "code": "MQ", + "name": "Martinique" + }, + { + "code": "MR", + "name": "Mauritania" + }, + { + "code": "MS", + "name": "Montserrat" + }, + { + "code": "MT", + "name": "Malta" + }, + { + "code": "MU", + "name": "Mauritius" + }, + { + "code": "MV", + "name": "Maldivene" + }, + { + "code": "MW", + "name": "Malawi" + }, + { + "code": "MX", + "name": "Mexico" + }, + { + "code": "MY", + "name": "Malaysia" + }, + { + "code": "MZ", + "name": "Mosambik" + }, + { + "code": "NA", + "name": "Namibia" + }, + { + "code": "NC", + "name": "Ny-Caledonia" + }, + { + "code": "NE", + "name": "Niger" + }, + { + "code": "NF", + "name": "Norfolkøya" + }, + { + "code": "NG", + "name": "Nigeria" + }, + { + "code": "NI", + "name": "Nicaragua" + }, + { + "code": "NL", + "name": "Nederland" + }, + { + "code": "NP", + "name": "Nepal" + }, + { + "code": "NR", + "name": "Nauru" + }, + { + "code": "NU", + "name": "Niue" + }, + { + "code": "NZ", + "name": "New Zealand" + }, + { + "code": "OM", + "name": "Oman" + }, + { + "code": "PA", + "name": "Panama" + }, + { + "code": "PE", + "name": "Peru" + }, + { + "code": "PF", + "name": "Fransk Polynesia" + }, + { + "code": "PG", + "name": "Papua Ny-Guinea" + }, + { + "code": "PH", + "name": "Filippinene" + }, + { + "code": "PK", + "name": "Pakistan" + }, + { + "code": "PL", + "name": "Polen" + }, + { + "code": "PM", + "name": "Saint Pierre og Miquelon" + }, + { + "code": "PN", + "name": "Pitcairn" + }, + { + "code": "PR", + "name": "Puerto Rico" + }, + { + "code": "PS", + "name": "Palestina" + }, + { + "code": "PT", + "name": "Portugal" + }, + { + "code": "PW", + "name": "Palau" + }, + { + "code": "PY", + "name": "Paraguay" + }, + { + "code": "QA", + "name": "Qatar" + }, + { + "code": "RE", + "name": "Reunion" + }, + { + "code": "RO", + "name": "Romania" + }, + { + "code": "RS", + "name": "Serbia" + }, + { + "code": "RU", + "name": "Russland" + }, + { + "code": "RW", + "name": "Rwanda" + }, + { + "code": "SA", + "name": "Saudi-Arabia" + }, + { + "code": "SB", + "name": "Salomonøyene" + }, + { + "code": "SC", + "name": "Seychellene" + }, + { + "code": "SD", + "name": "Sudan" + }, + { + "code": "SE", + "name": "Sverige" + }, + { + "code": "SG", + "name": "Singapore" + }, + { + "code": "SH", + "name": "St. Helena" + }, + { + "code": "SI", + "name": "Slovenia" + }, + { + "code": "SK", + "name": "Slovakia" + }, + { + "code": "SL", + "name": "Sierra Leone" + }, + { + "code": "SM", + "name": "San Marino" + }, + { + "code": "SN", + "name": "Senegal" + }, + { + "code": "SO", + "name": "Somalia" + }, + { + "code": "SR", + "name": "Surinam" + }, + { + "code": "SS", + "name": "Sør-Sudan" + }, + { + "code": "ST", + "name": "Sao Tome og Principe" + }, + { + "code": "SV", + "name": "El Salvador" + }, + { + "code": "SX", + "name": "Sint Marteen, NL" + }, + { + "code": "SY", + "name": "Syria" + }, + { + "code": "SZ", + "name": "Eswatini" + }, + { + "code": "TC", + "name": "Turks- og Caicosøyene" + }, + { + "code": "TD", + "name": "Tsjad" + }, + { + "code": "TF", + "name": "Franske Sørlige Territorier" + }, + { + "code": "TG", + "name": "Togo" + }, + { + "code": "TH", + "name": "Thailand" + }, + { + "code": "TJ", + "name": "Tadsjikistan" + }, + { + "code": "TK", + "name": "Tokelau" + }, + { + "code": "TL", + "name": "Øst-Timor" + }, + { + "code": "TM", + "name": "Turkmenistan" + }, + { + "code": "TN", + "name": "Tunisia" + }, + { + "code": "TO", + "name": "Tonga" + }, + { + "code": "TR", + "name": "Tyrkia" + }, + { + "code": "TT", + "name": "Trinidad og Tobago" + }, + { + "code": "TV", + "name": "Tuvalu" + }, + { + "code": "TW", + "name": "Taiwan" + }, + { + "code": "TZ", + "name": "Tanzania" + }, + { + "code": "UA", + "name": "Ukraina" + }, + { + "code": "UG", + "name": "Uganda" + }, + { + "code": "UM", + "name": "USAs ytre småøyer" + }, + { + "code": "US", + "name": "USA" + }, + { + "code": "UY", + "name": "Uruguay" + }, + { + "code": "UZ", + "name": "Usbekistan" + }, + { + "code": "VA", + "name": "Vatikanstaten" + }, + { + "code": "VC", + "name": "Saint Vincent og Grenadinene" + }, + { + "code": "VE", + "name": "Venezuela" + }, + { + "code": "VG", + "name": "Jomfruøyene, Britisk" + }, + { + "code": "VI", + "name": "Jomfruøyene, US" + }, + { + "code": "VN", + "name": "Vietnam" + }, + { + "code": "VU", + "name": "Vanuatu" + }, + { + "code": "WF", + "name": "Wallis- og Futunaøyene" + }, + { + "code": "WS", + "name": "Samoa" + }, + { + "code": "XB", + "name": "Kanariøyene" + }, + { + "code": "XC", + "name": "Ceuta og Melilla" + }, + { + "code": "XK", + "name": "Kosovo" + }, + { + "code": "YE", + "name": "Jemen" + }, + { + "code": "YT", + "name": "Mayotte" + }, + { + "code": "ZA", + "name": "Sør-Afrika" + }, + { + "code": "ZM", + "name": "Zambia" + }, + { + "code": "ZW", + "name": "Zimbabwe" + } +] \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/logback-test.xml b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/logback-test.xml new file mode 100644 index 000000000..5058ce913 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/automatiske_begrunnelser.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/automatiske_begrunnelser.feature new file mode 100644 index 000000000..d3a06515e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/automatiske_begrunnelser.feature @@ -0,0 +1,54 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for manuell saksbehandling + + Bakgrunn: + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET | SØKNAD | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.02.1988 | + | 1 | 4567 | BARN | 17.10.2005 | + | 1 | 5678 | BARN | 16.06.2010 | + + Scenario: Begrunnelser med øvrig trigger automatisk skal ikke være inkludert + Og følgende dagens dato 18.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD | | 11.02.1988 | | OPPFYLT | Nei | + | 1234 | BOSATT_I_RIKET | | 15.12.2022 | | OPPFYLT | Nei | + + | 4567 | BOSATT_I_RIKET,GIFT_PARTNERSKAP,LOVLIG_OPPHOLD,BOR_MED_SØKER | | 17.10.2005 | | OPPFYLT | Nei | + | 4567 | UNDER_18_ÅR | | 17.10.2005 | 16.10.2023 | OPPFYLT | Nei | + + | 5678 | BOR_MED_SØKER,BOSATT_I_RIKET,GIFT_PARTNERSKAP,LOVLIG_OPPHOLD | | 16.06.2010 | | OPPFYLT | Nei | + | 5678 | UNDER_18_ÅR | | 16.06.2010 | 15.06.2028 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 4567 | 1 | 01.01.2023 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 4567 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 4567 | 1 | 01.07.2023 | 30.09.2023 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + | 5678 | 1 | 01.01.2023 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 5678 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 5678 | 1 | 01.07.2023 | 31.05.2028 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.01.2023 | 28.02.2023 | UTBETALING | | | | + | 01.03.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | 30.09.2023 | UTBETALING | | | | + | 01.10.2023 | 31.05.2028 | UTBETALING | | | REDUKSJON_UNDER_18_ÅR_AUTOVEDTAK | + | 01.06.2028 | | OPPHØR | | | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/avslag.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/avslag.feature new file mode 100644 index 000000000..5dbcd1c07 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/avslag.feature @@ -0,0 +1,39 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for avslag + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 1 | 1 | | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1 | SØKER | 03.01.1978 | + | 1 | 2 | BARN | 16.02.2007 | + + Scenario: Skal ikke krasje ved avslag uten fom- eller tomdato + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1 | LOVLIG_OPPHOLD | | | | IKKE_OPPFYLT | Ja | + | 1 | UTVIDET_BARNETRYGD,BOSATT_I_RIKET | | 22.09.2022 | | OPPFYLT | Nei | + + | 2 | LOVLIG_OPPHOLD | | | | IKKE_OPPFYLT | Ja | + | 2 | GIFT_PARTNERSKAP | | 16.02.2007 | | OPPFYLT | Nei | + | 2 | UNDER_18_ÅR | | 16.02.2007 | 15.02.2025 | OPPFYLT | Nei | + | 2 | BOR_MED_SØKER,BOSATT_I_RIKET | | 22.09.2022 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + + Og med endrede utbetalinger for begrunnelse + | AktørId | Fra dato | Til dato | BehandlingId | Årsak | Prosent | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | | | AVSLAG | | | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/brev_periode_type.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/brev_periode_type.feature new file mode 100644 index 000000000..b44702bf8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/brev_periode_type.feature @@ -0,0 +1,70 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for forskjellige brevperiodetyper + + Bakgrunn: + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | DELVIS_INNVILGET_OG_OPPHØRT | SØKNAD | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1 | SØKER | 03.07.1967 | + | 1 | 2 | BARN | 29.04.2020 | + | 1 | 3 | BARN | 02.12.2019 | + + Og følgende dagens dato 25.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1 | LOVLIG_OPPHOLD | | 03.07.1967 | | OPPFYLT | Nei | + | 1 | BOSATT_I_RIKET | | 25.08.2020 | 25.09.2020 | OPPFYLT | Nei | + + | 2 | UNDER_18_ÅR | | 29.04.2020 | 28.04.2038 | OPPFYLT | Nei | + | 2 | LOVLIG_OPPHOLD,BOR_MED_SØKER,GIFT_PARTNERSKAP,BOSATT_I_RIKET | | 29.04.2020 | | OPPFYLT | Nei | + + | 3 | BOR_MED_SØKER,GIFT_PARTNERSKAP,LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 02.12.2019 | | OPPFYLT | Nei | + | 3 | UNDER_18_ÅR | | 02.12.2019 | 01.12.2037 | OPPFYLT | Nei | + + Scenario: Skal gi begrunnelse knyttet til utbetaling dersom det fremdeles er utbetaling + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 3 | 1 | 01.09.2020 | 30.09.2020 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 2 | 1 | 01.09.2020 | 30.09.2020 | 0 | ORDINÆR_BARNETRYGD | 0 | 1354 | + + Og med endrede utbetalinger for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent | + | 2 | 1 | 01.09.2020 | 01.09.2020 | ETTERBETALING_3ÅR | 0 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.09.2020 | 30.09.2020 | UTBETALING | | ENDRET_UTBETALING_TRE_ÅR_TILBAKE_I_TID_UTBETALING | ENDRET_UTBETALING_ETTERBETALING_TRE_ÅR_TILBAKE_I_TID | + | 01.10.2020 | | OPPHØR | | | | + + Scenario: Skal gi begrunnelse knyttet til "ingen utbetaling" dersom det ikke er utbetaling + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 3 | 1 | 01.09.2020 | 30.09.2020 | 0 | ORDINÆR_BARNETRYGD | 0 | 1354 | + | 2 | 1 | 01.09.2020 | 30.09.2020 | 0 | ORDINÆR_BARNETRYGD | 0 | 1354 | + + Og med endrede utbetalinger for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent | + | 2 | 1 | 01.09.2020 | 01.09.2020 | ETTERBETALING_3ÅR | 0 | + | 3 | 1 | 01.09.2020 | 01.09.2020 | ETTERBETALING_3ÅR | 0 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.09.2020 | 30.09.2020 | OPPHØR | | ENDRET_UTBETALING_ETTERBETALING_TRE_ÅR_TILBAKE_I_TID | ENDRET_UTBETALING_TRE_ÅR_TILBAKE_I_TID_UTBETALING | + | 01.10.2020 | | OPPHØR | | | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/endret_utbetaling.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/endret_utbetaling.feature new file mode 100644 index 000000000..3a4e74a47 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/endret_utbetaling.feature @@ -0,0 +1,146 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for endret utbetaling + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | + | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Begrunnelse endret utbetaling delt bosted + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | + + Og med endrede utbetalinger for begrunnelse + | AktørId | Fra dato | Til dato | BehandlingId | Årsak | Prosent | + | 3456 | 01.05.2020 | 31.01.2021 | 1 | DELT_BOSTED | 0 | + | 3456 | 01.02.2021 | 31.03.2038 | 1 | DELT_BOSTED | 100 | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | Prosent | + | 3456 | 01.05.2020 | 31.01.2021 | 0 | 1 | 0 | + | 3456 | 01.02.2021 | 31.03.2038 | 1354 | 1 | 100 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 31.01.2021 | UTBETALING | ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_KUN_ETTERBETALT_UTVIDET_NY | ENDRET_UTBETALING_SEKUNDÆR_DELT_BOSTED_FULL_UTBETALING_FØR_SØKNAD | + | 01.02.2021 | 31.03.2038 | UTBETALING | ENDRET_UTBETALING_SEKUNDÆR_DELT_BOSTED_FULL_UTBETALING_FØR_SØKNAD, ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_KUN_ETTERBETALT_UTVIDET_NY | | + | 01.04.2038 | | OPPHØR | OPPHØR_UNDER_18_ÅR | | + + Scenario: Begrunnelse etter endret utbetaling ETTERBETALING_3ÅR + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | + + Og med endrede utbetalinger for begrunnelse + | AktørId | Fra dato | Til dato | BehandlingId | Årsak | Prosent | + | 3456 | 01.05.2020 | 31.01.2021 | 1 | ETTERBETALING_3ÅR | 0 | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | Prosent | + | 3456 | 01.05.2020 | 31.01.2021 | 0 | 1 | 0 | + | 3456 | 01.02.2021 | 31.03.2038 | 1000 | 1 | 100 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.02.2021 | 31.03.2038 | UTBETALING | ETTER_ENDRET_UTBETALING_ETTERBETALING | | + | 01.04.2038 | | OPPHØR | OPPHØR_UNDER_18_ÅR | | + + + Scenario: Skal ikke krasje dersom siste periode er endret til null prosent + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 1 | 1 | | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | BARN | 03.08.2017 | + | 1 | 4567 | SØKER | 05.06.1988 | + + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 4567 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 05.06.1988 | | OPPFYLT | Nei | + + | 1234 | GIFT_PARTNERSKAP,BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 03.08.2017 | | OPPFYLT | Nei | + | 1234 | UNDER_18_ÅR | | 03.08.2017 | 02.08.2035 | OPPFYLT | Nei | + | 1234 | BOR_MED_SØKER | | 19.07.2023 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 1234 | 1 | 01.08.2023 | 31.08.2023 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.09.2023 | 31.07.2035 | 0 | ORDINÆR_BARNETRYGD | 0 | + + Og med endrede utbetalinger for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent | + | 1234 | 1 | 01.09.2023 | 01.07.2035 | ENDRE_MOTTAKER | 0 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.08.2023 | 31.08.2023 | UTBETALING | | | | + | 01.09.2023 | 31.07.2035 | OPPHØR | | | | + | 01.08.2035 | | OPPHØR | | | | + + + Scenario: Skal ikke ta med endret utbetalingsperioder som har type reduksjon dersom det ikke har vært en reduksjon + Og følgende dagens dato 2023-09-13 + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 4567 | BARN | 02.02.2015 | + | 1 | 1234 | SØKER | 06.06.1985 | + + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 06.06.1985 | | OPPFYLT | Nei | + + | 4567 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | Nei | + | 4567 | GIFT_PARTNERSKAP,BOR_MED_SØKER,BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 02.02.2015 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 4567 | 1 | 01.03.2015 | 31.08.2020 | 0 | ORDINÆR_BARNETRYGD | 0 | + | 4567 | 1 | 01.09.2020 | 31.01.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.02.2021 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.07.2023 | 31.01.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | + + Og med endrede utbetalinger for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent | + | 4567 | 1 | 01.03.2015 | 01.08.2020 | ETTERBETALING_3ÅR | 0 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.03.2015 | 31.08.2020 | OPPHØR | | ENDRET_UTBETALING_ETTERBETALING_TRE_ÅR_TILBAKE_I_TID | ENDRET_UTBETALING_ETTERBETALING_TRE_ÅR_TILBAKE_I_TID_KUN_UTVIDET_DEL_UTBETALING | + | 01.09.2020 | 31.01.2021 | UTBETALING | | | | + | 01.02.2021 | 28.02.2023 | UTBETALING | | | | + | 01.03.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | 31.01.2033 | UTBETALING | | | | + | 01.02.2033 | | OPPHØR | | | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/endret_utbetaling_med_reduksjon.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/endret_utbetaling_med_reduksjon.feature new file mode 100644 index 000000000..a4f4b4464 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/endret_utbetaling_med_reduksjon.feature @@ -0,0 +1,63 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser endret utbetaling med reduksjon + + Bakgrunn: + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | DELVIS_INNVILGET | SØKNAD | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 14.10.1987 | + | 1 | 3456 | BARN | 08.02.2013 | + | 1 | 5678 | BARN | 13.01.2017 | + + Scenario: Begrunnelse endret utbetaling - endre mottaker + Og følgende dagens dato 04.10.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | 14.10.1987 | | OPPFYLT | + + | 3456 | UNDER_18_ÅR | 08.02.2013 | 07.02.2031 | OPPFYLT | + | 3456 | GIFT_PARTNERSKAP | 08.02.2013 | | OPPFYLT | + | 3456 | BOSATT_I_RIKET,BOR_MED_SØKER,LOVLIG_OPPHOLD | 01.02.2022 | | OPPFYLT | + + | 5678 | UNDER_18_ÅR | 13.01.2017 | 12.01.2035 | OPPFYLT | + | 5678 | GIFT_PARTNERSKAP | 13.01.2017 | | OPPFYLT | + | 5678 | BOR_MED_SØKER,BOSATT_I_RIKET,LOVLIG_OPPHOLD | 01.02.2022 | | OPPFYLT | + + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 3456 | 1 | 01.03.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 3456 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 3456 | 1 | 01.07.2023 | 31.01.2031 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 5678 | 1 | 01.03.2022 | 31.12.2022 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 5678 | 1 | 01.01.2023 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 5678 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 5678 | 1 | 01.07.2023 | 30.09.2023 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 5678 | 1 | 01.10.2023 | 31.12.2034 | 0 | ORDINÆR_BARNETRYGD | 0 | 1310 | + + Og med endrede utbetalinger for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent | + | 5678 | 1 | 01.10.2023 | 31.12.2034 | ENDRE_MOTTAKER | 0 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.03.2022 | 31.12.2022 | UTBETALING | | | | + | 01.01.2023 | 28.02.2023 | UTBETALING | | | | + | 01.03.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | 30.09.2023 | UTBETALING | | | | + | 01.10.2023 | 31.01.2031 | UTBETALING | | ENDRET_UTBETALING_REDUKSJON_ENDRE_MOTTAKER | | + | 01.02.2031 | 31.12.2034 | OPPHØR | | | | + | 01.01.2035 | | OPPHØR | | | | \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/etter_endret_utbetaling.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/etter_endret_utbetaling.feature new file mode 100644 index 000000000..fb9e7db04 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/etter_endret_utbetaling.feature @@ -0,0 +1,75 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for etter endret utbetaling, en mor med ett barn + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | + | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Begrunnelse etter endret utbetaling delt bosted + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 03.01.2021 | Oppfylt | + | 3456 | BOR_MED_SØKER | 04.01.2021 | 15.01.2022 | Oppfylt | + | 3456 | BOR_MED_SØKER | 16.01.2022 | | Oppfylt | + + Og med endrede utbetalinger for begrunnelse + | AktørId | Fra dato | Til dato | BehandlingId | Årsak | Prosent | + | 3456 | 01.05.2020 | 31.01.2021 | 1 | DELT_BOSTED | 0 | + | 3456 | 01.02.2021 | 31.01.2022 | 1 | DELT_BOSTED | 100 | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | Prosent | + | 3456 | 01.05.2020 | 31.01.2021 | 0 | 1 | 0 | + | 3456 | 01.02.2021 | 31.01.2022 | 1354 | 1 | 100 | + | 3456 | 01.02.2022 | 31.03.2038 | 1354 | 1 | 100 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | Kommentar | + | 01.05.2020 | 31.01.2021 | UTBETALING | | | Ingen etter endret utbetalingsbegrunnelse skal med | + | 01.02.2021 | 31.01.2022 | UTBETALING | | ETTER_ENDRET_UTBETALING_HAR_AVTALE_DELT_BOSTED | | + | 01.02.2022 | 31.03.2038 | UTBETALING | ETTER_ENDRET_UTBETALING_HAR_AVTALE_DELT_BOSTED | | | + | 01.04.2038 | | OPPHØR | OPPHØR_UNDER_18_ÅR | | | + + Scenario: Begrunnelse etter endret utbetaling allerede utbetalt + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | + + Og med endrede utbetalinger for begrunnelse + | AktørId | Fra dato | Til dato | BehandlingId | Årsak | Prosent | + | 3456 | 01.05.2020 | 31.01.2021 | 1 | ETTERBETALING_3ÅR | 0 | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | Prosent | + | 3456 | 01.05.2020 | 31.01.2021 | 0 | 1 | 0 | + | 3456 | 01.02.2021 | 31.03.2038 | 1354 | 1 | 100 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 31.01.2021 | OPPHØR | | | + | 01.02.2021 | 31.03.2038 | UTBETALING | ETTER_ENDRET_UTBETALING_ETTERBETALING | | + | 01.04.2038 | | OPPHØR | OPPHØR_UNDER_18_ÅR | | + + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/fagsaktype.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/fagsaktype.feature new file mode 100644 index 000000000..f5f182179 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/fagsaktype.feature @@ -0,0 +1,71 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for fagsaktype + + Scenario: Skal ikke gi institusjonsbegrunnelser når vi har normal fagsak + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 200057161 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 100175168 | 200057161 | | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 100175168 | 2578520707923 | SØKER | 05.01.1983 | + | 100175168 | 2034260303343 | BARN | 02.09.2004 | + + Og lag personresultater for begrunnelse for behandling 100175168 + + Og legg til nye vilkårresultater for begrunnelse for behandling 100175168 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 2578520707923 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 05.01.1983 | | OPPFYLT | Nei | + + | 2034260303343 | UNDER_18_ÅR | | 02.09.2004 | 01.09.2022 | OPPFYLT | Nei | + | 2034260303343 | BOR_MED_SØKER,GIFT_PARTNERSKAP,BOSATT_I_RIKET | | 02.09.2004 | | OPPFYLT | Nei | + | 2034260303343 | LOVLIG_OPPHOLD | | 01.07.2022 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 2034260303343 | 100175168 | 01.08.2022 | 31.08.2022 | 1054 | ORDINÆR_BARNETRYGD | 100 | + + Når begrunnelsetekster genereres for behandling 100175168 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.08.2022 | 31.08.2022 | UTBETALING | | | | + | 01.09.2022 | | OPPHØR | | OPPHØR_UNDER_18_ÅR | OPPHØR_BARNET_ER_18_ÅR_INSTITUSJON | + + Scenario: Skal gi institusjonsbegrunnelser for institusjonssak + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 200057108 | INSTITUSJON | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 100175169 | 200057108 | | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 100175169 | 2034260303343 | BARN | 02.09.2004 | + + Og lag personresultater for begrunnelse for behandling 100175169 + + Og legg til nye vilkårresultater for begrunnelse for behandling 100175169 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 2034260303343 | BOSATT_I_RIKET,GIFT_PARTNERSKAP,BOR_MED_SØKER | | 02.09.2004 | | OPPFYLT | Nei | + | 2034260303343 | UNDER_18_ÅR | | 02.09.2004 | 01.09.2022 | OPPFYLT | Nei | + | 2034260303343 | LOVLIG_OPPHOLD | | 01.07.2022 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 2034260303343 | 100175169 | 01.08.2022 | 31.08.2022 | 1054 | ORDINÆR_BARNETRYGD | 100 | + + Når begrunnelsetekster genereres for behandling 100175169 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.08.2022 | 31.08.2022 | UTBETALING | | | | + | 01.09.2022 | | OPPHØR | | OPPHØR_BARNET_ER_18_ÅR_INSTITUSJON | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/fortsatt_innvilget.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/fortsatt_innvilget.feature new file mode 100644 index 000000000..9c3ef05cf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/fortsatt_innvilget.feature @@ -0,0 +1,66 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for fortsatt innvilget + + Bakgrunn: + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | ENDRET_UTBETALING | NYE_OPPLYSNINGER | + | 2 | 1 | 1 | FORTSATT_INNVILGET | NYE_OPPLYSNINGER | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1 | SØKER | 31.10.1987 | + | 1 | 2 | BARN | 19.02.2011 | + | 2 | 1 | SØKER | 31.10.1987 | + | 2 | 2 | BARN | 19.02.2011 | + + Scenario: Skal gi begrunnelser som passer med + Og følgende dagens dato 20.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1 | LOVLIG_OPPHOLD | | 31.10.1987 | | OPPFYLT | Nei | + | 1 | BOSATT_I_RIKET | | 31.10.1987 | 14.06.2023 | OPPFYLT | Nei | + | 1 | BOSATT_I_RIKET | VURDERT_MEDLEMSKAP | 15.06.2023 | | OPPFYLT | Nei | + + | 2 | GIFT_PARTNERSKAP,LOVLIG_OPPHOLD,BOR_MED_SØKER | | 19.02.2011 | | OPPFYLT | Nei | + | 2 | BOSATT_I_RIKET | | 19.02.2011 | 14.06.2023 | OPPFYLT | Nei | + | 2 | UNDER_18_ÅR | | 19.02.2011 | 18.02.2029 | OPPFYLT | Nei | + | 2 | BOSATT_I_RIKET | VURDERT_MEDLEMSKAP | 15.06.2023 | | OPPFYLT | Nei | + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1 | LOVLIG_OPPHOLD | | 31.10.1987 | | OPPFYLT | Nei | + | 1 | BOSATT_I_RIKET | | 31.10.1987 | 14.06.2023 | OPPFYLT | Nei | + | 1 | BOSATT_I_RIKET | VURDERT_MEDLEMSKAP | 15.06.2023 | | OPPFYLT | Nei | + + | 2 | GIFT_PARTNERSKAP,LOVLIG_OPPHOLD,BOR_MED_SØKER | | 19.02.2011 | | OPPFYLT | Nei | + | 2 | BOSATT_I_RIKET | | 19.02.2011 | 14.06.2023 | OPPFYLT | Nei | + | 2 | UNDER_18_ÅR | | 19.02.2011 | 18.02.2029 | OPPFYLT | Nei | + | 2 | BOSATT_I_RIKET | VURDERT_MEDLEMSKAP | 15.06.2023 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 2 | 1 | 01.03.2011 | 28.02.2019 | 970 | ORDINÆR_BARNETRYGD | 100 | 970 | + | 2 | 1 | 01.03.2019 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 2 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 2 | 1 | 01.07.2023 | 31.01.2029 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 2 | 2 | 01.03.2011 | 28.02.2019 | 970 | ORDINÆR_BARNETRYGD | 100 | 970 | + | 2 | 2 | 01.03.2019 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 2 | 2 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 2 | 2 | 01.07.2023 | 31.01.2029 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + Når begrunnelsetekster genereres for behandling 2 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk Inkluderte Begrunnelser | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | | | FORTSATT_INNVILGET | | FORTSATT_INNVILGET_MEDLEM_I_FOLKETRYGDEN | FORTSATT_INNVILGET_SØKER_BOSATT_I_RIKET, FORTSATT_INNVILGET_FORVARING_GIFT, FORTSATT_INNVILGET_FORTSATT_AVTALE_OM_DELT_BOSTED | + | | | FORTSATT_INNVILGET | EØS_FORORDNINGEN | | FORTSATT_INNVILGET_PRIMÆRLAND_STANDARD | \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/hendelser.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/hendelser.feature new file mode 100644 index 000000000..2d3f60c5a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/hendelser.feature @@ -0,0 +1,90 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for hendelser + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | + | 1 | + + Scenario: Skal ta med 6-års begrunnelse når barn blir 6 år + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | Dødsfalldato | + | 1 | 1234 | SØKER | 11.01.1970 | | + | 1 | 3456 | BARN | 13.04.2017 | | + + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2017 | 12.04.2035 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2017 | | Oppfylt | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2017 | 31.03.2023 | 1354 | 1 | + | 3456 | 01.04.2023 | 31.03.2035 | 1054 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2017 | 31.03.2023 | UTBETALING | | | + | 01.04.2023 | 31.03.2035 | UTBETALING | REDUKSJON_UNDER_6_ÅR | | + | 01.04.2035 | | OPPHØR | OPPHØR_UNDER_18_ÅR | | + + Scenario: Skal ta med dødsfallbegrunnelse om barnet er dødt + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | Dødsfalldato | + | 1 | 1234 | SØKER | 11.01.1970 | | + | 1 | 5678 | BARN | 13.04.2017 | 02.03.2024 | + + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 5678 | UNDER_18_ÅR | 13.04.2017 | 12.04.2035 | Oppfylt | + | 5678 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2017 | 02.03.2024 | Oppfylt | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 5678 | 01.05.2017 | 31.03.2023 | 1354 | 1 | + | 5678 | 01.04.2023 | 31.03.2024 | 1054 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2017 | 31.03.2023 | UTBETALING | | | + | 01.04.2023 | 31.03.2024 | UTBETALING | REDUKSJON_UNDER_6_ÅR | | + | 01.04.2024 | | OPPHØR | OPPHØR_BARN_DØD | | + + Scenario: Skal ta med satsendringbegrunnelse ved satsendring + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | Dødsfalldato | + | 1 | 1234 | SØKER | 11.01.1970 | | + | 1 | 3456 | BARN | 13.04.2017 | | + + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2017 | 12.04.2035 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2017 | | Oppfylt | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2017 | 28.02.2023 | 1676 | 1 | + | 3456 | 01.03.2023 | 31.03.2035 | 1083 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2017 | 28.02.2023 | UTBETALING | | | + | 01.03.2023 | 31.03.2035 | UTBETALING | REDUKSJON_UNDER_6_ÅR, REDUKSJON_SATSENDRING | | + | 01.04.2035 | | OPPHØR | OPPHØR_UNDER_18_ÅR | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/innvilget_ved_ingen_endring.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/innvilget_ved_ingen_endring.feature new file mode 100644 index 000000000..7174c97e4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/innvilget_ved_ingen_endring.feature @@ -0,0 +1,132 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser ved ingen endring + + Bakgrunn: + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET | SØKNAD | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1 | SØKER | 06.11.1984 | + | 1 | 2 | BARN | 07.09.2019 | + + Scenario: Gi innvilget-begrunnelser når det ikke er endring i andelene + Og følgende dagens dato 15.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 2 | UNDER_18_ÅR | | 07.09.2019 | 06.09.2037 | OPPFYLT | Nei | + | 2 | GIFT_PARTNERSKAP | | 07.09.2019 | | OPPFYLT | Nei | + | 2 | LOVLIG_OPPHOLD | | 07.09.2019 | 14.07.2023 | OPPFYLT | Nei | + | 2 | BOSATT_I_RIKET | BARN_BOR_I_NORGE | 07.09.2019 | 14.07.2023 | OPPFYLT | Nei | + | 2 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 07.06.2023 | 14.07.2023 | OPPFYLT | Nei | + | 2 | BOSATT_I_RIKET,LOVLIG_OPPHOLD,BOR_MED_SØKER | | 15.07.2023 | 15.08.2023 | OPPFYLT | Nei | + + | 1 | LOVLIG_OPPHOLD | | 06.11.1984 | | OPPFYLT | Nei | + | 1 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 11.11.2021 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 2 | 1 | 01.07.2023 | 31.08.2025 | 1766 | ORDINÆR_BARNETRYGD | 100 | 1766 | + + Og med kompetanser for begrunnelse + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland | + | 2 | 01.07.2023 | 31.07.2023 | NORGE_ER_PRIMÆRLAND | 1 | ARBEIDER | I_ARBEID | NO | BE | BE | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.07.2023 | 31.07.2023 | UTBETALING | | | | + | 01.08.2023 | 31.08.2023 | UTBETALING | | INNVILGET_OVERGANG_EØS_TIL_NASJONAL_SEPARASJONSAVTALEN | | + | 01.09.2023 | | OPPHØR | | | | + + Scenario: Skal inkludere begrunnelser med "Innvilget eller økning" resultat dersom periode resultatet er ingen endring for EØS + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | DELVIS_INNVILGET | SØKNAD | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | BARN | 18.11.2015 | + | 1 | 4567 | SØKER | 01.11.1976 | + | 1 | 5678 | BARN | 09.03.2021 | + + Og følgende dagens dato 25.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | GIFT_PARTNERSKAP | | 18.11.2015 | | OPPFYLT | Nei | + | 1234 | UNDER_18_ÅR | | 18.11.2015 | 17.11.2033 | OPPFYLT | Nei | + | 1234 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 12.01.2016 | | OPPFYLT | Nei | + | 1234 | LOVLIG_OPPHOLD | | 12.01.2016 | | OPPFYLT | Nei | + | 1234 | BOSATT_I_RIKET | BARN_BOR_I_EØS | 12.01.2016 | | OPPFYLT | Nei | + + | 4567 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 12.01.2016 | | OPPFYLT | Nei | + | 4567 | LOVLIG_OPPHOLD | | 12.01.2016 | | OPPFYLT | Nei | + + | 5678 | UNDER_18_ÅR | | 09.03.2021 | 08.03.2039 | OPPFYLT | Nei | + | 5678 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 09.03.2021 | | OPPFYLT | Nei | + | 5678 | BOSATT_I_RIKET | BARN_BOR_I_EØS | 09.03.2021 | | OPPFYLT | Nei | + | 5678 | LOVLIG_OPPHOLD | | 09.03.2021 | | OPPFYLT | Nei | + | 5678 | GIFT_PARTNERSKAP | | 09.03.2021 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 5678 | 1 | 01.04.2021 | 31.08.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 5678 | 1 | 01.09.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | 1654 | + | 5678 | 1 | 01.01.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 5678 | 1 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | 1723 | + | 5678 | 1 | 01.07.2023 | 28.02.2027 | 1766 | ORDINÆR_BARNETRYGD | 100 | 1766 | + | 5678 | 1 | 01.03.2027 | 28.02.2039 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 1234 | 1 | 01.02.2016 | 31.07.2019 | 0 | ORDINÆR_BARNETRYGD | 0 | 970 | + | 1234 | 1 | 01.08.2019 | 31.12.2019 | 943 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 1234 | 1 | 01.01.2020 | 31.08.2020 | 936 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 1234 | 1 | 01.09.2020 | 31.12.2020 | 1236 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 1234 | 1 | 01.01.2021 | 31.03.2021 | 1241 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 1234 | 1 | 01.04.2021 | 31.08.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 1234 | 1 | 01.09.2021 | 31.10.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | 1654 | + | 1234 | 1 | 01.11.2021 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 1234 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 1234 | 1 | 01.07.2023 | 31.10.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + Og med endrede utbetalinger for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent | + | 1234 | 1 | 01.02.2016 | 01.07.2019 | ETTERBETALING_3ÅR | 0 | + + Og med kompetanser for begrunnelse + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland | + | 5678, 1234 | 01.04.2021 | | NORGE_ER_PRIMÆRLAND | 1 | ARBEIDER | INAKTIV | NO | LV | LV | + | 1234 | 01.08.2019 | 31.03.2021 | NORGE_ER_SEKUNDÆRLAND | 1 | ARBEIDER | I_ARBEID | NO | LV | LV | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk Inkluderte Begrunnelser | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.02.2016 | 31.07.2019 | OPPHØR | | | | + | 01.08.2019 | 31.12.2019 | UTBETALING | | | | + | 01.01.2020 | 31.08.2020 | UTBETALING | EØS_FORORDNINGEN | INNVILGET_TILLEGGSTEKST_SATSENDRING_OG_VALUTAJUSTERING | | + | 01.09.2020 | 31.12.2020 | UTBETALING | | | | + | 01.01.2021 | 31.03.2021 | UTBETALING | | | | + | 01.04.2021 | 31.08.2021 | UTBETALING | | | | + | 01.09.2021 | 31.10.2021 | UTBETALING | | | | + | 01.11.2021 | 31.12.2021 | UTBETALING | | | | + | 01.01.2022 | 28.02.2023 | UTBETALING | | | | + | 01.03.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | 28.02.2027 | UTBETALING | | | | + | 01.03.2027 | 31.10.2033 | UTBETALING | | | | + | 01.11.2033 | 28.02.2039 | UTBETALING | | | | + | 01.03.2039 | | OPPHØR | | | | \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/kombinasjon_av_vilk\303\245r_og_endretutbetaling.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/kombinasjon_av_vilk\303\245r_og_endretutbetaling.feature" new file mode 100644 index 000000000..fee72d9b5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/kombinasjon_av_vilk\303\245r_og_endretutbetaling.feature" @@ -0,0 +1,85 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for kombinasjon av utgjørende vilkår og endret utbetaling årsaker + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 1 | 1 | | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | BARN | 02.02.2015 | + | 1 | 4567 | SØKER | 11.02.1985 | + + Scenario: Begrunnelse skal ikke vises dersom bare utgjørende vilkår er oppfylt + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | GIFT_PARTNERSKAP | | 02.02.2015 | | OPPFYLT | Nei | + | 1234 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | Nei | + | 1234 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 01.02.2022 | | OPPFYLT | Nei | + | 1234 | BOR_MED_SØKER | | 01.02.2022 | 08.05.2022 | OPPFYLT | Nei | + | 1234 | BOR_MED_SØKER | DELT_BOSTED_SKAL_IKKE_DELES | 09.05.2022 | | OPPFYLT | Nei | + + | 4567 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 01.02.2022 | | OPPFYLT | Nei | + | 4567 | UTVIDET_BARNETRYGD | | 09.05.2022 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 1234 | 1 | 01.03.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.07.2023 | 31.01.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.06.2022 | 28.02.2023 | 1054 | UTVIDET_BARNETRYGD | 100 | + | 4567 | 1 | 01.03.2023 | 30.06.2023 | 2489 | UTVIDET_BARNETRYGD | 100 | + | 4567 | 1 | 01.07.2023 | 31.01.2033 | 2516 | UTVIDET_BARNETRYGD | 100 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.03.2022 | 31.05.2022 | UTBETALING | | | | + | 01.06.2022 | 28.02.2023 | UTBETALING | | | ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_MOTTATT_FULL_ORDINÆR_ETTERBETALT_UTVIDET_NY | + | 01.03.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | 31.01.2033 | UTBETALING | | | | + | 01.02.2033 | | OPPHØR | | | | + + Scenario: Begrunnelse skal vises dersom både utgjørende vilkår er oppfylt og endret utbetalingsårsak er oppfylt + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | GIFT_PARTNERSKAP | | 02.02.2015 | | OPPFYLT | Nei | + | 1234 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | Nei | + | 1234 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 01.02.2022 | | OPPFYLT | Nei | + | 1234 | BOR_MED_SØKER | | 01.02.2022 | 08.05.2022 | OPPFYLT | Nei | + | 1234 | BOR_MED_SØKER | DELT_BOSTED_SKAL_IKKE_DELES | 09.05.2022 | | OPPFYLT | Nei | + + | 4567 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 01.02.2022 | | OPPFYLT | Nei | + | 4567 | UTVIDET_BARNETRYGD | | 09.05.2022 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 1234 | 1 | 01.03.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.07.2023 | 31.01.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.06.2022 | 28.02.2023 | 1054 | UTVIDET_BARNETRYGD | 100 | + | 4567 | 1 | 01.03.2023 | 30.06.2023 | 2489 | UTVIDET_BARNETRYGD | 100 | + | 4567 | 1 | 01.07.2023 | 31.01.2033 | 2516 | UTVIDET_BARNETRYGD | 100 | + + Og med endrede utbetalinger for begrunnelse + | AktørId | Fra dato | Til dato | BehandlingId | Årsak | Prosent | + | 4567 | 01.06.2022 | 28.02.2023 | 1 | DELT_BOSTED | 100 | + + Når begrunnelsetekster genereres for behandling 1 + + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.03.2022 | 31.05.2022 | UTBETALING | | | | + | 01.06.2022 | 28.02.2023 | UTBETALING | | ENDRET_UTBETALINGSPERIODE_DELT_BOSTED_MOTTATT_FULL_ORDINÆR_ETTERBETALT_UTVIDET_NY | | + | 01.03.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | 31.01.2033 | UTBETALING | | | | + | 01.02.2033 | | OPPHØR | | | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/kompetanser.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/kompetanser.feature new file mode 100644 index 000000000..c5a7dac89 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/kompetanser.feature @@ -0,0 +1,187 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for kompetanser + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | + | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Skal gi innvilget primærland begrunnelse basert på kompetanse + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 30.04.2021 | 1054 | 1 | + | 3456 | 01.05.2021 | 31.03.2038 | 1354 | 1 | + + Og med kompetanser for begrunnelse + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Annen forelders aktivitet | Barnets bostedsland | + | 3456 | 01.05.2020 | 30.04.2021 | NORGE_ER_PRIMÆRLAND | 1 | IKKE_AKTUELT | NO | + | 3456 | 01.05.2021 | 31.03.2038 | NORGE_ER_SEKUNDÆRLAND | 1 | I_ARBEID | PL | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk Inkluderte Begrunnelser | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 30.04.2021 | Utbetaling | EØS_FORORDNINGEN | INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE | INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_JOBBER_I_NORGE | + | 01.05.2021 | 31.03.2038 | Utbetaling | EØS_FORORDNINGEN | INNVILGET_SEKUNDÆRLAND_STANDARD | INNVILGET_SEKUNDÆRLAND_TO_ARBEIDSLAND_NORGE_UTBETALER | + | 01.04.2038 | | Opphør | | | | + + Scenario: Ikke vis kompetansebegrunnelser dersom kompetansen ikke endrer seg + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 100173206 | 200055603 | | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 100173206 | 2013549321777 | BARN | 02.02.2015 | + | 100173206 | 1448019142841 | SØKER | 30.09.1984 | + + Og lag personresultater for begrunnelse for behandling 100173206 + + Og legg til nye vilkårresultater for begrunnelse for behandling 100173206 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1448019142841 | LOVLIG_OPPHOLD | | 30.09.1984 | | OPPFYLT | Nei | + | 1448019142841 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 15.03.2023 | | OPPFYLT | Nei | + + | 2013549321777 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 02.02.2015 | | OPPFYLT | Nei | + | 2013549321777 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP | | 02.02.2015 | | OPPFYLT | Nei | + | 2013549321777 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | Nei | + | 2013549321777 | BOSATT_I_RIKET | BARN_BOR_I_NORGE | 02.02.2015 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 2013549321777 | 100173206 | 01.04.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | + | 2013549321777 | 100173206 | 01.07.2023 | 31.07.2023 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 2013549321777 | 100173206 | 01.08.2023 | 31.01.2033 | 167 | ORDINÆR_BARNETRYGD | 100 | + + Og med kompetanser for begrunnelse + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Annen forelders aktivitet | Barnets bostedsland | + | 2013549321777 | 01.04.2023 | 01.07.2023 | NORGE_ER_PRIMÆRLAND | 100173206 | INAKTIV | NO | + | 2013549321777 | 01.08.2023 | | NORGE_ER_SEKUNDÆRLAND | 100173206 | I_ARBEID | GB | + + Når begrunnelsetekster genereres for behandling 100173206 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk Inkluderte Begrunnelser | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.04.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | 31.07.2023 | UTBETALING | EØS_FORORDNINGEN | | INNVILGET_PRIMÆRLAND_STANDARD | + | 01.08.2023 | 31.01.2033 | UTBETALING | | | | + | 01.02.2033 | | OPPHØR | | | | + + Scenario: Skal gi riktig begrunnelse ved opphør av EØS-sak + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 100173207 | 200055651 | | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 100173207 | 2005858678161 | BARN | 02.02.2015 | + | 100173207 | 2305793738737 | SØKER | 12.11.1984 | + + Og lag personresultater for begrunnelse for behandling 100173207 + + Og legg til nye vilkårresultater for begrunnelse for behandling 100173207 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 2005858678161 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP | | 02.02.2015 | | OPPFYLT | Nei | + | 2005858678161 | BOSATT_I_RIKET | BARN_BOR_I_NORGE | 02.02.2015 | | OPPFYLT | Nei | + | 2005858678161 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 02.02.2015 | | OPPFYLT | Nei | + | 2005858678161 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | Nei | + + | 2305793738737 | LOVLIG_OPPHOLD | | 12.11.1984 | | OPPFYLT | Nei | + | 2305793738737 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 15.03.2023 | 15.08.2023 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 2005858678161 | 100173207 | 01.04.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | + | 2005858678161 | 100173207 | 01.07.2023 | 31.08.2023 | 1310 | ORDINÆR_BARNETRYGD | 100 | + + Og med kompetanser for begrunnelse + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland | + | 2005858678161 | 01.04.2023 | 31.08.2023 | NORGE_ER_PRIMÆRLAND | 100173207 | ARBEIDER | MOTTAR_PENSJON | NO | BE | NO | + + Når begrunnelsetekster genereres for behandling 100173207 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk Inkluderte Begrunnelser | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.04.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | 31.08.2023 | UTBETALING | | | | + | 01.09.2023 | | OPPHØR | EØS_FORORDNINGEN | OPPHØR_IKKE_STATSBORGER_I_EØS_LAND | | + + Scenario: Skal begrunne endring i kompetanse når det ikke er noen endringer i resten av behandlingen + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 1 | 1 | | + | 2 | 1 | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | BARN | 02.02.2015 | + | 1 | 5678 | SØKER | 10.05.1985 | + | 2 | 1234 | BARN | 02.02.2015 | + | 2 | 5678 | SØKER | 10.05.1985 | + + Og følgende dagens dato 2023-09-12 + Og lag personresultater for begrunnelse for behandling 1 + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 5678 | LOVLIG_OPPHOLD | | 10.05.1985 | | OPPFYLT | Nei | + | 5678 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 15.02.2021 | | OPPFYLT | Nei | + + | 1234 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 02.02.2015 | | OPPFYLT | Nei | + | 1234 | BOSATT_I_RIKET | BARN_BOR_I_NORGE | 02.02.2015 | | OPPFYLT | Nei | + | 1234 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | Nei | + | 1234 | GIFT_PARTNERSKAP,LOVLIG_OPPHOLD | | 02.02.2015 | | OPPFYLT | Nei | + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | GIFT_PARTNERSKAP,LOVLIG_OPPHOLD | | 02.02.2015 | | OPPFYLT | Nei | + | 1234 | BOSATT_I_RIKET | BARN_BOR_I_NORGE | 02.02.2015 | | OPPFYLT | Nei | + | 1234 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | Nei | + | 1234 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 02.02.2015 | | OPPFYLT | Nei | + + | 5678 | LOVLIG_OPPHOLD | | 10.05.1985 | | OPPFYLT | Nei | + | 5678 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 15.02.2021 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 1234 | 1 | 01.03.2021 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.07.2023 | 31.01.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 2 | 01.03.2021 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 2 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 2 | 01.07.2023 | 31.01.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | + + Og med kompetanser for begrunnelse + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland | + | 1234 | 01.03.2021 | | NORGE_ER_PRIMÆRLAND | 1 | ARBEIDER | INAKTIV | NO | BE | NO | + | 1234 | 01.03.2021 | 30.04.2023 | NORGE_ER_PRIMÆRLAND | 2 | ARBEIDER | INAKTIV | NO | BE | NO | + | 1234 | 01.05.2023 | | NORGE_ER_PRIMÆRLAND | 2 | MOTTAR_UFØRETRYGD | INAKTIV | NO | EE | NO | + + Når begrunnelsetekster genereres for behandling 2 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk Inkluderte Begrunnelser | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2023 | 30.06.2023 | UTBETALING | EØS_FORORDNINGEN | INNVILGET_PRIMÆRLAND_BARNET_BOR_I_NORGE | REDUKSJON_BARN_DØD_EØS, REDUKSJON_IKKE_ANSVAR_FOR_BARN, FORTSATT_INNVILGET_PRIMÆRLAND_STANDARD | + | 01.07.2023 | 31.01.2033 | UTBETALING | | | | + | 01.02.2033 | | OPPHØR | | | | + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/mangler_data_person.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/mangler_data_person.feature new file mode 100644 index 000000000..6fbbc03c8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/mangler_data_person.feature @@ -0,0 +1,60 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser når ett barn fødes etter et annet + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 1 | 1 | | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 27.01.1985 | + | 1 | 4567 | BARN | 02.02.2015 | + | 1 | 6789 | BARN | 22.08.2022 | + + Scenario: Skal tåle periode uten vilkår på person fra mars 2022 til august 2022 når barn 6789 ikke er født. + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | + | 1234 | LOVLIG_OPPHOLD, BOSATT_I_RIKET | | 27.01.1985 | | OPPFYLT | + | 1234 | UTVIDET_BARNETRYGD | | 11.11.2022 | 15.05.2023 | OPPFYLT | + + | 4567 | BOR_MED_SØKER | VURDERING_ANNET_GRUNNLAG | 15.02.2022 | | OPPFYLT | + | 4567 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | | 02.02.2015 | | OPPFYLT | + | 4567 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | + + | 6789 | BOSATT_I_RIKET, LOVLIG_OPPHOLD, GIFT_PARTNERSKAP | | 22.08.2022 | | OPPFYLT | + | 6789 | BOR_MED_SØKER | DELT_BOSTED | 22.08.2022 | | OPPFYLT | + | 6789 | UNDER_18_ÅR | | 22.08.2022 | 21.08.2040 | OPPFYLT | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 6789 | 1 | 01.09.2022 | 01.02.2023 | 838 | ORDINÆR_BARNETRYGD | 50 | + | 6789 | 1 | 01.03.2023 | 01.06.2023 | 862 | ORDINÆR_BARNETRYGD | 50 | + | 6789 | 1 | 01.07.2023 | 01.07.2028 | 883 | ORDINÆR_BARNETRYGD | 50 | + | 6789 | 1 | 01.08.2028 | 01.07.2040 | 655 | ORDINÆR_BARNETRYGD | 50 | + | 4567 | 1 | 01.03.2022 | 01.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.03.2023 | 01.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.07.2023 | 01.01.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.12.2022 | 01.02.2023 | 1054 | UTVIDET_BARNETRYGD | 100 | + | 1234 | 1 | 01.03.2023 | 01.05.2023 | 2489 | UTVIDET_BARNETRYGD | 100 | + + Og med endrede utbetalinger for begrunnelse + | AktørId | Fra dato | Til dato | BehandlingId | Årsak | Prosent | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.03.2022 | 31.08.2022 | UTBETALING | | | | + | 01.09.2022 | 30.11.2022 | UTBETALING | | | | + | 01.12.2022 | 28.02.2023 | UTBETALING | | | | + | 01.03.2023 | 31.05.2023 | UTBETALING | | | | + | 01.06.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | 31.07.2028 | UTBETALING | | | | + | 01.08.2028 | 31.01.2033 | UTBETALING | | | | + | 01.02.2033 | 31.07.2040 | UTBETALING | | | | + | 01.08.2040 | | OPPHØR | | | | diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/opph\303\270r_fra_forrige_behandling.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/opph\303\270r_fra_forrige_behandling.feature" new file mode 100644 index 000000000..11da2b2b6 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/opph\303\270r_fra_forrige_behandling.feature" @@ -0,0 +1,201 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for opphør fra forrige behandling + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 1 | 1 | | + | 2 | 1 | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 16.09.1984 | + | 1 | 3456 | BARN | 07.09.2019 | + | 2 | 1234 | SØKER | 16.09.1984 | + | 2 | 3456 | BARN | 07.09.2019 | + + Scenario: Skal gi opphør fra forrige behandling-begrunnelser knyttet til bor med søker, men ikke delt bosted + Og lag personresultater for begrunnelse for behandling 1 + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 16.09.1984 | | OPPFYLT | Nei | + + | 3456 | LOVLIG_OPPHOLD,BOSATT_I_RIKET,GIFT_PARTNERSKAP | | 07.09.2019 | | OPPFYLT | Nei | + | 3456 | BOR_MED_SØKER | | 07.09.2019 | 31.12.2021 | OPPFYLT | Nei | + | 3456 | UNDER_18_ÅR | | 07.09.2019 | 06.09.2037 | OPPFYLT | Nei | + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 16.09.1984 | | OPPFYLT | Nei | + + | 3456 | BOSATT_I_RIKET,LOVLIG_OPPHOLD,GIFT_PARTNERSKAP | | 07.09.2019 | | OPPFYLT | Nei | + | 3456 | UNDER_18_ÅR | | 07.09.2019 | 06.09.2037 | OPPFYLT | Nei | + | 3456 | BOR_MED_SØKER | | 07.09.2020 | 31.12.2021 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 3456 | 1 | 01.10.2019 | 31.08.2020 | 1054 | ORDINÆR_BARNETRYGD | 100 | + | 3456 | 1 | 01.09.2020 | 31.08.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | + | 3456 | 1 | 01.09.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | + | 3456 | 2 | 01.10.2020 | 31.08.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | + | 3456 | 2 | 01.09.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | + + Når begrunnelsetekster genereres for behandling 2 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.10.2019 | 30.09.2020 | OPPHØR | | OPPHØR_BARN_BODDE_IKKE_MED_SØKER | OPPHØR_AVTALE_DELT_BOSTED_IKKE_GYLDIG | + | 01.10.2020 | 31.08.2021 | UTBETALING | | | | + | 01.09.2021 | 31.12.2021 | UTBETALING | | | | + | 01.01.2022 | | OPPHØR | | | | + + Scenario: Skal gi opphør fra forrige behandling-begrunnelser knyttet til bor med søker, med delt bosted + Og lag personresultater for begrunnelse for behandling 1 + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 16.09.1984 | | OPPFYLT | Nei | + + | 3456 | LOVLIG_OPPHOLD,BOSATT_I_RIKET,GIFT_PARTNERSKAP | | 07.09.2019 | | OPPFYLT | Nei | + | 3456 | UNDER_18_ÅR | | 07.09.2019 | 06.09.2037 | OPPFYLT | Nei | + | 3456 | BOR_MED_SØKER | DELT_BOSTED | 07.09.2019 | 06.09.2020 | OPPFYLT | Nei | + | 3456 | BOR_MED_SØKER | | 07.09.2020 | 31.12.2021 | OPPFYLT | Nei | + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 16.09.1984 | | OPPFYLT | Nei | + + | 3456 | BOSATT_I_RIKET,LOVLIG_OPPHOLD,GIFT_PARTNERSKAP | | 07.09.2019 | | OPPFYLT | Nei | + | 3456 | UNDER_18_ÅR | | 07.09.2019 | 06.09.2037 | OPPFYLT | Nei | + | 3456 | BOR_MED_SØKER | | 07.09.2020 | 31.12.2021 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 3456 | 1 | 01.10.2019 | 31.08.2020 | 1054 | ORDINÆR_BARNETRYGD | 100 | + | 3456 | 1 | 01.09.2020 | 31.08.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | + | 3456 | 1 | 01.09.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | + | 3456 | 2 | 01.10.2020 | 31.08.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | + | 3456 | 2 | 01.09.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | + + Når begrunnelsetekster genereres for behandling 2 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.10.2019 | 30.09.2020 | OPPHØR | | OPPHØR_AVTALE_DELT_BOSTED_IKKE_GYLDIG | OPPHØR_BARN_BODDE_IKKE_MED_SØKER | + | 01.10.2020 | 31.08.2021 | UTBETALING | | | | + | 01.09.2021 | 31.12.2021 | UTBETALING | | | | + | 01.01.2022 | | OPPHØR | | | | + + Scenario: Skal gi opphør fra forrige behandling-begrunnelser knyttet til bosatt i riket + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET | SØKNAD | + | 2 | 1 | 1 | ENDRET_UTBETALING | NYE_OPPLYSNINGER | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 29.05.1988 | + | 1 | 3456 | BARN | 28.04.2006 | + | 1 | 5678 | BARN | 01.05.2010 | + | 2 | 1234 | SØKER | 29.05.1988 | + | 2 | 3456 | BARN | 28.04.2006 | + | 2 | 5678 | BARN | 01.05.2010 | + + Og følgende dagens dato 15.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD | | 29.05.1988 | | OPPFYLT | Nei | + | 1234 | BOSATT_I_RIKET | | 11.11.2021 | | OPPFYLT | Nei | + + | 3456 | GIFT_PARTNERSKAP,LOVLIG_OPPHOLD,BOR_MED_SØKER,BOSATT_I_RIKET | | 28.04.2006 | | OPPFYLT | Nei | + | 3456 | UNDER_18_ÅR | | 28.04.2006 | 27.04.2024 | OPPFYLT | Nei | + + | 5678 | GIFT_PARTNERSKAP,BOR_MED_SØKER,BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 01.05.2010 | | OPPFYLT | Nei | + | 5678 | UNDER_18_ÅR | | 01.05.2010 | 30.04.2028 | OPPFYLT | Nei | + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD | | 29.05.1988 | | OPPFYLT | Nei | + | 1234 | BOSATT_I_RIKET | | 19.01.2022 | | OPPFYLT | Nei | + + | 3456 | BOSATT_I_RIKET,GIFT_PARTNERSKAP,BOR_MED_SØKER,LOVLIG_OPPHOLD | | 28.04.2006 | | OPPFYLT | Nei | + | 3456 | UNDER_18_ÅR | | 28.04.2006 | 27.04.2024 | OPPFYLT | Nei | + + | 5678 | BOSATT_I_RIKET,LOVLIG_OPPHOLD,GIFT_PARTNERSKAP,BOR_MED_SØKER | | 01.05.2010 | | OPPFYLT | Nei | + | 5678 | UNDER_18_ÅR | | 01.05.2010 | 30.04.2028 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 3456 | 1 | 01.12.2021 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 3456 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 3456 | 1 | 01.07.2023 | 31.03.2024 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 5678 | 1 | 01.12.2021 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 5678 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 5678 | 1 | 01.07.2023 | 30.04.2028 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + | 3456 | 2 | 01.02.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 3456 | 2 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 3456 | 2 | 01.07.2023 | 31.03.2024 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 5678 | 2 | 01.02.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 5678 | 2 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 5678 | 2 | 01.07.2023 | 30.04.2028 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + Når begrunnelsetekster genereres for behandling 2 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.12.2021 | 31.01.2022 | OPPHØR | | OPPHØR_IKKE_BOSATT_I_NORGE | | + | 01.02.2022 | 28.02.2023 | UTBETALING | | | | + | 01.03.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | 31.03.2024 | UTBETALING | | | | + | 01.04.2024 | 30.04.2028 | UTBETALING | | | | + | 01.05.2028 | | OPPHØR | | | | + + Scenario: Skal ikke gi opphør fra forrige behandling, men normalt avslag + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET_OG_OPPHØRT | SØKNAD | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 23.04.1985 | + | 1 | 3456 | BARN | 20.03.2015 | + Og følgende dagens dato 28.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD | | 23.04.1985 | | OPPFYLT | Nei | + | 1234 | BOSATT_I_RIKET | VURDERING_ANNET_GRUNNLAG | 01.06.2019 | 28.02.2022 | OPPFYLT | Nei | + + | 3456 | UNDER_18_ÅR | | 20.03.2015 | 19.03.2033 | OPPFYLT | Nei | + | 3456 | GIFT_PARTNERSKAP | | 20.03.2015 | | OPPFYLT | Nei | + | 3456 | BOR_MED_SØKER,LOVLIG_OPPHOLD | | 01.06.2019 | | OPPFYLT | Nei | + | 3456 | BOSATT_I_RIKET | | 19.11.2021 | 28.02.2022 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 3456 | 1 | 01.12.2021 | 28.02.2022 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.12.2021 | 28.02.2022 | UTBETALING | | | | + | 01.03.2022 | | OPPHØR | | AVSLAG_BOSATT_I_RIKET | OPPHØR_IKKE_BOSATT_I_NORGE | diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/overgangsst\303\270nad_og_sm\303\245barnstillegg.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/overgangsst\303\270nad_og_sm\303\245barnstillegg.feature" new file mode 100644 index 000000000..a0b126972 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/overgangsst\303\270nad_og_sm\303\245barnstillegg.feature" @@ -0,0 +1,119 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for overgangsstønad og småbarnstillegg + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 1 | 1 | | + | 2 | 2 | 1 | + + Og følgende dagens dato 05.09.2023 + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 05.04.1985 | + | 1 | 4567 | BARN | 22.08.2022 | + | 2 | 1234 | SØKER | 05.04.1985 | + | 2 | 4567 | BARN | 22.08.2022 | + + Scenario: Skal slå sammen tidligere overgangsstønad dersom periodene er sammenhengende + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD,UTVIDET_BARNETRYGD,BOSATT_I_RIKET | | 05.04.1985 | | OPPFYLT | Nei | + + | 4567 | LOVLIG_OPPHOLD,BOR_MED_SØKER,BOSATT_I_RIKET,GIFT_PARTNERSKAP | | 22.08.2022 | | OPPFYLT | Nei | + | 4567 | UNDER_18_ÅR | | 22.08.2022 | 21.08.2040 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 4567 | 1 | 01.09.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.07.2023 | 31.07.2028 | 1766 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.08.2028 | 31.07.2040 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.09.2022 | 28.02.2023 | 1054 | UTVIDET_BARNETRYGD | 100 | + | 1234 | 1 | 01.03.2023 | 30.06.2023 | 2489 | UTVIDET_BARNETRYGD | 100 | + | 1234 | 1 | 01.07.2023 | 31.07.2040 | 2516 | UTVIDET_BARNETRYGD | 100 | + | 1234 | 1 | 01.02.2023 | 28.02.2023 | 660 | SMÅBARNSTILLEGG | 100 | + | 1234 | 1 | 01.03.2023 | 30.06.2023 | 678 | SMÅBARNSTILLEGG | 100 | + | 1234 | 1 | 01.07.2023 | 31.08.2024 | 696 | SMÅBARNSTILLEGG | 100 | + + Og med overgangsstønad for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | + | 4567 | 1 | 01.02.2023 | 30.04.2023 | + | 4567 | 1 | 01.05.2023 | 31.08.2024 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.09.2022 | 31.01.2023 | UTBETALING | | | INNVILGET_SMÅBARNSTILLEGG | + | 01.02.2023 | 28.02.2023 | UTBETALING | | INNVILGET_SMÅBARNSTILLEGG | | + | 01.03.2023 | 30.06.2023 | UTBETALING | | INNVILGET_SMÅBARNSTILLEGG | | + | 01.07.2023 | 31.08.2024 | UTBETALING | | INNVILGET_SMÅBARNSTILLEGG | | + | 01.09.2024 | 31.07.2028 | UTBETALING | | | INNVILGET_SMÅBARNSTILLEGG | + | 01.08.2028 | 31.07.2040 | UTBETALING | | | | + | 01.08.2040 | | OPPHØR | | | | + + Scenario: Skal splitte på riktige tidspunkter dersom overgangsstønaden har blitt forlenget siden siste behandling + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD,UTVIDET_BARNETRYGD,BOSATT_I_RIKET | | 05.04.1985 | | OPPFYLT | Nei | + + | 4567 | LOVLIG_OPPHOLD,BOR_MED_SØKER,BOSATT_I_RIKET,GIFT_PARTNERSKAP | | 22.08.2022 | | OPPFYLT | Nei | + | 4567 | UNDER_18_ÅR | | 22.08.2022 | 21.08.2040 | OPPFYLT | Nei | + + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD,UTVIDET_BARNETRYGD,BOSATT_I_RIKET | | 05.04.1985 | | OPPFYLT | Nei | + + | 4567 | LOVLIG_OPPHOLD,BOR_MED_SØKER,BOSATT_I_RIKET,GIFT_PARTNERSKAP | | 22.08.2022 | | OPPFYLT | Nei | + | 4567 | UNDER_18_ÅR | | 22.08.2022 | 21.08.2040 | OPPFYLT | Nei | + + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 4567 | 1 | 01.09.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.07.2023 | 31.07.2028 | 1766 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.08.2028 | 31.07.2040 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.09.2022 | 28.02.2023 | 1054 | UTVIDET_BARNETRYGD | 100 | + | 1234 | 1 | 01.03.2023 | 30.06.2023 | 2489 | UTVIDET_BARNETRYGD | 100 | + | 1234 | 1 | 01.07.2023 | 31.07.2040 | 2516 | UTVIDET_BARNETRYGD | 100 | + | 1234 | 1 | 01.02.2023 | 28.02.2023 | 660 | SMÅBARNSTILLEGG | 100 | + | 1234 | 1 | 01.03.2023 | 30.06.2023 | 678 | SMÅBARNSTILLEGG | 100 | + | 1234 | 1 | 01.07.2023 | 31.10.2023 | 696 | SMÅBARNSTILLEGG | 100 | + + | 4567 | 2 | 01.09.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 2 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 2 | 01.07.2023 | 31.07.2028 | 1766 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 2 | 01.08.2028 | 31.07.2040 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 2 | 01.09.2022 | 28.02.2023 | 1054 | UTVIDET_BARNETRYGD | 100 | + | 1234 | 2 | 01.03.2023 | 30.06.2023 | 2489 | UTVIDET_BARNETRYGD | 100 | + | 1234 | 2 | 01.07.2023 | 31.07.2040 | 2516 | UTVIDET_BARNETRYGD | 100 | + | 1234 | 2 | 01.02.2023 | 28.02.2023 | 660 | SMÅBARNSTILLEGG | 100 | + | 1234 | 2 | 01.03.2023 | 30.06.2023 | 678 | SMÅBARNSTILLEGG | 100 | + | 1234 | 2 | 01.07.2023 | 31.08.2024 | 696 | SMÅBARNSTILLEGG | 100 | + + Og med overgangsstønad for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | + | 4567 | 1 | 01.02.2023 | 30.04.2023 | + | 4567 | 1 | 01.05.2023 | 31.10.2023 | + | 4567 | 2 | 01.02.2023 | 30.04.2023 | + | 4567 | 2 | 01.05.2023 | 31.08.2024 | + + Når begrunnelsetekster genereres for behandling 2 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.11.2023 | 31.08.2024 | UTBETALING | | INNVILGET_SMÅBARNSTILLEGG | | + | 01.09.2024 | 31.07.2028 | UTBETALING | | | INNVILGET_SMÅBARNSTILLEGG | + | 01.08.2028 | 31.07.2040 | UTBETALING | | | | + | 01.08.2040 | | OPPHØR | | | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/reduksjon_fra_forrige_behandling.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/reduksjon_fra_forrige_behandling.feature new file mode 100644 index 000000000..d3d8184d5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/reduksjon_fra_forrige_behandling.feature @@ -0,0 +1,335 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for reduksjon fra forrige behandling + + Scenario: Skal gi reduksjon fra forrige behandling-begrunnelser knyttet til bor med søker når bor med søker er innvilget en måned senere i revurdering + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 1 | 1 | | + | 2 | 1 | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1 | SØKER | 31.12.1993 | + | 1 | 2 | BARN | 15.03.2023 | + | 1 | 3 | BARN | 15.03.2023 | + | 2 | 1 | SØKER | 31.12.1993 | + | 2 | 2 | BARN | 15.03.2023 | + | 2 | 3 | BARN | 15.03.2023 | + + Og lag personresultater for begrunnelse for behandling 1 + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 15.03.2023 | | OPPFYLT | Nei | + + | 2 | UNDER_18_ÅR | | 15.03.2023 | 14.03.2041 | OPPFYLT | Nei | + | 2 | BOR_MED_SØKER,GIFT_PARTNERSKAP,LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 15.03.2023 | 30.06.2023 | OPPFYLT | Nei | + + | 3 | UNDER_18_ÅR | | 15.03.2023 | 14.03.2041 | OPPFYLT | Nei | + | 3 | BOR_MED_SØKER,GIFT_PARTNERSKAP,LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 15.03.2023 | 30.06.2023 | OPPFYLT | Nei | + + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 15.03.2023 | | OPPFYLT | Nei | + + | 2 | UNDER_18_ÅR | | 15.03.2023 | 14.03.2041 | OPPFYLT | Nei | + | 2 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP,BOSATT_I_RIKET | | 15.03.2023 | 30.06.2023 | OPPFYLT | Nei | + | 2 | BOR_MED_SØKER | | 16.04.2023 | | OPPFYLT | Nei | + + | 3 | UNDER_18_ÅR | | 15.03.2023 | 14.03.2041 | OPPFYLT | Nei | + | 3 | BOR_MED_SØKER,GIFT_PARTNERSKAP,LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 15.03.2023 | 30.06.2023 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 2 | 1 | 01.04.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | + | 3 | 1 | 01.04.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 2 | 01.04.2023 | 30.04.2023 | 1722 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 2 | 01.05.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | + | 3 | 2 | 01.04.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | + + Når begrunnelsetekster genereres for behandling 2 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.04.2023 | 30.04.2023 | UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING | | REDUKSJON_BARN_BOR_IKKE_MED_SØKER | REDUKSJON_IKKE_OPPHOLDSTILLATELSE | + | 01.05.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | | OPPHØR | | | | + + Scenario: Skal gi reduksjon fra forrige behandling-begrunnelser knyttet til utvidet når utvidet ikke lenger er oppfylt + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 1 | 200056155 | | + | 2 | 200056155 | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | BARN | 22.08.2022 | + | 1 | 3456 | SØKER | 07.05.1985 | + | 2 | 1234 | BARN | 22.08.2022 | + | 2 | 3456 | SØKER | 07.05.1985 | + + Og lag personresultater for begrunnelse for behandling 1 + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 3456 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 07.05.1985 | | OPPFYLT | Nei | + | 3456 | UTVIDET_BARNETRYGD | | 19.01.2023 | | OPPFYLT | Nei | + + | 1234 | BOSATT_I_RIKET,LOVLIG_OPPHOLD,GIFT_PARTNERSKAP,BOR_MED_SØKER | | 22.08.2022 | | OPPFYLT | Nei | + | 1234 | UNDER_18_ÅR | | 22.08.2022 | 21.08.2040 | OPPFYLT | Nei | + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 3456 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 07.05.1985 | | OPPFYLT | Nei | + | 3456 | UTVIDET_BARNETRYGD | | 19.01.2023 | | IKKE_OPPFYLT | Nei | + + | 1234 | BOR_MED_SØKER,LOVLIG_OPPHOLD,BOSATT_I_RIKET,GIFT_PARTNERSKAP | | 22.08.2022 | | OPPFYLT | Nei | + | 1234 | UNDER_18_ÅR | | 22.08.2022 | 21.08.2040 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 1234 | 1 | 01.09.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.07.2023 | 31.07.2028 | 1766 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.08.2028 | 31.07.2040 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 3456 | 1 | 01.02.2023 | 28.02.2023 | 1054 | UTVIDET_BARNETRYGD | 100 | + | 3456 | 1 | 01.03.2023 | 30.06.2023 | 2489 | UTVIDET_BARNETRYGD | 100 | + | 3456 | 1 | 01.07.2023 | 31.07.2040 | 2516 | UTVIDET_BARNETRYGD | 100 | + | 1234 | 2 | 01.09.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 2 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 2 | 01.07.2023 | 31.07.2028 | 1766 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 2 | 01.08.2028 | 31.07.2040 | 1310 | ORDINÆR_BARNETRYGD | 100 | + + Når begrunnelsetekster genereres for behandling 2 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.02.2023 | 28.02.2023 | UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING | | REDUKSJON_SØKER_ER_GIFT | | + | 01.03.2023 | 30.06.2023 | UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING | | REDUKSJON_SØKER_ER_GIFT | | + | 01.07.2023 | 31.07.2028 | UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING | | REDUKSJON_SØKER_ER_GIFT | | + | 01.08.2028 | 31.07.2040 | UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING | | REDUKSJON_SØKER_ER_GIFT | | + | 01.08.2040 | | OPPHØR | | | | + + Scenario: Skal få reduksjon fra forrige behandling-begrunnelse knyttet til småbarnstillegg når overgangsstønad forsvinner + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET | SØKNAD | + | 2 | 1 | 1 | ENDRET_UTBETALING | SMÅBARNSTILLEGG | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 19.11.1984 | + | 1 | 3456 | BARN | 26.08.2016 | + | 2 | 1234 | SØKER | 19.11.1984 | + | 2 | 3456 | BARN | 26.08.2016 | + + + Og følgende dagens dato 17.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 19.11.1984 | | OPPFYLT | Nei | + | 1234 | UTVIDET_BARNETRYGD | | 26.08.2016 | | OPPFYLT | Nei | + + | 3456 | LOVLIG_OPPHOLD,BOR_MED_SØKER,GIFT_PARTNERSKAP | | 26.08.2016 | | OPPFYLT | Nei | + | 3456 | BOSATT_I_RIKET | | 26.08.2016 | 31.12.2018 | OPPFYLT | Nei | + | 3456 | UNDER_18_ÅR | | 26.08.2016 | 25.08.2034 | OPPFYLT | Nei | + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 19.11.1984 | | OPPFYLT | Nei | + | 1234 | UTVIDET_BARNETRYGD | | 26.08.2016 | | OPPFYLT | Nei | + + | 3456 | LOVLIG_OPPHOLD,BOR_MED_SØKER,GIFT_PARTNERSKAP | | 26.08.2016 | | OPPFYLT | Nei | + | 3456 | BOSATT_I_RIKET | | 26.08.2016 | 31.12.2018 | OPPFYLT | Nei | + | 3456 | UNDER_18_ÅR | | 26.08.2016 | 25.08.2034 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 1234 | 1 | 01.09.2016 | 31.12.2018 | 1054 | UTVIDET_BARNETRYGD | 100 | 1054 | + | 1234 | 1 | 01.04.2017 | 31.12.2018 | 660 | SMÅBARNSTILLEGG | 100 | 660 | + + | 3456 | 1 | 01.09.2016 | 31.12.2018 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + + | 1234 | 2 | 01.09.2016 | 31.12.2018 | 1054 | UTVIDET_BARNETRYGD | 100 | 1054 | + + | 3456 | 2 | 01.09.2016 | 31.12.2018 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + + Og med overgangsstønad for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | + | 3456 | 1 | 01.04.2017 | 30.06.2017 | + | 3456 | 1 | 01.07.2017 | 30.09.2017 | + | 3456 | 1 | 01.10.2017 | 31.12.2017 | + | 3456 | 1 | 01.01.2018 | 31.03.2018 | + | 3456 | 1 | 01.04.2018 | 30.06.2018 | + | 3456 | 1 | 01.07.2018 | 30.09.2018 | + | 3456 | 1 | 01.10.2018 | 31.12.2018 | + + Når begrunnelsetekster genereres for behandling 2 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.04.2017 | 31.12.2018 | UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING | | SMÅBARNSTILLEGG_HADDE_IKKE_FULL_OVERGANGSSTØNAD | | + | 01.01.2019 | | OPPHØR | | | | + + Scenario: Skal få reduksjon fra forrige behandling-begrunnelser knyttet til bor fast hos søker for det ene barnet + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET | SØKNAD | + | 2 | 1 | 1 | ENDRET_UTBETALING | NYE_OPPLYSNINGER | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 29.12.1984 | + | 1 | 3456 | BARN | 31.03.2019 | + | 1 | 5678 | BARN | 23.02.2022 | + | 2 | 1234 | SØKER | 29.12.1984 | + | 2 | 3456 | BARN | 31.03.2019 | + | 2 | 5678 | BARN | 23.02.2022 | + + Og følgende dagens dato 17.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | LOVLIG_OPPHOLD | 29.12.1984 | | OPPFYLT | + | 1234 | BOSATT_I_RIKET | 11.11.2021 | | OPPFYLT | + + | 3456 | UNDER_18_ÅR | 31.03.2019 | 30.03.2037 | OPPFYLT | + | 3456 | BOR_MED_SØKER | 31.03.2019 | 11.01.2023 | OPPFYLT | + | 3456 | GIFT_PARTNERSKAP,LOVLIG_OPPHOLD | 31.03.2019 | | OPPFYLT | + | 3456 | BOSATT_I_RIKET | 11.11.2021 | | OPPFYLT | + | 3456 | BOR_MED_SØKER | 12.01.2023 | 14.05.2023 | IKKE_OPPFYLT | + | 3456 | BOR_MED_SØKER | 15.05.2023 | | OPPFYLT | + + | 5678 | UNDER_18_ÅR | 23.02.2022 | 22.02.2040 | OPPFYLT | + | 5678 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP,BOSATT_I_RIKET,BOR_MED_SØKER | 23.02.2022 | | OPPFYLT | + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | LOVLIG_OPPHOLD | 29.12.1984 | | OPPFYLT | + | 1234 | BOSATT_I_RIKET | 11.11.2021 | | OPPFYLT | + + | 3456 | UNDER_18_ÅR | 31.03.2019 | 30.03.2037 | OPPFYLT | + | 3456 | BOR_MED_SØKER | 31.03.2019 | 11.01.2023 | OPPFYLT | + | 3456 | GIFT_PARTNERSKAP,LOVLIG_OPPHOLD | 31.03.2019 | | OPPFYLT | + | 3456 | BOSATT_I_RIKET | 11.11.2021 | | OPPFYLT | + | 3456 | BOR_MED_SØKER | 12.01.2023 | 15.08.2023 | IKKE_OPPFYLT | + | 3456 | BOR_MED_SØKER | 16.08.2023 | | OPPFYLT | + + | 5678 | UNDER_18_ÅR | 23.02.2022 | 22.02.2040 | OPPFYLT | + | 5678 | BOR_MED_SØKER,BOSATT_I_RIKET,LOVLIG_OPPHOLD,GIFT_PARTNERSKAP | 23.02.2022 | | OPPFYLT | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 3456 | 1 | 01.12.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | 1654 | + | 3456 | 1 | 01.01.2022 | 31.01.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 3456 | 1 | 01.06.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | 1723 | + | 3456 | 1 | 01.07.2023 | 28.02.2025 | 1766 | ORDINÆR_BARNETRYGD | 100 | 1766 | + | 3456 | 1 | 01.03.2025 | 28.02.2037 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + | 5678 | 1 | 01.03.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 5678 | 1 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | 1723 | + | 5678 | 1 | 01.07.2023 | 31.01.2028 | 1766 | ORDINÆR_BARNETRYGD | 100 | 1766 | + | 5678 | 1 | 01.02.2028 | 31.01.2040 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + | 3456 | 2 | 01.12.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | 1654 | + | 3456 | 2 | 01.01.2022 | 31.01.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 3456 | 2 | 01.09.2023 | 28.02.2025 | 1766 | ORDINÆR_BARNETRYGD | 100 | 1766 | + | 3456 | 2 | 01.03.2025 | 28.02.2037 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + | 5678 | 2 | 01.03.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 5678 | 2 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | 1723 | + | 5678 | 2 | 01.07.2023 | 31.01.2028 | 1766 | ORDINÆR_BARNETRYGD | 100 | 1766 | + | 5678 | 2 | 01.02.2028 | 31.01.2040 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + Når begrunnelsetekster genereres for behandling 2 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.06.2023 | 30.06.2023 | UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING | | REDUKSJON_BARN_BOR_IKKE_MED_SØKER | | + | 01.07.2023 | 31.08.2023 | UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING | | REDUKSJON_BARN_BOR_IKKE_MED_SØKER | | + | 01.09.2023 | 28.02.2025 | UTBETALING | | | | + | 01.03.2025 | 31.01.2028 | UTBETALING | | | | + | 01.02.2028 | 28.02.2037 | UTBETALING | | | | + | 01.03.2037 | 31.01.2040 | UTBETALING | | | | + | 01.02.2040 | | OPPHØR | | | | + + Scenario: Skal ikke få reduksjon fra forrige behandling-begrunnelse, men vanlig reduksjon + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET | SØKNAD | + | 2 | 1 | 1 | ENDRET_UTBETALING | NYE_OPPLYSNINGER | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 19.11.1984 | + | 1 | 3456 | BARN | 26.08.2016 | + | 1 | 5678 | BARN | 23.08.2017 | + | 2 | 1234 | SØKER | 19.11.1984 | + | 2 | 3456 | BARN | 26.08.2016 | + | 2 | 5678 | BARN | 23.08.2017 | + + + Og følgende dagens dato 28.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 19.11.1984 | | OPPFYLT | Nei | + + | 3456 | LOVLIG_OPPHOLD,BOR_MED_SØKER,GIFT_PARTNERSKAP | | 26.08.2016 | | OPPFYLT | Nei | + | 3456 | BOSATT_I_RIKET | | 26.08.2016 | 31.12.2018 | OPPFYLT | Nei | + | 3456 | UNDER_18_ÅR | | 26.08.2016 | 25.08.2034 | OPPFYLT | Nei | + + | 5678 | LOVLIG_OPPHOLD,BOR_MED_SØKER,GIFT_PARTNERSKAP | | 23.08.2017 | | OPPFYLT | Nei | + | 5678 | BOSATT_I_RIKET | | 23.08.2017 | 31.12.2018 | OPPFYLT | Nei | + | 5678 | UNDER_18_ÅR | | 23.08.2017 | 22.08.2035 | OPPFYLT | Nei | + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 19.11.1984 | | OPPFYLT | Nei | + + | 3456 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP, BOSATT_I_RIKET | | 26.08.2016 | | OPPFYLT | Nei | + | 3456 | BOR_MED_SØKER | | 26.08.2016 | 31.12.2018 | OPPFYLT | Nei | + | 3456 | UNDER_18_ÅR | | 26.08.2016 | 25.08.2034 | OPPFYLT | Nei | + + | 5678 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP, BOSATT_I_RIKET | | 23.08.2017 | | OPPFYLT | Nei | + | 5678 | BOR_MED_SØKER | | 23.08.2017 | 31.08.2018 | OPPFYLT | Nei | + | 5678 | UNDER_18_ÅR | | 23.08.2017 | 22.08.2035 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 3456 | 1 | 01.09.2016 | 31.12.2018 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 5678 | 1 | 01.09.2017 | 31.12.2018 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + + | 3456 | 2 | 01.09.2016 | 31.12.2018 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 5678 | 2 | 01.09.2017 | 31.08.2018 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + + Når begrunnelsetekster genereres for behandling 2 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.09.2018 | 31.12.2018 | UTBETALING | | REDUKSJON_FLYTTET_BARN | REDUKSJON_BARN_BOR_IKKE_MED_SØKER | + | 01.01.2019 | | OPPHØR | | | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/restSanityBegrunnelser b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/restSanityBegrunnelser new file mode 100644 index 000000000..0fd0ad233 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/restSanityBegrunnelser @@ -0,0 +1 @@ +[{"periodeType":"UTBETALING","_createdAt":"2021-10-25T06:43:42Z","_updatedAt":"2023-09-25T10:15:44Z","tema":"FELLES","nynorsk":[{"style":"normal","_key":"0b2fd6744558","markDefs":[],"children":[{"_key":"1e361373d1c1","_type":"span","marks":[],"text":""},{"_key":"6c9b09121ede","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi du er separert og bur aleine med ","_key":"32cb6ad89e8d"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"1e2e03eff3ea","skalHaStorForbokstav":false},{"marks":[],"text":". Du og den tidligare ektefellen din flytta frå kvarandre ","_key":"220a3ddef4bf","_type":"span"},{"_key":"fcf53502a235","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_key":"93e5abf283da","_type":"span","marks":[],"text":". "},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"546e6de25a51","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at de flytta frå kvarandre.","_key":"9fb5473b9ebb"}],"_type":"block"}],"valgbarhet":"STANDARD","navnISystem":"Flyttet etter separasjon","apiNavn":"innvilgetFlyttetEtterSeparasjon","_rev":"BtltdVb0HP4g4WJfnr4V7o","begrunnelsetype":"INNVILGET","mappe":["INNVILGET"],"bokmaal":[{"style":"normal","_key":"3a110f0a5c02","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"a3ff318311db"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"bbda4e72193b","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd fordi du er separert og bor alene med ","_key":"ce92f04536d9"},{"_type":"valgfeltV2","_key":"da9cfe5e8834","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":". Du og den tidligere ektefellen din flyttet fra hverandre ","_key":"73c75fb18d13"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"16dd275bc738"},{"marks":[],"text":". ","_key":"6e270e1a50a4","_type":"span"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"a931d9544068","skalHaStorForbokstav":true},{"_key":"fb2bea7b9a55","_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at dere flyttet fra hverandre."}],"_type":"block"}],"visningsnavn":"34. Flyttet etter separasjon","behandlingstema":"NASJONAL","_type":"begrunnelse","_id":"00c9f43d-3ca1-44c1-a5e6-1fad11f15675","hjemler":["2","9","11"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["UTVIDET_BARNETRYGD"]},{"apiNavn":"fortsattInnvilgetBrukerErBlittNorskStatsborger","visningsnavn":"34. Bruker er blitt norsk statsborger","rolle":["SOKER"],"hjemler":["2","4"],"_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","mappe":["FORTSATT_INNVILGET"],"_id":"01176dbd-80d7-4672-ad98-2e2854238e04","_updatedAt":"2023-09-25T10:23:31Z","_rev":"BtltdVb0HP4g4WJfnr4zMo","vilkaar":["LOVLIG_OPPHOLD"],"periodeType":"FORTSATT_INNVILGET","_createdAt":"2022-03-18T14:34:27Z","valgbarhet":"STANDARD","bokmaal":[{"style":"normal","_key":"82dae81ba6b6","markDefs":[],"children":[{"marks":[],"text":"Du får fortsatt barnetrygd fordi du er blitt norsk statsborger.","_key":"9c3b8ff2bb510","_type":"span"}],"_type":"block"}],"navnISystem":"Bruker er blitt norsk statsborger","begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","nynorsk":[{"style":"normal","_key":"1e784213b3db","markDefs":[],"children":[{"_key":"fe7a2240c51b0","_type":"span","marks":[],"text":"Du får fortsatt barnetrygd fordi du er blitt norsk statsborgar."}],"_type":"block"}]},{"periodeType":"FORTSATT_INNVILGET","rolle":["SOKER"],"nynorsk":[{"_type":"block","style":"normal","_key":"74736b060ac7","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd fordi du og ","_key":"f31c7c4a36c0","_type":"span"},{"_type":"valgReferanse","_key":"2da64bdf6691","_ref":"df8dc282-2637-4047-a656-8527205dc364"},{"_type":"span","marks":[],"text":" fortsatt er rekna som busett i Noreg. Ved opphald i utlandet som ikkje varer lenger enn tre månader, er de fortsatt rekna som busett i Noreg.","_key":"5da0ee30954e"}]}],"navnISystem":"Opphold i utlandet ikke mer enn 3 måneder søker og barn","apiNavn":"fortsattInnvilgetOppholdIUtlandetIkkeMerEnn3ManederSokerOgBarn","hjemler":["4"],"vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"FORTSATT_INNVILGET","bosattIRiketTriggere":["MEDLEMSKAP"],"vedtakResultat":"INGEN_ENDRING","mappe":["FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:22:10Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du og ","_key":"063c347d9c21"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"8fe6cecfddbb"},{"_type":"span","marks":[],"text":" fortsatt er regnet som bosatt i Norge. Ved opphold i utlandet som ikke varer lenger enn tre måneder, er dere fortsatt regnet som bosatt i Norge.","_key":"026a3690b5c0"}],"_type":"block","style":"normal","_key":"e60b80e48bee"}],"_id":"013c96a8-250b-40a9-b5ed-e9f01643b1cf","visningsnavn":"12. Opphold i utlandet ikke mer enn 3 måneder søker og barn","_rev":"FuD004taptHFqBZyEy6Pc8","_type":"begrunnelse","tema":"NASJONAL","_createdAt":"2021-09-24T12:24:46Z","valgbarhet":"STANDARD"},{"_rev":"BtltdVb0HP4g4WJfnr4V7o","vilkaar":["UTVIDET_BARNETRYGD"],"mappe":["ENDRET_UTBETALINGSPERIODE"],"_updatedAt":"2023-09-25T10:15:44Z","bokmaal":[{"style":"normal","_key":"1deb41c5b723","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du har allerede fått utbetalt ordinær barnetrygd for barn født ","_key":"63a9818902e80"},{"_type":"flettefelt","_key":"f62d49ff14f6","flettefelt":"barnasFodselsdatoer"},{"_key":"edcd8a9bdf06","_type":"span","marks":[],"text":". Det er opp til deg og den andre forelderen å bli enige om fordelingen av barnetrygden som allerede er utbetalt. Du får den utvidede delen av barnetrygden etterbetalt."}],"_type":"block"}],"navnISystem":"Delt bosted - mottatt full ordinær, får etterbetalt utvidet","hjemler":["2","12"],"begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","tema":"FELLES","endringsaarsaker":["DELT_BOSTED"],"_createdAt":"2022-03-07T19:38:17Z","_id":"02145f91-a7a6-4f88-a27e-3c7ad2a77bfe","visningsnavn":"11. NY Delt bosted - mottatt full ordinær, får etterbetalt utvidet","_type":"begrunnelse","valgbarhet":"STANDARD","apiNavn":"endretUtbetalingMottattFullOrdinaerFaarEtterbetaltUtvidet","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","nynorsk":[{"style":"normal","_key":"43f2837faa6c","markDefs":[],"children":[{"marks":[],"text":"Du har allereie fått utbetalt ordinær barnetrygd for barn fødd ","_key":"a4715ecfca280","_type":"span"},{"_type":"flettefelt","_key":"9f205e569630","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":". Det er opp til deg og den andre forelderen å bli einige om fordelinga av barnetrygda som allereie er utbetalt. Du får den utvida delen av barnetrygden etterbetalt.","_key":"b41b80ab0377"}],"_type":"block"}],"endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"UTBETALING_IKKE_RELEVANT"},{"hjemler":["9"],"vedtakResultat":"IKKE_INNVILGET","nynorsk":[{"_type":"block","style":"normal","_key":"a1664a75a9d3","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi du har sambuar.","_key":"de9f3b4a93d6"}]}],"mappe":["AVSLAG"],"apiNavn":"avslagSamboer","periodeType":"INGEN_UTBETALING","_updatedAt":"2023-09-25T10:29:35Z","navnISystem":"Samboer","vilkaar":["UTVIDET_BARNETRYGD"],"_createdAt":"2021-10-22T08:19:12Z","valgbarhet":"STANDARD","_id":"03c0db86-cba4-4297-887f-c5f9f64c9e3c","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi du har samboer.","_key":"158afc6a6673"}],"_type":"block","style":"normal","_key":"ac2a98220272","markDefs":[]}],"_type":"begrunnelse","_rev":"BtltdVb0HP4g4WJfnr5J6o","begrunnelsetype":"AVSLAG","tema":"FELLES","visningsnavn":"37. Samboer"},{"vedtakResultat":"INNVILGET_ELLER_ØKNING","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:44Z","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"","_key":"8f3e0175826f","_type":"span"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"b1cb43950803","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd fordi du bor alene med ","_key":"fad8f2ac5f400"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"7faaf5daf9bc","skalHaStorForbokstav":false},{"marks":[],"text":". ","_key":"cd3240ece6f4","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"b5479855243d","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" har fylt 16 år og vi krever ikke meklingsattest. Du og den tidligere samboeren din flyttet fra hverandre ","_key":"2aa1b6af554d"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"44c830664ecd"},{"_type":"span","marks":[],"text":". ","_key":"6ca547501689"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"4c888f571a8a","skalHaStorForbokstav":true},{"text":" utvidet barnetrygd fra måneden etter at dere flyttet fra hverandre.","_key":"b2129e2c5f7f","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"840da6d66500"}],"navnISystem":"Barn 16 år utvidet fra flytting","_createdAt":"2022-03-18T14:30:26Z","valgbarhet":"STANDARD","periodeType":"UTBETALING","tema":"FELLES","nynorsk":[{"children":[{"_key":"2cfada2b70f0","_type":"span","marks":[],"text":""},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"f007b67ae76c","skalHaStorForbokstav":false},{"marks":[],"text":" utvida barnetrygd fordi du bur aleine med ","_key":"aee783c8aa6f0","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"110411c5ddb0","skalHaStorForbokstav":false},{"_key":"26d5c6e94b83","_type":"span","marks":[],"text":". "},{"_key":"49dedaa6c2be","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" har fylt 16 år og vi krev ikkje meklingsattest. Du og den tidligare sambuaren din flytta frå kvarandre ","_key":"d56bd064e276"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"4623adb039ec"},{"_type":"span","marks":[],"text":". ","_key":"76acd0c7db52"},{"skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"945027625966"},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at de flytta frå kvarandre.","_key":"19e2754ea101"}],"_type":"block","style":"normal","_key":"743b489521cd","markDefs":[]}],"hjemler":["2","9","11"],"_rev":"BtltdVb0HP4g4WJfnr4V7o","vilkaar":["UTVIDET_BARNETRYGD"],"_type":"begrunnelse","begrunnelsetype":"INNVILGET","_id":"040441ef-497b-45ca-8cdb-a449a8c89ea4","apiNavn":"innvilgetBarn16AarUtvidetFraFlytting","visningsnavn":"77. Barn 16 år utvidet fra flytting","behandlingstema":"NASJONAL"},{"navnISystem":"Hele familien trygdeavtale","visningsnavn":"23. Hele familien trygdeavtale","vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","bokmaal":[{"_type":"block","style":"normal","_key":"58f2fb3740d5","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd ","_key":"1a7114a591a1","_type":"span"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"2052c2797a9e"},{"marks":[],"text":" under oppholdet i utlandet fordi hele familien er medlem i folketrygden etter trygdeavtale fra ","_key":"cc7f104f1516","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"5f5a44435406"},{"marks":[],"text":". Trygdeavtalen gir rett til barnetrygd fra Norge.","_key":"bed86b797301","_type":"span"}]}],"hjemler":["2","4","22"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","_updatedAt":"2023-09-25T10:15:44Z","tema":"NASJONAL","nynorsk":[{"_key":"13a14d58c868","markDefs":[],"children":[{"text":"Du får barnetrygd ","_key":"d6510833d586","_type":"span","marks":[]},{"_type":"valgReferanse","_key":"67877475d101","_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170"},{"text":" under opphaldet i utlandet fordi heile familien er medlem i folketrygda etter trygdeavtale frå ","_key":"464e65b446f6","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"43aaf3d6b0fb"},{"_type":"span","marks":[],"text":". Trygdeavtalen gjev rett til barnetrygd frå Noreg.","_key":"438934ea3deb"}],"_type":"block","style":"normal"}],"_createdAt":"2021-09-24T11:24:07Z","_id":"042fcbdc-d52c-4782-a492-ffd8df9da32d","mappe":["INNVILGET"],"apiNavn":"innvilgetHeleFamilienTrygdeavtale","behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","rolle":["SOKER","BARN"],"bosattIRiketTriggere":["MEDLEMSKAP"]},{"navnISystem":"Hele familien pliktig medlem","hjemler":["4","5"],"hjemlerFolketrygdloven":["2-5"],"mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:44Z","apiNavn":"innvilgetHeleFamilienPliktigMedlem","rolle":["SOKER","BARN"],"bosattIRiketTriggere":["MEDLEMSKAP"],"_createdAt":"2021-09-24T11:17:19Z","_id":"04632b4e-8cbc-4651-9431-0611db3b6ed5","visningsnavn":"21. Hele familien pliktig medlem","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","tema":"NASJONAL","nynorsk":[{"children":[{"_key":"5d379ad3b859","_type":"span","marks":[],"text":"Du får barnetrygd "},{"_key":"08f749f22082","_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse"},{"_type":"span","marks":[],"text":" under opphaldet i utlandet fordi heile familien er pliktig medlem i folketrygda frå ","_key":"7e86239a1afd"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"5c407bf5e826"},{"_type":"span","marks":[],"text":".","_key":"d84259c287db"}],"_type":"block","style":"normal","_key":"c2dcb232a678","markDefs":[]}],"bokmaal":[{"_key":"8e11373eebf7","markDefs":[],"children":[{"text":"Du får barnetrygd ","_key":"2d431dcaddc4","_type":"span","marks":[]},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"159f081c9ab2"},{"_type":"span","marks":[],"text":" under oppholdet i utlandet fordi hele familien er pliktig medlem i folketrygden fra ","_key":"8cdb18f53c81"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"e89b8f76bf88"},{"_type":"span","marks":[],"text":".","_key":"0e968866e183"}],"_type":"block","style":"normal"}],"vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD"},{"vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","tema":"NASJONAL","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi vi har kome fram til at du har opphaldsrett frå ","_key":"skjonnsmessigVurderingTaaltOppholdTredjelandsborger2"},{"_key":"cc70fbb9d99d","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"affc403447b2"}],"_type":"block","style":"normal","_key":"skjonnsmessigVurderingTaaltOppholdTredjelandsborger1","markDefs":[]}],"apiNavn":"innvilgetLovligOppholdSkjonnsmessigVurderingTredjelandsborgerSoker","hjemler":["2","4","11"],"_type":"begrunnelse","mappe":["INNVILGET"],"bokmaal":[{"style":"normal","_key":"skjonnsmessigVurderingTaaltOppholdTredjelandsborger1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi vi har kommet fram til at du har oppholdsrett fra ","_key":"skjonnsmessigVurderingTaaltOppholdTredjelandsborger2"},{"_key":"b92e9c4bbc2a","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"1e9628bbbbd9"}],"_type":"block"}],"behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr4V7o","periodeType":"UTBETALING","_createdAt":"2022-11-18T10:42:54Z","_id":"057378ce-799a-4d69-8b23-bdef52baa2ae","_updatedAt":"2023-09-25T10:15:44Z","vilkaar":["LOVLIG_OPPHOLD"],"rolle":["SOKER"],"lovligOppholdTriggere":["VURDERING_ANNET_GRUNNLAG"],"navnISystem":"Skjønnsmessig vurdering tålt opphold tredjelandsborger søker","visningsnavn":"13a. Skjønnsmessig vurdering tålt opphold tredjelandsborger søker","valgbarhet":"STANDARD"},{"visningsnavn":"18. Søker og barn pliktig medlem","periodeType":"UTBETALING","tema":"NASJONAL","valgbarhet":"STANDARD","mappe":["INNVILGET"],"bokmaal":[{"children":[{"marks":[],"text":"Du får barnetrygd ","_key":"50ea7531792c","_type":"span"},{"_type":"valgReferanse","_key":"fa52f7bda7c3","_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170"},{"_type":"span","marks":[],"text":" under oppholdet i utlandet fordi ","_key":"acd3101d3f86"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"87492e241018"},{"_type":"span","marks":[],"text":" er pliktig medlem i folketrygden fra ","_key":"b0463c2c242f"},{"_type":"flettefelt","_key":"762f82431695","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_type":"span","marks":[],"text":".","_key":"7100967e3d0c"}],"_type":"block","style":"normal","_key":"692e7eb23b7f","markDefs":[]}],"hjemler":["4","5"],"_type":"begrunnelse","hjemlerFolketrygdloven":["2-5"],"vilkaar":["BOSATT_I_RIKET"],"bosattIRiketTriggere":["MEDLEMSKAP"],"_updatedAt":"2023-09-25T10:15:44Z","navnISystem":"Søker og barn pliktig medlem","apiNavn":"innvilgetSokerOgBarnPliktigMedlem","behandlingstema":"NASJONAL","vedtakResultat":"INNVILGET_ELLER_ØKNING","nynorsk":[{"style":"normal","_key":"daeac437c1ba","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"4c9747a8b84f"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"badd7f8363c5"},{"_type":"span","marks":[],"text":" under opphaldet i utlandet fordi ","_key":"3a63535a80e9"},{"_key":"b3f74cf023d7","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse"},{"_type":"span","marks":[],"text":" er pliktig medlem i folketrygda frå ","_key":"35501aa63749"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"fcb4cac904fd"},{"_type":"span","marks":[],"text":".","_key":"6a4454714f28"}],"_type":"block"}],"_id":"0598ad7d-efce-441c-83dc-df86ce324cb5","_rev":"BtltdVb0HP4g4WJfnr4V7o","rolle":["SOKER","BARN"],"begrunnelsetype":"INNVILGET","_createdAt":"2021-09-24T10:42:22Z"},{"navnISystem":"Tilleggstekst EØS borger ektefelle utbetaling NAV","vilkaar":["BOSATT_I_RIKET","LOVLIG_OPPHOLD"],"rolle":["SOKER","BARN"],"_createdAt":"2022-05-24T14:17:16Z","_id":"05ad2fe3-abb7-4de7-b04a-e7bb935e5ef1","_updatedAt":"2023-09-25T10:15:44Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har oppholdsrett som EØS-borger. Familien bor i Norge og ektefellen din får utbetaling fra NAV som erstatter lønn.","_key":"d0780212e4560"}],"_type":"block","style":"normal","_key":"543c2679180b"}],"apiNavn":"innvilgetTilleggstekstEosBorgerEktefelleUtbetalingNav","_rev":"BtltdVb0HP4g4WJfnr4V7o","vedtakResultat":"INNVILGET_ELLER_ØKNING","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har opphaldsrett som EØS-borgar. Familien bur i Noreg og ektefellen din får utbetaling frå NAV som erstattar løn.","_key":"2ac83c3d01650"}],"_type":"block","style":"normal","_key":"ee6918315e9f"}],"mappe":["INNVILGET"],"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","tema":"NASJONAL","_type":"begrunnelse","valgbarhet":"TILLEGGSTEKST","hjemler":["2","4"],"visningsnavn":"86. Tilleggstekst EØS borger ektefelle utbetaling NAV","behandlingstema":"NASJONAL"},{"apiNavn":"reduksjonNyfodtBarn","_createdAt":"2021-10-22T11:32:24Z","valgbarhet":"STANDARD","_id":"06aaabba-3854-4725-a17c-88b93217c1b0","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","visningsnavn":"23. Nyfødt barn","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","vedtakResultat":"REDUKSJON","bokmaal":[{"style":"normal","_key":"848841b83f6f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får ikke lenger utvidet barnetrygd fordi du har fått barn ","_key":"98bdab81eb64"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"c98a34f91869"},{"_type":"span","marks":[],"text":". Hvis du fortsatt bor alene med barna kan du søke om utvidet barnetrygd på nytt.","_key":"b112adb3016b"}],"_type":"block"}],"hjemler":["2","11","14"],"begrunnelsetype":"REDUKSJON","tema":"FELLES","mappe":["REDUKSJON"],"navnISystem":"Nyfødt barn","behandlingstema":"NASJONAL","nynorsk":[{"style":"normal","_key":"5ea4abbb362e","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får ikkje lenger utvida barnetrygd fordi du har fått barn ","_key":"425ec671e46d"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"f5b70e96b68d"},{"text":". Dersom du fortsatt bur aleine med barna kan du søke om utvida barnetrygd på nytt.","_key":"e40250698e71","_type":"span","marks":[]}],"_type":"block"}],"_updatedAt":"2023-09-25T10:15:44Z"},{"behandlingstema":"NASJONAL","borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"_updatedAt":"2023-09-25T10:15:44Z","bokmaal":[{"style":"normal","_key":"5dcbffb6bc2c","markDefs":[],"children":[{"marks":[],"text":"Barnetrygden endres fordi den andre forelderen har søkt om barnetrygd for barn født ","_key":"0e456a349b04","_type":"span"},{"_type":"flettefelt","_key":"3eb2a6838e8f","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":".","_key":"81e9ea47ebf1"}],"_type":"block"}],"apiNavn":"reduksjonForeldreneBorSammenAnnenForelderSokt","vilkaar":["BOR_MED_SOKER"],"_id":"0746a44c-7dbb-4ca5-b668-d099f814357c","mappe":["REDUKSJON"],"vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","tema":"FELLES","valgbarhet":"STANDARD","navnISystem":"Foreldrene bor sammen, annen forelder søkt","hjemler":["2","12"],"visningsnavn":"63. Foreldrene bor sammen, annen forelder søkt","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi den andre forelderen har søkt om barnetrygd for barn fødd ","_key":"72824d350184"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"1bc731dd3a47"},{"_type":"span","marks":[],"text":".","_key":"4d4a3ea84911"}],"_type":"block","style":"normal","_key":"929b8f4cedf8","markDefs":[]}],"_createdAt":"2022-04-28T13:31:12Z"},{"_createdAt":"2021-08-30T13:53:52Z","_updatedAt":"2023-09-25T10:26:20Z","apiNavn":"avslagIkkeAvtaleOmDeltBosted","hjemler":["2"],"visningsnavn":"11. Ikke avtale om delt bosted","_rev":"BtltdVb0HP4g4WJfnr5B1o","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"3d5ffc29f52e"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"f2fb5294ffc7"},{"_key":"02f51e2e9287","_type":"span","marks":[],"text":" fordi du ikkje har ein avtale om delt bustad for "},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"23d2333708e7"},{"_key":"cee73a720d9c","_type":"span","marks":[],"text":""},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"882b82e71f72"},{"_type":"span","marks":[],"text":". Barnetrygda kan derfor ikkje delast.","_key":"326cefa6242e"}],"_type":"block","style":"normal","_key":"a751112c2b05"}],"_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOR_MED_SOKER"],"_id":"076fe3cf-dacb-45d8-bb00-39f26f7aaca3","mappe":["AVSLAG"],"valgbarhet":"STANDARD","bokmaal":[{"style":"normal","_key":"7cff5c8ad994","markDefs":[],"children":[{"_key":"14886b2b728e","_type":"span","marks":[],"text":"Delt barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"69753c9d7f3d"},{"_type":"span","marks":[],"text":" fordi du ikke har en avtale om delt bosted for ","_key":"d2f541f94f8e"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"1f1a79b685fe"},{"marks":[],"text":"","_key":"aec57fbe8158","_type":"span"},{"_type":"valgReferanse","_key":"883e98aab19a","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"_type":"span","marks":[],"text":". Barnetrygden kan derfor ikke deles.","_key":"f3c23ce35416"}],"_type":"block"}],"navnISystem":"Ikke avtale om delt bosted","behandlingstema":"NASJONAL","periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","tema":"NASJONAL"},{"_type":"begrunnelse","_createdAt":"2022-11-04T09:11:12Z","navnISystem":"Norsk statsborger institusjon","fagsakType":"INSTITUSJON","vedtakResultat":"INGEN_ENDRING","nynorsk":[{"children":[{"_key":"3a061e67271f0","_type":"span","marks":[],"text":"De får fortsatt barnetrygd fordi barnet er blitt norsk statsborgar."}],"_type":"block","style":"normal","_key":"cc52798d5241","markDefs":[]}],"valgbarhet":"SAKSPESIFIKK","apiNavn":"fortsattInnvilgetNorskStatsborgerInstitusjon","hjemler":["2","4"],"_rev":"h26rAhFEYSUtDGXJE0v6ZT","vilkaar":["LOVLIG_OPPHOLD"],"rolle":["SOKER","BARN"],"_id":"0788ebaa-35b3-4b24-bd4d-14a18b3db20f","mappe":["INSTITUSJON","FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:38:50Z","visningsnavn":"4. Norsk statsborger institusjon","behandlingstema":"NASJONAL_INSTITUSJON","bokmaal":[{"markDefs":[],"children":[{"_key":"3ee36816d6fa0","_type":"span","marks":[],"text":"Dere får fortsatt barnetrygd fordi barnet er blitt norsk statsborger."}],"_type":"block","style":"normal","_key":"a8a4809a84ec"}],"tema":"NASJONAL","periodeType":"FORTSATT_INNVILGET","begrunnelsetype":"FORTSATT_INNVILGET"},{"_updatedAt":"2023-09-25T10:15:44Z","apiNavn":"etterEndretUtbetalingEtterbetalingTreAarTilbakeITidInstitusjon","hjemler":["11"],"endretUtbetalingsperiodeTriggere":["ETTER_ENDRET_UTBETALINGSPERIODE"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"NASJONAL","_createdAt":"2022-11-04T08:08:55Z","fagsakType":"INSTITUSJON","endringsaarsaker":["ETTERBETALING_3ÅR"],"valgbarhet":"SAKSPESIFIKK","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","begrunnelsetype":"ETTER_ENDRET_UTBETALINGSPERIODE","nynorsk":[{"markDefs":[],"children":[{"text":"De søker om barnetrygd tilbake i tid for barn fødd ","_key":"34dc934fa9b70","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"ab7c7cfc5867"},{"marks":[],"text":". Vi fekk søknaden ","_key":"b59e3fcc7156","_type":"span"},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"8ed740b06fe6"},{"_type":"span","marks":[],"text":". Barnetrygd kan ikkje utbetalast for meir enn tre år tilbake i tid frå vi fekk søknaden.","_key":"45477c5f164e"}],"_type":"block","style":"normal","_key":"efec01380b62"}],"navnISystem":"Etterbetaling tre år tilbake i tid institusjon","visningsnavn":" Etterbetaling tre år tilbake i tid institusjon","periodeType":"UTBETALING","_id":"083189de-b9ec-4d5c-ab03-dc30a66407e9","mappe":["INSTITUSJON","ETTER_ENDRET_UTBETALINGSPERIODE"],"bokmaal":[{"_key":"fb003066b942","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Dere søker om barnetrygd tilbake i tid for barn født ","_key":"537e7abfabab0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"68845abb9117"},{"text":". Vi fikk søknaden ","_key":"5bce5481dd40","_type":"span","marks":[]},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"e49133c85ba2"},{"_type":"span","marks":[],"text":". Barnetrygd kan ikke utbetales for mer enn tre år tilbake i tid fra vi fikk søknaden.","_key":"a26185269890"}],"_type":"block","style":"normal"}]},{"endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"UTBETALING_IKKE_RELEVANT","bokmaal":[{"_type":"block","style":"normal","_key":"e23c3eb4794e","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får delt barnetrygd for barn født ","_key":"aafc61eb9bee"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"d003b26b7f03"},{"text":" fordi retten har bestemt at ","_key":"df34d1809c20","_type":"span","marks":[]},{"_key":"3276f66db1f1","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" skal ha delt bosted. Barnetrygden deles fra måneden etter at begge foreldrene har søkt om det.","_key":"1d7eef65bbb7"}]}],"hjemler":["2","11"],"endretUtbetalingsperiodeTriggere":["ETTER_ENDRET_UTBETALINGSPERIODE"],"periodeType":"UTBETALING","tema":"FELLES","navnISystem":"Rettsavgjørelse delt bosted","apiNavn":"etterEndretUtbetalingRettsavgjorelseDeltBosted","endringsaarsaker":["DELT_BOSTED"],"_createdAt":"2021-10-18T05:36:58Z","_id":"0895cd10-3fa3-49d2-8c37-219fe1fa70be","mappe":["ETTER_ENDRET_UTBETALINGSPERIODE"],"_updatedAt":"2023-09-25T10:15:44Z","_rev":"BtltdVb0HP4g4WJfnr4V7o","begrunnelsetype":"ETTER_ENDRET_UTBETALINGSPERIODE","vedtakResultat":"INNVILGET_ELLER_ØKNING","nynorsk":[{"markDefs":[],"children":[{"_key":"1a41af3cf8d1","_type":"span","marks":[],"text":"Du får delt barnetrygd for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"4ac89dda60af"},{"text":" fordi retten har bestemt at ","_key":"7b06c25ecad2","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"7e0a116bc025","skalHaStorForbokstav":false},{"marks":[],"text":" skal ha delt bustad. Barnetrygda er delt frå månaden etter at begge foreldra har søkt om det.","_key":"cc6785ac9453","_type":"span"}],"_type":"block","style":"normal","_key":"60574e75ee3f"}],"valgbarhet":"STANDARD","visningsnavn":"2. Rettsavgjørelse delt bosted","_type":"begrunnelse"},{"navnISystem":"Separert","hjemler":["2","9","11"],"vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","valgbarhet":"STANDARD","mappe":["INNVILGET"],"bokmaal":[{"_type":"block","style":"normal","_key":"ff506cc0f29c","markDefs":[],"children":[{"text":"","_key":"eaa0528a6b98","_type":"span","marks":[]},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"bdacb78a8e00","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd fordi du er separert og bor alene med ","_key":"577c7778ac63"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"15cbf26de4d2","skalHaStorForbokstav":false},{"_key":"4c6e5d9d474f","_type":"span","marks":[],"text":". Du ble separert "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"dc555c9bc527"},{"_type":"span","marks":[],"text":". ","_key":"31bbaf01e0ac"},{"skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"cb989da2d7d1"},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at du ble separert.","_key":"635af0178a7e"}]}],"apiNavn":"innvilgetSeparert","visningsnavn":"33. Separert","_type":"begrunnelse","nynorsk":[{"_key":"97b161c09443","markDefs":[],"children":[{"marks":[],"text":"","_key":"21f4b3326e36","_type":"span"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"11e2629b6076","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi du er separert og bur aleine med ","_key":"d748a3cca24d"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"34abcbcbfaab","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Du vart separert ","_key":"bb0a65a62daa"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"aee0a9d28f3c"},{"_type":"span","marks":[],"text":". ","_key":"e5f493e38e79"},{"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"},"_type":"valgfeltV2","_key":"8fe54ef7816f","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at du vart separert.","_key":"d74d736bb258"}],"_type":"block","style":"normal"}],"_createdAt":"2021-10-25T06:41:14Z","_updatedAt":"2023-09-25T10:15:44Z","behandlingstema":"NASJONAL","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","tema":"FELLES","_id":"08dc2077-cd11-47fa-bfd1-e94c68e77b31","_rev":"BtltdVb0HP4g4WJfnr4V7o"},{"mappe":["ETTER_ENDRET_UTBETALINGSPERIODE"],"_updatedAt":"2023-09-25T10:15:44Z","_type":"begrunnelse","periodeType":"UTBETALING","nynorsk":[{"_key":"7ff5c8d57fab","markDefs":[],"children":[{"text":"Du søker om barnetrygd tilbake i tid for barn fødd ","_key":"a56d33f711840","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"e992a3b59988"},{"_type":"span","marks":[],"text":". Vi fikk søknaden din ","_key":"e563e80fa3e2"},{"_type":"flettefelt","_key":"6874afa6c2da","flettefelt":"soknadstidspunkt"},{"_type":"span","marks":[],"text":". Barnetrygd kan ikkje utbetalast for meir enn tre år tilbake i tid frå vi fekk søknaden din.","_key":"813638073f4e"}],"_type":"block","style":"normal"}],"valgbarhet":"STANDARD","_id":"090a065e-b055-44bf-b974-950037ecd944","apiNavn":"etterEndretUtbetalingEtterbetalingTreAarTilbakeITid","hjemler":["11"],"_rev":"BtltdVb0HP4g4WJfnr4V7o","vedtakResultat":"IKKE_INNVILGET","endringsaarsaker":["ETTERBETALING_3ÅR"],"navnISystem":"Etterbetaling tre år tilbake i tid","visningsnavn":"5. Etterbetaling tre år tilbake i tid","begrunnelsetype":"ETTER_ENDRET_UTBETALINGSPERIODE","tema":"FELLES","behandlingstema":"NASJONAL","endretUtbetalingsperiodeTriggere":["ETTER_ENDRET_UTBETALINGSPERIODE"],"_createdAt":"2022-03-30T11:31:05Z","bokmaal":[{"style":"normal","_key":"a6a0c3c43288","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du søker om barnetrygd tilbake i tid for barn født ","_key":"f809ae1a6e050"},{"_type":"flettefelt","_key":"1b7ac4044c0c","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":". Vi fikk søknaden din ","_key":"bc5dace60c8d"},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"d0182480a9c5"},{"marks":[],"text":". Barnetrygd kan ikke utbetales for mer enn tre år tilbake i tid fra vi fikk søknaden din.","_key":"7893ee756d5f","_type":"span"}],"_type":"block"}]},{"visningsnavn":"67. Ikke oppholdstillatelse mer enn 12 måneder","vedtakResultat":"IKKE_INNVILGET","rolle":["SOKER","BARN"],"hjemler":["2","4"],"behandlingstema":"NASJONAL","periodeType":"INGEN_UTBETALING","tema":"NASJONAL","valgbarhet":"STANDARD","navnISystem":"Ikke oppholdstillatelse mer enn 12 måneder","nynorsk":[{"children":[{"marks":[],"text":"Barnetrygd for barn fødd ","_key":"882e52a7b3f60","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"c00f5ae2a344"},{"_type":"span","marks":[],"text":" fordi ","_key":"7dd9e2bab608"},{"valgReferanse":{"_type":"reference","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},"_type":"valgfeltV2","_key":"fe1e435c77fd","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikkje har opphaldsløyve i Noreg i meir enn 12 månader.","_key":"1dc7c777459e"}],"_type":"block","style":"normal","_key":"67bcbe9bbac2","markDefs":[]}],"_createdAt":"2022-09-21T08:39:22Z","mappe":["AVSLAG"],"apiNavn":"avslagIkkeOppholdstillatelseMerEnn12Maaneder","_rev":"FuD004taptHFqBZyEy7jig","_type":"begrunnelse","vilkaar":["LOVLIG_OPPHOLD"],"begrunnelsetype":"AVSLAG","lovligOppholdTriggere":["VURDERING_ANNET_GRUNNLAG"],"_id":"09378e2a-7e53-407d-8d84-96aee131f5cb","_updatedAt":"2023-09-25T10:31:13Z","bokmaal":[{"markDefs":[],"children":[{"text":"Barnetrygd for barn født ","_key":"6dca211774560","_type":"span","marks":[]},{"_type":"flettefelt","_key":"39eaa2b96e59","flettefelt":"barnasFodselsdatoer"},{"marks":[],"text":" fordi ","_key":"cf4650b5c30b","_type":"span"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"e030fdd1743c","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikke har oppholdstillatelse i Norge i mer enn 12 måneder.","_key":"59dd05c24f86"}],"_type":"block","style":"normal","_key":"ad89494616d4"}]},{"navnISystem":"Ikke medlem utenlandsopphold","behandlingstema":"NASJONAL","vilkaar":["BOSATT_I_RIKET"],"rolle":["SOKER","BARN"],"bokmaal":[{"_key":"8068935db321","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"27d50de7ef08"},{"_key":"9207fc630ba9","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi ","_key":"ef93c06971d1"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"befdcd3e88a0"},{"_type":"span","marks":[],"text":" ikke er medlem i folketrygden under oppholdet i utlandet","_key":"2c87d42f5bec"},{"_type":"valgReferanse","_key":"2b35d9279cef","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"_key":"6c99d70d18cf","_type":"span","marks":[],"text":"."}],"_type":"block","style":"normal"}],"apiNavn":"avslagIkkeMedlem","hjemlerFolketrygdloven":["2-5","2-8"],"begrunnelsetype":"AVSLAG","nynorsk":[{"style":"normal","_key":"4063c7355fbf","markDefs":[],"children":[{"_key":"3b6df257c682","_type":"span","marks":[],"text":"Barnetrygd for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"0a63a18f2119"},{"_type":"span","marks":[],"text":" fordi ","_key":"9fbb0f8724f1"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"b23bd1024c55"},{"_type":"span","marks":[],"text":" ikkje er medlem i folketrygda under opphaldet i utlandet","_key":"ed0db0d56b73"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"fd993fcf287d"},{"_key":"ecd97e69128a","_type":"span","marks":[],"text":"."}],"_type":"block"}],"mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:26:38Z","visningsnavn":"16. Ikke medlem utenlandsopphold","tema":"NASJONAL","valgbarhet":"STANDARD","_id":"09c842f4-ca5e-4d71-a09b-16495ed22dbe","hjemler":["4","5"],"_rev":"h26rAhFEYSUtDGXJE0sxVW","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","bosattIRiketTriggere":["MEDLEMSKAP"],"_createdAt":"2021-09-24T12:50:37Z"},{"hjemler":["9"],"visningsnavn":"40. Samboer ikke flyttet fra hverandre","periodeType":"INGEN_UTBETALING","tema":"FELLES","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi du og sambuaren din ikkje har flytta frå kvarandre.","_key":"2a470fe114c8"}],"_type":"block","style":"normal","_key":"e2f11da9338d","markDefs":[]}],"_id":"0c527156-7a1f-4b15-8467-6c25bb96bac5","mappe":["AVSLAG"],"_rev":"BtltdVb0HP4g4WJfnr5Jdo","begrunnelsetype":"AVSLAG","_createdAt":"2021-10-22T08:31:34Z","bokmaal":[{"_type":"block","style":"normal","_key":"8ee2326d5680","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi du og samboeren din ikke har flyttet fra hverandre.","_key":"9763413f21d7"}]}],"navnISystem":"Samboer ikke flyttet fra hverandre","apiNavn":"avslagSamboerIkkeFlyttetFraHverandre","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","vilkaar":["UTVIDET_BARNETRYGD"],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:29:45Z"},{"hjemler":["2","11"],"_rev":"BtltdVb0HP4g4WJfnr4V7o","periodeType":"UTBETALING","tema":"FELLES","mappe":["ETTER_ENDRET_UTBETALINGSPERIODE"],"apiNavn":"etterEndretUtbetalingVurderingAvtaleDeltBostedFolges","_type":"begrunnelse","endretUtbetalingsperiodeTriggere":["ETTER_ENDRET_UTBETALINGSPERIODE"],"begrunnelsetype":"ETTER_ENDRET_UTBETALINGSPERIODE","nynorsk":[{"_type":"block","style":"normal","_key":"d80d1b44c3fb","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får delt barnetrygd for barn fødd ","_key":"366f5cca47a5"},{"_type":"flettefelt","_key":"c888d7b35db1","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi du har skriftlig avtale om delt bustad for ","_key":"dfff389b54fa"},{"_type":"valgfeltV2","_key":"458d3b6b062b","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"marks":[],"text":". Vi har kome fram til at avtalen blir følgd. Barnetrygda er delt frå månaden etter at begge foreldra har søkt om det.","_key":"f12d6c77c7bc","_type":"span"}]}],"endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"UTBETALING_IKKE_RELEVANT","visningsnavn":"3. Vurdering avtale delt bosted følges","endringsaarsaker":["DELT_BOSTED"],"_updatedAt":"2023-09-25T10:15:44Z","bokmaal":[{"_type":"block","style":"normal","_key":"2d3d9e0d3d1d","markDefs":[],"children":[{"_key":"c1d7f96279b9","_type":"span","marks":[],"text":"Du får delt barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"b19eae8c32f8"},{"_key":"27b97d40ae94","_type":"span","marks":[],"text":" fordi du har skriftlig avtale om delt bosted for "},{"skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"7b7652cdbea9"},{"_key":"15072a450ff2","_type":"span","marks":[],"text":". Vi har kommet fram til at avtalen følges. Barnetrygden deles fra måneden etter at begge foreldrene har søkt om det."}]}],"vedtakResultat":"INNVILGET_ELLER_ØKNING","_createdAt":"2021-10-18T05:43:12Z","valgbarhet":"STANDARD","_id":"0ddd042d-cf81-435f-aafa-e42deea6e15c","navnISystem":"Vurdering avtale delt bosted følges"},{"rolle":["SOKER","BARN"],"tema":"NASJONAL","apiNavn":"avslagLovligOppholdEosBorger","hjemler":["2","4"],"visningsnavn":"5. EØS-borger uten oppholdsrett","behandlingstema":"NASJONAL","_rev":"h26rAhFEYSUtDGXJE0svjm","periodeType":"INGEN_UTBETALING","mappe":["AVSLAG"],"navnISystem":"EØS-borger uten oppholdsrett","vedtakResultat":"IKKE_INNVILGET","vilkaar":["LOVLIG_OPPHOLD"],"begrunnelsetype":"AVSLAG","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd fordi vi har kommet fram til at ","_key":"6d7cb8681968"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"a97d25bb7bcf"},{"_type":"span","marks":[],"text":" ikke har oppholdsrett som EØS-borger","_key":"bd8be45e07f0"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"e9949cde9277"},{"_type":"span","marks":[],"text":".","_key":"979dd0bfc3ce"}],"_type":"block","style":"normal","_key":"26e8195da460"}],"_type":"begrunnelse","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd fordi vi har kome fram til at ","_key":"edabb729c9c9"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"863c0077971a"},{"marks":[],"text":" ikkje har opphaldsrett som EØS-borgar","_key":"f547fe71d108","_type":"span"},{"_key":"2bcb587bdb27","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"_type":"span","marks":[],"text":".","_key":"99a192a879f2"}],"_type":"block","style":"normal","_key":"d3813ba958f9","markDefs":[]}],"_createdAt":"2021-08-30T12:58:32Z","valgbarhet":"STANDARD","_id":"1008cd2f-156d-4303-bf5d-8d0224310f4a","_updatedAt":"2023-09-25T10:25:57Z"},{"_rev":"h26rAhFEYSUtDGXJE0smx8","valgbarhet":"STANDARD","mappe":["FORTSATT_INNVILGET"],"navnISystem":"Tvungent psykisk helsevern gift","_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","begrunnelsetype":"FORTSATT_INNVILGET","_updatedAt":"2023-09-25T10:23:08Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får utvidet barnetrygd fordi ektefellen din fortsatt er i tvungent psykisk helsevern.","_key":"9805471808ce"}],"_type":"block","style":"normal","_key":"38eab7e7a9b5","markDefs":[]}],"visningsnavn":"27. Tvungent psykisk helsevern gift","vilkaar":["UTVIDET_BARNETRYGD"],"tema":"FELLES","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utvida barnetrygd fordi ektefellen din fortsatt er i tvungent psykisk helsevern.","_key":"7f04eb9c39ce"}],"_type":"block","style":"normal","_key":"0b9d39832247"}],"_createdAt":"2021-10-25T10:17:19Z","_id":"10faf745-8c43-43c9-8c16-8629ab5b4cd4","apiNavn":"fortsattInnvilgetTvungentPsykiskHelsevernGift","hjemler":["9"],"periodeType":"FORTSATT_INNVILGET"},{"_createdAt":"2021-10-26T10:05:23Z","mappe":["AVSLAG"],"nynorsk":[{"_type":"block","style":"normal","_key":"56c6816e8342","markDefs":[],"children":[{"marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"ba1190fb73c0","_type":"span"},{"_key":"8e25ed98a8b3","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi vi har fått ein avtale som seier at ","_key":"f332f98d7a4f"},{"_key":"a329a0ff4255","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" skal bu fast hos den andre forelderen. ","_key":"d1872cd9848d"}]}],"_updatedAt":"2023-09-25T10:30:39Z","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Delt barnetrygd for barn født ","_key":"bac2f10a336f","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"10e49fd63211"},{"_type":"span","marks":[],"text":" fordi vi har fått en avtale som sier at ","_key":"c5c6ab1e1c15"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"648544a3ae6d","skalHaStorForbokstav":false},{"_key":"43531cff0811","_type":"span","marks":[],"text":" skal bo fast hos den andre forelderen. "}],"_type":"block","style":"normal","_key":"ddeac63e00d1"}],"apiNavn":"avslagBarnHarFastBosted","_rev":"h26rAhFEYSUtDGXJE0t5KD","periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","tema":"NASJONAL","navnISystem":"Avtale om fast bosted","vedtakResultat":"IKKE_INNVILGET","_id":"11afd61e-719c-4e31-8922-b8770d2f10bd","valgbarhet":"STANDARD","hjemler":["2"],"visningsnavn":"58. Avtale om fast bosted","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["DELT_BOSTED"]},{"apiNavn":"innvilgetTredjelandsborgerLovligOppholdForBosattINorge","resultat":"INNVILGET_ELLER_ØKNING","vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"INNVILGET","tema":"NASJONAL","nynorsk":[{"children":[{"marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"3b35cf31818f","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"18a5401b4b0c"},{"_type":"span","marks":[],"text":" fordi ","_key":"745beb236234"},{"_type":"valgfeltV2","marks":[],"_key":"84c11a7c501a","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"}},{"marks":[],"text":" har opphaldsløyve og er busett i Noreg frå ","_key":"c5a760ac2575","_type":"span"},{"_key":"1fdcc99be760","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"11ec93c6fafe"}],"_type":"block","style":"normal","_key":"9876e62edefe","markDefs":[]}],"valgbarhet":"STANDARD","navnISystem":"Tredjelandsborger lovlig opphold før bosatt i Norge","_updatedAt":"2023-09-25T08:12:15Z","bokmaal":[{"_key":"f7a3842b8d4d","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn født ","_key":"d95c497c2ade"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"0e44a5ab1ac7"},{"_key":"0927f4fe8db9","_type":"span","marks":[],"text":" fordi "},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","marks":[],"_key":"5d25cd754aa2","skalHaStorForbokstav":false},{"text":" har oppholdstillatelse og er bosatt i Norge fra ","_key":"b385108a0d82","_type":"span","marks":[]},{"_key":"9ad2b8ab8474","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"fcc0c6035aef"}],"_type":"block","style":"normal"}],"mappe":["INNVILGET"],"_rev":"FuD004taptHFqBZyExrscu","rolle":["SOKER","BARN"],"_id":"11b7e978-b45a-427a-93aa-7998cecfbbf4","visningsnavn":"1B. Tredjelandsborger lovlig opphold før bosatt i Norge","periodeType":"UTBETALING","_createdAt":"2021-12-02T11:36:37Z","behandlingstema":"NASJONAL","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","hjemler":["2","4","11"]},{"navnISystem":"Vurdering flere korte opphold i utlandet siste årene","apiNavn":"avslagVurderingFlereKorteOppholdIUtlandetSisteArene","visningsnavn":"26. Vurdering flere korte opphold i utlandet siste årene","begrunnelsetype":"AVSLAG","mappe":["AVSLAG"],"bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"b11c396ce61b","_type":"span"},{"_type":"flettefelt","_key":"2e0322c1bdfd","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at ","_key":"3927cf0c4611"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"5918db1a092a"},{"_type":"span","marks":[],"text":" har hatt flere korte opphold i utlandet de siste årene. Oppholdene i utlandet utgjør til sammen mer enn 6 måneder for hvert år. ","_key":"9699e55bcb79"},{"_type":"valgfeltV2","_key":"43721a8c2d69","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"}},{"marks":[],"text":" regnes derfor derfor ikke som bosatt i Norge ","_key":"7543a417fdcd","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"0d6c6ce610b2"},{"marks":[],"text":". Vi har også kommet fram til at ","_key":"8dcf6e9e3727","_type":"span"},{"_type":"valgfeltV2","_key":"6d8c68df1a1d","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"}},{"_type":"span","marks":[],"text":" ikke fyller vilkårene for å være medlem i folketrygden.","_key":"2ce77d1f47a0"}],"_type":"block","style":"normal","_key":"51d3814f451f"}],"vedtakResultat":"IKKE_INNVILGET","bosattIRiketTriggere":["MEDLEMSKAP"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"d0d566c54dcf"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"a8d9cc4c46e3"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at ","_key":"aabad2f180a7"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"be3056beec00","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" har hatt fleire korte opphald i utlandet dei siste åra. Opphalda i utlandet utgjer til saman meir enn 6 månader for kvart år. ","_key":"5b419745357f"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"c31d2a8867b9","skalHaStorForbokstav":true},{"_key":"a28cd983f7db","_type":"span","marks":[],"text":" er derfor ikkje rekna som busett i Noreg "},{"_key":"c0628850c758","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":". Vi har også kome fram til at ","_key":"461b2a242699"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"23b12ed3612b","skalHaStorForbokstav":false},{"text":" ikkje fyller vilkåra for å vere medlem i folketrygda. ","_key":"eae5b40bd6e7","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"366d5773606c"},{"children":[{"_type":"span","marks":[],"text":"","_key":"673481a66360"}],"_type":"block","style":"normal","_key":"c8b5fdfdc4f0","markDefs":[]}],"_updatedAt":"2023-09-25T10:27:14Z","_type":"begrunnelse","hjemlerFolketrygdloven":["2-5","2-8"],"vilkaar":["BOSATT_I_RIKET"],"rolle":["SOKER","BARN"],"tema":"NASJONAL","_createdAt":"2021-09-29T11:40:19Z","_id":"120433d8-9869-4c36-85fe-70a84c45c42c","hjemler":["4","5"],"_rev":"FuD004taptHFqBZyEy7Jpq","periodeType":"INGEN_UTBETALING","valgbarhet":"STANDARD"},{"apiNavn":"innvilgetVaretektsfengselSamboer","behandlingstema":"NASJONAL","_createdAt":"2021-10-25T07:12:53Z","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:44Z","bokmaal":[{"_key":"2ad2583601c6","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"13f888387fb7"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"4b2c13eb0834","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd selv om du fortsatt er samboer fordi du bor alene med ","_key":"0aafdfec244c"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"364636e8d9b0","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Samboeren din er varetektsfengslet i seks måneder eller mer fra ","_key":"0da6a6388e1e"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"d74f89883eae"},{"text":". ","_key":"f267c3a5c395","_type":"span","marks":[]},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"eefea72625f9","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at varetektsfengslingen har vart i seks måneder.","_key":"85e4a3b82cc8"}],"_type":"block","style":"normal"}],"hjemler":["2","9","11"],"_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","nynorsk":[{"children":[{"marks":[],"text":"","_key":"54fd7a1006ba","_type":"span"},{"_key":"8ef9e3c359ba","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" utvida barnetrygd sjølv om du fortsatt er sambuar fordi du bur aleine med ","_key":"8c5d965b863d"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"67aab675bdfa","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Sambuaren din er varetektsfengsla i seks månader eller meir frå ","_key":"fe2ca10ffa10"},{"_key":"78780b177f72","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"marks":[],"text":". ","_key":"ae8c9009fceb","_type":"span"},{"_key":"859ab22ce19b","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"text":" utvida barnetrygd frå månaden etter at varetektsfengslinga har vart i seks månader.","_key":"938265336672","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"938a3ecf273e","markDefs":[]}],"mappe":["INNVILGET"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"NASJONAL","navnISystem":"Varetektsfengsel samboer","visningsnavn":"45. Varetektsfengsel samboer","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","_id":"124dd0c4-ca06-4ac2-8e9c-207536fb9596"},{"visningsnavn":"21. Fengsel gift","begrunnelsetype":"FORTSATT_INNVILGET","tema":"FELLES","_updatedAt":"2023-09-25T10:22:45Z","bokmaal":[{"style":"normal","_key":"3ec51c300674","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utvidet barnetrygd fordi ektefellen din fortsatt er i fengsel.","_key":"9ff46479544c"}],"_type":"block"}],"valgbarhet":"STANDARD","_id":"12b4498e-35a9-4c13-8f34-545597c2abb5","hjemler":["9"],"_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"markDefs":[],"children":[{"text":"Du får utvida barnetrygd fordi ektefellen din fortsatt er i fengsel.","_key":"3c8c4ff2eeff","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"ba3bd963414b"}],"_createdAt":"2021-10-25T10:08:29Z","mappe":["FORTSATT_INNVILGET"],"navnISystem":"Fengsel gift","apiNavn":"fortsattInnvilgetFengselGift","_rev":"h26rAhFEYSUtDGXJE0slRe","periodeType":"FORTSATT_INNVILGET"},{"_rev":"h26rAhFEYSUtDGXJE0t4sm","_type":"begrunnelse","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:30:28Z","hjemler":["9"],"bokmaal":[{"_type":"block","style":"normal","_key":"a39c076a9d02","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi vi har kommet fram til at du ikke har egen husholdning og derfor fortsatt er samboer.","_key":"969462b23a5d"}]}],"begrunnelsetype":"AVSLAG","visningsnavn":"52. Ikke egen husholdning samboer","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"INGEN_UTBETALING","tema":"FELLES","apiNavn":"avslagIkkeEgenHusholdningSamboer","vedtakResultat":"IKKE_INNVILGET","nynorsk":[{"style":"normal","_key":"191c44db97c4","markDefs":[],"children":[{"_key":"4914ef0c68c9","_type":"span","marks":[],"text":"Utvida barnetrygd fordi vi har kome fram til at du ikkje har eiga hushaldning og derfor fortsatt er sambuar."}],"_type":"block"}],"_createdAt":"2021-10-22T09:27:46Z","valgbarhet":"STANDARD","_id":"14913238-4617-4d57-879f-7020d8eb5225","navnISystem":"Ikke egen husholdning samboer"},{"navnISystem":"Barn uten fødselsnummer","apiNavn":"avslagUregistrertBarn","nynorsk":[{"style":"normal","_key":"de71fcfa1a3c","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd fordi vi har komme fram til at det ikkje er stadfesta at ","_key":"785913270de7"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"cb02015e164d"},{"marks":[],"text":" er busett i Noreg. ","_key":"3d94e8f463f9","_type":"span"}],"_type":"block"}],"_id":"15df58e8-cb73-4f79-a964-db2434853566","hjemler":["2","4"],"tema":"NASJONAL","valgbarhet":"AUTOMATISK","_createdAt":"2021-08-31T08:22:54Z","_updatedAt":"2023-09-25T10:26:27Z","visningsnavn":"13. Barn uten fødselsnummer","_rev":"FuD004taptHFqBZyEy7FR1","_type":"begrunnelse","mappe":["AVSLAG"],"bokmaal":[{"_type":"block","style":"normal","_key":"b28f208828a9","markDefs":[],"children":[{"_key":"a5a79922db91","_type":"span","marks":[],"text":"Barnetrygd fordi vi har kommet fram til at det ikke er bekreftet at "},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"a49bf36d48d7"},{"_type":"span","marks":[],"text":" er bosatt i Norge. ","_key":"b8ac0eae52d1"}]}],"vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG"},{"visningsnavn":"24. Vurdering annen forelder ikke medlem","vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"AVSLAG","_id":"16c0df27-d0c7-4f08-bfc4-546ef6a6364b","_updatedAt":"2023-09-25T10:27:07Z","apiNavn":"avslagVurderingAnnenForelderIkkeMedlem","behandlingstema":"NASJONAL","_rev":"FuD004taptHFqBZyEy7JA7","hjemlerFolketrygdloven":["2-5","2-8"],"periodeType":"INGEN_UTBETALING","mappe":["AVSLAG"],"hjemler":["4","5"],"_type":"begrunnelse","rolle":["SOKER","BARN"],"tema":"NASJONAL","bosattIRiketTriggere":["MEDLEMSKAP"],"_createdAt":"2021-09-29T11:08:57Z","valgbarhet":"STANDARD","navnISystem":"Vurdering annen forelder ikke medlem","vedtakResultat":"IKKE_INNVILGET","nynorsk":[{"_type":"block","style":"normal","_key":"775b73407ca5","markDefs":[],"children":[{"_key":"9cfef66a07d2","_type":"span","marks":[],"text":"Barnetrygd for barn fødd "},{"_key":"0eff42baa3c3","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at den andre forelderen ikkje fyller vilkåra for å vere medlem i folketrygda under opphaldet i utlandet","_key":"45e3afd9343e"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"f18d91a64e11"},{"marks":[],"text":". Bur begge foreldre saman under opphaldet i utlandet, må begge vere medlem i folketrygda for å få barnetrygd.","_key":"8d5a874cfe22","_type":"span"}]}],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"cb2fab001168"},{"_type":"flettefelt","_key":"12e2f2387020","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at den andre forelderen ikke fyller vilkårene for å være medlem i folketrygden under oppholdet i utlandet","_key":"1065c59b8199"},{"_key":"bd033a3a5347","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"_type":"span","marks":[],"text":". Bor begge foreldrene sammen under oppholdet i utlandet, må dere begge være medlem i folketrygden for å få barnetrygd. ","_key":"4eba9dd38d17"}],"_type":"block","style":"normal","_key":"3af91cbfa73e","markDefs":[]}]},{"vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi du har en avtale om delt bosted for barn født ","_key":"ad9352d8f1d9"},{"_key":"60a18bad504f","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":". Barnetrygden deles fra måneden etter at den andre forelderen har søkt om barnetrygd. Den deles før fristen du har fått for å uttale deg har gått ut. På denne måten unngår vi å utbetale for mye barnetrygd til deg. Vedtaket kan endres hvis det viser seg å være feil. ","_key":"6033932f502c"}],"_type":"block","style":"normal","_key":"4f494aa7290e","markDefs":[]}],"apiNavn":"reduksjonDeltBarnetrygdHastevedtak","visningsnavn":"60. Delt barnetrygd hastevedtak","_type":"begrunnelse","borMedSokerTriggere":["DELT_BOSTED"],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du har ein avtale om delt bustad for barn fødd ","_key":"e1d5e7fde25e"},{"_key":"0e36479011cd","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":". Barnetrygda er delt frå månaden etter at den andre forelderen har søkt om barnetrygd. Den er delt før fristen du har fått for å uttale deg har gått ut. På denne måten unngår vi å utbetale for mykje barnetrygd til deg. Vedtaket kan endrast dersom det viser seg å vere feil. ","_key":"26a3baf147c3"}],"_type":"block","style":"normal","_key":"e8a836823a6e","markDefs":[]}],"_createdAt":"2022-02-24T17:02:53Z","_id":"16cca0ac-b50a-4315-bf5e-f4fbedd4c29a","hjemler":["2","12","14"],"_rev":"BtltdVb0HP4g4WJfnr4V7o","begrunnelsetype":"REDUKSJON","valgbarhet":"STANDARD","navnISystem":"Delt barnetrygd hastevedtak","vilkaar":["BOR_MED_SOKER"],"tema":"NASJONAL","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:44Z"},{"begrunnelsetype":"FORTSATT_INNVILGET","_createdAt":"2021-10-25T10:04:58Z","mappe":["FORTSATT_INNVILGET"],"_type":"begrunnelse","_updatedAt":"2023-09-25T10:22:37Z","bokmaal":[{"_type":"block","style":"normal","_key":"09b22d4bc505","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utvidet barnetrygd fordi vi har kommet fram til av du fortsatt bor alene med ","_key":"8fb3d85ee54b"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"02f4b59f14b0","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":".","_key":"4af280316cd1"}]}],"apiNavn":"fortsattInnvilgetVurderingBorAleneMedBarn","hjemler":["9"],"periodeType":"FORTSATT_INNVILGET","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utvida barnetrygd fordi vi har komme fram til av du fortsatt bur aleine med ","_key":"244aa04101b9"},{"_key":"33aeaedb9af1","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"text":".","_key":"79f74ae3df88","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"6f0a8e496725"}],"vilkaar":["UTVIDET_BARNETRYGD"],"tema":"FELLES","valgbarhet":"STANDARD","_id":"1803c21b-ab37-4861-94b8-59dff1635014","navnISystem":"Vurdering bor alene med barn","visningsnavn":"19. Vurdering bor alene med barn","_rev":"BtltdVb0HP4g4WJfnr4uSJ","vedtakResultat":"INGEN_ENDRING"},{"apiNavn":"innvilgetEnighetOmAtAvtalenOmDeltBostedErOpphort","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["BOR_MED_SOKER"],"mappe":["INNVILGET"],"_type":"begrunnelse","_updatedAt":"2023-09-25T10:15:44Z","hjemler":["2","4","11"],"visningsnavn":"16. Enighet om opphør av avtale om delt bosted\t","_rev":"BtltdVb0HP4g4WJfnr4V7o","begrunnelsetype":"INNVILGET","tema":"NASJONAL","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for ","_key":"1729eeff4c09"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"eccb353f6cd4"},{"_type":"span","marks":[],"text":" fordi barnet/barna bur fast hos deg frå ","_key":"325c9be5f7a3"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"1389d1013a8c"},{"_type":"span","marks":[],"text":".","_key":"0094e1ff2c47"}],"_type":"block","style":"normal","_key":"868fa5180e52"}],"valgbarhet":"STANDARD","navnISystem":"Enighet om opphør av avtale om delt bosted","behandlingstema":"NASJONAL","periodeType":"UTBETALING","_createdAt":"2021-08-27T09:01:16Z","_id":"18825f58-232c-475f-9f66-d831c88af7f1","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"1ef51e15c1a3"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"13a7652887ca"},{"_type":"span","marks":[],"text":" fordi ","_key":"389ee6cc3744"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"94b6f5224bc2"},{"_type":"span","marks":[],"text":" bor fast hos deg fra ","_key":"8a346d337a39"},{"_key":"58867bc20208","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_key":"cb5ab47e5ee7","_type":"span","marks":[],"text":"."}],"_type":"block","style":"normal","_key":"c8d66ce72de0"}]},{"_id":"18e201c3-7f56-40b4-893a-671a0485a358","mappe":["EØS","INNVILGET"],"apiNavn":"innvilgetTilleggsbegrunnelseRefusjon","behandlingstema":"EØS","_createdAt":"2022-05-23T13:00:20Z","_updatedAt":"2023-09-25T10:15:44Z","visningsnavn":"17. Tilleggsbegrunnelse refusjon","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","navnISystem":"Tilleggsbegrunnelse refusjon","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Myndighetene i (SØKERS OPPRINNELIGE BOSTEDSLAND)/ (ANNEN FORELDRES AKTIVITETSLAND) har allereie utbetalt (UTBETALT FRA SØKERS OPPRINNELIGE BOSTEDSLAND)/(UTBETALT FRA ANNEN FORELDERS AKTIVITETSLAND) (VALUTA) i barnetrygd frå (DATO) til (DATO). Dei ber derfor om at Noreg betalar desse pengane tilbake. Derfor blir (UTBETALT FRA SØKERS OPPRINNELIGE BOSTEDSLAND OMREGNET TIL NORSKE KRONER)/(UTBETALT FRA ANNEN FORELDERS AKTIVITETSLAND OMREGNET TIL NORSKE KRONER) kroner av etterbetalinga utbetalt til myndighetene i (SØKERS OPPRINNELIGE BOSTEDSLAND)/(ANNEN FORELDRES AKTIVITETSLAND).","_key":"b56eca610d520"}],"_type":"block","style":"normal","_key":"b21ad6991775","markDefs":[]}],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Myndighetene i (SØKERS OPPRINNELIGE BOSTEDSLAND)/ (ANNEN FORELDRES AKTIVITETSLAND) har allerede utbetalt (UTBETALT FRA SØKERS OPPRINNELIGE BOSTEDSLAND)/(UTBETALT FRA ANNEN FORELDERS AKTIVITETSLAND) (VALUTA) i barnetrygd fra (DATO) til (DATO). De ber derfor om at Norge betaler disse pengene tilbake. Derfor blir (UTBETALT FRA SØKERS OPPRINNELIGE BOSTEDSLAND OMREGNET TIL NORSKE KRONER)/(UTBETALT FRA ANNEN FORELDERS AKTIVITETSLAND OMREGNET TIL NORSKE KRONER) kroner av etterbetalingen utbetalt til myndighetene i (SØKERS OPPRINNELIGE BOSTEDSLAND)/(ANNEN FORELDRES AKTIVITETSLAND).","_key":"5b6856b920610"}],"_type":"block","style":"normal","_key":"351e7339c9d7"}]},{"apiNavn":"innvilgetAvtaleDeltBostedFaarFraFlyttetidspunkt","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","navnISystem":"Avtale delt bosted får fra flyttetidspunkt","_rev":"BtltdVb0HP4g4WJfnr4V7o","tema":"NASJONAL","nynorsk":[{"_key":"05e33af5e161","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får delt barnetrygd for barn fødd ","_key":"4267d4b777c3"},{"_type":"flettefelt","_key":"0e3e348e5e56","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi du har skriftleg avtale om delt bustad for ","_key":"daed0d57382e"},{"_type":"valgfeltV2","_key":"c4ac2304f887","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":". Du og den andre forelderen har flytta frå kvarandre ","_key":"dbef496b0f82"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"1627a5d4674d"},{"_type":"span","marks":[],"text":". Barnetrygda er delt frå månaden etter flyttinga.","_key":"11b2a5db539b"}],"_type":"block","style":"normal"}],"_updatedAt":"2023-09-25T10:15:44Z","visningsnavn":"60. Avtale delt bosted får fra flyttetidspunkt","behandlingstema":"NASJONAL","_createdAt":"2021-10-26T08:43:18Z","valgbarhet":"STANDARD","_id":"1b32f5f1-3f3a-48b0-a699-e2c33b0f11d7","hjemler":["2","11"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","borMedSokerTriggere":["DELT_BOSTED"],"mappe":["INNVILGET"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får delt barnetrygd for barn født ","_key":"fcedcb11855c"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"df1fdd35da11"},{"_type":"span","marks":[],"text":" fordi du har skriftlig avtale om delt bosted for ","_key":"7e03543d2da0"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"e7a9bf515198","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Du og den andre forelderen har flyttet fra hverandre ","_key":"dd6968ced613"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"3b2b36cff9f6"},{"_key":"77a31cb33229","_type":"span","marks":[],"text":". Barnetrygden deles fra måneden etter flyttingen."}],"_type":"block","style":"normal","_key":"65dc0d75fdd2"}]},{"rolle":["SOKER"],"valgbarhet":"STANDARD","visningsnavn":"39. Gyldig kontonummer registrert","behandlingstema":"EØS","_rev":"BtltdVb0HP4g4WJfnr4V7o","vedtakResultat":"INNVILGET_ELLER_ØKNING","_createdAt":"2022-11-08T12:49:32Z","_id":"1b62abde-30eb-4ef0-9627-0f9742eaa255","navnISystem":"Gyldig kontonummer registrert ","apiNavn":"innvilgetGyldigKontonummerRegistrertEos","hjemler":["2","4","11"],"periodeType":"UTBETALING","_updatedAt":"2023-09-25T10:15:44Z","bokmaal":[{"style":"normal","_key":"12a3703966d5","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får fortsatt utbetalt barnetrygd fordi du nå har registrert gyldig kontonummer. Barnetrygden utbetales fra samme måned som den var stanset fra.","_key":"1fc965e72a990"}],"_type":"block"}],"_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"nynorsk":[{"markDefs":[],"children":[{"text":"Du får fortsatt utbetalt barnetrygd fordi du no har registrert gyldig kontonummer. Barnetrygda vert utbetalt frå same månad som den var stansa frå.","_key":"99778d8a992b0","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"41377ad38ea0"}],"mappe":["EØS","INNVILGET"],"begrunnelsetype":"INNVILGET","tema":"FELLES"},{"rolle":["SOKER"],"nynorsk":[{"children":[{"text":"Du får barnetrygd fordi heile familien fortsatt er medlem av folketrygda etter trygdeavtale under opphaldet i utlandet.","_key":"45dff1aa24a3","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"2fdb5d52a616","markDefs":[]}],"navnISystem":"Hele familien medlem etter trygdeavtale","hjemler":["2","4","22"],"visningsnavn":"15. Hele familien medlem etter trygdeavtale","_rev":"h26rAhFEYSUtDGXJE0sk5J","vedtakResultat":"INGEN_ENDRING","begrunnelsetype":"FORTSATT_INNVILGET","_updatedAt":"2023-09-25T10:22:21Z","bokmaal":[{"style":"normal","_key":"cd0a3d5631e9","markDefs":[],"children":[{"_key":"2896edc5e9dd","_type":"span","marks":[],"text":"Du får barnetrygd fordi hele familien fortsatt er medlem av folketrygden etter trygdeavtale under oppholdet i utlandet."}],"_type":"block"}],"_createdAt":"2021-09-24T12:35:59Z","apiNavn":"fortsattInnvilgetHeleFamilienMedlemEtterTrygdeavtale","_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"bosattIRiketTriggere":["MEDLEMSKAP"],"_id":"1cac97a7-e55e-4ebc-8a18-dd8e90631393","mappe":["FORTSATT_INNVILGET"],"behandlingstema":"NASJONAL","periodeType":"FORTSATT_INNVILGET","tema":"NASJONAL","valgbarhet":"STANDARD"},{"apiNavn":"fortsattInnvilgetOppholdIUtlandetIkkeMerEnn3ManederBarn","_type":"begrunnelse","_createdAt":"2021-09-24T12:31:35Z","_id":"1dcd70f7-719b-4cf7-8da0-2a05b2cc5708","mappe":["FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:22:14Z","_rev":"BtltdVb0HP4g4WJfnr4sao","vilkaar":["BOSATT_I_RIKET"],"periodeType":"FORTSATT_INNVILGET","begrunnelsetype":"FORTSATT_INNVILGET","nynorsk":[{"markDefs":[],"children":[{"_key":"39052f529323","_type":"span","marks":[],"text":"Du får barnetrygd fordi "},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"506fa108d37b"},{"_type":"span","marks":[],"text":" fortsatt er rekna som busett i Noreg. Ved opphald i utlandet som ikkje varer lenger enn tre månader, er ","_key":"22e4457f7b91"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"7272483ea073"},{"_type":"span","marks":[],"text":" fortsatt rekna som busett i Noreg.","_key":"ec30c616d767"}],"_type":"block","style":"normal","_key":"24cbf1125096"}],"valgbarhet":"STANDARD","visningsnavn":"13. Opphold i utlandet ikke mer enn 3 måneder barn","rolle":["BARN"],"tema":"NASJONAL","bosattIRiketTriggere":["MEDLEMSKAP"],"navnISystem":"Opphold i utlandet ikke mer enn 3 måneder barn","hjemler":["4"],"vedtakResultat":"INGEN_ENDRING","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi ","_key":"a0bf2b0af4ab"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"06697107cbe1"},{"text":" fortsatt er regnet som bosatt i Norge. Ved opphold i utlandet som ikke varer lenger enn tre måneder, er ","_key":"b47a1ad1b091","_type":"span","marks":[]},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"ce1f83fcd5b8"},{"text":" fortsatt regnet som bosatt i Norge.","_key":"918931478981","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"3e39a57dff8d","markDefs":[]}]},{"apiNavn":"innvilgetTilleggstekstEosBorgerUtbetalingNav","visningsnavn":"83. Tilleggstekst EØS borger utbetaling NAV","rolle":["SOKER","BARN"],"bokmaal":[{"_key":"e5501fc9f5e4","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har oppholdsrett som EØS-borger. Familien bor i Norge og du får utbetaling fra NAV som erstatter lønn.","_key":"0d891184b6780"}],"_type":"block","style":"normal"}],"_type":"begrunnelse","_id":"1dfb65f4-d9a6-481d-802d-209cb1b49fb9","nynorsk":[{"_key":"d057faf6ec05","markDefs":[],"children":[{"text":"Du får barnetrygd fordi du har opphaldsrett som EØS-borgar. Familien bur i Noreg og du får utbetaling frå NAV som erstattar løn.","_key":"eeed8ec64a920","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"navnISystem":"Tilleggstekst EØS borger utbetaling NAV","hjemler":["2","4"],"behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr4V7o","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","tema":"NASJONAL","_createdAt":"2022-05-24T14:10:03Z","valgbarhet":"TILLEGGSTEKST","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:44Z","vilkaar":["BOSATT_I_RIKET","LOVLIG_OPPHOLD"],"begrunnelsetype":"INNVILGET"},{"periodeType":"INGEN_UTBETALING","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:31:28Z","navnISystem":"Ikke bosatt i Norge","_rev":"h26rAhFEYSUtDGXJE0t8pf","hjemler":["2","4","11"],"begrunnelsetype":"OPPHØR","vilkaar":["BOSATT_I_RIKET"],"rolle":["SOKER","BARN"],"_id":"1e1cf498-bfe5-4833-9176-f234569c982b","mappe":["OPPHØR"],"bokmaal":[{"markDefs":[],"children":[{"_key":"bfb7ed9db99e","_type":"span","marks":[],"text":"Barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"696f34dde033"},{"marks":[],"text":" fordi ","_key":"45bc28e03ae6","_type":"span"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"75190c6569e9"},{"_type":"span","marks":[],"text":" ikke er bosatt i Norge ","_key":"4b95a1458ebf"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"dec566116f4a"},{"_type":"span","marks":[],"text":".","_key":"c489e9007a46"}],"_type":"block","style":"normal","_key":"0b222d5ca0da"}],"apiNavn":"opphorFlyttetFraNorge","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","tema":"NASJONAL","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"07c83657f607"},{"_key":"94acb7978e7d","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"text":" fordi ","_key":"429dbd95c8d0","_type":"span","marks":[]},{"_type":"valgReferanse","_key":"2b6316e6e798","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},{"text":" ikkje er busett i Noreg ","_key":"e5456fe5304c","_type":"span","marks":[]},{"_type":"valgReferanse","_key":"4633f4d824b3","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"_type":"span","marks":[],"text":".","_key":"cc2857be046e"}],"_type":"block","style":"normal","_key":"7605ca9d07d3"}],"_createdAt":"2021-08-30T10:06:27Z","visningsnavn":"3. Ikke bosatt i Norge","behandlingstema":"NASJONAL"},{"_id":"1fe56423-b1ad-4a09-b263-bb5a9c84cf6a","apiNavn":"fortsattInnvilgetSeparert","visningsnavn":"20. Separert","begrunnelsetype":"FORTSATT_INNVILGET","_createdAt":"2021-10-25T10:06:35Z","valgbarhet":"STANDARD","_rev":"h26rAhFEYSUtDGXJE0sl9M","vilkaar":["UTVIDET_BARNETRYGD"],"tema":"FELLES","mappe":["FORTSATT_INNVILGET"],"bokmaal":[{"_key":"e2c7d3e6a2c5","markDefs":[],"children":[{"text":"Du får utvidet barnetrygd fordi du fortsatt er separert og bor alene med ","_key":"279bfcf2c60e","_type":"span","marks":[]},{"_key":"d014b158933b","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"marks":[],"text":".","_key":"2d9cfb566177","_type":"span"}],"_type":"block","style":"normal"}],"navnISystem":"Separert","hjemler":["9"],"_type":"begrunnelse","periodeType":"FORTSATT_INNVILGET","_updatedAt":"2023-09-25T10:22:41Z","vedtakResultat":"INGEN_ENDRING","nynorsk":[{"children":[{"marks":[],"text":"Du får utvida barnetrygd fordi du fortsatt er separert og bur aleine med ","_key":"89c68648d7d7","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"eb6af89ca5b5","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":".","_key":"4ffad5c044c7"}],"_type":"block","style":"normal","_key":"ae52a78e1d83","markDefs":[]}]},{"tema":"NASJONAL","_id":"20e1f9b2-2359-4dec-a5b8-ecf617dc55aa","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"7fd3196ca8cd"},{"_key":"bca050b3436c","_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse"},{"_type":"span","marks":[],"text":" under oppholdet i utlandet. Vi har kommet fram til at hele familien fyller vilkårene for å bli frivillig medlem i folketrygden fra ","_key":"675640bf97fe"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"158eaefa8027"},{"_type":"span","marks":[],"text":". Dette gir rett til barnetrygd fra Norge.","_key":"dad321d0f21f"}],"_type":"block","style":"normal","_key":"f8ad880b3fe2"}],"bosattIRiketTriggere":["MEDLEMSKAP"],"_createdAt":"2021-09-24T12:09:27Z","mappe":["INNVILGET"],"apiNavn":"innvilgetVurderingHeleFamilienFrivilligMedlem","visningsnavn":"27. Vurdering hele familien frivillig medlem","hjemlerFolketrygdloven":["2-8"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["BOSATT_I_RIKET"],"navnISystem":"Vurdering hele familien frivillig medlem","hjemler":["4","5"],"_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","rolle":["SOKER","BARN"],"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","nynorsk":[{"style":"normal","_key":"981e16d1ed01","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"5e06e168d935"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"8ff94df2bb5e"},{"_key":"9542e69c3bf1","_type":"span","marks":[],"text":" under opphaldet i utlandet. Vi har kome fram til at heile familien fyller vilkåra for å bli frivillig medlem i folketrygda frå "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"34bab07e3c00"},{"_type":"span","marks":[],"text":". Dette gjev rett til barnetrygd frå Noreg.","_key":"4219618137ad"}],"_type":"block"}],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:44Z"},{"tema":"FELLES","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:29:39Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi du er gift. Å bo midlertidig hver for seg gir ikke rett til utvidet barnetrygd.","_key":"a3fdad860452"}],"_type":"block","style":"normal","_key":"1108a9e9b8d3"}],"navnISystem":"Gift midlertidig adskillelse","apiNavn":"avslagGiftMidlertidigAdskillelse","periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","hjemler":["9"],"valgbarhet":"STANDARD","behandlingstema":"NASJONAL","vilkaar":["UTVIDET_BARNETRYGD"],"_id":"226ca2b2-5937-4428-b930-70b73bc0c80a","nynorsk":[{"children":[{"text":"Utvida barnetrygd fordi du er gift. Å bu mellombels kvar for seg gjev ikkje rett til utvida barnetrygd.","_key":"5764eec13712","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"daf51937ae5e","markDefs":[]}],"_createdAt":"2021-10-22T08:23:29Z","visningsnavn":"38. Gift midlertidig adskillelse","_rev":"BtltdVb0HP4g4WJfnr5JOo","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING"},{"behandlingstema":"NASJONAL","vilkaar":["BOR_MED_SOKER"],"periodeType":"INGEN_UTBETALING","tema":"NASJONAL","mappe":["OPPHØR"],"navnISystem":"Delt bosted søker ber om opphør","hjemler":["2"],"_rev":"h26rAhFEYSUtDGXJE0v341","_updatedAt":"2023-09-25T10:36:51Z","bokmaal":[{"_key":"936ab00cf428","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn født ","_key":"9cf7b7d182860"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"045813b721e1"},{"marks":[],"text":" fordi du har bedt om at barnetrygden for ","_key":"1ea3ccb187a0","_type":"span"},{"_type":"valgfeltV2","_key":"4d31b9baf74f","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":" blir stanset.","_key":"1cbd23c162f7"}],"_type":"block","style":"normal"}],"_id":"23266556-75b5-424c-807e-6a906541c46b","visningsnavn":"50. Delt bosted søker ber om opphør","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","borMedSokerTriggere":["DELT_BOSTED"],"begrunnelsetype":"OPPHØR","_createdAt":"2022-11-02T12:21:45Z","apiNavn":"opphordeltBostedSoekerBerOmOpphoer","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"9dc52ab94eee0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"8323c4a6299b"},{"_type":"span","marks":[],"text":" fordi du har bedt om at barnetrygda for ","_key":"8b284759bf91"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"17a0c5d8c5c5","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" blir stansa.","_key":"ccc31ae6def7"}],"_type":"block","style":"normal","_key":"4aec1a2b0a57","markDefs":[]}],"valgbarhet":"STANDARD"},{"_createdAt":"2022-05-13T13:08:47Z","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:44Z","navnISystem":"Tilleggstekst samboer mindre enn 12 måneder før nytt barn","_rev":"BtltdVb0HP4g4WJfnr4V7o","vilkaar":["UTVIDET_BARNETRYGD"],"bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"","_key":"67a901e1fae1","_type":"span"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"e9a43ef19955","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd fram til du fikk nytt barn.","_key":"26a8cd3756480"}],"_type":"block","style":"normal","_key":"c535357c9b6c"}],"_type":"begrunnelse","tema":"FELLES","nynorsk":[{"children":[{"marks":[],"text":"","_key":"20656015e3a9","_type":"span"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"b33a6e399a8f","skalHaStorForbokstav":false},{"marks":[],"text":" utvida barnetrygd fram til du fekk nytt barn.","_key":"f830fd0e9f660","_type":"span"}],"_type":"block","style":"normal","_key":"337d5c5d79b6","markDefs":[]}],"_id":"25330ab4-29a0-45fc-bf03-1f290c9dcaea","apiNavn":"innvilgetTilleggstekstSamboerUnder12MaanederForNyttBarn","visningsnavn":"80. Tilleggstekst samboer under 12 måneder før nytt barn","vedtakResultat":"INNVILGET_ELLER_ØKNING","valgbarhet":"TILLEGGSTEKST","hjemler":["2","9","11"],"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET"},{"navnISystem":"Ikke flyttet fra tidligere ektefelle","vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"AVSLAG","nynorsk":[{"style":"normal","_key":"642dc5e9be6e","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi du og den tidligare ektefellen din ikkje har flytta frå kvarandre.","_key":"223e25f2e0c70"}],"_type":"block"}],"_createdAt":"2022-04-05T15:14:26Z","apiNavn":"avslagIkkeFlyttetFraTidligereEktefelle","valgbarhet":"STANDARD","_id":"25414622-a22c-4006-a929-a405c8a56289","_rev":"h26rAhFEYSUtDGXJE0t6VN","tema":"NASJONAL","bokmaal":[{"_key":"0a873444b46d","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi du og den tidligere ektefellen din ikke har flyttet fra hverandre.","_key":"21b36334657d0"}],"_type":"block","style":"normal"}],"hjemler":["9"],"visningsnavn":"63. Ikke flyttet fra tidligere ektefelle","_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"INGEN_UTBETALING","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:30:58Z"},{"vilkaar":["BOSATT_I_RIKET"],"periodeType":"INGEN_UTBETALING","tema":"NASJONAL","nynorsk":[{"_key":"6ec9f5b142cf","markDefs":[],"children":[{"_key":"416e320f97dc","_type":"span","marks":[],"text":"Barnetrygd for barn født "},{"_key":"378b28ee4117","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"text":" fordi den andre forelderen ikkje er pliktig medlem i folketrygda ","_key":"916a90cfe552","_type":"span","marks":[]},{"_type":"valgReferanse","_key":"edaada1bc6d0","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"_key":"fe2402656a0a","_type":"span","marks":[],"text":". Bur begge foreldre saman under opphaldet i utlandet, må begge vere medlem i folketrygda for å få barnetrygd. "}],"_type":"block","style":"normal"}],"mappe":["OPPHØR"],"visningsnavn":"18. Annen forelder ikke pliktig medlem","_rev":"BtltdVb0HP4g4WJfnr5Z1o","vedtakResultat":"IKKE_INNVILGET","_updatedAt":"2023-09-25T10:34:27Z","behandlingstema":"NASJONAL","hjemlerFolketrygdloven":["2-5"],"valgbarhet":"STANDARD","bosattIRiketTriggere":["MEDLEMSKAP"],"_id":"25754ebc-adf1-4af1-817d-e36f42c770dd","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"69f86955371a"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"90005f01d77b"},{"_key":"915347a5ff74","_type":"span","marks":[],"text":" fordi den andre forelderen ikke er pliktig medlem i folketrygden "},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"e6d8e189ee6f"},{"marks":[],"text":". Bor begge foreldrene sammen under oppholdet i utlandet, må dere begge være medlem i folketrygden for å få barnetrygd.","_key":"d71e14164791","_type":"span"}],"_type":"block","style":"normal","_key":"c74bb669e2ae"}],"navnISystem":"Annen forelder ikke pliktig medlem","rolle":["SOKER"],"begrunnelsetype":"OPPHØR","_createdAt":"2021-09-24T11:26:29Z","apiNavn":"opphorAnnenForelderIkkeLengerPliktigMedlem","hjemler":["4","5"],"_type":"begrunnelse"},{"_rev":"BtltdVb0HP4g4WJfnr5gkJ","vedtakResultat":"IKKE_INNVILGET","tema":"NASJONAL","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:35:58Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn født ","_key":"fa029855ed5d"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"6a2fa6a56c24"},{"text":" fordi du bodde sammen med den andre forelderen til ","_key":"5b3e55e1495c","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"f9f2ae2d177c","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":".","_key":"aaef1e578315"}],"_type":"block","style":"normal","_key":"e5b5da5ebfcb","markDefs":[]}],"navnISystem":"Foreldrene bodd sammen","_createdAt":"2021-11-05T11:22:18Z","ovrigeTriggere":["GJELDER_FØRSTE_PERIODE"],"nynorsk":[{"style":"normal","_key":"aef7b438bb80","markDefs":[],"children":[{"text":"Delt barnetrygd for barn fødd ","_key":"14de1af49d90","_type":"span","marks":[]},{"_type":"flettefelt","_key":"7fb71f6e291a","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi du budde saman med den andre forelderen til ","_key":"960f557b5f26"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"5c503ccd1ff1","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":".","_key":"d124068efa37"}],"_type":"block"}],"apiNavn":"opphorForeldreneBoddSammen","hjemler":["2"],"visningsnavn":"37. Foreldrene bodd sammen","vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"INGEN_UTBETALING","begrunnelsetype":"OPPHØR","_id":"26d98fe5-411b-4a90-be1e-884ed76899bb","_type":"begrunnelse","mappe":["OPPHØR"]},{"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:26:45Z","visningsnavn":"18. Ikke frivillig medlem","vilkaar":["BOSATT_I_RIKET"],"periodeType":"INGEN_UTBETALING","_createdAt":"2021-09-29T10:55:54Z","nynorsk":[{"style":"normal","_key":"0404d03e871d","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"8dd996b64437"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"3040c2fe3a7f"},{"_type":"span","marks":[],"text":" fordi ","_key":"5b1b90fa34b5"},{"_type":"valgReferanse","_key":"223870f63325","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},{"text":"ikkje er frivillig medlem i folketrygda","_key":"1ad626d0bdda","_type":"span","marks":[]},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"d5e8a414fd1c"},{"_type":"span","marks":[],"text":".","_key":"6e2d291213e8"}],"_type":"block"}],"bokmaal":[{"style":"normal","_key":"117d7fde1f1c","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"47b3996e02b7"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"359666eff9ff"},{"_type":"span","marks":[],"text":" fordi ","_key":"4d756eb1434c"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"d708efbdc292"},{"_type":"span","marks":[],"text":" ikke er frivillig medlem i folketrygden","_key":"a4502d5d6439"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"cb5060d39a4d"},{"text":". ","_key":"27ae4d283ed4","_type":"span","marks":[]}],"_type":"block"}],"_rev":"h26rAhFEYSUtDGXJE0sxjk","_type":"begrunnelse","hjemlerFolketrygdloven":["2-8"],"tema":"NASJONAL","navnISystem":"Ikke frivillig medlem","hjemler":["4","5"],"begrunnelsetype":"AVSLAG","mappe":["AVSLAG"],"bosattIRiketTriggere":["MEDLEMSKAP"],"_id":"2822b8d7-832a-4d6d-8657-7eca52a4a634","apiNavn":"avslagIkkeFrivilligMedlem","behandlingstema":"NASJONAL","vedtakResultat":"IKKE_INNVILGET","rolle":["SOKER","BARN"]},{"periodeType":"UTBETALING","nynorsk":[{"_key":"e7c989de44f0","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"8c0e3ce6a58b"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"7e23786f7dd0","skalHaStorForbokstav":true},{"text":" utvida barnetrygd sjølv om du fortsatt er gift fordi du bur aleine med ","_key":"9ea379b5f75c","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"db926cee5c48","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Ektefellen din er i tvungent psykisk helsevern frå ","_key":"1500bb79403a"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"3921c9ca74cd"},{"_type":"span","marks":[],"text":". ","_key":"5c817727f8a0"},{"_type":"valgfeltV2","_key":"ad2358f97d36","skalHaStorForbokstav":true,"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"}},{"text":" utvida barnetrygd frå månaden etter at du vart aleine med barnet/barna.","_key":"19bd9b4d60f9","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"_createdAt":"2021-10-25T07:20:03Z","navnISystem":"Tvungent psykisk helsevern gift","vilkaar":["UTVIDET_BARNETRYGD"],"behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"FELLES","_id":"28a1175a-267e-4e84-861a-dd58d6f21f6e","apiNavn":"innvilgetTvungentPsykiskHelsevernGift","visningsnavn":"48. Tvungent psykisk helsevern gift","mappe":["INNVILGET"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"","_key":"84db74b7f6a0"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"9f02616c5132","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd selv om du fortsatt er gift fordi du bor alene med ","_key":"7fbbebb95d59"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"3bbc3700cd02"},{"_key":"e02dab301a47","_type":"span","marks":[],"text":". Ektefellen din er i tvungent psykisk helsevern fra "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"35515a189168"},{"_type":"span","marks":[],"text":". ","_key":"4f63f8ca7935"},{"_type":"valgfeltV2","_key":"6a99880c3fef","skalHaStorForbokstav":true,"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"}},{"_key":"d91dfb0ac026","_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at du ble alene med barnet/barna."}],"_type":"block","style":"normal","_key":"e8bfc8a9d983","markDefs":[]}],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:44Z","hjemler":["2","9","11"],"begrunnelsetype":"INNVILGET"},{"periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","hjemler":["2","9","11"],"visningsnavn":"22. Vurdering søker giftet seg","vedtakResultat":"REDUKSJON","_id":"28b7c02f-5fbd-4764-80c0-379356b83973","navnISystem":"Vurdering søker giftet seg","tema":"FELLES","nynorsk":[{"_key":"ffa4db7cdf98","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi vi har kome fram til at du er gift frå ","_key":"eb3133057747"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"e01885fc13a4"},{"_type":"span","marks":[],"text":". Barnetrygda er endra frå månaden etter at du gifta deg.","_key":"e0bcd804b450"}],"_type":"block","style":"normal"}],"_rev":"BtltdVb0HP4g4WJfnr4V7o","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:44Z","_createdAt":"2021-10-22T10:32:36Z","mappe":["REDUKSJON"],"bokmaal":[{"_key":"6ad810be9f38","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi vi har kommet fram til at du er gift fra ","_key":"bae9e02ceb30"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"1143259faaec"},{"text":". Barnetrygden endres fra måneden etter at du giftet deg.","_key":"13c87c691d14","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"apiNavn":"reduksjonVurderingSokerGiftetSeg","_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"]},{"apiNavn":"innvilgetVurderingSokerOgBarnPliktigMedlem","hjemlerFolketrygdloven":["2-5"],"_updatedAt":"2023-09-25T10:15:44Z","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","_createdAt":"2021-09-24T11:28:32Z","valgbarhet":"STANDARD","_id":"295eafb4-a5eb-4ec8-bd0f-e59c8780f8e5","navnISystem":"Vurdering søker og barn pliktig medlem","hjemler":["4","5"],"visningsnavn":"24. Vurdering søker og barn pliktig medlem","vilkaar":["BOSATT_I_RIKET"],"rolle":["SOKER","BARN"],"bosattIRiketTriggere":["MEDLEMSKAP"],"nynorsk":[{"_key":"0560da1adec3","markDefs":[],"children":[{"_key":"66ca62e65cc0","_type":"span","marks":[],"text":"Du får barnetrygd "},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"481a72fe13cc"},{"_type":"span","marks":[],"text":" under opphaldet i utlandet. Vi har komme fram til at ","_key":"9ce69545a26c"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"016ce4a9162f"},{"_type":"span","marks":[],"text":" fyller vilkåra for å bli pliktig medlem i folketrygda frå ","_key":"74e3de62968b"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"1a8ac11adf06"},{"text":". Dette gjev rett til barnetrygd frå Noreg.","_key":"d75ed6ad603e","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"NASJONAL","mappe":["INNVILGET"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"b6d62a8a5010"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"d518b0ea77ca"},{"marks":[],"text":" under oppholdet i utlandet. Vi har kommet fram til at ","_key":"a2ac4a47b18e","_type":"span"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"fd72da834da2"},{"_type":"span","marks":[],"text":" fyller vilkårene for å bli pliktig medlem i folketrygden fra ","_key":"54001ff6fe80"},{"_type":"flettefelt","_key":"112c1220f2ee","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_type":"span","marks":[],"text":". Dette gir rett til barnetrygd fra Norge.","_key":"4f9175f31397"}],"_type":"block","style":"normal","_key":"8ce94262b2cf"}]},{"begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","bosattIRiketTriggere":["MEDLEMSKAP"],"nynorsk":[{"children":[{"text":"Du får barnetrygd fordi du fortsatt er medlem av folketrygda.","_key":"788dcc61f399","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"8d2d3f16d99b","markDefs":[]}],"visningsnavn":"7. Medlem i folketrygden","_rev":"h26rAhFEYSUtDGXJE0sieu","periodeType":"FORTSATT_INNVILGET","mappe":["FORTSATT_INNVILGET"],"hjemler":["2","4","11"],"vedtakResultat":"INGEN_ENDRING","vilkaar":["BOSATT_I_RIKET"],"_createdAt":"2021-08-30T13:07:39Z","valgbarhet":"STANDARD","navnISystem":"Medlem i folketrygden","_type":"begrunnelse","rolle":["SOKER"],"_id":"2986e91b-b447-4297-a72a-a5b169d44565","_updatedAt":"2023-09-25T10:21:55Z","bokmaal":[{"style":"normal","_key":"725a42c0223b","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du fortsatt er medlem av folketrygden.","_key":"107c73092642"}],"_type":"block"}],"apiNavn":"fortsattInnvilgetMedlemIFolketrygden"},{"_createdAt":"2022-03-09T08:47:10Z","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:44Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du har tidligere fått utbetalt for mye barnetrygd. Du har nå rett til etterbetaling av barnetrygd. Vi kommer til å redusere etterbetalingen med det du har fått for mye.","_key":"cadf4ac42cc40"}],"_type":"block","style":"normal","_key":"395539d9f456"}],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du har tidligare fått utbetalt for mykje barnetrygd. Du har no rett til etterbetaling av barnetrygd. Vi kjem til å redusere etterbetalinga med det du har fått for mykje.","_key":"0a36bc3c11ad0"}],"_type":"block","style":"normal","_key":"2f5321781391","markDefs":[]}],"rolle":["SOKER","BARN"],"begrunnelsetype":"INNVILGET","navnISystem":"Erklæring om motregning","vilkaar":["UNDER_18_ÅR","BOR_MED_SOKER","GIFT_PARTNERSKAP","BOSATT_I_RIKET","LOVLIG_OPPHOLD","UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","tema":"NASJONAL","_id":"29be055e-0f7e-407a-b396-2caff0451541","apiNavn":"innvilgetErklaeringOmMotregning","behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","valgbarhet":"TILLEGGSTEKST","visningsnavn":"69. Erklæring om motregning"},{"_rev":"h26rAhFEYSUtDGXJE0v6VP","periodeType":"FORTSATT_INNVILGET","rolle":["SOKER","BARN"],"begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","_createdAt":"2022-11-04T09:06:57Z","_updatedAt":"2023-09-25T10:38:43Z","fagsakType":"INSTITUSJON","_type":"begrunnelse","behandlingstema":"NASJONAL_INSTITUSJON","vedtakResultat":"INGEN_ENDRING","vilkaar":["LOVLIG_OPPHOLD"],"nynorsk":[{"_key":"d357f774dc3c","markDefs":[],"children":[{"marks":[],"text":"De får barnetrygd fordi barnet fortsatt har opphaldsløyve.","_key":"abc29e875f6c0","_type":"span"}],"_type":"block","style":"normal"}],"valgbarhet":"SAKSPESIFIKK","_id":"2a43156b-d81f-4d83-b8a0-361fe33746cc","apiNavn":"fortsattInnvilgetOppholdstillatelseInstitusjon","visningsnavn":"2. Oppholdstillatelse institusjon","mappe":["INSTITUSJON","FORTSATT_INNVILGET"],"bokmaal":[{"style":"normal","_key":"4ed75ec3ee9b","markDefs":[],"children":[{"marks":[],"text":"Dere får barnetrygd fordi barnet fortsatt har oppholdstillatelse.","_key":"ba6cd5a3594a0","_type":"span"}],"_type":"block"}],"navnISystem":"Oppholdstillatelse institusjon","hjemler":["2","4"]},{"hjemler":["2","9","11"],"_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"","_key":"2c57be58e984","_type":"span"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"1b51b65d9586","skalHaStorForbokstav":false},{"marks":[],"text":" utvida barnetrygd sjølv om du fortsatt er sambuar fordi du bur aleine med ","_key":"e111e9c46e11","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"eeb798d3e1d5","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Sambuaren din er i tvungent psykisk helsevern frå ","_key":"60ef539a49c9"},{"_key":"4690b7cd2e0b","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":". ","_key":"ebc892e43252"},{"_key":"c88cf74bd84b","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at du vart aleine med barnet/barna.","_key":"b0d008058192"}],"_type":"block","style":"normal","_key":"92c93008b12c"}],"_createdAt":"2021-10-25T07:22:54Z","valgbarhet":"STANDARD","behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr4V7o","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","_id":"2ae8748a-9670-4f72-b8be-4f6fb836a5e1","navnISystem":"Tvungent psykisk helsevern samboer","visningsnavn":"49. Tvungent psykisk helsevern samboer","begrunnelsetype":"INNVILGET","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:44Z","apiNavn":"innvilgetTvungentPsykiskHelsevernSamboer","tema":"FELLES","bokmaal":[{"style":"normal","_key":"4d01e1abb851","markDefs":[],"children":[{"marks":[],"text":"","_key":"7f18f19c5a93","_type":"span"},{"_type":"valgfeltV2","_key":"a5bfaf248a15","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"}},{"_type":"span","marks":[],"text":" utvidet barnetrygd selv om du fortsatt er samboer fordi du bor alene med ","_key":"bdd5abffadfb"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"8414784a93d3"},{"_type":"span","marks":[],"text":". Samboeren din er i tvungent psykisk helsevern fra ","_key":"a58832a10094"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"c9c61f4f3916"},{"text":". ","_key":"752bdc7b6497","_type":"span","marks":[]},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"6f78f2e34bae","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at du ble alene med barnet/barna.","_key":"f36e7b315d46"}],"_type":"block"}]},{"navnISystem":"Flere barn er døde","_createdAt":"2021-08-30T10:14:29Z","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","tema":"FELLES","valgbarhet":"AUTOMATISK","_id":"2ccfcf4a-1217-44cf-a0be-e40cfd63ad56","hjemler":["2","11"],"visningsnavn":"5b. Flere barn er døde","behandlingstema":"NASJONAL","_updatedAt":"2023-09-25T10:31:32Z","_type":"begrunnelse","begrunnelsetype":"OPPHØR","ovrigeTriggere":["BARN_DØD"],"mappe":["OPPHØR"],"bokmaal":[{"_key":"f54f70234998","markDefs":[],"children":[{"_key":"38241c150c7c","_type":"span","marks":[],"text":"Barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"dce1af524cb0"},{"_type":"span","marks":[],"text":" fordi barna døde. Barnetrygden opphører fra måneden etter at barna døde.","_key":"15df4834411a"}],"_type":"block","style":"normal"}],"apiNavn":"opphorFlereBarnErDode","_rev":"h26rAhFEYSUtDGXJE0t92s","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"f8698e408424"},{"_type":"flettefelt","_key":"e62634f8ea32","flettefelt":"barnasFodselsdatoer"},{"text":" fordi barna døydde. Barnetrygda opphøyrer frå månaden etter at barna døydde.","_key":"fd8a7e1027d5","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"cb71bb4bbcf5","markDefs":[]}]},{"navnISystem":"Sekundær delt bosted full utbetaling før søknad","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du har allereie fått heile barnetrygda Noreg utbetalar for barn fødd ","_key":"95f730f563fb0"},{"_type":"eosFlettefelt","_key":"00579bdf3ddb","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" i denne perioden. Det er opp til deg og den andre forelderen å bli einige om fordelinga av barnetrygda som er utbetalt.","_key":"0dd706901d26"}],"_type":"block","style":"normal","_key":"c56af9aa8e32","markDefs":[]}],"endringsaarsaker":["DELT_BOSTED"],"_createdAt":"2022-10-07T13:51:44Z","_updatedAt":"2023-09-25T10:15:44Z","visningsnavn":"13. Sekundær delt bosted full utbetaling før søknad","_id":"2debe79a-3eae-4659-b7cf-ff8617794222","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du har allerede fått hele barnetrygden Norge utbetaler for barn født ","_key":"4811ac5d5e480"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"b879f8530bc1"},{"_type":"span","marks":[],"text":" i denne perioden. Det er opp til deg og den andre forelderen å bli enige om hvordan dere vil fordele barnetrygden som er utbetalt.","_key":"7b4343132e54"}],"_type":"block","style":"normal","_key":"080d467c8822"}],"apiNavn":"endretUtbetalingSekundaerDeltBostedFullUtbetalingFoerSoeknad","_rev":"BtltdVb0HP4g4WJfnr4V7o","mappe":["ENDRET_UTBETALINGSPERIODE"],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","tema":"FELLES","endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"SKAL_UTBETALES","valgbarhet":"STANDARD"},{"_type":"begrunnelse","periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","_id":"2ed07e10-3d34-4eda-8cd5-207c3f7bfbd1","apiNavn":"avslagIkkeEgenHusholdningGift","_rev":"FuD004taptHFqBZyEy7bkO","visningsnavn":"51. Ikke egen husholdning gift","tema":"FELLES","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi vi har kome fram til at du ikkje har eiga hushaldning og derfor fortsatt er gift.","_key":"69f273c59aab"}],"_type":"block","style":"normal","_key":"ecb4a4e7cd88","markDefs":[]}],"navnISystem":"Ikke egen husholdning gift","hjemler":["9"],"_createdAt":"2021-10-22T09:25:08Z","valgbarhet":"STANDARD","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:30:25Z","bokmaal":[{"style":"normal","_key":"a53c7c697a77","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi vi har kommet fram til at du ikke har egen husholdning og derfor fortsatt er gift.","_key":"b44520fe5108"}],"_type":"block"}],"vedtakResultat":"IKKE_INNVILGET","vilkaar":["UTVIDET_BARNETRYGD"]},{"hjemler":["4","5"],"hjemlerFolketrygdloven":["2-5"],"_rev":"FuD004taptHFqBZyEy7Hof","_type":"begrunnelse","periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","valgbarhet":"STANDARD","_id":"2ef5fcba-9fee-4253-a193-038ee6612ae7","navnISystem":"Ikke pliktig medlem","apiNavn":"avslagIkkePliktigMedlem","_updatedAt":"2023-09-25T10:26:41Z","rolle":["SOKER","BARN"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"f53702a60d56"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"756355887bcf"},{"marks":[],"text":" fordi ","_key":"705e3aaa2113","_type":"span"},{"_type":"valgReferanse","_key":"8f98730158d4","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},{"marks":[],"text":"ikkje er pliktig medlem i folketrygda","_key":"ae5557545f37","_type":"span"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"5ed91db6eb11"},{"_type":"span","marks":[],"text":".","_key":"76c4465233ec"}],"_type":"block","style":"normal","_key":"804e92396c22"}],"mappe":["AVSLAG"],"visningsnavn":"17. Ikke pliktig medlem","behandlingstema":"NASJONAL","tema":"NASJONAL","bosattIRiketTriggere":["MEDLEMSKAP"],"_createdAt":"2021-09-29T10:52:07Z","bokmaal":[{"style":"normal","_key":"f1ebdd7a5fd8","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"fe1bf997dbe9"},{"_type":"flettefelt","_key":"5cb47b0f955b","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi ","_key":"965ad4280ebd"},{"_key":"6704444b630e","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse"},{"_type":"span","marks":[],"text":" ikke er pliktig medlem i folketrygden","_key":"6c9310f7fd6c"},{"_type":"valgReferanse","_key":"d0572602c78d","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"_type":"span","marks":[],"text":". ","_key":"8b687a1519a4"}],"_type":"block"}],"vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOSATT_I_RIKET"]},{"hjemler":["2"],"borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"periodeType":"INGEN_UTBETALING","tema":"NASJONAL","valgbarhet":"STANDARD","_id":"2f56712e-c5d5-4d43-a95e-d6e2005958c4","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:26:34Z","vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"AVSLAG","_createdAt":"2021-08-31T08:28:39Z","navnISystem":"Ektefelle eller samboers særkullsbarn","apiNavn":"avslagSaerkullsbarn","visningsnavn":"15. Ektefelle eller samboers særkullsbarn","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"nynorsk":[{"_key":"c25c23623128","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"c349a1a7d99f"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"7005b83b1bd2"},{"text":" fordi ","_key":"9bc3a20d55db","_type":"span","marks":[]},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"6234ccaa0ad9"},{"text":" bur saman med ein av foreldra sine. I slike tilfelle er det forelderen til ","_key":"3848c4c5c54a","_type":"span","marks":[]},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"ae1f1307baef"},{"_type":"span","marks":[],"text":" som har rett til barnetrygda.","_key":"3569157dbc6b"}],"_type":"block","style":"normal"}],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"54d526bbf8b6"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"18d757b4d045"},{"_type":"span","marks":[],"text":" fordi ","_key":"ee6052747af6"},{"_key":"df2a18d7896b","_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse"},{"_type":"span","marks":[],"text":" bor sammen med en av foreldrene sine. I slike tilfeller er det forelderen til ","_key":"d50ac7bc2bc5"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"f72456b90cb2"},{"_type":"span","marks":[],"text":" som har rett til barnetrygden.","_key":"2738554772dc"}],"_type":"block","style":"normal","_key":"55bce4c1f74f","markDefs":[]}],"_rev":"FuD004taptHFqBZyEy7GGN"},{"tema":"FELLES","mappe":["INNVILGET"],"hjemler":["2","9","11"],"_type":"begrunnelse","periodeType":"UTBETALING","valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"","_key":"2b3194ed250b","_type":"span"},{"_key":"7cdd118531cb","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"marks":[],"text":" utvidet barnetrygd selv om du fortsatt er gift fordi du bor alene med ","_key":"2f491df10d00","_type":"span"},{"_key":"c2f3f87a5c4c","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". Ektefellen din er i fengsel i seks måneder eller mer fra ","_key":"34776f66d405"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"2330515c107e"},{"text":". ","_key":"b294c30eb026","_type":"span","marks":[]},{"_type":"valgfeltV2","_key":"a93f16b4bb10","skalHaStorForbokstav":true,"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"}},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at oppholdet i fengselet startet.","_key":"76e1c9e056f5"}],"_type":"block","style":"normal","_key":"0b3cab5a1011"}],"navnISystem":"Fengsel gift","vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"INNVILGET","vedtakResultat":"INNVILGET_ELLER_ØKNING","nynorsk":[{"_key":"a17c2bb83d25","markDefs":[],"children":[{"_key":"134c2928e90c","_type":"span","marks":[],"text":""},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"5552e470082e","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvida barnetrygd sjølv om du fortsatt er gift fordi du bur aleine med ","_key":"2407dfa7f3e3"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"7f4fa836c0c4","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Ektefellen din er i fengsel i seks månader eller meir frå ","_key":"820a32dfbecb"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"0d9a1dcc39a9"},{"_type":"span","marks":[],"text":". ","_key":"c528114d728c"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"d1bf25cb01ab","skalHaStorForbokstav":true},{"text":" utvida barnetrygd frå månaden etter at opphaldet i fengselet starta.","_key":"957a43e5a8fa","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"_createdAt":"2021-10-25T07:04:49Z","_updatedAt":"2023-09-25T10:15:44Z","visningsnavn":"42. Fengsel gift","behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr4V7o","apiNavn":"innvilgetFengselGift","_id":"2f7bc671-f863-4583-8de6-e0fbd2004af8"},{"visningsnavn":"33. Fortsatt rettsavgjørelse om delt bosted","vilkaar":["BOR_MED_SOKER"],"_createdAt":"2021-10-26T10:21:32Z","_id":"300694bc-775c-4006-b20e-747e4e97adfa","navnISystem":"Fortsatt rettsavgjørelse om delt bosted","_rev":"h26rAhFEYSUtDGXJE0snz9","_type":"begrunnelse","nynorsk":[{"_type":"block","style":"normal","_key":"e760e8e38a86","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får delt barnetrygd fordi retten har bestemt at ","_key":"162708609f7e"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"ff2a383fe872","skalHaStorForbokstav":false},{"_key":"ad2845881ae0","_type":"span","marks":[],"text":" fortsatt skal ha delt bustad."}]}],"mappe":["FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:23:28Z","hjemler":["2"],"valgbarhet":"STANDARD","apiNavn":"fortsattInnvilgetFortsattRettsavgjorelseOmDeltBosted","vedtakResultat":"INGEN_ENDRING","borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"FORTSATT_INNVILGET","begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får delt barnetrygd fordi retten har bestemt at ","_key":"52ebd3d4cf0b"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"4e75b2604f75","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" fortsatt skal ha delt bosted.","_key":"f5731f2749ad"}],"_type":"block","style":"normal","_key":"c4eefcfbadc5"}]},{"visningsnavn":"5A. Beredskapshjem","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","mappe":["INNVILGET"],"apiNavn":"innvilgetBeredskapshjem","_type":"begrunnelse","tema":"NASJONAL","navnISystem":"Beredskapshjem","behandlingstema":"NASJONAL","borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"periodeType":"UTBETALING","_createdAt":"2021-10-14T15:00:19Z","_id":"30094136-639c-4719-8f3e-8c94d55c9edc","_updatedAt":"2023-09-25T10:15:44Z","hjemler":["2","4","11"],"vilkaar":["BOR_MED_SOKER"],"nynorsk":[{"_type":"block","style":"normal","_key":"c57c075fef58","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"0af26fb3523a"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"9a8350a0ec1d"},{"_key":"4a63c78ae7c6","_type":"span","marks":[],"text":" fordi vi har kome fram til at "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"7f2d2463e898","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" bur fast hos deg frå ","_key":"958e865dd319"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"d14ff6fc0d9b"},{"_type":"span","marks":[],"text":". Du får barnetrygd frå månaden etter det vart bestemt at ","_key":"9f641471c6e0"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"31e50f406ca7","skalHaStorForbokstav":false},{"marks":[],"text":" skal bu fast hos deg.","_key":"732a275a3950","_type":"span"}]}],"bokmaal":[{"children":[{"marks":[],"text":"Du får barnetrygd for barn født ","_key":"f96867d552ff","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"ffd0f1dd963c"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at ","_key":"9eaeacea35b8"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"194199fc030f","skalHaStorForbokstav":false},{"_type":"span","marks":["em"],"text":" ","_key":"291e8f1c626a"},{"_type":"span","marks":[],"text":"bor fast hos deg fra ","_key":"7bbff824816f"},{"_key":"8500902b0b57","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":". Du får barnetrygd fra måneden etter at det ble bestemt at ","_key":"f5cce3d006e1"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"ba80c43fbec8","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" skal bo fast hos deg.","_key":"fac0dd8c7ba6"}],"_type":"block","style":"normal","_key":"a886d2de824a","markDefs":[]}],"_rev":"BtltdVb0HP4g4WJfnr4V7o"},{"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du har allereie fått ","_key":"43ab281f6581"},{"flettefelt":"belop","_type":"flettefelt","_key":"0b1ad1c3c9b5"},{"_type":"span","marks":[],"text":" kroner måneden i denne perioden. Vi krev ikkje tilbake desse pengane. Du og den andre forelderen må sjølv bli einige om delinga av barnetrygda som allereie er utbetalt. Du får den utvida delen av barnetrygda etterbetalt.","_key":"83499679f411"}],"_type":"block","style":"normal","_key":"8ad16cdd414e"}],"_id":"309ff211-50fc-466d-a086-7b014067b0b0","visningsnavn":"4. Delt bosted - kun etterbetaling utvidet","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"SKAL_UTBETALES","mappe":["ENDRET_UTBETALINGSPERIODE"],"bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Du har allerede fått ","_key":"2167bd596e00","_type":"span"},{"flettefelt":"belop","_type":"flettefelt","_key":"ab1000cc7206"},{"_type":"span","marks":[],"text":" kroner i måneden i denne perioden. Vi krever ikke tilbake disse pengene. Du og den andre forelderen må selv bli enige om fordelingen av barnetrygden som allerede er utbetalt. Du får den utvidede delen av barnetrygden etterbetalt. ","_key":"1b869d0a5b36"}],"_type":"block","style":"normal","_key":"22511af52247"}],"_rev":"BtltdVb0HP4g4WJfnr4V7o","tema":"FELLES","endringsaarsaker":["DELT_BOSTED"],"_updatedAt":"2023-09-25T10:15:44Z","periodeType":"UTBETALING","_createdAt":"2021-10-18T12:19:39Z","valgbarhet":"STANDARD","navnISystem":"Delt bosted - Kun etterbetaling utvidet","apiNavn":"endretUtbetalingDeltBostedKunEtterbetalingUtvidet","hjemler":["2","12"],"vilkaar":["UTVIDET_BARNETRYGD"]},{"vilkaar":["BOR_MED_SOKER"],"tema":"NASJONAL","_updatedAt":"2023-09-25T10:31:53Z","mappe":["OPPHØR"],"visningsnavn":"11. Uenighet om opphør av avtale om delt bosted","_type":"begrunnelse","valgbarhet":"STANDARD","borMedSokerTriggere":["DELT_BOSTED"],"begrunnelsetype":"OPPHØR","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"75180534908a"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"80b8d7e530b7"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at avtale om delt bosted for ","_key":"d3e0b155cadd"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"11ae4be51e8c","skalHaStorForbokstav":false},{"_key":"128247a2e417","_type":"span","marks":[],"text":" ikke lenger følges."}],"_type":"block","style":"normal","_key":"8231f68ca4d5"},{"_type":"block","style":"normal","_key":"ee1a993916e1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du og den andre forelderen er uenige om avtalen om delt bosted. Ved uenighet mellom foreldrene om avtalen om delt bosted, kan barnetrygden opphøres fra måneden etter at vi fikk søknad om full barnetrygd.","_key":"7d3bb125485e"}]}],"navnISystem":"Uenighet om opphør av avtale om delt bosted","apiNavn":"opphorDeltBostedOpphortUenighet","vedtakResultat":"IKKE_INNVILGET","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"a4411b2f94e9"},{"_type":"flettefelt","_key":"1ace2d692e80","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at avtalen om delt bustad for ","_key":"72d29ca5f962"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"843a802d1a63","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikkje lenger vert følgd.","_key":"e7b66ee5a721"}],"_type":"block","style":"normal","_key":"325fdf621203"},{"_key":"f3643b93f5d7","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du og den andre forelderen er usamde om avtalen om delt bustad. Når de er usamde om avtalen om delt bustad, kan vi opphøyre barnetrygda til deg frå og med månaden etter at vi fekk søknad om full barnetrygd.","_key":"4f84f05e767c"}],"_type":"block","style":"normal"}],"_createdAt":"2021-08-30T11:30:55Z","_id":"30d91bab-c31a-4d2b-baf4-d8ba71375b69","hjemler":["2","11"],"_rev":"FuD004taptHFqBZyEy7owr","periodeType":"INGEN_UTBETALING"},{"navnISystem":"Vurdering samboer mer enn 12 måneder","_rev":"BtltdVb0HP4g4WJfnr4V7o","nynorsk":[{"markDefs":[],"children":[{"_key":"816a93a79ad6","_type":"span","marks":[],"text":"Barnetrygda er endra fordi vi har kome fram til at du og sambuaren din har budd saman i meir enn tolv månader frå "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"480f6ba7572a"},{"marks":[],"text":".","_key":"e3e387c8406e","_type":"span"}],"_type":"block","style":"normal","_key":"049c210c3e7d"}],"_createdAt":"2021-10-22T11:37:27Z","bokmaal":[{"markDefs":[],"children":[{"text":"Barnetrygden endres fordi vi har kommet fram til at du og samboeren din har bodd sammen i mer enn tolv måneder fra ","_key":"0137ead38c71","_type":"span","marks":[]},{"_key":"ffc65a2380dd","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"marks":[],"text":".","_key":"a9d65342c02b","_type":"span"}],"_type":"block","style":"normal","_key":"a3c19133b5b0"}],"apiNavn":"reduksjonVurderingSamboerMerEnn12Maaneder","visningsnavn":"25. Vurdering samboer mer enn 12 måneder","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:44Z","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","tema":"FELLES","hjemler":["2","9","11"],"_type":"begrunnelse","vedtakResultat":"REDUKSJON","begrunnelsetype":"REDUKSJON","_id":"311763a1-5d8d-4af5-bbe5-9452b2cf1993"},{"visningsnavn":"33. Ikke oppholdstillatelse","behandlingstema":"NASJONAL","vilkaar":["LOVLIG_OPPHOLD"],"rolle":["SOKER","BARN"],"nynorsk":[{"markDefs":[],"children":[{"_key":"8d5b8bc403c1","_type":"span","marks":[],"text":"Barnetrygd for barn fødd "},{"_type":"flettefelt","_key":"298ded22c9c1","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi ","_key":"39b1c9e1e114"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"39d7126f1956","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikkje hadde opphaldsløyve i Noreg. ","_key":"b2d059951ea6"}],"_type":"block","style":"normal","_key":"40a69b86fc18"}],"navnISystem":"Ikke oppholdstillatelse","valgbarhet":"STANDARD","_id":"3180733c-2ea5-43f2-acbe-713cf066da61","bokmaal":[{"style":"normal","_key":"f346d8f16115","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"813c6f2f3b19"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"d46aa84d7080"},{"_type":"span","marks":[],"text":" fordi ","_key":"7205d36bae70"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"78949a7d567b","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikke hadde oppholdstillatelse i Norge. ","_key":"d10db4f8271f"}],"_type":"block"}],"periodeType":"INGEN_UTBETALING","_rev":"FuD004taptHFqBZyEy8ZHW","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","_createdAt":"2021-11-05T10:24:53Z","_updatedAt":"2023-09-25T10:35:45Z","ovrigeTriggere":["GJELDER_FØRSTE_PERIODE","GJELDER_FRA_INNVILGELSESTIDSPUNKT"],"hjemler":["2","4"],"begrunnelsetype":"OPPHØR","tema":"NASJONAL","mappe":["OPPHØR"],"apiNavn":"opphorIkkeOppholdstillatelse"},{"hjemler":["2"],"_rev":"FuD004taptHFqBZyEy7igU","begrunnelsetype":"AVSLAG","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for deg sjølv. Det er den som har omsorga for deg som kan få barnetrygda.","_key":"f023919f84d60"}],"_type":"block","style":"normal","_key":"3549682b0c04"}],"_createdAt":"2022-09-09T11:43:56Z","_id":"31ca988f-bf60-41ab-a1d7-f6679941f2f1","navnISystem":"Søker barnetrygd for seg selv","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","tema":"NASJONAL","valgbarhet":"STANDARD","_type":"begrunnelse","mappe":["AVSLAG"],"apiNavn":"avslagSokerBarnetrygdForSegSelv","behandlingstema":"NASJONAL","_updatedAt":"2023-09-25T10:31:09Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for deg selv. Det er den som har omsorgen for deg som kan få barnetrygden.","_key":"4cb6a0e0fc930"}],"_type":"block","style":"normal","_key":"44342d67c3b0","markDefs":[]}],"visningsnavn":"66. Søker barnetrygd for seg selv"},{"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi vi har kommet fram til at du fortsatt har fast omsorg for ","_key":"8b2e8f614497"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"41a61f27eb56"},{"_type":"span","marks":[],"text":".","_key":"7392fc6ec86d"}],"_type":"block","style":"normal","_key":"e5a45c85307e","markDefs":[]}],"vilkaar":["BOR_MED_SOKER"],"_createdAt":"2021-08-30T13:03:09Z","navnISystem":"Fortsatt fast omsorg for barn","hjemler":["2","4","11"],"visningsnavn":"4. Fortsatt fast omsorg for barn","periodeType":"FORTSATT_INNVILGET","begrunnelsetype":"FORTSATT_INNVILGET","valgbarhet":"STANDARD","_id":"3363a993-3ebc-4da9-baa7-036140de8751","mappe":["FORTSATT_INNVILGET"],"apiNavn":"fortsattInnvilgetFastOmsorg","_rev":"h26rAhFEYSUtDGXJE0siNd","_type":"begrunnelse","_updatedAt":"2023-09-25T10:21:44Z","vedtakResultat":"INGEN_ENDRING","tema":"NASJONAL","nynorsk":[{"_key":"3871abaf9342","markDefs":[],"children":[{"_key":"174065666c4b","_type":"span","marks":[],"text":"Du får barnetrygd fordi vi har kome fram til at du fortsatt har fast omsorg for "},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"cf5628399990"},{"_type":"span","marks":[],"text":".","_key":"ceb5b7a4be96"}],"_type":"block","style":"normal"}]},{"periodeType":"INGEN_UTBETALING","begrunnelsetype":"OPPHØR","nynorsk":[{"style":"normal","_key":"d99aa76a51d4","markDefs":[],"children":[{"marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"2025c168d61f","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"a3e03781d020"},{"_key":"4a79f616db1d","_type":"span","marks":[],"text":" fordi vi har kome fram til at du budde saman med den andre forelderen til "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"404cacfa8307","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":".","_key":"04234f01acf8"}],"_type":"block"}],"valgbarhet":"STANDARD","mappe":["OPPHØR"],"ovrigeTriggere":["GJELDER_FØRSTE_PERIODE"],"apiNavn":"opphorVurderingForeldreneBoddeSammen","_type":"begrunnelse","borMedSokerTriggere":["DELT_BOSTED"],"hjemler":["2"],"visningsnavn":"38. Vurdering foreldrene bodde sammen","_createdAt":"2021-11-05T11:24:54Z","_id":"33b14d4f-7122-4dd8-82ba-493ecffb32b8","_updatedAt":"2023-09-25T10:36:02Z","navnISystem":"Vurdering foreldrene bodde sammen","vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOR_MED_SOKER"],"tema":"NASJONAL","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn født ","_key":"b5a34ffc1a60"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"bdd849c4b2ce"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at du bodde sammen med den andre forelderen til ","_key":"30c20b05e7e9"},{"_key":"ec45613f0b7f","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2"},{"text":".","_key":"5e146aa20000","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"db3bf96e6eb2","markDefs":[]}],"_rev":"h26rAhFEYSUtDGXJE0v187"},{"_rev":"BtltdVb0HP4g4WJfnr4V7o","resultat":"REDUKSJON","vilkaar":["BOR_MED_SOKER"],"valgbarhet":"STANDARD","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:44Z","navnISystem":"Avtale fast bosted","behandlingstema":"NASJONAL","borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"UTBETALING","tema":"NASJONAL","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi vi har fått en avtale som sier at barn født ","_key":"8fe75af2fda7"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"db5d4ce08621"},{"_type":"span","marks":[],"text":" skal bo fast hos den andre forelderen fra ","_key":"22a3bbc89625"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"e8d0e55eb3b6"},{"_type":"span","marks":[],"text":".","_key":"7e86df24486f"}],"_type":"block","style":"normal","_key":"370e0181c0ad","markDefs":[]}],"hjemler":["2","11"],"visningsnavn":"42. Avtale fast bosted","_type":"begrunnelse","vedtakResultat":"REDUKSJON","begrunnelsetype":"REDUKSJON","_createdAt":"2021-10-26T10:12:32Z","_id":"33be5760-52fa-4c7c-9eb7-69b9b0a2190f","apiNavn":"reduksjonAvtaleFastBosted","nynorsk":[{"style":"normal","_key":"41ece564be3b","markDefs":[],"children":[{"_key":"1cd1fd910d55","_type":"span","marks":[],"text":"Barnetrygda er endra fordi vi har fått ein avtale som seier at barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"3ddf19ddd23d"},{"_type":"span","marks":[],"text":" skal bu fast hos den andre forelderen frå ","_key":"fbd45fed9770"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"033e6f311ee8"},{"marks":[],"text":".","_key":"b7da9ee32722","_type":"span"}],"_type":"block"}]},{"apiNavn":"opphorBarnBorIkkeMedSoker","hjemler":["2","11"],"begrunnelsetype":"OPPHØR","nynorsk":[{"_key":"1c7d9e2c446d","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"349936b45f9d"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"6a2aba88b90d"},{"_type":"span","marks":[],"text":" fordi ","_key":"31110e21d972"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"1aef3b9b22ef"},{"_type":"span","marks":[],"text":" ikkje bur hos deg ","_key":"4f4c69524888"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"468238d9b124"},{"_key":"fcc7ce7dc751","_type":"span","marks":[],"text":"."}],"_type":"block","style":"normal"}],"_createdAt":"2021-08-30T10:02:50Z","mappe":["OPPHØR"],"_updatedAt":"2023-09-25T10:31:24Z","visningsnavn":"1. Barn bor ikke med søker","_type":"begrunnelse","resultat":"IKKE_INNVILGET","vedtakResultat":"IKKE_INNVILGET","bokmaal":[{"style":"normal","_key":"b7ca0256fb6d","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"527d82d0f727"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"7ed48133b78e"},{"text":" fordi ","_key":"1a1ea64112cf","_type":"span","marks":[]},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"3ba1b7ae68b4"},{"text":" ikke bor hos deg ","_key":"b5ad869e42da","_type":"span","marks":[]},{"_key":"deb7eb116b52","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"text":".","_key":"daf6759ecacd","_type":"span","marks":[]}],"_type":"block"}],"navnISystem":"Barn bor ikke med søker","_rev":"BtltdVb0HP4g4WJfnr5PxJ","tema":"NASJONAL","valgbarhet":"STANDARD","behandlingstema":"NASJONAL","vilkaar":["BOR_MED_SOKER"],"periodeType":"INGEN_UTBETALING","_id":"34e72655-0733-4486-9d22-2e2d1babc0e4"},{"navnISystem":"Tilleggsbegrunnelse refusjon uavklart","periodeType":"UTBETALING","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Myndighetene i (SØKERS OPPRINNELIGE BOSTEDSLAND)/ (ANNEN FORELDRES AKTIVITETSLAND) har allerede utbetalt (UTBETALT FRA SØKERS OPPRINNELIGE BOSTEDSLAND)/(UTBETALT FRA ANNEN FORELDERS AKTIVITETSLAND) (VALUTA) i barnetrygd fra (DATO) til (DATO). De ber derfor om at Norge ikke utbetaler disse pengene, fordi (SØKERS OPPRINNELIGE BOSTEDSLAND)/ (ANNEN FORELDRES AKTIVITETSLAND) vil kreve dem tilbake. Derfor blir (UTBETALT FRA SØKERS OPPRINNELIGE BOSTEDSLAND OMREGNET TIL NORSKE KRONER)/(UTBETALT FRA ANNEN FORELDERS AKTIVITETSLAND OMREGNET TIL NORSKE KRONER) kroner av etterbetalingen ikke utbetalt nå. Hvis (SØKERS OPPRINNELIGE BOSTEDSLAND)/(ANNEN FORELDRES AKTIVITETSLAND) ikke krever alle pengene tilbake blir resten utbetalt til deg senere.","_key":"28266bdf4aa70"}],"_type":"block","style":"normal","_key":"701ec680679a","markDefs":[]}],"_createdAt":"2022-05-23T13:01:58Z","_id":"3753db5e-af3c-4ba0-a011-d738b0b3e6eb","apiNavn":"innvilgetTilleggsbegrunnelseRefusjonUavklart","visningsnavn":"18. Tilleggsbegrunnelse refusjon uavklart","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","begrunnelsetype":"INNVILGET","nynorsk":[{"_type":"block","style":"normal","_key":"180689914976","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Myndighetene i (SØKERS OPPRINNELIGE BOSTEDSLAND)/ (ANNEN FORELDRES AKTIVITETSLAND) har allerereie utbetalt (UTBETALT FRA SØKERS OPPRINNELIGE BOSTEDSLAND)/(UTBETALT FRA ANNEN FORELDERS AKTIVITETSLAND) (VALUTA) i barnetrygd frå (DATO) til (DATO). Dei ber derfor om at Noreg ikkje utbetalar desse pengane, fordi (SØKERS OPPRINNELIGE BOSTEDSLAND)/ (ANNEN FORELDRES AKTIVITETSLAND) vil krevje dei tilbake. Derfor blir (UTBETALT FRA SØKERS OPPRINNELIGE BOSTEDSLAND OMREGNET TIL NORSKE KRONER)/(UTBETALT FRA ANNEN FORELDERS AKTIVITETSLAND OMREGNET TIL NORSKE KRONER) kroner av etterbetalinga ikkje utbetalt no. Dersom (SØKERS OPPRINNELIGE BOSTEDSLAND)/(ANNEN FORELDRES AKTIVITETSLAND) ikkje krev alle pengane tilbake blir resten utbetalt til deg seinare.","_key":"db292301fde30"}]}],"mappe":["EØS","INNVILGET"],"_updatedAt":"2023-09-25T10:15:44Z"},{"_createdAt":"2021-10-22T12:39:19Z","bokmaal":[{"_key":"03c51fb9decc","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi ektefellen din ikke lenger er i fengsel fra ","_key":"16b7cb861c62"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"b935a244be13"},{"_type":"span","marks":[],"text":".","_key":"3b040e36712e"}],"_type":"block","style":"normal"}],"navnISystem":"Ektefelle ikke i fengsel","periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","nynorsk":[{"style":"normal","_key":"b04c62927c01","markDefs":[],"children":[{"_key":"b8918a62f5e9","_type":"span","marks":[],"text":"Barnetrygda er endra fordi ektefellen din ikkje lenger er i fengsel frå "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"ca5457c5736e"},{"text":".","_key":"f9914f573a23","_type":"span","marks":[]}],"_type":"block"}],"_updatedAt":"2023-09-25T10:15:44Z","vedtakResultat":"REDUKSJON","vilkaar":["UTVIDET_BARNETRYGD"],"_id":"381949f0-6610-409a-ae32-26ab1f73e44c","mappe":["REDUKSJON"],"visningsnavn":"30. Ektefelle ikke i fengsel","valgbarhet":"STANDARD","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","tema":"FELLES","apiNavn":"reduksjonEktefelleIkkeIFengsel","hjemler":["2","9","11"]},{"endringsaarsaker":["ENDRE_MOTTAKER"],"_updatedAt":"2023-09-25T10:47:26Z","navnISystem":"Foreldrene bor sammen, endret mottaker","apiNavn":"endretUtbetalingReduksjonEndreMottaker","visningsnavn":"6. Endre mottaker - reduksjon","_createdAt":"2022-03-04T13:52:34Z","valgbarhet":"STANDARD","vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","tema":"FELLES","_id":"393fde5c-d661-4aee-8693-cbb1b6b6139e","hjemler":["2","12"],"_type":"begrunnelse","resultat":"REDUKSJON","bokmaal":[{"_key":"c30d8b3237ac","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden reduseres fordi den andre forelderen har søkt om barnetrygd for barn født ","_key":"fa157f19e06d"},{"_key":"47abb1151572","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"text":".","_key":"43b7c6192b52","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"_rev":"h26rAhFEYSUtDGXJE0vN6k","nynorsk":[{"children":[{"_key":"a825f65ce360","_type":"span","marks":[],"text":"Barnetrygda er redusert fordi den andre forelderen har søkt om barnetrygd for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"ab12560e0bdb"},{"marks":[],"text":".","_key":"512683858759","_type":"span"}],"_type":"block","style":"normal","_key":"ea3354ab8eae","markDefs":[]}],"mappe":["ENDRET_UTBETALINGSPERIODE"]},{"vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["DELT_BOSTED"],"endringsaarsaker":["DELT_BOSTED"],"navnISystem":"Avtale delt bosted","apiNavn":"etterEndretUtbetalingAvtaleDeltBosted","hjemler":["2","11"],"_type":"begrunnelse","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:44Z","visningsnavn":"1. Avtale delt bosted","tema":"FELLES","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utbetalt delt barnetrygd for barn fødd ","_key":"0a47f16723d5"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"20483307c114"},{"_type":"span","marks":[],"text":" fordi du har skriftleg avtale om delt bustad for ","_key":"d8b2afaa7cfe"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"5f4749536a98"},{"_key":"117e92aa380e","_type":"span","marks":[],"text":". Barnetrygda er delt frå månaden etter at begge foreldra har søkt om det."}],"_type":"block","style":"normal","_key":"eb100e34b36f"}],"mappe":["ETTER_ENDRET_UTBETALINGSPERIODE"],"_rev":"BtltdVb0HP4g4WJfnr4V7o","endretUtbetalingsperiodeTriggere":["ETTER_ENDRET_UTBETALINGSPERIODE"],"begrunnelsetype":"ETTER_ENDRET_UTBETALINGSPERIODE","bokmaal":[{"_key":"24c6633fbbdc","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utbetalt delt barnetrygd for barn født ","_key":"f9404cd97249"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"6cfcfbf6ac3f"},{"_type":"span","marks":[],"text":" fordi du har skriftlig avtale om delt bosted for ","_key":"caab7becb1be"},{"_type":"valgfeltV2","_key":"05b17170d681","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"}},{"_type":"span","marks":[],"text":". Barnetrygden deles fra måneden etter at begge foreldrene har søkt om det.","_key":"15e61f8c2e18"}],"_type":"block","style":"normal"}],"periodeType":"UTBETALING","_createdAt":"2021-10-18T05:32:00Z","endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"UTBETALING_IKKE_RELEVANT","_id":"39b004bb-6a08-46e4-8a31-1f9f118824ea"},{"begrunnelsetype":"AVSLAG","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:25:53Z","apiNavn":"avslagOmsorgForBarn","vedtakResultat":"IKKE_INNVILGET","borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"nynorsk":[{"style":"normal","_key":"bd8a6f74c7a9","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn fødd ","_key":"d23dd779f85e","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"c326dc803d85"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at du ikkje har fast omsorg for ","_key":"2a1f6dc1940e"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"83a2193e76f7"},{"_type":"span","marks":[],"text":"","_key":"8bf02033cecb"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"25f035aed317"},{"_type":"span","marks":[],"text":".","_key":"e8dc44971281"}],"_type":"block"}],"mappe":["AVSLAG"],"navnISystem":"Adopsjon, surrogati, beredskapshjem, vurdering av fast bosted\t","behandlingstema":"NASJONAL","periodeType":"INGEN_UTBETALING","tema":"NASJONAL","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"8c3eb89100bf"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"f4a5aee56cfe"},{"_key":"4bc38005228f","_type":"span","marks":[],"text":" fordi vi har kommet fram til at du ikke har fast omsorg for "},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"7a93f88145ea"},{"_key":"47370a682a86","_type":"span","marks":[],"text":""},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"bb59ce5aadef"},{"_type":"span","marks":[],"text":".","_key":"43e3bc5e89cf"}],"_type":"block","style":"normal","_key":"0c26757bdc26"}],"_rev":"FuD004taptHFqBZyEy79aK","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"_createdAt":"2021-08-30T12:58:18Z","_id":"39d0ef25-12bc-40a9-87f4-afbebb324ae8","hjemler":["2","4"],"visningsnavn":"4. Adopsjon, surrogati, beredskapshjem, vurdering av fast bosted"},{"hjemler":["2","12"],"_type":"begrunnelse","nynorsk":[{"_key":"7b1d054fb6fd","markDefs":[],"children":[{"_key":"c2cfe11ef492","_type":"span","marks":[],"text":"Du får heile barnetrygda fordi den andre forelderen ikkje har søkt om delt barnetrygd. Barnetrygda kan først delast frå månaden etter at begge foreldra har søkt om det."}],"_type":"block","style":"normal"}],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får hele barnetrygden fordi den andre forelderen ikke har søkt om delt barnetrygd. Barnetrygden kan først deles fra måneden etter at begge foreldrene har søkt om det.","_key":"3a67ddbb01d2"}],"_type":"block","style":"normal","_key":"3583fd5ee876","markDefs":[]}],"visningsnavn":"64. Annen forelder ikke søkt delt barnetrygd alle barna","vilkaar":["BOR_MED_SOKER"],"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","_createdAt":"2022-01-20T14:23:51Z","_id":"3c57059b-4cd8-4687-b39a-701cf68a5faa","mappe":["INNVILGET"],"apiNavn":"innvilgetAnnenForelderIkkeSoktDeltBarnetrygdAlleBarna","behandlingstema":"NASJONAL","tema":"NASJONAL","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:44Z","navnISystem":"Annen forelder ikke søkt delt barnetrygd alle barna","_rev":"BtltdVb0HP4g4WJfnr4V7o","vedtakResultat":"INNVILGET_ELLER_ØKNING","borMedSokerTriggere":["DELT_BOSTED_SKAL_IKKE_DELES"]},{"hjemler":["2","4","11"],"_type":"begrunnelse","_updatedAt":"2023-09-25T10:21:40Z","mappe":["FORTSATT_INNVILGET"],"behandlingstema":"NASJONAL","_rev":"FuD004taptHFqBZyEy6Lmc","periodeType":"FORTSATT_INNVILGET","begrunnelsetype":"FORTSATT_INNVILGET","nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd fordi ","_key":"74ba32586d36","_type":"span"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"18d4d3760440"},{"_type":"span","marks":[],"text":" fortsatt bur hos deg.","_key":"85a50ecf39df"}],"_type":"block","style":"normal","_key":"9fcec63906e2"}],"valgbarhet":"STANDARD","_id":"3c8fa13b-0751-4fb8-9021-b8aa47a859a7","navnISystem":"Barn bosatt med søker","visningsnavn":"3. Barn bosatt med søker","vedtakResultat":"INGEN_ENDRING","tema":"NASJONAL","_createdAt":"2021-08-30T13:01:47Z","apiNavn":"fortsattInnvilgetBorMedSoker","vilkaar":["BOR_MED_SOKER"],"bokmaal":[{"style":"normal","_key":"2f9f960148a2","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi ","_key":"fd8d8bd5470c"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"5b246335cc8f"},{"_type":"span","marks":[],"text":" fortsatt bor hos deg.","_key":"7c2cfceeaefa"}],"_type":"block"}]},{"navnISystem":"Ikke delt foreldrene bor sammen","visningsnavn":"29. Ikke delt foreldrene bor sammen","vedtakResultat":"IKKE_INNVILGET","nynorsk":[{"style":"normal","_key":"0cd9f5af7083","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"45a998516dc3"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"210274753293"},{"_type":"span","marks":[],"text":" fordi du bur saman med den andre forelderen til ","_key":"7401bbc706c7"},{"_type":"valgfeltV2","_key":"39fabc7b05dc","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"marks":[],"text":". Derfor kan ikkje barnetrygda delast.","_key":"c95449a47283","_type":"span"}],"_type":"block"}],"_createdAt":"2021-10-26T10:00:22Z","valgbarhet":"STANDARD","hjemler":["2","11"],"_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"tema":"NASJONAL","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn født ","_key":"a091c0723cb2"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"8ee5a9042a54"},{"text":" fordi du bor sammen med den andre forelderen til ","_key":"dbb569e23fee","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"3f77f3d25999","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Derfor kan ikke barnetrygden deles.","_key":"4c5f7fca61bf"}],"_type":"block","style":"normal","_key":"d1b50f735bfc","markDefs":[]}],"_rev":"BtltdVb0HP4g4WJfnr5fko","begrunnelsetype":"OPPHØR","_updatedAt":"2023-09-25T10:35:29Z","apiNavn":"opphorForeldreneBorSammen","borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"INGEN_UTBETALING","_id":"3de27b9d-8e2a-41fa-9db7-2dc0f9e7bb29","mappe":["OPPHØR"]},{"hjemler":["11"],"visningsnavn":"7. Etterbetaling tre år tilbake i tid SED","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","periodeType":"UTBETALING","begrunnelsetype":"ETTER_ENDRET_UTBETALINGSPERIODE","navnISystem":"Etterbetaling tre år tilbake i tid SED","apiNavn":"etterEndretUtbetalingEtterbetalingTreAarTilbakeITidSed","_id":"3e24a8e6-8978-4dd3-8692-6495f95c6456","nynorsk":[{"style":"normal","_key":"a6c7fdf29853","markDefs":[],"children":[{"marks":[],"text":"Det er søkt om barnetrygd for barn fødd ","_key":"379e09b05bca0","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"9eb01d0ba8e9"},{"_type":"span","marks":[],"text":" i eit annea EØS-land ","_key":"195f0c2c7187"},{"_type":"flettefelt","_key":"d723fe7b0806","flettefelt":"soknadstidspunkt"},{"marks":[],"text":". Barnetrygd kan ikkje utbetalast for meir enn tre år tilbake i tid frå tidspunktet det var søkt.","_key":"a6c88c3e4f23","_type":"span"}],"_type":"block"}],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:44Z","behandlingstema":"NASJONAL","_createdAt":"2022-09-23T11:23:45Z","bokmaal":[{"style":"normal","_key":"7a17d592288b","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Det er søkt om barnetrygd for barn født ","_key":"55a64b3512a90"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"c21a0541b7d1"},{"_type":"span","marks":[],"text":" i et annet EØS-land ","_key":"44a050a15912"},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"38e2be253224"},{"_type":"span","marks":[],"text":". Barnetrygd kan ikke utbetales for mer enn tre år tilbake i tid fra tidspunktet det var søkt.","_key":"393c502a261b"}],"_type":"block"}],"endretUtbetalingsperiodeTriggere":["ETTER_ENDRET_UTBETALINGSPERIODE"],"vedtakResultat":"IKKE_INNVILGET","mappe":["ETTER_ENDRET_UTBETALINGSPERIODE"],"tema":"FELLES","endringsaarsaker":["ETTERBETALING_3ÅR"]},{"periodeType":"UTBETALING","rolle":["SOKER","BARN"],"_id":"3ff52939-6eff-4f1e-bf8a-b5e53ae83bd1","mappe":["REDUKSJON"],"navnISystem":"Vurdering var ikke medlem","vilkaar":["BOSATT_I_RIKET"],"bosattIRiketTriggere":["MEDLEMSKAP"],"visningsnavn":"54. Vurdering var ikke medlem","hjemlerFolketrygdloven":["2-5","2-8"],"behandlingstema":"NASJONAL","_type":"begrunnelse","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi vi har kome fram til at du og barnet/barna fødd ","_key":"a7eed9f25b93"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"333b4b3f44c6"},{"_type":"span","marks":[],"text":" ikkje var medlem i folketrygda under opphaldet i utlandet.","_key":"ca009d6dccd7"}],"_type":"block","style":"normal","_key":"309a879e9b51","markDefs":[]}],"_createdAt":"2021-11-05T10:51:08Z","bokmaal":[{"children":[{"marks":[],"text":"Barnetrygden endres fordi vi har kommet fram til at du og barnet/barna født ","_key":"82d460702f2e","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"08711ce894a9"},{"_type":"span","marks":[],"text":" ikke var medlem i folketrygden under oppholdet i utlandet.","_key":"e1d86bf67a94"}],"_type":"block","style":"normal","_key":"66787028daea","markDefs":[]}],"ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"],"apiNavn":"reduksjonVurderingVarIkkeMedlem","hjemler":["4","5"],"begrunnelsetype":"REDUKSJON","tema":"NASJONAL","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:44Z","_rev":"BtltdVb0HP4g4WJfnr4V7o","vedtakResultat":"REDUKSJON"},{"hjemler":["9"],"_rev":"BtltdVb0HP4g4WJfnr4uDJ","periodeType":"FORTSATT_INNVILGET","begrunnelsetype":"FORTSATT_INNVILGET","tema":"FELLES","_createdAt":"2021-10-25T10:02:30Z","valgbarhet":"STANDARD","navnISystem":"Bor alene med barn","mappe":["FORTSATT_INNVILGET"],"_id":"40bb3703-39f6-4b12-91a6-c95cf0421ebf","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utvida barnetrygd fordi du fortsatt bur aleine med ","_key":"3a457bff44e3"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"9c3b47d16020","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":"","_key":"df172f944299"}],"_type":"block","style":"normal","_key":"adac7d360105"}],"vilkaar":["UTVIDET_BARNETRYGD"],"vedtakResultat":"INGEN_ENDRING","_updatedAt":"2023-09-25T10:22:33Z","bokmaal":[{"_key":"c392bef3ee0d","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utvidet barnetrygd fordi du fortsatt bor alene med ","_key":"535d7b61402c"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"be2e9e300bb4"},{"marks":[],"text":".","_key":"a6a51cf09dc1","_type":"span"}],"_type":"block","style":"normal"}],"_type":"begrunnelse","visningsnavn":"18. Bor alene med barn","apiNavn":"fortsattInnvilgetBorAleneMedBarn"},{"vedtakResultat":"REDUKSJON","_createdAt":"2021-10-22T11:34:58Z","_id":"4240bbf6-afd3-45bc-b961-48c648b72cd2","bokmaal":[{"children":[{"text":"Barnetrygden endres fordi du og samboeren din har bodd sammen i mer enn tolv måneder fra ","_key":"ede1f9a27085","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"59217da36a03"},{"_type":"span","marks":[],"text":".","_key":"8aff5a6a61eb"}],"_type":"block","style":"normal","_key":"a8bfb0e4d372","markDefs":[]}],"apiNavn":"reduksjonSamboerMerEnn12Maaneder","visningsnavn":"24. Samboer mer enn 12 måneder","_type":"begrunnelse","begrunnelsetype":"REDUKSJON","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"navnISystem":"Samboer mer enn 12 måneder","hjemler":["2","9","11"],"vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","nynorsk":[{"style":"normal","_key":"f0d60bb480c3","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du og sambuaren din har budd saman i meir enn tolv månader frå ","_key":"33ebccb5e9a7"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"b887fc49181b"},{"_type":"span","marks":[],"text":".","_key":"38bd22d75da3"}],"_type":"block"}],"_updatedAt":"2023-09-25T10:15:44Z","_rev":"BtltdVb0HP4g4WJfnr4V7o","tema":"FELLES"},{"bosattIRiketTriggere":["MEDLEMSKAP"],"_updatedAt":"2023-09-25T10:15:46Z","bokmaal":[{"style":"normal","_key":"ed6087ab325c","markDefs":[],"children":[{"marks":[],"text":"Barnetrygden endres fordi den andre forelderen til barn født ","_key":"d900252d0e10","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"555d7a44390a"},{"_type":"span","marks":[],"text":" ikke lenger er frivillig medlem i folketrygden under oppholdet i utlandet som startet ","_key":"37451760e748"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"d776ee96b86f"},{"_type":"span","marks":[],"text":". Bor begge foreldrene sammen under oppholdet i utlandet, må dere begge være medlem i folketrygden for å få barnetrygd.","_key":"ec934a926741"}],"_type":"block"}],"hjemler":["4","5"],"visningsnavn":"16. Annen forelder ikke lenger frivillig medlem","_type":"begrunnelse","periodeType":"UTBETALING","tema":"NASJONAL","_id":"42aaccb5-8e14-4360-ac83-403c111d6fe7","_rev":"FuD004taptHFqBZyEy5by3","hjemlerFolketrygdloven":["2-8"],"vedtakResultat":"REDUKSJON","rolle":["BARN"],"_createdAt":"2021-09-24T12:26:10Z","navnISystem":"Annen forelder ikke lenger frivillig medlem","apiNavn":"reduksjonAnnenForelderIkkeLengerFrivilligMedlem","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"REDUKSJON","nynorsk":[{"_type":"block","style":"normal","_key":"ab263f5aff2e","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi den andre forelderen til barn fødd ","_key":"1efa3a8f367d"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"82694a5c3a68"},{"_type":"span","marks":[],"text":" ikkje lenger er frivillig medlem i folketrygda under opphaldet i utlandet som starta ","_key":"80b99d46c43d"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"5c33e3bd7c82"},{"_key":"2bae6aca1cd8","_type":"span","marks":[],"text":". Bur begge foreldra saman under opphaldet i utlandet, må begge vere medlem i folketrygda for å få barnetrygd."}]}]},{"visningsnavn":"22. Annen forelder ikke medlem trygdeavtale","_createdAt":"2021-09-29T10:53:06Z","mappe":["AVSLAG"],"valgbarhet":"STANDARD","apiNavn":"avslagAnnenForelderIkkeMedlemEtterTrygdeavtale","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","bosattIRiketTriggere":["MEDLEMSKAP"],"_id":"43b0a3b3-16cc-431c-8dc0-99fb40c9f68a","_updatedAt":"2023-09-25T10:26:59Z","navnISystem":"Annen forelder ikke medlem trygdeavtale","hjemler":["2","4","22"],"behandlingstema":"NASJONAL","vilkaar":["BOSATT_I_RIKET"],"tema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr5DEJ","rolle":["SOKER","BARN"],"begrunnelsetype":"AVSLAG","nynorsk":[{"style":"normal","_key":"3a85a4752903","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"b111c482292e"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"afe3585e50ea"},{"text":" fordi den andre forelderen ikkje er medlem i folketrygda etter trygdeavtale under opphaldet i utlandet","_key":"f7e921bf2be3","_type":"span","marks":[]},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"402bd09244fd"},{"_key":"e6b707d12048","_type":"span","marks":[],"text":"."}],"_type":"block"}],"bokmaal":[{"style":"normal","_key":"78c7351e8e65","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"797fb67a4948"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"d64b3d460b5d"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikke er medlem i folketrygden etter trygdeavtale under oppholdet i utlandet","_key":"86168bba05d6"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"0fba69df12d7"},{"_type":"span","marks":[],"text":".","_key":"5c347cda6302"}],"_type":"block"}]},{"_type":"begrunnelse","periodeType":"UTBETALING","tema":"NASJONAL","navnISystem":"Barn bodde ikke med søker","_rev":"FuD004taptHFqBZyEy5by3","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi vi har kommet fram til at barn født ","_key":"b40861d719d3"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"6a671dc445a9"},{"marks":[],"text":" ikke bodde fast hos deg. ","_key":"8a322594c299","_type":"span"}],"_type":"block","style":"normal","_key":"6f746e9ff273"}],"vilkaar":["BOR_MED_SOKER"],"_createdAt":"2021-11-05T10:26:36Z","behandlingstema":"NASJONAL","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:46Z","apiNavn":"reduksjonBarnBoddeIkkeMedSoker","hjemler":["2"],"begrunnelsetype":"REDUKSJON","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi vi har kome fram til at barn fødd ","_key":"bb07a43ce8c6"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"b56c65433480"},{"_type":"span","marks":[],"text":" ikkje budde fast hos deg. ","_key":"ddd99ef1ccef"}],"_type":"block","style":"normal","_key":"ce813c3fcc70"}],"_id":"4560bbc6-a1f1-4633-99a9-94c9100b1c36","ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"],"visningsnavn":"46. Barn bodde ikke med søker","vedtakResultat":"REDUKSJON"},{"bokmaal":[{"_type":"block","style":"normal","_key":"bb0959304bfc","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får fortsatt barnetrygd fordi du og ","_key":"e6b85225ac0c0"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"8ca1610ebe99","skalHaStorForbokstav":false},{"_key":"72ea8c1453ce","_type":"span","marks":[],"text":" har varig oppholdstillatelse."}]}],"navnISystem":"Varig oppholdstillatelse","_rev":"h26rAhFEYSUtDGXJE0spKT","vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET","mappe":["FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:23:59Z","_type":"begrunnelse","begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","nynorsk":[{"style":"normal","_key":"a1d0d144f62a","markDefs":[],"children":[{"text":"Du får fortsatt barnetrygd fordi du og ","_key":"9d80b1b1fa1c0","_type":"span","marks":[]},{"_key":"e7e5dda986d7","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" har varig opphaldsløyve.","_key":"6892d8004323"}],"_type":"block"}],"_createdAt":"2022-06-08T14:13:35Z","apiNavn":"fortsattInnvilgetVarigOppholdstillatelse","visningsnavn":"40. Varig oppholdstillatelse","valgbarhet":"STANDARD","_id":"457e02c2-34e7-45c3-b577-ca52d8e96791","hjemler":["2","4"],"behandlingstema":"NASJONAL","vilkaar":["LOVLIG_OPPHOLD"],"rolle":["SOKER","BARN"]},{"navnISystem":"Tredjelandsborger søker og barn fortsatt lovlig opphold i Norge","apiNavn":"fortsattInnvilgetBarnOgSokerLovligOppholdOppholdstillatelse","visningsnavn":"2B. Tredjelandsborger søker og barn fortsatt lovlig opphold i Norge","rolle":["SOKER","BARN"],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:21:27Z","_rev":"BtltdVb0HP4g4WJfnr4qAo","nynorsk":[{"style":"normal","_key":"bee965da9e25","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du og ","_key":"9e2179b50980"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"000f83b26316"},{"_type":"span","marks":[],"text":" fortsatt har opphaldsløyve.","_key":"a4c27c07ae4a"}],"_type":"block"}],"_createdAt":"2021-08-30T12:58:36Z","_id":"45f1ed86-eaa0-4ded-a882-f3bd855763de","mappe":["FORTSATT_INNVILGET"],"hjemler":["2","4","11"],"_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET","begrunnelsetype":"FORTSATT_INNVILGET","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du og ","_key":"b13997fc6097"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"f60bb3776740","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" fortsatt har oppholdstillatelse.","_key":"bde3dbbe798c"}],"_type":"block","style":"normal","_key":"70b366604473"}],"vilkaar":["LOVLIG_OPPHOLD"],"tema":"NASJONAL"},{"navnISystem":"Ikke pliktig medlem","visningsnavn":"15. Ikke pliktig medlem","periodeType":"INGEN_UTBETALING","bosattIRiketTriggere":["MEDLEMSKAP"],"_id":"468d3f7b-31ab-4bb1-a48c-c106a512ffb4","mappe":["OPPHØR"],"apiNavn":"opphorSokerOgBarnIkkeLengerPliktigMedlem","_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"tema":"NASJONAL","_updatedAt":"2023-09-25T10:32:17Z","_rev":"h26rAhFEYSUtDGXJE0uoMY","hjemlerFolketrygdloven":["2-5"],"vedtakResultat":"IKKE_INNVILGET","rolle":["SOKER","BARN"],"valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"text":"Barnetrygd for barn født ","_key":"19fd40541602","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"da5987814944"},{"_type":"span","marks":[],"text":" fordi ","_key":"2dd51b569904"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"49d9101cc75e"},{"_type":"span","marks":[],"text":" ikke er pliktig medlem i folketrygden ","_key":"8f4c48fd6fda"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"1dedcf31588c"},{"_type":"span","marks":[],"text":". ","_key":"3f8efbfb77fc"}],"_type":"block","style":"normal","_key":"c4ff93523119"}],"hjemler":["4","5"],"behandlingstema":"NASJONAL","begrunnelsetype":"OPPHØR","nynorsk":[{"_key":"35815bbd6b60","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"70a3355b4343"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"142396d78f69"},{"_type":"span","marks":[],"text":" fordi ","_key":"5e610e086147"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"0f74056d5696"},{"_type":"span","marks":[],"text":" ikkje er pliktig medlem i folketrygda ","_key":"c67e1c01c5cb"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"44836dfd2529"},{"_type":"span","marks":[],"text":".","_key":"f0015a8d8944"}],"_type":"block","style":"normal"}],"_createdAt":"2021-09-24T11:18:43Z"},{"navnISystem":"Søker ber om opphør","_rev":"FuD004taptHFqBZyEy5by3","tema":"FELLES","_createdAt":"2022-06-08T11:21:36Z","valgbarhet":"STANDARD","_id":"4726101e-032c-441d-b749-a66bd0415931","_updatedAt":"2023-09-25T10:15:46Z","apiNavn":"reduksjonSokerBerOmOpphor","_type":"begrunnelse","vedtakResultat":"REDUKSJON","borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"nynorsk":[{"_key":"03811b522247","markDefs":[],"children":[{"marks":[],"text":"Barnetrygda er endra fordi du har bedt om at barnetrygda for barn fødd ","_key":"0d2a9066c8800","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"0f3d506ba2fe"},{"_type":"span","marks":[],"text":" blir stansa.","_key":"1af9693993f4"}],"_type":"block","style":"normal"}],"mappe":["REDUKSJON"],"hjemler":["2"],"visningsnavn":"65. Søker ber om opphør","behandlingstema":"NASJONAL","vilkaar":["BOR_MED_SOKER"],"periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","bokmaal":[{"children":[{"marks":[],"text":"Barnetrygden endres fordi du har bedt om at barnetrygden for barn født ","_key":"9f8065da4f5b0","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"0684b1c1b239"},{"marks":[],"text":" blir stanset.","_key":"674754460140","_type":"span"}],"_type":"block","style":"normal","_key":"94a4ae99f87c","markDefs":[]}]},{"nynorsk":[{"style":"normal","_key":"ae78e661b3e2","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi sambuaren din ikkje lenger er i tvungent psykisk helsevern frå ","_key":"95add429163d"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"2582a8415bc4"},{"_type":"span","marks":[],"text":".","_key":"a203cc88a09a"}],"_type":"block"}],"bokmaal":[{"_key":"f63410c68dc1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi samboeren din ikke lenger er i tvungent psykisk helsevern fra ","_key":"f79ccbc6ca62"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"7dffbe8f9aa1"},{"_type":"span","marks":[],"text":".","_key":"fbb7a2e14117"}],"_type":"block","style":"normal"}],"vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"REDUKSJON","vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","tema":"FELLES","_createdAt":"2021-10-23T06:03:40Z","navnISystem":"Samboer ikke i tvungent psykisk helsevern","_rev":"FuD004taptHFqBZyEy5by3","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:46Z","hjemler":["2","9","11"],"_type":"begrunnelse","_id":"47df7596-b2cc-4198-8cf6-fcafa543a23b","apiNavn":"reduksjonSamboerIkkeITvungentPsykiskHelsevern","visningsnavn":"35. Samboer ikke i tvungent psykisk helsevern"},{"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi vi har kommet fram til at du bodde sammen med den andre forelderen til barn født ","_key":"a054f4e4fcb3"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"6dfe2ec5c8a6"},{"_type":"span","marks":[],"text":".","_key":"02f776103afe"}],"_type":"block","style":"normal","_key":"cb4680e2bd9b"}],"hjemler":["2"],"periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","tema":"NASJONAL","nynorsk":[{"style":"normal","_key":"9ef26664b770","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi vi har kome fram til at du budde saman med den andre forelderen til barn fødd ","_key":"4c45e8be3a69"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"5a40a79033bb"},{"_type":"span","marks":[],"text":".","_key":"5e0cbb8eba92"}],"_type":"block"}],"valgbarhet":"STANDARD","apiNavn":"reduksjonVurderingForeldreneBoddeSammen","_id":"48fa663f-ce39-4c8c-97c0-e4d09a12de83","mappe":["REDUKSJON"],"ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"],"navnISystem":"Vurdering foreldrene bodde sammen","_type":"begrunnelse","vedtakResultat":"REDUKSJON","_updatedAt":"2023-09-25T10:15:46Z","visningsnavn":"52. Vurdering foreldrene bodde sammen","behandlingstema":"NASJONAL","_rev":"FuD004taptHFqBZyEy5by3","vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["DELT_BOSTED"],"_createdAt":"2021-11-05T10:41:38Z"},{"_type":"begrunnelse","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:30:54Z","bokmaal":[{"style":"normal","_key":"bc2f5a8e0b37","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"87cdd760a926"},{"_key":"a572ce43aaa2","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at ","_key":"0547e03de947"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","marks":[],"_key":"b8a75764d579"},{"_type":"span","marks":[],"text":" ikke skal bo i Norge i mer enn 12 måneder. ","_key":"f2f31006760a"}],"_type":"block"}],"navnISystem":"Vurdering bosatt under 12 måneder","vilkaar":["BOSATT_I_RIKET"],"valgbarhet":"STANDARD","tema":"NASJONAL","nynorsk":[{"markDefs":[],"children":[{"_key":"73ee19117744","_type":"span","marks":[],"text":"Barnetrygd for barn fødd "},{"_type":"flettefelt","_key":"411265021782","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at ","_key":"1458fcecfb38"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","marks":[],"_key":"aaab8bff1dbf","skalHaStorForbokstav":false},{"text":" ikkje skal bu i Noreg i meir enn 12 månader. ","_key":"755d01f39a3f","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"cb486d582e09"}],"_createdAt":"2021-11-22T12:20:59Z","_rev":"h26rAhFEYSUtDGXJE0t6H9","vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"AVSLAG","periodeType":"INGEN_UTBETALING","rolle":["SOKER","BARN"],"bosattIRiketTriggere":["VURDERING_ANNET_GRUNNLAG"],"_id":"49ce3863-5932-4548-897b-ceecd928456f","apiNavn":"avslagVurderingBosattUnder12Maaneder","hjemler":["2","4"],"visningsnavn":"62. Vurdering bosatt under 12 måneder"},{"_id":"4a02f7e4-84fd-495b-b02c-bd4471308a3c","apiNavn":"etterEndretUtbetalingEndreMottaker","periodeType":"UTBETALING","nynorsk":[{"markDefs":[],"children":[{"text":"Du får barnetrygd ","_key":"eaee5205e8d5","_type":"span","marks":[]},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"bc647b784775"},{"_key":"f20bfc24911e","_type":"span","marks":[],"text":" fordi "},{"_type":"valgReferanse","_key":"96f2173d0c87","_ref":"df8dc282-2637-4047-a656-8527205dc364"},{"text":" bur saman med deg. Du får barnetrygd frå same tidspunkt som barnetrygda til den andre forelderen opphøyrer. ","_key":"a974456d4796","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"9701363bfb76"}],"endringsaarsaker":["ALLEREDE_UTBETALT"],"_createdAt":"2022-03-04T13:57:04Z","visningsnavn":"4. Innvilget - Foreldrene bor sammen, endret mottaker","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"FELLES","behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr5osJ","bokmaal":[{"_key":"f2db8204998d","markDefs":[],"children":[{"_key":"7f7e02d2daa3","_type":"span","marks":[],"text":"Du får barnetrygd "},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"24b01ea91179"},{"_type":"span","marks":[],"text":" fordi ","_key":"59b9b60dd21e"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"3c377bad3c95"},{"_type":"span","marks":[],"text":" bor sammen med deg. Du får barnetrygd fra samme tidspunkt som barnetrygden til den andre forelderen opphører.","_key":"6b2268077f26"}],"_type":"block","style":"normal"}],"valgbarhet":"STANDARD","mappe":["ETTER_ENDRET_UTBETALINGSPERIODE"],"_updatedAt":"2023-09-25T10:37:43Z","navnISystem":"Foreldrene bor sammen, endret mottaker","hjemler":["2","4","11"],"endretUtbetalingsperiodeTriggere":["ETTER_ENDRET_UTBETALINGSPERIODE"],"begrunnelsetype":"ETTER_ENDRET_UTBETALINGSPERIODE"},{"visningsnavn":"54. Vurdering egen husholdning","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","nynorsk":[{"style":"normal","_key":"8536e433637f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"1bc6bc79008f"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"bd9c0b539853","skalHaStorForbokstav":false},{"text":" utvida barnetrygd fordi vi har kome fram til at du bur aleine med ","_key":"5023a1a067c5","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"51d808a6324b","skalHaStorForbokstav":false},{"text":" frå ","_key":"1584207ee0f9","_type":"span","marks":[]},{"_type":"flettefelt","_key":"e3cb99873690","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"marks":[],"text":". ","_key":"e6e1d9f22457","_type":"span"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"01d4acf60c7b","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at du fekk barn.","_key":"6bf9e8690917"}],"_type":"block"}],"_id":"4a09a751-ee32-4778-95aa-8b3380af5310","apiNavn":"innvilgetVurderingEgenHusholdning","_type":"begrunnelse","_updatedAt":"2023-09-25T10:15:46Z","navnISystem":"Vurdering egen husholdning","behandlingstema":"NASJONAL","_rev":"FuD004taptHFqBZyEy5by3","_createdAt":"2021-10-25T09:48:05Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"72bf045b18d2"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"9e704a13a16c","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd fordi vi har kommet fram til at du bor alene med ","_key":"9a1a1547327f"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"d91957663320","skalHaStorForbokstav":false},{"_key":"32fbab4c476f","_type":"span","marks":[],"text":" fra "},{"_type":"flettefelt","_key":"33cb3f7564d3","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_type":"span","marks":[],"text":". ","_key":"8010dca8c3b5"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"b092c4664ba5","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at du fikk barn.","_key":"1708167910ee"}],"_type":"block","style":"normal","_key":"39588b3f5c30"}],"hjemler":["2","9","11"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","tema":"FELLES","valgbarhet":"STANDARD","mappe":["INNVILGET"]},{"apiNavn":"avslagManglerAvtaleDeltBosted","hjemler":["2"],"_rev":"h26rAhFEYSUtDGXJE0t5YR","vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["DELT_BOSTED"],"begrunnelsetype":"AVSLAG","tema":"NASJONAL","navnISystem":"Mangler avtale delt bosted","_id":"4a259a01-32bf-4fca-87b2-dd6d36afb0e5","mappe":["AVSLAG"],"bokmaal":[{"_key":"0837138cf8ef","markDefs":[],"children":[{"_key":"7e03f9dba2c2","_type":"span","marks":[],"text":"Delt barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"8660030a4e06"},{"text":" fordi du ikke har sendt oss avtale om delt bosted for ","_key":"7135fb6d67ca","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"64e54def6c11","skalHaStorForbokstav":false},{"marks":[],"text":". Derfor kan ikke barnetrygden deles.","_key":"3c932afa3ae4","_type":"span"}],"_type":"block","style":"normal"}],"_type":"begrunnelse","periodeType":"INGEN_UTBETALING","nynorsk":[{"_type":"block","style":"normal","_key":"dd4881ac9403","markDefs":[],"children":[{"text":"Delt barnetrygd for barn fødd ","_key":"9d60199f06f9","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"b78521601513"},{"_type":"span","marks":[],"text":" fordi du ikkje har sendt oss avtale om delt bustad for ","_key":"314e0de5c539"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"3e08f21bb242","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Derfor kan ikkje barnetrygda delast.","_key":"eb5eb18f605a"}]}],"_createdAt":"2021-10-26T10:07:41Z","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:30:43Z","visningsnavn":"59. Mangler avtale delt bosted"},{"_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi vi har komme fram til at du ikkje har eiga hushaldning og derfor er sambuar.","_key":"7572ec512cfd"}],"_type":"block","style":"normal","_key":"899694d758f3","markDefs":[]}],"_createdAt":"2021-10-23T06:10:42Z","bokmaal":[{"_type":"block","style":"normal","_key":"5431aa9613aa","markDefs":[],"children":[{"_key":"581de19961c4","_type":"span","marks":[],"text":"Barnetrygden endres fordi vi har kommet fram til at du ikke har egen husholdning og derfor er samboer."}]}],"navnISystem":"Samboer ikke egen husholdning","begrunnelsetype":"REDUKSJON","apiNavn":"reduksjonSamboerIkkeEgenHusholdning","tema":"FELLES","valgbarhet":"STANDARD","_id":"4ab9276b-ba52-4870-b142-e27e353b7c47","_updatedAt":"2023-09-25T10:15:46Z","vedtakResultat":"REDUKSJON","visningsnavn":"39. Samboer ikke egen husholdning","_rev":"FuD004taptHFqBZyEy5by3","periodeType":"UTBETALING","mappe":["REDUKSJON"],"hjemler":["2","9","11"]},{"vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"AVSLAG","valgbarhet":"STANDARD","bokmaal":[{"_type":"block","style":"normal","_key":"2bfeabd5944c","markDefs":[],"children":[{"marks":[],"text":"Utvidet barnetrygd fordi vi har kommet fram til at du og ektefellen din ikke har flyttet fra hverandre.","_key":"66919eacf170","_type":"span"}]}],"apiNavn":"avslagVurderingIkkeFlyttetFraEktefelle","visningsnavn":"32. Vurdering ikke flyttet fra ektefelle","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:27:38Z","_type":"begrunnelse","_createdAt":"2021-10-22T07:57:35Z","tema":"FELLES","hjemler":["9"],"_rev":"h26rAhFEYSUtDGXJE0syee","periodeType":"INGEN_UTBETALING","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi vi har kome fram til at du og ektefellen din ikkje har flytta frå kvarandre.","_key":"cbaf0b346dcf"}],"_type":"block","style":"normal","_key":"8195f376623f"}],"_id":"4ad844f3-85fe-4415-b9df-ebf3a3eb4b53","navnISystem":"Vurdering ikke flyttet fra ektefelle","vedtakResultat":"INNVILGET_ELLER_ØKNING"},{"hjemler":["2","9","11"],"visningsnavn":"31. Samboer ikke i fengsel","_type":"begrunnelse","vedtakResultat":"REDUKSJON","valgbarhet":"STANDARD","_id":"4f23fd84-16f0-4df0-97e6-078c65330477","_updatedAt":"2023-09-25T10:15:46Z","navnISystem":"Samboer ikke i fengsel","apiNavn":"reduksjonSamboerIkkeIFengsel","begrunnelsetype":"REDUKSJON","tema":"FELLES","_createdAt":"2021-10-22T12:41:33Z","_rev":"FuD004taptHFqBZyEy5by3","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi sambuaren din ikkje lenger er i fengsel frå ","_key":"26f55f11716e"},{"_type":"flettefelt","_key":"a32dc0aab07a","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_type":"span","marks":[],"text":".","_key":"ad1817cd29f9"}],"_type":"block","style":"normal","_key":"55d593771d06"}],"mappe":["REDUKSJON"],"bokmaal":[{"_type":"block","style":"normal","_key":"96f04be0ee64","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi samboeren din ikke lenger er i fengsel fra ","_key":"1ff85aea3d37"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"e52fe3a1a494"},{"text":".","_key":"f6fe2792e9ae","_type":"span","marks":[]}]}]},{"hjemler":["4","5"],"rolle":["SOKER","BARN"],"_id":"502ba243-ac5f-4649-aef8-e695834a8763","apiNavn":"avslagAnnenForelderIkkePliktigMedlem","vilkaar":["BOSATT_I_RIKET"],"periodeType":"INGEN_UTBETALING","tema":"NASJONAL","bosattIRiketTriggere":["MEDLEMSKAP"],"valgbarhet":"STANDARD","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:26:52Z","_type":"begrunnelse","visningsnavn":"20. Annen forelder ikke pliktig medlem","_rev":"h26rAhFEYSUtDGXJE0sxpq","hjemlerFolketrygdloven":["2-5"],"vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"AVSLAG","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"30d194ce5752"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"e59ffe66cbb7"},{"marks":[],"text":" fordi den andre forelderen ikke er pliktig medlem i folketrygden","_key":"635f81315e4c","_type":"span"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"eefff4867934"},{"_type":"span","marks":[],"text":". Bor begge foreldrene sammen under oppholdet i utlandet, må dere begge være medlem i folketrygden for å få barnetrygd.","_key":"10128f8ebb46"}],"_type":"block","style":"normal","_key":"45c5ff62c10d"}],"navnISystem":"Annen forelder ikke pliktig medlem","nynorsk":[{"_type":"block","style":"normal","_key":"ec2628a4a2bb","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"7edfa5de67a7"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"357b132132a1"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikkje er pliktig medlem i folketrygda","_key":"903d07b0f789"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"727c86c2cec1"},{"_type":"span","marks":[],"text":". Bur begge foreldre saman under opphaldet i utlandet, må begge vere medlem i folketrygda for å få barnetrygd. ","_key":"e756fecfe997"}]}],"_createdAt":"2021-09-29T11:03:31Z","behandlingstema":"NASJONAL"},{"hjemler":["2","9","11"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","_createdAt":"2021-10-25T09:57:22Z","mappe":["INNVILGET"],"navnISystem":"Forsvunnet samboer","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","nynorsk":[{"markDefs":[],"children":[{"text":"","_key":"2971de4a7f54","_type":"span","marks":[]},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"7f65922e584f"},{"_type":"span","marks":[],"text":" utvida barnetrygd sjølv om du fortsatt er sambuar fordi du bur aleine med ","_key":"d379200ef250"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"9c27ff8ac3d1","skalHaStorForbokstav":false},{"text":". Sambuaren din har vore forsvunne i seks månader eller meir frå ","_key":"8fd80b067c4d","_type":"span","marks":[]},{"_type":"flettefelt","_key":"65881f27228c","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_type":"span","marks":[],"text":". ","_key":"added341c77d"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"71e73a8f6533","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at sambuaren din forsvann.","_key":"d13636180a3a"}],"_type":"block","style":"normal","_key":"fb6c30a434d4"}],"valgbarhet":"STANDARD","visningsnavn":"57. Forsvunnet samboer","_rev":"FuD004taptHFqBZyEy5by3","_type":"begrunnelse","tema":"FELLES","_updatedAt":"2023-09-25T10:15:46Z","apiNavn":"innvilgetForsvunnetSamboer","behandlingstema":"NASJONAL","_id":"50489fde-5011-45ae-98cd-650b9c0ab97e","bokmaal":[{"_key":"2c594d4e5177","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"74928a4df5ab"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"1577b9d54654","skalHaStorForbokstav":false},{"text":" utvidet barnetrygd selv om du fortsatt er samboer fordi du bor alene med ","_key":"ccd2238a91fb","_type":"span","marks":[]},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"1391894bea6f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Samboeren din har vært forsvunnet i seks måneder eller mer fra ","_key":"23b9ee9e524e"},{"_type":"flettefelt","_key":"90e35513e467","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_type":"span","marks":[],"text":". ","_key":"a8d466bc81ff"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"8738b423a569","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at samboeren din forsvant.","_key":"9931c70eb6ab"}],"_type":"block","style":"normal"}]},{"rolle":["BARN"],"bosattIRiketTriggere":["MEDLEMSKAP"],"nynorsk":[{"_key":"85e2645806da","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi den andre forelderen til barn fødd ","_key":"95774bdc5aec"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"5a41e5b2a51f"},{"_type":"span","marks":[],"text":" ikkje er medlem i folketrygda under opphaldet i utlandet som starta ","_key":"9bf76c60a69a"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"760f49ac64b1"},{"_type":"span","marks":[],"text":". Bur begge foreldra saman under opphaldet i utlandet, må begge vere medlem i folketrygda for å få barnetrygd.","_key":"cbec836b2aa4"}],"_type":"block","style":"normal"}],"visningsnavn":"15. Annen forelder ikke medlem","hjemler":["4","5"],"_type":"begrunnelse","vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:46Z","apiNavn":"reduksjonAnnenForelderIkkeMedlem","bokmaal":[{"children":[{"marks":[],"text":"Barnetrygden endres fordi den andre forelderen til barn født ","_key":"780db6080fa4","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"befeff82d0a9"},{"_type":"span","marks":[],"text":" ikke er medlem i folketrygden under oppholdet i utlandet som startet ","_key":"446c72bdddfd"},{"_key":"95f6dff774ab","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":". Bor begge foreldrene sammen under oppholdet i utlandet, må dere begge være medlem i folketrygden for å få barnetrygd.","_key":"0e5fea526ddc"}],"_type":"block","style":"normal","_key":"ad8febcfc31e","markDefs":[]}],"_rev":"FuD004taptHFqBZyEy5by3","hjemlerFolketrygdloven":["2-5","2-8"],"vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"REDUKSJON","_createdAt":"2021-09-24T12:22:57Z","_id":"51030978-2864-42f3-a375-9453485b866c","navnISystem":"Annen forelder ikke medlem","tema":"NASJONAL"},{"_id":"5224e67d-bf50-4353-ab9e-00265d947946","navnISystem":"Rettsavgjørelse fast bosted","apiNavn":"avslagRettsavgjorelseSamver","_rev":"h26rAhFEYSUtDGXJE0t50u","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"d7032f1a9b4c"},{"_key":"5b133b63ea1b","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_key":"0ad6ae98d858","_type":"span","marks":[],"text":" fordi retten har bestemt at "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"e14770846445","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" skal bu fast hos den andre forelderen. ","_key":"c176efb668cb"}],"_type":"block","style":"normal","_key":"3b5280832735"}],"mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:30:34Z","hjemler":["2"],"visningsnavn":"57. Rettsavgjørelse fast bosted","vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"INGEN_UTBETALING","tema":"NASJONAL","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"AVSLAG","_createdAt":"2021-10-26T10:02:57Z","valgbarhet":"STANDARD","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn født ","_key":"3f2d58acd743"},{"_type":"flettefelt","_key":"99200446eca3","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi retten har bestemt at ","_key":"a32212a93152"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"5f6b23e3b910","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" skal bo fast hos den andre forelderen. ","_key":"f1a9439fb08d"}],"_type":"block","style":"normal","_key":"08f6dc2964f2","markDefs":[]}]},{"vilkaar":["UTVIDET_BARNETRYGD"],"tema":"NASJONAL","_id":"523ac4a2-d0e7-481b-bfd9-49ea2771bf21","_updatedAt":"2023-09-25T10:27:27Z","_rev":"h26rAhFEYSUtDGXJE0syTT","vedtakResultat":"IKKE_INNVILGET","bokmaal":[{"_key":"13a82b21bc47","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi du ikke er separert.","_key":"fbd012a093b4"}],"_type":"block","style":"normal"}],"navnISystem":"Ikke separert","apiNavn":"avslagIkkeSeparert","visningsnavn":"29. Ikke separert","_type":"begrunnelse","periodeType":"INGEN_UTBETALING","nynorsk":[{"style":"normal","_key":"33c08756a79e","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi du ikke er separert.","_key":"49563a9f17b9"}],"_type":"block"}],"mappe":["AVSLAG"],"hjemler":["9"],"begrunnelsetype":"AVSLAG","_createdAt":"2021-10-22T07:52:38Z","valgbarhet":"STANDARD"},{"behandlingstema":"NASJONAL","periodeType":"INGEN_UTBETALING","_id":"53089eeb-1ca4-4b2b-9f52-aa18d3a1b614","_updatedAt":"2023-09-25T10:36:39Z","bokmaal":[{"style":"normal","_key":"816e566d25aa","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"0e931ff9e313"},{"_key":"5a4cd13db529","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_key":"72ad95411f99","_type":"span","marks":[],"text":" fordi den andre forelderen har søkt om barnetrygd for "},{"_type":"valgfeltV2","_key":"e2538271b131","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":".","_key":"34f4a7194405"}],"_type":"block"}],"visningsnavn":"47. Foreldrene bor sammen endret mottaker","_rev":"FuD004taptHFqBZyEy8gq8","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"begrunnelsetype":"OPPHØR","tema":"NASJONAL","navnISystem":"Foreldrene bor sammen endret mottaker","apiNavn":"opphorForeldreneBorSammenEndretMottaker","hjemler":["2","12"],"vedtakResultat":"IKKE_INNVILGET","nynorsk":[{"_key":"2858425def44","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"f7ef4ea6e52a","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"e25a62656b21"},{"_type":"span","marks":[],"text":" fordi den andre forelderen har søkt om barnetrygd for ","_key":"2ff884f2fe4e"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"4ecd909924ab"},{"text":".","_key":"b1aa0b66343b","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"valgbarhet":"STANDARD","mappe":["OPPHØR"],"_createdAt":"2022-04-28T13:20:57Z"},{"apiNavn":"avslagFengselUnder6MaanederEktefelle","_rev":"BtltdVb0HP4g4WJfnr5K4o","_type":"begrunnelse","_createdAt":"2021-10-22T09:03:20Z","valgbarhet":"STANDARD","_id":"5324d8e3-d0f8-49bc-af94-a4fe716b077d","navnISystem":"Fengsel under 6 måneder ektefelle","vedtakResultat":"IKKE_INNVILGET","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","nynorsk":[{"_key":"784f27e496d3","markDefs":[],"children":[{"_key":"a6b085671c38","_type":"span","marks":[],"text":"Utvida barnetrygd fordi fengselsopphaldet til ektefellen din er under seks månader."}],"_type":"block","style":"normal"}],"mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:29:52Z","bokmaal":[{"style":"normal","_key":"1624a7fd110f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi fengselsoppholdet til ektefellen din er under seks måneder.","_key":"e59643c849be"}],"_type":"block"}],"hjemler":["9"],"visningsnavn":"42. Fengsel under 6 måneder ektefelle","tema":"FELLES"},{"_createdAt":"2021-09-24T12:40:22Z","navnISystem":"Vurdering barn flere korte opphold i utlandet siste årene ","apiNavn":"reduksjonVurderingBarnFlereKorteOppholdIUtlandetSisteArene","visningsnavn":"20. Vurdering barn flere korte opphold i utlandet siste årene ","_rev":"FuD004taptHFqBZyEy5by3","vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"REDUKSJON","bosattIRiketTriggere":["MEDLEMSKAP"],"mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:46Z","vedtakResultat":"REDUKSJON","tema":"NASJONAL","hjemler":["4"],"bokmaal":[{"style":"normal","_key":"6904482da5dd","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Vi har kommet fram til at barn født ","_key":"250a817c0c44"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"8c08f9c3452d"},{"_type":"span","marks":[],"text":" har hatt flere korte opphold i utlandet de siste årene. Oppholdene i utlandet utgjør til sammen mer enn 6 måneder for hvert år. ","_key":"eb116b7baa05"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"dd10827f79e5"},{"_type":"span","marks":[],"text":" regnes derfor ikke som bosatt i Norge fra ","_key":"84ab0e7e0fef"},{"_key":"b73d072da852","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"7ff4fc8c471c"}],"_type":"block"}],"_type":"begrunnelse","periodeType":"UTBETALING","rolle":["BARN"],"nynorsk":[{"children":[{"marks":[],"text":"Vi har kome fram til at barn fødd ","_key":"9916e146474f","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"efac7e9cfafa"},{"_key":"cdba7558fa22","_type":"span","marks":[],"text":" har hatt fleire korte opphald i utlandet dei siste åra. Opphalda i utlandet utgjer til saman meir enn 6 månader for kvart år. "},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"95f249bea3f9"},{"_key":"3674efd7fc5b","_type":"span","marks":[],"text":" er derfor ikkje rekna som busett i Noreg frå "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"849e4cc1482b"},{"_type":"span","marks":[],"text":".","_key":"be3a13f9ea8f"}],"_type":"block","style":"normal","_key":"475d3e0a27cd","markDefs":[]}],"valgbarhet":"STANDARD","_id":"5373a23f-4c0c-405e-9465-bc550713fb1f"},{"_id":"53fa3c13-87c8-4f32-b80c-ca97aca64814","mappe":["INNVILGET"],"hjemler":["2","11"],"visningsnavn":"62. Avtale delt bosted får fra avtaletidspunkt","tema":"NASJONAL","nynorsk":[{"_type":"block","style":"normal","_key":"b2c0534d5275","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får delt barnetrygd for barn fødd ","_key":"17e07c8119b0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"711e827662d7"},{"_type":"span","marks":[],"text":" fordi du har skriftleg avtale om delt bustad for ","_key":"15305391fe43"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"64d59acb746d","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" frå ","_key":"3ab75270e2b0"},{"_key":"adbfb550ba73","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":". Barnetrygda er delt frå månaden etter datoen avtalen gjeld frå.","_key":"2b2bb28fb623"}]}],"apiNavn":"innvilgetAvtaleDeltBostedFaarFraAvtaletidspunkt","vedtakResultat":"INNVILGET_ELLER_ØKNING","_createdAt":"2021-10-26T08:54:06Z","valgbarhet":"STANDARD","borMedSokerTriggere":["DELT_BOSTED"],"begrunnelsetype":"INNVILGET","vilkaar":["BOR_MED_SOKER"],"periodeType":"UTBETALING","_updatedAt":"2023-09-25T10:15:46Z","bokmaal":[{"markDefs":[],"children":[{"text":"Du får delt barnetrygd for barn født ","_key":"5c5e38a6e6c2","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"e9d65a67becf"},{"_type":"span","marks":[],"text":" fordi du har skriftlig avtale om delt bosted for ","_key":"319cc2ed3e82"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"8a6ef343984e","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" fra ","_key":"39dfdc53d671"},{"_key":"6fb31d34a978","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"text":". Barnetrygden deles fra måneden etter datoen avtalen gjelder fra.","_key":"edc763713d2c","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"d10146c2fdeb"}],"navnISystem":"Avtale delt bosted får fra avtaletidspunkt","behandlingstema":"NASJONAL","_rev":"FuD004taptHFqBZyEy5by3","_type":"begrunnelse"},{"hjemler":["2"],"visningsnavn":"9. Delt bosted praktiseres fortsatt","behandlingstema":"NASJONAL","_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","borMedSokerTriggere":["DELT_BOSTED"],"navnISystem":"Delt bosted praktiseres fortsatt","apiNavn":"fortsattInnvilgetDeltBostedPraktiseresFortsatt","_createdAt":"2021-08-30T13:10:33Z","valgbarhet":"STANDARD","_id":"540afc22-b0cf-4a2e-aa78-383ee3d9d6c1","begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr4rco","bokmaal":[{"_key":"164122ed5d90","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får delt barnetrygd fordi vi har kommet fram til at dere fortsatt følger avtalen om delt bosted for ","_key":"8d7d9efbf0b2"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"0828aa0b4e84"},{"_type":"span","marks":[],"text":".","_key":"4cd72943ddd0"}],"_type":"block","style":"normal"}],"mappe":["FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:22:03Z","nynorsk":[{"children":[{"_key":"2b05624f735c","_type":"span","marks":[],"text":"Du får delt barnetrygd fordi vi har kome fram til at de fortsatt føl avtalen om delt bustad for "},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"75d2b5c7c93f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":".","_key":"5039562f4d85"}],"_type":"block","style":"normal","_key":"8cc0a8d7478c","markDefs":[]}],"vilkaar":["BOR_MED_SOKER"],"periodeType":"FORTSATT_INNVILGET"},{"_createdAt":"2022-10-07T09:21:40Z","mappe":["REDUKSJON"],"apiNavn":"reduksjonSoekerBerOmOpphoerUtvidet","hjemler":["9"],"_rev":"FuD004taptHFqBZyEy5by3","begrunnelsetype":"REDUKSJON","tema":"FELLES","_type":"begrunnelse","vedtakResultat":"REDUKSJON","bokmaal":[{"_type":"block","style":"normal","_key":"92e49ab4f7ca","markDefs":[],"children":[{"marks":[],"text":"Barnetrygden endres fordi du har bedt om at utvidet barnetrygd blir stanset.","_key":"0a83646700d60","_type":"span"}]}],"visningsnavn":"70. Søker ber om opphør utvidet","vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du har bedt om at utvida barnetrygd blir stansa.","_key":"2fcd1a941ac20"}],"_type":"block","style":"normal","_key":"b3c7663895d7","markDefs":[]}],"_id":"546618bf-7c75-431c-ba45-e6010e4a3d7a","_updatedAt":"2023-09-25T10:15:46Z","navnISystem":"Søker ber om opphør utvidet","behandlingstema":"NASJONAL","periodeType":"UTBETALING","valgbarhet":"STANDARD"},{"visningsnavn":"1. Barn 6 år institusjon","_rev":"FuD004taptHFqBZyEy5by3","_createdAt":"2022-11-04T08:46:57Z","_updatedAt":"2023-09-25T10:15:46Z","ovrigeTriggere":["BARN_MED_6_ÅRS_DAG","SATSENDRING"],"fagsakType":"INSTITUSJON","hjemler":["2","10"],"vedtakResultat":"REDUKSJON","begrunnelsetype":"REDUKSJON","tema":"NASJONAL","_id":"556f6778-9f49-41e0-935d-ac3e972959ed","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi barn født ","_key":"d66b6a364bcb0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"b072e670961d"},{"_type":"span","marks":[],"text":" er 6 år.","_key":"5e6ac89a964e"}],"_type":"block","style":"normal","_key":"a6c474f9b5fb"}],"navnISystem":"Barn 6 år institusjon","behandlingstema":"NASJONAL_INSTITUSJON","nynorsk":[{"markDefs":[],"children":[{"text":"Barnetrygda er endra fordi barn fødd ","_key":"1265e14895c80","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"d75c631852d1"},{"_key":"427d32d58f3c","_type":"span","marks":[],"text":" er 6 år."}],"_type":"block","style":"normal","_key":"c08ab7e9c84a"}],"valgbarhet":"SAKSPESIFIKK","_type":"begrunnelse","periodeType":"UTBETALING","apiNavn":"reduksjonBarn6AarInstitusjon","mappe":["INSTITUSJON","REDUKSJON"]},{"apiNavn":"innvilgetForvaringGift","periodeType":"UTBETALING","nynorsk":[{"style":"normal","_key":"ccbcd817ff95","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"132c0b28891b"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"99731e7213c2","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvida barnetrygd sjølv om du fortsatt er gift fordi du bur aleine med ","_key":"06ead2178f5f"},{"_key":"b450a45076db","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". Ektefellen din er i forvaring i seks månader eller meir frå ","_key":"c2cac0eb5c6f"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"0d8a81313f01"},{"_type":"span","marks":[],"text":". ","_key":"c8ff71437e83"},{"_type":"valgfeltV2","_key":"4686c14cd6be","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"}},{"_key":"597d0c0fff55","_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at forvaringa starta."}],"_type":"block"}],"navnISystem":"Forvaring gift","_type":"begrunnelse","tema":"FELLES","valgbarhet":"STANDARD","_id":"559a2122-cb47-4d49-8d95-b71dc13e7afb","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:46Z","behandlingstema":"NASJONAL","_rev":"FuD004taptHFqBZyEy5by3","begrunnelsetype":"INNVILGET","visningsnavn":"46. Forvaring gift","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["UTVIDET_BARNETRYGD"],"_createdAt":"2021-10-25T07:15:00Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"","_key":"823df272dd45"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"ee69aed7f38d","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd selv om du fortsatt er gift fordi du bor alene med ","_key":"9974d26fefcc"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"8b5bf970469d"},{"_type":"span","marks":[],"text":". Ektefellen din er i forvaring i seks måneder eller mer fra ","_key":"8a458056bd3d"},{"_key":"6a50f8f833e5","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":". ","_key":"b0c16ffb93e9"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"f90e4d53d18d","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at forvaringen startet.","_key":"5baf61250341"}],"_type":"block","style":"normal","_key":"d47b3f320c0a","markDefs":[]}],"hjemler":["2","9","11"]},{"apiNavn":"reduksjonVurderingFlyttetSammenMedEktefelle","vedtakResultat":"REDUKSJON","_createdAt":"2021-10-22T12:37:14Z","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:46Z","bokmaal":[{"_type":"block","style":"normal","_key":"33a8a3c7528c","markDefs":[],"children":[{"text":"Barnetrygden endres fordi vi har kommet fram til at du har flyttet sammen med ektefellen din i ","_key":"804b839dbd10","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"c08915d187b5"},{"_type":"span","marks":[],"text":". Barnetrygden endres fra måneden etter at dere flyttet sammen.","_key":"5bf9a145f7e9"}]}],"_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"style":"normal","_key":"d4ffa0ff9a08","markDefs":[],"children":[{"_key":"5c250c3b8282","_type":"span","marks":[],"text":"Barnetrygda er endra fordi vi har kome fram til at du har flytta saman med ektefellen din i "},{"_key":"627ef3ad015d","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":". Barnetrygda er endra frå månaden etter at de flytta saman.","_key":"9de84104915e"}],"_type":"block"}],"_id":"56587772-c047-40cd-a40b-2ab6da548f09","_rev":"FuD004taptHFqBZyEy5by3","periodeType":"UTBETALING","tema":"FELLES","navnISystem":"Vurdering flyttet sammen med ektefelle","hjemler":["2","9","11"],"visningsnavn":"29. Vurdering flyttet sammen med ektefelle","begrunnelsetype":"REDUKSJON","valgbarhet":"STANDARD"},{"_rev":"FuD004taptHFqBZyEy5by3","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får småbarnstillegg fordi du har barn under tre år, utvida barnetrygd og full overgangsstønad.","_key":"0b3bb1d96490"}],"_type":"block","style":"normal","_key":"b88869bd7d16"}],"valgbarhet":"AUTOMATISK","_id":"57272e71-0fe6-4aea-96a1-655968bae47f","_updatedAt":"2023-09-25T10:15:46Z","tema":"FELLES","utvidetBarnetrygdTriggere":["SMÅBARNSTILLEGG"],"navnISystem":"Småbarnstillegg","apiNavn":"innvilgetSmaabarnstillegg","_createdAt":"2021-11-08T15:09:42Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får småbarnstillegg fordi du har barn under tre år, utvidet barnetrygd og full overgangsstønad.","_key":"ee6f0a057825"}],"_type":"block","style":"normal","_key":"f301eae69074","markDefs":[]}],"hjemler":["2","10"],"visningsnavn":"59. Småbarnstillegg","_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","mappe":["INNVILGET"]},{"_createdAt":"2022-11-03T12:53:47Z","_updatedAt":"2023-09-25T10:15:46Z","bokmaal":[{"_type":"block","style":"normal","_key":"4f9a90214324","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter nasjonale regler for barn født ","_key":"f035ceca09570"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"193170a6dbb1"},{"_type":"span","marks":[],"text":" fordi hele familien er bosatt i Norge.","_key":"0e5d0e09a0c2"}]}],"navnISystem":"Overgang EØS til nasjonal norsk/nordisk familie","apiNavn":"innvilgetOvergangEosTilNasjonalNorskNordiskFamilie","hjemler":["2","4","11"],"rolle":["SOKER","BARN"],"begrunnelsetype":"INNVILGET","_id":"5763bdb0-deda-4262-be1d-6eb7b2a0ede0","visningsnavn":"94. Overgang EØS til nasjonal norsk/nordisk familie","behandlingstema":"NASJONAL","vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"NASJONAL","nynorsk":[{"markDefs":[],"children":[{"_key":"e87a97083bf40","_type":"span","marks":[],"text":"Du får barnetrygd etter nasjonale reglar for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"1c7fd5731fa3"},{"_type":"span","marks":[],"text":" fordi heile familien er busett i Noreg.","_key":"1cbc5d208e42"}],"_type":"block","style":"normal","_key":"a896c78aa952"}],"_rev":"FuD004taptHFqBZyEy5by3","periodeType":"UTBETALING","valgbarhet":"STANDARD","_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"mappe":["INNVILGET"]},{"_updatedAt":"2023-09-25T10:23:47Z","navnISystem":"Oppdatert konto-opplysninger","_rev":"BtltdVb0HP4g4WJfnr50JJ","_type":"begrunnelse","begrunnelsetype":"FORTSATT_INNVILGET","tema":"FELLES","_createdAt":"2022-04-01T13:42:56Z","mappe":["FORTSATT_INNVILGET"],"visningsnavn":"38. Oppdatert konto-opplysninger","hjemler":["2","12"],"vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får fortsatt barnetrygd fordi du har oppdatert konto-opplysningane dine.","_key":"1dc8e63e8d6f0"}],"_type":"block","style":"normal","_key":"bff5c85edd81","markDefs":[]}],"valgbarhet":"STANDARD","_id":"58121df1-8f0d-440f-8d90-b5d60d83f6c8","bokmaal":[{"style":"normal","_key":"a5af92981b27","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får fortsatt barnetrygd fordi du har oppdatert konto-opplysningene dine.","_key":"6622f167bfdc0"}],"_type":"block"}],"apiNavn":"fortsattInnvilgetOppdatertKontoOpplysninger"},{"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi det ikkje er dokumentert at du er skilt.","_key":"4749c26019df"}],"_type":"block","style":"normal","_key":"4ceccf309acb"}],"valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi det ikke er dokumentert at du er skilt.","_key":"e9dcf561d344"}],"_type":"block","style":"normal","_key":"0f620e3b7450"}],"visningsnavn":"35. Ikke dokumentert skilt","_rev":"FuD004taptHFqBZyEy7VQo","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:29:24Z","apiNavn":"avslagIkkeDokumentertSkilt","_type":"begrunnelse","_id":"5bbe690f-a91c-461e-8c25-e54465c7fb48","tema":"FELLES","_createdAt":"2021-10-22T08:13:36Z","vedtakResultat":"IKKE_INNVILGET","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","navnISystem":"Ikke dokumentert skilt","hjemler":["9"]},{"_updatedAt":"2023-09-25T10:15:46Z","apiNavn":"innvilgetAleneFraFodsel","visningsnavn":"41. Alene fra fødsel","_rev":"FuD004taptHFqBZyEy5by3","_type":"begrunnelse","begrunnelsetype":"INNVILGET","tema":"FELLES","vedtakResultat":"INNVILGET_ELLER_ØKNING","_createdAt":"2021-10-21T10:07:19Z","mappe":["INNVILGET"],"bokmaal":[{"style":"normal","_key":"4db490eaea5e","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"407ef20f9be0"},{"_type":"valgfeltV2","_key":"e442b534df8b","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"}},{"_type":"span","marks":[],"text":" utvidet barnetrygd fordi du bor alene med ","_key":"1d4fa7f43445"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"94e0cd49d993"},{"_key":"537819ee365f","_type":"span","marks":[],"text":" fra "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"1077b2fbcdaa"},{"_key":"648fd14a826a","_type":"span","marks":[],"text":". "},{"skalHaStorForbokstav":true,"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"},"_type":"valgfeltV2","_key":"7bff23c05404"},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at du fikk barn.","_key":"8c4a75cbdbc8"}],"_type":"block"}],"navnISystem":"Alene fra fødsel","hjemler":["2","9","11"],"behandlingstema":"NASJONAL","periodeType":"UTBETALING","valgbarhet":"STANDARD","vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"9d8f91569679"},{"_key":"5fb401ac2b28","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"text":" utvida barnetrygd fordi du bur aleine med ","_key":"1c4bbe70f382","_type":"span","marks":[]},{"_key":"d3e6320b8c48","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" frå ","_key":"06ef22fedd9d"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"42aec247c6d0"},{"_type":"span","marks":[],"text":". ","_key":"769ec602afd1"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"d7519ab57cef","skalHaStorForbokstav":true},{"_key":"2d8bc77c5ed8","_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at du fekk barn."}],"_type":"block","style":"normal","_key":"7c1fdaad2618"}],"_id":"5bd0135d-bf15-4988-8519-ea3a404c52b7"},{"hjemler":["9"],"visningsnavn":"34. Vurdering ikke meklingsattest","_rev":"h26rAhFEYSUtDGXJE0t0yw","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","tema":"FELLES","apiNavn":"avslagVurderingIkkeMeklingsattest","_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:29:21Z","bokmaal":[{"style":"normal","_key":"520d55015a4e","markDefs":[],"children":[{"_key":"d5533f89dde9","_type":"span","marks":[],"text":"Utvidet barnetrygd fordi vi har kommet fram til at du ikke har gyldig meklingsattest."}],"_type":"block"}],"navnISystem":"Vurdering ikke meklingsattest","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi vi har kome fram til at du ikkje har gyldig meklingsattest.","_key":"0cc796b80e41"}],"_type":"block","style":"normal","_key":"b2770eefa5b0"}],"_createdAt":"2021-10-22T08:00:14Z","valgbarhet":"STANDARD","_id":"5e2a16b9-f3f9-41ab-834c-3ce25de31f52","begrunnelsetype":"AVSLAG"},{"vilkaar":["BOR_MED_SOKER"],"_id":"5f516876-c645-448c-b2e0-a13f1c7f406f","hjemler":["2","12"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","_rev":"FuD004taptHFqBZyEy5by3","behandlingstema":"NASJONAL","_type":"begrunnelse","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får heile barnetrygda for barn fødd ","_key":"c5b98ebf4774"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"dfbb8e3067e3"},{"_key":"d86db5df90d7","_type":"span","marks":[],"text":"som har skriftleg avtale om delt bustad. Den andre forelderen har ikkje søkt om delt barnetrygd. Barnetrygda kan først delast frå månaden etter at begge foreldra har søkt om det."}],"_type":"block","style":"normal","_key":"a53cf614ecc9"}],"_createdAt":"2022-01-20T14:26:17Z","mappe":["INNVILGET"],"bokmaal":[{"markDefs":[],"children":[{"text":"Du får hele barnetrygden for barn født ","_key":"956a5324df08","_type":"span","marks":[]},{"_key":"b506fcc412b2","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" som har skriftlig avtale om delt bosted. Den andre forelderen har ikke søkt om delt barnetrygd. Barnetrygden kan først deles fra måneden etter at begge foreldrene har søkt om det.","_key":"3e10de972863"}],"_type":"block","style":"normal","_key":"010dfefc306a"}],"visningsnavn":"65. Annen forelder ikke søkt delt barnetrygd enkeltbarn","apiNavn":"innvilgetAnnenForelderIkkeSoktDeltBarnetrygdEnkeltbarn","borMedSokerTriggere":["DELT_BOSTED_SKAL_IKKE_DELES"],"begrunnelsetype":"INNVILGET","tema":"NASJONAL","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:46Z","navnISystem":"Annen forelder ikke søkt delt barnetrygd enkeltbarn"},{"tema":"FELLES","valgbarhet":"STANDARD","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:30:00Z","bokmaal":[{"_key":"bb78f42a7e18","markDefs":[],"children":[{"marks":[],"text":"Utvidet barnetrygd fordi forvaringsdommen til ektefellen din er under seks måneder.","_key":"e6d5ef5eef63","_type":"span"}],"_type":"block","style":"normal"}],"navnISystem":"Forvaring under 6 måneder ektefelle","visningsnavn":"44. Forvaring under 6 måneder ektefelle","_rev":"h26rAhFEYSUtDGXJE0t1vs","_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi forvaringsdommen til ektefellen din er under seks månader.","_key":"266787e12736"}],"_type":"block","style":"normal","_key":"83947a158cfd"}],"_createdAt":"2021-10-22T09:07:38Z","_id":"5ff10a39-430f-408b-b52b-28a05d2d2cd3","hjemler":["9"],"begrunnelsetype":"AVSLAG","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","apiNavn":"avslagForvaringUnder6MaanederEktefelle"},{"_id":"60da42d3-62a4-47d6-97ef-7bb03cec5240","_rev":"FuD004taptHFqBZyEy5by3","vedtakResultat":"REDUKSJON","borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"UTBETALING","_createdAt":"2021-10-26T10:18:06Z","navnISystem":"Vurdering foreldrene bor sammen","visningsnavn":"44. Vurdering foreldrene bor sammen","apiNavn":"reduksjonVurderingForeldreneBorSammen","vilkaar":["BOR_MED_SOKER"],"begrunnelsetype":"REDUKSJON","tema":"NASJONAL","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi vi har kome fram til at du har flytta saman med den andre forelderen til barn fødd ","_key":"4b49a1818284"},{"_type":"flettefelt","_key":"a389de69be84","flettefelt":"barnasFodselsdatoer"},{"_key":"75d3378b3ddf","_type":"span","marks":[],"text":" frå "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"9a59c47da25d"},{"_type":"span","marks":[],"text":".","_key":"0b2504db7cee"}],"_type":"block","style":"normal","_key":"9e02360796c9","markDefs":[]}],"valgbarhet":"STANDARD","mappe":["REDUKSJON"],"hjemler":["2","11"],"_type":"begrunnelse","_updatedAt":"2023-09-25T10:15:46Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi vi har kommet fram til at du har flyttet sammen med den andre forelderen til barn født ","_key":"0d154bec8eaf"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"5deaf4f1779b"},{"_type":"span","marks":[],"text":" fra ","_key":"c0525e27c731"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"cd19f68cd8d1"},{"_type":"span","marks":[],"text":".","_key":"06b421eb7ee7"}],"_type":"block","style":"normal","_key":"14eeb32cbc92"}]},{"hjemler":["2","4","11"],"visningsnavn":"17. Uenighet om opphør av avtale om delt bosted","_rev":"FuD004taptHFqBZyEy5by3","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","valgbarhet":"STANDARD","vilkaar":["BOR_MED_SOKER"],"_id":"60fa4086-2528-4989-841f-1575e690d437","_updatedAt":"2023-09-25T10:15:46Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du og den andre forelderen er uenige om avtalen om delt bosted. Vi har kommet fram til at barn født ","_key":"c4a446e9d972"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"30de4ca1f6ac"},{"text":" bor fast hos deg fra ","_key":"43e16982cd37","_type":"span","marks":[]},{"_key":"930d49e59c54","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":". ","_key":"47e8779e54a8"}],"_type":"block","style":"normal","_key":"89ac4fd63186","markDefs":[]},{"_key":"049830360d2c","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Når dere er uenige om avtalen om delt bosted, kan hele barnetrygden bare utbetales til deg fra og med måneden etter at vi fikk søknaden din.","_key":"08963045e54a0"}],"_type":"block","style":"normal"}],"navnISystem":"Uenighet om opphør av avtale om delt bosted","apiNavn":"innvilgetUenighetOmOpphorAvAvtaleOmDeltBosted","behandlingstema":"NASJONAL","tema":"NASJONAL","_createdAt":"2021-08-27T09:04:06Z","mappe":["INNVILGET"],"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","nynorsk":[{"markDefs":[],"children":[{"text":"Du og den andre forelderen er usamde om avtalen om delt bustad. Vi har kome fram til at barn fødd ","_key":"412cf35a7aed","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"fa575a0dcb29"},{"_type":"span","marks":[],"text":" bur fast hos deg frå ","_key":"5499092d039b"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"ddf4a27985d4"},{"marks":[],"text":". ","_key":"c2a57ec10bad","_type":"span"}],"_type":"block","style":"normal","_key":"a43129a0aa79"},{"_type":"block","style":"normal","_key":"cc5c9b481f56","markDefs":[],"children":[{"text":"Når de er usamde om avtalen om delt bustad, kan heile barnetrygda berre bli utbetalt til deg frå og med månaden etter at vi fekk søknaden din.","_key":"41bf2e4362fb","_type":"span","marks":[]}]}]},{"valgbarhet":"STANDARD","_id":"620621de-a3b6-4b57-bc5c-f9b701e6a255","hjemler":["9"],"_rev":"FuD004taptHFqBZyEy6ZoR","vilkaar":["UTVIDET_BARNETRYGD"],"_updatedAt":"2023-09-25T10:23:14Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får utvidet barnetrygd fordi samboeren din fortsatt er i tvungent psykisk helsevern.","_key":"d6d318ad2d46"}],"_type":"block","style":"normal","_key":"3048ec3d01d0","markDefs":[]}],"navnISystem":"Tvungent psykisk helsevern samboer","nynorsk":[{"children":[{"_key":"94a04692580e","_type":"span","marks":[],"text":"Du får utvida barnetrygd fordi sambuaren din fortsatt er i tvungent psykisk helsevern."}],"_type":"block","style":"normal","_key":"1f66afe25567","markDefs":[]}],"mappe":["FORTSATT_INNVILGET"],"apiNavn":"fortsattInnvilgetTvungentPsykiskHelsevernSamboer","_type":"begrunnelse","tema":"FELLES","begrunnelsetype":"FORTSATT_INNVILGET","_createdAt":"2021-10-25T10:19:04Z","visningsnavn":"28. Tvungent psykisk helsevern samboer","vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET"},{"hjemler":["2","9","11"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"FELLES","bokmaal":[{"_type":"block","style":"normal","_key":"988f29717b69","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"85acdc8718b6"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"596158109177","skalHaStorForbokstav":false},{"marks":[],"text":" utvidet barnetrygd fordi du bor alene med ","_key":"dc6b62e10f860","_type":"span"},{"_key":"ca43946df947","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". Forholdet er avsluttet og dere har ikke bodd sammen i minst seks måneder i ","_key":"fab5b3f54681"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"229e5dca1430"},{"_type":"span","marks":[],"text":". ","_key":"a926adcf341c"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"e06ed8e0930c","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at samlivsbruddet har vart i seks måneder.","_key":"11a1dfa84dac"}]}],"apiNavn":"innvilgetFaktiskSeparasjonSeparertEtterpaa","_rev":"FuD004taptHFqBZyEy5by3","periodeType":"UTBETALING","nynorsk":[{"style":"normal","_key":"97ef4e1c1cb4","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"cdbb4e5c5349"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"0bbda5678c78","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi du bur aleine med ","_key":"05d7256739600"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"bc2c7b9eee7a"},{"_type":"span","marks":[],"text":". Forholdet er avslutta og de har ikkje budd saman i minst seks månader i ","_key":"bf7314f0aa2e"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"c9729df1880e"},{"_type":"span","marks":[],"text":". ","_key":"057a4a91549b"},{"_type":"valgfeltV2","_key":"e7b9eab332d9","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"}},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at samlivsbruddet har vart i seks månader.","_key":"d8531ffe4df7"}],"_type":"block"}],"_updatedAt":"2023-09-25T10:15:46Z","visningsnavn":"76. Faktisk separasjon - separert etterpå","behandlingstema":"NASJONAL","_type":"begrunnelse","begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","_id":"63e27167-119d-44a4-8ae5-252683226582","mappe":["INNVILGET"],"navnISystem":"Faktisk separasjon - separert etterpå","_createdAt":"2022-03-18T14:26:43Z","vilkaar":["UTVIDET_BARNETRYGD"]},{"visningsnavn":"7. Bosatt i Norge unntatt medlemskap ","rolle":["SOKER","BARN"],"_createdAt":"2021-08-30T13:14:06Z","valgbarhet":"STANDARD","bosattIRiketTriggere":["MEDLEMSKAP"],"_id":"647c27f4-5a11-4d82-87ff-c08cffd58723","_updatedAt":"2023-09-25T10:26:04Z","behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr5Ago","_type":"begrunnelse","tema":"NASJONAL","begrunnelsetype":"AVSLAG","mappe":["AVSLAG"],"bokmaal":[{"style":"normal","_key":"2e6e1a45cf90","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd fordi ","_key":"21c8e510122e","_type":"span"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"138249b79e9c"},{"marks":[],"text":" ikke er medlem av folketrygden","_key":"0d6f7a0895a9","_type":"span"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"d4e33a128e25"},{"text":".","_key":"e7960a4fec07","_type":"span","marks":[]}],"_type":"block"}],"navnISystem":"Bosatt i Norge unntatt medlemskap","hjemler":["2","4"],"vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","apiNavn":"avslagMedlemIFolketrygden","vilkaar":["BOSATT_I_RIKET"],"nynorsk":[{"_type":"block","style":"normal","_key":"23230588c711","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd fordi ","_key":"0a909533b4d7"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"96a4a03fe260"},{"_type":"span","marks":[],"text":" ikkje er medlem av folketrygda","_key":"f4f3767699a2"},{"_key":"7ea5fc97357c","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"_type":"span","marks":[],"text":".","_key":"a4696cd07373"}]}]},{"mappe":["FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:21:32Z","navnISystem":"Tredjelandsborger barn fortsatt lovlig opphold i Norge","hjemler":["2","4","11"],"visningsnavn":"2C. Tredjelandsborger barn fortsatt lovlig opphold i Norge","rolle":["BARN"],"_id":"653994be-8c22-4669-932b-143636fc6458","apiNavn":"fortsattInnvilgetBarnLovligOppholdOppholdstillatelse","begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","nynorsk":[{"_key":"83cb01424c45","markDefs":[],"children":[{"_key":"abf58e1ae005","_type":"span","marks":[],"text":"Du får barnetrygd fordi "},{"_key":"181a0349336a","_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse"},{"text":" fortsatt har opphaldsløyve.","_key":"0814ab229d2e","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"_rev":"h26rAhFEYSUtDGXJE0si7N","_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","_createdAt":"2021-08-30T13:00:11Z","valgbarhet":"STANDARD","vilkaar":["LOVLIG_OPPHOLD"],"periodeType":"FORTSATT_INNVILGET","bokmaal":[{"_key":"756bd1de0e1b","markDefs":[],"children":[{"_key":"325bd9650598","_type":"span","marks":[],"text":"Du får barnetrygd fordi "},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"6138275fe450"},{"_type":"span","marks":[],"text":" fortsatt har oppholdstillatelse.","_key":"f28f3d732fed"}],"_type":"block","style":"normal"}]},{"vilkaar":["UTVIDET_BARNETRYGD"],"_id":"6551ddf9-bfb6-4674-be89-b868bcb8a412","apiNavn":"reduksjonSamboerIkkeIForvaring","vedtakResultat":"REDUKSJON","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi sambuaren din ikkje lenger er i forvaring frå ","_key":"d93b0851a896"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"7a9f3ae684f5"},{"_type":"span","marks":[],"text":".","_key":"049f8dbd72a1"}],"_type":"block","style":"normal","_key":"ea868b8028f8","markDefs":[]}],"_createdAt":"2021-10-22T12:53:54Z","_updatedAt":"2023-09-25T10:15:46Z","navnISystem":"Samboer ikke i forvaring","hjemler":["2","9","11"],"_rev":"FuD004taptHFqBZyEy5by3","periodeType":"UTBETALING","tema":"FELLES","bokmaal":[{"children":[{"text":"Barnetrygden endres fordi samboeren din ikke lenger er i forvaring fra ","_key":"946c742d1631","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"2aa65e707b45"},{"_type":"span","marks":[],"text":".","_key":"5feff4f94297"}],"_type":"block","style":"normal","_key":"994edaf8013a","markDefs":[]}],"visningsnavn":"33. Samboer ikke i forvaring","_type":"begrunnelse","begrunnelsetype":"REDUKSJON","valgbarhet":"STANDARD","mappe":["REDUKSJON"]},{"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["UNDER_18_ÅR"],"periodeType":"UTBETALING","ovrigeTriggere":["ALLTID_AUTOMATISK"],"navnISystem":"Nyfødt barn - første barn","apiNavn":"innvilgetFodselshendelseNyfodtBarnForste","hjemler":["2","4","11","14"],"visningsnavn":"Fødselshendelse, Nyfødt barn - første barn","behandlingstema":"NASJONAL","begrunnelsetype":"INNVILGET","tema":"NASJONAL","_createdAt":"2021-08-27T11:08:53Z","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:46Z","valgbarhet":"AUTOMATISK","_rev":"FuD004taptHFqBZyEy5by3","nynorsk":[{"_type":"block","style":"normal","_key":"59702553c6f0","markDefs":[],"children":[{"text":"Du får barnetrygd fordi du har fått barn og ","_key":"29ec9f571cf5","_type":"span","marks":[]},{"_type":"valgfeltV2","_key":"97822245904a","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"marks":[],"text":" bur saman med deg. Du får barnetrygda frå månaden etter at ","_key":"b98c209a2415","_type":"span"},{"_type":"valgfeltV2","_key":"0c930a9ef652","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"}},{"_key":"bd09c4359ebd","_type":"span","marks":[],"text":" er fødd."}]}],"_id":"65e32712-4b53-4927-ac5a-c6dd367c6f69","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har fått barn og ","_key":"393058a91243"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"a05061b6d269","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" bor sammen med deg. Du får barnetrygden fra måneden etter at ","_key":"bc58ecc1075f"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"27d607e002f0","skalHaStorForbokstav":false},{"text":" ble født.","_key":"92fc9eb90132","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"d17b99ba9bad"}]},{"bosattIRiketTriggere":["MEDLEMSKAP"],"_createdAt":"2021-09-24T11:21:11Z","apiNavn":"innvilgetHeleFamilienFrivilligMedlem","tema":"NASJONAL","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"f45bcb89b677"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"e7c44f3f3ab6"},{"_type":"span","marks":[],"text":" under opphaldet i utlandet fordi heile familien er frivillig medlem i folketrygda frå ","_key":"b3256e9efaf7"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"99a66a993116"},{"_type":"span","marks":[],"text":".","_key":"362c8663ca4c"}],"_type":"block","style":"normal","_key":"1974e2b9de00"}],"valgbarhet":"STANDARD","_id":"666bbf96-7bf0-4899-97a3-6432d87c8c06","_updatedAt":"2023-09-25T10:15:46Z","begrunnelsetype":"INNVILGET","navnISystem":"Hele familien frivillig medlem","hjemler":["4","5"],"visningsnavn":"22. Hele familien frivillig medlem","_rev":"FuD004taptHFqBZyEy5by3","hjemlerFolketrygdloven":["2-8"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"rolle":["SOKER","BARN"],"mappe":["INNVILGET"],"bokmaal":[{"children":[{"_key":"28f6aac09b0c","_type":"span","marks":[],"text":"Du får barnetrygd "},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"ed0d7655de0d"},{"text":" under oppholdet i utlandet fordi hele familien er frivillig medlem i folketrygden fra ","_key":"6a5b62f32aab","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"370a69459000"},{"_type":"span","marks":[],"text":".","_key":"c6f95ffad080"}],"_type":"block","style":"normal","_key":"7a148f5650b4","markDefs":[]}]},{"nynorsk":[{"_type":"block","style":"normal","_key":"a62b7c75e71f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får fortsatt barnetrygd fordi du er registrert med adresse.","_key":"f82b107f75090"}]}],"mappe":["FORTSATT_INNVILGET"],"bokmaal":[{"_key":"8db1598dcfd4","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får fortsatt barnetrygd fordi du er registrert med adresse.","_key":"e48c4306c9250"}],"_type":"block","style":"normal"}],"_type":"begrunnelse","periodeType":"FORTSATT_INNVILGET","_createdAt":"2022-04-01T13:44:21Z","begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","vedtakResultat":"INGEN_ENDRING","_id":"667baaa9-ec73-4d64-a3f2-85b69f3bc8bd","hjemler":["2","12"],"visningsnavn":"39. Adresse registrert","_rev":"h26rAhFEYSUtDGXJE0spBK","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:23:55Z","navnISystem":"Adresse registrert","apiNavn":"fortsattInnvilgetAdresseRegistrert"},{"_type":"begrunnelse","periodeType":"UTBETALING","valgbarhet":"STANDARD","_id":"679a6db7-42cb-4978-ae11-2f62a36648b8","_updatedAt":"2023-09-25T10:15:46Z","navnISystem":"Barn med samboer før bodd sammen 12 mnd","apiNavn":"reduksjonBarnMedSamboerForBoddSammen12Mnd","vedtakResultat":"REDUKSJON","vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"REDUKSJON","nynorsk":[{"children":[{"_key":"d637b436e4d70","_type":"span","marks":[],"text":"Barnetrygda er endra fordi du og sambuaren din har fått barn "},{"_key":"c9b3c1efeb68","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"351e2113c9e9"}],"_type":"block","style":"normal","_key":"d90b01f9d583","markDefs":[]}],"mappe":["REDUKSJON"],"bokmaal":[{"style":"normal","_key":"410a0b385dfa","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi du og samboeren din har fått barn ","_key":"1c4b3f01a9b40"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"2b3beca0d487"},{"marks":[],"text":".","_key":"8b9681d6cf29","_type":"span"}],"_type":"block"}],"hjemler":["2","9","11"],"visningsnavn":"67. Barn med samboer før bodd sammen 12 mnd","tema":"FELLES","_createdAt":"2022-08-19T12:49:49Z","behandlingstema":"NASJONAL","_rev":"FuD004taptHFqBZyEy5by3"},{"nynorsk":[{"style":"normal","_key":"68119ad07e0d","markDefs":[],"children":[{"text":"Barnetrygd for barn fødd ","_key":"9e4551545191","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"74a1bb8cbc61"},{"_key":"6c545a5fc735","_type":"span","marks":[],"text":" fordi "},{"skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},"_type":"valgfeltV2","_key":"99824c6332f0"},{"_type":"span","marks":[],"text":" ikkje var busett i Noreg.","_key":"ec59a1769487"}],"_type":"block"}],"bokmaal":[{"style":"normal","_key":"a7a17ed08f3a","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"76b3368cb3e4"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"c3e7a38ffdc1"},{"text":" fordi ","_key":"33570f5d1d11","_type":"span","marks":[]},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"2f7d80cdd753","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikke var bosatt i Norge.","_key":"f6dd4c918737"}],"_type":"block"}],"navnISystem":"Ikke bosatt i Norge","visningsnavn":"31. Ikke bosatt i Norge","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","begrunnelsetype":"OPPHØR","tema":"NASJONAL","_createdAt":"2021-11-05T10:14:19Z","ovrigeTriggere":["GJELDER_FØRSTE_PERIODE"],"apiNavn":"opphorIkkeBosattINorge","_type":"begrunnelse","_id":"67da4eee-8e5c-4a62-96c6-0463642cf4ea","mappe":["OPPHØR"],"_updatedAt":"2023-09-25T10:35:36Z","hjemler":["2","4"],"valgbarhet":"STANDARD","rolle":["SOKER","BARN"],"_rev":"h26rAhFEYSUtDGXJE0v0MM","vilkaar":["BOSATT_I_RIKET"]},{"apiNavn":"endretUtbetalingDeltBostedFullUtbetalingForSoknad","visningsnavn":"2. Delt bosted - full utbetaling før søknad","mappe":["ENDRET_UTBETALINGSPERIODE"],"begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","nynorsk":[{"markDefs":[],"children":[{"_key":"3443dcb661c3","_type":"span","marks":[],"text":"Du har allereie fått full barnetrygd med "},{"flettefelt":"belop","_type":"flettefelt","_key":"8d6dfb2ccc7a"},{"_type":"span","marks":[],"text":" kroner i månaden i denne perioden. Vi krev ikkje at du betaler tilbake barnetrygda. Du og den andre forelderen må sjølv bli einige om korleis de vil dele barnetrygda som er utbetalt.","_key":"0f5cc6485530"}],"_type":"block","style":"normal","_key":"aaddefc616e4"}],"hjemler":["2","12"],"_rev":"FuD004taptHFqBZyEy5by3","_createdAt":"2021-10-18T12:08:12Z","_updatedAt":"2023-09-25T10:15:46Z","bokmaal":[{"markDefs":[],"children":[{"_key":"e1487b81a71f","_type":"span","marks":[],"text":"Du har allerede fått full barnetrygd med "},{"flettefelt":"belop","_type":"flettefelt","_key":"96c9af12fdaa"},{"_type":"span","marks":[],"text":" kroner i måneden i denne perioden. Vi krever ikke at du betaler tilbake barnetrygden. Du og den andre forelderen må selv bli enige om hvordan dere vil fordele barnetrygden som er utbetalt.","_key":"91b06c9435f0"}],"_type":"block","style":"normal","_key":"5bffdac0bf17"}],"navnISystem":"Delt bosted - full utbetaling før søknad","_type":"begrunnelse","periodeType":"UTBETALING","endringsaarsaker":["DELT_BOSTED"],"endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"SKAL_UTBETALES","_id":"682e37c5-088d-463d-8e71-630cb1580ff1"},{"valgbarhet":"STANDARD","mappe":["REDUKSJON"],"hjemler":["4","5"],"behandlingstema":"NASJONAL","hjemlerFolketrygdloven":["2-5","2-8"],"vilkaar":["BOSATT_I_RIKET"],"periodeType":"UTBETALING","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi vi har kome fram til at den andre forelderen ikkje var medlem i folketrygda. Bur begge foreldra saman under opphaldet i utlandet, må de begge vere medlem i folketrygda for å få barnetrygd.","_key":"0fdff6744e9f"}],"_type":"block","style":"normal","_key":"aed19a4abec0","markDefs":[]}],"ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"],"navnISystem":"Vurdering den andre forelderen var ikke medlem","_createdAt":"2021-11-05T10:54:26Z","_id":"68b7379e-7034-4fa9-b697-7d0df2f21271","apiNavn":"reduksjonVurderingDenAndreForelderenVarIkkeMedlem","visningsnavn":"56. Vurdering den andre forelderen var ikke medlem","_type":"begrunnelse","tema":"NASJONAL","bosattIRiketTriggere":["MEDLEMSKAP"],"_updatedAt":"2023-09-25T10:15:46Z","_rev":"FuD004taptHFqBZyEy5by3","vedtakResultat":"REDUKSJON","rolle":["SOKER","BARN"],"begrunnelsetype":"REDUKSJON","bokmaal":[{"_type":"block","style":"normal","_key":"0bf4304036f7","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi vi har kommet fram til at den andre forelderen ikke var medlem i folketrygden. Bor begge foreldrene sammen under oppholdet i utlandet, må dere begge være medlem i folketrygden for å få barnetrygd.","_key":"7c736e4d475e"}]}]},{"visningsnavn":"26. Forvaring samboer","periodeType":"FORTSATT_INNVILGET","nynorsk":[{"_type":"block","style":"normal","_key":"e5e87ca88094","markDefs":[],"children":[{"text":"Du får utvida barnetrygd fordi sambuaren din fortsatt er i forvaring.","_key":"3611adc6a694","_type":"span","marks":[]}]}],"vilkaar":["UTVIDET_BARNETRYGD"],"_createdAt":"2021-10-25T10:15:41Z","mappe":["FORTSATT_INNVILGET"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utvidet barnetrygd fordi samboeren din fortsatt er i forvaring.","_key":"895c8fdd4dca"}],"_type":"block","style":"normal","_key":"1583f8ebf301"}],"apiNavn":"fortsattInnvilgetForvaringSamboer","hjemler":["9"],"vedtakResultat":"INGEN_ENDRING","_id":"6ab1b0d2-3a48-4067-b580-5867ee451b9c","_updatedAt":"2023-09-25T10:23:04Z","valgbarhet":"STANDARD","navnISystem":"Forvaring samboer","_rev":"BtltdVb0HP4g4WJfnr4x7J","_type":"begrunnelse","begrunnelsetype":"FORTSATT_INNVILGET","tema":"FELLES"},{"bosattIRiketTriggere":["MEDLEMSKAP"],"_updatedAt":"2023-09-25T10:15:46Z","_createdAt":"2021-09-24T12:05:23Z","valgbarhet":"STANDARD","_id":"6afecac1-d069-479d-9607-784cb1a02b79","mappe":["INNVILGET"],"bokmaal":[{"markDefs":[],"children":[{"_key":"c3a8ba4a213d","_type":"span","marks":[],"text":"Du får barnetrygd "},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"cdc6300102be"},{"_type":"span","marks":[],"text":" under oppholdet i utlandet. Vi har kommet fram til at hele familien fyller vilkårene for å bli pliktig medlem i folketrygden fra ","_key":"2d78e0d32aac"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"4d1629a0d769"},{"_type":"span","marks":[],"text":". Dette gir rett til barnetrygd fra Norge.","_key":"167257a3887d"}],"_type":"block","style":"normal","_key":"b64a26d9a47b"}],"visningsnavn":"26. Vurdering hele familien pliktig medlem","begrunnelsetype":"INNVILGET","hjemler":["4","5"],"_rev":"FuD004taptHFqBZyEy5by3","_type":"begrunnelse","periodeType":"UTBETALING","rolle":["SOKER","BARN"],"tema":"NASJONAL","navnISystem":"Vurdering hele familien pliktig medlem","apiNavn":"innvilgetVurderingHeleFamilienPliktigMedlem","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"1a5914a345f2"},{"_type":"valgReferanse","_key":"a8651d409694","_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170"},{"text":" under opphaldet i utlandet. Vi har kome fram til at heile familien fyller vilkåra for å bli pliktig medlem i folketrygda frå ","_key":"d8c55ce34de7","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"1df5927ce1f3"},{"_type":"span","marks":[],"text":". Dette gjev rett til barnetrygd frå Noreg.","_key":"28ad8fab27f0"}],"_type":"block","style":"normal","_key":"5d06dbaebf30","markDefs":[]}],"vilkaar":["BOSATT_I_RIKET"],"hjemlerFolketrygdloven":["2-5"],"vedtakResultat":"INNVILGET_ELLER_ØKNING"},{"nynorsk":[{"_type":"block","style":"normal","_key":"990a5ecced1f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har opphaldsrett som EØS-borgar. Familien bur i Noreg og sambuaren din jobbar her.","_key":"d288fc60843c0"}]}],"valgbarhet":"TILLEGGSTEKST","_id":"6b6d39a4-1acc-4b43-9a28-d748491df4c8","apiNavn":"innvilgetTilleggstekstEosBorgerSamboerJobber","hjemler":["2","4"],"_rev":"FuD004taptHFqBZyEy5by3","mappe":["INNVILGET"],"_type":"begrunnelse","tema":"NASJONAL","_createdAt":"2022-05-24T14:15:29Z","bokmaal":[{"_key":"e53e9658e788","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har oppholdsrett som EØS-borger. Familien bor i Norge og samboeren din jobber her.","_key":"3d564fc793d70"}],"_type":"block","style":"normal"}],"navnISystem":"Tilleggstekst EØS borger samboer jobber","behandlingstema":"NASJONAL","periodeType":"UTBETALING","rolle":["SOKER","BARN"],"begrunnelsetype":"INNVILGET","_updatedAt":"2023-09-25T10:15:46Z","visningsnavn":"85. Tilleggstekst EØS borger samboer jobber","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["BOSATT_I_RIKET","LOVLIG_OPPHOLD"]},{"tema":"NASJONAL","nynorsk":[{"_type":"block","style":"normal","_key":"98937f13f74a","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"a8b3713120400"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"1c604ba20d38"},{"_type":"span","marks":[],"text":" fordi barnet ikkje budde fast i institusjonen.","_key":"79e9af39768b"}]}],"_createdAt":"2022-11-04T08:26:10Z","_updatedAt":"2023-09-25T10:38:07Z","navnISystem":"Barn bodde ikke fast i institusjon","hjemler":["2","11"],"_rev":"BtltdVb0HP4g4WJfnr5q5J","vedtakResultat":"IKKE_INNVILGET","mappe":["INSTITUSJON","OPPHØR"],"bokmaal":[{"style":"normal","_key":"48f1022961a2","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"b467ec659e460","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"a7a5c3206080"},{"_type":"span","marks":[],"text":" fordi barnet ikke bodde fast i institusjonen.","_key":"08edc7cd710f"}],"_type":"block"}],"visningsnavn":"3. Barn bodde ikke fast i institusjon","behandlingstema":"NASJONAL_INSTITUSJON","_type":"begrunnelse","begrunnelsetype":"OPPHØR","_id":"6b9600ef-6ecf-46fe-8e4c-c648be1ddebb","periodeType":"INGEN_UTBETALING","valgbarhet":"SAKSPESIFIKK","ovrigeTriggere":["GJELDER_FØRSTE_PERIODE"],"apiNavn":"opphorBarnBoddeIkkeFastIInstitusjon","fagsakType":"INSTITUSJON","vilkaar":["BOR_MED_SOKER"]},{"begrunnelsetype":"REDUKSJON","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi ektefellen din ikkje lenger er i tvungent psykisk helsevern frå ","_key":"8e436e8c8a06"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"5f8495bff1c3"},{"_type":"span","marks":[],"text":".","_key":"d51e4eb61d28"}],"_type":"block","style":"normal","_key":"5b804410ae9e","markDefs":[]}],"bokmaal":[{"style":"normal","_key":"a796e4769100","markDefs":[],"children":[{"_key":"8581cba34b30","_type":"span","marks":[],"text":"Barnetrygden endres fordi ektefellen din ikke lenger er i tvungent psykisk helsevern fra "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"cc76f2e28c58"},{"_key":"2d9e4871577b","_type":"span","marks":[],"text":"."}],"_type":"block"}],"_rev":"FuD004taptHFqBZyEy5by3","_type":"begrunnelse","vedtakResultat":"REDUKSJON","vilkaar":["UTVIDET_BARNETRYGD"],"_createdAt":"2021-10-23T06:01:12Z","navnISystem":"Ektefelle ikke i tvungent psykisk helsevern","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:46Z","apiNavn":"reduksjonEktefelleIkkeITvungentPsykiskHelsevern","visningsnavn":"34. Ektefelle ikke i tvungent psykisk helsevern","periodeType":"UTBETALING","tema":"FELLES","_id":"6ce80722-acf2-47e3-bb5a-e6062b102909","hjemler":["2","9","11"]},{"begrunnelsetype":"OPPHØR","_id":"6cf2953f-0dae-4feb-b7da-af90525fc83c","visningsnavn":"13. Barn 18 år","_rev":"h26rAhFEYSUtDGXJE0unt5","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","nynorsk":[{"_type":"block","style":"normal","_key":"8abac82b87a3","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"61d893e39918"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"ff7a4797bd6e"},{"_type":"span","marks":[],"text":" fordi ","_key":"c45bfd48eb1e"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"e95162bd338c"},{"marks":[],"text":" er 18 år.","_key":"52c55154f505","_type":"span"}]}],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"58c3f3667a83"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"97ec4110dfc8"},{"_key":"dae0da8674c1","_type":"span","marks":[],"text":" fordi "},{"_key":"5dc7812e5652","_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse"},{"marks":[],"text":" er 18 år.\n","_key":"79cdb48c43bb","_type":"span"}],"_type":"block","style":"normal","_key":"ca8169d694a6"}],"apiNavn":"opphorUnder18Aar","hjemler":["2","11"],"_type":"begrunnelse","mappe":["OPPHØR"],"_updatedAt":"2023-09-25T10:32:00Z","navnISystem":"Barn 18 år","tema":"NASJONAL","_createdAt":"2021-08-30T11:56:36Z","valgbarhet":"STANDARD","vilkaar":["UNDER_18_ÅR"]},{"_id":"6d985a73-6dac-421e-b7ee-e4f1cfdfb422","_updatedAt":"2023-09-25T10:15:46Z","navnISystem":"Meklingsattest og vurdering egen husholdning","hjemler":["2","9","11"],"periodeType":"UTBETALING","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"4231ad96cb25"},{"_key":"997d2b1a4eea","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi vi har kome fram til at du bur aleine med ","_key":"f5fde4da3cab"},{"_type":"valgfeltV2","_key":"fb31643f3212","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":" frå ","_key":"b8888c0b1dc8"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"4ee404535a0c"},{"_type":"span","marks":[],"text":" og har meklingsattest. ","_key":"d13254f8ffe2"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"130828bd9276","skalHaStorForbokstav":true},{"marks":[],"text":" utvida barnetrygd frå månaden etter at du og den andre forelderen flytta frå kvarandre.","_key":"14d82e79ceaf","_type":"span"}],"_type":"block","style":"normal","_key":"9a5c63281cdf"}],"_createdAt":"2021-10-25T07:29:05Z","valgbarhet":"STANDARD","apiNavn":"innvilgetMeklingsattestOgVurderingEgenHusholdning","mappe":["INNVILGET"],"bokmaal":[{"_key":"d654346a0297","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"3ee510ef2b19"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"c1ccbb1faf75","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd fordi vi har kommet fram til at du bor alene med ","_key":"5f75c1693a66"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"6b15d63810ea","skalHaStorForbokstav":false},{"text":" fra ","_key":"9ab59bcdbfd6","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"10f558a453d1"},{"_type":"span","marks":[],"text":" og har meklingsattest. ","_key":"0727811bf0e8"},{"_type":"valgfeltV2","_key":"4633efb1f8c7","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"}},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at du og den andre forelderen flyttet fra hverandre.","_key":"be388bda5dc0"}],"_type":"block","style":"normal"}],"visningsnavn":"51. Meklingsattest og vurdering egen husholdning","vedtakResultat":"INNVILGET_ELLER_ØKNING","behandlingstema":"NASJONAL","_rev":"FuD004taptHFqBZyEy5by3","_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"INNVILGET","tema":"FELLES"},{"_rev":"h26rAhFEYSUtDGXJE0v0CC","borMedSokerTriggere":["DELT_BOSTED"],"tema":"NASJONAL","nynorsk":[{"_type":"block","style":"normal","_key":"a5c8f6eb65cb","markDefs":[],"children":[{"text":"Delt barnetrygd for barn fødd ","_key":"e3958a4b97aa","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"8475cc803195"},{"_type":"span","marks":[],"text":" fordi vi har fått ein avtale som seier at ","_key":"253be4329271"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"5b9664364be8","skalHaStorForbokstav":false},{"marks":[],"text":" skal bu fast hos den andre forelderen. ","_key":"f7fb24e4ff6d","_type":"span"}]}],"_id":"6e8725fc-e7a4-4312-8373-6c0fe5060cbe","_updatedAt":"2023-09-25T10:35:24Z","navnISystem":"Avtale om fast bosted","apiNavn":"opphorAvtaleOmFastBosted","hjemler":["2","11"],"mappe":["OPPHØR"],"_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"_createdAt":"2021-10-26T09:57:55Z","valgbarhet":"STANDARD","bokmaal":[{"_type":"block","style":"normal","_key":"401a69ed2c59","markDefs":[],"children":[{"marks":[],"text":"Delt barnetrygd for barn født ","_key":"ef38b1e3c2d1","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"6ec4b0c495bf"},{"text":" fordi vi har fått en avtale som sier at ","_key":"15f7b8e5fe3d","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"797ee2e1212d","skalHaStorForbokstav":false},{"marks":[],"text":" skal bo fast hos den andre forelderen. ","_key":"8c5aec605f72","_type":"span"}]}],"visningsnavn":"28. Avtale om fast bosted","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","begrunnelsetype":"OPPHØR"},{"tema":"FELLES","nynorsk":[{"children":[{"text":"Utvida barnetrygd fordi det ikkje er dokumentert at ektefellen din har vore forsvunnen i minst seks månader.","_key":"77cbc644d620","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"afb045a1cb84","markDefs":[]}],"navnISystem":"Ektefelle forsvunnet mindre enn 6 måneder","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"INGEN_UTBETALING","bokmaal":[{"_key":"3cf14d71bb7a","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi det ikke er dokumentert at ektefellen din har vært forsvunnet i minst seks måneder.","_key":"c4dde8753ac0"}],"_type":"block","style":"normal"}],"_rev":"FuD004taptHFqBZyEy7b4f","_createdAt":"2021-10-22T09:19:39Z","valgbarhet":"STANDARD","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:30:13Z","visningsnavn":"48. Ektefelle forsvunnet mindre enn 6 måneder","vedtakResultat":"IKKE_INNVILGET","_id":"6f10e463-5f1f-4dad-bc30-fe6f2530603b","begrunnelsetype":"AVSLAG","apiNavn":"avslagEktefelleForsvunnetMindreEnn6Maaneder","hjemler":["9"],"_type":"begrunnelse"},{"vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOR_MED_SOKER"],"nynorsk":[{"_key":"3a6f627c4344","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"29a35ccbaa8e0"},{"_key":"740d860ca75d","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi barnet ikkje bur fast i institusjonen.","_key":"4c89c3970717"}],"_type":"block","style":"normal"}],"_id":"701605fc-af0a-4508-bc90-0daa450f9c2a","fagsakType":"INSTITUSJON","_rev":"BtltdVb0HP4g4WJfnr5qKJ","behandlingstema":"NASJONAL_INSTITUSJON","begrunnelsetype":"AVSLAG","tema":"NASJONAL","_createdAt":"2022-11-04T08:50:37Z","_updatedAt":"2023-09-25T10:38:27Z","navnISystem":"Ikke bosatt i institusjon","visningsnavn":"1. Ikke bosatt i institusjon","mappe":["INSTITUSJON","AVSLAG"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"a5262a23c4690"},{"_key":"871a42fda7be","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi barnet ikke bor fast i institusjonen.","_key":"a67870cb1466"}],"_type":"block","style":"normal","_key":"1370e223a2ff"}],"_type":"begrunnelse","periodeType":"INGEN_UTBETALING","valgbarhet":"SAKSPESIFIKK","apiNavn":"avslagIkkeBosattIInstitusjon","hjemler":["2"]},{"begrunnelsetype":"REDUKSJON","tema":"NASJONAL","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"apiNavn":"reduksjonAnnenForelderIkkeLengerMedlemTrygdeavtale","behandlingstema":"NASJONAL","_type":"begrunnelse","bosattIRiketTriggere":["MEDLEMSKAP"],"_id":"71b31267-24ef-4e91-b418-12806433a0ad","navnISystem":"Annen forelder ikke lenger medlem trygdeavtale","_rev":"FuD004taptHFqBZyEy5by3","vilkaar":["BOSATT_I_RIKET"],"visningsnavn":"18. Annen forelder ikke lenger medlem trygdeavtale","periodeType":"UTBETALING","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi den andre forelderen til barn født ","_key":"5cdd180e0e5d"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"97cfcd5deeb8"},{"marks":[],"text":" ikke lenger er medlem i folketrygden etter trygdeavtale under oppholdet i utlandet som startet ","_key":"107621924c8f","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"628929376351"},{"_type":"span","marks":[],"text":".","_key":"200d4b24ad18"}],"_type":"block","style":"normal","_key":"448de4e566f1"}],"nynorsk":[{"_key":"f2c50f4bed37","markDefs":[],"children":[{"text":"Barnetrygda er endra fordi den andre forelderen til barn fødd ","_key":"80a74bbf68b0","_type":"span","marks":[]},{"_type":"flettefelt","_key":"be7f00141c73","flettefelt":"barnasFodselsdatoer"},{"marks":[],"text":" ikkje lenger er medlem i folketrygda etter trygdeavtale under opphaldet i utlandet som starta ","_key":"c917c0f7d9be","_type":"span"},{"_key":"5f8a90ba0fed","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"91fd7a052d6c"}],"_type":"block","style":"normal"}],"_createdAt":"2021-09-24T12:32:02Z","_updatedAt":"2023-09-25T10:15:46Z","hjemler":["2","4","22"],"vedtakResultat":"REDUKSJON","rolle":["BARN"]},{"begrunnelsetype":"OPPHØR","apiNavn":"opphorBarnHaddeIkkeOppholdstillatelseInstitusjon","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","_updatedAt":"2023-09-25T10:38:12Z","ovrigeTriggere":["GJELDER_FØRSTE_PERIODE"],"navnISystem":"Barn hadde ikke oppholdstillatelse","valgbarhet":"SAKSPESIFIKK","mappe":["INSTITUSJON","OPPHØR"],"_type":"begrunnelse","rolle":["SOKER","BARN"],"tema":"NASJONAL","_createdAt":"2022-11-04T08:43:55Z","_id":"71f329d7-5797-4579-a8a7-d0250708e07f","fagsakType":"INSTITUSJON","behandlingstema":"NASJONAL_INSTITUSJON","_rev":"h26rAhFEYSUtDGXJE0v6IC","nynorsk":[{"children":[{"_key":"0e7673238e480","_type":"span","marks":[],"text":"Barnetrygd for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"623ad091fa9b"},{"text":" fordi barnet ikkje hadde opphaldsløyve i Noreg. \n","_key":"b0022b27dc0e","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"089a61f7dd82","markDefs":[]}],"bokmaal":[{"style":"normal","_key":"05d2b5301db8","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"3b6a2c6bd1f30"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"bb1f85c72f0d"},{"_type":"span","marks":[],"text":" fordi barnet ikke hadde oppholdstillatelse i Norge. ","_key":"d27856078c0a"}],"_type":"block"}],"hjemler":["2","4"],"visningsnavn":"4. Barn hadde ikke oppholdstillatelse","vilkaar":["LOVLIG_OPPHOLD"]},{"tema":"NASJONAL","valgbarhet":"STANDARD","bokmaal":[{"style":"normal","_key":"b14d11ae7d49","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn født ","_key":"543da108d037"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"98a01203d57d"},{"_key":"9469f02bedfa","_type":"span","marks":[],"text":" fordi vi har kommet fram til at du bor sammen med den andre forelderen til "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"6570e196fca4","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Derfor kan ikke barnetrygden deles.","_key":"1eaa8f899a0c"}],"_type":"block"}],"_updatedAt":"2023-09-25T10:30:50Z","apiNavn":"avslagVurderingForeldreneBorSammen","hjemler":["2"],"vilkaar":["BOR_MED_SOKER"],"begrunnelsetype":"AVSLAG","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","nynorsk":[{"_type":"block","style":"normal","_key":"df4358fac9c3","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"c4b4d987a16a"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"02719370e36b"},{"text":" fordi vi har kome fram til at du bur saman med den andre forelderen til ","_key":"da9716a5adba","_type":"span","marks":[]},{"_type":"valgfeltV2","_key":"2bbcce9a6326","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_key":"011e744b9b42","_type":"span","marks":[],"text":". Derfor kan ikkje barnetrygda delast."}]}],"_id":"72aff57c-1bb7-4083-95a6-d8a36f4185c5","navnISystem":"Vurdering foreldrene bor sammen","visningsnavn":"61. Vurdering foreldrene bor sammen","_rev":"FuD004taptHFqBZyEy7fWh","_type":"begrunnelse","mappe":["AVSLAG"],"borMedSokerTriggere":["DELT_BOSTED"],"_createdAt":"2021-10-26T10:16:01Z"},{"navnISystem":"Bosatt i Norge unntatt medlemskap","visningsnavn":"25. Bosatt i Norge unntatt medlemskap","periodeType":"INGEN_UTBETALING","begrunnelsetype":"OPPHØR","hjemler":["4","5"],"_rev":"FuD004taptHFqBZyEy8OFr","_id":"7488d5fa-d0bb-4caa-b0f8-64fd7f440f2f","bokmaal":[{"_key":"850b50f5b352","markDefs":[],"children":[{"text":"Barnetrygd fordi ","_key":"fe0bcf512318","_type":"span","marks":[]},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"11d99f2ac3e5"},{"text":" ikke er medlem av folketrygden ","_key":"91d51d6d26f3","_type":"span","marks":[]},{"_key":"338f5232ea30","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"text":".","_key":"cfe6ebc7861e","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"_type":"begrunnelse","bosattIRiketTriggere":["MEDLEMSKAP"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd fordi ","_key":"7aac8ac65667"},{"_key":"3be0c308f992","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse"},{"_type":"span","marks":[],"text":" ikkje er medlem av folketrygda ","_key":"5107cdeacafb"},{"_type":"valgReferanse","_key":"9d3ad31f94ef","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"marks":[],"text":".","_key":"28e02c06b7b7","_type":"span"}],"_type":"block","style":"normal","_key":"06e1de0663a3"}],"_createdAt":"2021-09-24T12:12:52Z","valgbarhet":"STANDARD","mappe":["OPPHØR"],"apiNavn":"opphorBosattINorgeUnntattMedlemskap","vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOSATT_I_RIKET"],"rolle":["SOKER"],"tema":"NASJONAL","_updatedAt":"2023-09-25T10:35:00Z"},{"mappe":["REDUKSJON"],"apiNavn":"reduksjonEktefelleIkkeIForvaring","_type":"begrunnelse","periodeType":"UTBETALING","nynorsk":[{"children":[{"marks":[],"text":"Barnetrygda er endra fordi ektefellen din ikkje lenger er i forvaring frå ","_key":"067a0b276f9e","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"28e357fe754c"},{"_type":"span","marks":[],"text":".","_key":"15b8b04d16ec"}],"_type":"block","style":"normal","_key":"16dddaeb32b4","markDefs":[]}],"_id":"76075567-a2de-41c4-a489-48adbefcf4e6","navnISystem":"Ektefelle ikke i forvaring","visningsnavn":"32. Ektefelle ikke i forvaring","vilkaar":["UTVIDET_BARNETRYGD"],"_updatedAt":"2023-09-25T10:15:46Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi ektefellen din ikke lenger er i forvaring fra ","_key":"e75fe8f2b0de"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"855b9dd1a670"},{"_key":"d1f306d46c3f","_type":"span","marks":[],"text":"."}],"_type":"block","style":"normal","_key":"660a0f3bf378","markDefs":[]}],"hjemler":["2","9","11"],"_rev":"FuD004taptHFqBZyEy5by3","begrunnelsetype":"REDUKSJON","_createdAt":"2021-10-22T12:51:30Z","vedtakResultat":"REDUKSJON","tema":"FELLES","valgbarhet":"STANDARD"},{"visningsnavn":"41. Varig oppholdsrett EØS borger","periodeType":"FORTSATT_INNVILGET","_id":"767100af-cd03-483b-93ec-781f277848f8","bokmaal":[{"style":"normal","_key":"caf181b415d3","markDefs":[],"children":[{"text":"Du får fortsatt barnetrygd fordi du har varig oppholdsrett som EØS borger.","_key":"e7903e4e96d70","_type":"span","marks":[]}],"_type":"block"}],"navnISystem":"Varig oppholdsrett EØS borger","_rev":"h26rAhFEYSUtDGXJE0spRa","_type":"begrunnelse","mappe":["FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:24:02Z","behandlingstema":"NASJONAL","hjemler":["2","4"],"vedtakResultat":"INGEN_ENDRING","rolle":["SOKER","BARN"],"nynorsk":[{"_type":"block","style":"normal","_key":"27b94ab28240","markDefs":[],"children":[{"_key":"3794d0b4ff430","_type":"span","marks":[],"text":"Du får fortsatt barnetrygd fordi du har varig opphaldsrett som EØS borgar."}]}],"valgbarhet":"STANDARD","apiNavn":"fortsattInnvilgetVarigOppholdsrettEosBorger","begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","_createdAt":"2022-06-08T14:17:59Z","vilkaar":["LOVLIG_OPPHOLD"]},{"_rev":"FuD004taptHFqBZyEy5by3","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","navnISystem":"Fengsel samboer","_type":"begrunnelse","_updatedAt":"2023-09-25T10:15:46Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"861603729fe6"},{"_key":"a0ab82daab47","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" utvidet barnetrygd selv om du fortsatt er samboer fordi du bor alene med ","_key":"310047423eb4"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"3e84d715aa47","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Samboeren din er i fengsel i seks måneder eller mer fra ","_key":"e5b889599789"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"5a4185b73552"},{"_type":"span","marks":[],"text":". ","_key":"2279835dc41f"},{"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"},"_type":"valgfeltV2","_key":"68afdf065a17","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at oppholdet i fengselet startet.","_key":"1902a850ee8d"}],"_type":"block","style":"normal","_key":"01a2853f9f99"}],"hjemler":["2","9","11"],"tema":"FELLES","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"4e270ec35bb8"},{"skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"},"_type":"valgfeltV2","_key":"ef7ce71ff715"},{"_key":"b3d4eea0acff","_type":"span","marks":[],"text":" utvida barnetrygd sjølv om du fortsatt er sambuar fordi du bur aleine med "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"0478f5bf28ab","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Sambuaren din er i fengsel i seks månader eller meir frå ","_key":"520c729041b9"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"078d8d3c657d"},{"_type":"span","marks":[],"text":". ","_key":"bfd602c329ad"},{"skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"cd72b24ec664"},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at opphaldet i fengselet starta.","_key":"dd86c801964c"}],"_type":"block","style":"normal","_key":"8d4a22f392d7"}],"_createdAt":"2021-10-25T07:07:00Z","valgbarhet":"STANDARD","_id":"76cf0baf-7e7e-468f-ab72-f45fb1b2ef9a","mappe":["INNVILGET"],"apiNavn":"innvilgetFengselSamboer","visningsnavn":"43. Fengsel samboer","behandlingstema":"NASJONAL","begrunnelsetype":"INNVILGET"},{"bokmaal":[{"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"b9dd7d3d62aa","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"0e79fc71b07a"},{"_key":"98165aff5396","_type":"span","marks":[],"text":" fordi "},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"068c02199789"},{"_type":"span","marks":[],"text":" er over 18 år.","_key":"7b48a04937f6"}],"_type":"block","style":"normal","_key":"a3e0d9af56ee","markDefs":[]}],"apiNavn":"avslagUnder18Aar","periodeType":"INGEN_UTBETALING","nynorsk":[{"children":[{"_key":"3cc94035178c","_type":"span","marks":[],"text":"Barnetrygd for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"c1b57b9d1789"},{"text":" fordi ","_key":"46566db215d5","_type":"span","marks":[]},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"b630257a82e7"},{"_type":"span","marks":[],"text":" er over 18 år.","_key":"266eb27c8d04"}],"_type":"block","style":"normal","_key":"3a94c15fcfe7","markDefs":[]}],"_id":"7743cba6-f100-4fa4-8eb5-79c5452c95bf","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:26:13Z","hjemler":["11"],"visningsnavn":"9. Barn over 18 år","_rev":"BtltdVb0HP4g4WJfnr5AoJ","begrunnelsetype":"AVSLAG","navnISystem":"Barn over 18 år","_type":"begrunnelse","mappe":["AVSLAG"],"vedtakResultat":"IKKE_INNVILGET","vilkaar":["UNDER_18_ÅR"],"tema":"NASJONAL","_createdAt":"2021-08-30T13:21:57Z"},{"_rev":"FuD004taptHFqBZyEy7nI9","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","_id":"77d2fb40-5c89-4f9d-b420-20a400f151d2","_updatedAt":"2023-09-25T10:31:39Z","bokmaal":[{"_key":"727f301c1124","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"728be7c5661f","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"87956a4b744c"},{"_key":"d2eff3a427fc","_type":"span","marks":[],"text":" fordi vi har kommet fram til at "},{"_type":"valgfeltV2","_key":"b5d352512a79","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_key":"6022dbc882b3","_type":"span","marks":[],"text":" ikke lenger bor fast hos deg "},{"_type":"valgReferanse","_key":"e615614a0485","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"_type":"span","marks":[],"text":".","_key":"9880c7f01426"}],"_type":"block","style":"normal"}],"apiNavn":"opphorSokerHarIkkeFastOmsorg","vilkaar":["BOR_MED_SOKER"],"periodeType":"INGEN_UTBETALING","begrunnelsetype":"OPPHØR","tema":"NASJONAL","mappe":["OPPHØR"],"hjemler":["2","11"],"borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"_createdAt":"2021-08-30T10:31:29Z","valgbarhet":"STANDARD","navnISystem":"Søker har ikke lenger fast omsorg for barn (beredskapshjem, vurdering av fast bosted)","visningsnavn":"6. Søker har ikke lenger fast omsorg for barn (beredskapshjem, vurdering av fast bosted)","nynorsk":[{"_key":"83f1863e5983","markDefs":[],"children":[{"_key":"ec5b2c4e1d3e","_type":"span","marks":[],"text":"Barnetrygd for barn fødd "},{"_key":"a2eef4f2cfd7","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_key":"cff290606535","_type":"span","marks":[],"text":" fordi vi har kome fram til at \n"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"3d0681d84514","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikkje lenger bur fast hos deg ","_key":"685346fa3bab"},{"_type":"valgReferanse","_key":"833c7b4d0001","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"text":" .","_key":"bea87a26301c","_type":"span","marks":[]}],"_type":"block","style":"normal"}]},{"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden er endret fordi det har vært en satsendring.","_key":"d749218651ad0"}],"_type":"block","style":"normal","_key":"89b612747887"}],"apiNavn":"innvilgetSatsendringInstitusjon","fagsakType":"INSTITUSJON","visningsnavn":"2. Satsendring institusjon","behandlingstema":"NASJONAL_INSTITUSJON","vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"FELLES","nynorsk":[{"_type":"block","style":"normal","_key":"5e7bea203541","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi det har vore ei satsendring.","_key":"9c4f98716f570"}]},{"children":[{"_key":"4332f3500a780","_type":"span","marks":[],"text":"\n"}],"_type":"block","style":"normal","_key":"e12d1ced593d","markDefs":[]}],"valgbarhet":"SAKSPESIFIKK","_id":"7b1cfc51-5432-45c4-ab58-206a56f64119","mappe":["INSTITUSJON","INNVILGET"],"_updatedAt":"2023-09-25T10:15:46Z","ovrigeTriggere":["SATSENDRING"],"hjemler":["2","10"],"periodeType":"UTBETALING","navnISystem":"Satsendring institusjon","_rev":"FuD004taptHFqBZyEy5by3","_type":"begrunnelse","begrunnelsetype":"INNVILGET","_createdAt":"2022-11-04T08:01:20Z"},{"navnISystem":"EØS-borger ektefelle jobber","_type":"begrunnelse","rolle":["SOKER","BARN"],"tema":"NASJONAL","valgbarhet":"STANDARD","mappe":["INNVILGET"],"bokmaal":[{"style":"normal","_key":"89d35c3f47aa","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har oppholdsrett som EØS-borger. Familien bor i Norge og ektefellen din jobber her fra ","_key":"94930de31d2a0"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"91a01e81e860"},{"marks":[],"text":". ","_key":"ab62a93fb1d3","_type":"span"}],"_type":"block"}],"apiNavn":"innvilgetEosBorgerEktefelleJobber","visningsnavn":"72. EØS-borger ektefelle jobber","_updatedAt":"2023-09-25T10:15:46Z","hjemler":["2","4","11"],"_rev":"FuD004taptHFqBZyEy5by3","vilkaar":["LOVLIG_OPPHOLD"],"periodeType":"UTBETALING","nynorsk":[{"_key":"f89bc9ba0abe","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har opphaldsrett som EØS-borgar. Familien bur i Noreg og ektefellen din jobbar her frå ","_key":"bb562f4123880"},{"_key":"bf13986400e4","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"2945e4cf11be"}],"_type":"block","style":"normal"}],"vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","_createdAt":"2022-03-17T18:15:27Z","_id":"7b31207f-0643-4ad7-b542-762bc36fd5d3"},{"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"FELLES","_createdAt":"2021-10-25T06:48:23Z","navnISystem":"Flytting etter meklingsattest","hjemler":["2","9","11"],"behandlingstema":"NASJONAL","periodeType":"UTBETALING","mappe":["INNVILGET"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"","_key":"1a6d969421b8"},{"_type":"valgfeltV2","_key":"cebb053882a9","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"}},{"text":" utvidet barnetrygd fordi du bor alene med ","_key":"06d7daeeeecd","_type":"span","marks":[]},{"_type":"valgfeltV2","_key":"a494e20215d0","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":" fra ","_key":"0fbf9b65d6ee"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"760f94705a1b"},{"_type":"span","marks":[],"text":" og har meklingsattest. ","_key":"9635fbf1d8e3"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"bf5204465100","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at du og den andre forelderen flyttet fra hverandre.","_key":"758ed2807bc6"}],"_type":"block","style":"normal","_key":"d4bb66af552c","markDefs":[]}],"apiNavn":"innvilgetFlyttingEtterMeklingsattest","begrunnelsetype":"INNVILGET","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"","_key":"6693bce88bc3"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"70fc2407c4c6","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi du bur aleine med ","_key":"9f1a397bbb3a"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"890a950a1d23","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" frå ","_key":"8d146f28a332"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"fa9f8f7f57aa"},{"_type":"span","marks":[],"text":" og har meklingsattest. ","_key":"afc0b28b93a9"},{"_key":"6fc13ddaa743","skalHaStorForbokstav":true,"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"},"_type":"valgfeltV2"},{"text":" utvida barnetrygd frå månaden etter at du og den andre forelderen flytta frå kvarandre.","_key":"34dc951e6e65","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"91fae8c64491","markDefs":[]}],"_id":"7b95c2a0-df0e-49bd-a627-5407e5e161ae","_updatedAt":"2023-09-25T10:15:46Z","visningsnavn":"36. Flytting etter meklingsattest","_rev":"FuD004taptHFqBZyEy5by3","vilkaar":["UTVIDET_BARNETRYGD"],"valgbarhet":"STANDARD"},{"_type":"begrunnelse","begrunnelsetype":"REDUKSJON","tema":"FELLES","utvidetBarnetrygdTriggere":["SMÅBARNSTILLEGG"],"valgbarhet":"AUTOMATISK","_updatedAt":"2023-09-25T10:15:46Z","hjemler":["2","10"],"behandlingstema":"NASJONAL","vilkaar":["UTVIDET_BARNETRYGD"],"_createdAt":"2022-10-06T08:18:49Z","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygden endres fordi du ikke har utvidet barnetrygd. Derfor får du ikke småbarnstillegget.","_key":"aaeb2fcc8f3f0","_type":"span"}],"_type":"block","style":"normal","_key":"c2e2b635f59c"}],"navnISystem":"Småbarnstillegg har ikke utvidet barnetrygd","apiNavn":"reduksjonSmaabarnstilleggHarIkkeUtvidetBarnetrygd","vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","visningsnavn":"68. Småbarnstillegg har ikke utvidet barnetrygd","_rev":"FuD004taptHFqBZyEy5by3","mappe":["REDUKSJON"],"nynorsk":[{"_key":"29741a40cea2","markDefs":[],"children":[{"marks":[],"text":"Barnetrygda er endra fordi du ikkje har utvida barnetrygd. Derfor får du ikkje småbarnstillegget.","_key":"1f172aff55a80","_type":"span"}],"_type":"block","style":"normal"}],"_id":"7c8e4acf-9a70-44b3-8c13-0d335f0d9740"},{"_updatedAt":"2023-09-25T10:15:46Z","navnISystem":"Etterbetaling tre år tilbake i tid kun utvidet del","hjemler":["11"],"vedtakResultat":"IKKE_INNVILGET","tema":"FELLES","nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Du søker om utvida barnetrygd tilbake i tid. Vi fikk søknaden din ","_key":"47df15e9cecf0","_type":"span"},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"c8fc4e22674b"},{"text":". Barnetrygd kan ikkje utbetalast for meir enn tre år tilbake i tid frå vi fekk søknaden din.","_key":"45e23dbd58ec","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"1092e1b7b432"}],"visningsnavn":"6. Etterbetaling tre år tilbake i tid kun utvidet del","_type":"begrunnelse","begrunnelsetype":"ETTER_ENDRET_UTBETALINGSPERIODE","valgbarhet":"STANDARD","mappe":["ETTER_ENDRET_UTBETALINGSPERIODE"],"behandlingstema":"NASJONAL","_rev":"FuD004taptHFqBZyEy5by3","endringsaarsaker":["ETTERBETALING_3ÅR"],"_id":"7ca99085-f7ef-4d37-9a82-28bc7a83b8cb","bokmaal":[{"_key":"2f1b97ad6b02","markDefs":[],"children":[{"_key":"241844c6cc570","_type":"span","marks":[],"text":"Du søker om utvidet barnetrygd tilbake i tid. Vi fikk søknaden din "},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"d946fb868344"},{"_type":"span","marks":[],"text":". Barnetrygd kan ikke utbetales for mer enn tre år tilbake i tid fra vi fikk søknaden din.","_key":"991dac127c45"}],"_type":"block","style":"normal"}],"apiNavn":"etterEndretUtbetalingEtterbetalingTreAarTilbakeITidKunUtvidetDel","endretUtbetalingsperiodeTriggere":["ETTER_ENDRET_UTBETALINGSPERIODE"],"periodeType":"UTBETALING","_createdAt":"2022-05-31T15:04:30Z"},{"vilkaar":["BOR_MED_SOKER"],"periodeType":"INGEN_UTBETALING","tema":"NASJONAL","mappe":["AVSLAG"],"bokmaal":[{"_type":"block","style":"normal","_key":"3eff80569f14","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Hele barnetrygden for barn født ","_key":"b89a769f9cc50"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"4070a6c12a50"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at avtalen om delt bosted fortsatt følges.","_key":"1b2a9f953a62"}]}],"_type":"begrunnelse","_createdAt":"2022-04-28T15:01:57Z","_updatedAt":"2023-09-25T10:31:05Z","_id":"7d3ea44f-d19f-4c33-af8f-a0e6e74dfb7d","navnISystem":"Avtale om delt bosted følges fortsatt","visningsnavn":"65. Avtale om delt bosted følges fortsatt","_rev":"BtltdVb0HP4g4WJfnr5Nko","begrunnelsetype":"AVSLAG","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Heile barnetrygda for barn fødd ","_key":"92d7f8e470560"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"e61ee2e6f08c"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at avtalen om delt bustad framleis vert følgd.\n","_key":"9f43fab6eb63"}],"_type":"block","style":"normal","_key":"641ef1fedb07"}],"apiNavn":"avslagAvtaleOmDeltBostedFolgesFortsatt","hjemler":["2"],"vedtakResultat":"IKKE_INNVILGET","valgbarhet":"STANDARD"},{"nynorsk":[{"_type":"block","style":"normal","_key":"7dceab779abd","markDefs":[],"children":[{"text":"Utvida barnetrygd fordi du er sambuar. Å bu mellombels kvar for seg gjev ikkje rett til utvida barnetrygd.","_key":"b2f6264b22d7","_type":"span","marks":[]}]}],"_createdAt":"2021-10-22T08:29:51Z","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:29:42Z","navnISystem":"Samboer midlertidig adskillelse","hjemler":["9"],"vedtakResultat":"IKKE_INNVILGET","tema":"FELLES","bokmaal":[{"children":[{"marks":[],"text":"Utvidet barnetrygd fordi du er samboer. Å bo midlertidig hver for seg gir ikke rett til utvidet barnetrygd.","_key":"4c8f0813c3a8","_type":"span"}],"_type":"block","style":"normal","_key":"a21795031c5c","markDefs":[]}],"apiNavn":"avslagSamboerMidlertidigAdskillelse","_rev":"BtltdVb0HP4g4WJfnr5JZJ","_id":"7d797baa-a674-464e-b03f-56bf01dd4b68","behandlingstema":"NASJONAL","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","visningsnavn":"39. Samboer midlertidig adskillelse","_type":"begrunnelse","valgbarhet":"STANDARD"},{"behandlingstema":"NASJONAL","vilkaar":["BOSATT_I_RIKET"],"_id":"7d79a1d5-b7ad-45fd-b407-a8e5070bb6f6","_rev":"FuD004taptHFqBZyEy8C2N","_updatedAt":"2023-09-25T10:34:36Z","apiNavn":"opphorAnnenForelderIkkeLengerMedlemTrygdeavtale","hjemler":["2","4","22"],"visningsnavn":"20. Annen forelder ikke medlem trygdeavtale","periodeType":"INGEN_UTBETALING","rolle":["SOKER"],"begrunnelsetype":"OPPHØR","bosattIRiketTriggere":["MEDLEMSKAP"],"_createdAt":"2021-09-24T11:35:11Z","valgbarhet":"STANDARD","mappe":["OPPHØR"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"4844ca6dc41b"},{"_type":"flettefelt","_key":"81194b14643d","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikke er medlem i folketrygden etter trygdeavtale under oppholdet i utlandet ","_key":"d9c530aaeb3e"},{"_key":"514ff99623f7","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"_type":"span","marks":[],"text":".","_key":"62382015a478"}],"_type":"block","style":"normal","_key":"0fc21dedefdb","markDefs":[]}],"navnISystem":"Annen forelder ikke medlem trygdeavtale","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","tema":"NASJONAL","nynorsk":[{"style":"normal","_key":"8dde27a93aec","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"eba8c56286b7"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"50226952fe5e"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikkje er medlem i folketrygda etter trygdeavtale under opphaldet i utlandet ","_key":"9d91abb182a9"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"1be7f4e46b1d"},{"_type":"span","marks":[],"text":".","_key":"4cad44cb260f"}],"_type":"block"}]},{"navnISystem":"Foreldrene bor sammen","borMedSokerTriggere":["DELT_BOSTED"],"mappe":["REDUKSJON"],"_rev":"FuD004taptHFqBZyEy5by3","vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","_createdAt":"2021-10-26T10:15:21Z","_id":"7ddebe28-6a7a-4ba6-8cd9-cd01757de7c1","visningsnavn":"43. Foreldrene bor sammen ","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"begrunnelsetype":"REDUKSJON","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:46Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi du har flyttet sammen med den andre forelderen til barn født ","_key":"a58017a3bb63"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"345d5e47d7c7"},{"_type":"span","marks":[],"text":" fra ","_key":"ed0d1099ed89"},{"_type":"flettefelt","_key":"93ff7a0fe57c","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_type":"span","marks":[],"text":".","_key":"941d26fe2bfd"}],"_type":"block","style":"normal","_key":"de453759899a"}],"apiNavn":"reduksjonForeldreneBorSammen","hjemler":["2","11"],"tema":"NASJONAL","nynorsk":[{"_type":"block","style":"normal","_key":"2f862c89f8c6","markDefs":[],"children":[{"text":"Barnetrygda er endra fordi du har flytta saman med den andre forelderen til barn fødd ","_key":"d362ece7534c","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"8e60d58992fc"},{"text":" frå ","_key":"bb6ac36303a5","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"2e414c3395f5"},{"_type":"span","marks":[],"text":".","_key":"4cab3ca94b4c"}]}]},{"hjemler":["2","12"],"vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","_updatedAt":"2023-09-25T10:26:10Z","apiNavn":"avslagForeldreneBorSammen","_type":"begrunnelse","begrunnelsetype":"AVSLAG","tema":"NASJONAL","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"ceb842bac5f4"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"ddf026f1d0a0"},{"text":" fordi den andre forelderen allereie mottek barnetrygd for ","_key":"39369919decb","_type":"span","marks":[]},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"ec3c45553ede"},{"_type":"span","marks":[],"text":".","_key":"820f508c2199"}],"_type":"block","style":"normal","_key":"57907c2b1f85"}],"_id":"7e982416-a96b-43da-9296-2558f2036ae1","navnISystem":"Foreldrene bor sammen","visningsnavn":"8. Foreldrene bor sammen","_rev":"FuD004taptHFqBZyEy7Bhv","vilkaar":["BOR_MED_SOKER"],"mappe":["AVSLAG"],"bokmaal":[{"_key":"341c1244fad7","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"f8a6d17820a2"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"fd2d24054f3e"},{"_type":"span","marks":[],"text":" fordi den andre forelderen allerede får barnetrygd for ","_key":"f1f2c54d8db1"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"8ae3f6c9c019"},{"text":".","_key":"257808f68ca9","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"_createdAt":"2021-08-30T13:19:46Z","valgbarhet":"STANDARD"},{"apiNavn":"reduksjonDeltBarnetrygdAnnenForelderSokt","_type":"begrunnelse","mappe":["REDUKSJON"],"tema":"NASJONAL","nynorsk":[{"_key":"4e46fbb02103","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du har ein avtale om delt bustad for barn fødd ","_key":"28f55caf4c4c"},{"_key":"f6bd6205cf20","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":". Barnetrygda er delt frå månaden etter at den andre forelderen har søkt om barnetrygd.","_key":"2d7c9b83193e"}],"_type":"block","style":"normal"}],"navnISystem":"Delt barnetrygd annen forelder søkt","behandlingstema":"NASJONAL","_rev":"FuD004taptHFqBZyEy5by3","resultat":"REDUKSJON","vedtakResultat":"REDUKSJON","begrunnelsetype":"REDUKSJON","_updatedAt":"2023-09-25T10:15:46Z","vilkaar":["BOR_MED_SOKER"],"periodeType":"UTBETALING","_createdAt":"2022-01-20T14:34:19Z","_id":"7f238fa4-b903-4a2c-ae24-df6a13cda575","hjemler":["2","12"],"visningsnavn":"59. Delt barnetrygd annen forelder søkt","borMedSokerTriggere":["DELT_BOSTED"],"valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi du har en avtale om delt bosted for barn født ","_key":"3e48761ad1e4"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"6243fb09d13b"},{"_type":"span","marks":[],"text":". Barnetrygden deles fra måneden etter at den andre forelderen har søkt om barnetrygd.","_key":"9060ea88ca23"}],"_type":"block","style":"normal","_key":"713ec997745d"}]},{"_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"rolle":["BARN"],"begrunnelsetype":"REDUKSJON","mappe":["REDUKSJON"],"visningsnavn":"17. Annen forelder ikke lenger pliktig medlem","tema":"NASJONAL","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi den andre forelderen til barn fødd ","_key":"82698fd15ce3"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"2b3233ab9491"},{"_type":"span","marks":[],"text":" ikkje lenger er pliktig medlem i folketrygda under opphaldet i utlandet som starta ","_key":"db4072e4d099"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"62bc077c74ea"},{"_key":"474f1c974336","_type":"span","marks":[],"text":". Bur begge foreldra saman under opphaldet i utlandet, må begge vere medlem i folketrygda for å få barnetrygd."}],"_type":"block","style":"normal","_key":"39ef9b483e18","markDefs":[]}],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:46Z","hjemlerFolketrygdloven":["2-5"],"apiNavn":"reduksjonAnnenForelderIkkeLengerPliktigMedlem","hjemler":["4","5"],"_rev":"FuD004taptHFqBZyEy5by3","vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","navnISystem":"Annen forelder ikke lenger pliktig medlem","_createdAt":"2021-09-24T12:28:51Z","_id":"80d4f746-0b28-4f08-80e6-f6cf4d6de026","bokmaal":[{"_key":"5f9505bd5993","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi den andre forelderen til barn født ","_key":"591def6c8b9c"},{"_key":"0bc158f8cc75","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" ikke lenger er pliktig medlem i folketrygden under oppholdet i utlandet som startet ","_key":"dcaa976cd826"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"8bcc58123b8c"},{"_type":"span","marks":[],"text":". Bor begge foreldrene sammen under oppholdet i utlandet, må dere begge være medlem i folketrygden for å få barnetrygd.","_key":"e458a1cbe241"}],"_type":"block","style":"normal"}],"bosattIRiketTriggere":["MEDLEMSKAP"]},{"vedtakResultat":"IKKE_INNVILGET","valgbarhet":"STANDARD","bokmaal":[{"_key":"e2407a0b76d6","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"c8655f3dc2a7"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"8377f4b01551"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at ","_key":"55c004d6b524"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"5407ade64ff3"},{"_type":"span","marks":[],"text":" ikke bodde fast hos deg. ","_key":"26d7c04314d5"}],"_type":"block","style":"normal"}],"hjemler":["2","4"],"_rev":"FuD004taptHFqBZyEy5by3","_type":"begrunnelse","navnISystem":"Barn bodde ikke med søker","mappe":["OPPHØR"],"periodeType":"UTBETALING","tema":"NASJONAL","nynorsk":[{"children":[{"marks":[],"text":"Barnetrygd for barn fødd ","_key":"b56e2aa4a01f","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"7f62e89237bc"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at ","_key":"d1df5f8b225b"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"f96298d4fb8d","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikkje budde fast hos deg. ","_key":"8f2f59d3dcfb"}],"_type":"block","style":"normal","_key":"d21334859969","markDefs":[]}],"_createdAt":"2021-11-05T10:22:16Z","_id":"80d7b96d-53ad-4472-a5cc-2a840d485a6c","apiNavn":"opphorBarnBoddeIkkeMedSoker","visningsnavn":"32. Barn bodde ikke med søker","vilkaar":["BOR_MED_SOKER"],"_updatedAt":"2023-09-25T10:15:46Z","ovrigeTriggere":["GJELDER_FØRSTE_PERIODE"],"begrunnelsetype":"OPPHØR"},{"_createdAt":"2021-10-25T07:17:09Z","valgbarhet":"STANDARD","_id":"824db5ff-d9e8-4877-b476-b766f85bb3b2","_updatedAt":"2023-09-25T10:15:46Z","hjemler":["2","9","11"],"_type":"begrunnelse","begrunnelsetype":"INNVILGET","apiNavn":"innvilgetForvaringSamboer","nynorsk":[{"_type":"block","style":"normal","_key":"4e82b77a01f2","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"7cb29c376aac"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"9518318f385f","skalHaStorForbokstav":false},{"_key":"6c3ec10e3d18","_type":"span","marks":[],"text":" utvida barnetrygd sjølv om du fortsatt er sambuar fordi du bur aleine med "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"f9b7b57209c3","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Sambuaren din er i forvaring i seks månader eller meir frå ","_key":"7a74fed27fe8"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"22e3ff113a87"},{"_type":"span","marks":[],"text":". ","_key":"fea30b7ac721"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"c0c899fb19ce","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at forvaringa starta.","_key":"4211982b2211"}]}],"behandlingstema":"NASJONAL","periodeType":"UTBETALING","tema":"FELLES","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["UTVIDET_BARNETRYGD"],"mappe":["INNVILGET"],"bokmaal":[{"style":"normal","_key":"4f6cc8ab5f71","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"7e702f5a15cb"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"3a0166d942c5","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd selv om du fortsatt er samboer fordi du bor alene med ","_key":"916a9e9f6dad"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"7e1c9604d8b4","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Samboeren din er i forvaring i seks måneder eller mer fra ","_key":"7b1da0770ccb"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"e7454bb8db02"},{"_type":"span","marks":[],"text":". ","_key":"e576b659f33a"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"220d8cdda238","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at forvaringen startet.","_key":"3876f3d1c179"}],"_type":"block"}],"navnISystem":"Forvaring samboer","visningsnavn":"47. Forvaring samboer","_rev":"FuD004taptHFqBZyEy5by3"},{"_rev":"FuD004taptHFqBZyEy5by3","periodeType":"UTBETALING","_createdAt":"2021-10-25T07:26:18Z","apiNavn":"innvilgetSeparertOgVurderingEgenHusholdning","visningsnavn":"50. Separert og vurdering egen husholdning","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:46Z","bokmaal":[{"style":"normal","_key":"59082706c104","markDefs":[],"children":[{"text":"","_key":"3352c8a027bd","_type":"span","marks":[]},{"_key":"8162dedd478e","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" utvidet barnetrygd fordi du er separert og bor alene med ","_key":"0442b61935e2"},{"_key":"24ae50c0d504","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". Vi har kommet fram til at du og den tidligere ektefellen din flyttet fra hverandre ","_key":"67e4a6915cba"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"681cf662bc71"},{"text":". ","_key":"c65f77e56718","_type":"span","marks":[]},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"7fca62e7c7a3","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at dere flyttet fra hverandre.","_key":"2a228df094fe"}],"_type":"block"}],"_id":"8444877a-bbfa-4e90-9b84-abe327e1d307","navnISystem":"Separert og vurdering egen husholdning","hjemler":["2","9","11"],"behandlingstema":"NASJONAL","_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"tema":"FELLES","nynorsk":[{"_type":"block","style":"normal","_key":"69bda9b52680","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"f9d799372c1a"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"574b582870df","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi du er separert og bur aleine med ","_key":"5a36f2aac02e"},{"skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"1626b8594803"},{"marks":[],"text":". Vi har kome fram til at du og den tidligare ektefellen din flytta frå kvarandre ","_key":"ecb628b9e1d9","_type":"span"},{"_key":"725aafe78c50","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":". ","_key":"80bc05c09881"},{"skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"4219535bd5ed"},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at de flytta frå kvarandre.","_key":"5636e005d159"}]}],"valgbarhet":"STANDARD"},{"navnISystem":"Ikke oppholdstillatelse","periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","_id":"858d12b2-2321-45a3-812c-b79e156c6987","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi ","_key":"cd11f7b7fc46"},{"skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},"_type":"valgfeltV2","_key":"659fa614358e"},{"_type":"span","marks":[],"text":" ikke hadde oppholdstillatelse i Norge. ","_key":"48c16ddac16b"}],"_type":"block","style":"normal","_key":"04d1ea778ee7","markDefs":[]}],"_type":"begrunnelse","vilkaar":["LOVLIG_OPPHOLD"],"_createdAt":"2021-11-05T10:26:53Z","valgbarhet":"STANDARD","apiNavn":"reduksjonIkkeOppholdstillatelse","hjemler":["2","4"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","nynorsk":[{"_type":"block","style":"normal","_key":"e0dc6666ebf6","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi ","_key":"b4a92914d685"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"9e032b71f789","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikkje hadde opphaldsløyve i Noreg. ","_key":"1c30d27dde06"}]}],"visningsnavn":"47. Ikke oppholdstillatelse","behandlingstema":"NASJONAL","vedtakResultat":"REDUKSJON","rolle":["SOKER","BARN"],"tema":"NASJONAL","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:48Z","ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"]},{"vedtakResultat":"REDUKSJON","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","utvidetBarnetrygdTriggere":["SMÅBARNSTILLEGG"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du ikkje lenger har barn under 3 år. Difor får du ikkje småbarnstillegget.","_key":"239ce3436ae3"}],"_type":"block","style":"normal","_key":"ec9d4314584c"}],"_updatedAt":"2023-09-25T10:15:48Z","navnISystem":"Småbarnstillegg ikke lenger barn under tre år","hjemler":["2","10"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","_createdAt":"2021-11-12T14:51:12Z","valgbarhet":"AUTOMATISK","mappe":["REDUKSJON"],"bokmaal":[{"children":[{"text":"Barnetrygden endres fordi du ikke lenger har barn under 3 år. Derfor får du ikke småbarnstillegget.","_key":"f74b23ee0036","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"06ab636e3d12","markDefs":[]}],"apiNavn":"reduksjonSmaabarnstilleggIkkeLengerBarnUnderTreAar","behandlingstema":"NASJONAL","begrunnelsetype":"REDUKSJON","tema":"FELLES","_id":"8714a9b0-0cc6-4b2f-8f19-7986890f849d","visningsnavn":"57. Småbarnstillegg ikke lenger barn under tre år","_type":"begrunnelse"},{"mappe":["FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:22:29Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi vi har kommet fram til at hele familien fortsatt fyller vilkårene for medlemskap i folketrygden under oppholdet i utlandet.","_key":"4cd44fafe02d"}],"_type":"block","style":"normal","_key":"78ef41d89b19","markDefs":[]}],"hjemler":["4","5"],"hjemlerFolketrygdloven":["2-5","2-8"],"bosattIRiketTriggere":["MEDLEMSKAP"],"nynorsk":[{"children":[{"text":"Du får barnetrygd fordi vi har kome fram til at heile familien fortsatt fyller vilkåra for medlemskap i folketrygda under opphaldet i utlandet.","_key":"c64f65edb8cc","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"a40ee5dd5571","markDefs":[]}],"_createdAt":"2021-09-24T12:40:55Z","_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"rolle":["SOKER"],"begrunnelsetype":"FORTSATT_INNVILGET","valgbarhet":"STANDARD","navnISystem":"Vurdering hele familien medlem","visningsnavn":"17. Vurdering hele familien medlem","tema":"NASJONAL","_id":"8921d74d-4cf2-4410-bc6c-517aa6d73134","apiNavn":"fortsattInnvilgetVurderingHeleFamilienMedlem","_rev":"FuD004taptHFqBZyEy6SsL","vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET"},{"_type":"begrunnelse","begrunnelsetype":"INNVILGET","_createdAt":"2021-10-25T09:52:36Z","visningsnavn":"55. Barn 16 år","_rev":"h26rAhFEYSUtDGXJE0sTLq","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","valgbarhet":"STANDARD","navnISystem":"Barn 16 år","behandlingstema":"NASJONAL","vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"markDefs":[],"children":[{"_key":"4c2089f1eb1e","_type":"span","marks":[],"text":""},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"2390f3387529","skalHaStorForbokstav":false},{"text":" utvida barnetrygd fordi du bur aleine med ","_key":"bd31f930699e","_type":"span","marks":[]},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"db904b4e0137"},{"_type":"span","marks":[],"text":". Du og den tidligare sambuaren din har flytta frå kvarandre. ","_key":"ea9f0fc71c8b"},{"_key":"f6b66b042c59","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" har fylt 16 år og vi krev ikkje lenger meklingsattest. ","_key":"01a7a1faf170"},{"skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"23e7b525a354"},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at ","_key":"405cc94efa19"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"a3864e76bcc1","skalHaStorForbokstav":false},{"text":" fylte 16 år.","_key":"241ec2493b46","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"a6122d7b93b0"}],"_id":"89294d62-a050-4e68-ae60-be5a6aa97c43","_updatedAt":"2023-09-25T10:15:48Z","bokmaal":[{"_key":"523c076412ef","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"fa7b5829e210"},{"_type":"valgfeltV2","_key":"b4948ce2e28e","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"}},{"_key":"e5012521f130","_type":"span","marks":[],"text":" utvidet barnetrygd fordi du bor alene med "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"29a7afb85a74","skalHaStorForbokstav":false},{"marks":[],"text":". Du og den tidligere samboeren din har flyttet fra hverandre. ","_key":"3697df6dc448","_type":"span"},{"_key":"e213aaaa9e0b","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_key":"ff08aa1bff9c","_type":"span","marks":[],"text":" har fylt 16 år og vi krever ikke lenger meklingsattest. "},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"c2d0a6984e4d","skalHaStorForbokstav":true},{"text":" utvidet barnetrygd fra måneden etter at ","_key":"bca90794f86b","_type":"span","marks":[]},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"339144260b4d"},{"_type":"span","marks":[],"text":" fylte 16 år.","_key":"5f4da2259ddd"}],"_type":"block","style":"normal"}],"apiNavn":"innvilgetBarn16Ar","hjemler":["2","9","11"],"tema":"NASJONAL","mappe":["INNVILGET"]},{"fagsakType":"INSTITUSJON","behandlingstema":"NASJONAL_INSTITUSJON","begrunnelsetype":"FORTSATT_INNVILGET","nynorsk":[{"_key":"26785c566dee","markDefs":[],"children":[{"_type":"span","marks":[],"text":"De får barnetrygd fordi barnet fortsatt er busett i institusjonen.","_key":"dc1a166d56fd0"}],"_type":"block","style":"normal"}],"_rev":"FuD004taptHFqBZyEy8t0P","periodeType":"FORTSATT_INNVILGET","tema":"NASJONAL","mappe":["INSTITUSJON","FORTSATT_INNVILGET"],"bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Dere får barnetrygd fordi barnet fortsatt er bosatt i institusjonen.","_key":"ec17c6ea06fb0","_type":"span"}],"_type":"block","style":"normal","_key":"037098656822"}],"apiNavn":"fortsattInnvilgetBosattIInstitusjon","vedtakResultat":"INGEN_ENDRING","_createdAt":"2022-11-04T09:04:46Z","_id":"8ce3a71e-7370-4770-b9c0-3cd4d93ceac6","_updatedAt":"2023-09-25T10:38:39Z","navnISystem":"Bosatt i institusjon","hjemler":["2"],"visningsnavn":"1. Bosatt i institusjon","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"valgbarhet":"SAKSPESIFIKK"},{"mappe":["FORTSATT_INNVILGET"],"navnISystem":"EØS-borger: Søker har oppholdsrett","hjemler":["2","4","11"],"vedtakResultat":"INGEN_ENDRING","_createdAt":"2021-08-30T13:05:29Z","begrunnelsetype":"FORTSATT_INNVILGET","apiNavn":"fortsattInnvilgetLovligOppholdEOS","vilkaar":["LOVLIG_OPPHOLD"],"periodeType":"FORTSATT_INNVILGET","rolle":["SOKER"],"bokmaal":[{"style":"normal","_key":"1ad9f4bc582a","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi vi har kommet fram til at du fortsatt har oppholdsrett som EØS-borger.","_key":"54bfde925f13"}],"_type":"block"}],"visningsnavn":"5. EØS-borger: Søker har oppholdsrett","_rev":"h26rAhFEYSUtDGXJE0siTj","nynorsk":[{"_type":"block","style":"normal","_key":"298ed1a44a4e","markDefs":[],"children":[{"text":"Du får barnetrygd fordi vi har kome fram til at du fortsatt har opphaldsrett som EØS-borgar.","_key":"221a7c55863f","_type":"span","marks":[]}]}],"valgbarhet":"STANDARD","_type":"begrunnelse","tema":"NASJONAL","_id":"8e05da4b-368d-489c-abee-0710e5a74ec5","_updatedAt":"2023-09-25T10:21:47Z"},{"_createdAt":"2022-03-09T08:42:14Z","_updatedAt":"2023-09-25T10:15:48Z","behandlingstema":"NASJONAL","rolle":["SOKER","BARN"],"periodeType":"UTBETALING","valgbarhet":"TILLEGGSTEKST","_id":"8e07cd84-101a-4d4b-8f25-c381b70697ee","mappe":["INNVILGET"],"bokmaal":[{"markDefs":[],"children":[{"_key":"19907971ad000","_type":"span","marks":[],"text":"Du har allerede fått barnetrygd av Utlendingsdirektoratet. Derfor blir deler av etterbetalingen utbetalt til Utlendingsdirektoratet."}],"_type":"block","style":"normal","_key":"f1ebf5a7d6a7"}],"apiNavn":"innvilgetTilleggstekstTransporterklaeringDelerAvEtterbetalingen","_type":"begrunnelse","_rev":"h26rAhFEYSUtDGXJE0sTLq","begrunnelsetype":"INNVILGET","navnISystem":"Tilleggstekst transporterklæring - deler av etterbetalingen","visningsnavn":"68. Tilleggstekst transporteklæring - deler av etterbetalingen","tema":"NASJONAL","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du har allereie fått barnetrygd av Utlendingsdirektoratet. Derfor blir deler av etterbetalinga utbetalt til Utlendingsdirektoratet.","_key":"74f6cec319670"}],"_type":"block","style":"normal","_key":"23a9f7cc2814","markDefs":[]}],"vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["BOSATT_I_RIKET","LOVLIG_OPPHOLD"]},{"hjemler":["2","12"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","navnISystem":"Delt bosted - full ordinær og etterbetaling utvidet","_createdAt":"2021-10-18T12:17:01Z","mappe":["ENDRET_UTBETALINGSPERIODE"],"_updatedAt":"2023-09-25T10:15:48Z","_type":"begrunnelse","periodeType":"UTBETALING","endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"SKAL_IKKE_UTBETALES","_id":"8ebcf453-a18e-404c-a015-8973198efe65","apiNavn":"endretUtbetalingDeltBostedFullOrdinarOgEtterbetalingUtvidet","visningsnavn":"3. Delt bosted - full ordinær og etterbetaling utvidet","vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"_key":"5572099f9d2b","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Ordinær barnetrygd for tida før du søkte er allereie utbetalt til den andre forelderen. Vi kan ikkje etterbetale barnetrygd som allereie er utbetalt. Du og den andre forelderen må sjølv bli einige om korleis de vil dele den ordinære barnetrygda som er utbetalt. Du får den utvida delen av barnetrygda etterbetalt. ","_key":"973e5772240f"}],"_type":"block","style":"normal"}],"endringsaarsaker":["DELT_BOSTED"],"bokmaal":[{"_key":"5e0329033e5f","markDefs":[],"children":[{"_key":"03e732c8ab61","_type":"span","marks":[],"text":"Ordinær barnetrygd for tiden før du søkte er allerede utbetalt til den andre forelderen. Vi kan ikke etterbetale barnetrygd som allerede er utbetalt. Du og den andre forelderen må selv bli enige om hvordan dere vil fordele den ordinære barnetrygden som er utbetalt. Du får den utvidede delen av barnetrygden etterbetalt. "}],"_type":"block","style":"normal"}]},{"_createdAt":"2021-10-25T06:38:47Z","valgbarhet":"STANDARD","navnISystem":"Samboer død","apiNavn":"innvilgetSamboerDod","visningsnavn":"32. Samboer død","tema":"FELLES","bokmaal":[{"_key":"06eef3ae35c8","markDefs":[],"children":[{"_key":"e01146578999","_type":"span","marks":[],"text":""},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"1f9505dde000","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd fordi du bor alene med ","_key":"7094135d54b1"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"5a96bed7f16b","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" etter at samboeren din døde ","_key":"fb81cd236d3b"},{"_key":"f8b5c6740e1e","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":". ","_key":"69a88960d9dd"},{"_type":"valgfeltV2","_key":"22a70176eb6b","skalHaStorForbokstav":true,"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"}},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at du ble alene.","_key":"b1a8eb47b73f"}],"_type":"block","style":"normal"}],"hjemler":["9"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","vilkaar":["UTVIDET_BARNETRYGD"],"mappe":["INNVILGET"],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"","_key":"7623c2f8675f","_type":"span"},{"_type":"valgfeltV2","_key":"a6511b222a45","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"}},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi du bur aleine med ","_key":"770563c2d0db"},{"_type":"valgfeltV2","_key":"7b39dceb6f4c","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"}},{"_type":"span","marks":[],"text":" etter at sambuaren din døydde ","_key":"025c416363b5"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"055529744f7e"},{"_type":"span","marks":[],"text":". ","_key":"c3a27edcb149"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"1d89edd29701","skalHaStorForbokstav":true},{"_key":"9587cbc6c060","_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at du vart aleine."}],"_type":"block","style":"normal","_key":"618adc80e3f8"}],"_id":"902253da-dd08-4c1c-bf17-d2d082a26e2e","_updatedAt":"2023-09-25T10:15:48Z","behandlingstema":"NASJONAL","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET"},{"hjemler":["2","9","11"],"visningsnavn":"36. Ektefelle ikke lenger forsvunnet","vedtakResultat":"REDUKSJON","_createdAt":"2021-10-23T06:05:55Z","mappe":["REDUKSJON"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi ektefellen din ikke lenger er forsvunnet fra ","_key":"b4f4a7fc328c"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"8ad9c77a875e"},{"_type":"span","marks":[],"text":".","_key":"2fe9c0f0f348"}],"_type":"block","style":"normal","_key":"42251b860701"}],"apiNavn":"reduksjonEktefelleIkkeLengerForsvunnet","begrunnelsetype":"REDUKSJON","nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygda er endra fordi ektefellen din ikkje lenger er forsvunnen frå ","_key":"5eae9a6043c2","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"d0b37b01592f"},{"_type":"span","marks":[],"text":".","_key":"fb9e57880b24"}],"_type":"block","style":"normal","_key":"0cc47aa2a483"}],"_updatedAt":"2023-09-25T10:15:48Z","navnISystem":"Ektefelle ikke lenger forsvunnet","_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","tema":"FELLES","_id":"9318d00b-2946-4a97-8779-14216e70d499","_rev":"h26rAhFEYSUtDGXJE0sTLq","valgbarhet":"STANDARD"},{"_id":"93478557-4768-4265-9ff9-ee1889e9c0c6","bokmaal":[{"style":"normal","_key":"184e659355c7","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har oppholdsrett som EØS-borger. Familien bor i Norge og samboeren din jobber her fra ","_key":"91b24608ed400"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"8d323693c31d"},{"text":". ","_key":"568c5e071deb","_type":"span","marks":[]}],"_type":"block"}],"hjemler":["2","4","11"],"visningsnavn":"73. EØS-borger samboer jobber","vilkaar":["LOVLIG_OPPHOLD"],"periodeType":"UTBETALING","valgbarhet":"STANDARD","mappe":["INNVILGET"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","vedtakResultat":"INNVILGET_ELLER_ØKNING","rolle":["SOKER","BARN"],"nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd fordi du har opphaldsrett som EØS-borgar. Familien bur i Noreg og sambuaren din jobbar her frå ","_key":"c154efa11d240","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"b71ad47215e6"},{"marks":[],"text":".","_key":"3d67b1f623ab","_type":"span"}],"_type":"block","style":"normal","_key":"6ef378c9589e"}],"_createdAt":"2022-03-17T18:17:46Z","apiNavn":"innvilgetEosBorgerSamboerJobber","_type":"begrunnelse","begrunnelsetype":"INNVILGET","tema":"NASJONAL","navnISystem":"EØS-borger samboer jobber","_updatedAt":"2023-09-25T10:15:48Z"},{"valgbarhet":"STANDARD","_id":"936b3c93-dad9-4e2d-9ac6-4826bdd5c53e","navnISystem":"EØS-borger uten oppholdsrett","_type":"begrunnelse","periodeType":"INGEN_UTBETALING","tema":"NASJONAL","nynorsk":[{"_type":"block","style":"normal","_key":"05f6d15b43ab","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd fordi vi har kome fram til at ","_key":"dfbbeb440b9e0"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"a77bcf40479d"},{"_type":"span","marks":[],"text":" ikkje har opphaldsrett som EØS-borgar ","_key":"9be07df028ed"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"e9c67ca96f3b"},{"_type":"span","marks":[],"text":".","_key":"aadf97fad79c"}]}],"hjemler":["2","4","11"],"vedtakResultat":"IKKE_INNVILGET","vilkaar":["LOVLIG_OPPHOLD"],"mappe":["OPPHØR"],"bokmaal":[{"_type":"block","style":"normal","_key":"97f8112d3a3a","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd fordi vi har kommet frem til at ","_key":"94d8dcb4cdf40"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"b740e662572a"},{"text":" ikke har oppholdsrett som EØS-borger ","_key":"17c5d28a3d2c","_type":"span","marks":[]},{"_type":"valgReferanse","_key":"b1aea43f6918","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"_type":"span","marks":[],"text":".","_key":"2d8f1f4bec3a"}]}],"apiNavn":"opphorIkkeOppholdsrettEosBorger","visningsnavn":"43. EØS-borger uten oppholdsrett","_rev":"BtltdVb0HP4g4WJfnr5j8o","rolle":["SOKER"],"_updatedAt":"2023-09-25T10:36:22Z","begrunnelsetype":"OPPHØR","_createdAt":"2022-03-17T18:03:33Z"},{"apiNavn":"innvilgetEosBorgerEktefelleUtbetalingFraNav","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","nynorsk":[{"_type":"block","style":"normal","_key":"fcc44f847a76","markDefs":[],"children":[{"text":"Du får barnetrygd fordi du har opphaldsrett som EØS-borgar. Familien bur i Noreg og ektefellen din får utbetaling frå NAV som erstattar løn frå ","_key":"72b9d8fb647d0","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"8b48121ee57d"},{"_type":"span","marks":[],"text":".","_key":"4742240d31ef"}]}],"hjemler":["2","4","11"],"_type":"begrunnelse","_createdAt":"2022-03-17T18:21:04Z","_id":"9633f02c-0474-457d-9d0b-25a0ca59fdf9","mappe":["INNVILGET"],"visningsnavn":"74. EØS-borger ektefelle utbetaling fra NAV","vilkaar":["LOVLIG_OPPHOLD"],"tema":"NASJONAL","valgbarhet":"STANDARD","bokmaal":[{"_type":"block","style":"normal","_key":"db0dbd68306b","markDefs":[],"children":[{"text":"Du får barnetrygd fordi du har oppholdsrett som EØS-borger. Familien bor i Norge og ektefellen din får utbetaling fra NAV som erstatter lønn fra ","_key":"48d2bcfe95010","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"b32f187f90c6"},{"_key":"a3827f8539f2","_type":"span","marks":[],"text":". "}]}],"navnISystem":"EØS-borger ektefelle utbetaling fra NAV","_rev":"h26rAhFEYSUtDGXJE0sTLq","vedtakResultat":"INNVILGET_ELLER_ØKNING","rolle":["SOKER","BARN"],"_updatedAt":"2023-09-25T10:15:48Z"},{"navnISystem":"Ikke flyttet fra ektefelle","_rev":"h26rAhFEYSUtDGXJE0syaa","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","_updatedAt":"2023-09-25T10:27:35Z","visningsnavn":"31. Ikke flyttet fra ektefelle","periodeType":"INGEN_UTBETALING","nynorsk":[{"_type":"block","style":"normal","_key":"0d10c0656b33","markDefs":[],"children":[{"_key":"49c2a8556c8e","_type":"span","marks":[],"text":"Utvida barnetrygd fordi du og ektefellen din ikkje har flytta frå kvarandre."}]}],"_createdAt":"2021-10-22T07:55:58Z","valgbarhet":"STANDARD","apiNavn":"avslagIkkeFlyttetFraEktefelle","hjemler":["9"],"vilkaar":["UTVIDET_BARNETRYGD"],"mappe":["AVSLAG"],"begrunnelsetype":"AVSLAG","tema":"FELLES","_id":"964a1c04-3c94-4f17-b018-45fec24be559","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi du og ektefellen din ikke har flyttet fra hverandre.","_key":"ba8d7e2edece"}],"_type":"block","style":"normal","_key":"f29d5ec53439"}]},{"apiNavn":"reduksjonSatsendringInstitusjon","navnISystem":"Satsendring institusjon","_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","mappe":["INSTITUSJON","REDUKSJON"],"_updatedAt":"2023-09-25T10:15:48Z","hjemler":["2","10"],"nynorsk":[{"style":"normal","_key":"b77d8c74283b","markDefs":[],"children":[{"text":"Barnetrygda er endra fordi det har vore ei satsendring.","_key":"169fe04801080","_type":"span","marks":[]}],"_type":"block"}],"bokmaal":[{"style":"normal","_key":"6ad1143bce2b","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden er endret fordi det har vært en satsendring.","_key":"479e88e4cbb30"}],"_type":"block"}],"ovrigeTriggere":["SATSENDRING"],"visningsnavn":"2. Satsendring institusjon","behandlingstema":"NASJONAL_INSTITUSJON","vedtakResultat":"REDUKSJON","tema":"FELLES","_createdAt":"2022-11-04T08:49:45Z","valgbarhet":"AUTOMATISK","_id":"96624108-841d-45d1-aae6-658c5df85e44"},{"navnISystem":"Søker og barn medlem etter trygdeavtale","_rev":"FuD004taptHFqBZyEy6QhX","rolle":["SOKER"],"bosattIRiketTriggere":["MEDLEMSKAP"],"nynorsk":[{"_type":"block","style":"normal","_key":"a93c1563c98d","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du og ","_key":"ddcdea7160c5"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"ea6777d4c337"},{"marks":[],"text":" fortsatt er medlem av folketrygda etter trygdeavtale under opphaldet i utlandet.","_key":"34c7e3f6454c","_type":"span"}]}],"_createdAt":"2021-09-24T12:33:57Z","bokmaal":[{"_type":"block","style":"normal","_key":"8ea0a9b477f9","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du og ","_key":"83f6e6a51bc7"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"3bb2e3923946"},{"_key":"69f6428555c7","_type":"span","marks":[],"text":" fortsatt er medlem av folketrygden etter trygdeavtale under oppholdet i utlandet."}]}],"behandlingstema":"NASJONAL","begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","visningsnavn":"14. Søker og barn medlem etter trygdeavtale","vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET","valgbarhet":"STANDARD","mappe":["FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:22:18Z","apiNavn":"fortsattInnvilgetSokerOgBarnMedlemEtterTrygdeavtale","hjemler":["2","4","22"],"_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"_id":"97da29a5-3da2-483d-adeb-d6410cee1964"},{"begrunnelsetype":"REDUKSJON","tema":"FELLES","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"_type":"begrunnelse","vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","nynorsk":[{"markDefs":[],"children":[{"text":"Barnetrygda er endra fordi retten har bestemt at barn fødd ","_key":"3f162609b36d","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"8dc8606395a1"},{"marks":[],"text":" skal bu fast hos den andre forelderen frå ","_key":"b8dcb3a422cf","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"ad258fe2ec9a"},{"_type":"span","marks":[],"text":".","_key":"c68e7a3e522a"}],"_type":"block","style":"normal","_key":"05da529bc3ea"}],"_updatedAt":"2023-09-25T10:15:48Z","hjemler":["2","11"],"vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["DELT_BOSTED"],"_id":"97eb3de1-dc4b-49af-8cf0-df72e8ec5736","apiNavn":"reduksjonRettsavgjorelseFastBosted","_rev":"h26rAhFEYSUtDGXJE0sTLq","_createdAt":"2021-10-26T10:09:17Z","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygden endres fordi retten har bestemt at barn født ","_key":"fc794f36fcdd","_type":"span"},{"_type":"flettefelt","_key":"b1f86edc1b0e","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" skal bo fast hos den andre forelderen fra ","_key":"a899b195a58c"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"aa9c472a5808"},{"_type":"span","marks":[],"text":".","_key":"c4d49137ac8c"}],"_type":"block","style":"normal","_key":"3286fe2e4ad9"}],"navnISystem":"Rettsavgjørelse fast bosted","visningsnavn":"41. Rettsavgjørelse fast bosted"},{"_type":"begrunnelse","bokmaal":[{"_type":"block","style":"normal","_key":"d8fdc904c0d9","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"d5e49a7b7ad5"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"5904459d7a85"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at ","_key":"7004a9862c96"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"848d9b788113"},{"_key":"0a79b7905e90","_type":"span","marks":[],"text":" ikke fyller vilkårene for å være medlem i folketrygden under oppholdet i utlandet "},{"_key":"8ce8eff4210c","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"_type":"span","marks":[],"text":".","_key":"9fb3ba5d5da1"}]}],"_id":"9820e253-9f71-49f2-a813-649f855b86ec","_updatedAt":"2023-09-25T10:27:03Z","apiNavn":"avslagVurderingIkkeMedlem","hjemler":["4","5"],"visningsnavn":"23. Vurdering ikke medlem","vilkaar":["BOSATT_I_RIKET"],"periodeType":"INGEN_UTBETALING","tema":"NASJONAL","navnISystem":"Vurdering ikke medlem","begrunnelsetype":"AVSLAG","nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn fødd ","_key":"c971af84bc75","_type":"span"},{"_type":"flettefelt","_key":"bc1196f125f0","flettefelt":"barnasFodselsdatoer"},{"marks":[],"text":" fordi vi har kome fram til at ","_key":"e904850befcf","_type":"span"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"f30ab32c68e5"},{"_key":"8e505b9e38d9","_type":"span","marks":[],"text":" ikkje fyller vilkåra for å vere medlem i folketrygda under opphaldet i utlandet"},{"_key":"ac3845f7d5e0","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"_key":"3fd80f3273e4","_type":"span","marks":[],"text":"."}],"_type":"block","style":"normal","_key":"e57036d0debc"}],"mappe":["AVSLAG"],"valgbarhet":"STANDARD","_rev":"BtltdVb0HP4g4WJfnr5DZJ","hjemlerFolketrygdloven":["2-5","2-8"],"vedtakResultat":"IKKE_INNVILGET","rolle":["SOKER","BARN"],"bosattIRiketTriggere":["MEDLEMSKAP"],"_createdAt":"2021-09-29T11:01:45Z"},{"periodeType":"FORTSATT_INNVILGET","begrunnelsetype":"FORTSATT_INNVILGET","nynorsk":[{"markDefs":[],"children":[{"_key":"8050b19ca7eb","_type":"span","marks":[],"text":"Du får barnetrygd fordi vi har kome fram til at du fortsatt har opphaldsrett."}],"_type":"block","style":"normal","_key":"fa6239222557"}],"apiNavn":"fortsattInnvilgetLovligOppholdTredjelandsborger","visningsnavn":"6. Tålt opphold tredjelandsborger","_rev":"h26rAhFEYSUtDGXJE0siYo","_createdAt":"2021-08-30T13:06:42Z","bokmaal":[{"markDefs":[],"children":[{"text":"Du får barnetrygd fordi vi har kommet fram til at du fortsatt har oppholdsrett.","_key":"37ad85eeca49","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"d141f1d72d15"}],"navnISystem":"Tålt opphold tredjelandsborger","hjemler":["2","4","11"],"rolle":["SOKER"],"valgbarhet":"STANDARD","_id":"98597876-2fb2-41f1-83dc-5545c9937d11","_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","vilkaar":["LOVLIG_OPPHOLD"],"tema":"NASJONAL","mappe":["FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:21:52Z"},{"hjemler":["2","4","11"],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har opphaldsrett som EØS-borgar. Familien bur i Noreg og du får utbetaling frå NAV som erstattar løn frå ","_key":"ebe1659920650"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"a586b6911f1c"},{"_type":"span","marks":[],"text":".","_key":"c8f980a01ca8"}],"_type":"block","style":"normal","_key":"321031ed1de8","markDefs":[]}],"_id":"98b16567-75b1-43bc-be51-40d7ad38180a","apiNavn":"innvilgetEosBorgerUtbetalingFraNAV","tema":"NASJONAL","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:48Z","bokmaal":[{"children":[{"marks":[],"text":"Du får barnetrygd fordi du har oppholdsrett som EØS-borger. Familien bor i Norge og du får utbetaling fra NAV som erstatter lønn fra ","_key":"3b2916f5f0140","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"368769edb1e5"},{"_type":"span","marks":[],"text":". ","_key":"f66c356a57dc"}],"_type":"block","style":"normal","_key":"f8e9c296cd42","markDefs":[]}],"rolle":["SOKER","BARN"],"visningsnavn":"71. EØS-borger utbetaling fra NAV","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["LOVLIG_OPPHOLD"],"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","navnISystem":"EØS-borger utbetaling fra NAV","_type":"begrunnelse","_createdAt":"2022-03-17T18:12:42Z","_rev":"h26rAhFEYSUtDGXJE0sTLq"},{"behandlingstema":"NASJONAL","nynorsk":[{"style":"normal","_key":"57f748d05550","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd fordi du har opphaldsrett som EØS-borgar. Familien bur i Noreg og ektefellen din jobbar her.","_key":"d149221b79850","_type":"span"}],"_type":"block"}],"mappe":["INNVILGET"],"apiNavn":"innvilgetTilleggstekstEosBorgerEktefelleJobber","visningsnavn":"84. Tilleggstekst EØS borger ektefelle jobber","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["BOSATT_I_RIKET","LOVLIG_OPPHOLD"],"rolle":["SOKER","BARN"],"begrunnelsetype":"INNVILGET","tema":"NASJONAL","bokmaal":[{"_key":"64567887dbd5","markDefs":[],"children":[{"_key":"c47c27b37cf40","_type":"span","marks":[],"text":"Du får barnetrygd fordi du har oppholdsrett som EØS-borger. Familien bor i Norge og ektefellen din jobber her."}],"_type":"block","style":"normal"}],"navnISystem":"Tilleggstekst EØS borger ektefelle jobber","_rev":"h26rAhFEYSUtDGXJE0sTLq","_createdAt":"2022-05-24T14:13:54Z","valgbarhet":"TILLEGGSTEKST","hjemler":["2","4"],"periodeType":"UTBETALING","_id":"99bcf8ef-836b-496d-87d8-9b20c9651996","_updatedAt":"2023-09-25T10:15:48Z","_type":"begrunnelse"},{"vedtakResultat":"INNVILGET_ELLER_ØKNING","_createdAt":"2022-04-28T13:58:48Z","_type":"begrunnelse","nynorsk":[{"children":[{"_key":"192f238be502","_type":"span","marks":[],"text":"Du får ikkje lenger utvida barnetrygd fordi du har fått barn. Dersom du fortsatt bur åleine med barna kan du søke om utvida barnetrygd på nytt."}],"_type":"block","style":"normal","_key":"f0b7990f4969","markDefs":[]}],"valgbarhet":"TILLEGGSTEKST","_updatedAt":"2023-09-25T10:15:48Z","bokmaal":[{"children":[{"text":"Du får ikke lenger utvidet barnetrygd fordi du har fått barn. Hvis du fortsatt bor alene med barna kan du søke om utvidet barnetrygd på nytt.","_key":"564762a41369","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"9e725d55737f","markDefs":[]}],"hjemler":["2","9","11"],"vilkaar":["BOR_MED_SOKER"],"periodeType":"UTBETALING","tema":"FELLES","_id":"9b64cac0-5f5c-4464-9854-4bb604900f40","mappe":["INNVILGET"],"navnISystem":"Tilleggstekst opphør utvidet nyfødt barn","apiNavn":"innvilgetTilleggstekstOpphorUtvidetNyfoedtBarn","visningsnavn":"78. Tilleggstekst opphør utvidet nyfødt barn","_rev":"h26rAhFEYSUtDGXJE0sTLq","begrunnelsetype":"INNVILGET"},{"_id":"9c5a1b23-ebb8-4149-93f6-5bdeff0f85dd","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:27:42Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi du ikke har meklingsattest.","_key":"bb94e49971aa"}],"_type":"block","style":"normal","_key":"fd54b2598909"}],"vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"AVSLAG","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"INGEN_UTBETALING","nynorsk":[{"_key":"6da835c27f20","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi du ikkje har meklingsattest.","_key":"ea62dcf0dfcb"}],"_type":"block","style":"normal"}],"visningsnavn":"33. Ikke meklingsattest","_rev":"BtltdVb0HP4g4WJfnr5Eto","hjemler":["9"],"tema":"FELLES","_createdAt":"2021-10-22T07:58:49Z","navnISystem":"Ikke meklingsattest","apiNavn":"avslagIkkeMeklingsattest","_type":"begrunnelse","valgbarhet":"STANDARD"},{"vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","_createdAt":"2021-10-22T12:05:15Z","valgbarhet":"STANDARD","_rev":"h26rAhFEYSUtDGXJE0sTLq","visningsnavn":"26. Flyttet sammen med annen forelder","mappe":["REDUKSJON"],"bokmaal":[{"style":"normal","_key":"27d8d5eb88cc","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi du har flyttet sammen med den andre forelderen i ","_key":"44fb637a1e71"},{"_key":"8fade952fcde","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"text":". Barnetrygden endres fra måneden etter at dere flyttet sammen.","_key":"12da7602ece5","_type":"span","marks":[]}],"_type":"block"}],"hjemler":["2","9","11"],"_id":"9d28fff2-5a21-4b2e-931a-0b5021581c54","apiNavn":"reduksjonFlyttetSammenMedAnnenForelder","_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"REDUKSJON","tema":"FELLES","nynorsk":[{"_key":"b04534337ec5","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du har flytta saman med den andre forelderen i ","_key":"9c9f064a800c"},{"_key":"c0691afa6113","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":". Barnetrygda er endra frå månaden etter at de flytta saman.","_key":"1cf1b36d4601"}],"_type":"block","style":"normal"}],"_updatedAt":"2023-09-25T10:15:48Z","navnISystem":"Flyttet sammen med annen forelder"},{"valgbarhet":"STANDARD","vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"AVSLAG","_createdAt":"2021-10-22T09:09:56Z","mappe":["AVSLAG"],"navnISystem":"Forvaring under 6 måneder samboer","hjemler":["9"],"vedtakResultat":"IKKE_INNVILGET","tema":"FELLES","nynorsk":[{"style":"normal","_key":"dbc4d114267d","markDefs":[],"children":[{"_key":"c1976f27c8de","_type":"span","marks":[],"text":"Utvida barnetrygd fordi forvaringsdommen til sambuaren din er under seks månader."}],"_type":"block"}],"_id":"9df0ab3d-b126-40ce-a7be-f44b25bcb385","_updatedAt":"2023-09-25T10:30:04Z","bokmaal":[{"style":"normal","_key":"598a2d303141","markDefs":[],"children":[{"text":"Utvidet barnetrygd fordi forvaringsdommen til samboeren din er under seks måneder.","_key":"692a4367d2be","_type":"span","marks":[]}],"_type":"block"}],"visningsnavn":"45. Forvaring under 6 måneder samboer","_type":"begrunnelse","periodeType":"INGEN_UTBETALING","apiNavn":"avslagForvaringUnder6MaanederSamboer","_rev":"FuD004taptHFqBZyEy7ZmQ"},{"nynorsk":[{"_key":"21d32b86b707","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du ikkje har meklingsattest for barn fødd ","_key":"b66fb931fb050"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"4237f40a516d"},{"_type":"span","marks":[],"text":".","_key":"31a2e3dc9f58"}],"_type":"block","style":"normal"}],"_id":"9df5678e-e694-41be-8233-95ea8f53ab63","_type":"begrunnelse","vedtakResultat":"REDUKSJON","begrunnelsetype":"REDUKSJON","tema":"FELLES","_rev":"h26rAhFEYSUtDGXJE0sTLq","periodeType":"UTBETALING","_updatedAt":"2023-09-25T10:15:48Z","navnISystem":"Mangler meklingsattest","hjemler":["2","9","11"],"vilkaar":["UTVIDET_BARNETRYGD"],"mappe":["REDUKSJON"],"bokmaal":[{"markDefs":[],"children":[{"_key":"673c249944420","_type":"span","marks":[],"text":"Barnetrygden endres fordi du ikke har meklingsattest for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"5773e028630e"},{"_type":"span","marks":[],"text":".","_key":"11e39cd31938"}],"_type":"block","style":"normal","_key":"a357b6270427"}],"apiNavn":"reduksjonManglerMeklingsattest","visningsnavn":"64. Mangler meklingsattest","_createdAt":"2022-05-02T08:26:14Z","valgbarhet":"STANDARD"},{"visningsnavn":"10. Ugyldig avtale om delt bosted","periodeType":"INGEN_UTBETALING","_createdAt":"2021-08-30T13:24:17Z","bokmaal":[{"style":"normal","_key":"272ae5c56bbf","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn født ","_key":"bdc8ba47db64"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"64467595e3ba"},{"text":" fordi du ikke har en gyldig avtale om delt bosted for ","_key":"0a7ec13df0fb","_type":"span","marks":[]},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"2c7fb5dfc0b5"},{"_type":"span","marks":[],"text":"","_key":"8940c8be6f7d"},{"_key":"6000b708d34a","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"_key":"4895a41af525","_type":"span","marks":[],"text":". Barnetrygden kan derfor ikke deles."}],"_type":"block"}],"navnISystem":"Ugyldig avtale om delt bosted","behandlingstema":"NASJONAL","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOR_MED_SOKER"],"begrunnelsetype":"AVSLAG","nynorsk":[{"_key":"b29fb927a641","markDefs":[],"children":[{"marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"f9f40d0387a1","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"dacc15afa376"},{"_type":"span","marks":[],"text":" fordi du ikkje har ein gyldig avtale om delt bustad for ","_key":"17a669ac7dd9"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"9d223237542e"},{"_type":"span","marks":[],"text":"","_key":"5d54950dc353"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"bd009939625e"},{"_type":"span","marks":[],"text":". Barnetrygda kan derfor ikkje delast.","_key":"e0bf61c791c4"}],"_type":"block","style":"normal"}],"valgbarhet":"STANDARD","_id":"9e9ed6b1-25cd-4862-ae08-6e23dadf111f","mappe":["AVSLAG"],"apiNavn":"avslagUgyldigAvtaleOmDeltBosted","hjemler":["2"],"_rev":"BtltdVb0HP4g4WJfnr5Avo","tema":"NASJONAL","_updatedAt":"2023-09-25T10:26:17Z"},{"begrunnelsetype":"FORTSATT_INNVILGET","nynorsk":[{"children":[{"_key":"fb013a1c5c10","_type":"span","marks":[],"text":"Du får delt barnetrygd fordi du fortsatt har avtale om delt bustad for "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"cf824ffeba2b","skalHaStorForbokstav":false},{"text":".","_key":"52b611f94478","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"0b807d294227","markDefs":[]}],"_createdAt":"2021-10-26T10:18:49Z","hjemler":["2"],"valgbarhet":"STANDARD","_id":"a0650cfd-2abd-4c6a-a883-fcf7ed70bdbc","mappe":["FORTSATT_INNVILGET"],"_rev":"h26rAhFEYSUtDGXJE0snu4","vilkaar":["BOR_MED_SOKER"],"tema":"NASJONAL","vedtakResultat":"INGEN_ENDRING","apiNavn":"fortsattInnvilgetFortsattAVtaleOmDeltBosted","visningsnavn":"32. Fortsatt avtale om delt bosted","_type":"begrunnelse","borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"FORTSATT_INNVILGET","_updatedAt":"2023-09-25T10:23:24Z","bokmaal":[{"style":"normal","_key":"8ec635ddfa26","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får delt barnetrygd fordi du fortsatt har avtale om delt bosted for ","_key":"5dac38f7e4bf"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"c5edb39e704a","skalHaStorForbokstav":false},{"_key":"142f6469ebca","_type":"span","marks":[],"text":"."}],"_type":"block"}],"navnISystem":"Fortsatt avtale om delt bosted"},{"apiNavn":"fortsattInnvilgetFlereBarnErBlittNorskeStatsborgere","nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Du får fortsatt barnetrygd fordi barna er blitt norske statsborgarar.","_key":"47e20cf8a09e0","_type":"span"}],"_type":"block","style":"normal","_key":"8c01314a7c4e"}],"mappe":["FORTSATT_INNVILGET"],"visningsnavn":"37. Flere barn er blitt norske statsborgere","_rev":"BtltdVb0HP4g4WJfnr501J","vedtakResultat":"INGEN_ENDRING","vilkaar":["LOVLIG_OPPHOLD"],"periodeType":"FORTSATT_INNVILGET","rolle":["BARN"],"tema":"NASJONAL","_updatedAt":"2023-09-25T10:23:43Z","hjemler":["2","4"],"_type":"begrunnelse","valgbarhet":"STANDARD","_id":"a0c63302-400a-4e2f-b252-5685ef5cf3e8","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får fortsatt barnetrygd fordi barna er blitt norske statsborgere.","_key":"f780868048580"}],"_type":"block","style":"normal","_key":"ae3409d06af6","markDefs":[]}],"navnISystem":"Flere barn er blitt norske statsborgere","begrunnelsetype":"FORTSATT_INNVILGET","_createdAt":"2022-03-18T14:40:47Z"},{"vilkaar":["UTVIDET_BARNETRYGD"],"_createdAt":"2022-08-01T11:42:32Z","ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"],"hjemler":["2","10"],"visningsnavn":"66. Småbarnstillegg hadde ikke full overgangsstønad-","_rev":"h26rAhFEYSUtDGXJE0sTLq","vedtakResultat":"REDUKSJON","bokmaal":[{"style":"normal","_key":"870bd60b72ad","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi du ikke hadde full overgangsstønad. Derfor får du ikke småbarnstillegget.","_key":"52784c4b453e0"}],"_type":"block"}],"navnISystem":"Småbarnstillegg hadde ikke full overgangsstønad","apiNavn":"reduksjonSmaabarnstilleggHaddeIkkeFullOvergangsstonad","tema":"FELLES","valgbarhet":"AUTOMATISK","utvidetBarnetrygdTriggere":["SMÅBARNSTILLEGG"],"_id":"a18be289-239a-40d8-8ba1-9d28508427d1","_updatedAt":"2023-09-25T10:15:48Z","behandlingstema":"NASJONAL","_type":"begrunnelse","periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du ikkje hadde full overgangsstønad. Difor får du ikkje småbarnstillegget.","_key":"a01cf65b68b50"}],"_type":"block","style":"normal","_key":"7ced6496b1be"}],"mappe":["REDUKSJON"]},{"begrunnelsetype":"OPPHØR","nynorsk":[{"style":"normal","_key":"5bef032e1213","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd ","_key":"440b260546c1"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"ac4361814eea"},{"_type":"span","marks":[],"text":" fordi ","_key":"d36b4b195d19"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"9a1259ee9e1b"},{"_type":"span","marks":[],"text":" ikkje har opphaldsløyve i Noreg ","_key":"93394e0b9479"},{"_key":"c961386a8115","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"marks":[],"text":".","_key":"62282fb23b26","_type":"span"}],"_type":"block"}],"_updatedAt":"2023-09-25T10:31:42Z","navnISystem":"Tredjelandsborger uten lovlig opphold\t","vedtakResultat":"IKKE_INNVILGET","_createdAt":"2021-08-30T10:35:52Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd ","_key":"64164d484810"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"9d5a72620180"},{"_key":"637373a6cd68","_type":"span","marks":[],"text":" fordi "},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"e0b2d53dc67e"},{"_type":"span","marks":[],"text":" ikke har oppholdstillatelse i Norge ","_key":"b400222d5ab0"},{"_key":"f54db10f7588","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"_type":"span","marks":[],"text":".","_key":"31f3c38217a7"}],"_type":"block","style":"normal","_key":"d6679bb9e0e9"}],"behandlingstema":"NASJONAL","_type":"begrunnelse","visningsnavn":"7. Tredjelandsborger uten lovlig opphold","_rev":"h26rAhFEYSUtDGXJE0t9UJ","vilkaar":["LOVLIG_OPPHOLD"],"periodeType":"INGEN_UTBETALING","valgbarhet":"STANDARD","apiNavn":"opphorHarIkkeOppholdstillatelse","hjemler":["4","11"],"_id":"a22636fa-d3db-4402-968b-893253c451fa","mappe":["OPPHØR"],"rolle":["SOKER","BARN"],"tema":"NASJONAL"},{"rolle":["SOKER","BARN"],"begrunnelsetype":"AVSLAG","visningsnavn":"2. Tredjelandsborger uten lovlig opphold ","behandlingstema":"NASJONAL","_type":"begrunnelse","periodeType":"INGEN_UTBETALING","nynorsk":[{"_type":"block","style":"normal","_key":"30a939a92cb4","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd ","_key":"a5d88fb8fff0"},{"_key":"7439b7b046b5","_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse"},{"marks":[],"text":" fordi ","_key":"8743d68252ce","_type":"span"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"19144c9b275f"},{"marks":[],"text":" ikkje har opphaldsløyve i Noreg","_key":"6265c1f7da8a","_type":"span"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"282042fbce9d"},{"marks":[],"text":".","_key":"ec0ff230d6f8","_type":"span"}]}],"_createdAt":"2021-08-30T12:50:37Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd ","_key":"f25a3db09edd"},{"_key":"8239a7a88e5e","_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse"},{"_type":"span","marks":[],"text":" fordi ","_key":"6422a3c3088e"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"612767865a59"},{"_key":"523cdb48651b","_type":"span","marks":[],"text":" ikke har oppholdstillatelse i Norge"},{"_type":"valgReferanse","_key":"6c1b38cad49e","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"_type":"span","marks":[],"text":".","_key":"30c24a297c18"}],"_type":"block","style":"normal","_key":"c4094fa83e44"}],"apiNavn":"avslagLovligOppholdTredjelandsborger","hjemler":["2","4"],"vedtakResultat":"IKKE_INNVILGET","_rev":"BtltdVb0HP4g4WJfnr59kJ","_id":"a2a84fbb-19bc-424c-8966-67f9efeed0cd","_updatedAt":"2023-09-25T10:25:45Z","valgbarhet":"STANDARD","mappe":["AVSLAG"],"navnISystem":"Tredjelandsborger uten lovlig opphold ","vilkaar":["LOVLIG_OPPHOLD"],"tema":"NASJONAL"},{"hjemler":["2","9","11"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"vedtakResultat":"REDUKSJON","valgbarhet":"STANDARD","_id":"a322d963-434f-4dfd-8342-5307815f95a7","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:48Z","bokmaal":[{"children":[{"marks":[],"text":"Barnetrygden endres fordi vi har kommet fram til at du ikke har egen husholdning og derfor er gift.","_key":"eca55ae8f619","_type":"span"}],"_type":"block","style":"normal","_key":"b4ec81c72e14","markDefs":[]}],"navnISystem":"Gift ikke egen husholdning","visningsnavn":"38. Gift ikke egen husholdning","periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","tema":"FELLES","_createdAt":"2021-10-23T06:09:26Z","apiNavn":"reduksjonGiftIkkeEgenHusholdning","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi vi har komme fram til at du ikkje har eiga hushaldning og derfor er gift.","_key":"60bbc79e79dd"}],"_type":"block","style":"normal","_key":"8be72891bb32"}]},{"_rev":"FuD004taptHFqBZyEy8AHF","rolle":["SOKER","BARN"],"bosattIRiketTriggere":["MEDLEMSKAP"],"nynorsk":[{"children":[{"text":"Barnetrygd for barn fødd ","_key":"53973392148f","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"46824e1d438c"},{"_type":"span","marks":[],"text":" fordi ","_key":"5127984b2bbc"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"b98f31ba1c68"},{"marks":[],"text":" ikkje er medlem i folketrygda etter trygdeavtale ","_key":"b711094d99a6","_type":"span"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"e96355731325"},{"_type":"span","marks":[],"text":".","_key":"d9077ed39aea"}],"_type":"block","style":"normal","_key":"483c433f0ad5","markDefs":[]}],"valgbarhet":"STANDARD","behandlingstema":"NASJONAL","hjemler":["2","4","22"],"vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"OPPHØR","mappe":["OPPHØR"],"apiNavn":"opphorSokerOgBarnIkkeLengerMedlemTrygdeavtale","_type":"begrunnelse","periodeType":"INGEN_UTBETALING","_id":"a450b595-e3bf-4986-b235-0c132bde38a5","_updatedAt":"2023-09-25T10:34:23Z","navnISystem":"Ikke medlem trygdeavtale","tema":"NASJONAL","_createdAt":"2021-09-24T11:23:59Z","bokmaal":[{"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"373fe4617194","_type":"span"},{"_key":"2d03702c0563","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi ","_key":"76218c4a86d6"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"df24df7838f0"},{"_type":"span","marks":[],"text":" ikke er medlem i folketrygden etter trygdeavtale ","_key":"d1436a129139"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"e773bc79b2af"},{"text":". ","_key":"d2593aeb6e33","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"77427368b16a","markDefs":[]}],"visningsnavn":"17. Ikke medlem trygdeavtale"},{"mappe":["INNVILGET"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"periodeType":"UTBETALING","rolle":["SOKER","BARN"],"valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"_key":"56c6ade50e42","_type":"span","marks":[],"text":"Du får barnetrygd "},{"_type":"valgReferanse","_key":"f8185fa13760","_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170"},{"_key":"897f57a19b6f","_type":"span","marks":[],"text":" under oppholdet i utlandet. Ved opphold i utlandet som ikke varer lenger enn 3 måneder, er dere regnet som bosatt i Norge."}],"_type":"block","style":"normal","_key":"20072cdc1c96"},{"style":"normal","_key":"bdfa2ba5ea2a","markDefs":[],"children":[{"_type":"span","marks":[],"text":"\n","_key":"cdb4eee4e1910"}],"_type":"block"}],"hjemler":["4"],"visningsnavn":"28. Søker og barn opphold i utlandet ikke mer enn 3 måneder","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","_createdAt":"2021-09-24T12:12:52Z","apiNavn":"innvilgetSokerOgBarnOppholdIUtlandetIkkeMerEnn3Maneder","tema":"NASJONAL","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"8890e766d94b"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"00a376b519f0"},{"_type":"span","marks":[],"text":" under opphaldet i utlandet. Ved opphald i utlandet som ikkje varer lenger enn 3 månader, er de rekna som busett i Noreg.","_key":"6512c46d02a2"}],"_type":"block","style":"normal","_key":"f6b7720ca9ac"}],"_id":"a51cc90c-d761-4d99-844f-bc40730417c9","_updatedAt":"2023-09-25T10:15:48Z","navnISystem":"Søker og barn opphold i utlandet ikke mer enn 3 måneder","bosattIRiketTriggere":["MEDLEMSKAP"]},{"apiNavn":"innvilgetSatsendring","visningsnavn":"10. Satsendring","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","_createdAt":"2021-08-27T08:48:21Z","_rev":"h26rAhFEYSUtDGXJE0sTLq","vedtakResultat":"INNVILGET_ELLER_ØKNING","hjemler":["2","10"],"tema":"FELLES","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi det har vore ei satsendring.","_key":"0380c4b932ca"}],"_type":"block","style":"normal","_key":"87019c005544"}],"valgbarhet":"AUTOMATISK","_id":"a577e08c-e95c-48fc-b398-3d1f39a3d169","mappe":["INNVILGET"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygden er endret fordi det har vært en satsendring.","_key":"cae3cafdaa6c"}],"_type":"block","style":"normal","_key":"f3bf1a6403ef","markDefs":[]}],"navnISystem":"Satsendring","_type":"begrunnelse","_updatedAt":"2023-09-25T10:15:48Z","ovrigeTriggere":["SATSENDRING"]},{"navnISystem":"Meklingsattest","behandlingstema":"NASJONAL","periodeType":"UTBETALING","_id":"a6a6f649-41e8-43ba-8151-5687c64eaa6f","bokmaal":[{"_key":"082ddabea6cc","markDefs":[],"children":[{"text":"","_key":"a4c47bd6ee29","_type":"span","marks":[]},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"38e515df962d","skalHaStorForbokstav":false},{"_key":"93ad2289d024","_type":"span","marks":[],"text":" utvidet barnetrygd fordi du bor alene med "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"cf83a8868583","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" og har meklingsattest fra ","_key":"c396707d8657"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"a8452511c0c9"},{"text":". ","_key":"a7908ddb1084","_type":"span","marks":[]},{"_type":"valgfeltV2","_key":"c13627da6c67","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"}},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at du fikk meklingsattesten.","_key":"791016119f1c"}],"_type":"block","style":"normal"}],"apiNavn":"innvilgetMeklingsattest","visningsnavn":"35. Meklingsattest","_rev":"h26rAhFEYSUtDGXJE0sTLq","begrunnelsetype":"INNVILGET","_createdAt":"2021-10-25T06:45:49Z","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:48Z","_type":"begrunnelse","nynorsk":[{"style":"normal","_key":"6ba95d533928","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"d6fc1fc2c788"},{"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"},"_type":"valgfeltV2","_key":"ee9017205ac2","skalHaStorForbokstav":false},{"marks":[],"text":" utvida barnetrygd fordi du bur aleine med ","_key":"79c9c62ca78a","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"d46d8ae651d4","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" og har meklingsattest frå ","_key":"db17262d2cc2"},{"_type":"flettefelt","_key":"c4e2f5882cfb","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_type":"span","marks":[],"text":". ","_key":"26c26908dcb7"},{"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"},"_type":"valgfeltV2","_key":"3cb0099319d2","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at du fekk meklingsattesten.","_key":"10c9d5e4e073"}],"_type":"block"}],"hjemler":["2","9","11"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["UTVIDET_BARNETRYGD"],"tema":"FELLES","valgbarhet":"STANDARD"},{"navnISystem":"Ikke dokumentert ektefelle død","_rev":"FuD004taptHFqBZyEy7KVZ","vedtakResultat":"IKKE_INNVILGET","nynorsk":[{"_key":"facc98fec971","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi vi har kome fram til at du ikkje har dokumentert at ektefellen din er død.","_key":"47ad992c0898"}],"_type":"block","style":"normal"}],"valgbarhet":"STANDARD","_id":"a6c2daf6-9ba0-403d-ba81-92af1c0b7647","_type":"begrunnelse","periodeType":"INGEN_UTBETALING","_createdAt":"2021-10-22T07:50:29Z","_updatedAt":"2023-09-25T10:27:21Z","apiNavn":"avslagIkkeDokumentertEktefelleDod","hjemler":["9"],"visningsnavn":"28. Ikke dokumentert ektefelle død","vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"AVSLAG","tema":"NASJONAL","mappe":["AVSLAG"],"bokmaal":[{"_type":"block","style":"normal","_key":"3cf9381def80","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi vi har kommet fram til at du ikke har dokumentert at ektefellen din er død.","_key":"41fabc75cd2e"}]}]},{"_id":"a6f8bdfd-09df-4310-a7ca-287f070395a0","hjemler":["2","12"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","endringsaarsaker":["DELT_BOSTED"],"_updatedAt":"2023-09-25T10:15:48Z","_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","tema":"FELLES","_createdAt":"2022-03-07T19:28:44Z","apiNavn":"endretUtbetalingDeltBostedFaarKunEtterbetaltUtvidet","visningsnavn":"10. NY delt bosted - får kun etterbetalt utvidet","mappe":["ENDRET_UTBETALINGSPERIODE"],"bokmaal":[{"_key":"a39f135a0b7d","markDefs":[],"children":[{"marks":[],"text":"Ordinær barnetrygd for tiden før du søkte for barn født ","_key":"e864f032d15b0","_type":"span"},{"_key":"c0ab42f1bdff","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" er allerede utbetalt til den andre forelderen. Vi kan ikke etterbetale barnetrygd som allerede er utbetalt. Det er opp til dere å bli enige om hvordan dere vil fordele den ordinære barnetrygden som er utbetalt. Det er den utvidede delen av barnetrygden du får etterbetalt.","_key":"9401670edb48"}],"_type":"block","style":"normal"}],"valgbarhet":"STANDARD","navnISystem":"Delt bosted - får kun etterbetalt utvidet","periodeType":"UTBETALING","nynorsk":[{"style":"normal","_key":"5bcbd39f905e","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Ordinær barnetrygd for tiden før du søkte for barn fødd ","_key":"d0f9a90081d30"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"5a250dee9fa1"},{"_key":"a31756a3ac05","_type":"span","marks":[],"text":" er allereie utbetalt til den andre forelderen. Vi kan ikkje etterbetale barnetrygd som allereie er utbetalt. Det er opp til dykk å bli einige om korleis de vil fordele den ordinære barnetrygda som er utbetalt. Det er den utvida delen av barnetrygda du får etterbetalt."}],"_type":"block"}],"endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"UTBETALING_IKKE_RELEVANT"},{"periodeType":"UTBETALING","bokmaal":[{"style":"normal","_key":"f64535cd5a32","markDefs":[],"children":[{"text":"Du får hele barnetrygden for barn født ","_key":"8e43600672ba0","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"3e5f2b7df226"},{"marks":[],"text":". Du har sendt inn en avtale om delt bosted. Det er kun barnets foreldre som kan avtale delt bosted. Barnetrygden kan derfor ikke deles. ","_key":"8c20ab162698","_type":"span"}],"_type":"block"}],"vilkaar":["BOR_MED_SOKER"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"NASJONAL","_createdAt":"2022-10-28T11:34:54Z","apiNavn":"innvilgetFullUtbetalingAvtaleDeltBostedAnnenOmsorgsperson","behandlingstema":"NASJONAL","valgbarhet":"STANDARD","_id":"a882b1fa-eedf-40d6-b01a-e0bf725e257d","_updatedAt":"2023-09-25T10:15:48Z","visningsnavn":"92. Full utbetaling avtale delt bosted annen omsorgsperson","hjemler":["2","11"],"begrunnelsetype":"INNVILGET","nynorsk":[{"_key":"242ae1a21f84","markDefs":[],"children":[{"marks":[],"text":"Du får heile barnetrygda for barn fødd ","_key":"6f7c87dfdbd60","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"d33f076a3e1e"},{"_type":"span","marks":[],"text":". Du har sendt inn ein avtale om delt bustad. Det er berre barnet sine foreldre som kan avtale delt bustad. Barnetrygda kan derfor ikkje delast. ","_key":"6fe821de1f8c"}],"_type":"block","style":"normal"}],"mappe":["INNVILGET"],"navnISystem":"Full utbetaling avtale delt bosted annen omsorgsperson"},{"vedtakResultat":"IKKE_INNVILGET","bokmaal":[{"style":"normal","_key":"a4b891931ef3","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi vi har kommet fram til at ektefellen din ikke er under tvungent psykisk helsevern.","_key":"1e47a4b4ae87"}],"_type":"block"}],"_type":"begrunnelse","nynorsk":[{"style":"normal","_key":"ae134b6e3528","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi vi har kome fram til at ektefellen din ikkje er under tvungent psykisk helsevern.","_key":"b694f7439da2"}],"_type":"block"}],"mappe":["AVSLAG"],"_id":"a8e314dd-674c-4f1c-a99b-4a6bcfd76b7d","_updatedAt":"2023-09-25T10:30:07Z","navnISystem":"Vurdering ikke tvungent psykisk helsevern ektefelle","visningsnavn":"46. Vurdering ikke tvungent psykisk helsevern ektefelle","valgbarhet":"STANDARD","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","tema":"FELLES","_createdAt":"2021-10-22T09:11:42Z","apiNavn":"avslagVurderingIkkeTvungentPsykiskHelsevernEktefelle","hjemler":["9"],"_rev":"FuD004taptHFqBZyEy7a8t"},{"navnISystem":"Medlem av folketrygden uten dato","behandlingstema":"NASJONAL","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","_id":"a91a508d-1a9f-48dd-b469-75ee96efa58d","apiNavn":"innvilgetMedlemAvFolketrygdenUtenDato","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi ","_key":"26b0818871010"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"86849944b45c","skalHaStorForbokstav":false},{"_type":"span","marks":["em"],"text":" ","_key":"26b0818871015"},{"_type":"span","marks":[],"text":"er medlem av folketrygda.","_key":"26b0818871016"}],"_type":"block","style":"normal","_key":"3a95919271f7"}],"valgbarhet":"STANDARD","mappe":["INNVILGET"],"bokmaal":[{"_type":"block","style":"normal","_key":"c491d60dee23","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd fordi ","_key":"c654b3a7c29d0","_type":"span"},{"valgReferanse":{"_type":"reference","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},"_type":"valgfeltV2","_key":"0e6f3b1a4432","skalHaStorForbokstav":false},{"_type":"span","marks":["em"],"text":" ","_key":"c654b3a7c29d5"},{"_type":"span","marks":[],"text":"er medlem av folketrygden.","_key":"c654b3a7c29d6"}]}],"periodeType":"UTBETALING","bosattIRiketTriggere":["MEDLEMSKAP"],"_updatedAt":"2023-09-25T10:15:48Z","tema":"NASJONAL","_createdAt":"2022-09-06T07:41:00Z","hjemler":["2","4"],"visningsnavn":"90. Medlem av folketrygden uten dato","_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"rolle":["SOKER","BARN"]},{"nynorsk":[{"_key":"1299e765e934","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du ikkje lenger har full overgangsstønad. Difor får du ikkje småbarnstillegget.","_key":"f5b3d398544b"}],"_type":"block","style":"normal"}],"mappe":["REDUKSJON"],"visningsnavn":"58. Småbarnstillegg ikke lenger full overgangsstønad","_rev":"h26rAhFEYSUtDGXJE0sTLq","begrunnelsetype":"REDUKSJON","_id":"aa6c87d1-4507-4755-9002-8753b86af7e4","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygden endres fordi du ikke lenger har full overgangsstønad. Derfor får du ikke småbarnstillegget.","_key":"8f32059ace26","_type":"span"}],"_type":"block","style":"normal","_key":"493f5ffe0c52"}],"apiNavn":"reduksjonSmaabarnstilleggIkkeLengerFullOvergangsstonad","hjemler":["2","10"],"_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","tema":"FELLES","utvidetBarnetrygdTriggere":["SMÅBARNSTILLEGG"],"_createdAt":"2021-11-12T14:54:42Z","navnISystem":"Småbarnstillegg ikke lenger full overgangsstønad","behandlingstema":"NASJONAL","vedtakResultat":"REDUKSJON","valgbarhet":"AUTOMATISK","_updatedAt":"2023-09-25T10:15:48Z"},{"hjemler":["2","4"],"visningsnavn":"89. Opphold i utlandet ikke mer enn tre måneder","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","_createdAt":"2022-09-05T09:58:46Z","mappe":["INNVILGET"],"apiNavn":"innvilgetOppholdIUtlandetIkkeMerEnnTreMaaneder","periodeType":"UTBETALING","tema":"NASJONAL","bosattIRiketTriggere":["VURDERING_ANNET_GRUNNLAG"],"nynorsk":[{"_type":"block","style":"normal","_key":"3448f47e9163","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"e8585f632e680"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"6e02c43fcdb6"},{"_type":"span","marks":[],"text":" fordi ","_key":"23f5992653fc"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"fc302d27d1cc","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" er flytta tilbake til Noreg. Ved opphald i utlandet som ikkje varer lenger enn tre månader, er de fortsatt rekna som busett i Noreg.","_key":"e8585f632e686"}]}],"bokmaal":[{"style":"normal","_key":"95c02875d3f7","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn født ","_key":"50cec337870e0"},{"_type":"flettefelt","_key":"04c61c365414","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi ","_key":"a9aebf55fed8"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"203a769ee577","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" er flyttet tilbake til Norge. Ved opphold i utlandet som ikke varer lenger enn tre måneder, er dere fortsatt regnet som bosatt i Norge.","_key":"50cec337870e6"}],"_type":"block"}],"behandlingstema":"NASJONAL","_rev":"h26rAhFEYSUtDGXJE0sTLq","vilkaar":["BOSATT_I_RIKET"],"valgbarhet":"STANDARD","_id":"aaba6ce9-3cab-4bab-8de7-a23dee63b13e","navnISystem":"Opphold i utlandet ikke mer enn tre måneder","rolle":["SOKER","BARN"],"_updatedAt":"2023-09-25T10:15:48Z","_type":"begrunnelse"},{"_type":"begrunnelse","vedtakResultat":"REDUKSJON","_id":"aba72047-201b-4358-8185-4112c954d7d7","apiNavn":"reduksjonAutovedtakBarn6Aar","_rev":"h26rAhFEYSUtDGXJE0sTLq","valgbarhet":"AUTOMATISK","bokmaal":[{"markDefs":[],"children":[{"_key":"6a02c29371f5","_type":"span","marks":[],"text":"Barnetrygden reduseres fordi barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"cc459b712a13"},{"_type":"span","marks":[],"text":" fyller 6 år.","_key":"f6af11c39d41"}],"_type":"block","style":"normal","_key":"adbcce07a3ff"}],"navnISystem":"Autovedtak barn 6 år","tema":"NASJONAL","ovrigeTriggere":["BARN_MED_6_ÅRS_DAG","ALLTID_AUTOMATISK"],"periodeType":"UTBETALING","_createdAt":"2021-11-03T14:16:45Z","begrunnelsetype":"REDUKSJON","nynorsk":[{"markDefs":[],"children":[{"text":"Barnetrygda er redusert fordi barn fødd ","_key":"6d0f23d2e954","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"30ab063def93"},{"_type":"span","marks":[],"text":" fyller 6 år.","_key":"7e8ff6f74ce3"}],"_type":"block","style":"normal","_key":"1dfc02d80775"}],"mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:48Z","hjemler":["2","10"],"visningsnavn":"11 A. Autovedtak barn 6 år"},{"hjemler":["2","4"],"visningsnavn":"82. Tilleggstekst EØS borger jobber","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","nynorsk":[{"_type":"block","style":"normal","_key":"7d62bd0b253f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har opphaldsrett som EØS-borgar. Familien bur i Noreg og du jobbar her.","_key":"a25779f6785c0"}]}],"apiNavn":"innvilgetTilleggstekstEosBorgerJobber","_type":"begrunnelse","tema":"NASJONAL","_id":"abb4e2bf-2a2f-4c1c-9b75-3125d8641521","navnISystem":"Tilleggstekst EØS borger jobber","_rev":"h26rAhFEYSUtDGXJE0sTLq","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["BOSATT_I_RIKET","LOVLIG_OPPHOLD"],"rolle":["SOKER","BARN"],"bokmaal":[{"_key":"fffd6c0911dd","markDefs":[],"children":[{"_key":"2970f071ca8c0","_type":"span","marks":[],"text":"Du får barnetrygd fordi du har oppholdsrett som EØS-borger. Familien bor i Norge og du jobber her."}],"_type":"block","style":"normal"}],"behandlingstema":"NASJONAL","_createdAt":"2022-05-24T14:08:22Z","valgbarhet":"TILLEGGSTEKST","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:48Z"},{"visningsnavn":"9. Ikke mottatt opplysninger","_rev":"BtltdVb0HP4g4WJfnr5Qto","valgbarhet":"TILLEGGSTEKST","bokmaal":[{"style":"normal","_key":"e4209da5c20a","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du har ikke sendt oss de opplysningene vi ba om.","_key":"57f5e87fd09c"}],"_type":"block"}],"ovrigeTriggere":["MANGLER_OPPLYSNINGER"],"hjemler":["17","18"],"periodeType":"INGEN_UTBETALING","tema":"FELLES","nynorsk":[{"_type":"block","style":"normal","_key":"22e5f82e42f8","markDefs":[],"children":[{"text":"Du har ikkje sendt oss dei opplysningane vi ba om.","_key":"973ce356d3db","_type":"span","marks":[]}]}],"_createdAt":"2021-08-30T10:37:17Z","mappe":["OPPHØR"],"navnISystem":"Ikke mottatt opplysninger","apiNavn":"opphorIkkeMottattOpplysninger","_type":"begrunnelse","begrunnelsetype":"OPPHØR","_id":"abe68af6-a763-42c5-a708-fe07b9caa72e","vedtakResultat":"IKKE_INNVILGET","_updatedAt":"2023-09-25T10:31:46Z"},{"_updatedAt":"2023-09-25T10:15:48Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi du har en avtale om delt bosted for barn født ","_key":"b1fab82004fc0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"01f4ba57cdb4"},{"_type":"span","marks":[],"text":".","_key":"5beb6e3fddb2"}],"_type":"block","style":"normal","_key":"7bdd32448f5a"}],"_type":"begrunnelse","vedtakResultat":"REDUKSJON","vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["DELT_BOSTED"],"begrunnelsetype":"REDUKSJON","_createdAt":"2022-03-23T08:38:01Z","behandlingstema":"NASJONAL","tema":"NASJONAL","apiNavn":"reduksjonDeltBostedGenerell","visningsnavn":"61. Delt bosted generell","_rev":"h26rAhFEYSUtDGXJE0sTLq","periodeType":"UTBETALING","nynorsk":[{"markDefs":[],"children":[{"text":"Barnetrygda er endra fordi du har ein avtale om delt bustad for barn fødd ","_key":"d1c8043715e10","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"d64021e51aef"},{"_key":"1a6bd30037af","_type":"span","marks":[],"text":"."}],"_type":"block","style":"normal","_key":"e8c1be01ce67"}],"navnISystem":"Delt bosted generell","hjemler":["2"],"valgbarhet":"STANDARD","_id":"ad5f3791-719b-41c6-9927-a70bfba13158","mappe":["REDUKSJON"]},{"tema":"FELLES","nynorsk":[{"_key":"7ec48119ba32","markDefs":[],"children":[{"marks":[],"text":"Du får utvida barnetrygd fordi sambuaren din fortsatt er varetektsfengsla.","_key":"66a40d8bb74b","_type":"span"}],"_type":"block","style":"normal"}],"_updatedAt":"2023-09-25T10:22:57Z","vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET","visningsnavn":"24. Varetektsfengsel samboer","bokmaal":[{"_key":"d6109446964f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utvidet barnetrygd fordi samboeren din fortsatt er varetektsfengslet.","_key":"182a9c10e43e"}],"_type":"block","style":"normal"}],"_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"FORTSATT_INNVILGET","_createdAt":"2021-10-25T10:13:02Z","valgbarhet":"STANDARD","_id":"ad7e4e6e-91fa-415f-ba78-5595de147da2","apiNavn":"fortsattInnvilgetVaretektsfengselSamboer","_rev":"h26rAhFEYSUtDGXJE0smFR","mappe":["FORTSATT_INNVILGET"],"navnISystem":"Varetektsfengsel samboer","hjemler":["9"]},{"navnISystem":"Var ikke medlem","_rev":"h26rAhFEYSUtDGXJE0sTLq","vedtakResultat":"REDUKSJON","vilkaar":["BOSATT_I_RIKET"],"valgbarhet":"STANDARD","behandlingstema":"NASJONAL","tema":"NASJONAL","bosattIRiketTriggere":["MEDLEMSKAP"],"_id":"adb0e75e-f606-4692-b646-fed3db24fcd4","hjemler":["4","5"],"_type":"begrunnelse","begrunnelsetype":"REDUKSJON","_createdAt":"2021-11-05T10:48:53Z","bokmaal":[{"style":"normal","_key":"2f3102844487","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi du og barnet/barna født ","_key":"cc110c56e9c5"},{"_type":"flettefelt","_key":"c604217e3613","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" ikke var medlem i folketrygden under oppholdet i utlandet.","_key":"a05de5942360"}],"_type":"block"}],"mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:48Z","apiNavn":"reduksjonVarIkkeMedlem","visningsnavn":"53. Var ikke medlem","hjemlerFolketrygdloven":["2-5","2-8"],"periodeType":"UTBETALING","rolle":["SOKER","BARN"],"nynorsk":[{"style":"normal","_key":"61fb4cdf88a0","markDefs":[],"children":[{"_key":"0055268046c5","_type":"span","marks":[],"text":"Barnetrygda er endra fordi du og barnet/barna fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"272969f15d6f"},{"_type":"span","marks":[],"text":" ikkje var medlem i folketrygda under opphaldet i utlandet.","_key":"c9fdc3083019"}],"_type":"block"}],"ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"]},{"behandlingstema":"NASJONAL","_type":"begrunnelse","_createdAt":"2021-07-27T08:07:24Z","mappe":["INNVILGET"],"navnISystem":"Adopsjon, surrogati: Omsorgen for barn","apiNavn":"innvilgetOmsorgForBarn","hjemler":["2","4","11"],"tema":"NASJONAL","_id":"adopsjonSurrogatiOmsorgenForBarn","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","_updatedAt":"2023-09-25T10:15:48Z","bokmaal":[{"_type":"block","style":"normal","_key":"adopsjonSurrogatiOmsorgenForBarn1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"adopsjonSurrogatiOmsorgenForBarn2"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"9c92a76a2104"},{"_type":"span","marks":[],"text":" fordi du har omsorgen for ","_key":"00ac05749c00"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"c8393d51a6d2"},{"marks":[],"text":" fra ","_key":"c1263023cb3c","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"48a447b25ffb"},{"text":".","_key":"474766938fdd","_type":"span","marks":[]}]}],"visningsnavn":"3. Adopsjon, surrogati: Omsorgen for barn","_rev":"h26rAhFEYSUtDGXJE0sTLq","vilkaar":["BOR_MED_SOKER"],"begrunnelsetype":"INNVILGET","nynorsk":[{"_key":"adopsjonSurrogatiOmsorgenForBarn1","markDefs":[],"children":[{"text":"Du får barnetrygd ","_key":"adopsjonSurrogatiOmsorgenForBarn2","_type":"span","marks":[]},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"8bc558d67725"},{"text":" fordi du har omsorga for ","_key":"7c8b731c888b","_type":"span","marks":[]},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"d0c05bae9938"},{"text":" frå ","_key":"a3bddc6fd6a1","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"d1cd4be37b1b"},{"_type":"span","marks":[],"text":".","_key":"6ff395458803"}],"_type":"block","style":"normal"}],"valgbarhet":"STANDARD"},{"navnISystem":"Vurdering ikke separert","visningsnavn":"30. Vurdering ikke separert","vedtakResultat":"IKKE_INNVILGET","nynorsk":[{"children":[{"text":"Utvida barnetrygd fordi vi har kome fram til at du ikkje er separert.","_key":"00bec203c839","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"ba99fae944a5","markDefs":[]}],"valgbarhet":"STANDARD","vilkaar":["UTVIDET_BARNETRYGD"],"mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:27:32Z","bokmaal":[{"_type":"block","style":"normal","_key":"3d546f7567bc","markDefs":[],"children":[{"_key":"84744cac0619","_type":"span","marks":[],"text":"Utvidet barnetrygd fordi vi har kommet fram til at du ikke er separert."}]}],"_id":"ae55b512-8e01-46c8-b444-cc4e6ef9199f","apiNavn":"avslagVurderingIkkeSeparert","hjemler":["9"],"_rev":"BtltdVb0HP4g4WJfnr5ERJ","_type":"begrunnelse","periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","tema":"NASJONAL","_createdAt":"2021-10-22T07:54:08Z"},{"bokmaal":[{"style":"normal","_key":"451ca5571cf6","markDefs":[],"children":[{"_key":"1ad2781312540","_type":"span","marks":[],"text":"Dere får barnetrygd fordi barnet bor fast i institusjonen fra "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"e7a51759db3c"},{"_type":"span","marks":[],"text":".","_key":"547f71dca76c"}],"_type":"block"}],"vilkaar":["BOR_MED_SOKER"],"tema":"NASJONAL","nynorsk":[{"markDefs":[],"children":[{"_key":"863233760fd60","_type":"span","marks":[],"text":"De får barnetrygd fordi barnet bur fast i institusjonen frå "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"89ae9910da23"},{"_type":"span","marks":[],"text":".","_key":"bb163903ec50"}],"_type":"block","style":"normal","_key":"c13e0d0b8952"}],"mappe":["INSTITUSJON","INNVILGET"],"navnISystem":"Bor fast i institusjon","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","periodeType":"UTBETALING","_createdAt":"2022-11-03T14:38:49Z","_updatedAt":"2023-09-25T10:15:48Z","apiNavn":"innvilgetBorFastIInstitusjon","visningsnavn":"1. Bor fast i institusjon","behandlingstema":"NASJONAL_INSTITUSJON","_type":"begrunnelse","_id":"ae57b093-233d-4f27-adcc-e157e1cf1c5b","fagsakType":"INSTITUSJON","hjemler":["2","11"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","valgbarhet":"SAKSPESIFIKK"},{"nynorsk":[{"_key":"974b97f1067c","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"4dda60adbef4"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"96cd1f79a063"},{"text":" fordi avtalen om delt bustad for ","_key":"30848d24b708","_type":"span","marks":[]},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"d1c7e32c3ee1","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" er opphøyrt ","_key":"988f51b7bd25"},{"_key":"dfeee6854060","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"_type":"span","marks":[],"text":".","_key":"e10f550c6882"}],"_type":"block","style":"normal"}],"hjemler":["2","11"],"behandlingstema":"NASJONAL","vilkaar":["BOR_MED_SOKER"],"_createdAt":"2021-08-30T11:28:33Z","valgbarhet":"STANDARD","apiNavn":"opphorDeltBostedOpphortEnighet","borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"INGEN_UTBETALING","begrunnelsetype":"OPPHØR","_id":"ae99ad7b-b7b1-4f11-b13d-2d07f7a0a336","mappe":["OPPHØR"],"_updatedAt":"2023-09-25T10:31:49Z","bokmaal":[{"_key":"7997e6f474c5","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"a3ed9fb0514d"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"cc251b9473af"},{"marks":[],"text":" fordi avtalen om delt bosted for ","_key":"f258337db532","_type":"span"},{"_key":"64ecfd6a67b7","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_key":"3ffdd640e0fa","_type":"span","marks":[],"text":" er opphørt "},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"d351b5e90a15"},{"_type":"span","marks":[],"text":".","_key":"d253336cf246"}],"_type":"block","style":"normal"},{"style":"normal","_key":"34d4918e3975","markDefs":[],"children":[{"_type":"span","marks":[],"text":"\n","_key":"376f23bbbe1b0"}],"_type":"block"}],"navnISystem":"Enighet om opphør av avtale om delt bosted","visningsnavn":"10. Enighet om opphør av avtale om delt bosted","_rev":"h26rAhFEYSUtDGXJE0unCP","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","tema":"NASJONAL"},{"mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:48Z","vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","rolle":["BARN"],"bosattIRiketTriggere":["MEDLEMSKAP"],"nynorsk":[{"_type":"block","style":"normal","_key":"44a394f970e4","markDefs":[],"children":[{"marks":[],"text":"Vi har kome fram til at barn fødd ","_key":"9f7beeae67ad","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"f3f13415c069"},{"_type":"span","marks":[],"text":" har hatt fleire korte opphald i utlandet dei to siste åra. Opphalda i utlandet til saman meir enn 6 månader for kvart år. ","_key":"de8576a2ec35"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"82a518b05a69"},{"_type":"span","marks":[],"text":" er derfor ikkje rekna som busett i Noreg frå ","_key":"d4b2781875a5"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"0d62f2271aaf"},{"_type":"span","marks":[],"text":".","_key":"b97698274474"}]}],"_createdAt":"2021-09-24T12:36:14Z","bokmaal":[{"_key":"51d04fa68a47","markDefs":[],"children":[{"_key":"44308eb017b8","_type":"span","marks":[],"text":"Vi har kommet fram til at barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"593b3cad4c56"},{"marks":[],"text":" har hatt flere korte opphold i utlandet de to siste årene. Oppholdene i utlandet utgjør til sammen mer enn 6 måneder for hvert år. ","_key":"321c8ee5c16a","_type":"span"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"af55ec3f2fbf"},{"marks":[],"text":" regnes derfor ikke som bosatt i Norge fra ","_key":"a6db626eaa4a","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"519a129752ab"},{"_type":"span","marks":[],"text":".","_key":"35e82c0b3a2e"}],"_type":"block","style":"normal"}],"hjemler":["4"],"vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"REDUKSJON","tema":"NASJONAL","navnISystem":"Vurdering barn flere korte opphold i utlandet siste to år ","_rev":"h26rAhFEYSUtDGXJE0sTLq","apiNavn":"reduksjonVurderingBarnFlereKorteOppholdIUtlandetSisteToAr","visningsnavn":"19. Vurdering barn flere korte opphold i utlandet siste to år ","_type":"begrunnelse","valgbarhet":"STANDARD","_id":"afc960a1-405e-4408-9daf-38cdd2ba8e82"},{"_rev":"BtltdVb0HP4g4WJfnr5jco","vilkaar":["BOSATT_I_RIKET"],"mappe":["OPPHØR"],"bokmaal":[{"markDefs":[],"children":[{"_key":"71d2d7bf249a0","_type":"span","marks":[],"text":"Barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"85d77d560887"},{"marks":[],"text":" fordi du ikke var medlem i folketrygden.","_key":"ec25a38fb9f2","_type":"span"}],"_type":"block","style":"normal","_key":"8c9395d56dd0"}],"ovrigeTriggere":["GJELDER_FØRSTE_PERIODE"],"vedtakResultat":"IKKE_INNVILGET","rolle":["SOKER"],"bosattIRiketTriggere":["MEDLEMSKAP"],"navnISystem":"Bosatt i Norge var ikke medlem","apiNavn":"opphorBosattINorgeVarIkkeMedlem","visningsnavn":"44. Bosatt i Norge var ikke medlem","behandlingstema":"NASJONAL","_type":"begrunnelse","valgbarhet":"STANDARD","hjemler":["2","4"],"begrunnelsetype":"OPPHØR","nynorsk":[{"_key":"a3ce159527eb","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"f77c689b89b1"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"98b2d049519b"},{"_key":"9c0dce12801a","_type":"span","marks":[],"text":" fordi du ikkje var medlem i folketrygda."}],"_type":"block","style":"normal"}],"periodeType":"INGEN_UTBETALING","tema":"NASJONAL","_createdAt":"2022-03-29T06:36:47Z","_id":"b0529eaa-f8da-45a1-a355-261203ab0960","_updatedAt":"2023-09-25T10:36:30Z"},{"navnISystem":"Flyttet etter skilt","_type":"begrunnelse","_updatedAt":"2023-09-25T10:15:48Z","_rev":"h26rAhFEYSUtDGXJE0sTLq","tema":"FELLES","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"7e01d552e2d1"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"9beb86be3ea0","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi du er skild og bur aleine med ","_key":"4bce208b2eb3"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"1feff1d1a4b9","skalHaStorForbokstav":false},{"text":". Du og den tidligare ektefellen din flytta frå kvarandre ","_key":"e6e218611a01","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"7996a8791105"},{"_type":"span","marks":[],"text":". ","_key":"c5e1794c1e08"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"8bfbf6955a00","skalHaStorForbokstav":true},{"marks":[],"text":" utvida barnetrygd frå månaden etter at de flytta frå kvarandre.","_key":"bb90d8c1af0b","_type":"span"}],"_type":"block","style":"normal","_key":"64c956de6a9d"}],"_createdAt":"2021-10-25T06:53:17Z","begrunnelsetype":"INNVILGET","_id":"b0a21317-23b1-4f7a-af29-2c0ed661661c","mappe":["INNVILGET"],"visningsnavn":"38. Flyttet etter skilt","behandlingstema":"NASJONAL","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","apiNavn":"innvilgetFlyttetEtterSkilt","hjemler":["2","9","11"],"valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"_key":"25c55eca451d","_type":"span","marks":[],"text":""},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"a13146d8b8ef","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd fordi du er skilt og bor alene med ","_key":"dc36e1c622d8"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"de473db94426","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Du og den tidligere ektefellen din flyttet fra hverandre ","_key":"0894ddf1f518"},{"_type":"flettefelt","_key":"e01633252568","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"marks":[],"text":". ","_key":"25728c09b15a","_type":"span"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"b8d5d747e0c5","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at dere flyttet fra hverandre.","_key":"9032327beeb6"}],"_type":"block","style":"normal","_key":"9df8e8b5c10b"}]},{"hjemler":["2","9","11","12"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","periodeType":"UTBETALING","valgbarhet":"STANDARD","_id":"b0b2348b-e403-402c-b09d-a2ebd33bc4bf","_updatedAt":"2023-09-25T10:15:48Z","apiNavn":"innvilgetEnsligMindrearigFlyktning","visningsnavn":"58. Enslig mindreårig flyktning","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["UTVIDET_BARNETRYGD"],"tema":"FELLES","bokmaal":[{"_type":"block","style":"normal","_key":"afa379b386f7","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utvidet barnetrygd fordi vi har kommet fram til at du bor alene i egen leilighet.","_key":"972fccf508db"}]}],"behandlingstema":"NASJONAL","_createdAt":"2021-10-25T09:58:41Z","mappe":["INNVILGET"],"navnISystem":"Enslig mindreårig flyktning","begrunnelsetype":"INNVILGET","nynorsk":[{"_key":"0be6e8494dc1","markDefs":[],"children":[{"text":"Du får utvida barnetrygd fordi vi har kome fram til at du bur aleine i eigen leilighet.","_key":"47c2ad662169","_type":"span","marks":[]}],"_type":"block","style":"normal"}]},{"_updatedAt":"2023-09-25T10:35:48Z","hjemler":["2"],"vilkaar":["BOR_MED_SOKER"],"_id":"b237bef1-4acf-41d1-8fa6-ffb97a2fdf3b","navnISystem":"Avtale delt bosted ikke gyldig","_rev":"BtltdVb0HP4g4WJfnr5gVJ","tema":"NASJONAL","vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"OPPHØR","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"587b5b2e14d4"},{"_key":"9a8c0e8ae3e3","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi avtalen om delt bustad for ","_key":"c552517a1ea7"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"8dee64429376","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikkje var gyldig. ","_key":"9bead0944ca9"}],"_type":"block","style":"normal","_key":"a43eacadf541"}],"valgbarhet":"STANDARD","ovrigeTriggere":["GJELDER_FØRSTE_PERIODE"],"apiNavn":"opphorAvtaleDeltBostedIkkeGyldig","visningsnavn":"34. Avtale delt bosted ikke gyldig","_type":"begrunnelse","mappe":["OPPHØR"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn født ","_key":"7d1517138d3a"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"441f2d8aab88"},{"_type":"span","marks":[],"text":" fordi avtalen om delt bosted for ","_key":"af7eab00dbf3"},{"_key":"894534a88dd4","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" ikke var gyldig. ","_key":"fae899fe32b1"}],"_type":"block","style":"normal","_key":"243c1446183d","markDefs":[]}],"borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"INGEN_UTBETALING","_createdAt":"2021-11-05T10:26:55Z"},{"hjemler":["4","5"],"_type":"begrunnelse","begrunnelsetype":"OPPHØR","_createdAt":"2021-09-24T11:21:10Z","mappe":["OPPHØR"],"_updatedAt":"2023-09-25T10:34:14Z","apiNavn":"opphorSokerOgBarnIkkeLengerFrivilligMedlem","rolle":["SOKER","BARN"],"tema":"NASJONAL","valgbarhet":"STANDARD","bokmaal":[{"_key":"ec25324ada6c","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"daad26702a09"},{"_key":"0bf01a65122a","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"marks":[],"text":" fordi ","_key":"e1f16282db1e","_type":"span"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"311f18e9ef29"},{"_type":"span","marks":[],"text":" ikke er frivillig medlem i folketrygden ","_key":"29e38a2b694a"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"5ccb973546e2"},{"text":". ","_key":"a52256205a23","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"behandlingstema":"NASJONAL","vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOSATT_I_RIKET"],"periodeType":"INGEN_UTBETALING","bosattIRiketTriggere":["MEDLEMSKAP"],"nynorsk":[{"_type":"block","style":"normal","_key":"53c696a7f42a","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"1ecec1fe454b"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"4a9f304e5708"},{"_type":"span","marks":[],"text":" fordi ","_key":"ec1ffa65d4aa"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"543fa1c0e006"},{"_type":"span","marks":[],"text":" ikkje er frivillig medlem i folketrygda ","_key":"e1978caececd"},{"_type":"valgReferanse","_key":"b5e02e1a8bcd","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"_type":"span","marks":[],"text":".","_key":"82d981a5061a"}]}],"navnISystem":"Ikke lenger frivillig medlem","visningsnavn":"16. Ikke frivillig medlem","_rev":"BtltdVb0HP4g4WJfnr5Y0o","hjemlerFolketrygdloven":["2-8"],"_id":"b3013592-98af-44da-819d-049cdf8f3c2c"},{"_createdAt":"2021-09-24T12:01:30Z","_id":"b4cc56b7-4a85-45b1-a393-6877408c264a","_type":"begrunnelse","nynorsk":[{"_type":"block","style":"normal","_key":"484a0889b63d","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn fødd ","_key":"32ab5ea2a024","_type":"span"},{"_key":"01aebe6454b6","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at den andre forelderen ikkje fyller vilkåra for å vere medlem i folketrygda under opphaldet i utlandet ","_key":"a373e6604829"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"5f92256f12d5"},{"_type":"span","marks":[],"text":". Bur begge foreldre saman under opphaldet i utlandet, må begge vere medlem i folketrygda for å få barnetrygd.","_key":"e61e6185296a"}]}],"_rev":"BtltdVb0HP4g4WJfnr5a5o","vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"OPPHØR","tema":"NASJONAL","bosattIRiketTriggere":["MEDLEMSKAP"],"navnISystem":"Vurdering annen forelder ikke medlem","apiNavn":"opphorVurderingAnnenForelderIkkeMedlem","valgbarhet":"STANDARD","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"686e0587d880"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"2e87dded77be"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at den andre forelderen ikke fyller vilkårene for å være medlem i folketrygden under oppholdet i utlandet ","_key":"be284ae8c916"},{"_type":"valgReferanse","_key":"5fd61701dc90","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"_type":"span","marks":[],"text":". Bor begge foreldrene sammen under oppholdet i utlandet, må dere begge være medlem i folketrygden for å få barnetrygd. ","_key":"83437f480b2b"}],"_type":"block","style":"normal","_key":"4c78650a7a86","markDefs":[]}],"hjemler":["4","5"],"hjemlerFolketrygdloven":["2-5","2-8"],"periodeType":"INGEN_UTBETALING","rolle":["SOKER"],"mappe":["OPPHØR"],"_updatedAt":"2023-09-25T10:34:43Z","visningsnavn":"22. Vurdering annen forelder ikke medlem","vilkaar":["BOSATT_I_RIKET"]},{"visningsnavn":"79. Tilleggstekst samboer under 12 måneder før gift","vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"INNVILGET","nynorsk":[{"_key":"89616809aaa5","markDefs":[],"children":[{"_key":"c3d3061ae146","_type":"span","marks":[],"text":""},{"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"},"_type":"valgfeltV2","_key":"afe77cc48238","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvida barnetrygd fram til du gifta deg.","_key":"33387c8701a40"}],"_type":"block","style":"normal"}],"_id":"b4ec114b-4420-4337-94d3-b613e767b7a6","navnISystem":"Tilleggstekst samboer under 12 måneder før gift","apiNavn":"innvilgetTillleggstekstSamboerUnder12MaanederForGift","_type":"begrunnelse","periodeType":"UTBETALING","tema":"FELLES","mappe":["INNVILGET"],"bokmaal":[{"children":[{"marks":[],"text":"","_key":"72e0631b4762","_type":"span"},{"_key":"1971a804d0df","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" utvidet barnetrygd fram til du giftet deg.","_key":"98324eca2e8a0"}],"_type":"block","style":"normal","_key":"4926c0ee2522","markDefs":[]}],"_rev":"h26rAhFEYSUtDGXJE0sTLq","hjemler":["9","11"],"behandlingstema":"NASJONAL","vedtakResultat":"INNVILGET_ELLER_ØKNING","_createdAt":"2022-05-13T13:08:25Z","valgbarhet":"TILLEGGSTEKST","_updatedAt":"2023-09-25T10:15:48Z"},{"visningsnavn":"28. Flyttet sammen med ektefelle","_id":"b5514efc-d95b-46af-b001-1d68ee5f31fe","mappe":["REDUKSJON"],"navnISystem":"Flyttet sammen med ektefelle","periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","_createdAt":"2021-10-22T12:34:33Z","valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"text":"Barnetrygden endres fordi du har flyttet sammen med ektefellen din i ","_key":"d3ec9f283ccc","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"73fbb94b5d09"},{"_type":"span","marks":[],"text":". Barnetrygden endres fra måneden etter at dere flyttet sammen.","_key":"c9b1d2888b20"}],"_type":"block","style":"normal","_key":"f36cdaab5080"}],"vilkaar":["UTVIDET_BARNETRYGD"],"vedtakResultat":"REDUKSJON","hjemler":["2","9","11"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","tema":"FELLES","nynorsk":[{"style":"normal","_key":"40c50a11654f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du har flytta saman med ektefellen din i ","_key":"7fc4e35df347"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"9c221e205090"},{"text":". Barnetrygda er endra frå månaden etter at de flytta saman.","_key":"67b1bb7d4b13","_type":"span","marks":[]}],"_type":"block"}],"_updatedAt":"2023-09-25T10:15:48Z","apiNavn":"reduksjonFlyttetSammenMedEktefelle"},{"tema":"NASJONAL","_createdAt":"2021-11-05T10:33:16Z","valgbarhet":"STANDARD","hjemler":["2"],"visningsnavn":"48. Avtale delt bosted ikke gyldig","_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","vedtakResultat":"REDUKSJON","nynorsk":[{"_type":"block","style":"normal","_key":"584328cfef6f","markDefs":[],"children":[{"text":"Barnetrygda er endra fordi avtalen om delt bustad for barn fødd ","_key":"14812b6f3886","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"f2c6a07c3dcb"},{"text":" ikkje var gyldig. ","_key":"8bba08f2b1ca","_type":"span","marks":[]}]}],"mappe":["REDUKSJON"],"ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"],"navnISystem":"Avtale delt bosted ikke gyldig","borMedSokerTriggere":["DELT_BOSTED"],"_updatedAt":"2023-09-25T10:15:48Z","bokmaal":[{"children":[{"_key":"2b0bf0e51812","_type":"span","marks":[],"text":"Barnetrygden endres fordi avtalen om delt bosted for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"7446a091a686"},{"_type":"span","marks":[],"text":" ikke var gyldig. ","_key":"ec843d7f4be9"}],"_type":"block","style":"normal","_key":"1f5f2efdb096","markDefs":[]}],"_id":"b5754d2f-a78e-4af5-92ba-9396008aa67b","apiNavn":"reduksjonAvtaleOmDeltBostedIkkeGyldig","vilkaar":["BOR_MED_SOKER"],"periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON"},{"borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"begrunnelsetype":"OPPHØR","_updatedAt":"2023-09-25T10:31:57Z","navnISystem":"Foreldrene bor sammen, endret mottaker\t","_rev":"h26rAhFEYSUtDGXJE0unkx","valgbarhet":"STANDARD","bokmaal":[{"_type":"block","style":"normal","_key":"92b40ef09ac4","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"740f9f0feff0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"5a52443e8e5a"},{"_key":"2fad1b57c313","_type":"span","marks":[],"text":" fordi den andre forelderen har søkt om barnetrygd for "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"3e817ed68c00","skalHaStorForbokstav":false},{"_key":"bd96cada4181","_type":"span","marks":[],"text":"."}]}],"apiNavn":"opphorEndretMottaker","tema":"FELLES","periodeType":"INGEN_UTBETALING","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"813d069c1e51"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"001f3da335d9"},{"marks":[],"text":" fordi den andre forelderen har søkt om barnetrygd for ","_key":"5df14361794b","_type":"span"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"1bcd26d0f883","skalHaStorForbokstav":false},{"text":".","_key":"a3b2a8e5ab9a","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"877f2fc8d873","markDefs":[]}],"visningsnavn":"12. Foreldrene bor sammen, endret mottaker","vilkaar":["BOR_MED_SOKER"],"_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","_createdAt":"2021-08-30T11:55:20Z","_id":"b6b472a5-2d6f-48af-b866-b531b5000989","mappe":["OPPHØR"],"hjemler":["2","12"],"behandlingstema":"NASJONAL"},{"mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:48Z","navnISystem":"Ikke avtale delt bosted","periodeType":"UTBETALING","tema":"FELLES","_createdAt":"2021-10-26T10:06:14Z","valgbarhet":"STANDARD","behandlingstema":"NASJONAL","vedtakResultat":"REDUKSJON","vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["DELT_BOSTED"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi du ikke har en avtale om delt bosted for barn født ","_key":"e76796625496"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"19198ae6bf68"},{"_type":"span","marks":[],"text":".","_key":"a7dd989040fb"}],"_type":"block","style":"normal","_key":"e203e530a69a"}],"apiNavn":"reduksjonIkkeAvtaleDeltBosted","hjemler":["2","11"],"begrunnelsetype":"REDUKSJON","_id":"b74f1102-a24e-4f37-931f-84fa23b33099","visningsnavn":"40. Ikke avtale delt bosted","_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","nynorsk":[{"_type":"block","style":"normal","_key":"b1c2fa1de040","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du ikkje har ein avtale om delt bustad for barn fødd ","_key":"bd38c4762c78"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"53e49e2ca978"},{"_type":"span","marks":[],"text":".","_key":"0d3f6a9ec54e"}]}]},{"apiNavn":"avslagIkkeOppholdstillatelseInstitusjon","fagsakType":"INSTITUSJON","behandlingstema":"NASJONAL_INSTITUSJON","_rev":"BtltdVb0HP4g4WJfnr5qQJ","begrunnelsetype":"AVSLAG","nynorsk":[{"_key":"4d7b6c236983","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"f33b89e688c90"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"bf0d1bbb6e4d"},{"_type":"span","marks":[],"text":" fordi barnet ikkje har opphaldsløyve i Noreg.","_key":"ee0b42749170"}],"_type":"block","style":"normal"}],"visningsnavn":"2. Ikke oppholdstillatelse institusjon","_type":"begrunnelse","periodeType":"INGEN_UTBETALING","rolle":["SOKER","BARN"],"_createdAt":"2022-11-04T09:00:30Z","_updatedAt":"2023-09-25T10:38:31Z","vedtakResultat":"IKKE_INNVILGET","valgbarhet":"SAKSPESIFIKK","_id":"b7d6e4b9-dfe8-4857-b055-4723ed935277","bokmaal":[{"style":"normal","_key":"1d94ba741f54","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"ec8864cc5e150"},{"_key":"3bed20b57c2a","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi barnet ikke har oppholdstillatelse i Norge.","_key":"a1232a915573"}],"_type":"block"}],"navnISystem":"Ikke oppholdstillatelse institusjon","hjemler":["2","4"],"vilkaar":["LOVLIG_OPPHOLD"],"tema":"NASJONAL","mappe":["INSTITUSJON","AVSLAG"]},{"valgbarhet":"STANDARD","mappe":["INNVILGET"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"","_key":"322c07c731d1"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"fd4f65af742a","skalHaStorForbokstav":false},{"_key":"d63b6bc00410","_type":"span","marks":[],"text":" utvidet barnetrygd selv om du fortsatt er gift fordi du bor alene med "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"828549f15e71","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Ektefellen din er varetektsfengslet i seks måneder eller mer fra ","_key":"1a25bd76fd62"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"287b44a0c227"},{"_type":"span","marks":[],"text":". ","_key":"b4c0cbbdd99f"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"8d69809060d9","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at varetektsfengslingen har vart i seks måneder.","_key":"b49ad2ee8716"}],"_type":"block","style":"normal","_key":"7721cd36771c","markDefs":[]}],"_rev":"h26rAhFEYSUtDGXJE0sTLq","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","_updatedAt":"2023-09-25T10:15:48Z","apiNavn":"innvilgetVaretektsfengselGift","tema":"FELLES","_createdAt":"2021-10-25T07:10:00Z","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"0be0b3d082ba"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"b8c58b00234d"},{"_type":"span","marks":[],"text":" utvida barnetrygd sjølv om du fortsatt er gift fordi du bur aleine med ","_key":"53a8aae5c1d2"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"a6780811d903","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Ektefellen din er varetektsfengsla i seks månader eller meir frå ","_key":"f10a23020e5f"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"f04327c0a230"},{"_type":"span","marks":[],"text":". ","_key":"313e783e98be"},{"_type":"valgfeltV2","_key":"c7a4a0e1bd8e","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"}},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at varetektsfengslinga har vart i seks månader.","_key":"c69823d84aa0"}],"_type":"block","style":"normal","_key":"655c63e58850"}],"navnISystem":"Varetektsfengsel gift","visningsnavn":"44. Varetektsfengsel gift","behandlingstema":"NASJONAL","begrunnelsetype":"INNVILGET","_id":"b8648bb4-c557-4364-8eec-879c8ae3369f","hjemler":["2","9","11"],"_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"]},{"hjemler":["2","4","11"],"_rev":"FuD004taptHFqBZyEy6JLl","periodeType":"FORTSATT_INNVILGET","begrunnelsetype":"FORTSATT_INNVILGET","nynorsk":[{"style":"normal","_key":"19c3b1ef4513","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du fortsatt er busett i Noreg.","_key":"40ef7f36cbf9"}],"_type":"block"}],"_createdAt":"2021-08-30T12:51:49Z","navnISystem":"Søker oppholder seg i Norge","rolle":["SOKER"],"tema":"NASJONAL","valgbarhet":"STANDARD","mappe":["FORTSATT_INNVILGET"],"visningsnavn":"1A. Søker oppholder seg i Norge","vilkaar":["BOSATT_I_RIKET"],"_updatedAt":"2023-09-25T10:21:13Z","apiNavn":"fortsattInnvilgetSokerBosattIRiket","_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","_id":"b92aeac1-eb9d-4c0f-887e-d2beec81baba","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du fortsatt er bosatt i Norge.","_key":"d9d3dde516e6"}],"_type":"block","style":"normal","_key":"716e49a92815","markDefs":[]}]},{"begrunnelsetype":"OPPHØR","navnISystem":"Flyttet fra institusjon","apiNavn":"opphorFlyttetFraInstitusjon","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","valgbarhet":"SAKSPESIFIKK","mappe":["INSTITUSJON","OPPHØR"],"_updatedAt":"2023-09-25T10:38:00Z","bokmaal":[{"_key":"a1b206a13af7","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"c5589577ce680"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"88b467a2d877"},{"marks":[],"text":" fordi barnet flyttet fra institusjonen ","_key":"bd5bde8f2378","_type":"span"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"aae8e55ddd64"},{"text":".","_key":"c23583dc78b2","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"behandlingstema":"NASJONAL_INSTITUSJON","_rev":"FuD004taptHFqBZyEy8s1Q","tema":"NASJONAL","nynorsk":[{"style":"normal","_key":"e1fab64c8285","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"58e84e311dd80"},{"_type":"flettefelt","_key":"fdc10cba5edd","flettefelt":"barnasFodselsdatoer"},{"marks":[],"text":" fordi barnet flytta frå institusjonen ","_key":"c2a56f1bd23f","_type":"span"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"7ad285e31964"},{"text":".","_key":"1e050cd4bfb5","_type":"span","marks":[]}],"_type":"block"}],"hjemler":["2","11"],"_createdAt":"2022-11-04T08:18:04Z","_id":"b944c1ea-997b-4bfd-82a2-8a009476ca82","fagsakType":"INSTITUSJON","visningsnavn":"1. Flyttet fra institusjon","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"]},{"_createdAt":"2021-08-20T14:50:35Z","hjemler":["2","11"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","vilkaar":["UNDER_18_ÅR"],"tema":"NASJONAL","valgbarhet":"STANDARD","_id":"barn18Aar","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygden endres fordi barn født ","_key":"barn18Aar2","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"4ac3224b95f4"},{"marks":[],"text":" er 18 år.","_key":"2e0e167c1cdb","_type":"span"}],"_type":"block","style":"normal","_key":"barn18Aar1"}],"apiNavn":"reduksjonUnder18Aar","periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","visningsnavn":"10. Barn 18 år","vedtakResultat":"REDUKSJON","nynorsk":[{"_type":"block","style":"normal","_key":"barn18Aar1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi barn fødd ","_key":"barn18Aar2"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"cc242d6e5f34"},{"_type":"span","marks":[],"text":" er 18 år.","_key":"bf1446c39c3d"}]}],"_updatedAt":"2023-09-25T10:15:48Z","navnISystem":"Barn 18 år","_type":"begrunnelse","mappe":["REDUKSJON"]},{"tema":"FELLES","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:48Z","navnISystem":"Barn 6 år","apiNavn":"reduksjonUnder6Aar","hjemler":["2","10"],"periodeType":"UTBETALING","_rev":"h26rAhFEYSUtDGXJE0sTLq","begrunnelsetype":"REDUKSJON","valgbarhet":"AUTOMATISK","vedtakResultat":"REDUKSJON","_id":"barn6Aar","ovrigeTriggere":["BARN_MED_6_ÅRS_DAG","SATSENDRING"],"_createdAt":"2021-08-20T14:50:35Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi barn født ","_key":"barn6Aar2"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"b96048a79252"},{"_type":"span","marks":[],"text":" er 6 år.","_key":"c9c60ad6f969"}],"_type":"block","style":"normal","_key":"barn6Aar1"}],"visningsnavn":"11. Barn 6 år","behandlingstema":"NASJONAL","_type":"begrunnelse","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi barn fødd ","_key":"barn6Aar2"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"457fabfbddb0"},{"_type":"span","marks":[],"text":" er 6 år.","_key":"9f0796ab8dc8"}],"_type":"block","style":"normal","_key":"barn6Aar1"}]},{"nynorsk":[{"_key":"barnDod1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra frå månaden etter at barn fødd ","_key":"barnDod2"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"ecd7e8166505"},{"_type":"span","marks":[],"text":" døydde.","_key":"be17e9fdbb3f"}],"_type":"block","style":"normal"}],"apiNavn":"reduksjonBarnDod","hjemler":["2","11"],"vedtakResultat":"REDUKSJON","tema":"FELLES","_createdAt":"2021-08-20T14:50:35Z","valgbarhet":"AUTOMATISK","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden er endret fra måneden etter at barn født ","_key":"barnDod2"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"e35369aa5ac0"},{"text":" døde.","_key":"1d86844a4fca","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"barnDod1"}],"behandlingstema":"NASJONAL","_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","_id":"barnDod","ovrigeTriggere":["BARN_DØD"],"navnISystem":"Barn død","periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","visningsnavn":"4. Barn død","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:48Z"},{"behandlingstema":"NASJONAL","_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","periodeType":"UTBETALING","_updatedAt":"2023-09-25T10:15:48Z","visningsnavn":"3. Barn har flyttet fra Norge","tema":"NASJONAL","_createdAt":"2021-08-20T14:50:35Z","apiNavn":"reduksjonBosattIRiket","rolle":["BARN"],"begrunnelsetype":"REDUKSJON","_id":"barnHarFlyttetFraNorge","mappe":["REDUKSJON"],"bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygden endres fordi barn født ","_key":"barnHarFlyttetFraNorge2","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"17b5c89dc568"},{"_type":"span","marks":[],"text":" har flyttet fra Norge i ","_key":"20cb3ab1dc07"},{"_type":"flettefelt","_key":"607a62686927","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_type":"span","marks":[],"text":".","_key":"29463460b40b"}],"_type":"block","style":"normal","_key":"barnHarFlyttetFraNorge1"}],"navnISystem":"Barn har flyttet fra Norge","hjemler":["2","4","11"],"vedtakResultat":"REDUKSJON","vilkaar":["BOSATT_I_RIKET"],"nynorsk":[{"children":[{"text":"Barnetrygda er endra fordi barn fødd ","_key":"barnHarFlyttetFraNorge2","_type":"span","marks":[]},{"_type":"flettefelt","_key":"c69018afc144","flettefelt":"barnasFodselsdatoer"},{"_key":"7dec1ada35be","_type":"span","marks":[],"text":" har flytta frå Noreg i "},{"_key":"ff37621fed7c","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"d7a8fc0e0e89"}],"_type":"block","style":"normal","_key":"barnHarFlyttetFraNorge1","markDefs":[]}],"valgbarhet":"STANDARD"},{"nynorsk":[{"_key":"barnHarFlyttetFraSokerFlyttingMellomForeldreAndreOmsorgspersoner1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi barn fødd ","_key":"barnHarFlyttetFraSokerFlyttingMellomForeldreAndreOmsorgspersoner2"},{"_type":"flettefelt","_key":"95898ba3ebc9","flettefelt":"barnasFodselsdatoer"},{"text":" ikkje bur fast hos deg frå ","_key":"3accdbb49872","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"9fc1a57a9010"},{"_type":"span","marks":[],"text":".","_key":"1a146f45a0c7"}],"_type":"block","style":"normal"}],"valgbarhet":"STANDARD","navnISystem":"Barn har flyttet fra søker (flytting mellom foreldre, andre omsorgspersoner)","apiNavn":"reduksjonFlyttetBarn","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"begrunnelsetype":"REDUKSJON","_createdAt":"2021-08-20T14:50:35Z","mappe":["REDUKSJON"],"hjemler":["2","11"],"visningsnavn":"1. Barn har flyttet fra søker (flytting mellom foreldre, andre omsorgspersoner)","_rev":"h26rAhFEYSUtDGXJE0sTLq","periodeType":"UTBETALING","tema":"NASJONAL","vedtakResultat":"REDUKSJON","_updatedAt":"2023-09-25T10:15:48Z","behandlingstema":"NASJONAL","resultat":"REDUKSJON","_id":"barnHarFlyttetFraSokerFlyttingMellomForeldreAndreOmsorgspersoner","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygden endres fordi barn født ","_key":"barnHarFlyttetFraSokerFlyttingMellomForeldreAndreOmsorgspersoner2","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"cefea659e31d"},{"text":" ikke bor fast hos deg fra ","_key":"7069cb37f27d","_type":"span","marks":[]},{"_type":"flettefelt","_key":"f32b465dcb14","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_key":"4d1b0a9113c6","_type":"span","marks":[],"text":". "}],"_type":"block","style":"normal","_key":"barnHarFlyttetFraSokerFlyttingMellomForeldreAndreOmsorgspersoner1"}]},{"apiNavn":"innvilgetBorHosSoker","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd ","_key":"barnHarFlyttetTilSokerFlyttingMellomForeldreAndreOmsorgspersoner2","_type":"span"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"53c3df5b2c50"},{"marks":[],"text":" fordi ","_key":"88efc97d2392","_type":"span"},{"_key":"2d1aeee189b6","_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse"},{"_type":"span","marks":[],"text":" bor fast hos deg fra ","_key":"a7055e7a0916"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"677c1cf48845"},{"_type":"span","marks":[],"text":".","_key":"1e701ce82255"}],"_type":"block","style":"normal","_key":"barnHarFlyttetTilSokerFlyttingMellomForeldreAndreOmsorgspersoner1"}],"navnISystem":"Barn har flyttet til søker (flytting mellom foreldre, andre omsorgspersoner)","tema":"NASJONAL","_id":"barnHarFlyttetTilSokerFlyttingMellomForeldreAndreOmsorgspersoner","nynorsk":[{"_type":"block","style":"normal","_key":"barnHarFlyttetTilSokerFlyttingMellomForeldreAndreOmsorgspersoner1","markDefs":[],"children":[{"_key":"barnHarFlyttetTilSokerFlyttingMellomForeldreAndreOmsorgspersoner2","_type":"span","marks":[],"text":"Du får barnetrygd "},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"f630327a6199"},{"text":" fordi ","_key":"fff25f542a46","_type":"span","marks":[]},{"_type":"valgReferanse","_key":"f616b6a912d7","_ref":"df8dc282-2637-4047-a656-8527205dc364"},{"_type":"span","marks":[],"text":" bur fast hos deg frå ","_key":"84114509c0ad"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"8ddb025070a1"},{"_type":"span","marks":[],"text":". ","_key":"6c97a9c4ed3e"}]}],"valgbarhet":"STANDARD","mappe":["INNVILGET"],"visningsnavn":"4. Barn har flyttet til søker (flytting mellom foreldre, andre omsorgspersoner)","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","vilkaar":["BOR_MED_SOKER"],"_createdAt":"2021-07-27T08:07:24Z","_updatedAt":"2023-09-25T10:15:48Z","hjemler":["2","4","11"],"behandlingstema":"NASJONAL","_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse"},{"periodeType":"UTBETALING","tema":"NASJONAL","_id":"barnHarIkkeOppholdstillatelse","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:48Z","visningsnavn":"6. Barn har ikke oppholdstillatelse","_rev":"h26rAhFEYSUtDGXJE0sTLq","vilkaar":["LOVLIG_OPPHOLD"],"bokmaal":[{"style":"normal","_key":"barnHarIkkeOppholdstillatelse1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi barn født ","_key":"barnHarIkkeOppholdstillatelse2"},{"_key":"519039a056aa","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" ikke lenger har oppholdstillatelse i Norge fra ","_key":"a87112f7d79c"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"a2cd2dba9c53"},{"_type":"span","marks":[],"text":".","_key":"c2cdd774974d"}],"_type":"block"}],"apiNavn":"reduksjonLovligOppholdOppholdstillatelseBarn","begrunnelsetype":"REDUKSJON","_createdAt":"2021-08-20T14:50:35Z","hjemler":["4","11"],"vedtakResultat":"REDUKSJON","nynorsk":[{"children":[{"text":"Barnetrygda er endra fordi barn fødd ","_key":"barnHarIkkeOppholdstillatelse2","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"3186f6c779e1"},{"marks":[],"text":" ikkje lenger har opphaldsløyve i Noreg frå ","_key":"8be42d325104","_type":"span"},{"_key":"8915ca3b0ebc","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"marks":[],"text":".","_key":"f61a9b68de96","_type":"span"}],"_type":"block","style":"normal","_key":"barnHarIkkeOppholdstillatelse1","markDefs":[]}],"valgbarhet":"STANDARD","navnISystem":"Barn har ikke oppholdstillatelse","_type":"begrunnelse","rolle":["BARN"]},{"mappe":["OPPHØR"],"_updatedAt":"2023-09-25T10:36:09Z","navnISystem":"Vurdering var ikke medlem","vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"OPPHØR","_createdAt":"2021-11-05T11:34:57Z","_id":"bb27396a-8283-4102-b585-e0097177ffb9","ovrigeTriggere":["GJELDER_FØRSTE_PERIODE"],"rolle":["SOKER","BARN"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"508bd9166e9f"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"3f1435386f53"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at ","_key":"1ce2a1eac925"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"9f46ee97ea1e","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikkje var medlem i folketrygda under opphaldet i utlandet.","_key":"c66d2c816987"}],"_type":"block","style":"normal","_key":"39e3dbb85ca2"}],"bosattIRiketTriggere":["MEDLEMSKAP"],"bokmaal":[{"style":"normal","_key":"7cad16a03306","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"82ecaec14764"},{"_key":"a7e3428a49b2","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"text":" fordi vi har kommet fram til at ","_key":"eee5b715ef06","_type":"span","marks":[]},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"e1eee2028794"},{"text":" ikke var medlem i folketrygden under oppholdet i utlandet.","_key":"b78df50787a3","_type":"span","marks":[]}],"_type":"block"},{"children":[{"_key":"2adc80a7f0c1","_type":"span","marks":[],"text":""}],"_type":"block","style":"normal","_key":"7cbc7c40eb76","markDefs":[]}],"apiNavn":"opphorVurderingVarIkkeMedlem","hjemler":["4","5"],"visningsnavn":"40. Vurdering var ikke medlem","hjemlerFolketrygdloven":["2-5","2-8"],"periodeType":"INGEN_UTBETALING","tema":"NASJONAL","_rev":"h26rAhFEYSUtDGXJE0v1ON","_type":"begrunnelse","valgbarhet":"STANDARD"},{"hjemler":["2","4","11"],"_type":"begrunnelse","vilkaar":["LOVLIG_OPPHOLD"],"apiNavn":"fortsattInnvilgetSokerLovligOppholdOppholdstillatelse","periodeType":"FORTSATT_INNVILGET","valgbarhet":"STANDARD","_id":"bb5242fc-b715-4e50-9d64-330a4252c2e9","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du fortsatt har oppholdstillatelse.","_key":"87d76017c038"}],"_type":"block","style":"normal","_key":"40e8f56ceb8a","markDefs":[]}],"navnISystem":"Tredjelandsborger søker fortsatt lovlig opphold i Norge","tema":"NASJONAL","mappe":["FORTSATT_INNVILGET"],"visningsnavn":"2A. Tredjelandsborger søker fortsatt lovlig opphold i Norge","vedtakResultat":"INGEN_ENDRING","rolle":["SOKER"],"begrunnelsetype":"FORTSATT_INNVILGET","nynorsk":[{"style":"normal","_key":"c21cd0ac543b","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd fordi du fortsatt har opphaldsløyve.","_key":"f9a4deaec26d","_type":"span"}],"_type":"block"}],"_createdAt":"2021-08-30T12:57:01Z","_updatedAt":"2023-09-25T10:21:36Z","_rev":"BtltdVb0HP4g4WJfnr4qOJ"},{"_type":"begrunnelse","periodeType":"INGEN_UTBETALING","rolle":["SOKER"],"bosattIRiketTriggere":["MEDLEMSKAP"],"_createdAt":"2021-09-24T12:05:35Z","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:34:51Z","_rev":"BtltdVb0HP4g4WJfnr5cLJ","vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"OPPHØR","vedtakResultat":"IKKE_INNVILGET","tema":"NASJONAL","nynorsk":[{"_key":"eb74f0e96522","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"fa696e071cce"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"1a8a4baef969"},{"_key":"e005c95c52b3","_type":"span","marks":[],"text":" fordi vi har kome fram til at "},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"bdf2efb54ece"},{"_type":"span","marks":[],"text":" har hatt fleire korte opphald i utlandet dei to siste åra. Opphalda i utlandet utgjer til saman meir enn 6 månader for kvart år. ","_key":"331583ea3c2b"},{"valgReferanse":{"_type":"reference","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},"_type":"valgfeltV2","_key":"984625fa47aa","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" er derfor ikkje rekna som busett i Noreg frå ","_key":"5dbe70f4bf63"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"2c8c09125094"},{"_type":"span","marks":[],"text":". Vi har også kome fram til at ","_key":"64403b1aee1a"},{"_type":"valgfeltV2","_key":"fdd4acb0ddd2","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"}},{"_type":"span","marks":[],"text":" ikkje fyller vilkåra for å vere medlem i folketrygda. ","_key":"bf0ed837e38b"}],"_type":"block","style":"normal"}],"mappe":["OPPHØR"],"apiNavn":"opphorVurderingFlereKorteOppholdIUtlandetSisteToAr","visningsnavn":"23. Vurdering flere korte opphold i utlandet siste to år","hjemlerFolketrygdloven":["2-5","2-8"],"bokmaal":[{"style":"normal","_key":"0a0d1a3f5801","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"cf08fa51e4c5"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"162d7d2283ce"},{"text":" fordi vi har kommet fram til at ","_key":"b7c049511df9","_type":"span","marks":[]},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"a172c472db3a","skalHaStorForbokstav":false},{"marks":[],"text":" har hatt flere korte opphold i utlandet de to siste årene. Oppholdene i utlandet utgjør til sammen mer enn 6 måneder for hvert år. ","_key":"76640710ed99","_type":"span"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"f9ce5d3856f9","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" regnes derfor derfor ikke som bosatt i Norge ","_key":"aafdd2d27a3b"},{"_type":"flettefelt","_key":"07ebdbb98848","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_type":"span","marks":[],"text":". Vi har også kommet fram til at ","_key":"412fc33d79ae"},{"_type":"valgfeltV2","_key":"c6ae0022c411","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"}},{"_type":"span","marks":[],"text":" ikke fyller vilkårene for å være medlem i folketrygden.","_key":"a9c7a88f3298"}],"_type":"block"}],"navnISystem":"Vurdering flere korte opphold i utlandet siste to år","hjemler":["4","5"],"_id":"bde31c55-2e6b-435a-9559-ebb8fa64ef2a"},{"navnISystem":"Søker gifter seg","vedtakResultat":"REDUKSJON","_createdAt":"2021-10-22T10:30:05Z","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"apiNavn":"reduksjonSokerGifterSeg","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","_id":"be15a660-50c5-4811-b9f8-f6296d1d8f44","hjemler":["2","9","11"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","_updatedAt":"2023-09-25T10:15:48Z","nynorsk":[{"_type":"block","style":"normal","_key":"2e139d1c5f87","markDefs":[],"children":[{"marks":[],"text":"Barnetrygda er endra fordi du gifta deg ","_key":"1b4009792e38","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"dc96e974db9f"},{"_type":"span","marks":[],"text":". Barnetrygda er endra frå månaden etter at du gifta deg.","_key":"81bb9070e1a2"}]}],"bokmaal":[{"_key":"8405460a11c5","markDefs":[],"children":[{"text":"Barnetrygden endres fordi du giftet deg ","_key":"b12770515544","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"92d797a61951"},{"_type":"span","marks":[],"text":". Barnetrygden endres fra måneden etter at du giftet deg.","_key":"7fe1debe136c"}],"_type":"block","style":"normal"}],"visningsnavn":"21. Søker gifter seg","_type":"begrunnelse","begrunnelsetype":"REDUKSJON","tema":"FELLES"},{"apiNavn":"innvilgetSamboerUtenFellesBarnOgVurderingEgenHusholdning","hjemler":["2","9","11"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","_createdAt":"2021-10-25T08:26:30Z","_id":"be3d69e6-cc11-4a55-95cb-a71675c6b196","_updatedAt":"2023-09-25T10:15:48Z","behandlingstema":"NASJONAL","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","mappe":["INNVILGET"],"visningsnavn":"53. Samboer uten felles barn og vurdering egen husholdning","periodeType":"UTBETALING","tema":"FELLES","navnISystem":"Samboer uten felles barn og vurdering egen husholdning","_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"markDefs":[],"children":[{"text":"","_key":"733edb696bdd","_type":"span","marks":[]},{"_type":"valgfeltV2","_key":"5c8889ef55e2","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"}},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi du bur aleine med ","_key":"88dfa9830fdd"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"4c554286cb2e"},{"_type":"span","marks":[],"text":". Vi har kome fram til at du og den tidligare sambuaren din flytta frå kvarandre ","_key":"d037ff07305c"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"fde1e9536f79"},{"_type":"span","marks":[],"text":". ","_key":"9c2b6c85765e"},{"_type":"valgfeltV2","_key":"53408abc61fc","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"}},{"text":" utvida barnetrygd frå månaden etter at de flytta frå kvarandre.","_key":"6c8bb716f28e","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"02ef43815804"}],"bokmaal":[{"style":"normal","_key":"85de486753fd","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"d68936e6f666"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"7fdc0b53c43a","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd fordi du bor alene med ","_key":"257c9f25ecde"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"6b926d8c905b","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Vi har kommet fram til at du og den tidligere samboeren din flyttet fra hverandre ","_key":"efb99b5bd3c8"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"f892438c5ff3"},{"_type":"span","marks":[],"text":". ","_key":"4f354d65480c"},{"_type":"valgfeltV2","_key":"2ceee24ffa6a","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"}},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at dere flyttet fra hverandre.","_key":"d2660a421441"}],"_type":"block"}]},{"visningsnavn":"22. Etterbetaling tre år tilbake i tid SED - utbetaling","_updatedAt":"2023-09-25T10:15:48Z","periodeType":"UTBETALING","endringsaarsaker":["ETTERBETALING_3ÅR"],"mappe":["ENDRET_UTBETALINGSPERIODE"],"bokmaal":[{"_key":"6d031a62cefb","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Det er søkt om barnetrygd for barn født ","_key":"9db33ec3595b0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"5be8e2f0cae8"},{"_key":"625ca8220487","_type":"span","marks":[],"text":" i et annet EØS-land "},{"_key":"00ad492ef6ac","flettefelt":"soknadstidspunkt","_type":"flettefelt"},{"marks":[],"text":". Barnetrygd kan ikke utbetales for mer enn tre år tilbake i tid fra tidspunktet det var søkt.","_key":"fdd2ab64fe77","_type":"span"}],"_type":"block","style":"normal"}],"hjemler":["11"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","vedtakResultat":"IKKE_INNVILGET","tema":"NASJONAL","_createdAt":"2023-09-08T08:01:06Z","_id":"begrunnelse-022fe483-6cb9-40c7-9954-eab67ab6c363","navnISystem":"Etterbetaling tre år tilbake i tid SED","apiNavn":"endretUtbetalingEtterbetalingTreAarTilbakeITidSedUtbetaling","_type":"begrunnelse","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Det er søkt om barnetrygd for barn fødd ","_key":"5ffb001481650"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"b662c8ad6b15"},{"_type":"span","marks":[],"text":" i eit anna EØS-land ","_key":"8cacdd4877d1"},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"cb261252846b"},{"text":". Barnetrygd kan ikkje utbetalast for meir enn tre år tilbake i tid frå tidspunktet det var søkt.","_key":"2bcde50b878f","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"51dc7d931df3","markDefs":[]}],"valgbarhet":"STANDARD"},{"_updatedAt":"2023-09-25T10:15:50Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du søker om barnetrygd tilbake i tid for barn født ","_key":"52fa7468ae5b0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"0b92b628f743"},{"marks":[],"text":". Vi fikk søknaden din ","_key":"5e27f75e4e58","_type":"span"},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"9e2113f1ee7b"},{"_type":"span","marks":[],"text":". Barnetrygd kan ikke utbetales for mer enn tre år tilbake i tid fra vi fikk søknaden din.","_key":"1ded0deb49e7"}],"_type":"block","style":"normal","_key":"c19b56c6435f"}],"visningsnavn":"20. Etterbetaling tre år tilbake i tid - utbetaling","_rev":"h26rAhFEYSUtDGXJE0sV8b","vedtakResultat":"IKKE_INNVILGET","valgbarhet":"STANDARD","_id":"begrunnelse-0cfb5990-4d57-47ab-8d87-5dd07e0b06e2","mappe":["ENDRET_UTBETALINGSPERIODE"],"navnISystem":"Etterbetaling tre år tilbake i tid","periodeType":"UTBETALING","_type":"begrunnelse","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","tema":"NASJONAL","nynorsk":[{"_type":"block","style":"normal","_key":"2f4f6a546850","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du søker om barnetrygd tilbake i tid for barn fødd ","_key":"b08189b85d240"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"cfb214814c53"},{"_key":"d3d8227cdace","_type":"span","marks":[],"text":". Vi fekk søknaden din "},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"a2ac5a3f1559"},{"_type":"span","marks":[],"text":". Barnetrygd kan ikkje utbetalast for meir enn tre år tilbake i tid frå vi fekk søknaden din.","_key":"623e59ed86ac"}]}],"endringsaarsaker":["ETTERBETALING_3ÅR"],"_createdAt":"2023-09-08T07:55:20Z","apiNavn":"endretUtbetalingTreAarTilbakeITidUtbetaling","hjemler":["11"]},{"navnISystem":"Fast bosted avtale","_rev":"h26rAhFEYSUtDGXJE0sV8b","vedtakResultat":"REDUKSJON","nynorsk":[{"style":"normal","_key":"68b143268476","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Vi har fått ein avtale som seier at barn fødd ","_key":"e0e0acd69199"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"ac17958845bb"},{"_type":"span","marks":[],"text":" skal bu fast hos den andre forelderen frå ","_key":"db5f5b5945fa"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"bf6138d0dbe4"},{"_type":"span","marks":[],"text":".","_key":"514fb7fcf4d0"}],"_type":"block"}],"_id":"begrunnelse-1c4e8e08-7266-44fc-b093-6dae86288f37","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"periodeType":"UTBETALING","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"bokmaal":[{"_key":"89150f897b70","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Vi har fått en avtale som sier at barn født ","_key":"d95e834c7f69"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"32a417ac6efe"},{"_key":"e094ab51a3cd","_type":"span","marks":[],"text":" skal bo fast hos den andre forelderen fra "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"2879a999c43c"},{"_type":"span","marks":[],"text":".","_key":"c4b249a89e06"}],"_type":"block","style":"normal"}],"hjemler":["2","4","11"],"behandlingstema":"NASJONAL","begrunnelsetype":"REDUKSJON","_updatedAt":"2023-09-25T10:15:50Z","apiNavn":"reduksjonFastBostedAvtale","visningsnavn":"72. Fast bosted avtale","resultat":"REDUKSJON","borMedSokerTriggere":["DELT_BOSTED_SKAL_IKKE_DELES"],"tema":"NASJONAL","_createdAt":"2022-12-06T12:54:46Z"},{"begrunnelsetype":"REDUKSJON","nynorsk":[{"style":"normal","_key":"c4ac0358bf47","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi barn fødd ","_key":"52eb2f67100c0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"6c00035bb32c"},{"_key":"aa343f59c076","_type":"span","marks":[],"text":" bur i institusjon frå "},{"_key":"06fe05f618ff","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"eaa32127c06e"}],"_type":"block"}],"mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:50Z","hjemler":["2","11"],"visningsnavn":"74. Barn bor i institusjon","tema":"NASJONAL","valgbarhet":"STANDARD","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi barn født ","_key":"2eb77f6809940"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"70d518bfdd22"},{"_type":"span","marks":[],"text":" bor i institusjon fra ","_key":"a251ad12310a"},{"_key":"cbf269057c47","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"4678099a36dd"}],"_type":"block","style":"normal","_key":"7a3ed1d260e7","markDefs":[]}],"_rev":"h26rAhFEYSUtDGXJE0sV8b","borMedSokerTriggere":["DELT_BOSTED"],"_id":"begrunnelse-25c9b4b6-4884-4d95-aac7-d6f0256c2260","_createdAt":"2023-06-16T09:47:34Z","navnISystem":"Barn bor i institusjon","apiNavn":"reduksjonBarnBorIInstitusjon","_type":"begrunnelse","vedtakResultat":"REDUKSJON","vilkaar":["BOR_MED_SOKER"],"periodeType":"UTBETALING"},{"periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:50Z","visningsnavn":"73. Begge foreldre fått barnetrygd","_rev":"h26rAhFEYSUtDGXJE0sV8b","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"_createdAt":"2023-03-24T14:58:54Z","bokmaal":[{"_type":"block","style":"normal","_key":"168a631ee30c","markDefs":[],"children":[{"_key":"2a3ed8c38ebe0","_type":"span","marks":[],"text":"Barnetrygden endres fordi den andre forelderen har fått barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"26fcbcf6130c"},{"marks":[],"text":" i samme tidsrom.","_key":"fe58aab06fd3","_type":"span"}]}],"navnISystem":"Begge foreldre fått barnetrygd","apiNavn":"reduksjonBeggeForeldreFaattBarnetrygd","borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"tema":"NASJONAL","valgbarhet":"STANDARD","hjemler":["2","12"],"behandlingstema":"NASJONAL","vedtakResultat":"REDUKSJON","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi den andre forelderen har fått barnetrygd for barn fødd ","_key":"cb0814b1bd900"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"80a16bf9ed4a"},{"_type":"span","marks":[],"text":" i same tidsrom.","_key":"c6f6bbc77b7f"}],"_type":"block","style":"normal","_key":"652e13471251"}],"_id":"begrunnelse-2867a237-f203-44b7-9d3a-3f75a17a0fd3"},{"_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","_rev":"h26rAhFEYSUtDGXJE0sV8b","valgbarhet":"STANDARD","navnISystem":"Etterbetaling tre år tilbake i tid kun utvidet del","apiNavn":"endretUtbetalingEtterbetalingTreAarTilbakeITidKunUtvidetDelUtbetaling","hjemler":["11"],"_id":"begrunnelse-2909384c-4db9-42cb-86fd-2c005a03e125","mappe":["ENDRET_UTBETALINGSPERIODE"],"visningsnavn":"21. Etterbetaling tre år tilbake i tid kun utvidet del - utbetaling","periodeType":"UTBETALING","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du søker om utvida barnetrygd tilbake i tid. Vi fekk søknaden din ","_key":"1295ead370930"},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"a75387fdd4dd"},{"_type":"span","marks":[],"text":". Barnetrygd kan ikkje utbetalast for meir enn tre år tilbake i tid frå vi fekk søknaden din.","_key":"c9c6a068448d"}],"_type":"block","style":"normal","_key":"18ad57c02b83","markDefs":[]}],"_createdAt":"2023-09-08T07:58:21Z","_updatedAt":"2023-09-25T10:15:50Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du søker om utvidet barnetrygd tilbake i tid. Vi fikk søknaden din ","_key":"8ab88bddc0100"},{"_type":"flettefelt","_key":"d925b57b4451","flettefelt":"soknadstidspunkt"},{"_type":"span","marks":[],"text":". Barnetrygd kan ikke utbetales for mer enn tre år tilbake i tid fra vi fikk søknaden din.","_key":"bebc78dd0759"}],"_type":"block","style":"normal","_key":"865cd6b0def6","markDefs":[]}],"begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","tema":"NASJONAL","endringsaarsaker":["ETTERBETALING_3ÅR"]},{"navnISystem":"Etterbetaling tre år tilbake i tid SED","apiNavn":"endretUtbetalingEtterbetalingTreAarTilbakeITidSED","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","_id":"begrunnelse-37ea194e-93ce-4e3a-9941-d0655a784fc3","_updatedAt":"2023-09-25T10:24:30Z","visningsnavn":"19. Etterbetaling tre år tilbake i tid SED - Ingen utbetaling","_type":"begrunnelse","endringsaarsaker":["ETTERBETALING_3ÅR"],"valgbarhet":"STANDARD","mappe":["ENDRET_UTBETALINGSPERIODE"],"bokmaal":[{"_key":"fa9d3093bf4b","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"5fb5f00f45aa"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"cc9f39ed5aa1"},{"_type":"span","marks":[],"text":". Det er søkt om barnetrygd i et annet EØS-land ","_key":"1ed0c651a1d3"},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"f00e3b09acff"},{"_type":"span","marks":[],"text":". Barnetrygd kan ikke utbetales for mer enn tre år tilbake i tid fra tidspunktet det var søkt.","_key":"889eccf1cf96"}],"_type":"block","style":"normal"}],"tema":"FELLES","hjemler":["11"],"_rev":"BtltdVb0HP4g4WJfnr52Do","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","nynorsk":[{"_type":"block","style":"normal","_key":"ffc71ab9e989","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"c721dad3efd2"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"62867da69a67"},{"_type":"span","marks":[],"text":". Det er søkt om barnetrygd i eit annea EØS-land ","_key":"69c8fa32937f"},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"98860860cda9"},{"_type":"span","marks":[],"text":". Barnetrygd kan ikkje utbetalast for meir enn tre år tilbake i tid frå tidspunktet det var søkt.","_key":"21d606aed8c9"}]}],"_createdAt":"2023-09-08T06:19:59Z"},{"hjemler":["11"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","tema":"FELLES","endringsaarsaker":["ALLEREDE_UTBETALT"],"_id":"begrunnelse-3969c57f-8c30-4ebe-a24d-3d3416d611a8","navnISystem":"Etterbetaling utvidet EØS","bokmaal":[{"style":"normal","_key":"1a7e2a9450c1","markDefs":[],"children":[{"_key":"540db184ab000","_type":"span","marks":[],"text":"Ordinær barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"19aaa182e167"},{"_type":"span","marks":[],"text":" er allerede utbetalt til den andre forelderen. Vi kan ikke etterbetale barnetrygd som allerede er utbetalt. Det er den utvidede delen av barnetrygden du får etterbetalt.","_key":"96e1874a0a75"}],"_type":"block"}],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Ordinær barnetrygd for barn fødd ","_key":"ee063834dd420"},{"_type":"flettefelt","_key":"2ff0fd344f88","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" er allerede utbetalt til den andre forelderen. Vi kan ikkje etterbetale barnetrygd som allerede er utbetalt. Det er den utvida delen av barnetrygda du får etterbetalt.","_key":"774c9fe40811"}],"_type":"block","style":"normal","_key":"fa5da5d6a188","markDefs":[]}],"_updatedAt":"2023-09-25T10:15:50Z","apiNavn":"endretUtbetalingEtterbetalingUtvidetEos","_createdAt":"2023-06-14T11:42:28Z","mappe":["ENDRET_UTBETALINGSPERIODE"],"_rev":"h26rAhFEYSUtDGXJE0sV8b","_type":"begrunnelse","valgbarhet":"STANDARD","visningsnavn":"15. Etterbetaling utvidet EØS"},{"navnISystem":"Får etterbetalt utvidet for praktisert delt bosted","hjemler":["2","4","9","11","12"],"vilkaar":["UTVIDET_BARNETRYGD"],"behandlingstema":"NASJONAL","_rev":"h26rAhFEYSUtDGXJE0sV8b","periodeType":"UTBETALING","tema":"FELLES","_updatedAt":"2023-09-25T10:15:50Z","apiNavn":"innvilgetFaarEtterbetaltUtvidetForPraktisertDeltBosted","visningsnavn":"97. Får etterbetalt utvidet for praktisert delt bosted","begrunnelsetype":"INNVILGET","nynorsk":[{"_type":"block","style":"normal","_key":"d7e04664f79f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får etterbetalt full utvida barnetrygd fordi de har praktisert delt bustad for barn fødd ","_key":"9715e3a444460"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"bd158d52e50d"},{"_type":"span","marks":[],"text":" før de har ein skriftleg avtale om delt bustad. ","_key":"4b17a918c0ff"}]},{"_type":"block","style":"normal","_key":"319eebed182a","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Sidan det er du som har fått den ordinære barnetrygda utbetalt, får du også den utvida barnetrygda. Det er opp til deg og den andre forelderen å bli einige om korleis de vil fordele barnetrygda for tida før de har skriftleg avtale om delt bustad.","_key":"22f0a07bed30"}]}],"_createdAt":"2023-03-24T16:39:25Z","valgbarhet":"STANDARD","_id":"begrunnelse-3c6a2b3b-e3fe-476d-b51d-66fe32a55771","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","mappe":["INNVILGET"],"bokmaal":[{"style":"normal","_key":"725cb905be6c","markDefs":[],"children":[{"_key":"28a3138db4ad0","_type":"span","marks":[],"text":"Du får etterbetalt full utvidet barnetrygd fordi dere har praktisert delt bosted for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"8f8f6d25cc72"},{"_type":"span","marks":[],"text":" før dere har en skriftlig avtale om delt bosted. ","_key":"98f504c4da8b"}],"_type":"block"},{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Siden det er du som har fått den ordinære barnetrygden utbetalt, får du også den utvidede barnetrygden. Det er opp til deg og den andre forelderen å bli enige om hvordan dere vil fordele barnetrygden for tiden før dere har skriftlig avtale om delt bosted.","_key":"088a552d4224"}],"_type":"block","style":"normal","_key":"99373850f88a"}]},{"borMedSokerTriggere":["DELT_BOSTED_SKAL_IKKE_DELES"],"begrunnelsetype":"OPPHØR","valgbarhet":"STANDARD","visningsnavn":"51. Fast bosted avtale","vilkaar":["BOR_MED_SOKER"],"vedtakResultat":"IKKE_INNVILGET","tema":"NASJONAL","nynorsk":[{"style":"normal","_key":"fb2c135de3f6","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"b7918efca29e0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"6dcea66dc52e"},{"text":" fordi vi har fått ein avtale som sier at ","_key":"a8cd9b92c80a","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"7ff6eae3df4a","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" skal bu fast hos den andre forelderen. ","_key":"f885ed35a94c"}],"_type":"block"}],"_id":"begrunnelse-42213bf3-56b5-45de-800c-6177140bc120","mappe":["OPPHØR"],"_updatedAt":"2023-09-25T10:36:56Z","navnISystem":"Fast bosted avtale","behandlingstema":"NASJONAL","periodeType":"INGEN_UTBETALING","_createdAt":"2022-12-05T08:54:34Z","hjemler":["2","11"],"_type":"begrunnelse","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"a20fd2df9c73"},{"_type":"flettefelt","_key":"e35bc089b598","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi vi har fått en avtale som sier at ","_key":"130e188ba214"},{"_key":"21422d4cbd55","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" skal bo fast hos den andre forelderen. ","_key":"fd144a4c38bf"}],"_type":"block","style":"normal","_key":"1743cb3a3453"}],"apiNavn":"opphorFastBostedAvtale","_rev":"FuD004taptHFqBZyEy8ieT"},{"_id":"begrunnelse-473d7bec-d1d1-4c2a-9bfe-d01dd87c3ad7","bokmaal":[{"_key":"4687dd239b84","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd fordi du bor i institusjon.","_key":"7114bcda1eab0"}],"_type":"block","style":"normal"}],"navnISystem":"Enslig mindreårig flyktning bor i institusjon","periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","_createdAt":"2023-09-07T14:56:12Z","_updatedAt":"2023-09-25T10:31:16Z","visningsnavn":"68. Enslig mindreårig flyktning bor i institusjon","_rev":"FuD004taptHFqBZyEy7kEm","_type":"begrunnelse","nynorsk":[{"_type":"block","style":"normal","_key":"b7b0d970aa30","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd fordi du bur i institusjon.","_key":"52ce4b69e1c70"}]}],"vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOR_MED_SOKER"],"mappe":["AVSLAG"],"apiNavn":"avslagEnsligMindreaarigFlyktningBorIInstitusjon","hjemler":["2","12"],"tema":"NASJONAL","valgbarhet":"STANDARD"},{"begrunnelsetype":"OPPHØR","bokmaal":[{"children":[{"text":"Barnetrygd for barn født ","_key":"c4bd45e28ca30","_type":"span","marks":[]},{"_type":"flettefelt","_key":"5f6462629293","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi ","_key":"8db476960c14"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"7841c205ff8d","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" bor i institusjon ","_key":"1043429f065c"},{"_type":"valgReferanse","_key":"393b51cab35d","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"_type":"span","marks":[],"text":" .","_key":"9a4ef93d644d"}],"_type":"block","style":"normal","_key":"e2e5f3800242","markDefs":[]}],"_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"periodeType":"INGEN_UTBETALING","mappe":["OPPHØR"],"_updatedAt":"2023-09-25T10:36:59Z","hjemler":["2","11"],"vedtakResultat":"IKKE_INNVILGET","valgbarhet":"STANDARD","apiNavn":"opphorBarnetBorIInstitusjon","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"bde80839fcfe"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"a0a5684e57b9"},{"_type":"span","marks":[],"text":" fordi ","_key":"ff4182056a83"},{"_type":"valgfeltV2","_key":"f34e394f80fb","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":" bur i institusjon ","_key":"97156dae5cee"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"d788976ad783"},{"_type":"span","marks":[],"text":" .","_key":"698807d60507"}],"_type":"block","style":"normal","_key":"57269c188e2e","markDefs":[]}],"borMedSokerTriggere":["DELT_BOSTED"],"tema":"NASJONAL","_createdAt":"2023-06-16T09:05:03Z","_id":"begrunnelse-5776677f-3042-4f4a-9c6f-ca82a9cbe2b2","navnISystem":"Barnet bor i institusjon","visningsnavn":"52. Barnet bor i institusjon","_rev":"h26rAhFEYSUtDGXJE0v3cZ"},{"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn født ","_key":"135ae521a6c10"},{"_key":"b1cba1d3e3fb","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"text":" fordi du bor fast sammen med ","_key":"e49483731552","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"68ae031ebcc0","skalHaStorForbokstav":false},{"_key":"1df3dbf87089","_type":"span","marks":[],"text":". Barnetrygd kan kun etterbetales tre år tilbake i tid fra tidspunktet det var søkt."}],"_type":"block","style":"normal","_key":"bc29ae87635a"},{"style":"normal","_key":"90d8a0b3ff4e","markDefs":[],"children":[{"_key":"b3ea03171d750","_type":"span","marks":[],"text":"\n"}],"_type":"block"}],"_type":"begrunnelse","tema":"FELLES","nynorsk":[{"_key":"6f9f97e7ee57","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"2d72a79b04740"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"3f84a886895d"},{"_type":"span","marks":[],"text":" fordi du bur fast saman med ","_key":"df8f28877c1a"},{"_key":"0fe84659617c","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". Barnetrygd kan kun etterbetalast tre år tilbake i tid frå tidspunktet det var søkt.","_key":"418dc2c23728"}],"_type":"block","style":"normal"}],"_createdAt":"2023-09-13T13:07:01Z","_updatedAt":"2023-09-25T10:15:50Z","apiNavn":"etterEndretUtbetalingEtterbetalingTreAar","visningsnavn":"8. Etterbetaling tre år","_rev":"h26rAhFEYSUtDGXJE0sV8b","_id":"begrunnelse-722f1d53-203f-4006-85b4-1d915607428f","hjemler":["2","11"],"endretUtbetalingsperiodeTriggere":["ETTER_ENDRET_UTBETALINGSPERIODE"],"vedtakResultat":"IKKE_INNVILGET","periodeType":"UTBETALING","endringsaarsaker":["ETTERBETALING_3ÅR"],"navnISystem":"Etterbetaling tre år","begrunnelsetype":"ETTER_ENDRET_UTBETALINGSPERIODE","valgbarhet":"STANDARD","mappe":["ETTER_ENDRET_UTBETALINGSPERIODE"]},{"vedtakResultat":"IKKE_INNVILGET","endringsaarsaker":["ETTERBETALING_3ÅR"],"visningsnavn":"17. Etterbetaling tre år tilbake i tid - Ingen utbetaling","periodeType":"INGEN_UTBETALING","tema":"NASJONAL","_createdAt":"2023-09-08T06:06:30Z","valgbarhet":"STANDARD","_type":"begrunnelse","apiNavn":"endretUtbetalingEtterbetalingTreAarTilbakeITid","_rev":"FuD004taptHFqBZyEy6ivL","_id":"begrunnelse-7c29ccc4-9f3b-4fa6-8af0-a3851ffbbe7d","mappe":["ENDRET_UTBETALINGSPERIODE"],"_updatedAt":"2023-09-25T10:24:23Z","navnISystem":"Etterbetaling tre år tilbake i tid","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","nynorsk":[{"style":"normal","_key":"5949ec21ed3b","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"cee08510964f"},{"_type":"flettefelt","_key":"b96f7e57e64f","flettefelt":"barnasFodselsdatoer"},{"marks":[],"text":". Du søker om barnetrygd tilbake i tid. Vi fekk søknaden din ","_key":"030eb73383c3","_type":"span"},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"f4481275e665"},{"_type":"span","marks":[],"text":". Barnetrygd kan ikkje utbetalast for meir enn tre år tilbake i tid frå vi fekk søknaden din.","_key":"35b33e08da8a"}],"_type":"block"}],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"6e17c71ea0b7"},{"_type":"flettefelt","_key":"79a8b784111c","flettefelt":"barnasFodselsdatoer"},{"text":". Du søker om barnetrygd tilbake i tid. Vi fikk søknaden din ","_key":"cd11e0c15319","_type":"span","marks":[]},{"flettefelt":"soknadstidspunkt","_type":"flettefelt","_key":"8bc3103a2305"},{"_type":"span","marks":[],"text":". Barnetrygd kan ikke utbetales for mer enn tre år tilbake i tid fra vi fikk søknaden din.","_key":"043efffbd341"}],"_type":"block","style":"normal","_key":"08e5b214e4ec","markDefs":[]},{"markDefs":[],"children":[{"_type":"span","marks":[],"text":" ","_key":"b53426db041f"}],"_type":"block","style":"normal","_key":"6a9f009846fb"}],"hjemler":["11"]},{"hjemler":["2","4","5"],"_type":"begrunnelse","tema":"NASJONAL","nynorsk":[{"_type":"block","style":"normal","_key":"e953a939fae1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi du ikkje bur fast saman med barn fødd ","_key":"ae15fc2873fc0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"c59947b6c5d5"},{"_type":"span","marks":[],"text":".","_key":"3aec4ec36f1f"}]}],"navnISystem":"Bor ikke fast med barnet","visningsnavn":"14. Bor ikke fast med barnet","vedtakResultat":"IKKE_INNVILGET","_id":"begrunnelse-8fc3ea83-59aa-4a06-bc68-1d96fcae6f5c","mappe":["AVSLAG"],"apiNavn":"avslagBorIkkeFastMedBarnet","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:26:31Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi du ikke bor fast sammen med barn født ","_key":"6bc5c5c156a20"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"f1452700a832"},{"_key":"9413e366961f","_type":"span","marks":[],"text":"."}],"_type":"block","style":"normal","_key":"66a4bae9fc1e"}],"begrunnelsetype":"AVSLAG","resultat":"IKKE_INNVILGET","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"INGEN_UTBETALING","_createdAt":"2023-03-24T14:34:35Z","_rev":"h26rAhFEYSUtDGXJE0sx89"},{"_createdAt":"2023-06-16T13:25:48Z","_id":"begrunnelse-97048d0a-3ab6-4139-bb4b-66ea11704bbc","mappe":["INNVILGET"],"visningsnavn":"100. Opphold på Svalbard","periodeType":"UTBETALING","tema":"NASJONAL","bosattIRiketTriggere":["MEDLEMSKAP"],"_updatedAt":"2023-09-25T10:15:50Z","navnISystem":"Opphold på Svalbard","_type":"begrunnelse","begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","hjemler":["2","3","11"],"nynorsk":[{"style":"normal","_key":"b4cfc29e899d","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"badecb92c6f00"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"40fbd167c4c7"},{"_type":"span","marks":[],"text":" under opphaldet på Svalbard fordi ","_key":"3f32a35fb22b"},{"_key":"6fa104f5b31c","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" fortsatt er medlem i folketrygda.","_key":"0b24e6ac69dc"}],"_type":"block"}],"rolle":["SOKER","BARN"],"bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd for barn født ","_key":"f73738afa5410","_type":"span"},{"_type":"flettefelt","_key":"6a02eaa0c5b6","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" under oppholdet på Svalbard fordi ","_key":"2134e7bc0eef"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"3bcdd1b56cd3"},{"_type":"span","marks":[],"text":" fortsatt er medlem i folketrygden.","_key":"08947ea21c2a"}],"_type":"block","style":"normal","_key":"ac16d446d302"}],"apiNavn":"innvilgetOppholdPaaSvalbard","_rev":"h26rAhFEYSUtDGXJE0sV8b","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["BOSATT_I_RIKET"]},{"apiNavn":"innvilgetDatoSkriftligAvtaleDeltBosted","visningsnavn":"98. Dato skriftlig avtale delt bosted","borMedSokerTriggere":["DELT_BOSTED"],"begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:50Z","bokmaal":[{"_type":"block","style":"normal","_key":"858fd745bb88","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du har skriftlig avtale om delt bosted for barn født ","_key":"e693bb022c010"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"4ada74642839"},{"_type":"span","marks":[],"text":" fra ","_key":"ac2fc3a0341b"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"17c9a11ca2db"},{"text":".","_key":"4f3180819b7e","_type":"span","marks":[]}]}],"navnISystem":"Dato skriftlig avtale delt bosted","_rev":"h26rAhFEYSUtDGXJE0sV8b","vilkaar":["BOR_MED_SOKER"],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du har skriftleg avtale om delt bustad for barn fødd ","_key":"191d7c7042730"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"bb9fcbff4d0a"},{"_type":"span","marks":[],"text":" frå ","_key":"236b7fa64371"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"25000ad71d50"},{"_type":"span","marks":[],"text":".","_key":"74dceeb9068d"}],"_type":"block","style":"normal","_key":"c594eabc2194","markDefs":[]}],"hjemler":["2"],"behandlingstema":"NASJONAL","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","tema":"NASJONAL","_createdAt":"2023-03-24T16:44:28Z","_id":"begrunnelse-97a87975-b6d2-4398-868b-af595fec4ea7"},{"vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"nynorsk":[{"_key":"2833f87ab558","markDefs":[],"children":[{"text":"Barnetrygd for barn fødd ","_key":"e3fb833676ff0","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"d89e9265cf03"},{"text":" fordi den andre forelderen har fått barnetrygd for ","_key":"6491f2fb38a9","_type":"span","marks":[]},{"_type":"valgfeltV2","_key":"4eddb8c4e5e3","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"marks":[],"text":" i samme tidsrom.","_key":"aba2b0465820","_type":"span"}],"_type":"block","style":"normal"}],"_createdAt":"2023-03-24T14:51:05Z","valgbarhet":"STANDARD","hjemler":["2","12"],"apiNavn":"opphorBeggeForeldreFaattBarnetrygd","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","mappe":["OPPHØR"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"ff37490e7e520"},{"_type":"flettefelt","_key":"c71961bea421","flettefelt":"barnasFodselsdatoer"},{"_key":"3dfae77570e2","_type":"span","marks":[],"text":" fordi den andre forelderen har fått barnetrygd for "},{"_key":"5da8a7c3acc1","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"marks":[],"text":" i samme tidsrom.","_key":"76beed8dfe74","_type":"span"}],"_type":"block","style":"normal","_key":"f79cd0ac61bc","markDefs":[]}],"navnISystem":"Begge foreldre fått barnetrygd","behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr5gco","tema":"NASJONAL","_id":"begrunnelse-9fbfd6d9-0526-45f3-97b1-8f1c96112cf1","_updatedAt":"2023-09-25T10:35:55Z","visningsnavn":"36. Begge foreldre fått barnetrygd","begrunnelsetype":"OPPHØR","periodeType":"INGEN_UTBETALING"},{"mappe":["OPPHØR"],"apiNavn":"opphorVurderingIkkeBosattINorge","hjemler":["2","4","11"],"bosattIRiketTriggere":["VURDERING_ANNET_GRUNNLAG"],"_id":"begrunnelse-a142797b-23e1-4c35-a276-6c0d86e0e663","_updatedAt":"2023-09-25T10:37:07Z","periodeType":"INGEN_UTBETALING","begrunnelsetype":"OPPHØR","tema":"NASJONAL","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"7b77e048a9d70"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"f843292a15fb"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at ","_key":"7861ab00e551"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"78634fbdf0cb","skalHaStorForbokstav":false},{"_key":"f4f8618b65a5","_type":"span","marks":[],"text":" ikke er bosatt i Norge fra "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"2a2d6070ae0a"},{"_type":"span","marks":[],"text":".","_key":"bfdaebb28aaf"}],"_type":"block","style":"normal","_key":"5eb1d529a793"}],"navnISystem":"Vurdering ikke bosatt I Norge","rolle":["SOKER","BARN"],"_type":"begrunnelse","vedtakResultat":"REDUKSJON","vilkaar":["BOSATT_I_RIKET"],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"dc37ea8cdc2a0"},{"_key":"dc5694435ff3","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":"fordi vi har kome fram til at ","_key":"4117165b52a1"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"4d7e42480866","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikkje er busett i Noreg frå ","_key":"7e52d1374860"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"1896b314ddbd"},{"_type":"span","marks":[],"text":".","_key":"f99cf4d38147"}],"_type":"block","style":"normal","_key":"f1ded6181896","markDefs":[]},{"_type":"block","style":"normal","_key":"b89aaf2f5df0","markDefs":[],"children":[{"_type":"span","marks":[],"text":"\n","_key":"ee84a2e7565e0"}]}],"_createdAt":"2023-09-07T14:29:39Z","valgbarhet":"STANDARD","visningsnavn":"54. Vurdering ikke bosatt i Norge","_rev":"h26rAhFEYSUtDGXJE0v3zw"},{"periodeType":"UTBETALING","nynorsk":[{"_type":"block","style":"normal","_key":"dec7fc193c11","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for tida frå du og den andre forelderen flytta frå kvarandre, og praktiserte delt bustad, til de har ein skriftleg avtale om delt bustad for barn fødd ","_key":"7795bc4a83370"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"510fa37596e3"},{"_type":"span","marks":[],"text":", er utbetalt til den andre forelderen. ","_key":"c530939c2f12"}]},{"markDefs":[],"children":[{"_key":"b5fabfefd2dc0","_type":"span","marks":[],"text":"Den ordinære barnetrygda for perioden etter at de har skriftleg avtale om delt bustad fram til du søkte om barnetrygd er også utbetalt til den andre forelderen. "}],"_type":"block","style":"normal","_key":"528a04c6ed15"},{"children":[{"text":"Vi kan ikkje etterbetale barnetrygd som allereie er utbetalt. Det er opp til dykk å bli einige om korleis de vil fordele denne barnetrygda. ","_key":"11bb755e7e1a0","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"0d9ea9b099e2","markDefs":[]},{"style":"normal","_key":"4b27b2ffbd5f","markDefs":[],"children":[{"text":"Du får den halve utvida delen av barnetrygda etterbetalt frå månaden etter at de har skriftleg avtale om delt bustad.","_key":"cd522f7281070","_type":"span","marks":[]}],"_type":"block"},{"style":"normal","_key":"9e901b5673c0","markDefs":[],"children":[{"text":"\n","_key":"a9ec79985dd30","_type":"span","marks":[]}],"_type":"block"}],"_id":"begrunnelse-a961f86c-2a76-48e5-a622-6b6288e0bd6c","apiNavn":"endretUtbetalingEtterbetaltUtvidetDelFraSkriftligAvtaleSokerPraktisert","_type":"begrunnelse","_createdAt":"2023-03-24T17:00:45Z","valgbarhet":"STANDARD","hjemler":["2","12"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"UTBETALING_IKKE_RELEVANT","mappe":["ENDRET_UTBETALINGSPERIODE"],"_updatedAt":"2023-09-25T10:15:50Z","visningsnavn":"14. Etterbetalt utvidet del fra avtaletidspunkt søkt for praktisert delt","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","_rev":"h26rAhFEYSUtDGXJE0sV8b","tema":"FELLES","endringsaarsaker":["DELT_BOSTED"],"bokmaal":[{"style":"normal","_key":"4b4daf9e4920","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for tiden fra du og den andre forelderen flyttet fra hverandre, og praktiserte delt bosted, til dere har en skriftlig avtale om delt bosted for barn født ","_key":"055f1ffcc7a50"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"13db5c8fbbd5"},{"_type":"span","marks":[],"text":", er utbetalt til den andre forelderen. ","_key":"72bbedfedc72"}],"_type":"block"},{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Den ordinære barnetrygden for perioden etter at dere har skriftlig avtale om delt bosted fram til du søkte om barnetrygd er også utbetalt til den andre forelderen. ","_key":"f527db2407400"}],"_type":"block","style":"normal","_key":"099f3b8a266a"},{"_type":"block","style":"normal","_key":"7004f5356129","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Vi kan ikke etterbetale barnetrygd som allerede er utbetalt. Det er opp til dere å bli enige om hvordan dere vil fordele denne barnetrygden. ","_key":"8d3210adb30c0"}]},{"style":"normal","_key":"e2d3ac39240e","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får den halve utvidede delen av barnetrygden etterbetalt fra måneden etter at dere har skriftlig avtale om delt bosted.","_key":"9276eb9708470"}],"_type":"block"}],"navnISystem":"Etterbetalt utvidet del fra avtaletidspunkt søkt for praktisert delt","behandlingstema":"NASJONAL"},{"borMedSokerTriggere":["DELT_BOSTED_SKAL_IKKE_DELES"],"_id":"begrunnelse-aa1feef7-cb66-4ffa-a9a1-6e9ab92ccb06","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"d97ee69a3e150"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"ca87322e0107"},{"_type":"span","marks":[],"text":" fordi ","_key":"4c4459c18ef1"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"e9d052e9fbeb","skalHaStorForbokstav":false},{"marks":[],"text":" ikke bor hos deg fra ","_key":"d1d33b4aeda9","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"9ee5063632ad"},{"_key":"71026ee40db8","_type":"span","marks":[],"text":"."}],"_type":"block","style":"normal","_key":"64324d428a40"}],"_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"begrunnelsetype":"OPPHØR","apiNavn":"opphorBarnBorIkkeMedSokerEtterDeltBosted","hjemler":["2","11"],"vedtakResultat":"REDUKSJON","tema":"NASJONAL","valgbarhet":"STANDARD","mappe":["OPPHØR"],"navnISystem":"Barn bor ikke med søker","visningsnavn":"53. Barn bor ikke med søker etter delt bosted","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"9ebbd275c5220"},{"_key":"d9e7e37a83b1","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi ","_key":"50ff375cd07d"},{"_key":"9d94989826d6","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"text":" ikkje bur hos deg frå ","_key":"d5b388d4869a","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"f7fb081cbb47"},{"_key":"ebbe60278374","_type":"span","marks":[],"text":"."}],"_type":"block","style":"normal","_key":"54f51c439062"}],"_createdAt":"2023-09-07T14:06:40Z","_updatedAt":"2023-09-25T10:37:03Z","_rev":"BtltdVb0HP4g4WJfnr5mHo","periodeType":"INGEN_UTBETALING"},{"hjemler":["2","4","11"],"_id":"begrunnelse-b03a8211-36e9-4ce8-a130-870e2cb96a5f","apiNavn":"opphorOppholdstillatelseUtloptInstitusjon","_type":"begrunnelse","rolle":["SOKER","BARN"],"tema":"NASJONAL","nynorsk":[{"markDefs":[],"children":[{"text":"Barnetrygd fordi barnet ikkje lenger har opphaldsløyve i Noreg ","_key":"c14e79c5e2b3","_type":"span","marks":[]},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"a9d56999456f"},{"marks":[],"text":".","_key":"bae55587df90","_type":"span"}],"_type":"block","style":"normal","_key":"9d585f85c0e9"}],"_updatedAt":"2023-09-25T10:38:16Z","bokmaal":[{"_key":"d4b1472d35b4","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd fordi barnet ikke lenger har oppholdstillatelse i Norge ","_key":"e688cf76970b"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"9c40a6cdb1ae"},{"_type":"span","marks":[],"text":".","_key":"83f7401e0d70"}],"_type":"block","style":"normal"}],"fagsakType":"INSTITUSJON","behandlingstema":"NASJONAL_INSTITUSJON","_rev":"h26rAhFEYSUtDGXJE0v6LF","vedtakResultat":"IKKE_INNVILGET","vilkaar":["LOVLIG_OPPHOLD"],"periodeType":"INGEN_UTBETALING","valgbarhet":"SAKSPESIFIKK","navnISystem":"Oppholdstillatelse utløpt","type":"OPPHØR","begrunnelsetype":"OPPHØR","_createdAt":"2022-11-16T13:18:34Z","mappe":["INSTITUSJON","OPPHØR"],"visningsnavn":"5. Oppholdstillatelse utløpt"},{"visningsnavn":"75. Opphør av utvidet på grunn av nyfødt barn","periodeType":"UTBETALING","apiNavn":"reduksjonTilleggstekstOpphorUtvidetNyfoedtBarn","mappe":["REDUKSJON"],"vedtakResultat":"REDUKSJON","begrunnelsetype":"REDUKSJON","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:50Z","navnISystem":"Tilleggstekst opphør utvidet nyfødt barn","_type":"begrunnelse","tema":"FELLES","_createdAt":"2023-09-18T11:41:03Z","_id":"begrunnelse-b8fcc4ba-f98b-4009-93ed-2e9323cc3b7e","_rev":"h26rAhFEYSUtDGXJE0sV8b"},{"endretUtbetalingsperiodeTriggere":["ETTER_ENDRET_UTBETALINGSPERIODE"],"nynorsk":[{"_key":"a8059c687d74","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd frå same tidspunkt som utbetalinga til den andre forelderen er stansa. ","_key":"a5474bc1ad1e"}],"_type":"block","style":"normal"}],"endringsaarsaker":["ALLEREDE_UTBETALT"],"_createdAt":"2023-09-19T07:17:21Z","_id":"begrunnelse-bc7cdd85-8f78-4c5f-997a-f6ba617b2d01","_updatedAt":"2023-09-25T10:15:50Z","visningsnavn":"9. EØS barnetrygd allerede utbetalt","hjemler":["2","4","12"],"begrunnelsetype":"ETTER_ENDRET_UTBETALINGSPERIODE","navnISystem":"EØS etter endret utbetaling","vedtakResultat":"IKKE_INNVILGET","valgbarhet":"STANDARD","_rev":"h26rAhFEYSUtDGXJE0sV8b","_type":"begrunnelse","periodeType":"UTBETALING","tema":"FELLES","mappe":["ETTER_ENDRET_UTBETALINGSPERIODE"],"bokmaal":[{"style":"normal","_key":"63b76b61a11a","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd fra samme tidspunkt som utbetalingen til den andre forelderen er stanset. ","_key":"8aa5a792a082","_type":"span"}],"_type":"block"}],"apiNavn":"etterEndretUtbetalingEosBarnetrygdAlleredeUtbetalt"},{"navnISystem":"Overgang EØS til nasjonal separasjonsavtalen","apiNavn":"innvilgetOvergangEosTilNasjonalSeparasjonsavtalen","vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"NASJONAL","nynorsk":[{"_key":"bdc62e82a806","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter nasjonale reglar for barn fødd ","_key":"32b026c725660"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"8331d6b70744"},{"_type":"span","marks":[],"text":" fordi separasjonsavtalen mellom Storbritannia og Noreg ikkje gjeld for familien lenger.","_key":"d8a97ac3d620"}],"_type":"block","style":"normal"}],"_id":"begrunnelse-c2d72a98-f00d-4a2a-8c38-0efb8c40933e","bokmaal":[{"_type":"block","style":"normal","_key":"ae1ee3b44455","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter nasjonale regler for barn født ","_key":"04f00a8804d90"},{"_key":"5b5e377b4eaa","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"text":" fordi separasjonsavtalen mellom Storbritannia og Norge ikke gjelder for familien lenger.","_key":"2ac80287a648","_type":"span","marks":[]}]}],"hjemler":["2","4","11"],"_type":"begrunnelse","begrunnelsetype":"INNVILGET","_rev":"h26rAhFEYSUtDGXJE0sV8b","vilkaar":["BOSATT_I_RIKET"],"periodeType":"UTBETALING","rolle":["SOKER","BARN"],"visningsnavn":"96. Overgang EØS til nasjonal separasjonsavtalen","behandlingstema":"NASJONAL","_createdAt":"2023-03-24T15:48:56Z","valgbarhet":"STANDARD","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:50Z"},{"endringsaarsaker":["ALLEREDE_UTBETALT"],"_createdAt":"2023-06-14T12:42:10Z","_id":"begrunnelse-d3b871a7-ef0d-4e1e-9f4c-40e881dc98ee","mappe":["ENDRET_UTBETALINGSPERIODE"],"_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","navnISystem":"Allerede utbetalt - Test","valgbarhet":"STANDARD","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","tema":"FELLES","apiNavn":"endretUtbetalingAlleredeUtbetalt","hjemler":["12"],"periodeType":"INGEN_UTBETALING","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"520de0bd53e5"},{"_key":"2860a3924250","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi den andre forelderen allereie har fått barnetrygd for ","_key":"4de10fecab42"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"ba60ef55aa2d"},{"_type":"span","marks":[],"text":".","_key":"ea3e57beaf8b"}],"_type":"block","style":"normal","_key":"c1c3e217424e"}],"_updatedAt":"2023-09-25T11:17:44Z","bokmaal":[{"children":[{"text":"Barnetrygd for barn født ","_key":"8b2ace6c185a","_type":"span","marks":[]},{"_type":"flettefelt","_key":"d7da348c9051","flettefelt":"barnasFodselsdatoer"},{"text":" fordi den andre forelderen allerede har fått barnetrygd for ","_key":"620ca59b1c02","_type":"span","marks":[]},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"ff58089ffd27"},{"_type":"span","marks":[],"text":".","_key":"b7ba5d47afb6"}],"_type":"block","style":"normal","_key":"c0cbc7ef3a48","markDefs":[]}],"visningsnavn":"16. Allerede utbetalt","_rev":"h26rAhFEYSUtDGXJE0xX1X"},{"behandlingstema":"NASJONAL_INSTITUSJON","periodeType":"INGEN_UTBETALING","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd fordi barnet er 18 år.","_key":"5923bea519f0"}],"_type":"block","style":"normal","_key":"57977e54a00a"}],"_updatedAt":"2023-09-25T10:38:20Z","fagsakType":"INSTITUSJON","hjemler":["11"],"vedtakResultat":"IKKE_INNVILGET","navnISystem":"Barn 18 år","type":"OPPHØR","vilkaar":["UNDER_18_ÅR"],"_createdAt":"2022-11-16T13:27:20Z","valgbarhet":"SAKSPESIFIKK","tema":"NASJONAL","_id":"begrunnelse-e004dc2d-92cd-4b58-a1df-2ef1ea287661","mappe":["INSTITUSJON","OPPHØR"],"apiNavn":"opphorBarnetEr18AarInstitusjon","visningsnavn":"6. Barnet er 18 år institusjon","_rev":"BtltdVb0HP4g4WJfnr5qFo","_type":"begrunnelse","begrunnelsetype":"OPPHØR","bokmaal":[{"style":"normal","_key":"3dcba37278eb","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd fordi barnet er 18 år.","_key":"5ceab07a9818"}],"_type":"block"}]},{"navnISystem":"Delt fra skriftlig avtale har søkt for praktisert delt bosted","apiNavn":"innvilgetDeltFraSkriftligAvtaleHarSoktForPraktisertDeltBosted","visningsnavn":"99. Delt fra skriftlig avtale har søkt for praktisert delt bosted","behandlingstema":"NASJONAL","_createdAt":"2023-03-24T16:53:57Z","valgbarhet":"STANDARD","bokmaal":[{"_key":"abb0a7d048c9","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får delt barnetrygd for barn født ","_key":"187d87cd43730"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"245a1d5b2216"},{"_type":"span","marks":[],"text":" fordi du har skriftlig avtale om delt bosted for ","_key":"55fa5b27c19d"},{"_key":"104b9cf37489","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2"},{"_key":"56c1e6fbac1d","_type":"span","marks":[],"text":" fra "},{"_type":"flettefelt","_key":"42ec7aa75c81","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"text":". Barnetrygden deles fra måneden etter at dere har skriftlig avtale om delt bosted. ","_key":"e74fb39b2385","_type":"span","marks":[]}],"_type":"block","style":"normal"},{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for tiden fra du og den andre forelderen flyttet fra hverandre, og praktiserte delt bosted, til dere har en skriftlig avtale om delt bosted for ","_key":"4e4d93643a4b"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"e197d19ddd10","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":", er utbetalt til den andre forelderen. Det er opp til dere å bli enige om hvordan dere vil fordele denne barnetrygden.","_key":"f08ba9c91f9d"}],"_type":"block","style":"normal","_key":"512130071d06","markDefs":[]}],"hjemler":["2","4","11","12"],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","tema":"NASJONAL","_id":"begrunnelse-ef69ad9d-fd84-4ae9-8279-4584570fbc5f","borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"UTBETALING","_updatedAt":"2023-09-25T10:15:50Z","_rev":"h26rAhFEYSUtDGXJE0sV8b","vilkaar":["BOR_MED_SOKER"],"nynorsk":[{"markDefs":[],"children":[{"text":"Du får delt barnetrygd for barn fødd ","_key":"a76071f7aa9f0","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"627203dcfe44"},{"_key":"32533dc9f2d0","_type":"span","marks":[],"text":" fordi du har skriftleg avtale om delt bustad for "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"aa7a6da674d3","skalHaStorForbokstav":false},{"_key":"8ea9b6bc4cac","_type":"span","marks":[],"text":" frå "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"0f1a20af7ea6"},{"_type":"span","marks":[],"text":". Barnetrygda er delt frå månaden etter at de har skriftleg avtale om delt bustad. ","_key":"ce5d7e6ba9b9"}],"_type":"block","style":"normal","_key":"2cc8b774e43e"},{"style":"normal","_key":"cc770c8fdd6f","markDefs":[],"children":[{"_key":"a3164a1dbc5b","_type":"span","marks":[],"text":"Barnetrygd for tida frå du og den andre forelderen flytta frå kvarandre, og praktiserte delt bustad, til de har ein skriftleg avtale om delt bustad for "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"cd17cb643c28","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":", er utbetalt til den andre forelderen.Det er opp til dykk å bli einige om korleis de vil fordele denne barnetrygda.","_key":"1dffd37ade67"}],"_type":"block"}],"mappe":["INNVILGET"]},{"navnISystem":"EØS-borger samboer utbetaling fra NAV","hjemler":["2","4","11"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["LOVLIG_OPPHOLD"],"_updatedAt":"2023-09-25T10:15:50Z","visningsnavn":"75. EØS-borger samboer utbetaling fra NAV","_rev":"h26rAhFEYSUtDGXJE0sV8b","nynorsk":[{"_type":"block","style":"normal","_key":"b4011aa0cde2","markDefs":[],"children":[{"text":"Du får barnetrygd fordi du har opphaldsrett som EØS-borgar. Familien bur i Noreg og sambuaren din får utbetaling frå NAV som erstattar løn frå ","_key":"122a1831ef100","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"d9514e0e5e8a"},{"_type":"span","marks":[],"text":".","_key":"78446234b358"}]}],"valgbarhet":"STANDARD","_id":"bfae1def-bd0b-41d9-993f-562a510bed3c","mappe":["INNVILGET"],"_type":"begrunnelse","periodeType":"UTBETALING","rolle":["SOKER","BARN"],"apiNavn":"innvilgetEosBorgerSamboerUtbetalingFraNav","begrunnelsetype":"INNVILGET","tema":"NASJONAL","_createdAt":"2022-03-17T18:24:21Z","bokmaal":[{"_type":"block","style":"normal","_key":"9b4bb7d09652","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har oppholdsrett som EØS-borger. Familien bor i Norge og samboeren din får utbetaling fra NAV som erstatter lønn fra ","_key":"b4b3e1642dc30"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"8bb8edeebc85"},{"_type":"span","marks":[],"text":". ","_key":"fbc4cc81397f"}]}]},{"_rev":"h26rAhFEYSUtDGXJE0sV8b","vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi du bodde sammen med den andre forelderen til barn født ","_key":"5a6c509b2694"},{"_key":"80a6d1690201","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"ecf7554589f7"}],"_type":"block","style":"normal","_key":"80a036abe27c","markDefs":[]}],"valgbarhet":"STANDARD","_id":"c08ab2ca-654a-41f8-aa0f-775671118ee1","_updatedAt":"2023-09-25T10:15:50Z","visningsnavn":"51. Foreldrene bodde sammen","behandlingstema":"NASJONAL","_type":"begrunnelse","tema":"NASJONAL","ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"],"hjemler":["2"],"begrunnelsetype":"REDUKSJON","nynorsk":[{"_type":"block","style":"normal","_key":"74d22555c4fa","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du budde saman med den andre forelderen til barn fødd ","_key":"857fe5a84266"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"81bfb7507d79"},{"_type":"span","marks":[],"text":".","_key":"c003b67837db"}]}],"mappe":["REDUKSJON"],"_createdAt":"2021-11-05T10:38:35Z","navnISystem":"Foreldrene bodde sammen","apiNavn":"reduksjonForeldreneBoddeSammen","vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["DELT_BOSTED"]},{"navnISystem":"Forsvunnet ektefelle","apiNavn":"innvilgetForsvunnetEktefelle","bokmaal":[{"_type":"block","style":"normal","_key":"0033eef64171","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"a6dabd3acb83"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"cf3d1090d683","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd selv om du fortsatt er gift fordi du bor alene med ","_key":"7f4351455411"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"fc697af5c85a","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Ektefellen din har vært forsvunnet i seks måneder eller mer fra ","_key":"66a37a360e5d"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"4f08c42295c7"},{"_type":"span","marks":[],"text":". ","_key":"ca5477b7a8ac"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"a73604c33b2a","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at ektefellen din forsvant.","_key":"0311d798d232"}]}],"visningsnavn":"56. Forsvunnet ektefelle","_rev":"h26rAhFEYSUtDGXJE0sV8b","_createdAt":"2021-10-25T09:55:07Z","valgbarhet":"STANDARD","_id":"c1c79549-2691-46b5-8719-f8dd2e0b5963","_type":"begrunnelse","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","tema":"FELLES","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"0f513c47f132"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"871cbcfefa99","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd selv om du fortsatt er gift fordi du bor alene med ","_key":"b1e00ad0f891"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"aee310e45fbb"},{"_type":"span","marks":[],"text":". Ektefellen din har vært forsvunnet i seks måneder eller mer fra ","_key":"3d6ba634f8bd"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"4ece3910514a"},{"_type":"span","marks":[],"text":". ","_key":"bf24c37df7bd"},{"_key":"1ecc9c59c51f","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at ektefellen din forsvant.","_key":"b5a8c5bb8286"}],"_type":"block","style":"normal","_key":"2da8bdbf4549"}],"mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:50Z","hjemler":["2","9","11"],"behandlingstema":"NASJONAL","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["UTVIDET_BARNETRYGD"]},{"apiNavn":"reduksjonSamboerIkkeLengerForsvunnet","visningsnavn":"37. Samboer ikke lenger forsvunnet","begrunnelsetype":"REDUKSJON","bokmaal":[{"_type":"block","style":"normal","_key":"b50b51c28179","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi samboeren din ikke lenger er forsvunnet fra ","_key":"c7b22737ead8"},{"_key":"2357449c4b83","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"387c3c5ae466"}]}],"navnISystem":"Samboer ikke lenger forsvunnet","_rev":"h26rAhFEYSUtDGXJE0sV8b","_type":"begrunnelse","valgbarhet":"STANDARD","_id":"c218ced8-3223-4267-b84e-f90e89deba8a","vedtakResultat":"REDUKSJON","_createdAt":"2021-10-23T06:08:03Z","hjemler":["2","9","11"],"vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","tema":"FELLES","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi sambuaren din ikkje lenger er forsvunnen frå ","_key":"94d86a354970"},{"_key":"64b2d3a0902b","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"e36c60b0d62c"}],"_type":"block","style":"normal","_key":"7bd95ebae8c9"}],"mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:50Z"},{"vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utvida barnetrygd fordi sambuaren din fortsatt er i fengsel.","_key":"e6108084778c"}],"_type":"block","style":"normal","_key":"670a0904e08d"}],"_createdAt":"2021-10-25T10:09:53Z","mappe":["FORTSATT_INNVILGET"],"navnISystem":"Fengsel samboer","_rev":"h26rAhFEYSUtDGXJE0slgt","_type":"begrunnelse","_updatedAt":"2023-09-25T10:22:48Z","apiNavn":"fortsattInnvilgetFengselSamboer","hjemler":["9"],"periodeType":"FORTSATT_INNVILGET","tema":"FELLES","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Du får utvidet barnetrygd fordi samboeren din fortsatt er i fengsel.","_key":"84cc49e31be5","_type":"span"}],"_type":"block","style":"normal","_key":"ae2f8328910a"}],"visningsnavn":"22. Fengsel samboer","vedtakResultat":"INGEN_ENDRING","_id":"c2faa6a3-07b9-4359-bf45-d0e3652828da","begrunnelsetype":"FORTSATT_INNVILGET","valgbarhet":"STANDARD"},{"begrunnelsetype":"OPPHØR","tema":"FELLES","_createdAt":"2022-04-28T15:09:08Z","visningsnavn":"46. Ugyldig kontonummer","_rev":"BtltdVb0HP4g4WJfnr5jro","_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"rolle":["SOKER"],"bosattIRiketTriggere":["VURDERING_ANNET_GRUNNLAG"],"_id":"c389cc68-39d1-4943-af64-8a8bd33f9ffd","apiNavn":"opphorUgyldigKontonummer","hjemler":["17","18"],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:36:36Z","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","nynorsk":[{"style":"normal","_key":"0ec115e2c32b","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn fødd ","_key":"0e6aa856c352","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"763637b38305"},{"_key":"6b8a2cd18f4a","_type":"span","marks":[],"text":" fordi du ikkje har gyldig kontonummer."}],"_type":"block"}],"mappe":["OPPHØR"],"bokmaal":[{"style":"normal","_key":"dab8743c438b","markDefs":[],"children":[{"_key":"215529304cb3","_type":"span","marks":[],"text":"Barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"24749675242e"},{"_key":"61106f5a64aa","_type":"span","marks":[],"text":" fordi du ikke har gyldig kontonummer."}],"_type":"block"}],"navnISystem":"Ugyldig kontonummer","behandlingstema":"NASJONAL"},{"bokmaal":[{"_key":"15fe3d7b1c53","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"174e155c7134"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"0a4b3de8d162","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd selv om du fortsatt er gift fordi du bor alene med ","_key":"70c0b71c07ee"},{"_type":"valgfeltV2","_key":"c66356a0c2e0","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"marks":[],"text":". Forholdet er avsluttet og dere har ikke bodd sammen i minst seks måneder i ","_key":"fa947820b3e7","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"f330139fd8b8"},{"marks":[],"text":". ","_key":"739cd3234a73","_type":"span"},{"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"},"_type":"valgfeltV2","_key":"820744616e87","skalHaStorForbokstav":true},{"_key":"dc32c8561e28","_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at samlivsbruddet har vart i seks måneder."}],"_type":"block","style":"normal"}],"tema":"FELLES","behandlingstema":"NASJONAL","_rev":"h26rAhFEYSUtDGXJE0sV8b","_type":"begrunnelse","periodeType":"UTBETALING","_id":"c471dade-3a29-49fb-9861-a00c4062a8c2","mappe":["INNVILGET"],"hjemler":["2","9","11"],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:50Z","apiNavn":"innvilgetFaktiskSeparasjon","visningsnavn":"39. Faktisk separasjon","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"INNVILGET","nynorsk":[{"_type":"block","style":"normal","_key":"13481ab1e134","markDefs":[],"children":[{"_key":"330f2e4f7735","_type":"span","marks":[],"text":""},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"82983dd28b04","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvida barnetrygd sjølv om du fortsatt er gift fordi du bur aleine med ","_key":"8de59aa78936"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"99c31b6de3f2","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Forholdet er avslutta og de har ikkje budd saman i minst seks månader i ","_key":"3baadfc2c253"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"5572f96f8938"},{"_type":"span","marks":[],"text":". ","_key":"c4292cee1fb0"},{"skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"54134afa61fd"},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at samlivsbruddet har vart i seks månader.","_key":"aedb6a02dc5c"}]}],"_createdAt":"2021-10-25T06:55:43Z","navnISystem":"Faktisk separasjon"},{"hjemler":["9"],"_id":"c51fe359-b10d-4a8a-b9e6-6df652dec30f","_rev":"h26rAhFEYSUtDGXJE0t7NE","_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi vi har kome fram til at du og den tidligare ektefellen din ikkje har flytta frå kvarandre.","_key":"75ff4cd57a690"}],"_type":"block","style":"normal","_key":"4894ca47d075","markDefs":[]}],"_createdAt":"2022-04-05T15:18:07Z","visningsnavn":"64. Vurdering ikke flyttet fra tidligere ektefelle","periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:31:02Z","navnISystem":"Vurdering ikke flyttet fra tidligere ektefelle","apiNavn":"avslagVurderingIkkeFlyttetFraTidligereEktefelle","vedtakResultat":"IKKE_INNVILGET","tema":"NASJONAL","mappe":["AVSLAG"],"bokmaal":[{"_key":"656c8dcd4c7e","markDefs":[],"children":[{"marks":[],"text":"Utvidet barnetrygd fordi vi har kommet fram til at du og den tidligere ektefellen din ikke har flyttet fra hverandre.","_key":"38076c771e490","_type":"span"}],"_type":"block","style":"normal"}]},{"navnISystem":"Barn bor ikke med søker\t","apiNavn":"avslagBorHosSoker","behandlingstema":"NASJONAL","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","nynorsk":[{"_type":"block","style":"normal","_key":"53de5c7c5104","markDefs":[],"children":[{"_key":"edfd2a528499","_type":"span","marks":[],"text":"Barnetrygd for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"5044f1971a67"},{"_type":"span","marks":[],"text":" fordi ","_key":"dc7bb90b0c30"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"7a9bde07e364"},{"_type":"span","marks":[],"text":" ikkje bur hos deg","_key":"f25c0db5b105"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"8b6eaee142a6"},{"marks":[],"text":".","_key":"115a1093603e","_type":"span"}]}],"_rev":"FuD004taptHFqBZyEy7911","tema":"NASJONAL","hjemler":["2","4"],"begrunnelsetype":"AVSLAG","_updatedAt":"2023-09-25T10:25:49Z","visningsnavn":"3. Barn bor ikke med søker","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"_createdAt":"2021-08-30T12:56:06Z","valgbarhet":"STANDARD","_id":"c5236a6e-fc43-40b0-bd49-48b5fa8c8879","mappe":["AVSLAG"],"bokmaal":[{"_key":"6240f08af33d","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"5982e762df8d","_type":"span"},{"_key":"0648ba048e9c","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi ","_key":"1cac597f1811"},{"_type":"valgReferanse","_key":"b9c7be521e2e","_ref":"df8dc282-2637-4047-a656-8527205dc364"},{"_type":"span","marks":[],"text":" ikke bor hos deg","_key":"5fb8ba9dd8ae"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"5c72905b921d"},{"marks":[],"text":".","_key":"0807d4aaeeec","_type":"span"}],"_type":"block","style":"normal"}]},{"nynorsk":[{"style":"normal","_key":"799f5ffe6112","markDefs":[],"children":[{"_key":"d3ddf1e3ad1a","_type":"span","marks":[],"text":"Barnetrygd for barn født "},{"_key":"6070c12368e6","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikkje er frivillig medlem i folketrygda under opphaldet i utlandet","_key":"7f52f97c1809"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"097d9b094c49"},{"_key":"8fcf56000271","_type":"span","marks":[],"text":". Bur begge foreldre saman under opphaldet i utlandet, må begge vere medlem i folketrygda for å få barnetrygd."}],"_type":"block"}],"_type":"begrunnelse","periodeType":"INGEN_UTBETALING","tema":"NASJONAL","bosattIRiketTriggere":["MEDLEMSKAP"],"valgbarhet":"STANDARD","mappe":["AVSLAG"],"hjemler":["4","5"],"rolle":["SOKER"],"begrunnelsetype":"AVSLAG","_createdAt":"2021-09-29T10:48:49Z","vilkaar":["BOSATT_I_RIKET"],"_updatedAt":"2023-09-25T10:26:55Z","bokmaal":[{"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"e2b1563f80ce","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"305e0199d7a8"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikke er frivillig medlem i folketrygden under oppholdet i utlandet","_key":"bb00461e216c"},{"_type":"valgReferanse","_key":"f6e429e9319b","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"_type":"span","marks":[],"text":". Bor begge foreldrene sammen under oppholdet i utlandet, må dere begge være medlem i folketrygden for å få barnetrygd.","_key":"c6c6bebc42f1"}],"_type":"block","style":"normal","_key":"04884f449875","markDefs":[]}],"navnISystem":"Annen forelder ikke frivillig medlem","apiNavn":"avslagAnnenForelderIkkeFrivilligMedlem","visningsnavn":"21. Annen forelder ikke frivillig medlem","vedtakResultat":"IKKE_INNVILGET","behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr5Cro","hjemlerFolketrygdloven":["2-8"],"_id":"c57afa72-515c-4b0f-853f-d425a3615b44"},{"hjemler":["4","5"],"_type":"begrunnelse","hjemlerFolketrygdloven":["2-5","2-8"],"vilkaar":["BOSATT_I_RIKET"],"nynorsk":[{"markDefs":[],"children":[{"text":"Barnetrygd for barn fødd ","_key":"a69bd2d4b198","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"1f28224a2d4f"},{"_type":"span","marks":[],"text":" fordi ","_key":"caa600a46810"},{"_key":"636971517121","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" ikkje var medlem i folketrygda under opphaldet i utlandet.","_key":"f76eecf6b04a"}],"_type":"block","style":"normal","_key":"7f73b9983f91"}],"_createdAt":"2021-11-05T11:28:39Z","navnISystem":"Var ikke medlem","tema":"NASJONAL","_id":"c684b69e-0860-4f94-84e7-e85e9356295c","mappe":["OPPHØR"],"apiNavn":"opphorVarIkkeMedlem","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"INGEN_UTBETALING","rolle":["SOKER","BARN"],"ovrigeTriggere":["GJELDER_FØRSTE_PERIODE"],"visningsnavn":"39. Var ikke medlem","_rev":"FuD004taptHFqBZyEy8cBG","begrunnelsetype":"OPPHØR","bosattIRiketTriggere":["MEDLEMSKAP"],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:36:05Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"2a26d8c84557"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"05f75bb08676"},{"_key":"61fa2c32597a","_type":"span","marks":[],"text":" fordi "},{"_type":"valgfeltV2","_key":"8653336ddc8d","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"}},{"_type":"span","marks":[],"text":" ikke var medlem i folketrygden under oppholdet i utlandet.","_key":"6671b921c517"}],"_type":"block","style":"normal","_key":"2735d9ae7f35","markDefs":[]}]},{"nynorsk":[{"style":"normal","_key":"db51e876f061","markDefs":[],"children":[{"_key":"958888abc8e0","_type":"span","marks":[],"text":"Utvida barnetrygd fordi du er gift."}],"_type":"block"}],"_createdAt":"2021-10-22T08:17:29Z","mappe":["AVSLAG"],"hjemler":["9"],"visningsnavn":"36. Gift","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","apiNavn":"avslagGift","vilkaar":["UTVIDET_BARNETRYGD"],"_id":"c6b4f7dc-5740-4365-b6d5-ad6ae6814c6f","_rev":"BtltdVb0HP4g4WJfnr5IVJ","_updatedAt":"2023-09-25T10:29:28Z","valgbarhet":"STANDARD","bokmaal":[{"style":"normal","_key":"4ed27468aa1c","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi du er gift.","_key":"bf0c6f8310db"}],"_type":"block"}],"navnISystem":"Gift","periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","tema":"FELLES"},{"mappe":["FORTSATT_INNVILGET"],"navnISystem":"Forvaring gift","_id":"c6f1dffa-a496-41dc-bb14-6a8e27dcfbaa","_createdAt":"2021-10-25T10:14:17Z","valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utvidet barnetrygd fordi ektefellen din fortsatt er i forvaring.","_key":"6ccd2b49bdfe"}],"_type":"block","style":"normal","_key":"8c0872289e8b"}],"apiNavn":"fortsattInnvilgetForvaringGift","begrunnelsetype":"FORTSATT_INNVILGET","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"FORTSATT_INNVILGET","_updatedAt":"2023-09-25T10:23:01Z","visningsnavn":"25. Forvaring gift","_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","tema":"FELLES","nynorsk":[{"style":"normal","_key":"e76c5949e2a5","markDefs":[],"children":[{"_key":"34e585b67c7f","_type":"span","marks":[],"text":"Du får utvida barnetrygd fordi ektefellen din fortsatt er i forvaring."}],"_type":"block"}],"hjemler":["9"],"_rev":"h26rAhFEYSUtDGXJE0smYk"},{"apiNavn":"fortsattInnvilgetUendretTrygd","visningsnavn":"8. Har barnetrygden det er søkt om","nynorsk":[{"style":"normal","_key":"317c3d592d2c","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får uendra barnetrygd fordi du allereie mottek barnetrygda som du har søkt om.","_key":"a01e63e1051c"}],"_type":"block"}],"_updatedAt":"2023-09-25T10:21:59Z","hjemler":["2","4","11"],"_rev":"BtltdVb0HP4g4WJfnr4rNo","_type":"begrunnelse","valgbarhet":"STANDARD","navnISystem":"Har barnetrygden det er søkt om","begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","_createdAt":"2021-08-30T13:08:44Z","_id":"c76346c7-4bcf-4bc8-bba1-c8f42bb440b7","bokmaal":[{"_key":"6773a3d61f0b","markDefs":[],"children":[{"_key":"5be34a82ddb3","_type":"span","marks":[],"text":"Du får uendret barnetrygd fordi du allerede mottar barnetrygden som du har søkt om."}],"_type":"block","style":"normal"}],"vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET","mappe":["FORTSATT_INNVILGET"]},{"navnISystem":"Samboer uten felles barn","hjemler":["2","9","11"],"_rev":"h26rAhFEYSUtDGXJE0sV8b","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","_createdAt":"2021-10-25T07:01:40Z","_id":"c81f1cd2-efa9-4337-bc2a-2937bbcdc97b","apiNavn":"innvilgetSamboerUtenFellesBarn","periodeType":"UTBETALING","visningsnavn":"40. Samboer uten felles barn","behandlingstema":"NASJONAL","vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"INNVILGET","bokmaal":[{"markDefs":[],"children":[{"_key":"971c9171a4a1","_type":"span","marks":[],"text":""},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"6f37d1602116","skalHaStorForbokstav":false},{"_key":"c69b29511eba","_type":"span","marks":[],"text":" utvidet barnetrygd fordi du bor alene med "},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"653f4014496d"},{"_key":"d0d29b2c2513","_type":"span","marks":[],"text":". Du og den tidligere samboeren din flyttet fra hverandre "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"9da9285e5f24"},{"_type":"span","marks":[],"text":". ","_key":"2ef2efc5766c"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"11bf49b803f7","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at dere flyttet fra hverandre.","_key":"1220204c0632"}],"_type":"block","style":"normal","_key":"65639ff2072f"}],"tema":"FELLES","nynorsk":[{"children":[{"_key":"feddc464d373","_type":"span","marks":[],"text":""},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"c45273b9abcd"},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi du bur aleine med ","_key":"268ddba5065d"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"72997e3817ef","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Du og den tidligare sambuaren din flytta frå kvarandre ","_key":"eccaa3143878"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"2908338eb603"},{"text":". ","_key":"9c7a65e037ef","_type":"span","marks":[]},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"69e604b8d30b","skalHaStorForbokstav":true},{"marks":[],"text":" utvida barnetrygd frå månaden etter at de flytta frå kvarandre.","_key":"f9ab159a3b91","_type":"span"}],"_type":"block","style":"normal","_key":"1efce9a9e18a","markDefs":[]}],"valgbarhet":"STANDARD","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:50Z"},{"apiNavn":"avslagIkkeDokumentertSamboerDod","visningsnavn":"28a. Ikke dokumentert samboer død","vedtakResultat":"IKKE_INNVILGET","vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"_key":"6a966a760dd4","markDefs":[],"children":[{"marks":[],"text":"Utvida barnetrygd fordi vi har kome fram til at du ikkje har dokumentert at sambuaren din er død.","_key":"e33d6bdf39a8","_type":"span"}],"_type":"block","style":"normal"}],"_createdAt":"2021-10-28T14:21:18Z","valgbarhet":"STANDARD","navnISystem":"Ikke dokumentert samboer død","_type":"begrunnelse","begrunnelsetype":"AVSLAG","tema":"NASJONAL","_id":"c8353715-c6fc-45ba-8c53-1cfec2463359","hjemler":["9"],"periodeType":"INGEN_UTBETALING","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:27:24Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi vi har kommet fram til at du ikke har dokumentert at samboeren din er død.","_key":"c7d46c3ffd13"}],"_type":"block","style":"normal","_key":"46b0a37812c0"}],"_rev":"FuD004taptHFqBZyEy7LEV"},{"apiNavn":"opphorVurderingSokerOgBarnIkkeMedlem","hjemler":["4","5"],"_rev":"FuD004taptHFqBZyEy8Crj","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"OPPHØR","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"ab53e397e1cb"},{"_key":"87f10f8f6059","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_key":"441b8fcd8311","_type":"span","marks":[],"text":" fordi vi har kome fram til at "},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"a9f0daadfb63"},{"_type":"span","marks":[],"text":" ikkje fyller vilkåra for å vere medlem i folketrygda under opphaldet i utlandet ","_key":"f323a7c77e5f"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"6180978e2c4f"},{"marks":[],"text":".","_key":"210f46135272","_type":"span"}],"_type":"block","style":"normal","_key":"fabfeea031e6","markDefs":[]}],"_createdAt":"2021-09-24T11:44:36Z","_id":"c8cd35cf-3854-48fa-9171-c8ee53777cad","_updatedAt":"2023-09-25T10:34:40Z","visningsnavn":"21. Vurdering ikke medlem","rolle":["SOKER","BARN"],"valgbarhet":"STANDARD","mappe":["OPPHØR"],"hjemlerFolketrygdloven":["2-5","2-8"],"vilkaar":["BOSATT_I_RIKET"],"bokmaal":[{"_type":"block","style":"normal","_key":"3c7e9a8f2602","markDefs":[],"children":[{"_key":"dfc0bfab7c9f","_type":"span","marks":[],"text":"Barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"15e2b403cc44"},{"_key":"85ca9ebe326c","_type":"span","marks":[],"text":" fordi vi har kommet fram til at "},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"526c66ca6465"},{"marks":[],"text":" ikke fyller vilkårene for å være medlem i folketrygden under oppholdet i utlandet ","_key":"00112228886b","_type":"span"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"582f2c2b03e4"},{"_type":"span","marks":[],"text":".","_key":"4e0501c803fc"}]}],"navnISystem":"Vurdering ikke medlem","behandlingstema":"NASJONAL","periodeType":"INGEN_UTBETALING","tema":"NASJONAL","bosattIRiketTriggere":["MEDLEMSKAP"]},{"visningsnavn":"6. Skjønnsmessig vurdering opphold tredjelandsborger","vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"AVSLAG","tema":"NASJONAL","nynorsk":[{"style":"normal","_key":"165038040c3e","markDefs":[],"children":[{"_key":"c0d964b5ac38","_type":"span","marks":[],"text":"Barnetrygd fordi vi har komme fram til at "},{"_type":"valgReferanse","_key":"ac1b50243503","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},{"text":" ikkje har opphaldsrett i Noreg","_key":"4c49d5ba4e1f","_type":"span","marks":[]},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"4e6c5101366e"},{"_type":"span","marks":[],"text":".","_key":"aa1afaa85cf0"}],"_type":"block"}],"mappe":["AVSLAG"],"_createdAt":"2021-08-30T13:10:59Z","valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd fordi vi har kommet fram til at ","_key":"5596c05adbe2"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"9fed4f8958b7"},{"_key":"4c2b19032ffc","_type":"span","marks":[],"text":" ikke har oppholdsrett i Norge"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"b964e3586d7d"},{"text":".","_key":"7648d156f9eb","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"4143e465c7a1"}],"navnISystem":"Skjønnsmessig vurdering opphold tredjelandsborger","apiNavn":"avslagLovligOppholdSkjonnsmessigVurderingTredjelandsborger","hjemler":["2","4"],"_rev":"FuD004taptHFqBZyEy7AcW","vilkaar":["LOVLIG_OPPHOLD"],"lovligOppholdTriggere":["VURDERING_ANNET_GRUNNLAG"],"_updatedAt":"2023-09-25T10:26:01Z","behandlingstema":"NASJONAL","_type":"begrunnelse","periodeType":"INGEN_UTBETALING","rolle":["SOKER","BARN"],"_id":"ca002d95-3952-4abf-af6c-732e235d0aa2"},{"rolle":["BARN"],"begrunnelsetype":"FORTSATT_INNVILGET","_updatedAt":"2023-09-25T10:21:17Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi ","_key":"363eda90e997"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"a6114abdefb0"},{"_type":"span","marks":[],"text":" fortsatt er bosatt i Norge.","_key":"f0f734a8d420"}],"_type":"block","style":"normal","_key":"6830176096f2","markDefs":[]}],"apiNavn":"fortsattInnvilgetBarnBosattIRiket","_rev":"h26rAhFEYSUtDGXJE0shhy","_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","tema":"NASJONAL","_createdAt":"2021-08-30T12:55:04Z","_id":"cb284bd7-c8ca-48b3-bfde-62cb27a56456","mappe":["FORTSATT_INNVILGET"],"navnISystem":"Barn oppholder seg i Norge","hjemler":["2","4","11"],"periodeType":"FORTSATT_INNVILGET","valgbarhet":"STANDARD","visningsnavn":"1C. Barn oppholder seg i Norge","vilkaar":["BOSATT_I_RIKET"],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi ","_key":"8ed75db11cab"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"a0287566a521"},{"_type":"span","marks":[],"text":" fortsatt er busett i Noreg.","_key":"584da1033934"}],"_type":"block","style":"normal","_key":"482bb580d003","markDefs":[]}]},{"tema":"NASJONAL","_createdAt":"2022-10-06T12:22:33Z","mappe":["REDUKSJON"],"visningsnavn":"69. Søker er gift","vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"REDUKSJON","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du er gift.","_key":"1a78a2dfb2b70"}],"_type":"block","style":"normal","_key":"3959a66bb76c"}],"valgbarhet":"STANDARD","apiNavn":"reduksjonSoekerErGift","behandlingstema":"NASJONAL","_type":"begrunnelse","_id":"cbea4d17-b2c5-483b-974b-795ea0d2603c","navnISystem":"Søker er gift","hjemler":["9"],"vedtakResultat":"REDUKSJON","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygden endres fordi du er gift.","_key":"9bbd504997dd0","_type":"span"}],"_type":"block","style":"normal","_key":"c29ba6620a89"}],"ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"],"_rev":"h26rAhFEYSUtDGXJE0sV8b","periodeType":"UTBETALING","_updatedAt":"2023-09-25T10:15:50Z"},{"begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","tema":"FELLES","nynorsk":[{"_type":"block","style":"normal","_key":"af211c2b5038","markDefs":[],"children":[{"text":".","_key":"dcdcd67a6919","_type":"span","marks":[]}]}],"endringsaarsaker":["ENDRE_MOTTAKER"],"_createdAt":"2022-03-04T13:54:23Z","valgbarhet":"STANDARD","apiNavn":"endretUtbetalingOpphorEndreMottaker","vedtakResultat":"IKKE_INNVILGET","_id":"cc0c2d0a-36b6-4c45-bd88-eb36bd27775c","_updatedAt":"2023-09-25T10:47:21Z","_rev":"BtltdVb0HP4g4WJfnr6ETJ","periodeType":"INGEN_UTBETALING","_type":"begrunnelse","mappe":["ENDRET_UTBETALINGSPERIODE"],"navnISystem":"Foreldrene bor sammen, endret mottaker","visningsnavn":"7. Endre mottaker - opphør","hjemler":["2","12"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"81387af73828"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"77fda5d0c78d"},{"_type":"span","marks":[],"text":" fordi den andre forelderen har søkt om barnetrygd for ","_key":"563014c6de10"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"838e06ea9a1f"},{"_type":"span","marks":[],"text":".","_key":"702ca117467a"}],"_type":"block","style":"normal","_key":"1e2055dd55d6","markDefs":[]}]},{"vedtakResultat":"INNVILGET_ELLER_ØKNING","_createdAt":"2022-04-07T08:02:46Z","mappe":["ENDRET_UTBETALINGSPERIODE"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du har skriftlig avtale om delt bosted for barn født ","_key":"2705a0d74c790"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"1e4f9c207ae8"},{"_type":"span","marks":[],"text":". Avtalen gjelder fra ","_key":"592c59198aea"},{"flettefelt":"avtaletidspunktDeltBosted","_type":"flettefelt","_key":"821dcda87c21"},{"text":".","_key":"335dad486144","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"d919ed51f5c6"}],"apiNavn":"endretUtbetalingDeltBostedEndretUtbetaling","hjemler":["2","11"],"valgbarhet":"STANDARD","_id":"cc1ac1df-32c0-4736-8139-596f82a70857","tema":"FELLES","endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"UTBETALING_IKKE_RELEVANT","nynorsk":[{"children":[{"_key":"7e5c4a107c660","_type":"span","marks":[],"text":"Du har skriftleg avtale om delt bosted for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"7c042df6fbcb"},{"_type":"span","marks":[],"text":". Avtalen gjeld frå ","_key":"041b071122a9"},{"flettefelt":"avtaletidspunktDeltBosted","_type":"flettefelt","_key":"6dbe12a8487a"},{"_type":"span","marks":[],"text":".","_key":"0d80740a079c"}],"_type":"block","style":"normal","_key":"fc24db2dd103","markDefs":[]}],"_rev":"h26rAhFEYSUtDGXJE0sV8b","periodeType":"UTBETALING","_type":"begrunnelse","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","endringsaarsaker":["DELT_BOSTED"],"_updatedAt":"2023-09-25T10:15:50Z","navnISystem":"Delt bosted - endret utbetaling","visningsnavn":"12. Delt bosted - endret utbetaling"},{"begrunnelsetype":"OPPHØR","nynorsk":[{"style":"normal","_key":"a147a603f670","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"0ca07d043f9d"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"d9188612f404"},{"_type":"span","marks":[],"text":" fordi retten har bestemt at ","_key":"4bc48226da57"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"77dbbfd79eba","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" skal bu fast hos den andre forelderen. ","_key":"cd25573109f2"}],"_type":"block"}],"mappe":["OPPHØR"],"_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","navnISystem":"Rettsavgjørelse fast bosted","_updatedAt":"2023-09-25T10:35:21Z","tema":"NASJONAL","vilkaar":["BOR_MED_SOKER"],"periodeType":"INGEN_UTBETALING","visningsnavn":"27. Rettsavgjørelse fast bosted","_rev":"FuD004taptHFqBZyEy8X3V","borMedSokerTriggere":["DELT_BOSTED"],"_createdAt":"2021-10-26T08:54:18Z","valgbarhet":"STANDARD","_id":"cca33793-5afd-4f9d-8604-4314c5612638","apiNavn":"opphorRettsavgjorelseFastBosted","hjemler":["2","11"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn født ","_key":"dafdac313989"},{"_type":"flettefelt","_key":"2f489efd0991","flettefelt":"barnasFodselsdatoer"},{"marks":[],"text":" fordi retten har bestemt at ","_key":"f23d81e4298c","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"886809f9083b","skalHaStorForbokstav":false},{"marks":[],"text":" skal bo fast hos den andre forelderen. ","_key":"b0558181cc1f","_type":"span"}],"_type":"block","style":"normal","_key":"2a87c58413fc"}]},{"visningsnavn":"49. Samboer forsvunnet mindre enn 6 måneder","_rev":"BtltdVb0HP4g4WJfnr5LBo","tema":"FELLES","_updatedAt":"2023-09-25T10:30:17Z","navnISystem":"Samboer forsvunnet mindre enn 6 måneder","vedtakResultat":"IKKE_INNVILGET","mappe":["AVSLAG"],"valgbarhet":"STANDARD","_id":"cea821be-9df6-479f-b3c4-b3bde8607591","apiNavn":"avslagSamboerForsvunnetMindreEnn6Maaneder","hjemler":["9"],"vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"AVSLAG","nynorsk":[{"children":[{"_key":"56238b092417","_type":"span","marks":[],"text":"Utvida barnetrygd fordi det ikkje er dokumentert at sambuaren din har vore forsvunnen i minst seks månader."}],"_type":"block","style":"normal","_key":"0286a56a6c85","markDefs":[]}],"_type":"begrunnelse","periodeType":"INGEN_UTBETALING","_createdAt":"2021-10-22T09:21:28Z","bokmaal":[{"_type":"block","style":"normal","_key":"ce330233363a","markDefs":[],"children":[{"text":"Utvidet barnetrygd fordi det ikke er dokumentert at samboeren din har vært forsvunnet i minst seks måneder.","_key":"26916bf93b74","_type":"span","marks":[]}]}]},{"_id":"d07ec45b-8711-4cd8-a7b0-09e93d67be9b","mappe":["AVSLAG"],"visningsnavn":"47. Vurdering ikke tvungent psykisk helsevern samboer","vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"AVSLAG","tema":"FELLES","_createdAt":"2021-10-22T09:13:08Z","_rev":"BtltdVb0HP4g4WJfnr5Kzo","periodeType":"INGEN_UTBETALING","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi vi har kommet fram til at samboeren din ikke er under tvungent psykisk helsevern.","_key":"304480069baf"}],"_type":"block","style":"normal","_key":"16a4806114d3"}],"hjemler":["9"],"vedtakResultat":"IKKE_INNVILGET","valgbarhet":"STANDARD","navnISystem":"Vurdering ikke tvungent psykisk helsevern samboer","apiNavn":"avslagVurderingIkkeTvungentPsykiskHelsevernSamboer","_type":"begrunnelse","nynorsk":[{"children":[{"marks":[],"text":"Utvida barnetrygd fordi vi har kome fram til at sambuaren din ikkje er under tvungent psykisk helsevern.","_key":"e5988d32e095","_type":"span"}],"_type":"block","style":"normal","_key":"4100f715c938","markDefs":[]}],"_updatedAt":"2023-09-25T10:30:10Z"},{"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi vi har kome fram til at du og sambuaren din ikkje har flytta frå kvarandre.","_key":"8fad19c059a0"}],"_type":"block","style":"normal","_key":"2c687e9462c3","markDefs":[]}],"_id":"d0ab5cf3-03b4-4f27-98e5-0632ff99b5cf","mappe":["AVSLAG"],"apiNavn":"avslagVurderingSamboerIkkeFlyttetFraHverandre","hjemler":["9"],"vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","_createdAt":"2021-10-22T09:00:59Z","_updatedAt":"2023-09-25T10:29:48Z","navnISystem":"Vurdering samboer ikke flyttet fra hverandre","visningsnavn":"41. Vurdering samboer ikke flyttet fra hverandre","_rev":"FuD004taptHFqBZyEy7Y7i","vedtakResultat":"IKKE_INNVILGET","_type":"begrunnelse","tema":"FELLES","valgbarhet":"STANDARD","bokmaal":[{"_key":"2f74808a1cdc","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi vi har kommet fram til at du og samboeren din ikke har flyttet fra hverandre.","_key":"3f4d89830ffb"}],"_type":"block","style":"normal"}]},{"_createdAt":"2021-11-05T10:13:39Z","apiNavn":"reduksjonIkkeBosattINorge","_type":"begrunnelse","vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","nynorsk":[{"_key":"7c554981f54d","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi ","_key":"d40256d5444c"},{"_key":"fead99210c17","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" ikkje var busett i Noreg.","_key":"8514b65dcc09"}],"_type":"block","style":"normal"}],"_updatedAt":"2023-09-25T10:15:52Z","navnISystem":"Ikke bosatt i Norge","visningsnavn":"45. Ikke bosatt i Norge","_rev":"BtltdVb0HP4g4WJfnr4YPo","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"tema":"NASJONAL","ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi ","_key":"d5d30b0d3e3c"},{"_type":"valgfeltV2","_key":"6f310d857a7e","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"}},{"text":" ikke var bosatt i Norge.","_key":"a8757896a16a","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"c66fea15ab34"}],"hjemler":["2","4"],"vilkaar":["BOSATT_I_RIKET"],"rolle":["SOKER","BARN"],"begrunnelsetype":"REDUKSJON","_id":"d1fcba9f-5617-4a86-a1d0-8f75db5d9fb4"},{"apiNavn":"innvilgetFullUtbetalingAnnenForelderOnskerIkkeDeltBarnetrygd","hjemler":["2","11","12"],"periodeType":"UTBETALING","_rev":"BtltdVb0HP4g4WJfnr4YPo","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"valgbarhet":"STANDARD","_id":"d25d4107-d818-42ce-a17a-e10a9096fa7d","_updatedAt":"2023-09-25T10:15:52Z","behandlingstema":"NASJONAL","tema":"NASJONAL","_createdAt":"2022-10-28T13:13:29Z","borMedSokerTriggere":["DELT_BOSTED_SKAL_IKKE_DELES"],"begrunnelsetype":"INNVILGET","nynorsk":[{"markDefs":[],"children":[{"_key":"101afaccf0b90","_type":"span","marks":[],"text":"Du får heile barnetrygda for barn fødd "},{"_type":"flettefelt","_key":"a58bfcb39e67","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikkje ønsker å få utbetalt delt barnetrygd. Du får heile barnetrygd frå same tidspunkt som barnetrygda til den andre forelderen opphøyrer.","_key":"6051285e7f3c"}],"_type":"block","style":"normal","_key":"4110e458e989"}],"mappe":["INNVILGET"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får hele barnetrygden for barn født ","_key":"aee7019c42840"},{"_key":"d4891e952c7c","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikke ønsker å få utbetalt delt barnetrygd. Du får hele barnetrygd fra samme tidspunkt som barnetrygden til den andre forelderen opphører.","_key":"5e67f67f5aa5"}],"_type":"block","style":"normal","_key":"e2090dd2f8af"}],"navnISystem":"Full utbetaling annen forelder ønsker ikke delt barnetrygd","visningsnavn":"93. Full utbetaling annen forelder ønsker ikke delt barnetrygd","vedtakResultat":"INNVILGET_ELLER_ØKNING"},{"_rev":"h26rAhFEYSUtDGXJE0t4ke","nynorsk":[{"_key":"3edd0dae95ac","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi vi har kome fram til at du ikkje bur aleine i eigen leilighet.","_key":"59b8f500fbc1"}],"_type":"block","style":"normal"}],"valgbarhet":"STANDARD","vedtakResultat":"IKKE_INNVILGET","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"INGEN_UTBETALING","begrunnelsetype":"AVSLAG","_createdAt":"2021-10-22T09:23:23Z","_id":"d3a1678e-a75e-4748-a516-551f1882cd80","hjemler":["9"],"visningsnavn":"50. Enslig mindreårig flyktning","_type":"begrunnelse","tema":"FELLES","navnISystem":"Enslig mindreårig flyktning","apiNavn":"avslagEnsligMindreaarigFlyktning","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:30:21Z","bokmaal":[{"_type":"block","style":"normal","_key":"2328d21acfd2","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi vi har kommet fram til at du ikke bor alene i egen leilighet.","_key":"d46e02cf3e9a"}]}]},{"hjemler":["2","4"],"vilkaar":["LOVLIG_OPPHOLD"],"rolle":["SOKER","BARN"],"_createdAt":"2022-11-04T09:09:22Z","_id":"d400b9c4-b9bf-4b0e-9fe3-a90af952be00","mappe":["INSTITUSJON","FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:38:47Z","fagsakType":"INSTITUSJON","_rev":"BtltdVb0HP4g4WJfnr5qgo","nynorsk":[{"style":"normal","_key":"3191da3c0354","markDefs":[],"children":[{"_type":"span","marks":[],"text":"De får fortsatt barnetrygd fordi barnet har varig opphaldsløyve.","_key":"5eb407f7c6460"}],"_type":"block"}],"apiNavn":"fortsattInnvilgetVarigOppholdstillatelseInstitusjon","valgbarhet":"SAKSPESIFIKK","visningsnavn":"3. Varig oppholdstillatelse institusjon","behandlingstema":"NASJONAL_INSTITUSJON","_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET","begrunnelsetype":"FORTSATT_INNVILGET","tema":"FELLES","bokmaal":[{"_type":"block","style":"normal","_key":"96e440261472","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Dere får fortsatt barnetrygd fordi barnet har varig oppholdstillatelse.","_key":"271a21c8d6900"}]}],"navnISystem":"Varig oppholdstillatelse institusjon"},{"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får fortsatt utbetalt barnetrygd fordi du no har registrert gyldig kontonummer. Barnetrygda vert utbetalt frå same månad som den var stansa frå.","_key":"41c73eb2a1f70"}],"_type":"block","style":"normal","_key":"672fe3082706"}],"_createdAt":"2022-10-13T11:03:48Z","valgbarhet":"STANDARD","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:52Z","rolle":["SOKER"],"visningsnavn":"91. Gyldig kontonummer registrert","behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr4YPo","_type":"begrunnelse","begrunnelsetype":"INNVILGET","_id":"d44e120e-d51f-4007-b910-2c391719a30f","bokmaal":[{"_key":"5f5eb7e75450","markDefs":[],"children":[{"marks":[],"text":"Du får fortsatt utbetalt barnetrygd fordi du nå har registrert gyldig kontonummer. Barnetrygden utbetales fra samme måned som den var stanset fra.","_key":"d630b9581dc40","_type":"span"}],"_type":"block","style":"normal"}],"navnISystem":"Gyldig kontonummer registrert","vilkaar":["BOSATT_I_RIKET"],"tema":"NASJONAL","apiNavn":"innvilgetGyldigKontonummerRegistrert","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","hjemler":["2","4","11"]},{"navnISystem":"Autovedtak barn 18 år","visningsnavn":"10a. Autovedtak barn 18 år","tema":"NASJONAL","_updatedAt":"2023-09-25T10:15:52Z","hjemler":["2","11"],"_rev":"BtltdVb0HP4g4WJfnr4YPo","_type":"begrunnelse","periodeType":"UTBETALING","_createdAt":"2021-11-03T12:48:24Z","_id":"d4d0fc53-7676-468a-b661-4796486ef90c","apiNavn":"reduksjonAutovedtakBarn18Aar","begrunnelsetype":"REDUKSJON","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden reduseres fordi barn født ","_key":"a23017c22db4"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"6a2110fa230f"},{"_type":"span","marks":[],"text":" fyller 18 år.","_key":"e621b289c9f6"}],"_type":"block","style":"normal","_key":"818e6e07e798"}],"vedtakResultat":"REDUKSJON","vilkaar":["UNDER_18_ÅR"],"nynorsk":[{"_key":"6e7d9d91d678","markDefs":[],"children":[{"marks":[],"text":"Barnetrygda vert redusert fordi barn født ","_key":"0b49c30c595b","_type":"span"},{"_key":"3b3a7780955b","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_key":"74d8ad9accb0","_type":"span","marks":[],"text":" fyller 18 år."}],"_type":"block","style":"normal"}],"valgbarhet":"AUTOMATISK","mappe":["REDUKSJON"],"ovrigeTriggere":["ALLTID_AUTOMATISK"]},{"hjemler":["2"],"vilkaar":["BOR_MED_SOKER"],"begrunnelsetype":"AVSLAG","_createdAt":"2021-10-26T10:13:20Z","valgbarhet":"STANDARD","visningsnavn":"60. Ikke delt foreldrene bor sammen","borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"INGEN_UTBETALING","tema":"NASJONAL","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"52a1d3c42b6b"},{"_key":"0f898b640521","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"text":" fordi du bur saman med den andre forelderen til ","_key":"1833e890dcda","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"3fcd5352f8f5","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Derfor kan ikkje barnetrygda delast.","_key":"ea5637c1c819"}],"_type":"block","style":"normal","_key":"8c4a9c0dc03e"}],"_updatedAt":"2023-09-25T10:30:46Z","bokmaal":[{"_type":"block","style":"normal","_key":"e68db58567a9","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn født ","_key":"7e9f51bd0d94"},{"_key":"d1cc07c9e0f3","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi du bor sammen med den andre forelderen til ","_key":"9de6b31ead37"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"8e52546b9edc","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Derfor kan ikke barnetrygden deles.","_key":"8d47be4d3b0d"}]}],"navnISystem":"Ikke delt foreldrene bor sammen","apiNavn":"avslagIkkeDeltForeldreneBorSammen","mappe":["AVSLAG"],"_rev":"BtltdVb0HP4g4WJfnr5Mao","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","_id":"d591377e-f719-4788-b6b7-c1e14c379529"},{"vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","rolle":["SOKER","BARN"],"begrunnelsetype":"AVSLAG","navnISystem":"Ikke medlem etter trygdeavtale","visningsnavn":"19. Ikke medlem etter trygdeavtale","behandlingstema":"NASJONAL","_type":"begrunnelse","nynorsk":[{"_key":"7d25938d19d9","markDefs":[],"children":[{"text":"Barnetrygd for barn fødd ","_key":"8f51898305ac","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"ab54700d9178"},{"_type":"span","marks":[],"text":" fordi ","_key":"6c84fe63bcf6"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"f69f8818f891"},{"marks":[],"text":"ikkje er medlem i folketrygda etter trygdeavtale","_key":"978911f454b1","_type":"span"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"0c99b7e4e623"},{"text":".","_key":"397cbd840ed2","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"_createdAt":"2021-09-29T10:59:40Z","_updatedAt":"2023-09-25T10:26:49Z","bokmaal":[{"_key":"8842b9f48b77","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"4ab7796cac95"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"1c09fde8871b"},{"_type":"span","marks":[],"text":" fordi ","_key":"4218bb3c0c2f"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"7f15bca78e67"},{"_type":"span","marks":[],"text":" ikke er medlem i folketrygden etter trygdeavtale","_key":"b26d2c72a5d1"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"f639140de75f"},{"marks":[],"text":". ","_key":"631bcf28dbd0","_type":"span"}],"_type":"block","style":"normal"}],"hjemler":["2","4","22"],"vilkaar":["BOSATT_I_RIKET"],"bosattIRiketTriggere":["MEDLEMSKAP"],"valgbarhet":"STANDARD","tema":"NASJONAL","_id":"d69ea900-6023-4a9d-ae53-4abc4c439886","apiNavn":"avslagIkkeMedlemEtterTrygdeavtale","_rev":"h26rAhFEYSUtDGXJE0sxmn","mappe":["AVSLAG"]},{"vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"INGEN_UTBETALING","_createdAt":"2021-10-26T08:53:58Z","_id":"d85360f1-17b7-41cf-96e9-363724401055","mappe":["OPPHØR"],"apiNavn":"opphorIkkeAvtaleOmDeltBosted","bokmaal":[{"style":"normal","_key":"cb2165657bf5","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn født ","_key":"501070cc65d2"},{"_type":"flettefelt","_key":"56386162d87a","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi du ikke har en avtale om delt bosted for ","_key":"d6732230e854"},{"_key":"ec8fc659baa9","_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse"},{"_type":"span","marks":[],"text":" ","_key":"95bf2628730e"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"f684ad756889"},{"text":". Barnetrygden kan derfor ikke deles.","_key":"d56886197879","_type":"span","marks":[]}],"_type":"block"}],"navnISystem":"Ikke avtale om delt bosted","_rev":"BtltdVb0HP4g4WJfnr5eso","begrunnelsetype":"OPPHØR","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"70f06d6f03fb"},{"_key":"f637512a7906","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi du ikkje har ein avtale om delt bustad for ","_key":"4d8777cb1613"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"13791b0fed89"},{"marks":[],"text":" ","_key":"a8b318fcce02","_type":"span"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"3379f2cedf29"},{"_key":"85fd0700099f","_type":"span","marks":[],"text":". Barnetrygda kan derfor ikkje delast."}],"_type":"block","style":"normal","_key":"789e93876d57","markDefs":[]}],"_updatedAt":"2023-09-25T10:35:16Z","hjemler":["2","11"],"_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","tema":"NASJONAL","valgbarhet":"STANDARD","visningsnavn":"26. Ikke avtale om delt bosted"},{"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi du har bedt om at barnetrygda for barn fødd ","_key":"02613c101d630"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"f429392de347"},{"marks":[],"text":" blir stansa.","_key":"f44528e96c10","_type":"span"}],"_type":"block","style":"normal","_key":"c2d3bbad69f4"}],"_createdAt":"2022-11-02T12:19:26Z","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:52Z","bokmaal":[{"_type":"block","style":"normal","_key":"8c072baa554b","markDefs":[],"children":[{"marks":[],"text":"Barnetrygden endres fordi du har bedt om at barnetrygden for barn født ","_key":"1a7f5e2010190","_type":"span"},{"_key":"18392ed13cf6","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"marks":[],"text":"] blir stanset.","_key":"e1f50c0baf5b","_type":"span"}]}],"apiNavn":"reduksjonDeltBostedSoekerBerOmOpphoer","behandlingstema":"NASJONAL","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["DELT_BOSTED"],"visningsnavn":"71. Delt bosted søker ber om opphør","_rev":"BtltdVb0HP4g4WJfnr4YPo","periodeType":"UTBETALING","begrunnelsetype":"REDUKSJON","_id":"d9bd1dbc-a7f0-4de2-bc16-55c3353e961d","navnISystem":"Delt bosted søker ber om opphør","hjemler":["2"],"vedtakResultat":"REDUKSJON","tema":"NASJONAL"},{"mappe":["REDUKSJON"],"hjemler":["2","9","11"],"nynorsk":[{"children":[{"marks":[],"text":"Barnetrygda er endra fordi vi har kome fram til at du har flytta saman med den andre forelderen frå ","_key":"53991f8082ed","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"55db9b15538f"},{"_type":"span","marks":[],"text":". Barnetrygda er endra frå månaden etter at de flytta saman.","_key":"dc4022d86255"}],"_type":"block","style":"normal","_key":"30f9e08440f1","markDefs":[]}],"begrunnelsetype":"REDUKSJON","_createdAt":"2021-10-22T12:08:04Z","valgbarhet":"STANDARD","bokmaal":[{"_key":"afbe9677846d","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi vi har kommet fram til at du har flyttet sammen med den andre forelderen fra ","_key":"b508673edd36"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"be989af93036"},{"_type":"span","marks":[],"text":". Barnetrygden endres fra måneden etter at dere flyttet sammen.","_key":"4806d74b45d0"}],"_type":"block","style":"normal"}],"navnISystem":"Vurdering flyttet sammen med annen forelder","visningsnavn":"27. Vurdering flyttet sammen med annen forelder","vilkaar":["UTVIDET_BARNETRYGD"],"_id":"dace815b-e419-4862-84aa-3d970d3568a9","_updatedAt":"2023-09-25T10:15:52Z","apiNavn":"reduksjonVurderingFlyttetSammenMedAnnenForelder","vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","tema":"FELLES","_rev":"BtltdVb0HP4g4WJfnr4YPo","_type":"begrunnelse"},{"visningsnavn":"63. Vurdering avtale delt bosted følges","borMedSokerTriggere":["DELT_BOSTED"],"begrunnelsetype":"INNVILGET","_id":"db3b06ad-ca95-4406-a5c1-9eda6fd09bf9","bokmaal":[{"markDefs":[],"children":[{"text":"Du får delt barnetrygd for barn født ","_key":"696320dc1fe4","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"1fb6ffdae560"},{"_type":"span","marks":[],"text":" fordi du har skriftlig avtale om delt bosted for ","_key":"3d6d08adde6b"},{"_type":"valgfeltV2","_key":"8a4754877371","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_key":"9f3bfc91725f","_type":"span","marks":[],"text":" fra "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"a6a431052c9d"},{"_type":"span","marks":[],"text":". Vi har kommet fram til at avtalen følges.","_key":"d0bb2875ce3e"}],"_type":"block","style":"normal","_key":"f652a35cd5e0"}],"hjemler":["2","11"],"vilkaar":["BOR_MED_SOKER"],"periodeType":"UTBETALING","tema":"NASJONAL","nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Du får delt barnetrygd for barn fødd ","_key":"cab461ad8092","_type":"span"},{"_key":"ee0cad0f283a","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"marks":[],"text":" fordi du har skriftleg avtale om delt bustad for ","_key":"743a3a9dca69","_type":"span"},{"_type":"valgfeltV2","_key":"e4eba3cc603e","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":" frå ","_key":"2c36d25efd11"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"bd4818a2a4db"},{"_type":"span","marks":[],"text":". Vi har kome fram til at avtalen vert følgd.","_key":"fec9c7aec964"}],"_type":"block","style":"normal","_key":"c322fea3acfb"}],"valgbarhet":"STANDARD","navnISystem":"Vurdering avtale delt bosted følges","mappe":["INNVILGET"],"behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr4YPo","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","_createdAt":"2021-10-26T09:56:09Z","_updatedAt":"2023-09-25T10:15:52Z","apiNavn":"innvilgetVurderingAvtaleDeltBostedFolges"},{"apiNavn":"opphorSokerOgBarnIkkeMedlem","_type":"begrunnelse","hjemlerFolketrygdloven":["2-5","2-8"],"vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOSATT_I_RIKET"],"periodeType":"INGEN_UTBETALING","_createdAt":"2021-09-24T11:14:15Z","_updatedAt":"2023-09-25T10:32:03Z","behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr5RqJ","begrunnelsetype":"OPPHØR","tema":"NASJONAL","_id":"dc5052de-dc40-4adb-9387-6a062caada1c","rolle":["SOKER","BARN"],"bosattIRiketTriggere":["MEDLEMSKAP"],"nynorsk":[{"style":"normal","_key":"b76015413878","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"fe4f948201ba"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"9c67e2289ba8"},{"_type":"span","marks":[],"text":" fordi ","_key":"9602f6698c8f"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"a0615ae16c63"},{"_type":"span","marks":[],"text":" ikkje er medlem i folketrygda under opphaldet i utlandet ","_key":"1aef1312d350"},{"_key":"0e921ac6fa15","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"text":".","_key":"d6f9df227665","_type":"span","marks":[]}],"_type":"block"}],"mappe":["OPPHØR"],"bokmaal":[{"_key":"5d9900bfde42","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"5bee7e1362a8"},{"_type":"flettefelt","_key":"d7c2efc16037","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi ","_key":"051d3d9e6540"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"f13d23d0f01c"},{"_type":"span","marks":[],"text":" ikke er medlem i folketrygden under oppholdet i utlandet ","_key":"4cec58e0d098"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"18b1d52d14a6"},{"_type":"span","marks":[],"text":".","_key":"3d7ec7d24338"}],"_type":"block","style":"normal"}],"navnISystem":"Ikke medlem utenlandsopphold","hjemler":["4","5"],"visningsnavn":"14. Ikke medlem utenlandsopphold","valgbarhet":"STANDARD"},{"vilkaar":["UTVIDET_BARNETRYGD"],"_updatedAt":"2023-09-25T10:22:52Z","_createdAt":"2021-10-25T10:11:43Z","valgbarhet":"STANDARD","_id":"dce59517-7969-4eac-9fdb-16c1d368bce4","hjemler":["9"],"visningsnavn":"23. Varetektsfengsel gift","_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","begrunnelsetype":"FORTSATT_INNVILGET","mappe":["FORTSATT_INNVILGET"],"bokmaal":[{"_key":"2177590f8b61","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utvidet barnetrygd fordi ektefellen din fortsatt er varetektsfengslet.","_key":"1bd96b97f564"}],"_type":"block","style":"normal"}],"navnISystem":"Varektektsfengsel gift","apiNavn":"fortsattInnvilgetVaretektsfengselGift","_rev":"h26rAhFEYSUtDGXJE0slx9","periodeType":"FORTSATT_INNVILGET","tema":"FELLES","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får utvida barnetrygd fordi ektefellen din fortsatt er varetektsfengsla.","_key":"d4d61c70e621"}],"_type":"block","style":"normal","_key":"d6418370c46c","markDefs":[]}]},{"_rev":"FuD004taptHFqBZyEy7cGU","vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"AVSLAG","valgbarhet":"STANDARD","_id":"dd00c188-5e3e-47dd-b10e-de6e40977c27","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:30:31Z","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"54f608ff7851"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"dbbe6461037f"},{"_type":"span","marks":[],"text":" fordi du ikkje har ein gyldig avtale om delt bustad for ","_key":"9ba724a3479d"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"5dd05e4f543f"},{"text":". Derfor kan ikkje barnetrygda delast.","_key":"b1808888c4a7","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"8d6be154f48b"}],"navnISystem":"Ikke gyldig avtale delt bosted","apiNavn":"avslagIkkeGyldigAvtaleDeltBosted","hjemler":["2"],"visningsnavn":"56. Ikke gyldig avtale delt bosted","borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"INGEN_UTBETALING","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"tema":"NASJONAL","_createdAt":"2021-10-26T10:00:06Z","bokmaal":[{"_key":"d6d3410a0e20","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn født ","_key":"341e79073557"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"572673145ae8"},{"_type":"span","marks":[],"text":" fordi du ikke har en gyldig avtale om delt bosted for ","_key":"d163c213330b"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"8c97364bab2a","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Derfor kan ikke barnetrygden deles.","_key":"6150fe76f026"}],"_type":"block","style":"normal"}]},{"tema":"FELLES","valgbarhet":"STANDARD","_id":"dd417fb3-8e87-467b-b94c-a77bfd89b2b4","navnISystem":"Delt bosted ingen utbetaling","nynorsk":[{"style":"normal","_key":"a0833a70a033","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda for tida før du søkte for barn fødd ","_key":"dfbb4feb8bca0"},{"_type":"flettefelt","_key":"ea2699069e62","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" er allereie utbetalt til den andre forelderen. Då kan vi ikkje etterbetale barnetrygd. Det er opp til dykk å bli einige om korleis de vil fordele barnetrygda som er utbetalt.","_key":"1853ffbc793b"}],"_type":"block"}],"endringsaarsaker":["DELT_BOSTED"],"_createdAt":"2022-03-07T18:58:18Z","apiNavn":"endretUtbetalingDeltBostedIngenUtbetaling","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","mappe":["ENDRET_UTBETALINGSPERIODE"],"_updatedAt":"2023-09-25T10:15:52Z","bokmaal":[{"_type":"block","style":"normal","_key":"07526742512b","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden for tiden før du søkte for barn født ","_key":"6243406cd0f80"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"2a89bc187abd"},{"_type":"span","marks":[],"text":" er allerede utbetalt til den andre forelderen. Da kan vi ikke etterbetale barnetrygd. Det er opp til dere å bli enige om hvordan dere vil fordele barnetrygden som er utbetalt.","_key":"ed10d57c25ec"}]}],"_rev":"BtltdVb0HP4g4WJfnr4YPo","visningsnavn":"8. NY Delt bosted ingen utbetaling","behandlingstema":"NASJONAL","_type":"begrunnelse","periodeType":"UTBETALING","endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"UTBETALING_IKKE_RELEVANT","hjemler":["2","12"]},{"behandlingstema":"NASJONAL","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"ae703d95f3cf"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"2826bb016007"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at ","_key":"2ce365fadf39"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"cd917b586377"},{"_type":"span","marks":[],"text":" har hatt fleire korte opphald i utlandet dei to siste åra. Opphalda i utlandet utgjer til saman meir enn 6 månader for kvart år. ","_key":"2d644705943e"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"6f5beb18aa76","skalHaStorForbokstav":true},{"text":" er derfor ikkje rekna som busett i Noreg frå ","_key":"f1968063d146","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"2de391af9fb5"},{"_type":"span","marks":[],"text":". Vi har også kome fram til at ","_key":"b9ab63308f27"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"1c59d0cb161f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikkje fyller vilkåra for å vere medlem i folketrygda. ","_key":"a0aaf3cb2992"}],"_type":"block","style":"normal","_key":"1e1dc8b2cc60"},{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"cd4b98888d5d"}],"_type":"block","style":"normal","_key":"ee1811144c66"}],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"455caddd163e"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"53501d4b09e1"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at ","_key":"c162c6ae0a2d"},{"_key":"130d6eb2dbc1","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" har hatt flere korte opphold i utlandet de to siste årene. Oppholdene i utlandet utgjør til sammen mer enn 6 måneder for hvert år. ","_key":"e63f054bb481"},{"_key":"87e4be3f1d7e","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2"},{"_key":"aa4f67931089","_type":"span","marks":[],"text":" regnes derfor derfor ikke som bosatt i Norge "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"8b6728c2d620"},{"_type":"span","marks":[],"text":". Vi har også kommet fram til at ","_key":"c9ee88803339"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"d5f264767618","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikke fyller vilkårene for å være medlem i folketrygden.","_key":"fd11d82fff34"}],"_type":"block","style":"normal","_key":"365ff9b3e4d5","markDefs":[]},{"_key":"a20ee823e521","markDefs":[],"children":[{"marks":[],"text":"","_key":"a25132783481","_type":"span"}],"_type":"block","style":"normal"}],"navnISystem":"Vurdering flere korte opphold i utlandet siste to år","hjemler":["4","5"],"_rev":"BtltdVb0HP4g4WJfnr4YPo","hjemlerFolketrygdloven":["2-5","2-8"],"periodeType":"UTBETALING","mappe":["AVSLAG"],"apiNavn":"avslagVurderingFlereKorteOppholdIUtlandetSisteToAar","visningsnavn":"25. Vurdering flere korte opphold i utlandet siste to år","rolle":["SOKER","BARN"],"bosattIRiketTriggere":["MEDLEMSKAP"],"valgbarhet":"STANDARD","vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"AVSLAG","tema":"NASJONAL","_createdAt":"2021-09-29T11:30:23Z","_id":"dd6b09d6-f3e8-4feb-a3cc-41ed8ffc6f32","_updatedAt":"2023-09-25T10:15:52Z"},{"apiNavn":"avslagOpplysningsplikt","vedtakResultat":"IKKE_INNVILGET","_createdAt":"2021-08-30T14:09:55Z","_id":"de61e0fb-f93f-40ee-8bbd-57980daf1d51","_updatedAt":"2023-09-25T10:26:24Z","bokmaal":[{"children":[{"text":"Barnetrygd for barn født ","_key":"08e68deaf14a","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"dd4a2833c98e"},{"text":" fordi du ikke har sendt oss de opplysningene vi ba om","_key":"dc900e22af5c","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"733c1fd970e8","markDefs":[]}],"ovrigeTriggere":["MANGLER_OPPLYSNINGER"],"navnISystem":"Ikke mottatt opplysninger","hjemler":["17","18"],"_rev":"FuD004taptHFqBZyEy7Dsj","_type":"begrunnelse","nynorsk":[{"_type":"block","style":"normal","_key":"e1e079d10fa9","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"4a4ef9fc844d"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"457d993dcb5b"},{"_type":"span","marks":[],"text":" fordi du ikkje har sendt oss dei opplysningane vi ba om","_key":"2440477d7847"}]}],"begrunnelsetype":"AVSLAG","valgbarhet":"TILLEGGSTEKST","mappe":["AVSLAG"],"visningsnavn":"12. Ikke mottatt opplysninger","periodeType":"INGEN_UTBETALING","tema":"FELLES"},{"_rev":"BtltdVb0HP4g4WJfnr5kgo","vilkaar":["BOR_MED_SOKER"],"tema":"FELLES","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"b156e17484440"},{"_type":"flettefelt","_key":"c70a61de7422","flettefelt":"barnasFodselsdatoer"},{"marks":[],"text":" fordi du har bedt om at barnetrygda for ","_key":"ca213d8e21fe","_type":"span"},{"_type":"valgfeltV2","_key":"5d90661d3dfa","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":" blir stansa.","_key":"19e36fde7bac"}],"_type":"block","style":"normal","_key":"684cfa5c55c2"}],"valgbarhet":"STANDARD","_id":"de72b001-0e12-41e4-843f-24bab9b75bc3","apiNavn":"opphorSokerBerOmOpphor","visningsnavn":"48. Søker ber om opphør","borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"periodeType":"INGEN_UTBETALING","begrunnelsetype":"OPPHØR","_createdAt":"2022-06-08T11:15:40Z","navnISystem":"Søker ber om opphør","behandlingstema":"NASJONAL","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","_updatedAt":"2023-09-25T10:36:43Z","bokmaal":[{"markDefs":[],"children":[{"_key":"23532ccfca190","_type":"span","marks":[],"text":"Barnetrygd for barn født "},{"_type":"flettefelt","_key":"8b344ec4928b","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi du har bedt om at barnetrygden for ","_key":"7485379a3354"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"eddcc1c48127","skalHaStorForbokstav":false},{"_key":"0627ed07870a","_type":"span","marks":[],"text":" blir stanset."}],"_type":"block","style":"normal","_key":"bf3325ddb77d"}],"hjemler":["2"],"mappe":["OPPHØR"]},{"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"a75ae933d03e"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"315b05199006"},{"text":" under oppholdet i utlandet. Ved opphold i utlandet som ikke varer lenger enn 3 måneder, er ","_key":"cfc726a7f43a","_type":"span","marks":[]},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"19c4fdcbac0b"},{"_type":"span","marks":[],"text":" regnet som bosatt i Norge.","_key":"2c69d3fe87d8"}],"_type":"block","style":"normal","_key":"10d73ec4c1b0"}],"navnISystem":"Barn opphold i utlandet ikke mer enn 3 måneder","apiNavn":"innvilgetBarnOppholdIUtlandetIkkeMerEnn3Maneder","hjemler":["4"],"begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","vilkaar":["BOSATT_I_RIKET"],"rolle":["BARN"],"_createdAt":"2021-09-24T12:16:04Z","_updatedAt":"2023-09-25T10:15:52Z","mappe":["INNVILGET"],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","tema":"NASJONAL","bosattIRiketTriggere":["MEDLEMSKAP"],"visningsnavn":"29. Barn opphold i utlandet ikke mer enn 3 måneder","_rev":"BtltdVb0HP4g4WJfnr4YPo","nynorsk":[{"style":"normal","_key":"e9a9785df106","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"05336721082e"},{"_type":"valgReferanse","_key":"3b1b92a40a8d","_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170"},{"_type":"span","marks":[],"text":" under opphaldet i utlandet. Ved opphald i utlandet som ikkje varer lenger enn 3 månader, er ","_key":"182c4ce6a7b5"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"acec969e793c"},{"text":" rekna som busett i Noreg.","_key":"881f9efaa7b8","_type":"span","marks":[]}],"_type":"block"}],"_id":"de91c9a7-f809-4119-bcf5-a0ab9ea188df"},{"vedtakResultat":"INNVILGET_ELLER_ØKNING","borMedSokerTriggere":["DELT_BOSTED"],"tema":"NASJONAL","nynorsk":[{"_type":"block","style":"normal","_key":"9831790ca132","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"026ca2f5793d"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"b47c56b88003"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at du bur saman med den andre forelderen til ","_key":"449871c2ba0a"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"8eda2bda8aa9"},{"_type":"span","marks":[],"text":". Derfor kan ikkje barnetrygda delast.","_key":"d352b7688cb5"}]}],"valgbarhet":"STANDARD","mappe":["OPPHØR"],"hjemler":["2","11"],"visningsnavn":"30. Vurdering foreldrene bor sammen","vilkaar":["BOR_MED_SOKER"],"_rev":"BtltdVb0HP4g4WJfnr5fsJ","_type":"begrunnelse","_id":"e288ed1e-1858-40f1-bca2-c4d0efa8a5fa","apiNavn":"opphorVurderingForeldreneBorSammen","_createdAt":"2021-10-26T10:02:49Z","begrunnelsetype":"OPPHØR","_updatedAt":"2023-09-25T10:35:33Z","bokmaal":[{"_type":"block","style":"normal","_key":"2d790b8b660b","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn født ","_key":"988891041ccb"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"e8ebad024105"},{"marks":[],"text":" fordi vi har kommet fram til at du bor sammen med den andre forelderen til ","_key":"1d37ab8a36b9","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"bf584321bc66","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Derfor kan ikke barnetrygden deles.","_key":"661894a26f5f"}]}],"navnISystem":"Vurdering foreldrene bor sammen","periodeType":"INGEN_UTBETALING"},{"apiNavn":"avslagIkkeDokumentertBosattINorge","vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"AVSLAG","vedtakResultat":"IKKE_INNVILGET","rolle":["SOKER","BARN"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"c0c2309e231b"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"ae730357d6f5"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at ","_key":"ee3cc08fcaae"},{"_type":"valgfeltV2","marks":[],"_key":"3a9bb9167a16","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"}},{"_type":"span","marks":[],"text":" ikkje er busett i Noreg.","_key":"b09dd361c148"}],"_type":"block","style":"normal","_key":"4914d1a245f4"}],"valgbarhet":"STANDARD","_id":"e3594151-f651-46ed-bdf1-0ff225d59b5e","navnISystem":"Ikke dokumentert bosatt i Norge","hjemler":["2","4"],"_rev":"FuD004taptHFqBZyEy7K96","_type":"begrunnelse","tema":"NASJONAL","bosattIRiketTriggere":["VURDERING_ANNET_GRUNNLAG"],"visningsnavn":"27. Ikke dokumentert bosatt i Norge","periodeType":"INGEN_UTBETALING","_createdAt":"2021-10-19T15:09:52Z","mappe":["AVSLAG"],"_updatedAt":"2023-09-25T10:27:18Z","bokmaal":[{"markDefs":[],"children":[{"_key":"61c0e02c5fdf","_type":"span","marks":[],"text":"Barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"1a8ef4bd33d1"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at ","_key":"c068d69d5418"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","marks":[],"_key":"1f4cb6a02e54"},{"_type":"span","marks":[],"text":" ikke er bosatt i Norge.","_key":"5d4a360c6cfd"}],"_type":"block","style":"normal","_key":"6fc1f778e273"}]},{"vilkaar":["BOR_MED_SOKER"],"periodeType":"UTBETALING","_id":"e35a2e2c-3756-47df-9de3-f662a9558c0e","hjemler":["2"],"_rev":"BtltdVb0HP4g4WJfnr4YPo","vedtakResultat":"REDUKSJON","_createdAt":"2021-11-05T10:35:23Z","valgbarhet":"STANDARD","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:52Z","ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"],"begrunnelsetype":"REDUKSJON","tema":"NASJONAL","nynorsk":[{"style":"normal","_key":"0014b80aa49e","markDefs":[],"children":[{"text":"Barnetrygda er endra fordi vi har kome fram til at avtalen om delt bustad for barn fødd ","_key":"2e234c635de2","_type":"span","marks":[]},{"_type":"flettefelt","_key":"a491e8588903","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" ikke vert følgd.","_key":"48a1eda05b0f"}],"_type":"block"}],"navnISystem":"Avtale delt bosted følges ikke","apiNavn":"reduksjonAvtaleDeltBostedFolgesIkke","bokmaal":[{"markDefs":[],"children":[{"_key":"80d821059abf","_type":"span","marks":[],"text":"Barnetrygden endres fordi vi har kommet fram til at avtalen om delt bosted for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"208a0319d8cb"},{"_type":"span","marks":[],"text":" ikke følges.","_key":"d3132b5cb185"}],"_type":"block","style":"normal","_key":"8d09709c56b4"}],"visningsnavn":"49. Avtale delt bosted følges ikke","_type":"begrunnelse","borMedSokerTriggere":["DELT_BOSTED"]},{"hjemler":["4","5"],"hjemlerFolketrygdloven":["2-8"],"periodeType":"UTBETALING","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"3c510a589841"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"efcff6586223"},{"text":" under opphaldet i utlandet fordi ","_key":"0eeb22707315","_type":"span","marks":[]},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"5f4d7f8d271b"},{"_type":"span","marks":[],"text":" er frivillig medlem i folketrygda frå ","_key":"4c16da702d47"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"39665ae5c77c"},{"_type":"span","marks":[],"text":".","_key":"5923e73e1fa6"}],"_type":"block","style":"normal","_key":"317984cab4d1"}],"mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:52Z","navnISystem":"Søker og barn frivillig medlem","visningsnavn":"19. Søker og barn frivillig medlem","_rev":"BtltdVb0HP4g4WJfnr4YPo","_type":"begrunnelse","bokmaal":[{"style":"normal","_key":"4c03dab3caac","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"ec6773c64903"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"2fd4de4a09b5"},{"_type":"span","marks":[],"text":" under oppholdet i utlandet fordi ","_key":"739841fe16a3"},{"_type":"valgReferanse","_key":"46d8122a8b7c","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},{"text":" er frivillig medlem i folketrygden fra ","_key":"e7b6082235a7","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"083f7d75d124"},{"_type":"span","marks":[],"text":".","_key":"6687b763af6a"}],"_type":"block"}],"vedtakResultat":"INNVILGET_ELLER_ØKNING","rolle":["SOKER","BARN"],"begrunnelsetype":"INNVILGET","_id":"e3c36db8-3ff1-40dd-ada7-23f3abea4095","_createdAt":"2021-09-24T11:08:50Z","valgbarhet":"STANDARD","apiNavn":"innvilgetSokerOgBarnFrivilligMedlem","vilkaar":["BOSATT_I_RIKET"],"tema":"NASJONAL","bosattIRiketTriggere":["MEDLEMSKAP"]},{"bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd fordi du og ","_key":"9e7081772e0b","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"64a98e887fa7","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" fortsatt er bosatt i Norge.","_key":"5d0a21c85c9b"}],"_type":"block","style":"normal","_key":"b5756fa63789"}],"apiNavn":"fortsattInnvilgetSokerOgBarnBosattIRiket","_createdAt":"2021-08-30T12:53:37Z","_id":"e50bcdf3-9f30-40e6-8600-f0d16ad75c33","mappe":["FORTSATT_INNVILGET"],"navnISystem":"Søker og barn oppholder seg i Norge","vedtakResultat":"INGEN_ENDRING","_updatedAt":"2023-09-25T10:21:23Z","rolle":["SOKER","BARN"],"begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","valgbarhet":"STANDARD","visningsnavn":"1B. Søker og barn oppholder seg i Norge","_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"periodeType":"FORTSATT_INNVILGET","hjemler":["2","4","11"],"_rev":"BtltdVb0HP4g4WJfnr4prJ","nynorsk":[{"_key":"52e323cdb272","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du og ","_key":"dabe9ae77ee6"},{"_type":"valgfeltV2","_key":"887c4436211b","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_key":"a3694428eb74","_type":"span","marks":[],"text":" fortsatt er busett i Noreg."}],"_type":"block","style":"normal"}]},{"_id":"e5b01952-7c6b-4ba4-b11c-1743efced182","_updatedAt":"2023-09-25T10:15:52Z","apiNavn":"endretUtbetalingNyDeltBostedFullUtbetalingForSoknad","hjemler":["2","12"],"begrunnelsetype":"ENDRET_UTBETALINGSPERIODE","tema":"FELLES","_createdAt":"2022-03-07T19:09:42Z","navnISystem":"Delt bosted full utbetaling før søknad","_type":"begrunnelse","nynorsk":[{"markDefs":[],"children":[{"text":"Du har allereie fått full barnetrygd for barn fødd ","_key":"1e193fda8c580","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"3261801504db"},{"_type":"span","marks":[],"text":" i denne perioden. Det er opp til deg og den andre forelderen å bli einige om korleis de vil fordele barnetrygda som er utbetalt.","_key":"353ffea24666"}],"_type":"block","style":"normal","_key":"26130e248eb1"}],"mappe":["ENDRET_UTBETALINGSPERIODE"],"visningsnavn":"9.NY Delt bosted full utbetaling før søknad","endringsaarsaker":["DELT_BOSTED"],"endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"SKAL_UTBETALES","_rev":"BtltdVb0HP4g4WJfnr4YPo","vedtakResultat":"INGEN_ENDRING","periodeType":"UTBETALING","valgbarhet":"STANDARD","bokmaal":[{"_type":"block","style":"normal","_key":"756f2a3aeb4f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du har allerede fått full barnetrygd for barn født ","_key":"562e18bb1ba40"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"f0f60d94c426"},{"_type":"span","marks":[],"text":" i denne perioden. Det er opp til deg og den andre forelderen å bli enige om hvordan dere vil fordele barnetrygden som er utbetalt.","_key":"146f637a4c0a"}]}]},{"visningsnavn":"52. Skilt og vurdering egen husholdning","vedtakResultat":"INNVILGET_ELLER_ØKNING","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"f440ec4c1931"},{"_key":"7f949d41d986","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi du er skild og bur aleine med ","_key":"3e0ce758ccbe"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"e26d18650124","skalHaStorForbokstav":false},{"marks":[],"text":". Vi har kome fram til at du og den tidligare ektefellen din flytta frå kvarandre ","_key":"4ccb210c4916","_type":"span"},{"_type":"flettefelt","_key":"070e48b49b71","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_type":"span","marks":[],"text":". ","_key":"074ff9191e51"},{"_key":"dada7d2bbfe0","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at de flytta frå kvarandre.","_key":"4b15a8c90137"}],"_type":"block","style":"normal","_key":"74dc1234ad92"}],"_createdAt":"2021-10-25T08:23:38Z","mappe":["INNVILGET"],"bokmaal":[{"_type":"block","style":"normal","_key":"ea3be00c4734","markDefs":[],"children":[{"marks":[],"text":"","_key":"0ba29773b256","_type":"span"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"47190650021d","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd fordi du er skilt og bor alene med ","_key":"d2bc8224d8de"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"0d4381f39276","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Vi har kommet fram til at du og den tidligere ektefellen din flyttet fra hverandre ","_key":"94576dd6da2d"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"1f1c3b63f455"},{"text":". ","_key":"60c840ff8c66","_type":"span","marks":[]},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"82c6fc2f379d","skalHaStorForbokstav":true},{"_key":"9dd18617b5e8","_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at dere flyttet fra hverandre."}]}],"_rev":"BtltdVb0HP4g4WJfnr4YPo","vilkaar":["UTVIDET_BARNETRYGD"],"apiNavn":"innvilgetSkiltOgVurderingEgenHusholdning","behandlingstema":"NASJONAL","periodeType":"UTBETALING","tema":"FELLES","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:52Z","navnISystem":"Skilt og vurdering egen husholdning","hjemler":["2","9","11"],"_type":"begrunnelse","begrunnelsetype":"INNVILGET","_id":"e6480fe5-366c-4c42-bdee-c51b3a8eb5ff"},{"_id":"e68e55e6-b03c-45ee-96c8-970969519630","navnISystem":"Vurdering flere korte opphold i utlandet siste årene","apiNavn":"opphorVurderingFlereKorteOppholdIUtlandetSisteArene","hjemler":["4","5"],"visningsnavn":"24. Vurdering flere korte opphold i utlandet siste årene","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:34:55Z","_type":"begrunnelse","hjemlerFolketrygdloven":["2-5","2-8"],"tema":"NASJONAL","_createdAt":"2021-09-24T12:10:14Z","bokmaal":[{"_type":"block","style":"normal","_key":"156f6e63ba48","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"ac1b5a6d32c2"},{"_key":"4406da5b61d0","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"marks":[],"text":" fordi vi har kommet fram til at ","_key":"a18e94179d5c","_type":"span"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"800b03e0f60e","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" har hatt flere korte opphold i utlandet de siste årene. Oppholdene i utlandet utgjør til sammen mer enn 6 måneder for hvert år. ","_key":"34aba01a888d"},{"skalHaStorForbokstav":true,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"699e80115b54"},{"_type":"span","marks":[],"text":" regnes derfor derfor ikke som bosatt i Norge ","_key":"4c495d207b0f"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"0919ee2b526b"},{"_type":"span","marks":[],"text":". Vi har også kommet fram til at ","_key":"61d72cd12a3d"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"914ad4e3c3dc","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikke fyller vilkårene for å være medlem i folketrygden.","_key":"9ab67987dd2f"}]}],"_rev":"BtltdVb0HP4g4WJfnr5dlo","begrunnelsetype":"OPPHØR","bosattIRiketTriggere":["MEDLEMSKAP"],"mappe":["OPPHØR"],"vilkaar":["BOSATT_I_RIKET"],"rolle":["SOKER"],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"c98cffa34aa0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"82f3b20b784d"},{"text":" fordi vi har kome fram til at ","_key":"28b9cd9d9ba5","_type":"span","marks":[]},{"valgReferanse":{"_type":"reference","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},"_type":"valgfeltV2","_key":"8c32d50d2344","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" har hatt fleire korte opphald i utlandet dei siste åra. Opphalda i utlandet utgjer til saman meir enn 6 månader for kvart år. ","_key":"4be9bc40cd5c"},{"valgReferanse":{"_type":"reference","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},"_type":"valgfeltV2","_key":"152716cfa880","skalHaStorForbokstav":true},{"text":" er derfor ikkje rekna som busett i Noreg ","_key":"0f7629759ca9","_type":"span","marks":[]},{"_key":"4b47cac2e42d","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":". Vi har også kome fram til at ","_key":"ceedd34d84b1"},{"valgReferanse":{"_type":"reference","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},"_type":"valgfeltV2","_key":"0805fe8fbecc","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikkje fyller vilkåra for å vere medlem i folketrygda. ","_key":"50813054a6e1"}],"_type":"block","style":"normal","_key":"9cc01fb8a934","markDefs":[]}]},{"mappe":["OPPHØR"],"navnISystem":"Barn død samme måned som født","hjemler":["2","11"],"visningsnavn":"45. Barn død samme måned som født","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOR_MED_SOKER"],"_id":"e6a4c6f8-0982-42bc-b30b-192c97bf5c3a","behandlingstema":"NASJONAL","_rev":"FuD004taptHFqBZyEy8gNF","begrunnelsetype":"OPPHØR","tema":"NASJONAL","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barnet ditt som er fødd ","_key":"0b2f838ff5af"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"6f88ee7a2a84"},{"_type":"span","marks":[],"text":" fordi barnet døydde.","_key":"5f53c1899d48"}],"_type":"block","style":"normal","_key":"5cb4b914c2d5"}],"_updatedAt":"2023-09-25T10:36:33Z","apiNavn":"opphorBarnDodSammeMaanedSomFoedt","periodeType":"INGEN_UTBETALING","_createdAt":"2022-04-05T14:40:19Z","valgbarhet":"STANDARD","bokmaal":[{"_type":"block","style":"normal","_key":"a1dad5783a50","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barnet ditt som er født ","_key":"0e7bf3642f98"},{"_type":"flettefelt","_key":"57a8a18869ba","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi barnet døde.","_key":"982c83c06003"}]}]},{"periodeType":"FORTSATT_INNVILGET","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får fortsatt barnetrygd fordi du bur fast saman med barn i Noreg.","_key":"269e8df7d4180"}],"_type":"block","style":"normal","_key":"dc84236cdfaa"}],"valgbarhet":"STANDARD","_id":"e6f89250-3206-4709-8f84-68b53095291b","hjemler":["2","4"],"behandlingstema":"NASJONAL","_type":"begrunnelse","vilkaar":["BOR_MED_SOKER"],"mappe":["FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:24:06Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får fortsatt barnetrygd fordi du bor fast sammen med barn i Norge.","_key":"104499a7fc0a0"}],"_type":"block","style":"normal","_key":"708a0560754d"}],"visningsnavn":"42. Generell bor sammen med barn","_rev":"BtltdVb0HP4g4WJfnr51NJ","begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","navnISystem":"Generell bor sammen med barn","apiNavn":"fortsattInnvilgetGenerellBorSammenMedBarn","vedtakResultat":"INGEN_ENDRING","_createdAt":"2022-10-06T11:17:48Z"},{"_createdAt":"2021-10-25T06:33:38Z","visningsnavn":"30. Bor alene med barn","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","valgbarhet":"STANDARD","mappe":["INNVILGET"],"navnISystem":"Bor alene med barn","hjemler":["2","9","11"],"begrunnelsetype":"INNVILGET","tema":"FELLES","_id":"e75aa16e-e5e3-4ac8-a566-5c3f80aacaad","_updatedAt":"2023-09-25T10:15:52Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"aa0fd5361891"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"a24e87e380cd"},{"_key":"66817e32e20f","_type":"span","marks":[],"text":" utvidet barnetrygd fordi du bor alene med "},{"_type":"valgfeltV2","_key":"294c92507272","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":".","_key":"7fc5c436f948"}],"_type":"block","style":"normal","_key":"572ed58c24f1"}],"apiNavn":"innvilgetBorAleneMedBarn","vilkaar":["UTVIDET_BARNETRYGD"],"nynorsk":[{"_key":"fe10463ae1aa","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"8082d1ce5032"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"f59a347d8d25","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi du bur aleine med ","_key":"34cd03b553cf"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"4fea2b0e3763"},{"_type":"span","marks":[],"text":".","_key":"bf687ecc9dba"}],"_type":"block","style":"normal"}],"_rev":"BtltdVb0HP4g4WJfnr4YPo","periodeType":"UTBETALING"},{"nynorsk":[{"_key":"a80e3c8ce766","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barnet ditt som er fødd ","_key":"e19515d9e9eb","_type":"span"},{"_type":"flettefelt","_key":"435124f29dc7","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi barnet døydde. Barnetrygda opphøyrer frå månaden etter at barnet døydde.","_key":"2ca5d2aa3b2f"}],"_type":"block","style":"normal"}],"bokmaal":[{"_key":"8c45f2681645","markDefs":[],"children":[{"text":"Barnetrygd for barnet ditt som er født ","_key":"6daa540d1c27","_type":"span","marks":[]},{"_key":"56f625e53336","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"marks":[],"text":" fordi barnet døde. Barnetrygden opphører fra måneden etter at barnet døde.","_key":"bd9871e6f062","_type":"span"}],"_type":"block","style":"normal"}],"apiNavn":"opphorEtBarnErDodt","visningsnavn":"5a. Et barn er dødt","vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"OPPHØR","_createdAt":"2021-08-30T10:13:03Z","valgbarhet":"AUTOMATISK","_id":"e912459d-7267-49c3-a93f-9f5fdc37e943","mappe":["OPPHØR"],"hjemler":["2","11"],"_type":"begrunnelse","_updatedAt":"2023-09-25T10:31:35Z","navnISystem":"Et barn er dødt","periodeType":"INGEN_UTBETALING","tema":"FELLES","ovrigeTriggere":["BARN_DØD"],"behandlingstema":"NASJONAL","_rev":"h26rAhFEYSUtDGXJE0t9C1"},{"tema":"NASJONAL","valgbarhet":"STANDARD","mappe":["INNVILGET"],"visningsnavn":"70. EØS-borger jobber","_rev":"BtltdVb0HP4g4WJfnr4YPo","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["LOVLIG_OPPHOLD"],"_id":"e982e507-e626-4813-8d3f-880a0dca8eb3","_updatedAt":"2023-09-25T10:15:52Z","apiNavn":"innvilgetEosBorgerJobber","_type":"begrunnelse","periodeType":"UTBETALING","rolle":["SOKER","BARN"],"begrunnelsetype":"INNVILGET","bokmaal":[{"children":[{"_key":"0d4a0a88f17a0","_type":"span","marks":[],"text":"Du får barnetrygd fordi du har oppholdsrett som EØS-borger. Familien bor i Norge og du jobber her fra "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"9f9d73413203"},{"text":". ","_key":"ca8c2d367f59","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"e7ef03ddbfe4","markDefs":[]}],"navnISystem":"Eøs-borger jobber","hjemler":["2","4","11"],"nynorsk":[{"_type":"block","style":"normal","_key":"1c320de31092","markDefs":[],"children":[{"text":"Du får barnetrygd fordi du har opphaldsrett som EØS-borgar. Familien bur i Noreg og du jobbar her frå ","_key":"04745d5720440","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"8652e281411a"},{"marks":[],"text":".","_key":"1dc983d4ce9d","_type":"span"}]}],"_createdAt":"2022-03-17T18:10:02Z"},{"vilkaar":["UNDER_18_ÅR"],"periodeType":"UTBETALING","_createdAt":"2021-08-27T11:22:47Z","_id":"e9b1c057-73ae-4bc2-8f4a-92ad19623609","bokmaal":[{"children":[{"_key":"796a136ec18d","_type":"span","marks":[],"text":"Du får mer barnetrygd fordi du har fått nytt barn, og barna bor sammen med deg. Du får barnetrygden fra måneden etter at "},{"skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"c211bc2916cc"},{"text":" er født.","_key":"56de8113e69d","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"1aea0f6a48da","markDefs":[]}],"hjemler":["2","4","11","14"],"behandlingstema":"NASJONAL","begrunnelsetype":"INNVILGET","_updatedAt":"2023-09-25T08:12:12Z","_rev":"BtltdVb0HP4g4WJfnqyHTo","vedtakResultat":"INNVILGET_ELLER_ØKNING","visningsnavn":"Fødselshendelse, Nyfødt barn - har barn fra før","_type":"begrunnelse","nynorsk":[{"_key":"47084ad023e2","markDefs":[],"children":[{"marks":[],"text":"Du får meir barnetrygd fordi du har fått nytt barn, og barna bur saman med deg. Du får barnetrygda frå månaden etter at ","_key":"c3d02be29be8","_type":"span"},{"_key":"f21be790818b","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"marks":[],"text":" er fødd.","_key":"ec3296e545f0","_type":"span"}],"_type":"block","style":"normal"}],"valgbarhet":"AUTOMATISK","mappe":["INNVILGET"],"navnISystem":"Nyfødt barn - har barn fra før","apiNavn":"innvilgetFodselshendelseNyfodtBarn","tema":"NASJONAL","ovrigeTriggere":["ALLTID_AUTOMATISK"]},{"navnISystem":"Barn død","behandlingstema":"NASJONAL_INSTITUSJON","periodeType":"INGEN_UTBETALING","mappe":["INSTITUSJON","OPPHØR"],"ovrigeTriggere":["BARN_DØD"],"_updatedAt":"2023-09-25T10:38:03Z","bokmaal":[{"_key":"ef621db30e4f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barnet fordi barnet døde ","_key":"0c9fc94317660"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"ed616dc4127b"},{"_type":"span","marks":[],"text":". Barnetrygden opphører fra måneden etter at barnet døde.","_key":"88647e6ad3c0"}],"_type":"block","style":"normal"}],"hjemler":["2","11"],"visningsnavn":"2. Barn død","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","_createdAt":"2022-11-04T08:23:38Z","valgbarhet":"SAKSPESIFIKK","apiNavn":"opphorBarnDodInstitusjon","begrunnelsetype":"OPPHØR","tema":"NASJONAL","_id":"e9e14bf2-612a-4e91-9024-4c07aff6cdd4","fagsakType":"INSTITUSJON","_rev":"FuD004taptHFqBZyEy8sB3","nynorsk":[{"_key":"9b0ce21baa95","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barnet fordi darnet døydde ","_key":"a6491827606b0"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"a34bd743957f"},{"text":". Barnetrygda opphøyrer frå månaden etter at barnet døydde.","_key":"48d73573a302","_type":"span","marks":[]}],"_type":"block","style":"normal"}]},{"rolle":["SOKER","BARN"],"tema":"NASJONAL","nynorsk":[{"style":"normal","_key":"71e63aac4646","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"a1a74a04f4830"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"8234705965b7"},{"_key":"7b0930b55492","_type":"span","marks":[],"text":" fordi du og "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"a199c68ba313","skalHaStorForbokstav":false},{"marks":[],"text":" er busett i Noreg og har opphaldsløyve.","_key":"17be19abe13d","_type":"span"}],"_type":"block"}],"_createdAt":"2022-05-24T14:22:08Z","navnISystem":"Tilleggstekst tredjelandsborger oppholdstillatelse","apiNavn":"innvilgetTilleggstekstTredjelandsborgerOppholdstillatelse","hjemler":["2","4"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","mappe":["INNVILGET"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn født ","_key":"c469dacaabfa0"},{"_type":"flettefelt","_key":"adb625e1553a","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi du og ","_key":"25ca272a9b52"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"1c3e10367e8f","skalHaStorForbokstav":false},{"marks":[],"text":" er bosatt i Norge og har oppholdstillatelse.","_key":"d4b752c3066e","_type":"span"}],"_type":"block","style":"normal","_key":"12cf07115bb1"}],"_type":"begrunnelse","vilkaar":["LOVLIG_OPPHOLD"],"begrunnelsetype":"INNVILGET","_updatedAt":"2023-09-25T10:15:52Z","valgbarhet":"TILLEGGSTEKST","_id":"ea1642fa-ff12-474d-8b0a-2d55f341c06a","visningsnavn":"88. Tilleggstekst tredjelandsborger oppholdstillatelse","behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr4YPo","periodeType":"UTBETALING"},{"periodeType":"INGEN_UTBETALING","rolle":["SOKER"],"apiNavn":"opphorUgyldigKontonummerEos","visningsnavn":"13. Ugyldig kontonummer EØS","behandlingstema":"EØS","vedtakResultat":"IKKE_INNVILGET","nynorsk":[{"_type":"block","style":"normal","_key":"54a34895ef78","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"216cbb957b16"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"76bc109b9740"},{"_type":"span","marks":[],"text":" fordi du ikkje har gyldig kontonummer.","_key":"c048ba8a02a0"}]}],"_updatedAt":"2023-09-25T10:41:59Z","bokmaal":[{"_type":"block","style":"normal","_key":"9107c31b975e","markDefs":[],"children":[{"text":"Barnetrygd for barn født ","_key":"b2e90808e5040","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"48ef2ae36141"},{"_type":"span","marks":[],"text":" fordi du ikke har gyldig kontonummer.","_key":"4ebea8639a5f"}]}],"navnISystem":"Ugyldig kontonummer EØS","_rev":"FuD004taptHFqBZyEy97YK","valgbarhet":"STANDARD","_id":"ea9e5c58-693b-461c-b98a-f7fe444177e2","mappe":["EØS","OPPHØR"],"hjemler":["17","18"],"_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"OPPHØR","tema":"FELLES","_createdAt":"2022-11-08T12:45:59Z"},{"visningsnavn":"81. Tillegsstekst samboer under 12 måneder","_rev":"BtltdVb0HP4g4WJfnr4YPo","_createdAt":"2022-05-13T13:14:14Z","_id":"eab489bd-0baf-40e9-9130-0b706c9dfc02","navnISystem":"Tilleggstekst samboer under 12 måneder","vilkaar":["UTVIDET_BARNETRYGD"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","mappe":["INNVILGET"],"apiNavn":"innvilgetTilleggstekstSamboerUnder12Maaneder","hjemler":["2","9","11"],"_type":"begrunnelse","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","tema":"FELLES","nynorsk":[{"style":"normal","_key":"44ce425f5bab","markDefs":[],"children":[{"text":"","_key":"837534d39094","_type":"span","marks":[]},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"8125e23ce30f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvida barnetrygd sjølv om du var sambuar fordi du var sambuar i mindre enn 12 månader.","_key":"2ebb4a871fb70"}],"_type":"block"}],"valgbarhet":"TILLEGGSTEKST","_updatedAt":"2023-09-25T10:15:52Z","bokmaal":[{"_type":"block","style":"normal","_key":"72b4aa152f2b","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"5c0c686cf663"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"072a4f33818e","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd selv om du var samboer fordi du var samboer i mindre enn 12 måneder.","_key":"418389374e890"}]}]},{"begrunnelsetype":"INNVILGET","_id":"edb6d9a8-ba72-46c5-a4f0-a42917051f93","mappe":["INNVILGET"],"bokmaal":[{"style":"normal","_key":"6a986892bbb8","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"6126504ee4cd"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"30bb4b5840df"},{"_type":"span","marks":[],"text":" under oppholdet i utlandet. Vi har kommet fram til at ","_key":"81bf354f41f9"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"400ddf210ea7"},{"_type":"span","marks":[],"text":" fyller vilkårene for å bli frivillig medlem i folketrygden fra ","_key":"dfaf60054945"},{"_type":"flettefelt","_key":"baaaa9e0b191","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_type":"span","marks":[],"text":". Dette gir rett til barnetrygd fra Norge.","_key":"56b7ff5fda55"}],"_type":"block"}],"navnISystem":"Vurdering søker og barn frivillig medlem","vedtakResultat":"INNVILGET_ELLER_ØKNING","rolle":["SOKER","BARN"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"58887cd51003"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"0a6879cc3df0"},{"_type":"span","marks":[],"text":" under opphaldet i utlandet. Vi har kome fram til at ","_key":"a672a5bc1f5e"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"10c618722dd5"},{"text":" fyller vilkåra for å bli frivillig medlem i folketrygda frå ","_key":"aaf9c615ba07","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"e794c0d30688"},{"text":". Dette gjev rett til barnetrygd frå Noreg.","_key":"9000becfeefb","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"ad6ffd3ded63"}],"_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"_rev":"BtltdVb0HP4g4WJfnr4YPo","hjemlerFolketrygdloven":["2-8"],"bosattIRiketTriggere":["MEDLEMSKAP"],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:52Z","hjemler":["4","5"],"visningsnavn":"25. Vurdering søker og barn frivillig medlem","tema":"NASJONAL","_createdAt":"2021-09-24T12:02:21Z","apiNavn":"innvilgetVurderingSokerOgBarnFrivilligMedlem","periodeType":"UTBETALING"},{"_rev":"BtltdVb0HP4g4WJfnr4YPo","_type":"begrunnelse","begrunnelsetype":"INNVILGET","mappe":["INNVILGET"],"bokmaal":[{"style":"normal","_key":"a70650af3e30","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"e11279acf440"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"99597a68c02c","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd selv om du er samboer. ","_key":"7a76b7b116220"},{"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"},"_type":"valgfeltV2","_key":"03e65b8e0037","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fram til du har vært samboer i 12 av de siste 18 månedene. Barnetrygden blir redusert når du har vært samboer i til sammen 12 måneder.","_key":"4c4248496322"}],"_type":"block"}],"apiNavn":"innvilgetTilleggstekstSamboer12AvSiste18","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"UTBETALING","tema":"FELLES","_updatedAt":"2023-09-25T10:15:52Z","visningsnavn":"66. Tilleggstekst samboer 12 av siste 18","behandlingstema":"NASJONAL","nynorsk":[{"_key":"ec08dbf130e4","markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"00fd1ea8fcc4"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"21a9b40e1c4d"},{"marks":[],"text":" utvida barnetrygd sjølv om du er sambuar. ","_key":"80fe01c86ac70","_type":"span"},{"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"},"_type":"valgfeltV2","_key":"927edef2ea0a","skalHaStorForbokstav":true},{"text":" utvida barnetrygd fram til du har vore sambuar i 12 av dei siste 18 månadane. Barnetrygda blir redusert når du har vore sambuar i til saman 12 månader.","_key":"9e22c353866c","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"navnISystem":"Tilleggstekst samboer 12 av siste 18","vedtakResultat":"INNVILGET_ELLER_ØKNING","_createdAt":"2022-03-09T07:37:12Z","valgbarhet":"TILLEGGSTEKST","_id":"ef46f1af-3922-4f61-a330-620f355910c8","hjemler":["2","9","11"]},{"_rev":"h26rAhFEYSUtDGXJE0soTd","periodeType":"FORTSATT_INNVILGET","rolle":["BARN"],"valgbarhet":"STANDARD","begrunnelsetype":"FORTSATT_INNVILGET","tema":"NASJONAL","_createdAt":"2022-03-18T14:38:52Z","mappe":["FORTSATT_INNVILGET"],"navnISystem":"Et barn er blitt norsk statsborger","visningsnavn":"36. Et barn er blitt norsk statsborger","_type":"begrunnelse","vilkaar":["LOVLIG_OPPHOLD"],"_updatedAt":"2023-09-25T10:23:39Z","bokmaal":[{"markDefs":[],"children":[{"_key":"5a37d96186300","_type":"span","marks":[],"text":"Du får fortsatt barnetrygd fordi barnet er blitt norsk statsborger."}],"_type":"block","style":"normal","_key":"e32010991e17"}],"_id":"ef5c904b-dfbc-4c9a-a334-0002aafe0a7f","apiNavn":"fortsattInnvilgetEtBarnErBlittNorskStatsborger","hjemler":["2","4"],"vedtakResultat":"INGEN_ENDRING","nynorsk":[{"_key":"6a70451d9b84","markDefs":[],"children":[{"text":"Du får fortsatt barnetrygd fordi barnet er blitt norsk statsborgar.","_key":"3f5f04cb22140","_type":"span","marks":[]}],"_type":"block","style":"normal"}]},{"_type":"begrunnelse","vedtakResultat":"REDUKSJON","_createdAt":"2021-07-27T08:07:24Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi avtalen om delt bosted for barn født ","_key":"enighetOmOpphorAvAvtaleOmDeltBosted2"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"45ec4849c439"},{"text":" er opphørt fra ","_key":"2f77497254d8","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"3440c26f26e4"},{"_type":"span","marks":[],"text":".","_key":"90dfa2743a29"}],"_type":"block","style":"normal","_key":"enighetOmOpphorAvAvtaleOmDeltBosted1"}],"_rev":"BtltdVb0HP4g4WJfnr4YPo","tema":"NASJONAL","nynorsk":[{"_type":"block","style":"normal","_key":"enighetOmOpphorAvAvtaleOmDeltBosted1","markDefs":[],"children":[{"text":"Barnetrygda er endra fordi avtalen om delt bustad for barn fødd ","_key":"enighetOmOpphorAvAvtaleOmDeltBosted2","_type":"span","marks":[]},{"_key":"314b6b5de6dc","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"marks":[],"text":" er opphøyrt frå ","_key":"3201444aeb43","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"e62a4c3651d1"},{"_type":"span","marks":[],"text":".","_key":"eefcfc3d7cac"}]}],"valgbarhet":"STANDARD","borMedSokerTriggere":["DELT_BOSTED"],"vilkaar":["BOR_MED_SOKER"],"begrunnelsetype":"REDUKSJON","_id":"enighetOmOpphorAvAvtaleOmDeltBosted","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:52Z","hjemler":["2","11"],"apiNavn":"reduksjonDeltBostedEnighet","visningsnavn":"8. Enighet om opphør av avtale om delt bosted","periodeType":"UTBETALING","navnISystem":"Enighet om opphør av avtale om delt bosted"},{"_rev":"BtltdVb0HP4g4WJfnr4YPo","_type":"begrunnelse","nynorsk":[{"markDefs":[],"children":[{"_key":"eosborgerSkjonnsmessigVurderingAvOppholdsrett2","_type":"span","marks":[],"text":"Du får barnetrygd fordi vi har kome fram til at du har opphaldsrett som EØS-borgar frå "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"4e3435d5746f"},{"_key":"a48d3a9b7242","_type":"span","marks":[],"text":"."}],"_type":"block","style":"normal","_key":"eosborgerSkjonnsmessigVurderingAvOppholdsrett1"}],"_createdAt":"2021-07-27T08:07:24Z","valgbarhet":"STANDARD","_id":"eosborgerSkjonnsmessigVurderingAvOppholdsrett","mappe":["INNVILGET"],"apiNavn":"innvilgetLovligOppholdEOSBorgerSkjonnsmessigVurdering","hjemler":["2","4","11"],"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","lovligOppholdTriggere":["VURDERING_ANNET_GRUNNLAG"],"visningsnavn":"8. EØS-borger: Skjønnsmessig vurdering av oppholdsrett","vilkaar":["LOVLIG_OPPHOLD"],"rolle":["SOKER"],"tema":"NASJONAL","_updatedAt":"2023-09-25T10:15:52Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi vi har kommet fram til at du har oppholdsrett som EØS-borger fra ","_key":"eosborgerSkjonnsmessigVurderingAvOppholdsrett2"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"b9c8fe964a03"},{"_type":"span","marks":[],"text":".","_key":"294a3ada7d97"}],"_type":"block","style":"normal","_key":"eosborgerSkjonnsmessigVurderingAvOppholdsrett1"}],"navnISystem":"EØS-borger: Skjønnsmessig vurdering av oppholdsrett","vedtakResultat":"INNVILGET_ELLER_ØKNING"},{"_id":"eosborgerSokerHarOppholdsrett","mappe":["INNVILGET"],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","nynorsk":[{"style":"normal","_key":"eosborgerSokerHarOppholdsrett1","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd fordi du har opphaldsrett som EØS-borgar frå ","_key":"eosborgerSokerHarOppholdsrett2","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"f4e10d5b7ed5"},{"_type":"span","marks":[],"text":".","_key":"379633f2cbe5"}],"_type":"block"}],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:52Z","bokmaal":[{"markDefs":[],"children":[{"_key":"eosborgerSokerHarOppholdsrett2","_type":"span","marks":[],"text":"Du får barnetrygd fordi du har oppholdsrett som EØS-borger fra "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"7909c4d7a06c"},{"_type":"span","marks":[],"text":". ","_key":"691e8a6cb459"}],"_type":"block","style":"normal","_key":"eosborgerSokerHarOppholdsrett1"}],"apiNavn":"innvilgetLovligOppholdEOSBorger","hjemler":["2","4","11"],"_rev":"BtltdVb0HP4g4WJfnr4YPo","vilkaar":["LOVLIG_OPPHOLD"],"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","tema":"NASJONAL","_createdAt":"2021-07-27T08:07:24Z","navnISystem":"EØS-borger: Søker har oppholdsrett","visningsnavn":"7. EØS-borger: Søker har oppholdsrett","rolle":["SOKER"]},{"_rev":"BtltdVb0HP4g4WJfnr4YPo","_type":"begrunnelse","begrunnelsetype":"INNVILGET","nynorsk":[{"_key":"etterbetaling3Aar1","markDefs":[],"children":[{"_type":"span","text":"Du får etterbetalt barnetrygd for tre år tilbake i tid frå vi fekk søknaden din. Vi kan ikkje etterbetale barnetrygd lenger enn dette.","_key":"etterbetaling3Aar2"}],"_type":"block","style":"normal"}],"_createdAt":"2021-07-27T08:07:24Z","apiNavn":"innvilgetEtterbetaling3Aar","hjemler":["2","4","11"],"_updatedAt":"2023-09-25T10:15:52Z","visningsnavn":"9. Etterbetaling 3 år","vedtakResultat":"INNVILGET_ELLER_ØKNING","mappe":["INNVILGET"],"navnISystem":"Etterbetaling 3 år","valgbarhet":"STANDARD","tema":"FELLES","_id":"etterbetaling3Aar","bokmaal":[{"_key":"etterbetaling3Aar1","markDefs":[],"children":[{"_type":"span","text":"Du får etterbetalt barnetrygd for tre år tilbake i tid fra vi fikk søknaden din. Barnetrygd kan ikke etterbetales lenger enn dette.","_key":"etterbetaling3Aar2"}],"_type":"block","style":"normal"}],"behandlingstema":"NASJONAL","periodeType":"UTBETALING"},{"_type":"begrunnelse","_createdAt":"2021-10-18T12:03:33Z","mappe":["ENDRET_UTBETALINGSPERIODE"],"bokmaal":[{"_type":"block","style":"normal","_key":"6a9a2c2a2684","markDefs":[],"children":[{"text":"Barnetrygden for tiden før du søkte er allerede utbetalt til den andre forelderen. Da kan vi ikke etterbetale barnetrygd. Dere må selv bli enige om hvordan dere vil fordele barnetrygden som er utbetalt.","_key":"5cbf503770bf0","_type":"span","marks":[]}]}],"apiNavn":"endretUtbetalingDeltBostedIngenUtbetalingForSoknad","visningsnavn":"1. Delt bosted - ingen utbetaling før søknad","tema":"FELLES","_id":"f0df2103-198e-411a-9ff1-cfe5db97346d","nynorsk":[{"style":"normal","_key":"a5f0ae376311","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda for tida før du søkte er allereie utbetalt til den andre forelderen. Då kan vi ikkje etterbetale barnetrygd. De må sjølv bli einige om korleis de vil dele barnetrygda som er utbetalt.","_key":"31f1cec8e104"}],"_type":"block"}],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:52Z","hjemler":["2","12"],"_rev":"BtltdVb0HP4g4WJfnr4YPo","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","endringsaarsaker":["DELT_BOSTED"],"endretUtbetalingsperiodeDeltBostedUtbetalingTrigger":"UTBETALING_IKKE_RELEVANT","navnISystem":"Delt bosted - ingen utbetaling før søknad","behandlingstema":"NASJONAL","resultat":"INNVILGELSE","begrunnelsetype":"ENDRET_UTBETALINGSPERIODE"},{"bokmaal":[{"children":[{"text":"Du får utvidet barnetrygd fordi samboeren din fortsatt er forsvunnet.","_key":"a0f2a913e170","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"35b1fd526afe","markDefs":[]}],"hjemler":["9"],"visningsnavn":"30. Forsvunnet samboer","nynorsk":[{"style":"normal","_key":"fee2e67806b2","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får utvida barnetrygd fordi sambuaren din fortsatt er forsvunnen.","_key":"f6a19b3c0844"}],"_type":"block"}],"valgbarhet":"STANDARD","mappe":["FORTSATT_INNVILGET"],"apiNavn":"fortsattInnvilgetForsvunnetSamboer","begrunnelsetype":"FORTSATT_INNVILGET","_id":"f283c1eb-559c-46e0-82f6-727ec041fb56","tema":"FELLES","navnISystem":"Forsvunnet samboer","_rev":"h26rAhFEYSUtDGXJE0snit","_type":"begrunnelse","vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"FORTSATT_INNVILGET","vedtakResultat":"INGEN_ENDRING","_createdAt":"2021-10-25T10:21:40Z","_updatedAt":"2023-09-25T10:23:21Z"},{"rolle":["SOKER"],"tema":"NASJONAL","_id":"f2c577dd-adef-4eec-bffa-382e8a5c0036","mappe":["INNVILGET"],"bokmaal":[{"_type":"block","style":"normal","_key":"12cf07115bb1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn født ","_key":"c469dacaabfa0"},{"_type":"flettefelt","_key":"adb625e1553a","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi du er bosatt i Norge og har oppholdstillatelse.","_key":"25ca272a9b52"}]}],"vilkaar":["LOVLIG_OPPHOLD"],"begrunnelsetype":"INNVILGET","_createdAt":"2022-11-18T10:46:15Z","valgbarhet":"TILLEGGSTEKST","navnISystem":"Tilleggstekst tredjelandsborger oppholdstillatelse søker","vedtakResultat":"INNVILGET_ELLER_ØKNING","_rev":"BtltdVb0HP4g4WJfnr4YPo","_type":"begrunnelse","periodeType":"UTBETALING","nynorsk":[{"_key":"71e63aac4646","markDefs":[],"children":[{"_key":"a1a74a04f4830","_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"8234705965b7"},{"_type":"span","marks":[],"text":" fordi du er busett i Noreg og har opphaldsløyve.","_key":"7b0930b55492"}],"_type":"block","style":"normal"}],"apiNavn":"innvilgetTilleggstekstTredjelandsborgerOppholdstillatelseSoker","hjemler":["2","4"],"visningsnavn":"95. Tilleggstekst tredjelandsborger oppholdstillatelse søker","behandlingstema":"NASJONAL","_updatedAt":"2023-09-25T10:15:52Z"},{"bokmaal":[{"style":"normal","_key":"df9af858a9d9","markDefs":[],"children":[{"marks":[],"text":"Du får fortsatt barnetrygd fordi du og ","_key":"3306ac4e55ad0","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"1ff1140b88c3","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" er blitt norske statsborgere.","_key":"dd679388be79"}],"_type":"block"}],"_type":"begrunnelse","begrunnelsetype":"FORTSATT_INNVILGET","mappe":["FORTSATT_INNVILGET"],"_rev":"h26rAhFEYSUtDGXJE0soIS","rolle":["SOKER","BARN"],"tema":"NASJONAL","nynorsk":[{"_key":"a71b54ac0f8f","markDefs":[],"children":[{"text":"Du får fortsatt barnetrygd fordi du og ","_key":"85c943c94e000","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"b7704233a24f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" er blitt norske statsborgarar.","_key":"10246cf6f81c"}],"_type":"block","style":"normal"}],"valgbarhet":"STANDARD","apiNavn":"fortsattInnvilgetBrukerOgBarnErBlittNorskeStatsborgere","hjemler":["2","4"],"visningsnavn":"35. Bruker og barn er blitt norske statsborgere","_id":"f3494dcc-d449-4318-a1c9-f2f59941c71f","_updatedAt":"2023-09-25T10:23:35Z","periodeType":"FORTSATT_INNVILGET","_createdAt":"2022-03-18T14:37:08Z","navnISystem":"Bruker og barn er blitt norske statsborgere","vedtakResultat":"INGEN_ENDRING","vilkaar":["LOVLIG_OPPHOLD"]},{"mappe":["AVSLAG"],"apiNavn":"avslagFengselUnder6MaanederSamboer","visningsnavn":"43. Fengsel under 6 måneder samboer","begrunnelsetype":"AVSLAG","_id":"f3b6f845-afba-4304-a34e-3965f188f68a","bokmaal":[{"style":"normal","_key":"86185cc672eb","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvidet barnetrygd fordi fengselsoppholdet til samboeren din er under seks måneder.","_key":"50bc0be410db"}],"_type":"block"}],"hjemler":["9"],"_type":"begrunnelse","periodeType":"INGEN_UTBETALING","_updatedAt":"2023-09-25T10:29:57Z","navnISystem":"Fengsel under 6 måneder samboer","vilkaar":["UTVIDET_BARNETRYGD"],"tema":"FELLES","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Utvida barnetrygd fordi fengselsopphaldet til sambuaren din er under seks månader.","_key":"a7c7dcc679f1"}],"_type":"block","style":"normal","_key":"addc2f7a5dce"}],"_rev":"FuD004taptHFqBZyEy7Z6h","vedtakResultat":"IKKE_INNVILGET","_createdAt":"2021-10-22T09:05:10Z","valgbarhet":"STANDARD"},{"_rev":"BtltdVb0HP4g4WJfnr59MJ","behandlingstema":"NASJONAL","vilkaar":["BOSATT_I_RIKET"],"tema":"NASJONAL","navnISystem":"Ikke bosatt i Norge","hjemler":["2","4"],"vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","nynorsk":[{"_type":"block","style":"normal","_key":"98c437ef5c02","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"3a023bb82622"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"bf8cdd693cb7"},{"_type":"span","marks":[],"text":" fordi ","_key":"182cfc39c5ca"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"b57c61532367"},{"_type":"span","marks":[],"text":" ikkje er busett i Noreg","_key":"06b948d7cfb1"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"f857d8d9176d"},{"_type":"span","marks":[],"text":".","_key":"c007af2a41fb"}]}],"valgbarhet":"STANDARD","_id":"f3ffd1d4-7e10-4945-b75a-b81709040f02","mappe":["AVSLAG"],"apiNavn":"avslagBosattIRiket","bokmaal":[{"style":"normal","_key":"ed92e3f17374","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"98e6433a52c3","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"14a407e8291a"},{"_type":"span","marks":[],"text":" fordi ","_key":"e358578efa1a"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"4c0e352714fe"},{"_type":"span","marks":[],"text":" ikke er bosatt i Norge","_key":"52b3508be4df"},{"_key":"eec63465380f","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse"},{"_type":"span","marks":[],"text":".","_key":"0a405ea125f0"}],"_type":"block"}],"_type":"begrunnelse","rolle":["SOKER","BARN"],"begrunnelsetype":"AVSLAG","_createdAt":"2021-08-30T12:49:32Z","_updatedAt":"2023-09-25T10:25:41Z","visningsnavn":"1. Ikke bosatt i Norge"},{"vedtakResultat":"INNVILGET_ELLER_ØKNING","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"9d76f43693ee"},{"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"},"_type":"valgfeltV2","_key":"8f01ccf69aab","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi du er skilt og bur aleine med ","_key":"cbe66afe3cf3"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"538a2226d196","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Du vart skild ","_key":"031329d133cb"},{"_type":"flettefelt","_key":"914fc43278c1","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"marks":[],"text":". ","_key":"8902c7de3b23","_type":"span"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"a718f3f9fb8f","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at du vart skild.","_key":"c8e294ea1798"}],"_type":"block","style":"normal","_key":"d929cacc4b01"},{"style":"normal","_key":"eec808a7d378","markDefs":[],"children":[{"marks":[],"text":"\n","_key":"52c8a7f82e8d0","_type":"span"}],"_type":"block"}],"_id":"f47fe817-0fe8-4023-a5ba-2a7c5307405a","mappe":["INNVILGET"],"navnISystem":"Skilt","behandlingstema":"NASJONAL","_type":"begrunnelse","periodeType":"UTBETALING","tema":"FELLES","valgbarhet":"STANDARD","apiNavn":"innvilgetSkilt","hjemler":["2","9","11"],"vilkaar":["UTVIDET_BARNETRYGD"],"begrunnelsetype":"INNVILGET","_updatedAt":"2023-09-25T10:15:52Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"","_key":"8cefb22c9e1f"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"caf1469f8e90","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvidet barnetrygd fordi du er skilt og bor alene med ","_key":"005f89b27caf"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"0eb99be1a48a"},{"_type":"span","marks":[],"text":". Du ble skilt ","_key":"04c641bf6e17"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"d8fc26acbc95"},{"_key":"0efab46a6758","_type":"span","marks":[],"text":". "},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"dfef15216143","skalHaStorForbokstav":true},{"marks":[],"text":" utvidet barnetrygd fra måneden etter at du ble skilt.","_key":"01cf46e636b1","_type":"span"}],"_type":"block","style":"normal","_key":"cf1191d44cfb"}],"visningsnavn":"37. Skilt","_rev":"BtltdVb0HP4g4WJfnr4YPo","_createdAt":"2021-10-25T06:50:34Z"},{"hjemler":["2","4"],"visningsnavn":"87. Tilleggstekst EØS borger samboer utbetaling NAV","rolle":["SOKER","BARN"],"behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr4YPo","begrunnelsetype":"INNVILGET","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har opphaldsrett som EØS-borgar. Familien bur i Noreg og sambuaren din får utbetaling frå NAV som erstattar løn.","_key":"8cb4f73cfdc70"}],"_type":"block","style":"normal","_key":"ce4d81ce1fa9"}],"_createdAt":"2022-05-24T14:18:42Z","_updatedAt":"2023-09-25T10:15:52Z","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["BOSATT_I_RIKET","LOVLIG_OPPHOLD"],"bokmaal":[{"style":"normal","_key":"508a4056d40a","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd fordi du har oppholdsrett som EØS-borger. Familien bor i Norge og samboeren din får utbetaling fra NAV som erstatter lønn.","_key":"4e08602fc3710","_type":"span"}],"_type":"block"}],"navnISystem":"Tilleggstekst EØS borger samboer utbetaling NAV","apiNavn":"innvilgetTilleggstekstEosBorgerSamboerUtbetalingNav","_type":"begrunnelse","periodeType":"UTBETALING","tema":"NASJONAL","valgbarhet":"TILLEGGSTEKST","_id":"f5ccc32d-1367-4e00-9a96-6b73cb948b09","mappe":["INNVILGET"]},{"begrunnelsetype":"FORTSATT_INNVILGET","nynorsk":[{"markDefs":[],"children":[{"text":"Du får uendra barnetrygd fordi den andre forelderen ikkje har søkt om delt barnetrygd. Barnetrygda kan først delast frå månaden etter at begge foreldra har søkt om det.","_key":"d9c3258a891e","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"a061fd4ae8f0"}],"_id":"f62af5a9-4798-41c8-8883-913f638e7de9","_updatedAt":"2023-09-25T10:22:07Z","hjemler":["2","12"],"vilkaar":["BOR_MED_SOKER"],"tema":"NASJONAL","_createdAt":"2021-08-30T13:11:35Z","valgbarhet":"STANDARD","mappe":["FORTSATT_INNVILGET"],"navnISystem":"Annen forelder ikke søkt om delt barnetrygd","apiNavn":"fortsattInnvilgetAnnenForelderIkkeSokt","visningsnavn":"10. Annen forelder ikke søkt om delt barnetrygd","_type":"begrunnelse","borMedSokerTriggere":["DELT_BOSTED"],"_rev":"BtltdVb0HP4g4WJfnr4rwJ","vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET","bokmaal":[{"style":"normal","_key":"a6a5cc3474ab","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får uendret barnetrygd fordi den andre forelderen ikke har søkt om delt barnetrygd. Barnetrygden kan først deles fra måneden etter at begge foreldrene har søkt om det.","_key":"cb37a6c9c045"}],"_type":"block"}]},{"valgbarhet":"TILLEGGSTEKST","_id":"f64d6a4b-9639-4f8a-bc5f-7bd94aa8bbc1","behandlingstema":"NASJONAL","vilkaar":["BOSATT_I_RIKET","LOVLIG_OPPHOLD"],"rolle":["SOKER","BARN"],"begrunnelsetype":"INNVILGET","_createdAt":"2022-03-09T08:35:17Z","bokmaal":[{"style":"normal","_key":"66a97face3cf","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du har allerede fått barnetrygd av Utlendingsdirektoratet. Derfor blir hele etterbetalingen utbetalt til Utlendingsdirektoratet.","_key":"f11e1e4bc27e0"}],"_type":"block"}],"_rev":"BtltdVb0HP4g4WJfnr4YPo","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","nynorsk":[{"style":"normal","_key":"58486a5fb2bf","markDefs":[],"children":[{"_key":"868b833c73380","_type":"span","marks":[],"text":"Du har allereie fått barnetrygd av Utlendingsdirektoratet. Derfor blir heile etterbetalinga utbetalt til Utlendingsdirektoratet."}],"_type":"block"}],"mappe":["INNVILGET"],"navnISystem":"Tilleggstekst transporterklæring - hele etterbetalingen","apiNavn":"innvilgetTilleggstekstTransporterklaeringHeleEtterbetalingen","visningsnavn":"67. Tilleggstekst transporterklæring - hele etterbetalingen","_type":"begrunnelse","_updatedAt":"2023-09-25T10:15:52Z","tema":"NASJONAL"},{"periodeType":"UTBETALING","mappe":["INNVILGET"],"hjemler":["2","11"],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"NASJONAL","valgbarhet":"STANDARD","_id":"f75ca5ec-fc2d-4e6d-a071-cc0fee9b6cbf","visningsnavn":"61. Rettsavgjørelse delt bosted","vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["DELT_BOSTED"],"nynorsk":[{"style":"normal","_key":"36562ea012e1","markDefs":[],"children":[{"text":"Du får delt barnetrygd for barn fødd ","_key":"530dd1efa2fa","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"b7d854ea974b"},{"text":" fordi retten har bestemt at ","_key":"30812b4bcbbe","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"faed7eae5ee7","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" skal ha delt bustad frå ","_key":"9ceab5722779"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"3f334646469a"},{"_key":"f3121fee7ff9","_type":"span","marks":[],"text":". Barnetrygda er delt frå månden etter at retten avgjorde saka."}],"_type":"block"}],"_updatedAt":"2023-09-25T10:15:52Z","bokmaal":[{"children":[{"text":"Du får delt barnetrygd for barn født ","_key":"e27c6cb68116","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"8349dead8925"},{"_type":"span","marks":[],"text":" fordi retten har bestemt at ","_key":"390f72a85f93"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"d547e055589a","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" skal ha delt bosted fra ","_key":"fb5e0179dd4d"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"289d4d9ed7a5"},{"_type":"span","marks":[],"text":". Barnetrygden deles fra måneden etter at retten avgjorde saken.","_key":"dc3c29bfbe3c"}],"_type":"block","style":"normal","_key":"ca0d23f1cfea","markDefs":[]}],"apiNavn":"innvilgetRettsavgjorelseDeltBosted","_rev":"BtltdVb0HP4g4WJfnr4YPo","begrunnelsetype":"INNVILGET","_createdAt":"2021-10-26T08:48:52Z","navnISystem":"Rettsavgjørelse delt bosted"},{"vedtakResultat":"REDUKSJON","valgbarhet":"AUTOMATISK","apiNavn":"reduksjonBarnDodeSammeMaanedSomFoedt","hjemler":["2","11"],"visningsnavn":"62. Barn døde samme måned som født","_type":"begrunnelse","periodeType":"UTBETALING","_id":"f763e129-32af-41af-a197-b25fc269b6f9","tema":"FELLES","_createdAt":"2022-04-05T14:43:29Z","mappe":["REDUKSJON"],"bokmaal":[{"_key":"488f31c31374","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi barn født ","_key":"91bde5c78e3b0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"c5d97be5a254"},{"_type":"span","marks":[],"text":" døde.","_key":"4aa3a1be4bd2"}],"_type":"block","style":"normal"}],"ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"],"navnISystem":"Barn døde samme måned som født","_rev":"BtltdVb0HP4g4WJfnr4YPo","vilkaar":["BOR_MED_SOKER"],"begrunnelsetype":"REDUKSJON","nynorsk":[{"_key":"1b104c550c55","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi barn fødd ","_key":"df3a23e01d6e0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"0be9fe3539d7"},{"_type":"span","marks":[],"text":" døydde.","_key":"7099576dd88a"}],"_type":"block","style":"normal"}],"_updatedAt":"2023-09-25T10:15:52Z"},{"valgbarhet":"STANDARD","navnISystem":"Ikke oppholdstillatelse mer enn 12 måneder","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"7654d5a3cd35"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"75d060dd9515"},{"_type":"span","marks":[],"text":" fordi ","_key":"b1b1b99785e2"},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"f0e0124f6ac3","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikkje har opphaldsløyve i Noreg i meir enn 12 månader.","_key":"291688ba085b"}],"_type":"block","style":"normal","_key":"47607477067a"}],"_createdAt":"2022-10-31T14:17:51Z","apiNavn":"opphorIkkeOppholdstillatelseMerEnn12Maaneder","visningsnavn":"49. Ikke oppholdstillatelse mer enn 12 måneder","begrunnelsetype":"OPPHØR","periodeType":"INGEN_UTBETALING","rolle":["SOKER","BARN"],"lovligOppholdTriggere":["VURDERING_ANNET_GRUNNLAG"],"_id":"f79fe951-fd6b-4d44-a227-4ddf1653dbf2","mappe":["OPPHØR"],"_rev":"FuD004taptHFqBZyEy8hih","_type":"begrunnelse","vilkaar":["LOVLIG_OPPHOLD"],"tema":"NASJONAL","_updatedAt":"2023-09-25T10:36:47Z","bokmaal":[{"children":[{"_key":"d4dd5f5d36ea","_type":"span","marks":[],"text":"Barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"28abe051c106"},{"_type":"span","marks":[],"text":" fordi ","_key":"de54e31c6338"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"ef19328d0d2e"},{"_key":"70b34b071a8b","_type":"span","marks":[],"text":" ikke har oppholdstillatelse i Norge i mer enn 12 måneder."}],"_type":"block","style":"normal","_key":"933a763374e5","markDefs":[]}],"hjemler":["2","4"],"behandlingstema":"NASJONAL","vedtakResultat":"IKKE_INNVILGET"},{"begrunnelsetype":"OPPHØR","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:36:15Z","ovrigeTriggere":["GJELDER_FØRSTE_PERIODE"],"visningsnavn":"42. Vurdering den andre forelderen var ikke medlem","_rev":"h26rAhFEYSUtDGXJE0v1ji","tema":"NASJONAL","_id":"f7d723bd-3c53-470f-ab1b-9520c71c7e8c","mappe":["OPPHØR"],"bokmaal":[{"style":"normal","_key":"a8076a4a2367","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"c7417d037236"},{"_key":"773546002fc6","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at den andre forelderen ikke var medlem i folketrygden. Bor begge foreldrene sammen under oppholdet i utlandet, må dere begge være medlem i folketrygden for å få barnetrygd.","_key":"e6f2edb7a890"}],"_type":"block"}],"apiNavn":"opphorVurderingDenAndreForelderenVarIkkeMedlem","hjemler":["4","5"],"hjemlerFolketrygdloven":["2-5","2-8"],"vilkaar":["BOSATT_I_RIKET"],"bosattIRiketTriggere":["MEDLEMSKAP"],"navnISystem":"Vurdering den andre forelderen var ikke medlem","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","rolle":["SOKER","BARN"],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"e903abb22b19"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"02fb52e3d80c"},{"text":" fordi den andre forelderen ikkje var medlem i folketrygda. Bur begge foreldra saman under opphaldet i utlandet, må de begge vere medlem i folketrygda for å få barnetrygd.","_key":"043ee726996d","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"501c804dbde2","markDefs":[]}],"_createdAt":"2021-11-05T11:40:10Z","_type":"begrunnelse"},{"tema":"FELLES","_createdAt":"2021-08-27T09:00:44Z","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:52Z","navnISystem":"Foreldrene bor sammen, endret mottaker","hjemler":["2","4","11","12"],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"begrunnelsetype":"INNVILGET","_id":"f7fa2b54-e551-4fc6-a5a6-ce2762a6c50f","visningsnavn":"15. Foreldrene bor sammen, endret mottaker","behandlingstema":"NASJONAL","vilkaar":["BOR_MED_SOKER"],"periodeType":"UTBETALING","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"aac8518ba8d8"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"cd86d7b13c59"},{"_type":"span","marks":[],"text":" fordi ","_key":"87ba3e1d15c6"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"ec99bb2eefc8"},{"_type":"span","marks":[],"text":" bur saman med deg. Du får barnetrygd frå same tidspunkt som barnetrygda til den andre forelderen opphøyrer. ","_key":"a8ca636e0bf7"}],"_type":"block","style":"normal","_key":"a925976bc992"}],"valgbarhet":"STANDARD","bokmaal":[{"style":"normal","_key":"261df87080fc","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"2b1346bec8bd"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"0d1f7e893323"},{"marks":[],"text":" fordi ","_key":"f8210cca2d7d","_type":"span"},{"_key":"ceebdfdc0039","_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse"},{"_type":"span","marks":[],"text":" bor sammen med deg. Du får barnetrygd fra samme tidspunkt som barnetrygden til den andre forelderen opphører.","_key":"252fef1cd303"}],"_type":"block"}],"apiNavn":"innvilgetBarnBorSammenMedMottaker","_rev":"BtltdVb0HP4g4WJfnr4YPo"},{"apiNavn":"reduksjonDenAndreForelderenVarIkkeMedlem","hjemler":["4","5"],"behandlingstema":"NASJONAL","_type":"begrunnelse","_createdAt":"2021-11-05T10:52:40Z","_rev":"BtltdVb0HP4g4WJfnr4YPo","rolle":["SOKER","BARN"],"tema":"NASJONAL","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:52Z","ovrigeTriggere":["GJELDER_FRA_INNVILGELSESTIDSPUNKT"],"hjemlerFolketrygdloven":["2-5","2-8"],"begrunnelsetype":"REDUKSJON","nynorsk":[{"_type":"block","style":"normal","_key":"1b60f51c03b3","markDefs":[],"children":[{"_key":"9529dd49e95f","_type":"span","marks":[],"text":"Barnetrygda er endra fordi den andre forelderen ikkje var medlem i folketrygda. Bur begge foreldra saman under opphaldet i utlandet, må de begge vere medlem i folketrygda for å få barnetrygd."}]}],"valgbarhet":"STANDARD","bokmaal":[{"_key":"c13bd59a7062","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi den andre forelderen ikke var medlem i folketrygden. Bor begge foreldrene sammen under oppholdet i utlandet, må dere begge være medlem i folketrygden for å få barnetrygd.","_key":"1b9440b2703e"}],"_type":"block","style":"normal"}],"navnISystem":"Den andre forelderen var ikke medlem","visningsnavn":"55. Den andre forelderen var ikke medlem","vedtakResultat":"REDUKSJON","vilkaar":["BOSATT_I_RIKET"],"periodeType":"UTBETALING","bosattIRiketTriggere":["MEDLEMSKAP"],"_id":"f958a724-65bf-4757-affc-575528be7f27"},{"nynorsk":[{"_type":"block","style":"normal","_key":"99a8930928ba","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Delt barnetrygd for barn fødd ","_key":"cfc35b786a64"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"b5df01f45fc9"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at avtalen om delt bustad for ","_key":"4cc1652320d5"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"cd7d51d11b95","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikke vert følgd.","_key":"2a39a3c4d9e3"}]}],"mappe":["OPPHØR"],"_updatedAt":"2023-09-25T10:35:51Z","borMedSokerTriggere":["DELT_BOSTED"],"periodeType":"INGEN_UTBETALING","begrunnelsetype":"OPPHØR","tema":"NASJONAL","_id":"f9fec97b-593e-401b-a58f-37febc02a5b8","bokmaal":[{"style":"normal","_key":"09b0fac55584","markDefs":[],"children":[{"_key":"046015ac2954","_type":"span","marks":[],"text":"Delt barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"d3de324b9cea"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at avtalen om delt bosted for ","_key":"1a87d8c8b0db"},{"_type":"valgfeltV2","_key":"89be7036f2b7","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"}},{"marks":[],"text":" ikke følges.","_key":"08705eecb882","_type":"span"}],"_type":"block"}],"navnISystem":"Avtale delt bosted følges ikke","vedtakResultat":"IKKE_INNVILGET","vilkaar":["BOR_MED_SOKER"],"_createdAt":"2021-11-05T10:27:07Z","apiNavn":"opphorAvtaleDeltBostedFolgesIkke","_rev":"FuD004taptHFqBZyEy8aMv","_type":"begrunnelse","ovrigeTriggere":["GJELDER_FØRSTE_PERIODE"],"hjemler":["2"],"visningsnavn":"35. Avtale delt bosted følges ikke","valgbarhet":"STANDARD"},{"visningsnavn":"41. Den andre forelderen var ikke medlem","_rev":"h26rAhFEYSUtDGXJE0v1ba","hjemlerFolketrygdloven":["2-5","2-8"],"vedtakResultat":"IKKE_INNVILGET","_createdAt":"2021-11-05T11:37:27Z","nynorsk":[{"style":"normal","_key":"7804fb60749b","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"d2674eb1d4ef"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"a5ba8ab5e8be"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikkje var medlem i folketrygda. Bur begge foreldra saman under opphaldet i utlandet, må de begge vere medlem i folketrygda for å få barnetrygd.","_key":"6374cde85f2e"}],"_type":"block"}],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"1e06f1e6ad14"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"7f1e6bb64534"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikke var medlem i folketrygden. Bor begge foreldrene sammen under oppholdet i utlandet, må dere begge være medlem i folketrygden for å få barnetrygd.","_key":"8acc70c8dc1a"}],"_type":"block","style":"normal","_key":"85ebeb9fd3f5","markDefs":[]}],"navnISystem":"Den andre forelderen var ikke medlem","apiNavn":"opphorDenAndreForelderenVarIkkeMedlem","hjemler":["4","5"],"vilkaar":["BOSATT_I_RIKET"],"periodeType":"INGEN_UTBETALING","tema":"NASJONAL","_type":"begrunnelse","rolle":["SOKER","BARN"],"valgbarhet":"STANDARD","mappe":["OPPHØR"],"begrunnelsetype":"OPPHØR","bosattIRiketTriggere":["MEDLEMSKAP"],"_id":"fa3a8ef1-4db9-41dd-bf11-0df6cecb83bd","_updatedAt":"2023-09-25T10:36:12Z","ovrigeTriggere":["GJELDER_FØRSTE_PERIODE"]},{"vilkaar":["BOSATT_I_RIKET"],"periodeType":"UTBETALING","rolle":["SOKER","BARN"],"tema":"NASJONAL","valgbarhet":"STANDARD","mappe":["INNVILGET"],"hjemler":["2","4","22"],"visningsnavn":"20. Søker og barn trygdeavtale","_updatedAt":"2023-09-25T10:15:52Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"df14c805bc0b"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"98f3b5900184"},{"_key":"34882ca67630","_type":"span","marks":[],"text":" under oppholdet i utlandet fordi "},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"bfae6718a01a"},{"text":" er medlem i folketrygden etter trygdeavtale fra ","_key":"f11a92291b21","_type":"span","marks":[]},{"_key":"b7c55f454661","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":". Trygdeavtalen gir rett til barnetrygd fra Norge.","_key":"1f1e942c1aeb"}],"_type":"block","style":"normal","_key":"de8a797137cb","markDefs":[]}],"behandlingstema":"NASJONAL","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","_createdAt":"2021-09-24T11:12:30Z","navnISystem":"Søker og barn trygdeavtale","apiNavn":"innvilgetSokerOgBarnTrygdeavtale","bosattIRiketTriggere":["MEDLEMSKAP"],"nynorsk":[{"_type":"block","style":"normal","_key":"429cd8661d9f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"ccd801e172c1"},{"_key":"e026f708061a","_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse"},{"_type":"span","marks":[],"text":" under opphaldet i utlandet fordi ","_key":"09c708ff822e"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"3ecc8c19404a"},{"_key":"0b4c26a74f90","_type":"span","marks":[],"text":" er medlem i folketrygda etter trygdeavtale frå "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"fbad7d5dfb19"},{"_key":"8d50dac24387","_type":"span","marks":[],"text":". Trygdeavtalen gjev rett til barnetrygd frå Noreg."}]}],"_id":"fac379cb-d24b-4078-8dd1-cce475744b99","_rev":"BtltdVb0HP4g4WJfnr4YPo","_type":"begrunnelse"},{"_type":"begrunnelse","vilkaar":["BOSATT_I_RIKET"],"rolle":["SOKER"],"tema":"NASJONAL","_createdAt":"2021-09-24T12:39:31Z","visningsnavn":"16. Vurdering søker og barn medlem","begrunnelsetype":"FORTSATT_INNVILGET","bosattIRiketTriggere":["MEDLEMSKAP"],"valgbarhet":"STANDARD","mappe":["FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:22:25Z","bokmaal":[{"_type":"block","style":"normal","_key":"18f769fa178c","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi vi har kommet fram til at du og ","_key":"ad9fced8d6b6"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"a2fb18061ff6"},{"text":" fortsatt fyller vilkårene for medlemskap i folketrygden under oppholdet i utlandet.","_key":"e6583c115e4f","_type":"span","marks":[]}]}],"navnISystem":"Vurdering søker og barn medlem","hjemlerFolketrygdloven":["2-5","2-8"],"periodeType":"FORTSATT_INNVILGET","_id":"fae6bf17-6826-4e55-a229-529983b995f4","apiNavn":"fortsattInnvilgetVurderingSokerOgBarnMedlem","hjemler":["4","5"],"_rev":"BtltdVb0HP4g4WJfnr4tOJ","vedtakResultat":"INGEN_ENDRING","nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd fordi vi har kome fram til at du og ","_key":"b9c031a00b27","_type":"span"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"34884c6090b8"},{"_key":"49c0aa09b440","_type":"span","marks":[],"text":" fortsatt fyller vilkåra for medlemskap i folketrygda under opphaldet i utlandet."}],"_type":"block","style":"normal","_key":"9f8a748a117e"}]},{"hjemler":["2","9","11"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"","_key":"1929bcacc287"},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"459c3073d745","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" utvida barnetrygd fordi du bur aleine med ","_key":"0ac836b77437"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"0c7af16f5387","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" etter at ektefellen din døydde ","_key":"cbc817002840"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"a8338ee9a3f8"},{"text":". ","_key":"afb9ff2f8350","_type":"span","marks":[]},{"skalHaStorForbokstav":true,"valgReferanse":{"_type":"reference","_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437"},"_type":"valgfeltV2","_key":"62740581c3f7"},{"_type":"span","marks":[],"text":" utvida barnetrygd frå månaden etter at du vart aleine.","_key":"449265ad3f0d"}],"_type":"block","style":"normal","_key":"719066453350","markDefs":[]}],"visningsnavn":"31. Ektefelle død","behandlingstema":"NASJONAL","_rev":"BtltdVb0HP4g4WJfnr4YPo","_type":"begrunnelse","periodeType":"UTBETALING","mappe":["INNVILGET"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"","_key":"b5f5f20af144"},{"_key":"06a5d3371d50","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" utvidet barnetrygd fordi du bor alene med ","_key":"12f812a380d2"},{"_type":"valgfeltV2","_key":"08123a33806d","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":" etter at ektefellen din døde ","_key":"c28608c4527f"},{"_type":"flettefelt","_key":"3060154bf872","flettefelt":"maanedOgAarBegrunnelsenGjelderFor"},{"_key":"f39bc6ed4aa4","_type":"span","marks":[],"text":". "},{"valgReferanse":{"_ref":"e8a4b865-71a0-4eb0-95e5-759d90b8c437","_type":"reference"},"_type":"valgfeltV2","_key":"5dce0fc06844","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" utvidet barnetrygd fra måneden etter at du ble alene.","_key":"bc0ab0f76c04"}],"_type":"block","style":"normal","_key":"df42850d9bf4","markDefs":[]}],"apiNavn":"innvilgetEktefelleDod","vilkaar":["UTVIDET_BARNETRYGD"],"navnISystem":"Ektefelle død","begrunnelsetype":"INNVILGET","tema":"FELLES","_createdAt":"2021-10-25T06:36:27Z","valgbarhet":"STANDARD","_id":"fbfca2c9-691a-4c04-9107-3b1076134468","_updatedAt":"2023-09-25T10:15:52Z"},{"apiNavn":"fortsattInnvilgetForsvunnetEktefelle","hjemler":["9"],"vilkaar":["UTVIDET_BARNETRYGD"],"periodeType":"FORTSATT_INNVILGET","_createdAt":"2021-10-25T10:20:23Z","mappe":["FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:23:17Z","navnISystem":"Forsvunnet ektefelle","visningsnavn":"29. Forsvunnet ektefelle","_rev":"BtltdVb0HP4g4WJfnr4yXo","vedtakResultat":"INGEN_ENDRING","valgbarhet":"STANDARD","_id":"ff5c4453-385e-40e8-b866-b92c9104e56a","bokmaal":[{"markDefs":[],"children":[{"text":"Du får utvidet barnetrygd fordi ektefellen din fortsatt er forsvunnet.","_key":"dab5d46a3242","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"c2226f04a784"}],"_type":"begrunnelse","begrunnelsetype":"FORTSATT_INNVILGET","tema":"FELLES","nynorsk":[{"style":"normal","_key":"60423a92daa2","markDefs":[],"children":[{"marks":[],"text":"Du får utvida barnetrygd fordi ektefellen din fortsatt er forsvunnen.","_key":"dc7775514d88","_type":"span"}],"_type":"block"}]},{"apiNavn":"opphorAnnenForelderIkkeLengerFrivilligMedlem","vedtakResultat":"IKKE_INNVILGET","bosattIRiketTriggere":["VURDERING_ANNET_GRUNNLAG","MEDLEMSKAP"],"bokmaal":[{"_key":"5070bd5bc136","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"d53e331b0310"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"15e39e6a7963"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikke er frivillig medlem i folketrygden under oppholdet i utlandet ","_key":"3fa9a9bb2a7d"},{"_type":"valgReferanse","_key":"7eb8abe56eea","_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d"},{"_type":"span","marks":[],"text":". Bor begge foreldrene sammen under oppholdet i utlandet, må dere begge være medlem i folketrygden for å få barnetrygd.","_key":"3f30301def39"}],"_type":"block","style":"normal"}],"_rev":"h26rAhFEYSUtDGXJE0ut18","_type":"begrunnelse","periodeType":"INGEN_UTBETALING","_createdAt":"2021-09-24T11:29:51Z","_id":"fff3eff3-1d42-4612-a07e-8266981b69e8","_updatedAt":"2023-09-25T10:34:33Z","navnISystem":"Annen forelder ikke frivillig medlem","hjemler":["4","5"],"visningsnavn":"19. Annen forelder ikke frivillig medlem","mappe":["OPPHØR"],"valgbarhet":"STANDARD","hjemlerFolketrygdloven":["2-8"],"vilkaar":["BOSATT_I_RIKET"],"rolle":["SOKER"],"begrunnelsetype":"OPPHØR","tema":"NASJONAL","nynorsk":[{"_key":"926b22769ac9","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"19db3f7bda21"},{"_key":"3657f4e2c00f","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikkje er frivillig medlem i folketrygda under opphaldet i utlandet ","_key":"b8dac3c97aa8"},{"_ref":"06fffef6-ae9a-47e4-8399-b034d86d0c0d","_type":"valgReferanse","_key":"98c493453595"},{"_type":"span","marks":[],"text":". Bur begge foreldre saman under opphaldet i utlandet, må begge vere medlem i folketrygda for å få barnetrygd.","_key":"1c6c81759beb"}],"_type":"block","style":"normal"}]},{"apiNavn":"reduksjonEndretMottaker","visningsnavn":"13. Foreldrene bor sammen, endret mottaker","vedtakResultat":"REDUKSJON","vilkaar":["BOR_MED_SOKER"],"_createdAt":"2021-07-27T08:07:24Z","bokmaal":[{"_key":"foreldreneBorSammenEndretMottaker1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden endres fordi den andre forelderen har søkt om barnetrygd for barn født ","_key":"foreldreneBorSammenEndretMottaker2"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"4edfbc9f112d"},{"_key":"49e180c16891","_type":"span","marks":[],"text":"."}],"_type":"block","style":"normal"}],"navnISystem":"Foreldrene bor sammen, endret mottaker","_type":"begrunnelse","mappe":["REDUKSJON"],"_updatedAt":"2023-09-25T10:15:54Z","hjemler":["2","12"],"_rev":"h26rAhFEYSUtDGXJE0sX1S","begrunnelsetype":"REDUKSJON","nynorsk":[{"_type":"block","style":"normal","_key":"foreldreneBorSammenEndretMottaker1","markDefs":[],"children":[{"marks":[],"text":"Barnetrygda er endra fordi den andre forelderen har søkt om barnetrygd for barn fødd ","_key":"foreldreneBorSammenEndretMottaker2","_type":"span"},{"_type":"flettefelt","_key":"5e3a95c95c73","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":".","_key":"0212885682f6"}]}],"valgbarhet":"STANDARD","_id":"foreldreneBorSammenEndretMottaker","borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"periodeType":"UTBETALING","tema":"FELLES"},{"hjemler":["17","18"],"mappe":["REDUKSJON"],"_rev":"h26rAhFEYSUtDGXJE0sX1S","_id":"ikkeMottattOpplysninger","navnISystem":"Ikke mottatt opplysninger","begrunnelsetype":"REDUKSJON","nynorsk":[{"style":"normal","_key":"ikkeMottattOpplysninger1","markDefs":[],"children":[{"marks":[],"text":"Barnetrygda er endra fordi du ikkje har sendt oss dei opplysningane vi ba om.","_key":"ikkeMottattOpplysninger2","_type":"span"}],"_type":"block"}],"bokmaal":[{"children":[{"_key":"ikkeMottattOpplysninger2","_type":"span","marks":[],"text":"Barnetrygden endres fordi du ikke har sendt oss de opplysningene vi ba om."}],"_type":"block","style":"normal","_key":"ikkeMottattOpplysninger1","markDefs":[]}],"ovrigeTriggere":["MANGLER_OPPLYSNINGER"],"apiNavn":"reduksjonManglendeOpplysninger","visningsnavn":"7. Ikke mottatt opplysninger","_type":"begrunnelse","vedtakResultat":"REDUKSJON","periodeType":"UTBETALING","tema":"NASJONAL","_createdAt":"2021-08-20T14:50:35Z","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:54Z"},{"navnISystem":"Medlem i Folketrygden","hjemler":["2","4","11"],"behandlingstema":"NASJONAL","_rev":"h26rAhFEYSUtDGXJE0sX1S","vedtakResultat":"INNVILGET_ELLER_ØKNING","rolle":["SOKER","BARN"],"bosattIRiketTriggere":["MEDLEMSKAP"],"_createdAt":"2021-07-27T08:07:24Z","bokmaal":[{"children":[{"_key":"medlemIFolketrygden2","_type":"span","marks":[],"text":"Du får barnetrygd fordi du er medlem av folketrygden fra "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"8866ae8efcd2"},{"_type":"span","marks":[],"text":".","_key":"264fb9980095"}],"_type":"block","style":"normal","_key":"medlemIFolketrygden1","markDefs":[]}],"hjemlerFolketrygdloven":["2-5"],"periodeType":"UTBETALING","tema":"NASJONAL","visningsnavn":"14. Medlem i folketrygden","_type":"begrunnelse","nynorsk":[{"_key":"medlemIFolketrygden1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du er medlem av folketrygda frå ","_key":"medlemIFolketrygden2"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"3483f81dfeca"},{"_type":"span","marks":[],"text":".","_key":"e0d1bbd8f4ee"}],"_type":"block","style":"normal"}],"_id":"medlemIFolketrygden","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:54Z","apiNavn":"innvilgetMedlemIFolketrygden","vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD"},{"mappe":["INNVILGET"],"bokmaal":[{"style":"normal","_key":"norskNordiskBosattINorge1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"norskNordiskBosattINorge2"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"9b9d44210666"},{"_type":"span","marks":[],"text":" fordi ","_key":"da436f778745"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"2c706ad6e516"},{"_type":"span","marks":[],"text":" er bosatt i Norge fra ","_key":"074f79afb020"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"6ec7093afdf2"},{"text":".","_key":"c1eaf6f27d5a","_type":"span","marks":[]}],"_type":"block"}],"_type":"begrunnelse","tema":"NASJONAL","valgbarhet":"STANDARD","_id":"norskNordiskBosattINorge","apiNavn":"innvilgetBosattIRiket","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"norskNordiskBosattINorge2"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"a9ce5463fef4"},{"_type":"span","marks":[],"text":" fordi ","_key":"b2bd85cea902"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"72e69078e19e"},{"text":" er busett i Noreg frå ","_key":"e82a1a1ed2af","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"09b807a2c05b"},{"text":".","_key":"1221ac0d03a9","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"norskNordiskBosattINorge1"}],"periodeType":"UTBETALING","rolle":["SOKER","BARN"],"_updatedAt":"2023-09-25T08:12:17Z","navnISystem":"Norsk, nordisk bosatt i Norge","_rev":"FuD004taptHFqBZyExrsmX","behandlingstema":"NASJONAL","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"INNVILGET","_createdAt":"2021-07-27T08:07:24Z","hjemler":["2","4","11"],"visningsnavn":"1. Norsk, nordisk bosatt i Norge"},{"visningsnavn":"11. Nyfødt barn - første barn","_type":"begrunnelse","vilkaar":["UNDER_18_ÅR"],"nynorsk":[{"style":"normal","_key":"nyfodtBarnForsteBarn1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har fått barn og ","_key":"nyfodtBarnForsteBarn2"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"6d461db5ab2f"},{"_type":"span","marks":[],"text":" bur saman med deg. Du får barnetrygda frå månaden etter at ","_key":"b50c957df89d"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"a770eafc5efc"},{"_type":"span","marks":[],"text":" er fødd.","_key":"8397ba87fa16"}],"_type":"block"}],"_updatedAt":"2023-09-25T10:15:54Z","hjemler":["2","4","11"],"mappe":["INNVILGET"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi du har fått barn og ","_key":"nyfodtBarnForsteBarn2"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"80a5b8381dec"},{"_type":"span","marks":[],"text":" bor sammen med deg. Du får barnetrygden fra måneden etter at ","_key":"f51b9c832bff"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"380eb3313005"},{"text":" er født.","_key":"ee4120587fe0","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"nyfodtBarnForsteBarn1","markDefs":[]}],"begrunnelsetype":"INNVILGET","_rev":"h26rAhFEYSUtDGXJE0sX1S","navnISystem":"Nyfødt barn - første barn","behandlingstema":"NASJONAL","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","tema":"NASJONAL","_createdAt":"2021-07-27T08:07:24Z","valgbarhet":"STANDARD","_id":"nyfodtBarnForsteBarn","apiNavn":"innvilgetNyfodtBarnForste"},{"vilkaar":["UNDER_18_ÅR"],"periodeType":"UTBETALING","_createdAt":"2021-07-27T08:07:24Z","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:54Z","navnISystem":"Nyfødt barn - har barn fra før","_rev":"h26rAhFEYSUtDGXJE0sX1S","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får meir barnetrygd fordi du har fått nytt barn, og barna bur saman med deg. Du får barnetrygda frå månaden etter at ","_key":"nyfodtBarnHarBarnFraFor2"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"809a999c18a9"},{"_type":"span","marks":[],"text":" er fødd.","_key":"4ddda05072e8"}],"_type":"block","style":"normal","_key":"nyfodtBarnHarBarnFraFor1","markDefs":[]}],"valgbarhet":"STANDARD","_id":"nyfodtBarnHarBarnFraFor","bokmaal":[{"style":"normal","_key":"nyfodtBarnHarBarnFraFor1","markDefs":[],"children":[{"_key":"nyfodtBarnHarBarnFraFor2","_type":"span","marks":[],"text":"Du får mer barnetrygd fordi du har fått nytt barn, og barna bor sammen med deg. Du får barnetrygden fra måneden etter at "},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"74544ab68550"},{"marks":[],"text":" ble født.","_key":"358fe1af79fd","_type":"span"}],"_type":"block"}],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","apiNavn":"innvilgetNyfodtBarn","hjemler":["2","4","11"],"visningsnavn":"12. Nyfødt barn - har barn fra før","begrunnelsetype":"INNVILGET","tema":"NASJONAL"},{"_id":"satsendring","hjemler":["2","10"],"vedtakResultat":"REDUKSJON","tema":"FELLES","bokmaal":[{"children":[{"text":"Barnetrygden er endret fordi det har vært en satsendring.","_key":"satsendring2","_type":"span"}],"_type":"block","style":"normal","_key":"satsendring1","markDefs":[]}],"apiNavn":"reduksjonSatsendring","_createdAt":"2021-07-27T08:07:24Z","valgbarhet":"AUTOMATISK","mappe":["REDUKSJON"],"ovrigeTriggere":["SATSENDRING"],"navnISystem":"Satsendring","begrunnelsetype":"REDUKSJON","nynorsk":[{"markDefs":[],"children":[{"_type":"span","text":"Barnetrygda er endra fordi det har vore ei satsendring.","_key":"satsendring2"}],"_type":"block","style":"normal","_key":"satsendring1"}],"periodeType":"UTBETALING","_updatedAt":"2023-09-25T10:15:54Z","visningsnavn":"12. Satsendring","_rev":"h26rAhFEYSUtDGXJE0sX1S","_type":"begrunnelse"},{"_rev":"h26rAhFEYSUtDGXJE0sX1S","tema":"NASJONAL","_createdAt":"2021-07-27T08:07:24Z","mappe":["INNVILGET"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"skjonnsmessigVurderingBarnHarFlyttetTilSokerFlyttingMellomForeldreAndreOmsorgspersoner2"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"06f93d677a83"},{"_key":"a11d399bb860","_type":"span","marks":[],"text":" fordi vi har kommet fram til at "},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"b6cc2298cc7c"},{"_type":"span","marks":[],"text":" bor fast hos deg fra ","_key":"131091ba0433"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"44513d2b9c48"},{"_key":"cbd4bdc3a459","_type":"span","marks":[],"text":"."}],"_type":"block","style":"normal","_key":"skjonnsmessigVurderingBarnHarFlyttetTilSokerFlyttingMellomForeldreAndreOmsorgspersoner1","markDefs":[]}],"visningsnavn":"5. Skjønnsmessig vurdering - Barn har flyttet til søker (flytting mellom foreldre, andre omsorgspersoner)","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"_id":"skjonnsmessigVurderingBarnHarFlyttetTilSokerFlyttingMellomForeldreAndreOmsorgspersoner","hjemler":["2","4","11"],"nynorsk":[{"style":"normal","_key":"skjonnsmessigVurderingBarnHarFlyttetTilSokerFlyttingMellomForeldreAndreOmsorgspersoner1","markDefs":[],"children":[{"_key":"skjonnsmessigVurderingBarnHarFlyttetTilSokerFlyttingMellomForeldreAndreOmsorgspersoner2","_type":"span","marks":[],"text":"Du får barnetrygd "},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"576cbb740cc5"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at ","_key":"f817f9f740a5"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"62b985ce43a7"},{"_type":"span","marks":[],"text":" bur fast hos deg frå ","_key":"0c5898c43ef0"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"2790425c02b2"},{"_key":"6e54679124fc","_type":"span","marks":[],"text":"."}],"_type":"block"}],"navnISystem":"Skjønnsmessig vurdering - Barn har flyttet til søker (flytting mellom foreldre, andre omsorgspersoner)","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:54Z","apiNavn":"innvilgetBorHosSokerSkjonnsmessig"},{"mappe":["INNVILGET"],"apiNavn":"innvilgetLovligOppholdSkjonnsmessigVurderingTredjelandsborger","behandlingstema":"NASJONAL","periodeType":"UTBETALING","rolle":["SOKER","BARN"],"nynorsk":[{"_type":"block","style":"normal","_key":"skjonnsmessigVurderingTaaltOppholdTredjelandsborger1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi vi har kome fram til at ","_key":"skjonnsmessigVurderingTaaltOppholdTredjelandsborger2"},{"_key":"b9bb059aac09","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse"},{"_type":"span","marks":[],"text":" har opphaldsrett frå ","_key":"c5b4361dca4a"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"cc70fbb9d99d"},{"_type":"span","marks":[],"text":".","_key":"affc403447b2"}]}],"lovligOppholdTriggere":["VURDERING_ANNET_GRUNNLAG"],"_id":"skjonnsmessigVurderingTaaltOppholdTredjelandsborger","bokmaal":[{"_type":"block","style":"normal","_key":"skjonnsmessigVurderingTaaltOppholdTredjelandsborger1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fordi vi har kommet fram til at ","_key":"skjonnsmessigVurderingTaaltOppholdTredjelandsborger2"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"a1ba05230073"},{"marks":[],"text":" har oppholdsrett fra ","_key":"6c244f98e1a6","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"b92e9c4bbc2a"},{"_type":"span","marks":[],"text":".","_key":"1e9628bbbbd9"}]}],"navnISystem":"Skjønnsmessig vurdering tålt opphold tredjelandsborger","visningsnavn":"13. Skjønnsmessig vurdering tålt opphold tredjelandsborger","_rev":"h26rAhFEYSUtDGXJE0sX1S","vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"NASJONAL","_type":"begrunnelse","begrunnelsetype":"INNVILGET","_createdAt":"2021-07-27T08:07:24Z","_updatedAt":"2023-09-25T10:15:54Z","hjemler":["2","4","11"],"vilkaar":["LOVLIG_OPPHOLD"],"valgbarhet":"STANDARD"},{"periodeType":"UTBETALING","_createdAt":"2021-07-27T08:07:24Z","hjemler":["2","4","11"],"borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"valgbarhet":"STANDARD","navnISystem":"Søker har fast omsorg for barn (beredskapshjem, vurdering av fast bosted)","visningsnavn":"6. Søker har fast omsorg for barn (beredskapshjem, vurdering av fast bosted)","_rev":"h26rAhFEYSUtDGXJE0sX1S","_type":"begrunnelse","tema":"NASJONAL","nynorsk":[{"style":"normal","_key":"sokerHarFastOmsorgForBarnBeredskapshjemVurderingAvFastBosted1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"sokerHarFastOmsorgForBarnBeredskapshjemVurderingAvFastBosted2"},{"_type":"valgReferanse","_key":"947ba3d1a76e","_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at du har fått fast omsorg for ","_key":"9a02f2456a06"},{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"valgReferanse","_key":"de092398fd1f"},{"_type":"span","marks":[],"text":" frå ","_key":"bbefef03322e"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"37e6c70bcaf9"},{"_key":"98f103ef5d99","_type":"span","marks":[],"text":"."}],"_type":"block"}],"_updatedAt":"2023-09-25T10:15:54Z","apiNavn":"innvilgetFastOmsorgForBarn","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["BOR_MED_SOKER"],"begrunnelsetype":"INNVILGET","_id":"sokerHarFastOmsorgForBarnBeredskapshjemVurderingAvFastBosted","mappe":["INNVILGET"],"bokmaal":[{"style":"normal","_key":"sokerHarFastOmsorgForBarnBeredskapshjemVurderingAvFastBosted1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"sokerHarFastOmsorgForBarnBeredskapshjemVurderingAvFastBosted2"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"16ff8c46b3e4"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at du har fått fast omsorg for ","_key":"b451b52e54b8"},{"_type":"valgReferanse","_key":"02889e0343cb","_ref":"df8dc282-2637-4047-a656-8527205dc364"},{"_type":"span","marks":[],"text":" fra ","_key":"abac1b63dd45"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"2b989b05ef39"},{"marks":[],"text":".","_key":"85131630fea6","_type":"span"}],"_type":"block"}]},{"vilkaar":["BOR_MED_SOKER"],"borMedSokerTriggere":["VURDERING_ANNET_GRUNNLAG"],"_updatedAt":"2023-09-25T10:15:54Z","_rev":"h26rAhFEYSUtDGXJE0sX1S","hjemler":["2","11"],"vedtakResultat":"REDUKSJON","begrunnelsetype":"REDUKSJON","valgbarhet":"STANDARD","_id":"sokerHarIkkeLengerFastOmsorgForBarnBeredskapshjemVurderingAvFastBosted","apiNavn":"reduksjonFastOmsorgForBarn","_type":"begrunnelse","tema":"NASJONAL","nynorsk":[{"_type":"block","style":"normal","_key":"sokerHarIkkeLengerFastOmsorgForBarnBeredskapshjemVurderingAvFastBosted1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi vi har kome fram til at barn fødd ","_key":"sokerHarIkkeLengerFastOmsorgForBarnBeredskapshjemVurderingAvFastBosted2"},{"_key":"c718420cf343","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":" ikkje lenger bur fast hos deg frå ","_key":"a34591ee3a81"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"dfa4cf53401c"},{"text":".","_key":"f61be1883f19","_type":"span","marks":[]}]}],"bokmaal":[{"markDefs":[],"children":[{"text":"Barnetrygden endres fordi vi har kommet fram til at barn født ","_key":"sokerHarIkkeLengerFastOmsorgForBarnBeredskapshjemVurderingAvFastBosted2","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"5d381a7ae1c6"},{"_key":"a1ac74d185df","_type":"span","marks":[],"text":" ikke lenger bor fast hos deg fra "},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"d45c4c4800b4"},{"text":".","_key":"ccdda6b27db9","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"sokerHarIkkeLengerFastOmsorgForBarnBeredskapshjemVurderingAvFastBosted1"}],"visningsnavn":"5. Søker har ikke lenger fast omsorg for barn (beredskapshjem, vurdering av fast bosted)","periodeType":"UTBETALING","_createdAt":"2021-08-20T14:50:35Z","mappe":["REDUKSJON"],"navnISystem":"Søker har ikke lenger fast omsorg for barn (beredskapshjem, vurdering av fast bosted)"},{"hjemler":["2","4","11"],"periodeType":"UTBETALING","valgbarhet":"STANDARD","_id":"tredjelandsborgerBosattForLovligOppholdINorge","bokmaal":[{"style":"normal","_key":"tredjelandsborgerBosattForLovligOppholdINorge1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"tredjelandsborgerBosattForLovligOppholdINorge2"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"cb79b0c4e791"},{"_type":"span","marks":[],"text":" fordi ","_key":"9f55c66bfc1f"},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"6e95d02aa95a"},{"_type":"span","marks":[],"text":" har oppholdstillatelse fra ","_key":"0098cdc280ad"},{"_key":"803362cccbd6","flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt"},{"_type":"span","marks":[],"text":".","_key":"90e68b309b02"}],"_type":"block"}],"behandlingstema":"NASJONAL","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","mappe":["INNVILGET"],"_updatedAt":"2023-09-25T10:15:54Z","navnISystem":"Tredjelandsborger bosatt før lovlig opphold i Norge","apiNavn":"innvilgetLovligOppholdOppholdstillatelse","_type":"begrunnelse","rolle":["SOKER","BARN"],"visningsnavn":"2. Tredjelandsborger bosatt før lovlig opphold i Norge","_rev":"h26rAhFEYSUtDGXJE0sX1S","vilkaar":["LOVLIG_OPPHOLD"],"tema":"NASJONAL","nynorsk":[{"_key":"tredjelandsborgerBosattForLovligOppholdINorge1","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd ","_key":"tredjelandsborgerBosattForLovligOppholdINorge2","_type":"span"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"d86dd0e474be"},{"_key":"29589a04b2bd","_type":"span","marks":[],"text":" fordi "},{"_type":"valgReferanse","_key":"2cc931c02180","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},{"_type":"span","marks":[],"text":" har opphaldsløyve frå ","_key":"c86a06207985"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"bf93288bf72e"},{"_type":"span","marks":[],"text":".","_key":"b96de4cb5717"}],"_type":"block","style":"normal"}],"_createdAt":"2021-07-27T08:07:24Z"},{"bokmaal":[{"style":"normal","_key":"tredjelandsborgerMedLovligOppholdSamtidigSomBosattINorge1","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd ","_key":"tredjelandsborgerMedLovligOppholdSamtidigSomBosattINorge2","_type":"span"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"92eba39c388d"},{"_key":"e5dc1661a734","_type":"span","marks":[],"text":" fordi "},{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"valgReferanse","_key":"0b5a54bd4484"},{"marks":[],"text":" er bosatt i Norge og har oppholdstillatelse fra ","_key":"1e4fe42e685f","_type":"span"},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"1c3988201482"},{"_type":"span","marks":[],"text":".","_key":"619bae503756"}],"_type":"block"}],"visningsnavn":"1A. Tredjelandsborger med lovlig opphold samtidig som bosatt i Norge","_rev":"BtltdVb0HP4g4WJfnqyHtJ","_type":"begrunnelse","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd ","_key":"tredjelandsborgerMedLovligOppholdSamtidigSomBosattINorge2"},{"_ref":"990fb3af-f654-4f78-bcc9-16caaaa99170","_type":"valgReferanse","_key":"beb018567d5f"},{"_type":"span","marks":[],"text":" fordi ","_key":"52fd254ed677"},{"_type":"valgReferanse","_key":"588ac29edae2","_ref":"49509c87-0882-429c-9604-f4fbfc5876ca"},{"text":" er busett i Noreg og har opphaldsløyve frå ","_key":"eeddbc8b9f31","_type":"span","marks":[]},{"flettefelt":"maanedOgAarBegrunnelsenGjelderFor","_type":"flettefelt","_key":"a76122c0138d"},{"marks":[],"text":".","_key":"bcebaeed5652","_type":"span"}],"_type":"block","style":"normal","_key":"tredjelandsborgerMedLovligOppholdSamtidigSomBosattINorge1","markDefs":[]}],"mappe":["INNVILGET"],"apiNavn":"innvilgetBosattIRiketLovligOpphold","hjemler":["2","4","11"],"begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","rolle":["SOKER","BARN"],"tema":"NASJONAL","_updatedAt":"2023-09-25T08:12:19Z","navnISystem":"Tredjelandsborger med lovlig opphold samtidig som bosatt i Norge","vedtakResultat":"INNVILGET_ELLER_ØKNING","vilkaar":["LOVLIG_OPPHOLD"],"periodeType":"UTBETALING","behandlingstema":"NASJONAL","_createdAt":"2021-07-27T08:07:24Z","_id":"tredjelandsborgerMedLovligOppholdSamtidigSomBosattINorge"},{"_id":"uenighetOmOpphorAvAvtaleOmDeltBosted","_updatedAt":"2023-09-25T10:15:54Z","hjemler":["2","11"],"visningsnavn":"9. Uenighet om opphør av avtale om delt bosted","begrunnelsetype":"REDUKSJON","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du og den andre forelderen er usamde om avtalen om delt bustad. Vi har kome fram til at avtalen om delt bustad for barn fødd ","_key":"uenighetOmOpphorAvAvtaleOmDeltBosted2"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"646cf36b03b7"},{"_key":"fd222119b7cd","_type":"span","marks":[],"text":" ikkje lenger vert følgd."}],"_type":"block","style":"normal","_key":"uenighetOmOpphorAvAvtaleOmDeltBosted1","markDefs":[]},{"markDefs":[],"children":[{"_key":"52ef657d89c9","_type":"span","marks":[],"text":"\nNår de er usamde om avtalen om delt bustad, kan vi opphøyre barnetrygda til deg frå og med månaden etter at vi fekk søknad om full barnetrygd."}],"_type":"block","style":"normal","_key":"1a56b75cdab7"}],"_createdAt":"2021-07-27T08:07:24Z","valgbarhet":"STANDARD","_rev":"h26rAhFEYSUtDGXJE0sX1S","vilkaar":["BOR_MED_SOKER"],"periodeType":"UTBETALING","bokmaal":[{"markDefs":[],"children":[{"_key":"uenighetOmOpphorAvAvtaleOmDeltBosted2","_type":"span","marks":[],"text":"Du og den andre forelderen er uenige om avtalen om delt bosted. Vi har kommet fram til at avtalen om delt bosted for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"d76f7cdf5d15"},{"_type":"span","marks":[],"text":" ikke lenger følges. ","_key":"deddc4c517cc"}],"_type":"block","style":"normal","_key":"uenighetOmOpphorAvAvtaleOmDeltBosted1"},{"_key":"7dbf0950c2fa","markDefs":[],"children":[{"_type":"span","marks":[],"text":"\nVed uenighet mellom foreldrene om avtalen om delt bosted, kan barnetrygden opphøres fra måneden etter at vi fikk søknad om full barnetrygd.","_key":"09b41a58d2ef"}],"_type":"block","style":"normal"}],"apiNavn":"reduksjonDeltBostedUenighet","vedtakResultat":"REDUKSJON","tema":"NASJONAL","navnISystem":"Uenighet om opphør av avtale om delt bosted","_type":"begrunnelse","borMedSokerTriggere":["DELT_BOSTED"],"mappe":["REDUKSJON"]}] \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/restSanityE\303\230SBegrunnelser" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/restSanityE\303\230SBegrunnelser" new file mode 100644 index 000000000..858898579 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/restSanityE\303\230SBegrunnelser" @@ -0,0 +1 @@ +[{"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","_createdAt":"2022-05-23T12:52:32Z","triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","mappe":["EØS","INNVILGET"],"hjemler":["2","4","11"],"hjemlerEOSForordningen883":["2","11-16","67","68"],"hjemlerSeperasjonsavtalenStorbritannina":["29"],"begrunnelsetype":"INNVILGET","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"3b6c12fc362c0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"c4565bc437b4"},{"_type":"span","marks":[],"text":". Du ","_key":"2e3bdbf79ce1"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"1277d956784d","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"078409a1ea3b"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"dfb07494d3a6","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bur i ","_key":"484fd667771a"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"b04c0ff86136"},{"marks":[],"text":". Den andre forelderen jobbar ikkje i ","_key":"421e77fff18c","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"04f928a96c0e"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen får du heile barnetrygda frå Noreg.","_key":"7bcb5edf81f0"}],"_type":"block","style":"normal","_key":"011da4e44501","markDefs":[]}],"_updatedAt":"2023-09-25T10:15:44Z","annenForeldersAktivitet":["MOTTAR_PENSJON","INAKTIV"],"navnISystem":"Primærland UK standard","behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_id":"05f451e5-75ec-4a86-b490-0d946ca9597b","barnetsBostedsland":["NORGE","IKKE_NORGE"],"apiNavn":"innvilgetPrimarlandUKStandard","visningsnavn":"5. Primærland UK standard","_rev":"BtltdVb0HP4g4WJfnr4V7o","tema":"EØS","bokmaal":[{"_type":"block","style":"normal","_key":"e8bcc6aa0522","markDefs":[],"children":[{"_key":"25853463a9460","_type":"span","marks":[],"text":"Du får barnetrygd for barn født "},{"_type":"eosFlettefelt","_key":"e9a7f538e926","flettefelt":"barnasFodselsdatoer"},{"marks":[],"text":". Du ","_key":"4997ff478100","_type":"span"},{"_key":"f11834df4094","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". ","_key":"fcf7dd70d5bc"},{"_key":"b554cfe215e3","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" bor i ","_key":"4cc02396635f"},{"_type":"eosFlettefelt","_key":"1ff12f789073","flettefelt":"barnetsBostedsland"},{"_type":"span","marks":[],"text":". Den andre forelderen jobber ikke i ","_key":"d9999f5e8173"},{"_type":"eosFlettefelt","_key":"ea85b24a8776","flettefelt":"annenForeldersAktivitetsland"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen får du hele barnetrygden fra Norge.","_key":"fa2dc6b356db"}]}]},{"visningsnavn":"8. Primærland barnet bor i Norge","hjemlerEOSForordningen883":["2","11-16","67","68"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"apiNavn":"innvilgetPrimarlandBarnetBorINorge","hjemler":["2","4","11"],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"valgbarhet":"STANDARD","barnetsBostedsland":["NORGE"],"bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"78f8b0000cf20","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"bffbf96931c0"},{"_type":"span","marks":[],"text":". Du ","_key":"3d1852245b69"},{"_type":"valgfeltV2","_key":"6d694d9717a0","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb"}},{"text":". ","_key":"4bbc80846b8a","_type":"span","marks":[]},{"_key":"18d59471b88c","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"text":" bor sammen med deg i Norge. Den andre forelderen ","_key":"2c0065f2984d","_type":"span","marks":[]},{"_key":"0f1783ab384e","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" i ","_key":"d7fa39147044"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"a721441d92fc"},{"marks":[],"text":". Du får derfor hele barnetrygden fra Norge.","_key":"6025b9fa0db7","_type":"span"}],"_type":"block","style":"normal","_key":"1b8aeb9c5ae8"}],"navnISystem":"Primærland barnet bor i Norge","behandlingstema":"EØS","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","triggereIBruk":["KOMPETANSE"],"_id":"0d83eeb0-d625-4c94-9585-9e00466ddfac","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","_createdAt":"2022-05-23T12:57:47Z","mappe":["EØS","INNVILGET"],"_updatedAt":"2023-09-25T10:15:44Z","tema":"EØS","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"f8e22ebcfbfe0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"3f5b3fa0a442"},{"_type":"span","marks":[],"text":". Du ","_key":"9b38c6cd1db2"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"9c0f43fb8b3a"},{"_key":"249c04d56726","_type":"span","marks":[],"text":". "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"eeeacfca825b","skalHaStorForbokstav":true},{"text":" bur saman med deg i Noreg. Den andre forelderen ","_key":"82406b41892a","_type":"span","marks":[]},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"64306346e22c","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"3aaec12d3d84"},{"_type":"eosFlettefelt","_key":"c266ac2c595d","flettefelt":"annenForeldersAktivitetsland"},{"marks":[],"text":". Du får derfor heile barnetrygda frå Noreg.","_key":"c09eadff90f9","_type":"span"}],"_type":"block","style":"normal","_key":"26a3591af479"}]},{"periodeType":"INGEN_UTBETALING","_createdAt":"2022-10-17T11:57:21Z","hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"OPPHØR","navnISystem":"Søker og barn bor ikke i EØS-land","hjemler":["2","4","11"],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND","TO_PRIMÆRLAND"],"_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"mappe":["EØS","OPPHØR"],"visningsnavn":"9. Søker og barn bor ikke i EØS land","_rev":"BtltdVb0HP4g4WJfnr5vkJ","tema":"EØS","nynorsk":[{"style":"normal","_key":"c78c6e25906e","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"162ddb6d98c70"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"d16357e2c766"},{"text":" fordi ","_key":"b0053cf11ed8","_type":"span","marks":[]},{"_type":"valgfeltV2","_key":"9cf9d8ab2108","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"}},{"_type":"span","marks":[],"text":" ikkje lenger bur i eit EØS-land.","_key":"16a532a87061"}],"_type":"block"}],"triggereIBruk":["KOMPETANSE"],"_id":"13281470-2d24-4b6a-9b9b-7d0d0205ec0e","_updatedAt":"2023-09-25T10:41:44Z","bokmaal":[{"_key":"8c6d6d83dda6","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"ae5055f4cced0","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"882e29047596"},{"text":" fordi ","_key":"211ca5b9f96f","_type":"span","marks":[]},{"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"3464100cf57c","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikke lenger bor i et EØS-land.","_key":"0f61f15a54a1"}],"_type":"block","style":"normal"}],"apiNavn":"opphorSoekerOgBarnBorIkkeIEosLand","valgbarhet":"STANDARD"},{"_createdAt":"2022-08-12T12:25:31Z","valgbarhet":"STANDARD","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"apiNavn":"opphorBorIkkeIEtEOSland","_rev":"FuD004taptHFqBZyEy94OX","barnetsBostedsland":["NORGE","IKKE_NORGE"],"bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"8c1fff496bfb0","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"7306a4e515c3"},{"text":" fordi ","_key":"826bcac1ba60","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"0f5a5016783b","skalHaStorForbokstav":false},{"_key":"d96f4bb74679","_type":"span","marks":[],"text":" ikke lenger bor i et EØS-land."}],"_type":"block","style":"normal","_key":"eb4397678581"}],"navnISystem":"Barn bor ikke i et EØS land","_id":"146a8e94-81bc-491a-8f6d-a48fa0a1c341","begrunnelsetype":"OPPHØR","mappe":["EØS","OPPHØR"],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND","TO_PRIMÆRLAND"],"_type":"begrunnelse","behandlingstema":"EØS","vedtakResultat":"IKKE_INNVILGET","hjemlerEOSForordningen883":["2","11-16","67"],"periodeType":"INGEN_UTBETALING","tema":"EØS","nynorsk":[{"style":"normal","_key":"520e9561e138","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"07fe37dfa1270"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"b82ab4e4f9eb"},{"_type":"span","marks":[],"text":" fordi ","_key":"3d1c942a382d"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"34e32c4e070f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" ikkje lenger bur i eit EØS-land.","_key":"124c1aa163ea"}],"_type":"block"}],"hjemler":["2","4","11"],"visningsnavn":"3. Barn bor ikke i et EØS land","triggereIBruk":["KOMPETANSE"],"_updatedAt":"2023-09-25T10:41:23Z"},{"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"visningsnavn":"5. Separasjonsavtalen gjelder ikke","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND"],"vedtakResultat":"IKKE_INNVILGET","_createdAt":"2022-08-12T12:33:56Z","valgbarhet":"STANDARD","barnetsBostedsland":["NORGE","IKKE_NORGE"],"_updatedAt":"2023-09-25T10:41:30Z","apiNavn":"opphorSeparasjonsavtaleGjelderIkke","_rev":"BtltdVb0HP4g4WJfnr5vDJ","periodeType":"INGEN_UTBETALING","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"8f3430750f730"},{"_type":"eosFlettefelt","_key":"3879217ffc60","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi separasjonsavtalen mellom Storbritannia og Noreg ikke lenger gjeld for familien.","_key":"450817814f23"}],"_type":"block","style":"normal","_key":"45899ab75ad4"}],"mappe":["EØS","OPPHØR"],"bokmaal":[{"style":"normal","_key":"a0f07f3391a5","markDefs":[],"children":[{"text":"Barnetrygd for barn født ","_key":"e54723d9f9700","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"08bc5ddad114"},{"_type":"span","marks":[],"text":" fordi separasjonsavtalen mellom Storbritannia og Norge ikke lenger gjelder for familien.","_key":"8087fa892488"}],"_type":"block"}],"hjemler":["2","4","11"],"behandlingstema":"EØS","triggereIBruk":["KOMPETANSE"],"_id":"1b29f277-28f9-40cd-804a-9f745ff8fab8","tema":"EØS","navnISystem":"Separasjonsavtalen gjelder ikke","_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67"],"begrunnelsetype":"OPPHØR"},{"hjemler":["2","4","12"],"behandlingstema":"EØS","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","apiNavn":"innvilgetPrimarlandBarnetrygdAlleredeUtbetalt","triggereIBruk":["KOMPETANSE"],"_id":"1c983aec-d505-4117-bb2f-c2cb067a40fa","_updatedAt":"2023-09-25T10:15:44Z","nynorsk":[{"_key":"af6bd523e36d","markDefs":[],"children":[{"text":"Du får barnetrygd frå same tidspunkt som utbetalinga til den andre forelderen er stansa. ","_key":"04eaa72adfec0","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"_createdAt":"2022-05-23T13:03:44Z","barnetsBostedsland":["NORGE","IKKE_NORGE"],"visningsnavn":"19. Primærland barnetrygd allerede utbetalt","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"EØS","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"bokmaal":[{"style":"normal","_key":"6a43dea1bf5c","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd fra samme tidspunkt som utbetalingen til den andre forelderen er stanset. ","_key":"b03e31253d590"}],"_type":"block"}],"navnISystem":"Barnetrygd allerede utbetalt","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND","TO_PRIMÆRLAND"],"hjemlerEOSForordningen883":["2","11-16","67","68"],"mappe":["EØS","INNVILGET"]},{"visningsnavn":"1. Ikke EØS-borger","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","hjemlerEOSForordningen883":["2"],"periodeType":"INGEN_UTBETALING","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"a6d5ae1bf62e0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"45a993c7a161"},{"_type":"span","marks":[],"text":" fordi du ikkje er EØS-borgar.","_key":"cdc7a2d777a3"}],"_type":"block","style":"normal","_key":"d0aaea9f7dd9","markDefs":[]}],"_id":"1e4d1ad1-6a5d-4c22-a017-a4eb06615ae3","eosVilkaar":["BOSATT_I_RIKET"],"_updatedAt":"2023-09-25T10:42:25Z","mappe":["EØS","AVSLAG"],"hjemler":["2","4","5"],"begrunnelsetype":"AVSLAG","tema":"EØS","triggereIBruk":["VILKÅRSVURDERING"],"bokmaal":[{"style":"normal","_key":"9452d4ac8042","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"631318741b5b0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"27147ace8ea8"},{"_key":"e0a1688451bd","_type":"span","marks":[],"text":" fordi du ikke er EØS-borger."}],"_type":"block"}],"apiNavn":"avslagEosIkkeEosBorger","behandlingstema":"EØS","_rev":"h26rAhFEYSUtDGXJE0vE2p","resultat":"IKKE_INNVILGET","_createdAt":"2022-11-01T11:46:14Z","valgbarhet":"STANDARD","navnISystem":"Ikke EØS-borger"},{"_rev":"h26rAhFEYSUtDGXJE0vChV","begrunnelsetype":"OPPHØR","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"7566c33e71170"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"a967b125e59e"},{"_type":"span","marks":[],"text":" fordi du har bedt om at barnetrygda for ","_key":"288cc2572201"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"0a4ca79d9208","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" blir stansa.","_key":"2647d2292956"}],"_type":"block","style":"normal","_key":"b7ed4926b3d3"}],"mappe":["EØS","OPPHØR"],"_updatedAt":"2023-09-25T10:41:19Z","valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"28c223a07d7f0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"2d4c1cfb38fd"},{"_type":"span","marks":[],"text":" fordi du har bedt om at barnetrygden for ","_key":"d3a4c283e088"},{"_type":"valgfeltV2","_key":"40dd61172db4","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":" blir stanset.","_key":"4114357e1255"}],"_type":"block","style":"normal","_key":"f27b70dcf827"}],"hjemler":["2","4","11"],"visningsnavn":"2. EØS søker ber om opphør","behandlingstema":"EØS","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","tema":"EØS","navnISystem":"EØS søker ber opphør","triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"apiNavn":"opphorEosSokerBerOmOpphor","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND"],"_type":"begrunnelse","_createdAt":"2022-06-16T12:42:38Z","_id":"3217a53d-b19c-4336-8046-486124654261"},{"apiNavn":"innvilgetTilleggstekstSekundaerAvtaleDeltBosted","triggereIBruk":["KOMPETANSE"],"mappe":["EØS","INNVILGET"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"periodeType":"UTBETALING","nynorsk":[{"_type":"block","style":"normal","_key":"66660a6de373","markDefs":[],"children":[{"_key":"79ecd0b0cfea0","_type":"span","marks":[],"text":"Du har avtale om delt bustad for barn fødd "},{"_type":"eosFlettefelt","_key":"2a522c34f21a","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":". ","_key":"c774596fef6f"}]}],"_id":"32383b16-96d4-4904-81f6-ccadadcee1d9","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Du har avtale om delt bosted for barn født ","_key":"aa3a1649dc420","_type":"span"},{"_key":"87277fa40722","flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". ","_key":"5eb85b956951"}],"_type":"block","style":"normal","_key":"947979ba4d6d"}],"vedtakResultat":"INNVILGET_ELLER_ØKNING","navnISystem":"Tilleggstekst sekundær avtale delt bosted","hjemler":["2"],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"tema":"EØS","_createdAt":"2022-10-17T10:32:06Z","valgbarhet":"TILLEGGSTEKST","barnetsBostedsland":["NORGE","IKKE_NORGE"],"visningsnavn":"37. Tilleggstekst sekundær avtale delt bosted","_rev":"BtltdVb0HP4g4WJfnr4V7o","_type":"begrunnelse","begrunnelsetype":"INNVILGET","_updatedAt":"2023-09-25T10:15:44Z"},{"visningsnavn":"20. Primærland UK barnetrygd allerede utbetalt","_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"INNVILGET","nynorsk":[{"_key":"22db3b9dad04","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du har rett til barnetrygd for barn fødd ","_key":"7a6f9dd957920"},{"_key":"9e20e020a8bc","flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt"},{"marks":[],"text":". Du ","_key":"66ec636b08cb","_type":"span"},{"_type":"valgfeltV2","_key":"d767542c3ffc","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"}},{"marks":[],"text":". ","_key":"6e3e7e7ddf7b","_type":"span"},{"_type":"valgfeltV2","_key":"d895f7917236","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_key":"14fca469574a","_type":"span","marks":[],"text":" bur i "},{"_key":"7c924d6bcb17","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Den andre forelderen jobbar ikkje i ","_key":"397ffb97b534"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"ee5aea62b125"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen har du rett på heile barnetrygda frå Noreg. Det blir ingen utbetaling fordi den andre forelderen allereie har fått barnetrygda for perioden.","_key":"a85f087d828e"}],"_type":"block","style":"normal"}],"navnISystem":"Primærland UK barnetrygd allerede utbetalt","hjemlerSeperasjonsavtalenStorbritannina":["29"],"_id":"3602ccbc-bde0-498e-9ae7-dcc755d27d50","annenForeldersAktivitet":["MOTTAR_PENSJON","INAKTIV"],"apiNavn":"innvilgetPrimarlandUkBarnetrygdAlleredeUtbetalt","behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_rev":"BtltdVb0HP4g4WJfnr4V7o","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","tema":"EØS","_createdAt":"2022-05-23T13:05:11Z","hjemler":["2","4","12"],"valgbarhet":"STANDARD","barnetsBostedsland":["IKKE_NORGE"],"mappe":["EØS","INNVILGET"],"_updatedAt":"2023-09-25T10:15:44Z","bokmaal":[{"style":"normal","_key":"f37b82e44f20","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du har rett til barnetrygd for barn født ","_key":"5b5ad20cddd20"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"124fe16a3f7a"},{"text":". Du ","_key":"bd7b0ac22505","_type":"span","marks":[]},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"3a3df14e89e0","skalHaStorForbokstav":false},{"marks":[],"text":". ","_key":"1ad35b10ccdc","_type":"span"},{"_key":"131122f7caca","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"text":" bor i ","_key":"85379f74ff6b","_type":"span","marks":[]},{"_type":"eosFlettefelt","_key":"5129e446dd1a","flettefelt":"barnetsBostedsland"},{"_type":"span","marks":[],"text":". Den andre forelderen jobber ikke i ","_key":"7120a9d85076"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"478b954546c8"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen har du rett på hele barnetrygden fra Norge Det blir ingen utbetaling fordi den andre forelderen allerede har fått barnetrygden for perioden.","_key":"111828af8c0a"}],"_type":"block"}],"triggereIBruk":["KOMPETANSE"]},{"nynorsk":[{"_type":"block","style":"normal","_key":"4594e7ddebd1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"ce6d96f199130"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"b3d6d9bc16de"},{"_type":"span","marks":[],"text":". ","_key":"16f76ce6a5f3"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"870532b0303c","skalHaStorForbokstav":true},{"text":" bur i ","_key":"620f66089b8c","_type":"span","marks":[]},{"_key":"b0d608b6f12a","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Du og den andre forelderen bur i Noreg. Det er ikkje rett til barnetrygd frå ","_key":"945c33963b2d"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"883e9ddef432"},{"_key":"dc43d5621ba3","_type":"span","marks":[],"text":" fordi vi har kome fram til at norsk lovgjevnad gjeld for barnet. Du får derfor heile barnetrygda frå Noreg."}]}],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"mappe":["EØS","INNVILGET"],"bokmaal":[{"_type":"block","style":"normal","_key":"cb25dbcb9e27","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"11bd637700470"},{"_key":"30d7cc673c41","flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". ","_key":"3b0056e77e08"},{"skalHaStorForbokstav":true,"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"421e1b9a712e"},{"_type":"span","marks":[],"text":" bor i ","_key":"862470c0f1b9"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"922ab765bfce"},{"_type":"span","marks":[],"text":". Du og den andre forelderen bor i Norge. Det er ikke rett til barnetrygd fra ","_key":"aa5dbaa1f713"},{"_key":"f4128f5a3239","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"text":" fordi vi har kommet fram til at norsk lovgivning gjelder for barnet. Du får derfor hele barnetrygden fra Norge. ","_key":"1c3e1911bddd","_type":"span","marks":[]}]}],"apiNavn":"innvilgetPrimarlandBeggeForeldreBosattINorge","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"hjemlerEOSForordningen883":["2","11-16","67","68"],"tema":"EØS","annenForeldersAktivitet":["MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"navnISystem":"Primærland begge foreldre bosatt i Norge","_type":"begrunnelse","triggereIBruk":["KOMPETANSE"],"_id":"3815559b-8f00-47d6-841f-6fa483f9dcc3","vedtakResultat":"INNVILGET_ELLER_ØKNING","hjemler":["2","4","11"],"visningsnavn":"3. Primærland begge foreldre bosatt i Norge","behandlingstema":"EØS","_rev":"BtltdVb0HP4g4WJfnr4V7o","_updatedAt":"2023-09-25T10:15:44Z","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","_createdAt":"2022-05-23T12:48:39Z","valgbarhet":"STANDARD"},{"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_type":"begrunnelse","resultat":"INNVILGELSE","tema":"EØS","_id":"3f9636a2-61b2-4963-97de-4cd06c2d8ffb","mappe":["EØS","INNVILGET"],"nynorsk":[{"_key":"f28baaa742cb","markDefs":[],"children":[{"text":"Du får barnetrygd for barn fødd ","_key":"08d24ac174350","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"a21d347c62af"},{"_type":"span","marks":[],"text":". Du ","_key":"2c486b7edfe3"},{"_type":"valgfeltV2","_key":"f0f8323600e6","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"}},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"738839ec8d81"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"5165f4d92499"},{"_type":"span","marks":[],"text":" i ","_key":"f8367f1b34c5"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"f06c46c4ea01"},{"_key":"6105b6880bdb","_type":"span","marks":[],"text":". "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"97bdbc9a769d","skalHaStorForbokstav":true},{"_key":"2bf7b626987b","_type":"span","marks":[],"text":" bur i "},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"e8d9e6e4eb7a"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen får du heile barnetrygda frå Noreg.","_key":"079d3556b454"}],"_type":"block","style":"normal"}],"bokmaal":[{"_key":"07ab81457cc4","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd for barn født ","_key":"5e03ace64e5a0","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"04f3f3c2da4e"},{"marks":[],"text":". Du ","_key":"149e3f8e0438","_type":"span"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"68b54a70edd3","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"1c56943ed452"},{"_key":"355cdc3eaa1e","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" i ","_key":"ad05226b6fc1"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"783416be0815"},{"_type":"span","marks":[],"text":". ","_key":"b533aadb0576"},{"_type":"valgfeltV2","_key":"86fef5f87717","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"marks":[],"text":" bor i ","_key":"39b1ab3197b8","_type":"span"},{"_type":"eosFlettefelt","_key":"391727a02fa1","flettefelt":"barnetsBostedsland"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen får du hele barnetrygden fra Norge.","_key":"b7e31408e2a0"}],"_type":"block","style":"normal"}],"_rev":"BtltdVb0HP4g4WJfnr4V7o","hjemlerEOSForordningen883":["2","11-16","67","68"],"barnetsBostedsland":["IKKE_NORGE"],"_updatedAt":"2023-09-25T10:15:44Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"visningsnavn":"7. Primærland UK og utland standard","navnISystem":"Primærland UK og utland standard","hjemler":["2","4","11"],"behandlingstema":"EØS","begrunnelsetype":"INNVILGET","apiNavn":"innvilgetPrimarlandUKOgUtlandStandard","periodeType":"UTBETALING","_createdAt":"2022-05-23T12:56:05Z","triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","hjemlerSeperasjonsavtalenStorbritannina":["29"],"vedtakResultat":"INNVILGET_ELLER_ØKNING"},{"kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_rev":"BtltdVb0HP4g4WJfnr4V7o","_createdAt":"2022-10-11T09:58:45Z","_id":"4138203b-fcb8-449c-a6eb-8d1d108b8374","mappe":["EØS","INNVILGET"],"annenForeldersAktivitet":["FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"navnISystem":"Sekundærland begge foreldre bosatt i Norge","apiNavn":"innvilgetSekundaerlandBeggeForeldreBosattINorge","vedtakResultat":"INNVILGET_ELLER_ØKNING","nynorsk":[{"_type":"block","style":"normal","_key":"2f5d39ee8dc1","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"2c4115d9298d0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"35dcefd8bb94"},{"_type":"span","marks":[],"text":". Barnet bur i ","_key":"23470ad69dcf"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"898f73b68660"},{"_type":"span","marks":[],"text":". Du og den andre forelderen bur i Noreg. Du og den andre forelderen er ikkje i arbeidsaktivitet i Noreg. ","_key":"940030b28bcd"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"e9326ff9698c"},{"text":" har hovudansvaret for utbetaling av barnetrygd fordi vi har kome fram til at lovgjevnaden i ","_key":"f56c07f02ec5","_type":"span","marks":[]},{"_key":"e4557910dcac","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"_key":"8943565a5468","_type":"span","marks":[],"text":" gjeld for barnet. Noreg utbetalar derfor forskjellen mellom barnetrygda i "},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"7cdc8a05ec87"},{"_type":"span","marks":[],"text":" og norsk barnetrygd.","_key":"105160ded4f7"}]}],"visningsnavn":"34. Sekundærland begge foreldre bosatt i Norge","triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["IKKE_NORGE"],"_updatedAt":"2023-09-25T10:15:44Z","valgbarhet":"STANDARD","hjemler":["2","4","11"],"behandlingstema":"EØS","_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","tema":"EØS","bokmaal":[{"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"587020f143260","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"5f4f32273b83"},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"ae4cb1b033ca"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"1b71b742cff0"},{"marks":[],"text":". Du og den andre forelderen bor i Norge. Du og den andre forelderen er ikke i arbeidsaktivitet i Norge. ","_key":"30a946bcda97","_type":"span"},{"_key":"9c82be8e0293","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":" har hovedansvaret for utbetaling av barnetrygd fordi vi har kommet fram til at lovgivningen i ","_key":"f161dda1d3bb"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"77833d9a2b8f"},{"_type":"span","marks":[],"text":" gjelder for barnet. Norge utbetaler derfor forskjellen mellom barnetrygden i ","_key":"e3e68423934c"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"b24941091bbf"},{"_type":"span","marks":[],"text":" og norsk barnetrygd.","_key":"746b2fe7e7c7"}],"_type":"block","style":"normal","_key":"d9a0b21a58b7","markDefs":[]}]},{"triggereIBruk":["VILKÅRSVURDERING"],"valgbarhet":"STANDARD","_rev":"BtltdVb0HP4g4WJfnr60ho","periodeType":"INGEN_UTBETALING","behandlingstema":"EØS","vedtakResultat":"IKKE_INNVILGET","tema":"EØS","nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn fødd ","_key":"ff2ebdc45f550","_type":"span"},{"_type":"eosFlettefelt","_key":"e86ff2b0fbbf","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi du ikkje har opphaldsrett som familiemedlem til ein EØS-borgar.","_key":"6c704fc08b35"}],"_type":"block","style":"normal","_key":"72467a6d96c0"}],"_updatedAt":"2023-09-25T10:43:33Z","bokmaal":[{"style":"normal","_key":"f474dd9ec3e3","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"5ef8fcf8b1040"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"97afa04d505b"},{"text":" fordi du ikke har oppholdsrett som familiemedlem til en EØS-borger.","_key":"8c26085784eb","_type":"span","marks":[]}],"_type":"block"}],"hjemler":["2","4","5"],"visningsnavn":"10. Ikke oppholdsrett som familiemedlem av EØS-borger","eosVilkaar":["LOVLIG_OPPHOLD"],"_type":"begrunnelse","_id":"43acd3c8-4c3a-4b78-aeef-a749e83c414d","navnISystem":"Ikke oppholdsrett som familiemedlem av EØS-borger","apiNavn":"avslagEosIkkeOppholdsrettSomFamiliemedlemAvEosBorger","_createdAt":"2022-11-01T14:00:50Z","mappe":["EØS","AVSLAG"],"hjemlerEOSForordningen883":["2","11-16","67"],"begrunnelsetype":"AVSLAG"},{"hjemlerSeperasjonsavtalenStorbritannina":["29"],"periodeType":"UTBETALING","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND"],"visningsnavn":"26. Sekundærland UK og utland standard","_rev":"FuD004taptHFqBZyEy5by3","hjemlerEOSForordningen883":["2","11-16","67","68"],"_createdAt":"2022-08-26T12:21:36Z","barnetsBostedsland":["IKKE_NORGE"],"_updatedAt":"2023-09-25T10:15:46Z","_id":"44876d22-03fb-4bcf-ab0e-ea210af18df8","behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","tema":"EØS","triggereIBruk":["KOMPETANSE"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"d99e7257363c"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"3b51db198124"},{"text":". Du","_key":"c472e1dceb84","_type":"span","marks":[]},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"3371bac346cc","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"0e5d4ede4864"},{"_key":"1cc57104e1bb","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"863ab823e0cc"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"08c5b103883b","skalHaStorForbokstav":false},{"marks":[],"text":" i ","_key":"73b4abc11d67","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"c8fbd5597570"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen har Storbritannia hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i Storbritannia og norsk barnetrygd. ","_key":"8bb231ab8547"}],"_type":"block","style":"normal","_key":"c586dc92341f","markDefs":[]}],"navnISystem":"Sekundærland UK og utland standard","apiNavn":"innvilgetSekundaerlandUkOgUtlandStandard","hjemler":["2","4","11"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"d5382cf9cd6b0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"c44fd1ac4ada"},{"_type":"span","marks":[],"text":". Du","_key":"43e8fc5528d9"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"bbd52171653f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bur i ","_key":"2fe99803cc56"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"6edbf8a57e93"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"8d882ffebd5c"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"8c78368686fe","skalHaStorForbokstav":false},{"text":" i ","_key":"f661fe7f792f","_type":"span","marks":[]},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"bdca64a3448c"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen har Storbritannia hovudansvaret for utbetaling av barnetrygd. Noreg utbetalar derfor forskjellen mellom barnetrygda i Storbritannia og norsk barnetrygd. ","_key":"e1420b0b3e32"}],"_type":"block","style":"normal","_key":"0c45cab2e1ea"}],"valgbarhet":"STANDARD","mappe":["EØS","INNVILGET"]},{"triggereIBruk":["KOMPETANSE"],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND","TO_PRIMÆRLAND"],"vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","nynorsk":[{"children":[{"marks":[],"text":"Barnetrygd for barn fødd ","_key":"dfbc946c7680","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"9c08ccd4ebc8"},{"_type":"span","marks":[],"text":" fordi du ikkje lenger er statsborgar i eit EØS-land.","_key":"ed8d1dd8e197"}],"_type":"block","style":"normal","_key":"768a6af52840","markDefs":[]}],"_id":"49b3e4c8-9374-4fca-b716-d1d72dc6394d","barnetsBostedsland":["NORGE","IKKE_NORGE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"bokmaal":[{"children":[{"text":"Barnetrygd for barn født ","_key":"166da3fc95f1","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"58d5f11f566c"},{"_type":"span","marks":[],"text":" fordi du ikke lenger er statsborger i et EØS-land.","_key":"7fb4df1a99a0"}],"_type":"block","style":"normal","_key":"8cc44f2d4557","markDefs":[]}],"navnISystem":"Ikke statsborger i EØS-land","hjemler":["2","4","11"],"_rev":"h26rAhFEYSUtDGXJE0vCpd","_createdAt":"2022-08-12T12:29:15Z","apiNavn":"opphorIkkeStatsborgerIEosLand","hjemlerEOSForordningen883":["2","11-16","67"],"mappe":["EØS","OPPHØR"],"_updatedAt":"2023-09-25T10:41:27Z","tema":"EØS","valgbarhet":"STANDARD","visningsnavn":"4. Ikke statsborger i EØS-land","behandlingstema":"EØS","_type":"begrunnelse","begrunnelsetype":"OPPHØR"},{"begrunnelsetype":"INNVILGET","_createdAt":"2022-05-23T12:45:49Z","barnetsBostedsland":["IKKE_NORGE"],"mappe":["EØS","INNVILGET"],"hjemler":["2","4","11"],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","hjemlerEOSForordningen883":["2","11-16","67","68"],"annenForeldersAktivitet":["MOTTAR_PENSJON","IKKE_AKTUELT"],"navnISystem":"Primærland særkullsbarn/andre barn overtatt ansvar","periodeType":"UTBETALING","tema":"EØS","_id":"4f0e0c7d-4ed6-42c0-94ff-3268c48307c7","behandlingstema":"EØS","_updatedAt":"2023-09-25T10:15:46Z","nynorsk":[{"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"41dd7509e7360","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"8eb27fb33fb4"},{"_type":"span","marks":[],"text":". Du ","_key":"315aeba97548"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"6356d5b4deb8","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"b566840d5cc0"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"f6abe41d03bb","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bur i ","_key":"dc4ca7c76731"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"1698e30877e4"},{"text":". Du har ansvar for ","_key":"28da2ed69432","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"9c9c2593ec9d","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Du får derfor heile barnetrygda frå Noreg.","_key":"741bafe6d50e"}],"_type":"block","style":"normal","_key":"42f315c10a13","markDefs":[]}],"triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"4c9bff0284330"},{"_type":"eosFlettefelt","_key":"3b2d9b3247e6","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":". Du ","_key":"bb4401eeff9a"},{"valgReferanse":{"_type":"reference","_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb"},"_type":"valgfeltV2","_key":"54f79b6642ed","skalHaStorForbokstav":false},{"marks":[],"text":". ","_key":"cfa22b250fb1","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"ceb5629423cd","skalHaStorForbokstav":true},{"marks":[],"text":" bor i ","_key":"069dea85ffdc","_type":"span"},{"_type":"eosFlettefelt","_key":"fad2b02a230a","flettefelt":"barnetsBostedsland"},{"_type":"span","marks":[],"text":". Du har ansvar for ","_key":"1731141c2fac"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"22d836a12515","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Du får derfor hele barnetrygden fra Norge.","_key":"fcb6099c46e4"}],"_type":"block","style":"normal","_key":"5bfdeca5ec1d"}],"apiNavn":"innvilgetPrimarlandSaerkullsbarnAndreBarnOvertattAnsvar","visningsnavn":"11. Primærland særkullsbarn/andre barn overtatt ansvar","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_rev":"FuD004taptHFqBZyEy5by3"},{"hjemlerEOSForordningen883":["2","11-16","67"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"3d5218a7e75a0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"47cb6124d17f"},{"_key":"1d1b5138fa56","_type":"span","marks":[],"text":" fordi separasjonsavtalen mellom Storbritannia og Norge ikkje gjeld for familien."}],"_type":"block","style":"normal","_key":"af8f902944a5"}],"triggereIBruk":["VILKÅRSVURDERING"],"mappe":["EØS","AVSLAG"],"bokmaal":[{"_key":"52717848777e","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"ca69839469fc0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"a6aa2729a38f"},{"text":" fordi separasjonsavtalen mellom Storbritannia og Norge ikke gjelder for familien.","_key":"68d9a259929f","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:43:26Z","navnISystem":"Separasjonsavtalen gjelder ikke","apiNavn":"avslagEosSeparasjonsavtalenGjelderIkke","hjemler":["2","4","5"],"behandlingstema":"EØS","_type":"begrunnelse","_createdAt":"2022-11-01T13:47:05Z","eosVilkaar":["BOSATT_I_RIKET"],"vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"AVSLAG","visningsnavn":"8. Separasjonsavtalen gjelder ikke","_rev":"BtltdVb0HP4g4WJfnr60LJ","hjemlerSeperasjonsavtalenStorbritannina":["29"],"periodeType":"INGEN_UTBETALING","tema":"EØS","_id":"51188fef-930d-4dc7-996f-9cd22d230ea0"},{"barnetsBostedsland":["NORGE"],"apiNavn":"innvilgetSekundaerlandToArbeidslandNorgeUtbetaler","behandlingstema":"EØS","tema":"EØS","valgbarhet":"STANDARD","_id":"52b14fa9-596d-4767-9f50-7b27c88596cd","triggereIBruk":["KOMPETANSE"],"_updatedAt":"2023-09-25T10:15:46Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_PENSJON"],"kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_rev":"FuD004taptHFqBZyEy5by3","_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"nynorsk":[{"_key":"64d91576262e","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"6adf8d4033950","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"f147b4104f13"},{"text":". Du ","_key":"b2ac4e89f3d9","_type":"span","marks":[]},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"09b1e5bed07b","skalHaStorForbokstav":false},{"marks":[],"text":". Barnet bur i Noreg. Den andre forelderen jobbar i ","_key":"9efaf588cc35","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"7a7c360f67d3"},{"_type":"span","marks":[],"text":". Landa de jobbar i har hovudansvar for utbetaling av barnetrygd. Norsk barnetrygd er høgare enn barnetrygda i (SØKERS AKTIVITETSLAND) og i ","_key":"8ffac01db78a"},{"_type":"eosFlettefelt","_key":"c86f5fbf7282","flettefelt":"annenForeldersAktivitetsland"},{"_type":"span","marks":[],"text":". Noreg utbetalar derfor forskjellen mellom barnetrygda i desse landa og norsk barnetrygd.","_key":"68c97cea0a74"}],"_type":"block","style":"normal"}],"bokmaal":[{"markDefs":[],"children":[{"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"0dd5b7bb38620","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"9dca1b7fdb85"},{"_key":"418b67f82c19","_type":"span","marks":[],"text":". Du "},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"32b8f54d5ca1"},{"_type":"span","marks":[],"text":". Barnet bor i Norge. Den andre forelderen jobber i ","_key":"668245f590de"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"a93d311077a2"},{"_type":"span","marks":[],"text":". Landene dere jobber i har hovedansvar for utbetaling av barnetrygd. Norsk barnetrygd er høyere enn barnetrygden i (SØKERS AKTIVITETSLAND) og i ","_key":"8127ee737bf4"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"caaee310ce49"},{"text":". Norge utbetaler derfor forskjellen mellom barnetrygden i disse landene og norsk barnetrygd.","_key":"8469291cdf66","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"34189880e873"},{"_key":"e691d66f1a8c","markDefs":[],"children":[{"marks":[],"text":"\n","_key":"f2f985b9ef180","_type":"span"}],"_type":"block","style":"normal"}],"hjemlerEOSForordningen987":["58"],"mappe":["EØS","INNVILGET"],"navnISystem":"Sekundærland to arbeidsland Norge utbetaler","hjemler":["2","4","11"],"visningsnavn":"27. Sekundærland to arbeidsland Norge utbetaler","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","periodeType":"UTBETALING","_createdAt":"2022-08-26T12:56:47Z"},{"triggereIBruk":["KOMPETANSE"],"hjemler":["2","4","11"],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"bdb4a067e8e30"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"18f65b52c247"},{"_type":"span","marks":[],"text":". Du ","_key":"de7d9e4aed1d"},{"_key":"80cfc4a9d010","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". ","_key":"91b018299dae"},{"skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"e83da6dba25d"},{"_type":"span","marks":[],"text":" bur i ","_key":"3812cb1277a9"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"9eef28ff3124"},{"_key":"f71347c87fe3","_type":"span","marks":[],"text":". Den andre forelderen "},{"_key":"2fce94aa816e","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" i ","_key":"b26c562daaf5"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"4fd7912b2327"},{"_key":"24c760eb957a","_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Du og den andre forelderen har rett til barnetrygd frå Noreg og "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"bf9a77c9364d"},{"_key":"703741c88476","_type":"span","marks":[],"text":" fordi de jobbar i andre land enn der "},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"f2eb31754cff","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" bur. Landet med den høgaste barnetrygda skal utbetale. Norsk barnetrygd er høgare enn barnetrygda i ","_key":"66bd9b613107"},{"_key":"38cf3ab56446","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"text":". Du får derfor heile barnetrygda frå Noreg.","_key":"d42173f9e27d","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"722187fddc72","markDefs":[]}],"_createdAt":"2022-05-23T12:54:13Z","barnetsBostedsland":["IKKE_NORGE"],"_type":"begrunnelse","mappe":["EØS","INNVILGET"],"navnISystem":"Primærland UK to arbeidsland Norge utbetaler","_updatedAt":"2023-09-25T10:15:46Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_PENSJON"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","valgbarhet":"STANDARD","visningsnavn":"14. Primærland UK to arbeidsland Norge utbetaler","tema":"EØS","_rev":"FuD004taptHFqBZyEy5by3","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"UTBETALING","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn født ","_key":"49521d7745030"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"c278d52d89c0"},{"_key":"917ebb5cb0db","_type":"span","marks":[],"text":". Du "},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"4823f31d9cc3","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"74105f1535dd"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"dc229a67d696","skalHaStorForbokstav":true},{"marks":[],"text":" bor i ","_key":"f7c9c0f2fb1c","_type":"span"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"97f0a4fa3100"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"97900cd27508"},{"_type":"valgfeltV2","_key":"218842d2477a","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"}},{"_key":"4f94813e171b","_type":"span","marks":[],"text":" i "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"ffa71b820f31"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Du og den andre forelderen har rett til barnetrygd fra Norge og ","_key":"91c367b12a37"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"ef9af98dbb30"},{"text":" fordi dere jobber i andre land enn der ","_key":"015d5416f248","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"dfdebd57355e","skalHaStorForbokstav":false},{"marks":[],"text":" bor. Landet med den høyeste barnetrygden skal utbetale. Norsk barnetrygd er høyere enn barnetrygden i ","_key":"618813616491","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"f488ef2dc97d"},{"_type":"span","marks":[],"text":". Du får derfor hele barnetrygden fra Norge.","_key":"96e95734d31f"}],"_type":"block","style":"normal","_key":"828561347f2c"}],"hjemlerEOSForordningen987":["58"],"_id":"5b462eb4-1133-4736-be94-71eadad8e97a","hjemlerSeperasjonsavtalenStorbritannina":["29"],"apiNavn":"innvilgetPrimarlandUkToArbeidslandNorgeUtbetaler","behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"begrunnelsetype":"INNVILGET"},{"barnetsBostedsland":["NORGE","IKKE_NORGE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","valgbarhet":"TILLEGGSTEKST","mappe":["EØS","INNVILGET"],"bokmaal":[{"children":[{"_key":"227449f84c8f0","_type":"span","marks":[],"text":"Barnetrygden er endret fordi det har vært en endring av valutakursen."}],"_type":"block","style":"normal","_key":"ffffa5c34602","markDefs":[]}],"navnISystem":"Tilleggstekst valutajustering","visningsnavn":"30. Tilleggstekst valutajustering","_rev":"FuD004taptHFqBZyEy5by3","_id":"6226743c-e415-4412-85b6-a80e7d5bb7af","triggereIBruk":["KOMPETANSE"],"_updatedAt":"2023-09-25T10:15:46Z","begrunnelsetype":"INNVILGET","tema":"EØS","nynorsk":[{"_type":"block","style":"normal","_key":"3afda9f6f0e3","markDefs":[],"children":[{"marks":[],"text":"Barnetrygda er endra fordi det har vore ei endring av valutakursen.","_key":"f30ee9200ee70","_type":"span"}]}],"_createdAt":"2022-09-23T11:30:30Z","apiNavn":"innvilgetTilleggstekstValutajustering","_type":"begrunnelse","periodeType":"UTBETALING"},{"barnetsBostedsland":["NORGE","IKKE_NORGE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"89e917d8d74b0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"13d1d66cf340"},{"_type":"span","marks":[],"text":" fordi du ikke lenger har oppholdsrett som familiemedlem til en statsborger i et EØS-land.","_key":"dcda4d950dff"}],"_type":"block","style":"normal","_key":"40fc1f29ffa5"}],"behandlingstema":"EØS","_createdAt":"2022-08-12T12:49:24Z","_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67"],"periodeType":"INGEN_UTBETALING","tema":"EØS","hjemler":["2","4","11"],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND"],"_rev":"BtltdVb0HP4g4WJfnr5vco","nynorsk":[{"markDefs":[],"children":[{"text":"Barnetrygd for barn fødd ","_key":"76c6813b393c","_type":"span","marks":[]},{"_type":"eosFlettefelt","_key":"367b0cab05f1","flettefelt":"barnasFodselsdatoer"},{"_key":"e4951d171ef2","_type":"span","marks":[],"text":" fordi du ikkje lenger har opphaldsrett som familiemedlem til ein statsborgar i eit EØS-land."}],"_type":"block","style":"normal","_key":"6e768e11b296"}],"mappe":["EØS","OPPHØR"],"_updatedAt":"2023-09-25T10:41:41Z","apiNavn":"opphorIkkeOppholdsrettSomFamiliemedlem","visningsnavn":"8. Ikke oppholdsrett som familiemedlem","begrunnelsetype":"OPPHØR","triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","_id":"6761b645-e0b0-49a3-aad3-57fa889a43e6","navnISystem":"Ikke oppholdsrett som familiemedlem","vedtakResultat":"IKKE_INNVILGET"},{"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"0419456732940"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"1eb527b68cf8"},{"marks":[],"text":". Du ","_key":"cce43890c7db","_type":"span"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"f8e7aaf1241c"},{"_key":"be862584b288","_type":"span","marks":[],"text":". Barnet bur i Noreg. Den andre forelderen jobbar i "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"b08cbe5f08d4"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Landa de jobbar i har hovudansvar for utbetaling av barnetrygd. Norsk barnetrygd er høgare enn barnetrygda i (SØKERS AKTIVITETSLAND) og i ","_key":"49f8de1c5ffc"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"8a53d6accb0c"},{"_key":"8b08aba2d49d","_type":"span","marks":[],"text":". Noreg utbetalar derfor forskjellen mellom barnetrygda i desse landa og norsk barnetrygd."}],"_type":"block","style":"normal","_key":"94776766a51d"}],"bokmaal":[{"children":[{"_key":"ffac2b4a48070","_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født "},{"_key":"ad026f9f12b9","flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Du ","_key":"276c702c17de"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"200c1c1a5fc0","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bor i Norge. Den andre forelderen jobber i ","_key":"44c60032666e"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"456b6bc846a3"},{"_key":"879890a5a473","_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Landene dere jobber i har hovedansvar for utbetaling av barnetrygd. Norsk barnetrygd er høyere enn barnetrygden i (SØKERS AKTIVITETSLAND) og i "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"3a14def32dcf"},{"_type":"span","marks":[],"text":". Norge utbetaler derfor forskjellen mellom barnetrygden i disse landene og norsk barnetrygd.","_key":"0b0d4eedd819"}],"_type":"block","style":"normal","_key":"42f3865c55d1","markDefs":[]}],"visningsnavn":"28. Sekundærland UK to arbeidsland Norge utbetaler","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","behandlingstema":"EØS","_rev":"FuD004taptHFqBZyEy5by3","hjemlerEOSForordningen883":["2","11-16","67","68"],"hjemlerSeperasjonsavtalenStorbritannina":["29"],"_updatedAt":"2023-09-25T10:15:46Z","hjemlerEOSForordningen987":["58"],"apiNavn":"innvilgetSekundaerlandUkToArbeidslandNorgeUtbetaler","periodeType":"UTBETALING","_createdAt":"2022-08-26T13:01:13Z","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_type":"begrunnelse","tema":"EØS","triggereIBruk":["KOMPETANSE"],"navnISystem":"Sekundærland UK to arbeidsland Norge Utbetaler","hjemler":["2","4","11"],"barnetsBostedsland":["NORGE"],"annenForeldersAktivitet":["I_ARBEID"],"_id":"68061cb3-cdf3-4eb7-8880-a64d86dcd1a0","mappe":["EØS","INNVILGET"]},{"tema":"EØS","_createdAt":"2022-05-23T12:49:10Z","mappe":["EØS","INNVILGET"],"bokmaal":[{"_key":"13802da42162","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"6cc24fac0d240"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"e6ea3c00a798"},{"_type":"span","marks":[],"text":". Du ","_key":"97a5b0d267e3"},{"_key":"370881efe381","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". ","_key":"0ae33857dda1"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"bbb391720f2c","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bor i ","_key":"cb6116f19f5b"},{"_key":"7c6a79516e9e","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"fa693a3ce4f7"},{"valgReferanse":{"_type":"reference","_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a"},"_type":"valgfeltV2","_key":"7976810bd58b","skalHaStorForbokstav":false},{"_key":"c9175fb56e4a","_type":"span","marks":[],"text":" i "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"c8854719fda0"},{"_type":"span","marks":[],"text":". Du og den andre forelderen har rett til barnetrygd fra Norge og ","_key":"0ed2bdd9eb6c"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"a0b61e712ae3"},{"_type":"span","marks":[],"text":" fordi dere jobber i andre land enn der ","_key":"d97f3cb98d8d"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"f254eca75b56","skalHaStorForbokstav":false},{"_key":"f1c65e45dfaa","_type":"span","marks":[],"text":" bor. Landet med den høyeste barnetrygden skal utbetale. Norsk barnetrygd er høyere enn barnetrygden i "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"5fda44fb0624"},{"_type":"span","marks":[],"text":". Du får derfor hele barnetrygden fra Norge.","_key":"34b250a6f943"}],"_type":"block","style":"normal"}],"apiNavn":"innvilgetPrimarlandToArbeidslandNorgeUtbetaler","visningsnavn":"12. Primærland to arbeidsland Norge utbetaler","behandlingstema":"EØS","_type":"begrunnelse","hjemlerEOSForordningen987":["58"],"barnetsBostedsland":["IKKE_NORGE"],"hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"UTBETALING","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"b1d238e6dda30"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"76c40e949858"},{"text":". Du ","_key":"57fe0d8b621f","_type":"span","marks":[]},{"_type":"valgfeltV2","_key":"8ee92b268fda","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb"}},{"_type":"span","marks":[],"text":". ","_key":"3b14b227c4cb"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"4436a0ac96b0","skalHaStorForbokstav":true},{"marks":[],"text":" bur i ","_key":"8d15ffd46ba0","_type":"span"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"d7fdeae17451"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"d94ebef1dedb"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"b81eb27ac5b1"},{"_type":"span","marks":[],"text":" i ","_key":"dd72aff3fdb6"},{"_type":"eosFlettefelt","_key":"353d3f594a84","flettefelt":"annenForeldersAktivitetsland"},{"_type":"span","marks":[],"text":". Du og den andre forelderen har rett til barnetrygd frå Noreg og ","_key":"68dd4119000e"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"237022a74b13"},{"_type":"span","marks":[],"text":" fordi de jobbar i andre land enn der ","_key":"2dd02034f626"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"b58988ef5464","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" bur. Landet med den høgaste barnetrygda skal utbetale. Norsk barnetrygd er høgare enn barnetrygda i ","_key":"fa827dc9738d"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"d4b0e913d78a"},{"_type":"span","marks":[],"text":". Du får derfor heile barnetrygda frå Noreg.","_key":"ec41e727306a"}],"_type":"block","style":"normal","_key":"f14bbadc9545","markDefs":[]}],"valgbarhet":"STANDARD","_id":"76375b9e-508a-4730-bd92-bcf5dfa4ffef","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON"],"navnISystem":"Primærland to arbeidsland Norge utbetaler","hjemler":["2","4","11"],"kompetanseResultat":["TO_PRIMÆRLAND"],"_rev":"FuD004taptHFqBZyEy5by3","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","triggereIBruk":["KOMPETANSE"],"_updatedAt":"2023-09-25T10:15:46Z"},{"hjemlerEOSForordningen883":["2","11-16","67"],"tema":"EØS","navnISystem":"Jobber ikke","_createdAt":"2022-11-01T13:23:48Z","_id":"7b4599e0-a176-402d-8a6e-5fb1cc08de41","hjemler":["2","4","5"],"_rev":"h26rAhFEYSUtDGXJE0vECz","_type":"begrunnelse","utdypendeVilkaarsvurderinger":["BARN_BOR_I_STORBRITANNIA_MED_ANNEN_FORELDER"],"begrunnelsetype":"AVSLAG","nynorsk":[{"style":"normal","_key":"da4c91d9a00d","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn fødd ","_key":"0aec8e3fa3410","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"2ae6f6c6cfdc"},{"_type":"span","marks":[],"text":" fordi du ikkje jobbar i Noreg.","_key":"ec7fd5683d4d"}],"_type":"block"}],"triggereIBruk":["VILKÅRSVURDERING"],"mappe":["EØS","AVSLAG"],"apiNavn":"avslagEosJobberIkke","bokmaal":[{"_key":"c2a6a4b66102","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"cb56045d7a4f0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"a03d03eb7ad6"},{"_key":"083a6fd11cde","_type":"span","marks":[],"text":" fordi du ikke jobber i Norge."}],"_type":"block","style":"normal"}],"visningsnavn":"3. Jobber ikke","behandlingstema":"EØS","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:42:35Z","eosVilkaar":["LOVLIG_OPPHOLD"]},{"kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND"],"_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"OPPHØR","tema":"EØS","bokmaal":[{"style":"normal","_key":"439d82b2716c","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"08c918b019ba0","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"5fe2f2ab9170"},{"_key":"68ae2819e148","_type":"span","marks":[],"text":" fordi du jobber i Norge som utsendt fra et annet EØS-land. Vi har derfor kommet fram til at lovgivningen i det andre EØS-landet gjelder for deg."}],"_type":"block"}],"apiNavn":"opphorUtsendtArbeidstakerFraEosLand","hjemler":["2","4","11"],"visningsnavn":"12. Utsendt arbeidstaker fra annet EØS-land","periodeType":"INGEN_UTBETALING","_createdAt":"2022-10-26T13:11:33Z","valgbarhet":"STANDARD","_id":"7f68d3aa-332b-447b-a550-02458872d1e5","mappe":["EØS","OPPHØR"],"navnISystem":"Utsendt arbeidstaker fra annet EØS-land","_rev":"h26rAhFEYSUtDGXJE0vDH4","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"c3b07cc234e80"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"f70475f0522f"},{"marks":[],"text":" fordi du jobbar i Noreg som utsendt frå eit anna EØS-land. Vi har derfor kome fram til at lovgjevnaden i det andre EØS-landet gjeld for deg.","_key":"7f1de6b7a7d2","_type":"span"}],"_type":"block","style":"normal","_key":"b2b7630d73bc"}],"triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"behandlingstema":"EØS","_updatedAt":"2023-09-25T10:41:55Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"]},{"_id":"8052fa75-fcb1-4757-8016-9eba1437bd2a","mappe":["EØS","INNVILGET"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"hjemler":["2","12"],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","barnetsBostedsland":["NORGE","IKKE_NORGE"],"_updatedAt":"2023-09-25T10:15:46Z","navnISystem":"Tilleggstekst sekundær delt bosted annen forelder ikke søkt","apiNavn":"innvilgetTilleggstekstSekundaerDeltBostedAnnenForelderIkkeSoekt","_type":"begrunnelse","periodeType":"UTBETALING","valgbarhet":"TILLEGGSTEKST","visningsnavn":"32. Tilleggstekst sekundær delt bosted annen forelder ikke søkt","tema":"EØS","_createdAt":"2022-10-07T13:23:06Z","triggereIBruk":["KOMPETANSE"],"_rev":"FuD004taptHFqBZyEy5by3","begrunnelsetype":"INNVILGET","nynorsk":[{"style":"normal","_key":"6b149b31c2a2","markDefs":[],"children":[{"_key":"8819ee3ec5ca0","_type":"span","marks":[],"text":"Du får heile barnetrygda Noreg utbetalar for barn fødd "},{"_type":"eosFlettefelt","_key":"f8a09613aca2","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" som har delt bustad. Den andre forelderen har ikkje søkt om delt barnetrygd. Barnetrygda kan først delast frå månaden etter at begge foreldra har søkt om det.","_key":"2addef079244"}],"_type":"block"}],"bokmaal":[{"children":[{"_key":"bd1e605ef7e60","_type":"span","marks":[],"text":"Du får hele barnetrygden Norge utbetaler for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"caeb8886b946"},{"_type":"span","marks":[],"text":" som har delt bosted. Den andre forelderen har ikke søkt om delt barnetrygd. Barnetrygden kan først deles fra måneden etter at begge foreldrene har søkt om det.","_key":"507fdedda68c"}],"_type":"block","style":"normal","_key":"5444e6fbc1ea","markDefs":[]}]},{"_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67"],"nynorsk":[{"_key":"e632f73599d4","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"5479b5878d63"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"d4f6be29b6f5"},{"text":" fordi du ikkje ","_key":"d81dc83b8567","_type":"span","marks":[]},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"b46948c5813e","skalHaStorForbokstav":false},{"text":".","_key":"0c2852022c8f","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"valgbarhet":"STANDARD","navnISystem":"Opphør standard","apiNavn":"opphorEosStandard","hjemler":["2","4","11"],"behandlingstema":"EØS","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"periodeType":"INGEN_UTBETALING","_createdAt":"2022-06-16T12:36:53Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"863500d5e5610"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"44f5d09b62c5"},{"_type":"span","marks":[],"text":" fordi du ikke ","_key":"d4b9d0483159"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"a10012d4e43b","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":".","_key":"04fce56e6bb5"}],"_type":"block","style":"normal","_key":"ab855980ba0f"}],"visningsnavn":"1. Opphør standard","_rev":"FuD004taptHFqBZyEy9424","resultat":"IKKE_INNVILGET","vedtakResultat":"IKKE_INNVILGET","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND","TO_PRIMÆRLAND"],"begrunnelsetype":"OPPHØR","tema":"EØS","_id":"8507a24d-4739-4a95-b7bf-156f58d88bda","triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"mappe":["EØS","OPPHØR"],"_updatedAt":"2023-09-25T10:41:15Z"},{"_rev":"h26rAhFEYSUtDGXJE0sTLq","hjemlerEOSForordningen883":["2","11-16","67","68"],"triggereIBruk":["KOMPETANSE"],"_id":"85fb78c8-66eb-4528-9583-ffc5f1d26161","barnetsBostedsland":["NORGE","IKKE_NORGE"],"mappe":["EØS","INNVILGET"],"_updatedAt":"2023-09-25T10:15:48Z","navnISystem":"Sekundærland standard","periodeType":"UTBETALING","nynorsk":[{"_type":"block","style":"normal","_key":"dc6af9fd5314","markDefs":[],"children":[{"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"a02191c1f8b10","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"c98bdfe90fc7"},{"marks":[],"text":". Du ","_key":"61cdb9d0b8d0","_type":"span"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"ad9b20fc423a","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"8d8e6a5632fb"},{"_type":"valgfeltV2","_key":"8caaf258be0c","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"marks":[],"text":" bur i ","_key":"d40e69618763","_type":"span"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"f4d6519f7f30"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"33f9a2357fba"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"08b4ec59b8e4","skalHaStorForbokstav":false},{"marks":[],"text":" i ","_key":"3d0d82d7b93b","_type":"span"},{"_key":"60e0c0a7800e","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"marks":[],"text":". ","_key":"927864e6ee12","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"d17af91b5717"},{"marks":[],"text":" har hovudansvaret for utbetaling av barnetrygd. Noreg utbetaler derfor skilnaden mellom barnetrygda i ","_key":"7db4f598825b","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"b4f55dd2b270"},{"marks":[],"text":" og norsk barnetrygd.","_key":"f32bd8649418","_type":"span"}]}],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"EØS","_createdAt":"2022-06-16T11:54:35Z","valgbarhet":"STANDARD","apiNavn":"innvilgetSekundaerlandStandard","hjemler":["2","4","11"],"behandlingstema":"EØS","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"bokmaal":[{"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"e03bacf6d3350","_type":"span"},{"_type":"eosFlettefelt","_key":"b09fe3a80b3f","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":". Du ","_key":"6855a5102055"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"47de250a32ca","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"4fab6d7425ea"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"6ee4a18e684e","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bor i ","_key":"4dfdf44a8a51"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"46a28b940eb7"},{"text":". Den andre forelderen ","_key":"543e9cf23cb5","_type":"span","marks":[]},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"a67b1878781e"},{"_type":"span","marks":[],"text":" i ","_key":"f930990d76f9"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"54d00f7634b7"},{"_type":"span","marks":[],"text":". ","_key":"0d59272b167c"},{"_key":"928213c74637","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"marks":[],"text":" har hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i ","_key":"2ebc94aeba9b","_type":"span"},{"_key":"557b43d232e6","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":" og norsk barnetrygd.","_key":"c1e795369567"}],"_type":"block","style":"normal","_key":"c812bc23355e","markDefs":[]}],"visningsnavn":"21. Sekundærland standard","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"begrunnelsetype":"INNVILGET"},{"resultat":"INNVILGELSE","nynorsk":[{"_type":"block","style":"normal","_key":"7529e12dddcb","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"dc4f363975010"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"587e9f9ca132"},{"_type":"span","marks":[],"text":". Du ","_key":"1b3a4f60180f"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"1cf188c00a1e"},{"_type":"span","marks":[],"text":". ","_key":"b25610edbba5"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"58ca033136e0","skalHaStorForbokstav":true},{"text":" bur i ","_key":"b31f15023aca","_type":"span","marks":[]},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"6d8c79480bbc"},{"marks":[],"text":". Den andre forelderen ","_key":"1a57b7d06d12","_type":"span"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"971cfaba390e","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"f9a2b9e28c91"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"f84ed78cd688"},{"_type":"span","marks":[],"text":". Du får derfor heile barnetrygda frå Noreg.","_key":"e15d176e8970"}]}],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"_updatedAt":"2023-09-25T10:15:48Z","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"5516f8ded0a40","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"4e8cc450c424"},{"_type":"span","marks":[],"text":". Du ","_key":"7c01f6de4d9f"},{"_key":"5d283247b968","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2"},{"_key":"d63bbe76f5c8","_type":"span","marks":[],"text":". "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"fce88f3ce052","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bor i ","_key":"072963c1c3f8"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"340eb5a74f4d"},{"text":". Den andre forelderen ","_key":"1aaf793cc761","_type":"span","marks":[]},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"19e09654202f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"f66f5dd01ae7"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"46dfdb327d39"},{"_type":"span","marks":[],"text":". Du får derfor hele barnetrygden fra Norge. ","_key":"f2a562335b93"}],"_type":"block","style":"normal","_key":"f0bc94fff663"}],"visningsnavn":"1. Primærland standard","_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","_createdAt":"2022-05-23T11:59:15Z","mappe":["EØS","INNVILGET"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER"],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"periodeType":"UTBETALING","tema":"EØS","behandlingstema":"EØS","vedtakResultat":"INNVILGET_ELLER_ØKNING","hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"INNVILGET","_id":"88332d28-ba43-425a-80a3-c714d9b55e38","navnISystem":"Primærland standard","apiNavn":"innvilgetPrimarlandStandard","hjemler":["2","4","11"],"triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD"},{"_createdAt":"2022-08-26T11:49:42Z","_id":"8b6c35d4-6938-4985-a88d-085a73cc1ec1","_updatedAt":"2023-09-25T10:15:48Z","visningsnavn":"24. Sekundærland UK standard","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","hjemlerSeperasjonsavtalenStorbritannina":["29"],"begrunnelsetype":"INNVILGET","tema":"EØS","triggereIBruk":["KOMPETANSE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND"],"navnISystem":"Sekundærland UK standard","behandlingstema":"EØS","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"76b31b2850c70"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"04c05bcbe6b9"},{"_type":"span","marks":[],"text":". Du ","_key":"1b08ca45f0a1"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"0bb68009bac0","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"26158f473c23"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"62e7ac415467"},{"_type":"span","marks":[],"text":". Den andre forelderen jobber i ","_key":"1c0b29ba9c3b"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"b26c7a92630d"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen har Storbritannia hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i Storbritannia og norsk barnetrygd.","_key":"f9ee34b35bbf"}],"_type":"block","style":"normal","_key":"be3f5b2cb8dd"}],"hjemler":["2","4","11"],"barnetsBostedsland":["IKKE_NORGE"],"valgbarhet":"STANDARD","mappe":["EØS","INNVILGET"],"apiNavn":"innvilgetSekundaerlandUkStandard","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"UTBETALING","nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"60db05e024da0","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"ab976b462f85"},{"_type":"span","marks":[],"text":". Du ","_key":"7b26e74b1ba0"},{"_key":"e744d28b75d5","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". Barnet bur i ","_key":"620806a4d124"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"86be57bac49f"},{"_type":"span","marks":[],"text":". Den andre forelderen jobbar i ","_key":"d827f54c74e4"},{"_key":"1d87fb8dcfde","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen har Storbritannia hovudansvaret for utbetaling av barnetrygd. Noreg utbetalar derfor forskjellen mellom barnetrygda i Storbritannia og norsk barnetrygd.","_key":"2baafcb75727"}],"_type":"block","style":"normal","_key":"07b49d85f8b1"}]},{"hjemler":["2"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","_updatedAt":"2023-09-25T10:15:48Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"apiNavn":"innvilgetTilleggstekstsekundaerDeltBostedAnnenForelderIkkeRett","_type":"begrunnelse","periodeType":"UTBETALING","_createdAt":"2022-10-17T10:43:29Z","triggereIBruk":["KOMPETANSE"],"mappe":["EØS","INNVILGET"],"visningsnavn":"38. Tilleggstekst sekundær delt bosted annen forelder ikke rett","behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","begrunnelsetype":"INNVILGET","tema":"EØS","nynorsk":[{"style":"normal","_key":"78e61d8ad2e0","markDefs":[],"children":[{"marks":[],"text":"Du får heile barnetrygda Noreg utbetalar for barn fødd ","_key":"b3879a9b72160","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"5fd0a213c001"},{"_type":"span","marks":[],"text":" som har delt bustad. Den andre forelderen har ikkje rett til barnetrygd. Barnetrygda kan derfor ikke delast.","_key":"83c6ffbc0639"}],"_type":"block"}],"_id":"93c11d77-9f4d-4eb9-b127-f98945890269","navnISystem":"Tilleggstekst sekundær delt bosted annen forelder ikke rett","bokmaal":[{"children":[{"text":"Du får hele barnetrygden Norge utbetaler for barn født ","_key":"a2b2aa17fcab0","_type":"span","marks":[]},{"_type":"eosFlettefelt","_key":"401484cec8db","flettefelt":"barnasFodselsdatoer"},{"marks":[],"text":" som har delt bosted. Den andre forelderen har ikke rett til barnetrygd. Barnetrygden kan derfor ikke deles.","_key":"7b26ba6d7582","_type":"span"}],"_type":"block","style":"normal","_key":"c90748b99c27","markDefs":[]}],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"valgbarhet":"TILLEGGSTEKST"},{"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden er endret fordi det har vært en satsendring og en endring av valutakursen.","_key":"1650443bed010"}],"_type":"block","style":"normal","_key":"999cc27769fe"}],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"navnISystem":"Tilleggstekst satsendring og valutajustering","visningsnavn":"31. Tilleggstekst satsendring og valutajustering","_type":"begrunnelse","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi det har vore ei satsendring og ei endring av valutakursen.","_key":"ff948e4d662d0"}],"_type":"block","style":"normal","_key":"f4ab76294b09"}],"_createdAt":"2022-09-23T11:40:55Z","triggereIBruk":["KOMPETANSE"],"_id":"97cb8815-e2d7-4d16-b6cd-a5892b2383c8","mappe":["EØS","INNVILGET"],"apiNavn":"innvilgetTilleggstekstSatsendringOgValutajustering","behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","_updatedAt":"2023-09-25T10:15:48Z","_rev":"h26rAhFEYSUtDGXJE0sTLq","tema":"EØS","valgbarhet":"TILLEGGSTEKST","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"]},{"behandlingstema":"EØS","_rev":"BtltdVb0HP4g4WJfnr5yMJ","vedtakResultat":"IKKE_INNVILGET","hjemlerEOSForordningen883":["2","11-16","67"],"_id":"98973584-17c8-4a6f-9932-0cadaf59c293","_updatedAt":"2023-09-25T10:42:38Z","hjemler":["2","4","5"],"triggereIBruk":["VILKÅRSVURDERING"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"87021d503e410"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"ac4a1890d927"},{"_type":"span","marks":[],"text":" fordi du jobber i Norge som utsendt fra et annet EØS-land. Vi har derfor kommet fram til at lovgivningen i det andre EØS-landet gjelder for deg.","_key":"4ec38a385e66"}],"_type":"block","style":"normal","_key":"73f10e52f7a8"}],"apiNavn":"avslagEosUtsendtArbeidstakerFraAnnetEosLand","begrunnelsetype":"AVSLAG","nynorsk":[{"_key":"5eb1fc0e3b89","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"da11411000480"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"08d358343d96"},{"_type":"span","marks":[],"text":" fordi du du jobbar i Noreg som utsendt frå eit anna EØS-land. Vi har derfor kome fram til at lovgjevnaden i det andre EØS-landet gjeld for deg.","_key":"f7eb95d36326"}],"_type":"block","style":"normal"}],"eosVilkaar":["BOSATT_I_RIKET"],"visningsnavn":"4. Utsendt arbeidstaker fra annet EØS-land","_type":"begrunnelse","periodeType":"INGEN_UTBETALING","tema":"EØS","_createdAt":"2022-11-01T13:32:57Z","valgbarhet":"STANDARD","mappe":["EØS","AVSLAG"],"navnISystem":"Utsendt arbeidstaker fra annet EØS-land"},{"hjemler":["2","4","11"],"_id":"9b56a18d-925a-41cf-ba30-1abff7f89ddf","bokmaal":[{"_type":"block","style":"normal","_key":"4a8f3e7c2160","markDefs":[],"children":[{"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"bed86d4b64d60","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"b232c6a3cd0c"},{"_type":"span","marks":[],"text":". Du ","_key":"640d71bbab93"},{"_key":"6a48ff9299bb","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"2cfadd4243b0"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"e0aca8a8328c"},{"marks":[],"text":". Du har ansvar for barnet alene. ","_key":"2a84bd46b321","_type":"span"},{"_type":"eosFlettefelt","_key":"03af8bfff508","flettefelt":"barnetsBostedsland"},{"_type":"span","marks":[],"text":" har hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i ","_key":"c59e1abd9c30"},{"_type":"eosFlettefelt","_key":"15c92dba48a8","flettefelt":"sokersAktivitetsland"},{"_type":"span","marks":[],"text":" og norsk barnetrygd. ","_key":"73b1f37bf213"}]}],"navnISystem":"Sekundærland aleneansvar","hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","tema":"EØS","nynorsk":[{"style":"normal","_key":"a3352276173c","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"a47aaf72b0710"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"686431cfd6fb"},{"_type":"span","marks":[],"text":". Du ","_key":"41665d93df65"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"05ea724e0b21"},{"_type":"span","marks":[],"text":". Barnet bur i ","_key":"fca9d2e9dd48"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"69b89e533586"},{"_type":"span","marks":[],"text":". Du har ansvar for barnet åleine. ","_key":"a4203cae90be"},{"_key":"7bdeeec9700c","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":" har hovudansvaret for utbetaling av barnetrygd. Noreg utbetalar derfor forskjellen mellom barnetrygda i ","_key":"1389429a4fb0"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"1842d098b751"},{"_type":"span","marks":[],"text":" og norsk barnetrygd. ","_key":"ae1cfd77a3d1"}],"_type":"block"}],"apiNavn":"innvilgetSekundaerlandAleneansvar","visningsnavn":"23. Sekundærland aleneansvar","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_rev":"h26rAhFEYSUtDGXJE0sTLq","triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"annenForeldersAktivitet":["IKKE_AKTUELT"],"_updatedAt":"2023-09-25T10:15:48Z","behandlingstema":"EØS","_type":"begrunnelse","_createdAt":"2022-08-26T11:42:55Z","mappe":["EØS","INNVILGET"]},{"apiNavn":"innvilgetTilleggstekstSatsendring","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"begrunnelsetype":"INNVILGET","nynorsk":[{"markDefs":[],"children":[{"text":"Barnetrygda er endra fordi det har vore ei satsendring.","_key":"ff595fe5992f0","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"c38056e19932"},{"_key":"9d0cdc8a459e","markDefs":[],"children":[{"marks":[],"text":"\n","_key":"1df47c8720760","_type":"span"}],"_type":"block","style":"normal"}],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden er endret fordi det har vært en satsendring.","_key":"9098817ffd870"}],"_type":"block","style":"normal","_key":"69607b8168cf"}],"visningsnavn":"29. Tilleggstekst satsendring","_rev":"h26rAhFEYSUtDGXJE0sTLq","periodeType":"UTBETALING","navnISystem":"Tilleggstekst satsendring","tema":"EØS","_createdAt":"2022-09-05T11:58:16Z","valgbarhet":"TILLEGGSTEKST","_id":"9ed4bab6-7c43-4177-b661-4c9a1f6960ca","mappe":["EØS","INNVILGET"],"_updatedAt":"2023-09-25T10:15:48Z"},{"navnISystem":"Søker bor ikke i EØS-land","apiNavn":"opphorSoekerBorIkkeIEosLand","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"42c604156b80"},{"_type":"eosFlettefelt","_key":"772843d50b50","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi du ikkje lenger bur i eit EØS-land.","_key":"b2b71ac6925a"}],"_type":"block","style":"normal","_key":"0057da8f07b9"}],"_updatedAt":"2023-09-25T10:41:48Z","hjemler":["2","4","11"],"visningsnavn":"10. Søker bor ikke i EØS-land","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","hjemlerEOSForordningen883":["2","11-16","67","68"],"tema":"EØS","triggereIBruk":["KOMPETANSE"],"_id":"a03cad98-177d-416f-b98b-b4ba2c2fd261","behandlingstema":"EØS","_rev":"h26rAhFEYSUtDGXJE0vD7v","begrunnelsetype":"OPPHØR","_createdAt":"2022-10-17T12:02:57Z","valgbarhet":"STANDARD","barnetsBostedsland":["NORGE","IKKE_NORGE"],"mappe":["EØS","OPPHØR"],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND","TO_PRIMÆRLAND"],"periodeType":"INGEN_UTBETALING","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"bokmaal":[{"_key":"e7dde6b8d10e","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"9f857b3b06d70"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"c77837af1e9b"},{"_type":"span","marks":[],"text":" fordi du ikke lenger bor i et EØS-land.","_key":"4bd2bea71224"}],"_type":"block","style":"normal"}]},{"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND"],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"navnISystem":"Ikke ansvar for barn","vedtakResultat":"IKKE_INNVILGET","tema":"EØS","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"05cb328969c20"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"075abf3b90e8"},{"marks":[],"text":" fordi du ikkje lenger har ansvar for ","_key":"8160c92c1c36","_type":"span"},{"_type":"valgfeltV2","_key":"c4b0812b74c7","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"marks":[],"text":".","_key":"3fa25a78acb5","_type":"span"}],"_type":"block","style":"normal","_key":"334f80430a77"}],"valgbarhet":"STANDARD","_id":"a46e9cdd-0036-44c0-aad1-0ab6bcb55b89","triggereIBruk":["KOMPETANSE"],"_updatedAt":"2023-09-25T10:15:48Z","apiNavn":"opphorIkkeAnsvarForBarn","hjemler":["2","4","11"],"visningsnavn":"7. Ikke ansvar for barn","_rev":"h26rAhFEYSUtDGXJE0sTLq","_type":"begrunnelse","periodeType":"UTBETALING","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"hjemlerEOSForordningen883":["2","11-16","67"],"begrunnelsetype":"OPPHØR","_createdAt":"2022-08-12T12:47:32Z","mappe":["EØS","OPPHØR"],"bokmaal":[{"_type":"block","style":"normal","_key":"7e26cac5cebe","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"07b4c82e904f0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"1272b8a20ccd"},{"marks":[],"text":" fordi du ikke lenger har ansvar for ","_key":"84422ed932ef","_type":"span"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"8c6a7007211d","skalHaStorForbokstav":false},{"_key":"f91c1eaeed33","_type":"span","marks":[],"text":"."}]}]},{"eosVilkaar":["BOR_MED_SØKER"],"triggereIBruk":["VILKÅRSVURDERING"],"_id":"ae8fd8ff-acb6-43bb-9726-464ce894dd86","mappe":["EØS","AVSLAG"],"bokmaal":[{"markDefs":[],"children":[{"_key":"06360b079efa0","_type":"span","marks":[],"text":"Barnetrygd for barn født "},{"_type":"eosFlettefelt","_key":"8942b8e0395c","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at du ikke har ansvar for ","_key":"2c7bbf19f017"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"05b5924a3d4e","skalHaStorForbokstav":false},{"marks":[],"text":".","_key":"c6dc0b05a9fe","_type":"span"}],"_type":"block","style":"normal","_key":"ce7d2e70e0fe"}],"navnISystem":"Vurdering ikke ansvar for barn","hjemler":["2","4","5"],"_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67"],"begrunnelsetype":"AVSLAG","tema":"EØS","_createdAt":"2022-11-01T14:17:45Z","apiNavn":"avslagEosVurderingIkkeAnsvarForBarn","visningsnavn":"13. Vurdering ikke ansvar for barn","behandlingstema":"EØS","_rev":"h26rAhFEYSUtDGXJE0vGI2","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","nynorsk":[{"style":"normal","_key":"37ef2d5267c3","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"c2df64d52b730"},{"_type":"eosFlettefelt","_key":"af4810c0cda7","flettefelt":"barnasFodselsdatoer"},{"_key":"c92b04b1eb8b","_type":"span","marks":[],"text":" fordi vi har kome fram til at du ikkje har ansvar for "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"bd6901295214","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":".","_key":"6ae1892e1e85"}],"_type":"block"}],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:43:42Z"},{"_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67"],"periodeType":"INGEN_UTBETALING","valgbarhet":"STANDARD","mappe":["EØS","AVSLAG"],"vedtakResultat":"IKKE_INNVILGET","tema":"EØS","triggereIBruk":["VILKÅRSVURDERING"],"navnISystem":"Ikke lovlig opphold som EØS-borger","eosVilkaar":["LOVLIG_OPPHOLD"],"hjemler":["2","4","5"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"3fb4b89e79a10"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"3ba850d0e12e"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at du ikkje har lovleg opphald som EØS-borgar.","_key":"7fd8fd2fafb1"}],"_type":"block","style":"normal","_key":"0f197ec300c2"}],"_id":"b2d1a325-e8b4-42ed-9cc2-497dcc8e9355","_updatedAt":"2023-09-25T10:43:29Z","apiNavn":"avslagEosIkkeLovligOppholdSomEosBorger","visningsnavn":"9. Ikke lovlig opphold som EØS-borger","behandlingstema":"EØS","_rev":"FuD004taptHFqBZyEy9ExJ","begrunnelsetype":"AVSLAG","_createdAt":"2022-11-01T13:51:24Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"8e81bd27d3b30"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"34ef4ba807a6"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at du ikke har lovlig opphold som EØS-borger.","_key":"169f1a598601"}],"_type":"block","style":"normal","_key":"dced7f570797","markDefs":[]}]},{"periodeType":"INGEN_UTBETALING","tema":"EØS","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:41:51Z","hjemler":["2","4","11"],"visningsnavn":"11. Arbeider mer enn 25 prosent i annet EØS-land","vedtakResultat":"IKKE_INNVILGET","barnetsBostedsland":["NORGE","IKKE_NORGE"],"bokmaal":[{"_key":"4f90dedf2d7f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"255620271a350"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"71ed8f0cb95d"},{"_type":"span","marks":[],"text":" fordi du jobber i Norge og et annet EØS land. Du jobber mer enn 25 prosent i det andre EØS-landet, der du også bor. Vi har derfor kommet fram til at lovgivningen i det andre EØS-landet gjelder for deg.","_key":"1b29a725dd38"}],"_type":"block","style":"normal"},{"children":[{"text":"","_key":"7dfce752c3400","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"05f34671693b","markDefs":[]}],"navnISystem":"Arbeider mer enn 25 prosent i annet EØS-land","_rev":"h26rAhFEYSUtDGXJE0vDAy","_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"OPPHØR","nynorsk":[{"children":[{"text":"Barnetrygd for barn fødd ","_key":"676ec48837490","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"2e0ccc0ef4c4"},{"_type":"span","marks":[],"text":" fordi du jobbar i Noreg og eit anna EØS land. Du jobbar meir enn 25 prosent i det andre EØS-landet, der du også bur. Vi har derfor kome fram til at lovgjevnaden i det andre EØS-landet gjeld for deg.","_key":"ef5c02c470d1"}],"_type":"block","style":"normal","_key":"7bd7ea8ec58d","markDefs":[]}],"_id":"b46d85e2-3ff4-47cc-b516-2a35c55320ed","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND","TO_PRIMÆRLAND"],"triggereIBruk":["KOMPETANSE"],"mappe":["EØS","OPPHØR"],"apiNavn":"opphorArbeiderMerEnn25ProsentIAnnetEosLand","_createdAt":"2022-10-26T13:07:09Z"},{"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","hjemlerEOSForordningen883":["2","11-16","67","68"],"tema":"EØS","hjemler":["2","4","11"],"visningsnavn":"6. Primærland UK aleneansvar","behandlingstema":"EØS","_rev":"h26rAhFEYSUtDGXJE0sTLq","mappe":["EØS","INNVILGET"],"triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","_id":"b69a7aaa-1135-4349-bcc6-dbcd204fd859","barnetsBostedsland":["IKKE_NORGE"],"nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"6c20a04b52a20","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"308288399035"},{"marks":[],"text":". Du ","_key":"ad6bbf62f4bc","_type":"span"},{"_key":"9a785a68dd0c","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". ","_key":"a7c1424c63e4"},{"_key":"ae0f4b6760f6","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" bur i Storbritannia. Du har ansvar for ","_key":"f1215f66076c"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"3ffc47f58427","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" åleine. Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen får du heile barnetrygda frå Noreg.","_key":"dd22b1bd807d"}],"_type":"block","style":"normal","_key":"39ad65175da6"}],"_updatedAt":"2023-09-25T10:15:48Z","apiNavn":"innvilgetPrimarlandUKAleneansvar","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"hjemlerSeperasjonsavtalenStorbritannina":["29"],"periodeType":"UTBETALING","navnISystem":"Primærland UK aleneansvar","annenForeldersAktivitet":["MOTTAR_PENSJON","IKKE_AKTUELT"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn født ","_key":"5db613fb957b0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"00524e8e53e6"},{"_type":"span","marks":[],"text":". Du ","_key":"f1452062c7e2"},{"valgReferanse":{"_type":"reference","_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb"},"_type":"valgfeltV2","_key":"92be52b80b7b","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"f7dbeafabfb3"},{"skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"f39aa48acdff"},{"_key":"5392165b5889","_type":"span","marks":[],"text":" bor i Storbritannia. Du har ansvar for "},{"_type":"valgfeltV2","_key":"2481af7a5f8b","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":" alene. Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen får du hele barnetrygden fra Norge.","_key":"81b856303b55"}],"_type":"block","style":"normal","_key":"5eea365018fa","markDefs":[]}],"begrunnelsetype":"INNVILGET","_createdAt":"2022-05-23T12:54:21Z"},{"_updatedAt":"2023-09-25T10:41:34Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"navnISystem":"Sentrum for livsinteresse","apiNavn":"opphorSentrumForLivsinteresse","visningsnavn":"6. Sentrum for livsinteresse","_type":"begrunnelse","valgbarhet":"STANDARD","barnetsBostedsland":["NORGE","IKKE_NORGE"],"triggereIBruk":["KOMPETANSE"],"mappe":["EØS","OPPHØR"],"hjemler":["2","4","11"],"_rev":"BtltdVb0HP4g4WJfnr5vPJ","hjemlerEOSForordningen883":["2","11-16","67"],"periodeType":"INGEN_UTBETALING","begrunnelsetype":"OPPHØR","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"6fc11e5771e20"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"4356d8e92616"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at du ikkje lenger har rett til barnetrygd frå Noreg.","_key":"b25a8e133ce7"}],"_type":"block","style":"normal","_key":"67c60374d389"}],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND"],"_createdAt":"2022-08-12T12:44:22Z","_id":"b9c97ea2-670b-4d1e-9d3c-bd91f577cfce","vedtakResultat":"IKKE_INNVILGET","tema":"EØS","bokmaal":[{"markDefs":[],"children":[{"text":"Barnetrygd for barn født ","_key":"2ffab768f76f0","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"b7ea1bdf6294"},{"_type":"span","marks":[],"text":" fordi vi har kommet fram til at du ikke lenger har rett til barnetrygd fra Norge.","_key":"a2385d38bd3e"}],"_type":"block","style":"normal","_key":"ac26bc14e848"}]},{"barnetsBostedsland":["NORGE","IKKE_NORGE"],"mappe":["EØS","INNVILGET"],"apiNavn":"innvilgetTilleggstekstSekundaerIkkeFaattSvarPaaSed","visningsnavn":"40. Tilleggstekst sekundær ikke fått svar på SED","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","_createdAt":"2022-11-15T09:04:27Z","nynorsk":[{"_type":"block","style":"normal","_key":"edc0518df3af","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Vi har ikkje fått svar på opplysningar vi har bedt om frå ","_key":"1ce240ceaf3f0"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"676501eaffd3"},{"_type":"span","marks":[],"text":". Noreg utbetalar derfor forskjellen mellom barnetrygda familien kan ha rett på frå ","_key":"c694cb486d31"},{"_key":"2b8f8a4dd1f2","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":" og norsk barnetrygd. Saka må vurderast på nytt dersom vi får svar frå ","_key":"74050fda4acb"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"95bb972d711c"},{"marks":[],"text":".","_key":"9991844c74cf","_type":"span"}]}],"valgbarhet":"TILLEGGSTEKST","_id":"begrunnelse-069a3c64-e9f9-437f-9d4f-5cdcbd68333b","behandlingstema":"EØS","_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"INNVILGET","tema":"EØS","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"bokmaal":[{"_type":"block","style":"normal","_key":"2477b3cb0830","markDefs":[],"children":[{"marks":[],"text":"Vi har ikke fått svar på opplysninger vi har bedt om fra ","_key":"05c9a27168030","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"c7c58af8d48e"},{"_key":"d0084ac3555c","_type":"span","marks":[],"text":". Norge utbetaler derfor forskjellen mellom barnetrygden familien kan ha rett på fra "},{"_type":"eosFlettefelt","_key":"6d0340812da2","flettefelt":"annenForeldersAktivitetsland"},{"_type":"span","marks":[],"text":" og norsk barnetrygd. Saken må vurderes på nytt hvis vi får svar fra ","_key":"099527aef188"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"32924eaf4ae4"},{"marks":[],"text":".","_key":"0b27ca5e6370","_type":"span"}]}],"hjemler":["2","4","11"],"periodeType":"UTBETALING","triggereIBruk":["KOMPETANSE"],"_updatedAt":"2023-09-25T10:15:48Z","navnISystem":"Tilleggstekst sekundær ikke fått svar på SED","_rev":"h26rAhFEYSUtDGXJE0sTLq"},{"kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"FORTSATT_INNVILGET","nynorsk":[{"children":[{"marks":[],"text":"Du får full barnetrygd frå Noreg fordi det ikkje er rett til barnetrygd frå ","_key":"d1daa1c2b009","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"35611f36b8c9"},{"marks":[],"text":".","_key":"237bb83425a1","_type":"span"}],"_type":"block","style":"normal","_key":"74110d41a1e1","markDefs":[]}],"valgbarhet":"TILLEGGSTEKST","apiNavn":"fortsattInnvilgetTilleggstekstSekundaerFullUtbetaling","behandlingstema":"EØS","_createdAt":"2022-12-01T08:23:24Z","triggereIBruk":["KOMPETANSE"],"_id":"begrunnelse-0a45f374-6bf2-4dd5-b97f-32f66ba13c33","bokmaal":[{"_key":"0e8ce0146345","markDefs":[],"children":[{"text":"Du får full barnetrygd fra Norge fordi det ikke er rett til barnetrygd fra ","_key":"485fb6f6b0c6","_type":"span","marks":[]},{"_type":"eosFlettefelt","_key":"93bd9d2c36b2","flettefelt":"annenForeldersAktivitetsland"},{"_key":"6cfff326a0b9","_type":"span","marks":[],"text":"."}],"_type":"block","style":"normal"}],"visningsnavn":"25. Tilleggstekst sekundær full utbetaling","tema":"EØS","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"begrunnelsetype":"FORTSATT_INNVILGET","_updatedAt":"2023-09-25T10:46:24Z","vedtakResultat":"INGEN_ENDRING","barnetsBostedsland":["NORGE","IKKE_NORGE"],"mappe":["EØS","FORTSATT_INNVILGET"],"navnISystem":"Tilleggstekst sekundær full utbetaling","_rev":"BtltdVb0HP4g4WJfnr6CIJ"},{"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_type":"begrunnelse","_updatedAt":"2023-09-24T16:51:22Z","annenForeldersAktivitet":["UTSENDT_ARBEIDSTAKER","UTSENDT_ARBEIDSTAKER_FRA_NORGE"],"hjemler":["2","4","11"],"hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"INNVILGET","_createdAt":"2023-09-19T13:48:36Z","_id":"begrunnelse-180a3158-b004-499c-b0d4-65d689994419","bokmaal":[{"style":"normal","_key":"ff7f41c79730","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"3d89e79030be0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"a769c2d8fd5c"},{"_type":"span","marks":[],"text":". Du ","_key":"2e5dd47e504b"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"9858b2df5f08","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"b9d068e648c0"},{"_type":"eosFlettefelt","_key":"999a69647cf7","flettefelt":"sokersAktivitetsland"},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"ba50a2f608bc"},{"_type":"eosFlettefelt","_key":"92a13fa500b3","flettefelt":"barnetsBostedsland"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"ce5d145b945f"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"48f4c4b249dc","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"30607e0c5840"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"35a740709cba"},{"_type":"span","marks":[],"text":" og tilhører derfor norsk lovgivning. Du får hele barnetrygden fra Norge, fordi den andre forelderen tilhører norsk lovgivning.","_key":"d10c63fa2d68"}],"_type":"block"}],"navnISystem":"Selvstendig rett primærland utsendt arbeidstaker ","_rev":"h26rAhFEYSUtDGXJE0Wc33","vedtakResultat":"INNVILGET_ELLER_ØKNING","triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","mappe":["EØS","INNVILGET"],"apiNavn":"innvilgetSelvstendigRettPrimaerlandUtsendtArbeidstaker","visningsnavn":"46. Selvstendig rett primærland utsendt arbeidstaker ","periodeType":"UTBETALING","tema":"EØS","nynorsk":[{"style":"normal","_key":"67d44ca69e89","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"3013a5906f8e0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"b4c448ceeb92"},{"_key":"1f6323eb2d33","_type":"span","marks":[],"text":". Du "},{"_type":"valgfeltV2","_key":"0a4a84e0eccf","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"}},{"_type":"span","marks":[],"text":" i ","_key":"d3c7e5021308"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"b18f6497ec88"},{"_type":"span","marks":[],"text":". Barnet bur i ","_key":"3b7f3e7b0d70"},{"_key":"8e2c6e75a66f","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"13788d8b816f"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"fed4ae9a0d33","skalHaStorForbokstav":false},{"text":" i ","_key":"719160e526c0","_type":"span","marks":[]},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"f5fb285e2fd1"},{"_type":"span","marks":[],"text":" og tilhøyrer derfor norsk lovgjeving. Du får heile barnetrygda frå Noreg, fordi den andre forelderen tilhøyrer norsk lovgjeving.","_key":"dd01d6c23e72"}],"_type":"block"}],"barnetsBostedsland":["IKKE_NORGE"]},{"navnISystem":"Selvstendig rett utsendt arbeidstaker fra annet EØS-land","hjemlerEOSForordningen883":["2","11-16","67"],"tema":"EØS","nynorsk":[{"_type":"block","style":"normal","_key":"ef5c000b3c72","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"86e31ccfb6df0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"67e2d8188fed"},{"_type":"span","marks":[],"text":" fordi den andre forelderen jobbar i Noreg som utsendt frå eit anna EØS-land, og derfor ikkje tilhøyrer norsk lovgjeving. ","_key":"da8c82f5c753"}]}],"_id":"begrunnelse-1b03eb23-2e4a-49a9-a5e7-3d967faacca6","hjemler":["2","4","5"],"valgbarhet":"STANDARD","eosVilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"AVSLAG","_createdAt":"2023-09-19T14:23:27Z","mappe":["EØS","AVSLAG"],"_updatedAt":"2023-09-24T17:08:10Z","apiNavn":"avslagSelvstendigRettUtsendtArbeidstakerFraAnnetEosLand","visningsnavn":"19. Selvstendig rett utsendt arbeidstaker fra annet EØS-land","_rev":"h26rAhFEYSUtDGXJE0X04h","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","periodeType":"INGEN_UTBETALING","triggereIBruk":["VILKÅRSVURDERING"],"bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"2fd8096344c90","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"7b9d8a22f4f5"},{"_type":"span","marks":[],"text":" fordi den andre forelderen jobber i Norge som utsendt fra et annet EØS-land, og derfor ikke tilhører norsk lovgivning.","_key":"97f720ae6342"}],"_type":"block","style":"normal","_key":"09c48ff377f8"}]},{"hjemler":["2","4","11"],"visningsnavn":"49. Selvstendig rett sekundærland UK og utland standard","begrunnelsetype":"INNVILGET","triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","navnISystem":"Selvstendig rett sekundærland UK og utland standard","_rev":"BtltdVb0HP4g4WJfnqVIfo","vedtakResultat":"INNVILGET_ELLER_ØKNING","hjemlerSeperasjonsavtalenStorbritannina":["29"],"nynorsk":[{"style":"normal","_key":"5e12e0b47181","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"f4ee8ce2900d0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"6f972f50b190"},{"marks":[],"text":". Du","_key":"1facf40af5f5","_type":"span"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"5641bd78f555","skalHaStorForbokstav":false},{"_key":"793666d547ea","_type":"span","marks":[],"text":" i "},{"_type":"eosFlettefelt","_key":"c87600c6717d","flettefelt":"sokersAktivitetsland"},{"marks":[],"text":". Barnet bur i ","_key":"307ad61edc2f","_type":"span"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"815639f2e6e0"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"a21e0df87b10"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"df33b457e8b2","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"700640693160"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"6815a2e5d49c"},{"_type":"span","marks":[],"text":" og tilhøyrer derfor norsk lovgjeving. Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Storbritannia har hovudansvaret for utbetaling av barnetrygd. Noreg utbetalar skilnaden mellom barnetrygda i Storbritannia og norsk barnetrygd.","_key":"60439fbc62ac"}],"_type":"block"}],"_createdAt":"2023-09-19T14:07:01Z","bokmaal":[{"style":"normal","_key":"95547ec5b7e9","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"213300d189e50"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"bc24c382795c"},{"_key":"50d35362168c","_type":"span","marks":[],"text":". Du"},{"_type":"valgfeltV2","_key":"3c88b3cd3e8c","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"}},{"_key":"0fb9b4bdf31d","_type":"span","marks":[],"text":" i "},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"e59fd2e986c8"},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"db77888ec35f"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"fcd551a7b3f1"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"9e1eefc57146"},{"_type":"valgfeltV2","_key":"e2bfc84d7a04","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"}},{"_type":"span","marks":[],"text":" i ","_key":"255bf4d1692c"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"01f2521dc265"},{"_type":"span","marks":[],"text":" og tilhører derfor norsk lovgivning. Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Storbritannia har hovedansvaret for utbetaling av barnetrygd. Norge utbetaler forskjellen mellom barnetrygden i Storbritannia og norsk barnetrygd.","_key":"f27a719a65a8"}],"_type":"block"}],"kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"UTBETALING","tema":"EØS","barnetsBostedsland":["IKKE_NORGE"],"mappe":["EØS","INNVILGET"],"apiNavn":"innvilgetSelvstendigRettSekundaerlandUkOgUtlandStandard","_id":"begrunnelse-1ef1880f-ed1d-4bc8-bb82-eb02656ae61c","_updatedAt":"2023-09-24T17:05:57Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER","ARBEIDER","SELVSTENDIG_NÆRINGSDRIVENDE","ARBEIDER_PÅ_NORSK_SOKKEL"]},{"_createdAt":"2022-11-30T14:29:57Z","bokmaal":[{"_key":"337c158e45af","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"d42baaed109d"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"7498822722c7"},{"text":". Du ","_key":"2f00eeb4b8e1","_type":"span","marks":[]},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"491ae8de1704","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"e2edd6b05857"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"ccf9b6dbc76d","skalHaStorForbokstav":true},{"marks":[],"text":" bor i ","_key":"d8bae0a3874e","_type":"span"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"9b8eb0a86e98"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"dfe659ea0a99"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"6b3507f46130"},{"_type":"span","marks":[],"text":" i ","_key":"c20e88c9770a"},{"_key":"fe12c9a63100","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Du og den andre forelderen har rett til barnetrygd fra Norge og ","_key":"20cff20cf3e8"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"faddf488d79d"},{"_type":"span","marks":[],"text":" fordi dere jobber i andre land enn der ","_key":"21233fef8f3b"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"212865e6e6eb","skalHaStorForbokstav":false},{"marks":[],"text":" bor. Landet med den høyeste barnetrygden skal utbetale. Norsk barnetrygd er høyere enn barnetrygden i ","_key":"4814181d3cf1","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"16f84f702e38"},{"_key":"8c54d61daa77","_type":"span","marks":[],"text":". Du får derfor hele barnetrygden fra Norge."}],"_type":"block","style":"normal"}],"navnISystem":"Primærland to arbeidsland Norge utbetaler","behandlingstema":"EØS","_type":"begrunnelse","nynorsk":[{"_type":"block","style":"normal","_key":"bd28eee59b0a","markDefs":[],"children":[{"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"97cab5a0f659","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"0059fdfb66eb"},{"_type":"span","marks":[],"text":". Du ","_key":"1c49ef865165"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"40f7fbac4c82","skalHaStorForbokstav":false},{"marks":[],"text":". ","_key":"4dd3464f481c","_type":"span"},{"_type":"valgfeltV2","_key":"6dae2ea02abc","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_key":"7fd149fca798","_type":"span","marks":[],"text":" bur i "},{"_key":"8b1b9d6c9917","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"text":". Den andre forelderen ","_key":"e934250fd850","_type":"span","marks":[]},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"ca4644eb0eb9","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"440bcf91051d"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"ec085180fe03"},{"_key":"b6998e800665","_type":"span","marks":[],"text":". Du og den andre forelderen har rett til barnetrygd frå Noreg og "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"15c451526375"},{"_type":"span","marks":[],"text":" fordi de jobbar i andre land enn der ","_key":"31aa97f4cf96"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"43a70a2d1b9b","skalHaStorForbokstav":false},{"marks":[],"text":" bur. Landet med den høgaste barnetrygda skal utbetale. Norsk barnetrygd er høgare enn barnetrygda i ","_key":"8654157c3a5d","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"a72f344794fa"},{"_type":"span","marks":[],"text":". Du får derfor heile barnetrygda frå Noreg.","_key":"913725dbab98"}]}],"valgbarhet":"STANDARD","hjemlerEOSForordningen987":["58"],"periodeType":"UTBETALING","begrunnelsetype":"FORTSATT_INNVILGET","tema":"EØS","triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["IKKE_NORGE"],"mappe":["EØS","FORTSATT_INNVILGET"],"apiNavn":"fortsattInnvilgetPrimaerlandToArbeidslandNorgeUtbetaler","visningsnavn":"10. Primærland to arbeidsland Norge utbetaler","kompetanseResultat":["TO_PRIMÆRLAND"],"hjemlerEOSForordningen883":["2","11-16","67","68"],"_id":"begrunnelse-2338087e-5440-46e8-947d-f459c8fd7027","_updatedAt":"2023-09-25T10:15:50Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON"],"hjemler":["2","4","11"],"_rev":"h26rAhFEYSUtDGXJE0sV8b","vedtakResultat":"INGEN_ENDRING"},{"bokmaal":[{"_type":"block","style":"normal","_key":"bf4762085869","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"d7be2e4dc373"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"ed5809a8e122"},{"marks":[],"text":". Du ","_key":"86b3efdd3c15","_type":"span"},{"_key":"d4a6df61bc7f","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". ","_key":"7978aada4707"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"a1a1113ae741","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bor i ","_key":"6f0990e96512"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"179c469802ee"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"397568b658e7"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"b38b4dbf1b45","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"0ae32d6dbf41"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"7221721a1fe8"},{"_type":"span","marks":[],"text":". Du får derfor hele barnetrygden fra Norge.","_key":"0249aa5f4dc2"}]},{"children":[{"_key":"bac2a83012b7","_type":"span","marks":[],"text":""}],"_type":"block","style":"normal","_key":"bf33be99dee7","markDefs":[]}],"visningsnavn":"4. Primærland begge foreldre jobber i Norge","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"UTBETALING","begrunnelsetype":"FORTSATT_INNVILGET","_rev":"h26rAhFEYSUtDGXJE0sV8b","mappe":["EØS","FORTSATT_INNVILGET"],"apiNavn":"fortsattInnvilgetPrimaerlandBeggeForeldreJobberINorge","hjemler":["2","4","11"],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"_updatedAt":"2023-09-25T10:15:50Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON"],"_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","_createdAt":"2022-11-30T13:59:05Z","valgbarhet":"STANDARD","_id":"begrunnelse-2743d60b-3b53-485b-95c6-7ee1b7b12c16","navnISystem":"Primærland begge foreldre jobber i Norge","tema":"EØS","nynorsk":[{"markDefs":[],"children":[{"_key":"e079d18381e2","_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"4714124efbfd"},{"_key":"fb6030ec94e0","_type":"span","marks":[],"text":". Du "},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"ccf8150817bf","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"a440953b4bf3"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"00dc272d378a","skalHaStorForbokstav":true},{"text":" bur i ","_key":"8ad3daa1fefe","_type":"span","marks":[]},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"a9db5ffc8574"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"f68076a9b371"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"04d03985f100","skalHaStorForbokstav":false},{"marks":[],"text":" i ","_key":"ac848ed5ead5","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"6266e9b54b6e"},{"_type":"span","marks":[],"text":". Du får derfor heile barnetrygda frå Noreg.","_key":"325ab879230b"}],"_type":"block","style":"normal","_key":"cfffb5fd836b"}],"triggereIBruk":["KOMPETANSE"]},{"_createdAt":"2022-11-15T09:08:33Z","triggereIBruk":["KOMPETANSE"],"_id":"begrunnelse-2791f47a-92b9-401d-94d0-253d2035293b","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får hele barnetrygden utbetalt fra Norge. Fordi familien din betaler \"High Income Tax\" i Storbritannia, får dere ikke utbetalt barnetrygd fra Storbritannia.","_key":"2ba7cdc6495c0"}],"_type":"block","style":"normal","_key":"a5cc719946a0"}],"navnISystem":"Tilleggstekst UK full etterbetaling","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"UTBETALING","barnetsBostedsland":["NORGE","IKKE_NORGE"],"mappe":["EØS","INNVILGET"],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","_rev":"h26rAhFEYSUtDGXJE0sV8b","tema":"EØS","nynorsk":[{"markDefs":[],"children":[{"_key":"1893544829660","_type":"span","marks":[],"text":"Du får heile barnetrygda utbetalt frå Noreg. Fordi familien din betalar \"High Income Tax\" i Storbritannia, får de ikkje utbetalt barnetrygd frå Storbritannia."}],"_type":"block","style":"normal","_key":"7ca5e920b1fc"},{"children":[{"text":"\n","_key":"72436be3fed70","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"e337b84f41dc","markDefs":[]}],"_updatedAt":"2023-09-25T10:15:50Z","visningsnavn":"41. Tilleggstekst UK full etterbetaling","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND"],"behandlingstema":"EØS","hjemlerSeperasjonsavtalenStorbritannina":["29"],"begrunnelsetype":"INNVILGET","valgbarhet":"TILLEGGSTEKST","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"apiNavn":"innvilgetTilleggestekstUkFullEtterbetaling","hjemler":["2","4","11"]},{"hjemler":["2","4","11"],"visningsnavn":"3. Primærland begge foreldre bosatt i Norge","behandlingstema":"EØS","_updatedAt":"2023-09-25T10:44:19Z","annenForeldersAktivitet":["MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"64a92c8f8e80","_type":"span"},{"_key":"1d025e856046","flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt"},{"text":". ","_key":"ab33315113d1","_type":"span","marks":[]},{"_key":"21311684194f","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" bor i ","_key":"fbb3a49825e7"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"1718fdff0ef5"},{"_type":"span","marks":[],"text":". Du og den andre forelderen bor i Norge. Det er ikke rett til barnetrygd fra ","_key":"3b23d0268c96"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"2c440cba51d3"},{"text":" fordi vi har kommet fram til at norsk lovgivning gjelder for barnet. Du får derfor hele barnetrygden fra Norge. ","_key":"5b01677645fb","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"b633e2735fd9"}],"_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"tema":"EØS","nynorsk":[{"children":[{"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"cc2b039e0057","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"e7bb9dbc0375"},{"_type":"span","marks":[],"text":". ","_key":"06f352c3ee3b"},{"_key":"5f4300fa06f3","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" bur i ","_key":"1a3f104134e9"},{"_type":"eosFlettefelt","_key":"867edaa3dd9e","flettefelt":"barnetsBostedsland"},{"_type":"span","marks":[],"text":". Du og den andre forelderen bur i Noreg. Det er ikkje rett til barnetrygd frå ","_key":"13c95ae2693f"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"6f282ba009f5"},{"_type":"span","marks":[],"text":" fordi vi har kome fram til at norsk lovgjevnad gjeld for barnet. Du får derfor heile barnetrygda frå Noreg.","_key":"c400c585d068"}],"_type":"block","style":"normal","_key":"d45e1f0403a1","markDefs":[]}],"_createdAt":"2022-11-30T13:54:11Z","triggereIBruk":["KOMPETANSE"],"_id":"begrunnelse-2b4326cf-47a8-4eea-a73d-b01bcc017f9a","navnISystem":"Primærland begge foreldre bosatt i Norge","valgbarhet":"STANDARD","barnetsBostedsland":["NORGE","IKKE_NORGE"],"apiNavn":"fortsattInnvilgetPrimaerlandBeggeForeldreBosattINorge","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_rev":"BtltdVb0HP4g4WJfnr62rJ","vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET","begrunnelsetype":"FORTSATT_INNVILGET","mappe":["EØS","FORTSATT_INNVILGET"]},{"triggereIBruk":["KOMPETANSE"],"navnISystem":"Flere barn er døde EØS","apiNavn":"opphorFlereBarnErDodeEos","behandlingstema":"EØS","begrunnelsetype":"OPPHØR","nynorsk":[{"style":"normal","_key":"ee1d7090d27d","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barna dine som er fødd ","_key":"dcd42ffc788c0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"76140f5fbcf0"},{"_type":"span","marks":[],"text":" fordi barna døydde. Barnetrygda opphøyrer frå månaden etter at barna døydde.","_key":"671eaa2c50ac"}],"_type":"block"}],"mappe":["EØS","OPPHØR"],"_updatedAt":"2023-09-25T10:42:07Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"_type":"begrunnelse","periodeType":"INGEN_UTBETALING","_createdAt":"2022-11-30T10:01:49Z","_id":"begrunnelse-2e55cba3-c0e1-46b0-a713-6ad607ff387b","barnetsBostedsland":["NORGE","IKKE_NORGE"],"vedtakResultat":"IKKE_INNVILGET","valgbarhet":"AUTOMATISK","bokmaal":[{"_type":"block","style":"normal","_key":"196fdd36da0b","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barna dine som er født ","_key":"a918ed020a430"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"84280c65fe45"},{"_type":"span","marks":[],"text":" fordi barna døde. Barnetrygden opphører fra måneden etter at barna døde.","_key":"776db4041c26"}]}],"hjemler":["2","11"],"visningsnavn":"15. Flere barn er døde EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND","TO_PRIMÆRLAND"],"_rev":"FuD004taptHFqBZyEy97lA","tema":"EØS"},{"apiNavn":"fortsattInnvilgetSekundaerlandUkAleneansvar","_type":"begrunnelse","_createdAt":"2022-12-01T07:54:37Z","triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["NORGE"],"_updatedAt":"2023-09-25T10:45:32Z","_rev":"BtltdVb0HP4g4WJfnr691o","tema":"EØS","mappe":["EØS","FORTSATT_INNVILGET"],"annenForeldersAktivitet":["IKKE_AKTUELT"],"navnISystem":"Sekundærland UK aleneansvar","hjemler":["2","4","11"],"visningsnavn":"20. Sekundærland UK aleneansvar","behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"5dec0789a382","_type":"span"},{"_key":"38c526540419","flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Du ","_key":"75cb2cce3811"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"895e44612b93","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bor i Norge. Du har ansvar for barnet alene. Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen har Storbritannia hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i Storbritannia og norsk barnetrygd.","_key":"a2f9fa10a3a9"}],"_type":"block","style":"normal","_key":"b6b39b493025"}],"vedtakResultat":"INGEN_ENDRING","hjemlerEOSForordningen883":["2","11-16","67","68"],"hjemlerSeperasjonsavtalenStorbritannina":["29"],"periodeType":"FORTSATT_INNVILGET","begrunnelsetype":"FORTSATT_INNVILGET","nynorsk":[{"markDefs":[],"children":[{"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"ffe02589ecd9","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"10e7fc4262bf"},{"marks":[],"text":". Du ","_key":"a1e4df8b1685","_type":"span"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"74adc2e68881","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bur i Noreg. Du har ansvar for barnet åleine. Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen har Storbritannia hovudansvaret for utbetaling av barnetrygd. Noreg utbetalar derfor forskjellen mellom barnetrygda i Storbritannia og norsk barnetrygd.","_key":"b5dbf2556a8f"}],"_type":"block","style":"normal","_key":"32f794105ad2"}],"_id":"begrunnelse-33ad23ee-b837-4ee0-9b66-110a41bfda02"},{"apiNavn":"reduksjonBarnDoedEos","_rev":"h26rAhFEYSUtDGXJE0sV8b","navnISystem":"Barn død EØS","visningsnavn":"1. Barn død EØS","vedtakResultat":"REDUKSJON","tema":"EØS","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"hjemler":["2","11"],"periodeType":"UTBETALING","_createdAt":"2022-11-30T08:27:19Z","valgbarhet":"STANDARD","barnetsBostedsland":["NORGE","IKKE_NORGE"],"bokmaal":[{"_key":"0cc631028675","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden er endret fra måneden etter at barn født ","_key":"11a736a956850"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"5077108fcb5c"},{"_key":"fc01a21b1e3f","_type":"span","marks":[],"text":" døde."}],"_type":"block","style":"normal"}],"_type":"begrunnelse","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND","TO_PRIMÆRLAND"],"begrunnelsetype":"REDUKSJON","nynorsk":[{"_type":"block","style":"normal","_key":"791c134643e5","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra frå månaden etter at barn fødd ","_key":"e095f03a756f0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"32f3b4f508cb"},{"marks":[],"text":" døydde.","_key":"0649f7a5a8ad","_type":"span"}]}],"triggereIBruk":["KOMPETANSE"],"_id":"begrunnelse-3a3d7d82-059b-4509-903d-ff150cbe6ea8","mappe":["EØS","REDUKSJON"],"_updatedAt":"2023-09-25T10:15:50Z","behandlingstema":"EØS"},{"navnISystem":"Selvstendig rett sekundærland standard","_rev":"h26rAhFEYSUtDGXJE0sV8b","_type":"begrunnelse","_createdAt":"2023-09-19T14:44:46Z","valgbarhet":"STANDARD","apiNavn":"fortsattInnvilgetSelvstendigRettSekundaerlandStandard","hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"FORTSATT_INNVILGET","triggereIBruk":["KOMPETANSE"],"_updatedAt":"2023-09-25T10:15:50Z","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"periodeType":"UTBETALING","_id":"begrunnelse-42665fdf-0699-410f-96d7-8c406c24ae7b","mappe":["EØS","FORTSATT_INNVILGET"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER"],"barnetsBostedsland":["IKKE_NORGE"],"bokmaal":[{"_type":"block","style":"normal","_key":"3d39ae6b3904","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"fabde130694d0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"25bdc893175b"},{"_type":"span","marks":[],"text":". Du ","_key":"3f7f74f28d3c"},{"_type":"valgfeltV2","_key":"2e00dd01053f","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"}},{"_type":"span","marks":[],"text":" i ","_key":"c6f19c03341c"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"cbf90ae693f3"},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"1a492871a7c2"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"4673669878a0"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"b9ca2f4729af"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"e3a751cca3f7"},{"_type":"span","marks":[],"text":" i ","_key":"95291e254e26"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"9f17f57dd73e"},{"_key":"37828a0c7ce1","_type":"span","marks":[],"text":" og tilhører derfor norsk lovgivning. "},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"28f2b2e8538c"},{"_key":"71bd7700ec28","_type":"span","marks":[],"text":" har hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i "},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"1a5056517d88"},{"_key":"ee6b8a7e90c8","_type":"span","marks":[],"text":" og norsk barnetrygd. "}]}],"hjemler":["2","4","11"],"visningsnavn":"31. Selvstendig rett sekundærland standard","vedtakResultat":"INGEN_ENDRING","tema":"EØS","nynorsk":[{"style":"normal","_key":"9da425d99019","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"32b60dd72bfb0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"892600ba8fc3"},{"text":". Du ","_key":"12ea9126cf85","_type":"span","marks":[]},{"_type":"valgfeltV2","_key":"b48a23aaec20","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"}},{"_type":"span","marks":[],"text":" i ","_key":"ef5c6f9d9e03"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"805dc489961d"},{"_key":"9375502c593b","_type":"span","marks":[],"text":". Barnet bur i "},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"316e33e39686"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"35674ea6d9cd"},{"_type":"valgfeltV2","_key":"45f65f41159c","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"}},{"_type":"span","marks":[],"text":" i ","_key":"2ee96fb9e2b7"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"45d38ee4d322"},{"text":"og tilhøyrer derfor norsk lovgjeving. ","_key":"73d5aafa0a3c","_type":"span","marks":[]},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"764beddabe71"},{"_type":"span","marks":[],"text":" har hovudansvaret for utbetaling av barnetrygd. Noreg utbetaler derfor skilnaden mellom barnetrygda i ","_key":"7be4f7dbefc2"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"7b82a79d6d5e"},{"text":" og norsk barnetrygd. ","_key":"eec205c3cabc","_type":"span","marks":[]}],"_type":"block"}]},{"_updatedAt":"2023-09-25T10:15:50Z","bokmaal":[{"style":"normal","_key":"f751cd7676ce","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn født ","_key":"679c4364b4b8"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"d33ce89696af"},{"_type":"span","marks":[],"text":". Du ","_key":"b908b786f92a"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"fd6f98d1ee70","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"e3b3f544f662"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"12bf19363815","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bor i ","_key":"657bb3df4b0e"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"26154ca8aa89"},{"marks":[],"text":". Den andre forelderen ","_key":"83e7253f0311","_type":"span"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"133e039da16f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"d1d0c911c43c"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"fe315c06beb6"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Du og den andre forelderen har rett til barnetrygd fra Norge og ","_key":"4e9c058b7ba9"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"a2c06f4baa92"},{"_key":"03040e8d09ac","_type":"span","marks":[],"text":" fordi dere jobber i andre land enn der "},{"_type":"valgfeltV2","_key":"e9c0e0f57636","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"marks":[],"text":" bor. Landet med den høyeste barnetrygden skal utbetale. Norsk barnetrygd er høyere enn barnetrygden i ","_key":"a35da73c3f5d","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"2e4d6dd806c1"},{"_key":"b883cfd6c0b7","_type":"span","marks":[],"text":". Du får derfor hele barnetrygden fra Norge."}],"_type":"block"}],"apiNavn":"fortsattInnvilgetPrimaerlandUkToArbeidslandNorgeUtbetaler","behandlingstema":"EØS","triggereIBruk":["KOMPETANSE"],"nynorsk":[{"_type":"block","style":"normal","_key":"1f52ce008d7d","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"6b9ea4bf6aba","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"4ea9df2d6578"},{"marks":[],"text":". Du ","_key":"10ba1d05b868","_type":"span"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"f9ccba5b6a51","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"02a433f790fe"},{"_key":"d317d06278da","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" bur i ","_key":"f59433ac6e80"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"b0f4d8560943"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"42c1e9b5b98a"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"8f609a57c5ae","skalHaStorForbokstav":false},{"marks":[],"text":" i ","_key":"ac4ebf199451","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"68009cb53b96"},{"_key":"463c2628f280","_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Du og den andre forelderen har rett til barnetrygd frå Noreg og "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"a62ea0d4cb66"},{"_type":"span","marks":[],"text":" fordi de jobbar i andre land enn der ","_key":"c6e955f79ed2"},{"_type":"valgfeltV2","_key":"ea2c783ad868","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":" bur. Landet med den høgaste barnetrygda skal utbetale. Norsk barnetrygd er høgare enn barnetrygda i ","_key":"ff5ada0cb88c"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"c54e38830c30"},{"_key":"60f4d97e4573","_type":"span","marks":[],"text":". Du får derfor heile barnetrygda frå Noreg."}]}],"_createdAt":"2022-11-30T14:46:49Z","hjemlerEOSForordningen883":["2","11-16","67","68"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_PENSJON"],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"begrunnelsetype":"FORTSATT_INNVILGET","tema":"EØS","valgbarhet":"STANDARD","navnISystem":"Primærland UK to arbeidsland Norge utbetaler","_rev":"h26rAhFEYSUtDGXJE0sV8b","visningsnavn":"12. Primærland UK to arbeidsland Norge utbetaler","vedtakResultat":"INGEN_ENDRING","hjemlerSeperasjonsavtalenStorbritannina":["29"],"hjemlerEOSForordningen987":["58"],"barnetsBostedsland":["IKKE_NORGE"],"_type":"begrunnelse","_id":"begrunnelse-4289e62d-7650-4dd4-a0a0-663951383d3a","mappe":["EØS","FORTSATT_INNVILGET"],"hjemler":["2","4","11"],"periodeType":"UTBETALING"},{"vedtakResultat":"INGEN_ENDRING","tema":"EØS","nynorsk":[{"_key":"49907ae9757b","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"1786cf7e32d0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"a5941ca9609f"},{"_type":"span","marks":[],"text":". Du ","_key":"6a1b18837ea8"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"ebebbc7b16d8","skalHaStorForbokstav":false},{"text":". Barnet bur i Noreg. Den andre forelderen jobbar i ","_key":"ad63b4ee2cb6","_type":"span","marks":[]},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"3fa3eb52041f"},{"_type":"span","marks":[],"text":". Landa de jobbar i har hovudansvar for utbetaling av barnetrygd. Norsk barnetrygd er høgare enn barnetrygda i (SØKERS AKTIVITETSLAND) og i ","_key":"c09abbbcc32a"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"5cdd919715c0"},{"_type":"span","marks":[],"text":". Noreg utbetalar derfor forskjellen mellom barnetrygda i desse landa og norsk barnetrygd.","_key":"db623209c82f"}],"_type":"block","style":"normal"}],"triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["NORGE"],"mappe":["EØS","FORTSATT_INNVILGET"],"kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_type":"begrunnelse","annenForeldersAktivitet":["I_ARBEID","MOTTAR_PENSJON"],"_createdAt":"2022-12-01T08:13:21Z","valgbarhet":"STANDARD","_rev":"BtltdVb0HP4g4WJfnr6BKJ","periodeType":"FORTSATT_INNVILGET","apiNavn":"fortsattInnvilgetSekundaerlandToArbeidslandNorgeUtbetaler","begrunnelsetype":"FORTSATT_INNVILGET","visningsnavn":"22. Sekundærland to arbeidsland Norge utbetaler","behandlingstema":"EØS","hjemlerEOSForordningen883":["2","11-16","67","68"],"_id":"begrunnelse-42beafab-e9d3-4465-91a2-aea3c49464ab","_updatedAt":"2023-09-25T10:46:12Z","bokmaal":[{"markDefs":[],"children":[{"_key":"022009e781df","_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"a0d3f6b09fdb"},{"_key":"7c2273079928","_type":"span","marks":[],"text":". Du "},{"_key":"073e3c06bd9f","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2"},{"marks":[],"text":". Barnet bor i Norge. Den andre forelderen jobber i ","_key":"c2b5ac050061","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"5a9eb4777056"},{"_type":"span","marks":[],"text":". Landene dere jobber i har hovedansvar for utbetaling av barnetrygd. Norsk barnetrygd er høyere enn barnetrygden i (SØKERS AKTIVITETSLAND) og i ","_key":"c169488ba50f"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"ddaf02e05647"},{"_type":"span","marks":[],"text":". Norge utbetaler derfor forskjellen mellom barnetrygden i disse landene og norsk barnetrygd.","_key":"0d034c00bc82"}],"_type":"block","style":"normal","_key":"dbfb6fdbcab7"}],"navnISystem":"Sekundærland to arbeidsland Norge utbetaler","hjemler":["2","4","11"],"hjemlerEOSForordningen987":["58"]},{"hjemler":["2","4","11"],"vedtakResultat":"IKKE_INNVILGET","_id":"begrunnelse-4459f552-225f-48bf-b6b7-cead98ccb2c2","_updatedAt":"2023-09-25T10:42:10Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER"],"begrunnelsetype":"OPPHØR","_createdAt":"2023-09-19T14:12:16Z","valgbarhet":"STANDARD","apiNavn":"opphorSelvstendigRettOpphoer","visningsnavn":"16. Selvstendig rett opphør ","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND","TO_PRIMÆRLAND"],"_rev":"FuD004taptHFqBZyEy97un","hjemlerEOSForordningen883":["2","11-16","67"],"triggereIBruk":["KOMPETANSE"],"mappe":["EØS","OPPHØR"],"barnetsBostedsland":["IKKE_NORGE"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"2ac8151278900"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"52ffe20a6da6"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikke ","_key":"6fb667e80558"},{"_key":"81fdb40824f3","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". Den andre forelderen tilhører ikke norsk lovgivning og du har ikke lenger rett til barnetrygd fra Norge. ","_key":"909c1b8ed303"}],"_type":"block","style":"normal","_key":"ef3a39259df7"}],"navnISystem":"Selvstendig rett opphør ","_type":"begrunnelse","periodeType":"INGEN_UTBETALING","tema":"EØS","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"a56bd237b7590"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"b6031542b991"},{"_key":"bce9625646ba","_type":"span","marks":[],"text":" fordi den andre forelderen ikkje "},{"_key":"1d6d9c494b60","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2"},{"_key":"bb8da259204f","_type":"span","marks":[],"text":". Den andre forelderen tilhøyrer ikkje norsk lovgjeving og du har ikkje lenger rett til barnetrygd frå Noreg. "}],"_type":"block","style":"normal","_key":"d83f0b3266c1","markDefs":[]}]},{"apiNavn":"fortsattInnvilgetSelvstendigRettPrimaerlandStandard","visningsnavn":"28. Selvstendig rett primærland standard","periodeType":"FORTSATT_INNVILGET","nynorsk":[{"style":"normal","_key":"c20bffe627be","markDefs":[],"children":[{"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"2de12466f7470","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"49f4fc04716c"},{"_type":"span","marks":[],"text":". Du ","_key":"2074524cb979"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"b466df1bec71","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"f1f23bb26ca1"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"96f69361c60a"},{"_type":"span","marks":[],"text":". Barnet bur i ","_key":"03963611763a"},{"_type":"eosFlettefelt","_key":"1d64950ea5e4","flettefelt":"barnetsBostedsland"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"1145390cbb59"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"4c82bd75e392","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"6d4ed829cb7d"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"817a712018ff"},{"_key":"242b565f497c","_type":"span","marks":[],"text":" og tilhøyrer derfor norsk lovgjeving. Du får derfor heile barnetrygda frå Noreg. "}],"_type":"block"}],"_id":"begrunnelse-4e0ef512-b76f-4b68-b5eb-bc9758a61cf4","navnISystem":"Selvstendig rett primærland standard","_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","hjemlerEOSForordningen883":["2","11-16","67","68"],"triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["IKKE_NORGE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER"],"hjemler":["2","4","11"],"_rev":"BtltdVb0HP4g4WJfnr6DGJ","begrunnelsetype":"FORTSATT_INNVILGET","valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:46:35Z","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_createdAt":"2023-09-19T14:30:16Z","mappe":["EØS","FORTSATT_INNVILGET"],"bokmaal":[{"children":[{"_key":"87733ffbae6e0","_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"a0f0190977a7"},{"_type":"span","marks":[],"text":". Du ","_key":"96063413771b"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"700e6d15edea","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"3dd4d4fde43c"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"1c1350109c76"},{"text":". Barnet bor i ","_key":"76e18a8e8c14","_type":"span","marks":[]},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"12bdd72e8747"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"0ec5dba8f38a"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"b87094a29aca","skalHaStorForbokstav":false},{"text":" i ","_key":"d41aa3567a23","_type":"span","marks":[]},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"6cacb4dfe41f"},{"_type":"span","marks":[],"text":" og tilhører derfor norsk lovgivning. Du får derfor hele barnetrygden fra Norge.","_key":"6046266991c7"}],"_type":"block","style":"normal","_key":"57860aedb416","markDefs":[]}],"tema":"EØS"},{"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"visningsnavn":"3.Barn bor ikke i EØS-land","behandlingstema":"EØS","_type":"begrunnelse","vedtakResultat":"REDUKSJON","_id":"begrunnelse-527150b9-20cd-46e7-ab62-70096cd743a3","mappe":["EØS","REDUKSJON"],"navnISystem":"Barn bor ikke i EØS-land","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"UTBETALING","tema":"EØS","barnetsBostedsland":["NORGE","IKKE_NORGE"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden er endret fordi barn født ","_key":"c317a4549bfe0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"ef3b2ff21c6d"},{"text":" ikke lenger bor i et EØS-land.","_key":"b1e05f2df2b1","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"c018c37b4229"}],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND"],"_rev":"h26rAhFEYSUtDGXJE0sV8b","begrunnelsetype":"REDUKSJON","_createdAt":"2022-11-30T08:46:43Z","valgbarhet":"STANDARD","apiNavn":"reduksjonBarnBorIkkeIEosLand","hjemler":["2","4","11"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygda er endra fordi barn fødd ","_key":"b190dd0109190"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"b577a1889848"},{"_type":"span","marks":[],"text":" ikkje lenger bur i eit EØS-land.","_key":"96da2368a4e2"}],"_type":"block","style":"normal","_key":"4cff15fb169f"}],"triggereIBruk":["KOMPETANSE"],"_updatedAt":"2023-09-25T10:15:50Z"},{"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET","valgbarhet":"STANDARD","barnetsBostedsland":["IKKE_NORGE"],"_rev":"BtltdVb0HP4g4WJfnr6DhJ","begrunnelsetype":"FORTSATT_INNVILGET","triggereIBruk":["KOMPETANSE"],"mappe":["EØS","FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:46:43Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER"],"bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn født ","_key":"8c7c2bf3029d0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"737514034e71"},{"_type":"span","marks":[],"text":". Du ","_key":"467a8162c11b"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"8f0b2ec583f0","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"0038f3ce808c"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"151c681292ad"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"2be70c385370"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"724048ee7b8c","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"b2054bbb2b25"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"ff2df15bbf97"},{"_type":"span","marks":[],"text":" og tilhører derfor norsk lovgivning. Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen får du hele barnetrygden fra Norge.","_key":"9c4b4c9d2ed0"}],"_type":"block","style":"normal","_key":"d2d5d7b7db3f","markDefs":[]}],"navnISystem":"Selvstendig rett primærland UK og utland standard","hjemlerSeperasjonsavtalenStorbritannina":["29"],"nynorsk":[{"_type":"block","style":"normal","_key":"41cfb2519469","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"7637cd7969f40"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"32075f436226"},{"_type":"span","marks":[],"text":". Du ","_key":"330bf5c8c31c"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"e2823f4213f0"},{"text":" i ","_key":"b3e14d549739","_type":"span","marks":[]},{"_key":"48c777055527","flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt"},{"_key":"cd8a0957bd9f","_type":"span","marks":[],"text":". Den andre forelderen "},{"_type":"valgfeltV2","_key":"43340ad701b6","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"}},{"_type":"span","marks":[],"text":" i ","_key":"295cc62a4300"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"0ea648fca88f"},{"_key":"f591c5e1d3b5","_type":"span","marks":[],"text":" og tilhøyrer derfor norsk lovgjeving. Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen får du heile barnetrygda frå Noreg."}]}],"_createdAt":"2023-09-19T14:39:23Z","_id":"begrunnelse-5d0edb5e-c686-4e02-9487-ce4801b3640b","tema":"EØS","apiNavn":"fortsattInnvilgetSelvstendigRettPrimaerlandUkOgUtlandStandard","hjemler":["2","4","11"],"visningsnavn":"30. Selvstendig rett primærland UK og utland standard","_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"]},{"bokmaal":[{"style":"normal","_key":"6e7b394d7087","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"b26509b3ce6b","_type":"span"},{"_key":"d65e83db1074","flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Du ","_key":"3bceb1e8ad71"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"c4c61dae77f4","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bor i Norge. Du har ansvar for barnet alene. ","_key":"b108012f560b"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"305adc913b21"},{"_type":"span","marks":[],"text":" har hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i ","_key":"76dca32567c3"},{"_type":"eosFlettefelt","_key":"1eb5a0fe0691","flettefelt":"sokersAktivitetsland"},{"_key":"a5508674eced","_type":"span","marks":[],"text":" og norsk barnetrygd. "}],"_type":"block"}],"apiNavn":"fortsattInnvilgetSekundaerlandAleneansvar","behandlingstema":"EØS","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"a4458e5618a1"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"b585ddac2416"},{"_type":"span","marks":[],"text":". Du ","_key":"f643dabcd0d1"},{"_type":"valgfeltV2","_key":"6e443f428a10","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"}},{"_key":"6fef2b65ee2a","_type":"span","marks":[],"text":". Barnet bur i Noreg. Du har ansvar for barnet åleine. "},{"_key":"24b997f54c96","flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt"},{"marks":[],"text":" har hovudansvaret for utbetaling av barnetrygd. Noreg utbetalar derfor forskjellen mellom barnetrygda i ","_key":"b0ac752c0582","_type":"span"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"54ccde85c3f9"},{"_type":"span","marks":[],"text":" og norsk barnetrygd. ","_key":"2172a38ef34b"}],"_type":"block","style":"normal","_key":"6da3994c6ae4"}],"_createdAt":"2022-12-01T07:48:55Z","valgbarhet":"STANDARD","barnetsBostedsland":["NORGE"],"mappe":["EØS","FORTSATT_INNVILGET"],"_type":"begrunnelse","periodeType":"FORTSATT_INNVILGET","_id":"begrunnelse-5d3e056e-897b-4e4a-a68d-65c4d0b9f5ab","annenForeldersAktivitet":["IKKE_AKTUELT"],"navnISystem":"Sekundærland aleneansvar","visningsnavn":"18. Sekundærland aleneansvar","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"vedtakResultat":"INGEN_ENDRING","begrunnelsetype":"FORTSATT_INNVILGET","tema":"EØS","hjemler":["2","4","11"],"_rev":"BtltdVb0HP4g4WJfnr68Ro","hjemlerEOSForordningen883":["2","11-16","67","68"],"triggereIBruk":["KOMPETANSE"],"_updatedAt":"2023-09-25T10:45:16Z"},{"apiNavn":"fortsattInnvilgetSelvstendigRettSekundaerlandUkStandard","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_createdAt":"2023-09-19T14:50:20Z","triggereIBruk":["KOMPETANSE"],"begrunnelsetype":"FORTSATT_INNVILGET","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"6b37ca3d10f00"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"471af96ea620"},{"_type":"span","marks":[],"text":". Du ","_key":"59ee15683751"},{"_key":"bb408c429d81","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" i ","_key":"ac815e7ba3bf"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"b09f534ea2a1"},{"marks":[],"text":". Barnet bur i ","_key":"176c69d56314","_type":"span"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"1a8191940be4"},{"_key":"9850070e3d1f","_type":"span","marks":[],"text":". Den andre forelderen "},{"_type":"valgfeltV2","_key":"840a0d29ff1f","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"}},{"_key":"55ab61981db4","_type":"span","marks":[],"text":" i "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"1a71b9ceec38"},{"_type":"span","marks":[],"text":" og tilhøyrer derfor norsk lovgivning. Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen har Storbritannia hovudansvaret for utbetaling av barnetrygd. Noreg utbetalar derfor forskjellen mellom barnetrygda i Storbritannia og norsk barnetrygd.","_key":"a4eb08e931fd"}],"_type":"block","style":"normal","_key":"09b4fe219b88","markDefs":[]}],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"33a4529e73b00"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"78cd4220aa35"},{"_type":"span","marks":[],"text":". Du ","_key":"ef1b030fd803"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"ab2c3b05ae6a","skalHaStorForbokstav":false},{"text":" i ","_key":"be71ed45fbf0","_type":"span","marks":[]},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"cd6ce36e64c4"},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"75ff34905030"},{"_type":"eosFlettefelt","_key":"a02a58a69842","flettefelt":"barnetsBostedsland"},{"_key":"8e768f4ebf2b","_type":"span","marks":[],"text":". Den andre forelderen "},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"13789c48b90e","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"02616d53e109"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"a0cc7b018258"},{"text":" og tilhører derfor norsk lovgivning. Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen har Storbritannia hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i Storbritannia og norsk barnetrygd.","_key":"014337cf3c82","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"729887966844"}],"hjemler":["2","4","11"],"vedtakResultat":"INGEN_ENDRING","hjemlerSeperasjonsavtalenStorbritannina":["29"],"periodeType":"FORTSATT_INNVILGET","barnetsBostedsland":["IKKE_NORGE"],"mappe":["EØS","FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:46:52Z","navnISystem":"Selvstendig rett sekundærland UK standard","_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"valgbarhet":"STANDARD","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER"],"visningsnavn":"32. Selvstendig rett sekundærland UK standard","_rev":"h26rAhFEYSUtDGXJE0vMAp","tema":"EØS","_id":"begrunnelse-614b3faf-a341-465a-9f6b-1f926b43b636"},{"hjemler":["2","4","5"],"behandlingstema":"EØS","_type":"begrunnelse","resultat":"IKKE_INNVILGET","vedtakResultat":"IKKE_INNVILGET","navnISystem":"Får dagpenger fra annet EØS land","nynorsk":[{"style":"normal","_key":"8a96c8809ed4","markDefs":[],"children":[{"_key":"57ea62e11b900","_type":"span","marks":[],"text":"Barnetrygd for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"02df97c721ef"},{"_type":"span","marks":[],"text":" fordi du får dagpengar frå eit anna EØS-land når du bur i Noreg. Vi har derfor kome fram til at lovgjevnaden i det andre EØS-landet gjeld for deg.","_key":"d203fa781557"}],"_type":"block"}],"_createdAt":"2023-05-04T06:18:17Z","triggereIBruk":["VILKÅRSVURDERING"],"periodeType":"INGEN_UTBETALING","visningsnavn":"16. Får dagpenger fra annet EØS land","hjemlerEOSForordningen883":["2","11-16","67"],"tema":"EØS","valgbarhet":"STANDARD","_id":"begrunnelse-616f9643-c7fd-45ee-9a88-6de404b53eed","mappe":["EØS","AVSLAG"],"_updatedAt":"2023-09-25T10:43:50Z","eosVilkaar":["BOSATT_I_RIKET"],"_rev":"BtltdVb0HP4g4WJfnr61uo","begrunnelsetype":"AVSLAG","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"4880c15076550"},{"_key":"fce39d3cb70a","flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":" fordi du får dagpenger fra et annet EØS-land når du bor i Norge. Vi har derfor kommet fram til at lovgivningen i det andre EØS-landet gjelder for deg.","_key":"eb2096f8513e"}],"_type":"block","style":"normal","_key":"c324b204219c","markDefs":[]}],"apiNavn":"avslagFaarDagpengerFraAnnetEosLand"},{"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_type":"begrunnelse","periodeType":"FORTSATT_INNVILGET","_id":"begrunnelse-677dde25-daa5-4bdd-a2e6-8f882ea19bb4","hjemlerSeperasjonsavtalenStorbritannina":["29"],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"54e973100ad3"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"bf8b79bfa4bd"},{"_type":"span","marks":[],"text":". Du ","_key":"7184ffd1dcdf"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"699d5aae44ff"},{"_type":"span","marks":[],"text":". Den andre forelderen jobbar ikkje i ","_key":"dc631218c793"},{"_type":"eosFlettefelt","_key":"fde892d6e8b2","flettefelt":"annenForeldersAktivitetsland"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen får du heile barnetrygda frå Noreg.","_key":"fe42303de18f"}],"_type":"block","style":"normal","_key":"7eeb9562e69e","markDefs":[]}],"_createdAt":"2022-11-30T14:15:57Z","barnetsBostedsland":["IKKE_NORGE"],"mappe":["EØS","FORTSATT_INNVILGET"],"annenForeldersAktivitet":["MOTTAR_PENSJON","INAKTIV"],"navnISystem":"Primærland UK og utland standard","behandlingstema":"EØS","_rev":"FuD004taptHFqBZyEy9Lwc","vedtakResultat":"INGEN_ENDRING","triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:44:34Z","bokmaal":[{"_key":"f2fc0cad6b10","markDefs":[],"children":[{"text":"Du får barnetrygd for barn født ","_key":"06cfada9d44d","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"6c7de338a9d5"},{"_type":"span","marks":[],"text":". Du ","_key":"ff86891183ae"},{"valgReferanse":{"_type":"reference","_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb"},"_type":"valgfeltV2","_key":"835eea0289a5","skalHaStorForbokstav":false},{"text":". Den andre forelderen jobber ikke i ","_key":"87b8f7872d57","_type":"span","marks":[]},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"616fa596b575"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen får du hele barnetrygden fra Norge.","_key":"1b18a37488be"}],"_type":"block","style":"normal"}],"apiNavn":"fortsattInnvilgetPrimaerlandUkOgUtlandStandard","hjemler":["2","4","11"],"visningsnavn":"7. Primærland UK og utland standard","hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"FORTSATT_INNVILGET","tema":"EØS"},{"hjemler":["2","4","11"],"_type":"begrunnelse","_createdAt":"2022-12-01T08:20:23Z","triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["IKKE_NORGE"],"annenForeldersAktivitet":["FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"visningsnavn":"24. Sekundærland begge foreldre bosatt i Norge","behandlingstema":"EØS","_rev":"BtltdVb0HP4g4WJfnr6BxJ","begrunnelsetype":"FORTSATT_INNVILGET","tema":"EØS","valgbarhet":"STANDARD","navnISystem":"Sekundærland begge foreldre bosatt i Norge","vedtakResultat":"INGEN_ENDRING","hjemlerEOSForordningen883":["2","11-16","67","68"],"_id":"begrunnelse-75b649cd-93a8-46e6-823b-df25e111eae7","mappe":["EØS","FORTSATT_INNVILGET"],"bokmaal":[{"_key":"175d303e5683","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"5ad1075642c3"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"cf5c03a152d6"},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"1c44c8f76e4f"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"a7e8afeda3cc"},{"_type":"span","marks":[],"text":". Du og den andre forelderen bor i Norge. Du og den andre forelderen er ikke i arbeidsaktivitet i Norge. ","_key":"cf483880a214"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"5c7a5e547457"},{"text":" har hovedansvaret for utbetaling av barnetrygd fordi vi har kommet fram til at lovgivningen i ","_key":"6bcb0d9dd46f","_type":"span","marks":[]},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"179a93ed9916"},{"_type":"span","marks":[],"text":" gjelder for barnet. Norge utbetaler derfor forskjellen mellom barnetrygden i ","_key":"cb523b811443"},{"_key":"87efa1314e33","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"text":" og norsk barnetrygd.","_key":"a7532229c7a4","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"apiNavn":"fortsattInnvilgetSekundaerlandBeggeForeldreBosattINorge","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"periodeType":"FORTSATT_INNVILGET","nynorsk":[{"style":"normal","_key":"7f48ed65bd7b","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"a18b672bcd08"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"d786ca15fccd"},{"text":". Barnet bur i ","_key":"224c4012d703","_type":"span","marks":[]},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"8fa6c5f73072"},{"_type":"span","marks":[],"text":". Du og den andre forelderen bur i Noreg. Du og den andre forelderen er ikkje i arbeidsaktivitet i Noreg. ","_key":"7790863dfbc8"},{"_type":"eosFlettefelt","_key":"85e72956047a","flettefelt":"barnetsBostedsland"},{"_key":"5100c316b5df","_type":"span","marks":[],"text":" har hovudansvaret for utbetaling av barnetrygd fordi vi har kome fram til at lovgjevnaden i "},{"_key":"0aac5bc3e8ff","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"text":" gjeld for barnet. Noreg utbetalar derfor forskjellen mellom barnetrygda i ","_key":"e4607c0d0d32","_type":"span","marks":[]},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"c183bb8a0d77"},{"text":" og norsk barnetrygd.","_key":"c5b7ff321762","_type":"span","marks":[]}],"_type":"block"}],"_updatedAt":"2023-09-25T10:46:20Z"},{"hjemler":["2","4","11"],"visningsnavn":"44. Selvstendig rett primærland UK standard","begrunnelsetype":"INNVILGET","nynorsk":[{"markDefs":[],"children":[{"_key":"7e5bbd1146b40","_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"ccbe1a017f8d"},{"_type":"span","marks":[],"text":". Du ","_key":"7ed18487c474"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"80c51ff921c1"},{"text":" i ","_key":"de6e31511727","_type":"span","marks":[]},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"ceb5bb435a7c"},{"_type":"span","marks":[],"text":". Barnet bur i ","_key":"5590f399dab7"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"c41782cbae41"},{"text":". Den andre forelderen ","_key":"6fddfc566f61","_type":"span","marks":[]},{"_key":"6838d7d89fe9","skalHaStorForbokstav":false,"valgReferanse":{"_type":"reference","_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" i ","_key":"9c4acd558362"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"6e567cdd2353"},{"_key":"fa01968d6f87","_type":"span","marks":[],"text":"og tilhøyrer derfor norsk lovgjeving. Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Du får heile barnetrygda frå Noreg."}],"_type":"block","style":"normal","_key":"6d3aff6dbb65"}],"triggereIBruk":["KOMPETANSE"],"_id":"begrunnelse-77b5f1ac-1611-4058-abf3-f1efaf82fafa","navnISystem":"Selvstendig rett primærland UK standard","hjemlerSeperasjonsavtalenStorbritannina":["29"],"periodeType":"UTBETALING","barnetsBostedsland":["IKKE_NORGE"],"bokmaal":[{"_type":"block","style":"normal","_key":"021160c45ecc","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn født ","_key":"f97df597039d0"},{"flettefelt":"barnasFodselsdatoer","_type":"flettefelt","_key":"ff377122037b"},{"_type":"span","marks":[],"text":". Du ","_key":"33a3ac4200ad"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"0638fdd96c0f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"bf4ac6fa0b06"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"572ec56779ee"},{"_key":"edfe16e9ca77","_type":"span","marks":[],"text":". Barnet bor i "},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"d9cfe69bf640"},{"_key":"14ec65a0635e","_type":"span","marks":[],"text":". Den andre forelderen "},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"c0c0c0207167","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"f96663118b56"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"9e13cae65bbc"},{"_type":"span","marks":[],"text":" og tilhører derfor norsk lovgivning. Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Du får hele barnetrygden fra Norge.","_key":"86fd284f5a88"}]}],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","tema":"EØS","mappe":["EØS","INNVILGET"],"_updatedAt":"2023-09-24T16:36:27Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","UTSENDT_ARBEIDSTAKER","ARBEIDER","SELVSTENDIG_NÆRINGSDRIVENDE","MOTTAR_UTBETALING_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET","MOTTAR_UFØRETRYGD_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET"],"apiNavn":"innvilgetSelvstendigRettPrimaerlandUkStandard","_rev":"FuD004taptHFqBZyEwvdtX","_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"_createdAt":"2023-09-19T12:41:14Z","valgbarhet":"STANDARD"},{"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER","ARBEIDER","SELVSTENDIG_NÆRINGSDRIVENDE"],"valgbarhet":"STANDARD","apiNavn":"innvilgetSelvstendigRettSekundaerlandStandard","_rev":"FuD004taptHFqBZyEwx0U9","_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"tema":"EØS","navnISystem":"Selvstendig rett sekundærland standard","periodeType":"UTBETALING","nynorsk":[{"style":"normal","_key":"befe3e559c66","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"52b69c6194640"},{"_type":"eosFlettefelt","_key":"d64da213c0aa","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":". Du ","_key":"b0da8081f978"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"0cf0944ef148","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"790a767176e8"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"d8467f030c9e"},{"_type":"span","marks":[],"text":". Barnet bur i ","_key":"56c007ed3e00"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"0cdc3c467fef"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"7e5184939e8a"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"1a8497e0b502","skalHaStorForbokstav":false},{"text":" i ","_key":"5e5f1b6d1ca5","_type":"span","marks":[]},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"4c9116df09cf"},{"_type":"span","marks":[],"text":"og tilhøyrer derfor norsk lovgjeving. ","_key":"808cf9e4afd9"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"cca3169bb44c"},{"_type":"span","marks":[],"text":" har hovudansvaret for utbetaling av barnetrygd. Noreg utbetaler skilnaden mellom barnetrygda i ","_key":"ae66db4fce4f"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"e8018e9def37"},{"text":" og norsk barnetrygd.","_key":"8819849f16e5","_type":"span","marks":[]}],"_type":"block"}],"_createdAt":"2023-09-19T13:55:35Z","triggereIBruk":["KOMPETANSE"],"_id":"begrunnelse-7a7cccd6-4c1c-4e0f-855d-899942129841","barnetsBostedsland":["IKKE_NORGE"],"mappe":["EØS","INNVILGET"],"kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"bokmaal":[{"_key":"3be8d0fead06","markDefs":[],"children":[{"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"9395e4d728e70","_type":"span","marks":[]},{"_type":"eosFlettefelt","_key":"758f1a49d4f9","flettefelt":"barnasFodselsdatoer"},{"_key":"bb66aeff224d","_type":"span","marks":[],"text":". Du "},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"a5587f50e068","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"4c20b3f9531d"},{"_type":"eosFlettefelt","_key":"e23b65754c0b","flettefelt":"sokersAktivitetsland"},{"text":". Barnet bor i ","_key":"5b8376d8d771","_type":"span","marks":[]},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"7f8720d484ff"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"d9d58487dc8c"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"33e8bba8ee74","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"8e2b0f62fdfc"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"bf9eafc5fb2d"},{"_type":"span","marks":[],"text":" og tilhører derfor norsk lovgnining. ","_key":"0ce0021f848f"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"9ff18fe0450b"},{"_type":"span","marks":[],"text":" har hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i ","_key":"314f686f00e6"},{"_key":"d257f50e986d","flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":" og norsk barnetrygd.","_key":"eefe41a8dc93"}],"_type":"block","style":"normal"}],"_updatedAt":"2023-09-24T16:58:59Z","visningsnavn":"47. Selvstendig rett sekundærland standard","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","hjemler":["2","4","11"]},{"_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"FORTSATT_INNVILGET","_createdAt":"2022-11-30T14:45:47Z","_rev":"BtltdVb0HP4g4WJfnr63no","mappe":["EØS","FORTSATT_INNVILGET"],"_id":"begrunnelse-7d93b817-edea-4fe6-a493-f1f5e7bcace4","hjemler":["2","4","11"],"visningsnavn":"11. Primærland to arbeidsland Annet land utbetaler","behandlingstema":"EØS","begrunnelsetype":"FORTSATT_INNVILGET","nynorsk":[{"markDefs":[],"children":[{"_key":"ea7122fe4f96","_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"a1efc5a6a818"},{"text":". Du ","_key":"ed20c8ac8b4a","_type":"span","marks":[]},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"0896ec154ce2","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"52ab128c8a29"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"bb36840c9fd9","skalHaStorForbokstav":true},{"marks":[],"text":" bur i ","_key":"7b87b68535cf","_type":"span"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"8aa3e9e6a10d"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"b4838cbbbff8"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"597b27729d65","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"16ddfb294d0f"},{"_key":"fd68fa485790","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"marks":[],"text":". Du og den andre forelderen har rett til barnetrygd frå Noreg og ","_key":"60a75f906175","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"1e0530a02354"},{"_type":"span","marks":[],"text":" fordi de jobbar i andre land enn der barnet bur. Landet med den høgaste barnetrygda skal utbetale. Barnetrygda i ","_key":"d36a5297d9c9"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"6207cf8a1b4a"},{"_type":"span","marks":[],"text":" er høgare enn barnetrygda i Noreg. Du får derfor heile barnetrygda frå ","_key":"f6735f075aec"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"fc7beb14ce80"},{"marks":[],"text":".","_key":"69f692bb5127","_type":"span"}],"_type":"block","style":"normal","_key":"b998d86cd6dc"}],"valgbarhet":"STANDARD","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON"],"navnISystem":"Primærland to arbeidsland Annet land utbetaler","hjemlerEOSForordningen987":["58"],"kompetanseResultat":["TO_PRIMÆRLAND"],"vedtakResultat":"INGEN_ENDRING","tema":"EØS","triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["IKKE_NORGE"],"_updatedAt":"2023-09-25T10:44:51Z","bokmaal":[{"markDefs":[],"children":[{"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"5e3166e2d18a","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"ee1b232a0c94"},{"_type":"span","marks":[],"text":". Du ","_key":"acb5c0674c25"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"c294a098024a","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"32ad6f5e33a5"},{"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"},"_type":"valgfeltV2","_key":"ad10661614c0","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bor i ","_key":"9e82dfb39dd7"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"aea63d09ce3a"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"10efab64b126"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"3273889c83c0","skalHaStorForbokstav":false},{"marks":[],"text":" i ","_key":"5f13d29e02d6","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"c38f2eb49019"},{"_type":"span","marks":[],"text":". Du og den andre forelderen har rett til barnetrygd fra Norge og ","_key":"557ba1e276e1"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"e62f3fc6688a"},{"_type":"span","marks":[],"text":" fordi dere jobber i andre land enn der ","_key":"ead739c67896"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"85371a04e9de","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" bor. Landet med den høyeste barnetrygden skal utbetale. Barnetrygden i ","_key":"95167a01c115"},{"_type":"eosFlettefelt","_key":"7ed59470f531","flettefelt":"annenForeldersAktivitetsland"},{"marks":[],"text":" er høyere enn barnetrygden i Norge. Du får derfor hele barnetrygden fra ","_key":"bd7fec797b54","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"e2ce884398c7"},{"_type":"span","marks":[],"text":".","_key":"fc702d1d3070"}],"_type":"block","style":"normal","_key":"77a0fa7f5d42"}],"apiNavn":"fortsattInnvilgetPrimaerlandToArbeidslandAnnetLandUtbetaler"},{"_type":"begrunnelse","begrunnelsetype":"FORTSATT_INNVILGET","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER"],"visningsnavn":"33. Selvstendig rett sekundærland UK og utland standard\t","_rev":"h26rAhFEYSUtDGXJE0sV8b","_createdAt":"2023-09-19T14:54:40Z","valgbarhet":"STANDARD","mappe":["EØS","FORTSATT_INNVILGET"],"hjemler":["2","4","11"],"hjemlerEOSForordningen883":["2","11-16","67","68"],"hjemlerSeperasjonsavtalenStorbritannina":["29"],"periodeType":"UTBETALING","tema":"EØS","triggereIBruk":["KOMPETANSE"],"kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"vedtakResultat":"INGEN_ENDRING","nynorsk":[{"_type":"block","style":"normal","_key":"071d7ec18b57","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"7c726d445ecc0"},{"_key":"f018f1a51c7f","flettefelt":"barnasFodselsdatoer","_type":"flettefelt"},{"_type":"span","marks":[],"text":". Du ","_key":"548eb7eb1ef8"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"246df486720e","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bur i ","_key":"eacac7dbd5dc"},{"_key":"556ba7ccd642","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"text":". Den andre forelderen ","_key":"4719aff38b59","_type":"span","marks":[]},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"500a133e26c1","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"3e2aee4b09f3"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"5a6719da9746"},{"_type":"span","marks":[],"text":" og tilhøyrer derfor norsk lovgjeving. Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen har Storbritannia hovudansvaret for utbetaling av barnetrygd. Noreg utbetalar derfor forskjellen mellom barnetrygda i Storbritannia og norsk barnetrygd.","_key":"7df33f714b3d"}]}],"_id":"begrunnelse-95d5d187-0f01-4d57-b477-19204c80ac43","barnetsBostedsland":["IKKE_NORGE"],"_updatedAt":"2023-09-25T10:15:50Z","bokmaal":[{"_type":"block","style":"normal","_key":"6b3bce9db1aa","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"97610f46bfdc0"},{"_type":"eosFlettefelt","_key":"7bf632700898","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":". Du ","_key":"162c4ccd83eb"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"d3434f82ac7f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"67c24b0cc6dd"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"ae0fb5c0d5b7"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"16df397d0807"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"ad5ce7d28fe5","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"885579668153"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"0bacab70f60b"},{"_type":"span","marks":[],"text":"og tilhører derfor norsk lovgivning. Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen har Storbritannia hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i Storbritannia og norsk barnetrygd. ","_key":"7846422b9423"}]}],"navnISystem":"Selvstendig rett sekundærland UK og utland standard\t","apiNavn":"fortsattInnvilgetSelvstendigRettSekundaerlandUkOgUtlandStandard"},{"navnISystem":"Primærland den andre forelderen utsendt arbeidstaker","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"tema":"EØS","triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:50Z","annenForeldersAktivitet":["UTSENDT_ARBEIDSTAKER"],"_rev":"h26rAhFEYSUtDGXJE0sV8b","hjemlerFolketrygdloven":["2-5"],"_createdAt":"2023-03-24T14:21:04Z","mappe":["EØS","INNVILGET"],"apiNavn":"innvilgetPrimaerlandDenAndreForelderenUtsendtArbeidstaker","_type":"begrunnelse","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","nynorsk":[{"_key":"5a6c478022d0","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"7f2ec559bcb60"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"23669c2faf9a"},{"_type":"span","marks":[],"text":". Du jobbar ikkje i ","_key":"46a55b16c0ad"},{"_type":"eosFlettefelt","_key":"79af949a02cd","flettefelt":"sokersAktivitetsland"},{"_key":"bf5a0943d936","_type":"span","marks":[],"text":". Barnet bur i "},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"cea87ffbe8e8"},{"text":". Den andre forelderen ","_key":"8e18b158036b","_type":"span","marks":[]},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"a0a24de85ae5","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Heile familien er pliktig medlem i folketrygda. Du får derfor heile barnetrygda frå Noreg. ","_key":"f7e4d42f9905"}],"_type":"block","style":"normal"}],"barnetsBostedsland":["IKKE_NORGE"],"bokmaal":[{"_key":"1751f9c6de7c","markDefs":[],"children":[{"_key":"4a098bfb01be0","_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"8d78a9bcbbba"},{"_type":"span","marks":[],"text":". Du jobber ikke i ","_key":"c77151556873"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"763413c69c59"},{"_type":"span","marks":[],"text":" . Barnet bor i ","_key":"0e5a384b3800"},{"_type":"eosFlettefelt","_key":"69dffc6528bd","flettefelt":"barnetsBostedsland"},{"marks":[],"text":". Den andre forelderen ","_key":"74b40a08f2c1","_type":"span"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"f35000d1397d","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Hele familien er pliktig medlem i folketrygden. Du får derfor hele barnetrygden fra Norge. ","_key":"e0d05195387a"}],"_type":"block","style":"normal"}],"hjemler":["2","4","11"],"visningsnavn":"42. Primærland den andre forelderen utsendt arbeidstaker","behandlingstema":"EØS","vedtakResultat":"INNVILGET_ELLER_ØKNING","hjemlerEOSForordningen883":["2","11-16","67","68"],"_id":"begrunnelse-968b9427-945f-407e-92f8-3e5b84af13e5"},{"hjemler":["2","4","11"],"_type":"begrunnelse","periodeType":"INGEN_UTBETALING","begrunnelsetype":"OPPHØR","_createdAt":"2023-09-19T14:14:38Z","_id":"begrunnelse-96a4e346-f496-4ce1-99e9-2eb13b830448","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER"],"navnISystem":"Selvstendig rett utsendt arbeidstaker fra annet EØS-land","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND","TO_PRIMÆRLAND"],"triggereIBruk":["KOMPETANSE"],"apiNavn":"opphorSelvstendigRettUtsendtArbedstakerFraAnnetEosLand","_rev":"h26rAhFEYSUtDGXJE0vDfS","vedtakResultat":"IKKE_INNVILGET","hjemlerEOSForordningen883":["2","11-16","67"],"tema":"EØS","barnetsBostedsland":["IKKE_NORGE"],"mappe":["EØS","OPPHØR"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"a97536e2fc1e0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"39ff1401ea26"},{"marks":[],"text":" fordi den andre forelderen jobber i Norge som utsendt fra et annet EØS-land. Vi har derfor kommet fram til at den andre forelderen ikke tilhører norsk lovgivning.","_key":"ec1c73344b91","_type":"span"}],"_type":"block","style":"normal","_key":"6bcf2bb5d761"}],"visningsnavn":"17. Selvstendig rett utsendt arbeidstaker fra annet EØS-land","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"4d6289a57ec00"},{"_key":"ebe9ce158b4f","flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":" fordi den andre forelderen jobbar i Noreg som utsendt frå eit anna EØS-land. Vi har derfor kome fram til at den andre forelderen ikkje tilhøyrer norsk lovgjeving.","_key":"b4302c94a3a1"}],"_type":"block","style":"normal","_key":"cf84706db7b1","markDefs":[]}],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:42:14Z"},{"hjemler":["2","4","11"],"kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_createdAt":"2022-12-01T07:36:44Z","valgbarhet":"STANDARD","begrunnelsetype":"FORTSATT_INNVILGET","tema":"EØS","_id":"begrunnelse-975103e7-664d-4a39-aa8a-1c74254cda16","behandlingstema":"EØS","_rev":"h26rAhFEYSUtDGXJE0sV8b","vedtakResultat":"INGEN_ENDRING","hjemlerEOSForordningen883":["2","11-16","67","68"],"navnISystem":"Sekundærland standard","visningsnavn":"16. Sekundærland standard","mappe":["EØS","FORTSATT_INNVILGET"],"triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"_updatedAt":"2023-09-25T10:15:50Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"apiNavn":"fortsattInnvilgetSekundaerlandStandard","_type":"begrunnelse","periodeType":"UTBETALING","nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"4ed76e8a736b","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"35d2eedf597e"},{"_key":"12ddc90a3159","_type":"span","marks":[],"text":". Du "},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"90d80f1c483e","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"568ba4ce627b"},{"_type":"valgfeltV2","_key":"ed7463a24e3f","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":" bur i ","_key":"0812094e20c0"},{"_type":"eosFlettefelt","_key":"24eb1970d3e8","flettefelt":"barnetsBostedsland"},{"text":". Den andre forelderen ","_key":"23bc43bd4a78","_type":"span","marks":[]},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"03ba8b6a58eb","skalHaStorForbokstav":false},{"marks":[],"text":" i ","_key":"1588acfc5e74","_type":"span"},{"_type":"eosFlettefelt","_key":"d8430fc6f4b0","flettefelt":"annenForeldersAktivitetsland"},{"_type":"span","marks":[],"text":". ","_key":"6fcfa0ef9e9b"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"487eb83a82c3"},{"_type":"span","marks":[],"text":" har hovudansvaret for utbetaling av barnetrygd. Noreg utbetaler derfor skilnaden mellom barnetrygda i ","_key":"7c73019996bc"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"6643a6e3bb61"},{"text":" og norsk barnetrygd.","_key":"82a84851299a","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"88398c1230e4"}],"bokmaal":[{"children":[{"_key":"63cd17744689","_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"08fd6505a04c"},{"_type":"span","marks":[],"text":". Du ","_key":"0262f572f34d"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"c80fb62a7219"},{"_type":"span","marks":[],"text":". ","_key":"be611075dbc1"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"4a5a024e0656","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bor i ","_key":"db7d73744ff2"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"31017c5752a8"},{"text":". Den andre forelderen ","_key":"a20312f1270b","_type":"span","marks":[]},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"13f966abaf58","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"9280188cfb00"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"b16a15f9c250"},{"_type":"span","marks":[],"text":". ","_key":"9dd5853e0fc8"},{"_key":"02b99d693af9","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"_key":"293642049a03","_type":"span","marks":[],"text":" har hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"57757ced77d9"},{"_type":"span","marks":[],"text":" og norsk barnetrygd.","_key":"bcb500bb7876"}],"_type":"block","style":"normal","_key":"1e5e23a2a89e","markDefs":[]}]},{"_createdAt":"2022-11-30T11:54:01Z","barnetsBostedsland":["NORGE","IKKE_NORGE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"navnISystem":"Primærland standard","hjemler":["2","4","11"],"visningsnavn":"1. Primærland standard","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"begrunnelsetype":"FORTSATT_INNVILGET","valgbarhet":"STANDARD","_id":"begrunnelse-9bcaf6a3-77d3-487b-a287-e4903ac242c5","bokmaal":[{"children":[{"_key":"578505f7e2140","_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"cffe46118192"},{"_key":"fc014a8f919c","_type":"span","marks":[],"text":". Du "},{"valgReferanse":{"_type":"reference","_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb"},"_type":"valgfeltV2","_key":"34c26266bcaf","skalHaStorForbokstav":false},{"marks":[],"text":". ","_key":"c213d7103580","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"dbf2db7f1485","skalHaStorForbokstav":true},{"marks":[],"text":" bor i ","_key":"ebca0ee7d980","_type":"span"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"920b06ac42b1"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"165aecb05b9a"},{"_type":"valgfeltV2","_key":"43d9903b7b7f","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"}},{"_type":"span","marks":[],"text":" i ","_key":"bd86aaa45273"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"d6f2ea604710"},{"_type":"span","marks":[],"text":". Du får derfor hele barnetrygden fra Norge.","_key":"423b6c9c779f"}],"_type":"block","style":"normal","_key":"570fc7317cc4","markDefs":[]}],"apiNavn":"fortsattInnvilgetPrimaerlandStandard","behandlingstema":"EØS","periodeType":"FORTSATT_INNVILGET","tema":"EØS","triggereIBruk":["KOMPETANSE"],"_type":"begrunnelse","mappe":["EØS","FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:44:10Z","_rev":"BtltdVb0HP4g4WJfnr62cJ","vedtakResultat":"INGEN_ENDRING","hjemlerEOSForordningen883":["2","11-16","67"],"nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"32a2e2033ea10","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"7b753ac3cd33"},{"_type":"span","marks":[],"text":". Du ","_key":"092e12ee2408"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"7a82029e484c","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"e38d476bffd2"},{"skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"3122c9f97647"},{"_key":"73be527c4c70","_type":"span","marks":[],"text":" bur i "},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"71aa155f1343"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"57aebfe81123"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"f2ac15ee8cf4","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"3014317d3cf0"},{"_type":"eosFlettefelt","_key":"3b7bf0ed8b8b","flettefelt":"annenForeldersAktivitetsland"},{"text":". Du får derfor heile barnetrygda frå Noreg.","_key":"231619b80410","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"0678a0458778"}]},{"navnISystem":"Barn uten d-nummer","visningsnavn":"14. Barn uten d-nummer","behandlingstema":"EØS","_rev":"h26rAhFEYSUtDGXJE0vGVF","barnetsBostedsland":["NORGE","IKKE_NORGE"],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND"],"vedtakResultat":"IKKE_INNVILGET","nynorsk":[{"style":"normal","_key":"a850a8346399","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"e9325aa0730e0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"869147417e30"},{"_key":"56df654d6964","_type":"span","marks":[],"text":" fordi vi ikkje kan sjå å ha fått opplysningar som viser at du har ansvar for "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"6df13e138368","skalHaStorForbokstav":false},{"marks":[],"text":".","_key":"08dfd8793192","_type":"span"}],"_type":"block"}],"_id":"begrunnelse-9bf85392-bdc1-424c-aefc-1b009a41ee62","_updatedAt":"2023-09-25T10:43:46Z","apiNavn":"avslagEosUregistrertBarn","hjemler":["2","4","5"],"periodeType":"INGEN_UTBETALING","_createdAt":"2023-02-23T14:05:15Z","triggereIBruk":["KOMPETANSE"],"_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67"],"begrunnelsetype":"AVSLAG","tema":"EØS","valgbarhet":"STANDARD","mappe":["EØS","AVSLAG"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT","UTSENDT_ARBEIDSTAKER"],"bokmaal":[{"_type":"block","style":"normal","_key":"f93ec95272d4","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"7a3be072a21a0"},{"_key":"c046f73f77a6","flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt"},{"marks":[],"text":" fordi vi ikke kan se å ha fått opplysninger som viser at du har ansvar for ","_key":"ee18f1e29477","_type":"span"},{"_key":"76a82ad6cf1b","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":".","_key":"206ef0017f24"}]}]},{"_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","begrunnelsetype":"FORTSATT_INNVILGET","navnISystem":"Tilleggsbegrunnelse utbetaling til annen forelder","apiNavn":"fortsattInnvilgetTilleggsbegrunnelseUtbetalingTilAnnenForelder","_rev":"FuD004taptHFqBZyEy9O7Q","mappe":["EØS","FORTSATT_INNVILGET"],"hjemler":["2","4","11"],"periodeType":"FORTSATT_INNVILGET","valgbarhet":"TILLEGGSTEKST","tema":"EØS","nynorsk":[{"markDefs":[],"children":[{"_key":"3a83602e1d9d","_type":"span","marks":[],"text":"Den andre forelderen har søkt om å få barnetrygda utbetalt til seg. Vi har kome fram til at barnetrygda ikke kjem barnet til gode. Vi utbetalar derfor barnetrygda til den andre forelderen."}],"_type":"block","style":"normal","_key":"7b0d2010c3f3"}],"visningsnavn":"14. Tilleggsbegrunnelse utbetaling til annen forelder","behandlingstema":"EØS","hjemlerEOSForordningen883":["68"],"_id":"begrunnelse-9ca803ef-3762-4cef-92e8-1efcb6d66029","barnetsBostedsland":["NORGE","IKKE_NORGE"],"_updatedAt":"2023-09-25T10:45:03Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"bokmaal":[{"children":[{"marks":[],"text":"Den andre forelderen har søkt om å få barnetrygden utbetalt til seg. Vi har kommet fram til at barnetrygden ikke kommer barnet til gode. Vi utbetaler derfor barnetrygden til den andre forelderen.","_key":"26b9fa2c40f1","_type":"span"}],"_type":"block","style":"normal","_key":"71b6eb8215d6","markDefs":[]}],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND"],"_createdAt":"2022-11-30T14:57:35Z","triggereIBruk":["KOMPETANSE"]},{"mappe":["EØS","FORTSATT_INNVILGET"],"hjemler":["2","4","11"],"visningsnavn":"6. Primærland UK aleneansvar","behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"vedtakResultat":"INGEN_ENDRING","triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["IKKE_NORGE"],"annenForeldersAktivitet":["MOTTAR_PENSJON","IKKE_AKTUELT"],"navnISystem":"Primærland UK aleneansvar","apiNavn":"fortsattInnvilgetPrimaerlandUkAleneansvar","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"FORTSATT_INNVILGET","valgbarhet":"STANDARD","_id":"begrunnelse-a8f16f17-9232-4ad8-9261-b7a2703a4ee3","bokmaal":[{"_type":"block","style":"normal","_key":"339d09e4f350","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn født ","_key":"b7b26b6f9758"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"2166da1f83b2"},{"marks":[],"text":". Du ","_key":"1c6861b1c13f","_type":"span"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"9c437cef206b","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"6b4475359aed"},{"skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"9ac34ec6763b"},{"marks":[],"text":" bor i Storbritannia. Du har ansvar for ","_key":"2f9b9faf029f","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"cb93f1c2654d","skalHaStorForbokstav":false},{"text":" alene. Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen får du hele barnetrygden fra Norge.","_key":"6d36e16c875f","_type":"span","marks":[]}]}],"_rev":"FuD004taptHFqBZyEy9LTj","hjemlerSeperasjonsavtalenStorbritannina":["29"],"begrunnelsetype":"FORTSATT_INNVILGET","_type":"begrunnelse","tema":"EØS","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"cf095ce4418c"},{"_type":"eosFlettefelt","_key":"416fc538ae08","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":". Du ","_key":"22dce977276a"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"06a43e7764a7","skalHaStorForbokstav":false},{"_key":"57345d402338","_type":"span","marks":[],"text":". "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"8a9ceb050a82","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bur i Storbritannia. Du har ansvar for ","_key":"1bb9dd32081d"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"618e46d03ad9","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" åleine. Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen får du heile barnetrygda frå Noreg.","_key":"bb042c5b1528"}],"_type":"block","style":"normal","_key":"083e20a4ffd5"}],"_createdAt":"2022-11-30T14:12:01Z","_updatedAt":"2023-09-25T10:44:31Z"},{"_rev":"h26rAhFEYSUtDGXJE0sV8b","vedtakResultat":"IKKE_INNVILGET","periodeType":"UTBETALING","triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","barnetsBostedsland":["NORGE","IKKE_NORGE"],"navnISystem":"Søker ber om opphør EØS","tema":"EØS","visningsnavn":"2. Søker ber om opphør EØS","behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","TO_PRIMÆRLAND"],"_type":"begrunnelse","_id":"begrunnelse-b0ee7482-4f46-4f78-a548-cd3c018453a1","_updatedAt":"2023-09-25T10:15:50Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden er endret fordi du har bedt om at barnetrygden for barn født ","_key":"6b07c9de63c60"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"296af4045bda"},{"text":" blir stanset.","_key":"c4fcb28e0195","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"06272c67c19e"}],"apiNavn":"reduksjonSokerBerOmOpphoer","hjemler":["2"],"begrunnelsetype":"REDUKSJON","nynorsk":[{"markDefs":[],"children":[{"text":"Barnetrygda er endra fordi du har bedt om at barnetrygda for barn fødd ","_key":"e8c79bc26b1d0","_type":"span","marks":[]},{"_key":"f86db0330806","flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt"},{"_key":"f3caa23d96d3","_type":"span","marks":[],"text":" blir stansa."}],"_type":"block","style":"normal","_key":"3a4ae922a2e6"}],"_createdAt":"2022-11-30T08:43:21Z","mappe":["EØS","REDUKSJON"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"]},{"_type":"begrunnelse","vedtakResultat":"INGEN_ENDRING","_id":"begrunnelse-b4af1bb3-a999-46b9-80a9-9b2d616eb9c1","annenForeldersAktivitet":["I_ARBEID","MOTTAR_PENSJON"],"bokmaal":[{"markDefs":[],"children":[{"text":"Du får barnetrygd for barn født ","_key":"6717ee24a720","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"81b064000d21"},{"_type":"span","marks":[],"text":". Du ","_key":"f9a3c7e2500e"},{"_key":"a00e8a22f401","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":". ","_key":"c947d2de3336"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"3491ed2b4b01","skalHaStorForbokstav":true},{"_key":"098eb828d004","_type":"span","marks":[],"text":" bor i "},{"_type":"eosFlettefelt","_key":"1ee39734e65b","flettefelt":"barnetsBostedsland"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"272cce542fc5"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"20605226e333"},{"_type":"span","marks":[],"text":" i ","_key":"1f088ae61be0"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"2a1d08ad4848"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Du og den andre forelderen har rett til barnetrygd fra Norge og ","_key":"af8f975b1be0"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"4d391f19a9d7"},{"_type":"span","marks":[],"text":" fordi dere jobber i andre land enn der ","_key":"6fb91ed88422"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"054b3c74f0d8","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" bor. Landet med den høyeste barnetrygden skal utbetale. ","_key":"f930bc5b4283"},{"_type":"eosFlettefelt","_key":"98ae08c09ee1","flettefelt":"annenForeldersAktivitetsland"},{"_type":"span","marks":[],"text":" er høyere enn barnetrygden i Norge. Du får derfor hele barnetrygden fra ","_key":"9141b68d6097"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"740ab84398d5"},{"marks":[],"text":".","_key":"4e8bec6a1775","_type":"span"}],"_type":"block","style":"normal","_key":"626e8cab3a88"}],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"hjemlerSeperasjonsavtalenStorbritannina":["29"],"periodeType":"UTBETALING","hjemlerEOSForordningen987":["58"],"behandlingstema":"EØS","valgbarhet":"STANDARD","navnISystem":"Primærland UK to arbeidsland. Annet land utbetaler","_rev":"h26rAhFEYSUtDGXJE0sV8b","hjemlerEOSForordningen883":["2","11-16","67","68"],"apiNavn":"fortsattInnvilgetPrimaerlandUkToArbeidslandAnnetLandUtbetaler","begrunnelsetype":"FORTSATT_INNVILGET","triggereIBruk":["KOMPETANSE"],"mappe":["EØS","FORTSATT_INNVILGET"],"hjemler":["2","4","11"],"_createdAt":"2022-11-30T14:52:50Z","_updatedAt":"2023-09-25T10:15:50Z","tema":"EØS","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"0c5901bb713e"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"7bb2eab6e057"},{"_type":"span","marks":[],"text":". Du ","_key":"79eb8cf26f26"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"7c2581fd6e2f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"ad551fcf32bd"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"746a62c3001f","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bur i ","_key":"14d6c600f995"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"4339d10f849d"},{"marks":[],"text":". Den andre forelderen ","_key":"f3f4c94f595c","_type":"span"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"e04c52452068","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"8b1aad3805b6"},{"_key":"2b1f41c41b2a","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"_key":"4df98be2b718","_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Du og den andre forelderen har rett til barnetrygd frå Noreg og "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"be23562d8dd1"},{"_type":"span","marks":[],"text":" fordi de jobbar i andre land enn der ","_key":"c7de85ee9831"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"0df3a02c6f97","skalHaStorForbokstav":false},{"_key":"b91a21498f2c","_type":"span","marks":[],"text":" bur. Landet med den høgaste barnetrygda skal utbetale. "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"13f7ef90b6ef"},{"text":" er høgare enn barnetrygda i Noreg. Du får derfor heile barnetrygda frå ","_key":"718deb7d45e8","_type":"span","marks":[]},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"90c546aa9b7a"},{"_type":"span","marks":[],"text":".","_key":"7b9e7cdc0d7d"}],"_type":"block","style":"normal","_key":"262770dac5e1"}],"barnetsBostedsland":["IKKE_NORGE"],"visningsnavn":"13. Primærland UK to arbeidsland. Annet land utbetaler"},{"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barnet ditt som er fødd ","_key":"f13469a05a9a0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"a688a8231aa0"},{"marks":[],"text":" fordi barnet døydde. Barnetrygda opphøyrer frå månaden etter at barnet døydde.","_key":"25cdb9821c3e","_type":"span"}],"_type":"block","style":"normal","_key":"792d78060e54"}],"_id":"begrunnelse-b5a331d7-1a08-4ab0-8f8f-c3403de34c0b","barnetsBostedsland":["NORGE","IKKE_NORGE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"_type":"begrunnelse","periodeType":"INGEN_UTBETALING","tema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND","TO_PRIMÆRLAND"],"vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"OPPHØR","navnISystem":"Ett barn død EØS","apiNavn":"opphorEttBarnDodEos","hjemler":["2","11"],"_updatedAt":"2023-09-25T10:42:03Z","behandlingstema":"EØS","resultat":"IKKE_INNVILGET","valgbarhet":"AUTOMATISK","triggereIBruk":["KOMPETANSE"],"mappe":["EØS","OPPHØR"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barnet ditt som er født ","_key":"988bbff5b94e0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"8ff27bb96516"},{"_type":"span","marks":[],"text":" fordi barnet døde. Barnetrygden opphører fra måneden etter at barnet døde.","_key":"af1d3e4af947"}],"_type":"block","style":"normal","_key":"6922857acec0"}],"visningsnavn":"14. Ett barn død EØS","_rev":"BtltdVb0HP4g4WJfnr5w2J","_createdAt":"2022-11-30T09:57:45Z"},{"triggereIBruk":["KOMPETANSE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER","ARBEIDER","SELVSTENDIG_NÆRINGSDRIVENDE"],"nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"3b744b659f060"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"a83ab3d8d9e8"},{"_type":"span","marks":[],"text":". Du ","_key":"659938ada2d2"},{"_type":"valgfeltV2","_key":"71b3aee38ac4","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"}},{"text":" i ","_key":"9a6eac91181e","_type":"span","marks":[]},{"_key":"0376d4ea7bf3","flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Barnet bur i ","_key":"93cf628cafce"},{"_type":"eosFlettefelt","_key":"ba958956be6b","flettefelt":"barnetsBostedsland"},{"_key":"135906047d14","_type":"span","marks":[],"text":". Den andre forelderen "},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"7880dbaf2f51","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"4d695eabccc3"},{"_type":"eosFlettefelt","_key":"a2a153885b69","flettefelt":"annenForeldersAktivitetsland"},{"text":" og tilhøyrer derfor norsk lovgjeving. Du får heile barnetrygda frå Noreg.","_key":"c2bd0538bee0","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"4da5884fd4ef"}],"_id":"begrunnelse-b5f9280b-da2e-4d58-a58d-7d1e6bbd66e2","navnISystem":"Selvstendig rett primærland standard","apiNavn":"innvilgetSelvstendigRettPrimaerlandStandard","_rev":"h26rAhFEYSUtDGXJE0WOdr","periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","bokmaal":[{"_type":"block","style":"normal","_key":"e86c485d5741","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"62f042e2b4650"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"0e686390621f"},{"_type":"span","marks":[],"text":". Du ","_key":"ff1773472380"},{"_key":"35011eddda2e","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" i ","_key":"027d5a513db9"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"38d0d16a7557"},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"de0f4a4064bb"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"9964d5e6429a"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"0fd338269be7"},{"valgReferanse":{"_type":"reference","_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a"},"_type":"valgfeltV2","_key":"da16fe809d13","skalHaStorForbokstav":false},{"text":" i ","_key":"544d350b2d36","_type":"span","marks":[]},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"7adeaafc67e9"},{"_key":"e4716ebe060c","_type":"span","marks":[],"text":" og tilhører derfor norsk lovgivning. Du får hele barnetrygden fra Norge."}]}],"hjemler":["2","4","11"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","hjemlerEOSForordningen883":["2","11-16","67","68"],"barnetsBostedsland":["IKKE_NORGE"],"_updatedAt":"2023-09-24T16:40:21Z","valgbarhet":"STANDARD","mappe":["EØS","INNVILGET"],"visningsnavn":"43. Selvstendig rett primærland standard","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_type":"begrunnelse","tema":"EØS","_createdAt":"2023-09-19T12:28:46Z"},{"hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"FORTSATT_INNVILGET","valgbarhet":"STANDARD","mappe":["EØS","FORTSATT_INNVILGET"],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_type":"begrunnelse","begrunnelsetype":"FORTSATT_INNVILGET","triggereIBruk":["KOMPETANSE"],"_id":"begrunnelse-bbdd8222-df7e-49ac-a10f-3f8df0a02d76","apiNavn":"fortsattInnvilgetPrimaerlandUkStandard","hjemler":["2","4","11"],"hjemlerSeperasjonsavtalenStorbritannina":["29"],"nynorsk":[{"markDefs":[],"children":[{"_key":"9befac8d88b6","_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"bc210c4b11e7"},{"marks":[],"text":". Du ","_key":"00db197fe502","_type":"span"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"4c3cff9ee23b","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"0fea690083b6"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"d3e46b6553d2","skalHaStorForbokstav":true},{"text":" bur i ","_key":"fa5bbfe8fc6b","_type":"span","marks":[]},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"d989a453b0e7"},{"_type":"span","marks":[],"text":". Den andre forelderen jobbar ikkje i ","_key":"edbe95a1366b"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"021d07863ea1"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen får du heile barnetrygda frå Noreg.","_key":"cf75e69b152d"}],"_type":"block","style":"normal","_key":"75ddf652b1a6"}],"_createdAt":"2022-11-30T14:03:43Z","_updatedAt":"2023-09-25T10:44:27Z","annenForeldersAktivitet":["MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","INAKTIV"],"visningsnavn":"5. Primærland UK standard","_rev":"h26rAhFEYSUtDGXJE0vHXG","vedtakResultat":"INGEN_ENDRING","bokmaal":[{"_key":"52c2654ebbcb","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn født ","_key":"11bf3f752621"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"778f1a9fb6b2"},{"_type":"span","marks":[],"text":". Du ","_key":"120282565962"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"d4051e863c89","skalHaStorForbokstav":false},{"text":". ","_key":"f284c5f9ddc7","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"a7c8d8f6f36f","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bor i ","_key":"f764eb1f1d2c"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"e994cc9761ea"},{"_type":"span","marks":[],"text":". Den andre forelderen jobber ikke i ","_key":"68d7d999899d"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"e0fe4fb5c78d"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen får du hele barnetrygden fra Norge.","_key":"0479f9090d5b"}],"_type":"block","style":"normal"}],"navnISystem":"Primærland UK standard","tema":"EØS","barnetsBostedsland":["IKKE_NORGE"]},{"vedtakResultat":"INGEN_ENDRING","nynorsk":[{"_type":"block","style":"normal","_key":"3bbdac542235","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"8ab955504ad6"},{"_type":"eosFlettefelt","_key":"7c285bbaf22c","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":". Du ","_key":"c98600598b6b"},{"_type":"valgfeltV2","_key":"2b6feb3dda50","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"}},{"_type":"span","marks":[],"text":". Barnet bur i ","_key":"ab1afd011cbb"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"bccc45d9069f"},{"_type":"span","marks":[],"text":". Den andre forelderen jobbar i ","_key":"3a3a14622b9a"},{"_key":"f9a7ace81763","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen har Storbritannia hovudansvaret for utbetaling av barnetrygd. Noreg utbetalar derfor forskjellen mellom barnetrygda i Storbritannia og norsk barnetrygd.","_key":"9a8044a2837e"}]}],"triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","barnetsBostedsland":["IKKE_NORGE"],"_updatedAt":"2023-09-25T10:45:57Z","_rev":"h26rAhFEYSUtDGXJE0vJzg","periodeType":"FORTSATT_INNVILGET","tema":"EØS","mappe":["EØS","FORTSATT_INNVILGET"],"navnISystem":"Sekundærland UK standard","hjemler":["2","4","11"],"visningsnavn":"19. Sekundærland UK standard","hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"FORTSATT_INNVILGET","_createdAt":"2022-12-01T07:51:41Z","_id":"begrunnelse-c599327d-eaae-4203-9b65-c81bfcf19ec5","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND"],"bokmaal":[{"style":"normal","_key":"17a542027f58","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"e58829b6e630","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"4e9bea9c2dab"},{"_type":"span","marks":[],"text":". Du ","_key":"7484803098a3"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"d35d9443b095","skalHaStorForbokstav":false},{"text":". Barnet bor i ","_key":"526b09b94ee8","_type":"span","marks":[]},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"1cd32afcf00f"},{"marks":[],"text":". Den andre forelderen jobber i ","_key":"3ed56d60d260","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"e9eb2edf3095"},{"marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen har Storbritannia hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i Storbritannia og norsk barnetrygd.","_key":"b5796a9db5a7","_type":"span"}],"_type":"block"}],"apiNavn":"fortsattInnvilgetSekundaerlandUkStandard","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_type":"begrunnelse","hjemlerSeperasjonsavtalenStorbritannina":["29"]},{"nynorsk":[{"children":[{"_key":"91a1ffa7235d","_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"6cde956a5423"},{"_key":"a24bacdc56ce","_type":"span","marks":[],"text":". Du "},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"a55aeb6d2877","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"8dcc21ccba8d"},{"_key":"69ba3282487d","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_key":"494a6cbd27db","_type":"span","marks":[],"text":" bur saman med deg i Noreg. Den andre forelderen "},{"valgReferanse":{"_type":"reference","_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a"},"_type":"valgfeltV2","_key":"eec92c048a61","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"c4d68169d337"},{"_type":"eosFlettefelt","_key":"bf341db650c5","flettefelt":"annenForeldersAktivitetsland"},{"_type":"span","marks":[],"text":". Du får derfor heile barnetrygda frå Noreg.","_key":"8faf75448b3d"}],"_type":"block","style":"normal","_key":"00ef72ae7452","markDefs":[]}],"_createdAt":"2022-11-30T14:22:33Z","barnetsBostedsland":["IKKE_NORGE"],"mappe":["EØS","FORTSATT_INNVILGET"],"navnISystem":"Primærland særkullsbarn/andre barn","behandlingstema":"EØS","hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"FORTSATT_INNVILGET","_updatedAt":"2023-09-25T10:44:42Z","_type":"begrunnelse","_id":"begrunnelse-c5a6c851-5a1b-4a13-b5d5-fef9f80ca877","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"f81acccbea20"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"339b96571855"},{"_type":"span","marks":[],"text":". Du ","_key":"dbee46f8c852"},{"_key":"351ea34007cd","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2"},{"_key":"1a8858d52491","_type":"span","marks":[],"text":". "},{"skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"d4cc348ab612"},{"_type":"span","marks":[],"text":" bor sammen med deg i Norge. Den andre forelderen ","_key":"02a3e2705a95"},{"valgReferanse":{"_type":"reference","_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a"},"_type":"valgfeltV2","_key":"3e3bf3ac727e","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"f8b422312cc8"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"b3d1dca7822c"},{"_type":"span","marks":[],"text":". Du får derfor hele barnetrygden fra Norge.","_key":"fdea9c86e90e"}],"_type":"block","style":"normal","_key":"904cdbea0aa8"}],"tema":"EØS","triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","hjemler":["2","4","11"],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET","apiNavn":"fortsattInnvilgetPrimaerlandSaerkullsbarnAndreBarn","visningsnavn":"9. Primærland særkullsbarn/andre barn","_rev":"FuD004taptHFqBZyEy9MsO","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"]},{"barnetsBostedsland":["IKKE_NORGE"],"_updatedAt":"2023-09-24T17:04:33Z","hjemler":["2","4","11"],"visningsnavn":"48. Selvstendig rett sekundærland UK standard","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","_createdAt":"2023-09-19T14:01:33Z","navnISystem":"Selvstendig rett sekundærland UK standard","apiNavn":"innvilgetSelvstendigRettSekundaerlandUkStandard","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"hjemlerEOSForordningen883":["2","11-16","67","68"],"_id":"begrunnelse-c78f7ed1-b10d-4d71-bf32-a2bd00866449","_type":"begrunnelse","valgbarhet":"STANDARD","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER","ARBEIDER","SELVSTENDIG_NÆRINGSDRIVENDE"],"bokmaal":[{"_type":"block","style":"normal","_key":"3438a78a75c0","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"5356a11b65a00"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"90410cca98f3"},{"_type":"span","marks":[],"text":". Du ","_key":"62a0d023b1b7"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"574c8a1cfbf9","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"e36722118714"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"f28a2fec998f"},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"a7c675b90966"},{"_type":"eosFlettefelt","_key":"559dd483d7cc","flettefelt":"barnetsBostedsland"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"a3c5ef683636"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"111f05b77cf9","skalHaStorForbokstav":false},{"_key":"08af109c8972","_type":"span","marks":[],"text":" i "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"23160bf2f0ce"},{"text":" og tilhører derfor norsk lovgivning. Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Storbritannia har hovedansvaret for utbetaling av barnetrygd. Norge utbetaler forskjellen mellom barnetrygden i Storbritannia og norsk barnetrygd.","_key":"def67e14e7b2","_type":"span","marks":[]}]}],"triggereIBruk":["KOMPETANSE"],"mappe":["EØS","INNVILGET"],"_rev":"BtltdVb0HP4g4WJfnqVEmJ","hjemlerSeperasjonsavtalenStorbritannina":["29"],"periodeType":"UTBETALING","tema":"EØS","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"a63df58b2cce0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"cda20b847fa3"},{"_type":"span","marks":[],"text":". Du ","_key":"27ebe95967d4"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"0c905381b9a7","skalHaStorForbokstav":false},{"_key":"8b14f47ef3da","_type":"span","marks":[],"text":" i "},{"_type":"eosFlettefelt","_key":"9cd577565589","flettefelt":"sokersAktivitetsland"},{"_key":"0bfce6c53b95","_type":"span","marks":[],"text":". Barnet bur i "},{"_type":"eosFlettefelt","_key":"bf87baffb822","flettefelt":"barnetsBostedsland"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"83118f55d23d"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"99dfee4af134","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"ddd967955e84"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"a1edb9932c55"},{"_type":"span","marks":[],"text":" og tilhøyrer derfor norsk lovgjeving. Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Storbritannia har hovudansvaret for utbetaling av barnetrygd. Noreg utbetalar skilnaden mellom barnetrygda i Storbritannia og norsk barnetrygd.","_key":"abad3ff4d465"}],"_type":"block","style":"normal","_key":"acbb41c1eb6e"}]},{"barnetsBostedsland":["IKKE_NORGE"],"apiNavn":"fortsattInnvilgetTilleggstekstNullutbetaling","visningsnavn":"17. Tilleggstekst nullutbetaling","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET","nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Barnetrygda i ","_key":"a37f2d098770","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"e64e6f60b806"},{"_type":"span","marks":[],"text":" er høgare enn norsk barnetrygd. Derfor får du ikkje utbetalt barnetrygd frå Noreg.","_key":"9ba8443e10ad"}],"_type":"block","style":"normal","_key":"95b97bc2b039"}],"valgbarhet":"TILLEGGSTEKST","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden i ","_key":"3d737192bd4b"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"b717323a86b5"},{"_key":"ba5b0ca48ab9","_type":"span","marks":[],"text":" er høyere enn norsk barnetrygd. Derfor får du ikke utbetalt barnetrygd fra Norge."}],"_type":"block","style":"normal","_key":"bcda0bc874b5"}],"_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"tema":"EØS","_createdAt":"2022-12-01T07:39:08Z","_id":"begrunnelse-ca04b393-0432-4cdd-b2fa-0627eca040e4","_updatedAt":"2023-09-25T10:45:12Z","hjemler":["11"],"_rev":"h26rAhFEYSUtDGXJE0vIcK","begrunnelsetype":"FORTSATT_INNVILGET","triggereIBruk":["KOMPETANSE"],"mappe":["EØS","FORTSATT_INNVILGET"],"navnISystem":"Tilleggstekst nullutbetaling","behandlingstema":"EØS","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND"]},{"_rev":"h26rAhFEYSUtDGXJE0sV8b","periodeType":"UTBETALING","nynorsk":[{"_type":"block","style":"normal","_key":"4007e624838e","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"2e22b1f86e300"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"3d1e24b29c49"},{"_type":"span","marks":[],"text":". Du ","_key":"f3c73a0a35e5"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"77a06c1d9c54","skalHaStorForbokstav":false},{"text":" i ","_key":"e3b2d7e1675d","_type":"span","marks":[]},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"f7dc4261e310"},{"_type":"span","marks":[],"text":". Barnet bur i ","_key":"364d12b4da02"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"07de477e5d42"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"06ace10e9e98"},{"_key":"655d212fad6a","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2"},{"_type":"span","marks":[],"text":" i ","_key":"656ef2ad2e10"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"a40ce0e05767"},{"_type":"span","marks":[],"text":" og tilhøyrer derfor norsk lovgjeving. Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen får du heile barnetrygda frå Noreg.","_key":"b3228a3fb9ca"}]}],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER"],"bokmaal":[{"children":[{"_key":"843423e6c6e80","_type":"span","marks":[],"text":"Du får barnetrygd for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"3e98639b6c16"},{"marks":[],"text":". Du ","_key":"abcb58172249","_type":"span"},{"valgReferanse":{"_type":"reference","_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb"},"_type":"valgfeltV2","_key":"96bfd6f14604","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"3417fb0e4da0"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"ba138917c334"},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"9b5aa9e8be52"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"fcea5a23f588"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"4d874c43c48f"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"4eb1208e3b3e","skalHaStorForbokstav":false},{"_key":"a0c0792227fd","_type":"span","marks":[],"text":" i "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"2f589f930365"},{"_type":"span","marks":[],"text":" og tilhører derfor norsk lovgivning. Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen får du hele barnetrygden fra Norge.","_key":"b5f86c049cc4"}],"_type":"block","style":"normal","_key":"22c7dabc5232","markDefs":[]}],"apiNavn":"fortsattInnvilgetSelvstendigRettPrimaerlandUkStandard","hjemlerSeperasjonsavtalenStorbritannina":["29"],"valgbarhet":"STANDARD","_id":"begrunnelse-ccb7de18-7547-4b42-8bc9-122dbcb7ee26","mappe":["EØS","FORTSATT_INNVILGET"],"_createdAt":"2023-09-19T14:35:09Z","barnetsBostedsland":["IKKE_NORGE"],"_updatedAt":"2023-09-25T10:15:50Z","hjemler":["2","4","11"],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"tema":"EØS","navnISystem":"Selvstendig rett primærland UK standard","visningsnavn":"29. Selvstendig rett primærland UK standard","vedtakResultat":"INGEN_ENDRING","begrunnelsetype":"FORTSATT_INNVILGET","triggereIBruk":["KOMPETANSE"]},{"visningsnavn":"18. Selvstendig rett standard avslag","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","tema":"EØS","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"9f464f85bd980"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"4dfaf1a04d7b"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikkje tilhøyrer norsk lovgjeving.","_key":"990ae348b630"}],"_type":"block","style":"normal","_key":"c3e012a8bf1d"}],"bokmaal":[{"_key":"81f1c7833fe4","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"52b74a028c670"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"6fa0e1338f11"},{"_type":"span","marks":[],"text":" fordi den andre forelderen ikke tilhører norsk lovgivning.","_key":"763982f467e8"}],"_type":"block","style":"normal"}],"navnISystem":"Selvstendig rett standard avslag","apiNavn":"avslagSelvstendigRettStandardAvslag","hjemler":["2","4","11"],"hjemlerEOSForordningen883":["2","11-16","67"],"valgbarhet":"STANDARD","_rev":"h26rAhFEYSUtDGXJE0vGlV","periodeType":"INGEN_UTBETALING","triggereIBruk":["VILKÅRSVURDERING"],"_updatedAt":"2023-09-25T10:43:58Z","eosVilkaar":["BOSATT_I_RIKET"],"begrunnelsetype":"AVSLAG","_createdAt":"2023-09-19T14:20:21Z","_id":"begrunnelse-ce16cdbd-0e33-4317-a4e4-6791fbfcd52b","mappe":["EØS","AVSLAG"]},{"periodeType":"INGEN_UTBETALING","_updatedAt":"2023-09-25T10:43:53Z","bokmaal":[{"_type":"block","style":"normal","_key":"18d5e81b6276","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"4d5d7b02c9720"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"f81fb835c27c"},{"marks":[],"text":" fordi du jobber som selvstendig næringsdrivende i Norge, samtidig som du er arbeidstaker i et annet EØS-land. Vi har derfor kommet fram til at lovgivningen i det andre EØS-landet gjelder for deg.","_key":"77829d2f487f","_type":"span"}]}],"visningsnavn":"17. Selvstendig næringsdrivende Norge arbeidstaker i annet EØS land","vedtakResultat":"IKKE_INNVILGET","tema":"EØS","_createdAt":"2023-05-04T06:24:42Z","triggereIBruk":["VILKÅRSVURDERING"],"mappe":["EØS","AVSLAG"],"_type":"begrunnelse","hjemler":["2","4","5"],"behandlingstema":"EØS","_rev":"BtltdVb0HP4g4WJfnr626o","eosVilkaar":["BOSATT_I_RIKET"],"apiNavn":"avslagSelvstendigNaeringsdrivendeNorgeArbeidstakerIAnnetEosLand","hjemlerEOSForordningen883":["2","11-16","67"],"begrunnelsetype":"AVSLAG","nynorsk":[{"style":"normal","_key":"e50f3cceccb3","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"d85c13aea86f0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"bc23781bf2e9"},{"_key":"fb80bd940de2","_type":"span","marks":[],"text":" fordi du jobbar som sjølvstendig næringsdrivande i Noreg, samtidig som du er arbeidstakar i eit anna EØS-land. Vi har derfor kome fram til at lovgjevnaden i det andre EØS-landet gjeld for deg."}],"_type":"block"}],"valgbarhet":"STANDARD","_id":"begrunnelse-ce46b661-bd27-4d53-8f0d-9d362a352c6b","navnISystem":"Selvstendig næringsdrivende Norge arbeidstaker i annet EØS land"},{"_type":"begrunnelse","_createdAt":"2022-12-01T08:10:27Z","valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"ddd2e1a861b2"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"70a89edca688"},{"text":". Du ","_key":"fc190feba5f6","_type":"span","marks":[]},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"7aa625229c1f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bor i ","_key":"30c121c40485"},{"_key":"e8acd153c4ad","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"marks":[],"text":". Den andre forelderen ","_key":"c3e7935c6932","_type":"span"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"3f3c2c383743"},{"_type":"span","marks":[],"text":" i ","_key":"761758f83525"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"021434eed71f"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen har Storbritannia hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i Storbritannia og norsk barnetrygd. ","_key":"f48b558f0370"}],"_type":"block","style":"normal","_key":"a371935a5a96"}],"vedtakResultat":"INGEN_ENDRING","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"FORTSATT_INNVILGET","nynorsk":[{"style":"normal","_key":"6145fb4cea68","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"9ba4bcdee577"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"94d1f1100157"},{"_type":"span","marks":[],"text":". Du ","_key":"e0bca92e40c6"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"bc448db5d43a","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bur i ","_key":"e2bf4653a6d8"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"6e5dd198ab93"},{"_key":"19c3167405cd","_type":"span","marks":[],"text":". Den andre forelderen "},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"b258713cc360","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"3cb29cb8dd89"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"189337850954"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen har Storbritannia hovudansvaret for utbetaling av barnetrygd. Noreg utbetalar derfor forskjellen mellom barnetrygda i Storbritannia og norsk barnetrygd. ","_key":"dc5492891671"}],"_type":"block"}],"navnISystem":"Sekundærland UK og utland standard","apiNavn":"fortsattInnvilgetSekundaerlandUkOgUtland","hjemler":["2","4","11"],"behandlingstema":"EØS","visningsnavn":"21. Sekundærland UK og utland standard","hjemlerSeperasjonsavtalenStorbritannina":["29"],"tema":"EØS","barnetsBostedsland":["IKKE_NORGE"],"_id":"begrunnelse-cf5aee9b-8548-474d-826a-f206828c674a","mappe":["EØS","FORTSATT_INNVILGET"],"_updatedAt":"2023-09-25T10:46:08Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND"],"kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_rev":"BtltdVb0HP4g4WJfnr6B2J","begrunnelsetype":"FORTSATT_INNVILGET","triggereIBruk":["KOMPETANSE"]},{"periodeType":"FORTSATT_INNVILGET","_id":"begrunnelse-d01ecf3d-3f34-4769-a01a-7eaf03c40e31","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"hjemler":["2","4","11"],"visningsnavn":"26. Tilleggstekst sekundær ikke fått svar på SED","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"hjemlerEOSForordningen883":["2","11-16","67","68"],"_createdAt":"2022-12-01T08:27:25Z","barnetsBostedsland":["NORGE","IKKE_NORGE"],"navnISystem":"Tilleggstekst sekundær ikke fått svar på SED","behandlingstema":"EØS","_rev":"BtltdVb0HP4g4WJfnr6Cho","tema":"EØS","_updatedAt":"2023-09-25T10:46:28Z","_type":"begrunnelse","begrunnelsetype":"FORTSATT_INNVILGET","triggereIBruk":["KOMPETANSE"],"mappe":["EØS","FORTSATT_INNVILGET"],"bokmaal":[{"markDefs":[],"children":[{"text":"Vi har ikke fått svar på opplysninger vi har bedt om fra ","_key":"0edfb5b8e534","_type":"span","marks":[]},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"7d6f3490088a"},{"_type":"span","marks":[],"text":". Norge utbetaler derfor forskjellen mellom barnetrygden familien kan ha rett på fra ","_key":"4d3b63e4ea49"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"7731fedfefaa"},{"_type":"span","marks":[],"text":" og norsk barnetrygd. Saken må vurderes på nytt hvis vi får svar fra ","_key":"7af8fd4c6372"},{"_key":"84e74d22df27","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":".","_key":"ce3155c56086"}],"_type":"block","style":"normal","_key":"a7076dbe1386"}],"apiNavn":"fortsattInnvilgetTilleggsteksterSekundaerIkkeFaattSvarPaaSed","vedtakResultat":"INGEN_ENDRING","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Vi har ikkje fått svar på opplysningar vi har bedt om frå ","_key":"abfdd176d8e9"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"94aa7c8d53ea"},{"_type":"span","marks":[],"text":". Noreg utbetalar derfor forskjellen mellom barnetrygda familien kan ha rett på frå ","_key":"cc1c360c5e1e"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"8275ebc657a0"},{"_type":"span","marks":[],"text":" og norsk barnetrygd. Saka må vurderast på nytt dersom vi får svar frå ","_key":"2c6e844bf8c9"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"c1049662fcdc"},{"_type":"span","marks":[],"text":".","_key":"a1700d903ffd"}],"_type":"block","style":"normal","_key":"a6c3a6d6bd11"}],"valgbarhet":"TILLEGGSTEKST"},{"navnISystem":"Primærland barnet bor i Norge","vedtakResultat":"INGEN_ENDRING","tema":"EØS","triggereIBruk":["KOMPETANSE"],"apiNavn":"fortsattInnvilgetPrimaerlandBarnetBorINorge","_type":"begrunnelse","begrunnelsetype":"FORTSATT_INNVILGET","_rev":"BtltdVb0HP4g4WJfnr63Mo","hjemlerEOSForordningen883":["2","11-16","67","68"],"barnetsBostedsland":["NORGE"],"_updatedAt":"2023-09-25T10:44:38Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"d1a826be8dd5"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"428608c5a966"},{"marks":[],"text":". Du ","_key":"ccbfd2856ac5","_type":"span"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"946219a52906"},{"_type":"span","marks":[],"text":". ","_key":"e3e5c339c0cd"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"4f25ffa2bcad","skalHaStorForbokstav":true},{"text":" bor sammen med deg i Norge. Den andre forelderen ","_key":"405b0cbf9afb","_type":"span","marks":[]},{"_type":"valgfeltV2","_key":"3b7a0a847725","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"}},{"marks":[],"text":" i ","_key":"5587bd631560","_type":"span"},{"_type":"eosFlettefelt","_key":"006e583ba65f","flettefelt":"annenForeldersAktivitetsland"},{"_type":"span","marks":[],"text":". Du får derfor hele barnetrygden fra Norge.","_key":"f4be16a3d385"}],"_type":"block","style":"normal","_key":"c3b6f6313509","markDefs":[]}],"hjemler":["2","4","11"],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_createdAt":"2022-11-30T14:18:53Z","valgbarhet":"STANDARD","_id":"begrunnelse-d4e340a9-749e-47dd-b5eb-d648693ddbdc","mappe":["EØS","FORTSATT_INNVILGET"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"visningsnavn":"8. Primærland barnet bor i Norge","periodeType":"FORTSATT_INNVILGET","nynorsk":[{"_type":"block","style":"normal","_key":"177bcfbb6ec0","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"532253bddf9c","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"c2f566cd2296"},{"_key":"779401da1e3d","_type":"span","marks":[],"text":". Du "},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"6398009f8aa1"},{"_type":"span","marks":[],"text":". ","_key":"554847f423ae"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"e3c3b85ad2b3","skalHaStorForbokstav":true},{"marks":[],"text":" bur saman med deg i Noreg. Den andre forelderen ","_key":"df9b94c164ef","_type":"span"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"5f0e9cb1e5da","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"a11b7ee44545"},{"_key":"04f785bf0d16","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"_key":"c726b135c40d","_type":"span","marks":[],"text":". Du får derfor heile barnetrygda frå Noreg."}]}]},{"apiNavn":"fortsattInnvilgetTilleggstekstUkFullUtbetaling","_rev":"BtltdVb0HP4g4WJfnr6Cqo","_type":"begrunnelse","valgbarhet":"TILLEGGSTEKST","vedtakResultat":"INGEN_ENDRING","periodeType":"FORTSATT_INNVILGET","_createdAt":"2022-12-01T08:47:13Z","_id":"begrunnelse-d84182b0-0ca6-4b86-a73a-0776d161e0c8","nynorsk":[{"markDefs":[],"children":[{"text":"Du får heile barnetrygda utbetalt frå Noreg. Fordi familien din betalar \"High Income Tax\" i Storbritannia, får de ikkje utbetalt barnetrygd frå Storbritannia.","_key":"b04fda3b259c","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"94d9a7184c17"},{"_key":"4da6cd7907c3","markDefs":[],"children":[{"text":"","_key":"45753ed0cdf2","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"triggereIBruk":["KOMPETANSE"],"mappe":["EØS","FORTSATT_INNVILGET"],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND"],"hjemlerEOSForordningen883":["2","11-16","67","68"],"hjemlerSeperasjonsavtalenStorbritannina":["29"],"begrunnelsetype":"FORTSATT_INNVILGET","_updatedAt":"2023-09-25T10:46:32Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"bokmaal":[{"_key":"0a062e7d4e01","markDefs":[],"children":[{"marks":[],"text":"Du får hele barnetrygden utbetalt fra Norge. Fordi familien din betaler \"High Income Tax\" i Storbritannia, får dere ikke utbetalt barnetrygd fra Storbritannia.","_key":"cf3f4b6eabc9","_type":"span"}],"_type":"block","style":"normal"}],"navnISystem":"Tilleggstekst UK full utbetaling","hjemler":["2","4","11"],"visningsnavn":"27. Tilleggstekst UK full utbetaling","tema":"EØS","barnetsBostedsland":["NORGE","IKKE_NORGE"]},{"hjemlerSeperasjonsavtalenStorbritannina":["29"],"periodeType":"UTBETALING","nynorsk":[{"_key":"2c65f56cb91e","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"f56fe435afe00","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"3de2edd6f4da"},{"_type":"span","marks":[],"text":". Du ","_key":"93db55f8a1c5"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"ae2db6f40062"},{"_type":"span","marks":[],"text":" i ","_key":"0af9e136b414"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"f35c88a4823b"},{"_key":"0fd6cb8257b7","_type":"span","marks":[],"text":". Den andre forelderen "},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"625b214876d3","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"71f529c83836"},{"_type":"eosFlettefelt","_key":"bfa24b6c758c","flettefelt":"annenForeldersAktivitetsland"},{"_key":"443e16db2bee","_type":"span","marks":[],"text":" og tilhøyrer derfor norsk lovgjeving. Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Du får heile barnetrygda frå Noreg."}],"_type":"block","style":"normal"}],"navnISystem":"Selvstendig rett primærland UK og utland standard","hjemler":["2","4","11"],"visningsnavn":"45. Selvstendig rett primærland UK og utland standard","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"vedtakResultat":"INNVILGET_ELLER_ØKNING","triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["IKKE_NORGE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","UTSENDT_ARBEIDSTAKER","ARBEIDER","SELVSTENDIG_NÆRINGSDRIVENDE","ARBEIDER_PÅ_NORSKREGISTRERT_SKIP","ARBEIDER_PÅ_NORSK_SOKKEL","ARBEIDER_FOR_ET_NORSK_FLYSELSKAP","ARBEIDER_VED_UTENLANDSK_UTENRIKSSTASJON"],"bokmaal":[{"_type":"block","style":"normal","_key":"b265e9d55ea7","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn født ","_key":"ac44fc1a47260"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"22685791519b"},{"_type":"span","marks":[],"text":"]. Du ","_key":"369fb778b8b5"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"d42daa2d916c","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"356312220644"},{"flettefelt":"sokersAktivitetsland","_type":"eosFlettefelt","_key":"67bb07c88d2c"},{"marks":[],"text":". Den andre forelderen ","_key":"bca03c18d207","_type":"span"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"58e65800601e"},{"marks":[],"text":" i ","_key":"5f6a7efc2a60","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"dbd2cb756056"},{"_type":"span","marks":[],"text":" og tilhører derfor norsk lovgiving. Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Du får hele barnetrygden fra Norge.","_key":"63a6f3484b52"}]}],"apiNavn":"innvilgetSelvstendigRettPrimaerlandUkOgStandard","_rev":"BtltdVb0HP4g4WJfnqUeho","tema":"EØS","_createdAt":"2023-09-19T12:53:55Z","_updatedAt":"2023-09-24T16:46:48Z","hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","_id":"begrunnelse-dd61f6f7-94a1-4623-a671-d95c81ec604e","mappe":["EØS","INNVILGET"],"_type":"begrunnelse"},{"_updatedAt":"2023-09-25T10:44:15Z","annenForeldersAktivitet":["IKKE_AKTUELT"],"_rev":"BtltdVb0HP4g4WJfnr62mo","periodeType":"FORTSATT_INNVILGET","vedtakResultat":"INGEN_ENDRING","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"aa1b296b1e2a"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"7f690bd2402a"},{"_type":"span","marks":[],"text":". Du ","_key":"5442356611bb"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"5336bccdb4ef","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"a36c00daea33"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"c538bc9e5da3","skalHaStorForbokstav":true},{"_key":"9d3618159bd6","_type":"span","marks":[],"text":" bur i "},{"_type":"eosFlettefelt","_key":"00fcb91739ab","flettefelt":"barnetsBostedsland"},{"_type":"span","marks":[],"text":". Du har ansvar for ","_key":"5020cfc0522d"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"5fc31e5e98e5"},{"text":" åleine. Du får derfor heile barnetryga frå Noreg.","_key":"33ce1eab738b","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"d8ec9ecb6c20","markDefs":[]}],"_createdAt":"2022-11-30T11:59:03Z","triggereIBruk":["KOMPETANSE"],"_id":"begrunnelse-e8c52a45-12ca-471e-a39b-d87481f15422","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"e41be3bba48f","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"56b7494d2d3d"},{"_type":"span","marks":[],"text":". Du ","_key":"635d844979a0"},{"valgReferanse":{"_type":"reference","_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb"},"_type":"valgfeltV2","_key":"87a209e9d856","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"a98f1a363602"},{"_key":"1eb93ee87681","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_key":"22fb4fd33056","_type":"span","marks":[],"text":" bor i "},{"_key":"0296ebf94823","flettefelt":"barnetsBostedsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Du har ansvar for ","_key":"a08788f62073"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"93df5b2211df","skalHaStorForbokstav":false},{"_key":"570aa4eb9ea4","_type":"span","marks":[],"text":" alene. Du får derfor hele barnetrygden fra Norge."}],"_type":"block","style":"normal","_key":"e7bb76234e1a"}],"hjemler":["2","4","11"],"visningsnavn":"2. Primærland aleneansvar","tema":"EØS","navnISystem":"Primærland aleneansvar","_type":"begrunnelse","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"FORTSATT_INNVILGET","valgbarhet":"STANDARD","barnetsBostedsland":["NORGE","IKKE_NORGE"],"mappe":["EØS","FORTSATT_INNVILGET"],"apiNavn":"fortsattInnvilgetPrimaerlandAleneansvar","behandlingstema":"EØS"},{"tema":"EØS","triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"navnISystem":"Tilleggsbegrunnelse vedtak før SED","hjemler":["2","4","11"],"_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"UTBETALING","_updatedAt":"2023-09-25T10:15:50Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_rev":"h26rAhFEYSUtDGXJE0sV8b","vedtakResultat":"INGEN_ENDRING","begrunnelsetype":"FORTSATT_INNVILGET","apiNavn":"fortsattInnvilgetTilleggsbegrunnelseVedtakForSed","nynorsk":[{"_type":"block","style":"normal","_key":"f5bc661344b8","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Vedtaket er gjort før vi har fått opplysningar frå ","_key":"a90cd0e592290"},{"_type":"eosFlettefelt","_key":"e73fdf4dcea9","flettefelt":"annenForeldersAktivitetsland"},{"_type":"span","marks":[],"text":". Saka må vurderast på nytt dersom vi mottar nye opplysningar.","_key":"77475f13a8d4"}]}],"_createdAt":"2022-11-30T15:04:43Z","_id":"begrunnelse-ee794f3a-848d-4ec7-a6e9-e115c0362e15","bokmaal":[{"_key":"bdbfd2893e54","markDefs":[],"children":[{"marks":[],"text":"Vedtaket er gjort før vi har fått opplysninger fra ","_key":"4cec17f562da0","_type":"span"},{"_key":"2fed4453b4d4","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"marks":[],"text":". Saken må vurderes på nytt dersom vi mottar nye opplysninger.","_key":"41b545582329","_type":"span"}],"_type":"block","style":"normal"}],"visningsnavn":"15. Tilleggsbegrunnelse vedtak før SED","valgbarhet":"TILLEGGSTEKST","mappe":["EØS","FORTSATT_INNVILGET"]},{"_rev":"h26rAhFEYSUtDGXJE0sV8b","vedtakResultat":"REDUKSJON","triggereIBruk":["KOMPETANSE"],"barnetsBostedsland":["NORGE","IKKE_NORGE"],"mappe":["EØS","REDUKSJON"],"bokmaal":[{"_type":"block","style":"normal","_key":"2bf908b5f9e2","markDefs":[],"children":[{"text":"Barnetrygden er endret fordi du ikke lenger har ansvar for barn født ","_key":"ca9c8bcdeeef0","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"9cd10580a613"},{"_key":"c03dc3ad9540","_type":"span","marks":[],"text":". "}]}],"apiNavn":"reduksjonIkkeAnsvarForBarn","behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND","TO_PRIMÆRLAND"],"hjemlerEOSForordningen883":["2","11-16","67","68"],"nynorsk":[{"_key":"df0fcce4207c","markDefs":[],"children":[{"text":"Barnetrygda er endra fordi du ikkje lenger har ansvar for barn fødd ","_key":"6fb060c6e8cf0","_type":"span","marks":[]},{"_key":"78efa3da44f5","flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt"},{"text":". ","_key":"a5c0ab5fe190","_type":"span","marks":[]}],"_type":"block","style":"normal"}],"visningsnavn":"4. Ikke ansvar for barn","_type":"begrunnelse","begrunnelsetype":"REDUKSJON","tema":"EØS","_createdAt":"2022-11-30T08:49:30Z","_id":"begrunnelse-fa45497c-893d-4b1c-81c3-7ca3de3c8c2d","_updatedAt":"2023-09-25T10:15:50Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"navnISystem":"Ikke ansvar for barn","hjemler":["2","4","11"],"periodeType":"UTBETALING","valgbarhet":"STANDARD"},{"hjemlerEOSForordningen987":["58"],"_type":"begrunnelse","valgbarhet":"STANDARD","mappe":["EØS","FORTSATT_INNVILGET"],"navnISystem":"Sekundærland UK to arbeidsland Norge utbetaler","barnetsBostedsland":["NORGE"],"annenForeldersAktivitet":["I_ARBEID"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"f8ec0a60b05e"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"119d821f8430"},{"marks":[],"text":". Du ","_key":"c812b20bbfe3","_type":"span"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"0730969e9e0f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bor i Norge. Den andre forelderen jobber i ","_key":"fa839bb15965"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"a21610b58fb0"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Landene dere jobber i har hovedansvar for utbetaling av barnetrygd. Norsk barnetrygd er høyere enn barnetrygden i (SØKERS AKTIVITETSLAND) og i ","_key":"abc7bbe8085a"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"990d729f6d25"},{"_type":"span","marks":[],"text":". Norge utbetaler derfor forskjellen mellom barnetrygden i disse landene og norsk barnetrygd.","_key":"69972bc37724"}],"_type":"block","style":"normal","_key":"25d4062f7c1c"}],"kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_updatedAt":"2023-09-25T10:46:16Z","apiNavn":"fortsattInnvilgetSekundaerlandUkToArbeidslandNorgeUtbetaler","visningsnavn":"23. Sekundærland UK to arbeidsland Norge utbetaler","behandlingstema":"EØS","tema":"EØS","triggereIBruk":["KOMPETANSE"],"_id":"begrunnelse-fb6935eb-0d28-4ad6-8d1f-bb48e5a2bc15","hjemler":["2","4","11"],"_rev":"FuD004taptHFqBZyEy9VTC","hjemlerSeperasjonsavtalenStorbritannina":["29"],"_createdAt":"2022-12-01T08:16:59Z","begrunnelsetype":"FORTSATT_INNVILGET","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"FORTSATT_INNVILGET","nynorsk":[{"markDefs":[],"children":[{"_key":"6da6e4fcb17c","_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd "},{"_type":"eosFlettefelt","_key":"80fd59b41d81","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":". Du ","_key":"ebcb4f4e0c4a"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"946c5a4d65a3"},{"_key":"b4fd8963e4d5","_type":"span","marks":[],"text":". Barnet bur i Noreg. Den andre forelderen jobbar i "},{"_type":"eosFlettefelt","_key":"abd6bde4c619","flettefelt":"annenForeldersAktivitetsland"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Landa de jobbar i har hovudansvar for utbetaling av barnetrygd. Norsk barnetrygd er høgare enn barnetrygda i (SØKERS AKTIVITETSLAND) og i ","_key":"d956c4d7afe3"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"c6d3253e48de"},{"_type":"span","marks":[],"text":". Noreg utbetalar derfor forskjellen mellom barnetrygda i desse landa og norsk barnetrygd.","_key":"b6b6e21aa83f"}],"_type":"block","style":"normal","_key":"242a31338f95"}],"vedtakResultat":"INGEN_ENDRING"},{"visningsnavn":"35. Tilleggstekst primær delt bosted annen forelder ikke rett","triggereIBruk":["KOMPETANSE"],"_updatedAt":"2023-09-25T10:15:50Z","apiNavn":"innvilgetTilleggstekstPrimaerDeltBostedAnnenForelderIkkeRett","_rev":"h26rAhFEYSUtDGXJE0sV8b","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","nynorsk":[{"style":"normal","_key":"9f900e9597d5","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får heile barnetrygda for barn fødd ","_key":"fb771dc6f2b70"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"51d377edd630"},{"_type":"span","marks":[],"text":" som har delt bustad. Den andre forelderen har ikkje rett til barnetrygd. Barnetrygda kan derfor ikkje delast.","_key":"ce6b1bcdcd53"}],"_type":"block"}],"valgbarhet":"TILLEGGSTEKST","_id":"c2c2061d-fe04-4ef7-95be-ff5b3b33aa74","navnISystem":"Tilleggstekst primær delt bosted annen forelder ikke rett","bokmaal":[{"style":"normal","_key":"2c73011c45d7","markDefs":[],"children":[{"_key":"4a0f540fc06c0","_type":"span","marks":[],"text":"Du får hele barnetrygden for barn født "},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"da7312763068"},{"marks":[],"text":" som har delt bosted. Den andre forelderen har ikke rett til barnetrygd. Barnetrygden kan derfor ikke deles.","_key":"3c29010f7241","_type":"span"}],"_type":"block"}],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"begrunnelsetype":"INNVILGET","tema":"EØS","barnetsBostedsland":["NORGE","IKKE_NORGE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"hjemler":["2"],"_createdAt":"2022-10-17T10:25:12Z","mappe":["EØS","INNVILGET"],"behandlingstema":"EØS"},{"periodeType":"UTBETALING","bokmaal":[{"style":"normal","_key":"b5a62e0bc9cf","markDefs":[],"children":[{"_key":"25c3ef0c262f0","_type":"span","marks":[],"text":"Du får full barnetrygd fra Norge fordi det ikke er rett til barnetrygd fra "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"04868742bb22"},{"_type":"span","marks":[],"text":".","_key":"ffff5a3c75fb"}],"_type":"block"}],"apiNavn":"innvilgetTilleggstekstSekundaerFullUtbetaling","_createdAt":"2022-10-17T10:30:01Z","_id":"c48a1743-aac6-4792-b443-4e677d95a76e","barnetsBostedsland":["NORGE","IKKE_NORGE"],"mappe":["EØS","INNVILGET"],"_rev":"h26rAhFEYSUtDGXJE0sV8b","behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","triggereIBruk":["KOMPETANSE"],"valgbarhet":"TILLEGGSTEKST","navnISystem":"Tilleggstekst sekundær full utbetaling","_updatedAt":"2023-09-25T10:15:50Z","hjemlerEOSForordningen883":["2","11-16","67","68"],"tema":"EØS","nynorsk":[{"style":"normal","_key":"878e856585d9","markDefs":[],"children":[{"_key":"59781fb125390","_type":"span","marks":[],"text":"Du får full barnetrygd frå Noreg fordi det ikkje er rett til barnetrygd frå "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"6d394e829168"},{"text":".","_key":"6625cc4d73d1","_type":"span","marks":[]}],"_type":"block"}],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"visningsnavn":"36. Tilleggstekst sekundær full utbetaling"},{"visningsnavn":"5. Arbeider mer enn 25 prosent i annet EØS-land","hjemlerEOSForordningen883":["2","11-16","67"],"periodeType":"INGEN_UTBETALING","valgbarhet":"STANDARD","hjemler":["2","4","5"],"_rev":"h26rAhFEYSUtDGXJE0vEN9","vedtakResultat":"IKKE_INNVILGET","tema":"EØS","nynorsk":[{"_type":"block","style":"normal","_key":"75a8e1d2f377","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"9fdf73aaf7d40"},{"_key":"bd624a7ee55d","flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":" fordi du jobbar meir enn 25 prosent i eit anna EØS-land, der du også bur. Vi har derfor kome fram til at lovgjevnaden i det andre EØS-landet gjeld for deg.","_key":"3b5f11f02f11"}]}],"triggereIBruk":["VILKÅRSVURDERING"],"_id":"cc7c7e8a-b948-4b7e-9111-3503ca6e1452","mappe":["EØS","AVSLAG"],"navnISystem":"Arbeider mer enn 25 prosent i annet EØS-land","apiNavn":"avslagEosArbeiderMerEnn25ProsentIAnnetEosLand","behandlingstema":"EØS","_type":"begrunnelse","begrunnelsetype":"AVSLAG","_createdAt":"2022-11-01T13:39:39Z","_updatedAt":"2023-09-25T10:42:42Z","eosVilkaar":["BOSATT_I_RIKET"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"c194dce250db0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"9f2309b5a7ed"},{"_type":"span","marks":[],"text":" fordi du jobber mer enn 25 prosent i et annet EØS-land, der du også bor. Vi har derfor kommet fram til at lovgivningen i det andre EØS-landet gjelder for deg.","_key":"068457fee5ee"}],"_type":"block","style":"normal","_key":"c37322173ff4"}]},{"nynorsk":[{"style":"normal","_key":"c2a0a73debee","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn fødd ","_key":"e84cb9a3bddc0","_type":"span"},{"_type":"eosFlettefelt","_key":"3bd18981ea77","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi du berre har jobba i Noreg i korte periodar.","_key":"923d5946488c"}],"_type":"block"}],"triggereIBruk":["VILKÅRSVURDERING"],"bokmaal":[{"_type":"block","style":"normal","_key":"dd847e9e73df","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"f5204ba890460"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"56cd46561532"},{"_type":"span","marks":[],"text":" fordi du bare har jobbet i Norge i korte perioder.","_key":"d018f3a5b346"}]}],"hjemler":["2","4","5"],"visningsnavn":"6. Kun korte usammenhengende arbeidsperioder","behandlingstema":"EØS","_rev":"h26rAhFEYSUtDGXJE0vEVH","_type":"begrunnelse","vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"AVSLAG","tema":"EØS","navnISystem":"Kun korte usammenhengende arbeidsperioder","eosVilkaar":["LOVLIG_OPPHOLD"],"hjemlerEOSForordningen883":["2","11-16","67"],"valgbarhet":"STANDARD","mappe":["EØS","AVSLAG"],"apiNavn":"avslagEosKunKorteUsammenhengendeArbeidsperioder","periodeType":"INGEN_UTBETALING","_createdAt":"2022-11-01T13:42:18Z","_id":"cd67c682-4442-4210-bdac-19463bcb5377","_updatedAt":"2023-09-25T10:42:45Z"},{"_createdAt":"2022-05-23T12:58:54Z","valgbarhet":"TILLEGGSTEKST","bokmaal":[{"_type":"block","style":"normal","_key":"e0a9026fb581","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Den andre forelderen har søkt om å få barnetrygden utbetalt til seg. Vi har kommet fram til at barnetrygden ikke kommer barnet til gode. Vi utbetaler derfor barnetrygden til den andre forelderen.","_key":"7bcdd35062cb0"}]}],"navnISystem":"Tilleggsbegrunnelse utbetaling til annen forelder","kompetanseResultat":["NORGE_ER_PRIMÆRLAND","NORGE_ER_SEKUNDÆRLAND"],"tema":"EØS","hjemlerEOSForordningen883":["68"],"begrunnelsetype":"INNVILGET","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV","IKKE_AKTUELT"],"apiNavn":"innvilgetTilleggsbegrunnelseUtbetalingTilAnnenForelder","hjemler":["2","4","11","12"],"behandlingstema":"EØS","barnetsBostedsland":["NORGE","IKKE_NORGE"],"mappe":["EØS","INNVILGET"],"_rev":"BtltdVb0HP4g4WJfnr4YPo","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Den andre forelderen har søkt om å få barnetrygda utbetalt til seg. Vi har kome fram til at barnetrygda ikke kjem barnet til gode. Vi utbetalar derfor barnetrygda til den andre forelderen.","_key":"708aa1e5addf0"}],"_type":"block","style":"normal","_key":"782ac54c7a63"}],"triggereIBruk":["KOMPETANSE"],"periodeType":"UTBETALING","_id":"cecef583-8b2d-44c2-b8c0-6c77e79537b5","_updatedAt":"2023-09-25T10:15:52Z","visningsnavn":"16. Tilleggsbegrunnelse utbetaling til annen forelder","_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING"},{"behandlingstema":"EØS","tema":"EØS","_type":"begrunnelse","begrunnelsetype":"AVSLAG","_createdAt":"2022-11-01T14:02:55Z","triggereIBruk":["VILKÅRSVURDERING"],"valgbarhet":"STANDARD","eosVilkaar":["LOVLIG_OPPHOLD"],"hjemler":["2","4","5"],"visningsnavn":"11. Ikke student ","_id":"cff26194-180c-4079-a7c0-3c1533042426","navnISystem":"Ikke student ","_rev":"FuD004taptHFqBZyEy9Fd2","mappe":["EØS","AVSLAG"],"periodeType":"INGEN_UTBETALING","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"876bb6f1e4480"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"d17d8d307d16"},{"_type":"span","marks":[],"text":" fordi du ikkje studerar ved ein godkjend utdanningsinstitusjon.","_key":"577d7516e0de"}],"_type":"block","style":"normal","_key":"2bd5ab3faf26","markDefs":[]}],"_updatedAt":"2023-09-25T10:43:36Z","bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn født ","_key":"55d155c07ae50"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"7d3819980f26"},{"text":" fordi du ikke studerer ved en godkjent utdanningsinstitusjon.","_key":"4883c599b286","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"03a801852c9c"}],"apiNavn":"avslagEosIkkeStudent","vedtakResultat":"IKKE_INNVILGET","hjemlerEOSForordningen883":["2","11-16","67"]},{"visningsnavn":"33. Primærland tilleggstekst vedtak før SED","behandlingstema":"EØS","vedtakResultat":"INNVILGET_ELLER_ØKNING","periodeType":"UTBETALING","apiNavn":"innvilgetPrimaerlandTilleggstekstVedtakFoerSed","_type":"begrunnelse","begrunnelsetype":"INNVILGET","tema":"EØS","nynorsk":[{"_key":"0d5fcbfaf37c","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Vedtaket er gjort før vi har fått opplysningar frå ","_key":"38e47a2ec2310"},{"_type":"eosFlettefelt","_key":"d1373bd4eda7","flettefelt":"annenForeldersAktivitetsland"},{"_type":"span","marks":[],"text":". Saka må vurderast på nytt dersom vi får nye opplysningar.","_key":"0bcc70fb7736"}],"_type":"block","style":"normal"}],"_createdAt":"2022-10-10T13:45:58Z","barnetsBostedsland":["NORGE","IKKE_NORGE"],"mappe":["EØS","INNVILGET"],"_rev":"BtltdVb0HP4g4WJfnr4YPo","hjemlerEOSForordningen987":["60"],"_updatedAt":"2023-09-25T10:15:52Z","triggereIBruk":["KOMPETANSE"],"_id":"d17f4bef-a247-43c8-ab9c-7fac9222b592","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"valgbarhet":"TILLEGGSTEKST","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"bokmaal":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Vedtaket er gjort før vi har fått opplysninger fra ","_key":"ac28cf145bb60"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"fe805664657d"},{"_type":"span","marks":[],"text":". Saken må vurderes på nytt dersom vi får nye opplysninger.","_key":"68473aaa3bf0"}],"_type":"block","style":"normal","_key":"80ce574760c2"}],"navnISystem":"Primærland tilleggstekst vedtak før SED"},{"visningsnavn":"9. Primærland barnet flyttet til Norge ","_rev":"BtltdVb0HP4g4WJfnr4YPo","tema":"EØS","nynorsk":[{"style":"normal","_key":"02110357821d","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"2f08efc2900d0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"1167969efdac"},{"_type":"span","marks":[],"text":". Du ","_key":"7e4a42db6485"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"e72619e27bf5","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"d30fe5c24476"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"69d98b37e7d4","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bur saman med deg i Noreg. Den andre forelderen ","_key":"6d4d28a42615"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"7e1599edd8c4","skalHaStorForbokstav":false},{"_key":"ba93ebac644b","_type":"span","marks":[],"text":" i "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"a496b4adbe28"},{"_type":"span","marks":[],"text":". Du får derfor heile barnetrygda frå Noreg.","_key":"8bd93ecebdc8"}],"_type":"block"}],"_id":"dc14a112-c13f-4a5d-be2b-73bbc27a2271","barnetsBostedsland":["NORGE"],"mappe":["EØS","INNVILGET"],"hjemlerEOSForordningen883":["2","11-16","67","68"],"valgbarhet":"STANDARD","_updatedAt":"2023-09-25T10:15:52Z","_createdAt":"2022-05-23T12:59:37Z","navnISystem":"Primærland barnet flyttet til Norge ","hjemler":["2","4","11"],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_type":"begrunnelse","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","apiNavn":"innvilgetPrimarlandBarnetFlyttetTilNorge","periodeType":"UTBETALING","triggereIBruk":["KOMPETANSE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON","INAKTIV"],"bokmaal":[{"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"79a06b61f5e90","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"beb3ead35bad"},{"marks":[],"text":". Du ","_key":"e75378442b42","_type":"span"},{"_type":"valgfeltV2","_key":"cdea22b93d4e","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"}},{"marks":[],"text":". ","_key":"f9f369e93e4b","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"a34a93a5f603","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bor sammen med deg i Norge. Den andre forelderen ","_key":"6f9ce7499273"},{"_type":"valgfeltV2","_key":"f6391b5ed7fc","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"}},{"marks":[],"text":" i ","_key":"d68fbd82b222","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"ba34ad1b3b09"},{"_key":"8bb5b4e7e02e","_type":"span","marks":[],"text":". Du får derfor hele barnetrygden fra Norge."}],"_type":"block","style":"normal","_key":"108ec198ba09","markDefs":[]}]},{"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_rev":"BtltdVb0HP4g4WJfnr4YPo","vedtakResultat":"INNVILGET_ELLER_ØKNING","valgbarhet":"STANDARD","barnetsBostedsland":["NORGE"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON"],"apiNavn":"innvilgetPrimarlandBeggeForeldreJobberINorge","visningsnavn":"4. Primærland begge foreldre jobber i Norge","behandlingstema":"EØS","periodeType":"UTBETALING","_createdAt":"2022-05-23T12:50:38Z","_id":"e041db5a-9ab5-4106-b6e8-471adf769993","mappe":["EØS","INNVILGET"],"_updatedAt":"2023-09-25T10:15:52Z","bokmaal":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"17fd57a697b7"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"2059449b4181"},{"_type":"span","marks":[],"text":". Du ","_key":"3e4a28d2e1d9"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"10703ab7305c","skalHaStorForbokstav":false},{"text":". ","_key":"2772e8d69bd5","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"9e93a8bff142","skalHaStorForbokstav":true},{"marks":[],"text":" bor i ","_key":"5a1dc30dead3","_type":"span"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"2902f69de589"},{"marks":[],"text":". Den andre forelderen ","_key":"a865e8fe9b12","_type":"span"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"dfbc0d3a0534","skalHaStorForbokstav":false},{"_key":"0bfe0683d76e","_type":"span","marks":[],"text":" i "},{"_key":"bdca2f3d601d","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":". Du får derfor hele barnetrygden fra Norge.","_key":"255952243959"}],"_type":"block","style":"normal","_key":"8845ddcc98b9","markDefs":[]}],"hjemler":["2","4","11"],"_type":"begrunnelse","triggereIBruk":["KOMPETANSE"],"navnISystem":"Primærland begge foreldre jobber i Norge","hjemlerEOSForordningen883":["2","11-16","67","68"],"begrunnelsetype":"INNVILGET","tema":"EØS","nynorsk":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"8d9bdb0987740","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"fa0f2b0d4ef2"},{"_type":"span","marks":[],"text":". Du ","_key":"d6aaa19197c6"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"5210d8fb166e","skalHaStorForbokstav":false},{"text":". ","_key":"0f6c2e46c145","_type":"span","marks":[]},{"_key":"9851e0943d50","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_key":"4be695e2f85b","_type":"span","marks":[],"text":" bur i "},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"18aa06d29da2"},{"marks":[],"text":". Den andre forelderen ","_key":"8f8fbff483ed","_type":"span"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"e8ba4b03717c","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"f00b097e0cba"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"33a535b90e41"},{"_key":"f7d26c166743","_type":"span","marks":[],"text":". Du får derfor heile barnetrygda frå Noreg."}],"_type":"block","style":"normal","_key":"f53dbb4f3923"}]},{"_rev":"BtltdVb0HP4g4WJfnr4YPo","vedtakResultat":"INNVILGET_ELLER_ØKNING","hjemlerEOSForordningen883":["2","11-16","67","68"],"triggereIBruk":["KOMPETANSE"],"hjemler":["2","4","11"],"kompetanseResultat":["TO_PRIMÆRLAND"],"periodeType":"UTBETALING","begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","_id":"e0ed4c3d-1ea5-4da5-9682-9b2444f5fae7","mappe":["EØS","INNVILGET"],"annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","MOTTAR_PENSJON"],"navnISystem":"Primærland to arbeidsland annet land utbetaler","hjemlerEOSForordningen987":["58"],"visningsnavn":"13. Primærland to arbeidsland annet land utbetaler","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du har rett til barnetrygd etter EØS-reglane for barn fødd ","_key":"388674e81b180"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"07dad7e21b35"},{"_key":"93ed0b4dd32b","_type":"span","marks":[],"text":". Du "},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"6a4e5a2180c9","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"e8b25498f3e9"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"9b1be056c584","skalHaStorForbokstav":true},{"text":" bur i ","_key":"ff052a3f333d","_type":"span","marks":[]},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"f09ec7a1d0aa"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"be3204e48e7d"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"e796e20b6140","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"49c4b60a7eec"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"3a07589de835"},{"_type":"span","marks":[],"text":". Du og den andre forelderen har rett til barnetrygd frå Noreg og ","_key":"e87ffbc28297"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"3fc5f032523d"},{"_key":"d975a020d522","_type":"span","marks":[],"text":" fordi de jobbar i andre land enn der barnet bur. Landet med den høgaste barnetrygda skal utbetale. Barnetrygda i "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"a359b4de38b2"},{"marks":[],"text":" er høgare enn barnetrygda i Noreg. Familien får derfor heile barnetrygda frå ","_key":"604f37aaf87c","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"d65321ec9236"},{"marks":[],"text":".","_key":"6bcac62081e2","_type":"span"}],"_type":"block","style":"normal","_key":"f5cf7ce98a86","markDefs":[]}],"_createdAt":"2022-05-23T12:51:58Z","barnetsBostedsland":["IKKE_NORGE"],"apiNavn":"innvilgetPrimarlandToArbeidslandAnnetLandUtbetaler","_type":"begrunnelse","tema":"EØS","_updatedAt":"2023-09-25T10:15:52Z","bokmaal":[{"_key":"32745ef5ca4c","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du har rett til barnetrygd etter EØS-reglene for barn født ","_key":"b324caa1df1b0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"410eab0a0c52"},{"text":". Du ","_key":"97383fccbafb","_type":"span","marks":[]},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"2c2414eb29c3","skalHaStorForbokstav":false},{"marks":[],"text":". ","_key":"5f58e403882b","_type":"span"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"55b7f9e7a058","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bor i ","_key":"6badf760fddd"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"cbbaed8b2b20"},{"_key":"774b20557826","_type":"span","marks":[],"text":". Den andre forelderen "},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"758d873ccfc3","skalHaStorForbokstav":false},{"marks":[],"text":" i ","_key":"ceb2c16719a2","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"c8f9d5485933"},{"marks":[],"text":". Du og den andre forelderen har rett til barnetrygd fra Norge og ","_key":"e401d1662774","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"cadd744bf836"},{"_type":"span","marks":[],"text":" fordi dere jobber i andre land enn der ","_key":"3b36244b69e6"},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"61eac5318d50"},{"_key":"df99117f4d45","_type":"span","marks":[],"text":" bor. Landet med den høyeste barnetrygden skal utbetale. Barnetrygden i "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"ae1d63298c14"},{"_type":"span","marks":[],"text":" er høyere enn barnetrygden i Norge. Familien får derfor hele barnetrygden fra ","_key":"beeaac7f847b"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"6816e92435e5"},{"marks":[],"text":".","_key":"4684a5f11ce7","_type":"span"}],"_type":"block","style":"normal"}],"behandlingstema":"EØS"},{"barnetsBostedsland":["NORGE","IKKE_NORGE"],"_updatedAt":"2023-09-25T10:15:52Z","bokmaal":[{"_type":"block","style":"normal","_key":"2a15581e691f","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygden i ","_key":"ba5cd74506e30"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"9526174c4d7b"},{"text":" er høyere enn norsk barnetrygd. Derfor får du ikke utbetalt barnetrygd fra Norge.","_key":"6a03a1645b66","_type":"span","marks":[]}]}],"_rev":"BtltdVb0HP4g4WJfnr4YPo","vedtakResultat":"INNVILGET_ELLER_ØKNING","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"UTBETALING","navnISystem":"Tilleggstekst nullutbetaling","hjemler":["11"],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"mappe":["EØS","INNVILGET"],"tema":"EØS","triggereIBruk":["KOMPETANSE"],"valgbarhet":"TILLEGGSTEKST","_id":"e5dea43e-69b0-4b0d-ad53-c156513b47e2","apiNavn":"innvilgetTilleggstekstNullutbetaling","visningsnavn":"22. Tilleggstekst nullutbetaling","nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Barnetrygda i ","_key":"814d68085d1e0"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"8dcc13440c17"},{"text":" er høgare enn norsk barnetrygd. Derfor får du ikkje utbetalt barnetrygd frå Noreg.","_key":"e62e6f07e0c6","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"d33759d7c542","markDefs":[]}],"_type":"begrunnelse","begrunnelsetype":"INNVILGET","_createdAt":"2022-08-26T11:34:18Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN","FORSIKRET_I_BOSTEDSLAND","IKKE_AKTUELT"]},{"visningsnavn":"2. Ikke bosatt i EØS-land","valgbarhet":"STANDARD","mappe":["EØS","AVSLAG"],"_updatedAt":"2023-09-25T10:42:30Z","bokmaal":[{"markDefs":[],"children":[{"text":"Barnetrygd for barn født ","_key":"693c518e88420","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"0bdee15986f5"},{"text":" fordi ","_key":"05287beff143","_type":"span","marks":[]},{"skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"},"_type":"valgfeltV2","_key":"ef27edb2277c"},{"_key":"c4cbac8d1722","_type":"span","marks":[],"text":" ikke er bosatt i et EØS-land."}],"_type":"block","style":"normal","_key":"910d48f55a07"}],"navnISystem":"Ikke bosatt i EØS-land","hjemler":["2","4","5"],"periodeType":"INGEN_UTBETALING","_createdAt":"2022-11-01T12:47:01Z","apiNavn":"avslagEosIkkeBosattIEosLand","behandlingstema":"EØS","_type":"begrunnelse","begrunnelsetype":"AVSLAG","tema":"EØS","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"a3c247180ee90"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"f344ba68ddf8"},{"_type":"span","marks":[],"text":" fordi ","_key":"33f5e3e84149"},{"_type":"valgfeltV2","_key":"8843069de798","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"49509c87-0882-429c-9604-f4fbfc5876ca","_type":"reference"}},{"text":" ikkje er busett i eit EØS-land.","_key":"6a1502331df6","_type":"span","marks":[]}],"_type":"block","style":"normal","_key":"4e755ce5409c"}],"eosVilkaar":["BOSATT_I_RIKET"],"vedtakResultat":"IKKE_INNVILGET","hjemlerEOSForordningen883":["2"],"triggereIBruk":["VILKÅRSVURDERING"],"_id":"e99196d6-b704-4769-bb33-39057683ed42","_rev":"h26rAhFEYSUtDGXJE0vE8v"},{"visningsnavn":"12. Ikke ansvar for barn","hjemlerEOSForordningen883":["2","11-16","67"],"periodeType":"INGEN_UTBETALING","tema":"EØS","triggereIBruk":["VILKÅRSVURDERING"],"valgbarhet":"STANDARD","navnISystem":"Ikke ansvar for barn","apiNavn":"avslagEosIkkeAnsvarForBarn","mappe":["EØS","AVSLAG"],"_updatedAt":"2023-09-25T10:43:39Z","begrunnelsetype":"AVSLAG","_createdAt":"2022-11-01T14:14:49Z","behandlingstema":"EØS","bokmaal":[{"style":"normal","_key":"bb2d8d6d01aa","markDefs":[],"children":[{"marks":[],"text":"Barnetrygd for barn født ","_key":"a3d6504481bc0","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"c89e507f2e02"},{"_type":"span","marks":[],"text":" fordi du ikke har ansvar for ","_key":"13fe6867fc31"},{"_type":"valgfeltV2","_key":"439759fe1d93","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_key":"f98fcf070201","_type":"span","marks":[],"text":""}],"_type":"block"}],"eosVilkaar":["BOR_MED_SØKER"],"hjemler":["2","4","5"],"vedtakResultat":"IKKE_INNVILGET","nynorsk":[{"style":"normal","_key":"6d8b30f1d7dc","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"c91cf96133d90"},{"_type":"eosFlettefelt","_key":"4d0228e9a0d3","flettefelt":"barnasFodselsdatoer"},{"_type":"span","marks":[],"text":" fordi du ikkje har ansvar for ","_key":"057fedcb9cb7"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"af5ab1ce639b","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":".","_key":"39a3b901d296"}],"_type":"block"}],"_id":"ec478168-1fdd-4016-b31d-74369f5397c6","_rev":"BtltdVb0HP4g4WJfnr611J","_type":"begrunnelse"},{"vedtakResultat":"IKKE_INNVILGET","begrunnelsetype":"AVSLAG","nynorsk":[{"style":"normal","_key":"c617ea852c8c","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Barnetrygd for barn fødd ","_key":"a71f31bfd42e0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"d2e0b44d2347"},{"_type":"span","marks":[],"text":" fordi du ikkje får pengar frå NAV som erstattar løn.","_key":"0ab6d378206b"}],"_type":"block"}],"_createdAt":"2022-11-01T13:44:56Z","valgbarhet":"STANDARD","navnISystem":"Ikke penger fra NAV som erstatter lønn","eosVilkaar":["LOVLIG_OPPHOLD"],"_rev":"FuD004taptHFqBZyEy9CJc","_id":"f3c70132-97c9-4040-b1ed-4f9d655cd83b","mappe":["EØS","AVSLAG"],"tema":"EØS","triggereIBruk":["VILKÅRSVURDERING"],"apiNavn":"avslagEosIkkePengerFraNavSomErstatterLoenn","hjemler":["2","4","5"],"hjemlerEOSForordningen883":["2","11-16","67"],"behandlingstema":"EØS","periodeType":"INGEN_UTBETALING","bokmaal":[{"markDefs":[],"children":[{"text":"Barnetrygd for barn født ","_key":"29ffaffabc710","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"21a9792f0118"},{"_type":"span","marks":[],"text":" fordi du ikke får penger fra NAV som erstatter lønn.","_key":"b8a38112e0a0"}],"_type":"block","style":"normal","_key":"65b823bfd3ca"}],"visningsnavn":"7. Ikke penger fra NAV som erstatter lønn","_type":"begrunnelse","_updatedAt":"2023-09-25T10:42:50Z"},{"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"ba5982e889760"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"942e514a386f"},{"_key":"7f01d2967b32","_type":"span","marks":[],"text":". Du "},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"05b6ed54eabe","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"aee52c5edec5"},{"_type":"valgfeltV2","_key":"ddfa5f662d17","skalHaStorForbokstav":true,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"}},{"_type":"span","marks":[],"text":" bur i ","_key":"9c480135e808"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"b2159bef6241"},{"text":". Du har ansvar for ","_key":"f84785ed406d","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"db041ef1f1f8","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Du får derfor heile barnetrygda frå Noreg.","_key":"3a2c3d881b72"}],"_type":"block","style":"normal","_key":"df63c5e89488","markDefs":[]}],"barnetsBostedsland":["IKKE_NORGE"],"behandlingstema":"EØS","_type":"begrunnelse","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"periodeType":"UTBETALING","tema":"EØS","_createdAt":"2022-05-23T13:01:14Z","_id":"f5596fdc-1c28-4309-9aa8-efe5ec5030d5","_updatedAt":"2023-09-25T10:15:52Z","navnISystem":"Primærland særkullsbarn/andre barn","hjemler":["2","4","11"],"annenForeldersAktivitet":["MOTTAR_PENSJON","IKKE_AKTUELT"],"begrunnelsetype":"INNVILGET","valgbarhet":"STANDARD","mappe":["EØS","INNVILGET"],"bokmaal":[{"_type":"block","style":"normal","_key":"71f23b9e7be5","markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"20cc095016ec0","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"70e5c2a3921f"},{"text":". Du ","_key":"535ce914a928","_type":"span","marks":[]},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"e0e9395c4242","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"ab680bd0a53f"},{"_type":"valgfeltV2","_key":"b7e8b9044c2a","skalHaStorForbokstav":true,"valgReferanse":{"_type":"reference","_ref":"df8dc282-2637-4047-a656-8527205dc364"}},{"_type":"span","marks":[],"text":" bor i ","_key":"b1a8cea191a2"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"8c5083b21816"},{"text":". Du har ansvar for ","_key":"b56ebea77120","_type":"span","marks":[]},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"514e46247ab6","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Du får derfor hele barnetrygden fra Norge.","_key":"29e406448bea"}]}],"vedtakResultat":"INNVILGET_ELLER_ØKNING","hjemlerEOSForordningen883":["2","11-16","67","68"],"_rev":"BtltdVb0HP4g4WJfnr4YPo","triggereIBruk":["KOMPETANSE"],"apiNavn":"innvilgetPrimarlandSarkullsbarnAndreBarn","visningsnavn":"10. Primærland særkullsbarn/andre barn"},{"_rev":"BtltdVb0HP4g4WJfnr4YPo","tema":"EØS","_id":"f5aa05e5-7e6d-43e0-8b43-5ffae47d2532","_updatedAt":"2023-09-25T10:15:52Z","nynorsk":[{"markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"03643c4b09ad0"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"93d0e83b4f8b"},{"_key":"7bd8bc4d1a5f","_type":"span","marks":[],"text":". Du "},{"valgReferanse":{"_type":"reference","_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb"},"_type":"valgfeltV2","_key":"4e487149f04f","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bur i Noreg. Du har ansvar for barnet åleine. Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Etter avtalen har Storbritannia hovudansvaret for utbetaling av barnetrygd. Noreg utbetalar derfor forskjellen mellom barnetrygda i Storbritannia og norsk barnetrygd.","_key":"e23e9d869e70"}],"_type":"block","style":"normal","_key":"70e7cb8e7838"}],"barnetsBostedsland":["NORGE"],"mappe":["EØS","INNVILGET"],"annenForeldersAktivitet":["IKKE_AKTUELT"],"hjemler":["2","4","11"],"visningsnavn":"25. Sekundærland UK aleneansvar","behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_SEKUNDÆRLAND"],"valgbarhet":"STANDARD","bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"7641a086b7610","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"80e2fe2be827"},{"_type":"span","marks":[],"text":". Du ","_key":"bb36ea0fd12f"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"9d06897c4a84","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". Barnet bor i Norge. Du har ansvar for barnet alene. Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Etter avtalen har Storbritannia hovedansvaret for utbetaling av barnetrygd. Norge utbetaler derfor forskjellen mellom barnetrygden i Storbritannia og norsk barnetrygd.","_key":"b42713c9fae0"}],"_type":"block","style":"normal","_key":"25f7d5b1d251"}],"navnISystem":"Sekundærland UK aleneansvar","vedtakResultat":"INNVILGET_ELLER_ØKNING","hjemlerSeperasjonsavtalenStorbritannina":["29"],"begrunnelsetype":"INNVILGET","_createdAt":"2022-08-26T12:15:52Z","triggereIBruk":["KOMPETANSE"],"apiNavn":"innvilgetSekundaerlandUkAleneansvar","_type":"begrunnelse","hjemlerEOSForordningen883":["2","11-16","67","68"],"periodeType":"UTBETALING"},{"_rev":"BtltdVb0HP4g4WJfnr4YPo","visningsnavn":"15. Primærland UK to arbeidsland annet land utbetaler","vedtakResultat":"INNVILGET_ELLER_ØKNING","mappe":["EØS","INNVILGET"],"navnISystem":"Primærland UK to arbeidsland annet land utbetaler","barnetsBostedsland":["IKKE_NORGE"],"bokmaal":[{"markDefs":[],"children":[{"marks":[],"text":"Du får barnetrygd for barn født ","_key":"51e6a58fa30e0","_type":"span"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"aa6e3ccc9068"},{"_type":"span","marks":[],"text":". Du ","_key":"89ac39d42a40"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"b4bf53eac228","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"8cf8abf10922"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"1428edca3aae","skalHaStorForbokstav":true},{"text":" bor i ","_key":"42b69af7be29","_type":"span","marks":[]},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"958cb473eda4"},{"_type":"span","marks":[],"text":". Den andre forelderen ","_key":"17b49901e614"},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"4f761a542c24","skalHaStorForbokstav":false},{"text":" i ","_key":"19fa20d49885","_type":"span","marks":[]},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"df1898be12b2"},{"_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Norge gjelder for familien. Du og den andre forelderen har rett til barnetrygd fra Norge og ","_key":"51f2a5603912"},{"_key":"2dec750f5d29","flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt"},{"_type":"span","marks":[],"text":" fordi dere jobber i andre land enn der ","_key":"97bad80316fa"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"2400bf41f87d","skalHaStorForbokstav":false},{"_key":"d64be4b26c22","_type":"span","marks":[],"text":" bor. Landet med den høyeste barnetrygden skal utbetale. "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"25a88b80b35d"},{"marks":[],"text":" er høyere enn barnetrygden i Norge. Du får derfor hele barnetrygden fra ","_key":"00ffa69f700c","_type":"span"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"1918662ede7d"},{"_type":"span","marks":[],"text":".","_key":"0ece5969c1f3"}],"_type":"block","style":"normal","_key":"42fb71782853"}],"behandlingstema":"EØS","kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"tema":"EØS","triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","periodeType":"UTBETALING","hjemlerEOSForordningen883":["2","11-16","67","68"],"nynorsk":[{"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd for barn fødd ","_key":"272dcae7f2110"},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"dc8975e71e9f"},{"marks":[],"text":". Du ","_key":"b8dab93a7d4c","_type":"span"},{"valgReferanse":{"_type":"reference","_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb"},"_type":"valgfeltV2","_key":"1efc18cbc14b","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"3a4b35cef874"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"ffe330017368","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bur i ","_key":"3641a1771b31"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"44d7c14e6c1f"},{"_key":"a45077dbd2c9","_type":"span","marks":[],"text":". Den andre forelderen "},{"valgReferanse":{"_ref":"44a17a41-a760-44f7-8d7d-a3cf1676b85a","_type":"reference"},"_type":"valgfeltV2","_key":"1bfd0b917431","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" i ","_key":"318694b34cb7"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"16fd5a9eac33"},{"_key":"34f6a6114f32","_type":"span","marks":[],"text":". Separasjonsavtalen mellom Storbritannia og Noreg gjeld for familien. Du og den andre forelderen har rett til barnetrygd frå Noreg og "},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"a27b37277a59"},{"_type":"span","marks":[],"text":" fordi de jobbar i andre land enn der ","_key":"7e140688a8a9"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"6ca8fdaf7136","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":" bur. Landet med den høgaste barnetrygda skal utbetale. ","_key":"219da02b6a37"},{"_type":"eosFlettefelt","_key":"83f4375a7b2e","flettefelt":"annenForeldersAktivitetsland"},{"_type":"span","marks":[],"text":" er høgare enn barnetrygda i Noreg. Du får derfor heile barnetrygda frå ","_key":"c91d9f5b9b95"},{"flettefelt":"annenForeldersAktivitetsland","_type":"eosFlettefelt","_key":"d2343042df27"},{"_type":"span","marks":[],"text":".","_key":"d61fb743e3ba"}],"_type":"block","style":"normal","_key":"2f52b0aad22e","markDefs":[]}],"_updatedAt":"2023-09-25T10:15:52Z","annenForeldersAktivitet":["I_ARBEID","MOTTAR_PENSJON"],"apiNavn":"innvilgetPrimarlandUkToArbeidslandAnnetLandUtbetaler","_type":"begrunnelse","begrunnelsetype":"INNVILGET","hjemler":["2","4","11"],"_id":"f5dc64d6-8962-46a9-bc82-342eb604ec49","_createdAt":"2022-05-23T12:56:32Z","hjemlerEOSForordningen987":["58"],"hjemlerSeperasjonsavtalenStorbritannina":["29"]},{"tema":"EØS","barnetsBostedsland":["NORGE","IKKE_NORGE"],"_updatedAt":"2023-09-25T10:15:52Z","visningsnavn":"2. Primærland aleneansvar","resultat":"INNVILGELSE","vedtakResultat":"INNVILGET_ELLER_ØKNING","begrunnelsetype":"INNVILGET","triggereIBruk":["KOMPETANSE"],"valgbarhet":"STANDARD","annenForeldersAktivitet":["MOTTAR_PENSJON","IKKE_AKTUELT"],"behandlingstema":"EØS","_rev":"BtltdVb0HP4g4WJfnr4YPo","_type":"begrunnelse","periodeType":"UTBETALING","nynorsk":[{"style":"normal","_key":"fb9828316011","markDefs":[],"children":[{"_type":"span","marks":[],"text":"Du får barnetrygd etter EØS-reglane for barn fødd ","_key":"6355129c1f090"},{"_type":"eosFlettefelt","_key":"407b32120d24","flettefelt":"barnasFodselsdatoer"},{"marks":[],"text":". Du ","_key":"d2faf6aa4966","_type":"span"},{"valgReferanse":{"_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb","_type":"reference"},"_type":"valgfeltV2","_key":"0cb3aaac7d7d","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"ad42c76966d2"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"55bec7e3e7a8","skalHaStorForbokstav":true},{"text":" bur i ","_key":"d37a447eea1a","_type":"span","marks":[]},{"_type":"eosFlettefelt","_key":"28a3aebedd22","flettefelt":"barnetsBostedsland"},{"_type":"span","marks":[],"text":". Du har ansvar for ","_key":"95ef387aed85"},{"_key":"016c944f81f7","skalHaStorForbokstav":false,"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2"},{"_key":"427ae7681f7b","_type":"span","marks":[],"text":" åleine. Du får derfor heile barnetryga frå Noreg."}],"_type":"block"}],"apiNavn":"innvilgetPrimarlandAleneansvar","hjemler":["2","4","11"],"kompetanseResultat":["NORGE_ER_PRIMÆRLAND"],"_id":"fab3f1df-4168-481f-ab75-c33615da0189","mappe":["EØS","INNVILGET"],"bokmaal":[{"markDefs":[],"children":[{"text":"Du får barnetrygd etter EØS-reglene for barn født ","_key":"85a1704cf6650","_type":"span","marks":[]},{"flettefelt":"barnasFodselsdatoer","_type":"eosFlettefelt","_key":"76f3f134ad4d"},{"_type":"span","marks":[],"text":". Du ","_key":"8235aa2186e0"},{"valgReferanse":{"_type":"reference","_ref":"c76d7bb2-2871-492f-8c13-95c31d2dd0cb"},"_type":"valgfeltV2","_key":"b4e4f599b1fe","skalHaStorForbokstav":false},{"_type":"span","marks":[],"text":". ","_key":"88d048316477"},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"9cbd3107031c","skalHaStorForbokstav":true},{"_type":"span","marks":[],"text":" bor i ","_key":"b1a5c4a4fe69"},{"flettefelt":"barnetsBostedsland","_type":"eosFlettefelt","_key":"3a433a70c0f6"},{"_key":"3b7d45009666","_type":"span","marks":[],"text":". Du har ansvar for "},{"valgReferanse":{"_ref":"df8dc282-2637-4047-a656-8527205dc364","_type":"reference"},"_type":"valgfeltV2","_key":"f92683a2c3f2","skalHaStorForbokstav":false},{"marks":[],"text":" alene. Du får derfor hele barnetrygden fra Norge.","_key":"d8c2b32bbc4b","_type":"span"}],"_type":"block","style":"normal","_key":"1b794ce79f5d"}],"navnISystem":"Primærland aleneansvar","hjemlerEOSForordningen883":["2","11-16","67","68"],"_createdAt":"2022-05-23T12:45:41Z"}] \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/revurdering_med_opph\303\270r.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/revurdering_med_opph\303\270r.feature" new file mode 100644 index 000000000..d0bcc0b68 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/revurdering_med_opph\303\270r.feature" @@ -0,0 +1,53 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for revurdering med opphør + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 1 | 1 | | + | 2 | 1 | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 29.10.1984 | + | 1 | 4567 | BARN | 22.08.2022 | + | 2 | 1234 | SØKER | 29.10.1984 | + | 2 | 4567 | BARN | 22.08.2022 | + + Scenario: Skal håndtere opphør på tvers av behandlinger + Og lag personresultater for begrunnelse for behandling 1 + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 29.10.1984 | | OPPFYLT | Nei | + | 1234 | UTVIDET_BARNETRYGD | | 07.09.2022 | | OPPFYLT | Nei | + + | 4567 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP,BOSATT_I_RIKET | | 22.08.2022 | | OPPFYLT | Nei | + | 4567 | BOR_MED_SØKER | | 22.06.2023 | | OPPFYLT | Nei | + | 4567 | UNDER_18_ÅR | | 22.08.2022 | 21.08.2040 | OPPFYLT | Nei | + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 29.10.1984 | | OPPFYLT | Nei | + | 1234 | UTVIDET_BARNETRYGD | | 07.09.2022 | | OPPFYLT | Nei | + + | 4567 | GIFT_PARTNERSKAP,BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 22.08.2022 | | OPPFYLT | Nei | + | 4567 | BOR_MED_SØKER | | 22.06.2023 | 15.08.2023 | OPPFYLT | Nei | + | 4567 | UNDER_18_ÅR | | 22.08.2022 | 21.08.2040 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 4567 | 1 | 01.07.2023 | 01.07.2028 | 1766 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.08.2028 | 01.07.2040 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.07.2023 | 01.07.2040 | 2516 | UTVIDET_BARNETRYGD | 100 | + | 4567 | 2 | 01.07.2023 | 01.08.2023 | 1766 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 2 | 01.07.2023 | 01.08.2023 | 2516 | UTVIDET_BARNETRYGD | 100 | + + Når begrunnelsetekster genereres for behandling 2 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.09.2023 | | OPPHØR | | OPPHØR_BARN_FLYTTET_FRA_SØKER | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/rolle.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/rolle.feature new file mode 100644 index 000000000..bfff6a627 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/rolle.feature @@ -0,0 +1,88 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for rolle ved endring av vilkår + + Bakgrunn: + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + | 2 | BARN_ENSLIG_MINDREÅRIG | + + Gitt følgende behandling + | BehandlingId | FagsakId | + | 1 | 1 | + | 2 | 2 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + | 2 | 4567 | BARN | 13.04.2020 | + + Scenario: Skal få med begrunnelse som kun gjelder søker når søker sine vilkår endrer seg + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Utdypende vilkår | + | 1234 | LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 1234 | BOSATT_I_RIKET | 11.01.1970 | 10.05.2020 | Oppfylt | VURDERING_ANNET_GRUNNLAG | + + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 31.05.2020 | UTBETALING | | | + | 01.06.2020 | | OPPHØR | OPPHØR_UGYLDIG_KONTONUMMER | | + + + Scenario: Skal ikke få med begrunnelse som kun gjelder søker når barn sine vilkår endrer seg + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Utdypende vilkår | + | 1234 | LOVLIG_OPPHOLD, BOSATT_I_RIKET | 11.01.1970 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | | + | 3456 | BOSATT_I_RIKET | 13.04.2020 | 10.05.2020 | Oppfylt | OMFATTET_AV_NORSK_LOVGIVNING | + | 3456 | BOSATT_I_RIKET | 11.05.2020 | | Oppfylt | | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 31.05.2020 | UTBETALING | | FORTSATT_INNVILGET_SØKER_BOSATT_I_RIKET | + | 01.06.2020 | 31.03.2038 | UTBETALING | | FORTSATT_INNVILGET_SØKER_BOSATT_I_RIKET | + | 01.04.2038 | | OPPHØR | | | + + Scenario: Skal få med begrunnelse som kun gjelder søker når barn sine vilkår endrer seg om barn er søker + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Utdypende vilkår | + | 4567 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + | 4567 | GIFT_PARTNERSKAP, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | | + | 4567 | BOSATT_I_RIKET | 13.04.2020 | 10.05.2022 | Oppfylt | VURDERING_ANNET_GRUNNLAG | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 4567 | 01.05.2020 | 31.05.2022 | 1354 | 2 | + + Når begrunnelsetekster genereres for behandling 2 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 31.05.2022 | UTBETALING | | | + | 01.06.2022 | | OPPHØR | OPPHØR_UGYLDIG_KONTONUMMER | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/tema.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/tema.feature new file mode 100644 index 000000000..b081910a8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/tema.feature @@ -0,0 +1,217 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for behandlingstema + + Bakgrunn: + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 1 | 1 | | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 02.01.1985 | + | 1 | 4567 | BARN | 07.09.2019 | + + Scenario: Man skal ikke få nasjonale begrunnelser dersom vedtaksperiode overlapper med eøs perioder + Og følgende dagens dato 2023-09-13 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD | | 02.01.1985 | | OPPFYLT | Nei | + | 1234 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 02.01.1985 | | OPPFYLT | Nei | + + | 4567 | UNDER_18_ÅR | | 07.09.2019 | 06.09.2037 | OPPFYLT | Nei | + | 4567 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP | | 07.09.2019 | | OPPFYLT | Nei | + | 4567 | BOSATT_I_RIKET | BARN_BOR_I_NORGE | 07.09.2019 | | OPPFYLT | Nei | + | 4567 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 07.09.2019 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 4567 | 1 | 01.10.2019 | 31.08.2020 | 1054 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.09.2020 | 31.08.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.09.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.01.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.07.2023 | 31.08.2025 | 1766 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.09.2025 | 31.08.2037 | 1310 | ORDINÆR_BARNETRYGD | 100 | + + Og med kompetanser for begrunnelse + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland | + | 4567 | 01.10.2019 | | NORGE_ER_PRIMÆRLAND | 1 | ARBEIDER | I_ARBEID | NO | SE | SE | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk Inkluderte Begrunnelser | Inkluderte Begrunnelser | Regelverk Ekskluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.10.2019 | 31.08.2020 | UTBETALING | EØS_FORORDNINGEN | INNVILGET_PRIMÆRLAND_BARNETRYGD_ALLEREDE_UTBETALT | NASJONALE_REGLER | INNVILGET_NYFØDT_BARN_FØRSTE | + | 01.09.2020 | 31.08.2021 | UTBETALING | | | | | + | 01.09.2021 | 31.12.2021 | UTBETALING | | | | | + | 01.01.2022 | 28.02.2023 | UTBETALING | | | | | + | 01.03.2023 | 30.06.2023 | UTBETALING | | | | | + | 01.07.2023 | 31.08.2025 | UTBETALING | | | | | + | 01.09.2025 | 31.08.2037 | UTBETALING | | | | | + | 01.09.2037 | | OPPHØR | | | | | + + Scenario: Man skal ikke få eøs begrunnelser dersom vedtaksperiode ikke overlapper med nasjonale perioder + Og følgende dagens dato 2023-09-13 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD | | 02.01.1985 | | OPPFYLT | Nei | + | 1234 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 02.01.1985 | | OPPFYLT | Nei | + + | 4567 | UNDER_18_ÅR | | 07.09.2019 | 06.09.2037 | OPPFYLT | Nei | + | 4567 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP | | 07.09.2019 | | OPPFYLT | Nei | + | 4567 | BOSATT_I_RIKET | BARN_BOR_I_NORGE | 07.09.2019 | | OPPFYLT | Nei | + | 4567 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 07.09.2019 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 4567 | 1 | 01.10.2019 | 31.08.2020 | 1054 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.09.2020 | 31.08.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.09.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.01.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.07.2023 | 31.08.2025 | 1766 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.09.2025 | 31.08.2037 | 1310 | ORDINÆR_BARNETRYGD | 100 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk Inkluderte Begrunnelser | Inkluderte Begrunnelser | Regelverk Ekskluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.10.2019 | 31.08.2020 | UTBETALING | NASJONALE_REGLER | INNVILGET_BOSATT_I_RIKTET_LOVLIG_OPPHOLD | EØS_FORORDNINGEN | INNVILGET_PRIMÆRLAND_BARNETRYGD_ALLEREDE_UTBETALT | + | 01.09.2020 | 31.08.2021 | UTBETALING | | | | | + | 01.09.2021 | 31.12.2021 | UTBETALING | | | | | + | 01.01.2022 | 28.02.2023 | UTBETALING | | | | | + | 01.03.2023 | 30.06.2023 | UTBETALING | | | | | + | 01.07.2023 | 31.08.2025 | UTBETALING | | | | | + | 01.09.2025 | 31.08.2037 | UTBETALING | | | | | + | 01.09.2037 | | OPPHØR | | | | | + + Scenario: Søker skal ikke ha noe nasjonal begrunnelser etter vilkår dersom vilkårene er vurdert etter eøs forordningen + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | DELVIS_INNVILGET | SØKNAD | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 16.07.1985 | + | 1 | 4567 | BARN | 18.06.2019 | + | 1 | 5678 | BARN | 20.12.2014 | + + Og følgende dagens dato 28.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | Vurderes etter | + | 4567 | UNDER_18_ÅR | | 18.06.2019 | 17.06.2037 | OPPFYLT | Nei | | + | 4567 | GIFT_PARTNERSKAP | | 18.06.2019 | | OPPFYLT | Nei | | + | 4567 | LOVLIG_OPPHOLD | | 18.05.2022 | | OPPFYLT | Nei | | + | 4567 | BOSATT_I_RIKET | BARN_BOR_I_NORGE | 18.05.2022 | | OPPFYLT | Nei | | + | 4567 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 18.05.2022 | | OPPFYLT | Nei | | + + | 1234 | LOVLIG_OPPHOLD | | 18.05.2022 | | OPPFYLT | Nei | EØS_FORORDNINGEN | + | 1234 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 18.05.2022 | | OPPFYLT | Nei | EØS_FORORDNINGEN | + + | 5678 | GIFT_PARTNERSKAP | | 20.12.2014 | | OPPFYLT | Nei | | + | 5678 | UNDER_18_ÅR | | 20.12.2014 | 19.12.2032 | OPPFYLT | Nei | | + | 5678 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 18.05.2022 | | OPPFYLT | Nei | | + | 5678 | BOSATT_I_RIKET | BARN_BOR_I_NORGE | 18.05.2022 | | OPPFYLT | Nei | | + | 5678 | LOVLIG_OPPHOLD | | 18.05.2022 | | OPPFYLT | Nei | | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 4567 | 1 | 01.06.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 4567 | 1 | 01.03.2023 | 31.08.2023 | 0 | ORDINÆR_BARNETRYGD | 0 | 1723 | + | 4567 | 1 | 01.09.2023 | 31.05.2025 | 1766 | ORDINÆR_BARNETRYGD | 100 | 1766 | + | 4567 | 1 | 01.06.2025 | 31.05.2037 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 5678 | 1 | 01.06.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 5678 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 5678 | 1 | 01.07.2023 | 30.11.2032 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + Og med endrede utbetalinger for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent | + | 4567 | 1 | 01.03.2023 | 31.08.2023 | ALLEREDE_UTBETALT | 0 | + + Og med kompetanser for begrunnelse + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland | + | 5678, 4567 | 01.09.2023 | | NORGE_ER_PRIMÆRLAND | 1 | ARBEIDER | I_ARBEID | NO | BE | BE | + | 5678 | 01.03.2023 | 31.08.2023 | NORGE_ER_PRIMÆRLAND | 1 | ARBEIDER | I_ARBEID | NO | BE | BE | + | 5678, 4567 | 01.06.2022 | 28.02.2023 | NORGE_ER_PRIMÆRLAND | 1 | ARBEIDER | I_ARBEID | NO | BE | BE | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk Inkluderte Begrunnelser | Inkluderte Begrunnelser | Regelverk Ekskluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.06.2022 | 28.02.2023 | UTBETALING | EØS_FORORDNINGEN | INNVILGET_PRIMÆRLAND_UK_OG_UTLAND_STANDARD | NASJONALE_REGLER | INNVILGET_EØS_BORGER_EKTEFELLE_JOBBER | + | 01.03.2023 | 30.06.2023 | UTBETALING | | | | | + | 01.07.2023 | 31.08.2023 | UTBETALING | | | | | + | 01.09.2023 | 31.05.2025 | UTBETALING | | | | | + | 01.06.2025 | 30.11.2032 | UTBETALING | | | | | + | 01.12.2032 | 31.05.2037 | UTBETALING | | | | | + | 01.06.2037 | | OPPHØR | | | | | + + + Scenario: Man skal kunne få begrunnelsetekster med tema Felles uavhengig om det er EØS eller Nasjonal + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET | SØKNAD | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 30.04.1994 | + | 1 | 4567 | BARN | 29.04.2015 | + + + Og følgende dagens dato 03.10.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 4567 | UNDER_18_ÅR | | 29.04.2015 | 28.04.2033 | OPPFYLT | Nei | + | 4567 | GIFT_PARTNERSKAP | | 29.04.2015 | | OPPFYLT | Nei | + | 4567 | BOR_MED_SØKER | BARN_BOR_I_NORGE_MED_SØKER | 25.07.2022 | | OPPFYLT | Nei | + | 4567 | LOVLIG_OPPHOLD | | 25.07.2022 | | OPPFYLT | Nei | + | 4567 | BOSATT_I_RIKET | BARN_BOR_I_NORGE | 25.07.2022 | | OPPFYLT | Nei | + + | 1234 | LOVLIG_OPPHOLD | | 25.07.2022 | | OPPFYLT | Nei | + | 1234 | UTVIDET_BARNETRYGD | | 25.07.2022 | 30.05.2023 | OPPFYLT | Nei | + | 1234 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 25.07.2022 | | OPPFYLT | Nei | + | 1234 | UTVIDET_BARNETRYGD | | 31.05.2023 | | IKKE_OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 4567 | 1 | 01.08.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 4567 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 4567 | 1 | 01.07.2023 | 31.03.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 1234 | 1 | 01.08.2022 | 28.02.2023 | 1054 | UTVIDET_BARNETRYGD | 100 | 1054 | + | 1234 | 1 | 01.03.2023 | 31.05.2023 | 2489 | UTVIDET_BARNETRYGD | 100 | 2489 | + + Og med kompetanser for begrunnelse + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland | + | 4567 | 01.08.2022 | | NORGE_ER_PRIMÆRLAND | 1 | ARBEIDER | I_ARBEID | NO | IE | NO | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.08.2022 | 28.02.2023 | UTBETALING | | INNVILGET_FLYTTET_ETTER_SEPARASJON | | + | 01.03.2023 | 31.05.2023 | UTBETALING | | | | + | 01.06.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | 31.03.2033 | UTBETALING | | | | + | 01.04.2033 | | OPPHØR | | | | \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/utdypende_vilk\303\245rsvurdering_reduksjon.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/utdypende_vilk\303\245rsvurdering_reduksjon.feature" new file mode 100644 index 000000000..723736df8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/utdypende_vilk\303\245rsvurdering_reduksjon.feature" @@ -0,0 +1,84 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for utdypende vilkårsvurdering med reduksjon + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | + | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Skal gi riktige reduksjonsbegrunnelser for delt bosted + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Utdypende vilkår | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 10.03.2021 | Oppfylt | | + | 3456 | BOR_MED_SØKER | 11.03.2021 | 13.04.2022 | Oppfylt | DELT_BOSTED | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2021 | 1000 | 1 | + | 3456 | 01.04.2021 | 31.05.2022 | 500 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 31.03.2021 | UTBETALING | | | + | 01.04.2021 | 31.04.2022 | UTBETALING | REDUKSJON_AVTALE_FAST_BOSTED | | + | 01.05.2022 | | OPPHØR | OPPHØR_AVTALE_OM_FAST_BOSTED | | + + Scenario: Skal gi DELT_BOSTED_SKAL_IKKE_DELES reduksjonsbegrunnelse når BOR_MED_SØKER avsluttes med det utdypende vilkåret + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Utdypende vilkår | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 10.03.2021 | Oppfylt | DELT_BOSTED_SKAL_IKKE_DELES | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2021 | 1000 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 31.03.2021 | UTBETALING | | | + | 01.04.2021 | | OPPHØR | OPPHØR_FAST_BOSTED_AVTALE | | + + + Scenario: Skal gi VURDERING_ANNET_GRUNNLAG reduksjonsbegrunnelse når vilkår med det utdypende vilkåret avsluttes + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Utdypende vilkår | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | | + | 3456 | LOVLIG_OPPHOLD | 13.04.2020 | 10.03.2021 | Oppfylt | VURDERING_ANNET_GRUNNLAG | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2021 | 1354 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 31.03.2021 | UTBETALING | | | + | 01.04.2021 | | OPPHØR | OPPHØR_IKKE_OPPHOLDSTILLATELSE_MER_ENN_12_MÅNEDER | | + + + diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/utdypende_vilk\303\245rsvurdering_utbetaling.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/utdypende_vilk\303\245rsvurdering_utbetaling.feature" new file mode 100644 index 000000000..91a9c6523 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/utdypende_vilk\303\245rsvurdering_utbetaling.feature" @@ -0,0 +1,84 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for utdypende vilkårsvurdering med utbetaling + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | + | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Skal gi riktige begrunnelser for delt bosted med og uten deling + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Utdypende vilkår | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 10.03.2021 | Oppfylt | DELT_BOSTED | + | 3456 | BOR_MED_SØKER | 11.03.2021 | 13.04.2022 | Oppfylt | DELT_BOSTED_SKAL_IKKE_DELES | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2021 | 500 | 1 | + | 3456 | 01.04.2021 | 31.05.2022 | 1354 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 31.03.2021 | UTBETALING | INNVILGET_AVTALE_DELT_BOSTED_FÅR_FRA_FLYTTETIDSPUNKT | | + | 01.04.2021 | 31.04.2022 | UTBETALING | INNVILGET_ANNEN_FORELDER_IKKE_SØKT_DELT_BARNETRYGD_ALLE_BARNA | | + | 01.05.2022 | | OPPHØR | OPPHØR_FAST_BOSTED_AVTALE | | + + Scenario: Skal gi riktige begrunnelser for utdypende vilkår vurdering annet grunnlag + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Utdypende vilkår | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | | + | 3456 | LOVLIG_OPPHOLD | 13.04.2020 | 10.03.2021 | Oppfylt | VURDERING_ANNET_GRUNNLAG | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2021 | 1354 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 31.03.2021 | UTBETALING | INNVILGET_BOSATT_I_RIKTET | INNVILGET_LOVLIG_OPPHOLD_OPPHOLDSTILLATELSE | + | 01.04.2021 | | OPPHØR | OPPHØR_IKKE_OPPHOLDSTILLATELSE_MER_ENN_12_MÅNEDER | | + + Scenario: Skal detektere endring i utdypende vilkår + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Utdypende vilkår | + | 1234 | LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 1234 | BOSATT_I_RIKET | 11.01.1970 | 10.03.2022 | Oppfylt | VURDERING_ANNET_GRUNNLAG | + + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2022 | 1354 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 31.03.2022 | UTBETALING | | | + | 01.04.2022 | | OPPHØR | OPPHØR_UGYLDIG_KONTONUMMER | | + + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/utvidet.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/utvidet.feature new file mode 100644 index 000000000..1d8cbf633 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/utvidet.feature @@ -0,0 +1,44 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for utvidet barnetrygd + + Bakgrunn: + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET | SØKNAD | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1 | SØKER | 26.04.1985 | + | 1 | 2 | BARN | 12.01.2022 | + + Scenario: Skal gi innvilgelsesbegrunnelse INNVILGET_BOR_ALENE_MED_BARN for utvidet i første utbetalingsperiode etter at utvidet er oppfylt + Og følgende dagens dato 28.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 26.04.1985 | | OPPFYLT | Nei | + | 1 | UTVIDET_BARNETRYGD | | 13.02.2023 | | OPPFYLT | Nei | + + | 2 | GIFT_PARTNERSKAP | | 12.01.2022 | | OPPFYLT | Nei | + | 2 | UNDER_18_ÅR | | 12.01.2022 | 11.01.2040 | OPPFYLT | Nei | + | 2 | BOSATT_I_RIKET,BOR_MED_SØKER | | 13.02.2023 | | OPPFYLT | Nei | + | 2 | LOVLIG_OPPHOLD | | 23.04.2023 | 30.06.2023 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 1 | 1 | 01.05.2023 | 30.06.2023 | 2489 | UTVIDET_BARNETRYGD | 100 | 2489 | + | 2 | 1 | 01.05.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | 1723 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2023 | 30.06.2023 | UTBETALING | | INNVILGET_BOR_ALENE_MED_BARN | INNVILGET_SKILT | + | 01.07.2023 | | OPPHØR | | | | diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/utvidet_og_sm\303\245barnstillegg.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/utvidet_og_sm\303\245barnstillegg.feature" new file mode 100644 index 000000000..3766f0c57 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/utvidet_og_sm\303\245barnstillegg.feature" @@ -0,0 +1,84 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser for utvidet barnetrygd og småbarnstillegg + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 1 | 1 | | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 01.09.1984 | + | 1 | 4567 | BARN | 02.02.2015 | + + Scenario: Skal håndtere reduksjon utvidet + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 01.09.1984 | | OPPFYLT | Nei | + | 1234 | UTVIDET_BARNETRYGD | | 15.10.2022 | 15.05.2023 | OPPFYLT | Nei | + + | 4567 | GIFT_PARTNERSKAP,LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 02.02.2015 | | OPPFYLT | Nei | + | 4567 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | Nei | + | 4567 | BOR_MED_SØKER | | 15.02.2022 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 4567 | 1 | 01.03.2022 | 01.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.03.2023 | 01.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.07.2023 | 01.01.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.11.2022 | 01.02.2023 | 1054 | UTVIDET_BARNETRYGD | 100 | + | 1234 | 1 | 01.03.2023 | 01.05.2023 | 2489 | UTVIDET_BARNETRYGD | 100 | + + Og med endrede utbetalinger for begrunnelse + | AktørId | Fra dato | Til dato | BehandlingId | Årsak | Prosent | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.03.2022 | 31.10.2022 | UTBETALING | | | | + | 01.11.2022 | 28.02.2023 | UTBETALING | | | | + | 01.03.2023 | 31.05.2023 | UTBETALING | | | | + | 01.06.2023 | 30.06.2023 | UTBETALING | | REDUKSJON_SAMBOER_IKKE_LENGER_FORSVUNNET | | + | 01.07.2023 | 31.01.2033 | UTBETALING | | | | + | 01.02.2033 | | OPPHØR | | | | + + Scenario: Skal gi riktige begrunnelser ved småbarnstillegg + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 01.09.1984 | | OPPFYLT | Nei | + | 1234 | UTVIDET_BARNETRYGD | | 15.10.2022 | 15.05.2023 | OPPFYLT | Nei | + + | 4567 | GIFT_PARTNERSKAP,LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 02.02.2015 | | OPPFYLT | Nei | + | 4567 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | Nei | + | 4567 | BOR_MED_SØKER | | 15.02.2022 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 4567 | 1 | 01.03.2022 | 01.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.03.2023 | 01.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | + | 4567 | 1 | 01.07.2023 | 01.01.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 1234 | 1 | 01.12.2022 | 01.02.2023 | 2489 | SMÅBARNSTILLEGG | 100 | + | 1234 | 1 | 01.11.2022 | 01.02.2023 | 1054 | UTVIDET_BARNETRYGD | 100 | + | 1234 | 1 | 01.03.2023 | 01.05.2023 | 2489 | UTVIDET_BARNETRYGD | 100 | + + Og med endrede utbetalinger for begrunnelse + | AktørId | Fra dato | Til dato | BehandlingId | Årsak | Prosent | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.03.2022 | 31.10.2022 | UTBETALING | | | | + | 01.11.2022 | 30.11.2022 | UTBETALING | | | INNVILGET_SMÅBARNSTILLEGG | + | 01.12.2022 | 28.02.2023 | UTBETALING | | INNVILGET_SMÅBARNSTILLEGG | | + | 01.03.2023 | 31.05.2023 | UTBETALING | | REDUKSJON_SMÅBARNSTILLEGG_IKKE_LENGER_BARN_UNDER_TRE_ÅR | INNVILGET_SMÅBARNSTILLEGG | + | 01.06.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | 31.01.2033 | UTBETALING | | | | + | 01.02.2033 | | OPPHØR | | | | diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/vilk\303\245r.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/vilk\303\245r.feature" new file mode 100644 index 000000000..fc7715992 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/begrunnelsetekster/vilk\303\245r.feature" @@ -0,0 +1,213 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Gyldige begrunnelser ved endring av vilkår + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | + | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Skal lage vedtaksperioder for mor med et barn med vilkår + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 10.03.2021 | Oppfylt | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2021 | 1354 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 31.03.2021 | UTBETALING | | | + | 01.04.2021 | | OPPHØR | OPPHØR_BARN_FLYTTET_FRA_SØKER | | + + Scenario: Søker får lovlig opphold etter barn + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 15.01.2021 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 10.03.2021 | Oppfylt | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.02.2021 | 31.03.2021 | 1354 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.02.2021 | 31.03.2021 | UTBETALING | INNVILGET_BOSATT_I_RIKTET_LOVLIG_OPPHOLD | | + | 01.04.2021 | | OPPHØR | OPPHØR_BARN_FLYTTET_FRA_SØKER | | + + Scenario: Bor med søker for barn er eneste utgjørende vilkår skal kun gi bor med søker begrunnelser + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 15.01.1990 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2021 | 10.03.2022 | Oppfylt | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2021 | 31.03.2022 | 1354 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2021 | 31.03.2022 | UTBETALING | INNVILGET_BOR_HOS_SØKER | REDUKSJON_IKKE_BOSATT_I_NORGE | + | 01.04.2022 | | OPPHØR | OPPHØR_BARN_FLYTTET_FRA_SØKER | | + + Scenario: Skal ikke gi reduksjonsbegrunnelse når det er innvilgelse + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 15.01.1990 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2021 | 10.03.2022 | Oppfylt | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2021 | 31.03.2022 | 1354 | 1 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2021 | 31.03.2022 | UTBETALING | INNVILGET_BOR_HOS_SØKER | REDUKSJON_FLYTTET_BARN | + | 01.04.2022 | | OPPHØR | OPPHØR_BARN_FLYTTET_FRA_SØKER | | + + Scenario: Skal ikke gi innvilgettekster for mistede vilkår + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 100173051 | 200055501 | | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 100173051 | 2276892299373 | SØKER | 18.10.1984 | + | 100173051 | 2799787304865 | BARN | 02.02.2015 | + + Og lag personresultater for begrunnelse for behandling 100173051 + + Og legg til nye vilkårresultater for begrunnelse for behandling 100173051 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 2799787304865 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | Nei | + | 2799787304865 | LOVLIG_OPPHOLD,BOSATT_I_RIKET,GIFT_PARTNERSKAP | | 02.02.2015 | | OPPFYLT | Nei | + | 2799787304865 | BOR_MED_SØKER | | 15.10.2022 | | OPPFYLT | Nei | + + | 2276892299373 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 18.10.1984 | | OPPFYLT | Nei | + | 2276892299373 | UTVIDET_BARNETRYGD | | 15.01.2023 | 15.05.2023 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 2799787304865 | 100173051 | 01.11.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | + | 2799787304865 | 100173051 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | + | 2799787304865 | 100173051 | 01.07.2023 | 31.01.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 2276892299373 | 100173051 | 01.02.2023 | 28.02.2023 | 1054 | UTVIDET_BARNETRYGD | 100 | + | 2276892299373 | 100173051 | 01.03.2023 | 31.05.2023 | 2489 | UTVIDET_BARNETRYGD | 100 | + + Når begrunnelsetekster genereres for behandling 100173051 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.11.2022 | 31.01.2023 | UTBETALING | | | | + | 01.02.2023 | 28.02.2023 | UTBETALING | | | | + | 01.03.2023 | 31.05.2023 | UTBETALING | | | | + | 01.06.2023 | 30.06.2023 | UTBETALING | | | INNVILGET_FLYTTET_ETTER_SEPARASJON | + | 01.07.2023 | 31.01.2033 | UTBETALING | | | | + | 01.02.2033 | | OPPHØR | | | | + + Scenario: Skal gå ok når søker sine vilkår endrer seg etter opphør + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 100173207 | 200055651 | | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 100173207 | 2005858678161 | BARN | 02.02.2015 | + | 100173207 | 2305793738737 | SØKER | 12.11.1984 | + + Og lag personresultater for begrunnelse for behandling 100173207 + + Og legg til nye vilkårresultater for begrunnelse for behandling 100173207 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 2005858678161 | BOSATT_I_RIKET | BARN_BOR_I_NORGE | 02.02.2015 | | OPPFYLT | Nei | + | 2005858678161 | GIFT_PARTNERSKAP,LOVLIG_OPPHOLD | | 02.02.2015 | | OPPFYLT | Nei | + | 2005858678161 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | Nei | + | 2005858678161 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 02.02.2015 | | OPPFYLT | Nei | + + | 2305793738737 | LOVLIG_OPPHOLD | | 12.11.1984 | | OPPFYLT | Nei | + | 2305793738737 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 15.03.2023 | 15.08.2023 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 2005858678161 | 100173207 | 01.04.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | + | 2005858678161 | 100173207 | 01.07.2023 | 31.08.2023 | 1310 | ORDINÆR_BARNETRYGD | 100 | + + Når begrunnelsetekster genereres for behandling 100173207 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.04.2023 | 30.06.2023 | UTBETALING | | | | + | 01.07.2023 | 31.08.2023 | UTBETALING | | | | + | 01.09.2023 | | OPPHØR | | | | + + Scenario: Skal vise begrunnelse når vi aktivt lager en splitt i vilkåret + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET_OG_OPPHØRT | SØKNAD | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1 | BARN | 07.03.2016 | + | 1 | 2 | SØKER | 14.02.1972 | + + Og følgende dagens dato 27.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1 | UNDER_18_ÅR | 07.03.2016 | 06.03.2034 | OPPFYLT | + | 1 | GIFT_PARTNERSKAP,LOVLIG_OPPHOLD,BOSATT_I_RIKET,BOR_MED_SØKER | 07.03.2016 | | OPPFYLT | + + | 2 | LOVLIG_OPPHOLD | 14.02.1972 | | OPPFYLT | + | 2 | BOSATT_I_RIKET | 15.12.2022 | 15.02.2023 | OPPFYLT | + | 2 | UTVIDET_BARNETRYGD | 14.02.1972 | 14.01.2023 | OPPFYLT | + | 2 | UTVIDET_BARNETRYGD | 15.01.2023 | | OPPFYLT | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 1 | 1 | 01.01.2023 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 2 | 1 | 01.01.2023 | 28.02.2023 | 1054 | UTVIDET_BARNETRYGD | 100 | 1054 | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.01.2023 | 31.01.2023 | UTBETALING | | | | + | 01.02.2023 | 28.02.2023 | UTBETALING | | INNVILGET_BOR_ALENE_MED_BARN | | + | 01.03.2023 | | OPPHØR | | | | diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/innvilget_f\303\270rste_dag_i_m\303\245neden.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/innvilget_f\303\270rste_dag_i_m\303\245neden.feature" new file mode 100644 index 000000000..61dcc8adc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/innvilget_f\303\270rste_dag_i_m\303\245neden.feature" @@ -0,0 +1,67 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Brevbegrunnelser med utvidet barnetrygd + + Bakgrunn: + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET | SØKNAD | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 3456 | BARN | 10.12.2016 | + | 1 | 1234 | SØKER | 24.12.1987 | + + Scenario: Skal finne alle personer når vi har to etterfølgende innvilgede perioder der vi mister et utdypende vilkår + Og følgende dagens dato 04.10.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | + | 1234 | LOVLIG_OPPHOLD | | 24.12.1987 | 31.08.2021 | OPPFYLT | + | 1234 | UTVIDET_BARNETRYGD | | 24.12.1987 | | OPPFYLT | + | 1234 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 01.06.2019 | 31.08.2021 | OPPFYLT | + | 1234 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 01.09.2021 | | OPPFYLT | + + | 3456 | BOSATT_I_RIKET | BARN_BOR_I_EØS | 10.12.2016 | 31.08.2021 | OPPFYLT | + | 3456 | LOVLIG_OPPHOLD | | 10.12.2016 | 31.08.2021 | OPPFYLT | + | 3456 | UNDER_18_ÅR | | 10.12.2016 | 09.12.2034 | OPPFYLT | + | 3456 | GIFT_PARTNERSKAP | | 10.12.2016 | | OPPFYLT | + | 3456 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 10.12.2016 | 31.08.2021 | OPPFYLT | + | 3456 | BOR_MED_SØKER,BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 01.09.2021 | | OPPFYLT | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 3456 | 1 | 01.07.2019 | 31.12.2019 | 0 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 3456 | 1 | 01.01.2020 | 31.08.2020 | 0 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 3456 | 1 | 01.09.2020 | 31.08.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 3456 | 1 | 01.09.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | 1654 | + | 3456 | 1 | 01.01.2022 | 30.11.2022 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 3456 | 1 | 01.12.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 3456 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 3456 | 1 | 01.07.2023 | 30.11.2034 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 1234 | 1 | 01.07.2019 | 31.12.2019 | 928 | UTVIDET_BARNETRYGD | 100 | 1054 | + | 1234 | 1 | 01.01.2020 | 31.08.2020 | 838 | UTVIDET_BARNETRYGD | 100 | 1054 | + | 1234 | 1 | 01.09.2020 | 28.02.2023 | 1054 | UTVIDET_BARNETRYGD | 100 | 1054 | + | 1234 | 1 | 01.03.2023 | 30.06.2023 | 2489 | UTVIDET_BARNETRYGD | 100 | 2489 | + | 1234 | 1 | 01.07.2023 | 30.11.2034 | 2516 | UTVIDET_BARNETRYGD | 100 | 2516 | + + Og med kompetanser for begrunnelse + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland | + | 3456 | 01.07.2019 | 31.08.2020 | NORGE_ER_SEKUNDÆRLAND | 1 | ARBEIDER | MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN | NO | SE | SE | + | 3456 | 01.09.2020 | 31.08.2021 | NORGE_ER_PRIMÆRLAND | 1 | ARBEIDER | I_ARBEID | NO | SE | NO | + + Og med vedtaksperioder for behandling 1 + | Fra dato | Til dato | Standardbegrunnelser | + | 01.09.2021 | 31.12.2021 | INNVILGET_OVERGANG_EØS_TIL_NASJONAL_NORSK_NORDISK_FAMILIE | + + Så forvent følgende brevbegrunnelser for behandling 1 i periode 01.09.2021 til 31.12.2021 + | Begrunnelse | Gjelder søker | Barnas fødselsdatoer | Antall barn | Måned og år begrunnelsen gjelder for | Målform | Beløp | Søknadstidspunkt | Søkers rett til utvidet | + | INNVILGET_OVERGANG_EØS_TIL_NASJONAL_NORSK_NORDISK_FAMILIE | Ja | 10.12.16 | 1 | august 2021 | NB | 2 708 | | SØKER_FÅR_UTVIDET | + + diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/innvilgete-vilk\303\245r.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/innvilgete-vilk\303\245r.feature" new file mode 100644 index 000000000..2d5efce64 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/innvilgete-vilk\303\245r.feature" @@ -0,0 +1,176 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Brevbegrunnelser med riktig fletting av personer med innvilgede vilkår + + Bakgrunn: + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET | SØKNAD | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 31.01.1985 | + | 1 | 3456 | BARN | 02.02.2015 | + | 1 | 5678 | BARN | 07.09.2019 | + + Scenario: Du og barna - skal ha med begge barn og søker + Og følgende dagens dato 26.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | LOVLIG_OPPHOLD | 31.01.1985 | | OPPFYLT | + | 1234 | BOSATT_I_RIKET | 11.11.2022 | | OPPFYLT | + + | 3456 | LOVLIG_OPPHOLD,BOR_MED_SØKER,GIFT_PARTNERSKAP | 02.02.2015 | | OPPFYLT | + | 3456 | UNDER_18_ÅR | 02.02.2015 | 01.02.2033 | OPPFYLT | + | 3456 | BOSATT_I_RIKET | 11.11.2022 | | OPPFYLT | + + | 5678 | BOR_MED_SØKER,GIFT_PARTNERSKAP,LOVLIG_OPPHOLD | 07.09.2019 | | OPPFYLT | + | 5678 | UNDER_18_ÅR | 07.09.2019 | 06.09.2037 | OPPFYLT | + | 5678 | BOSATT_I_RIKET | 11.11.2022 | | OPPFYLT | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 3456 | 1 | 01.12.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 3456 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 3456 | 1 | 01.07.2023 | 31.01.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 5678 | 1 | 01.12.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 5678 | 1 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | 1723 | + | 5678 | 1 | 01.07.2023 | 31.08.2025 | 1766 | ORDINÆR_BARNETRYGD | 100 | 1766 | + | 5678 | 1 | 01.09.2025 | 31.08.2037 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + Og med vedtaksperioder for behandling 1 + | Fra dato | Til dato | Standardbegrunnelser | Eøsbegrunnelser | Fritekster | + | 01.12.2022 | 28.02.2023 | INNVILGET_BOSATT_I_RIKTET | | | + + Så forvent følgende brevbegrunnelser for behandling 1 i periode 01.12.2022 til 28.02.2023 + | Begrunnelse | Gjelder søker | Barnas fødselsdatoer | Antall barn | Måned og år begrunnelsen gjelder for | Målform | Beløp | Søknadstidspunkt | Søkers rett til utvidet | + | INNVILGET_BOSATT_I_RIKTET | Ja | 02.02.15 og 07.09.19 | 2 | november 2022 | NB | 2 730 | | SØKER_HAR_IKKE_RETT | + + + Scenario: barnet - skal kun ta med et barn når det bare er et barn som har endring i vilkår + Og følgende dagens dato 27.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | 31.01.1985 | | OPPFYLT | + + | 3456 | GIFT_PARTNERSKAP,BOSATT_I_RIKET,LOVLIG_OPPHOLD | 02.02.2015 | | OPPFYLT | + | 3456 | UNDER_18_ÅR | 02.02.2015 | 01.02.2033 | OPPFYLT | + | 3456 | BOR_MED_SØKER | 15.05.2023 | | OPPFYLT | + + | 5678 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP,BOR_MED_SØKER | 07.09.2019 | | OPPFYLT | + | 5678 | UNDER_18_ÅR | 07.09.2019 | 06.09.2037 | OPPFYLT | + | 5678 | BOSATT_I_RIKET | 11.11.2022 | | OPPFYLT | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 3456 | 1 | 01.06.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 3456 | 1 | 01.07.2023 | 31.01.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 5678 | 1 | 01.12.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 5678 | 1 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | 1723 | + | 5678 | 1 | 01.07.2023 | 31.08.2025 | 1766 | ORDINÆR_BARNETRYGD | 100 | 1766 | + | 5678 | 1 | 01.09.2025 | 31.08.2037 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + Og med vedtaksperioder for behandling 1 + | Fra dato | Til dato | Standardbegrunnelser | Eøsbegrunnelser | Fritekster | + | 01.12.2022 | 28.02.2023 | INNVILGET_BOSATT_I_RIKTET | | | + + Så forvent følgende brevbegrunnelser for behandling 1 i periode 01.12.2022 til 28.02.2023 + | Begrunnelse | Gjelder søker | Barnas fødselsdatoer | Antall barn | Måned og år begrunnelsen gjelder for | Målform | Beløp | Søknadstidspunkt | Søkers rett til utvidet | + | INNVILGET_BOSATT_I_RIKTET | Nei | 07.09.19 | 1 | november 2022 | NB | 1 676 | | SØKER_HAR_IKKE_RETT | + + + Scenario: Back to back perioder - ønsker kun å begrunne barnet som har flyttet til søker i INNVILGET_BOR_HOS_SØKER + Og følgende dagens dato 27.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 5678 | BOSATT_I_RIKET,GIFT_PARTNERSKAP,LOVLIG_OPPHOLD | | 07.09.2019 | | OPPFYLT | Nei | + | 5678 | UNDER_18_ÅR | | 07.09.2019 | 06.09.2037 | OPPFYLT | Nei | + | 5678 | BOR_MED_SØKER | | 11.11.2022 | 10.05.2023 | OPPFYLT | Nei | + | 5678 | BOR_MED_SØKER | DELT_BOSTED | 11.05.2023 | | OPPFYLT | Nei | + + | 3456 | GIFT_PARTNERSKAP,BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 02.02.2015 | | OPPFYLT | Nei | + | 3456 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | Nei | + | 3456 | BOR_MED_SØKER | | 11.05.2023 | | OPPFYLT | Nei | + + | 1234 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 26.11.1984 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 3456 | 1 | 01.06.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 3456 | 1 | 01.07.2023 | 31.01.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 5678 | 1 | 01.12.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 5678 | 1 | 01.03.2023 | 31.05.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | 1723 | + | 5678 | 1 | 01.06.2023 | 30.06.2023 | 862 | ORDINÆR_BARNETRYGD | 50 | 1723 | + | 5678 | 1 | 01.07.2023 | 31.08.2025 | 883 | ORDINÆR_BARNETRYGD | 50 | 1766 | + | 5678 | 1 | 01.09.2025 | 31.08.2037 | 655 | ORDINÆR_BARNETRYGD | 50 | 1310 | + + Og med vedtaksperioder for behandling 1 + | Fra dato | Til dato | Standardbegrunnelser | Eøsbegrunnelser | Fritekster | + | 01.12.2022 | 28.02.2023 | INNVILGET_BOR_HOS_SØKER | | | + | 01.06.2023 | 30.06.2023 | INNVILGET_BOR_HOS_SØKER, REDUKSJON_AVTALE_FAST_BOSTED | | | + + Så forvent følgende brevbegrunnelser for behandling 1 i periode 01.12.2022 til 28.02.2023 + | Begrunnelse | Gjelder søker | Barnas fødselsdatoer | Antall barn | Måned og år begrunnelsen gjelder for | Målform | Beløp | Søknadstidspunkt | Søkers rett til utvidet | + | INNVILGET_BOR_HOS_SØKER | Nei | 07.09.19 | 1 | november 2022 | NB | 1 676 | | SØKER_HAR_IKKE_RETT | + + Så forvent følgende brevbegrunnelser for behandling 1 i periode 01.06.2023 til 30.06.2023 + | Begrunnelse | Gjelder søker | Barnas fødselsdatoer | Antall barn | Måned og år begrunnelsen gjelder for | Målform | Beløp | Søknadstidspunkt | Søkers rett til utvidet | + | INNVILGET_BOR_HOS_SØKER | Nei | 02.02.15 | 1 | mai 2023 | NB | 1 083 | | SØKER_HAR_IKKE_RETT | + | REDUKSJON_AVTALE_FAST_BOSTED | Nei | 07.09.19 | 1 | mai 2023 | NB | 862 | | SØKER_HAR_IKKE_RETT | + + + Scenario:Endret for 1 av 2 - Skal kun flette inn barnet som det er utbetaling for når det andre barnet etterbetales + Og følgende dagens dato 27.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 5678 | BOSATT_I_RIKET,LOVLIG_OPPHOLD,GIFT_PARTNERSKAP,BOR_MED_SØKER | 07.09.2019 | | OPPFYLT | + | 5678 | UNDER_18_ÅR | 07.09.2019 | 06.09.2037 | OPPFYLT | + + | 1234 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | 24.09.1984 | | OPPFYLT | + + | 3456 | GIFT_PARTNERSKAP,BOSATT_I_RIKET,LOVLIG_OPPHOLD | 02.02.2015 | | OPPFYLT | + | 3456 | UNDER_18_ÅR | 02.02.2015 | 01.02.2033 | OPPFYLT | + | 3456 | BOR_MED_SØKER | 07.09.2019 | | OPPFYLT | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 3456 | 1 | 01.10.2019 | 30.09.2020 | 0 | ORDINÆR_BARNETRYGD | 0 | 1054 | + | 3456 | 1 | 01.10.2020 | 31.01.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 3456 | 1 | 01.02.2021 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 3456 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 3456 | 1 | 01.07.2023 | 31.01.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 5678 | 1 | 01.10.2019 | 31.08.2020 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 5678 | 1 | 01.09.2020 | 31.08.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 5678 | 1 | 01.09.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | 1654 | + | 5678 | 1 | 01.01.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 5678 | 1 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | 1723 | + | 5678 | 1 | 01.07.2023 | 31.08.2025 | 1766 | ORDINÆR_BARNETRYGD | 100 | 1766 | + | 5678 | 1 | 01.09.2025 | 31.08.2037 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + Og med endrede utbetalinger for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent | + | 3456 | 1 | 01.10.2019 | 30.09.2020 | ETTERBETALING_3ÅR | 0 | + + Og med vedtaksperioder for behandling 1 + | Fra dato | Til dato | Standardbegrunnelser | Eøsbegrunnelser | Fritekster | + | 01.10.2019 | 31.08.2020 | INNVILGET_BOR_HOS_SØKER | | | + + Så forvent følgende brevbegrunnelser for behandling 1 i periode 01.10.2019 til 31.08.2020 + | Begrunnelse | Gjelder søker | Barnas fødselsdatoer | Antall barn | Måned og år begrunnelsen gjelder for | Målform | Beløp | Søknadstidspunkt | Søkers rett til utvidet | + | INNVILGET_BOR_HOS_SØKER | Nei | 07.09.19 | 1 | september 2019 | NB | 1 054 | | SØKER_HAR_IKKE_RETT | + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/kompetanser.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/kompetanser.feature new file mode 100644 index 000000000..21baa65bd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/kompetanser.feature @@ -0,0 +1,55 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Brevbegrunnelser ved endring av kompetanse + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | + | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Skal gi riktige restobjekter ved innvilget primærland + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 30.04.2021 | 1054 | 1 | + | 3456 | 01.05.2021 | 31.03.2038 | 1354 | 1 | + + Og med kompetanser for begrunnelse + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Annen forelders aktivitet | Barnets bostedsland | + | 3456 | 01.05.2020 | 30.04.2021 | NORGE_ER_PRIMÆRLAND | 1 | IKKE_AKTUELT | NO | + | 3456 | 01.05.2021 | 31.03.2038 | NORGE_ER_SEKUNDÆRLAND | 1 | I_ARBEID | PL | + + Når begrunnelsetekster genereres for behandling 1 + + Så forvent følgende standardBegrunnelser + | Fra dato | Til dato | VedtaksperiodeType | Regelverk Inkluderte Begrunnelser | Inkluderte Begrunnelser | Ekskluderte Begrunnelser | + | 01.05.2020 | 30.04.2021 | Utbetaling | EØS_FORORDNINGEN | INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE | INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_JOBBER_I_NORGE | + | 01.05.2021 | 31.03.2038 | Utbetaling | EØS_FORORDNINGEN | INNVILGET_SEKUNDÆRLAND_STANDARD | INNVILGET_SEKUNDÆRLAND_TO_ARBEIDSLAND_NORGE_UTBETALER | + | 01.04.2038 | | Opphør | | | | + + Og med vedtaksperioder for behandling 1 + | Fra dato | Til dato | Standardbegrunnelser | Eøsbegrunnelser | Fritekster | + | 01.05.2020 | 30.04.2021 | | INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE | | + | 01.05.2021 | 31.03.2038 | | INNVILGET_SEKUNDÆRLAND_STANDARD | | + + Så forvent følgende brevbegrunnelser for behandling 1 i periode 01.05.2020 til 30.04.2021 + | Begrunnelse | Type | Barnas fødselsdatoer | Antall barn | Målform | Annen forelders aktivitetsland | Barnets bostedsland | Søkers aktivitetsland | Annen forelders aktivitet | Søkers aktivitet | + | INNVILGET_PRIMÆRLAND_BEGGE_FORELDRE_BOSATT_I_NORGE | EØS | 13.04.20 | 1 | NB | Norge | Norge | Polen | IKKE_AKTUELT | ARBEIDER | + + Så forvent følgende brevbegrunnelser for behandling 1 i periode 01.05.2021 til 31.03.2038 + | Begrunnelse | Type | Barnas fødselsdatoer | Antall barn | Målform | Annen forelders aktivitetsland | Barnets bostedsland | Søkers aktivitetsland | Annen forelders aktivitet | Søkers aktivitet | + | INNVILGET_SEKUNDÆRLAND_STANDARD | EØS | 13.04.20 | 1 | NB | Norge | Polen | Polen | I_ARBEID | ARBEIDER | + diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/opph\303\270r_samme_m\303\245ned.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/opph\303\270r_samme_m\303\245ned.feature" new file mode 100644 index 000000000..c104c090a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/opph\303\270r_samme_m\303\245ned.feature" @@ -0,0 +1,75 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Brevbegrunnelser ved opphør der vilkår blir innvilget og opphørt innenfor samme måned. + + Bakgrunn: + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET | SØKNAD | + | 2 | 1 | 1 | OPPHØRT | NYE_OPPLYSNINGER | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 18.09.1984 | + | 1 | 3456 | BARN | 16.06.2015 | + | 1 | 5678 | BARN | 30.11.2016 | + + | 2 | 1234 | SØKER | 18.09.1984 | + | 2 | 3456 | BARN | 16.06.2015 | + | 2 | 5678 | BARN | 30.11.2016 | + + Scenario: Vilkårresultat er oppfylt for kun én måned. Forventer ikke utbetaling + Og følgende dagens dato 28.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + Og lag personresultater for begrunnelse for behandling 2 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | LOVLIG_OPPHOLD | 18.09.1984 | | OPPFYLT | + | 1234 | BOSATT_I_RIKET | 15.08.2020 | | OPPFYLT | + + | 3456 | BOSATT_I_RIKET,GIFT_PARTNERSKAP,LOVLIG_OPPHOLD,BOR_MED_SØKER | 16.06.2015 | | OPPFYLT | + | 3456 | UNDER_18_ÅR | 16.06.2015 | 15.06.2033 | OPPFYLT | + + | 5678 | LOVLIG_OPPHOLD,BOSATT_I_RIKET,GIFT_PARTNERSKAP,BOR_MED_SØKER | 30.11.2016 | | OPPFYLT | + | 5678 | UNDER_18_ÅR | 30.11.2016 | 29.11.2034 | OPPFYLT | + + Og legg til nye vilkårresultater for begrunnelse for behandling 2 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | LOVLIG_OPPHOLD | 18.09.1984 | | OPPFYLT | + | 1234 | BOSATT_I_RIKET | 15.08.2020 | 16.08.2020 | OPPFYLT | + + | 3456 | BOSATT_I_RIKET,BOR_MED_SØKER,GIFT_PARTNERSKAP,LOVLIG_OPPHOLD | 16.06.2015 | | OPPFYLT | + | 3456 | UNDER_18_ÅR | 16.06.2015 | 15.06.2033 | OPPFYLT | + + | 5678 | LOVLIG_OPPHOLD,BOR_MED_SØKER,GIFT_PARTNERSKAP,BOSATT_I_RIKET | 30.11.2016 | | OPPFYLT | + | 5678 | UNDER_18_ÅR | 30.11.2016 | 29.11.2034 | OPPFYLT | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 3456 | 1 | 01.09.2020 | 31.05.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 3456 | 1 | 01.06.2021 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 3456 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 3456 | 1 | 01.07.2023 | 31.05.2033 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + | 5678 | 1 | 01.09.2020 | 31.08.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 5678 | 1 | 01.09.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | 1654 | + | 5678 | 1 | 01.01.2022 | 31.10.2022 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 5678 | 1 | 01.11.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 5678 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 5678 | 1 | 01.07.2023 | 31.10.2034 | 1310 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + Når begrunnelsetekster genereres for behandling 2 + + Og med vedtaksperioder for behandling 2 + | Fra dato | Til dato | Standardbegrunnelser | Eøsbegrunnelser | Fritekster | + | 01.09.2020 | | OPPHØR_IKKE_BOSATT_I_NORGE | | | + + Så forvent følgende brevbegrunnelser for behandling 2 i periode 01.09.2020 til - + | Begrunnelse | Gjelder søker | Barnas fødselsdatoer | Antall barn | Måned og år begrunnelsen gjelder for | Målform | Beløp | Søknadstidspunkt | Søkers rett til utvidet | + | OPPHØR_IKKE_BOSATT_I_NORGE | Ja | 16.06.15 og 30.11.16 | 2 | august 2020 | NB | 0 | | SØKER_HAR_IKKE_RETT | + diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/vilk\303\245r.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/vilk\303\245r.feature" new file mode 100644 index 000000000..7fdbc7511 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevBegrunnelser/vilk\303\245r.feature" @@ -0,0 +1,37 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Brevbegrunnelser ved endring av vilkår + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | + | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Skal lage vedtaksperioder for mor med et barn med vilkår + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 10.03.2021 | Oppfylt | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2021 | 1354 | 1 | + + Og med vedtaksperioder for behandling 1 + | Fra dato | Til dato | Standardbegrunnelser | Eøsbegrunnelser | Fritekster | + | 01.04.2021 | | OPPHØR_BARN_FLYTTET_FRA_SØKER | | | + + Så forvent følgende brevbegrunnelser for behandling 1 i periode 01.04.2021 til - + | Begrunnelse | Gjelder søker | Barnas fødselsdatoer | Antall barn | Måned og år begrunnelsen gjelder for | Målform | Beløp | Søknadstidspunkt | Søkers rett til utvidet | + | OPPHØR_BARN_FLYTTET_FRA_SØKER | Nei | 13.04.20 | 1 | mars 2021 | NB | 0 | | SØKER_HAR_IKKE_RETT | + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevPerioder/brevperiodetype.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevPerioder/brevperiodetype.feature new file mode 100644 index 000000000..762449b34 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevPerioder/brevperiodetype.feature @@ -0,0 +1,51 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Brevperioder: Brevperiodetype + + Bakgrunn: + Gitt følgende fagsaker for begrunnelse + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende behandling + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET | SØKNAD | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1 | BARN | 16.03.2022 | + | 1 | 2 | SØKER | 28.07.1985 | + + Scenario: Skal få brevperiode av typen utbetaling når vi har eøs nullutbetaling + Og følgende dagens dato 29.09.2023 + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 2 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 28.07.1985 | | OPPFYLT | Nei | + | 2 | LOVLIG_OPPHOLD | | 28.07.1985 | | OPPFYLT | Nei | + + | 1 | UNDER_18_ÅR | | 16.03.2022 | 15.03.2040 | OPPFYLT | Nei | + | 1 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 16.03.2022 | | OPPFYLT | Nei | + | 1 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP | | 16.03.2022 | | OPPFYLT | Nei | + | 1 | BOSATT_I_RIKET | BARN_BOR_I_NORGE | 16.03.2022 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 1 | 1 | 01.04.2022 | 28.02.2023 | 0 | ORDINÆR_BARNETRYGD | 100 | 1676 | + | 1 | 1 | 01.03.2023 | 30.06.2023 | 0 | ORDINÆR_BARNETRYGD | 100 | 1723 | + | 1 | 1 | 01.07.2023 | 29.02.2028 | 23 | ORDINÆR_BARNETRYGD | 100 | 1766 | + | 1 | 1 | 01.03.2028 | 29.02.2040 | 0 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + Og med kompetanser for begrunnelse + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland | + | 1 | 01.04.2022 | | NORGE_ER_SEKUNDÆRLAND | 1 | ARBEIDER | I_ARBEID | NO | PL | PL | + + Og med vedtaksperioder for behandling 1 + | Fra dato | Til dato | Standardbegrunnelser | Eøsbegrunnelser | Fritekster | + | 01.04.2022 | 30.06.2023 | | INNVILGET_SEKUNDÆRLAND_STANDARD | | + + Så forvent følgende brevperioder for behandling 1 + | Brevperiodetype | Fra dato | Til dato | Beløp | Antall barn med utbetaling | Barnas fødselsdager | Du eller institusjonen | + | UTBETALING | april 2022 | til juni 2023 | 0 | 0 | | du | diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevPerioder/vilk\303\245r.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevPerioder/vilk\303\245r.feature" new file mode 100644 index 000000000..a1a513da0 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/brevPerioder/vilk\303\245r.feature" @@ -0,0 +1,37 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Brevperioder ved endring av vilkår + + Bakgrunn: + Gitt følgende behandling + | BehandlingId | + | 1 | + + Og følgende persongrunnlag for begrunnelse + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Skal lage vedtaksperioder for mor med et barn med vilkår + Og lag personresultater for begrunnelse for behandling 1 + + Og legg til nye vilkårresultater for begrunnelse for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 10.03.2021 | Oppfylt | + + Og med andeler tilkjent ytelse for begrunnelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2021 | 1354 | 1 | + + Og med vedtaksperioder for behandling 1 + | Fra dato | Til dato | Standardbegrunnelser | Eøsbegrunnelser | Fritekster | + | 01.04.2021 | | OPPHØR_BARN_FLYTTET_FRA_SØKER | | | + + Så forvent følgende brevperioder for behandling 1 + | Brevperiodetype | Fra dato | Til dato | Beløp | Antall barn med utbetaling | Barnas fødselsdager | Du eller institusjonen | + | INGEN_UTBETALING | april 2021 | | 0 | 0 | | du | + diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/0bel\303\270p.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/0bel\303\270p.feature" new file mode 100644 index 000000000..9fffbb4c2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/0bel\303\270p.feature" @@ -0,0 +1,93 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Utbetalingsoppdrag: Håndtering av 0-beløp + + + Scenario: Endrer en tidligere periode til 0-utbetaling + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + | 1 | 04.2021 | 04.2021 | 700 | + | 2 | 03.2021 | 03.2021 | 0 | + | 2 | 04.2021 | 04.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 04.2021 | | 700 | NY | Nei | 1 | 0 | + + | 2 | 04.2021 | 04.2021 | 03.2021 | 700 | ENDR | Ja | 1 | 0 | + | 2 | 04.2021 | 04.2021 | | 700 | ENDR | Nei | 2 | 1 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 04.2021 | | 700 | NY | Nei | 1 | 0 | + + | 2 | 04.2021 | 04.2021 | 03.2021 | 700 | ENDR | Ja | 1 | 0 | + | 2 | 04.2021 | 04.2021 | | 700 | ENDR | Nei | 2 | 1 | + + Scenario: Splitter en periode til 2 perioder der en av de får 0-beløp + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 04.2021 | 700 | + | 1 | 05.2021 | 06.2021 | 800 | + + | 2 | 03.2021 | 03.2021 | 700 | + | 2 | 04.2021 | 04.2021 | 0 | + | 2 | 05.2021 | 06.2021 | 800 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 04.2021 | | 700 | NY | Nei | 0 | | + | 1 | 05.2021 | 06.2021 | | 800 | NY | Nei | 1 | 0 | + + | 2 | 05.2021 | 06.2021 | 04.2021 | 800 | ENDR | Ja | 1 | 0 | + | 2 | 05.2021 | 06.2021 | | 800 | ENDR | Nei | 2 | 1 | + + Scenario: 0 beløp før forrige periode skal opphøra bak i tiden, for å kunne opphøre bak i tiden til når infotrygd eide dataen + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + + | 2 | 02.2021 | 02.2021 | 0 | + | 2 | 03.2021 | 03.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + + | 2 | 03.2021 | 03.2021 | 02.2021 | 700 | ENDR | Ja | 0 | | + | 2 | 03.2021 | 03.2021 | | 700 | ENDR | Nei | 1 | 0 | + + Scenario: 0-beløp beholdes, og får en ny andel + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + | 1 | 04.2021 | 04.2021 | 0 | + | 1 | 05.2021 | 05.2021 | 800 | + + | 2 | 03.2021 | 03.2021 | 700 | + | 2 | 04.2021 | 04.2021 | 0 | + | 2 | 05.2021 | 05.2021 | 800 | + | 2 | 06.2021 | 06.2021 | 900 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 05.2021 | 05.2021 | | 800 | NY | Nei | 1 | 0 | + + | 2 | 06.2021 | 06.2021 | | 900 | ENDR | Nei | 2 | 1 | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/endretMigreringsdato.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/endretMigreringsdato.feature new file mode 100644 index 000000000..92ad6bd86 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/endretMigreringsdato.feature @@ -0,0 +1,61 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Utbetalingsoppdrag: Endring av migreringsdato + + + Scenario: Endrer migreringsdato på en behandling før første fom + + Gitt følgende behandlingsinformasjon + | BehandlingId | Endret migreringsdato | + | 1 | 03.2021 | + | 2 | 01.2021 | + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + | 2 | 03.2021 | 03.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + + | 2 | 03.2021 | 03.2021 | 01.2021 | 700 | ENDR | Ja | 0 | | + | 2 | 03.2021 | 03.2021 | | 700 | ENDR | Nei | 1 | 0 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + + | 2 | 03.2021 | 03.2021 | 01.2021 | 700 | ENDR | Ja | 0 | | + | 2 | 03.2021 | 03.2021 | | 700 | ENDR | Nei | 1 | 0 | + + Scenario: Endrer migreringsdato på en behandling etter første fom + + Gitt følgende behandlingsinformasjon + | BehandlingId | Endret migreringsdato | + | 1 | 03.2021 | + | 2 | 04.2021 | + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + | 2 | 03.2021 | 03.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent at en exception kastes for behandling 2 + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + + | 2 | 03.2021 | 03.2021 | 04.2021 | 700 | ENDR | Ja | 0 | | + | 2 | 03.2021 | 03.2021 | | 700 | ENDR | Nei | 1 | 0 | + + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/flerePersoner.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/flerePersoner.feature new file mode 100644 index 000000000..652f3b43e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/flerePersoner.feature @@ -0,0 +1,63 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Utbetalingsoppdrag: Vedtak med flere identer + + + Scenario: Vedtak med to perioder på ulike identer + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | Ident | + | 1 | 03.2021 | 03.2021 | 700 | 1 | + | 1 | 03.2021 | 03.2021 | 700 | 2 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | Kildebehandling | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | 1 | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 1 | | 1 | + + + Scenario: Revurderer og legger til en periode på en av personene + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | Ident | + | 1 | 03.2021 | 03.2021 | 700 | 1 | + | 1 | 03.2021 | 03.2021 | 700 | 2 | + + | 2 | 03.2021 | 03.2021 | 700 | 1 | + | 2 | 04.2021 | 04.2021 | 800 | 1 | + | 2 | 03.2021 | 03.2021 | 700 | 2 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | Kildebehandling | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | 1 | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 1 | | 1 | + | 2 | 04.2021 | 04.2021 | | 800 | ENDR | Nei | 2 | 0 | 2 | + + + Scenario: Revurderer og avkorter stønadsperiode på en av personene + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | Ident | + | 1 | 03.2021 | 03.2021 | 700 | 1 | + | 1 | 03.2021 | 04.2021 | 700 | 2 | + + | 2 | 03.2021 | 03.2021 | 700 | 1 | + | 2 | 03.2021 | 03.2021 | 700 | 2 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | Kildebehandling | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | 1 | + | 1 | 03.2021 | 04.2021 | | 700 | NY | Nei | 1 | | 1 | + + # kildebehandling på den første raden burde peke til den første behandlingen. EF gjør det samme her + | 2 | 03.2021 | 04.2021 | 03.2021 | 700 | ENDR | Ja | 1 | | 2 | + | 2 | 03.2021 | 03.2021 | | 700 | ENDR | Nei | 2 | 1 | 2 | + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/oppdrag.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/oppdrag.feature new file mode 100644 index 000000000..f91587aa7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/oppdrag.feature @@ -0,0 +1,174 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Utbetalingsoppdrag: Vedtak for førstegangsbehandling + + + Scenario: Vedtak med en periode + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + + Scenario: Revurdering uten endring av andeler + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + | 1 | 04.2021 | 04.2021 | 700 | + | 2 | 03.2021 | 03.2021 | 700 | + | 2 | 04.2021 | 04.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 04.2021 | | 700 | NY | Nei | 1 | 0 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 04.2021 | | 700 | NY | Nei | 1 | 0 | + + + Scenario: Vedtak med to perioder + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + | 1 | 04.2021 | 05.2021 | 800 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 05.2021 | | 800 | NY | Nei | 1 | 0 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 05.2021 | | 800 | NY | Nei | 1 | 0 | + + + Scenario: Revurdering som legger til en periode, simulering skal opphøre fra start for å kunne vise all historikk + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + | 2 | 03.2021 | 03.2021 | 700 | + | 2 | 04.2021 | 04.2021 | 800 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 2 | 04.2021 | 04.2021 | | 800 | ENDR | Nei | 1 | 0 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 2 | 04.2021 | 04.2021 | | 800 | ENDR | Nei | 1 | 0 | + + Så forvent følgende simulering + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 2 | 03.2021 | 03.2021 | 03.2021 | 700 | ENDR | Ja | 0 | | + | 2 | 03.2021 | 03.2021 | | 700 | ENDR | Nei | 1 | 0 | + | 2 | 04.2021 | 04.2021 | | 800 | ENDR | Nei | 2 | 1 | + + Så forvent følgende simulering med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 2 | 03.2021 | 03.2021 | 03.2021 | 700 | ENDR | Ja | 0 | | + | 2 | 03.2021 | 03.2021 | | 700 | ENDR | Nei | 1 | 0 | + | 2 | 04.2021 | 04.2021 | | 800 | ENDR | Nei | 2 | 1 | + + Scenario: 2 revurderinger som legger til en periode + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + + | 2 | 03.2021 | 03.2021 | 700 | + | 2 | 04.2021 | 04.2021 | 800 | + + | 3 | 03.2021 | 03.2021 | 700 | + | 3 | 04.2021 | 04.2021 | 800 | + | 3 | 05.2021 | 05.2021 | 900 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | Kildebehandling | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | 1 | + | 2 | 04.2021 | 04.2021 | | 800 | ENDR | Nei | 1 | 0 | 2 | + | 3 | 05.2021 | 05.2021 | | 900 | ENDR | Nei | 2 | 1 | 3 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | Kildebehandling | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | 1 | + | 2 | 04.2021 | 04.2021 | | 800 | ENDR | Nei | 1 | 0 | 2 | + | 3 | 05.2021 | 05.2021 | | 900 | ENDR | Nei | 2 | 1 | 3 | + + Scenario: Endrer beløp fra april + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 06.2021 | 700 | + + | 2 | 03.2021 | 03.2021 | 700 | + | 2 | 04.2021 | 06.2021 | 800 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 06.2021 | | 700 | NY | Nei | 0 | | + | 2 | 03.2021 | 06.2021 | 03.2021 | 700 | ENDR | Ja | 0 | | + | 2 | 03.2021 | 03.2021 | | 700 | ENDR | Nei | 1 | 0 | + | 2 | 04.2021 | 06.2021 | | 800 | ENDR | Nei | 2 | 1 | + + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 06.2021 | | 700 | NY | Nei | 0 | | + | 2 | 04.2021 | 06.2021 | | 800 | ENDR | Nei | 1 | 0 | + + + Scenario: Endrer beløp fra start + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 06.2021 | 700 | + + | 2 | 03.2021 | 03.2021 | 800 | + | 2 | 04.2021 | 06.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 06.2021 | | 700 | NY | Nei | 0 | | + | 2 | 03.2021 | 06.2021 | 03.2021 | 700 | ENDR | Ja | 0 | | + | 2 | 03.2021 | 03.2021 | | 800 | ENDR | Nei | 1 | 0 | + | 2 | 04.2021 | 06.2021 | | 700 | ENDR | Nei | 2 | 1 | + + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 06.2021 | | 700 | NY | Nei | 0 | | + | 2 | 03.2021 | 03.2021 | | 800 | ENDR | Nei | 1 | 0 | + | 2 | 04.2021 | 06.2021 | | 700 | ENDR | Nei | 2 | 1 | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/opphoer.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/opphoer.feature new file mode 100644 index 000000000..556082abd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/opphoer.feature @@ -0,0 +1,228 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Utbetalingsoppdrag: Opphør + + + Scenario: Opphør en periode + + Gitt følgende tilkjente ytelser + | BehandlingId | Uten andeler | Fra dato | Til dato | Beløp | + | 1 | | 03.2021 | 03.2021 | 700 | + | 2 | Ja | | | | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 2 | 03.2021 | 03.2021 | 03.2021 | 700 | ENDR | Ja | 0 | | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 2 | 03.2021 | 03.2021 | 03.2021 | 700 | ENDR | Ja | 0 | | + + Scenario: Iverksetter på nytt etter opphør + + Gitt følgende tilkjente ytelser + | BehandlingId | Uten andeler | Fra dato | Til dato | Beløp | + | 1 | | 03.2021 | 03.2021 | 700 | + | 2 | Ja | | | | + | 3 | | 03.2021 | 03.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 2 | 03.2021 | 03.2021 | 03.2021 | 700 | ENDR | Ja | 0 | | + | 3 | 03.2021 | 03.2021 | | 700 | ENDR | Nei | 1 | 0 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 2 | 03.2021 | 03.2021 | 03.2021 | 700 | ENDR | Ja | 0 | | + | 3 | 03.2021 | 03.2021 | | 700 | ENDR | Nei | 1 | 0 | + + Scenario: Opphør en av 2 perioder + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + | 1 | 04.2021 | 04.2021 | 800 | + | 2 | 03.2021 | 03.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 04.2021 | | 800 | NY | Nei | 1 | 0 | + | 2 | 04.2021 | 04.2021 | 04.2021 | 800 | ENDR | Ja | 1 | 0 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 04.2021 | | 800 | NY | Nei | 1 | 0 | + | 2 | 04.2021 | 04.2021 | 04.2021 | 800 | ENDR | Ja | 1 | 0 | + + Scenario: Opphører en lang periode + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 06.2021 | 700 | + | 2 | 03.2021 | 04.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 06.2021 | | 700 | NY | Nei | 0 | | + | 2 | 03.2021 | 06.2021 | 03.2021 | 700 | ENDR | Ja | 0 | | + | 2 | 03.2021 | 04.2021 | | 700 | ENDR | Nei | 1 | 0 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 06.2021 | | 700 | NY | Nei | 0 | | + | 2 | 03.2021 | 06.2021 | 05.2021 | 700 | ENDR | Ja | 0 | | + + Scenario: Opphør en tidligere periode da vi kun har med den andre av 2 perioder + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + | 1 | 04.2021 | 04.2021 | 700 | + | 2 | 04.2021 | 04.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | Kildebehandling | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | 1 | + | 1 | 04.2021 | 04.2021 | | 700 | NY | Nei | 1 | 0 | 1 | + + # kildebehandling på den første raden burde peke til den første behandlingen. EF gjør det samme her + | 2 | 04.2021 | 04.2021 | 03.2021 | 700 | ENDR | Ja | 1 | 0 | 2 | + | 2 | 04.2021 | 04.2021 | | 700 | ENDR | Nei | 2 | 1 | 2 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | Kildebehandling | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | | + | 1 | 04.2021 | 04.2021 | | 700 | NY | Nei | 1 | 0 | | + + | 2 | 04.2021 | 04.2021 | 03.2021 | 700 | ENDR | Ja | 1 | 0 | | + | 2 | 04.2021 | 04.2021 | | 700 | ENDR | Nei | 2 | 1 | | + + Scenario: Endrer en tidligere periode til 0-utbetaling + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + | 1 | 04.2021 | 04.2021 | 700 | + | 2 | 03.2021 | 03.2021 | 0 | + | 2 | 04.2021 | 04.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 04.2021 | | 700 | NY | Nei | 1 | 0 | + + | 2 | 04.2021 | 04.2021 | 03.2021 | 700 | ENDR | Ja | 1 | 0 | + | 2 | 04.2021 | 04.2021 | | 700 | ENDR | Nei | 2 | 1 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 04.2021 | | 700 | NY | Nei | 1 | 0 | + + | 2 | 04.2021 | 04.2021 | 03.2021 | 700 | ENDR | Ja | 1 | 0 | + | 2 | 04.2021 | 04.2021 | | 700 | ENDR | Nei | 2 | 1 | + + + Scenario: 2 opphør etter hverendre på ulike perioder + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + | 1 | 04.2021 | 04.2021 | 800 | + | 1 | 05.2021 | 05.2021 | 900 | + | 2 | 03.2021 | 03.2021 | 700 | + | 2 | 04.2021 | 04.2021 | 800 | + | 3 | 03.2021 | 03.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 04.2021 | | 800 | NY | Nei | 1 | 0 | + | 1 | 05.2021 | 05.2021 | | 900 | NY | Nei | 2 | 1 | + + | 2 | 05.2021 | 05.2021 | 05.2021 | 900 | ENDR | Ja | 2 | 1 | + + | 3 | 05.2021 | 05.2021 | 04.2021 | 900 | ENDR | Ja | 2 | 1 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 04.2021 | | 800 | NY | Nei | 1 | 0 | + | 1 | 05.2021 | 05.2021 | | 900 | NY | Nei | 2 | 1 | + + | 2 | 05.2021 | 05.2021 | 05.2021 | 900 | ENDR | Ja | 2 | 1 | + + | 3 | 05.2021 | 05.2021 | 04.2021 | 900 | ENDR | Ja | 2 | 1 | + + + Scenario: Opphør mellom 2 andeler + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 08.2021 | 700 | + | 2 | 03.2021 | 04.2021 | 700 | + | 2 | 07.2021 | 08.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 08.2021 | | 700 | NY | Nei | 0 | | + | 2 | 03.2021 | 08.2021 | 03.2021 | 700 | ENDR | Ja | 0 | | + | 2 | 03.2021 | 04.2021 | | 700 | ENDR | Nei | 1 | 0 | + | 2 | 07.2021 | 08.2021 | | 700 | ENDR | Nei | 2 | 1 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 08.2021 | | 700 | NY | Nei | 0 | | + | 2 | 03.2021 | 08.2021 | 05.2021 | 700 | ENDR | Ja | 0 | | + | 2 | 07.2021 | 08.2021 | | 700 | ENDR | Nei | 1 | 0 | + + Scenario: Avkorter en periode, som man sen opphører. Her må opphøret ha peiling på siste andelen med riktig tom + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | + | 1 | 03.2021 | 03.2021 | 700 | + | 1 | 04.2021 | 08.2021 | 700 | + | 2 | 03.2021 | 03.2021 | 700 | + | 2 | 04.2021 | 05.2021 | 700 | + | 3 | 03.2021 | 03.2021 | 700 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 08.2021 | | 700 | NY | Nei | 1 | 0 | + | 2 | 04.2021 | 08.2021 | 04.2021 | 700 | ENDR | Ja | 1 | 0 | + | 2 | 04.2021 | 05.2021 | | 700 | ENDR | Nei | 2 | 1 | + | 3 | 04.2021 | 05.2021 | 04.2021 | 700 | ENDR | Ja | 2 | 1 | + + Så forvent følgende utbetalingsoppdrag med ny utbetalingsgenerator + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | NY | Nei | 0 | | + | 1 | 04.2021 | 08.2021 | | 700 | NY | Nei | 1 | 0 | + | 2 | 04.2021 | 08.2021 | 06.2021 | 700 | ENDR | Ja | 1 | 0 | + | 3 | 04.2021 | 08.2021 | 04.2021 | 700 | ENDR | Ja | 1 | 0 | + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/ytelsestyper.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/ytelsestyper.feature new file mode 100644 index 000000000..168467679 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/oppdrag/ytelsestyper.feature @@ -0,0 +1,72 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Utbetalingsoppdrag: Ulike ytelsestyper på andelene + + + Scenario: Søker med utvidet og småbarnstillegg + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | Ident | Ytelse | + | 1 | 03.2021 | 03.2021 | 700 | 1 | UTVIDET_BARNETRYGD | + | 1 | 03.2021 | 03.2021 | 800 | 1 | SMÅBARNSTILLEGG | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Ytelse | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 700 | UTVIDET_BARNETRYGD | NY | Nei | 0 | | + | 1 | 03.2021 | 03.2021 | | 800 | SMÅBARNSTILLEGG | NY | Nei | 1 | | + + Scenario: Revurdering endrer beløp på småbarnstillegg fra april + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | Ident | Ytelse | + | 1 | 03.2021 | 05.2021 | 700 | 1 | UTVIDET_BARNETRYGD | + | 1 | 03.2021 | 05.2021 | 800 | 1 | SMÅBARNSTILLEGG | + | 2 | 03.2021 | 05.2021 | 700 | 1 | UTVIDET_BARNETRYGD | + | 2 | 03.2021 | 03.2021 | 800 | 1 | SMÅBARNSTILLEGG | + | 2 | 04.2021 | 05.2021 | 800 | 1 | SMÅBARNSTILLEGG | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Ytelse | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 05.2021 | | 700 | UTVIDET_BARNETRYGD | NY | Nei | 0 | | + | 1 | 03.2021 | 05.2021 | | 800 | SMÅBARNSTILLEGG | NY | Nei | 1 | | + | 2 | 03.2021 | 05.2021 | 03.2021 | 800 | SMÅBARNSTILLEGG | ENDR | Ja | 1 | | + | 2 | 03.2021 | 03.2021 | | 800 | SMÅBARNSTILLEGG | ENDR | Nei | 2 | 1 | + | 2 | 04.2021 | 05.2021 | | 800 | SMÅBARNSTILLEGG | ENDR | Nei | 3 | 2 | + + Scenario: Forelder og barn har flere stønadstyper som alle blir egne kjeder. Øker hvert beløp med 100kr i revurderingen for å verifisere at det fortsatt blir 4 ulike kjeder + + Gitt følgende tilkjente ytelser + | BehandlingId | Fra dato | Til dato | Beløp | Ytelse | Ident | + | 1 | 03.2021 | 03.2021 | 100 | UTVIDET_BARNETRYGD | 1 | + | 1 | 03.2021 | 03.2021 | 200 | SMÅBARNSTILLEGG | 1 | + | 1 | 03.2021 | 03.2021 | 300 | ORDINÆR_BARNETRYGD | 2 | + | 1 | 03.2021 | 03.2021 | 400 | UTVIDET_BARNETRYGD | 2 | + + | 2 | 03.2021 | 03.2021 | 200 | UTVIDET_BARNETRYGD | 1 | + | 2 | 03.2021 | 03.2021 | 300 | SMÅBARNSTILLEGG | 1 | + | 2 | 03.2021 | 03.2021 | 400 | ORDINÆR_BARNETRYGD | 2 | + | 2 | 03.2021 | 03.2021 | 500 | UTVIDET_BARNETRYGD | 2 | + + Når beregner utbetalingsoppdrag + + Så forvent følgende utbetalingsoppdrag + | BehandlingId | Fra dato | Til dato | Opphørsdato | Beløp | Ytelse | Kode endring | Er endring | Periode id | Forrige periode id | + | 1 | 03.2021 | 03.2021 | | 100 | UTVIDET_BARNETRYGD | NY | Nei | 0 | | + | 1 | 03.2021 | 03.2021 | | 200 | SMÅBARNSTILLEGG | NY | Nei | 1 | | + | 1 | 03.2021 | 03.2021 | | 300 | ORDINÆR_BARNETRYGD | NY | Nei | 2 | | + | 1 | 03.2021 | 03.2021 | | 400 | UTVIDET_BARNETRYGD | NY | Nei | 3 | | + + | 2 | 03.2021 | 03.2021 | 03.2021 | 100 | UTVIDET_BARNETRYGD | ENDR | Ja | 0 | | + | 2 | 03.2021 | 03.2021 | 03.2021 | 200 | SMÅBARNSTILLEGG | ENDR | Ja | 1 | | + | 2 | 03.2021 | 03.2021 | 03.2021 | 300 | ORDINÆR_BARNETRYGD | ENDR | Ja | 2 | | + | 2 | 03.2021 | 03.2021 | 03.2021 | 400 | UTVIDET_BARNETRYGD | ENDR | Ja | 3 | | + + | 2 | 03.2021 | 03.2021 | | 200 | UTVIDET_BARNETRYGD | ENDR | Nei | 4 | 0 | + | 2 | 03.2021 | 03.2021 | | 300 | SMÅBARNSTILLEGG | ENDR | Nei | 5 | 1 | + | 2 | 03.2021 | 03.2021 | | 400 | ORDINÆR_BARNETRYGD | ENDR | Nei | 6 | 2 | + | 2 | 03.2021 | 03.2021 | | 500 | UTVIDET_BARNETRYGD | ENDR | Nei | 7 | 3 | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/andel_tilkjent_ytelse.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/andel_tilkjent_ytelse.feature new file mode 100644 index 000000000..036c87be9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/andel_tilkjent_ytelse.feature @@ -0,0 +1,88 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperioder med andeler tilkjent ytelse + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | + | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Skal lage vedtaksperioder for mor med ett barn med andeler tilkjent ytelse + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 30.04.2021 | 1054 | 1 | + | 3456 | 01.05.2021 | 31.03.2038 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 30.04.2021 | Utbetaling | Barn og søker | + | 01.05.2021 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.04.2038 | | Opphør | Kun søker | + + + Scenario: Skal lage vedtaksperioder for mor med ett barn med andel tilkjent ytelse med lik verdi og forskjellig begrunnelse + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 30.04.2021 | 1054 | 1 | + | 3456 | 01.05.2021 | 31.03.2038 | 1054 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.04.2038 | | Opphør | Kun søker | + + Scenario: Skal lage vedtaksperioder selv om andel tilkjent ytelse har 0 i beløp + Gitt følgende vedtak + | BehandlingId | + | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.10.1987 | + | 1 | 3456 | BARN | 04.09.2020 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.10.1987 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 04.09.2020 | 03.09.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 04.09.2020 | | Oppfylt | + + Og med kompetanser + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | + | 3456 | 01.10.2020 | | NORGE_ER_SEKUNDÆRLAND | 1 | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.10.2020 | 31.08.2038 | 0 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.10.2020 | 31.08.2038 | Utbetaling | Barn og søker | + | 01.09.2038 | | Opphør | Kun søker | \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/avslag.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/avslag.feature new file mode 100644 index 000000000..de317641c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/avslag.feature @@ -0,0 +1,202 @@ +# language: no +# encoding: UTF-8 + + +Egenskap: Vedtaksperioder med mor og to barn + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | + | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 24.12.1987 | + | 1 | 3456 | BARN | 01.02.2016 | + + + Scenario: Skal kun lage én avslagsperiode når det er avslag på søker hele perioden og ingen andre avslag + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET | 24.12.1987 | | Oppfylt | | + | 1234 | LOVLIG_OPPHOLD | 24.12.1987 | | ikke_oppfylt | Ja | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 01.12.2016 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 01.12.2016 | 30.11.2034 | Oppfylt | | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 1988-01-01 | | AVSLAG | Kun søker | + + + Scenario: Skal kun lage én avslagsperiode når det er avslag på søker hele perioden og ingen andre avslag. Ingen startdato + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET | 24.12.1987 | | Oppfylt | | + | 1234 | LOVLIG_OPPHOLD | | | ikke_oppfylt | Ja | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 01.12.2016 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 01.12.2016 | 30.11.2034 | Oppfylt | | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | | | AVSLAG | Kun søker | + + Scenario: Skal lage to avslagsperioder når søker har konstant avslag og barn har en avslagsperiode med fom og tom + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET | 24.12.1987 | | Oppfylt | | + | 1234 | LOVLIG_OPPHOLD | | | ikke_oppfylt | Ja | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 01.12.2016 | | Oppfylt | | + | 3456 | BOR_MED_SØKER | 01.12.2016 | 01.12.2020 | Oppfylt | | + | 3456 | UNDER_18_ÅR | 01.12.2016 | 30.11.2034 | Oppfylt | | + | 3456 | BOR_MED_SØKER | 02.12.2020 | 30.09.2021 | ikke_oppfylt | Ja | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | | | AVSLAG | Kun søker har avslag | + | 01.01.2021 | 30.09.2021 | AVSLAG | Barn har avslag | + + + Scenario: Skal lage avslagsperioder når søker har konstant avslag og barn har vilkår med overlappende avslag + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 24.12.1987 | + | 1 | 3456 | BARN | 01.12.2016 | + | 1 | 5678 | BARN | 01.02.2017 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET | 24.12.1987 | | Oppfylt | | + | 1234 | LOVLIG_OPPHOLD | | | ikke_oppfylt | Ja | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET | 01.12.2016 | | Oppfylt | | + | 3456 | LOVLIG_OPPHOLD | 01.12.2016 | 30.05.2020 | Oppfylt | | + | 3456 | BOR_MED_SØKER | 01.12.2016 | 01.12.2020 | Oppfylt | | + | 3456 | UNDER_18_ÅR | 01.12.2016 | 30.11.2034 | Oppfylt | | + | 5678 | GIFT_PARTNERSKAP, BOSATT_I_RIKET | 01.12.2017 | | Oppfylt | | + | 5678 | LOVLIG_OPPHOLD | 01.12.2017 | 30.05.2021 | Oppfylt | | + | 5678 | BOR_MED_SØKER | 01.12.2017 | 01.12.2021 | Oppfylt | | + | 5678 | UNDER_18_ÅR | 01.12.2017 | 30.11.2035 | Oppfylt | | + + | 3456 | LOVLIG_OPPHOLD | 08.06.2020 | 30.09.2021 | ikke_oppfylt | Ja | + | 3456 | BOR_MED_SØKER | 02.12.2020 | 31.12.2021 | ikke_oppfylt | Ja | + + | 5678 | LOVLIG_OPPHOLD | 08.06.2021 | 30.09.2022 | ikke_oppfylt | Ja | + | 5678 | BOR_MED_SØKER | 02.12.2021 | 31.12.2022 | ikke_oppfylt | Ja | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | | | AVSLAG | Søker har avslag | + + | 01.07.2020 | 31.12.2020 | AVSLAG | Barn1 har avslag på LOVLIG_OPPHOLD | + | 01.01.2021 | 30.09.2021 | AVSLAG | Barn1 har avslag på to vilkår | + | 01.10.2021 | 31.12.2021 | AVSLAG | Barn1 har avslag på BOR_MED_SØKER | + + | 01.07.2021 | 31.12.2021 | AVSLAG | Barn2 har avslag på LOVLIG_OPPHOLD | + | 01.01.2022 | 30.09.2022 | AVSLAG | Barn2 har avslag på to vilkår | + | 01.10.2022 | 31.12.2022 | AVSLAG | Barn2 har avslag på BOR_MED_SØKER | + + + Scenario: Skal lage opphørsperiode når det er overlapp i opphør og avslag på tvers av tvillinger der en har eksplisitt avslag + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 24.12.1987 | + | 1 | 3456 | BARN | 02.12.2016 | + | 1 | 5678 | BARN | 02.12.2016 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET | 24.12.1987 | | Oppfylt | | + | 1234 | LOVLIG_OPPHOLD | 24.12.1987 | 01.12.2020 | Oppfylt | | + + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 02.12.2016 | | Oppfylt | | + | 3456 | BOR_MED_SØKER | 02.12.2016 | 01.12.2020 | Oppfylt | | + | 3456 | UNDER_18_ÅR | 02.12.2016 | 30.11.2034 | Oppfylt | | + + | 5678 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 02.12.2016 | | Oppfylt | | + | 5678 | BOR_MED_SØKER | 02.12.2016 | 01.12.2020 | Oppfylt | | + | 5678 | UNDER_18_ÅR | 02.12.2016 | 30.11.2034 | Oppfylt | | + + | 1234 | LOVLIG_OPPHOLD | 02.12.2020 | 30.09.2021 | ikke_oppfylt | Ja | + | 1234 | LOVLIG_OPPHOLD | 01.10.2021 | | Oppfylt | | + + | 3456 | BOR_MED_SØKER | 02.12.2020 | 30.09.2021 | ikke_oppfylt | Ja | + | 3456 | BOR_MED_SØKER | 01.10.2021 | | Oppfylt | | + + | 5678 | BOR_MED_SØKER | 02.12.2020 | 30.09.2021 | ikke_oppfylt | | + | 5678 | BOR_MED_SØKER | 01.10.2021 | | Oppfylt | | + + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.12.2016 | 31.12.2020 | 1234 | 1 | + | 3456 | 01.10.2021 | 30.11.2034 | 1234 | 1 | + | 5678 | 01.12.2016 | 31.12.2020 | 1234 | 1 | + | 5678 | 01.10.2021 | 30.11.2034 | 1234 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.01.2017 | 31.12.2020 | Utbetaling | | + | 01.01.2021 | 31.10.2021 | Avslag | Søker har opphør som overlapper med avslag hos barn | + | 01.11.2021 | 30.11.2034 | Utbetaling | | + | 01.12.2034 | | Opphør | Barn er over 18 | + + + Scenario: Skal lage opphørsperiode når ett barn har eksplisitt avslag og det andre har ingen utbetaling i samme periode + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 24.12.1987 | + | 1 | 3456 | BARN | 02.12.2016 | + | 1 | 5678 | BARN | 02.12.2016 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET | 24.12.1987 | | Oppfylt | | + | 1234 | LOVLIG_OPPHOLD | 24.12.1987 | | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 02.12.2016 | | Oppfylt | | + | 3456 | BOR_MED_SØKER | 02.12.2016 | 01.12.2020 | Oppfylt | | + | 3456 | UNDER_18_ÅR | 02.12.2016 | 30.11.2034 | Oppfylt | | + | 5678 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 02.12.2016 | | Oppfylt | | + | 5678 | BOR_MED_SØKER | 02.12.2016 | 01.12.2020 | Oppfylt | | + | 5678 | UNDER_18_ÅR | 02.12.2016 | 30.11.2034 | Oppfylt | | + + | 3456 | BOR_MED_SØKER | 02.12.2020 | 30.09.2021 | ikke_oppfylt | Ja | + | 3456 | BOR_MED_SØKER | 01.10.2021 | | Oppfylt | | + | 5678 | BOR_MED_SØKER | 02.12.2020 | 30.09.2021 | ikke_oppfylt | | + | 5678 | BOR_MED_SØKER | 01.10.2021 | | Oppfylt | | + + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.12.2016 | 31.12.2020 | 1234 | 1 | + | 3456 | 01.10.2021 | 30.11.2034 | 1234 | 1 | + | 5678 | 01.12.2016 | 31.12.2020 | 1234 | 1 | + | 5678 | 01.10.2021 | 30.11.2034 | 1234 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.01.2017 | 31.12.2020 | Utbetaling | | + | 01.01.2021 | 31.10.2021 | OPPHØR | Barn 1 har avslag som overlapper med opphør hos barn2 | + | 01.11.2021 | 30.11.2034 | Utbetaling | | + | 01.12.2034 | | Opphør | Barna er over 18 | \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/endrede_utbetalinger.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/endrede_utbetalinger.feature new file mode 100644 index 000000000..284b3c6ba --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/endrede_utbetalinger.feature @@ -0,0 +1,135 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperioder med endrede utbetalinger + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | ForrigeBehandlingId | + | 1 | | + | 2 | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Skal lage vedtaksperioder for mor med ett barn med endrede utbetalinger + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + + Og med endrede utbetalinger + | AktørId | Fra dato | Til dato | BehandlingId | + | 3456 | 01.05.2021 | 31.03.2038 | 1 | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 30.04.2021 | 1054 | 1 | + | 3456 | 01.05.2021 | 31.03.2038 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 30.04.2021 | Utbetaling | Barn og søker | + | 01.05.2021 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.04.2038 | | Opphør | Kun søker | + + + Scenario: Skal lage opphørsperiode når bor_med_søker-vilkåret ikke er oppfylt ved revurdering + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 15.12.1976 | + | 1 | 3456 | BARN | 16.06.2016 | + | 1 | 5678 | BARN | 11.09.2013 | + | 2 | 1234 | SØKER | 15.12.1976 | + | 2 | 3456 | BARN | 16.06.2016 | + | 2 | 5678 | BARN | 11.09.2013 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 15.12.1976 | | Oppfylt | + | 1234 | UTVIDET_BARNETRYGD | 31.05.2022 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 16.06.2016 | 15.06.2034 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 16.06.2016 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 08.12.2021 | | Oppfylt | + | 5678 | BOSATT_I_RIKET, LOVLIG_OPPHOLD, GIFT_PARTNERSKAP | 11.09.2013 | | Oppfylt | + | 5678 | BOR_MED_SØKER | 08.12.2021 | | Oppfylt | + | 5678 | UNDER_18_ÅR | 11.09.2013 | 10.09.2031 | Oppfylt | + + Og lag personresultater for behandling 2 + Og legg til nye vilkårresultater for behandling 2 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 15.12.1976 | | Oppfylt | + | 1234 | UTVIDET_BARNETRYGD | 31.05.2022 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 16.06.2016 | 15.06.2034 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 16.06.2016 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 08.12.2021 | | IKKE_OPPFYLT | + | 5678 | BOSATT_I_RIKET, LOVLIG_OPPHOLD, GIFT_PARTNERSKAP | 11.09.2013 | | Oppfylt | + | 5678 | BOR_MED_SØKER | 08.12.2021 | | IKKE_OPPFYLT | + | 5678 | UNDER_18_ÅR | 11.09.2013 | 10.09.2031 | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 1234 | 31.05.2022 | 31.06.2034 | 1354 | 1 | + | 3456 | 08.12.2021 | 31.06.2034 | 1354 | 1 | + | 3456 | 08.12.2021 | 31.06.2034 | 1354 | 2 | + | 5678 | 08.12.2021 | 31.10.2031 | 1354 | 1 | + | 5678 | 08.12.2021 | 31.10.2031 | 1354 | 2 | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.01.2022 | | Opphør | Kun søker | + + + Scenario: Skal ta med etterfølgende perioder når den første endres + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 09.08.1991 | + | 1 | 3456 | BARN | 31.10.2015 | + | 2 | 1234 | SØKER | 09.08.1991 | + | 2 | 3456 | BARN | 31.10.2015 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 09.08.1991 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 31.10.2015 | 30.10.2033 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 31.10.2015 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 04.05.2021 | 02.03.2023 | Oppfylt | + | 3456 | BOR_MED_SØKER | 03.03.2023 | | Oppfylt | + + + Og lag personresultater for behandling 2 + Og legg til nye vilkårresultater for behandling 2 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 09.08.1991 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 31.10.2015 | 30.10.2033 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 31.10.2015 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 04.05.2021 | 02.03.2023 | IKKE_OPPFYLT | + | 3456 | BOR_MED_SØKER | 03.03.2023 | | Oppfylt | + + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 1234 | 31.05.2021 | 30.10.2033 | 1354 | 1 | + | 3456 | 31.05.2021 | 30.10.2033 | 1354 | 1 | + | 3456 | 31.04.2023 | 30.10.2033 | 1354 | 2 | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.06.2021 | 31.03.2023 | Opphør | Barnet bodde ikke hos søker i denne perioden allikevel | + | 01.04.2023 | 30.09.2033 | Utbetaling | | + | 01.10.2033 | | Opphør | Over 18 | + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/endret_utbetaling.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/endret_utbetaling.feature new file mode 100644 index 000000000..6138dcd03 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/endret_utbetaling.feature @@ -0,0 +1,103 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperioder med endret utbetaling der endringstidspunkt påvirker periodene + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | + | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 24.12.1987 | + | 1 | 3456 | BARN | 02.12.2016 | + + Scenario: Skal lage ikke utbetalingsperiode når andelene er endret til 0% og det ikke er delt bosted + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 02.12.2016 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 02.12.2016 | 01.12.2034 | Oppfylt | | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | Prosent | + | 3456 | 01.01.2017 | 30.11.2034 | 1234 | 1 | 0 | + + Og med endrede utbetalinger + | AktørId | Fra dato | Til dato | BehandlingId | Årsak | Prosent | + | 3456 | 01.01.2017 | 30.11.2034 | 1 | ETTERBETALING_3ÅR | 0 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.01.2017 | 30.11.2034 | Opphør | Endret utbetaling 0% | + | 01.12.2034 | | Opphør | Opphør 18 år | + + Scenario: Skal lage utbetalingsperiode når andelene er endret til 0% og det er delt bosted + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 02.12.2016 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 02.12.2016 | 01.12.2034 | Oppfylt | | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | Prosent | + | 3456 | 01.01.2017 | 30.11.2034 | 1234 | 1 | 0 | + + Og med endrede utbetalinger + | AktørId | Fra dato | Til dato | BehandlingId | Årsak | prosent | + | 3456 | 01.01.2017 | 30.11.2034 | 1 | DELT_BOSTED | 0 | + + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.01.2017 | 30.11.2034 | Utbetaling | Delt bosted | + | 01.12.2034 | | Opphør | Barn er over 18 | + + Scenario: Skal ikke slå sammen vedtaksperiodene som ikke er innvilget dersom det er på grunn av endret utbetaling + Gitt følgende fagsaker + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende vedtak + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | AVSLÅTT | SØKNAD | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1 | BARN | 02.02.2015 | + | 1 | 2 | SØKER | 17.04.1985 | + + Og følgende dagens dato 27.09.2023 + Og lag personresultater for behandling 1 + + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 2 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 17.04.1985 | | OPPFYLT | Nei | + + | 1 | BOSATT_I_RIKET,GIFT_PARTNERSKAP,LOVLIG_OPPHOLD | | 02.02.2015 | | OPPFYLT | Nei | + | 1 | UNDER_18_ÅR | | 02.02.2015 | 01.02.2033 | OPPFYLT | Nei | + | 1 | BOR_MED_SØKER | | 02.02.2015 | 15.12.2018 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 1 | 1 | 01.03.2015 | 31.12.2018 | 0 | ORDINÆR_BARNETRYGD | 0 | 970 | + + Og med endrede utbetalinger + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent | + | 1 | 1 | 01.03.2015 | 31.12.2018 | ETTERBETALING_3ÅR | 0 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | + | 01.03.2015 | 31.12.2018 | OPPHØR | + | 01.01.2019 | | OPPHØR | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/endringstidspunkt.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/endringstidspunkt.feature new file mode 100644 index 000000000..cb9216b1d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/endringstidspunkt.feature @@ -0,0 +1,240 @@ +# language: no +# encoding: UTF-8 + + +Egenskap: Vedtaksperioder - Endringstidspunkt + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | + | 1 | + | 2 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 24.12.1987 | + | 1 | 3456 | BARN | 02.12.2016 | + | 2 | 1234 | SØKER | 24.12.1987 | + | 2 | 3456 | BARN | 02.12.2016 | + + + Scenario: Skal kun ta med vedtaksperioder som kommer etter + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET | 24.12.1987 | | Oppfylt | | + | 1234 | LOVLIG_OPPHOLD | 24.12.1987 | 01.12.2020 | Oppfylt | | + | 1234 | LOVLIG_OPPHOLD | 02.12.2020 | 30.09.2021 | ikke_oppfylt | Ja | + | 1234 | LOVLIG_OPPHOLD | 01.10.2021 | | Oppfylt | | + + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 02.12.2016 | | Oppfylt | | + | 3456 | BOR_MED_SØKER | 02.12.2016 | 01.12.2020 | Oppfylt | | + | 3456 | UNDER_18_ÅR | 02.12.2016 | 01.12.2034 | Oppfylt | | + | 3456 | BOR_MED_SØKER | 02.12.2020 | 30.09.2021 | ikke_oppfylt | Ja | + | 3456 | BOR_MED_SØKER | 01.10.2021 | | Oppfylt | | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.12.2016 | 31.12.2020 | 1234 | 1 | + | 3456 | 01.10.2021 | 30.11.2034 | 1234 | 1 | + + Og med overstyrt endringstidspunkt + | Endringstidspunkt | BehandlingId | + | 01.11.2021 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.01.2021 | 31.10.2021 | Avslag | Avslag skal alltid med, selv om de er før endringstidspunktet | + | 01.11.2021 | 30.11.2034 | Utbetaling | Etter endringstidspunktet | + | 01.12.2034 | | Opphør | Barn er over 18 | + + Scenario: Skal ta med eøs-perioder som kommer før første periode + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 15.07.2021 | | Oppfylt | | + + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 15.07.2021 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 02.12.2016 | 01.12.2034 | Oppfylt | | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.08.2021 | 30.11.2034 | 1234 | 1 | + + Og med overstyrt endringstidspunkt + | Endringstidspunkt | BehandlingId | + | 01.11.2021 | 1 | + + Og lag personresultater for behandling 2 + Og legg til nye vilkårresultater for behandling 2 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 15.07.2021 | | Oppfylt | + + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 15.07.2021 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 02.12.2016 | 01.12.2034 | Oppfylt | + + | 1234 | BOSATT_I_RIKET | 15.06.2021 | 14.07.2021 | Oppfylt | + | 1234 | LOVLIG_OPPHOLD | 15.06.2021 | 14.07.2021 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP | 15.06.2021 | 14.07.2021 | Oppfylt | + | 3456 | BOSATT_I_RIKET | 15.06.2021 | 14.07.2021 | Oppfylt | + | 3456 | LOVLIG_OPPHOLD | 15.06.2021 | 14.07.2021 | Oppfylt | + | 3456 | BOR_MED_SØKER | 15.06.2021 | 14.07.2021 | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.07.2021 | 31.07.2021 | 0 | 2 | + | 3456 | 01.08.2021 | 30.11.2034 | 1234 | 2 | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.07.2021 | 31.07.2021 | Utbetaling | Sekundærland EØS | + | 01.08.2021 | 30.11.2034 | Utbetaling | | + | 01.12.2034 | | Opphør | Barn er over 18 | + + Scenario: Skal ikke se på endring i avslåtte vilkår fra forrige behandling når vi beregner endringstidspunktet + Gitt følgende fagsaker + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende vedtak + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 1 | 1 | | + | 2 | 1 | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1 | SØKER | 16.02.1985 | + | 1 | 2 | BARN | 23.04.2017 | + | 1 | 3 | BARN | 22.03.2015 | + | 2 | 1 | SØKER | 16.02.1985 | + | 2 | 2 | BARN | 23.04.2017 | + | 2 | 3 | BARN | 22.03.2015 | + + Og følgende dagens dato 19.09.2023 + Og lag personresultater for behandling 1 + Og lag personresultater for behandling 2 + + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1 | UTVIDET_BARNETRYGD | | | | IKKE_OPPFYLT | Ja | + | 1 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 16.02.1985 | 15.02.2023 | OPPFYLT | Nei | + + | 3 | LOVLIG_OPPHOLD,BOR_MED_SØKER,BOSATT_I_RIKET,GIFT_PARTNERSKAP | | 22.03.2015 | | OPPFYLT | Nei | + | 3 | UNDER_18_ÅR | | 22.03.2015 | 21.03.2033 | OPPFYLT | Nei | + + | 2 | GIFT_PARTNERSKAP,LOVLIG_OPPHOLD,BOSATT_I_RIKET,BOR_MED_SØKER | | 23.04.2017 | | OPPFYLT | Nei | + | 2 | UNDER_18_ÅR | | 23.04.2017 | 22.04.2035 | OPPFYLT | Nei | + + Og legg til nye vilkårresultater for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | | 16.02.1985 | 15.02.2023 | OPPFYLT | Nei | + + | 2 | BOSATT_I_RIKET,GIFT_PARTNERSKAP,LOVLIG_OPPHOLD | | 23.04.2017 | | OPPFYLT | Nei | + | 2 | BOR_MED_SØKER | | 23.04.2017 | 08.01.2023 | OPPFYLT | Nei | + | 2 | UNDER_18_ÅR | | 23.04.2017 | 22.04.2035 | OPPFYLT | Nei | + | 2 | BOR_MED_SØKER | DELT_BOSTED_SKAL_IKKE_DELES | 09.01.2023 | | OPPFYLT | Nei | + + | 3 | LOVLIG_OPPHOLD,BOSATT_I_RIKET,GIFT_PARTNERSKAP | | 22.03.2015 | | OPPFYLT | Nei | + | 3 | BOR_MED_SØKER | | 22.03.2015 | 08.01.2023 | OPPFYLT | Nei | + | 3 | UNDER_18_ÅR | | 22.03.2015 | 21.03.2033 | OPPFYLT | Nei | + | 3 | BOR_MED_SØKER | DELT_BOSTED | 09.01.2023 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 2 | 1 | 01.05.2017 | 28.02.2019 | 970 | ORDINÆR_BARNETRYGD | 100 | 970 | + | 2 | 1 | 01.03.2019 | 31.08.2020 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 2 | 1 | 01.09.2020 | 31.08.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 2 | 1 | 01.09.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | 1654 | + | 2 | 1 | 01.01.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + + | 3 | 1 | 01.04.2015 | 28.02.2019 | 970 | ORDINÆR_BARNETRYGD | 100 | 970 | + | 3 | 1 | 01.03.2019 | 31.08.2020 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 3 | 1 | 01.09.2020 | 28.02.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 3 | 1 | 01.03.2021 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + + | 3 | 2 | 01.04.2015 | 28.02.2019 | 970 | ORDINÆR_BARNETRYGD | 100 | 970 | + | 3 | 2 | 01.03.2019 | 31.08.2020 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 3 | 2 | 01.09.2020 | 28.02.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 3 | 2 | 01.03.2021 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + + | 2 | 2 | 01.05.2017 | 28.02.2019 | 970 | ORDINÆR_BARNETRYGD | 100 | 970 | + | 2 | 2 | 01.03.2019 | 31.08.2020 | 1054 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 2 | 2 | 01.09.2020 | 31.08.2021 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 2 | 2 | 01.09.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | 1654 | + | 2 | 2 | 01.01.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | 1676 | + + Og med endrede utbetalinger + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent | + | 3 | 2 | 01.02.2023 | 28.02.2023 | DELT_BOSTED | 100 | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | + | 2023-02-01 | 2023-02-28 | UTBETALING | + | 2023-03-01 | | OPPHØR | + + Scenario: Avslag i behandlingen skal ikke påvirke endringstidspunktet + Gitt følgende fagsaker + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende vedtak + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | ENDRET_OG_FORTSATT_INNVILGET | SØKNAD | + | 2 | 1 | 1 | AVSLÅTT | SØKNAD | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1 | SØKER | 25.11.1987 | + | 1 | 2 | BARN | 19.06.2017 | + | 2 | 1 | SØKER | 25.11.1987 | + | 2 | 2 | BARN | 19.06.2017 | + + Og følgende dagens dato 20.09.2023 + Og lag personresultater for behandling 1 + Og lag personresultater for behandling 2 + + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 01.09.2020 | 15.05.2035 | OPPFYLT | Nei | + + | 2 | UNDER_18_ÅR | | 19.06.2017 | 18.06.2035 | OPPFYLT | Nei | + | 2 | BOSATT_I_RIKET,LOVLIG_OPPHOLD,GIFT_PARTNERSKAP | | 19.06.2017 | | OPPFYLT | Nei | + | 2 | BOR_MED_SØKER | | 19.06.2017 | 31.08.2020 | OPPFYLT | Nei | + | 2 | BOR_MED_SØKER | DELT_BOSTED | 01.09.2020 | | OPPFYLT | Nei | + + Og legg til nye vilkårresultater for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1 | UTVIDET_BARNETRYGD | | | | IKKE_OPPFYLT | Ja | + | 1 | LOVLIG_OPPHOLD,BOSATT_I_RIKET | | 01.09.2020 | 15.05.2035 | OPPFYLT | Nei | + + | 2 | BOR_MED_SØKER | | 19.06.2017 | 31.08.2020 | OPPFYLT | Nei | + | 2 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP,BOSATT_I_RIKET | | 19.06.2017 | | OPPFYLT | Nei | + | 2 | UNDER_18_ÅR | | 19.06.2017 | 18.06.2035 | OPPFYLT | Nei | + | 2 | BOR_MED_SØKER | DELT_BOSTED | 01.09.2020 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 2 | 1 | 01.09.2020 | 30.09.2020 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 2 | 1 | 01.10.2020 | 31.05.2035 | 0 | ORDINÆR_BARNETRYGD | 0 | 1354 | + + | 2 | 2 | 01.09.2020 | 30.09.2020 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + | 2 | 2 | 01.10.2020 | 31.05.2035 | 0 | ORDINÆR_BARNETRYGD | 0 | 1354 | + + Og med endrede utbetalinger + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent | + | 2 | 1 | 01.10.2020 | 01.05.2035 | DELT_BOSTED | 0 | + | 2 | 2 | 01.10.2020 | 01.05.2035 | DELT_BOSTED | 0 | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | + | 01.06.2035 | | OPPHØR | + | | | AVSLAG | \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/enslig_mindre\303\245rig.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/enslig_mindre\303\245rig.feature" new file mode 100644 index 000000000..d99447c76 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/enslig_mindre\303\245rig.feature" @@ -0,0 +1,62 @@ +# language: no +# encoding: UTF-8 + + +Egenskap: Vedtaksperioder for enslig mindreårig + + Bakgrunn: + Gitt følgende fagsaker + | FagsakId | Fagsaktype | + | 1 | BARN_ENSLIG_MINDREÅRIG | + + Gitt følgende vedtak + | BehandlingId | FagsakId | + | 1 | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 3456 | BARN | 02.12.2016 | + + + Scenario: Enslig barn har utvidet oppfylt + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 02.12.2016 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 02.12.2016 | 01.12.2034 | Oppfylt | + | 3456 | UTVIDET_BARNETRYGD | 02.12.2016 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | Ytelse type | + | 3456 | 01.12.2016 | 30.11.2034 | 1234 | 1 | Ordinær_barnetrygd | + | 3456 | 01.12.2016 | 30.11.2034 | 2000 | 1 | Utvidet_barnetrygd | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.01.2017 | 30.11.2034 | Utbetaling | | + | 01.12.2034 | | Opphør | Barn er over 18 | + + + Scenario: Enslig barn har rett til barnetrygd oppfylt, men avslag på utvidet barnetrygd + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 02.12.2016 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 02.12.2016 | 01.12.2034 | Oppfylt | | + | 3456 | UTVIDET_BARNETRYGD | | | ikke_oppfylt | Ja | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | Ytelse type | + | 3456 | 01.12.2016 | 30.11.2034 | 1234 | 1 | Ordinær_barnetrygd | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.01.2017 | 30.11.2034 | Utbetaling | | + | 01.12.2034 | | Opphør | Barn er over 18 | + | | | Avslag | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/filtrer_vekk_ikke_innvilgete_perioder.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/filtrer_vekk_ikke_innvilgete_perioder.feature new file mode 100644 index 000000000..007f1cd96 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/filtrer_vekk_ikke_innvilgete_perioder.feature @@ -0,0 +1,94 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperioder skal filtrere vekk irrelevante perioder + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | + | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Skal kun ta med første opphørsperiode etter siste utbetalingsperiode. Eksplisitte avslag skal med uansett. + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET | 13.04.2020 | 06.06.2021 | Oppfylt | | + | 3456 | LOVLIG_OPPHOLD | 13.04.2020 | 08.08.2021 | Oppfylt | | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 08.08.2021 | Oppfylt | | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + | 3456 | BOR_MED_SØKER | 01.09.2021 | 04.10.2022 | ikke_oppfylt | Ja | + | 3456 | BOR_MED_SØKER | 05.10.2022 | | ikke_oppfylt | | + + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1054 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 30.06.2021 | Utbetaling | Barn1 og søker | + | 01.07.2021 | | Opphør | Første opphør etter siste utbetalingsperiode | + | 01.10.2021 | 31.10.2022 | Avslag | Eksplisitt avslag skal med selv om de er etter siste utbetalingsperiode | + + + Scenario: Skal ikke fjerne perioder når siste periode er ikke-innvilget + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1054 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.03.2038 | Utbetaling | Barn1 og søker | + | 01.04.2038 | | Opphør | Første opphør | + + + Scenario: Skal ikke fjerne perioder når det kun er innvilgete perioder + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1054 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.03.2038 | Utbetaling | Barn1 og søker | + | 01.04.2038 | | Opphør | Første opphør | + + + + + + + + + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/fortsatt_innvilget.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/fortsatt_innvilget.feature new file mode 100644 index 000000000..10dd31ff7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/fortsatt_innvilget.feature @@ -0,0 +1,77 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperioder for fortsatt innvilget + + Bakgrunn: + Gitt følgende fagsaker + | FagsakId | Fagsaktype | + | 1 | NORMAL | + + Gitt følgende vedtak + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | INNVILGET | SØKNAD | + | 2 | 1 | 1 | FORTSATT_INNVILGET | ÅRLIG_KONTROLL | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1 | SØKER | 27.04.1991 | + | 1 | 2 | BARN | 07.10.2021 | + | 2 | 1 | SØKER | 27.04.1991 | + | 2 | 2 | BARN | 07.10.2021 | + + Scenario: Skal gi riktige perioder når behandlingsresultatet er fortsatt innvilget + Og lag personresultater for behandling 1 + Og lag personresultater for behandling 2 + + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 2 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP | | 07.10.2021 | | OPPFYLT | Nei | + | 2 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 07.10.2021 | | OPPFYLT | Nei | + | 2 | UNDER_18_ÅR | | 07.10.2021 | 06.10.2039 | OPPFYLT | Nei | + | 2 | BOSATT_I_RIKET | BARN_BOR_I_EØS | 07.10.2021 | | OPPFYLT | Nei | + + | 1 | LOVLIG_OPPHOLD | | 27.04.1991 | | OPPFYLT | Nei | + | 1 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 11.05.2021 | | OPPFYLT | Nei | + + Og legg til nye vilkårresultater for behandling 2 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1 | LOVLIG_OPPHOLD | | 27.04.1991 | | OPPFYLT | Nei | + | 1 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 11.05.2021 | | OPPFYLT | Nei | + + | 2 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 07.10.2021 | | OPPFYLT | Nei | + | 2 | LOVLIG_OPPHOLD,GIFT_PARTNERSKAP | | 07.10.2021 | | OPPFYLT | Nei | + | 2 | BOSATT_I_RIKET | BARN_BOR_I_EØS | 07.10.2021 | | OPPFYLT | Nei | + | 2 | UNDER_18_ÅR | | 07.10.2021 | 06.10.2039 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | + | 2 | 1 | 01.11.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 1 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 1 | 01.07.2023 | 30.09.2027 | 1766 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 1 | 01.10.2027 | 30.09.2039 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 1 | 01.03.2022 | 30.11.2022 | 553 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 1 | 01.01.2022 | 28.02.2022 | 1676 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 1 | 01.12.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 2 | 01.11.2021 | 31.12.2021 | 1654 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 2 | 01.03.2023 | 30.06.2023 | 1723 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 2 | 01.07.2023 | 30.09.2027 | 1766 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 2 | 01.10.2027 | 30.09.2039 | 1310 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 2 | 01.01.2022 | 28.02.2022 | 1676 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 2 | 01.03.2022 | 30.11.2022 | 553 | ORDINÆR_BARNETRYGD | 100 | + | 2 | 2 | 01.12.2022 | 28.02.2023 | 1676 | ORDINÆR_BARNETRYGD | 100 | + + Og med kompetanser + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland | + | 2 | 01.11.2021 | 28.02.2022 | NORGE_ER_PRIMÆRLAND | 1 | ARBEIDER | INAKTIV | NO | PL | PL | + | 2 | 01.03.2022 | 30.11.2022 | NORGE_ER_SEKUNDÆRLAND | 1 | ARBEIDER | I_ARBEID | NO | PL | PL | + | 2 | 01.12.2022 | | NORGE_ER_PRIMÆRLAND | 1 | ARBEIDER | INAKTIV | NO | PL | PL | + | 2 | 01.11.2021 | 28.02.2022 | NORGE_ER_PRIMÆRLAND | 2 | ARBEIDER | INAKTIV | NO | PL | PL | + | 2 | 01.03.2022 | 30.11.2022 | NORGE_ER_SEKUNDÆRLAND | 2 | ARBEIDER | I_ARBEID | NO | PL | PL | + | 2 | 01.12.2022 | | NORGE_ER_PRIMÆRLAND | 2 | ARBEIDER | INAKTIV | NO | PL | PL | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | + | | | FORTSATT_INNVILGET | diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/innvilget-vilk\303\245r-i-opph\303\270speriode-skal-ikke-splitte.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/innvilget-vilk\303\245r-i-opph\303\270speriode-skal-ikke-splitte.feature" new file mode 100644 index 000000000..61413baa5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/innvilget-vilk\303\245r-i-opph\303\270speriode-skal-ikke-splitte.feature" @@ -0,0 +1,43 @@ +# language: no +# encoding: UTF-8 + + +Egenskap: Vedtaksperioder for opphørsperioder skal håndtere vilkårsendringer + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | + | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 24.12.1987 | + | 1 | 3456 | BARN | 01.02.2016 | + + Scenario: Skal kun lage én opphørsperiode selv om et vilkår blir oppfylt og det fortsatt er opphør + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET | 24.12.1987 | | Oppfylt | + | 1234 | LOVLIG_OPPHOLD | 24.12.1987 | 04.04.2019 | Oppfylt | + | 1234 | LOVLIG_OPPHOLD | 07.07.2021 | | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 01.12.2016 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 01.12.2016 | 04.04.2019 | Oppfylt | + | 3456 | BOR_MED_SØKER | 08.08.2022 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 01.12.2016 | 30.11.2034 | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.12.2016 | 31.04.2019 | 1054 | 1 | + | 3456 | 01.05.2019 | 31.08.2022 | 1354 | 1 | + | 3456 | 01.09.2022 | 30.11.2034 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | + | 01.01.2017 | 30.04.2019 | Utbetaling | + | 01.05.2019 | 31.08.2022 | Opphør | + | 01.09.2022 | 31.01.2034 | Utbetaling | + | 01.02.2034 | | Opphør | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/institusjon.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/institusjon.feature new file mode 100644 index 000000000..dfa9da5e1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/institusjon.feature @@ -0,0 +1,43 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperioder for institusjonssaker + + Bakgrunn: + Gitt følgende fagsaker + | FagsakId | Fagsaktype | + | 1 | INSTITUSJON | + + Gitt følgende vedtak + | BehandlingId | FagsakId | ForrigeBehandlingId | Behandlingsresultat | Behandlingsårsak | + | 1 | 1 | | IKKE_VURDERT | SØKNAD | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1 | BARN | 10.01.2018 | + + Scenario: Skal kunne endre utbetalingen til 0 prosent + Og følgende dagens dato 26.09.2023 + Og lag personresultater for behandling 1 + + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1 | UNDER_18_ÅR | | 10.01.2018 | 09.01.2036 | OPPFYLT | Nei | + | 1 | GIFT_PARTNERSKAP,BOSATT_I_RIKET,LOVLIG_OPPHOLD,BOR_MED_SØKER | | 15.08.2020 | 15.10.2020 | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 1 | 1 | 01.09.2020 | 30.09.2020 | 0 | ORDINÆR_BARNETRYGD | 0 | 970 | + | 1 | 1 | 01.10.2020 | 31.10.2020 | 1354 | ORDINÆR_BARNETRYGD | 100 | 1354 | + + Og med endrede utbetalinger + | AktørId | BehandlingId | Fra dato | Til dato | Årsak | Prosent | + | 1 | 1 | 01.09.2018 | 30.09.2020 | ETTERBETALING_3ÅR | 0 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.09.2020 | 30.09.2020 | OPPHØR | Etterbetaling 3 år | + | 01.10.2020 | 31.10.2020 | UTBETALING | | + | 01.11.2020 | | OPPHØR | | \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/kompetanser.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/kompetanser.feature new file mode 100644 index 000000000..6e7b31fbe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/kompetanser.feature @@ -0,0 +1,70 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperioder med kompetanser + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | + | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Skal lage vedtaksperioder for mor med ett barn med kompetanser + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + + Og med kompetanser + | AktørId | Fra dato | Til dato | Resultat |BehandlingId | + | 3456 | 01.05.2020 | 30.04.2021 | NORGE_ER_PRIMÆRLAND |1 | + | 3456 | 01.05.2021 | 31.03.2038 | NORGE_ER_SEKUNDÆRLAND |1 | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 30.04.2021 | 1054 | 1 | + | 3456 | 01.05.2021 | 31.03.2038 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 30.04.2021 | Utbetaling | Barn og søker | + | 01.05.2021 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.04.2038 | | Opphør | Kun søker | + + Scenario: Skal kunne ha kompetanse uten tom + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 04.09.2020 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.10.1987 | | Oppfylt | + | 3456 | BOSATT_I_RIKET, BOR_MED_SØKER, LOVLIG_OPPHOLD, GIFT_PARTNERSKAP | 04.09.2020 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 04.09.2020 | 03.09.2038 | Oppfylt | + + Og med kompetanser + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | + | 3456 | 01.10.2020 | | NORGE_ER_SEKUNDÆRLAND | 1 | + + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.10.2020 | 31.08.2038 | 0 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.10.2020 | 31.08.2038 | Utbetaling | Barn og søker | + | 01.09.2038 | | Opphør | Barn over 18 | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/kompetanser_to_barn.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/kompetanser_to_barn.feature new file mode 100644 index 000000000..d7c191b85 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/kompetanser_to_barn.feature @@ -0,0 +1,48 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperioder med kompetanser for flere barn + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | + | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + | 1 | 7890 | BARN | 07.12.2022 | + + Scenario: Skal lage vedtaksperioder for mor med to barn med kompetanser + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 7890 | UNDER_18_ÅR | 07.12.2022 | 06.12.2040 | Oppfylt | + | 7890 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 07.12.2022 | | Oppfylt | + + Og med kompetanser + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | + | 3456 | 01.05.2020 | 31.12.2022 | NORGE_ER_PRIMÆRLAND | 1 | + | 3456, 7890 | 01.01.2023 | 30.04.2023 | NORGE_ER_SEKUNDÆRLAND | 1 | + | 3456, 7890 | 01.05.2023 | 31.03.2038 | NORGE_ER_PRIMÆRLAND | 1 | + | 7890 | 01.04.2038 | 30.11.2040 | NORGE_ER_SEKUNDÆRLAND | 1 | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1054 | 1 | + | 7890 | 01.01.2023 | 30.11.2040 | 1354 | 1 | + + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.12.2022 | Utbetaling | Barn og søker | + | 01.01.2023 | 30.04.2023 | Utbetaling | Barna og søker | + | 01.05.2023 | 31.03.2038 | Utbetaling | Barna og søker | + | 01.04.2038 | 30.11.2040 | Utbetaling | Barn og søker | + | 01.12.2040 | | Opphør | Kun søker | diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/overgangst\303\270ndad.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/overgangst\303\270ndad.feature" new file mode 100644 index 000000000..27c5a298e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/overgangst\303\270ndad.feature" @@ -0,0 +1,86 @@ +# language: no +# encoding: UTF-8 + + +Egenskap: Vedtaksperioder med overgangsstønad + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | + | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 24.12.1987 | + | 1 | 3456 | BARN | 01.07.2023 | + + + Scenario: Skal ikke splitte på overgangsstønad dersom det ikke påvirker fagsaken + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD, BOSATT_I_RIKET | 24.12.1987 | | Oppfylt | | + | 1234 | UTVIDET_BARNETRYGD | 01.07.2023 | 30.06.2029 | Oppfylt | | + + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 01.07.2023 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 01.07.2023 | 30.06.2041 | Oppfylt | | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | Ytelse type | + | 1234 | 01.08.2023 | 30.06.2029 | 2516 | 1 | UTVIDET_BARNETRYGD | + | 1234 | 01.08.2023 | 30.06.2026 | 696 | 1 | SMÅBARNSTILLEGG | + + | 3456 | 01.08.2023 | 30.06.2029 | 1766 | 1 | ORDINÆR_BARNETRYGD | + | 3456 | 01.07.2029 | 30.11.2041 | 1310 | 1 | ORDINÆR_BARNETRYGD | + + Og med overgangsstønad + | AktørId | Fra dato | Til dato | BehandlingId | + | 1234 | 01.08.2023 | 30.06.2024 | 1 | + | 1234 | 01.08.2024 | 30.06.2031 | 1 | + | 1234 | 01.09.2034 | 30.06.2036 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.08.2023 | 30.06.2024 | Utbetaling | | + | 01.07.2024 | 31.07.2024 | Utbetaling | På grunn av splitt i overgangsstønaden | + | 01.08.2024 | 30.06.2026 | Utbetaling | På grunn av splitt i overgangsstønaden | + | 01.07.2026 | 30.06.2029 | Utbetaling | Yngste barn over 3 år. Mister småbarnstillegg | + | 01.07.2029 | 30.06.2041 | Utbetaling | Barn over 6 år | + | 01.07.2041 | | Opphør | Barn er over 18 | + + + Scenario: Skal ikke splitte på overgangsstønad dersom andelene på småbarnstillegg er satt til 0 prosent + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD, BOSATT_I_RIKET | 24.12.1987 | | Oppfylt | | + | 1234 | UTVIDET_BARNETRYGD | 01.07.2023 | 30.06.2029 | Oppfylt | | + + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 01.07.2023 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 01.07.2023 | 30.06.2041 | Oppfylt | | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | Ytelse type | + | 1234 | 01.08.2023 | 30.06.2029 | 2516 | 1 | UTVIDET_BARNETRYGD | + | 1234 | 01.08.2023 | 30.06.2026 | 0 | 1 | SMÅBARNSTILLEGG | + + | 3456 | 01.08.2023 | 30.06.2029 | 0 | 1 | ORDINÆR_BARNETRYGD | + | 3456 | 01.07.2029 | 30.11.2041 | 1310 | 1 | ORDINÆR_BARNETRYGD | + + Og med overgangsstønad + | AktørId | Fra dato | Til dato | BehandlingId | + | 1234 | 01.08.2023 | 30.06.2024 | 1 | + | 1234 | 01.08.2024 | 30.06.2031 | 1 | + | 1234 | 01.09.2034 | 30.06.2036 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.08.2023 | 30.06.2026 | Utbetaling | | + | 01.07.2026 | 30.06.2029 | Utbetaling | Yngste barn over 3 år. Mister småbarnstillegg | + | 01.07.2029 | 30.06.2041 | Utbetaling | Barn over 6 år | + | 01.07.2041 | | Opphør | Barn er over 18 | + diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/p\303\245f\303\270lgende-opph\303\270rsperioder-merges.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/p\303\245f\303\270lgende-opph\303\270rsperioder-merges.feature" new file mode 100644 index 000000000..584706678 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/p\303\245f\303\270lgende-opph\303\270rsperioder-merges.feature" @@ -0,0 +1,73 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperioder med reduksjon fra forrige periode eller behandling + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | ForrigeBehandlingId | + | 1 | | + | 2 | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 22.02.1988 | + | 1 | 3456 | BARN | 27.05.2005 | + | 1 | 5678 | BARN | 06.10.2007 | + | 2 | 1234 | SØKER | 22.02.1988 | + | 2 | 3456 | BARN | 27.05.2005 | + | 2 | 5678 | BARN | 06.10.2007 | + + Scenario: Skal ikke splitte når det er reduksjon fra forrige periode selv om det er reduksjon fra forrige behandling + Og følgende dagens dato 18.09.2023 + Og lag personresultater for behandling 1 + Og lag personresultater for behandling 2 + + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | 22.02.1988 | | OPPFYLT | + + | 3456 | BOSATT_I_RIKET,LOVLIG_OPPHOLD,GIFT_PARTNERSKAP | 27.05.2005 | | OPPFYLT | + | 3456 | UNDER_18_ÅR | 27.05.2005 | 26.05.2023 | OPPFYLT | + | 3456 | BOR_MED_SØKER | 15.03.2022 | | OPPFYLT | + + | 5678 | GIFT_PARTNERSKAP,BOSATT_I_RIKET,LOVLIG_OPPHOLD | 06.10.2007 | | OPPFYLT | + | 5678 | UNDER_18_ÅR | 06.10.2007 | 05.10.2025 | OPPFYLT | + | 5678 | BOR_MED_SØKER | 15.03.2022 | | OPPFYLT | + + + Og legg til nye vilkårresultater for behandling 2 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET,LOVLIG_OPPHOLD | 22.02.1988 | | OPPFYLT | + + | 3456 | GIFT_PARTNERSKAP,BOSATT_I_RIKET,LOVLIG_OPPHOLD | 27.05.2005 | | OPPFYLT | + | 3456 | UNDER_18_ÅR | 27.05.2005 | 26.05.2023 | OPPFYLT | + | 3456 | BOR_MED_SØKER | 15.03.2022 | 15.03.2023 | OPPFYLT | + + | 5678 | LOVLIG_OPPHOLD,BOSATT_I_RIKET,GIFT_PARTNERSKAP | 06.10.2007 | | OPPFYLT | + | 5678 | UNDER_18_ÅR | 06.10.2007 | 05.10.2025 | OPPFYLT | + | 5678 | BOR_MED_SØKER | 15.03.2022 | | OPPFYLT | + + Og med andeler tilkjent ytelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Sats | + | 3456 | 1 | 01.04.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 1054 | + | 3456 | 1 | 01.03.2023 | 30.04.2023 | 1083 | ORDINÆR_BARNETRYGD | 1083 | + + | 5678 | 1 | 01.04.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 1054 | + | 5678 | 1 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 1083 | + | 5678 | 1 | 01.07.2023 | 30.09.2025 | 1310 | ORDINÆR_BARNETRYGD | 1310 | + + | 3456 | 2 | 01.04.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 1054 | + | 3456 | 2 | 01.03.2023 | 31.03.2023 | 1083 | ORDINÆR_BARNETRYGD | 1083 | + + | 5678 | 2 | 01.04.2022 | 28.02.2023 | 1054 | ORDINÆR_BARNETRYGD | 1054 | + | 5678 | 2 | 01.03.2023 | 30.06.2023 | 1083 | ORDINÆR_BARNETRYGD | 1083 | + | 5678 | 2 | 01.07.2023 | 30.09.2025 | 1310 | ORDINÆR_BARNETRYGD | 1310 | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.04.2023 | 30.06.2023 | UTBETALING | | + | 01.07.2023 | 30.09.2025 | UTBETALING | | + | 01.10.2025 | | OPPHØR | | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/sammenlign_mot_forrige_behandling.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/sammenlign_mot_forrige_behandling.feature new file mode 100644 index 000000000..c1b5dae44 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/sammenlign_mot_forrige_behandling.feature @@ -0,0 +1,140 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperiode for behandling som opphører perioder fra forrige behandling + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | ForrigeBehandlingId | + | 1 | | + | 2 | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1,2 | 1234 | SØKER | 11.01.1970 | + | 1,2 | 3456 | BARN | 13.04.2020 | + + Scenario: Skal lage vedtaksperioder for revurdering mot forrige behandling hvor det viser seg at barnet ikke bodde hos mor det første året. + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + + Og lag personresultater for behandling 2 + Og legg til nye vilkårresultater for behandling 2 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 12.01.2021 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + | 3456 | 01.02.2021 | 31.03.2038 | 1354 | 2 | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.01.2021 | Opphør | Barnetrygd for Barn 3456 opphører fra forrige behandling | + | 01.02.2021 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.04.2038 | | Opphør | Kun søker | + + Scenario: Skal lage vedtaksperioder for revurdering mot forrige behandling hvor ett barn mister andel i perioden + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1,2 | 1234 | SØKER | 11.01.1970 | + | 1,2 | 3456 | BARN | 13.04.2020 | + | 1,2 | 5678 | BARN | 13.04.2021 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 5678 | UNDER_18_ÅR | 13.04.2021 | 12.04.2039 | Oppfylt | + | 5678 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2021 | | Oppfylt | + + Og lag personresultater for behandling 2 + Og legg til nye vilkårresultater for behandling 2 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | + | 5678 | UNDER_18_ÅR | 13.04.2021 | 12.04.2039 | Oppfylt | + | 5678 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2021 | | Oppfylt | + | 5678 | BOR_MED_SØKER | 12.01.2022 | | Oppfylt | + + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 2 | + | 5678 | 01.05.2021 | 31.03.2039 | 1354 | 1 | + | 5678 | 01.02.2022 | 31.03.2039 | 1354 | 2 | + + Og med overstyrt endringstidspunkt + | Endringstidspunkt | BehandlingId | + | 01.01.2021 | 2 | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 30.04.2021 | Utbetaling | Barn 3456 og søker har ordinære vilkår oppfylt | + | 01.05.2021 | 31.01.2022 | UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING | Reduksjon. Barn 5678 mister utbetaling fra forrige behandling. TODO: Typen skal være Utbetaling når ny begrunnelsesløsning er inne | + | 01.02.2022 | 31.03.2038 | Utbetaling | Utbetaling begge barn | + | 01.04.2038 | 31.03.2039 | Utbetaling | Kun barn 5678 | + | 01.04.2039 | | Opphør | Kun søker har vilkår oppfylt | + + Scenario: Skal lage vedtaksperioder for revurdering mot forrige behandling hvor gjeldende behandling har opphør av flere grunner enn forrige. + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 31.12.2020 | Oppfylt | + + | 3456 | BOR_MED_SØKER | 01.01.2021 | 31.12.2021 | ikke_oppfylt | + | 3456 | BOR_MED_SØKER | 01.01.2022 | | Oppfylt | + + Og lag personresultater for behandling 2 + Og legg til nye vilkårresultater for behandling 2 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOSATT_I_RIKET | 13.04.2020 | 15.07.2021 | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 31.12.2020 | Oppfylt | + + | 3456 | BOR_MED_SØKER | 01.01.2021 | 31.12.2021 | ikke_oppfylt | + | 3456 | BOSATT_I_RIKET | 16.07.2021 | 31.12.2021 | ikke_oppfylt | + | 3456 | BOSATT_I_RIKET | 01.01.2022 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 01.01.2022 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.12.2020 | 1354 | 1 | + | 3456 | 01.02.2022 | 31.03.2038 | 1354 | 1 | + | 3456 | 01.05.2020 | 31.12.2020 | 1354 | 2 | + | 3456 | 01.02.2022 | 31.03.2038 | 1354 | 2 | + + Og med overstyrt endringstidspunkt + | Endringstidspunkt | BehandlingId | + | 01.01.2020 | 2 | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.12.2020 | Utbetaling | | + | 01.01.2021 | 31.01.2022 | Opphør | Kun søker | + | 01.02.2022 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.04.2038 | | Opphør | Kun søker | + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/sammenlign_mot_forrige_behandling_kompetanser.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/sammenlign_mot_forrige_behandling_kompetanser.feature new file mode 100644 index 000000000..113bb69a1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/sammenlign_mot_forrige_behandling_kompetanser.feature @@ -0,0 +1,66 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperiode for behandling som revurderer kompetanse eller endret utbetaling + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | ForrigeBehandlingId | + | 1 | | + | 2 | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1,2 | 1234 | SØKER | 11.01.1970 | + | 1,2 | 3456 | BARN | 13.04.2020 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + + Og lag personresultater for behandling 2 + Og legg til nye vilkårresultater for behandling 2 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 2 | + + Scenario: Skal lage vedtaksperioder for revurdering mot forrige behandling med endring i kompetanse. + + Og med kompetanser + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | + | 3456 | 01.05.2020 | 30.04.2021 | NORGE_ER_PRIMÆRLAND | 1 | + | 3456 | 01.05.2021 | 31.03.2038 | NORGE_ER_SEKUNDÆRLAND | 1 | + | 3456 | 01.05.2020 | 30.06.2021 | NORGE_ER_PRIMÆRLAND | 2 | + | 3456 | 01.07.2021 | 31.03.2038 | NORGE_ER_SEKUNDÆRLAND | 2 | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 30.06.2021 | Utbetaling | Norge primærland | + | 01.07.2021 | 31.03.2038 | Utbetaling | Norge sekundærland | + | 01.04.2038 | | Opphør | Kun søker | + + Scenario: Skal lage vedtaksperioder for revurdering mot forrige behandling med endring i endrede utbetalinger. + + Og med endrede utbetalinger + | AktørId | Fra dato | Til dato | BehandlingId | + | 3456 | 01.05.2021 | 31.03.2038 | 1 | + | 3456 | 01.07.2021 | 31.03.2038 | 2 | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 30.06.2021 | Utbetaling | Norge primærland | + | 01.07.2021 | 31.03.2038 | Utbetaling | Norge sekundærland | + | 01.04.2038 | | Opphør | Kun søker | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/sammenlign_mot_forrige_behandling_null.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/sammenlign_mot_forrige_behandling_null.feature new file mode 100644 index 000000000..658663aeb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/sammenlign_mot_forrige_behandling_null.feature @@ -0,0 +1,45 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperiode for behandling med opphør fra start + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | ForrigeBehandlingId | + | 1 | | + | 2 | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1,2 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + | 2 | 3456 | BARN | 12.05.2020 | + + Scenario: Vedtaksperiode der barn får endret fødselsdato + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + + Og lag personresultater for behandling 2 + Og legg til nye vilkårresultater for behandling 2 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 12.05.2020 | 11.05.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 12.05.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 12.05.2020 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + | 3456 | 01.06.2020 | 31.04.2038 | 1354 | 2 | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.05.2020 | Opphør | Mister fra forrige behandling | + | 01.06.2020 | 31.04.2038 | Utbetaling | Barn og søker | + | 01.05.2038 | | Opphør | Kun søker | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/satsendring-ekstra-periode.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/satsendring-ekstra-periode.feature new file mode 100644 index 000000000..bb6054ea8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/satsendring-ekstra-periode.feature @@ -0,0 +1,54 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperioder for Satsendring + + Bakgrunn: + Gitt følgende fagsaker + | FagsakId | Fagsaktype | + | 200053601 | NORMAL | + + Gitt følgende vedtak + | BehandlingId | FagsakId | ForrigeBehandlingId | + | 100175851 | 200053601 | | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 100175851 | 2499861499383 | SØKER | 24.12.1987 | + | 100175851 | 2435441739050 | BARN | 10.07.2012 | + + Scenario: Skal ikke lage splitt på satsendring + Og følgende dagens dato 2023-09-13 + Og lag personresultater for behandling 100175851 + + Og legg til nye vilkårresultater for behandling 100175851 + | AktørId | Vilkår | Utdypende vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 2499861499383 | LOVLIG_OPPHOLD | | 01.07.2019 | | OPPFYLT | Nei | + | 2499861499383 | BOSATT_I_RIKET | OMFATTET_AV_NORSK_LOVGIVNING | 01.07.2019 | | OPPFYLT | Nei | + + | 2435441739050 | UNDER_18_ÅR | | 10.07.2012 | 09.07.2030 | OPPFYLT | Nei | + | 2435441739050 | GIFT_PARTNERSKAP | | 09.06.2017 | | OPPFYLT | Nei | + | 2435441739050 | LOVLIG_OPPHOLD | | 01.07.2019 | | OPPFYLT | Nei | + | 2435441739050 | BOR_MED_SØKER | BARN_BOR_I_EØS_MED_SØKER | 01.07.2019 | | OPPFYLT | Nei | + | 2435441739050 | BOSATT_I_RIKET | BARN_BOR_I_EØS | 01.07.2019 | | OPPFYLT | Nei | + + Og med andeler tilkjent ytelse + | AktørId | BehandlingId | Fra dato | Til dato | Beløp | Ytelse type | Prosent | Sats | + | 2435441739050 | 100175851 | 01.08.2019 | 31.12.2019 | 0 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 2435441739050 | 100175851 | 01.01.2020 | 31.12.2020 | 0 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 2435441739050 | 100175851 | 01.01.2021 | 31.12.2021 | 0 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 2435441739050 | 100175851 | 01.01.2022 | 28.02.2023 | 0 | ORDINÆR_BARNETRYGD | 100 | 1054 | + | 2435441739050 | 100175851 | 01.03.2023 | 30.06.2023 | 0 | ORDINÆR_BARNETRYGD | 100 | 1083 | + | 2435441739050 | 100175851 | 01.07.2023 | 30.06.2030 | 187 | ORDINÆR_BARNETRYGD | 100 | 1310 | + + Og med kompetanser + | AktørId | Fra dato | Til dato | Resultat | BehandlingId | Søkers aktivitet | Annen forelders aktivitet | Søkers aktivitetsland | Annen forelders aktivitetsland | Barnets bostedsland | + | 2435441739050 | 01.08.2019 | | NORGE_ER_SEKUNDÆRLAND | 100175851 | ARBEIDER | I_ARBEID | NO | PL | PL | + + Når vedtaksperioder med begrunnelser genereres for behandling 100175851 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | + | 01.08.2019 | 30.06.2023 | UTBETALING | + | 01.07.2023 | 30.06.2030 | UTBETALING | + | 01.07.2030 | | OPPHØR | diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/uregistrerte_barn.feature b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/uregistrerte_barn.feature new file mode 100644 index 000000000..d92148636 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/uregistrerte_barn.feature @@ -0,0 +1,99 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperioder for behandling med uregistrert barn + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | + | 1 | + + Scenario: Skal lage avslagsperiode uten datoer når vi har et uregistrert barn + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + + Og med uregistrerte barn + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | Begrunnelser | + | | | Avslag | | AVSLAG_UREGISTRERT_BARN | + + Scenario: Skal lage avslagsperiode uten datoer når vi har uregistrert barn og barn med eksplistt avslag + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 02.12.2016 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | | | Ikke_oppfylt | Ja | + | 3456 | UNDER_18_ÅR | 02.12.2016 | 01.12.2034 | Oppfylt | | + + Og med uregistrerte barn + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | Begrunnelser | + | | | Avslag | | AVSLAG_UREGISTRERT_BARN | + + Scenario: Skal lage avslagsperiode uten datoer når vi har uregistrert barn og et barn med utbetaling + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 02.12.2016 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 02.12.2016 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 02.12.2016 | 01.12.2034 | Oppfylt | | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.01.2017 | 30.11.2034 | 1234 | 1 | + + Og med uregistrerte barn + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | Begrunnelser | + | 01.01.2017 | 30.11.2034 | Utbetaling | | | + | 01.12.2034 | | Opphør | | | + | | | Avslag | | AVSLAG_UREGISTRERT_BARN | + + Scenario: Skal lage avslagsperiode som begrunner eksplisitt avslag i søkers vilkår dersom det bare finnes uregistrert barn + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET | 11.01.1970 | | Oppfylt | | + | 1234 | LOVLIG_OPPHOLD | 11.01.1970 | | Ikke_oppfylt | Ja | + + Og med uregistrerte barn + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | Begrunnelser | + | 01.02.1970 | | Avslag | | | + | | | Avslag | | AVSLAG_UREGISTRERT_BARN | \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/vilk\303\245r.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/vilk\303\245r.feature" new file mode 100644 index 000000000..c499c2d30 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/vilk\303\245r.feature" @@ -0,0 +1,385 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperioder ved endring av vilkår for mor og et barn + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | + | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + + Scenario: Skal lage vedtaksperioder for mor med ett barn med vilkår + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 1234 | BOSATT_I_RIKET | 11.01.1970 | 01.01.2021 | Oppfylt | + | 1234 | BOSATT_I_RIKET | 02.01.2021 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 01.03.2021 | Oppfylt | + | 3456 | BOR_MED_SØKER | 02.03.2021 | | Oppfylt | + + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.01.2021 | Utbetaling | Barn og søker | + | 01.02.2021 | 31.03.2021 | Utbetaling | Barn og søker | + | 01.04.2021 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.04.2038 | | Opphør | Kun søker | + + + Scenario: Skal lage vedtaksperioder når det er generelt avslag som overlapper med oppfylt periode + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET | 11.01.1970 | | Oppfylt | | + | 1234 | LOVLIG_OPPHOLD | 11.01.2020 | 05.05.2022 | Oppfylt | | + | 1234 | LOVLIG_OPPHOLD | | | ikke_oppfylt | Ja | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | | + + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 1234 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.05.2022 | Utbetaling | Barn og søker | + | 01.06.2022 | | Opphør | Lovlig opphold opphører for søker | + | | | Avslag | Generelt avslag lovlig opphold | + + + Scenario: Skal lage vedtaksperioder når det er åpent avslag på bor med søker samtidig som oppfylt + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.01.2020 | | Oppfylt | | + | 3456 | BOR_MED_SØKER | | | ikke_oppfylt | Ja | + + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 1234 | 01.01.2020 | 31.03.2038 | 1354 | 1 | + | 3456 | 01.01.2020 | 31.03.2038 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.04.2038 | | Opphør | Barn og søker | + | | | Avslag | Barn og søker | + + Scenario: Skal lage vedtaksperioder for mor med ett barn med vilkår - barn flytter til søker etter 1 år + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER | 20.08.2021 | | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.09.2021 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.04.2038 | | Opphør | Kun søker | + + + Scenario: Skal lage vedtaksperioder for mor med ett barn med vilkår - barn har vilkår fra tidenes morgen + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | BOR_MED_SØKER | | | Ikke_oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + + Scenario: Skal lage vedtaksperioder med begrunnelser for mor med vilkår når barnet flytter ut + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 21.07.2029 | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.07.2029 | Utbetaling | Barn og søker | + | 01.08.2029 | | Opphør | Barn har oppfylte vilkår, men ett som ikke oppfylles i perioden | + + + Scenario: Skal lage vedtaksperioder med begrunnelser for mor med vilkår når barnet flytter ut og inn igjen + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 21.07.2029 | Oppfylt | + | 3456 | BOR_MED_SØKER | 22.07.2029 | 16.05.2030 | Ikke_oppfylt | + | 3456 | BOR_MED_SØKER | 17.05.2030 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.07.2029 | Utbetaling | Barn og søker | + | 01.08.2029 | 31.05.2030 | Opphør | Kun søker | + | 01.06.2030 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.04.2038 | | Opphør | Kun søker | + + + Scenario: Skal ikke lage opphør på mor når det kun er opphør på barn + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 21.07.2021 | Oppfylt | + | 3456 | BOSATT_I_RIKET | 13.04.2020 | 21.07.2022 | Oppfylt | + | 3456 | BOR_MED_SØKER | 22.07.2021 | 16.05.2030 | Ikke_oppfylt | + | 3456 | BOSATT_I_RIKET | 22.07.2022 | 16.05.2030 | Ikke_oppfylt | + | 3456 | BOR_MED_SØKER | 17.05.2030 | | Oppfylt | + | 3456 | BOSATT_I_RIKET | 17.05.2030 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.07.2021 | Utbetaling | Barn og søker | + | 01.08.2021 | 31.05.2030 | Opphør | Kun søker | + | 01.06.2030 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.04.2038 | | Opphør | Kun søker | + + + Scenario: Skal lage opphør på mor når det kun er opphør i utvidet + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, LOVLIG_OPPHOLD | 13.04.2020 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 13.04.2020 | 21.07.2021 | Oppfylt | + | 3456 | BOSATT_I_RIKET | 13.04.2020 | 21.07.2022 | Oppfylt | + | 3456 | BOR_MED_SØKER | 22.07.2021 | 16.05.2030 | Ikke_oppfylt | + | 3456 | BOSATT_I_RIKET | 22.07.2022 | 16.05.2030 | Ikke_oppfylt | + | 3456 | BOR_MED_SØKER | 17.05.2030 | | Oppfylt | + | 3456 | BOSATT_I_RIKET | 17.05.2030 | | Oppfylt | + | 1234 | UTVIDET_BARNETRYGD | 13.04.2020 | 16.02.2021 | Oppfylt | + | 1234 | UTVIDET_BARNETRYGD | 17.02.2021 | 16.05.2030 | Ikke_oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 1234 | 01.05.2020 | 01.03.2021 | 678 | 1 | + | 3456 | 01.05.2020 | 31.07.2021 | 1245 | 1 | + | 3456 | 01.06.2030 | 31.03.2038 | 1245 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 28.02.2021 | Utbetaling | Barn og søker. Søker har utvidet | + | 01.03.2021 | 31.07.2021 | Utbetaling | Barn og søker. Søker har ikke utvidet | + | 01.08.2021 | 31.05.2030 | Opphør | Opphør barn. Bor ikke med søker | + | 01.06.2030 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.04.2038 | | Opphør | Kun søker | + + + Scenario: Skal gi opphør i periode barn ikke har lovlig opphold selv om mor har lovlig opphold + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | + | 3456 | LOVLIG_OPPHOLD | 13.04.2020 | 21.07.2021 | Oppfylt | + | 3456 | LOVLIG_OPPHOLD | 17.05.2023 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.07.2021 | 1245 | 1 | + | 3456 | 01.06.2023 | 31.03.2038 | 1245 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.07.2021 | Utbetaling | Barn og søker | + | 01.08.2021 | 31.05.2023 | Opphør | Kun søker | + | 01.06.2023 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.04.2038 | | Opphør | Kun søker | + + + Scenario: Skal kun gi utbetalingsperioder for utvidet om både søker og ett barn har oppfylt de ordinære vilkårene + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | 13.04.2021 | Oppfylt | + | 1234 | UTVIDET_BARNETRYGD | 13.04.2020 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, BOR_MED_SØKER, LOVLIG_OPPHOLD | 13.04.2020 | 13.04.2022 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, BOR_MED_SØKER, LOVLIG_OPPHOLD | 01.01.2030 | | Oppfylt | + | 1234 | BOSATT_I_RIKET | 13.04.2022 | | Oppfylt | + | 1234 | LOVLIG_OPPHOLD | 13.04.2022 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 1234 | 01.05.2020 | 30.04.2021 | 678 | 1 | + | 1234 | 01.01.2030 | 30.04.2038 | 678 | 1 | + | 3456 | 01.05.2020 | 30.04.2021 | 1245 | 1 | + | 3456 | 01.01.2030 | 30.04.2038 | 1245 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 30.04.2021 | Utbetaling | Barn og søker | + | 01.02.2030 | 31.03.2038 | Utbetaling | Barn og søker | + | 01.05.2021 | 31.01.2030 | Opphør | Søker har ikke oppfylt vilkårene | + | 01.04.2038 | | Opphør | | + + Scenario: Skal ikke dra med splitter fra forrige behandling inn i behandlingen + Gitt følgende vedtak + | BehandlingId | ForrigeBehandlingId | + | 1 | | + | 2 | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1, 2 | 1234 | SØKER | 13.07.1987 | + | 1, 2 | 3456 | BARN | 26.01.2021 | + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 13.07.1987 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 26.01.2021 | 25.01.2039 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOR_MED_SØKER, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 26.01.2021 | | Oppfylt | + + Og lag personresultater for behandling 2 + Og legg til nye vilkårresultater for behandling 2 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET | 13.07.1987 | | Oppfylt | + | 1234 | LOVLIG_OPPHOLD | 13.07.1987 | 09.01.2023 | Oppfylt | + | 1234 | LOVLIG_OPPHOLD | 30.03.2023 | | Oppfylt | + + | 3456 | UNDER_18_ÅR | 26.01.2021 | 25.01.2039 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 26.01.2021 | | Oppfylt | + | 3456 | BOR_MED_SØKER | 26.01.2021 | 21.03.2023 | Oppfylt | + | 3456 | BOR_MED_SØKER | 05.01.2030 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.02.2021 | 31.08.2021 | 1354 | 1 | + | 3456 | 01.09.2021 | 31.12.2021 | 1654 | 1 | + | 3456 | 01.01.2022 | 28.02.2023 | 1676 | 1 | + | 3456 | 01.03.2023 | 30.02.2039 | 1723 | 1 | + + | 3456 | 01.02.2021 | 31.08.2021 | 1354 | 2 | + | 3456 | 01.09.2021 | 31.12.2021 | 1654 | 2 | + | 3456 | 01.01.2022 | 31.03.2023 | 1676 | 2 | + | 3456 | 01.01.2030 | 30.02.2039 | 1676 | 2 | + + Og med overstyrt endringstidspunkt + | Endringstidspunkt | BehandlingId | + | 01.01.2021 | 2 | + + Når vedtaksperioder med begrunnelser genereres for behandling 2 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | + | 2021-02-01 | 2021-08-31 | UTBETALING | + | 2021-09-01 | 2021-12-31 | UTBETALING | + | 2022-01-01 | 2023-01-31 | UTBETALING | + | 2030-02-01 | 2038-12-31 | UTBETALING | + | 2023-02-01 | 2030-01-31 | OPPHØR | + | 2039-01-01 | | OPPHØR | + + + Scenario: Skal lage periode selv om det ikke finnes barn når det er eksplisitt avslag på søker + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | Er eksplisitt avslag | + | 1234 | LOVLIG_OPPHOLD | 11.01.1970 | 14.08.2022 | Oppfylt | | + | 1234 | BOSATT_I_RIKET | 11.01.1970 | | Oppfylt | | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | | + | 1234 | UTVIDET_BARNETRYGD | | | Ikke_oppfylt | Ja | + | 1234 | LOVLIG_OPPHOLD | 15.08.2022 | 02.02.2023 | Ikke_oppfylt | Ja | + | 1234 | LOVLIG_OPPHOLD | 03.02.2023 | | Oppfylt | | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.08.2022 | 1354 | 1 | + | 3456 | 01.03.2023 | 31.03.2038 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | | | Avslag | Søker avslag utvidet | + | 01.05.2020 | 31.08.2022 | Utbetaling | Barn og søker har ordinære vilkår oppfylt | + | 01.09.2022 | 28.02.2023 | Avslag | Søker avslag utvidet og lovlig opphold | + | 01.03.2023 | 31.03.2038 | Utbetaling | Barn og søker har ordinære vilkår oppfylt | + | 01.04.2038 | | Opphør | Barn over 18 | + + + + diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/vilk\303\245r_to_barn.feature" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/vilk\303\245r_to_barn.feature" new file mode 100644 index 000000000..8680e8d9d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/no/nav/familie/ba/sak/cucumber/vedtaksperioder/vilk\303\245r_to_barn.feature" @@ -0,0 +1,44 @@ +# language: no +# encoding: UTF-8 + +Egenskap: Vedtaksperioder ved endring av vilkår for mor og to barn + + Bakgrunn: + Gitt følgende vedtak + | BehandlingId | + | 1 | + + Og følgende persongrunnlag + | BehandlingId | AktørId | Persontype | Fødselsdato | + | 1 | 1234 | SØKER | 11.01.1970 | + | 1 | 3456 | BARN | 13.04.2020 | + | 1 | 7890 | BARN | 07.12.2022 | + + Scenario: Skal lage vedtaksperioder for mor, og to barn med vilkår - nytt barn kommer til + + Og lag personresultater for behandling 1 + Og legg til nye vilkårresultater for behandling 1 + | AktørId | Vilkår | Fra dato | Til dato | Resultat | + | 1234 | BOSATT_I_RIKET, LOVLIG_OPPHOLD | 11.01.1970 | | Oppfylt | + | 3456 | UNDER_18_ÅR | 13.04.2020 | 12.04.2038 | Oppfylt | + | 3456 | GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD, BOR_MED_SØKER | 13.04.2020 | | Oppfylt | + | 7890 | UNDER_18_ÅR | 07.12.2022 | 06.12.2040 | Oppfylt | + | 7890 | BOR_MED_SØKER, GIFT_PARTNERSKAP, BOSATT_I_RIKET, LOVLIG_OPPHOLD | 07.12.2022 | | Oppfylt | + + Og med andeler tilkjent ytelse + | AktørId | Fra dato | Til dato | Beløp | BehandlingId | + | 3456 | 01.05.2020 | 31.03.2038 | 1054 | 1 | + | 7890 | 01.01.2023 | 30.11.2040 | 1354 | 1 | + + Når vedtaksperioder med begrunnelser genereres for behandling 1 + + Så forvent følgende vedtaksperioder med begrunnelser + | Fra dato | Til dato | Vedtaksperiodetype | Kommentar | + | 01.05.2020 | 31.12.2022 | Utbetaling | Barn1 og søker | + | 01.01.2023 | 31.03.2038 | Utbetaling | Begge barn og søker | + | 01.04.2038 | 30.11.2040 | Utbetaling | Barn2 og søker | + | 01.12.2040 | | Opphør | Kun søker | + + + + diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForAdressebeskyttelse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForAdressebeskyttelse.json new file mode 100644 index 000000000..37ab42b46 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForAdressebeskyttelse.json @@ -0,0 +1,6 @@ +{ + "variables": { + "ident": "32345678901" + }, + "query": "GRAPHQL-PLACEHOLDER" +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForAdressebeskyttelse2.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForAdressebeskyttelse2.json new file mode 100644 index 000000000..eafa84892 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForAdressebeskyttelse2.json @@ -0,0 +1,6 @@ +{ + "variables": { + "ident": "32345678902" + }, + "query": "GRAPHQL-PLACEHOLDER" +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForAdressebeskyttelse3.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForAdressebeskyttelse3.json new file mode 100644 index 000000000..a0c931072 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForAdressebeskyttelse3.json @@ -0,0 +1,6 @@ +{ + "variables": { + "ident": "32345678903" + }, + "query": "GRAPHQL-PLACEHOLDER" +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBarn.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBarn.json new file mode 100644 index 000000000..37ab42b46 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBarn.json @@ -0,0 +1,6 @@ +{ + "variables": { + "ident": "32345678901" + }, + "query": "GRAPHQL-PLACEHOLDER" +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBarn2.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBarn2.json new file mode 100644 index 000000000..eafa84892 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBarn2.json @@ -0,0 +1,6 @@ +{ + "variables": { + "ident": "32345678902" + }, + "query": "GRAPHQL-PLACEHOLDER" +} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBarnMedOpph\303\270rtStatus.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBarnMedOpph\303\270rtStatus.json" new file mode 100644 index 000000000..232bb31c2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBarnMedOpph\303\270rtStatus.json" @@ -0,0 +1,6 @@ +{ + "variables": { + "ident": "92345678901" + }, + "query": "GRAPHQL-PLACEHOLDER" +} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBarnUtenF\303\270dselsdato.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBarnUtenF\303\270dselsdato.json" new file mode 100644 index 000000000..844289ffd --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBarnUtenF\303\270dselsdato.json" @@ -0,0 +1,6 @@ +{ + "variables": { + "ident": "92345678902" + }, + "query": "GRAPHQL-PLACEHOLDER" +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBostedsadresseperioder.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBostedsadresseperioder.json new file mode 100644 index 000000000..ccd5c0212 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForBostedsadresseperioder.json @@ -0,0 +1,6 @@ +{ + "variables": { + "ident": "22345678901" + }, + "query": "GRAPHQL-PLACEHOLDER" +} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForD\303\270dMor.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForD\303\270dMor.json" new file mode 100644 index 000000000..21259c705 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForD\303\270dMor.json" @@ -0,0 +1,6 @@ +{ + "variables": { + "ident": "44556612345" + }, + "query": "GRAPHQL-PLACEHOLDER" +} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForMor3Barn1Opph\303\270rt1UtenF\303\270dselsdato.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForMor3Barn1Opph\303\270rt1UtenF\303\270dselsdato.json" new file mode 100644 index 000000000..f19e87458 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForMor3Barn1Opph\303\270rt1UtenF\303\270dselsdato.json" @@ -0,0 +1,6 @@ +{ + "variables": { + "ident": "94556612349" + }, + "query": "GRAPHQL-PLACEHOLDER" +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForMorMedTomBostedsadresse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForMorMedTomBostedsadresse.json new file mode 100644 index 000000000..fc22a86cd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForMorMedTomBostedsadresse.json @@ -0,0 +1,6 @@ +{ + "variables": { + "ident": "22345678903" + }, + "query": "GRAPHQL-PLACEHOLDER" +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForMorMedUkjentStatsborgerskap.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForMorMedUkjentStatsborgerskap.json new file mode 100644 index 000000000..101cb99bd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForMorMedUkjentStatsborgerskap.json @@ -0,0 +1,6 @@ +{ + "variables": { + "ident": "22345678902" + }, + "query": "GRAPHQL-PLACEHOLDER" +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForMorMedXXXStatsborgerskap.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForMorMedXXXStatsborgerskap.json new file mode 100644 index 000000000..ccd5c0212 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/gyldigRequestForMorMedXXXStatsborgerskap.json @@ -0,0 +1,6 @@ +{ + "variables": { + "ident": "22345678901" + }, + "query": "GRAPHQL-PLACEHOLDER" +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/pdlAdressebeskyttelseMedTomListeResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/pdlAdressebeskyttelseMedTomListeResponse.json new file mode 100644 index 000000000..35a8ee418 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/pdlAdressebeskyttelseMedTomListeResponse.json @@ -0,0 +1,7 @@ +{ + "data": { + "person": { + "adressebeskyttelse": [] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForBarn.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForBarn.json new file mode 100644 index 000000000..6a257a5c0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForBarn.json @@ -0,0 +1,79 @@ +{ + "data": { + "pdlIdenter": { + "identer": [ + { + "ident": "32345678901", + "historisk": false, + "gruppe": "FOLKEREGISTERIDENT" + } + ] + }, + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "32345678901", + "status": "I_BRUK", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "BARN", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "2005-01-17" + } + ], + "kjoenn": [ + { + "kjoenn": "MANN" + } + ], + "bostedsadresse": [ + { + "vegadresse": { + "matrikkelId": "1234", + "husnummer": "3", + "husbokstav": null, + "bruksenhetsnummer": null, + "adressenavn": "OTTO SVERDRUPS VEG", + "kommunenummer": "1566", + "tilleggsnavn": null, + "postnummer": "6650" + }, + "matrikkeladresse": null, + "ukjentBosted": null + } + ], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "22345678901", + "relatertPersonsRolle": "MOR" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "statsborgerskap": [ + { + "land": "NOR", + "gyldigFraOgMed": "2005-01-20", + "gyldigTilOgMed": null + } + ], + "doedsfall": [], + "kontaktinformasjonForDoedsbo": [] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForBarnMedAdressebeskyttelse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForBarnMedAdressebeskyttelse.json new file mode 100644 index 000000000..c48890264 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForBarnMedAdressebeskyttelse.json @@ -0,0 +1,79 @@ +{ + "data": { + "pdlIdenter": { + "identer": [ + { + "ident": "32345678902", + "historisk": false, + "gruppe": "FOLKEREGISTERIDENT" + } + ] + }, + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "32345678902", + "status": "I_BRUK", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "REAL", + "etternavn": "RUNDINGSBØYE" + } + ], + "foedsel": [ + { + "foedselsdato": "2019-01-17" + } + ], + "kjoenn": [ + { + "kjoenn": "MANN" + } + ], + "bostedsadresse": [ + { + "vegadresse": { + "matrikkelId": "1234", + "husnummer": "3", + "husbokstav": null, + "bruksenhetsnummer": null, + "adressenavn": "OTTO SVERDRUPS VEG", + "kommunenummer": "1566", + "tilleggsnavn": null, + "postnummer": "6650" + }, + "matrikkeladresse": null, + "ukjentBosted": null + } + ], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "22345678901", + "relatertPersonsRolle": "MOR" + } + ], + "adressebeskyttelse": [ + { + "gradering": "STRENGT_FORTROLIG" + } + ], + "statsborgerskap": [ + { + "land": "NOR", + "gyldigFraOgMed": "2019-01-20", + "gyldigTilOgMed": null + } + ], + "doedsfall": [], + "kontaktinformasjonForDoedsbo": [] + } + } +} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForBarnMedOpph\303\270rtStatus.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForBarnMedOpph\303\270rtStatus.json" new file mode 100644 index 000000000..24d8ba720 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForBarnMedOpph\303\270rtStatus.json" @@ -0,0 +1,79 @@ +{ + "data": { + "pdlIdenter": { + "identer": [ + { + "ident": "92345678901", + "historisk": false, + "gruppe": "FOLKEREGISTERIDENT" + } + ] + }, + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "92345678901", + "status": "OPPHOERT", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "BARN", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "2005-01-17" + } + ], + "kjoenn": [ + { + "kjoenn": "MANN" + } + ], + "bostedsadresse": [ + { + "vegadresse": { + "matrikkelId": "1234", + "husnummer": "3", + "husbokstav": null, + "bruksenhetsnummer": null, + "adressenavn": "OTTO SVERDRUPS VEG", + "kommunenummer": "1566", + "tilleggsnavn": null, + "postnummer": "6650" + }, + "matrikkeladresse": null, + "ukjentBosted": null + } + ], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "22345678901", + "relatertPersonsRolle": "MOR" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "statsborgerskap": [ + { + "land": "NOR", + "gyldigFraOgMed": "2005-01-20", + "gyldigTilOgMed": null + } + ], + "doedsfall": [], + "kontaktinformasjonForDoedsbo": [] + } + } +} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForBarnUtenF\303\270dselsdato.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForBarnUtenF\303\270dselsdato.json" new file mode 100644 index 000000000..afe7f70c1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForBarnUtenF\303\270dselsdato.json" @@ -0,0 +1,75 @@ +{ + "data": { + "pdlIdenter": { + "identer": [ + { + "ident": "92345678902", + "historisk": false, + "gruppe": "FOLKEREGISTERIDENT" + } + ] + }, + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "92345678902", + "status": "I_BRUK", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "BARN", + "etternavn": "FYR" + } + ], + "foedsel": [], + "kjoenn": [ + { + "kjoenn": "MANN" + } + ], + "bostedsadresse": [ + { + "vegadresse": { + "matrikkelId": "1234", + "husnummer": "3", + "husbokstav": null, + "bruksenhetsnummer": null, + "adressenavn": "OTTO SVERDRUPS VEG", + "kommunenummer": "1566", + "tilleggsnavn": null, + "postnummer": "6650" + }, + "matrikkeladresse": null, + "ukjentBosted": null + } + ], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "22345678901", + "relatertPersonsRolle": "MOR" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "statsborgerskap": [ + { + "land": "NOR", + "gyldigFraOgMed": "2005-01-20", + "gyldigTilOgMed": null + } + ], + "doedsfall": [], + "kontaktinformasjonForDoedsbo": [] + } + } +} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForD\303\270dMor.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForD\303\270dMor.json" new file mode 100644 index 000000000..f4745e0ef --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForD\303\270dMor.json" @@ -0,0 +1,97 @@ +{ + "data": { + "pdlIdenter": { + "identer": [ + { + "ident": "44556612345", + "historisk": false, + "gruppe": "FOLKEREGISTERIDENT" + } + ] + }, + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "44556612345", + "status": "I_BRUK", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "MOR", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "1990-01-17" + } + ], + "kjoenn": [ + { + "kjoenn": "KVINNE" + } + ], + "bostedsadresse": [ + { + "vegadresse": { + "matrikkelId": "1234", + "husnummer": "3", + "husbokstav": null, + "bruksenhetsnummer": null, + "adressenavn": "OTTO SVERDRUPS VEG", + "kommunenummer": "1566", + "tilleggsnavn": null, + "postnummer": "6650" + }, + "matrikkeladresse": null, + "ukjentBosted": null + } + ], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "32345678901", + "relatertPersonsRolle": "BARN" + }, + { + "relatertPersonsIdent": "32345678902", + "relatertPersonsRolle": "BARN" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "statsborgerskap": [ + { + "land": "XXX", + "gyldigFraOgMed": null, + "gyldigTilOgMed": null + } + ], + "opphold": [ + ], + "doedsfall": [ + { + "doedsdato": "2020-04-04" + } + ], + "kontaktinformasjonForDoedsbo": [ + { + "adresse": { + "adresselinje1": "Gatenavn 1", + "postnummer": "1234", + "poststedsnavn": "Oslo" + } + } + ] + } + } +} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForMor3Barn1Opph\303\270rt1UtenF\303\270dselsdato.json" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForMor3Barn1Opph\303\270rt1UtenF\303\270dselsdato.json" new file mode 100644 index 000000000..81760c595 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForMor3Barn1Opph\303\270rt1UtenF\303\270dselsdato.json" @@ -0,0 +1,101 @@ +{ + "data": { + "pdlIdenter": { + "identer": [ + { + "ident": "94556612349", + "historisk": false, + "gruppe": "FOLKEREGISTERIDENT" + } + ] + }, + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "94556612349", + "status": "I_BRUK", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "MOR", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "1990-01-17" + } + ], + "kjoenn": [ + { + "kjoenn": "KVINNE" + } + ], + "bostedsadresse": [ + { + "vegadresse": { + "matrikkelId": "1234", + "husnummer": "3", + "husbokstav": null, + "bruksenhetsnummer": null, + "adressenavn": "OTTO SVERDRUPS VEG", + "kommunenummer": "1566", + "tilleggsnavn": null, + "postnummer": "6650" + }, + "matrikkeladresse": null, + "ukjentBosted": null + } + ], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "32345678901", + "relatertPersonsRolle": "BARN" + }, + { + "relatertPersonsIdent": "92345678901", + "relatertPersonsRolle": "BARN" + }, + { + "relatertPersonsIdent": "92345678902", + "relatertPersonsRolle": "BARN" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "statsborgerskap": [ + { + "land": "XXX", + "gyldigFraOgMed": null, + "gyldigTilOgMed": null + } + ], + "opphold": [ + ], + "doedsfall": [ + { + "doedsdato": "2020-04-04" + } + ], + "kontaktinformasjonForDoedsbo": [ + { + "adresse": { + "adresselinje1": "Gatenavn 1", + "postnummer": "1234", + "poststedsnavn": "Oslo" + } + } + ] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForMorMedTomtStatsborgerskap.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForMorMedTomtStatsborgerskap.json new file mode 100644 index 000000000..223b2abfa --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForMorMedTomtStatsborgerskap.json @@ -0,0 +1,91 @@ +{ + "data": { + "pdlIdenter": { + "identer": [ + { + "ident": "22345678901", + "historisk": false, + "gruppe": "FOLKEREGISTERIDENT" + }, + { + "ident": "2872543507203", + "historisk": false, + "gruppe": "AKTORID" + }, + { + "ident": "2872543000000", + "historisk": true, + "gruppe": "AKTORID" + } + ] + }, + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "44556612345", + "status": "I_BRUK", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "ENGASJERT", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "1955-09-13" + } + ], + "kjoenn": [ + { + "kjoenn": "KVINNE" + } + ], + "bostedsadresse": [ + { + "vegadresse": { + "matrikkelId": "1234", + "husnummer": "3", + "husbokstav": null, + "bruksenhetsnummer": null, + "adressenavn": "OTTO SVERDRUPS VEG", + "kommunenummer": "1566", + "tilleggsnavn": null, + "postnummer": "6650" + }, + "matrikkeladresse": null, + "ukjentBosted": null + } + ], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "32345678901", + "relatertPersonsRolle": "BARN" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "statsborgerskap": [ + ], + "opphold": [ + { + "type": "MIDLERTIDIG", + "oppholdFra": "2010-01-20", + "oppholdTil": "2022-01-20" + } + ], + "doedsfall": [], + "kontaktinformasjonForDoedsbo": [] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForMorMedUkjentStatsborgerskap.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForMorMedUkjentStatsborgerskap.json new file mode 100644 index 000000000..9b05628d5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForMorMedUkjentStatsborgerskap.json @@ -0,0 +1,96 @@ +{ + "data": { + "pdlIdenter": { + "identer": [ + { + "ident": "22345678901", + "historisk": false, + "gruppe": "FOLKEREGISTERIDENT" + }, + { + "ident": "2872543507203", + "historisk": false, + "gruppe": "AKTORID" + }, + { + "ident": "2872543000000", + "historisk": true, + "gruppe": "AKTORID" + } + ] + }, + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "44556612345", + "status": "I_BRUK", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "ENGASJERT", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "1955-09-13" + } + ], + "kjoenn": [ + { + "kjoenn": "KVINNE" + } + ], + "bostedsadresse": [ + { + "vegadresse": { + "matrikkelId": "1234", + "husnummer": "3", + "husbokstav": null, + "bruksenhetsnummer": null, + "adressenavn": "OTTO SVERDRUPS VEG", + "kommunenummer": "1566", + "tilleggsnavn": null, + "postnummer": "6650" + }, + "matrikkeladresse": null, + "ukjentBosted": null + } + ], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "32345678901", + "relatertPersonsRolle": "BARN" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "statsborgerskap": [ + { + "land": "XUK", + "gyldigFraOgMed": null, + "gyldigTilOgMed": null + } + ], + "opphold": [ + { + "type": "MIDLERTIDIG", + "oppholdFra": "2010-01-20", + "oppholdTil": "2022-01-20" + } + ], + "doedsfall": [], + "kontaktinformasjonForDoedsbo": [] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForMorMedXXXStatsborgerskap.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForMorMedXXXStatsborgerskap.json new file mode 100644 index 000000000..cfdbcdacc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/personinfoResponseForMorMedXXXStatsborgerskap.json @@ -0,0 +1,100 @@ +{ + "data": { + "pdlIdenter": { + "identer": [ + { + "ident": "22345678901", + "historisk": false, + "gruppe": "FOLKEREGISTERIDENT" + }, + { + "ident": "2872543507203", + "historisk": false, + "gruppe": "AKTORID" + }, + { + "ident": "2872543000000", + "historisk": true, + "gruppe": "AKTORID" + } + ] + }, + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "44556612345", + "status": "I_BRUK", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "ENGASJERT", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "1955-09-13" + } + ], + "kjoenn": [ + { + "kjoenn": "KVINNE" + } + ], + "bostedsadresse": [ + { + "vegadresse": { + "matrikkelId": "1234", + "husnummer": "3", + "husbokstav": null, + "bruksenhetsnummer": null, + "adressenavn": "OTTO SVERDRUPS VEG", + "kommunenummer": "1566", + "tilleggsnavn": null, + "postnummer": "6650" + }, + "matrikkeladresse": null, + "ukjentBosted": null + } + ], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "32345678901", + "relatertPersonsRolle": "BARN" + }, + { + "relatertPersonsIdent": "32345678902", + "relatertPersonsRolle": "BARN" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "statsborgerskap": [ + { + "land": "XXX", + "gyldigFraOgMed": null, + "gyldigTilOgMed": null + } + ], + "opphold": [ + { + "type": "MIDLERTIDIG", + "oppholdFra": "2010-01-20", + "oppholdTil": "2022-01-20" + } + ], + "doedsfall": [], + "kontaktinformasjonForDoedsbo": [] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/tomBostedsadresseResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/tomBostedsadresseResponse.json new file mode 100644 index 000000000..bdbeb8036 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/tomBostedsadresseResponse.json @@ -0,0 +1,9 @@ +{ + "data": { + + "person": { + + "bostedsadresse": [] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/utenlandskAdresseResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/utenlandskAdresseResponse.json new file mode 100644 index 000000000..1aae818f9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/PdlIntegrasjon/utenlandskAdresseResponse.json @@ -0,0 +1,17 @@ +{ + "data": { + "person": { + "bostedsadresse": [ + { + "utenlandskAdresse": { + "adressenavnNummer": "Widbrook Way", + "postkode": "SL68YA", + "bySted": "Maidenhead", + "regionDistriktOmraade": "Berkshire", + "landkode": "GBR" + } + } + ] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlAdressebeskyttelseResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlAdressebeskyttelseResponse.json new file mode 100644 index 000000000..e569624a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlAdressebeskyttelseResponse.json @@ -0,0 +1,11 @@ +{ + "data": { + "person": { + "adressebeskyttelse": [ + { + "gradering": "STRENGT_FORTROLIG" + } + ] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlAktorIdResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlAktorIdResponse.json new file mode 100644 index 000000000..54927a1b5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlAktorIdResponse.json @@ -0,0 +1,23 @@ +{ + "data": { + "pdlIdenter": { + "identer": [ + { + "ident": "21127725540", + "historisk": false, + "gruppe": "FOLKEREGISTERIDENT" + }, + { + "ident": "2872543507203", + "historisk": false, + "gruppe": "AKTORID" + }, + { + "ident": "2872543000000", + "historisk": true, + "gruppe": "AKTORID" + } + ] + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlDoedsfallIkkeDoedResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlDoedsfallIkkeDoedResponse.json new file mode 100644 index 000000000..ef9b86ee5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlDoedsfallIkkeDoedResponse.json @@ -0,0 +1,7 @@ +{ + "data": { + "person": { + "doedsfall": [] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlDoedsfallResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlDoedsfallResponse.json new file mode 100644 index 000000000..fa1cd8e4e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlDoedsfallResponse.json @@ -0,0 +1,14 @@ +{ + "data": { + "person": { + "doedsfall": [ + { + "doedsdato": null + }, + { + "doedsdato": "2019-07-02" + } + ] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlDoedsfallUtenDatoResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlDoedsfallUtenDatoResponse.json new file mode 100644 index 000000000..0b40d0ad5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlDoedsfallUtenDatoResponse.json @@ -0,0 +1,11 @@ +{ + "data": { + "person": { + "doedsfall": [ + { + "doedsdato": null + } + ] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlForelderBarnRelasjonResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlForelderBarnRelasjonResponse.json new file mode 100644 index 000000000..abb60ec85 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlForelderBarnRelasjonResponse.json @@ -0,0 +1,12 @@ +{ + "data": { + "person": { + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "32345678901", + "relatertPersonsRolle": "BARN" + } + ] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlGyldigRequest.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlGyldigRequest.json new file mode 100644 index 000000000..162b201d8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlGyldigRequest.json @@ -0,0 +1 @@ +{"variables":{"ident":"12345678901"},"query":"GRAPHQL-PLACEHOLDER"} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlManglerFoedselResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlManglerFoedselResponse.json new file mode 100644 index 000000000..34aa73f9b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlManglerFoedselResponse.json @@ -0,0 +1,43 @@ +{ + "data": { + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "1234", + "status": "I_BRUK", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "ENGASJERT", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": null + } + ], + "kjoenn": [ + { + "kjoenn": "MANN" + } + ], + "bostedsadresse": [], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "forelderBarnRelasjon": [], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "doedsfall": [], + "kontaktinformasjonForDoedsbo": [] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlMatrikkelAdresseOkResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlMatrikkelAdresseOkResponse.json new file mode 100644 index 000000000..74dc7eb95 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlMatrikkelAdresseOkResponse.json @@ -0,0 +1,55 @@ +{ + "data": { + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "1234", + "status": "I_BRUK", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "ENGASJERT", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "1955-09-13" + } + ], + "kjoenn": [ + { + "kjoenn": "MANN" + } + ], + "bostedsadresse": [ + { + "matrikkeladresse": { + "matrikkelId": "2147483649", + "postnummer": "0274" + } + } + ], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "12345678910", + "relatertPersonsRolle": "BARN" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "doedsfall": [], + "kontaktinformasjonForDoedsbo": [] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlOkResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlOkResponse.json new file mode 100644 index 000000000..8a20add4e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlOkResponse.json @@ -0,0 +1,63 @@ +{ + "data": { + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "1234", + "status": "I_BRUK", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "ENGASJERT", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "1955-09-13" + } + ], + "kjoenn": [ + { + "kjoenn": "MANN" + } + ], + "bostedsadresse": [ + { + "vegadresse": { + "matrikkelId": "1234", + "husnummer": "3", + "husbokstav": null, + "bruksenhetsnummer": null, + "adressenavn": "OTTO SVERDRUPS VEG", + "kommunenummer": "1566", + "tilleggsnavn": null, + "postnummer": "6650" + }, + "matrikkeladresse": null, + "ukjentBosted": null + } + ], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "12345678910", + "relatertPersonsRolle": "BARN" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "doedsfall": [], + "kontaktinformasjonForDoedsbo": [] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlOkResponseEnkel.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlOkResponseEnkel.json new file mode 100644 index 000000000..5d04385ce --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlOkResponseEnkel.json @@ -0,0 +1,50 @@ +{ + "data": { + "person": { + "navn": [ + { + "fornavn": "ENGASJERT", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "1955-09-13" + } + ], + "kjoenn": [ + { + "kjoenn": "MANN" + } + ], + "bostedsadresse": [ + { + "vegadresse": { + "matrikkelId": "1234", + "husnummer": "3", + "husbokstav": null, + "bruksenhetsnummer": null, + "adressenavn": "OTTO SVERDRUPS VEG", + "kommunenummer": "1566", + "tilleggsnavn": null, + "postnummer": "6650" + }, + "matrikkeladresse": null, + "ukjentBosted": null + } + ], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "doedsfall": [], + "kontaktinformasjonForDoedsbo": [] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlPersonIkkeFunnetResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlPersonIkkeFunnetResponse.json new file mode 100644 index 000000000..5f858a182 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlPersonIkkeFunnetResponse.json @@ -0,0 +1,22 @@ +{ + "errors": [ + { + "message": "Fant ikke person", + "extensions": { + "code": "not_found" + }, + "path": [ + "person" + ] + }, + { + "message": "Ikke tilgang", + "path": [ + "person" + ] + } + ], + "data": { + "person": null + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlStatsborgerskap.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlStatsborgerskap.json new file mode 100644 index 000000000..67c6a603c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlStatsborgerskap.json @@ -0,0 +1,14 @@ +{ + "data": { + "person": { + "statsborgerskap": [ + { + "land": "NOR", + "bekreftelsesdato": "2020-01-20", + "gyldigFraOgMed": "2020-01-20", + "gyldigTilOgMed": null + } + ] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlStatsborgerskapTom.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlStatsborgerskapTom.json new file mode 100644 index 000000000..8e2e1c500 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlStatsborgerskapTom.json @@ -0,0 +1,8 @@ +{ + "data": { + "person": { + "statsborgerskap": [ + ] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlTomAdresseOkResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlTomAdresseOkResponse.json new file mode 100644 index 000000000..9aaf9a7a7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlTomAdresseOkResponse.json @@ -0,0 +1,48 @@ +{ + "data": { + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "1234", + "status": "I_BRUK", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "ENGASJERT", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "1955-09-13" + } + ], + "kjoenn": [ + { + "kjoenn": "MANN" + } + ], + "bostedsadresse": [], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "12345678910", + "relatertPersonsRolle": "BARN" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "doedsfall": [], + "kontaktinformasjonForDoedsbo": [] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlUkjentBostedAdresseOkResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlUkjentBostedAdresseOkResponse.json new file mode 100644 index 000000000..6285a52d5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlUkjentBostedAdresseOkResponse.json @@ -0,0 +1,50 @@ +{ + "data": { + "person": { + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "1234", + "status": "I_BRUK", + "type": "FNR" + } + ], + "navn": [ + { + "fornavn": "ENGASJERT", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "1955-09-13" + } + ], + "kjoenn": [ + { + "kjoenn": "MANN" + } + ], + "bostedsadresse": [ + { + "ukjentBosted": { + "bostedskommune": "Oslo" + } + } + ], + "sivilstand": [], + "forelderBarnRelasjon": [ + { + "relatertPersonsIdent": "12345678910", + "relatertPersonsRolle": "BARN" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "doedsfall": [], + "kontaktinformasjonForDoedsbo": [] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlVergeFinnesIkkeResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlVergeFinnesIkkeResponse.json new file mode 100644 index 000000000..6c820fd4f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlVergeFinnesIkkeResponse.json @@ -0,0 +1,11 @@ +{ + "data": { + "person": { + "vergemaalEllerFremtidsfullmakt": [ + { + "type": "stadfestetFremtidsfullmakt" + } + ] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlVergeFinnesResponse.json b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlVergeFinnesResponse.json new file mode 100644 index 000000000..307df4aac --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/pdl/pdlVergeFinnesResponse.json @@ -0,0 +1,11 @@ +{ + "data": { + "person": { + "vergemaalEllerFremtidsfullmakt": [ + { + "type": "midlertidigForVoksen" + } + ] + } + } +} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far har mottatt delt utvidet barnetrygd for barn 12\303\245r - S\303\270ker n\303\245 om barnetrygd for barn som flyttet til han for over 3 \303\245r siden.png" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far har mottatt delt utvidet barnetrygd for barn 12\303\245r - S\303\270ker n\303\245 om barnetrygd for barn som flyttet til han for over 3 \303\245r siden.png" new file mode 100644 index 000000000..cf82d66a3 Binary files /dev/null and "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far har mottatt delt utvidet barnetrygd for barn 12\303\245r - S\303\270ker n\303\245 om barnetrygd for barn som flyttet til han for over 3 \303\245r siden.png" differ diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om delt bosted - Mor har tidligere mottatt fult utvidet og ordin\303\246r barnetrygd.png" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om delt bosted - Mor har tidligere mottatt fult utvidet og ordin\303\246r barnetrygd.png" new file mode 100644 index 000000000..1583813e0 Binary files /dev/null and "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om delt bosted - Mor har tidligere mottatt fult utvidet og ordin\303\246r barnetrygd.png" differ diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om delt bosted - Mor har tidligere mottatt fult, men har ikke mottatt utvidet.png" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om delt bosted - Mor har tidligere mottatt fult, men har ikke mottatt utvidet.png" new file mode 100644 index 000000000..1de7de3d6 Binary files /dev/null and "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om delt bosted - Mor har tidligere mottatt fult, men har ikke mottatt utvidet.png" differ diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om utvidet barnetrygd - Har full overgangsst\303\270nad, men s\303\270ker sent og f\303\245r ikke etterbetalt mer enn 3\303\245r.png" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om utvidet barnetrygd - Har full overgangsst\303\270nad, men s\303\270ker sent og f\303\245r ikke etterbetalt mer enn 3\303\245r.png" new file mode 100644 index 000000000..4529dd727 Binary files /dev/null and "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om utvidet barnetrygd - Har full overgangsst\303\270nad, men s\303\270ker sent og f\303\245r ikke etterbetalt mer enn 3\303\245r.png" differ diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om utvidet barnetrygd for barn under 3 \303\245r - Har full overgangsst\303\270nad som opph\303\270rer n\303\245r barnet fyller 3 \303\245r.png" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om utvidet barnetrygd for barn under 3 \303\245r - Har full overgangsst\303\270nad som opph\303\270rer n\303\245r barnet fyller 3 \303\245r.png" new file mode 100644 index 000000000..acaa648f6 Binary files /dev/null and "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om utvidet barnetrygd for barn under 3 \303\245r - Har full overgangsst\303\270nad som opph\303\270rer n\303\245r barnet fyller 3 \303\245r.png" differ diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om utvidet barnetrygd for barn under 3 \303\245r - han har full overgangsstlnad for bare deler av perioden.png" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om utvidet barnetrygd for barn under 3 \303\245r - han har full overgangsstlnad for bare deler av perioden.png" new file mode 100644 index 000000000..a7dc2613d Binary files /dev/null and "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om utvidet barnetrygd for barn under 3 \303\245r - han har full overgangsstlnad for bare deler av perioden.png" differ diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om utvidet barnetrygd for barn under 3\303\245r, men oppfyller vilk\303\245rene kun tilbake i tid.png" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om utvidet barnetrygd for barn under 3\303\245r, men oppfyller vilk\303\245rene kun tilbake i tid.png" new file mode 100644 index 000000000..fc04a9131 Binary files /dev/null and "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Far s\303\270ker om utvidet barnetrygd for barn under 3\303\245r, men oppfyller vilk\303\245rene kun tilbake i tid.png" differ diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Mor har tidligere mottatt barnetrygden - Far har n\303\245 s\303\270kt om delt bosted og mors barnetrygd skal ogs\303\245 deles 2.png" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Mor har tidligere mottatt barnetrygden - Far har n\303\245 s\303\270kt om delt bosted og mors barnetrygd skal ogs\303\245 deles 2.png" new file mode 100644 index 000000000..a01acb43f Binary files /dev/null and "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Mor har tidligere mottatt barnetrygden - Far har n\303\245 s\303\270kt om delt bosted og mors barnetrygd skal ogs\303\245 deles 2.png" differ diff --git "a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Mor har tidligere mottatt barnetrygden - Far har n\303\245 s\303\270kt om delt bosted og mors barnetrygd skal ogs\303\245 deles.png" "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Mor har tidligere mottatt barnetrygden - Far har n\303\245 s\303\270kt om delt bosted og mors barnetrygd skal ogs\303\245 deles.png" new file mode 100644 index 000000000..f689c002d Binary files /dev/null and "b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/scenario/Mor har tidligere mottatt barnetrygden - Far har n\303\245 s\303\270kt om delt bosted og mors barnetrygd skal ogs\303\245 deles.png" differ diff --git a/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/sql/prosessering_jdbc.sql b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/sql/prosessering_jdbc.sql new file mode 100644 index 000000000..837f2c036 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-ba-sak/src/test/resources/sql/prosessering_jdbc.sql @@ -0,0 +1,30 @@ +CREATE TABLE IF NOT EXISTS task ( + id BIGINT NOT NULL CONSTRAINT henvendelse_pkey PRIMARY KEY, + payload TEXT NOT NULL, + status VARCHAR DEFAULT 'UBEHANDLET':: CHARACTER VARYING NOT NULL, + versjon BIGINT DEFAULT 0, + opprettet_tid TIMESTAMP(3) DEFAULT LOCALTIMESTAMP, + type VARCHAR NOT NULL, + metadata VARCHAR, + trigger_tid TIMESTAMP DEFAULT LOCALTIMESTAMP, + avvikstype VARCHAR +); + +CREATE INDEX IF NOT EXISTS henvendelse_status_idx ON task (status); + +CREATE TABLE IF NOT EXISTS task_logg ( + id BIGINT NOT NULL CONSTRAINT henvendelse_logg_pkey PRIMARY KEY, + task_id BIGINT NOT NULL CONSTRAINT henvendelse_logg_henvendelse_id_fkey + REFERENCES task, + type VARCHAR NOT NULL, + node VARCHAR NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT LOCALTIMESTAMP, + melding TEXT, + endret_av VARCHAR DEFAULT 'VL':: CHARACTER VARYING +); + +CREATE INDEX IF NOT EXISTS henvendelse_logg_henvendelse_id_idx + ON task_logg (task_id); + +CREATE SEQUENCE IF NOT EXISTS task_seq INCREMENT BY 50; +CREATE SEQUENCE IF NOT EXISTS task_logg_seq INCREMENT BY 50; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/.deploy/azurerator/azure-ad-app-lokal.yaml b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/azurerator/azure-ad-app-lokal.yaml new file mode 100644 index 000000000..7d06c3095 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/azurerator/azure-ad-app-lokal.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: nais.io/v1 +kind: AzureAdApplication +metadata: + name: familie-tilbake-lokal + namespace: teamfamilie + labels: + team: teamfamilie +spec: + claims: + extra: + - "NAVident" + groups: + - id: d21e00a4-969d-4b28-8782-dc818abfae65 # 0000-GA-Barnetrygd + - id: 9449c153-5a1e-44a7-84c6-7cc7a8867233 # 0000-GA-Barnetrygd-Beslutter + - id: 93a26831-9866-4410-927b-74ff51a9107c # 0000-GA-Barnetrygd-Veileder + - id: ee5e0b5e-454c-4612-b931-1fe363df7c2c # 0000-GA-Enslig-Forsorger-Saksbehandler + - id: 01166863-22f1-4e16-9785-d7a05a22df74 # 0000-GA-Enslig-Forsorger-Beslutter + - id: 19dcbfde-4cdb-4c64-a1ea-ac9802b03339 # 0000-GA-Enslig-Forsorger-Veileder + - id: c62e908a-cf20-4ad0-b7b3-3ff6ca4bf38b # teamfamilie-forvaltning + - id: 928636f4-fd0d-4149-978e-a6fb68bb19de # 0000-GA-STDAPPS - tilgang til prosessering + preAuthorizedApplications: + - application: familie-tilbake-frontend-lokal + cluster: dev-gcp + namespace: teamfamilie + - application: familie-ba-sak + cluster: dev-gcp + - application: familie-ks-sak + cluster: dev-gcp + - application: familie-ba-sak-lokal + cluster: dev-gcp + namespace: teamfamilie + - application: familie-ef-sak-lokal + cluster: dev-gcp + namespace: teamfamilie + - application: familie-ks-sak-lokal + cluster: dev-gcp + namespace: teamfamilie + - application: familie-ef-iverksett-lokal + cluster: dev-gcp + namespace: teamfamilie + - application: ida + cluster: prod-fss + namespace: traktor + replyUrls: + - url: "http://localhost:8030/swagger-ui/oauth2-redirect.html" + tenant: trygdeetaten.no + secretName: azuread-familie-tilbake-lokal + singlePageApplication: true + +# secret kan hentes fra cluster med "kubectl -n teamfamilie get secret azuread-familie-tilbake-lokal -o json | jq '.data | map_values(@base64d)'" diff --git a/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/app-dev-gcp.yaml b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/app-dev-gcp.yaml new file mode 100644 index 000000000..486a9cc69 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/app-dev-gcp.yaml @@ -0,0 +1,119 @@ +apiVersion: "nais.io/v1alpha1" +kind: "Application" +metadata: + name: familie-tilbake + namespace: teamfamilie + labels: + team: teamfamilie + +spec: + envFrom: + - secret: familie-tilbake + - secret: familie-tilbake-unleash-api-token + image: {{ image }} + port: 8030 + leaderElection: true + liveness: + path: /internal/status/isAlive + initialDelay: 30 + failureThreshold: 10 + readiness: + path: /internal/status/isAlive + initialDelay: 30 + failureThreshold: 10 + prometheus: + enabled: true + path: /internal/prometheus + vault: + enabled: false + gcp: # Database + sqlInstances: + - type: POSTGRES_14 + tier: db-custom-1-3840 + name: familie-tilbake + autoBackupTime: "03:00" + databases: + - name: familie-tilbake + envVarPrefix: DB + azure: + application: + claims: + extra: + - "NAVident" + groups: + - id: d21e00a4-969d-4b28-8782-dc818abfae65 # 0000-GA-Barnetrygd + - id: 9449c153-5a1e-44a7-84c6-7cc7a8867233 # 0000-GA-Barnetrygd-Beslutter + - id: 93a26831-9866-4410-927b-74ff51a9107c # 0000-GA-Barnetrygd-Veileder + - id: ee5e0b5e-454c-4612-b931-1fe363df7c2c # 0000-GA-Enslig-Forsorger-Saksbehandler + - id: 01166863-22f1-4e16-9785-d7a05a22df74 # 0000-GA-Enslig-Forsorger-Beslutter + - id: 19dcbfde-4cdb-4c64-a1ea-ac9802b03339 # 0000-GA-Enslig-Forsorger-Veileder + - id: 71f503a2-c28f-4394-a05a-8da263ceca4a # 0000-GA-Kontantstøtte-Veilder + - id: c7e0b108-7ae6-432c-9ab4-946174c240c0 # 0000-GA-Kontantstøtte + - id: 52fe1bef-224f-49df-a40a-29f92d4520f8 # 0000-GA-Kontantstøtte-Beslutter + - id: c62e908a-cf20-4ad0-b7b3-3ff6ca4bf38b # teamfamilie-forvaltning + - id: 928636f4-fd0d-4149-978e-a6fb68bb19de # 0000-GA-STDAPPS - tilgang til prosessering + enabled: true + tenant: trygdeetaten.no + replyURLs: + - "https://familie-tilbake.intern.dev.nav.no/swagger-ui/oauth2-redirect.html" + singlePageApplication: true + accessPolicy: + inbound: + rules: + - application: familie-ba-sak + namespace: teamfamilie + cluster: dev-gcp + - application: familie-ks-sak + namespace: teamfamilie + cluster: dev-gcp + - application: familie-ef-sak + namespace: teamfamilie + cluster: dev-gcp + - application: familie-ef-iverksett + namespace: teamfamilie + cluster: dev-gcp + - application: familie-ks-sak + namespace: teamfamilie + cluster: dev-gcp + - application: familie-tilbake-frontend + namespace: teamfamilie + cluster: dev-gcp + - application: ida + namespace: traktor + cluster: prod-fss + - application: familie-prosessering + namespace: teamfamilie + cluster: dev-gcp + - application: familie-prosessering-lokal + namespace: teamfamilie + cluster: dev-gcp + outbound: + rules: + - application: familie-historikk + external: + - host: teamfamilie-unleash-api.nav.cloud.nais.io + - host: familie-integrasjoner.dev-fss-pub.nais.io + - host: pdl-api.dev-fss-pub.nais.io + - host: b27apvl220.preprod.local + ports: + - name: mq + port: 1413 + protocol: TCP + replicas: + min: 2 + max: 4 + resources: + limits: + memory: 1024Mi + requests: + memory: 512Mi + cpu: 500m + ingresses: + - https://familie-tilbake.intern.dev.nav.no + secureLogs: + enabled: true + env: + - name: SPRING_PROFILES_ACTIVE + value: dev + kafka: + pool: nav-dev \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/app-prod-gcp.yaml b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/app-prod-gcp.yaml new file mode 100644 index 000000000..d92e966a6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/app-prod-gcp.yaml @@ -0,0 +1,116 @@ +apiVersion: "nais.io/v1alpha1" +kind: "Application" +metadata: + name: familie-tilbake + namespace: teamfamilie + labels: + team: teamfamilie + +spec: + image: {{ image }} + port: 8030 + leaderElection: true + liveness: + path: /internal/status/isAlive + initialDelay: 30 + failureThreshold: 10 + readiness: + path: /internal/status/isAlive + initialDelay: 30 + failureThreshold: 10 + prometheus: + enabled: true + path: /internal/prometheus + vault: + enabled: false + gcp: # Database + sqlInstances: + - type: POSTGRES_14 + tier: db-custom-1-3840 + name: familie-tilbake + diskAutoresize: true + highAvailability: true + autoBackupTime: "03:00" + databases: + - name: familie-tilbake + envVarPrefix: DB + azure: + application: + claims: + extra: + - "NAVident" + groups: + - id: 847e3d72-9dc1-41c3-80ff-f5d4acdd5d46 # 0000-GA-Barnetrygd + - id: 7a271f87-39fb-468b-a9ee-6cf3c070f548 # 0000-GA-Barnetrygd-Beslutter + - id: 199c2b39-e535-4ae8-ac59-8ccbee7991ae # 0000-GA-Barnetrygd-Veileder + - id: e40090eb-c2fb-400e-b412-e9084019a73b # 0000-GA-Kontantstøtte + - id: 54cd86b8-2e23-48b2-8852-b05b5827bb0f # 0000-GA-Kontantstøtte-Veileder + - id: 3d718ae5-f25e-47a4-b4b3-084a97604c1d # teamfamilie-forvaltning + - id: 87190cf3-b278-457d-8ab7-1a5c55a9edd7 # Group_87190cf3-b278-457d-8ab7-1a5c55a9edd7 tilgang til prosessering + - id: 31778fd8-3b71-4867-8db6-a81235fbe001 # 0000-GA-Enslig-Forsorger-Veileder + - id: 6406aba2-b930-41d3-a85b-dd13731bc974 # 0000-GA-Enslig-Forsorger-Saksbehandler + - id: 5fcc0e1d-a4c2-49f0-93dc-27c9fea41e54 # 0000-GA-Enslig-Forsorger-Beslutter + - id: 54cd86b8-2e23-48b2-8852-b05b5827bb0f # 0000-GA-Kontantstøtte-Veileder + - id: e40090eb-c2fb-400e-b412-e9084019a73b # 0000-GA-Kontantstøtte + - id: 4e7f23d9-5db1-45c0-acec-89c86a9ec678 # 0000-GA-Kontantstøtte-Beslutter + enabled: true + replyURLs: + - "https://familie-tilbake.intern.nav.no/swagger-ui/oauth2-redirect.html" + singlePageApplication: true + accessPolicy: + inbound: + rules: + - application: familie-ba-sak + namespace: teamfamilie + cluster: prod-gcp + - application: familie-ks-sak + namespace: teamfamilie + cluster: prod-gcp + - application: familie-ef-sak + namespace: teamfamilie + cluster: prod-gcp + - application: familie-ef-iverksett + namespace: teamfamilie + cluster: prod-gcp + - application: familie-ks-sak + namespace: teamfamilie + cluster: prod-gcp + - application: familie-tilbake-frontend + namespace: teamfamilie + cluster: prod-gcp + - application: familie-prosessering + namespace: teamfamilie + cluster: prod-gcp + outbound: + rules: + - application: familie-historikk + external: + - host: teamfamilie-unleash-api.nav.cloud.nais.io + - host: familie-integrasjoner.prod-fss-pub.nais.io + - host: pdl-api.prod-fss-pub.nais.io + - host: mpls02.adeo.no + ports: + - name: mq + port: 1414 + protocol: TCP + replicas: + min: 2 + max: 4 + resources: + limits: + memory: 1024Mi + requests: + memory: 512Mi + cpu: 500m + secureLogs: + enabled: true + ingresses: # Optional. List of ingress URLs that will route HTTP traffic to the application. + - https://familie-tilbake.intern.nav.no + env: + - name: SPRING_PROFILES_ACTIVE + value: prod + envFrom: + - secret: familie-tilbake + - secret: familie-tilbake-unleash-api-token + kafka: + pool: nav-prod \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/dvh_sak_topic.yaml b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/dvh_sak_topic.yaml new file mode 100644 index 000000000..4aa362b59 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/dvh_sak_topic.yaml @@ -0,0 +1,29 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-tbk-datavarehus-sak-topic + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-dev + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: -1 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-tilbake #owner + access: readwrite # readwrite + - team: ptsak + application: pt-sak-famtilbake-dev + access: read + - team: ptsak + application: pt-sak-famtilbake-preprod + access: read + - team: teamfamilie + application: familie-tilbake-kafka-manager #forvalter + access: read # read diff --git a/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/dvh_vedtak_topic.yaml b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/dvh_vedtak_topic.yaml new file mode 100644 index 000000000..e0d04b8be --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/dvh_vedtak_topic.yaml @@ -0,0 +1,29 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-tbk-datavarehus-vedtak-topic + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-dev + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: -1 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-tilbake #owner + access: readwrite # readwrite + - team: ptsak + application: pt-sak-famtilbake-dev + access: read + - team: ptsak + application: pt-sak-famtilbake-preprod + access: read + - team: teamfamilie + application: familie-tilbake-kafka-manager #forvalter + access: read # read \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/hentfagsystemsbehandling_request_topic.yaml b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/hentfagsystemsbehandling_request_topic.yaml new file mode 100644 index 000000000..3471ac110 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/hentfagsystemsbehandling_request_topic.yaml @@ -0,0 +1,32 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: privat-tbk-hentfagsystemsbehandling-request-topic + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-dev + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 72 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-tilbake #owner + access: readwrite # readwrite + - team: teamfamilie + application: familie-ba-sak #owner + access: read # readwrite + - team: teamfamilie + application: familie-ks-sak + access: read # read + - team: teamfamilie + application: familie-ef-iverksett #owner + access: read # readwrite + - team: teamfamilie + application: familie-tilbake-kafka-manager #forvalter + access: read # read diff --git a/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/hentfagsystemsbehandling_respons_topic.yaml b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/hentfagsystemsbehandling_respons_topic.yaml new file mode 100644 index 000000000..6d1584e7d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/hentfagsystemsbehandling_respons_topic.yaml @@ -0,0 +1,32 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: privat-tbk-hentfagsystemsbehandling-respons-topic + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-dev + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 72 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-tilbake #owner + access: readwrite # readwrite + - team: teamfamilie + application: familie-ba-sak #owner + access: write # readwrite + - team: teamfamilie + application: familie-ks-sak + access: write # readwrite + - team: teamfamilie + application: familie-ef-iverksett #owner + access: write # readwrite + - team: teamfamilie + application: familie-tilbake-kafka-manager #forvalter + access: read # read diff --git a/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/prod/dvh_sak_topic_prod.yaml b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/prod/dvh_sak_topic_prod.yaml new file mode 100644 index 000000000..a847d1c61 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/prod/dvh_sak_topic_prod.yaml @@ -0,0 +1,27 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-tbk-datavarehus-sak-topic + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-prod + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: -1 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-tilbake #owner + access: readwrite # readwrite + - team: ptsak + application: pt-sak-famtilbake + access: read + - team: teamfamilie + application: familie-tilbake-kafka-manager #forvalter + access: read # read + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/prod/dvh_vedtak_topic_prod.yaml b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/prod/dvh_vedtak_topic_prod.yaml new file mode 100644 index 000000000..244ee773b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/prod/dvh_vedtak_topic_prod.yaml @@ -0,0 +1,26 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: aapen-tbk-datavarehus-vedtak-topic + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-prod + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: -1 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-tilbake #owner + access: readwrite # readwrite + - team: ptsak + application: pt-sak-famtilbake + access: read + - team: teamfamilie + application: familie-tilbake-kafka-manager #forvalter + access: read # read diff --git a/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/prod/hentfagsystemsbehandling_request_topic_prod.yaml b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/prod/hentfagsystemsbehandling_request_topic_prod.yaml new file mode 100644 index 000000000..9e07439e2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/prod/hentfagsystemsbehandling_request_topic_prod.yaml @@ -0,0 +1,33 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: privat-tbk-hentfagsystemsbehandling-request-topic + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-prod + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 72 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-tilbake #owner + access: readwrite # readwrite + - team: teamfamilie + application: familie-ba-sak #owner + access: read # readwrite + - team: teamfamilie + application: familie-ks-sak + access: read # read + - team: teamfamilie + application: familie-ef-iverksett #owner + access: read # readwrite + - team: teamfamilie + application: familie-tilbake-kafka-manager #forvalter + access: read # read + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/prod/hentfagsystemsbehandling_respons_topic_prod.yaml b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/prod/hentfagsystemsbehandling_respons_topic_prod.yaml new file mode 100644 index 000000000..5eba9992f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/kafka/prod/hentfagsystemsbehandling_respons_topic_prod.yaml @@ -0,0 +1,33 @@ +apiVersion: kafka.nais.io/v1 +kind: Topic +metadata: + name: privat-tbk-hentfagsystemsbehandling-respons-topic + namespace: teamfamilie + labels: + team: teamfamilie +spec: + pool: nav-prod + config: # optional; all fields are optional too; defaults shown + cleanupPolicy: delete # delete, compact + minimumInSyncReplicas: 2 + partitions: 1 + replication: 3 # see min/max requirements + retentionBytes: -1 # -1 means unlimited + retentionHours: 72 # -1 means unlimited + acl: + - team: teamfamilie + application: familie-tilbake #owner + access: readwrite # readwrite + - team: teamfamilie + application: familie-ba-sak #owner + access: write # readwrite + - team: teamfamilie + application: familie-ks-sak + access: write # readwrite + - team: teamfamilie + application: familie-ef-iverksett #owner + access: write # readwrite + - team: teamfamilie + application: familie-tilbake-kafka-manager #forvalter + access: read # read + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/unleash/unleash-apitoken-preprod.yaml b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/unleash/unleash-apitoken-preprod.yaml new file mode 100644 index 000000000..9c85aac43 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/unleash/unleash-apitoken-preprod.yaml @@ -0,0 +1,17 @@ +apiVersion: unleash.nais.io/v1 +kind: ApiToken +metadata: + name: familie-klage + namespace: teamfamilie + labels: + team: teamfamilie +spec: + unleashInstance: + apiVersion: unleash.nais.io/v1 + kind: RemoteUnleash + name: teamfamilie + secretName: familie-tilbake-unleash-api-token + + # Specify which environment the API token should be created for. + # Can be one of: development, or production. + environment: development \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/unleash/unleash-apitoken-prod.yaml b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/unleash/unleash-apitoken-prod.yaml new file mode 100644 index 000000000..e96316bf4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/.deploy/nais/unleash/unleash-apitoken-prod.yaml @@ -0,0 +1,17 @@ +apiVersion: unleash.nais.io/v1 +kind: ApiToken +metadata: + name: familie-klage + namespace: teamfamilie + labels: + team: teamfamilie +spec: + unleashInstance: + apiVersion: unleash.nais.io/v1 + kind: RemoteUnleash + name: teamfamilie + secretName: familie-tilbake-unleash-api-token + + # Specify which environment the API token should be created for. + # Can be one of: development, or production. + environment: production \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/CODEOWNERS b/jdk_17_maven/cs/rest/familie-tilbake/CODEOWNERS new file mode 100644 index 000000000..d5ade9f35 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/CODEOWNERS @@ -0,0 +1 @@ +* @navikt/teamfamilie diff --git a/jdk_17_maven/cs/rest/familie-tilbake/Dockerfile b/jdk_17_maven/cs/rest/familie-tilbake/Dockerfile new file mode 100644 index 000000000..9659c283a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/Dockerfile @@ -0,0 +1,3 @@ +FROM ghcr.io/navikt/baseimages/temurin:17 +ENV APP_NAME=familie-tilbake +COPY ./target/familie-tilbake.jar "app.jar" diff --git a/jdk_17_maven/cs/rest/familie-tilbake/LICENSE b/jdk_17_maven/cs/rest/familie-tilbake/LICENSE new file mode 100644 index 000000000..77a07b080 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 NAV + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/jdk_17_maven/cs/rest/familie-tilbake/README.md b/jdk_17_maven/cs/rest/familie-tilbake/README.md new file mode 100644 index 000000000..f07e75007 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/README.md @@ -0,0 +1,53 @@ +# familie-tilbake +Applikasjon for tilbakekreving av barnetrygd og enslig forsørger + +## Bygging +Bygging gjøres med `mvn verify`. + +## Kjøring lokalt +For å kjøre opp appen lokalt kan en kjøre `DevLauncher` med Spring-profilen `dev` satt. Dette kan feks gjøres ved å sette +`-Dspring.profiles.active=dev` under Edit Configurations -> VM Options. +Appen tilgjengeliggjøres da på `localhost:8030`. + +### Database +Dersom man vil kjøre med postgres, kan man bytte til Spring-profilen `postgres`. +Da må man sette opp postgres-databasen, dette gjøres slik: +``` +docker run --name familie-tilbake-postgres -e POSTGRES_PASSWORD=test -d -p 5432:5432 postgres +docker ps (finn container id) +docker exec -it bash +winpty docker exec -it bash(fra git-bash windows) +psql -U postgres +CREATE DATABASE "familie-tilbake"; +\l (til å verifisere om databasen er opprettet) +``` + +### Autentisering +Dersom man vil gjøre autentiserte kall mot andre tjenester, må man sette opp følgende miljø-variabler: +* Client secret +* Client id +* Scope for den aktuelle tjenesten + +Variablene legges inn under DevLauncher -> Edit Configurations -> Environment Variables. + +Miljøvariablene kan hentes fra `azuread-familie-tilbake-lokal` i +dev-gcp-clusteret ved å gjøre følgende: + +1. Logg på `gcloud`, typisk med kommandoen: `gcloud auth login` +2. Koble deg til dev-gcp-cluster'et: `kubectl config use-context dev-gcp` +3. Hent info: + `kubectl -n teamfamilie get secret azuread-familie-tilbake-lokal -o json | jq '.data | map_values(@base64d)'`. + +AZURE_APP_CLIENT_ID må settes til `AZURE_APP_CLIENT_ID` og AZURE_APP_CLIENT_SECRET til`AZURE_APP_CLIENT_SECRET` + +## Produksjonssetting +Master-branchen blir automatisk bygget ved merge og deployet til prod. + +## Kontaktinformasjon +For NAV-interne kan henvendelser om applikasjonen rettes til #team-familie-tilbakekreving på slack. +Ellers kan man opprette et issue her på github. + +# Generering av dokumetnasjon +https://confluence.adeo.no/display/TFA/Generert+dokumentasjon + +[Filer for generering av dokumentasjon](/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/pom.xml b/jdk_17_maven/cs/rest/familie-tilbake/pom.xml new file mode 100644 index 000000000..141ec5f91 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/pom.xml @@ -0,0 +1,550 @@ + + + 4.0.0 + + no.nav + jar + familie-tilbake + ${revision}${sha1}${changelist} + + org.springframework.boot + spring-boot-starter-parent + + + 3.2.0 + + + + + 1.0 + + -SNAPSHOT + 17 + 17 + no.nav.familie.tilbake.LauncherKt + 1.9.10 + 2.2021.09.17_07.09-67428a6422cc + 3.0_20231006095938_4f85a1d + 2.20230928165350_3e5b5e9 + + 5.7.2 + 3.1.5 + 4.9.1 + 1.0.10 + 4.0.1 + 4.0.3 + 1.24.1 + 1.13.8 + + ${SONAR_PROJECTKEY} + navit + https://sonarcloud.io + **/config/FlywayConfig.kt + + ${SONAR_LOGIN} + 4.0.4 + 2.2.0 + 1.19.1 + 2618.0448179 + 1.0_20230808160403_18dbb48 + 2.20231005144526_f184554 + + + + + + + org.springframework.boot + spring-boot-starter-jetty + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-actuator + + + org.aspectj + aspectjrt + ${aspectj.version} + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + + + jakarta.xml.bind + jakarta.xml.bind-api + ${jakarta.xml.bind-api.version} + + + org.glassfish.jaxb + jaxb-core + ${jaxb-impl.version} + + + com.sun.xml.bind + jaxb-impl + ${jaxb-impl.version} + + + javax.activation + activation + 1.1.1 + + + com.fasterxml.jackson.module + jackson-module-kotlin + + + + + com.github.jknack + handlebars + 4.3.1 + + + com.github.jknack + handlebars-jackson2 + 4.3.1 + + + com.openhtmltopdf + openhtmltopdf-core + ${com.openhtmltopdf.version} + + + com.openhtmltopdf + openhtmltopdf-pdfbox + ${com.openhtmltopdf.version} + + + com.openhtmltopdf + openhtmltopdf-svg-support + ${com.openhtmltopdf.version} + + + xalan + xalan + + + + + com.openhtmltopdf + openhtmltopdf-slf4j + ${com.openhtmltopdf.version} + + + org.apache.pdfbox + pdfbox + 2.0.29 + + + + org.verapdf + core-jakarta + ${verapdf.version} + + + org.verapdf + validation-model + ${verapdf.version} + + + + + + com.fasterxml.jackson.core + jackson-databind + + + com.ibm.mq + com.ibm.mq.jakarta.client + 9.3.3.1 + + + jakarta.jms + jakarta.jms-api + + + org.springframework + spring-jms + + + org.apache.activemq + activemq-jms-pool + + + + + org.postgresql + postgresql + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + org.flywaydb + flyway-core + + + + + org.springframework.kafka + spring-kafka + + + org.apache.kafka + kafka-clients + + + + + no.nav.familie + prosessering-core + ${prosessering-core.version} + + + no.nav.security + token-validation-spring + ${token-validation-spring.version} + + + no.nav.familie.felles + kafka + ${felles.version} + + + no.nav.familie.felles + log + ${felles.version} + + + no.nav.familie.felles + http-client + ${felles.version} + + + no.nav.familie.felles + sikkerhet + ${felles.version} + + + ch.qos.logback + logback-classic + + + com.papertrailapp + logback-syslog4j + 1.0.0 + + + no.nav.familie.felles + leader + ${felles.version} + + + no.nav.familie.kontrakter + felles + ${kontrakter.version} + + + no.nav.tjenestespesifikasjoner + avstemming-v1-tjenestespesifikasjon + ${tjenestespesifikasjoner.version} + + + no.nav.familie.tjenestespesifikasjoner + tilbakekreving-v1-tjenestespesifikasjon + ${familie-tjenestespesifikasjoner.version} + + + + + io.micrometer + micrometer-registry-prometheus + + + org.springframework.retry + spring-retry + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} + + + org.springdoc + springdoc-openapi-starter-common + ${springdoc.version} + + + no.nav.familie.felles + unleash + ${felles.version} + + + org.messaginghub + pooled-jms + 3.1.4 + + + + org.springframework.boot + spring-boot-starter-test + test + + + io.kotest + kotest-runner-junit5-jvm + ${kotest.version} + test + + + io.mockk + mockk + + + + + no.nav.security + token-validation-spring-test + ${token-validation-spring.version} + test + + + io.mockk + mockk-jvm + ${mockk.version} + test + + + org.springframework.cloud + spring-cloud-contract-wiremock + ${spring.cloud-contract} + test + + + + org.testcontainers + testcontainers + ${testcontainers.version} + test + + + org.testcontainers + postgresql + ${testcontainers.version} + test + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + + com.github.tomakehurst + wiremock-jre8 + 3.0.1 + test + + + io.jsonwebtoken + jjwt + 0.12.3 + test + + + org.springframework.kafka + spring-kafka-test + test + + + org.apache.kafka + kafka_2.13 + test + + + + + + github + https://maven.pkg.github.com/navikt/familie-kontrakter + + + + + + coverage + + true + + + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + report + + report + + + + + + + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + ${project.artifactId} + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + 3.10.0.2594 + + + org.jacoco + jacoco-maven-plugin + 0.8.10 + + + **/config/FlywayConfig.kt + **/config/OppdragMQConfig.kt + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + ${main-class} + familie-tilbake + sut + + + + + repackage + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + -Xjsr305=strict + + + spring + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + ktlint + verify + + + + + + + + + run + + + + ktlint-format + validate + + + + + + + + + + + + run + + + + + + com.pinterest + ktlint + 0.50.0 + + + + + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/Launcher.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/Launcher.kt new file mode 100644 index 000000000..664b63c52 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/Launcher.kt @@ -0,0 +1,13 @@ +package no.nav.familie.tilbake + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.jms.annotation.EnableJms + +@SpringBootApplication +@EnableJms +class Launcher + +fun main(args: Array) { + SpringApplication.run(Launcher::class.java, *args) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/BehandlingController.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/BehandlingController.kt new file mode 100644 index 000000000..4db377213 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/BehandlingController.kt @@ -0,0 +1,203 @@ +package no.nav.familie.tilbake.api + +import io.swagger.v3.oas.annotations.Operation +import jakarta.validation.Valid +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettManueltTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettTilbakekrevingRequest +import no.nav.familie.tilbake.api.dto.BehandlingDto +import no.nav.familie.tilbake.api.dto.BehandlingPåVentDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegDto +import no.nav.familie.tilbake.api.dto.ByttEnhetDto +import no.nav.familie.tilbake.api.dto.HenleggelsesbrevFritekstDto +import no.nav.familie.tilbake.api.dto.OpprettRevurderingDto +import no.nav.familie.tilbake.behandling.BehandlingService +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.sikkerhet.AuditLoggerEvent +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.HenteParam +import no.nav.familie.tilbake.sikkerhet.Rolletilgangssjekk +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequestMapping("/api/behandling") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class BehandlingController( + private val behandlingService: BehandlingService, + private val stegService: StegService, +) { + + @Operation(summary = "Opprett tilbakekrevingsbehandling automatisk, kan kalles av fagsystem, batch") + @PostMapping( + path = ["/v1"], + consumes = [MediaType.APPLICATION_JSON_VALUE], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk(Behandlerrolle.SAKSBEHANDLER, "Oppretter tilbakekreving", AuditLoggerEvent.CREATE) + fun opprettBehandling( + @Valid @RequestBody + opprettTilbakekrevingRequest: OpprettTilbakekrevingRequest, + ): Ressurs { + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + return Ressurs.success(behandling.eksternBrukId.toString(), melding = "Behandling er opprettet.") + } + + @Operation(summary = "Opprett tilbakekrevingsbehandling manuelt") + @PostMapping( + path = ["/manuelt/task/v1"], + consumes = [MediaType.APPLICATION_JSON_VALUE], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk(Behandlerrolle.SAKSBEHANDLER, "Oppretter tilbakekreving manuelt", AuditLoggerEvent.CREATE) + fun opprettBehandlingManuellTask( + @Valid @RequestBody + opprettManueltTilbakekrevingRequest: OpprettManueltTilbakekrevingRequest, + ): Ressurs { + behandlingService.opprettBehandlingManuellTask(opprettManueltTilbakekrevingRequest) + return Ressurs.success("Manuell opprettelse av tilbakekreving er startet") + } + + @Operation(summary = "Opprett tilbakekrevingsrevurdering") + @PostMapping( + path = ["/revurdering/v1"], + consumes = [MediaType.APPLICATION_JSON_VALUE], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk(Behandlerrolle.SAKSBEHANDLER, "Oppretter tilbakekrevingsrevurdering", AuditLoggerEvent.CREATE) + fun opprettRevurdering( + @Valid @RequestBody + opprettRevurderingDto: OpprettRevurderingDto, + ): Ressurs { + val behandling = behandlingService.opprettRevurdering(opprettRevurderingDto) + return Ressurs.success(behandling.eksternBrukId.toString(), melding = "Revurdering er opprettet.") + } + + @Operation(summary = "Hent behandling") + @GetMapping( + path = ["/v1/{behandlingId}"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + minimumBehandlerrolle = Behandlerrolle.VEILEDER, + handling = "Henter tilbakekrevingsbehandling", + AuditLoggerEvent.ACCESS, + henteParam = HenteParam.BEHANDLING_ID, + ) + fun hentBehandling(@PathVariable("behandlingId") behandlingId: UUID): Ressurs { + return Ressurs.success(behandlingService.hentBehandling(behandlingId)) + } + + @Operation(summary = "Utfør behandlingssteg og fortsett behandling til neste steg") + @PostMapping( + path = ["{behandlingId}/steg/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + // Rollen blir endret til BESLUTTER i Tilgangskontroll for FatteVedtak steg + @Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Utfører behandlingens aktiv steg og fortsetter den til neste steg", + AuditLoggerEvent.UPDATE, + HenteParam.BEHANDLING_ID, + ) + fun utførBehandlingssteg( + @PathVariable("behandlingId") behandlingId: UUID, + @Valid @RequestBody + behandlingsstegDto: BehandlingsstegDto, + ): Ressurs { + // Oppdaterer ansvarlig saksbehandler først slik at historikkinnslag får riktig saksbehandler + // Hvis det feiler noe,bør det rullet tilbake helt siden begge 2 er på samme transaksjon + if (stegService.kanAnsvarligSaksbehandlerOppdateres(behandlingId, behandlingsstegDto)) { + behandlingService.oppdaterAnsvarligSaksbehandler(behandlingId) + } + stegService.håndterSteg(behandlingId, behandlingsstegDto) + + return Ressurs.success("OK") + } + + @Operation(summary = "Sett behandling på vent") + @PutMapping( + path = ["{behandlingId}/vent/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Setter saksbehandler behandling på vent eller utvider fristen", + AuditLoggerEvent.UPDATE, + HenteParam.BEHANDLING_ID, + ) + fun settBehandlingPåVent( + @PathVariable("behandlingId") behandlingId: UUID, + @Valid @RequestBody + behandlingPåVentDto: BehandlingPåVentDto, + ): Ressurs { + behandlingService.settBehandlingPåVent(behandlingId, behandlingPåVentDto) + return Ressurs.success("OK") + } + + @Operation(summary = "Ta behandling av vent") + @PutMapping( + path = ["{behandlingId}/gjenoppta/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Saksbehandler tar behandling av vent etter å motta brukerrespons eller dokumentasjon", + AuditLoggerEvent.UPDATE, + HenteParam.BEHANDLING_ID, + ) + fun taBehandlingAvVent(@PathVariable("behandlingId") behandlingId: UUID): Ressurs { + behandlingService.taBehandlingAvvent(behandlingId) + return Ressurs.success("OK") + } + + @Operation(summary = "Henlegg behandling") + @PutMapping( + path = ["{behandlingId}/henlegg/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Saksbehandler henlegger behandling", + AuditLoggerEvent.UPDATE, + HenteParam.BEHANDLING_ID, + ) + fun henleggBehandling( + @PathVariable("behandlingId") behandlingId: UUID, + @Valid @RequestBody + henleggelsesbrevFritekstDto: HenleggelsesbrevFritekstDto, + ): Ressurs { + behandlingService.henleggBehandling(behandlingId, henleggelsesbrevFritekstDto) + return Ressurs.success("OK") + } + + @Operation(summary = "Bytt enhet") + @PutMapping( + path = ["{behandlingId}/bytt-enhet/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Saksbehandler bytter enhet på behandling", + AuditLoggerEvent.UPDATE, + HenteParam.BEHANDLING_ID, + ) + fun byttEnhet( + @PathVariable("behandlingId") behandlingId: UUID, + @Valid @RequestBody + byttEnhetDto: ByttEnhetDto, + ): Ressurs { + behandlingService.byttBehandlendeEnhet(behandlingId, byttEnhetDto) + return Ressurs.success("OK") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/BeregningController.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/BeregningController.kt new file mode 100644 index 000000000..0b411f53d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/BeregningController.kt @@ -0,0 +1,64 @@ +package no.nav.familie.tilbake.api + +import io.swagger.v3.oas.annotations.Operation +import jakarta.validation.Valid +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.tilbake.api.dto.BeregnetPerioderDto +import no.nav.familie.tilbake.api.dto.BeregningsresultatDto +import no.nav.familie.tilbake.beregning.TilbakekrevingsberegningService +import no.nav.familie.tilbake.sikkerhet.AuditLoggerEvent +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.HenteParam +import no.nav.familie.tilbake.sikkerhet.Rolletilgangssjekk +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequestMapping("/api/behandling/") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class BeregningController(val tilbakekrevingsberegningService: TilbakekrevingsberegningService) { + + @Operation(summary = "Beregn feilutbetalt beløp for nye delte perioder") + @PostMapping( + path = ["{behandlingId}/beregn/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + minimumBehandlerrolle = Behandlerrolle.SAKSBEHANDLER, + handling = "Beregner feilutbetalt beløp for nye delte perioder", + AuditLoggerEvent.ACCESS, + henteParam = HenteParam.BEHANDLING_ID, + ) + fun beregnBeløp( + @PathVariable("behandlingId") behandlingId: UUID, + @Valid @RequestBody + perioder: List, + ): Ressurs { + return Ressurs.success(tilbakekrevingsberegningService.beregnBeløp(behandlingId, perioder)) + } + + @Operation(summary = "Hent beregningsresultat") + @GetMapping( + path = ["{behandlingId}/beregn/resultat/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + minimumBehandlerrolle = Behandlerrolle.VEILEDER, + handling = "Henter beregningsresultat", + AuditLoggerEvent.ACCESS, + henteParam = HenteParam.BEHANDLING_ID, + ) + fun hentBeregningsresultat(@PathVariable("behandlingId") behandlingId: UUID): Ressurs { + return Ressurs.success(tilbakekrevingsberegningService.hentBeregningsresultat(behandlingId)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/DokumentController.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/DokumentController.kt new file mode 100644 index 000000000..e177d95aa --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/DokumentController.kt @@ -0,0 +1,137 @@ +package no.nav.familie.tilbake.api + +import io.swagger.v3.oas.annotations.Operation +import jakarta.validation.Valid +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.tilbakekreving.ForhåndsvisVarselbrevRequest +import no.nav.familie.tilbake.api.dto.BestillBrevDto +import no.nav.familie.tilbake.api.dto.ForhåndsvisningHenleggelsesbrevDto +import no.nav.familie.tilbake.api.dto.FritekstavsnittDto +import no.nav.familie.tilbake.api.dto.HentForhåndvisningVedtaksbrevPdfDto +import no.nav.familie.tilbake.behandling.LagreUtkastVedtaksbrevService +import no.nav.familie.tilbake.dokumentbestilling.DokumentbehandlingService +import no.nav.familie.tilbake.dokumentbestilling.brevmaler.Dokumentmalstype +import no.nav.familie.tilbake.dokumentbestilling.henleggelse.HenleggelsesbrevService +import no.nav.familie.tilbake.dokumentbestilling.varsel.VarselbrevService +import no.nav.familie.tilbake.dokumentbestilling.vedtak.Avsnitt +import no.nav.familie.tilbake.dokumentbestilling.vedtak.VedtaksbrevService +import no.nav.familie.tilbake.sikkerhet.AuditLoggerEvent +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.HenteParam +import no.nav.familie.tilbake.sikkerhet.Rolletilgangssjekk +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequestMapping("/api/dokument") +@ProtectedWithClaims(issuer = "azuread") +class DokumentController( + private val varselbrevService: VarselbrevService, + private val dokumentbehandlingService: DokumentbehandlingService, + private val henleggelsesbrevService: HenleggelsesbrevService, + private val vedtaksbrevService: VedtaksbrevService, + private val lagreUtkastVedtaksbrevService: LagreUtkastVedtaksbrevService, +) { + + @Operation(summary = "Bestill brevsending") + @PostMapping("/bestill") + @Rolletilgangssjekk(Behandlerrolle.SAKSBEHANDLER, "Sender brev", AuditLoggerEvent.CREATE) + fun bestillBrev( + @RequestBody @Valid + bestillBrevDto: BestillBrevDto, + ): Ressurs { + val maltype: Dokumentmalstype = bestillBrevDto.brevmalkode + dokumentbehandlingService.bestillBrev(bestillBrevDto.behandlingId, maltype, bestillBrevDto.fritekst) + return Ressurs.success(null) + } + + @Operation(summary = "Forhåndsvis brev") + @PostMapping("/forhandsvis") + @Rolletilgangssjekk(Behandlerrolle.SAKSBEHANDLER, "Forhåndsviser brev", AuditLoggerEvent.ACCESS) + fun forhåndsvisBrev( + @RequestBody @Valid + bestillBrevDto: BestillBrevDto, + ): Ressurs { + val dokument: ByteArray = dokumentbehandlingService.forhåndsvisBrev( + bestillBrevDto.behandlingId, + bestillBrevDto.brevmalkode, + bestillBrevDto.fritekst, + ) + return Ressurs.success(dokument) + } + + @Operation(summary = "Forhåndsvis varselbrev") + @PostMapping( + "/forhandsvis-varselbrev", + produces = [MediaType.APPLICATION_PDF_VALUE], + ) + @Rolletilgangssjekk(Behandlerrolle.SAKSBEHANDLER, "Forhåndsviser brev", AuditLoggerEvent.ACCESS) + fun hentForhåndsvisningVarselbrev( + @Valid @RequestBody + forhåndsvisVarselbrevRequest: ForhåndsvisVarselbrevRequest, + ): ByteArray { + return varselbrevService.hentForhåndsvisningVarselbrev(forhåndsvisVarselbrevRequest) + } + + @Operation(summary = "Forhåndsvis henleggelsesbrev") + @PostMapping( + "/forhandsvis-henleggelsesbrev", + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk(Behandlerrolle.SAKSBEHANDLER, "Forhåndsviser henleggelsesbrev", AuditLoggerEvent.ACCESS) + fun hentForhåndsvisningHenleggelsesbrev( + @Valid @RequestBody + dto: ForhåndsvisningHenleggelsesbrevDto, + ): Ressurs { + return Ressurs.success(henleggelsesbrevService.hentForhåndsvisningHenleggelsesbrev(dto.behandlingId, dto.fritekst)) + } + + @Operation(summary = "Forhåndsvis vedtaksbrev") + @PostMapping( + "/forhandsvis-vedtaksbrev", + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk(Behandlerrolle.SAKSBEHANDLER, "Forhåndsviser brev", AuditLoggerEvent.ACCESS) + fun hentForhåndsvisningVedtaksbrev( + @Valid @RequestBody + dto: HentForhåndvisningVedtaksbrevPdfDto, + ): Ressurs { + return Ressurs.success(vedtaksbrevService.hentForhåndsvisningVedtaksbrevMedVedleggSomPdf(dto)) + } + + @Operation(summary = "Hent vedtaksbrevtekst") + @GetMapping( + "/vedtaksbrevtekst/{behandlingId}", + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk(Behandlerrolle.VEILEDER, "Henter vedtaksbrevtekst", AuditLoggerEvent.ACCESS, HenteParam.BEHANDLING_ID) + fun hentVedtaksbrevtekst(@PathVariable behandlingId: UUID): Ressurs> { + return Ressurs.success(vedtaksbrevService.hentVedtaksbrevSomTekst(behandlingId)) + } + + @Operation(summary = "Lagre utkast av vedtaksbrev") + @PostMapping( + "/vedtaksbrevtekst/{behandlingId}/utkast", + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Lagrer utkast av vedtaksbrev", + AuditLoggerEvent.UPDATE, + HenteParam.BEHANDLING_ID, + ) + fun lagreUtkastVedtaksbrev( + @PathVariable behandlingId: UUID, + @RequestBody fritekstavsnitt: FritekstavsnittDto, + ): Ressurs { + lagreUtkastVedtaksbrevService.lagreUtkast(behandlingId, fritekstavsnitt) + return Ressurs.success("OK") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/FagsakController.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/FagsakController.kt new file mode 100644 index 000000000..1a1a562e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/FagsakController.kt @@ -0,0 +1,125 @@ +package no.nav.familie.tilbake.api + +import io.swagger.v3.oas.annotations.Operation +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.klage.FagsystemVedtak +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandling +import no.nav.familie.kontrakter.felles.tilbakekreving.FinnesBehandlingResponse +import no.nav.familie.kontrakter.felles.tilbakekreving.KanBehandlingOpprettesManueltRespons +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.api.dto.FagsakDto +import no.nav.familie.tilbake.behandling.FagsakService +import no.nav.familie.tilbake.sikkerhet.AuditLoggerEvent +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.HenteParam +import no.nav.familie.tilbake.sikkerhet.Rolletilgangssjekk +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class FagsakController(private val fagsakService: FagsakService) { + + @Operation(summary = "Hent fagsak informasjon med bruker og behandlinger") + @GetMapping( + path = ["/fagsystem/{fagsystem}/fagsak/{eksternFagsakId}/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.VEILEDER, + "Henter fagsak informasjon med bruker og behandlinger", + AuditLoggerEvent.ACCESS, + HenteParam.FAGSYSTEM_OG_EKSTERN_FAGSAK_ID, + ) + fun hentFagsak( + @PathVariable fagsystem: Fagsystem, + @PathVariable eksternFagsakId: String, + ): Ressurs { + return Ressurs.success(fagsakService.hentFagsak(fagsystem, eksternFagsakId)) + } + + @Operation(summary = "Sjekk om det finnes en åpen tilbakekrevingsbehandling") + @GetMapping( + path = ["/fagsystem/{fagsystem}/fagsak/{eksternFagsakId}/finnesApenBehandling/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.VEILEDER, + "Sjekk om det finnes en åpen tilbakekrevingsbehandling", + AuditLoggerEvent.ACCESS, + HenteParam.FAGSYSTEM_OG_EKSTERN_FAGSAK_ID, + ) + fun finnesÅpenTilbakekrevingsbehandling( + @PathVariable fagsystem: Fagsystem, + @PathVariable eksternFagsakId: String, + ): Ressurs { + return Ressurs.success( + fagsakService.finnesÅpenTilbakekrevingsbehandling( + fagsystem = fagsystem, + eksternFagsakId = eksternFagsakId, + ), + ) + } + + @Operation(summary = "Sjekk om det er mulig å opprette behandling manuelt") + @GetMapping( + path = ["/ytelsestype/{ytelsestype}/fagsak/{eksternFagsakId}/kanBehandlingOpprettesManuelt/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Sjekk om det er mulig å opprette behandling manuelt", + AuditLoggerEvent.ACCESS, + HenteParam.YTELSESTYPE_OG_EKSTERN_FAGSAK_ID, + ) + fun kanBehandlingOpprettesManuelt( + @PathVariable ytelsestype: Ytelsestype, + @PathVariable eksternFagsakId: String, + ): Ressurs { + return Ressurs.success(fagsakService.kanBehandlingOpprettesManuelt(eksternFagsakId, ytelsestype)) + } + + @Operation(summary = "Hent behandlinger, kalles av fagsystem") + @GetMapping( + path = ["/fagsystem/{fagsystem}/fagsak/{eksternFagsakId}/behandlinger/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + minimumBehandlerrolle = Behandlerrolle.VEILEDER, + handling = "Henter behandlinger for bruk i fagsystem", + AuditLoggerEvent.ACCESS, + henteParam = HenteParam.FAGSYSTEM_OG_EKSTERN_FAGSAK_ID, + ) + fun hentBehandlingerForFagsystem( + @PathVariable fagsystem: Fagsystem, + @PathVariable eksternFagsakId: String, + ): Ressurs> { + return Ressurs.success(fagsakService.hentBehandlingerForFagsak(fagsystem, eksternFagsakId)) + } + + @Operation(summary = "Hent behandlinger, kalles av fagsystem") + @GetMapping( + path = ["/fagsystem/{fagsystem}/fagsak/{eksternFagsakId}/vedtak/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + minimumBehandlerrolle = Behandlerrolle.VEILEDER, + handling = "Henter behandlinger for bruk i fagsystem", + AuditLoggerEvent.ACCESS, + henteParam = HenteParam.FAGSYSTEM_OG_EKSTERN_FAGSAK_ID, + ) + fun hentVedtakForFagsystem( + @PathVariable fagsystem: Fagsystem, + @PathVariable eksternFagsakId: String, + ): Ressurs> { + return Ressurs.success(fagsakService.hentVedtakForFagsak(fagsystem, eksternFagsakId)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/FaktaFeilutbetalingController.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/FaktaFeilutbetalingController.kt new file mode 100644 index 000000000..eb29b99c3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/FaktaFeilutbetalingController.kt @@ -0,0 +1,45 @@ +package no.nav.familie.tilbake.api + +import io.swagger.v3.oas.annotations.Operation +import jakarta.validation.constraints.NotNull +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.tilbake.api.dto.FaktaFeilutbetalingDto +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingService +import no.nav.familie.tilbake.sikkerhet.AuditLoggerEvent +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.HenteParam +import no.nav.familie.tilbake.sikkerhet.Rolletilgangssjekk +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequestMapping("/api") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class FaktaFeilutbetalingController(val faktaFeilutbetalingService: FaktaFeilutbetalingService) { + + @Operation(summary = "Hent fakta om feilutbetaling") + @GetMapping( + path = ["/behandling/{behandlingId}/fakta/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.VEILEDER, + "Henter fakta om feilutbetaling for en gitt behandling", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + fun hentFaktaomfeilutbetaling( + @NotNull + @PathVariable("behandlingId") + behandlingId: UUID, + ): Ressurs { + return Ressurs.success(faktaFeilutbetalingService.hentFaktaomfeilutbetaling(behandlingId)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/ForeldelseController.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/ForeldelseController.kt new file mode 100644 index 000000000..008fd4c06 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/ForeldelseController.kt @@ -0,0 +1,40 @@ +package no.nav.familie.tilbake.api + +import io.swagger.v3.oas.annotations.Operation +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.tilbake.api.dto.VurdertForeldelseDto +import no.nav.familie.tilbake.foreldelse.ForeldelseService +import no.nav.familie.tilbake.sikkerhet.AuditLoggerEvent +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.HenteParam +import no.nav.familie.tilbake.sikkerhet.Rolletilgangssjekk +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequestMapping("/api/behandling/") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class ForeldelseController(val foreldelseService: ForeldelseService) { + + @Operation(summary = "Hent foreldelsesinformasjon") + @GetMapping( + path = ["{behandlingId}/foreldelse/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.VEILEDER, + "Henter foreldelsesinformasjon for en gitt behandling", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + fun hentVurdertForeldelse(@PathVariable("behandlingId") behandlingId: UUID): Ressurs { + return Ressurs.success(foreldelseService.hentVurdertForeldelse(behandlingId)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/JournalpostController.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/JournalpostController.kt new file mode 100644 index 000000000..b51a4b2e9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/JournalpostController.kt @@ -0,0 +1,42 @@ +package no.nav.familie.tilbake.api + +import io.swagger.v3.oas.annotations.Operation +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.journalpost.Journalpost +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.JournalføringService +import no.nav.familie.tilbake.sikkerhet.AuditLoggerEvent +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.HenteParam +import no.nav.familie.tilbake.sikkerhet.Rolletilgangssjekk +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequestMapping("/api/behandling") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class JournalpostController(private val journalføringService: JournalføringService) { + + @Operation(summary = "Hent dokument fra journalføring") + @GetMapping("/{behandlingId}/journalpost/{journalpostId}/dokument/{dokumentInfoId}") + @Rolletilgangssjekk(Behandlerrolle.VEILEDER, "Henter journalført dokument", AuditLoggerEvent.ACCESS, HenteParam.BEHANDLING_ID) + fun hentDokument( + @PathVariable behandlingId: UUID, + @PathVariable journalpostId: String, + @PathVariable dokumentInfoId: String, + ): Ressurs { + return Ressurs.success(journalføringService.hentDokument(journalpostId, dokumentInfoId), "OK") + } + + @Operation(summary = "Hent journalpost informasjon") + @GetMapping("/{behandlingId}/journalposter") + @Rolletilgangssjekk(Behandlerrolle.VEILEDER, "Henter journalført dokument", AuditLoggerEvent.ACCESS, HenteParam.BEHANDLING_ID) + fun hentJournalposter(@PathVariable behandlingId: UUID): Ressurs> { + return Ressurs.success(journalføringService.hentJournalposter(behandlingId)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/ManuellBrevmottakerController.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/ManuellBrevmottakerController.kt new file mode 100644 index 000000000..d9c52817d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/ManuellBrevmottakerController.kt @@ -0,0 +1,135 @@ +package no.nav.familie.tilbake.api + +import io.swagger.v3.oas.annotations.Operation +import jakarta.validation.Valid +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.tilbake.api.dto.ManuellBrevmottakerRequestDto +import no.nav.familie.tilbake.api.dto.ManuellBrevmottakerResponsDto +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.ManuellBrevmottakerMapper +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.ManuellBrevmottakerService +import no.nav.familie.tilbake.sikkerhet.AuditLoggerEvent +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.HenteParam +import no.nav.familie.tilbake.sikkerhet.Rolletilgangssjekk +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequestMapping("/api/brevmottaker/manuell") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class ManuellBrevmottakerController(private val manuellBrevmottakerService: ManuellBrevmottakerService) { + + @Operation(summary = "Legger til brevmottaker manuelt") + @PostMapping( + path = ["/{behandlingId}"], + consumes = [MediaType.APPLICATION_JSON_VALUE], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Legger til brevmottaker manuelt", + AuditLoggerEvent.CREATE, + HenteParam.BEHANDLING_ID, + ) + fun leggTilBrevmottaker( + @PathVariable behandlingId: UUID, + @Valid @RequestBody + manuellBrevmottakerRequestDto: ManuellBrevmottakerRequestDto, + ): Ressurs { + val id = manuellBrevmottakerService.leggTilBrevmottaker(behandlingId, manuellBrevmottakerRequestDto) + return Ressurs.success(id, melding = "Manuell brevmottaker er lagt til.") + } + + @Operation(summary = "Henter manuell brevmottakere") + @GetMapping( + path = ["/{behandlingId}"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + minimumBehandlerrolle = Behandlerrolle.SAKSBEHANDLER, + handling = "Henter manuelle brevmottakere", + auditLoggerEvent = AuditLoggerEvent.ACCESS, + henteParam = HenteParam.BEHANDLING_ID, + ) + fun hentManuellBrevmottakere(@PathVariable behandlingId: UUID): Ressurs> { + return Ressurs + .success( + manuellBrevmottakerService.hentBrevmottakere(behandlingId) + .map { ManuellBrevmottakerMapper.tilRespons(it) }, + ) + } + + @Operation(summary = "Oppdaterer manuell brevmottaker") + @PutMapping( + path = ["/{behandlingId}/{manuellBrevmottakerId}"], + consumes = [MediaType.APPLICATION_JSON_VALUE], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Oppdaterer manuell brevmottaker", + AuditLoggerEvent.UPDATE, + HenteParam.BEHANDLING_ID, + ) + fun oppdaterManuellBrevmottaker( + @PathVariable behandlingId: UUID, + @PathVariable manuellBrevmottakerId: UUID, + @Valid @RequestBody + manuellBrevmottakerRequestDto: ManuellBrevmottakerRequestDto, + ): Ressurs { + manuellBrevmottakerService.oppdaterBrevmottaker(behandlingId, manuellBrevmottakerId, manuellBrevmottakerRequestDto) + return Ressurs.success("", melding = "Manuell brevmottaker er oppdatert") + } + + @Operation(summary = "Fjerner manuell brevmottaker") + @DeleteMapping(path = ["/{behandlingId}/{manuellBrevmottakerId}"]) + @Rolletilgangssjekk( + minimumBehandlerrolle = Behandlerrolle.SAKSBEHANDLER, + handling = "Fjerner manuell brevmottaker", + auditLoggerEvent = AuditLoggerEvent.UPDATE, + ) + fun fjernManuellBrevmottaker( + @PathVariable behandlingId: UUID, + @PathVariable manuellBrevmottakerId: UUID, + ): Ressurs { + manuellBrevmottakerService.fjernBrevmottaker(behandlingId, manuellBrevmottakerId) + return Ressurs.success("", melding = "Manuell brevmottaker er fjernet") + } + + @Operation(summary = "Opprett og aktiver brevmottaker-steg på behandling") + @PostMapping(path = ["/{behandlingId}/aktiver"]) + @Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Oppretter brevmottaker-steg på behandling", + AuditLoggerEvent.CREATE, + HenteParam.BEHANDLING_ID, + ) + fun opprettBrevmottakerSteg(@PathVariable("behandlingId") behandlingId: UUID): Ressurs { + manuellBrevmottakerService.opprettBrevmottakerSteg(behandlingId) + return Ressurs.success("OK") + } + + @Operation(summary = "Fjern manuelle brevmottakere og deaktiver steg") + @PutMapping(path = ["/{behandlingId}/deaktiver"]) + @Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Fjern ev. manuelt registrerte brevmottakere og deaktiver steg.", + AuditLoggerEvent.UPDATE, + HenteParam.BEHANDLING_ID, + ) + fun fjernBrevmottakerSteg(@PathVariable("behandlingId") behandlingId: UUID): Ressurs { + manuellBrevmottakerService.fjernManuelleBrevmottakereOgTilbakeførSteg(behandlingId) + return Ressurs.success("OK") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/TotrinnController.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/TotrinnController.kt new file mode 100644 index 000000000..47d87559a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/TotrinnController.kt @@ -0,0 +1,40 @@ +package no.nav.familie.tilbake.api + +import io.swagger.v3.oas.annotations.Operation +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.tilbake.api.dto.TotrinnsvurderingDto +import no.nav.familie.tilbake.sikkerhet.AuditLoggerEvent +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.HenteParam +import no.nav.familie.tilbake.sikkerhet.Rolletilgangssjekk +import no.nav.familie.tilbake.totrinn.TotrinnService +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequestMapping("/api/behandling") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class TotrinnController(private val totrinnService: TotrinnService) { + + @Operation(summary = "Hent totrinnsvurderinger") + @GetMapping( + path = ["/{behandlingId}/totrinn/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.VEILEDER, + "Henter totrinnsvurderinger for en gitt behandling", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + fun hentTotrinnsvurderinger(@PathVariable("behandlingId") behandlingId: UUID): Ressurs { + return Ressurs.success(totrinnService.hentTotrinnsvurderinger(behandlingId)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/VergeController.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/VergeController.kt new file mode 100644 index 000000000..ce6861a4f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/VergeController.kt @@ -0,0 +1,65 @@ +package no.nav.familie.tilbake.api + +import io.swagger.v3.oas.annotations.Operation +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.tilbake.api.dto.VergeDto +import no.nav.familie.tilbake.behandling.VergeService +import no.nav.familie.tilbake.sikkerhet.AuditLoggerEvent +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.HenteParam +import no.nav.familie.tilbake.sikkerhet.Rolletilgangssjekk +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequestMapping("/api/behandling/v1/{behandlingId}/verge", produces = [MediaType.APPLICATION_JSON_VALUE]) +@ProtectedWithClaims(issuer = "azuread") +@Validated +class VergeController(private val vergeService: VergeService) { + + @Operation(summary = "Opprett verge steg på behandling") + @PostMapping + @Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Oppretter verge steg på behandling", + AuditLoggerEvent.CREATE, + HenteParam.BEHANDLING_ID, + ) + fun opprettVergeSteg(@PathVariable("behandlingId") behandlingId: UUID): Ressurs { + vergeService.opprettVergeSteg(behandlingId) + return Ressurs.success("OK") + } + + @Operation(summary = "Fjern verge") + @PutMapping + @Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Deaktiverer ev. eksisterende verge.", + AuditLoggerEvent.UPDATE, + HenteParam.BEHANDLING_ID, + ) + fun fjernVerge(@PathVariable("behandlingId") behandlingId: UUID): Ressurs { + vergeService.fjernVerge(behandlingId) + return Ressurs.success("OK") + } + + @Operation(summary = "Hent verge") + @GetMapping + @Rolletilgangssjekk( + Behandlerrolle.VEILEDER, + "Henter verge informasjon", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + fun hentVerge(@PathVariable("behandlingId") behandlingId: UUID): Ressurs { + return Ressurs.success(vergeService.hentVerge(behandlingId)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/VersionController.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/VersionController.kt new file mode 100644 index 000000000..e1828d080 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/VersionController.kt @@ -0,0 +1,27 @@ +package no.nav.familie.tilbake.api + +import io.swagger.v3.oas.annotations.Operation +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.security.token.support.core.api.Unprotected +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@Unprotected +@RequestMapping("/api/info") +class VersionController { + + @Operation(summary = "Hent applikasjonsinformasjon") + @GetMapping + fun hentInfo(): Ressurs { + val appImage = System.getenv("NAIS_APP_IMAGE") ?: "udefinert" + val appName = System.getenv("NAIS_APP_NAME") ?: "udefinert" + val namespace = System.getenv("NAIS_NAMESPACE") ?: "udefinert" + val clusterName = System.getenv("NAIS_CLUSTER_NAME") ?: "udefinert" + + return Ressurs.success(Info(appImage = appImage, appName = appName, namespace = namespace, clusterName = clusterName)) + } +} + +data class Info(val appImage: String, val appName: String, val namespace: String, val clusterName: String) diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/Vilk\303\245rsvurderingController.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/Vilk\303\245rsvurderingController.kt" new file mode 100644 index 000000000..f5f3567fd --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/Vilk\303\245rsvurderingController.kt" @@ -0,0 +1,40 @@ +package no.nav.familie.tilbake.api + +import io.swagger.v3.oas.annotations.Operation +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.tilbake.api.dto.VurdertVilkårsvurderingDto +import no.nav.familie.tilbake.sikkerhet.AuditLoggerEvent +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.HenteParam +import no.nav.familie.tilbake.sikkerhet.Rolletilgangssjekk +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingService +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequestMapping("/api/behandling/") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class VilkårsvurderingController(val vilkårsvurderingService: VilkårsvurderingService) { + + @Operation(summary = "Hent vilkårsvurdering") + @GetMapping( + path = ["{behandlingId}/vilkarsvurdering/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.VEILEDER, + "Henter vilkårsvurdering for en gitt behandling", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + fun hentVurdertVilkårsvurdering(@PathVariable("behandlingId") behandlingId: UUID): Ressurs { + return Ressurs.success(vilkårsvurderingService.hentVilkårsvurdering(behandlingId)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BehandlingDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BehandlingDto.kt new file mode 100644 index 000000000..947bade1d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BehandlingDto.kt @@ -0,0 +1,49 @@ +package no.nav.familie.tilbake.api.dto + +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsakstype +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.UUID + +data class BehandlingDto( + val eksternBrukId: UUID, + val behandlingId: UUID, + val erBehandlingHenlagt: Boolean, + val type: Behandlingstype, + val status: Behandlingsstatus, + val opprettetDato: LocalDate, + val avsluttetDato: LocalDate? = null, + val endretTidspunkt: LocalDateTime, + val vedtaksdato: LocalDate? = null, + val enhetskode: String, + val enhetsnavn: String, + val resultatstype: Behandlingsresultatstype? = null, + val ansvarligSaksbehandler: String, + val ansvarligBeslutter: String? = null, + val erBehandlingPåVent: Boolean, + val kanHenleggeBehandling: Boolean, + val kanRevurderingOpprettes: Boolean = false, + val harVerge: Boolean, + val kanEndres: Boolean, + val varselSendt: Boolean, + val behandlingsstegsinfo: List, + val fagsystemsbehandlingId: String, + val eksternFagsakId: String, + val behandlingsårsakstype: Behandlingsårsakstype? = null, + val støtterManuelleBrevmottakere: Boolean, + val harManuelleBrevmottakere: Boolean, + val manuelleBrevmottakere: List, +) + +data class BehandlingsstegsinfoDto( + val behandlingssteg: Behandlingssteg, + val behandlingsstegstatus: Behandlingsstegstatus, + val venteårsak: Venteårsak? = null, + val tidsfrist: LocalDate? = null, +) diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BehandlingP\303\245VentDto.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BehandlingP\303\245VentDto.kt" new file mode 100644 index 000000000..dec376f91 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BehandlingP\303\245VentDto.kt" @@ -0,0 +1,9 @@ +package no.nav.familie.tilbake.api.dto + +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import java.time.LocalDate + +data class BehandlingPåVentDto( + val venteårsak: Venteårsak, + val tidsfrist: LocalDate, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BehandlingsstegDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BehandlingsstegDto.kt new file mode 100644 index 000000000..122e1506a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BehandlingsstegDto.kt @@ -0,0 +1,209 @@ +package no.nav.familie.tilbake.api.dto + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.annotation.JsonTypeName +import jakarta.validation.Valid +import jakarta.validation.constraints.Size +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.tilbakekreving.Vergetype +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import java.math.BigDecimal +import java.time.LocalDate + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY) +@JsonSubTypes( + JsonSubTypes.Type(value = BehandlingsstegVergeDto::class), + JsonSubTypes.Type(value = BehandlingsstegBrevmottakerDto::class), + JsonSubTypes.Type(value = BehandlingsstegFaktaDto::class), + JsonSubTypes.Type(value = BehandlingsstegForeldelseDto::class), + JsonSubTypes.Type(value = BehandlingsstegVilkårsvurderingDto::class), + JsonSubTypes.Type(value = BehandlingsstegForeslåVedtaksstegDto::class), + JsonSubTypes.Type(value = BehandlingsstegFatteVedtaksstegDto::class), +) +abstract class BehandlingsstegDto protected constructor() { + + abstract fun getSteg(): String +} + +@JsonTypeName(BehandlingsstegVergeDto.STEGNAVN) +data class BehandlingsstegVergeDto(val verge: VergeDto) : BehandlingsstegDto() { + + override fun getSteg(): String { + return STEGNAVN + } + + companion object { + + const val STEGNAVN = "VERGE" + } +} + +@JsonTypeName(BehandlingsstegBrevmottakerDto.STEGNAVN) +data class BehandlingsstegBrevmottakerDto(val brevmottakerstegDto: BrevmottakerstegDto) : BehandlingsstegDto() { + + override fun getSteg(): String { + return STEGNAVN + } + + companion object { + const val STEGNAVN = "BREVMOTTAKER" + } +} + +data class VergeDto( + val ident: String? = null, + val orgNr: String? = null, + val type: Vergetype, + val navn: String, + @Size(max = 4000, message = "Begrunnelse er for lang") + val begrunnelse: String?, +) + +data class BrevmottakerstegDto( + @Size(max = 4000, message = "Begrunnelse er for lang") + val begrunnelse: String?, +) + +@JsonTypeName(BehandlingsstegFaktaDto.STEGNAVN) +data class BehandlingsstegFaktaDto( + val feilutbetaltePerioder: List, + @Size(max = 1500, message = "begrunnelse er for lang") + val begrunnelse: String, +) : BehandlingsstegDto() { + + override fun getSteg(): String { + return STEGNAVN + } + + companion object { + + const val STEGNAVN = "FAKTA" + } +} + +data class FaktaFeilutbetalingsperiodeDto( + val periode: Datoperiode, + val hendelsestype: Hendelsestype, + val hendelsesundertype: Hendelsesundertype, +) + +@JsonTypeName(BehandlingsstegForeldelseDto.STEGNAVN) +data class BehandlingsstegForeldelseDto(val foreldetPerioder: List) : BehandlingsstegDto() { + + override fun getSteg(): String { + return STEGNAVN + } + + companion object { + + const val STEGNAVN = "FORELDELSE" + } +} + +data class ForeldelsesperiodeDto( + val periode: Datoperiode, + @Size(max = 1500, message = "begrunnelse er for lang") + val begrunnelse: String, + val foreldelsesvurderingstype: Foreldelsesvurderingstype, + val foreldelsesfrist: LocalDate? = null, + val oppdagelsesdato: LocalDate? = null, +) + +@JsonTypeName(BehandlingsstegVilkårsvurderingDto.STEGNAVN) +data class BehandlingsstegVilkårsvurderingDto(val vilkårsvurderingsperioder: List) : + BehandlingsstegDto() { + + override fun getSteg(): String { + return STEGNAVN + } + + companion object { + + const val STEGNAVN = "VILKÅRSVURDERING" + } +} + +data class VilkårsvurderingsperiodeDto( + val periode: Datoperiode, + val vilkårsvurderingsresultat: Vilkårsvurderingsresultat, + @Size(max = 1500, message = "begrunnelse er for lang") + val begrunnelse: String, + val godTroDto: GodTroDto? = null, + val aktsomhetDto: AktsomhetDto? = null, +) + +data class GodTroDto( + val beløpErIBehold: Boolean, + val beløpTilbakekreves: BigDecimal? = null, + @Size(max = 1500, message = "begrunnelse er for lang") + val begrunnelse: String, +) + +data class AktsomhetDto( + val aktsomhet: Aktsomhet, + val ileggRenter: Boolean? = null, + val andelTilbakekreves: BigDecimal? = null, + val beløpTilbakekreves: BigDecimal? = null, + @Size(max = 1500, message = "begrunnelse er for lang") + val begrunnelse: String, + val særligeGrunner: List? = null, + val særligeGrunnerTilReduksjon: Boolean = false, + val tilbakekrevSmåbeløp: Boolean = true, + val særligeGrunnerBegrunnelse: String? = null, +) + +data class SærligGrunnDto( + val særligGrunn: SærligGrunn, + @Size(max = 1500, message = "begrunnelse er for lang") + val begrunnelse: String? = null, +) + +@JsonTypeName(BehandlingsstegForeslåVedtaksstegDto.STEGNAVN) +data class BehandlingsstegForeslåVedtaksstegDto(val fritekstavsnitt: FritekstavsnittDto) : BehandlingsstegDto() { + + override fun getSteg(): String { + return STEGNAVN + } + + companion object { + + const val STEGNAVN = "FORESLÅ_VEDTAK" + } +} + +data class FritekstavsnittDto( + @Size(max = 10000, message = "Oppsummeringstekst er for lang") + var oppsummeringstekst: String? = null, + @Size(max = 100, message = "For mange perioder") + @Valid + var perioderMedTekst: List, +) + +@JsonTypeName(BehandlingsstegFatteVedtaksstegDto.STEGNAVN) +data class BehandlingsstegFatteVedtaksstegDto(val totrinnsvurderinger: List) : BehandlingsstegDto() { + + override fun getSteg(): String { + return STEGNAVN + } + + companion object { + + const val STEGNAVN = "FATTE_VEDTAK" + } +} + +data class VurdertTotrinnDto( + val behandlingssteg: Behandlingssteg, + val godkjent: Boolean, + @Size(max = 2000, message = "begrunnelse er for lang") + val begrunnelse: String? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BeregningsresultatDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BeregningsresultatDto.kt new file mode 100644 index 000000000..dd4918e94 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BeregningsresultatDto.kt @@ -0,0 +1,21 @@ +package no.nav.familie.tilbake.api.dto + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vurdering +import java.math.BigDecimal + +data class BeregningsresultatDto( + val beregningsresultatsperioder: List, + val vedtaksresultat: Vedtaksresultat, +) + +data class BeregningsresultatsperiodeDto( + val periode: Datoperiode, + val vurdering: Vurdering? = null, + val feilutbetaltBeløp: BigDecimal, + val andelAvBeløp: BigDecimal? = null, + val renteprosent: BigDecimal? = null, + val tilbakekrevingsbeløp: BigDecimal? = null, + val tilbakekrevesBeløpEtterSkatt: BigDecimal? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BestillBrevDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BestillBrevDto.kt new file mode 100644 index 000000000..4b46ab52c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/BestillBrevDto.kt @@ -0,0 +1,12 @@ +package no.nav.familie.tilbake.api.dto + +import jakarta.validation.constraints.Size +import no.nav.familie.tilbake.dokumentbestilling.brevmaler.Dokumentmalstype +import java.util.UUID + +class BestillBrevDto( + val behandlingId: UUID, + val brevmalkode: Dokumentmalstype, + @Size(min = 1, max = 3000) + val fritekst: String, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/ByttEnhetDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/ByttEnhetDto.kt new file mode 100644 index 000000000..2ad81931a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/ByttEnhetDto.kt @@ -0,0 +1,9 @@ +package no.nav.familie.tilbake.api.dto + +import jakarta.validation.constraints.Size + +data class ByttEnhetDto( + val enhet: String, + @Size(max = 400, message = "Begrunnelse er for lang") + val begrunnelse: String, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/Datoperiode.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/Datoperiode.kt new file mode 100644 index 000000000..717b03ba1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/Datoperiode.kt @@ -0,0 +1,8 @@ +package no.nav.familie.tilbake.api.dto + +import no.nav.familie.kontrakter.felles.Datoperiode +import java.math.BigDecimal + +data class BeregnetPeriodeDto(val periode: Datoperiode, val feilutbetaltBeløp: BigDecimal) + +data class BeregnetPerioderDto(val beregnetPerioder: List) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/FagsakDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/FagsakDto.kt new file mode 100644 index 000000000..47fb6dd19 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/FagsakDto.kt @@ -0,0 +1,40 @@ +package no.nav.familie.tilbake.api.dto + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.integration.pdl.internal.Kjønn +import java.time.LocalDate +import java.util.UUID + +data class FagsakDto( + val eksternFagsakId: String, + val ytelsestype: Ytelsestype, + val fagsystem: Fagsystem, + val språkkode: Språkkode, + val bruker: BrukerDto, + val behandlinger: List, + val institusjon: InstitusjonDto? = null, +) + +data class BrukerDto( + val personIdent: String, + val navn: String, + val fødselsdato: LocalDate, + val kjønn: Kjønn, + val dødsdato: LocalDate? = null, +) + +data class BehandlingsoppsummeringDto( + val behandlingId: UUID, + val eksternBrukId: UUID, + val type: Behandlingstype, + val status: Behandlingsstatus, +) + +data class InstitusjonDto( + val organisasjonsnummer: String, + val navn: String, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/FaktaFeilutbetalingDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/FaktaFeilutbetalingDto.kt new file mode 100644 index 000000000..8d381d754 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/FaktaFeilutbetalingDto.kt @@ -0,0 +1,27 @@ +package no.nav.familie.tilbake.api.dto + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import java.math.BigDecimal +import java.time.LocalDate + +data class FaktaFeilutbetalingDto( + val varsletBeløp: Long? = null, + val totalFeilutbetaltPeriode: Datoperiode, + val feilutbetaltePerioder: List, + val totaltFeilutbetaltBeløp: BigDecimal, + val revurderingsvedtaksdato: LocalDate, + val begrunnelse: String, + val faktainfo: Faktainfo, +) { + val gjelderDødsfall get() = feilutbetaltePerioder.any { it.hendelsestype == Hendelsestype.DØDSFALL } +} + +data class FeilutbetalingsperiodeDto( + val periode: Datoperiode, + val feilutbetaltBeløp: BigDecimal, + val hendelsestype: Hendelsestype? = null, + val hendelsesundertype: Hendelsesundertype? = null, +) diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/Forh\303\245ndsvisningHenleggelsesbrevDto.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/Forh\303\245ndsvisningHenleggelsesbrevDto.kt" new file mode 100644 index 000000000..d624fd99c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/Forh\303\245ndsvisningHenleggelsesbrevDto.kt" @@ -0,0 +1,10 @@ +package no.nav.familie.tilbake.api.dto + +import jakarta.validation.constraints.Size +import java.util.UUID + +data class ForhåndsvisningHenleggelsesbrevDto( + val behandlingId: UUID, + @Size(max = 1500, message = "Fritekst er for lang") + val fritekst: String?, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/HenleggelsesbrevFritekstDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/HenleggelsesbrevFritekstDto.kt new file mode 100644 index 000000000..5a876f81f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/HenleggelsesbrevFritekstDto.kt @@ -0,0 +1,11 @@ +package no.nav.familie.tilbake.api.dto + +import jakarta.validation.constraints.Size +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype + +data class HenleggelsesbrevFritekstDto( + val behandlingsresultatstype: Behandlingsresultatstype, + val begrunnelse: String, + @Size(max = 1500, message = "Fritekst er for lang") + val fritekst: String? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/HentFagsystemsbehandlingRequestDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/HentFagsystemsbehandlingRequestDto.kt new file mode 100644 index 000000000..37e580493 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/HentFagsystemsbehandlingRequestDto.kt @@ -0,0 +1,9 @@ +package no.nav.familie.tilbake.api.dto + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype + +data class HentFagsystemsbehandlingRequestDto( + val ytelsestype: Ytelsestype, + val eksternFagsakId: String, + val eksternId: String, +) diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/HentForh\303\245ndvisningVedtaksbrevPdfDto.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/HentForh\303\245ndvisningVedtaksbrevPdfDto.kt" new file mode 100644 index 000000000..0e87ee9f7 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/HentForh\303\245ndvisningVedtaksbrevPdfDto.kt" @@ -0,0 +1,13 @@ +package no.nav.familie.tilbake.api.dto + +import jakarta.validation.Valid +import jakarta.validation.constraints.Size +import java.util.UUID + +class HentForhåndvisningVedtaksbrevPdfDto( + var behandlingId: UUID, + @Size(max = 10000, message = "Oppsummeringstekst er for lang") + var oppsummeringstekst: String? = null, + @Size(max = 100, message = "For mange perioder") @Valid + var perioderMedTekst: List, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/ManuellBrevmottakerRequestDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/ManuellBrevmottakerRequestDto.kt new file mode 100644 index 000000000..2606c1382 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/ManuellBrevmottakerRequestDto.kt @@ -0,0 +1,11 @@ +package no.nav.familie.tilbake.api.dto + +import no.nav.familie.kontrakter.felles.tilbakekreving.Brevmottaker +import java.util.UUID + +typealias ManuellBrevmottakerRequestDto = Brevmottaker + +data class ManuellBrevmottakerResponsDto( + val id: UUID, + val brevmottaker: Brevmottaker, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/OpprettRevurderingDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/OpprettRevurderingDto.kt new file mode 100644 index 000000000..581b08533 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/OpprettRevurderingDto.kt @@ -0,0 +1,11 @@ +package no.nav.familie.tilbake.api.dto + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsakstype +import java.util.UUID + +data class OpprettRevurderingDto( + val ytelsestype: Ytelsestype, // kun brukes for tilgangskontroll + val originalBehandlingId: UUID, + val årsakstype: Behandlingsårsakstype, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/PeriodeMedTekstDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/PeriodeMedTekstDto.kt new file mode 100644 index 000000000..481695ba8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/PeriodeMedTekstDto.kt @@ -0,0 +1,18 @@ +package no.nav.familie.tilbake.api.dto + +import jakarta.validation.constraints.Size +import no.nav.familie.kontrakter.felles.Datoperiode + +class PeriodeMedTekstDto( + val periode: Datoperiode, + @Size(max = 4000, message = "Fritekst for fakta er for lang") + val faktaAvsnitt: String? = null, + @Size(max = 4000, message = "Fritekst for foreldelse er for lang") + val foreldelseAvsnitt: String? = null, + @Size(max = 4000, message = "Fritekst for vilkår er for lang") + val vilkårAvsnitt: String? = null, + @Size(max = 4000, message = "Fritekst for særlige grunner er for lang") + val særligeGrunnerAvsnitt: String? = null, + @Size(max = 4000, message = "Fritekst for særlige grunner annet er for lang") + val særligeGrunnerAnnetAvsnitt: String? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/TotrinnsvurderingDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/TotrinnsvurderingDto.kt new file mode 100644 index 000000000..6235eba7c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/TotrinnsvurderingDto.kt @@ -0,0 +1,11 @@ +package no.nav.familie.tilbake.api.dto + +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg + +data class TotrinnsvurderingDto(val totrinnsstegsinfo: List) + +data class Totrinnsstegsinfo( + val behandlingssteg: Behandlingssteg, + val godkjent: Boolean? = null, + val begrunnelse: String? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/VurdertForeldelseDto.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/VurdertForeldelseDto.kt new file mode 100644 index 000000000..b957e2e1d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/VurdertForeldelseDto.kt @@ -0,0 +1,17 @@ +package no.nav.familie.tilbake.api.dto + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import java.math.BigDecimal +import java.time.LocalDate + +data class VurdertForeldelseDto(val foreldetPerioder: List) + +data class VurdertForeldelsesperiodeDto( + val periode: Datoperiode, + val feilutbetaltBeløp: BigDecimal, + val begrunnelse: String? = null, + val foreldelsesvurderingstype: Foreldelsesvurderingstype? = null, + val foreldelsesfrist: LocalDate? = null, + val oppdagelsesdato: LocalDate? = null, +) diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/VurdertVilk\303\245rsvurderingDto.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/VurdertVilk\303\245rsvurderingDto.kt" new file mode 100644 index 000000000..5a4d75354 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/dto/VurdertVilk\303\245rsvurderingDto.kt" @@ -0,0 +1,57 @@ +package no.nav.familie.tilbake.api.dto + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import java.math.BigDecimal + +data class VurdertVilkårsvurderingDto( + val perioder: List, + val rettsgebyr: Long, +) + +data class VurdertVilkårsvurderingsperiodeDto( + val periode: Datoperiode, + val feilutbetaltBeløp: BigDecimal, + val hendelsestype: Hendelsestype, + val reduserteBeløper: List = listOf(), + val aktiviteter: List = listOf(), + val vilkårsvurderingsresultatInfo: VurdertVilkårsvurderingsresultatDto? = null, + val begrunnelse: String? = null, + val foreldet: Boolean, +) + +data class VurdertVilkårsvurderingsresultatDto( + val vilkårsvurderingsresultat: Vilkårsvurderingsresultat? = null, + val godTro: VurdertGodTroDto? = null, + val aktsomhet: VurdertAktsomhetDto? = null, +) + +data class VurdertGodTroDto( + val beløpErIBehold: Boolean, + val beløpTilbakekreves: BigDecimal? = null, + val begrunnelse: String, +) + +data class VurdertAktsomhetDto( + val aktsomhet: Aktsomhet, + val ileggRenter: Boolean? = null, + val andelTilbakekreves: BigDecimal? = null, + val beløpTilbakekreves: BigDecimal? = null, + val begrunnelse: String, + val særligeGrunner: List? = null, + val særligeGrunnerTilReduksjon: Boolean = false, + val tilbakekrevSmåbeløp: Boolean = true, + val særligeGrunnerBegrunnelse: String? = null, +) + +data class VurdertSærligGrunnDto( + val særligGrunn: SærligGrunn, + val begrunnelse: String? = null, +) + +data class RedusertBeløpDto(val trekk: Boolean, val beløp: BigDecimal) + +data class AktivitetDto(val aktivitet: String, val beløp: BigDecimal) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/e2e/AutotestController.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/e2e/AutotestController.kt new file mode 100644 index 000000000..2cdfeebce --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/e2e/AutotestController.kt @@ -0,0 +1,147 @@ +package no.nav.familie.tilbake.api.e2e + +import jakarta.validation.Valid +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandling +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRespons +import no.nav.familie.kontrakter.felles.tilbakekreving.Institusjon +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettManueltTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingRequestSendtRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.KafkaConfig +import no.nav.familie.tilbake.kravgrunnlag.task.BehandleKravgrunnlagTask +import no.nav.familie.tilbake.kravgrunnlag.task.BehandleStatusmeldingTask +import no.nav.familie.tilbake.sikkerhet.AuditLoggerEvent +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.HenteParam +import no.nav.familie.tilbake.sikkerhet.Rolletilgangssjekk +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.apache.kafka.clients.producer.ProducerRecord +import org.springframework.context.annotation.Profile +import org.springframework.core.env.Environment +import org.springframework.http.MediaType +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate +import java.util.Properties +import java.util.UUID + +@RestController +@RequestMapping("/api/autotest") +@ProtectedWithClaims(issuer = "azuread") +@Profile("e2e", "local", "integrasjonstest") +class AutotestController( + private val taskService: TaskService, + private val behandlingRepository: BehandlingRepository, + private val requestSendtRepository: HentFagsystemsbehandlingRequestSendtRepository, + private val kafkaTemplate: KafkaTemplate, + private val environment: Environment, +) { + + @PostMapping(path = ["/opprett/kravgrunnlag/"]) + fun opprettKravgrunnlag(@RequestBody kravgrunnlag: String): Ressurs { + taskService.save( + Task( + type = BehandleKravgrunnlagTask.TYPE, + payload = kravgrunnlag, + properties = Properties().apply { + this["callId"] = UUID.randomUUID() + }, + ), + ) + return Ressurs.success("OK") + } + + @PostMapping(path = ["/opprett/statusmelding/"]) + fun opprettStatusmelding(@RequestBody statusmelding: String): Ressurs { + taskService.save( + Task( + type = BehandleStatusmeldingTask.TYPE, + payload = statusmelding, + properties = Properties().apply { + this["callId"] = UUID.randomUUID() + }, + ), + ) + return Ressurs.success("OK") + } + + @PutMapping( + path = ["/behandling/{behandlingId}/endre/saksbehandler/{nyAnsvarligSaksbehandler}"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + minimumBehandlerrolle = Behandlerrolle.SAKSBEHANDLER, + handling = "endre ansvarlig saksbehandler", + AuditLoggerEvent.UPDATE, + henteParam = HenteParam.BEHANDLING_ID, + ) + fun endreAnsvarligSaksbehandler( + @PathVariable behandlingId: UUID, + @PathVariable nyAnsvarligSaksbehandler: String, + ): Ressurs { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update(behandling.copy(ansvarligSaksbehandler = nyAnsvarligSaksbehandler)) + return Ressurs.success("OK") + } + + @PostMapping(path = ["/publiser/fagsystemsbehandling"]) + fun publishFagsystemsbehandlingsdata( + @Valid @RequestBody + opprettManueltTilbakekrevingRequest: OpprettManueltTilbakekrevingRequest, + @RequestParam(required = false, name = "erInstitusjon") erInstitusjon: Boolean = false, + ): Ressurs { + val eksternFagsakId = opprettManueltTilbakekrevingRequest.eksternFagsakId + val ytelsestype = opprettManueltTilbakekrevingRequest.ytelsestype + val eksternId = opprettManueltTilbakekrevingRequest.eksternId + val institusjon = if (erInstitusjon) Institusjon(organisasjonsnummer = "987654321") else null + val fagsystemsbehandling = HentFagsystemsbehandling( + eksternFagsakId = eksternFagsakId, + ytelsestype = ytelsestype, + eksternId = eksternId, + personIdent = "12345678901", + språkkode = Språkkode.NB, + enhetId = "8020", + enhetsnavn = "testverdi", + revurderingsvedtaksdato = LocalDate.now(), + faktainfo = Faktainfo( + revurderingsårsak = "testverdi", + revurderingsresultat = "OPPHØR", + tilbakekrevingsvalg = Tilbakekrevingsvalg + .IGNORER_TILBAKEKREVING, + ), + institusjon = institusjon, + ) + val requestSendt = requestSendtRepository.findByEksternFagsakIdAndYtelsestypeAndEksternId( + eksternFagsakId, + ytelsestype, + eksternId, + ) + val melding = + objectMapper.writeValueAsString(HentFagsystemsbehandlingRespons(hentFagsystemsbehandling = fagsystemsbehandling)) + if (environment.activeProfiles.any { it.contains("e2e") }) { + requestSendtRepository.update(requestSendt!!.copy(respons = melding)) + } else { + val producerRecord = ProducerRecord( + KafkaConfig.HENT_FAGSYSTEMSBEHANDLING_RESPONS_TOPIC, + requestSendt?.id.toString(), + melding, + ) + kafkaTemplate.send(producerRecord) + } + return Ressurs.success("OK") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/forvaltning/ForvaltningController.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/forvaltning/ForvaltningController.kt new file mode 100644 index 000000000..471b109ca --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/api/forvaltning/ForvaltningController.kt @@ -0,0 +1,154 @@ +package no.nav.familie.tilbake.api.forvaltning + +import io.swagger.v3.oas.annotations.Operation +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.forvaltning.ForvaltningService +import no.nav.familie.tilbake.sikkerhet.AuditLoggerEvent +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.HenteParam +import no.nav.familie.tilbake.sikkerhet.Rolletilgangssjekk +import no.nav.security.token.support.core.api.ProtectedWithClaims +import org.springframework.http.MediaType +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.math.BigInteger +import java.time.LocalDateTime +import java.util.UUID + +// Denne kontrollen inneholder tjenester som kun brukes av forvaltningsteam via swagger. Frontend bør ikke kalle disse tjenestene. + +@RestController +@RequestMapping("/api/forvaltning") +@ProtectedWithClaims(issuer = "azuread") +@Validated +class ForvaltningController(private val forvaltningService: ForvaltningService) { + + @Operation(summary = "Hent korrigert kravgrunnlag") + @PutMapping( + path = ["/behandling/{behandlingId}/kravgrunnlag/{eksternKravgrunnlagId}/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.FORVALTER, + "Henter korrigert kravgrunnlag fra økonomi og oppdaterer kravgrunnlag431", + AuditLoggerEvent.NONE, + HenteParam.BEHANDLING_ID, + ) + fun korrigerKravgrunnlag( + @PathVariable behandlingId: UUID, + @PathVariable eksternKravgrunnlagId: BigInteger, + ): Ressurs { + forvaltningService.korrigerKravgrunnlag(behandlingId, eksternKravgrunnlagId) + return Ressurs.success("OK") + } + + @Operation(summary = "Hent korrigert kravgrunnlag") + @PutMapping( + path = ["/behandling/{behandlingId}/kravgrunnlag/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.FORVALTER, + "Henter korrigert kravgrunnlag fra økonomi og oppdaterer kravgrunnlag431", + AuditLoggerEvent.NONE, + HenteParam.BEHANDLING_ID, + ) + fun korrigerKravgrunnlag( + @PathVariable behandlingId: UUID, + ): Ressurs { + forvaltningService.korrigerKravgrunnlag(behandlingId) + return Ressurs.success("OK") + } + + @Operation(summary = "Arkiver mottatt kravgrunnlag") + @PutMapping( + path = ["/arkiver/kravgrunnlag/{mottattXmlId}/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.FORVALTER, + "Arkiverer mottatt kravgrunnlag", + AuditLoggerEvent.NONE, + HenteParam.MOTTATT_XML_ID, + ) + fun arkiverMottattKravgrunnlag(@PathVariable mottattXmlId: UUID): Ressurs { + forvaltningService.arkiverMottattKravgrunnlag(mottattXmlId) + return Ressurs.success("OK") + } + + @Operation(summary = "Tvinghenlegg behandling") + @PutMapping( + path = ["/behandling/{behandlingId}/tving-henleggelse/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk(Behandlerrolle.FORVALTER, "Tving henlegger behandling", AuditLoggerEvent.NONE, HenteParam.BEHANDLING_ID) + fun tvingHenleggBehandling(@PathVariable behandlingId: UUID): Ressurs { + forvaltningService.tvingHenleggBehandling(behandlingId) + return Ressurs.success("OK") + } + + @Operation(summary = "Flytt behandling tilbake til fakta") + @PutMapping( + path = ["/behandling/{behandlingId}/flytt-behandling/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.FORVALTER, + "Flytter behandling tilbake til Fakta", + AuditLoggerEvent.UPDATE, + HenteParam.BEHANDLING_ID, + ) + fun flyttBehandlingTilFakta(@PathVariable behandlingId: UUID): Ressurs { + forvaltningService.flyttBehandlingsstegTilbakeTilFakta(behandlingId) + return Ressurs.success("OK") + } + + @Operation(summary = "Annuler kravgrunnlag") + @PutMapping( + path = ["/annuler/kravgrunnlag/{eksternKravgrunnlagId}/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.FORVALTER, + "Annulerer kravgrunnlag", + AuditLoggerEvent.NONE, + HenteParam.EKSTERN_KRAVGRUNNLAG_ID, + ) + fun annulerKravgrunnlag(@PathVariable eksternKravgrunnlagId: BigInteger): Ressurs { + forvaltningService.annulerKravgrunnlag(eksternKravgrunnlagId) + return Ressurs.success("OK") + } + + @Operation(summary = "Hent informasjon som kreves for forvaltning") + @GetMapping( + path = ["/ytelsestype/{ytelsestype}/fagsak/{eksternFagsakId}/v1"], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + @Rolletilgangssjekk( + Behandlerrolle.FORVALTER, + "Henter forvaltningsinformasjon", + AuditLoggerEvent.NONE, + HenteParam.YTELSESTYPE_OG_EKSTERN_FAGSAK_ID, + ) + fun hentForvaltningsinfo( + @PathVariable ytelsestype: Ytelsestype, + @PathVariable eksternFagsakId: String, + ): Ressurs> { + return Ressurs.success(forvaltningService.hentForvaltningsinfo(ytelsestype, eksternFagsakId)) + } +} + +data class Forvaltningsinfo( + val eksternKravgrunnlagId: BigInteger, + val kravgrunnlagId: UUID?, + val kravgrunnlagKravstatuskode: String?, + val mottattXmlId: UUID?, + val eksternId: String, + val opprettetTid: LocalDateTime, + val behandlingId: UUID?, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/AvstemmingService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/AvstemmingService.kt new file mode 100644 index 000000000..8e65b368f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/AvstemmingService.kt @@ -0,0 +1,129 @@ +package no.nav.familie.tilbake.avstemming + +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.tilbake.avstemming.marshaller.ØkonomiKvitteringTolk +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Institusjon +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.IntegrasjonerConfig +import no.nav.familie.tilbake.iverksettvedtak.TilbakekrevingsvedtakMarshaller +import no.nav.familie.tilbake.iverksettvedtak.domain.ØkonomiXmlSendt +import no.nav.familie.tilbake.iverksettvedtak.ØkonomiXmlSendtRepository +import no.nav.okonomi.tilbakekrevingservice.TilbakekrevingsvedtakRequest +import no.nav.tilbakekreving.typer.v1.MmelDto +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class AvstemmingService( + private val behandlingRepository: BehandlingRepository, + private val sendtXmlRepository: ØkonomiXmlSendtRepository, + private val fagsakRepository: FagsakRepository, + private val integrasjonerConfig: IntegrasjonerConfig, +) { + + private val logger = LoggerFactory.getLogger(this::class.java) + + fun oppsummer(dato: LocalDate): ByteArray? { + val sendteVedtak = sendtXmlRepository.findByOpprettetPåDato(dato) + var antallFeilet = 0 + var antallFørstegangsvedtakUtenTilbakekreving = 0 + val rader = sendteVedtak.mapNotNull { sendtVedtak -> + if (!erSendtOK(sendtVedtak)) { + antallFeilet++ + return@mapNotNull null + } + val behandling = behandlingRepository.findByIdOrThrow(sendtVedtak.behandlingId) + val oppsummering: TilbakekrevingsvedtakOppsummering = oppsummer(sendtVedtak) + if (erFørstegangsvedtakUtenTilbakekreving(behandling, oppsummering)) { + antallFørstegangsvedtakUtenTilbakekreving++ + return@mapNotNull null + } + lagAvstemmingsradForVedtaket(behandling, oppsummering) + } + if (antallFeilet == 0) { + logger.info( + "Avstemmer {}. Sender {} vedtak til avstemming. Totalt ble {} vedtak sendt til OS dette døgnet. " + + "{} førstegangsvedtak uten tilbakekreving sendes ikke til avstemming", + dato, + rader.size, + sendteVedtak.size, + antallFørstegangsvedtakUtenTilbakekreving, + ) + } else { + logger.warn( + "Avstemmer {}. Sender {} vedtak til avstemming. Totalt ble {} vedtak sendt til OS dette døgnet. " + + "{} førstegangsvedtak uten tilbakekreving sendes ikke til avstemming. " + + "{} vedtak fikk negativ kvittering fra OS og sendes ikke til avstemming", + dato, + rader.size, + sendteVedtak.size, + antallFørstegangsvedtakUtenTilbakekreving, + antallFeilet, + ) + } + return if (rader.isEmpty()) { + null + } else { + FilMapper(rader).tilFlatfil() + } + } + + private fun erFørstegangsvedtakUtenTilbakekreving( + behandling: Behandling, + oppsummering: TilbakekrevingsvedtakOppsummering, + ): Boolean { + return behandling.type == Behandlingstype.TILBAKEKREVING && oppsummering.harIngenTilbakekreving() + } + + private fun lagAvstemmingsradForVedtaket( + behandling: Behandling, + oppsummering: TilbakekrevingsvedtakOppsummering, + ): Rad { + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val vedtaksdato = behandling.sisteResultat?.behandlingsvedtak?.vedtaksdato ?: error("Vedtaksdato mangler") + return Rad( + avsender = integrasjonerConfig.applicationName, + vedtakId = oppsummering.økonomivedtakId, + fnr = if (fagsak.institusjon != null) padOrganisasjonsnummer(fagsak.institusjon) else fagsak.bruker.ident, + vedtaksdato = vedtaksdato, + fagsakYtelseType = fagsak.ytelsestype, + tilbakekrevesBruttoUtenRenter = oppsummering.tilbakekrevesBruttoUtenRenter, + tilbakekrevesNettoUtenRenter = oppsummering.tilbakekrevesNettoUtenRenter, + skatt = oppsummering.skatt, + renter = oppsummering.renter, + erOmgjøringTilIngenTilbakekreving = erOmgjøringTilIngenTilbakekreving(oppsummering, behandling), + ) + } + + private fun padOrganisasjonsnummer(institusjon: Institusjon): String { + return "00" + institusjon.organisasjonsnummer + } + + private fun erOmgjøringTilIngenTilbakekreving( + oppsummering: TilbakekrevingsvedtakOppsummering, + behandling: Behandling, + ): Boolean { + return behandling.type == Behandlingstype.REVURDERING_TILBAKEKREVING && oppsummering.harIngenTilbakekreving() + } + + private fun oppsummer(sendtMelding: ØkonomiXmlSendt): TilbakekrevingsvedtakOppsummering { + val xml: String = sendtMelding.melding + val melding: TilbakekrevingsvedtakRequest = + TilbakekrevingsvedtakMarshaller.unmarshall(xml, sendtMelding.behandlingId, sendtMelding.id) + return TilbakekrevingsvedtakOppsummering.oppsummer(melding.tilbakekrevingsvedtak) + } + + companion object { + + private fun erSendtOK(melding: ØkonomiXmlSendt): Boolean { + val kvittering: MmelDto = melding.kvittering?.let { objectMapper.readValue(it) } ?: return false + return ØkonomiKvitteringTolk.erKvitteringOK(kvittering) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/FilMapper.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/FilMapper.kt new file mode 100644 index 000000000..51f0eb394 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/FilMapper.kt @@ -0,0 +1,23 @@ +package no.nav.familie.tilbake.avstemming + +class FilMapper(private val rader: List) { + + fun tilFlatfil(): ByteArray { + return (HEADER + rader.joinToString(SKILLETEGN_RADER) { it.toCsvString() }).toByteArray() + } + + companion object { + + private const val SKILLETEGN_RADER = "\n" + const val HEADER = "avsender" + Rad.SKILLETEGN_KOLONNER + + "vedtakId" + Rad.SKILLETEGN_KOLONNER + + "fnr" + Rad.SKILLETEGN_KOLONNER + + "vedtaksdato" + Rad.SKILLETEGN_KOLONNER + + "fagsakYtelseType" + Rad.SKILLETEGN_KOLONNER + + "tilbakekrevesBruttoUtenRenter" + Rad.SKILLETEGN_KOLONNER + + "skatt" + Rad.SKILLETEGN_KOLONNER + + "tilbakekrevesNettoUtenRenter" + Rad.SKILLETEGN_KOLONNER + + "renter" + Rad.SKILLETEGN_KOLONNER + + "erOmgjøringTilIngenTilbakekreving" + SKILLETEGN_RADER + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/Rad.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/Rad.kt new file mode 100644 index 000000000..c6a92f672 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/Rad.kt @@ -0,0 +1,62 @@ +package no.nav.familie.tilbake.avstemming + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import java.math.BigDecimal +import java.math.RoundingMode +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +class Rad( + val avsender: String, + val vedtakId: String, + val fnr: String, + val vedtaksdato: LocalDate, + val fagsakYtelseType: Ytelsestype, + val tilbakekrevesBruttoUtenRenter: BigDecimal, + val skatt: BigDecimal, + val tilbakekrevesNettoUtenRenter: BigDecimal, + val renter: BigDecimal, + val erOmgjøringTilIngenTilbakekreving: Boolean = false, +) { + + fun toCsvString(): String { + return ( + format(avsender) + + SKILLETEGN_KOLONNER + format(vedtakId) + + SKILLETEGN_KOLONNER + format(fnr) + + SKILLETEGN_KOLONNER + format(vedtaksdato) + + SKILLETEGN_KOLONNER + format(fagsakYtelseType) + + SKILLETEGN_KOLONNER + format(tilbakekrevesBruttoUtenRenter) + + SKILLETEGN_KOLONNER + format(skatt) + + SKILLETEGN_KOLONNER + format(tilbakekrevesNettoUtenRenter) + + SKILLETEGN_KOLONNER + format(renter) + + SKILLETEGN_KOLONNER + formatOmgjøring(erOmgjøringTilIngenTilbakekreving) + ) + } + + private fun format(verdi: String): String { + return verdi + } + + private fun format(dato: LocalDate): String { + return dato.format(DATOFORMAT) + } + + private fun format(kode: Ytelsestype): String { + return format(kode.kode) + } + + private fun format(verdi: BigDecimal): String { + return verdi.setScale(0, RoundingMode.UNNECESSARY).toPlainString() + } + + private fun formatOmgjøring(verdi: Boolean): String { + return if (verdi) "Omgjoring0" else "" + } + + companion object { + + const val SKILLETEGN_KOLONNER = ";" + private val DATOFORMAT = DateTimeFormatter.ofPattern("yyyyMMdd") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/TilbakekrevingsvedtakOppsummering.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/TilbakekrevingsvedtakOppsummering.kt new file mode 100644 index 000000000..0fd4e91cc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/TilbakekrevingsvedtakOppsummering.kt @@ -0,0 +1,40 @@ +package no.nav.familie.tilbake.avstemming + +import no.nav.tilbakekreving.tilbakekrevingsvedtak.vedtak.v1.TilbakekrevingsvedtakDto +import java.math.BigDecimal + +class TilbakekrevingsvedtakOppsummering( + val økonomivedtakId: String, + val tilbakekrevesBruttoUtenRenter: BigDecimal, + val tilbakekrevesNettoUtenRenter: BigDecimal, + val renter: BigDecimal, + val skatt: BigDecimal, +) { + + fun harIngenTilbakekreving(): Boolean { + return tilbakekrevesBruttoUtenRenter.signum() == 0 + } + + companion object { + + fun oppsummer(tilbakekrevingsvedtak: TilbakekrevingsvedtakDto): TilbakekrevingsvedtakOppsummering { + var bruttoUtenRenter = BigDecimal.ZERO + var renter = BigDecimal.ZERO + var skatt = BigDecimal.ZERO + for (periode in tilbakekrevingsvedtak.tilbakekrevingsperiode) { + renter = renter.add(periode.belopRenter) + for (beløp in periode.tilbakekrevingsbelop) { + bruttoUtenRenter = bruttoUtenRenter.add(beløp.belopTilbakekreves) + skatt = skatt.add(beløp.belopSkatt) + } + } + return TilbakekrevingsvedtakOppsummering( + renter = renter, + skatt = skatt, + tilbakekrevesBruttoUtenRenter = bruttoUtenRenter, + tilbakekrevesNettoUtenRenter = bruttoUtenRenter.subtract(skatt), + økonomivedtakId = tilbakekrevingsvedtak.vedtakId.toString(), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/domain/Avstemmingsfil.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/domain/Avstemmingsfil.kt new file mode 100644 index 000000000..af0cf167e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/domain/Avstemmingsfil.kt @@ -0,0 +1,19 @@ +package no.nav.familie.tilbake.avstemming.domain + +import no.nav.familie.kontrakter.felles.Fil +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import java.util.UUID + +data class Avstemmingsfil( + @Id + val id: UUID = UUID.randomUUID(), + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val fil: Fil, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/domain/AvstemmingsfilRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/domain/AvstemmingsfilRepository.kt new file mode 100644 index 000000000..c6256d57d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/domain/AvstemmingsfilRepository.kt @@ -0,0 +1,7 @@ +package no.nav.familie.tilbake.avstemming.domain + +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import java.util.UUID + +interface AvstemmingsfilRepository : RepositoryInterface, InsertUpdateRepository diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/marshaller/\303\230konomiKvitteringTolk.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/marshaller/\303\230konomiKvitteringTolk.kt" new file mode 100644 index 000000000..a8b094100 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/marshaller/\303\230konomiKvitteringTolk.kt" @@ -0,0 +1,12 @@ +package no.nav.familie.tilbake.avstemming.marshaller + +import no.nav.tilbakekreving.typer.v1.MmelDto + +object ØkonomiKvitteringTolk { + + private val KVITTERING_OK_KODER = setOf("00", "04") + + fun erKvitteringOK(kvittering: MmelDto): Boolean { + return KVITTERING_OK_KODER.contains(kvittering.alvorlighetsgrad) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/task/AvstemmingTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/task/AvstemmingTask.kt new file mode 100644 index 000000000..65f20351b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/avstemming/task/AvstemmingTask.kt @@ -0,0 +1,77 @@ +package no.nav.familie.tilbake.avstemming.task + +import no.nav.familie.kontrakter.felles.Fil +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.avstemming.AvstemmingService +import no.nav.familie.tilbake.avstemming.domain.Avstemmingsfil +import no.nav.familie.tilbake.avstemming.domain.AvstemmingsfilRepository +import no.nav.familie.tilbake.common.fagsystem +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import org.slf4j.LoggerFactory +import org.springframework.core.env.Environment +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Properties +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = AvstemmingTask.TYPE, + beskrivelse = "Avstemming av krav.", +) +class AvstemmingTask( + private val taskService: TaskService, + private val avstemmingService: AvstemmingService, + private val avstemmingsfilRepository: AvstemmingsfilRepository, + private val integrasjonerClient: IntegrasjonerClient, + private val environment: Environment, +) : AsyncTaskStep { + + private val applikasjon = "familie-tilbake" + private val logger = LoggerFactory.getLogger(AvstemmingTask::class.java) + + private val miljø = if (environment.activeProfiles.contains("prod")) "p" else "q" + + override fun doTask(task: Task) { + val dato = LocalDate.parse(task.payload) + val batchRun = TYPE + "-" + UUID.randomUUID() + logger.info("Kjører avstemming for {} i batch {}", dato, batchRun) + val resultat = avstemmingService.oppsummer(dato) + if (resultat != null) { + val forDato = dato.format(DATO_FORMATTER) + val kjøreTidspunkt = LocalDateTime.now().format(DATO_TIDSPUNKT_FORMATTER) + val filnavn = String.format(FILNAVN_MAL, applikasjon, miljø, forDato, kjøreTidspunkt) + val fil = Fil(filnavn, resultat) + avstemmingsfilRepository.insert(Avstemmingsfil(fil = fil)) + integrasjonerClient.sendFil(fil) + logger.info("Filen {} er overført til avstemming sftp", filnavn) + } + } + + override fun onCompletion(task: Task) { + if (environment.activeProfiles.contains("e2e")) return + + val dato = LocalDate.parse(task.payload) + val nesteAvstemming = Task( + type = TYPE, + payload = dato.plusDays(1).toString(), + properties = Properties().apply { setProperty(PropertyName.FAGSYSTEM, task.fagsystem()) }, + ) + .medTriggerTid(dato.plusDays(2).atTime(8, 0)) + taskService.save(nesteAvstemming) + } + + companion object { + + const val TYPE = "task.avstemming" + private val DATO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd") + private val DATO_TIDSPUNKT_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmm") + private const val FILNAVN_MAL = "%s-%s-%s-%s.csv" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingManuellOpprettelseService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingManuellOpprettelseService.kt new file mode 100644 index 000000000..e70be9a48 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingManuellOpprettelseService.kt @@ -0,0 +1,57 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingstype +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandling +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class BehandlingManuellOpprettelseService(private val behandlingService: BehandlingService) { + + @Transactional + fun opprettBehandlingManuell( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + eksternId: String, + ansvarligSaksbehandler: String, + fagsystemsbehandlingData: HentFagsystemsbehandling, + ) { + val opprettTilbakekrevingRequest = lagOpprettBehandlingsrequest( + eksternFagsakId = eksternFagsakId, + ytelsestype = ytelsestype, + eksternId = eksternId, + fagsystemsbehandlingData = fagsystemsbehandlingData, + ansvarligSaksbehandler = ansvarligSaksbehandler, + ) + behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + } + + private fun lagOpprettBehandlingsrequest( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + eksternId: String, + fagsystemsbehandlingData: HentFagsystemsbehandling, + ansvarligSaksbehandler: String, + ): OpprettTilbakekrevingRequest { + return OpprettTilbakekrevingRequest( + fagsystem = FagsystemUtil.hentFagsystemFraYtelsestype(ytelsestype), + ytelsestype = ytelsestype, + eksternFagsakId = eksternFagsakId, + eksternId = eksternId, + behandlingstype = Behandlingstype.TILBAKEKREVING, + manueltOpprettet = true, + saksbehandlerIdent = ansvarligSaksbehandler, + personIdent = fagsystemsbehandlingData.personIdent, + språkkode = fagsystemsbehandlingData.språkkode, + enhetId = fagsystemsbehandlingData.enhetId, + enhetsnavn = fagsystemsbehandlingData.enhetsnavn, + revurderingsvedtaksdato = fagsystemsbehandlingData.revurderingsvedtaksdato, + faktainfo = fagsystemsbehandlingData.faktainfo, + verge = fagsystemsbehandlingData.verge, + varsel = null, + institusjon = fagsystemsbehandlingData.institusjon, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingMapper.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingMapper.kt new file mode 100644 index 000000000..9b8339c86 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingMapper.kt @@ -0,0 +1,300 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.klage.FagsystemType +import no.nav.familie.kontrakter.felles.saksbehandler.Saksbehandler +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsresultatstype.DELVIS_TILBAKEBETALING +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsresultatstype.FULL_TILBAKEBETALING +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsresultatstype.HENLAGT +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsresultatstype.INGEN_TILBAKEBETALING +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingstype.REVURDERING_TILBAKEKREVING +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingstype.TILBAKEKREVING +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsårsakstype.REVURDERING_FEILUTBETALT_BELØP_HELT_ELLER_DELVIS_BORTFALT +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsårsakstype.REVURDERING_KLAGE_KA +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsårsakstype.REVURDERING_KLAGE_NFP +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsårsakstype.REVURDERING_OPPLYSNINGER_OM_FORELDELSE +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsårsakstype.REVURDERING_OPPLYSNINGER_OM_VILKÅR +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettTilbakekrevingRequest +import no.nav.familie.tilbake.api.dto.BehandlingDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegsinfoDto +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultat +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsak +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsakstype +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandling.domain.Fagsystemsbehandling +import no.nav.familie.tilbake.behandling.domain.Fagsystemskonsekvens +import no.nav.familie.tilbake.behandling.domain.Varsel +import no.nav.familie.tilbake.behandling.domain.Varselsperiode +import no.nav.familie.tilbake.behandling.domain.Verge +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.ManuellBrevmottakerMapper +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.domene.ManuellBrevmottaker + +object BehandlingMapper { + + fun tilDomeneBehandling( + opprettTilbakekrevingRequest: OpprettTilbakekrevingRequest, + fagsystem: Fagsystem, + fagsak: Fagsak, + ansvarligSaksbehandler: Saksbehandler, + ): Behandling { + val faktainfo = opprettTilbakekrevingRequest.faktainfo + val fagsystemskonsekvenser = faktainfo.konsekvensForYtelser.map { Fagsystemskonsekvens(konsekvens = it) }.toSet() + val fagsystemsbehandling = + Fagsystemsbehandling( + eksternId = opprettTilbakekrevingRequest.eksternId, + tilbakekrevingsvalg = faktainfo.tilbakekrevingsvalg, + revurderingsvedtaksdato = opprettTilbakekrevingRequest.revurderingsvedtaksdato, + resultat = faktainfo.revurderingsresultat, + årsak = faktainfo.revurderingsårsak, + konsekvenser = fagsystemskonsekvenser, + ) + val varsler = tilDomeneVarsel(opprettTilbakekrevingRequest) + val verger = tilDomeneVerge(fagsystem, opprettTilbakekrevingRequest) + + return Behandling( + fagsakId = fagsak.id, + type = Behandlingstype.TILBAKEKREVING, + ansvarligSaksbehandler = ansvarligSaksbehandler.navIdent, + behandlendeEnhet = opprettTilbakekrevingRequest.enhetId, + behandlendeEnhetsNavn = opprettTilbakekrevingRequest.enhetsnavn, + manueltOpprettet = opprettTilbakekrevingRequest.manueltOpprettet, + fagsystemsbehandling = setOf(fagsystemsbehandling), + varsler = varsler, + verger = verger, + regelverk = opprettTilbakekrevingRequest.regelverk, + ) + } + + fun tilRespons( + behandling: Behandling, + erBehandlingPåVent: Boolean, + kanHenleggeBehandling: Boolean, + kanEndres: Boolean, + kanRevurderingOpprettes: Boolean, + behandlingsstegsinfoer: List, + varselSendt: Boolean, + eksternFagsakId: String, + manuelleBrevmottakere: List, + støtterManuelleBrevmottakere: Boolean, + ): BehandlingDto { + val resultat: Behandlingsresultat? = behandling.resultater.maxByOrNull { + it.sporbar.endret.endretTid + } + + return BehandlingDto( + eksternBrukId = behandling.eksternBrukId, + behandlingId = behandling.id, + type = behandling.type, + status = behandling.status, + erBehandlingHenlagt = resultat?.erBehandlingHenlagt() ?: false, + resultatstype = resultat?.resultatstypeTilFrontend(), + enhetskode = behandling.behandlendeEnhet, + enhetsnavn = behandling.behandlendeEnhetsNavn, + ansvarligSaksbehandler = behandling.ansvarligSaksbehandler, + ansvarligBeslutter = behandling.ansvarligBeslutter, + opprettetDato = behandling.opprettetDato, + avsluttetDato = behandling.avsluttetDato, + vedtaksdato = behandling.sisteResultat?.behandlingsvedtak?.vedtaksdato, + endretTidspunkt = behandling.endretTidspunkt, + harVerge = behandling.harVerge, + kanHenleggeBehandling = kanHenleggeBehandling, + kanRevurderingOpprettes = kanRevurderingOpprettes, + erBehandlingPåVent = erBehandlingPåVent, + kanEndres = kanEndres, + varselSendt = varselSendt, + behandlingsstegsinfo = tilBehandlingstegsinfoDto(behandlingsstegsinfoer), + fagsystemsbehandlingId = behandling.aktivFagsystemsbehandling.eksternId, + eksternFagsakId = eksternFagsakId, + behandlingsårsakstype = behandling.sisteÅrsak?.type, + harManuelleBrevmottakere = manuelleBrevmottakere.isNotEmpty(), + støtterManuelleBrevmottakere = støtterManuelleBrevmottakere, + manuelleBrevmottakere = manuelleBrevmottakere.map { ManuellBrevmottakerMapper.tilRespons(it) }, + ) + } + + private fun tilBehandlingstegsinfoDto(behandlingsstegsinfoListe: List): List { + return behandlingsstegsinfoListe.map { + BehandlingsstegsinfoDto( + behandlingssteg = it.behandlingssteg, + behandlingsstegstatus = it.behandlingsstegstatus, + venteårsak = it.venteårsak, + tidsfrist = it.tidsfrist, + ) + } + } + + private fun tilDomeneVarsel(opprettTilbakekrevingRequest: OpprettTilbakekrevingRequest): Set { + return opprettTilbakekrevingRequest.varsel?.let { + val varselsperioder = + it.perioder.map { periode -> + Varselsperiode(fom = periode.fom, tom = periode.tom) + }.toSet() + return setOf( + Varsel( + varseltekst = it.varseltekst, + varselbeløp = it.sumFeilutbetaling.longValueExact(), + perioder = varselsperioder, + ), + ) + } ?: emptySet() + } + + private fun tilDomeneVerge(fagsystem: Fagsystem, opprettTilbakekrevingRequest: OpprettTilbakekrevingRequest): Set { + opprettTilbakekrevingRequest.verge?.let { + return setOf( + Verge( + type = it.vergetype, + kilde = fagsystem.name, + navn = it.navn, + orgNr = it.organisasjonsnummer, + ident = it.personIdent, + ), + ) + } + return emptySet() + } + + fun tilBehandlingerForFagsystem(behandling: Behandling): no.nav.familie.kontrakter.felles.tilbakekreving.Behandling { + val resultat: Behandlingsresultat? = behandling.resultater.maxByOrNull { + it.sporbar.endret.endretTid + } + return no.nav.familie.kontrakter.felles.tilbakekreving.Behandling( + behandlingId = behandling.eksternBrukId, + opprettetTidspunkt = behandling.opprettetTidspunkt, + aktiv = !behandling.erAvsluttet, + type = mapType(behandling), + status = mapStatus(behandling), + årsak = mapÅrsak(behandling), + vedtaksdato = behandling.avsluttetDato?.atStartOfDay(), + resultat = mapResultat(resultat), + ) + } + + fun tilVedtakForFagsystem(behandlinger: List): List { + return behandlinger + .filter { it.erAvsluttet } + .filter { it.sisteResultat?.erBehandlingFastsatt() ?: false } + .map { + val avsluttetDato = it.avsluttetDato ?: error("Mangler avsluttet dato på behandling=${it.id}") + val sisteResultat = it.sisteResultat ?: error("Mangler resultat på behandling=${it.id}") + + no.nav.familie.kontrakter.felles.klage.FagsystemVedtak( + eksternBehandlingId = it.eksternBrukId.toString(), + behandlingstype = mapType(it).visningsnavn, + resultat = sisteResultat.type.navn, + vedtakstidspunkt = avsluttetDato.atStartOfDay(), + fagsystemType = FagsystemType.TILBAKEKREVING, + regelverk = it.regelverk, + ) + } + } + + private fun mapType(behandling: Behandling): no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingstype { + return when (behandling.type) { + Behandlingstype.TILBAKEKREVING -> TILBAKEKREVING + Behandlingstype.REVURDERING_TILBAKEKREVING -> REVURDERING_TILBAKEKREVING + } + } + + private fun mapStatus(behandling: Behandling): no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsstatus { + return when (behandling.status) { + Behandlingsstatus.AVSLUTTET -> no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsstatus.AVSLUTTET + Behandlingsstatus.UTREDES -> no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsstatus.UTREDES + Behandlingsstatus.FATTER_VEDTAK -> no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsstatus.FATTER_VEDTAK + Behandlingsstatus.IVERKSETTER_VEDTAK -> + no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsstatus.IVERKSETTER_VEDTAK + Behandlingsstatus.OPPRETTET -> no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsstatus.OPPRETTET + } + } + + private fun mapÅrsak(behandling: Behandling): no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsårsakstype? { + if (behandling.årsaker.isEmpty()) return null + return when (behandling.årsaker.firstOrNull()?.type) { + Behandlingsårsakstype.REVURDERING_KLAGE_KA -> REVURDERING_KLAGE_KA + Behandlingsårsakstype.REVURDERING_KLAGE_NFP -> REVURDERING_KLAGE_NFP + Behandlingsårsakstype.REVURDERING_OPPLYSNINGER_OM_VILKÅR -> REVURDERING_OPPLYSNINGER_OM_VILKÅR + Behandlingsårsakstype.REVURDERING_OPPLYSNINGER_OM_FORELDELSE -> REVURDERING_OPPLYSNINGER_OM_FORELDELSE + Behandlingsårsakstype.REVURDERING_FEILUTBETALT_BELØP_HELT_ELLER_DELVIS_BORTFALT -> + REVURDERING_FEILUTBETALT_BELØP_HELT_ELLER_DELVIS_BORTFALT + else -> null + } + } + + private fun mapResultat(resultat: Behandlingsresultat?): no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingsresultatstype? { + return when (resultat?.type) { + Behandlingsresultatstype.DELVIS_TILBAKEBETALING -> DELVIS_TILBAKEBETALING + Behandlingsresultatstype.FULL_TILBAKEBETALING -> FULL_TILBAKEBETALING + Behandlingsresultatstype.INGEN_TILBAKEBETALING -> INGEN_TILBAKEBETALING + Behandlingsresultatstype.HENLAGT, + Behandlingsresultatstype.HENLAGT_FEILOPPRETTET, + Behandlingsresultatstype.HENLAGT_FEILOPPRETTET_MED_BREV, + Behandlingsresultatstype.HENLAGT_FEILOPPRETTET_UTEN_BREV, + Behandlingsresultatstype.HENLAGT_KRAVGRUNNLAG_NULLSTILT, + Behandlingsresultatstype.HENLAGT_TEKNISK_VEDLIKEHOLD, + -> HENLAGT + Behandlingsresultatstype.IKKE_FASTSATT, + null, + -> null + } + } + + fun tilDomeneBehandlingRevurdering( + originalBehandling: Behandling, + behandlingsårsakstype: Behandlingsårsakstype, + ): Behandling { + val verger: Set = kopiVerge(originalBehandling)?.let { setOf(it) } ?: emptySet() + return Behandling( + fagsakId = originalBehandling.fagsakId, + type = Behandlingstype.REVURDERING_TILBAKEKREVING, + ansvarligSaksbehandler = ContextService.hentSaksbehandler(), + behandlendeEnhet = originalBehandling.behandlendeEnhet, + behandlendeEnhetsNavn = originalBehandling.behandlendeEnhetsNavn, + manueltOpprettet = false, + årsaker = setOf( + Behandlingsårsak( + type = behandlingsårsakstype, + originalBehandlingId = originalBehandling.id, + ), + ), + fagsystemsbehandling = setOf(kopiFagsystemsbehandling(originalBehandling)), + verger = verger, + regelverk = originalBehandling.regelverk, + ) + } + + private fun kopiFagsystemsbehandling(originalBehandling: Behandling): Fagsystemsbehandling { + val fagsystemsbehandling = originalBehandling.aktivFagsystemsbehandling + return Fagsystemsbehandling( + eksternId = fagsystemsbehandling.eksternId, + årsak = fagsystemsbehandling.årsak, + resultat = fagsystemsbehandling.resultat, + tilbakekrevingsvalg = fagsystemsbehandling.tilbakekrevingsvalg, + revurderingsvedtaksdato = fagsystemsbehandling.revurderingsvedtaksdato, + konsekvenser = kopiFagsystemskonsekvenser(fagsystemsbehandling.konsekvenser), + ) + } + + private fun kopiFagsystemskonsekvenser(originalKonsekvenser: Set): Set { + return originalKonsekvenser.map { Fagsystemskonsekvens(konsekvens = it.konsekvens) }.toSet() + } + + private fun kopiVerge(originalBehandling: Behandling): Verge? { + val verge = originalBehandling.aktivVerge + return verge?.let { + Verge( + type = it.type, + ident = it.ident, + orgNr = it.orgNr, + navn = it.navn, + begrunnelse = it.begrunnelse, + kilde = it.kilde, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingRepository.kt new file mode 100644 index 000000000..6a0fe8620 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingRepository.kt @@ -0,0 +1,77 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import org.springframework.data.jdbc.repository.query.Query +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.util.UUID + +@Repository +@Transactional +interface BehandlingRepository : RepositoryInterface, InsertUpdateRepository { + + // language=PostgreSQL + @Query( + """ + SELECT beh.* FROM behandling beh JOIN fagsak f ON beh.fagsak_id = f.id + WHERE f.ytelsestype=:ytelsestype AND f.ekstern_fagsak_id=:eksternFagsakId + AND beh.status <>'AVSLUTTET' AND beh.type='TILBAKEKREVING' + """, + ) + fun finnÅpenTilbakekrevingsbehandling( + ytelsestype: Ytelsestype, + eksternFagsakId: String, + ): Behandling? + + // language=PostgreSQL + @Query( + """ + SELECT beh.* FROM behandling beh WHERE id=(SELECT arsak.behandling_id FROM behandlingsarsak arsak + WHERE arsak.original_behandling_id=:behandlingId ORDER BY arsak.opprettet_tid DESC LIMIT 1) + AND beh.status <>'AVSLUTTET' AND beh.type='REVURDERING_TILBAKEKREVING' + """, + ) + fun finnÅpenTilbakekrevingsrevurdering(behandlingId: UUID): Behandling? + + // language=PostgreSQL + @Query( + """ + SELECT beh.* FROM behandling beh JOIN fagsystemsbehandling fag ON fag.behandling_id= beh.id + WHERE fag.ekstern_id=:eksternId AND fag.aktiv=TRUE + AND beh.type='TILBAKEKREVING' AND beh.status='AVSLUTTET' ORDER BY beh.opprettet_tid DESC + """, + ) + fun finnAvsluttetTilbakekrevingsbehandlinger(eksternId: String): List + + // language=PostgreSQL + + fun findByFagsakId(fagsakId: UUID): List + + // language=PostgreSQL + @Query( + """ + SELECT beh.* FROM behandling beh JOIN behandlingsstegstilstand tilstand ON tilstand.behandling_id = beh.id + WHERE beh.type='TILBAKEKREVING' AND beh.status='UTREDES' AND + tilstand.behandlingssteg='FAKTA' AND tilstand.behandlingsstegsstatus='KLAR' + """, + ) + fun finnAlleBehandlingerKlarForSaksbehandling(): List + + // language=PostgreSQL + @Query( + """ + SELECT beh.* FROM behandling beh + JOIN behandlingsstegstilstand tilstand ON tilstand.behandling_id = beh.id + WHERE beh.type='TILBAKEKREVING' + AND beh.status='UTREDES' + AND tilstand.behandlingsstegsstatus='VENTER' + AND tilstand.behandlingssteg<>'GRUNNLAG' + AND tilstand.tidsfrist <= :dagensdato + """, + ) + fun finnAlleBehandlingerKlarForGjenoppta(dagensdato: LocalDate): List +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingService.kt new file mode 100644 index 000000000..fdaf62cd2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingService.kt @@ -0,0 +1,661 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandling +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettManueltTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.api.dto.BehandlingDto +import no.nav.familie.tilbake.api.dto.BehandlingPåVentDto +import no.nav.familie.tilbake.api.dto.ByttEnhetDto +import no.nav.familie.tilbake.api.dto.HenleggelsesbrevFritekstDto +import no.nav.familie.tilbake.api.dto.OpprettRevurderingDto +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultat +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Behandlingstype.REVURDERING_TILBAKEKREVING +import no.nav.familie.tilbake.behandling.domain.Behandlingstype.TILBAKEKREVING +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandling.domain.Fagsystemsbehandling +import no.nav.familie.tilbake.behandling.domain.Fagsystemskonsekvens +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.behandling.task.OpprettBehandlingManueltTask +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.FeatureToggleConfig +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.datavarehus.saksstatistikk.BehandlingTilstandService +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingService +import no.nav.familie.tilbake.dokumentbestilling.henleggelse.SendHenleggelsesbrevTask +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.ManuellBrevmottakerRepository +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.domene.ManuellBrevmottaker +import no.nav.familie.tilbake.dokumentbestilling.varsel.SendVarselbrevTask +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.task.FinnKravgrunnlagTask +import no.nav.familie.tilbake.kravgrunnlag.task.HentKravgrunnlagTask +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattRepository +import no.nav.familie.tilbake.micrometer.TellerService +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.TilgangService +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.util.Properties +import java.util.UUID + +@Service +class BehandlingService( + private val behandlingRepository: BehandlingRepository, + private val fagsakService: FagsakService, + private val taskService: TaskService, + private val brevsporingService: BrevsporingService, + private val manuellBrevmottakerRepository: ManuellBrevmottakerRepository, + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val økonomiXmlMottattRepository: ØkonomiXmlMottattRepository, + private val behandlingskontrollService: BehandlingskontrollService, + private val behandlingTilstandService: BehandlingTilstandService, + private val tellerService: TellerService, + private val stegService: StegService, + private val oppgaveTaskService: OppgaveTaskService, + private val historikkTaskService: HistorikkTaskService, + private val tilgangService: TilgangService, + @Value("\${OPPRETTELSE_DAGER_BEGRENSNING:6}") + private val opprettelseDagerBegrensning: Long, + private val integrasjonerClient: IntegrasjonerClient, + private val featureToggleService: FeatureToggleService, +) { + + private val logger: Logger = LoggerFactory.getLogger(this.javaClass) + private val secureLogger = LoggerFactory.getLogger("secureLogger") + + @Transactional + fun opprettBehandling(opprettTilbakekrevingRequest: OpprettTilbakekrevingRequest): Behandling { + val behandling: Behandling = opprettFørstegangsbehandling(opprettTilbakekrevingRequest) + + // Lag oppgave for behandling + oppgaveTaskService.opprettOppgaveTask(behandling, Oppgavetype.BehandleSak) + + if (opprettTilbakekrevingRequest.faktainfo.tilbakekrevingsvalg === Tilbakekrevingsvalg + .OPPRETT_TILBAKEKREVING_MED_VARSEL && !behandling.manueltOpprettet + ) { + val sendVarselbrev = Task( + type = SendVarselbrevTask.TYPE, + payload = behandling.id.toString(), + properties = Properties().apply { + setProperty(PropertyName.FAGSYSTEM, opprettTilbakekrevingRequest.fagsystem.name) + }, + ) + taskService.save(sendVarselbrev) + } + + return behandling + } + + @Transactional + fun opprettBehandlingManuellTask(opprettManueltTilbakekrevingRequest: OpprettManueltTilbakekrevingRequest) { + val kanBehandlingOpprettesManuelt = + fagsakService.kanBehandlingOpprettesManuelt( + opprettManueltTilbakekrevingRequest.eksternFagsakId, + opprettManueltTilbakekrevingRequest.ytelsestype, + ) + if (!kanBehandlingOpprettesManuelt.kanBehandlingOpprettes) { + throw Feil(message = kanBehandlingOpprettesManuelt.melding) + } + logger.info("Oppretter OpprettBehandlingManueltTask for request=$opprettManueltTilbakekrevingRequest") + val properties = Properties().apply { + setProperty("eksternFagsakId", opprettManueltTilbakekrevingRequest.eksternFagsakId) + setProperty("ytelsestype", opprettManueltTilbakekrevingRequest.ytelsestype.name) + setProperty("eksternId", opprettManueltTilbakekrevingRequest.eksternId) + setProperty( + PropertyName.FAGSYSTEM, + FagsystemUtil.hentFagsystemFraYtelsestype(opprettManueltTilbakekrevingRequest.ytelsestype).name, + ) + setProperty("ansvarligSaksbehandler", ContextService.hentSaksbehandler()) + } + taskService.save( + Task( + type = OpprettBehandlingManueltTask.TYPE, + properties = properties, + payload = "", + ), + ) + } + + @Transactional + fun opprettRevurdering(opprettRevurderingDto: OpprettRevurderingDto): Behandling { + val originalBehandlingId = opprettRevurderingDto.originalBehandlingId + logger.info("Oppretter revurdering for behandling $originalBehandlingId") + val originalBehandling = behandlingRepository.findByIdOrThrow(originalBehandlingId) + if (!kanRevurderingOpprettes(originalBehandling)) { + val feilmelding = "Revurdering kan ikke opprettes for behandling $originalBehandlingId. " + + "Enten behandlingen er ikke avsluttet med kravgrunnlag eller " + + "det finnes allerede en åpen revurdering" + throw Feil(message = feilmelding, frontendFeilmelding = feilmelding) + } + val revurdering = + BehandlingMapper.tilDomeneBehandlingRevurdering(originalBehandling, opprettRevurderingDto.årsakstype) + behandlingRepository.insert(revurdering) + + val fagsystem = FagsystemUtil.hentFagsystemFraYtelsestype(opprettRevurderingDto.ytelsestype) + historikkTaskService.lagHistorikkTask( + revurdering.id, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_OPPRETTET, + Aktør.SAKSBEHANDLER, + ) + + behandlingskontrollService.fortsettBehandling(revurdering.id) + stegService.håndterSteg(revurdering.id) + + // kjør HentKravgrunnlagTask for å hente kravgrunnlag på nytt fra økonomi + taskService.save( + Task( + type = HentKravgrunnlagTask.TYPE, + payload = revurdering.id.toString(), + properties = Properties().apply { setProperty(PropertyName.FAGSYSTEM, fagsystem.name) }, + ), + ) + + // Lag oppgave for behandling + oppgaveTaskService.opprettOppgaveTask(revurdering, Oppgavetype.BehandleSak) + + return revurdering + } + + @Transactional(readOnly = true) + fun hentBehandling(behandlingId: UUID): BehandlingDto { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsak = fagsakService.hentFagsak(behandling.fagsakId) + val erBehandlingPåVent: Boolean = behandlingskontrollService.erBehandlingPåVent(behandling.id) + val behandlingsstegsinfoer: List = behandlingskontrollService + .hentBehandlingsstegstilstand(behandling) + val varselSendt = brevsporingService.erVarselSendt(behandlingId) + val kanBehandlingHenlegges: Boolean = kanHenleggeBehandling(behandling) + val kanEndres: Boolean = kanBehandlingEndres(behandling, fagsak.fagsystem) + val kanRevurderingOpprettes: Boolean = + tilgangService.tilgangTilÅOppretteRevurdering(fagsak.fagsystem) && kanRevurderingOpprettes(behandling) + val støtterManuelleBrevmottakere = sjekkOmManuelleBrevmottakereErStøttet( + behandling = behandling, + fagsak = fagsak, + ) + val manuelleBrevmottakere = if (støtterManuelleBrevmottakere) { + manuellBrevmottakerRepository.findByBehandlingId(behandlingId) + } else { + emptyList() + } + + return BehandlingMapper.tilRespons( + behandling, + erBehandlingPåVent, + kanBehandlingHenlegges, + kanEndres, + kanRevurderingOpprettes, + behandlingsstegsinfoer, + varselSendt, + fagsak.eksternFagsakId, + manuelleBrevmottakere, + støtterManuelleBrevmottakere, + ) + } + + @Transactional + fun settBehandlingPåVent(behandlingId: UUID, behandlingPåVentDto: BehandlingPåVentDto) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + sjekkOmBehandlingAlleredeErAvsluttet(behandling) + + if (LocalDate.now() >= behandlingPåVentDto.tidsfrist) { + throw Feil( + message = "Fristen må være større enn dagens dato for behandling $behandlingId", + frontendFeilmelding = "Fristen må være større enn dagens dato for behandling $behandlingId", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + oppdaterAnsvarligSaksbehandler(behandlingId) + + behandlingskontrollService.settBehandlingPåVent( + behandlingId, + behandlingPåVentDto.venteårsak, + behandlingPåVentDto.tidsfrist, + ) + + val beskrivelse = when (behandlingPåVentDto.venteårsak) { + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING -> "Frist er oppdatert pga mottatt tilbakemelding fra bruker" + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG -> "Ny frist satt på bakgrunn av mottatt kravgrunnlag fra økonomi" + else -> "Frist er oppdatert av saksbehandler ${ContextService.hentSaksbehandler()}" + } + oppgaveTaskService.oppdaterOppgaveTask( + behandlingId, + beskrivelse, + behandlingPåVentDto.tidsfrist, + ContextService.hentSaksbehandler(), + ) + } + + @Transactional + fun taBehandlingAvvent(behandlingId: UUID) { // Denne metoden brukes kun for å gjenoppta behandling manuelt av saksbehandler + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + sjekkOmBehandlingAlleredeErAvsluttet(behandling) + + if (!behandlingskontrollService.erBehandlingPåVent(behandlingId)) { + throw Feil( + message = "Behandling $behandlingId er ikke på vent, kan ike gjenoppta", + frontendFeilmelding = "Behandling $behandlingId er ikke på vent, kan ike gjenoppta", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + oppdaterAnsvarligSaksbehandler(behandlingId) + + historikkTaskService.lagHistorikkTask( + behandling.id, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_GJENOPPTATT, + Aktør.SAKSBEHANDLER, + ) + + stegService.gjenopptaSteg(behandlingId) + oppgaveTaskService.oppdaterOppgaveTask( + behandlingId = behandlingId, + beskrivelse = "Behandling er tatt av vent", + frist = LocalDate.now(), + saksbehandler = ContextService.hentSaksbehandler(), + ) + + // oppdaterer oppgave hvis saken er fortsatt på vent, + // f.eks saken var på vent med brukerstilbakemelding og har ikke fått kravgrunnlag + val aktivStegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingId) + if (aktivStegstilstand?.behandlingsstegsstatus == Behandlingsstegstatus.VENTER) { + oppgaveTaskService.oppdaterOppgaveTaskMedTriggertid( + behandlingId = behandlingId, + beskrivelse = aktivStegstilstand.venteårsak!!.beskrivelse, + frist = aktivStegstilstand.tidsfrist!!, + triggerTid = 2L, + ) + } + } + + @Transactional + fun henleggBehandling(behandlingId: UUID, henleggelsesbrevFritekstDto: HenleggelsesbrevFritekstDto) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + sjekkOmBehandlingAlleredeErAvsluttet(behandling) + + val behandlingsresultatstype = henleggelsesbrevFritekstDto.behandlingsresultatstype + + if (!kanHenleggeBehandling(behandling, behandlingsresultatstype)) { + throw Feil( + message = "Behandling med behandlingId=$behandlingId kan ikke henlegges.", + frontendFeilmelding = "Behandling med behandlingId=$behandlingId kan ikke henlegges.", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + + // oppdaterer behandlingsstegstilstand + behandlingskontrollService.henleggBehandlingssteg(behandlingId) + + // oppdaterer behandlingsresultat og behandling + behandlingRepository.update( + behandling.copy( + resultater = setOf(Behandlingsresultat(type = behandlingsresultatstype)), + status = Behandlingsstatus.AVSLUTTET, + avsluttetDato = LocalDate.now(), + ), + ) + + oppdaterAnsvarligSaksbehandler(behandlingId) + behandlingTilstandService.opprettSendingAvBehandlingenHenlagt(behandlingId) + val fagsystem = fagsakService.finnFagsystem(behandling.fagsakId) + + val aktør = when (behandlingsresultatstype) { + Behandlingsresultatstype.HENLAGT_KRAVGRUNNLAG_NULLSTILT, + Behandlingsresultatstype.HENLAGT_TEKNISK_VEDLIKEHOLD, + -> Aktør.VEDTAKSLØSNING + + else -> Aktør.SAKSBEHANDLER + } + historikkTaskService.lagHistorikkTask( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BEHANDLING_HENLAGT, + aktør = aktør, + beskrivelse = henleggelsesbrevFritekstDto.begrunnelse, + ) + + if (kanSendeHenleggelsesbrev(behandling, behandlingsresultatstype)) { + taskService.save( + SendHenleggelsesbrevTask.opprettTask( + behandlingId, + fagsystem, + henleggelsesbrevFritekstDto.fritekst, + ), + ) + } + + // Ferdigstill oppgave + oppgaveTaskService.ferdigstilleOppgaveTask(behandlingId) + tellerService.tellVedtak(Behandlingsresultatstype.HENLAGT, behandling) + } + + @Transactional + fun oppdaterAnsvarligSaksbehandler(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update(behandling.copy(ansvarligSaksbehandler = ContextService.hentSaksbehandler())) + } + + @Transactional + fun oppdaterFaktainfo( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + eksternId: String, + respons: HentFagsystemsbehandling, + ) { + val behandling = behandlingRepository.finnÅpenTilbakekrevingsbehandling(ytelsestype, eksternFagsakId) + ?: throw Feil( + "Det finnes ikke en åpen behandling for " + + "eksternFagsakId=$eksternFagsakId,ytelsestype=$ytelsestype", + ) + val faktainfo = respons.faktainfo + val fagsystemskonsekvenser = + faktainfo.konsekvensForYtelser.map { Fagsystemskonsekvens(konsekvens = it) }.toSet() + if (behandling.aktivFagsystemsbehandling.eksternId == eksternId) { + logger.info( + "Det trenger ikke å oppdatere fakta info siden tilbakekrevingsbehandling " + + "er allerede koblet med riktig fagsystemsbehandling", + ) + return + } + val gammelFagsystemsbehandling = behandling.aktivFagsystemsbehandling.copy(aktiv = false) + val nyFagsystemsbehandling = Fagsystemsbehandling( + eksternId = eksternId, + årsak = faktainfo.revurderingsårsak, + resultat = faktainfo.revurderingsresultat, + // kopier gammel tilbakekrevingsvalg om det ikke finnes i fagsystem + tilbakekrevingsvalg = faktainfo.tilbakekrevingsvalg + ?: gammelFagsystemsbehandling.tilbakekrevingsvalg, + revurderingsvedtaksdato = respons.revurderingsvedtaksdato, + konsekvenser = fagsystemskonsekvenser, + ) + behandlingRepository.update( + behandling.copy( + fagsystemsbehandling = setOf( + gammelFagsystemsbehandling, + nyFagsystemsbehandling, + ), + regelverk = respons.regelverk, + ), + ) + } + + @Transactional + fun byttBehandlendeEnhet(behandlingId: UUID, byttEnhetDto: ByttEnhetDto) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + sjekkOmBehandlingAlleredeErAvsluttet(behandling) + val fagsystem = fagsakService.finnFagsystem(behandling.fagsakId) + if (fagsystem != Fagsystem.BA) { + throw Feil( + message = "Ikke implementert for fagsystem $fagsystem", + frontendFeilmelding = "Ikke implementert for fagsystem: ${fagsystem.navn}", + ) + } + val enhet = integrasjonerClient.hentNavkontor(byttEnhetDto.enhet) + behandlingRepository.update( + behandling.copy( + behandlendeEnhet = byttEnhetDto.enhet, + behandlendeEnhetsNavn = enhet.navn, + ), + ) + oppdaterAnsvarligSaksbehandler(behandlingId) + + historikkTaskService.lagHistorikkTask( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.ENDRET_ENHET, + aktør = Aktør.SAKSBEHANDLER, + beskrivelse = byttEnhetDto.begrunnelse, + ) + + oppgaveTaskService.oppdaterEnhetOppgaveTask( + behandlingId = behandlingId, + beskrivelse = "Endret tildelt enhet: " + byttEnhetDto.enhet, + enhetId = byttEnhetDto.enhet, + ) + } + + private fun opprettFørstegangsbehandling(opprettTilbakekrevingRequest: OpprettTilbakekrevingRequest): Behandling { + val ytelsestype = opprettTilbakekrevingRequest.ytelsestype + val fagsystem = opprettTilbakekrevingRequest.fagsystem + validateFagsystem(ytelsestype, fagsystem) + val eksternFagsakId = opprettTilbakekrevingRequest.eksternFagsakId + val eksternId = opprettTilbakekrevingRequest.eksternId + val erManueltOpprettet = opprettTilbakekrevingRequest.manueltOpprettet + val brevmottakere = opprettTilbakekrevingRequest.manuelleBrevmottakere + + val ansvarligsaksbehandler = + integrasjonerClient.hentSaksbehandler(opprettTilbakekrevingRequest.saksbehandlerIdent) + + logger.info( + "Oppretter Tilbakekrevingsbehandling for ytelsestype=$ytelsestype,eksternFagsakId=$eksternFagsakId " + + "og eksternId=$eksternId", + ) + secureLogger.info( + "Oppretter Tilbakekrevingsbehandling for ytelsestype=$ytelsestype,eksternFagsakId=$eksternFagsakId " + + " og personIdent=${opprettTilbakekrevingRequest.personIdent}", + ) + + kanBehandlingOpprettes(ytelsestype, eksternFagsakId, eksternId, erManueltOpprettet) + + // oppretter fagsak hvis det ikke finnes ellers bruker det eksisterende + val eksisterendeFagsak = fagsakService.finnFagsak(fagsystem, eksternFagsakId) + val fagsak = eksisterendeFagsak + ?: fagsakService.opprettFagsak(opprettTilbakekrevingRequest, ytelsestype, fagsystem) + + val behandling = BehandlingMapper.tilDomeneBehandling( + opprettTilbakekrevingRequest, + fagsystem, + fagsak, + ansvarligsaksbehandler, + ) + behandlingRepository.insert(behandling) + + historikkTaskService.lagHistorikkTask( + behandling.id, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_OPPRETTET, + Aktør.VEDTAKSLØSNING, + ) + + behandlingskontrollService.fortsettBehandling(behandling.id) + stegService.håndterSteg(behandling.id) + + val manuelleBrevmottakere = brevmottakere.map { brevmottaker -> + ManuellBrevmottaker( + behandlingId = behandling.id, + type = brevmottaker.type, + ident = brevmottaker.personIdent, + orgNr = brevmottaker.organisasjonsnummer, + adresselinje1 = brevmottaker.manuellAdresseInfo?.adresselinje1, + adresselinje2 = brevmottaker.manuellAdresseInfo?.adresselinje2, + postnummer = brevmottaker.manuellAdresseInfo?.postnummer, + poststed = brevmottaker.manuellAdresseInfo?.poststed, + landkode = brevmottaker.manuellAdresseInfo?.landkode, + navn = brevmottaker.navn, + vergetype = brevmottaker.vergetype, + ) + } + + if (manuelleBrevmottakere.isNotEmpty()) { + logger.info("Lagrer ${manuelleBrevmottakere.size} manuell(e) brevmottaker(e) oversendt fra $fagsystem-sak") + manuellBrevmottakerRepository.insertAll(manuelleBrevmottakere) + håndterBrevmottakerSteg(behandling, fagsak) + } + + // kjør FinnGrunnlagTask for å finne og koble grunnlag med behandling + taskService.save( + Task( + type = FinnKravgrunnlagTask.TYPE, + payload = behandling.id.toString(), + properties = Properties().apply { setProperty(PropertyName.FAGSYSTEM, fagsystem.name) }, + ), + ) + + return behandling + } + + private fun validateFagsystem( + ytelsestype: Ytelsestype, + fagsystem: Fagsystem, + ) { + if (FagsystemUtil.hentFagsystemFraYtelsestype(ytelsestype) != fagsystem) { + throw Feil( + message = "Behandling kan ikke opprettes med ytelsestype=$ytelsestype og fagsystem=$fagsystem", + frontendFeilmelding = "Behandling kan ikke opprettes med ytelsestype=$ytelsestype og fagsystem=$fagsystem", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + + private fun kanBehandlingOpprettes( + ytelsestype: Ytelsestype, + eksternFagsakId: String, + eksternId: String, + erManueltOpprettet: Boolean, + ) { + val behandling: Behandling? = + behandlingRepository.finnÅpenTilbakekrevingsbehandling(ytelsestype, eksternFagsakId) + if (behandling != null) { + val feilMelding = "Det finnes allerede en åpen behandling for ytelsestype=$ytelsestype " + + "og eksternFagsakId=$eksternFagsakId, kan ikke opprette en ny." + throw Feil( + message = feilMelding, + frontendFeilmelding = feilMelding, + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + + // hvis behandlingen er henlagt, kan det opprettes ny behandling + // hvis toggelen KAN_OPPRETTE_BEH_MED_EKSTERNID_SOM_HAR_AVSLUTTET_TBK er på, + // sjekker ikke om det finnes en avsluttet tilbakekreving for eksternId + if (!featureToggleService.isEnabled(FeatureToggleConfig.KAN_OPPRETTE_BEH_MED_EKSTERNID_SOM_HAR_AVSLUTTET_TBK)) { + val avsluttetBehandlinger = behandlingRepository.finnAvsluttetTilbakekrevingsbehandlinger(eksternId) + if (avsluttetBehandlinger.isNotEmpty()) { + val sisteAvsluttetBehandling: Behandling = avsluttetBehandlinger.first() + val erSisteBehandlingHenlagt: Boolean = + sisteAvsluttetBehandling.resultater.any { it.erBehandlingHenlagt() } + if (!erSisteBehandlingHenlagt) { + val feilMelding = "Det finnes allerede en avsluttet behandling for ytelsestype=$ytelsestype " + + "og eksternFagsakId=$eksternFagsakId som ikke er henlagt, kan ikke opprette en ny." + throw Feil( + message = feilMelding, + frontendFeilmelding = feilMelding, + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + } + + // uten kravgrunnlag er det ikke mulig å opprette behandling manuelt + if (erManueltOpprettet && !økonomiXmlMottattRepository + .existsByEksternFagsakIdAndYtelsestypeAndReferanse(eksternFagsakId, ytelsestype, eksternId) + ) { + val feilMelding = + "Det finnes intet kravgrunnlag for ytelsestype=$ytelsestype,eksternFagsakId=$eksternFagsakId " + + "og eksternId=$eksternId. Tilbakekrevingsbehandling kan ikke opprettes manuelt." + throw Feil(message = feilMelding, frontendFeilmelding = feilMelding) + } + } + + private fun kanHenleggeBehandling( + behandling: Behandling, + behandlingsresultatstype: Behandlingsresultatstype? = null, + ): Boolean { + if (Behandlingsresultatstype.HENLAGT_KRAVGRUNNLAG_NULLSTILT == behandlingsresultatstype) { + return true + } else if (TILBAKEKREVING == behandling.type) { + return !behandling.erAvsluttet && ( + !behandling.manueltOpprettet && + behandling.opprettetTidspunkt < LocalDate.now() + .atStartOfDay() + .minusDays(opprettelseDagerBegrensning) + ) && + !kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandling.id) + } + return true + } + + private fun kanSendeHenleggelsesbrev( + behandling: Behandling, + behandlingsresultatstype: Behandlingsresultatstype, + ): Boolean { + return when (behandling.type) { + TILBAKEKREVING -> brevsporingService.erVarselSendt(behandling.id) + REVURDERING_TILBAKEKREVING -> Behandlingsresultatstype.HENLAGT_FEILOPPRETTET_MED_BREV == behandlingsresultatstype + } + } + + private fun sjekkOmBehandlingAlleredeErAvsluttet(behandling: Behandling) { + if (behandling.erSaksbehandlingAvsluttet) { + throw Feil( + "Behandling med id=${behandling.id} er allerede ferdig behandlet.", + frontendFeilmelding = "Behandling med id=${behandling.id} er allerede ferdig behandlet.", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + + private fun kanBehandlingEndres(behandling: Behandling, fagsystem: Fagsystem): Boolean { + if (behandling.erSaksbehandlingAvsluttet) { + return false + } + if (Behandlingsstatus.FATTER_VEDTAK == behandling.status && + behandling.ansvarligSaksbehandler == ContextService.hentSaksbehandler() + ) { + return false + } + + return when (tilgangService.finnBehandlerrolle(fagsystem)) { + Behandlerrolle.SAKSBEHANDLER -> (Behandlingsstatus.UTREDES == behandling.status) + Behandlerrolle.BESLUTTER, Behandlerrolle.SYSTEM -> true + else -> false + } + } + + private fun kanRevurderingOpprettes(behandling: Behandling): Boolean { + return behandling.erAvsluttet && + !Behandlingsresultat.ALLE_HENLEGGELSESKODER.contains(behandling.sisteResultat?.type) && + kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandling.id) && + behandlingRepository.finnÅpenTilbakekrevingsrevurdering(behandling.id) == null + } + + private fun håndterBrevmottakerSteg( + behandling: Behandling, + fagsak: Fagsak, + ) { + if (sjekkOmManuelleBrevmottakereErStøttet( + behandling = behandling, + fagsak = fagsak, + ) + ) { + behandlingskontrollService.behandleBrevmottakerSteg(behandling.id) + } + } + + companion object { + fun sjekkOmManuelleBrevmottakereErStøttet( + behandling: Behandling, + fagsak: Fagsak, + ): Boolean = fagsak.institusjon == null && !behandling.harVerge + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingsvedtakService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingsvedtakService.kt new file mode 100644 index 000000000..c85a3af25 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/BehandlingsvedtakService.kt @@ -0,0 +1,61 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultat +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsvedtak +import no.nav.familie.tilbake.behandling.domain.Iverksettingsstatus +import no.nav.familie.tilbake.beregning.TilbakekrevingsberegningService +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.micrometer.TellerService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.util.UUID + +@Service +class BehandlingsvedtakService( + private val behandlingRepository: BehandlingRepository, + private val tellerService: TellerService, + private val tilbakeBeregningService: TilbakekrevingsberegningService, +) { + + @Transactional + fun opprettBehandlingsvedtak(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + + val beregningsresultat = tilbakeBeregningService.beregn(behandlingId) + val behandlingsresultatstype = utledBehandlingsresultatstype(beregningsresultat.vedtaksresultat) + + val behandlingsvedtak = Behandlingsvedtak( + vedtaksdato = LocalDate.now(), + iverksettingsstatus = Iverksettingsstatus.IKKE_IVERKSATT, + ) + val behandlingsresultat = Behandlingsresultat( + type = behandlingsresultatstype, + behandlingsvedtak = behandlingsvedtak, + ) + behandlingRepository.update(behandling.copy(resultater = setOf(behandlingsresultat))) + tellerService.tellVedtak(behandlingsresultatstype, behandling) + } + + @Transactional + fun oppdaterBehandlingsvedtak(behandlingId: UUID, iverksettingsstatus: Iverksettingsstatus): Behandling { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val aktivBehandlingsresultat = requireNotNull(behandling.sisteResultat) { "Behandlingsresultat kan ikke være null" } + val behandlingsvedtak = + requireNotNull(aktivBehandlingsresultat.behandlingsvedtak) { "Behandlingsvedtak kan ikke være null" } + val oppdatertBehandlingsresultat = aktivBehandlingsresultat + .copy(behandlingsvedtak = behandlingsvedtak.copy(iverksettingsstatus = iverksettingsstatus)) + return behandlingRepository.update(behandling.copy(resultater = setOf(oppdatertBehandlingsresultat))) + } + + private fun utledBehandlingsresultatstype(vedtaksresultat: Vedtaksresultat): Behandlingsresultatstype { + return when (vedtaksresultat) { + Vedtaksresultat.INGEN_TILBAKEBETALING -> Behandlingsresultatstype.INGEN_TILBAKEBETALING + Vedtaksresultat.DELVIS_TILBAKEBETALING -> Behandlingsresultatstype.DELVIS_TILBAKEBETALING + Vedtaksresultat.FULL_TILBAKEBETALING -> Behandlingsresultatstype.FULL_TILBAKEBETALING + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/FagsakMapper.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/FagsakMapper.kt new file mode 100644 index 000000000..6d819242a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/FagsakMapper.kt @@ -0,0 +1,50 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.tilbake.api.dto.BehandlingsoppsummeringDto +import no.nav.familie.tilbake.api.dto.BrukerDto +import no.nav.familie.tilbake.api.dto.FagsakDto +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import no.nav.familie.tilbake.organisasjon.OrganisasjonService + +object FagsakMapper { + + fun tilRespons( + fagsak: Fagsak, + personinfo: Personinfo, + behandlinger: List, + organisasjonService: OrganisasjonService, + ): FagsakDto { + val bruker = BrukerDto( + personIdent = fagsak.bruker.ident, + navn = personinfo.navn, + fødselsdato = personinfo.fødselsdato, + kjønn = personinfo.kjønn, + dødsdato = personinfo.dødsdato, + ) + + val behandlingListe = behandlinger.map { + BehandlingsoppsummeringDto( + behandlingId = it.id, + eksternBrukId = it.eksternBrukId, + type = it.type, + status = it.status, + ) + } + + val institusjon = fagsak.institusjon?.let { + organisasjonService.mapTilInstitusjonDto(orgnummer = it.organisasjonsnummer) + } + + return FagsakDto( + eksternFagsakId = fagsak.eksternFagsakId, + ytelsestype = fagsak.ytelsestype, + fagsystem = fagsak.fagsystem, + språkkode = fagsak.bruker.språkkode, + bruker = bruker, + behandlinger = behandlingListe, + institusjon = institusjon, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/FagsakRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/FagsakRepository.kt new file mode 100644 index 000000000..fa7831906 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/FagsakRepository.kt @@ -0,0 +1,24 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import org.springframework.data.jdbc.repository.query.Query +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository +interface FagsakRepository : RepositoryInterface, InsertUpdateRepository { + + @Query("""SELECT f.* FROM fagsak f JOIN behandling b ON b.fagsak_id = f.id WHERE b.id = :behandlingId""") + fun finnFagsakForBehandlingId(behandlingId: UUID): Fagsak + + @Query("""SELECT f.* FROM fagsak f JOIN behandling b ON b.fagsak_id = f.id WHERE b.ekstern_bruk_id = :eksternBrukId""") + fun finnFagsakForEksternBrukId(eksternBrukId: UUID): Fagsak + + fun findByFagsystemAndEksternFagsakId(fagsystem: Fagsystem, eksternFagsakId: String): Fagsak? + + @Query("""SELECT f.* FROM fagsak f WHERE f.fagsystem = :fagsystem AND f.bruker_ident=:personIdent""") + fun finnFagsakForFagsystemAndIdent(fagsystem: Fagsystem, personIdent: String): Fagsak? +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/FagsakService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/FagsakService.kt new file mode 100644 index 000000000..0c4c07d32 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/FagsakService.kt @@ -0,0 +1,231 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.tilbakekreving.FinnesBehandlingResponse +import no.nav.familie.kontrakter.felles.tilbakekreving.KanBehandlingOpprettesManueltRespons +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.api.dto.FagsakDto +import no.nav.familie.tilbake.behandling.domain.Bruker +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandling.domain.Institusjon +import no.nav.familie.tilbake.behandling.event.EndretPersonIdentEvent +import no.nav.familie.tilbake.behandling.task.OpprettBehandlingManueltTask +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattRepository +import no.nav.familie.tilbake.organisasjon.OrganisasjonService +import no.nav.familie.tilbake.person.PersonService +import org.springframework.context.event.EventListener +import org.springframework.data.domain.Pageable +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class FagsakService( + private val fagsakRepository: FagsakRepository, + private val behandlingRepository: BehandlingRepository, + private val taskService: TaskService, + private val økonomiXmlMottattRepository: ØkonomiXmlMottattRepository, + private val personService: PersonService, + private val organisasjonService: OrganisasjonService, +) { + + fun hentFagsak(fagsakId: UUID): Fagsak { + return fagsakRepository.findByIdOrThrow(fagsakId) + } + + @Transactional(readOnly = true) + fun hentFagsak(fagsystem: Fagsystem, eksternFagsakId: String): FagsakDto { + val fagsak = fagsakRepository.findByFagsystemAndEksternFagsakId( + fagsystem = fagsystem, + eksternFagsakId = eksternFagsakId, + ) + ?: throw Feil( + message = "Fagsak finnes ikke for ${fagsystem.navn} og $eksternFagsakId", + frontendFeilmelding = "Fagsak finnes ikke for ${fagsystem.navn} og $eksternFagsakId", + httpStatus = HttpStatus.BAD_REQUEST, + ) + val personInfo = personService.hentPersoninfo( + personIdent = fagsak.bruker.ident, + fagsystem = fagsak.fagsystem, + ) + val behandlinger = behandlingRepository.findByFagsakId(fagsakId = fagsak.id) + + return FagsakMapper.tilRespons( + fagsak = fagsak, + personinfo = personInfo, + behandlinger = behandlinger, + organisasjonService = organisasjonService, + ) + } + + @Transactional + fun finnFagsak(fagsystem: Fagsystem, eksternFagsakId: String): Fagsak? { + return fagsakRepository.findByFagsystemAndEksternFagsakId( + fagsystem = fagsystem, + eksternFagsakId = eksternFagsakId, + ) + } + + @Transactional + fun finnFagsystem(fagsakId: UUID): Fagsystem { + return fagsakRepository.findByIdOrThrow(fagsakId).fagsystem + } + + @Transactional + fun finnFagsystemForBehandlingId(behandlingId: UUID): Fagsystem { + return fagsakRepository.finnFagsakForBehandlingId(behandlingId).fagsystem + } + + @Transactional + fun opprettFagsak( + opprettTilbakekrevingRequest: OpprettTilbakekrevingRequest, + ytelsestype: Ytelsestype, + fagsystem: Fagsystem, + ): Fagsak { + val bruker = Bruker( + ident = opprettTilbakekrevingRequest.personIdent, + språkkode = opprettTilbakekrevingRequest.språkkode, + ) + val institusjon = opprettTilbakekrevingRequest.institusjon?.let { + Institusjon( + organisasjonsnummer = it.organisasjonsnummer, + ) + } + return fagsakRepository.insert( + Fagsak( + bruker = bruker, + eksternFagsakId = opprettTilbakekrevingRequest.eksternFagsakId, + ytelsestype = ytelsestype, + fagsystem = fagsystem, + institusjon = institusjon, + ), + ) + } + + @Transactional(readOnly = true) + fun finnesÅpenTilbakekrevingsbehandling(fagsystem: Fagsystem, eksternFagsakId: String): FinnesBehandlingResponse { + val fagsak = fagsakRepository.findByFagsystemAndEksternFagsakId( + fagsystem = fagsystem, + eksternFagsakId = eksternFagsakId, + ) + var finnesÅpenBehandling = false + if (fagsak != null) { + finnesÅpenBehandling = + behandlingRepository.finnÅpenTilbakekrevingsbehandling( + ytelsestype = fagsak.ytelsestype, + eksternFagsakId = eksternFagsakId, + ) != null + } + return FinnesBehandlingResponse(finnesÅpenBehandling = finnesÅpenBehandling) + } + + @Transactional(readOnly = true) + fun hentBehandlingerForFagsak( + fagsystem: Fagsystem, + eksternFagsakId: String, + ): List { + val fagsak = fagsakRepository.findByFagsystemAndEksternFagsakId( + fagsystem = fagsystem, + eksternFagsakId = eksternFagsakId, + ) + + return fagsak?.let { + val behandlinger = behandlingRepository.findByFagsakId(fagsakId = fagsak.id) + behandlinger.map { BehandlingMapper.tilBehandlingerForFagsystem(it) } + } ?: emptyList() + } + + @Transactional(readOnly = true) + fun hentVedtakForFagsak( + fagsystem: Fagsystem, + eksternFagsakId: String, + ): List { + val fagsak = fagsakRepository.findByFagsystemAndEksternFagsakId( + fagsystem = fagsystem, + eksternFagsakId = eksternFagsakId, + ) + + return fagsak?.let { + val behandlinger = behandlingRepository.findByFagsakId(fagsakId = fagsak.id) + BehandlingMapper.tilVedtakForFagsystem(behandlinger) + } ?: emptyList() + } + + @Transactional(readOnly = true) + fun kanBehandlingOpprettesManuelt( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + ): KanBehandlingOpprettesManueltRespons { + val finnesÅpenTilbakekreving: Boolean = + behandlingRepository.finnÅpenTilbakekrevingsbehandling(ytelsestype, eksternFagsakId) != null + if (finnesÅpenTilbakekreving) { + return KanBehandlingOpprettesManueltRespons( + kanBehandlingOpprettes = false, + melding = "Det finnes allerede en åpen tilbakekrevingsbehandling. " + + "Den ligger i saksoversikten.", + ) + } + val kravgrunnlagene = økonomiXmlMottattRepository.findByEksternFagsakIdAndYtelsestype(eksternFagsakId, ytelsestype) + if (kravgrunnlagene.isEmpty()) { + return KanBehandlingOpprettesManueltRespons( + kanBehandlingOpprettes = false, + melding = "Det finnes ingen feilutbetaling på saken, så du kan " + + "ikke opprette tilbakekrevingsbehandling.", + ) + } + val kravgrunnlagsreferanse = kravgrunnlagene.first().referanse + val harAlledeMottattForespørselen: Boolean = + taskService.finnTasksMedStatus( + listOf( + Status.UBEHANDLET, + Status.BEHANDLER, + Status.KLAR_TIL_PLUKK, + Status.PLUKKET, + Status.FEILET, + ), + Pageable.unpaged(), + ) + .any { + OpprettBehandlingManueltTask.TYPE == it.type && + eksternFagsakId == it.metadata.getProperty("eksternFagsakId") && + ytelsestype.kode == it.metadata.getProperty("ytelsestype") + kravgrunnlagsreferanse == it.metadata.getProperty("eksternId") + } + + if (harAlledeMottattForespørselen) { + return KanBehandlingOpprettesManueltRespons( + kanBehandlingOpprettes = false, + melding = "Det finnes allerede en forespørsel om å opprette " + + "tilbakekrevingsbehandling. Behandlingen vil snart bli " + + "tilgjengelig i saksoversikten. Dersom den ikke dukker opp, " + + "ta kontakt med brukerstøtte for å rapportere feilen.", + ) + } + return KanBehandlingOpprettesManueltRespons( + kanBehandlingOpprettes = true, + kravgrunnlagsreferanse = kravgrunnlagsreferanse, + melding = "Det er mulig å opprette behandling manuelt.", + ) + } + + @EventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun oppdaterPersonIdent(endretPersonIdentEvent: EndretPersonIdentEvent) { + val fagsak = fagsakRepository.findByIdOrThrow(endretPersonIdentEvent.fagsakId) + fagsakRepository.update( + fagsak.copy( + bruker = Bruker( + ident = endretPersonIdentEvent.source as String, + språkkode = fagsak.bruker.språkkode, + ), + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/FagsystemUtil.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/FagsystemUtil.kt new file mode 100644 index 000000000..29d5fafb3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/FagsystemUtil.kt @@ -0,0 +1,17 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype + +object FagsystemUtil { + + fun hentFagsystemFraYtelsestype(type: Ytelsestype): Fagsystem { + return when (type) { + Ytelsestype.BARNETRYGD -> Fagsystem.BA + Ytelsestype.KONTANTSTØTTE -> Fagsystem.KONT + Ytelsestype.OVERGANGSSTØNAD -> Fagsystem.EF + Ytelsestype.BARNETILSYN -> Fagsystem.EF + Ytelsestype.SKOLEPENGER -> Fagsystem.EF + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/HentFagsystemsbehandlingRequestSendtRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/HentFagsystemsbehandlingRequestSendtRepository.kt new file mode 100644 index 000000000..24d0af036 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/HentFagsystemsbehandlingRequestSendtRepository.kt @@ -0,0 +1,20 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.behandling.domain.HentFagsystemsbehandlingRequestSendt +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository +interface HentFagsystemsbehandlingRequestSendtRepository : + RepositoryInterface, + InsertUpdateRepository { + + fun findByEksternFagsakIdAndYtelsestypeAndEksternId( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + eksternId: String, + ): HentFagsystemsbehandlingRequestSendt? +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/HentFagsystemsbehandlingService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/HentFagsystemsbehandlingService.kt new file mode 100644 index 000000000..c70ddadb3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/HentFagsystemsbehandlingService.kt @@ -0,0 +1,92 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRespons +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.behandling.domain.HentFagsystemsbehandlingRequestSendt +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.integration.kafka.KafkaProducer +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class HentFagsystemsbehandlingService( + private val requestSendtRepository: HentFagsystemsbehandlingRequestSendtRepository, + private val kafkaProducer: KafkaProducer, +) { + + @Transactional + fun sendHentFagsystemsbehandlingRequest( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + eksternId: String, + ) { + val eksisterendeRequestSendt = + requestSendtRepository.findByEksternFagsakIdAndYtelsestypeAndEksternId(eksternFagsakId, ytelsestype, eksternId) + if (eksisterendeRequestSendt == null) { + opprettOgSendHentFagsystembehandlingRequest(eksternFagsakId, ytelsestype, eksternId) + } + } + + private fun opprettOgSendHentFagsystembehandlingRequest( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + eksternId: String, + ) { + val requestSendt = requestSendtRepository.insert( + HentFagsystemsbehandlingRequestSendt( + eksternFagsakId = eksternFagsakId, + ytelsestype = ytelsestype, + eksternId = eksternId, + ), + ) + + val request = HentFagsystemsbehandlingRequest(eksternFagsakId, ytelsestype, eksternId) + kafkaProducer.sendHentFagsystemsbehandlingRequest(requestSendt.id, request) + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun slettOgSendNyHentFagsystembehandlingRequest( + requestSendtId: UUID, + eksternFagsakId: String, + ytelsestype: Ytelsestype, + eksternId: String, + ) { + fjernHentFagsystemsbehandlingRequest(requestSendtId) + opprettOgSendHentFagsystembehandlingRequest(eksternFagsakId, ytelsestype, eksternId) + } + + @Transactional + fun lagreHentFagsystemsbehandlingRespons( + requestId: UUID, + respons: String, + ) { + val fagsystemsbehandlingRequestSendt = requestSendtRepository.findByIdOrThrow(requestId) + requestSendtRepository.update(fagsystemsbehandlingRequestSendt.copy(respons = respons)) + } + + @Transactional + fun hentFagsystemsbehandlingRequestSendt( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + eksternId: String, + ): HentFagsystemsbehandlingRequestSendt? { + return requestSendtRepository.findByEksternFagsakIdAndYtelsestypeAndEksternId( + eksternFagsakId, + ytelsestype, + eksternId, + ) + } + + @Transactional + fun fjernHentFagsystemsbehandlingRequest(requestId: UUID) { + requestSendtRepository.deleteById(requestId) + } + + fun lesRespons(respons: String): HentFagsystemsbehandlingRespons { + return objectMapper.readValue(respons, HentFagsystemsbehandlingRespons::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/LagreUtkastVedtaksbrevService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/LagreUtkastVedtaksbrevService.kt new file mode 100644 index 000000000..8faab1f00 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/LagreUtkastVedtaksbrevService.kt @@ -0,0 +1,33 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.tilbake.api.dto.FritekstavsnittDto +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.dokumentbestilling.vedtak.VedtaksbrevService +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class LagreUtkastVedtaksbrevService( + val behandlingRepository: BehandlingRepository, + val behandlingskontrollService: BehandlingskontrollService, + val vedtaksbrevService: VedtaksbrevService, +) { + + fun lagreUtkast(behandlingId: UUID, fritekstavsnitt: FritekstavsnittDto) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + if (behandling.erSaksbehandlingAvsluttet) { + throw Feil("Behandling med id=$behandlingId er allerede ferdig behandlet") + } + if (behandlingskontrollService.erBehandlingPåVent(behandlingId)) { + throw Feil( + message = "Behandling med id=$behandlingId er på vent, kan ikke lagre utkast av vedtaksbrevet", + frontendFeilmelding = "Behandling med id=$behandlingId er på vent, kan ikke lagre utkast av vedtaksbrevet", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + vedtaksbrevService.lagreUtkastAvFritekster(behandlingId, fritekstavsnitt) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/ValiderBrevmottakerService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/ValiderBrevmottakerService.kt new file mode 100644 index 000000000..5d696df61 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/ValiderBrevmottakerService.kt @@ -0,0 +1,47 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.ManuellBrevmottakerRepository +import no.nav.familie.tilbake.person.PersonService +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class ValiderBrevmottakerService( + private val manuellBrevmottakerRepository: ManuellBrevmottakerRepository, + private val fagsakService: FagsakService, + private val personService: PersonService, +) { + fun validerAtBehandlingIkkeInneholderStrengtFortroligPersonMedManuelleBrevmottakere(behandlingId: UUID, fagsakId: UUID) { + val manuelleBrevmottakere = manuellBrevmottakerRepository.findByBehandlingId(behandlingId).takeIf { it.isNotEmpty() } ?: return + val fagsak = fagsakService.hentFagsak(fagsakId) + val bruker = fagsak.bruker + val fagsystem = fagsak.fagsystem + val personIdenter = listOfNotNull(bruker.ident) + if (personIdenter.isEmpty()) return + val strengtFortroligePersonIdenter = personService.hentIdenterMedStrengtFortroligAdressebeskyttelse(personIdenter, fagsystem) + if (strengtFortroligePersonIdenter.isNotEmpty()) { + val melding = + "Behandlingen (id: $behandlingId) inneholder person med strengt fortrolig adressebeskyttelse og kan ikke kombineres med manuelle brevmottakere (${manuelleBrevmottakere.size} stk)." + val frontendFeilmelding = + "Behandlingen inneholder person med strengt fortrolig adressebeskyttelse og kan ikke kombineres med manuelle brevmottakere." + throw Feil(melding, frontendFeilmelding) + } + } + + fun validerAtBehandlingenIkkeInneholderStrengtFortroligPerson(behandlingId: UUID, fagsakId: UUID) { + val fagsak = fagsakService.hentFagsak(fagsakId) + val bruker = fagsak.bruker + val fagsystem = fagsak.fagsystem + val personIdenter = listOfNotNull(bruker.ident) + if (personIdenter.isEmpty()) return + val strengtFortroligePersonIdenter = personService.hentIdenterMedStrengtFortroligAdressebeskyttelse(personIdenter, fagsystem) + if (strengtFortroligePersonIdenter.isNotEmpty()) { + val melding = + "Behandlingen (id: $behandlingId) inneholder person med strengt fortrolig adressebeskyttelse og kan ikke kombineres med manuelle brevmottakere." + val frontendFeilmelding = + "Behandlingen inneholder person med strengt fortrolig adressebeskyttelse og kan ikke kombineres med manuelle brevmottakere." + throw Feil(melding, frontendFeilmelding) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/VarselService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/VarselService.kt new file mode 100644 index 000000000..21a2eb7e0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/VarselService.kt @@ -0,0 +1,37 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.tilbake.behandling.domain.Varsel +import no.nav.familie.tilbake.behandling.domain.Varselsperiode +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingService +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class VarselService( + private val behandlingRepository: BehandlingRepository, + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val faktaFeilutbetalingService: FaktaFeilutbetalingService, +) { + + fun lagre(behandlingId: UUID, varseltekst: String, varselbeløp: Long) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val varselsperioder: Set = if (kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandlingId)) { + val perioder = faktaFeilutbetalingService.hentFaktaomfeilutbetaling(behandlingId).feilutbetaltePerioder + perioder.map { Varselsperiode(fom = it.periode.fom, tom = it.periode.tom) }.toSet() + } else { + behandling.aktivtVarsel?.perioder?.map { Varselsperiode(fom = it.fom, tom = it.tom) }?.toSet() + ?: error("Aktivt varsel har ikke med varselsperioder") + } + + val varsler = behandling.varsler.map { it.copy(aktiv = false) } + + Varsel( + varseltekst = varseltekst, + varselbeløp = varselbeløp, + perioder = varselsperioder, + ) + val copy = behandling.copy(varsler = varsler.toSet()) + behandlingRepository.update(copy) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/VergeService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/VergeService.kt new file mode 100644 index 000000000..80e465523 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/VergeService.kt @@ -0,0 +1,147 @@ +package no.nav.familie.tilbake.behandling + +import no.nav.familie.kontrakter.felles.Applikasjon +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.tilbakekreving.Vergetype +import no.nav.familie.tilbake.api.dto.VergeDto +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Verge +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import no.nav.familie.tilbake.person.PersonService +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class VergeService( + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, + private val historikkTaskService: HistorikkTaskService, + private val behandlingskontrollService: BehandlingskontrollService, + private val integrasjonerClient: IntegrasjonerClient, + private val personService: PersonService, +) { + + @Transactional + fun lagreVerge(behandlingId: UUID, vergeDto: VergeDto) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + validerBehandling(behandling) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + validerVergeData(vergeDto, fagsak.fagsystem) + + val verge = tilDomene(vergeDto) + val oppdatertBehandling = behandling.copy(verger = behandling.verger.map { it.copy(aktiv = false) }.toSet() + verge) + behandlingRepository.update(oppdatertBehandling) + historikkTaskService.lagHistorikkTask( + behandling.id, + TilbakekrevingHistorikkinnslagstype.VERGE_OPPRETTET, + Aktør.SAKSBEHANDLER, + ) + } + + @Transactional + fun opprettVergeSteg(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + validerBehandling(behandling) + behandlingskontrollService.behandleVergeSteg(behandlingId) + } + + @Transactional + fun fjernVerge(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val finnesAktivVerge = behandling.harVerge + + if (finnesAktivVerge) { + val oppdatertBehandling = behandling.copy(verger = behandling.verger.map { it.copy(aktiv = false) }.toSet()) + behandlingRepository.update(oppdatertBehandling) + historikkTaskService.lagHistorikkTask( + behandling.id, + TilbakekrevingHistorikkinnslagstype.VERGE_FJERNET, + Aktør.SAKSBEHANDLER, + ) + } + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.VERGE, + Behandlingsstegstatus.TILBAKEFØRT, + ), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + @Transactional(readOnly = true) + fun hentVerge(behandlingId: UUID): VergeDto? { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + return behandling.aktivVerge?.let { tilRespons(it) } + } + + private fun validerBehandling(behandling: Behandling) { + if (behandling.erSaksbehandlingAvsluttet) { + throw Feil( + "Behandling med id=${behandling.id} er allerede ferdig behandlet.", + frontendFeilmelding = "Behandling med id=${behandling.id} er allerede ferdig behandlet.", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + if (behandlingskontrollService.erBehandlingPåVent(behandling.id)) { + throw Feil( + "Behandling med id=${behandling.id} er på vent.", + frontendFeilmelding = "Behandling med id=${behandling.id} er på vent.", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + + private fun validerVergeData(vergeDto: VergeDto, fagsystem: Fagsystem) { + when (vergeDto.type) { + Vergetype.ADVOKAT -> { + requireNotNull(vergeDto.orgNr) { "orgNr kan ikke være null for ${Vergetype.ADVOKAT}" } + val erGyldig = integrasjonerClient.validerOrganisasjon(vergeDto.orgNr) + if (!erGyldig) { + throw Feil( + message = "Organisasjon ${vergeDto.orgNr} er ikke gyldig", + frontendFeilmelding = "Organisasjon ${vergeDto.orgNr} er ikke gyldig", + ) + } + } + else -> { + requireNotNull(vergeDto.ident) { "ident kan ikke være null for ${vergeDto.type}" } + // Henter personen å verifisere om det finnes. Hvis det ikke finnes, kaster det en exception + personService.hentPersoninfo(vergeDto.ident, fagsystem) + } + } + } + + private fun tilDomene(vergeDto: VergeDto): Verge { + return Verge( + ident = vergeDto.ident, + orgNr = vergeDto.orgNr, + aktiv = true, + type = vergeDto.type, + navn = vergeDto.navn, + kilde = Applikasjon.FAMILIE_TILBAKE.name, + begrunnelse = vergeDto.begrunnelse, + ) + } + + private fun tilRespons(verge: Verge): VergeDto { + return VergeDto( + ident = verge.ident, + orgNr = verge.orgNr, + type = verge.type, + navn = verge.navn, + begrunnelse = verge.begrunnelse, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingBatch.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingBatch.kt new file mode 100644 index 000000000..4dab957cd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingBatch.kt @@ -0,0 +1,73 @@ +package no.nav.familie.tilbake.behandling.batch + +import no.nav.familie.leader.LeaderClient +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.PropertyName +import org.slf4j.LoggerFactory +import org.springframework.core.env.Environment +import org.springframework.data.domain.Pageable +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.Properties + +@Service +class AutomatiskGjenopptaBehandlingBatch( + private val fagsakRepository: FagsakRepository, + private val automatiskGjenopptaBehandlingService: AutomatiskGjenopptaBehandlingService, + private val taskService: TaskService, + private val environment: Environment, +) { + + private val logger = LoggerFactory.getLogger(this::class.java) + + @Scheduled(cron = "\${CRON_AUTOMATISK_GJENOPPTA}") + @Transactional + fun automatiskGjenopptaBehandling() { + if (LeaderClient.isLeader() != true && !environment.activeProfiles.any { + it.contains("local") || it.contains("integrasjonstest") + } + ) { + return + } + logger.info("Starter AutomatiskGjenopptaBehandlingBatch..") + logger.info("Henter alle behandlinger som kan gjenopptas automatisk.") + val behandlinger = automatiskGjenopptaBehandlingService.hentAlleBehandlingerKlarForGjenoppta() + + logger.info("Det finnes ${behandlinger.size} klar for automatisk gjenoppta") + + if (behandlinger.isNotEmpty()) { + val alleFeiledeTasker = taskService.finnTasksMedStatus( + listOf(Status.FEILET, Status.PLUKKET, Status.KLAR_TIL_PLUKK), + Pageable.unpaged(), + ) + behandlinger.forEach { + val finnesTask = alleFeiledeTasker.any { task -> + task.type == AutomatiskGjenopptaBehandlingTask.TYPE && task.payload == it.id.toString() + } + if (!finnesTask) { + val fagsystem = fagsakRepository.findByIdOrThrow(it.fagsakId).fagsystem + taskService.save( + Task( + type = AutomatiskGjenopptaBehandlingTask.TYPE, + payload = it.id.toString(), + properties = Properties().apply { + setProperty( + PropertyName.FAGSYSTEM, + fagsystem.name, + ) + }, + ), + ) + } else { + logger.info("Det finnes allerede en feilet AutomatiskGjenopptaBehandlingTask for behandlingId=${it.id}") + } + } + } + logger.info("Stopper AutomatiskGjenopptaBehandlingBatch..") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingService.kt new file mode 100644 index 000000000..abd1b8836 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingService.kt @@ -0,0 +1,79 @@ +package no.nav.familie.tilbake.behandling.batch + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.util.UUID + +@Service +class AutomatiskGjenopptaBehandlingService( + private val behandlingRepository: BehandlingRepository, + private val behandlingskontrollService: BehandlingskontrollService, + private val historikkTaskService: HistorikkTaskService, + private val stegService: StegService, + private val oppgaveTaskService: OppgaveTaskService, +) { + + private val logger = LoggerFactory.getLogger(this::class.java) + + fun hentAlleBehandlingerKlarForGjenoppta(): List { + return behandlingRepository.finnAlleBehandlingerKlarForGjenoppta(dagensdato = LocalDate.now()) + } + + @Transactional + fun gjenopptaBehandling(behandlingId: UUID) { + val behandlingsstegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingId) + ?: error("Behandling $behandlingId har ikke aktivt steg") + val tidsfrist = behandlingsstegstilstand.tidsfrist + ?: error("Behandling $behandlingId er på vent uten tidsfrist") + + historikkTaskService.lagHistorikkTask( + behandlingId, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_GJENOPPTATT, + Aktør.VEDTAKSLØSNING, + ) + stegService.gjenopptaSteg(behandlingId) + + val behandlingsnystegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingId) + if (behandlingsnystegstilstand?.behandlingssteg == Behandlingssteg.GRUNNLAG && + behandlingsnystegstilstand.behandlingsstegsstatus == Behandlingsstegstatus.VENTER + ) { + logger.warn( + "Behandling $behandlingId har ikke fått kravgrunnlag ennå " + + "eller mottok kravgrunnlag er sperret/avsluttet. " + + "Behandlingen bør analyseres og henlegges ved behov", + ) + } + + oppgaveTaskService.oppdaterOppgaveTask( + behandlingId = behandlingId, + beskrivelse = "Behandling er tatt av vent automatisk", + frist = tidsfrist, + saksbehandler = ContextService.hentSaksbehandler(), + ) + + // oppdaterer oppgave hvis saken er fortsatt på vent, + // f.eks saken var på vent med brukerstilbakemelding og har ikke fått kravgrunnlag + val aktivStegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingId) + if (aktivStegstilstand?.behandlingsstegsstatus == Behandlingsstegstatus.VENTER) { + oppgaveTaskService.oppdaterOppgaveTaskMedTriggertid( + behandlingId = behandlingId, + beskrivelse = aktivStegstilstand.venteårsak!!.beskrivelse, + frist = aktivStegstilstand.tidsfrist!!, + triggerTid = 2L, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingTask.kt new file mode 100644 index 000000000..8f1f54699 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingTask.kt @@ -0,0 +1,33 @@ +package no.nav.familie.tilbake.behandling.batch + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = AutomatiskGjenopptaBehandlingTask.TYPE, + beskrivelse = "gjenopptar behandling automatisk", + maxAntallFeil = 3, + triggerTidVedFeilISekunder = 60 * 5L, +) +class AutomatiskGjenopptaBehandlingTask( + private val automatiskGjenopptaBehandlingService: AutomatiskGjenopptaBehandlingService, +) : AsyncTaskStep { + + private val logger = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + logger.info("AutomatiskGjenopptaBehandlingTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val behandlingId = UUID.fromString(task.payload) + automatiskGjenopptaBehandlingService.gjenopptaBehandling(behandlingId) + } + + companion object { + + const val TYPE = "gjenoppta.behandling.automatisk" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingBatch.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingBatch.kt new file mode 100644 index 000000000..c4c3bf29a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingBatch.kt @@ -0,0 +1,78 @@ +package no.nav.familie.tilbake.behandling.batch + +import no.nav.familie.leader.LeaderClient +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.PropertyName +import org.slf4j.LoggerFactory +import org.springframework.core.env.Environment +import org.springframework.data.domain.Pageable +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.Properties + +@Service +class AutomatiskSaksbehandlingBatch( + private val automatiskSaksbehandlingService: AutomatiskSaksbehandlingService, + private val fagsakRepository: FagsakRepository, + private val taskService: TaskService, + private val environment: Environment, +) { + + private val logger = LoggerFactory.getLogger(this::class.java) + + @Scheduled(cron = "\${CRON_AUTOMATISK_SAKSBEHANDLING}") + @Transactional + fun behandleAutomatisk() { + if (LeaderClient.isLeader() != true && !environment.activeProfiles.any { + it.contains("local") || + it.contains("integrasjonstest") + } + ) { + return + } + logger.info("Starter AutomatiskSaksbehandlingBatch..") + + logger.info("Henter alle behandlinger som kan behandle automatisk.") + val behandlinger = automatiskSaksbehandlingService.hentAlleBehandlingerSomKanBehandleAutomatisk() + logger.info("Det finnes ${behandlinger.size} behandlinger som kan behandles automatisk") + + if (behandlinger.isNotEmpty()) { + val alleFeiledeTasker = taskService.finnTasksMedStatus( + listOf( + Status.FEILET, + Status.PLUKKET, + Status.KLAR_TIL_PLUKK, + ), + Pageable.unpaged(), + ) + behandlinger.forEach { + val finnesTask = alleFeiledeTasker.any { task -> + task.type == AutomatiskSaksbehandlingTask.TYPE && task.payload == it.id.toString() + } + if (!finnesTask) { + val fagsystem = fagsakRepository.findByIdOrThrow(it.fagsakId).fagsystem + taskService.save( + Task( + type = AutomatiskSaksbehandlingTask.TYPE, + payload = it.id.toString(), + properties = Properties().apply { + setProperty( + PropertyName.FAGSYSTEM, + fagsystem.name, + ) + }, + ), + ) + } else { + logger.info("Det finnes allerede en feilet AutomatiskSaksbehandlingTask for samme behandlingId=${it.id}") + } + } + } + logger.info("Stopper AutomatiskSaksbehandlingBatch..") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingService.kt new file mode 100644 index 000000000..ba4aa01d2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingService.kt @@ -0,0 +1,98 @@ +package no.nav.familie.tilbake.behandling.batch + +import no.nav.familie.kontrakter.felles.Regelverk +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Saksbehandlingstype +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingRepository +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.UUID + +@Service +class AutomatiskSaksbehandlingService( + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val brevsporingRepository: BrevsporingRepository, + private val stegService: StegService, + @Value("\${AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_BARNETRYGD}") + private val alderGrenseBarnetrygd: Long, + @Value("\${AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_BARNETILSYN}") + private val alderGrenseBarnetilsyn: Long, + @Value("\${AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_OVERGANGSSTØNAD}") + private val alderGrenseOvergangsstønad: Long, + @Value("\${AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_SKOLEPENGER}") + private val alderGrenseSkolepenger: Long, + @Value("\${AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_KONTANTSTØTTE}") + private val alderGrenseKontantstøtte: Long, +) { + + fun hentAlleBehandlingerSomKanBehandleAutomatisk(): List { + val behandlinger = + behandlingRepository.finnAlleBehandlingerKlarForSaksbehandling().filter { it.regelverk != Regelverk.EØS } + return behandlinger.filter { + val fagsak = fagsakRepository.findByIdOrThrow(it.fagsakId) + val bestemtDato = LocalDate.now().minusWeeks(ALDERSGRENSE_I_UKER.getValue(fagsak.ytelsestype)) + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(it.id) + val kontrollFelt = LocalDate.parse( + kravgrunnlag.kontrollfelt, + DateTimeFormatter.ofPattern("yyyy-MM-dd-HH.mm.ss.SSSSSS"), + ) + val sumNyttBeløp: BigDecimal = kravgrunnlag.perioder.sumOf { periode -> + periode.beløp.filter { beløp -> beløp.klassetype == Klassetype.FEIL } + .sumOf(Kravgrunnlagsbeløp433::nyttBeløp) + } + + kontrollFelt < bestemtDato && + sumNyttBeløp < Constants.MAKS_FEILUTBETALTBELØP_PER_YTELSE.getValue(fagsak.ytelsestype) && + // behandlinger som ikke sendte brev + !brevsporingRepository.existsByBehandlingId(it.id) + } + } + + @Transactional + fun oppdaterBehandling(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update( + behandling.copy( + saksbehandlingstype = Saksbehandlingstype + .AUTOMATISK_IKKE_INNKREVING_LAVT_BELØP, + ansvarligSaksbehandler = "VL", + ), + ) + } + + @Transactional + fun behandleAutomatisk(behandlingId: UUID) { + stegService.håndterStegAutomatisk(behandlingId) + } + + private val ALDERSGRENSE_I_UKER = mapOf( + Ytelsestype.BARNETRYGD to alderGrenseBarnetrygd, + Ytelsestype.BARNETILSYN to alderGrenseBarnetilsyn, + Ytelsestype.OVERGANGSSTØNAD to alderGrenseOvergangsstønad, + Ytelsestype.SKOLEPENGER to alderGrenseSkolepenger, + Ytelsestype.KONTANTSTØTTE to alderGrenseKontantstøtte, + ) +} + +fun main() { + val dato = LocalDate.parse( + "2022-02-10-18.43.15.192503", + DateTimeFormatter.ofPattern("yyyy-MM-dd-HH.mm.ss.SSSSSS"), + ) + print(dato < LocalDate.now()) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingTask.kt new file mode 100644 index 000000000..145d532ee --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingTask.kt @@ -0,0 +1,33 @@ +package no.nav.familie.tilbake.behandling.batch + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = AutomatiskSaksbehandlingTask.TYPE, + beskrivelse = "behandler behandling automatisk", + maxAntallFeil = 3, + triggerTidVedFeilISekunder = 60 * 5L, +) +class AutomatiskSaksbehandlingTask(private val automatiskSaksbehandlingService: AutomatiskSaksbehandlingService) : AsyncTaskStep { + + private val logger = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + logger.info("AutomatiskSaksbehandlingTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val behandlingId = UUID.fromString(task.payload) + automatiskSaksbehandlingService.oppdaterBehandling(behandlingId) + + automatiskSaksbehandlingService.behandleAutomatisk(behandlingId) + } + + companion object { + + const val TYPE = "saksbehandle.automatisk" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/consumer/HentFagsystemsbehandlingResponsConsumer.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/consumer/HentFagsystemsbehandlingResponsConsumer.kt new file mode 100644 index 000000000..902f69362 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/consumer/HentFagsystemsbehandlingResponsConsumer.kt @@ -0,0 +1,38 @@ +package no.nav.familie.tilbake.behandling.consumer + +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingService +import no.nav.familie.tilbake.config.KafkaConfig +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.Profile +import org.springframework.kafka.annotation.KafkaListener +import org.springframework.kafka.support.Acknowledgment +import org.springframework.stereotype.Service +import java.util.UUID +import java.util.concurrent.CountDownLatch + +@Service +@Profile("!integrasjonstest & !e2e") +class HentFagsystemsbehandlingResponsConsumer(private val fagsystemsbehandlingService: HentFagsystemsbehandlingService) { + + private val logger = LoggerFactory.getLogger(this::class.java) + private val secureLogger = LoggerFactory.getLogger("secureLogger") + + var latch: CountDownLatch = CountDownLatch(1) + + @KafkaListener( + id = "familie-tilbake", + topics = [KafkaConfig.HENT_FAGSYSTEMSBEHANDLING_RESPONS_TOPIC], + containerFactory = "concurrentKafkaListenerContainerFactory", + ) + fun listen(consumerRecord: ConsumerRecord, ack: Acknowledgment) { + logger.info("Fagsystemsbehandlingsdata er mottatt i kafka med key=${consumerRecord.key()}") + secureLogger.info("Fagsystemsbehandlingsdata er mottatt i kafka $consumerRecord") + + val requestId = UUID.fromString(consumerRecord.key()) + val data: String = consumerRecord.value() + fagsystemsbehandlingService.lagreHentFagsystemsbehandlingRespons(requestId, data) + latch.countDown() + ack.acknowledge() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Behandling.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Behandling.kt new file mode 100644 index 000000000..ed1dbf7fe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Behandling.kt @@ -0,0 +1,201 @@ +package no.nav.familie.tilbake.behandling.domain + +import no.nav.familie.kontrakter.felles.Regelverk +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Vergetype +import no.nav.familie.tilbake.common.repository.Sporbar +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Embedded +import org.springframework.data.relational.core.mapping.MappedCollection +import org.springframework.data.relational.core.mapping.Table +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.UUID + +data class Behandling( + @Id + val id: UUID = UUID.randomUUID(), + val fagsakId: UUID, + val status: Behandlingsstatus = Behandlingsstatus.OPPRETTET, + val type: Behandlingstype, + val saksbehandlingstype: Saksbehandlingstype = Saksbehandlingstype.ORDINÆR, + val opprettetDato: LocalDate = LocalDate.now(), + val avsluttetDato: LocalDate? = null, + val ansvarligSaksbehandler: String, + val ansvarligBeslutter: String? = null, + val behandlendeEnhet: String, + val behandlendeEnhetsNavn: String, + val manueltOpprettet: Boolean, + val eksternBrukId: UUID = UUID.randomUUID(), + @MappedCollection(idColumn = "behandling_id") + val fagsystemsbehandling: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val varsler: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val verger: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val resultater: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val årsaker: Set = setOf(), + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), + val regelverk: Regelverk? = null, +) { + + val erAvsluttet get() = Behandlingsstatus.AVSLUTTET == status + + val erUnderIverksettelse get() = Behandlingsstatus.IVERKSETTER_VEDTAK == status + + val erSaksbehandlingAvsluttet get() = erAvsluttet || erUnderIverksettelse + + val aktivVerge get() = verger.firstOrNull { it.aktiv } + + val aktivtVarsel get() = varsler.firstOrNull { it.aktiv } + + val aktivFagsystemsbehandling get() = fagsystemsbehandling.first { it.aktiv } + + val harVerge get() = verger.any { it.aktiv } + + val sisteResultat get() = resultater.maxByOrNull { it.sporbar.endret.endretTid } + + val sisteÅrsak get() = årsaker.firstOrNull() + + val erRevurdering get() = type == Behandlingstype.REVURDERING_TILBAKEKREVING + + val opprettetTidspunkt: LocalDateTime + get() = sporbar.opprettetTid + + val endretTidspunkt: LocalDateTime + get() = sporbar.endret.endretTid + + fun utledVedtaksbrevstype(): Vedtaksbrevstype { + return if (erTilbakekrevingRevurderingHarÅrsakFeilutbetalingBortfalt()) { + Vedtaksbrevstype.FRITEKST_FEILUTBETALING_BORTFALT + } else { + Vedtaksbrevstype.ORDINÆR + } + } + + private fun erTilbakekrevingRevurderingHarÅrsakFeilutbetalingBortfalt(): Boolean { + return Behandlingstype.REVURDERING_TILBAKEKREVING == this.type && + this.årsaker.any { + Behandlingsårsakstype.REVURDERING_FEILUTBETALT_BELØP_HELT_ELLER_DELVIS_BORTFALT == it.type + } + } +} + +data class Fagsystemsbehandling( + @Id + val id: UUID = UUID.randomUUID(), + val eksternId: String, + val aktiv: Boolean = true, + val tilbakekrevingsvalg: Tilbakekrevingsvalg? = null, + val resultat: String, + @Column("arsak") + val årsak: String, + val revurderingsvedtaksdato: LocalDate, + @MappedCollection(idColumn = "fagsystemsbehandling_id") + val konsekvenser: Set = setOf(), + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +@Table("fagsystemskonsekvens") +data class Fagsystemskonsekvens( + @Id + val id: UUID = UUID.randomUUID(), + val konsekvens: String, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +data class Varsel( + @Id + val id: UUID = UUID.randomUUID(), + val varseltekst: String, + @Column("varselbelop") + val varselbeløp: Long, + @MappedCollection(idColumn = "varsel_id") + val perioder: Set = setOf(), + val aktiv: Boolean = true, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +data class Varselsperiode( + @Id + val id: UUID = UUID.randomUUID(), + val fom: LocalDate, + val tom: LocalDate, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +data class Verge( + @Id + val id: UUID = UUID.randomUUID(), + val ident: String? = null, + val orgNr: String? = null, + val aktiv: Boolean = true, + val type: Vergetype, + val navn: String, + val kilde: String, + val begrunnelse: String? = "", + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +@Table("behandlingsarsak") +data class Behandlingsårsak( + @Id + val id: UUID = UUID.randomUUID(), + val originalBehandlingId: UUID?, + val type: Behandlingsårsakstype, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +enum class Behandlingsårsakstype(val navn: String) { + REVURDERING_KLAGE_NFP("Revurdering NFP omgjør vedtak basert på klage"), + REVURDERING_KLAGE_KA("Revurdering etter KA-behandlet klage"), + REVURDERING_OPPLYSNINGER_OM_VILKÅR("Nye opplysninger om vilkårsvurdering"), + REVURDERING_OPPLYSNINGER_OM_FORELDELSE("Nye opplysninger om foreldelse"), + REVURDERING_FEILUTBETALT_BELØP_HELT_ELLER_DELVIS_BORTFALT("Feilutbetalt beløp helt eller delvis bortfalt"), +} + +enum class Behandlingsstatus(val kode: String) { + + AVSLUTTET("AVSLU"), + FATTER_VEDTAK("FVED"), + IVERKSETTER_VEDTAK("IVED"), + OPPRETTET("OPPRE"), + UTREDES("UTRED"), +} + +enum class Behandlingstype { + + TILBAKEKREVING, + REVURDERING_TILBAKEKREVING, +} + +enum class Saksbehandlingstype { + ORDINÆR, + AUTOMATISK_IKKE_INNKREVING_LAVT_BELØP, +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Behandlingsresultat.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Behandlingsresultat.kt new file mode 100644 index 000000000..05ef03a82 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Behandlingsresultat.kt @@ -0,0 +1,84 @@ +package no.nav.familie.tilbake.behandling.domain + +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import org.springframework.data.relational.core.mapping.MappedCollection +import java.time.LocalDate +import java.util.UUID + +data class Behandlingsresultat( + @Id + val id: UUID = UUID.randomUUID(), + val type: Behandlingsresultatstype = Behandlingsresultatstype.IKKE_FASTSATT, + @MappedCollection(idColumn = "behandlingsresultat_id") + val behandlingsvedtak: Behandlingsvedtak? = null, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) { + + companion object { + + val ALLE_HENLEGGELSESKODER: Set = + setOf( + Behandlingsresultatstype.HENLAGT_KRAVGRUNNLAG_NULLSTILT, + Behandlingsresultatstype.HENLAGT_FEILOPPRETTET, + Behandlingsresultatstype.HENLAGT_TEKNISK_VEDLIKEHOLD, + Behandlingsresultatstype.HENLAGT_FEILOPPRETTET_MED_BREV, + Behandlingsresultatstype.HENLAGT_FEILOPPRETTET_UTEN_BREV, + ) + + val ALLE_FASTSATTKODER = setOf( + Behandlingsresultatstype.INGEN_TILBAKEBETALING, + Behandlingsresultatstype.DELVIS_TILBAKEBETALING, + Behandlingsresultatstype.FULL_TILBAKEBETALING, + ) + } + + fun erBehandlingHenlagt(): Boolean { + return ALLE_HENLEGGELSESKODER.contains(type) + } + + fun erBehandlingFastsatt(): Boolean = ALLE_FASTSATTKODER.contains(type) + + fun resultatstypeTilFrontend(): Behandlingsresultatstype { + if (erBehandlingHenlagt()) { + return Behandlingsresultatstype.HENLAGT + } + return type + } +} + +data class Behandlingsvedtak( + @Id + val id: UUID = UUID.randomUUID(), + val vedtaksdato: LocalDate, + val iverksettingsstatus: Iverksettingsstatus = Iverksettingsstatus.IKKE_IVERKSATT, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +enum class Iverksettingsstatus { + IKKE_IVERKSATT, + UNDER_IVERKSETTING, + IVERKSATT, +} + +enum class Behandlingsresultatstype(val navn: String) { + IKKE_FASTSATT("Ikke fastsatt"), + HENLAGT_FEILOPPRETTET("Henlagt, søknaden er feilopprettet"), + HENLAGT_FEILOPPRETTET_MED_BREV("Feilaktig opprettet - med henleggelsesbrev"), + HENLAGT_FEILOPPRETTET_UTEN_BREV("Feilaktig opprettet - uten henleggelsesbrev"), + HENLAGT_KRAVGRUNNLAG_NULLSTILT("Kravgrunnlaget er nullstilt"), + HENLAGT_TEKNISK_VEDLIKEHOLD("Teknisk vedlikehold"), + HENLAGT("Henlagt"), // kun brukes i frontend + + INGEN_TILBAKEBETALING("Ingen tilbakebetaling"), + DELVIS_TILBAKEBETALING("Delvis tilbakebetaling"), + FULL_TILBAKEBETALING("Full tilbakebetaling"), +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Bruker.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Bruker.kt new file mode 100644 index 000000000..c3510845c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Bruker.kt @@ -0,0 +1,10 @@ +package no.nav.familie.tilbake.behandling.domain + +import no.nav.familie.kontrakter.felles.Språkkode +import org.springframework.data.relational.core.mapping.Column + +data class Bruker( + val ident: String, + @Column("sprakkode") + val språkkode: Språkkode = Språkkode.NB, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Fagsak.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Fagsak.kt new file mode 100644 index 000000000..7cb19c0c4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Fagsak.kt @@ -0,0 +1,30 @@ +package no.nav.familie.tilbake.behandling.domain + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import java.util.UUID + +data class Fagsak( + @Id + val id: UUID = UUID.randomUUID(), + @Embedded(prefix = "bruker_", onEmpty = Embedded.OnEmpty.USE_EMPTY) + val bruker: Bruker, + val eksternFagsakId: String, + val fagsystem: Fagsystem, + val ytelsestype: Ytelsestype, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), + @Embedded(prefix = "institusjon_", onEmpty = Embedded.OnEmpty.USE_NULL) + val institusjon: Institusjon? = null, +) { + + val ytelsesnavn + get() = ytelsestype.navn[bruker.språkkode] + ?: throw IllegalStateException("Programmeringsfeil: Språkkode lagt til uten støtte") +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/HentFagsystemsbehandlingRequestSendt.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/HentFagsystemsbehandlingRequestSendt.kt new file mode 100644 index 000000000..0ac5f2334 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/HentFagsystemsbehandlingRequestSendt.kt @@ -0,0 +1,21 @@ +package no.nav.familie.tilbake.behandling.domain + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import java.util.UUID + +data class HentFagsystemsbehandlingRequestSendt( + @Id + val id: UUID = UUID.randomUUID(), + val eksternFagsakId: String, + val ytelsestype: Ytelsestype, + val eksternId: String, + val respons: String? = null, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Institusjon.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Institusjon.kt new file mode 100644 index 000000000..fbf0466ab --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/domain/Institusjon.kt @@ -0,0 +1,5 @@ +package no.nav.familie.tilbake.behandling.domain + +data class Institusjon( + val organisasjonsnummer: String, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/event/EndretPersonIdentEvent.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/event/EndretPersonIdentEvent.kt new file mode 100644 index 000000000..d66b89460 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/event/EndretPersonIdentEvent.kt @@ -0,0 +1,6 @@ +package no.nav.familie.tilbake.behandling.event + +import org.springframework.context.ApplicationEvent +import java.util.UUID + +class EndretPersonIdentEvent(source: Any, val fagsakId: UUID) : ApplicationEvent(source) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/event/EndretPersonIdentEventPublisher.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/event/EndretPersonIdentEventPublisher.kt new file mode 100644 index 000000000..c61e2fc60 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/event/EndretPersonIdentEventPublisher.kt @@ -0,0 +1,14 @@ +package no.nav.familie.tilbake.behandling.event + +import org.springframework.context.ApplicationEventPublisher +import org.springframework.stereotype.Component +import java.util.UUID + +@Component +class EndretPersonIdentEventPublisher(val applicationEventPublisher: ApplicationEventPublisher) { + + fun fireEvent(nyIdent: String, fagsakId: UUID) { + val endretPersonIdentEvent = EndretPersonIdentEvent(nyIdent, fagsakId) + applicationEventPublisher.publishEvent(endretPersonIdentEvent) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Brevmottakersteg.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Brevmottakersteg.kt new file mode 100644 index 000000000..bed46073d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Brevmottakersteg.kt @@ -0,0 +1,63 @@ +package no.nav.familie.tilbake.behandling.steg + +import no.nav.familie.tilbake.api.dto.BehandlingsstegDto +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.AUTOUTFØRT +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.UTFØRT +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class Brevmottakersteg( + private val behandlingskontrollService: BehandlingskontrollService, + private val oppgaveTaskService: OppgaveTaskService, +) : IBehandlingssteg { + + private val logger = LoggerFactory.getLogger(this::class.java) + + @Transactional + override fun utførSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.BREVMOTTAKER} steg") + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.BREVMOTTAKER, + AUTOUTFØRT, + ), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + @Transactional + override fun utførSteg(behandlingId: UUID, behandlingsstegDto: BehandlingsstegDto) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.BREVMOTTAKER} steg") + oppgaveTaskService.oppdaterAnsvarligSaksbehandlerOppgaveTask(behandlingId) + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo(Behandlingssteg.BREVMOTTAKER, UTFØRT), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + @Transactional + override fun gjenopptaSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId gjenopptar på ${Behandlingssteg.BREVMOTTAKER} steg") + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.BREVMOTTAKER, + Behandlingsstegstatus.KLAR, + ), + ) + } + + override fun getBehandlingssteg(): Behandlingssteg { + return Behandlingssteg.BREVMOTTAKER + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/FaktaFeilutbetalingssteg.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/FaktaFeilutbetalingssteg.kt new file mode 100644 index 000000000..fb0d689a7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/FaktaFeilutbetalingssteg.kt @@ -0,0 +1,100 @@ +package no.nav.familie.tilbake.behandling.steg + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.tilbake.api.dto.BehandlingsstegDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegFaktaDto +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingService +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.event.EndretKravgrunnlagEvent +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import org.slf4j.LoggerFactory +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class FaktaFeilutbetalingssteg( + private val behandlingskontrollService: BehandlingskontrollService, + private val faktaFeilutbetalingService: FaktaFeilutbetalingService, + private val historikkTaskService: HistorikkTaskService, + private val oppgaveTaskService: OppgaveTaskService, +) : IBehandlingssteg { + + private val logger = LoggerFactory.getLogger(this::class.java) + + override fun utførSteg(behandlingId: UUID) { + // Denne metoden gjør ingenting. Det skrives bare for å unngå feilen når ENDR kravgrunnlag mottas + } + + @Transactional + override fun utførSteg(behandlingId: UUID, behandlingsstegDto: BehandlingsstegDto) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.FAKTA} steg") + val behandlingsstegFaktaDto: BehandlingsstegFaktaDto = behandlingsstegDto as BehandlingsstegFaktaDto + + faktaFeilutbetalingService.lagreFaktaomfeilutbetaling(behandlingId, behandlingsstegFaktaDto) + + oppgaveTaskService.oppdaterAnsvarligSaksbehandlerOppgaveTask(behandlingId) + + historikkTaskService.lagHistorikkTask( + behandlingId, + TilbakekrevingHistorikkinnslagstype.FAKTA_VURDERT, + Aktør.SAKSBEHANDLER, + ) + + if (faktaFeilutbetalingService.hentAktivFaktaOmFeilutbetaling(behandlingId) != null) { + flyttBehandlingVidere(behandlingId) + } + } + + @Transactional + override fun utførStegAutomatisk(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.FAKTA} steg og behandler automatisk..") + faktaFeilutbetalingService.lagreFastFaktaForAutomatiskSaksbehandling(behandlingId) + + historikkTaskService.lagHistorikkTask( + behandlingId, + TilbakekrevingHistorikkinnslagstype.FAKTA_VURDERT, + Aktør.VEDTAKSLØSNING, + ) + + flyttBehandlingVidere(behandlingId) + } + + @Transactional + override fun gjenopptaSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId gjenopptar på ${Behandlingssteg.FAKTA} steg") + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.FAKTA, + Behandlingsstegstatus.KLAR, + ), + ) + } + + override fun getBehandlingssteg(): Behandlingssteg { + return Behandlingssteg.FAKTA + } + + @EventListener + fun deaktiverEksisterendeFaktaOmFeilutbetaling(endretKravgrunnlagEvent: EndretKravgrunnlagEvent) { + faktaFeilutbetalingService.deaktiverEksisterendeFaktaOmFeilutbetaling(behandlingId = endretKravgrunnlagEvent.behandlingId) + } + + private fun flyttBehandlingVidere(behandlingId: UUID) { + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.FAKTA, + Behandlingsstegstatus.UTFØRT, + ), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Fattevedtakssteg.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Fattevedtakssteg.kt new file mode 100644 index 000000000..a1df20b43 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Fattevedtakssteg.kt @@ -0,0 +1,127 @@ +package no.nav.familie.tilbake.behandling.steg + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.tilbake.api.dto.BehandlingsstegDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegFatteVedtaksstegDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.BehandlingsvedtakService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import no.nav.familie.tilbake.totrinn.TotrinnService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class Fattevedtakssteg( + private val behandlingskontrollService: BehandlingskontrollService, + private val behandlingRepository: BehandlingRepository, + private val totrinnService: TotrinnService, + private val oppgaveTaskService: OppgaveTaskService, + private val historikkTaskService: HistorikkTaskService, + private val behandlingsvedtakService: BehandlingsvedtakService, +) : IBehandlingssteg { + + private val logger = LoggerFactory.getLogger(this::class.java) + + override fun utførSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.FATTE_VEDTAK} steg") + } + + @Transactional + override fun utførSteg(behandlingId: UUID, behandlingsstegDto: BehandlingsstegDto) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.FATTE_VEDTAK} steg") + // step1: oppdater ansvarligBeslutter + totrinnService.validerAnsvarligBeslutter(behandlingId) + totrinnService.oppdaterAnsvarligBeslutter(behandlingId) + + // step2: lagre totrinnsvurderinger + val fatteVedtaksstegDto = behandlingsstegDto as BehandlingsstegFatteVedtaksstegDto + totrinnService.lagreTotrinnsvurderinger(behandlingId, fatteVedtaksstegDto.totrinnsvurderinger) + + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + // step3: lukk Godkjenne vedtak oppgaver + oppgaveTaskService.ferdigstilleOppgaveTask(behandlingId = behandlingId, oppgavetype = Oppgavetype.GodkjenneVedtak.name) + + // step4: flytter behandling tilbake til Foreslå Vedtak om beslutter underkjente noen steg + val finnesUnderkjenteSteg = fatteVedtaksstegDto.totrinnsvurderinger.any { !it.godkjent } + if (finnesUnderkjenteSteg) { + behandlingskontrollService.behandleStegPåNytt(behandlingId, Behandlingssteg.FORESLÅ_VEDTAK) + + historikkTaskService.lagHistorikkTask( + behandlingId, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_SENDT_TILBAKE_TIL_SAKSBEHANDLER, + Aktør.BESLUTTER, + beslutter = behandling.ansvarligBeslutter, + ) + totrinnService.fjernAnsvarligBeslutter(behandlingId) + oppgaveTaskService.opprettOppgaveTask( + behandling, + Oppgavetype.BehandleUnderkjentVedtak, + behandling.ansvarligSaksbehandler, + ) + } else { + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.FATTE_VEDTAK, + Behandlingsstegstatus.UTFØRT, + ), + ) + historikkTaskService.lagHistorikkTask( + behandlingId, + TilbakekrevingHistorikkinnslagstype.VEDTAK_FATTET, + Aktør.BESLUTTER, + ) + // step 5: opprett behandlingsvedtak og oppdater behandlingsresultat + behandlingsvedtakService.opprettBehandlingsvedtak(behandlingId) + } + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + @Transactional + override fun utførStegAutomatisk(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.FATTE_VEDTAK} steg og behandler automatisk..") + totrinnService.oppdaterAnsvarligBeslutter(behandlingId) + totrinnService.lagreFastTotrinnsvurderingerForAutomatiskSaksbehandling(behandlingId) + + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.FATTE_VEDTAK, + Behandlingsstegstatus.UTFØRT, + ), + ) + historikkTaskService.lagHistorikkTask( + behandlingId, + TilbakekrevingHistorikkinnslagstype.VEDTAK_FATTET, + Aktør.BESLUTTER, + ) + behandlingsvedtakService.opprettBehandlingsvedtak(behandlingId) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + @Transactional + override fun gjenopptaSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId gjenopptar på ${Behandlingssteg.FATTE_VEDTAK} steg") + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.FATTE_VEDTAK, + Behandlingsstegstatus.KLAR, + ), + ) + } + + override fun getBehandlingssteg(): Behandlingssteg { + return Behandlingssteg.FATTE_VEDTAK + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Foreldelsessteg.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Foreldelsessteg.kt new file mode 100644 index 000000000..4a4f8cf78 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Foreldelsessteg.kt @@ -0,0 +1,122 @@ +package no.nav.familie.tilbake.behandling.steg + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.tilbake.api.dto.BehandlingsstegDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegForeldelseDto +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.foreldelse.ForeldelseService +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.event.EndretKravgrunnlagEvent +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.util.UUID + +@Service +class Foreldelsessteg( + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val behandlingskontrollService: BehandlingskontrollService, + private val foreldelseService: ForeldelseService, + private val historikkTaskService: HistorikkTaskService, + @Value("\${FORELDELSE_ANTALL_MÅNED:30}") + private val foreldelseAntallMåned: Long, + private val oppgaveTaskService: OppgaveTaskService, +) : IBehandlingssteg { + + private val logger = LoggerFactory.getLogger(this::class.java) + + @Transactional + override fun utførSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.FORELDELSE} steg") + if (!harGrunnlagForeldetPeriode(behandlingId)) { + lagHistorikkinnslag(behandlingId, Aktør.VEDTAKSLØSNING) + + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.FORELDELSE, + Behandlingsstegstatus.AUTOUTFØRT, + ), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + } + + @Transactional + override fun utførSteg(behandlingId: UUID, behandlingsstegDto: BehandlingsstegDto) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.FORELDELSE} steg") + foreldelseService.lagreVurdertForeldelse(behandlingId, (behandlingsstegDto as BehandlingsstegForeldelseDto)) + + oppgaveTaskService.oppdaterAnsvarligSaksbehandlerOppgaveTask(behandlingId) + + lagHistorikkinnslag(behandlingId, Aktør.SAKSBEHANDLER) + + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.FORELDELSE, + Behandlingsstegstatus.UTFØRT, + ), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + @Transactional + override fun utførStegAutomatisk(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.FORELDELSE} steg og behandler automatisk..") + if (!harGrunnlagForeldetPeriode(behandlingId)) { + utførSteg(behandlingId) + return + } + foreldelseService.lagreFastForeldelseForAutomatiskSaksbehandling(behandlingId) + lagHistorikkinnslag(behandlingId, Aktør.VEDTAKSLØSNING) + + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.FORELDELSE, + Behandlingsstegstatus.UTFØRT, + ), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + @Transactional + override fun gjenopptaSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId gjenopptar på ${Behandlingssteg.FORELDELSE} steg") + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.FORELDELSE, + Behandlingsstegstatus.KLAR, + ), + ) + } + + private fun harGrunnlagForeldetPeriode(behandlingId: UUID): Boolean { + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + return kravgrunnlag.perioder.any { it.periode.fom.atDay(1) < LocalDate.now().minusMonths(foreldelseAntallMåned) } + } + + private fun lagHistorikkinnslag(behandlingId: UUID, aktør: Aktør) { + historikkTaskService.lagHistorikkTask(behandlingId, TilbakekrevingHistorikkinnslagstype.FORELDELSE_VURDERT, aktør) + } + + override fun getBehandlingssteg(): Behandlingssteg { + return Behandlingssteg.FORELDELSE + } + + @EventListener + fun deaktiverEksisterendeVurdertForeldelse(endretKravgrunnlagEvent: EndretKravgrunnlagEvent) { + foreldelseService.deaktiverEksisterendeVurdertForeldelse(behandlingId = endretKravgrunnlagEvent.behandlingId) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Foresl\303\245vedtakssteg.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Foresl\303\245vedtakssteg.kt" new file mode 100644 index 000000000..0c0f18e94 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Foresl\303\245vedtakssteg.kt" @@ -0,0 +1,143 @@ +package no.nav.familie.tilbake.behandling.steg + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.tilbake.api.dto.BehandlingsstegDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegForeslåVedtaksstegDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.domain.Saksbehandlingstype +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.dokumentbestilling.vedtak.VedtaksbrevService +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.event.EndretKravgrunnlagEvent +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import no.nav.familie.tilbake.totrinn.TotrinnService +import org.slf4j.LoggerFactory +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.util.UUID + +@Service +class Foreslåvedtakssteg( + private val behandlingRepository: BehandlingRepository, + private val behandlingskontrollService: BehandlingskontrollService, + private val vedtaksbrevService: VedtaksbrevService, + private val oppgaveTaskService: OppgaveTaskService, + private val totrinnService: TotrinnService, + private val historikkTaskService: HistorikkTaskService, +) : IBehandlingssteg { + + private val logger = LoggerFactory.getLogger(this::class.java) + + @Transactional + override fun utførSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.FORESLÅ_VEDTAK} steg") + flyttBehandlingVidere(behandlingId) + } + + @Transactional + override fun utførSteg(behandlingId: UUID, behandlingsstegDto: BehandlingsstegDto) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.FORESLÅ_VEDTAK} steg") + val foreslåvedtaksstegDto = behandlingsstegDto as BehandlingsstegForeslåVedtaksstegDto + vedtaksbrevService.lagreFriteksterFraSaksbehandler(behandlingId, foreslåvedtaksstegDto.fritekstavsnitt) + + historikkTaskService.lagHistorikkTask( + behandlingId, + TilbakekrevingHistorikkinnslagstype.FORESLÅ_VEDTAK_VURDERT, + Aktør.SAKSBEHANDLER, + ) + + flyttBehandlingVidere(behandlingId) + + // lukker BehandleSak oppgave og oppretter GodkjenneVedtak oppgave + håndterOppgave(behandlingId) + + historikkTaskService.lagHistorikkTask( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype + .BEHANDLING_SENDT_TIL_BESLUTTER, + aktør = Aktør.SAKSBEHANDLER, + triggerTid = LocalDateTime.now().plusSeconds(2), + ) + } + + @Transactional + override fun utførStegAutomatisk(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.FORESLÅ_VEDTAK} steg og behandler automatisk..") + historikkTaskService.lagHistorikkTask( + behandlingId, + TilbakekrevingHistorikkinnslagstype.FORESLÅ_VEDTAK_VURDERT, + Aktør.VEDTAKSLØSNING, + ) + flyttBehandlingVidere(behandlingId) + + // lukker BehandleSak oppgave og oppretter GodkjenneVedtak oppgave + håndterOppgave(behandlingId) + + historikkTaskService.lagHistorikkTask( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype + .BEHANDLING_SENDT_TIL_BESLUTTER, + aktør = Aktør.VEDTAKSLØSNING, + triggerTid = LocalDateTime.now().plusSeconds(2), + ) + } + + @Transactional + override fun gjenopptaSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId gjenopptar på ${Behandlingssteg.FORESLÅ_VEDTAK} steg") + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.FORESLÅ_VEDTAK, + Behandlingsstegstatus.KLAR, + ), + ) + } + + @EventListener + fun deaktiverEksisterendeVilkårsvurdering(endretKravgrunnlagEvent: EndretKravgrunnlagEvent) { + vedtaksbrevService.deaktiverEksisterendeVedtaksbrevdata(endretKravgrunnlagEvent.behandlingId) + } + + private fun flyttBehandlingVidere(behandlingId: UUID) { + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.FORESLÅ_VEDTAK, + Behandlingsstegstatus.UTFØRT, + ), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + private fun håndterOppgave(behandlingId: UUID) { + val finnesUnderkjenteSteg = totrinnService.finnesUnderkjenteStegITotrinnsvurdering(behandlingId) + var oppgavetype = Oppgavetype.BehandleSak + if (finnesUnderkjenteSteg) { + oppgavetype = Oppgavetype.BehandleUnderkjentVedtak + } + oppgaveTaskService.ferdigstilleOppgaveTask(behandlingId = behandlingId, oppgavetype = oppgavetype.name) + + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + if (behandling.saksbehandlingstype == Saksbehandlingstype.ORDINÆR) { + oppgaveTaskService.opprettOppgaveTask( + behandling = behandling, + oppgavetype = Oppgavetype.GodkjenneVedtak, + opprettetAv = ContextService.hentSaksbehandlerNavn(), + ) + } + } + + override fun getBehandlingssteg(): Behandlingssteg { + return Behandlingssteg.FORESLÅ_VEDTAK + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Grunnlagssteg.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Grunnlagssteg.kt new file mode 100644 index 000000000..82a1822dc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Grunnlagssteg.kt @@ -0,0 +1,56 @@ +package no.nav.familie.tilbake.behandling.steg + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.util.UUID + +@Service +class Grunnlagssteg( + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val behandlingskontrollService: BehandlingskontrollService, + private val historikkTaskService: HistorikkTaskService, +) : IBehandlingssteg { + + private val logger = LoggerFactory.getLogger(this::class.java) + + @Transactional + override fun utførSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.GRUNNLAG} steg") + if (kravgrunnlagRepository.existsByBehandlingIdAndAktivTrueAndSperretFalse(behandlingId)) { + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.GRUNNLAG, + Behandlingsstegstatus.UTFØRT, + ), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + historikkTaskService.lagHistorikkTask( + behandlingId, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_GJENOPPTATT, + Aktør.VEDTAKSLØSNING, + triggerTid = LocalDateTime.now().plusSeconds(2), + ) + } + } + + @Transactional + override fun gjenopptaSteg(behandlingId: UUID) { + utførSteg(behandlingId) + logger.info("Behandling $behandlingId gjenopptar på ${Behandlingssteg.GRUNNLAG} steg") + } + + override fun getBehandlingssteg(): Behandlingssteg { + return Behandlingssteg.GRUNNLAG + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/IBehandlingssteg.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/IBehandlingssteg.kt new file mode 100644 index 000000000..84c3aa893 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/IBehandlingssteg.kt @@ -0,0 +1,27 @@ +package no.nav.familie.tilbake.behandling.steg + +import no.nav.familie.tilbake.api.dto.BehandlingsstegDto +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import java.util.UUID + +interface IBehandlingssteg { + + fun utførSteg(behandlingId: UUID) { + throw Feil(message = "Implementasjon mangler, er i default method implementasjon for $behandlingId") + } + + fun utførSteg(behandlingId: UUID, behandlingsstegDto: BehandlingsstegDto) { + throw Feil(message = "Implementasjon mangler, er i default method implementasjon for $behandlingId") + } + + fun utførStegAutomatisk(behandlingId: UUID) { + throw Feil(message = "Implementasjon mangler, er i default method implementasjon for $behandlingId") + } + + fun gjenopptaSteg(behandlingId: UUID) { + throw Feil(message = "Implementasjon mangler, er i default method implementasjon for $behandlingId") + } + + fun getBehandlingssteg(): Behandlingssteg +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/IverksettVedtakssteg.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/IverksettVedtakssteg.kt new file mode 100644 index 000000000..a1704debe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/IverksettVedtakssteg.kt @@ -0,0 +1,53 @@ +package no.nav.familie.tilbake.behandling.steg + +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.BehandlingsvedtakService +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Iverksettingsstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.iverksettvedtak.task.SendØkonomiTilbakekrevingsvedtakTask +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.Properties +import java.util.UUID + +@Service +class IverksettVedtakssteg( + private val behandlingsvedtakService: BehandlingsvedtakService, + private val fagsakRepository: FagsakRepository, + private val taskService: TaskService, +) : IBehandlingssteg { + + private val logger = LoggerFactory.getLogger(this::class.java) + + override fun utførSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.IVERKSETT_VEDTAK} steg") + + val behandling = behandlingsvedtakService.oppdaterBehandlingsvedtak(behandlingId, Iverksettingsstatus.UNDER_IVERKSETTING) + val fagsystem = fagsakRepository.findByIdOrThrow(behandling.fagsakId).fagsystem + val properties = Properties().apply { + setProperty("ansvarligSaksbehandler", ContextService.hentSaksbehandler()) + setProperty(PropertyName.FAGSYSTEM, fagsystem.name) + } + taskService.save( + Task( + type = SendØkonomiTilbakekrevingsvedtakTask.TYPE, + payload = behandlingId.toString(), + properties = properties, + ), + ) + } + + override fun utførStegAutomatisk(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.IVERKSETT_VEDTAK} steg og behandler automatisk..") + utførSteg(behandlingId) + } + + override fun getBehandlingssteg(): Behandlingssteg { + return Behandlingssteg.IVERKSETT_VEDTAK + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/StegService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/StegService.kt new file mode 100644 index 000000000..3b49d3941 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/StegService.kt @@ -0,0 +1,168 @@ +package no.nav.familie.tilbake.behandling.steg + +import no.nav.familie.kontrakter.felles.Regelverk +import no.nav.familie.tilbake.api.dto.BehandlingsstegDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegFatteVedtaksstegDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.ValiderBrevmottakerService +import no.nav.familie.tilbake.behandling.domain.Saksbehandlingstype +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class StegService( + val steg: List, + val behandlingRepository: BehandlingRepository, + val behandlingskontrollService: BehandlingskontrollService, + val validerBrevmottakerService: ValiderBrevmottakerService, +) { + + @Transactional + fun håndterSteg(behandlingId: UUID) { + var aktivtBehandlingssteg: Behandlingssteg = hentAktivBehandlingssteg(behandlingId) + + hentStegInstans(aktivtBehandlingssteg).utførSteg(behandlingId) + + // Autoutfør brevmottaker steg og verge steg om verge informasjon er kopiert fra fagsystem + aktivtBehandlingssteg = hentAktivBehandlingssteg(behandlingId) + when (aktivtBehandlingssteg) { + Behandlingssteg.BREVMOTTAKER, Behandlingssteg.VERGE -> hentStegInstans(aktivtBehandlingssteg).utførSteg(behandlingId) + else -> return + } + } + + @Transactional + fun håndterSteg(behandlingId: UUID, behandlingsstegDto: BehandlingsstegDto) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + if (behandling.erSaksbehandlingAvsluttet) { + throw Feil("Behandling med id=$behandlingId er allerede ferdig behandlet") + } + val behandledeSteg: Behandlingssteg = Behandlingssteg.fraNavn(behandlingsstegDto.getSteg()) + if (behandlingskontrollService.erBehandlingPåVent(behandlingId)) { + throw Feil( + message = "Behandling med id=$behandlingId er på vent, kan ikke behandle steg $behandledeSteg", + frontendFeilmelding = "Behandling med id=$behandlingId er på vent, kan ikke behandle steg $behandledeSteg", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + + var aktivtBehandlingssteg: Behandlingssteg = hentAktivBehandlingssteg(behandlingId) + if (Behandlingssteg.FORESLÅ_VEDTAK == aktivtBehandlingssteg) { + validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligPersonMedManuelleBrevmottakere(behandlingId = behandling.id, fagsakId = behandling.fagsakId) + } + // Behandling kan ikke tilbakeføres når er på FatteVedtak/IverksetteVedtak steg + if (Behandlingssteg.FATTE_VEDTAK == aktivtBehandlingssteg || Behandlingssteg.IVERKSETT_VEDTAK == aktivtBehandlingssteg) { + if (behandlingsstegDto is BehandlingsstegFatteVedtaksstegDto) { + hentStegInstans(behandledeSteg).utførSteg(behandlingId, behandlingsstegDto) + + aktivtBehandlingssteg = hentAktivBehandlingssteg(behandlingId) + if (aktivtBehandlingssteg == Behandlingssteg.IVERKSETT_VEDTAK) { + hentStegInstans(aktivtBehandlingssteg).utførSteg(behandlingId) + } + } + return + } + behandlingskontrollService.behandleStegPåNytt(behandlingId, behandledeSteg) + hentStegInstans(behandledeSteg).utførSteg(behandlingId, behandlingsstegDto) + + // sjekk om aktivtBehandlingssteg kan autoutføres + aktivtBehandlingssteg = hentAktivBehandlingssteg(behandlingId) + if (aktivtBehandlingssteg in listOf( + Behandlingssteg.FORELDELSE, + Behandlingssteg.VILKÅRSVURDERING, + ) + ) { + hentStegInstans(aktivtBehandlingssteg).utførSteg(behandlingId) + } + } + + @Transactional + fun håndterStegAutomatisk(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + if (behandling.erSaksbehandlingAvsluttet) { + throw Feil("Behandling med id=$behandlingId er allerede ferdig behandlet") + } + if (behandling.regelverk == Regelverk.EØS) { + throw Feil("Behandling med id=$behandlingId behandles etter EØS-regelverket, og skal dermed ikke behandles automatisk.") + } + var aktivtBehandlingssteg = hentAktivBehandlingssteg(behandlingId) + val behandledeSteg = aktivtBehandlingssteg.name + if (behandlingskontrollService.erBehandlingPåVent(behandlingId)) { + throw Feil(message = "Behandling med id=$behandlingId er på vent, kan ikke behandle steg $behandledeSteg") + } + if (behandling.saksbehandlingstype != Saksbehandlingstype.AUTOMATISK_IKKE_INNKREVING_LAVT_BELØP) { + throw Feil( + message = "Behandling med id=$behandlingId er sett til ordinær saksbehandling. " + + "Kan ikke saksbehandle den automatisk", + ) + } + while (aktivtBehandlingssteg != Behandlingssteg.AVSLUTTET) { + hentStegInstans(aktivtBehandlingssteg).utførStegAutomatisk(behandlingId) + if (aktivtBehandlingssteg == Behandlingssteg.IVERKSETT_VEDTAK) { + break + } + aktivtBehandlingssteg = hentAktivBehandlingssteg(behandlingId) + } + } + + @Transactional + fun gjenopptaSteg(behandlingId: UUID) { + var aktivtBehandlingssteg = hentAktivBehandlingssteg(behandlingId) + + hentStegInstans(aktivtBehandlingssteg).gjenopptaSteg(behandlingId) + + // Autoutfør brevmottaker steg og verge steg om verge informasjon er kopiert fra fagsystem + aktivtBehandlingssteg = hentAktivBehandlingssteg(behandlingId) + when (aktivtBehandlingssteg) { + Behandlingssteg.BREVMOTTAKER, Behandlingssteg.VERGE -> hentStegInstans(aktivtBehandlingssteg).utførSteg(behandlingId) + else -> return + } + } + + fun kanAnsvarligSaksbehandlerOppdateres( + behandlingId: UUID, + behandlingsstegDto: BehandlingsstegDto, + ): Boolean { + val behandlingssteg = Behandlingssteg.fraNavn(behandlingsstegDto.getSteg()) + return when (behandlingssteg) { + Behandlingssteg.IVERKSETT_VEDTAK, Behandlingssteg.FATTE_VEDTAK -> false + else -> true + } + } + + private fun hentAktivBehandlingssteg(behandlingId: UUID): Behandlingssteg { + val aktivtBehandlingssteg = behandlingskontrollService.finnAktivtSteg(behandlingId) + ?: throw Feil( + message = "Behandling $behandlingId har ikke noe aktiv steg", + frontendFeilmelding = "Behandling $behandlingId har ikke noe aktiv steg", + ) + if (aktivtBehandlingssteg !in setOf( + Behandlingssteg.VARSEL, + Behandlingssteg.GRUNNLAG, + Behandlingssteg.BREVMOTTAKER, + Behandlingssteg.VERGE, + Behandlingssteg.FAKTA, + Behandlingssteg.FORELDELSE, + Behandlingssteg.VILKÅRSVURDERING, + Behandlingssteg.FORESLÅ_VEDTAK, + Behandlingssteg.FATTE_VEDTAK, + Behandlingssteg.IVERKSETT_VEDTAK, + ) + ) { + throw Feil(message = "Steg $aktivtBehandlingssteg er ikke implementer ennå") + } + + return aktivtBehandlingssteg + } + + private fun hentStegInstans(behandlingssteg: Behandlingssteg): IBehandlingssteg { + return steg.singleOrNull { it.getBehandlingssteg() == behandlingssteg } + ?: error("Finner ikke behandlingssteg $behandlingssteg") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Varselssteg.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Varselssteg.kt new file mode 100644 index 000000000..de7b2840f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Varselssteg.kt @@ -0,0 +1,43 @@ +package no.nav.familie.tilbake.behandling.steg + +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class Varselssteg(private val behandlingskontrollService: BehandlingskontrollService) : IBehandlingssteg { + + private val logger = LoggerFactory.getLogger(this::class.java) + + @Transactional + override fun utførSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.VARSEL} steg") + logger.info( + "Behandling $behandlingId venter på ${Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING}. " + + "Den kan kun tas av vent av saksbehandler ved å gjenoppta behandlingen", + ) + } + + @Transactional + override fun gjenopptaSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId gjenopptar på ${Behandlingssteg.VARSEL} steg") + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.VARSEL, + Behandlingsstegstatus.UTFØRT, + ), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + override fun getBehandlingssteg(): Behandlingssteg { + return Behandlingssteg.VARSEL + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Vergessteg.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Vergessteg.kt new file mode 100644 index 000000000..6e1188c79 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Vergessteg.kt @@ -0,0 +1,75 @@ +package no.nav.familie.tilbake.behandling.steg + +import no.nav.familie.tilbake.api.dto.BehandlingsstegDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegVergeDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.VergeService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.AUTOUTFØRT +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.UTFØRT +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class Vergessteg( + private val behandlingRepository: BehandlingRepository, + private val vergeService: VergeService, + private val behandlingskontrollService: BehandlingskontrollService, + private val oppgaveTaskService: OppgaveTaskService, +) : IBehandlingssteg { + + private val logger = LoggerFactory.getLogger(this::class.java) + + @Transactional + override fun utførSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.VERGE} steg") + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + if (behandling.harVerge) { + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.VERGE, + AUTOUTFØRT, + ), + ) + } + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + @Transactional + override fun utførSteg(behandlingId: UUID, behandlingsstegDto: BehandlingsstegDto) { + logger.info("Behandling $behandlingId er på ${Behandlingssteg.VERGE} steg") + vergeService.lagreVerge(behandlingId, (behandlingsstegDto as BehandlingsstegVergeDto).verge) + + oppgaveTaskService.oppdaterAnsvarligSaksbehandlerOppgaveTask(behandlingId) + + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo(Behandlingssteg.VERGE, UTFØRT), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + @Transactional + override fun gjenopptaSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId gjenopptar på ${Behandlingssteg.VERGE} steg") + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.VERGE, + Behandlingsstegstatus.KLAR, + ), + ) + } + + override fun getBehandlingssteg(): Behandlingssteg { + return Behandlingssteg.VERGE + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Vilk\303\245rsvurderingssteg.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Vilk\303\245rsvurderingssteg.kt" new file mode 100644 index 000000000..dc74806a8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/steg/Vilk\303\245rsvurderingssteg.kt" @@ -0,0 +1,126 @@ +package no.nav.familie.tilbake.behandling.steg + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.tilbake.api.dto.BehandlingsstegDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegVilkårsvurderingDto +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg.VILKÅRSVURDERING +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.AUTOUTFØRT +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.UTFØRT +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.foreldelse.ForeldelseService +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.event.EndretKravgrunnlagEvent +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingService +import org.slf4j.LoggerFactory +import org.springframework.context.event.EventListener +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class Vilkårsvurderingssteg( + private val behandlingskontrollService: BehandlingskontrollService, + private val vilkårsvurderingService: VilkårsvurderingService, + private val foreldelseService: ForeldelseService, + private val historikkTaskService: HistorikkTaskService, + private val oppgaveTaskService: OppgaveTaskService, +) : IBehandlingssteg { + + private val logger = LoggerFactory.getLogger(this::class.java) + + @Transactional + override fun utførSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på $VILKÅRSVURDERING steg") + if (harAllePerioderForeldet(behandlingId)) { + // hvis det finnes noen periode som ble vurdert før i vilkårsvurdering, må slettes + vilkårsvurderingService.deaktiverEksisterendeVilkårsvurdering(behandlingId) + + lagHistorikkinnslag(behandlingId, Aktør.VEDTAKSLØSNING) + + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo(VILKÅRSVURDERING, AUTOUTFØRT), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + } + + @Transactional + override fun utførSteg(behandlingId: UUID, behandlingsstegDto: BehandlingsstegDto) { + logger.info("Behandling $behandlingId er på $VILKÅRSVURDERING steg") + if (harAllePerioderForeldet(behandlingId)) { + throw Feil( + message = "Alle perioder er foreldet for $behandlingId,kan ikke behandle vilkårsvurdering", + frontendFeilmelding = "Alle perioder er foreldet for $behandlingId,kan ikke behandle vilkårsvurdering", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + vilkårsvurderingService.lagreVilkårsvurdering(behandlingId, behandlingsstegDto as BehandlingsstegVilkårsvurderingDto) + + oppgaveTaskService.oppdaterAnsvarligSaksbehandlerOppgaveTask(behandlingId) + + lagHistorikkinnslag(behandlingId, Aktør.SAKSBEHANDLER) + + behandlingskontrollService.oppdaterBehandlingsstegStatus(behandlingId, Behandlingsstegsinfo(VILKÅRSVURDERING, UTFØRT)) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + @Transactional + override fun utførStegAutomatisk(behandlingId: UUID) { + logger.info("Behandling $behandlingId er på $VILKÅRSVURDERING steg og behandler automatisk..") + if (harAllePerioderForeldet(behandlingId)) { + utførSteg(behandlingId) + return + } + + vilkårsvurderingService.lagreFastVilkårForAutomatiskSaksbehandling(behandlingId) + lagHistorikkinnslag(behandlingId, Aktør.VEDTAKSLØSNING) + + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo(VILKÅRSVURDERING, UTFØRT), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + @Transactional + override fun gjenopptaSteg(behandlingId: UUID) { + logger.info("Behandling $behandlingId gjenopptar på $VILKÅRSVURDERING steg") + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + VILKÅRSVURDERING, + Behandlingsstegstatus.KLAR, + ), + ) + } + + override fun getBehandlingssteg(): Behandlingssteg { + return VILKÅRSVURDERING + } + + @EventListener + fun deaktiverEksisterendeVilkårsvurdering(endretKravgrunnlagEvent: EndretKravgrunnlagEvent) { + vilkårsvurderingService.deaktiverEksisterendeVilkårsvurdering(endretKravgrunnlagEvent.behandlingId) + } + + private fun harAllePerioderForeldet(behandlingId: UUID): Boolean { + return foreldelseService.hentAktivVurdertForeldelse(behandlingId) + ?.foreldelsesperioder?.all { it.erForeldet() } ?: false + } + + private fun lagHistorikkinnslag(behandlingId: UUID, aktør: Aktør) { + historikkTaskService.lagHistorikkTask( + behandlingId, + TilbakekrevingHistorikkinnslagstype.VILKÅRSVURDERING_VURDERT, + aktør, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/task/OppdaterFaktainfoTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/task/OppdaterFaktainfoTask.kt new file mode 100644 index 000000000..d7a85baf5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/task/OppdaterFaktainfoTask.kt @@ -0,0 +1,62 @@ +package no.nav.familie.tilbake.behandling.task + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.BehandlingService +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingService +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +@TaskStepBeskrivelse( + taskStepType = OppdaterFaktainfoTask.TYPE, + beskrivelse = "oppdaterer fakta info når kravgrunnlag mottas av ny referanse", + maxAntallFeil = 10, + triggerTidVedFeilISekunder = 5L, +) +class OppdaterFaktainfoTask( + private val hentFagsystemsbehandlingService: HentFagsystemsbehandlingService, + private val behandlingService: BehandlingService, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("OppdaterFaktainfoTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val eksternFagsakId = task.metadata.getProperty("eksternFagsakId") + val ytelsestype = Ytelsestype.valueOf(task.metadata.getProperty("ytelsestype")) + val eksternId = task.metadata.getProperty("eksternId") + + val requestSendt = requireNotNull( + hentFagsystemsbehandlingService.hentFagsystemsbehandlingRequestSendt( + eksternFagsakId, + ytelsestype, + eksternId, + ), + ) + // kaster exception inntil respons-en har mottatt + val hentFagsystemsbehandlingRespons = requireNotNull(requestSendt.respons) { + "HentFagsystemsbehandlingRespons er ikke mottatt fra fagsystem for " + + "eksternFagsakId=$eksternFagsakId,ytelsestype=$ytelsestype,eksternId=$eksternId." + + "Task kan kjøre på nytt manuelt når respons er mottatt." + } + + val respons = hentFagsystemsbehandlingService.lesRespons(hentFagsystemsbehandlingRespons) + val feilMelding = respons.feilMelding + if (feilMelding != null) { + throw Feil( + "Noen gikk galt mens henter fagsystemsbehandling fra fagsystem. " + + "Feiler med $feilMelding", + ) + } + behandlingService.oppdaterFaktainfo(eksternFagsakId, ytelsestype, eksternId, respons.hentFagsystemsbehandling!!) + } + + companion object { + + const val TYPE = "oppdater.faktainfo" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/task/OpprettBehandlingManueltTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/task/OpprettBehandlingManueltTask.kt new file mode 100644 index 000000000..c3c85952a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandling/task/OpprettBehandlingManueltTask.kt @@ -0,0 +1,94 @@ +package no.nav.familie.tilbake.behandling.task + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.BehandlingManuellOpprettelseService +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingService +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional + +@Service +@TaskStepBeskrivelse( + taskStepType = OpprettBehandlingManueltTask.TYPE, + beskrivelse = "oppretter behandling manuelt", + maxAntallFeil = 10, + triggerTidVedFeilISekunder = 5L, +) +class OpprettBehandlingManueltTask( + private val hentFagsystemsbehandlingService: HentFagsystemsbehandlingService, + private val behManuellOpprService: BehandlingManuellOpprettelseService, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + @Transactional(propagation = Propagation.REQUIRES_NEW) + override fun preCondition(task: Task) { + log.info("Henter fagsystemsbehandling for OpprettBehandlingManueltTask med id ${task.id} og metadata ${task.metadata}") + val eksternFagsakId = task.metadata.getProperty("eksternFagsakId") + val ytelsestype = Ytelsestype.valueOf(task.metadata.getProperty("ytelsestype")) + val eksternId = task.metadata.getProperty("eksternId") + hentFagsystemsbehandlingService.sendHentFagsystemsbehandlingRequest(eksternFagsakId, ytelsestype, eksternId) + } + + @Transactional + override fun doTask(task: Task) { + log.info("OpprettBehandlingManueltTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val eksternFagsakId = task.metadata.getProperty("eksternFagsakId") + val ytelsestype = Ytelsestype.valueOf(task.metadata.getProperty("ytelsestype")) + val eksternId = task.metadata.getProperty("eksternId") + + val requestSendt = requireNotNull( + hentFagsystemsbehandlingService.hentFagsystemsbehandlingRequestSendt( + eksternFagsakId, + ytelsestype, + eksternId, + ), + ) + // kaster exception inntil respons-en har mottatt + val respons = requireNotNull(requestSendt.respons) { + "HentFagsystemsbehandling respons-en har ikke mottatt fra fagsystem for " + + "eksternFagsakId=$eksternFagsakId,ytelsestype=$ytelsestype,eksternId=$eksternId." + + "Task-en kan kjøre på nytt manuelt når respons-en er mottatt" + } + + val hentFagsystemsbehandlingRespons = hentFagsystemsbehandlingService.lesRespons(respons) + val feilMelding = hentFagsystemsbehandlingRespons.feilMelding + if (feilMelding != null) { + hentFagsystemsbehandlingService.slettOgSendNyHentFagsystembehandlingRequest( + requestSendtId = requestSendt.id, + eksternFagsakId = eksternFagsakId, + ytelsestype = ytelsestype, + eksternId = eksternId, + ) + throw Feil( + "Noe gikk galt ved henting av fagsystemsbehandling fra fagsystem. Legger ny melding på topic. Task må rekjøres. " + + "Feiler med $feilMelding", + ) + } + + // opprett behandling + val ansvarligSaksbehandler = task.metadata.getProperty("ansvarligSaksbehandler") + log.info( + "Oppretter manuell tilbakekrevingsbehandling request for " + + "eksternFagsakId=$eksternFagsakId,ytelsestype=$ytelsestype,eksternId=$eksternId.", + ) + behManuellOpprService.opprettBehandlingManuell( + eksternFagsakId = eksternFagsakId, + ytelsestype = ytelsestype, + eksternId = eksternId, + ansvarligSaksbehandler = ansvarligSaksbehandler, + fagsystemsbehandlingData = hentFagsystemsbehandlingRespons + .hentFagsystemsbehandling!!, + ) + } + + companion object { + + const val TYPE = "opprettBehandlingManuelt" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandlingskontroll/BehandlingskontrollService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandlingskontroll/BehandlingskontrollService.kt new file mode 100644 index 000000000..4ebe5e503 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandlingskontroll/BehandlingskontrollService.kt @@ -0,0 +1,419 @@ +package no.nav.familie.tilbake.behandlingskontroll + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.AUTOUTFØRT +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.AVBRUTT +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.KLAR +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.TILBAKEFØRT +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.UTFØRT +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.VENTER +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.datavarehus.saksstatistikk.BehandlingTilstandService +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.ManuellBrevmottakerRepository +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.util.UUID + +@Service +class BehandlingskontrollService( + private val behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository, + private val behandlingRepository: BehandlingRepository, + private val behandlingTilstandService: BehandlingTilstandService, + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val historikkTaskService: HistorikkTaskService, + private val featureToggleService: FeatureToggleService, + private val brevmottakerRepository: ManuellBrevmottakerRepository, +) { + + private val log = LoggerFactory.getLogger(this::class.java) + + @Transactional + fun fortsettBehandling(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + if (behandling.erAvsluttet) { + return + } + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + val aktivtStegstilstand = finnAktivStegstilstand(behandlingsstegstilstand) + + if (aktivtStegstilstand == null) { + val nesteStegMetaData = finnNesteBehandlingsstegMedStatus(behandling, behandlingsstegstilstand) + persisterBehandlingsstegOgStatus(behandlingId, nesteStegMetaData) + if (nesteStegMetaData.behandlingsstegstatus == VENTER) { + historikkTaskService + .lagHistorikkTask( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT, + aktør = Aktør.VEDTAKSLØSNING, + beskrivelse = nesteStegMetaData.venteårsak?.beskrivelse, + ) + } + } else { + log.info( + "Behandling har allerede et aktivt steg=${aktivtStegstilstand.behandlingssteg} " + + "med status=${aktivtStegstilstand.behandlingsstegsstatus}", + ) + } + } + + @Transactional + fun tilbakehoppBehandlingssteg(behandlingId: UUID, behandlingsstegsinfo: Behandlingsstegsinfo) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + if (behandling.erAvsluttet) { + throw Feil( + "Behandling med id=$behandlingId er allerede ferdig behandlet, " + + "så kan ikke forsette til ${behandlingsstegsinfo.behandlingssteg}", + ) + } + val behandlingsstegstilstand: List = + behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + val aktivtBehandlingssteg = finnAktivStegstilstand(behandlingsstegstilstand) + ?: throw Feil("Behandling med id=$behandlingId har ikke noe aktivt steg") + // steg som kan behandles, kan avbrytes + if (aktivtBehandlingssteg.behandlingssteg.kanSaksbehandles) { + behandlingsstegstilstandRepository.update(aktivtBehandlingssteg.copy(behandlingsstegsstatus = AVBRUTT)) + persisterBehandlingsstegOgStatus(behandlingId, behandlingsstegsinfo) + } + } + + @Transactional + fun tilbakeførBehandledeSteg(behandlingId: UUID) { + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + val alleIkkeVentendeSteg = behandlingsstegstilstand.filter { it.behandlingsstegsstatus != VENTER } + .filter { it.behandlingssteg !in listOf(Behandlingssteg.VARSEL, Behandlingssteg.GRUNNLAG) } + alleIkkeVentendeSteg.forEach { + log.info("Tilbakefører ${it.behandlingssteg} for behandling $behandlingId") + oppdaterBehandlingsstegStatus(behandlingId, Behandlingsstegsinfo(it.behandlingssteg, TILBAKEFØRT)) + } + } + + @Transactional + fun behandleStegPåNytt(behandlingId: UUID, behandledeSteg: Behandlingssteg) { + val aktivtBehandlingssteg = finnAktivtSteg(behandlingId) + ?: throw Feil("Behandling med id=$behandlingId har ikke noe aktivt steg") + + if (behandledeSteg.sekvens < aktivtBehandlingssteg.sekvens) { + for (i in aktivtBehandlingssteg.sekvens downTo behandledeSteg.sekvens + 1 step 1) { + val behandlingssteg = Behandlingssteg.fraSekvens(i, sjekkOmBrevmottakerErstatterVergeForSekvens(i, behandlingId)) + oppdaterBehandlingsstegStatus(behandlingId, Behandlingsstegsinfo(behandlingssteg, TILBAKEFØRT)) + } + oppdaterBehandlingsstegStatus(behandlingId, Behandlingsstegsinfo(behandledeSteg, KLAR)) + } + } + + @Transactional + fun behandleVergeSteg(behandlingId: UUID) { + tilbakeførBehandledeSteg(behandlingId) + log.info("Oppretter verge steg for behandling med id=$behandlingId") + val eksisterendeVergeSteg = behandlingsstegstilstandRepository.findByBehandlingIdAndBehandlingssteg( + behandlingId, + Behandlingssteg.VERGE, + ) + when { + eksisterendeVergeSteg != null -> { + oppdaterBehandlingsstegStatus(behandlingId, Behandlingsstegsinfo(Behandlingssteg.VERGE, KLAR)) + } + else -> { + opprettBehandlingsstegOgStatus(behandlingId, Behandlingsstegsinfo(Behandlingssteg.VERGE, KLAR)) + } + } + } + + @Transactional + fun behandleBrevmottakerSteg(behandlingId: UUID) { + log.info("Aktiverer brevmottaker steg for behandling med id=$behandlingId") + behandlingsstegstilstandRepository.findByBehandlingIdAndBehandlingssteg( + behandlingId, + Behandlingssteg.BREVMOTTAKER, + ) ?.apply { + oppdaterBehandlingsstegStatus(behandlingId, Behandlingsstegsinfo(Behandlingssteg.BREVMOTTAKER, AUTOUTFØRT)) + } ?: opprettBehandlingsstegOgStatus( + behandlingId = behandlingId, + nesteStegMedStatus = Behandlingsstegsinfo(Behandlingssteg.BREVMOTTAKER, AUTOUTFØRT), + opprettSendingAvBehandlingensTilstand = false, // da det settes AUTOUTFØRT, forblir aktivt steg / tilstanden den samme + ) + } + + @Transactional + fun settBehandlingPåVent(behandlingId: UUID, venteårsak: Venteårsak, tidsfrist: LocalDate) { + val behandlingsstegstilstand: List = + behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + val aktivtBehandlingsstegstilstand = finnAktivStegstilstand(behandlingsstegstilstand) + ?: throw Feil( + message = "Behandling $behandlingId " + + "har ikke aktivt steg", + frontendFeilmelding = "Behandling $behandlingId " + + "har ikke aktivt steg", + ) + behandlingsstegstilstandRepository.update( + aktivtBehandlingsstegstilstand.copy( + behandlingsstegsstatus = VENTER, + venteårsak = venteårsak, + tidsfrist = tidsfrist, + ), + ) + // oppdater tilsvarende behandlingsstatus + oppdaterBehandlingsstatus(behandlingId, aktivtBehandlingsstegstilstand.behandlingssteg) + + historikkTaskService.lagHistorikkTask( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT, + aktør = Aktør.SAKSBEHANDLER, + beskrivelse = venteårsak.beskrivelse, + ) + } + + @Transactional + fun henleggBehandlingssteg(behandlingId: UUID) { + val behandlingsstegstilstand: List = + behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + behandlingsstegstilstand.filter { it.behandlingssteg != Behandlingssteg.VARSEL } + .forEach { + behandlingsstegstilstandRepository.update(it.copy(behandlingsstegsstatus = AVBRUTT)) + } + } + + fun erBehandlingPåVent(behandlingId: UUID): Boolean { + val behandlingsstegstilstand: List = + behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + val aktivtBehandlingsstegstilstand: Behandlingsstegstilstand = finnAktivStegstilstand(behandlingsstegstilstand) + ?: return false + return VENTER == aktivtBehandlingsstegstilstand.behandlingsstegsstatus + } + + fun hentBehandlingsstegstilstand(behandling: Behandling): List { + val behandlingsstegstilstand: List = + behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + return behandlingsstegstilstand.map { + Behandlingsstegsinfo( + behandlingssteg = it.behandlingssteg, + behandlingsstegstatus = it.behandlingsstegsstatus, + venteårsak = it.venteårsak, + tidsfrist = it.tidsfrist, + ) + } + } + + fun finnAktivtSteg(behandlingId: UUID): Behandlingssteg? { + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + return finnAktivStegstilstand(behandlingsstegstilstand)?.behandlingssteg + } + + fun finnAktivStegstilstand(behandlingsstegstilstand: List): Behandlingsstegstilstand? { + return behandlingsstegstilstand + .firstOrNull { Behandlingsstegstatus.erStegAktiv(it.behandlingsstegsstatus) } + // forutsetter at behandling kan ha kun et aktiv steg om gangen + } + + fun finnAktivStegstilstand(behandlingId: UUID): Behandlingsstegstilstand? { + return finnAktivStegstilstand(behandlingsstegstilstandRepository.findByBehandlingId(behandlingId)) + } + + @Transactional + fun oppdaterBehandlingsstegStatus(behandlingId: UUID, behandlingsstegsinfo: Behandlingsstegsinfo) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + if (behandling.erAvsluttet && ( + behandlingsstegsinfo.behandlingssteg != Behandlingssteg.AVSLUTTET && + behandlingsstegsinfo.behandlingsstegstatus != UTFØRT + ) + ) { + throw Feil( + "Behandling med id=$behandlingId er allerede ferdig behandlet, " + + "så status=${behandlingsstegsinfo.behandlingsstegstatus} kan ikke oppdateres", + ) + } + val behandlingsstegstilstand = + behandlingsstegstilstandRepository + .findByBehandlingIdAndBehandlingssteg(behandlingId, behandlingsstegsinfo.behandlingssteg) + ?: throw Feil( + message = "Behandling med id=$behandlingId og " + + "steg=${behandlingsstegsinfo.behandlingssteg} finnes ikke", + ) + + behandlingsstegstilstandRepository + .update( + behandlingsstegstilstand.copy( + behandlingsstegsstatus = behandlingsstegsinfo.behandlingsstegstatus, + venteårsak = behandlingsstegsinfo.venteårsak, + tidsfrist = behandlingsstegsinfo.tidsfrist, + ), + ) + + // oppdater tilsvarende behandlingsstatus + oppdaterBehandlingsstatus(behandlingId, behandlingsstegsinfo.behandlingssteg) + behandlingTilstandService.opprettSendingAvBehandlingensTilstand(behandlingId, behandlingsstegsinfo) + } + + private fun opprettBehandlingsstegOgStatus( + behandlingId: UUID, + nesteStegMedStatus: Behandlingsstegsinfo, + opprettSendingAvBehandlingensTilstand: Boolean = true, + ) { + // startet nytt behandlingssteg + behandlingsstegstilstandRepository + .insert( + Behandlingsstegstilstand( + behandlingId = behandlingId, + behandlingssteg = nesteStegMedStatus.behandlingssteg, + venteårsak = nesteStegMedStatus.venteårsak, + tidsfrist = nesteStegMedStatus.tidsfrist, + behandlingsstegsstatus = nesteStegMedStatus.behandlingsstegstatus, + ), + ) + // oppdater tilsvarende behandlingsstatus + oppdaterBehandlingsstatus(behandlingId, nesteStegMedStatus.behandlingssteg) + if (opprettSendingAvBehandlingensTilstand) { + behandlingTilstandService.opprettSendingAvBehandlingensTilstand(behandlingId, nesteStegMedStatus) + } + } + + private fun persisterBehandlingsstegOgStatus( + behandlingId: UUID, + behandlingsstegsinfo: Behandlingsstegsinfo, + ) { + val gammelBehandlingsstegstilstand = + behandlingsstegstilstandRepository.findByBehandlingIdAndBehandlingssteg( + behandlingId, + behandlingsstegsinfo.behandlingssteg, + ) + when (gammelBehandlingsstegstilstand) { + null -> { + opprettBehandlingsstegOgStatus(behandlingId, behandlingsstegsinfo) + } + else -> { + oppdaterBehandlingsstegStatus(behandlingId, behandlingsstegsinfo) + } + } + } + + private fun finnNesteBehandlingsstegMedStatus( + behandling: Behandling, + stegstilstand: List, + ): Behandlingsstegsinfo { + if (stegstilstand.isEmpty()) { + return when { + // setter tidsfristen fra opprettelse dato + kanSendeVarselsbrev(behandling) -> lagBehandlingsstegsinfo( + Behandlingssteg.VARSEL, + VENTER, + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + behandling.opprettetDato, + ) + !harAktivtGrunnlag(behandling) -> lagBehandlingsstegsinfo( + Behandlingssteg.GRUNNLAG, + VENTER, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + behandling.opprettetDato, + ) + else -> lagBehandlingsstegsinfo(Behandlingssteg.FAKTA, KLAR) + } + } + + val finnesAvbruttSteg = stegstilstand.any { AVBRUTT == it.behandlingsstegsstatus } + if (finnesAvbruttSteg) { + // forutsetter behandling har et AVBRUTT steg om gangen + val avbruttSteg = stegstilstand.first { AVBRUTT == it.behandlingsstegsstatus } + return lagBehandlingsstegsinfo(avbruttSteg.behandlingssteg, KLAR) + } + + val sisteUtførteSteg = stegstilstand.filter { Behandlingsstegstatus.erStegUtført(it.behandlingsstegsstatus) } + .maxByOrNull { it.sporbar.endret.endretTid }!!.behandlingssteg + + if (Behandlingssteg.VARSEL == sisteUtførteSteg) { + return håndterOmSisteUtførteStegErVarsel(behandling) + } + return lagBehandlingsstegsinfo( + behandlingssteg = Behandlingssteg.finnNesteBehandlingssteg( + behandlingssteg = sisteUtførteSteg, + harVerge = behandling.harVerge, + harManuelleBrevmottakere = brevmottakerRepository.findByBehandlingId(behandling.id).isNotEmpty(), + ), + KLAR, + ) + } + + private fun håndterOmSisteUtførteStegErVarsel(behandling: Behandling): Behandlingsstegsinfo { + return when { + erKravgrunnlagSperret(behandling) -> { + val kravgrunnlag = kravgrunnlagRepository + .findByBehandlingIdAndAktivIsTrueAndSperretTrue(behandling.id) + + // setter tidsfristen fra sperret dato + lagBehandlingsstegsinfo( + behandlingssteg = Behandlingssteg.GRUNNLAG, + behandlingsstegstatus = VENTER, + venteårsak = Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + tidsfrist = kravgrunnlag.sporbar.endret.endretTid + .toLocalDate(), + ) + } + harAktivtGrunnlag(behandling) -> { + if (behandling.harVerge) { + lagBehandlingsstegsinfo(behandlingssteg = Behandlingssteg.VERGE, behandlingsstegstatus = KLAR) + } else { + lagBehandlingsstegsinfo(behandlingssteg = Behandlingssteg.FAKTA, behandlingsstegstatus = KLAR) + } + } + // setter tidsfristen fra opprettelse dato + else -> lagBehandlingsstegsinfo( + behandlingssteg = Behandlingssteg.GRUNNLAG, + behandlingsstegstatus = VENTER, + venteårsak = Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + tidsfrist = behandling.opprettetDato, + ) + } + } + + private fun kanSendeVarselsbrev(behandling: Behandling): Boolean { + return Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL == behandling.aktivFagsystemsbehandling.tilbakekrevingsvalg && + !behandling.manueltOpprettet && !behandling.erRevurdering + } + + private fun harAktivtGrunnlag(behandling: Behandling): Boolean { + return kravgrunnlagRepository.existsByBehandlingIdAndAktivTrueAndSperretFalse(behandling.id) + } + + private fun erKravgrunnlagSperret(behandling: Behandling): Boolean { + return kravgrunnlagRepository.existsByBehandlingIdAndAktivTrueAndSperretTrue(behandling.id) + } + + private fun lagBehandlingsstegsinfo( + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + venteårsak: Venteårsak? = null, + tidsfrist: LocalDate? = null, + ): Behandlingsstegsinfo { + return Behandlingsstegsinfo( + behandlingssteg = behandlingssteg, + behandlingsstegstatus = behandlingsstegstatus, + venteårsak = venteårsak, + tidsfrist = venteårsak?.defaultVenteTidIUker?.let { tidsfrist?.plusWeeks(it) }, + ) + } + + private fun oppdaterBehandlingsstatus(behandlingId: UUID, behandlingssteg: Behandlingssteg) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + // Oppdaterer tilsvarende behandlingsstatus bortsett fra Avsluttet steg. Det håndteres separat av AvsluttBehandlingTask + if (Behandlingssteg.AVSLUTTET != behandlingssteg) { + behandlingRepository.update(behandling.copy(status = behandlingssteg.behandlingsstatus)) + } + } + + private fun sjekkOmBrevmottakerErstatterVergeForSekvens(sekvens: Int, behandlingId: UUID) = + sekvens == Behandlingssteg.VERGE.sekvens && behandlingsstegstilstandRepository + .findByBehandlingIdAndBehandlingssteg(behandlingId, Behandlingssteg.BREVMOTTAKER) != null +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandlingskontroll/Behandlingsstegsinfo.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandlingskontroll/Behandlingsstegsinfo.kt new file mode 100644 index 000000000..a7357d1df --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandlingskontroll/Behandlingsstegsinfo.kt @@ -0,0 +1,13 @@ +package no.nav.familie.tilbake.behandlingskontroll + +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import java.time.LocalDate + +data class Behandlingsstegsinfo( + val behandlingssteg: Behandlingssteg, + val behandlingsstegstatus: Behandlingsstegstatus, + val venteårsak: Venteårsak? = null, + val tidsfrist: LocalDate? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandlingskontroll/BehandlingsstegstilstandRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandlingskontroll/BehandlingsstegstilstandRepository.kt new file mode 100644 index 000000000..b98eb7591 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandlingskontroll/BehandlingsstegstilstandRepository.kt @@ -0,0 +1,24 @@ +package no.nav.familie.tilbake.behandlingskontroll + +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository +interface BehandlingsstegstilstandRepository : + RepositoryInterface, + InsertUpdateRepository { + + fun findByBehandlingId(behandlingId: UUID): List + + fun findByBehandlingIdAndBehandlingsstegsstatusIn( + behandlingId: UUID, + statuser: List, + ): Behandlingsstegstilstand? + + fun findByBehandlingIdAndBehandlingssteg(behandlingId: UUID, behandlingssteg: Behandlingssteg): Behandlingsstegstilstand? +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandlingskontroll/domain/Behandlingsstegstilstand.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandlingskontroll/domain/Behandlingsstegstilstand.kt new file mode 100644 index 000000000..31fdce7d2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/behandlingskontroll/domain/Behandlingsstegstilstand.kt @@ -0,0 +1,134 @@ +package no.nav.familie.tilbake.behandlingskontroll.domain + +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Embedded +import java.time.LocalDate +import java.util.UUID + +data class Behandlingsstegstilstand( + @Id + val id: UUID = UUID.randomUUID(), + val behandlingId: UUID, + val behandlingssteg: Behandlingssteg, + val behandlingsstegsstatus: Behandlingsstegstatus, + @Column("ventearsak") + val venteårsak: Venteårsak? = null, + val tidsfrist: LocalDate? = null, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +enum class Behandlingssteg( + val sekvens: Int, + val kanSaksbehandles: Boolean, + val kanBesluttes: Boolean, + val behandlingsstatus: Behandlingsstatus, + private val beskrivelse: String, +) { + + VARSEL(1, false, false, Behandlingsstatus.UTREDES, "Vurdere om varsel om tilbakekreving skal sendes til søker"), + GRUNNLAG(2, false, false, Behandlingsstatus.UTREDES, "Mottat kravgrunnlag fra økonomi for tilbakekrevingsrevurdering"), + BREVMOTTAKER(3, true, false, Behandlingsstatus.UTREDES, "Registrere brevmottakere manuelt. Erstatter Verge-steget"), + + @Deprecated("Erstattes av BREVMOTTAKER") + VERGE(3, true, false, Behandlingsstatus.UTREDES, "Fakta om verge"), + FAKTA(4, true, true, Behandlingsstatus.UTREDES, "Fakta om Feilutbetaling"), + FORELDELSE(5, true, true, Behandlingsstatus.UTREDES, "Vurder om feilutbetalte perioder er foreldet"), + VILKÅRSVURDERING(6, true, true, Behandlingsstatus.UTREDES, "Vurdere om og hva som skal tilbakekreves"), + FORESLÅ_VEDTAK(7, true, true, Behandlingsstatus.UTREDES, "Foreslår vedtak"), + FATTE_VEDTAK(8, true, false, Behandlingsstatus.FATTER_VEDTAK, "Fatter vedtak"), + IVERKSETT_VEDTAK( + 9, + false, + false, + Behandlingsstatus.IVERKSETTER_VEDTAK, + "Iverksett vedtak fra en behandling. Forutsetter at et vedtak er fattet", + ), + AVSLUTTET(10, false, false, Behandlingsstatus.AVSLUTTET, "Behandlingen er ferdig behandlet"), + ; + + companion object { + + fun finnNesteBehandlingssteg( + behandlingssteg: Behandlingssteg, + harVerge: Boolean, + harManuelleBrevmottakere: Boolean, + ): Behandlingssteg { + val nesteBehandlingssteg = fraSekvens(behandlingssteg.sekvens + 1, harManuelleBrevmottakere) + if (nesteBehandlingssteg == VERGE && !harVerge) { + // Hvis behandling opprettes ikke med verge, kan behandlingen flyttes til neste steg + return fraSekvens(nesteBehandlingssteg.sekvens + 1) + } + return nesteBehandlingssteg + } + + fun fraSekvens(sekvens: Int, brevmottakerErstatterVerge: Boolean = false): Behandlingssteg { + for (behandlingssteg in values()) { + if (sekvens == behandlingssteg.sekvens) { + return when (behandlingssteg) { + BREVMOTTAKER, VERGE -> if (brevmottakerErstatterVerge) BREVMOTTAKER else VERGE + else -> behandlingssteg + } + } + } + throw IllegalArgumentException("Behandlingssteg finnes ikke med sekvens=$sekvens") + } + + fun fraNavn(navn: String): Behandlingssteg { + return values().firstOrNull { it.name == navn } + ?: throw IllegalArgumentException("Ukjent Behandlingssteg $navn") + } + } +} + +enum class Behandlingsstegstatus(private val beskrivelse: String) { + VENTER("Steget er satt på vent, f.eks. venter på brukertilbakemelding eller kravgrunnlag"), + KLAR("Klar til saksbehandling"), + UTFØRT("Steget er ferdig utført"), + AUTOUTFØRT("Steget utføres automatisk av systemet"), + TILBAKEFØRT("Steget er avbrutt og tilbakeført til et tidligere steg"), + AVBRUTT("Steget er avbrutt"), + ; + + companion object { + + val aktiveStegStatuser = listOf(VENTER, KLAR) + private val utførteStegStatuser = listOf(UTFØRT, AUTOUTFØRT) + + fun erStegAktiv(status: Behandlingsstegstatus): Boolean { + return aktiveStegStatuser.contains(status) + } + + fun erStegUtført(status: Behandlingsstegstatus): Boolean { + return utførteStegStatuser.contains(status) + } + } +} + +enum class Venteårsak(val defaultVenteTidIUker: Long, val beskrivelse: String) { + + VENT_PÅ_BRUKERTILBAKEMELDING(3, "Venter på tilbakemelding fra bruker"), + VENT_PÅ_TILBAKEKREVINGSGRUNNLAG(4, "Venter på kravgrunnlag fra økonomi"), + AVVENTER_DOKUMENTASJON(0, "Avventer dokumentasjon"), + UTVIDET_TILSVAR_FRIST(0, "Utvidet tilsvarsfrist"), + ENDRE_TILKJENT_YTELSE(0, "Mulig endring i tilkjent ytelse"), + VENT_PÅ_MULIG_MOTREGNING(0, "Mulig motregning med annen ytelse"), + ; + + companion object { + + fun venterPåBruker(venteårsak: Venteårsak?): Boolean { + return venteårsak in listOf(VENT_PÅ_BRUKERTILBAKEMELDING, UTVIDET_TILSVAR_FRIST, AVVENTER_DOKUMENTASJON) + } + + fun venterPåØkonomi(venteårsak: Venteårsak?): Boolean { + return venteårsak in listOf(VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, VENT_PÅ_MULIG_MOTREGNING) + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/Bel\303\270psberegningUtil.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/Bel\303\270psberegningUtil.kt" new file mode 100644 index 000000000..285fecac7 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/Bel\303\270psberegningUtil.kt" @@ -0,0 +1,30 @@ +package no.nav.familie.tilbake.beregning + +import no.nav.familie.kontrakter.felles.Månedsperiode +import java.math.BigDecimal +import java.math.RoundingMode + +object BeløpsberegningUtil { + + fun beregnBeløpPerMåned(beløp: BigDecimal, kravgrunnlagsperiode: Månedsperiode): BigDecimal { + return beløp.divide(BigDecimal.valueOf(kravgrunnlagsperiode.lengdeIHeleMåneder()), 2, RoundingMode.HALF_UP) + } + + fun beregnBeløp(vurderingsperiode: Månedsperiode, kravgrunnlagsperiode: Månedsperiode, beløpPerMåned: BigDecimal): BigDecimal { + val overlapp = kravgrunnlagsperiode.snitt(vurderingsperiode) + if (overlapp != null) { + return beløpPerMåned.multiply(BigDecimal.valueOf(overlapp.lengdeIHeleMåneder())) + } + return BigDecimal.ZERO + } + + fun beregnBeløpForPeriode( + tilbakekrevesBeløp: BigDecimal, + vurderingsperiode: Månedsperiode, + kravgrunnlagsperiode: Månedsperiode, + ): BigDecimal { + val grunnlagBeløpPerMåned: BigDecimal = beregnBeløpPerMåned(tilbakekrevesBeløp, kravgrunnlagsperiode) + val ytelseBeløp: BigDecimal = beregnBeløp(vurderingsperiode, kravgrunnlagsperiode, grunnlagBeløpPerMåned) + return ytelseBeløp.setScale(0, RoundingMode.HALF_UP) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/KravgrunnlagsberegningService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/KravgrunnlagsberegningService.kt new file mode 100644 index 000000000..3df8f6efd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/KravgrunnlagsberegningService.kt @@ -0,0 +1,100 @@ +package no.nav.familie.tilbake.beregning + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.beregning.modell.FordeltKravgrunnlagsbeløp +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.isNotZero +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsperiode432 +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import java.math.BigDecimal +import java.math.RoundingMode +import java.time.YearMonth +import java.util.function.Function + +@Service +object KravgrunnlagsberegningService { + + private val feilutbetaltYtelsesbeløputleder: (Kravgrunnlagsperiode432) -> BigDecimal = { kgPeriode: Kravgrunnlagsperiode432 -> + kgPeriode.beløp + .filter { it.klassetype == Klassetype.FEIL } + .sumOf(Kravgrunnlagsbeløp433::nyttBeløp) + } + + private val utbetaltYtelsesbeløputleder = { kgPeriode: Kravgrunnlagsperiode432 -> + kgPeriode.beløp + .filter { it.klassetype == Klassetype.YTEL } + .sumOf(Kravgrunnlagsbeløp433::opprinneligUtbetalingsbeløp) + } + + private val riktigYteslesbeløputleder = { kgPeriode: Kravgrunnlagsperiode432 -> + kgPeriode.beløp + .filter { it.klassetype == Klassetype.YTEL } + .sumOf(Kravgrunnlagsbeløp433::nyttBeløp) + } + + fun fordelKravgrunnlagBeløpPåPerioder( + kravgrunnlag: Kravgrunnlag431, + vurderingsperioder: List, + ): Map { + return vurderingsperioder.associateWith { + FordeltKravgrunnlagsbeløp( + beregnBeløp(kravgrunnlag, it, feilutbetaltYtelsesbeløputleder), + beregnBeløp(kravgrunnlag, it, utbetaltYtelsesbeløputleder), + beregnBeløp(kravgrunnlag, it, riktigYteslesbeløputleder), + ) + } + } + + fun summerKravgrunnlagBeløpForPerioder(kravgrunnlag: Kravgrunnlag431): Map { + return kravgrunnlag.perioder.associate { + it.periode to FordeltKravgrunnlagsbeløp( + feilutbetaltYtelsesbeløputleder(it), + utbetaltYtelsesbeløputleder(it), + riktigYteslesbeløputleder(it), + ) + } + } + + fun beregnFeilutbetaltBeløp(kravgrunnlag: Kravgrunnlag431, vurderingsperiode: Månedsperiode): BigDecimal { + return beregnBeløp(kravgrunnlag, vurderingsperiode, feilutbetaltYtelsesbeløputleder) + } + + fun validatePerioder(perioder: List) { + val perioderSomIkkeErHeleMåneder = perioder.filter { + it.fom.dayOfMonth != 1 || + it.tom.dayOfMonth != YearMonth.from(it.tom).lengthOfMonth() + } + + if (perioderSomIkkeErHeleMåneder.isNotEmpty()) { + throw Feil( + message = "Periode med ${perioderSomIkkeErHeleMåneder[0]} er ikke i hele måneder", + frontendFeilmelding = "Periode med ${perioderSomIkkeErHeleMåneder[0]} er ikke i hele måneder", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + + private fun beregnBeløp( + kravgrunnlag: Kravgrunnlag431, + vurderingsperiode: Månedsperiode, + beløpsummerer: Function, + ): BigDecimal { + val sum = kravgrunnlag.perioder + .sortedBy { it.periode.fom } + .sumOf { + val beløp = beløpsummerer.apply(it) + if (beløp.isNotZero()) { + val beløpPerMåned: BigDecimal = BeløpsberegningUtil.beregnBeløpPerMåned(beløp, it.periode) + BeløpsberegningUtil.beregnBeløp(vurderingsperiode, it.periode, beløpPerMåned) + } else { + BigDecimal.ZERO + } + } + return sum.setScale(0, RoundingMode.HALF_UP) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/TilbakekrevingsberegningService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/TilbakekrevingsberegningService.kt new file mode 100644 index 000000000..be6e5764a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/TilbakekrevingsberegningService.kt @@ -0,0 +1,255 @@ +package no.nav.familie.tilbake.beregning + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.api.dto.BeregnetPeriodeDto +import no.nav.familie.tilbake.api.dto.BeregnetPerioderDto +import no.nav.familie.tilbake.api.dto.BeregningsresultatDto +import no.nav.familie.tilbake.api.dto.BeregningsresultatsperiodeDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Saksbehandlingstype +import no.nav.familie.tilbake.beregning.modell.Beregningsresultat +import no.nav.familie.tilbake.beregning.modell.Beregningsresultatsperiode +import no.nav.familie.tilbake.beregning.modell.FordeltKravgrunnlagsbeløp +import no.nav.familie.tilbake.beregning.modell.GrunnlagsperiodeMedSkatteprosent +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.FeatureToggleConfig.Companion.BRUK_6_DESIMALER_I_SKATTEBEREGNING +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.foreldelse.VurdertForeldelseRepository +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesperiode +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.Fagområdekode +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingRepository +import no.nav.familie.tilbake.vilkårsvurdering.domain.AnnenVurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsperiode +import org.springframework.stereotype.Service +import java.math.BigDecimal +import java.util.UUID + +@Service +class TilbakekrevingsberegningService( + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val vurdertForeldelseRepository: VurdertForeldelseRepository, + private val vilkårsvurderingRepository: VilkårsvurderingRepository, + private val behandlingRepository: BehandlingRepository, + private val kravgrunnlagsberegningService: KravgrunnlagsberegningService, + private val featureToggleService: FeatureToggleService, +) { + + fun hentBeregningsresultat(behandlingId: UUID): BeregningsresultatDto { + val beregningsresultat = beregn(behandlingId) + val beregningsresultatsperioder = beregningsresultat.beregningsresultatsperioder.map { + BeregningsresultatsperiodeDto( + periode = it.periode.toDatoperiode(), + vurdering = it.vurdering, + feilutbetaltBeløp = it.feilutbetaltBeløp, + andelAvBeløp = it.andelAvBeløp, + renteprosent = it.renteprosent, + tilbakekrevingsbeløp = it.tilbakekrevingsbeløp, + tilbakekrevesBeløpEtterSkatt = it.tilbakekrevingsbeløpEtterSkatt, + ) + } + return BeregningsresultatDto( + beregningsresultatsperioder = beregningsresultatsperioder, + vedtaksresultat = beregningsresultat.vedtaksresultat, + ) + } + + fun beregn(behandlingId: UUID): Beregningsresultat { + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + val vurdertForeldelse = hentVurdertForeldelse(behandlingId) + val vilkårsvurdering = hentVilkårsvurdering(behandlingId) + val vurderingsperioder: List = finnPerioder(vurdertForeldelse, vilkårsvurdering) + val perioderMedBeløp: Map = + kravgrunnlagsberegningService.fordelKravgrunnlagBeløpPåPerioder(kravgrunnlag, vurderingsperioder) + val beregningsresultatperioder = + beregn( + kravgrunnlag, + vurdertForeldelse, + vilkårsvurdering, + perioderMedBeløp, + skalBeregneRenter(kravgrunnlag.fagområdekode), + ) + val totalTilbakekrevingsbeløp = beregningsresultatperioder.sumOf { it.tilbakekrevingsbeløp } + val totalFeilutbetaltBeløp = beregningsresultatperioder.sumOf { it.feilutbetaltBeløp } + return Beregningsresultat( + vedtaksresultat = bestemVedtakResultat( + behandlingId, + totalTilbakekrevingsbeløp, + totalFeilutbetaltBeløp, + ), + beregningsresultatsperioder = (beregningsresultatperioder), + ) + } + + fun beregnBeløp(behandlingId: UUID, perioder: List): BeregnetPerioderDto { + // Alle familieytelsene er månedsytelser. Så periode som skal lagres bør være innenfor en måned. + KravgrunnlagsberegningService.validatePerioder(perioder) + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + + return BeregnetPerioderDto( + beregnetPerioder = perioder.map { + val feilutbetaltBeløp = + KravgrunnlagsberegningService.beregnFeilutbetaltBeløp(kravgrunnlag, it.toMånedsperiode()) + BeregnetPeriodeDto( + periode = it, + feilutbetaltBeløp = feilutbetaltBeløp, + ) + }, + ) + } + + private fun hentVilkårsvurdering(behandlingId: UUID): Vilkårsvurdering? { + return vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + } + + private fun hentVurdertForeldelse(behandlingId: UUID): VurdertForeldelse? { + return vurdertForeldelseRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + } + + private fun finnPerioder( + vurdertForeldelse: VurdertForeldelse?, + vilkårsvurdering: Vilkårsvurdering?, + ): List { + return finnForeldedePerioder(vurdertForeldelse) + finnIkkeForeldedePerioder(vilkårsvurdering) + } + + private fun beregn( + kravgrunnlag: Kravgrunnlag431, + vurdertForeldelse: VurdertForeldelse?, + vilkårsvurdering: Vilkårsvurdering?, + perioderMedBeløp: Map, + beregnRenter: Boolean, + ): List { + return ( + beregnForForeldedePerioder(vurdertForeldelse, perioderMedBeløp) + + beregnForIkkeForeldedePerioder(kravgrunnlag, vilkårsvurdering, perioderMedBeløp, beregnRenter) + ) + .sortedBy { it.periode.fom } + } + + private fun finnIkkeForeldedePerioder(vilkårsvurdering: Vilkårsvurdering?): List { + return vilkårsvurdering?.perioder?.map(Vilkårsvurderingsperiode::periode) + ?: emptyList() + } + + private fun finnForeldedePerioder(vurdertForeldelse: VurdertForeldelse?): List { + return vurdertForeldelse?.foreldelsesperioder + ?.filter(Foreldelsesperiode::erForeldet) + ?.map(Foreldelsesperiode::periode) + ?: emptyList() + } + + private fun beregnForIkkeForeldedePerioder( + kravgrunnlag: Kravgrunnlag431, + vilkårsvurdering: Vilkårsvurdering?, + kravbeløpPerPeriode: Map, + beregnRenter: Boolean, + ): Collection { + return vilkårsvurdering?.perioder + ?.map { beregnIkkeForeldetPeriode(kravgrunnlag, it, kravbeløpPerPeriode, beregnRenter) } + ?: emptyList() + } + + private fun beregnForForeldedePerioder( + vurdertForeldelse: VurdertForeldelse?, + kravbeløpPerPeriode: Map, + ): Collection { + return vurdertForeldelse?.foreldelsesperioder + ?.filter { Foreldelsesvurderingstype.FORELDET == it.foreldelsesvurderingstype } + ?.map { beregnForeldetPeriode(kravbeløpPerPeriode, it) } + ?: emptyList() + } + + private fun beregnForeldetPeriode( + beløpPerPeriode: Map, + foreldelsePeriode: Foreldelsesperiode, + ): Beregningsresultatsperiode { + val periode: Månedsperiode = foreldelsePeriode.periode + val delresultat: FordeltKravgrunnlagsbeløp = + beløpPerPeriode[periode] ?: throw IllegalStateException("Periode i finnes ikke i map beløpPerPeriode") + + return Beregningsresultatsperiode( + periode = periode, + feilutbetaltBeløp = delresultat.feilutbetaltBeløp, + riktigYtelsesbeløp = delresultat.riktigYtelsesbeløp, + utbetaltYtelsesbeløp = delresultat.utbetaltYtelsesbeløp, + tilbakekrevingsbeløp = BigDecimal.ZERO, + tilbakekrevingsbeløpUtenRenter = BigDecimal.ZERO, + rentebeløp = BigDecimal.ZERO, + andelAvBeløp = BigDecimal.ZERO, + vurdering = AnnenVurdering.FORELDET, + skattebeløp = BigDecimal.ZERO, + tilbakekrevingsbeløpEtterSkatt = BigDecimal.ZERO, + ) + } + + private fun beregnIkkeForeldetPeriode( + kravgrunnlag: Kravgrunnlag431, + vurdering: Vilkårsvurderingsperiode, + kravbeløpPerPeriode: Map, + beregnRenter: Boolean, + ): Beregningsresultatsperiode { + val delresultat = kravbeløpPerPeriode[vurdering.periode] + ?: throw IllegalStateException("Periode i finnes ikke i map kravbeløpPerPeriode") + val perioderMedSkattProsent = lagGrunnlagPeriodeMedSkattProsent(vurdering.periode, kravgrunnlag) + + val bruk6desimalerISkatteberegning = featureToggleService.isEnabled(BRUK_6_DESIMALER_I_SKATTEBEREGNING) + return TilbakekrevingsberegningVilkår.beregn( + vurdering, + delresultat, + perioderMedSkattProsent, + beregnRenter, + bruk6desimalerISkatteberegning, + ) + } + + private fun lagGrunnlagPeriodeMedSkattProsent( + vurderingsperiode: Månedsperiode, + kravgrunnlag: Kravgrunnlag431, + ): List { + return kravgrunnlag.perioder + .sortedBy { it.periode.fom } + .map { + it.beløp.map { kgBeløp -> + val maksTilbakekrevesBeløp: BigDecimal = + BeløpsberegningUtil.beregnBeløpForPeriode( + kgBeløp.tilbakekrevesBeløp, + vurderingsperiode, + it.periode, + ) + GrunnlagsperiodeMedSkatteprosent(it.periode, maksTilbakekrevesBeløp, kgBeløp.skatteprosent) + } + }.flatten() + } + + private fun skalBeregneRenter(fagområdekode: Fagområdekode): Boolean = + when (fagområdekode) { + Fagområdekode.BA, Fagområdekode.KS -> false + else -> true + } + + private fun bestemVedtakResultat( + behandlingId: UUID, + tilbakekrevingsbeløp: BigDecimal, + feilutbetaltBeløp: BigDecimal?, + ): Vedtaksresultat { + val behandling: Behandling = behandlingRepository.findByIdOrThrow(behandlingId) + if (Saksbehandlingstype.AUTOMATISK_IKKE_INNKREVING_LAVT_BELØP == behandling.saksbehandlingstype) { + return Vedtaksresultat.INGEN_TILBAKEBETALING + } + if (tilbakekrevingsbeløp.compareTo(BigDecimal.ZERO) == 0) { + return Vedtaksresultat.INGEN_TILBAKEBETALING + } + if (tilbakekrevingsbeløp < feilutbetaltBeløp) { + return Vedtaksresultat.DELVIS_TILBAKEBETALING + } + return Vedtaksresultat.FULL_TILBAKEBETALING + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/TilbakekrevingsberegningVilk\303\245r.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/TilbakekrevingsberegningVilk\303\245r.kt" new file mode 100644 index 000000000..f0d1eb63d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/TilbakekrevingsberegningVilk\303\245r.kt" @@ -0,0 +1,163 @@ +package no.nav.familie.tilbake.beregning + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.beregning.modell.Beregningsresultatsperiode +import no.nav.familie.tilbake.beregning.modell.FordeltKravgrunnlagsbeløp +import no.nav.familie.tilbake.beregning.modell.GrunnlagsperiodeMedSkatteprosent +import no.nav.familie.tilbake.common.isZero +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.AnnenVurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingAktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingGodTro +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsperiode +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vurdering +import java.math.BigDecimal +import java.math.RoundingMode + +internal object TilbakekrevingsberegningVilkår { + + private val HUNDRE_PROSENT: BigDecimal = BigDecimal.valueOf(100) + private val RENTESATS: BigDecimal = BigDecimal.valueOf(10) + private val RENTEFAKTOR: BigDecimal = RENTESATS.divide(HUNDRE_PROSENT, 2, RoundingMode.UNNECESSARY) + + fun beregn( + vilkårVurdering: Vilkårsvurderingsperiode, + delresultat: FordeltKravgrunnlagsbeløp, + perioderMedSkatteprosent: List, + beregnRenter: Boolean, + bruk6desimalerISkatteberegning: Boolean = false, + ): Beregningsresultatsperiode { + val periode: Månedsperiode = vilkårVurdering.periode + val vurdering: Vurdering = finnVurdering(vilkårVurdering) + val renter = beregnRenter && finnRenter(vilkårVurdering) + val andel: BigDecimal? = finnAndelAvBeløp(vilkårVurdering) + val manueltBeløp: BigDecimal? = finnManueltSattBeløp(vilkårVurdering) + val ignoreresPgaLavtBeløp = false == vilkårVurdering.aktsomhet?.tilbakekrevSmåbeløp + val beløpUtenRenter: BigDecimal = + if (ignoreresPgaLavtBeløp) { + BigDecimal.ZERO + } else { + finnBeløpUtenRenter( + delresultat.feilutbetaltBeløp, + andel, + manueltBeløp, + ) + } + val rentebeløp: BigDecimal = beregnRentebeløp(beløpUtenRenter, renter) + val tilbakekrevingBeløp: BigDecimal = beløpUtenRenter.add(rentebeløp) + val skattBeløp: BigDecimal = + beregnSkattBeløp( + periode, + beløpUtenRenter, + perioderMedSkatteprosent, + bruk6desimalerISkatteberegning, + ) + val nettoBeløp: BigDecimal = tilbakekrevingBeløp.subtract(skattBeløp) + return Beregningsresultatsperiode( + periode = periode, + vurdering = vurdering, + renteprosent = if (renter) RENTESATS else null, + feilutbetaltBeløp = delresultat.feilutbetaltBeløp, + riktigYtelsesbeløp = delresultat.riktigYtelsesbeløp, + utbetaltYtelsesbeløp = delresultat.utbetaltYtelsesbeløp, + andelAvBeløp = andel, + manueltSattTilbakekrevingsbeløp = manueltBeløp, + tilbakekrevingsbeløpUtenRenter = beløpUtenRenter, + rentebeløp = rentebeløp, + tilbakekrevingsbeløpEtterSkatt = nettoBeløp, + skattebeløp = skattBeløp, + tilbakekrevingsbeløp = tilbakekrevingBeløp, + ) + } + + private fun beregnRentebeløp(beløp: BigDecimal, renter: Boolean): BigDecimal { + return if (renter) beløp.multiply(RENTEFAKTOR).setScale(0, RoundingMode.DOWN) else BigDecimal.ZERO + } + + private fun beregnSkattBeløp( + periode: Månedsperiode, + bruttoTilbakekrevesBeløp: BigDecimal, + perioderMedSkatteprosent: List, + bruk6desimalerISkatteberegning: Boolean, + ): BigDecimal { + val totalKgTilbakekrevesBeløp: BigDecimal = perioderMedSkatteprosent.sumOf { it.tilbakekrevingsbeløp } + val andel: BigDecimal = if (totalKgTilbakekrevesBeløp.isZero()) { + BigDecimal.ZERO + } else { + bruttoTilbakekrevesBeløp.divide(totalKgTilbakekrevesBeløp, 4, RoundingMode.HALF_UP) + } + var skattBeløp: BigDecimal = BigDecimal.ZERO + for (grunnlagPeriodeMedSkattProsent in perioderMedSkatteprosent) { + if (periode.overlapper(grunnlagPeriodeMedSkattProsent.periode)) { + val scale = if (bruk6desimalerISkatteberegning) 6 else 4 + val delTilbakekrevesBeløp: BigDecimal = grunnlagPeriodeMedSkattProsent.tilbakekrevingsbeløp.multiply(andel) + val beregnetSkattBeløp = delTilbakekrevesBeløp.multiply(grunnlagPeriodeMedSkattProsent.skatteprosent) + .divide(BigDecimal.valueOf(100), scale, RoundingMode.HALF_UP) + skattBeløp = skattBeløp.add(beregnetSkattBeløp).setScale(0, RoundingMode.DOWN) + } + } + return skattBeløp + } + + private fun finnBeløpUtenRenter(kravgrunnlagBeløp: BigDecimal, andel: BigDecimal?, manueltBeløp: BigDecimal?): BigDecimal { + if (manueltBeløp != null) { + return manueltBeløp + } + if (andel != null) { + return kravgrunnlagBeløp.multiply(andel).divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP) + } + throw IllegalArgumentException("Utvikler-feil: Forventer at utledet andel eller manuelt beløp er satt begge manglet") + } + + private fun finnRenter(vurdering: Vilkårsvurderingsperiode): Boolean { + val aktsomhet: VilkårsvurderingAktsomhet? = vurdering.aktsomhet + if (aktsomhet != null) { + val erForsett: Boolean = Aktsomhet.FORSETT == aktsomhet.aktsomhet + return erForsett && (aktsomhet.ileggRenter == null || aktsomhet.ileggRenter) || + aktsomhet.ileggRenter != null && aktsomhet.ileggRenter + } + return false + } + + private fun finnAndelAvBeløp(vurdering: Vilkårsvurderingsperiode): BigDecimal? { + val aktsomhet: VilkårsvurderingAktsomhet? = vurdering.aktsomhet + val godTro: VilkårsvurderingGodTro? = vurdering.godTro + if (aktsomhet != null) { + return finnAndelForAktsomhet(aktsomhet) + } else if (godTro != null && !godTro.beløpErIBehold) { + return BigDecimal.ZERO + } + return null + } + + private fun finnAndelForAktsomhet(aktsomhet: VilkårsvurderingAktsomhet): BigDecimal? { + return if (Aktsomhet.SIMPEL_UAKTSOMHET == aktsomhet.aktsomhet && !aktsomhet.tilbakekrevSmåbeløp) { + BigDecimal.ZERO + } else if (Aktsomhet.FORSETT == aktsomhet.aktsomhet || !aktsomhet.særligeGrunnerTilReduksjon) { + HUNDRE_PROSENT + } else { + aktsomhet.andelTilbakekreves + } + } + + private fun finnManueltSattBeløp(vurdering: Vilkårsvurderingsperiode): BigDecimal? { + val aktsomhet: VilkårsvurderingAktsomhet? = vurdering.aktsomhet + val godTro: VilkårsvurderingGodTro? = vurdering.godTro + if (aktsomhet != null) { + return aktsomhet.manueltSattBeløp + } else if (godTro != null) { + return godTro.beløpTilbakekreves + } + throw IllegalArgumentException("VVurdering skal peke til GodTro-entiet eller Aktsomhet-entitet") + } + + private fun finnVurdering(vurdering: Vilkårsvurderingsperiode): Vurdering { + if (vurdering.aktsomhet != null) { + return vurdering.aktsomhet.aktsomhet + } + if (vurdering.godTro != null) { + return AnnenVurdering.GOD_TRO + } + throw IllegalArgumentException("VVurdering skal peke til GodTro-entiet eller Aktsomhet-entitet") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/Beregningsresultat.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/Beregningsresultat.kt new file mode 100644 index 000000000..0aec5dcae --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/Beregningsresultat.kt @@ -0,0 +1,16 @@ +package no.nav.familie.tilbake.beregning.modell + +import java.math.BigDecimal + +class Beregningsresultat( + val beregningsresultatsperioder: List, + val vedtaksresultat: Vedtaksresultat, +) { + + val totaltTilbakekrevesUtenRenter = beregningsresultatsperioder.sumOf { it.tilbakekrevingsbeløpUtenRenter } + val totaltTilbakekrevesMedRenter = beregningsresultatsperioder.sumOf { it.tilbakekrevingsbeløp } + val totaltRentebeløp = beregningsresultatsperioder.sumOf { it.rentebeløp } + private val totaltSkattetrekk = beregningsresultatsperioder.sumOf { it.skattebeløp } + val totaltTilbakekrevesBeløpMedRenterUtenSkatt: BigDecimal = totaltTilbakekrevesMedRenter.subtract(totaltSkattetrekk) + val totaltFeilutbetaltBeløp = beregningsresultatsperioder.sumOf { it.feilutbetaltBeløp } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/Beregningsresultatsperiode.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/Beregningsresultatsperiode.kt new file mode 100644 index 000000000..8a0703c75 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/Beregningsresultatsperiode.kt @@ -0,0 +1,21 @@ +package no.nav.familie.tilbake.beregning.modell + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vurdering +import java.math.BigDecimal + +data class Beregningsresultatsperiode( + val periode: Månedsperiode, + val vurdering: Vurdering? = null, + val feilutbetaltBeløp: BigDecimal, + val andelAvBeløp: BigDecimal? = null, + val renteprosent: BigDecimal? = null, + val manueltSattTilbakekrevingsbeløp: BigDecimal? = null, + val tilbakekrevingsbeløpUtenRenter: BigDecimal, + val rentebeløp: BigDecimal, + val tilbakekrevingsbeløp: BigDecimal, + val skattebeløp: BigDecimal, + val tilbakekrevingsbeløpEtterSkatt: BigDecimal, + val utbetaltYtelsesbeløp: BigDecimal, // Rått beløp, ikke justert for ev. trekk + val riktigYtelsesbeløp: BigDecimal, +) // Rått beløp, ikke justert for ev. trekk diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/FordeltKravgrunnlagsbel\303\270p.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/FordeltKravgrunnlagsbel\303\270p.kt" new file mode 100644 index 000000000..bc2c6ed6d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/FordeltKravgrunnlagsbel\303\270p.kt" @@ -0,0 +1,9 @@ +package no.nav.familie.tilbake.beregning.modell + +import java.math.BigDecimal + +class FordeltKravgrunnlagsbeløp( + val feilutbetaltBeløp: BigDecimal, + val utbetaltYtelsesbeløp: BigDecimal, + val riktigYtelsesbeløp: BigDecimal, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/GrunnlagsperiodeMedSkatteprosent.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/GrunnlagsperiodeMedSkatteprosent.kt new file mode 100644 index 000000000..8bced36b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/GrunnlagsperiodeMedSkatteprosent.kt @@ -0,0 +1,10 @@ +package no.nav.familie.tilbake.beregning.modell + +import no.nav.familie.kontrakter.felles.Månedsperiode +import java.math.BigDecimal + +class GrunnlagsperiodeMedSkatteprosent( + val periode: Månedsperiode, + val tilbakekrevingsbeløp: BigDecimal, + val skatteprosent: BigDecimal, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/Vedtaksresultat.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/Vedtaksresultat.kt new file mode 100644 index 000000000..dae93c81b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/beregning/modell/Vedtaksresultat.kt @@ -0,0 +1,8 @@ +package no.nav.familie.tilbake.beregning.modell + +enum class Vedtaksresultat(val navn: String) { + // Kun brukes for å sende data til frontend + FULL_TILBAKEBETALING("Tilbakebetaling"), + DELVIS_TILBAKEBETALING("Delvis tilbakebetaling"), + INGEN_TILBAKEBETALING("Ingen tilbakebetaling"), +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/BigDecimalExt.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/BigDecimalExt.kt new file mode 100644 index 000000000..dc89d26aa --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/BigDecimalExt.kt @@ -0,0 +1,11 @@ +package no.nav.familie.tilbake.common + +import java.math.BigDecimal + +fun BigDecimal.isNotZero() = this.signum() != 0 + +fun BigDecimal.isZero() = this.signum() == 0 + +fun BigDecimal.isGreaterThanZero() = this.signum() > 0 + +fun BigDecimal.isLessThanZero() = this.signum() < 0 diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/ContextService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/ContextService.kt new file mode 100644 index 000000000..f8a8afb26 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/ContextService.kt @@ -0,0 +1,195 @@ +package no.nav.familie.tilbake.common + +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.config.RolleConfig +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.InnloggetBrukertilgang +import no.nav.familie.tilbake.sikkerhet.Tilgangskontrollsfagsystem +import no.nav.security.token.support.spring.SpringTokenValidationContextHolder +import org.springframework.http.HttpStatus + +object ContextService { + + private const val SYSTEM_NAVN = "System" + + fun hentSaksbehandler(): String { + return hentPåloggetSaksbehandler(Constants.BRUKER_ID_VEDTAKSLØSNINGEN) + } + + fun hentPåloggetSaksbehandler(defaultverdi: String?): String { + return Result.runCatching { SpringTokenValidationContextHolder().tokenValidationContext } + .fold( + onSuccess = { + return it.getClaims("azuread")?.get("NAVident")?.toString() + ?: defaultverdi + ?: throw Feil("Ingen defaultverdi for bruker ved maskinelt oppslag") + }, + onFailure = { defaultverdi ?: throw Feil("Ingen defaultverdi for bruker ved maskinelt oppslag") }, + ) + } + + fun hentSaksbehandlerNavn(strict: Boolean = false): String { + return Result.runCatching { SpringTokenValidationContextHolder().tokenValidationContext } + .fold( + onSuccess = { + it.getClaims("azuread")?.get("name")?.toString() + ?: if (strict) error("Finner ikke navn i azuread token") else SYSTEM_NAVN + }, + onFailure = { if (strict) error("Finner ikke navn på innlogget bruker") else SYSTEM_NAVN }, + ) + } + + private fun hentGrupper(): List { + return Result.runCatching { SpringTokenValidationContextHolder().tokenValidationContext } + .fold( + onSuccess = { + @Suppress("UNCHECKED_CAST") + it.getClaims("azuread")?.get("groups") as List? ?: emptyList() + }, + onFailure = { emptyList() }, + ) + } + + fun hentHøyesteRolletilgangOgYtelsestypeForInnloggetBruker( + rolleConfig: RolleConfig, + handling: String, + ): InnloggetBrukertilgang { + val saksbehandler = hentSaksbehandler() + val brukerTilganger = mutableMapOf() + if (saksbehandler == Constants.BRUKER_ID_VEDTAKSLØSNINGEN) { + brukerTilganger[Tilgangskontrollsfagsystem.SYSTEM_TILGANG] = Behandlerrolle.SYSTEM + } + val grupper = hentGrupper() + + if (grupper.contains(rolleConfig.beslutterRolleBarnetrygd)) { + brukerTilganger.putAll( + hentTilgangMedRolle( + fagsystem = Tilgangskontrollsfagsystem.BARNETRYGD, + behandlerrolle = Behandlerrolle.BESLUTTER, + brukerTilganger = brukerTilganger, + ), + ) + } + if (grupper.contains(rolleConfig.saksbehandlerRolleBarnetrygd)) { + brukerTilganger.putAll( + hentTilgangMedRolle( + fagsystem = Tilgangskontrollsfagsystem.BARNETRYGD, + behandlerrolle = Behandlerrolle.SAKSBEHANDLER, + brukerTilganger = brukerTilganger, + ), + ) + } + if (grupper.contains(rolleConfig.veilederRolleBarnetrygd)) { + brukerTilganger.putAll( + hentTilgangMedRolle( + fagsystem = Tilgangskontrollsfagsystem.BARNETRYGD, + behandlerrolle = Behandlerrolle.VEILEDER, + brukerTilganger = brukerTilganger, + ), + ) + } + if (grupper.contains(rolleConfig.beslutterRolleEnslig)) { + brukerTilganger.putAll( + hentTilgangMedRolle( + fagsystem = Tilgangskontrollsfagsystem.ENSLIG_FORELDER, + behandlerrolle = Behandlerrolle.BESLUTTER, + brukerTilganger = brukerTilganger, + ), + ) + } + if (grupper.contains(rolleConfig.saksbehandlerRolleEnslig)) { + brukerTilganger.putAll( + hentTilgangMedRolle( + fagsystem = Tilgangskontrollsfagsystem.ENSLIG_FORELDER, + behandlerrolle = Behandlerrolle.SAKSBEHANDLER, + brukerTilganger = brukerTilganger, + ), + ) + } + if (grupper.contains(rolleConfig.veilederRolleEnslig)) { + brukerTilganger.putAll( + hentTilgangMedRolle( + fagsystem = Tilgangskontrollsfagsystem.ENSLIG_FORELDER, + behandlerrolle = Behandlerrolle.VEILEDER, + brukerTilganger = brukerTilganger, + ), + ) + } + if (grupper.contains(rolleConfig.beslutterRolleKontantStøtte)) { + brukerTilganger.putAll( + hentTilgangMedRolle( + fagsystem = Tilgangskontrollsfagsystem.KONTANTSTØTTE, + behandlerrolle = Behandlerrolle.BESLUTTER, + brukerTilganger = brukerTilganger, + ), + ) + } + if (grupper.contains(rolleConfig.saksbehandlerRolleKontantStøtte)) { + brukerTilganger.putAll( + hentTilgangMedRolle( + fagsystem = Tilgangskontrollsfagsystem.KONTANTSTØTTE, + behandlerrolle = Behandlerrolle.SAKSBEHANDLER, + brukerTilganger = brukerTilganger, + ), + ) + } + if (grupper.contains(rolleConfig.veilederRolleKontantStøtte)) { + brukerTilganger.putAll( + hentTilgangMedRolle( + fagsystem = Tilgangskontrollsfagsystem.KONTANTSTØTTE, + behandlerrolle = Behandlerrolle.VEILEDER, + brukerTilganger = brukerTilganger, + ), + ) + } + // forvalter har system tilgang + if (grupper.contains(rolleConfig.forvalterRolleTeamfamilie)) { + brukerTilganger.putAll( + hentTilgangMedRolle( + fagsystem = Tilgangskontrollsfagsystem.FORVALTER_TILGANG, + behandlerrolle = Behandlerrolle.FORVALTER, + brukerTilganger = brukerTilganger, + ), + ) + } + if (brukerTilganger.isEmpty()) { + throw Feil( + message = "Bruker har mangler tilgang til $handling", + frontendFeilmelding = "Bruker har mangler tilgang til $handling", + httpStatus = HttpStatus.FORBIDDEN, + ) + } + + return InnloggetBrukertilgang(brukerTilganger.toMap()) + } + + fun erMaskinTilMaskinToken(): Boolean { + val claims = SpringTokenValidationContextHolder().tokenValidationContext.getClaims("azuread") + return claims.get("oid") != null && + claims.get("oid") == claims.get("sub") && + claims.getAsList("roles").contains("access_as_application") + } + + private fun hentTilgangMedRolle( + fagsystem: Tilgangskontrollsfagsystem, + behandlerrolle: Behandlerrolle, + brukerTilganger: Map, + ): Map { + if (!harBrukerAlleredeHøyereTilgangPåSammeFagssystem(fagsystem, behandlerrolle, brukerTilganger)) { + return mapOf(fagsystem to behandlerrolle) + } + return emptyMap() + } + + private fun harBrukerAlleredeHøyereTilgangPåSammeFagssystem( + fagsystem: Tilgangskontrollsfagsystem, + behandlerrolle: Behandlerrolle, + brukerTilganger: Map, + ): Boolean { + if (brukerTilganger.containsKey(fagsystem)) { + return brukerTilganger[fagsystem]!!.nivå > behandlerrolle.nivå + } + return false + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/DatoUtil.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/DatoUtil.kt new file mode 100644 index 000000000..2a12a013c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/DatoUtil.kt @@ -0,0 +1,9 @@ +package no.nav.familie.tilbake.common + +import java.time.format.DateTimeFormatter +import java.util.Locale + +object DatoUtil { + + val DATO_FORMAT_DATO_MÅNEDSNAVN_ÅR = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale("no")) +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/Grunnbel\303\270p.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/Grunnbel\303\270p.kt" new file mode 100644 index 000000000..369cf6a1b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/Grunnbel\303\270p.kt" @@ -0,0 +1,120 @@ +package no.nav.familie.tilbake.common + +import no.nav.familie.kontrakter.felles.Månedsperiode +import java.math.BigDecimal + +object Grunnbeløpsperioder { + + fun finnGrunnbeløpsperioder(periode: Månedsperiode): List { + require(periode.tom <= grunnbeløpsperioderMaksTom) { + "Har ikke lagt inn grunnbeløpsperiode frem til ${periode.tom}" + } + val perioder = grunnbeløpsperioder.filter { + it.periode.overlapper(periode) + } + require(perioder.isNotEmpty()) { + "Forventer å finne treff for ${periode.fom} - ${periode.tom} i grunnbeløpsperioder" + } + return perioder.sortedBy { it.periode } + } +} + +data class Grunnbeløp( + val periode: Månedsperiode, + val grunnbeløp: BigDecimal, + val perMnd: BigDecimal, + val gjennomsnittPerÅr: BigDecimal? = null, +) + +// Kopiert inn fra https://github.com/navikt/g +private val grunnbeløpsperioder: List = + listOf( + Grunnbeløp( + periode = Månedsperiode("2023-05" to "2024-04"), // Setter ikke MAX for å unngå at grunnbeløpet ikke er oppdatert for neste periode + grunnbeløp = 118_620.toBigDecimal(), + perMnd = 9_885.toBigDecimal(), + gjennomsnittPerÅr = 116239.toBigDecimal(), + ), + Grunnbeløp( + periode = Månedsperiode("2022-05" to "2023-04"), + grunnbeløp = 111_477.toBigDecimal(), + perMnd = 9_290.toBigDecimal(), + gjennomsnittPerÅr = 109_784.toBigDecimal(), + ), + Grunnbeløp( + periode = Månedsperiode("2021-05" to "2022-04"), + grunnbeløp = 106_399.toBigDecimal(), + perMnd = 8_867.toBigDecimal(), + gjennomsnittPerÅr = 104_716.toBigDecimal(), + ), + Grunnbeløp( + periode = Månedsperiode("2020-05" to "2021-04"), + grunnbeløp = 101_351.toBigDecimal(), + perMnd = 8_446.toBigDecimal(), + gjennomsnittPerÅr = 100_853.toBigDecimal(), + ), + Grunnbeløp( + periode = Månedsperiode("2019-05" to "2020-04"), + grunnbeløp = 99_858.toBigDecimal(), + perMnd = 8_322.toBigDecimal(), + gjennomsnittPerÅr = 98_866.toBigDecimal(), + + ), + Grunnbeløp( + periode = Månedsperiode("2018-05" to "2019-04"), + grunnbeløp = 96_883.toBigDecimal(), + perMnd = 8_074.toBigDecimal(), + gjennomsnittPerÅr = 95_800.toBigDecimal(), + ), + Grunnbeløp( + periode = Månedsperiode("2017-05" to "2018-04"), + grunnbeløp = 93_634.toBigDecimal(), + perMnd = 7_803.toBigDecimal(), + gjennomsnittPerÅr = 93_281.toBigDecimal(), + + ), + Grunnbeløp( + periode = Månedsperiode("2016-05" to "2017-04"), + grunnbeløp = 92_576.toBigDecimal(), + perMnd = 7_715.toBigDecimal(), + gjennomsnittPerÅr = 91_740.toBigDecimal(), + + ), + Grunnbeløp( + periode = Månedsperiode("2015-05" to "2016-04"), + grunnbeløp = 90_068.toBigDecimal(), + perMnd = 7_506.toBigDecimal(), + gjennomsnittPerÅr = 89_502.toBigDecimal(), + + ), + Grunnbeløp( + periode = Månedsperiode("2014-05" to "2015-04"), + grunnbeløp = 88_370.toBigDecimal(), + perMnd = 7_364.toBigDecimal(), + gjennomsnittPerÅr = 87_328.toBigDecimal(), + + ), + Grunnbeløp( + periode = Månedsperiode("2013-05" to "2014-04"), + grunnbeløp = 85_245.toBigDecimal(), + perMnd = 7_104.toBigDecimal(), + gjennomsnittPerÅr = 84_204.toBigDecimal(), + + ), + Grunnbeløp( + periode = Månedsperiode("2012-05" to "2013-04"), + grunnbeløp = 82_122.toBigDecimal(), + perMnd = 6_844.toBigDecimal(), + gjennomsnittPerÅr = 81_153.toBigDecimal(), + + ), + Grunnbeløp( + periode = Månedsperiode("2011-05" to "2012-04"), + grunnbeløp = 79_216.toBigDecimal(), + perMnd = 6_601.toBigDecimal(), + gjennomsnittPerÅr = 78_024.toBigDecimal(), + + ), + ) + +private val grunnbeløpsperioderMaksTom = grunnbeløpsperioder.maxOf { it.periode.tom } diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/RessursUtils.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/RessursUtils.kt new file mode 100644 index 000000000..a2cb97880 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/RessursUtils.kt @@ -0,0 +1,44 @@ +package no.nav.familie.tilbake.common + +import no.nav.familie.kontrakter.felles.Ressurs +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity + +object RessursUtils { + + private val LOG = LoggerFactory.getLogger(this::class.java) + private val secureLogger = LoggerFactory.getLogger("secureLogger") + + fun unauthorized(errorMessage: String): ResponseEntity> = + ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Ressurs.failure(errorMessage)) + + fun notFound(errorMessage: String): ResponseEntity> = + errorResponse(HttpStatus.NOT_FOUND, errorMessage, null) + + fun badRequest(errorMessage: String, throwable: Throwable?): ResponseEntity> = + errorResponse(HttpStatus.BAD_REQUEST, errorMessage, throwable) + + fun forbidden(errorMessage: String): ResponseEntity> = + errorResponse(HttpStatus.FORBIDDEN, errorMessage, null) + + fun illegalState(errorMessage: String, throwable: Throwable): ResponseEntity> = + errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, errorMessage, throwable) + + fun ok(data: T): ResponseEntity> = ResponseEntity.ok(Ressurs.success(data)) + fun created(): ResponseEntity> = ResponseEntity.status(HttpStatus.CREATED).build() + + fun noContent(): ResponseEntity> = ResponseEntity.noContent().build() + + private fun errorResponse( + httpStatus: HttpStatus, + errorMessage: String, + throwable: Throwable?, + ): ResponseEntity> { + val className = if (throwable != null) "[${throwable::class.java.name}] " else "" + + secureLogger.error("$className En feil har oppstått: $errorMessage", throwable) + LOG.error("$className En feil har oppstått: $errorMessage") + return ResponseEntity.status(httpStatus).body(Ressurs.failure(errorMessage)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/TaskExtension.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/TaskExtension.kt new file mode 100644 index 000000000..9a7d65795 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/TaskExtension.kt @@ -0,0 +1,6 @@ +package no.nav.familie.tilbake.common + +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.config.PropertyName + +fun Task.fagsystem(): String = this.metadata.getProperty(PropertyName.FAGSYSTEM, "UKJENT") diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/exceptionhandler/ApiExceptionHandler.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/exceptionhandler/ApiExceptionHandler.kt new file mode 100644 index 000000000..65d7b44b7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/exceptionhandler/ApiExceptionHandler.kt @@ -0,0 +1,99 @@ +package no.nav.familie.tilbake.common.exceptionhandler + +import no.nav.familie.kontrakter.felles.Ressurs +import org.slf4j.LoggerFactory +import org.springframework.core.NestedExceptionUtils +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler + +@Suppress("unused") +@ControllerAdvice +class ApiExceptionHandler { + + private val logger = LoggerFactory.getLogger(ApiExceptionHandler::class.java) + private val secureLogger = LoggerFactory.getLogger("secureLogger") + + private fun rootCause(throwable: Throwable): String { + return NestedExceptionUtils.getMostSpecificCause(throwable).javaClass.simpleName + } + + @ExceptionHandler(Throwable::class) + fun handleThrowable(throwable: Throwable): ResponseEntity> { + secureLogger.error("En feil har oppstått", throwable) + logger.error("En feil har oppstått: ${rootCause(throwable)} ") + + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Ressurs.failure(errorMessage = "Uventet feil", frontendFeilmelding = "En uventet feil oppstod.")) + } + + @ExceptionHandler(ApiFeil::class) + fun handleThrowable(feil: ApiFeil): ResponseEntity> { + return ResponseEntity.status(feil.httpStatus).body(Ressurs.failure(frontendFeilmelding = feil.feil)) + } + + @ExceptionHandler(Feil::class) + fun handleThrowable(feil: Feil): ResponseEntity> { + secureLogger.error("En håndtert feil har oppstått(${feil.httpStatus}): ${feil.message}", feil) + logger.info("En håndtert feil har oppstått(${feil.httpStatus}) exception=${rootCause(feil)}: ${feil.message} ") + return ResponseEntity.status(feil.httpStatus).body( + Ressurs.failure( + errorMessage = feil.message, + frontendFeilmelding = feil.frontendFeilmelding, + ), + ) + } + + @ExceptionHandler(ManglerTilgang::class) + fun handleThrowable(manglerTilgang: ManglerTilgang): ResponseEntity> { + secureLogger.error("En håndtert tilgangsfeil har oppstått - ${manglerTilgang.melding}", manglerTilgang) + logger.info("En håndtert tilgangsfeil har oppstått") + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Ressurs.ikkeTilgang(melding = manglerTilgang.melding)) + } + + @ExceptionHandler(IntegrasjonException::class) + fun handleThrowable(feil: IntegrasjonException): ResponseEntity> { + secureLogger.error("Feil i integrasjoner har oppstått: uri={} data={}", feil.uri, feil.data, feil) + logger.error("Feil i integrasjoner har oppstått exception=${rootCause(feil)}") + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Ressurs.failure(frontendFeilmelding = feil.message, error = feil.cause)) + } + + @ExceptionHandler(MethodArgumentNotValidException::class) + fun handleThrowable(feil: MethodArgumentNotValidException): ResponseEntity> { + val feilMelding = StringBuilder() + feil.bindingResult.fieldErrors.forEach { fieldError -> + secureLogger.error( + "Validering feil har oppstått: field={} message={} verdi={}", + fieldError.field, + fieldError.defaultMessage, + fieldError.rejectedValue, + ) + logger.error("Validering feil har oppstått: field={} message={}", fieldError.field, fieldError.defaultMessage) + feilMelding.append(fieldError.defaultMessage) + feilMelding.append(";") + } + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Ressurs.failure(errorMessage = feilMelding.toString(), frontendFeilmelding = feilMelding.toString())) + } + + @ExceptionHandler(UgyldigKravgrunnlagFeil::class) + fun handleThrowable(feil: UgyldigKravgrunnlagFeil): ResponseEntity> { + secureLogger.error("En håndtert feil har oppstått - ${feil.melding}", feil) + logger.info("En håndtert feil har oppstått - ${feil.melding}") + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Ressurs.failure(frontendFeilmelding = feil.message)) + } + + @ExceptionHandler(UgyldigStatusmeldingFeil::class) + fun handleThrowable(feil: UgyldigStatusmeldingFeil): ResponseEntity> { + secureLogger.error("En håndtert feil har oppstått - ${feil.melding}", feil) + logger.info("En håndtert feil har oppstått - ${feil.melding}") + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Ressurs.failure(frontendFeilmelding = feil.message)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/exceptionhandler/Feil.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/exceptionhandler/Feil.kt new file mode 100644 index 000000000..5f0a0ffdd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/exceptionhandler/Feil.kt @@ -0,0 +1,28 @@ +package no.nav.familie.tilbake.common.exceptionhandler + +import org.springframework.http.HttpStatus + +data class ApiFeil(val feil: String, val httpStatus: HttpStatus) : RuntimeException() + +class Feil( + message: String, + val frontendFeilmelding: String? = null, + val httpStatus: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR, + throwable: Throwable? = null, +) : RuntimeException(message, throwable) { + + constructor(message: String, throwable: Throwable?, httpStatus: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR) : + this(message, null, httpStatus, throwable) +} + +class ManglerTilgang(val melding: String) : RuntimeException(melding) + +class UgyldigKravgrunnlagFeil(val melding: String) : RuntimeException(melding) + +class UkjentravgrunnlagFeil(val melding: String) : RuntimeException(melding) + +class UgyldigStatusmeldingFeil(val melding: String) : RuntimeException(melding) + +class SperretKravgrunnlagFeil(val melding: String) : IntegrasjonException(melding) + +class KravgrunnlagIkkeFunnetFeil(val melding: String) : IntegrasjonException(melding) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/exceptionhandler/IntegrasjonException.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/exceptionhandler/IntegrasjonException.kt new file mode 100644 index 000000000..6e93804b6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/exceptionhandler/IntegrasjonException.kt @@ -0,0 +1,10 @@ +package no.nav.familie.tilbake.common.exceptionhandler + +import java.net.URI + +open class IntegrasjonException( + msg: String, + throwable: Throwable? = null, + val uri: URI? = null, + val data: Any? = null, +) : RuntimeException(msg, throwable) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/InsertUpdateRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/InsertUpdateRepository.kt new file mode 100644 index 000000000..6b6cb4247 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/InsertUpdateRepository.kt @@ -0,0 +1,10 @@ +package no.nav.familie.tilbake.common.repository + +interface InsertUpdateRepository { + + fun insert(t: T): T + fun insertAll(list: List): List + + fun update(t: T): T + fun updateAll(list: List): List +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/InsertUpdateRepositoryImpl.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/InsertUpdateRepositoryImpl.kt new file mode 100644 index 000000000..5da3f91a0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/InsertUpdateRepositoryImpl.kt @@ -0,0 +1,29 @@ +package no.nav.familie.tilbake.common.repository + +import org.springframework.data.jdbc.core.JdbcAggregateOperations +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +class InsertUpdateRepositoryImpl(val entityOperations: JdbcAggregateOperations) : InsertUpdateRepository { + + @Transactional + override fun insert(t: T): T { + return entityOperations.insert(t) + } + + @Transactional + override fun insertAll(list: List): List { + return list.map(this::insert) + } + + @Transactional + override fun update(t: T): T { + return entityOperations.update(t) + } + + @Transactional + override fun updateAll(list: List): List { + return list.map(this::update) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/RepositoryExtension.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/RepositoryExtension.kt new file mode 100644 index 000000000..ae4897b23 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/RepositoryExtension.kt @@ -0,0 +1,8 @@ +package no.nav.familie.tilbake.common.repository + +import org.springframework.data.repository.CrudRepository +import org.springframework.data.repository.findByIdOrNull + +inline fun CrudRepository.findByIdOrThrow(id: ID): T { + return findByIdOrNull(id) ?: throw IllegalStateException("Finner ikke ${T::class.simpleName} med id=$id") +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/RepositoryInterface.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/RepositoryInterface.kt new file mode 100644 index 000000000..061515350 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/RepositoryInterface.kt @@ -0,0 +1,21 @@ +package no.nav.familie.tilbake.common.repository + +import org.springframework.data.repository.CrudRepository +import org.springframework.data.repository.NoRepositoryBean + +/** + * På grunn av att vi setter id's på våre entitetet så prøver spring å oppdatere våre entiteter i stedet for å ta insert + */ +@NoRepositoryBean +interface RepositoryInterface : CrudRepository { + + @Deprecated("Støttes ikke, bruk insert/update") + override fun save(entity: S): S { + error("Not implemented - Use InsertUpdateRepository - insert/update") + } + + @Deprecated("Støttes ikke, bruk insertAll/updateAll") + override fun saveAll(entities: Iterable): Iterable { + error("Not implemented - Use InsertUpdateRepository - insertAll/updateAll") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/Sporbar.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/Sporbar.kt new file mode 100644 index 000000000..0f9b69675 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/common/repository/Sporbar.kt @@ -0,0 +1,25 @@ +package no.nav.familie.tilbake.common.repository + +import no.nav.familie.tilbake.common.ContextService +import org.springframework.data.annotation.LastModifiedBy +import org.springframework.data.relational.core.mapping.Embedded +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +data class Sporbar( + val opprettetAv: String = ContextService.hentSaksbehandler(), + val opprettetTid: LocalDateTime = SporbarUtils.now(), + @LastModifiedBy + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val endret: Endret = Endret(), +) + +data class Endret( + val endretAv: String = ContextService.hentSaksbehandler(), + val endretTid: LocalDateTime = SporbarUtils.now(), +) + +object SporbarUtils { + + fun now(): LocalDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.MILLIS) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/ApplicationConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/ApplicationConfig.kt new file mode 100644 index 000000000..998da078c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/ApplicationConfig.kt @@ -0,0 +1,116 @@ +package no.nav.familie.tilbake.config + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import no.nav.familie.http.client.RetryOAuth2HttpClient +import no.nav.familie.http.config.RestTemplateAzure +import no.nav.familie.kafka.KafkaErrorHandler +import no.nav.familie.log.filter.LogFilter +import no.nav.familie.prosessering.config.ProsesseringInfoProvider +import no.nav.security.token.support.client.core.http.OAuth2HttpClient +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse +import no.nav.security.token.support.client.spring.oauth2.EnableOAuth2Client +import no.nav.security.token.support.spring.SpringTokenValidationContextHolder +import no.nav.security.token.support.spring.api.EnableJwtTokenValidation +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.context.properties.ConfigurationPropertiesScan +import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory +import org.springframework.boot.web.servlet.FilterRegistrationBean +import org.springframework.boot.web.servlet.server.ServletWebServerFactory +import org.springframework.cache.annotation.EnableCaching +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Import +import org.springframework.context.annotation.Primary +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter +import org.springframework.scheduling.annotation.EnableScheduling +import org.springframework.web.client.RestTemplate +import java.time.Duration +import java.time.temporal.ChronoUnit + +@SpringBootConfiguration +@ComponentScan(ApplicationConfig.pakkenavn, "no.nav.familie.sikkerhet", "no.nav.familie.prosessering", "no.nav.familie.unleash") +@EnableJwtTokenValidation(ignore = ["org.springframework", "org.springdoc"]) +@Import(RestTemplateAzure::class, KafkaErrorHandler::class) +@EnableOAuth2Client(cacheEnabled = true) +@EnableScheduling +@EnableCaching +@ConfigurationPropertiesScan +class ApplicationConfig { + + @Bean + fun servletWebServerFactory(): ServletWebServerFactory { + val serverFactory = JettyServletWebServerFactory() + serverFactory.port = 8030 + return serverFactory + } + + @Bean + fun logFilter(): FilterRegistrationBean { + val filterRegistration = FilterRegistrationBean() + filterRegistration.filter = LogFilter() + filterRegistration.order = 1 + return filterRegistration + } + + @Bean + fun kotlinModule(): KotlinModule = KotlinModule() + + /** + * Overskriver felles sin som bruker proxy, som ikke skal brukes på gcp. + */ + @Bean + @Primary + fun restTemplateBuilder(objectMapper: ObjectMapper): RestTemplateBuilder { + val jackson2HttpMessageConverter = MappingJackson2HttpMessageConverter(objectMapper) + return RestTemplateBuilder() + .setConnectTimeout(Duration.of(2, ChronoUnit.SECONDS)) + .setReadTimeout(Duration.of(30, ChronoUnit.SECONDS)) + .additionalMessageConverters(listOf(jackson2HttpMessageConverter) + RestTemplate().messageConverters) + } + + /** + * Overskriver OAuth2HttpClient som settes opp i token-support som ikke kan få med objectMapper fra felles + * pga. .setVisibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.NONE) + * og [OAuth2AccessTokenResponse] som burde settes med setters, då feltnavn heter noe annet enn feltet i json + */ + @Bean + @Primary + fun oAuth2HttpClient(): OAuth2HttpClient { + return RetryOAuth2HttpClient( + RestTemplateBuilder() + .setConnectTimeout(Duration.of(2, ChronoUnit.SECONDS)) + .setReadTimeout(Duration.of(4, ChronoUnit.SECONDS)), + ) + } + + @Bean + fun prosesseringInfoProvider(@Value("\${rolle.prosessering}") prosesseringRolle: String) = object : + ProsesseringInfoProvider { + + override fun hentBrukernavn(): String = try { + SpringTokenValidationContextHolder().tokenValidationContext.getClaims("azuread") + .getStringClaim("preferred_username") + } catch (e: Exception) { + throw e + } + + override fun harTilgang(): Boolean = grupper().contains(prosesseringRolle) + + private fun grupper(): List { + return try { + SpringTokenValidationContextHolder().tokenValidationContext.getClaims("azuread") + ?.get("groups") as List? ?: emptyList() + } catch (e: Exception) { + emptyList() + } + } + } + + companion object { + + const val pakkenavn = "no.nav.familie.tilbake" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/Constants.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/Constants.kt new file mode 100644 index 000000000..2f773af58 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/Constants.kt @@ -0,0 +1,54 @@ +package no.nav.familie.tilbake.config + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import java.math.BigDecimal +import java.time.LocalDate +import java.time.Period + +object Constants { + + private val rettsgebyrForDato = listOf( + Datobeløp(LocalDate.of(2021, 1, 1), 1199), + Datobeløp(LocalDate.of(2022, 1, 1), 1223), + Datobeløp(LocalDate.of(2023, 1, 1), 1243), + ) + + private val brukersSvarfrist: Period = Period.ofWeeks(2) + + fun brukersSvarfrist(): LocalDate = LocalDate.now().plus(brukersSvarfrist) + + fun saksbehandlersTidsfrist(): LocalDate = LocalDate.now() + .plusWeeks(Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING.defaultVenteTidIUker) + + const val kravgrunnlagXmlRootElement: String = "urn:detaljertKravgrunnlagMelding" + + const val statusmeldingXmlRootElement: String = "urn:endringKravOgVedtakstatus" + + val rettsgebyr = rettsgebyrForDato.filter { it.gyldigFra <= LocalDate.now() }.maxByOrNull { it.gyldigFra }!!.beløp + + private class Datobeløp(val gyldigFra: LocalDate, val beløp: Long) + + const val BRUKER_ID_VEDTAKSLØSNINGEN = "VL" + + val MAKS_FEILUTBETALTBELØP_PER_YTELSE = + mapOf( + Ytelsestype.BARNETRYGD to BigDecimal.valueOf(500), + Ytelsestype.BARNETILSYN to BigDecimal.valueOf(rettsgebyr).multiply(BigDecimal(0.5)), + Ytelsestype.OVERGANGSSTØNAD to BigDecimal.valueOf(rettsgebyr) + .multiply(BigDecimal(0.5)), + Ytelsestype.SKOLEPENGER to BigDecimal.valueOf(rettsgebyr) + .multiply(BigDecimal(0.5)), + Ytelsestype.KONTANTSTØTTE to BigDecimal.valueOf(rettsgebyr) + .multiply(BigDecimal(0.5)), + ) + + const val AUTOMATISK_SAKSBEHANDLING_BEGUNNLESE = "Automatisk satt verdi" +} + +object PropertyName { + + const val FAGSYSTEM = "fagsystem" + const val ENHET = "enhet" + const val BESLUTTER = "beslutter" +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/DatabaseConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/DatabaseConfig.kt new file mode 100644 index 000000000..a927268c0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/DatabaseConfig.kt @@ -0,0 +1,99 @@ +package no.nav.familie.tilbake.config + +import no.nav.familie.prosessering.PropertiesWrapperTilStringConverter +import no.nav.familie.prosessering.StringTilPropertiesWrapperConverter +import no.nav.familie.tilbake.common.repository.Endret +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.convert.converter.Converter +import org.springframework.data.convert.ReadingConverter +import org.springframework.data.convert.WritingConverter +import org.springframework.data.domain.AuditorAware +import org.springframework.data.jdbc.core.convert.JdbcCustomConversions +import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration +import org.springframework.data.jdbc.repository.config.EnableJdbcAuditing +import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate +import org.springframework.jdbc.datasource.DataSourceTransactionManager +import org.springframework.transaction.PlatformTransactionManager +import java.sql.Date +import java.time.LocalDate +import java.time.YearMonth +import java.util.Optional +import javax.sql.DataSource + +@Configuration +@EnableJdbcAuditing +@EnableJdbcRepositories("no.nav.familie.tilbake", "no.nav.familie.prosessering") +class DatabaseConfig : AbstractJdbcConfiguration() { + + @Bean + fun operations(dataSource: DataSource): NamedParameterJdbcOperations { + return NamedParameterJdbcTemplate(dataSource) + } + + @Bean + fun transactionManager(dataSource: DataSource): PlatformTransactionManager { + return DataSourceTransactionManager(dataSource) + } + + @Bean + fun auditSporbarEndret(): AuditorAware { + return AuditorAware { + Optional.of(Endret()) + } + } + + @Bean + override fun jdbcCustomConversions(): JdbcCustomConversions { + return JdbcCustomConversions( + listOf( + KravstatuskodeLesConverter(), + KravstatuskodeSkrivConverter(), + YearMonthTilLocalDateConverter(), + LocalDateTilYearMonthConverter(), + StringTilPropertiesWrapperConverter(), + PropertiesWrapperTilStringConverter(), + ), + ) + } + + @ReadingConverter + class KravstatuskodeLesConverter : Converter { + + override fun convert(kode: String): Kravstatuskode { + return Kravstatuskode.fraKode(kode) + } + } + + @WritingConverter + class KravstatuskodeSkrivConverter : Converter { + + override fun convert(kravstatuskode: Kravstatuskode): String { + return kravstatuskode.kode + } + } + + @WritingConverter + class YearMonthTilLocalDateConverter : Converter { + + override fun convert(yearMonth: YearMonth): LocalDate { + return yearMonth.let { + LocalDate.of(it.year, it.month, 1) + } + } + } + + @ReadingConverter + class LocalDateTilYearMonthConverter : Converter { + + override fun convert(date: Date): YearMonth { + return date.let { + val localDate = date.toLocalDate() + YearMonth.of(localDate.year, localDate.month) + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/FeatureToggleConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/FeatureToggleConfig.kt new file mode 100644 index 000000000..0f3840bd9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/FeatureToggleConfig.kt @@ -0,0 +1,54 @@ +package no.nav.familie.tilbake.config + +import io.getunleash.strategy.Strategy +import no.nav.familie.unleash.UnleashService +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Service + +@Configuration +class FeatureToggleConfig(@Value("\${NAIS_CLUSTER_NAME}") private val clusterName: String) { + + @Bean + fun strategies(): List { + return listOf(ByClusterStrategy(clusterName)) + } + + companion object { + + const val KAN_OPPRETTE_BEH_MED_EKSTERNID_SOM_HAR_AVSLUTTET_TBK = + "familie-tilbake.beh.kanopprettes.eksternid.avsluttet.tilbakekreving" + + const val OVERSTYR_DELVILS_TILBAKEKREVING_TIL_FULL_TILBAKEKREVING = "familie-tilbake.overstyr-delvis-hvis-full" + + const val BRUK_6_DESIMALER_I_SKATTEBEREGNING = "familie-tilbake.bruk-seks-desimaler-skatt" + + const val IKKE_VALIDER_SÆRLIG_GRUNNET_ANNET_FRITEKST = + "familie-tilbake.ikke-valider-saerlig-grunnet-annet-fritekst" + } +} + +@Service +@Profile("!integrasjonstest") +class FeatureToggleService(val unleashService: UnleashService) { + + fun isEnabled(toggleId: String): Boolean { + return unleashService.isEnabled(toggleId, false) + } + + fun isEnabled(toggleId: String, defaultValue: Boolean): Boolean { + return unleashService.isEnabled(toggleId, defaultValue) + } +} + +class ByClusterStrategy(private val clusterName: String) : Strategy { + + override fun isEnabled(parameters: MutableMap): Boolean { + if (parameters.isEmpty()) return false + return parameters["cluster"]?.contains(clusterName) ?: false + } + + override fun getName(): String = "byCluster" +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/FlywayConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/FlywayConfig.kt new file mode 100644 index 000000000..d3aa5789f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/FlywayConfig.kt @@ -0,0 +1,31 @@ +package no.nav.familie.tilbake.config + +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.flyway.FlywayConfigurationCustomizer +import org.springframework.context.annotation.Bean +import org.springframework.core.env.Environment + +data class FlywayConfig(private val role: String) { + + private val logger = LoggerFactory.getLogger(this::class.java) + + @Bean + fun flywayConfig( + @Value("\${spring.flyway.placeholders.ignoreIfProd}") ignoreIfProd: String, + environment: Environment, + ): FlywayConfigurationCustomizer { + logger.info("DB-oppdateringer kjøres med rolle $role") + val isProd = environment.activeProfiles.contains("prod") + val ignore = ignoreIfProd == "--" + return FlywayConfigurationCustomizer { + it.initSql(String.format("SET ROLE \"%s\"", role)) + if (isProd && !ignore) { + throw RuntimeException("Prod profile-en har ikke riktig verdi for placeholder ignoreIfProd=$ignoreIfProd") + } + if (!isProd && ignore) { + throw RuntimeException("Profile=${environment.activeProfiles} har ignoreIfProd=false") + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/IntegrasjonerConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/IntegrasjonerConfig.kt new file mode 100644 index 000000000..7c314074b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/IntegrasjonerConfig.kt @@ -0,0 +1,28 @@ +package no.nav.familie.tilbake.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Configuration +import java.net.URI + +@Configuration +class IntegrasjonerConfig( + @Value("\${FAMILIE_INTEGRASJONER_URL}") val integrasjonUri: URI, + @Value("\${application.name}") val applicationName: String, +) { + + companion object { + + const val PATH_PING = "internal/status/isAlive" + const val PATH_ORGANISASJON = "api/organisasjon" + const val PATH_SAKSBEHANDLER = "api/saksbehandler" + const val PATH_TILGANGSSJEKK = "api/tilgang/personer" + const val PATH_ARKIVER = "api/arkiv/v4" + const val PATH_DISTRIBUER = "api/dist/v1" + const val PATH_SFTP = "api/adramatch/avstemming" + const val PATH_OPPGAVE = "api/oppgave" + const val PATH_NAVKONTOR = "api/arbeidsfordeling/nav-kontor" + + const val PATH_JOURNALPOST = "api/journalpost" + const val PATH_HENTDOKUMENT = "api/journalpost/hentdokument" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/KafkaConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/KafkaConfig.kt new file mode 100644 index 000000000..30f8c81e6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/KafkaConfig.kt @@ -0,0 +1,100 @@ +package no.nav.familie.tilbake.config + +import no.nav.familie.kafka.KafkaErrorHandler +import no.nav.familie.kontrakter.felles.Applikasjon +import org.apache.kafka.clients.CommonClientConfigs +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.config.SslConfigs +import org.apache.kafka.common.serialization.StringDeserializer +import org.apache.kafka.common.serialization.StringSerializer +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import org.springframework.kafka.annotation.EnableKafka +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory +import org.springframework.kafka.core.ConsumerFactory +import org.springframework.kafka.core.DefaultKafkaConsumerFactory +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.core.ProducerFactory +import org.springframework.kafka.listener.ContainerProperties + +@Configuration +@EnableKafka +@Profile("dev", "prod") +class KafkaConfig( + @Value("\${KAFKA_BROKERS:localhost}") private val kafkaBrokers: String, + @Value("\${KAFKA_TRUSTSTORE_PATH}") private val kafkaTruststorePath: String, + @Value("\${KAFKA_CREDSTORE_PASSWORD}") private val kafkaCredstorePassword: String, + @Value("\${KAFKA_KEYSTORE_PATH}") private val kafkaKeystorePath: String, +) { + + @Bean + fun producerFactory(): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs()) + } + + @Bean + fun kafkaTemplate(): KafkaTemplate { + return KafkaTemplate(producerFactory()) + } + + @Bean + fun consumerFactory(): ConsumerFactory { + return DefaultKafkaConsumerFactory(consumerConfigs()) + } + + @Bean + fun concurrentKafkaListenerContainerFactory(kafkaErrorHandler: KafkaErrorHandler): ConcurrentKafkaListenerContainerFactory { + val factory = ConcurrentKafkaListenerContainerFactory() + factory.setConcurrency(1) + factory.containerProperties.ackMode = ContainerProperties.AckMode.MANUAL + factory.consumerFactory = consumerFactory() + factory.setCommonErrorHandler(kafkaErrorHandler) + return factory + } + + private fun producerConfigs() = mapOf( + ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to kafkaBrokers, + ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, + ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG to true, // Den sikrer rekkefølge + ProducerConfig.ACKS_CONFIG to "all", // Den sikrer at data ikke mistes + ProducerConfig.CLIENT_ID_CONFIG to Applikasjon.FAMILIE_TILBAKE.name, + ) + securityConfig() + + fun consumerConfigs() = mapOf( + ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to kafkaBrokers, + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, + ConsumerConfig.GROUP_ID_CONFIG to "familie-tilbake", + ConsumerConfig.CLIENT_ID_CONFIG to "consumer-familie-tilbake-1", + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "latest", + CommonClientConfigs.RETRIES_CONFIG to 10, + CommonClientConfigs.RETRY_BACKOFF_MS_CONFIG to 100, + ) + securityConfig() + + private fun securityConfig() = + mapOf( + CommonClientConfigs.SECURITY_PROTOCOL_CONFIG to "SSL", + SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG to "", // Disable server host name verification + SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG to "JKS", + SslConfigs.SSL_KEYSTORE_TYPE_CONFIG to "PKCS12", + SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG to kafkaTruststorePath, + SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG to kafkaCredstorePassword, + SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG to kafkaKeystorePath, + SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG to kafkaCredstorePassword, + SslConfigs.SSL_KEY_PASSWORD_CONFIG to kafkaCredstorePassword, + ) + + companion object { + + const val HISTORIKK_TOPIC = "teamfamilie.privat-historikk-topic" + const val HENT_FAGSYSTEMSBEHANDLING_REQUEST_TOPIC = "teamfamilie.privat-tbk-hentfagsystemsbehandling-request-topic" + const val HENT_FAGSYSTEMSBEHANDLING_RESPONS_TOPIC = "teamfamilie.privat-tbk-hentfagsystemsbehandling-respons-topic" + const val SAK_TOPIC = "teamfamilie.aapen-tbk-datavarehus-sak-topic" + const val VEDTAK_TOPIC = "teamfamilie.aapen-tbk-datavarehus-vedtak-topic" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/OppdragMQConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/OppdragMQConfig.kt new file mode 100644 index 000000000..5f1ea0183 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/OppdragMQConfig.kt @@ -0,0 +1,85 @@ +package no.nav.familie.tilbake.config + +import com.ibm.mq.constants.CMQC +import com.ibm.mq.jakarta.jms.MQQueueConnectionFactory +import com.ibm.msg.client.jakarta.jms.JmsConstants +import com.ibm.msg.client.jakarta.wmq.common.CommonConstants +import jakarta.jms.ConnectionFactory +import jakarta.jms.JMSException +import org.messaginghub.pooled.jms.JmsPoolConnectionFactory +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import org.springframework.core.env.Environment +import org.springframework.jms.config.DefaultJmsListenerContainerFactory +import org.springframework.jms.config.JmsListenerContainerFactory +import org.springframework.jms.connection.JmsTransactionManager +import org.springframework.jms.connection.UserCredentialsConnectionFactoryAdapter + +private const val UTF_8_WITH_PUA = 1208 + +@Configuration +@Profile("!integrasjonstest") +class OppdragMQConfig( + @Value("\${oppdrag.mq.hostname}") val hostname: String, + @Value("\${oppdrag.mq.queuemanager}") val queuemanager: String, + @Value("\${oppdrag.mq.channel}") val channel: String, + @Value("\${oppdrag.mq.port}") val port: Int, + @Value("\${CREDENTIAL_USERNAME}") val user: String, + @Value("\${CREDENTIAL_PASSWORD}") val password: String, + val environment: Environment, +) { + + private val logger = LoggerFactory.getLogger(OppdragMQConfig::class.java) + + @Bean + @Throws(JMSException::class) + fun mqQueueConnectionFactory(): JmsPoolConnectionFactory { + val targetFactory = MQQueueConnectionFactory() + targetFactory.hostName = hostname + targetFactory.queueManager = queuemanager + targetFactory.channel = channel + targetFactory.port = port + targetFactory.transportType = CommonConstants.WMQ_CM_CLIENT + targetFactory.ccsid = UTF_8_WITH_PUA + targetFactory.setIntProperty(JmsConstants.JMS_IBM_ENCODING, CMQC.MQENC_NATIVE) + targetFactory.setBooleanProperty(JmsConstants.USER_AUTHENTICATION_MQCSP, true) + targetFactory.setIntProperty(JmsConstants.JMS_IBM_CHARACTER_SET, UTF_8_WITH_PUA) + + val cf = UserCredentialsConnectionFactoryAdapter() + cf.setUsername(user) + cf.setPassword(password) + cf.setTargetConnectionFactory(targetFactory) + + val pooledFactory = JmsPoolConnectionFactory() + pooledFactory.connectionFactory = cf + pooledFactory.maxConnections = 10 + pooledFactory.maxSessionsPerConnection = 10 + + logger.info("MQ bruker $user") + + return pooledFactory + } + + @Bean + fun jmsListenerContainerFactory( + @Qualifier("mqQueueConnectionFactory") connectionFactory: ConnectionFactory, + configurer: DefaultJmsListenerContainerFactoryConfigurer, + ): JmsListenerContainerFactory<*> { + val factory = DefaultJmsListenerContainerFactory() + configurer.configure(factory, connectionFactory) + + val transactionManager = JmsTransactionManager() + transactionManager.connectionFactory = connectionFactory + factory.setTransactionManager(transactionManager) + factory.setSessionTransacted(true) + if (environment.activeProfiles.any { it.contains("local") }) { + factory.setAutoStartup(false) + } + return factory + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/PdlConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/PdlConfig.kt new file mode 100644 index 000000000..6ccea1cd8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/PdlConfig.kt @@ -0,0 +1,30 @@ +package no.nav.familie.tilbake.config + +import org.apache.commons.lang3.StringUtils +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Configuration +import org.springframework.web.util.UriComponentsBuilder +import java.net.URI + +@Configuration +class PdlConfig(@Value("\${PDL_URL}") pdlUrl: URI) { + + val pdlUri: URI = UriComponentsBuilder.fromUri(pdlUrl).path(PATH_GRAPHQL).build().toUri() + + companion object { + + const val PATH_GRAPHQL = "graphql" + + val hentEnkelPersonQuery = graphqlQuery("hentperson-enkel") + val hentIdenterQuery = graphqlQuery("hentIdenter") + val hentAdressebeskyttelseBolkQuery = graphqlQuery("hent-adressebeskyttelse-bolk") + + private fun graphqlQuery(pdlResource: String) = PdlConfig::class.java.getResource("/pdl/$pdlResource.graphql") + .readText() + .graphqlCompatible() + + private fun String.graphqlCompatible(): String { + return StringUtils.normalizeSpace(this.replace("\n", "")) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/RolleConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/RolleConfig.kt new file mode 100644 index 000000000..173cc1511 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/RolleConfig.kt @@ -0,0 +1,28 @@ +package no.nav.familie.tilbake.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Configuration + +@Configuration +class RolleConfig( + @Value("\${rolle.barnetrygd.beslutter}") + val beslutterRolleBarnetrygd: String, + @Value("\${rolle.barnetrygd.saksbehandler}") + val saksbehandlerRolleBarnetrygd: String, + @Value("\${rolle.barnetrygd.veileder}") + val veilederRolleBarnetrygd: String, + @Value("\${rolle.enslig.beslutter}") + val beslutterRolleEnslig: String, + @Value("\${rolle.enslig.saksbehandler}") + val saksbehandlerRolleEnslig: String, + @Value("\${rolle.enslig.veileder}") + val veilederRolleEnslig: String, + @Value("\${rolle.kontantstøtte.beslutter}") + val beslutterRolleKontantStøtte: String, + @Value("\${rolle.kontantstøtte.saksbehandler}") + val saksbehandlerRolleKontantStøtte: String, + @Value("\${rolle.kontantstøtte.veileder}") + val veilederRolleKontantStøtte: String, + @Value("\${rolle.teamfamilie.forvalter}") + val forvalterRolleTeamfamilie: String, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/SwaggerConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/SwaggerConfig.kt new file mode 100644 index 000000000..b7b3b92c8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/config/SwaggerConfig.kt @@ -0,0 +1,47 @@ +package no.nav.familie.tilbake.config + +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Info +import io.swagger.v3.oas.models.security.OAuthFlow +import io.swagger.v3.oas.models.security.OAuthFlows +import io.swagger.v3.oas.models.security.Scopes +import io.swagger.v3.oas.models.security.SecurityRequirement +import io.swagger.v3.oas.models.security.SecurityScheme +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class SwaggerConfig( + @Value("\${AUTHORIZATION_URL}") + val authorizationUrl: String, + @Value("\${TOKEN_URL}") + val tokenUrl: String, + @Value("\${API_SCOPE}") + val apiScope: String, +) { + + @Bean + fun tilbakekrevingApi(): OpenAPI { + return OpenAPI().info(Info().title("Tilbakekreving API").version("v1")) + .components(Components().addSecuritySchemes("oauth2", securitySchemes())) + .addSecurityItem(SecurityRequirement().addList("oauth2", listOf("read", "write"))) + } + + private fun securitySchemes(): SecurityScheme { + return SecurityScheme() + .name("oauth2") + .type(SecurityScheme.Type.OAUTH2) + .scheme("oauth2") + .`in`(SecurityScheme.In.HEADER) + .flows( + OAuthFlows() + .authorizationCode( + OAuthFlow().authorizationUrl(authorizationUrl) + .tokenUrl(tokenUrl) + .scopes(Scopes().addString(apiScope, "read,write")), + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/BehandlingTilstandService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/BehandlingTilstandService.kt new file mode 100644 index 000000000..5a7fbef0a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/BehandlingTilstandService.kt @@ -0,0 +1,124 @@ +package no.nav.familie.tilbake.datavarehus.saksstatistikk + +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.tilbakekreving.Periode +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.FagsystemUtil +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.datavarehus.saksstatistikk.sakshendelse.Behandlingstilstand +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingService +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.util.Properties +import java.util.UUID + +@Service +@Transactional +class BehandlingTilstandService( + private val behandlingRepository: BehandlingRepository, + private val behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository, + private val fagsakRepository: FagsakRepository, + private val taskService: TaskService, + private val faktaFeilutbetalingService: FaktaFeilutbetalingService, +) { + + fun opprettSendingAvBehandlingensTilstand(behandlingId: UUID, info: Behandlingsstegsinfo) { + val hendelsesbeskrivelse = "Ny behandlingsstegstilstand " + + "${info.behandlingssteg}:${info.behandlingsstegstatus} " + + "for behandling $behandlingId" + + val tilstand = hentBehandlingensTilstand(behandlingId) + opprettProsessTask(behandlingId, tilstand, hendelsesbeskrivelse) + } + + fun opprettSendingAvBehandlingenHenlagt(behandlingId: UUID) { + val hendelsesbeskrivelse = "Henlegger behandling $behandlingId" + + val tilstand = hentBehandlingensTilstand(behandlingId) + opprettProsessTask(behandlingId, tilstand, hendelsesbeskrivelse) + } + + private fun opprettProsessTask(behandlingId: UUID, behandlingstilstand: Behandlingstilstand, hendelsesbeskrivelse: String) { + val task = Task( + SendSakshendelseTilDvhTask.TASK_TYPE, + behandlingId.toString(), + Properties().apply { + setProperty("behandlingstilstand", objectMapper.writeValueAsString(behandlingstilstand)) + setProperty("beskrivelse", hendelsesbeskrivelse) + setProperty( + PropertyName.FAGSYSTEM, + FagsystemUtil.hentFagsystemFraYtelsestype(behandlingstilstand.ytelsestype).name, + ) + }, + ) + taskService.save(task) + } + + fun hentBehandlingensTilstand(behandlingId: UUID): Behandlingstilstand { + val behandling: Behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val eksternBehandling = behandling.aktivFagsystemsbehandling.eksternId + val behandlingsresultat = behandling.sisteResultat?.type ?: Behandlingsresultatstype.IKKE_FASTSATT + val behandlingsstegstilstand = behandlingsstegstilstandRepository + .findByBehandlingIdAndBehandlingsstegsstatusIn(behandlingId, Behandlingsstegstatus.aktiveStegStatuser) + val venterPåBruker: Boolean = Venteårsak.venterPåBruker(behandlingsstegstilstand?.venteårsak) + val venterPåØkonomi: Boolean = Venteårsak.venterPåØkonomi(behandlingsstegstilstand?.venteårsak) + val behandlingsårsak = behandling.årsaker.firstOrNull() + val forrigeBehandling = behandlingsårsak?.originalBehandlingId?.let { behandlingRepository.findByIdOrNull(it) } + + var totalFeilutbetaltPeriode: Periode? = null + var totalFeilutbetaltBeløp: BigDecimal? = null + val erBehandlingsstegEtterGrunnlagSteg = + behandlingsstegstilstand?.behandlingssteg?.sekvens?.let { it > Behandlingssteg.GRUNNLAG.sekvens } ?: false + val erBehandlingHenlagt = behandling.sisteResultat?.erBehandlingHenlagt() ?: false + if (erBehandlingsstegEtterGrunnlagSteg) { + val fakta = faktaFeilutbetalingService.hentFaktaomfeilutbetaling(behandlingId) + totalFeilutbetaltPeriode = Periode(fakta.totalFeilutbetaltPeriode.fom, fakta.totalFeilutbetaltPeriode.tom) + totalFeilutbetaltBeløp = fakta.totaltFeilutbetaltBeløp + } else if (behandlingsstegstilstand?.behandlingssteg == Behandlingssteg.VARSEL && !erBehandlingHenlagt) { + val varsel = behandling.aktivtVarsel ?: throw Feil("Behandling $behandlingId venter på varselssteg uten varsel data") + val førsteDagIVarselsperiode = varsel.perioder.minOf { it.fom } + val sisteDagIVarselsperiode = varsel.perioder.maxOf { it.tom } + + totalFeilutbetaltBeløp = varsel.varselbeløp.toBigDecimal() + totalFeilutbetaltPeriode = Periode(førsteDagIVarselsperiode, sisteDagIVarselsperiode) + } + + return Behandlingstilstand( + ytelsestype = fagsak.ytelsestype, + saksnummer = fagsak.eksternFagsakId, + behandlingUuid = behandling.eksternBrukId, + referertFagsaksbehandling = eksternBehandling, + behandlingstype = behandling.type, + behandlingsstatus = behandling.status, + behandlingsresultat = behandlingsresultat, + ansvarligEnhet = behandling.behandlendeEnhet, + ansvarligBeslutter = behandling.ansvarligBeslutter, + ansvarligSaksbehandler = behandling.ansvarligSaksbehandler, + behandlingErManueltOpprettet = behandling.manueltOpprettet, + funksjoneltTidspunkt = OffsetDateTime.now(ZoneOffset.UTC), + venterPåBruker = venterPåBruker, + venterPåØkonomi = venterPåØkonomi, + forrigeBehandling = forrigeBehandling?.let(Behandling::eksternBrukId), + revurderingOpprettetÅrsak = behandlingsårsak?.type, + totalFeilutbetaltBeløp = totalFeilutbetaltBeløp, + totalFeilutbetaltPeriode = totalFeilutbetaltPeriode, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/SendSakshendelseTilDvhTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/SendSakshendelseTilDvhTask.kt new file mode 100644 index 000000000..e4efd6086 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/SendSakshendelseTilDvhTask.kt @@ -0,0 +1,39 @@ +package no.nav.familie.tilbake.datavarehus.saksstatistikk + +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.datavarehus.saksstatistikk.sakshendelse.Behandlingstilstand +import no.nav.familie.tilbake.integration.kafka.KafkaProducer +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = SendSakshendelseTilDvhTask.TASK_TYPE, + beskrivelse = "Sending av sakshendelser til datavarehus", +) +class SendSakshendelseTilDvhTask(private val kafkaProducer: KafkaProducer) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("SendSakshendelseTilDvhTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val behandlingId = UUID.fromString(task.payload) + val behandlingstilstand: Behandlingstilstand = objectMapper.readValue(task.metadata.getProperty("behandlingstilstand")) + kafkaProducer.sendSaksdata( + behandlingId, + behandlingstilstand.copy(tekniskTidspunkt = OffsetDateTime.now(ZoneOffset.UTC)), + ) + } + + companion object { + + const val TASK_TYPE = "dvh.send.sakshendelse" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/SendVedtaksoppsummeringTilDvhTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/SendVedtaksoppsummeringTilDvhTask.kt new file mode 100644 index 000000000..839df1a17 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/SendVedtaksoppsummeringTilDvhTask.kt @@ -0,0 +1,52 @@ +package no.nav.familie.tilbake.datavarehus.saksstatistikk + +import jakarta.validation.Validation +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak.Vedtaksoppsummering +import no.nav.familie.tilbake.integration.kafka.KafkaProducer +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = SendVedtaksoppsummeringTilDvhTask.TYPE, + beskrivelse = "Sender oppsummering av vedtak til datavarehus.", +) +class SendVedtaksoppsummeringTilDvhTask( + private val vedtaksoppsummeringService: VedtaksoppsummeringService, + private val kafkaProducer: KafkaProducer, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + private val secureLogger = LoggerFactory.getLogger("secureLogger") + private val validator = Validation.buildDefaultValidatorFactory().validator + + override fun doTask(task: Task) { + log.info("SendVedtaksoppsummeringTilDvhTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val behandlingId = UUID.fromString(task.payload) + val vedtaksoppsummering: Vedtaksoppsummering = vedtaksoppsummeringService.hentVedtaksoppsummering(behandlingId) + validate(vedtaksoppsummering) + + secureLogger.info( + "Sender Vedtaksoppsummering=${objectMapper.writeValueAsString(vedtaksoppsummering)} til Dvh " + + "for behandling $behandlingId", + ) + kafkaProducer.sendVedtaksdata(behandlingId, vedtaksoppsummering) + } + + private fun validate(vedtaksoppsummering: Vedtaksoppsummering) { + val valideringsfeil = validator.validate(vedtaksoppsummering) + require(valideringsfeil.isEmpty()) { + "Valideringsfeil for ${vedtaksoppsummering::class.simpleName}: Valideringsfeil:$valideringsfeil" + } + } + + companion object { + + const val TYPE = "dvh.send.vedtak" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/VedtaksoppsummeringService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/VedtaksoppsummeringService.kt new file mode 100644 index 000000000..0b4647bb8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/VedtaksoppsummeringService.kt @@ -0,0 +1,142 @@ +package no.nav.familie.tilbake.datavarehus.saksstatistikk + +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.beregning.TilbakekrevingsberegningService +import no.nav.familie.tilbake.beregning.modell.Beregningsresultat +import no.nav.familie.tilbake.beregning.modell.Beregningsresultatsperiode +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak.SærligeGrunner +import no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak.UtvidetVilkårsresultat +import no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak.VedtakPeriode +import no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak.Vedtaksoppsummering +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingRepository +import no.nav.familie.tilbake.foreldelse.VurdertForeldelseRepository +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingRepository +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingSærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsperiode +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import java.time.ZoneOffset +import java.util.UUID + +@Service +class VedtaksoppsummeringService( + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, + private val vilkårsvurderingRepository: VilkårsvurderingRepository, + private val foreldelseRepository: VurdertForeldelseRepository, + private val faktaFeilutbetalingRepository: FaktaFeilutbetalingRepository, + private val beregningService: TilbakekrevingsberegningService, +) { + + fun hentVedtaksoppsummering(behandlingId: UUID): Vedtaksoppsummering { + val behandling: Behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val eksternBehandling = behandling.aktivFagsystemsbehandling.eksternId + val behandlingsvedtak = behandling.sisteResultat?.behandlingsvedtak + ?: error("Behandling med id=$behandlingId mangler vedtak.Kan ikke sende data til DVH") + val behandlingsårsak = behandling.årsaker.firstOrNull() + val forrigeBehandling = behandlingsårsak?.originalBehandlingId?.let { behandlingRepository.findByIdOrNull(it) } + val ansvarligBeslutter = + behandling.ansvarligBeslutter + ?: error("Behandling med Id=$behandlingId mangler ansvarlig beslutter. Kan ikke sende data til DVH") + return Vedtaksoppsummering( + behandlingUuid = behandling.eksternBrukId, + saksnummer = fagsak.eksternFagsakId, + ytelsestype = fagsak.ytelsestype, + ansvarligSaksbehandler = behandling.ansvarligSaksbehandler, + ansvarligBeslutter = ansvarligBeslutter, + behandlingstype = behandling.type, + behandlingOpprettetTidspunkt = behandling.opprettetTidspunkt.atOffset(ZoneOffset.UTC), + vedtakFattetTidspunkt = behandlingsvedtak.sporbar.opprettetTid.atOffset(ZoneOffset.UTC), + referertFagsaksbehandling = eksternBehandling, + behandlendeEnhet = behandling.behandlendeEnhet, + erBehandlingManueltOpprettet = behandling.manueltOpprettet, + forrigeBehandling = forrigeBehandling?.let(Behandling::eksternBrukId), + perioder = hentVedtakPerioder(behandlingId), + ) + } + + private fun hentVedtakPerioder(behandlingId: UUID): List { + val vilkårsvurdering = vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + val vurdertForeldelse = foreldelseRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + val beregningsresultat = beregningService.beregn(behandlingId) + val vilkårsperioder = vilkårsvurdering?.let { + hentVilkårPerioder(behandlingId, beregningsresultat, it) + } ?: emptyList() + val foreldelsesperioder = vurdertForeldelse?.let { + hentForeldelsePerioder(behandlingId, beregningsresultat, it) + } ?: emptyList() + return vilkårsperioder + foreldelsesperioder + } + + private fun hentVilkårPerioder( + behandlingId: UUID, + beregningsresultat: Beregningsresultat, + vilkårsvurdering: Vilkårsvurdering, + ): List { + return vilkårsvurdering.perioder.map { periode -> + val beregningsresultatsperiode: Beregningsresultatsperiode = + beregningsresultat.beregningsresultatsperioder.first { it.periode == periode.periode } + val faktaFeilutbetaling = + faktaFeilutbetalingRepository.findFaktaFeilutbetalingByBehandlingIdAndAktivIsTrue(behandlingId) + val faktaperiode = faktaFeilutbetaling.perioder.first { it.periode.overlapper(periode.periode) } + + VedtakPeriode( + fom = periode.periode.fomDato, + tom = periode.periode.tomDato, + hendelsestype = faktaperiode.hendelsestype.name, + hendelsesundertype = faktaperiode.hendelsesundertype.name, + harBruktSjetteLedd = periode.aktsomhet?.tilbakekrevSmåbeløp == false, + aktsomhet = periode.aktsomhet?.aktsomhet, + vilkårsresultat = UtvidetVilkårsresultat.valueOf(periode.vilkårsvurderingsresultat.name), + særligeGrunner = hentSærligGrunner(periode), + feilutbetaltBeløp = beregningsresultatsperiode.feilutbetaltBeløp, + bruttoTilbakekrevingsbeløp = beregningsresultatsperiode.tilbakekrevingsbeløp, + rentebeløp = beregningsresultatsperiode.rentebeløp, + ) + } + } + + private fun hentForeldelsePerioder( + behandlingId: UUID, + beregningsresultat: Beregningsresultat, + vurdertForeldelse: VurdertForeldelse, + ): List { + return vurdertForeldelse.foreldelsesperioder.mapNotNull { periode -> + if (periode.erForeldet()) { + val resultatPeriode: Beregningsresultatsperiode = + beregningsresultat.beregningsresultatsperioder.first { it.periode == periode.periode } + val faktaFeilutbetalingEntitet = + faktaFeilutbetalingRepository.findFaktaFeilutbetalingByBehandlingIdAndAktivIsTrue(behandlingId) + val faktaPeriode = faktaFeilutbetalingEntitet.perioder.first { it.periode.overlapper(periode.periode) } + + VedtakPeriode( + fom = periode.periode.fomDato, + tom = periode.periode.tomDato, + hendelsestype = faktaPeriode.hendelsestype.name, + hendelsesundertype = faktaPeriode.hendelsesundertype.name, + vilkårsresultat = UtvidetVilkårsresultat.FORELDET, + feilutbetaltBeløp = resultatPeriode.feilutbetaltBeløp, + bruttoTilbakekrevingsbeløp = resultatPeriode.tilbakekrevingsbeløp, + rentebeløp = resultatPeriode.rentebeløp, + ) + } else { + null + } + } + } + + private fun hentSærligGrunner(periodeEntitet: Vilkårsvurderingsperiode): SærligeGrunner? { + if (periodeEntitet.aktsomhet?.vilkårsvurderingSærligeGrunner?.isEmpty() == false) { + val særligeGrunner = + periodeEntitet.aktsomhet.vilkårsvurderingSærligeGrunner.map(VilkårsvurderingSærligGrunn::særligGrunn) + return SærligeGrunner(periodeEntitet.aktsomhet.særligeGrunnerTilReduksjon, særligeGrunner) + } + return null + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/sakshendelse/Behandlingstilstand.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/sakshendelse/Behandlingstilstand.kt new file mode 100644 index 000000000..02086e7d5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/sakshendelse/Behandlingstilstand.kt @@ -0,0 +1,33 @@ +package no.nav.familie.tilbake.datavarehus.saksstatistikk.sakshendelse + +import no.nav.familie.kontrakter.felles.tilbakekreving.Periode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsakstype +import java.math.BigDecimal +import java.time.OffsetDateTime +import java.util.UUID + +data class Behandlingstilstand( + val funksjoneltTidspunkt: OffsetDateTime, + val tekniskTidspunkt: OffsetDateTime? = null, + val saksnummer: String, + val ytelsestype: Ytelsestype, + val behandlingUuid: UUID, + val referertFagsaksbehandling: String, + val behandlingstype: Behandlingstype, + val behandlingsstatus: Behandlingsstatus, + val behandlingsresultat: Behandlingsresultatstype, + val behandlingErManueltOpprettet: Boolean, + val venterPåBruker: Boolean, + val venterPåØkonomi: Boolean, + val ansvarligEnhet: String, + val ansvarligSaksbehandler: String, + val ansvarligBeslutter: String?, + val forrigeBehandling: UUID?, + val revurderingOpprettetÅrsak: Behandlingsårsakstype?, + val totalFeilutbetaltBeløp: BigDecimal?, + val totalFeilutbetaltPeriode: Periode?, +) diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/vedtak/S\303\246rligeGrunner.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/vedtak/S\303\246rligeGrunner.kt" new file mode 100644 index 000000000..7ebc45d27 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/vedtak/S\303\246rligeGrunner.kt" @@ -0,0 +1,8 @@ +package no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak + +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn + +class SærligeGrunner( + var erSærligeGrunnerTilReduksjon: Boolean = false, + var særligeGrunner: List = emptyList(), +) diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/vedtak/UtvidetVilk\303\245rsresultat.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/vedtak/UtvidetVilk\303\245rsresultat.kt" new file mode 100644 index 000000000..b1d7ac949 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/vedtak/UtvidetVilk\303\245rsresultat.kt" @@ -0,0 +1,9 @@ +package no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak + +enum class UtvidetVilkårsresultat { + FORELDET, + FORSTO_BURDE_FORSTÅTT, + MANGELFULLE_OPPLYSNINGER_FRA_BRUKER, + FEIL_OPPLYSNINGER_FRA_BRUKER, + GOD_TRO, +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/vedtak/VedtakPeriode.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/vedtak/VedtakPeriode.kt new file mode 100644 index 000000000..70e6ef299 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/vedtak/VedtakPeriode.kt @@ -0,0 +1,19 @@ +package no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak + +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import java.math.BigDecimal +import java.time.LocalDate + +class VedtakPeriode( + var fom: LocalDate, + var tom: LocalDate, + var hendelsestype: String, + var hendelsesundertype: String? = null, + var vilkårsresultat: UtvidetVilkårsresultat, + var feilutbetaltBeløp: BigDecimal, + var bruttoTilbakekrevingsbeløp: BigDecimal, + var rentebeløp: BigDecimal, + var harBruktSjetteLedd: Boolean = false, + var aktsomhet: Aktsomhet? = null, + var særligeGrunner: SærligeGrunner? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/vedtak/Vedtaksoppsummering.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/vedtak/Vedtaksoppsummering.kt new file mode 100644 index 000000000..892802a7c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/vedtak/Vedtaksoppsummering.kt @@ -0,0 +1,25 @@ +package no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak + +import jakarta.validation.constraints.Size +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import java.time.OffsetDateTime +import java.util.UUID + +class Vedtaksoppsummering( + @Size(min = 1, max = 20) + val saksnummer: String, + val ytelsestype: Ytelsestype, + val behandlingUuid: UUID, + val behandlingstype: Behandlingstype, + val erBehandlingManueltOpprettet: Boolean = false, + val behandlingOpprettetTidspunkt: OffsetDateTime, + val vedtakFattetTidspunkt: OffsetDateTime, + val referertFagsaksbehandling: String, + val forrigeBehandling: UUID? = null, + val ansvarligSaksbehandler: String, + val ansvarligBeslutter: String, + val behandlendeEnhet: String, + @Size(min = 1, max = 100) + val perioder: List, +) diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/Distribusjonsh\303\245ndteringService.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/Distribusjonsh\303\245ndteringService.kt" new file mode 100644 index 000000000..65915ead1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/Distribusjonsh\303\245ndteringService.kt" @@ -0,0 +1,147 @@ +package no.nav.familie.tilbake.dokumentbestilling + +import no.nav.familie.kontrakter.felles.dokdist.AdresseType +import no.nav.familie.kontrakter.felles.dokdist.ManuellAdresse +import no.nav.familie.kontrakter.felles.tilbakekreving.MottakerType.BRUKER_MED_UTENLANDSK_ADRESSE +import no.nav.familie.kontrakter.felles.tilbakekreving.MottakerType.DØDSBO +import no.nav.familie.tilbake.behandling.BehandlingService +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmetadataUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager.BRUKER +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager.INSTITUSJON +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager.MANUELL_BRUKER +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager.MANUELL_TILLEGGSMOTTAKER +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager.VERGE +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.Brevdata +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.PdfBrevService +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.ManuellBrevmottakerRepository +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.domene.ManuellBrevmottaker +import no.nav.familie.tilbake.dokumentbestilling.vedtak.VedtaksbrevgunnlagService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class DistribusjonshåndteringService( + private val brevmetadataUtil: BrevmetadataUtil, + private val fagsakRepository: FagsakRepository, + private val manuelleBrevmottakerRepository: ManuellBrevmottakerRepository, + private val pdfBrevService: PdfBrevService, + private val vedtaksbrevgrunnlagService: VedtaksbrevgunnlagService, + private val featureToggleService: FeatureToggleService, +) { + + fun sendBrev( + behandling: Behandling, + brevtype: Brevtype, + varsletBeløp: Long? = null, + fritekst: String? = null, + brevdata: (Brevmottager, Brevmetadata?) -> Brevdata, + ) { + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val vedtaksbrevgrunnlag = when (brevtype) { + Brevtype.VEDTAK -> vedtaksbrevgrunnlagService.hentVedtaksbrevgrunnlag(behandling.id) + else -> null + } + val støtterManuelleBrevmottakere: Boolean = BehandlingService.sjekkOmManuelleBrevmottakereErStøttet( + behandling = behandling, + fagsak = fagsak, + ) + val brevmottakere = utledMottakere( + behandling = behandling, + fagsak = fagsak, + erManuelleMottakereStøttet = støtterManuelleBrevmottakere, + manueltRegistrerteMottakere = manuelleBrevmottakerRepository.findByBehandlingId(behandling.id).toSet(), + ).toList() + + brevmottakere.filterNotNull().forEachIndexed { index, brevmottaker -> + pdfBrevService.sendBrev( + behandling = behandling, + fagsak = fagsak, + brevtype = brevtype, + data = brevdata( + brevmottaker.somBrevmottager, + brevmetadataUtil.genererMetadataForBrev( + behandling.id, + vedtaksbrevgrunnlag, + brevmottager = brevmottaker.somBrevmottager, + manuellAdresseinfo = brevmottaker.manuellAdresse, + annenMottakersNavn = brevmottakere[brevmottakere.lastIndex - index].navn, + ), + ), + varsletBeløp = varsletBeløp, + fritekst = fritekst, + ) + } + } + companion object { + fun utledMottakere( + behandling: Behandling, + fagsak: Fagsak, + erManuelleMottakereStøttet: Boolean, + manueltRegistrerteMottakere: Set, + ): Pair { + return if (erManuelleMottakereStøttet) { + require(manueltRegistrerteMottakere.all { it.behandlingId == behandling.id }) + + val (manuellBrukeradresse, manuellTilleggsmottaker) = manueltRegistrerteMottakere + .partition { it.type == BRUKER_MED_UTENLANDSK_ADRESSE || it.type == DØDSBO } + Pair( + first = manuellBrukeradresse.singleOrNull()?.let { ManuellBrevmottakerType(it) } ?: BrevmottagerType(BRUKER), + second = manuellTilleggsmottaker.singleOrNull()?.let { ManuellBrevmottakerType(it) }, + ) + } else { + val defaultMottaker = if (fagsak.institusjon != null) INSTITUSJON else BRUKER + val tilleggsmottaker = if (behandling.harVerge) VERGE else null + Pair( + first = BrevmottagerType(defaultMottaker), + second = tilleggsmottaker?.let { BrevmottagerType(it) }, + ) + } + } + } +} + +sealed interface Brevmottaker + +class BrevmottagerType(val mottaker: Brevmottager) : Brevmottaker +class ManuellBrevmottakerType(val mottaker: ManuellBrevmottaker) : Brevmottaker + +val Brevmottaker?.navn: String? + get() = if (this is ManuellBrevmottakerType) mottaker.navn else null +val Brevmottaker.somBrevmottager: Brevmottager + get() = (this as? BrevmottagerType)?.mottaker ?: (this as ManuellBrevmottakerType).run { + if (mottaker.erTilleggsmottaker) MANUELL_TILLEGGSMOTTAKER else MANUELL_BRUKER + } +val Brevmottaker?.manuellAdresse: Adresseinfo? + get() = if (this is ManuellBrevmottakerType) { + Adresseinfo( + ident = mottaker.ident.orEmpty(), + mottagernavn = mottaker.navn, + manuellAdresse = if (mottaker.hasManuellAdresse()) { + ManuellAdresse( + adresseType = when (mottaker.landkode) { + "NO" -> AdresseType.norskPostadresse + else -> AdresseType.utenlandskPostadresse + }, + adresselinje1 = mottaker.adresselinje1, + adresselinje2 = mottaker.adresselinje2, + postnummer = mottaker.postnummer, + poststed = mottaker.poststed, + land = mottaker.landkode!!, + ) + } else { + null + }, + ) + } else { + null + } diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/DokumentbehandlingService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/DokumentbehandlingService.kt new file mode 100644 index 000000000..1084c235f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/DokumentbehandlingService.kt @@ -0,0 +1,87 @@ +package no.nav.familie.tilbake.dokumentbestilling + +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.dokumentbestilling.brevmaler.Dokumentmalstype +import no.nav.familie.tilbake.dokumentbestilling.innhentdokumentasjon.InnhentDokumentasjonbrevService +import no.nav.familie.tilbake.dokumentbestilling.innhentdokumentasjon.InnhentDokumentasjonbrevTask +import no.nav.familie.tilbake.dokumentbestilling.varsel.manuelt.ManueltVarselbrevService +import no.nav.familie.tilbake.dokumentbestilling.varsel.manuelt.SendManueltVarselbrevTask +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +@Transactional +class DokumentbehandlingService( + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, + private val behandlingskontrollService: BehandlingskontrollService, + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val taskService: TaskService, + private val manueltVarselBrevService: ManueltVarselbrevService, + private val innhentDokumentasjonBrevService: InnhentDokumentasjonbrevService, +) { + + fun bestillBrev(behandlingId: UUID, maltype: Dokumentmalstype, fritekst: String) { + val behandling: Behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val ansvarligSaksbehandler = ContextService.hentSaksbehandler() + if (behandling.ansvarligSaksbehandler != ansvarligSaksbehandler) { + behandlingRepository.update(behandling.copy(ansvarligSaksbehandler = ansvarligSaksbehandler)) + } + if (Dokumentmalstype.VARSEL == maltype || Dokumentmalstype.KORRIGERT_VARSEL == maltype) { + håndterManueltSendVarsel(behandling, maltype, fritekst) + } else if (Dokumentmalstype.INNHENT_DOKUMENTASJON == maltype) { + håndterInnhentDokumentasjon(behandling, fritekst) + } + } + + fun forhåndsvisBrev(behandlingId: UUID, maltype: Dokumentmalstype, fritekst: String): ByteArray { + var dokument = ByteArray(0) + if (Dokumentmalstype.VARSEL == maltype || Dokumentmalstype.KORRIGERT_VARSEL == maltype) { + dokument = manueltVarselBrevService.hentForhåndsvisningManueltVarselbrev(behandlingId, maltype, fritekst) + } else if (Dokumentmalstype.INNHENT_DOKUMENTASJON == maltype) { + dokument = innhentDokumentasjonBrevService.hentForhåndsvisningInnhentDokumentasjonBrev(behandlingId, fritekst) + } + return dokument + } + + private fun håndterManueltSendVarsel(behandling: Behandling, maltype: Dokumentmalstype, fritekst: String) { + if (!kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandling.id)) { + error("Kan ikke sende varselbrev fordi grunnlag finnes ikke for behandlingId = ${behandling.id}") + } + val fagsystem = fagsakRepository.findByIdOrThrow(behandling.fagsakId).fagsystem + val sendVarselbrev = + SendManueltVarselbrevTask.opprettTask(behandling.id, fagsystem, maltype, fritekst) + taskService.save(sendVarselbrev) + settPåVent(behandling) + } + + private fun settPåVent(behandling: Behandling) { + val tidsfrist = Constants.saksbehandlersTidsfrist() + behandlingskontrollService.settBehandlingPåVent( + behandling.id, + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + tidsfrist, + ) + } + + private fun håndterInnhentDokumentasjon(behandling: Behandling, fritekst: String) { + if (!kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandling.id)) { + error("Kan ikke sende innhent dokumentasjonsbrev fordi grunnlag finnes ikke for behandlingId = ${behandling.id}") + } + val fagsystem = fagsakRepository.findByIdOrThrow(behandling.fagsakId).fagsystem + val sendInnhentDokumentasjonBrev = + InnhentDokumentasjonbrevTask.opprettTask(behandling.id, fagsystem, fritekst) + taskService.save(sendInnhentDokumentasjonBrev) + settPåVent(behandling) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/brevmaler/Dokumentmalstype.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/brevmaler/Dokumentmalstype.kt new file mode 100644 index 000000000..d921400d2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/brevmaler/Dokumentmalstype.kt @@ -0,0 +1,8 @@ +package no.nav.familie.tilbake.dokumentbestilling.brevmaler + +enum class Dokumentmalstype { + INNHENT_DOKUMENTASJON, + FRITEKSTBREV, + VARSEL, + KORRIGERT_VARSEL, +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/Adresseinfo.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/Adresseinfo.kt new file mode 100644 index 000000000..d1432e54f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/Adresseinfo.kt @@ -0,0 +1,10 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles + +import no.nav.familie.kontrakter.felles.dokdist.ManuellAdresse + +class Adresseinfo( + val ident: String, + val mottagernavn: String, + val annenMottagersNavn: String? = null, + val manuellAdresse: ManuellAdresse? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/Brevmetadata.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/Brevmetadata.kt new file mode 100644 index 000000000..fe836bdbd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/Brevmetadata.kt @@ -0,0 +1,33 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles + +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.dokumentbestilling.felles.header.Institusjon +import no.nav.familie.tilbake.dokumentbestilling.handlebars.dto.Språkstøtte + +data class Brevmetadata( + val sakspartId: String, + val sakspartsnavn: String, + val finnesVerge: Boolean = false, + val finnesAnnenMottaker: Boolean = finnesVerge, + val vergenavn: String? = null, + val annenMottakersNavn: String? = null, + val mottageradresse: Adresseinfo, + val behandlendeEnhetId: String? = null, + val behandlendeEnhetsNavn: String, + val ansvarligSaksbehandler: String, + val saksnummer: String, + override val språkkode: Språkkode, + val ytelsestype: Ytelsestype, + val behandlingstype: Behandlingstype? = null, + val tittel: String? = null, + val gjelderDødsfall: Boolean, + val institusjon: Institusjon? = null, +) : Språkstøtte { + init { + if (finnesAnnenMottaker && !finnesVerge) { + requireNotNull(annenMottakersNavn) { "annenMottakersNavn kan ikke være null" } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevmetadataUtil.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevmetadataUtil.kt new file mode 100644 index 000000000..b6be524b2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevmetadataUtil.kt @@ -0,0 +1,156 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles + +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.BehandlingService +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.dokumentbestilling.DistribusjonshåndteringService +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.ManuellBrevmottakerRepository +import no.nav.familie.tilbake.dokumentbestilling.manuellAdresse +import no.nav.familie.tilbake.dokumentbestilling.somBrevmottager +import no.nav.familie.tilbake.dokumentbestilling.vedtak.Vedtaksbrevgrunnlag +import no.nav.familie.tilbake.organisasjon.OrganisasjonService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class BrevmetadataUtil( + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, + private val manuelleBrevmottakerRepository: ManuellBrevmottakerRepository, + private val eksterneDataForBrevService: EksterneDataForBrevService, + private val organisasjonService: OrganisasjonService, + private val featureToggleService: FeatureToggleService, +) { + private val logger = LoggerFactory.getLogger(this::class.java) + + fun genererMetadataForBrev( + behandlingId: UUID, + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag? = null, + brevmottager: Brevmottager = Brevmottager.BRUKER, + manuellAdresseinfo: Adresseinfo? = null, + annenMottakersNavn: String? = null, + ): Brevmetadata? { + require(brevmottager != brevmottager.MANUELL || manuellAdresseinfo != null) { + "For en manuelt registrert brevmottaker kan ikke manuellAdresseinfo være null" + } + + val behandling: Behandling by lazy { behandlingRepository.findByIdOrThrow(behandlingId) } + val fagsak: Fagsak by lazy { fagsakRepository.findByIdOrThrow(behandling.fagsakId) } + val fagsystem = vedtaksbrevgrunnlag?.fagsystem ?: fagsak.fagsystem + + val aktivVerge = vedtaksbrevgrunnlag?.aktivVerge ?: behandling.aktivVerge + + val personinfo = eksterneDataForBrevService.hentPerson( + ident = vedtaksbrevgrunnlag?.bruker?.ident ?: fagsak.bruker.ident, + fagsystem = fagsystem, + ) + val adresseinfo = manuellAdresseinfo ?: eksterneDataForBrevService.hentAdresse( + personinfo = personinfo, + brevmottager = brevmottager, + verge = aktivVerge, + fagsystem = fagsystem, + ) + val vergenavn = BrevmottagerUtil.getVergenavn(aktivVerge, adresseinfo) + + val gjelderDødsfall = personinfo.dødsdato != null + + val persistertSaksbehandlerId = + vedtaksbrevgrunnlag?.behandling?.ansvarligSaksbehandler ?: behandling.ansvarligSaksbehandler + + val brevmetadata = Brevmetadata( + sakspartId = personinfo.ident, + sakspartsnavn = personinfo.navn, + finnesVerge = aktivVerge != null, + vergenavn = vergenavn, + finnesAnnenMottaker = annenMottakersNavn != null || aktivVerge != null, + annenMottakersNavn = annenMottakersNavn, + mottageradresse = adresseinfo, + behandlendeEnhetId = vedtaksbrevgrunnlag?.behandling?.behandlendeEnhet ?: behandling.behandlendeEnhet, + behandlendeEnhetsNavn = vedtaksbrevgrunnlag?.behandling?.behandlendeEnhetsNavn ?: behandling.behandlendeEnhetsNavn, + ansvarligSaksbehandler = hentAnsvarligSaksbehandlerNavn(persistertSaksbehandlerId, vedtaksbrevgrunnlag), + saksnummer = vedtaksbrevgrunnlag?.eksternFagsakId ?: fagsak.eksternFagsakId, + språkkode = vedtaksbrevgrunnlag?.bruker?.språkkode ?: fagsak.bruker.språkkode, + ytelsestype = vedtaksbrevgrunnlag?.ytelsestype ?: fagsak.ytelsestype, + gjelderDødsfall = gjelderDødsfall, + institusjon = (vedtaksbrevgrunnlag?.institusjon ?: fagsak.institusjon)?.let { + organisasjonService.mapTilInstitusjonForBrevgenerering(it.organisasjonsnummer) + }, + ) + return brevmetadata + } + + fun lagBrevmetadataForMottakerTilForhåndsvisning( + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag, + ): Pair { + return lagBrevmetadataForMottakerTilForhåndsvisning( + behandlingId = vedtaksbrevgrunnlag.behandling.id, + vedtaksbrevgrunnlag = vedtaksbrevgrunnlag, + ) + } + + fun lagBrevmetadataForMottakerTilForhåndsvisning( + behandlingId: UUID, + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag? = null, + ): Pair { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + + val støtterManuelleBrevmottakere = BehandlingService.sjekkOmManuelleBrevmottakereErStøttet( + behandling = behandling, + fagsak = fagsak, + ) + val (bruker, tilleggsmottaker) = DistribusjonshåndteringService.utledMottakere( + behandling = behandling, + fagsak = fagsak, + erManuelleMottakereStøttet = støtterManuelleBrevmottakere, + manueltRegistrerteMottakere = manuelleBrevmottakerRepository.findByBehandlingId(behandling.id).toSet(), + ) + val (brevmottager, manuellAdresseinfo) = when (tilleggsmottaker) { + null -> bruker.somBrevmottager to bruker.manuellAdresse + else -> tilleggsmottaker.somBrevmottager to tilleggsmottaker.manuellAdresse + } + val metadata = genererMetadataForBrev( + behandlingId = behandling.id, + vedtaksbrevgrunnlag = vedtaksbrevgrunnlag, + brevmottager = brevmottager, + manuellAdresseinfo = manuellAdresseinfo, + annenMottakersNavn = tilleggsmottaker?.let { + eksterneDataForBrevService.hentPerson(fagsak.bruker.ident, fagsak.fagsystem).navn + }, + ) + return metadata to brevmottager + } + + private fun hentAnsvarligSaksbehandlerNavn( + persistertSaksbehandlerId: String, + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag?, + ): String { + return when { + vedtaksbrevgrunnlag?.aktivtSteg == Behandlingssteg.FORESLÅ_VEDTAK -> + eksterneDataForBrevService.hentPåloggetSaksbehandlernavnMedDefault(persistertSaksbehandlerId) + + vedtaksbrevgrunnlag != null -> + eksterneDataForBrevService.hentSaksbehandlernavn(persistertSaksbehandlerId) + + persistertSaksbehandlerId != Constants.BRUKER_ID_VEDTAKSLØSNINGEN -> + eksterneDataForBrevService.hentPåloggetSaksbehandlernavnMedDefault(persistertSaksbehandlerId) + + else -> "" + } + } +} + +private val Brevmottager.MANUELL + get() = when (this) { + Brevmottager.MANUELL_BRUKER, + Brevmottager.MANUELL_TILLEGGSMOTTAKER, + -> this + else -> null + } diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/Brevmottager.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/Brevmottager.kt new file mode 100644 index 000000000..1de719e02 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/Brevmottager.kt @@ -0,0 +1,9 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles + +enum class Brevmottager { + BRUKER, + VERGE, + INSTITUSJON, + MANUELL_BRUKER, + MANUELL_TILLEGGSMOTTAKER, +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevmottagerUtil.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevmottagerUtil.kt new file mode 100644 index 000000000..5567d0b07 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevmottagerUtil.kt @@ -0,0 +1,49 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles + +import no.nav.familie.kontrakter.felles.tilbakekreving.Vergetype +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.kontrakter.felles.tilbakekreving.Verge as VergeDto +import no.nav.familie.tilbake.behandling.domain.Verge as DomainVerge + +object BrevmottagerUtil { + + fun getAnnenMottagersNavn(brevmetadata: Brevmetadata): String? { + if (brevmetadata.annenMottakersNavn != null) { + return brevmetadata.annenMottakersNavn + } + + val mottagernavn: String = brevmetadata.mottageradresse.mottagernavn + val brukernavn = brevmetadata.sakspartsnavn + val vergenavn = brevmetadata.vergenavn + + return if (mottagernavn.equals(brukernavn, ignoreCase = true)) { + if (brevmetadata.finnesVerge) vergenavn else "" + } else { + brukernavn + } + } + + fun getVergenavn(verge: DomainVerge?, adresseinfo: Adresseinfo): String { + return if (Vergetype.ADVOKAT == verge?.type) { + adresseinfo.annenMottagersNavn!! // Når verge er advokat, viser vi verge navn som "Virksomhet navn v/ verge navn" + } else { + verge?.navn ?: "" + } + } + + fun getVergenavn(verge: VergeDto?, adresseinfo: Adresseinfo): String { + return if (Vergetype.ADVOKAT == verge?.vergetype) { + adresseinfo.annenMottagersNavn!! // Når verge er advokat, viser vi verge navn som "Virksomhet navn v/ verge navn" + } else { + verge?.navn ?: "" + } + } + + fun utledBrevmottager( + behandling: Behandling, + fagsak: Fagsak, + ): Brevmottager { + return if (behandling.harVerge) Brevmottager.VERGE else if (fagsak.institusjon != null) Brevmottager.INSTITUSJON else Brevmottager.BRUKER + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevsporingRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevsporingRepository.kt new file mode 100644 index 000000000..ff395234b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevsporingRepository.kt @@ -0,0 +1,20 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles + +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevsporing +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Repository +@Transactional +interface BrevsporingRepository : RepositoryInterface, InsertUpdateRepository { + + fun findFirstByBehandlingIdAndBrevtypeOrderBySporbarOpprettetTidDesc(behandlingId: UUID, brevtype: Brevtype): Brevsporing? + + fun existsByBehandlingIdAndBrevtypeIn(behandlingId: UUID, brevtype: Set): Boolean + + fun existsByBehandlingId(behandlingId: UUID): Boolean +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevsporingService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevsporingService.kt new file mode 100644 index 000000000..dace5d0d8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevsporingService.kt @@ -0,0 +1,41 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles + +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevsporing +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class BrevsporingService(private val brevsporingRepository: BrevsporingRepository) { + + fun lagreInfoOmUtsendtBrev( + behandlingId: UUID, + dokumentId: String, + journalpostId: String, + brevtype: Brevtype, + ) { + val brevSporing = Brevsporing( + behandlingId = behandlingId, + dokumentId = dokumentId, + journalpostId = journalpostId, + brevtype = brevtype, + ) + brevsporingRepository.insert(brevSporing) + } + + fun finnSisteVarsel(behandlingId: UUID): Brevsporing? { + val varselbrev = brevsporingRepository + .findFirstByBehandlingIdAndBrevtypeOrderBySporbarOpprettetTidDesc(behandlingId, Brevtype.VARSEL) + val korrigertVarselbrev = brevsporingRepository + .findFirstByBehandlingIdAndBrevtypeOrderBySporbarOpprettetTidDesc(behandlingId, Brevtype.KORRIGERT_VARSEL) + + return korrigertVarselbrev ?: varselbrev + } + + fun erVarselSendt(behandlingId: UUID): Boolean { + return brevsporingRepository.existsByBehandlingIdAndBrevtypeIn( + behandlingId, + setOf(Brevtype.VARSEL, Brevtype.KORRIGERT_VARSEL), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/EksterneDataForBrevService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/EksterneDataForBrevService.kt new file mode 100644 index 000000000..aad6e45da --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/EksterneDataForBrevService.kt @@ -0,0 +1,116 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.organisasjon.Organisasjon +import no.nav.familie.kontrakter.felles.tilbakekreving.Vergetype +import no.nav.familie.tilbake.behandling.domain.Verge +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import no.nav.familie.tilbake.person.PersonService +import org.springframework.stereotype.Service +import no.nav.familie.kontrakter.felles.tilbakekreving.Verge as VergeDto + +@Service +class EksterneDataForBrevService( + private val personService: PersonService, + private val integrasjonerClient: IntegrasjonerClient, +) { + + fun hentPerson(ident: String, fagsystem: Fagsystem): Personinfo { + return personService.hentPersoninfo(ident, fagsystem) + } + + fun hentSaksbehandlernavn(id: String): String { + val saksbehandler = integrasjonerClient.hentSaksbehandler(id) + return saksbehandler.fornavn + " " + saksbehandler.etternavn + } + + fun hentPåloggetSaksbehandlernavnMedDefault(defaultId: String?): String { + val saksbehandlerId = ContextService.hentPåloggetSaksbehandler(defaultId) + val saksbehandler = integrasjonerClient.hentSaksbehandler(saksbehandlerId) + return saksbehandler.fornavn + " " + saksbehandler.etternavn + } + + private fun hentAdresse(personinfo: Personinfo): Adresseinfo { + return Adresseinfo(personinfo.ident, personinfo.navn) + } + + fun hentAdresse( + personinfo: Personinfo, + brevmottager: Brevmottager, + verge: Verge?, + fagsystem: Fagsystem, + ): Adresseinfo { + return verge?.let { hentAdresse(it.type, it.orgNr, it.navn, personinfo, brevmottager, it.ident, fagsystem) } + ?: hentAdresse(personinfo) + } + + fun hentAdresse( + personinfo: Personinfo, + brevmottager: Brevmottager, + vergeDto: VergeDto?, + fagsystem: Fagsystem, + ): Adresseinfo { + return vergeDto?.let { + hentAdresse( + it.vergetype, + it.organisasjonsnummer, + it.navn, + personinfo, + brevmottager, + it.personIdent, + fagsystem, + ) + } ?: hentAdresse(personinfo) + } + + private fun hentAdresse( + vergeType: Vergetype, + organisasjonsnummer: String?, + navn: String, + personinfo: Personinfo, + brevmottager: Brevmottager, + personIdent: String?, + fagsystem: Fagsystem, + ): Adresseinfo { + if (Vergetype.ADVOKAT == vergeType) { + return hentOrganisasjonsadresse( + organisasjonsnummer ?: error("organisasjonsnummer er påkrevd for $vergeType"), + navn, + personinfo, + brevmottager, + ) + } else if (Brevmottager.VERGE == brevmottager) { + val person = hentPerson(personIdent ?: error("personIdent er påkrevd for $vergeType"), fagsystem) + return hentAdresse(person) + } + return hentAdresse(personinfo) + } + + private fun hentOrganisasjonsadresse( + organisasjonsnummer: String, + vergenavn: String, + personinfo: Personinfo, + brevmottager: Brevmottager, + ): Adresseinfo { + val organisasjon = integrasjonerClient.hentOrganisasjon(organisasjonsnummer) + return lagAdresseinfo(organisasjon, vergenavn, personinfo, brevmottager) + } + + private fun lagAdresseinfo( + organisasjon: Organisasjon, + vergeNavn: String, + personinfo: Personinfo, + brevmottager: Brevmottager, + ): Adresseinfo { + val organisasjonsnavn: String = organisasjon.navn + val vedVergeNavn = "v/ $vergeNavn" + val annenMottagersNavn = "$organisasjonsnavn $vedVergeNavn" + return if (Brevmottager.VERGE == brevmottager) { + Adresseinfo(organisasjon.organisasjonsnummer, organisasjon.navn, personinfo.navn) + } else { + Adresseinfo(personinfo.ident, personinfo.navn, annenMottagersNavn) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/domain/Brevsporing.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/domain/Brevsporing.kt new file mode 100644 index 000000000..52fbecd93 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/domain/Brevsporing.kt @@ -0,0 +1,33 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.domain + +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import java.util.UUID + +data class Brevsporing( + @Id + val id: UUID = UUID.randomUUID(), + val behandlingId: UUID, + val journalpostId: String, + val dokumentId: String, + val brevtype: Brevtype, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +enum class Brevtype { + VARSEL, + KORRIGERT_VARSEL, + VEDTAK, + HENLEGGELSE, + INNHENT_DOKUMENTASJON, + ; + + fun gjelderVarsel(): Boolean { + return this in setOf(VARSEL, KORRIGERT_VARSEL) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/Brev.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/Brev.kt new file mode 100644 index 000000000..dd7951518 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/Brev.kt @@ -0,0 +1,8 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.header + +import java.time.LocalDate + +class Brev( + val overskrift: String?, + val dato: LocalDate = LocalDate.now(), +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/HeaderData.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/HeaderData.kt new file mode 100644 index 000000000..fc9b7bedd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/HeaderData.kt @@ -0,0 +1,11 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.header + +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.tilbake.dokumentbestilling.handlebars.dto.Språkstøtte + +class HeaderData( + override val språkkode: Språkkode, + val person: Person, + val brev: Brev, + val institusjon: Institusjon? = null, +) : Språkstøtte diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/Institusjon.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/Institusjon.kt new file mode 100644 index 000000000..784a33507 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/Institusjon.kt @@ -0,0 +1,6 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.header + +class Institusjon( + val organisasjonsnummer: String, + val navn: String, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/Person.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/Person.kt new file mode 100644 index 000000000..9e8928c87 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/Person.kt @@ -0,0 +1,6 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.header + +class Person( + val navn: String, + val ident: String, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/TekstformatererHeader.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/TekstformatererHeader.kt new file mode 100644 index 000000000..2365b1692 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/TekstformatererHeader.kt @@ -0,0 +1,23 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.header + +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.sanitize +import no.nav.familie.tilbake.dokumentbestilling.handlebars.FellesTekstformaterer + +object TekstformatererHeader { + + fun lagHeader(brevmetadata: Brevmetadata, overskrift: String): String { + return lagHeader( + HeaderData( + språkkode = brevmetadata.språkkode, + person = Person(brevmetadata.sakspartsnavn, brevmetadata.sakspartId), + brev = Brev(overskrift), + institusjon = if (brevmetadata.institusjon != null) Institusjon(brevmetadata.institusjon.organisasjonsnummer, sanitize(brevmetadata.institusjon.navn)) else null, + ), + ) + } + + private fun lagHeader(data: HeaderData): String { + return FellesTekstformaterer.lagBrevtekst(data, "header") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/Brevdata.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/Brevdata.kt new file mode 100644 index 000000000..03db715a8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/Brevdata.kt @@ -0,0 +1,13 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.pdf + +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager + +data class Brevdata( + var metadata: Brevmetadata, + val tittel: String? = null, + val overskrift: String, + val mottager: Brevmottager, + val brevtekst: String, + val vedleggHtml: String = "", +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/DokprodTilHtml.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/DokprodTilHtml.kt new file mode 100644 index 000000000..e1b789393 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/DokprodTilHtml.kt @@ -0,0 +1,119 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.pdf + +object DokprodTilHtml { + + fun dokprodInnholdTilHtml(tekst: String): String { + val dokprod: String = sanitize(tekst) + val builder = StringBuilder() + val avsnittene = hentAvsnittene(dokprod) + var samepageStarted = false + avsnittene.forEach { avsnitt -> + var inBulletpoints = false + var harAvsnitt = false + val linjer = avsnitt.split("\n\r?".toRegex()) + for (linje in linjer) { + if (linje.isBlank()) { + builder.append("
") + continue + } + if (linje.startsWith("*-")) { + inBulletpoints = true + builder.append("
    ") + if (linje.substring(2).isBlank()) { + continue + } + } + if (inBulletpoints) { + val bp = linje.replace("*-", "") + if (bp.trimEnd().endsWith("-*")) { + builder.append("
  • ").append(bp.replace("-*", "")).append("
") + inBulletpoints = false + } else { + builder.append("
  • ").append(bp).append("
  • ") + } + continue + } + if (linje.startsWith("{venstrejustert}")) { + val la = linje.replace("{venstrejustert}", "") + val saksbehandler = la.substringBefore("{høyrejustert}") + val beslutter = la.substringAfter("{høyrejustert}") + builder.append( + """
    + + + + +
    $saksbehandler$beslutter
    """, + ) + + continue + } + val overskrift = linje.startsWith("_") + if (overskrift) { + if (samepageStarted) { + builder.append("") + } else { + samepageStarted = true + } + builder.append("
    ") + val underoverskrift = linje.startsWith("__") + if (underoverskrift) { + builder.append("

    ").append(linje.substring(2)).append("

    ") + } else { + builder.append("

    ").append(linje.substring(1)).append("

    ") + } + } else { + if (!harAvsnitt) { + harAvsnitt = true + builder.append("

    ") + } else { + builder.append("
    ") + } + builder.append(linje) + } + } + if (harAvsnitt) { + builder.append("

    ") + } + if (samepageStarted) { + samepageStarted = false + builder.append("
    ") + } + } + return ekstraLinjeskiftFørHilsing(konverterNbsp(builder.toString())) + } + + private fun hentAvsnittene(dokprod: String): List { + // avsnitt ved dobbelt linjeskift + // avsnitt ved overskrift (linje starter med _) + return dokprod.split("(\n\r?\n\r?)|(\n\r?(?=_))".toRegex()) + } + + private fun konverterNbsp(s: String): String { + val utf8nonBreakingSpace = "\u00A0" + val htmlNonBreakingSpace = " " + return s.replace(utf8nonBreakingSpace.toRegex(), htmlNonBreakingSpace) + } + + private fun ekstraLinjeskiftFørHilsing(s: String): String { + return s.replace("

    Med vennlig hilsen", "

    Med vennlig hilsen") + .replace("

    Med venleg helsing", "

    Med venleg helsing") + } +} + +fun sanitize(name: String): String { + val builder = StringBuilder() + for (element in name) { + when (element) { + '"' -> builder.append(""") + '\'' -> builder.append("'") + '<' -> builder.append("<") + '>' -> builder.append(">") + '&' -> builder.append("&") + else -> { + builder.append(element) + } + } + } + return builder.toString() +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/Dokumentkategori.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/Dokumentkategori.kt new file mode 100644 index 000000000..23809244c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/Dokumentkategori.kt @@ -0,0 +1,7 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.pdf + +enum class Dokumentkategori(val kode: String) { + + BREV("B"), + VEDTAKSBREV("VB"), +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/Journalf\303\270ringService.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/Journalf\303\270ringService.kt" new file mode 100644 index 000000000..b129e46c5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/Journalf\303\270ringService.kt" @@ -0,0 +1,184 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.pdf + +import no.nav.familie.kontrakter.felles.BrukerIdType +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Tema +import no.nav.familie.kontrakter.felles.dokarkiv.AvsenderMottaker +import no.nav.familie.kontrakter.felles.dokarkiv.Dokumenttype +import no.nav.familie.kontrakter.felles.dokarkiv.v2.ArkiverDokumentRequest +import no.nav.familie.kontrakter.felles.dokarkiv.v2.Dokument +import no.nav.familie.kontrakter.felles.dokarkiv.v2.Filtype +import no.nav.familie.kontrakter.felles.journalpost.Bruker +import no.nav.familie.kontrakter.felles.journalpost.Journalpost +import no.nav.familie.kontrakter.felles.journalpost.JournalposterForBrukerRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandling.domain.Verge +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.fritekstbrev.JournalpostIdOgDokumentId +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class JournalføringService( + private val integrasjonerClient: IntegrasjonerClient, + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, +) { + + private val logger = LoggerFactory.getLogger(this::class.java) + + fun hentDokument(journalpostId: String, dokumentInfoId: String): ByteArray { + return integrasjonerClient.hentDokument(dokumentInfoId, journalpostId) + } + + fun hentJournalposter(behandlingId: UUID): List { + val behandling = behandlingRepository.findById(behandlingId).orElseThrow() + val fagsak = behandling.let { fagsakRepository.findById(it.fagsakId).orElseThrow() } + val journalposter = fagsak.let { + integrasjonerClient.hentJournalposterForBruker( + JournalposterForBrukerRequest( + antall = 1000, + brukerId = Bruker( + id = fagsak.bruker.ident, + type = BrukerIdType.FNR, + ), + tema = listOf(hentTema(fagsystem = fagsak.fagsystem)), + ), + ) + } + return journalposter.filter { it.sak?.fagsakId == fagsak.eksternFagsakId } + } + + fun journalførUtgåendeBrev( + behandling: Behandling, + fagsak: Fagsak, + dokumentkategori: Dokumentkategori, + brevmetadata: Brevmetadata, + brevmottager: Brevmottager, + vedleggPdf: ByteArray, + eksternReferanseId: String?, + ): JournalpostIdOgDokumentId { + logger.info("Starter journalføring av {} til {} for behandlingId={}", dokumentkategori, brevmottager, behandling.id) + val dokument = Dokument( + dokument = vedleggPdf, + filtype = Filtype.PDFA, + filnavn = if (dokumentkategori == Dokumentkategori.VEDTAKSBREV) "vedtak.pdf" else "brev.pdf", + tittel = brevmetadata.tittel, + dokumenttype = velgDokumenttype(fagsak, dokumentkategori), + ) + val request = ArkiverDokumentRequest( + fnr = fagsak.bruker.ident, + forsøkFerdigstill = true, + hoveddokumentvarianter = listOf(dokument), + fagsakId = fagsak.eksternFagsakId, + journalførendeEnhet = behandling.behandlendeEnhet, + avsenderMottaker = lagMottager(behandling, brevmottager, brevmetadata), + eksternReferanseId = eksternReferanseId, + ) + + val response = integrasjonerClient.arkiver(request) + + val dokumentinfoId = response.dokumenter?.first()?.dokumentInfoId + ?: error( + "Feil ved Journalføring av $dokumentkategori " + + "til $brevmottager for behandlingId=${behandling.id}", + ) + logger.info( + "Journalførte utgående {} til {} for behandlingId={} med journalpostid={}", + dokumentkategori, + brevmottager, + behandling.id, + response.journalpostId, + ) + return JournalpostIdOgDokumentId(response.journalpostId, dokumentinfoId) + } + + private fun lagMottager(behandling: Behandling, mottager: Brevmottager, brevmetadata: Brevmetadata): AvsenderMottaker { + val adresseinfo: Adresseinfo = brevmetadata.mottageradresse + val mottagerIdent = adresseinfo.ident.takeIf { it.isNotBlank() } + return when (mottager) { + Brevmottager.BRUKER, + Brevmottager.MANUELL_BRUKER, + Brevmottager.MANUELL_TILLEGGSMOTTAKER, + -> AvsenderMottaker( + id = mottagerIdent, + idType = utledIdType(mottagerIdent), + navn = adresseinfo.mottagernavn, + ) + Brevmottager.VERGE -> lagVergemottager(behandling) + Brevmottager.INSTITUSJON -> lagInstitusjonmottager(behandling, brevmetadata) + } + } + + private fun utledIdType(mottagerIdent: String?) = when (mottagerIdent?.length) { + 0, null -> null + 9 -> BrukerIdType.ORGNR + 11 -> BrukerIdType.FNR + else -> throw IllegalArgumentException("Ugyldig idType") + } + + private fun lagInstitusjonmottager(behandling: Behandling, brevmetadata: Brevmetadata): AvsenderMottaker { + val institusjon = brevmetadata.institusjon ?: throw IllegalStateException( + "Brevmottager er institusjon, men institusjon finnes ikke. " + + "Fagsak ${behandling.fagsakId} og behandling ${behandling.id}", + ) + return AvsenderMottaker( + idType = BrukerIdType.ORGNR, + id = institusjon.organisasjonsnummer, + navn = institusjon.navn, + ) + } + + private fun lagVergemottager(behandling: Behandling): AvsenderMottaker { + val verge: Verge = behandling.aktivVerge + ?: throw IllegalStateException( + "Brevmottager er verge, men verge finnes ikke. " + + "Behandling ${behandling.id}", + ) + return if (verge.orgNr != null) { + AvsenderMottaker( + idType = BrukerIdType.ORGNR, + id = verge.orgNr, + navn = verge.navn, + ) + } else { + AvsenderMottaker( + idType = BrukerIdType.FNR, + id = verge.ident!!, + navn = verge.navn, + ) + } + } + + private fun velgDokumenttype(fagsak: Fagsak, dokumentkategori: Dokumentkategori): Dokumenttype { + return if (dokumentkategori == Dokumentkategori.VEDTAKSBREV) { + when (fagsak.ytelsestype) { + Ytelsestype.BARNETRYGD -> Dokumenttype.BARNETRYGD_TILBAKEKREVING_VEDTAK + Ytelsestype.OVERGANGSSTØNAD -> Dokumenttype.OVERGANGSSTØNAD_TILBAKEKREVING_VEDTAK + Ytelsestype.BARNETILSYN -> Dokumenttype.BARNETILSYN_TILBAKEKREVING_VEDTAK + Ytelsestype.SKOLEPENGER -> Dokumenttype.SKOLEPENGER_TILBAKEKREVING_VEDTAK + Ytelsestype.KONTANTSTØTTE -> Dokumenttype.KONTANTSTØTTE_TILBAKEKREVING_VEDTAK + } + } else { + when (fagsak.ytelsestype) { + Ytelsestype.BARNETRYGD -> Dokumenttype.BARNETRYGD_TILBAKEKREVING_BREV + Ytelsestype.OVERGANGSSTØNAD -> Dokumenttype.OVERGANGSSTØNAD_TILBAKEKREVING_BREV + Ytelsestype.BARNETILSYN -> Dokumenttype.BARNETILSYN_TILBAKEKREVING_BREV + Ytelsestype.SKOLEPENGER -> Dokumenttype.SKOLEPENGER_TILBAKEKREVING_BREV + Ytelsestype.KONTANTSTØTTE -> Dokumenttype.KONTANTSTØTTE_TILBAKEKREVING_BREV + } + } + } + + private fun hentTema(fagsystem: Fagsystem): Tema { + return Tema.valueOf(fagsystem.tema) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/PdfBrevService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/PdfBrevService.kt new file mode 100644 index 000000000..5690cf767 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/PdfBrevService.kt @@ -0,0 +1,194 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.pdf + +import no.nav.familie.http.client.RessursException +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstidspunkt +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstype +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.log.mdc.MDCConstants +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.felles.header.TekstformatererHeader +import no.nav.familie.tilbake.dokumentbestilling.felles.task.PubliserJournalpostTask +import no.nav.familie.tilbake.dokumentbestilling.felles.task.PubliserJournalpostTaskData +import no.nav.familie.tilbake.dokumentbestilling.fritekstbrev.JournalpostIdOgDokumentId +import no.nav.familie.tilbake.integration.pdl.internal.secureLogger +import no.nav.familie.tilbake.micrometer.TellerService +import no.nav.familie.tilbake.pdfgen.Dokumentvariant +import no.nav.familie.tilbake.pdfgen.PdfGenerator +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import java.util.Base64 +import java.util.Properties + +@Service +class PdfBrevService( + private val journalføringService: JournalføringService, + private val tellerService: TellerService, + private val taskService: TaskService, +) { + + private val logger = LoggerFactory.getLogger(PdfBrevService::class.java) + private val pdfGenerator: PdfGenerator = PdfGenerator() + + fun genererForhåndsvisning(data: Brevdata): ByteArray { + val html = lagHtml(data) + return pdfGenerator.genererPDFMedLogo(html, Dokumentvariant.UTKAST) + } + + fun sendBrev( + behandling: Behandling, + fagsak: Fagsak, + brevtype: Brevtype, + data: Brevdata, + varsletBeløp: Long? = null, + fritekst: String? = null, + ) { + valider(brevtype, varsletBeløp) + val dokumentreferanse: JournalpostIdOgDokumentId = lagOgJournalførBrev(behandling, fagsak, brevtype, data) + if (data.mottager != Brevmottager.VERGE && + !data.metadata.annenMottakersNavn.equals(data.metadata.sakspartsnavn, ignoreCase = true) + ) { + // Ikke tell kopier sendt til verge eller fullmektig + tellerService.tellBrevSendt(fagsak, brevtype) + } + lagTaskerForUtsendingOgSporing(behandling, fagsak, brevtype, varsletBeløp, fritekst, data, dokumentreferanse) + } + + private fun lagTaskerForUtsendingOgSporing( + behandling: Behandling, + fagsak: Fagsak, + brevtype: Brevtype, + varsletBeløp: Long?, + fritekst: String?, + brevdata: Brevdata, + dokumentreferanse: JournalpostIdOgDokumentId, + ) { + val payload = objectMapper.writeValueAsString( + PubliserJournalpostTaskData( + behandlingId = behandling.id, + manuellAdresse = brevdata.metadata.mottageradresse.manuellAdresse, + ), + ) + val properties: Properties = Properties().apply { + setProperty("journalpostId", dokumentreferanse.journalpostId) + setProperty(PropertyName.FAGSYSTEM, fagsak.fagsystem.name) + setProperty("dokumentId", dokumentreferanse.dokumentId) + setProperty("mottager", brevdata.mottager.name) + setProperty("brevtype", brevtype.name) + setProperty("ansvarligSaksbehandler", behandling.ansvarligSaksbehandler) + setProperty("distribusjonstype", utledDistribusjonstype(brevtype).name) + setProperty("distribusjonstidspunkt", distribusjonstidspunkt) + varsletBeløp?.also { setProperty("varselbeløp", varsletBeløp.toString()) } + fritekst?.also { setProperty("fritekst", Base64.getEncoder().encodeToString(fritekst.toByteArray())) } + brevdata.tittel?.also { setProperty("tittel", it) } + } + logger.info( + "Oppretter task for publisering av brev for behandlingId=${behandling.id}, eksternFagsakId=${fagsak.eksternFagsakId}", + ) + taskService.save(Task(PubliserJournalpostTask.TYPE, payload, properties)) + } + + private fun lagOgJournalførBrev( + behandling: Behandling, + fagsak: Fagsak, + brevtype: Brevtype, + data: Brevdata, + ): JournalpostIdOgDokumentId { + val html = lagHtml(data) + + val pdf = try { + pdfGenerator.genererPDFMedLogo(html, Dokumentvariant.ENDELIG) + } catch (e: Exception) { + secureLogger.info("Feil ved generering av brev: brevData=$data, html=$html", e) + throw e + } + + val dokumentkategori = mapBrevtypeTilDokumentkategori(brevtype) + val eksternReferanseId = lagEksternReferanseId(behandling, brevtype, data.mottager) + + try { + return journalføringService.journalførUtgåendeBrev( + behandling, + fagsak, + dokumentkategori, + data.metadata, + data.mottager, + pdf, + eksternReferanseId, + ) + } catch (ressursException: RessursException) { + if (ressursException.httpStatus == HttpStatus.CONFLICT) { + logger.info("Dokarkiv svarte med 409 CONFLICT. Forsøker å hente eksisterende journalpost for $dokumentkategori") + val journalpost = + journalføringService.hentJournalposter(behandling.id).find { it.eksternReferanseId == eksternReferanseId } + ?: error("Klarte ikke finne igjen opprettet journalpost med eksternReferanseId $eksternReferanseId") + + return JournalpostIdOgDokumentId( + journalpostId = journalpost.journalpostId, + dokumentId = journalpost.dokumenter?.first()?.dokumentInfoId ?: error( + "Feil ved Journalføring av $dokumentkategori til ${data.mottager} for behandlingId=${behandling.id}", + ), + ) + } + throw ressursException + } + } + + private fun lagEksternReferanseId(behandling: Behandling, brevtype: Brevtype, mottager: Brevmottager): String { + // alle brev kan potensielt bli sendt til både bruker og kopi verge. 2 av breva kan potensielt bli sendt flere gonger + val callId = MDC.get(MDCConstants.MDC_CALL_ID) + return "${behandling.eksternBrukId}_${brevtype.name.lowercase()}_${mottager.name.lowercase()}_$callId" + } + + private fun mapBrevtypeTilDokumentkategori(brevtype: Brevtype): Dokumentkategori { + return if (Brevtype.VEDTAK === brevtype) { + Dokumentkategori.VEDTAKSBREV + } else { + Dokumentkategori.BREV + } + } + + private fun lagHtml(data: Brevdata): String { + val header = lagHeader(data) + val innholdHtml = lagInnhold(data) + return header + innholdHtml + data.vedleggHtml + } + + private fun lagInnhold(data: Brevdata): String { + return DokprodTilHtml.dokprodInnholdTilHtml(data.brevtekst) + } + + private fun lagHeader(data: Brevdata): String { + return TekstformatererHeader.lagHeader( + brevmetadata = data.metadata, + overskrift = data.overskrift, + ) + } + + private fun utledDistribusjonstype(brevtype: Brevtype): Distribusjonstype { + return when (brevtype) { + Brevtype.VARSEL, Brevtype.KORRIGERT_VARSEL, Brevtype.INNHENT_DOKUMENTASJON -> Distribusjonstype.VIKTIG + Brevtype.VEDTAK -> Distribusjonstype.VEDTAK + Brevtype.HENLEGGELSE -> Distribusjonstype.ANNET + } + } + + private val distribusjonstidspunkt = Distribusjonstidspunkt.KJERNETID.name + + companion object { + + private fun valider(brevtype: Brevtype, varsletBeløp: Long?) { + val harVarsletBeløp = varsletBeløp != null + require(brevtype.gjelderVarsel() == harVarsletBeløp) { + "Utvikler-feil: Varslet beløp skal brukes hvis, og bare hvis, brev gjelder varsel" + } + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/DistribuerDokumentVedD\303\270dsfallTask.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/DistribuerDokumentVedD\303\270dsfallTask.kt" new file mode 100644 index 000000000..1c1bd1089 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/DistribuerDokumentVedD\303\270dsfallTask.kt" @@ -0,0 +1,96 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.task + +import no.nav.familie.http.client.RessursException +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstidspunkt +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstype +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.util.UUID + +const val ANTALL_SEKUNDER_I_EN_UKE = 604800L + +@Service +@TaskStepBeskrivelse( + taskStepType = DistribuerDokumentVedDødsfallTask.TYPE, + beskrivelse = "Send dødsfall dokument til Dokdist", + triggerTidVedFeilISekunder = ANTALL_SEKUNDER_I_EN_UKE, + // ~8 måneder dersom vi prøver én gang i uka. + // Tasken skal stoppe etter 6 måneder, så om vi kommer hit har det skjedd noe galt. + maxAntallFeil = 4 * 8, + settTilManuellOppfølgning = true, +) +class DistribuerDokumentVedDødsfallTask( + private val integrasjonerClient: IntegrasjonerClient, + private val historikkTaskService: HistorikkTaskService, +) : AsyncTaskStep { + + val logger: Logger = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + logger.info("${this::class.simpleName} prosesserer med id=${task.id} og metadata ${task.metadata}") + + val journalpostId = task.metadata.getProperty("journalpostId") + val fagsystem = task.metadata.getProperty("fagsystem") + + val erTaskEldreEnn6Mnd = task.opprettetTid.isBefore(LocalDateTime.now().minusMonths(6)) + + if (erTaskEldreEnn6Mnd) { + logger.info("Stopper \"DistribuerDokumentVedDødsfallTask\" fordi den er eldre enn 6 måneder.") + opprettHistorikkinnslag(task, TilbakekrevingHistorikkinnslagstype.DISTRIBUSJON_BREV_DØDSBO_FEILET_6_MND, true) + } else { + try { + integrasjonerClient.distribuerJournalpost( + journalpostId, + Fagsystem.valueOf(fagsystem), + Distribusjonstype.valueOf(task.metadata.getProperty("distribusjonstype")), + Distribusjonstidspunkt.valueOf(task.metadata.getProperty("distribusjonstidspunkt")), + ) + + logger.info("Task \"DistribuerDokumentVedDødsfallTask\" har kjørt suksessfullt, og brev er sendt") + opprettHistorikkinnslag(task, TilbakekrevingHistorikkinnslagstype.DISTRIBUSJON_BREV_DØDSBO_SUKSESS) + } catch (ressursException: RessursException) { + if (mottakerErDødUtenDødsboadresse(ressursException)) { + logger.info("Klarte ikke å distribuere journalpost $journalpostId for fagsystem $fagsystem. Prøver igjen om 7 dager.") + throw ressursException + } else { + throw ressursException + } + } + } + } + + private fun opprettHistorikkinnslag(task: Task, tilbakekrevingHistorikkinnslagstype: TilbakekrevingHistorikkinnslagstype, feilet: Boolean = false) { + val mottager = Brevmottager.valueOf(task.metadata.getProperty("mottager")) + val brevtype = Brevtype.valueOf(task.metadata.getProperty("brevtype")) + val ansvarligSaksbehandler = task.metadata.getProperty("ansvarligSaksbehandler") + val opprinneligHistorikkinnslagstype = LagreBrevsporingTask.utledHistorikkinnslagType(brevtype, mottager) + historikkTaskService.lagHistorikkTask( + behandlingId = UUID.fromString(task.payload), + historikkinnslagstype = tilbakekrevingHistorikkinnslagstype, + aktør = LagreBrevsporingTask.utledAktør(brevtype, ansvarligSaksbehandler), + beskrivelse = opprinneligHistorikkinnslagstype.tekst, + brevtype = if (!feilet) brevtype else null, + ) + } + + companion object { + + const val TYPE = "distribuerDokumentVedDødsfallPåFagsak" + + // 410 GONE er unikt for bruker død og ingen dødsboadresse mot Dokdist + // https://nav-it.slack.com/archives/C6W9E5GPJ/p1647956660364779?thread_ts=1647936835.099329&cid=C6W9E5GPJ + fun mottakerErDødUtenDødsboadresse(ressursException: RessursException): Boolean = ressursException.httpStatus == HttpStatus.GONE + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/LagreBrevsporingTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/LagreBrevsporingTask.kt new file mode 100644 index 000000000..6d058382b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/LagreBrevsporingTask.kt @@ -0,0 +1,128 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.task + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingService +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.iverksettvedtak.task.AvsluttBehandlingTask +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = LagreBrevsporingTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Lagrer brev", + triggerTidVedFeilISekunder = 60 * 5L, +) +class LagreBrevsporingTask( + private val brevsporingService: BrevsporingService, + private val taskService: TaskService, + private val historikkTaskService: HistorikkTaskService, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("${this::class.simpleName} prosesserer med id=${task.id} og metadata ${task.metadata}") + val dokumentId = task.metadata.getProperty("dokumentId") + val journalpostId = task.metadata.getProperty("journalpostId") + val brevtype = Brevtype.valueOf(task.metadata.getProperty("brevtype")) + + brevsporingService.lagreInfoOmUtsendtBrev( + UUID.fromString(task.payload), + dokumentId, + journalpostId, + brevtype, + ) + } + + override fun onCompletion(task: Task) { + val mottager = Brevmottager.valueOf(task.metadata.getProperty("mottager")) + val brevtype = Brevtype.valueOf(task.metadata.getProperty("brevtype")) + val ansvarligSaksbehandler = task.metadata.getProperty("ansvarligSaksbehandler") + val ukjentAdresse = (task.metadata.getOrDefault("ukjentAdresse", "false") as String).toBoolean() + val dødsboUkjentAdresse = (task.metadata.getOrDefault("dødsboUkjentAdresse", "false") as String).toBoolean() + val opprinneligHistorikkinnslagstype = utledHistorikkinnslagType(brevtype, mottager) + + if (ukjentAdresse) { + historikkTaskService.lagHistorikkTask( + behandlingId = UUID.fromString(task.payload), + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BREV_IKKE_SENDT_UKJENT_ADRESSE, + aktør = utledAktør(brevtype, ansvarligSaksbehandler), + beskrivelse = opprinneligHistorikkinnslagstype.tekst, + brevtype = brevtype, + ) + } else if (dødsboUkjentAdresse) { + historikkTaskService.lagHistorikkTask( + behandlingId = UUID.fromString(task.payload), + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BREV_IKKE_SENDT_DØDSBO_UKJENT_ADRESSE, + aktør = utledAktør(brevtype, ansvarligSaksbehandler), + beskrivelse = opprinneligHistorikkinnslagstype.tekst, + brevtype = brevtype, + ) + } else { + historikkTaskService.lagHistorikkTask( + behandlingId = UUID.fromString(task.payload), + historikkinnslagstype = opprinneligHistorikkinnslagstype, + aktør = utledAktør(brevtype, ansvarligSaksbehandler), + brevtype = brevtype, + ) + } + + if (brevtype.gjelderVarsel() && mottager != Brevmottager.VERGE) { + taskService.save(Task(LagreVarselbrevsporingTask.TYPE, task.payload, task.metadata)) + } + + // Behandling bør avsluttes etter å sende vedtaksbrev + // AvsluttBehandlingTask må kalles kun en gang selv om behandling har to brevmottakere + if (brevtype == Brevtype.VEDTAK && mottager !in listOf(Brevmottager.VERGE, Brevmottager.MANUELL_TILLEGGSMOTTAKER)) { + taskService.save(Task(type = AvsluttBehandlingTask.TYPE, payload = task.payload, task.metadata)) + } + } + + companion object { + + fun utledHistorikkinnslagType(brevtype: Brevtype, mottager: Brevmottager): TilbakekrevingHistorikkinnslagstype { + if (Brevmottager.VERGE == mottager) { + return when (brevtype) { + Brevtype.VARSEL -> TilbakekrevingHistorikkinnslagstype.VARSELBREV_SENDT_TIL_VERGE + Brevtype.KORRIGERT_VARSEL -> TilbakekrevingHistorikkinnslagstype.KORRIGERT_VARSELBREV_SENDT_TIL_VERGE + Brevtype.INNHENT_DOKUMENTASJON -> TilbakekrevingHistorikkinnslagstype.INNHENT_DOKUMENTASJON_BREV_SENDT_TIL_VERGE + Brevtype.HENLEGGELSE -> TilbakekrevingHistorikkinnslagstype.HENLEGGELSESBREV_SENDT_TIL_VERGE + Brevtype.VEDTAK -> TilbakekrevingHistorikkinnslagstype.VEDTAKSBREV_SENDT_TIL_VERGE + } + } + return when (brevtype) { + Brevtype.VARSEL -> TilbakekrevingHistorikkinnslagstype.VARSELBREV_SENDT + Brevtype.KORRIGERT_VARSEL -> TilbakekrevingHistorikkinnslagstype.KORRIGERT_VARSELBREV_SENDT + Brevtype.INNHENT_DOKUMENTASJON -> TilbakekrevingHistorikkinnslagstype.INNHENT_DOKUMENTASJON_BREV_SENDT + Brevtype.HENLEGGELSE -> TilbakekrevingHistorikkinnslagstype.HENLEGGELSESBREV_SENDT + Brevtype.VEDTAK -> TilbakekrevingHistorikkinnslagstype.VEDTAKSBREV_SENDT + } + } + + fun utledAktør( + brevtype: Brevtype, + ansvarligSaksbehandler: String?, + ): Aktør { + return when { + brevtype == Brevtype.INNHENT_DOKUMENTASJON -> Aktør.SAKSBEHANDLER + brevtype == Brevtype.KORRIGERT_VARSEL -> Aktør.SAKSBEHANDLER + !ansvarligSaksbehandler.isNullOrEmpty() && ansvarligSaksbehandler != Constants.BRUKER_ID_VEDTAKSLØSNINGEN -> + Aktør.SAKSBEHANDLER + else -> Aktør.VEDTAKSLØSNING + } + } + + const val TYPE = "lagreBrevsporing" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/LagreVarselbrevsporingTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/LagreVarselbrevsporingTask.kt new file mode 100644 index 000000000..70401ce11 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/LagreVarselbrevsporingTask.kt @@ -0,0 +1,37 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.task + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.VarselService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.Base64 +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = LagreVarselbrevsporingTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Lagrer varselbrev", + triggerTidVedFeilISekunder = 60 * 5L, +) +class LagreVarselbrevsporingTask(private val varselService: VarselService) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("${this::class.simpleName} prosesserer med id=${task.id} og metadata ${task.metadata}") + + val varseltekstBase64: String = task.metadata.getProperty("fritekst") + val varseltekst = Base64.getDecoder().decode(varseltekstBase64).decodeToString() + val varselbeløp: Long = task.metadata.getProperty("varselbeløp").toLong() + val behandlingId = UUID.fromString(task.payload) + varselService.lagre(behandlingId, varseltekst, varselbeløp) + } + + companion object { + + const val TYPE = "lagreVarselbrevsporing" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/PubliserJournalpostTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/PubliserJournalpostTask.kt new file mode 100644 index 000000000..dbe7e85df --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/PubliserJournalpostTask.kt @@ -0,0 +1,106 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.task + +import no.nav.familie.http.client.RessursException +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstidspunkt +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstype +import no.nav.familie.kontrakter.felles.dokdist.ManuellAdresse +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = PubliserJournalpostTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Publiserer journalpost", + triggerTidVedFeilISekunder = 60 * 5L, +) +class PubliserJournalpostTask( + private val integrasjonerClient: IntegrasjonerClient, + private val taskService: TaskService, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("${this::class.simpleName} prosesserer med id=${task.id} og metadata ${task.metadata}") + + val journalpostId = task.metadata.getProperty("journalpostId") + val (behandlingId, manuellAdresse) = objectMapper.readValue(task.payload, PubliserJournalpostTaskData::class.java) + .let { it.behandlingId to it.manuellAdresse } + + prøvDistribuerJournalpost(journalpostId, task, behandlingId, manuellAdresse) + } + + private fun prøvDistribuerJournalpost( + journalpostId: String, + task: Task, + behandlingId: UUID, + manuellAdresse: ManuellAdresse? = null, + ) { + try { + integrasjonerClient.distribuerJournalpost( + journalpostId, + Fagsystem.valueOf(task.metadata.getProperty("fagsystem")), + Distribusjonstype.valueOf(task.metadata.getProperty("distribusjonstype")), + Distribusjonstidspunkt.valueOf(task.metadata.getProperty("distribusjonstidspunkt")), + manuellAdresse, + ) + } catch (ressursException: RessursException) { + when { + mottakerErIkkeDigitalOgHarUkjentAdresse(ressursException) -> { + // ta med info om ukjent adresse + task.metadata["ukjentAdresse"] = "true" + } + + DistribuerDokumentVedDødsfallTask.mottakerErDødUtenDødsboadresse(ressursException) -> { + // ta med info om ukjent adresse for dødsbo + task.metadata["dødsboUkjentAdresse"] = "true" + taskService.save(Task(DistribuerDokumentVedDødsfallTask.TYPE, behandlingId.toString(), task.metadata)) + } + + dokumentetErAlleredeDistribuert(ressursException) -> { + log.warn( + "Journalpost med Id=$journalpostId er allerede distiribuert. Hopper over distribuering. BehandlingId=$behandlingId.", + ) + } + + else -> throw ressursException + } + } + } + + override fun onCompletion(task: Task) { + val behandlingId = objectMapper.readValue(task.payload, PubliserJournalpostTaskData::class.java).behandlingId + taskService.save(Task(LagreBrevsporingTask.TYPE, behandlingId.toString(), task.metadata)) + } + + // 400 BAD_REQUEST + kanal print er eneste måten å vite at bruker ikke er digital og har ukjent adresse fra Dokdist + // https://nav-it.slack.com/archives/C6W9E5GPJ/p1647947002270879?thread_ts=1647936835.099329&cid=C6W9E5GPJ + fun mottakerErIkkeDigitalOgHarUkjentAdresse(ressursException: RessursException) = + ressursException.httpStatus == HttpStatus.BAD_REQUEST && + ressursException.cause?.message?.contains("Mottaker har ukjent adresse") == true + + // 409 Conflict betyr duplikatdistribusjon + // https://nav-it.slack.com/archives/C6W9E5GPJ/p1657610907144549?thread_ts=1657610829.116619&cid=C6W9E5GPJ + fun dokumentetErAlleredeDistribuert(ressursException: RessursException) = + ressursException.httpStatus == HttpStatus.CONFLICT + + companion object { + + const val TYPE = "publiserJournalpost" + } +} + +class PubliserJournalpostTaskData( + val behandlingId: UUID, + val manuellAdresse: ManuellAdresse?, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/fritekstbrev/Fritekstbrevsdata.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/fritekstbrev/Fritekstbrevsdata.kt new file mode 100644 index 000000000..ab93e7151 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/fritekstbrev/Fritekstbrevsdata.kt @@ -0,0 +1,9 @@ +package no.nav.familie.tilbake.dokumentbestilling.fritekstbrev + +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata + +class Fritekstbrevsdata( + val overskrift: String, + val brevtekst: String, + val brevmetadata: Brevmetadata, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/fritekstbrev/JournalpostIdOgDokumentId.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/fritekstbrev/JournalpostIdOgDokumentId.kt new file mode 100644 index 000000000..800b61025 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/fritekstbrev/JournalpostIdOgDokumentId.kt @@ -0,0 +1,6 @@ +package no.nav.familie.tilbake.dokumentbestilling.fritekstbrev + +class JournalpostIdOgDokumentId( + val journalpostId: String, + val dokumentId: String, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/CustomHelpers.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/CustomHelpers.kt new file mode 100644 index 000000000..d3361095f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/CustomHelpers.kt @@ -0,0 +1,145 @@ +package no.nav.familie.tilbake.dokumentbestilling.handlebars + +import com.github.jknack.handlebars.Context +import com.github.jknack.handlebars.Helper +import com.github.jknack.handlebars.Options +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.tilbake.common.DatoUtil.DATO_FORMAT_DATO_MÅNEDSNAVN_ÅR +import org.apache.commons.lang3.StringUtils +import java.math.BigDecimal +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.time.LocalDate +import java.time.YearMonth +import java.time.format.DateTimeFormatter +import java.util.Locale + +class SwitchHelper : Helper { + + override fun apply(variabel: Any, options: Options): Any { + val variabelnavn: MutableList = ArrayList() + val variabelverdier: MutableList = ArrayList() + variabelnavn.add("__condition_fulfilled") + variabelverdier.add(0) + variabelnavn.add("__condition_variable") + variabelverdier.add(if (options.hash.isEmpty()) variabel else options.hash) + val ctx: Context = Context.newBlockParamContext(options.context, variabelnavn, variabelverdier) + val resultat: String = options.fn.apply(ctx) + val antall = ctx.get("__condition_fulfilled") as Int + if (Integer.valueOf(1) == antall) { + return resultat + } + throw IllegalArgumentException( + "Switch-case må treffe i 1 case, men traff i " + antall + + " med verdien " + ctx.get("__condition_variable"), + ) + } +} + +class CaseHelper : Helper { + + override fun apply(caseKonstant: Any?, options: Options): Any { + val konstant = if (options.hash.isEmpty()) caseKonstant else options.hash + + @Suppress("UNCHECKED_CAST") + val model = options.context.model() as MutableMap + val conditionVariable = model["__condition_variable"] + if (konstant == conditionVariable) { + val antall = model["__condition_fulfilled"] as Int? + model["__condition_fulfilled"] = antall!! + 1 + return options.fn() + } + return options.inverse() + } +} + +class VariableHelper : Helper { + + override fun apply(context: Any?, options: Options): Any { + val variabelnavn: MutableList = ArrayList() + val variabelverdier: MutableList = ArrayList() + for ((key, value) in options.hash.entries) { + variabelnavn.add(key) + variabelverdier.add(value) + } + val ctx: Context = Context.newBlockParamContext(options.context, variabelnavn, variabelverdier) + return options.fn.apply(ctx) + } +} + +class MapLookupHelper : Helper { + + override fun apply(context: Any?, options: Options): Any { + val key = context.toString() + val defaultVerdi: Any? = options.param(0, null) + return options.hash(key, defaultVerdi) + ?: throw IllegalArgumentException("Fant ikke verdi for " + key + " i " + options.hash) + } +} + +class DatoHelper : Helper { + + private val format = DATO_FORMAT_DATO_MÅNEDSNAVN_ÅR + + override fun apply(context: Any, options: Options?): Any { + val date = objectMapper.convertValue(context, LocalDate::class.java) + return format.format(date) + } +} + +class KortdatoHelper : Helper { + + private val format = DateTimeFormatter.ofPattern("dd.MM.yyyy") + + override fun apply(context: Any, options: Options?): Any { + val date = objectMapper.convertValue(context, LocalDate::class.java) + return format.format(date) + } +} + +class MånedHelper : Helper { + + private val format = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.forLanguageTag("NO")) + + override fun apply(context: Any, options: Options?): Any { + val date = objectMapper.convertValue(context, YearMonth::class.java) + return format.format(date) + } +} + +class StorBokstavHelper : Helper { + + override fun apply(context: String, options: Options?): Any { + return StringUtils.capitalize(context) + } +} + +class KroneFormattererMedTusenskille : Helper { + + override fun apply(context: Any?, options: Options?): Any { + if (context == null) { + return "ERROR" + } + val key = context.toString() + + return formatterKronerMedTusenskille(BigDecimal(key), utf8nonBreakingSpace) + } + + companion object { + val utf8nonBreakingSpace = '\u00A0' + + fun formatterKronerMedTusenskille(verdi: BigDecimal, space: Char): String { + val beløp = verdi + val beløpMedTusenskille = medTusenskille(beløp, space) + val benevning = if (beløp.compareTo(BigDecimal.ONE) == 0) "krone" else "kroner" + return beløpMedTusenskille + space + benevning + } + + fun medTusenskille(verdi: BigDecimal, tusenskille: Char): String { + val symbols = DecimalFormatSymbols.getInstance() + symbols.groupingSeparator = tusenskille + val formatter = DecimalFormat("###,###", symbols) + return formatter.format(verdi.toLong()) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/FellesTekstformaterer.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/FellesTekstformaterer.kt new file mode 100644 index 000000000..f6ddc2251 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/FellesTekstformaterer.kt @@ -0,0 +1,112 @@ +package no.nav.familie.tilbake.dokumentbestilling.handlebars + +import com.fasterxml.jackson.databind.JsonNode +import com.github.jknack.handlebars.Context +import com.github.jknack.handlebars.Handlebars +import com.github.jknack.handlebars.JsonNodeValueResolver +import com.github.jknack.handlebars.Template +import com.github.jknack.handlebars.context.JavaBeanValueResolver +import com.github.jknack.handlebars.context.MapValueResolver +import com.github.jknack.handlebars.helper.ConditionalHelpers +import com.github.jknack.handlebars.io.ClassPathTemplateLoader +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.tilbake.dokumentbestilling.handlebars.dto.Språkstøtte +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.util.Locale + +object FellesTekstformaterer { + + private val TEMPLATE_CACHE: MutableMap = HashMap() + + private val OM = ObjectMapperForUtvekslingAvDataMedHandlebars.INSTANCE + + fun lagBrevtekst(data: Språkstøtte, filsti: String): String { + val template = getTemplate(data.språkkode, filsti) + return applyTemplate(data, template) + } + + fun lagDeltekst(data: Språkstøtte, filsti: String): String { + val template = getTemplateFraPartial(data.språkkode, filsti) + return applyTemplate(data, template) + } + + private fun getTemplate(språkkode: Språkkode, filsti: String): Template { + val språkstøttetFilsti: String = lagSpråkstøttetFilsti(filsti, språkkode) + if (TEMPLATE_CACHE.containsKey(språkstøttetFilsti)) { + return TEMPLATE_CACHE[språkstøttetFilsti]!! + } + TEMPLATE_CACHE[språkstøttetFilsti] = + opprettTemplate(språkstøttetFilsti) + return TEMPLATE_CACHE[språkstøttetFilsti]!! + } + + private fun getTemplateFraPartial(språkkode: Språkkode, partial: String): Template { + val språkstøttetFilsti: String = lagSpråkstøttetFilsti(partial, språkkode) + if (TEMPLATE_CACHE.containsKey(språkstøttetFilsti)) { + return TEMPLATE_CACHE[språkstøttetFilsti]!! + } + TEMPLATE_CACHE[språkstøttetFilsti] = opprettTemplateFraPartials( + lagSpråkstøttetFilsti("vedtak/vedtak_felles", språkkode), + språkstøttetFilsti, + ) + return TEMPLATE_CACHE[språkstøttetFilsti]!! + } + + private fun opprettTemplate(språkstøttetFilsti: String): Template { + return opprettHandlebarsKonfigurasjon().compile(språkstøttetFilsti) + } + + private fun opprettTemplateFraPartials(vararg partials: String): Template { + val partialString = partials.joinToString("") { "{{> $it}}\n" } + return try { + opprettHandlebarsKonfigurasjon().compileInline(partialString) + } catch (e: IOException) { + error("Klarte ikke å kompilere partial template $partials") + } + } + + private fun applyTemplate(data: Any, template: Template): String { + return try { + // Går via JSON for å + // 1. tilrettelegger for å flytte generering til PDF etc til ekstern applikasjon + // 2. ha egen navngiving på variablene i template for enklere å lese template + // 3. unngår at template feiler når variable endrer navn + val jsonNode: JsonNode = OM.valueToTree(data) + val context = Context.newBuilder(jsonNode) + .resolver(JsonNodeValueResolver.INSTANCE, JavaBeanValueResolver.INSTANCE, MapValueResolver.INSTANCE) + .build() + template.apply(context).trim() + } catch (e: IOException) { + throw IllegalStateException("Feil ved tekstgenerering.") + } + } + + private fun opprettHandlebarsKonfigurasjon(): Handlebars { + val loader = ClassPathTemplateLoader().apply { + charset = StandardCharsets.UTF_8 + prefix = "/templates/" + suffix = ".hbs" + } + + return Handlebars(loader).apply { + charset = StandardCharsets.UTF_8 + setInfiniteLoops(false) + setPrettyPrint(true) + registerHelpers(ConditionalHelpers::class.java) + registerHelper("kroner", KroneFormattererMedTusenskille()) + registerHelper("dato", DatoHelper()) + registerHelper("kortdato", KortdatoHelper()) + registerHelper("måned", MånedHelper()) + registerHelper("storForbokstav", StorBokstavHelper()) + registerHelper("switch", SwitchHelper()) + registerHelper("case", CaseHelper()) + registerHelper("var", VariableHelper()) + registerHelper("lookup-map", MapLookupHelper()) + } + } + + private fun lagSpråkstøttetFilsti(filsti: String, språkkode: Språkkode): String { + return String.format("%s/%s", språkkode.name.lowercase(Locale.getDefault()), filsti) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/ObjectMapperForUtvekslingAvDataMedHandlebars.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/ObjectMapperForUtvekslingAvDataMedHandlebars.kt new file mode 100644 index 000000000..1e6fa7485 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/ObjectMapperForUtvekslingAvDataMedHandlebars.kt @@ -0,0 +1,21 @@ +package no.nav.familie.tilbake.dokumentbestilling.handlebars + +import com.fasterxml.jackson.annotation.JsonAutoDetect +import com.fasterxml.jackson.annotation.PropertyAccessor +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule + +object ObjectMapperForUtvekslingAvDataMedHandlebars { + + val INSTANCE: ObjectMapper = ObjectMapper() + + init { + INSTANCE.registerModule(JavaTimeModule()) + INSTANCE.registerModule(Jdk8Module()) + INSTANCE.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE) + INSTANCE.setVisibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.NONE) + INSTANCE.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + INSTANCE.setVisibility(PropertyAccessor.CREATOR, JsonAutoDetect.Visibility.ANY) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/dto/BaseDokument.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/dto/BaseDokument.kt new file mode 100644 index 000000000..d259b9d07 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/dto/BaseDokument.kt @@ -0,0 +1,130 @@ +package no.nav.familie.tilbake.dokumentbestilling.handlebars.dto + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.behandling.FagsystemUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.header.Institusjon + +private const val EF_URL = "nav.no/alene-med-barn" + +open class BaseDokument( + val ytelsestype: Ytelsestype, + override val språkkode: Språkkode, + val behandlendeEnhetsNavn: String, + val ansvarligSaksbehandler: String, + val gjelderDødsfall: Boolean, + val institusjon: Institusjon? = null, +) : Språkstøtte { + + val avsenderenhet = + if (FagsystemUtil.hentFagsystemFraYtelsestype(ytelsestype) == Fagsystem.EF) { + "NAV Arbeid og ytelser" + } else { + behandlendeEnhetsNavn + } + + private val infoMap = + mapOf( + Ytelsestype.BARNETRYGD to Ytelsesinfo( + "nav.no/barnetrygd", + mapOf( + Språkkode.NB to Ytelsesnavn( + "barnetrygd", + "barnetrygden", + "barnetrygden din", + ), + Språkkode.NN to Ytelsesnavn( + "barnetrygd", + "barnetrygda", + "barnetrygda di", + ), + ), + ), + Ytelsestype.OVERGANGSSTØNAD to Ytelsesinfo( + EF_URL, + mapOf( + Språkkode.NB to Ytelsesnavn( + "overgangsstønad", + "overgangsstønaden", + "overgangsstønaden din", + ), + Språkkode.NN to Ytelsesnavn( + "overgangsstønad", + "overgangsstønaden", + "overgangsstønaden din", + ), + ), + ), + Ytelsestype.BARNETILSYN to Ytelsesinfo( + EF_URL, + mapOf( + Språkkode.NB to Ytelsesnavn( + "stønad til barnetilsyn", + "stønaden til barnetilsyn", + "stønaden din til barnetilsyn", + ), + Språkkode.NN to Ytelsesnavn( + "stønad til barnetilsyn", + "stønaden til barnetilsyn", + "stønaden din til barnetilsyn", + ), + ), + ), + Ytelsestype.SKOLEPENGER to Ytelsesinfo( + EF_URL, + mapOf( + Språkkode.NB to Ytelsesnavn( + "stønad til skolepenger", + "stønaden til skolepenger", + "stønaden din til skolepenger", + ), + Språkkode.NN to Ytelsesnavn( + "stønad til skulepengar", + "stønaden til skulepengar", + "stønaden din til skulepengar", + ), + ), + ), + Ytelsestype.KONTANTSTØTTE to Ytelsesinfo( + "nav.no/kontantstotte", + mapOf( + Språkkode.NB to Ytelsesnavn( + "kontantstøtte", + "kontantstøtten", + "kontantstøtten din", + ), + Språkkode.NN to Ytelsesnavn( + "kontantstøtte", + "kontantstøtta", + "kontantstøtta di", + ), + ), + ), + ) + + private val ytelsesinfo + get() = infoMap[ytelsestype] + ?: error("Dokument forsøkt generert for ugyldig ytelsestype: $ytelsestype ") + + private val ytelsesnavn + get() = ytelsesinfo.navn[språkkode] + ?: error("Dokument forsøkt generert for ugyldig språkkode: $språkkode ytelse: $ytelsestype") + + @Suppress("unused") // Handlebars + val ytelsesnavnUbestemt = ytelsesnavn.ubestemt + + @Suppress("unused") + open // Handlebars + val ytelsesnavnBestemt = ytelsesnavn.bestemt + + @Suppress("unused") // Handlebars + val ytelsesnavnEiendomsform = ytelsesnavn.eiendomsform + + @Suppress("unused") // Handlebars + val ytelseUrl = ytelsesinfo.url + + private class Ytelsesinfo(val url: String, val navn: Map) + + private class Ytelsesnavn(val ubestemt: String, val bestemt: String, val eiendomsform: String) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/dto/Brevoverskriftsdata.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/dto/Brevoverskriftsdata.kt new file mode 100644 index 000000000..f266991ab --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/dto/Brevoverskriftsdata.kt @@ -0,0 +1,12 @@ +package no.nav.familie.tilbake.dokumentbestilling.handlebars.dto + +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata + +data class Brevoverskriftsdata(val brevmetadata: Brevmetadata) : BaseDokument( + brevmetadata.ytelsestype, + brevmetadata.språkkode, + brevmetadata.behandlendeEnhetsNavn, + brevmetadata.ansvarligSaksbehandler, + brevmetadata.gjelderDødsfall, + brevmetadata.institusjon, +) diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/dto/Spr\303\245kst\303\270tte.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/dto/Spr\303\245kst\303\270tte.kt" new file mode 100644 index 000000000..8ad04cad1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/dto/Spr\303\245kst\303\270tte.kt" @@ -0,0 +1,8 @@ +package no.nav.familie.tilbake.dokumentbestilling.handlebars.dto + +import no.nav.familie.kontrakter.felles.Språkkode + +interface Språkstøtte { + + val språkkode: Språkkode +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/HenleggelsesbrevService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/HenleggelsesbrevService.kt new file mode 100644 index 000000000..f17ba3fe0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/HenleggelsesbrevService.kt @@ -0,0 +1,194 @@ +package no.nav.familie.tilbake.dokumentbestilling.henleggelse + +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.dokumentbestilling.DistribusjonshåndteringService +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmetadataUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmottagerUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingService +import no.nav.familie.tilbake.dokumentbestilling.felles.EksterneDataForBrevService +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.Brevdata +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.PdfBrevService +import no.nav.familie.tilbake.dokumentbestilling.fritekstbrev.Fritekstbrevsdata +import no.nav.familie.tilbake.dokumentbestilling.henleggelse.handlebars.dto.Henleggelsesbrevsdokument +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import no.nav.familie.tilbake.organisasjon.OrganisasjonService +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class HenleggelsesbrevService( + private val behandlingRepository: BehandlingRepository, + private val brevsporingService: BrevsporingService, + private val fagsakRepository: FagsakRepository, + private val eksterneDataForBrevService: EksterneDataForBrevService, + private val pdfBrevService: PdfBrevService, + private val organisasjonService: OrganisasjonService, + private val distribusjonshåndteringService: DistribusjonshåndteringService, + private val brevmetadataUtil: BrevmetadataUtil, +) { + + fun sendHenleggelsebrev(behandlingId: UUID, fritekst: String?, brevmottager: Brevmottager? = null) { + val behandling: Behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + if (brevmottager == null) { + distribusjonshåndteringService.sendBrev(behandling, Brevtype.HENLEGGELSE) { brevmottaker, brevmetadata -> + val henleggelsesbrevSamletInfo = lagHenleggelsebrev(behandling, fagsak, fritekst, brevmottaker, brevmetadata) + val fritekstbrevData: Fritekstbrevsdata = + if (Behandlingstype.TILBAKEKREVING == behandling.type) { + lagHenleggelsesbrev(henleggelsesbrevSamletInfo) + } else { + lagRevurderingHenleggelsebrev(henleggelsesbrevSamletInfo) + } + Brevdata( + mottager = brevmottaker, + metadata = fritekstbrevData.brevmetadata, + overskrift = fritekstbrevData.overskrift, + brevtekst = fritekstbrevData.brevtekst, + ) + } + } else { + val henleggelsesbrevSamletInfo = lagHenleggelsebrev(behandling, fagsak, fritekst, brevmottager) + val fritekstbrevData: Fritekstbrevsdata = + if (Behandlingstype.TILBAKEKREVING == behandling.type) { + lagHenleggelsesbrev(henleggelsesbrevSamletInfo) + } else { + lagRevurderingHenleggelsebrev(henleggelsesbrevSamletInfo) + } + pdfBrevService.sendBrev( + behandling, + fagsak, + Brevtype.HENLEGGELSE, + Brevdata( + mottager = brevmottager, + metadata = fritekstbrevData.brevmetadata, + overskrift = fritekstbrevData.overskrift, + brevtekst = fritekstbrevData.brevtekst, + ), + ) + } + } + + fun hentForhåndsvisningHenleggelsesbrev(behandlingUuid: UUID, fritekst: String?): ByteArray { + val behandling: Behandling = behandlingRepository.findByIdOrThrow(behandlingUuid) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val (metadata, brevmottager) = + brevmetadataUtil.lagBrevmetadataForMottakerTilForhåndsvisning(behandling.id) + val henleggelsesbrevSamletInfo = lagHenleggelsebrev(behandling, fagsak, fritekst, brevmottager, metadata) + val fritekstbrevData: Fritekstbrevsdata = + if (Behandlingstype.TILBAKEKREVING == behandling.type) { + lagHenleggelsesbrev(henleggelsesbrevSamletInfo) + } else { + lagRevurderingHenleggelsebrev(henleggelsesbrevSamletInfo) + } + return pdfBrevService.genererForhåndsvisning( + Brevdata( + mottager = brevmottager, + metadata = fritekstbrevData.brevmetadata, + overskrift = fritekstbrevData.overskrift, + brevtekst = fritekstbrevData.brevtekst, + ), + ) + } + + private fun lagHenleggelsebrev( + behandling: Behandling, + fagsak: Fagsak, + fritekst: String?, + brevmottager: Brevmottager, + forhåndsgenerertMetadata: Brevmetadata? = null, + ): Henleggelsesbrevsdokument { + val brevSporing = brevsporingService.finnSisteVarsel(behandling.id) + if (Behandlingstype.TILBAKEKREVING == behandling.type && brevSporing == null) { + throw IllegalStateException( + "Varselbrev er ikke sendt. Kan ikke forhåndsvise/sende " + + "henleggelsesbrev for behandlingId=${behandling.id} når varsel ikke er sendt.", + ) + } else if (Behandlingstype.REVURDERING_TILBAKEKREVING == behandling.type && fritekst.isNullOrEmpty()) { + throw IllegalStateException( + "Kan ikke forhåndsvise/sende henleggelsesbrev uten fritekst for " + + "Tilbakekreving Revurdering med behandlingsid=${behandling.id}.", + ) + } + + val ansvarligSaksbehandler = if (behandling.ansvarligSaksbehandler == Constants.BRUKER_ID_VEDTAKSLØSNINGEN) { + SIGNATUR_AUTOMATISK_HENLEGGELSESBREV + } else { + eksterneDataForBrevService.hentPåloggetSaksbehandlernavnMedDefault(behandling.ansvarligSaksbehandler) + } + + val metadata = forhåndsgenerertMetadata ?: run { + val personinfo: Personinfo = eksterneDataForBrevService.hentPerson(fagsak.bruker.ident, fagsak.fagsystem) + val adresseinfo: Adresseinfo = eksterneDataForBrevService.hentAdresse( + personinfo, + brevmottager, + behandling.aktivVerge, + fagsak.fagsystem, + ) + + val vergenavn: String = BrevmottagerUtil.getVergenavn(behandling.aktivVerge, adresseinfo) + val gjelderDødsfall = personinfo.dødsdato != null + + Brevmetadata( + sakspartId = personinfo.ident, + sakspartsnavn = personinfo.navn, + finnesVerge = behandling.harVerge, + vergenavn = vergenavn, + mottageradresse = adresseinfo, + behandlendeEnhetId = behandling.behandlendeEnhet, + behandlendeEnhetsNavn = behandling.behandlendeEnhetsNavn, + ansvarligSaksbehandler = ansvarligSaksbehandler, + saksnummer = fagsak.eksternFagsakId, + språkkode = fagsak.bruker.språkkode, + ytelsestype = fagsak.ytelsestype, + gjelderDødsfall = gjelderDødsfall, + institusjon = fagsak.institusjon?.let { + organisasjonService.mapTilInstitusjonForBrevgenerering(it.organisasjonsnummer) + }, + ) + } + + return Henleggelsesbrevsdokument( + metadata.copy( + tittel = TITTEL_HENLEGGELSESBREV, + behandlingstype = behandling.type, + ansvarligSaksbehandler = ansvarligSaksbehandler, + ), + brevSporing?.sporbar?.opprettetTid?.toLocalDate(), + fritekst, + ) + } + + private fun lagHenleggelsesbrev(dokument: Henleggelsesbrevsdokument): Fritekstbrevsdata { + return Fritekstbrevsdata( + TekstformatererHenleggelsesbrev.lagOverskrift(dokument.brevmetadata), + TekstformatererHenleggelsesbrev.lagFritekst(dokument), + dokument.brevmetadata, + ) + } + + private fun lagRevurderingHenleggelsebrev(dokument: Henleggelsesbrevsdokument): Fritekstbrevsdata { + return Fritekstbrevsdata( + TekstformatererHenleggelsesbrev.lagRevurderingsoverskrift(dokument.brevmetadata), + TekstformatererHenleggelsesbrev.lagRevurderingsfritekst(dokument), + dokument.brevmetadata, + ) + } + + companion object { + + private const val TITTEL_HENLEGGELSESBREV = "Informasjon om at tilbakekrevingssaken er henlagt" + private const val SIGNATUR_AUTOMATISK_HENLEGGELSESBREV = """ + +Henleggelsen er gjort automatisk. Brevet er derfor ikke underskrevet av saksbehandler.""" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/SendHenleggelsesbrevTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/SendHenleggelsesbrevTask.kt new file mode 100644 index 000000000..66f4cd4be --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/SendHenleggelsesbrevTask.kt @@ -0,0 +1,60 @@ +package no.nav.familie.tilbake.dokumentbestilling.henleggelse + +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.config.PropertyName +import org.springframework.stereotype.Component +import java.time.LocalDateTime +import java.util.Properties +import java.util.UUID + +@Component +@TaskStepBeskrivelse( + taskStepType = SendHenleggelsesbrevTask.TYPE, + maxAntallFeil = 50, + triggerTidVedFeilISekunder = 15 * 60L, + beskrivelse = "Send henleggelsesbrev.", +) +class SendHenleggelsesbrevTask( + private val henleggelsesbrevService: HenleggelsesbrevService, + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, + private val featureToggleService: FeatureToggleService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val taskdata: SendBrevTaskdata = objectMapper.readValue(task.payload) + val behandling = behandlingRepository.findByIdOrThrow(taskdata.behandlingId) + henleggelsesbrevService.sendHenleggelsebrev(behandling.id, taskdata.fritekst) + } + + companion object { + + fun opprettTask( + behandlingId: UUID, + fagsystem: Fagsystem, + fritekst: String?, + ): Task = + Task( + type = TYPE, + payload = objectMapper.writeValueAsString(SendBrevTaskdata(behandlingId, fritekst)), + properties = Properties().apply { setProperty(PropertyName.FAGSYSTEM, fagsystem.name) }, + ) + .medTriggerTid(LocalDateTime.now().plusSeconds(15)) + + const val TYPE = "distribuerHenleggelsesbrev" + } +} + +data class SendBrevTaskdata( + val behandlingId: UUID, + val fritekst: String?, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/TekstformatererHenleggelsesbrev.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/TekstformatererHenleggelsesbrev.kt new file mode 100644 index 000000000..a6b3db006 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/TekstformatererHenleggelsesbrev.kt @@ -0,0 +1,28 @@ +package no.nav.familie.tilbake.dokumentbestilling.henleggelse + +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.handlebars.FellesTekstformaterer +import no.nav.familie.tilbake.dokumentbestilling.handlebars.dto.Brevoverskriftsdata +import no.nav.familie.tilbake.dokumentbestilling.henleggelse.handlebars.dto.Henleggelsesbrevsdokument + +internal object TekstformatererHenleggelsesbrev { + + fun lagFritekst(dokument: Henleggelsesbrevsdokument): String { + return FellesTekstformaterer.lagBrevtekst(dokument, "henleggelse/henleggelse") + } + + fun lagOverskrift(brevmetadata: Brevmetadata): String { + return FellesTekstformaterer.lagBrevtekst(Brevoverskriftsdata(brevmetadata), "henleggelse/henleggelse_overskrift") + } + + fun lagRevurderingsfritekst(dokument: Henleggelsesbrevsdokument): String { + return FellesTekstformaterer.lagBrevtekst(dokument, "henleggelse/henleggelse_revurdering") + } + + fun lagRevurderingsoverskrift(brevmetadata: Brevmetadata): String { + return FellesTekstformaterer.lagBrevtekst( + Brevoverskriftsdata(brevmetadata), + "henleggelse/henleggelse_revurdering_overskrift", + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/handlebars/dto/Henleggelsesbrevsdokument.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/handlebars/dto/Henleggelsesbrevsdokument.kt new file mode 100644 index 000000000..0425918c2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/handlebars/dto/Henleggelsesbrevsdokument.kt @@ -0,0 +1,45 @@ +package no.nav.familie.tilbake.dokumentbestilling.henleggelse.handlebars.dto + +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmottagerUtil +import no.nav.familie.tilbake.dokumentbestilling.handlebars.dto.BaseDokument +import java.time.LocalDate +import java.util.Objects + +data class Henleggelsesbrevsdokument( + val brevmetadata: Brevmetadata, + val varsletDato: LocalDate?, + val fritekstFraSaksbehandler: String?, +) : BaseDokument( + brevmetadata.ytelsestype, + brevmetadata.språkkode, + brevmetadata.behandlendeEnhetsNavn, + brevmetadata.ansvarligSaksbehandler, + brevmetadata.gjelderDødsfall, + brevmetadata.institusjon, +) { + + private val tilbakekrevingsrevurdering = Behandlingstype.REVURDERING_TILBAKEKREVING == brevmetadata.behandlingstype + + val finnesVerge: Boolean = brevmetadata.finnesVerge + + val annenMottagersNavn: String? = BrevmottagerUtil.getAnnenMottagersNavn(brevmetadata) + + init { + if (finnesVerge) { + Objects.requireNonNull(annenMottagersNavn, "annenMottagersNavn kan ikke være null") + } + } + + fun init() { + if (tilbakekrevingsrevurdering) { + requireNotNull(fritekstFraSaksbehandler) { "fritekst kan ikke være null" } + } else { + requireNotNull(varsletDato) { "varsletDato kan ikke være null" } + } + if (finnesVerge) { + requireNotNull(annenMottagersNavn) { "annenMottagersNavn kan ikke være null" } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/InnhentDokumentasjonbrevService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/InnhentDokumentasjonbrevService.kt new file mode 100644 index 000000000..a857c61b0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/InnhentDokumentasjonbrevService.kt @@ -0,0 +1,157 @@ +package no.nav.familie.tilbake.dokumentbestilling.innhentdokumentasjon + +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.dokumentbestilling.DistribusjonshåndteringService +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmetadataUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmottagerUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.EksterneDataForBrevService +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.Brevdata +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.PdfBrevService +import no.nav.familie.tilbake.dokumentbestilling.fritekstbrev.Fritekstbrevsdata +import no.nav.familie.tilbake.dokumentbestilling.innhentdokumentasjon.handlebars.dto.InnhentDokumentasjonsbrevsdokument +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import no.nav.familie.tilbake.organisasjon.OrganisasjonService +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class InnhentDokumentasjonbrevService( + private val fagsakRepository: FagsakRepository, + private val behandlingRepository: BehandlingRepository, + private val eksterneDataForBrevService: EksterneDataForBrevService, + private val pdfBrevService: PdfBrevService, + private val organisasjonService: OrganisasjonService, + private val distribusjonshåndteringService: DistribusjonshåndteringService, + private val brevmetadataUtil: BrevmetadataUtil, +) { + + fun sendInnhentDokumentasjonBrev(behandling: Behandling, fritekst: String, brevmottager: Brevmottager? = null) { + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + if (brevmottager == null) { + distribusjonshåndteringService.sendBrev(behandling, Brevtype.INNHENT_DOKUMENTASJON) { mottaker, brevmetadata -> + val dokument = settOppInnhentDokumentasjonsbrevsdokument(behandling, fagsak, fritekst, mottaker, brevmetadata) + val fritekstbrevsdata: Fritekstbrevsdata = lagInnhentDokumentasjonsbrev(dokument) + Brevdata( + mottager = mottaker, + metadata = fritekstbrevsdata.brevmetadata, + overskrift = fritekstbrevsdata.overskrift, + brevtekst = fritekstbrevsdata.brevtekst, + ) + } + } else { + val dokument = settOppInnhentDokumentasjonsbrevsdokument(behandling, fagsak, fritekst, brevmottager) + val fritekstbrevsdata: Fritekstbrevsdata = lagInnhentDokumentasjonsbrev(dokument) + val brevdata = Brevdata( + mottager = brevmottager, + metadata = fritekstbrevsdata.brevmetadata, + overskrift = fritekstbrevsdata.overskrift, + brevtekst = fritekstbrevsdata.brevtekst, + ) + pdfBrevService.sendBrev( + behandling = behandling, + fagsak = fagsak, + brevtype = Brevtype.INNHENT_DOKUMENTASJON, + data = brevdata, + fritekst = fritekst, + ) + } + } + + fun hentForhåndsvisningInnhentDokumentasjonBrev( + behandlingId: UUID, + fritekst: String, + ): ByteArray { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val (metadata, brevmottager) = + brevmetadataUtil.lagBrevmetadataForMottakerTilForhåndsvisning(behandlingId) + val dokument = settOppInnhentDokumentasjonsbrevsdokument(behandling, fagsak, fritekst, brevmottager, metadata) + val fritekstbrevsdata: Fritekstbrevsdata = lagInnhentDokumentasjonsbrev(dokument) + + return pdfBrevService.genererForhåndsvisning( + Brevdata( + mottager = brevmottager, + metadata = fritekstbrevsdata.brevmetadata, + overskrift = fritekstbrevsdata.overskrift, + brevtekst = fritekstbrevsdata.brevtekst, + ), + ) + } + + private fun lagInnhentDokumentasjonsbrev(dokument: InnhentDokumentasjonsbrevsdokument): Fritekstbrevsdata { + val overskrift = + TekstformatererInnhentDokumentasjonsbrev.lagOverskrift(dokument.brevmetadata) + val brevtekst = TekstformatererInnhentDokumentasjonsbrev.lagFritekst(dokument) + return Fritekstbrevsdata( + overskrift = overskrift, + brevtekst = brevtekst, + brevmetadata = dokument.brevmetadata, + ) + } + + private fun settOppInnhentDokumentasjonsbrevsdokument( + behandling: Behandling, + fagsak: Fagsak, + fritekst: String, + brevmottager: Brevmottager, + forhåndsgenerertMetadata: Brevmetadata? = null, + ): InnhentDokumentasjonsbrevsdokument { + val brevmetadata = forhåndsgenerertMetadata ?: run { + val personinfo: Personinfo = eksterneDataForBrevService.hentPerson(fagsak.bruker.ident, fagsak.fagsystem) + val adresseinfo: Adresseinfo = + eksterneDataForBrevService.hentAdresse(personinfo, brevmottager, behandling.aktivVerge, fagsak.fagsystem) + val vergenavn = BrevmottagerUtil.getVergenavn(behandling.aktivVerge, adresseinfo) + val ansvarligSaksbehandler = + eksterneDataForBrevService.hentPåloggetSaksbehandlernavnMedDefault(behandling.ansvarligSaksbehandler) + val gjelderDødsfall = personinfo.dødsdato != null + + Brevmetadata( + sakspartId = personinfo.ident, + sakspartsnavn = personinfo.navn, + finnesVerge = behandling.harVerge, + vergenavn = vergenavn, + mottageradresse = adresseinfo, + behandlendeEnhetId = behandling.behandlendeEnhet, + behandlendeEnhetsNavn = behandling.behandlendeEnhetsNavn, + ansvarligSaksbehandler = ansvarligSaksbehandler, + saksnummer = fagsak.eksternFagsakId, + språkkode = fagsak.bruker.språkkode, + ytelsestype = fagsak.ytelsestype, + gjelderDødsfall = gjelderDødsfall, + institusjon = fagsak.institusjon?.let { + organisasjonService.mapTilInstitusjonForBrevgenerering(it.organisasjonsnummer) + }, + ) + } + return InnhentDokumentasjonsbrevsdokument( + brevmetadata = brevmetadata.copy(tittel = getTittel(brevmottager) + fagsak.ytelsestype.navn[Språkkode.NB]), + fristdato = Constants.brukersSvarfrist(), + fritekstFraSaksbehandler = fritekst, + ) + } + + private fun getTittel(brevmottager: Brevmottager): String { + return if (Brevmottager.VERGE == brevmottager) { + TITTEL_INNHENTDOKUMENTASJONBREV_HISTORIKKINNSLAG_TIL_VERGE + } else { + TITTEL_INNHENTDOKUMENTASJONBREV_HISTORIKKINNSLAG + } + } + + companion object { + + const val TITTEL_INNHENTDOKUMENTASJONBREV_HISTORIKKINNSLAG = "Innhent dokumentasjon Tilbakekreving" + const val TITTEL_INNHENTDOKUMENTASJONBREV_HISTORIKKINNSLAG_TIL_VERGE = + "Innhent dokumentasjon Tilbakekreving til verge" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/InnhentDokumentasjonbrevTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/InnhentDokumentasjonbrevTask.kt new file mode 100644 index 000000000..946ffc9af --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/InnhentDokumentasjonbrevTask.kt @@ -0,0 +1,86 @@ +package no.nav.familie.tilbake.dokumentbestilling.innhentdokumentasjon + +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.Properties +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = InnhentDokumentasjonbrevTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Sender innhent dokumentasjonsbrev", + triggerTidVedFeilISekunder = 60 * 5L, +) +class InnhentDokumentasjonbrevTask( + private val behandlingRepository: BehandlingRepository, + private val innhentDokumentasjonBrevService: InnhentDokumentasjonbrevService, + private val behandlingskontrollService: BehandlingskontrollService, + private val oppgaveTaskService: OppgaveTaskService, + private val fagsakRepository: FagsakRepository, + private val featureToggleService: FeatureToggleService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val taskdata: InnhentDokumentasjonbrevTaskdata = objectMapper.readValue(task.payload) + val behandling = behandlingRepository.findByIdOrThrow(taskdata.behandlingId) + val fritekst: String = taskdata.fritekst + + innhentDokumentasjonBrevService.sendInnhentDokumentasjonBrev(behandling, fritekst) + + val fristTid = Constants.saksbehandlersTidsfrist() + oppgaveTaskService.oppdaterOppgaveTask( + behandlingId = behandling.id, + beskrivelse = "Frist er oppdatert. Saksbehandler ${behandling + .ansvarligSaksbehandler} har bedt om mer informasjon av bruker", + frist = fristTid, + saksbehandler = behandling.ansvarligSaksbehandler, + ) + // Oppdaterer fristen dersom tasken har tidligere feilet. Behandling ble satt på vent i DokumentBehandlingService. + if (task.opprettetTid.toLocalDate() < LocalDate.now()) { + behandlingskontrollService.settBehandlingPåVent( + behandling.id, + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + fristTid, + ) + } + } + + companion object { + + fun opprettTask( + behandlingId: UUID, + fagsystem: Fagsystem, + fritekst: String, + ): Task = + Task( + type = TYPE, + payload = objectMapper.writeValueAsString(InnhentDokumentasjonbrevTaskdata(behandlingId, fritekst)), + properties = Properties().apply { setProperty(PropertyName.FAGSYSTEM, fagsystem.name) }, + ) + .medTriggerTid(LocalDateTime.now().plusSeconds(15)) + + const val TYPE = "brev.sendInnhentDokumentasjon" + } +} + +data class InnhentDokumentasjonbrevTaskdata( + val behandlingId: UUID, + val fritekst: String, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/TekstformatererInnhentDokumentasjonsbrev.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/TekstformatererInnhentDokumentasjonsbrev.kt new file mode 100644 index 000000000..02da6c09c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/TekstformatererInnhentDokumentasjonsbrev.kt @@ -0,0 +1,16 @@ +package no.nav.familie.tilbake.dokumentbestilling.innhentdokumentasjon + +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.handlebars.FellesTekstformaterer +import no.nav.familie.tilbake.dokumentbestilling.innhentdokumentasjon.handlebars.dto.InnhentDokumentasjonsbrevsdokument + +internal object TekstformatererInnhentDokumentasjonsbrev { + + fun lagFritekst(dokument: InnhentDokumentasjonsbrevsdokument): String { + return FellesTekstformaterer.lagBrevtekst(dokument, "innhentdokumentasjon/innhent_dokumentasjon") + } + + fun lagOverskrift(brevmetadata: Brevmetadata): String { + return FellesTekstformaterer.lagBrevtekst(brevmetadata, "innhentdokumentasjon/innhent_dokumentasjon_overskrift") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/handlebars/dto/InnhentDokumentasjonsbrevsdokument.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/handlebars/dto/InnhentDokumentasjonsbrevsdokument.kt new file mode 100644 index 000000000..c7374f76d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/handlebars/dto/InnhentDokumentasjonsbrevsdokument.kt @@ -0,0 +1,41 @@ +package no.nav.familie.tilbake.dokumentbestilling.innhentdokumentasjon.handlebars.dto + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmottagerUtil +import no.nav.familie.tilbake.dokumentbestilling.handlebars.dto.BaseDokument +import java.time.LocalDate +import java.util.Objects + +data class InnhentDokumentasjonsbrevsdokument( + val brevmetadata: Brevmetadata, + val fritekstFraSaksbehandler: String, + val fristdato: LocalDate, +) : BaseDokument( + brevmetadata.ytelsestype, + brevmetadata.språkkode, + brevmetadata.behandlendeEnhetsNavn, + brevmetadata.ansvarligSaksbehandler, + brevmetadata.gjelderDødsfall, + brevmetadata.institusjon, +) { + + val finnesVerge: Boolean = brevmetadata.finnesVerge + + val annenMottagersNavn: String? = BrevmottagerUtil.getAnnenMottagersNavn(brevmetadata) + + @Suppress("unused") // Handlebars + val isRentepliktig = ytelsestype != Ytelsestype.BARNETRYGD && ytelsestype != Ytelsestype.KONTANTSTØTTE + + @Suppress("unused") // Handlebars + val isBarnetrygd = ytelsestype == Ytelsestype.BARNETRYGD + + @Suppress("unused") // Handlebars + val isKontantstøtte = ytelsestype == Ytelsestype.KONTANTSTØTTE + + init { + if (finnesVerge) { + Objects.requireNonNull(annenMottagersNavn, "annenMottagersNavn kan ikke være null") + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/ManuellBrevmottakerMapper.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/ManuellBrevmottakerMapper.kt new file mode 100644 index 000000000..e91b8f701 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/ManuellBrevmottakerMapper.kt @@ -0,0 +1,48 @@ +package no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker + +import no.nav.familie.kontrakter.felles.tilbakekreving.Brevmottaker +import no.nav.familie.kontrakter.felles.tilbakekreving.ManuellAdresseInfo +import no.nav.familie.tilbake.api.dto.ManuellBrevmottakerRequestDto +import no.nav.familie.tilbake.api.dto.ManuellBrevmottakerResponsDto +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.domene.ManuellBrevmottaker +import java.util.UUID + +object ManuellBrevmottakerMapper { + + fun tilDomene(behandlingId: UUID, manuellBrevmottakerRequestDto: ManuellBrevmottakerRequestDto, navnFraRegister: String?) = + ManuellBrevmottaker( + behandlingId = behandlingId, + type = manuellBrevmottakerRequestDto.type, + navn = navnFraRegister ?: manuellBrevmottakerRequestDto.navn, + ident = manuellBrevmottakerRequestDto.personIdent, + orgNr = manuellBrevmottakerRequestDto.organisasjonsnummer, + adresselinje1 = manuellBrevmottakerRequestDto.manuellAdresseInfo?.adresselinje1, + adresselinje2 = manuellBrevmottakerRequestDto.manuellAdresseInfo?.adresselinje2, + postnummer = manuellBrevmottakerRequestDto.manuellAdresseInfo?.postnummer?.trim(), + poststed = manuellBrevmottakerRequestDto.manuellAdresseInfo?.poststed?.trim(), + landkode = manuellBrevmottakerRequestDto.manuellAdresseInfo?.landkode, + vergetype = manuellBrevmottakerRequestDto.vergetype, + ) + + fun tilRespons(manuellBrevmottaker: ManuellBrevmottaker) = ManuellBrevmottakerResponsDto( + id = manuellBrevmottaker.id, + brevmottaker = Brevmottaker( + type = manuellBrevmottaker.type, + navn = manuellBrevmottaker.navn, + personIdent = manuellBrevmottaker.ident, + organisasjonsnummer = manuellBrevmottaker.orgNr, + manuellAdresseInfo = if (manuellBrevmottaker.hasManuellAdresse()) { + ManuellAdresseInfo( + adresselinje1 = manuellBrevmottaker.adresselinje1!!, + adresselinje2 = manuellBrevmottaker.adresselinje2, + postnummer = manuellBrevmottaker.postnummer!!, + poststed = manuellBrevmottaker.poststed!!, + landkode = manuellBrevmottaker.landkode!!, + ) + } else { + null + }, + vergetype = manuellBrevmottaker.vergetype, + ), + ) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/ManuellBrevmottakerRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/ManuellBrevmottakerRepository.kt new file mode 100644 index 000000000..dc559644e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/ManuellBrevmottakerRepository.kt @@ -0,0 +1,15 @@ +package no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker + +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.domene.ManuellBrevmottaker +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository +interface ManuellBrevmottakerRepository : + RepositoryInterface, + InsertUpdateRepository { + + fun findByBehandlingId(behandlingId: UUID): List +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/ManuellBrevmottakerService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/ManuellBrevmottakerService.kt new file mode 100644 index 000000000..3bb58e908 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/ManuellBrevmottakerService.kt @@ -0,0 +1,225 @@ +package no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker + +import no.nav.familie.kontrakter.felles.dokdist.AdresseType +import no.nav.familie.kontrakter.felles.dokdist.AdresseType.norskPostadresse +import no.nav.familie.kontrakter.felles.dokdist.AdresseType.utenlandskPostadresse +import no.nav.familie.kontrakter.felles.dokdist.ManuellAdresse +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.tilbakekreving.MottakerType.BRUKER_MED_UTENLANDSK_ADRESSE +import no.nav.familie.tilbake.api.dto.ManuellBrevmottakerRequestDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakService +import no.nav.familie.tilbake.behandling.ValiderBrevmottakerService +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.domene.ManuellBrevmottaker +import no.nav.familie.tilbake.historikkinnslag.HistorikkService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import no.nav.familie.tilbake.integration.pdl.PdlClient +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.util.UUID +import kotlin.jvm.optionals.getOrNull + +@Service +class ManuellBrevmottakerService( + private val manuellBrevmottakerRepository: ManuellBrevmottakerRepository, + private val historikkService: HistorikkService, + private val behandlingRepository: BehandlingRepository, + private val behandlingskontrollService: BehandlingskontrollService, + private val fagsakService: FagsakService, + private val pdlClient: PdlClient, + private val integrasjonerClient: IntegrasjonerClient, + private val validerBrevmottakerService: ValiderBrevmottakerService, +) { + + @Transactional + fun leggTilBrevmottaker(behandlingId: UUID, requestDto: ManuellBrevmottakerRequestDto): UUID { + val navnFraRegister: String? = hentPersonEllerOrganisasjonNavnFraRegister(requestDto, behandlingId) + val manuellBrevmottaker = ManuellBrevmottakerMapper.tilDomene(behandlingId, requestDto, navnFraRegister) + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + validerBrevmottakerService.validerAtBehandlingenIkkeInneholderStrengtFortroligPerson(behandlingId = behandling.id, fagsakId = behandling.fagsakId) + val id = manuellBrevmottakerRepository.insert(manuellBrevmottaker).id + historikkService.lagHistorikkinnslag( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BREVMOTTAKER_LAGT_TIL, + aktør = Aktør.SAKSBEHANDLER, + opprettetTidspunkt = LocalDateTime.now(), + beskrivelse = lagHistorikkBeskrivelseForBrevmottaker(manuellBrevmottaker), + tittel = "${manuellBrevmottaker.type.visningsnavn} er lagt til som brevmottaker", + ) + return id + } + + fun hentBrevmottakere(behandlingId: UUID) = manuellBrevmottakerRepository.findByBehandlingId(behandlingId) + + @Transactional + fun oppdaterBrevmottaker( + behandlingId: UUID, + manuellBrevmottakerId: UUID, + manuellBrevmottakerRequestDto: ManuellBrevmottakerRequestDto, + ) { + val manuellBrevmottaker = manuellBrevmottakerRepository.findById(manuellBrevmottakerId).getOrNull() + ?: throw Feil("Finnes ikke brevmottakere med id=$manuellBrevmottakerId") + + val oppdatertBrevmottaker = manuellBrevmottaker.copy( + type = manuellBrevmottakerRequestDto.type, + navn = hentPersonEllerOrganisasjonNavnFraRegister(manuellBrevmottakerRequestDto, behandlingId) + ?: manuellBrevmottakerRequestDto.navn, + ident = manuellBrevmottakerRequestDto.personIdent, + orgNr = manuellBrevmottakerRequestDto.organisasjonsnummer, + adresselinje1 = manuellBrevmottakerRequestDto.manuellAdresseInfo?.adresselinje1, + adresselinje2 = manuellBrevmottakerRequestDto.manuellAdresseInfo?.adresselinje2, + postnummer = manuellBrevmottakerRequestDto.manuellAdresseInfo?.postnummer, + poststed = manuellBrevmottakerRequestDto.manuellAdresseInfo?.poststed, + landkode = manuellBrevmottakerRequestDto.manuellAdresseInfo?.landkode, + vergetype = manuellBrevmottakerRequestDto.vergetype, + ) + + val historikkinnslagtittel = + if (manuellBrevmottaker.type == oppdatertBrevmottaker.type) { + "${manuellBrevmottaker.type.visningsnavn} er endret" + } else { + "${manuellBrevmottaker.type.visningsnavn} er endret til ${oppdatertBrevmottaker.type.visningsnavn}" + } + + historikkService.lagHistorikkinnslag( + behandlingId = manuellBrevmottaker.behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BREVMOTTAKER_ENDRET, + aktør = Aktør.SAKSBEHANDLER, + opprettetTidspunkt = LocalDateTime.now(), + beskrivelse = lagHistorikkBeskrivelseForBrevmottaker(oppdatertBrevmottaker), + tittel = historikkinnslagtittel, + ) + + manuellBrevmottakerRepository.update(oppdatertBrevmottaker) + } + + @Transactional + fun fjernBrevmottaker(behandlingId: UUID, manuellBrevmottakerId: UUID) { + val manuellBrevmottakere = manuellBrevmottakerRepository.findByBehandlingId(behandlingId) + if (manuellBrevmottakere.none { it.id == manuellBrevmottakerId }) { + throw Feil("Finnes ikke brevmottakere med id=$manuellBrevmottakerId for behandlingId=$behandlingId") + } + fjernBrevmottakerOgLagHistorikkinnslag( + manuellBrevmottakere.single { it.id == manuellBrevmottakerId }, + behandlingId, + ) + } + + @Transactional + fun opprettBrevmottakerSteg(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + validerBrevmottakerStegopprettelse(behandling) + behandlingskontrollService.behandleBrevmottakerSteg(behandlingId) + } + + @Transactional + fun fjernManuelleBrevmottakereOgTilbakeførSteg(behandlingId: UUID) { + hentBrevmottakere(behandlingId).forEach { manuellBrevmottaker -> + fjernBrevmottakerOgLagHistorikkinnslag(manuellBrevmottaker, behandlingId) + } + + behandlingskontrollService.oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + Behandlingssteg.BREVMOTTAKER, + Behandlingsstegstatus.TILBAKEFØRT, + ), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + private fun fjernBrevmottakerOgLagHistorikkinnslag(manuellBrevmottaker: ManuellBrevmottaker, behandlingId: UUID) { + historikkService.lagHistorikkinnslag( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BREVMOTTAKER_FJERNET, + aktør = Aktør.SAKSBEHANDLER, + opprettetTidspunkt = LocalDateTime.now(), + beskrivelse = lagHistorikkBeskrivelseForBrevmottaker(manuellBrevmottaker), + tittel = "${manuellBrevmottaker.type.visningsnavn} er fjernet som brevmottaker", + ) + + manuellBrevmottakerRepository.deleteById(manuellBrevmottaker.id) + } + + private fun validerBrevmottakerStegopprettelse(behandling: Behandling) { + if (behandling.erSaksbehandlingAvsluttet) { + throw Feil( + "Behandling med id=${behandling.id} er allerede ferdig behandlet.", + frontendFeilmelding = "Behandling med id=${behandling.id} er allerede ferdig behandlet.", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + if (behandlingskontrollService.erBehandlingPåVent(behandling.id)) { + throw Feil( + "Behandling med id=${behandling.id} er på vent.", + frontendFeilmelding = "Behandling med id=${behandling.id} er på vent.", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + validerBrevmottakerService.validerAtBehandlingenIkkeInneholderStrengtFortroligPerson(behandlingId = behandling.id, fagsakId = behandling.fagsakId) + } + + private fun lagHistorikkBeskrivelseForBrevmottaker(brevmottaker: ManuellBrevmottaker) = + listOfNotNull( + brevmottaker.navn, + brevmottaker.adresselinje1, + brevmottaker.adresselinje2, + brevmottaker.postnummer, + brevmottaker.poststed, + brevmottaker.landkode, + ).joinToString(separator = System.lineSeparator()) + + private fun hentPersonEllerOrganisasjonNavnFraRegister( + dto: ManuellBrevmottakerRequestDto, + behandlingId: UUID, + ): String? { + return dto.personIdent?.let { + pdlClient.hentPersoninfo( + ident = it, + fagsystem = fagsakService.finnFagsystemForBehandlingId(behandlingId), + ).navn + } ?: dto.organisasjonsnummer?.let { + if (!integrasjonerClient.validerOrganisasjon(it)) { + throw Feil( + message = "Organisasjon $it er ikke gyldig", + frontendFeilmelding = "Organisasjon $it er ikke gyldig", + ) + } + integrasjonerClient.hentOrganisasjon(it).navn + if (dto.navn.isNotBlank()) " v/ ${dto.navn}" else "" + } + } +} + +private fun findAdresseType(brevmottaker: ManuellBrevmottaker): AdresseType { + return when { + brevmottaker.landkode == "NO" && brevmottaker.type != BRUKER_MED_UTENLANDSK_ADRESSE -> norskPostadresse + brevmottaker.landkode != "NO" && brevmottaker.type == BRUKER_MED_UTENLANDSK_ADRESSE -> utenlandskPostadresse + else -> throw Feil("landkode stemmer ikke overens med type for brevmottaker ${brevmottaker.id}") + } +} + +fun List.toManuelleAdresser(): List = + this.mapNotNull { manuellBrevmottaker -> + if (manuellBrevmottaker.hasManuellAdresse()) { + ManuellAdresse( + adresseType = findAdresseType(manuellBrevmottaker), + adresselinje1 = manuellBrevmottaker.adresselinje1, + adresselinje2 = manuellBrevmottaker.adresselinje2, + postnummer = manuellBrevmottaker.postnummer, + poststed = manuellBrevmottaker.poststed, + land = manuellBrevmottaker.landkode!!, + ) + } else { + null + } + } diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/domene/ManuellBrevmottaker.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/domene/ManuellBrevmottaker.kt new file mode 100644 index 000000000..7cbee2823 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/domene/ManuellBrevmottaker.kt @@ -0,0 +1,46 @@ +package no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.domene + +import no.nav.familie.kontrakter.felles.tilbakekreving.MottakerType +import no.nav.familie.kontrakter.felles.tilbakekreving.Vergetype +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Embedded +import java.util.UUID + +data class ManuellBrevmottaker( + @Id + val id: UUID = UUID.randomUUID(), + val behandlingId: UUID, + val type: MottakerType, + var vergetype: Vergetype? = null, + val navn: String, + val ident: String? = null, + @Column("org_nr") + val orgNr: String? = null, + @Column("adresselinje_1") + val adresselinje1: String? = null, + @Column("adresselinje_2") + val adresselinje2: String? = null, + val postnummer: String? = null, + val poststed: String? = null, + val landkode: String? = null, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) { + override fun toString(): String = "${javaClass.simpleName}(id=$id,behandlingId=$behandlingId)" + + fun hasManuellAdresse(): Boolean { + return !( + adresselinje1.isNullOrBlank() || + postnummer.isNullOrBlank() || + poststed.isNullOrBlank() || + landkode.isNullOrBlank() + ) + } + + val erTilleggsmottaker get() = type == MottakerType.VERGE || type == MottakerType.FULLMEKTIG +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/SendVarselbrevTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/SendVarselbrevTask.kt new file mode 100644 index 000000000..6f717f7b4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/SendVarselbrevTask.kt @@ -0,0 +1,37 @@ +package no.nav.familie.tilbake.dokumentbestilling.varsel + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.FeatureToggleService +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = SendVarselbrevTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Sender varselbrev", + triggerTidVedFeilISekunder = 60 * 5L, +) +class SendVarselbrevTask( + private val varselbrevService: VarselbrevService, + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, + private val featureToggleService: FeatureToggleService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val behandlingId = UUID.fromString(task.payload) + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + varselbrevService.sendVarselbrev(behandling) + } + + companion object { + + const val TYPE = "brev.sendVarsel" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/TekstformatererVarselbrev.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/TekstformatererVarselbrev.kt new file mode 100644 index 000000000..2896e1bf8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/TekstformatererVarselbrev.kt @@ -0,0 +1,24 @@ +package no.nav.familie.tilbake.dokumentbestilling.varsel + +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.handlebars.FellesTekstformaterer +import no.nav.familie.tilbake.dokumentbestilling.handlebars.dto.Brevoverskriftsdata +import no.nav.familie.tilbake.dokumentbestilling.varsel.handlebars.dto.Varselbrevsdokument +import no.nav.familie.tilbake.dokumentbestilling.varsel.handlebars.dto.Vedleggsdata + +object TekstformatererVarselbrev { + + fun lagFritekst(varselbrevsdokument: Varselbrevsdokument, erKorrigert: Boolean): String { + val filsti = if (erKorrigert) "varsel/korrigert_varsel" else "varsel/varsel" + return FellesTekstformaterer.lagBrevtekst(varselbrevsdokument, filsti) + } + + fun lagVarselbrevsoverskrift(brevmetadata: Brevmetadata, erKorrigert: Boolean): String { + val filsti = if (erKorrigert) "varsel/korrigert_varsel_overskrift" else "varsel/varsel_overskrift" + return FellesTekstformaterer.lagBrevtekst(Brevoverskriftsdata(brevmetadata), filsti) + } + + fun lagVarselbrevsvedleggHtml(vedleggsdata: Vedleggsdata): String { + return FellesTekstformaterer.lagBrevtekst(vedleggsdata, "varsel/vedlegg") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/VarselbrevService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/VarselbrevService.kt new file mode 100644 index 000000000..04960f3d1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/VarselbrevService.kt @@ -0,0 +1,174 @@ +package no.nav.familie.tilbake.dokumentbestilling.varsel + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.tilbakekreving.ForhåndsvisVarselbrevRequest +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandling.domain.Varsel +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.dokumentbestilling.DistribusjonshåndteringService +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmottagerUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.EksterneDataForBrevService +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.Brevdata +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.PdfBrevService +import no.nav.familie.tilbake.dokumentbestilling.fritekstbrev.Fritekstbrevsdata +import no.nav.familie.tilbake.dokumentbestilling.varsel.VarselbrevUtil.Companion.TITTEL_VARSEL_TILBAKEBETALING +import no.nav.familie.tilbake.dokumentbestilling.varsel.handlebars.dto.Varselbrevsdokument +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class VarselbrevService( + private val fagsakRepository: FagsakRepository, + private val eksterneDataForBrevService: EksterneDataForBrevService, + private val pdfBrevService: PdfBrevService, + private val varselbrevUtil: VarselbrevUtil, + private val distribusjonshåndteringService: DistribusjonshåndteringService, +) { + + fun sendVarselbrev(behandling: Behandling, brevmottager: Brevmottager? = null) { + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val varsletFeilutbetaling = behandling.aktivtVarsel?.varselbeløp ?: 0L + val fritekst = behandling.aktivtVarsel?.varseltekst + + if (brevmottager == null) { + distribusjonshåndteringService.sendBrev(behandling, Brevtype.VARSEL, varsletFeilutbetaling, fritekst) { brevmottaker, brevmetadata -> + val varselbrevsdokument = lagVarselbrevForSending(behandling, fagsak, brevmottaker, brevmetadata) + val overskrift = TekstformatererVarselbrev.lagVarselbrevsoverskrift(varselbrevsdokument.brevmetadata, false) + val brevtekst = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument, false) + + val vedlegg = varselbrevUtil.lagVedlegg( + varselbrevsdokument, + behandling.aktivFagsystemsbehandling.eksternId, + varselbrevsdokument.beløp, + ) + Brevdata( + mottager = brevmottaker, + metadata = varselbrevsdokument.brevmetadata, + overskrift = overskrift, + brevtekst = brevtekst, + vedleggHtml = vedlegg, + ) + } + } else { + val varselbrevsdokument = lagVarselbrevForSending(behandling, fagsak, brevmottager) + val overskrift = TekstformatererVarselbrev.lagVarselbrevsoverskrift(varselbrevsdokument.brevmetadata, false) + val brevtekst = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument, false) + val varsletFeilutbetaling = varselbrevsdokument.beløp + val fritekst = varselbrevsdokument.varseltekstFraSaksbehandler + val vedlegg = varselbrevUtil.lagVedlegg( + varselbrevsdokument, + behandling.aktivFagsystemsbehandling.eksternId, + varselbrevsdokument.beløp, + ) + + pdfBrevService.sendBrev( + behandling, + fagsak, + Brevtype.VARSEL, + Brevdata( + mottager = brevmottager, + metadata = varselbrevsdokument.brevmetadata, + overskrift = overskrift, + brevtekst = brevtekst, + vedleggHtml = vedlegg, + ), + varsletFeilutbetaling, + fritekst, + ) + } + } + + private fun lagVarselbrevForSending( + behandling: Behandling, + fagsak: Fagsak, + brevmottager: Brevmottager, + forhåndsgenerertMetadata: Brevmetadata? = null, + ): Varselbrevsdokument { + val metadata = forhåndsgenerertMetadata ?: run { + // Henter data fra pdl + val personinfo = eksterneDataForBrevService.hentPerson(fagsak.bruker.ident, fagsak.fagsystem) + val verge = behandling.aktivVerge + val adresseinfo = eksterneDataForBrevService.hentAdresse(personinfo, brevmottager, verge, fagsak.fagsystem) + + varselbrevUtil.sammenstillInfoForBrevmetadata( + behandling = behandling, + personinfo = personinfo, + adresseinfo = adresseinfo, + fagsak = fagsak, + vergenavn = BrevmottagerUtil.getVergenavn(verge, adresseinfo), + erKorrigert = false, + gjelderDødsfall = personinfo.dødsdato != null, + ) + } + + val varsel = behandling.aktivtVarsel + + return Varselbrevsdokument( + brevmetadata = metadata.copy(tittel = TITTEL_VARSEL_TILBAKEBETALING + fagsak.ytelsesnavn), + beløp = varsel?.varselbeløp ?: 0L, + revurderingsvedtaksdato = behandling.aktivFagsystemsbehandling.revurderingsvedtaksdato, + fristdatoForTilbakemelding = Constants.brukersSvarfrist(), + varseltekstFraSaksbehandler = varsel?.varseltekst, + feilutbetaltePerioder = mapFeilutbetaltePerioder(varsel), + ) + } + + fun hentForhåndsvisningVarselbrev(forhåndsvisVarselbrevRequest: ForhåndsvisVarselbrevRequest): ByteArray { + val varselbrevsdokument = lagVarselbrevForForhåndsvisning(forhåndsvisVarselbrevRequest) + val overskrift = TekstformatererVarselbrev.lagVarselbrevsoverskrift(varselbrevsdokument.brevmetadata, false) + val brevtekst = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument, false) + val data = Fritekstbrevsdata( + overskrift = overskrift, + brevtekst = brevtekst, + brevmetadata = varselbrevsdokument.brevmetadata, + ) + val brevmottager = utledBrevmottager(forhåndsvisVarselbrevRequest) + val vedlegg = varselbrevUtil.lagVedlegg( + varselbrevsdokument, + forhåndsvisVarselbrevRequest.fagsystemsbehandlingId, + varselbrevsdokument.beløp, + ) + return pdfBrevService.genererForhåndsvisning( + Brevdata( + mottager = brevmottager, + metadata = data.brevmetadata, + overskrift = data.overskrift, + brevtekst = data.brevtekst, + vedleggHtml = vedlegg, + ), + ) + } + + private fun lagVarselbrevForForhåndsvisning(request: ForhåndsvisVarselbrevRequest): Varselbrevsdokument { + val brevmottager = utledBrevmottager(request) + val personinfo = eksterneDataForBrevService.hentPerson(request.ident, request.fagsystem) + val adresseinfo: Adresseinfo = eksterneDataForBrevService.hentAdresse( + personinfo, + brevmottager, + request.verge, + request.fagsystem, + ) + + return varselbrevUtil.sammenstillInfoForForhåndvisningVarselbrev( + adresseinfo, + request, + personinfo, + ) + } + + private fun utledBrevmottager(request: ForhåndsvisVarselbrevRequest): Brevmottager { + return if (request.verge != null) Brevmottager.VERGE else if (request.institusjon != null) Brevmottager.INSTITUSJON else Brevmottager.BRUKER + } + + private fun mapFeilutbetaltePerioder(varsel: Varsel?): List { + return varsel?.perioder?.map { Datoperiode(it.fom, it.tom) } ?: emptyList() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/VarselbrevUtil.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/VarselbrevUtil.kt new file mode 100644 index 000000000..26b8418c3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/VarselbrevUtil.kt @@ -0,0 +1,248 @@ +package no.nav.familie.tilbake.dokumentbestilling.varsel + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.simulering.HentFeilutbetalingerFraSimuleringRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.FeilutbetaltePerioderDto +import no.nav.familie.kontrakter.felles.tilbakekreving.ForhåndsvisVarselbrevRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.api.dto.FaktaFeilutbetalingDto +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandling.domain.Varsel +import no.nav.familie.tilbake.beregning.KravgrunnlagsberegningService +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmottagerUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.EksterneDataForBrevService +import no.nav.familie.tilbake.dokumentbestilling.varsel.handlebars.dto.FeilutbetaltPeriode +import no.nav.familie.tilbake.dokumentbestilling.varsel.handlebars.dto.Varselbrevsdokument +import no.nav.familie.tilbake.dokumentbestilling.varsel.handlebars.dto.Vedleggsdata +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import no.nav.familie.tilbake.integration.økonomi.OppdragClient +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.organisasjon.OrganisasjonService +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.YearMonth +import java.util.UUID + +@Service +class VarselbrevUtil( + private val eksterneDataForBrevService: EksterneDataForBrevService, + private val oppdragClient: OppdragClient, + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val organisasjonService: OrganisasjonService, +) { + + companion object { + + const val TITTEL_KORRIGERT_VARSEL_TILBAKEBETALING = "Korrigert Varsel tilbakebetaling " + const val TITTEL_VARSEL_TILBAKEBETALING = "Varsel tilbakebetaling " + } + + fun sammenstillInfoForForhåndvisningVarselbrev( + adresseinfo: Adresseinfo, + request: ForhåndsvisVarselbrevRequest, + personinfo: Personinfo, + ): Varselbrevsdokument { + val tittel = getTittelForVarselbrev(request.ytelsestype.navn[request.språkkode]!!, false) + val vergenavn = BrevmottagerUtil.getVergenavn(request.verge, adresseinfo) + val ansvarligSaksbehandler = + eksterneDataForBrevService.hentPåloggetSaksbehandlernavnMedDefault(ContextService.hentSaksbehandler()) + + val metadata = Brevmetadata( + sakspartId = personinfo.ident, + sakspartsnavn = personinfo.navn, + finnesVerge = request.verge != null, + vergenavn = vergenavn, + mottageradresse = adresseinfo, + behandlendeEnhetId = request.behandlendeEnhetId, + behandlendeEnhetsNavn = request.behandlendeEnhetsNavn, + ansvarligSaksbehandler = ansvarligSaksbehandler, + saksnummer = request.eksternFagsakId, + språkkode = request.språkkode, + ytelsestype = request.ytelsestype, + tittel = tittel, + gjelderDødsfall = personinfo.dødsdato != null, + institusjon = request.institusjon?.let { + organisasjonService.mapTilInstitusjonForBrevgenerering(it.organisasjonsnummer) + }, + ) + + return Varselbrevsdokument( + brevmetadata = metadata, + beløp = request.feilutbetaltePerioderDto.sumFeilutbetaling, + revurderingsvedtaksdato = request.vedtaksdato ?: LocalDate.now(), + fristdatoForTilbakemelding = Constants.brukersSvarfrist(), + varseltekstFraSaksbehandler = request.varseltekst, + feilutbetaltePerioder = mapFeilutbetaltePerioder(request.feilutbetaltePerioderDto), + ) + } + + fun sammenstillInfoForBrevmetadata( + behandling: Behandling, + personinfo: Personinfo, + adresseinfo: Adresseinfo, + fagsak: Fagsak, + vergenavn: String?, + erKorrigert: Boolean, + gjelderDødsfall: Boolean, + ): Brevmetadata { + val ansvarligSaksbehandler = + eksterneDataForBrevService.hentPåloggetSaksbehandlernavnMedDefault(behandling.ansvarligSaksbehandler) + + return Brevmetadata( + sakspartId = personinfo.ident, + sakspartsnavn = personinfo.navn, + finnesVerge = behandling.harVerge, + vergenavn = vergenavn, + mottageradresse = adresseinfo, + behandlendeEnhetId = behandling.behandlendeEnhet, + behandlendeEnhetsNavn = behandling.behandlendeEnhetsNavn, + ansvarligSaksbehandler = ansvarligSaksbehandler, + saksnummer = fagsak.eksternFagsakId, + språkkode = fagsak.bruker.språkkode, + ytelsestype = fagsak.ytelsestype, + tittel = getTittelForVarselbrev(fagsak.ytelsesnavn, erKorrigert), + gjelderDødsfall = gjelderDødsfall, + institusjon = fagsak.institusjon?.let { + organisasjonService.mapTilInstitusjonForBrevgenerering(it.organisasjonsnummer) + }, + ) + } + + fun sammenstillInfoFraFagsystemerForSendingManueltVarselBrev( + metadata: Brevmetadata, + fritekst: String?, + feilutbetalingsfakta: FaktaFeilutbetalingDto, + varsel: Varsel?, + ): Varselbrevsdokument { + return Varselbrevsdokument( + brevmetadata = metadata, + beløp = feilutbetalingsfakta.totaltFeilutbetaltBeløp.toLong(), + revurderingsvedtaksdato = feilutbetalingsfakta.revurderingsvedtaksdato, + fristdatoForTilbakemelding = Constants.brukersSvarfrist(), + varseltekstFraSaksbehandler = fritekst, + feilutbetaltePerioder = mapFeilutbetaltePerioder(feilutbetalingsfakta), + erKorrigert = varsel != null, + varsletDato = varsel?.sporbar?.opprettetTid?.toLocalDate(), + varsletBeløp = varsel?.varselbeløp, + ) + } + + private fun sammenstillInfoFraSimuleringForVedlegg( + varselbrevsdokument: Varselbrevsdokument, + eksternBehandlingId: String, + varsletTotalbeløp: Long, + ): Vedleggsdata { + val request = HentFeilutbetalingerFraSimuleringRequest( + varselbrevsdokument.ytelsestype, + varselbrevsdokument.brevmetadata.saksnummer, + eksternBehandlingId, + ) + + val feilutbetalingerFraSimulering = oppdragClient.hentFeilutbetalingerFraSimulering(request) + + val perioder = feilutbetalingerFraSimulering.feilutbetaltePerioder.map { + FeilutbetaltPeriode( + YearMonth.from(it.fom), + it.nyttBeløp, + it.tidligereUtbetaltBeløp, + it.feilutbetaltBeløp, + ) + } + + validerKorrektTotalbeløp( + perioder, + varsletTotalbeløp, + varselbrevsdokument.ytelsestype, + varselbrevsdokument.brevmetadata.saksnummer, + eksternBehandlingId, + ) + return Vedleggsdata(varselbrevsdokument.språkkode, varselbrevsdokument.isYtelseMedSkatt, perioder) + } + + private fun sammenstillInfoFraKravgrunnlag( + varselbrevsdokument: Varselbrevsdokument, + behandlingId: UUID, + ): Vedleggsdata { + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + + val beregningsresultat = KravgrunnlagsberegningService.summerKravgrunnlagBeløpForPerioder(kravgrunnlag) + + val perioder = beregningsresultat.map { + FeilutbetaltPeriode( + YearMonth.from(it.key.fom), + it.value.riktigYtelsesbeløp, + it.value.utbetaltYtelsesbeløp, + it.value.feilutbetaltBeløp, + ) + } + + return Vedleggsdata(varselbrevsdokument.språkkode, varselbrevsdokument.isYtelseMedSkatt, perioder) + } + + fun lagVedlegg( + varselbrevsdokument: Varselbrevsdokument, + fagsystemsbehandlingId: String?, + varsletTotalbeløp: Long, + ): String { + return if (varselbrevsdokument.harVedlegg) { + if (fagsystemsbehandlingId == null) { + error( + "fagsystemsbehandlingId mangler for forhåndsvisning av varselbrev. " + + "Saksnummer ${varselbrevsdokument.brevmetadata.saksnummer}", + ) + } + + val vedleggsdata = + sammenstillInfoFraSimuleringForVedlegg(varselbrevsdokument, fagsystemsbehandlingId, varsletTotalbeløp) + TekstformatererVarselbrev.lagVarselbrevsvedleggHtml(vedleggsdata) + } else { + "" + } + } + + fun lagVedlegg(varselbrevsdokument: Varselbrevsdokument, behandlingId: UUID): String { + return if (varselbrevsdokument.harVedlegg) { + val vedleggsdata = sammenstillInfoFraKravgrunnlag(varselbrevsdokument, behandlingId) + TekstformatererVarselbrev.lagVarselbrevsvedleggHtml(vedleggsdata) + } else { + "" + } + } + + private fun validerKorrektTotalbeløp( + feilutbetaltePerioder: List, + varsletTotalFeilutbetaltBeløp: Long, + ytelsestype: Ytelsestype, + eksternFagsakId: String, + eksternId: String, + ) { + if (feilutbetaltePerioder.sumOf { it.feilutbetaltBeløp.toLong() } != varsletTotalFeilutbetaltBeløp) { + throw Feil( + "Varslet totalFeilutbetaltBeløp matcher ikke med hentet totalFeilutbetaltBeløp fra " + + "simulering for ytelsestype=$ytelsestype, eksternFagsakId=$eksternFagsakId og eksternId=$eksternId", + ) + } + } + + private fun getTittelForVarselbrev(ytelsesnavn: String, erKorrigert: Boolean): String { + return if (erKorrigert) { + TITTEL_KORRIGERT_VARSEL_TILBAKEBETALING + ytelsesnavn + } else { + TITTEL_VARSEL_TILBAKEBETALING + ytelsesnavn + } + } + + private fun mapFeilutbetaltePerioder(feilutbetaltePerioderDto: FeilutbetaltePerioderDto): List { + return feilutbetaltePerioderDto.perioder.map { Datoperiode(it.fom, it.tom) } + } + + private fun mapFeilutbetaltePerioder(feilutbetalingsfakta: FaktaFeilutbetalingDto): List { + return feilutbetalingsfakta.feilutbetaltePerioder.map { Datoperiode(it.periode.fom, it.periode.tom) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/handlebars/dto/Varselbrevsdokument.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/handlebars/dto/Varselbrevsdokument.kt new file mode 100644 index 000000000..44c438138 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/handlebars/dto/Varselbrevsdokument.kt @@ -0,0 +1,74 @@ +package no.nav.familie.tilbake.dokumentbestilling.varsel.handlebars.dto + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmottagerUtil +import no.nav.familie.tilbake.dokumentbestilling.handlebars.dto.BaseDokument +import java.time.LocalDate + +data class Varselbrevsdokument( + val brevmetadata: Brevmetadata, + val beløp: Long, + val revurderingsvedtaksdato: LocalDate, + val feilutbetaltePerioder: List, + val varseltekstFraSaksbehandler: String? = null, + val fristdatoForTilbakemelding: LocalDate, + val varsletDato: LocalDate? = null, + val varsletBeløp: Long? = null, + val erKorrigert: Boolean = false, +) : BaseDokument( + brevmetadata.ytelsestype, + brevmetadata.språkkode, + brevmetadata.behandlendeEnhetsNavn, + brevmetadata.ansvarligSaksbehandler, + brevmetadata.gjelderDødsfall, + brevmetadata.institusjon, +) { + + val finnesVerge: Boolean = brevmetadata.finnesVerge + + val harVedlegg: Boolean = brevmetadata.ytelsestype in setOf(Ytelsestype.BARNETILSYN, Ytelsestype.OVERGANGSSTØNAD) + + private val datoerHvisSammenhengendePeriode: Datoperiode? = if (feilutbetaltePerioder.size == 1) { + Datoperiode(feilutbetaltePerioder.first().fom, feilutbetaltePerioder.first().tom) + } else { + null + } + + val annenMottagersNavn: String? = BrevmottagerUtil.getAnnenMottagersNavn(brevmetadata) + + @Suppress("unused") // Handlebars + val isYtelseMedSkatt = ytelsestype == Ytelsestype.OVERGANGSSTØNAD + + @Suppress("unused") // Handlebars + val isRentepliktig = ytelsestype != Ytelsestype.BARNETRYGD && ytelsestype != Ytelsestype.KONTANTSTØTTE + + @Suppress("unused") // Handlebars + val isBarnetrygd = ytelsestype == Ytelsestype.BARNETRYGD + + @Suppress("unused") // Handlebars + val isKontantstøtte = ytelsestype == Ytelsestype.KONTANTSTØTTE + + @Suppress("unused") // Handlebars + val isFinnesVerge + get() = finnesVerge + + init { + if (feilutbetaltePerioder.size == 1) { + requireNotNull(datoerHvisSammenhengendePeriode) { "datoer for sammenhengende periode" } + } else if (feilutbetaltePerioder.size > 1) { + feilutbetaltePerioder.forEach { + requireNotNull(it.fom) { "fraogmed-dato for feilutbetalingsperiode" } + requireNotNull(it.tom) { "tilogmed-dato for feilutbetalingsperiode" } + } + } + if (erKorrigert) { + requireNotNull(varsletDato) { "varsletDato" } + requireNotNull(varsletBeløp) { "varsletBelop" } + } + if (finnesVerge) { + requireNotNull(annenMottagersNavn) { "annenMottagersNavn" } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/handlebars/dto/Vedleggsdata.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/handlebars/dto/Vedleggsdata.kt new file mode 100644 index 000000000..431043a88 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/handlebars/dto/Vedleggsdata.kt @@ -0,0 +1,20 @@ +package no.nav.familie.tilbake.dokumentbestilling.varsel.handlebars.dto + +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.tilbake.dokumentbestilling.handlebars.dto.Språkstøtte +import java.math.BigDecimal +import java.time.YearMonth + +class Vedleggsdata( + override val språkkode: Språkkode, + @Suppress("unused") // Handlebars + val ytelseMedSkatt: Boolean, + val feilutbetaltePerioder: List, +) : Språkstøtte + +data class FeilutbetaltPeriode( + val måned: YearMonth, + val nyttBeløp: BigDecimal, + val tidligereUtbetaltBeløp: BigDecimal, + val feilutbetaltBeløp: BigDecimal, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/manuelt/ManueltVarselbrevService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/manuelt/ManueltVarselbrevService.kt new file mode 100644 index 000000000..ac8cdea5c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/manuelt/ManueltVarselbrevService.kt @@ -0,0 +1,178 @@ +package no.nav.familie.tilbake.dokumentbestilling.varsel.manuelt + +import no.nav.familie.tilbake.api.dto.FaktaFeilutbetalingDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandling.domain.Varsel +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.dokumentbestilling.DistribusjonshåndteringService +import no.nav.familie.tilbake.dokumentbestilling.brevmaler.Dokumentmalstype +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmetadataUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmottagerUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.EksterneDataForBrevService +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.Brevdata +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.PdfBrevService +import no.nav.familie.tilbake.dokumentbestilling.varsel.TekstformatererVarselbrev.lagFritekst +import no.nav.familie.tilbake.dokumentbestilling.varsel.TekstformatererVarselbrev.lagVarselbrevsoverskrift +import no.nav.familie.tilbake.dokumentbestilling.varsel.VarselbrevUtil +import no.nav.familie.tilbake.dokumentbestilling.varsel.handlebars.dto.Varselbrevsdokument +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingService +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class ManueltVarselbrevService( + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, + private val eksterneDataForBrevService: EksterneDataForBrevService, + private val pdfBrevService: PdfBrevService, + private val faktaFeilutbetalingService: FaktaFeilutbetalingService, + private val varselbrevUtil: VarselbrevUtil, + private val distribusjonshåndteringService: DistribusjonshåndteringService, + private val brevmetadataUtil: BrevmetadataUtil, +) { + + fun sendManueltVarselBrev(behandling: Behandling, fritekst: String, brevmottager: Brevmottager) { + sendVarselBrev(behandling, fritekst, brevmottager, false) + } + + fun sendKorrigertVarselBrev(behandling: Behandling, fritekst: String, brevmottager: Brevmottager) { + sendVarselBrev(behandling, fritekst, brevmottager, true) + } + + fun sendVarselbrev(behandling: Behandling, fritekst: String, erKorrigert: Boolean) { + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val feilutbetalingsfakta = faktaFeilutbetalingService.hentFaktaomfeilutbetaling(behandling.id) + val varsletFeilutbetaling = feilutbetalingsfakta.totaltFeilutbetaltBeløp.toLong() + + distribusjonshåndteringService.sendBrev( + behandling = behandling, + brevtype = if (erKorrigert) Brevtype.KORRIGERT_VARSEL else Brevtype.VARSEL, + varsletBeløp = varsletFeilutbetaling, + fritekst = fritekst, + ) { brevmottager, brevmetadata -> + + val varselbrevsdokument = lagVarselbrev( + behandling = behandling, + fagsak = fagsak, + brevmottager = brevmottager, + fritekst = fritekst, + erKorrigert = erKorrigert, + feilutbetalingsfakta = feilutbetalingsfakta, + aktivtVarsel = behandling.aktivtVarsel, + forhåndsgenerertMetadata = brevmetadata, + ) + Brevdata( + mottager = brevmottager, + overskrift = lagVarselbrevsoverskrift(varselbrevsdokument.brevmetadata, erKorrigert), + brevtekst = lagFritekst(varselbrevsdokument, erKorrigert), + metadata = varselbrevsdokument.brevmetadata, + vedleggHtml = varselbrevUtil.lagVedlegg(varselbrevsdokument, behandling.id), + ) + } + } + + fun sendVarselBrev(behandling: Behandling, fritekst: String, brevmottager: Brevmottager, erKorrigert: Boolean) { + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val feilutbetalingsfakta = faktaFeilutbetalingService.hentFaktaomfeilutbetaling(behandling.id) + val varselbrevsdokument = + lagVarselbrev(fritekst, behandling, fagsak, brevmottager, erKorrigert, feilutbetalingsfakta, behandling.aktivtVarsel) + val overskrift = + lagVarselbrevsoverskrift(varselbrevsdokument.brevmetadata, erKorrigert) + val brevtekst = lagFritekst(varselbrevsdokument, erKorrigert) + val varsletFeilutbetaling = varselbrevsdokument.beløp + val vedlegg = varselbrevUtil.lagVedlegg(varselbrevsdokument, behandling.id) + pdfBrevService.sendBrev( + behandling, + fagsak, + if (erKorrigert) Brevtype.KORRIGERT_VARSEL else Brevtype.VARSEL, + Brevdata( + mottager = brevmottager, + overskrift = overskrift, + brevtekst = brevtekst, + metadata = varselbrevsdokument.brevmetadata, + vedleggHtml = vedlegg, + ), + varsletFeilutbetaling, + fritekst, + ) + } + + fun hentForhåndsvisningManueltVarselbrev( + behandlingId: UUID, + maltype: Dokumentmalstype, + fritekst: String, + ): ByteArray { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val (metadata, brevmottager) = + brevmetadataUtil.lagBrevmetadataForMottakerTilForhåndsvisning(behandlingId) + val erKorrigert = maltype == Dokumentmalstype.KORRIGERT_VARSEL + val feilutbetalingsfakta = faktaFeilutbetalingService.hentFaktaomfeilutbetaling(behandling.id) + val aktivtVarsel = behandling.aktivtVarsel + + val varselbrevsdokument = + lagVarselbrev(fritekst, behandling, fagsak, brevmottager, erKorrigert, feilutbetalingsfakta, aktivtVarsel, metadata) + val overskrift = + lagVarselbrevsoverskrift(varselbrevsdokument.brevmetadata, erKorrigert) + val brevtekst = lagFritekst(varselbrevsdokument, erKorrigert) + val vedlegg = varselbrevUtil.lagVedlegg(varselbrevsdokument, behandlingId) + return pdfBrevService.genererForhåndsvisning( + Brevdata( + mottager = brevmottager, + overskrift = overskrift, + brevtekst = brevtekst, + metadata = varselbrevsdokument.brevmetadata, + vedleggHtml = vedlegg, + ), + ) + } + + private fun lagVarselbrev( + fritekst: String, + behandling: Behandling, + fagsak: Fagsak, + brevmottager: Brevmottager, + erKorrigert: Boolean, + feilutbetalingsfakta: FaktaFeilutbetalingDto, + aktivtVarsel: Varsel? = null, + forhåndsgenerertMetadata: Brevmetadata? = null, + ): Varselbrevsdokument { + val metadata = forhåndsgenerertMetadata ?: run { + // Henter data fra pdl + val personinfo = eksterneDataForBrevService.hentPerson(fagsak.bruker.ident, fagsak.fagsystem) + val adresseinfo: Adresseinfo = + eksterneDataForBrevService.hentAdresse(personinfo, brevmottager, behandling.aktivVerge, fagsak.fagsystem) + val vergenavn: String = BrevmottagerUtil.getVergenavn(behandling.aktivVerge, adresseinfo) + varselbrevUtil.sammenstillInfoForBrevmetadata( + behandling, + personinfo, + adresseinfo, + fagsak, + vergenavn, + erKorrigert, + personinfo.dødsdato != null, + ) + } + + return varselbrevUtil.sammenstillInfoFraFagsystemerForSendingManueltVarselBrev( + metadata.copy(tittel = getTittelForVarselbrev(fagsak.ytelsesnavn, erKorrigert)), + fritekst, + feilutbetalingsfakta, + aktivtVarsel, + ) + } + private fun getTittelForVarselbrev(ytelsesnavn: String, erKorrigert: Boolean): String { + return if (erKorrigert) { + VarselbrevUtil.TITTEL_KORRIGERT_VARSEL_TILBAKEBETALING + ytelsesnavn + } else { + VarselbrevUtil.TITTEL_VARSEL_TILBAKEBETALING + ytelsesnavn + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/manuelt/SendManueltVarselbrevTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/manuelt/SendManueltVarselbrevTask.kt new file mode 100644 index 000000000..1b58139af --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/manuelt/SendManueltVarselbrevTask.kt @@ -0,0 +1,102 @@ +package no.nav.familie.tilbake.dokumentbestilling.varsel.manuelt + +import com.fasterxml.jackson.module.kotlin.readValue +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.dokumentbestilling.brevmaler.Dokumentmalstype +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.util.Properties +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = SendManueltVarselbrevTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Sender manuelt varselbrev", + triggerTidVedFeilISekunder = 60 * 5L, +) +class SendManueltVarselbrevTask( + private val behandlingRepository: BehandlingRepository, + private val manueltVarselBrevService: ManueltVarselbrevService, + private val behandlingskontrollService: BehandlingskontrollService, + private val oppgaveTaskService: OppgaveTaskService, + private val fagsakRepository: FagsakRepository, + private val featureToggleService: FeatureToggleService, +) : AsyncTaskStep { + + override fun doTask(task: Task) { + val taskdata: SendManueltVarselbrevTaskdata = objectMapper.readValue(task.payload) + val behandling = behandlingRepository.findByIdOrThrow(taskdata.behandlingId) + val maltype = taskdata.maltype + val fritekst = taskdata.fritekst + + manueltVarselBrevService.sendVarselbrev( + behandling = behandling, + fritekst = fritekst, + erKorrigert = maltype.erKorrigert, + ) + + val fristTid = Constants.saksbehandlersTidsfrist() + oppgaveTaskService.oppdaterOppgaveTask( + behandlingId = behandling.id, + beskrivelse = "Frist er oppdatert. Saksbehandler ${ + behandling + .ansvarligSaksbehandler + } har sendt varselbrev til bruker", + frist = fristTid, + saksbehandler = behandling.ansvarligSaksbehandler, + ) + // Oppdaterer fristen dersom tasken har tidligere feilet. Behandling ble satt på vent i DokumentBehandlingService. + if (task.opprettetTid.toLocalDate() < LocalDate.now()) { + behandlingskontrollService.settBehandlingPåVent( + behandling.id, + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + fristTid, + ) + } + } + + companion object { + + fun opprettTask(behandlingId: UUID, fagsystem: Fagsystem, maltype: Dokumentmalstype, fritekst: String): Task = + Task( + type = TYPE, + payload = objectMapper.writeValueAsString( + SendManueltVarselbrevTaskdata( + behandlingId = behandlingId, + maltype = maltype, + fritekst = fritekst, + ), + ), + properties = Properties().apply { setProperty(PropertyName.FAGSYSTEM, fagsystem.name) }, + ) + + const val TYPE = "brev.sendManueltVarsel" + } +} + +data class SendManueltVarselbrevTaskdata( + val behandlingId: UUID, + val maltype: Dokumentmalstype, + val fritekst: String, +) + +private val Dokumentmalstype.erKorrigert: Boolean + get() = when (this) { + Dokumentmalstype.KORRIGERT_VARSEL -> true + Dokumentmalstype.VARSEL -> false + else -> throw IllegalArgumentException("SendManueltVarselbrevTask kan ikke sende Dokumentmalstype.$this") + } diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/Avsnitt.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/Avsnitt.kt new file mode 100644 index 000000000..94ff61766 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/Avsnitt.kt @@ -0,0 +1,39 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import java.time.LocalDate + +enum class Avsnittstype { + OPPSUMMERING, + PERIODE, + TILLEGGSINFORMASJON, +} + +data class Avsnitt( + val overskrift: String? = null, + val underavsnittsliste: List = listOf(), + val avsnittstype: Avsnittstype? = null, + val fom: LocalDate? = null, + val tom: LocalDate? = null, +) + +class Underavsnitt( + val overskrift: String? = null, + val brødtekst: String? = null, + val fritekst: String? = null, + val fritekstTillatt: Boolean = false, + val fritekstPåkrevet: Boolean = false, + val underavsnittstype: Underavsnittstype? = null, +) { + + init { + require(!(!fritekstTillatt && fritekstPåkrevet)) { "Det gir ikke mening at fritekst er påkrevet når fritekst ikke er tillatt" } + } +} + +enum class Underavsnittstype { + FAKTA, + FORELDELSE, + VILKÅR, + SÆRLIGEGRUNNER, + SÆRLIGEGRUNNER_ANNET, +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/AvsnittUtil.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/AvsnittUtil.kt new file mode 100644 index 000000000..cb8890f67 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/AvsnittUtil.kt @@ -0,0 +1,181 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.tilbake.dokumentbestilling.handlebars.FellesTekstformaterer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevPeriodeOgFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevsdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype + +internal object AvsnittUtil { + + const val PARTIAL_PERIODE_FAKTA = "vedtak/periode_fakta" + const val PARTIAL_PERIODE_FORELDELSE = "vedtak/periode_foreldelse" + const val PARTIAL_PERIODE_VILKÅR = "vedtak/periode_vilkår" + const val PARTIAL_PERIODE_SÆRLIGE_GRUNNER = "vedtak/periode_særlige_grunner" + + fun lagVedtaksbrevDeltIAvsnitt(vedtaksbrevsdata: HbVedtaksbrevsdata, hovedoverskrift: String): List { + val resultat: MutableList = ArrayList() + val vedtaksbrevsdataMedFriteksmarkeringer = Vedtaksbrevsfritekst.settInnMarkeringForFritekst(vedtaksbrevsdata) + resultat.add(lagOppsummeringsavsnitt(vedtaksbrevsdataMedFriteksmarkeringer, hovedoverskrift)) + if (vedtaksbrevsdata.felles.vedtaksbrevstype == Vedtaksbrevstype.ORDINÆR) { + resultat.addAll(lagPerioderavsnitt(vedtaksbrevsdataMedFriteksmarkeringer)) + } + resultat.add(lagAvsluttendeAvsnitt(vedtaksbrevsdataMedFriteksmarkeringer)) + return resultat + } + + private fun lagOppsummeringsavsnitt(vedtaksbrevsdata: HbVedtaksbrevsdata, hovedoverskrift: String): Avsnitt { + val tekst = lagVedtaksstart(vedtaksbrevsdata.felles) + val avsnitt = Avsnitt(avsnittstype = Avsnittstype.OPPSUMMERING, overskrift = hovedoverskrift) + return parseTekst(tekst, avsnitt, null) + } + + private fun lagVedtaksstart(vedtaksbrevFelles: HbVedtaksbrevFelles): String { + val filsti = when (vedtaksbrevFelles.vedtaksbrevstype) { + Vedtaksbrevstype.FRITEKST_FEILUTBETALING_BORTFALT -> + "vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt_start" + Vedtaksbrevstype.ORDINÆR -> "vedtak/vedtak_start" + } + return FellesTekstformaterer.lagDeltekst(vedtaksbrevFelles, filsti) + } + + private fun lagPerioderavsnitt(vedtaksbrevsdata: HbVedtaksbrevsdata): List { + return vedtaksbrevsdata.perioder.map { + lagPeriodeAvsnitt(HbVedtaksbrevPeriodeOgFelles(vedtaksbrevsdata.felles, it)) + } + } + + private fun lagAvsluttendeAvsnitt(vedtaksbrevsdata: HbVedtaksbrevsdata): Avsnitt { + val tekst = FellesTekstformaterer.lagDeltekst(vedtaksbrevsdata, "vedtak/vedtak_slutt") + val avsnitt = Avsnitt(avsnittstype = Avsnittstype.TILLEGGSINFORMASJON) + return parseTekst(tekst, avsnitt, null) + } + + private fun lagPeriodeAvsnitt(data: HbVedtaksbrevPeriodeOgFelles): Avsnitt { + val overskrift = FellesTekstformaterer.lagDeltekst(data, "vedtak/periode_overskrift") + val faktatekst = FellesTekstformaterer.lagDeltekst(data, PARTIAL_PERIODE_FAKTA) + val foreldelsestekst = FellesTekstformaterer.lagDeltekst(data, PARTIAL_PERIODE_FORELDELSE) + val vilkårstekst = FellesTekstformaterer.lagDeltekst(data, PARTIAL_PERIODE_VILKÅR) + val særligeGrunnerstekst = FellesTekstformaterer.lagDeltekst(data, PARTIAL_PERIODE_SÆRLIGE_GRUNNER) + val avsluttendeTekst = FellesTekstformaterer.lagDeltekst(data, "vedtak/periode_slutt") + var avsnitt = Avsnitt( + avsnittstype = Avsnittstype.PERIODE, + fom = data.periode.periode.fom, + tom = data.periode.periode.tom, + overskrift = fjernOverskriftFormattering(overskrift), + ) + + avsnitt = parseTekst(faktatekst, avsnitt, Underavsnittstype.FAKTA) + avsnitt = parseTekst(foreldelsestekst, avsnitt, Underavsnittstype.FORELDELSE) + avsnitt = parseTekst(vilkårstekst, avsnitt, Underavsnittstype.VILKÅR) + avsnitt = parseTekst(særligeGrunnerstekst, avsnitt, Underavsnittstype.SÆRLIGEGRUNNER) + avsnitt = parseTekst(avsluttendeTekst, avsnitt, null) + return avsnitt + } + + fun parseTekst( + generertTekst: String, + avsnitt: Avsnitt, + underavsnittstype: Underavsnittstype?, + ): Avsnitt { + var lokaltAvsnitt = avsnitt + var lokalUnderavsnittstype = underavsnittstype + val splittet = generertTekst.split("\r?\n".toRegex()).toMutableList() + if (avsnitt.overskrift.isNullOrBlank() && erOverskrift(splittet.first())) { + val linje: String = splittet.removeAt(0) + lokaltAvsnitt = avsnitt.copy(overskrift = fjernOverskriftFormattering(linje)) + } + var leserFritekst = false + var fritekstPåkrevet = false + var fritekstTillatt = false + var overskrift: String? = null + var fritekst: MutableList = ArrayList() + var brødtekst: String? = null + val underavsnitt = mutableListOf() + + for (linje in splittet) { + fun nyOverskriftOgAvsnittetHarAlleredeOverskrift(linje: String) = erOverskrift(linje) && overskrift != null + fun avsnittHarBrødtekstSomIkkeEtterfølgesAvFritekst(linje: String) = + brødtekst != null && !Vedtaksbrevsfritekst.erFritekstStart(linje) + + if (!leserFritekst && + ( + fritekstTillatt || + nyOverskriftOgAvsnittetHarAlleredeOverskrift(linje) || + avsnittHarBrødtekstSomIkkeEtterfølgesAvFritekst(linje) + ) + ) { + underavsnitt.add( + Underavsnitt( + overskrift, + brødtekst, + fritekst.joinToString("\n"), + fritekstTillatt, + fritekstPåkrevet, + lokalUnderavsnittstype, + ), + ) + overskrift = null + brødtekst = null + fritekstTillatt = false + fritekstPåkrevet = false + fritekst = ArrayList() + } + + when { + Vedtaksbrevsfritekst.erFritekstStart(linje) -> { + check(!leserFritekst) { "Feil med vedtaksbrev, har markering for 2 fritekst-start etter hverandre" } + fritekstPåkrevet = Vedtaksbrevsfritekst.erFritekstPåkrevetStart(linje) + lokalUnderavsnittstype = parseUnderavsnittstype(linje) + fritekstTillatt = true + leserFritekst = true + } + Vedtaksbrevsfritekst.erFritekstSlutt(linje) -> { + check(leserFritekst) { "Feil med vedtaksbrev, fikk markering for fritekst-slutt før fritekst-start" } + leserFritekst = false + } + leserFritekst -> { + fritekst.add(linje) + } + erOverskrift(linje) -> { + overskrift = fjernOverskriftFormattering(linje) + } + linje.isNotBlank() -> { + brødtekst = fjernFormattering(linje) + } + } + } + + if (overskrift != null || brødtekst != null || fritekstTillatt) { + underavsnitt.add( + Underavsnitt( + overskrift, + brødtekst, + fritekst.joinToString("\n"), + fritekstTillatt, + fritekstPåkrevet, + lokalUnderavsnittstype, + ), + ) + } + + return lokaltAvsnitt.copy(underavsnittsliste = lokaltAvsnitt.underavsnittsliste + underavsnitt) + } + + private fun fjernFormattering(linje: String): String { + return linje.removePrefix("{venstrejustert}").replace("{høyrejustert}", "\t\t\t") + } + + private fun parseUnderavsnittstype(tekst: String): Underavsnittstype? { + val rest = Vedtaksbrevsfritekst.fjernFritekstmarkering(tekst) + return Underavsnittstype.values().firstOrNull { it.name == rest } + } + + private fun erOverskrift(tekst: String): Boolean { + return tekst.startsWith("_") + } + + private fun fjernOverskriftFormattering(tekst: String): String { + return tekst.removePrefix("__").removePrefix("_") + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/HbGrunnbel\303\270pUtil.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/HbGrunnbel\303\270pUtil.kt" new file mode 100644 index 000000000..9666b13b1 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/HbGrunnbel\303\270pUtil.kt" @@ -0,0 +1,47 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.common.DatoUtil +import no.nav.familie.tilbake.common.Grunnbeløp +import no.nav.familie.tilbake.common.Grunnbeløpsperioder +import no.nav.familie.tilbake.dokumentbestilling.handlebars.KroneFormattererMedTusenskille +import no.nav.familie.tilbake.dokumentbestilling.handlebars.KroneFormattererMedTusenskille.Companion.utf8nonBreakingSpace +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbGrunnbeløp +import java.math.BigDecimal + +object HbGrunnbeløpUtil { + + fun lagHbGrunnbeløp(periode: Månedsperiode): HbGrunnbeløp { + val grunnbeløpsperioder = Grunnbeløpsperioder.finnGrunnbeløpsperioder(periode) + + val formattertPerioder = grunnbeløpsperioder.map { + formatterGrunnbeløp(it, periode) + } + + return if (grunnbeløpsperioder.size > 1) { + val kommaSeparertePerioder = formattertPerioder.dropLast(1).joinToString(", ") + HbGrunnbeløp( + null, + "$kommaSeparertePerioder og ${formattertPerioder.last()}", + ) + } else { + HbGrunnbeløp(grunnbeløpX6(grunnbeløpsperioder.single()), null) + } + } + + private fun formatterGrunnbeløp(grunnbeløp: Grunnbeløp, periode: Månedsperiode): String { + val format = DatoUtil.DATO_FORMAT_DATO_MÅNEDSNAVN_ÅR + val snitt = grunnbeløp.periode.snitt(periode) ?: error("Finner ikke snitt for ${grunnbeløp.periode} og $periode") + + return "${formatterBeløpX6(grunnbeløp)} for perioden ${format.format(snitt.fomDato)} " + + "til ${format.format(snitt.tomDato)}" + } + + private fun formatterBeløpX6(grunnbeløp: Grunnbeløp): String { + val grunnbeløpX6 = grunnbeløpX6(grunnbeløp) + return KroneFormattererMedTusenskille.formatterKronerMedTusenskille(grunnbeløpX6, utf8nonBreakingSpace) + } + + private fun grunnbeløpX6(grunnbeløp: Grunnbeløp) = + grunnbeløp.grunnbeløp.multiply(BigDecimal.valueOf(6L)) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/SendVedtaksbrevTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/SendVedtaksbrevTask.kt new file mode 100644 index 000000000..8813b21bd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/SendVedtaksbrevTask.kt @@ -0,0 +1,87 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsakstype +import no.nav.familie.tilbake.behandling.domain.Saksbehandlingstype +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.datavarehus.saksstatistikk.SendVedtaksoppsummeringTilDvhTask +import no.nav.familie.tilbake.iverksettvedtak.task.AvsluttBehandlingTask +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.Properties +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = SendVedtaksbrevTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Sender vedtaksbrev", + triggerTidVedFeilISekunder = 60 * 5L, +) +class SendVedtaksbrevTask( + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, + private val vedtaksbrevService: VedtaksbrevService, + private val taskService: TaskService, + private val featureToggleService: FeatureToggleService, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + val behandlingId = UUID.fromString(task.payload) + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsystem = fagsakRepository.findByIdOrThrow(behandling.fagsakId).fagsystem + if (behandling.saksbehandlingstype == Saksbehandlingstype.AUTOMATISK_IKKE_INNKREVING_LAVT_BELØP) { + log.info("Behandlingen $behandlingId ble saksbehandlet automatisk, sender ikke vedtaksbrev") + taskService.save( + Task( + type = AvsluttBehandlingTask.TYPE, + payload = task.payload, + properties = Properties().apply { setProperty(PropertyName.FAGSYSTEM, fagsystem.name) }, + ), + ) + return + } + + if (behandling.type == Behandlingstype.REVURDERING_TILBAKEKREVING && + behandling.sisteÅrsak?.type in setOf(Behandlingsårsakstype.REVURDERING_KLAGE_KA) + ) { + log.info("Sender ikke vedtaksbrev etter revurdering som følge av klage for behandling: {}", behandlingId) + taskService.save( + Task( + type = AvsluttBehandlingTask.TYPE, + payload = task.payload, + properties = Properties().apply { setProperty(PropertyName.FAGSYSTEM, fagsystem.name) }, + ), + ) + return + } + + vedtaksbrevService.sendVedtaksbrev(behandling) + log.info("Utført for behandling: {}", behandlingId) + } + + override fun onCompletion(task: Task) { + taskService.save( + Task( + type = SendVedtaksoppsummeringTilDvhTask.TYPE, + payload = task.payload, + properties = task.metadata, + ), + ) + } + + companion object { + + const val TYPE = "iverksetteVedtak.sendVedtaksbrev" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrev.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrev.kt new file mode 100644 index 000000000..51d29ad94 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrev.kt @@ -0,0 +1,28 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.tilbake.dokumentbestilling.handlebars.FellesTekstformaterer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevsdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype + +internal object TekstformatererVedtaksbrev { + + fun lagVedtaksbrevsfritekst(vedtaksbrevsdata: HbVedtaksbrevsdata): String { + return when (vedtaksbrevsdata.felles.vedtaksbrevstype) { + Vedtaksbrevstype.FRITEKST_FEILUTBETALING_BORTFALT -> + lagVedtaksbrev("vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt", vedtaksbrevsdata) + Vedtaksbrevstype.ORDINÆR -> lagVedtaksbrev("vedtak/vedtak", vedtaksbrevsdata) + } + } + + private fun lagVedtaksbrev(mal: String, vedtaksbrevsdata: HbVedtaksbrevsdata): String { + return FellesTekstformaterer.lagBrevtekst(vedtaksbrevsdata, mal) + } + + fun lagVedtaksbrevsvedleggHtml(vedtaksbrevsdata: HbVedtaksbrevsdata): String { + return FellesTekstformaterer.lagBrevtekst(vedtaksbrevsdata, "vedtak/vedlegg") + } + + fun lagVedtaksbrevsoverskrift(vedtaksbrevsdata: HbVedtaksbrevsdata): String { + return FellesTekstformaterer.lagBrevtekst(vedtaksbrevsdata, "vedtak/vedtak_overskrift") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtakHjemmel.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtakHjemmel.kt new file mode 100644 index 000000000..7be2c9680 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtakHjemmel.kt @@ -0,0 +1,135 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbHjemmel +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsperiode +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat + +object VedtakHjemmel { + + private val Vilkårsvurderingsresultat_MED_FORSETT_ALLTID_RENTER: List = + listOf( + Vilkårsvurderingsresultat.MANGELFULLE_OPPLYSNINGER_FRA_BRUKER, + Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + ) + + fun lagHjemmel( + vedtaksresultatstype: Vedtaksresultat, + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag, + effektForBruker: EffektForBruker, + språkkode: Språkkode, + visHjemmelForRenter: Boolean, + klagebehandling: Boolean, + ): HbHjemmel { + val foreldetVanlig = erNoeSattTilVanligForeldet(vedtaksbrevgrunnlag.vurdertForeldelse) + val foreldetMedTilleggsfrist = erTilleggsfristBenyttet(vedtaksbrevgrunnlag.vurdertForeldelse) + val ignorerteSmåbeløp = heleVurderingPgaSmåbeløp( + vedtaksresultatstype, + vedtaksbrevgrunnlag.vilkårsvurderingsperioder, + ) + val renter = visHjemmelForRenter && erRenterBenyttet(vedtaksbrevgrunnlag.vilkårsvurderingsperioder) + val barnetrygd = Ytelsestype.BARNETRYGD == vedtaksbrevgrunnlag.ytelsestype + val kontantstøtte = Ytelsestype.KONTANTSTØTTE == vedtaksbrevgrunnlag.ytelsestype + val hjemler: MutableList = ArrayList() + if (vedtaksbrevgrunnlag.vilkårsvurderingsperioder.isNotEmpty()) { + when { + barnetrygd && ignorerteSmåbeløp -> hjemler.addAll(setOf(Hjemler.BARNETRYGD_13, Hjemler.FOLKETRYGD_22_15_SJETTE)) + ignorerteSmåbeløp -> hjemler.add(Hjemler.FOLKETRYGD_22_15_SJETTE) + barnetrygd -> hjemler.addAll(setOf(Hjemler.BARNETRYGD_13, Hjemler.FOLKETRYGD_22_15)) + kontantstøtte -> hjemler.addAll(setOf(Hjemler.KONTANTSTØTTE_11, Hjemler.FOLKETRYGD_22_15)) + renter -> hjemler.add(Hjemler.FOLKETRYGD_22_15_OG_22_17_A) + else -> hjemler.add(Hjemler.FOLKETRYGD_22_15) + } + } + if (foreldetMedTilleggsfrist) { + hjemler.add(Hjemler.FORELDELSE_2_3_OG_10) + } else if (foreldetVanlig) { + hjemler.add(Hjemler.FORELDELSE_2_3) + } + + if (!klagebehandling) { + if (EffektForBruker.ENDRET_TIL_GUNST_FOR_BRUKER == effektForBruker) { + hjemler.add(Hjemler.FORVALTNING_35_A) + } + if (EffektForBruker.ENDRET_TIL_UGUNST_FOR_BRUKER == effektForBruker) { + hjemler.add(Hjemler.FORVALTNING_35_C) + } + } + val hjemmelstekst = join(hjemler, " og ", språkkode) + return HbHjemmel(hjemmelstekst, hjemmelstekst.contains("og")) + } + + private fun erRenterBenyttet(vilkårPerioder: Set): Boolean { + return vilkårPerioder.any { + it.aktsomhet?.ileggRenter == true || erForsettOgAlltidRenter(it) + } + } + + private fun erForsettOgAlltidRenter(v: Vilkårsvurderingsperiode): Boolean { + return Vilkårsvurderingsresultat_MED_FORSETT_ALLTID_RENTER.contains(v.vilkårsvurderingsresultat) && + Aktsomhet.FORSETT == v.aktsomhet?.aktsomhet + } + + private fun heleVurderingPgaSmåbeløp( + vedtakResultatType: Vedtaksresultat, + vilkårPerioder: Set, + ): Boolean { + return Vedtaksresultat.INGEN_TILBAKEBETALING == vedtakResultatType && + vilkårPerioder.any { false == it.aktsomhet?.tilbakekrevSmåbeløp } + } + + private fun erTilleggsfristBenyttet(foreldelse: VurdertForeldelse?): Boolean { + return foreldelse?.foreldelsesperioder?.any { it.foreldelsesvurderingstype == Foreldelsesvurderingstype.TILLEGGSFRIST } + ?: false + } + + private fun erNoeSattTilVanligForeldet(foreldelse: VurdertForeldelse?): Boolean { + return foreldelse?.foreldelsesperioder?.any { it.foreldelsesvurderingstype == Foreldelsesvurderingstype.FORELDET } + ?: false + } + + private fun join( + elementer: List, + sisteSkille: String, + lokale: Språkkode, + ): String { + val lokalListe = elementer.map { it.hjemmelTekst(lokale) } + if (lokalListe.size == 1) { + return lokalListe.first()!! + } + return lokalListe.subList(0, elementer.size - 1).joinToString(", ") + sisteSkille + lokalListe.last() + } + + enum class EffektForBruker { + FØRSTEGANGSVEDTAK, + ENDRET_TIL_GUNST_FOR_BRUKER, + ENDRET_TIL_UGUNST_FOR_BRUKER, + } + + private enum class Hjemler(bokmål: String, nynorsk: String) { + FOLKETRYGD_22_15("folketrygdloven § 22-15", "folketrygdlova § 22-15"), + FOLKETRYGD_22_15_SJETTE("folketrygdloven § 22-15 sjette ledd", "folketrygdlova § 22-15 sjette ledd"), + FOLKETRYGD_22_15_OG_22_17_A("folketrygdloven §§ 22-15 og 22-17 a", "folketrygdlova §§ 22-15 og 22-17 a"), + FORELDELSE_2_3_OG_10("foreldelsesloven §§ 2, 3 og 10", "foreldingslova §§ 2, 3 og 10"), + FORELDELSE_2_3("foreldelsesloven §§ 2 og 3", "foreldingslova §§ 2 og 3"), + FORVALTNING_35_A("forvaltningsloven § 35 a)", "forvaltningslova § 35 a)"), + FORVALTNING_35_C("forvaltningsloven § 35 c)", "forvaltningslova § 35 c)"), + KONTANTSTØTTE_11("kontantstøtteloven § 11", "kontantstøttelova § 11"), + BARNETRYGD_13("barnetrygdloven § 13", "barnetrygdlova § 13"), + ; + + private val hjemmelTekster = mapOf( + Språkkode.NB to bokmål, + Språkkode.NN to nynorsk, + ) + + fun hjemmelTekst(språkkode: Språkkode): String? { + return hjemmelTekster.getOrDefault(språkkode, hjemmelTekster[Språkkode.NB]) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevFritekstKonfigurasjon.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevFritekstKonfigurasjon.kt new file mode 100644 index 000000000..9c5417386 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevFritekstKonfigurasjon.kt @@ -0,0 +1,8 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype + +object VedtaksbrevFritekstKonfigurasjon { + + val UNDERTYPER_MED_PÅKREVD_FRITEKST: Set = setOf(Hendelsesundertype.ANNET_FRITEKST) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevFritekstMapper.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevFritekstMapper.kt new file mode 100644 index 000000000..2e1f7fa78 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevFritekstMapper.kt @@ -0,0 +1,121 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.api.dto.PeriodeMedTekstDto +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Friteksttype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsoppsummering +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsperiode +import java.util.UUID + +object VedtaksbrevFritekstMapper { + + fun tilDomene(behandlingId: UUID, oppsummeringstekst: String?): Vedtaksbrevsoppsummering { + return Vedtaksbrevsoppsummering( + behandlingId = behandlingId, + oppsummeringFritekst = oppsummeringstekst, + ) + } + + fun tilDomeneVedtaksbrevsperiode( + behandlingId: UUID, + perioderMedFritekst: List, + ): List { + val vedtaksbrevsperioder = mutableListOf() + perioderMedFritekst.forEach { + lagFaktaAvsnitt(behandlingId, it)?.let { faktaAvsnitt -> vedtaksbrevsperioder.add(faktaAvsnitt) } + lagForeldelseAvsnitt(behandlingId, it)?.let { foreldelseAvsnitt -> vedtaksbrevsperioder.add(foreldelseAvsnitt) } + lagVilkårsvurderingAvsnitt(behandlingId, it)?.let { vilkårAvsnitt -> vedtaksbrevsperioder.add(vilkårAvsnitt) } + lagSærligGrunnerAvsnitt( + behandlingId, + it, + )?.let { særligGrunnerAvsnitt -> vedtaksbrevsperioder.add(særligGrunnerAvsnitt) } + lagSærligGrunnerAnnetAvsnitt(behandlingId, it)?.let { annetAvsnitt -> vedtaksbrevsperioder.add(annetAvsnitt) } + } + return vedtaksbrevsperioder + } + + fun mapFritekstFraDb(fritekstPerioder: Set): List { + val perioderTilMap = HashMap>() + + fritekstPerioder.forEach { + val avsnittTilTekst = perioderTilMap.getOrDefault(it.periode, mutableMapOf()) + avsnittTilTekst[it.fritekststype] = it.fritekst + perioderTilMap[it.periode] = avsnittTilTekst + } + + return perioderTilMap.entries.map { (periode, avsnittTilTekst) -> + PeriodeMedTekstDto( + periode = periode.toDatoperiode(), + faktaAvsnitt = avsnittTilTekst[Friteksttype.FAKTA], + foreldelseAvsnitt = avsnittTilTekst[Friteksttype.FORELDELSE], + vilkårAvsnitt = avsnittTilTekst[Friteksttype.VILKÅR], + særligeGrunnerAvsnitt = avsnittTilTekst[Friteksttype.SÆRLIGE_GRUNNER], + særligeGrunnerAnnetAvsnitt = avsnittTilTekst[Friteksttype.SÆRLIGE_GRUNNER_ANNET], + ) + } + } + + private fun lagFaktaAvsnitt(behandlingId: UUID, faktaAvsnittMedPeriode: PeriodeMedTekstDto): Vedtaksbrevsperiode? { + return faktaAvsnittMedPeriode.faktaAvsnitt?.let { + Vedtaksbrevsperiode( + behandlingId = behandlingId, + periode = faktaAvsnittMedPeriode.periode.toMånedsperiode(), + fritekst = faktaAvsnittMedPeriode.faktaAvsnitt, + fritekststype = Friteksttype.FAKTA, + ) + } + } + + private fun lagForeldelseAvsnitt(behandlingId: UUID, foreldelsesAvsnittMedPeriode: PeriodeMedTekstDto): Vedtaksbrevsperiode? { + return foreldelsesAvsnittMedPeriode.foreldelseAvsnitt?.let { + Vedtaksbrevsperiode( + behandlingId = behandlingId, + periode = foreldelsesAvsnittMedPeriode.periode.toMånedsperiode(), + fritekst = foreldelsesAvsnittMedPeriode.foreldelseAvsnitt, + fritekststype = Friteksttype.FORELDELSE, + ) + } + } + + private fun lagVilkårsvurderingAvsnitt( + behandlingId: UUID, + vilkårAvsnittMedPeriode: PeriodeMedTekstDto, + ): Vedtaksbrevsperiode? { + return vilkårAvsnittMedPeriode.vilkårAvsnitt?.let { + Vedtaksbrevsperiode( + behandlingId = behandlingId, + periode = vilkårAvsnittMedPeriode.periode.toMånedsperiode(), + fritekst = vilkårAvsnittMedPeriode.vilkårAvsnitt, + fritekststype = Friteksttype.VILKÅR, + ) + } + } + + private fun lagSærligGrunnerAvsnitt( + behandlingId: UUID, + særligGrunnerAvsnittMedPeriode: PeriodeMedTekstDto, + ): Vedtaksbrevsperiode? { + return særligGrunnerAvsnittMedPeriode.særligeGrunnerAvsnitt?.let { + Vedtaksbrevsperiode( + behandlingId = behandlingId, + periode = særligGrunnerAvsnittMedPeriode.periode.toMånedsperiode(), + fritekst = særligGrunnerAvsnittMedPeriode.særligeGrunnerAvsnitt, + fritekststype = Friteksttype.SÆRLIGE_GRUNNER, + ) + } + } + + private fun lagSærligGrunnerAnnetAvsnitt( + behandlingId: UUID, + særligGrunnerAnnetAvsnittMedPeriode: PeriodeMedTekstDto, + ): Vedtaksbrevsperiode? { + return særligGrunnerAnnetAvsnittMedPeriode.særligeGrunnerAnnetAvsnitt?.let { + Vedtaksbrevsperiode( + behandlingId = behandlingId, + periode = særligGrunnerAnnetAvsnittMedPeriode.periode.toMånedsperiode(), + fritekst = særligGrunnerAnnetAvsnittMedPeriode.særligeGrunnerAnnetAvsnitt, + fritekststype = Friteksttype.SÆRLIGE_GRUNNER_ANNET, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevFritekstValidator.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevFritekstValidator.kt new file mode 100644 index 000000000..7028d087a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevFritekstValidator.kt @@ -0,0 +1,194 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.api.dto.PeriodeMedTekstDto +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsakstype +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Friteksttype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsoppsummering +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsperiode +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype.ORDINÆR +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurdering +import org.springframework.http.HttpStatus + +object VedtaksbrevFritekstValidator { + + @Throws(Feil::class) + fun validerObligatoriskeFritekster( + behandling: Behandling, + faktaFeilutbetaling: FaktaFeilutbetaling, + vilkårsvurdering: Vilkårsvurdering?, + vedtaksbrevFritekstPerioder: List, + avsnittMedPerioder: List, + vedtaksbrevsoppsummering: Vedtaksbrevsoppsummering, + vedtaksbrevstype: Vedtaksbrevstype, + validerPåkrevetFritekster: Boolean, + skalIkkeValidereAnnetFritekst: Boolean, + ) { + validerPerioder(behandling, avsnittMedPerioder, faktaFeilutbetaling) + + if (!skalIkkeValidereAnnetFritekst) { + vilkårsvurdering?.let { + validerFritekstISærligGrunnerAnnetAvsnitt( + it, + vedtaksbrevFritekstPerioder, + validerPåkrevetFritekster, + ) + } + } + + if (ORDINÆR == vedtaksbrevstype) { + validerFritekstIFaktaAvsnitt( + faktaFeilutbetaling, + vedtaksbrevFritekstPerioder, + avsnittMedPerioder, + validerPåkrevetFritekster, + ) + } + validerOppsummeringsfritekstLengde(behandling, vedtaksbrevsoppsummering, vedtaksbrevstype) + if (validerPåkrevetFritekster) { + validerNårOppsummeringsfritekstErPåkrevd(behandling, vedtaksbrevsoppsummering) + } + } + + private fun validerPerioder( + behandling: Behandling, + avsnittMedPerioder: List, + faktaFeilutbetaling: FaktaFeilutbetaling, + ) { + avsnittMedPerioder.forEach { + if (!faktaFeilutbetaling.perioder.any { faktaPeriode -> + faktaPeriode.periode.inneholder(it.periode.toMånedsperiode()) + } + ) { + throw Feil( + message = "Periode ${it.periode.fom}-${it.periode.tom} er ugyldig for behandling ${behandling.id}", + frontendFeilmelding = "Periode ${it.periode.fom}-${it.periode.tom} er ugyldig " + + "for behandling ${behandling.id}", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + } + + private fun validerNårOppsummeringsfritekstErPåkrevd( + behandling: Behandling, + vedtaksbrevsoppsummering: Vedtaksbrevsoppsummering, + ) { + val revurderingIkkeOpprettetEtterKlage = behandling.årsaker.none { + it.type in setOf( + Behandlingsårsakstype.REVURDERING_KLAGE_KA, + Behandlingsårsakstype.REVURDERING_KLAGE_NFP, + ) + } + if (Behandlingstype.REVURDERING_TILBAKEKREVING == behandling.type && + revurderingIkkeOpprettetEtterKlage && + vedtaksbrevsoppsummering.oppsummeringFritekst.isNullOrEmpty() + ) { + throw Feil( + message = "oppsummering fritekst påkrevet for revurdering ${behandling.id}", + frontendFeilmelding = "oppsummering fritekst påkrevet for revurdering ${behandling.id}", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + + private fun validerOppsummeringsfritekstLengde( + behandling: Behandling, + vedtaksbrevsoppsummering: Vedtaksbrevsoppsummering, + vedtaksbrevstype: Vedtaksbrevstype, + ) { + val maksTekstLengde = when (vedtaksbrevstype) { + ORDINÆR -> 4000 + else -> 10000 + } + if (vedtaksbrevsoppsummering.oppsummeringFritekst != null && + vedtaksbrevsoppsummering.oppsummeringFritekst.length > maksTekstLengde + ) { + throw Feil( + message = "Oppsummeringstekst er for lang for behandling ${behandling.id}", + frontendFeilmelding = "Oppsummeringstekst er for lang for behandling ${behandling.id}", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + + private fun validerFritekstIFaktaAvsnitt( + faktaFeilutbetaling: FaktaFeilutbetaling, + vedtaksbrevFritekstPerioder: List, + avsnittMedPerioder: List, + validerPåkrevetFritekster: Boolean, + ) { + faktaFeilutbetaling.perioder.filter { Hendelsesundertype.ANNET_FRITEKST == it.hendelsesundertype } + .forEach { faktaFeilutbetalingsperiode -> + val perioder = finnFritekstPerioder( + vedtaksbrevFritekstPerioder, + faktaFeilutbetalingsperiode.periode, + Friteksttype.FAKTA, + ) + if (perioder.isEmpty() && validerPåkrevetFritekster) { + throw Feil( + message = "Mangler fakta fritekst for alle fakta perioder", + frontendFeilmelding = "Mangler Fakta fritekst for alle fakta perioder", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + // Hvis en av de periodene mangler fritekst + val omsluttetPerioder = avsnittMedPerioder.filter { + faktaFeilutbetalingsperiode.periode.inneholder(it.periode.toMånedsperiode()) + } + omsluttetPerioder.forEach { + if (it.faktaAvsnitt.isNullOrBlank() && validerPåkrevetFritekster) { + throw Feil( + message = "Mangler fakta fritekst for ${it.periode.fom}-${it.periode.tom}", + frontendFeilmelding = "Mangler Fakta fritekst for ${it.periode.fom}-${it.periode.tom}", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + } + } + + private fun validerFritekstISærligGrunnerAnnetAvsnitt( + vilkårsvurdering: Vilkårsvurdering, + vedtaksbrevFritekstPerioder: List, + validerPåkrevetFritekster: Boolean, + ) { + vilkårsvurdering.perioder.filter { + it.aktsomhet?.vilkårsvurderingSærligeGrunner != null && + it.aktsomhet.vilkårsvurderingSærligeGrunner + .any { særligGrunn -> SærligGrunn.ANNET == særligGrunn.særligGrunn } + }.forEach { + val perioder = finnFritekstPerioder( + vedtaksbrevFritekstPerioder, + it.periode, + Friteksttype.SÆRLIGE_GRUNNER_ANNET, + ) + + if (perioder.isEmpty() && validerPåkrevetFritekster) { + throw Feil( + message = "Mangler ANNET Særliggrunner fritekst for ${it.periode}", + frontendFeilmelding = "Mangler ANNET Særliggrunner fritekst for ${it.periode} ", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + } + + private fun finnFritekstPerioder( + vedtaksbrevFritekstPerioder: List, + vurdertPeriode: Månedsperiode, + friteksttype: Friteksttype, + ): List { + return vedtaksbrevFritekstPerioder.filter { + friteksttype == it.fritekststype && + vurdertPeriode.inneholder(it.periode) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevService.kt new file mode 100644 index 000000000..eba986baf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevService.kt @@ -0,0 +1,125 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.tilbake.api.dto.FritekstavsnittDto +import no.nav.familie.tilbake.api.dto.HentForhåndvisningVedtaksbrevPdfDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.FeatureToggleConfig +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.dokumentbestilling.DistribusjonshåndteringService +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.PdfBrevService +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingRepository +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class VedtaksbrevService( + private val behandlingRepository: BehandlingRepository, + private val vedtaksbrevgeneratorService: VedtaksbrevgeneratorService, + private val vedtaksbrevgrunnlagService: VedtaksbrevgunnlagService, + private val faktaRepository: FaktaFeilutbetalingRepository, + private val vilkårsvurderingRepository: VilkårsvurderingRepository, + private val fagsakRepository: FagsakRepository, + private val vedtaksbrevsoppsummeringRepository: VedtaksbrevsoppsummeringRepository, + private val vedtaksbrevsperiodeRepository: VedtaksbrevsperiodeRepository, + private val pdfBrevService: PdfBrevService, + private val distribusjonshåndteringService: DistribusjonshåndteringService, + private val featureToggleService: FeatureToggleService, +) { + + fun sendVedtaksbrev(behandling: Behandling, brevmottager: Brevmottager? = null) { + val vedtaksbrevgrunnlag = vedtaksbrevgrunnlagService.hentVedtaksbrevgrunnlag(behandling.id) + if (brevmottager == null) { + distribusjonshåndteringService.sendBrev(behandling, Brevtype.VEDTAK) { brevmottaker, brevmetadata -> + vedtaksbrevgeneratorService.genererVedtaksbrevForSending(vedtaksbrevgrunnlag, brevmottaker, brevmetadata) + } + } else { + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val brevdata = vedtaksbrevgeneratorService.genererVedtaksbrevForSending(vedtaksbrevgrunnlag, brevmottager) + pdfBrevService.sendBrev( + behandling, + fagsak, + Brevtype.VEDTAK, + brevdata, + ) + } + } + + fun hentForhåndsvisningVedtaksbrevMedVedleggSomPdf(dto: HentForhåndvisningVedtaksbrevPdfDto): ByteArray { + val vedtaksbrevgrunnlag = vedtaksbrevgrunnlagService.hentVedtaksbrevgrunnlag(dto.behandlingId) + val brevdata = vedtaksbrevgeneratorService.genererVedtaksbrevForForhåndsvisning(vedtaksbrevgrunnlag, dto) + + return pdfBrevService.genererForhåndsvisning(brevdata) + } + + fun hentVedtaksbrevSomTekst(behandlingId: UUID): List { + val vedtaksbrevgrunnlag = vedtaksbrevgrunnlagService.hentVedtaksbrevgrunnlag(behandlingId) + + val hbVedtaksbrevsdata = vedtaksbrevgeneratorService.genererVedtaksbrevsdataTilVisningIFrontendSkjema(vedtaksbrevgrunnlag) + val hovedoverskrift = TekstformatererVedtaksbrev.lagVedtaksbrevsoverskrift(hbVedtaksbrevsdata) + return AvsnittUtil.lagVedtaksbrevDeltIAvsnitt(hbVedtaksbrevsdata, hovedoverskrift) + } + + @Transactional + fun lagreUtkastAvFritekster(behandlingId: UUID, fritekstavsnittDto: FritekstavsnittDto) { + lagreFriteksterFraSaksbehandler(behandlingId, fritekstavsnittDto, false) + } + + @Transactional + fun lagreFriteksterFraSaksbehandler(behandlingId: UUID, fritekstavsnittDto: FritekstavsnittDto) { + lagreFriteksterFraSaksbehandler(behandlingId, fritekstavsnittDto, true) + } + + private fun lagreFriteksterFraSaksbehandler( + behandlingId: UUID, + fritekstavsnittDto: FritekstavsnittDto, + validerPåkrevetFritekster: Boolean = false, + ) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val vedtaksbrevstype = behandling.utledVedtaksbrevstype() + val vedtaksbrevsoppsummering = VedtaksbrevFritekstMapper.tilDomene(behandlingId, fritekstavsnittDto.oppsummeringstekst) + val vedtaksbrevsperioder = VedtaksbrevFritekstMapper + .tilDomeneVedtaksbrevsperiode(behandlingId, fritekstavsnittDto.perioderMedTekst) + + // Valider om obligatoriske fritekster er satt + val faktaFeilutbetaling = faktaRepository.findFaktaFeilutbetalingByBehandlingIdAndAktivIsTrue(behandlingId) + val vilkårsvurdering = vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + val skalIkkeValidereAnnetFritekst = featureToggleService.isEnabled(FeatureToggleConfig.IKKE_VALIDER_SÆRLIG_GRUNNET_ANNET_FRITEKST) + + VedtaksbrevFritekstValidator.validerObligatoriskeFritekster( + behandling = behandling, + faktaFeilutbetaling = faktaFeilutbetaling, + vilkårsvurdering = vilkårsvurdering, + vedtaksbrevFritekstPerioder = vedtaksbrevsperioder, + avsnittMedPerioder = fritekstavsnittDto.perioderMedTekst, + vedtaksbrevsoppsummering = vedtaksbrevsoppsummering, + vedtaksbrevstype = vedtaksbrevstype, + validerPåkrevetFritekster = validerPåkrevetFritekster, + skalIkkeValidereAnnetFritekst = skalIkkeValidereAnnetFritekst, + ) + // slett og legge til Vedtaksbrevsoppsummering + val eksisterendeVedtaksbrevsoppsummering = vedtaksbrevsoppsummeringRepository.findByBehandlingId(behandlingId) + if (eksisterendeVedtaksbrevsoppsummering != null) { + vedtaksbrevsoppsummeringRepository.delete(eksisterendeVedtaksbrevsoppsummering) + } + vedtaksbrevsoppsummeringRepository.insert(vedtaksbrevsoppsummering) + + // slett og legge til Vedtaksbrevsperiode + val eksisterendeVedtaksbrevperioder = vedtaksbrevsperiodeRepository.findByBehandlingId(behandlingId) + eksisterendeVedtaksbrevperioder.forEach { vedtaksbrevsperiodeRepository.deleteById(it.id) } + vedtaksbrevsperioder.forEach { vedtaksbrevsperiodeRepository.insert(it) } + } + + @Transactional + fun deaktiverEksisterendeVedtaksbrevdata(behandlingId: UUID) { + vedtaksbrevsoppsummeringRepository.findByBehandlingId(behandlingId) + ?.let { vedtaksbrevsoppsummeringRepository.deleteById(it.id) } + vedtaksbrevsperiodeRepository.findByBehandlingId(behandlingId).forEach { vedtaksbrevsperiodeRepository.deleteById(it.id) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevgeneratorService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevgeneratorService.kt new file mode 100644 index 000000000..41e2f1bb9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevgeneratorService.kt @@ -0,0 +1,476 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import com.github.jknack.handlebars.internal.text.WordUtils +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.tilbake.api.dto.HentForhåndvisningVedtaksbrevPdfDto +import no.nav.familie.tilbake.api.dto.PeriodeMedTekstDto +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsak +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.beregning.TilbakekrevingsberegningService +import no.nav.familie.tilbake.beregning.modell.Beregningsresultat +import no.nav.familie.tilbake.beregning.modell.Beregningsresultatsperiode +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat.DELVIS_TILBAKEBETALING +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat.FULL_TILBAKEBETALING +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmetadataUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmottagerUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.EksterneDataForBrevService +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.Brevdata +import no.nav.familie.tilbake.dokumentbestilling.fritekstbrev.Fritekstbrevsdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.HbGrunnbeløpUtil.lagHbGrunnbeløp +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbBehandling +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbKonfigurasjon +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbPerson +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbTotalresultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVarsel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevDatoer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevsdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbFakta +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbKravgrunnlag +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbResultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbSærligeGrunner +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVedtaksbrevsperiode +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVurderinger +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesperiode +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import no.nav.familie.tilbake.organisasjon.OrganisasjonService +import no.nav.familie.tilbake.vilkårsvurdering.domain.AnnenVurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsperiode +import org.springframework.stereotype.Service +import java.math.BigDecimal + +@Service +class VedtaksbrevgeneratorService( + private val tilbakekrevingBeregningService: TilbakekrevingsberegningService, + private val eksterneDataForBrevService: EksterneDataForBrevService, + private val organisasjonService: OrganisasjonService, + private val brevmetadataUtil: BrevmetadataUtil, +) { + + fun genererVedtaksbrevForSending( + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag, + brevmottager: Brevmottager, + forhåndsgenerertMetadata: Brevmetadata? = null, + ): Brevdata { + val vedtaksbrevsdata = hentDataForVedtaksbrev(vedtaksbrevgrunnlag, brevmottager, forhåndsgenerertMetadata) + val hbVedtaksbrevsdata: HbVedtaksbrevsdata = vedtaksbrevsdata.vedtaksbrevsdata + val data = Fritekstbrevsdata( + TekstformatererVedtaksbrev.lagVedtaksbrevsoverskrift(hbVedtaksbrevsdata), + TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(hbVedtaksbrevsdata), + vedtaksbrevsdata.metadata, + ) + val vedleggHtml = if (vedtaksbrevsdata.vedtaksbrevsdata.felles.harVedlegg) { + TekstformatererVedtaksbrev.lagVedtaksbrevsvedleggHtml(vedtaksbrevsdata.vedtaksbrevsdata) + } else { + "" + } + return Brevdata( + mottager = brevmottager, + metadata = data.brevmetadata, + overskrift = data.overskrift, + brevtekst = data.brevtekst, + vedleggHtml = vedleggHtml, + ) + } + + fun genererVedtaksbrevForForhåndsvisning( + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag, + dto: HentForhåndvisningVedtaksbrevPdfDto, + ): Brevdata { + val (brevmetadata, brevmottager) = + brevmetadataUtil.lagBrevmetadataForMottakerTilForhåndsvisning(vedtaksbrevgrunnlag) + val vedtaksbrevsdata = hentDataForVedtaksbrev( + vedtaksbrevgrunnlag, + dto.oppsummeringstekst, + dto.perioderMedTekst, + brevmottager, + brevmetadata, + ) + val hbVedtaksbrevsdata: HbVedtaksbrevsdata = vedtaksbrevsdata.vedtaksbrevsdata + + val vedleggHtml = if (hbVedtaksbrevsdata.felles.harVedlegg) { + TekstformatererVedtaksbrev.lagVedtaksbrevsvedleggHtml(vedtaksbrevsdata.vedtaksbrevsdata) + } else { + "" + } + + return Brevdata( + mottager = brevmottager, + metadata = vedtaksbrevsdata.metadata, + overskrift = TekstformatererVedtaksbrev.lagVedtaksbrevsoverskrift(hbVedtaksbrevsdata), + brevtekst = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(hbVedtaksbrevsdata), + vedleggHtml = vedleggHtml, + ) + } + + fun genererVedtaksbrevsdataTilVisningIFrontendSkjema( + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag, + ): HbVedtaksbrevsdata { + val (brevmetadata, brevmottager) = + brevmetadataUtil.lagBrevmetadataForMottakerTilForhåndsvisning(vedtaksbrevgrunnlag) + val vedtaksbrevsdata = hentDataForVedtaksbrev(vedtaksbrevgrunnlag, brevmottager, brevmetadata) + return vedtaksbrevsdata.vedtaksbrevsdata + } + + private fun hentDataForVedtaksbrev( + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag, + brevmottager: Brevmottager, + brevmetadata: Brevmetadata? = null, + ): Vedtaksbrevsdata { + val fritekstoppsummering = vedtaksbrevgrunnlag.behandling.vedtaksbrevOppsummering?.oppsummeringFritekst + val fritekstPerioder: List = + VedtaksbrevFritekstMapper.mapFritekstFraDb(vedtaksbrevgrunnlag.behandling.eksisterendePerioderForBrev) + return hentDataForVedtaksbrev(vedtaksbrevgrunnlag, fritekstoppsummering, fritekstPerioder, brevmottager, brevmetadata) + } + + private fun hentDataForVedtaksbrev( + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag, + oppsummeringFritekst: String?, + perioderFritekst: List, + brevmottager: Brevmottager, + forhåndsgenerertMetadata: Brevmetadata? = null, + ): Vedtaksbrevsdata { + val språkkode: Språkkode = vedtaksbrevgrunnlag.bruker.språkkode + val personinfo: Personinfo = eksterneDataForBrevService.hentPerson( + vedtaksbrevgrunnlag.bruker.ident, + vedtaksbrevgrunnlag.fagsystem, + ) + val beregnetResultat = tilbakekrevingBeregningService.beregn(vedtaksbrevgrunnlag.behandling.id) + val brevMetadata: Brevmetadata = ( + forhåndsgenerertMetadata ?: lagMetadataForVedtaksbrev( + vedtaksbrevgrunnlag, + personinfo, + brevmottager, + språkkode, + ) + ).copy( + tittel = finnTittelVedtaksbrev( + ytelsesnavn = vedtaksbrevgrunnlag.ytelsestype.navn[språkkode]!!, + tilbakekreves = beregnetResultat.vedtaksresultat == FULL_TILBAKEBETALING || + beregnetResultat.vedtaksresultat == DELVIS_TILBAKEBETALING, + ), + ) + val data: HbVedtaksbrevsdata = lagHbVedtaksbrevsdata( + vedtaksbrevgrunnlag, + personinfo, + beregnetResultat, + oppsummeringFritekst, + perioderFritekst, + brevMetadata, + ) + return Vedtaksbrevsdata(data, brevMetadata) + } + + private fun lagHbVedtaksbrevsdata( + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag, + personinfo: Personinfo, + beregningsresultat: Beregningsresultat, + oppsummeringFritekst: String?, + perioderFritekst: List, + brevmetadata: Brevmetadata, + ): HbVedtaksbrevsdata { + val vedtaksbrevtype = vedtaksbrevgrunnlag.utledVedtaksbrevstype() + val effektForBruker: VedtakHjemmel.EffektForBruker = + utledEffektForBruker(vedtaksbrevgrunnlag, beregningsresultat) + val klagebehandling = vedtaksbrevgrunnlag.klagebehandling + val hbHjemmel = VedtakHjemmel.lagHjemmel( + beregningsresultat.vedtaksresultat, + vedtaksbrevgrunnlag, + effektForBruker, + brevmetadata.språkkode, + visHjemmelForRenter = true, + klagebehandling, + ) // sannsynligvis hjemmel + val perioder: List = lagHbVedtaksbrevPerioder( + vedtaksbrevgrunnlag, + beregningsresultat, + perioderFritekst, + ) + val hbTotalresultat: HbTotalresultat = + lagHbTotalresultat(beregningsresultat.vedtaksresultat, beregningsresultat) + val hbBehandling: HbBehandling = lagHbBehandling(vedtaksbrevgrunnlag) + val varsletBeløp = vedtaksbrevgrunnlag.varsletBeløp + val varsletDato = vedtaksbrevgrunnlag.sisteVarsel?.sporbar?.opprettetTid?.toLocalDate() + val ansvarligBeslutter = if (vedtaksbrevgrunnlag.aktivtSteg in setOf( + Behandlingssteg.FATTE_VEDTAK, + Behandlingssteg.IVERKSETT_VEDTAK, + Behandlingssteg.AVSLUTTET, + ) + ) { + eksterneDataForBrevService + .hentPåloggetSaksbehandlernavnMedDefault(vedtaksbrevgrunnlag.behandling.ansvarligBeslutter) + } else { + null + } + val erFeilutbetaltBeløpKorrigertNed = + varsletBeløp != null && beregningsresultat.totaltFeilutbetaltBeløp < varsletBeløp + val vedtaksbrevFelles = + HbVedtaksbrevFelles( + brevmetadata = brevmetadata, + fagsaksvedtaksdato = vedtaksbrevgrunnlag.aktivFagsystemsbehandling.revurderingsvedtaksdato, + behandling = hbBehandling, + varsel = HbVarsel(varsletDato, varsletBeløp), + erFeilutbetaltBeløpKorrigertNed = erFeilutbetaltBeløpKorrigertNed, + totaltFeilutbetaltBeløp = beregningsresultat.totaltFeilutbetaltBeløp, + fritekstoppsummering = oppsummeringFritekst, + vedtaksbrevstype = vedtaksbrevtype, + ansvarligBeslutter = ansvarligBeslutter, + hjemmel = hbHjemmel, + totalresultat = hbTotalresultat, + konfigurasjon = HbKonfigurasjon(klagefristIUker = KLAGEFRIST_UKER), + datoer = HbVedtaksbrevDatoer(perioder), + søker = utledSøker(personinfo), + ) + return HbVedtaksbrevsdata(vedtaksbrevFelles, perioder) + } + + private fun utledEffektForBruker( + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag, + beregningsresultat: Beregningsresultat, + ): VedtakHjemmel.EffektForBruker { + return if (vedtaksbrevgrunnlag.erRevurdering) { + hentEffektForBruker(vedtaksbrevgrunnlag, beregningsresultat.totaltTilbakekrevesMedRenter) + } else { + VedtakHjemmel.EffektForBruker.FØRSTEGANGSVEDTAK + } + } + + private fun lagHbTotalresultat( + vedtakResultatType: Vedtaksresultat, + beregningsresultat: Beregningsresultat, + ): HbTotalresultat { + return HbTotalresultat( + vedtakResultatType, + beregningsresultat.totaltTilbakekrevesUtenRenter, + beregningsresultat.totaltTilbakekrevesMedRenter, + beregningsresultat.totaltTilbakekrevesBeløpMedRenterUtenSkatt, + beregningsresultat.totaltRentebeløp, + ) + } + + private fun lagHbBehandling(vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag): HbBehandling { + return HbBehandling( + erRevurdering = vedtaksbrevgrunnlag.erRevurdering, + erRevurderingEtterKlage = vedtaksbrevgrunnlag.erRevurderingEtterKlage, + erRevurderingEtterKlageNfp = vedtaksbrevgrunnlag.erRevurderingEtterKlageNfp, + originalBehandlingsdatoFagsakvedtak = vedtaksbrevgrunnlag.finnOriginalBehandlingVedtaksdato(), + ) + } + + private fun lagHbVedtaksbrevPerioder( + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag, + beregningsresultat: Beregningsresultat, + perioderFritekst: List, + ): List { + val fakta = vedtaksbrevgrunnlag.faktaFeilutbetaling + ?: error("Vedtaksbrev mangler fakta for behandling: ${vedtaksbrevgrunnlag.behandling.id}") + return if (vedtaksbrevgrunnlag.utledVedtaksbrevstype() == Vedtaksbrevstype.FRITEKST_FEILUTBETALING_BORTFALT) { + emptyList() + } else { + beregningsresultat.beregningsresultatsperioder.mapIndexed { index, it -> + lagBrevdataPeriode( + resultatPeriode = it, + fakta = fakta, + vilkårPerioder = vedtaksbrevgrunnlag.vilkårsvurderingsperioder, + foreldelse = vedtaksbrevgrunnlag.vurdertForeldelse, + perioderFritekst = perioderFritekst, + førstePeriode = index == 0, + ) + } + } + } + + private fun hentEffektForBruker( + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag, + totaltTilbakekrevesMedRenter: BigDecimal, + ): VedtakHjemmel.EffektForBruker { + val behandlingÅrsak: Behandlingsårsak = vedtaksbrevgrunnlag.behandling.årsaker.first() + val originaltBeregnetResultat = tilbakekrevingBeregningService.beregn(behandlingÅrsak.originalBehandlingId!!) + val originalBeregningsresultatsperioder = originaltBeregnetResultat.beregningsresultatsperioder + + val originalBehandlingTotaltMedRenter: BigDecimal = + originalBeregningsresultatsperioder.sumOf { it.tilbakekrevingsbeløp } + val positivtForBruker: Boolean = totaltTilbakekrevesMedRenter < originalBehandlingTotaltMedRenter + return if (positivtForBruker) { + VedtakHjemmel.EffektForBruker.ENDRET_TIL_GUNST_FOR_BRUKER + } else { + VedtakHjemmel.EffektForBruker.ENDRET_TIL_UGUNST_FOR_BRUKER + } + } + + private fun utledSøker(personinfo: Personinfo): HbPerson { + return HbPerson( + navn = WordUtils.capitalizeFully(personinfo.navn, ' ', '-'), + dødsdato = null, + ) + } + + private fun lagMetadataForVedtaksbrev( + vedtaksbrevgrunnlag: Vedtaksbrevgrunnlag, + personinfo: Personinfo, + brevmottager: Brevmottager, + språkkode: Språkkode, + ): Brevmetadata { + val adresseinfo: Adresseinfo = eksterneDataForBrevService.hentAdresse( + personinfo, + brevmottager, + vedtaksbrevgrunnlag.aktivVerge, + vedtaksbrevgrunnlag.fagsystem, + ) + val vergeNavn: String = BrevmottagerUtil.getVergenavn(vedtaksbrevgrunnlag.aktivVerge, adresseinfo) + val ansvarligSaksbehandler = if (vedtaksbrevgrunnlag.aktivtSteg == Behandlingssteg.FORESLÅ_VEDTAK) { + eksterneDataForBrevService + .hentPåloggetSaksbehandlernavnMedDefault(vedtaksbrevgrunnlag.behandling.ansvarligSaksbehandler) + } else { + eksterneDataForBrevService.hentSaksbehandlernavn(vedtaksbrevgrunnlag.behandling.ansvarligSaksbehandler) + } + return Brevmetadata( + sakspartId = personinfo.ident, + sakspartsnavn = personinfo.navn, + finnesVerge = vedtaksbrevgrunnlag.harVerge, + vergenavn = vergeNavn, + mottageradresse = adresseinfo, + behandlendeEnhetId = vedtaksbrevgrunnlag.behandling.behandlendeEnhet, + behandlendeEnhetsNavn = vedtaksbrevgrunnlag.behandling.behandlendeEnhetsNavn, + ansvarligSaksbehandler = ansvarligSaksbehandler, + saksnummer = vedtaksbrevgrunnlag.eksternFagsakId, + språkkode = språkkode, + ytelsestype = vedtaksbrevgrunnlag.ytelsestype, + gjelderDødsfall = personinfo.dødsdato != null, + institusjon = vedtaksbrevgrunnlag.institusjon?.let { organisasjonService.mapTilInstitusjonForBrevgenerering(it.organisasjonsnummer) }, + ) + } + + private fun lagBrevdataPeriode( + resultatPeriode: Beregningsresultatsperiode, + fakta: FaktaFeilutbetaling, + vilkårPerioder: Set, + foreldelse: VurdertForeldelse?, + perioderFritekst: List, + førstePeriode: Boolean, + ): HbVedtaksbrevsperiode { + val periode = resultatPeriode.periode + val fritekster: PeriodeMedTekstDto? = + perioderFritekst.firstOrNull { Månedsperiode(it.periode.fom, it.periode.tom) == periode } + return HbVedtaksbrevsperiode( + periode = periode.toDatoperiode(), + kravgrunnlag = utledKravgrunnlag(resultatPeriode), + fakta = utledFakta(periode, fakta, fritekster), + vurderinger = utledVurderinger(periode, vilkårPerioder, foreldelse, fritekster), + resultat = utledResultat(resultatPeriode, foreldelse), + førstePeriode = førstePeriode, + grunnbeløp = lagHbGrunnbeløp(periode), + ) + } + + private fun utledKravgrunnlag(resultatPeriode: Beregningsresultatsperiode): HbKravgrunnlag { + return HbKravgrunnlag( + resultatPeriode.riktigYtelsesbeløp, + resultatPeriode.utbetaltYtelsesbeløp, + resultatPeriode.feilutbetaltBeløp, + ) + } + + private fun utledFakta(periode: Månedsperiode, fakta: FaktaFeilutbetaling, fritekst: PeriodeMedTekstDto?): HbFakta { + return fakta.perioder.first { it.periode.inneholder(periode) } + .let { + HbFakta(it.hendelsestype, it.hendelsesundertype, fritekst?.faktaAvsnitt) + } + } + + private fun utledVurderinger( + periode: Månedsperiode, + vilkårPerioder: Set, + foreldelse: VurdertForeldelse?, + fritekst: PeriodeMedTekstDto?, + ): HbVurderinger { + val foreldelsePeriode = finnForeldelsePeriode(foreldelse, periode) + val vilkårsvurdering = vilkårPerioder.firstOrNull { it.periode.inneholder(periode) } + val vilkårsvurderingAktsomhet = vilkårsvurdering?.aktsomhet + val godTro = vilkårsvurdering?.godTro + val beløpSomErIBehold = godTro?.beløpSomErIBehold + val aktsomhetsresultat = when { + foreldelsePeriode?.erForeldet() == true -> AnnenVurdering.FORELDET + godTro != null -> AnnenVurdering.GOD_TRO + else -> vilkårsvurderingAktsomhet?.aktsomhet + } + + val hbSærligeGrunner = + if (vilkårsvurderingAktsomhet?.skalHaSærligeGrunner == true) { + val fritekstSærligeGrunner = fritekst?.særligeGrunnerAvsnitt + val fritekstSærligGrunnAnnet = fritekst?.særligeGrunnerAnnetAvsnitt + HbSærligeGrunner( + vilkårsvurderingAktsomhet.særligeGrunner, + fritekstSærligeGrunner, + fritekstSærligGrunnAnnet, + ) + } else { + null + } + + return HbVurderinger( + fritekst = fritekst?.vilkårAvsnitt, + vilkårsvurderingsresultat = vilkårsvurdering?.vilkårsvurderingsresultat, + unntasInnkrevingPgaLavtBeløp = vilkårsvurderingAktsomhet?.tilbakekrevSmåbeløp == false, + særligeGrunner = hbSærligeGrunner, + aktsomhetsresultat = aktsomhetsresultat, + beløpIBehold = beløpSomErIBehold, + foreldelsevurdering = foreldelsePeriode?.foreldelsesvurderingstype + ?: Foreldelsesvurderingstype.IKKE_VURDERT, + foreldelsesfrist = foreldelsePeriode?.foreldelsesfrist, + oppdagelsesdato = foreldelsePeriode?.oppdagelsesdato, + fritekstForeldelse = fritekst?.foreldelseAvsnitt, + ) + } + + private fun utledResultat(resultatPeriode: Beregningsresultatsperiode, foreldelse: VurdertForeldelse?): HbResultat { + val foreldelsePeriode = finnForeldelsePeriode(foreldelse, resultatPeriode.periode) + val foreldetPeriode = foreldelsePeriode != null && foreldelsePeriode.erForeldet() + + return HbResultat( + tilbakekrevesBeløp = resultatPeriode.tilbakekrevingsbeløpUtenRenter, + tilbakekrevesBeløpUtenSkattMedRenter = resultatPeriode.tilbakekrevingsbeløpEtterSkatt, + rentebeløp = resultatPeriode.rentebeløp, + foreldetBeløp = + if (foreldetPeriode) { + resultatPeriode.feilutbetaltBeløp.subtract(resultatPeriode.tilbakekrevingsbeløp) + } else { + null + }, + ) + } + + private fun finnForeldelsePeriode(foreldelse: VurdertForeldelse?, periode: Månedsperiode): Foreldelsesperiode? { + return if (foreldelse == null) { + null + } else { + foreldelse.foreldelsesperioder + .firstOrNull { p -> p.periode.inneholder(periode) } + ?: error("Fant ikke VurdertForeldelse-periode som omslutter periode $periode") + } + } + + private fun finnTittelVedtaksbrev(ytelsesnavn: String, tilbakekreves: Boolean): String { + return if (tilbakekreves) { + TITTEL_VEDTAK_TILBAKEBETALING + ytelsesnavn + } else { + TITTEL_VEDTAK_INGEN_TILBAKEBETALING + ytelsesnavn + } + } + + companion object { + + private const val TITTEL_VEDTAK_TILBAKEBETALING = "Vedtak tilbakebetaling " + private const val TITTEL_VEDTAK_INGEN_TILBAKEBETALING = "Vedtak ingen tilbakebetaling " + private const val KLAGEFRIST_UKER = 6 + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/Vedtaksbrevgrunnlag.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/Vedtaksbrevgrunnlag.kt new file mode 100644 index 000000000..2040a0c04 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/Vedtaksbrevgrunnlag.kt @@ -0,0 +1,165 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultat +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsak +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsakstype +import no.nav.familie.tilbake.behandling.domain.Bruker +import no.nav.familie.tilbake.behandling.domain.Fagsystemsbehandling +import no.nav.familie.tilbake.behandling.domain.Institusjon +import no.nav.familie.tilbake.behandling.domain.Varsel +import no.nav.familie.tilbake.behandling.domain.Verge +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevsporing +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsoppsummering +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsperiode +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurdering +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Embedded +import org.springframework.data.relational.core.mapping.MappedCollection +import org.springframework.data.relational.core.mapping.Table +import java.math.BigDecimal +import java.time.LocalDate +import java.util.UUID + +/** + * Alternativ spring-visualisering av fagsak-tabellen med tilhørende behandlinger. + * Tilpasset databehovet for vedtaksbrev. + */ +@Table("fagsak") +data class Vedtaksbrevgrunnlag( + @Id val id: UUID, + @Embedded(prefix = "bruker_", onEmpty = Embedded.OnEmpty.USE_EMPTY) val bruker: Bruker, + val eksternFagsakId: String, + val fagsystem: Fagsystem, + val ytelsestype: Ytelsestype, + @MappedCollection(idColumn = "fagsak_id") val behandlinger: Set, + @Embedded(prefix = "institusjon_", onEmpty = Embedded.OnEmpty.USE_NULL) val institusjon: Institusjon? = null, +) { + + val behandling + get() = behandlinger.maxByOrNull { it.avsluttetDato ?: LocalDate.MAX } + ?: error("Behandling finnes ikke for vedtak") + + val klagebehandling get() = behandling.sisteÅrsak?.type == Behandlingsårsakstype.REVURDERING_KLAGE_NFP + + val harVerge get() = behandling.verger.any { it.aktiv } + + val brevmottager get() = if (harVerge) Brevmottager.VERGE else if (institusjon != null) Brevmottager.INSTITUSJON else Brevmottager.BRUKER + + val aktivVerge get() = behandling.verger.firstOrNull { it.aktiv } + + val erRevurdering get() = behandling.type == Behandlingstype.REVURDERING_TILBAKEKREVING + + val erRevurderingEtterKlage + get() = behandling.erRevurdering && behandling.årsaker.any { + it.type in setOf(Behandlingsårsakstype.REVURDERING_KLAGE_KA, Behandlingsårsakstype.REVURDERING_KLAGE_NFP) + } + + val varsletBeløp get() = behandling.aktivtVarsel?.varselbeløp?.let { BigDecimal(it) } + + val aktivFagsystemsbehandling get() = behandling.fagsystemsbehandling.first { it.aktiv } + + val erRevurderingEtterKlageNfp + get() = behandling.erRevurdering && behandling.årsaker.any { + it.type == Behandlingsårsakstype.REVURDERING_KLAGE_NFP + } + + val vilkårsvurderingsperioder get() = behandling.vilkårsvurdering.firstOrNull { it.aktiv }?.perioder ?: emptySet() + val vurdertForeldelse get() = behandling.vurderteForeldelser.firstOrNull { it.aktiv } + val faktaFeilutbetaling get() = behandling.faktaFeilutbetaling.firstOrNull { it.aktiv } + + val aktivtSteg + get() = behandling.behandlingsstegstilstander.firstOrNull { + Behandlingsstegstatus.erStegAktiv(it.behandlingsstegsstatus) + }?.behandlingssteg + + val sisteVarsel + get() = behandling.brevsporing.filter { it.brevtype in setOf(Brevtype.VARSEL, Brevtype.KORRIGERT_VARSEL) } + .maxByOrNull { it.sporbar.opprettetTid } + + fun finnOriginalBehandlingVedtaksdato(): LocalDate? { + return if (erRevurdering) { + val behandlingÅrsak = behandling.årsaker.first() + behandlingÅrsak.originalBehandlingId + ?: error("Mangler originalBehandlingId for behandling: ${behandling.id}") + + behandlinger.first { + it.id == behandlingÅrsak.originalBehandlingId + }.sisteResultat?.behandlingsvedtak?.vedtaksdato + ?: error("Mangler vedtaksdato for original behandling med id : ${behandlingÅrsak.originalBehandlingId}") + } else { + null + } + } + + fun utledVedtaksbrevstype(): Vedtaksbrevstype { + return if (erTilbakekrevingRevurderingHarÅrsakFeilutbetalingBortfalt()) { + Vedtaksbrevstype.FRITEKST_FEILUTBETALING_BORTFALT + } else { + Vedtaksbrevstype.ORDINÆR + } + } + + private fun erTilbakekrevingRevurderingHarÅrsakFeilutbetalingBortfalt(): Boolean { + return Behandlingstype.REVURDERING_TILBAKEKREVING == behandling.type && behandling.årsaker.any { + Behandlingsårsakstype.REVURDERING_FEILUTBETALT_BELØP_HELT_ELLER_DELVIS_BORTFALT == it.type + } + } +} + +/** + * Alternativ spring-visualisering av behandling-tabellen med tilhørende relasjoner som er nødvendig for vedtaksbrev. + */ +@Table("behandling") +data class Vedtaksbrevbehandling( + @Id + val id: UUID, + val type: Behandlingstype, + val ansvarligSaksbehandler: String, + val ansvarligBeslutter: String? = null, + val avsluttetDato: LocalDate? = null, + val behandlendeEnhet: String, + val behandlendeEnhetsNavn: String, + @MappedCollection(idColumn = "behandling_id") + val verger: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val fagsystemsbehandling: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val vedtaksbrevOppsummering: Vedtaksbrevsoppsummering?, + @MappedCollection(idColumn = "behandling_id") + val eksisterendePerioderForBrev: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val vilkårsvurdering: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val faktaFeilutbetaling: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val varsler: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val brevsporing: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val resultater: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val behandlingsstegstilstander: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val årsaker: Set = setOf(), + @MappedCollection(idColumn = "behandling_id") + val vurderteForeldelser: Set = setOf(), +) { + + val erRevurdering get() = type == Behandlingstype.REVURDERING_TILBAKEKREVING + + val sisteResultat get() = resultater.maxByOrNull { it.sporbar.endret.endretTid } + + val sisteÅrsak get() = årsaker.firstOrNull() + + val aktivtVarsel get() = varsler.firstOrNull { it.aktiv } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevgrunnlagRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevgrunnlagRepository.kt new file mode 100644 index 000000000..184c94999 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevgrunnlagRepository.kt @@ -0,0 +1,16 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import org.intellij.lang.annotations.Language +import org.springframework.data.jdbc.repository.query.Query +import java.util.UUID + +interface VedtaksbrevgrunnlagRepository : + RepositoryInterface, + InsertUpdateRepository { + + @Language("PostgreSQL") + @Query("SELECT fagsak_id FROM behandling WHERE id = :behandlingId") + fun finnFagsakIdForBehandlingId(behandlingId: UUID): UUID +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevgunnlagService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevgunnlagService.kt new file mode 100644 index 000000000..9e7cfb562 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevgunnlagService.kt @@ -0,0 +1,23 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class VedtaksbrevgunnlagService(private val vedtaksbrevgrunnlagRepository: VedtaksbrevgrunnlagRepository) { + + fun hentVedtaksbrevgrunnlag(behandlingId: UUID): Vedtaksbrevgrunnlag { + val fagsakId = vedtaksbrevgrunnlagRepository.finnFagsakIdForBehandlingId(behandlingId) + val vedtaksbrevgrunnlag = vedtaksbrevgrunnlagRepository.findByIdOrThrow(fagsakId) + + val originalBehandlingId = + vedtaksbrevgrunnlag.behandlinger.first { it.id == behandlingId }.sisteÅrsak?.originalBehandlingId + + return vedtaksbrevgrunnlag.copy( + behandlinger = vedtaksbrevgrunnlag.behandlinger.filter { + it.id == behandlingId || it.id == originalBehandlingId + }.toSet(), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/Vedtaksbrevsdata.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/Vedtaksbrevsdata.kt new file mode 100644 index 000000000..f9b8f84a9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/Vedtaksbrevsdata.kt @@ -0,0 +1,13 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevsdata + +class Vedtaksbrevsdata( + val vedtaksbrevsdata: HbVedtaksbrevsdata, + val metadata: Brevmetadata, +) { + + val hovedresultat: Vedtaksresultat get() = vedtaksbrevsdata.felles.hovedresultat +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/Vedtaksbrevsfritekst.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/Vedtaksbrevsfritekst.kt new file mode 100644 index 000000000..e7c01df58 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/Vedtaksbrevsfritekst.kt @@ -0,0 +1,130 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevsdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVurderinger +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype + +object Vedtaksbrevsfritekst { + + private const val FRITEKST_MARKERING_START = "\\\\FRITEKST_START" + private const val FRITEKST_PÅKREVET_MARKERING_START = "\\\\PÅKREVET_FRITEKST_START" + private const val FRITEKST_MARKERING_SLUTT = "\\\\FRITEKST_SLUTT" + + fun settInnMarkeringForFritekst(vedtaksbrevsdata: HbVedtaksbrevsdata): HbVedtaksbrevsdata { + val perioder = vedtaksbrevsdata.perioder.map { periode -> + val fritekstTypeForFakta = utledFritekstTypeFakta(periode.fakta.hendelsesundertype) + val fakta = periode.fakta.copy( + fritekstFakta = markerFritekst( + fritekstTypeForFakta, + periode.fakta.fritekstFakta, + Underavsnittstype.FAKTA, + ), + ) + val vurderinger: HbVurderinger = + periode.vurderinger + .copy( + fritekstForeldelse = markerValgfriFritekst( + periode.vurderinger.fritekstForeldelse, + Underavsnittstype.FORELDELSE, + ), + fritekst = markerValgfriFritekst( + periode.vurderinger.fritekst, + Underavsnittstype.VILKÅR, + ), + særligeGrunner = periode.vurderinger.særligeGrunner + ?.copy( + fritekst = markerValgfriFritekst( + periode.vurderinger.særligeGrunner.fritekst, + Underavsnittstype.SÆRLIGEGRUNNER, + ), + fritekstAnnet = markerPåkrevetFritekst( + periode.vurderinger + .særligeGrunner + .fritekstAnnet, + Underavsnittstype.SÆRLIGEGRUNNER_ANNET, + ), + ), + ) + periode.copy( + fakta = fakta, + vurderinger = vurderinger, + ) + } + + val fritekstType = utledFritekstTypeForOppsummering(vedtaksbrevsdata) + val felles = vedtaksbrevsdata.felles + .copy(fritekstoppsummering = markerFritekst(fritekstType, vedtaksbrevsdata.felles.fritekstoppsummering, null)) + return vedtaksbrevsdata.copy( + felles = felles, + perioder = perioder, + ) + } + + private fun utledFritekstTypeForOppsummering(vedtaksbrevsdata: HbVedtaksbrevsdata): FritekstType { + val hbBehandling = vedtaksbrevsdata.felles.behandling + return if (hbBehandling.erRevurdering && !hbBehandling.erRevurderingEtterKlage) { + FritekstType.PÅKREVET + } else { + FritekstType.VALGFRI + } + } + + private fun utledFritekstTypeFakta(underType: Hendelsesundertype): FritekstType { + return if (underType in VedtaksbrevFritekstKonfigurasjon.UNDERTYPER_MED_PÅKREVD_FRITEKST) { + FritekstType.PÅKREVET + } else { + FritekstType.VALGFRI + } + } + + @JvmOverloads fun markerValgfriFritekst( + fritekst: String?, + underavsnittstype: Underavsnittstype? = null, + ): String { + return markerFritekst(FritekstType.VALGFRI, fritekst, underavsnittstype) + } + + fun markerPåkrevetFritekst(fritekst: String?, underavsnittstype: Underavsnittstype?): String { + return markerFritekst(FritekstType.PÅKREVET, fritekst, underavsnittstype) + } + + private fun markerFritekst( + fritekstType: FritekstType, + fritekst: String?, + underavsnittstype: Underavsnittstype?, + ): String { + val fritekstTypeMarkør = + if (fritekstType == FritekstType.PÅKREVET) FRITEKST_PÅKREVET_MARKERING_START else FRITEKST_MARKERING_START + val startmarkør = if (underavsnittstype == null) fritekstTypeMarkør else fritekstTypeMarkør + underavsnittstype + return if (fritekst == null) { + "\n$startmarkør\n$FRITEKST_MARKERING_SLUTT" + } else { + "\n$startmarkør\n$fritekst\n$FRITEKST_MARKERING_SLUTT" + } + } + + fun erFritekstStart(tekst: String): Boolean { + return tekst.startsWith(FRITEKST_MARKERING_START) || tekst.startsWith(FRITEKST_PÅKREVET_MARKERING_START) + } + + fun erFritekstPåkrevetStart(tekst: String): Boolean { + return tekst.startsWith(FRITEKST_PÅKREVET_MARKERING_START) + } + + fun fjernFritekstmarkering(tekst: String): String { + return when { + tekst.startsWith(FRITEKST_MARKERING_START) -> tekst.substring(FRITEKST_MARKERING_START.length) + tekst.startsWith(FRITEKST_PÅKREVET_MARKERING_START) -> tekst.substring(FRITEKST_PÅKREVET_MARKERING_START.length) + else -> throw IllegalArgumentException("Utvikler-feil: denne metoden skal bare brukes på fritekstmarkering-start") + } + } + + fun erFritekstSlutt(tekst: String): Boolean { + return FRITEKST_MARKERING_SLUTT == tekst + } + + enum class FritekstType { + VALGFRI, + PÅKREVET, + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsoppsummeringRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsoppsummeringRepository.kt new file mode 100644 index 000000000..96f05e6c9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsoppsummeringRepository.kt @@ -0,0 +1,17 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsoppsummering +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Repository +@Transactional +interface VedtaksbrevsoppsummeringRepository : + RepositoryInterface, + InsertUpdateRepository { + + fun findByBehandlingId(behandlingId: UUID): Vedtaksbrevsoppsummering? +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsperiodeRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsperiodeRepository.kt new file mode 100644 index 000000000..347e45cc0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsperiodeRepository.kt @@ -0,0 +1,17 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsperiode +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Repository +@Transactional +interface VedtaksbrevsperiodeRepository : + RepositoryInterface, + InsertUpdateRepository { + + fun findByBehandlingId(behandlingId: UUID): List +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsvedleggService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsvedleggService.kt new file mode 100644 index 000000000..db003c957 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsvedleggService.kt @@ -0,0 +1,13 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.tilbake.pdfgen.Dokumentvariant +import no.nav.familie.tilbake.pdfgen.PdfGenerator + +class VedtaksbrevsvedleggService { + + private val pdfGenerator: PdfGenerator = PdfGenerator() + fun lagVedlegg(data: Vedtaksbrevsdata, dokumentVariant: Dokumentvariant): ByteArray { + val dokumentSomSteng = TekstformatererVedtaksbrev.lagVedtaksbrevsvedleggHtml(data.vedtaksbrevsdata) + return pdfGenerator.genererPDF(dokumentSomSteng, dokumentVariant) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/domain/Vedtaksbrevsoppsummering.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/domain/Vedtaksbrevsoppsummering.kt new file mode 100644 index 000000000..e45fd7d1b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/domain/Vedtaksbrevsoppsummering.kt @@ -0,0 +1,18 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.domain + +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import java.util.UUID + +data class Vedtaksbrevsoppsummering( + @Id + val id: UUID = UUID.randomUUID(), + val behandlingId: UUID, + val oppsummeringFritekst: String?, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/domain/Vedtaksbrevsperiode.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/domain/Vedtaksbrevsperiode.kt new file mode 100644 index 000000000..d647569cd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/domain/Vedtaksbrevsperiode.kt @@ -0,0 +1,30 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.domain + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import java.util.UUID + +data class Vedtaksbrevsperiode( + @Id + val id: UUID = UUID.randomUUID(), + val behandlingId: UUID, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val periode: Månedsperiode, + val fritekst: String, + val fritekststype: Friteksttype, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +enum class Friteksttype { + FAKTA, + FORELDELSE, + VILKÅR, + SÆRLIGE_GRUNNER, + SÆRLIGE_GRUNNER_ANNET, +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbBehandling.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbBehandling.kt new file mode 100644 index 000000000..c1a4edcb8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbBehandling.kt @@ -0,0 +1,19 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto + +import java.time.LocalDate + +class HbBehandling( + val erRevurdering: Boolean = false, + val erRevurderingEtterKlageNfp: Boolean = false, + val originalBehandlingsdatoFagsakvedtak: LocalDate? = null, + val erRevurderingEtterKlage: Boolean = false, +) { + + init { + if (erRevurdering) { + requireNotNull(originalBehandlingsdatoFagsakvedtak) { "vedtaksdato for original behandling er ikke satt" } + } else { + require(!erRevurderingEtterKlageNfp) { "En revurdering etter klage må være en revurdering." } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbHjemmel.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbHjemmel.kt new file mode 100644 index 000000000..1287d3994 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbHjemmel.kt @@ -0,0 +1,7 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto + +@Suppress("unused") // Handlebars +class HbHjemmel( + val lovhjemmelVedtak: String, + val lovhjemmelFlertall: Boolean = false, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbKonfigurasjon.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbKonfigurasjon.kt new file mode 100644 index 000000000..67dc9cc49 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbKonfigurasjon.kt @@ -0,0 +1,10 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto + +import no.nav.familie.tilbake.config.Constants +import java.math.BigDecimal + +@Suppress("unused") // Handlebars +class HbKonfigurasjon( + val fireRettsgebyr: BigDecimal = BigDecimal.valueOf(Constants.rettsgebyr * 4), + val klagefristIUker: Int, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbPerson.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbPerson.kt new file mode 100644 index 000000000..ec9788e26 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbPerson.kt @@ -0,0 +1,9 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto + +import java.time.LocalDate + +@Suppress("unused") // Handlebars +class HbPerson( + private val navn: String, + private val dødsdato: LocalDate? = null, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbTotalresultat.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbTotalresultat.kt new file mode 100644 index 000000000..b382302d1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbTotalresultat.kt @@ -0,0 +1,15 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto + +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import java.math.BigDecimal + +data class HbTotalresultat( + val hovedresultat: Vedtaksresultat, + val totaltTilbakekrevesBeløp: BigDecimal, + val totaltTilbakekrevesBeløpMedRenter: BigDecimal, + val totaltTilbakekrevesBeløpMedRenterUtenSkatt: BigDecimal, + val totaltRentebeløp: BigDecimal, +) { + + val harSkattetrekk = totaltTilbakekrevesBeløpMedRenterUtenSkatt < totaltTilbakekrevesBeløpMedRenter +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVarsel.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVarsel.kt new file mode 100644 index 000000000..ed8166a77 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVarsel.kt @@ -0,0 +1,10 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto + +import java.math.BigDecimal +import java.time.LocalDate + +@Suppress("unused") // Handlebars +class HbVarsel( + private val varsletDato: LocalDate?, + private val varsletBeløp: BigDecimal?, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVedtaksbrevDatoer.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVedtaksbrevDatoer.kt new file mode 100644 index 000000000..97964e439 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVedtaksbrevDatoer.kt @@ -0,0 +1,35 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto + +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVedtaksbrevsperiode +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import java.time.LocalDate + +class HbVedtaksbrevDatoer( + val opphørsdatoDødSøker: LocalDate? = null, + val opphørsdatoDødtBarn: LocalDate? = null, + val opphørsdatoIkkeOmsorg: LocalDate? = null, +) { + + constructor(perioder: List) : this( + getFørsteDagForHendelsesundertype( + perioder, + Hendelsesundertype.BRUKER_DØD, + ), + getFørsteDagForHendelsesundertype( + perioder, + Hendelsesundertype.BARN_DØD, + ), + ) + + companion object { + + private fun getFørsteDagForHendelsesundertype( + perioder: List, + vararg hendelsesundertyper: Hendelsesundertype, + ): LocalDate? { + return perioder.firstOrNull { + hendelsesundertyper.contains(it.fakta.hendelsesundertype) + }?.periode?.fom + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVedtaksbrevFelles.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVedtaksbrevFelles.kt new file mode 100644 index 000000000..1ab254a5c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVedtaksbrevFelles.kt @@ -0,0 +1,53 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmottagerUtil +import no.nav.familie.tilbake.dokumentbestilling.handlebars.dto.BaseDokument +import java.math.BigDecimal +import java.time.LocalDate + +data class HbVedtaksbrevFelles( + val brevmetadata: Brevmetadata, + val søker: HbPerson, + val fagsaksvedtaksdato: LocalDate, + val varsel: HbVarsel? = null, + val totalresultat: HbTotalresultat, + val hjemmel: HbHjemmel, + val konfigurasjon: HbKonfigurasjon, + val fritekstoppsummering: String? = null, + val vedtaksbrevstype: Vedtaksbrevstype, + val ansvarligBeslutter: String? = null, + val behandling: HbBehandling, + val erFeilutbetaltBeløpKorrigertNed: Boolean = false, + val totaltFeilutbetaltBeløp: BigDecimal, + val datoer: HbVedtaksbrevDatoer? = null, +) : BaseDokument( + brevmetadata.ytelsestype, + brevmetadata.språkkode, + brevmetadata.behandlendeEnhetsNavn, + brevmetadata.ansvarligSaksbehandler, + brevmetadata.gjelderDødsfall, + brevmetadata.institusjon, +) { + + @Suppress("unused") // Handlebars + val opphørsdatoDødSøker = datoer?.opphørsdatoDødSøker + + @Suppress("unused") // Handlebars + val opphørsdatoDødtBarn = datoer?.opphørsdatoDødtBarn + + @Suppress("unused") // Handlebars + val opphørsdatoIkkeOmsorg = datoer?.opphørsdatoIkkeOmsorg + + val annenMottagersNavn: String? = BrevmottagerUtil.getAnnenMottagersNavn(brevmetadata) + + @Suppress("unused") // Handlebars + val skattepliktig = Ytelsestype.OVERGANGSSTØNAD == brevmetadata.ytelsestype + + @Suppress("unused") // Handlebars + val isSkalIkkeViseSkatt = Ytelsestype.OVERGANGSSTØNAD != brevmetadata.ytelsestype || !totalresultat.harSkattetrekk + + val harVedlegg = vedtaksbrevstype == Vedtaksbrevstype.ORDINÆR + val hovedresultat = totalresultat.hovedresultat +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVedtaksbrevPeriodeOgFelles.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVedtaksbrevPeriodeOgFelles.kt new file mode 100644 index 000000000..09f5562ce --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVedtaksbrevPeriodeOgFelles.kt @@ -0,0 +1,16 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto + +import com.fasterxml.jackson.annotation.JsonUnwrapped +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.tilbake.dokumentbestilling.handlebars.dto.Språkstøtte +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVedtaksbrevsperiode + +class HbVedtaksbrevPeriodeOgFelles( + @get:JsonUnwrapped + val felles: HbVedtaksbrevFelles, + @get:JsonUnwrapped + val periode: HbVedtaksbrevsperiode, +) : Språkstøtte { + + override val språkkode: Språkkode = felles.språkkode +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVedtaksbrevsdata.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVedtaksbrevsdata.kt new file mode 100644 index 000000000..f33e7f068 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/HbVedtaksbrevsdata.kt @@ -0,0 +1,17 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto + +import com.fasterxml.jackson.annotation.JsonUnwrapped +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.tilbake.dokumentbestilling.handlebars.dto.Språkstøtte +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVedtaksbrevsperiode + +data class HbVedtaksbrevsdata( + @get:JsonUnwrapped val felles: HbVedtaksbrevFelles, + val perioder: List, +) : Språkstøtte { + + @Suppress("unused") // Handlebars + val antallPerioder = perioder.size + + override val språkkode: Språkkode = felles.språkkode +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/Vedtaksbrevstype.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/Vedtaksbrevstype.kt new file mode 100644 index 000000000..10f9ee367 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/Vedtaksbrevstype.kt @@ -0,0 +1,6 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto + +enum class Vedtaksbrevstype { + ORDINÆR, + FRITEKST_FEILUTBETALING_BORTFALT, +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbFakta.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbFakta.kt new file mode 100644 index 000000000..d3831d670 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbFakta.kt @@ -0,0 +1,16 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode + +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.HendelsesundertypePerHendelsestype + +data class HbFakta( + val hendelsestype: Hendelsestype, + val hendelsesundertype: Hendelsesundertype, + val fritekstFakta: String? = null, +) { + + init { + require(HendelsesundertypePerHendelsestype.getHendelsesundertyper(hendelsestype).contains(hendelsesundertype)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbKravgrunnlag.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbKravgrunnlag.kt new file mode 100644 index 000000000..70e62ea00 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbKravgrunnlag.kt @@ -0,0 +1,18 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode + +import java.math.BigDecimal + +@Suppress("unused") // Handlebars +class HbKravgrunnlag( + val riktigBeløp: BigDecimal? = null, + val utbetaltBeløp: BigDecimal? = null, + val feilutbetaltBeløp: BigDecimal, +) { + + companion object { + + fun forFeilutbetaltBeløp(feilutbetaltBeløp: BigDecimal): HbKravgrunnlag { + return HbKravgrunnlag(feilutbetaltBeløp = feilutbetaltBeløp) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbResultat.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbResultat.kt new file mode 100644 index 000000000..d4c4a596e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbResultat.kt @@ -0,0 +1,14 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode + +import java.math.BigDecimal + +@Suppress("unused") // Handlebars +class HbResultat( + val tilbakekrevesBeløp: BigDecimal, + val rentebeløp: BigDecimal, + val foreldetBeløp: BigDecimal? = null, + val tilbakekrevesBeløpUtenSkattMedRenter: BigDecimal, +) { + + val tilbakekrevesBeløpMedRenter: BigDecimal = tilbakekrevesBeløp.add(rentebeløp) +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbS\303\246rligeGrunner.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbS\303\246rligeGrunner.kt" new file mode 100644 index 000000000..a08c6150a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbS\303\246rligeGrunner.kt" @@ -0,0 +1,26 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode + +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn + +data class HbSærligeGrunner( + val størrelse: Boolean = false, + val annet: Boolean = false, + val navfeil: Boolean = false, + val tid: Boolean = false, + val fritekst: String? = null, + val fritekstAnnet: String? = null, +) { + + constructor( + grunner: Collection, + fritekst: String? = null, + fritekstAnnet: String? = null, + ) : this( + grunner.contains(SærligGrunn.STØRRELSE_BELØP), + grunner.contains(SærligGrunn.ANNET), + grunner.contains(SærligGrunn.HELT_ELLER_DELVIS_NAVS_FEIL), + grunner.contains(SærligGrunn.TID_FRA_UTBETALING), + fritekst, + fritekstAnnet, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbVedtaksbrevsperiode.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbVedtaksbrevsperiode.kt new file mode 100644 index 000000000..040498641 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbVedtaksbrevsperiode.kt @@ -0,0 +1,24 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import java.math.BigDecimal + +data class HbVedtaksbrevsperiode( + val periode: Datoperiode, + val kravgrunnlag: HbKravgrunnlag, + val fakta: HbFakta, + val vurderinger: HbVurderinger, + val resultat: HbResultat, + val førstePeriode: Boolean, + val grunnbeløp: HbGrunnbeløp? = null, +) { + + init { + if (fakta.hendelsesundertype == Hendelsesundertype.INNTEKT_OVER_6G) { + require(grunnbeløp != null) { "${Hendelsesundertype.INNTEKT_OVER_6G} krever verdi for grunnbeløp." } + } + } +} + +data class HbGrunnbeløp(val grunnbeløpGanger6: BigDecimal?, val tekst6GangerGrunnbeløp: String?) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbVurderinger.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbVurderinger.kt new file mode 100644 index 000000000..de15c6be7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbVurderinger.kt @@ -0,0 +1,45 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode + +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.vilkårsvurdering.domain.AnnenVurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vurdering +import java.math.BigDecimal +import java.time.LocalDate + +data class HbVurderinger( + val vilkårsvurderingsresultat: Vilkårsvurderingsresultat? = null, + val fritekst: String? = null, + val aktsomhetsresultat: Vurdering? = null, + val unntasInnkrevingPgaLavtBeløp: Boolean = false, + val særligeGrunner: HbSærligeGrunner? = null, + val foreldelsevurdering: Foreldelsesvurderingstype = Foreldelsesvurderingstype.IKKE_VURDERT, + val foreldelsesfrist: LocalDate? = null, + val oppdagelsesdato: LocalDate? = null, + val fritekstForeldelse: String? = null, + val beløpIBehold: BigDecimal? = null, +) { + + val harForeldelsesavsnitt = foreldelsevurdering in setOf( + Foreldelsesvurderingstype.FORELDET, + Foreldelsesvurderingstype.TILLEGGSFRIST, + ) + + init { + if (Foreldelsesvurderingstype.IKKE_VURDERT == foreldelsevurdering || + Foreldelsesvurderingstype.IKKE_FORELDET == foreldelsevurdering + ) { + requireNotNull(vilkårsvurderingsresultat) { "Vilkårsvurderingsresultat er ikke satt" } + } else if (Foreldelsesvurderingstype.FORELDET == foreldelsevurdering) { + requireNotNull(foreldelsesfrist) { "foreldelsesfrist er ikke satt" } + } else if (Foreldelsesvurderingstype.TILLEGGSFRIST == foreldelsevurdering) { + requireNotNull(foreldelsesfrist) { "foreldelsesfrist er ikke satt" } + requireNotNull(oppdagelsesdato) { "oppdagelsesdato er ikke satt" } + } + if (AnnenVurdering.GOD_TRO == aktsomhetsresultat) { + requireNotNull(beløpIBehold) { "beløp i behold er ikke satt" } + } else { + require(beløpIBehold == null) { "beløp i behold skal ikke være satt når aktsomhetsresultat er $aktsomhetsresultat" } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingMapper.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingMapper.kt new file mode 100644 index 000000000..710cfc4da --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingMapper.kt @@ -0,0 +1,82 @@ +package no.nav.familie.tilbake.faktaomfeilutbetaling + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.tilbake.api.dto.FaktaFeilutbetalingDto +import no.nav.familie.tilbake.api.dto.FeilutbetalingsperiodeDto +import no.nav.familie.tilbake.behandling.domain.Fagsystemsbehandling +import no.nav.familie.tilbake.behandling.domain.Varsel +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagUtil +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +object FaktaFeilutbetalingMapper { + + fun tilRespons( + faktaFeilutbetaling: FaktaFeilutbetaling?, + kravgrunnlag: Kravgrunnlag431, + revurderingsvedtaksdato: LocalDate, + varsletData: Varsel?, + fagsystemsbehandling: Fagsystemsbehandling, + ): FaktaFeilutbetalingDto { + val logiskePerioder = + LogiskPeriodeUtil.utledLogiskPeriode(KravgrunnlagUtil.finnFeilutbetalingPrPeriode(kravgrunnlag)) + val feilutbetaltePerioder = hentFeilutbetaltePerioder( + faktaFeilutbetaling = faktaFeilutbetaling, + logiskePerioder = logiskePerioder, + ) + val faktainfo = Faktainfo( + revurderingsårsak = fagsystemsbehandling.årsak, + revurderingsresultat = fagsystemsbehandling.resultat, + tilbakekrevingsvalg = fagsystemsbehandling.tilbakekrevingsvalg, + konsekvensForYtelser = fagsystemsbehandling.konsekvenser.map { it.konsekvens }.toSet(), + ) + + return FaktaFeilutbetalingDto( + varsletBeløp = varsletData?.varselbeløp, + revurderingsvedtaksdato = revurderingsvedtaksdato, + begrunnelse = faktaFeilutbetaling?.begrunnelse ?: "", + faktainfo = faktainfo, + feilutbetaltePerioder = feilutbetaltePerioder, + totaltFeilutbetaltBeløp = logiskePerioder.sumOf(LogiskPeriode::feilutbetaltBeløp), + totalFeilutbetaltPeriode = utledTotalFeilutbetaltPeriode(logiskePerioder), + ) + } + + private fun hentFeilutbetaltePerioder( + faktaFeilutbetaling: FaktaFeilutbetaling?, + logiskePerioder: List, + ): List { + return faktaFeilutbetaling?.perioder?.map { + FeilutbetalingsperiodeDto( + periode = it.periode.toDatoperiode(), + feilutbetaltBeløp = hentFeilutbetaltBeløp(logiskePerioder, it.periode), + hendelsestype = it.hendelsestype, + hendelsesundertype = it.hendelsesundertype, + ) + } ?: logiskePerioder.map { + FeilutbetalingsperiodeDto( + periode = it.periode.toDatoperiode(), + feilutbetaltBeløp = it.feilutbetaltBeløp, + ) + } + } + + private fun hentFeilutbetaltBeløp(logiskePerioder: List, faktaPeriode: Månedsperiode): BigDecimal { + return logiskePerioder.first { faktaPeriode == it.periode }.feilutbetaltBeløp + } + + private fun utledTotalFeilutbetaltPeriode(perioder: List): Datoperiode { + var totalPeriodeFom: YearMonth? = null + var totalPeriodeTom: YearMonth? = null + for (periode in perioder) { + totalPeriodeFom = if (totalPeriodeFom == null || totalPeriodeFom > periode.fom) periode.fom else totalPeriodeFom + totalPeriodeTom = if (totalPeriodeTom == null || totalPeriodeTom < periode.tom) periode.tom else totalPeriodeTom + } + return Datoperiode(totalPeriodeFom!!, totalPeriodeTom!!) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingRepository.kt new file mode 100644 index 000000000..8d5a98949 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingRepository.kt @@ -0,0 +1,17 @@ +package no.nav.familie.tilbake.faktaomfeilutbetaling + +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository +interface FaktaFeilutbetalingRepository : + RepositoryInterface, + InsertUpdateRepository { + + fun findByBehandlingIdAndAktivIsTrue(behandlingId: UUID): FaktaFeilutbetaling? + + fun findFaktaFeilutbetalingByBehandlingIdAndAktivIsTrue(behandlingId: UUID): FaktaFeilutbetaling +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingService.kt new file mode 100644 index 000000000..c5fc72d0a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingService.kt @@ -0,0 +1,89 @@ +package no.nav.familie.tilbake.faktaomfeilutbetaling + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.api.dto.BehandlingsstegFaktaDto +import no.nav.familie.tilbake.api.dto.FaktaFeilutbetalingDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetalingsperiode +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class FaktaFeilutbetalingService( + private val behandlingRepository: BehandlingRepository, + private val faktaFeilutbetalingRepository: FaktaFeilutbetalingRepository, + private val kravgrunnlagRepository: KravgrunnlagRepository, +) { + + @Transactional(readOnly = true) + fun hentFaktaomfeilutbetaling(behandlingId: UUID): FaktaFeilutbetalingDto { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val faktaFeilutbetaling = hentAktivFaktaOmFeilutbetaling(behandlingId) + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + return FaktaFeilutbetalingMapper + .tilRespons( + faktaFeilutbetaling = faktaFeilutbetaling, + kravgrunnlag = kravgrunnlag, + revurderingsvedtaksdato = behandling.aktivFagsystemsbehandling.revurderingsvedtaksdato, + varsletData = behandling.aktivtVarsel, + fagsystemsbehandling = behandling.aktivFagsystemsbehandling, + ) + } + + @Transactional + fun lagreFaktaomfeilutbetaling(behandlingId: UUID, behandlingsstegFaktaDto: BehandlingsstegFaktaDto) { + deaktiverEksisterendeFaktaOmFeilutbetaling(behandlingId) + + val feilutbetaltePerioder: Set = behandlingsstegFaktaDto.feilutbetaltePerioder.map { + FaktaFeilutbetalingsperiode( + periode = Månedsperiode(it.periode.fom, it.periode.tom), + hendelsestype = it.hendelsestype, + hendelsesundertype = it.hendelsesundertype, + ) + }.toSet() + + faktaFeilutbetalingRepository.insert( + FaktaFeilutbetaling( + behandlingId = behandlingId, + perioder = feilutbetaltePerioder, + begrunnelse = behandlingsstegFaktaDto.begrunnelse, + ), + ) + } + + @Transactional + fun lagreFastFaktaForAutomatiskSaksbehandling(behandlingId: UUID) { + val feilutbetaltePerioder = hentFaktaomfeilutbetaling(behandlingId).feilutbetaltePerioder.map { + FaktaFeilutbetalingsperiode( + periode = it.periode.toMånedsperiode(), + hendelsestype = Hendelsestype.ANNET, + hendelsesundertype = Hendelsesundertype.ANNET_FRITEKST, + ) + }.toSet() + faktaFeilutbetalingRepository.insert( + FaktaFeilutbetaling( + behandlingId = behandlingId, + perioder = feilutbetaltePerioder, + begrunnelse = Constants.AUTOMATISK_SAKSBEHANDLING_BEGUNNLESE, + ), + ) + } + + fun hentAktivFaktaOmFeilutbetaling(behandlingId: UUID): FaktaFeilutbetaling? { + return faktaFeilutbetalingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + } + + @Transactional + fun deaktiverEksisterendeFaktaOmFeilutbetaling(behandlingId: UUID) { + faktaFeilutbetalingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId)?.copy(aktiv = false)?.let { + faktaFeilutbetalingRepository.update(it) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/LogiskPeriode.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/LogiskPeriode.kt new file mode 100644 index 000000000..efd58d3e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/LogiskPeriode.kt @@ -0,0 +1,58 @@ +package no.nav.familie.tilbake.faktaomfeilutbetaling + +import no.nav.familie.kontrakter.felles.Månedsperiode +import java.math.BigDecimal +import java.time.YearMonth +import java.util.SortedMap + +data class LogiskPeriode( + val periode: Månedsperiode, + val feilutbetaltBeløp: BigDecimal, +) { + + val fom get() = periode.fom + val tom get() = periode.tom +} + +object LogiskPeriodeUtil { + + fun utledLogiskPeriode(feilutbetalingPrPeriode: SortedMap): List { + var førsteMåned: YearMonth? = null + var sisteMåned: YearMonth? = null + var logiskPeriodeBeløp = BigDecimal.ZERO + val resultat = mutableListOf() + for ((periode, feilutbetaltBeløp) in feilutbetalingPrPeriode) { + if (førsteMåned == null && sisteMåned == null) { + førsteMåned = periode.fom + sisteMåned = periode.tom + } else { + if (harOppholdMellom(sisteMåned!!, periode.fom)) { + resultat.add( + LogiskPeriode( + periode = Månedsperiode(førsteMåned!!, sisteMåned), + feilutbetaltBeløp = logiskPeriodeBeløp, + ), + ) + førsteMåned = periode.fom + logiskPeriodeBeløp = BigDecimal.ZERO + } + sisteMåned = periode.tom + } + logiskPeriodeBeløp = logiskPeriodeBeløp.add(feilutbetaltBeløp) + } + if (BigDecimal.ZERO.compareTo(logiskPeriodeBeløp) != 0) { + resultat.add( + LogiskPeriode( + periode = Månedsperiode(førsteMåned!!, sisteMåned!!), + feilutbetaltBeløp = logiskPeriodeBeløp, + ), + ) + } + return resultat.toList() + } + + private fun harOppholdMellom(måned1: YearMonth, måned2: YearMonth): Boolean { + require(måned2 > måned1) { "dag2 må være etter dag1" } + return måned1.plusMonths(1) != måned2 + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/domain/FaktaFeilutbetaling.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/domain/FaktaFeilutbetaling.kt new file mode 100644 index 000000000..a0d84d24f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/domain/FaktaFeilutbetaling.kt @@ -0,0 +1,22 @@ +package no.nav.familie.tilbake.faktaomfeilutbetaling.domain + +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import org.springframework.data.relational.core.mapping.MappedCollection +import java.util.UUID + +data class FaktaFeilutbetaling( + @Id + val id: UUID = UUID.randomUUID(), + val behandlingId: UUID, + val aktiv: Boolean = true, + val begrunnelse: String?, + @MappedCollection(idColumn = "fakta_feilutbetaling_id") + val perioder: Set = setOf(), + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/domain/FaktaFeilutbetalingsperiode.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/domain/FaktaFeilutbetalingsperiode.kt new file mode 100644 index 000000000..52fffbda4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/domain/FaktaFeilutbetalingsperiode.kt @@ -0,0 +1,168 @@ +package no.nav.familie.tilbake.faktaomfeilutbetaling.domain + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import java.util.UUID + +data class FaktaFeilutbetalingsperiode( + @Id + val id: UUID = UUID.randomUUID(), + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val periode: Månedsperiode, + val hendelsestype: Hendelsestype, + val hendelsesundertype: Hendelsesundertype, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +enum class Hendelsestype { + ANNET, + BOR_MED_SØKER, + BOSATT_I_RIKET, + LOVLIG_OPPHOLD, + DØDSFALL, + DELT_BOSTED, + BARNS_ALDER, + MEDLEMSKAP, + OPPHOLD_I_NORGE, + ENSLIG_FORSØRGER, + OVERGANGSSTØNAD, + YRKESRETTET_AKTIVITET, + STØNADSPERIODE, + INNTEKT, + PENSJONSYTELSER, + STØNAD_TIL_BARNETILSYN, + SKOLEPENGER, + SATSER, + SMÅBARNSTILLEGG, + MEDLEMSKAP_BA, + UTVIDET, + VILKÅR_BARN, + VILKÅR_SØKER, + BARN_I_FOSTERHJEM_ELLER_INSTITUSJON, + KONTANTSTØTTENS_STØRRELSE, + STØTTEPERIODE, + UTBETALING, + KONTANTSTØTTE_FOR_ADOPTERTE_BARN, + ANNET_KS, +} + +enum class Hendelsesundertype { + + ANNET_FRITEKST, + BOR_IKKE_MED_BARN, + BARN_FLYTTET_FRA_NORGE, + BRUKER_FLYTTET_FRA_NORGE, + BARN_BOR_IKKE_I_NORGE, + BRUKER_BOR_IKKE_I_NORGE, + UTEN_OPPHOLDSTILLATELSE, + BARN_DØD, + BRUKER_DØD, + ENIGHET_OM_OPPHØR_DELT_BOSTED, + UENIGHET_OM_OPPHØR_DELT_BOSTED, + BARN_OVER_18_ÅR, + BARN_OVER_6_ÅR, + MEDLEM_SISTE_5_ÅR, + LOVLIG_OPPHOLD, + BRUKER_IKKE_OPPHOLD_I_NORGE, + BARN_IKKE_OPPHOLD_I_NORGE, + OPPHOLD_UTLAND_6_UKER_ELLER_MER, + UGIFT, + SEPARERT_SKILT, + SAMBOER, + NYTT_BARN_SAMME_PARTNER, + ENDRET_SAMVÆRSORDNING, + BARN_FLYTTET, + NÆRE_BOFORHOLD, + FORELDRE_LEVER_SAMMEN, + BARN_8_ÅR, + BARN_FYLT_1_ÅR, + UTDANNING, + ETABLERER_EGEN_VIRKSOMHET, + HOVEDPERIODE_3_ÅR, + UTVIDELSE_UTDANNING, + UTVIDELSE_SÆRLIG_TILSYNSKREVENDE_BARN, + UTVIDELSE_FORBIGÅENDE_SYKDOM, + PÅVENTE_AV_SKOLESTART_STARTET_IKKE, + PÅVENTE_SKOLESTART_STARTET_TIDLIGERE, + PÅVENTE_ARBEIDSTILBUD_STARTET_IKKE, + PÅVENTE_ARBEIDSTILBUD_STARTET_TIDLIGERE, + PÅVENTE_BARNETILSYN_IKKE_HA_TILSYN, + PÅVENTE_BARNETILSYN_STARTET_TIDLIGERE, + ARBEIDSSØKER, + REELL_ARBEIDSSØKER, + ARBEIDSINNTEKT_FÅTT_INNTEKT, + ARBEIDSINNTEKT_ENDRET_INNTEKT, + ANDRE_FOLKETRYGDYTELSER, + SELVSTENDIG_NÆRINGSDRIVENDE_FÅTT_INNTEKT, + SELVSTENDIG_NÆRINGSDRIVENDE_ENDRET_INNTEKT, + UFØRETRYGD, + GJENLEVENDE_EKTEFELLE, + ARBEID, + EGEN_VIRKSOMHET, + TILSYNSUTGIFTER_OPPHØRT, + TILSYNSUTGIFTER_ENDRET, + FORBIGÅENDE_SYKDOM, + ETTER_4_SKOLEÅR_UTGIFTENE_OPPHØRT, + ETTER_4_SKOLEÅR_ENDRET_ARBEIDSTID, + INNTEKT_OVER_6G, + KONTANTSTØTTE, + ØKT_KONTANTSTØTTE, + IKKE_RETT_TIL_OVERGANGSSTØNAD, + SLUTTET_I_UTDANNING, + IKKE_ARBEID, + SMÅBARNSTILLEGG_OVERGANGSSTØNAD, + SATSENDRING, + SMÅBARNSTILLEGG_3_ÅR, + BRUKER_OG_BARN_FLYTTET_FRA_NORGE, + BRUKER_OG_BARN_BOR_IKKE_I_NORGE, + FLYTTET_SAMMEN, + UTENLANDS_IKKE_MEDLEM, + MEDLEMSKAP_OPPHØRT, + ANNEN_FORELDER_IKKE_MEDLEM, + ANNEN_FORELDER_OPPHØRT_MEDLEMSKAP, + FLERE_UTENLANDSOPPHOLD, + BOSATT_IKKE_MEDLEM, + GIFT, + NYTT_BARN, + SAMBOER_12_MÅNEDER, + FLYTTET_SAMMEN_ANNEN_FORELDER, + FLYTTET_SAMMEN_EKTEFELLE, + FLYTTET_SAMMEN_SAMBOER, + GIFT_IKKE_EGEN_HUSHOLDNING, + SAMBOER_IKKE_EGEN_HUSHOLDNING, + EKTEFELLE_AVSLUTTET_SONING, + SAMBOER_AVSLUTTET_SONING, + EKTEFELLE_INSTITUSJON, + SAMBOER_INSTITUSJON, + BARN_IKKE_BOSATT, + BARN_IKKE_OPPHOLDSTILLATELSE, + BARN_OVER_2_ÅR, + DEN_ANDRE_FORELDEREN_IKKE_MEDLEM_FOLKETRYGDEN, + DEN_ANDRE_FORELDEREN_IKKE_MEDLEM_FOLKETRYGDEN_ELLER_EØS, + SØKER_IKKE_MEDLEM_FOLKETRYGDEN, + SØKER_IKKE_MEDLEM_FOLKETRYGDEN_ELLER_EØS, + BEGGE_FORELDRENE_IKKE_MEDLEM_FOLKETRYGDEN, + BEGGE_FORELDRENE_IKKE_MEDLEM_FOLKETRYGDEN_ELLER_EØS, + BARN_BOR_IKKE_HOS_SØKER, + UTENLANDSOPPHOLD_OVER_3_MÅNEDER, + SØKER_FLYTTET_FRA_NORGE, + SØKER_IKKE_BOSATT, + SØKER_IKKE_OPPHOLDSTILLATELSE, + SØKER_IKKE_OPPHOLDSTILLATELSE_I_MER_ENN_12_MÅNEDER, + BARN_I_FOSTERHJEM, + BARN_I_INSTITUSJON, + FULLTIDSPLASS_BARNEHAGE, + DELTIDSPLASS_BARNEHAGEPLASS, + ØKT_TIMEANTALL_I_BARNEHAGE, + BARN_2_ÅR, + DELT_BOSTED_AVTALE_OPPHØRT, + DOBBELUTBETALING, + MER_ENN_11_MÅNEDER, + BARN_STARTET_PÅ_SKOLEN, +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/domain/HendelsestypePerYtelsestype.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/domain/HendelsestypePerYtelsestype.kt new file mode 100644 index 000000000..9191b9eac --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/domain/HendelsestypePerYtelsestype.kt @@ -0,0 +1,64 @@ +package no.nav.familie.tilbake.faktaomfeilutbetaling.domain + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype + +object HendelsestypePerYtelsestype { + + private val HIERARKI = mapOf( + Ytelsestype.BARNETRYGD to setOf( + Hendelsestype.ANNET, + Hendelsestype.SATSER, + Hendelsestype.SMÅBARNSTILLEGG, + Hendelsestype.MEDLEMSKAP_BA, + Hendelsestype.BOR_MED_SØKER, + Hendelsestype.DØDSFALL, + Hendelsestype.BOSATT_I_RIKET, + Hendelsestype.LOVLIG_OPPHOLD, + Hendelsestype.DELT_BOSTED, + Hendelsestype.BARNS_ALDER, + Hendelsestype.UTVIDET, + ), + Ytelsestype.OVERGANGSSTØNAD to setOf( + Hendelsestype.ANNET, + Hendelsestype.MEDLEMSKAP, + Hendelsestype.OPPHOLD_I_NORGE, + Hendelsestype.ENSLIG_FORSØRGER, + Hendelsestype.OVERGANGSSTØNAD, + Hendelsestype.YRKESRETTET_AKTIVITET, + Hendelsestype.STØNADSPERIODE, + Hendelsestype.INNTEKT, + Hendelsestype.PENSJONSYTELSER, + Hendelsestype.DØDSFALL, + ), + Ytelsestype.BARNETILSYN to setOf( + Hendelsestype.ANNET, + Hendelsestype.MEDLEMSKAP, + Hendelsestype.OPPHOLD_I_NORGE, + Hendelsestype.ENSLIG_FORSØRGER, + Hendelsestype.STØNAD_TIL_BARNETILSYN, + Hendelsestype.DØDSFALL, + ), + Ytelsestype.SKOLEPENGER to setOf( + Hendelsestype.ANNET, + Hendelsestype.MEDLEMSKAP, + Hendelsestype.OPPHOLD_I_NORGE, + Hendelsestype.ENSLIG_FORSØRGER, + Hendelsestype.DØDSFALL, + Hendelsestype.SKOLEPENGER, + ), + Ytelsestype.KONTANTSTØTTE to setOf( + Hendelsestype.VILKÅR_BARN, + Hendelsestype.VILKÅR_SØKER, + Hendelsestype.BARN_I_FOSTERHJEM_ELLER_INSTITUSJON, + Hendelsestype.KONTANTSTØTTENS_STØRRELSE, + Hendelsestype.STØTTEPERIODE, + Hendelsestype.UTBETALING, + Hendelsestype.KONTANTSTØTTE_FOR_ADOPTERTE_BARN, + Hendelsestype.ANNET_KS, + ), + ) + + fun getHendelsestyper(ytelsestype: Ytelsestype): Set { + return HIERARKI[ytelsestype] ?: error("Ikke-støttet ytelsestype: $ytelsestype") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/domain/HendelsesundertypePerHendelsestype.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/domain/HendelsesundertypePerHendelsestype.kt new file mode 100644 index 000000000..a207837f1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/domain/HendelsesundertypePerHendelsestype.kt @@ -0,0 +1,183 @@ +package no.nav.familie.tilbake.faktaomfeilutbetaling.domain + +object HendelsesundertypePerHendelsestype { + + val HIERARKI = mapOf( + Hendelsestype.ANNET to setOf(Hendelsesundertype.ANNET_FRITEKST), + Hendelsestype.SATSER to setOf(Hendelsesundertype.SATSENDRING), + Hendelsestype.SMÅBARNSTILLEGG to setOf( + Hendelsesundertype.SMÅBARNSTILLEGG_3_ÅR, + Hendelsesundertype.SMÅBARNSTILLEGG_OVERGANGSSTØNAD, + ), + Hendelsestype.BOR_MED_SØKER to setOf(Hendelsesundertype.BOR_IKKE_MED_BARN), + Hendelsestype.BOSATT_I_RIKET to setOf( + Hendelsesundertype.BARN_FLYTTET_FRA_NORGE, + Hendelsesundertype.BRUKER_FLYTTET_FRA_NORGE, + Hendelsesundertype.BARN_BOR_IKKE_I_NORGE, + Hendelsesundertype.BRUKER_BOR_IKKE_I_NORGE, + Hendelsesundertype.BRUKER_OG_BARN_FLYTTET_FRA_NORGE, + Hendelsesundertype.BRUKER_OG_BARN_BOR_IKKE_I_NORGE, + ), + Hendelsestype.LOVLIG_OPPHOLD to setOf(Hendelsesundertype.UTEN_OPPHOLDSTILLATELSE), + Hendelsestype.DØDSFALL to setOf( + Hendelsesundertype.BARN_DØD, + Hendelsesundertype.BRUKER_DØD, + ), + Hendelsestype.DELT_BOSTED to setOf( + Hendelsesundertype.ENIGHET_OM_OPPHØR_DELT_BOSTED, + Hendelsesundertype.UENIGHET_OM_OPPHØR_DELT_BOSTED, + Hendelsesundertype.FLYTTET_SAMMEN, + ), + Hendelsestype.BARNS_ALDER to setOf( + Hendelsesundertype.BARN_OVER_6_ÅR, + Hendelsesundertype.BARN_OVER_18_ÅR, + ), + Hendelsestype.MEDLEMSKAP to setOf( + Hendelsesundertype.MEDLEM_SISTE_5_ÅR, + Hendelsesundertype.LOVLIG_OPPHOLD, + ), + Hendelsestype.MEDLEMSKAP_BA to setOf( + Hendelsesundertype.UTENLANDS_IKKE_MEDLEM, + Hendelsesundertype.MEDLEMSKAP_OPPHØRT, + Hendelsesundertype.ANNEN_FORELDER_IKKE_MEDLEM, + Hendelsesundertype.ANNEN_FORELDER_OPPHØRT_MEDLEMSKAP, + Hendelsesundertype.FLERE_UTENLANDSOPPHOLD, + Hendelsesundertype.BOSATT_IKKE_MEDLEM, + ), + Hendelsestype.UTVIDET to setOf( + Hendelsesundertype.GIFT, + Hendelsesundertype.NYTT_BARN, + Hendelsesundertype.SAMBOER_12_MÅNEDER, + Hendelsesundertype.FLYTTET_SAMMEN_ANNEN_FORELDER, + Hendelsesundertype.FLYTTET_SAMMEN_EKTEFELLE, + Hendelsesundertype.FLYTTET_SAMMEN_SAMBOER, + Hendelsesundertype.GIFT_IKKE_EGEN_HUSHOLDNING, + Hendelsesundertype.SAMBOER_IKKE_EGEN_HUSHOLDNING, + Hendelsesundertype.EKTEFELLE_AVSLUTTET_SONING, + Hendelsesundertype.SAMBOER_AVSLUTTET_SONING, + Hendelsesundertype.EKTEFELLE_INSTITUSJON, + Hendelsesundertype.SAMBOER_INSTITUSJON, + ), + Hendelsestype.OPPHOLD_I_NORGE to setOf( + Hendelsesundertype.BRUKER_IKKE_OPPHOLD_I_NORGE, + Hendelsesundertype.BARN_IKKE_OPPHOLD_I_NORGE, + Hendelsesundertype.BRUKER_FLYTTET_FRA_NORGE, + Hendelsesundertype.BARN_FLYTTET_FRA_NORGE, + Hendelsesundertype.OPPHOLD_UTLAND_6_UKER_ELLER_MER, + ), + Hendelsestype.ENSLIG_FORSØRGER to setOf( + Hendelsesundertype.UGIFT, + Hendelsesundertype.SEPARERT_SKILT, + Hendelsesundertype.SAMBOER, + Hendelsesundertype.NYTT_BARN_SAMME_PARTNER, + Hendelsesundertype.ENDRET_SAMVÆRSORDNING, + Hendelsesundertype.BARN_FLYTTET, + Hendelsesundertype.NÆRE_BOFORHOLD, + Hendelsesundertype.FORELDRE_LEVER_SAMMEN, + ), + Hendelsestype.OVERGANGSSTØNAD to setOf(Hendelsesundertype.BARN_8_ÅR), + Hendelsestype.YRKESRETTET_AKTIVITET to setOf( + Hendelsesundertype.ARBEID, + Hendelsesundertype.REELL_ARBEIDSSØKER, + Hendelsesundertype.UTDANNING, + Hendelsesundertype.ETABLERER_EGEN_VIRKSOMHET, + Hendelsesundertype.BARN_FYLT_1_ÅR, + + ), + Hendelsestype.STØNADSPERIODE to setOf( + Hendelsesundertype.HOVEDPERIODE_3_ÅR, + Hendelsesundertype.UTVIDELSE_UTDANNING, + Hendelsesundertype.UTVIDELSE_SÆRLIG_TILSYNSKREVENDE_BARN, + Hendelsesundertype.UTVIDELSE_FORBIGÅENDE_SYKDOM, + Hendelsesundertype.PÅVENTE_AV_SKOLESTART_STARTET_IKKE, + Hendelsesundertype.PÅVENTE_SKOLESTART_STARTET_TIDLIGERE, + Hendelsesundertype.PÅVENTE_ARBEIDSTILBUD_STARTET_IKKE, + Hendelsesundertype.PÅVENTE_ARBEIDSTILBUD_STARTET_TIDLIGERE, + Hendelsesundertype.PÅVENTE_BARNETILSYN_IKKE_HA_TILSYN, + Hendelsesundertype.PÅVENTE_BARNETILSYN_STARTET_TIDLIGERE, + Hendelsesundertype.ARBEIDSSØKER, + ), + Hendelsestype.INNTEKT to setOf( + Hendelsesundertype.ARBEIDSINNTEKT_FÅTT_INNTEKT, + Hendelsesundertype.ARBEIDSINNTEKT_ENDRET_INNTEKT, + Hendelsesundertype.ANDRE_FOLKETRYGDYTELSER, + Hendelsesundertype.SELVSTENDIG_NÆRINGSDRIVENDE_FÅTT_INNTEKT, + Hendelsesundertype.SELVSTENDIG_NÆRINGSDRIVENDE_ENDRET_INNTEKT, + ), + Hendelsestype.PENSJONSYTELSER to setOf( + Hendelsesundertype.UFØRETRYGD, + Hendelsesundertype.GJENLEVENDE_EKTEFELLE, + ), + Hendelsestype.STØNAD_TIL_BARNETILSYN to setOf( + Hendelsesundertype.IKKE_ARBEID, + Hendelsesundertype.EGEN_VIRKSOMHET, + Hendelsesundertype.TILSYNSUTGIFTER_OPPHØRT, + Hendelsesundertype.TILSYNSUTGIFTER_ENDRET, + Hendelsesundertype.FORBIGÅENDE_SYKDOM, + Hendelsesundertype.ETTER_4_SKOLEÅR_UTGIFTENE_OPPHØRT, + Hendelsesundertype.ETTER_4_SKOLEÅR_ENDRET_ARBEIDSTID, + Hendelsesundertype.INNTEKT_OVER_6G, + Hendelsesundertype.KONTANTSTØTTE, + Hendelsesundertype.ØKT_KONTANTSTØTTE, + ), + Hendelsestype.SKOLEPENGER to setOf( + Hendelsesundertype.IKKE_RETT_TIL_OVERGANGSSTØNAD, + Hendelsesundertype.SLUTTET_I_UTDANNING, + ), + + Hendelsestype.VILKÅR_BARN to setOf( + Hendelsesundertype.FULLTIDSPLASS_BARNEHAGE, + Hendelsesundertype.DELTIDSPLASS_BARNEHAGEPLASS, + Hendelsesundertype.BARN_IKKE_BOSATT, + Hendelsesundertype.BARN_IKKE_OPPHOLDSTILLATELSE, + Hendelsesundertype.BARN_FLYTTET_FRA_NORGE, + Hendelsesundertype.BARN_OVER_2_ÅR, + ), + Hendelsestype.VILKÅR_SØKER to setOf( + Hendelsesundertype.DEN_ANDRE_FORELDEREN_IKKE_MEDLEM_FOLKETRYGDEN, + Hendelsesundertype.DEN_ANDRE_FORELDEREN_IKKE_MEDLEM_FOLKETRYGDEN_ELLER_EØS, + Hendelsesundertype.SØKER_IKKE_MEDLEM_FOLKETRYGDEN, + Hendelsesundertype.SØKER_IKKE_MEDLEM_FOLKETRYGDEN_ELLER_EØS, + Hendelsesundertype.BEGGE_FORELDRENE_IKKE_MEDLEM_FOLKETRYGDEN, + Hendelsesundertype.BEGGE_FORELDRENE_IKKE_MEDLEM_FOLKETRYGDEN_ELLER_EØS, + Hendelsesundertype.BARN_BOR_IKKE_HOS_SØKER, + Hendelsesundertype.UTENLANDSOPPHOLD_OVER_3_MÅNEDER, + Hendelsesundertype.SØKER_FLYTTET_FRA_NORGE, + Hendelsesundertype.SØKER_IKKE_BOSATT, + Hendelsesundertype.SØKER_IKKE_OPPHOLDSTILLATELSE, + Hendelsesundertype.SØKER_IKKE_OPPHOLDSTILLATELSE_I_MER_ENN_12_MÅNEDER, + ), + Hendelsestype.BARN_I_FOSTERHJEM_ELLER_INSTITUSJON to setOf( + Hendelsesundertype.BARN_I_FOSTERHJEM, + Hendelsesundertype.BARN_I_INSTITUSJON, + + ), + Hendelsestype.KONTANTSTØTTENS_STØRRELSE to setOf( + Hendelsesundertype.FULLTIDSPLASS_BARNEHAGE, + Hendelsesundertype.DELTIDSPLASS_BARNEHAGEPLASS, + Hendelsesundertype.ØKT_TIMEANTALL_I_BARNEHAGE, + Hendelsesundertype.SATSENDRING, + ), + Hendelsestype.STØTTEPERIODE to setOf( + Hendelsesundertype.BARN_2_ÅR, + ), + Hendelsestype.UTBETALING to setOf( + Hendelsesundertype.DELT_BOSTED_AVTALE_OPPHØRT, + Hendelsesundertype.DOBBELUTBETALING, + ), + Hendelsestype.KONTANTSTØTTE_FOR_ADOPTERTE_BARN to setOf( + Hendelsesundertype.MER_ENN_11_MÅNEDER, + Hendelsesundertype.BARN_STARTET_PÅ_SKOLEN, + ), + Hendelsestype.ANNET_KS to setOf( + Hendelsesundertype.ANNET_FRITEKST, + Hendelsesundertype.BARN_DØD, + Hendelsesundertype.BRUKER_DØD, + ), + + ) + + fun getHendelsesundertyper(hendelsestype: Hendelsestype): Set { + return HIERARKI[hendelsestype] ?: error("Ikke-støttet hendelseType: $hendelsestype") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/ForeldelseMapper.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/ForeldelseMapper.kt new file mode 100644 index 000000000..1f36301f9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/ForeldelseMapper.kt @@ -0,0 +1,57 @@ +package no.nav.familie.tilbake.foreldelse + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.api.dto.ForeldelsesperiodeDto +import no.nav.familie.tilbake.api.dto.VurdertForeldelseDto +import no.nav.familie.tilbake.api.dto.VurdertForeldelsesperiodeDto +import no.nav.familie.tilbake.beregning.KravgrunnlagsberegningService +import no.nav.familie.tilbake.faktaomfeilutbetaling.LogiskPeriode +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesperiode +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import java.math.RoundingMode +import java.util.UUID + +object ForeldelseMapper { + + fun tilRespons( + logiskePerioder: List, + kravgrunnlag431: Kravgrunnlag431, + vurdertForeldelse: VurdertForeldelse?, + ): VurdertForeldelseDto { + val vurdertForeldelsesperioder: List = vurdertForeldelse?.foreldelsesperioder?.map { + VurdertForeldelsesperiodeDto( + periode = it.periode.toDatoperiode(), + feilutbetaltBeløp = KravgrunnlagsberegningService + .beregnFeilutbetaltBeløp( + kravgrunnlag431, + it.periode, + ).setScale(0, RoundingMode.HALF_UP), + begrunnelse = it.begrunnelse, + foreldelsesvurderingstype = it.foreldelsesvurderingstype, + foreldelsesfrist = it.foreldelsesfrist, + oppdagelsesdato = it.oppdagelsesdato, + ) + } ?: logiskePerioder.map { + VurdertForeldelsesperiodeDto( + periode = it.periode.toDatoperiode(), + feilutbetaltBeløp = it.feilutbetaltBeløp.setScale(0, RoundingMode.HALF_UP), + ) + } + + return VurdertForeldelseDto(foreldetPerioder = vurdertForeldelsesperioder) + } + + fun tilDomene(behandlingId: UUID, vurdertForeldetPerioder: List): VurdertForeldelse { + val foreldelsesperioder: Set = vurdertForeldetPerioder.map { + Foreldelsesperiode( + periode = Månedsperiode(it.periode.fom, it.periode.tom), + foreldelsesvurderingstype = it.foreldelsesvurderingstype, + begrunnelse = it.begrunnelse, + foreldelsesfrist = it.foreldelsesfrist, + oppdagelsesdato = it.oppdagelsesdato, + ) + }.toSet() + return VurdertForeldelse(behandlingId = behandlingId, foreldelsesperioder = foreldelsesperioder) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/ForeldelseService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/ForeldelseService.kt new file mode 100644 index 000000000..b227f0a51 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/ForeldelseService.kt @@ -0,0 +1,89 @@ +package no.nav.familie.tilbake.foreldelse + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.api.dto.BehandlingsstegForeldelseDto +import no.nav.familie.tilbake.api.dto.ForeldelsesperiodeDto +import no.nav.familie.tilbake.api.dto.VurdertForeldelseDto +import no.nav.familie.tilbake.beregning.KravgrunnlagsberegningService +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.faktaomfeilutbetaling.LogiskPeriodeUtil +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagUtil +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class ForeldelseService( + private val foreldelseRepository: VurdertForeldelseRepository, + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val vilkårsvurderingRepository: VilkårsvurderingRepository, +) { + + fun hentVurdertForeldelse(behandlingId: UUID): VurdertForeldelseDto { + val vurdertForeldelse: VurdertForeldelse? = foreldelseRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + // Faktaperioder kan ikke deles. Så logisk periode er samme som faktaperiode + val feilutbetaltePerioder = LogiskPeriodeUtil + .utledLogiskPeriode(KravgrunnlagUtil.finnFeilutbetalingPrPeriode(kravgrunnlag)) + + return ForeldelseMapper.tilRespons(feilutbetaltePerioder, kravgrunnlag, vurdertForeldelse) + } + + fun hentAktivVurdertForeldelse(behandlingId: UUID): VurdertForeldelse? { + return foreldelseRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + } + + fun erPeriodeForeldet(behandlingId: UUID, periode: Månedsperiode): Boolean { + return hentAktivVurdertForeldelse(behandlingId)?.foreldelsesperioder + ?.any { periode == it.periode && it.erForeldet() } + ?: false + } + + @Transactional + fun lagreVurdertForeldelse(behandlingId: UUID, behandlingsstegForeldelseDto: BehandlingsstegForeldelseDto) { + // Alle familieytelsene er månedsytelser. Så periode som skal lagres bør være innenfor en måned + KravgrunnlagsberegningService.validatePerioder(behandlingsstegForeldelseDto.foreldetPerioder.map { it.periode }) + val vurdertForeldelse = ForeldelseMapper.tilDomene(behandlingId, behandlingsstegForeldelseDto.foreldetPerioder) + + nullstillVilkårsvurderingForEndringerIForeldelsesperiode(behandlingId, vurdertForeldelse) + deaktiverEksisterendeVurdertForeldelse(behandlingId) + foreldelseRepository.insert(vurdertForeldelse) + } + + @Transactional + fun lagreFastForeldelseForAutomatiskSaksbehandling(behandlingId: UUID) { + val foreldetPerioder = hentVurdertForeldelse(behandlingId).foreldetPerioder.map { + ForeldelsesperiodeDto( + periode = it.periode, + begrunnelse = Constants.AUTOMATISK_SAKSBEHANDLING_BEGUNNLESE, + foreldelsesvurderingstype = Foreldelsesvurderingstype.IKKE_FORELDET, + ) + } + foreldelseRepository.insert(ForeldelseMapper.tilDomene(behandlingId, foreldetPerioder)) + } + + @Transactional + fun deaktiverEksisterendeVurdertForeldelse(behandlingId: UUID) { + foreldelseRepository.findByBehandlingIdAndAktivIsTrue(behandlingId)?.copy(aktiv = false)?.let { + foreldelseRepository.update(it) + } + } + + @Transactional + fun nullstillVilkårsvurderingForEndringerIForeldelsesperiode(behandlingId: UUID, vurdertForeldelse: VurdertForeldelse) { + val eksisterendeVurdertForeldelse = foreldelseRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) ?: return + val eksisterendeVurdertForeldelsesperioder = eksisterendeVurdertForeldelse.foreldelsesperioder.map { it.periode }.toSet() + val nyVurdertForeldelsesperioder = vurdertForeldelse.foreldelsesperioder.map { it.periode }.toSet() + val endringerIPeriode = eksisterendeVurdertForeldelsesperioder.minus(nyVurdertForeldelsesperioder) + + if (endringerIPeriode.isEmpty()) return + // Hvis foreldelsesperioder har endret, må saksbehandler behandle vilkårsvurdering på nytt + vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId)?.copy(aktiv = false)?.let { vilkårsvurdering -> + vilkårsvurderingRepository.update(vilkårsvurdering) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/VurdertForeldelseRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/VurdertForeldelseRepository.kt new file mode 100644 index 000000000..1603a9669 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/VurdertForeldelseRepository.kt @@ -0,0 +1,13 @@ +package no.nav.familie.tilbake.foreldelse + +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository +interface VurdertForeldelseRepository : RepositoryInterface, InsertUpdateRepository { + + fun findByBehandlingIdAndAktivIsTrue(behandlingId: UUID): VurdertForeldelse? +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/domain/Foreldelsesperiode.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/domain/Foreldelsesperiode.kt new file mode 100644 index 000000000..0caaead36 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/domain/Foreldelsesperiode.kt @@ -0,0 +1,36 @@ +package no.nav.familie.tilbake.foreldelse.domain + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import java.time.LocalDate +import java.util.UUID + +data class Foreldelsesperiode( + @Id + val id: UUID = UUID.randomUUID(), + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val periode: Månedsperiode, + val foreldelsesvurderingstype: Foreldelsesvurderingstype, + val begrunnelse: String, + val foreldelsesfrist: LocalDate? = null, + val oppdagelsesdato: LocalDate? = null, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) { + + fun erForeldet(): Boolean { + return Foreldelsesvurderingstype.FORELDET == foreldelsesvurderingstype + } +} + +enum class Foreldelsesvurderingstype(val navn: String) { + IKKE_VURDERT("Perioden er ikke vurdert"), + FORELDET("Perioden er foreldet"), + IKKE_FORELDET("Perioden er ikke foreldet"), + TILLEGGSFRIST("Perioden er ikke foreldet, regel om tilleggsfrist (10 år) benyttes"), +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/domain/VurdertForeldelse.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/domain/VurdertForeldelse.kt new file mode 100644 index 000000000..e97d2ac4a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/foreldelse/domain/VurdertForeldelse.kt @@ -0,0 +1,21 @@ +package no.nav.familie.tilbake.foreldelse.domain + +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import org.springframework.data.relational.core.mapping.MappedCollection +import java.util.UUID + +data class VurdertForeldelse( + @Id + val id: UUID = UUID.randomUUID(), + val behandlingId: UUID, + val aktiv: Boolean = true, + @MappedCollection(idColumn = "vurdert_foreldelse_id") + val foreldelsesperioder: Set = setOf(), + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/forvaltning/ForvaltningService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/forvaltning/ForvaltningService.kt new file mode 100644 index 000000000..08e0c693e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/forvaltning/ForvaltningService.kt @@ -0,0 +1,217 @@ +package no.nav.familie.tilbake.forvaltning + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.api.forvaltning.Forvaltningsinfo +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingService +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultat +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.datavarehus.saksstatistikk.BehandlingTilstandService +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.AnnulerKravgrunnlagService +import no.nav.familie.tilbake.kravgrunnlag.HentKravgrunnlagService +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.KodeAksjon +import no.nav.familie.tilbake.kravgrunnlag.event.EndretKravgrunnlagEventPublisher +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattRepository +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattService +import no.nav.familie.tilbake.micrometer.TellerService +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.math.BigInteger +import java.time.LocalDate +import java.util.UUID + +@Service +class ForvaltningService( + private val behandlingRepository: BehandlingRepository, + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val økonomiXmlMottattRepository: ØkonomiXmlMottattRepository, + private val hentKravgrunnlagService: HentKravgrunnlagService, + private val annulerKravgrunnlagService: AnnulerKravgrunnlagService, + private val økonomiXmlMottattService: ØkonomiXmlMottattService, + private val stegService: StegService, + private val behandlingskontrollService: BehandlingskontrollService, + private val behandlingTilstandService: BehandlingTilstandService, + private val historikkTaskService: HistorikkTaskService, + private val oppgaveTaskService: OppgaveTaskService, + private val tellerService: TellerService, + private val hentFagsystemsbehandlingService: HentFagsystemsbehandlingService, + private val endretKravgrunnlagEventPublisher: EndretKravgrunnlagEventPublisher, +) { + + private val logger: Logger = LoggerFactory.getLogger(this.javaClass) + + @Transactional + fun korrigerKravgrunnlag( + behandlingId: UUID, + kravgrunnlagId: BigInteger, + ) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + sjekkOmBehandlingErAvsluttet(behandling) + val hentetKravgrunnlag = hentKravgrunnlagService.hentKravgrunnlagFraØkonomi( + kravgrunnlagId, + KodeAksjon.HENT_KORRIGERT_KRAVGRUNNLAG, + ) + + val kravgrunnlag = kravgrunnlagRepository.findByEksternKravgrunnlagIdAndAktivIsTrue(kravgrunnlagId) + if (kravgrunnlag != null) { + kravgrunnlagRepository.update(kravgrunnlag.copy(aktiv = false)) + } + hentKravgrunnlagService.lagreHentetKravgrunnlag(behandlingId, hentetKravgrunnlag) + + stegService.håndterSteg(behandlingId) + } + + @Transactional + fun korrigerKravgrunnlag( + behandlingId: UUID, + ) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + sjekkOmBehandlingErAvsluttet(behandling) + + val kravgrunnlagId = + kravgrunnlagRepository.findByBehandlingId(behandling.id).filter { it.aktiv }.first().eksternKravgrunnlagId + val hentetKravgrunnlag = hentKravgrunnlagService.hentKravgrunnlagFraØkonomi( + kravgrunnlagId, + KodeAksjon.HENT_KORRIGERT_KRAVGRUNNLAG, + ) + + val kravgrunnlag = kravgrunnlagRepository.findByEksternKravgrunnlagIdAndAktivIsTrue(kravgrunnlagId) + if (kravgrunnlag != null) { + kravgrunnlagRepository.update(kravgrunnlag.copy(aktiv = false)) + } + hentKravgrunnlagService.lagreHentetKravgrunnlag(behandlingId, hentetKravgrunnlag) + + stegService.håndterSteg(behandlingId) + } + + @Transactional + fun arkiverMottattKravgrunnlag(mottattXmlId: UUID) { + logger.info("Arkiverer mottattXml for Id=$mottattXmlId") + val mottattKravgrunnlag = økonomiXmlMottattService.hentMottattKravgrunnlag(mottattXmlId) + økonomiXmlMottattService.arkiverMottattXml( + mottattKravgrunnlag.melding, + mottattKravgrunnlag.eksternFagsakId, + mottattKravgrunnlag.ytelsestype, + ) + økonomiXmlMottattService.slettMottattXml(mottattXmlId) + } + + @Transactional + fun tvingHenleggBehandling(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + sjekkOmBehandlingErAvsluttet(behandling) + + // oppdaterer behandlingsstegstilstand + behandlingskontrollService.henleggBehandlingssteg(behandlingId) + + // oppdaterer behandlingsresultat og behandling + val behandlingsresultat = Behandlingsresultat(type = Behandlingsresultatstype.HENLAGT_TEKNISK_VEDLIKEHOLD) + behandlingRepository.update( + behandling.copy( + resultater = setOf(behandlingsresultat), + status = Behandlingsstatus.AVSLUTTET, + ansvarligSaksbehandler = ContextService.hentSaksbehandler(), + avsluttetDato = LocalDate.now(), + ), + ) + behandlingTilstandService.opprettSendingAvBehandlingenHenlagt(behandlingId) + + historikkTaskService.lagHistorikkTask( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BEHANDLING_HENLAGT, + aktør = Aktør.SAKSBEHANDLER, + beskrivelse = "", + ) + oppgaveTaskService.ferdigstilleOppgaveTask(behandlingId) + tellerService.tellVedtak(Behandlingsresultatstype.HENLAGT, behandling) + } + + @Transactional + fun flyttBehandlingsstegTilbakeTilFakta(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + sjekkOmBehandlingErAvsluttet(behandling) + + // fjerner eksisterende saksbehandlet data + endretKravgrunnlagEventPublisher.fireEvent(behandlingId) + behandlingskontrollService.behandleStegPåNytt(behandlingId, Behandlingssteg.FAKTA) + + historikkTaskService.lagHistorikkTask( + behandlingId, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_FLYTTET_MED_FORVALTNING, + Aktør.VEDTAKSLØSNING, + ) + } + + fun annulerKravgrunnlag(eksternKravgrunnlagId: BigInteger) { + val økonomiXmlMottatt = økonomiXmlMottattRepository.findByEksternKravgrunnlagId(eksternKravgrunnlagId) + val kravgrunnlag431 = kravgrunnlagRepository.findByEksternKravgrunnlagIdAndAktivIsTrue(eksternKravgrunnlagId) + if (økonomiXmlMottatt == null && kravgrunnlag431 == null) { + throw Feil(message = "Finnes ikke eksternKravgrunnlagId=$eksternKravgrunnlagId") + } + val vedtakId = økonomiXmlMottatt?.vedtakId ?: kravgrunnlag431!!.vedtakId + annulerKravgrunnlagService.annulerKravgrunnlagRequest(eksternKravgrunnlagId, vedtakId) + } + + fun hentForvaltningsinfo(ytelsestype: Ytelsestype, eksternFagsakId: String): List { + val behandling = behandlingRepository.finnÅpenTilbakekrevingsbehandling(ytelsestype, eksternFagsakId) + if (behandling != null && kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandling.id)) { + val kravgrunnlag431 = kravgrunnlagRepository.findByBehandlingId(behandling.id).filter { it.aktiv } + return kravgrunnlag431.map { kravgrunnlag -> + Forvaltningsinfo( + eksternKravgrunnlagId = kravgrunnlag.eksternKravgrunnlagId, + kravgrunnlagId = kravgrunnlag.id, + kravgrunnlagKravstatuskode = kravgrunnlag.kravstatuskode.kode, + mottattXmlId = null, + eksternId = kravgrunnlag.referanse, + opprettetTid = kravgrunnlag.sporbar.opprettetTid, + behandlingId = behandling.id, + ) + } + } + val økonomiXmlMottatt = + økonomiXmlMottattRepository.findByEksternFagsakIdAndYtelsestype(eksternFagsakId, ytelsestype) + if (økonomiXmlMottatt.isEmpty()) { + throw Feil( + "Finnes ikke data i systemet for ytelsestype=$ytelsestype og eksternFagsakId=$eksternFagsakId", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + return økonomiXmlMottatt.map { xml -> + Forvaltningsinfo( + eksternKravgrunnlagId = xml.eksternKravgrunnlagId!!, + kravgrunnlagId = null, + kravgrunnlagKravstatuskode = null, + mottattXmlId = xml.id, + eksternId = xml.referanse, + opprettetTid = xml.sporbar.opprettetTid, + behandlingId = null, + ) + } + } + + private fun sjekkOmBehandlingErAvsluttet(behandling: Behandling) { + if (behandling.erAvsluttet) { + throw Feil( + "Behandling med id=${behandling.id} er allerede ferdig behandlet.", + frontendFeilmelding = "Behandling med id=${behandling.id} er allerede ferdig behandlet.", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/historikkinnslag/HistorikkService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/historikkinnslag/HistorikkService.kt new file mode 100644 index 000000000..215ce133c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/historikkinnslag/HistorikkService.kt @@ -0,0 +1,148 @@ +package no.nav.familie.tilbake.historikkinnslag + +import no.nav.familie.kontrakter.felles.Applikasjon +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.historikkinnslag.OpprettHistorikkinnslagRequest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingRepository +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevsporing +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype.BEHANDLING_HENLAGT +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype.BREVMOTTAKER_ENDRET +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype.BREVMOTTAKER_FJERNET +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype.BREVMOTTAKER_LAGT_TIL +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype.BREV_IKKE_SENDT_DØDSBO_UKJENT_ADRESSE +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype.BREV_IKKE_SENDT_UKJENT_ADRESSE +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype.DISTRIBUSJON_BREV_DØDSBO_FEILET_6_MND +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype.DISTRIBUSJON_BREV_DØDSBO_SUKSESS +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype.ENDRET_ENHET +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype.VEDTAK_FATTET +import no.nav.familie.tilbake.integration.kafka.KafkaProducer +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.util.UUID + +@Service +class HistorikkService( + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, + private val brevsporingRepository: BrevsporingRepository, + private val kafkaProducer: KafkaProducer, +) { + + @Transactional + fun lagHistorikkinnslag( + behandlingId: UUID, + historikkinnslagstype: TilbakekrevingHistorikkinnslagstype, + aktør: Aktør, + opprettetTidspunkt: LocalDateTime, + beskrivelse: String? = null, + brevtype: String? = null, + beslutter: String? = null, + tittel: String? = null, + ) { + val request = lagHistorikkinnslagRequest( + behandlingId, + aktør, + historikkinnslagstype, + opprettetTidspunkt, + beskrivelse, + brevtype, + beslutter, + tittel, + ) + kafkaProducer.sendHistorikkinnslag(behandlingId, request.behandlingId, request) + } + + private fun lagHistorikkinnslagRequest( + behandlingId: UUID, + aktør: Aktør, + historikkinnslagstype: TilbakekrevingHistorikkinnslagstype, + opprettetTidspunkt: LocalDateTime, + beskrivelse: String?, + brevtype: String?, + beslutter: String?, + tittel: String?, + ): OpprettHistorikkinnslagRequest { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val brevdata = hentBrevdata(behandling, brevtype) + + return OpprettHistorikkinnslagRequest( + behandlingId = behandling.eksternBrukId.toString(), + eksternFagsakId = fagsak.eksternFagsakId, + fagsystem = fagsak.fagsystem, + applikasjon = Applikasjon.FAMILIE_TILBAKE, + type = historikkinnslagstype.type, + aktør = aktør, + aktørIdent = hentAktørIdent(behandling, aktør, beslutter), + opprettetTidspunkt = opprettetTidspunkt, + steg = historikkinnslagstype.steg?.name, + tittel = tittel ?: historikkinnslagstype.tittel, + tekst = lagTekst(behandling, historikkinnslagstype, beskrivelse), + journalpostId = brevdata?.journalpostId, + dokumentId = brevdata?.dokumentId, + ) + } + + private fun lagTekst( + behandling: Behandling, + historikkinnslagstype: TilbakekrevingHistorikkinnslagstype, + beskrivelse: String?, + ): String? { + return when (historikkinnslagstype) { + BEHANDLING_PÅ_VENT -> historikkinnslagstype.tekst + beskrivelse + VEDTAK_FATTET -> behandling.sisteResultat?.type?.let { historikkinnslagstype.tekst + it.navn } + BEHANDLING_HENLAGT -> { + when (val resultatstype = requireNotNull(behandling.sisteResultat?.type)) { + Behandlingsresultatstype.HENLAGT_KRAVGRUNNLAG_NULLSTILT, + Behandlingsresultatstype.HENLAGT_TEKNISK_VEDLIKEHOLD, + -> historikkinnslagstype.tekst + resultatstype.navn + else -> historikkinnslagstype.tekst + resultatstype.navn + ", Begrunnelse: " + beskrivelse + } + } + ENDRET_ENHET -> historikkinnslagstype.tekst + behandling.behandlendeEnhet + ", Begrunnelse: " + beskrivelse + BREV_IKKE_SENDT_UKJENT_ADRESSE -> "$beskrivelse er ikke sendt" + BREV_IKKE_SENDT_DØDSBO_UKJENT_ADRESSE -> "$beskrivelse er ikke sendt" + DISTRIBUSJON_BREV_DØDSBO_SUKSESS -> "$beskrivelse er sendt" + DISTRIBUSJON_BREV_DØDSBO_FEILET_6_MND -> "${historikkinnslagstype.tekst}. $beskrivelse er ikke sendt" + BREVMOTTAKER_ENDRET, BREVMOTTAKER_LAGT_TIL, BREVMOTTAKER_FJERNET -> beskrivelse + else -> historikkinnslagstype.tekst + } + } + + private fun hentAktørIdent( + behandling: Behandling, + aktør: Aktør, + beslutter: String?, + ): String { + return when (aktør) { + Aktør.VEDTAKSLØSNING -> Constants.BRUKER_ID_VEDTAKSLØSNINGEN + Aktør.SAKSBEHANDLER -> behandling.ansvarligSaksbehandler + Aktør.BESLUTTER -> + behandling.ansvarligBeslutter + ?: beslutter + ?: error("Beslutter mangler ident for behandling: ${behandling.id}") + } + } + + private fun hentBrevdata( + behandling: Behandling, + brevtypeIString: String?, + ): Brevsporing? { + val brevtype = brevtypeIString?.let { Brevtype.valueOf(it) } + return brevtype?.let { + brevsporingRepository.findFirstByBehandlingIdAndBrevtypeOrderBySporbarOpprettetTidDesc( + behandling.id, + brevtype, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/historikkinnslag/HistorikkTaskService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/historikkinnslag/HistorikkTaskService.kt new file mode 100644 index 000000000..0abb42093 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/historikkinnslag/HistorikkTaskService.kt @@ -0,0 +1,53 @@ +package no.nav.familie.tilbake.historikkinnslag + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.FagsakService +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.util.Properties +import java.util.UUID + +@Service +class HistorikkTaskService( + private val taskService: TaskService, + private val fagsakService: FagsakService, +) { + + fun lagHistorikkTask( + behandlingId: UUID, + historikkinnslagstype: TilbakekrevingHistorikkinnslagstype, + aktør: Aktør, + triggerTid: LocalDateTime? = null, + beskrivelse: String? = null, + brevtype: Brevtype? = null, + beslutter: String? = null, + ) { + val fagsystem = fagsakService.finnFagsystemForBehandlingId(behandlingId) + val properties = Properties().apply { + setProperty("historikkinnslagstype", historikkinnslagstype.name) + setProperty("aktør", aktør.name) + setProperty(PropertyName.FAGSYSTEM, fagsystem.name) + setProperty("opprettetTidspunkt", LocalDateTime.now().toString()) + beslutter?.let { setProperty(PropertyName.BESLUTTER, beslutter) } + beskrivelse?.let { setProperty("beskrivelse", fjernNewlinesFraString(it)) } + brevtype?.let { setProperty("brevtype", brevtype.name) } + } + + val task = Task( + type = LagHistorikkinnslagTask.TYPE, + payload = behandlingId.toString(), + properties = properties, + ) + triggerTid?.let { taskService.save(task.medTriggerTid(triggerTid)) } ?: taskService.save(task) + } + + private fun fjernNewlinesFraString(tekst: String): String { + return tekst + .replace("\r", "") + .replace("\n", " ") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/historikkinnslag/LagHistorikkinnslagTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/historikkinnslag/LagHistorikkinnslagTask.kt new file mode 100644 index 000000000..c9ed9427c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/historikkinnslag/LagHistorikkinnslagTask.kt @@ -0,0 +1,51 @@ +package no.nav.familie.tilbake.historikkinnslag + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.config.PropertyName +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = LagHistorikkinnslagTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Lag historikkinnslag og sender det til kafka", + triggerTidVedFeilISekunder = 60 * 5L, +) +class LagHistorikkinnslagTask(private val historikkService: HistorikkService) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("LagHistorikkinnslagTask prosesserer med id=${task.id} og metadata ${task.metadata}") + + val behandlingId: UUID = UUID.fromString(task.payload) + val historikkinnslagstype = + TilbakekrevingHistorikkinnslagstype.valueOf(task.metadata.getProperty("historikkinnslagstype")) + val aktør = Aktør.valueOf(task.metadata.getProperty("aktør")) + val opprettetTidspunkt = LocalDateTime.parse(task.metadata.getProperty("opprettetTidspunkt")) + val beskrivelse = task.metadata.getProperty("beskrivelse") + val brevtype = task.metadata.getProperty("brevtype") + val beslutter = task.metadata.getProperty(PropertyName.BESLUTTER) + + historikkService.lagHistorikkinnslag( + behandlingId, + historikkinnslagstype, + aktør, + opprettetTidspunkt, + beskrivelse, + brevtype, + beslutter, + ) + } + + companion object { + + const val TYPE = "lagHistorikkinnslag" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/historikkinnslag/TilbakekrevingHistorikkinnslagstype.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/historikkinnslag/TilbakekrevingHistorikkinnslagstype.kt new file mode 100644 index 000000000..787d1a882 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/historikkinnslag/TilbakekrevingHistorikkinnslagstype.kt @@ -0,0 +1,131 @@ +package no.nav.familie.tilbake.historikkinnslag + +import no.nav.familie.kontrakter.felles.historikkinnslag.Historikkinnslagstype +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg + +private const val VARSELBREV_TEKST = "Varselbrev tilbakebetaling" + +enum class TilbakekrevingHistorikkinnslagstype( + val tittel: String, + val tekst: String? = null, + val type: Historikkinnslagstype = Historikkinnslagstype.HENDELSE, + val steg: Behandlingssteg? = null, +) { + + // Hendelse type + BEHANDLING_OPPRETTET(tittel = "Behandling opprettet"), + BEHANDLING_PÅ_VENT(tittel = "Behandling er satt på vent", tekst = "Årsak: "), + BEHANDLING_GJENOPPTATT(tittel = "Behandling gjenopptatt"), + KRAVGRUNNLAG_MOTTATT(tittel = "Kravgrunnlag mottatt"), + KRAVGRUNNLAG_HENT(tittel = "Kravgrunnlag innhentet"), + VERGE_FJERNET(tittel = "Verge fjernet"), + BEHANDLING_SENDT_TIL_BESLUTTER(tittel = "Sendt til beslutter"), + BEHANDLING_SENDT_TILBAKE_TIL_SAKSBEHANDLER(tittel = "Vedtak underkjent"), + VEDTAK_FATTET(tittel = "Vedtak fattet", tekst = "Resultat: "), + BEHANDLING_AVSLUTTET(tittel = "Behandling avsluttet"), + BEHANDLING_HENLAGT(tittel = "Behandling henlagt", tekst = "Årsak: "), + ENDRET_ENHET(tittel = "Endret enhet", tekst = "Ny enhet: "), + BEHANDLING_FLYTTET_MED_FORVALTNING(tittel = "Problem i forvaltning", tekst = "Behandling flyttet tilbake til Fakta"), + BREVMOTTAKER_LAGT_TIL("Brevmottaker er lagt til"), + BREVMOTTAKER_ENDRET("Brevmottaker er endret"), + BREVMOTTAKER_FJERNET("Brevmottaker er fjernet"), + + // Skjermlenke type + VERGE_OPPRETTET( + tittel = "Verge registert", + type = Historikkinnslagstype.SKJERMLENKE, + steg = Behandlingssteg.VERGE, + ), + FAKTA_VURDERT( + tittel = "Fakta vurdert", + type = Historikkinnslagstype.SKJERMLENKE, + steg = Behandlingssteg.FAKTA, + ), + FORELDELSE_VURDERT( + tittel = "Foreldelse vurdert", + type = Historikkinnslagstype.SKJERMLENKE, + steg = Behandlingssteg.FORELDELSE, + ), + VILKÅRSVURDERING_VURDERT( + tittel = "Vilkår vurdert", + type = Historikkinnslagstype.SKJERMLENKE, + steg = Behandlingssteg.VILKÅRSVURDERING, + ), + FORESLÅ_VEDTAK_VURDERT( + tittel = "Vedtak foreslått", + type = Historikkinnslagstype.SKJERMLENKE, + steg = Behandlingssteg.FORESLÅ_VEDTAK, + ), + + // Brev type + VARSELBREV_SENDT( + tittel = "Varselbrev sendt", + tekst = VARSELBREV_TEKST, + type = Historikkinnslagstype.BREV, + ), + VARSELBREV_SENDT_TIL_VERGE( + tittel = "Varselbrev sendt til verge", + tekst = VARSELBREV_TEKST, + type = Historikkinnslagstype.BREV, + ), + KORRIGERT_VARSELBREV_SENDT( + tittel = "Varselbrev sendt", + tekst = VARSELBREV_TEKST, + type = Historikkinnslagstype.BREV, + ), + KORRIGERT_VARSELBREV_SENDT_TIL_VERGE( + tittel = "Varselbrev sendt til verge", + tekst = VARSELBREV_TEKST, + type = Historikkinnslagstype.BREV, + ), + VEDTAKSBREV_SENDT( + tittel = "Vedtaksbrev sendt", + tekst = "Vedtak om tilbakebetaling", + type = Historikkinnslagstype.BREV, + ), + VEDTAKSBREV_SENDT_TIL_VERGE( + tittel = "Vedtaksbrev sendt til verge", + tekst = "Vedtak om tilbakebetaling", + type = Historikkinnslagstype.BREV, + ), + HENLEGGELSESBREV_SENDT( + tittel = "Henleggelsesbrev sendt", + tekst = "Henleggelsesbrev", + type = Historikkinnslagstype.BREV, + ), + HENLEGGELSESBREV_SENDT_TIL_VERGE( + tittel = "Henleggelsesbrev sendt til verge", + tekst = "Henleggelsesbrev", + type = Historikkinnslagstype.BREV, + ), + INNHENT_DOKUMENTASJON_BREV_SENDT( + tittel = "Innhent dokumentasjon sendt", + tekst = "Innhent dokumentasjon", + type = Historikkinnslagstype.BREV, + ), + INNHENT_DOKUMENTASJON_BREV_SENDT_TIL_VERGE( + tittel = "Innhent dokumentasjon sendt til verge", + tekst = "Innhent dokumentasjon", + type = Historikkinnslagstype.BREV, + ), + BREV_IKKE_SENDT_UKJENT_ADRESSE( + tittel = "Bruker har ukjent adresse, brev ikke sendt", + tekst = "", + type = Historikkinnslagstype.BREV, + ), + BREV_IKKE_SENDT_DØDSBO_UKJENT_ADRESSE( + tittel = "Brev ikke distribuert. Ukjent dødsbo", + tekst = "Mottaker har ukjent dødsboadresse, og brevet blir ikke sendt før adressen er satt", + type = Historikkinnslagstype.BREV, + ), + DISTRIBUSJON_BREV_DØDSBO_FEILET_6_MND( + tittel = "Distribusjon av brev til dødsbo feilet", + tekst = "Mottaker har ikke fått dødsboadresse etter 6 måneder", + type = Historikkinnslagstype.HENDELSE, + ), + DISTRIBUSJON_BREV_DØDSBO_SUKSESS( + tittel = "Distribusjon av brev til dødsbo fullført", + tekst = "", + type = Historikkinnslagstype.BREV, + ), +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/familie/IntegrasjonerClient.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/familie/IntegrasjonerClient.kt new file mode 100644 index 000000000..2afa74f64 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/familie/IntegrasjonerClient.kt @@ -0,0 +1,237 @@ +package no.nav.familie.tilbake.integration.familie + +import no.nav.familie.http.client.AbstractPingableRestClient +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Fil +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.dokarkiv.ArkiverDokumentResponse +import no.nav.familie.kontrakter.felles.dokarkiv.v2.ArkiverDokumentRequest +import no.nav.familie.kontrakter.felles.dokdist.DistribuerJournalpostRequest +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstidspunkt +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstype +import no.nav.familie.kontrakter.felles.dokdist.ManuellAdresse +import no.nav.familie.kontrakter.felles.getDataOrThrow +import no.nav.familie.kontrakter.felles.journalpost.Journalpost +import no.nav.familie.kontrakter.felles.journalpost.JournalposterForBrukerRequest +import no.nav.familie.kontrakter.felles.navkontor.NavKontorEnhet +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveRequest +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveResponseDto +import no.nav.familie.kontrakter.felles.oppgave.MappeDto +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import no.nav.familie.kontrakter.felles.oppgave.OppgaveResponse +import no.nav.familie.kontrakter.felles.oppgave.OpprettOppgaveRequest +import no.nav.familie.kontrakter.felles.organisasjon.Organisasjon +import no.nav.familie.kontrakter.felles.saksbehandler.Saksbehandler +import no.nav.familie.kontrakter.felles.tilgangskontroll.Tilgang +import no.nav.familie.tilbake.config.IntegrasjonerConfig +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.cache.annotation.Cacheable +import org.springframework.http.HttpHeaders +import org.springframework.retry.annotation.Backoff +import org.springframework.retry.annotation.Retryable +import org.springframework.stereotype.Component +import org.springframework.web.client.RestOperations +import org.springframework.web.util.UriComponentsBuilder +import java.net.URI + +@Component +class IntegrasjonerClient( + @Qualifier("azure") restOperations: RestOperations, + private val integrasjonerConfig: IntegrasjonerConfig, +) : + AbstractPingableRestClient(restOperations, "familie.integrasjoner") { + + override val pingUri: URI = + UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri).path(IntegrasjonerConfig.PATH_PING).build().toUri() + + private val arkiverUri: URI = UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_ARKIVER) + .build() + .toUri() + + private val distribuerUri: URI = UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_DISTRIBUER) + .build() + .toUri() + + private val sftpUri: URI = UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_SFTP) + .build() + .toUri() + + private val tilgangssjekkUri = UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_TILGANGSSJEKK) + .build() + .toUri() + + private fun hentSaksbehandlerUri(id: String) = UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_SAKSBEHANDLER, id) + .build() + .toUri() + + private val opprettOppgaveUri = UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_OPPGAVE, "opprett") + .build() + .toUri() + + private fun patchOppgaveUri(oppgave: Oppgave) = UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_OPPGAVE, oppgave.id!!.toString(), "oppdater") + .build() + .toUri() + private fun tilordneOppgaveNyEnhetUri(oppgaveId: Long, nyEnhet: String, fjernMappeFraOppgave: Boolean) = UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_OPPGAVE, oppgaveId.toString(), "enhet", nyEnhet) + .queryParam("fjernMappeFraOppgave", fjernMappeFraOppgave) + .build() + .toUri() + + private val finnoppgaverUri = UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_OPPGAVE, "v4") + .build() + .toUri() + + private fun ferdigstillOppgaveUri(oppgaveId: Long) = UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_OPPGAVE, oppgaveId.toString(), "ferdigstill") + .build() + .toUri() + + private fun hentOrganisasjonUri(organisasjonsnummer: String) = + UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_ORGANISASJON, organisasjonsnummer) + .build() + .toUri() + + private fun validerOrganisasjonUri(organisasjonsnummer: String) = + UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_ORGANISASJON, organisasjonsnummer, "valider") + .build() + .toUri() + + private fun hentJournalpostUri() = UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_JOURNALPOST) + .build() + .toUri() + + private fun hentJournalpostHentDokumentUri(journalpostId: String, dokumentInfoId: String) = + UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_HENTDOKUMENT, journalpostId, dokumentInfoId) + .build() + .toUri() + + private fun hentNavkontorUri(enhetsId: String) = UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_NAVKONTOR, enhetsId) + .build() + .toUri() + + private fun finnMapperUri(enhetNr: String): URI = UriComponentsBuilder.fromUri(integrasjonerConfig.integrasjonUri) + .pathSegment(IntegrasjonerConfig.PATH_OPPGAVE, "mappe", "finn", enhetNr) + .build() + .toUri() + + fun arkiver(arkiverDokumentRequest: ArkiverDokumentRequest): ArkiverDokumentResponse { + val response = postForEntity>(arkiverUri, arkiverDokumentRequest) + return response.getDataOrThrow() + } + + fun sendFil(fil: Fil) { + putForEntity(sftpUri, fil) + } + + fun distribuerJournalpost( + journalpostId: String, + fagsystem: Fagsystem, + distribusjonstype: Distribusjonstype, + distribusjonstidspunkt: Distribusjonstidspunkt, + manuellAdresse: ManuellAdresse? = null, + ): String { + val request = DistribuerJournalpostRequest( + journalpostId, + fagsystem, + integrasjonerConfig.applicationName, + distribusjonstype, + distribusjonstidspunkt, + manuellAdresse, + ) + return postForEntity>(distribuerUri, request).getDataOrThrow() + } + + fun hentDokument(dokumentInfoId: String, journalpostId: String): ByteArray { + return getForEntity>(hentJournalpostHentDokumentUri(journalpostId, dokumentInfoId)).getDataOrThrow() + } + + fun hentOrganisasjon(organisasjonsnummer: String): Organisasjon { + return getForEntity>(hentOrganisasjonUri(organisasjonsnummer)).getDataOrThrow() + } + + fun validerOrganisasjon(organisasjonsnummer: String): Boolean { + return try { + getForEntity>(validerOrganisasjonUri(organisasjonsnummer)).data == true + } catch (e: Exception) { + log.error("Organisasjonsnummeret $organisasjonsnummer er ikke gyldig. Feiler med ${e.message}") + false + } + } + + fun hentSaksbehandler(id: String): Saksbehandler { + return getForEntity>(hentSaksbehandlerUri(id)).getDataOrThrow() + } + + fun opprettOppgave(opprettOppgave: OpprettOppgaveRequest): OppgaveResponse { + return postForEntity>(opprettOppgaveUri, opprettOppgave).getDataOrThrow() + } + + fun patchOppgave(patchOppgave: Oppgave): OppgaveResponse { + val uri = patchOppgaveUri(patchOppgave) + return patchForEntity>(uri, patchOppgave).getDataOrThrow() + } + + internal fun tilordneOppgaveNyEnhet(oppgaveId: Long, nyEnhet: String, fjernMappeFraOppgave: Boolean): OppgaveResponse { + val uri = tilordneOppgaveNyEnhetUri(oppgaveId, nyEnhet, fjernMappeFraOppgave) + return patchForEntity>(uri, "", HttpHeaders().medContentTypeJsonUTF8()).getDataOrThrow() + } + + fun finnOppgaver(finnOppgaveRequest: FinnOppgaveRequest): FinnOppgaveResponseDto { + return postForEntity>(finnoppgaverUri, finnOppgaveRequest).getDataOrThrow() + } + + fun ferdigstillOppgave(oppgaveId: Long) { + patchForEntity>(ferdigstillOppgaveUri(oppgaveId), "") + } + + @Cacheable("mappeCache") + fun finnMapper(enhet: String): List { + val respons = getForEntity>>(finnMapperUri(enhet)) + return respons.getDataOrThrow() + } + + fun hentNavkontor(enhetsId: String): NavKontorEnhet { + return getForEntity>(hentNavkontorUri(enhetsId)).getDataOrThrow() + } + + /* + * Sjekker personene i behandlingen er egen ansatt, kode 6 eller kode 7. Og om saksbehandler har rettigheter til å behandle + * slike personer. + */ + @Retryable( + value = [Exception::class], + maxAttempts = 3, + backoff = Backoff(delayExpression = "5000"), + ) + fun sjekkTilgangTilPersoner(personIdenter: List): List { + return postForEntity(tilgangssjekkUri, personIdenter) + } + + fun hentJournalposterForBruker(journalposterForBrukerRequest: JournalposterForBrukerRequest): List { + secureLogger.info( + "henter journalposter for bruker med ident ${journalposterForBrukerRequest.brukerId} " + + "og data $journalposterForBrukerRequest", + ) + + return postForEntity>>(hentJournalpostUri(), journalposterForBrukerRequest).getDataOrThrow() + } +} + +fun HttpHeaders.medContentTypeJsonUTF8(): HttpHeaders { + this.add("Content-Type", "application/json;charset=UTF-8") + this.acceptCharset = listOf(Charsets.UTF_8) + return this +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/kafka/KafkaProducer.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/kafka/KafkaProducer.kt new file mode 100644 index 000000000..3afe4ac65 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/kafka/KafkaProducer.kt @@ -0,0 +1,89 @@ +package no.nav.familie.tilbake.integration.kafka + +import no.nav.familie.kontrakter.felles.historikkinnslag.OpprettHistorikkinnslagRequest +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRequest +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.config.KafkaConfig +import no.nav.familie.tilbake.datavarehus.saksstatistikk.sakshendelse.Behandlingstilstand +import no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak.Vedtaksoppsummering +import org.apache.kafka.clients.producer.ProducerRecord +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.Profile +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.stereotype.Service +import java.util.UUID + +interface KafkaProducer { + + fun sendHistorikkinnslag(behandlingId: UUID, key: String, request: OpprettHistorikkinnslagRequest) + fun sendSaksdata(behandlingId: UUID, request: Behandlingstilstand) + fun sendVedtaksdata(behandlingId: UUID, request: Vedtaksoppsummering) + fun sendHentFagsystemsbehandlingRequest(requestId: UUID, request: HentFagsystemsbehandlingRequest) +} + +@Service +@Profile("!integrasjonstest & !e2e") +class DefaultKafkaProducer(private val kafkaTemplate: KafkaTemplate) : KafkaProducer { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun sendHistorikkinnslag(behandlingId: UUID, key: String, request: OpprettHistorikkinnslagRequest) { + sendKafkamelding(behandlingId, KafkaConfig.HISTORIKK_TOPIC, key, request) + } + + override fun sendSaksdata(behandlingId: UUID, request: Behandlingstilstand) { + sendKafkamelding(behandlingId, KafkaConfig.SAK_TOPIC, request.behandlingUuid.toString(), request) + } + + override fun sendVedtaksdata(behandlingId: UUID, request: Vedtaksoppsummering) { + sendKafkamelding(behandlingId, KafkaConfig.VEDTAK_TOPIC, request.behandlingUuid.toString(), request) + } + + override fun sendHentFagsystemsbehandlingRequest(requestId: UUID, request: HentFagsystemsbehandlingRequest) { + sendKafkamelding(requestId, KafkaConfig.HENT_FAGSYSTEMSBEHANDLING_REQUEST_TOPIC, requestId.toString(), request) + } + + private fun sendKafkamelding(behandlingId: UUID, topic: String, key: String, request: Any) { + val melding = objectMapper.writeValueAsString(request) + val producerRecord = ProducerRecord(topic, key, melding) + kotlin.runCatching { + val callback = kafkaTemplate.send(producerRecord).get() + log.info( + "Melding på topic $topic for $behandlingId med $key er sendt. " + + "Fikk offset ${callback?.recordMetadata?.offset()}", + ) + }.onFailure { + val feilmelding = "Melding på topic $topic kan ikke sendes for $behandlingId med $key. " + + "Feiler med ${it.message}" + log.warn(feilmelding) + throw Feil(message = feilmelding) + } + } +} + +@Service +@Profile("e2e", "integrasjonstest") +class E2EKafkaProducer : KafkaProducer { + + override fun sendHistorikkinnslag(behandlingId: UUID, key: String, request: OpprettHistorikkinnslagRequest) { + logger.info("Skipper sending av historikkinnslag for behandling $behandlingId fordi kafka ikke er enablet") + } + + override fun sendSaksdata(behandlingId: UUID, request: Behandlingstilstand) { + logger.info("Skipper sending av saksstatistikk for behandling $behandlingId fordi kafka ikke er enablet") + } + + override fun sendVedtaksdata(behandlingId: UUID, request: Vedtaksoppsummering) { + logger.info("Skipper sending av vedtaksstatistikk for behandling $behandlingId fordi kafka ikke er enablet") + } + + override fun sendHentFagsystemsbehandlingRequest(requestId: UUID, request: HentFagsystemsbehandlingRequest) { + logger.info("Skipper sending av info-request for fagsystembehandling ${request.eksternId} fordi kafka ikke er enablet") + } + + companion object { + + private val logger = LoggerFactory.getLogger(E2EKafkaProducer::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/PdlClient.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/PdlClient.kt new file mode 100644 index 000000000..9bdfcb16a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/PdlClient.kt @@ -0,0 +1,140 @@ +package no.nav.familie.tilbake.integration.pdl + +import no.nav.familie.http.client.AbstractPingableRestClient +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.config.PdlConfig +import no.nav.familie.tilbake.integration.pdl.internal.PdlAdressebeskyttelsePerson +import no.nav.familie.tilbake.integration.pdl.internal.PdlBolkResponse +import no.nav.familie.tilbake.integration.pdl.internal.PdlHentIdenterResponse +import no.nav.familie.tilbake.integration.pdl.internal.PdlHentPersonResponse +import no.nav.familie.tilbake.integration.pdl.internal.PdlPerson +import no.nav.familie.tilbake.integration.pdl.internal.PdlPersonBolkRequest +import no.nav.familie.tilbake.integration.pdl.internal.PdlPersonBolkRequestVariables +import no.nav.familie.tilbake.integration.pdl.internal.PdlPersonRequest +import no.nav.familie.tilbake.integration.pdl.internal.PdlPersonRequestVariables +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import no.nav.familie.tilbake.integration.pdl.internal.feilsjekkOgReturnerData +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.web.client.RestOperations +import java.net.URI +import java.time.LocalDate + +@Service +class PdlClient( + private val pdlConfig: PdlConfig, + @Qualifier("azureClientCredential") restTemplate: RestOperations, +) : + AbstractPingableRestClient(restTemplate, "pdl.personinfo") { + + private val logger: Logger = LoggerFactory.getLogger(this.javaClass) + + fun hentPersoninfo(ident: String, fagsystem: Fagsystem): Personinfo { + val pdlPersonRequest = PdlPersonRequest( + variables = PdlPersonRequestVariables(ident), + query = PdlConfig.hentEnkelPersonQuery, + ) + val respons: PdlHentPersonResponse = postForEntity( + pdlConfig.pdlUri, + pdlPersonRequest, + httpHeaders(mapTilTema(fagsystem)), + ) + if (respons.harAdvarsel()) { + logger.warn("Advarsel ved henting av personinfo fra PDL. Se securelogs for detaljer.") + secureLogger.warn("Advarsel ved henting av personinfo fra PDL: ${respons.extensions?.warnings}") + } + if (!respons.harFeil()) { + return respons.data.person!!.let { + val aktivtIdent = it.identer.first().identifikasjonsnummer ?: error("Kan ikke hente aktivt ident fra PDL") + Personinfo( + ident = aktivtIdent, + fødselsdato = LocalDate.parse(it.fødsel.first().fødselsdato!!), + navn = it.navn.first().fulltNavn(), + kjønn = it.kjønn.first().kjønn, + dødsdato = it.dødsfall.firstOrNull()?.let { dødsfall -> LocalDate.parse(dødsfall.dødsdato) }, + ) + } + } else { + logger.warn("Respons fra PDL:${objectMapper.writeValueAsString(respons)}") + throw Feil( + message = "Feil ved oppslag på person: ${respons.errorMessages()}", + frontendFeilmelding = "Feil ved oppslag på person $ident: ${respons.errorMessages()}", + httpStatus = HttpStatus.INTERNAL_SERVER_ERROR, + ) + } + } + + fun hentIdenter(personIdent: String, fagsystem: Fagsystem): PdlHentIdenterResponse { + val pdlPersonRequest = PdlPersonRequest( + variables = PdlPersonRequestVariables(personIdent), + query = PdlConfig.hentIdenterQuery, + ) + val response = postForEntity( + pdlConfig.pdlUri, + pdlPersonRequest, + httpHeaders(mapTilTema(fagsystem)), + ) + if (response.harAdvarsel()) { + logger.warn("Advarsel ved henting av personidenter fra PDL. Se securelogs for detaljer.") + secureLogger.warn("Advarsel ved henting av personidenter fra PDL: ${response.extensions?.warnings}") + } + if (!response.harFeil()) return response + throw Feil( + message = "Feil mot pdl: ${response.errorMessages()}", + frontendFeilmelding = "Fant ikke identer for person $personIdent: ${response.errorMessages()}", + httpStatus = HttpStatus.NOT_FOUND, + ) + } + + fun hentAdressebeskyttelseBolk(personIdentList: List, fagsystem: Fagsystem): Map { + val pdlRequest = PdlPersonBolkRequest( + variables = PdlPersonBolkRequestVariables(personIdentList), + query = PdlConfig.hentAdressebeskyttelseBolkQuery, + ) + val pdlResponse = postForEntity>( + pdlConfig.pdlUri, + pdlRequest, + httpHeaders(mapTilTema(fagsystem)), + ) + return feilsjekkOgReturnerData( + pdlResponse = pdlResponse, + ) + } + + private fun httpHeaders(tema: Tema): HttpHeaders { + return HttpHeaders().apply { + add("Tema", tema.name) + add("behandlingsnummer", tema.behandlingsnummer) + } + } + + override val pingUri: URI + get() = pdlConfig.pdlUri + + override fun ping() { + operations.optionsForAllow(pingUri) + } + private fun mapTilTema(fagsystem: Fagsystem): Tema { + return when (fagsystem) { + Fagsystem.EF -> Tema.ENF + Fagsystem.KONT -> Tema.KON + Fagsystem.BA -> Tema.BAR + else -> error("Ugyldig fagsystem=${fagsystem.navn}") + } + } +} + +/** + * TODO : Fjern når versjon 3 av kontrakter blir tatt i bruk. + */ +private enum class Tema(val fagsaksystem: String, val behandlingsnummer: String) { + BAR("BA", "B284"), + ENF("EF", "B288"), + KON("KONT", "B278"), +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlAdressebeskyttelsePerson.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlAdressebeskyttelsePerson.kt new file mode 100644 index 000000000..1c1604122 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlAdressebeskyttelsePerson.kt @@ -0,0 +1,5 @@ +package no.nav.familie.tilbake.integration.pdl.internal + +import no.nav.familie.kontrakter.felles.personopplysning.Adressebeskyttelse + +class PdlAdressebeskyttelsePerson(val adressebeskyttelse: List) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlBaseResponse.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlBaseResponse.kt new file mode 100644 index 000000000..e795e08e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlBaseResponse.kt @@ -0,0 +1,17 @@ +package no.nav.familie.tilbake.integration.pdl.internal + +open class PdlBaseResponse(open val errors: List?, open val extensions: PdlExtensions?) { + + fun harFeil(): Boolean { + return errors != null && errors!!.isNotEmpty() + } + fun harAdvarsel(): Boolean { + return !extensions?.warnings.isNullOrEmpty() + } + fun errorMessages(): String { + return errors?.joinToString { it -> it.message } ?: "" + } +} + +data class PdlExtensions(val warnings: List?) +data class PdlWarning(val details: Any?, val id: String?, val message: String?, val query: String?) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlBolkResponse.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlBolkResponse.kt new file mode 100644 index 000000000..b5ac2615f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlBolkResponse.kt @@ -0,0 +1,15 @@ +package no.nav.familie.tilbake.integration.pdl.internal + +data class PdlBolkResponse(val data: PersonBolk?, val errors: List?, val extensions: PdlExtensions?) { + + fun errorMessages(): String { + return errors?.joinToString { it -> it.message } ?: "" + } + fun harAdvarsel(): Boolean { + return !extensions?.warnings.isNullOrEmpty() + } +} + +data class PersonBolk(val personBolk: List>) + +data class PersonDataBolk(val ident: String, val code: String, val person: T?) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlHentIdenterResponse.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlHentIdenterResponse.kt new file mode 100644 index 000000000..54e4e01da --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlHentIdenterResponse.kt @@ -0,0 +1,17 @@ +package no.nav.familie.tilbake.integration.pdl.internal + +data class PdlHentIdenterResponse( + val data: Data, + override val extensions: PdlExtensions?, + override val errors: List?, +) : + PdlBaseResponse(errors, extensions) + +data class Data(val pdlIdenter: PdlIdenter?) + +data class PdlIdenter(val identer: List) + +data class IdentInformasjon( + val ident: String, + val gruppe: String, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlHentPersonResponse.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlHentPersonResponse.kt new file mode 100644 index 000000000..a3bd136f9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlHentPersonResponse.kt @@ -0,0 +1,63 @@ +package no.nav.familie.tilbake.integration.pdl.internal + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +data class PdlHentPersonResponse( + val data: T, + override val errors: List?, + override val extensions: PdlExtensions?, +) : PdlBaseResponse(errors, extensions) + +data class PdlPerson(val person: PdlPersonData?) + +data class PdlPersonData( + @JsonProperty("foedsel") val fødsel: List, + val navn: List, + @JsonProperty("kjoenn") val kjønn: List, + @JsonProperty("doedsfall") val dødsfall: List = emptyList(), + @JsonProperty("folkeregisteridentifikator") val identer: List, +) + +data class PdlFødselsDato(@JsonProperty("foedselsdato") val fødselsdato: String?) + +data class PdlError( + val message: String, + val extensions: PdlErrorExtensions?, +) + +data class PdlErrorExtensions(val code: String?) + +data class PdlNavn( + val fornavn: String, + val mellomnavn: String? = null, + val etternavn: String, +) { + + fun fulltNavn(): String { + return when (mellomnavn) { + null -> "$fornavn $etternavn" + else -> "$fornavn $mellomnavn $etternavn" + } + } +} + +data class PdlKjønn(@JsonProperty("kjoenn") val kjønn: Kjønn) + +enum class Kjønn { + MANN, + KVINNE, + UKJENT, +} + +data class PdlDødsfall(@JsonProperty("doedsdato") val dødsdato: String? = null) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PdlFolkeregisteridentifikator( + val identifikasjonsnummer: String?, + val status: FolkeregisteridentifikatorStatus, + val type: FolkeregisteridentifikatorType?, +) + +enum class FolkeregisteridentifikatorStatus { I_BRUK, OPPHOERT } +enum class FolkeregisteridentifikatorType { FNR, DNR } diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlPersonRequest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlPersonRequest.kt new file mode 100644 index 000000000..7519fde4a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlPersonRequest.kt @@ -0,0 +1,11 @@ +package no.nav.familie.tilbake.integration.pdl.internal + +data class PdlPersonRequest( + val variables: PdlPersonRequestVariables, + val query: String, +) + +data class PdlPersonBolkRequest( + val variables: PdlPersonBolkRequestVariables, + val query: String, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlPersonRequestVariables.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlPersonRequestVariables.kt new file mode 100644 index 000000000..e468cfeb8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlPersonRequestVariables.kt @@ -0,0 +1,5 @@ +package no.nav.familie.tilbake.integration.pdl.internal + +data class PdlPersonRequestVariables(var ident: String) + +data class PdlPersonBolkRequestVariables(var identer: List) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlUtil.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlUtil.kt new file mode 100644 index 000000000..16edee2f2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/PdlUtil.kt @@ -0,0 +1,26 @@ +package no.nav.familie.tilbake.integration.pdl.internal + +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +val secureLogger: Logger = LoggerFactory.getLogger("secureLogger") +val logger: Logger = LoggerFactory.getLogger("PdlUtil") + +inline fun feilsjekkOgReturnerData(pdlResponse: PdlBolkResponse): Map { + if (pdlResponse.data == null) { + secureLogger.error("Data fra pdl er null ved bolkoppslag av ${T::class} fra PDL: ${pdlResponse.errorMessages()}") + throw Feil("Data er null fra PDL - ${T::class}. Se secure logg for detaljer.") + } + + val feil = pdlResponse.data.personBolk.filter { it.code != "ok" }.associate { it.ident to it.code } + if (feil.isNotEmpty()) { + secureLogger.error("Feil ved henting av ${T::class} fra PDL: $feil") + throw Feil("Feil ved henting av ${T::class} fra PDL. Se secure logg for detaljer.") + } + if (pdlResponse.harAdvarsel()) { + logger.warn("Advarsel ved henting av ${T::class} fra PDL. Se securelogs for detaljer.") + secureLogger.warn("Advarsel ved henting av ${T::class} fra PDL: ${pdlResponse.extensions?.warnings}") + } + return pdlResponse.data.personBolk.associateBy({ it.ident }, { it.person!! }) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/Personinfo.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/Personinfo.kt new file mode 100644 index 000000000..732c594bc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/pdl/internal/Personinfo.kt @@ -0,0 +1,11 @@ +package no.nav.familie.tilbake.integration.pdl.internal + +import java.time.LocalDate + +data class Personinfo( + val ident: String, + val fødselsdato: LocalDate, + val navn: String, + val kjønn: Kjønn = Kjønn.UKJENT, + val dødsdato: LocalDate? = null, +) diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/\303\270konomi/OppdragClient.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/\303\270konomi/OppdragClient.kt" new file mode 100644 index 000000000..bc2387698 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/integration/\303\270konomi/OppdragClient.kt" @@ -0,0 +1,391 @@ +package no.nav.familie.tilbake.integration.økonomi + +import no.nav.familie.http.client.AbstractPingableRestClient +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.getDataOrThrow +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.simulering.FeilutbetalingerFraSimulering +import no.nav.familie.kontrakter.felles.simulering.FeilutbetaltPeriode +import no.nav.familie.kontrakter.felles.simulering.HentFeilutbetalingerFraSimuleringRequest +import no.nav.familie.tilbake.common.exceptionhandler.IntegrasjonException +import no.nav.familie.tilbake.common.exceptionhandler.KravgrunnlagIkkeFunnetFeil +import no.nav.familie.tilbake.common.exceptionhandler.SperretKravgrunnlagFeil +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagMapper +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagUtil +import no.nav.familie.tilbake.kravgrunnlag.domain.Fagområdekode +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsperiode432 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattRepository +import no.nav.okonomi.tilbakekrevingservice.KravgrunnlagAnnulerRequest +import no.nav.okonomi.tilbakekrevingservice.KravgrunnlagAnnulerResponse +import no.nav.okonomi.tilbakekrevingservice.KravgrunnlagHentDetaljRequest +import no.nav.okonomi.tilbakekrevingservice.KravgrunnlagHentDetaljResponse +import no.nav.okonomi.tilbakekrevingservice.TilbakekrevingsvedtakRequest +import no.nav.okonomi.tilbakekrevingservice.TilbakekrevingsvedtakResponse +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagBelopDto +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagDto +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagPeriodeDto +import no.nav.tilbakekreving.typer.v1.MmelDto +import no.nav.tilbakekreving.typer.v1.PeriodeDto +import no.nav.tilbakekreving.typer.v1.TypeGjelderDto +import no.nav.tilbakekreving.typer.v1.TypeKlasseDto +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Service +import org.springframework.web.client.RestOperations +import org.springframework.web.util.UriComponentsBuilder +import java.math.BigDecimal +import java.math.BigInteger +import java.net.URI +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.YearMonth +import java.time.format.DateTimeFormatter +import java.util.UUID + +interface OppdragClient { + + fun iverksettVedtak( + behandlingId: UUID, + tilbakekrevingsvedtakRequest: TilbakekrevingsvedtakRequest, + ): TilbakekrevingsvedtakResponse + + fun hentKravgrunnlag(kravgrunnlagId: BigInteger, hentKravgrunnlagRequest: KravgrunnlagHentDetaljRequest): DetaljertKravgrunnlagDto + + fun annulerKravgrunnlag(eksternKravgrunnlagId: BigInteger, kravgrunnlagAnnulerRequest: KravgrunnlagAnnulerRequest) + + fun hentFeilutbetalingerFraSimulering(request: HentFeilutbetalingerFraSimuleringRequest): FeilutbetalingerFraSimulering +} + +@Service +@Profile("!e2e & !mock-økonomi") +class DefaultOppdragClient( + @Qualifier("azure") restOperations: RestOperations, + @Value("\${FAMILIE_OPPDRAG_URL}") private val familieOppdragUrl: URI, +) : + AbstractPingableRestClient(restOperations, "familie.oppdrag"), OppdragClient { + + private val logger: Logger = LoggerFactory.getLogger(this::class.java) + + override val pingUri: URI = UriComponentsBuilder.fromUri(familieOppdragUrl) + .path(PING_PATH).build().toUri() + + private fun iverksettelseUri(behandlingId: UUID): URI = UriComponentsBuilder.fromUri(familieOppdragUrl) + .pathSegment(IVERKSETTELSE_PATH, behandlingId.toString()).build().toUri() + + private fun hentKravgrunnlagUri(kravgrunnlagId: BigInteger): URI = UriComponentsBuilder.fromUri(familieOppdragUrl) + .pathSegment(HENT_KRAVGRUNNLAG_PATH, kravgrunnlagId.toString()).build().toUri() + + private fun annulerKravgrunnlagUri(kravgrunnlagId: BigInteger): URI = UriComponentsBuilder.fromUri(familieOppdragUrl) + .pathSegment(ANNULER_KRAVGRUNNLAG_PATH, kravgrunnlagId.toString()).build().toUri() + + private val hentFeilutbetalingerFraSimuleringUri: URI = UriComponentsBuilder.fromUri(familieOppdragUrl) + .pathSegment(HENT_FEILUTBETALINGER_PATH).build().toUri() + + override fun iverksettVedtak(behandlingId: UUID, tilbakekrevingsvedtakRequest: TilbakekrevingsvedtakRequest): TilbakekrevingsvedtakResponse { + logger.info("Sender tilbakekrevingsvedtak til økonomi for behandling $behandlingId") + try { + val respons = postForEntity>( + uri = iverksettelseUri(behandlingId), + payload = tilbakekrevingsvedtakRequest, + ) + .getDataOrThrow() + if (!erResponsOk(respons.mmel)) { + logger.error( + "Fikk feil respons fra økonomi ved iverksetting av behandling=$behandlingId." + + "Mottatt respons:${lagRespons(respons.mmel)}", + ) + throw IntegrasjonException( + msg = "Fikk feil respons fra økonomi ved iverksetting av behandling=$behandlingId." + + "Mottatt respons:${lagRespons(respons.mmel)}", + ) + } + logger.info("Mottatt respons: ${lagRespons(respons.mmel)} fra økonomi ved iverksetting av behandling=$behandlingId.") + return respons + } catch (exception: Exception) { + logger.error( + "tilbakekrevingsvedtak kan ikke sende til økonomi for behandling=$behandlingId. " + + "Feiler med ${exception.message}.", + ) + throw IntegrasjonException( + msg = "Noe gikk galt ved iverksetting av behandling=$behandlingId", + throwable = exception, + ) + } + } + + override fun hentKravgrunnlag(kravgrunnlagId: BigInteger, hentKravgrunnlagRequest: KravgrunnlagHentDetaljRequest): DetaljertKravgrunnlagDto { + logger.info("Henter kravgrunnlag fra økonomi for kravgrunnlagId=$kravgrunnlagId") + try { + val respons = postForEntity>( + uri = hentKravgrunnlagUri(kravgrunnlagId), + payload = hentKravgrunnlagRequest, + ) + .getDataOrThrow() + validerHentKravgrunnlagRespons(respons.mmel, kravgrunnlagId) + logger.info("Mottatt respons: ${lagRespons(respons.mmel)} fra økonomi til kravgrunnlagId=$kravgrunnlagId.") + return respons.detaljertkravgrunnlag + } catch (exception: Exception) { + logger.error( + "Kravgrunnlag kan ikke hentes fra økonomi for eksternKravgrunnlagId=$kravgrunnlagId. " + + "Feiler med ${exception.message}", + ) + if (exception.message?.contains("Kravgrunnlag ikke funnet") == true) { + throw KravgrunnlagIkkeFunnetFeil(exception.message!!) + } + throw IntegrasjonException( + msg = "Noe gikk galt ved henting av kravgrunnlag for kravgrunnlagId=$kravgrunnlagId", + throwable = exception, + ) + } + } + + override fun annulerKravgrunnlag( + eksternKravgrunnlagId: BigInteger, + kravgrunnlagAnnulerRequest: KravgrunnlagAnnulerRequest, + ) { + logger.info("Annulerer kravgrunnlag for kravgrunnlagId=$eksternKravgrunnlagId") + try { + val respons = postForEntity>( + uri = annulerKravgrunnlagUri(eksternKravgrunnlagId), + payload = kravgrunnlagAnnulerRequest, + ) + .getDataOrThrow() + if (!erResponsOk(respons.mmel)) { + logger.error( + "Fikk feil respons fra økonomi ved annulering " + + "av kravgrunnlag med eksternKravgrunnlagId=$eksternKravgrunnlagId." + + "Mottatt respons:${lagRespons(respons.mmel)}", + ) + throw IntegrasjonException( + msg = "Fikk feil respons fra økonomi ved annulering " + + "av kravgrunnlag med eksternKravgrunnlagId=$eksternKravgrunnlagId." + + "Mottatt respons:${lagRespons(respons.mmel)}", + ) + } + logger.info("Mottatt respons: ${lagRespons(respons.mmel)} fra økonomi til kravgrunnlagId=$eksternKravgrunnlagId.") + } catch (exception: Exception) { + logger.error( + "Kravgrunnlag kan ikke hentes fra økonomi for eksternKravgrunnlagId=$eksternKravgrunnlagId. " + + "Feiler med ${exception.message}", + ) + throw IntegrasjonException( + "Noe gikk galt ved henting av kravgrunnlag for kravgrunnlagId=$eksternKravgrunnlagId", + exception, + ) + } + } + + override fun hentFeilutbetalingerFraSimulering(request: HentFeilutbetalingerFraSimuleringRequest): FeilutbetalingerFraSimulering { + logger.info( + "Henter feilubetalinger fra simulering for ytelsestype=${request.ytelsestype}, " + + "eksternFagsakId=${request.eksternFagsakId} og eksternId=${request.fagsystemsbehandlingId}", + ) + try { + return postForEntity>( + uri = hentFeilutbetalingerFraSimuleringUri, + payload = request, + ) + .getDataOrThrow() + } catch (exception: Exception) { + logger.error( + "Feilutbetalinger kan ikke hentes fra simulering for for ytelsestype=${request.ytelsestype}, " + + "eksternFagsakId=${request.eksternFagsakId} og eksternId=${request.fagsystemsbehandlingId} " + + "Feiler med ${exception.message}", + ) + throw IntegrasjonException( + msg = "Noe gikk galt ved henting av feilutbetalinger fra simulering", + throwable = exception, + ) + } + } + + private fun validerHentKravgrunnlagRespons(mmelDto: MmelDto, kravgrunnlagId: BigInteger) { + if (!erResponsOk(mmelDto) || erKravgrunnlagIkkeFinnes(mmelDto)) { + logger.error( + "Fikk feil respons:${lagRespons(mmelDto)} fra økonomi ved henting av kravgrunnlag " + + "for kravgrunnlagId=$kravgrunnlagId.", + ) + throw IntegrasjonException( + msg = "Fikk feil respons:${lagRespons(mmelDto)} fra økonomi " + + "ved henting av kravgrunnlag for kravgrunnlagId=$kravgrunnlagId.", + ) + } else if (erKravgrunnlagSperret(mmelDto)) { + logger.warn("Hentet kravgrunnlag for kravgrunnlagId=$kravgrunnlagId er sperret") + throw SperretKravgrunnlagFeil(melding = "Hentet kravgrunnlag for kravgrunnlagId=$kravgrunnlagId er sperret") + } + } + + private fun erResponsOk(mmelDto: MmelDto): Boolean { + return mmelDto.alvorlighetsgrad in setOf("00", "04") + } + + private fun erKravgrunnlagSperret(mmelDto: MmelDto): Boolean { + return KODE_MELDING_SPERRET_KRAVGRUNNLAG == mmelDto.kodeMelding + } + + private fun erKravgrunnlagIkkeFinnes(mmelDto: MmelDto): Boolean { + return KODE_MELDING_KRAVGRUNNLAG_IKKE_FINNES == mmelDto.kodeMelding + } + + private fun lagRespons(mmelDto: MmelDto): String { + return objectMapper.writeValueAsString(mmelDto) + } + + companion object { + + const val KODE_MELDING_SPERRET_KRAVGRUNNLAG = "B420012I" + const val KODE_MELDING_KRAVGRUNNLAG_IKKE_FINNES = "B420010I" + + const val IVERKSETTELSE_PATH = "api/tilbakekreving/iverksett" + const val HENT_KRAVGRUNNLAG_PATH = "api/tilbakekreving/kravgrunnlag" + const val ANNULER_KRAVGRUNNLAG_PATH = "api/tilbakekreving/annuler/kravgrunnlag" + const val HENT_FEILUTBETALINGER_PATH = "api/simulering/feilutbetalinger" + const val PING_PATH = "internal/status/alive" + } +} + +@Service +@Profile("e2e", "mock-økonomi") +class MockOppdragClient( + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val økonomiXmlMottattRepository: ØkonomiXmlMottattRepository, +) : OppdragClient { + + override fun iverksettVedtak( + behandlingId: UUID, + tilbakekrevingsvedtakRequest: TilbakekrevingsvedtakRequest, + ): TilbakekrevingsvedtakResponse { + logger.info("Sender mock iverksettelse respons i e2e-profil") + val mmelDto = MmelDto() + mmelDto.alvorlighetsgrad = "00" + val response = TilbakekrevingsvedtakResponse() + response.mmel = mmelDto + return response + } + + override fun hentKravgrunnlag( + kravgrunnlagId: BigInteger, + hentKravgrunnlagRequest: KravgrunnlagHentDetaljRequest, + ): DetaljertKravgrunnlagDto { + logger.info("Henter kravgrunnlag fra økonomi for kravgrunnlagId=$kravgrunnlagId") + val respons = lagKravgrunnlagRespons(hentKravgrunnlagRequest) + logger.info("Mottatt respons: ${lagRespons(respons.mmel)} fra økonomi til kravgrunnlagId=$kravgrunnlagId.") + return respons.detaljertkravgrunnlag + } + + override fun annulerKravgrunnlag(eksternKravgrunnlagId: BigInteger, kravgrunnlagAnnulerRequest: KravgrunnlagAnnulerRequest) { + logger.info("Kaller mock annulering request i e2e-profil") + } + + override fun hentFeilutbetalingerFraSimulering(request: HentFeilutbetalingerFraSimuleringRequest): FeilutbetalingerFraSimulering { + logger.info( + "Henter feilubetalinger fra simulering i e2e-profil for ytelsestype=${request.ytelsestype}, " + + "eksternFagsakId=${request.eksternFagsakId} og eksternId=${request.fagsystemsbehandlingId}", + ) + val feilutbetaltPeriode = FeilutbetaltPeriode( + fom = YearMonth.now().minusMonths(2).atDay(1), + tom = YearMonth.now().minusMonths(1).atDay(1), + feilutbetaltBeløp = BigDecimal("20000"), + tidligereUtbetaltBeløp = BigDecimal("30000"), + nyttBeløp = BigDecimal("10000"), + ) + return FeilutbetalingerFraSimulering(feilutbetaltePerioder = listOf(feilutbetaltPeriode)) + } + + fun lagKravgrunnlagRespons(request: KravgrunnlagHentDetaljRequest): KravgrunnlagHentDetaljResponse { + val hentKravgrunnlagRequest = request.hentkravgrunnlag + val eksisterendeKravgrunnlag = kravgrunnlagRepository + .findByEksternKravgrunnlagIdAndAktivIsTrue( + hentKravgrunnlagRequest + .kravgrunnlagId, + ) + ?: hentMottattKravgrunnlag(hentKravgrunnlagRequest.kravgrunnlagId) + + val respons = KravgrunnlagHentDetaljResponse() + respons.mmel = lagMmelDto() + + respons.detaljertkravgrunnlag = DetaljertKravgrunnlagDto().apply { + kravgrunnlagId = hentKravgrunnlagRequest.kravgrunnlagId + enhetAnsvarlig = hentKravgrunnlagRequest.enhetAnsvarlig + enhetBehandl = hentKravgrunnlagRequest.enhetAnsvarlig + enhetBosted = hentKravgrunnlagRequest.enhetAnsvarlig + saksbehId = hentKravgrunnlagRequest.saksbehId + kodeFagomraade = Fagområdekode.BA.name + vedtakId = eksisterendeKravgrunnlag?.vedtakId ?: BigInteger.ZERO + kodeStatusKrav = Kravstatuskode.NYTT.kode + fagsystemId = eksisterendeKravgrunnlag?.fagsystemId ?: "0" + datoVedtakFagsystem = eksisterendeKravgrunnlag?.fagsystemVedtaksdato ?: LocalDate.now() + vedtakIdOmgjort = eksisterendeKravgrunnlag?.omgjortVedtakId ?: BigInteger.ZERO + vedtakGjelderId = eksisterendeKravgrunnlag?.gjelderVedtakId ?: "1234" + typeGjelderId = TypeGjelderDto.PERSON + utbetalesTilId = eksisterendeKravgrunnlag?.utbetalesTilId ?: "1234" + typeUtbetId = TypeGjelderDto.PERSON + kontrollfelt = eksisterendeKravgrunnlag?.kontrollfelt ?: LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("YYYY-MM-dd-HH.mm.ss.SSSSSS")) + referanse = eksisterendeKravgrunnlag?.referanse ?: "0" + tilbakekrevingsPeriode.addAll(mapPeriode(eksisterendeKravgrunnlag?.perioder!!)) + } + return respons + } + + private fun lagMmelDto(): MmelDto { + val mmelDto = MmelDto() + mmelDto.alvorlighetsgrad = "00" + mmelDto.kodeMelding = "OK" + return mmelDto + } + + private fun mapPeriode(perioder: Set): List { + return perioder.map { + DetaljertKravgrunnlagPeriodeDto().apply { + periode = PeriodeDto().apply { + fom = it.periode.fomDato + tom = it.periode.tomDato + } + belopSkattMnd = it.månedligSkattebeløp + tilbakekrevingsBelop.addAll(mapBeløp(it.beløp)) + } + } + } + + private fun mapBeløp(beløper: Set): List { + return beløper.map { + DetaljertKravgrunnlagBelopDto().apply { + kodeKlasse = it.klassekode.name + typeKlasse = TypeKlasseDto.fromValue(it.klassetype.name) + belopNy = it.nyttBeløp + belopOpprUtbet = it.opprinneligUtbetalingsbeløp + belopUinnkrevd = it.uinnkrevdBeløp + belopTilbakekreves = it.tilbakekrevesBeløp + skattProsent = it.skatteprosent + } + } + } + + private fun lagRespons(mmelDto: MmelDto): String { + return objectMapper.writeValueAsString(mmelDto) + } + + private fun hentMottattKravgrunnlag(eksternKravgrunnlagId: BigInteger): Kravgrunnlag431? { + val mottattXml = økonomiXmlMottattRepository + .findByEksternKravgrunnlagId(eksternKravgrunnlagId)?.melding + return mottattXml?.let { + KravgrunnlagMapper.tilKravgrunnlag431( + KravgrunnlagUtil.unmarshalKravgrunnlag(it), + UUID.randomUUID(), + ) + } + } + + companion object { + + private val logger: Logger = LoggerFactory.getLogger(this::class.java) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/IverksettelseService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/IverksettelseService.kt new file mode 100644 index 000000000..4900faacb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/IverksettelseService.kt @@ -0,0 +1,183 @@ +package no.nav.familie.tilbake.iverksettvedtak + +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.BehandlingsvedtakService +import no.nav.familie.tilbake.behandling.domain.Iverksettingsstatus +import no.nav.familie.tilbake.beregning.TilbakekrevingsberegningService +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.FeatureToggleConfig +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.integration.økonomi.OppdragClient +import no.nav.familie.tilbake.iverksettvedtak.domain.KodeResultat +import no.nav.familie.tilbake.iverksettvedtak.domain.Tilbakekrevingsbeløp +import no.nav.familie.tilbake.iverksettvedtak.domain.Tilbakekrevingsperiode +import no.nav.familie.tilbake.iverksettvedtak.domain.ØkonomiXmlSendt +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.KodeAksjon +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.okonomi.tilbakekrevingservice.TilbakekrevingsvedtakRequest +import no.nav.tilbakekreving.tilbakekrevingsvedtak.vedtak.v1.TilbakekrevingsbelopDto +import no.nav.tilbakekreving.tilbakekrevingsvedtak.vedtak.v1.TilbakekrevingsperiodeDto +import no.nav.tilbakekreving.tilbakekrevingsvedtak.vedtak.v1.TilbakekrevingsvedtakDto +import no.nav.tilbakekreving.typer.v1.PeriodeDto +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal +import java.time.LocalDate +import java.util.UUID + +@Service +class IverksettelseService( + private val behandlingRepository: BehandlingRepository, + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val økonomiXmlSendtRepository: ØkonomiXmlSendtRepository, + private val tilbakekrevingsvedtakBeregningService: TilbakekrevingsvedtakBeregningService, + private val beregningService: TilbakekrevingsberegningService, + private val behandlingVedtakService: BehandlingsvedtakService, + private val oppdragClient: OppdragClient, + private val featureToggleService: FeatureToggleService, +) { + + private val secureLogger: Logger = LoggerFactory.getLogger("secureLogger") + + @Transactional + fun sendIverksettVedtak(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + + val beregnetPerioder = tilbakekrevingsvedtakBeregningService.beregnVedtaksperioder(behandlingId, kravgrunnlag) + // Validerer beregning slik at rapporterte beløp må være samme i vedtaksbrev og iverksettelse + validerBeløp(behandlingId, beregnetPerioder) + + val request = lagIveksettelseRequest(behandling.ansvarligSaksbehandler, kravgrunnlag, beregnetPerioder) + // lagre request i en separat transaksjon slik at det lagrer selv om tasken feiler + val requestXml = TilbakekrevingsvedtakMarshaller.marshall(behandlingId, request) + secureLogger.info("Sender tilbakekrevingsvedtak til økonomi for behandling=$behandlingId request=$requestXml") + var økonomiXmlSendt = lagreIverksettelsesvedtakRequest(behandlingId, requestXml) + + // Send request til økonomi + val respons = oppdragClient.iverksettVedtak(behandlingId, request) + + // oppdater respons + økonomiXmlSendt = økonomiXmlSendtRepository.findByIdOrThrow(økonomiXmlSendt.id) + økonomiXmlSendtRepository.update(økonomiXmlSendt.copy(kvittering = objectMapper.writeValueAsString(respons.mmel))) + + behandlingVedtakService.oppdaterBehandlingsvedtak(behandlingId, Iverksettingsstatus.IVERKSATT) + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun lagreIverksettelsesvedtakRequest(behandlingId: UUID, requestXml: String): ØkonomiXmlSendt { + return økonomiXmlSendtRepository.insert(ØkonomiXmlSendt(behandlingId = behandlingId, melding = requestXml)) + } + + private fun lagIveksettelseRequest( + ansvarligSaksbehandler: String, + kravgrunnlag: Kravgrunnlag431, + beregnetPerioder: List, + ): TilbakekrevingsvedtakRequest { + val request = TilbakekrevingsvedtakRequest() + val vedtak = TilbakekrevingsvedtakDto() + vedtak.apply { + vedtakId = kravgrunnlag.vedtakId + kodeAksjon = KodeAksjon.FATTE_VEDTAK.kode + kodeHjemmel = "22-15" // fast verdi + datoVedtakFagsystem = kravgrunnlag.fagsystemVedtaksdato ?: LocalDate.now() + enhetAnsvarlig = kravgrunnlag.ansvarligEnhet + kontrollfelt = kravgrunnlag.kontrollfelt + saksbehId = ansvarligSaksbehandler + tilbakekrevingsperiode.addAll(lagVedtaksperiode(beregnetPerioder)) + } + return request.apply { tilbakekrevingsvedtak = vedtak } + } + + private fun lagVedtaksperiode(beregnetPerioder: List): List { + return beregnetPerioder.map { + val tilbakekrevingsperiode = TilbakekrevingsperiodeDto() + tilbakekrevingsperiode.apply { + val periode = PeriodeDto() + periode.fom = it.periode.fom.atDay(1) + periode.tom = it.periode.tom.atEndOfMonth() + this.periode = periode + belopRenter = it.renter + tilbakekrevingsbelop.addAll(lagVedtaksbeløp(it.beløp)) + } + } + } + + private fun lagVedtaksbeløp(beregnetBeløper: List): List { + return beregnetBeløper.map { + val tilbakekrevingsbeløp = TilbakekrevingsbelopDto() + tilbakekrevingsbeløp.apply { + kodeKlasse = it.klassekode.name + belopNy = it.nyttBeløp + belopOpprUtbet = it.utbetaltBeløp + belopTilbakekreves = it.tilbakekrevesBeløp + belopUinnkrevd = it.uinnkrevdBeløp + belopSkatt = it.skattBeløp + if (Klassetype.YTEL == it.klassetype) { + kodeResultat = utledKodeResultat(it) + kodeAarsak = "ANNET" // fast verdi + kodeSkyld = "IKKE_FORDELT" // fast verdi + } + } + } + } + + private fun utledKodeResultat(tilbakekrevingsbeløp: Tilbakekrevingsbeløp): String { + return if (harSattDelvisTilbakekrevingMenKreverTilbakeFulltBeløp(tilbakekrevingsbeløp)) { + secureLogger.warn( + """Fant tilbakekrevingsperiode med delvis tilbakekreving hvor vi krever tilbake hele beløpet. + | Økonomi krever trolig at vi setter full tilbakekreving. + | Dersom kjøringen feiler mot økonomi med feilmelding: Innkrevd beløp = feilutbetalt ved delvis tilbakekreving. + | Vurder å skru på featuretoggle familie-tilbake-overstyr-delvis-tilbakekreving og rekjør. + | Tilbakekrevingsbeløp=$tilbakekrevingsbeløp """.trimMargin(), + ) + if (featureToggleService.isEnabled(FeatureToggleConfig.OVERSTYR_DELVILS_TILBAKEKREVING_TIL_FULL_TILBAKEKREVING)) { + KodeResultat.FULL_TILBAKEKREVING.kode + } else { + KodeResultat.DELVIS_TILBAKEKREVING.kode + } + } else { + tilbakekrevingsbeløp.kodeResultat.kode + } + } + + private fun harSattDelvisTilbakekrevingMenKreverTilbakeFulltBeløp(tilbakekrevingsbeløp: Tilbakekrevingsbeløp) = + tilbakekrevingsbeløp.kodeResultat == KodeResultat.DELVIS_TILBAKEKREVING && tilbakekrevingsbeløp.uinnkrevdBeløp == BigDecimal.ZERO + + fun validerBeløp(behandlingId: UUID, beregnetPerioder: List) { + val beregnetResultat = beregningService.beregn(behandlingId) + val beregnetPerioderForVedtaksbrev = beregnetResultat.beregningsresultatsperioder + + // Beløpene beregnes for vedtaksbrev + val totalTilbakekrevingsbeløpUtenRenter = beregnetPerioderForVedtaksbrev.sumOf { it.tilbakekrevingsbeløpUtenRenter } + val totalRenteBeløp = beregnetPerioderForVedtaksbrev.sumOf { it.rentebeløp } + val totalSkatteBeløp = beregnetPerioderForVedtaksbrev.sumOf { it.skattebeløp } + + // Beløpene beregnes for iverksettelse + val beregnetTotatlTilbakekrevingsbeløpUtenRenter = + beregnetPerioder.sumOf { it.beløp.sumOf { beløp -> beløp.tilbakekrevesBeløp } } + val beregnetTotalRenteBeløp = beregnetPerioder.sumOf { it.renter } + val beregnetSkattBeløp = beregnetPerioder.sumOf { it.beløp.sumOf { beløp -> beløp.skattBeløp } } + + if (totalTilbakekrevingsbeløpUtenRenter != beregnetTotatlTilbakekrevingsbeløpUtenRenter || + totalRenteBeløp != beregnetTotalRenteBeløp || totalSkatteBeløp != beregnetSkattBeløp + ) { + throw Feil( + message = "Det gikk noe feil i beregning under iverksettelse for behandlingId=$behandlingId." + + "Beregnet beløp i vedtaksbrev er " + + "totalTilbakekrevingsbeløpUtenRenter=$totalTilbakekrevingsbeløpUtenRenter," + + "totalRenteBeløp=$totalRenteBeløp, totalSkatteBeløp=$totalSkatteBeløp mens " + + "Beregnet beløp i iverksettelse er " + + "beregnetTotatlTilbakekrevingsbeløpUtenRenter=$beregnetTotatlTilbakekrevingsbeløpUtenRenter," + + "beregnetTotalRenteBeløp=$beregnetTotalRenteBeløp, beregnetSkattBeløp=$beregnetSkattBeløp", + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/TilbakekrevingsvedtakBeregningService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/TilbakekrevingsvedtakBeregningService.kt new file mode 100644 index 000000000..1a5c5adba --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/TilbakekrevingsvedtakBeregningService.kt @@ -0,0 +1,305 @@ +package no.nav.familie.tilbake.iverksettvedtak + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.beregning.TilbakekrevingsberegningService +import no.nav.familie.tilbake.beregning.modell.Beregningsresultatsperiode +import no.nav.familie.tilbake.common.isGreaterThanZero +import no.nav.familie.tilbake.common.isLessThanZero +import no.nav.familie.tilbake.common.isZero +import no.nav.familie.tilbake.iverksettvedtak.domain.KodeResultat +import no.nav.familie.tilbake.iverksettvedtak.domain.Tilbakekrevingsbeløp +import no.nav.familie.tilbake.iverksettvedtak.domain.Tilbakekrevingsperiode +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsperiode432 +import no.nav.familie.tilbake.vilkårsvurdering.domain.AnnenVurdering +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.UUID + +@Service +class TilbakekrevingsvedtakBeregningService(private val tilbakekrevingsberegningService: TilbakekrevingsberegningService) { + + private val logger = LoggerFactory.getLogger(this::class.java) + + fun beregnVedtaksperioder( + behandlingId: UUID, + kravgrunnlag431: Kravgrunnlag431, + ): List { + val beregningsresultat = tilbakekrevingsberegningService.beregn(behandlingId) + + val kravgrunnlagsperioder = kravgrunnlag431.perioder.toList().sortedBy { it.periode.fom } + val beregnetePerioder = beregningsresultat.beregningsresultatsperioder.sortedBy { it.periode.fom } + + // oppretter kravgrunnlagsperioderMedSkatt basert på månedligSkattebeløp + var kravgrunnlagsperioderMedSkatt = kravgrunnlagsperioder.associate { it.periode to it.månedligSkattebeløp } + + return beregnetePerioder.map { beregnetPeriode -> + var perioder = lagTilbakekrevingsperioder(kravgrunnlagsperioder, beregnetPeriode) + + // avrunding tilbakekrevesbeløp og uinnkrevd beløp + perioder = justerAvrunding(beregnetPeriode, perioder) + + // skatt + // oppdaterer kravgrunnlagsperioderMedSkatt med gjenstående skatt + // (ved å trekke totalSkattBeløp fra månedligeSkattebeløp) + kravgrunnlagsperioderMedSkatt = oppdaterGjenståendeSkattetrekk(perioder, kravgrunnlagsperioderMedSkatt) + perioder = justerAvrundingSkatt(beregnetPeriode, perioder, kravgrunnlagsperioderMedSkatt) + + // renter + perioder = beregnRenter(beregnetPeriode, perioder) + justerAvrundingRenter(beregnetPeriode, perioder) + }.flatten() + } + + private fun lagTilbakekrevingsperioder( + kravgrunnlagsperioder: List, + beregnetPeriode: Beregningsresultatsperiode, + ): List { + return kravgrunnlagsperioder.filter { it.periode.snitt(beregnetPeriode.periode) != null } + .map { Tilbakekrevingsperiode(it.periode, BigDecimal.ZERO, lagTilbakekrevingsbeløp(it.beløp, beregnetPeriode)) } + } + + private fun lagTilbakekrevingsbeløp( + kravgrunnlagsbeløp: Set, + beregnetPeriode: Beregningsresultatsperiode, + ): List { + return kravgrunnlagsbeløp.mapNotNull { + when (it.klassetype) { + Klassetype.FEIL -> Tilbakekrevingsbeløp( + klassetype = it.klassetype, + klassekode = it.klassekode, + nyttBeløp = it.nyttBeløp.setScale(0, RoundingMode.HALF_UP), + utbetaltBeløp = BigDecimal.ZERO, + tilbakekrevesBeløp = BigDecimal.ZERO, + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal.ZERO, + kodeResultat = utledKodeResulat(beregnetPeriode), + ) + Klassetype.YTEL -> { + val beregnetTilbakrevesbeløp = beregnTilbakekrevesbeløp(beregnetPeriode, it) + val opprinneligTilbakekrevesbeløp: BigDecimal = it.tilbakekrevesBeløp + + Tilbakekrevingsbeløp( + klassetype = it.klassetype, + klassekode = it.klassekode, + nyttBeløp = it.nyttBeløp.setScale(0, RoundingMode.HALF_UP), + utbetaltBeløp = it.opprinneligUtbetalingsbeløp.setScale(0, RoundingMode.HALF_UP), + tilbakekrevesBeløp = beregnetTilbakrevesbeløp, + uinnkrevdBeløp = opprinneligTilbakekrevesbeløp + .subtract(beregnetTilbakrevesbeløp).setScale(0, RoundingMode.HALF_UP), + skattBeløp = beregnSkattBeløp( + beregnetTilbakrevesbeløp, + it.skatteprosent, + ), + kodeResultat = utledKodeResulat(beregnetPeriode), + ) + } + + else -> null + } + } + } + + private fun beregnSkattBeløp( + bruttoTilbakekrevesBeløp: BigDecimal, + skattProsent: BigDecimal, + ): BigDecimal { + return bruttoTilbakekrevesBeløp.multiply(skattProsent).divide(BigDecimal(100), 0, RoundingMode.DOWN) + } + + private fun utledKodeResulat(beregnetPeriode: Beregningsresultatsperiode): KodeResultat { + return when { + beregnetPeriode.vurdering == AnnenVurdering.FORELDET -> { + KodeResultat.FORELDET + } + beregnetPeriode.tilbakekrevingsbeløpUtenRenter.isZero() -> { + KodeResultat.INGEN_TILBAKEKREVING + } + beregnetPeriode.feilutbetaltBeløp == beregnetPeriode.tilbakekrevingsbeløpUtenRenter -> { + KodeResultat.FULL_TILBAKEKREVING + } + else -> KodeResultat.DELVIS_TILBAKEKREVING + } + } + + private fun justerAvrunding( + beregnetPeriode: Beregningsresultatsperiode, + perioder: List, + ): List { + val tilbakekrevingsbeløpUtenRenter = beregnetPeriode.tilbakekrevingsbeløpUtenRenter + val totalTilbakekrevingsbeløp = beregnTotalTilbakekrevesbeløp(perioder) + val differanse = totalTilbakekrevingsbeløp.subtract(tilbakekrevingsbeløpUtenRenter) + + return when { + differanse.isGreaterThanZero() -> justerNed(differanse, perioder) + differanse.isLessThanZero() -> justerOpp(differanse, perioder) + else -> perioder + } + } + + private fun justerNed(differanse: BigDecimal, perioder: List): List { + var diff = differanse + return perioder.map { periode -> + var justertebeløp = periode.beløp + while (diff.isGreaterThanZero()) { + justertebeløp = justertebeløp.map { beløp -> + if (Klassetype.FEIL == beløp.klassetype) { + beløp + } else { + diff = diff.subtract(BigDecimal.ONE) + beløp.copy( + tilbakekrevesBeløp = beløp.tilbakekrevesBeløp.subtract(BigDecimal.ONE), + uinnkrevdBeløp = beløp.uinnkrevdBeløp.add(BigDecimal.ONE), + ) + } + } + } + periode.copy(beløp = justertebeløp) + } + } + + private fun justerOpp(differanse: BigDecimal, perioder: List): List { + var diff = differanse + return perioder.map { periode -> + var justertebeløp = periode.beløp + while (diff.isLessThanZero()) { + justertebeløp = justertebeløp.map { beløp -> + if (Klassetype.FEIL == beløp.klassetype) { + beløp + } else { + diff = diff.add(BigDecimal.ONE) + beløp.copy( + tilbakekrevesBeløp = beløp.tilbakekrevesBeløp.add(BigDecimal.ONE), + uinnkrevdBeløp = beløp.uinnkrevdBeløp.subtract(BigDecimal.ONE), + ) + } + } + } + periode.copy(beløp = justertebeløp) + } + } + + private fun oppdaterGjenståendeSkattetrekk( + perioder: List, + kravgrunnlagsperioderMedSkatt: Map, + ): Map { + val grunnlagsperioderMedSkatt = kravgrunnlagsperioderMedSkatt.toMutableMap() + perioder.forEach { + val skattBeløp = it.beløp + .filter { beløp -> Klassetype.YTEL == beløp.klassetype } + .sumOf { ytelsebeløp -> ytelsebeløp.skattBeløp } + val gjenståendeSkattBeløp = kravgrunnlagsperioderMedSkatt.getNotNull(it.periode).subtract(skattBeløp) + grunnlagsperioderMedSkatt[it.periode] = gjenståendeSkattBeløp + } + return grunnlagsperioderMedSkatt + } + + private fun justerAvrundingSkatt( + beregnetPeriode: Beregningsresultatsperiode, + perioder: List, + kravgrunnlagsperioderMedSkatt: Map, + ): List { + val grunnlagsperioderMedSkatt = kravgrunnlagsperioderMedSkatt.toMutableMap() + val totalSkattBeløp = perioder.sumOf { it.beløp.sumOf { beløp -> beløp.skattBeløp } } + val beregnetSkattBeløp = beregnetPeriode.skattebeløp + var differanse = totalSkattBeløp.subtract(beregnetSkattBeløp) + + return perioder.map { + val periode = it.periode + var justertebeløp = it.beløp + justertebeløp = justertebeløp.map { beløp -> + if (Klassetype.FEIL == beløp.klassetype) { + beløp + } else { + val justerSkattOpp = differanse.isLessThanZero() && + grunnlagsperioderMedSkatt.getNotNull(periode) >= BigDecimal.ONE + val justerSkattNed = differanse.isGreaterThanZero() && + beløp.skattBeløp.compareTo(BigDecimal.ONE) >= 1 + if (justerSkattOpp || justerSkattNed) { + val justering = BigDecimal(differanse.signum()).negate() + grunnlagsperioderMedSkatt[periode] = grunnlagsperioderMedSkatt.getNotNull(periode).add(justering) + differanse = differanse.add(justering) + beløp.copy(skattBeløp = beløp.skattBeløp.add(justering)) + } else { + beløp + } + } + } + it.copy(beløp = justertebeløp) + } + } + + private fun beregnTilbakekrevesbeløp( + beregnetPeriode: Beregningsresultatsperiode, + kravgrunnlagsbeløp: Kravgrunnlagsbeløp433, + ): BigDecimal { + return kravgrunnlagsbeløp.tilbakekrevesBeløp.multiply(beregnetPeriode.tilbakekrevingsbeløpUtenRenter) + .divide(beregnetPeriode.feilutbetaltBeløp, 0, RoundingMode.HALF_UP) + } + + private fun beregnTotalTilbakekrevesbeløp(perioder: List): BigDecimal { + return perioder.sumOf { it.beløp.sumOf { beløp -> beløp.tilbakekrevesBeløp } } + } + + private fun summerTilbakekrevesbeløp(periode: Tilbakekrevingsperiode): BigDecimal { + return periode.beløp.sumOf { it.tilbakekrevesBeløp } + } + + private fun Map.getNotNull(key: Månedsperiode) = requireNotNull(this[key]) + + private fun beregnRenter( + beregnetPeriode: Beregningsresultatsperiode, + perioder: List, + ): List { + return perioder.map { + val tilbakekrevesbeløp = summerTilbakekrevesbeløp(it) + var renteBeløp = BigDecimal.ZERO + if (beregnetPeriode.tilbakekrevingsbeløpUtenRenter != BigDecimal.ZERO) { + renteBeløp = beregnetPeriode.rentebeløp.multiply(tilbakekrevesbeløp) + .divide(beregnetPeriode.tilbakekrevingsbeløpUtenRenter, 0, RoundingMode.HALF_UP) + } + it.copy(renter = renteBeløp) + } + } + + private fun justerAvrundingRenter( + beregnetPeriode: Beregningsresultatsperiode, + perioder: List, + ): List { + val totalBeregnetRenteBeløp = beregnetPeriode.rentebeløp + val totalBeregnetRenterIIverksettelse = perioder.sumOf { it.renter } + logger.info( + "Total beregnet renteBeløp som sendes i vedtaksbrev er $totalBeregnetRenteBeløp " + + "mens total beregnet renteBeløp under iverksettelse er $totalBeregnetRenterIIverksettelse ", + ) + var differanse = totalBeregnetRenteBeløp.minus(totalBeregnetRenterIIverksettelse) + + return when { + differanse.isGreaterThanZero() -> { + perioder.map { periode -> + var renteBeløp = periode.renter + while (differanse.isGreaterThanZero()) { + renteBeløp = renteBeløp.add(BigDecimal.ONE) + differanse = differanse.minus(BigDecimal.ONE) + } + periode.copy(renter = renteBeløp) + } + } + differanse.isLessThanZero() -> { + perioder.map { periode -> + var renteBeløp = periode.renter + while (differanse.isLessThanZero()) { + renteBeløp = renteBeløp.minus(BigDecimal.ONE) + differanse = differanse.plus(BigDecimal.ONE) + } + periode.copy(renter = renteBeløp) + } + } + else -> perioder + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/TilbakekrevingsvedtakMarshaller.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/TilbakekrevingsvedtakMarshaller.kt new file mode 100644 index 000000000..d1725d439 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/TilbakekrevingsvedtakMarshaller.kt @@ -0,0 +1,39 @@ +package no.nav.familie.tilbake.iverksettvedtak + +import jakarta.xml.bind.JAXBContext +import jakarta.xml.bind.JAXBException +import jakarta.xml.bind.Marshaller +import jakarta.xml.bind.Unmarshaller +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.okonomi.tilbakekrevingservice.TilbakekrevingsvedtakRequest +import java.io.StringReader +import java.io.StringWriter +import java.util.UUID + +object TilbakekrevingsvedtakMarshaller { + + private val context = JAXBContext.newInstance(TilbakekrevingsvedtakRequest::class.java) + + fun marshall(behandlingId: UUID, request: TilbakekrevingsvedtakRequest): String { + return try { + val marshaller = context.createMarshaller() + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, false) + val stringWriter = StringWriter() + marshaller.marshal(request, stringWriter) + + stringWriter.toString() + } catch (e: JAXBException) { + throw Feil("Kunne ikke marshalle TilbakekrevingsvedtakRequest for behandlingId=$behandlingId", e) + } + } + + fun unmarshall(xml: String, behandlingId: UUID, xmlId: UUID): TilbakekrevingsvedtakRequest { + return try { + val unmarshaller: Unmarshaller = context.createUnmarshaller() + + (unmarshaller.unmarshal(StringReader(xml)) as TilbakekrevingsvedtakRequest) + } catch (e: JAXBException) { + throw Feil("Kunne ikke unmarshalle requestXml=$xml med id=$xmlId for behandling=$behandlingId", e) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/domain/Tilbakekrevingsperiode.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/domain/Tilbakekrevingsperiode.kt new file mode 100644 index 000000000..903196b57 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/domain/Tilbakekrevingsperiode.kt @@ -0,0 +1,34 @@ +package no.nav.familie.tilbake.iverksettvedtak.domain + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassekode +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import java.math.BigDecimal + +/* Brukes bare for iverksettelse */ + +data class Tilbakekrevingsperiode( + val periode: Månedsperiode, + val renter: BigDecimal = BigDecimal.ZERO, + val beløp: List = listOf(), +) + +data class Tilbakekrevingsbeløp( + val klassetype: Klassetype, + val klassekode: Klassekode, + val nyttBeløp: BigDecimal, + val utbetaltBeløp: BigDecimal, + val tilbakekrevesBeløp: BigDecimal, + val uinnkrevdBeløp: BigDecimal, + val skattBeløp: BigDecimal, + val kodeResultat: KodeResultat, +) + +enum class KodeResultat(val kode: String) { + + FORELDET("FORELDET"), + FEILREGISTRERT("FEILREGISTRERT"), + INGEN_TILBAKEKREVING("INGEN_TILBAKEKREV"), + DELVIS_TILBAKEKREVING("DELVIS_TILBAKEKREV"), + FULL_TILBAKEKREVING("FULL_TILBAKEKREV"), +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/domain/\303\230konomiXmlSendt.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/domain/\303\230konomiXmlSendt.kt" new file mode 100644 index 000000000..6a417556c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/domain/\303\230konomiXmlSendt.kt" @@ -0,0 +1,21 @@ +package no.nav.familie.tilbake.iverksettvedtak.domain + +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import org.springframework.data.relational.core.mapping.Table +import java.util.UUID + +@Table("okonomi_xml_sendt") +data class ØkonomiXmlSendt( + @Id + val id: UUID = UUID.randomUUID(), + val behandlingId: UUID, + val melding: String, + val kvittering: String? = null, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/task/AvsluttBehandlingTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/task/AvsluttBehandlingTask.kt new file mode 100644 index 000000000..79bd94443 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/task/AvsluttBehandlingTask.kt @@ -0,0 +1,75 @@ +package no.nav.familie.tilbake.iverksettvedtak.task + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = AvsluttBehandlingTask.TYPE, + beskrivelse = "Avslutter behandling", + triggerTidVedFeilISekunder = 60 * 5L, +) +class AvsluttBehandlingTask( + private val behandlingRepository: BehandlingRepository, + private val behandlingskontrollService: BehandlingskontrollService, + private val historikkTaskService: HistorikkTaskService, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + @Transactional + override fun doTask(task: Task) { + log.info("AvsluttBehandlingTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val behandlingId = UUID.fromString(task.payload) + + var behandling = behandlingRepository.findByIdOrThrow(behandlingId) + if (!behandling.erUnderIverksettelse) { + throw Feil(message = "Behandling med id=$behandlingId kan ikke avsluttes") + } + + behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update( + behandling.copy( + status = Behandlingsstatus.AVSLUTTET, + avsluttetDato = LocalDate.now(), + ), + ) + + behandlingskontrollService + .oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + behandlingssteg = Behandlingssteg.AVSLUTTET, + behandlingsstegstatus = Behandlingsstegstatus.UTFØRT, + ), + ) + + historikkTaskService.lagHistorikkTask( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BEHANDLING_AVSLUTTET, + aktør = Aktør.VEDTAKSLØSNING, + ) + } + + companion object { + + const val TYPE = "avsluttBehandling" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/task/Send\303\230konomiTilbakekrevingsvedtakTask.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/task/Send\303\230konomiTilbakekrevingsvedtakTask.kt" new file mode 100644 index 000000000..fa32fc263 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/task/Send\303\230konomiTilbakekrevingsvedtakTask.kt" @@ -0,0 +1,64 @@ +package no.nav.familie.tilbake.iverksettvedtak.task + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.dokumentbestilling.vedtak.SendVedtaksbrevTask +import no.nav.familie.tilbake.iverksettvedtak.IverksettelseService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = SendØkonomiTilbakekrevingsvedtakTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Sender tilbakekrevingsvedtak til økonomi", + triggerTidVedFeilISekunder = 300L, +) +class SendØkonomiTilbakekrevingsvedtakTask( + private val iverksettelseService: IverksettelseService, + private val taskService: TaskService, + private val behandlingskontrollService: BehandlingskontrollService, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("SendØkonomiTilbakekrevingsvedtakTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val behandlingId = UUID.fromString(task.payload) + iverksettelseService.sendIverksettVedtak(behandlingId) + + behandlingskontrollService + .oppdaterBehandlingsstegStatus( + behandlingId, + Behandlingsstegsinfo( + behandlingssteg = Behandlingssteg.IVERKSETT_VEDTAK, + behandlingsstegstatus = Behandlingsstegstatus.UTFØRT, + ), + ) + behandlingskontrollService.fortsettBehandling(behandlingId) + } + + @Transactional + override fun onCompletion(task: Task) { + taskService.save( + Task( + type = SendVedtaksbrevTask.TYPE, + payload = task.payload, + properties = task.metadata, + ), + ) + } + + companion object { + + const val TYPE = "sendØkonomiVedtak" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/\303\230konomiXmlSendtRepository.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/\303\230konomiXmlSendtRepository.kt" new file mode 100644 index 000000000..ec072002f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/iverksettvedtak/\303\230konomiXmlSendtRepository.kt" @@ -0,0 +1,21 @@ +package no.nav.familie.tilbake.iverksettvedtak + +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import no.nav.familie.tilbake.iverksettvedtak.domain.ØkonomiXmlSendt +import org.springframework.data.jdbc.repository.query.Query +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.util.UUID + +@Repository +@Transactional +interface ØkonomiXmlSendtRepository : RepositoryInterface<ØkonomiXmlSendt, UUID>, InsertUpdateRepository<ØkonomiXmlSendt> { + + fun findByBehandlingId(behandlingId: UUID): ØkonomiXmlSendt? + + // language=PostgreSQL + @Query("SELECT * FROM okonomi_xml_sendt WHERE opprettet_tid::DATE = :opprettetTid ") + fun findByOpprettetPåDato(opprettetTid: LocalDate): Collection<ØkonomiXmlSendt> +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/AnnulerKravgrunnlagService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/AnnulerKravgrunnlagService.kt new file mode 100644 index 000000000..0e0469117 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/AnnulerKravgrunnlagService.kt @@ -0,0 +1,27 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import no.nav.familie.tilbake.integration.økonomi.OppdragClient +import no.nav.familie.tilbake.kravgrunnlag.domain.KodeAksjon +import no.nav.okonomi.tilbakekrevingservice.KravgrunnlagAnnulerRequest +import no.nav.tilbakekreving.kravgrunnlag.annuller.v1.AnnullerKravgrunnlagDto +import org.springframework.stereotype.Service +import java.math.BigInteger + +@Service +class AnnulerKravgrunnlagService(private val oppdragClient: OppdragClient) { + + fun annulerKravgrunnlagRequest( + eksternKravgrunnlagId: BigInteger, + vedtakId: BigInteger, + ) { + val annullerKravgrunnlagDto = AnnullerKravgrunnlagDto() + annullerKravgrunnlagDto.kodeAksjon = KodeAksjon.ANNULERE_GRUNNLAG.kode // fast verdi + annullerKravgrunnlagDto.vedtakId = vedtakId + annullerKravgrunnlagDto.saksbehId = "K231B433" // fast verdi + + val annulerRequest = KravgrunnlagAnnulerRequest() + annulerRequest.annullerkravgrunnlag = annullerKravgrunnlagDto + + oppdragClient.annulerKravgrunnlag(eksternKravgrunnlagId, annulerRequest) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/HentKravgrunnlagService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/HentKravgrunnlagService.kt new file mode 100644 index 000000000..d50c5f348 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/HentKravgrunnlagService.kt @@ -0,0 +1,74 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.tilbake.historikkinnslag.HistorikkService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.integration.økonomi.OppdragClient +import no.nav.familie.tilbake.kravgrunnlag.domain.KodeAksjon +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.okonomi.tilbakekrevingservice.KravgrunnlagHentDetaljRequest +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagDto +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.HentKravgrunnlagDetaljDto +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.math.BigInteger +import java.time.LocalDateTime +import java.util.UUID + +@Service +class HentKravgrunnlagService( + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val oppdragClient: OppdragClient, + private val historikkService: HistorikkService, +) { + + private val logger: Logger = LoggerFactory.getLogger(this.javaClass) + + fun hentKravgrunnlagFraØkonomi(kravgrunnlagId: BigInteger, kodeAksjon: KodeAksjon): DetaljertKravgrunnlagDto { + logger.info("Henter kravgrunnlag for kravgrunnlagId=$kravgrunnlagId for kodeAksjon=$kodeAksjon") + return oppdragClient.hentKravgrunnlag(kravgrunnlagId, lagRequest(kravgrunnlagId, kodeAksjon)) + } + + fun hentTilbakekrevingskravgrunnlag(behandlingId: UUID): Kravgrunnlag431 { + return kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + } + + @Transactional + fun lagreHentetKravgrunnlag(behandlingId: UUID, kravgrunnlag: DetaljertKravgrunnlagDto) { + logger.info("Lagrer hentet kravgrunnlag for behandling $behandlingId") + val kravgrunnlag431 = KravgrunnlagMapper.tilKravgrunnlag431(kravgrunnlag, behandlingId) + kravgrunnlagRepository.insert(kravgrunnlag431) + } + + @Transactional + fun opprettHistorikkinnslag(behandlingId: UUID) { + logger.info( + "Oppretter historikkinnslag ${TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_HENT} " + + "for behandling $behandlingId", + ) + historikkService.lagHistorikkinnslag( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_HENT, + aktør = Aktør.VEDTAKSLØSNING, + opprettetTidspunkt = LocalDateTime.now(), + ) + } + + private fun lagRequest( + kravgrunnlagId: BigInteger, + kodeAksjon: KodeAksjon, + ): KravgrunnlagHentDetaljRequest { + val hentkravgrunnlag = HentKravgrunnlagDetaljDto() + hentkravgrunnlag.kravgrunnlagId = kravgrunnlagId + hentkravgrunnlag.kodeAksjon = kodeAksjon.kode + hentkravgrunnlag.enhetAnsvarlig = "8020" // fast verdi + hentkravgrunnlag.saksbehId = "K231B433" // fast verdi + + val request = KravgrunnlagHentDetaljRequest() + request.hentkravgrunnlag = hentkravgrunnlag + + return request + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagMapper.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagMapper.kt new file mode 100644 index 000000000..983c3c32d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagMapper.kt @@ -0,0 +1,72 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.kravgrunnlag.domain.Fagområdekode +import no.nav.familie.tilbake.kravgrunnlag.domain.GjelderType +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassekode +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsperiode432 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagBelopDto +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagDto +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagPeriodeDto +import java.util.UUID + +object KravgrunnlagMapper { + + fun tilKravgrunnlag431(kravgrunnlag: DetaljertKravgrunnlagDto, behandlingId: UUID): Kravgrunnlag431 { + return Kravgrunnlag431( + behandlingId = behandlingId, + vedtakId = kravgrunnlag.vedtakId, + omgjortVedtakId = kravgrunnlag.vedtakIdOmgjort, + kravstatuskode = Kravstatuskode.fraKode(kravgrunnlag.kodeStatusKrav), + fagområdekode = Fagområdekode.fraKode(kravgrunnlag.kodeFagomraade), + fagsystemId = kravgrunnlag.fagsystemId, + fagsystemVedtaksdato = kravgrunnlag.datoVedtakFagsystem, + gjelderVedtakId = kravgrunnlag.vedtakGjelderId, + gjelderType = GjelderType.fraKode(kravgrunnlag.typeGjelderId.value()), + utbetalesTilId = kravgrunnlag.utbetalesTilId, + utbetIdType = GjelderType.fraKode(kravgrunnlag.typeUtbetId.value()), + hjemmelkode = kravgrunnlag.kodeHjemmel, + beregnesRenter = "J" == kravgrunnlag.renterBeregnes?.value(), + ansvarligEnhet = kravgrunnlag.enhetAnsvarlig, + behandlingsenhet = kravgrunnlag.enhetBehandl, + bostedsenhet = kravgrunnlag.enhetBosted, + kontrollfelt = kravgrunnlag.kontrollfelt, + saksbehandlerId = kravgrunnlag.saksbehId, + referanse = kravgrunnlag.referanse, + eksternKravgrunnlagId = kravgrunnlag.kravgrunnlagId, + perioder = tilKravgrunnlagsperiode(kravgrunnlag.tilbakekrevingsPeriode), + ) + } + + private fun tilKravgrunnlagsperiode(perioder: List): Set { + return perioder.map { + Kravgrunnlagsperiode432( + periode = Månedsperiode(it.periode.fom, it.periode.tom), + månedligSkattebeløp = it.belopSkattMnd, + beløp = tilKravgrunnlagsbeløp(it.tilbakekrevingsBelop), + ) + }.toSet() + } + + private fun tilKravgrunnlagsbeløp(beløpPosteringer: List): Set { + return beløpPosteringer.map { + val klassetype = Klassetype.fraKode(it.typeKlasse.value()) + Kravgrunnlagsbeløp433( + klassetype = klassetype, + klassekode = Klassekode.fraKode(it.kodeKlasse, klassetype), + opprinneligUtbetalingsbeløp = it.belopOpprUtbet, + nyttBeløp = it.belopNy, + tilbakekrevesBeløp = it.belopTilbakekreves, + uinnkrevdBeløp = it.belopUinnkrevd, + skatteprosent = it.skattProsent, + resultatkode = it.kodeResultat, + årsakskode = it.kodeAArsak, + skyldkode = it.kodeSkyld, + ) + }.toSet() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagMottaker.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagMottaker.kt new file mode 100644 index 000000000..b96f0b07b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagMottaker.kt @@ -0,0 +1,53 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import jakarta.jms.TextMessage +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.kravgrunnlag.task.BehandleKravgrunnlagTask +import no.nav.familie.tilbake.kravgrunnlag.task.BehandleStatusmeldingTask +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.Profile +import org.springframework.jms.annotation.JmsListener +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.Properties +import java.util.UUID + +@Service +@Profile("!e2e & !integrasjonstest") +class KravgrunnlagMottaker(private val taskService: TaskService) { + + private val log = LoggerFactory.getLogger(this::class.java) + private val secureLog = LoggerFactory.getLogger("secureLogger") + + @Transactional + @JmsListener(destination = "\${oppdrag.mq.kravgrunnlag}", containerFactory = "jmsListenerContainerFactory") + fun mottaMeldingFraOppdrag(melding: TextMessage) { + val meldingFraOppdrag = melding.text as String + + log.info("Mottatt melding fra oppdrag") + secureLog.info(meldingFraOppdrag) + if (meldingFraOppdrag.contains(Constants.kravgrunnlagXmlRootElement)) { + taskService.save( + Task( + type = BehandleKravgrunnlagTask.TYPE, + payload = meldingFraOppdrag, + properties = Properties().apply { + this["callId"] = UUID.randomUUID() + }, + ), + ) + } else { + taskService.save( + Task( + type = BehandleStatusmeldingTask.TYPE, + payload = meldingFraOppdrag, + properties = Properties().apply { + this["callId"] = UUID.randomUUID() + }, + ), + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagRepository.kt new file mode 100644 index 000000000..c42873bdb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagRepository.kt @@ -0,0 +1,30 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional +import java.math.BigInteger +import java.util.UUID + +@Repository +@Transactional +interface KravgrunnlagRepository : + RepositoryInterface, + InsertUpdateRepository { + + fun findByBehandlingIdAndAktivIsTrue(behandlingId: UUID): Kravgrunnlag431 + + fun findByBehandlingIdAndAktivIsTrueAndSperretTrue(behandlingId: UUID): Kravgrunnlag431 + + fun existsByBehandlingIdAndAktivTrue(behandlingId: UUID): Boolean + + fun existsByBehandlingIdAndAktivTrueAndSperretFalse(behandlingId: UUID): Boolean + + fun existsByBehandlingIdAndAktivTrueAndSperretTrue(behandlingId: UUID): Boolean + + fun findByBehandlingId(behandlingId: UUID): List + + fun findByEksternKravgrunnlagIdAndAktivIsTrue(eksternKravgrunnlagId: BigInteger): Kravgrunnlag431? +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagService.kt new file mode 100644 index 000000000..43890fd36 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagService.kt @@ -0,0 +1,241 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsystemUtil +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingService +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.behandling.task.OppdaterFaktainfoTask +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.familie.tilbake.kravgrunnlag.event.EndretKravgrunnlagEventPublisher +import no.nav.familie.tilbake.micrometer.TellerService +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagDto +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.temporal.ChronoUnit +import java.util.Properties + +@Service +class KravgrunnlagService( + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val behandlingRepository: BehandlingRepository, + private val mottattXmlService: ØkonomiXmlMottattService, + private val stegService: StegService, + private val behandlingskontrollService: BehandlingskontrollService, + private val taskService: TaskService, + private val tellerService: TellerService, + private val oppgaveTaskService: OppgaveTaskService, + private val historikkTaskService: HistorikkTaskService, + private val hentFagsystemsbehandlingService: HentFagsystemsbehandlingService, + private val endretKravgrunnlagEventPublisher: EndretKravgrunnlagEventPublisher, +) { + + private val log = LoggerFactory.getLogger(this::class.java) + + @Transactional + fun håndterMottattKravgrunnlag(kravgrunnlagXml: String) { + val kravgrunnlag: DetaljertKravgrunnlagDto = KravgrunnlagUtil.unmarshalKravgrunnlag(kravgrunnlagXml) + // valider grunnlag + KravgrunnlagValidator.validerGrunnlag(kravgrunnlag) + + val fagsystemId = kravgrunnlag.fagsystemId + val ytelsestype: Ytelsestype = KravgrunnlagUtil.tilYtelsestype(kravgrunnlag.kodeFagomraade) + + val behandling: Behandling? = finnÅpenBehandling(ytelsestype, fagsystemId) + val fagsystem = FagsystemUtil.hentFagsystemFraYtelsestype(ytelsestype) + if (behandling == null) { + mottattXmlService.arkiverEksisterendeGrunnlag(kravgrunnlag) + mottattXmlService.lagreMottattXml(kravgrunnlagXml, kravgrunnlag, ytelsestype) + tellerService.tellUkobletKravgrunnlag(fagsystem) + return + } + // mapper grunnlag til Kravgrunnlag431 + val kravgrunnlag431: Kravgrunnlag431 = KravgrunnlagMapper.tilKravgrunnlag431(kravgrunnlag, behandling.id) + sjekkIdentiskKravgrunnlag(kravgrunnlag431, behandling) + lagreKravgrunnlag(kravgrunnlag431, ytelsestype) + mottattXmlService.arkiverMottattXml(kravgrunnlagXml, fagsystemId, ytelsestype) + + historikkTaskService.lagHistorikkTask( + behandling.id, + TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT, + Aktør.VEDTAKSLØSNING, + ) + + // oppdater frist på oppgave når behandling venter på grunnlag + val aktivBehandlingsstegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandling.id) + if (aktivBehandlingsstegstilstand?.venteårsak == Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG) { + håndterOppgave(behandling) + } else { + håndterOppgavePrioritet(behandling) + } + + if (Kravstatuskode.ENDRET == kravgrunnlag431.kravstatuskode) { + log.info("Mottatt ENDR kravgrunnlag. Fjerner eksisterende data for behandling ${behandling.id}") + endretKravgrunnlagEventPublisher.fireEvent(behandlingId = behandling.id) + // flytter behandlingssteg tilbake til fakta, + // behandling har allerede fått SPER melding og venter på kravgrunnlag + when (aktivBehandlingsstegstilstand?.behandlingsstegsstatus) { + Behandlingsstegstatus.VENTER -> { + log.info( + "Behandling ${behandling.id} venter på kravgrunnlag, mottatt ENDR kravgrunnlag. " + + "Flytter behandlingen til fakta steg", + ) + behandlingskontrollService.tilbakeførBehandledeSteg(behandling.id) + } + + else -> { // behandling har ikke fått SPER melding og har noen steg som blir behandlet + log.info( + "Behandling ${behandling.id} blir behandlet, mottatt ENDR kravgrunnlag. " + + "Flytter behandlingen til fakta steg", + ) + behandlingskontrollService.behandleStegPåNytt(behandling.id, Behandlingssteg.FAKTA) + } + } + } + stegService.håndterSteg(behandling.id) + tellerService.tellKobletKravgrunnlag(fagsystem) + } + + private fun finnÅpenBehandling( + ytelsestype: Ytelsestype, + fagsystemId: String, + ): Behandling? { + return behandlingRepository.finnÅpenTilbakekrevingsbehandling( + ytelsestype = ytelsestype, + eksternFagsakId = fagsystemId, + ) + } + + private fun lagreKravgrunnlag(kravgrunnlag431: Kravgrunnlag431, ytelsestype: Ytelsestype) { + val finnesKravgrunnlag = kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(kravgrunnlag431.behandlingId) + if (finnesKravgrunnlag) { + val eksisterendeKravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(kravgrunnlag431.behandlingId) + loggFeilHvisGammeltKravgrunnlag(kravgrunnlag431, eksisterendeKravgrunnlag) + kravgrunnlagRepository.update(eksisterendeKravgrunnlag.copy(aktiv = false)) + if (eksisterendeKravgrunnlag.referanse != kravgrunnlag431.referanse) { + hentOgOppdaterFaktaInfo(kravgrunnlag431, ytelsestype) + } + } + kravgrunnlagRepository.insert(kravgrunnlag431) + } + + private fun loggFeilHvisGammeltKravgrunnlag( + kravgrunnlag431: Kravgrunnlag431, + eksisterendeKravgrunnlag: Kravgrunnlag431, + ) { + if (kravgrunnlag431.kontrollfelt < eksisterendeKravgrunnlag.kontrollfelt) { + log.error( + "Skitpomfrit! Det hentes inn et eldre kravgrunnlag enn det som allerede finnes på behandlingen. Dette må sjekkes nærmere! " + + "Gjelder behandling=${kravgrunnlag431.behandlingId}, " + + "eksisterendeKravgrunnlagId=${eksisterendeKravgrunnlag.id}, " + + "eksisterendeEksternKravgrunnlagId=${eksisterendeKravgrunnlag.eksternKravgrunnlagId}, " + + "nyKravgrunnlagId=${kravgrunnlag431.id}, " + + "nyEksternKravgrunnlagId=${kravgrunnlag431.eksternKravgrunnlagId}", + ) + } + } + + private fun hentOgOppdaterFaktaInfo( + kravgrunnlag431: Kravgrunnlag431, + ytelsestype: Ytelsestype, + ) { + // henter faktainfo fra fagsystem for ny referanse via kafka + hentFagsystemsbehandlingService.sendHentFagsystemsbehandlingRequest( + eksternFagsakId = kravgrunnlag431.fagsystemId, + ytelsestype = ytelsestype, + eksternId = kravgrunnlag431.referanse, + ) + // OppdaterFaktainfoTask skal oppdatere fakta info med ny hentet faktainfo + taskService.save( + Task( + type = OppdaterFaktainfoTask.TYPE, + payload = "", + properties = Properties().apply { + setProperty("eksternFagsakId", kravgrunnlag431.fagsystemId) + setProperty("ytelsestype", ytelsestype.name) + setProperty("eksternId", kravgrunnlag431.referanse) + setProperty(PropertyName.FAGSYSTEM, FagsystemUtil.hentFagsystemFraYtelsestype(ytelsestype).name) + }, + ), + ) + } + + private fun håndterOppgave(behandling: Behandling) { + val revurderingsvedtaksdato = behandling.aktivFagsystemsbehandling.revurderingsvedtaksdato + val interval = ChronoUnit.DAYS.between(revurderingsvedtaksdato, LocalDate.now()) + if (interval >= FRIST_DATO_GRENSE) { + oppgaveTaskService.oppdaterOppgaveTask( + behandlingId = behandling.id, + beskrivelse = "Behandling er tatt av vent, pga mottatt kravgrunnlag", + frist = LocalDate.now().plusDays(1), + ) + } else { + val beskrivelse = "Behandling er tatt av vent, " + + "men revurderingsvedtaksdato er mindre enn $FRIST_DATO_GRENSE dager fra dagens dato." + + "Fristen settes derfor $FRIST_DATO_GRENSE dager fra revurderingsvedtaksdato " + + "for å sikre at behandlingen har mottatt oppdatert kravgrunnlag" + oppgaveTaskService.oppdaterOppgaveTask( + behandlingId = behandling.id, + beskrivelse = beskrivelse, + frist = revurderingsvedtaksdato.plusDays(FRIST_DATO_GRENSE), + ) + } + } + + private fun håndterOppgavePrioritet(behandling: Behandling) { + oppgaveTaskService.oppdaterOppgavePrioritetTask(behandlingId = behandling.id, fagsakId = behandling.aktivFagsystemsbehandling.eksternId) + } + + private fun sjekkIdentiskKravgrunnlag(endretKravgrunnlag: Kravgrunnlag431, behandling: Behandling) { + if (endretKravgrunnlag.kravstatuskode != Kravstatuskode.ENDRET || + // sjekker ikke identisk kravgrunnlag for behandlinger som har sendt varselbrev + behandling.aktivtVarsel != null || + // sjekker ikke identisk kravgrunnlag når behandling ikke har koblet med et NYTT kravgrunnlag + !kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(endretKravgrunnlag.behandlingId) + ) { + return + } + val forrigeKravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(endretKravgrunnlag.behandlingId) + val harSammeAntallPerioder = forrigeKravgrunnlag.perioder.size == endretKravgrunnlag.perioder.size + val perioderIForrigeKravgrunnlag = forrigeKravgrunnlag.perioder.sortedBy { it.periode } + val perioderIEndretKravgrunnlag = endretKravgrunnlag.perioder.sortedBy { it.periode } + var erIdentiskKravgrunnlag = harSammeAntallPerioder + if (harSammeAntallPerioder) { + for (i in perioderIEndretKravgrunnlag.indices step 1) { + if (!perioderIEndretKravgrunnlag[i].harIdentiskKravgrunnlagsperiode(perioderIForrigeKravgrunnlag[i])) { + erIdentiskKravgrunnlag = false + } + } + } + if (erIdentiskKravgrunnlag) { + log.warn( + "Mottatt kravgrunnlag med kravgrunnlagId ${endretKravgrunnlag.eksternKravgrunnlagId}," + + "status ${endretKravgrunnlag.kravstatuskode.kode} og referanse ${endretKravgrunnlag.referanse} " + + "for behandlingId=${endretKravgrunnlag.behandlingId} " + + "er identisk med eksisterende kravgrunnlag med kravgrunnlagId ${forrigeKravgrunnlag.eksternKravgrunnlagId}," + + "status ${forrigeKravgrunnlag.kravstatuskode.kode} og referanse ${forrigeKravgrunnlag.referanse}." + + "Undersøk om ny referanse kan gi feil i brev..", + ) + } + } + + companion object { + + const val FRIST_DATO_GRENSE = 10L + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagUtil.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagUtil.kt new file mode 100644 index 000000000..32f8a37c6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagUtil.kt @@ -0,0 +1,147 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import jakarta.xml.bind.JAXBContext +import jakarta.xml.bind.JAXBException +import jakarta.xml.bind.Unmarshaller +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.common.exceptionhandler.UgyldigKravgrunnlagFeil +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagDto +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagMelding +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagPeriodeDto +import no.nav.tilbakekreving.status.v1.EndringKravOgVedtakstatus +import no.nav.tilbakekreving.status.v1.KravOgVedtakstatus +import org.apache.commons.lang3.builder.DiffBuilder +import org.apache.commons.lang3.builder.ToStringStyle +import java.io.StringReader +import java.math.BigDecimal +import java.util.SortedMap +import javax.xml.XMLConstants +import javax.xml.validation.SchemaFactory + +object KravgrunnlagUtil { + + private val jaxbContext: JAXBContext = JAXBContext.newInstance(DetaljertKravgrunnlagMelding::class.java) + private val statusmeldingJaxbContext: JAXBContext = JAXBContext.newInstance(EndringKravOgVedtakstatus::class.java) + + fun finnFeilutbetalingPrPeriode(kravgrunnlag: Kravgrunnlag431): SortedMap { + val feilutbetalingPrPeriode = mutableMapOf() + for (kravgrunnlagPeriode432 in kravgrunnlag.perioder) { + val feilutbetaltBeløp = kravgrunnlagPeriode432.beløp + .filter { Klassetype.FEIL == it.klassetype } + .sumOf(Kravgrunnlagsbeløp433::nyttBeløp) + if (feilutbetaltBeløp.compareTo(BigDecimal.ZERO) != 0) { + feilutbetalingPrPeriode[kravgrunnlagPeriode432.periode] = feilutbetaltBeløp + } + } + return feilutbetalingPrPeriode.toSortedMap(Comparator.comparing(Månedsperiode::fom).thenComparing(Månedsperiode::tom)) + } + + fun unmarshalKravgrunnlag(kravgrunnlagXML: String): DetaljertKravgrunnlagDto { + return try { + val jaxbUnmarshaller: Unmarshaller = jaxbContext.createUnmarshaller() + + // satt xsd for å validere mottatt xml + val schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) + val kravgrunnlagSchema = + schemaFactory.newSchema(this.javaClass.classLoader.getResource("xsd/kravgrunnlag_detalj.xsd")) + jaxbUnmarshaller.schema = kravgrunnlagSchema + + (jaxbUnmarshaller.unmarshal(StringReader(kravgrunnlagXML)) as DetaljertKravgrunnlagMelding).detaljertKravgrunnlag + } catch (e: JAXBException) { + throw UgyldigKravgrunnlagFeil(melding = "Mottatt kravgrunnlagXML er ugyldig! Den feiler med $e") + } + } + + fun unmarshalStatusmelding(statusmeldingXml: String): KravOgVedtakstatus { + return try { + val jaxbUnmarshaller: Unmarshaller = statusmeldingJaxbContext.createUnmarshaller() + + // satt xsd for å validere mottatt xml + val schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) + val statusmeldingSchema = + schemaFactory.newSchema(this.javaClass.classLoader.getResource("xsd/krav_og_vedtakstatus.xsd")) + jaxbUnmarshaller.schema = statusmeldingSchema + + (jaxbUnmarshaller.unmarshal(StringReader(statusmeldingXml)) as EndringKravOgVedtakstatus).kravOgVedtakstatus + } catch (e: JAXBException) { + throw UgyldigKravgrunnlagFeil(melding = "Mottatt statusmeldingXML er ugyldig! Den feiler med $e") + } + } + + fun tilYtelsestype(fagområdekode: String): Ytelsestype { + return Ytelsestype.values().firstOrNull { it.kode == fagområdekode } + ?: throw IllegalArgumentException("Ukjent Ytelsestype for $fagområdekode") + } + + fun sammenlignKravgrunnlag(mottattKravgrunnlag: DetaljertKravgrunnlagDto, hentetKravgrunnlag: DetaljertKravgrunnlagDto): String { + val builder = DiffBuilder(mottattKravgrunnlag, hentetKravgrunnlag, ToStringStyle.JSON_STYLE) + .append("kravgrunnlagId", mottattKravgrunnlag.kravgrunnlagId, hentetKravgrunnlag.kravgrunnlagId) + .append("vedtakId", mottattKravgrunnlag.vedtakId, hentetKravgrunnlag.vedtakId) + .append("kodeStatusKrav", mottattKravgrunnlag.kodeStatusKrav, hentetKravgrunnlag.kodeStatusKrav) + .append("kodeFagomraade", mottattKravgrunnlag.kodeFagomraade, hentetKravgrunnlag.kodeFagomraade) + .append("fagsystemId", mottattKravgrunnlag.fagsystemId, hentetKravgrunnlag.fagsystemId) + .append("datoVedtakFagsystem", mottattKravgrunnlag.datoVedtakFagsystem, hentetKravgrunnlag.datoVedtakFagsystem) + .append("vedtakIdOmgjort", mottattKravgrunnlag.vedtakIdOmgjort, hentetKravgrunnlag.vedtakIdOmgjort) + .append("vedtakGjelderId", mottattKravgrunnlag.vedtakGjelderId, hentetKravgrunnlag.vedtakGjelderId) + .append("typeGjelderId", mottattKravgrunnlag.typeGjelderId, hentetKravgrunnlag.typeGjelderId) + .append("utbetalesTilId", mottattKravgrunnlag.utbetalesTilId, hentetKravgrunnlag.utbetalesTilId) + .append("typeUtbetId", mottattKravgrunnlag.typeUtbetId, hentetKravgrunnlag.typeUtbetId) + .append("kontrollfelt", mottattKravgrunnlag.kontrollfelt, hentetKravgrunnlag.kontrollfelt) + .append("referanse", mottattKravgrunnlag.referanse, hentetKravgrunnlag.referanse) + + val mottattPerioder = mottattKravgrunnlag.tilbakekrevingsPeriode.sortedBy { it.periode.fom } + val hentetPerioder = hentetKravgrunnlag.tilbakekrevingsPeriode.sortedBy { it.periode.fom } + val differanser = sammenlignPerioder(mottattPerioder, hentetPerioder) + val differanseBuilder = StringBuilder() + if (differanser.isNotEmpty()) { + differanseBuilder.append("Mangler periode ${differanser.map { konvertPeriode(it) }}.") + } + + val perioder = mottattPerioder.zip(hentetPerioder) + perioder.forEach { + val periode = konvertPeriode(it.first) + builder.append("periode", periode, konvertPeriode(it.second)) + .append("belopSkattMnd", it.first.belopSkattMnd, it.second.belopSkattMnd) + + val beløper = it.first.tilbakekrevingsBelop.sortedBy { beløp -> beløp.typeKlasse } + .zip(it.second.tilbakekrevingsBelop.sortedBy { beløp -> beløp.typeKlasse }) + + beløper.forEach { beløp -> + builder.append("kodeKlasse", beløp.first.kodeKlasse, beløp.second.kodeKlasse) + .append("kodeKlasse", beløp.first.kodeKlasse, beløp.second.kodeKlasse) + .append("$periode->belopOpprUtbet", beløp.first.belopOpprUtbet, beløp.second.belopOpprUtbet) + .append("$periode->belopNy", beløp.first.belopNy, beløp.second.belopNy) + .append("$periode->belopUinnkrevd", beløp.first.belopUinnkrevd, beløp.second.belopUinnkrevd) + .append( + "$periode->belopTilbakekreves", + beløp.first.belopTilbakekreves, + beløp.second.belopTilbakekreves, + ) + .append("$periode->skattProsent", beløp.first.skattProsent, beløp.second.skattProsent) + } + } + + return differanseBuilder.append(builder.build().toString()).toString() + } + + private fun konvertPeriode(periodeDto: DetaljertKravgrunnlagPeriodeDto): Månedsperiode { + return Månedsperiode(periodeDto.periode.fom, periodeDto.periode.tom) + } + + private fun sammenlignPerioder( + mottattPerioder: List, + hentetPerioder: List, + ): List { + if (mottattPerioder.size == hentetPerioder.size) { + return mottattPerioder.filter { hentetPerioder.none { mindre -> mindre.periode.fom == it.periode.fom } } + } + val størrePerioder = if (mottattPerioder.size > hentetPerioder.size) mottattPerioder else hentetPerioder + val mindrePerioder = if (mottattPerioder.size < hentetPerioder.size) mottattPerioder else hentetPerioder + + return størrePerioder.filter { mindrePerioder.none { mindre -> mindre.periode.fom == it.periode.fom } } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagValidator.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagValidator.kt new file mode 100644 index 000000000..ff6142f64 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagValidator.kt @@ -0,0 +1,239 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.common.exceptionhandler.UgyldigKravgrunnlagFeil +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagBelopDto +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagDto +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagPeriodeDto +import no.nav.tilbakekreving.typer.v1.PeriodeDto +import no.nav.tilbakekreving.typer.v1.TypeKlasseDto +import java.math.BigDecimal +import java.math.BigInteger +import java.math.RoundingMode +import java.time.YearMonth + +object KravgrunnlagValidator { + + @Throws(UgyldigKravgrunnlagFeil::class) + fun validerGrunnlag(kravgrunnlag: DetaljertKravgrunnlagDto) { + validerReferanse(kravgrunnlag) + validerPeriodeInnenforMåned(kravgrunnlag) + validerPeriodeStarterFørsteDagIMåned(kravgrunnlag) + validerPeriodeSlutterSisteDagIMåned(kravgrunnlag) + validerOverlappendePerioder(kravgrunnlag) + validerSkatt(kravgrunnlag) + validerPerioderHarFeilutbetalingspostering(kravgrunnlag) + validerPerioderHarYtelsespostering(kravgrunnlag) + validerPerioderHarFeilPosteringMedNegativFeilutbetaltBeløp(kravgrunnlag) + validerYtelseMotFeilutbetaling(kravgrunnlag) + validerYtelsesPosteringTilbakekrevesMotNyttOgOpprinneligUtbetalt(kravgrunnlag) + } + + private fun validerReferanse(kravgrunnlag: DetaljertKravgrunnlagDto) { + kravgrunnlag.referanse ?: throw UgyldigKravgrunnlagFeil( + melding = "Ugyldig kravgrunnlag for kravgrunnlagId " + + "${kravgrunnlag.kravgrunnlagId}. Mangler referanse.", + ) + } + + private fun validerPeriodeInnenforMåned(kravgrunnlag: DetaljertKravgrunnlagDto) { + kravgrunnlag.tilbakekrevingsPeriode.forEach { + val periode = it.periode + val fomMåned = YearMonth.of(periode.fom.year, periode.fom.month) + val tomMåned = YearMonth.of(periode.tom.year, periode.tom.month) + if (fomMåned != tomMåned) { + throw UgyldigKravgrunnlagFeil( + "Ugyldig kravgrunnlag for kravgrunnlagId ${kravgrunnlag.kravgrunnlagId}." + + " Perioden ${periode.fom}-${periode.tom} er ikke innenfor en kalendermåned.", + ) + } + } + } + + private fun validerPeriodeStarterFørsteDagIMåned(kravgrunnlag: DetaljertKravgrunnlagDto) { + kravgrunnlag.tilbakekrevingsPeriode.forEach { + if (it.periode.fom.dayOfMonth != 1) { + throw UgyldigKravgrunnlagFeil( + "Ugyldig kravgrunnlag for kravgrunnlagId ${kravgrunnlag.kravgrunnlagId}." + + " Perioden ${it.periode.fom}-${it.periode.tom} starter ikke første dag i måned.", + ) + } + } + } + + private fun validerPeriodeSlutterSisteDagIMåned(kravgrunnlag: DetaljertKravgrunnlagDto) { + kravgrunnlag.tilbakekrevingsPeriode.forEach { + if (it.periode.tom.dayOfMonth != YearMonth.from(it.periode.tom).lengthOfMonth()) { + throw UgyldigKravgrunnlagFeil( + "Ugyldig kravgrunnlag for kravgrunnlagId ${kravgrunnlag.kravgrunnlagId}." + + " Perioden ${it.periode.fom}-${it.periode.tom} slutter ikke siste dag i måned.", + ) + } + } + } + + private fun validerPerioderHarFeilutbetalingspostering(kravgrunnlag: DetaljertKravgrunnlagDto) { + kravgrunnlag.tilbakekrevingsPeriode.forEach { + if (it.tilbakekrevingsBelop.none { beløp -> finnesFeilutbetalingspostering(beløp.typeKlasse) }) { + throw UgyldigKravgrunnlagFeil( + "Ugyldig kravgrunnlag for kravgrunnlagId ${kravgrunnlag.kravgrunnlagId}. " + + "Perioden ${it.periode.fom}-${it.periode.tom} " + + "mangler postering med klassetype=FEIL.", + ) + } + } + } + + private fun validerPerioderHarYtelsespostering(kravgrunnlag: DetaljertKravgrunnlagDto) { + kravgrunnlag.tilbakekrevingsPeriode.forEach { + if (it.tilbakekrevingsBelop.none { beløp -> finnesYtelsespostering(beløp.typeKlasse) }) { + throw UgyldigKravgrunnlagFeil( + "Ugyldig kravgrunnlag for kravgrunnlagId ${kravgrunnlag.kravgrunnlagId}. " + + "Perioden ${it.periode.fom}-${it.periode.tom} " + + "mangler postering med klassetype=YTEL.", + ) + } + } + } + + private fun validerOverlappendePerioder(kravgrunnlag: DetaljertKravgrunnlagDto) { + val sortertePerioder: List = kravgrunnlag.tilbakekrevingsPeriode + .map { p -> Månedsperiode(p.periode.fom, p.periode.tom) } + .sorted() + for (i in 1 until sortertePerioder.size) { + val forrigePeriode = sortertePerioder[i - 1] + val nåværendePeriode = sortertePerioder[i] + if (nåværendePeriode.fom <= forrigePeriode.tom) { + throw UgyldigKravgrunnlagFeil( + "Ugyldig kravgrunnlag for kravgrunnlagId ${kravgrunnlag.kravgrunnlagId}." + + " Overlappende perioder $forrigePeriode og $nåværendePeriode.", + ) + } + } + } + + private fun validerSkatt(kravgrunnlag: DetaljertKravgrunnlagDto) { + val grupppertPåMåned: Map> = kravgrunnlag.tilbakekrevingsPeriode + .groupBy { tilMåned(it.periode) }.toMap() + + for ((key, value) in grupppertPåMåned) { + validerSkattForPeriode(key, value, kravgrunnlag.kravgrunnlagId) + } + } + + private fun validerSkattForPeriode( + måned: YearMonth, + perioder: List, + kravgrunnlagId: BigInteger, + ) { + var månedligSkattBeløp: BigDecimal? = null + var totalSkatt = BigDecimal.ZERO + for (periode in perioder) { + if (månedligSkattBeløp == null) { + månedligSkattBeløp = periode.belopSkattMnd + } else { + if (månedligSkattBeløp.compareTo(periode.belopSkattMnd) != 0) { + throw UgyldigKravgrunnlagFeil( + "Ugyldig kravgrunnlag for kravgrunnlagId $kravgrunnlagId. " + + "For måned $måned er opplyses ulike verdier maks skatt i ulike perioder", + ) + } + } + for (postering in periode.tilbakekrevingsBelop) { + totalSkatt += postering.belopTilbakekreves.multiply(postering.skattProsent) + } + } + totalSkatt = totalSkatt.divide(BigDecimal.valueOf(100), 0, RoundingMode.DOWN) + if (månedligSkattBeløp == null) { + throw UgyldigKravgrunnlagFeil( + "Ugyldig kravgrunnlag for kravgrunnlagId $kravgrunnlagId. " + + "Mangler max skatt for måned $måned", + ) + } + if (totalSkatt > månedligSkattBeløp) { + throw UgyldigKravgrunnlagFeil( + "Ugyldig kravgrunnlag for kravgrunnlagId $kravgrunnlagId. " + + "For måned $måned er maks skatt $månedligSkattBeløp, " + + "men maks tilbakekreving ganget med skattesats blir $totalSkatt", + ) + } + } + + private fun validerPerioderHarFeilPosteringMedNegativFeilutbetaltBeløp(kravgrunnlag: DetaljertKravgrunnlagDto) { + for (kravgrunnlagsperiode in kravgrunnlag.tilbakekrevingsPeriode) { + for (beløp in kravgrunnlagsperiode.tilbakekrevingsBelop) { + if (finnesFeilutbetalingspostering(beløp.typeKlasse) && beløp.belopNy < BigDecimal.ZERO) { + throw UgyldigKravgrunnlagFeil( + "Ugyldig kravgrunnlag for kravgrunnlagId ${kravgrunnlag.kravgrunnlagId}. " + + "Perioden ${kravgrunnlagsperiode.periode.fom}-" + + "${kravgrunnlagsperiode.periode.tom} " + + "har FEIL postering med negativ beløp", + ) + } + } + } + } + + private fun validerYtelseMotFeilutbetaling(kravgrunnlag: DetaljertKravgrunnlagDto) { + for (kravgrunnlagsperiode in kravgrunnlag.tilbakekrevingsPeriode) { + val sumTilbakekrevesFraYtelsePosteringer = kravgrunnlagsperiode.tilbakekrevingsBelop + .filter { finnesYtelsespostering(it.typeKlasse) } + .sumOf(DetaljertKravgrunnlagBelopDto::getBelopTilbakekreves) + val sumNyttBelopFraFeilposteringer = kravgrunnlagsperiode.tilbakekrevingsBelop + .filter { finnesFeilutbetalingspostering(it.typeKlasse) } + .sumOf(DetaljertKravgrunnlagBelopDto::getBelopNy) + if (sumNyttBelopFraFeilposteringer.compareTo(sumTilbakekrevesFraYtelsePosteringer) != 0) { + throw UgyldigKravgrunnlagFeil( + "Ugyldig kravgrunnlag for kravgrunnlagId ${kravgrunnlag.kravgrunnlagId}. " + + "For perioden ${kravgrunnlagsperiode.periode.fom}" + + "-${kravgrunnlagsperiode.periode.tom} total tilkakekrevesBeløp i YTEL " + + "posteringer er $sumTilbakekrevesFraYtelsePosteringer, mens total nytt beløp i " + + "FEIL posteringer er $sumNyttBelopFraFeilposteringer. " + + "Det er forventet at disse er like.", + ) + } + } + } + + private fun validerYtelsesPosteringTilbakekrevesMotNyttOgOpprinneligUtbetalt(kravgrunnlag: DetaljertKravgrunnlagDto) { + var harPeriodeMedBeløpMindreEnnDiff = false + var harPeriodeMedBeløpStørreEnnDiff = false + + for (kravgrunnlagsperiode in kravgrunnlag.tilbakekrevingsPeriode) { + for (kgBeløp in kravgrunnlagsperiode.tilbakekrevingsBelop) { + if (finnesYtelsespostering(kgBeløp.typeKlasse)) { + val diff: BigDecimal = kgBeløp.belopOpprUtbet.subtract(kgBeløp.belopNy) + if (kgBeløp.belopTilbakekreves > diff) { + harPeriodeMedBeløpStørreEnnDiff = true + } else { + harPeriodeMedBeløpMindreEnnDiff = true + } + } + } + } + + // Hvis vi kun har YTEL-posteringer som er sørre enn diferansen mellom nyttBeløp og opprinneligBeløp + // vil vi kaste en valideringsfeil + if (harPeriodeMedBeløpStørreEnnDiff && !harPeriodeMedBeløpMindreEnnDiff) { + throw UgyldigKravgrunnlagFeil( + "Ugyldig kravgrunnlag for kravgrunnlagId ${kravgrunnlag.kravgrunnlagId}. " + + "Har en eller flere perioder med YTEL-postering " + + "med tilbakekrevesBeløp som er større enn differanse mellom " + + "nyttBeløp og opprinneligBeløp", + ) + } + } + + private fun tilMåned(periode: PeriodeDto): YearMonth { + return YearMonth.of(periode.fom.year, periode.fom.month) + } + + private fun finnesFeilutbetalingspostering(typeKlasse: TypeKlasseDto): Boolean { + return Klassetype.FEIL.name == typeKlasse.value() + } + + private fun finnesYtelsespostering(typeKlasse: TypeKlasseDto): Boolean { + return Klassetype.YTEL.name == typeKlasse.value() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravvedtakstatusService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravvedtakstatusService.kt new file mode 100644 index 000000000..ce4cd3f66 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravvedtakstatusService.kt @@ -0,0 +1,183 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.api.dto.HenleggelsesbrevFritekstDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.BehandlingService +import no.nav.familie.tilbake.behandling.FagsystemUtil +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.exceptionhandler.UgyldigStatusmeldingFeil +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottatt +import no.nav.familie.tilbake.micrometer.TellerService +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import no.nav.tilbakekreving.status.v1.KravOgVedtakstatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.util.UUID + +@Service +class KravvedtakstatusService( + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val behandlingRepository: BehandlingRepository, + private val mottattXmlService: ØkonomiXmlMottattService, + private val stegService: StegService, + private val tellerService: TellerService, + private val behandlingskontrollService: BehandlingskontrollService, + private val behandlingService: BehandlingService, + private val historikkTaskService: HistorikkTaskService, + private val oppgaveTaskService: OppgaveTaskService, +) { + + @Transactional + fun håndterMottattStatusmelding(statusmeldingXml: String) { + val kravOgVedtakstatus: KravOgVedtakstatus = KravgrunnlagUtil.unmarshalStatusmelding(statusmeldingXml) + + validerStatusmelding(kravOgVedtakstatus) + + val fagsystemId = kravOgVedtakstatus.fagsystemId + val vedtakId = kravOgVedtakstatus.vedtakId + val ytelsestype: Ytelsestype = KravgrunnlagUtil.tilYtelsestype(kravOgVedtakstatus.kodeFagomraade) + + val behandling: Behandling? = finnÅpenBehandling(ytelsestype, fagsystemId) + if (behandling == null) { + val kravgrunnlagXmlListe = mottattXmlService + .hentMottattKravgrunnlag( + eksternFagsakId = fagsystemId, + ytelsestype = ytelsestype, + vedtakId = vedtakId, + ) + håndterStatusmeldingerUtenBehandling(kravgrunnlagXmlListe, kravOgVedtakstatus) + mottattXmlService.arkiverMottattXml(statusmeldingXml, fagsystemId, ytelsestype) + tellerService.tellUkobletStatusmelding(FagsystemUtil.hentFagsystemFraYtelsestype(ytelsestype)) + return + } + val kravgrunnlag431: Kravgrunnlag431 = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + håndterStatusmeldingerMedBehandling(kravgrunnlag431, kravOgVedtakstatus, behandling) + mottattXmlService.arkiverMottattXml(statusmeldingXml, fagsystemId, ytelsestype) + tellerService.tellKobletStatusmelding(FagsystemUtil.hentFagsystemFraYtelsestype(ytelsestype)) + } + + private fun validerStatusmelding(kravOgVedtakstatus: KravOgVedtakstatus) { + kravOgVedtakstatus.referanse + ?: throw UgyldigStatusmeldingFeil( + melding = "Ugyldig statusmelding for vedtakId=${kravOgVedtakstatus.vedtakId}, " + + "Mangler referanse.", + ) + } + + private fun finnÅpenBehandling( + ytelsestype: Ytelsestype, + fagsystemId: String, + ): Behandling? { + return behandlingRepository.finnÅpenTilbakekrevingsbehandling( + ytelsestype = ytelsestype, + eksternFagsakId = fagsystemId, + ) + } + + private fun håndterStatusmeldingerUtenBehandling( + kravgrunnlagXmlListe: List<ØkonomiXmlMottatt>, + kravOgVedtakstatus: KravOgVedtakstatus, + ) { + when (val kravstatuskode = Kravstatuskode.fraKode(kravOgVedtakstatus.kodeStatusKrav)) { + Kravstatuskode.SPERRET, Kravstatuskode.MANUELL -> + kravgrunnlagXmlListe.forEach { mottattXmlService.oppdaterMottattXml(it.copy(sperret = true)) } + Kravstatuskode.ENDRET -> kravgrunnlagXmlListe.forEach { + mottattXmlService + .oppdaterMottattXml(it.copy(sperret = false)) + } + Kravstatuskode.AVSLUTTET -> kravgrunnlagXmlListe.forEach { + mottattXmlService.arkiverMottattXml( + it.melding, + it.eksternFagsakId, + it.ytelsestype, + ) + mottattXmlService.slettMottattXml(it.id) + } + else -> throw IllegalArgumentException("Ukjent statuskode $kravstatuskode i statusmelding") + } + } + + private fun håndterStatusmeldingerMedBehandling( + kravgrunnlag431: Kravgrunnlag431, + kravOgVedtakstatus: KravOgVedtakstatus, + behandling: Behandling, + ) { + when (val kravstatuskode = Kravstatuskode.fraKode(kravOgVedtakstatus.kodeStatusKrav)) { + Kravstatuskode.SPERRET, Kravstatuskode.MANUELL -> { + håndterSperMeldingMedBehandling(behandling.id, kravgrunnlag431) + } + Kravstatuskode.ENDRET -> { + kravgrunnlagRepository.update(kravgrunnlag431.copy(sperret = false)) + stegService.håndterSteg(behandling.id) + oppgaveTaskService.oppdaterOppgaveTask( + behandlingId = behandling.id, + beskrivelse = "Behandling er tatt av vent, pga mottatt ENDR melding", + frist = LocalDate.now(), + ) + } + Kravstatuskode.AVSLUTTET -> { + kravgrunnlagRepository.update(kravgrunnlag431.copy(avsluttet = true)) + behandlingService + .henleggBehandling( + behandlingId = behandling.id, + HenleggelsesbrevFritekstDto( + behandlingsresultatstype = Behandlingsresultatstype + .HENLAGT_KRAVGRUNNLAG_NULLSTILT, + begrunnelse = "", + ), + ) + } + else -> throw IllegalArgumentException("Ukjent statuskode $kravstatuskode i statusmelding") + } + } + + @Transactional + fun håndterSperMeldingMedBehandling( + behandlingId: UUID, + kravgrunnlag431: Kravgrunnlag431, + ) { + kravgrunnlagRepository.update(kravgrunnlag431.copy(sperret = true)) + val venteårsak = Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG + val tidsfrist = LocalDate.now().plusWeeks(venteårsak.defaultVenteTidIUker) + behandlingskontrollService + .tilbakehoppBehandlingssteg( + behandlingId, + Behandlingsstegsinfo( + behandlingssteg = Behandlingssteg.GRUNNLAG, + behandlingsstegstatus = Behandlingsstegstatus.VENTER, + venteårsak = venteårsak, + tidsfrist = tidsfrist, + ), + ) + historikkTaskService.lagHistorikkTask( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT, + aktør = Aktør.VEDTAKSLØSNING, + beskrivelse = venteårsak.beskrivelse, + ) + + // oppgave oppdateres ikke dersom behandling venter på varsel + val aktivtBehandlingssteg = behandlingskontrollService.finnAktivtSteg(behandlingId) + if (aktivtBehandlingssteg?.let { it != Behandlingssteg.VARSEL } == true) { + oppgaveTaskService.oppdaterOppgaveTask( + behandlingId = behandlingId, + beskrivelse = venteårsak.beskrivelse, + frist = tidsfrist, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/batch/HentFagsystemsbehandlingTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/batch/HentFagsystemsbehandlingTask.kt new file mode 100644 index 000000000..188ed074a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/batch/HentFagsystemsbehandlingTask.kt @@ -0,0 +1,59 @@ +package no.nav.familie.tilbake.kravgrunnlag.batch + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = HentFagsystemsbehandlingTask.TYPE, + beskrivelse = "Sender kafka request til fagsystem for å hente behandling data", + maxAntallFeil = 3, + triggerTidVedFeilISekunder = 60 * 5L, +) +class HentFagsystemsbehandlingTask( + private val håndterGamleKravgrunnlagService: HåndterGamleKravgrunnlagService, + private val hentFagsystemsbehandlingService: HentFagsystemsbehandlingService, + private val taskService: TaskService, +) : AsyncTaskStep { + + private val logger = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + logger.info("HentFagsystemsbehandlingTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val mottattXmlId = UUID.fromString(task.payload) + val mottattXml = håndterGamleKravgrunnlagService.hentFrakobletKravgrunnlag(mottattXmlId) + task.metadata["eksternFagsakId"] = mottattXml.eksternFagsakId + + håndterGamleKravgrunnlagService.sjekkOmDetFinnesEnAktivBehandling(mottattXml) + hentFagsystemsbehandlingService.sendHentFagsystemsbehandlingRequest( + eksternFagsakId = mottattXml.eksternFagsakId, + ytelsestype = mottattXml.ytelsestype, + eksternId = mottattXml.referanse, + ) + } + + @Transactional + override fun onCompletion(task: Task) { + logger.info("Oppretter HåndterGammelKravgrunnlagTask for mottattXmlId=${task.payload}") + taskService.save( + Task( + type = HåndterGammelKravgrunnlagTask.TYPE, + payload = task.payload, + properties = task.metadata, + ).medTriggerTid(LocalDateTime.now().plusSeconds(60)), + ) + } + + companion object { + + const val TYPE = "gammelKravgrunnlag.hentFagsystemsbehandling" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/batch/H\303\245ndterGamleKravgrunnlagBatch.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/batch/H\303\245ndterGamleKravgrunnlagBatch.kt" new file mode 100644 index 000000000..10221f20f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/batch/H\303\245ndterGamleKravgrunnlagBatch.kt" @@ -0,0 +1,116 @@ +package no.nav.familie.tilbake.kravgrunnlag.batch + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype.BARNETILSYN +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype.BARNETRYGD +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype.KONTANTSTØTTE +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype.OVERGANGSSTØNAD +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype.SKOLEPENGER +import no.nav.familie.leader.LeaderClient +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.FagsystemUtil +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattService +import org.slf4j.LoggerFactory +import org.springframework.core.env.Environment +import org.springframework.data.domain.Pageable +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.util.Properties + +@Service +class HåndterGamleKravgrunnlagBatch( + private val mottattXmlService: ØkonomiXmlMottattService, + private val taskService: TaskService, + private val environment: Environment, +) { + + private val logger = LoggerFactory.getLogger(this::class.java) + + @Scheduled(cron = "\${CRON_HÅNDTER_GAMMEL_KRAVGRUNNLAG}") + @Transactional + fun utfør() { + if (LeaderClient.isLeader() != true && !environment.activeProfiles.any { + it.contains("local") || + it.contains("integrasjonstest") + } + ) { + return + } + + logger.info("Starter HåndterGamleKravgrunnlagBatch..") + logger.info("Henter kravgrunnlag som er eldre enn $ALDERSGRENSE_I_UKER uker") + val mottattXmlIdsMedYtelse = mottattXmlService.hentFrakobletGamleMottattXmlIds( + beregnBestemtDato(BARNETRYGD), + beregnBestemtDato(BARNETILSYN), + beregnBestemtDato(OVERGANGSSTØNAD), + beregnBestemtDato(SKOLEPENGER), + beregnBestemtDato(KONTANTSTØTTE), + ) + + if (mottattXmlIdsMedYtelse.isNotEmpty()) { + logger.info( + "Det finnes ${mottattXmlIdsMedYtelse.size} kravgrunnlag som er eldre enn " + + "$ALDERSGRENSE_I_UKER uker fra dagens dato", + ) + + val alleFeiledeTasker = taskService.finnTasksMedStatus( + listOf( + Status.FEILET, + Status.KLAR_TIL_PLUKK, + Status.MANUELL_OPPFØLGING, + ), + Pageable.unpaged(), + ) + mottattXmlIdsMedYtelse.forEach { mottattXmlIdOgYtelse -> + val finnesTask = alleFeiledeTasker.any { + it.payload == mottattXmlIdOgYtelse.id.toString() && + (it.type == HåndterGammelKravgrunnlagTask.TYPE || it.type == HentFagsystemsbehandlingTask.TYPE) + } + if (!finnesTask) { + val fagsystem = FagsystemUtil.hentFagsystemFraYtelsestype(mottattXmlIdOgYtelse.ytelsestype) + taskService.save( + Task( + type = HentFagsystemsbehandlingTask.TYPE, + payload = mottattXmlIdOgYtelse.id.toString(), + properties = Properties().apply { + setProperty( + PropertyName.FAGSYSTEM, + fagsystem.name, + ) + }, + ), + ) + } else { + logger.info( + "Det finnes allerede en feilet HåndterGammelKravgrunnlagTask " + + "eller HentFagsystemsbehandlingTask " + + "på det samme kravgrunnlaget med id ${mottattXmlIdOgYtelse.id}", + ) + } + } + } else { + logger.info("Det finnes ingen kravgrunnlag som er eldre enn $ALDERSGRENSE_I_UKER uker fra dagens dato") + } + logger.info("Stopper HåndterGamleKravgrunnlagBatch..") + } + + private fun beregnBestemtDato(ytelsestype: Ytelsestype): LocalDate { + return LocalDate.now().minusWeeks(ALDERSGRENSE_I_UKER.getValue(ytelsestype)) + } + + companion object { + + val ALDERSGRENSE_I_UKER = mapOf( + BARNETRYGD to 8, + BARNETILSYN to 8, + OVERGANGSSTØNAD to 8, + SKOLEPENGER to 8, + KONTANTSTØTTE to 8, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/batch/H\303\245ndterGamleKravgrunnlagService.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/batch/H\303\245ndterGamleKravgrunnlagService.kt" new file mode 100644 index 000000000..75f0c8c76 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/batch/H\303\245ndterGamleKravgrunnlagService.kt" @@ -0,0 +1,270 @@ +package no.nav.familie.tilbake.kravgrunnlag.batch + +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.tilbakekreving.Behandlingstype +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandling +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.BehandlingService +import no.nav.familie.tilbake.behandling.FagsystemUtil +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.exceptionhandler.KravgrunnlagIkkeFunnetFeil +import no.nav.familie.tilbake.common.exceptionhandler.SperretKravgrunnlagFeil +import no.nav.familie.tilbake.common.exceptionhandler.UgyldigKravgrunnlagFeil +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.historikkinnslag.HistorikkService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.HentKravgrunnlagService +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagMapper +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagUtil +import no.nav.familie.tilbake.kravgrunnlag.domain.Fagområdekode +import no.nav.familie.tilbake.kravgrunnlag.domain.KodeAksjon +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottatt +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottattArkiv +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattService +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagDto +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.UUID + +@Service +class HåndterGamleKravgrunnlagService( + private val behandlingRepository: BehandlingRepository, + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val behandlingService: BehandlingService, + private val behandlingskontrollService: BehandlingskontrollService, + private val økonomiXmlMottattService: ØkonomiXmlMottattService, + private val hentKravgrunnlagService: HentKravgrunnlagService, + private val stegService: StegService, + private val historikkService: HistorikkService, +) { + + private val logger = LoggerFactory.getLogger(this::class.java) + + fun hentFrakobletKravgrunnlag(mottattXmlId: UUID): ØkonomiXmlMottatt { + return økonomiXmlMottattService.hentMottattKravgrunnlag(mottattXmlId) + } + + fun sjekkOmDetFinnesEnAktivBehandling(mottattXml: ØkonomiXmlMottatt) { + val eksternFagsakId = mottattXml.eksternFagsakId + val ytelsestype = mottattXml.ytelsestype + val mottattXmlId = mottattXml.id + + logger.info("Sjekker om det finnes en aktiv behandling for fagsak=$eksternFagsakId og ytelsestype=$ytelsestype") + if (behandlingRepository.finnÅpenTilbakekrevingsbehandling(ytelsestype, eksternFagsakId) != null) { + throw UgyldigKravgrunnlagFeil( + melding = "Kravgrunnlag med $mottattXmlId er ugyldig." + + "Det finnes allerede en åpen behandling for " + + "fagsak=$eksternFagsakId og ytelsestype=$ytelsestype. " + + "Kravgrunnlaget skulle være koblet. Kravgrunnlaget arkiveres manuelt" + + "ved å bruke forvaltningsrutine etter feilundersøkelse.", + ) + } + } + + fun sjekkArkivForDuplikatKravgrunnlagMedKravstatusAvsluttet(kravgrunnlagIkkeFunnet: ØkonomiXmlMottatt): Boolean { + val arkiverteXmlMottattPåSammeFagsak = økonomiXmlMottattService.hentArkiverteMottattXml( + eksternFagsakId = kravgrunnlagIkkeFunnet.eksternFagsakId, + ytelsestype = kravgrunnlagIkkeFunnet.ytelsestype, + ) + val arkiverteKravgrunnlag = arkiverteXmlMottattPåSammeFagsak + .filter { it.melding.contains(Constants.kravgrunnlagXmlRootElement) } + val arkiverteStatusmeldinger = arkiverteXmlMottattPåSammeFagsak + .filter { it.melding.contains(Constants.statusmeldingXmlRootElement) } + + return arkiverteKravgrunnlag + .any { arkivertKravgrunnlag -> + arkivertKravgrunnlag.sporbar.opprettetTid.isAfter(kravgrunnlagIkkeFunnet.sporbar.opprettetTid) && + sjekkDiff( + arkivertXml = arkivertKravgrunnlag, + mottattXml = kravgrunnlagIkkeFunnet, + forventedeAvvik = listOf("kravgrunnlagId", "vedtakId", "kontrollfelt"), + ) && + arkivertKravgrunnlag.harKravstatusAvsluttet(arkiverteStatusmeldinger) + } + } + + @Transactional(rollbackFor = [Exception::class]) + fun håndter(fagsystemsbehandlingData: HentFagsystemsbehandling, mottattXml: ØkonomiXmlMottatt, task: Task) { + logger.info("Håndterer kravgrunnlag med kravgrunnlagId=${mottattXml.eksternKravgrunnlagId}") + val hentetData: Pair = try { + hentKravgrunnlagFraØkonomi(mottattXml) + } catch (e: KravgrunnlagIkkeFunnetFeil) { + if (sjekkArkivForDuplikatKravgrunnlagMedKravstatusAvsluttet(kravgrunnlagIkkeFunnet = mottattXml)) { + logger.warn( + "Kravgrunnlag(id=${mottattXml.id}, eksternFagsakId=${mottattXml.eksternFagsakId}) ble ikke funnet hos økonomi," + + " men identisk kravgrunnlag med påfølgende melding om at kravet er avsluttet ble funnet i arkivet.", + ) + arkiverKravgrunnlag(mottattXml.id) + task.metadata["merknad"] = + "Arkivert da kravgrunnlag ikke ble funnet hos økonomi, og duplikat kravgrunnlag med kravstatus AVSLUTTET funnet i arkivet" + return + } else { + throw e + } + } + val hentetKravgrunnlag = hentetData.first + val erSperret = hentetData.second + + arkiverKravgrunnlag(mottattXml.id) + val behandling = opprettBehandling(hentetKravgrunnlag, fagsystemsbehandlingData) + val behandlingId = behandling.id + + val mottattKravgrunnlag = KravgrunnlagUtil.unmarshalKravgrunnlag(mottattXml.melding) + val diffs = KravgrunnlagUtil.sammenlignKravgrunnlag(mottattKravgrunnlag, hentetKravgrunnlag) + if (diffs.isNotEmpty()) { + logger.warn("Det finnes avvik mellom hentet kravgrunnlag og mottatt kravgrunnlag for ${hentetKravgrunnlag.kodeFagomraade}. Avvikene er $diffs") + } + logger.info( + "Kobler kravgrunnlag med kravgrunnlagId=${hentetKravgrunnlag.kravgrunnlagId} " + + "til behandling=$behandlingId", + ) + val kravgrunnlag = KravgrunnlagMapper.tilKravgrunnlag431(hentetKravgrunnlag, behandlingId) + kravgrunnlagRepository.insert(kravgrunnlag) + + historikkService.lagHistorikkinnslag( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_HENT, + aktør = Aktør.VEDTAKSLØSNING, + opprettetTidspunkt = LocalDateTime.now(), + ) + + stegService.håndterSteg(behandlingId) + if (erSperret) { + logger.info( + "Hentet kravgrunnlag med kravgrunnlagId=${hentetKravgrunnlag.kravgrunnlagId} " + + "til behandling=$behandlingId er sperret. Venter behandlingen på ny kravgrunnlag fra økonomi", + ) + sperKravgrunnlag(behandlingId) + } + } + + @Transactional + fun arkiverKravgrunnlag(mottattXmlId: UUID) { + val mottattXml = hentFrakobletKravgrunnlag(mottattXmlId) + økonomiXmlMottattService.arkiverMottattXml(mottattXml.melding, mottattXml.eksternFagsakId, mottattXml.ytelsestype) + økonomiXmlMottattService.slettMottattXml(mottattXmlId) + } + + private fun hentKravgrunnlagFraØkonomi(mottattXml: ØkonomiXmlMottatt): Pair { + return try { + hentKravgrunnlagService.hentKravgrunnlagFraØkonomi( + mottattXml.eksternKravgrunnlagId!!, + KodeAksjon.HENT_KORRIGERT_KRAVGRUNNLAG, + ) to false + } catch (e: SperretKravgrunnlagFeil) { + logger.warn(e.melding) + KravgrunnlagUtil.unmarshalKravgrunnlag(mottattXml.melding) to true + } + } + + fun opprettBehandling( + hentetKravgrunnlag: DetaljertKravgrunnlagDto, + fagsystemsbehandlingData: HentFagsystemsbehandling, + ): Behandling { + val opprettTilbakekrevingRequest = + lagOpprettBehandlingsrequest( + eksternFagsakId = hentetKravgrunnlag.fagsystemId, + ytelsestype = Fagområdekode.fraKode(hentetKravgrunnlag.kodeFagomraade) + .ytelsestype, + eksternId = hentetKravgrunnlag.referanse, + fagsystemsbehandlingData = fagsystemsbehandlingData, + ) + return behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + } + + private fun lagOpprettBehandlingsrequest( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + eksternId: String, + fagsystemsbehandlingData: HentFagsystemsbehandling, + ): OpprettTilbakekrevingRequest { + return OpprettTilbakekrevingRequest( + fagsystem = FagsystemUtil.hentFagsystemFraYtelsestype(ytelsestype), + ytelsestype = ytelsestype, + eksternFagsakId = eksternFagsakId, + eksternId = eksternId, + behandlingstype = Behandlingstype.TILBAKEKREVING, + manueltOpprettet = false, + saksbehandlerIdent = "VL", + personIdent = fagsystemsbehandlingData.personIdent, + språkkode = fagsystemsbehandlingData.språkkode, + enhetId = fagsystemsbehandlingData.enhetId, + enhetsnavn = fagsystemsbehandlingData.enhetsnavn, + revurderingsvedtaksdato = fagsystemsbehandlingData.revurderingsvedtaksdato, + faktainfo = setFaktainfo(fagsystemsbehandlingData.faktainfo), + verge = fagsystemsbehandlingData.verge, + varsel = null, + ) + } + + private fun sperKravgrunnlag(behandlingId: UUID) { + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + kravgrunnlagRepository.update(kravgrunnlag.copy(sperret = true)) + val venteårsak = Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG + behandlingskontrollService + .tilbakehoppBehandlingssteg( + behandlingId, + Behandlingsstegsinfo( + behandlingssteg = Behandlingssteg.GRUNNLAG, + behandlingsstegstatus = Behandlingsstegstatus.VENTER, + venteårsak = venteårsak, + tidsfrist = LocalDate.now() + .plusWeeks(venteårsak.defaultVenteTidIUker), + ), + ) + historikkService.lagHistorikkinnslag( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT, + aktør = Aktør.VEDTAKSLØSNING, + beskrivelse = venteårsak.beskrivelse, + opprettetTidspunkt = LocalDateTime.now(), + ) + } + + private fun setFaktainfo(faktainfo: Faktainfo): Faktainfo { + return Faktainfo( + revurderingsresultat = faktainfo.revurderingsresultat, + revurderingsårsak = faktainfo.revurderingsårsak, + tilbakekrevingsvalg = Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING, + konsekvensForYtelser = faktainfo.konsekvensForYtelser, + ) + } + + private fun sjekkDiff( + arkivertXml: ØkonomiXmlMottattArkiv, + mottattXml: ØkonomiXmlMottatt, + forventedeAvvik: List, + ) = arkivertXml.melding.linjeformatert.lines().minus(mottattXml.melding.linjeformatert.lines()).none { avvik -> + forventedeAvvik.none { it in avvik } + } +} + +private val String.linjeformatert: String + get() = replace("): Boolean { + val kravgrunnlagDto = KravgrunnlagUtil.unmarshalKravgrunnlag(melding) + + return statusmeldingerMottatt.any { + KravgrunnlagUtil.unmarshalStatusmelding(it.melding).let { statusmelding -> + statusmelding.vedtakId == kravgrunnlagDto.vedtakId && + statusmelding.kodeStatusKrav == Kravstatuskode.AVSLUTTET.kode + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/batch/H\303\245ndterGammelKravgrunnlagTask.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/batch/H\303\245ndterGammelKravgrunnlagTask.kt" new file mode 100644 index 000000000..27ba66018 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/batch/H\303\245ndterGammelKravgrunnlagTask.kt" @@ -0,0 +1,66 @@ +package no.nav.familie.tilbake.kravgrunnlag.batch + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingService +import no.nav.familie.tilbake.common.exceptionhandler.UkjentravgrunnlagFeil +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = HåndterGammelKravgrunnlagTask.TYPE, + beskrivelse = "Håndter frakoblet gammel kravgrunnlag som er eldre enn en bestemt dato", + maxAntallFeil = 3, + triggerTidVedFeilISekunder = 60 * 5L, +) +class HåndterGammelKravgrunnlagTask( + private val håndterGamleKravgrunnlagService: HåndterGamleKravgrunnlagService, + private val hentFagsystemsbehandlingService: HentFagsystemsbehandlingService, +) : + AsyncTaskStep { + + private val logger = LoggerFactory.getLogger(this::class.java) + + @Transactional + override fun doTask(task: Task) { + logger.info("HåndterGammelKravgrunnlagTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val mottattXmlId = UUID.fromString(task.payload) + val mottattXml = håndterGamleKravgrunnlagService.hentFrakobletKravgrunnlag(mottattXmlId) + val eksternFagsakId = mottattXml.eksternFagsakId + val ytelsestype = mottattXml.ytelsestype + val eksternId = mottattXml.referanse + + val requestSendt = requireNotNull( + hentFagsystemsbehandlingService.hentFagsystemsbehandlingRequestSendt( + eksternFagsakId, + ytelsestype, + eksternId, + ), + ) + // kaster exception inntil respons-en har mottatt + val respons = requireNotNull(requestSendt.respons) { + "HentFagsystemsbehandling respons-en har ikke mottatt fra fagsystem for " + + "eksternFagsakId=$eksternFagsakId,ytelsestype=$ytelsestype,eksternId=$eksternId." + + "Task-en kan kjøre på nytt manuelt når respons-en er mottatt." + } + + val hentFagsystemsbehandlingRespons = hentFagsystemsbehandlingService.lesRespons(respons) + val feilMelding = hentFagsystemsbehandlingRespons.feilMelding + if (feilMelding != null) { + throw UkjentravgrunnlagFeil( + "Noen gikk galt mens henter fagsystemsbehandling fra fagsystem. " + + "Feiler med $feilMelding", + ) + } + håndterGamleKravgrunnlagService.håndter(hentFagsystemsbehandlingRespons.hentFagsystemsbehandling!!, mottattXml, task) + } + + companion object { + + const val TYPE = "gammelKravgrunnlag.håndter" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/Fagomr\303\245dekode.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/Fagomr\303\245dekode.kt" new file mode 100644 index 000000000..7d192b5ae --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/Fagomr\303\245dekode.kt" @@ -0,0 +1,25 @@ +package no.nav.familie.tilbake.kravgrunnlag.domain + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype + +enum class Fagområdekode(val navn: String, val ytelsestype: Ytelsestype) { + + BA("Barnetrygd", Ytelsestype.BARNETRYGD), + KS("Kontantstøtte", Ytelsestype.KONTANTSTØTTE), + EFOG("Enslig forelder - Overgangsstønad", Ytelsestype.OVERGANGSSTØNAD), + EFBT("Enslig forelder - Barnetilsyn", Ytelsestype.BARNETILSYN), + EFSP("Enslig forelder - Skolepenger", Ytelsestype.SKOLEPENGER), + ; + + companion object { + + fun fraKode(kode: String): Fagområdekode { + for (fagområdekode in values()) { + if (fagområdekode.name == kode) { + return fagområdekode + } + } + throw IllegalArgumentException("Ukjent Fagområdekode $kode") + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/GjelderType.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/GjelderType.kt new file mode 100644 index 000000000..bf7fda674 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/GjelderType.kt @@ -0,0 +1,22 @@ +package no.nav.familie.tilbake.kravgrunnlag.domain + +enum class GjelderType(val navn: String) { + + PERSON("Person"), + ORGANISASJON("Organisasjon"), + SAMHANDLER("Samhandler"), + APPLIKASJONSBRUKER("Applikasjonsbruker"), + ; + + companion object { + + fun fraKode(kode: String): GjelderType { + for (gjelderType in values()) { + if (gjelderType.name == kode) { + return gjelderType + } + } + throw IllegalArgumentException("Ukjent GjelderType $kode") + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/KodeAksjon.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/KodeAksjon.kt new file mode 100644 index 000000000..cf6139ec3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/KodeAksjon.kt @@ -0,0 +1,9 @@ +package no.nav.familie.tilbake.kravgrunnlag.domain + +enum class KodeAksjon(val kode: String) { + FINN_GRUNNLAG_OMGJØRING("3"), + HENT_KORRIGERT_KRAVGRUNNLAG("4"), + HENT_GRUNNLAG_OMGJØRING("5"), + FATTE_VEDTAK("8"), + ANNULERE_GRUNNLAG("A"), +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/Kravgrunnlag431.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/Kravgrunnlag431.kt new file mode 100644 index 000000000..15d60a32c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/Kravgrunnlag431.kt @@ -0,0 +1,182 @@ +package no.nav.familie.tilbake.kravgrunnlag.domain + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Embedded +import org.springframework.data.relational.core.mapping.MappedCollection +import org.springframework.data.relational.core.mapping.Table +import java.math.BigDecimal +import java.math.BigInteger +import java.time.LocalDate +import java.util.Objects +import java.util.UUID + +data class Kravgrunnlag431( + @Id + val id: UUID = UUID.randomUUID(), + val behandlingId: UUID, + val aktiv: Boolean = true, + val sperret: Boolean = false, + val avsluttet: Boolean = false, + val vedtakId: BigInteger, + val omgjortVedtakId: BigInteger? = null, + val kravstatuskode: Kravstatuskode, + @Column("fagomradekode") + val fagområdekode: Fagområdekode, + val fagsystemId: String, + val fagsystemVedtaksdato: LocalDate? = null, + val gjelderVedtakId: String, + val gjelderType: GjelderType, + val utbetalesTilId: String, + val utbetIdType: GjelderType, + val hjemmelkode: String? = null, + val beregnesRenter: Boolean? = null, + val ansvarligEnhet: String, + val bostedsenhet: String, + val behandlingsenhet: String, + val kontrollfelt: String, + val saksbehandlerId: String, + val referanse: String, + val eksternKravgrunnlagId: BigInteger, + @MappedCollection(idColumn = "kravgrunnlag431_id") + val perioder: Set = setOf(), + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +data class Kravgrunnlagsperiode432( + @Id + val id: UUID = UUID.randomUUID(), + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val periode: Månedsperiode, + @Column("manedlig_skattebelop") + val månedligSkattebeløp: BigDecimal, + @MappedCollection(idColumn = "kravgrunnlagsperiode432_id") + val beløp: Set = setOf(), + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) { + + fun harIdentiskKravgrunnlagsperiode(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val that = other as Kravgrunnlagsperiode432 + return this.månedligSkattebeløp == that.månedligSkattebeløp && + this.periode == that.periode && + this.beløp == that.beløp + } +} + +@Table("kravgrunnlagsbelop433") +data class Kravgrunnlagsbeløp433( + @Id + val id: UUID = UUID.randomUUID(), + val klassekode: Klassekode, + val klassetype: Klassetype, + @Column("opprinnelig_utbetalingsbelop") + val opprinneligUtbetalingsbeløp: BigDecimal, + @Column("nytt_belop") + val nyttBeløp: BigDecimal, + @Column("tilbakekreves_belop") + val tilbakekrevesBeløp: BigDecimal, + @Column("uinnkrevd_belop") + val uinnkrevdBeløp: BigDecimal? = null, + val resultatkode: String? = null, + @Column("arsakskode") + val årsakskode: String? = null, + val skyldkode: String? = null, + val skatteprosent: BigDecimal, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val that = other as Kravgrunnlagsbeløp433 + return this.klassekode == that.klassekode && + this.klassetype == that.klassetype && + this.nyttBeløp == that.nyttBeløp && + this.opprinneligUtbetalingsbeløp == that.opprinneligUtbetalingsbeløp && + this.uinnkrevdBeløp == that.uinnkrevdBeløp && + this.tilbakekrevesBeløp == that.tilbakekrevesBeløp && + this.resultatkode == that.resultatkode && + this.skatteprosent == that.skatteprosent && + this.skyldkode == that.skyldkode && + this.årsakskode == that.årsakskode + } + + override fun hashCode(): Int { + return Objects.hash( + klassekode, + klassetype, + nyttBeløp, + opprinneligUtbetalingsbeløp, + uinnkrevdBeløp, + tilbakekrevesBeløp, + resultatkode, + skatteprosent, + skyldkode, + årsakskode, + ) + } +} + +enum class Klassekode(val aktivitet: String) { + KL_KODE_FEIL_BA(""), + KL_KODE_FEIL_EFOG(""), + KL_KODE_FEIL_PEN(""), + KL_KODE_FEIL_KS(""), // Kontantstøtte + KL_KODE_JUST_BA(""), + KL_KODE_JUST_KS(""), + KL_KODE_JUST_EFOG(""), + KL_KODE_JUST_PEN(""), + BATR("Barnetrygd"), + BATRSMA("Småbarnstillegg"), + BAOREUMS("Barnetrygd-EU-Norge-Infotrygd"), + BAOROSMS("Barnetrygd-Infotrygd"), + BAUTEFMS("BarnetrygdUtvidet-Infotrygd"), + BAUTEFSM("Småbarnstillegg-Infotrygd"), + BAUTMDMS("Barnetrygd-Utvidet-Delt-Bosted"), + EFOG("Overgangsstønad"), + EFBT("Barnetilsyn"), + EFBTOR("Barnetilsyn-Infotrygd"), + EFSP("Skolepenger"), + KS("Kontantstøtte"), + TREK_KODER(""), // Felles klassekode for alle TREK klassetyper + ; + companion object { + + fun fraKode(kode: String, klassetype: Klassetype): Klassekode { + if (klassetype == Klassetype.TREK) return TREK_KODER + return values().firstOrNull { it.name == kode } + ?: throw IllegalArgumentException("Ukjent KlasseKode $kode") + } + } +} + +enum class Klassetype(val navn: String) { + FEIL("Feilkonto"), + JUST("Justeringskonto"), + SKAT("Skatt"), + TREK("Trekk"), + YTEL("Ytelseskonto"), + ; + + companion object { + + fun fraKode(kode: String): Klassetype { + return values().firstOrNull { it.name == kode } + ?: throw IllegalArgumentException("Ukjent Klassetype $kode") + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/Kravstatuskode.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/Kravstatuskode.kt new file mode 100644 index 000000000..29c5a34d9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/Kravstatuskode.kt @@ -0,0 +1,31 @@ +package no.nav.familie.tilbake.kravgrunnlag.domain + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonValue + +enum class Kravstatuskode(@JsonValue val kode: String, val navn: String) { + + ANNULERT("ANNU", "Kravgrunnlag annullert"), + ANNULLERT_OMG("ANOM", "Kravgrunnlag annullert ved omg"), + AVSLUTTET("AVSL", "Avsluttet kravgrunnlag"), + BEHANDLET("BEHA", "Kravgrunnlag ferdigbehandlet"), + ENDRET("ENDR", "Endret kravgrunnlag"), + FEIL("FEIL", "Feil på kravgrunnlag"), + MANUELL("MANU", "Manuell behandling"), + NYTT("NY", "Nytt kravgrunnlag"), + SPERRET("SPER", "Kravgrunnlag sperret"), + ; + + companion object { + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + fun fraKode(kode: String): Kravstatuskode { + for (kravstatuskode in values()) { + if (kode == kravstatuskode.kode) { + return kravstatuskode + } + } + throw IllegalArgumentException("Kravstatuskode finnes ikke for kode $kode") + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/\303\230konomiXmlMottatt.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/\303\230konomiXmlMottatt.kt" new file mode 100644 index 000000000..974cb0fc6 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/\303\230konomiXmlMottatt.kt" @@ -0,0 +1,29 @@ +package no.nav.familie.tilbake.kravgrunnlag.domain + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import org.springframework.data.relational.core.mapping.Table +import java.math.BigInteger +import java.util.UUID + +@Table("okonomi_xml_mottatt") +data class ØkonomiXmlMottatt( + @Id + val id: UUID = UUID.randomUUID(), + val melding: String, + val kravstatuskode: Kravstatuskode, + val eksternFagsakId: String, + val ytelsestype: Ytelsestype, + val referanse: String, + val eksternKravgrunnlagId: BigInteger?, + val vedtakId: BigInteger, + val kontrollfelt: String?, + val sperret: Boolean = false, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/\303\230konomiXmlMottattArkiv.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/\303\230konomiXmlMottattArkiv.kt" new file mode 100644 index 000000000..6c383ce2c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/\303\230konomiXmlMottattArkiv.kt" @@ -0,0 +1,22 @@ +package no.nav.familie.tilbake.kravgrunnlag.domain + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import org.springframework.data.relational.core.mapping.Table +import java.util.UUID + +@Table("okonomi_xml_mottatt_arkiv") +data class ØkonomiXmlMottattArkiv( + @Id + val id: UUID = UUID.randomUUID(), + val melding: String, + val eksternFagsakId: String, + val ytelsestype: Ytelsestype, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/\303\230konomiXmlMottattIdOgYtelse.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/\303\230konomiXmlMottattIdOgYtelse.kt" new file mode 100644 index 000000000..b0b0fdf28 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/domain/\303\230konomiXmlMottattIdOgYtelse.kt" @@ -0,0 +1,6 @@ +package no.nav.familie.tilbake.kravgrunnlag.domain + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import java.util.UUID + +class ØkonomiXmlMottattIdOgYtelse(val id: UUID, val ytelsestype: Ytelsestype) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/event/EndretKravgrunnlagEvent.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/event/EndretKravgrunnlagEvent.kt new file mode 100644 index 000000000..47ad0db83 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/event/EndretKravgrunnlagEvent.kt @@ -0,0 +1,6 @@ +package no.nav.familie.tilbake.kravgrunnlag.event + +import org.springframework.context.ApplicationEvent +import java.util.UUID + +class EndretKravgrunnlagEvent(source: Any, val behandlingId: UUID) : ApplicationEvent(source) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/event/EndretKravgrunnlagEventPublisher.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/event/EndretKravgrunnlagEventPublisher.kt new file mode 100644 index 000000000..070a1a95e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/event/EndretKravgrunnlagEventPublisher.kt @@ -0,0 +1,14 @@ +package no.nav.familie.tilbake.kravgrunnlag.event + +import org.springframework.context.ApplicationEventPublisher +import org.springframework.stereotype.Component +import java.util.UUID + +@Component +class EndretKravgrunnlagEventPublisher(val applicationEventPublisher: ApplicationEventPublisher) { + + fun fireEvent(behandlingId: UUID) { + val endretKravgrunnlagEvent = EndretKravgrunnlagEvent(this, behandlingId) + applicationEventPublisher.publishEvent(endretKravgrunnlagEvent) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/task/BehandleKravgrunnlagTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/task/BehandleKravgrunnlagTask.kt new file mode 100644 index 000000000..fc8efe50b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/task/BehandleKravgrunnlagTask.kt @@ -0,0 +1,32 @@ +package no.nav.familie.tilbake.kravgrunnlag.task + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +@TaskStepBeskrivelse( + taskStepType = BehandleKravgrunnlagTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Håndter mottatt kravgrunnlag fra oppdrag", + triggerTidVedFeilISekunder = 60 * 5L, +) +class BehandleKravgrunnlagTask(private val kravgrunnlagService: KravgrunnlagService) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + private val secureLog = LoggerFactory.getLogger("secureLogger") + + override fun doTask(task: Task) { + log.info("BehandleKravgrunnlagTask prosesserer med id=${task.id} og metadata ${task.metadata}") + secureLog.info("BehandleKravgrunnlagTask prosesserer med id=${task.id} og metadata ${task.metadata}") + kravgrunnlagService.håndterMottattKravgrunnlag(task.payload) + } + + companion object { + + const val TYPE = "behandleKravgrunnlag" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/task/BehandleStatusmeldingTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/task/BehandleStatusmeldingTask.kt new file mode 100644 index 000000000..97ffd441a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/task/BehandleStatusmeldingTask.kt @@ -0,0 +1,32 @@ +package no.nav.familie.tilbake.kravgrunnlag.task + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.kravgrunnlag.KravvedtakstatusService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +@TaskStepBeskrivelse( + taskStepType = BehandleStatusmeldingTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Håndter mottatt statusmelding fra oppdrag", + triggerTidVedFeilISekunder = 60 * 5L, +) +class BehandleStatusmeldingTask(private val kravvedtakstatusService: KravvedtakstatusService) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + private val secureLog = LoggerFactory.getLogger("secureLogger") + + override fun doTask(task: Task) { + log.info("BehandleStatusmeldingTask prosesserer med id=${task.id} og metadata ${task.metadata}") + secureLog.info("BehandleStatusmeldingTask prosesserer med id=${task.id} og metadata ${task.metadata}") + kravvedtakstatusService.håndterMottattStatusmelding(task.payload) + } + + companion object { + + const val TYPE = "behandleStatusmelding" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/task/FinnKravgrunnlagTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/task/FinnKravgrunnlagTask.kt new file mode 100644 index 000000000..bd91e7cf7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/task/FinnKravgrunnlagTask.kt @@ -0,0 +1,61 @@ +package no.nav.familie.tilbake.kravgrunnlag.task + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagService +import no.nav.familie.tilbake.kravgrunnlag.KravvedtakstatusService +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattRepository +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = FinnKravgrunnlagTask.TYPE, + beskrivelse = "Finner frakoblet grunnlag og statusmeldinger for samme fagsak " + + "og kobler dem til behandling", + triggerTidVedFeilISekunder = 60 * 5L, +) +class FinnKravgrunnlagTask( + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, + private val økonomiXmlMottattRepository: ØkonomiXmlMottattRepository, + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val kravgrunnlagService: KravgrunnlagService, + private val kravvedtakstatusService: KravvedtakstatusService, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("FinnKravgrunnlagTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val behandlingId = UUID.fromString(task.payload) + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + + val mottattKravgrunnlagene = økonomiXmlMottattRepository + .findByEksternFagsakIdAndYtelsestype(fagsak.eksternFagsakId, fagsak.ytelsestype) + .sortedBy { it.sporbar.opprettetTid } + mottattKravgrunnlagene.forEach { mottattKravgrunnlag -> + kravgrunnlagService.håndterMottattKravgrunnlag(mottattKravgrunnlag.melding) + if (mottattKravgrunnlag.sperret) { + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + kravvedtakstatusService.håndterSperMeldingMedBehandling(behandlingId, kravgrunnlag) + } + // Fjern mottatt xml om det koblet med behandlingen + if (kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandlingId)) { + økonomiXmlMottattRepository.deleteById(mottattKravgrunnlag.id) + } + } + } + + companion object { + + const val TYPE = "finnKravgrunnlag" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/task/HentKravgrunnlagTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/task/HentKravgrunnlagTask.kt new file mode 100644 index 000000000..b856e3a2c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/task/HentKravgrunnlagTask.kt @@ -0,0 +1,58 @@ +package no.nav.familie.tilbake.kravgrunnlag.task + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.kravgrunnlag.HentKravgrunnlagService +import no.nav.familie.tilbake.kravgrunnlag.domain.KodeAksjon +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = HentKravgrunnlagTask.TYPE, + beskrivelse = "Henter kravgrunnlag fra økonomi", + triggerTidVedFeilISekunder = 300L, +) +class HentKravgrunnlagTask( + private val behandlingRepository: BehandlingRepository, + private val hentKravgrunnlagService: HentKravgrunnlagService, + private val stegService: StegService, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + @Transactional + override fun doTask(task: Task) { + log.info("HentKravgrunnlagTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val behandlingId = UUID.fromString(task.payload) + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + if (behandling.type != Behandlingstype.REVURDERING_TILBAKEKREVING) { + throw Feil(message = "HentKravgrunnlagTask kan kjøres bare for tilbakekrevingsrevurdering.") + } + val originalBehandlingId = requireNotNull(behandling.sisteÅrsak?.originalBehandlingId) + + val tilbakekrevingsgrunnlag = hentKravgrunnlagService.hentTilbakekrevingskravgrunnlag(originalBehandlingId) + val hentetKravgrunnlag = hentKravgrunnlagService.hentKravgrunnlagFraØkonomi( + tilbakekrevingsgrunnlag.eksternKravgrunnlagId, + KodeAksjon.HENT_GRUNNLAG_OMGJØRING, + ) + hentKravgrunnlagService.lagreHentetKravgrunnlag(behandlingId, hentetKravgrunnlag) + + hentKravgrunnlagService.opprettHistorikkinnslag(behandlingId) + + stegService.håndterSteg(behandlingId) + } + + companion object { + + const val TYPE = "hentKravgrunnlag" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattArkivRepository.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattArkivRepository.kt" new file mode 100644 index 000000000..81e154ad3 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattArkivRepository.kt" @@ -0,0 +1,18 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottattArkiv +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Repository +@Transactional +interface ØkonomiXmlMottattArkivRepository : + RepositoryInterface<ØkonomiXmlMottattArkiv, UUID>, + InsertUpdateRepository<ØkonomiXmlMottattArkiv> { + + fun findByEksternFagsakIdAndYtelsestype(eksternFagsakId: String, ytelsestype: Ytelsestype): List<ØkonomiXmlMottattArkiv> +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattRepository.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattRepository.kt" new file mode 100644 index 000000000..517fa3748 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattRepository.kt" @@ -0,0 +1,61 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottatt +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottattIdOgYtelse +import org.springframework.data.jdbc.repository.query.Query +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional +import java.math.BigInteger +import java.time.LocalDate +import java.util.UUID + +@Repository +@Transactional +interface ØkonomiXmlMottattRepository : RepositoryInterface<ØkonomiXmlMottatt, UUID>, InsertUpdateRepository<ØkonomiXmlMottatt> { + + fun findByEksternKravgrunnlagIdAndVedtakId(eksternKravgrunnlagId: BigInteger, vedtakId: BigInteger): List<ØkonomiXmlMottatt> + + fun findByEksternFagsakIdAndYtelsestypeAndVedtakId( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + vedtakId: BigInteger, + ): List<ØkonomiXmlMottatt> + + fun findByEksternFagsakIdAndYtelsestype( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + ): List<ØkonomiXmlMottatt> + + fun existsByEksternFagsakIdAndYtelsestypeAndReferanse( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + referanse: String, + ): Boolean + + fun findByEksternKravgrunnlagId(eksternKravgrunnlagId: BigInteger): ØkonomiXmlMottatt? + + // language=PostgreSQL + @Query( + """ + SELECT oko.id, oko.ytelsestype + FROM okonomi_xml_mottatt oko + WHERE CASE (ytelsestype) + WHEN 'BARNETRYGD' THEN opprettet_tid < :barnetrygdBestemtDato + WHEN 'BARNETILSYN' THEN opprettet_tid < :barnetilsynBestemtDato + WHEN 'OVERGANGSSTØNAD' THEN opprettet_tid < :overgangsstonadbestemtdato + WHEN 'SKOLEPENGER' THEN opprettet_tid < :skolePengerBestemtDato + WHEN 'KONTANTSTØTTE' THEN opprettet_tid < :kontantstottebestemtdato + END + """, + ) + fun hentFrakobletGamleMottattXmlIds( + barnetrygdBestemtDato: LocalDate, + barnetilsynBestemtDato: LocalDate, + overgangsstonadbestemtdato: LocalDate, + skolePengerBestemtDato: LocalDate, + kontantstottebestemtdato: LocalDate, + ): List<ØkonomiXmlMottattIdOgYtelse> +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattService.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattService.kt" new file mode 100644 index 000000000..271fc182f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattService.kt" @@ -0,0 +1,127 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottatt +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottattArkiv +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottattIdOgYtelse +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagDto +import org.springframework.stereotype.Service +import java.math.BigInteger +import java.time.LocalDate +import java.util.UUID + +@Service +class ØkonomiXmlMottattService( + private val mottattXmlRepository: ØkonomiXmlMottattRepository, + private val mottattXmlArkivRepository: ØkonomiXmlMottattArkivRepository, +) { + + fun lagreMottattXml( + kravgrunnlagXml: String, + kravgrunnlag: DetaljertKravgrunnlagDto, + ytelsestype: Ytelsestype, + ) { + mottattXmlRepository.insert( + ØkonomiXmlMottatt( + melding = kravgrunnlagXml, + kravstatuskode = Kravstatuskode.fraKode(kravgrunnlag.kodeStatusKrav), + eksternFagsakId = kravgrunnlag.fagsystemId, + ytelsestype = ytelsestype, + referanse = kravgrunnlag.referanse, + eksternKravgrunnlagId = kravgrunnlag.kravgrunnlagId, + vedtakId = kravgrunnlag.vedtakId, + kontrollfelt = kravgrunnlag.kontrollfelt, + ), + ) + } + + fun hentMottattKravgrunnlag(eksternKravgrunnlagId: BigInteger, vedtakId: BigInteger): List<ØkonomiXmlMottatt> { + return mottattXmlRepository.findByEksternKravgrunnlagIdAndVedtakId(eksternKravgrunnlagId, vedtakId) + } + + fun arkiverEksisterendeGrunnlag(kravgrunnlag: DetaljertKravgrunnlagDto) { + val eksisterendeKravgrunnlag: List<ØkonomiXmlMottatt> = + hentMottattKravgrunnlag( + eksternKravgrunnlagId = kravgrunnlag.kravgrunnlagId, + vedtakId = kravgrunnlag.vedtakId, + ) + eksisterendeKravgrunnlag.forEach { + arkiverMottattXml( + mottattXml = it.melding, + fagsystemId = it.eksternFagsakId, + ytelsestype = it.ytelsestype, + ) + } + eksisterendeKravgrunnlag.forEach { slettMottattXml(it.id) } + } + + fun hentMottattKravgrunnlag( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + vedtakId: BigInteger, + ): List<ØkonomiXmlMottatt> { + val mottattXmlListe = mottattXmlRepository + .findByEksternFagsakIdAndYtelsestypeAndVedtakId(eksternFagsakId, ytelsestype, vedtakId) + val kravgrunnlagXmlListe = mottattXmlListe.filter { it.melding.contains(Constants.kravgrunnlagXmlRootElement) } + if (kravgrunnlagXmlListe.isEmpty()) { + throw Feil( + message = "Det finnes intet kravgrunnlag for fagsystemId=$eksternFagsakId og " + + "ytelsestype=$ytelsestype", + ) + } + return kravgrunnlagXmlListe + } + + fun hentFrakobletGamleMottattXmlIds( + barnetrygdBestemtDato: LocalDate, + barnetilsynBestemtDato: LocalDate, + overgangsstønadBestemtDato: LocalDate, + skolePengerBestemtDato: LocalDate, + kontantStøtteBestemtDato: LocalDate, + ): List<ØkonomiXmlMottattIdOgYtelse> { + return mottattXmlRepository.hentFrakobletGamleMottattXmlIds( + barnetrygdBestemtDato = barnetrygdBestemtDato, + barnetilsynBestemtDato = barnetilsynBestemtDato, + overgangsstonadbestemtdato = overgangsstønadBestemtDato, + skolePengerBestemtDato = skolePengerBestemtDato, + kontantstottebestemtdato = kontantStøtteBestemtDato, + ) + } + + fun hentMottattKravgrunnlag(mottattXmlId: UUID): ØkonomiXmlMottatt { + return mottattXmlRepository.findByIdOrThrow(mottattXmlId) + } + + fun oppdaterMottattXml(mottattXml: ØkonomiXmlMottatt) { + mottattXmlRepository.update(mottattXml) + } + + fun slettMottattXml(mottattXmlId: UUID) { + mottattXmlRepository.deleteById(mottattXmlId) + } + + fun arkiverMottattXml( + mottattXml: String, + fagsystemId: String, + ytelsestype: Ytelsestype, + ) { + mottattXmlArkivRepository.insert( + ØkonomiXmlMottattArkiv( + melding = mottattXml, + eksternFagsakId = fagsystemId, + ytelsestype = ytelsestype, + ), + ) + } + + fun hentArkiverteMottattXml( + eksternFagsakId: String, + ytelsestype: Ytelsestype, + ): List<ØkonomiXmlMottattArkiv> { + return mottattXmlArkivRepository.findByEksternFagsakIdAndYtelsestype(eksternFagsakId, ytelsestype) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/M\303\245lerService.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/M\303\245lerService.kt" new file mode 100644 index 000000000..87f0940b8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/M\303\245lerService.kt" @@ -0,0 +1,260 @@ +package no.nav.familie.tilbake.micrometer + +import io.micrometer.core.instrument.Metrics +import io.micrometer.core.instrument.MultiGauge +import io.micrometer.core.instrument.Tags +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.leader.LeaderClient +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultat +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.common.fagsystem +import no.nav.familie.tilbake.micrometer.domain.MeldingstellingRepository +import no.nav.familie.tilbake.micrometer.domain.Meldingstype +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service + +@Service +class MålerService( + private val meldingstellingRepository: MeldingstellingRepository, + private val taskService: TaskService, +) { + + private val åpneBehandlingerGauge = MultiGauge.builder("UavsluttedeBehandlinger").register(Metrics.globalRegistry) + private val klarTilBehandlingGauge = MultiGauge.builder("KlarTilBehandling").register(Metrics.globalRegistry) + private val ventendeBehandlingGauge = MultiGauge.builder("VentendeBehandlinger").register(Metrics.globalRegistry) + private val sendteBrevGauge = MultiGauge.builder("SendteBrev").register(Metrics.globalRegistry) + private val vedtakGauge = MultiGauge.builder("Vedtak").register(Metrics.globalRegistry) + private val mottatteKravgrunnlagGauge = MultiGauge.builder("MottatteKravgrunnlag").register(Metrics.globalRegistry) + private val mottatteStatusmeldingerGauge = MultiGauge.builder("mottatteStatusmeldinger").register(Metrics.globalRegistry) + private val feiledeTasker = MultiGauge.builder("FeiledeTasker").register(Metrics.globalRegistry) + + private val logger = LoggerFactory.getLogger(this::class.java) + + @Scheduled(initialDelay = 60000L, fixedDelay = OPPDATERINGSFREKVENS) + fun åpneBehandlinger() { + if (LeaderClient.isLeader() != true) return + val behandlinger = meldingstellingRepository.finnÅpneBehandlinger() + Fagsystem.values().map { fagsystem -> + val forekomster = behandlinger.filter { it.fagsystem == fagsystem } + if (forekomster.isNotEmpty()) { + logger.info( + "Åpne behandlinger for ${fagsystem.name} returnerte ${forekomster.sumOf { it.antall }} " + + "fordelt på ${forekomster.size} uker.", + ) + } + } + val rows = behandlinger.map { + MultiGauge.Row.of( + Tags.of( + "fagsystem", + it.fagsystem.name, + "uke", + it.år.toString() + "-" + it.uke.toString().padStart(2, '0'), + ), + it.antall, + ) + } + + åpneBehandlingerGauge.register(rows, true) + } + + @Scheduled(initialDelay = 90000L, fixedDelay = OPPDATERINGSFREKVENS) + fun behandlingerKlarTilSaksbehandling() { + if (LeaderClient.isLeader() != true) return + val behandlinger = meldingstellingRepository.finnKlarTilBehandling() + Fagsystem.values().map { fagsystem -> + val forekomster = behandlinger.filter { it.fagsystem == fagsystem } + if (forekomster.isNotEmpty()) { + logger.info( + "Behandlinger klar til saksbehandling for ${fagsystem.name} returnerte ${forekomster.sumOf { it.antall }} " + + "fordelt på ${forekomster.size} steg.", + ) + } + } + val rows = behandlinger.map { + MultiGauge.Row.of( + Tags.of( + "fagsystem", + it.fagsystem.name, + "steg", + it.behandlingssteg.name, + ), + it.antall, + ) + } + + klarTilBehandlingGauge.register(rows, true) + } + + @Scheduled(initialDelay = 120000L, fixedDelay = OPPDATERINGSFREKVENS) + fun behandlingerPåVent() { + if (LeaderClient.isLeader() != true) return + val behandlinger = meldingstellingRepository.finnVentendeBehandlinger() + Fagsystem.values().map { fagsystem -> + val forekomster = behandlinger.filter { it.fagsystem == fagsystem } + if (forekomster.isNotEmpty()) { + logger.info( + "Behandlinger på vent for ${fagsystem.name} returnerte ${forekomster.sumOf { it.antall }} " + + "fordelt på ${forekomster.size} steg.", + ) + } + } + + val rows = behandlinger.map { + MultiGauge.Row.of( + Tags.of( + "fagsystem", + it.fagsystem.name, + "steg", + it.behandlingssteg.name, + ), + it.antall, + ) + } + + ventendeBehandlingGauge.register(rows, true) + } + + @Scheduled(initialDelay = 150000L, fixedDelay = OPPDATERINGSFREKVENS) + fun sendteBrev() { + if (LeaderClient.isLeader() != true) return + val data = meldingstellingRepository.finnSendteBrev() + Fagsystem.values().map { fagsystem -> + val forekomster = data.filter { it.fagsystem == fagsystem } + if (forekomster.isNotEmpty()) { + logger.info("Sendte brev for ${fagsystem.name} returnerte ${forekomster.sumOf { it.antall }} fordelt på ${forekomster.size} typer/uker.") + } + } + + val rows = data.map { + MultiGauge.Row.of( + Tags.of( + "fagsystem", + it.fagsystem.name, + "brevtype", + it.brevtype.name, + "uke", + it.år.toString() + "-" + it.uke.toString().padStart(2, '0'), + ), + it.antall, + ) + } + sendteBrevGauge.register(rows, true) + } + + @Scheduled(initialDelay = 180000L, fixedDelay = OPPDATERINGSFREKVENS) + fun vedtak() { + if (LeaderClient.isLeader() != true) return + val data = meldingstellingRepository.finnVedtak() + Fagsystem.values().map { fagsystem -> + val forekomster = data.filter { it.fagsystem == fagsystem } + if (forekomster.isNotEmpty()) { + logger.info( + "Vedtak for ${fagsystem.name} returnerte ${forekomster.sumOf { it.antall }} " + + "fordelt på ${forekomster.size} typer/uker.", + ) + } + } + + val rows = data.map { + val vedtakstype = if (it.vedtakstype in Behandlingsresultat.ALLE_HENLEGGELSESKODER) { + Behandlingsresultatstype.HENLAGT.name + } else { + it.vedtakstype.name + } + + MultiGauge.Row.of( + Tags.of( + "fagsystem", + it.fagsystem.name, + "vedtakstype", + vedtakstype, + "uke", + it.år.toString() + "-" + it.uke.toString().padStart(2, '0'), + ), + it.antall, + ) + } + vedtakGauge.register(rows, true) + } + + @Scheduled(initialDelay = 210000L, fixedDelay = OPPDATERINGSFREKVENS) + fun mottatteKravgrunnlagKoblet() { + val data = meldingstellingRepository.findByType(Meldingstype.KRAVGRUNNLAG) + Fagsystem.values().map { fagsystem -> + val forekomster = data.filter { it.fagsystem == fagsystem } + if (forekomster.isNotEmpty()) { + logger.info( + "Mottatte kravgrunnlag koblet for ${fagsystem.name} returnerte ${forekomster.sumOf { it.antall }} " + + "fordelt på ${forekomster.size} fagsystem/dager.", + ) + } + } + + val rows = data.map { + MultiGauge.Row.of( + Tags.of( + "fagsystem", + it.fagsystem.name, + "type", + it.type.name, + "status", + it.status.name, + "dato", + it.dato.toString(), + ), + it.antall, + ) + } + mottatteKravgrunnlagGauge.register(rows, true) + } + + @Scheduled(initialDelay = 240000L, fixedDelay = OPPDATERINGSFREKVENS) + fun mottatteStatusmeldinger() { + val data = meldingstellingRepository.summerAntallForType(Meldingstype.STATUSMELDING) + Fagsystem.values().map { fagsystem -> + val forekomster = data.filter { it.fagsystem == fagsystem } + if (forekomster.isNotEmpty()) { + logger.info( + "Mottatte statusmeldinger for ${fagsystem.name} " + + "returnerte ${forekomster.sumOf { it.antall }} fordelt på ${forekomster.size} fagsystem/dager.", + ) + } + } + + val rows = data.map { + MultiGauge.Row.of( + Tags.of( + "fagsystem", + it.fagsystem.name, + "dato", + it.dato.toString(), + ), + it.antall, + ) + } + mottatteStatusmeldingerGauge.register(rows, true) + } + + @Scheduled(initialDelay = 270000L, fixedDelay = OPPDATERINGSFREKVENS) + fun feiledeTasker() { + val data = taskService.finnAlleFeiledeTasks() + val fagsystemTilTasker = data.groupBy { it.fagsystem() } + + val rows = Fagsystem.values().map { + MultiGauge.Row.of( + Tags.of("fagsystem", it.name), + (fagsystemTilTasker[it.name]?.size ?: 0) + (fagsystemTilTasker["UKJENT"]?.size ?: 0), + ) + } + + feiledeTasker.register(rows, true) + } + + companion object { + + const val OPPDATERINGSFREKVENS = 1800 * 1000L + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/TellerService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/TellerService.kt new file mode 100644 index 000000000..b25556848 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/TellerService.kt @@ -0,0 +1,90 @@ +package no.nav.familie.tilbake.micrometer + +import io.micrometer.core.instrument.Metrics +import io.micrometer.core.instrument.Tags +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultat +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.micrometer.domain.Meldingstelling +import no.nav.familie.tilbake.micrometer.domain.MeldingstellingRepository +import no.nav.familie.tilbake.micrometer.domain.Meldingstype +import no.nav.familie.tilbake.micrometer.domain.Mottaksstatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate + +@Service +@Transactional +class TellerService( + private val fagsakRepository: FagsakRepository, + private val meldingstellingRepository: MeldingstellingRepository, +) { + + fun tellKobletKravgrunnlag(fagsystem: Fagsystem) = + tellMelding(fagsystem, Meldingstype.KRAVGRUNNLAG, Mottaksstatus.KOBLET) + + fun tellUkobletKravgrunnlag(fagsystem: Fagsystem) = + tellMelding(fagsystem, Meldingstype.KRAVGRUNNLAG, Mottaksstatus.UKOBLET) + + fun tellKobletStatusmelding(fagsystem: Fagsystem) = + tellMelding(fagsystem, Meldingstype.STATUSMELDING, Mottaksstatus.KOBLET) + + fun tellUkobletStatusmelding(fagsystem: Fagsystem) = + tellMelding(fagsystem, Meldingstype.STATUSMELDING, Mottaksstatus.UKOBLET) + + fun tellMelding(fagsystem: Fagsystem, type: Meldingstype, status: Mottaksstatus) { + val meldingstelling = meldingstellingRepository.findByFagsystemAndTypeAndStatusAndDato( + fagsystem, + type, + status, + LocalDate.now(), + ) + if (meldingstelling == null) { + meldingstellingRepository.insert( + Meldingstelling( + fagsystem = fagsystem, + type = type, + status = status, + ), + ) + } else { + meldingstellingRepository.oppdaterTeller(fagsystem, type, status) + } + } + + fun tellBrevSendt(fagsak: Fagsak, brevtype: Brevtype) { + Metrics.counter( + "Brevteller", + Tags.of( + "fagsystem", + fagsak.fagsystem.name, + "brevtype", + brevtype.name, + ), + ).increment() + } + + fun tellVedtak(behandlingsresultatstype: Behandlingsresultatstype, behandling: Behandling) { + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val vedtakstype = if (behandlingsresultatstype in Behandlingsresultat.ALLE_HENLEGGELSESKODER) { + Behandlingsresultatstype.HENLAGT.name + } else { + behandlingsresultatstype.name + } + + Metrics.counter( + "Vedtaksteller", + Tags.of( + "fagsystem", + fagsak.fagsystem.name, + "vedtakstype", + vedtakstype, + ), + ).increment() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/BehandlingerPerSteg.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/BehandlingerPerSteg.kt new file mode 100644 index 000000000..fe306bd3b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/BehandlingerPerSteg.kt @@ -0,0 +1,10 @@ +package no.nav.familie.tilbake.micrometer.domain + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg + +class BehandlingerPerSteg( + val fagsystem: Fagsystem, + val behandlingssteg: Behandlingssteg, + val antall: Int, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/BrevPerUke.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/BrevPerUke.kt new file mode 100644 index 000000000..e458bc541 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/BrevPerUke.kt @@ -0,0 +1,12 @@ +package no.nav.familie.tilbake.micrometer.domain + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype + +class BrevPerUke( + val år: Int, + val uke: Int, + val fagsystem: Fagsystem, + val brevtype: Brevtype, + val antall: Int, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/ForekomsterPerDag.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/ForekomsterPerDag.kt new file mode 100644 index 000000000..3751cfdf0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/ForekomsterPerDag.kt @@ -0,0 +1,10 @@ +package no.nav.familie.tilbake.micrometer.domain + +import no.nav.familie.kontrakter.felles.Fagsystem +import java.time.LocalDate + +class ForekomsterPerDag( + val dato: LocalDate, + val fagsystem: Fagsystem, + val antall: Int, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/ForekomsterPerUke.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/ForekomsterPerUke.kt new file mode 100644 index 000000000..9450561a7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/ForekomsterPerUke.kt @@ -0,0 +1,10 @@ +package no.nav.familie.tilbake.micrometer.domain + +import no.nav.familie.kontrakter.felles.Fagsystem + +class ForekomsterPerUke( + val år: Int, + val uke: Int, + val fagsystem: Fagsystem, + val antall: Int, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/Meldingstelling.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/Meldingstelling.kt new file mode 100644 index 000000000..8aa0c84a9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/Meldingstelling.kt @@ -0,0 +1,28 @@ +package no.nav.familie.tilbake.micrometer.domain + +import no.nav.familie.kontrakter.felles.Fagsystem +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Table +import java.time.LocalDate +import java.util.UUID + +@Table +class Meldingstelling( + @Id + val id: UUID = UUID.randomUUID(), + val fagsystem: Fagsystem, + val type: Meldingstype, + val status: Mottaksstatus, + val antall: Int = 1, + val dato: LocalDate = LocalDate.now(), +) + +enum class Mottaksstatus { + KOBLET, + UKOBLET, +} + +enum class Meldingstype { + KRAVGRUNNLAG, + STATUSMELDING, +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/MeldingstellingRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/MeldingstellingRepository.kt new file mode 100644 index 000000000..c199a242d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/MeldingstellingRepository.kt @@ -0,0 +1,111 @@ +package no.nav.familie.tilbake.micrometer.domain + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import org.springframework.data.jdbc.repository.query.Modifying +import org.springframework.data.jdbc.repository.query.Query +import java.time.LocalDate +import java.util.UUID + +interface MeldingstellingRepository : + RepositoryInterface, + InsertUpdateRepository { + + fun findByFagsystemAndTypeAndStatusAndDato( + fagsystem: Fagsystem, + type: Meldingstype, + status: Mottaksstatus, + dato: LocalDate = LocalDate.now(), + ): Meldingstelling? + + fun findByType(type: Meldingstype): List + + @Query( + """SELECT fagsystem, dato, SUM(antall) as antall FROM meldingstelling + WHERE type = :type + GROUP BY fagsystem, dato""", + ) + fun summerAntallForType(type: Meldingstype): List + + @Modifying + @Query( + """UPDATE meldingstelling SET antall = antall + 1 + WHERE fagsystem = :fagsystem + AND type = :type + AND status = :status + AND dato = :dato""", + ) + fun oppdaterTeller( + fagsystem: Fagsystem, + type: Meldingstype, + status: Mottaksstatus, + dato: LocalDate = LocalDate.now(), + ) + + // language=PostgreSQL + @Query( + """SELECT fagsystem, + extract(ISOYEAR from behandling.opprettet_dato) as år, + extract(WEEK from behandling.opprettet_dato) as uke, + COUNT(*) AS antall + FROM fagsak + JOIN behandling ON fagsak.id = behandling.fagsak_id + WHERE status <> 'AVSLUTTET' + GROUP BY fagsystem, år, uke""", + ) + fun finnÅpneBehandlinger(): List + + // language=PostgreSQL + @Query( + """SELECT fagsystem, behandlingssteg, COUNT(*) AS antall + FROM fagsak + JOIN behandling ON fagsak.id = behandling.fagsak_id + JOIN behandlingsstegstilstand b ON behandling.id = b.behandling_id + WHERE status <> 'AVSLUTTET' + AND behandlingsstegsstatus = 'KLAR' + GROUP BY fagsystem, behandlingssteg""", + ) + fun finnKlarTilBehandling(): List + + // language=PostgreSQL + @Query( + """SELECT fagsystem, behandlingssteg, COUNT(*) AS antall + FROM fagsak + JOIN behandling ON fagsak.id = behandling.fagsak_id + JOIN behandlingsstegstilstand b ON behandling.id = b.behandling_id + WHERE status <> 'AVSLUTTET' + AND behandlingsstegsstatus = 'VENTER' + GROUP BY fagsystem, behandlingssteg""", + ) + fun finnVentendeBehandlinger(): List + + // language=PostgreSQL + @Query( + """SELECT fagsystem, + b.brevtype, + extract(ISOYEAR from b.opprettet_tid) as år, + extract(WEEK from b.opprettet_tid) as uke, + COUNT(*) AS antall + FROM fagsak + JOIN behandling ON fagsak.id = behandling.fagsak_id + JOIN brevsporing b ON behandling.id = b.behandling_id + GROUP BY fagsystem, b.brevtype, år, uke""", + ) + fun finnSendteBrev(): List + + // language=PostgreSQL + @Query( + """SELECT fagsystem, + behandlingsresultat.type as vedtakstype, + extract(ISOYEAR from avsluttet_dato) as år, + extract(WEEK from avsluttet_dato) as uke, + COUNT(*) AS antall + FROM fagsak + JOIN behandling ON fagsak.id = behandling.fagsak_id + JOIN behandlingsresultat ON behandling.id = behandlingsresultat.behandling_id + WHERE status = 'AVSLUTTET' + GROUP BY fagsystem, vedtakstype, år, uke""", + ) + fun finnVedtak(): List +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/VedtakPerUke.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/VedtakPerUke.kt new file mode 100644 index 000000000..67b0c5616 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/micrometer/domain/VedtakPerUke.kt @@ -0,0 +1,12 @@ +package no.nav.familie.tilbake.micrometer.domain + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype + +class VedtakPerUke( + val år: Int, + val uke: Int, + val fagsystem: Fagsystem, + val vedtakstype: Behandlingsresultatstype, + val antall: Int, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/FerdigstillOppgaveTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/FerdigstillOppgaveTask.kt new file mode 100644 index 000000000..8eb6e91b0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/FerdigstillOppgaveTask.kt @@ -0,0 +1,39 @@ +package no.nav.familie.tilbake.oppgave + +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = FerdigstillOppgaveTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Ferdigstiller oppgave for behandling", + triggerTidVedFeilISekunder = 60 * 5L, +) +class FerdigstillOppgaveTask(private val oppgaveService: OppgaveService) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("FerdigstillOppgaveTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val oppgavetype = if (task.metadata.containsKey("oppgavetype")) { + Oppgavetype.valueOf(task.metadata.getProperty("oppgavetype")) + } else { + null + } + oppgaveService.ferdigstillOppgave( + behandlingId = UUID.fromString(task.payload), + oppgavetype = oppgavetype, + ) + } + + companion object { + + const val TYPE = "ferdigstillOppgave" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/LagOppgaveTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/LagOppgaveTask.kt new file mode 100644 index 000000000..b3e71e5ae --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/LagOppgaveTask.kt @@ -0,0 +1,65 @@ +package no.nav.familie.tilbake.oppgave + +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.config.PropertyName +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = LagOppgaveTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Lager oppgave for nye behandlinger", + triggerTidVedFeilISekunder = 300L, +) +class LagOppgaveTask( + private val oppgaveService: OppgaveService, + private val behandlingskontrollService: BehandlingskontrollService, + private val oppgavePrioritetService: OppgavePrioritetService, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("LagOppgaveTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val oppgavetype = Oppgavetype.valueOf(task.metadata.getProperty("oppgavetype")) + val saksbehandler = task.metadata.getProperty("saksbehandler") + val enhet = task.metadata.getProperty(PropertyName.ENHET) ?: "" // elvis-operator for bakoverkompatibilitet + val behandlingId = UUID.fromString(task.payload) + + val behandlingsstegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingId) + + val sendtTilBeslutningAv: String? = if (behandlingsstegstilstand?.behandlingssteg == Behandlingssteg.FATTE_VEDTAK) { + task.metadata.getProperty("opprettetAv")?.let { "Sendt til godkjenning av $it" } + } else { + null + } + + val fristeUker = behandlingsstegstilstand?.venteårsak?.defaultVenteTidIUker ?: 0 + val venteårsak = behandlingsstegstilstand?.venteårsak?.beskrivelse ?: "" + val beskrivelse = sendtTilBeslutningAv?.let { "$sendtTilBeslutningAv $venteårsak" } ?: venteårsak + val prioritet = oppgavePrioritetService.utledOppgaveprioritet(behandlingId) + + oppgaveService.opprettOppgave( + UUID.fromString(task.payload), + oppgavetype, + enhet, + beskrivelse, + LocalDate.now().plusWeeks(fristeUker), + saksbehandler, + prioritet, + ) + } + + companion object { + + const val TYPE = "lagOppgave" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppdaterAnsvarligSaksbehandlerTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppdaterAnsvarligSaksbehandlerTask.kt new file mode 100644 index 000000000..97830b5b5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppdaterAnsvarligSaksbehandlerTask.kt @@ -0,0 +1,44 @@ +package no.nav.familie.tilbake.oppgave + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = OppdaterAnsvarligSaksbehandlerTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Oppdaterer saksbehandler på oppgave", + triggerTidVedFeilISekunder = 300L, +) +class OppdaterAnsvarligSaksbehandlerTask( + private val oppgaveService: OppgaveService, + private val behandlingRepository: BehandlingRepository, + private val oppgavePrioritetService: OppgavePrioritetService, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("OppdaterSaksbehandlerPåOppgaveTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val behandlingId = UUID.fromString(task.payload) + + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val oppgave = oppgaveService.finnOppgaveForBehandlingUtenOppgaveType(behandlingId) + val prioritet = oppgavePrioritetService.utledOppgaveprioritet(behandlingId, oppgave) + + if (oppgave.tilordnetRessurs != behandling.ansvarligSaksbehandler || oppgave.prioritet != prioritet) { + oppgaveService.patchOppgave(oppgave.copy(tilordnetRessurs = behandling.ansvarligSaksbehandler, prioritet = prioritet)) + } + } + + companion object { + + const val TYPE = "oppdaterSaksbehandlerOppgave" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppdaterEnhetOppgaveTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppdaterEnhetOppgaveTask.kt new file mode 100644 index 000000000..1f5e879a8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppdaterEnhetOppgaveTask.kt @@ -0,0 +1,52 @@ +package no.nav.familie.tilbake.oppgave + +import no.nav.familie.kontrakter.felles.Tema +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.config.Constants +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = OppdaterEnhetOppgaveTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Oppdaterer enhet på oppgave", + triggerTidVedFeilISekunder = 300L, +) +class OppdaterEnhetOppgaveTask(private val oppgaveService: OppgaveService) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("OppdaterEnhetOppgaveTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val enhetId = task.metadata.getProperty("enhetId") + val beskrivelse = task.metadata.getProperty("beskrivelse") + val saksbehandler = task.metadata.getProperty("saksbehandler") + val behandlingId = UUID.fromString(task.payload) + + val oppgave = oppgaveService.finnOppgaveForBehandlingUtenOppgaveType(behandlingId) + val nyBeskrivelse = LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yy hh:mm")) + ":" + + beskrivelse + System.lineSeparator() + oppgave.beskrivelse + var patchetOppgave = oppgave.copy(beskrivelse = nyBeskrivelse) + if (!saksbehandler.isNullOrEmpty() && saksbehandler != Constants.BRUKER_ID_VEDTAKSLØSNINGEN) { + patchetOppgave = patchetOppgave.copy(tilordnetRessurs = saksbehandler) + } + oppgaveService.patchOppgave(patchetOppgave) + + if (oppgave.tema == Tema.ENF) { + oppgaveService.tilordneOppgaveNyEnhet(oppgave.id!!, enhetId, false) // ENF bruker generelle mapper + } else { + oppgaveService.tilordneOppgaveNyEnhet(oppgave.id!!, enhetId, true) // KON og BAR bruker mapper som hører til enhetene + } + } + + companion object { + + const val TYPE = "oppdaterEnhetOppgave" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppdaterOppgaveTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppdaterOppgaveTask.kt new file mode 100644 index 000000000..ffd406dcc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppdaterOppgaveTask.kt @@ -0,0 +1,60 @@ +package no.nav.familie.tilbake.oppgave + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.config.Constants +import org.slf4j.LoggerFactory +import org.springframework.core.env.Environment +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = OppdaterOppgaveTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Oppdaterer oppgave", + triggerTidVedFeilISekunder = 300L, +) +class OppdaterOppgaveTask( + private val oppgaveService: OppgaveService, + val environment: Environment, + private val oppgavePrioritetService: OppgavePrioritetService, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("OppdaterOppgaveTask prosesserer med id=${task.id} og metadata ${task.metadata}") + if (environment.activeProfiles.contains("e2e")) return + + val frist = task.metadata.getProperty("frist") + val beskrivelse = task.metadata.getProperty("beskrivelse") + val saksbehandler = task.metadata.getProperty("saksbehandler") + val behandlingId = UUID.fromString(task.payload) + + val oppgave = oppgaveService.finnOppgaveForBehandlingUtenOppgaveType(behandlingId) + + val nyBeskrivelse = LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yy HH:mm")) + ":" + + beskrivelse + System.lineSeparator() + oppgave.beskrivelse + + val prioritet = oppgavePrioritetService.utledOppgaveprioritet(behandlingId, oppgave) + + var patchetOppgave = oppgave.copy( + fristFerdigstillelse = frist, + beskrivelse = nyBeskrivelse, + prioritet = prioritet, + ) + if (!saksbehandler.isNullOrEmpty() && saksbehandler != Constants.BRUKER_ID_VEDTAKSLØSNINGEN) { + patchetOppgave = patchetOppgave.copy(tilordnetRessurs = saksbehandler) + } + oppgaveService.patchOppgave(patchetOppgave) + } + + companion object { + + const val TYPE = "oppdaterOppgave" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppdaterPrioritetTask.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppdaterPrioritetTask.kt new file mode 100644 index 000000000..3e762c8c9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppdaterPrioritetTask.kt @@ -0,0 +1,38 @@ +package no.nav.familie.tilbake.oppgave + +import no.nav.familie.prosessering.AsyncTaskStep +import no.nav.familie.prosessering.TaskStepBeskrivelse +import no.nav.familie.prosessering.domene.Task +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +@TaskStepBeskrivelse( + taskStepType = OppdaterPrioritetTask.TYPE, + maxAntallFeil = 3, + beskrivelse = "Oppdaterer prioritet på oppgave", + triggerTidVedFeilISekunder = 300L, +) +class OppdaterPrioritetTask( + private val oppgaveService: OppgaveService, + private val oppgavePrioritetService: OppgavePrioritetService, +) : AsyncTaskStep { + + private val log = LoggerFactory.getLogger(this::class.java) + + override fun doTask(task: Task) { + log.info("OppdaterPrioritetTask prosesserer med id=${task.id} og metadata ${task.metadata}") + val behandlingId = UUID.fromString(task.payload) + + val oppgave = oppgaveService.finnOppgaveForBehandlingUtenOppgaveType(behandlingId) + val prioritet = oppgavePrioritetService.utledOppgaveprioritet(behandlingId, oppgave) + + oppgaveService.patchOppgave(oppgave.copy(prioritet = prioritet)) + } + + companion object { + + const val TYPE = "oppdaterPrioritetForOppgave" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppgavePrioritetService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppgavePrioritetService.kt new file mode 100644 index 000000000..4ac4bcf82 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppgavePrioritetService.kt @@ -0,0 +1,41 @@ +package no.nav.familie.tilbake.oppgave + +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import no.nav.familie.kontrakter.felles.oppgave.OppgavePrioritet +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import org.springframework.stereotype.Service +import java.math.BigDecimal +import java.util.UUID + +@Service +class OppgavePrioritetService( + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val featureToggleService: FeatureToggleService, +) { + + fun utledOppgaveprioritet(behandlingId: UUID, oppgave: Oppgave? = null): OppgavePrioritet { + val finnesKravgrunnlag = kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandlingId) + + return if (finnesKravgrunnlag) { + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + + val feilutbetaltBeløp = utledFeilutbetaling(kravgrunnlag) + + when { + feilutbetaltBeløp < BigDecimal(10_000) -> OppgavePrioritet.LAV + feilutbetaltBeløp > BigDecimal(70_000) -> OppgavePrioritet.HOY + else -> OppgavePrioritet.NORM + } + } else { + oppgave?.prioritet ?: OppgavePrioritet.NORM + } + } + + private fun utledFeilutbetaling(kravgrunnlag: Kravgrunnlag431) = + kravgrunnlag.perioder.sumOf { periode -> + periode.beløp.filter { beløp -> beløp.klassetype == Klassetype.FEIL }.sumOf { it.nyttBeløp } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppgaveService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppgaveService.kt new file mode 100644 index 000000000..893b9b6fc --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppgaveService.kt @@ -0,0 +1,262 @@ +package no.nav.familie.tilbake.oppgave + +import io.micrometer.core.instrument.Metrics +import no.nav.familie.kontrakter.felles.oppgave.Behandlingstype +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveRequest +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveResponseDto +import no.nav.familie.kontrakter.felles.oppgave.IdentGruppe +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import no.nav.familie.kontrakter.felles.oppgave.OppgaveIdentV2 +import no.nav.familie.kontrakter.felles.oppgave.OppgavePrioritet +import no.nav.familie.kontrakter.felles.oppgave.OppgaveResponse +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.kontrakter.felles.oppgave.OpprettOppgaveRequest +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import no.nav.familie.tilbake.person.PersonService +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.core.env.Environment +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.UUID + +@Service +class OppgaveService( + private val behandlingRepository: BehandlingRepository, + private val fagsakRepository: FagsakRepository, + private val integrasjonerClient: IntegrasjonerClient, + private val personService: PersonService, + private val taskService: TaskService, + private val environment: Environment, +) { + + private val logger = LoggerFactory.getLogger(this::class.java) + private val secureLogger: Logger = LoggerFactory.getLogger("secureLogger") + + private val antallOppgaveTyper = Oppgavetype.values().associateWith { + Metrics.counter("oppgave.opprettet", "type", it.name) + } + + fun finnOppgaveForBehandlingUtenOppgaveType(behandlingId: UUID): Oppgave { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + + val finnOppgaveRequest = FinnOppgaveRequest( + behandlingstype = Behandlingstype.Tilbakekreving, + saksreferanse = behandling.eksternBrukId.toString(), + tema = fagsak.ytelsestype.tilTema(), + ) + val finnOppgaveResponse = integrasjonerClient.finnOppgaver(finnOppgaveRequest) + when { + finnOppgaveResponse.oppgaver.size > 1 -> { + secureLogger.error( + "Mer enn en oppgave åpen for behandling ${behandling.eksternBrukId}, " + + "$finnOppgaveRequest, $finnOppgaveResponse", + ) + throw Feil("Har mer enn en åpen oppgave for behandling ${behandling.eksternBrukId}") + } + + finnOppgaveResponse.oppgaver.isEmpty() -> { + secureLogger.error( + "Fant ingen oppgave for behandling ${behandling.eksternBrukId} på fagsak ${fagsak.eksternFagsakId}, " + + "$finnOppgaveRequest, $finnOppgaveResponse", + ) + throw Feil("Fant ingen oppgave for behandling ${behandling.eksternBrukId} på fagsak ${fagsak.eksternFagsakId}. Oppgaven kan være manuelt lukket.") + } + + else -> { + return finnOppgaveResponse.oppgaver.first() + } + } + } + + fun opprettOppgave( + behandlingId: UUID, + oppgavetype: Oppgavetype, + enhet: String, + beskrivelse: String?, + fristForFerdigstillelse: LocalDate, + saksbehandler: String?, + prioritet: OppgavePrioritet, + ): OppgaveResponse { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsakId = behandling.fagsakId + val fagsak = fagsakRepository.findByIdOrThrow(fagsakId) + val aktørId = personService.hentAktivAktørId(fagsak.bruker.ident, fagsak.fagsystem) + + // Sjekk om oppgave allerede finnes for behandling + val (_, finnOppgaveRespons) = finnOppgave(behandling, oppgavetype, fagsak) + if (finnOppgaveRespons.oppgaver.isNotEmpty() && !finnesFerdigstillOppgaveForBehandling(behandlingId, oppgavetype)) { + throw Feil( + "Det finnes allerede en oppgave $oppgavetype for behandling $behandlingId og " + + "finnes ikke noen ferdigstilleoppgaver. Eksisterende oppgaven $oppgavetype må lukke først.", + ) + } + + val opprettOppgave = OpprettOppgaveRequest( + ident = OppgaveIdentV2( + ident = aktørId, + gruppe = IdentGruppe.AKTOERID, + ), + saksId = behandling.eksternBrukId.toString(), + tema = fagsak.ytelsestype.tilTema(), + oppgavetype = oppgavetype, + behandlesAvApplikasjon = "familie-tilbake", + fristFerdigstillelse = fristForFerdigstillelse, + beskrivelse = lagOppgaveTekst( + fagsak.eksternFagsakId, + behandling.eksternBrukId.toString(), + fagsak.fagsystem.name, + beskrivelse, + ), + enhetsnummer = behandling.behandlendeEnhet, + tilordnetRessurs = saksbehandler, + behandlingstype = Behandlingstype.Tilbakekreving.value, + behandlingstema = null, + mappeId = finnAktuellMappe(enhet, oppgavetype), + prioritet = prioritet, + ) + + val opprettetOppgaveId = integrasjonerClient.opprettOppgave(opprettOppgave) + + antallOppgaveTyper[oppgavetype]!!.increment() + + return opprettetOppgaveId + } + + private fun finnAktuellMappe(enhetsnummer: String?, oppgavetype: Oppgavetype): Long? { + if (enhetsnummer == NAY_ENSLIG_FORSØRGER) { + val søkemønster = lagSøkeuttrykk(oppgavetype) ?: return null + val mapper = integrasjonerClient.finnMapper(enhetsnummer) + + val mappeIdForOppgave = mapper.find { it.navn.matches(søkemønster) }?.id?.toLong() + mappeIdForOppgave?.let { + logger.info("Legger oppgave i Godkjenne vedtak-mappe") + } ?: logger.error("Fant ikke mappe for oppgavetype = $oppgavetype") + + return mappeIdForOppgave + } + return null + } + + private fun lagSøkeuttrykk(oppgavetype: Oppgavetype): Regex? { + val pattern = when (oppgavetype) { + Oppgavetype.BehandleSak, Oppgavetype.BehandleUnderkjentVedtak -> "50 Tilbakekreving?.+" + Oppgavetype.GodkjenneVedtak -> "70 Godkjenne?.vedtak?.+" + else -> { + logger.error("Ukjent oppgavetype = $oppgavetype") + return null + } + } + return Regex(pattern, RegexOption.IGNORE_CASE) + } + + fun patchOppgave(patchOppgave: Oppgave): OppgaveResponse { + return integrasjonerClient.patchOppgave(patchOppgave) + } + + fun tilordneOppgaveNyEnhet(oppgaveId: Long, nyEnhet: String, fjernMappeFraOppgave: Boolean): OppgaveResponse { + return integrasjonerClient.tilordneOppgaveNyEnhet(oppgaveId, nyEnhet, fjernMappeFraOppgave) + } + + fun ferdigstillOppgave(behandlingId: UUID, oppgavetype: Oppgavetype?) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val (finnOppgaveRequest, finnOppgaveResponse) = finnOppgave(behandling, oppgavetype, fagsak) + + when { + finnOppgaveResponse.oppgaver.size > 1 -> { + secureLogger.error( + "Mer enn en oppgave åpen for behandling ${behandling.eksternBrukId}, " + + "$finnOppgaveRequest, $finnOppgaveResponse", + ) + throw Feil("Har mer enn en åpen oppgave for behandling ${behandling.eksternBrukId}") + } + + finnOppgaveResponse.oppgaver.isEmpty() -> { + logger.error("Fant ingen oppgave å ferdigstille for behandling ${behandling.eksternBrukId}") + secureLogger.error( + "Fant ingen oppgave å ferdigstille ${behandling.eksternBrukId}, " + + "$finnOppgaveRequest, $finnOppgaveResponse", + ) + } + + else -> { + integrasjonerClient.ferdigstillOppgave(finnOppgaveResponse.oppgaver[0].id!!) + } + } + } + + private fun finnOppgave( + behandling: Behandling, + oppgavetype: Oppgavetype?, + fagsak: Fagsak, + ): Pair { + val finnOppgaveRequest = FinnOppgaveRequest( + behandlingstype = Behandlingstype.Tilbakekreving, + saksreferanse = behandling.eksternBrukId.toString(), + oppgavetype = oppgavetype, + tema = fagsak.ytelsestype.tilTema(), + ) + val finnOppgaveResponse = integrasjonerClient.finnOppgaver(finnOppgaveRequest) + return Pair(finnOppgaveRequest, finnOppgaveResponse) + } + + private fun lagOppgaveTekst( + eksternFagsakId: String, + eksternbrukBehandlingID: String, + fagsystem: String, + beskrivelse: String? = null, + ): String { + return if (beskrivelse != null) { + beskrivelse + "\n" + } else { + "" + } + "--- Opprettet av familie-tilbake ${LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)} --- \n" + + "https://${lagFamilieTilbakeFrontendUrl()}/fagsystem/$fagsystem/fagsak/$eksternFagsakId/behandling/" + + eksternbrukBehandlingID + } + + private fun lagFamilieTilbakeFrontendUrl(): String { + return if (environment.activeProfiles.contains("prod")) { + "familietilbakekreving.intern.nav.no" + } else { + "familie-tilbake-frontend.intern.dev.nav.no" + } + } + + private fun finnesFerdigstillOppgaveForBehandling(behandlingId: UUID, oppgavetype: Oppgavetype): Boolean { + val ubehandledeTasker = taskService.finnTasksMedStatus( + status = listOf( + Status.UBEHANDLET, + Status.PLUKKET, + Status.FEILET, + Status.KLAR_TIL_PLUKK, + Status.BEHANDLER, + ), + type = FerdigstillOppgaveTask.TYPE, + page = Pageable.unpaged(), + ) + return ubehandledeTasker.any { + it.payload == behandlingId.toString() && + it.metadata.getProperty("oppgavetype") == oppgavetype.name + } + } + + companion object { + + private const val NAY_ENSLIG_FORSØRGER = "4489" + private const val NAY_EGNE_ANSATTE = "4483" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppgaveTaskService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppgaveTaskService.kt new file mode 100644 index 000000000..f37b451a5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/oppgave/OppgaveTaskService.kt @@ -0,0 +1,163 @@ +package no.nav.familie.tilbake.oppgave + +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.FagsakService +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.config.PropertyName +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.Properties +import java.util.UUID + +@Service +class OppgaveTaskService( + private val taskService: TaskService, + private val fagsakService: FagsakService, +) { + + @Transactional + fun opprettOppgaveTask( + behandling: Behandling, + oppgavetype: Oppgavetype, + saksbehandler: String? = null, + opprettetAv: String? = null, + ) { + val fagsystem = fagsakService.finnFagsystemForBehandlingId(behandling.id) + val properties = Properties().apply { + setProperty("oppgavetype", oppgavetype.name) + setProperty(PropertyName.FAGSYSTEM, fagsystem.name) + setProperty(PropertyName.ENHET, behandling.behandlendeEnhet) + saksbehandler?.let { setProperty("saksbehandler", it) } + opprettetAv?.let { setProperty("opprettetAv", it) } + } + taskService.save( + Task( + type = LagOppgaveTask.TYPE, + payload = behandling.id.toString(), + properties = properties, + ), + ) + } + + @Transactional + fun ferdigstilleOppgaveTask(behandlingId: UUID, oppgavetype: String? = null) { + val fagsystem = fagsakService.finnFagsystemForBehandlingId(behandlingId) + val properties = Properties().apply { + if (!oppgavetype.isNullOrEmpty()) { + setProperty("oppgavetype", oppgavetype) + } + setProperty(PropertyName.FAGSYSTEM, fagsystem.name) + } + taskService.save( + Task( + type = FerdigstillOppgaveTask.TYPE, + payload = behandlingId.toString(), + properties = properties, + ), + ) + } + + @Transactional + fun oppdaterOppgaveTask(behandlingId: UUID, beskrivelse: String, frist: LocalDate, saksbehandler: String? = null) { + opprettOppdaterOppgaveTask( + behandlingId = behandlingId, + beskrivelse = beskrivelse, + frist = frist, + saksbehandler = saksbehandler, + ) + } + + @Transactional + fun oppdaterOppgaveTaskMedTriggertid( + behandlingId: UUID, + beskrivelse: String, + frist: LocalDate, + triggerTid: Long, + saksbehandler: String? = null, + ) { + opprettOppdaterOppgaveTask( + behandlingId = behandlingId, + beskrivelse = beskrivelse, + frist = frist, + triggerTid = triggerTid, + saksbehandler = saksbehandler, + ) + } + + private fun opprettOppdaterOppgaveTask( + behandlingId: UUID, + beskrivelse: String, + frist: LocalDate, + triggerTid: Long? = null, + saksbehandler: String? = null, + ) { + val fagsystem = fagsakService.finnFagsystemForBehandlingId(behandlingId) + val properties = Properties().apply { + setProperty(PropertyName.FAGSYSTEM, fagsystem.name) + setProperty("beskrivelse", beskrivelse) + setProperty("frist", frist.toString()) + saksbehandler?.let { setProperty("saksbehandler", it) } + } + val task = Task( + type = OppdaterOppgaveTask.TYPE, + payload = behandlingId.toString(), + properties = properties, + ) + triggerTid?.let { task.medTriggerTid(LocalDateTime.now().plusSeconds(it)) } + taskService.save(task) + } + + @Transactional + fun oppdaterEnhetOppgaveTask(behandlingId: UUID, beskrivelse: String, enhetId: String) { + val fagsystem = fagsakService.finnFagsystemForBehandlingId(behandlingId) + val properties = Properties().apply { + setProperty(PropertyName.FAGSYSTEM, fagsystem.name) + setProperty("beskrivelse", beskrivelse) + setProperty("enhetId", enhetId) + setProperty("saksbehandler", ContextService.hentSaksbehandler()) + } + taskService.save( + Task( + type = OppdaterEnhetOppgaveTask.TYPE, + payload = behandlingId.toString(), + properties = properties, + ), + ) + } + + @Transactional + fun oppdaterAnsvarligSaksbehandlerOppgaveTask(behandlingId: UUID) { + val fagsystem = fagsakService.finnFagsystemForBehandlingId(behandlingId) + val properties = Properties().apply { + setProperty(PropertyName.FAGSYSTEM, fagsystem.name) + } + taskService.save( + Task( + type = OppdaterAnsvarligSaksbehandlerTask.TYPE, + payload = behandlingId.toString(), + properties = properties, + ), + ) + } + + fun oppdaterOppgavePrioritetTask(behandlingId: UUID, fagsakId: String) { + val fagsystem = fagsakService.finnFagsystemForBehandlingId(behandlingId) + val properties = Properties().apply { + setProperty(PropertyName.FAGSYSTEM, fagsystem.name) + setProperty("behandlingId", behandlingId.toString()) + setProperty("ekstertFagsakId", fagsakId) + } + taskService.save( + Task( + type = OppdaterPrioritetTask.TYPE, + payload = behandlingId.toString(), + properties = properties, + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/organisasjon/OrganisasjonService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/organisasjon/OrganisasjonService.kt new file mode 100644 index 000000000..43b83e2d0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/organisasjon/OrganisasjonService.kt @@ -0,0 +1,26 @@ +package no.nav.familie.tilbake.organisasjon + +import no.nav.familie.kontrakter.felles.organisasjon.Organisasjon +import no.nav.familie.tilbake.api.dto.InstitusjonDto +import no.nav.familie.tilbake.dokumentbestilling.felles.header.Institusjon +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import org.springframework.stereotype.Service + +@Service +class OrganisasjonService(private val integrasjonerClient: IntegrasjonerClient) { + + fun mapTilInstitusjonDto(orgnummer: String): InstitusjonDto { + val organisasjon = hentOrganisasjon(orgnummer) + return InstitusjonDto(organisasjonsnummer = orgnummer, navn = organisasjon.navn) + } + + fun mapTilInstitusjonForBrevgenerering(orgnummer: String): Institusjon { + val organisasjon = hentOrganisasjon(orgnummer) + return Institusjon(organisasjonsnummer = orgnummer, navn = organisasjon.navn) + } + + private fun hentOrganisasjon(orgnummer: String): Organisasjon { + val organisasjon = integrasjonerClient.hentOrganisasjon(orgnummer) + return organisasjon + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/DocFormat.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/DocFormat.kt new file mode 100644 index 000000000..840b4a679 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/DocFormat.kt @@ -0,0 +1,14 @@ +package no.nav.familie.tilbake.pdfgen + +import java.util.Locale + +enum class DocFormat { + PDF, + HTML, + EMAIL, + ; + + override fun toString(): String { + return name.lowercase(Locale.getDefault()) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/Dokumentvariant.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/Dokumentvariant.kt new file mode 100644 index 000000000..eebb52e5f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/Dokumentvariant.kt @@ -0,0 +1,6 @@ +package no.nav.familie.tilbake.pdfgen + +enum class Dokumentvariant { + ENDELIG, + UTKAST, +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/FileStructureUtil.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/FileStructureUtil.kt new file mode 100644 index 000000000..c4dcca957 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/FileStructureUtil.kt @@ -0,0 +1,26 @@ +package no.nav.familie.tilbake.pdfgen + +import java.io.IOException +import java.nio.charset.StandardCharsets + +object FileStructureUtil { + + // colorprofile fra https://pippin.gimp.org/sRGBz/ + val colorProfile: ByteArray + get() = // colorprofile fra https://pippin.gimp.org/sRGBz/ + readResource("colorprofile/sRGBz.icc") + + fun readResource(location: String): ByteArray { + val inputStream = FileStructureUtil::class.java.classLoader.getResourceAsStream(location) + requireNotNull(inputStream) { "Fant ikke resource $location" } + return try { + inputStream.readAllBytes() + } catch (e: IOException) { + throw IllegalArgumentException("Klarte ikke å lese resource $location") + } + } + + fun readResourceAsString(location: String): String { + return String(readResource(location), StandardCharsets.UTF_8) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/PdfGenerator.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/PdfGenerator.kt new file mode 100644 index 000000000..f4398c8b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/PdfGenerator.kt @@ -0,0 +1,122 @@ +package no.nav.familie.tilbake.pdfgen + +import com.openhtmltopdf.extend.FSSupplier +import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder +import com.openhtmltopdf.pdfboxout.PdfRendererBuilder +import com.openhtmltopdf.slf4j.Slf4jLogger +import com.openhtmltopdf.svgsupport.BatikSVGDrawer +import com.openhtmltopdf.util.XRLog +import no.nav.familie.tilbake.pdfgen.validering.PdfaValidator +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.util.Locale + +class PdfGenerator { + + companion object { + + private val FONT_CACHE: MutableMap = HashMap() + + private fun lagBodyStartTag(dokumentvariant: Dokumentvariant): String { + return when (dokumentvariant) { + Dokumentvariant.ENDELIG -> "" + Dokumentvariant.UTKAST -> "" + } + } + + init { + XRLog.setLoggingEnabled(true) + XRLog.setLoggerImpl(Slf4jLogger()) + } + } + + fun genererPDFMedLogo(html: String, dokumentvariant: Dokumentvariant): ByteArray { + val logo = FileStructureUtil.readResourceAsString("formats/pdf/nav_logo_svg.html") + return genererPDF(logo + html, dokumentvariant) + } + + fun genererPDF(html: String, dokumentvariant: Dokumentvariant): ByteArray { + val baos = ByteArrayOutputStream() + genererPDF(html, baos, dokumentvariant) + val bytes = baos.toByteArray() + if (dokumentvariant == Dokumentvariant.ENDELIG) { + // validering er for treig for å brukes for interaktiv bruk, tar typisk 1-2 sekunder pr dokument + // validering er også bare nødvendig før journalføring, så det er OK + PdfaValidator.validatePdf(bytes) + } + return bytes + } + + private fun genererPDF(htmlContent: String, outputStream: ByteArrayOutputStream, dokumentvariant: Dokumentvariant) { + val htmlDocument = appendHtmlMetadata(htmlContent, DocFormat.PDF, dokumentvariant) + val builder = PdfRendererBuilder() + try { + builder.useFont( + fontSupplier("SourceSansPro-Regular.ttf"), + "Source Sans Pro", + 400, + BaseRendererBuilder.FontStyle.NORMAL, + true, + ) + .useFont( + fontSupplier("SourceSansPro-Bold.ttf"), + "Source Sans Pro", + 700, + BaseRendererBuilder.FontStyle.OBLIQUE, + true, + ) + .useFont( + fontSupplier("SourceSansPro-It.ttf"), + "Source Sans Pro", + 400, + BaseRendererBuilder.FontStyle.ITALIC, + true, + ) + .useColorProfile(FileStructureUtil.colorProfile) + .useSVGDrawer(BatikSVGDrawer()) + .usePdfAConformance(PdfRendererBuilder.PdfAConformance.PDFA_2_U) + .withHtmlContent(htmlDocument, "") + .toStream(outputStream) + .useFastMode() + .buildPdfRenderer() + .createPDF() + } catch (e: IOException) { + throw RuntimeException("Feil ved generering av pdf", e) + } + } + + private fun appendHtmlMetadata(html: String, format: DocFormat, dokumentvariant: Dokumentvariant): String { + // nødvendig doctype for å støtte non-breaking space i openhtmltopdf + return "" + + "" + + "" + + "" + + "" + + "" + + lagBodyStartTag(dokumentvariant) + + "

    " + + html + + "
    " + + "" + + "" + } + + private fun fontSupplier(fontName: String): FSSupplier { + if (FONT_CACHE.containsKey(fontName)) { + val bytes = FONT_CACHE[fontName] + return FSSupplier { ByteArrayInputStream(bytes) } + } + val bytes = FileStructureUtil.readResource("fonts/$fontName") + FONT_CACHE[fontName] = bytes + return FSSupplier { ByteArrayInputStream(bytes) } + } + + private fun hentCss(format: DocFormat): String { + return FileStructureUtil.readResourceAsString("formats/" + format.name.lowercase(Locale.getDefault()) + "/style.css") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/validering/PdfaValidator.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/validering/PdfaValidator.kt new file mode 100644 index 000000000..d963ddc50 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/validering/PdfaValidator.kt @@ -0,0 +1,51 @@ +package no.nav.familie.tilbake.pdfgen.validering + +import org.verapdf.core.EncryptedPdfException +import org.verapdf.core.ModelParsingException +import org.verapdf.core.ValidationException +import org.verapdf.gf.foundry.VeraGreenfieldFoundryProvider +import org.verapdf.pdfa.Foundries +import org.verapdf.pdfa.flavours.PDFAFlavour +import org.verapdf.pdfa.results.ValidationResult +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream + +/** + * Bruker VeraPDF for å validere at produsert pdf er gyldig PDFA og dermed egnet for arkivering + * + * + * For dokumentasjon, se https://docs.verapdf.org/develop/ + */ +object PdfaValidator { + + fun validatePdf(pdf: ByteArray?) { + try { + validatePdf(ByteArrayInputStream(pdf)) + } catch (e: ModelParsingException) { + throw PdfaValideringException("Feil ved parsing av pdf modell", e) + } catch (e: EncryptedPdfException) { + throw PdfaValideringException("Klarer ikke å håndtere kryptert pdf", e) + } catch (e: IOException) { + throw PdfaValideringException("IO exception ved validering av pdf", e) + } catch (e: ValidationException) { + throw PdfaValideringException("Validering av pdf feilet", e) + } + } + + private fun validatePdf(inputStream: InputStream?) { + val flavour: PDFAFlavour = PDFAFlavour.fromString("2u") + Foundries.defaultInstance().createValidator(flavour, false).use { validator -> + Foundries.defaultInstance().createParser(inputStream, flavour).use { parser -> + val result: ValidationResult = validator.validate(parser) + if (!result.isCompliant) { + throw PdfaValideringException(result) + } + } + } + } + + init { + VeraGreenfieldFoundryProvider.initialise() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/validering/PdfaValideringException.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/validering/PdfaValideringException.kt new file mode 100644 index 000000000..1fc6b0d60 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/pdfgen/validering/PdfaValideringException.kt @@ -0,0 +1,23 @@ +package no.nav.familie.tilbake.pdfgen.validering + +import org.verapdf.pdfa.results.TestAssertion +import org.verapdf.pdfa.results.ValidationResult +import java.util.stream.Collectors + +class PdfaValideringException : RuntimeException { + constructor(message: String?) : super(message) + constructor(message: String?, cause: Throwable?) : super(message, cause) + constructor(result: ValidationResult) : this(formater(result)) + + companion object { + + private fun formater(result: ValidationResult): String { + val feilmeldinger: List = result.testAssertions.stream() + .filter { ta -> ta.status !== TestAssertion.Status.PASSED } + .map { ta -> ta.status.toString() + ":" + ta.message } + .collect(Collectors.toList()) + return "Validering av pdf feilet. Validerer versjon " + result.pdfaFlavour + .toString() + " feil er: " + java.lang.String.join(", ", feilmeldinger) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/person/PersonService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/person/PersonService.kt new file mode 100644 index 000000000..cde4d7753 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/person/PersonService.kt @@ -0,0 +1,50 @@ +package no.nav.familie.tilbake.person + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.personopplysning.ADRESSEBESKYTTELSEGRADERING +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.event.EndretPersonIdentEventPublisher +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.integration.pdl.PdlClient +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import org.springframework.stereotype.Service + +@Service +class PersonService( + private val pdlClient: PdlClient, + private val fagsakRepository: FagsakRepository, + private val endretPersonIdentEventPublisher: EndretPersonIdentEventPublisher, +) { + + fun hentPersoninfo(personIdent: String, fagsystem: Fagsystem): Personinfo { + val personInfo = pdlClient.hentPersoninfo(personIdent, fagsystem) + // fire event for å oppdatere personIdent når lagret personIdent ikke matcher med PDL. + if (personIdent != personInfo.ident) { + val fagsak = fagsakRepository.finnFagsakForFagsystemAndIdent(fagsystem, personIdent) + ?: throw Feil("Finnes ikke fagsak") + endretPersonIdentEventPublisher.fireEvent(personInfo.ident, fagsak.id) + } + return personInfo + } + + fun hentIdenterMedStrengtFortroligAdressebeskyttelse(personIdenter: List, fagsystem: Fagsystem): List { + val adresseBeskyttelseBolk = pdlClient.hentAdressebeskyttelseBolk(personIdenter, fagsystem) + return adresseBeskyttelseBolk.filter { (_, person) -> + person.adressebeskyttelse.any { adressebeskyttelse -> + adressebeskyttelse.gradering == ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG || + adressebeskyttelse.gradering == ADRESSEBESKYTTELSEGRADERING.STRENGT_FORTROLIG_UTLAND + } + }.map { it.key } + } + + fun hentAktørId(personIdent: String, fagsystem: Fagsystem): List { + val hentIdenter = pdlClient.hentIdenter(personIdent, fagsystem) + return hentIdenter.data.pdlIdenter!!.identer.filter { it.gruppe == "AKTORID" }.map { it.ident } + } + + fun hentAktivAktørId(ident: String, fagsystem: Fagsystem): String { + val aktørId = hentAktørId(ident, fagsystem) + if (aktørId.isEmpty()) error("Finner ingen aktiv aktørId for ident") + return aktørId.first() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/AuditLogger.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/AuditLogger.kt new file mode 100644 index 000000000..27c52a729 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/AuditLogger.kt @@ -0,0 +1,82 @@ +package no.nav.familie.tilbake.sikkerhet + +import jakarta.servlet.http.HttpServletRequest +import no.nav.familie.log.mdc.MDCConstants +import no.nav.familie.tilbake.common.ContextService +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.web.context.request.RequestContextHolder +import org.springframework.web.context.request.ServletRequestAttributes + +/** + * [custom1], [custom2], [custom3] brukes for å logge ekstra felter, eks fagsak, behandling, + * disse logges til cs3,cs5,cs6 då cs1,cs2 og cs4 er til internt bruk + * Kan brukes med eks CustomKeyValue(key=fagsak, value=fagsakId) + */ +data class Sporingsdata( + val event: AuditLoggerEvent, + val personIdent: String, + val custom1: CustomKeyValue? = null, + val custom2: CustomKeyValue? = null, + val custom3: CustomKeyValue? = null, +) + +enum class AuditLoggerEvent(val type: String) { + CREATE("create"), + UPDATE("update"), + ACCESS("access"), + NONE("Not logged"), +} + +data class CustomKeyValue(val key: String, val value: String) + +@Component +class AuditLogger(@Value("\${NAIS_APP_NAME:appName}") private val applicationName: String) { + + private val logger = LoggerFactory.getLogger(javaClass) + private val audit = LoggerFactory.getLogger("auditLogger") + + fun log(data: Sporingsdata) { + val request = getRequest() ?: throw IllegalArgumentException("Ikke brukt i context av en HTTP request") + + if (!ContextService.erMaskinTilMaskinToken()) { + audit.info(createAuditLogString(data, request)) + } else { + logger.debug("Maskin til maskin token i request") + } + } + + private fun getRequest(): HttpServletRequest? { + return RequestContextHolder.getRequestAttributes() + ?.takeIf { it is ServletRequestAttributes } + ?.let { it as ServletRequestAttributes } + ?.request + } + + private fun createAuditLogString(data: Sporingsdata, request: HttpServletRequest): String { + val timestamp = System.currentTimeMillis() + val name = "Saksbehandling" + return "CEF:0|Familie|$applicationName|1.0|audit:${data.event.type}|$name|INFO|end=$timestamp " + + "suid=${ContextService.hentSaksbehandler()} " + + "duid=${data.personIdent} " + + "sproc=${getCallId()} " + + "requestMethod=${request.method} " + + "request=${request.requestURI} " + + createCustomString(data) + } + + private fun createCustomString(data: Sporingsdata): String { + return listOfNotNull( + data.custom1?.let { "cs3Label=${it.key} cs3=${it.value}" }, + data.custom2?.let { "cs5Label=${it.key} cs5=${it.value}" }, + data.custom3?.let { "cs6Label=${it.key} cs6=${it.value}" }, + ) + .joinToString(" ") + } + + private fun getCallId(): String { + return MDC.get(MDCConstants.MDC_CALL_ID) ?: throw IllegalStateException("Mangler callId") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/Behandlerrolle.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/Behandlerrolle.kt new file mode 100644 index 000000000..bb3a468c6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/Behandlerrolle.kt @@ -0,0 +1,11 @@ +package no.nav.familie.tilbake.sikkerhet + +enum class Behandlerrolle(val nivå: Int) { + SYSTEM(5), + BESLUTTER(4), + SAKSBEHANDLER(3), + FORVALTER(2), + VEILEDER(1), +} + +class InnloggetBrukertilgang(val tilganger: Map) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/Rolletilgangssjekk.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/Rolletilgangssjekk.kt new file mode 100644 index 000000000..b00c21b43 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/Rolletilgangssjekk.kt @@ -0,0 +1,11 @@ +package no.nav.familie.tilbake.sikkerhet + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Rolletilgangssjekk( + val minimumBehandlerrolle: Behandlerrolle, + val handling: String, + val auditLoggerEvent: AuditLoggerEvent, + val henteParam: HenteParam = HenteParam.INGEN, +) // brukes kun i GET request/request uten body diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/TilgangAdvice.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/TilgangAdvice.kt new file mode 100644 index 000000000..ce273a7aa --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/TilgangAdvice.kt @@ -0,0 +1,399 @@ +package no.nav.familie.tilbake.sikkerhet + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.api.dto.BehandlingsstegFatteVedtaksstegDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.FagsystemUtil +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.config.RolleConfig +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattRepository +import org.aspectj.lang.JoinPoint +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Before +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.web.context.request.RequestContextHolder +import org.springframework.web.context.request.ServletRequestAttributes +import java.math.BigInteger +import java.util.UUID +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.declaredMemberProperties + +enum class HenteParam { + BEHANDLING_ID, + YTELSESTYPE_OG_EKSTERN_FAGSAK_ID, + FAGSYSTEM_OG_EKSTERN_FAGSAK_ID, + MOTTATT_XML_ID, + EKSTERN_KRAVGRUNNLAG_ID, + INGEN, +} + +@Aspect +@Configuration +class TilgangAdvice( + private val rolleConfig: RolleConfig, + private val fagsakRepository: FagsakRepository, + private val behandlingRepository: BehandlingRepository, + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val auditLogger: AuditLogger, + private val økonomiXmlMottattRepository: ØkonomiXmlMottattRepository, + private val integrasjonerClient: IntegrasjonerClient, +) { + + private val feltnavnFagsystem = "fagsystem" + private val feltnavnYtelsestype = "ytelsestype" + private val feltnavnBehandlingId = "behandlingId" + private val feltnavnEksternBrukId = "eksternBrukId" + private val feltnavnEksternFagsakId = "eksternFagsakId" + + private val logger: Logger = LoggerFactory.getLogger(this.javaClass) + + @Before("@annotation(rolletilgangssjekk) ") + fun sjekkTilgang(joinpoint: JoinPoint, rolletilgangssjekk: Rolletilgangssjekk) { + if (ContextService.hentSaksbehandler() == Constants.BRUKER_ID_VEDTAKSLØSNINGEN) { + // når behandler har system tilgang, trenges ikke det validering på fagsystem eller rolle + return + } + + val httpRequest = (RequestContextHolder.currentRequestAttributes() as ServletRequestAttributes).request + + if (HttpMethod.GET.matches(httpRequest.method) || rolletilgangssjekk.henteParam != HenteParam.INGEN) { + validateFagsystemTilgangIGetRequest( + rolletilgangssjekk.henteParam, + joinpoint.args, + rolletilgangssjekk, + ) + } else if (HttpMethod.POST.matches(httpRequest.method) || HttpMethod.PUT.matches(httpRequest.method)) { + validateFagsystemTilgangIPostRequest( + joinpoint.args[0], + rolletilgangssjekk, + ) + } else { + logger.error("${httpRequest.requestURI} støtter ikke tilgangssjekk") + } + } + + private fun validateFagsystemTilgangIGetRequest( + param: HenteParam, + requestBody: Array, + rolletilgangssjekk: Rolletilgangssjekk, + ) { + when (param) { + HenteParam.BEHANDLING_ID -> { + val behandlingId = requestBody.first() as UUID + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + + var behandlerRolle = rolletilgangssjekk.minimumBehandlerrolle + if (requestBody.size > 1) { + behandlerRolle = bestemBehandlerRolleForUtførFatteVedtakSteg( + requestBody[1], + rolletilgangssjekk.minimumBehandlerrolle, + ) + } + validate( + fagsystem = fagsak.fagsystem, + minimumBehandlerrolle = behandlerRolle, + fagsak = fagsak, + handling = rolletilgangssjekk.handling, + ) + logAccess(rolletilgangssjekk, fagsak, behandling) + } + HenteParam.YTELSESTYPE_OG_EKSTERN_FAGSAK_ID -> { + val ytelsestype = Ytelsestype.valueOf(requestBody.first().toString()) + val fagsystem = FagsystemUtil.hentFagsystemFraYtelsestype(ytelsestype) + val eksternFagsakId = requestBody[1].toString() + val fagsak = fagsakRepository.findByFagsystemAndEksternFagsakId(fagsystem, eksternFagsakId) + + validate( + fagsystem = fagsystem, + minimumBehandlerrolle = rolletilgangssjekk.minimumBehandlerrolle, + fagsak = fagsak, + handling = rolletilgangssjekk.handling, + ) + logAccess(rolletilgangssjekk, fagsak) + } + HenteParam.FAGSYSTEM_OG_EKSTERN_FAGSAK_ID -> { + val fagsystem = Fagsystem.valueOf(requestBody.first().toString()) + val eksternFagsakId = requestBody[1].toString() + val fagsak = fagsakRepository.findByFagsystemAndEksternFagsakId(fagsystem, eksternFagsakId) + + validate( + fagsystem = fagsystem, + minimumBehandlerrolle = rolletilgangssjekk.minimumBehandlerrolle, + fagsak = fagsak, + handling = rolletilgangssjekk.handling, + ) + logAccess(rolletilgangssjekk, fagsak) + } + HenteParam.MOTTATT_XML_ID -> { + val mottattXmlId = requestBody.first() as UUID + val økonomiXmlMottatt = økonomiXmlMottattRepository.findByIdOrThrow(mottattXmlId) + + validate( + fagsystem = FagsystemUtil.hentFagsystemFraYtelsestype(økonomiXmlMottatt.ytelsestype), + minimumBehandlerrolle = rolletilgangssjekk.minimumBehandlerrolle, + fagsak = null, + handling = rolletilgangssjekk.handling, + ) + } + HenteParam.EKSTERN_KRAVGRUNNLAG_ID -> { + val eksternKravgrunnlagId = requestBody.first() as BigInteger + val økonomiXmlMottatt = økonomiXmlMottattRepository.findByEksternKravgrunnlagId(eksternKravgrunnlagId) + val kravgrunnlag = kravgrunnlagRepository.findByEksternKravgrunnlagIdAndAktivIsTrue(eksternKravgrunnlagId) + if (økonomiXmlMottatt == null && kravgrunnlag == null) { + throw Feil( + message = "Finnes ikke eksternKravgrunnlagId=$eksternKravgrunnlagId", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + val ytelsestype = økonomiXmlMottatt?.ytelsestype ?: kravgrunnlag!!.fagområdekode.ytelsestype + + validate( + fagsystem = FagsystemUtil.hentFagsystemFraYtelsestype(ytelsestype), + minimumBehandlerrolle = rolletilgangssjekk.minimumBehandlerrolle, + fagsak = null, + handling = rolletilgangssjekk.handling, + ) + } + else -> { + kastTilgangssjekkException(rolletilgangssjekk.handling) + } + } + } + + private fun validateFagsystemTilgangIPostRequest( + requestBody: Any, + rolletilgangssjekk: Rolletilgangssjekk, + ) { + val fields: Collection> = requestBody::class.declaredMemberProperties + + val ytelsestypeFraRequest: KProperty1? = fields.find { feltnavnYtelsestype == it.name } + val fagsystemFraRequest = fields.find { feltnavnFagsystem == it.name } + val behandlingIdFraRequest = fields.find { feltnavnBehandlingId == it.name } + val eksternBrukIdFraRequest = fields.find { feltnavnEksternBrukId == it.name } + val eksternFagsakIdFraRequest = fields.find { feltnavnEksternFagsakId == it.name } + + when { + behandlingIdFraRequest != null -> { + val behandlingId: UUID = behandlingIdFraRequest.getter.call(requestBody) as UUID + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + + validate( + fagsystem = fagsak.fagsystem, + minimumBehandlerrolle = rolletilgangssjekk.minimumBehandlerrolle, + fagsak = fagsak, + handling = rolletilgangssjekk.handling, + ) + logAccess(rolletilgangssjekk, fagsak, behandling) + } + eksternBrukIdFraRequest != null -> { + val eksternBrukId: UUID = eksternBrukIdFraRequest.getter.call(requestBody) as UUID + val fagsak = fagsakRepository.finnFagsakForEksternBrukId(eksternBrukId) + + validate( + fagsystem = fagsak.fagsystem, + minimumBehandlerrolle = rolletilgangssjekk.minimumBehandlerrolle, + fagsak = fagsak, + handling = rolletilgangssjekk.handling, + ) + logAccess(rolletilgangssjekk, fagsak) + } + fagsystemFraRequest != null && eksternFagsakIdFraRequest != null -> { + val fagsystem = fagsystemFraRequest.getter.call(requestBody) as Fagsystem + val eksternFagsakId = eksternFagsakIdFraRequest.getter.call(requestBody).toString() + val fagsak = fagsakRepository.findByFagsystemAndEksternFagsakId(fagsystem, eksternFagsakId) + + validate( + fagsystem = fagsystem, + minimumBehandlerrolle = rolletilgangssjekk.minimumBehandlerrolle, + fagsak = fagsak, + handling = rolletilgangssjekk.handling, + ) + logAccess(rolletilgangssjekk, fagsak) + } + ytelsestypeFraRequest != null && eksternFagsakIdFraRequest != null -> { + val ytelsestype = Ytelsestype.valueOf(ytelsestypeFraRequest.getter.call(requestBody).toString()) + val fagsystem = FagsystemUtil.hentFagsystemFraYtelsestype(ytelsestype) + val eksternFagsakId = eksternFagsakIdFraRequest.getter.call(requestBody).toString() + val fagsak = fagsakRepository.findByFagsystemAndEksternFagsakId(fagsystem, eksternFagsakId) + + validate( + fagsystem = fagsystem, + minimumBehandlerrolle = rolletilgangssjekk.minimumBehandlerrolle, + fagsak = fagsak, + handling = rolletilgangssjekk.handling, + ) + logAccess(rolletilgangssjekk, fagsak) + } + ytelsestypeFraRequest != null -> { + val ytelsestype = Ytelsestype.valueOf(ytelsestypeFraRequest.getter.call(requestBody).toString()) + + validate( + fagsystem = FagsystemUtil.hentFagsystemFraYtelsestype(ytelsestype), + minimumBehandlerrolle = rolletilgangssjekk.minimumBehandlerrolle, + fagsak = null, + handling = rolletilgangssjekk.handling, + ) + } + else -> { + kastTilgangssjekkException(rolletilgangssjekk.handling) + } + } + } + + private fun validate( + fagsystem: Fagsystem, + minimumBehandlerrolle: Behandlerrolle, + fagsak: Fagsak?, + handling: String, + ) { + val brukerRolleOgFagsystemstilgang = + ContextService.hentHøyesteRolletilgangOgYtelsestypeForInnloggetBruker(rolleConfig, handling) + + // når behandler har forvaltningstilgang, blir rollen bare validert + if (brukerRolleOgFagsystemstilgang.tilganger.contains(Tilgangskontrollsfagsystem.FORVALTER_TILGANG)) { + validateForvaltingsrolle( + brukerRolleOgFagsystemstilgang = brukerRolleOgFagsystemstilgang, + minimumBehandlerrolle = minimumBehandlerrolle, + handling = handling, + ) + return + } + val tilgangskontrollsfagsystem = Tilgangskontrollsfagsystem.fraFagsystem(fagsystem) + // sjekk om saksbehandler har riktig gruppe å aksessere denne ytelsestypen + validateFagsystem(tilgangskontrollsfagsystem, brukerRolleOgFagsystemstilgang, handling) + + // sjekk om saksbehandler har riktig rolle å aksessere denne ytelsestypen + validateRolle( + brukersrolleTilFagsystemet = brukerRolleOgFagsystemstilgang.tilganger.getValue(tilgangskontrollsfagsystem), + minimumBehandlerrolle = minimumBehandlerrolle, + handling = handling, + ) + + validateEgenAnsattKode6Kode7( + fagsak = fagsak, + handling = handling, + ) + } + + fun logAccess(rolletilgangssjekk: Rolletilgangssjekk, fagsak: Fagsak?, behandling: Behandling? = null) { + fagsak?.let { + auditLogger.log( + Sporingsdata( + rolletilgangssjekk.auditLoggerEvent, + fagsak.bruker.ident, + CustomKeyValue("eksternFagsakId", fagsak.eksternFagsakId), + behandling?.let { + CustomKeyValue("behandlingEksternBrukId", behandling.eksternBrukId.toString()) + }, + ), + ) + } + } + + private fun validateEgenAnsattKode6Kode7( + fagsak: Fagsak?, + handling: String, + ) { + val personerIBehandlingen = fagsak?.bruker?.ident?.let { listOf(it) } ?: return + val tilganger = integrasjonerClient.sjekkTilgangTilPersoner(personerIBehandlingen) + if (tilganger.any { !it.harTilgang }) { + throw Feil( + message = "${ContextService.hentSaksbehandler()} har ikke tilgang til person i $handling", + frontendFeilmelding = "${ContextService.hentSaksbehandler()} har ikke tilgang til person i $handling", + httpStatus = HttpStatus.FORBIDDEN, + ) + } + } + + private fun validateFagsystem( + fagsystem: Tilgangskontrollsfagsystem, + brukerRolleOgFagsystemstilgang: InnloggetBrukertilgang, + handling: String, + ) { + if (!brukerRolleOgFagsystemstilgang.tilganger.contains(fagsystem)) { + throw Feil( + message = "${ContextService.hentSaksbehandler()} har ikke tilgang til $handling", + frontendFeilmelding = "${ContextService.hentSaksbehandler()} har ikke tilgang til $handling", + httpStatus = HttpStatus.FORBIDDEN, + ) + } + } + + private fun validateRolle( + brukersrolleTilFagsystemet: Behandlerrolle, + minimumBehandlerrolle: Behandlerrolle, + handling: String, + ) { + if (minimumBehandlerrolle == Behandlerrolle.FORVALTER) { + throw Feil( + message = "${ContextService.hentSaksbehandler()} med rolle $brukersrolleTilFagsystemet " + + "har ikke tilgang til å kalle forvaltningstjeneste $handling. Krever FORVALTER.", + frontendFeilmelding = "Du har ikke tilgang til å $handling.", + httpStatus = HttpStatus.FORBIDDEN, + ) + } + if (minimumBehandlerrolle.nivå > brukersrolleTilFagsystemet.nivå) { + throw Feil( + message = "${ContextService.hentSaksbehandler()} med rolle $brukersrolleTilFagsystemet " + + "har ikke tilgang til å $handling. Krever $minimumBehandlerrolle.", + frontendFeilmelding = "Du har ikke tilgang til å $handling.", + httpStatus = HttpStatus.FORBIDDEN, + ) + } + } + + private fun validateForvaltingsrolle( + brukerRolleOgFagsystemstilgang: InnloggetBrukertilgang, + minimumBehandlerrolle: Behandlerrolle, + handling: String, + ) { + val tilganger = brukerRolleOgFagsystemstilgang.tilganger + // Forvalter kan kun kalle forvaltningstjenestene og tjenestene som kan kalles av Veileder + if (minimumBehandlerrolle.nivå > Behandlerrolle.FORVALTER.nivå && + tilganger.all { it.value == Behandlerrolle.FORVALTER } + ) { + throw Feil( + message = "${ContextService.hentSaksbehandler()} med rolle FORVALTER " + + "har ikke tilgang til å $handling. Krever $minimumBehandlerrolle.", + frontendFeilmelding = "Du har ikke tilgang til å $handling.", + httpStatus = HttpStatus.FORBIDDEN, + ) + } + } + + private fun kastTilgangssjekkException(handling: String) { + val feilmelding: String = "$handling kan ikke valideres for tilgangssjekk. " + + "Det finnes ikke en av de påkrevde parameterne i request" + throw Feil( + message = feilmelding, + frontendFeilmelding = feilmelding, + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + + private fun bestemBehandlerRolleForUtførFatteVedtakSteg( + requestBody: Any, + minimumBehandlerrolle: Behandlerrolle, + ): Behandlerrolle { + // Behandlerrolle blir endret til Beslutter kun når FatteVedtak steg utføres + if (requestBody is BehandlingsstegFatteVedtaksstegDto) { + return Behandlerrolle.BESLUTTER + } + return minimumBehandlerrolle + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/TilgangService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/TilgangService.kt new file mode 100644 index 000000000..23f1c31ea --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/TilgangService.kt @@ -0,0 +1,32 @@ +package no.nav.familie.tilbake.sikkerhet + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.config.RolleConfig +import org.springframework.stereotype.Service + +@Service +class TilgangService(private val rolleConfig: RolleConfig) { + + fun tilgangTilÅOppretteRevurdering(fagsystem: Fagsystem): Boolean { + return finnBehandlerrolle(fagsystem) !in listOf(Behandlerrolle.VEILEDER, Behandlerrolle.FORVALTER) + } + + fun finnBehandlerrolle(fagsystem: Fagsystem): Behandlerrolle? { + val inloggetBrukerstilgang = ContextService + .hentHøyesteRolletilgangOgYtelsestypeForInnloggetBruker(rolleConfig, "henter behandling") + + val tilganger = inloggetBrukerstilgang.tilganger + var behandlerrolle: Behandlerrolle? = Behandlerrolle.VEILEDER + if (tilganger.containsKey(Tilgangskontrollsfagsystem.SYSTEM_TILGANG)) { + behandlerrolle = Behandlerrolle.SYSTEM + } + if (tilganger.containsKey(Tilgangskontrollsfagsystem.FORVALTER_TILGANG)) { + behandlerrolle = Behandlerrolle.FORVALTER + } + if (tilganger.containsKey(Tilgangskontrollsfagsystem.fraFagsystem(fagsystem))) { + behandlerrolle = tilganger[Tilgangskontrollsfagsystem.fraFagsystem(fagsystem)] + } + return behandlerrolle + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/Tilgangskontrollsfagsystem.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/Tilgangskontrollsfagsystem.kt new file mode 100644 index 000000000..573ec15ab --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/sikkerhet/Tilgangskontrollsfagsystem.kt @@ -0,0 +1,35 @@ +package no.nav.familie.tilbake.sikkerhet + +import no.nav.familie.kontrakter.felles.Fagsystem + +// Denne enum-en brukes kun for tilgangskontroll +enum class Tilgangskontrollsfagsystem(val kode: String) { + + BARNETRYGD("BA"), + ENSLIG_FORELDER("EF"), + KONTANTSTØTTE("KONT"), + FORVALTER_TILGANG("FT"), // brukes internt bare for tilgangsskontroll + SYSTEM_TILGANG(""), // brukes internt bare for tilgangsskontroll + ; + + companion object { + + fun fraKode(kode: String): Tilgangskontrollsfagsystem { + for (fagsystem in values()) { + if (fagsystem.kode == kode) { + return fagsystem + } + } + throw IllegalArgumentException("Fagsystem finnes ikke for kode $kode") + } + + fun fraFagsystem(kontraktFagsystem: Fagsystem): Tilgangskontrollsfagsystem { + for (fagsystem in values()) { + if (fagsystem.kode == kontraktFagsystem.name) { + return fagsystem + } + } + throw IllegalArgumentException("Fagsystem finnes ikke for kode $kontraktFagsystem") + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/totrinn/TotrinnMapper.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/totrinn/TotrinnMapper.kt new file mode 100644 index 000000000..0edd79e79 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/totrinn/TotrinnMapper.kt @@ -0,0 +1,35 @@ +package no.nav.familie.tilbake.totrinn + +import no.nav.familie.tilbake.api.dto.Totrinnsstegsinfo +import no.nav.familie.tilbake.api.dto.TotrinnsvurderingDto +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.totrinn.domain.Totrinnsvurdering + +object TotrinnMapper { + + fun tilRespons( + totrinnsvurderinger: List, + behandlingsstegstilstand: List, + ): TotrinnsvurderingDto { + val totrinnsstegsinfo = if (totrinnsvurderinger.isEmpty()) { + hentStegSomGjelderForTotrinn(behandlingsstegstilstand) + } else { + totrinnsvurderinger.map { + Totrinnsstegsinfo( + behandlingssteg = it.behandlingssteg, + godkjent = it.godkjent, + begrunnelse = it.begrunnelse, + ) + } + hentStegSomGjelderForTotrinn(behandlingsstegstilstand) // Ny behandlingssteg kan være gyldig for totrinn + .filter { stegstilstand -> totrinnsvurderinger.none { it.behandlingssteg == stegstilstand.behandlingssteg } } + } + return TotrinnsvurderingDto(totrinnsstegsinfo = totrinnsstegsinfo.sortedBy { it.behandlingssteg.sekvens }) + } + + private fun hentStegSomGjelderForTotrinn(behandlingsstegstilstand: List) = + behandlingsstegstilstand.filter { + it.behandlingssteg.kanBesluttes && + it.behandlingsstegsstatus != Behandlingsstegstatus.AUTOUTFØRT + }.map { Totrinnsstegsinfo(behandlingssteg = it.behandlingssteg) } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/totrinn/TotrinnService.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/totrinn/TotrinnService.kt new file mode 100644 index 000000000..e15b259ec --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/totrinn/TotrinnService.kt @@ -0,0 +1,128 @@ +package no.nav.familie.tilbake.totrinn + +import no.nav.familie.tilbake.api.dto.TotrinnsvurderingDto +import no.nav.familie.tilbake.api.dto.VurdertTotrinnDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.totrinn.domain.Totrinnsvurdering +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class TotrinnService( + private val behandlingRepository: BehandlingRepository, + private val behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository, + private val totrinnsvurderingRepository: TotrinnsvurderingRepository, +) { + + @Transactional(readOnly = true) + fun hentTotrinnsvurderinger(behandlingId: UUID): TotrinnsvurderingDto { + val totrinnsvurderinger = totrinnsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + return TotrinnMapper.tilRespons(totrinnsvurderinger, behandlingsstegstilstand) + } + + @Transactional + fun lagreTotrinnsvurderinger(behandlingId: UUID, totrinnsvurderinger: List) { + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + // valider request + validerOmAlleBesluttendeStegFinnes(totrinnsvurderinger, behandlingsstegstilstand) + + // deaktiver eksisterende totrinnsvurderinger + val eksisterendeTotrinnsvurdering = totrinnsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + eksisterendeTotrinnsvurdering.forEach { totrinnsvurderingRepository.update(it.copy(aktiv = false)) } + + totrinnsvurderinger.filter { finnOmStegKanBesluttes(it.behandlingssteg, behandlingsstegstilstand) } + .forEach { + totrinnsvurderingRepository.insert( + Totrinnsvurdering( + behandlingId = behandlingId, + behandlingssteg = it.behandlingssteg, + godkjent = it.godkjent, + begrunnelse = it.begrunnelse, + ), + ) + } + } + + @Transactional + fun lagreFastTotrinnsvurderingerForAutomatiskSaksbehandling(behandlingId: UUID) { + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + val totrinnsvurderinger = behandlingsstegstilstand.filter { it.behandlingssteg.kanBesluttes }.map { + Totrinnsvurdering( + behandlingId = behandlingId, + behandlingssteg = it.behandlingssteg, + godkjent = true, + begrunnelse = Constants.AUTOMATISK_SAKSBEHANDLING_BEGUNNLESE, + ) + } + totrinnsvurderinger.forEach { totrinnsvurderingRepository.insert(it) } + } + + fun validerAnsvarligBeslutter(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + if (behandling.ansvarligSaksbehandler == ContextService.hentSaksbehandler()) { + throw Feil( + message = "ansvarlig beslutter kan ikke være samme som ansvarlig saksbehandler", + frontendFeilmelding = "ansvarlig beslutter kan ikke være samme som ansvarlig saksbehandler", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + + fun finnesUnderkjenteStegITotrinnsvurdering(behandlingId: UUID): Boolean { + return totrinnsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId).any { !it.godkjent } + } + + @Transactional + fun oppdaterAnsvarligBeslutter(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update(behandling.copy(ansvarligBeslutter = ContextService.hentSaksbehandler())) + } + + @Transactional + fun fjernAnsvarligBeslutter(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update(behandling.copy(ansvarligBeslutter = null)) + } + + private fun finnOmStegKanBesluttes( + behandlingssteg: Behandlingssteg, + behandlingsstegstilstand: List, + ): Boolean { + return behandlingsstegstilstand.any { + behandlingssteg == it.behandlingssteg && + it.behandlingssteg.kanBesluttes && + it.behandlingsstegsstatus != Behandlingsstegstatus.AUTOUTFØRT + } + } + + private fun validerOmAlleBesluttendeStegFinnes( + totrinnsvurderinger: List, + behandlingsstegstilstand: List, + ) { + val stegSomBørVurderes: List = behandlingsstegstilstand.filter { + it.behandlingssteg.kanBesluttes && + it.behandlingsstegsstatus != Behandlingsstegstatus.AUTOUTFØRT + }.map { it.behandlingssteg } + + val vurderteSteg: List = totrinnsvurderinger.map { it.behandlingssteg } + val manglendeSteg = stegSomBørVurderes.minus(vurderteSteg) + if (manglendeSteg.isNotEmpty()) { + throw Feil( + message = "Stegene $manglendeSteg mangler totrinnsvurdering", + frontendFeilmelding = "Stegene $manglendeSteg mangler totrinnsvurdering", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/totrinn/TotrinnsvurderingRepository.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/totrinn/TotrinnsvurderingRepository.kt new file mode 100644 index 000000000..4bf3e9c0b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/totrinn/TotrinnsvurderingRepository.kt @@ -0,0 +1,15 @@ +package no.nav.familie.tilbake.totrinn + +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import no.nav.familie.tilbake.totrinn.domain.Totrinnsvurdering +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Repository +@Transactional +interface TotrinnsvurderingRepository : RepositoryInterface, InsertUpdateRepository { + + fun findByBehandlingIdAndAktivIsTrue(behandlingId: UUID): List +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/totrinn/domain/Totrinnsvurdering.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/totrinn/domain/Totrinnsvurdering.kt new file mode 100644 index 000000000..53a48fb20 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/totrinn/domain/Totrinnsvurdering.kt @@ -0,0 +1,22 @@ +package no.nav.familie.tilbake.totrinn.domain + +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Embedded +import java.util.UUID + +data class Totrinnsvurdering( + @Id + val id: UUID = UUID.randomUUID(), + val behandlingId: UUID, + val behandlingssteg: Behandlingssteg, + val godkjent: Boolean, + val begrunnelse: String?, + val aktiv: Boolean = true, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingMapper.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingMapper.kt" new file mode 100644 index 000000000..427398e51 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingMapper.kt" @@ -0,0 +1,273 @@ +package no.nav.familie.tilbake.vilkårsvurdering + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.api.dto.AktivitetDto +import no.nav.familie.tilbake.api.dto.AktsomhetDto +import no.nav.familie.tilbake.api.dto.GodTroDto +import no.nav.familie.tilbake.api.dto.RedusertBeløpDto +import no.nav.familie.tilbake.api.dto.SærligGrunnDto +import no.nav.familie.tilbake.api.dto.VilkårsvurderingsperiodeDto +import no.nav.familie.tilbake.api.dto.VurdertAktsomhetDto +import no.nav.familie.tilbake.api.dto.VurdertGodTroDto +import no.nav.familie.tilbake.api.dto.VurdertSærligGrunnDto +import no.nav.familie.tilbake.api.dto.VurdertVilkårsvurderingDto +import no.nav.familie.tilbake.api.dto.VurdertVilkårsvurderingsperiodeDto +import no.nav.familie.tilbake.api.dto.VurdertVilkårsvurderingsresultatDto +import no.nav.familie.tilbake.beregning.BeløpsberegningUtil +import no.nav.familie.tilbake.beregning.KravgrunnlagsberegningService +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetalingsperiode +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingAktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingGodTro +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingSærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsperiode +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.UUID + +object VilkårsvurderingMapper { + + fun tilRespons( + vilkårsvurdering: Vilkårsvurdering?, + perioder: List, + foreldetPerioderMedBegrunnelse: Map, + faktaFeilutbetaling: FaktaFeilutbetaling, + kravgrunnlag431: Kravgrunnlag431, + ): VurdertVilkårsvurderingDto { + // allerede behandlet perioder uten perioder som er foreldet + val vilkårsvurdertePerioder = vilkårsvurdering?.perioder + ?.filter { it.periode !in foreldetPerioderMedBegrunnelse } + ?.map { + VurdertVilkårsvurderingsperiodeDto( + periode = it.periode.toDatoperiode(), + feilutbetaltBeløp = beregnFeilutbetaltBeløp(kravgrunnlag431, it.periode), + hendelsestype = hentHendelsestype( + faktaFeilutbetaling.perioder, + it.periode, + ), + reduserteBeløper = utledReduserteBeløp(kravgrunnlag431, it.periode), + aktiviteter = hentAktiviteter(kravgrunnlag431, it.periode), + begrunnelse = it.begrunnelse, + foreldet = false, + vilkårsvurderingsresultatInfo = tilVilkårsvurderingsresultatDto(it), + ) + } + + val ikkeBehandletPerioder = perioder.map { + VurdertVilkårsvurderingsperiodeDto( + periode = it.toDatoperiode(), + feilutbetaltBeløp = beregnFeilutbetaltBeløp(kravgrunnlag431, it), + hendelsestype = hentHendelsestype(faktaFeilutbetaling.perioder, it), + reduserteBeløper = utledReduserteBeløp(kravgrunnlag431, it), + aktiviteter = hentAktiviteter(kravgrunnlag431, it), + foreldet = false, + ) + } + + val foreldetPerioder = foreldetPerioderMedBegrunnelse.map { (periode, begrunnelse) -> + VurdertVilkårsvurderingsperiodeDto( + periode = periode.toDatoperiode(), + feilutbetaltBeløp = beregnFeilutbetaltBeløp(kravgrunnlag431, periode), + hendelsestype = hentHendelsestype(faktaFeilutbetaling.perioder, periode), + reduserteBeløper = utledReduserteBeløp(kravgrunnlag431, periode), + aktiviteter = hentAktiviteter(kravgrunnlag431, periode), + foreldet = true, + begrunnelse = begrunnelse, + ) + } + + val samletPerioder = ikkeBehandletPerioder.toMutableList() + samletPerioder.addAll(foreldetPerioder) + vilkårsvurdertePerioder?.let { samletPerioder.addAll(it) } + + return VurdertVilkårsvurderingDto( + perioder = samletPerioder.sortedBy { it.periode.fom }, + rettsgebyr = Constants.rettsgebyr, + ) + } + + fun tilDomene( + behandlingId: UUID, + vilkårsvurderingsperioder: List, + fagsystem: Fagsystem, + ): Vilkårsvurdering { + val vilkårsvurderingsperiode = vilkårsvurderingsperioder.map { + Vilkårsvurderingsperiode( + periode = Månedsperiode(it.periode.fom, it.periode.tom), + begrunnelse = it.begrunnelse, + vilkårsvurderingsresultat = it.vilkårsvurderingsresultat, + godTro = tilDomeneGodTro(it.godTroDto), + aktsomhet = tilDomeneAktsomhet(it.aktsomhetDto, fagsystem), + ) + }.toSet() + return Vilkårsvurdering( + behandlingId = behandlingId, + perioder = vilkårsvurderingsperiode, + ) + } + + private fun tilVilkårsvurderingsresultatDto(vilkårsvurderingsperiode: Vilkårsvurderingsperiode): VurdertVilkårsvurderingsresultatDto { + return VurdertVilkårsvurderingsresultatDto( + vilkårsvurderingsresultat = vilkårsvurderingsperiode.vilkårsvurderingsresultat, + godTro = tilGodTroDto(vilkårsvurderingsperiode.godTro), + aktsomhet = tilAktsomhetDto(vilkårsvurderingsperiode.aktsomhet), + ) + } + + private fun tilGodTroDto(vilkårsvurderingGodTro: VilkårsvurderingGodTro?): VurdertGodTroDto? { + if (vilkårsvurderingGodTro != null) { + return VurdertGodTroDto( + begrunnelse = vilkårsvurderingGodTro.begrunnelse, + beløpErIBehold = vilkårsvurderingGodTro.beløpErIBehold, + beløpTilbakekreves = vilkårsvurderingGodTro.beløpTilbakekreves, + ) + } + return null + } + + private fun tilDomeneGodTro(godTroDto: GodTroDto?): VilkårsvurderingGodTro? { + if (godTroDto != null) { + return VilkårsvurderingGodTro( + begrunnelse = godTroDto.begrunnelse, + beløpErIBehold = godTroDto.beløpErIBehold, + beløpTilbakekreves = godTroDto.beløpTilbakekreves, + ) + } + return null + } + + private fun tilAktsomhetDto(vilkårsvurderingAktsomhet: VilkårsvurderingAktsomhet?): VurdertAktsomhetDto? { + if (vilkårsvurderingAktsomhet != null) { + return VurdertAktsomhetDto( + aktsomhet = vilkårsvurderingAktsomhet.aktsomhet, + ileggRenter = vilkårsvurderingAktsomhet.ileggRenter, + andelTilbakekreves = vilkårsvurderingAktsomhet.andelTilbakekreves, + beløpTilbakekreves = vilkårsvurderingAktsomhet.manueltSattBeløp, + begrunnelse = vilkårsvurderingAktsomhet.begrunnelse, + særligeGrunnerTilReduksjon = vilkårsvurderingAktsomhet.særligeGrunnerTilReduksjon, + særligeGrunnerBegrunnelse = vilkårsvurderingAktsomhet.særligeGrunnerBegrunnelse, + særligeGrunner = tilSærligGrunnerDto( + vilkårsvurderingAktsomhet + .vilkårsvurderingSærligeGrunner, + ), + tilbakekrevSmåbeløp = vilkårsvurderingAktsomhet.tilbakekrevSmåbeløp, + ) + } + return null + } + + private fun tilDomeneAktsomhet(aktsomhetDto: AktsomhetDto?, fagsystem: Fagsystem): VilkårsvurderingAktsomhet? { + if (aktsomhetDto != null) { + return VilkårsvurderingAktsomhet( + aktsomhet = aktsomhetDto.aktsomhet, + ileggRenter = utledIleggRenter(aktsomhetDto.ileggRenter, fagsystem), + andelTilbakekreves = aktsomhetDto.andelTilbakekreves, + manueltSattBeløp = aktsomhetDto.beløpTilbakekreves, + begrunnelse = aktsomhetDto.begrunnelse, + særligeGrunnerTilReduksjon = aktsomhetDto.særligeGrunnerTilReduksjon, + særligeGrunnerBegrunnelse = aktsomhetDto.særligeGrunnerBegrunnelse, + vilkårsvurderingSærligeGrunner = tilSærligGrunnerDomene(aktsomhetDto.særligeGrunner), + tilbakekrevSmåbeløp = aktsomhetDto.tilbakekrevSmåbeløp, + ) + } + return null + } + + private fun tilSærligGrunnerDto(særligGrunner: Set): List = + særligGrunner.map { + VurdertSærligGrunnDto( + særligGrunn = it.særligGrunn, + begrunnelse = it.begrunnelse, + ) + } + + private fun tilSærligGrunnerDomene(særligGrunner: List?): Set = + særligGrunner?.map { + VilkårsvurderingSærligGrunn( + særligGrunn = it.særligGrunn, + begrunnelse = it.begrunnelse, + ) + }?.toSet() ?: emptySet() + + private fun beregnFeilutbetaltBeløp(kravgrunnlag431: Kravgrunnlag431, periode: Månedsperiode): BigDecimal = + KravgrunnlagsberegningService.beregnFeilutbetaltBeløp(kravgrunnlag431, periode) + .setScale(0, RoundingMode.HALF_UP) + + private fun hentHendelsestype( + faktaPerioder: Set, + vurdertVilkårsperiode: Månedsperiode, + ): Hendelsestype = + faktaPerioder.first { it.periode.overlapper(vurdertVilkårsperiode) }.hendelsestype + + private fun utledReduserteBeløp( + kravgrunnlag431: Kravgrunnlag431, + vurdertVilkårsperiode: Månedsperiode, + ): List { + val perioder = kravgrunnlag431.perioder.filter { vurdertVilkårsperiode.overlapper(it.periode) } + val redusertBeløper = mutableListOf() + // reduserte beløper for SKAT/TREK + perioder.forEach { periode -> + periode.beløp + .filter { Klassetype.SKAT == it.klassetype || Klassetype.TREK == it.klassetype } + .filter { it.opprinneligUtbetalingsbeløp.signum() == -1 } + .forEach { redusertBeløper.add(RedusertBeløpDto(true, it.opprinneligUtbetalingsbeløp.abs())) } + } + // reduserte beløper for JUST(etterbetaling) + perioder.forEach { periode -> + periode.beløp + .filter { Klassetype.JUST == it.klassetype } + .filter { it.opprinneligUtbetalingsbeløp.signum() == 0 && it.nyttBeløp.signum() == 1 } + .forEach { redusertBeløper.add(RedusertBeløpDto(false, it.nyttBeløp)) } + } + return redusertBeløper + } + + private fun hentAktiviteter( + kravgrunnlag431: Kravgrunnlag431, + vurdertVilkårsperiode: Månedsperiode, + ): List { + val perioder = kravgrunnlag431.perioder.filter { vurdertVilkårsperiode.overlapper(it.periode) } + val aktiviteter = mutableListOf() + perioder.forEach { periode -> + periode.beløp + .filter { Klassetype.YTEL == it.klassetype && it.tilbakekrevesBeløp.compareTo(BigDecimal.ZERO) != 0 } + .forEach { + aktiviteter.add( + AktivitetDto( + aktivitet = it.klassekode.aktivitet, + beløp = BeløpsberegningUtil + .beregnBeløpForPeriode( + tilbakekrevesBeløp = it.tilbakekrevesBeløp, + vurderingsperiode = vurdertVilkårsperiode, + kravgrunnlagsperiode = periode.periode, + ), + ), + ) + } + } + // oppsummere samme aktiviteter + val aktivitetMap = mutableMapOf() + aktiviteter.forEach { + val beløp = aktivitetMap[it.aktivitet] + if (beløp != null) { + aktivitetMap[it.aktivitet] = beløp.plus(it.beløp) + } else { + aktivitetMap[it.aktivitet] = it.beløp + } + } + return aktivitetMap.map { AktivitetDto(it.key, it.value) } + } + + private fun utledIleggRenter(ileggRenter: Boolean?, fagsystem: Fagsystem): Boolean? { + return when { + ileggRenter != null && listOf(Fagsystem.BA, Fagsystem.KONT).contains(fagsystem) -> false + else -> ileggRenter + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingRepository.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingRepository.kt" new file mode 100644 index 000000000..53ba65542 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingRepository.kt" @@ -0,0 +1,15 @@ +package no.nav.familie.tilbake.vilkårsvurdering + +import no.nav.familie.tilbake.common.repository.InsertUpdateRepository +import no.nav.familie.tilbake.common.repository.RepositoryInterface +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurdering +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Repository +@Transactional +interface VilkårsvurderingRepository : RepositoryInterface, InsertUpdateRepository { + + fun findByBehandlingIdAndAktivIsTrue(behandlingId: UUID): Vilkårsvurdering? +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingService.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingService.kt" new file mode 100644 index 000000000..c035efb4d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingService.kt" @@ -0,0 +1,127 @@ +package no.nav.familie.tilbake.vilkårsvurdering + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.api.dto.AktsomhetDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegVilkårsvurderingDto +import no.nav.familie.tilbake.api.dto.VilkårsvurderingsperiodeDto +import no.nav.familie.tilbake.api.dto.VurdertVilkårsvurderingDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingService +import no.nav.familie.tilbake.foreldelse.ForeldelseService +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class VilkårsvurderingService( + val vilkårsvurderingRepository: VilkårsvurderingRepository, + val kravgrunnlagRepository: KravgrunnlagRepository, + val fagsakRepository: FagsakRepository, + val behandlingRepository: BehandlingRepository, + val foreldelseService: ForeldelseService, + val faktaFeilutbetalingService: FaktaFeilutbetalingService, +) { + + fun hentVilkårsvurdering(behandlingId: UUID): VurdertVilkårsvurderingDto { + val faktaOmFeilutbetaling = faktaFeilutbetalingService.hentAktivFaktaOmFeilutbetaling(behandlingId) + ?: throw Feil( + message = "Fakta om feilutbetaling finnes ikke for behandling=$behandlingId, " + + "kan ikke hente vilkårsvurdering", + ) + val kravgrunnlag431 = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + val vilkårsvurdering = vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + val perioder = mutableListOf() + val foreldetPerioderMedBegrunnelse = mutableMapOf() + val vurdertForeldelse = foreldelseService.hentAktivVurdertForeldelse(behandlingId) + if (vurdertForeldelse == null) { + // fakta perioder + faktaOmFeilutbetaling.perioder + .filter { !erPeriodeAlleredeVurdert(vilkårsvurdering, it.periode) } + .forEach { perioder.add(it.periode) } + } else { + // Ikke foreldet perioder uten perioder som allerede vurdert i vilkårsvurdering + vurdertForeldelse.foreldelsesperioder.filter { !it.erForeldet() } + .filter { !erPeriodeAlleredeVurdert(vilkårsvurdering, it.periode) } + .forEach { perioder.add(it.periode) } + // foreldet perioder + vurdertForeldelse.foreldelsesperioder.filter { it.erForeldet() } + .forEach { foreldetPerioderMedBegrunnelse[it.periode] = it.begrunnelse } + } + return VilkårsvurderingMapper.tilRespons( + vilkårsvurdering = vilkårsvurdering, + perioder = perioder.toList(), + foreldetPerioderMedBegrunnelse = foreldetPerioderMedBegrunnelse.toMap(), + faktaFeilutbetaling = faktaOmFeilutbetaling, + kravgrunnlag431 = kravgrunnlag431, + ) + } + + @Transactional + fun lagreVilkårsvurdering(behandlingId: UUID, behandlingsstegVilkårsvurderingDto: BehandlingsstegVilkårsvurderingDto) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsystem = fagsakRepository.findByIdOrThrow(behandling.fagsakId).fagsystem + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + // Valider request + VilkårsvurderingValidator.validerVilkårsvurdering( + vilkårsvurderingDto = behandlingsstegVilkårsvurderingDto, + kravgrunnlag431 = kravgrunnlag, + ) + // filter bort perioder som er foreldet + val ikkeForeldetPerioder = behandlingsstegVilkårsvurderingDto.vilkårsvurderingsperioder + .filter { !foreldelseService.erPeriodeForeldet(behandlingId, Månedsperiode(it.periode.fom, it.periode.tom)) } + deaktiverEksisterendeVilkårsvurdering(behandlingId) + vilkårsvurderingRepository.insert( + VilkårsvurderingMapper.tilDomene( + behandlingId = behandlingId, + vilkårsvurderingsperioder = ikkeForeldetPerioder, + fagsystem = fagsystem, + ), + ) + } + + @Transactional + fun lagreFastVilkårForAutomatiskSaksbehandling(behandlingId: UUID) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val fagsystem = fagsakRepository.findByIdOrThrow(behandling.fagsakId).fagsystem + + val perioder = hentVilkårsvurdering(behandlingId).perioder + val vurdertePerioder = perioder.filter { !it.foreldet }.map { + VilkårsvurderingsperiodeDto( + periode = it.periode, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + begrunnelse = Constants.AUTOMATISK_SAKSBEHANDLING_BEGUNNLESE, + aktsomhetDto = AktsomhetDto( + aktsomhet = Aktsomhet.SIMPEL_UAKTSOMHET, + tilbakekrevSmåbeløp = false, + begrunnelse = Constants.AUTOMATISK_SAKSBEHANDLING_BEGUNNLESE, + ), + ) + } + vilkårsvurderingRepository.insert( + VilkårsvurderingMapper.tilDomene( + behandlingId = behandlingId, + vilkårsvurderingsperioder = vurdertePerioder, + fagsystem = fagsystem, + ), + ) + } + + @Transactional + fun deaktiverEksisterendeVilkårsvurdering(behandlingId: UUID) { + vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId)?.copy(aktiv = false)?.let { + vilkårsvurderingRepository.update(it) + } + } + + private fun erPeriodeAlleredeVurdert(vilkårsvurdering: Vilkårsvurdering?, periode: Månedsperiode): Boolean { + return vilkårsvurdering?.perioder?.any { periode.inneholder(it.periode) } == true + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingValidator.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingValidator.kt" new file mode 100644 index 000000000..48c6d35ac --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingValidator.kt" @@ -0,0 +1,84 @@ +package no.nav.familie.tilbake.vilkårsvurdering + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.api.dto.AktsomhetDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegVilkårsvurderingDto +import no.nav.familie.tilbake.api.dto.VilkårsvurderingsperiodeDto +import no.nav.familie.tilbake.beregning.KravgrunnlagsberegningService +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import org.springframework.http.HttpStatus +import java.math.BigDecimal + +object VilkårsvurderingValidator { + + @Throws(Feil::class) + fun validerVilkårsvurdering(vilkårsvurderingDto: BehandlingsstegVilkårsvurderingDto, kravgrunnlag431: Kravgrunnlag431) { + vilkårsvurderingDto.vilkårsvurderingsperioder.forEach { + validerAndelTilbakekrevesBeløp(it.aktsomhetDto) + validerAnnetBegrunnelse(it.aktsomhetDto) + validerBeløp(kravgrunnlag431, Månedsperiode(it.periode.fom, it.periode.tom), it) + } + } + + private fun validerAndelTilbakekrevesBeløp(aktsomhetDto: AktsomhetDto?) { + if (aktsomhetDto?.andelTilbakekreves?.compareTo(BigDecimal(100)) == 1) { + throw Feil( + message = "Andel som skal tilbakekreves kan ikke være mer enn 100 prosent", + frontendFeilmelding = "Andel som skal tilbakekreves kan ikke være mer enn 100 prosent", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + + private fun validerAnnetBegrunnelse(aktsomhetDto: AktsomhetDto?) { + if (aktsomhetDto?.særligeGrunner != null) { + val særligGrunner = aktsomhetDto.særligeGrunner + when { + særligGrunner.any { SærligGrunn.ANNET != it.særligGrunn && it.begrunnelse != null } -> { + throw Feil( + message = "Begrunnelse kan fylles ut kun for ANNET begrunnelse", + frontendFeilmelding = "Begrunnelse kan fylles ut kun for ANNET begrunnelse", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + særligGrunner.any { SærligGrunn.ANNET == it.særligGrunn && it.begrunnelse == null } -> { + throw Feil( + message = "ANNET særlig grunner må ha ANNET begrunnelse", + frontendFeilmelding = "ANNET særlig grunner må ha ANNET begrunnelse", + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + } + } + + private fun validerBeløp( + kravgrunnlag431: Kravgrunnlag431, + periode: Månedsperiode, + vilkårsvurderingsperiode: VilkårsvurderingsperiodeDto, + ) { + val feilMelding = "Beløp som skal tilbakekreves kan ikke være mer enn feilutbetalt beløp" + if (vilkårsvurderingsperiode.godTroDto?.beløpTilbakekreves != null) { + val feilutbetalteBeløp = KravgrunnlagsberegningService.beregnFeilutbetaltBeløp(kravgrunnlag431, periode) + if (vilkårsvurderingsperiode.godTroDto.beløpTilbakekreves > feilutbetalteBeløp) { + throw Feil( + message = feilMelding, + frontendFeilmelding = feilMelding, + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + if (vilkårsvurderingsperiode.aktsomhetDto?.beløpTilbakekreves != null) { + val feilutbetalteBeløp = KravgrunnlagsberegningService.beregnFeilutbetaltBeløp(kravgrunnlag431, periode) + if (vilkårsvurderingsperiode.aktsomhetDto.beløpTilbakekreves > feilutbetalteBeløp) { + throw Feil( + message = feilMelding, + frontendFeilmelding = feilMelding, + httpStatus = HttpStatus.BAD_REQUEST, + ) + } + } + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/domain/Vilk\303\245rsvurdering.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/domain/Vilk\303\245rsvurdering.kt" new file mode 100644 index 000000000..6b25275ba --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/domain/Vilk\303\245rsvurdering.kt" @@ -0,0 +1,158 @@ +package no.nav.familie.tilbake.vilkårsvurdering.domain + +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.common.repository.Sporbar +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Embedded +import org.springframework.data.relational.core.mapping.MappedCollection +import org.springframework.data.relational.core.mapping.Table +import java.math.BigDecimal +import java.util.UUID + +@Table("vilkarsvurdering") +data class Vilkårsvurdering( + @Id + val id: UUID = UUID.randomUUID(), + val behandlingId: UUID, + val aktiv: Boolean = true, + @MappedCollection(idColumn = "vilkarsvurdering_id") + val perioder: Set = setOf(), + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +@Table("vilkarsvurderingsperiode") +data class Vilkårsvurderingsperiode( + @Id + val id: UUID = UUID.randomUUID(), + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val periode: Månedsperiode, + @Column("vilkarsvurderingsresultat") + val vilkårsvurderingsresultat: Vilkårsvurderingsresultat, + val begrunnelse: String, + @MappedCollection(idColumn = "vilkarsvurderingsperiode_id") + val aktsomhet: VilkårsvurderingAktsomhet? = null, + @MappedCollection(idColumn = "vilkarsvurderingsperiode_id") + val godTro: VilkårsvurderingGodTro? = null, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +@Table("vilkarsvurdering_god_tro") +data class VilkårsvurderingGodTro( + @Id + val id: UUID = UUID.randomUUID(), + @Column("belop_er_i_behold") + val beløpErIBehold: Boolean, + @Column("belop_tilbakekreves") + val beløpTilbakekreves: BigDecimal? = null, + val begrunnelse: String, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) { + + val beløpSomErIBehold get() = if (this.beløpErIBehold) beløpTilbakekreves else BigDecimal.ZERO +} + +@Table("vilkarsvurdering_aktsomhet") +data class VilkårsvurderingAktsomhet( + @Id + val id: UUID = UUID.randomUUID(), + val aktsomhet: Aktsomhet, + val ileggRenter: Boolean? = null, + val andelTilbakekreves: BigDecimal? = null, + @Column("manuelt_satt_belop") + val manueltSattBeløp: BigDecimal? = null, + val begrunnelse: String, + @Column("serlige_grunner_til_reduksjon") + val særligeGrunnerTilReduksjon: Boolean = false, + @Column("tilbakekrev_smabelop") + val tilbakekrevSmåbeløp: Boolean = true, + @MappedCollection(idColumn = "vilkarsvurdering_aktsomhet_id") + val vilkårsvurderingSærligeGrunner: Set = setOf(), + @Column("serlige_grunner_begrunnelse") + val særligeGrunnerBegrunnelse: String? = null, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) { + + init { + require(!(andelTilbakekreves != null && manueltSattBeløp != null)) { "Kan ikke sette både prosenterSomTilbakekreves og beløpSomTilbakekreves" } + if (aktsomhet == Aktsomhet.FORSETT) { + check(!særligeGrunnerTilReduksjon) { "Ved FORSETT skal ikke særligeGrunnerTilReduksjon settes her" } + check(manueltSattBeløp == null) { "Ved FORSETT er beløp automatisk, og skal ikke settes her" } + check(andelTilbakekreves == null) { "Ved FORSETT er andel automatisk, og skal ikke settes her" } + check(tilbakekrevSmåbeløp) { "Dette er gyldig bare for Simpel uaktsom" } + } + if (aktsomhet == Aktsomhet.GROV_UAKTSOMHET) { + check(tilbakekrevSmåbeløp) { "Dette er gyldig bare for Simpel uaktsom" } + } + } + + val skalHaSærligeGrunner + get() = Aktsomhet.GROV_UAKTSOMHET == aktsomhet || Aktsomhet.SIMPEL_UAKTSOMHET == aktsomhet && this.tilbakekrevSmåbeløp + + val særligeGrunner get() = vilkårsvurderingSærligeGrunner.map(VilkårsvurderingSærligGrunn::særligGrunn) +} + +@Table("vilkarsvurdering_serlig_grunn") +data class VilkårsvurderingSærligGrunn( + @Id + val id: UUID = UUID.randomUUID(), + @Column("serlig_grunn") + val særligGrunn: SærligGrunn, + val begrunnelse: String?, + @Version + val versjon: Long = 0, + @Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) + val sporbar: Sporbar = Sporbar(), +) + +enum class SærligGrunn(val navn: String) { + GRAD_AV_UAKTSOMHET("Graden av uaktsomhet hos den kravet retter seg mot"), + HELT_ELLER_DELVIS_NAVS_FEIL("Om feilen helt eller delvis kan tilskrives NAV"), + STØRRELSE_BELØP("Størrelsen på feilutbetalt beløp"), + TID_FRA_UTBETALING("Hvor lang tid siden utbetalingen fant sted"), + ANNET("Annet"), +} + +interface Vurdering { + + val navn: String +} + +enum class Aktsomhet(override val navn: String) : Vurdering { + FORSETT("Forsett"), + GROV_UAKTSOMHET("Grov uaktsomhet"), + SIMPEL_UAKTSOMHET("Simpel uaktsomhet"), +} + +enum class AnnenVurdering(override val navn: String) : Vurdering { + + GOD_TRO("Handlet i god tro"), + FORELDET("Foreldet"), +} + +enum class Vilkårsvurderingsresultat(val navn: String) { + FORSTO_BURDE_FORSTÅTT("Ja, mottaker forsto eller burde forstått at utbetalingen skyldtes en feil (1. ledd, 1. punkt)"), + MANGELFULLE_OPPLYSNINGER_FRA_BRUKER( + "Ja, mottaker har forårsaket feilutbetalingen ved forsett " + + "eller uaktsomt gitt mangelfulle opplysninger (1. ledd, 2 punkt)", + ), + FEIL_OPPLYSNINGER_FRA_BRUKER( + "Ja, mottaker har forårsaket feilutbetalingen ved forsett eller " + + "uaktsomt gitt feilaktige opplysninger (1. ledd, 2 punkt)", + ), + GOD_TRO("Nei, mottaker har mottatt beløpet i god tro (1. ledd)"), + UDEFINERT("Ikke Definert"), +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application-dev.yaml b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application-dev.yaml new file mode 100644 index 000000000..c363cff13 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application-dev.yaml @@ -0,0 +1,51 @@ + +spring: + datasource: + url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/familie-tilbake + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + + +oppdrag.mq: + queuemanager: MQLS02 + kravgrunnlag: QA.Q1_FAMILIE_TILBAKE.KRAVGRUNNLAG + channel: Q1_FAMILIE_TILBAKE + hostname: b27apvl220.preprod.local + port: 1413 + enabled: true + +rolle: + barnetrygd: + veileder: "93a26831-9866-4410-927b-74ff51a9107c" + saksbehandler: "d21e00a4-969d-4b28-8782-dc818abfae65" + beslutter: "9449c153-5a1e-44a7-84c6-7cc7a8867233" + enslig: + veileder: "19dcbfde-4cdb-4c64-a1ea-ac9802b03339" + saksbehandler: "ee5e0b5e-454c-4612-b931-1fe363df7c2c" + beslutter: "01166863-22f1-4e16-9785-d7a05a22df74" + kontantstøtte: + veileder: "71f503a2-c28f-4394-a05a-8da263ceca4a" + saksbehandler: "c7e0b108-7ae6-432c-9ab4-946174c240c0" + beslutter: "52fe1bef-224f-49df-a40a-29f92d4520f8" + teamfamilie: + forvalter: "c62e908a-cf20-4ad0-b7b3-3ff6ca4bf38b" + prosessering: "928636f4-fd0d-4149-978e-a6fb68bb19de" # 0000-GA-STDAPPS + +FAMILIE_INTEGRASJONER_URL: https://familie-integrasjoner.dev-fss-pub.nais.io +FAMILIE_INTEGRASJONER_SCOPE: api://dev-fss.teamfamilie.familie-integrasjoner/.default +FAMILIE_OPPDRAG_URL: https://familie-oppdrag.dev-fss-pub.nais.io +SECURITYTOKENSERVICE_URL: https://api-gw-q1.oera.no/security-token-service/SecurityTokenServiceProvider/ + +PDL_URL: https://pdl-api.dev-fss-pub.nais.io +PDL_SCOPE: api://dev-fss.pdl.pdl-api/.default +FORELDELSE_ANTALL_MÅNED: 3 +OPPRETTELSE_DAGER_BEGRENSNING: 1 +CRON_HÅNDTER_GAMMEL_KRAVGRUNNLAG: 0 10 * ? * MON-FRI +CRON_AUTOMATISK_SAKSBEHANDLING: 0 20 * ? * MON-FRI +CRON_AUTOMATISK_GJENOPPTA: 0 30 * ? * MON-FRI + +AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_BARNETRYGD: 0 +AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_BARNETILSYN: 1 +AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_OVERGANGSSTØNAD: 0 +AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_SKOLEPENGER: 1 +AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_KONTANTSTØTTE: 1 \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application-e2e.yaml b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application-e2e.yaml new file mode 100644 index 000000000..417d54fe5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application-e2e.yaml @@ -0,0 +1,100 @@ +no.nav.security.jwt: + issuer.azuread: + discoveryurl: http://mock-oauth2-server:1111/v2.0/.well-known/openid-configuration + accepted_audience: api://${TILBAKE_CLIENT_ID}/.default + cookie_name: azure_token + client: + registration: + familie-integrasjoner: + resource-url: ${FAMILIE_INTEGRASJONER_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${TILBAKE_CLIENT_ID} + client-secret: ${TILBAKE_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-integrasjoner-clientcredentials: + resource-url: ${FAMILIE_INTEGRASJONER_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: client_credentials + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${TILBAKE_CLIENT_ID} + client-secret: ${TILBAKE_CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl: + resource-url: ${PDL_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${PDL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl-clientcredentials: + resource-url: ${PDL_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: client_credentials + scope: ${PDL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag: + resource-url: ${FAMILIE_OPPDRAG_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag-clientcredentials: + resource-url: ${FAMILIE_OPPDRAG_URL} + token-endpoint-url: ${AZUREAD_TOKEN_ENDPOINT_URL} + grant-type: client_credentials + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + +logging: + config: "classpath:logback-e2e.xml" + +spring: + datasource: + url: jdbc:postgresql://postgres-tilbake:5432/familie-tilbake + username: familie + password: familie-pwd + cloud: + vault: + database: + role: familie + +STS_URL: http://nav-auth-mock:8200/nais-sts/token +PDL_URL: http://familie-mock-server:1337/rest/api/pdl/ +PDL_SCOPE: api://dummy/.default +SECURITYTOKENSERVICE_URL: https://localhost:8063/soap/SecurityTokenServiceProvider/ + +FAMILIE_INTEGRASJONER_URL: http://familie-integrasjoner:8085 +FAMILIE_OPPDRAG_URL: http://dummy +AZUREAD_TOKEN_ENDPOINT_URL: http://mock-oauth2-server:1111/v2.0/token + +CREDENTIAL_USERNAME: srvfamilie-tilbake +CREDENTIAL_PASSWORD: +API_SCOPE: api://${TILBAKE_CLIENT_ID}/.default + +prosessering: + fixedDelayString: + in: + milliseconds: 2500 + +unleash: + enabled: true + +NAIS_APP_NAME: familie-klage +UNLEASH_SERVER_API_URL: http://localhost:4242/api +UNLEASH_SERVER_API_TOKEN: token +NAIS_CLUSTER_NAME: dev-gcp \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application-fagsak_e2e.yaml b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application-fagsak_e2e.yaml new file mode 100644 index 000000000..0a83f0f41 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application-fagsak_e2e.yaml @@ -0,0 +1,56 @@ +no.nav.security.jwt: + issuer.azuread: + discoveryurl: http://mock-oauth2-server:1111/v2.0/.well-known/openid-configuration + accepted_audience: api://${TILBAKE_CLIENT_ID}/.default + proxyurl: + cookie_name: azure_token + client: + registration: + familie-integrasjoner: + resource-url: ${FAMILIE_INTEGRASJONER_URL} + token-endpoint-url: http://mock-oauth2-server:1111/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${TILBAKE_CLIENT_ID} + client-secret: ${TILBAKE_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-integrasjoner-clientcredentials: + resource-url: ${FAMILIE_INTEGRASJONER_URL} + token-endpoint-url: http://mock-oauth2-server:1111/v2.0/token + grant-type: client_credentials + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${TILBAKE_CLIENT_ID} + client-secret: ${TILBAKE_CLIENT_SECRET} + client-auth-method: client_secret_basic + +logging: + config: "classpath:logback-e2e.xml" + +spring: + datasource: + url: jdbc:postgresql://postgres-tilbake:5432/familie-tilbake + username: familie + password: familie-pwd + cloud: + vault: + database: + role: familie + +unleash: + enabled: true + +STS_URL: http://nav-auth-mock:8200/nais-sts/token +PDL_URL: http://familie-mock-server:1337/rest/api/pdl/ +SECURITYTOKENSERVICE_URL: https://localhost:8063/soap/SecurityTokenServiceProvider/ + +FAMILIE_INTEGRASJONER_URL: http://familie-integrasjoner:8085/api + +CREDENTIAL_USERNAME: srvfamilie-tilbake +CREDENTIAL_PASSWORD: + +NAIS_APP_NAME: familie-klage +UNLEASH_SERVER_API_URL: http://localhost:4242/api +UNLEASH_SERVER_API_TOKEN: token +NAIS_CLUSTER_NAME: dev-gcp diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application-prod.yaml b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application-prod.yaml new file mode 100644 index 000000000..745d0d3d4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application-prod.yaml @@ -0,0 +1,45 @@ +spring: + datasource: + url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/familie-tilbake + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + flyway: + placeholders: + ignoreIfProd: -- + +oppdrag.mq: + queuemanager: MPLS02 + kravgrunnlag: QA.P_FAMILIE_TILBAKE.KRAVGRUNNLAG + channel: P_FAMILIE_TILBAKE + hostname: mpls02.adeo.no + port: 1414 + enabled: false + +rolle: + barnetrygd: + veileder: "199c2b39-e535-4ae8-ac59-8ccbee7991ae" + saksbehandler: "847e3d72-9dc1-41c3-80ff-f5d4acdd5d46" + beslutter: "7a271f87-39fb-468b-a9ee-6cf3c070f548" + enslig: + veileder: "31778fd8-3b71-4867-8db6-a81235fbe001" + saksbehandler: "6406aba2-b930-41d3-a85b-dd13731bc974" + beslutter: "5fcc0e1d-a4c2-49f0-93dc-27c9fea41e54" + kontantstøtte: + veileder: "54cd86b8-2e23-48b2-8852-b05b5827bb0f" + saksbehandler: "e40090eb-c2fb-400e-b412-e9084019a73b" + beslutter: "4e7f23d9-5db1-45c0-acec-89c86a9ec678" + teamfamilie: + forvalter: "3d718ae5-f25e-47a4-b4b3-084a97604c1d" + prosessering: "87190cf3-b278-457d-8ab7-1a5c55a9edd7" # Gruppen teamfamilie + + +SECURITYTOKENSERVICE_URL: https://security-token-service.nais.adeo.no/SecurityTokenServiceProvider/ + +PDL_URL: https://pdl-api.prod-fss-pub.nais.io +PDL_SCOPE: api://prod-fss.pdl.pdl-api/.default + +FAMILIE_INTEGRASJONER_URL: https://familie-integrasjoner.prod-fss-pub.nais.io +FAMILIE_OPPDRAG_URL: https://familie-oppdrag.prod-fss-pub.nais.io +CRON_HÅNDTER_GAMMEL_KRAVGRUNNLAG: 0 0 7 ? * MON-FRI +AUTHORIZATION_URL: https://login.microsoftonline.com/navno.onmicrosoft.com/oauth2/v2.0/authorize +TOKEN_URL: https://login.microsoftonline.com/navno.onmicrosoft.com/oauth2/v2.0/token diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application.yaml b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application.yaml new file mode 100644 index 000000000..75af4059f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/application.yaml @@ -0,0 +1,175 @@ +application: + name: familie-tilbake + +server: + port: 8030 + servlet: + context-path: / + +spring: + autoconfigure.exclude: org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration + main.banner-mode: "off" + data: + jdbc: + repositories: + enabled: true + main: + allow-bean-definition-overriding: true + flyway: + enabled: true + placeholders: + ignoreIfProd: + datasource: + hikari: + maximum-pool-size: 4 + connection-test-query: "select 1" + max-lifetime: 30000 + minimum-idle: 1 + data-source-properties.stringtype: unspecified # Nødvendig for å kunde sende en String til et json-felt i PostgresSql + aop: + auto: true + proxy-target-class: true + +springdoc: + packages-to-scan: "no.nav.familie.tilbake" + paths-to-match: "/api/**" + swagger-ui: + oauth: + use-pkce-with-authorization-code-grant: true + client-id: ${AZURE_APP_CLIENT_ID} + scope-separator: "," + disable-swagger-default-url: true + +no.nav.security.jwt: + issuer.azuread: + discoveryurl: ${AZURE_APP_WELL_KNOWN_URL} + accepted_audience: ${AZURE_APP_CLIENT_ID} + cookie_name: azure_token + client: + registration: + familie-integrasjoner: + resource-url: ${FAMILIE_INTEGRASJONER_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-integrasjoner-clientcredentials: + resource-url: ${FAMILIE_INTEGRASJONER_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_INTEGRASJONER_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl: + resource-url: ${PDL_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${PDL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + pdl-clientcredentials: + resource-url: ${PDL_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${PDL_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag: + resource-url: ${FAMILIE_OPPDRAG_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + familie-oppdrag-clientcredentials: + resource-url: ${FAMILIE_OPPDRAG_URL} + token-endpoint-url: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} + grant-type: client_credentials + scope: ${FAMILIE_OPPDRAG_SCOPE} + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + +management: + endpoint.health.show-details: always + endpoints.web: + exposure.include: info, health, metrics, prometheus + base-path: "/internal" + path-mapping: + info: "status/isAlive" + metrics.export.prometheus.enabled: true + health: + db: + enabled: true + +unleash: + enabled: true + +prosessering: + continuousRunning.enabled: true + maxantall: 1 + fixedDelayString: + in: + milliseconds: 4000 + delete: + after: + weeks: 4 + +oppdrag.mq: + queuemanager: QM1 + kravgrunnlag: DEV.QUEUE.1 + channel: DEV.ADMIN.SVRCONN + hostname: localhost + port: 1414 + user: admin + password: passw0rd + enabled: true + +rolle: + barnetrygd: + veileder: "" + saksbehandler: "" + beslutter: "" + enslig: + veileder: "" + saksbehandler: "" + beslutter: "" + kontantstøtte: + veileder: "" + saksbehandler: "" + beslutter: "" + teamfamilie: + forvalter: "" + prosessering: "" + +SECURITYTOKENSERVICE_URL: https://sts-q1.preprod.local/SecurityTokenServiceProvider/ # brukes kun av integrasjonstester +STS_URL: https://security-token-service.dev.adeo.no/security-token-service/rest/v1/sts/token?grant_type=client_credentials&scope=openid # brukes kun av integrasjonstester +PDL_URL: https://pdl-api-q1.dev-fss-pub.nais.io +FAMILIE_TILBAKE_FRONTEND_CLIENT_ID: "dummy" +FORELDELSE_ANTALL_MÅNED: 30 +PROXY_URL: http://webproxy-nais.nav.no:8088 +CRON_HÅNDTER_GAMMEL_KRAVGRUNNLAG: 0 0 7 ? * MON-FRI +CRON_AUTOMATISK_SAKSBEHANDLING: 0 0 8 ? * MON-FRI +CRON_AUTOMATISK_GJENOPPTA: 0 0 6 ? * MON-FRI +AUTHORIZATION_URL: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/authorize +TOKEN_URL: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token +API_SCOPE: api://${AZURE_APP_CLIENT_ID}/.default + +AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_BARNETRYGD: 8 +AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_BARNETILSYN: 8 +AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_OVERGANGSSTØNAD: 8 +AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_SKOLEPENGER: 8 +AUTOMATISK_SAKSBEHANDLING_ALDERGRENSE_KONTANTSTØTTE: 8 + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/colorprofile/sRGBz.icc b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/colorprofile/sRGBz.icc new file mode 100644 index 000000000..8d0ee2a10 Binary files /dev/null and b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/colorprofile/sRGBz.icc differ diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V10__altered_okonomi_mottatt_xml_krav_grunnlag_431.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V10__altered_okonomi_mottatt_xml_krav_grunnlag_431.sql new file mode 100644 index 000000000..3186ec16b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V10__altered_okonomi_mottatt_xml_krav_grunnlag_431.sql @@ -0,0 +1,26 @@ +DROP TABLE gruppering_kravvedtaksstatus; +DROP TABLE kravvedtaksstatus437; + +ALTER TABLE okonomi_xml_mottatt + ADD COLUMN sperret BOOLEAN DEFAULT FALSE NOT NULL; + +COMMENT ON COLUMN okonomi_xml_mottatt.sperret + IS 'Angir om grunnlaget har fått sper melding fra økonomi'; + +ALTER TABLE kravgrunnlag431 + ADD COLUMN avsluttet BOOLEAN DEFAULT FALSE NOT NULL; + +COMMENT ON COLUMN kravgrunnlag431.avsluttet + IS 'Angir om grunnlaget har fått avsl melding fra økonomi'; + +ALTER TABLE okonomi_xml_mottatt_arkiv + ADD COLUMN ekstern_fagsak_id VARCHAR NOT NULL; + +COMMENT ON COLUMN okonomi_xml_mottatt_arkiv.ekstern_fagsak_id + IS 'Saksnummer(som økonomi har sendt)'; + +ALTER TABLE okonomi_xml_mottatt_arkiv + ADD COLUMN ytelsestype VARCHAR NOT NULL; + +COMMENT ON COLUMN okonomi_xml_mottatt_arkiv.ytelsestype + IS 'Angir tilhørende ytelsestype'; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V11__altered_foreldelse_tabeller.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V11__altered_foreldelse_tabeller.sql new file mode 100644 index 000000000..480c28b55 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V11__altered_foreldelse_tabeller.sql @@ -0,0 +1,10 @@ +ALTER TABLE totrinnsresultatsgrunnlag + DROP COLUMN gruppering_vurdert_foreldelse_id; + +ALTER TABLE totrinnsresultatsgrunnlag + ADD COLUMN vurdert_foreldelse_id UUID REFERENCES vurdert_foreldelse; + +COMMENT ON COLUMN totrinnsresultatsgrunnlag.vurdert_foreldelse_id + IS 'Fk til aktivt vurdertforeldelse ved totrinnsbehandlingen'; + +DROP TABLE gruppering_vurdert_foreldelse; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V12__altered_vilkarsvurdering_tabeller.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V12__altered_vilkarsvurdering_tabeller.sql new file mode 100644 index 000000000..0a0eb2f4d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V12__altered_vilkarsvurdering_tabeller.sql @@ -0,0 +1 @@ +ALTER TABLE vilkarsvurderingsperiode DROP COLUMN navoppfulgt; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V13__altered_totrinnsresultatgrunnlag.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V13__altered_totrinnsresultatgrunnlag.sql new file mode 100644 index 000000000..a80b851e1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V13__altered_totrinnsresultatgrunnlag.sql @@ -0,0 +1,3 @@ +DROP INDEX totrinnsresultatsgrunnlag_gruppering_fakta_feilutbetaling_i_idx; + +CREATE INDEX totrinnsresultatsgrunnlag_fakta_feilutbetaling_i_idx ON totrinnsresultatsgrunnlag (fakta_feilutbetaling_id); diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V14__altered_vedtaksbrevoppsummering.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V14__altered_vedtaksbrevoppsummering.sql new file mode 100644 index 000000000..bc745d482 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V14__altered_vedtaksbrevoppsummering.sql @@ -0,0 +1,2 @@ +ALTER TABLE vedtaksbrevsoppsummering + DROP COLUMN fritekst; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V15__altered_fagsak.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V15__altered_fagsak.sql new file mode 100644 index 000000000..1a2cb6f42 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V15__altered_fagsak.sql @@ -0,0 +1,2 @@ +ALTER TABLE fagsak + DROP COLUMN status; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V16__totrinnskontroll.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V16__totrinnskontroll.sql new file mode 100644 index 000000000..05eaaf197 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V16__totrinnskontroll.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS totrinnsresultatsgrunnlag; +DROP TABLE IF EXISTS revurderingsarsak; +DROP TABLE IF EXISTS arsak_totrinnsvurdering; + +ALTER TABLE totrinnsvurdering + DROP COLUMN aksjonspunktsdefinisjon; + +ALTER TABLE totrinnsvurdering + ADD COLUMN behandlingssteg VARCHAR NOT NULL; + +COMMENT ON COLUMN totrinnsvurdering.behandlingssteg + IS 'Behandlingssteg som kan besluttes'; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V17__altered_okonomi_xml_sendt_behandlingsvedtak.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V17__altered_okonomi_xml_sendt_behandlingsvedtak.sql new file mode 100644 index 000000000..9dc74301a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V17__altered_okonomi_xml_sendt_behandlingsvedtak.sql @@ -0,0 +1,5 @@ +ALTER TABLE okonomi_xml_sendt + DROP COLUMN IF EXISTS meldingstype; + +ALTER TABLE behandlingsvedtak + DROP COLUMN IF EXISTS ansvarlig_saksbehandler; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V18__fjern_ubrukt_tabeller.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V18__fjern_ubrukt_tabeller.sql new file mode 100644 index 000000000..f5b66ed09 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V18__fjern_ubrukt_tabeller.sql @@ -0,0 +1,4 @@ +DROP TABLE IF EXISTS aksjonspunkt; + +DROP TABLE IF EXISTS mottakers_varselrespons; + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V19__Avstemmingsfil.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V19__Avstemmingsfil.sql new file mode 100644 index 000000000..a5df58c0d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V19__Avstemmingsfil.sql @@ -0,0 +1,10 @@ +CREATE TABLE avstemmingsfil ( + id UUID PRIMARY KEY NOT NULL, + navn VARCHAR NOT NULL, + innhold BYTEA NOT NULL, + versjon BIGINT NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL', + opprettet_tid TIMESTAMP(3) DEFAULT LOCALTIMESTAMP, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V1__base.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V1__base.sql new file mode 100644 index 000000000..61dd3ba22 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V1__base.sql @@ -0,0 +1,1478 @@ +CREATE TABLE fagsak ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + fagsystem VARCHAR NOT NULL, + ekstern_fagsak_id VARCHAR, + status VARCHAR NOT NULL, + bruker_ident VARCHAR, + bruker_sprakkode VARCHAR DEFAULT 'NB' NOT NULL, + ytelsestype VARCHAR DEFAULT 'BA' NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE fagsak + IS 'Fagsak for tilbakekreving. Alle behandling er koblet mot en fagsak.'; + +COMMENT ON COLUMN fagsak.id + IS 'Primary key'; + +COMMENT ON COLUMN fagsak.fagsystem + IS 'Fagsystemet som er eier av tilbakekrevingsbehandling'; + +COMMENT ON COLUMN fagsak.ekstern_fagsak_id + IS 'Saksnummer (som gsak har mottatt)'; + +COMMENT ON COLUMN fagsak.status + IS 'Fk:status fagsak'; + +COMMENT ON COLUMN fagsak.bruker_ident + IS 'Fk:Ident på bruker'; + +COMMENT ON COLUMN fagsak.ytelsestype + IS 'Fremmednøkkel til kodeverkstabellen som inneholder oversikt over ytelser'; + +CREATE UNIQUE INDEX ON fagsak (ekstern_fagsak_id); + +CREATE INDEX ON fagsak (status); + +CREATE INDEX ON fagsak (bruker_ident); + +CREATE INDEX ON fagsak (ytelsestype); + +CREATE TABLE behandling ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + fagsak_id UUID NOT NULL REFERENCES fagsak, + status VARCHAR NOT NULL, + type VARCHAR NOT NULL, + opprettet_dato DATE DEFAULT current_date NOT NULL, + avsluttet_dato DATE, + ansvarlig_saksbehandler VARCHAR, + ansvarlig_beslutter VARCHAR, + behandlende_enhet VARCHAR, + behandlende_enhets_navn VARCHAR, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + manuelt_opprettet BOOLEAN NOT NULL, + ekstern_bruk_id UUID NOT NULL, + saksbehandlingstype VARCHAR NOT NULL + CONSTRAINT chk_saksbehandlingstype + CHECK (saksbehandlingstype IN ('ORDINÆR', 'AUTOMATISK_IKKE_INNKREVING_LAVT_BELØP')) +); + +COMMENT ON TABLE behandling + IS 'Behandling av fagsak'; + +COMMENT ON COLUMN behandling.id + IS 'Primary key'; + +COMMENT ON COLUMN behandling.fagsak_id + IS 'Fk: fagsak fremmednøkkel for kobling til fagsak'; + +COMMENT ON COLUMN behandling.status + IS 'Fk: behandlingsstatus fremmednøkkel til tabellen som viser status på behandlinger'; + +COMMENT ON COLUMN behandling.type + IS 'Fk: type behandling '; + +COMMENT ON COLUMN behandling.opprettet_dato + IS 'Dato når behandlingen ble opprettet.'; + +COMMENT ON COLUMN behandling.avsluttet_dato + IS 'Dato når behandlingen ble avsluttet.'; + +COMMENT ON COLUMN behandling.ansvarlig_saksbehandler + IS 'Id til saksbehandler som oppretter forslag til vedtak ved totrinnsbehandling.'; + +COMMENT ON COLUMN behandling.ansvarlig_beslutter + IS 'Beslutter som har fattet vedtaket'; + +COMMENT ON COLUMN behandling.behandlende_enhet + IS 'Nav-enhet som behandler behandlingen'; + +COMMENT ON COLUMN behandling.behandlende_enhets_navn + IS 'Navn på behandlende enhet'; + +COMMENT ON COLUMN behandling.manuelt_opprettet + IS 'Angir om behandlingen ble opprettet manuelt. '; + +COMMENT ON COLUMN behandling.ekstern_bruk_id + IS 'Unik uuid for behandling til utvortes bruk'; + +COMMENT ON COLUMN behandling.saksbehandlingstype + IS 'Angir hvordan behandlingen saksbehandles '; + +CREATE INDEX ON behandling (fagsak_id); + +CREATE INDEX ON behandling (status); + +CREATE INDEX ON behandling (type); + +CREATE UNIQUE INDEX ON behandling (ekstern_bruk_id); + +CREATE TABLE ekstern_behandling ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + aktiv BOOLEAN DEFAULT TRUE NOT NULL, + ekstern_id VARCHAR NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + UNIQUE (behandling_id, ekstern_id) +); + +COMMENT ON TABLE ekstern_behandling + IS 'Referanse til ekstern behandling'; + +COMMENT ON COLUMN ekstern_behandling.id + IS 'Primary key'; + +COMMENT ON COLUMN ekstern_behandling.behandling_id + IS 'Fk: behandling fremmednøkkel for kobling til intern behandling'; + +COMMENT ON COLUMN ekstern_behandling.aktiv + IS 'Angir om ekstern behandling data er gjeldende'; + +COMMENT ON COLUMN ekstern_behandling.ekstern_id + IS 'ekstern_id;referanse. Peker på referanse-feltet i kravgrunnlaget, og kommer opprinnelig fra fagsystemet.'; + +CREATE INDEX ON ekstern_behandling (behandling_id); + +CREATE INDEX ON ekstern_behandling (ekstern_id); + +CREATE TABLE varsel ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + aktiv BOOLEAN NOT NULL, + varseltekst VARCHAR NOT NULL, + varselbelop BIGINT, + revurderingsvedtaksdato DATE NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE varsel + IS 'Tabell for å lagre varsel info'; + +COMMENT ON COLUMN varsel.id + IS 'Primary key'; + +COMMENT ON COLUMN varsel.behandling_id + IS 'Fk: behandling fremmednøkkel for tilknyttet behandling'; + +COMMENT ON COLUMN varsel.aktiv + IS 'Angir status av varsel'; + +COMMENT ON COLUMN varsel.varseltekst + IS 'Fritekst som brukes i varselbrev'; + +COMMENT ON COLUMN varsel.varselbelop + IS 'Beløp som brukes i varselbrev'; + +COMMENT ON COLUMN varsel.revurderingsvedtaksdato + IS 'vedtaksdato av fagsystemsrevurdering. Brukes av varselbrev'; + +CREATE INDEX ON varsel (behandling_id); + +CREATE TABLE varselsperiode ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + varsel_id UUID NOT NULL REFERENCES varsel, + fom DATE NOT NULL, + tom DATE NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE varselsperiode + IS 'Feilutbetalingsperiode som brukes i varselbrev'; + +COMMENT ON COLUMN varselsperiode.id + IS 'Primary key'; + +COMMENT ON COLUMN varselsperiode.varsel_id + IS 'FK: varsel fremmednøkel for kobling til varsel'; + +COMMENT ON COLUMN varselsperiode.fom + IS 'Første dag av feilutbetalingsperiode'; + +COMMENT ON COLUMN varselsperiode.fom + IS 'Siste dag av feilutbetalingsperiode'; + +COMMENT ON COLUMN varselsperiode.versjon + IS 'Bruker for optimistisk låsing'; + +CREATE INDEX ON varselsperiode (varsel_id); + +CREATE TABLE verge ( + id UUID PRIMARY KEY, + behandling_id UUID NOT NULL REFERENCES behandling, + versjon BIGINT NOT NULL, + ident VARCHAR, + gyldig_fom DATE NOT NULL, + gyldig_tom DATE NOT NULL, + aktiv BOOLEAN NOT NULL, + type VARCHAR NOT NULL, + org_nr VARCHAR, + navn VARCHAR NOT NULL, + kilde VARCHAR NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + begrunnelse VARCHAR +); + +COMMENT ON TABLE verge + IS 'Informasjon om verge'; + +COMMENT ON COLUMN verge.id + IS 'Primary key'; + +COMMENT ON COLUMN verge.ident + IS 'Aktørid av verge person'; + +COMMENT ON COLUMN verge.gyldig_fom + IS 'Hvis fullmakt er begrenset i periode, dato for når fullmakten er gyldig fra'; + +COMMENT ON COLUMN verge.gyldig_tom + IS 'Hvis fullmakt er begrenset i periode, dato for når fullmakten er gyldig til'; + +COMMENT ON COLUMN verge.type + IS 'Type verge'; + +COMMENT ON COLUMN verge.org_nr + IS 'Vergens organisasjonsnummer'; + +COMMENT ON COLUMN verge.navn + IS 'Navn på vergen, som tastet inn av saksbehandler'; + +COMMENT ON COLUMN verge.kilde + IS 'Opprinnelsen av verge.enten fpsak hvis det kopierte fra fpsak eller fptilbake'; + +COMMENT ON COLUMN verge.begrunnelse + IS 'Begrunnelse for verge'; + +CREATE TABLE behandlingsresultat ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + type VARCHAR DEFAULT 'IKKE_FASTSATT' NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE behandlingsresultat + IS 'Beregningsresultat. Knytter sammen beregning og behandling.'; + +COMMENT ON COLUMN behandlingsresultat.id + IS 'Primary key'; + +COMMENT ON COLUMN behandlingsresultat.versjon + IS 'Bruker for optimistisk låsing'; + +COMMENT ON COLUMN behandlingsresultat.behandling_id + IS 'Fk: behandling fremmednøkkel for kobling til behandling'; + +COMMENT ON COLUMN behandlingsresultat.type + IS 'Resultat av behandlingen'; + +CREATE INDEX ON behandlingsresultat (behandling_id); + +CREATE INDEX ON behandlingsresultat (type); + +CREATE TABLE behandlingsarsak ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + type VARCHAR NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + original_behandling_id UUID REFERENCES behandling +); + +COMMENT ON TABLE behandlingsarsak + IS 'Årsak for rebehandling'; + +COMMENT ON COLUMN behandlingsarsak.id + IS 'Primary key'; + +COMMENT ON COLUMN behandlingsarsak.behandling_id + IS 'Fk: behandling fremmednøkkel for kobling til behandling'; + +COMMENT ON COLUMN behandlingsarsak.type + IS 'Fk: behandlingsårsakstype fremmednøkkel til oversikten over hvilke årsaker en behandling kan begrunnes med'; + +COMMENT ON COLUMN behandlingsarsak.original_behandling_id + IS 'Fk: behandling fremmednøkkel for kobling til behandlingen denne raden i tabellen hører til'; + +CREATE INDEX ON behandlingsarsak (behandling_id); + +CREATE INDEX ON behandlingsarsak (original_behandling_id); + +CREATE TABLE aksjonspunkt ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + totrinnsbehandling BOOLEAN NOT NULL, + behandlingsstegstype VARCHAR, + aksjonspunktsdefinisjon VARCHAR NOT NULL, + status VARCHAR NOT NULL, + tidsfrist TIMESTAMP(3), + ventearsak VARCHAR DEFAULT '-' NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + reaktiveringsstatus VARCHAR DEFAULT 'AKTIV' NOT NULL, + manuelt_opprettet BOOLEAN DEFAULT FALSE NOT NULL, + revurdering BOOLEAN DEFAULT FALSE NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE aksjonspunkt + IS 'Aksjoner som en saksbehandler må utføre manuelt.'; + +COMMENT ON COLUMN aksjonspunkt.id + IS 'Primary key'; + +COMMENT ON COLUMN aksjonspunkt.totrinnsbehandling + IS 'Indikerer at aksjonspunkter krever en totrinnsbehandling'; + +COMMENT ON COLUMN aksjonspunkt.behandlingsstegstype + IS 'Hvilket steg ble dette aksjonspunktet funnet i?'; + +COMMENT ON COLUMN aksjonspunkt.status + IS 'Fk:Status på aksjonspunktet'; + +COMMENT ON COLUMN aksjonspunkt.aksjonspunktsdefinisjon + IS 'aksjonspunktsdefinisjon enum'; + +COMMENT ON COLUMN aksjonspunkt.tidsfrist + IS 'Behandling blir automatisk gjenopptatt etter dette tidspunktet'; + +COMMENT ON COLUMN aksjonspunkt.ventearsak + IS 'Årsak for at behandling er satt på vent'; + +COMMENT ON COLUMN aksjonspunkt.behandling_id + IS 'Fremmednøkkel for kobling til behandling'; + +COMMENT ON COLUMN aksjonspunkt.status + IS 'Angir om aksjonspunktet er aktivt. Inaktive aksjonspunkter er historiske som ble kopiert når en revurdering ble opprettet. De eksisterer for å kunne vise den opprinnelige begrunnelsen, uten at saksbehandler må ta stilling til det på nytt.'; + +COMMENT ON COLUMN aksjonspunkt.manuelt_opprettet + IS 'Angir om aksjonspunktet ble opprettet manuelt. Typisk skjer dette ved overstyring, og når saksbehandler manuelt reaktiverer et historisk aksjonspunkt i en revurdering. Brukes når behandlingskontroll skal rydde ved hopp.'; + +COMMENT ON COLUMN aksjonspunkt.revurdering + IS 'Flagget settes på aksjonspunkter som kopieres i det en revurdering opprettes. Trengs for å kunne vurdere om aksjonspunktet er kandidat for totrinnskontroll dersom det har blitt en endring i aksjonspunktet under revurderingen.'; + +CREATE UNIQUE INDEX ON aksjonspunkt (behandling_id, aksjonspunktsdefinisjon); + +CREATE INDEX ON aksjonspunkt (behandlingsstegstype); + +CREATE INDEX ON aksjonspunkt (aksjonspunktsdefinisjon); + +CREATE INDEX ON aksjonspunkt (ventearsak); + +CREATE INDEX ON aksjonspunkt (status); + +CREATE INDEX ON aksjonspunkt (reaktiveringsstatus); + +ALTER TABLE aksjonspunkt + ADD CONSTRAINT chk_unique_beh_ad + UNIQUE (behandling_id, aksjonspunktsdefinisjon); + +CREATE TABLE revurderingsarsak ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + aksjonspunkt_id UUID NOT NULL REFERENCES aksjonspunkt, + arsakstype VARCHAR NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE revurderingsarsak + IS 'Årsaken til at aksjonspunkt må vurderes på nytt'; + +COMMENT ON COLUMN revurderingsarsak.id + IS 'Primary key'; + +COMMENT ON COLUMN revurderingsarsak.aksjonspunkt_id + IS 'Fk:Aksjonspunkt fremmednøkkel til aksjonspunktet som må vurderes på nytt'; + +COMMENT ON COLUMN revurderingsarsak.arsakstype + IS 'Årsak for at aksjonspunkt må vurderes på nytt'; + +CREATE INDEX ON revurderingsarsak (aksjonspunkt_id); + +CREATE INDEX ON revurderingsarsak (arsakstype); + +CREATE TABLE behandlingsstegstilstand ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + behandlingsstegstype VARCHAR NOT NULL, + behandlingsstegsstatus VARCHAR NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE behandlingsstegstilstand + IS 'Angir tilstand for behandlingsteg som kjøres'; + +COMMENT ON COLUMN behandlingsstegstilstand.id + IS 'Primary key'; + +COMMENT ON COLUMN behandlingsstegstilstand.behandling_id + IS 'Fk: behandling fremmednøkkel for kobling til behandlingen dette steget er tilknyttet'; + +COMMENT ON COLUMN behandlingsstegstilstand.behandlingsstegstype + IS 'Hvilket behandlingsteg som kjøres'; + +COMMENT ON COLUMN behandlingsstegstilstand.behandlingsstegsstatus + IS 'Status på steg: (ved) inngang, startet, venter, (ved) utgang, utført'; + +CREATE INDEX ON behandlingsstegstilstand (behandling_id); + +CREATE INDEX ON behandlingsstegstilstand (behandlingsstegsstatus); + +CREATE INDEX ON behandlingsstegstilstand (behandlingsstegstype); + +CREATE TABLE behandlingsvedtak ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + vedtaksdato DATE NOT NULL, + ansvarlig_saksbehandler VARCHAR NOT NULL, + behandlingsresultat_id UUID NOT NULL REFERENCES behandlingsresultat, + iverksettingsstatus VARCHAR DEFAULT 'IKKE_IVERKSATT' NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE behandlingsvedtak + IS 'Vedtak koblet til en behandling via et behandlingsresultat.'; + +COMMENT ON COLUMN behandlingsvedtak.id + IS 'Primary key'; + +COMMENT ON COLUMN behandlingsvedtak.vedtaksdato + IS 'Vedtaksdato.'; + +COMMENT ON COLUMN behandlingsvedtak.ansvarlig_saksbehandler + IS 'Ansvarlig saksbehandler som godkjente vedtaket.'; + +COMMENT ON COLUMN behandlingsvedtak.behandlingsresultat_id + IS 'Fk:Behandling_resultat fremmednøkkel til tabellen som viser behandlingsresultatet'; + +COMMENT ON COLUMN behandlingsvedtak.iverksettingsstatus + IS 'Status for iverksettingssteget'; + +CREATE UNIQUE INDEX ON behandlingsvedtak (behandlingsresultat_id); + +CREATE INDEX ON behandlingsvedtak (ansvarlig_saksbehandler); + +CREATE INDEX ON behandlingsvedtak (vedtaksdato); + +CREATE INDEX ON behandlingsvedtak (iverksettingsstatus); + +CREATE TABLE totrinnsvurdering ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + aksjonspunktsdefinisjon VARCHAR NOT NULL, + aktiv BOOLEAN DEFAULT TRUE NOT NULL, + godkjent BOOLEAN NOT NULL, + begrunnelse VARCHAR, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE totrinnsvurdering + IS 'Statisk read only totrinnsvurdering som brukes til å vise vurderinger til aksjonspunkter uavhengig av status'; + +COMMENT ON COLUMN totrinnsvurdering.godkjent + IS 'Beslutters godkjenning'; + +COMMENT ON COLUMN totrinnsvurdering.begrunnelse + IS 'Beslutters begrunnelse'; + +CREATE INDEX ON totrinnsvurdering (aksjonspunktsdefinisjon); + +CREATE INDEX ON totrinnsvurdering (behandling_id); + +CREATE TABLE arsak_totrinnsvurdering ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + arsakstype VARCHAR NOT NULL, + totrinnsvurdering_id UUID NOT NULL REFERENCES totrinnsvurdering, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE arsak_totrinnsvurdering + IS 'Årsaken til at aksjonspunkt må vurderes på nytt'; + +COMMENT ON COLUMN arsak_totrinnsvurdering.arsakstype + IS 'Årsak til at løsning på aksjonspunkt er underkjent'; + +CREATE INDEX ON arsak_totrinnsvurdering (totrinnsvurdering_id); + +CREATE INDEX ON arsak_totrinnsvurdering (arsakstype); + +CREATE TABLE mottakers_varselrespons ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + akseptert_faktagrunnlag BOOLEAN, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + kilde VARCHAR NOT NULL +); + +COMMENT ON TABLE mottakers_varselrespons + IS 'Respons fra mottakere av tbk. Varsel'; + +COMMENT ON COLUMN mottakers_varselrespons.id + IS 'Primary key'; + +COMMENT ON COLUMN mottakers_varselrespons.behandling_id + IS 'Behandlingen responsen hører til'; + +COMMENT ON COLUMN mottakers_varselrespons.akseptert_faktagrunnlag + IS 'Angir om faktagrunnlag har blitt akseptert av bruker'; + +COMMENT ON COLUMN mottakers_varselrespons.kilde + IS 'Angir hvor responsen ble registrert'; + +CREATE UNIQUE INDEX ON mottakers_varselrespons (behandling_id); + +CREATE TABLE vurdert_foreldelse ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL, + aktiv BOOLEAN DEFAULT TRUE NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE vurdert_foreldelse + IS 'Aggregate tabell for å lagre vurdert foreldelse'; + +COMMENT ON COLUMN vurdert_foreldelse.id + IS 'Primary key'; + +COMMENT ON COLUMN vurdert_foreldelse.behandling_id + IS 'Fk: behandling fremmednøkkel for tilknyttet behandling'; + +COMMENT ON COLUMN vurdert_foreldelse.aktiv + IS 'Angir status av vurdert foreldelse'; + +CREATE TABLE gruppering_vurdert_foreldelse ( + id UUID NOT NULL PRIMARY KEY, + versjon BIGINT NOT NULL, + vurdert_foreldelse_id UUID NOT NULL + REFERENCES vurdert_foreldelse, + behandling_id UUID NOT NULL + REFERENCES behandling, + aktiv BOOLEAN DEFAULT TRUE NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +) +; + +COMMENT ON TABLE gruppering_vurdert_foreldelse IS 'Aggregate tabell for å lagre vurdert foreldelse' +; + +COMMENT ON COLUMN gruppering_vurdert_foreldelse.id IS 'Primary Key' +; + +COMMENT ON COLUMN gruppering_vurdert_foreldelse.vurdert_foreldelse_id IS 'FK:VURDERT_FORELDELSE' +; + +COMMENT ON COLUMN gruppering_vurdert_foreldelse.behandling_id IS 'FK: BEHANDLING fremmednøkkel for tilknyttet behandling' +; + +COMMENT ON COLUMN gruppering_vurdert_foreldelse.aktiv IS 'Angir status av vurdert foreldelse' +; + +CREATE INDEX idx_gr_vurdert_foreldelse_1 + ON gruppering_vurdert_foreldelse (vurdert_foreldelse_id) +; + + +CREATE TABLE foreldelsesperiode ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + vurdert_foreldelse_id UUID NOT NULL REFERENCES vurdert_foreldelse, + fom DATE NOT NULL, + tom DATE NOT NULL, + foreldelsesvurderingstype VARCHAR NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + begrunnelse VARCHAR NOT NULL, + foreldelsesfrist DATE, + oppdagelsesdato DATE +); + +COMMENT ON TABLE foreldelsesperiode + IS 'Tabell for å lagre ny utbetaling periode opprettet av saksbehandler'; + +COMMENT ON COLUMN foreldelsesperiode.id + IS 'Primary key'; + +COMMENT ON COLUMN foreldelsesperiode.vurdert_foreldelse_id + IS 'Fk:Vurdert_foreldelse'; + +COMMENT ON COLUMN foreldelsesperiode.fom + IS 'Første dag av ny utbetaling periode'; + +COMMENT ON COLUMN foreldelsesperiode.tom + IS 'Siste dag av ny utbetaling periode'; + +COMMENT ON COLUMN foreldelsesperiode.foreldelsesvurderingstype + IS 'Foreldelse vurdering type av en periode'; + +COMMENT ON COLUMN foreldelsesperiode.begrunnelse + IS 'Begrunnelse for endre periode'; + +COMMENT ON COLUMN foreldelsesperiode.foreldelsesfrist + IS 'Foreldelsesfrist for når feilutbetalingen kan innkreves'; + +COMMENT ON COLUMN foreldelsesperiode.oppdagelsesdato + IS 'Dato for når feilutbetalingen ble oppdaget'; + +CREATE INDEX ON foreldelsesperiode (vurdert_foreldelse_id); + +CREATE INDEX ON foreldelsesperiode (foreldelsesvurderingstype); + +CREATE TABLE kravgrunnlag431 ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + vedtak_id VARCHAR NOT NULL, + kravstatuskode VARCHAR NOT NULL, + fagomradekode VARCHAR NOT NULL, + fagsystem VARCHAR NOT NULL, + fagsystem_vedtaksdato DATE, + omgjort_vedtak_id VARCHAR, + gjelder_vedtak_id VARCHAR NOT NULL, + gjelder_type VARCHAR NOT NULL, + utbetales_til_id VARCHAR NOT NULL, + hjemmelkode VARCHAR, + beregnes_renter BOOLEAN, + ansvarlig_enhet VARCHAR NOT NULL, + bostedsenhet VARCHAR NOT NULL, + behandlingsenhet VARCHAR NOT NULL, + kontrollfelt VARCHAR NOT NULL, + saksbehandler_id VARCHAR NOT NULL, + referanse VARCHAR, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + ekstern_kravgrunnlag_id VARCHAR +); + +COMMENT ON TABLE kravgrunnlag431 + IS 'Tabell for tilbakekrevingsvedtak fra økonomi'; + +COMMENT ON COLUMN kravgrunnlag431.vedtak_id + IS 'Identifikasjon av tilbakekrevingsvedtaket opprettet av tilbakekrevingskomponenten'; + +COMMENT ON COLUMN kravgrunnlag431.kravstatuskode + IS 'Status på kravgrunnlaget'; + +COMMENT ON COLUMN kravgrunnlag431.fagomradekode + IS 'Fagområdet på feilutbetalingen'; + +COMMENT ON COLUMN kravgrunnlag431.fagsystem + IS 'Fagsystemets identifikasjon av vedtaket som har feilutbetaling'; + +COMMENT ON COLUMN kravgrunnlag431.fagsystem_vedtaksdato + IS 'Fagsystemets vedtaksdato for vedtaket'; + +COMMENT ON COLUMN kravgrunnlag431.omgjort_vedtak_id + IS 'Henvisning til forrige gyldige vedtak'; + +COMMENT ON COLUMN kravgrunnlag431.gjelder_vedtak_id + IS 'Vanligvis stønadsmottaker (fnr;org.nr.) i feilutbetalingen'; + +COMMENT ON COLUMN kravgrunnlag431.gjelder_type + IS 'Angir om vedtak-gjelder-id er fnr, org.nr., tss-nr etc'; + +COMMENT ON COLUMN kravgrunnlag431.utbetales_til_id + IS 'Mottaker av pengene i feilutbetalingen'; + +COMMENT ON COLUMN kravgrunnlag431.hjemmelkode + IS 'Lovhjemmel for tilbakekrevingsvedtaket'; + +COMMENT ON COLUMN kravgrunnlag431.beregnes_renter + IS 'J dersom det skal beregnes renter på kravet'; + +COMMENT ON COLUMN kravgrunnlag431.ansvarlig_enhet + IS 'Enhet ansvarlig'; + +COMMENT ON COLUMN kravgrunnlag431.bostedsenhet + IS 'Bostedsenhet, hentet fra feilutbetalingen'; + +COMMENT ON COLUMN kravgrunnlag431.behandlingsenhet + IS 'Behandlende enhet, hentet fra feilutbetalingen'; + +COMMENT ON COLUMN kravgrunnlag431.kontrollfelt + IS 'Brukes ved innsending av tilbakekrevingsvedtak for å kontrollere at kravgrunnlaget ikke er blitt endret i mellomtiden'; + +COMMENT ON COLUMN kravgrunnlag431.saksbehandler_id + IS 'Saksbehandler'; + +COMMENT ON COLUMN kravgrunnlag431.referanse + IS 'Henvisning fra nyeste oppdragslinje'; + +COMMENT ON COLUMN kravgrunnlag431.ekstern_kravgrunnlag_id + IS 'Referanse til kravgrunnlag fra ostbk. Brukes ved omgjøring for å hente nytt grunnlag.'; + +CREATE INDEX ON kravgrunnlag431 (kravstatuskode); + +CREATE INDEX ON kravgrunnlag431 (fagomradekode); + +CREATE INDEX ON kravgrunnlag431 (gjelder_type); + +CREATE INDEX ON kravgrunnlag431 (utbetales_til_id); + +CREATE INDEX ON kravgrunnlag431 (vedtak_id); + +CREATE TABLE kravgrunnlagsperiode432 ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + kravgrunnlag431_id UUID NOT NULL REFERENCES kravgrunnlag431, + fom DATE NOT NULL, + tom DATE NOT NULL, + manedlig_skattebelop NUMERIC(12, 2) NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE kravgrunnlagsperiode432 + IS 'Perioder av tilbakekrevingsvedtak fra økonomi'; + +COMMENT ON COLUMN kravgrunnlagsperiode432.fom + IS 'Første dag i periode'; + +COMMENT ON COLUMN kravgrunnlagsperiode432.tom + IS 'Siste dag i periode'; + +COMMENT ON COLUMN kravgrunnlagsperiode432.kravgrunnlag431_id + IS 'Fk:Krav_grunnlag431'; + +COMMENT ON COLUMN kravgrunnlagsperiode432.manedlig_skattebelop + IS 'Angir totalt skattebeløp per måned'; + +CREATE INDEX ON kravgrunnlagsperiode432 (kravgrunnlag431_id); + +CREATE TABLE kravgrunnlagsbelop433 ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + klassekode VARCHAR NOT NULL, + klassetype VARCHAR NOT NULL, + opprinnelig_utbetalingsbelop NUMERIC(12, 2), + nytt_belop NUMERIC(12, 2) NOT NULL, + tilbakekreves_belop NUMERIC(12, 2), + uinnkrevd_belop NUMERIC(12, 2), + resultatkode VARCHAR, + arsakskode VARCHAR, + skyldkode VARCHAR, + kravgrunnlagsperiode432_id UUID NOT NULL REFERENCES kravgrunnlagsperiode432, + skatteprosent NUMERIC(7, 4) NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE kravgrunnlagsbelop433 + IS 'Tabell for tilbakekrevingsbeløp fra økonomi'; + +COMMENT ON COLUMN kravgrunnlagsbelop433.klassekode + IS 'Klassifisering av stønad, skatt, trekk etc.'; + +COMMENT ON COLUMN kravgrunnlagsbelop433.klassetype + IS 'Angir type av klassekoden'; + +COMMENT ON COLUMN kravgrunnlagsbelop433.opprinnelig_utbetalingsbelop + IS 'Opprinnelig beregnet beløp, dvs utbetalingen som førte til feilutbetaling'; + +COMMENT ON COLUMN kravgrunnlagsbelop433.nytt_belop + IS 'Beløpet som ble beregnet ved korrigeringen'; + +COMMENT ON COLUMN kravgrunnlagsbelop433.tilbakekreves_belop + IS 'Beløpet som skal tilbakekreves'; + +COMMENT ON COLUMN kravgrunnlagsbelop433.uinnkrevd_belop + IS 'Beløp som ikke skal tilbakekreves'; + +COMMENT ON COLUMN kravgrunnlagsbelop433.resultatkode + IS 'Hvilket vedtak som er fattet ang tilbakekreving'; + +COMMENT ON COLUMN kravgrunnlagsbelop433.arsakskode + IS 'Årsak til feilutbetalingen'; + +COMMENT ON COLUMN kravgrunnlagsbelop433.skyldkode + IS 'Hvem som har skyld i at det ble feilutbetalt'; + +COMMENT ON COLUMN kravgrunnlagsbelop433.kravgrunnlagsperiode432_id + IS 'Fk:Krav_grunnlag_periode432'; + +COMMENT ON COLUMN kravgrunnlagsbelop433.skatteprosent + IS 'Angir gjeldende skatt prosent som skal trekke fra brutto tilbakekrevingsbeløp for netto tilbakekreving'; + +CREATE INDEX ON kravgrunnlagsbelop433 (klassekode); + +CREATE INDEX ON kravgrunnlagsbelop433 (klassetype); + +CREATE INDEX ON kravgrunnlagsbelop433 (kravgrunnlagsperiode432_id); + +CREATE TABLE kravvedtaksstatus437 ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + vedtak_id VARCHAR NOT NULL, + kravstatuskode VARCHAR NOT NULL, + fagomradekode VARCHAR NOT NULL, + fagsystem_id VARCHAR NOT NULL, + gjelder_vedtak_id VARCHAR NOT NULL, + gjelder_type VARCHAR NOT NULL, + referanse VARCHAR, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE kravvedtaksstatus437 + IS 'Tabell for krav og vedtak status endringer fra økonomi'; + +COMMENT ON COLUMN kravvedtaksstatus437.vedtak_id + IS 'Identifikasjon av tilbakekrevingsvedtaket opprettet av tilbakekrevingskomponenten'; + +COMMENT ON COLUMN kravvedtaksstatus437.kravstatuskode + IS 'Status på kravgrunnlaget'; + +COMMENT ON COLUMN kravvedtaksstatus437.fagomradekode + IS 'Fagområdet på feilutbetalingen'; + +COMMENT ON COLUMN kravvedtaksstatus437.fagsystem_id + IS 'Fagsystemets identifikasjon av vedtaket som har feilutbetaling'; + +COMMENT ON COLUMN kravvedtaksstatus437.gjelder_vedtak_id + IS 'Vanligvis stønadsmottaker (fnr;org.nr.) i feilutbetalingen'; + +COMMENT ON COLUMN kravvedtaksstatus437.gjelder_type + IS 'Angir om vedtak-gjelder-id er fnr, org.nr., tss-nr etc'; + +COMMENT ON COLUMN kravvedtaksstatus437.referanse + IS 'Henvisning fra nyeste oppdragslinje'; + +CREATE INDEX ON kravvedtaksstatus437 (kravstatuskode); + +CREATE INDEX ON kravvedtaksstatus437 (fagomradekode); + +CREATE INDEX ON kravvedtaksstatus437 (gjelder_type); + +CREATE TABLE gruppering_krav_grunnlag ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + kravgrunnlag431_id UUID NOT NULL REFERENCES kravgrunnlag431, + behandling_id UUID NOT NULL REFERENCES behandling, + aktiv BOOLEAN DEFAULT TRUE NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + sperret BOOLEAN +); + +COMMENT ON TABLE gruppering_krav_grunnlag + IS 'Aggregate tabell for å lagre grunnlag'; + +COMMENT ON COLUMN gruppering_krav_grunnlag.id + IS 'Primary key'; + +COMMENT ON COLUMN gruppering_krav_grunnlag.kravgrunnlag431_id + IS 'Fk:Krav_grunnlag431.angir grunnlag kommer fra økonomi'; + +COMMENT ON COLUMN gruppering_krav_grunnlag.behandling_id + IS 'Fk: behandling fremmednøkkel for tilknyttet behandling'; + +COMMENT ON COLUMN gruppering_krav_grunnlag.aktiv + IS 'Angir status av grunnlag'; + +COMMENT ON COLUMN gruppering_krav_grunnlag.sperret + IS 'Angir om grunnlaget har fått sper melding fra økonomi'; + +CREATE INDEX ON gruppering_krav_grunnlag (kravgrunnlag431_id); + +CREATE INDEX ON gruppering_krav_grunnlag (behandling_id); + +CREATE TABLE vilkarsvurdering ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL, + aktiv BOOLEAN NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE vilkarsvurdering + IS 'Kobler flere perioder av vilkårsvurdering for tilbakekreving'; + +COMMENT ON COLUMN vilkarsvurdering.behandling_id + IS 'Referanse til behandling'; + +COMMENT ON COLUMN vilkarsvurdering.aktiv + IS 'Angir status av manuell vilkårsvurdering'; + +CREATE INDEX ON vilkarsvurdering (behandling_id); + +CREATE TABLE vilkarsvurderingsperiode ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + vilkarsvurdering_id UUID NOT NULL REFERENCES vilkarsvurdering, + fom DATE NOT NULL, + tom DATE NOT NULL, + navoppfulgt VARCHAR NOT NULL, + vilkarsvurderingsresultat VARCHAR NOT NULL, + begrunnelse VARCHAR NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE vilkarsvurderingsperiode + IS 'Periode med vilkårsvurdering for tilbakekreving'; + +COMMENT ON COLUMN vilkarsvurderingsperiode.vilkarsvurdering_id + IS 'Fk:vilkår'; + +COMMENT ON COLUMN vilkarsvurderingsperiode.fom + IS 'Fra-og-med-dato'; + +COMMENT ON COLUMN vilkarsvurderingsperiode.tom + IS 'Til-og-med-dato'; + +COMMENT ON COLUMN vilkarsvurderingsperiode.navoppfulgt + IS 'Vurdering av hvordan nav har fulgt opp'; + +COMMENT ON COLUMN vilkarsvurderingsperiode.vilkarsvurderingsresultat + IS 'Hovedresultat av vilkårsvurdering (kodeverk)'; + +COMMENT ON COLUMN vilkarsvurderingsperiode.begrunnelse + IS 'Saksbehandlers begrunnelse'; + +CREATE INDEX ON vilkarsvurderingsperiode (vilkarsvurdering_id); + +CREATE INDEX ON vilkarsvurderingsperiode (navoppfulgt); + +CREATE INDEX ON vilkarsvurderingsperiode (vilkarsvurderingsresultat); + +CREATE TABLE vilkarsvurdering_aktsomhet ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + vilkarsvurderingsperiode_id UUID NOT NULL REFERENCES vilkarsvurderingsperiode, + aktsomhet VARCHAR NOT NULL, + ilegg_renter BOOLEAN, + andel_tilbakekreves NUMERIC(5, 2), + manuelt_satt_belop BIGINT, + begrunnelse VARCHAR NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + serlige_grunner_til_reduksjon BOOLEAN, + tilbakekrev_smabelop BOOLEAN, + serlige_grunner_begrunnelse VARCHAR, + CHECK ("andel_tilbakekreves" IS NULL OR manuelt_satt_belop IS NULL) +); + +COMMENT ON TABLE vilkarsvurdering_aktsomhet + IS 'Videre vurderinger når det er vurdert at bruker ikke mottok beløp i god tro'; + +COMMENT ON COLUMN vilkarsvurdering_aktsomhet.vilkarsvurderingsperiode_id + IS 'Fk:vilkårsperiode'; + +COMMENT ON COLUMN vilkarsvurdering_aktsomhet.aktsomhet + IS 'Resultat av aktsomhetsvurdering (kodeverk)'; + +COMMENT ON COLUMN vilkarsvurdering_aktsomhet.ilegg_renter + IS 'Hvorvidt renter skal ilegges'; + +COMMENT ON COLUMN vilkarsvurdering_aktsomhet.andel_tilbakekreves + IS 'Hvor stor del av feilutbetalt beløp som skal tilbakekreves'; + +COMMENT ON COLUMN vilkarsvurdering_aktsomhet.manuelt_satt_belop + IS 'Feilutbetalt beløp som skal tilbakekreves som bestemt ved saksbehandler'; + +COMMENT ON COLUMN vilkarsvurdering_aktsomhet.begrunnelse + IS 'Beskrivelse av aktsomhet'; + +COMMENT ON COLUMN vilkarsvurdering_aktsomhet.serlige_grunner_til_reduksjon + IS 'Angir om særlig grunner gi reduksjon av beløpet'; + +COMMENT ON COLUMN vilkarsvurdering_aktsomhet.tilbakekrev_smabelop + IS 'Angir om skal tilbakekreves når totalbeløpet er under 4 rettsgebyr'; + +COMMENT ON COLUMN vilkarsvurdering_aktsomhet.serlige_grunner_begrunnelse + IS 'Beskrivelse av særlig grunner'; + +CREATE INDEX ON vilkarsvurdering_aktsomhet (vilkarsvurderingsperiode_id); + +CREATE INDEX ON vilkarsvurdering_aktsomhet (aktsomhet); + +CREATE TABLE vilkarsvurdering_serlig_grunn ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + vilkarsvurdering_aktsomhet_id UUID NOT NULL REFERENCES vilkarsvurdering_aktsomhet, + serlig_grunn VARCHAR NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + begrunnelse VARCHAR +); + +COMMENT ON TABLE vilkarsvurdering_serlig_grunn + IS 'Særlige grunner ved vurdering'; + +COMMENT ON COLUMN vilkarsvurdering_serlig_grunn.vilkarsvurdering_aktsomhet_id + IS 'Fk:vilkarsvurdering_aktsomhet'; + +COMMENT ON COLUMN vilkarsvurdering_serlig_grunn.serlig_grunn + IS 'Særlig grunn (kodeverk)'; + +COMMENT ON COLUMN vilkarsvurdering_serlig_grunn.begrunnelse + IS 'Beskrivelse av særlig grunn hvis grunn er annet'; + +CREATE INDEX ON vilkarsvurdering_serlig_grunn (vilkarsvurdering_aktsomhet_id); + +CREATE INDEX ON vilkarsvurdering_serlig_grunn (serlig_grunn); + +CREATE TABLE vilkarsvurdering_god_tro ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + vilkarsvurderingsperiode_id UUID NOT NULL REFERENCES vilkarsvurderingsperiode, + belop_er_i_behold BOOLEAN NOT NULL, + belop_tilbakekreves BIGINT, + begrunnelse VARCHAR NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE vilkarsvurdering_god_tro + IS 'Videre vurderinger når det er vurdert at bruker mottok feilutbetaling i god tro'; + +COMMENT ON COLUMN vilkarsvurdering_god_tro.vilkarsvurderingsperiode_id + IS 'Fk:vilkarsvurderingsperiode'; + +COMMENT ON COLUMN vilkarsvurdering_god_tro.belop_er_i_behold + IS 'Indikerer at beløp er i behold'; + +COMMENT ON COLUMN vilkarsvurdering_god_tro.belop_tilbakekreves + IS 'Hvor mye av feilutbetalt beløp som skal tilbakekreves'; + +COMMENT ON COLUMN vilkarsvurdering_god_tro.begrunnelse + IS 'Beskrivelse av god tro vilkår'; + +CREATE INDEX ON vilkarsvurdering_god_tro (vilkarsvurderingsperiode_id); + +CREATE TABLE fakta_feilutbetaling ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + begrunnelse VARCHAR +); + +COMMENT ON TABLE fakta_feilutbetaling + IS 'Kobler flere perioder av fakta om feilutbetaling for tilbakekreving'; + +COMMENT ON COLUMN fakta_feilutbetaling.begrunnelse + IS 'Begrunnelse for endringer gjort i fakta om feilutbetaling'; + +CREATE TABLE fakta_feilutbetalingsperiode ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + fom DATE NOT NULL, + tom DATE NOT NULL, + hendelsestype VARCHAR NOT NULL, + hendelsesundertype VARCHAR NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + fakta_feilutbetaling_id UUID NOT NULL REFERENCES fakta_feilutbetaling +); + +COMMENT ON TABLE fakta_feilutbetalingsperiode + IS 'Tabell for å lagre feilutbetaling årsak og underårsak for hver perioder'; + +COMMENT ON COLUMN fakta_feilutbetalingsperiode.id + IS 'Primary key'; + +COMMENT ON COLUMN fakta_feilutbetalingsperiode.fom + IS 'Første dag av feilutbetaling periode'; + +COMMENT ON COLUMN fakta_feilutbetalingsperiode.tom + IS 'Siste dag av feilutbetaling periode'; + +COMMENT ON COLUMN fakta_feilutbetalingsperiode.hendelsestype + IS 'Hendelse som er årsak til feilutbetalingen'; + +COMMENT ON COLUMN fakta_feilutbetalingsperiode.hendelsesundertype + IS 'Hendelse som er årsak til feilutbetalingen (underårsak)'; + +COMMENT ON COLUMN fakta_feilutbetalingsperiode.fakta_feilutbetaling_id + IS 'Fk:Feilutbetaling'; + +CREATE INDEX ON fakta_feilutbetalingsperiode (fakta_feilutbetaling_id); + +CREATE INDEX ON fakta_feilutbetalingsperiode (hendelsestype); + +CREATE INDEX ON fakta_feilutbetalingsperiode (hendelsesundertype); + +CREATE TABLE gruppering_fakta_feilutbetaling ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + fakta_feilutbetaling_id UUID NOT NULL REFERENCES fakta_feilutbetaling, + aktiv BOOLEAN NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE gruppering_fakta_feilutbetaling + IS 'Versjonering av fakta om feilutbetaling for tilbakekreving'; + +COMMENT ON COLUMN gruppering_fakta_feilutbetaling.behandling_id + IS 'Referanse til behandling'; + +COMMENT ON COLUMN gruppering_fakta_feilutbetaling.fakta_feilutbetaling_id + IS 'Fk:Feilutbetaling'; + +COMMENT ON COLUMN gruppering_fakta_feilutbetaling.aktiv + IS 'Angir status av fakta om feilutbetaling'; + +CREATE INDEX ON gruppering_fakta_feilutbetaling (behandling_id); + +CREATE INDEX ON gruppering_fakta_feilutbetaling (fakta_feilutbetaling_id); + +CREATE TABLE okonomi_xml_mottatt ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + melding TEXT NOT NULL, + sekvens INTEGER, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + tilkoblet BOOLEAN, + ekstern_fagsak_id VARCHAR, + henvisning VARCHAR +); + +COMMENT ON TABLE okonomi_xml_mottatt + IS 'Lagrer mottatt kravgrunnlag-xml i påvente av at den skal prosesseres. Brukes for at mottak skal være mer robust'; + +COMMENT ON COLUMN okonomi_xml_mottatt.id + IS 'Primærnøkkel'; + +COMMENT ON COLUMN okonomi_xml_mottatt.melding + IS 'Kravgrunnlag-xml'; + +COMMENT ON COLUMN okonomi_xml_mottatt.sekvens + IS 'Teller innenfor en behandling'; + +COMMENT ON COLUMN okonomi_xml_mottatt.tilkoblet + IS 'Angir om mottatt xml er tilkoblet med en behandling'; + +COMMENT ON COLUMN okonomi_xml_mottatt.ekstern_fagsak_id + IS 'Saksnummer(som økonomi har sendt)'; + +COMMENT ON COLUMN okonomi_xml_mottatt.henvisning + IS 'Henvisning;referanse. Peker på referanse-feltet i kravgrunnlaget, og kommer opprinnelig fra fagsystemet. For fptilbake er den lik fpsak.behandlingid. For k9-tilbake er den lik base64(bytes(behandlinguuid))'; + +CREATE INDEX ON okonomi_xml_mottatt (henvisning); + +CREATE INDEX ON okonomi_xml_mottatt (ekstern_fagsak_id); + +CREATE INDEX ON okonomi_xml_mottatt (opprettet_tid); + +CREATE INDEX ON okonomi_xml_mottatt (tilkoblet); + +CREATE TABLE totrinnsresultatsgrunnlag ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + gruppering_fakta_feilutbetaling_id UUID NOT NULL REFERENCES gruppering_fakta_feilutbetaling, + gruppering_vurdert_foreldelse_id UUID REFERENCES gruppering_vurdert_foreldelse, + vilkarsvurdering_id UUID REFERENCES vilkarsvurdering, + aktiv BOOLEAN NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE totrinnsresultatsgrunnlag + IS 'Tabell som held grunnlagsid for data vist i panelet fra beslutter.'; + +COMMENT ON COLUMN totrinnsresultatsgrunnlag.id + IS 'Pk'; + +COMMENT ON COLUMN totrinnsresultatsgrunnlag.behandling_id + IS 'Fk til behandling som hører til totrinnsresultatet'; + +COMMENT ON COLUMN totrinnsresultatsgrunnlag.gruppering_fakta_feilutbetaling_id + IS 'Fk til aktivt feilutbetalingaggregate ved totrinnsbehandlingen'; + +COMMENT ON COLUMN totrinnsresultatsgrunnlag.gruppering_vurdert_foreldelse_id + IS 'Fk til aktivt vurdertforeldelseaggregate ved totrinnsbehandlingen'; + +COMMENT ON COLUMN totrinnsresultatsgrunnlag.vilkarsvurdering_id + IS 'Fk til aktivt vilkårvurderingaggregate ved totrinnsbehandlingen'; + +CREATE INDEX ON totrinnsresultatsgrunnlag (behandling_id); + +CREATE INDEX ON totrinnsresultatsgrunnlag (gruppering_fakta_feilutbetaling_id); + +CREATE INDEX ON totrinnsresultatsgrunnlag (gruppering_vurdert_foreldelse_id); + +CREATE INDEX ON totrinnsresultatsgrunnlag (vilkarsvurdering_id); + +CREATE TABLE vedtaksbrevsoppsummering ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + oppsummering_fritekst VARCHAR, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + fritekst TEXT +); + +COMMENT ON TABLE vedtaksbrevsoppsummering + IS 'Inneholder friteksten til vedtaksoppsummeringen som er skrevet inn av saksbehandler.'; + +COMMENT ON COLUMN vedtaksbrevsoppsummering.id + IS 'Primary key'; + +COMMENT ON COLUMN vedtaksbrevsoppsummering.behandling_id + IS 'Fk: behandling fremmednøkkel for kobling til behandling i fptilbake'; + +COMMENT ON COLUMN vedtaksbrevsoppsummering.oppsummering_fritekst + IS 'Fritekst fra saksbehandler til oppsummering av vedtaket'; + +COMMENT ON COLUMN vedtaksbrevsoppsummering.fritekst + IS 'Fritekst fra saksbehandler til oppsummering av vedtaket'; + +CREATE INDEX ON vedtaksbrevsoppsummering (behandling_id); + +CREATE TABLE vedtaksbrevsperiode ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + fom DATE NOT NULL, + tom DATE NOT NULL, + fritekst VARCHAR NOT NULL, + fritekststype VARCHAR NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE vedtaksbrevsperiode + IS 'Inneholder en periode i et vedtaksbrev, samt fritekst'; + +COMMENT ON COLUMN vedtaksbrevsperiode.id + IS 'Primary key'; + +COMMENT ON COLUMN vedtaksbrevsperiode.behandling_id + IS 'Fk: behandling fremmednøkkel for kobling til behandling i fptilbake'; + +COMMENT ON COLUMN vedtaksbrevsperiode.fom + IS 'Fom-dato for perioden'; + +COMMENT ON COLUMN vedtaksbrevsperiode.tom + IS 'Tom-dato for perioden'; + +COMMENT ON COLUMN vedtaksbrevsperiode.fritekst + IS 'Fritekst skrevet til et av avsnittene i vedtaksbrevet'; + +COMMENT ON COLUMN vedtaksbrevsperiode.fritekststype + IS 'Hvilket avsnitt friteksten gjelder'; + +CREATE INDEX ON vedtaksbrevsperiode (behandling_id); + +CREATE TABLE okonomi_xml_sendt ( + id UUID PRIMARY KEY NOT NULL, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + melding TEXT NOT NULL, + kvittering TEXT, + opprettet_av VARCHAR DEFAULT 'VL', + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp, + endret_av VARCHAR, + endret_tid TIMESTAMP(3), + meldingstype VARCHAR NOT NULL +); + +COMMENT ON TABLE okonomi_xml_sendt + IS 'Tabell som tar vare på xml sendt til os, brukes for feilsøking'; + +COMMENT ON COLUMN okonomi_xml_sendt.id + IS 'Primary key'; + +COMMENT ON COLUMN okonomi_xml_sendt.behandling_id + IS 'Behandlingen det gjelder'; + +COMMENT ON COLUMN okonomi_xml_sendt.melding + IS 'Xml sendt til os'; + +COMMENT ON COLUMN okonomi_xml_sendt.kvittering + IS 'Respons fra os'; + +COMMENT ON COLUMN okonomi_xml_sendt.meldingstype + IS 'Meldingstype'; + +CREATE UNIQUE INDEX ON okonomi_xml_sendt (id); + +CREATE INDEX ON okonomi_xml_sendt (behandling_id); + +CREATE INDEX ON okonomi_xml_sendt (meldingstype); + + +CREATE TABLE gruppering_kravvedtaksstatus ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + kravvedtaksstatus437_id UUID NOT NULL REFERENCES kravvedtaksstatus437, + behandling_id UUID NOT NULL REFERENCES behandling, + aktiv BOOLEAN NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE gruppering_kravvedtaksstatus + IS 'Aggregate tabell for å lagre krav- og vedtakstatus'; + +COMMENT ON COLUMN gruppering_kravvedtaksstatus.id + IS 'Primary key'; + +COMMENT ON COLUMN gruppering_kravvedtaksstatus.kravvedtaksstatus437_id + IS 'Fk:Krav_vedtak_status437.angir krav- og vedtakstatus kommer fra økonomi'; + +COMMENT ON COLUMN gruppering_kravvedtaksstatus.behandling_id + IS 'Fk: behandling fremmednøkkel for tilknyttet behandling'; + +COMMENT ON COLUMN gruppering_kravvedtaksstatus.aktiv + IS 'Angir status av grunnlag'; + +CREATE INDEX ON gruppering_kravvedtaksstatus (kravvedtaksstatus437_id); + +CREATE INDEX ON gruppering_kravvedtaksstatus (behandling_id); + +CREATE TABLE brevsporing ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + behandling_id UUID NOT NULL REFERENCES behandling, + journalpost_id VARCHAR NOT NULL, + dokument_id VARCHAR NOT NULL, + brevtype VARCHAR NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE brevsporing + IS 'Brevsporing inneholder informasjon om forskjellige brev som er bestilt.'; + +COMMENT ON COLUMN brevsporing.id + IS 'Primary key'; + +COMMENT ON COLUMN brevsporing.behandling_id + IS 'Fk: behandling fremmednøkkel for kobling til behandling i fptilbake'; + +COMMENT ON COLUMN brevsporing.journalpost_id + IS 'Journalpostid i doksys'; + +COMMENT ON COLUMN brevsporing.dokument_id + IS 'Dokumentid i doksys'; + +COMMENT ON COLUMN brevsporing.brevtype + IS 'Bestilt brevtype'; + +CREATE INDEX ON brevsporing (behandling_id); + +CREATE INDEX ON brevsporing (brevtype); + +CREATE TABLE okonomi_xml_mottatt_arkiv ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + melding TEXT NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE okonomi_xml_mottatt_arkiv + IS 'Tabell for å arkivere gamle kravgrunnlag som ikke finnes i økonomi.'; + +COMMENT ON COLUMN okonomi_xml_mottatt_arkiv.id + IS 'Primary key'; + +COMMENT ON COLUMN okonomi_xml_mottatt_arkiv.melding + IS 'Gammel kravgrunnlag xml'; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V20__hent_fagsystemsbehandling_request_sendt.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V20__hent_fagsystemsbehandling_request_sendt.sql new file mode 100644 index 000000000..be61fe283 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V20__hent_fagsystemsbehandling_request_sendt.sql @@ -0,0 +1,32 @@ +CREATE TABLE hent_fagsystemsbehandling_request_sendt ( + id UUID PRIMARY KEY, + ekstern_fagsak_id VARCHAR NOT NULL, + ytelsestype VARCHAR NOT NULL, + ekstern_id VARCHAR NOT NULL, + respons TEXT, + versjon BIGINT NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE hent_fagsystemsbehandling_request_sendt + IS 'Tabell for å lagre hentFagsystemsbehandling data som sendes til fagsystem via kafka'; + +COMMENT ON COLUMN hent_fagsystemsbehandling_request_sendt.id + IS 'Primary key'; + +COMMENT ON COLUMN hent_fagsystemsbehandling_request_sendt.ekstern_fagsak_id + IS 'Saksnummer (som gsak har mottatt)'; + +COMMENT ON COLUMN hent_fagsystemsbehandling_request_sendt.ytelsestype + IS 'Ytelsestypen til fagsystemsbehandling'; + +COMMENT ON COLUMN hent_fagsystemsbehandling_request_sendt.ekstern_id + IS 'Referansen til fagsystemsbehandling'; + +COMMENT ON COLUMN hent_fagsystemsbehandling_request_sendt.respons + IS 'Respons-en mottas fra fagsystem på Kafka'; + +CREATE UNIQUE INDEX ON hent_fagsystemsbehandling_request_sendt (ekstern_fagsak_id, ytelsestype, ekstern_id); \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V21__drop_fom_og_tom_p\303\245 verge.sql" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V21__drop_fom_og_tom_p\303\245 verge.sql" new file mode 100644 index 000000000..6b45acc74 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V21__drop_fom_og_tom_p\303\245 verge.sql" @@ -0,0 +1,3 @@ +ALTER TABLE verge + DROP gyldig_fom, + DROP gyldig_tom; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V22__gcp_tilgang.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V22__gcp_tilgang.sql new file mode 100644 index 000000000..5abeaa369 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V22__gcp_tilgang.sql @@ -0,0 +1,10 @@ +DO $$ + BEGIN + IF EXISTS + ( SELECT 1 from pg_roles where rolname='cloudsqliamuser') + THEN + GRANT SELECT ON ALL TABLES IN SCHEMA public TO cloudsqliamuser; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO cloudsqliamuser; + END IF ; + END +$$ ; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V23__gcp_skrive_tilgang_i_prepod.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V23__gcp_skrive_tilgang_i_prepod.sql new file mode 100644 index 000000000..4139cb128 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V23__gcp_skrive_tilgang_i_prepod.sql @@ -0,0 +1,10 @@ +DO $$ + BEGIN + IF EXISTS + ( SELECT 1 from pg_roles where rolname='cloudsqliamuser') + THEN + ${ignoreIfProd} ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO cloudsqliamuser; + ${ignoreIfProd} GRANT ALL ON ALL TABLES IN SCHEMA public TO cloudsqliamuser; + END IF ; + END +$$ ; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V24__avstemmingsjobb.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V24__avstemmingsjobb.sql new file mode 100644 index 000000000..2422fa281 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V24__avstemmingsjobb.sql @@ -0,0 +1,12 @@ +DO +$$ + BEGIN + IF NOT EXISTS + (SELECT 1 FROM task WHERE type = 'task.avstemming') + THEN + INSERT INTO task(payload, type, status, metadata, versjon, opprettet_tid, trigger_tid) + VALUES ('2020-09-20', 'task.avstemming', 'UBEHANDLET', 'callId=e5ac23de-21b7-497a-9be3-dbe1d8110088 +', 0, now(), now()); + END IF; + END +$$; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V25__update_task_versjon.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V25__update_task_versjon.sql new file mode 100644 index 000000000..5a69e0bd5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V25__update_task_versjon.sql @@ -0,0 +1 @@ +update task set versjon = 1, payload = '2021-10-01' where versjon = 0 and type = 'task.avstemming'; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V26__opprett_kravgrunnlagstelling.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V26__opprett_kravgrunnlagstelling.sql new file mode 100644 index 000000000..5c0bc31d2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V26__opprett_kravgrunnlagstelling.sql @@ -0,0 +1,10 @@ +CREATE TABLE meldingstelling ( + id UUID PRIMARY KEY, + ytelsestype VARCHAR NOT NULL, + type VARCHAR NOT NULL, + status VARCHAR NOT NULL, + dato DATE NOT NULL, + antall INT NOT NULL +); + +CREATE UNIQUE INDEX ON meldingstelling (type, dato, ytelsestype, status); diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V27__endre_kravgrunnlagstelling.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V27__endre_kravgrunnlagstelling.sql new file mode 100644 index 000000000..4227538a7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V27__endre_kravgrunnlagstelling.sql @@ -0,0 +1,6 @@ +ALTER TABLE meldingstelling + RENAME COLUMN ytelsestype TO fagsystem; + +UPDATE meldingstelling +SET fagsystem = 'EF' +WHERE fagsystem LIKE 'EF%'; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V28__oppdater_meldingstelling.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V28__oppdater_meldingstelling.sql new file mode 100644 index 000000000..282a1a444 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V28__oppdater_meldingstelling.sql @@ -0,0 +1,7 @@ +UPDATE meldingstelling +SET fagsystem = 'EF' +WHERE fagsystem IN ('OVERGANGSSTØNAD', 'BARNETILSYN', 'SKOLEPENGER'); + +UPDATE meldingstelling +SET fagsystem = 'BA' +WHERE fagsystem = 'BARNETRYGD'; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V29__institusjon_i_fagsak.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V29__institusjon_i_fagsak.sql new file mode 100644 index 000000000..4da091e9b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V29__institusjon_i_fagsak.sql @@ -0,0 +1,12 @@ + +ALTER TABLE fagsak + ADD COLUMN institusjon_organisasjonsnummer VARCHAR; + +COMMENT ON COLUMN fagsak.institusjon_organisasjonsnummer + IS 'Organisasjonsnummer for institusjon'; + +ALTER TABLE fagsak + ADD COLUMN institusjon_navn VARCHAR; + +COMMENT ON COLUMN fagsak.institusjon_navn + IS 'Navn på intitusjon'; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V2__altered_fagsak.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V2__altered_fagsak.sql new file mode 100644 index 000000000..f9d41331b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V2__altered_fagsak.sql @@ -0,0 +1,10 @@ +DROP INDEX fagsak_ekstern_fagsak_id_idx; +DROP INDEX fagsak_ytelsestype_idx; + +CREATE UNIQUE INDEX ON fagsak (ekstern_fagsak_id, ytelsestype); + +ALTER TABLE fagsak + ALTER COLUMN ekstern_fagsak_id SET NOT NULL; + +ALTER TABLE fagsak + ALTER COLUMN bruker_ident SET NOT NULL; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V30__fjerner_navn_institusjon.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V30__fjerner_navn_institusjon.sql new file mode 100644 index 000000000..88dbc4edd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V30__fjerner_navn_institusjon.sql @@ -0,0 +1,2 @@ + +ALTER TABLE fagsak DROP COLUMN institusjon_navn; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V31__regelverk.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V31__regelverk.sql new file mode 100644 index 000000000..a47071547 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V31__regelverk.sql @@ -0,0 +1,2 @@ +ALTER TABLE behandling + ADD COLUMN regelverk VARCHAR; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V32__manuellbrevmottaker.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V32__manuellbrevmottaker.sql new file mode 100644 index 000000000..f5f138f18 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V32__manuellbrevmottaker.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS manuell_brevmottaker +( + id UUID PRIMARY KEY, + behandling_id UUID NOT NULL REFERENCES behandling, + type VARCHAR(50) NOT NULL, + vergetype VARCHAR(50), + navn VARCHAR NOT NULL, + adresselinje_1 VARCHAR NOT NULL, + adresselinje_2 VARCHAR, + postnummer VARCHAR NOT NULL, + poststed VARCHAR NOT NULL, + landkode VARCHAR(2) NOT NULL, + versjon BIGINT DEFAULT 0 NOT NULL, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT localtimestamp NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +CREATE INDEX IF NOT EXISTS manuell_brevmottaker_behandling_id_idx ON manuell_brevmottaker (behandling_id); diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V33__manuellbrevmottaker_flere_felter.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V33__manuellbrevmottaker_flere_felter.sql new file mode 100644 index 000000000..8a383c885 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V33__manuellbrevmottaker_flere_felter.sql @@ -0,0 +1,7 @@ +ALTER TABLE manuell_brevmottaker ADD COLUMN ident VARCHAR; +ALTER TABLE manuell_brevmottaker ADD COLUMN org_nr VARCHAR; +ALTER TABLE manuell_brevmottaker ALTER COLUMN adresselinje_1 DROP NOT NULL; +ALTER TABLE manuell_brevmottaker ALTER COLUMN postnummer DROP NOT NULL; +ALTER TABLE manuell_brevmottaker ALTER COLUMN poststed DROP NOT NULL; +ALTER TABLE manuell_brevmottaker ALTER COLUMN landkode DROP NOT NULL; + diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V34__constraint_p\303\245_kravgrunnlag431_aktiv.sql" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V34__constraint_p\303\245_kravgrunnlag431_aktiv.sql" new file mode 100644 index 000000000..4eddb913a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V34__constraint_p\303\245_kravgrunnlag431_aktiv.sql" @@ -0,0 +1,2 @@ +CREATE UNIQUE INDEX idx_kravgrunnlag431_aktiv ON kravgrunnlag431 (behandling_id) WHERE aktiv; + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V3__altered_eksternbehandling.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V3__altered_eksternbehandling.sql new file mode 100644 index 000000000..34bc47355 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V3__altered_eksternbehandling.sql @@ -0,0 +1,37 @@ +ALTER TABLE ekstern_behandling + RENAME TO fagsystemsbehandling; + +ALTER TABLE fagsystemsbehandling + ADD COLUMN tilbakekrevingsvalg VARCHAR; + +ALTER TABLE fagsystemsbehandling + ADD COLUMN resultat VARCHAR; + +ALTER TABLE fagsystemsbehandling + ADD COLUMN arsak VARCHAR; + +CREATE TABLE fagsystemskonsekvens ( + id UUID PRIMARY KEY, + versjon BIGINT NOT NULL, + konsekvens VARCHAR NOT NULL, + fagsystemsbehandling_id UUID NOT NULL REFERENCES fagsystemsbehandling, + opprettet_av VARCHAR DEFAULT 'VL' NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT LOCALTIMESTAMP NOT NULL, + endret_av VARCHAR, + endret_tid TIMESTAMP(3) +); + +COMMENT ON TABLE fagsystemskonsekvens + IS 'Fagsystemskonsekvens for ytelser, vises i Fakta'; + +COMMENT ON COLUMN fagsystemskonsekvens.id + IS 'Primary key'; + +COMMENT ON COLUMN fagsystemskonsekvens.konsekvens + IS 'Konsekvens for ytelser til fagsystemsrevurdering'; + +COMMENT ON COLUMN fagsystemskonsekvens.fagsystemsbehandling_id + IS 'FK:fagsystemsbehandling fremmednøkkel'; + +CREATE INDEX ON fagsystemskonsekvens (fagsystemsbehandling_id); + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V4__task.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V4__task.sql new file mode 100644 index 000000000..dc263e79a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V4__task.sql @@ -0,0 +1,29 @@ +CREATE TABLE IF NOT EXISTS task ( + id BIGSERIAL PRIMARY KEY, + payload VARCHAR NOT NULL, + status VARCHAR DEFAULT 'UBEHANDLET'::CHARACTER VARYING NOT NULL, + versjon BIGINT DEFAULT 0, + opprettet_tid TIMESTAMP(3) DEFAULT LOCALTIMESTAMP, + type VARCHAR NOT NULL, + metadata VARCHAR, + trigger_tid TIMESTAMP(3) DEFAULT LOCALTIMESTAMP, + avvikstype VARCHAR +); + +CREATE INDEX IF NOT EXISTS henvendelse_status_idx + ON task (status); + +CREATE TABLE IF NOT EXISTS task_logg ( + id BIGSERIAL PRIMARY KEY, + task_id BIGINT NOT NULL + CONSTRAINT henvendelse_logg_henvendelse_id_fkey REFERENCES task, + type VARCHAR NOT NULL, + node VARCHAR NOT NULL, + opprettet_tid TIMESTAMP(3) DEFAULT LOCALTIMESTAMP, + melding VARCHAR, + endret_av VARCHAR DEFAULT 'VL'::CHARACTER VARYING +); + + +CREATE INDEX IF NOT EXISTS henvendelse_logg_henvendelse_id_idx + ON task_logg (task_id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V5__altered_fakta_tabeller.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V5__altered_fakta_tabeller.sql new file mode 100644 index 000000000..039fc0b7d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V5__altered_fakta_tabeller.sql @@ -0,0 +1,46 @@ +ALTER TABLE totrinnsresultatsgrunnlag + RENAME COLUMN gruppering_fakta_feilutbetaling_id TO fakta_feilutbetaling_id; + +ALTER TABLE totrinnsresultatsgrunnlag + DROP CONSTRAINT totrinnsresultatsgrunnlag_gruppering_fakta_feilutbetaling__fkey; + +ALTER TABLE totrinnsresultatsgrunnlag + ADD CONSTRAINT totrinnsresultatsgrunnlag_fakta_feilutbetaling_fkey FOREIGN KEY (fakta_feilutbetaling_id) REFERENCES fakta_feilutbetaling; + +ALTER TABLE fakta_feilutbetaling + ADD COLUMN behandling_id UUID REFERENCES behandling; + +COMMENT ON COLUMN fakta_feilutbetaling.behandling_id + IS 'Referanse til behandling'; + +ALTER TABLE fakta_feilutbetaling + ADD COLUMN aktiv BOOLEAN; + +COMMENT ON COLUMN fakta_feilutbetaling.aktiv + IS 'Angir status av fakta om feilutbetaling'; + +ALTER TABLE fakta_feilutbetaling + ALTER COLUMN behandling_id SET NOT NULL; + +ALTER TABLE fakta_feilutbetaling + ALTER COLUMN aktiv SET NOT NULL; + +ALTER TABLE fakta_feilutbetalingsperiode + ALTER COLUMN hendelsestype DROP NOT NULL; + +ALTER TABLE fakta_feilutbetalingsperiode + ALTER COLUMN hendelsesundertype DROP NOT NULL; + +DROP TABLE gruppering_fakta_feilutbetaling; + +ALTER TABLE fagsystemsbehandling + ADD COLUMN revurderingsvedtaksdato DATE; + +COMMENT ON COLUMN fagsystemsbehandling.revurderingsvedtaksdato + IS 'vedtaksdato av fagsystemsrevurdering'; + +ALTER TABLE fagsystemsbehandling + ALTER COLUMN revurderingsvedtaksdato SET NOT NULL; + +ALTER TABLE varsel + DROP COLUMN revurderingsvedtaksdato; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V6__altered_grunnlag_tabeller.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V6__altered_grunnlag_tabeller.sql new file mode 100644 index 000000000..e2c2e258e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V6__altered_grunnlag_tabeller.sql @@ -0,0 +1,39 @@ +ALTER TABLE kravgrunnlag431 + ADD COLUMN behandling_id UUID REFERENCES behandling; + +COMMENT ON COLUMN kravgrunnlag431.behandling_id + IS 'Fk: behandling fremmednøkkel for tilknyttet behandling'; + +ALTER TABLE kravgrunnlag431 + ADD COLUMN aktiv BOOLEAN; + +COMMENT ON COLUMN kravgrunnlag431.aktiv + IS 'Angir status av grunnlag'; + +ALTER TABLE kravgrunnlag431 + ADD COLUMN sperret BOOLEAN; + +COMMENT ON COLUMN kravgrunnlag431.sperret + IS 'Angir om grunnlaget har fått sper melding fra økonomi'; + +ALTER TABLE kravgrunnlag431 + ADD COLUMN utbet_id_type VARCHAR; + +COMMENT ON COLUMN kravgrunnlag431.utbet_id_type IS 'Angir om Utbetales-til-id er fnr, orgnr, TSS-nr etc'; + +ALTER TABLE kravgrunnlag431 + ALTER COLUMN behandling_id SET NOT NULL; + +ALTER TABLE kravgrunnlag431 + ALTER COLUMN aktiv SET NOT NULL; + +ALTER TABLE kravgrunnlag431 + ALTER COLUMN sperret SET NOT NULL; + +ALTER TABLE kravgrunnlag431 + ALTER COLUMN utbet_id_type SET NOT NULL; + +ALTER TABLE kravgrunnlag431 + RENAME COLUMN fagsystem TO fagsystem_id; + +DROP TABLE gruppering_krav_grunnlag; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V7__altered_behandling_steg_tilstand.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V7__altered_behandling_steg_tilstand.sql new file mode 100644 index 000000000..1cedb518b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V7__altered_behandling_steg_tilstand.sql @@ -0,0 +1,2 @@ +ALTER TABLE behandlingsstegstilstand + RENAME COLUMN behandlingsstegstype TO behandlingssteg; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V8__altered_behandling_steg_tilstand.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V8__altered_behandling_steg_tilstand.sql new file mode 100644 index 000000000..d137ee57f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V8__altered_behandling_steg_tilstand.sql @@ -0,0 +1,11 @@ +ALTER TABLE behandlingsstegstilstand + ADD COLUMN ventearsak VARCHAR; + +COMMENT ON COLUMN behandlingsstegstilstand.ventearsak + IS 'Årsak for at behandling er satt på vent'; + +ALTER TABLE behandlingsstegstilstand + ADD COLUMN tidsfrist DATE; + +COMMENT ON COLUMN behandlingsstegstilstand.tidsfrist + IS 'Behandling blir automatisk gjenopptatt etter dette tidspunktet'; diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V9__altered_okonomi_mottatt_xml_krav_grunnlag_431.sql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V9__altered_okonomi_mottatt_xml_krav_grunnlag_431.sql new file mode 100644 index 000000000..b18e5c25d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/db/migration/V9__altered_okonomi_mottatt_xml_krav_grunnlag_431.sql @@ -0,0 +1,60 @@ +ALTER TABLE okonomi_xml_mottatt + DROP COLUMN sekvens; + +ALTER TABLE okonomi_xml_mottatt + DROP COLUMN tilkoblet; + +ALTER TABLE okonomi_xml_mottatt + ADD COLUMN kravstatuskode VARCHAR NOT NULL; + +COMMENT ON COLUMN okonomi_xml_mottatt.kravstatuskode + IS 'Angir mottatt xmls kravstatus'; + +ALTER TABLE okonomi_xml_mottatt + ADD COLUMN ytelsestype VARCHAR NOT NULL; + +COMMENT ON COLUMN okonomi_xml_mottatt.ytelsestype + IS 'Angir tilhørende ytelsestype'; + +ALTER TABLE okonomi_xml_mottatt + RENAME COLUMN henvisning TO referanse; + +COMMENT ON COLUMN okonomi_xml_mottatt.referanse + IS 'Peker på referanse-feltet i kravgrunnlaget, og kommer opprinnelig fra fagsystemet'; + +ALTER TABLE okonomi_xml_mottatt + ALTER COLUMN referanse SET NOT NULL; + +ALTER TABLE okonomi_xml_mottatt + ADD COLUMN kontrollfelt VARCHAR; + +COMMENT ON COLUMN okonomi_xml_mottatt.kontrollfelt + IS 'Brukes ved innsending av tilbakekrevingsvedtak for å kontrollere at kravgrunnlaget ikke er blitt endret i mellomtiden'; + +ALTER TABLE okonomi_xml_mottatt + ADD COLUMN ekstern_kravgrunnlag_id BIGINT; + +COMMENT ON COLUMN okonomi_xml_mottatt.ekstern_kravgrunnlag_id + IS 'Referanse til kravgrunnlag fra ostbk. Brukes ved omgjøring for å hente nytt grunnlag'; + +ALTER TABLE okonomi_xml_mottatt + ADD COLUMN vedtak_id BIGINT NOT NULL; + +COMMENT ON COLUMN okonomi_xml_mottatt.vedtak_id + IS 'Identifikasjon av tilbakekrevingsvedtaket opprettet av tilbakekrevingskomponenten'; + +ALTER TABLE okonomi_xml_mottatt + ALTER COLUMN ekstern_fagsak_id SET NOT NULL; + +ALTER TABLE kravgrunnlag431 + ALTER COLUMN vedtak_id TYPE BIGINT USING vedtak_id::BIGINT; + +ALTER TABLE kravgrunnlag431 + ALTER COLUMN omgjort_vedtak_id TYPE BIGINT USING omgjort_vedtak_id::BIGINT; + +ALTER TABLE kravgrunnlag431 + ALTER COLUMN ekstern_kravgrunnlag_id TYPE BIGINT USING ekstern_kravgrunnlag_id::BIGINT; + +ALTER TABLE kravgrunnlag431 + ALTER COLUMN ekstern_kravgrunnlag_id SET NOT NULL; + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/fonts/SourceSansPro-Bold.ttf b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/fonts/SourceSansPro-Bold.ttf new file mode 100644 index 000000000..a253fdc20 Binary files /dev/null and b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/fonts/SourceSansPro-Bold.ttf differ diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/fonts/SourceSansPro-It.ttf b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/fonts/SourceSansPro-It.ttf new file mode 100644 index 000000000..51e634210 Binary files /dev/null and b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/fonts/SourceSansPro-It.ttf differ diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/fonts/SourceSansPro-Regular.ttf b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/fonts/SourceSansPro-Regular.ttf new file mode 100644 index 000000000..d9344ce81 Binary files /dev/null and b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/fonts/SourceSansPro-Regular.ttf differ diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/formats/pdf/nav_logo_svg.html b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/formats/pdf/nav_logo_svg.html new file mode 100644 index 000000000..d141bd45b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/formats/pdf/nav_logo_svg.html @@ -0,0 +1,42 @@ + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/formats/pdf/style.css b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/formats/pdf/style.css new file mode 100644 index 000000000..a7ddd2082 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/formats/pdf/style.css @@ -0,0 +1,193 @@ +/* WARNING: Uses CSS 2.1 */ + +@page { + margin: 10%; + size: A4 portrait; + @bottom-right { + font-family: "Source Sans Pro", sans-serif; + color: #3E3832; + content: "Side " counter(page) " av " counter(pages); + } +} + +@page tblpage { + /* sørger for at vedlegg havner på egen side */ + margin-top: 10%; + size: A4 portrait; + @bottom-right { + font-family: "Source Sans Pro", sans-serif; + color: #3E3832; + content: "Side " counter(page) " av " counter(pages); + } +} + +table, h4 { + /* sørger for at vedlegg havner på egen side */ + page: tblpage; +} + +* { + font-family: "Source Sans Pro", sans-serif !important; + color: #3E3832; + border-spacing: 0; +} + +html { + height: 0; /* unngår blank side til slutt */ +} + +body { + height: 100%; + padding: 0; + margin: 0; + position: relative; + line-height: 125%; +} + +body.utkast { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAxoAAARTCAYAAADssFV4AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAADAPSURBVHhe7d0JVxvn3YdhOV4gNhhjO/n+X6+Nd7Ax3vLmL2b6Ugq2NPMbaZbrOkfHjNo4iJ5Kc/Ns9/7+xwoAACDot+ZPAACAGKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AICtff/+vfkK4HZCAwDYysXFxerVq1ery8vL5hmA/yU0AICNVWS8f/9+9ffff6/evn0rNoA7CQ0AYCNtZFwnNoC7CA0A4Jdui4yW2ABuIzQAgJ/6WWS0xAZwk9AAAO60SWS0xAZwndAAAG61TWS0xAbQEhoAwP/oEhktsQEUoQEA/Jc+kdH68OHDegtcYLmEBgDwH4nIuH///ur09HR179695hlgiYQGALCWjIwHDx40zwBLJTQAAJEBxAkNAFg4kQEMQWgAwIKJDGAoQgMAFkpkAEMSGgCwQCIDGJrQAICFERnALggNAFgQkQHsitAAgIUQGcAuCQ0AWACRAeya0ACAmRMZwD4IDQCYMZEB7IvQAICZEhnAPgkNAJihRGSUZ8+eiQygE6EBADOTioxSf8+PHz+aK4DNCQ0AmJFkZJRv376t3rx5IzaArQkNAJiJdGS0xAbQhdAAgBkYKjJaYgPYltAAgIkbOjJaYgPYhtAAgAnbVWS0xAawKaEBABO168hoiQ1gE0IDACZoX5HREhvArwgNAJiYfUdGS2wAPyM0AGBCxhIZLbEB3EVoAMBEJCLj/v37qxcvXqwePnzYPNOf2ABuIzQAYAJSkfH8+fN1ZJyenooNYFBCAwBGLhkZ9Wf57bffxAYwKKEBACM2RGS0xAYwJKEBACM1ZGS0xAYwFKEBACO0i8hoiQ1gCEIDAEbm+/fvO4uMltgA0oQGAIxMxUHd9He1bWS0xAaQJDQAYIQODg46xUbXyGiJDSBFaADASG0bG30joyU2gAShAQAjtmlspCKjJTaAvoQGAIzcr2IjHRktsQH0ITQAYALuio2hIqMlNoCuhAYATMTN2Bg6MlpiA+ji3t//aL4GACbg8vJy9eHDh51ExnUVBW/fvl19/fq1eaa/Bw8erF9HxQwwL0IDACaoPr7v3bvXXO2O2AA25f/RADBB+4iMYhoVsCmhAQBsRWwAmxAaAMDWxAbwK0IDAOhEbAA/IzQAgM7EBnAXu04BwMDqo7Zunr98+bK+ef7+/fv6uXrU9rTto3Zfqse+Fnr3Ua/LblTAdUIDAAZQH6910/358+f1uRcVF5uoyDg8PFw9fvx4ctEhNoDrhAYABNXHasXF+fn5xnFxl7rJfvLkyTo8phIcYgNoCQ0ACKmRi7Ozs/U0qaRHjx6tnj59ur7hngKxARShAQA9VVhUYFRoDOno6Gg9wjGF0Q2xAQgNAOihpkm9f/9+PWVqFw4ODlYnJyeTuNkWG7BsQgMAOqiPz1qH8fHjx+aZ3ak1G8+ePWuuxk1swHL5fygAbKm9ed5HZNTNdU2hmor6fp2zActkRAMAtrDPm9xam1G/yU/etO+KkQ1YHv/PBIAN1U3yPn+Tnh4Z2CUjG7A8QgMANrDvyKg1GbXN7ZSJDVgWoQEAv9DezO5rtnHtMlULwOdgiNiogxH7Ho4I5AkNAPiJfUfG8fHx6vfff2+u5iEZG7VuZcpTymDOhAYA3KF+S14LmPc1LacO56vHHCVio42MqU8pg7my6xQA3KI+Hisyvnz50jzTTx20V7sk1Q12O9XnZyeJ1yjG06dPJ3EKeB8VcV12oxIZMH5CAwBuUWdknJ2dNVfd1M1wTX2q9RUVGDfVTXadLH5xcfFfN9oVJbX4e+6R0do2NkQGTIPQAIAb6ob39evXzVU3NSJRkXFbYNxUH8UVHB8+fFhPJaqb6KVERmvT2BAZMB1CAwCuqRveioyuuxj1uRGuf3fZJE7m6FexITJgWiwGB4Brzs/PO0dGBUKdVN31Rrj++aVGRqnXXiFx2wJxkQHTIzQAoFFb2X769Km52k4bGbZZ7ee22BAZME2mTgFA4927d+u1EtuqG+F9RUZNN6rdq2p3rPq6PtavP+p7q92u6ntrH/Xc2LXTqCr+RAZMk9AAgH/UDe2rV6+aq+30mS7VRd2E37Zb1SZqxODx48frxer3799vnh2nep01jW0fAQf0JzQA4B+141OXaVN1oF7tLrULFUO1hqTLqMtt2gMBl7wuBBiO0ABg8eqj8N///vf6z23UiMDLly8Hn4pUgVHnetQIRlpFRh0MWGd9ACQJDQAWr9Y41HqAbdXagTpcbyhDBsZNNbJxdHQ0ifUbwDQYKwVg8Woh9bZqgfVQkVHrEt6/f79eM7KLyCgVNBVbtS4CIEFoALB4XUKjFlOn1SSDWoPx119/7Swwrqufg9gAUoQGAItWN/c1RWlb6dGMmr5VIxgVGvtUu1hVbJhZDfQlNABYtLqh7nJTndoatqZJ1Y19PbqeSJ5WsVG7cAH0ITQAWLQu04Rqp6a+i6avT5Oq0Yyxqalb+5i+BcyH0ACALVWcdJlu1RrLNKlfqQXpYxllAaZHaACwaF1HJrocmtcuth7TNKlfGXsMAeMlNABYtK6hUaeIbzqqUYHx5s2b9WOM06R+pqZP1ZoNgG0JDQAWrdZb1GNbNX2qRibuugmvNRg16vH69et1YHTZQncsKqoAtuVkcAAWr4Khz0jDw4cPV4eHh+tgqY/Vio+KjLl8xNaoz59//tl7ATywLEIDgMWrU7HPzs6aq2lqTyp/9OjR+utS60BqNKLLepKbTk5OBjmkEJgvoQHA4tUNeW0zO0UVF0+ePFkHxl1qLUnfBeg1YvPs2bPmCuDXrNEAYPHq8L2f3aiPUX2/z58/X52env7ye68RjhcvXvQ6ZHDKa0yA/RAaAPCPx48fN1+NW8VCxcUmgXFdrR/pMyJRi9+7HG4ILJfQAIB/1BSkWtQ9VrUQ+/j4ePXy5cv199plYXa9vj7rLBzeB2xDaADAP9ob+TGq9REVGLUWo+/OT31Cw4gGsA2hAQCNmopUN/NjUWsrah1GTXnqs77iuj6jNvaPAbYhNADgmqOjo70vDG9HV2oBd/p7qb+7a7QIDWAbQgMArqkb8RpB2Nd6jZra9Mcff0SmSd2l69+bGlUBlkFoAMANtUPTtrs69VULvGsEow7Gq3//kLqOTAz9fQHz4h0DAG7RxsbQazYqZtrzMHYxilKR0XX3KCMawDaEBgDc4fpaiVqYnVQjGBUY9djlyMnXr1+br7ZTETTUVC5gnu79bWUXAPxSfVxeXFysPn36tPr27Vvz7HZqlKQOBqx1GPsaHfjw4cP6NWyrRnbGuv0vME5CAwC2VKFR0XF5ebmehvSzj9IarWgf+x4VqHMw/vrrr05rNGpqV43CAGxKaABAD/UxWo+Kj7qRr5CoR41e1KjFmKYbnZ2drT5+/Nhcba5eS+2EZeoUsA2hAQALUCH06tWr5mo7dbZIPQC2YTE4AMxcjbS8e/euudperSsB2JbQAIAZq4kLtQC86wL2WgTu/AygC+8cADBTFRnn5+erz58/N89spwJj6HNEgPkSGgAwQ21kdFn83ap1GUYzgK68ewCwN/Wb9q4HyM1NTW1K7c/STpfqExm1lW2d9wHQlV2nANiLOjSuboZLnTFRC47r5naJW6jWWRyvX79ev/aTk5NeJ4VXsNTC765rMkqNYrx8+dJoBtCL0ABg565HxnV1Y1vBUY+l3OTWx3BFxvUwqNCoU7jrgL9N1c5S9XOtUYy+H+3Pnz/vFTsARWgAsFN3RcZNNW2ngmObm+2pqY/g9+/f37lYu272Dw8P14/bwqv++QqUOqG8fq4VG305MwNIERoA7MymkXFdhUbtfDTHaVW1WLsem6hTxttHqaio9S2JuGhV2JyenjoBHIgQGgDsRJfIuG5u06pqFKPPIXppDx48WE+ZWsqUNWB4QgOAwfWNjJumPq2qRiLevHnTey1FSo2SvHjxQmQAUUIDgEGlI+O6KU6rqqlOtfi7dpoag4qMGslop2QBpAgNAAYzZGRcN5VpVfWR+/bt29WXL1+aZ/ZLZABDEhoADGJXkXHTmKdV1Q5TFxcXzdV+iQxgaEIDgLh9RcZ1Y5tWNYafSat+NrW7lDUZwJCEBgBRda7Dq1evmqv9G8O0qjH9TOrnUIcB2sIWGJrQACCqPlZunnQ9FvucVlXb2dbUqX197FZYnJycrA//A9gFoQFA3JimCd1mX9OqKr7q57LrxeB1RsazZ8/WfwLsitAAIK4+Wv7666/oqdVD2Me0qvrZXF5ers7Ozgbf4rYiqoJqn9PGgOUSGgAM4vz8fP2Yil1Pq6qP348fP64fQ3wU12upyLCrFLAvQgOAQdRv62tUY2oqNOomvdYy7GJaVf2casvbWsORWNdS3/fR0ZFpUsDeCQ0ABjOmcyO2tY9pVV+/fl0HRz02nVZVMVRxUetNHj16ZIoUMBpCA4DB1I1z7UDVRf1Wvv75Ws+wb3UjX9OQdrlbVa1vqdioUY76sw2PCouKiZoSVY/6nna5oB1gU0IDgEG9efOm0y5L9dv5Orm6brRrHcMYRkZ2Pa0KYMqEBgCDqmlA7969a6628+LFi/+MItRv9Gvb3Hrs+6OrnVZVC8gttga4ndAAYFD1MVOnYm+65uC6Gjmo8x+uqylFNbpRoxxj2D53H9OqAKZAaAAwuIqCOjeii5cvX966g1J9fNVoSf3dYziF3LQqgP8mNAAYXI081Fa3XT5y6ub96dOnzdX/qr+z1oBUcOz6xO3bmFYFcEVoALATHz58WK+v6OKPP/7Y6Ka9dqmq4KiRjjEwrQpYMqEBwE7UGo2uB/jVzfrx8XFz9Ws1lapdOD4GplUBSyQ0ANiZ2n2qy2hD3ZzXqMa2h9HVlK2KjRrlGMPHnWlVwJIIDQB2ptZQ1LkaXdSIRo1sdFEfde1OVV12vxqCaVXA3AkNAHamPnIqNGotxbZqNKBGNfpMPap/f500XsHR5XsYgmlVwFwJDQB2qkYW3r9/31xtp3afqpvyvuqjr104XuExBqZVAXOz3WRXAOipfnO/7VqLVmqtRY0cPHr0aHVwcNA8s3+1nuT8/Hy9YL7WsoxlxAWgK6EBwE7VTX7XUYlaX5EagahF4rXl7hjVgvmaYjaGgwgBuhIaAAu2r4XRfaY/JUY1xhwZpWLs9PT01hPRAaZCaAAsVLvt6z7U1Klai9BFTSnqM61oKpFRU7sApkxoACxQe7O9z/1Aum5VW2otQxciA2B3hAbAwozlZrumBXW9oa7zOLYd1Ui87vqej46OOi9m/xmRAcyN0ABYkJs32/ve4bzPqMY2075SkfH8+fN1aNR5HrXVbmoNhcgA5khoACzEGKcN1Y1115v12plpk12ZkpHRjmRUGNSC9hcvXvQOBJEBzJXQAFiAMUZGaW/Yu6rX9TNDRMZ19f3XWRz1n798+XLrBe4iA5gzoQEwcz+72d731KlSN+d1w91FvbY66O42Q0fGTfXfPTk5Wf35558breMQGcDcCQ2AGUvcbA+t76jGbWs1dh0Z19V//1frOEQGsARCA2CmNrnZHsOIRuk7fer6qMY+I+O6NqBuruMQGcBS3PvnQ2YcnzIAxGx6s103u3VDPQbv3r1bL/Du4vj4eL2D1Vgi4y61eL0+dh8+fNg8AzBfRjQAZmabm+0x/a6p71a39RhzZJT6+0UGsBRCA2BGEr/R35e6Ae96E15Tp87OzpqrboaODICl8W4KMBNTjoxWn1GNPkQGQJ53VIAZ+PLlS6fIGNsyvTqT4v79+83VbogMgGF4VwWYgbvOkpiavlvdbktkAAzHOyvADNQNehdj3HiwzwF+2xAZAMPy7gowA7u4Md+VuvGv2BiSyAAYnndYgBmYU2iUIadPiQyA3fAuCzADc5o6VSoGamF4msgA2B3vtAAzMLcRjZLe6lZkAOyWd1uAGZjbiEapw/sqDhJEBsDueccFmIE5jmjUa0qMaogMgP3wrgswA3MMjXJ4eNgrEOrwP5EBsB/eeQFmoktsjHnqVKnX1GcHqnp9c40wgLETGgAzMdcb6j6hUSemX1xcNFcA7JLQAJiJrqEx9lGNmvbU5wC/jx8/jv41AsyR0ACYiTlPEeqzKPz79++ry8vL5gqAXREaADMx59ConaMePXrUXG3PqAbA7gkNgJmY69SpVp9Rja9fv64fAOyO0ACYiTmPaJQa0ajtars6Pz9vvgJgF4QGwEzMfUSjXl+fUY0vX74Y1QDYIaEBMBNzH9EotftUn9dZazUA2A2hATATSwiNeo19ztX4/Pnz6tu3b80VAEMSGgAz0TU0prYbU5/QKEY1AHZDaADsWP1WvU6sTlvCiEapBeGHh4fN1fbqpPA6WwOAYQkNgB369OnT6t27d6s3b97EY2MpIxqlz6LwUv87ADAsoQGwI3Vz++HDh/XXtU4gHRtLGdEoDx8+XD+6qv8thhhVAuD/CQ2AHbgeGa10bCwpNEqfUY0axakpVAAMR2gADOy2yGglY6NraNR6hSlOnzo4OFj99lv3jzHTpwCGde+fD5fpfboATMTPIuO6Bw8erJ4/f97rxvny8nL19u3b5mo79e9tpyO1jz7fy67UDlJnZ2fN1eYqyk5PT9enjQMwDKEBMJBNI6PVNzbq5OsaHUmp3Z1uxsfYpmfVSNBff/211YiMyADYDaEBMIBtI6PVJza+fv26ev36dXM1jPr+6ga9DY+KkX3HR/2cN50GJTIAdkdoAIR1jYxW19io9R6vXr1qrnajbtyvj3i08bFLm75ukQGwW0IDIKhvZLS6xEZNI/r3v//dXO1Pfc8346PrdLBN1dqUWqNyF5EBsHtCAyCkTvyuw/hSto2Nejv/17/+1VyNS72W6+FR18kpVz9bCC8yAPZDaACE1IhCLcauqTwp28TGmEPjNtfDox591nvUa6/1KTd/9iIDYH+EBkDQvmOjQmOqb+sVBRUcFQVtfGwz5aoO4Hv//n1zJTIA9k1oAITtMzamHBq32WaL3XrdtdVt/fxFBsD+CQ2AAewrNupGu076nrP6ObTRUSFxfcrV+fn5+hA/kQGwf0IDYCD7iI3a5jX575uCdspVPernU+EhMgD2b/PJrwBspWKgoqBuflMqIipeKmJuc9e0ojmr35fVqei1tbDIABgPoQGwhdpGdZuB4F3HxhJDo1iTATA+QgNgQ/Ub8zqroXY2GmtsLDE0RAbAOAkNgA1cP/G7DuYba2x0CY0nT56sH7XGYWpEBsB4WQwO8AvXI+O6w8PD1cnJyVY390MvEK8AqvMktlGv4ffff19/Xd/f169f11PEat1D8vtMExkA4yY0AH7irshojS02anvX+p63cT00bqrvtYKjDY+xbJ0rMgDGT2gA3OFXkdEaU2zU9Kc+Ixq/UqFxPTzqdeyayACYBqEBcItNI6M1ltjo4unTp6vHjx83V5urj48KjzY66jH0R4rIAJgOoQFww7aR0ZpqbHQNjZvq46Rex/XwSBIZANMiNACu6RoZrSnGRio0bqqPl+sLy+vrrkQGwPQIDYBG38godTL1ixcv1jtAbWOfsTFUaNxUr7HLjlYiA2CahAbAP1KRUTs/1Z9d7Cs2jo+P1+do7Fq93uvTrG7b0UpkAEyX0AAWbwyR0dpHbOwrNG66bWG5yACYLqEBLNqYIqO169gYS2hcVx9N9dh2ChoA4+EdHFisMUZGqZvr+jvrXIylqilTIgNg2ryLA4s01sho7TI2DGwDMAShASzO2COjZWQDgCkTGsCiTCUyWmIDgKkSGsBiTC0yWmIDgCkSGsAiTDUyWkPGhjUaAAxBaACzN/XIaA0VG3V2RW2pCwBJQgOYtblERmuI2KjzOurcDrEBQJLQAGZrbpHREhsATIHQAGZprpHREhsAjJ3QAGZn7pHREhsAjJnQAGYlERnlyZMno46MltgAYKyEBjAbqcgo9fd8/vy5uRo3sQHAGAkNYBaSkdF69+6d2BAbAHQkNIDJGyIyWmJDbADQjdAAJm3IyGiJDbEBwPaEBjBZu4iMltgQGwBsR2gAk7TLyGiJDbEBwOaEBjA5+4iMltgQGwBsRmgAk7LPyGiJDbEBwK8JDWAyxhAZLbEhNgD4OaEBTEIiMuqGO2mKsfHw4cPmmf4qNupn8PfffzfPAMD/ExrA6CUi4/79+6sXL16sjo+Pm2cyphYbp6ensdi4d+/e6ujoaP0nANwkNIBRS0VG/Ta//nzy5InYCMRGxUX9PY8ePWqeAYD/JjSA0UpHRkts9IsNkQHAJoQGMEpDRUZLbHSLDZEBwKaEBjA6Q0dGS2xsFxsiA4BtCA1gVHYVGS2xsVlsiAwAtiU0gNGobVIvLi6aq262iYyW2Ph5bIgMALoQGsBotDe0XRcpd4mMlti4PTZEBgBdCQ1gVLouUu4TGS2x8d8/e5EBQB/3/nakKzBCP378WL19+3b19evX5pm7JSLjuo8fP67Ozs6aq4xnz56tDg8Pm6txq599BVIdxicyAOhKaACjtUlspCOjtfTYAIC+TJ0CRutX06iGioyy9GlUANCX0ABG7a7YGDIyWmIDALoTGsBOXV5errex3cbN2NhFZLTEBgB0Y40GsDPtYXwHBwfr9Qq1q9E2as1G/fN147+LyLjOmg0A2I7QAHbi5onfXWNjn8QGAGzO1ClgcDcjo9QUqppCNKXfdZhGBQCbExrAoG6LjJbYuCI2AJgjoQEM5meR0RIbV8QGAHMjNIBBbBIZLbFxRWwAMCdCA4jbJjJaYuOK2ABgLoQGENUlMlpi44rYAGAOhAYQ0ycyWmLjitgAYOqEBhBxcXHROzJaYuOK2ABgyoQGEPHw4cPVb7/l3lLExhWxAcBUCQ0g4sGDB6vnz5+LDbEBAGtCA4gRG1fEBgAIDSBMbFwRGwAsndAA4sTGFbEBwJIJDWAQYuOK2ABgqYQGMBixcUVsALBEQgMYlNi4IjYAWBqhAQxObFxJx8a9e/dW9+/fb64AYFyEBrATYuNKKjYqMurnWQclAsAYCQ1gZ8TGlb6xITIAmAKhAeyU2LjSNTZEBgBTITSAnRMbV7aNDZEBwJQIDWAvxMaVTWNDZAAwNUID2BuxceVXsSEyAJgioQHsldi4cldsiAwApkpoAHsnNq7cjA2RAcCU3fvnQ3g6n8LArH379m315s2b1Y8fP5pn+js4OFg9e/ZsfdM+FR8/flydn5+LDAAmTWgAozJEbBweHq5OTk4mFRv1+pMjPACwaz7FYKHG+juGIaZRff78efX+/ftJTaMSGQBMnU8yWKBPnz6N+sZbbADA9AkNWJiKjA8fPoz+xnuo2Dg7O2uuAIAhCQ1YkDYyWkuMjfoZ1GJrAGBYQgMW4mZktJYYGzWqUa8bABiO0IAFuCsyWkuMjTpj48uXL80VAJAmNGDmfhUZrSXGxtu3b9fb6QIAeUIDZmzTyGgtLTbqdVZsfP/+vXkGAEgRGjBT20ZGa2mxUZFRsTHW1wsAUyU0YIa6RkZrabFR06dsewsAWUIDZqZvZLSWFhv1c7u8vGyuAIC+hAbMyI8fP1bn5+fNVX9Li416rfUzBAD6ExowI3XDnbzxLlOJjXv37jXPdFeR4TA/AMgQGjAz6d/ylynExunpaXPVT4WGXagAoD+hATO0xNh49OjR6tmzZ81VPxcXF81XAEBXQgNmaomxcXh4uHr69Glz1V2FxlhfIwBMhdCAGVtibDx+/Hh1dHTUXHVTU6ecGA4A/QgNmLklxsaTJ09WBwcHzVU39RoBgO6EBizA0mKjdqA6OTnp9XqFBgD0IzRgIZYWG/U6j4+Pm6vt1fQp6zQAoDuhAQuytNioxeF9ztdweB8AdCc0YGGWFBsVGQ8fPmyutmdEAwC6ExqwQEuKjTpfoyuhAQDdCQ1YqKXERp8RjT7TrgBg6YQGLNgSYqPP93H//v3mKwBgW0IDFm7usdH1e6ifixENAOhOaACzjo2vX782X22nz5QrAEBoAI05xkb9ey8vL5ur7dTPAwDoTmgA/zG32KjRjDp4r4s+u1UBAEIDuGEusVH/rg8fPjRX26lF4EY0AKAfoQH8jznExqdPn1bfvn1rrrbz+++/WwgOAD0JDeBWU46Nmi51fn7eXG3v8ePHzVcAQFdCA7jTULHx7t27wWLjx48fqzdv3nT++ysykq8XAJbKpynwU0PERu0EVTFQUZDURkbXBeDlyZMnzVcAQB9CA/ilIWKjdoTqGwXXVWS8ffu287qM8vTpU6eBA0CI0AA2MkRsVBS8fv26VxyUipb6e7oezlcODg7Wi8ABgIx7f+9qCxhgFioK0tOeaoenGk3Y9ka/3r4uLi46b2Pbqnh6+fKltRkAECQ0gK0NERulRhWOjo5WDx8+bJ65Xb1t1ejF2dlZr1GMVo3UOKAPALKEBtDJULFRKjQqOurmv0YZasSj3qpqPceXL1/WO1el1nbU4u/j4+PmCgBIERpAZ0PGxi5UyJyenjqcDwAGYEIy0NkQC8R3pUZNRAYADEdoAL20sTGlbWFFBgAMT2gAvU0pNup7rciwwxQADMsaDSCmPTQvsRPUENqRDJEBAMMTGkBUvaW8f/9+vTPUmNTC72fPnokMANgRoQHE1dvKx48fV+fn580z+2V3KQDYPaEBDKZO7a7RjX2qczLqEECRAQC7JTSAQdUBe7VuY9dvNRUWJycnq8PDw+YZAGCXhAYwuDrYr2IjdZr3r9TOUrUeo/4EAPZDaAA7UW81Z2dnq0+fPjXPDOPx48er4+NjU6UAYM+EBrBTNZWq1m2kRzdqwffTp0+NYgDASAgNYOfqbadGNmpnqjp7o4+Dg4P1KEb9CQCMh9AA9qbefmr9Rp25UY9NRjlqSlSNWtQi73pM4TRyAFgioQGMQr0VVWi0jxrpaEc7KizqUVFRB+5ZfwEA4yc0AACAuN+aPwEAAGKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABAnNAAAgDihAQAAxAkNAAAgTmgAAABxQgMAAIgTGgAAQJzQAAAA4oQGAAAQJzQAAIA4oQEAAMQJDQAAIE5oAAAAcUIDAACIExoAAECc0AAAAOKEBgAAECc0AACAOKEBAADECQ0AACBOaAAAAHFCAwAAiBMaAABAnNAAAADihAYAABC2Wv0fvzMrEjqfXm4AAAAASUVORK5CYII"); + background-size: 168mm 247mm; +} + +#person { + color: #807d7c; + margin-bottom: 10mm; +} + +#institusjon { + color: #807d7c; + margin-bottom: 5mm; +} + +#content { + margin: 0; +} + +#footer { + position: absolute; + margin: 0; + bottom: 0; + width: 100%; + height: 25mm; +} + +#nav_logo { + width: 30mm; + height: 18.2mm; + margin: auto; + padding: 0 0 10mm 0; + position: absolute; + right: 0; +} + +#dato { + position: absolute; + right: 0; + top: 25mm; + margin: auto; +} + +#hovedoverskrift { + padding-top: 30mm; +} + +.samepage { + page-break-inside: avoid; +} + +h1 { + font-size: 22px; + font-weight: 700; + color: #3E3832; + line-height: 125%; +} + +h2 { + font-size: 16px; + margin: 10mm 0 1.5mm 0; + color: #3E3832; +} + +h3 { + font-size: 16px; + margin: 10mm 0 -1.5mm 0; + color: #666463; +} + +h4 { + font-size: 16px; + margin: 10mm 0 1.5mm 0; + color: #3E3832; +} + +p { + font-size: 16px; + margin: 3mm 0 2mm 0; +} + +p.hilsen { + margin-top: 10mm; +} + +table { + border-collapse: collapse; + border-spacing: 0; + font-size: 12px; +} + +table, th, td { + border: 1px solid #3E3832; +} + +.sumrad { + border: 1px solid #3E3832; + border-top: 2px solid #3E3832; +} + +th, td { + padding: 5px 10px; + margin: 0; + overflow: hidden; +} + +th.sum, td.sum { + max-width: 40mm; +} + +th.periode, td.periode { + width: 33mm; +} + +th { + vertical-align: top; +} + +thead { + background-color: #c6d9f1; +} + +.sum { + font-weight: 700; +} + +.tall { + text-align: right; +} +.signatur { + page: auto; + padding: 0; + font-size: 16px; + border: none; + width: 100% +} + +.saksbehandler{ + padding: 0; + border: none; + text-align: left; +} +.beslutter{ + padding: 0; + text-align: right; + border: none; +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/logback-e2e.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/logback-e2e.xml new file mode 100644 index 000000000..1371e1ea6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/logback-e2e.xml @@ -0,0 +1,27 @@ + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %magenta([%thread]) %highlight(%-5level) %logger{36}.%M - %msg%n + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %magenta([%thread]) %highlight(%-5level) %magenta(%logger{36}.%M) - %msg%n + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/logback-spring.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..cc0d94a2c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/logback-spring.xml @@ -0,0 +1,59 @@ + + + + + + + /secure-logs/secure.log + + /secure-logs/secure.log.%i + 1 + 1 + + + 50MB + + + + + + + + + + + + + + + + %m%n%xEx + + + + audit.nais + 6514 + FAMILIE-TILBAKE + 128000 + + + + + + + + + + + + + + + + + + + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/pdl/hent-adressebeskyttelse-bolk.graphql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/pdl/hent-adressebeskyttelse-bolk.graphql new file mode 100644 index 000000000..042f3a3a6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/pdl/hent-adressebeskyttelse-bolk.graphql @@ -0,0 +1,9 @@ +query($identer: [ID!]!) {personBolk: hentPersonBolk(identer: $identer) { + ident, + person { + adressebeskyttelse { + gradering + } + }, + code +}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/pdl/hentIdenter.graphql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/pdl/hentIdenter.graphql new file mode 100644 index 000000000..80eda199b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/pdl/hentIdenter.graphql @@ -0,0 +1,8 @@ +query($ident: ID!) { + pdlIdenter: hentIdenter(ident: $ident, historikk: false) { + identer{ + ident + gruppe + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/pdl/hentperson-enkel.graphql b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/pdl/hentperson-enkel.graphql new file mode 100644 index 000000000..4f543d3ae --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/pdl/hentperson-enkel.graphql @@ -0,0 +1,21 @@ +query($ident: ID!) {person: hentPerson(ident: $ident) { + foedsel { + foedselsdato + } + navn { + fornavn + mellomnavn + etternavn + } + kjoenn { + kjoenn + } + doedsfall { + doedsdato + } + folkeregisteridentifikator { + identifikasjonsnummer + status + type + } +}} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/felles/sp\303\270rsm\303\245l_kontaktinformasjon.hbs" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/felles/sp\303\270rsm\303\245l_kontaktinformasjon.hbs" new file mode 100644 index 000000000..5ad4d3f51 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/felles/sp\303\270rsm\303\245l_kontaktinformasjon.hbs" @@ -0,0 +1,8 @@ +{{~#* inline "pronomen"~}}{{#or gjelderDødsfall institusjon}}dere{{else}}du{{/or}}{{~/inline~}} +{{~#* inline "Pronomen"~}}{{#or gjelderDødsfall institusjon}}Dere{{else}}Du{{/or}}{{~/inline~}} +_Har {{> pronomen}} spørsmål? +{{> Pronomen}} finner mer informasjon på {{{ytelseUrl}}}. + +På nav.no/kontakt kan {{> pronomen}} chatte eller skrive til oss. + +Hvis {{> pronomen}} ikke finner svar på nav.no kan {{> pronomen}} ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/header.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/header.hbs new file mode 100644 index 000000000..24dfbf237 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/header.hbs @@ -0,0 +1,12 @@ +
    Dato: {{{kortdato brev.dato}}}
    +

    {{{brev.overskrift}}}

    +{{#if institusjon }} +
    +Navn: {{{institusjon.navn}}}
    +Organisasjonsnummer: {{{institusjon.organisasjonsnummer}}} +
    +{{/if}} +
    +{{#if institusjon}}Gjelder{{else}}Navn{{/if}}: {{{person.navn}}}
    +Fødselsnummer: {{{person.ident}}} +
    diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/henleggelse/henleggelse.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/henleggelse/henleggelse.hbs new file mode 100644 index 000000000..46ba96d98 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/henleggelse/henleggelse.hbs @@ -0,0 +1,14 @@ +{{~#* inline "dereEllerdu"~}}{{#if institusjon}}dere{{else}}du{{/if}}{{~/inline~}} +{{~#* inline "dereEllerDeg"~}}{{#if institusjon}}dere{{else}}deg{{/if}}{{~/inline~}} + +Vi sendte {{#if (not gjelderDødsfall)}}{{> dereEllerDeg}} {{/if}}et varsel {{{dato varsletDato}}} om at {{#if gjelderDødsfall}}det var utbetalt for mye{{else}}{{> dereEllerdu}} hadde fått for mye utbetalt{{/if}} i {{{ytelsesnavnUbestemt}}}. Vi har i ettertid avsluttet saken om tilbakebetaling fordi det ikke lenger er et feilutbetalt beløp å betale tilbake. + +{{> nb/felles/spørsmål_kontaktinformasjon}} + + +Med vennlig hilsen +{{{avsenderenhet}}} + +{{{ansvarligSaksbehandler}}}{{#if brevmetadata.finnesAnnenMottaker}} + +Brev med likt innhold er sendt til {{{annenMottagersNavn}}}{{/if}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/henleggelse/henleggelse_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/henleggelse/henleggelse_overskrift.hbs new file mode 100644 index 000000000..f4afa24bd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/henleggelse/henleggelse_overskrift.hbs @@ -0,0 +1 @@ +NAV har avsluttet saken {{#and (not gjelderDødsfall) (not institusjon)}}din {{/and}}om tilbakebetaling \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/henleggelse/henleggelse_revurdering.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/henleggelse/henleggelse_revurdering.hbs new file mode 100644 index 000000000..3ca20ad12 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/henleggelse/henleggelse_revurdering.hbs @@ -0,0 +1,11 @@ +{{{fritekstFraSaksbehandler}}} + +{{> nb/felles/spørsmål_kontaktinformasjon}} + + +Med vennlig hilsen +{{{avsenderenhet}}} + +{{{ansvarligSaksbehandler}}}{{#if brevmetadata.finnesAnnenMottaker}} + +Brev med likt innhold er sendt til {{{annenMottagersNavn}}}{{/if}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/henleggelse/henleggelse_revurdering_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/henleggelse/henleggelse_revurdering_overskrift.hbs new file mode 100644 index 000000000..fc9d50caf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/henleggelse/henleggelse_revurdering_overskrift.hbs @@ -0,0 +1 @@ +Tilbakebetaling {{{ytelsesnavnUbestemt}}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/innhentdokumentasjon/innhent_dokumentasjon.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/innhentdokumentasjon/innhent_dokumentasjon.hbs new file mode 100644 index 000000000..500f0fb69 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/innhentdokumentasjon/innhent_dokumentasjon.hbs @@ -0,0 +1,25 @@ +Vi har fått nye opplysninger i saken om tilbakebetaling av {{{ytelsesnavnUbestemt}}}, men trenger mer dokumentasjon for å behandle saken. +_{{#or gjelderDødsfall institusjon}}Dere{{else}}Du{{/or}} må sende oss +{{{fritekstFraSaksbehandler}}} +_Saken blir behandlet etter frist har gått ut +Hvis vi ikke får opplysningene innen {{{dato fristdato}}}, behandler vi saken med de opplysningene vi har. + +Dette går fram av {{#if isBarnetrygd}}barnetrygdloven §§ 17 og 18{{else if isKontantstøtte}}kontantstøtteloven §§ 12 og 13{{else}}folketrygdloven § 21-3{{/if}}. +{{#or gjelderDødsfall institusjon}} +_Rett til uttalelse +Dere kan sende uttalelsen til oss i posten. Adressen finner dere på nav.no/ettersendelser. +Dere kan også ta kontakt med oss på nav.no/kontakt. +{{else}} +_Slik uttaler du deg +Du kan sende uttalelsen din ved å logge deg inn på nav.no/beskjedtilnav og velge «Send beskjed til NAV». Du kan også sende uttalelsen din til oss i posten. Adressen finner du på +nav.no/ettersendelser. +{{/or}} +{{> nb/felles/spørsmål_kontaktinformasjon}} + + +Med vennlig hilsen +{{{avsenderenhet}}} + +{{{ansvarligSaksbehandler}}}{{#if brevmetadata.finnesAnnenMottaker}} + +Brev med likt innhold er sendt til {{{annenMottagersNavn}}}{{/if}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/innhentdokumentasjon/innhent_dokumentasjon_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/innhentdokumentasjon/innhent_dokumentasjon_overskrift.hbs new file mode 100644 index 000000000..06d9bee60 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/innhentdokumentasjon/innhent_dokumentasjon_overskrift.hbs @@ -0,0 +1 @@ +Vi trenger flere opplysninger \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/korrigert_varsel.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/korrigert_varsel.hbs new file mode 100644 index 000000000..84be4a4c5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/korrigert_varsel.hbs @@ -0,0 +1,11 @@ +{{~#* inline "førSkatt"}}{{#if ytelseMedSkatt}} før skatt{{/if}}{{~/inline~}} +{{~#* inline "duEllerDere"~}}{{#if institusjon}}dere{{else}}du{{/if}}{{~/inline~}} + +Vi varslet {{#and (not gjelderDødsfall) (not institusjon)}}deg {{/and}}{{{dato varsletDato}}} om at {{#or gjelderDødsfall institusjon}}det var{{else}}du hadde fått{{/or}} utbetalt {{{kroner varsletBeløp}}} for mye i {{{ytelsesnavnUbestemt}}}{{> førSkatt}}. + +Riktig beløp som er utbetalt for mye, er {{{kroner beløp}}}{{> førSkatt}}. + +{{#eq feilutbetaltePerioder.length 1}}Perioden{{else}}Periodene{{/eq}} {{#if gjelderDødsfall}}det er{{else}}{{> duEllerDere}} har fått{{/if}} for mye utbetalt for, er: +*-{{#each feilutbetaltePerioder }} +Fra og med {{{dato fom}}} til og med {{{dato tom}}}.{{/each}}-* +{{> nb/varsel/varsel_slutt}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/korrigert_varsel_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/korrigert_varsel_overskrift.hbs new file mode 100644 index 000000000..bee448585 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/korrigert_varsel_overskrift.hbs @@ -0,0 +1 @@ +Korrigert varsel om feilutbetalt {{{ytelsesnavnUbestemt}}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/varsel.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/varsel.hbs new file mode 100644 index 000000000..a8e20d219 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/varsel.hbs @@ -0,0 +1,15 @@ +{{~#* inline "førSkatt"}}{{#if ytelseMedSkatt}} Dette er før skatt.{{/if}}{{~/inline~}} +{{~#* inline "DuEllerInstitusjonen"~}}{{#if institusjon}}Institusjonen{{else}}Du{{/if}}{{~/inline~}} +{{~#* inline "duEllerinstitusjonen"~}}{{#if institusjon}}institusjonen{{else}}du{{/if}}{{~/inline~}} + +{{#if datoerHvisSammenhengendePeriode.fom}} +{{#if gjelderDødsfall}}Det er utbetalt {{{kroner beløp}}} for mye{{else}}{{> DuEllerInstitusjonen}} har fått {{{kroner beløp}}} for mye utbetalt{{/if}} i {{{ytelsesnavnUbestemt}}} fra og med {{{dato datoerHvisSammenhengendePeriode.fom }}} til og med {{{dato datoerHvisSammenhengendePeriode.tom}}}.{{> førSkatt}} + +{{else}} +{{#if gjelderDødsfall}}Det er utbetalt {{{kroner beløp}}} for mye{{else}}{{> DuEllerInstitusjonen}} har fått {{{kroner beløp}}} for mye utbetalt{{/if}} i {{{ytelsesnavnUbestemt}}}.{{#if ytelseMedSkatt}} Dette er før skatt.{{/if}} + +{{#eq feilutbetaltePerioder.length 1}}Perioden{{else}}Periodene{{/eq}} {{#if gjelderDødsfall}}det er{{else}}{{> duEllerinstitusjonen}} har fått{{/if}} for mye utbetalt for, er: +*-{{#each feilutbetaltePerioder }} +Fra og med {{{dato fom}}} til og med {{{dato tom}}}.{{/each}}-* +{{/if}} +{{> nb/varsel/varsel_slutt}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/varsel_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/varsel_overskrift.hbs new file mode 100644 index 000000000..79774f143 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/varsel_overskrift.hbs @@ -0,0 +1,7 @@ +{{~#* inline "duEllerInstitusjonen"~}}{{#if institusjon}}institusjonen{{else}}du{{/if}}{{~/inline~}} + +{{#if gjelderDødsfall}} +NAV vurderer om feilutbetalt {{{ytelsesnavnUbestemt}}} skal betales tilbake +{{ else }} +NAV vurderer om {{> duEllerInstitusjonen}} må betale tilbake {{{ytelsesnavnUbestemt}}} +{{/if}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/varsel_slutt.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/varsel_slutt.hbs new file mode 100644 index 000000000..fd5251214 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/varsel_slutt.hbs @@ -0,0 +1,61 @@ +{{~#* inline "DuEllerInstitusjonen"~}}{{#if institusjon}}Institusjonen{{else}}Du{{/if}}{{~/inline~}} +{{~#* inline "duEllerinstitusjonen"~}}{{#if institusjon}}institusjonen{{else}}du{{/if}}{{~/inline~}} +{{~#* inline "duEllerDere"~}}{{#if institusjon}}dere{{else}}du{{/if}}{{~/inline~}} + +Før vi avgjør om {{#or gjelderDødsfall institusjon}}pengene skal betales{{else}}du skal betale{{/or}} tilbake, kan {{#or gjelderDødsfall institusjon}}dere uttale dere{{else}}du uttale deg{{/or}} innen {{{dato fristdatoForTilbakemelding}}}. +{{#if ytelseMedSkatt}} + +Hvis {{#or gjelderDødsfall institusjon}}det må betales{{else}}du må betale{{/or}} tilbake, reduserer vi beløpet med trukket skatt. +{{/if}} +_Dette har skjedd +{{#or gjelderDødsfall institusjon}}{{{storForbokstav ytelsesnavnBestemt}}}{{else}}{{{storForbokstav ytelsesnavnEiendomsform}}}{{/or}} ble endret {{{dato revurderingsvedtaksdato}}}, og endringen har ført til at {{#if gjelderDødsfall}}det er{{else}}{{> duEllerinstitusjonen}} har fått{{/if}} utbetalt for mye. +{{#if varseltekstFraSaksbehandler}} + +{{{varseltekstFraSaksbehandler}}} +{{/if}} +_Dette legger vi vekt på i vurderingen vår +For å avgjøre om vi kan kreve tilbake, tar vi først stilling til de {{#if gjelderDødsfall}}to{{else}}tre{{/if}} øverste punktene i denne listen. Hvis resultatet blir at vi kan kreve tilbake, vurderer vi om {{#if (not gjelderDødsfall)}}{{> duEllerDere}} skal betale tilbake {{/if}}hele eller deler av beløpet{{#if gjelderDødsfall}} skal betales tilbake{{/if}}. + +Vi legger vekt på + +*-om {{#or gjelderDødsfall institusjon}}dere{{else}}du{{/or}} forstod eller burde forstått at beløpet {{#if gjelderDødsfall}}som er{{else}}{{> duEllerDere}} fikk{{/if}} utbetalt var feil +{{#if gjelderDødsfall}}om vi har fått informasjon fra dere{{else}}om {{> duEllerDere}} har gitt riktig informasjon til NAV +om {{> duEllerDere}} har gitt all informasjon til NAV i rett tid{{/if}} +hvor lang tid det har gått siden {{{ytelsesnavnBestemt}}} ble feilutbetalt +hvor stort det feilutbetalte beløpet er +om feilen kan skyldes NAV-* + +Selv om det er NAV som er skyld i feilutbetalingen, kan vi likevel kreve at {{#or gjelderDødsfall institusjon}}feilutbetalt beløp betales tilbake{{else}}du betaler tilbake pengene{{/or}}. + +{{#if rentepliktig}} +Hvis {{#or gjelderDødsfall institusjon}}dere{{else}}du{{/or}} må betale tilbake, og {{#or gjelderDødsfall institusjon}}dere{{else}}du{{/or}} har gitt oss feil eller mangelfull informasjon, kan vi kreve at {{#or gjelderDødsfall institusjon}}dere{{else}}du{{/or}} betaler et rentetillegg på ti prosent av beløpet. + +{{/if}} +Dette går fram av {{#if isBarnetrygd}}barnetrygdloven § 13{{else if isKontantstøtte}}kontantstøtteloven § 11{{else}}folketrygdloven §§ 22-15 og 22-17a{{/if}}. +{{#or gjelderDødsfall institusjon}} +_Rett til uttalelse +Dere kan sende uttalelsen til oss i posten. Adressen finner dere på nav.no/ettersendelser. +Dere kan også ta kontakt med oss på nav.no/kontakt. +_Rett til innsyn +Dere kan be om innsyn ved å ta kontakt med oss. +{{else}} +_Slik uttaler du deg +Du kan sende uttalelsen din ved å logge deg inn på nav.no/beskjedtilnav og velge «Send beskjed til NAV». Du kan også sende uttalelsen din til oss i posten. Adressen finner du på +nav.no/ettersendelser. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +{{/or}} +{{> nb/felles/spørsmål_kontaktinformasjon}} + + +Med vennlig hilsen +{{{avsenderenhet}}} + +{{{ansvarligSaksbehandler}}}{{#if brevmetadata.finnesAnnenMottaker}} + +Brev med likt innhold er sendt til {{{annenMottagersNavn}}}{{/if}} +{{#if harVedlegg}} + + +Vedlegg: Oversikt feilutbetalt beløp per måned +{{/if}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/vedlegg.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/vedlegg.hbs new file mode 100644 index 000000000..995a44912 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/varsel/vedlegg.hbs @@ -0,0 +1,27 @@ +

    Feilutbetalt beløp per måned

    + + + + + {{#if ytelseMedSkatt}} + + + + {{else}} + + + + {{/if}} + + + + {{#each feilutbetaltePerioder }} + + + + + + + {{/each}} + +
    PeriodeUtbetalt beløp før skatt er trukket fraNytt beregnet beløp før skatt er trukket fraFeilutbetalt beløp før skatt er trukket fraUtbetalt beløpNytt beregnet beløpFeilutbetalt beløp
    {{{måned måned}}}{{{kroner tidligereUtbetaltBeløp}}}{{{kroner nyttBeløp}}}{{{kroner feilutbetaltBeløp}}}
    \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt.hbs new file mode 100644 index 000000000..2e6215c3e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt.hbs @@ -0,0 +1,3 @@ +{{> nb/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt_start}} + +{{> nb/vedtak/vedtak_slutt}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt_start.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt_start.hbs new file mode 100644 index 000000000..bec306e40 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt_start.hbs @@ -0,0 +1 @@ +{{{fritekstoppsummering}}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-fakta/periode_fakta_ba.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-fakta/periode_fakta_ba.hbs new file mode 100644 index 000000000..0c603162d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-fakta/periode_fakta_ba.hbs @@ -0,0 +1,184 @@ +{{> nb/vedtak/periode-fakta/periode_fakta_felles}} +{{#switch fakta.hendelsestype}} + {{#case "ANNET"}} + {{#switch fakta.hendelsesundertype}} + {{#case "ANNET_FRITEKST"}} + {{/case}} + {{/switch}} + {{/case}} + {{#case "SATSER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "SATSENDRING"}} +Barnetrygden er endret fordi det har vært en satsendring. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "SMÅBARNSTILLEGG"}} + {{#switch fakta.hendelsesundertype}} + {{#case "SMÅBARNSTILLEGG_3_ÅR"}} +Du har fått småbarnstillegg etter at barnet fylte 3 år. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "SMÅBARNSTILLEGG_OVERGANGSSTØNAD"}} +Du har fått småbarnstillegg etter at du ikke lenger hadde full overgangsstønad. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "BOR_MED_SØKER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BOR_IKKE_MED_BARN"}} +Du har fått {{{ytelsesnavnUbestemt}}} for barn som ikke bor fast hos deg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "BOSATT_I_RIKET"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BARN_FLYTTET_FRA_NORGE"}} +Du har fått {{{ytelsesnavnUbestemt}}} for barn som har flyttet fra Norge. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "BRUKER_FLYTTET_FRA_NORGE"}} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du flyttet fra Norge. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden + {{/case}} + {{#case "BARN_BOR_IKKE_I_NORGE"}} +Du har fått {{{ytelsesnavnUbestemt}}} for barn som ikke bodde i Norge. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "BRUKER_BOR_IKKE_I_NORGE"}} +Du har fått {{{ytelsesnavnUbestemt}}} uten at du bodde i Norge. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "BRUKER_OG_BARN_FLYTTET_FRA_NORGE"}} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du og barn flyttet fra Norge. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "BRUKER_OG_BARN_BOR_IKKE_I_NORGE"}} +Du har fått {{{ytelsesnavnUbestemt}}} uten at du og barn bodde i Norge. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "LOVLIG_OPPHOLD"}} + {{#switch fakta.hendelsesundertype}} + {{#case "UTEN_OPPHOLDSTILLATELSE"}} + Du har fått {{{ytelsesnavnUbestemt}}} uten at du hadde oppholdstillatelse i Norge. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "DØDSFALL"}} + {{> fakta-død}} + {{/case}} + {{#case "DELT_BOSTED"}} + {{#switch fakta.hendelsesundertype}} + {{#case "ENIGHET_OM_OPPHØR_DELT_BOSTED"}} +Du har fått {{{ytelsesnavnUbestemt}}} etter at avtalen om delt bosted er opphørt. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "UENIGHET_OM_OPPHØR_DELT_BOSTED"}} +Du har fått {{{ytelsesnavnUbestemt}}} etter at avtalen om delt bosted ikke lenger praktiseres. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "FLYTTET_SAMMEN"}} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du flyttet sammen med den andre forelderen. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "BARNS_ALDER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BARN_OVER_18_ÅR"}} +Du har fått {{{ytelsesnavnUbestemt}}} etter at barn fylte 18 år. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "BARN_OVER_6_ÅR"}} +Du har fått {{{ytelsesnavnUbestemt}}} med feil beløp etter at barn har fylt 6 år. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "MEDLEMSKAP_BA"}} + {{#switch fakta.hendelsesundertype}} + {{#case "UTENLANDS_IKKE_MEDLEM"}} + Du har fått {{{ytelsesnavnUbestemt}}} etter at du og barn flyttet til utlandet. Dere er ikke lenger medlem i folketrygden under oppholdet i utlandet. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "MEDLEMSKAP_OPPHØRT"}} + Du har fått {{{ytelsesnavnUbestemt}}} etter at du og barn ikke lenger er medlem i folketrygden under oppholdet i utlandet. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "ANNEN_FORELDER_IKKE_MEDLEM"}} + Du har fått {{{ytelsesnavnUbestemt}}} etter at du og barn flyttet til utlandet. Begge foreldrene må være medlem i folketrygden for å få barnetrygd. Den andre forelderen er ikke medlem i folketrygden. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "ANNEN_FORELDER_OPPHØRT_MEDLEMSKAP"}} + Du har fått {{{ytelsesnavnUbestemt}}} etter at du og barn flyttet til utlandet. Begge foreldrene må være medlem i folketrygden for å få barnetrygd. Den andre forelderen er ikke lenger medlem i folketrygden. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "FLERE_UTENLANDSOPPHOLD"}} + Du er ikke medlem i folketrygden fordi du har oppholdt deg så mye i utlandet at du ikke regnes som bosatt i Norge. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "BOSATT_IKKE_MEDLEM"}} + Du har fått {{{ytelsesnavnUbestemt}}} uten at du er medlem i folketrygden. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "UTVIDET"}} + {{#switch fakta.hendelsesundertype}} + {{#case "GIFT"}} + Du har fått utvidet {{{ytelsesnavnUbestemt}}} etter at du giftet deg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "NYTT_BARN"}} + Du har fått utvidet {{{ytelsesnavnUbestemt}}} etter at du har fått nytt barn. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "SAMBOER_12_MÅNEDER"}} + Du har fått utvidet {{{ytelsesnavnUbestemt}}} etter at du har vært samboer i mer enn 12 måneder. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "FLYTTET_SAMMEN_ANNEN_FORELDER"}} + Du har fått utvidet {{{ytelsesnavnUbestemt}}} etter at du flyttet sammen med den andre forelderen. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "FLYTTET_SAMMEN_EKTEFELLE"}} + Du har fått utvidet {{{ytelsesnavnUbestemt}}} etter at du flyttet sammen med ektefellen din. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "FLYTTET_SAMMEN_SAMBOER"}} + Du har fått utvidet {{{ytelsesnavnUbestemt}}} etter at du flyttet sammen med samboeren din. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "GIFT_IKKE_EGEN_HUSHOLDNING"}} + Du har fått utvidet {{{ytelsesnavnUbestemt}}} selv om du regnes som gift fordi du ikke har egen husholdning. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "SAMBOER_IKKE_EGEN_HUSHOLDNING"}} + Du har fått utvidet {{{ytelsesnavnUbestemt}}} selv om du regnes som samboer fordi du ikke har egen husholdning. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "EKTEFELLE_AVSLUTTET_SONING"}} + Du har fått utvidet {{{ytelsesnavnUbestemt}}} etter at ektefellen din avsluttet soningen. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "SAMBOER_AVSLUTTET_SONING"}} + Du har fått utvidet {{{ytelsesnavnUbestemt}}} etter at samboeren din avsluttet soningen. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "EKTEFELLE_INSTITUSJON"}} + Du har fått utvidet {{{ytelsesnavnUbestemt}}} etter at ektefellen din avsluttet oppholdet i institusjon. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "SAMBOER_INSTITUSJON"}} + Du har fått utvidet {{{ytelsesnavnUbestemt}}} etter at samboeren din avsluttet oppholdet i institusjon. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} +{{/switch}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-fakta/periode_fakta_ef.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-fakta/periode_fakta_ef.hbs new file mode 100644 index 000000000..6afd34d40 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-fakta/periode_fakta_ef.hbs @@ -0,0 +1,265 @@ +{{> nb/vedtak/periode-fakta/periode_fakta_felles}} +{{#switch fakta.hendelsestype}} + {{#case "ANNET"}} + {{#switch fakta.hendelsesundertype}} + {{#case "ANNET_FRITEKST"}} + {{/case}} + {{/switch}} + {{/case}} + {{#case "DØDSFALL"}} + {{> fakta-død}} + {{/case}} + {{#case "MEDLEMSKAP"}} + {{#switch fakta.hendelsesundertype}} + {{#case "MEDLEM_SISTE_5_ÅR" }} +Du har ikke vært medlem i folketrygden i de siste 5 årene før du søkte om {{{ytelsesnavnUbestemt}}}. Derfor har du ikke rett til stønaden. + +Du har fått {{kroner kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt. + {{/case}} + {{#case "LOVLIG_OPPHOLD" }} +Du er ikke medlem av folketrygden fordi du ikke har oppholdstillatelse i Norge. Derfor har du ikke rett til {{{ytelsesnavnUbestemt}}}. + +Du har fått {{kroner kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt. + {{/case}} + {{/switch}} + {{/case}} + {{#case "OPPHOLD_I_NORGE"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BRUKER_IKKE_OPPHOLD_I_NORGE" }} +Du har fått {{{ytelsesnavnUbestemt}}} uten at du har oppholdt deg i Norge. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "BARN_IKKE_OPPHOLD_I_NORGE" }} +Du har fått {{{ytelsesnavnUbestemt}}} for barn som ikke har oppholdt seg i Norge. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "BRUKER_FLYTTET_FRA_NORGE" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du flyttet fra Norge. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "BARN_FLYTTET_FRA_NORGE" }} +Du har fått {{{ytelsesnavnUbestemt}}} for barn som har flyttet fra Norge. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "OPPHOLD_UTLAND_6_UKER_ELLER_MER" }} +Du har oppholdt deg i utlandet i mer enn 6 uker i løpet av de siste 12 måneder. Derfor har du ikke rett til {{{ytelsesnavnUbestemt}}}. + +Du har fått {{kroner kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt. + {{/case}} + {{/switch}} + {{/case}} + {{#case "ENSLIG_FORSØRGER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "UGIFT" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du giftet deg. Du har ikke rett på {{{ytelsesnavnUbestemt}}} når du er gift. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "SEPARERT_SKILT" }} +Du har fått {{{ytelsesnavnUbestemt}}} uten at det foreligger en formell separasjon eller skilsmisse. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "SAMBOER" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du ble samboer. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "NYTT_BARN_SAMME_PARTNER" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du fikk nytt barn med samme partner. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "ENDRET_SAMVÆRSORDNING" }} +Du har mottatt {{{ytelsesnavnUbestemt}}} etter at samværsordningen er endret slik at du ikke lenger har klart mer av den daglige omsorgen. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "BARN_FLYTTET" }} +Du har fått {{{ytelsesnavnUbestemt}}} for barn som ikke bor fast hos deg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "NÆRE_BOFORHOLD" }} +Du og den andre av barnets foreldre bor nære hverandre og du har ikke klart mer av den daglige omsorgen. Derfor har du ikke rett til {{{ytelsesnavnBestemt}}}. + +Du har fått {{kroner kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt. + {{/case}} + {{#case "FORELDRE_LEVER_SAMMEN" }} +Du har fått {{{ytelsesnavnUbestemt}}} samtidig som du lever sammen med den andre av barnets foreldre. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "OVERGANGSSTØNAD"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BARN_8_ÅR" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at ditt yngste barn fylte åtte år. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "YRKESRETTET_AKTIVITET"}} + {{#switch fakta.hendelsesundertype}} + {{#case "ARBEID" }} +Du har fått {{{ytelsesnavnUbestemt}}} uten at du har vært i arbeid som utgjør minst halvparten av full tid. + +Derfor har du fått {{kroner kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "REELL_ARBEIDSSØKER" }} +Du har fått {{{ytelsesnavnUbestemt}}} som reell arbeidssøker. Du har ikke stått tilmeldt NAV eller sendt meldekort til rett tid. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "UTDANNING" }} +Du har fått {{{ytelsesnavnUbestemt}}} uten å ha vært i utdanning som utgjør minst halvparten av full tid. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "ETABLERER_EGEN_VIRKSOMHET" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du avsluttet etableringen av egen virksomhet. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "BARN_FYLT_1_ÅR" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at barnet ditt fylte 1 år uten at du har fylt aktivitetsplikten. Derfor har du fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "STØNADSPERIODE"}} + {{#switch fakta.hendelsesundertype}} + {{#case "HOVEDPERIODE_3_ÅR" }} +Du har fått {{{ytelsesnavnUbestemt}}} i mer enn tre år og fyller ikke vilkårene for utvidelse av stønadsperioden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "UTVIDELSE_UTDANNING" }} +Du har fått {{{ytelsesnavnUbestemt}}} uten å ha vært i utdanning som utgjør minst halvparten av full tid. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "UTVIDELSE_SÆRLIG_TILSYNSKREVENDE_BARN" }} +Du har fått forlenget {{{ytelsesnavnUbestemt}}} fordi du har et særlig tilsynskrevende barn som hindret deg fra å være i arbeid. Du er ikke lenger forhindret fra å arbeide. Derfor har du ikke rett til {{{ytelsesnavnUbestemt}}}. + +Du har fått {{kroner kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt. + {{/case}} + {{#case "UTVIDELSE_FORBIGÅENDE_SYKDOM" }} +Du har fått forlenget {{{ytelsesnavnUbestemt}}} uten at du eller barnet hadde en forbigående sykdom som hindret deg i å arbeide. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "PÅVENTE_AV_SKOLESTART_STARTET_IKKE" }} +Du har fått forlenget {{{ytelsesnavnUbestemt}}} fordi du skulle starte på skole. Fordi du ikke startet på skolen, har du ikke rett på stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "PÅVENTE_SKOLESTART_STARTET_TIDLIGERE" }} +Du har fått forlenget {{{ytelsesnavnUbestemt}}} fordi du skulle starte på skole. Du har startet på skole og har ikke lenger rett til stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "PÅVENTE_ARBEIDSTILBUD_STARTET_IKKE" }} +Du har fått forlenget {{{ytelsesnavnUbestemt}}} fordi du ventet på å begynne i en konkret jobb. Du skal ikke lenger starte i denne jobben. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "PÅVENTE_ARBEIDSTILBUD_STARTET_TIDLIGERE" }} +Du har fått forlenget {{{ytelsesnavnUbestemt}}} fordi du ventet på å begynne i en konkret jobb. Du har begynt å arbeide. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "PÅVENTE_BARNETILSYN_IKKE_HA_TILSYN" }} +Du har fått forlenget {{{ytelsesnavnUbestemt}}} fordi du ventet på barnetilsyn. Fordi barnet ikke lenger skal ha en tilsynsordning, har du ikke rett til stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "PÅVENTE_BARNETILSYN_STARTET_TIDLIGERE" }} +Du har fått forlenget {{{ytelsesnavnUbestemt}}} fordi du ventet på barnetilsyn. Fordi barnet har startet i en tilsynsordning, har du ikke lenger rett på stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "ARBEIDSSØKER" }} +Du har fått {{{ytelsesnavnUbestemt}}} fordi du meldte deg som arbeidssøker. Fordi du ikke er reell arbeidssøker lenger, har du ikke rett på stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "INNTEKT"}} + {{#switch fakta.hendelsesundertype}} + {{#case "ARBEIDSINNTEKT_FÅTT_INNTEKT" }} +Du har hatt arbeidsinntekt uten at {{{ytelsesnavnBestemt}}} har blitt redusert. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "ARBEIDSINNTEKT_ENDRET_INNTEKT" }} +Du har fått {{{ytelsesnavnBestemt}}} redusert etter arbeidsinntekt. Arbeidsinntekten din har økt med 10 prosent eller mer. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "ANDRE_FOLKETRYGDYTELSER" }} +Du har mottatt andre folketrygdytelser som regnes som arbeidsinntekt, uten at {{{ytelsesnavnBestemt}}} har blitt redusert. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "SELVSTENDIG_NÆRINGSDRIVENDE_FÅTT_INNTEKT" }} +Du har hatt næringsinntekt uten at {{{ytelsesnavnBestemt}}} har blitt redusert. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "SELVSTENDIG_NÆRINGSDRIVENDE_ENDRET_INNTEKT" }} +Du har fått {{{ytelsesnavnUbestemt}}} redusert etter næringsinntekt. Næringsinntekten din har økt med 10 prosent eller mer. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "PENSJONSYTELSER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "UFØRETRYGD" }} +Du har fått uføretrygd som skulle ha redusert {{{ytelsesnavnBestemt}}}. Fordi stønaden ikke har blitt redusert, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "GJENLEVENDE_EKTEFELLE" }} +Du har fått pensjon som gjenlevende ektefelle som skulle ha redusert {{{ytelsesnavnBestemt}}}. Fordi stønaden ikke har blitt redusert, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "STØNAD_TIL_BARNETILSYN"}} + {{#switch fakta.hendelsesundertype}} + {{#case "IKKE_ARBEID" }} +Du har fått stønad til barnetilsyn. Fordi du ikke lenger er i arbeid, har du ikke rett til stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "EGEN_VIRKSOMHET" }} +Du har fått stønad til barnetilsyn. Fordi du har avsluttet etableringen av egen virksomhet, har du ikke rett på stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "TILSYNSUTGIFTER_OPPHØRT" }} +Du har fått stønad til barnetilsyn etter at tilsynsordningen er avsluttet. Fordi du ikke lenger har utgifter til barnetilsyn, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "TILSYNSUTGIFTER_ENDRET" }} +Du har fått stønad til barnetilsyn. Fordi utgiftene til barnetilsyn er redusert, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "FORBIGÅENDE_SYKDOM" }} +Du har fått stønad til barnetilsyn fordi du hadde en forbigående sykdom som hindret deg fra å arbeide. Fordi du ikke lenger er hindret fra å arbeide, har du ikke rett til stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "ETTER_4_SKOLEÅR_UTGIFTENE_OPPHØRT" }} +Du har fått stønad til barnetilsyn for barn som har fullført fjerde skoleår. Fordi du ikke lenger har utgifter til tilsyn av barn, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "ETTER_4_SKOLEÅR_ENDRET_ARBEIDSTID" }} +Du har fått stønad til barnetilsyn for barn som har fullført fjerde skoleår. Fordi du ikke lenger har tilsynsbehov for barnet ut over en vanlig arbeidsdag, har du ikke rett til stønad til barnetilsyn. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "INNTEKT_OVER_6G" }} +Du har fått stønad til barnetilsyn. Fordi inntekten din er over {{#if grunnbeløp.grunnbeløpGanger6}}{{kroner grunnbeløp.grunnbeløpGanger6}}{{else}}seks ganger grunnbeløpet{{/if}}, har du ikke lenger rett til stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. +{{#if grunnbeløp.tekst6GangerGrunnbeløp}} + +Seks ganger grunnbeløpet er {{grunnbeløp.tekst6GangerGrunnbeløp}}. +{{/if}} + {{/case}} + {{#case "KONTANTSTØTTE" }} +Du har fått stønad til barnetilsyn og kontantstøtte. Kontantstøtte skal trekkes fra tilsynsutgiftene før stønad til barnetilsyn beregnes. Fordi stønaden ikke er blitt redusert med kontantstøtten, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "ØKT_KONTANTSTØTTE" }} +Du har fått stønad til barnetilsyn og kontantstøtte. Kontantstøtte skal trekkes fra tilsynsutgiftene før stønad til barnetilsyn beregnes. Fordi stønaden ikke har blitt redusert etter at kontantstøtten økte, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "SKOLEPENGER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "IKKE_RETT_TIL_OVERGANGSSTØNAD" }} +Du har fått skolepenger. Du kan kun få skolepenger dersom du har rett til overgangsstønad. Da du ikke lenger har rett til overgangsstønad, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{#case "SLUTTET_I_UTDANNING" }} +Du har fått stønad til dekning av skolepenger etter at du sluttet på utdanningen. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} +{{/switch}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-fakta/periode_fakta_felles.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-fakta/periode_fakta_felles.hbs new file mode 100644 index 000000000..a03eceb56 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-fakta/periode_fakta_felles.hbs @@ -0,0 +1,18 @@ +{{#* inline "fakta-død"}} + {{#if førstePeriode}} + {{#switch fakta.hendelsesundertype}} + {{#case "BARN_DØD"}} +Vi har fått melding om at barnet ditt døde. {{storForbokstav + ytelsesnavnBestemt}} skulle vært stanset fra og med {{{dato opphørsdatoDødtBarn}}}. + {{/case}} + {{#case "BRUKER_DØD"}} +Vi har fått melding om at {{{søker.navn}}} døde. {{storForbokstav + ytelsesnavnBestemt}} skulle vært stanset fra og med {{{dato opphørsdatoDødSøker}}}. + {{/case}} + {{/switch}} + +Fordi {{{ytelsesnavnBestemt}}} er utbetalt etter denne datoen er det utbetalt {{kroner kravgrunnlag.feilutbetaltBeløp}} for mye. + {{else}} +Det er utbetalt {{kroner kravgrunnlag.feilutbetaltBeløp}} for mye for denne perioden. + {{/if}} +{{/inline}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-fakta/periode_fakta_ks.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-fakta/periode_fakta_ks.hbs new file mode 100644 index 000000000..4d0a4fc61 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-fakta/periode_fakta_ks.hbs @@ -0,0 +1,192 @@ +{{> nb/vedtak/periode-fakta/periode_fakta_felles}} +{{#switch fakta.hendelsestype}} + {{#case "VILKÅR_BARN"}} + {{#switch fakta.hendelsesundertype}} + {{#case "FULLTIDSPLASS_BARNEHAGE"}} +Du har fått kontantstøtte etter at barnet var tildelt fulltidsplass i barnehage. Når barnet er tildelt barnehageplass med 33 timer eller mer i uka, regnes dette som fulltidsplass. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "DELTIDSPLASS_BARNEHAGEPLASS"}} +Du har fått kontantstøtte etter at barnet var tildelt deltidsplass i barnehage. Når barnet er tildelt barnehageplass med mindre enn 33 timer i uka, regnes dette som deltidsplass. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "BARN_IKKE_BOSATT"}} +Du har fått kontantstøtte selv om barn ikke var bosatt i Norge. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "BARN_IKKE_OPPHOLDSTILLATELSE"}} +Du har fått kontantstøtte for barn som ikke hadde oppholdstillatelse i Norge i mer enn 12 måneder. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "BARN_FLYTTET_FRA_NORGE"}} +Du har fått kontantstøtte for barn som har flyttet fra Norge. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "BARN_OVER_2_ÅR"}} +Du har fått kontantstøtte etter at barn har fylt 2 år. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "VILKÅR_SØKER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "DEN_ANDRE_FORELDEREN_IKKE_MEDLEM_FOLKETRYGDEN"}} +Den andre forelderen har ikke vært medlem i folketrygden i 5 år. Begge foreldrene må ha vært medlem i folketrygden i 5 år for å få kontantstøtte. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "DEN_ANDRE_FORELDEREN_IKKE_MEDLEM_FOLKETRYGDEN_ELLER_EØS"}} +Den andre forelderen har ikke vært medlem i folketrygden eller i trygdeordninger i andre EØS-land i til sammen i 5 år. Begge foreldrene må fylle kravet om medlemskap for å få kontantstøtte. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "SØKER_IKKE_MEDLEM_FOLKETRYGDEN"}} +Du har ikke vært medlem i folketrygden i 5 år og har derfor ikke rett til kontantstøtte. + +Du har fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt. + {{/case}} + {{#case "SØKER_IKKE_MEDLEM_FOLKETRYGDEN_ELLER_EØS"}} +Du har ikke vært medlem i folketrygden eller i trygdeordninger i andre EØS-land i til sammen 5 år. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "BEGGE_FORELDRENE_IKKE_MEDLEM_FOLKETRYGDEN"}} +Du og den andre forelderen har ikke vært medlem i folketrygden i 5 år. Begge foreldrene må ha vært medlem i folketrygden i 5 år for å få kontantstøtte. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "BEGGE_FORELDRENE_IKKE_MEDLEM_FOLKETRYGDEN_ELLER_EØS"}} +Du og den andre forelderen har ikke vært medlem i folketrygden eller i trygdeordninger i andre EØS-land i til sammen 5 år. Begge foreldrene må fylle kravet om medlemskap for å få kontantstøtte. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "BARN_BOR_IKKE_HOS_SØKER"}} +Du har fått kontantstøtte for barn som ikke bor fast hos deg. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "UTENLANDSOPPHOLD_OVER_3_MÅNEDER"}} +Du har fått kontantstøtte når du har vært i utlandet. Når opphold i utlandet er mer enn 3 måneder, har du ikke rett til kontantstøtte. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "SØKER_FLYTTET_FRA_NORGE"}} +Du har fått kontantstøtte etter at du flyttet fra Norge. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden + {{/case}} + {{#case "SØKER_IKKE_BOSATT"}} +Du har fått kontantstøtte selv om du ikke var bosatt i Norge. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "SØKER_IKKE_OPPHOLDSTILLATELSE"}} +Du har fått kontantstøtte uten at du hadde oppholdstillatelse i Norge. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "SØKER_IKKE_OPPHOLDSTILLATELSE_I_MER_ENN_12_MÅNEDER"}} +Du har fått kontantstøtte selv om du ikke hadde oppholdstillatelse i Norge i mer enn 12 måneder. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "BARN_I_FOSTERHJEM_ELLER_INSTITUSJON"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BARN_I_FOSTERHJEM"}} +Du har fått kontantstøtte når barnet bodde i fosterhjem. + +Derfor har du fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "BARN_I_INSTITUSJON"}} +Du har fått kontantstøtte når barnet bodde i institusjon. + +Derfor har du fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "KONTANTSTØTTENS_STØRRELSE"}} + {{#switch fakta.hendelsesundertype}} + {{#case "FULLTIDSPLASS_BARNEHAGE"}} +Du har fått kontantstøtte etter at barnet var tildelt fulltidsplass i barnehage. Når barnet er tildelt barnehageplass med 33 timer eller mer i uka, regnes dette som fulltidsplass. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "DELTIDSPLASS_BARNEHAGEPLASS"}} +Du har fått kontantstøtte etter at barnet var tildelt deltidsplass i barnehage. Når barnet er tildelt barnehageplass med mindre enn 33 timer i uka, regnes dette som deltidsplass. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "ØKT_TIMEANTALL_I_BARNEHAGE"}} +Du har fått kontantstøtte etter at barnet var tildelt flere timer i barnehage. Når barnet er tildelt mer oppholdstid i uka enn tidligere, får du mindre i kontantstøtte. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "SATSENDRING"}} +Du har fått kontantstøtte med feil beløp fordi det har vært en satsendring. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "STØTTEPERIODE"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BARN_2_ÅR"}} +Du har fått kontantstøtte etter at barn har fylt 2 år. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "UTBETALING"}} + {{#switch fakta.hendelsesundertype}} + {{#case "DELT_BOSTED_AVTALE_OPPHØRT"}} +Du har fått kontantstøtte etter at avtalen om delt bosted er opphørt. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "DOBBELUTBETALING"}} +Du har fått kontantstøtte for samme barn og i samme tidsrom som den andre forelderen. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "KONTANTSTØTTE_FOR_ADOPTERTE_BARN"}} + {{#switch fakta.hendelsesundertype}} + {{#case "MER_ENN_11_MÅNEDER"}} +Du har fått kontantstøtte i mer enn 11 måneder. Kontantstøtte for adopterte barn kan maksimalt gis for 11 måneder. + +Derfor har du fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "BARN_STARTET_PÅ_SKOLEN"}} +Du har fått kontantstøtte etter at barn har begynt på skolen. Kontantstøtte for adopterte barn kan gis fram til barnet begynner på skolen. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "ANNET_KS"}} + {{#switch fakta.hendelsesundertype}} + {{#case "ANNET_FRITEKST"}} + {{/case}} + {{#case "BARN_DØD"}} +Vi har fått melding om at barnet ditt døde. Kontantstøtten skulle vært stanset fra og med {{{dato opphørsdatoDødtBarn}}}. Kontantstøtten er utbetalt etter denne datoen. + +Det er derfor utbetalt {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye + {{/case}} + {{#case "BRUKER_DØD"}} +Vi har fått melding om at barnet ditt døde. Kontantstøtten skulle vært stanset fra og med {{{dato opphørsdatoDødtBarn}}}. Kontantstøtten er utbetalt etter denne datoen. + +Det er derfor utbetalt {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye + {{/case}} + {{/switch}} + {{/case}} +{{/switch}} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-s\303\246rlige-grunner/periode_s\303\246rlige_grunner_feilaktig_mangelfull.hbs" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-s\303\246rlige-grunner/periode_s\303\246rlige_grunner_feilaktig_mangelfull.hbs" new file mode 100644 index 000000000..5d6cb2e40 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-s\303\246rlige-grunner/periode_s\303\246rlige_grunner_feilaktig_mangelfull.hbs" @@ -0,0 +1,126 @@ +{{#switch navfeil=vurderinger.særligeGrunner.navfeil størrelse=vurderinger.særligeGrunner.størrelse tid=vurderinger.særligeGrunner.tid reduksjon=(neq kravgrunnlag.feilutbetaltBeløp resultat.tilbakekrevesBeløp) annet=vurderinger.særligeGrunner.annet}} + {{#case navfeil=false størrelse=false tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Det er lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=false reduksjon=true annet=false}} + {{#if (eq vurderinger.aktsomhetsresultat "SIMPEL_UAKTSOMHET")}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi vurderer likevel at uaktsomheten din har vært så liten at vi har redusert beløpet du må betale tilbake. + {{/if}} + {{/case}} + {{#case navfeil=false størrelse=false tid=false reduksjon=true annet=true}} + {{#if (eq vurderinger.aktsomhetsresultat "GROV_UAKTSOMHET")}} +Vi har lagt vekt på at du må ha forstått at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Det er i vurderingen lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{else}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi vurderer likevel at uaktsomheten din har vært så liten at vi har redusert beløpet du må betale tilbake. Det er i vurderingen også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/if}} + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok og at vi ikke kunne unngått å betale ut feil beløp. Du må derfor betale hele beløpet tilbake. + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok og at vi ikke kunne unngått å betale ut feil beløp. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=true annet=false}} + {{#if (eq vurderinger.aktsomhetsresultat "SIMPEL_UAKTSOMHET")}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi vurderer likevel at uaktsomheten din har vært så liten at vi har redusert beløpet du må betale tilbake. + {{/if}} + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=true annet=true}} + {{#if (eq vurderinger.aktsomhetsresultat "GROV_UAKTSOMHET")}} +Vi har lagt vekt på at du må ha forstått at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Det er i vurderingen lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{else}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi vurderer likevel at uaktsomheten din har vært så liten at vi har redusert beløpet du må betale tilbake. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/if}} + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen og at beløpet er høyt. Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen og at beløpet er høyt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=true annet=false}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake, fordi det feilutbetalte beløpet er lavt. + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=true annet=true}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake, fordi det feilutbetalte beløpet er lavt og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen og at det er kort tid siden feilutbetalingen skjedde. Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen og at det er kort tid siden feilutbetalingen skjedde. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake fordi det er lang tid siden feilutbetalingen skjedde. + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake fordi det er lang tid siden feilutbetalingen skjedde og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen og at beløpet er høyt. Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen og at beløpet er høyt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=true annet=false}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake, fordi det feilutbetalte beløpet er lavt. + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=true annet=true}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake, fordi det feilutbetalte beløpet er lavt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Det er også kort tid siden utbetalingen skjedde og beløpet er høyt. Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Det er også kort tid siden utbetalingen skjedde og beløpet er høyt og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake fordi det har gått lang tid siden feilutbetalingen skjedde og beløpet er lavt. + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake fordi det har gått lang tid siden feilutbetalingen skjedde og beløpet er lavt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Det er også kort tid siden utbetalingen skjedde og beløpet er høyt. Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Det er også kort tid siden utbetalingen skjedde og beløpet er høyt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake fordi det har gått lang tid siden feilutbetalingen skjedde og beløpet er lavt. + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake fordi det har gått lang tid siden feilutbetalingen skjedde og beløpet er lavt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen og at det er kort tid siden feilutbetalingen skjedde. Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen og at det er kort tid siden feilutbetalingen skjedde og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake fordi det er lang tid siden feilutbetalingen skjedde. + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake fordi det er lang tid siden feilutbetalingen skjedde. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} +{{/switch}} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-s\303\246rlige-grunner/periode_s\303\246rlige_grunner_forstod.hbs" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-s\303\246rlige-grunner/periode_s\303\246rlige_grunner_forstod.hbs" new file mode 100644 index 000000000..9ceda3067 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode-s\303\246rlige-grunner/periode_s\303\246rlige_grunner_forstod.hbs" @@ -0,0 +1,114 @@ +{{#switch navfeil=vurderinger.særligeGrunner.navfeil størrelse=vurderinger.særligeGrunner.størrelse tid=vurderinger.særligeGrunner.tid reduksjon=(neq kravgrunnlag.feilutbetaltBeløp resultat.tilbakekrevesBeløp) annet=vurderinger.særligeGrunner.annet}} + {{#case navfeil=false størrelse=false tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at beløpet du fikk utbetalt var feil, og det er ingen bestemte grunner til å redusere beløpet. Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=false reduksjon=false annet=true}} +Selv om du {{> skulle-forstått}} at du fikk for mye utbetalt, har vi vurdert om det er grunner til å redusere beløpet. Det er lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Vi har kommet fram til at du må betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=false reduksjon=true annet=false}} +Vi har lagt vekt på at feilutbetalingen ikke var tydelig nok, selv om du {{> skulle-forstått}} at du fikk for mye utbetalt. Derfor har vi redusert beløpet du må betale tilbake. + {{/case}} + {{#case navfeil=false størrelse=false tid=false reduksjon=true annet=true}} +Vi har lagt vekt på at feilutbetalingen ikke var tydelig nok, selv om du {{> skulle-forstått}} at du fikk for mye utbetalt. Derfor har vi redusert beløpet du må betale tilbake. Det er i vurderingen også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Selv om vi kunne unngått at du fikk for mye utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at beløpet var feil. Du må derfor betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Selv om vi kunne unngått at du fikk for mye utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at beløpet var feil. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Du må derfor betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi vi kunne unngått at du fikk utbetalt for mye. + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi vi kunne unngått at du fikk utbetalt for mye. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til og at det feilutbetalte beløpet er høyt. Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til og at det feilutbetalte beløpet er høyt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Fordi det feilutbetalte beløpet er lavt, har vi redusert beløpet du må betale tilbake. + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi beløpet er lavt og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til og at det er kort tid siden feilutbetalingen skjedde. Du må derfor betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til og at det er kort tid siden feilutbetalingen skjedde. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du derfor betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Fordi det er lenge siden feilutbetalingen skjedde, har vi redusert beløpet du må betale tilbake. + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi det er lenge siden feilutbetalingen skjedde og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Selv om vi kunne unngått at du fikk for mye utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til og at det feilutbetalte beløpet er høyt. Du må derfor betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Selv om vi kunne unngått at du fikk for mye utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til og at det feilutbetalte beløpet er høyt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Du må derfor betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi vi kunne unngått at du fikk utbetalt for mye og beløpet er lavt. + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi vi kunne unngått at du fikk utbetalt for mye og beløpet er lavt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Selv om vi kunne unngått at du fikk for mye utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har også lagt vekt på at det feilutbetalte beløpet er høyt og at det er kort tid siden utbetalingen skjedde. Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Selv om vi kunne unngått at du fikk for mye utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til og at det feilutbetalte beløpet er høyt. Vi har også lagt vekt på at det er kort tid siden utbetalingen skjedde og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi vi kunne unngått at du fikk utbetalt for mye, beløpet er lavt og det er lenge siden feilutbetalingen skjedde. + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi vi kunne unngått at du fikk utbetalt for mye, beløpet er lavt og det er lenge siden feilutbetalingen skjedde. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at beløpet var feil. Vi har også lagt vekt på at det feilutbetalte beløpet er høyt og at det er kort tid siden utbetalingen skjedde. Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at beløpet var feil og at det feilutbetalte beløpet er høyt. Vi har også lagt vekt på at det er kort tid siden utbetalingen skjedde og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi beløpet er lavt og det er lenge siden feilutbetalingen skjedde. + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi beløpet er lavt og det er lenge siden feilutbetalingen skjedde. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunner til å redusere beløpet. Selv om vi kunne unngått at du fikk for mye utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har også lagt vekt på at det er kort tid siden utbetalingen skjedde. Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunner til å redusere beløpet. Selv om vi kunne unngått at du fikk for mye utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har også lagt vekt på at det er kort tid siden utbetalingen skjedde og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake hele beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi vi kunne unngått at du fikk utbetalt for mye og det er lenge siden feilutbetalingen skjedde. + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fikk penger du ikke har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi vi kunne unngått at du fikk utbetalt for mye og det er lenge siden feilutbetalingen skjedde. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} +{{/switch}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_fakta.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_fakta.hbs new file mode 100644 index 000000000..cd943db05 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_fakta.hbs @@ -0,0 +1,27 @@ +{{#switch brevmetadata.ytelsestype}} +{{#case "BARNETRYGD"}} +{{> nb/vedtak/periode-fakta/periode_fakta_ba}} +{{/case}} +{{#case "OVERGANGSSTØNAD"}} +{{> nb/vedtak/periode-fakta/periode_fakta_ef}} +{{/case}} +{{#case "KONTANTSTØTTE"}} +{{> nb/vedtak/periode-fakta/periode_fakta_ks}} +{{/case}} +{{#case "SKOLEPENGER"}} +{{> nb/vedtak/periode-fakta/periode_fakta_ef}} +{{/case}} +{{#case "BARNETILSYN"}} +{{> nb/vedtak/periode-fakta/periode_fakta_ef}} +{{/case}} +{{/switch}} +{{#if fakta.fritekstFakta}} + {{#if (eq fakta.hendelsesundertype "ANNET_FRITEKST")}} +{{{fakta.fritekstFakta}}} + +{{#if gjelderDødsfall}}Det er utbetalt {{{kroner kravgrunnlag.feilutbetaltBeløp}}} for mye.{{else}}Du har fått {{{kroner kravgrunnlag.feilutbetaltBeløp}}} for mye utbetalt.{{/if}} + {{else}} + +{{{fakta.fritekstFakta}}} + {{/if}} +{{/if}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_foreldelse.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_foreldelse.hbs new file mode 100644 index 000000000..aa4422a20 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_foreldelse.hbs @@ -0,0 +1,33 @@ +{{#* inline "fritekstForeldelse"}} + {{#if vurderinger.fritekstForeldelse}} + +{{{vurderinger.fritekstForeldelse}}} + {{/if}} +{{/inline}} +{{#if vurderinger.harForeldelsesavsnitt }} +__Hvordan har vi kommet fram til at du {{#if resultat.tilbakekrevesBeløp}}{{else}}ikke {{/if}}må betale tilbake? +{{/if}} +{{#switch vurderinger.foreldelsevurdering}} + {{#case "FORELDET"}} +Fristen for å kreve tilbake {{{ytelsesnavnBestemt}}} er tre år og starter fra det tidspunktet feilutbetalingen skjedde. + +Foreldelsesfristen er {{{dato vurderinger.foreldelsesfrist}}}. Vi kan ikke kreve tilbake penger som er blitt utbetalt mer enn tre år før denne datoen. + +Du skal derfor ikke betale tilbake {{{kroner resultat.foreldetBeløp}}}{{> før-skatt}}, som er feilutbetalt fra og med {{{dato periode.fom}}} til og med {{{dato periode.tom}}}. +{{> fritekstForeldelse}} + + {{/case}} + {{#case "TILLEGGSFRIST"}} +Fristen for å kreve tilbake {{{ytelsesnavnBestemt}}} er tre år fra det tidspunktet feilutbetalingen skjedde. Vi kan utvide fristen med ytterligere 10 år hvis det ikke var mulig for NAV å oppdage feilen tidligere. Fra tidspunktet vi oppdaget feilen, har vi ett år på å kreve pengene tilbake. +{{> fritekstForeldelse}} + +Vi har oppdaget feilutbetalingen {{{dato vurderinger.oppdagelsesdato}}}, og har behandlet saken din innen ett år. Derfor må du betale tilbake {{{ytelsesnavnBestemt}}} du har fått fra og med {{{dato periode.fom}}} til og med {{{dato periode.tom}}}. + + {{/case}} + {{#case "IKKE_FORELDET"}} + {{! ikke noe tekst her}} + {{/case}} + {{#case "IKKE_VURDERT"}} + {{! ikke noe tekst her}} + {{/case}} +{{/switch}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_overskrift.hbs new file mode 100644 index 000000000..0496af2b1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_overskrift.hbs @@ -0,0 +1 @@ +_Perioden fra og med {{{dato periode.fom}}} til og med {{{dato periode.tom}}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_slutt.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_slutt.hbs new file mode 100644 index 000000000..af0508e4b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_slutt.hbs @@ -0,0 +1,32 @@ +{{~#* inline "pronomen"~}}{{#if gjelderDødsfall}}dere{{else}}du{{/if}}{{~/inline~}} +{{#if resultat.rentebeløp}} +__Renter + {{#switch vurderinger.aktsomhetsresultat}} + {{#case "GROV_UAKTSOMHET"}} + {{#switch vurderinger.vilkårsvurderingsresultat}} + {{#case "FORSTO_BURDE_FORSTÅTT"}} +Etter vår vurdering må {{> pronomen}} ha forstått at det {{> pronomen}} fikk utbetalt var feil. Derfor må {{> pronomen}} betale et rentetillegg på 10 prosent. Det vil si {{{kroner resultat.rentebeløp}}}{{> før-skatt}}. Dette er i tillegg til det feilutbetalte beløpet. + {{/case}} + {{#case "FEIL_OPPLYSNINGER_FRA_BRUKER"}} +Etter vår vurdering må {{> pronomen}} ha forstått at {{> pronomen}} ga oss uriktige opplysninger. Derfor må {{> pronomen}} betale et rentetillegg på 10 prosent. Det vil si {{{kroner resultat.rentebeløp}}}{{> før-skatt}}. Dette er i tillegg til det feilutbetalte beløpet. + {{/case}} + {{#case "MANGELFULLE_OPPLYSNINGER_FRA_BRUKER"}} +Etter vår vurdering må {{> pronomen}} ha forstått at {{> pronomen}} ikke ga NAV alle nødvendige opplysninger. Derfor må {{> pronomen}} betale et rentetillegg på 10 prosent. Det vil si {{{kroner resultat.rentebeløp}}}{{> før-skatt}}. Dette er i tillegg til det feilutbetalte beløpet. + {{/case}} + {{/switch}} + {{/case}} + {{#case "FORSETT"}} + {{#switch vurderinger.vilkårsvurderingsresultat}} + {{#case "FORSTO_BURDE_FORSTÅTT"}} +Etter vår vurdering forsto {{> pronomen}} at det {{> pronomen}} fikk utbetalt var feil. Derfor må {{> pronomen}} betale et rentetillegg på 10 prosent. Det vil si {{{kroner resultat.rentebeløp}}}{{> før-skatt}}. Dette er i tillegg til det feilutbetalte beløpet. + {{/case}} + {{#case "FEIL_OPPLYSNINGER_FRA_BRUKER"}} +Fordi vi har vurdert at {{> pronomen}} forsto at {{> pronomen}} ga oss uriktige opplysninger, må {{> pronomen}} betale et rentetillegg på 10 prosent. Det vil si {{{kroner resultat.rentebeløp}}}{{> før-skatt}}. Dette er i tillegg til det feilutbetalte beløpet. + {{/case}} + {{#case "MANGELFULLE_OPPLYSNINGER_FRA_BRUKER"}} +Fordi vi har vurdert at {{> pronomen}} forsto at {{> pronomen}} ikke ga oss alle nødvendige opplysninger, må {{> pronomen}} betale et rentetillegg på 10 prosent. Det vil si {{{kroner resultat.rentebeløp}}}{{> før-skatt}}. Dette er i tillegg til det feilutbetalte beløpet. + {{/case}} + {{/switch}} + {{/case}} + {{/switch}} +{{/if}} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_s\303\246rlige_grunner.hbs" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_s\303\246rlige_grunner.hbs" new file mode 100644 index 000000000..56a52e442 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_s\303\246rlige_grunner.hbs" @@ -0,0 +1,21 @@ +{{~#* inline "skulle-forstått" ~}} + {{{lookup-map vurderinger.aktsomhetsresultat SIMPEL_UAKTSOMHET="burde forstått" GROV_UAKTSOMHET="må ha forstått"}}} +{{~/inline~}} +{{#if vurderinger.særligeGrunner}} +__Er det særlige grunner til å redusere beløpet? + {{#if vurderinger.særligeGrunner.fritekst}} +{{{vurderinger.særligeGrunner.fritekst}}} + + {{/if}} + {{#if (not gjelderDødsfall)}} + {{#if (eq vurderinger.vilkårsvurderingsresultat "FORSTO_BURDE_FORSTÅTT") }} +{{> nb/vedtak/periode-særlige-grunner/periode_særlige_grunner_forstod}} + {{else}} +{{> nb/vedtak/periode-særlige-grunner/periode_særlige_grunner_feilaktig_mangelfull}} + {{/if}} + {{#if (neq kravgrunnlag.feilutbetaltBeløp resultat.tilbakekrevesBeløp)}} + +Du må betale {{{kroner resultat.tilbakekrevesBeløpMedRenter}}}{{> før-skatt}}. + {{/if}} + {{/if}} +{{/if}} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_vilk\303\245r.hbs" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_vilk\303\245r.hbs" new file mode 100644 index 000000000..3f26fe719 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/periode_vilk\303\245r.hbs" @@ -0,0 +1,111 @@ +{{~#* inline "pronomen"~}}{{#if gjelderDødsfall}}dere{{else}}du{{/if}}{{~/inline~}} +{{~#* inline "Pronomen"~}}{{#if gjelderDødsfall}}Dere{{else}}Du{{/if}}{{~/inline~}} +{{~#* inline "forsettTekst"~}} + {{#switch vurderinger.vilkårsvurderingsresultat}} + {{#case "FORSTO_BURDE_FORSTÅTT"}} +Selv hvis du har meldt fra til oss, kan vi kreve tilbake det du har fått for mye hvis du forsto at beløpet var feil. At du må betale tilbake, betyr ikke at du selv har skyld i feilutbetalingen. + +Ut fra informasjonen du har fått, legger vi til grunn at du forsto at du fikk utbetalt for mye. Derfor kan vi kreve tilbake. + {{/case}} + {{#case "FEIL_OPPLYSNINGER_FRA_BRUKER"}} +Etter vår vurdering forsto du at opplysningene du ga oss var uriktige. Derfor kan vi kreve pengene tilbake. + {{/case}} + {{#case "MANGELFULLE_OPPLYSNINGER_FRA_BRUKER"}} +Etter vår vurdering forsto du at du ikke ga oss alle opplysningene vi trengte tidsnok for å sikre at du fikk riktig utbetaling. Derfor kan vi kreve pengene tilbake. + {{/case}} + {{/switch}} +{{~/inline~}} +{{#* inline "fritekst"}} +{{#if vurderinger.fritekst}} + +{{{vurderinger.fritekst}}} +{{/if}} +{{/inline}} +{{#* inline "fritekst-brukerdød-tilbakekreves"}} +{{#if vurderinger.fritekst}} +{{#if (eq fakta.hendelsesundertype "BRUKER_DØD") }} + +{{/if}} +{{{vurderinger.fritekst}}} +{{/if}} +{{/inline}} +{{#* inline "ikke-krev-småbeløp"}} +{{> Pronomen}} har fått vite om {{> pronomen}} har rett til {{{ytelsesnavnUbestemt}}} og hvor mye {{> pronomen}} har rett til. Selv om {{> pronomen}} burde forstått at beløpet var feil, er beløpet lavere enn {{{kroner konfigurasjon.fireRettsgebyr}}}. {{> Pronomen}} må derfor ikke betale tilbake beløpet. +{{/inline}} +{{#if (not vurderinger.harForeldelsesavsnitt) }} +__Hvordan har vi kommet fram til at {{#if gjelderDødsfall}}{{{ytelsesnavnBestemt}}}{{else}}du{{/if}} {{#if (not resultat.tilbakekrevesBeløp)}}ikke {{/if}}må betale{{#if gjelderDødsfall}}s{{/if}} tilbake? +{{/if}} +{{#if gjelderDødsfall}} + {{#if resultat.tilbakekrevesBeløp}} + {{#if (eq fakta.hendelsesundertype "BRUKER_DØD") }} +Det burde vært oppdaget og meldt ifra om at {{{ytelsesnavnUbestemt}}} ble utbetalt etter at {{{søker.navn}}} døde. + {{/if}} +{{> fritekst-brukerdød-tilbakekreves}} + {{else}} + {{#if vurderinger.unntasInnkrevingPgaLavtBeløp}} +Selv om dere burde forstått at {{{ytelsesnavnBestemt}}} er utbetalt ved en feil, kreves ikke pengene tilbake. Det er fordi feilutbetalt beløp er lavere enn {{{kroner konfigurasjon.fireRettsgebyr}}}. + {{else}} +Dere har ikke fått den informasjonen dere trengte for å forstå at beløpet som ble utbetalt var feil. {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner skal derfor ikke betales tilbake. + {{/if}} +{{> fritekst}} + {{/if}} +{{else}} + {{#if (eq vurderinger.vilkårsvurderingsresultat "FORSTO_BURDE_FORSTÅTT")}} + {{#if (eq vurderinger.aktsomhetsresultat "SIMPEL_UAKTSOMHET")}} + {{#if vurderinger.unntasInnkrevingPgaLavtBeløp}} + {{> ikke-krev-småbeløp}} + {{else}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og hvor mye du har rett til. Selv hvis du har meldt fra til oss, kan vi kreve tilbake det du har fått for mye hvis du burde forstått at beløpet var feil. At du må betale tilbake, betyr ikke at du selv har skyld i feilutbetalingen. +{{> fritekst}} + +Ut fra informasjonen du har fått, burde du etter vår vurdering forstått at du fikk for mye utbetalt. Derfor kan vi kreve tilbake. + {{/if}} + {{/if}} + {{#if (eq vurderinger.aktsomhetsresultat "GROV_UAKTSOMHET")}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og hvor mye du har rett til. Selv hvis du har meldt fra til oss, kan vi kreve tilbake det du har fått for mye hvis du må ha forstått at beløpet var feil. At du må betale tilbake, betyr ikke at du selv har skyld i feilutbetalingen. +{{> fritekst}} + +Ut fra informasjonen du har fått, må du etter vår vurdering ha forstått at du fikk for mye utbetalt. Derfor kan vi kreve tilbake. + {{/if}} + {{/if}} + {{#if (eq vurderinger.vilkårsvurderingsresultat "FEIL_OPPLYSNINGER_FRA_BRUKER")}} + {{#if (eq vurderinger.aktsomhetsresultat "SIMPEL_UAKTSOMHET")}} + {{#if vurderinger.unntasInnkrevingPgaLavtBeløp}} + {{> ikke-krev-småbeløp}} + {{else}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og hvor mye du har rett til. Etter vår vurdering burde du forstått at opplysningene du ga oss var uriktige. Derfor kan vi kreve pengene tilbake. +{{> fritekst}} + {{/if}} + {{/if}} + {{#if (eq vurderinger.aktsomhetsresultat "GROV_UAKTSOMHET")}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og hvor mye du har rett til. Etter vår vurdering må du ha forstått at opplysningene du ga oss var uriktige. Derfor kan vi kreve pengene tilbake. +{{> fritekst}} + {{/if}} + {{/if}} + {{#if (eq vurderinger.vilkårsvurderingsresultat "MANGELFULLE_OPPLYSNINGER_FRA_BRUKER")}} + {{#if (eq vurderinger.aktsomhetsresultat "SIMPEL_UAKTSOMHET")}} + {{#if vurderinger.unntasInnkrevingPgaLavtBeløp}} + {{> ikke-krev-småbeløp}} + {{else}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og hvor mye du har rett til. Etter vår vurdering burde du forstått at du ikke ga oss alle opplysningene vi trengte tidsnok for å sikre at du fikk riktig utbetaling. Derfor kan vi kreve pengene tilbake. +{{> fritekst}} + {{/if}} + {{/if}} + {{#if (eq vurderinger.aktsomhetsresultat "GROV_UAKTSOMHET")}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og hvor mye du har rett til. Etter vår vurdering må du ha forstått at du ikke ga oss alle opplysningene vi trengte tidsnok for å sikre at du fikk riktig utbetaling. Derfor kan vi kreve pengene tilbake. +{{> fritekst}} + {{/if}} + {{/if}} + {{#if (eq vurderinger.vilkårsvurderingsresultat "GOD_TRO")}} + {{#if vurderinger.beløpIBehold}} +Vi har ikke gitt deg den informasjonen du trengte for å forstå at beløpet du fikk utbetalt var feil. Du har opplyst at du ikke har brukt {{{kroner vurderinger.beløpIBehold}}}. Disse kan vi kreve betale, selv om du ikke forstod at utbetalingen var feil. + {{else}} +Vi har ikke gitt deg den informasjonen du trengte for å forstå at beløpet du fikk utbetalt var feil. Du må derfor ikke betale tilbake. + {{/if}} +{{> fritekst}} + {{/if}} + {{#if (eq vurderinger.aktsomhetsresultat "FORSETT")}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og hvor mye du har rett til. {{> forsettTekst ~}} +{{>fritekst}} + {{/if}} +{{/if}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedlegg.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedlegg.hbs new file mode 100644 index 000000000..72e80f3c9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedlegg.hbs @@ -0,0 +1,54 @@ +{{~#* inline "pronomen"~}}{{#if gjelderDødsfall}}dere{{else}}du{{/if}}{{~/inline~}} +

    Oversikt over resultatet av tilbakebetalingssaken

    + + + + + + + {{#if totalresultat.totaltRentebeløp}} + + {{/if}} + {{#if totalresultat.harSkattetrekk}} + + + {{else}} + + {{/if}} + + + + {{#each perioder }} + + + + {{#if (not resultat.tilbakekrevesBeløp)}} + + {{else if (eq resultat.tilbakekrevesBeløp kravgrunnlag.feilutbetaltBeløp)}} + + {{else}} + + {{/if}} + {{#if totalresultat.totaltRentebeløp}} + + {{/if}} + {{#if totalresultat.harSkattetrekk}} + + {{/if}} + + + {{/each}} + + + + + {{#if totalresultat.totaltRentebeløp}} + + {{/if}} + {{#if totalresultat.harSkattetrekk}} + + {{/if}} + + + +
    PeriodeFeilutbetalt beløpTilbakebetalingRenterBeløp før skattBeløp {{> pronomen}} skal betale tilbake etter skatt er trukket fraBeløp {{> pronomen}} skal betale tilbake
    {{{kortdato periode.fom}}} – {{{kortdato periode.tom}}}{{{kroner kravgrunnlag.feilutbetaltBeløp}}}Ingen tilbakebetalingHele beløpetDeler av beløpet{{#if resultat.rentebeløp}}{{{kroner resultat.rentebeløp}}}{{/if}}{{{kroner resultat.tilbakekrevesBeløpMedRenter}}}{{{kroner resultat.tilbakekrevesBeløpUtenSkattMedRenter}}}
    Sum{{{kroner totalresultat.totaltTilbakekrevesBeløpMedRenter}}}{{{kroner totalresultat.totaltTilbakekrevesBeløpMedRenterUtenSkatt}}}
    \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak.hbs new file mode 100644 index 000000000..009addce8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak.hbs @@ -0,0 +1,11 @@ +{{> nb/vedtak/vedtak_felles}} +{{> nb/vedtak/vedtak_start}} +{{#each perioder }} +{{> nb/vedtak/periode_overskrift}} +{{> nb/vedtak/periode_fakta}} +{{> nb/vedtak/periode_foreldelse}} +{{> nb/vedtak/periode_vilkår}} +{{> nb/vedtak/periode_særlige_grunner}} +{{> nb/vedtak/periode_slutt}} +{{/each}} +{{> nb/vedtak/vedtak_slutt}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak_felles.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak_felles.hbs new file mode 100644 index 000000000..95583d192 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak_felles.hbs @@ -0,0 +1,4 @@ +{{#* inline "før-skatt"}}{{#if totalresultat.harSkattetrekk}} før skatt{{/if}}{{/inline}} +{{~#* inline "korrigert-total-beløp" ~}} +{{#if erFeilutbetaltBeløpKorrigertNed}}Beløpet har blitt endret. Nytt feilutbetalt beløp utgjør {{{kroner totaltFeilutbetaltBeløp}}}. {{/if}} +{{~/inline~}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak_overskrift.hbs new file mode 100644 index 000000000..3b17ce301 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak_overskrift.hbs @@ -0,0 +1,23 @@ +{{#switch totalresultat.hovedresultat}} + {{#case "FULL_TILBAKEBETALING"}} + {{#if gjelderDødsfall}} +Feilutbetalt {{{ytelsesnavnUbestemt}}} må betales tilbake + {{else}} +Du må betale tilbake {{{ytelsesnavnBestemt}}} + {{/if}} + {{/case}} + {{#case "DELVIS_TILBAKEBETALING"}} + {{#if gjelderDødsfall}} +Feilutbetalt {{{ytelsesnavnUbestemt}}} må betales tilbake + {{else}} +Du må betale tilbake {{{ytelsesnavnBestemt}}} + {{/if}} + {{/case}} + {{#case "INGEN_TILBAKEBETALING"}} + {{#if gjelderDødsfall}} +Feilutbetalt {{{ytelsesnavnUbestemt}}} må ikke betales tilbake + {{else}} +Du må ikke betale tilbake {{{ytelsesnavnBestemt}}} + {{/if}} + {{/case}} +{{/switch}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak_slutt.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak_slutt.hbs new file mode 100644 index 000000000..dcbd23db6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak_slutt.hbs @@ -0,0 +1,59 @@ +{{~#* inline "pronomen"~}}{{#if gjelderDødsfall}}dere{{else}}du{{/if}}{{~/inline~}} +{{~#* inline "Pronomen"~}}{{#if gjelderDødsfall}}Dere{{else}}Du{{/if}}{{~/inline~}} +{{#if (neq 1 antallPerioder)}}_{{#if hjemmel.lovhjemmelFlertall}}Lovhjemlene{{else}}Lovhjemmelen{{/if}} vi har brukt +{{else}} + +{{/if}} +Vedtaket er gjort etter {{{hjemmel.lovhjemmelVedtak}}}. +{{#if (neq totalresultat.hovedresultat "INGEN_TILBAKEBETALING")}} + {{#if skattepliktig}} +_Skatt og skatteoppgjør + {{#if totalresultat.harSkattetrekk }} +Skatten som er trukket fra beløpet {{#if gjelderDødsfall}}som skal betales{{else}}du skal betale{{/if}} tilbake, er beregnet etter det {{#if gjelderDødsfall}}som{{else}}du{{/if}} har blitt trukket i skatt i gjennomsnitt per måned.{{#if (not gjelderDødsfall)}} Det betyr at beløpet du skal betale tilbake etter skatt, ikke alltid er likt med det beløpet du fikk inn på konto.{{/if}} + + {{/if}} +NAV gir opplysninger til Skatteetaten{{#if totalresultat.harSkattetrekk }} om skattebeløpet og om beløpet {{#if gjelderDødsfall}}som skal betales{{else}}du skal betale{{/if}} tilbake før skatt er trukket fra{{/if}}. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjør. + {{/if}} +_Hvordan betaler du tilbake? + {{#if behandling.erRevurdering}} +Skatteetaten vil korrigere beløpet {{> pronomen}} skal betale tilbake. {{> Pronomen}} finner mer informasjon på +skatteetaten.no/betale. + {{else}} +{{> Pronomen}} vil få faktura fra Skatteetaten på det beløpet {{#if gjelderDødsfall}}som skal betales{{else}}du skal betale{{/if}} tilbake. + +På fakturaen vil det stå informasjon om nøyaktig beløp, kontonummer og forfallsdato. {{> Pronomen}} trenger ikke å gjøre noe før du får fakturaen. + +{{> Pronomen}} finner mer informasjon på skatteetaten.no/betale. + {{/if}} +{{/if}} +_{{#if gjelderDødsfall}}R{{else}}Du har r{{/if}}ett til å klage +{{> Pronomen}} kan klage innen {{{konfigurasjon.klagefristIUker}}} uker fra den datoen {{> pronomen}} mottok vedtaket. {{> Pronomen}} finner skjema og informasjon på nav.no/klage. +{{#if (neq totalresultat.hovedresultat "INGEN_TILBAKEBETALING")}} + + {{#if gjelderDødsfall}} +Beløpet må betales selv om det klages dette vedtaket. Dette følger av forvaltningsloven § 42. Hvis vedtaket blir gjort om slik at hele eller deler av beløpet likevel ikke skal betales tilbake, betaler vi pengene tilbake. + {{else}} +Du må som hovedregel begynne å betale tilbake beløpet når du får fakturaen, selv om du klager på dette vedtaket. Dette følger av forvaltningsloven § 42. Hvis du får vedtak om at du ikke trengte å betale tilbake hele eller deler av beløpet du skyldte, betaler vi pengene tilbake til deg. + {{/if}} +{{/if}} +_{{#if gjelderDødsfall}}R{{else}}Du har r{{/if}}ett til innsyn +{{#if gjelderDødsfall}} +{{> Pronomen}} kan be om innsyn ved å ta kontakt med oss. +{{else}} +På nav.no/dittnav kan du se dokumentene i saken din. +{{/if}} +{{> nb/felles/spørsmål_kontaktinformasjon}} + + +Med vennlig hilsen +{{{avsenderenhet}}} + +{venstrejustert}{{{ansvarligSaksbehandler}}}{høyrejustert}{{{ansvarligBeslutter}}}{{#if brevmetadata.finnesAnnenMottaker}} + + +Brev med likt innhold er sendt til {{{annenMottagersNavn}}}{{/if}} +{{#if harVedlegg}} + + +Vedlegg: Resultatet av tilbakebetalingssaken +{{/if}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak_start.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak_start.hbs new file mode 100644 index 000000000..f11809284 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nb/vedtak/vedtak_start.hbs @@ -0,0 +1,41 @@ +{{~#* inline "evt-renter-utsagn" ~}}{{#if totalresultat.totaltRentebeløp}} Dette beløpet er med renter.{{/if}}{{~/inline~}} +{{~#* inline "tilbakebetaling" ~}} +{{#if totalresultat.harSkattetrekk}} +{{#if gjelderDødsfall}}Utestående beløp{{else}}Beløpet du skylder{{/if}} før skatt er {{{kroner totalresultat.totaltTilbakekrevesBeløpMedRenter}}}. Dette er {{#if (eq totalresultat.hovedresultat "FULL_TILBAKEBETALING")}}hele{{else}}deler av{{/if}} det feilutbetalte beløpet.{{>evt-renter-utsagn}} + +Det {{#if gjelderDødsfall}}som skal betales{{else}}du skal betale{{/if}} tilbake etter at skatten er trukket fra, er {{{kroner totalresultat.totaltTilbakekrevesBeløpMedRenterUtenSkatt}}}. + {{else}} +{{#if (not gjelderDødsfall)}}Du må betale tilbake {{/if}}{{{kroner totalresultat.totaltTilbakekrevesBeløpMedRenter}}}{{#if gjelderDødsfall}} må betales tilbake{{/if}}, som er {{#if (eq totalresultat.hovedresultat "FULL_TILBAKEBETALING")}}hele{{else}}deler av{{/if}} det feilutbetalte beløpet.{{>evt-renter-utsagn}} + {{/if}} +{{/inline}} +{{#if behandling.erRevurdering}} +Vi har vurdert saken {{#if (not gjelderDødsfall)}}din {{/if}}om tilbakebetaling på nytt{{#if behandling.erRevurderingEtterKlageNfp}}, fordi {{#if gjelderDødsfall}}dere{{else}}du{{/if}} har klaget{{/if}}. Derfor gjelder ikke det tidligere vedtaket av {{{dato behandling.originalBehandlingsdatoFagsakvedtak}}} om tilbakebetaling av {{{ytelsesnavnUbestemt}}}. + +{{/if}} +{{#if varsel.varsletDato}} + {{#switch totalresultat.hovedresultat}} + {{#case "FULL_TILBAKEBETALING"}} +{{#if gjelderDødsfall}}Vi varslet {{{dato varsel.varsletDato}}} om at det er utbetalt{{else}}Du fikk varsel fra oss {{{dato varsel.varsletDato}}} om at du har fått utbetalt{{/if}} for mye. {{>korrigert-total-beløp}}{{>tilbakebetaling}} + {{/case}} + {{#case "DELVIS_TILBAKEBETALING"}} +{{#if gjelderDødsfall}}Vi varslet {{{dato varsel.varsletDato}}} om at det er utbetalt{{else}}Du fikk varsel fra oss {{{dato varsel.varsletDato}}} om at du har fått utbetalt{{/if}} {{{kroner varsel.varsletBeløp}}} for mye. {{>korrigert-total-beløp}}{{>tilbakebetaling}} + {{/case}} + {{#case "INGEN_TILBAKEBETALING"}} +{{#if gjelderDødsfall}}Vi varslet {{{dato varsel.varsletDato}}} om at det er utbetalt{{else}}Du fikk varsel fra oss {{{dato varsel.varsletDato}}} om at du har fått utbetalt{{/if}} {{{kroner varsel.varsletBeløp}}} for mye. {{>korrigert-total-beløp}}{{#if (not gjelderDødsfall)}}Vi har behandlet saken og du må ikke betale tilbake det feilutbetalte beløpet.{{else}}Det feilutbetalte beløpet må ikke betales tilbake.{{/if}} + {{/case}} + {{/switch}} +{{else}} +{{~#* inline "brev-ytelse-endret" ~}} + {{#if behandling.erRevurderingEtterKlageNfp}} +{{#if gjelderDødsfall}}Det er utbetalt{{else}}Du har fått{{/if}} {{{kroner totaltFeilutbetaltBeløp}}} for mye. {{else}} +I brev {{{dato fagsaksvedtaksdato}}} {{#if gjelderDødsfall}}sendte vi{{else}}fikk du{{/if}} melding om at {{#if gjelderDødsfall}}{{{ytelsesnavnBestemt}}}{{else}}{{{ytelsesnavnEiendomsform}}}{{/if}} er endret. Endringen førte til at {{#if gjelderDødsfall}}det ble{{else}}du har fått{{/if}} utbetalt for mye. {{/if}} +{{~/inline~}} +{{#if (neq totalresultat.hovedresultat "INGEN_TILBAKEBETALING")}} +{{>brev-ytelse-endret}} {{>tilbakebetaling}} +{{else}} +{{>brev-ytelse-endret}}{{#if gjelderDødsfall}}Det feilutbetalte beløpet må ikke betales tilbake.{{else}}Du må ikke betale tilbake det du har fått for mye.{{/if}} +{{/if}} +{{/if}} +{{#if fritekstoppsummering}} +{{{fritekstoppsummering}}} +{{/if}} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/felles/sp\303\270rsm\303\245l_kontaktinformasjon.hbs" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/felles/sp\303\270rsm\303\245l_kontaktinformasjon.hbs" new file mode 100644 index 000000000..98b67fae4 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/felles/sp\303\270rsm\303\245l_kontaktinformasjon.hbs" @@ -0,0 +1,8 @@ +{{~#* inline "pronomen"~}}{{#or gjelderDødsfall institusjon}}de{{else}}du{{/or}}{{~/inline~}} +{{~#* inline "Pronomen"~}}{{#or gjelderDødsfall institusjon}}De{{else}}Du{{/or}}{{~/inline~}} +_Har {{> pronomen}} spørsmål? +{{> Pronomen}} finn meir informasjon på {{{ytelseUrl}}}. + +På nav.no/kontakt kan {{> pronomen}} chatte eller skrive til oss. + +Om {{> pronomen}} ikkje finn svar på nav.no, kan {{> pronomen}} ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/header.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/header.hbs new file mode 100644 index 000000000..7db2e8f44 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/header.hbs @@ -0,0 +1,12 @@ +
    Dato: {{{kortdato brev.dato}}}
    +

    {{{brev.overskrift}}}

    +{{#if institusjon }} +
    +Navn: {{{institusjon.navn}}}
    +Organisasjonsnummer: {{{institusjon.organisasjonsnummer}}} +
    +{{/if}} +
    +{{#if institusjon}}Gjeld{{else}}Namn{{/if}}: {{{person.navn}}}
    +Fødselsnummer: {{{person.ident}}} +
    diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/henleggelse/henleggelse.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/henleggelse/henleggelse.hbs new file mode 100644 index 000000000..f5b1b74ee --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/henleggelse/henleggelse.hbs @@ -0,0 +1,14 @@ +{{~#* inline "deEllerdu"~}}{{#if institusjon}}de{{else}}du{{/if}}{{~/inline~}} +{{~#* inline "dykkEllerDeg"~}}{{#if institusjon}}dykk{{else}}deg{{/if}}{{~/inline~}} + +Vi sende {{#if (not gjelderDødsfall)}}{{> dykkEllerDeg}} {{/if}}eit varsel {{{dato varsletDato}}} om at {{#if gjelderDødsfall}}det var utbetalt for mykje{{else}}{{> deEllerdu}} hadde fått for mykje utbetalt{{/if}} i {{{ytelsesnavnUbestemt}}}. Vi har i ettertid avslutta saka om tilbakebetaling fordi det ikkje lenger er eit feilutbetalt beløp å betale tilbake. + +{{> nn/felles/spørsmål_kontaktinformasjon}} + + +Med venleg helsing +{{{avsenderenhet}}} + +{{{ansvarligSaksbehandler}}}{{#if brevmetadata.finnesAnnenMottaker}} + +Brev med likt innhald er sendt til {{{annenMottagersNavn}}}{{/if}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/henleggelse/henleggelse_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/henleggelse/henleggelse_overskrift.hbs new file mode 100644 index 000000000..8b7dbccbe --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/henleggelse/henleggelse_overskrift.hbs @@ -0,0 +1 @@ +NAV har avslutta saka {{#and (not gjelderDødsfall) (not institusjon)}}di {{/and}}om tilbakebetaling \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/henleggelse/henleggelse_revurdering.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/henleggelse/henleggelse_revurdering.hbs new file mode 100644 index 000000000..2d26aa1b1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/henleggelse/henleggelse_revurdering.hbs @@ -0,0 +1,11 @@ +{{{fritekstFraSaksbehandler}}} + +{{> nn/felles/spørsmål_kontaktinformasjon}} + + +Med venleg helsing +{{{avsenderenhet}}} + +{{{ansvarligSaksbehandler}}}{{#if brevmetadata.finnesAnnenMottaker}} + +Brev med likt innhald er sendt til {{{annenMottagersNavn}}}{{/if}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/henleggelse/henleggelse_revurdering_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/henleggelse/henleggelse_revurdering_overskrift.hbs new file mode 100644 index 000000000..fc9d50caf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/henleggelse/henleggelse_revurdering_overskrift.hbs @@ -0,0 +1 @@ +Tilbakebetaling {{{ytelsesnavnUbestemt}}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/innhentdokumentasjon/innhent_dokumentasjon.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/innhentdokumentasjon/innhent_dokumentasjon.hbs new file mode 100644 index 000000000..60dfab7b9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/innhentdokumentasjon/innhent_dokumentasjon.hbs @@ -0,0 +1,25 @@ +Vi har fått nye opplysningar i saka om tilbakebetaling av {{{ytelsesnavnUbestemt}}}, men trenger meir dokumentasjon for å kunne behandla saka. +_{{#or gjelderDødsfall institusjon}}De{{else}}Du{{/or}} må sende oss +{{{fritekstFraSaksbehandler}}} +_Saka blir behandla etter frist har gått ut +Dersom vi ikkje får opplysningane innan {{{dato fristdato}}}, behandlar vi saka med dei opplysningane vi har. + +Dette går fram av {{#if isBarnetrygd}}barnetrygdlova §§ 17 og 18{{else if isKontantstøtte}}kontantstøttelova §§ 12 og 13{{else}}folketrygdlova § 21-3{{/if}}. +{{#or gjelderDødsfall institusjon}} +_Rett til uttale +De kan sende uttalen til oss i posten. Adressa finn de på nav.no/ettersendelser. +De kan også ta kontakt med oss på nav.no/kontakt. +{{else}} +_Slik uttaler du deg +Du kan sende uttalen din ved å logge deg inn på nav.no/beskjedtilnav og velje «Send beskjed til NAV». Du kan også sende uttalen din til oss i posten. Adressa finn du på +nav.no/ettersendelser. +{{/or}} +{{> nn/felles/spørsmål_kontaktinformasjon}} + + +Med venleg helsing +{{{avsenderenhet}}} + +{{{ansvarligSaksbehandler}}}{{#if brevmetadata.finnesAnnenMottaker}} + +Brev med likt innhald er sendt til {{{annenMottagersNavn}}}{{/if}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/innhentdokumentasjon/innhent_dokumentasjon_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/innhentdokumentasjon/innhent_dokumentasjon_overskrift.hbs new file mode 100644 index 000000000..b08479a0b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/innhentdokumentasjon/innhent_dokumentasjon_overskrift.hbs @@ -0,0 +1 @@ +Vi trenger fleire opplysningar \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/korrigert_varsel.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/korrigert_varsel.hbs new file mode 100644 index 000000000..3a411a704 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/korrigert_varsel.hbs @@ -0,0 +1,11 @@ +{{~#* inline "førSkatt"}}{{#if ytelseMedSkatt}} før skatt{{/if}}{{~/inline~}} +{{~#* inline "duEllerDe"~}}{{#if institusjon}}de{{else}}du{{/if}}{{~/inline~}} + +Vi varslet {{#and (not gjelderDødsfall) (not institusjon)}}deg {{/and}}{{{dato varsletDato}}} om at {{#or gjelderDødsfall institusjon}}det var{{else}}du hadde fått{{/or}} utbetalt {{{kroner varsletBeløp}}} for mykje i {{{ytelsesnavnUbestemt}}}{{> førSkatt}}. + +Riktig beløp som er utbetalt for mykje, er {{{kroner beløp}}}{{> førSkatt}}. + +{{#eq feilutbetaltePerioder.length 1}}Perioden{{else}}Periodane{{/eq}} {{#if gjelderDødsfall}}det er{{else}}{{> duEllerDe}} har fått{{/if}} for mykje utbetalt for, er: +*-{{#each feilutbetaltePerioder}} +Frå og med {{{dato fom}}} til og med {{{dato tom}}}.{{/each}}-* +{{> nn/varsel/varsel_slutt}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/korrigert_varsel_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/korrigert_varsel_overskrift.hbs new file mode 100644 index 000000000..bee448585 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/korrigert_varsel_overskrift.hbs @@ -0,0 +1 @@ +Korrigert varsel om feilutbetalt {{{ytelsesnavnUbestemt}}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/varsel.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/varsel.hbs new file mode 100644 index 000000000..568f70b62 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/varsel.hbs @@ -0,0 +1,15 @@ +{{~#* inline "førSkatt"}}{{#if ytelseMedSkatt}} Dette er før skatt.{{/if}}{{~/inline~}} +{{~#* inline "DuEllerInstitusjonen"~}}{{#if institusjon}}Institusjonen{{else}}Du{{/if}}{{~/inline~}} +{{~#* inline "duEllerinstitusjonen"~}}{{#if institusjon}}institusjonen{{else}}du{{/if}}{{~/inline~}} + +{{#if datoerHvisSammenhengendePeriode.fom}} +{{#if gjelderDødsfall}}Det er utbetalt {{{kroner beløp}}} for mykje{{else}}{{> DuEllerInstitusjonen}} har fått {{{kroner beløp}}} for mykje utbetalt{{/if}} i {{{ytelsesnavnUbestemt}}} frå og med {{{dato datoerHvisSammenhengendePeriode.fom }}} til og med {{{dato datoerHvisSammenhengendePeriode.tom}}}.{{> førSkatt}} + +{{else}} +{{#if gjelderDødsfall}}Det er utbetalt {{{kroner beløp}}} for mykje{{else}}{{> DuEllerInstitusjonen}} har fått {{{kroner beløp}}} for mykje utbetalt{{/if}} i {{{ytelsesnavnUbestemt}}}.{{#if ytelseMedSkatt}} Dette er før skatt.{{/if}} + +{{#eq feilutbetaltePerioder.length 1}}Perioden{{else}}Periodane{{/eq}} {{#if gjelderDødsfall}}det er{{else}}{{> duEllerinstitusjonen}} har fått{{/if}} for mykje utbetalt for, er: +*-{{#each feilutbetaltePerioder}} +Frå og med {{{dato fom}}} til og med {{{dato tom}}}.{{/each}}-* +{{/if}} +{{> nn/varsel/varsel_slutt}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/varsel_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/varsel_overskrift.hbs new file mode 100644 index 000000000..c79e5279c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/varsel_overskrift.hbs @@ -0,0 +1,7 @@ +{{~#* inline "duEllerInstitusjonen"~}}{{#if institusjon}}institusjonen{{else}}du{{/if}}{{~/inline~}} + +{{#if gjelderDødsfall}} +NAV vurderer om feilutbetalt {{{ytelsesnavnUbestemt}}} skal betalast tilbake +{{ else }} +NAV vurderer om {{> duEllerInstitusjonen}} må betale tilbake {{{ytelsesnavnUbestemt}}} +{{/if}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/varsel_slutt.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/varsel_slutt.hbs new file mode 100644 index 000000000..15f556f7d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/varsel_slutt.hbs @@ -0,0 +1,61 @@ +{{~#* inline "DuEllerInstitusjonen"~}}{{#if institusjon}}Institusjonen{{else}}Du{{/if}}{{~/inline~}} +{{~#* inline "duEllerinstitusjonen"~}}{{#if institusjon}}institusjonen{{else}}du{{/if}}{{~/inline~}} +{{~#* inline "duEllerDe"~}}{{#if institusjon}}de{{else}}du{{/if}}{{~/inline~}} + +Før vi avgjer om {{#or gjelderDødsfall institusjon}}pengane skal betalast{{else}}du skal betale{{/or}} tilbake, kan {{#or gjelderDødsfall institusjon}}de uttale dykk{{else}}du uttale deg{{/or}} innan {{{dato fristdatoForTilbakemelding}}}. +{{#if ytelseMedSkatt}} + +Dersom {{#or gjelderDødsfall institusjon}}det må betalast{{else}}du må betale{{/or}} tilbake, reduserer vi beløpet med trekt skatt. +{{/if}} +_Dette har skjedd +{{#or gjelderDødsfall institusjon}}{{{storForbokstav ytelsesnavnBestemt}}}{{else}}{{{storForbokstav ytelsesnavnEiendomsform}}}{{/or}} blei endra {{{dato revurderingsvedtaksdato}}}, og endringa har ført til at {{#if gjelderDødsfall}}det er{{else}}{{> duEllerinstitusjonen}} har fått{{/if}} utbetalt for mykje. +{{#if varseltekstFraSaksbehandler}} + +{{{varseltekstFraSaksbehandler}}} +{{/if}} +_Dette legg vi vekt på i vurderinga vår +For å avgjere om vi kan krevje tilbake, tek vi først stilling til dei {{#if gjelderDødsfall}}to{{else}}tre{{/if}} øvste punkta i denne lista. Dersom resultatet blir at vi kan krevje tilbake, vurderer vi om {{#if (not gjelderDødsfall)}}{{> duEllerDe}} skal betale tilbake {{/if}}heile eller delar av beløpet{{#if gjelderDødsfall}} skal betalast tilbake{{/if}}. + +Vi legg vekt på + +*-om {{#or gjelderDødsfall institusjon}}de{{else}}du{{/or}} forstod eller burde forstått at beløpet {{#or gjelderDødsfall institusjon}}som er{{else}}du fekk{{/or}} utbetalt var feil +{{#if gjelderDødsfall}}om vi har fått informasjon frå dykk{{else}}om {{> duEllerDe}} har gitt riktig informasjon til NAV +om {{> duEllerDe}} har gitt all informasjon til NAV i rett tid{{/if}} +kor lang tid det har gått sidan {{{ytelsesnavnBestemt}}} blei feilutbetalt +kor stort det feilutbetalte beløpet er +om feilen kan vere NAV si skuld-* + +Sjølv om det er NAV som er skuld i feilutbetalinga, kan vi likevel krevje at {{#or gjelderDødsfall institusjon}}feilutbetalt beløp vert betalt tilbake{{else}}du betaler tilbake pengane{{/or}}. + +{{#if rentepliktig}} +Dersom {{#if gjelderDødsfall}}de{{else}}{{> duEllerinstitusjonen}}{{/if}} må betale tilbake, og {{#if gjelderDødsfall}}de{{else}}{{> duEllerinstitusjonen}}{{/if}} har gitt oss feil eller mangelfull informasjon, kan vi krevje at {{#if gjelderDødsfall}}de{{else}}{{> duEllerinstitusjonen}}{{/if}} betaler eit rentetillegg på ti prosent av beløpet {{#if gjelderDødsfall}}de{{else}}{{> duEllerinstitusjonen}}{{/if}} skuldar. + +{{/if}} +Dette går fram av {{#if isBarnetrygd}}barnetrygdlova § 13{{else if isKontantstøtte}}kontantstøttelova § 11{{else}}folketrygdlova §§ 22-15 og 22-17a{{/if}}. +{{#or gjelderDødsfall institusjon}} +_Rett til uttale +De kan sende uttalen til oss i posten. Adressa finn de på nav.no/ettersendelser. +De kan også ta kontakt med oss på nav.no/kontakt. +_Rett til innsyn +De kan be om innsyn ved å ta kontakt med oss. +{{else}} +_Slik uttaler du deg +Du kan sende uttalen din ved å logge deg inn på nav.no/beskjedtilnav og velje «Send beskjed til NAV». Du kan også sende uttalen din til oss i posten. Adressa finn du på +nav.no/ettersendelser. +_Du har rett til innsyn +På nav.no/dittnav kan du sjå dokumenta i saka di. +{{/or}} +{{> nn/felles/spørsmål_kontaktinformasjon}} + + +Med venleg helsing +{{{avsenderenhet}}} + +{{{ansvarligSaksbehandler}}}{{#if brevmetadata.finnesAnnenMottaker}} + +Brev med likt innhald er sendt til {{{annenMottagersNavn}}}{{/if}} +{{#if harVedlegg}} + + +Vedlegg: Oversikt feilutbetalt beløp per måned +{{/if}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/vedlegg.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/vedlegg.hbs new file mode 100644 index 000000000..56d3150ab --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/varsel/vedlegg.hbs @@ -0,0 +1,27 @@ +

    Feilutbetalt beløp per måned

    + + + + + {{#if ytelseMedSkatt}} + + + + {{else}} + + + + {{/if}} + + + + {{#each feilutbetaltePerioder }} + + + + + + + {{/each}} + +
    PeriodeUtbetalt beløp før skatt er trekt fråNytt beregna beløp før skatt er trekt fråFeilutbetalt beløp før skatt er trekt fråUtbetalt beløpNytt beregna beløpFeilutbetalt beløp
    {{{måned måned}}}{{{kroner tidligereUtbetaltBeløp}}}{{{kroner nyttBeløp}}}{{{kroner feilutbetaltBeløp}}}
    \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt.hbs new file mode 100644 index 000000000..7dc5c2bf0 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt.hbs @@ -0,0 +1,3 @@ +{{> nn/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt_start}} + +{{> nn/vedtak/vedtak_slutt}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt_start.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt_start.hbs new file mode 100644 index 000000000..bec306e40 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/fritekstFeilutbetalingBortfalt/fritekstFeilutbetalingBortfalt_start.hbs @@ -0,0 +1 @@ +{{{fritekstoppsummering}}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-fakta/periode_fakta_ba.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-fakta/periode_fakta_ba.hbs new file mode 100644 index 000000000..674019afa --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-fakta/periode_fakta_ba.hbs @@ -0,0 +1,184 @@ +{{> nn/vedtak/periode-fakta/periode_fakta_felles}} +{{#switch fakta.hendelsestype}} + {{#case "ANNET"}} + {{#switch fakta.hendelsesundertype}} + {{#case "ANNET_FRITEKST"}} + {{/case}} + {{/switch}} + {{/case}} + {{#case "SATSER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "SATSENDRING"}} +Barnetrygda er endra fordi det har vore ei satsendring. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "SMÅBARNSTILLEGG"}} + {{#switch fakta.hendelsesundertype}} + {{#case "SMÅBARNSTILLEGG_3_ÅR"}} +Du har fått småbarnstillegg etter at barnet fylte 3 år. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SMÅBARNSTILLEGG_OVERGANGSSTØNAD"}} +Du har fått småbarnstillegg etter at du ikkje lenger hadde full overgangsstønad. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "BOR_MED_SØKER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BOR_IKKE_MED_BARN"}} + Du har fått {{{ytelsesnavnUbestemt}}} for barn som ikkje bur fast hos deg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "BOSATT_I_RIKET"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BARN_FLYTTET_FRA_NORGE"}} +Du har fått {{{ytelsesnavnUbestemt}}} for barn som har flytta frå Noreg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BRUKER_FLYTTET_FRA_NORGE"}} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du flytta frå Noreg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BARN_BOR_IKKE_I_NORGE"}} +Du har fått {{{ytelsesnavnUbestemt}}} for barn som ikkje budde i Noreg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BRUKER_BOR_IKKE_I_NORGE"}} +Du har fått {{{ytelsesnavnUbestemt}}} utan at du budde i Noreg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BRUKER_OG_BARN_FLYTTET_FRA_NORGE"}} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du og barn flytta frå Noreg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BRUKER_OG_BARN_BOR_IKKE_I_NORGE"}} +Du har fått {{{ytelsesnavnUbestemt}}} utan at du og barn budde i Noreg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "LOVLIG_OPPHOLD"}} + {{#switch fakta.hendelsesundertype}} + {{#case "UTEN_OPPHOLDSTILLATELSE"}} + Du har fått {{{ytelsesnavnUbestemt}}} utan at du hadde opphaldsløyve i Noreg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "DØDSFALL"}} + {{> fakta-død}} + {{/case}} + {{#case "DELT_BOSTED"}} + {{#switch fakta.hendelsesundertype}} + {{#case "ENIGHET_OM_OPPHØR_DELT_BOSTED"}} +Du har fått {{{ytelsesnavnUbestemt}}} etter at avtalen om delt bustad er opphøyrt. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "UENIGHET_OM_OPPHØR_DELT_BOSTED"}} +Du har fått {{{ytelsesnavnUbestemt}}} etter at avtalen om delt bustad ikkje lenger vert praktisert. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "FLYTTET_SAMMEN"}} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du flytta saman med den andre forelderen. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "BARNS_ALDER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BARN_OVER_18_ÅR"}} +Du har fått {{{ytelsesnavnUbestemt}}} etter at barn fylte 18 år. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BARN_OVER_6_ÅR"}} +Du har fått {{{ytelsesnavnUbestemt}}} med feil beløp etter at barn har fylt 6 år. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "MEDLEMSKAP_BA"}} + {{#switch fakta.hendelsesundertype}} + {{#case "UTENLANDS_IKKE_MEDLEM"}} + Du har fått {{{ytelsesnavnUbestemt}}} etter at du og barn flytta til utlandet. De er ikkje lenger medlem i folketrygda under opphaldet i utlandet. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "MEDLEMSKAP_OPPHØRT"}} + Du har fått {{{ytelsesnavnUbestemt}}} etter at du og barn ikkje lenger er medlem i folketrygda under opphaldet i utlandet. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "ANNEN_FORELDER_IKKE_MEDLEM"}} + Du har fått {{{ytelsesnavnUbestemt}}} etter at du og barn flytta til utlandet. Begge foreldra må vere medlem i folketrygda for å få barnetrygd. Den andre forelderen er ikkje medlem i folketrygda. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "ANNEN_FORELDER_OPPHØRT_MEDLEMSKAP"}} + Du har fått {{{ytelsesnavnUbestemt}}} etter at du og barn flytta til utlandet. Begge foreldra må vere medlem i folketrygden for å få barnetrygd. Den andre forelderen er ikkje lenger medlem i folketrygda. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "FLERE_UTENLANDSOPPHOLD"}} + Du er ikkje medlem i folketrygda fordi du har opphaldt deg så mykje i utlandet at du ikkje er rekna som busett i Noreg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BOSATT_IKKE_MEDLEM"}} + Du har fått {{{ytelsesnavnUbestemt}}} utan at du er medlem i folketrygda. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "UTVIDET"}} + {{#switch fakta.hendelsesundertype}} + {{#case "GIFT"}} + Du har fått utvida {{{ytelsesnavnUbestemt}}} etter at du gifta deg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "NYTT_BARN"}} + Du har fått utvida {{{ytelsesnavnUbestemt}}} etter at du har fått nytt barn. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SAMBOER_12_MÅNEDER"}} + Du har fått utvida {{{ytelsesnavnUbestemt}}} etter at du har vore sambuar i meir enn 12 månader. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "FLYTTET_SAMMEN_ANNEN_FORELDER"}} + Du har fått utvida {{{ytelsesnavnUbestemt}}} etter at du flytta saman med den andre forelderen. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "FLYTTET_SAMMEN_EKTEFELLE"}} + Du har fått utvida {{{ytelsesnavnUbestemt}}} etter at du flytta saman med ektefellen din. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "FLYTTET_SAMMEN_SAMBOER"}} + Du har fått utvida {{{ytelsesnavnUbestemt}}} etter at du flytta saman med sambuaren din. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "GIFT_IKKE_EGEN_HUSHOLDNING"}} + Du har fått utvida {{{ytelsesnavnUbestemt}}} sjølv om du var rekna som gift fordi du ikkje har eiga hushaldning. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SAMBOER_IKKE_EGEN_HUSHOLDNING"}} + Du har fått utvida {{{ytelsesnavnUbestemt}}} sjølv om du var rekna som sambuar fordi du ikkje har eiga hushaldning. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "EKTEFELLE_AVSLUTTET_SONING"}} + Du har fått utvida {{{ytelsesnavnUbestemt}}} etter at ektefellen din avslutta soninga. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SAMBOER_AVSLUTTET_SONING"}} + Du har fått utvida {{{ytelsesnavnUbestemt}}} etter at sambuaren din avslutta soninga. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "EKTEFELLE_INSTITUSJON"}} + Du har fått utvida {{{ytelsesnavnUbestemt}}} etter at ektefellen din avslutta opphaldet i institusjon. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SAMBOER_INSTITUSJON"}} + Du har fått utvida {{{ytelsesnavnUbestemt}}} etter at sambuaren din avslutta opphaldet i institusjon. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} +{{/switch}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-fakta/periode_fakta_ef.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-fakta/periode_fakta_ef.hbs new file mode 100644 index 000000000..3390acfb2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-fakta/periode_fakta_ef.hbs @@ -0,0 +1,265 @@ +{{> nn/vedtak/periode-fakta/periode_fakta_felles}} +{{#switch fakta.hendelsestype}} + {{#case "ANNET"}} + {{#switch fakta.hendelsesundertype}} + {{#case "ANNET_FRITEKST"}} + {{/case}} + {{/switch}} + {{/case}} + {{#case "DØDSFALL"}} + {{> fakta-død}} + {{/case}} + {{#case "MEDLEMSKAP"}} + {{#switch fakta.hendelsesundertype}} + {{#case "MEDLEM_SISTE_5_ÅR" }} +Du har ikkje vore medlem i folketrygda i dei siste 5 åra før du søkte om {{{ytelsesnavnUbestemt}}}. Derfor har du ikkje rett til stønaden. + +Du har fått {{kroner kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt. + {{/case}} + {{#case "LOVLIG_OPPHOLD" }} +Du er ikkje medlem av folketrygda fordi du ikkje har opphaldsløyve i Noreg. Derfor har du ikkje rett til {{{ytelsesnavnUbestemt}}}. + +Du har fått {{kroner kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt. + {{/case}} + {{/switch}} + {{/case}} + {{#case "OPPHOLD_I_NORGE"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BRUKER_IKKE_OPPHOLD_I_NORGE" }} +Du har fått {{{ytelsesnavnUbestemt}}} utan at du har opphalde deg i Noreg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BARN_IKKE_OPPHOLD_I_NORGE" }} +Du har fått {{{ytelsesnavnUbestemt}}} for barn som ikkje har opphalde seg i Noreg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BRUKER_FLYTTET_FRA_NORGE" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du flytta frå Noreg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BARN_FLYTTET_FRA_NORGE" }} +Du har fått {{{ytelsesnavnUbestemt}}} for barn som har flytta frå Noreg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "OPPHOLD_UTLAND_6_UKER_ELLER_MER" }} +Du har opphalde deg i utlandet i meir enn 6 veker i løpet av dei siste 12 månadane. Derfor har du ikkje rett til {{{ytelsesnavnUbestemt}}}. + +Du har fått {{kroner kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt. + {{/case}} + {{/switch}} + {{/case}} + {{#case "ENSLIG_FORSØRGER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "UGIFT" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du gifta deg. Du har ikkje rett på {{{ytelsesnavnUbestemt}}} når du er gift. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SEPARERT_SKILT" }} +Du har fått {{{ytelsesnavnUbestemt}}} utan å ha ein formell separasjon eller skilsmisse. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SAMBOER" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du vart sambuar. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "NYTT_BARN_SAMME_PARTNER" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du fekk nytt barn med same partnar. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "ENDRET_SAMVÆRSORDNING" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at samværsordninga er endra slik at du ikkje lenger har klart meir av den daglege omsorga. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BARN_FLYTTET" }} +Du har fått {{{ytelsesnavnUbestemt}}} for barn som ikkje bur fast hos deg. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "NÆRE_BOFORHOLD" }} +Du og den andre av foreldra til barnet bur nære kvarandre og du har ikkje klart meir av den daglege omsorga. Derfor har du ikkje rett til {{{ytelsesnavnBestemt}}}. + +Du har fått {{kroner kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt. + {{/case}} + {{#case "FORELDRE_LEVER_SAMMEN" }} +Du har fått {{{ytelsesnavnUbestemt}}} samtidig som du lever saman med den andre av foreldra til barnet. Du har derfor fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "OVERGANGSSTØNAD"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BARN_8_ÅR" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at det yngste barnet ditt fylte åtte år. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "YRKESRETTET_AKTIVITET"}} + {{#switch fakta.hendelsesundertype}} + {{#case "ARBEID" }} +Du har fått {{{ytelsesnavnUbestemt}}} utan at du har vore i arbeid som utgjer minst halvparten av full tid. + +Derfor har du fått {{kroner kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "REELL_ARBEIDSSØKER" }} +Du har fått {{{ytelsesnavnUbestemt}}} som reell arbeidssøkar. Du har ikkje stått tilmeldt NAV eller sendt meldekort til rett tid. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "UTDANNING" }} +Du har fått {{{ytelsesnavnUbestemt}}} utan å ha vore i utdanning som utgjer minst halvparten av full tid. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "ETABLERER_EGEN_VIRKSOMHET" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at du avslutta etableringa av eiga verksemd. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BARN_FYLT_1_ÅR" }} +Du har fått {{{ytelsesnavnUbestemt}}} etter at barnet ditt fylte 1 år uten at du har fylt aktivitetsplikta. Derfor har du fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "STØNADSPERIODE"}} + {{#switch fakta.hendelsesundertype}} + {{#case "HOVEDPERIODE_3_ÅR" }} +Du har fått {{{ytelsesnavnUbestemt}}} i meir enn tre år og fyller ikkje vilkåra for utviding av stønadsperioden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "UTVIDELSE_UTDANNING" }} +Du har fått {{{ytelsesnavnUbestemt}}} utan å ha vore i utdanning som utgjer minst halvparten av full tid. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "UTVIDELSE_SÆRLIG_TILSYNSKREVENDE_BARN" }} +Du har fått forlenga {{{ytelsesnavnUbestemt}}} fordi du har eit barn som krev særleg tilsyn, og som hindra deg frå å vera i arbeid. Du er ikkje lenger hindra frå å arbeide. Derfor har du ikkje rett til {{{ytelsesnavnUbestemt}}}. + +Du har fått {{kroner kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt. + {{/case}} + {{#case "UTVIDELSE_FORBIGÅENDE_SYKDOM" }} +Du har fått forlenga {{{ytelsesnavnUbestemt}}} utan at du eller barnet hadde ein forbigåande sjukdom som hindra deg i å arbeida. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "PÅVENTE_AV_SKOLESTART_STARTET_IKKE" }} +Du har fått forlenga {{{ytelsesnavnUbestemt}}} fordi du skulle byrje på skule. Fordi du ikkje byrja på skulen, har du ikkje rett på stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "PÅVENTE_SKOLESTART_STARTET_TIDLIGERE" }} +Du har fått forlenga {{{ytelsesnavnUbestemt}}} fordi du skulle byrje på skule. Du har byrja på skule og har ikkje lenger rett til stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "PÅVENTE_ARBEIDSTILBUD_STARTET_IKKE" }} +Du har fått forlenga {{{ytelsesnavnUbestemt}}} fordi du venta på å byrje i ein konkret jobb. Du skal ikkje lenger byrja i denne jobben. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "PÅVENTE_ARBEIDSTILBUD_STARTET_TIDLIGERE" }} +Du har fått forlenga {{{ytelsesnavnUbestemt}}} fordi du venta på å byrje i ein konkret jobb. Du har byrja å arbeida. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "PÅVENTE_BARNETILSYN_IKKE_HA_TILSYN" }} +Du har fått forlenga {{{ytelsesnavnUbestemt}}} fordi du venta på barnetilsyn. Fordi barnet ikkje lenger skal ha ei tilsynsordning, har du ikkje rett til stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "PÅVENTE_BARNETILSYN_STARTET_TIDLIGERE" }} +Du har fått forlenga {{{ytelsesnavnUbestemt}}} fordi du venta på barnetilsyn. Fordi barnet har byrja i ei tilsynsordning, har du ikkje lenger rett på stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "ARBEIDSSØKER" }} +Du har fått {{{ytelsesnavnUbestemt}}} fordi du melde deg som arbeidssøkar. Fordi du ikkje er reell arbeidssøkar lenger, har du ikkje rett på stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "INNTEKT"}} + {{#switch fakta.hendelsesundertype}} + {{#case "ARBEIDSINNTEKT_FÅTT_INNTEKT" }} +Du har hatt arbeidsinntekt utan at {{{ytelsesnavnBestemt}}} har blitt redusert. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "ARBEIDSINNTEKT_ENDRET_INNTEKT" }} +Du har fått {{{ytelsesnavnBestemt}}} redusert etter arbeidsinntekt. Arbeidsinntekta di er auka med 10 prosent eller meir. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "ANDRE_FOLKETRYGDYTELSER" }} +Du har mottatt andre ytingar frå folketrygda som blir rekna som arbeidsinntekt, utan at {{{ytelsesnavnBestemt}}} har blitt redusert. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SELVSTENDIG_NÆRINGSDRIVENDE_FÅTT_INNTEKT" }} +Du har hatt næringsinntekt utan at {{{ytelsesnavnBestemt}}} har blitt redusert. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SELVSTENDIG_NÆRINGSDRIVENDE_ENDRET_INNTEKT" }} +Du har fått {{{ytelsesnavnUbestemt}}} redusert etter næringsinntekt. Næringsinntekta di er auka med 10 prosent eller meir. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "PENSJONSYTELSER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "UFØRETRYGD" }} +Du har fått uføretrygd som skulle ha redusert {{{ytelsesnavnBestemt}}}. Fordi stønaden ikkje har blitt redusert, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "GJENLEVENDE_EKTEFELLE" }} +Du har fått pensjon som attlevande ektefelle som skulle ha redusert {{{ytelsesnavnBestemt}}}. Fordi stønaden ikkje har blitt redusert, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "STØNAD_TIL_BARNETILSYN"}} + {{#switch fakta.hendelsesundertype}} + {{#case "IKKE_ARBEID" }} +Du har fått stønad til barnetilsyn. Fordi du ikkje lenger er i arbeid, har du ikkje rett til stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "EGEN_VIRKSOMHET" }} +Du har fått stønad til barnetilsyn. Fordi du har avslutta etableringa av eiga verksemd, har du ikkje rett på stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "TILSYNSUTGIFTER_OPPHØRT" }} +Du har fått stønad til barnetilsyn etter at tilsynsordninga er avslutta. Fordi du ikkje lenger har utgifter til barnetilsyn, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "TILSYNSUTGIFTER_ENDRET" }} +Du har fått stønad til barnetilsyn. Fordi utgiftene til barnetilsyn er reduserte, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "FORBIGÅENDE_SYKDOM" }} +Du har fått stønad til barnetilsyn fordi du hadde ein forbigåande sjukdom som hindra deg fra å arbeide. Fordi du ikkje lenger er hindra frå å arbeida, har du ikkje rett til stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "ETTER_4_SKOLEÅR_UTGIFTENE_OPPHØRT" }} +Du har fått stønad til barnetilsyn for barn som har fullført fjerde skuleår. Fordi du ikkje lenger har utgifter til tilsyn av barn, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "ETTER_4_SKOLEÅR_ENDRET_ARBEIDSTID" }} +Du har fått stønad til barnetilsyn for barn som har fullført fjerde skuleår. Fordi du ikkje lenger har tilsynsbehov for barnet ut over ein vanleg arbeidsdag, har du ikkje rett til stønad til barnetilsyn. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "INNTEKT_OVER_6G" }} +Du har fått stønad til barnetilsyn. Fordi inntekta di er over {{#if grunnbeløp.grunnbeløpGanger6}}{{kroner grunnbeløp.grunnbeløpGanger6}}{{else}}seks gonger grunnbeløpet{{/if}}, har du ikkje lengre rett til stønaden. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. +{{#if grunnbeløp.tekst6GangerGrunnbeløp}} + +Seks gonger grunnbeløpet er {{grunnbeløp.tekst6GangerGrunnbeløp}}. +{{/if}} + {{/case}} + {{#case "KONTANTSTØTTE" }} +Du har fått stønad til barnetilsyn og kontantstøtte. Kontantstøtte skal trekkast frå tilsynsutgiftene før stønad til barnetilsyn blir berekna. Fordi stønaden ikkje har blitt redusert med kontantstøtta, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "ØKT_KONTANTSTØTTE" }} +Du har fått stønad til barnetilsyn og kontantstøtte. Kontantstøtte skal trekkast frå tilsynsutgiftene før stønad til barnetilsyn blir berekna. Fordi stønaden ikkje har blitt redusert etter at kontantstøtta auka, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "SKOLEPENGER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "IKKE_RETT_TIL_OVERGANGSSTØNAD" }} +Du har fått skulepengar. Du kan berre få skulepengar dersom du har rett til overgangsstønad. Då du ikkje lenger har rett til overgangsstønad, har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SLUTTET_I_UTDANNING" }} +Du har fått stønad til dekning av skulepengar etter at du slutta på utdanninga. Derfor har du fått {{kroner + kravgrunnlag.feilutbetaltBeløp}} for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} +{{/switch}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-fakta/periode_fakta_felles.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-fakta/periode_fakta_felles.hbs new file mode 100644 index 000000000..9b57aaceb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-fakta/periode_fakta_felles.hbs @@ -0,0 +1,18 @@ +{{#* inline "fakta-død"}} + {{#if førstePeriode}} + {{#switch fakta.hendelsesundertype}} + {{#case "BARN_DØD"}} +Vi har fått melding om at barnet ditt døydde. {{storForbokstav + ytelsesnavnBestemt}} skulle vore stansa frå og med {{{dato opphørsdatoDødtBarn}}}. + {{/case}} + {{#case "BRUKER_DØD"}} +Vi har fått melding om at {{{søker.navn}}} døydde. {{storForbokstav + ytelsesnavnBestemt}} skulle vore stansa frå og med {{{dato opphørsdatoDødSøker}}}. + {{/case}} + {{/switch}} + +Fordi {{{ytelsesnavnBestemt}}} er utbetalt etter denne datoen er det utbetalt {{kroner kravgrunnlag.feilutbetaltBeløp}} for mykje. + {{else}} +Det er utbetalt {{kroner kravgrunnlag.feilutbetaltBeløp}} for mykje for denne perioden. + {{/if}} +{{/inline}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-fakta/periode_fakta_ks.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-fakta/periode_fakta_ks.hbs new file mode 100644 index 000000000..2b310640c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-fakta/periode_fakta_ks.hbs @@ -0,0 +1,193 @@ +{{> nn/vedtak/periode-fakta/periode_fakta_felles}} +{{#switch fakta.hendelsestype}} + {{#case "VILKÅR_BARN"}} + {{#switch fakta.hendelsesundertype}} + {{#case "FULLTIDSPLASS_BARNEHAGE"}} +Du har fått kontantstøtte etter at barnet var tildelt fulltidsplass i barnehage. Når barnet er tildelt barnehageplass med 33 timar eller meir i veka, er dette rekna som fulltidsplass. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "DELTIDSPLASS_BARNEHAGEPLASS"}} +Du har fått kontantstøtte etter at barnet var tildelt deltidsplass i barnehage. Når barnet er tildelt barnehageplass med mindre enn 33 timar i veka, er dette rekna som deltidsplass. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BARN_IKKE_BOSATT"}} +Du har fått kontantstøtte sjølv om barn ikkje var busett i Noreg. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BARN_IKKE_OPPHOLDSTILLATELSE"}} +Du har fått kontantstøtte for barn som ikkje hadde opphaldsløyve i Noreg i meir enn 12 månader. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BARN_FLYTTET_FRA_NORGE"}} +Du har fått kontantstøtte for barn som har flytta frå Noreg. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BARN_OVER_2_ÅR"}} +Du har fått kontantstøtte etter at barn har fylt 2 år. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "VILKÅR_SØKER"}} + {{#switch fakta.hendelsesundertype}} + {{#case "DEN_ANDRE_FORELDEREN_IKKE_MEDLEM_FOLKETRYGDEN"}} +Den andre forelderen har ikkje vore medlem i folketrygda i 5 år. Begge foreldra må ha vore medlem i folketrygda i 5 år for å få kontantstøtte. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "DEN_ANDRE_FORELDEREN_IKKE_MEDLEM_FOLKETRYGDEN_ELLER_EØS"}} +Den andre forelderen har ikkje vore medlem i folketrygda eller i trygdeordningar i andre EØS-land i til saman i 5 år. Begge foreldra må fylle kravet om medlemskap for å få kontantstøtte. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SØKER_IKKE_MEDLEM_FOLKETRYGDEN"}} +Du har ikkje vore medlem i folketrygda i 5 år og har derfor ikkje rett til kontantstøtte. + +Du har fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt. + {{/case}} + {{#case "SØKER_IKKE_MEDLEM_FOLKETRYGDEN_ELLER_EØS"}} +Du har ikkje vore medlem i folketrygda eller i trygdeordningar i andre EØS-land i til saman 5 år. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BEGGE_FORELDRENE_IKKE_MEDLEM_FOLKETRYGDEN"}} +Du og den andre forelderen har ikkje vore medlem i folketrygda i 5 år. Begge foreldra må ha vore medlem i folketrygda i 5 år for å få kontantstøtte. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BEGGE_FORELDRENE_IKKE_MEDLEM_FOLKETRYGDEN_ELLER_EØS"}} +Du og den andre forelderen har ikkje vore medlem i folketrygda eller i trygdeordningar i andre EØS-land i til saman 5 år. Begge foreldra må fylle kravet om medlemskap for å få kontantstøtte. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BARN_BOR_IKKE_HOS_SØKER"}} +Du har fått kontantstøtte for barn som ikkje bur fast hos deg. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "UTENLANDSOPPHOLD_OVER_3_MÅNEDER"}} +Du har fått kontantstøtte når du har vore i utlandet. Når opphald i utlandet er meir enn 3 månader, har du ikkje rett til kontantstøtte. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SØKER_FLYTTET_FRA_NORGE"}} +Du har fått kontantstøtte etter at du flytta frå Noreg. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden + {{/case}} + {{#case "SØKER_IKKE_BOSATT"}} +Du har fått kontantstøtte sjølv om du ikkje var busett i Noreg. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SØKER_IKKE_OPPHOLDSTILLATELSE"}} +Du har fått kontantstøtte utan at du hadde opphaldsløyve i Noreg. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SØKER_IKKE_OPPHOLDSTILLATELSE_I_MER_ENN_12_MÅNEDER"}} +Du har fått kontantstøtte sjølv om du ikkje hadde opphaldsløyve i Noreg i meir enn 12 månader. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "BARN_I_FOSTERHJEM_ELLER_INSTITUSJON"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BARN_I_FOSTERHJEM"}} +Du har fått kontantstøtte når barnet bodde i fosterhjem. + +Derfor har du fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{#case "BARN_I_INSTITUSJON"}} +Du har fått kontantstøtte når barnet bodde i institusjon. + +Derfor har du fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mye utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "KONTANTSTØTTENS_STØRRELSE"}} + {{#switch fakta.hendelsesundertype}} + {{#case "FULLTIDSPLASS_BARNEHAGE"}} +Du har fått kontantstøtte etter at barnet var tildelt fulltidsplass i barnehage. Når barnet er tildelt barnehageplass med 33 timar eller meir i veka, er det rekna som fulltidsplass. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "DELTIDSPLASS_BARNEHAGEPLASS"}} +Du har fått kontantstøtte etter at barnet var tildelt deltidsplass i barnehage. Når barnet er tildelt barnehageplass med mindre enn 33 timar i veka, er dette rekna som deltidsplass. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "ØKT_TIMEANTALL_I_BARNEHAGE"}} +Du har fått kontantstøtte etter at barnet var tildelt fleire timar i barnehage. Når barnet er tildelt meir opphaldstid i veka enn tidligare, får du mindre i kontantstøtte. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "SATSENDRING"}} +Du har fått kontantstøtte med feil beløp fordi det har vore ei satsendring. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + + {{#case "STØTTEPERIODE"}} + {{#switch fakta.hendelsesundertype}} + {{#case "BARN_2_ÅR"}} +Du har fått kontantstøtte etter at barn har fylt 2 år. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "UTBETALING"}} + {{#switch fakta.hendelsesundertype}} + {{#case "DELT_BOSTED_AVTALE_OPPHØRT"}} +Du har fått kontantstøtte etter at avtalen om delt bustad er opphørt. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "DOBBELUTBETALING"}} +Du har fått kontantstøtte for samme barn og i same tidsrom som den andre forelderen. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "KONTANTSTØTTE_FOR_ADOPTERTE_BARN"}} + {{#switch fakta.hendelsesundertype}} + {{#case "MER_ENN_11_MÅNEDER"}} +Du har fått kontantstøtte i meir enn 11 månader. Kontantstøtte for adopterte barn kan maksimalt gjevast for 11 månader. + +Derfor har du fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{#case "BARN_STARTET_PÅ_SKOLEN"}} +Du har fått kontantstøtte etter at barn har byrja på skulen. Kontantstøtte for adopterte barn kan gjevast fram til barnet byrjar på skulen. + +Du har derfor fått {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje utbetalt i denne perioden. + {{/case}} + {{/switch}} + {{/case}} + {{#case "ANNET_KS"}} + {{#switch fakta.hendelsesundertype}} + {{#case "ANNET_FRITEKST"}} + {{/case}} + {{#case "BARN_DØD"}} +Vi har fått melding om at barnet ditt døydde. Kontantstøtta skulle vore stansa frå og med {{{dato opphørsdatoDødtBarn}}}. Kontantstøtta er utbetalt etter denne datoen. + +Det er derfor utbetalt {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje + {{/case}} + {{#case "BRUKER_DØD"}} +Vi har fått melding om at døydde. Kontantstøtta skulle vore stansa frå og med {{{dato opphørsdatoDødtBarn}}}. Kontantstøtta er utbetalt etter denne datoen. + +Det er derfor utbetalt {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner for mykje. + {{/case}} + {{/switch}} + {{/case}} +{{/switch}} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-s\303\246rlige-grunner/periode_s\303\246rlige_grunner_feilaktig_mangelfull.hbs" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-s\303\246rlige-grunner/periode_s\303\246rlige_grunner_feilaktig_mangelfull.hbs" new file mode 100644 index 000000000..df7c20ccd --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-s\303\246rlige-grunner/periode_s\303\246rlige_grunner_feilaktig_mangelfull.hbs" @@ -0,0 +1,126 @@ +{{#switch navfeil=vurderinger.særligeGrunner.navfeil størrelse=vurderinger.særligeGrunner.størrelse tid=vurderinger.særligeGrunner.tid reduksjon=(neq kravgrunnlag.feilutbetaltBeløp resultat.tilbakekrevesBeløp) annet=vurderinger.særligeGrunner.annet}} + {{#case navfeil=false størrelse=false tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Det er lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=false reduksjon=true annet=false}} + {{#if (eq vurderinger.aktsomhetsresultat "SIMPEL_UAKTSOMHET")}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi vurderer likevel at aktløysa di har vore så lita at vi har redusert beløpet du må betale tilbake. + {{/if}} + {{/case}} + {{#case navfeil=false størrelse=false tid=false reduksjon=true annet=true}} + {{#if (eq vurderinger.aktsomhetsresultat "GROV_UAKTSOMHET")}} +Vi har lagt vekt på at du må ha forstått at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Det er i vurderingen også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{else}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi vurderer likevel at aktløysa di har vore så lita at vi har redusert beløpet du må betale tilbake. Det er i vurderingen også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/if}} + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok, og at vi ikkje kunne unngått å betale ut feil beløp. Du må derfor betale heile beløpet tilbake. + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok, og at vi ikkje kunne unngått å betale ut feil beløp. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=true annet=false}} + {{#if (eq vurderinger.aktsomhetsresultat "SIMPEL_UAKTSOMHET")}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi vurderer likevel at aktløysa di har vore så lita at vi har redusert beløpet du må betale tilbake. + {{/if}} + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=true annet=true}} + {{#if (eq vurderinger.aktsomhetsresultat "GROV_UAKTSOMHET")}} +Vi har lagt vekt på at du må ha forstått at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Det er i vurderingen også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{else}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi vurderer likevel at aktløysa di har vore så lita at vi har redusert beløpet du må betale tilbake. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/if}} + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga, og at beløpet er høgt. Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga, og at beløpet er høgt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=true annet=false}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det feilutbetalte beløpet er lågt. + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=true annet=true}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det feilutbetalte beløpet er lågt og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga, og at det er kort tid sidan feilutbetalinga skjedde. Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga, og at det er kort tid sidan feilutbetalinga skjedde. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det er lang tid sidan feilutbetalinga skjedde. + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det er lang tid sidan feilutbetalinga skjedde og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga, og at beløpet er høgt. Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga, og at beløpet er høgt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=true annet=false}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det feilutbetalte beløpet er lågt. + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=true annet=true}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det feilutbetalte beløpet er lågt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Det er også kort tid sidan utbetalinga skjedde, og beløpet er høgt. Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Det er også kort tid sidan utbetalinga skjedde, og beløpet er høgt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det har gått lang tid sidan feilutbetalinga skjedde, og beløpet er lågt. + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det har gått lang tid sidan feilutbetalinga skjedde, og beløpet er lågt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Det er også kort tid sidan utbetalinga skjedde, og beløpet er høgt. Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Det er også kort tid sidan utbetalinga skjedde, og beløpet er høgt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det har gått lang tid sidan feilutbetalinga skjedde, og beløpet er lågt. + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det har gått lang tid sidan feilutbetalinga skjedde, og beløpet er lågt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga, og at det er kort tid sidan feilutbetalinga skjedde. Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga, og at det er kort tid sidan feilutbetalinga skjedde. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det er lang tid sidan feilutbetalinga skjedde. + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det er lang tid sidan feilutbetalinga skjedde. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} +{{/switch}} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-s\303\246rlige-grunner/periode_s\303\246rlige_grunner_forstod.hbs" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-s\303\246rlige-grunner/periode_s\303\246rlige_grunner_forstod.hbs" new file mode 100644 index 000000000..4b118850a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode-s\303\246rlige-grunner/periode_s\303\246rlige_grunner_forstod.hbs" @@ -0,0 +1,114 @@ +{{#switch navfeil=vurderinger.særligeGrunner.navfeil størrelse=vurderinger.særligeGrunner.størrelse tid=vurderinger.særligeGrunner.tid reduksjon=(neq kravgrunnlag.feilutbetaltBeløp resultat.tilbakekrevesBeløp) annet=vurderinger.særligeGrunner.annet}} + {{#case navfeil=false størrelse=false tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at beløpet du fekk utbetalt var feil, og det er ingen bestemte grunnar til å redusere beløpet. Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=false reduksjon=false annet=true}} +Sjølv om du {{> skulle-forstått}} at du fekk for mykje utbetalt, har vi vurdert om det er grunnar til å redusere beløpet. Det er lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Vi har kome fram til at du må betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=false reduksjon=true annet=false}} +Vi har lagt vekt på at feilutbetalinga ikkje var tydeleg nok, sjølv om du {{> skulle-forstått}} at du fekk for mykje utbetalt. Derfor har vi redusert beløpet du må betale tilbake. + {{/case}} + {{#case navfeil=false størrelse=false tid=false reduksjon=true annet=true}} +Vi har lagt vekt på at feilutbetalinga ikkje var tydeleg nok, sjølv om du {{> skulle-forstått}} at du fekk for mykje utbetalt. Derfor har vi redusert beløpet du må betale tilbake. Det er i vurderingen også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Sjølv om vi kunne unngått at du fekk for mykje utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at beløpet var feil. Du må derfor betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Sjølv om vi kunne unngått at du fekk for mykje utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at beløpet var feil. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Du må derfor betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi vi kunne ha unngått at du fekk utbetalt for mykje. + {{/case}} + {{#case navfeil=true størrelse=false tid=false reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi vi kunne ha unngått at du fekk utbetalt for mykje. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til, og at det feilutbetalte beløpet er høgt. Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til, og at det feilutbetalte beløpet er høgt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Fordi det feilutbetalte beløpet er lågt, har vi redusert beløpet du må betale tilbake. + {{/case}} + {{#case navfeil=false størrelse=true tid=false reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi beløpet er lågt og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til, og at det er kort tid sidan feilutbetalinga skjedde. Du må derfor betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til, og at det er kort tid sidan feilutbetalinga skjedde. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du derfor betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Fordi det er lenge sidan feilutbetalinga skjedde, har vi redusert beløpet du må betale tilbake. + {{/case}} + {{#case navfeil=false størrelse=false tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har likevel redusert beløpet du må betale tilbake fordi det er lenge sidan feilutbetalinga skjedde og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Sjølv om vi kunne unngått at du fekk for mykje utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til, og at det feilutbetalte beløpet er høgt. Du må derfor betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Sjølv om vi kunne unngått at du fekk for mykje utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til, og at det feilutbetalte beløpet er høgt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Du må derfor betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har likevel redusert beløpet du må betale tilbake, fordi vi kunne unngått at du fekk utbetalt for mykje og beløpet er lågt. + {{/case}} + {{#case navfeil=true størrelse=true tid=false reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har likevel redusert beløpet du må betale tilbake, fordi vi kunne unngått at du fekk utbetalt for mykje og beløpet er lågt. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Sjølv om vi kunne unngått at du fekk for mykje utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har også lagt vekt på at det feilutbetalte beløpet er høgt og at det er kort tid sidan utbetalinga skjedde. Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Sjølv om vi kunne unngått at du fekk for mykje utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til,og at det feilutbetalte beløpet er høgt. Vi har også lagt vekt på at det er kort tid sidan utbetalinga skjedde og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har likevel redusert beløpet du må betale tilbake, fordi vi kunne unngått at du fekk utbetalt for mykje, beløpet er lågt og det er lenge sidan feilutbetalinga skjedde. + {{/case}} + {{#case navfeil=true størrelse=true tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har likevel redusert beløpet du må betale tilbake, fordi vi kunne unngått at du fekk utbetalt for mykje, beløpet er lågt og det er lenge sidan feilutbetalinga skjedde. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at beløpet var feil. Vi har også lagt vekt på at det feilutbetalte beløpet er høgt, og at det er kort tid sidan utbetalinga skjedde. Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Vi har lagt vekt på at du {{> skulle-forstått}} at beløpet var feil og at det feilutbetalte beløpet er høgt. Vi har også lagt vekt på at det er kort tid sidan utbetalinga skjedde og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har likevel redusert beløpet du må betale tilbake, fordi beløpet er lågt og det er lenge sidan feilutbetalinga skjedde. + {{/case}} + {{#case navfeil=false størrelse=true tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har likevel redusert beløpet du må betale tilbake, fordi beløpet er lågt og det er lenge sidan feilutbetalinga skjedde. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=false annet=false}} +Vi har vurdert om det er grunnar til å redusere beløpet. Sjølv om vi kunne unngått at du fekk for mykje utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har også lagt vekt på at det er kort tid sidan utbetalinga skjedde. Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=false annet=true}} +Vi har vurdert om det er grunnar til å redusere beløpet. Sjølv om vi kunne unngått at du fekk for mykje utbetalt, har vi lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har også lagt vekt på at det er kort tid sidan utbetalinga skjedde og {{{vurderinger.særligeGrunner.fritekstAnnet}}} + +Derfor må du betale tilbake heile beløpet. + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=true annet=false}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har likevel redusert beløpet du må betale tilbake, fordi vi kunne unngått at du fekk utbetalt for mykje, og det er lenge sidan feilutbetalinga skjedde. + {{/case}} + {{#case navfeil=true størrelse=false tid=true reduksjon=true annet=true}} +Vi har lagt vekt på at du {{> skulle-forstått}} at du fekk pengar du ikkje har rett til. Vi har likevel redusert beløpet du må betale tilbake, fordi vi kunne unngått at du fekk utbetalt for mykje, og det er lenge sidan feilutbetalinga skjedde. Det er også lagt vekt på {{{vurderinger.særligeGrunner.fritekstAnnet}}} + {{/case}} +{{/switch}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_fakta.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_fakta.hbs new file mode 100644 index 000000000..6f215b541 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_fakta.hbs @@ -0,0 +1,27 @@ +{{#switch brevmetadata.ytelsestype}} +{{#case "BARNETRYGD"}} +{{> nn/vedtak/periode-fakta/periode_fakta_ba}} +{{/case}} +{{#case "OVERGANGSSTØNAD"}} +{{> nn/vedtak/periode-fakta/periode_fakta_ef}} +{{/case}} +{{#case "KONTANTSTØTTE"}} +{{> nn/vedtak/periode-fakta/periode_fakta_ks}} +{{/case}} +{{#case "SKOLEPENGER"}} +{{> nn/vedtak/periode-fakta/periode_fakta_ef}} +{{/case}} +{{#case "BARNETILSYN"}} +{{> nn/vedtak/periode-fakta/periode_fakta_ef}} +{{/case}} +{{/switch}} +{{#if fakta.fritekstFakta}} + {{#if (eq fakta.hendelsesundertype "ANNET_FRITEKST")}} +{{{fakta.fritekstFakta}}} + +{{#if gjelderDødsfall}}Det er utbetalt {{{kroner kravgrunnlag.feilutbetaltBeløp}}} for mykje.{{else}}Du har fått {{{kroner kravgrunnlag.feilutbetaltBeløp}}} for mykje utbetalt.{{/if}} + {{else}} + +{{{fakta.fritekstFakta}}} + {{/if}} +{{/if}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_foreldelse.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_foreldelse.hbs new file mode 100644 index 000000000..cf9b7263a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_foreldelse.hbs @@ -0,0 +1,33 @@ +{{#* inline "fritekstForeldelse"}} + {{#if vurderinger.fritekstForeldelse}} + +{{{vurderinger.fritekstForeldelse}}} + {{/if}} +{{/inline}} +{{#if vurderinger.harForeldelsesavsnitt }} +__Korleis har vi kome fram til at du {{#if resultat.tilbakekrevesBeløp}}{{else}}ikkje {{/if}}må betale tilbake? +{{/if}} +{{#switch vurderinger.foreldelsevurdering}} + {{#case "FORELDET"}} +Fristen for å krevje tilbake {{{ytelsesnavnBestemt}}} er tre år og startar frå det tidspunktet feilutbetalinga skjedde. + +Foreldelsesfristen er {{{dato vurderinger.foreldelsesfrist}}}. Vi kan ikkje krevje tilbake pengar som er blitt utbetalt meir enn tre år før denne datoen. + +Du skal derfor ikkje betale tilbake {{{kroner resultat.foreldetBeløp}}}{{> før-skatt}}, som er feilutbetalt frå og med {{{dato periode.fom}}} til og med {{{dato periode.tom}}}. +{{> fritekstForeldelse}} + + {{/case}} + {{#case "TILLEGGSFRIST"}} +Fristen for å krevje tilbake {{{ytelsesnavnBestemt}}} er tre år frå det tidspunktet feilutbetalinga skjedde. Vi kan utvide fristen med ytterlegare 10 år dersom det ikkje var mogleg for NAV å oppdage feilen tidlegare. Frå tidspunktet vi oppdaga feilen, har vi eitt år på å krevje pengane tilbake. +{{> fritekstForeldelse}} + +Vi har oppdaga feilutbetalinga {{{dato vurderinger.oppdagelsesdato}}}, og har behandla saka di innan eitt år. Derfor må du betale tilbake {{{ytelsesnavnBestemt}}} du har fått frå og med {{{dato periode.fom}}} til og med {{{dato periode.tom}}}. + + {{/case}} + {{#case "IKKE_FORELDET"}} + {{! ikke noe tekst her}} + {{/case}} + {{#case "IKKE_VURDERT"}} + {{! ikke noe tekst her}} + {{/case}} +{{/switch}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_overskrift.hbs new file mode 100644 index 000000000..fb6dab118 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_overskrift.hbs @@ -0,0 +1 @@ +_Perioden frå og med {{{dato periode.fom}}} til og med {{{dato periode.tom}}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_slutt.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_slutt.hbs new file mode 100644 index 000000000..6e870641e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_slutt.hbs @@ -0,0 +1,32 @@ +{{~#* inline "pronomen"~}}{{#if gjelderDødsfall}}de{{else}}du{{/if}}{{~/inline~}} +{{#if resultat.rentebeløp}} +__Renter + {{#switch vurderinger.aktsomhetsresultat}} + {{#case "GROV_UAKTSOMHET"}} + {{#switch vurderinger.vilkårsvurderingsresultat}} + {{#case "FORSTO_BURDE_FORSTÅTT"}} +Etter vår vurdering må {{> pronomen}} ha forstått at det {{> pronomen}} fekk utbetalt var feil. Derfor må {{> pronomen}} betale eit rentetillegg på 10 prosent. Det vil seie {{{kroner resultat.rentebeløp}}}{{> før-skatt}}. Dette er i tillegg til det feilutbetalte beløpet. + {{/case}} + {{#case "FEIL_OPPLYSNINGER_FRA_BRUKER"}} +Etter vår vurdering må {{> pronomen}} ha forstått at {{> pronomen}} gav oss uriktige opplysningar. Derfor må {{> pronomen}} betale eit rentetillegg på 10 prosent. Det vil seie {{{kroner resultat.rentebeløp}}}{{> før-skatt}}. Dette er i tillegg til det feilutbetalte beløpet. + {{/case}} + {{#case "MANGELFULLE_OPPLYSNINGER_FRA_BRUKER"}} +Etter vår vurdering må {{> pronomen}} ha forstått at {{> pronomen}} ikkje gav NAV alle nødvendige opplysningar. Derfor må {{> pronomen}} betale eit rentetillegg på 10 prosent. Det vil seie {{{kroner resultat.rentebeløp}}}{{> før-skatt}}. Dette er i tillegg til det feilutbetalte beløpet. + {{/case}} + {{/switch}} + {{/case}} + {{#case "FORSETT"}} + {{#switch vurderinger.vilkårsvurderingsresultat}} + {{#case "FORSTO_BURDE_FORSTÅTT"}} +Etter vår vurdering forstod {{> pronomen}} at det {{> pronomen}} fekk utbetalt var feil. Derfor må {{> pronomen}} betale eit rentetillegg på 10 prosent. Det vil seie {{{kroner resultat.rentebeløp}}}{{> før-skatt}}. Dette er i tillegg til det feilutbetalte beløpet. + {{/case}} + {{#case "FEIL_OPPLYSNINGER_FRA_BRUKER"}} +Fordi vi har vurdert at {{> pronomen}} forstod at {{> pronomen}} gav oss uriktige opplysningar, må {{> pronomen}} betale eit rentetillegg på 10 prosent. Det vil seie {{{kroner resultat.rentebeløp}}}{{> før-skatt}}. Dette er i tillegg til det feilutbetalte beløpet. + {{/case}} + {{#case "MANGELFULLE_OPPLYSNINGER_FRA_BRUKER"}} +Fordi vi har vurdert at {{> pronomen}} forstod at {{> pronomen}} ikkje gav oss alle nødvendige opplysningar, må {{> pronomen}} betale eit rentetillegg på 10 prosent. Det vil seie {{{kroner resultat.rentebeløp}}}{{> før-skatt}}. Dette er i tillegg til det feilutbetalte beløpet. + {{/case}} + {{/switch}} + {{/case}} + {{/switch}} +{{/if}} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_s\303\246rlige_grunner.hbs" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_s\303\246rlige_grunner.hbs" new file mode 100644 index 000000000..ee5a6823e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_s\303\246rlige_grunner.hbs" @@ -0,0 +1,21 @@ +{{~#* inline "skulle-forstått" ~}} + {{{lookup-map vurderinger.aktsomhetsresultat SIMPEL_UAKTSOMHET="burde ha forstått" GROV_UAKTSOMHET="må ha forstått"}}} +{{~/inline~}} +{{#if vurderinger.særligeGrunner}} +__Er det særlege grunnar til å redusere beløpet? + {{#if vurderinger.særligeGrunner.fritekst}} +{{{vurderinger.særligeGrunner.fritekst}}} + + {{/if}} + {{#if (not gjelderDødsfall)}} + {{#if (eq vurderinger.vilkårsvurderingsresultat "FORSTO_BURDE_FORSTÅTT") }} +{{> nn/vedtak/periode-særlige-grunner/periode_særlige_grunner_forstod}} + {{else}} +{{> nn/vedtak/periode-særlige-grunner/periode_særlige_grunner_feilaktig_mangelfull}} + {{/if}} + {{#if (neq kravgrunnlag.feilutbetaltBeløp resultat.tilbakekrevesBeløp)}} + +Du må betale {{{kroner resultat.tilbakekrevesBeløpMedRenter}}}{{> før-skatt}}. + {{/if}} + {{/if}} +{{/if}} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_vilk\303\245r.hbs" "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_vilk\303\245r.hbs" new file mode 100644 index 000000000..3856ad938 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/periode_vilk\303\245r.hbs" @@ -0,0 +1,111 @@ +{{~#* inline "pronomen"~}}{{#if gjelderDødsfall}}de{{else}}du{{/if}}{{~/inline~}} +{{~#* inline "Pronomen"~}}{{#if gjelderDødsfall}}De{{else}}Du{{/if}}{{~/inline~}} +{{~#* inline "forsettTekst"~}} + {{#switch vurderinger.vilkårsvurderingsresultat}} + {{#case "FORSTO_BURDE_FORSTÅTT"}} + Sjølv om du har meldt frå til oss, kan vi krevje tilbake det du har fått for mykje dersom du forsto at beløpet var feil. At du må betale tilbake, betyr ikkje at du sjølv har skuld i feilutbetalinga. + + Ut frå informasjonen du har fått, legg vi til grunn at du forsto at du fekk utbetalt for mykje. Derfor kan vi krevje tilbake. + {{/case}} + {{#case "FEIL_OPPLYSNINGER_FRA_BRUKER"}} + Etter vår vurdering forsto du at opplysningane du gav oss var uriktige. Derfor kan vi krevje pengane tilbake. + {{/case}} + {{#case "MANGELFULLE_OPPLYSNINGER_FRA_BRUKER"}} + Etter vår vurdering forsto du at du ikkje gav oss alle opplysningane vi hadde bruk for tidsnok for å sikre at du fekk riktig utbetaling. Derfor kan vi krevje pengane tilbake. + {{/case}} + {{/switch}} +{{~/inline~}} +{{#* inline "fritekst"}} +{{#if vurderinger.fritekst}} + +{{{vurderinger.fritekst}}} +{{/if}} +{{/inline}} +{{#* inline "fritekst-brukerdød-tilbakekreves"}} +{{#if vurderinger.fritekst}} +{{#if (eq fakta.hendelsesundertype "BRUKER_DØD") }} + +{{/if}} +{{{vurderinger.fritekst}}} +{{/if}} +{{/inline}} +{{#* inline "ikke-krev-småbeløp"}} +{{> Pronomen}} har fått vite om {{> pronomen}} har rett til {{{ytelsesnavnUbestemt}}} og kor mykje {{> pronomen}} har rett til. Sjølv om {{> pronomen}} burde forstått at beløpet var feil, er beløpet lågare enn {{{kroner konfigurasjon.fireRettsgebyr}}}. {{> Pronomen}} må derfor ikkje betale tilbake beløpet. +{{/inline}} +{{#if (not vurderinger.harForeldelsesavsnitt) }} +__Korleis har vi kome fram til at {{#if gjelderDødsfall}}{{{ytelsesnavnBestemt}}}{{else}}du{{/if}} {{#if (not resultat.tilbakekrevesBeløp)}}ikkje {{/if}}må {{#if gjelderDødsfall}}betalast{{else}}betale{{/if}} tilbake? +{{/if}} +{{#if gjelderDødsfall}} + {{#if resultat.tilbakekrevesBeløp}} + {{#if (eq fakta.hendelsesundertype "BRUKER_DØD") }} +Det burde blitt oppdaga og meldt ifrå om at {{{ytelsesnavnUbestemt}}} blei utbetalt etter at {{{søker.navn}}} døydde. + {{/if}} +{{> fritekst-brukerdød-tilbakekreves}} + {{else}} + {{#if vurderinger.unntasInnkrevingPgaLavtBeløp}} +Sjølv om de burde ha forstått at {{{ytelsesnavnBestemt}}} er utbetalt ved ein feil, vert ikkje pengane krevd tilbake. Det er fordi feilutbetalt beløp er lågare enn {{{kroner konfigurasjon.fireRettsgebyr}}}. + {{else}} +De har ikkje fått den informasjonen de trong for å forstå at beløpet som blei utbetalt var feil. {{kroner kravgrunnlag.feilutbetaltBeløp}} kroner skal derfor ikkje betalast tilbake. + {{/if}} +{{> fritekst}} + {{/if}} +{{else}} + {{#if (eq vurderinger.vilkårsvurderingsresultat "FORSTO_BURDE_FORSTÅTT")}} + {{#if (eq vurderinger.aktsomhetsresultat "SIMPEL_UAKTSOMHET")}} + {{#if vurderinger.unntasInnkrevingPgaLavtBeløp}} + {{> ikke-krev-småbeløp}} + {{else}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og kor mykje du har rett til. Sjølv om du har meldt frå til oss, kan vi krevje tilbake det du har fått for mykje, dersom du burde forstått at beløpet var feil. At du må betale tilbake, betyr ikkje at du sjølv har skuld i feilutbetalinga. +{{> fritekst}} + +Ut frå informasjonen du har fått, burde du etter vår vurdering forstått at du fekk for mykje utbetalt. Derfor kan vi krevje pengane tilbake. + {{/if}} + {{/if}} + {{#if (eq vurderinger.aktsomhetsresultat "GROV_UAKTSOMHET")}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og kor mykje du har rett til. Sjølv om du har meldt frå til oss, kan vi krevje tilbake det du har fått for mykje, dersom du må ha forstått at beløpet var feil. At du må betale tilbake, betyr ikkje at du sjølv har skuld i feilutbetalinga. +{{> fritekst}} + +Ut frå informasjonen du har fått, må du etter vår vurdering ha forstått at du fekk for mykje utbetalt. Derfor kan vi krevje pengane tilbake. + {{/if}} + {{/if}} + {{#if (eq vurderinger.vilkårsvurderingsresultat "FEIL_OPPLYSNINGER_FRA_BRUKER")}} + {{#if (eq vurderinger.aktsomhetsresultat "SIMPEL_UAKTSOMHET")}} + {{#if vurderinger.unntasInnkrevingPgaLavtBeløp}} + {{> ikke-krev-småbeløp}} + {{else}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og kor mykje du har rett til. Vi vurderer det slik at du burde ha forstått at opplysningane du gav oss var uriktige. Derfor kan vi krevje pengane tilbake. +{{> fritekst}} + {{/if}} + {{/if}} + {{#if (eq vurderinger.aktsomhetsresultat "GROV_UAKTSOMHET")}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og kor mykje du har rett til. Vi vurderer det slik at du må ha forstått at opplysningane du gav oss var uriktige. Derfor kan vi krevje pengane tilbake. +{{> fritekst}} + {{/if}} + {{/if}} + {{#if (eq vurderinger.vilkårsvurderingsresultat "MANGELFULLE_OPPLYSNINGER_FRA_BRUKER")}} + {{#if (eq vurderinger.aktsomhetsresultat "SIMPEL_UAKTSOMHET")}} + {{#if vurderinger.unntasInnkrevingPgaLavtBeløp}} + {{> ikke-krev-småbeløp}} + {{else}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og kor mykje du har rett til. Vi vurderer det slik at du burde ha forstått at du ikkje gav oss alle opplysningane vi trong, tidsnok for å sikre at du fekk riktig utbetaling. Derfor kan vi krevje pengane tilbake. +{{> fritekst}} + {{/if}} + {{/if}} + {{#if (eq vurderinger.aktsomhetsresultat "GROV_UAKTSOMHET")}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og kor mykje du har rett til. Vi vurderer det slik at du må ha forstått at du ikkje gav oss alle opplysningane vi trong, tidsnok for å sikre at du fekk riktig utbetaling. Derfor kan vi krevje pengane tilbake. +{{> fritekst}} + {{/if}} + {{/if}} + {{#if (eq vurderinger.vilkårsvurderingsresultat "GOD_TRO")}} + {{#if vurderinger.beløpIBehold}} +Vi har ikkje gitt deg den informasjonen du trong for å forstå at beløpet du fekk utbetalt var feil. Du har opplyst at du ikkje har brukt {{{kroner vurderinger.beløpIBehold}}}. Desse kan vi krevje betale, sjølv om du ikkje forstod at utbetalinga var feil. + {{else}} +Vi har ikkje gitt deg den informasjonen du trong for å forstå at beløpet du fekk utbetalt var feil. Du må derfor ikkje betale tilbake. + {{/if}} +{{> fritekst}} + {{/if}} + {{#if (eq vurderinger.aktsomhetsresultat "FORSETT")}} +Du har fått vite om du har rett til {{{ytelsesnavnUbestemt}}} og kor mykje du har rett til. {{> forsettTekst ~}} +{{>fritekst}} + {{/if}} +{{/if}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedlegg.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedlegg.hbs new file mode 100644 index 000000000..5a3760d24 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedlegg.hbs @@ -0,0 +1,54 @@ +{{~#* inline "pronomen"~}}{{#if gjelderDødsfall}}de{{else}}du{{/if}}{{~/inline~}} +

    Oversikt over resultatet av tilbakebetalingssaka

    + + + + + + + {{#if totalresultat.totaltRentebeløp}} + + {{/if}} + {{#if totalresultat.harSkattetrekk}} + + + {{else}} + + {{/if}} + + + + {{#each perioder }} + + + + {{#if (not resultat.tilbakekrevesBeløp)}} + + {{else if (eq resultat.tilbakekrevesBeløp kravgrunnlag.feilutbetaltBeløp)}} + + {{else}} + + {{/if}} + {{#if totalresultat.totaltRentebeløp}} + + {{/if}} + {{#if totalresultat.harSkattetrekk}} + + {{/if}} + + + {{/each}} + + + + + {{#if totalresultat.totaltRentebeløp}} + + {{/if}} + {{#if totalresultat.harSkattetrekk}} + + {{/if}} + + + +
    PeriodeFeilutbetalt beløpTilbakebetalingRenterBeløp før skattBeløp {{> pronomen}} skal betale tilbake etter at skatt er trekt fråBeløp {{> pronomen}} skal betale tilbake
    {{{kortdato periode.fom}}} – {{{kortdato periode.tom}}}{{{kroner kravgrunnlag.feilutbetaltBeløp}}}Inga tilbakebetalingHeile beløpetDelar av beløpet{{#if resultat.rentebeløp}}{{{kroner resultat.rentebeløp}}}{{/if}}{{{kroner resultat.tilbakekrevesBeløpMedRenter}}}{{{kroner resultat.tilbakekrevesBeløpUtenSkattMedRenter}}}
    Sum{{{kroner totalresultat.totaltTilbakekrevesBeløpMedRenter}}}{{{kroner totalresultat.totaltTilbakekrevesBeløpMedRenterUtenSkatt}}}
    \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak.hbs new file mode 100644 index 000000000..1e817f78a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak.hbs @@ -0,0 +1,11 @@ +{{> nn/vedtak/vedtak_felles}} +{{> nn/vedtak/vedtak_start}} +{{#each perioder }} +{{> nn/vedtak/periode_overskrift}} +{{> nn/vedtak/periode_fakta}} +{{> nn/vedtak/periode_foreldelse}} +{{> nn/vedtak/periode_vilkår}} +{{> nn/vedtak/periode_særlige_grunner}} +{{> nn/vedtak/periode_slutt}} +{{/each}} +{{> nn/vedtak/vedtak_slutt}} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak_felles.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak_felles.hbs new file mode 100644 index 000000000..ee290b3e1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak_felles.hbs @@ -0,0 +1,4 @@ +{{#* inline "før-skatt"}}{{#if totalresultat.harSkattetrekk}} før skatt{{/if}}{{/inline}} +{{~#* inline "korrigert-total-beløp" ~}} +{{#if erFeilutbetaltBeløpKorrigertNed}}Beløpet har blitt endra. Nytt feilutbetalt beløp utgjør {{{kroner totaltFeilutbetaltBeløp}}}. {{/if}} +{{~/inline~}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak_overskrift.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak_overskrift.hbs new file mode 100644 index 000000000..47b6e10e6 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak_overskrift.hbs @@ -0,0 +1,23 @@ +{{#switch totalresultat.hovedresultat}} + {{#case "FULL_TILBAKEBETALING"}} + {{#if gjelderDødsfall}} +Feilutbetalt {{{ytelsesnavnUbestemt}}} må betalast tilbake + {{else}} +Du må betale tilbake {{{ytelsesnavnBestemt}}} + {{/if}} + {{/case}} + {{#case "DELVIS_TILBAKEBETALING"}} + {{#if gjelderDødsfall}} +Feilutbetalt {{{ytelsesnavnUbestemt}}} må betalast tilbake + {{else}} +Du må betale tilbake {{{ytelsesnavnBestemt}}} + {{/if}} + {{/case}} + {{#case "INGEN_TILBAKEBETALING"}} + {{#if gjelderDødsfall}} +Feilutbetalt {{{ytelsesnavnUbestemt}}} må ikkje betalast tilbake + {{else}} +Du må ikkje betale tilbake {{{ytelsesnavnBestemt}}} + {{/if}} + {{/case}} +{{/switch}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak_slutt.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak_slutt.hbs new file mode 100644 index 000000000..05483730b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak_slutt.hbs @@ -0,0 +1,59 @@ +{{~#* inline "pronomen"~}}{{#if gjelderDødsfall}}de{{else}}du{{/if}}{{~/inline~}} +{{~#* inline "Pronomen"~}}{{#if gjelderDødsfall}}De{{else}}Du{{/if}}{{~/inline~}} +{{#if (neq 1 antallPerioder)}}_{{#if hjemmel.lovhjemmelFlertall}}Lovheimlene{{else}}Lovheimelen{{/if}} vi har brukt +{{else}} + +{{/if}} +Vedtaket er gjort etter {{{hjemmel.lovhjemmelVedtak}}}. +{{#if (neq totalresultat.hovedresultat "INGEN_TILBAKEBETALING")}} + {{#if skattepliktig}} +_Skatt og skatteoppgjer + {{#if totalresultat.harSkattetrekk }} +Skatten som er trekt frå beløpet {{#if gjelderDødsfall}}som skal betalast{{else}}du skal betale{{/if}} tilbake, er berekna etter det {{#if gjelderDødsfall}}som{{else}}du{{/if}} har blitt trekt i skatt i gjennomsnitt per månad.{{#if (not gjelderDødsfall)}} Det betyr at beløpet du skal betale tilbake etter skatt, ikkje alltid er likt med det beløpet du fekk inn på konto.{{/if}} + + {{/if}} +NAV gjev opplysningar til Skatteetaten{{#if totalresultat.harSkattetrekk }} om skattebeløpet og om beløpet {{#if gjelderDødsfall}}som skal betalast{{else}}du skal betale{{/if}} tilbake før skatt er trekt frå{{/if}}. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjer. + {{/if}} +_Korleis betalar du tilbake? + {{#if behandling.erRevurdering}} +Skatteetaten vil korrigere beløpet {{> pronomen}} skal betale tilbake. {{> Pronomen}} finn meir informasjon på +skatteetaten.no/betale. + {{else}} +{{> Pronomen}} vil få faktura frå Skatteetaten på det beløpet {{#if gjelderDødsfall}}som skal betalast{{else}}du skal betale{{/if}} tilbake. + +På fakturaen vil det stå informasjon om nøyaktig beløp, kontonummer og forfallsdato. {{> Pronomen}} treng ikkje å gjere noko før du får fakturaen. + +{{> Pronomen}} finn meir informasjon på skatteetaten.no/betale. + {{/if}} +{{/if}} +_{{#if gjelderDødsfall}}R{{else}}Du har r{{/if}}ett til å klage +{{> Pronomen}} kan klage innan {{{konfigurasjon.klagefristIUker}}} veker frå den datoen {{> pronomen}} fekk vedtaket. {{> Pronomen}} finn skjema og informasjon på nav.no/klage. +{{#if (neq totalresultat.hovedresultat "INGEN_TILBAKEBETALING")}} + + {{#if gjelderDødsfall}} +Beløpet må betalast sjølv om det klagast dette vedtaket. Dette følgjer av forvaltningslova § 42. Om vedtaket blir gjort om slik at heile eller delar av beløpet likevel ikke skal betalast tilbake, betalar vi pengane tilbake. + {{else}} +Du må som hovudregel begynne å betale tilbake beløpet når du får fakturaen, sjølv om du klagar på dette vedtaket. Dette følgjer av forvaltningslova § 42. Vi vil betale tilbake pengane du har betalt inn, om du får vedtak om at du ikkje trong å betale tilbake heile eller delar av beløpet du skulda. + {{/if}} +{{/if}} +_{{#if gjelderDødsfall}}R{{else}}Du har r{{/if}}ett til innsyn +{{#if gjelderDødsfall}} +{{> Pronomen}} kan be om innsyn ved å ta kontakt med oss. +{{else}} +På nav.no/dittnav kan du sjå dokumenta i saka di. +{{/if}} +{{> nn/felles/spørsmål_kontaktinformasjon}} + + +Med venleg helsing +{{{avsenderenhet}}} + +{venstrejustert}{{{ansvarligSaksbehandler}}}{høyrejustert}{{{ansvarligBeslutter}}}{{#if brevmetadata.finnesAnnenMottaker}} + + +Brev med likt innhald er sendt til {{{annenMottagersNavn}}}{{/if}} +{{#if harVedlegg}} + + +Vedlegg: Resultatet av tilbakebetalingssaka +{{/if}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak_start.hbs b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak_start.hbs new file mode 100644 index 000000000..f0b67503e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/templates/nn/vedtak/vedtak_start.hbs @@ -0,0 +1,41 @@ +{{~#* inline "evt-renter-utsagn" ~}}{{#if totalresultat.totaltRentebeløp}} Dette beløpet er med renter.{{/if}}{{~/inline~}} +{{~#* inline "tilbakebetaling" ~}} +{{#if totalresultat.harSkattetrekk}} +{{#if gjelderDødsfall}}Utestående beløp{{else}}Beløpet du skuldar{{/if}} før skatt er {{{kroner totalresultat.totaltTilbakekrevesBeløpMedRenter}}}. Dette er {{#if (eq totalresultat.hovedresultat "FULL_TILBAKEBETALING")}}heile{{else}}delar av{{/if}} det feilutbetalte beløpet.{{>evt-renter-utsagn}} + +Det {{#if gjelderDødsfall}}som skal betalast{{else}}du skal betale{{/if}} tilbake etter skatten er trekt frå er {{{kroner totalresultat.totaltTilbakekrevesBeløpMedRenterUtenSkatt}}}. + {{else}} +{{#if (not gjelderDødsfall)}}Du må betale tilbake {{/if}}{{{kroner totalresultat.totaltTilbakekrevesBeløpMedRenter}}}{{#if gjelderDødsfall}} må betalast tilbake{{/if}}, som er {{#if (eq totalresultat.hovedresultat "FULL_TILBAKEBETALING")}}heile{{else}}delar av{{/if}} det feilutbetalte beløpet.{{>evt-renter-utsagn}} + {{/if}} +{{/inline}} +{{#if behandling.erRevurdering}} +Vi har vurdert saka {{#if (not gjelderDødsfall)}}di {{/if}}om tilbakebetaling på nytt{{#if behandling.erRevurderingEtterKlageNfp}}, fordi {{#if gjelderDødsfall}}de{{else}}du{{/if}} klaga{{/if}}. Derfor gjeld ikkje det tidlegare vedtaket av {{{dato behandling.originalBehandlingsdatoFagsakvedtak}}} om tilbakebetaling av {{{ytelsesnavnUbestemt}}}. + +{{/if}} +{{#if varsel.varsletDato}} + {{#switch totalresultat.hovedresultat}} + {{#case "FULL_TILBAKEBETALING"}} +{{#if gjelderDødsfall}}Vi varsla {{{dato varsel.varsletDato}}} om at det er utbetalt{{else}}Du fekk varsel fra oss {{{dato varsel.varsletDato}}} om at du har fått utbetalt{{/if}} for mykje. {{>korrigert-total-beløp}}{{>tilbakebetaling}} + {{/case}} + {{#case "DELVIS_TILBAKEBETALING"}} +{{#if gjelderDødsfall}}Vi varsla {{{dato varsel.varsletDato}}} om at det er utbetalt{{else}}Du fekk varsel fra oss {{{dato varsel.varsletDato}}} om at du har fått utbetalt{{/if}} {{{kroner varsel.varsletBeløp}}} for mykje. {{>korrigert-total-beløp}}{{>tilbakebetaling}} + {{/case}} + {{#case "INGEN_TILBAKEBETALING"}} +{{#if gjelderDødsfall}}Vi varsla {{{dato varsel.varsletDato}}} om at det er utbetalt{{else}}Du fekk varsel fra oss {{{dato varsel.varsletDato}}} om at du har fått utbetalt{{/if}} {{{kroner varsel.varsletBeløp}}} for mykje. {{>korrigert-total-beløp}}{{#if (not gjelderDødsfall)}}Vi har behandla saka di og du må ikkje betale tilbake det feilutbetalte beløpet.{{else}}Det feilutbetalte beløpet må ikkje betalast tilbake.{{/if}} + {{/case}} + {{/switch}} +{{else}} +{{~#* inline "brev-ytelse-endret" ~}} + {{#if behandling.erRevurderingEtterKlageNfp}} +{{#if gjelderDødsfall}}Det er utbetalt{{else}}Du har fått{{/if}} {{{kroner totaltFeilutbetaltBeløp}}} for mykje. {{else}} +I brev {{{dato fagsaksvedtaksdato}}} {{#if gjelderDødsfall}}sende vi{{else}}fekk du{{/if}} melding om at {{#if gjelderDødsfall}}{{{ytelsesnavnBestemt}}}{{else}}{{{ytelsesnavnEiendomsform}}}{{/if}} er endra. Endringa førte til at {{#if gjelderDødsfall}}det blei{{else}}du har fått{{/if}} utbetalt for mykje. {{/if}} +{{~/inline~}} +{{#if (neq totalresultat.hovedresultat "INGEN_TILBAKEBETALING")}} +{{>brev-ytelse-endret}} {{>tilbakebetaling}} +{{else}} +{{>brev-ytelse-endret}}{{#if gjelderDødsfall}}Det feilutbetalte beløpet må ikkje betalast tilbake.{{else}}Du må ikkje betale tilbake det du har fått for mykje.{{/if}} +{{/if}} +{{/if}} +{{#if fritekstoppsummering}} +{{{fritekstoppsummering}}} +{{/if}} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/binding.xjb b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/binding.xjb new file mode 100644 index 000000000..8e0cb722b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/binding.xjb @@ -0,0 +1,11 @@ + + + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/krav_og_vedtakstatus.xsd b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/krav_og_vedtakstatus.xsd new file mode 100644 index 000000000..0d0f19552 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/krav_og_vedtakstatus.xsd @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + 437 - Endring krav og vedtakstatus + + + + + + + + + + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/kravgrunnlag_annuller.xsd b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/kravgrunnlag_annuller.xsd new file mode 100644 index 000000000..2fd66a690 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/kravgrunnlag_annuller.xsd @@ -0,0 +1,22 @@ + + + + + + + + 446 - Annuller kravgrunnlag + + + + + + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/kravgrunnlag_detalj.xsd b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/kravgrunnlag_detalj.xsd new file mode 100644 index 000000000..4a91b9b15 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/kravgrunnlag_detalj.xsd @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + 420 - Hent kravgrunnlag + + + + + + + + + + + + 431 - Detaljert kravgrunnlag + + + + + + + + + + + + + + + + + + + + + + + + + + + + 432 - Detaljert kravgrunnlag periode + + + + + + + + + + + 433 - Detaljert kravgrunnlag belop + + + + + + + + + + + + + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/kravgrunnlag_liste.xsd b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/kravgrunnlag_liste.xsd new file mode 100644 index 000000000..1dafefa4f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/kravgrunnlag_liste.xsd @@ -0,0 +1,52 @@ + + + + + + + + 420 - Hent kravgrunnlag + + + + + + + + + + + + + + + + + + + 421 - Returnert kravgrunnlag + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/tilbakekrevingsvedtak.xsd b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/tilbakekrevingsvedtak.xsd new file mode 100644 index 000000000..d13cd547c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/tilbakekrevingsvedtak.xsd @@ -0,0 +1,57 @@ + + + + + + + + 441 - Tilbakekrevingsvedtak + + + + + + + + + + + + + + + + + 442 - Tilbakekrevingsperiode + + + + + + + + + + + + 443 - Tilbakekrevingsbelop + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/typer/typer.xsd b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/typer/typer.xsd new file mode 100644 index 000000000..eec51ccb7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/main/resources/xsd/typer/typer.xsd @@ -0,0 +1,110 @@ + + + + + + MMEL - Inneholder elementene som skal være med i en status output + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/DatabaseChangesTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/DatabaseChangesTest.kt new file mode 100644 index 000000000..32a78f46a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/DatabaseChangesTest.kt @@ -0,0 +1,36 @@ +package no.nav.familie.tilbake + +import org.junit.jupiter.api.Test +import java.nio.file.Files +import java.nio.file.Paths + +class DatabaseChangesTest { + + companion object { + + // Denne knekker bygg med høyere db-versjon enn main. Oppdater kun når du er klar for å merge db-endringer. + const val MERGED_DB_VERSION = 34 + } + + /** + * Hvis du har en databaseoppdatering vil denne testen feile, slik at ikke branch blir deployet ved en feil + */ + @Test + internal fun `valider migreringsscript`() { + val resourcesPath = Paths.get(Paths.get("").toAbsolutePath().toString(), "/src/main/resources/db/migration") + if (Files.walk(resourcesPath).anyMatch { it.toFile().isDirectory && it.fileName.toString() != "migration" }) { + throw RuntimeException("Fant directory med annet navn enn migration") + } + val migreringsscript = Files.walk(resourcesPath).map { it.fileName.toString() }.filter { it.endsWith(".sql") }.toList() + if (migreringsscript.isEmpty()) { + throw RuntimeException("Fant ikke noen migreringsscript") + } + migreringsscript + .forEach { + val fileVersion = it.substring(1, it.indexOf("_")) + if (fileVersion.toInt() > MERGED_DB_VERSION) { + throw RuntimeException("Det finnes migreringsscript som har høyere versjon enn det som er merget") + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/LauncherLocal.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/LauncherLocal.kt new file mode 100644 index 000000000..12170a6a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/LauncherLocal.kt @@ -0,0 +1,23 @@ +package no.nav.familie.tilbake + +import no.nav.familie.tilbake.config.ApplicationConfig +import no.nav.familie.tilbake.database.DbContainerInitializer +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration +import org.springframework.boot.builder.SpringApplicationBuilder + +@SpringBootApplication(exclude = [ErrorMvcAutoConfiguration::class]) +class LauncherLocal + +fun main(args: Array) { + // QAD hack for å få riktige profiler til spring 2.4.3 + System.setProperty( + "spring.profiles.active", + "local, mock-pdl, mock-oauth, mock-oppgave, mock-integrasjoner, mock-økonomi", + ) + + SpringApplicationBuilder(ApplicationConfig::class.java) + .initializers(DbContainerInitializer()) + .profiles("local", "mock-pdl", "mock-oauth", "mock-oppgave", "mock-integrasjoner", "mock-økonomi") + .run(*args) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/LauncherLocalPostgres.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/LauncherLocalPostgres.kt new file mode 100644 index 000000000..f9082d914 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/LauncherLocalPostgres.kt @@ -0,0 +1,28 @@ +package no.nav.familie.tilbake + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration +import org.springframework.boot.builder.SpringApplicationBuilder +import java.util.Properties + +@SpringBootApplication(exclude = [ErrorMvcAutoConfiguration::class]) +class LauncherLocalPostgres + +fun main(args: Array) { + val properties = Properties() + properties["DATASOURCE_URL"] = "jdbc:postgresql://localhost:5432/familie-tilbake" + properties["DATASOURCE_USERNAME"] = "postgres" + properties["DATASOURCE_PASSWORD"] = "test" + properties["DATASOURCE_DRIVER"] = "org.postgresql.Driver" + + // QAD hack for å få riktige profiler til spring 2.4.3 + System.setProperty( + "spring.profiles.active", + "local, mock-pdl, mock-oauth, mock-oppgave, mock-integrasjoner, mock-økonomi", + ) + + SpringApplicationBuilder(LauncherLocalPostgres::class.java) + .profiles("local", "mock-pdl", "mock-oauth", "mock-oppgave", "mock-integrasjoner", "mock-økonomi") + .properties(properties) + .run(*args) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/OppslagSpringRunnerTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/OppslagSpringRunnerTest.kt new file mode 100644 index 000000000..30b4d06ba --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/OppslagSpringRunnerTest.kt @@ -0,0 +1,182 @@ +package no.nav.familie.tilbake + +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender +import com.github.tomakehurst.wiremock.WireMockServer +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.domene.TaskLogg +import no.nav.familie.tilbake.avstemming.domain.Avstemmingsfil +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultat +import no.nav.familie.tilbake.behandling.domain.Behandlingsvedtak +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsak +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandling.domain.Fagsystemsbehandling +import no.nav.familie.tilbake.behandling.domain.Fagsystemskonsekvens +import no.nav.familie.tilbake.behandling.domain.HentFagsystemsbehandlingRequestSendt +import no.nav.familie.tilbake.behandling.domain.Varsel +import no.nav.familie.tilbake.behandling.domain.Varselsperiode +import no.nav.familie.tilbake.behandling.domain.Verge +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.database.DbContainerInitializer +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevsporing +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.domene.ManuellBrevmottaker +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsoppsummering +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsperiode +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetalingsperiode +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesperiode +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import no.nav.familie.tilbake.iverksettvedtak.domain.ØkonomiXmlSendt +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsperiode432 +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottatt +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottattArkiv +import no.nav.familie.tilbake.micrometer.domain.Meldingstelling +import no.nav.familie.tilbake.totrinn.domain.Totrinnsvurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingAktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingGodTro +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingSærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsperiode +import no.nav.security.mock.oauth2.MockOAuth2Server +import no.nav.security.token.support.spring.test.EnableMockOAuth2Server +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.cache.CacheManager +import org.springframework.context.ApplicationContext +import org.springframework.data.jdbc.core.JdbcAggregateOperations +import org.springframework.data.relational.core.conversion.DbActionExecutionException +import org.springframework.http.HttpHeaders +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.transaction.annotation.Transactional + +@ExtendWith(SpringExtension::class) +@ContextConfiguration(initializers = [DbContainerInitializer::class]) +@SpringBootTest(classes = [LauncherLocal::class], webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("integrasjonstest", "mock-oauth", "mock-pdl", "mock-integrasjoner", "mock-oppgave", "mock-økonomi") +@EnableMockOAuth2Server +abstract class OppslagSpringRunnerTest { + + private val listAppender = initLoggingEventListAppender() + protected var loggingEvents: MutableList = listAppender.list + protected val restTemplate = TestRestTemplate() + protected val headers = HttpHeaders() + + @Autowired + private lateinit var jdbcAggregateOperations: JdbcAggregateOperations + + @Autowired + private lateinit var applicationContext: ApplicationContext + + @Autowired + private lateinit var cacheManager: CacheManager + + @Autowired + private lateinit var mockOAuth2Server: MockOAuth2Server + + @LocalServerPort + private var port: Int? = 0 + + @AfterEach + @Transactional + fun reset() { + loggingEvents.clear() + resetDatabase() + clearCaches() + resetWiremockServers() + } + + protected fun lokalTestToken(): String = mockOAuth2Server.issueToken("issuer1", audience = "aud-localhost").serialize() + + protected fun localhost(uri: String): String { + return LOCALHOST + getPort() + uri + } + + fun readXml(fileName: String): String { + val url = requireNotNull(this::class.java.getResource(fileName)) { "fil med filnavn=$fileName finnes ikke" } + return url.readText() + } + + private fun resetWiremockServers() { + applicationContext.getBeansOfType(WireMockServer::class.java).values.forEach(WireMockServer::resetRequests) + } + + private fun clearCaches() { + cacheManager.cacheNames.mapNotNull { cacheManager.getCache(it) } + .forEach { it.clear() } + } + + private fun resetDatabase() { + listOf( + Fagsak::class, + Behandling::class, + Behandlingsårsak::class, + Fagsystemsbehandling::class, + Fagsystemskonsekvens::class, + Behandlingsstegstilstand::class, + Behandlingsresultat::class, + Behandlingsvedtak::class, + Totrinnsvurdering::class, + VurdertForeldelse::class, + Foreldelsesperiode::class, + Kravgrunnlag431::class, + Kravgrunnlagsperiode432::class, + Kravgrunnlagsbeløp433::class, + Vilkårsvurdering::class, + Vilkårsvurderingsperiode::class, + VilkårsvurderingAktsomhet::class, + VilkårsvurderingSærligGrunn::class, + VilkårsvurderingGodTro::class, + FaktaFeilutbetaling::class, + FaktaFeilutbetalingsperiode::class, + ØkonomiXmlMottatt::class, + Vedtaksbrevsoppsummering::class, + Vedtaksbrevsperiode::class, + ØkonomiXmlSendt::class, + Varsel::class, + Varselsperiode::class, + Brevsporing::class, + ØkonomiXmlMottattArkiv::class, + Verge::class, + Avstemmingsfil::class, + HentFagsystemsbehandlingRequestSendt::class, + Task::class, + TaskLogg::class, + Meldingstelling::class, + ManuellBrevmottaker::class, + ) + .reversed() + .forEach { + try { + jdbcAggregateOperations.deleteAll(it.java) + } catch (e: DbActionExecutionException) { + while (jdbcAggregateOperations.count(TaskLogg::class.java) > 0) { + jdbcAggregateOperations.deleteAll(TaskLogg::class.java) + } + jdbcAggregateOperations.deleteAll(it.java) + } + } + } + + protected fun getPort(): String { + return port.toString() + } + + companion object { + + private const val LOCALHOST = "http://localhost:" + protected fun initLoggingEventListAppender(): ListAppender { + val listAppender = ListAppender() + listAppender.start() + return listAppender + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/avstemming/FilMapperTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/avstemming/FilMapperTest.kt new file mode 100644 index 000000000..bc1c856cb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/avstemming/FilMapperTest.kt @@ -0,0 +1,69 @@ +package no.nav.familie.tilbake.avstemming + +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate + +class FilMapperTest { + + @Test + fun `tilFlatfil skal liste ut med forventet format for datoer og tall skal multipliseres med 100`() { + val avstemmingsfil = FilMapper(listOf(testRad())) + + avstemmingsfil.tilFlatfil() + .decodeToString() shouldBe FORVENTET_HEADER + "familie-tilbake;1234;12345678901;20191231;BA;1000;200;800;100;" + } + + @Test + fun `tilFlatfil skal ha newline for å skille rader`() { + val avstemmingsfil = FilMapper(listOf(testRad(), testRad())) + val enRad = "familie-tilbake;1234;12345678901;20191231;BA;1000;200;800;100;" + avstemmingsfil.tilFlatfil().decodeToString() shouldBe "$FORVENTET_HEADER$enRad\n$enRad" + } + + @Test + fun `tilFlatfil skal bruke kode i siste kolonne når det er omgjøring til ingen tilbakekreving`() { + val avstemmingsfil = FilMapper( + listOf( + Rad( + avsender = "familie-tilbake", + vedtakId = "1234", + fnr = "12345678901", + vedtaksdato = LocalDate.of(2019, 12, 31), + fagsakYtelseType = Ytelsestype.BARNETRYGD, + tilbakekrevesBruttoUtenRenter = BigDecimal.ZERO, + tilbakekrevesNettoUtenRenter = BigDecimal.ZERO, + renter = BigDecimal.ZERO, + skatt = BigDecimal.ZERO, + erOmgjøringTilIngenTilbakekreving = true, + ), + ), + ) + avstemmingsfil.tilFlatfil() + .decodeToString() shouldBe FORVENTET_HEADER + "familie-tilbake;1234;12345678901;20191231;BA;0;0;0;0;Omgjoring0" + } + + private fun testRad(): Rad { + return Rad( + avsender = "familie-tilbake", + vedtakId = "1234", + fnr = "12345678901", + vedtaksdato = LocalDate.of(2019, 12, 31), + fagsakYtelseType = Ytelsestype.BARNETRYGD, + tilbakekrevesBruttoUtenRenter = BigDecimal.valueOf(1000), + tilbakekrevesNettoUtenRenter = BigDecimal.valueOf(800), + skatt = BigDecimal.valueOf(200), + renter = BigDecimal.valueOf(100), + erOmgjøringTilIngenTilbakekreving = false, + ) + } + + companion object { + + private const val FORVENTET_HEADER = + "avsender;vedtakId;fnr;vedtaksdato;fagsakYtelseType;tilbakekrevesBruttoUtenRenter;skatt;" + + "tilbakekrevesNettoUtenRenter;renter;erOmgjøringTilIngenTilbakekreving\n" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/avstemming/domain/AvstemmingsfilRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/avstemming/domain/AvstemmingsfilRepositoryTest.kt new file mode 100644 index 000000000..ec4626980 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/avstemming/domain/AvstemmingsfilRepositoryTest.kt @@ -0,0 +1,32 @@ +package no.nav.familie.tilbake.avstemming.domain + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class AvstemmingsfilRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var avstemmingsfilRepository: AvstemmingsfilRepository + + private val avstemmingsfil = Testdata.avstemmingsfil + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av Avstemmingsfil til basen`() { + avstemmingsfilRepository.insert(avstemmingsfil) + + val lagretAvstemmingsfil = avstemmingsfilRepository.findByIdOrThrow(avstemmingsfil.id) + + lagretAvstemmingsfil.shouldBeEqualToComparingFieldsExcept( + avstemmingsfil, + Avstemmingsfil::fil, + Avstemmingsfil::sporbar, + Avstemmingsfil::versjon, + ) + lagretAvstemmingsfil.versjon shouldBe 1 + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/BehandlingMapperTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/BehandlingMapperTest.kt new file mode 100644 index 000000000..6c413dc95 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/BehandlingMapperTest.kt @@ -0,0 +1,91 @@ +package no.nav.familie.tilbake.behandling + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import no.nav.familie.kontrakter.felles.Regelverk +import no.nav.familie.kontrakter.felles.klage.FagsystemType +import no.nav.familie.tilbake.behandling.BehandlingMapper.tilVedtakForFagsystem +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultat +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.config.Constants +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.util.UUID + +internal class BehandlingMapperTest { + + @Nested + inner class TilVedtakForFagsystem { + + @Test + internal fun `mapper avsluttet behandling`() { + val behandling = behandling() + val resultat = tilVedtakForFagsystem(listOf(behandling)) + resultat.shouldHaveSize(1) + + resultat[0].resultat shouldBe "Full tilbakebetaling" + resultat[0].behandlingstype shouldBe "Tilbakekreving" + resultat[0].eksternBehandlingId shouldBe behandling.eksternBrukId.toString() + resultat[0].vedtakstidspunkt shouldBe LocalDate.of(2021, 7, 13).atStartOfDay() + resultat[0].fagsystemType shouldBe FagsystemType.TILBAKEKREVING + resultat[0].regelverk shouldBe Regelverk.NASJONAL + } + + @Test + internal fun `mapper ikke behandlinger er henlagt`() { + val behandling = behandling(behandlingsresultatstype = Behandlingsresultatstype.HENLAGT_FEILOPPRETTET) + tilVedtakForFagsystem(listOf(behandling)).shouldBeEmpty() + } + + @Test + internal fun `mapper ikke behandlinger har behandlingsresultat ikke_fastsatt`() { + val behandling = behandling(behandlingsresultatstype = Behandlingsresultatstype.IKKE_FASTSATT) + tilVedtakForFagsystem(listOf(behandling)).shouldBeEmpty() + } + + @Test + internal fun `mapper ikke behandlinger hvis behandlingsresultat mangler`() { + val behandling = behandling(behandlingsresultatstype = null) + tilVedtakForFagsystem(listOf(behandling)).shouldBeEmpty() + } + + @Test + internal fun `mapper ikke behandlinger som ikke er avsluttet`() { + val behandling = behandling(status = Behandlingsstatus.FATTER_VEDTAK) + tilVedtakForFagsystem(listOf(behandling)).shouldBeEmpty() + } + + @Test + internal fun `forventer at behandling inneholder avsluttet dato`() { + val behandling = behandling(avsluttetDato = null) + val exception = shouldThrow { + tilVedtakForFagsystem(listOf(behandling)) + } + exception.message shouldContain "Mangler avsluttet dato på behandling=" + } + } + + private fun behandling( + status: Behandlingsstatus = Behandlingsstatus.AVSLUTTET, + avsluttetDato: LocalDate? = LocalDate.of(2021, 7, 13), + behandlingsresultatstype: Behandlingsresultatstype? = Behandlingsresultatstype.FULL_TILBAKEBETALING, + ) = Behandling( + fagsakId = UUID.randomUUID(), + type = Behandlingstype.TILBAKEKREVING, + ansvarligSaksbehandler = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + behandlendeEnhet = "8020", + behandlendeEnhetsNavn = "Oslo", + manueltOpprettet = false, + status = status, + avsluttetDato = avsluttetDato, + resultater = behandlingsresultatstype?.let { setOf(Behandlingsresultat(type = behandlingsresultatstype)) } ?: emptySet(), + regelverk = Regelverk.NASJONAL, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/BehandlingRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/BehandlingRepositoryTest.kt new file mode 100644 index 000000000..e5c305f9e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/BehandlingRepositoryTest.kt @@ -0,0 +1,60 @@ +package no.nav.familie.tilbake.behandling + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class BehandlingRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + private val behandling = Testdata.behandling + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + } + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av Behandling til basen`() { + behandlingRepository.insert(behandling) + + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + lagretBehandling.shouldBeEqualToComparingFieldsExcept( + behandling, + Behandling::endretTidspunkt, + Behandling::sporbar, + Behandling::versjon, + ) + lagretBehandling.versjon shouldBe 1 + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av Behandling i basen`() { + behandlingRepository.insert(behandling) + val behandling = behandlingRepository.findByIdOrThrow(behandling.id) + val oppdatertBehandling = behandling.copy(status = Behandlingsstatus.UTREDES) + + behandlingRepository.update(oppdatertBehandling) + + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + lagretBehandling.shouldBeEqualToComparingFieldsExcept( + oppdatertBehandling, + Behandling::endretTidspunkt, + Behandling::sporbar, + Behandling::versjon, + ) + lagretBehandling.versjon shouldBe 2 + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/BehandlingServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/BehandlingServiceTest.kt new file mode 100644 index 000000000..5b6e41e23 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/BehandlingServiceTest.kt @@ -0,0 +1,1671 @@ +package no.nav.familie.tilbake.behandling + +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSingleElement +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.kontrakter.felles.tilbakekreving.Brevmottaker +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.kontrakter.felles.tilbakekreving.Institusjon +import no.nav.familie.kontrakter.felles.tilbakekreving.ManuellAdresseInfo +import no.nav.familie.kontrakter.felles.tilbakekreving.MottakerType +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettManueltTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Periode +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Varsel +import no.nav.familie.kontrakter.felles.tilbakekreving.Verge +import no.nav.familie.kontrakter.felles.tilbakekreving.Vergetype +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype.BARNETILSYN +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype.BARNETRYGD +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.BehandlingDto +import no.nav.familie.tilbake.api.dto.BehandlingPåVentDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegsinfoDto +import no.nav.familie.tilbake.api.dto.ByttEnhetDto +import no.nav.familie.tilbake.api.dto.HenleggelsesbrevFritekstDto +import no.nav.familie.tilbake.api.dto.OpprettRevurderingDto +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsakstype +import no.nav.familie.tilbake.behandling.domain.Saksbehandlingstype +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.behandling.task.OpprettBehandlingManueltTask +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.Sporbar +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.datavarehus.saksstatistikk.BehandlingTilstandService +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingRepository +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingService +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevsporing +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.henleggelse.SendHenleggelsesbrevTask +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.ManuellBrevmottakerRepository +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.LagHistorikkinnslagTask +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.task.FinnKravgrunnlagTask +import no.nav.familie.tilbake.kravgrunnlag.task.HentKravgrunnlagTask +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattRepository +import no.nav.familie.tilbake.micrometer.TellerService +import no.nav.familie.tilbake.oppgave.FerdigstillOppgaveTask +import no.nav.familie.tilbake.oppgave.LagOppgaveTask +import no.nav.familie.tilbake.oppgave.OppdaterEnhetOppgaveTask +import no.nav.familie.tilbake.oppgave.OppdaterOppgaveTask +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import no.nav.familie.tilbake.sikkerhet.Behandlerrolle +import no.nav.familie.tilbake.sikkerhet.InnloggetBrukertilgang +import no.nav.familie.tilbake.sikkerhet.TilgangService +import no.nav.familie.tilbake.sikkerhet.Tilgangskontrollsfagsystem +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.UUID + +internal class BehandlingServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var brevsporingRepository: BrevsporingRepository + + @Autowired + private lateinit var manuellBrevmottakerRepository: ManuellBrevmottakerRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var økonomiXmlMottattRepository: ØkonomiXmlMottattRepository + + @Autowired + private lateinit var behandlingService: BehandlingService + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + @Autowired + private lateinit var featureToggleService: FeatureToggleService + + private val fom: LocalDate = LocalDate.now().minusMonths(1) + private val tom: LocalDate = LocalDate.now() + + @BeforeEach + fun init() { + mockkObject(ContextService) + every { ContextService.hentSaksbehandler() }.returns("Z0000") + every { ContextService.hentHøyesteRolletilgangOgYtelsestypeForInnloggetBruker(any(), any()) } + .returns(InnloggetBrukertilgang(mapOf(Tilgangskontrollsfagsystem.SYSTEM_TILGANG to Behandlerrolle.SYSTEM))) + } + + @AfterEach + fun tearDown() { + clearMocks(ContextService) + } + + @Test + fun `opprettBehandling skal opprette automatisk behandling uten verge`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + assertBehandling(behandling, opprettTilbakekrevingRequest) + assertFagsak(behandling, opprettTilbakekrevingRequest) + assertFagsystemsbehandling(behandling, opprettTilbakekrevingRequest) + assertVarselData(behandling, opprettTilbakekrevingRequest) + behandling.verger.shouldBeEmpty() + assertHistorikkTask(behandling.id, TilbakekrevingHistorikkinnslagstype.BEHANDLING_OPPRETTET, Aktør.VEDTAKSLØSNING) + assertFinnKravgrunnlagTask(behandling.id) + assertOppgaveTask(behandling.id, LagOppgaveTask.TYPE) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.VARSEL, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + ) + } + + @Test + fun `opprettBehandling skal opprette automatisk behandling med verge`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = true, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + assertBehandling(behandling, opprettTilbakekrevingRequest) + assertFagsak(behandling, opprettTilbakekrevingRequest) + assertFagsystemsbehandling(behandling, opprettTilbakekrevingRequest) + assertVarselData(behandling, opprettTilbakekrevingRequest) + assertVerge(behandling, opprettTilbakekrevingRequest) + assertHistorikkTask(behandling.id, TilbakekrevingHistorikkinnslagstype.BEHANDLING_OPPRETTET, Aktør.VEDTAKSLØSNING) + assertFinnKravgrunnlagTask(behandling.id) + assertOppgaveTask(behandling.id, LagOppgaveTask.TYPE) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.VARSEL, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + ) + } + + @Test + fun `opprettBehandling skal opprette automatisk behandling uten varsel`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = false, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + assertBehandling(behandling, opprettTilbakekrevingRequest) + assertFagsak(behandling, opprettTilbakekrevingRequest) + assertFagsystemsbehandling(behandling, opprettTilbakekrevingRequest) + behandling.varsler.shouldBeEmpty() + behandling.verger.shouldBeEmpty() + assertHistorikkTask(behandling.id, TilbakekrevingHistorikkinnslagstype.BEHANDLING_OPPRETTET, Aktør.VEDTAKSLØSNING) + assertFinnKravgrunnlagTask(behandling.id) + assertOppgaveTask(behandling.id, LagOppgaveTask.TYPE) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.GRUNNLAG, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + ) + } + + @Test + fun `opprettBehandling skal opprette automatisk behandling fagsak med institusjon`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + finnesInstitusjon = true, + ) + + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + assertBehandling(behandling, opprettTilbakekrevingRequest) + assertFagsak(behandling, opprettTilbakekrevingRequest, true) + assertFagsystemsbehandling(behandling, opprettTilbakekrevingRequest) + assertVarselData(behandling, opprettTilbakekrevingRequest) + behandling.verger.shouldBeEmpty() + assertHistorikkTask(behandling.id, TilbakekrevingHistorikkinnslagstype.BEHANDLING_OPPRETTET, Aktør.VEDTAKSLØSNING) + assertFinnKravgrunnlagTask(behandling.id) + assertOppgaveTask(behandling.id, LagOppgaveTask.TYPE) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.VARSEL, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + ) + } + + @Test + fun `opprettBehandling skal legge inn manuellBrevmottaker fra request og autoutføre behandlingssteg BREVMOTTAKER`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + finnesInstitusjon = false, + finnesManuelleBrevmottakere = true, + ) + + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + val manuelleBrevmottakere = manuellBrevmottakerRepository.findByBehandlingId(behandling.id) + manuelleBrevmottakere.shouldHaveSize(1) + manuelleBrevmottakere.first().navn shouldBe "Kari Nordmann" + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.BREVMOTTAKER, + Behandlingsstegstatus.AUTOUTFØRT, + ) + } + + @Test + fun `opprettBehandling oppretter ikke behandling når det finnes åpen tilbakekreving for samme eksternFagsakId`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = true, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + val exception = shouldThrow { + behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + } + exception.message shouldBe "Det finnes allerede en åpen behandling for ytelsestype=" + + opprettTilbakekrevingRequest.ytelsestype + + " og eksternFagsakId=${opprettTilbakekrevingRequest.eksternFagsakId}, " + + "kan ikke opprette en ny." + } + + @Test + fun `opprettBehandling skal ikke opprette automatisk behandling når siste tilbakekreving er ikke henlagt`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = true, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(lagretBehandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val exception = shouldThrow { + behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + } + exception.message shouldBe + "Det finnes allerede en avsluttet behandling for ytelsestype=" + opprettTilbakekrevingRequest.ytelsestype + + " og eksternFagsakId=${opprettTilbakekrevingRequest.eksternFagsakId} " + + "som ikke er henlagt, kan ikke opprette en ny." + } + + @Test + fun `opprettBehandling skal opprette automatisk behandling når siste tilbakekreving er ikke henlagt og toggelen er på`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = true, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandlingRepository = mockk() + val fagsakService = mockk() + val taskService = mockk(relaxed = true) + val brevSporingService = mockk(relaxed = true) + val manuellBrevmottakerRepository = mockk(relaxed = true) + val kravgrunnlagRepository = mockk(relaxed = true) + val økonomiXmlMottattRepository = mockk<ØkonomiXmlMottattRepository>(relaxed = true) + val behandlingskontrollService = mockk(relaxed = true) + val behandlingstilstandService = mockk(relaxed = true) + val tellerService = mockk(relaxed = true) + val stegService = mockk(relaxed = true) + val oppgaveTaskService = mockk(relaxed = true) + val historikkTaskService = mockk(relaxed = true) + val tilgangService = mockk(relaxed = true) + val integrasjonerClient = mockk(relaxed = true) + val featureToggleService = mockk() + + val behandlingServiceMock = BehandlingService( + behandlingRepository, fagsakService, + taskService, brevSporingService, manuellBrevmottakerRepository, kravgrunnlagRepository, økonomiXmlMottattRepository, + behandlingskontrollService, behandlingstilstandService, tellerService, stegService, oppgaveTaskService, + historikkTaskService, tilgangService, 6, integrasjonerClient, featureToggleService, + ) + every { featureToggleService.isEnabled(any()) } returns true // default toggelen er av + every { behandlingRepository.finnÅpenTilbakekrevingsbehandling(any(), any()) } returns null + every { behandlingRepository.finnAvsluttetTilbakekrevingsbehandlinger(any()) } returns listOf(Testdata.behandling) + every { behandlingRepository.insert(any()) } returns Testdata.behandling + every { fagsakService.finnFagsak(any(), any()) } returns null + every { fagsakService.opprettFagsak(any(), any(), any()) } returns Testdata.fagsak + + val behandling = shouldNotThrowAny { behandlingServiceMock.opprettBehandling(opprettTilbakekrevingRequest) } + behandling.shouldNotBeNull() + } + + @Test + fun `opprettBehandling skal opprette behandling når siste tilbakekreving er henlagt og toggelen er av`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = true, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandlingRepository = mockk() + val fagsakService = mockk() + val taskService = mockk(relaxed = true) + val brevSporingService = mockk(relaxed = true) + val manuellBrevmottakerRepository = mockk(relaxed = true) + val kravgrunnlagRepository = mockk(relaxed = true) + val økonomiXmlMottattRepository = mockk<ØkonomiXmlMottattRepository>(relaxed = true) + val behandlingskontrollService = mockk(relaxed = true) + val behandlingstilstandService = mockk(relaxed = true) + val tellerService = mockk(relaxed = true) + val stegService = mockk(relaxed = true) + val oppgaveTaskService = mockk(relaxed = true) + val historikkTaskService = mockk(relaxed = true) + val tilgangService = mockk(relaxed = true) + val integrasjonerClient = mockk(relaxed = true) + val featureToggleService = mockk() + + val behandlingServiceMock = BehandlingService( + behandlingRepository, fagsakService, + taskService, brevSporingService, manuellBrevmottakerRepository, kravgrunnlagRepository, økonomiXmlMottattRepository, + behandlingskontrollService, behandlingstilstandService, tellerService, stegService, oppgaveTaskService, + historikkTaskService, tilgangService, 6, integrasjonerClient, featureToggleService, + ) + every { featureToggleService.isEnabled(any()) } returns false // default toggelen er av + every { behandlingRepository.finnÅpenTilbakekrevingsbehandling(any(), any()) } returns null + every { behandlingRepository.finnAvsluttetTilbakekrevingsbehandlinger(any()) } returns listOf(Testdata.behandling.copy(resultater = setOf(Testdata.behandlingsresultat.copy(type = Behandlingsresultatstype.HENLAGT_KRAVGRUNNLAG_NULLSTILT)))) + every { behandlingRepository.insert(any()) } returns Testdata.behandling.copy(resultater = setOf(Testdata.behandlingsresultat.copy(type = Behandlingsresultatstype.HENLAGT_KRAVGRUNNLAG_NULLSTILT))) + every { fagsakService.finnFagsak(any(), any()) } returns null + every { fagsakService.opprettFagsak(any(), any(), any()) } returns Testdata.fagsak + + val behandling = shouldNotThrowAny { behandlingServiceMock.opprettBehandling(opprettTilbakekrevingRequest) } + behandling.shouldNotBeNull() + } + + @Test + fun `opprettBehandling skal opprette automatisk når det allerede finnes avsluttet behandling for samme fagsak`() { + val forrigeOpprettTilbakekrevingRequest = lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg + .OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val forrigeBehandling = behandlingService.opprettBehandling(forrigeOpprettTilbakekrevingRequest) + + val lagretBehandling = behandlingRepository.findByIdOrThrow(forrigeBehandling.id) + behandlingRepository.update(lagretBehandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + // oppretter ny behandling for en annen eksternId + val nyOpprettTilbakekrevingRequest = lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg + .OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(nyOpprettTilbakekrevingRequest) + assertBehandling(behandling, nyOpprettTilbakekrevingRequest) + assertFagsak(behandling, nyOpprettTilbakekrevingRequest) + assertFagsystemsbehandling(behandling, nyOpprettTilbakekrevingRequest) + assertVarselData(behandling, nyOpprettTilbakekrevingRequest) + behandling.verger.shouldBeEmpty() + assertHistorikkTask(behandling.id, TilbakekrevingHistorikkinnslagstype.BEHANDLING_OPPRETTET, Aktør.VEDTAKSLØSNING) + assertFinnKravgrunnlagTask(behandling.id) + assertOppgaveTask(behandling.id, LagOppgaveTask.TYPE) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.VARSEL, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + ) + } + + @Test + fun `opprettBehandling skal ikke opprette manuelt når det ikke finnes kravgrunnlag for samme fagsak,ytelsestype,eksternId`() { + val opprettTilbakekrevingRequest = lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = false, + manueltOpprettet = true, + tilbakekrevingsvalg = Tilbakekrevingsvalg + .OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + + val exception = shouldThrow { behandlingService.opprettBehandling(opprettTilbakekrevingRequest) } + exception.message shouldBe "Det finnes intet kravgrunnlag for ytelsestype=${opprettTilbakekrevingRequest.ytelsestype}," + + "eksternFagsakId=${opprettTilbakekrevingRequest.eksternFagsakId} " + + "og eksternId=${opprettTilbakekrevingRequest.eksternId}. " + + "Tilbakekrevingsbehandling kan ikke opprettes manuelt." + } + + @Test + fun `opprettBehandling skal opprette manuelt når det finnes kravgrunnlag for samme fagsak,ytelsestype,eksternId`() { + val opprettTilbakekrevingRequest = lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = false, + manueltOpprettet = true, + tilbakekrevingsvalg = Tilbakekrevingsvalg + .OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + val økonomiXmlMottatt = Testdata.økonomiXmlMottatt + økonomiXmlMottattRepository.insert( + økonomiXmlMottatt.copy( + eksternFagsakId = opprettTilbakekrevingRequest.eksternFagsakId, + ytelsestype = opprettTilbakekrevingRequest.ytelsestype, + referanse = opprettTilbakekrevingRequest.eksternId, + ), + ) + + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + assertBehandling(behandling, opprettTilbakekrevingRequest, true) + assertFagsak(behandling, opprettTilbakekrevingRequest) + assertFagsystemsbehandling(behandling, opprettTilbakekrevingRequest) + behandling.varsler.shouldBeEmpty() + behandling.verger.shouldBeEmpty() + assertHistorikkTask(behandling.id, TilbakekrevingHistorikkinnslagstype.BEHANDLING_OPPRETTET, Aktør.VEDTAKSLØSNING) + assertFinnKravgrunnlagTask(behandling.id) + assertOppgaveTask(behandling.id, LagOppgaveTask.TYPE) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.GRUNNLAG, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + ) + } + + @Test + fun `opprettBehandlingManuellTask skal feile hvis det ikke finnes kravgrunnlag`() { + shouldThrow { + behandlingService.opprettBehandlingManuellTask( + OpprettManueltTilbakekrevingRequest( + eksternFagsakId = "testverdi", + ytelsestype = BARNETRYGD, + eksternId = "testverdi", + ), + ) + } + } + + @Test + fun `opprettBehandlingManuellTask skal opprette OpprettBehandlingManueltTask`() { + val økonomiXmlMottatt = Testdata.økonomiXmlMottatt + økonomiXmlMottattRepository.insert( + økonomiXmlMottatt.copy( + eksternFagsakId = "testverdi", + ytelsestype = BARNETRYGD, + referanse = "testverdi", + ), + ) + + behandlingService.opprettBehandlingManuellTask( + OpprettManueltTilbakekrevingRequest( + eksternFagsakId = "testverdi", + ytelsestype = BARNETRYGD, + eksternId = "testverdi", + ), + ) + + val taskene = taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)) + taskene.size shouldBe 1 + val task = taskene[0] + task.type shouldBe OpprettBehandlingManueltTask.TYPE + task.metadata["eksternFagsakId"] shouldBe "testverdi" + task.metadata["ytelsestype"] shouldBe BARNETRYGD.name + task.metadata["eksternId"] shouldBe "testverdi" + task.metadata["ansvarligSaksbehandler"] shouldBe "Z0000" + } + + @Test + fun `opprettRevurdering skal opprette revurdering for gitt avsluttet tilbakekrevingsbehandling`() { + fagsakRepository.insert(Testdata.fagsak) + var behandling = behandlingRepository.insert(Testdata.behandling) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + var revurdering = behandlingService.opprettRevurdering(lagOpprettRevurderingDto(behandling.id)) + revurdering = behandlingRepository.findByIdOrThrow(revurdering.id) + revurdering.type shouldBe Behandlingstype.REVURDERING_TILBAKEKREVING + revurdering.sisteÅrsak?.type shouldBe Behandlingsårsakstype.REVURDERING_OPPLYSNINGER_OM_VILKÅR + revurdering.status shouldBe Behandlingsstatus.UTREDES + revurdering.behandlendeEnhet shouldBe behandling.behandlendeEnhet + revurdering.behandlendeEnhetsNavn shouldBe behandling.behandlendeEnhetsNavn + behandling.manueltOpprettet.shouldBeFalse() + + val aktivFagsystemsbehandling = revurdering.aktivFagsystemsbehandling + aktivFagsystemsbehandling.tilbakekrevingsvalg shouldBe behandling.aktivFagsystemsbehandling.tilbakekrevingsvalg + aktivFagsystemsbehandling.revurderingsvedtaksdato shouldBe behandling.aktivFagsystemsbehandling.revurderingsvedtaksdato + aktivFagsystemsbehandling.eksternId shouldBe behandling.aktivFagsystemsbehandling.eksternId + aktivFagsystemsbehandling.årsak shouldBe behandling.aktivFagsystemsbehandling.årsak + aktivFagsystemsbehandling.resultat shouldBe behandling.aktivFagsystemsbehandling.resultat + assertHistorikkTask(revurdering.id, TilbakekrevingHistorikkinnslagstype.BEHANDLING_OPPRETTET, Aktør.SAKSBEHANDLER) + assertHistorikkTask( + revurdering.id, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT, + Aktør.VEDTAKSLØSNING, + "Venter på kravgrunnlag fra økonomi", + ) + taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)).shouldHaveSingleElement { + HentKravgrunnlagTask.TYPE == it.type && + revurdering.id.toString() == it.payload + } + val behandlingsstegstilstand = behandlingskontrollService.finnAktivStegstilstand(revurdering.id) + behandlingsstegstilstand.shouldNotBeNull() + behandlingsstegstilstand.behandlingssteg shouldBe Behandlingssteg.GRUNNLAG + behandlingsstegstilstand.behandlingsstegsstatus shouldBe Behandlingsstegstatus.VENTER + } + + @Test + fun `opprettRevurdering skal ikke opprette revurdering for tilbakekreving som er avsluttet uten kravgrunnlag`() { + fagsakRepository.insert(Testdata.fagsak) + var behandling = behandlingRepository.insert(Testdata.behandling) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val exception = shouldThrow { + behandlingService.opprettRevurdering(lagOpprettRevurderingDto(behandling.id)) + } + exception.message shouldBe "Revurdering kan ikke opprettes for behandling ${behandling.id}. " + + "Enten behandlingen er ikke avsluttet med kravgrunnlag eller " + + "det finnes allerede en åpen revurdering" + } + + @Test + fun `hentBehandling skal hente behandling som opprettet uten varsel`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = true, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + val behandlingDto = behandlingService.hentBehandling(behandling.id) + + assertFellesBehandlingRespons(behandlingDto, behandling) + behandlingDto.kanHenleggeBehandling.shouldBeFalse() + behandlingDto.harVerge.shouldBeTrue() + behandlingDto.harVerge.shouldBeTrue() + behandlingDto.erBehandlingPåVent.shouldBeTrue() + behandlingDto.kanEndres.shouldBeTrue() + assertBehandlingsstegsinfo( + behandlingDto = behandlingDto, + behandling = behandling, + behandlingssteg = Behandlingssteg.GRUNNLAG, + behandlingsstegstatus = Behandlingsstegstatus.VENTER, + venteårsak = Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + ) + } + + @Test + fun `hentBehandling skal hente behandling som ikke kan henlegges med verge`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = true, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + val behandlingDto = behandlingService.hentBehandling(behandling.id) + + assertFellesBehandlingRespons(behandlingDto, behandling) + behandlingDto.kanHenleggeBehandling.shouldBeFalse() + behandlingDto.harVerge.shouldBeTrue() + behandlingDto.erBehandlingPåVent.shouldBeTrue() + behandlingDto.kanEndres.shouldBeTrue() + assertBehandlingsstegsinfo( + behandlingDto = behandlingDto, + behandling = behandling, + behandlingssteg = Behandlingssteg.VARSEL, + behandlingsstegstatus = Behandlingsstegstatus.VENTER, + venteårsak = Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + ) + } + + @Test + fun `hentBehandling skal hente behandling som kan henlegges uten verge`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + val sporbar = behandling.sporbar.copy(opprettetTid = LocalDate.now().minusDays(10).atStartOfDay()) + val oppdatertBehandling = lagretBehandling.copy(sporbar = sporbar) + behandlingRepository.update(oppdatertBehandling) + + val behandlingDto = behandlingService.hentBehandling(behandling.id) + + assertFellesBehandlingRespons(behandlingDto, oppdatertBehandling) + behandlingDto.kanHenleggeBehandling.shouldBeTrue() + behandlingDto.harVerge.shouldBeFalse() + behandlingDto.erBehandlingPåVent.shouldBeTrue() + behandlingDto.kanEndres.shouldBeTrue() + assertBehandlingsstegsinfo( + behandlingDto = behandlingDto, + behandling = behandling, + behandlingssteg = Behandlingssteg.VARSEL, + behandlingsstegstatus = Behandlingsstegstatus.VENTER, + venteårsak = Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + ) + } + + @Test + fun `hentBehandling skal hente behandling når behandling er avsluttet`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(lagretBehandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val behandlingDto = behandlingService.hentBehandling(behandling.id) + + behandlingDto.kanEndres.shouldBeFalse() + behandlingDto.kanHenleggeBehandling.shouldBeFalse() + } + + @Test + fun `hentBehandling skal ikke endre behandling av veileder`() { + every { ContextService.hentHøyesteRolletilgangOgYtelsestypeForInnloggetBruker(any(), any()) } + .returns(InnloggetBrukertilgang(mapOf(Tilgangskontrollsfagsystem.BARNETRYGD to Behandlerrolle.VEILEDER))) + + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + val behandlingDto = behandlingService.hentBehandling(behandling.id) + + behandlingDto.kanEndres.shouldBeFalse() + } + + @Test + fun `hentBehandling skal ikke endre behandling av forvalter`() { + every { ContextService.hentHøyesteRolletilgangOgYtelsestypeForInnloggetBruker(any(), any()) } + .returns(InnloggetBrukertilgang(mapOf(Tilgangskontrollsfagsystem.FORVALTER_TILGANG to Behandlerrolle.FORVALTER))) + + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + val behandlingDto = behandlingService.hentBehandling(behandling.id) + + behandlingDto.kanEndres.shouldBeFalse() + } + + @Test + fun `hentBehandling skal endre behandling av saksbehandler`() { + every { ContextService.hentHøyesteRolletilgangOgYtelsestypeForInnloggetBruker(any(), any()) } + .returns( + InnloggetBrukertilgang( + mapOf( + Tilgangskontrollsfagsystem.FORVALTER_TILGANG to Behandlerrolle.FORVALTER, + Tilgangskontrollsfagsystem.BARNETRYGD to Behandlerrolle.SAKSBEHANDLER, + ), + ), + ) + + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + val behandlingDto = behandlingService.hentBehandling(behandling.id) + + behandlingDto.kanEndres.shouldBeTrue() + } + + @Test + fun `hentBehandling skal ikke endre behandling av saksbehandler når behandling er på fattevedtak steg`() { + every { ContextService.hentHøyesteRolletilgangOgYtelsestypeForInnloggetBruker(any(), any()) } + .returns(InnloggetBrukertilgang(mapOf(Tilgangskontrollsfagsystem.BARNETRYGD to Behandlerrolle.SAKSBEHANDLER))) + + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(lagretBehandling.copy(status = Behandlingsstatus.FATTER_VEDTAK)) + + val behandlingDto = behandlingService.hentBehandling(behandling.id) + + behandlingDto.kanEndres.shouldBeFalse() + } + + @Test + fun `hentBehandling skal endre behandling av beslutter når behandling er på fattevedtak steg`() { + every { ContextService.hentHøyesteRolletilgangOgYtelsestypeForInnloggetBruker(any(), any()) } + .returns(InnloggetBrukertilgang(mapOf(Tilgangskontrollsfagsystem.BARNETRYGD to Behandlerrolle.BESLUTTER))) + + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update( + lagretBehandling.copy( + status = Behandlingsstatus.FATTER_VEDTAK, + ansvarligSaksbehandler = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + ), + ) + + val behandlingDto = behandlingService.hentBehandling(behandling.id) + + behandlingDto.kanEndres.shouldBeTrue() + } + + @Test + fun `hentBehandling skal ikke endre behandling med fattevedtak steg og beslutter er samme som saksbehandler`() { + every { ContextService.hentHøyesteRolletilgangOgYtelsestypeForInnloggetBruker(any(), any()) } + .returns(InnloggetBrukertilgang(mapOf(Tilgangskontrollsfagsystem.BARNETRYGD to Behandlerrolle.BESLUTTER))) + + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update( + lagretBehandling.copy( + status = Behandlingsstatus.FATTER_VEDTAK, + ansvarligSaksbehandler = "Z0000", + ), + ) + + val behandlingDto = behandlingService.hentBehandling(behandling.id) + + behandlingDto.kanEndres.shouldBeFalse() + } + + @Test + fun `hentBehandling kan ikke opprette revurdering når tilbakekreving ikke har kravgrunnlag`() { + fagsakRepository.insert(Testdata.fagsak) + var behandling = behandlingRepository.insert(Testdata.behandling) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val behandlingDto = behandlingService.hentBehandling(behandling.id) + behandlingDto.kanRevurderingOpprettes.shouldBeFalse() + } + + @Test + fun `hentBehandling kan opprette revurdering når tilbakekreving er avsluttet med kravgrunnlag`() { + fagsakRepository.insert(Testdata.fagsak) + var behandling = behandlingRepository.insert(Testdata.behandling) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val behandlingDto = behandlingService.hentBehandling(behandling.id) + behandlingDto.kanRevurderingOpprettes.shouldBeTrue() + } + + @Test + fun `hentBehandling kan ikke opprette revurdering når tilbakekreving har en åpen revurdering`() { + fagsakRepository.insert(Testdata.fagsak) + var behandling = behandlingRepository.insert(Testdata.behandling) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + behandlingRepository.insert(Testdata.revurdering) + + val behandlingDto = behandlingService.hentBehandling(behandling.id) + behandlingDto.kanRevurderingOpprettes.shouldBeFalse() + } + + @Test + fun `hentBehandling kan opprette revurdering når tilbakekreving har en avsluttet revurdering`() { + fagsakRepository.insert(Testdata.fagsak) + var behandling = behandlingRepository.insert(Testdata.behandling) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + var revurdering = behandlingRepository.insert(Testdata.revurdering) + revurdering = behandlingRepository.findByIdOrThrow(revurdering.id) + behandlingRepository.update(revurdering.copy(status = Behandlingsstatus.AVSLUTTET)) + + val behandlingDto = behandlingService.hentBehandling(behandling.id) + behandlingDto.kanRevurderingOpprettes.shouldBeTrue() + } + + @Test + fun `settBehandlingPåVent skal ikke sett behandling på vent hvis fristdato er mindre enn i dag`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = true, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + val exception = shouldThrow { + behandlingService.settBehandlingPåVent( + behandling.id, + BehandlingPåVentDto( + venteårsak = Venteårsak.ENDRE_TILKJENT_YTELSE, + tidsfrist = LocalDate.now().minusDays(4), + ), + ) + } + exception.message shouldBe "Fristen må være større enn dagens dato for behandling ${behandling.id}" + } + + @Test + fun `settBehandlingPåVent skal ikke sett behandling på vent hvis fristdato er i dag`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = true, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + val exception = shouldThrow { + behandlingService.settBehandlingPåVent( + behandling.id, + BehandlingPåVentDto( + venteårsak = Venteårsak.ENDRE_TILKJENT_YTELSE, + tidsfrist = LocalDate.now(), + ), + ) + } + exception.message shouldBe "Fristen må være større enn dagens dato for behandling ${behandling.id}" + } + + @Test + fun `settBehandlingPåVent skal sette behandling på vent hvis fristdato er større enn i dag`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = true, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + val behandlingPåVentDto = BehandlingPåVentDto( + venteårsak = Venteårsak.ENDRE_TILKJENT_YTELSE, + tidsfrist = LocalDate.now().plusDays(1), + ) + + behandlingService.settBehandlingPåVent(behandling.id, behandlingPåVentDto) + + behandlingskontrollService.erBehandlingPåVent(behandling.id).shouldBeTrue() + assertAnsvarligSaksbehandler(behandling) + assertHistorikkTask( + behandling.id, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT, + Aktør.SAKSBEHANDLER, + Venteårsak.ENDRE_TILKJENT_YTELSE.beskrivelse, + ) + assertOppgaveTask( + behandling.id, + OppdaterOppgaveTask.TYPE, + "Frist er oppdatert av saksbehandler Z0000", + behandlingPåVentDto.tidsfrist, + ) + } + + @Test + fun `taBehandlingAvvent skal ikke gjenoppta når behandling er avsluttet`() { + fagsakRepository.insert(Testdata.fagsak) + val behandling = behandlingRepository.insert(Testdata.behandling) + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(lagretBehandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val exception = shouldThrow { behandlingService.taBehandlingAvvent(lagretBehandling.id) } + exception.message shouldBe "Behandling med id=${lagretBehandling.id} er allerede ferdig behandlet." + } + + @Test + fun `taBehandlingAvvent skal ikke gjenoppta når behandling er ikke på vent`() { + fagsakRepository.insert(Testdata.fagsak) + val behandling = behandlingRepository.insert(Testdata.behandling) + + val exception = shouldThrow { behandlingService.taBehandlingAvvent(behandling.id) } + exception.message shouldBe "Behandling ${behandling.id} er ikke på vent, kan ike gjenoppta" + } + + @Test + fun `taBehandlingAvvent skal gjenoppta når behandling er på vent`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = true, + finnesVarsel = false, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431.copy(behandlingId = behandling.id)) + + behandlingService.settBehandlingPåVent( + behandlingId = behandling.id, + behandlingPåVentDto = BehandlingPåVentDto( + Venteårsak.AVVENTER_DOKUMENTASJON, + LocalDate.now().plusDays(2), + ), + ) + + behandlingskontrollService.erBehandlingPåVent(behandling.id).shouldBeTrue() + + behandlingService.taBehandlingAvvent(behandling.id) + + behandlingskontrollService.erBehandlingPåVent(behandling.id).shouldBeFalse() + assertAnsvarligSaksbehandler(behandling) + assertHistorikkTask(behandling.id, TilbakekrevingHistorikkinnslagstype.BEHANDLING_GJENOPPTATT, Aktør.SAKSBEHANDLER) + assertOppgaveTask( + behandling.id, + OppdaterOppgaveTask.TYPE, + "Behandling er tatt av vent", + LocalDate.now(), + ) + } + + @Test + fun `taBehandlingAvvent skal gjenoppta behandling og hoppe til FAKTA steg når behandling venter på bruker med grunnlag`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = true, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + var behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.shouldHaveSingleElement { + it.behandlingssteg == Behandlingssteg.VARSEL && + it.behandlingsstegsstatus == Behandlingsstegstatus.VENTER && + it.venteårsak == Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING + } + + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431.copy(behandlingId = behandling.id)) + + behandlingService.taBehandlingAvvent(behandling.id) + + behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.shouldHaveSingleElement { + it.behandlingssteg == Behandlingssteg.VARSEL && + it.behandlingsstegsstatus == Behandlingsstegstatus.UTFØRT + } + behandlingsstegstilstand.shouldHaveSingleElement { + it.behandlingssteg == Behandlingssteg.FAKTA && + it.behandlingsstegsstatus == Behandlingsstegstatus.KLAR + } + behandlingsstegstilstand.any { it.behandlingssteg == Behandlingssteg.GRUNNLAG }.shouldBeFalse() + + behandlingskontrollService.erBehandlingPåVent(behandling.id).shouldBeFalse() + assertAnsvarligSaksbehandler(behandling) + assertHistorikkTask(behandling.id, TilbakekrevingHistorikkinnslagstype.BEHANDLING_GJENOPPTATT, Aktør.SAKSBEHANDLER) + assertOppgaveTask( + behandling.id, + OppdaterOppgaveTask.TYPE, + "Behandling er tatt av vent", + LocalDate.now(), + ) + } + + @Test + fun `taBehandlingAvvent skal gjenoppta behandling og venter på GRUNNLAG steg når behandling venter på bruker`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = true, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + var behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.shouldHaveSingleElement { + it.behandlingssteg == Behandlingssteg.VARSEL && + it.behandlingsstegsstatus == Behandlingsstegstatus.VENTER && + it.venteårsak == Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING + } + + behandlingService.taBehandlingAvvent(behandling.id) + + behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.shouldHaveSingleElement { + it.behandlingssteg == Behandlingssteg.VARSEL && + it.behandlingsstegsstatus == Behandlingsstegstatus.UTFØRT + } + behandlingsstegstilstand.shouldHaveSingleElement { + it.behandlingssteg == Behandlingssteg.GRUNNLAG && + it.behandlingsstegsstatus == Behandlingsstegstatus.VENTER + } + + behandlingskontrollService.erBehandlingPåVent(behandling.id).shouldBeTrue() + assertAnsvarligSaksbehandler(behandling) + assertHistorikkTask(behandling.id, TilbakekrevingHistorikkinnslagstype.BEHANDLING_GJENOPPTATT, Aktør.SAKSBEHANDLER) + assertOppgaveTask( + behandling.id, + OppdaterOppgaveTask.TYPE, + "Behandling er tatt av vent", + LocalDate.now(), + ) + assertOppgaveTask( + behandling.id, + OppdaterOppgaveTask.TYPE, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG.beskrivelse, + LocalDate.now().plusWeeks(Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG.defaultVenteTidIUker), + ) + } + + @Test + fun `henleggBehandling skal henlegge behandling og sende henleggelsesbrev`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = true, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + var behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + // oppdaterer opprettettidspunkt slik at behandlingen kan henlegges + behandlingRepository.update( + behandling.copy( + sporbar = Sporbar( + opprettetAv = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + opprettetTid = LocalDateTime.now().minusDays(10), + ), + ), + ) + // sender varselsbrev + brevsporingRepository.insert( + Brevsporing( + behandlingId = behandling.id, + journalpostId = "testverdi", + dokumentId = "testverdi", + brevtype = Brevtype.VARSEL, + ), + ) + behandlingService.taBehandlingAvvent(behandlingId = behandling.id) + + behandlingService.henleggBehandling( + behandling.id, + HenleggelsesbrevFritekstDto( + Behandlingsresultatstype.HENLAGT_FEILOPPRETTET, + "testverdi", + ), + ) + + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandling.status shouldBe Behandlingsstatus.AVSLUTTET + behandling.avsluttetDato shouldBe LocalDate.now() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 2 + behandlingsstegstilstand.filter { it.behandlingssteg == Behandlingssteg.VARSEL }.size shouldBe 1 + behandlingsstegstilstand.first { it.behandlingssteg == Behandlingssteg.VARSEL }.behandlingsstegsstatus shouldBe Behandlingsstegstatus.UTFØRT + behandlingsstegstilstand.filter { it.behandlingssteg == Behandlingssteg.GRUNNLAG }.size shouldBe 1 + behandlingsstegstilstand.first { it.behandlingssteg == Behandlingssteg.GRUNNLAG }.behandlingsstegsstatus shouldBe Behandlingsstegstatus.AVBRUTT + + val behandlingssresultat = behandling.sisteResultat + behandlingssresultat.shouldNotBeNull() + behandlingssresultat.type shouldBe Behandlingsresultatstype.HENLAGT_FEILOPPRETTET + + taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)).any { it.type == SendHenleggelsesbrevTask.TYPE }.shouldBeTrue() + assertHistorikkTask( + behandling.id, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_HENLAGT, + Aktør.SAKSBEHANDLER, + "testverdi", + ) + assertOppgaveTask(behandling.id, FerdigstillOppgaveTask.TYPE) + } + + @Test + fun `henleggBehandling skal henlegge behandling uten henleggelsesbrev`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = false, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + var behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + // oppdaterer opprettettidspunkt slik at behandlingen kan henlegges + behandlingRepository.update( + behandling.copy( + sporbar = Sporbar( + opprettetAv = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + opprettetTid = LocalDateTime.now().minusDays(10), + ), + ), + ) + + behandlingService.henleggBehandling( + behandling.id, + HenleggelsesbrevFritekstDto( + Behandlingsresultatstype.HENLAGT_TEKNISK_VEDLIKEHOLD, + "testverdi", + ), + ) + + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandling.status shouldBe Behandlingsstatus.AVSLUTTET + behandling.avsluttetDato shouldBe LocalDate.now() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 1 + behandlingsstegstilstand[0].behandlingssteg shouldBe Behandlingssteg.GRUNNLAG + behandlingsstegstilstand[0].behandlingsstegsstatus shouldBe Behandlingsstegstatus.AVBRUTT + + val behandlingssresultat = behandling.sisteResultat + behandlingssresultat.shouldNotBeNull() + behandlingssresultat.type shouldBe Behandlingsresultatstype.HENLAGT_TEKNISK_VEDLIKEHOLD + + taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)).find { task -> task.type == SendHenleggelsesbrevTask.TYPE }.shouldBeNull() + assertHistorikkTask( + behandling.id, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_HENLAGT, + Aktør.VEDTAKSLØSNING, + "testverdi", + ) + assertOppgaveTask(behandling.id, FerdigstillOppgaveTask.TYPE) + } + + @Test + fun `henleggBehandling skal ikke henlegge behandling som opprettet nå`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = false, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + + val exception = shouldThrow { + behandlingService + .henleggBehandling( + behandling.id, + HenleggelsesbrevFritekstDto( + Behandlingsresultatstype.HENLAGT_TEKNISK_VEDLIKEHOLD, + "testverdi", + ), + ) + } + exception.message shouldBe "Behandling med behandlingId=${behandling.id} kan ikke henlegges." + } + + @Test + fun `henleggBehandling skal ikke henlegge behandling som har aktivt kravgrunnlag`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = false, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + val kravgrunnlag = Testdata.kravgrunnlag431 + kravgrunnlagRepository.insert(kravgrunnlag.copy(behandlingId = behandling.id)) + + val exception = shouldThrow { + behandlingService + .henleggBehandling( + behandling.id, + HenleggelsesbrevFritekstDto( + Behandlingsresultatstype.HENLAGT_TEKNISK_VEDLIKEHOLD, + "testverdi", + ), + ) + } + exception.message shouldBe "Behandling med behandlingId=${behandling.id} kan ikke henlegges." + } + + @Test + fun `henleggBehandling skal ikke henlegge behandling som er allerede avsluttet`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = false, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + var behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val exception = shouldThrow { + behandlingService + .henleggBehandling( + behandling.id, + HenleggelsesbrevFritekstDto( + Behandlingsresultatstype.HENLAGT_TEKNISK_VEDLIKEHOLD, + "testverdi", + ), + ) + } + exception.message shouldBe "Behandling med id=${behandling.id} er allerede ferdig behandlet." + } + + @Test + fun `byttBehandlendeEnhet skal bytte og oppdatere oppgave`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = false, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + var behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + + behandlingService.byttBehandlendeEnhet( + behandling.id, + ByttEnhetDto( + "4806", + "bytter i unittest" + + "\n\nmed linjeskift" + + "\n\nto til og med", + ), + ) + + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandling.behandlendeEnhet shouldBe "4806" + behandling.behandlendeEnhetsNavn shouldBe "Mock NAV Drammen" + + taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)).any { + it.type == OppdaterEnhetOppgaveTask.TYPE && + "Endret tildelt enhet: 4806" == it.metadata["beskrivelse"] && + "4806" == it.metadata["enhetId"] + }.shouldBeTrue() + assertHistorikkTask( + behandling.id, + TilbakekrevingHistorikkinnslagstype.ENDRET_ENHET, + Aktør.SAKSBEHANDLER, + "bytter i unittest med linjeskift to til og med", + ) + } + + @Test + fun `byttBehandlendeEnhet skal ikke kunne bytte på behandling med fagsystem EF`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = false, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + .copy( + fagsystem = Fagsystem.EF, + ytelsestype = BARNETILSYN, + ) + var behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + + val exception = shouldThrow { + behandlingService.byttBehandlendeEnhet(behandling.id, ByttEnhetDto("4806", "bytter i unittest")) + } + exception.message shouldBe "Ikke implementert for fagsystem EF" + } + + @Test + fun `byttBehandlendeEnhet skal ikke kunne bytte på avsluttet behandling`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesVerge = false, + finnesVarsel = false, + manueltOpprettet = false, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + var behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val exception = shouldThrow { + behandlingService.byttBehandlendeEnhet(behandling.id, ByttEnhetDto("4806", "bytter i unittest")) + } + exception.message shouldBe "Behandling med id=${behandling.id} er allerede ferdig behandlet." + } + + @Test + fun `Behandling på fagsak av type institusjon skal ikke støtte manuelle brevmottakere`() { + val opprettTilbakekrevingRequest = + lagOpprettTilbakekrevingRequest( + finnesInstitusjon = true, + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + + val behandling = behandlingService.opprettBehandling(opprettTilbakekrevingRequest) + val behandlingDto = behandlingService.hentBehandling(behandling.id) + + behandlingDto.støtterManuelleBrevmottakere shouldBe false + } + + private fun assertFellesBehandlingRespons( + behandlingDto: BehandlingDto, + behandling: Behandling, + ) { + behandlingDto.eksternBrukId shouldBe behandling.eksternBrukId + behandlingDto.erBehandlingHenlagt.shouldBeFalse() + behandlingDto.type shouldBe Behandlingstype.TILBAKEKREVING + behandlingDto.status shouldBe Behandlingsstatus.UTREDES + behandlingDto.opprettetDato shouldBe behandling.opprettetDato + behandlingDto.avsluttetDato.shouldBeNull() + behandlingDto.vedtaksdato.shouldBeNull() + behandlingDto.enhetskode shouldBe "8020" + behandlingDto.enhetsnavn shouldBe "Oslo" + behandlingDto.resultatstype.shouldBeNull() + behandlingDto.ansvarligSaksbehandler shouldBe "bb1234" + behandlingDto.ansvarligBeslutter.shouldBeNull() + behandlingDto.kanRevurderingOpprettes.shouldBeFalse() + } + + private fun assertBehandlingsstegsinfo( + behandlingDto: BehandlingDto, + behandling: Behandling, + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + venteårsak: Venteårsak, + ) { + val behandlingsstegsinfo: List = behandlingDto.behandlingsstegsinfo + behandlingsstegsinfo.size shouldBe 1 + behandlingsstegsinfo[0].behandlingssteg shouldBe behandlingssteg + behandlingsstegsinfo[0].behandlingsstegstatus shouldBe behandlingsstegstatus + behandlingsstegsinfo[0].venteårsak shouldBe venteårsak + behandlingsstegsinfo[0].tidsfrist shouldBe behandling.opprettetDato.plusWeeks(venteårsak.defaultVenteTidIUker) + } + + private fun assertBehandlingsstegstilstand( + behandlingsstegstilstand: List, + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + venteårsak: Venteårsak? = null, + ) { + behandlingsstegstilstand.any { + it.behandlingssteg == behandlingssteg && + it.behandlingsstegsstatus == behandlingsstegstatus + it.venteårsak == venteårsak + }.shouldBeTrue() + } + + private fun assertFagsak( + behandling: Behandling, + opprettTilbakekrevingRequest: OpprettTilbakekrevingRequest, + finnesInstitusjon: Boolean = false, + ) { + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + fagsak.eksternFagsakId shouldBe opprettTilbakekrevingRequest.eksternFagsakId + fagsak.ytelsestype.name shouldBe opprettTilbakekrevingRequest.ytelsestype.name + fagsak.fagsystem shouldBe opprettTilbakekrevingRequest.fagsystem + fagsak.bruker.språkkode shouldBe opprettTilbakekrevingRequest.språkkode + fagsak.bruker.ident shouldBe opprettTilbakekrevingRequest.personIdent + if (finnesInstitusjon) { + fagsak.institusjon shouldNotBe null + fagsak.institusjon!!.organisasjonsnummer shouldBe opprettTilbakekrevingRequest.institusjon!!.organisasjonsnummer + } else { + fagsak.institusjon shouldBe null + } + } + + private fun assertBehandling( + behandling: Behandling, + opprettTilbakekrevingRequest: OpprettTilbakekrevingRequest, + manueltOpprettet: Boolean? = false, + ) { + behandling.type.name shouldBe Behandlingstype.TILBAKEKREVING.name + behandling.status.name shouldBe Behandlingsstatus.OPPRETTET.name + behandling.manueltOpprettet shouldBe manueltOpprettet + behandling.behandlendeEnhet shouldBe opprettTilbakekrevingRequest.enhetId + behandling.behandlendeEnhetsNavn shouldBe opprettTilbakekrevingRequest.enhetsnavn + behandling.saksbehandlingstype.name shouldBe Saksbehandlingstype.ORDINÆR.name + behandling.opprettetDato shouldBe LocalDate.now() + } + + private fun assertFagsystemsbehandling( + behandling: Behandling, + opprettTilbakekrevingRequest: OpprettTilbakekrevingRequest, + ) { + val fagsystemsbehandlinger = behandling.fagsystemsbehandling + fagsystemsbehandlinger.size shouldBe 1 + val fagsystemsbehandling = fagsystemsbehandlinger.toList().first() + fagsystemsbehandling.aktiv shouldBe true + fagsystemsbehandling.eksternId shouldBe opprettTilbakekrevingRequest.eksternId + fagsystemsbehandling.tilbakekrevingsvalg shouldBe opprettTilbakekrevingRequest.faktainfo.tilbakekrevingsvalg + fagsystemsbehandling.revurderingsvedtaksdato shouldBe opprettTilbakekrevingRequest.revurderingsvedtaksdato + fagsystemsbehandling.resultat shouldBe "testresultat" + fagsystemsbehandling.årsak shouldBe "testverdi" + fagsystemsbehandling.konsekvenser.shouldBeEmpty() + } + + private fun assertVarselData( + behandling: Behandling, + opprettTilbakekrevingRequest: OpprettTilbakekrevingRequest, + ) { + val varsler = behandling.varsler + varsler.size shouldBe 1 + val varsel = varsler.toList().first() + opprettTilbakekrevingRequest.varsel?.let { + varsel.varseltekst shouldBe it.varseltekst + varsel.varselbeløp.toBigDecimal() shouldBe it.sumFeilutbetaling + varsel.perioder.size shouldBe it.perioder.size + varsel.perioder.toList().first().fom shouldBe it.perioder.first().fom + varsel.perioder.toList().first().tom shouldBe it.perioder.first().tom + } + } + + private fun assertVerge( + behandling: Behandling, + opprettTilbakekrevingRequest: OpprettTilbakekrevingRequest, + ) { + behandling.verger.shouldNotBeEmpty() + behandling.verger.size shouldBe 1 + val verge = behandling.verger.toList().first() + verge.type.navn shouldBe opprettTilbakekrevingRequest.verge?.vergetype?.navn + verge.navn shouldBe opprettTilbakekrevingRequest.verge?.navn + verge.orgNr shouldBe opprettTilbakekrevingRequest.verge?.organisasjonsnummer + verge.ident shouldBe opprettTilbakekrevingRequest.verge?.personIdent + } + + private fun lagOpprettTilbakekrevingRequest( + tilbakekrevingsvalg: Tilbakekrevingsvalg, + finnesVerge: Boolean = false, + finnesVarsel: Boolean = false, + manueltOpprettet: Boolean = false, + finnesInstitusjon: Boolean = false, + finnesManuelleBrevmottakere: Boolean = false, + ): OpprettTilbakekrevingRequest { + val varsel = if (finnesVarsel) { + Varsel( + varseltekst = "testverdi", + sumFeilutbetaling = BigDecimal.valueOf(1500L), + perioder = listOf(Periode(fom, tom)), + ) + } else { + null + } + val verge = if (finnesVerge) { + Verge( + vergetype = Vergetype.VERGE_FOR_BARN, + navn = "Andy", + personIdent = "321321321", + ) + } else { + null + } + + val faktainfo = Faktainfo( + revurderingsårsak = "testverdi", + revurderingsresultat = "testresultat", + tilbakekrevingsvalg = tilbakekrevingsvalg, + ) + val institusjon = + if (finnesInstitusjon) Institusjon(organisasjonsnummer = "987654321") else null + + val manuelleBrevmottakere = if (finnesManuelleBrevmottakere) { + setOf( + Brevmottaker( + type = MottakerType.DØDSBO, + navn = "Kari Nordmann", + manuellAdresseInfo = ManuellAdresseInfo( + "testadresse", + postnummer = "0000", + poststed = "OSLO", + landkode = "NO", + ), + ), + ) + } else { + emptySet() + } + + return OpprettTilbakekrevingRequest( + ytelsestype = BARNETRYGD, + fagsystem = Fagsystem.BA, + eksternFagsakId = "1234567", + personIdent = "321321322", + eksternId = UUID.randomUUID().toString(), + manueltOpprettet = manueltOpprettet, + språkkode = Språkkode.NN, + enhetId = "8020", + enhetsnavn = "Oslo", + varsel = varsel, + revurderingsvedtaksdato = fom, + verge = verge, + faktainfo = faktainfo, + saksbehandlerIdent = "Z0000", + institusjon = institusjon, + manuelleBrevmottakere = manuelleBrevmottakere, + ) + } + + private fun lagOpprettRevurderingDto(originalBehandlingId: UUID): OpprettRevurderingDto { + return OpprettRevurderingDto(BARNETRYGD, originalBehandlingId, Behandlingsårsakstype.REVURDERING_OPPLYSNINGER_OM_VILKÅR) + } + + private fun assertAnsvarligSaksbehandler(behandling: Behandling) { + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + lagretBehandling.ansvarligSaksbehandler shouldBe "Z0000" + lagretBehandling.ansvarligBeslutter.shouldBeNull() + } + + private fun assertHistorikkTask( + behandlingId: UUID, + historikkinnslagstype: TilbakekrevingHistorikkinnslagstype, + aktør: Aktør, + tekst: String? = null, + ) { + taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)).any { + LagHistorikkinnslagTask.TYPE == it.type && + historikkinnslagstype.name == it.metadata["historikkinnslagstype"] && + aktør.name == it.metadata["aktør"] && + behandlingId.toString() == it.payload && + tekst == it.metadata["beskrivelse"] + }.shouldBeTrue() + } + + private fun assertFinnKravgrunnlagTask(behandlingId: UUID) { + taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)).any { + FinnKravgrunnlagTask.TYPE == it.type && behandlingId.toString() == it.payload + }.shouldBeTrue() + } + + private fun assertOppgaveTask( + behandlingId: UUID, + taskType: String, + beskrivelse: String? = null, + frist: LocalDate? = null, + ) { + taskService.findAll().any { + it.type == taskType && + behandlingId.toString() == it.payload && + Oppgavetype.BehandleSak.value == it.metadata["oppgavetype"] + beskrivelse == it.metadata["beskrivelse"] && + frist == it.metadata["frist"]?.let { dato -> LocalDate.parse(dato as CharSequence) } + }.shouldBeTrue() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/FagsakRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/FagsakRepositoryTest.kt new file mode 100644 index 000000000..2c646c679 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/FagsakRepositoryTest.kt @@ -0,0 +1,40 @@ +package no.nav.familie.tilbake.behandling + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class FagsakRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + private val fagsak = Testdata.fagsak + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av Fagsak til basen`() { + fagsakRepository.insert(fagsak) + + val lagretFagsak = fagsakRepository.findByIdOrThrow(fagsak.id) + lagretFagsak.shouldBeEqualToComparingFieldsExcept(fagsak, Fagsak::sporbar, Fagsak::versjon) + lagretFagsak.versjon shouldBe 1 + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av Fagsak i basen`() { + fagsakRepository.insert(fagsak) + var lagretFagsak = fagsakRepository.findByIdOrThrow(fagsak.id) + val oppdatertFagsak = lagretFagsak.copy(eksternFagsakId = "1") + + fagsakRepository.update(oppdatertFagsak) + + lagretFagsak = fagsakRepository.findByIdOrThrow(fagsak.id) + lagretFagsak.shouldBeEqualToComparingFieldsExcept(oppdatertFagsak, Fagsak::sporbar, Fagsak::versjon) + lagretFagsak.versjon shouldBe 2 + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/FagsakServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/FagsakServiceTest.kt new file mode 100644 index 000000000..699ca9322 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/FagsakServiceTest.kt @@ -0,0 +1,304 @@ +package no.nav.familie.tilbake.behandling + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Bruker +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandling.domain.Institusjon +import no.nav.familie.tilbake.behandling.task.OpprettBehandlingManueltTask +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.integration.pdl.internal.Kjønn +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattRepository +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.web.client.exchange +import org.springframework.http.HttpEntity +import org.springframework.http.HttpMethod +import org.springframework.http.ResponseEntity +import org.springframework.web.util.UriComponentsBuilder +import java.time.LocalDate +import java.util.Properties +import java.util.UUID + +internal class FagsakServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var økonomiXmlMottattRepository: ØkonomiXmlMottattRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var fagsakService: FagsakService + + @Test + fun test() { + headers.setBearerAuth(lokalTestToken()) + val uriHentSaksnummer = UriComponentsBuilder.fromHttpUrl(localhost("/api/fagsystem/EF/fagsak/123456/v1")).toUriString() + + val response: ResponseEntity>> = restTemplate.exchange( + uriHentSaksnummer, + HttpMethod.GET, + HttpEntity(headers), + ) + + println(response) + } + + @Test + fun `hentFagsak skal hente fagsak for barnetrygd`() { + val eksternFagsakId = UUID.randomUUID().toString() + val behandling = opprettBehandling(Ytelsestype.BARNETRYGD, eksternFagsakId) + + val fagsakDto = fagsakService.hentFagsak(Fagsystem.BA, eksternFagsakId) + + fagsakDto.eksternFagsakId shouldBe eksternFagsakId + fagsakDto.språkkode shouldBe Språkkode.NB + fagsakDto.ytelsestype shouldBe Ytelsestype.BARNETRYGD + fagsakDto.fagsystem shouldBe Fagsystem.BA + fagsakDto.institusjon shouldBe null + + val brukerDto = fagsakDto.bruker + brukerDto.personIdent shouldBe "32132132111" + brukerDto.navn shouldBe "testverdi" + brukerDto.kjønn shouldBe Kjønn.MANN + brukerDto.fødselsdato shouldBe LocalDate.now().minusYears(20) + brukerDto.dødsdato shouldBe null + + val behandlinger = fagsakDto.behandlinger + behandlinger.size shouldBe 1 + val behandlingsoppsummeringtDto = behandlinger.toList()[0] + behandlingsoppsummeringtDto.behandlingId shouldBe behandling.id + behandlingsoppsummeringtDto.eksternBrukId shouldBe behandling.eksternBrukId + behandlingsoppsummeringtDto.status shouldBe behandling.status + behandlingsoppsummeringtDto.type shouldBe behandling.type + } + + @Test + fun `hentFagsak skal hente fagsak for død person`() { + val eksternFagsakId = UUID.randomUUID().toString() + val behandling = opprettBehandling(Ytelsestype.BARNETRYGD, eksternFagsakId, "doed1234") + + val fagsakDto = fagsakService.hentFagsak(Fagsystem.BA, eksternFagsakId) + + fagsakDto.eksternFagsakId shouldBe eksternFagsakId + fagsakDto.språkkode shouldBe Språkkode.NB + fagsakDto.ytelsestype shouldBe Ytelsestype.BARNETRYGD + fagsakDto.fagsystem shouldBe Fagsystem.BA + + val brukerDto = fagsakDto.bruker + brukerDto.personIdent shouldBe "doed1234" + brukerDto.navn shouldBe "testverdi" + brukerDto.kjønn shouldBe Kjønn.MANN + brukerDto.fødselsdato shouldBe LocalDate.now().minusYears(20) + brukerDto.dødsdato shouldBe LocalDate.of(2022, 4, 1) + + val behandlinger = fagsakDto.behandlinger + behandlinger.size shouldBe 1 + val behandlingsoppsummeringtDto = behandlinger.toList()[0] + behandlingsoppsummeringtDto.behandlingId shouldBe behandling.id + behandlingsoppsummeringtDto.eksternBrukId shouldBe behandling.eksternBrukId + behandlingsoppsummeringtDto.status shouldBe behandling.status + behandlingsoppsummeringtDto.type shouldBe behandling.type + } + + @Test + fun `hentFagsak skal hente og oppdatere fagsak for barnetrygd`() { + val eksternFagsakId = UUID.randomUUID().toString() + opprettBehandling(Ytelsestype.BARNETRYGD, eksternFagsakId, "12345678910") + + // Antar mock PDL client returnerer 32132132111 + // første kall mot PDL får differanse på ident og kaster endretPersonIdentPublisher event + fagsakService.hentFagsak(Fagsystem.BA, eksternFagsakId) + val fagsakDto = fagsakService.hentFagsak(Fagsystem.BA, eksternFagsakId) + + fagsakDto.eksternFagsakId shouldBe eksternFagsakId + fagsakDto.språkkode shouldBe Språkkode.NB + fagsakDto.ytelsestype shouldBe Ytelsestype.BARNETRYGD + fagsakDto.fagsystem shouldBe Fagsystem.BA + + val brukerDto = fagsakDto.bruker + brukerDto.personIdent shouldBe "12345678910" + brukerDto.navn shouldBe "testverdi" + brukerDto.kjønn shouldBe Kjønn.MANN + brukerDto.fødselsdato shouldBe LocalDate.now().minusYears(20) + } + + @Test + fun `hentFagsak skal hente fagsak for barnetrygd med institusjon`() { + val eksternFagsakId = UUID.randomUUID().toString() + val behandling = opprettBehandling( + ytelsestype = Ytelsestype.BARNETRYGD, + eksternFagsakId = eksternFagsakId, + institusjon = Institusjon(organisasjonsnummer = "998765432"), + ) + + val fagsakDto = fagsakService.hentFagsak(Fagsystem.BA, eksternFagsakId) + + fagsakDto.eksternFagsakId shouldBe eksternFagsakId + fagsakDto.språkkode shouldBe Språkkode.NB + fagsakDto.ytelsestype shouldBe Ytelsestype.BARNETRYGD + fagsakDto.fagsystem shouldBe Fagsystem.BA + fagsakDto.institusjon shouldNotBe null + fagsakDto.institusjon!!.organisasjonsnummer shouldBe "998765432" + fagsakDto.institusjon!!.navn shouldBe "Testinstitusjon" + + val brukerDto = fagsakDto.bruker + brukerDto.personIdent shouldBe "32132132111" + brukerDto.navn shouldBe "testverdi" + brukerDto.kjønn shouldBe Kjønn.MANN + brukerDto.fødselsdato shouldBe LocalDate.now().minusYears(20) + brukerDto.dødsdato shouldBe null + + val behandlinger = fagsakDto.behandlinger + behandlinger.size shouldBe 1 + val behandlingsoppsummeringtDto = behandlinger.toList()[0] + behandlingsoppsummeringtDto.behandlingId shouldBe behandling.id + behandlingsoppsummeringtDto.eksternBrukId shouldBe behandling.eksternBrukId + behandlingsoppsummeringtDto.status shouldBe behandling.status + behandlingsoppsummeringtDto.type shouldBe behandling.type + } + + @Test + fun `hentFagsak skal ikke hente fagsak for barnetrygd når det ikke finnes`() { + val eksternFagsakId = UUID.randomUUID().toString() + val exception = shouldThrow { fagsakService.hentFagsak(Fagsystem.BA, eksternFagsakId) } + exception.message shouldBe "Fagsak finnes ikke for Barnetrygd og $eksternFagsakId" + } + + @Test + fun `finnesÅpenTilbakekrevingsbehandling skal returnere false om fagsak ikke finnes`() { + val finnesBehandling = fagsakService.finnesÅpenTilbakekrevingsbehandling(Fagsystem.BA, UUID.randomUUID().toString()) + finnesBehandling.finnesÅpenBehandling.shouldBeFalse() + } + + @Test + fun `finnesÅpenTilbakekrevingsbehandling skal returnere false om behandling er avsluttet`() { + val eksternFagsakId = UUID.randomUUID().toString() + var behandling = opprettBehandling(Ytelsestype.BARNETRYGD, eksternFagsakId) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val finnesBehandling = fagsakService.finnesÅpenTilbakekrevingsbehandling(Fagsystem.BA, eksternFagsakId) + finnesBehandling.finnesÅpenBehandling.shouldBeFalse() + } + + @Test + fun `finnesÅpenTilbakekrevingsbehandling skal returnere true om det finnes en åpen behandling`() { + val eksternFagsakId = UUID.randomUUID().toString() + opprettBehandling(Ytelsestype.BARNETRYGD, eksternFagsakId) + + val finnesBehandling = fagsakService.finnesÅpenTilbakekrevingsbehandling(Fagsystem.BA, eksternFagsakId) + finnesBehandling.finnesÅpenBehandling.shouldBeTrue() + } + + @Test + fun `kanBehandlingOpprettesManuelt skal returnere false når det finnes en åpen tilbakekrevingsbehandling`() { + val eksternFagsakId = UUID.randomUUID().toString() + opprettBehandling(Ytelsestype.BARNETRYGD, eksternFagsakId) + + val respons = fagsakService.kanBehandlingOpprettesManuelt(eksternFagsakId, Ytelsestype.BARNETRYGD) + respons.kanBehandlingOpprettes.shouldBeFalse() + respons.kravgrunnlagsreferanse.shouldBeNull() + respons.melding shouldBe "Det finnes allerede en åpen tilbakekrevingsbehandling. Den ligger i saksoversikten." + } + + @Test + fun `kanBehandlingOpprettesManuelt skal returnere false når det ikke finnes et frakoblet kravgrunnlag`() { + val respons = fagsakService.kanBehandlingOpprettesManuelt(UUID.randomUUID().toString(), Ytelsestype.BARNETRYGD) + respons.kanBehandlingOpprettes.shouldBeFalse() + respons.kravgrunnlagsreferanse.shouldBeNull() + respons.melding shouldBe "Det finnes ingen feilutbetaling på saken, så du kan ikke opprette tilbakekrevingsbehandling." + } + + @Test + fun `kanBehandlingOpprettesManuelt skal returnere false når det allerede finnes en opprettelse request`() { + val mottattXml = Testdata.økonomiXmlMottatt + økonomiXmlMottattRepository.insert(mottattXml) + + val properties = Properties() + properties["eksternFagsakId"] = mottattXml.eksternFagsakId + properties["ytelsestype"] = Ytelsestype.BARNETRYGD.kode + properties["eksternId"] = mottattXml.referanse + taskService.save(Task(type = OpprettBehandlingManueltTask.TYPE, properties = properties, payload = "")) + + val respons = fagsakService.kanBehandlingOpprettesManuelt(mottattXml.eksternFagsakId, Ytelsestype.BARNETRYGD) + respons.kanBehandlingOpprettes.shouldBeFalse() + respons.kravgrunnlagsreferanse.shouldBeNull() + respons.melding shouldBe "Det finnes allerede en forespørsel om å opprette tilbakekrevingsbehandling. " + + "Behandlingen vil snart bli tilgjengelig i saksoversikten. Dersom den ikke dukker opp, " + + "ta kontakt med brukerstøtte for å rapportere feilen." + } + + @Test + fun `kanBehandlingOpprettesManuelt skal returnere true når det finnes et frakoblet grunnlag`() { + val mottattXml = Testdata.økonomiXmlMottatt + økonomiXmlMottattRepository.insert(mottattXml) + + val respons = fagsakService.kanBehandlingOpprettesManuelt(mottattXml.eksternFagsakId, Ytelsestype.BARNETRYGD) + respons.kanBehandlingOpprettes.shouldBeTrue() + respons.kravgrunnlagsreferanse shouldBe mottattXml.referanse + respons.melding shouldBe "Det er mulig å opprette behandling manuelt." + } + + @Nested + inner class HentVedtakForFagsak { + + @Test + internal fun `skal returnere tom liste hvis det ikke finnes noen vedtak for fagsak`() { + assertThat(fagsakService.hentVedtakForFagsak(Fagsystem.EF, UUID.randomUUID().toString())) + .isEmpty() + } + } + + private fun opprettBehandling( + ytelsestype: Ytelsestype, + eksternFagsakId: String, + personIdent: String = "32132132111", + institusjon: Institusjon? = null, + ): Behandling { + val fagsak = Fagsak( + eksternFagsakId = eksternFagsakId, + bruker = Bruker(personIdent, Språkkode.NB), + ytelsestype = ytelsestype, + fagsystem = FagsystemUtil.hentFagsystemFraYtelsestype(ytelsestype), + institusjon = institusjon, + ) + fagsakRepository.insert(fagsak) + + val behandling = Behandling( + fagsakId = fagsak.id, + type = Behandlingstype.TILBAKEKREVING, + ansvarligSaksbehandler = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + behandlendeEnhet = "8020", + behandlendeEnhetsNavn = "Oslo", + manueltOpprettet = false, + ) + behandlingRepository.insert(behandling) + return behandling + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/ValiderBrevmottakerServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/ValiderBrevmottakerServiceTest.kt new file mode 100644 index 000000000..aa5618727 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/ValiderBrevmottakerServiceTest.kt @@ -0,0 +1,118 @@ +package no.nav.familie.tilbake.behandling + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.kontrakter.felles.tilbakekreving.MottakerType +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.ManuellBrevmottakerRepository +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.domene.ManuellBrevmottaker +import no.nav.familie.tilbake.person.PersonService +import org.assertj.core.api.Assertions.assertThatNoException +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test +import java.util.* + +class ValiderBrevmottakerServiceTest { + private val manuellBrevmottakerRepository = mockk() + private val fagsakService = mockk() + private val personService = mockk() + val validerBrevmottakerService = ValiderBrevmottakerService( + manuellBrevmottakerRepository, + fagsakService, + personService, + ) + private val behandlingId = UUID.randomUUID() + private val fagsak = Testdata.fagsak + private val manuellBrevmottaker = ManuellBrevmottaker( + type = MottakerType.BRUKER_MED_UTENLANDSK_ADRESSE, + behandlingId = behandlingId, + navn = "Donald Duck", + adresselinje1 = "adresselinje1", + postnummer = "postnummer", + poststed = "poststed", + landkode = "NO", + ) + + @Test + fun `Skal ikke kaste en Feil exception når en behandling ikke inneholder noen manuelle brevmottakere`() { + every { manuellBrevmottakerRepository.findByBehandlingId(any()) } returns emptyList() + assertThatNoException().isThrownBy { + validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligPersonMedManuelleBrevmottakere( + behandlingId, + fagsak.id, + ) + } + } + + @Test + fun `Skal kaste en Feil exception når en behandling inneholder en strengt fortrolig person og minst en manuell brevmottaker`() { + every { manuellBrevmottakerRepository.findByBehandlingId(behandlingId) } returns listOf(manuellBrevmottaker) + every { fagsakService.hentFagsak(any()) } returns fagsak + every { personService.hentIdenterMedStrengtFortroligAdressebeskyttelse(any(), any()) } returns listOf( + fagsak.bruker.ident, + ) + assertThatThrownBy { + validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligPersonMedManuelleBrevmottakere( + behandlingId, + fagsak.id, + ) + }.isInstanceOf(Feil::class.java) + .hasMessageContaining("strengt fortrolig adressebeskyttelse og kan ikke kombineres med manuelle brevmottakere") + } + + @Test + fun `Skal ikke kaste Feil exception når behandling ikke inneholder strengt fortrolig person og inneholder en manuell brevmottaker`() { + every { manuellBrevmottakerRepository.findByBehandlingId(behandlingId) } returns listOf(manuellBrevmottaker) + every { fagsakService.hentFagsak(any()) } returns fagsak + every { personService.hentIdenterMedStrengtFortroligAdressebeskyttelse(any(), any()) } returns emptyList() + assertThatNoException().isThrownBy { + validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligPersonMedManuelleBrevmottakere( + behandlingId, + fagsak.id, + ) + } + } + + @Test + fun `Skal ikke kaste Feil exception når en behandling inneholder strengt fortrolig person og ingen manuelle brevmottakere`() { + every { manuellBrevmottakerRepository.findByBehandlingId(behandlingId) } returns emptyList() + every { fagsakService.hentFagsak(any()) } returns fagsak + every { personService.hentIdenterMedStrengtFortroligAdressebeskyttelse(any(), any()) } returns listOf( + fagsak.bruker.ident, + ) + assertThatNoException().isThrownBy { + validerBrevmottakerService.validerAtBehandlingIkkeInneholderStrengtFortroligPersonMedManuelleBrevmottakere( + behandlingId, + fagsak.id, + ) + } + } + + @Test + fun `Skal ikke kaste en Feil exception når en behandling ikke inneholder en strengt fortrolig person`() { + every { fagsakService.hentFagsak(any()) } returns fagsak + every { personService.hentIdenterMedStrengtFortroligAdressebeskyttelse(any(), any()) } returns emptyList() + assertThatNoException().isThrownBy { + validerBrevmottakerService.validerAtBehandlingenIkkeInneholderStrengtFortroligPerson( + behandlingId, + fagsak.id, + ) + } + } + + @Test + fun `Skal kaste en Feil exception når en behandling inneholder en strengt fortrolig person`() { + every { fagsakService.hentFagsak(any()) } returns fagsak + every { personService.hentIdenterMedStrengtFortroligAdressebeskyttelse(any(), any()) } returns listOf( + fagsak.bruker.ident, + ) + assertThatThrownBy { + validerBrevmottakerService.validerAtBehandlingenIkkeInneholderStrengtFortroligPerson( + behandlingId, + fagsak.id, + ) + }.isInstanceOf(Feil::class.java) + .hasMessageContaining("strengt fortrolig adressebeskyttelse og kan ikke kombineres med manuelle brevmottakere") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/VarselServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/VarselServiceTest.kt new file mode 100644 index 000000000..017743294 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/VarselServiceTest.kt @@ -0,0 +1,82 @@ +package no.nav.familie.tilbake.behandling + +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class VarselServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var varselService: VarselService + + private val behandling = Testdata.behandling + private val kravgrunnlag = Testdata.kravgrunnlag431 + + @BeforeEach + fun setup() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(behandling) + } + + @Test + fun `lagre skal lagre varselbrev med perioder fra feilutbetaling når behandling har mottatt kravgrunnlag`() { + kravgrunnlagRepository.insert(kravgrunnlag) + varselService.lagre(behandling.id, "Hello", 1000) + + val oppdatertBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + val varsler = oppdatertBehandling.varsler + + varsler.size shouldBe 2 + varsler.any { !it.aktiv }.shouldBeTrue() + val aktivVarsel = oppdatertBehandling.aktivtVarsel + aktivVarsel.shouldNotBeNull() + aktivVarsel.varselbeløp shouldBe 1000 + aktivVarsel.varseltekst shouldBe "Hello" + + val varselsperioder = aktivVarsel.perioder + varselsperioder.shouldNotBeEmpty() + varselsperioder.any { + it.fom == kravgrunnlag.perioder.first().periode.fomDato && + it.tom == kravgrunnlag.perioder.first().periode.tomDato + }.shouldBeTrue() + } + + @Test + fun `lagre skal lagre varselbrev med perioder ved opprettelse når behandling ikke har mottatt kravgrunnlag`() { + varselService.lagre(behandling.id, "Hello", 1000) + + val oppdatertBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + val varsler = oppdatertBehandling.varsler + + varsler.size shouldBe 2 + varsler.any { !it.aktiv }.shouldBeTrue() + val aktivVarsel = oppdatertBehandling.aktivtVarsel + aktivVarsel.shouldNotBeNull() + aktivVarsel.varselbeløp shouldBe 1000 + aktivVarsel.varseltekst shouldBe "Hello" + + val varselsperioder = aktivVarsel.perioder + varselsperioder.shouldNotBeEmpty() + varselsperioder.any { + it.fom == Testdata.varsel.perioder.first().fom && + it.tom == Testdata.varsel.perioder.first().tom + }.shouldBeTrue() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/VergeServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/VergeServiceTest.kt new file mode 100644 index 000000000..d7ec58b50 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/VergeServiceTest.kt @@ -0,0 +1,341 @@ +package no.nav.familie.tilbake.behandling + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.collections.shouldHaveSingleElement +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.kontrakter.felles.Applikasjon +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.tilbakekreving.Vergetype +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.VergeDto +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.event.EndretPersonIdentEventPublisher +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import no.nav.familie.tilbake.integration.pdl.PdlClient +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.person.PersonService +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.util.UUID + +internal class VergeServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + @Autowired + private lateinit var integrasjonerClient: IntegrasjonerClient + + @Autowired + private lateinit var personService: PersonService + + private lateinit var vergeService: VergeService + + private val historikkTaskService: HistorikkTaskService = mockk(relaxed = true) + + private val vergeDto = VergeDto( + orgNr = "987654321", + type = Vergetype.ADVOKAT, + navn = "Stor Herlig Straff", + begrunnelse = "Det var nødvendig", + ) + + @BeforeEach + fun setUp() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(Testdata.behandling) + vergeService = VergeService( + behandlingRepository, + fagsakRepository, + historikkTaskService, + behandlingskontrollService, + integrasjonerClient, + personService, + ) + clearAllMocks(answers = false) + } + + @Test + fun `lagreVerge skal lagre verge i basen`() { + vergeService.lagreVerge(Testdata.behandling.id, vergeDto) + + val behandling = behandlingRepository.findByIdOrThrow(Testdata.behandling.id) + val verge = behandling.aktivVerge!! + verge.aktiv shouldBe true + verge.orgNr shouldBe "987654321" + verge.type shouldBe Vergetype.ADVOKAT + verge.navn shouldBe "Stor Herlig Straff" + verge.kilde shouldBe Applikasjon.FAMILIE_TILBAKE.name + verge.begrunnelse shouldBe "Det var nødvendig" + } + + @Test + fun `lagreVerge skal deaktivere eksisterende verger i basen`() { + val behandlingFørOppdatering = behandlingRepository.findByIdOrThrow(Testdata.behandling.id) + val gammelVerge = behandlingFørOppdatering.aktivVerge!! + + vergeService.lagreVerge(Testdata.behandling.id, vergeDto) + + val behandling = behandlingRepository.findByIdOrThrow(Testdata.behandling.id) + val deaktivertVerge = behandling.verger.first { !it.aktiv } + deaktivertVerge.id shouldBe gammelVerge.id + } + + @Test + fun `lagreVerge skal kalle historikkTaskService for å opprette historikkTask`() { + vergeService.lagreVerge(Testdata.behandling.id, vergeDto) + + val behandling = behandlingRepository.findByIdOrThrow(Testdata.behandling.id) + + verify { + historikkTaskService.lagHistorikkTask( + behandling.id, + TilbakekrevingHistorikkinnslagstype.VERGE_OPPRETTET, + Aktør.SAKSBEHANDLER, + ) + } + } + + @Test + fun `lagreVerge skal ikke lagre verge når organisasjonen er tom`() { + val vergeDto = vergeDto.copy(orgNr = null, ident = "123") + val exception = shouldThrow { vergeService.lagreVerge(Testdata.behandling.id, vergeDto) } + exception.message shouldBe "orgNr kan ikke være null for ${Vergetype.ADVOKAT}" + } + + @Test + fun `lagreVerge skal ikke lagre verge når organisasjonen ikke er gyldig`() { + val mockIntegrasjonerClient = mockk() + val vergeService = VergeService( + behandlingRepository, + fagsakRepository, + historikkTaskService, + behandlingskontrollService, + mockIntegrasjonerClient, + personService, + ) + + every { mockIntegrasjonerClient.validerOrganisasjon(any()) } returns false + + val exception = shouldThrow { vergeService.lagreVerge(Testdata.behandling.id, vergeDto) } + exception.message shouldBe "Organisasjon ${vergeDto.orgNr} er ikke gyldig" + } + + @Test + fun `lagreVerge skal ikke lagre verge når ident er tom`() { + val vergeDto = vergeDto.copy(type = Vergetype.VERGE_FOR_BARN) + val exception = shouldThrow { vergeService.lagreVerge(Testdata.behandling.id, vergeDto) } + exception.message shouldBe "ident kan ikke være null for ${vergeDto.type}" + } + + @Test + fun `lagreVerge skal ikke lagre verge når personen ikke finnes i PDL`() { + val mockPdlClient = mockk() + val mockEndretPersonIdentEventPublisher: EndretPersonIdentEventPublisher = mockk() + val personService = PersonService(mockPdlClient, fagsakRepository, mockEndretPersonIdentEventPublisher) + val vergeService = VergeService( + behandlingRepository, + fagsakRepository, + historikkTaskService, + behandlingskontrollService, + integrasjonerClient, + personService, + ) + + every { mockPdlClient.hentPersoninfo(any(), any()) } throws Feil(message = "Feil ved oppslag på person") + + val vergeDto = VergeDto(ident = "123", type = Vergetype.VERGE_FOR_BARN, navn = "testverdi", begrunnelse = "testverdi") + val exception = shouldThrow { vergeService.lagreVerge(Testdata.behandling.id, vergeDto) } + exception.message shouldBe "Feil ved oppslag på person" + } + + @Test + fun `fjernVerge skal deaktivere verge i basen hvis det finnes aktiv verge`() { + val behandlingFørOppdatering = behandlingRepository.findByIdOrThrow(Testdata.behandling.id) + val gammelVerge = behandlingFørOppdatering.aktivVerge!! + + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + lagBehandlingsstegstilstand(behandlingFørOppdatering.id, Behandlingssteg.VARSEL, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(behandlingFørOppdatering.id, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(behandlingFørOppdatering.id, Behandlingssteg.VERGE, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(behandlingFørOppdatering.id, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + + vergeService.fjernVerge(Testdata.behandling.id) + + val behandling = behandlingRepository.findByIdOrThrow(Testdata.behandling.id) + val deaktivertVerge = behandling.verger.first() + deaktivertVerge.id shouldBe gammelVerge.id + deaktivertVerge.aktiv shouldBe false + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VARSEL, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VERGE, Behandlingsstegstatus.TILBAKEFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + } + + @Test + fun `fjernVerge skal tilbakeføre verge steg når behandling er på vilkårsvurdering steg og verge fjernet`() { + val behandlingFørOppdatering = behandlingRepository.findByIdOrThrow(Testdata.behandling.id) + val gammelVerge = behandlingFørOppdatering.aktivVerge + gammelVerge.shouldNotBeNull() + + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + lagBehandlingsstegstilstand(behandlingFørOppdatering.id, Behandlingssteg.VARSEL, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(behandlingFørOppdatering.id, Behandlingssteg.VERGE, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(behandlingFørOppdatering.id, Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(behandlingFørOppdatering.id, Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.KLAR) + + vergeService.fjernVerge(Testdata.behandling.id) + + behandlingRepository.findByIdOrThrow(behandlingFørOppdatering.id).harVerge.shouldBeFalse() + verify { + historikkTaskService.lagHistorikkTask( + behandlingFørOppdatering.id, + TilbakekrevingHistorikkinnslagstype.VERGE_FJERNET, + Aktør.SAKSBEHANDLER, + ) + } + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingFørOppdatering.id) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VARSEL, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VERGE, Behandlingsstegstatus.TILBAKEFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.KLAR) + } + + @Test + fun `fjernVerge skal tilbakeføre verge steg og fortsette til fakta når behandling er på verge steg og verge fjernet`() { + val behandlingFørOppdatering = behandlingRepository.findByIdOrThrow(Testdata.behandling.id) + val behandlingUtenVerge = behandlingRepository.update(behandlingFørOppdatering.copy(verger = emptySet())) + + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + lagBehandlingsstegstilstand(behandlingUtenVerge.id, Behandlingssteg.VARSEL, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(behandlingUtenVerge.id, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(behandlingUtenVerge.id, Behandlingssteg.VERGE, Behandlingsstegstatus.KLAR) + + vergeService.fjernVerge(behandlingUtenVerge.id) + + behandlingUtenVerge.harVerge.shouldBeFalse() + verify(exactly = 0) { historikkTaskService.lagHistorikkTask(any(), any(), any(), any()) } + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingUtenVerge.id) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VARSEL, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VERGE, Behandlingsstegstatus.TILBAKEFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + } + + @Test + fun `opprettVergeSteg skal opprette verge steg når behandling er på vilkårsvurdering steg`() { + val behandling = behandlingRepository.findByIdOrThrow(Testdata.behandling.id) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + lagBehandlingsstegstilstand(behandling.id, Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(behandling.id, Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(behandling.id, Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.KLAR) + + vergeService.opprettVergeSteg(behandling.id) + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.TILBAKEFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FORELDELSE, Behandlingsstegstatus.TILBAKEFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.TILBAKEFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VERGE, Behandlingsstegstatus.KLAR) + } + + @Test + fun `opprettVergeSteg skal ikke opprette verge steg når behandling er avsluttet`() { + val behandling = behandlingRepository.findByIdOrThrow(Testdata.behandling.id) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val exception = shouldThrow { vergeService.opprettVergeSteg(behandling.id) } + exception.message shouldBe "Behandling med id=${behandling.id} er allerede ferdig behandlet." + } + + @Test + fun `opprettVergeSteg skal ikke opprette verge steg når behandling er på vent`() { + val behandling = behandlingRepository.findByIdOrThrow(Testdata.behandling.id) + lagBehandlingsstegstilstand(behandling.id, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + behandlingskontrollService.settBehandlingPåVent( + behandling.id, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + LocalDate.now().plusWeeks(4), + ) + + val exception = shouldThrow { vergeService.opprettVergeSteg(behandling.id) } + exception.message shouldBe "Behandling med id=${behandling.id} er på vent." + } + + @Test + fun `hentVerge skal returnere lagret verge data`() { + val aktivVerge = Testdata.behandling.aktivVerge + aktivVerge.shouldNotBeNull() + + val respons = vergeService.hentVerge(Testdata.behandling.id) + + respons.shouldNotBeNull() + respons.begrunnelse shouldBe aktivVerge.begrunnelse + respons.type shouldBe aktivVerge.type + respons.ident shouldBe aktivVerge.ident + respons.navn shouldBe aktivVerge.navn + respons.orgNr shouldBe aktivVerge.orgNr + } + + private fun lagBehandlingsstegstilstand( + behandlingId: UUID, + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ) { + behandlingsstegstilstandRepository.insert( + Behandlingsstegstilstand( + behandlingId = behandlingId, + behandlingssteg = behandlingssteg, + behandlingsstegsstatus = behandlingsstegstatus, + ), + ) + } + + private fun assertBehandlingssteg( + behandlingsstegstilstand: List, + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ) { + behandlingsstegstilstand.shouldHaveSingleElement { + behandlingssteg == it.behandlingssteg && + behandlingsstegstatus == it.behandlingsstegsstatus + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingBatchTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingBatchTest.kt new file mode 100644 index 000000000..8e7e751c1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingBatchTest.kt @@ -0,0 +1,76 @@ +package no.nav.familie.tilbake.behandling.batch + +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.matchers.booleans.shouldBeTrue +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.data.Testdata +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +internal class AutomatiskGjenopptaBehandlingBatchTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var automatiskGjenopptaBehandlingBatch: AutomatiskGjenopptaBehandlingBatch + + @Test + fun `skal lage task på behandling som venter på varsel og tidsfristen har utgått`() { + fagsakRepository.insert(Testdata.fagsak) + val behandling = behandlingRepository.insert(Testdata.behandling.copy(status = Behandlingsstatus.UTREDES)) + behandlingsstegstilstandRepository.insert( + Testdata.behandlingsstegstilstand.copy( + behandlingssteg = Behandlingssteg.VARSEL, + behandlingsstegsstatus = Behandlingsstegstatus.VENTER, + venteårsak = Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + tidsfrist = LocalDate.now().minusWeeks(4), + ), + ) + shouldNotThrow { automatiskGjenopptaBehandlingBatch.automatiskGjenopptaBehandling() } + + taskService.findAll().any { + it.type == AutomatiskGjenopptaBehandlingTask.TYPE && + it.payload == behandling.id.toString() + }.shouldBeTrue() + } + + @Test + fun `skal lage task på behandling som venter på avvent dokumentasjon`() { + fagsakRepository.insert(Testdata.fagsak) + val behandling = behandlingRepository.insert(Testdata.behandling.copy(status = Behandlingsstatus.UTREDES)) + val tidsfrist = LocalDate.now().minusWeeks(1) + behandlingsstegstilstandRepository.insert( + Testdata.behandlingsstegstilstand.copy( + behandlingssteg = Behandlingssteg.VILKÅRSVURDERING, + behandlingsstegsstatus = Behandlingsstegstatus.VENTER, + venteårsak = Venteårsak.AVVENTER_DOKUMENTASJON, + tidsfrist = tidsfrist, + ), + ) + shouldNotThrow { automatiskGjenopptaBehandlingBatch.automatiskGjenopptaBehandling() } + + taskService.findAll().any { + it.type == AutomatiskGjenopptaBehandlingTask.TYPE && + it.payload == behandling.id.toString() + }.shouldBeTrue() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingTaskTest.kt new file mode 100644 index 000000000..c5b2eb2e7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskGjenopptaBehandlingTaskTest.kt @@ -0,0 +1,197 @@ +package no.nav.familie.tilbake.behandling.batch + +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.matchers.booleans.shouldBeTrue +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.historikkinnslag.LagHistorikkinnslagTask +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.oppgave.OppdaterOppgaveTask +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.util.Properties +import java.util.UUID + +internal class AutomatiskGjenopptaBehandlingTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var automatiskGjenopptaBehandlingTask: AutomatiskGjenopptaBehandlingTask + + @Test + fun `skal gjenoppta behandling som venter på varsel og har allerede fått kravgrunnlag til FAKTA steg`() { + fagsakRepository.insert(Testdata.fagsak) + val behandling = behandlingRepository.insert(Testdata.behandling.copy(status = Behandlingsstatus.UTREDES)) + val tidsfrist = LocalDate.now().minusWeeks(4) + behandlingsstegstilstandRepository.insert( + Testdata.behandlingsstegstilstand.copy( + behandlingssteg = Behandlingssteg.VARSEL, + behandlingsstegsstatus = Behandlingsstegstatus.VENTER, + venteårsak = Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + tidsfrist = tidsfrist, + ), + ) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + shouldNotThrow { automatiskGjenopptaBehandlingTask.doTask(lagTask(behandling.id)) } + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.any { + it.behandlingssteg == Behandlingssteg.VARSEL && + it.behandlingsstegsstatus == Behandlingsstegstatus.UTFØRT + }.shouldBeTrue() + behandlingsstegstilstand.any { + it.behandlingssteg == Behandlingssteg.FAKTA && + it.behandlingsstegsstatus == Behandlingsstegstatus.KLAR + }.shouldBeTrue() + + taskService.findAll().any { + it.type == LagHistorikkinnslagTask.TYPE && + it.payload == behandling.id.toString() && + it.metadata["historikkinnslagstype"] == TilbakekrevingHistorikkinnslagstype.BEHANDLING_GJENOPPTATT.name && + it.metadata["aktør"] == Aktør.VEDTAKSLØSNING.name + }.shouldBeTrue() + + taskService.findAll().any { + it.type == OppdaterOppgaveTask.TYPE && + it.payload == behandling.id.toString() && + it.metadata["beskrivelse"] == "Behandling er tatt av vent automatisk" && + it.metadata["frist"] == tidsfrist.toString() && + it.metadata["saksbehandler"] == "VL" + }.shouldBeTrue() + } + + @Test + fun `skal gjenoppta behandling som venter på varsel og har ikke fått kravgrunnlag til GRUNNLAG steg`() { + fagsakRepository.insert(Testdata.fagsak) + val behandling = behandlingRepository.insert(Testdata.behandling.copy(status = Behandlingsstatus.UTREDES)) + val tidsfrist = LocalDate.now().minusWeeks(4) + behandlingsstegstilstandRepository.insert( + Testdata.behandlingsstegstilstand.copy( + behandlingssteg = Behandlingssteg.VARSEL, + behandlingsstegsstatus = Behandlingsstegstatus.VENTER, + venteårsak = Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + tidsfrist = tidsfrist, + ), + ) + shouldNotThrow { automatiskGjenopptaBehandlingTask.doTask(lagTask(behandling.id)) } + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.any { + it.behandlingssteg == Behandlingssteg.VARSEL && + it.behandlingsstegsstatus == Behandlingsstegstatus.UTFØRT + }.shouldBeTrue() + behandlingsstegstilstand.any { + it.behandlingssteg == Behandlingssteg.GRUNNLAG && + it.behandlingsstegsstatus == Behandlingsstegstatus.VENTER && + it.venteårsak == Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG + }.shouldBeTrue() + + taskService.findAll().any { + it.type == LagHistorikkinnslagTask.TYPE && + it.payload == behandling.id.toString() && + it.metadata["historikkinnslagstype"] == TilbakekrevingHistorikkinnslagstype.BEHANDLING_GJENOPPTATT.name && + it.metadata["aktør"] == Aktør.VEDTAKSLØSNING.name + }.shouldBeTrue() + + taskService.findAll().any { + it.type == LagHistorikkinnslagTask.TYPE && + it.payload == behandling.id.toString() && + it.metadata["historikkinnslagstype"] == TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT.name && + it.metadata["aktør"] == Aktør.VEDTAKSLØSNING.name + }.shouldBeTrue() + + taskService.findAll().any { + it.type == OppdaterOppgaveTask.TYPE && + it.payload == behandling.id.toString() && + it.metadata["beskrivelse"] == "Behandling er tatt av vent automatisk" && + it.metadata["frist"] == tidsfrist.toString() && + it.metadata["saksbehandler"] == "VL" + }.shouldBeTrue() + + val venteårsak = Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG + taskService.findAll().any { + it.type == OppdaterOppgaveTask.TYPE && + it.payload == behandling.id.toString() && + it.metadata["beskrivelse"] == venteårsak.beskrivelse + it.metadata["frist"] == LocalDate.now().plusWeeks(venteårsak.defaultVenteTidIUker).toString() + }.shouldBeTrue() + } + + @Test + fun `skal gjenoppta behandling som venter på avvent dokumentasjon`() { + fagsakRepository.insert(Testdata.fagsak) + val behandling = behandlingRepository.insert(Testdata.behandling.copy(status = Behandlingsstatus.UTREDES)) + val tidsfrist = LocalDate.now().minusWeeks(1) + behandlingsstegstilstandRepository.insert( + Testdata.behandlingsstegstilstand.copy( + behandlingssteg = Behandlingssteg.VILKÅRSVURDERING, + behandlingsstegsstatus = Behandlingsstegstatus.VENTER, + venteårsak = Venteårsak.AVVENTER_DOKUMENTASJON, + tidsfrist = tidsfrist, + ), + ) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + shouldNotThrow { automatiskGjenopptaBehandlingTask.doTask(lagTask(behandling.id)) } + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.any { + it.behandlingssteg == Behandlingssteg.VILKÅRSVURDERING && + it.behandlingsstegsstatus == Behandlingsstegstatus.KLAR + it.venteårsak == null && it.tidsfrist == null + }.shouldBeTrue() + + taskService.findAll().any { + it.type == LagHistorikkinnslagTask.TYPE && + it.payload == behandling.id.toString() && + it.metadata["historikkinnslagstype"] == TilbakekrevingHistorikkinnslagstype.BEHANDLING_GJENOPPTATT.name && + it.metadata["aktør"] == Aktør.VEDTAKSLØSNING.name + }.shouldBeTrue() + + taskService.findAll().any { + it.type == OppdaterOppgaveTask.TYPE && + it.payload == behandling.id.toString() && + it.metadata["beskrivelse"] == "Behandling er tatt av vent automatisk" && + it.metadata["frist"] == tidsfrist.toString() && + it.metadata["saksbehandler"] == "VL" + }.shouldBeTrue() + } + + private fun lagTask(behandlingId: UUID) = Task( + type = AutomatiskGjenopptaBehandlingTask.TYPE, + payload = behandlingId.toString(), + Properties().apply { + setProperty( + PropertyName.FAGSYSTEM, + Fagsystem.BA.name, + ) + }, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingBatchTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingBatchTest.kt new file mode 100644 index 000000000..270eb0110 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingBatchTest.kt @@ -0,0 +1,218 @@ +package no.nav.familie.tilbake.behandling.batch + +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.collections.shouldHaveSingleElement +import no.nav.familie.kontrakter.felles.Regelverk +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingRepository +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +internal class AutomatiskSaksbehandlingBatchTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var brevsporingRepository: BrevsporingRepository + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + @Autowired + private lateinit var automatiskSaksbehandlingBatch: AutomatiskSaksbehandlingBatch + + private val fagsak: Fagsak = Testdata.fagsak + private val behandling: Behandling = Testdata.behandling + + @BeforeEach + fun init() { + fagsakRepository.insert(fagsak) + val fagsystemsbehandling = behandling.aktivFagsystemsbehandling.copy( + tilbakekrevingsvalg = Tilbakekrevingsvalg + .OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + behandlingRepository.insert( + behandling.copy( + fagsystemsbehandling = setOf(fagsystemsbehandling), + status = Behandlingsstatus.UTREDES, + ), + ) + val feilKravgrunnlagBeløp = Testdata.feilKravgrunnlagsbeløp433.copy(nyttBeløp = BigDecimal("100")) + val ytelKravgrunnlagsbeløp433 = + Testdata.ytelKravgrunnlagsbeløp433.copy( + opprinneligUtbetalingsbeløp = BigDecimal("100"), + tilbakekrevesBeløp = BigDecimal("100"), + ) + + val kravgrunnlag = Testdata.kravgrunnlag431 + .copy( + kontrollfelt = "2019-11-22-19.09.31.458065", + perioder = setOf( + Testdata.kravgrunnlagsperiode432.copy( + beløp = setOf( + feilKravgrunnlagBeløp, + ytelKravgrunnlagsbeløp433, + ), + ), + ), + ) + + kravgrunnlagRepository.insert(kravgrunnlag) + behandlingsstegstilstandRepository.insert( + lagBehandlingsstegstilstand( + Behandlingssteg.GRUNNLAG, + Behandlingsstegstatus.UTFØRT, + ), + ) + behandlingsstegstilstandRepository.insert( + lagBehandlingsstegstilstand( + Behandlingssteg.FAKTA, + Behandlingsstegstatus.KLAR, + ), + ) + } + + @Test + fun `behandleAutomatisk skal opprette tasker når det finnes en behandling klar for automatisk saksbehandling`() { + automatiskSaksbehandlingBatch.behandleAutomatisk() + taskService.findAll().shouldHaveSingleElement { + it.type == AutomatiskSaksbehandlingTask.TYPE && + it.payload == behandling.id.toString() + } + } + + @Test + fun `behandleAutomatisk skal ikke opprette tasker for EØS-behandlinger`() { + val behandling = behandlingRepository.findByIdOrThrow(behandling.id).copy(regelverk = Regelverk.EØS) + .also { behandlingRepository.update(it) } + + automatiskSaksbehandlingBatch.behandleAutomatisk() + taskService.findAll().any { + it.type == AutomatiskSaksbehandlingTask.TYPE && + it.payload == behandling.id.toString() + }.shouldBeFalse() + } + + @Test + fun `behandleAutomatisk skal ikke opprette tasker når behandlingen allerede sendte varselsbrev`() { + val behandling = behandlingRepository.findByIdOrThrow(behandling.id) + val fagsystemsbehandling = behandling.aktivFagsystemsbehandling.copy( + tilbakekrevingsvalg = Tilbakekrevingsvalg + .OPPRETT_TILBAKEKREVING_MED_VARSEL, + ) + behandlingRepository.update(behandling.copy(fagsystemsbehandling = setOf(fagsystemsbehandling))) + brevsporingRepository.insert(Testdata.brevsporing) + + automatiskSaksbehandlingBatch.behandleAutomatisk() + taskService.findAll().any { + it.type == AutomatiskSaksbehandlingTask.TYPE && + it.payload == behandling.id.toString() + }.shouldBeFalse() + } + + @Test + fun `behandleAutomatisk skal ikke opprette tasker når behandlingen er på vent`() { + behandlingskontrollService.settBehandlingPåVent( + behandling.id, + Venteårsak.AVVENTER_DOKUMENTASJON, + LocalDate.now().plusWeeks(2), + ) + + automatiskSaksbehandlingBatch.behandleAutomatisk() + taskService.findAll().any { + it.type == AutomatiskSaksbehandlingTask.TYPE && + it.payload == behandling.id.toString() + }.shouldBeFalse() + } + + @Test + fun `behandleAutomatisk skal ikke opprette tasker når behandlingens kravgrunnlag som ikke er gammel enn begrensning`() { + kravgrunnlagRepository.update( + kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + .copy( + kontrollfelt = LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("YYYY-MM-dd-HH.mm.ss.SSSSSS")), + ), + ) + + automatiskSaksbehandlingBatch.behandleAutomatisk() + taskService.findAll().any { + it.type == AutomatiskSaksbehandlingTask.TYPE && + it.payload == behandling.id.toString() + }.shouldBeFalse() + } + + @Test + fun `behandleAutomatisk skal ikke opprette tasker når behandlingens kravgrunnlag har feilbeløp mer enn begrensning`() { + kravgrunnlagRepository.update( + kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + .copy(perioder = setOf(Testdata.kravgrunnlagsperiode432)), + ) + + automatiskSaksbehandlingBatch.behandleAutomatisk() + taskService.findAll().any { + it.type == AutomatiskSaksbehandlingTask.TYPE && + it.payload == behandling.id.toString() + }.shouldBeFalse() + } + + @Test + fun `behandleAutomatisk skal ikke opprette tasker når det allerede finnes en feilede tasker`() { + val task = taskService.save(Task(type = AutomatiskSaksbehandlingTask.TYPE, payload = behandling.id.toString())) + taskService.save(taskService.findById(task.id).copy(status = Status.FEILET)) + + automatiskSaksbehandlingBatch.behandleAutomatisk() + taskService.findAll().any { + it.type == AutomatiskSaksbehandlingTask.TYPE && + it.payload == behandling.id.toString() + it.status != Status.FEILET + }.shouldBeFalse() + } + + private fun lagBehandlingsstegstilstand( + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ): Behandlingsstegstilstand { + return Behandlingsstegstilstand( + behandlingId = behandling.id, + behandlingssteg = behandlingssteg, + behandlingsstegsstatus = behandlingsstegstatus, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingTaskTest.kt new file mode 100644 index 000000000..5506fccde --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/batch/AutomatiskSaksbehandlingTaskTest.kt @@ -0,0 +1,257 @@ +package no.nav.familie.tilbake.behandling.batch + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldHaveSingleElement +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandling.domain.Saksbehandlingstype +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.SendVedtaksbrevTask +import no.nav.familie.tilbake.dokumentbestilling.vedtak.VedtaksbrevsoppsummeringRepository +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingRepository +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.VurdertForeldelseRepository +import no.nav.familie.tilbake.iverksettvedtak.task.AvsluttBehandlingTask +import no.nav.familie.tilbake.iverksettvedtak.task.SendØkonomiTilbakekrevingsvedtakTask +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingRepository +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate +import java.util.Properties + +internal class AutomatiskSaksbehandlingTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var faktaFeilutbetalingRepository: FaktaFeilutbetalingRepository + + @Autowired + private lateinit var foreldelsesRepository: VurdertForeldelseRepository + + @Autowired + private lateinit var vilkårsvurderingRepository: VilkårsvurderingRepository + + @Autowired + private lateinit var vedtaksbrevsoppsummeringRepository: VedtaksbrevsoppsummeringRepository + + @Autowired + private lateinit var sendØkonomiTilbakekrevingsvedtakTask: SendØkonomiTilbakekrevingsvedtakTask + + @Autowired + private lateinit var sendVedtaksbrevTask: SendVedtaksbrevTask + + @Autowired + private lateinit var avsluttBehandlingTask: AvsluttBehandlingTask + + private val taskService: TaskService = mockk() + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + @Autowired + private lateinit var automatiskSaksbehandlingTask: AutomatiskSaksbehandlingTask + + private val fagsak: Fagsak = Testdata.fagsak + private val behandling: Behandling = Testdata.behandling + + @BeforeEach + fun init() { + fagsakRepository.insert(fagsak) + val fagsystemsbehandling = behandling.aktivFagsystemsbehandling.copy( + tilbakekrevingsvalg = Tilbakekrevingsvalg + .OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + behandlingRepository.insert( + behandling.copy( + fagsystemsbehandling = setOf(fagsystemsbehandling), + status = Behandlingsstatus.UTREDES, + ), + ) + val feilKravgrunnlagBeløp = Testdata.feilKravgrunnlagsbeløp433.copy(nyttBeløp = BigDecimal("100")) + val ytelKravgrunnlagsbeløp433 = + Testdata.ytelKravgrunnlagsbeløp433.copy( + opprinneligUtbetalingsbeløp = BigDecimal("100"), + tilbakekrevesBeløp = BigDecimal("100"), + ) + + val kravgrunnlag = Testdata.kravgrunnlag431 + .copy( + kontrollfelt = "2019-11-22-19.09.31.458065", + perioder = setOf( + Testdata.kravgrunnlagsperiode432.copy( + beløp = setOf( + feilKravgrunnlagBeløp, + ytelKravgrunnlagsbeløp433, + ), + ), + ), + ) + + kravgrunnlagRepository.insert(kravgrunnlag) + behandlingsstegstilstandRepository.insert( + lagBehandlingsstegstilstand( + Behandlingssteg.GRUNNLAG, + Behandlingsstegstatus.UTFØRT, + ), + ) + behandlingsstegstilstandRepository.insert( + lagBehandlingsstegstilstand( + Behandlingssteg.FAKTA, + Behandlingsstegstatus.KLAR, + ), + ) + } + + @Test + fun `doTask skal ikke behandle når behandling allerede er avsluttet`() { + val behandling = behandlingRepository.findByIdOrThrow(behandling.id).copy(status = Behandlingsstatus.AVSLUTTET) + behandlingRepository.update(behandling) + + val exception = shouldThrow { automatiskSaksbehandlingTask.doTask(lagTask()) } + exception.message shouldBe "Behandling med id=${behandling.id} er allerede ferdig behandlet" + } + + @Test + fun `doTask skal ikke behandle når behandling er på vent`() { + behandlingskontrollService.settBehandlingPåVent( + behandling.id, + Venteårsak.ENDRE_TILKJENT_YTELSE, + LocalDate.now().plusWeeks(2), + ) + + val exception = shouldThrow { automatiskSaksbehandlingTask.doTask(lagTask()) } + exception.message shouldBe "Behandling med id=${behandling.id} er på vent, kan ikke behandle steg FAKTA" + } + + @Test + fun `doTask skal behandle behandling automatisk`() { + automatiskSaksbehandlingTask.doTask(lagTask()) + mockTaskExecution() + + val behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandling.saksbehandlingstype shouldBe Saksbehandlingstype.AUTOMATISK_IKKE_INNKREVING_LAVT_BELØP + behandling.ansvarligSaksbehandler shouldBe "VL" + behandling.ansvarligBeslutter shouldBe "VL" + behandling.status shouldBe Behandlingsstatus.AVSLUTTET + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.IVERKSETT_VEDTAK, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.AVSLUTTET, Behandlingsstegstatus.UTFØRT) + + val faktaFeilutbetaling = faktaFeilutbetalingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + faktaFeilutbetaling.shouldNotBeNull() + faktaFeilutbetaling.begrunnelse shouldBe Constants.AUTOMATISK_SAKSBEHANDLING_BEGUNNLESE + faktaFeilutbetaling.perioder.shouldHaveSingleElement { + Hendelsestype.ANNET == it.hendelsestype && + Hendelsesundertype.ANNET_FRITEKST == it.hendelsesundertype + } + + foreldelsesRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldBeNull() + + val vilkårsvurdering = vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + vilkårsvurdering.shouldNotBeNull() + vilkårsvurdering.perioder.shouldHaveSingleElement { + Constants.AUTOMATISK_SAKSBEHANDLING_BEGUNNLESE == it.begrunnelse && + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT == it.vilkårsvurderingsresultat && + it.aktsomhet != null && it.aktsomhet!!.aktsomhet == Aktsomhet.SIMPEL_UAKTSOMHET + !it.aktsomhet!!.tilbakekrevSmåbeløp + } + + vedtaksbrevsoppsummeringRepository.findByBehandlingId(behandling.id).shouldBeNull() + } + + private fun lagBehandlingsstegstilstand( + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ): Behandlingsstegstilstand { + return Behandlingsstegstilstand( + behandlingId = behandling.id, + behandlingssteg = behandlingssteg, + behandlingsstegsstatus = behandlingsstegstatus, + ) + } + + private fun lagTask(): Task { + return Task(type = AutomatiskSaksbehandlingTask.TYPE, payload = behandling.id.toString()) + } + + private fun assertBehandlingsstegstilstand( + behandlingsstegstilstand: List, + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ) { + behandlingsstegstilstand.any { + behandlingssteg == it.behandlingssteg && + behandlingsstegstatus == it.behandlingsstegsstatus + }.shouldBeTrue() + } + + private fun mockTaskExecution() { + val sendVedtakTilØkonomiTask = Task( + type = SendØkonomiTilbakekrevingsvedtakTask.TYPE, + payload = behandling.id.toString(), + properties = Properties().apply { + setProperty( + "ansvarligSaksbehandler", + ContextService.hentSaksbehandler(), + ) + }, + ) + every { taskService.save(sendVedtakTilØkonomiTask) }.run { + sendØkonomiTilbakekrevingsvedtakTask.doTask(sendVedtakTilØkonomiTask) + } + + val vedtaksbrevTask = Task(type = SendVedtaksbrevTask.TYPE, payload = behandling.id.toString()) + every { taskService.save(vedtaksbrevTask) }.run { + sendVedtaksbrevTask.doTask(vedtaksbrevTask) + } + + val avsluttTask = Task(type = AvsluttBehandlingTask.TYPE, payload = behandling.id.toString()) + every { taskService.save(avsluttTask) }.run { avsluttBehandlingTask.doTask(avsluttTask) } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/steg/StegServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/steg/StegServiceTest.kt new file mode 100644 index 000000000..0e8dd8e94 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/steg/StegServiceTest.kt @@ -0,0 +1,1085 @@ +package no.nav.familie.tilbake.behandling.steg + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSingleElement +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockkObject +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.kontrakter.felles.Regelverk +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.tilbakekreving.Vergetype +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.BehandlingsstegFaktaDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegFatteVedtaksstegDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegForeldelseDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegForeslåVedtaksstegDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegVergeDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegVilkårsvurderingDto +import no.nav.familie.tilbake.api.dto.FaktaFeilutbetalingsperiodeDto +import no.nav.familie.tilbake.api.dto.ForeldelsesperiodeDto +import no.nav.familie.tilbake.api.dto.FritekstavsnittDto +import no.nav.familie.tilbake.api.dto.GodTroDto +import no.nav.familie.tilbake.api.dto.PeriodeMedTekstDto +import no.nav.familie.tilbake.api.dto.VergeDto +import no.nav.familie.tilbake.api.dto.VilkårsvurderingsperiodeDto +import no.nav.familie.tilbake.api.dto.VurdertTotrinnDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.VergeService +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Iverksettingsstatus +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingService +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.ForeldelseService +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.historikkinnslag.LagHistorikkinnslagTask +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.iverksettvedtak.task.SendØkonomiTilbakekrevingsvedtakTask +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.oppgave.FerdigstillOppgaveTask +import no.nav.familie.tilbake.oppgave.LagOppgaveTask +import no.nav.familie.tilbake.totrinn.TotrinnsvurderingRepository +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingRepository +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.domain.Pageable +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth +import java.util.UUID + +internal class StegServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var vilkårsvurderingRepository: VilkårsvurderingRepository + + @Autowired + private lateinit var totrinnsvurderingRepository: TotrinnsvurderingRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + @Autowired + private lateinit var faktaFeilutbetalingService: FaktaFeilutbetalingService + + @Autowired + private lateinit var foreldelseService: ForeldelseService + + @Autowired + private lateinit var vergeService: VergeService + + @Autowired + private lateinit var stegService: StegService + + private val fagsak = Testdata.fagsak + private val behandling = Testdata.behandling + private val behandlingId = behandling.id + + private val FOM = YearMonth.now().minusMonths(1).atDay(1) + private val TOM = YearMonth.now().atEndOfMonth() + + @BeforeEach + fun init() { + fagsakRepository.insert(fagsak) + behandlingRepository.insert(behandling) + + mockkObject(ContextService) + every { ContextService.hentSaksbehandler() }.returns("Z0000") + } + + @AfterEach + fun tearDown() { + clearMocks(ContextService) + } + + @Test + fun `håndterSteg skal utføre grunnlagssteg og fortsette til Fakta steg når behandling ikke har verge og har fått grunnlag`() { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update(behandling.copy(verger = emptySet())) + + lagBehandlingsstegstilstand( + Behandlingssteg.GRUNNLAG, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + ) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + stegService.håndterSteg(behandlingId) + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + } + + @Test + fun `håndterSteg skal utføre grunnlagssteg,autoutføre verge steg og fortsette til Fakta steg når behandling har verge`() { + lagBehandlingsstegstilstand( + Behandlingssteg.GRUNNLAG, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + ) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + stegService.håndterSteg(behandlingId) + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VERGE, Behandlingsstegstatus.AUTOUTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + } + + @Test + fun `håndterSteg skal ikke utføre faktafeilutbetaling når behandling er avsluttet`() { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val behandlingsstegFaktaDto = lagBehandlingsstegFaktaDto() + + val exception = shouldThrow { stegService.håndterSteg(behandlingId, behandlingsstegFaktaDto) } + exception.message shouldBe "Behandling med id=$behandlingId er allerede ferdig behandlet" + } + + @Test + fun `håndterStegAutomatisk skal ikke utføre automatisk behandling når den følger EØS-regelverket`() { + behandlingRepository.findByIdOrThrow(behandlingId) + .copy(regelverk = Regelverk.EØS) + .also { behandlingRepository.update(it) } + + val exception = shouldThrow { stegService.håndterStegAutomatisk(behandlingId) } + exception.message shouldBe "Behandling med id=$behandlingId behandles etter EØS-regelverket, og skal dermed ikke behandles automatisk." + } + + @Test + fun `håndterSteg skal ikke utføre faktafeilutbetaling når behandling er på vent`() { + lagBehandlingsstegstilstand( + Behandlingssteg.FAKTA, + Behandlingsstegstatus.VENTER, + Venteårsak.AVVENTER_DOKUMENTASJON, + ) + + val behandlingsstegFaktaDto = lagBehandlingsstegFaktaDto() + + val exception = shouldThrow { stegService.håndterSteg(behandlingId, behandlingsstegFaktaDto) } + exception.message shouldBe "Behandling med id=$behandlingId er på vent, kan ikke behandle steg FAKTA" + } + + @Test + fun `håndterSteg skal utføre faktafeilutbetalingssteg for behandling`() { + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + val behandlingsstegFaktaDto = lagBehandlingsstegFaktaDto() + stegService.håndterSteg(behandlingId, behandlingsstegFaktaDto) + + val behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + behandlingsstegstilstander.size shouldBe 3 + val aktivtBehandlingssteg = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstander) + aktivtBehandlingssteg?.behandlingssteg shouldBe Behandlingssteg.VILKÅRSVURDERING + aktivtBehandlingssteg?.behandlingsstegsstatus shouldBe Behandlingsstegstatus.KLAR + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + assertBehandlingsstatus(behandlingId, Behandlingsstatus.UTREDES) + + assertFaktadata(behandlingsstegFaktaDto) + + // historikk + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.FAKTA_VURDERT, Aktør.SAKSBEHANDLER) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.FORELDELSE_VURDERT, Aktør.VEDTAKSLØSNING) + } + + @Test + fun `håndterSteg skal utføre faktafeilutbetaling og fortsette til vilkårsvurdering når behandling er på foreslåvedtak`() { + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.KLAR) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + val behandlingsstegFaktaDto = lagBehandlingsstegFaktaDto() + + stegService.håndterSteg(behandlingId, behandlingsstegFaktaDto) + + val behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + behandlingsstegstilstander.size shouldBe 4 + val aktivtBehandlingssteg = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstander) + aktivtBehandlingssteg?.behandlingssteg shouldBe Behandlingssteg.VILKÅRSVURDERING + aktivtBehandlingssteg?.behandlingsstegsstatus shouldBe Behandlingsstegstatus.KLAR + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + assertBehandlingssteg( + behandlingsstegstilstander, + Behandlingssteg.FORESLÅ_VEDTAK, + Behandlingsstegstatus.TILBAKEFØRT, + ) + assertBehandlingsstatus(behandlingId, Behandlingsstatus.UTREDES) + + assertFaktadata(behandlingsstegFaktaDto) + + // historikk + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.FAKTA_VURDERT, Aktør.SAKSBEHANDLER) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.FORELDELSE_VURDERT, Aktør.VEDTAKSLØSNING) + } + + @Test + fun `håndterSteg skal utføre faktafeilutbetaling og fortsette til foreldelse når foreldelse ikke er autoutført`() { + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.KLAR) + + var kravgrunnlag431 = Testdata.kravgrunnlag431 + for (grunnlagsperiode in kravgrunnlag431.perioder) { + kravgrunnlag431 = + kravgrunnlag431.copy( + perioder = + setOf( + grunnlagsperiode.copy( + periode = Månedsperiode( + fom = LocalDate.of(2010, 1, 1), + tom = LocalDate.of(2010, 1, 31), + ), + ), + ), + ) + } + kravgrunnlagRepository.insert(kravgrunnlag431) + val faktaFeilutbetaltePerioderDto = FaktaFeilutbetalingsperiodeDto( + periode = Datoperiode( + LocalDate.of(2010, 1, 1), + LocalDate.of(2010, 1, 31), + ), + hendelsestype = Hendelsestype.ANNET, + hendelsesundertype = Hendelsesundertype.ANNET_FRITEKST, + ) + val behandlingsstegFaktaDto = BehandlingsstegFaktaDto( + feilutbetaltePerioder = listOf(faktaFeilutbetaltePerioderDto), + begrunnelse = "testverdi", + ) + + stegService.håndterSteg(behandlingId, behandlingsstegFaktaDto) + + val behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + behandlingsstegstilstander.size shouldBe 4 + val aktivtBehandlingssteg = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstander) + aktivtBehandlingssteg?.behandlingssteg shouldBe Behandlingssteg.FORELDELSE + aktivtBehandlingssteg?.behandlingsstegsstatus shouldBe Behandlingsstegstatus.KLAR + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg( + behandlingsstegstilstander, + Behandlingssteg.VILKÅRSVURDERING, + Behandlingsstegstatus.TILBAKEFØRT, + ) + assertBehandlingssteg( + behandlingsstegstilstander, + Behandlingssteg.FORESLÅ_VEDTAK, + Behandlingsstegstatus.TILBAKEFØRT, + ) + assertBehandlingsstatus(behandlingId, Behandlingsstatus.UTREDES) + + assertFaktadata(behandlingsstegFaktaDto) + // historikk + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.FAKTA_VURDERT, Aktør.SAKSBEHANDLER) + } + + @Test + fun `håndterSteg skal utføre foreldelse og fortsette til foreslå vedtak når alle perioder er foreldet`() { + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.KLAR) + + var kravgrunnlag431 = Testdata.kravgrunnlag431 + for (grunnlagsperiode in kravgrunnlag431.perioder) { + kravgrunnlag431 = + kravgrunnlag431.copy( + perioder = + setOf( + grunnlagsperiode.copy( + periode = Månedsperiode( + fom = LocalDate.of(2010, 1, 1), + tom = LocalDate.of(2010, 1, 31), + ), + ), + ), + ) + } + kravgrunnlagRepository.insert(kravgrunnlag431) + val behandlingsstegForeldelseDto = + BehandlingsstegForeldelseDto( + listOf( + ForeldelsesperiodeDto( + Datoperiode( + LocalDate.of(2010, 1, 1), + LocalDate.of(2010, 1, 31), + ), + "foreldelses begrunnelse", + Foreldelsesvurderingstype.FORELDET, + ), + ), + ) + stegService.håndterSteg(behandlingId, behandlingsstegForeldelseDto) + + val behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + behandlingsstegstilstander.size shouldBe 4 + val aktivtBehandlingssteg = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstander) + aktivtBehandlingssteg?.behandlingssteg shouldBe Behandlingssteg.FORESLÅ_VEDTAK + aktivtBehandlingssteg?.behandlingsstegsstatus shouldBe Behandlingsstegstatus.KLAR + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FORELDELSE, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg( + behandlingsstegstilstander, + Behandlingssteg.VILKÅRSVURDERING, + Behandlingsstegstatus.AUTOUTFØRT, + ) + assertBehandlingsstatus(behandlingId, Behandlingsstatus.UTREDES) + + assertForeldelsesdata(behandlingsstegForeldelseDto.foreldetPerioder[0]) + + // historikk + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.FORELDELSE_VURDERT, Aktør.SAKSBEHANDLER) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.VILKÅRSVURDERING_VURDERT, Aktør.VEDTAKSLØSNING) + } + + @Test + fun `håndterSteg skal utføre foreldelse og fortsette til vilkårsvurdering når minst en periode ikke er foreldet`() { + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.KLAR) + + val førstePeriode = Testdata.kravgrunnlagsperiode432 + .copy( + id = UUID.randomUUID(), + periode = Månedsperiode( + fom = LocalDate.of(2018, 1, 1), + tom = LocalDate.of(2018, 1, 31), + ), + beløp = setOf( + Testdata.feilKravgrunnlagsbeløp433.copy(id = UUID.randomUUID()), + Testdata.ytelKravgrunnlagsbeløp433.copy(id = UUID.randomUUID()), + ), + ) + val andrePeriode = Testdata.kravgrunnlagsperiode432 + .copy( + id = UUID.randomUUID(), + periode = Månedsperiode( + fom = LocalDate.of(2018, 2, 1), + tom = LocalDate.of(2018, 2, 28), + ), + beløp = setOf( + Testdata.feilKravgrunnlagsbeløp433.copy(id = UUID.randomUUID()), + Testdata.ytelKravgrunnlagsbeløp433.copy(id = UUID.randomUUID()), + ), + ) + + val kravgrunnlag431 = Testdata.kravgrunnlag431.copy(perioder = setOf(førstePeriode, andrePeriode)) + kravgrunnlagRepository.insert(kravgrunnlag431) + val behandlingsstegForeldelseDto = + BehandlingsstegForeldelseDto( + listOf( + ForeldelsesperiodeDto( + førstePeriode.periode.toDatoperiode(), + "foreldelses begrunnelse", + Foreldelsesvurderingstype.FORELDET, + ), + ForeldelsesperiodeDto( + andrePeriode.periode.toDatoperiode(), + "foreldelses begrunnelse", + Foreldelsesvurderingstype.IKKE_FORELDET, + ), + ), + ) + stegService.håndterSteg(behandlingId, behandlingsstegForeldelseDto) + + val behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + behandlingsstegstilstander.size shouldBe 3 + val aktivtBehandlingssteg = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstander) + aktivtBehandlingssteg?.behandlingssteg shouldBe Behandlingssteg.VILKÅRSVURDERING + aktivtBehandlingssteg?.behandlingsstegsstatus shouldBe Behandlingsstegstatus.KLAR + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FORELDELSE, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstatus(behandlingId, Behandlingsstatus.UTREDES) + + // historikk + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.FORELDELSE_VURDERT, Aktør.SAKSBEHANDLER) + } + + @Test + fun `håndterSteg skal utføre foreldelse og fortsette til foreslå vedtak når alle perioder endret til foreldet`() { + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.KLAR) + + var kravgrunnlag431 = Testdata.kravgrunnlag431 + for (grunnlagsperiode in kravgrunnlag431.perioder) { + kravgrunnlag431 = + kravgrunnlag431.copy( + perioder = setOf( + grunnlagsperiode.copy( + periode = Månedsperiode( + LocalDate.of(2010, 1, 1), + LocalDate.of(2010, 1, 31), + ), + ), + ), + ) + } + kravgrunnlagRepository.insert(kravgrunnlag431) + // foreldelsesteg vurderte som IKKE_FORELDET med første omgang + var behandlingsstegForeldelseDto = + BehandlingsstegForeldelseDto( + listOf( + ForeldelsesperiodeDto( + Datoperiode( + LocalDate.of(2010, 1, 1), + LocalDate.of(2010, 1, 31), + ), + "foreldelses begrunnelse", + Foreldelsesvurderingstype.IKKE_FORELDET, + ), + ), + ) + stegService.håndterSteg(behandlingId, behandlingsstegForeldelseDto) + var behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.KLAR) + + // behandle vilkårsvurderingssteg + val behandlingsstegVilkårsvurderingDto = lagBehandlingsstegVilkårsvurderingDto( + Datoperiode( + LocalDate.of(2010, 1, 1), + LocalDate.of(2010, 1, 31), + ), + ) + stegService.håndterSteg(behandlingId, behandlingsstegVilkårsvurderingDto) + behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.KLAR) + assertBehandlingssteg( + behandlingsstegstilstander, + Behandlingssteg.VILKÅRSVURDERING, + Behandlingsstegstatus.UTFØRT, + ) + assertBehandlingsstatus(behandlingId, Behandlingsstatus.UTREDES) + + // behandler foreldelse steg på nytt og endrer periode til foreldet + behandlingsstegForeldelseDto = + BehandlingsstegForeldelseDto( + listOf( + ForeldelsesperiodeDto( + Datoperiode( + LocalDate.of(2010, 1, 1), + LocalDate.of(2010, 1, 31), + ), + "foreldelses begrunnelse", + Foreldelsesvurderingstype.FORELDET, + ), + ), + ) + stegService.håndterSteg(behandlingId, behandlingsstegForeldelseDto) + behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.KLAR) + assertBehandlingssteg( + behandlingsstegstilstander, + Behandlingssteg.VILKÅRSVURDERING, + Behandlingsstegstatus.AUTOUTFØRT, + ) + assertBehandlingsstatus(behandlingId, Behandlingsstatus.UTREDES) + + // deaktiverte tildligere behandlet vilkårsvurdering når alle perioder er foreldet + vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId).shouldBeNull() + + // historikk + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.FORELDELSE_VURDERT, Aktør.SAKSBEHANDLER) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.VILKÅRSVURDERING_VURDERT, Aktør.SAKSBEHANDLER) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.VILKÅRSVURDERING_VURDERT, Aktør.VEDTAKSLØSNING) + } + + @Test + fun `håndterSteg skal utføre foreslå vedtak og forsette til fatte vedtak`() { + // behandle fakta steg + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + val behandlingsstegFaktaDto = lagBehandlingsstegFaktaDto() + stegService.håndterSteg(behandlingId, lagBehandlingsstegFaktaDto()) + + // behandle vilkårsvurderingssteg + stegService.håndterSteg(behandlingId, lagBehandlingsstegVilkårsvurderingDto(Datoperiode(FOM, TOM))) + + val fritekstavsnitt = + FritekstavsnittDto( + perioderMedTekst = listOf( + PeriodeMedTekstDto( + periode = Datoperiode(FOM, TOM), + faktaAvsnitt = "fakta tekst", + ), + ), + ) + stegService.håndterSteg(behandlingId, BehandlingsstegForeslåVedtaksstegDto(fritekstavsnitt)) + val behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + assertBehandlingssteg( + behandlingsstegstilstander, + Behandlingssteg.VILKÅRSVURDERING, + Behandlingsstegstatus.UTFØRT, + ) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + assertBehandlingsstatus(behandlingId, Behandlingsstatus.FATTER_VEDTAK) + assertFaktadata(behandlingsstegFaktaDto) + + assertOppgave(FerdigstillOppgaveTask.TYPE) + assertOppgave(LagOppgaveTask.TYPE) + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.FORESLÅ_VEDTAK_VURDERT, Aktør.SAKSBEHANDLER) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.BEHANDLING_SENDT_TIL_BESLUTTER, Aktør.SAKSBEHANDLER) + } + + @Test + fun `håndterSteg skal utføre foreslå vedtak på nytt når beslutter underkjente steg og forsette til fatte vedtak`() { + // behandle fakta steg + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + stegService.håndterSteg(behandlingId, lagBehandlingsstegFaktaDto()) + + // behandle vilkårsvurderingssteg + stegService.håndterSteg(behandlingId, lagBehandlingsstegVilkårsvurderingDto(Datoperiode(FOM, TOM))) + + val fritekstavsnitt = FritekstavsnittDto( + perioderMedTekst = listOf( + PeriodeMedTekstDto( + periode = Datoperiode(FOM, TOM), + faktaAvsnitt = "fakta tekst", + ), + ), + ) + stegService.håndterSteg(behandlingId, BehandlingsstegForeslåVedtaksstegDto(fritekstavsnitt = fritekstavsnitt)) + + assertOppgave(FerdigstillOppgaveTask.TYPE) + assertOppgave(LagOppgaveTask.TYPE) + + stegService.håndterSteg(behandlingId, lagBehandlingsstegFatteVedtaksstegDto(godkjent = false)) + + assertOppgave(FerdigstillOppgaveTask.TYPE, 2) + assertOppgave(LagOppgaveTask.TYPE, 2) + + val behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.KLAR) + assertBehandlingssteg( + behandlingsstegstilstander, + Behandlingssteg.FATTE_VEDTAK, + Behandlingsstegstatus.TILBAKEFØRT, + ) + + stegService.håndterSteg(behandlingId, BehandlingsstegForeslåVedtaksstegDto(fritekstavsnitt = fritekstavsnitt)) + + assertOppgave(FerdigstillOppgaveTask.TYPE, 3) + assertOppgave(LagOppgaveTask.TYPE, 3) + + assertHistorikkTask( + TilbakekrevingHistorikkinnslagstype.BEHANDLING_SENDT_TILBAKE_TIL_SAKSBEHANDLER, + Aktør.BESLUTTER, + ) + } + + @Test + fun `håndterSteg skal utføre fatte vedtak og forsette til iverksette vedtak når beslutter godkjenner alt`() { + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + + stegService.håndterSteg(behandlingId, lagBehandlingsstegFatteVedtaksstegDto(godkjent = true)) + + val behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.IVERKSETT_VEDTAK, Behandlingsstegstatus.KLAR) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.UTFØRT) + + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandling.ansvarligBeslutter shouldBe "Z0000" + behandling.status shouldBe Behandlingsstatus.IVERKSETTER_VEDTAK + + val totrinnsvurderinger = totrinnsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + totrinnsvurderinger.shouldNotBeEmpty() + totrinnsvurderinger.any { it.behandlingssteg == Behandlingssteg.FAKTA && it.godkjent }.shouldBeTrue() + totrinnsvurderinger.any { it.behandlingssteg == Behandlingssteg.FORELDELSE }.shouldBeFalse() + totrinnsvurderinger.any { it.behandlingssteg == Behandlingssteg.VILKÅRSVURDERING && it.godkjent }.shouldBeTrue() + totrinnsvurderinger.any { it.behandlingssteg == Behandlingssteg.FORESLÅ_VEDTAK && it.godkjent }.shouldBeTrue() + + assertOppgave(FerdigstillOppgaveTask.TYPE) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.VEDTAK_FATTET, Aktør.BESLUTTER) + + val behandlingsresultat = behandling.sisteResultat + behandlingsresultat.shouldNotBeNull() + behandlingsresultat.type shouldBe Behandlingsresultatstype.INGEN_TILBAKEBETALING + val behandlingsvedtak = behandlingsresultat.behandlingsvedtak + behandlingsvedtak.shouldNotBeNull() + behandlingsvedtak.iverksettingsstatus shouldBe Iverksettingsstatus.UNDER_IVERKSETTING + taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)) + .any { it.type == SendØkonomiTilbakekrevingsvedtakTask.TYPE }.shouldBeTrue() + } + + @Test + fun `håndterSteg skal tilbakeføre fatte vedtak og flytte til foreslå vedtak når beslutter underkjente steg`() { + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + + stegService.håndterSteg(behandlingId, lagBehandlingsstegFatteVedtaksstegDto(godkjent = false)) + + val behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.KLAR) + assertBehandlingssteg( + behandlingsstegstilstander, + Behandlingssteg.FATTE_VEDTAK, + Behandlingsstegstatus.TILBAKEFØRT, + ) + + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandling.ansvarligBeslutter shouldBe null + behandling.status shouldBe Behandlingsstatus.UTREDES + + val totrinnsvurderinger = totrinnsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) + totrinnsvurderinger.shouldNotBeEmpty() + totrinnsvurderinger.any { it.behandlingssteg == Behandlingssteg.FAKTA && !it.godkjent }.shouldBeTrue() + totrinnsvurderinger.any { it.behandlingssteg == Behandlingssteg.FORELDELSE }.shouldBeFalse() + totrinnsvurderinger.any { it.behandlingssteg == Behandlingssteg.VILKÅRSVURDERING && !it.godkjent } + .shouldBeTrue() + totrinnsvurderinger.any { it.behandlingssteg == Behandlingssteg.FORESLÅ_VEDTAK && !it.godkjent }.shouldBeTrue() + + assertOppgave(FerdigstillOppgaveTask.TYPE) + assertOppgave(LagOppgaveTask.TYPE) + } + + @Test + fun `håndterSteg skal ikke utføre fakta steg når behandling er på fatte vedtak steg`() { + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + + stegService.håndterSteg(behandlingId, lagBehandlingsstegFaktaDto()) + + val behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + + totrinnsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandlingId).shouldBeEmpty() + } + + @Test + fun `håndterSteg skal ikke utføre fatte vedtak steg når beslutter er samme som saksbehandler`() { + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update( + behandling.copy( + status = Behandlingsstatus.FATTER_VEDTAK, + ansvarligSaksbehandler = "Z0000", + ), + ) + + val exception = shouldThrow { + stegService.håndterSteg(behandlingId, lagBehandlingsstegFatteVedtaksstegDto(godkjent = true)) + } + + exception.message shouldBe "ansvarlig beslutter kan ikke være samme som ansvarlig saksbehandler" + } + + @Test + fun `håndterSteg skal opprette og utføre verge steg når behandling er på foreslå vedtak`() { + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + + vergeService.opprettVergeSteg(behandlingId) + + var behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingssteg( + behandlingsstegstilstander, + Behandlingssteg.FORESLÅ_VEDTAK, + Behandlingsstegstatus.TILBAKEFØRT, + ) + assertBehandlingssteg( + behandlingsstegstilstander, + Behandlingssteg.VILKÅRSVURDERING, + Behandlingsstegstatus.TILBAKEFØRT, + ) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FORELDELSE, Behandlingsstegstatus.TILBAKEFØRT) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FAKTA, Behandlingsstegstatus.TILBAKEFØRT) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.VERGE, Behandlingsstegstatus.KLAR) + + val vergeData = BehandlingsstegVergeDto( + verge = VergeDto( + ident = "32132132111", + type = Vergetype.VERGE_FOR_BARN, + navn = "testverdi", + begrunnelse = "testverdi", + ), + ) + stegService.håndterSteg(behandlingId, vergeData) + behandlingsstegstilstander = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingssteg( + behandlingsstegstilstander, + Behandlingssteg.FORESLÅ_VEDTAK, + Behandlingsstegstatus.TILBAKEFØRT, + ) + assertBehandlingssteg( + behandlingsstegstilstander, + Behandlingssteg.VILKÅRSVURDERING, + Behandlingsstegstatus.TILBAKEFØRT, + ) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FORELDELSE, Behandlingsstegstatus.TILBAKEFØRT) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + assertBehandlingssteg(behandlingsstegstilstander, Behandlingssteg.VERGE, Behandlingsstegstatus.UTFØRT) + } + + @Test + fun `gjenopptaSteg skal gjenoppta behandling og fortsette til grunnlag når behandling er i varselssteg uten grunnlag`() { + lagBehandlingsstegstilstand( + Behandlingssteg.VARSEL, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + ) + + stegService.gjenopptaSteg(behandlingId) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + val aktivtBehandlingsstegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingId) + aktivtBehandlingsstegstilstand.shouldNotBeNull() + aktivtBehandlingsstegstilstand.behandlingssteg shouldBe Behandlingssteg.GRUNNLAG + aktivtBehandlingsstegstilstand.behandlingsstegsstatus shouldBe Behandlingsstegstatus.VENTER + assertBehandlingsstatus(behandlingId, Behandlingsstatus.UTREDES) + aktivtBehandlingsstegstilstand.venteårsak shouldBe Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG + aktivtBehandlingsstegstilstand.tidsfrist shouldBe LocalDate.now() + .plusWeeks(Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG.defaultVenteTidIUker) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VARSEL, Behandlingsstegstatus.UTFØRT) + } + + @Test + fun `gjenopptaSteg skal gjenoppta behandling og fortsette til fakta når behandling er i varselssteg med grunnlag`() { + lagBehandlingsstegstilstand( + Behandlingssteg.VARSEL, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + ) + + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + stegService.gjenopptaSteg(behandlingId) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + val aktivtBehandlingsstegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingId) + aktivtBehandlingsstegstilstand.shouldNotBeNull() + aktivtBehandlingsstegstilstand.behandlingssteg shouldBe Behandlingssteg.FAKTA + aktivtBehandlingsstegstilstand.behandlingsstegsstatus shouldBe Behandlingsstegstatus.KLAR + assertBehandlingsstatus(behandlingId, Behandlingsstatus.UTREDES) + + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VARSEL, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VERGE, Behandlingsstegstatus.AUTOUTFØRT) + } + + @Test + fun `gjenopptaSteg skal ikke gjenoppta behandling når behandling er i grunnlagssteg uten grunnlag`() { + lagBehandlingsstegstilstand( + Behandlingssteg.GRUNNLAG, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + ) + + stegService.gjenopptaSteg(behandlingId) + + val aktivtBehandlingsstegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingId) + aktivtBehandlingsstegstilstand.shouldNotBeNull() + aktivtBehandlingsstegstilstand.behandlingssteg shouldBe Behandlingssteg.GRUNNLAG + aktivtBehandlingsstegstilstand.behandlingsstegsstatus shouldBe Behandlingsstegstatus.VENTER + aktivtBehandlingsstegstilstand.venteårsak shouldBe Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG + aktivtBehandlingsstegstilstand.tidsfrist shouldBe LocalDate.now() + .plusWeeks(Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG.defaultVenteTidIUker) + } + + @Test + fun `gjenopptaSteg skal gjenoppta behandling når behandling er i grunnlagssteg med grunnlag`() { + lagBehandlingsstegstilstand( + Behandlingssteg.GRUNNLAG, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + ) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + stegService.gjenopptaSteg(behandlingId) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + val aktivtBehandlingsstegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingId) + aktivtBehandlingsstegstilstand.shouldNotBeNull() + aktivtBehandlingsstegstilstand.behandlingssteg shouldBe Behandlingssteg.FAKTA + aktivtBehandlingsstegstilstand.behandlingsstegsstatus shouldBe Behandlingsstegstatus.KLAR + assertBehandlingsstatus(behandlingId, Behandlingsstatus.UTREDES) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.VERGE, Behandlingsstegstatus.AUTOUTFØRT) + } + + @Test + fun `gjenopptaSteg skal gjenoppta behandling når behandling er i vilkårsvurderingssteg`() { + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + lagBehandlingsstegstilstand( + Behandlingssteg.VILKÅRSVURDERING, + Behandlingsstegstatus.VENTER, + Venteårsak.AVVENTER_DOKUMENTASJON, + ) + + stegService.gjenopptaSteg(behandlingId) + + val aktivtBehandlingsstegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingId) + aktivtBehandlingsstegstilstand.shouldNotBeNull() + aktivtBehandlingsstegstilstand.behandlingssteg shouldBe Behandlingssteg.VILKÅRSVURDERING + aktivtBehandlingsstegstilstand.behandlingsstegsstatus shouldBe Behandlingsstegstatus.KLAR + assertBehandlingsstatus(behandlingId, Behandlingsstatus.UTREDES) + } + + @Test + fun `kanAnsvarligSaksbehandlerOppdateres skal returnere true når behandling er sendt til beslutter`() { + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.KLAR) + + val behandlingsstegDto = + BehandlingsstegForeslåVedtaksstegDto(FritekstavsnittDto(perioderMedTekst = emptyList())) + stegService.kanAnsvarligSaksbehandlerOppdateres(behandlingId, behandlingsstegDto) + .shouldBeTrue() + } + + @Test + fun `kanAnsvarligSaksbehandlerOppdateres skal returnere false når beslutter underkjenner vedtak`() { + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + + val behandlingsstegDto = lagBehandlingsstegFatteVedtaksstegDto(godkjent = false) + stegService.kanAnsvarligSaksbehandlerOppdateres(behandlingId, behandlingsstegDto) + .shouldBeFalse() + } + + @Test + fun `kanAnsvarligSaksbehandlerOppdateres skal returnere false når beslutter godkjenner vedtak`() { + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + + val behandlingsstegDto = lagBehandlingsstegFatteVedtaksstegDto(godkjent = true) + stegService.kanAnsvarligSaksbehandlerOppdateres(behandlingId, behandlingsstegDto) + .shouldBeFalse() + } + + @Test + fun `kanAnsvarligSaksbehandlerOppdateres skal returnere true når saksbehandler utfører vilkårsvurderingssteg`() { + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.KLAR) + + val behandlingsstegDto = lagBehandlingsstegVilkårsvurderingDto( + Datoperiode( + LocalDate.of(2021, 1, 1), + LocalDate.of(2021, 1, 31), + ), + ) + stegService.kanAnsvarligSaksbehandlerOppdateres(behandlingId, behandlingsstegDto) + .shouldBeTrue() + } + + private fun lagBehandlingsstegstilstand( + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + venteårsak: Venteårsak? = null, + ) { + val tidsfrist: LocalDate? = venteårsak?.let { LocalDate.now().plusWeeks(it.defaultVenteTidIUker) } + behandlingsstegstilstandRepository.insert( + Behandlingsstegstilstand( + behandlingssteg = behandlingssteg, + behandlingsstegsstatus = behandlingsstegstatus, + venteårsak = venteårsak, + tidsfrist = tidsfrist, + behandlingId = behandlingId, + ), + ) + } + + private fun lagBehandlingsstegFaktaDto(): BehandlingsstegFaktaDto { + val faktaFeilutbetaltePerioderDto = FaktaFeilutbetalingsperiodeDto( + periode = Datoperiode(FOM, TOM), + hendelsestype = Hendelsestype.ANNET, + hendelsesundertype = Hendelsesundertype.ANNET_FRITEKST, + ) + return BehandlingsstegFaktaDto( + feilutbetaltePerioder = listOf(faktaFeilutbetaltePerioderDto), + begrunnelse = "testverdi", + ) + } + + private fun lagBehandlingsstegVilkårsvurderingDto(periode: Datoperiode): BehandlingsstegVilkårsvurderingDto { + return BehandlingsstegVilkårsvurderingDto( + listOf( + VilkårsvurderingsperiodeDto( + periode, + Vilkårsvurderingsresultat.GOD_TRO, + "Vilkårsvurdering begrunnelse", + GodTroDto( + false, + null, + "God tro begrunnelse", + ), + ), + ), + ) + } + + private fun lagBehandlingsstegFatteVedtaksstegDto(godkjent: Boolean): BehandlingsstegFatteVedtaksstegDto { + return BehandlingsstegFatteVedtaksstegDto( + listOf( + VurdertTotrinnDto( + behandlingssteg = Behandlingssteg.FAKTA, + godkjent = godkjent, + begrunnelse = "fakta totrinn begrunnelse", + ), + VurdertTotrinnDto( + behandlingssteg = Behandlingssteg.FORELDELSE, + godkjent = godkjent, + begrunnelse = "foreldelse totrinn begrunnelse", + ), + VurdertTotrinnDto( + behandlingssteg = Behandlingssteg.VILKÅRSVURDERING, + godkjent = godkjent, + begrunnelse = "vilkårsvurdering totrinn begrunnelse", + ), + VurdertTotrinnDto( + behandlingssteg = Behandlingssteg.FORESLÅ_VEDTAK, + godkjent = godkjent, + begrunnelse = "foreslåvedtak totrinn begrunnelse", + ), + ), + ) + } + + private fun assertBehandlingssteg( + behandlingsstegstilstand: List, + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ) { + behandlingsstegstilstand.shouldHaveSingleElement { + behandlingssteg == it.behandlingssteg && + behandlingsstegstatus == it.behandlingsstegsstatus + } + } + + private fun assertFaktadata(behandlingsstegFaktaDto: BehandlingsstegFaktaDto) { + val faktaFeilutbetaling = faktaFeilutbetalingService.hentAktivFaktaOmFeilutbetaling(behandlingId) + faktaFeilutbetaling.shouldNotBeNull() + val faktaFeilutbetalingsperioder = faktaFeilutbetaling.perioder.toList() + faktaFeilutbetalingsperioder.size shouldBe 1 + val faktaFeilutbetaltePerioderDto: FaktaFeilutbetalingsperiodeDto = + behandlingsstegFaktaDto.feilutbetaltePerioder[0] + faktaFeilutbetalingsperioder[0].periode.toDatoperiode() shouldBe faktaFeilutbetaltePerioderDto.periode + faktaFeilutbetalingsperioder[0].hendelsestype shouldBe faktaFeilutbetaltePerioderDto.hendelsestype + faktaFeilutbetalingsperioder[0].hendelsesundertype shouldBe faktaFeilutbetaltePerioderDto.hendelsesundertype + "testverdi" shouldBe faktaFeilutbetaling.begrunnelse + } + + private fun assertForeldelsesdata(foreldelsesperiodeDto: ForeldelsesperiodeDto) { + val vurdertForeldelsesdata = foreldelseService.hentVurdertForeldelse(behandlingId) + vurdertForeldelsesdata.foreldetPerioder.size shouldBe 1 + val vurdertForeldetData = vurdertForeldelsesdata.foreldetPerioder[0] + vurdertForeldetData.begrunnelse shouldBe foreldelsesperiodeDto.begrunnelse + vurdertForeldetData.foreldelsesvurderingstype shouldBe foreldelsesperiodeDto.foreldelsesvurderingstype + vurdertForeldetData.feilutbetaltBeløp shouldBe BigDecimal("10000") + vurdertForeldetData.periode shouldBe foreldelsesperiodeDto.periode + } + + private fun assertBehandlingsstatus(behandlingId: UUID, behandlingsstatus: Behandlingsstatus) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandling.status shouldBe behandlingsstatus + } + + private fun assertOppgave(tasktype: String, forventet: Int = 1) { + val taskene = taskService.finnTasksMedStatus( + status = listOf( + Status.KLAR_TIL_PLUKK, + Status.UBEHANDLET, + Status.BEHANDLER, + Status.FERDIG, + ), + page = Pageable.unpaged(), + ) + .filter { tasktype == it.type } + + taskene.size shouldBe forventet + } + + private fun assertHistorikkTask( + historikkinnslagstype: TilbakekrevingHistorikkinnslagstype, + aktør: Aktør, + ) { + taskService.finnTasksMedStatus( + listOf( + Status.KLAR_TIL_PLUKK, + Status.UBEHANDLET, + Status.BEHANDLER, + Status.FERDIG, + ), + page = Pageable.unpaged(), + ).any { + LagHistorikkinnslagTask.TYPE == it.type && + historikkinnslagstype.name == it.metadata["historikkinnslagstype"] && + aktør.name == it.metadata["aktør"] && + behandlingId.toString() == it.payload + }.shouldBeTrue() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/task/OppdaterFaktainfoTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/task/OppdaterFaktainfoTaskTest.kt new file mode 100644 index 000000000..8bd68126d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/task/OppdaterFaktainfoTaskTest.kt @@ -0,0 +1,148 @@ +package no.nav.familie.tilbake.behandling.task + +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandling +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRespons +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.BehandlingService +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingRequestSendtRepository +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingService +import no.nav.familie.tilbake.behandling.domain.HentFagsystemsbehandlingRequestSendt +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.integration.kafka.KafkaProducer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.util.Properties + +internal class OppdaterFaktainfoTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var requestSendtRepository: HentFagsystemsbehandlingRequestSendtRepository + + @Autowired + private lateinit var behandlingService: BehandlingService + + private val mockKafkaProducer: KafkaProducer = mockk() + private lateinit var hentFagsystemsbehandlingService: HentFagsystemsbehandlingService + private lateinit var oppdaterFaktainfoTask: OppdaterFaktainfoTask + + private val fagsak = Testdata.fagsak + private val behandling = Testdata.behandling + + @BeforeEach + fun init() { + hentFagsystemsbehandlingService = HentFagsystemsbehandlingService(requestSendtRepository, mockKafkaProducer) + oppdaterFaktainfoTask = OppdaterFaktainfoTask(hentFagsystemsbehandlingService, behandlingService) + + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(behandling) + } + + @AfterEach + fun tearDown() { + requestSendtRepository.deleteAll() + } + + @Test + fun `doTask skal oppdatere fakta info når respons-en har mottatt fra fagsystem`() { + requestSendtRepository + .insert( + HentFagsystemsbehandlingRequestSendt( + eksternFagsakId = fagsak.eksternFagsakId, + eksternId = "1", + ytelsestype = fagsak.ytelsestype, + respons = objectMapper.writeValueAsString(lagRespons(eksternId = "1")), + ), + ) + + oppdaterFaktainfoTask.doTask(lagTask()) + + val oppdatertBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + oppdatertBehandling.aktivFagsystemsbehandling.eksternId shouldBe "1" + oppdatertBehandling.aktivFagsystemsbehandling.resultat shouldBe "testresultat" + oppdatertBehandling.aktivFagsystemsbehandling.årsak shouldBe "testårsak" + oppdatertBehandling.aktivFagsystemsbehandling.tilbakekrevingsvalg shouldBe Tilbakekrevingsvalg.IGNORER_TILBAKEKREVING + } + + @Test + fun `doTask skal ikke oppdatere fakta info når respons-en ikke har mottatt fra fagsystem`() { + requestSendtRepository.insert( + HentFagsystemsbehandlingRequestSendt( + eksternFagsakId = fagsak.eksternFagsakId, + eksternId = "1", + ytelsestype = fagsak.ytelsestype, + ), + ) + val exception = shouldThrow { oppdaterFaktainfoTask.doTask(lagTask()) } + exception.message shouldBe "HentFagsystemsbehandlingRespons er ikke mottatt fra fagsystem for " + + "eksternFagsakId=${fagsak.eksternFagsakId},ytelsestype=${fagsak.ytelsestype},eksternId=1." + + "Task kan kjøre på nytt manuelt når respons er mottatt." + } + + @Test + fun `doTask skal ikke oppdatere fakta info når tilbakekrevingsbehandling allerede er tilkoblet med riktig fagsak`() { + requestSendtRepository.insert( + HentFagsystemsbehandlingRequestSendt( + eksternFagsakId = fagsak.eksternFagsakId, + eksternId = behandling.aktivFagsystemsbehandling.eksternId, + ytelsestype = fagsak.ytelsestype, + respons = objectMapper.writeValueAsString(lagRespons(eksternId = behandling.aktivFagsystemsbehandling.eksternId)), + ), + ) + shouldNotThrowAny { oppdaterFaktainfoTask.doTask(lagTask(eksternId = behandling.aktivFagsystemsbehandling.eksternId)) } + val oppdatertBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + oppdatertBehandling.endretTidspunkt.isEqual(behandling.endretTidspunkt) + oppdatertBehandling.aktivFagsystemsbehandling.sporbar.endret.endretTid.isEqual(behandling.aktivFagsystemsbehandling.sporbar.endret.endretTid) + } + + private fun lagRespons(eksternId: String): HentFagsystemsbehandlingRespons { + val hentFagsystemsbehandling = HentFagsystemsbehandling( + eksternFagsakId = fagsak.eksternFagsakId, + eksternId = eksternId, + ytelsestype = fagsak.ytelsestype, + personIdent = fagsak.bruker.ident, + språkkode = fagsak.bruker.språkkode, + enhetId = behandling.behandlendeEnhet, + enhetsnavn = behandling.behandlendeEnhetsNavn, + revurderingsvedtaksdato = LocalDate.now(), + faktainfo = Faktainfo( + revurderingsårsak = "testårsak", + revurderingsresultat = "testresultat", + tilbakekrevingsvalg = Tilbakekrevingsvalg + .IGNORER_TILBAKEKREVING, + ), + ) + return HentFagsystemsbehandlingRespons(hentFagsystemsbehandling = hentFagsystemsbehandling) + } + + private fun lagTask(eksternId: String = "1"): Task { + return Task( + type = OppdaterFaktainfoTask.TYPE, + payload = "", + properties = Properties().apply { + setProperty("eksternFagsakId", fagsak.eksternFagsakId) + setProperty("ytelsestype", fagsak.ytelsestype.name) + setProperty("eksternId", eksternId) + }, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/task/OpprettBehandlingManuellTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/task/OpprettBehandlingManuellTaskTest.kt new file mode 100644 index 000000000..7ad4d8e46 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandling/task/OpprettBehandlingManuellTaskTest.kt @@ -0,0 +1,294 @@ +package no.nav.familie.tilbake.behandling.task + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandling +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRespons +import no.nav.familie.kontrakter.felles.tilbakekreving.Institusjon +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingManuellOpprettelseService +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.BehandlingService +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.FagsystemUtil +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingRequestSendtRepository +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingService +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.integration.kafka.DefaultKafkaProducer +import no.nav.familie.tilbake.integration.kafka.KafkaProducer +import no.nav.familie.tilbake.kravgrunnlag.task.FinnKravgrunnlagTask +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattRepository +import org.apache.kafka.clients.producer.ProducerRecord +import org.apache.kafka.clients.producer.RecordMetadata +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.support.SendResult +import java.time.LocalDate +import java.util.Properties +import java.util.UUID + +internal class OpprettBehandlingManuellTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var requestSendtRepository: HentFagsystemsbehandlingRequestSendtRepository + + @Autowired + private lateinit var økonomiXmlMottattRepository: ØkonomiXmlMottattRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingService: BehandlingService + + private val mockKafkaTemplate: KafkaTemplate = mockk() + private lateinit var spyKafkaProducer: KafkaProducer + + private lateinit var hentFagsystemsbehandlingService: HentFagsystemsbehandlingService + private lateinit var behandlingManuellOpprettelseService: BehandlingManuellOpprettelseService + private lateinit var opprettBehandlingManueltTask: OpprettBehandlingManueltTask + + private val requestIdSlot = slot() + private val hentFagsystemsbehandlingRequestSlot = slot() + + private val eksternFagsakId = "testverdi" + private val ytelsestype = Ytelsestype.BARNETRYGD + private val eksternId = "testverdi" + private val ansvarligSaksbehandler = "Z0000" + + @BeforeEach + fun init() { + spyKafkaProducer = spyk(DefaultKafkaProducer(mockKafkaTemplate)) + hentFagsystemsbehandlingService = HentFagsystemsbehandlingService(requestSendtRepository, spyKafkaProducer) + behandlingManuellOpprettelseService = BehandlingManuellOpprettelseService(behandlingService) + opprettBehandlingManueltTask = OpprettBehandlingManueltTask( + hentFagsystemsbehandlingService, + behandlingManuellOpprettelseService, + ) + + val recordMetadata = mockk() + every { recordMetadata.offset() } returns 1 + val result = SendResult(mockk(), recordMetadata) + every { mockKafkaTemplate.send(any>()).get() } returns result + } + + @AfterEach + fun tearDown() { + requestSendtRepository.deleteAll() + } + + @Test + fun `preCondition skal sende hentFagsystemsbehandling request`() { + opprettBehandlingManueltTask.preCondition(lagTask()) + + verify { + spyKafkaProducer.sendHentFagsystemsbehandlingRequest( + capture(requestIdSlot), + capture(hentFagsystemsbehandlingRequestSlot), + ) + } + val requestId = requestIdSlot.captured + val requestSendt = requestSendtRepository + .findByEksternFagsakIdAndYtelsestypeAndEksternId( + eksternFagsakId, + ytelsestype, + eksternId, + ) + requestSendt.shouldNotBeNull() + requestSendt.id shouldBe requestId + requestSendt.eksternFagsakId shouldBe eksternFagsakId + requestSendt.ytelsestype shouldBe ytelsestype + requestSendt.eksternId shouldBe eksternId + requestSendt.respons.shouldBeNull() + } + + @Test + fun `doTask skal ikke opprette behandling når responsen ikke har mottatt fra fagsystem`() { + opprettBehandlingManueltTask.preCondition(lagTask()) + + val exception = shouldThrow { opprettBehandlingManueltTask.doTask(lagTask()) } + exception.message shouldBe "HentFagsystemsbehandling respons-en har ikke mottatt fra fagsystem for " + + "eksternFagsakId=$eksternFagsakId,ytelsestype=$ytelsestype," + + "eksternId=$eksternId." + + "Task-en kan kjøre på nytt manuelt når respons-en er mottatt" + + val requestSendt = requestSendtRepository + .findByEksternFagsakIdAndYtelsestypeAndEksternId( + eksternFagsakId, + ytelsestype, + eksternId, + ) + requestSendt.shouldNotBeNull() + } + + @Test + fun `doTask skal ikke opprette behandling når responsen har mottatt fra fagsystem men finnes ikke kravgrunnlag`() { + opprettBehandlingManueltTask.preCondition(lagTask()) + + val requestSendt = requestSendtRepository + .findByEksternFagsakIdAndYtelsestypeAndEksternId( + eksternFagsakId, + ytelsestype, + eksternId, + ) + val respons = lagHentFagsystemsbehandlingRespons() + requestSendt?.let { requestSendtRepository.update(it.copy(respons = objectMapper.writeValueAsString(respons))) } + + val exception = shouldThrow { opprettBehandlingManueltTask.doTask(lagTask()) } + exception.message shouldBe "Det finnes intet kravgrunnlag for ytelsestype=$ytelsestype,eksternFagsakId=$eksternFagsakId " + + "og eksternId=$eksternId. Tilbakekrevingsbehandling kan ikke opprettes manuelt." + } + + @Test + fun `doTask skal opprette behandling når responsen har mottatt fra fagsystem og finnes kravgrunnlag`() { + opprettBehandlingManueltTask.preCondition(lagTask()) + + val requestSendt = requestSendtRepository + .findByEksternFagsakIdAndYtelsestypeAndEksternId( + eksternFagsakId, + ytelsestype, + eksternId, + ) + val respons = lagHentFagsystemsbehandlingRespons() + requestSendt?.let { requestSendtRepository.update(it.copy(respons = objectMapper.writeValueAsString(respons))) } + + val økonomiXmlMottatt = Testdata.økonomiXmlMottatt + økonomiXmlMottattRepository.insert(økonomiXmlMottatt.copy(eksternFagsakId = eksternFagsakId, referanse = eksternId)) + + opprettBehandlingManueltTask.doTask(lagTask()) + + taskService.findAll().any { FinnKravgrunnlagTask.TYPE == it.type }.shouldBeTrue() + + val behandling = behandlingRepository.finnÅpenTilbakekrevingsbehandling(ytelsestype, eksternFagsakId) + behandling.shouldNotBeNull() + behandling.manueltOpprettet.shouldBeTrue() + behandling.aktivtVarsel.shouldBeNull() + behandling.aktivVerge.shouldBeNull() + behandling.aktivFagsystemsbehandling.eksternId shouldBe eksternId + + val fagsystemsbehandling = respons.hentFagsystemsbehandling + fagsystemsbehandling.shouldNotBeNull() + behandling.aktivFagsystemsbehandling.resultat shouldBe fagsystemsbehandling.faktainfo.revurderingsresultat + behandling.aktivFagsystemsbehandling.årsak shouldBe fagsystemsbehandling.faktainfo.revurderingsårsak + behandling.behandlendeEnhet shouldBe fagsystemsbehandling.enhetId + behandling.behandlendeEnhetsNavn shouldBe fagsystemsbehandling.enhetsnavn + behandling.ansvarligSaksbehandler shouldBe "bb1234" + behandling.ansvarligBeslutter.shouldBeNull() + behandling.status shouldBe Behandlingsstatus.UTREDES + + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + fagsak.bruker.språkkode shouldBe fagsystemsbehandling.språkkode + fagsak.fagsystem shouldBe FagsystemUtil.hentFagsystemFraYtelsestype(fagsystemsbehandling.ytelsestype) + fagsak.institusjon shouldBe null + } + + @Test + fun `doTask skal opprette behandling for institusjon når responsen har mottatt fra fagsystem og finnes kravgrunnlag`() { + opprettBehandlingManueltTask.preCondition(lagTask()) + + val requestSendt = requestSendtRepository + .findByEksternFagsakIdAndYtelsestypeAndEksternId( + eksternFagsakId, + ytelsestype, + eksternId, + ) + val respons = lagHentFagsystemsbehandlingRespons(erInstitusjon = true) + requestSendt?.let { requestSendtRepository.update(it.copy(respons = objectMapper.writeValueAsString(respons))) } + + val økonomiXmlMottatt = Testdata.økonomiXmlMottatt + økonomiXmlMottattRepository.insert(økonomiXmlMottatt.copy(eksternFagsakId = eksternFagsakId, referanse = eksternId)) + + opprettBehandlingManueltTask.doTask(lagTask()) + + taskService.findAll().any { FinnKravgrunnlagTask.TYPE == it.type }.shouldBeTrue() + + val behandling = behandlingRepository.finnÅpenTilbakekrevingsbehandling(ytelsestype, eksternFagsakId) + behandling.shouldNotBeNull() + behandling.manueltOpprettet.shouldBeTrue() + behandling.aktivtVarsel.shouldBeNull() + behandling.aktivVerge.shouldBeNull() + behandling.aktivFagsystemsbehandling.eksternId shouldBe eksternId + + val fagsystemsbehandling = respons.hentFagsystemsbehandling + fagsystemsbehandling.shouldNotBeNull() + behandling.aktivFagsystemsbehandling.resultat shouldBe fagsystemsbehandling.faktainfo.revurderingsresultat + behandling.aktivFagsystemsbehandling.årsak shouldBe fagsystemsbehandling.faktainfo.revurderingsårsak + behandling.behandlendeEnhet shouldBe fagsystemsbehandling.enhetId + behandling.behandlendeEnhetsNavn shouldBe fagsystemsbehandling.enhetsnavn + behandling.ansvarligSaksbehandler shouldBe "bb1234" + behandling.ansvarligBeslutter.shouldBeNull() + behandling.status shouldBe Behandlingsstatus.UTREDES + + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + fagsak.bruker.språkkode shouldBe fagsystemsbehandling.språkkode + fagsak.fagsystem shouldBe FagsystemUtil.hentFagsystemFraYtelsestype(fagsystemsbehandling.ytelsestype) + fagsak.institusjon shouldNotBe null + fagsak.institusjon!!.organisasjonsnummer shouldBe "987654321" + } + + private fun lagTask(): Task { + return Task( + type = OpprettBehandlingManueltTask.TYPE, + payload = "", + properties = Properties().apply { + setProperty("eksternFagsakId", eksternFagsakId) + setProperty("ytelsestype", ytelsestype.name) + setProperty("eksternId", eksternId) + setProperty("ansvarligSaksbehandler", ansvarligSaksbehandler) + }, + ) + } + + private fun lagHentFagsystemsbehandlingRespons( + erInstitusjon: Boolean = false, + feilmelding: String? = null, + ): HentFagsystemsbehandlingRespons { + var institusjon = if (erInstitusjon) Institusjon(organisasjonsnummer = "987654321") else null + val fagsystemsbehandling = HentFagsystemsbehandling( + eksternFagsakId = eksternFagsakId, + ytelsestype = ytelsestype, + eksternId = eksternId, + personIdent = "testverdi", + språkkode = Språkkode.NB, + enhetId = "8020", + enhetsnavn = "testverdi", + revurderingsvedtaksdato = LocalDate.now(), + faktainfo = Faktainfo( + revurderingsårsak = "testverdi", + revurderingsresultat = "OPPHØR", + tilbakekrevingsvalg = Tilbakekrevingsvalg + .IGNORER_TILBAKEKREVING, + ), + institusjon = institusjon, + ) + return HentFagsystemsbehandlingRespons(hentFagsystemsbehandling = fagsystemsbehandling, feilMelding = feilmelding) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandlingskontroll/BehandlingskontrollServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandlingskontroll/BehandlingskontrollServiceTest.kt new file mode 100644 index 000000000..be9b60029 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandlingskontroll/BehandlingskontrollServiceTest.kt @@ -0,0 +1,508 @@ +package no.nav.familie.tilbake.behandlingskontroll + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Fagsystemsbehandling +import no.nav.familie.tilbake.behandling.domain.Varsel +import no.nav.familie.tilbake.behandling.domain.Varselsperiode +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg.AVSLUTTET +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg.FAKTA +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg.FORELDELSE +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg.GRUNNLAG +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg.VARSEL +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg.VILKÅRSVURDERING +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.AUTOUTFØRT +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.AVBRUTT +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.KLAR +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.UTFØRT +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus.VENTER +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.util.UUID + +internal class BehandlingskontrollServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + private val behandling = Testdata.behandling + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(behandling) + } + + @Test + fun `fortsettBehandling skal oppdatere til varselssteg etter behandling er opprettet med varsel`() { + val fagsystemsbehandling = lagFagsystemsbehandling(Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL) + val varsel = Varsel( + varseltekst = "testverdi", + varselbeløp = 1000L, + perioder = setOf( + Varselsperiode( + fom = LocalDate.now().minusMonths(2), + tom = LocalDate.now(), + ), + ), + ) + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update( + lagretBehandling.copy( + fagsystemsbehandling = setOf(fagsystemsbehandling), + varsler = setOf(varsel), + ), + ) + + behandlingskontrollService.fortsettBehandling(behandlingId = behandling.id) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 1 + val sisteStegstilstand = behandlingsstegstilstand[0] + sisteStegstilstand.behandlingssteg shouldBe VARSEL + sisteStegstilstand.behandlingsstegsstatus shouldBe VENTER + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + sisteStegstilstand.venteårsak shouldBe Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING + sisteStegstilstand.tidsfrist shouldBe behandling.opprettetDato.plusWeeks(3) + } + + @Test + fun `fortsettBehandling skal ikke fortsette til grunnlagssteg når behandling venter på varsel steg`() { + behandlingskontrollService.fortsettBehandling(behandlingId = behandling.id) + var behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 1 + behandlingsstegstilstand[0].behandlingssteg shouldBe VARSEL + behandlingsstegstilstand[0].behandlingsstegsstatus shouldBe VENTER + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + + behandlingskontrollService.fortsettBehandling(behandlingId = behandling.id) + + behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 1 + val nyStegstilstand = behandlingsstegstilstand[0] + nyStegstilstand.behandlingssteg shouldBe VARSEL + nyStegstilstand.behandlingsstegsstatus shouldBe VENTER + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + nyStegstilstand.venteårsak shouldBe Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING + nyStegstilstand.tidsfrist shouldBe behandling.opprettetDato.plusWeeks(3) + } + + @Test + fun `fortsettBehandling skal oppdatere til grunnlagssteg etter behandling er opprettet uten varsel`() { + val fagsystemsbehandling = lagFagsystemsbehandling(Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL) + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update( + lagretBehandling.copy( + fagsystemsbehandling = setOf(fagsystemsbehandling), + varsler = emptySet(), + ), + ) + + behandlingskontrollService.fortsettBehandling(behandlingId = behandling.id) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 1 + val sisteStegstilstand = behandlingsstegstilstand[0] + sisteStegstilstand.behandlingssteg shouldBe GRUNNLAG + sisteStegstilstand.behandlingsstegsstatus shouldBe VENTER + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + sisteStegstilstand.venteårsak shouldBe Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG + sisteStegstilstand.tidsfrist shouldBe behandling.opprettetDato.plusWeeks(4) + } + + @Test + fun `fortsettBehandling skal fortsette til grunnlagssteg når varselsrespons ble mottatt uten kravgrunnlag`() { + lagBehandlingsstegstilstand(setOf(Behandlingsstegsinfo(VARSEL, UTFØRT))) + + behandlingskontrollService.fortsettBehandling(behandlingId = behandling.id) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 2 + val aktivtstegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstand) + aktivtstegstilstand.shouldNotBeNull() + aktivtstegstilstand.behandlingssteg shouldBe GRUNNLAG + aktivtstegstilstand.behandlingsstegsstatus shouldBe VENTER + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + aktivtstegstilstand.venteårsak shouldBe Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG + aktivtstegstilstand.tidsfrist shouldBe behandling.opprettetDato.plusWeeks(4) + } + + @Test + fun `fortsettBehandling skal fortsette til grunnlagssteg når varselsrespons ble mottatt med sperret kravgrunnlag`() { + lagBehandlingsstegstilstand(setOf(Behandlingsstegsinfo(VARSEL, UTFØRT))) + val kravgrunnlag = Testdata.kravgrunnlag431 + val oppdatertKravgrunnlag = kravgrunnlag.copy(sperret = true) + kravgrunnlagRepository.insert(oppdatertKravgrunnlag) + + behandlingskontrollService.fortsettBehandling(behandlingId = behandling.id) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 2 + val aktivtstegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstand) + aktivtstegstilstand.shouldNotBeNull() + aktivtstegstilstand.behandlingssteg shouldBe GRUNNLAG + aktivtstegstilstand.behandlingsstegsstatus shouldBe VENTER + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + aktivtstegstilstand.venteårsak shouldBe Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG + aktivtstegstilstand.tidsfrist shouldBe oppdatertKravgrunnlag.sporbar.endret.endretTid.plusWeeks(4).toLocalDate() + } + + @Test + fun `fortsettBehandling skal fortsette til fakta steg når varselsrespons ble mottatt med aktivt kravgrunnlag`() { + val behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(behandling.copy(verger = emptySet())) + + lagBehandlingsstegstilstand(setOf(Behandlingsstegsinfo(VARSEL, UTFØRT))) + val kravgrunnlag = Testdata.kravgrunnlag431 + kravgrunnlagRepository.insert(kravgrunnlag) + + behandlingskontrollService.fortsettBehandling(behandlingId = behandling.id) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 2 + val aktivtstegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstand) + aktivtstegstilstand.shouldNotBeNull() + aktivtstegstilstand.behandlingssteg shouldBe FAKTA + aktivtstegstilstand.behandlingsstegsstatus shouldBe KLAR + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + aktivtstegstilstand.venteårsak.shouldBeNull() + aktivtstegstilstand.tidsfrist.shouldBeNull() + } + + @Test + fun `fortsettBehandling skal oppdatere til fakta steg etter behandling er opprettet uten varsel og mottok kravgrunnlag`() { + val fagsystemsbehandling = lagFagsystemsbehandling(Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL) + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update( + lagretBehandling.copy( + fagsystemsbehandling = setOf(fagsystemsbehandling), + varsler = emptySet(), + ), + ) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + behandlingskontrollService.fortsettBehandling(behandlingId = behandling.id) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 1 + val sisteStegstilstand = behandlingsstegstilstand[0] + sisteStegstilstand.behandlingssteg shouldBe FAKTA + sisteStegstilstand.behandlingsstegsstatus shouldBe KLAR + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + sisteStegstilstand.venteårsak.shouldBeNull() + sisteStegstilstand.tidsfrist.shouldBeNull() + } + + @Test + fun `fortsettBehandling skal oppdatere til foreldelsessteg etter fakta steg er utført`() { + lagBehandlingsstegstilstand(setOf(Behandlingsstegsinfo(FAKTA, UTFØRT))) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + behandlingskontrollService.fortsettBehandling(behandlingId = behandling.id) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 2 + val sisteStegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstand) + sisteStegstilstand.shouldNotBeNull() + sisteStegstilstand.behandlingssteg shouldBe FORELDELSE + sisteStegstilstand.behandlingsstegsstatus shouldBe KLAR + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + sisteStegstilstand.venteårsak.shouldBeNull() + sisteStegstilstand.tidsfrist.shouldBeNull() + } + + @Test + fun `fortsettBehandling skal oppdatere til vilkårsvurderingssteg etter foreldelse steg er utført`() { + lagBehandlingsstegstilstand( + setOf( + Behandlingsstegsinfo(FAKTA, UTFØRT), + Behandlingsstegsinfo(FORELDELSE, UTFØRT), + ), + ) + + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + behandlingskontrollService.fortsettBehandling(behandlingId = behandling.id) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 3 + val sisteStegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstand) + sisteStegstilstand.shouldNotBeNull() + sisteStegstilstand.behandlingssteg shouldBe VILKÅRSVURDERING + sisteStegstilstand.behandlingsstegsstatus shouldBe KLAR + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + sisteStegstilstand.venteårsak.shouldBeNull() + sisteStegstilstand.tidsfrist.shouldBeNull() + } + + @Test + fun `fortsettBehandling skal ikke oppdatere til foreldelsessteg når fakta steg ikke er utført`() { + lagBehandlingsstegstilstand( + setOf( + Behandlingsstegsinfo(VARSEL, UTFØRT), + Behandlingsstegsinfo(GRUNNLAG, UTFØRT), + Behandlingsstegsinfo(FAKTA, KLAR), + ), + ) + + behandlingskontrollService.fortsettBehandling(behandlingId = behandling.id) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 3 + val sisteStegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstand) + sisteStegstilstand.shouldNotBeNull() + sisteStegstilstand.behandlingssteg shouldBe FAKTA + sisteStegstilstand.behandlingsstegsstatus shouldBe KLAR + sisteStegstilstand.venteårsak.shouldBeNull() + sisteStegstilstand.tidsfrist.shouldBeNull() + } + + @Test + fun `fortsettBehandling skal oppdatere til fakta steg etter mottok endr melding`() { + lagBehandlingsstegstilstand( + setOf( + Behandlingsstegsinfo(VARSEL, UTFØRT), + Behandlingsstegsinfo(GRUNNLAG, UTFØRT), + Behandlingsstegsinfo(FAKTA, AVBRUTT), + Behandlingsstegsinfo(FORELDELSE, AVBRUTT), + Behandlingsstegsinfo(VILKÅRSVURDERING, AVBRUTT), + ), + ) + + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + behandlingskontrollService.fortsettBehandling(behandlingId = behandling.id) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 5 + val sisteStegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstand) + sisteStegstilstand.shouldNotBeNull() + sisteStegstilstand.behandlingssteg shouldBe FAKTA + sisteStegstilstand.behandlingsstegsstatus shouldBe KLAR + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + sisteStegstilstand.venteårsak.shouldBeNull() + sisteStegstilstand.tidsfrist.shouldBeNull() + } + + @Test + fun `tilbakehoppBehandlingssteg skal oppdatere til varselssteg når manuelt varsel sendt og behandling er i vilkår steg `() { + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + lagBehandlingsstegstilstand( + setOf( + Behandlingsstegsinfo(VARSEL, UTFØRT), + Behandlingsstegsinfo(GRUNNLAG, UTFØRT), + Behandlingsstegsinfo(FAKTA, UTFØRT), + Behandlingsstegsinfo(FORELDELSE, AUTOUTFØRT), + Behandlingsstegsinfo(VILKÅRSVURDERING, KLAR), + ), + ) + + behandlingskontrollService + .tilbakehoppBehandlingssteg( + behandlingId = behandling.id, + behandlingsstegsinfo = + lagBehandlingsstegsinfo(VARSEL, Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING), + ) + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 5 + + val sisteStegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstand) + sisteStegstilstand.shouldNotBeNull() + sisteStegstilstand.behandlingssteg shouldBe VARSEL + sisteStegstilstand.behandlingsstegsstatus shouldBe VENTER + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + sisteStegstilstand.venteårsak shouldBe Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING + sisteStegstilstand.tidsfrist shouldBe behandling.opprettetDato.plusWeeks(3) + + behandlingsstegstilstand.first { GRUNNLAG == it.behandlingssteg }.behandlingsstegsstatus shouldBe UTFØRT + behandlingsstegstilstand.first { FAKTA == it.behandlingssteg }.behandlingsstegsstatus shouldBe UTFØRT + behandlingsstegstilstand.first { FORELDELSE == it.behandlingssteg }.behandlingsstegsstatus shouldBe AUTOUTFØRT + behandlingsstegstilstand.first { VILKÅRSVURDERING == it.behandlingssteg }.behandlingsstegsstatus shouldBe AVBRUTT + } + + @Test + fun `tilbakehoppBehandlingssteg skal oppdatere til varselssteg når mottok sper melding og behandling er i vilkår steg `() { + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + lagBehandlingsstegstilstand( + setOf( + Behandlingsstegsinfo(VARSEL, UTFØRT), + Behandlingsstegsinfo(GRUNNLAG, UTFØRT), + Behandlingsstegsinfo(FAKTA, UTFØRT), + Behandlingsstegsinfo(FORELDELSE, AUTOUTFØRT), + Behandlingsstegsinfo(VILKÅRSVURDERING, KLAR), + ), + ) + + behandlingskontrollService + .tilbakehoppBehandlingssteg( + behandlingId = behandling.id, + behandlingsstegsinfo = + lagBehandlingsstegsinfo(GRUNNLAG, Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG), + ) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 5 + + val sisteStegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstand) + sisteStegstilstand.shouldNotBeNull() + sisteStegstilstand.behandlingssteg shouldBe GRUNNLAG + sisteStegstilstand.behandlingsstegsstatus shouldBe VENTER + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + sisteStegstilstand.venteårsak shouldBe Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG + sisteStegstilstand.tidsfrist shouldBe behandling.opprettetDato.plusWeeks(4) + + behandlingsstegstilstand.first { VARSEL == it.behandlingssteg }.behandlingsstegsstatus shouldBe UTFØRT + behandlingsstegstilstand.first { FAKTA == it.behandlingssteg }.behandlingsstegsstatus shouldBe UTFØRT + behandlingsstegstilstand.first { FORELDELSE == it.behandlingssteg }.behandlingsstegsstatus shouldBe AUTOUTFØRT + behandlingsstegstilstand.first { VILKÅRSVURDERING == it.behandlingssteg }.behandlingsstegsstatus shouldBe AVBRUTT + } + + @Test + fun `settBehandlingPåVent skal sette behandling på vent med avventer dokumentasjon når behandling er i fakta steg`() { + val tidsfrist: LocalDate = LocalDate.now().plusWeeks(2) + lagBehandlingsstegstilstand( + setOf( + Behandlingsstegsinfo(VARSEL, UTFØRT), + Behandlingsstegsinfo(GRUNNLAG, UTFØRT), + Behandlingsstegsinfo(FAKTA, KLAR), + ), + ) + + behandlingskontrollService.settBehandlingPåVent( + behandlingId = behandling.id, + venteårsak = Venteårsak.AVVENTER_DOKUMENTASJON, + tidsfrist = tidsfrist, + ) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 3 + + val sisteStegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstand) + sisteStegstilstand.shouldNotBeNull() + sisteStegstilstand.behandlingssteg shouldBe FAKTA + sisteStegstilstand.behandlingsstegsstatus shouldBe VENTER + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + sisteStegstilstand.venteårsak shouldBe Venteårsak.AVVENTER_DOKUMENTASJON + sisteStegstilstand.tidsfrist shouldBe tidsfrist + } + + @Test + fun `settBehandlingPåVent skal ikke sette behandling på vent med avventer dokumentasjon når behandling er avsluttet`() { + val tidsfrist: LocalDate = LocalDate.now().plusWeeks(2) + lagBehandlingsstegstilstand( + setOf( + Behandlingsstegsinfo(VARSEL, AVBRUTT), + Behandlingsstegsinfo(AVSLUTTET, UTFØRT), + ), + ) + + val exception = shouldThrow(block = { + behandlingskontrollService.settBehandlingPåVent( + behandlingId = behandling.id, + venteårsak = Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + tidsfrist = tidsfrist.minusDays(5), + ) + }) + exception.message shouldBe "Behandling ${behandling.id} har ikke aktivt steg" + } + + @Test + fun `settBehandlingPåVent skal utvide fristen med brukerstilbakemelding når behandling er i varsel steg`() { + val tidsfrist: LocalDate = + behandling.opprettetDato.plusWeeks(Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING.defaultVenteTidIUker) + lagBehandlingsstegstilstand( + setOf( + Behandlingsstegsinfo( + VARSEL, + VENTER, + venteårsak = Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + tidsfrist = tidsfrist, + ), + ), + ) + + behandlingskontrollService.settBehandlingPåVent( + behandlingId = behandling.id, + venteårsak = Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + tidsfrist = tidsfrist.plusWeeks(2), + ) + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.size shouldBe 1 + + val sisteStegstilstand = behandlingskontrollService.finnAktivStegstilstand(behandlingsstegstilstand) + sisteStegstilstand.shouldNotBeNull() + sisteStegstilstand.behandlingssteg shouldBe VARSEL + sisteStegstilstand.behandlingsstegsstatus shouldBe VENTER + assertBehandlingsstatus(behandling.id, Behandlingsstatus.UTREDES) + sisteStegstilstand.venteårsak shouldBe Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING + sisteStegstilstand.tidsfrist shouldBe tidsfrist.plusWeeks(2) + } + + private fun lagBehandlingsstegstilstand(stegMetadata: Set) { + stegMetadata.map { + behandlingsstegstilstandRepository.insert( + Behandlingsstegstilstand( + behandlingId = behandling.id, + behandlingssteg = it.behandlingssteg, + behandlingsstegsstatus = it.behandlingsstegstatus, + venteårsak = it.venteårsak, + tidsfrist = it.tidsfrist, + ), + ) + } + } + + private fun lagFagsystemsbehandling(tilbakekrevingsvalg: Tilbakekrevingsvalg): Fagsystemsbehandling { + return Fagsystemsbehandling( + eksternId = "123", + tilbakekrevingsvalg = tilbakekrevingsvalg, + resultat = "testverdi", + årsak = "testverdi", + revurderingsvedtaksdato = LocalDate.now().minusDays(1), + ) + } + + private fun lagBehandlingsstegsinfo( + behandlingssteg: Behandlingssteg, + venteårsak: Venteårsak, + ): Behandlingsstegsinfo { + return Behandlingsstegsinfo( + behandlingssteg = behandlingssteg, + behandlingsstegstatus = VENTER, + venteårsak = venteårsak, + tidsfrist = behandling.opprettetDato.plusWeeks(venteårsak.defaultVenteTidIUker), + ) + } + + private fun assertBehandlingsstatus(behandlingId: UUID, behandlingsstatus: Behandlingsstatus) { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandling.status shouldBe behandlingsstatus + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandlingskontroll/BehandlingsstegstilstandRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandlingskontroll/BehandlingsstegstilstandRepositoryTest.kt new file mode 100644 index 000000000..43300493f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/behandlingskontroll/BehandlingsstegstilstandRepositoryTest.kt @@ -0,0 +1,66 @@ +package no.nav.familie.tilbake.behandlingskontroll + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class BehandlingsstegstilstandRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + private val behandlingsstegstilstand = Testdata.behandlingsstegstilstand + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(Testdata.behandling) + } + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av Behandlingsstegstilstand til basen`() { + behandlingsstegstilstandRepository.insert(behandlingsstegstilstand) + + val lagretBehandlingsstegstilstand = behandlingsstegstilstandRepository.findByIdOrThrow(behandlingsstegstilstand.id) + + lagretBehandlingsstegstilstand.shouldBeEqualToComparingFieldsExcept( + behandlingsstegstilstand, + Behandlingsstegstilstand::sporbar, + Behandlingsstegstilstand::versjon, + ) + lagretBehandlingsstegstilstand.versjon shouldBe 1 + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av Behandlingsstegstilstand i basen`() { + behandlingsstegstilstandRepository.insert(behandlingsstegstilstand) + var lagretBehandlingsstegstilstand = behandlingsstegstilstandRepository.findByIdOrThrow(behandlingsstegstilstand.id) + val oppdatertBehandlingsstegstilstand = + lagretBehandlingsstegstilstand.copy(behandlingsstegsstatus = Behandlingsstegstatus.KLAR) + + behandlingsstegstilstandRepository.update(oppdatertBehandlingsstegstilstand) + + lagretBehandlingsstegstilstand = behandlingsstegstilstandRepository.findByIdOrThrow(behandlingsstegstilstand.id) + lagretBehandlingsstegstilstand.shouldBeEqualToComparingFieldsExcept( + oppdatertBehandlingsstegstilstand, + Behandlingsstegstilstand::sporbar, + Behandlingsstegstilstand::versjon, + ) + lagretBehandlingsstegstilstand.versjon shouldBe 2 + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/beregning/TilbakekrevingsberegningServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/beregning/TilbakekrevingsberegningServiceTest.kt new file mode 100644 index 000000000..f0c502613 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/beregning/TilbakekrevingsberegningServiceTest.kt @@ -0,0 +1,482 @@ +package no.nav.familie.tilbake.beregning + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.bigdecimal.shouldBeZero +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.beregning.modell.Beregningsresultat +import no.nav.familie.tilbake.beregning.modell.Beregningsresultatsperiode +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.data.Testdata.lagFeilBeløp +import no.nav.familie.tilbake.data.Testdata.lagYtelBeløp +import no.nav.familie.tilbake.foreldelse.VurdertForeldelseRepository +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesperiode +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsperiode432 +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingRepository +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.AnnenVurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingAktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsperiode +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth +import java.util.UUID + +class TilbakekrevingsberegningServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var tilbakekrevingsberegningService: TilbakekrevingsberegningService + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var vurdertForeldelseRepository: VurdertForeldelseRepository + + @Autowired + private lateinit var vilkårsvurderingRepository: VilkårsvurderingRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(Testdata.behandling) + } + + @Test + fun `beregn skalberegne tilbakekrevingsbeløp for periode som ikke er foreldet`() { + val periode = Månedsperiode(LocalDate.of(2019, 5, 1), LocalDate.of(2019, 5, 3)) + lagKravgrunnlag(periode, BigDecimal.ZERO) + lagForeldelse(Testdata.behandling.id, periode, Foreldelsesvurderingstype.IKKE_FORELDET, null) + lagVilkårsvurderingMedForsett(Testdata.behandling.id, periode) + val beregningsresultat: Beregningsresultat = tilbakekrevingsberegningService.beregn(Testdata.behandling.id) + val resultat: List = beregningsresultat.beregningsresultatsperioder + resultat.shouldHaveSize(1) + val r: Beregningsresultatsperiode = resultat[0] + r.periode shouldBe periode + r.tilbakekrevingsbeløp shouldBe BigDecimal.valueOf(11000) + r.vurdering shouldBe Aktsomhet.FORSETT + r.renteprosent shouldBe BigDecimal.valueOf(10) + r.feilutbetaltBeløp shouldBe BigDecimal.valueOf(10000) + r.manueltSattTilbakekrevingsbeløp shouldBe null + r.andelAvBeløp shouldBe BigDecimal.valueOf(100) + beregningsresultat.vedtaksresultat shouldBe Vedtaksresultat.FULL_TILBAKEBETALING + } + + @Test + fun `hentBeregningsresultat skal hente beregningsresultat for periode som ikke er foreldet`() { + val periode = Månedsperiode(LocalDate.of(2019, 5, 1), LocalDate.of(2019, 5, 3)) + lagKravgrunnlag(periode, BigDecimal.ZERO) + lagForeldelse(Testdata.behandling.id, periode, Foreldelsesvurderingstype.IKKE_FORELDET, null) + lagVilkårsvurderingMedForsett(Testdata.behandling.id, periode) + + val beregningsresultat = tilbakekrevingsberegningService.hentBeregningsresultat(Testdata.behandling.id) + beregningsresultat.beregningsresultatsperioder.size shouldBe 1 + val beregningsresultatsperiode = beregningsresultat.beregningsresultatsperioder[0] + beregningsresultatsperiode.periode shouldBe periode.toDatoperiode() + beregningsresultatsperiode.tilbakekrevingsbeløp shouldBe BigDecimal.valueOf(11000) + beregningsresultatsperiode.vurdering shouldBe Aktsomhet.FORSETT + beregningsresultatsperiode.renteprosent shouldBe BigDecimal.valueOf(10) + beregningsresultatsperiode.feilutbetaltBeløp shouldBe BigDecimal.valueOf(10000) + beregningsresultatsperiode.andelAvBeløp shouldBe BigDecimal.valueOf(100) + beregningsresultat.vedtaksresultat shouldBe Vedtaksresultat.FULL_TILBAKEBETALING + } + + @Test + fun `beregn skalberegne tilbakekrevingsbeløp for periode som er foreldet`() { + val periode = Månedsperiode(LocalDate.of(2019, 5, 1), LocalDate.of(2019, 5, 3)) + lagKravgrunnlag(periode, BigDecimal.ZERO) + lagForeldelse(Testdata.behandling.id, periode, Foreldelsesvurderingstype.FORELDET, periode.fom.plusMonths(8).atDay(1)) + val beregningsresultat: Beregningsresultat = tilbakekrevingsberegningService.beregn(Testdata.behandling.id) + val resultat: List = beregningsresultat.beregningsresultatsperioder + resultat.shouldHaveSize(1) + val r: Beregningsresultatsperiode = resultat[0] + r.periode shouldBe periode + r.tilbakekrevingsbeløp.shouldBeZero() + r.vurdering shouldBe AnnenVurdering.FORELDET + r.renteprosent shouldBe null + r.feilutbetaltBeløp shouldBe BigDecimal.valueOf(10000) + r.manueltSattTilbakekrevingsbeløp shouldBe null + r.andelAvBeløp shouldBe BigDecimal.ZERO + r.rentebeløp.shouldBeZero() + r.tilbakekrevingsbeløpUtenRenter.shouldBeZero() + beregningsresultat.vedtaksresultat shouldBe Vedtaksresultat.INGEN_TILBAKEBETALING + } + + @Test + fun `hentBeregningsresultat skal hente beregningsresultat for periode som er foreldet`() { + val periode = Månedsperiode(LocalDate.of(2019, 5, 1), LocalDate.of(2019, 5, 3)) + lagKravgrunnlag(periode, BigDecimal.ZERO) + lagForeldelse(Testdata.behandling.id, periode, Foreldelsesvurderingstype.FORELDET, periode.fom.plusMonths(8).atDay(1)) + + val beregningsresultat = tilbakekrevingsberegningService.hentBeregningsresultat(Testdata.behandling.id) + beregningsresultat.beregningsresultatsperioder.size shouldBe 1 + val beregningsresultatsperiode = beregningsresultat.beregningsresultatsperioder[0] + beregningsresultatsperiode.periode shouldBe periode.toDatoperiode() + beregningsresultatsperiode.tilbakekrevingsbeløp.shouldNotBeNull() + beregningsresultatsperiode.tilbakekrevingsbeløp!!.shouldBeZero() + beregningsresultatsperiode.vurdering shouldBe AnnenVurdering.FORELDET + beregningsresultatsperiode.renteprosent shouldBe null + beregningsresultatsperiode.feilutbetaltBeløp shouldBe BigDecimal.valueOf(10000) + beregningsresultatsperiode.andelAvBeløp shouldBe BigDecimal.ZERO + beregningsresultat.vedtaksresultat shouldBe Vedtaksresultat.INGEN_TILBAKEBETALING + } + + @Test + fun `beregn skalberegne tilbakekrevingsbeløp for periode som ikke er foreldet med skattProsent`() { + val periode = Månedsperiode(LocalDate.of(2019, 5, 1), LocalDate.of(2019, 5, 3)) + lagKravgrunnlag(periode, BigDecimal.valueOf(10)) + lagForeldelse(Testdata.behandling.id, periode, Foreldelsesvurderingstype.IKKE_FORELDET, null) + lagVilkårsvurderingMedForsett(Testdata.behandling.id, periode) + val beregningsresultat: Beregningsresultat = tilbakekrevingsberegningService.beregn(Testdata.behandling.id) + val resultat: List = beregningsresultat.beregningsresultatsperioder + resultat.shouldHaveSize(1) + val r: Beregningsresultatsperiode = resultat[0] + r.periode shouldBe periode + r.tilbakekrevingsbeløp shouldBe BigDecimal.valueOf(11000) + r.vurdering shouldBe Aktsomhet.FORSETT + r.renteprosent shouldBe BigDecimal.valueOf(10) + r.feilutbetaltBeløp shouldBe BigDecimal.valueOf(10000) + r.manueltSattTilbakekrevingsbeløp shouldBe null + r.andelAvBeløp shouldBe BigDecimal.valueOf(100) + r.skattebeløp shouldBe BigDecimal.valueOf(1000) + r.tilbakekrevingsbeløpEtterSkatt shouldBe BigDecimal.valueOf(10000) + beregningsresultat.vedtaksresultat shouldBe Vedtaksresultat.FULL_TILBAKEBETALING + } + + @Test + fun `hentBeregningsresultat skal hente beregningsresultat for periode som ikke er foreldet med skattProsent`() { + val periode = Månedsperiode(LocalDate.of(2019, 5, 1), LocalDate.of(2019, 5, 3)) + lagKravgrunnlag(periode, BigDecimal.valueOf(10)) + lagForeldelse(Testdata.behandling.id, periode, Foreldelsesvurderingstype.IKKE_FORELDET, null) + lagVilkårsvurderingMedForsett(Testdata.behandling.id, periode) + + val beregningsresultat = tilbakekrevingsberegningService.hentBeregningsresultat(Testdata.behandling.id) + beregningsresultat.beregningsresultatsperioder.size shouldBe 1 + val beregningsresultatsperiode = beregningsresultat.beregningsresultatsperioder[0] + beregningsresultatsperiode.periode shouldBe periode.toDatoperiode() + beregningsresultatsperiode.tilbakekrevingsbeløp shouldBe BigDecimal.valueOf(11000) + beregningsresultatsperiode.vurdering shouldBe Aktsomhet.FORSETT + beregningsresultatsperiode.renteprosent shouldBe BigDecimal.valueOf(10) + beregningsresultatsperiode.feilutbetaltBeløp shouldBe BigDecimal.valueOf(10000) + beregningsresultatsperiode.andelAvBeløp shouldBe BigDecimal.valueOf(100) + beregningsresultatsperiode.tilbakekrevesBeløpEtterSkatt shouldBe BigDecimal.valueOf(10000) + beregningsresultat.vedtaksresultat shouldBe Vedtaksresultat.FULL_TILBAKEBETALING + } + + @Test + fun `beregn skalberegne riktig beløp og utbetalt beløp for periode`() { + val periode = Månedsperiode(LocalDate.of(2019, 5, 1), LocalDate.of(2019, 5, 3)) + lagKravgrunnlag(periode, BigDecimal.valueOf(10)) + lagForeldelse(Testdata.behandling.id, periode, Foreldelsesvurderingstype.IKKE_FORELDET, null) + lagVilkårsvurderingMedForsett(Testdata.behandling.id, periode) + val beregningsresultat: Beregningsresultat = tilbakekrevingsberegningService.beregn(Testdata.behandling.id) + val resultat: List = beregningsresultat.beregningsresultatsperioder + resultat.shouldHaveSize(1) + val r: Beregningsresultatsperiode = resultat[0] + r.utbetaltYtelsesbeløp shouldBe BigDecimal.valueOf(10000) + r.riktigYtelsesbeløp shouldBe BigDecimal.ZERO + } + + @Test + fun `beregn skal beregne riktige beløp ved delvis feilutbetaling for perioder sammenslått til en logisk periode`() { + val skatteprosent = BigDecimal.valueOf(10) + val periode1 = Månedsperiode(LocalDate.of(2019, 5, 1), LocalDate.of(2019, 5, 3)) + val periode2 = Månedsperiode(LocalDate.of(2019, 5, 4), LocalDate.of(2019, 5, 6)) + val logiskPeriode = Månedsperiode(LocalDate.of(2019, 5, 1), LocalDate.of(2019, 5, 6)) + val utbetalt1 = BigDecimal.valueOf(10000) + val nyttBeløp1 = BigDecimal.valueOf(5000) + val utbetalt2 = BigDecimal.valueOf(10000) + val nyttBeløp2 = BigDecimal.valueOf(100) + val feilutbetalt2 = utbetalt2.subtract(nyttBeløp2) + val feilutbetalt1 = utbetalt1.subtract(nyttBeløp1) + val grunnlagPeriode1: Kravgrunnlagsperiode432 = + lagGrunnlagPeriode( + periode1, + 1000, + setOf( + lagYtelBeløp(utbetalt1, nyttBeløp1, skatteprosent), + lagFeilBeløp(feilutbetalt1), + ), + ) + val grunnlagPeriode2: Kravgrunnlagsperiode432 = + lagGrunnlagPeriode( + periode2, + 1000, + setOf( + lagYtelBeløp(utbetalt2, nyttBeløp2, skatteprosent), + lagFeilBeløp(feilutbetalt2), + ), + ) + val grunnlag: Kravgrunnlag431 = lagGrunnlag(setOf(grunnlagPeriode1, grunnlagPeriode2)) + kravgrunnlagRepository.insert(grunnlag) + lagForeldelse(Testdata.behandling.id, logiskPeriode, Foreldelsesvurderingstype.IKKE_FORELDET, null) + lagVilkårsvurderingMedForsett(Testdata.behandling.id, logiskPeriode) + val beregningsresultat: Beregningsresultat = tilbakekrevingsberegningService.beregn(Testdata.behandling.id) + val resultat: List = beregningsresultat.beregningsresultatsperioder + resultat.shouldHaveSize(1) + val r: Beregningsresultatsperiode = resultat[0] + r.periode shouldBe logiskPeriode + r.utbetaltYtelsesbeløp shouldBe utbetalt1.add(utbetalt2) + r.riktigYtelsesbeløp shouldBe nyttBeløp1.add(nyttBeløp2) + } + + @Test + fun `beregn skal beregne tilbakekrevingsbeløp for ikkeForeldetPeriode når beregnetPeriode er på tvers av grunnlagPeriode`() { + val periode = Månedsperiode(LocalDate.of(2019, 5, 1), LocalDate.of(2019, 5, 31)) + val periode1 = Månedsperiode(LocalDate.of(2019, 6, 1), LocalDate.of(2019, 6, 30)) + val logiskPeriode = Månedsperiode( + LocalDate.of(2019, 5, 1), + LocalDate.of(2019, 6, 30), + ) + val grunnlagPeriode: Kravgrunnlagsperiode432 = + lagGrunnlagPeriode( + periode, + 1000, + setOf( + lagYtelBeløp(BigDecimal.valueOf(10000), BigDecimal.valueOf(10)), + lagFeilBeløp(BigDecimal.valueOf(10000)), + ), + ) + val grunnlagPeriode1: Kravgrunnlagsperiode432 = + lagGrunnlagPeriode( + periode1, + 1000, + setOf( + lagYtelBeløp( + BigDecimal.valueOf(10000), + BigDecimal.valueOf(10), + ), + lagFeilBeløp(BigDecimal.valueOf(10000)), + ), + ) + val grunnlag: Kravgrunnlag431 = lagGrunnlag(setOf(grunnlagPeriode, grunnlagPeriode1)) + kravgrunnlagRepository.insert(grunnlag) + lagForeldelse(Testdata.behandling.id, logiskPeriode, Foreldelsesvurderingstype.IKKE_FORELDET, null) + lagVilkårsvurderingMedForsett(Testdata.behandling.id, logiskPeriode) + + val beregningsresultat: Beregningsresultat = tilbakekrevingsberegningService.beregn(Testdata.behandling.id) + val resultat: List = beregningsresultat.beregningsresultatsperioder + resultat.shouldHaveSize(1) + val r: Beregningsresultatsperiode = resultat[0] + r.periode shouldBe logiskPeriode + r.tilbakekrevingsbeløp shouldBe BigDecimal.valueOf(22000) + r.vurdering shouldBe Aktsomhet.FORSETT + r.renteprosent shouldBe BigDecimal.valueOf(10) + r.feilutbetaltBeløp shouldBe BigDecimal.valueOf(20000) + r.manueltSattTilbakekrevingsbeløp shouldBe null + r.andelAvBeløp shouldBe BigDecimal.valueOf(100) + r.skattebeløp shouldBe BigDecimal.valueOf(2000) + r.tilbakekrevingsbeløpEtterSkatt shouldBe BigDecimal.valueOf(20000) + beregningsresultat.vedtaksresultat shouldBe Vedtaksresultat.FULL_TILBAKEBETALING + } + + @Test + fun `beregnBeløp skal beregne feilutbetaltBeløp når saksbehandler deler opp periode`() { + val kravgrunnlag431 = Testdata.kravgrunnlag431 + val feilkravgrunnlagsbeløp = Testdata.feilKravgrunnlagsbeløp433 + val yteseskravgrunnlagsbeløp = Testdata.ytelKravgrunnlagsbeløp433 + val førsteKravgrunnlagsperiode = Testdata.kravgrunnlagsperiode432 + .copy( + periode = Månedsperiode(YearMonth.of(2017, 1), YearMonth.of(2017, 1)), + beløp = setOf( + feilkravgrunnlagsbeløp.copy(id = UUID.randomUUID()), + yteseskravgrunnlagsbeløp.copy(id = UUID.randomUUID()), + ), + ) + val andreKravgrunnlagsperiode = Testdata.kravgrunnlagsperiode432 + .copy( + id = UUID.randomUUID(), + periode = Månedsperiode(YearMonth.of(2017, 2), YearMonth.of(2017, 2)), + beløp = setOf( + feilkravgrunnlagsbeløp.copy(id = UUID.randomUUID()), + yteseskravgrunnlagsbeløp.copy(id = UUID.randomUUID()), + ), + ) + kravgrunnlagRepository.insert( + kravgrunnlag431.copy( + perioder = setOf( + førsteKravgrunnlagsperiode, + andreKravgrunnlagsperiode, + ), + ), + ) + + val beregnetPerioderDto = tilbakekrevingsberegningService.beregnBeløp( + behandlingId = Testdata.behandling.id, + perioder = listOf( + Datoperiode( + LocalDate.of( + 2017, + 1, + 1, + ), + LocalDate.of( + 2017, + 1, + 31, + ), + ), + Datoperiode( + LocalDate.of( + 2017, + 2, + 1, + ), + LocalDate.of( + 2017, + 2, + 28, + ), + ), + ), + ) + beregnetPerioderDto.beregnetPerioder.size shouldBe 2 + beregnetPerioderDto.beregnetPerioder[0].periode shouldBe Datoperiode(LocalDate.of(2017, 1, 1), LocalDate.of(2017, 1, 31)) + beregnetPerioderDto.beregnetPerioder[0].feilutbetaltBeløp shouldBe BigDecimal("10000") + beregnetPerioderDto.beregnetPerioder[1].periode shouldBe Datoperiode(LocalDate.of(2017, 2, 1), LocalDate.of(2017, 2, 28)) + beregnetPerioderDto.beregnetPerioder[1].feilutbetaltBeløp shouldBe BigDecimal("10000") + } + + @Test + fun `beregnBeløp skal ikke beregne feilutbetaltBeløp når saksbehandler deler opp periode som ikke starter første dato`() { + val exception = shouldThrow { + tilbakekrevingsberegningService.beregnBeløp( + behandlingId = Testdata.behandling.id, + perioder = listOf( + Datoperiode( + LocalDate.of(2017, 1, 1), + LocalDate.of(2017, 1, 31), + ), + Datoperiode( + LocalDate.of(2017, 2, 16), + LocalDate.of(2017, 2, 28), + ), + ), + ) + } + exception.message shouldBe "Periode med ${ + Datoperiode( + LocalDate.of(2017, 2, 16), + LocalDate.of(2017, 2, 28), + ) + } er ikke i hele måneder" + } + + @Test + fun `beregnBeløp skal ikke beregne feilutbetaltBeløp når saksbehandler deler opp periode som ikke slutter siste dato`() { + val exception = shouldThrow { + tilbakekrevingsberegningService.beregnBeløp( + behandlingId = Testdata.behandling.id, + perioder = listOf( + Datoperiode( + LocalDate.of(2017, 1, 1), + LocalDate.of(2017, 1, 27), + ), + Datoperiode( + LocalDate.of(2017, 2, 1), + LocalDate.of(2017, 2, 28), + ), + ), + ) + } + exception.message shouldBe "Periode med ${ + Datoperiode( + LocalDate.of(2017, 1, 1), + LocalDate.of(2017, 1, 27), + ) + } er ikke i hele måneder" + } + + private fun lagVilkårsvurderingMedForsett(behandlingId: UUID, vararg perioder: Månedsperiode) { + val vurderingsperioder = perioder.map { + Vilkårsvurderingsperiode( + periode = Månedsperiode(it.fom, it.tom), + begrunnelse = "foo", + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.FORSETT, + begrunnelse = "foo", + ), + ) + }.toSet() + val vurdering = Vilkårsvurdering( + behandlingId = behandlingId, + perioder = vurderingsperioder, + ) + + vilkårsvurderingRepository.insert(vurdering) + } + + private fun lagForeldelse( + behandlingId: UUID, + periode: Månedsperiode, + resultat: Foreldelsesvurderingstype, + foreldelsesFrist: LocalDate?, + ) { + val vurdertForeldelse = + VurdertForeldelse( + behandlingId = behandlingId, + foreldelsesperioder = setOf( + Foreldelsesperiode( + periode = periode, + begrunnelse = "foo", + foreldelsesvurderingstype = resultat, + foreldelsesfrist = foreldelsesFrist, + ), + ), + ) + vurdertForeldelseRepository.insert(vurdertForeldelse) + } + + private fun lagKravgrunnlag(periode: Månedsperiode, skattProsent: BigDecimal) { + val p = Testdata.kravgrunnlagsperiode432.copy( + id = UUID.randomUUID(), + periode = periode, + beløp = setOf( + lagFeilBeløp(BigDecimal.valueOf(10000)), + lagYtelBeløp(BigDecimal.valueOf(10000), skattProsent), + ), + ) + val grunnlag: Kravgrunnlag431 = Testdata.kravgrunnlag431.copy(perioder = setOf(p)) + kravgrunnlagRepository.insert(grunnlag) + } + + private fun lagGrunnlagPeriode( + periode: Månedsperiode, + skattMnd: Int, + beløp: Set = setOf(), + ): Kravgrunnlagsperiode432 { + return Kravgrunnlagsperiode432( + periode = periode, + månedligSkattebeløp = BigDecimal.valueOf(skattMnd.toLong()), + beløp = beløp, + ) + } + + private fun lagGrunnlag(perioder: Set): Kravgrunnlag431 { + return Testdata.kravgrunnlag431.copy(perioder = perioder) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/beregning/TilbakekrevingsberegningVilk\303\245rTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/beregning/TilbakekrevingsberegningVilk\303\245rTest.kt" new file mode 100644 index 000000000..1d6b309f2 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/beregning/TilbakekrevingsberegningVilk\303\245rTest.kt" @@ -0,0 +1,635 @@ +package no.nav.familie.tilbake.beregning + +import com.google.common.collect.Lists +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.beregning.modell.Beregningsresultatsperiode +import no.nav.familie.tilbake.beregning.modell.FordeltKravgrunnlagsbeløp +import no.nav.familie.tilbake.beregning.modell.GrunnlagsperiodeMedSkatteprosent +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.AnnenVurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingAktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingGodTro +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsperiode +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vurdering +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate + +class TilbakekrevingsberegningVilkårTest { + + private lateinit var vilkårsvurderingsperiode: Vilkårsvurderingsperiode + private lateinit var grunnlagsperiodeMedSkatteprosent: GrunnlagsperiodeMedSkatteprosent + private lateinit var forstodBurdeForståttVurdering: Vilkårsvurderingsperiode + + private val FEILUTBETALT_BELØP = BigDecimal.valueOf(10_000) + private val RENTEPROSENT = BigDecimal.valueOf(10) + + @BeforeEach + fun setup() { + vilkårsvurderingsperiode = + Vilkårsvurderingsperiode( + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + periode = Månedsperiode( + LocalDate.of(2019, 5, 1), + LocalDate.of(2019, 5, 3), + ), + begrunnelse = "foo", + ) + forstodBurdeForståttVurdering = + Vilkårsvurderingsperiode( + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + periode = Månedsperiode( + LocalDate.of(2019, 5, 1), + LocalDate.of(2019, 5, 3), + ), + begrunnelse = "foo", + ) + + grunnlagsperiodeMedSkatteprosent = + GrunnlagsperiodeMedSkatteprosent(vilkårsvurderingsperiode.periode, BigDecimal.valueOf(10000), BigDecimal.ZERO) + } + + @Nested + inner class VilkårsvurderingGodTro { + @Test + fun `beregn skalkreve tilbake beløp som er i_behold uten renter ved god tro`() { + val manueltBeløp = BigDecimal.valueOf(8991) + + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + godTro = VilkårsvurderingGodTro( + beløpErIBehold = true, + beløpTilbakekreves = manueltBeløp, + begrunnelse = "foo", + ), + ) + + val resultat: Beregningsresultatsperiode = + beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagsperiodeMedSkatteprosent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = AnnenVurdering.GOD_TRO, + manueltSattTilbakekrevingsbeløp = manueltBeløp, + tilbakekrevingsbeløpUtenRenter = manueltBeløp, + tilbakekrevingsbeløp = manueltBeløp, + andelAvBeløp = null, + ) + } + + @Test + fun `beregn skalkreve tilbake ingenting når det er god tro og beløp ikke er i_behold`() { + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + godTro = VilkårsvurderingGodTro( + beløpErIBehold = false, + begrunnelse = "foo", + ), + ) + + val resultat: Beregningsresultatsperiode = + beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagsperiodeMedSkatteprosent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = AnnenVurdering.GOD_TRO, + tilbakekrevingsbeløpUtenRenter = BigDecimal.ZERO, + tilbakekrevingsbeløp = BigDecimal.ZERO, + andelAvBeløp = BigDecimal.ZERO, + ) + } + + @Test + fun `beregn skalkreve tilbake beløp som er i_behold uten renter ved god tro med skatt prosent`() { + val beløpTilbakekreves = BigDecimal.valueOf(8991) + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + godTro = VilkårsvurderingGodTro( + beløpErIBehold = true, + beløpTilbakekreves = beløpTilbakekreves, + begrunnelse = "foo", + ), + ) + val grunnlagPeriodeMedSkattProsent = + GrunnlagsperiodeMedSkatteprosent( + periode = vilkårsvurdering.periode, + tilbakekrevingsbeløp = FEILUTBETALT_BELØP, + skatteprosent = BigDecimal.valueOf(10), + ) + + val resultat: Beregningsresultatsperiode = + beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagPeriodeMedSkattProsent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = AnnenVurdering.GOD_TRO, + tilbakekrevingsbeløpUtenRenter = beløpTilbakekreves, + tilbakekrevingsbeløp = beløpTilbakekreves, + manueltSattTilbakekrevingsbeløp = beløpTilbakekreves, + skattebeløp = BigDecimal.valueOf(899), + tilbakekrevingsbeløpEtterSkatt = BigDecimal.valueOf(8092), + andelAvBeløp = null, + ) + } + } + + @Nested + inner class VilkårsvurderingAktsomhetForsett { + @Test + fun `beregn skal kreve tilbake alt med renter ved forsett og illeggRenter ikke satt`() { + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.FORSETT, + begrunnelse = "foo", + ), + ) + + val resultat: Beregningsresultatsperiode = + beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagsperiodeMedSkatteprosent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.FORSETT, + tilbakekrevingsbeløpUtenRenter = FEILUTBETALT_BELØP, + tilbakekrevingsbeløp = BigDecimal.valueOf(11000), + rentebeløp = BigDecimal.valueOf(1000), + renteprosent = RENTEPROSENT, + andelAvBeløp = BigDecimal.valueOf(100), + ) + } + + @Test + fun `beregn skal kreve tilbake alt med renter ved forsett og illeggRenter satt true`() { + val vilkårsvurdering = + forstodBurdeForståttVurdering.copy( + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.FORSETT, + begrunnelse = "foo", + ileggRenter = true, + ), + ) + + val resultat: Beregningsresultatsperiode = beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagsperiodeMedSkatteprosent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.FORSETT, + tilbakekrevingsbeløpUtenRenter = FEILUTBETALT_BELØP, + tilbakekrevingsbeløp = BigDecimal.valueOf(11000), + rentebeløp = BigDecimal.valueOf(1000), + renteprosent = RENTEPROSENT, + andelAvBeløp = BigDecimal.valueOf(100), + ) + } + + @Test + fun `beregn skalkreve tilbake alt uten renter ved forsett og illeggRenter satt false`() { + val vilkårsvurdering = + forstodBurdeForståttVurdering.copy( + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.FORSETT, + begrunnelse = "foo", + ileggRenter = false, + ), + ) + + val resultat: Beregningsresultatsperiode = beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagsperiodeMedSkatteprosent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.FORSETT, + tilbakekrevingsbeløpUtenRenter = FEILUTBETALT_BELØP, + tilbakekrevingsbeløp = FEILUTBETALT_BELØP, + ) + } + + @Test + fun `beregn skalkreve tilbake alt med renter ved forsett med skatt prosent`() { + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.FORSETT, + begrunnelse = "foo", + ), + ) + val grunnlagPeriodeMedSkattProsent = + GrunnlagsperiodeMedSkatteprosent( + periode = vilkårsvurdering.periode, + tilbakekrevingsbeløp = FEILUTBETALT_BELØP, + skatteprosent = BigDecimal.valueOf(10), + ) + + val resultat: Beregningsresultatsperiode = + beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagPeriodeMedSkattProsent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.FORSETT, + tilbakekrevingsbeløp = BigDecimal.valueOf(11000), + renteprosent = RENTEPROSENT, + rentebeløp = BigDecimal.valueOf(1000), + skattebeløp = BigDecimal.valueOf(1000), + tilbakekrevingsbeløpEtterSkatt = BigDecimal.valueOf(10000), + ) + } + + @Test + fun `beregn skalkreve tilbake alt uten renter ved forsett men frisinn med skatt prosent`() { + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.FORSETT, + begrunnelse = "foo", + ), + ) + val grunnlagPeriodeMedSkattProsent = + GrunnlagsperiodeMedSkatteprosent( + periode = vilkårsvurdering.periode, + tilbakekrevingsbeløp = FEILUTBETALT_BELØP, + skatteprosent = BigDecimal.valueOf(10), + ) + + val resultat: Beregningsresultatsperiode = + beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagPeriodeMedSkattProsent), + beregnRenter = false, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.FORSETT, + skattebeløp = BigDecimal.valueOf(1000), + tilbakekrevingsbeløpEtterSkatt = BigDecimal.valueOf(9000), + ) + } + + @Test + fun `beregn skatt med 4 og 6 desimaler`() { + val tilbakekrevingsbeløp = BigDecimal.valueOf(4212) + val skatteprosent = BigDecimal.valueOf(33.9981) + + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.FORSETT, + begrunnelse = "foo", + ), + ) + + val grunnlagPeriodeMedSkattProsent = + listOf( + GrunnlagsperiodeMedSkatteprosent( + periode = vilkårsvurdering.periode, + tilbakekrevingsbeløp = tilbakekrevingsbeløp, + skatteprosent = skatteprosent, + ), + ) + + val resultat = beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = tilbakekrevingsbeløp, + perioderMedSkatteprosent = grunnlagPeriodeMedSkattProsent, + beregnRenter = true, + bruk6desimalerISkatteberegning = false, + ) + val resultatMed6Desimaler = beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = tilbakekrevingsbeløp, + perioderMedSkatteprosent = grunnlagPeriodeMedSkattProsent, + beregnRenter = true, + bruk6desimalerISkatteberegning = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.FORSETT, + feilutbetalt = tilbakekrevingsbeløp, + renteprosent = RENTEPROSENT, + rentebeløp = BigDecimal.valueOf(421), + tilbakekrevingsbeløpUtenRenter = tilbakekrevingsbeløp, + tilbakekrevingsbeløp = BigDecimal.valueOf(4633), + tilbakekrevingsbeløpEtterSkatt = BigDecimal.valueOf(3201), + skattebeløp = BigDecimal.valueOf(1432), + ) + + resultatMed6Desimaler.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.FORSETT, + feilutbetalt = tilbakekrevingsbeløp, + renteprosent = RENTEPROSENT, + rentebeløp = BigDecimal.valueOf(421), + tilbakekrevingsbeløpUtenRenter = tilbakekrevingsbeløp, + tilbakekrevingsbeløp = BigDecimal.valueOf(4633), + tilbakekrevingsbeløpEtterSkatt = BigDecimal.valueOf(3202), + skattebeløp = BigDecimal.valueOf(1431), + ) + } + } + + @Nested + inner class VilkårsvurderingAktsomhetGrovUaktsomhet { + @Test + fun `beregn skalkreve tilbake alt ved grov uaktsomhet når ikke annet er valgt`() { + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.GROV_UAKTSOMHET, + begrunnelse = "foo", + særligeGrunnerTilReduksjon = false, + ileggRenter = true, + ), + ) + + val resultat: Beregningsresultatsperiode = + beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagsperiodeMedSkatteprosent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.GROV_UAKTSOMHET, + tilbakekrevingsbeløp = BigDecimal.valueOf(11000), + renteprosent = RENTEPROSENT, + rentebeløp = BigDecimal.valueOf(1000), + ) + } + + @Test + fun `beregn skalkreve tilbake deler ved grov uaktsomhet når særlige grunner er valgt og ilegge renter når det er valgt`() { + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.GROV_UAKTSOMHET, + begrunnelse = "foo", + særligeGrunnerTilReduksjon = true, + ileggRenter = true, + andelTilbakekreves = BigDecimal.valueOf(70), + ), + ) + + val resultat: Beregningsresultatsperiode = + beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagsperiodeMedSkatteprosent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.GROV_UAKTSOMHET, + andelAvBeløp = BigDecimal.valueOf(70), + tilbakekrevingsbeløpUtenRenter = BigDecimal.valueOf(7000), + tilbakekrevingsbeløp = BigDecimal.valueOf(7700), + renteprosent = RENTEPROSENT, + rentebeløp = BigDecimal.valueOf(700), + ) + } + + @Test + fun `beregn skal kreve tilbake deler ved grov uaktsomhet ved når særlige grunner og ikke ilegge renter når det er false`() { + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.GROV_UAKTSOMHET, + begrunnelse = "foo", + særligeGrunnerTilReduksjon = true, + ileggRenter = false, + andelTilbakekreves = BigDecimal.valueOf(70), + ), + ) + + val resultat: Beregningsresultatsperiode = + beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagsperiodeMedSkatteprosent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.GROV_UAKTSOMHET, + andelAvBeløp = BigDecimal.valueOf(70), + tilbakekrevingsbeløpUtenRenter = BigDecimal.valueOf(7000), + tilbakekrevingsbeløp = BigDecimal.valueOf(7000), + ) + } + + @Test + fun `beregn skaltakle desimaler på prosenter som tilbakekreves`() { + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.GROV_UAKTSOMHET, + begrunnelse = "foo", + særligeGrunnerTilReduksjon = true, + ileggRenter = false, + andelTilbakekreves = BigDecimal("0.01"), + ), + ) + + val resultat: Beregningsresultatsperiode = + beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagsperiodeMedSkatteprosent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.GROV_UAKTSOMHET, + andelAvBeløp = BigDecimal("0.01"), + tilbakekrevingsbeløpUtenRenter = BigDecimal.valueOf(1), + tilbakekrevingsbeløp = BigDecimal.valueOf(1), + ) + } + + @Test + fun `beregn skalkreve tilbake manuelt beløp når det er satt`() { + val manueltSattBeløp = BigDecimal.valueOf(6556) + + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.GROV_UAKTSOMHET, + begrunnelse = "foo", + særligeGrunnerTilReduksjon = true, + ileggRenter = false, + manueltSattBeløp = manueltSattBeløp, + ), + godTro = null, + ) + + val resultat: Beregningsresultatsperiode = + beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagsperiodeMedSkatteprosent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.GROV_UAKTSOMHET, + tilbakekrevingsbeløpUtenRenter = manueltSattBeløp, + tilbakekrevingsbeløp = manueltSattBeløp, + manueltSattTilbakekrevingsbeløp = manueltSattBeløp, + andelAvBeløp = null, + ) + } + + @Test + fun `beregn skalkreve tilbake manuelt beløp med renter når det er satt`() { + val manueltSattBeløp = BigDecimal.valueOf(6000) + + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.GROV_UAKTSOMHET, + begrunnelse = "foo", + særligeGrunnerTilReduksjon = true, + ileggRenter = true, + manueltSattBeløp = manueltSattBeløp, + ), + ) + + val resultat: Beregningsresultatsperiode = + beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = FEILUTBETALT_BELØP, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagsperiodeMedSkatteprosent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.GROV_UAKTSOMHET, + tilbakekrevingsbeløpUtenRenter = manueltSattBeløp, + tilbakekrevingsbeløp = BigDecimal.valueOf(6600), + manueltSattTilbakekrevingsbeløp = manueltSattBeløp, + andelAvBeløp = null, + renteprosent = BigDecimal.valueOf(10), + rentebeløp = BigDecimal.valueOf(600), + ) + } + } + + @Nested + inner class VilkårsvurderingAktsomhetSimpelUaktsomhet { + @Test + fun `beregn skalikke kreve noe når sjette ledd benyttes for å ikke gjøre innkreving av småbeløp`() { + val feilutbetaltBeløp = BigDecimal.valueOf(522) + + val vilkårsvurdering = vilkårsvurderingsperiode.copy( + aktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.SIMPEL_UAKTSOMHET, + begrunnelse = "foo", + særligeGrunnerTilReduksjon = false, + tilbakekrevSmåbeløp = false, + ), + ) + + val resultat: Beregningsresultatsperiode = + beregn( + vilkårVurdering = vilkårsvurdering, + feilutbetalt = feilutbetaltBeløp, + perioderMedSkatteprosent = Lists.newArrayList(grunnlagsperiodeMedSkatteprosent), + beregnRenter = true, + ) + + resultat.skalHaVerdier( + vilkårsvurdering = vilkårsvurdering, + vurdering = Aktsomhet.SIMPEL_UAKTSOMHET, + feilutbetalt = feilutbetaltBeløp, + andelAvBeløp = BigDecimal.ZERO, + tilbakekrevingsbeløpUtenRenter = BigDecimal.ZERO, + tilbakekrevingsbeløp = BigDecimal.ZERO, + ) + } + } + + private fun beregn( + vilkårVurdering: Vilkårsvurderingsperiode, + feilutbetalt: BigDecimal, + perioderMedSkatteprosent: List, + beregnRenter: Boolean, + bruk6desimalerISkatteberegning: Boolean = false, + ): Beregningsresultatsperiode { + val delresultat = FordeltKravgrunnlagsbeløp(feilutbetalt, feilutbetalt, BigDecimal.ZERO) + return TilbakekrevingsberegningVilkår.beregn( + vilkårVurdering = vilkårVurdering, + delresultat = delresultat, + perioderMedSkatteprosent = perioderMedSkatteprosent, + beregnRenter = beregnRenter, + bruk6desimalerISkatteberegning = bruk6desimalerISkatteberegning, + ) + } + + /** + * Metode brukt for å sørge for at alle verdiene i resultatet sjekkes i alle tester. + * Default verdiene tilsvarer full tilbakekreving (andel = 100%) av hele ytelsesbeløpet uten renter og skatt. + */ + private fun Beregningsresultatsperiode.skalHaVerdier( + vilkårsvurdering: Vilkårsvurderingsperiode, + vurdering: Vurdering, + feilutbetalt: BigDecimal = FEILUTBETALT_BELØP, + andelAvBeløp: BigDecimal? = BigDecimal.valueOf(100), + renteprosent: BigDecimal? = null, + manueltSattTilbakekrevingsbeløp: BigDecimal? = null, + tilbakekrevingsbeløpUtenRenter: BigDecimal = feilutbetalt, + rentebeløp: BigDecimal = BigDecimal.ZERO, + tilbakekrevingsbeløp: BigDecimal = tilbakekrevingsbeløpUtenRenter, // Med mindre annet er fylt inn vil det ikke legges på renter + skattebeløp: BigDecimal = BigDecimal.ZERO, + tilbakekrevingsbeløpEtterSkatt: BigDecimal = tilbakekrevingsbeløp, // Med mindre annet er fylt inn vil det ikke tas hensyn til skatt + utbetaltYtelsesbeløp: BigDecimal = feilutbetalt, + riktigYtelsesbeløp: BigDecimal = BigDecimal.ZERO, + ) { + assertThat(this.periode).isEqualTo(vilkårsvurdering.periode) + assertThat(this.vurdering).isEqualTo(vurdering) + assertThat(this.feilutbetaltBeløp).isEqualTo(feilutbetalt) + assertThat(this.andelAvBeløp).isEqualTo(andelAvBeløp) + assertThat(this.renteprosent).isEqualTo(renteprosent) + assertThat(this.manueltSattTilbakekrevingsbeløp).isEqualTo(manueltSattTilbakekrevingsbeløp) + assertThat(this.tilbakekrevingsbeløpUtenRenter).isEqualTo(tilbakekrevingsbeløpUtenRenter) + assertThat(this.rentebeløp).isEqualTo(rentebeløp) + assertThat(this.tilbakekrevingsbeløp).isEqualTo(tilbakekrevingsbeløp) + assertThat(this.skattebeløp).isEqualTo(skattebeløp) + assertThat(this.tilbakekrevingsbeløpEtterSkatt).isEqualTo(tilbakekrevingsbeløpEtterSkatt) + assertThat(this.utbetaltYtelsesbeløp).isEqualTo(utbetaltYtelsesbeløp) + assertThat(this.riktigYtelsesbeløp).isEqualTo(riktigYtelsesbeløp) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/common/Grunnbel\303\270psperioderTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/common/Grunnbel\303\270psperioderTest.kt" new file mode 100644 index 000000000..88f8151ad --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/common/Grunnbel\303\270psperioderTest.kt" @@ -0,0 +1,83 @@ +package no.nav.familie.tilbake.common + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.common.Grunnbeløpsperioder.finnGrunnbeløpsperioder +import org.junit.jupiter.api.Test +import java.time.YearMonth + +internal class GrunnbeløpsperioderTest { + + @Test + internal fun `skal kaste feil hvis man ikke får noen treff på grunnbeløpsperioder bak i tiden`() { + shouldThrow { + finnGrunnbeløpsperioder(Månedsperiode(YearMonth.of(1900, 1))) + }.message shouldBe "Forventer å finne treff for 1900-01 - 1900-01 i grunnbeløpsperioder" + } + + @Test + internal fun `skal kaste feil når perioden sitt sluttdato er etter siste grunnbeløpet sin tom-dato`() { + shouldThrow { + finnGrunnbeløpsperioder(Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2300, 1))) + }.message shouldBe "Har ikke lagt inn grunnbeløpsperiode frem til 2300-01" + } + + @Test + internal fun `skal finne en treff for enmåneds-perioder som ikke går over flere grunnbeløpsperioder`() { + IntRange(5, 12).forEach { måned -> + assertGrunnbeløp(Månedsperiode(YearMonth.of(2020, måned)), 101_351) + } + IntRange(1, 4).forEach { måned -> + assertGrunnbeløp(Månedsperiode(YearMonth.of(2021, måned)), 101_351) + } + IntRange(5, 12).forEach { måned -> + assertGrunnbeløp(Månedsperiode(YearMonth.of(2021, måned)), 106_399) + } + } + + @Test + internal fun `skal finne en treff for periode som ikke går over flere grunnbeløpsperioder`() { + assertGrunnbeløp(Månedsperiode(YearMonth.of(2020, 5), YearMonth.of(2021, 4)), 101_351) + assertGrunnbeløp(Månedsperiode(YearMonth.of(2021, 5), YearMonth.of(2022, 4)), 106_399) + } + + @Test + internal fun `overlapper med 1 måned skal returnere 2 grunnbeløpsperioder`() { + val fra = YearMonth.of(2020, 4) + val til = YearMonth.of(2020, 5) + val resultat = finnGrunnbeløpsperioder(Månedsperiode(fra, til)) + resultat shouldHaveSize 2 + resultat[0].grunnbeløp shouldBe 99_858.toBigDecimal() + resultat[1].grunnbeløp shouldBe 101_351.toBigDecimal() + } + + @Test + internal fun `overlapper med flere måneder skal returnere 2 grunnbeløpsperioder`() { + val fra = YearMonth.of(2019, 5) + val til = YearMonth.of(2021, 4) + val resultat = finnGrunnbeløpsperioder(Månedsperiode(fra, til)) + resultat shouldHaveSize 2 + resultat[0].grunnbeløp shouldBe 99_858.toBigDecimal() + resultat[1].grunnbeløp shouldBe 101_351.toBigDecimal() + } + + @Test + internal fun `overlapper 3 grunnbeløpsperioder skal returnere 3 beløpsperioder`() { + val fra = YearMonth.of(2019, 5) + val til = YearMonth.of(2021, 5) + val resultat = finnGrunnbeløpsperioder(Månedsperiode(fra, til)) + resultat shouldHaveSize 3 + resultat[0].grunnbeløp shouldBe 99_858.toBigDecimal() + resultat[1].grunnbeløp shouldBe 101_351.toBigDecimal() + resultat[2].grunnbeløp shouldBe 106_399.toBigDecimal() + } + + private fun assertGrunnbeløp(periode: Månedsperiode, beløp: Int) { + val resultat = finnGrunnbeløpsperioder(periode) + resultat shouldHaveSize 1 + resultat[0].periode.inneholder(periode) shouldBe true + resultat[0].grunnbeløp shouldBe beløp.toBigDecimal() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/common/PeriodeTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/common/PeriodeTest.kt new file mode 100644 index 000000000..1d0c4ae36 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/common/PeriodeTest.kt @@ -0,0 +1,47 @@ +package no.nav.familie.tilbake.common + +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Månedsperiode +import org.junit.jupiter.api.Test +import java.time.YearMonth + +internal class PeriodeTest { + + @Test + fun `snitt returnerer lik periode for like perioder`() { + val periode1 = Månedsperiode(YearMonth.of(2019, 1), YearMonth.of(2019, 5)) + val periode2 = Månedsperiode(YearMonth.of(2019, 1), YearMonth.of(2019, 5)) + val snitt = periode1.snitt(periode2) + + snitt shouldBe periode1 + } + + @Test + fun `snitt returnerer null for periode uten overlap`() { + val periode1 = Månedsperiode(YearMonth.of(2019, 1), YearMonth.of(2019, 5)) + val periode2 = Månedsperiode(YearMonth.of(2018, 1), YearMonth.of(2018, 12)) + val snitt = periode1.snitt(periode2) + + snitt shouldBe null + } + + @Test + fun `snitt returnerer lik periode uansett hvilken periode som ligger til grunn`() { + val periode1 = Månedsperiode(YearMonth.of(2019, 1), YearMonth.of(2019, 5)) + val periode2 = Månedsperiode(YearMonth.of(2019, 3), YearMonth.of(2019, 12)) + val snitt1til2 = periode1.snitt(periode2) + val snitt2til1 = periode2.snitt(periode1) + + snitt1til2 shouldBe snitt2til1 + snitt1til2 shouldBe Månedsperiode(YearMonth.of(2019, 3), YearMonth.of(2019, 5)) + } + + @Test + fun `omslutter returnerer true for periode med overlap`() { + val periode1 = Månedsperiode(YearMonth.of(2019, 1), YearMonth.of(2019, 3)) + val periode2 = Månedsperiode(YearMonth.of(2019, 1), YearMonth.of(2019, 1)) + + periode1.inneholder(periode2).shouldBeTrue() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/FeatureToggleMockConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/FeatureToggleMockConfig.kt new file mode 100644 index 000000000..d62909f7d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/FeatureToggleMockConfig.kt @@ -0,0 +1,25 @@ +package no.nav.familie.tilbake.config + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile + +@TestConfiguration +@Profile("integrasjonstest") +class FeatureToggleMockConfig { + + @Bean + fun featureToggle(): FeatureToggleService { + val mockFeatureToggleService: FeatureToggleService = mockk() + val defaultValue = slot() + + every { mockFeatureToggleService.isEnabled(any()) } returns false + every { mockFeatureToggleService.isEnabled(any(), capture(defaultValue)) } answers { + defaultValue.captured + } + return mockFeatureToggleService + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/IntegrasjonerClientConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/IntegrasjonerClientConfig.kt new file mode 100644 index 000000000..01cc57e82 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/IntegrasjonerClientConfig.kt @@ -0,0 +1,284 @@ +package no.nav.familie.tilbake.config + +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import no.nav.familie.http.client.RessursException +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.dokarkiv.ArkiverDokumentResponse +import no.nav.familie.kontrakter.felles.dokarkiv.v2.ArkiverDokumentRequest +import no.nav.familie.kontrakter.felles.journalpost.DokumentInfo +import no.nav.familie.kontrakter.felles.journalpost.Journalpost +import no.nav.familie.kontrakter.felles.journalpost.Journalposttype +import no.nav.familie.kontrakter.felles.journalpost.Journalstatus +import no.nav.familie.kontrakter.felles.journalpost.RelevantDato +import no.nav.familie.kontrakter.felles.navkontor.NavKontorEnhet +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveResponseDto +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import no.nav.familie.kontrakter.felles.organisasjon.Organisasjon +import no.nav.familie.kontrakter.felles.saksbehandler.Saksbehandler +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import org.springframework.http.HttpStatus +import org.springframework.web.client.RestClientResponseException +import java.time.LocalDateTime +import java.util.UUID + +@Configuration +@Profile("mock-integrasjoner") +class IntegrasjonerClientConfig { + + @Bean + @Primary + fun integrasjonerClient(): IntegrasjonerClient { + val integrasjonerClient: IntegrasjonerClient = mockk(relaxed = true) + + val arkiverRequest = slot() + every { integrasjonerClient.arkiver(capture(arkiverRequest)) } answers { + when (arkiverRequest.captured.fnr) { + "12345678901" -> ArkiverDokumentResponse( + "jpUkjentDødsbo", + false, + listOf(no.nav.familie.kontrakter.felles.dokarkiv.DokumentInfo("id")), + ) + "04098203010" -> ArkiverDokumentResponse( + "jpUkjentDødsbo", + false, + listOf(no.nav.familie.kontrakter.felles.dokarkiv.DokumentInfo("id")), + ) + else -> ArkiverDokumentResponse( + "jpId", + false, + listOf(no.nav.familie.kontrakter.felles.dokarkiv.DokumentInfo("id")), + ) + } + } + + val journalpostId = slot() + every { integrasjonerClient.distribuerJournalpost(capture(journalpostId), any(), any(), any()) } answers { + when ( + journalpostId.captured + ) { + "jpUkjentDødsbo" -> + throw RessursException( + httpStatus = HttpStatus.GONE, + ressurs = Ressurs.failure("Ukjent adresse dødsbo"), + cause = RestClientResponseException("Ukjent adresse dødsbo", 410, "gone", null, null, null), + ) + "jpUkjentAdresse" -> + throw RessursException( + httpStatus = HttpStatus.BAD_REQUEST, + ressurs = Ressurs.failure("Mottaker har ukjent adresse"), + cause = RestClientResponseException("Mottaker har ukjent adresse", 401, "not there", null, null, null), + ) + "jpDuplikatDistribusjon" -> + throw RessursException( + httpStatus = HttpStatus.CONFLICT, + ressurs = Ressurs.failure("Dokumentet er allerede distribuert"), + cause = RestClientResponseException("Dokumentet er allerede distribuert", 409, "conflict", null, null, null), + ) + else -> "42" + } + } + + every { integrasjonerClient.hentDokument(any(), any()) } returns readMockfileFromResources() + + every { integrasjonerClient.hentJournalposterForBruker(any()) } + .returns( + listOf( + Journalpost( + journalpostId = "jpId1", + journalposttype = Journalposttype.I, + journalstatus = Journalstatus.FERDIGSTILT, + tittel = "Journalførte dokumenter 1", + relevanteDatoer = listOf( + RelevantDato( + dato = LocalDateTime.now().minusDays(7), + datotype = "DATO_REGISTRERT", + ), + RelevantDato( + dato = LocalDateTime.now().minusDays(7), + datotype = "DATO_JOURNALFOERT", + ), + ), + dokumenter = listOf( + DokumentInfo( + dokumentInfoId = "dokId1", + tittel = "Dokument 1.1", + ), + DokumentInfo( + dokumentInfoId = "dokId2", + tittel = "Dokument 1.2", + ), + ), + ), + Journalpost( + journalpostId = "jpId2", + journalposttype = Journalposttype.U, + journalstatus = Journalstatus.FERDIGSTILT, + tittel = "Journalførte dokumenter 2", + relevanteDatoer = listOf( + RelevantDato( + dato = LocalDateTime.now().minusDays(4), + datotype = "DATO_EKSPEDERT", + ), + RelevantDato( + dato = LocalDateTime.now().minusDays(4), + datotype = "DATO_JOURNALFOERT", + ), + ), + dokumenter = listOf( + DokumentInfo( + dokumentInfoId = "dokId1", + tittel = "Dokument 2.1", + ), + DokumentInfo( + dokumentInfoId = "dokId2", + tittel = "Dokument 2.2", + ), + ), + ), + Journalpost( + journalpostId = "jpId3", + journalposttype = Journalposttype.N, + journalstatus = Journalstatus.FERDIGSTILT, + tittel = "Journalførte dokumenter 3", + relevanteDatoer = listOf( + RelevantDato( + dato = LocalDateTime.now().minusDays(2), + datotype = "DATO_JOURNALFOERT", + ), + ), + dokumenter = listOf( + DokumentInfo( + dokumentInfoId = "dokId1", + tittel = "Dokument 3.1", + ), + DokumentInfo( + dokumentInfoId = "dokId2", + tittel = "Dokument 3.2", + ), + ), + ), + Journalpost( + journalpostId = "jpId4", + journalposttype = Journalposttype.I, + journalstatus = Journalstatus.FERDIGSTILT, + tittel = "Journalførte dokumenter 4", + relevanteDatoer = listOf( + RelevantDato( + dato = LocalDateTime.now().minusDays(3), + datotype = "DATO_JOURNALFOERT", + ), + ), + dokumenter = listOf( + DokumentInfo( + dokumentInfoId = "dokId1", + tittel = "Dokument 4.1", + ), + ), + ), + Journalpost( + journalpostId = "jpId5", + journalposttype = Journalposttype.U, + journalstatus = Journalstatus.FERDIGSTILT, + tittel = "Journalførte dokumenter 5", + relevanteDatoer = listOf( + RelevantDato( + dato = LocalDateTime.now().minusDays(1), + datotype = "DATO_JOURNALFOERT", + ), + ), + dokumenter = listOf( + DokumentInfo( + dokumentInfoId = "dokId1", + tittel = "Dokument 5.1", + ), + ), + ), + Journalpost( + journalpostId = "jpId6", + journalposttype = Journalposttype.N, + journalstatus = Journalstatus.UNDER_ARBEID, + tittel = "Journalførte dokumenter 6", + relevanteDatoer = listOf( + RelevantDato( + dato = LocalDateTime.now().minusDays(6), + datotype = "DATO_DOKUMENT", + ), + ), + dokumenter = listOf( + DokumentInfo( + dokumentInfoId = "dokId1", + tittel = "Dokument 6.1", + ), + ), + ), + + ), + ) + + val organisasjonsnummer = slot() + every { integrasjonerClient.hentOrganisasjon(capture(organisasjonsnummer)) } answers { + when (organisasjonsnummer.captured) { + "998765432" -> Organisasjon( + "998765432", + "Testinstitusjon", + ) + "999876543" -> Organisasjon( + "999876543", + "Testinstitusjon med langt navn for test i frontend", + ) + else -> Organisasjon( + "987654321", + "Bobs Burgers", + ) + } + } + + every { integrasjonerClient.validerOrganisasjon(any()) } returns true + + every { integrasjonerClient.hentSaksbehandler(any()) } returns Saksbehandler( + UUID.randomUUID(), + "bb1234", + "Bob", + "Burger", + "enhet", + ) + + every { integrasjonerClient.finnOppgaver(any()) } answers + { + if (Thread.currentThread().stackTrace.any { it.methodName == "opprettOppgave" }) { + FinnOppgaveResponseDto( + antallTreffTotalt = 0, + oppgaver = emptyList(), + ) + } else { + FinnOppgaveResponseDto( + antallTreffTotalt = 1, + oppgaver = listOf(Oppgave(id = 1)), + ) + } + } + + every { integrasjonerClient.ferdigstillOppgave(any()) } just Runs + + every { integrasjonerClient.hentNavkontor(any()) } returns NavKontorEnhet( + enhetId = 4806, + navn = "Mock NAV Drammen", + enhetNr = "mock", + status = "mock", + ) + + return integrasjonerClient + } + + fun readMockfileFromResources(): ByteArray { + return javaClass.getResource("/mockpdf/mocktest.pdf").readBytes() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/IntegrasjonstestConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/IntegrasjonstestConfig.kt new file mode 100644 index 000000000..7099e9e5d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/IntegrasjonstestConfig.kt @@ -0,0 +1,8 @@ +package no.nav.familie.tilbake.config + +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile + +@Profile("integrasjonstest") +@Configuration +class IntegrasjonstestConfig diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/KafkaLokalConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/KafkaLokalConfig.kt new file mode 100644 index 000000000..3165a7c1f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/KafkaLokalConfig.kt @@ -0,0 +1,108 @@ +package no.nav.familie.tilbake.config + +import no.nav.familie.kontrakter.felles.Applikasjon +import org.apache.kafka.clients.CommonClientConfigs +import org.apache.kafka.clients.admin.NewTopic +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.serialization.StringDeserializer +import org.apache.kafka.common.serialization.StringSerializer +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import org.springframework.kafka.annotation.EnableKafka +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory +import org.springframework.kafka.config.TopicBuilder +import org.springframework.kafka.core.ConsumerFactory +import org.springframework.kafka.core.DefaultKafkaConsumerFactory +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.core.ProducerFactory +import org.springframework.kafka.listener.ContainerProperties +import org.springframework.kafka.test.EmbeddedKafkaBroker + +@Configuration +@EnableKafka +@Profile("local") +class KafkaLokalConfig( + @Value("\${LOKAL_BROKER_KAFKA_PORT:8093}") private val brokerKafkaPort: Int, + @Value("\${LOKAL_BROKER_REMOTE_PORT:8094}") private val brokerRemotePort: Int, +) { + + @Bean + fun broker(): EmbeddedKafkaBroker { + return EmbeddedKafkaBroker(1) + // For å teste historikkinnslag, må EmbeddedKafkaBroker kjøre på port 8093 + // For å teste opprett behandling manuelt, må EmbeddedKafkaBroker kjøre på port 9092 + .kafkaPorts(brokerKafkaPort) + .brokerProperty( + "listeners", + "PLAINTEXT://localhost:$brokerKafkaPort,REMOTE://localhost:$brokerRemotePort", + ) + .brokerProperty( + "advertised.listeners", + "PLAINTEXT://localhost:$brokerKafkaPort,REMOTE://localhost:$brokerRemotePort", + ) + .brokerProperty("listener.security.protocol.map", "PLAINTEXT:PLAINTEXT,REMOTE:PLAINTEXT") + .brokerListProperty("spring.kafka.bootstrap-servers") + } + + @Bean + fun hentFagsystemsbehandlingRequestTopic(): NewTopic { + return TopicBuilder.name(KafkaConfig.HENT_FAGSYSTEMSBEHANDLING_REQUEST_TOPIC) + .partitions(1) + .replicas(1).build() + } + + @Bean + fun hentFagsystemsbehandlingResponsTopic(): NewTopic { + return TopicBuilder.name(KafkaConfig.HENT_FAGSYSTEMSBEHANDLING_RESPONS_TOPIC) + .partitions(1) + .replicas(1).build() + } + + @Bean + fun producerFactory(): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs()) + } + + @Bean + fun kafkaTemplate(): KafkaTemplate { + return KafkaTemplate(producerFactory()) + } + + @Bean + fun consumerFactory(): ConsumerFactory { + return DefaultKafkaConsumerFactory(consumerConfigs()) + } + + @Bean + fun concurrentKafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory { + val factory = ConcurrentKafkaListenerContainerFactory() + factory.setConcurrency(1) + factory.containerProperties.ackMode = ContainerProperties.AckMode.MANUAL + factory.consumerFactory = consumerFactory() + return factory + } + + private fun producerConfigs() = mapOf( + ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to "localhost:9092", + ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, + ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG to true, // Den sikrer rekkefølge + ProducerConfig.ACKS_CONFIG to "all", // Den sikrer at data ikke mistes + ProducerConfig.CLIENT_ID_CONFIG to Applikasjon.FAMILIE_TILBAKE.name, + ) + + private fun consumerConfigs() = mapOf( + ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to "localhost:9092", + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, + ConsumerConfig.GROUP_ID_CONFIG to "familie-tilbake", + ConsumerConfig.CLIENT_ID_CONFIG to "consumer-familie-tilbake-1", + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "latest", + CommonClientConfigs.RETRIES_CONFIG to 10, + CommonClientConfigs.RETRY_BACKOFF_MS_CONFIG to 100, + ) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/OAuth2AccessTokenTestConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/OAuth2AccessTokenTestConfig.kt new file mode 100644 index 000000000..459c3ae18 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/OAuth2AccessTokenTestConfig.kt @@ -0,0 +1,31 @@ +package no.nav.familie.tilbake.config + +import io.mockk.every +import io.mockk.mockk +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenService +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile + +@Configuration +@Profile("mock-oauth") +class OAuth2AccessTokenTestConfig { + + @Bean + @Primary + fun oAuth2AccessTokenServiceMock(): OAuth2AccessTokenService { + val tokenMockService: OAuth2AccessTokenService = mockk() + every { tokenMockService.getAccessToken(any()) } + .returns( + OAuth2AccessTokenResponse( + "Mock-token-response", + 60, + 60, + null, + ), + ) + return tokenMockService + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/OppdragClientLokalConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/OppdragClientLokalConfig.kt new file mode 100644 index 000000000..21628d069 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/OppdragClientLokalConfig.kt @@ -0,0 +1,26 @@ +package no.nav.familie.tilbake.config + +import no.nav.familie.tilbake.integration.økonomi.MockOppdragClient +import no.nav.familie.tilbake.integration.økonomi.OppdragClient +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattRepository +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile + +@Configuration +@ComponentScan(value = ["no.nav.familie.tilbake.kravgrunnlag"]) +@Profile("mock-økonomi") +class OppdragClientLokalConfig( + private val kravgrunnlagRepository: KravgrunnlagRepository, + private val økonomiXmlMottattRepository: ØkonomiXmlMottattRepository, +) { + + @Bean + @Primary + fun oppdragClient(): OppdragClient { + return MockOppdragClient(kravgrunnlagRepository, økonomiXmlMottattRepository) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/PdlClientConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/PdlClientConfig.kt new file mode 100644 index 000000000..7cd3bb145 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/config/PdlClientConfig.kt @@ -0,0 +1,56 @@ +package no.nav.familie.tilbake.config + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import no.nav.familie.tilbake.integration.pdl.PdlClient +import no.nav.familie.tilbake.integration.pdl.internal.Data +import no.nav.familie.tilbake.integration.pdl.internal.IdentInformasjon +import no.nav.familie.tilbake.integration.pdl.internal.Kjønn +import no.nav.familie.tilbake.integration.pdl.internal.PdlHentIdenterResponse +import no.nav.familie.tilbake.integration.pdl.internal.PdlIdenter +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import java.time.LocalDate + +@Configuration +@Profile("mock-pdl") +class PdlClientConfig { + + @Bean + @Primary + fun pdlClient(): PdlClient { + val pdlClient: PdlClient = mockk() + + val identerDødePersoner = listOf("doed1234") + val ident = slot() + every { pdlClient.hentPersoninfo(capture(ident), any()) } answers { + val dødsdato = if (identerDødePersoner.contains(ident.captured)) { + LocalDate.of(2022, 4, 1) + } else { + null + } + Personinfo( + ident = ident.captured ?: "32132132111", + fødselsdato = LocalDate.now().minusYears(20), + navn = "testverdi", + kjønn = Kjønn.MANN, + dødsdato = dødsdato, + ) + } + every { pdlClient.hentIdenter(any(), any()) } answers { + PdlHentIdenterResponse( + data = Data(PdlIdenter(identer = listOf(IdentInformasjon("123", "AKTORID")))), + extensions = null, + errors = listOf(), + ) + } + every { pdlClient.hentAdressebeskyttelseBolk(any(), any()) } answers { + emptyMap() + } + return pdlClient + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/data/Testdata.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/data/Testdata.kt new file mode 100644 index 000000000..6acac519d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/data/Testdata.kt @@ -0,0 +1,403 @@ +package no.nav.familie.tilbake.data + +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Fil +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Vergetype +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.avstemming.domain.Avstemmingsfil +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultat +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsvedtak +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsak +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsakstype +import no.nav.familie.tilbake.behandling.domain.Bruker +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandling.domain.Fagsystemsbehandling +import no.nav.familie.tilbake.behandling.domain.Varsel +import no.nav.familie.tilbake.behandling.domain.Varselsperiode +import no.nav.familie.tilbake.behandling.domain.Verge +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevsporing +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.Vedtaksbrevbehandling +import no.nav.familie.tilbake.dokumentbestilling.vedtak.Vedtaksbrevgrunnlag +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Friteksttype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsoppsummering +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsperiode +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetalingsperiode +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesperiode +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import no.nav.familie.tilbake.iverksettvedtak.domain.ØkonomiXmlSendt +import no.nav.familie.tilbake.kravgrunnlag.domain.Fagområdekode +import no.nav.familie.tilbake.kravgrunnlag.domain.GjelderType +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassekode +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsperiode432 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottatt +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottattArkiv +import no.nav.familie.tilbake.totrinn.domain.Totrinnsvurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingAktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingGodTro +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingSærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsperiode +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import java.math.BigDecimal +import java.math.BigInteger +import java.time.LocalDate +import java.time.YearMonth +import java.util.UUID + +object Testdata { + + val avstemmingsfil = Avstemmingsfil(fil = Fil("File.txt", ByteArray(100) { 1 })) + + private val bruker = Bruker(ident = "32132132111") + + val fagsak = Fagsak( + ytelsestype = Ytelsestype.BARNETRYGD, + fagsystem = Fagsystem.BA, + eksternFagsakId = "testverdi", + bruker = bruker, + ) + + private val date = LocalDate.now() + + private val fagsystemsbehandling = + Fagsystemsbehandling( + eksternId = UUID.randomUUID().toString(), + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + revurderingsvedtaksdato = date.minusDays(1), + resultat = "OPPHØR", + årsak = "testverdi", + ) + + val varsel = Varsel( + varseltekst = "testverdi", + varselbeløp = 123, + perioder = setOf(Varselsperiode(fom = date.minusMonths(2), tom = date)), + ) + + val verge = Verge( + ident = "32132132111", + type = Vergetype.VERGE_FOR_BARN, + orgNr = "testverdi", + navn = "testverdi", + kilde = "testverdi", + begrunnelse = "testverdi", + ) + + private val behandlingsvedtak = Behandlingsvedtak(vedtaksdato = LocalDate.now()) + + val behandlingsresultat = Behandlingsresultat(behandlingsvedtak = behandlingsvedtak) + + val behandling = Behandling( + fagsakId = fagsak.id, + type = Behandlingstype.TILBAKEKREVING, + opprettetDato = LocalDate.now(), + avsluttetDato = null, + ansvarligSaksbehandler = "saksbehandler", + ansvarligBeslutter = "beslutter", + behandlendeEnhet = "testverdi", + behandlendeEnhetsNavn = "testverdi", + manueltOpprettet = false, + fagsystemsbehandling = setOf(fagsystemsbehandling), + resultater = setOf(behandlingsresultat), + varsler = setOf(varsel), + verger = setOf(verge), + eksternBrukId = UUID.randomUUID(), + ) + + val revurdering = Behandling( + fagsakId = fagsak.id, + årsaker = setOf( + Behandlingsårsak( + originalBehandlingId = behandling.id, + type = Behandlingsårsakstype.REVURDERING_KLAGE_KA, + ), + ), + type = Behandlingstype.REVURDERING_TILBAKEKREVING, + opprettetDato = LocalDate.now(), + ansvarligSaksbehandler = "saksbehandler", + behandlendeEnhet = "testverdi", + behandlendeEnhetsNavn = "testverdi", + manueltOpprettet = false, + fagsystemsbehandling = setOf(fagsystemsbehandling.copy(id = UUID.randomUUID())), + resultater = emptySet(), + varsler = emptySet(), + verger = setOf(verge.copy(id = UUID.randomUUID())), + eksternBrukId = UUID.randomUUID(), + ) + + val behandlingsårsak = Behandlingsårsak( + type = Behandlingsårsakstype.REVURDERING_KLAGE_KA, + originalBehandlingId = behandling.id, + ) + + val behandlingsstegstilstand = Behandlingsstegstilstand( + behandlingId = behandling.id, + behandlingssteg = Behandlingssteg.FAKTA, + behandlingsstegsstatus = Behandlingsstegstatus.KLAR, + ) + + val totrinnsvurdering = Totrinnsvurdering( + behandlingId = behandling.id, + behandlingssteg = Behandlingssteg.FAKTA, + godkjent = true, + begrunnelse = "testverdi", + ) + + private val foreldelsesperiode = Foreldelsesperiode( + periode = Månedsperiode(LocalDate.now(), LocalDate.now().plusDays(1)), + foreldelsesvurderingstype = Foreldelsesvurderingstype.IKKE_FORELDET, + begrunnelse = "testverdi", + foreldelsesfrist = LocalDate.now(), + oppdagelsesdato = LocalDate.now(), + ) + + val vurdertForeldelse = VurdertForeldelse( + behandlingId = behandling.id, + foreldelsesperioder = setOf(foreldelsesperiode), + ) + + val feilKravgrunnlagsbeløp433 = Kravgrunnlagsbeløp433( + klassekode = Klassekode.KL_KODE_FEIL_BA, + klassetype = Klassetype.FEIL, + opprinneligUtbetalingsbeløp = BigDecimal.ZERO, + nyttBeløp = BigDecimal("10000"), + tilbakekrevesBeløp = BigDecimal.ZERO, + uinnkrevdBeløp = BigDecimal.ZERO, + resultatkode = "testverdi", + årsakskode = "testverdi", + skyldkode = "testverdi", + skatteprosent = BigDecimal("35.1100"), + ) + + val ytelKravgrunnlagsbeløp433 = Kravgrunnlagsbeløp433( + klassekode = Klassekode.BATR, + klassetype = Klassetype.YTEL, + opprinneligUtbetalingsbeløp = BigDecimal("10000"), + nyttBeløp = BigDecimal.ZERO, + tilbakekrevesBeløp = BigDecimal("10000"), + uinnkrevdBeløp = BigDecimal.ZERO, + resultatkode = "testverdi", + årsakskode = "testverdi", + skyldkode = "testverdi", + skatteprosent = BigDecimal("35.1100"), + ) + + val kravgrunnlagsperiode432 = Kravgrunnlagsperiode432( + periode = Månedsperiode( + YearMonth.now().minusMonths(1), + YearMonth.now(), + ), + beløp = setOf( + feilKravgrunnlagsbeløp433, + ytelKravgrunnlagsbeløp433, + ), + månedligSkattebeløp = BigDecimal("123.11"), + ) + + val kravgrunnlag431 = Kravgrunnlag431( + behandlingId = behandling.id, + vedtakId = BigInteger.ZERO, + kravstatuskode = Kravstatuskode.NYTT, + fagområdekode = Fagområdekode.EFOG, + fagsystemId = "testverdi", + fagsystemVedtaksdato = LocalDate.now(), + omgjortVedtakId = BigInteger.ZERO, + gjelderVedtakId = "testverdi", + gjelderType = GjelderType.PERSON, + utbetalesTilId = "testverdi", + utbetIdType = GjelderType.PERSON, + hjemmelkode = "testverdi", + beregnesRenter = true, + ansvarligEnhet = "testverdi", + bostedsenhet = "testverdi", + behandlingsenhet = "testverdi", + kontrollfelt = "testverdi", + saksbehandlerId = "testverdi", + referanse = "testverdi", + eksternKravgrunnlagId = BigInteger.ZERO, + perioder = setOf(kravgrunnlagsperiode432), + aktiv = true, + sperret = false, + ) + + private val vilkårsvurderingSærligGrunn = VilkårsvurderingSærligGrunn( + særligGrunn = SærligGrunn.GRAD_AV_UAKTSOMHET, + begrunnelse = "testverdi", + ) + + private val vilkårsvurderingGodTro = VilkårsvurderingGodTro( + beløpErIBehold = true, + beløpTilbakekreves = BigDecimal("32165"), + begrunnelse = "testverdi", + ) + + private val vilkårsvurderingAktsomhet = + VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.GROV_UAKTSOMHET, + ileggRenter = true, + andelTilbakekreves = BigDecimal("123.11"), + manueltSattBeløp = null, + begrunnelse = "testverdi", + særligeGrunnerTilReduksjon = true, + særligeGrunnerBegrunnelse = "testverdi", + vilkårsvurderingSærligeGrunner = setOf(vilkårsvurderingSærligGrunn), + ) + + val vilkårsperiode = + Vilkårsvurderingsperiode( + periode = Månedsperiode(LocalDate.now(), LocalDate.now().plusDays(1)), + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + begrunnelse = "testverdi", + aktsomhet = vilkårsvurderingAktsomhet, + godTro = vilkårsvurderingGodTro, + ) + + val vilkårsvurdering = Vilkårsvurdering( + behandlingId = behandling.id, + perioder = setOf(vilkårsperiode), + ) + + private val faktaFeilutbetalingsperiode = + FaktaFeilutbetalingsperiode( + periode = Månedsperiode(LocalDate.now(), LocalDate.now().plusDays(1)), + hendelsestype = Hendelsestype.ANNET, + hendelsesundertype = Hendelsesundertype.ANNET_FRITEKST, + ) + + val faktaFeilutbetaling = FaktaFeilutbetaling( + begrunnelse = "testverdi", + aktiv = true, + behandlingId = behandling.id, + perioder = setOf( + FaktaFeilutbetalingsperiode( + periode = Månedsperiode("2020-04" to "2022-08"), + hendelsestype = Hendelsestype.ANNET, + hendelsesundertype = Hendelsesundertype.ANNET_FRITEKST, + ), + faktaFeilutbetalingsperiode, + ), + ) + + val økonomiXmlMottatt = ØkonomiXmlMottatt( + melding = "testverdi", + kravstatuskode = Kravstatuskode.NYTT, + eksternFagsakId = "testverdi", + ytelsestype = Ytelsestype.BARNETRYGD, + referanse = "testverdi", + eksternKravgrunnlagId = BigInteger.ZERO, + vedtakId = BigInteger.ZERO, + kontrollfelt = "testverdi", + ) + + val økonomiXmlMottattArkiv = ØkonomiXmlMottattArkiv( + melding = "testverdi", + eksternFagsakId = "testverdi", + ytelsestype = Ytelsestype.BARNETRYGD, + ) + + val vedtaksbrevsoppsummering = Vedtaksbrevsoppsummering( + behandlingId = behandling.id, + oppsummeringFritekst = "testverdi", + ) + + val vedtaksbrevsperiode = Vedtaksbrevsperiode( + behandlingId = behandling.id, + periode = Månedsperiode(LocalDate.now(), LocalDate.now()), + fritekst = "testverdi", + fritekststype = Friteksttype.FAKTA, + ) + + val økonomiXmlSendt = ØkonomiXmlSendt( + behandlingId = behandling.id, + melding = "testverdi", + kvittering = "testverdi", + ) + + val brevsporing = Brevsporing( + behandlingId = behandling.id, + journalpostId = "testverdi", + dokumentId = "testverdi", + brevtype = Brevtype.VARSEL, + ) + + val vedtaksbrevbehandling = Vedtaksbrevbehandling( + id = fagsak.id, + type = Behandlingstype.TILBAKEKREVING, + ansvarligSaksbehandler = "saksbehandler", + ansvarligBeslutter = "beslutter", + behandlendeEnhet = "testverdi", + behandlendeEnhetsNavn = "testverdi", + fagsystemsbehandling = setOf(fagsystemsbehandling), + resultater = setOf(behandlingsresultat), + varsler = setOf(varsel), + verger = setOf(verge), + vedtaksbrevOppsummering = vedtaksbrevsoppsummering, + ) + + val vedtaksbrevgrunnlag = Vedtaksbrevgrunnlag( + id = behandling.id, + bruker = bruker, + eksternFagsakId = "testverdi", + fagsystem = Fagsystem.BA, + ytelsestype = Ytelsestype.BARNETRYGD, + behandlinger = setOf(vedtaksbrevbehandling), + ) + + fun lagFeilBeløp(feilutbetaling: BigDecimal): Kravgrunnlagsbeløp433 { + return Kravgrunnlagsbeløp433( + klassekode = Klassekode.KL_KODE_FEIL_BA, + klassetype = Klassetype.FEIL, + nyttBeløp = feilutbetaling, + opprinneligUtbetalingsbeløp = BigDecimal.ZERO, + tilbakekrevesBeløp = BigDecimal.ZERO, + uinnkrevdBeløp = BigDecimal.ZERO, + skatteprosent = BigDecimal.ZERO, + ) + } + + fun lagYtelBeløp(utbetalt: BigDecimal, skatteprosent: BigDecimal): Kravgrunnlagsbeløp433 { + return Kravgrunnlagsbeløp433( + klassekode = Klassekode.BATR, + klassetype = Klassetype.YTEL, + tilbakekrevesBeløp = BigDecimal("10000"), + opprinneligUtbetalingsbeløp = utbetalt, + nyttBeløp = BigDecimal.ZERO, + skatteprosent = skatteprosent, + ) + } + + fun lagYtelBeløp( + utbetalt: BigDecimal, + nyttBeløp: BigDecimal, + skatteprosent: BigDecimal, + ): Kravgrunnlagsbeløp433 { + return Kravgrunnlagsbeløp433( + klassekode = Klassekode.BATR, + klassetype = Klassetype.YTEL, + tilbakekrevesBeløp = BigDecimal("10000"), + opprinneligUtbetalingsbeløp = utbetalt, + nyttBeløp = nyttBeløp, + skatteprosent = skatteprosent, + skyldkode = UUID.randomUUID() + .toString(), + ) // brukte skyldkode for å få ulike Kravgrunnlagsbeløp433 + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/database/DbContainerInitializer.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/database/DbContainerInitializer.kt new file mode 100644 index 000000000..c75356ce3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/database/DbContainerInitializer.kt @@ -0,0 +1,32 @@ +package no.nav.familie.tilbake.database + +import org.springframework.boot.test.util.TestPropertyValues +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.testcontainers.containers.PostgreSQLContainer + +class DbContainerInitializer : ApplicationContextInitializer { + + override fun initialize(applicationContext: ConfigurableApplicationContext) { + postgres.start() + TestPropertyValues.of( + "spring.datasource.url=${postgres.jdbcUrl}", + "spring.datasource.username=${postgres.username}", + "spring.datasource.password=${postgres.password}", + ).applyTo(applicationContext.environment) + } + + companion object { + + // Lazy because we only want it to be initialized when accessed + private val postgres: KPostgreSQLContainer by lazy { + KPostgreSQLContainer("postgres:14.6") + .withDatabaseName("familie-tilbake") + .withUsername("postgres") + .withPassword("test") + } + } +} + +// Hack needed because testcontainers use of generics confuses Kotlin +class KPostgreSQLContainer(imageName: String) : PostgreSQLContainer(imageName) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/datavarehus/JsonGenerator.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/datavarehus/JsonGenerator.kt new file mode 100644 index 000000000..6ee534df2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/datavarehus/JsonGenerator.kt @@ -0,0 +1,617 @@ +package no.nav.familie.tilbake.datavarehus + +import com.fasterxml.jackson.annotation.JsonPropertyDescription +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.BeanProperty +import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.introspect.AnnotatedClassResolver +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonAnyFormatVisitor +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonArrayFormatVisitor +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonBooleanFormatVisitor +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatTypes +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonMapFormatVisitor +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonNullFormatVisitor +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonNumberFormatVisitor +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.JsonNodeFactory +import com.fasterxml.jackson.databind.node.ObjectNode +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.tilbake.datavarehus.saksstatistikk.sakshendelse.Behandlingstilstand +import no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak.Vedtaksoppsummering +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import java.io.File + +class JsonGenerator { + + // Kommenter ut disabled for å generere json-skjema til datavarehus. + @Disabled + @Test + fun genererSkjemaTilDatavarehus() { + val jsonSchemaGenerator = JsonSchemaGenerator(objectMapper) + val behandlingstilstandSchema: JsonNode = jsonSchemaGenerator.generateJsonSchema(Behandlingstilstand::class.java) + val vedtaksoppsummeringSchema: JsonNode = jsonSchemaGenerator.generateJsonSchema(Vedtaksoppsummering::class.java) + + File("Behandlingstilstand.json").writeText(objectMapper.writeValueAsString(behandlingstilstandSchema)) + File("vedtaksoppsummering.json").writeText(objectMapper.writeValueAsString(vedtaksoppsummeringSchema)) + } +} + +/** + * Based on JsonSchemaGenerator Created by Roee Shlomo on 11/29/2016, which was + * forked from Scala code by mbknor @ https://github.com/mbknor/mbknor-jackson-jsonSchema + */ +class JsonSchemaGenerator(private val rootObjectMapper: ObjectMapper) { + + private val customType2FormatMapping = mapOf( + "java.time.LocalDateTime" to "datetime-local", + "java.time.OffsetDateTime" to "datetime", + "java.time.LocalDate" to "date", + ) + + companion object { + + @JvmStatic val JSON_SCHEMA_DRAFT_2020_12_URL = "https://json-schema.org/draft/2020-12/schema" + } + + open class MySerializerProvider { + + private var myProvider: SerializerProvider? = null + + fun setProvider(provider: SerializerProvider?) { + myProvider = provider + } + + fun getProvider(): SerializerProvider { + return myProvider!! + } + } + + abstract class EnumSupport { + + abstract val node: ObjectNode + fun enumTypes(enums: MutableSet?) { + val enumValuesNode = JsonNodeFactory.instance.arrayNode() + node.set("enum", enumValuesNode) + enums?.forEach { + enumValuesNode.add(it) + } + } + } + + private fun setFormat(node: ObjectNode, format: String) { + node.put("format", format) + } + + data class DefinitionInfo(val ref: String?, val jsonObjectFormatVisitor: JsonObjectFormatVisitor?) + + data class WorkInProgress(val classInProgress: Class<*>, val nodeInProgress: ObjectNode) + + // Class that manages creating new defenitions or getting $refs to existing definitions + inner class DefinitionsHandler { + + private var class2Ref = HashMap, String>() + + private val definitionsNode = JsonNodeFactory.instance.objectNode() + + // Used when 'combining' multiple invocations to getOrCreateDefinition when processing polymorphism. + private var workInProgress: WorkInProgress? = null + + private var workInProgressStack: MutableList = mutableListOf() + + fun pushWorkInProgress() { + workInProgressStack.add(workInProgressStack.size, workInProgress) + workInProgress = null + } + + fun popworkInProgress() { + val item = workInProgressStack.size - 1 + workInProgress = workInProgressStack.removeAt(item) + } + + // Either creates new definitions or return $ref to existing one + fun getOrCreateDefinition( + clazz: Class<*>, + objectDefinitionBuilder: (ObjectNode) -> JsonObjectFormatVisitor?, + ): DefinitionInfo { + val ref = class2Ref[clazz] + if (ref != null) { + if (workInProgress != null) { + // this is a recursive polymorphism call + if (clazz != workInProgress!!.classInProgress) { + throw Exception("Wrong class - working on ${workInProgress!!.classInProgress} - got $clazz") + } + return DefinitionInfo(null, objectDefinitionBuilder(workInProgress!!.nodeInProgress)) + } + return DefinitionInfo(ref, null) + } + // new one - must build it + var retryCount = 0 + var shortRef = clazz.simpleName + var longRef = "#/definitions/$shortRef" + while (class2Ref.values.contains(longRef)) { + retryCount += 1 + shortRef = clazz.simpleName + "_" + retryCount + longRef = "#/definitions/" + clazz.simpleName + "_" + retryCount + } + class2Ref[clazz] = longRef + + // create definition + val node = JsonNodeFactory.instance.objectNode() + + // When processing polymorphism, we might get multiple recursive calls to getOrCreateDefinition - + // this is a wau to combine them + workInProgress = WorkInProgress(clazz, node) + definitionsNode.set(shortRef, node) + val jsonObjectFormatVisitor = objectDefinitionBuilder.invoke(node) + workInProgress = null + return DefinitionInfo(longRef, jsonObjectFormatVisitor) + } + + fun getFinalDefinitionsNode(): ObjectNode? { + if (class2Ref.isEmpty()) { + return null + } + return definitionsNode + } + } + + data class PolymorphismInfo(val typePropertyName: String, val subTypeName: String) + + data class PropertyNode(val main: ObjectNode, val meta: ObjectNode) + + inner class MyJsonFormatVisitorWrapper( + val objectMapper: ObjectMapper, + private val level: Int = 0, + val node: ObjectNode = JsonNodeFactory.instance.objectNode(), + val definitionsHandler: DefinitionsHandler, + // This property may represent the BeanProperty when we're directly processing beneath the property + val currentProperty: BeanProperty?, + ) : JsonFormatVisitorWrapper, + MySerializerProvider() { + + open inner class MyJsonObjectFormatVisitor( + private val thisObjectNode: ObjectNode, + private val propertiesNode: ObjectNode, + ) : JsonObjectFormatVisitor, + MySerializerProvider() { + + private fun myPropertyHandler( + propertyName: String, + propertyType: JavaType, + prop: BeanProperty?, + jsonPropertyRequired: Boolean, + ) { + if (propertiesNode.get(propertyName) != null) { + return + } + + val thisPropertyNode1 = JsonNodeFactory.instance.objectNode() + propertiesNode.set(propertyName, thisPropertyNode1) + val thisPropertyNode = PropertyNode(thisPropertyNode1, thisPropertyNode1) + + // Continue processing this property + + val childVisitor = createChild(thisPropertyNode.main, currentProperty = prop) + + definitionsHandler.pushWorkInProgress() + objectMapper.acceptJsonFormatVisitor(propertyType, childVisitor) + definitionsHandler.popworkInProgress() + + // Check if we should set this property as required + val rawClass = propertyType.rawClass + val requiredProperty = when { + rawClass.isPrimitive -> true + jsonPropertyRequired -> true + else -> prop?.getAnnotation(NotNull::class.java) != null + } + + if (requiredProperty) { + getRequiredArrayNode(thisObjectNode).add(propertyName) + } + + if (prop != null) { + resolvePropertyFormat(prop)?.let { + setFormat(thisPropertyNode.main, it) + } + + prop.getAnnotation(JsonPropertyDescription::class.java)?.let { + thisPropertyNode.meta.put("description", it.value) + } + } + } + + override fun property(writer: BeanProperty?) { + if (writer != null) { + myPropertyHandler(writer.name, writer.type, writer, jsonPropertyRequired = true) + } + } + + override fun property(name: String, handler: JsonFormatVisitable?, propertyTypeHint: JavaType) { + myPropertyHandler(name, propertyTypeHint, null, jsonPropertyRequired = true) + } + + override fun optionalProperty(writer: BeanProperty?) { + if (writer != null) { + myPropertyHandler(writer.name, writer.type, writer, jsonPropertyRequired = false) + } + } + + override fun optionalProperty(name: String, handler: JsonFormatVisitable?, propertyTypeHint: JavaType) { + myPropertyHandler(name, propertyTypeHint, null, jsonPropertyRequired = false) + } + } + + fun createChild(childNode: ObjectNode, currentProperty: BeanProperty?): MyJsonFormatVisitorWrapper { + return MyJsonFormatVisitorWrapper( + objectMapper, + level + 1, + node = childNode, + definitionsHandler = definitionsHandler, + currentProperty = currentProperty, + ) + } + + override fun expectStringFormat(type: JavaType?): JsonStringFormatVisitor { + node.put("type", "string") + + if (currentProperty != null) { + // Look for @Pattern + currentProperty.getAnnotation(Pattern::class.java)?.let { + node.put("pattern", it.regexp) + } + + // Look for @Size + currentProperty.getAnnotation(Size::class.java)?.let { + if (it.min > 0) { + node.put("minLength", it.min) + } + if (it.max != Integer.MAX_VALUE) { + node.put("maxLength", it.max) + } + } + } + + return object : JsonStringFormatVisitor, EnumSupport() { + override val node: ObjectNode + get() = this@MyJsonFormatVisitorWrapper.node + + override fun format(format: JsonValueFormat?) { + setFormat(this@MyJsonFormatVisitorWrapper.node, format.toString()) + } + } + } + + override fun expectArrayFormat(type: JavaType): JsonArrayFormatVisitor { + node.put("type", "array") + + val itemsNode = JsonNodeFactory.instance.objectNode() + node.set("items", itemsNode) + + // We get improved result while processing kotlin-collections by getting elementType this way + // instead of using the one which we receive in JsonArrayFormatVisitor.itemsFormat + // This approach also works for Java + val preferredElementType: JavaType? = type.contentType + + return object : JsonArrayFormatVisitor, MySerializerProvider() { + + override fun itemsFormat(handler: JsonFormatVisitable?, elementType: JavaType?) { + objectMapper.acceptJsonFormatVisitor( + preferredElementType ?: elementType, + createChild(itemsNode, currentProperty = null), + ) + } + + override fun itemsFormat(format: JsonFormatTypes?) { + if (format != null) { + itemsNode.put("type", format.value()) + } + } + } + } + + override fun expectNullFormat(type: JavaType?): JsonNullFormatVisitor { + return object : JsonNullFormatVisitor {} + } + + override fun expectNumberFormat(type: JavaType?): JsonNumberFormatVisitor { + node.put("type", "number") + + // Look for @Min, @Max => minumum, maximum + currentProperty?.let { property -> + property.getAnnotation(Min::class.java)?.let { + node.put("minimum", it.value) + } + property.getAnnotation(Max::class.java)?.let { + node.put("maximum", it.value) + } + } + + return object : JsonNumberFormatVisitor, EnumSupport() { + override val node: ObjectNode + get() = this@MyJsonFormatVisitorWrapper.node + + override fun format(format: JsonValueFormat?) { + setFormat(this@MyJsonFormatVisitorWrapper.node, format.toString()) + } + + override fun numberType(type: JsonParser.NumberType?) { + } + } + } + + override fun expectAnyFormat(type: JavaType?): JsonAnyFormatVisitor { + return object : JsonAnyFormatVisitor {} + } + + override fun expectMapFormat(type: JavaType?): JsonMapFormatVisitor { + // There is no way to specify map in jsonSchema, + // So we're going to treat it as type=object with additionalProperties = true, + // so that it can hold whatever the map can hold + + val additionalPropsObject = JsonNodeFactory.instance.objectNode() + + node.put("type", "object") + node.set("additionalProperties", additionalPropsObject) + + // TODO: this is from latest mbknor - is it better? +// definitionsHandler.pushWorkInProgress() +// val childVisitor = createChild(additionalPropsObject, null) +// objectMapper.acceptJsonFormatVisitor(type!!.containedType(1), childVisitor) +// definitionsHandler.popworkInProgress() + + return object : JsonMapFormatVisitor, MySerializerProvider() { + override fun valueFormat(handler: JsonFormatVisitable?, valueType: JavaType?) { + objectMapper.acceptJsonFormatVisitor(valueType, createChild(additionalPropsObject, currentProperty = null)) + } + + override fun keyFormat(handler: JsonFormatVisitable?, keyType: JavaType?) { + if (keyType != null) { + if (!keyType.isTypeOrSubTypeOf(String::class.java)) { + node.put("additionalProperties", true) + } + } + } + } + } + + override fun expectIntegerFormat(type: JavaType?): JsonIntegerFormatVisitor { + node.put("type", "integer") + + // Look for @Min, @Max => minumum, maximum + currentProperty?.let { property -> + property.getAnnotation(Min::class.java)?.let { + node.put("minimum", it.value) + } + property.getAnnotation(Max::class.java)?.let { + node.put("maximum", it.value) + } + } + return object : JsonIntegerFormatVisitor, EnumSupport() { + override val node: ObjectNode + get() = this@MyJsonFormatVisitorWrapper.node + + override fun format(format: JsonValueFormat?) { + setFormat(this@MyJsonFormatVisitorWrapper.node, format.toString()) + } + + override fun numberType(type: JsonParser.NumberType?) { + } + } + } + + override fun expectBooleanFormat(type: JavaType?): JsonBooleanFormatVisitor { + node.put("type", "boolean") + + return object : JsonBooleanFormatVisitor, EnumSupport() { + override val node: ObjectNode + get() = this@MyJsonFormatVisitorWrapper.node + + override fun format(format: JsonValueFormat?) { + setFormat(this@MyJsonFormatVisitorWrapper.node, format.toString()) + } + } + } + + private fun getRequiredArrayNode(objectNode: ObjectNode): ArrayNode { + if (objectNode.has("required")) { + val node = objectNode.get("required") + if (node is ArrayNode) { + return node + } + } + val rn = JsonNodeFactory.instance.arrayNode() + objectNode.set("required", rn) + return rn + } + + private fun extractPolymorphismInfo(_type: JavaType): PolymorphismInfo? { + // look for @JsonTypeInfo + val ac = AnnotatedClassResolver.resolve(objectMapper.deserializationConfig, _type, objectMapper.deserializationConfig) + val jsonTypeInfo: JsonTypeInfo? = ac.annotations?.get(JsonTypeInfo::class.java) + + if (jsonTypeInfo != null) { + if (jsonTypeInfo.include != JsonTypeInfo.As.PROPERTY) { + throw Exception("We only support polymorphism using jsonTypeInfo.include() == JsonTypeInfo.As.PROPERTY") + } + if (jsonTypeInfo.use != JsonTypeInfo.Id.NAME) { + throw Exception("We only support polymorphism using jsonTypeInfo.use == JsonTypeInfo.Id.NAME") + } + + val propertyName = jsonTypeInfo.property + val subTypeName: String = objectMapper.subtypeResolver + .collectAndResolveSubtypesByClass(objectMapper.deserializationConfig, ac) + .first { it.type == _type.rawClass } // find first + .name + return PolymorphismInfo(propertyName, subTypeName) + } + return null + } + + override fun expectObjectFormat(_type: JavaType): JsonObjectFormatVisitor? { + val ac = AnnotatedClassResolver.resolve(objectMapper.deserializationConfig, _type, objectMapper.deserializationConfig) + val resolvedSubTypes = + objectMapper.subtypeResolver.collectAndResolveSubtypesByClass(objectMapper.deserializationConfig, ac) + + val subTypes = resolvedSubTypes.map { it.type }.filter { + _type.rawClass.isAssignableFrom(it) && _type.rawClass != it + } + + if (subTypes.isNotEmpty()) { + val anyOfArrayNode = JsonNodeFactory.instance.arrayNode() + node.set("oneOf", anyOfArrayNode) + + subTypes.forEach { clazz -> + + val definitionInfo: DefinitionInfo = definitionsHandler.getOrCreateDefinition(clazz) { + val childVisitor = createChild(it, currentProperty = null) + objectMapper.acceptJsonFormatVisitor(clazz, childVisitor) + null + } + + val thisOneOfNode = JsonNodeFactory.instance.objectNode() + thisOneOfNode.put("\$ref", definitionInfo.ref) + anyOfArrayNode.add(thisOneOfNode) + } + + return null // Returning null to stop jackson from visiting this object since we have done it manually + } else { + val objectBuilder: (ObjectNode) -> JsonObjectFormatVisitor? = { thisObjectNode -> + + thisObjectNode.put("type", "object") + thisObjectNode.put("additionalProperties", false) + + // If class is annotated with com.dr.ktjsonschema.JsonSchemaFormat, we should add it + val annotatedClass = AnnotatedClassResolver.resolve( + objectMapper.deserializationConfig, + _type, + objectMapper.deserializationConfig, + ) + resolvePropertyFormat(_type)?.let { + setFormat(thisObjectNode, it) + } + + annotatedClass.annotations.get(JsonPropertyDescription::class.java)?.let { + thisObjectNode.put("description", it.value) + } + + val propertiesNode = JsonNodeFactory.instance.objectNode() + thisObjectNode.set("properties", propertiesNode) + + extractPolymorphismInfo(_type)?.let { + val pi = it + + // This class is a child in a polymorphism config. + // Set the title = subTypeName + thisObjectNode.put("title", pi.subTypeName) + + // must inject the 'type'-param and value as enum with only one possible value + val enumValuesNode = JsonNodeFactory.instance.arrayNode() + enumValuesNode.add(pi.subTypeName) + + val enumObjectNode = JsonNodeFactory.instance.objectNode() + enumObjectNode.put("type", "string") + enumObjectNode.set("enum", enumValuesNode) + enumObjectNode.put("default", pi.subTypeName) + + // Make sure the editor hides this polymorphism-specific property + val optionsNode = JsonNodeFactory.instance.objectNode() + enumObjectNode.set("options", optionsNode) + optionsNode.put("hidden", true) + + propertiesNode.set(pi.typePropertyName, enumObjectNode) + + getRequiredArrayNode(thisObjectNode).add(pi.typePropertyName) + } + + MyJsonObjectFormatVisitor(thisObjectNode, propertiesNode) + } + + return if (level == 0) { + // This is the first level - we must not use definitions + objectBuilder(node) + } else { + val definitionInfo: DefinitionInfo = definitionsHandler.getOrCreateDefinition(_type.rawClass, objectBuilder) + + definitionInfo.ref?.let { + // Must add ref to def at "this location" + node.put("\$ref", it) + } + + definitionInfo.jsonObjectFormatVisitor + } + } + } + } + + private fun generateTitleFromPropertyName(propertyName: String): String { + // Code found here: + // http://stackoverflow.com/questions/2559759/how-do-i-convert-camelcase-into-human-readable-names-in-java + val s = propertyName.replace( + Regex( + String.format( + "%s|%s|%s", + "(?<=[A-Z])(?=[A-Z][a-z])", + "(?<=[^A-Z])(?=[A-Z])", + "(?<=[A-Za-z])(?=[^A-Za-z])", + ), + ), + " ", + ) + + // Make the first letter uppercase + return s.substring(0, 1).uppercase() + s.substring(1) + } + + fun resolvePropertyFormat(_type: JavaType): String? { + return resolvePropertyFormat(_type.rawClass.name) + } + + fun resolvePropertyFormat(prop: BeanProperty): String? { + return resolvePropertyFormat(prop.type.rawClass.name) + } + + private fun resolvePropertyFormat(rawClassName: String): String? { + return customType2FormatMapping[rawClassName] + } + + fun generateJsonSchema(clazz: Class): JsonNode { + val rootNode = JsonNodeFactory.instance.objectNode() + + // Specify that this is a v2020-12 json schema + rootNode.put("\$schema", JSON_SCHEMA_DRAFT_2020_12_URL) + + // Add schema title + rootNode.put("title", generateTitleFromPropertyName(clazz.simpleName)) + + val definitionsHandler = DefinitionsHandler() + val rootVisitor = MyJsonFormatVisitorWrapper( + rootObjectMapper, + node = rootNode, + definitionsHandler = definitionsHandler, + currentProperty = null, + ) + rootObjectMapper.acceptJsonFormatVisitor(clazz, rootVisitor) + + definitionsHandler.getFinalDefinitionsNode()?.let { + rootNode.set("definitions", it) + } + + return rootNode + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/BehandlingTilstandServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/BehandlingTilstandServiceTest.kt new file mode 100644 index 000000000..8651ea277 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/BehandlingTilstandServiceTest.kt @@ -0,0 +1,265 @@ +package no.nav.familie.tilbake.datavarehus.saksstatistikk + +import io.kotest.matchers.date.shouldBeBetween +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Periode +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL +import no.nav.familie.kontrakter.felles.tilbakekreving.Varsel +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.BehandlingPåVentDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.BehandlingService +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultat +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingService +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate +import java.time.OffsetDateTime +import java.time.YearMonth +import java.util.UUID + +class BehandlingTilstandServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var behandlingService: BehandlingService + + @Autowired + private lateinit var faktaFeilutbetalingService: FaktaFeilutbetalingService + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + private lateinit var service: BehandlingTilstandService + + private lateinit var behandling: Behandling + + @BeforeEach + fun setup() { + service = BehandlingTilstandService( + behandlingRepository, + behandlingsstegstilstandRepository, + fagsakRepository, + taskService, + faktaFeilutbetalingService, + ) + + fagsakRepository.insert(Testdata.fagsak) + behandling = behandlingRepository.insert(Testdata.behandling) + } + + @Test + fun `hentBehandlingensTilstand skal utlede behandlingtilstand for nyopprettet behandling`() { + val behandling = behandlingService.opprettBehandling( + lagOpprettTilbakekrevingRequest( + true, + OPPRETT_TILBAKEKREVING_MED_VARSEL, + ), + ) + val tilstand = service.hentBehandlingensTilstand(behandling.id) + + tilstand.ytelsestype shouldBe Ytelsestype.BARNETRYGD + tilstand.saksnummer shouldBe "1234567" + tilstand.behandlingUuid shouldBe behandling.eksternBrukId + tilstand.referertFagsaksbehandling shouldBe behandling.aktivFagsystemsbehandling.eksternId + tilstand.behandlingstype shouldBe Behandlingstype.TILBAKEKREVING + tilstand.behandlingsstatus shouldBe Behandlingsstatus.UTREDES + tilstand.behandlingsresultat shouldBe Behandlingsresultatstype.IKKE_FASTSATT + + tilstand.venterPåBruker shouldBe true + tilstand.venterPåØkonomi shouldBe false + tilstand.behandlingErManueltOpprettet shouldBe false + tilstand.funksjoneltTidspunkt.shouldBeBetween(OffsetDateTime.now().minusMinutes(1), OffsetDateTime.now().plusSeconds(1)) + tilstand.tekniskTidspunkt shouldBe null + tilstand.ansvarligBeslutter shouldBe behandling.ansvarligBeslutter + tilstand.ansvarligSaksbehandler shouldBe behandling.ansvarligSaksbehandler + tilstand.ansvarligEnhet shouldBe behandling.behandlendeEnhet + tilstand.totalFeilutbetaltBeløp shouldBe BigDecimal("1500") + tilstand.totalFeilutbetaltPeriode.shouldNotBeNull() + tilstand.totalFeilutbetaltPeriode!!.should { + it.fom == YearMonth.now().minusMonths(1).atDay(1) && + it.tom == YearMonth.now().atEndOfMonth() + } + } + + @Test + fun `hentBehandlingensTilstand skal utlede behandlingtilstand for nyopprettet behandling uten varsel`() { + val behandling = behandlingService.opprettBehandling( + lagOpprettTilbakekrevingRequest( + false, + OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ), + ) + val tilstand = service.hentBehandlingensTilstand(behandling.id) + + tilstand.ytelsestype shouldBe Ytelsestype.BARNETRYGD + tilstand.saksnummer shouldBe "1234567" + tilstand.behandlingUuid shouldBe behandling.eksternBrukId + tilstand.referertFagsaksbehandling shouldBe behandling.aktivFagsystemsbehandling.eksternId + tilstand.behandlingstype shouldBe Behandlingstype.TILBAKEKREVING + tilstand.behandlingsstatus shouldBe Behandlingsstatus.UTREDES + tilstand.behandlingsresultat shouldBe Behandlingsresultatstype.IKKE_FASTSATT + + tilstand.venterPåBruker shouldBe false + tilstand.venterPåØkonomi shouldBe true + tilstand.behandlingErManueltOpprettet shouldBe false + tilstand.funksjoneltTidspunkt.shouldBeBetween(OffsetDateTime.now().minusMinutes(1), OffsetDateTime.now().plusSeconds(1)) + tilstand.tekniskTidspunkt shouldBe null + tilstand.ansvarligBeslutter shouldBe behandling.ansvarligBeslutter + tilstand.ansvarligSaksbehandler shouldBe behandling.ansvarligSaksbehandler + tilstand.ansvarligEnhet shouldBe behandling.behandlendeEnhet + tilstand.totalFeilutbetaltBeløp.shouldBeNull() + tilstand.totalFeilutbetaltPeriode.shouldBeNull() + } + + @Test + fun `hentBehandlingensTilstand skal utlede behandlingtilstand for fattet behandling`() { + val behandlingsresultat = Behandlingsresultat(type = Behandlingsresultatstype.FULL_TILBAKEBETALING) + val fattetBehandling = behandling.copy( + behandlendeEnhet = "1234", + behandlendeEnhetsNavn = "foo bar", + ansvarligSaksbehandler = "Z111111", + ansvarligBeslutter = "Z111112", + resultater = setOf(behandlingsresultat), + ) + behandlingRepository.update(fattetBehandling) + behandlingsstegstilstandRepository.insert(Testdata.behandlingsstegstilstand.copy(behandlingssteg = Behandlingssteg.FATTE_VEDTAK)) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + val tilstand = service.hentBehandlingensTilstand(behandling.id) + + tilstand.ytelsestype shouldBe Ytelsestype.BARNETRYGD + tilstand.saksnummer shouldBe Testdata.fagsak.eksternFagsakId + tilstand.behandlingUuid shouldBe behandling.eksternBrukId + tilstand.referertFagsaksbehandling shouldBe behandling.aktivFagsystemsbehandling.eksternId + tilstand.behandlingstype shouldBe behandling.type + tilstand.behandlingsstatus shouldBe behandling.status + tilstand.behandlingsresultat shouldBe behandlingsresultat.type + tilstand.venterPåBruker shouldBe false + tilstand.venterPåØkonomi shouldBe false + tilstand.behandlingErManueltOpprettet shouldBe false + tilstand.funksjoneltTidspunkt.shouldBeBetween(OffsetDateTime.now().minusMinutes(1), OffsetDateTime.now().plusSeconds(1)) + tilstand.tekniskTidspunkt shouldBe null + tilstand.ansvarligBeslutter shouldBe "Z111112" + tilstand.ansvarligSaksbehandler shouldBe "Z111111" + tilstand.ansvarligEnhet shouldBe "1234" + tilstand.totalFeilutbetaltBeløp shouldBe BigDecimal("10000.00") + tilstand.totalFeilutbetaltPeriode.shouldNotBeNull() + tilstand.totalFeilutbetaltPeriode!!.should { + it.fom == YearMonth.now().minusMonths(1).atDay(1) && + it.tom == YearMonth.now().atEndOfMonth() + } + } + + @Test + fun `hentBehandlingensTilstand skal utlede behandlingstilstand for behandling på vent`() { + behandlingsstegstilstandRepository.insert(Testdata.behandlingsstegstilstand) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + behandlingService.settBehandlingPåVent( + behandling.id, + BehandlingPåVentDto( + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + LocalDate.now().plusDays(1), + ), + ) + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + + val tilstand = service.hentBehandlingensTilstand(behandling.id) + + tilstand.ytelsestype shouldBe Ytelsestype.BARNETRYGD + tilstand.saksnummer shouldBe Testdata.fagsak.eksternFagsakId + tilstand.behandlingUuid shouldBe behandling.eksternBrukId + tilstand.referertFagsaksbehandling shouldBe behandling.aktivFagsystemsbehandling.eksternId + tilstand.behandlingstype shouldBe behandling.type + tilstand.behandlingsstatus shouldBe behandling.status + tilstand.behandlingsresultat shouldBe Testdata.behandlingsresultat.type + tilstand.venterPåBruker shouldBe true + tilstand.venterPåØkonomi shouldBe false + tilstand.funksjoneltTidspunkt.shouldBeBetween( + OffsetDateTime.now().minusMinutes(1), + OffsetDateTime.now().plusSeconds(1), + ) + + tilstand.totalFeilutbetaltBeløp shouldBe BigDecimal("10000.00") + tilstand.totalFeilutbetaltPeriode.shouldNotBeNull() + tilstand.totalFeilutbetaltPeriode!!.should { + it.fom == YearMonth.now().minusMonths(1).atDay(1) && + it.tom == YearMonth.now().atEndOfMonth() + } + } + + private fun lagOpprettTilbakekrevingRequest( + finnesVarsel: Boolean, + tilbakekrevingsvalg: Tilbakekrevingsvalg, + ): OpprettTilbakekrevingRequest { + val fom = YearMonth.now().minusMonths(1).atDay(1) + val tom = YearMonth.now().atEndOfMonth() + + val varsel = if (finnesVarsel) { + Varsel( + varseltekst = "testverdi", + sumFeilutbetaling = BigDecimal.valueOf(1500L), + perioder = listOf(Periode(fom, tom)), + ) + } else { + null + } + + val faktainfo = Faktainfo( + revurderingsårsak = "testverdi", + revurderingsresultat = "testresultat", + tilbakekrevingsvalg = tilbakekrevingsvalg, + ) + + return OpprettTilbakekrevingRequest( + ytelsestype = Ytelsestype.BARNETRYGD, + fagsystem = Fagsystem.BA, + eksternFagsakId = "1234567", + personIdent = "321321322", + eksternId = UUID.randomUUID().toString(), + manueltOpprettet = false, + språkkode = Språkkode.NN, + enhetId = "8020", + enhetsnavn = "Oslo", + varsel = varsel, + revurderingsvedtaksdato = fom, + faktainfo = faktainfo, + saksbehandlerIdent = "Z0000", + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/VedtaksoppsummeringServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/VedtaksoppsummeringServiceTest.kt new file mode 100644 index 000000000..eae835e9f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/datavarehus/saksstatistikk/VedtaksoppsummeringServiceTest.kt @@ -0,0 +1,328 @@ +package no.nav.familie.tilbake.datavarehus.saksstatistikk + +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldNotBeEmpty +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultat +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsvedtak +import no.nav.familie.tilbake.behandling.domain.Iverksettingsstatus +import no.nav.familie.tilbake.beregning.TilbakekrevingsberegningService +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak.UtvidetVilkårsresultat +import no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak.VedtakPeriode +import no.nav.familie.tilbake.datavarehus.saksstatistikk.vedtak.Vedtaksoppsummering +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingRepository +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetalingsperiode +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.VurdertForeldelseRepository +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesperiode +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.Fagområdekode +import no.nav.familie.tilbake.kravgrunnlag.domain.GjelderType +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassekode +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsperiode432 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingRepository +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingAktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingGodTro +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingSærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsperiode +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +class VedtaksoppsummeringServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var vilkårsvurderingRepository: VilkårsvurderingRepository + + @Autowired + private lateinit var foreldelseRepository: VurdertForeldelseRepository + + @Autowired + private lateinit var faktaFeilutbetalingRepository: FaktaFeilutbetalingRepository + + @Autowired + private lateinit var beregningService: TilbakekrevingsberegningService + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + private lateinit var vedtaksoppsummeringService: VedtaksoppsummeringService + + private lateinit var behandling: Behandling + private lateinit var saksnummer: String + + private val periode: Månedsperiode = Månedsperiode(YearMonth.of(2020, 1), YearMonth.of(2020, 1)) + + @BeforeEach + fun setup() { + vedtaksoppsummeringService = VedtaksoppsummeringService( + behandlingRepository, + fagsakRepository, + vilkårsvurderingRepository, + foreldelseRepository, + faktaFeilutbetalingRepository, + beregningService, + ) + + behandling = Testdata.behandling.copy( + ansvarligSaksbehandler = ANSVARLIG_SAKSBEHANDLER, + ansvarligBeslutter = ANSVARLIG_BESLUTTER, + behandlendeEnhet = "8020", + ) + fagsakRepository.insert(Testdata.fagsak.copy(fagsystem = Fagsystem.EF, ytelsestype = Ytelsestype.OVERGANGSSTØNAD)) + behandling = behandlingRepository.insert(behandling) + saksnummer = Testdata.fagsak.eksternFagsakId + lagKravgrunnlag() + lagFakta() + } + + @Test + fun `hentVedtaksoppsummering skal lage oppsummering for foreldelse perioder`() { + lagForeldelse() + lagBehandlingVedtak() + + val vedtaksoppsummering: Vedtaksoppsummering = vedtaksoppsummeringService.hentVedtaksoppsummering(behandling.id) + + fellesAssertVedtaksoppsummering(vedtaksoppsummering) + val vedtakPerioder: List = vedtaksoppsummering.perioder + val vedtakPeriode: VedtakPeriode = fellesAssertVedtakPeriode(vedtakPerioder) + vedtakPeriode.feilutbetaltBeløp shouldBe BigDecimal.valueOf(1000) + vedtakPeriode.rentebeløp shouldBe BigDecimal.ZERO + vedtakPeriode.bruttoTilbakekrevingsbeløp shouldBe BigDecimal.ZERO + vedtakPeriode.aktsomhet shouldBe null + vedtakPeriode.vilkårsresultat shouldBe UtvidetVilkårsresultat.FORELDET + vedtakPeriode.harBruktSjetteLedd shouldBe false + vedtakPeriode.særligeGrunner shouldBe null + } + + @Test + fun `hentVedtaksoppsummering skal lage oppsummering for perioder med god tro`() { + lagVilkårMedGodTro() + lagBehandlingVedtak() + + val vedtaksoppsummering: Vedtaksoppsummering = vedtaksoppsummeringService.hentVedtaksoppsummering(behandling.id) + + fellesAssertVedtaksoppsummering(vedtaksoppsummering) + val vedtakPerioder: List = vedtaksoppsummering.perioder + val vedtakPeriode: VedtakPeriode = fellesAssertVedtakPeriode(vedtakPerioder) + vedtakPeriode.feilutbetaltBeløp shouldBe BigDecimal.valueOf(1000) + vedtakPeriode.rentebeløp shouldBe BigDecimal.ZERO + vedtakPeriode.bruttoTilbakekrevingsbeløp shouldBe BigDecimal.valueOf(1000) + vedtakPeriode.aktsomhet shouldBe null + vedtakPeriode.vilkårsresultat shouldBe UtvidetVilkårsresultat.GOD_TRO + vedtakPeriode.harBruktSjetteLedd shouldBe false + vedtakPeriode.særligeGrunner shouldBe null + } + + @Test + fun `hentVedtaksoppsummering skal lage oppsummering for perioder med aktsomhet`() { + lagVilkårMedAktsomhet() + lagBehandlingVedtak() + val vedtaksoppsummering: Vedtaksoppsummering = vedtaksoppsummeringService.hentVedtaksoppsummering(behandling.id) + fellesAssertVedtaksoppsummering(vedtaksoppsummering) + val vedtakPerioder: List = vedtaksoppsummering.perioder + val vedtakPeriode: VedtakPeriode = fellesAssertVedtakPeriode(vedtakPerioder) + vedtakPeriode.feilutbetaltBeløp shouldBe BigDecimal.valueOf(1000) + vedtakPeriode.rentebeløp shouldBe BigDecimal.valueOf(100) + vedtakPeriode.bruttoTilbakekrevingsbeløp shouldBe BigDecimal.valueOf(1100) + vedtakPeriode.aktsomhet shouldBe Aktsomhet.SIMPEL_UAKTSOMHET + vedtakPeriode.vilkårsresultat shouldBe UtvidetVilkårsresultat.FORSTO_BURDE_FORSTÅTT + vedtakPeriode.harBruktSjetteLedd shouldBe false + vedtakPeriode.særligeGrunner.shouldNotBeNull() + vedtakPeriode.særligeGrunner?.erSærligeGrunnerTilReduksjon shouldBe false + vedtakPeriode.særligeGrunner?.særligeGrunner.shouldNotBeEmpty() + } + + private fun fellesAssertVedtaksoppsummering(vedtaksoppsummering: Vedtaksoppsummering) { + vedtaksoppsummering.behandlingUuid.shouldNotBeNull() + vedtaksoppsummering.ansvarligBeslutter shouldBe ANSVARLIG_BESLUTTER + vedtaksoppsummering.ansvarligSaksbehandler shouldBe ANSVARLIG_SAKSBEHANDLER + vedtaksoppsummering.behandlendeEnhet.shouldNotBeEmpty() + vedtaksoppsummering.behandlingOpprettetTidspunkt.shouldNotBeNull() + vedtaksoppsummering.behandlingOpprettetTidspunkt + vedtaksoppsummering.behandlingstype shouldBe Behandlingstype.TILBAKEKREVING + vedtaksoppsummering.erBehandlingManueltOpprettet shouldBe false + vedtaksoppsummering.referertFagsaksbehandling.shouldNotBeNull() + vedtaksoppsummering.saksnummer shouldBe saksnummer + vedtaksoppsummering.vedtakFattetTidspunkt.shouldNotBeNull() + vedtaksoppsummering.ytelsestype shouldBe Ytelsestype.OVERGANGSSTØNAD + vedtaksoppsummering.forrigeBehandling shouldBe null + } + + private fun fellesAssertVedtakPeriode(vedtakPerioder: List): VedtakPeriode { + vedtakPerioder.size shouldBe 1 + val vedtakPeriode: VedtakPeriode = vedtakPerioder[0] + vedtakPeriode.fom shouldBe periode.fomDato + vedtakPeriode.tom shouldBe periode.tomDato + vedtakPeriode.hendelsestype shouldBe "BOSATT_I_RIKET" + vedtakPeriode.hendelsesundertype shouldBe "BRUKER_BOR_IKKE_I_NORGE" + return vedtakPeriode + } + + private fun lagFakta() { + val faktaFeilutbetalingPeriode = + FaktaFeilutbetalingsperiode( + periode = periode, + hendelsestype = Hendelsestype.BOSATT_I_RIKET, + hendelsesundertype = Hendelsesundertype.BRUKER_BOR_IKKE_I_NORGE, + ) + val faktaFeilutbetaling = FaktaFeilutbetaling( + behandlingId = behandling.id, + perioder = setOf(faktaFeilutbetalingPeriode), + begrunnelse = "fakta begrunnelse", + ) + + faktaFeilutbetalingRepository.insert(faktaFeilutbetaling) + } + + private fun lagForeldelse() { + val foreldelsePeriode = Foreldelsesperiode( + periode = periode, + foreldelsesvurderingstype = Foreldelsesvurderingstype.FORELDET, + begrunnelse = "foreldelse begrunnelse", + foreldelsesfrist = periode.fomDato.plusMonths(8), + ) + val vurdertForeldelse = VurdertForeldelse( + behandlingId = behandling.id, + foreldelsesperioder = setOf(foreldelsePeriode), + ) + + foreldelseRepository.insert(vurdertForeldelse) + } + + private fun lagVilkårMedAktsomhet() { + val særligGrunn = + VilkårsvurderingSærligGrunn( + særligGrunn = no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn.STØRRELSE_BELØP, + begrunnelse = "særlig grunner begrunnelse", + ) + val vilkårVurderingAktsomhet = VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.SIMPEL_UAKTSOMHET, + ileggRenter = true, + særligeGrunnerTilReduksjon = false, + begrunnelse = "aktsomhet begrunnelse", + vilkårsvurderingSærligeGrunner = setOf(særligGrunn), + ) + val vilkårVurderingPeriode = + Vilkårsvurderingsperiode( + periode = periode, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + begrunnelse = "vilkår begrunnelse", + aktsomhet = vilkårVurderingAktsomhet, + ) + val vilkårVurdering = Testdata.vilkårsvurdering.copy(perioder = setOf(vilkårVurderingPeriode)) + + vilkårsvurderingRepository.insert(vilkårVurdering) + } + + private fun lagVilkårMedGodTro() { + val vilkårVurderingGodTro = VilkårsvurderingGodTro( + beløpTilbakekreves = BigDecimal.valueOf(1000), + beløpErIBehold = false, + begrunnelse = "god tro begrunnelse", + ) + val vilkårVurderingPeriode = + Vilkårsvurderingsperiode( + periode = periode, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + begrunnelse = "vilkår begrunnelse", + godTro = vilkårVurderingGodTro, + ) + val vilkårsvurdering = Testdata.vilkårsvurdering.copy(perioder = setOf(vilkårVurderingPeriode)) + vilkårsvurderingRepository.insert(vilkårsvurdering) + } + + private fun lagBehandlingVedtak() { + val behandlingVedtak = Behandlingsvedtak( + iverksettingsstatus = Iverksettingsstatus.IVERKSATT, + vedtaksdato = LocalDate.now(), + ) + val behandlingsresultat = Behandlingsresultat( + type = Behandlingsresultatstype.FULL_TILBAKEBETALING, + behandlingsvedtak = behandlingVedtak, + ) + + val behandling = behandling.copy(resultater = setOf(behandlingsresultat)) + behandlingRepository.update(behandling) + } + + private fun lagKravgrunnlag() { + val ytelPostering = Kravgrunnlagsbeløp433( + klassekode = Klassekode.EFOG, + klassetype = Klassetype.YTEL, + tilbakekrevesBeløp = BigDecimal.valueOf(1000), + opprinneligUtbetalingsbeløp = BigDecimal.valueOf(1000), + nyttBeløp = BigDecimal.ZERO, + skatteprosent = BigDecimal.valueOf(10), + ) + val feilPostering = Kravgrunnlagsbeløp433( + klassekode = Klassekode.EFOG, + klassetype = Klassetype.FEIL, + nyttBeløp = BigDecimal.valueOf(1000), + skatteprosent = BigDecimal.valueOf(10), + tilbakekrevesBeløp = BigDecimal.valueOf(1000), + opprinneligUtbetalingsbeløp = BigDecimal.valueOf(1000), + ) + val kravgrunnlagPeriode432 = Kravgrunnlagsperiode432( + periode = periode, + månedligSkattebeløp = BigDecimal.valueOf(100), + beløp = setOf(feilPostering, ytelPostering), + ) + val kravgrunnlag431 = Kravgrunnlag431( + behandlingId = behandling.id, + eksternKravgrunnlagId = 12345L.toBigInteger(), + vedtakId = 12345L.toBigInteger(), + behandlingsenhet = "8020", + bostedsenhet = "8020", + ansvarligEnhet = "8020", + fagområdekode = Fagområdekode.EFOG, + kravstatuskode = Kravstatuskode.NYTT, + utbetalesTilId = "1234567890", + utbetIdType = GjelderType.PERSON, + gjelderVedtakId = "1234567890", + gjelderType = GjelderType.PERSON, + kontrollfelt = "2020", + saksbehandlerId = ANSVARLIG_SAKSBEHANDLER, + fagsystemId = saksnummer + "100", + referanse = "1", + perioder = setOf(kravgrunnlagPeriode432), + ) + kravgrunnlagRepository.insert(kravgrunnlag431) + } + + companion object { + + private const val ANSVARLIG_SAKSBEHANDLER = "Z13456" + private const val ANSVARLIG_BESLUTTER = "Z12456" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorPeriodeFakta.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorPeriodeFakta.kt new file mode 100644 index 000000000..91a393711 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorPeriodeFakta.kt @@ -0,0 +1,219 @@ +package no.nav.familie.tilbake.dokumentasjonsgenerator + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.handlebars.FellesTekstformaterer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.AvsnittUtil +import no.nav.familie.tilbake.dokumentbestilling.vedtak.HendelseMedUndertype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbBehandling +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbHjemmel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbKonfigurasjon +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbPerson +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbTotalresultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVarsel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevDatoer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevPeriodeOgFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbFakta +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbGrunnbeløp +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbKravgrunnlag +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbResultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVedtaksbrevsperiode +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVurderinger +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.HendelsestypePerYtelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.HendelsesundertypePerHendelsestype +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.vilkårsvurdering.domain.AnnenVurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +/** + * Brukes for å generere faktatekster for perioder. Resultatet er tekster med markup, som med "Insert markup"-macroen + * kan limes inn i Confluence, og dermed bli formattert tekst. + * + * Confluence: + * https://confluence.adeo.no/display/TFA/Generert+dokumentasjon + */ +@Disabled("Kjøres ved behov for å regenerere dokumentasjon") +class DokumentasjonsgeneratorPeriodeFakta { + + private val januar = Datoperiode(YearMonth.of(2019, 1), YearMonth.of(2019, 1)) + + @Test + fun `list ut permutasjoner for BA bokmål`() { + val felles: HbVedtaksbrevFelles = lagFelles(Ytelsestype.BARNETRYGD, Språkkode.NB) + val resultat: Map = lagFaktatekster(felles) + prettyPrint(resultat) + } + + @Test + fun `list ut permutasjoner for BA nynorsk`() { + val felles: HbVedtaksbrevFelles = lagFelles(Ytelsestype.BARNETRYGD, Språkkode.NN) + val resultat: Map = lagFaktatekster(felles) + prettyPrint(resultat) + } + + @Test + fun `list ut permutasjoner for EFOG bokmål`() { + val felles: HbVedtaksbrevFelles = lagFelles(Ytelsestype.OVERGANGSSTØNAD, Språkkode.NB) + val resultat: Map = lagFaktatekster(felles) + prettyPrint(resultat) + } + + @Test + fun `list ut permutasjoner for EFOG nynorsk`() { + val felles: HbVedtaksbrevFelles = lagFelles(Ytelsestype.OVERGANGSSTØNAD, Språkkode.NN) + val resultat: Map = lagFaktatekster(felles) + prettyPrint(resultat) + } + + @Test + fun `list ut permutasjoner for EFBT bokmål`() { + val felles: HbVedtaksbrevFelles = lagFelles(Ytelsestype.BARNETILSYN, Språkkode.NB) + val resultat: Map = lagFaktatekster(felles) + prettyPrint(resultat) + } + + @Test + fun `list ut permutasjoner for EFBT nynorsk`() { + val felles: HbVedtaksbrevFelles = lagFelles(Ytelsestype.BARNETILSYN, Språkkode.NN) + val resultat: Map = lagFaktatekster(felles) + prettyPrint(resultat) + } + + @Test + fun `list ut permutasjoner for EFSP bokmål`() { + val felles: HbVedtaksbrevFelles = lagFelles(Ytelsestype.SKOLEPENGER, Språkkode.NB) + val resultat: Map = lagFaktatekster(felles) + prettyPrint(resultat) + } + + @Test + fun `list ut permutasjoner for EFSP nynorsk`() { + val felles: HbVedtaksbrevFelles = lagFelles(Ytelsestype.SKOLEPENGER, Språkkode.NN) + val resultat: Map = lagFaktatekster(felles) + prettyPrint(resultat) + } + + private fun prettyPrint(resultat: Map) { + resultat.forEach { (typer, generertTekst) -> + println("*[ ${typer.hendelsestype.name} - ${typer.hendelsesundertype.name} ]*") + val parametrisertTekst = generertTekst + .replace(" 10\u00A0000\u00A0kroner".toRegex(), " kroner") + .replace(" 33\u00A0333\u00A0kroner".toRegex(), " kroner") + .replace(" 23\u00A0333\u00A0kroner".toRegex(), " kroner") + .replace("Søker Søkersen".toRegex(), "") + .replace("2. mars 2018".toRegex(), "") + .replace("3. mars 2018".toRegex(), "") + .replace("4. mars 2018".toRegex(), "") + .replace("ektefellen".toRegex(), "") + println(parametrisertTekst) + println() + } + } + + private fun lagFaktatekster(felles: HbVedtaksbrevFelles): Map { + return getFeilutbetalingsårsaker(felles.brevmetadata.ytelsestype).associateWith { + val periode: HbVedtaksbrevsperiode = lagPeriodeBuilder(it) + val data = HbVedtaksbrevPeriodeOgFelles(felles, periode) + FellesTekstformaterer.lagDeltekst(data, AvsnittUtil.PARTIAL_PERIODE_FAKTA) + } + } + + private fun lagPeriodeBuilder(undertype: HendelseMedUndertype): HbVedtaksbrevsperiode { + return HbVedtaksbrevsperiode( + periode = januar, + vurderinger = HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + aktsomhetsresultat = AnnenVurdering.GOD_TRO, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + beløpIBehold = BigDecimal.valueOf(5000), + ), + kravgrunnlag = HbKravgrunnlag( + feilutbetaltBeløp = BigDecimal.valueOf(10000), + riktigBeløp = BigDecimal.valueOf(23333), + utbetaltBeløp = BigDecimal.valueOf(33333), + ), + resultat = HbResultat( + tilbakekrevesBeløp = BigDecimal.valueOf(5000), + tilbakekrevesBeløpUtenSkattMedRenter = BigDecimal.valueOf(4002), + rentebeløp = BigDecimal.ZERO, + ), + fakta = HbFakta(undertype.hendelsestype, undertype.hendelsesundertype), + grunnbeløp = HbGrunnbeløp(BigDecimal.TEN, "120"), + førstePeriode = true, + ) + } + + private fun lagFelles( + ytelsestype: Ytelsestype, + språkkode: Språkkode, + ): HbVedtaksbrevFelles { + val datoer = HbVedtaksbrevDatoer( + LocalDate.of(2018, 3, 2), + LocalDate.of(2018, 3, 3), + LocalDate.of(2018, 3, 4), + ) + + return HbVedtaksbrevFelles( + brevmetadata = lagMetadata(ytelsestype, språkkode), + fagsaksvedtaksdato = LocalDate.now(), + behandling = HbBehandling(), + hjemmel = HbHjemmel("Folketrygdloven"), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.FULL_TILBAKEBETALING, + totaltRentebeløp = BigDecimal.valueOf(1000), + totaltTilbakekrevesBeløp = BigDecimal.valueOf(10000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal.valueOf(11000), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = + BigDecimal.valueOf(6855), + ), + totaltFeilutbetaltBeløp = BigDecimal.valueOf(6855), + varsel = HbVarsel( + varsletBeløp = BigDecimal.valueOf(10000), + varsletDato = LocalDate.now().minusDays(100), + ), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 6), + søker = HbPerson(navn = "Søker Søkersen"), + datoer = datoer, + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + } + + private fun lagMetadata( + ytelsestype: Ytelsestype, + språkkode: Språkkode, + ): Brevmetadata { + return Brevmetadata( + sakspartId = "", + sakspartsnavn = "", + mottageradresse = Adresseinfo("01020312345", "Bob"), + behandlendeEnhetsNavn = "Oslo", + ansvarligSaksbehandler = "Bob", + saksnummer = "1232456", + språkkode = språkkode, + ytelsestype = ytelsestype, + gjelderDødsfall = false, + ) + } + + private fun getFeilutbetalingsårsaker(ytelseType: Ytelsestype): List { + val hendelseTyper: Set = HendelsestypePerYtelsestype.getHendelsestyper(ytelseType) + val hendelseUndertypePrHendelsestype = HendelsesundertypePerHendelsestype.HIERARKI + val resultat: List = + hendelseTyper.map { + hendelseUndertypePrHendelsestype[it]?.map { undertype -> HendelseMedUndertype(it, undertype) } ?: listOf() + }.flatten() + return resultat + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorPeriodeS\303\246rligeGrunner.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorPeriodeS\303\246rligeGrunner.kt" new file mode 100644 index 000000000..ebe1bbe7f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorPeriodeS\303\246rligeGrunner.kt" @@ -0,0 +1,264 @@ +package no.nav.familie.tilbake.dokumentasjonsgenerator + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.handlebars.FellesTekstformaterer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.AvsnittUtil +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbBehandling +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbHjemmel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbKonfigurasjon +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbPerson +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbTotalresultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVarsel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevDatoer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevPeriodeOgFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbFakta +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbKravgrunnlag +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbResultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbSærligeGrunner +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVedtaksbrevsperiode +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVurderinger +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +/** + * Brukes for å generere tekster for særlige grunner for perioder. Resultatet er tekster med markup, som med + * "Insert markup"-macroen kan limes inn i Confluence, og dermed bli formattert tekst. + * + * Confluence: + * https://confluence.adeo.no/display/TFA/Generert+dokumentasjon + */ +@Disabled("Kjøres ved behov for å regenerere dokumentasjon") +class DokumentasjonsgeneratorPeriodeSærligeGrunner { + + private val januar = Datoperiode(YearMonth.of(2019, 1), YearMonth.of(2019, 1)) + + @Test + fun `list ut særlige grunner forstod burde forstått simpel uaktsomhet bokmål`() { + val felles: HbVedtaksbrevFelles = lagFellesdel(Språkkode.NB) + lagSærligeGrunnerTekster(felles, Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, Aktsomhet.SIMPEL_UAKTSOMHET) + } + + @Test + fun `list ut særlige grunner forstod burde forstått simpel uaktsomhet nynorsk`() { + val felles: HbVedtaksbrevFelles = lagFellesdel(Språkkode.NN) + lagSærligeGrunnerTekster(felles, Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, Aktsomhet.SIMPEL_UAKTSOMHET) + } + + @Test + fun `list ut særlige grunner forstod burde forstått grov uaktsomhet bokmål`() { + val felles: HbVedtaksbrevFelles = lagFellesdel(Språkkode.NB) + lagSærligeGrunnerTekster(felles, Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, Aktsomhet.GROV_UAKTSOMHET) + } + + @Test + fun `list ut særlige grunner forstod burde forstått grov uaktsomhet nynorsk`() { + val felles: HbVedtaksbrevFelles = lagFellesdel(Språkkode.NN) + lagSærligeGrunnerTekster(felles, Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, Aktsomhet.GROV_UAKTSOMHET) + } + + @Test + fun `list ut særlige grunner feilaktig mangelfulle opplysninger simpel uaktsomhet bokmål`() { + val felles: HbVedtaksbrevFelles = lagFellesdel(Språkkode.NB) + lagSærligeGrunnerTekster(felles, Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, Aktsomhet.SIMPEL_UAKTSOMHET) + } + + @Test + fun `list ut særlige grunner feilaktig mangelfulle opplysninger simpel uaktsomhet nynorsk`() { + val felles: HbVedtaksbrevFelles = lagFellesdel(Språkkode.NN) + lagSærligeGrunnerTekster(felles, Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, Aktsomhet.SIMPEL_UAKTSOMHET) + } + + @Test + fun `list ut særlige grunner feilaktig mangelfulle opplysninger grov uaktsomhet bokmål`() { + val felles: HbVedtaksbrevFelles = lagFellesdel(Språkkode.NB) + lagSærligeGrunnerTekster(felles, Vilkårsvurderingsresultat.MANGELFULLE_OPPLYSNINGER_FRA_BRUKER, Aktsomhet.GROV_UAKTSOMHET) + } + + @Test + fun `list ut særlige grunner feilaktig mangelfulle opplysninger grov uaktsomhet nynorsk`() { + val felles: HbVedtaksbrevFelles = lagFellesdel(Språkkode.NN) + lagSærligeGrunnerTekster(felles, Vilkårsvurderingsresultat.MANGELFULLE_OPPLYSNINGER_FRA_BRUKER, Aktsomhet.GROV_UAKTSOMHET) + } + + private fun lagSærligeGrunnerTekster( + felles: HbVedtaksbrevFelles, + forstoBurdeForstått: Vilkårsvurderingsresultat, + simpelUaktsom: Aktsomhet, + ) { + val boolske = booleanArrayOf(false, true) + for (sgNav in boolske) { + for (sgBeløp in boolske) { + for (sgTid in boolske) { + for (reduksjon in boolske) { + for (sgAnnet in boolske) { + lagSærligeGrunnerTekster( + felles, + forstoBurdeForstått, + simpelUaktsom, + sgNav, + sgBeløp, + sgTid, + reduksjon, + sgAnnet, + ) + } + } + } + } + } + } + + private fun lagSærligeGrunnerTekster( + felles: HbVedtaksbrevFelles, + vilkårResultat: Vilkårsvurderingsresultat, + aktsomhet: Aktsomhet, + sgNav: Boolean, + sgBeløp: Boolean, + sgTid: Boolean, + reduksjon: Boolean, + sgAnnet: Boolean, + ) { + val periode: HbVedtaksbrevsperiode = lagPeriodeDel(vilkårResultat, aktsomhet, sgNav, sgBeløp, sgTid, sgAnnet, reduksjon) + val s: String = FellesTekstformaterer.lagDeltekst( + HbVedtaksbrevPeriodeOgFelles(felles, periode), + AvsnittUtil.PARTIAL_PERIODE_SÆRLIGE_GRUNNER, + ) + val overskrift = overskrift(sgNav, sgBeløp, sgTid, sgAnnet, reduksjon) + val prettyPrint = prettyPrint(s, overskrift) + println() + println(prettyPrint) + } + + private fun overskrift(sgNav: Boolean, sgBeløp: Boolean, sgTid: Boolean, sgAnnet: Boolean, reduksjon: Boolean): String { + val deler: MutableList = ArrayList() + deler.add("grad av uaktsomhet") + if (sgNav) { + deler.add("NAV helt/delvis skyld") + } + if (sgBeløp) { + deler.add("størrelsen på beløpet") + } + if (sgTid) { + deler.add("hvor lang tid har det gått") + } + if (reduksjon) { + deler.add("reduksjon") + } + if (sgAnnet) { + deler.add("annet") + } + return deler.joinToString(" - ", "*[ ", " ]*") + } + + private fun lagPeriodeDel( + vilkårResultat: Vilkårsvurderingsresultat, + aktsomhet: Aktsomhet, + sgNav: Boolean, + sgBeløp: Boolean, + sgTid: Boolean, + sgAnnet: Boolean, + reduksjon: Boolean, + ): HbVedtaksbrevsperiode { + val sg: MutableList = ArrayList() + if (sgNav) { + sg.add(SærligGrunn.HELT_ELLER_DELVIS_NAVS_FEIL) + } + if (sgBeløp) { + sg.add(SærligGrunn.STØRRELSE_BELØP) + } + if (sgTid) { + sg.add(SærligGrunn.TID_FRA_UTBETALING) + } + if (sgAnnet) { + sg.add(SærligGrunn.ANNET) + } + val fritekstSærligeGrunnerAnnet = "[ fritekst her ]" + return HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag(feilutbetaltBeløp = BigDecimal.valueOf(1000)), + fakta = HbFakta(Hendelsestype.BARNS_ALDER, Hendelsesundertype.BARN_OVER_6_ÅR), + vurderinger = HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = vilkårResultat, + aktsomhetsresultat = aktsomhet, + særligeGrunner = HbSærligeGrunner(sg, null, fritekstSærligeGrunnerAnnet), + ), + resultat = HbResultat( + tilbakekrevesBeløp = + BigDecimal.valueOf(if (reduksjon) 500L else 1000L), + tilbakekrevesBeløpUtenSkattMedRenter = + BigDecimal.valueOf(if (reduksjon) 400L else 800L), + rentebeløp = BigDecimal.ZERO, + ), + førstePeriode = true, + ) + } + + private fun lagFellesdel(språkkode: Språkkode): HbVedtaksbrevFelles { + val datoer = HbVedtaksbrevDatoer( + LocalDate.of(2018, 3, 2), + LocalDate.of(2018, 3, 3), + LocalDate.of(2018, 3, 4), + ) + + return HbVedtaksbrevFelles( + brevmetadata = lagMetadata(språkkode), + fagsaksvedtaksdato = LocalDate.now(), + behandling = HbBehandling(), + hjemmel = HbHjemmel("Folketrygdloven"), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.FULL_TILBAKEBETALING, + totaltRentebeløp = BigDecimal.valueOf(1000), + totaltTilbakekrevesBeløp = BigDecimal.valueOf(10000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal.valueOf(11000), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = + BigDecimal.valueOf(6855), + ), + totaltFeilutbetaltBeløp = BigDecimal.valueOf(6855), + varsel = HbVarsel( + varsletBeløp = BigDecimal.valueOf(10000), + varsletDato = LocalDate.now().minusDays(100), + ), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 6), + søker = HbPerson(navn = "Søker Søkersen"), + datoer = datoer, + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + } + + private fun lagMetadata(språkkode: Språkkode): Brevmetadata { + return Brevmetadata( + sakspartId = "", + sakspartsnavn = "", + mottageradresse = Adresseinfo("01020312345", "Bob"), + behandlendeEnhetsNavn = "Oslo", + ansvarligSaksbehandler = "Bob", + saksnummer = "1232456", + språkkode = språkkode, + ytelsestype = Ytelsestype.BARNETRYGD, + gjelderDødsfall = false, + ) + } + + private fun prettyPrint(s: String, overskrift: String): String { + return s.replace("__Er det særlige grunner til å redusere beløpet?", overskrift) + .replace("__Er det særlege grunnar til å redusere beløpet?", overskrift) + .replace(" 500\u00A0kroner", " kroner") + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorPeriodeVilk\303\245r.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorPeriodeVilk\303\245r.kt" new file mode 100644 index 000000000..71500fb17 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorPeriodeVilk\303\245r.kt" @@ -0,0 +1,395 @@ +package no.nav.familie.tilbake.dokumentasjonsgenerator + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.handlebars.FellesTekstformaterer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.AvsnittUtil +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbBehandling +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbHjemmel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbKonfigurasjon +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbPerson +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbTotalresultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVarsel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevDatoer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevPeriodeOgFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbFakta +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbKravgrunnlag +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbResultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVedtaksbrevsperiode +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVurderinger +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.AnnenVurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vurdering +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +/** + * Brukes for å generere vilkårtekster for perioder. Resultatet er tekster med markup, som med "Insert markup"-macroen + * kan limes inn i Confluence, og dermed bli formattert tekst. + * + * Confluence: + * https://confluence.adeo.no/display/TFA/Generert+dokumentasjon + */ +// @Disabled("Kjøres ved behov for å regenerere dokumentasjon") +class DokumentasjonsgeneratorPeriodeVilkår { + + @Test + fun `generer vilkår for BA bokmål`() { + lagVilkårstekster(Ytelsestype.BARNETRYGD, Språkkode.NB) + } + + @Test + fun `generer vilkår for BA nynorsk`() { + lagVilkårstekster(Ytelsestype.BARNETRYGD, Språkkode.NN) + } + + @Test + fun `generer vilkår for EFOG bokmål`() { + lagVilkårstekster(Ytelsestype.OVERGANGSSTØNAD, Språkkode.NB) + } + + @Test + fun `generer vilkår for EFOG nynorsk`() { + lagVilkårstekster(Ytelsestype.OVERGANGSSTØNAD, Språkkode.NN) + } + + @Test + fun `generer vilkår for EFBT bokmål`() { + lagVilkårstekster(Ytelsestype.BARNETILSYN, Språkkode.NB) + } + + @Test + fun `generer vilkår for EFBT nynorsk`() { + lagVilkårstekster(Ytelsestype.BARNETILSYN, Språkkode.NN) + } + + @Test + fun `generer vilkår for EFSP bokmål`() { + lagVilkårstekster(Ytelsestype.SKOLEPENGER, Språkkode.NB) + } + + @Test + fun `generer vilkår for EFSP nynorsk`() { + lagVilkårstekster(Ytelsestype.SKOLEPENGER, Språkkode.NN) + } + + private fun lagVilkårstekster(ytelsetype: Ytelsestype, språkkode: Språkkode) { + vilkårResultat.forEach { resultat -> + aktsomheter.forEach { vurdering -> + foreldelseVurderinger.forEach { foreldelseVurdering -> + lagResultatOgVurderingTekster( + ytelsetype, + språkkode, + resultat, + vurdering, + foreldelseVurdering, + fritekst = false, + pengerIBehold = false, + lavtBeløp = false, + ) + lagResultatOgVurderingTekster( + ytelsetype, + språkkode, + resultat, + vurdering, + foreldelseVurdering, + fritekst = true, + pengerIBehold = false, + lavtBeløp = false, + ) + if (vurdering === Aktsomhet.SIMPEL_UAKTSOMHET) { + lagResultatOgVurderingTekster( + ytelsetype, + språkkode, + resultat, + vurdering, + foreldelseVurdering, + fritekst = false, + pengerIBehold = false, + lavtBeløp = true, + ) + } + } + } + } + foreldelseVurderinger.forEach { foreldelseVurdering -> + trueFalse.forEach { fritekst: Boolean -> + trueFalse.forEach { pengerIBehold -> + lagResultatOgVurderingTekster( + ytelsetype, + språkkode, + Vilkårsvurderingsresultat.GOD_TRO, + AnnenVurdering.GOD_TRO, + foreldelseVurdering, + fritekst, + pengerIBehold, + lavtBeløp = false, + ) + } + } + } + lagResultatOgVurderingTekster( + ytelsetype, + språkkode, + Vilkårsvurderingsresultat.UDEFINERT, + AnnenVurdering.FORELDET, + Foreldelsesvurderingstype.FORELDET, + fritekst = false, + pengerIBehold = false, + lavtBeløp = false, + ) + lagResultatOgVurderingTekster( + ytelsetype, + språkkode, + Vilkårsvurderingsresultat.UDEFINERT, + AnnenVurdering.FORELDET, + Foreldelsesvurderingstype.FORELDET, + fritekst = true, + pengerIBehold = false, + lavtBeløp = false, + ) + } + + private fun lagResultatOgVurderingTekster( + ytelsetype: Ytelsestype, + språkkode: Språkkode, + resultat: Vilkårsvurderingsresultat, + vurdering: Vurdering, + foreldelsevurdering: Foreldelsesvurderingstype, + fritekst: Boolean, + pengerIBehold: Boolean, + lavtBeløp: Boolean, + ) { + val periodeOgFelles = lagPeriodeOgFelles( + ytelsetype, + språkkode, + resultat, + vurdering, + lavtBeløp, + foreldelsevurdering, + fritekst, + pengerIBehold, + ) + val vilkårTekst = lagVilkårTekst(periodeOgFelles) + val overskrift = overskrift(resultat, vurdering, lavtBeløp, fritekst, pengerIBehold, foreldelsevurdering) + val prettyprint = prettyprint(vilkårTekst, overskrift) + println() + println(prettyprint) + } + + private fun lagVilkårTekst(periodeOgFelles: HbVedtaksbrevPeriodeOgFelles): String { + if (periodeOgFelles.periode.vurderinger.harForeldelsesavsnitt) { + return FellesTekstformaterer.lagDeltekst(periodeOgFelles, AvsnittUtil.PARTIAL_PERIODE_FORELDELSE) + + System.lineSeparator() + System.lineSeparator() + + FellesTekstformaterer.lagDeltekst(periodeOgFelles, AvsnittUtil.PARTIAL_PERIODE_VILKÅR) + } + return FellesTekstformaterer.lagDeltekst(periodeOgFelles, AvsnittUtil.PARTIAL_PERIODE_VILKÅR) + } + + private fun lagPeriodeOgFelles( + ytelsetype: Ytelsestype, + språkkode: Språkkode, + vilkårResultat: Vilkårsvurderingsresultat?, + vurdering: Vurdering, + lavtBeløp: Boolean, + foreldelsevurdering: Foreldelsesvurderingstype, + fritekst: Boolean, + pengerIBehold: Boolean, + ): HbVedtaksbrevPeriodeOgFelles { + val fellesBuilder = lagFelles(ytelsetype, språkkode) + + val vurderinger = + HbVurderinger( + foreldelsevurdering = foreldelsevurdering, + aktsomhetsresultat = vurdering, + unntasInnkrevingPgaLavtBeløp = lavtBeløp, + fritekst = if (fritekst) "[ fritekst her ]" else null, + vilkårsvurderingsresultat = vilkårResultat, + beløpIBehold = if (AnnenVurdering.GOD_TRO === vurdering) { + if (pengerIBehold) BigDecimal.valueOf(3999) else BigDecimal.ZERO + } else { + null + }, + foreldelsesfrist = if (foreldelsevurdering in setOf( + Foreldelsesvurderingstype.FORELDET, + Foreldelsesvurderingstype.TILLEGGSFRIST, + ) + ) { + FORELDELSESFRIST + } else { + null + }, + fritekstForeldelse = if (foreldelsevurdering in setOf( + Foreldelsesvurderingstype.FORELDET, + Foreldelsesvurderingstype.TILLEGGSFRIST, + ) && + fritekst + ) { + "[ fritekst her ]" + } else { + null + }, + oppdagelsesdato = if (Foreldelsesvurderingstype.TILLEGGSFRIST == foreldelsevurdering) { + OPPDAGELSES_DATO + } else { + null + }, + ) + + val periodeBuilder = + HbVedtaksbrevsperiode( + periode = JANUAR, + kravgrunnlag = HbKravgrunnlag(feilutbetaltBeløp = BigDecimal.ZERO), + fakta = HbFakta(Hendelsestype.ANNET, Hendelsesundertype.ANNET_FRITEKST), + vurderinger = vurderinger, + resultat = HbResultat( + tilbakekrevesBeløp = BigDecimal.valueOf(9999), + rentebeløp = BigDecimal.ZERO, + tilbakekrevesBeløpUtenSkattMedRenter = BigDecimal.valueOf(9999), + foreldetBeløp = BigDecimal.valueOf(2999), + ), + førstePeriode = true, + ) + return HbVedtaksbrevPeriodeOgFelles(fellesBuilder, periodeBuilder) + } + + private fun lagFelles( + ytelsestype: Ytelsestype, + språkkode: Språkkode, + ): HbVedtaksbrevFelles { + val datoer = HbVedtaksbrevDatoer( + LocalDate.of(2018, 3, 2), + LocalDate.of(2018, 3, 3), + LocalDate.of(2018, 3, 4), + ) + + return HbVedtaksbrevFelles( + brevmetadata = lagMetadata(ytelsestype, språkkode), + fagsaksvedtaksdato = LocalDate.now(), + behandling = HbBehandling(), + hjemmel = HbHjemmel("Folketrygdloven"), + totaltFeilutbetaltBeløp = BigDecimal.valueOf(6855), + varsel = HbVarsel( + varsletBeløp = BigDecimal.valueOf(10000), + varsletDato = LocalDate.now().minusDays(100), + ), + konfigurasjon = HbKonfigurasjon( + fireRettsgebyr = BigDecimal.valueOf(4321), + klagefristIUker = 4, + ), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.FULL_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal.ZERO, + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal.ZERO, + totaltTilbakekrevesBeløpMedRenter = BigDecimal.ZERO, + totaltRentebeløp = BigDecimal.ZERO, + ), + søker = HbPerson(navn = "Søker Søkersen"), + datoer = datoer, + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + } + + private fun lagMetadata( + ytelsestype: Ytelsestype, + språkkode: Språkkode, + ): Brevmetadata { + return Brevmetadata( + sakspartId = "", + sakspartsnavn = "", + mottageradresse = Adresseinfo("01020312345", "Bob"), + behandlendeEnhetsNavn = "Oslo", + ansvarligSaksbehandler = "Bob", + saksnummer = "1232456", + språkkode = språkkode, + ytelsestype = ytelsestype, + gjelderDødsfall = false, + ) + } + + private fun overskrift( + resultat: Vilkårsvurderingsresultat, + vurdering: Vurdering?, + lavtBeløp: Boolean, + fritekst: Boolean, + pengerIBehold: Boolean, + foreldelsevurdering: Foreldelsesvurderingstype, + ): String { + return ( + "*[ ${hentVilkårresultatOverskriftDel(resultat)}" + + (if (vurdering != null) " - " + vurdering.navn else "") + + (if (fritekst) " - med fritekst" else " - uten fritekst") + + hentVIlkårsvurderingOverskriftDel(foreldelsevurdering) + + (if (pengerIBehold) " - penger i behold" else "") + + (if (lavtBeløp) " - lavt beløp" else "") + + " ]*" + ) + } + + private fun prettyprint(vilkårTekst: String, overskrift: String): String { + return vilkårTekst.replace("__.+".toRegex(), overskrift) + .replace(" 4\u00A0321\u00A0kroner", " <4 rettsgebyr> kroner") + .replace(" 2\u00A0999\u00A0kroner", " kroner") + .replace(" 3\u00A0999\u00A0kroner", " kroner") + .replace("1. januar 2019", "") + .replace("31. januar 2019", "") + .replace("1. mars 2019", "") + .replace("1. desember 2019", "") + } + + companion object { + + private val vilkårResultat = arrayOf( + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + Vilkårsvurderingsresultat.MANGELFULLE_OPPLYSNINGER_FRA_BRUKER, + Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + ) + private val foreldelseVurderinger = arrayOf( + Foreldelsesvurderingstype.IKKE_VURDERT, + Foreldelsesvurderingstype.IKKE_FORELDET, + Foreldelsesvurderingstype.TILLEGGSFRIST, + ) + private val aktsomheter = arrayOf( + Aktsomhet.SIMPEL_UAKTSOMHET, + Aktsomhet.GROV_UAKTSOMHET, + Aktsomhet.FORSETT, + ) + private val trueFalse = booleanArrayOf(true, false) + private val JANUAR = Datoperiode(YearMonth.of(2019, 1), YearMonth.of(2019, 1)) + private val FORELDELSESFRIST = LocalDate.of(2019, 12, 1) + private val OPPDAGELSES_DATO = LocalDate.of(2019, 3, 1) + } + + private fun hentVilkårresultatOverskriftDel(resultat: Vilkårsvurderingsresultat): String { + return when (resultat) { + Vilkårsvurderingsresultat.UDEFINERT -> "Foreldelse" + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT -> "Forsto/Burde forstått" + Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER -> "Feilaktive opplysninger" + Vilkårsvurderingsresultat.MANGELFULLE_OPPLYSNINGER_FRA_BRUKER -> "Mangelfull opplysninger" + Vilkårsvurderingsresultat.GOD_TRO -> "God tro" + else -> throw IllegalArgumentException("Vilkårsvurderingsresultat ikke støttet. Resultat: $resultat") + } + } + + private fun hentVIlkårsvurderingOverskriftDel(foreldelsevurdering: Foreldelsesvurderingstype): String { + return when (foreldelsevurdering) { + Foreldelsesvurderingstype.IKKE_VURDERT -> " - automatisk vurdert" + Foreldelsesvurderingstype.IKKE_FORELDET -> " - ikke foreldet" + Foreldelsesvurderingstype.FORELDET -> " - foreldet" + Foreldelsesvurderingstype.TILLEGGSFRIST -> " - med tilleggsfrist" + else -> throw IllegalArgumentException("Foreldelsesvurderingstype ikke støttet. Type: $foreldelsevurdering") + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorVedtaksoppsummering.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorVedtaksoppsummering.kt new file mode 100644 index 000000000..312083d63 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorVedtaksoppsummering.kt @@ -0,0 +1,339 @@ +package no.nav.familie.tilbake.dokumentasjonsgenerator + +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.handlebars.FellesTekstformaterer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbBehandling +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbHjemmel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbKonfigurasjon +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbPerson +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbTotalresultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVarsel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate + +/** + * Brukes for å generere tekster for oppsummering av vedtaket i vedtaksbrevet. Resultatet er tekster med markup, som med + * "Insert markup"-macroen kan limes inn i Confluence, og dermed bli formattert tekst. + * + * Confluence: + * https://confluence.adeo.no/display/TFA/Generert+dokumentasjon + */ +@Disabled("Kjøres ved behov for å regenerere dokumentasjon") +class DokumentasjonsgeneratorVedtaksoppsummering { + + companion object { + + private val JANUAR_15: LocalDate = LocalDate.of(2020, 1, 15) + private val FEBRUAR_15: LocalDate = LocalDate.of(2020, 2, 15) + private val tilbakekrevingsResultat = listOf( + Vedtaksresultat.FULL_TILBAKEBETALING, + Vedtaksresultat.DELVIS_TILBAKEBETALING, + Vedtaksresultat.INGEN_TILBAKEBETALING, + ) + private val trueFalse = booleanArrayOf(true, false) + + const val VEDTAK_START = "vedtak/vedtak_start" + } + + @Test + fun `list ut vedtak start for BA bokmål`() { + val ytelseType = Ytelsestype.BARNETRYGD + val nb: Språkkode = Språkkode.NB + for (resultatType in tilbakekrevingsResultat) { + for (medVarsel in trueFalse) { + listVedtakStartUtenRenterUtenSkatt(ytelseType, nb, resultatType, medVarsel) + } + listVedtakStartMedKorrigertBeløpUtenRenterUtenSkatt(ytelseType, nb, resultatType) + } + } + + @Test + fun `list ut vedtak start for BA nynorsk`() { + val ytelseType: Ytelsestype = Ytelsestype.BARNETRYGD + val språkkode: Språkkode = Språkkode.NN + for (resultatType in tilbakekrevingsResultat) { + for (medVarsel in trueFalse) { + listVedtakStartUtenRenterUtenSkatt(ytelseType, språkkode, resultatType, medVarsel) + } + listVedtakStartMedKorrigertBeløpUtenRenterUtenSkatt(ytelseType, språkkode, resultatType) + } + } + + @Test + fun `list ut vedtak start for EFBT bokmål`() { + val ytelseType: Ytelsestype = Ytelsestype.BARNETILSYN + val nb: Språkkode = Språkkode.NB + for (resultatType in tilbakekrevingsResultat) { + for (medVarsel in trueFalse) { + listVedtakStartUtenSkatt(ytelseType, nb, resultatType, medVarsel) + } + listVedtakStartMedKorrigertBeløpUtenSkatt(ytelseType, nb, resultatType) + } + } + + @Test + fun `list ut vedtak start for EFBT nynorsk`() { + val ytelseType: Ytelsestype = Ytelsestype.BARNETILSYN + val språkkode: Språkkode = Språkkode.NN + for (resultatType in tilbakekrevingsResultat) { + for (medVarsel in trueFalse) { + listVedtakStartUtenSkatt(ytelseType, språkkode, resultatType, medVarsel) + } + listVedtakStartMedKorrigertBeløpUtenSkatt(ytelseType, språkkode, resultatType) + } + } + + @Test + fun `list ut vedtak start for EFOG bokmål`() { + val ytelseType: Ytelsestype = Ytelsestype.OVERGANGSSTØNAD + val nb: Språkkode = Språkkode.NB + for (resultatType in tilbakekrevingsResultat) { + for (medVarsel in trueFalse) { + listVedtakStartAllePermutasjoner(ytelseType, nb, resultatType, medVarsel) + } + listVedtakStartMedKorrigertBeløpAllePermutasjoner(ytelseType, nb, resultatType) + } + } + + @Test + fun `list ut vedtak start for EFOG nynorsk`() { + val ytelseType: Ytelsestype = Ytelsestype.OVERGANGSSTØNAD + val språkkode: Språkkode = Språkkode.NN + for (resultatType in tilbakekrevingsResultat) { + for (medVarsel in trueFalse) { + listVedtakStartAllePermutasjoner(ytelseType, språkkode, resultatType, medVarsel) + } + listVedtakStartMedKorrigertBeløpAllePermutasjoner(ytelseType, språkkode, resultatType) + } + } + + @Test + fun `list ut vedtak start for EFSP bokmål`() { + val ytelseType: Ytelsestype = Ytelsestype.SKOLEPENGER + val nb: Språkkode = Språkkode.NB + for (resultatType in tilbakekrevingsResultat) { + for (medVarsel in trueFalse) { + listVedtakStartUtenSkatt(ytelseType, nb, resultatType, medVarsel) + } + listVedtakStartMedKorrigertBeløpUtenSkatt(ytelseType, nb, resultatType) + } + } + + @Test + fun `list ut vedtak start for EFSP nynorsk`() { + val ytelseType: Ytelsestype = Ytelsestype.SKOLEPENGER + val språkkode: Språkkode = Språkkode.NN + for (resultatType in tilbakekrevingsResultat) { + for (medVarsel in trueFalse) { + listVedtakStartUtenSkatt(ytelseType, språkkode, resultatType, medVarsel) + } + listVedtakStartMedKorrigertBeløpUtenSkatt(ytelseType, språkkode, resultatType) + } + } + + private fun listVedtakStartAllePermutasjoner( + ytelseType: Ytelsestype, + nb: Språkkode, + resultatType: Vedtaksresultat, + medVarsel: Boolean, + ) { + genererVedtakStart(ytelseType, nb, resultatType, medVarsel, 10, 100) + genererVedtakStart(ytelseType, nb, resultatType, medVarsel, 0, 100) + genererVedtakStart(ytelseType, nb, resultatType, medVarsel, 10, 0) + genererVedtakStart(ytelseType, nb, resultatType, medVarsel, 0, 0) + } + + private fun listVedtakStartUtenRenterUtenSkatt( + ytelseType: Ytelsestype, + nb: Språkkode, + resultatType: Vedtaksresultat, + medVarsel: Boolean, + ) { + genererVedtakStart(ytelseType, nb, resultatType, medVarsel, 0, 0) + } + + private fun listVedtakStartUtenSkatt( + ytelseType: Ytelsestype, + nb: Språkkode, + resultatType: Vedtaksresultat, + medVarsel: Boolean, + ) { + genererVedtakStart(ytelseType, nb, resultatType, medVarsel, 10, 0) + genererVedtakStart(ytelseType, nb, resultatType, medVarsel, 0, 0) + } + + private fun listVedtakStartMedKorrigertBeløpAllePermutasjoner( + ytelseType: Ytelsestype, + nb: Språkkode, + resultatType: Vedtaksresultat, + ) { + genererVedtakStartMedKorrigertBeløp(ytelseType, nb, resultatType, 10, 100) + genererVedtakStartMedKorrigertBeløp(ytelseType, nb, resultatType, 0, 100) + genererVedtakStartMedKorrigertBeløp(ytelseType, nb, resultatType, 10, 0) + genererVedtakStartMedKorrigertBeløp(ytelseType, nb, resultatType, 0, 0) + } + + private fun listVedtakStartMedKorrigertBeløpUtenRenterUtenSkatt( + ytelseType: Ytelsestype, + nb: Språkkode, + resultatType: Vedtaksresultat, + ) { + genererVedtakStartMedKorrigertBeløp(ytelseType, nb, resultatType, 0, 0) + } + + private fun listVedtakStartMedKorrigertBeløpUtenSkatt( + ytelseType: Ytelsestype, + nb: Språkkode, + resultatType: Vedtaksresultat, + ) { + genererVedtakStartMedKorrigertBeløp(ytelseType, nb, resultatType, 10, 0) + genererVedtakStartMedKorrigertBeløp(ytelseType, nb, resultatType, 0, 0) + } + + private fun genererVedtakStart( + ytelseType: Ytelsestype, + språkkode: Språkkode, + tilbakebetaling: Vedtaksresultat, + medVarsel: Boolean, + renter: Long, + skatt: Long, + ) { + genererVedtakStart(ytelseType, språkkode, tilbakebetaling, medVarsel, renter, skatt, false, false, false) + genererVedtakStart(ytelseType, språkkode, tilbakebetaling, medVarsel, renter, skatt, false, true, false) + genererVedtakStart(ytelseType, språkkode, tilbakebetaling, medVarsel, renter, skatt, false, true, true) + } + + private fun genererVedtakStartMedKorrigertBeløp( + ytelseType: Ytelsestype, + språkkode: Språkkode, + tilbakebetaling: Vedtaksresultat, + renter: Long, + skatt: Long, + ) { + genererVedtakStart(ytelseType, språkkode, tilbakebetaling, true, renter, skatt, true, false, false) + genererVedtakStart(ytelseType, språkkode, tilbakebetaling, true, renter, skatt, true, true, false) + genererVedtakStart(ytelseType, språkkode, tilbakebetaling, true, renter, skatt, true, true, true) + } + + private fun genererVedtakStart( + ytelseType: Ytelsestype, + språkkode: Språkkode, + tilbakebetaling: Vedtaksresultat, + medVarsel: Boolean, + renter: Long, + skatt: Long, + medKorrigertBeløp: Boolean, + erRevurdering: Boolean, + erRevurderingEtterKlageNfp: Boolean, + ) { + val totalt = 1000L + val totaltMedRenter = totalt + renter + val resultat = HbTotalresultat( + hovedresultat = tilbakebetaling, + totaltTilbakekrevesBeløp = BigDecimal.valueOf(totalt), + totaltTilbakekrevesBeløpMedRenter = BigDecimal.valueOf(totaltMedRenter), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal.valueOf(totaltMedRenter - skatt), + totaltRentebeløp = BigDecimal.valueOf(renter), + ) + val varsel = if (medKorrigertBeløp) { + HbVarsel( + varsletBeløp = BigDecimal.valueOf(25000L), + varsletDato = JANUAR_15, + ) + } else if (medVarsel) { + HbVarsel( + varsletBeløp = BigDecimal.valueOf(1000L), + varsletDato = JANUAR_15, + ) + } else { + null + } + + val behandling = HbBehandling( + erRevurdering = erRevurdering, + erRevurderingEtterKlageNfp = erRevurderingEtterKlageNfp, + originalBehandlingsdatoFagsakvedtak = if (erRevurdering) FEBRUAR_15 else null, + ) + + val felles = HbVedtaksbrevFelles( + brevmetadata = lagMetadata(ytelseType, språkkode), + totalresultat = resultat, + søker = HbPerson(navn = ""), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 6), + hjemmel = HbHjemmel(""), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + fagsaksvedtaksdato = JANUAR_15, + varsel = varsel, + erFeilutbetaltBeløpKorrigertNed = medKorrigertBeløp, + totaltFeilutbetaltBeløp = BigDecimal.valueOf(1000), + behandling = behandling, + ) + val vedtakStart: String = FellesTekstformaterer.lagDeltekst(felles, VEDTAK_START) + prettyPrint( + tilbakebetaling, + medVarsel, + renter, + skatt, + vedtakStart, + medKorrigertBeløp, + erRevurdering, + erRevurderingEtterKlageNfp, + ) + } + + private fun lagMetadata( + ytelsestype: Ytelsestype, + språkkode: Språkkode, + ): Brevmetadata { + return Brevmetadata( + sakspartId = "", + sakspartsnavn = "", + mottageradresse = Adresseinfo("01020312345", "Bob"), + behandlendeEnhetsNavn = "Oslo", + ansvarligSaksbehandler = "Bob", + saksnummer = "1232456", + språkkode = språkkode, + ytelsestype = ytelsestype, + gjelderDødsfall = false, + ) + } + + private fun prettyPrint( + tilbakebetaling: Vedtaksresultat, + medVarsel: Boolean, + renter: Long, + skatt: Long, + generertTekst: String, + medKorrigertBeløp: Boolean, + erRevurdering: Boolean, + erRevurderingEtterKlageNfp: Boolean, + ) { + println( + ("*[ " + tilbakebetaling.navn) + " - " + + (if (medVarsel) "med varsel" else "uten varsel") + " - " + + (if (skatt != 0L) "med skatt" else "uten skatt") + " - " + + (if (renter != 0L) "med renter" else "uten renter") + + (if (medKorrigertBeløp) " - med korrigert beløp" else "") + + (if (erRevurdering) " - revurdering " else "") + + (if (erRevurderingEtterKlageNfp) " klage nfp" else "") + " ]*", + ) + val parametrisertTekst = generertTekst + .replace(" 1\u00A0010\u00A0kroner".toRegex(), " kroner") + .replace(" 1\u00A0000\u00A0kroner".toRegex(), " kroner") + .replace(" 910\u00A0kroner".toRegex(), " kroner") + .replace(" 900\u00A0kroner".toRegex(), " kroner") + .replace(" 25\u00A0000\u00A0kroner".toRegex(), " kroner") + .replace("15. januar 2020", if (medVarsel) "" else "") + .replace("15. februar 2020", "") + println(parametrisertTekst) + println() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorVedtaksslutt.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorVedtaksslutt.kt new file mode 100644 index 000000000..a772b1740 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentasjonsgenerator/DokumentasjonsgeneratorVedtaksslutt.kt @@ -0,0 +1,301 @@ +package no.nav.familie.tilbake.dokumentasjonsgenerator + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.handlebars.FellesTekstformaterer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbBehandling +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbHjemmel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbKonfigurasjon +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbPerson +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbTotalresultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVarsel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevsdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbFakta +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbKravgrunnlag +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbResultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbSærligeGrunner +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVedtaksbrevsperiode +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVurderinger +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth + +/** + * Brukes for å generere tekster for slutten av vedtaksbrevet. Resultatet er tekster med markup, som med "Insert markup"-macroen + * kan limes inn i Confluence, og dermed bli formattert tekst. + * + * Confluence: + * https://confluence.adeo.no/display/TFA/Generert+dokumentasjon + */ +// @Disabled("Kjøres ved behov for å regenerere dokumentasjon") +class DokumentasjonsgeneratorVedtaksslutt { + + @Test + fun `list ut vedtak slutt EFOG bokmål`() { + lagVedtakSluttTekster(Ytelsestype.OVERGANGSSTØNAD, Språkkode.NB, Vedtaksresultat.FULL_TILBAKEBETALING) + lagVedtakSluttTekster(Ytelsestype.OVERGANGSSTØNAD, Språkkode.NB, Vedtaksresultat.INGEN_TILBAKEBETALING) + } + + @Test + fun `list ut vedtak slutt EFOG nynorsk`() { + lagVedtakSluttTekster(Ytelsestype.OVERGANGSSTØNAD, Språkkode.NN, Vedtaksresultat.FULL_TILBAKEBETALING) + lagVedtakSluttTekster(Ytelsestype.OVERGANGSSTØNAD, Språkkode.NN, Vedtaksresultat.INGEN_TILBAKEBETALING) + } + + @Test + fun `list ut vedtak slutt EFBT bokmål`() { + lagVedtakSluttTekster(Ytelsestype.BARNETILSYN, Språkkode.NB, Vedtaksresultat.FULL_TILBAKEBETALING, false) + lagVedtakSluttTekster(Ytelsestype.BARNETILSYN, Språkkode.NB, Vedtaksresultat.INGEN_TILBAKEBETALING, false) + } + + @Test + fun `list ut vedtak slutt EFBT nynorsk`() { + lagVedtakSluttTekster(Ytelsestype.BARNETILSYN, Språkkode.NN, Vedtaksresultat.FULL_TILBAKEBETALING, false) + lagVedtakSluttTekster(Ytelsestype.BARNETILSYN, Språkkode.NN, Vedtaksresultat.INGEN_TILBAKEBETALING, false) + } + + @Test + fun `list ut vedtak slutt BA bokmål`() { + lagVedtakSluttTekster(Ytelsestype.BARNETRYGD, Språkkode.NB, Vedtaksresultat.FULL_TILBAKEBETALING, false) + lagVedtakSluttTekster(Ytelsestype.BARNETRYGD, Språkkode.NB, Vedtaksresultat.INGEN_TILBAKEBETALING, false) + } + + @Test + fun `list ut vedtak slutt BA nynorsk`() { + lagVedtakSluttTekster(Ytelsestype.BARNETRYGD, Språkkode.NN, Vedtaksresultat.FULL_TILBAKEBETALING, false) + lagVedtakSluttTekster(Ytelsestype.BARNETRYGD, Språkkode.NN, Vedtaksresultat.INGEN_TILBAKEBETALING, false) + } + + @Test + fun `list ut vedtak slutt EFSP bokmål`() { + lagVedtakSluttTekster(Ytelsestype.SKOLEPENGER, Språkkode.NB, Vedtaksresultat.FULL_TILBAKEBETALING, false) + lagVedtakSluttTekster(Ytelsestype.SKOLEPENGER, Språkkode.NB, Vedtaksresultat.INGEN_TILBAKEBETALING, false) + } + + @Test + fun `list ut vedtak slutt EFSP nynorsk`() { + lagVedtakSluttTekster(Ytelsestype.SKOLEPENGER, Språkkode.NN, Vedtaksresultat.FULL_TILBAKEBETALING, false) + lagVedtakSluttTekster(Ytelsestype.SKOLEPENGER, Språkkode.NN, Vedtaksresultat.INGEN_TILBAKEBETALING, false) + } + + private fun lagVedtakSluttTekster(ytelsetype: Ytelsestype, språkkode: Språkkode, resultatType: Vedtaksresultat) { + for (medSkattetrekk in trueFalse) { + lagVedtakSluttTekster(ytelsetype, språkkode, resultatType, medSkattetrekk) + } + } + + private fun lagVedtakSluttTekster( + ytelsetype: Ytelsestype, + språkkode: Språkkode, + resultatType: Vedtaksresultat, + medSkattetrekk: Boolean, + ) { + trueFalse.forEach { flerePerioder -> + trueFalse.forEach { flereLovhjemler -> + trueFalse.forEach { medVerge -> + trueFalse.forEach { feilutbetaltBeløpBortfalt -> + lagVedtakSluttTekster( + ytelsetype, + språkkode, + resultatType, + flerePerioder, + medSkattetrekk, + flereLovhjemler, + medVerge, + feilutbetaltBeløpBortfalt, + false, + ) + if (Vedtaksresultat.INGEN_TILBAKEBETALING != resultatType) { + lagVedtakSluttTekster( + ytelsetype, + språkkode, + resultatType, + flerePerioder, + medSkattetrekk, + flereLovhjemler, + medVerge, + feilutbetaltBeløpBortfalt, + true, + ) + } + } + } + } + } + } + + private fun lagVedtakSluttTekster( + ytelsetype: Ytelsestype, + språkkode: Språkkode, + resultatType: Vedtaksresultat, + flerePerioder: Boolean, + medSkattetrekk: Boolean, + flereLovhjemler: Boolean, + medVerge: Boolean, + feilutbetaltBeløpBortfalt: Boolean, + erRevurdering: Boolean, + ) { + val felles: HbVedtaksbrevFelles = lagFellesdel( + ytelsetype, + språkkode, + resultatType, + medSkattetrekk, + flereLovhjemler, + medVerge, + feilutbetaltBeløpBortfalt, + erRevurdering, + ) + val perioder: List = lagPerioder(flerePerioder) + val sluttTekst: String = FellesTekstformaterer.lagDeltekst(HbVedtaksbrevsdata(felles, perioder), VEDTAK_SLUTT) + println() + println(overskrift(flerePerioder, medSkattetrekk, flereLovhjemler, medVerge, feilutbetaltBeløpBortfalt, erRevurdering)) + println(prettyprint(sluttTekst)) + } + + private fun lagFellesdel( + ytelsetype: Ytelsestype, + språkkode: Språkkode, + vedtakResultatType: Vedtaksresultat, + medSkattetrekk: Boolean, + flereLovhjemler: Boolean, + medVerge: Boolean, + feilutbetaltBeløpBortfalt: Boolean, + erRevurdering: Boolean, + ): HbVedtaksbrevFelles { + return HbVedtaksbrevFelles( + brevmetadata = lagMetadata(ytelsetype, språkkode, medVerge), + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = + HbTotalresultat( + hovedresultat = vedtakResultatType, + totaltTilbakekrevesBeløp = BigDecimal.valueOf(1000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal.valueOf(1100), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = + BigDecimal.valueOf(if (medSkattetrekk) 900 else 1100.toLong()), + totaltRentebeløp = BigDecimal.valueOf(100), + ), + totaltFeilutbetaltBeløp = BigDecimal.valueOf(1000), + hjemmel = if (flereLovhjemler) { + HbHjemmel("", true) + } else { + HbHjemmel("") + }, + varsel = HbVarsel( + varsletBeløp = BigDecimal.valueOf(1000), + varsletDato = LocalDate.of(2020, 4, 4), + ), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 4), + ansvarligBeslutter = "", + søker = HbPerson(navn = ""), + vedtaksbrevstype = if (feilutbetaltBeløpBortfalt) { + Vedtaksbrevstype.FRITEKST_FEILUTBETALING_BORTFALT + } else { + Vedtaksbrevstype.ORDINÆR + }, + behandling = HbBehandling( + erRevurdering = erRevurdering, + originalBehandlingsdatoFagsakvedtak = if (erRevurdering) { + PERIODE1.fom + } else { + null + }, + ), + ) + } + + private fun lagMetadata( + ytelsestype: Ytelsestype, + språkkode: Språkkode, + medVerge: Boolean, + ): Brevmetadata { + val annenMottagersNavn = if (medVerge) "" else null + return Brevmetadata( + sakspartId = "", + sakspartsnavn = "", + finnesVerge = medVerge, + vergenavn = "", + mottageradresse = Adresseinfo("01020312345", "", annenMottagersNavn), + behandlendeEnhetsNavn = "", + ansvarligSaksbehandler = "", + saksnummer = "1232456", + språkkode = språkkode, + ytelsestype = ytelsestype, + gjelderDødsfall = false, + ) + } + + private fun lagPerioder(flerePerioder: Boolean): List { + if (flerePerioder) { + return listOf(lagPeriode(PERIODE1), lagPeriode(PERIODE2)) + } + return listOf(lagPeriode(PERIODE1)) + } + + private fun lagPeriode(periode: Datoperiode): HbVedtaksbrevsperiode { + return HbVedtaksbrevsperiode( + periode = periode, + kravgrunnlag = HbKravgrunnlag(feilutbetaltBeløp = BigDecimal.valueOf(1000)), + fakta = HbFakta(Hendelsestype.ANNET, Hendelsesundertype.ANNET_FRITEKST), + vurderinger = HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + aktsomhetsresultat = Aktsomhet.SIMPEL_UAKTSOMHET, + særligeGrunner = HbSærligeGrunner(emptyList(), null, null), + ), + resultat = HbResultat( + tilbakekrevesBeløp = BigDecimal.valueOf(1000), + tilbakekrevesBeløpUtenSkattMedRenter = BigDecimal.valueOf(800), + rentebeløp = BigDecimal.ZERO, + ), + førstePeriode = true, + ) + } + + private fun overskrift( + flerePerioder: Boolean, + medSkattetrekk: Boolean, + flereLovhjemler: Boolean, + medVerge: Boolean, + feilutbetaltBeløpBortfalt: Boolean, + erRevurdering: Boolean, + ): String { + return ( + "*[ " + (if (flerePerioder) "flere perioder" else "en periode") + + " - " + (if (medSkattetrekk) "med skattetrekk" else "uten skattetrekk") + + " - " + (if (flereLovhjemler) "flere lovhjemmel" else "en lovhjemmel") + + " - " + (if (medVerge) "med verge" else "uten verge") + + " - " + (if (feilutbetaltBeløpBortfalt) "feilutbetalt beløp bortfalt" else "ordinær") + + (if (erRevurdering) " - revurdering" else "") + + " ]*" + ) + } + + private fun prettyprint(s: String): String { + return s.replace("{venstrejustert}", "") + .replace("{høyrejustert}", "\t\t") + .replace("4 uker", " uker") + .replace("4 veker", " veker") + .replace("(_.+)".toRegex(), "\n*$1*") + } + + companion object { + + private val PERIODE1 = Datoperiode(YearMonth.of(2019, 1), YearMonth.of(2019, 1)) + private val PERIODE2 = Datoperiode(YearMonth.of(2019, 1), YearMonth.of(2019, 1)) + private val trueFalse = booleanArrayOf(true, false) + const val VEDTAK_SLUTT = "vedtak/vedtak_slutt" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/Distribusjonsh\303\245ndteringServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/Distribusjonsh\303\245ndteringServiceTest.kt" new file mode 100644 index 000000000..a59648f47 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/Distribusjonsh\303\245ndteringServiceTest.kt" @@ -0,0 +1,216 @@ +package no.nav.familie.tilbake.dokumentbestilling + +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.equals.shouldNotBeEqual +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.kontrakter.felles.tilbakekreving.MottakerType +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmetadataUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager.BRUKER +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager.MANUELL_BRUKER +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager.MANUELL_TILLEGGSMOTTAKER +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager.VERGE +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingService +import no.nav.familie.tilbake.dokumentbestilling.felles.EksterneDataForBrevService +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.Brevdata +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.JournalføringService +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.PdfBrevService +import no.nav.familie.tilbake.dokumentbestilling.henleggelse.HenleggelsesbrevService +import no.nav.familie.tilbake.dokumentbestilling.henleggelse.SendHenleggelsesbrevTask +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.ManuellBrevmottakerRepository +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.domene.ManuellBrevmottaker +import no.nav.familie.tilbake.dokumentbestilling.vedtak.VedtaksbrevgunnlagService +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.util.Optional +import java.util.UUID + +class DistribusjonshåndteringServiceTest { + + private val behandlingRepository: BehandlingRepository = mockk() + private val fagsakRepository: FagsakRepository = mockk() + private val manuelleBrevmottakerRepository: ManuellBrevmottakerRepository = mockk(relaxed = true) + private val journalføringService: JournalføringService = mockk(relaxed = true) + private val featureToggleService: FeatureToggleService = mockk() + private val eksterneDataForBrevService: EksterneDataForBrevService = mockk() + private val vedtaksbrevgrunnlagService: VedtaksbrevgunnlagService = mockk() + + private val pdfBrevService = spyk( + PdfBrevService( + journalføringService = journalføringService, + tellerService = mockk(relaxed = true), + taskService = mockk(relaxed = true), + ), + ) + private val brevmetadataUtil = BrevmetadataUtil( + behandlingRepository = behandlingRepository, + fagsakRepository = fagsakRepository, + manuelleBrevmottakerRepository = manuelleBrevmottakerRepository, + eksterneDataForBrevService = eksterneDataForBrevService, + organisasjonService = mockk(), + featureToggleService = featureToggleService, + ) + private val distribusjonshåndteringService = DistribusjonshåndteringService( + brevmetadataUtil = brevmetadataUtil, + fagsakRepository = fagsakRepository, + manuelleBrevmottakerRepository = manuelleBrevmottakerRepository, + pdfBrevService = pdfBrevService, + vedtaksbrevgrunnlagService = vedtaksbrevgrunnlagService, + featureToggleService = featureToggleService, + ) + private val brevsporingService: BrevsporingService = mockk() + private val henleggelsesbrevService = HenleggelsesbrevService( + behandlingRepository = behandlingRepository, + brevsporingService = brevsporingService, + fagsakRepository = fagsakRepository, + eksterneDataForBrevService = eksterneDataForBrevService, + pdfBrevService = pdfBrevService, + organisasjonService = mockk(), + distribusjonshåndteringService = distribusjonshåndteringService, + brevmetadataUtil = brevmetadataUtil, + ) + private val sendHenleggelsesbrevTask = SendHenleggelsesbrevTask( + henleggelsesbrevService = henleggelsesbrevService, + behandlingRepository = behandlingRepository, + fagsakRepository = fagsakRepository, + featureToggleService = featureToggleService, + ) + + private val behandling = Testdata.behandling + private val fagsak = Testdata.fagsak + private val personinfoBruker = Personinfo(fagsak.bruker.ident, LocalDate.now(), navn = "brukernavn") + private val brukerAdresse = Adresseinfo(personinfoBruker.ident, personinfoBruker.navn) + private val verge = behandling.aktivVerge!! + private val vergeAdresse = Adresseinfo(verge.ident!!, verge.navn) + + @BeforeEach + fun setUp() { + every { behandlingRepository.findById(any()) } returns Optional.of(behandling) + every { fagsakRepository.findById(any()) } returns Optional.of(fagsak) + every { eksterneDataForBrevService.hentPerson(fagsak.bruker.ident, fagsak.fagsystem) } returns + personinfoBruker + every { + eksterneDataForBrevService.hentAdresse(personinfoBruker, BRUKER, behandling.aktivVerge, any()) + } returns brukerAdresse + every { + eksterneDataForBrevService.hentAdresse(personinfoBruker, VERGE, behandling.aktivVerge, any()) + } returns vergeAdresse + every { eksterneDataForBrevService.hentSaksbehandlernavn(any()) } returns behandling.ansvarligSaksbehandler + every { eksterneDataForBrevService.hentPåloggetSaksbehandlernavnMedDefault(any()) } returns behandling.ansvarligSaksbehandler + every { brevsporingService.finnSisteVarsel(any()) } returns Testdata.brevsporing + every { featureToggleService.isEnabled(any()) } returns false + } + + @Test + fun `skal kun sende til bruker`() { + val behandlingUtenVerge = behandling.copy(verger = emptySet()) + + every { behandlingRepository.findById(any()) } returns Optional.of(behandlingUtenVerge) + every { + eksterneDataForBrevService.hentAdresse(personinfoBruker, BRUKER, behandlingUtenVerge.aktivVerge, any()) + } returns brukerAdresse + + val task = SendHenleggelsesbrevTask.opprettTask(behandling.id, fagsak.fagsystem, "fritekst") + val brevdata = mutableListOf() + + sendHenleggelsesbrevTask.doTask(task) + + verify { + pdfBrevService.sendBrev( + behandlingUtenVerge, + fagsak, + Brevtype.HENLEGGELSE, + capture(brevdata), + ) + } + + val metadata = brevdata.single().metadata + + metadata.mottageradresse shouldBeEqual brukerAdresse + + metadata.finnesVerge.shouldBeFalse() + metadata.finnesAnnenMottaker.shouldBeFalse() + + metadata.annenMottakersNavn.isNullOrEmpty().shouldBeTrue() + metadata.vergenavn.isNullOrEmpty().shouldBeTrue() + } + + @Test + fun `skal journalføre og sende brev med samme brødtekst til både manuell bruker og manuell tilleggsmottaker`() { + val behandlingId = UUID.randomUUID() + val behandlingMedManuelleBrevmottakere = behandling.copy(id = behandlingId, verger = emptySet()) + + every { behandlingRepository.findById(behandlingId) } returns Optional.of(behandlingMedManuelleBrevmottakere) + every { manuelleBrevmottakerRepository.findByBehandlingId(behandlingId) } returns listOf( + ManuellBrevmottaker( + type = MottakerType.BRUKER_MED_UTENLANDSK_ADRESSE, + behandlingId = behandlingId, + navn = personinfoBruker.navn, + adresselinje1 = "adresselinje1", + postnummer = "postnummer", + poststed = "poststed", + landkode = "NO", + ), + ManuellBrevmottaker( + type = MottakerType.VERGE, + behandlingId = behandlingId, + navn = verge.navn, + ident = verge.ident, + ), + ) + + val task = SendHenleggelsesbrevTask.opprettTask(behandlingId, fagsak.fagsystem, "fritekst") + val brevdata = mutableListOf() + val eksternReferanseIdVedJournalføring = mutableListOf() + + sendHenleggelsesbrevTask.doTask(task) + + verify(exactly = 2) { + pdfBrevService.sendBrev( + behandling = behandlingMedManuelleBrevmottakere, + fagsak = fagsak, + brevtype = Brevtype.HENLEGGELSE, + data = capture(brevdata), + ) + journalføringService.journalførUtgåendeBrev( + behandling = behandlingMedManuelleBrevmottakere, + fagsak = fagsak, + dokumentkategori = any(), + brevmetadata = any(), + brevmottager = any(), + vedleggPdf = any(), + eksternReferanseId = capture(eksternReferanseIdVedJournalføring), + ) + } + + eksternReferanseIdVedJournalføring.first() shouldNotBeEqual eksternReferanseIdVedJournalføring.last() + eksternReferanseIdVedJournalføring.all { + it.contains("manuell_bruker") || it.contains("manuell_tilleggsmottaker") + } + + val brevdataTilBruker = brevdata.first { it.mottager == MANUELL_BRUKER } + val brevdataTilManuellVerge = brevdata.first { it.mottager == MANUELL_TILLEGGSMOTTAKER } + + val (brødtekstTilManuellVerge, annenMottakerOppgittTilVerge) = brevdataTilManuellVerge.brevtekst + .split("Brev med likt innhold er sendt til ") + val (brødtekstTilBruker, annenMottakerOppgittTilBruker) = brevdataTilBruker.brevtekst + .split("Brev med likt innhold er sendt til ") + + brødtekstTilManuellVerge shouldBeEqual brødtekstTilBruker + + annenMottakerOppgittTilBruker shouldBeEqual verge.navn + annenMottakerOppgittTilVerge shouldBeEqual personinfoBruker.navn + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/DokumentBehandlingServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/DokumentBehandlingServiceTest.kt new file mode 100644 index 000000000..365e0c761 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/DokumentBehandlingServiceTest.kt @@ -0,0 +1,172 @@ +package no.nav.familie.tilbake.dokumentbestilling + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.brevmaler.Dokumentmalstype +import no.nav.familie.tilbake.dokumentbestilling.innhentdokumentasjon.InnhentDokumentasjonbrevService +import no.nav.familie.tilbake.dokumentbestilling.innhentdokumentasjon.InnhentDokumentasjonbrevTask +import no.nav.familie.tilbake.dokumentbestilling.varsel.manuelt.ManueltVarselbrevService +import no.nav.familie.tilbake.dokumentbestilling.varsel.manuelt.SendManueltVarselbrevTask +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.Fagområdekode +import no.nav.familie.tilbake.kravgrunnlag.domain.GjelderType +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassekode +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsperiode432 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.domain.Pageable +import java.math.BigDecimal +import java.math.BigInteger +import java.time.LocalDate +import java.util.UUID + +class DokumentBehandlingServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var taskService: TaskService + + private lateinit var fagsak: Fagsak + + private lateinit var behandling: Behandling + + private val mockManueltVarselBrevService: ManueltVarselbrevService = mockk() + private val mockInnhentDokumentasjonbrevService: InnhentDokumentasjonbrevService = mockk() + + private lateinit var dokumentBehandlingService: DokumentbehandlingService + + @BeforeEach + fun init() { + fagsak = fagsakRepository.insert(Testdata.fagsak) + behandling = behandlingRepository.insert(Testdata.behandling) + behandlingsstegstilstandRepository.insert(Testdata.behandlingsstegstilstand) + dokumentBehandlingService = DokumentbehandlingService( + behandlingRepository, + fagsakRepository, + behandlingskontrollService, + kravgrunnlagRepository, + taskService, + mockManueltVarselBrevService, + mockInnhentDokumentasjonbrevService, + ) + } + + @Test + fun `bestillBrev skal kunne bestille varselbrev når grunnlag finnes`() { + val behandlingId = opprettOgLagreKravgrunnlagPåBehandling() + + dokumentBehandlingService.bestillBrev(behandlingId, Dokumentmalstype.VARSEL, "Bestilt varselbrev") + + val tasks = taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET), Pageable.unpaged()) + tasks.first().type shouldBe SendManueltVarselbrevTask.TYPE + } + + @Test + fun `bestillBrev skal ikke kunne bestille varselbrev når grunnlag ikke finnes`() { + shouldThrow { + dokumentBehandlingService.bestillBrev(behandling.id, Dokumentmalstype.VARSEL, "Bestilt varselbrev") + }.message shouldBe "Kan ikke sende varselbrev fordi grunnlag finnes ikke for behandlingId = ${behandling.id}" + } + + @Test + fun `bestillBrev skal kunne bestille innhent dokumentasjon brev når grunnlag finnes`() { + val behandlingId = opprettOgLagreKravgrunnlagPåBehandling() + + dokumentBehandlingService.bestillBrev( + behandlingId, + Dokumentmalstype.INNHENT_DOKUMENTASJON, + "Bestilt innhent dokumentasjon", + ) + + val tasks = taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET), Pageable.unpaged()) + tasks.first().type shouldBe InnhentDokumentasjonbrevTask.TYPE + } + + @Test + fun `bestillBrev skal ikke kunne bestille innhent dokumentasjonbrev når grunnlag ikke finnes`() { + shouldThrow { + dokumentBehandlingService.bestillBrev( + behandling.id, + Dokumentmalstype.INNHENT_DOKUMENTASJON, + "Bestilt innhent dokumentasjon", + ) + }.message shouldBe "Kan ikke sende innhent dokumentasjonsbrev fordi grunnlag finnes ikke for behandlingId = " + + "${behandling.id}" + } + + private fun opprettOgLagreKravgrunnlagPåBehandling(): UUID { + val ytelBeløp = Kravgrunnlagsbeløp433( + klassetype = Klassetype.YTEL, + klassekode = Klassekode.BATR, + nyttBeløp = BigDecimal.ZERO, + tilbakekrevesBeløp = BigDecimal.valueOf(1000), + opprinneligUtbetalingsbeløp = BigDecimal.valueOf(1000), + skatteprosent = BigDecimal.ZERO, + ) + val feilBeløp = Kravgrunnlagsbeløp433( + klassetype = Klassetype.FEIL, + klassekode = Klassekode.BATR, + nyttBeløp = BigDecimal.valueOf(1000), + tilbakekrevesBeløp = BigDecimal.ZERO, + opprinneligUtbetalingsbeløp = BigDecimal.ZERO, + skatteprosent = BigDecimal.ZERO, + ) + val periode = Kravgrunnlagsperiode432( + periode = Månedsperiode(LocalDate.of(2019, 5, 1), LocalDate.of(2019, 5, 31)), + månedligSkattebeløp = BigDecimal.ZERO, + beløp = setOf(ytelBeløp, feilBeløp), + ) + val kravgrunnlag431 = Kravgrunnlag431( + behandlingId = behandling.id, + fagområdekode = Fagområdekode.BA, + vedtakId = BigInteger.valueOf(12342L), + eksternKravgrunnlagId = BigInteger.valueOf(1234), + kravstatuskode = Kravstatuskode.NYTT, + fagsystemId = "1234", + utbetalesTilId = "11323432111", + utbetIdType = GjelderType.PERSON, + gjelderVedtakId = "11323432111", + gjelderType = GjelderType.PERSON, + ansvarligEnhet = "enhet", + bostedsenhet = "enhet", + behandlingsenhet = "enhet", + kontrollfelt = "132323", + saksbehandlerId = "23454334", + referanse = "testverdi", + perioder = setOf(periode), + ) + kravgrunnlagRepository.insert(kravgrunnlag431) + return kravgrunnlag431.behandlingId + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevsporingRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevsporingRepositoryTest.kt new file mode 100644 index 000000000..62930ce10 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/BrevsporingRepositoryTest.kt @@ -0,0 +1,80 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.Sporbar +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevsporing +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDateTime +import java.util.UUID + +internal class BrevsporingRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var brevsporingRepository: BrevsporingRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + private val brevsporing = Testdata.brevsporing + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(Testdata.behandling) + } + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av Brevsporing til basen`() { + brevsporingRepository.insert(brevsporing) + + val lagretBrevsporing = brevsporingRepository.findByIdOrThrow(brevsporing.id) + + lagretBrevsporing.shouldBeEqualToComparingFieldsExcept(brevsporing, Brevsporing::sporbar, Brevsporing::versjon) + lagretBrevsporing.versjon shouldBe 1 + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av Brevsporing i basen`() { + brevsporingRepository.insert(brevsporing) + var lagretBrevsporing = brevsporingRepository.findByIdOrThrow(brevsporing.id) + val oppdatertBrevsporing = lagretBrevsporing.copy(brevtype = Brevtype.HENLEGGELSE) + + brevsporingRepository.update(oppdatertBrevsporing) + + lagretBrevsporing = brevsporingRepository.findByIdOrThrow(brevsporing.id) + lagretBrevsporing.shouldBeEqualToComparingFieldsExcept(oppdatertBrevsporing, Brevsporing::sporbar, Brevsporing::versjon) + lagretBrevsporing.versjon shouldBe 2 + } + + @Test + fun `findFirstByBehandlingIdAndBrevtypeOrderBySporbarOpprettetTidDesc rerturnerer siste brevsporing`() { + brevsporingRepository.insert(brevsporing) + val nyesteBrevsporing = brevsporingRepository + .insert( + brevsporing.copy( + id = UUID.randomUUID(), + sporbar = Sporbar(opprettetTid = LocalDateTime.now().plusSeconds(1)), + ), + ) + + val funnetBrevsporing = + brevsporingRepository.findFirstByBehandlingIdAndBrevtypeOrderBySporbarOpprettetTidDesc( + Testdata.behandling.id, + Brevtype.VARSEL, + ) + + funnetBrevsporing?.shouldBeEqualToComparingFieldsExcept(nyesteBrevsporing, Brevsporing::sporbar, Brevsporing::versjon) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/TekstformatererHeaderTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/TekstformatererHeaderTest.kt new file mode 100644 index 000000000..409d55237 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/header/TekstformatererHeaderTest.kt @@ -0,0 +1,69 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.header + +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +class TekstformatererHeaderTest { + + private val brevmetadata = Brevmetadata( + sakspartId = "12345678901", + sakspartsnavn = "Test", + vergenavn = "John Doe", + mottageradresse = Adresseinfo("12345678901", "Test"), + behandlendeEnhetsNavn = "NAV Familie- og pensjonsytelser Skien", + ansvarligSaksbehandler = "Bob", + saksnummer = "1232456", + språkkode = Språkkode.NB, + ytelsestype = Ytelsestype.BARNETILSYN, + gjelderDødsfall = false, + ) + + @Test + fun `lagHeader brev til bruker`() { + val generertHeader: String = TekstformatererHeader.lagHeader(brevmetadata, "Dette er en header") + generertHeader shouldBe personHeader() + } + + @Test + fun `lagHeader brev til institusjon`() { + val generertHeader: String = + TekstformatererHeader.lagHeader( + brevmetadata = brevmetadata.copy(institusjon = Institusjon("987654321", "Test & institusjon")), + overskrift = "Dette er en header", + ) + generertHeader shouldBe institusjonHeader() + } + + private fun personHeader(): String { + return """
    Dato: ${dagensDato()}
    +

    Dette er en header

    +
    +Navn: Test
    +Fødselsnummer: 12345678901 +
    """ + } + + private fun institusjonHeader(): String { + return """
    Dato: ${dagensDato()}
    +

    Dette er en header

    +
    +Navn: Test & institusjon
    +Organisasjonsnummer: 987654321 +
    +
    +Gjelder: Test
    +Fødselsnummer: 12345678901 +
    """ + } + + private val format = DateTimeFormatter.ofPattern("dd.MM.yyyy") + private fun dagensDato(): String { + return format.format(LocalDate.now()) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/DokprodTilHtmlTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/DokprodTilHtmlTest.kt new file mode 100644 index 000000000..da2bce5fa --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/DokprodTilHtmlTest.kt @@ -0,0 +1,60 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.pdf + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test + +internal class DokprodTilHtmlTest { + + @Test + fun `dokprodInnholdTilHtml skal Konvertere Overskrift Og Avsnitt`() { + val resultat = + DokprodTilHtml.dokprodInnholdTilHtml("_Overskrift\nFørste avsnitt\n\nAndre avsnitt\n\nTredje avsnitt") + resultat shouldBe "

    Overskrift

    Første avsnitt

    " + + "

    Andre avsnitt

    Tredje avsnitt

    " + } + + @Test + fun `dokprodInnholdTilHtml skal Konvertere Non Breaking Space`() { + // utf8nonBreakingSpace = "\u00A0"; + val resultat = DokprodTilHtml.dokprodInnholdTilHtml("10\u00A0000\u00A0kroner") + + resultat shouldBe "

    10 000 kroner

    " + } + + @Test + fun `dokprodInnholdTilHtml skal Konvertere Bullet Points`() { + val resultat = DokprodTilHtml.dokprodInnholdTilHtml("*-bulletpoint 1\nbulletpoint 2\nsiste bulletpoint-*") + + resultat shouldBe "
    • bulletpoint 1
    • bulletpoint 2
    • siste bulletpoint
    " + } + + @Test + fun `dokprodInnholdTilHtml skal Konvertere Bullet Points Når Første Linje Er Tom`() { + val resultat = DokprodTilHtml.dokprodInnholdTilHtml("*-\nbulletpoint 1\nbulletpoint 2\nsiste bulletpoint-*") + + resultat shouldBe "
    • bulletpoint 1
    • bulletpoint 2
    • siste bulletpoint
    " + } + + @Test + fun `dokprodInnholdTilHtml skal Konvertere Halvhjertede Avsnitt`() { + // halvhjertet avsnitt er hvor det er tatt kun ett linjeskift. + val resultat = DokprodTilHtml.dokprodInnholdTilHtml("Foo\nBar") + + resultat shouldBe "

    Foo
    Bar

    " + } + + @Test + fun `dokprodInnholdTilHtml skal Spesialbehandle Hilsen`() { + // halvhjertet avsnitt er hvor det er tatt kun ett linjeskift. + val resultat = DokprodTilHtml.dokprodInnholdTilHtml("Med vennlig hilsen\nNAV Familie- og pensjonsytelser") + + resultat shouldBe "

    Med vennlig hilsen
    NAV Familie- og pensjonsytelser

    " + } + + @Test + fun `dokprodInnholdTilHtml skal konvertere ampersand`() { + val resultat = DokprodTilHtml.dokprodInnholdTilHtml("Foo & Bar") + + resultat shouldBe "

    Foo & Bar

    " + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/PdfBrevServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/PdfBrevServiceTest.kt new file mode 100644 index 000000000..c64e40399 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/pdf/PdfBrevServiceTest.kt @@ -0,0 +1,136 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.pdf + +import io.kotest.matchers.shouldBe +import io.mockk.CapturingSlot +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstidspunkt +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstype +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.felles.header.Institusjon +import no.nav.familie.tilbake.micrometer.TellerService +import no.nav.familie.tilbake.organisasjon.OrganisasjonService +import org.junit.jupiter.api.Test +import java.util.Base64 + +internal class PdfBrevServiceTest { + + private val journalføringService: JournalføringService = mockk(relaxed = true) + private val tellerService: TellerService = mockk(relaxed = true) + private val taskService: TaskService = mockk(relaxed = true) + private val organisasjonService: OrganisasjonService = mockk(relaxed = true) + + private val pdfBrevService = PdfBrevService( + journalføringService, + tellerService, + taskService, + ) + + @Test + fun `sendBrev oppretter en task med korrekt fritekst`() { + val fritekst = "Dette er en \n\nfritekst med \n\nlinjeskift" + val slot = CapturingSlot() + every { taskService.save(capture(slot)) } returns mockk() + + val brevdata = lagBrevdata() + + pdfBrevService.sendBrev( + Testdata.behandling, + Testdata.fagsak, + brevtype = Brevtype.VARSEL, + brevdata, + 5L, + fritekst, + ) + + val task = slot.captured + val base64fritekst = task.metadata.getProperty("fritekst") + Base64.getDecoder().decode(base64fritekst.toByteArray()).decodeToString() shouldBe fritekst + + val distribusjonstype = task.metadata.getProperty("distribusjonstype") + distribusjonstype.shouldBe(Distribusjonstype.VIKTIG.name) + + val distribusjonstidspunkt = task.metadata.getProperty("distribusjonstidspunkt") + distribusjonstidspunkt.shouldBe(Distribusjonstidspunkt.KJERNETID.name) + } + + @Test + fun `sendBrev sender vedtaksbrev med riktig distribusjonstype og distribusjonstidspunkt`() { + val slot = CapturingSlot() + every { taskService.save(capture(slot)) } returns mockk() + val brevdata = lagBrevdata() + + pdfBrevService.sendBrev(Testdata.behandling, Testdata.fagsak, brevtype = Brevtype.VEDTAK, brevdata) + + val task = slot.captured + + val distribusjonstype = task.metadata.getProperty("distribusjonstype") + distribusjonstype.shouldBe(Distribusjonstype.VEDTAK.name) + + val distribusjonstidspunkt = task.metadata.getProperty("distribusjonstidspunkt") + distribusjonstidspunkt.shouldBe(Distribusjonstidspunkt.KJERNETID.name) + } + + @Test + fun `sendBrev sender henleggelsesbrev med riktig distribusjonstype og distribusjonstidspunkt`() { + val slot = CapturingSlot() + every { taskService.save(capture(slot)) } returns mockk() + val brevdata = lagBrevdata() + + pdfBrevService.sendBrev(Testdata.behandling, Testdata.fagsak, brevtype = Brevtype.HENLEGGELSE, brevdata) + + val task = slot.captured + + val distribusjonstype = task.metadata.getProperty("distribusjonstype") + distribusjonstype.shouldBe(Distribusjonstype.ANNET.name) + + val distribusjonstidspunkt = task.metadata.getProperty("distribusjonstidspunkt") + distribusjonstidspunkt.shouldBe(Distribusjonstidspunkt.KJERNETID.name) + } + + @Test + fun `sendBrev støtter å sende brev til institusjon med ampsand i navnet`() { + val slot = CapturingSlot() + every { taskService.save(capture(slot)) } returns mockk() + val brevdata = lagBrevdata().apply { + metadata = this.metadata.copy(institusjon = Institusjon("876543210", "Foo & Bar AS")) + } + + pdfBrevService.sendBrev(Testdata.behandling, Testdata.fagsak, brevtype = Brevtype.HENLEGGELSE, brevdata) + + val task = slot.captured + + val distribusjonstype = task.metadata.getProperty("distribusjonstype") + distribusjonstype.shouldBe(Distribusjonstype.ANNET.name) + + val distribusjonstidspunkt = task.metadata.getProperty("distribusjonstidspunkt") + distribusjonstidspunkt.shouldBe(Distribusjonstidspunkt.KJERNETID.name) + } + + private fun lagBrevdata() = Brevdata( + metadata = Brevmetadata( + sakspartId = "", + sakspartsnavn = "", + mottageradresse = Adresseinfo(" ", ""), + behandlendeEnhetsNavn = "", + ansvarligSaksbehandler = "Bob", + språkkode = Språkkode.NB, + ytelsestype = Ytelsestype.OVERGANGSSTØNAD, + saksnummer = "1232456", + behandlingstype = Behandlingstype.TILBAKEKREVING, + gjelderDødsfall = false, + ), + overskrift = "", + mottager = Brevmottager.BRUKER, + brevtekst = "", + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/DistribuerDokumentVedD\303\270dsfallTaskTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/DistribuerDokumentVedD\303\270dsfallTaskTest.kt" new file mode 100644 index 000000000..11243e1aa --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/DistribuerDokumentVedD\303\270dsfallTaskTest.kt" @@ -0,0 +1,106 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.task + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSingleElement +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstidspunkt +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstype +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.historikkinnslag.LagHistorikkinnslagTask +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDateTime +import java.util.Properties + +internal class DistribuerDokumentVedDødsfallTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var distribuerDokumentVedDødsfallTask: DistribuerDokumentVedDødsfallTask + + private val behandling = Testdata.behandling + private val behandlingId = behandling.id + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(behandling) + } + + @Test + fun `skal kjøre ferdig når adressen er blitt oppdatert`() { + distribuerDokumentVedDødsfallTask.doTask(opprettTask("jp1")) + + assertHistorikkTask( + TilbakekrevingHistorikkinnslagstype.DISTRIBUSJON_BREV_DØDSBO_SUKSESS, + ) + } + + @Test + fun `skal feile når adressen ikke har blitt oppdatert`() { + val exception = shouldThrow { + distribuerDokumentVedDødsfallTask.doTask(opprettTask("jpUkjentDødsbo")) + } + + exception.message shouldBe "org.springframework.web.client.RestClientResponseException: Ukjent adresse dødsbo" + } + + @Test + fun `skal opprette historikkinnslag når tasken er for gammel`() { + distribuerDokumentVedDødsfallTask.doTask( + opprettTask("jpUkjentDødsbo").copy( + opprettetTid = LocalDateTime.now() + .minusMonths(7), + ), + ) + + assertHistorikkTask( + TilbakekrevingHistorikkinnslagstype.DISTRIBUSJON_BREV_DØDSBO_FEILET_6_MND, + ) + } + + private fun opprettTask(journalpostId: String): Task { + return Task( + type = DistribuerDokumentVedDødsfallTask.TYPE, + payload = behandling.id.toString(), + properties = Properties().apply { + this["journalpostId"] = journalpostId + this["fagsystem"] = Fagsystem.BA.name + this["distribusjonstype"] = Distribusjonstype.VIKTIG.name + this["distribusjonstidspunkt"] = Distribusjonstidspunkt.KJERNETID.name + this["mottager"] = Brevmottager.BRUKER.name + this["brevtype"] = Brevtype.VEDTAK.name + this["ansvarligSaksbehandler"] = Constants.BRUKER_ID_VEDTAKSLØSNINGEN + }, + ) + } + + private fun assertHistorikkTask( + historikkinnslagstype: TilbakekrevingHistorikkinnslagstype, + ) { + taskService.findAll().shouldHaveSingleElement { + LagHistorikkinnslagTask.TYPE == it.type && + historikkinnslagstype.name == it.metadata["historikkinnslagstype"] && + behandlingId.toString() == it.payload + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/LagreBrevsporingTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/LagreBrevsporingTaskTest.kt new file mode 100644 index 000000000..f953d5c61 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/LagreBrevsporingTaskTest.kt @@ -0,0 +1,240 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.task + +import io.kotest.matchers.collections.shouldHaveSingleElement +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager.MANUELL_TILLEGGSMOTTAKER +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager.VERGE +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingRepository +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.historikkinnslag.LagHistorikkinnslagTask +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.iverksettvedtak.task.AvsluttBehandlingTask +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.util.Properties +import java.util.UUID + +internal class LagreBrevsporingTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var brevsporingRepository: BrevsporingRepository + + @Autowired + private lateinit var lagreBrevsporingTask: LagreBrevsporingTask + + private val behandling = Testdata.behandling + private val behandlingId = behandling.id + + private val dokumentId: String = "testverdi" + private val journalpostId: String = "testverdi" + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(behandling) + } + + @Test + fun `doTask skal lagre brevsporing for varselbrev`() { + lagreBrevsporingTask.doTask(opprettTask(behandlingId, Brevtype.VARSEL)) + + assertBrevsporing(Brevtype.VARSEL) + } + + @Test + fun `onCompletion skal lage historikk task for varselbrev`() { + lagreBrevsporingTask.onCompletion(opprettTask(behandlingId, Brevtype.VARSEL)) + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.VARSELBREV_SENDT, Aktør.VEDTAKSLØSNING, Brevtype.VARSEL) + } + + @Test + fun `onCompletion skal lage historikk task for manuelt varselbrev`() { + lagreBrevsporingTask.onCompletion(opprettTask(behandlingId, Brevtype.VARSEL, "Z0000")) + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.VARSELBREV_SENDT, Aktør.SAKSBEHANDLER, Brevtype.VARSEL) + } + + @Test + fun `doTask skal lagre brevsporing for korrigert varselbrev`() { + lagreBrevsporingTask.doTask(opprettTask(behandlingId, Brevtype.KORRIGERT_VARSEL)) + + assertBrevsporing(Brevtype.KORRIGERT_VARSEL) + } + + @Test + fun `onCompletion skal lage historikk task for korrigert varselbrev`() { + lagreBrevsporingTask.onCompletion(opprettTask(behandlingId, Brevtype.KORRIGERT_VARSEL)) + + assertHistorikkTask( + TilbakekrevingHistorikkinnslagstype.KORRIGERT_VARSELBREV_SENDT, + Aktør.SAKSBEHANDLER, + Brevtype.KORRIGERT_VARSEL, + ) + } + + @Test + fun `doTask skal lagre brevsporing for henleggelsesbrev`() { + lagreBrevsporingTask.doTask(opprettTask(behandlingId, Brevtype.HENLEGGELSE)) + + assertBrevsporing(Brevtype.HENLEGGELSE) + } + + @Test + fun `onCompletion skal lage historikk task for henleggelsesbrev`() { + lagreBrevsporingTask.onCompletion(opprettTask(behandlingId, Brevtype.HENLEGGELSE)) + + assertHistorikkTask( + TilbakekrevingHistorikkinnslagstype.HENLEGGELSESBREV_SENDT, + Aktør.VEDTAKSLØSNING, + Brevtype.HENLEGGELSE, + ) + } + + @Test + fun `doTask skal lagre brevsporing for innhent dokumentasjon`() { + lagreBrevsporingTask.doTask(opprettTask(behandlingId, Brevtype.INNHENT_DOKUMENTASJON)) + + assertBrevsporing(Brevtype.INNHENT_DOKUMENTASJON) + } + + @Test + fun `onCompletion skal lage historikk task for innhent dokumentasjon`() { + lagreBrevsporingTask.onCompletion(opprettTask(behandlingId, Brevtype.INNHENT_DOKUMENTASJON)) + + assertHistorikkTask( + TilbakekrevingHistorikkinnslagstype.INNHENT_DOKUMENTASJON_BREV_SENDT, + Aktør.SAKSBEHANDLER, + Brevtype.INNHENT_DOKUMENTASJON, + ) + } + + @Test + fun `doTask skal lagre brevsporing for vedtaksbrev`() { + lagreBrevsporingTask.doTask(opprettTask(behandlingId, Brevtype.VEDTAK)) + + assertBrevsporing(Brevtype.VEDTAK) + } + + @Test + fun `onCompletion skal lage historikk task for vedtaksbrev`() { + lagreBrevsporingTask.onCompletion(opprettTask(behandlingId, Brevtype.VEDTAK)) + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.VEDTAKSBREV_SENDT, Aktør.VEDTAKSLØSNING, Brevtype.VEDTAK) + } + + @Test + fun `onCompletion skal lage historikk task for vedtaksbrev når mottaker adresse er ukjent`() { + lagreBrevsporingTask.onCompletion( + opprettTask(behandlingId, Brevtype.VEDTAK).also { task -> + task.metadata.also { it["ukjentAdresse"] = "true" } + }, + ) + + assertHistorikkTask( + TilbakekrevingHistorikkinnslagstype.BREV_IKKE_SENDT_UKJENT_ADRESSE, + Aktør.VEDTAKSLØSNING, + Brevtype.VEDTAK, + ) + taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)) + .shouldHaveSingleElement { + it.type == LagHistorikkinnslagTask.TYPE && + TilbakekrevingHistorikkinnslagstype.VEDTAKSBREV_SENDT.tekst == it.metadata["beskrivelse"] + } + } + + @Test + fun `onCompletion skal lage historikk task for vedtaksbrev når adresse til dødsbo er ukjent`() { + lagreBrevsporingTask.onCompletion( + opprettTask(behandlingId, Brevtype.VEDTAK).also { task -> + task.metadata.also { it["dødsboUkjentAdresse"] = "true" } + }, + ) + + assertHistorikkTask( + TilbakekrevingHistorikkinnslagstype.BREV_IKKE_SENDT_DØDSBO_UKJENT_ADRESSE, + Aktør.VEDTAKSLØSNING, + Brevtype.VEDTAK, + ) + taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)) + .shouldHaveSingleElement { + it.type == LagHistorikkinnslagTask.TYPE && + TilbakekrevingHistorikkinnslagstype.VEDTAKSBREV_SENDT.tekst == it.metadata["beskrivelse"] + } + } + + @Test + fun `onCompletion skal lage AvsluttBehandlingTask ved brevtype VEDTAK, men kun når mottakeren ikke er en tilleggsmottaker`() { + lagreBrevsporingTask.onCompletion(opprettTask(behandlingId, Brevtype.VEDTAK)) + lagreBrevsporingTask.onCompletion(opprettTask(behandlingId, Brevtype.VEDTAK, brevmottager = MANUELL_TILLEGGSMOTTAKER)) + lagreBrevsporingTask.onCompletion(opprettTask(behandlingId, Brevtype.VEDTAK, brevmottager = VERGE)) + + taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)) + .single { it.type == AvsluttBehandlingTask.TYPE } + .also { it.metadata["mottager"] shouldBe Brevmottager.BRUKER.name } + } + + private fun opprettTask( + behandlingId: UUID, + brevtype: Brevtype, + ansvarligSaksbehandler: String? = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + brevmottager: Brevmottager = Brevmottager.BRUKER, + ): Task { + return Task( + type = LagreBrevsporingTask.TYPE, + payload = behandlingId.toString(), + properties = Properties().apply { + this["dokumentId"] = dokumentId + this["journalpostId"] = journalpostId + this["brevtype"] = brevtype.name + this["mottager"] = brevmottager.name + this["ansvarligSaksbehandler"] = ansvarligSaksbehandler + }, + ) + } + + private fun assertBrevsporing(brevtype: Brevtype) { + val brevsporing = brevsporingRepository.findFirstByBehandlingIdAndBrevtypeOrderBySporbarOpprettetTidDesc( + behandlingId, + brevtype, + ) + brevsporing.shouldNotBeNull() + brevsporing.dokumentId shouldBe dokumentId + brevsporing.journalpostId shouldBe journalpostId + } + + private fun assertHistorikkTask( + historikkinnslagstype: TilbakekrevingHistorikkinnslagstype, + aktør: Aktør, + brevtype: Brevtype, + ) { + taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)).shouldHaveSingleElement { + LagHistorikkinnslagTask.TYPE == it.type && + historikkinnslagstype.name == it.metadata["historikkinnslagstype"] && + aktør.name == it.metadata["aktør"] && + behandlingId.toString() == it.payload && + brevtype.name == it.metadata["brevtype"] + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/PubliserJournalpostTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/PubliserJournalpostTaskTest.kt new file mode 100644 index 000000000..c3f0f15d2 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/felles/task/PubliserJournalpostTaskTest.kt @@ -0,0 +1,82 @@ +package no.nav.familie.tilbake.dokumentbestilling.felles.task + +import io.kotest.matchers.collections.shouldHaveSingleElement +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstidspunkt +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstype +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.util.Properties +import java.util.UUID + +internal class PubliserJournalpostTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var publiserJournalpostTask: PubliserJournalpostTask + + @Test + fun `skal kjøre OK`() { + val opprettetTask = opprettTask("jp1") + publiserJournalpostTask.doTask(opprettetTask) + + opprettetTask.metadata["ukjentAdresse"] shouldBe null + opprettetTask.metadata["dødsboUkjentAdresse"] shouldBe null + } + + @Test + fun `skal merke task med ukjentadresse når bruker har ukjent adresse`() { + val opprettetTask = opprettTask("jpUkjentAdresse") + publiserJournalpostTask.doTask(opprettetTask) + + opprettetTask.metadata["ukjentAdresse"] shouldBe "true" + opprettetTask.metadata["dødsboUkjentAdresse"] shouldBe null + } + + @Test + fun `skal opprette DistribuerDokumentVedDødsfallTask ved ukjent adresse dødsbø`() { + val opprettetTask = opprettTask("jpUkjentDødsbo") + publiserJournalpostTask.doTask(opprettetTask) + + assertDistribuerDokumentVedDødsfallTask() + + opprettetTask.metadata["ukjentAdresse"] shouldBe null + opprettetTask.metadata["dødsboUkjentAdresse"] shouldBe "true" + } + + @Test + fun `skal kjøre OK når dokdist sender kode 409`() { + val opprettetTask = opprettTask("jpDuplikatDistribusjon") + publiserJournalpostTask.doTask(opprettetTask) + + opprettetTask.metadata["ukjentAdresse"] shouldBe null + opprettetTask.metadata["dødsboUkjentAdresse"] shouldBe null + } + + private fun opprettTask(journalpostId: String): Task { + return Task( + type = PubliserJournalpostTask.TYPE, + payload = objectMapper.writeValueAsString(PubliserJournalpostTaskData(UUID.randomUUID(), manuellAdresse = null)), + properties = Properties().apply { + this["journalpostId"] = journalpostId + this["fagsystem"] = Fagsystem.BA.name + this["distribusjonstype"] = Distribusjonstype.VIKTIG.name + this["distribusjonstidspunkt"] = Distribusjonstidspunkt.KJERNETID.name + }, + ) + } + + private fun assertDistribuerDokumentVedDødsfallTask() { + taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)).shouldHaveSingleElement { + DistribuerDokumentVedDødsfallTask.TYPE == it.type + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/KroneFormattererMedTusenskilleTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/KroneFormattererMedTusenskilleTest.kt new file mode 100644 index 000000000..304e9f147 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/KroneFormattererMedTusenskilleTest.kt @@ -0,0 +1,22 @@ +package no.nav.familie.tilbake.dokumentbestilling.handlebars + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +class KroneFormattererMedTusenskilleTest { + + @Test + fun `medTusenskille skal gi riktig tusenskille`() { + KroneFormattererMedTusenskille.medTusenskille(BigDecimal.valueOf(1), ' ') shouldBe "1" + KroneFormattererMedTusenskille.medTusenskille(BigDecimal.valueOf(12), ' ') shouldBe "12" + KroneFormattererMedTusenskille.medTusenskille(BigDecimal.valueOf(123), ' ') shouldBe "123" + KroneFormattererMedTusenskille.medTusenskille(BigDecimal.valueOf(1234), ' ') shouldBe "1 234" + KroneFormattererMedTusenskille.medTusenskille(BigDecimal.valueOf(12345), ' ') shouldBe "12 345" + KroneFormattererMedTusenskille.medTusenskille(BigDecimal.valueOf(123456), ' ') shouldBe "123 456" + KroneFormattererMedTusenskille.medTusenskille(BigDecimal.valueOf(1234567), ' ') shouldBe "1 234 567" + KroneFormattererMedTusenskille.medTusenskille(BigDecimal.valueOf(12345678), ' ') shouldBe "12 345 678" + KroneFormattererMedTusenskille.medTusenskille(BigDecimal.valueOf(123456789), ' ') shouldBe "123 456 789" + KroneFormattererMedTusenskille.medTusenskille(BigDecimal.valueOf(1234567), '\u00A0') shouldBe "1\u00A0234\u00A0567" + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/M\303\245nedHelperTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/M\303\245nedHelperTest.kt" new file mode 100644 index 000000000..f9fa93d0b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/handlebars/M\303\245nedHelperTest.kt" @@ -0,0 +1,37 @@ +package no.nav.familie.tilbake.dokumentbestilling.handlebars + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import java.time.YearMonth + +internal class MånedHelperTest { + + @Test + fun `apply returnerer alle måneder på korrekt format`() { + val januar = MånedHelper().apply(YearMonth.of(2022, 1), null) + val februar = MånedHelper().apply(YearMonth.of(2022, 2), null) + val mars = MånedHelper().apply(YearMonth.of(2022, 3), null) + val april = MånedHelper().apply(YearMonth.of(2022, 4), null) + val mai = MånedHelper().apply(YearMonth.of(2022, 5), null) + val juni = MånedHelper().apply(YearMonth.of(2022, 6), null) + val juli = MånedHelper().apply(YearMonth.of(2022, 7), null) + val august = MånedHelper().apply(YearMonth.of(2022, 8), null) + val september = MånedHelper().apply(YearMonth.of(2022, 9), null) + val oktober = MånedHelper().apply(YearMonth.of(2022, 10), null) + val november = MånedHelper().apply(YearMonth.of(2022, 11), null) + val desember = MånedHelper().apply(YearMonth.of(2022, 12), null) + + januar shouldBe "januar 2022" + februar shouldBe "februar 2022" + mars shouldBe "mars 2022" + april shouldBe "april 2022" + mai shouldBe "mai 2022" + juni shouldBe "juni 2022" + juli shouldBe "juli 2022" + august shouldBe "august 2022" + september shouldBe "september 2022" + oktober shouldBe "oktober 2022" + november shouldBe "november 2022" + desember shouldBe "desember 2022" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/HenleggelsesbrevServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/HenleggelsesbrevServiceTest.kt new file mode 100644 index 000000000..022006bb5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/HenleggelsesbrevServiceTest.kt @@ -0,0 +1,159 @@ +package no.nav.familie.tilbake.dokumentbestilling.henleggelse + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.string.shouldContain +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Verge +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.DistribusjonshåndteringService +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmetadataUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingService +import no.nav.familie.tilbake.dokumentbestilling.felles.EksterneDataForBrevService +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.PdfBrevService +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import no.nav.familie.tilbake.organisasjon.OrganisasjonService +import no.nav.familie.tilbake.pdfgen.validering.PdfaValidator +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class HenleggelsesbrevServiceTest : OppslagSpringRunnerTest() { + + private val eksterneDataForBrevService: EksterneDataForBrevService = mockk() + + private lateinit var henleggelsesbrevService: HenleggelsesbrevService + private var behandlingId = Testdata.behandling.id + + @Autowired + lateinit var pdfBrevService: PdfBrevService + lateinit var spyPdfBrevService: PdfBrevService + private val fagsakRepository: FagsakRepository = mockk() + private val brevsporingService: BrevsporingService = mockk() + private val behandlingRepository: BehandlingRepository = mockk() + private val organisasjonService: OrganisasjonService = mockk() + private val distribusjonshåndteringService: DistribusjonshåndteringService = mockk() + private val featureToggleService: FeatureToggleService = mockk(relaxed = true) + + private val brevmetadataUtil = BrevmetadataUtil( + behandlingRepository = behandlingRepository, + fagsakRepository = fagsakRepository, + manuelleBrevmottakerRepository = mockk(relaxed = true), + eksterneDataForBrevService = eksterneDataForBrevService, + organisasjonService = organisasjonService, + featureToggleService = featureToggleService, + ) + + @BeforeEach + fun setup() { + spyPdfBrevService = spyk(pdfBrevService) + henleggelsesbrevService = HenleggelsesbrevService( + behandlingRepository, + brevsporingService, + fagsakRepository, + eksterneDataForBrevService, + spyPdfBrevService, + organisasjonService, + distribusjonshåndteringService, + brevmetadataUtil, + ) + every { fagsakRepository.findByIdOrThrow(Testdata.fagsak.id) } returns Testdata.fagsak + every { behandlingRepository.findByIdOrThrow(Testdata.behandling.id) } returns Testdata.behandling + val personinfo = Personinfo("DUMMY_FNR_1", LocalDate.now(), "Fiona") + val ident = Testdata.fagsak.bruker.ident + every { eksterneDataForBrevService.hentPerson(ident, Fagsystem.BA) } returns personinfo + every { eksterneDataForBrevService.hentAdresse(any(), any(), any(), any()) } + .returns(Adresseinfo("DUMMY_FNR_2", "Bob")) + every { eksterneDataForBrevService.hentPåloggetSaksbehandlernavnMedDefault(any()) } returns "Siri Saksbehandler" + every { + brevsporingService.finnSisteVarsel(behandlingId) + } returns (Testdata.brevsporing) + } + + @Test + fun `sendHenleggelsebrev skal sende henleggelsesbrev`() { + henleggelsesbrevService.sendHenleggelsebrev(behandlingId, null, Brevmottager.BRUKER) + + verify { + spyPdfBrevService.sendBrev( + Testdata.behandling, + Testdata.fagsak, + Brevtype.HENLEGGELSE, + any(), + any(), + any(), + ) + } + } + + @Test + fun `hentForhåndsvisningHenleggelsesbrev skal returnere pdf for henleggelsebrev`() { + val bytes = henleggelsesbrevService.hentForhåndsvisningHenleggelsesbrev(behandlingId, null) + + PdfaValidator.validatePdf(bytes) + } + + @Test + fun `hentForhåndsvisningHenleggelsesbrev skal returnere pdf for henleggelsebrev for tilbakekreving revurdering`() { + every { behandlingRepository.findByIdOrThrow(Testdata.behandling.id) } + .returns(Testdata.behandling.copy(type = Behandlingstype.REVURDERING_TILBAKEKREVING)) + + val bytes = henleggelsesbrevService.hentForhåndsvisningHenleggelsesbrev( + behandlingId, + REVURDERING_HENLEGGELSESBREV_FRITEKST, + ) + + PdfaValidator.validatePdf(bytes) + } + + @Test + fun `sendHenleggelsebrev skal ikke sende henleggelsesbrev hvis varselbrev ikke sendt`() { + every { + brevsporingService.finnSisteVarsel(behandlingId) + } returns (null) + + val e = shouldThrow { + henleggelsesbrevService.sendHenleggelsebrev( + behandlingId, + null, + Brevmottager.BRUKER, + ) + } + + e.message shouldContain "varsel ikke er sendt" + } + + @Test + fun `sendHenleggelsebrev skal ikke sende henleggelsesbrev for tilbakekreving revurdering uten fritekst`() { + every { behandlingRepository.findByIdOrThrow(Testdata.behandling.id) } + .returns(Testdata.behandling.copy(type = Behandlingstype.REVURDERING_TILBAKEKREVING)) + + val e = shouldThrow { + henleggelsesbrevService.sendHenleggelsebrev( + behandlingId, + null, + Brevmottager.BRUKER, + ) + } + + e.message shouldContain "henleggelsesbrev uten fritekst" + } + + companion object { + + private const val REVURDERING_HENLEGGELSESBREV_FRITEKST = "Revurderingen ble henlagt" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/TekstformatererHenleggelsesbrevTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/TekstformatererHenleggelsesbrevTest.kt new file mode 100644 index 000000000..b373f2b88 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/henleggelse/TekstformatererHenleggelsesbrevTest.kt @@ -0,0 +1,218 @@ +package no.nav.familie.tilbake.dokumentbestilling.henleggelse + +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.header.Institusjon +import no.nav.familie.tilbake.dokumentbestilling.henleggelse.handlebars.dto.Henleggelsesbrevsdokument +import org.junit.jupiter.api.Test +import java.nio.charset.StandardCharsets +import java.time.LocalDate +import java.util.Scanner + +class TekstformatererHenleggelsesbrevTest { + + private val niendeMars = LocalDate.of(2019, 3, 9) + + private val brevmetadata = Brevmetadata( + sakspartId = "12345678901", + sakspartsnavn = "Test", + vergenavn = "John Doe", + mottageradresse = Adresseinfo("12345678901", "Test"), + behandlendeEnhetsNavn = "NAV Familie- og pensjonsytelser Skien", + ansvarligSaksbehandler = "Bob", + saksnummer = "1232456", + språkkode = Språkkode.NB, + ytelsestype = Ytelsestype.BARNETILSYN, + gjelderDødsfall = false, + ) + + private val henleggelsesbrevsdokument = Henleggelsesbrevsdokument( + brevmetadata, + niendeMars, + REVURDERING_HENLEGGELSESBREV_FRITEKST, + ) + + @Test + fun `lagFritekst skal generere henleggelsesbrev`() { + val generertBrev: String = TekstformatererHenleggelsesbrev.lagFritekst(henleggelsesbrevsdokument) + val fasit = les("/henleggelsesbrev/henleggelsesbrev.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagFritekst skal generere henleggelsesbrev dødsfall bruker`() { + val brevmetadata = brevmetadata.copy(gjelderDødsfall = true) + val henleggelsesbrevsdokument = henleggelsesbrevsdokument.copy(brevmetadata = brevmetadata) + val generertBrev: String = TekstformatererHenleggelsesbrev.lagFritekst(henleggelsesbrevsdokument) + val fasit = les("/henleggelsesbrev/henleggelsesbrev_død_bruker.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagFritekst skal generere henleggelsesbrev institusjon`() { + val brevmetadata = brevmetadata.copy(institusjon = Institusjon("test", "test")) + val henleggelsesbrevsdokument = henleggelsesbrevsdokument.copy(brevmetadata = brevmetadata) + val generertBrev: String = TekstformatererHenleggelsesbrev.lagFritekst(henleggelsesbrevsdokument) + val fasit = les("/henleggelsesbrev/henleggelsesbrev_institusjon.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagRevurderingsfritekst skal generere henleggelsesbrev for tilbakekreving revurdering`() { + val generertBrev: String = + TekstformatererHenleggelsesbrev.lagRevurderingsfritekst(henleggelsesbrevsdokument) + val fasit = les("/henleggelsesbrev/henleggelsesbrev_revurdering.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagRevurderingsfritekst skal generere henleggelsesbrev for tilbakekreving revurdering dødsfall bruker`() { + val brevmetadata = brevmetadata.copy(gjelderDødsfall = true) + val henleggelsesbrevsdokument = henleggelsesbrevsdokument.copy(brevmetadata = brevmetadata) + val generertBrev: String = + TekstformatererHenleggelsesbrev.lagRevurderingsfritekst(henleggelsesbrevsdokument) + val fasit = les("/henleggelsesbrev/henleggelsesbrev_revurdering_død_bruker.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagFritekst skal generere henleggelsesbrev med verge`() { + val brevmetadata = brevmetadata.copy(finnesVerge = true, finnesAnnenMottaker = true) + val henleggelsesbrevsdokument = henleggelsesbrevsdokument.copy(brevmetadata = brevmetadata) + val generertBrev: String = TekstformatererHenleggelsesbrev.lagFritekst(henleggelsesbrevsdokument) + val fasit = les("/henleggelsesbrev/henleggelsesbrev.txt") + val vergeTekst = les("/varselbrev/verge.txt") + generertBrev shouldBe "$fasit${System.lineSeparator().repeat(2)}$vergeTekst" + } + + @Test + fun `lagRevurderingsfritekst skal generere henleggelsesbrev for tilbakekreving revurdering med verge`() { + val brevmetadata = brevmetadata.copy(finnesVerge = true, finnesAnnenMottaker = true) + val henleggelsesbrevsdokument = henleggelsesbrevsdokument.copy(brevmetadata = brevmetadata) + val generertBrev: String = + TekstformatererHenleggelsesbrev.lagRevurderingsfritekst(henleggelsesbrevsdokument) + val fasit = les("/henleggelsesbrev/henleggelsesbrev_revurdering.txt") + val vergeTekst = les("/varselbrev/verge.txt") + + generertBrev shouldBe "$fasit${System.lineSeparator().repeat(2)}$vergeTekst" + } + + @Test + fun `lagFritekst skal generere henleggelsesbrev nynorsk`() { + val brevmetadata = brevmetadata.copy(språkkode = Språkkode.NN) + val henleggelsesbrevsdokument = henleggelsesbrevsdokument.copy(brevmetadata = brevmetadata) + val generertBrev: String = TekstformatererHenleggelsesbrev.lagFritekst(henleggelsesbrevsdokument) + val fasit = les("/henleggelsesbrev/henleggelsesbrev_nn.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagFritekst skal generere henleggelsesbrev nynorsk dødsfall bruker`() { + val brevmetadata = brevmetadata.copy(språkkode = Språkkode.NN, gjelderDødsfall = true) + val henleggelsesbrevsdokument = henleggelsesbrevsdokument.copy(brevmetadata = brevmetadata) + val generertBrev: String = TekstformatererHenleggelsesbrev.lagFritekst(henleggelsesbrevsdokument) + val fasit = les("/henleggelsesbrev/henleggelsesbrev_nn_død_bruker.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagFritekst skal generere henleggelsesbrev nynorsk institusjon`() { + val brevmetadata = brevmetadata.copy(språkkode = Språkkode.NN, institusjon = Institusjon("123", "123")) + val henleggelsesbrevsdokument = henleggelsesbrevsdokument.copy(brevmetadata = brevmetadata) + val generertBrev: String = TekstformatererHenleggelsesbrev.lagFritekst(henleggelsesbrevsdokument) + val fasit = les("/henleggelsesbrev/henleggelsesbrev_nn_institusjon.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagRevurderingsfritekst skal generere henleggelsesbrev nynorsk for tilbakekreving revurderning`() { + val brevmetadata = brevmetadata.copy(språkkode = Språkkode.NN) + val henleggelsesbrevsdokument = henleggelsesbrevsdokument.copy(brevmetadata = brevmetadata) + val generertBrev: String = + TekstformatererHenleggelsesbrev.lagRevurderingsfritekst(henleggelsesbrevsdokument) + val fasit = les("/henleggelsesbrev/henleggelsesbrev_revurdering_nn.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagRevurderingsfritekst skal generere henleggelsesbrev nynorsk for tilbakekreving revurderning dødsfall bruker`() { + val brevmetadata = brevmetadata.copy(språkkode = Språkkode.NN, gjelderDødsfall = true) + val henleggelsesbrevsdokument = henleggelsesbrevsdokument.copy(brevmetadata = brevmetadata) + val generertBrev: String = + TekstformatererHenleggelsesbrev.lagRevurderingsfritekst(henleggelsesbrevsdokument) + val fasit = les("/henleggelsesbrev/henleggelsesbrev_revurdering_nn_død_bruker.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagOverskrift skal generere henleggelsesbrev overskrift`() { + val overskrift: String = TekstformatererHenleggelsesbrev.lagOverskrift(brevmetadata) + val fasit = "NAV har avsluttet saken din om tilbakebetaling" + overskrift shouldBe fasit + } + + @Test + fun `lagOverskrift skal generere henleggelsesbrev overskrift dødsfall bruker`() { + val brevmetadata = brevmetadata.copy(gjelderDødsfall = true) + val overskrift: String = TekstformatererHenleggelsesbrev.lagOverskrift(brevmetadata) + val fasit = "NAV har avsluttet saken om tilbakebetaling" + overskrift shouldBe fasit + } + + @Test + fun `lagOverskrift skal generere henleggelsesbrev institusjon`() { + val brevmetadata = brevmetadata.copy(institusjon = Institusjon("test", "test")) + val overskrift: String = TekstformatererHenleggelsesbrev.lagOverskrift(brevmetadata) + val fasit = "NAV har avsluttet saken om tilbakebetaling" + overskrift shouldBe fasit + } + + @Test + fun `lagOverskrift skal generere henleggelsesbrev overskrift for tilbakekreving revurdering`() { + val overskrift: String = + TekstformatererHenleggelsesbrev.lagRevurderingsoverskrift(brevmetadata) + val fasit = "Tilbakebetaling stønad til barnetilsyn" + overskrift shouldBe fasit + } + + @Test + fun `lagOverskrift skal generere henleggelsesbrev overskrift nynorsk`() { + val brevmetadata = brevmetadata.copy(språkkode = Språkkode.NN) + val overskrift: String = TekstformatererHenleggelsesbrev.lagOverskrift(brevmetadata) + val fasit = "NAV har avslutta saka di om tilbakebetaling" + overskrift shouldBe fasit + } + + @Test + fun `lagOverskrift skal generere henleggelsesbrev overskrift nynorsk dødsfall bruker`() { + val brevmetadata = brevmetadata.copy(språkkode = Språkkode.NN, gjelderDødsfall = true) + val overskrift: String = TekstformatererHenleggelsesbrev.lagOverskrift(brevmetadata) + val fasit = "NAV har avslutta saka om tilbakebetaling" + overskrift shouldBe fasit + } + + @Test + fun `lagOverskrift skal generere henleggelsesbrev overskrift institusjon`() { + val brevmetadata = brevmetadata.copy(språkkode = Språkkode.NN, institusjon = Institusjon("123", "123")) + val overskrift: String = TekstformatererHenleggelsesbrev.lagOverskrift(brevmetadata) + val fasit = "NAV har avslutta saka om tilbakebetaling" + overskrift shouldBe fasit + } + + private fun les(filnavn: String): String? { + javaClass.getResourceAsStream(filnavn).use { resource -> + Scanner(resource, StandardCharsets.UTF_8).use { scanner -> + scanner.useDelimiter("\\A") + return if (scanner.hasNext()) scanner.next() else null + } + } + } + + companion object { + + private const val REVURDERING_HENLEGGELSESBREV_FRITEKST = "Revurderingen ble henlagt" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/InnhentDokumentasjonbrevServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/InnhentDokumentasjonbrevServiceTest.kt new file mode 100644 index 000000000..6d441aca5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/InnhentDokumentasjonbrevServiceTest.kt @@ -0,0 +1,83 @@ +package no.nav.familie.tilbake.dokumentbestilling.innhentdokumentasjon + +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Verge +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.DistribusjonshåndteringService +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmetadataUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.EksterneDataForBrevService +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.PdfBrevService +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import no.nav.familie.tilbake.organisasjon.OrganisasjonService +import no.nav.familie.tilbake.pdfgen.validering.PdfaValidator +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class InnhentDokumentasjonbrevServiceTest : OppslagSpringRunnerTest() { + + private val flereOpplysninger = "Vi trenger flere opplysninger" + private val mockEksterneDataForBrevService: EksterneDataForBrevService = mockk() + + @Autowired + lateinit var pdfBrevService: PdfBrevService + lateinit var spyPdfBrevService: PdfBrevService + + @Autowired + lateinit var distribusjonshåndteringService: DistribusjonshåndteringService + private val fagsakRepository: FagsakRepository = mockk() + private val behandlingRepository: BehandlingRepository = mockk() + private lateinit var innhentDokumentasjonBrevService: InnhentDokumentasjonbrevService + private val organisasjonService: OrganisasjonService = mockk() + private val featureToggleService: FeatureToggleService = mockk(relaxed = true) + private val brevmetadataUtil = BrevmetadataUtil( + behandlingRepository = behandlingRepository, + fagsakRepository = fagsakRepository, + manuelleBrevmottakerRepository = mockk(relaxed = true), + eksterneDataForBrevService = mockEksterneDataForBrevService, + organisasjonService = organisasjonService, + featureToggleService = featureToggleService, + ) + + @BeforeEach + fun setup() { + spyPdfBrevService = spyk(pdfBrevService) + innhentDokumentasjonBrevService = InnhentDokumentasjonbrevService( + fagsakRepository, + behandlingRepository, + mockEksterneDataForBrevService, + spyPdfBrevService, + organisasjonService, + distribusjonshåndteringService, + brevmetadataUtil, + ) + every { fagsakRepository.findByIdOrThrow(Testdata.fagsak.id) } returns Testdata.fagsak + every { behandlingRepository.findByIdOrThrow(Testdata.behandling.id) } returns Testdata.behandling + val personinfo = Personinfo("DUMMY_FØDSELSNUMMER", LocalDate.now(), "Fiona") + val ident = Testdata.fagsak.bruker.ident + every { mockEksterneDataForBrevService.hentPerson(ident, Fagsystem.BA) } returns personinfo + every { mockEksterneDataForBrevService.hentPåloggetSaksbehandlernavnMedDefault(any()) } returns "Siri Saksbehandler" + every { mockEksterneDataForBrevService.hentAdresse(any(), any(), any(), any()) } + .returns(Adresseinfo("DUMMY_FØDSELSNUMMER", "Bob")) + } + + @Test + fun `hentForhåndsvisningInnhentDokumentasjonBrev returnere pdf for innhent dokumentasjonbrev`() { + val data = innhentDokumentasjonBrevService.hentForhåndsvisningInnhentDokumentasjonBrev( + Testdata.behandling.id, + flereOpplysninger, + ) + + PdfaValidator.validatePdf(data) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/TekstformatererInnhentDokumentasjonsbrevTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/TekstformatererInnhentDokumentasjonsbrevTest.kt new file mode 100644 index 000000000..ade4b869c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/innhentdokumentasjon/TekstformatererInnhentDokumentasjonsbrevTest.kt @@ -0,0 +1,159 @@ +package no.nav.familie.tilbake.dokumentbestilling.innhentdokumentasjon + +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.header.Institusjon +import no.nav.familie.tilbake.dokumentbestilling.innhentdokumentasjon.handlebars.dto.InnhentDokumentasjonsbrevsdokument +import org.junit.jupiter.api.Test +import java.nio.charset.StandardCharsets +import java.time.LocalDate +import java.util.Scanner + +class TekstformatererInnhentDokumentasjonsbrevTest { + + private val metadata = Brevmetadata( + sakspartId = "12345678901", + sakspartsnavn = "Test", + mottageradresse = Adresseinfo("12345678901", "Test"), + behandlendeEnhetsNavn = "NAV Familie- og pensjonsytelser Skien", + ansvarligSaksbehandler = "Bob", + saksnummer = "1232456", + språkkode = Språkkode.NB, + ytelsestype = Ytelsestype.BARNETILSYN, + gjelderDødsfall = false, + ) + private val innhentDokumentasjonsbrevsdokument = + InnhentDokumentasjonsbrevsdokument( + brevmetadata = metadata, + fritekstFraSaksbehandler = "Dette er ein fritekst.", + fristdato = LocalDate.of(2020, 3, 2), + ) + + @Test + fun `lagInnhentDokumentasjonBrevFritekst skal generere innhentdokumentasjonbrev`() { + val generertBrev = TekstformatererInnhentDokumentasjonsbrev.lagFritekst(innhentDokumentasjonsbrevsdokument) + + val fasit = les("/innhentdokumentasjonbrev/innhentdokumentasjonbrev.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagInnhentDokumentasjonBrevFritekst skal generere innhentdokumentasjonbrev dødsfall bruker`() { + val metadata = metadata.copy(gjelderDødsfall = true) + val dokument = innhentDokumentasjonsbrevsdokument.copy(brevmetadata = metadata) + val generertBrev = TekstformatererInnhentDokumentasjonsbrev.lagFritekst(dokument) + + val fasit = les("/innhentdokumentasjonbrev/innhentdokumentasjonbrev_død_bruker.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagInnhentDokumentasjonBrevFritekst skal generere innhentdokumentasjonbrev institusjon`() { + val metadata = metadata.copy(institusjon = Institusjon("test", "123")) + val dokument = innhentDokumentasjonsbrevsdokument.copy(brevmetadata = metadata) + val generertBrev = TekstformatererInnhentDokumentasjonsbrev.lagFritekst(dokument) + + val fasit = les("/innhentdokumentasjonbrev/innhentdokumentasjonbrev_institusjon.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagInnhentDokumentasjonBrevFritekst skal generere innhentdokumentasjonbrev for verge`() { + val brevMetadata = metadata.copy(vergenavn = "John Doe", finnesVerge = true, finnesAnnenMottaker = true) + val dokument = innhentDokumentasjonsbrevsdokument.copy(brevmetadata = brevMetadata) + + val generertBrev = TekstformatererInnhentDokumentasjonsbrev.lagFritekst(dokument) + + val fasit = les("/innhentdokumentasjonbrev/innhentdokumentasjonbrev.txt") + val vergeTekst = les("/varselbrev/verge.txt") + generertBrev shouldBe "$fasit${System.lineSeparator().repeat(2)}$vergeTekst" + } + + @Test + fun `lagInnhentDokumentasjonBrevFritekst skal generere innhentdokumentasjonbrev for verge organisasjon`() { + val brevMetadata = metadata.copy( + mottageradresse = Adresseinfo( + ident = "12345678901", + mottagernavn = "Semba AS c/o John Doe", + ), + sakspartsnavn = "Test", + vergenavn = "John Doe", + finnesVerge = true, + finnesAnnenMottaker = true, + ) + val dokument = innhentDokumentasjonsbrevsdokument.copy(brevmetadata = brevMetadata) + + val generertBrev = TekstformatererInnhentDokumentasjonsbrev.lagFritekst(dokument) + + val fasit = les("/innhentdokumentasjonbrev/innhentdokumentasjonbrev.txt") + val vergeTekst = "Brev med likt innhold er sendt til Test" + generertBrev shouldBe "$fasit${System.lineSeparator().repeat(2)}$vergeTekst" + } + + @Test + fun `lagInnhentDokumentasjonBrevFritekst skal generere innhentdokumentasjonbrev nynorsk`() { + val brevMetadata = metadata.copy(språkkode = Språkkode.NN) + val dokument = + innhentDokumentasjonsbrevsdokument.copy(brevmetadata = brevMetadata.copy(ytelsestype = Ytelsestype.BARNETRYGD)) + + val generertBrev = TekstformatererInnhentDokumentasjonsbrev.lagFritekst(dokument) + + val fasit = les("/innhentdokumentasjonbrev/innhentdokumentasjonbrev_nn.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagInnhentDokumentasjonBrevFritekst skal generere innhentdokumentasjonbrev nynorsk dødsfall bruker`() { + val brevMetadata = metadata.copy(språkkode = Språkkode.NN, gjelderDødsfall = true) + val dokument = + innhentDokumentasjonsbrevsdokument.copy(brevmetadata = brevMetadata.copy(ytelsestype = Ytelsestype.BARNETRYGD)) + + val generertBrev = TekstformatererInnhentDokumentasjonsbrev.lagFritekst(dokument) + + val fasit = les("/innhentdokumentasjonbrev/innhentdokumentasjonbrev_nn_død_bruker.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagInnhentDokumentasjonBrevFritekst skal generere innhentdokumentasjonbrev nynorsk institusjon`() { + val brevMetadata = metadata.copy(språkkode = Språkkode.NN, institusjon = Institusjon("123", "123")) + val dokument = + innhentDokumentasjonsbrevsdokument.copy(brevmetadata = brevMetadata.copy(ytelsestype = Ytelsestype.BARNETRYGD)) + + val generertBrev = TekstformatererInnhentDokumentasjonsbrev.lagFritekst(dokument) + + val fasit = les("/innhentdokumentasjonbrev/innhentdokumentasjonbrev_nn_institusjon.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagInnhentDokumentasjonBrevOverskrift skal generere innhentdokumentasjonbrev overskrift`() { + val overskrift = TekstformatererInnhentDokumentasjonsbrev.lagOverskrift(innhentDokumentasjonsbrevsdokument.brevmetadata) + + val fasit = "Vi trenger flere opplysninger" + overskrift shouldBe fasit + } + + @Test + fun `lagInnhentDokumentasjonBrevOverskrift skal generere innhentdokumentasjonbrev overskrift nynorsk`() { + val brevMetadata = metadata.copy(språkkode = Språkkode.NN) + + val overskrift = TekstformatererInnhentDokumentasjonsbrev + .lagOverskrift(brevMetadata) + + val fasit = "Vi trenger fleire opplysningar" + overskrift shouldBe fasit + } + + private fun les(filnavn: String): String? { + javaClass.getResourceAsStream(filnavn).use { resource -> + Scanner(resource, StandardCharsets.UTF_8).use { scanner -> + scanner.useDelimiter("\\A") + return if (scanner.hasNext()) scanner.next() else null + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/ManuellBrevmottakerServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/ManuellBrevmottakerServiceTest.kt new file mode 100644 index 000000000..64aa33799 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/manuell/brevmottaker/ManuellBrevmottakerServiceTest.kt @@ -0,0 +1,348 @@ +package no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker + +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSingleElement +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.organisasjon.Organisasjon +import no.nav.familie.kontrakter.felles.tilbakekreving.ManuellAdresseInfo +import no.nav.familie.kontrakter.felles.tilbakekreving.MottakerType.DØDSBO +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.ManuellBrevmottakerRequestDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.FagsakService +import no.nav.familie.tilbake.behandling.ValiderBrevmottakerService +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.manuell.brevmottaker.domene.ManuellBrevmottaker +import no.nav.familie.tilbake.historikkinnslag.HistorikkService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import no.nav.familie.tilbake.integration.pdl.PdlClient +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.UUID + +class ManuellBrevmottakerServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var manuellBrevmottakerRepository: ManuellBrevmottakerRepository + private val mockHistorikkService: HistorikkService = mockk(relaxed = true) + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var fagsakService: FagsakService + + @Autowired + private lateinit var validerBrevmottakerService: ValiderBrevmottakerService + + private lateinit var behandling: Behandling + private lateinit var manuellBrevmottakerService: ManuellBrevmottakerService + private val opprettetTidspunktSlot = mutableListOf() + + private val manuellBrevmottakerRequestDto = ManuellBrevmottakerRequestDto( + type = DØDSBO, + navn = "John Doe", + manuellAdresseInfo = ManuellAdresseInfo( + adresselinje1 = "test adresse1", + adresselinje2 = "test adresse2", + postnummer = "0000", + poststed = "Oslo", + landkode = "NO", + ), + ) + + private val mockPdlClient: PdlClient = mockk() + + private val mockIntegrasjonerClient: IntegrasjonerClient = mockk() + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandling = behandlingRepository.insert(Testdata.behandling) + + manuellBrevmottakerService = ManuellBrevmottakerService( + manuellBrevmottakerRepository = manuellBrevmottakerRepository, + historikkService = mockHistorikkService, + behandlingRepository = behandlingRepository, + behandlingskontrollService = behandlingskontrollService, + fagsakService = fagsakService, + pdlClient = mockPdlClient, + integrasjonerClient = mockIntegrasjonerClient, + validerBrevmottakerService = validerBrevmottakerService, + + ) + + every { + mockHistorikkService.lagHistorikkinnslag( + behandlingId = any(), + historikkinnslagstype = any(), + aktør = any(), + opprettetTidspunkt = capture(opprettetTidspunktSlot), + tittel = any(), + beskrivelse = any(), + ) + } just runs + + every { mockPdlClient.hentPersoninfo(any(), any()) } returns Personinfo("12345678901", LocalDate.MIN, "Eldar") + every { mockIntegrasjonerClient.validerOrganisasjon(any()) } returns true + every { mockIntegrasjonerClient.hentOrganisasjon("123456789") } returns + Organisasjon("123456789", navn = "Organisasjon AS") + } + + @AfterEach + fun clearSlot() { + opprettetTidspunktSlot.clear() + } + + @Test + fun `leggTilBrevmottaker skal legge til brevmottakere og oppdatere med oppdaterBrevmottaker`() { + shouldNotThrow { + manuellBrevmottakerService.leggTilBrevmottaker(behandling.id, manuellBrevmottakerRequestDto) + } + shouldNotThrow { + manuellBrevmottakerService.leggTilBrevmottaker( + behandling.id, + manuellBrevmottakerRequestDto.copy( + navn = "Kari Nordmann", + manuellAdresseInfo = null, + personIdent = "12345678910", + ), + ) + } + + var manuellBrevmottakere = manuellBrevmottakerService.hentBrevmottakere(behandling.id) + + manuellBrevmottakere.shouldHaveSize(2) + val dbManuellBrevmottaker = manuellBrevmottakere.filter { it.navn.equals("John Doe") }.first() + assertEqualsManuellBrevmottaker(dbManuellBrevmottaker, manuellBrevmottakerRequestDto) + + verify(exactly = 2) { + mockHistorikkService.lagHistorikkinnslag( + behandlingId = behandling.id, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BREVMOTTAKER_LAGT_TIL, + aktør = Aktør.SAKSBEHANDLER, + opprettetTidspunkt = or(opprettetTidspunktSlot[0], opprettetTidspunktSlot[1]), + beskrivelse = any(), + tittel = any(), + ) + } + + val oppdatertManuellBrevmottaker = manuellBrevmottakerRequestDto.copy( + manuellAdresseInfo = ManuellAdresseInfo( + adresselinje1 = "ny", + postnummer = "1111", + poststed = "stavanger", + landkode = "NO", + ), + ) + shouldNotThrow { + manuellBrevmottakerService.oppdaterBrevmottaker( + behandling.id, + dbManuellBrevmottaker.id, + oppdatertManuellBrevmottaker, + ) + } + + manuellBrevmottakere = manuellBrevmottakerService.hentBrevmottakere(behandling.id) + manuellBrevmottakere.shouldHaveSize(2) + val dbOppdatertManuellBrevmottaker = manuellBrevmottakere.filter { it.navn.equals("John Doe") }.first() + assertEqualsManuellBrevmottaker(dbOppdatertManuellBrevmottaker, oppdatertManuellBrevmottaker) + } + + @Test + fun `fjernBrevmottaker fjerner brevmottaker`() { + shouldNotThrow { + manuellBrevmottakerService.leggTilBrevmottaker(behandling.id, manuellBrevmottakerRequestDto) + } + shouldNotThrow { + manuellBrevmottakerService.leggTilBrevmottaker( + behandling.id, + manuellBrevmottakerRequestDto.copy(navn = "Kari Nordmann"), + ) + } + + val manuellBrevmottakere = manuellBrevmottakerService.hentBrevmottakere(behandling.id) + + manuellBrevmottakere.shouldHaveSize(2) + val dbManuellBrevmottaker = manuellBrevmottakere.filter { it.navn.equals("John Doe") }.first() + assertEqualsManuellBrevmottaker(dbManuellBrevmottaker, manuellBrevmottakerRequestDto) + + verify(exactly = 2) { + mockHistorikkService.lagHistorikkinnslag( + behandlingId = behandling.id, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BREVMOTTAKER_LAGT_TIL, + aktør = Aktør.SAKSBEHANDLER, + opprettetTidspunkt = or(opprettetTidspunktSlot[0], opprettetTidspunktSlot[1]), + beskrivelse = any(), + tittel = any(), + ) + } + + shouldNotThrow { + manuellBrevmottakerService.fjernBrevmottaker(behandling.id, dbManuellBrevmottaker.id) + } + + manuellBrevmottakerService.hentBrevmottakere(behandling.id).filter { it.navn.equals("John Doe") } + .shouldBeEmpty() + + verify(exactly = 1) { + mockHistorikkService.lagHistorikkinnslag( + behandlingId = behandling.id, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BREVMOTTAKER_FJERNET, + aktør = Aktør.SAKSBEHANDLER, + opprettetTidspunkt = opprettetTidspunktSlot[2], + beskrivelse = any(), + tittel = any(), + ) + } + } + + @Test + fun `opprettBrevmottakerSteg skal opprette og autoutføre behandlingssteg BREVMOTTAKER`() { + manuellBrevmottakerService.opprettBrevmottakerSteg(behandling.id) + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.shouldHaveSingleElement { + it.behandlingssteg == Behandlingssteg.BREVMOTTAKER && + it.behandlingsstegsstatus == Behandlingsstegstatus.AUTOUTFØRT + } + } + + @Test + fun `opprettBrevmottakerSteg skal ikke opprette steg når behandling er avsluttet`() { + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val exception = shouldThrow { manuellBrevmottakerService.opprettBrevmottakerSteg(behandling.id) } + exception.message shouldBe "Behandling med id=${behandling.id} er allerede ferdig behandlet." + } + + @Test + fun `opprettBrevmottakerSteg skal ikke opprette steg når behandling er på vent`() { + val behandling = behandlingRepository.findByIdOrThrow(Testdata.behandling.id) + lagBehandlingsstegstilstand(behandling.id, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + + behandlingskontrollService.settBehandlingPåVent( + behandling.id, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + LocalDate.now().plusWeeks(4), + ) + + val exception = shouldThrow { manuellBrevmottakerService.opprettBrevmottakerSteg(behandling.id) } + exception.message shouldBe "Behandling med id=${behandling.id} er på vent." + } + + @Test + fun `fjernManuelleBrevmottakereOgTilbakeførSteg skal fjerne brevmottakere og tilbakeføre steget`() { + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + lagBehandlingsstegstilstand(behandling.id, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + + manuellBrevmottakerService.opprettBrevmottakerSteg(behandling.id) + manuellBrevmottakerService.leggTilBrevmottaker(behandling.id, manuellBrevmottakerRequestDto) + manuellBrevmottakerService.hentBrevmottakere(behandling.id).shouldHaveSize(1) + + manuellBrevmottakerService.fjernManuelleBrevmottakereOgTilbakeførSteg(behandling.id) + manuellBrevmottakerService.hentBrevmottakere(behandling.id).shouldHaveSize(0) + + behandlingsstegstilstandRepository.findByBehandlingId(behandling.id).shouldHaveSingleElement { + it.behandlingssteg == Behandlingssteg.BREVMOTTAKER && + it.behandlingsstegsstatus == Behandlingsstegstatus.TILBAKEFØRT + } + } + + @Test + fun `skal hente og legge til navn fra registeroppslag når request inneholder identinformasjon`() { + val requestMedPersonIdent = manuellBrevmottakerRequestDto.copy( + personIdent = "12345678910", + manuellAdresseInfo = null, + ) + manuellBrevmottakerService.leggTilBrevmottaker(behandling.id, requestMedPersonIdent) + + var lagretMottaker = manuellBrevmottakerService.hentBrevmottakere(behandling.id).single() + lagretMottaker.navn shouldBe mockPdlClient.hentPersoninfo("12345678910", Fagsystem.BA).navn + + val requestMedOrgnrUtenKontaktperson = manuellBrevmottakerRequestDto.copy( + navn = " ", + organisasjonsnummer = "123456789", + manuellAdresseInfo = null, + ) + manuellBrevmottakerService.oppdaterBrevmottaker(behandling.id, lagretMottaker.id, requestMedOrgnrUtenKontaktperson) + + lagretMottaker = manuellBrevmottakerService.hentBrevmottakere(behandling.id).single() + lagretMottaker.navn shouldBe "Organisasjon AS" + + val requestMedOrgnrMedKontaktperson = manuellBrevmottakerRequestDto.copy( + organisasjonsnummer = "123456789", + manuellAdresseInfo = null, + ) + manuellBrevmottakerService.oppdaterBrevmottaker(behandling.id, lagretMottaker.id, requestMedOrgnrMedKontaktperson) + + lagretMottaker = manuellBrevmottakerService.hentBrevmottakere(behandling.id).single() + lagretMottaker.navn shouldBe "Organisasjon AS v/ ${manuellBrevmottakerRequestDto.navn}" + } + + private fun assertEqualsManuellBrevmottaker(a: ManuellBrevmottaker, b: ManuellBrevmottakerRequestDto) { + a.id.shouldNotBeNull() + a.orgNr shouldBe b.organisasjonsnummer + a.ident shouldBe b.personIdent + a.type shouldBe b.type + a.vergetype shouldBe b.vergetype + a.adresselinje1 shouldBe b.manuellAdresseInfo?.adresselinje1 + a.adresselinje2 shouldBe b.manuellAdresseInfo?.adresselinje2 + a.postnummer shouldBe b.manuellAdresseInfo?.postnummer + a.poststed shouldBe b.manuellAdresseInfo?.poststed + a.landkode shouldBe b.manuellAdresseInfo?.landkode + } + + private fun lagBehandlingsstegstilstand( + behandlingId: UUID, + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ) { + behandlingsstegstilstandRepository.insert( + Behandlingsstegstilstand( + behandlingId = behandlingId, + behandlingssteg = behandlingssteg, + behandlingsstegsstatus = behandlingsstegstatus, + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/TekstformatererVarselbrevTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/TekstformatererVarselbrevTest.kt new file mode 100644 index 000000000..15352008b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/TekstformatererVarselbrevTest.kt @@ -0,0 +1,348 @@ +package no.nav.familie.tilbake.dokumentbestilling.varsel + +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.felles.header.Institusjon +import no.nav.familie.tilbake.dokumentbestilling.varsel.handlebars.dto.FeilutbetaltPeriode +import no.nav.familie.tilbake.dokumentbestilling.varsel.handlebars.dto.Varselbrevsdokument +import no.nav.familie.tilbake.dokumentbestilling.varsel.handlebars.dto.Vedleggsdata +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.nio.charset.StandardCharsets +import java.time.LocalDate +import java.time.YearMonth +import java.util.Scanner + +class TekstformatererVarselbrevTest { + + private val metadata = Brevmetadata( + sakspartId = "123456", + sakspartsnavn = "Test", + mottageradresse = lagAdresseinfo(), + behandlendeEnhetsNavn = "NAV Familie- og pensjonsytelser Skien", + ansvarligSaksbehandler = "Bob", + saksnummer = "1232456", + språkkode = Språkkode.NB, + ytelsestype = Ytelsestype.OVERGANGSSTØNAD, + gjelderDødsfall = false, + ) + + private val varselbrevsdokument = + Varselbrevsdokument( + varseltekstFraSaksbehandler = "Dette er fritekst skrevet av saksbehandler.", + beløp = 595959L, + feilutbetaltePerioder = lagFeilutbetalingerMedKunEnPeriode(), + fristdatoForTilbakemelding = LocalDate.of(2020, 4, 4), + revurderingsvedtaksdato = LocalDate.of(2019, 12, 18), + brevmetadata = metadata, + ) + + @Test + fun `lagVarselbrevsfritekst skal generere varseltekst for flere perioder overgangsstønad`() { + val metadata = metadata.copy(språkkode = Språkkode.NN) + val varselbrevsdokument = varselbrevsdokument.copy( + brevmetadata = metadata, + feilutbetaltePerioder = lagFeilutbetalingerMedFlerePerioder(), + ) + val generertBrev = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument, false) + val fasit = les("/varselbrev/OS_flere_perioder_nn.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVarselbrevsfritekst skal generere varseltekst for enkelt periode overgangsstønad`() { + val varselbrevsdokument = varselbrevsdokument.copy(feilutbetaltePerioder = lagFeilutbetalingerMedKunEnPeriode()) + val generertBrev = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument, false) + val fasit = les("/varselbrev/OS_en_periode.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVarselbrevsfritekst skal generere varseltekst for enkelt periode institusjon overgangsstønad`() { + val metadata = metadata.copy(institusjon = Institusjon("test", "test")) + val varselbrevsdokument = varselbrevsdokument.copy( + feilutbetaltePerioder = lagFeilutbetalingerMedKunEnPeriode(), + brevmetadata = metadata, + ) + val generertBrev = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument, false) + val fasit = les("/varselbrev/OS_en_periode_institusjon.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVarselbrevsfritekst skal generere korrigert varseltekst for enkelt periode institusjon overgangsstønad`() { + val metadata = metadata.copy(institusjon = Institusjon("test", "test")) + val varselbrevsdokument = varselbrevsdokument.copy( + feilutbetaltePerioder = lagFeilutbetalingerMedKunEnPeriode(), + brevmetadata = metadata, + varsletDato = LocalDate.of(2023, 9, 26), + varsletBeløp = 5000, + ) + val generertBrev = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument = varselbrevsdokument, erKorrigert = true) + val fasit = les("/varselbrev/OS_en_periode_korrigert_institusjon.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVarselbrevsfritekst skal generere korrigert varseltekst for enkelt periode institusjon overgangsstønad nynorsk`() { + val metadata = metadata.copy(institusjon = Institusjon("test", "test"), språkkode = Språkkode.NN) + val varselbrevsdokument = varselbrevsdokument.copy( + feilutbetaltePerioder = lagFeilutbetalingerMedKunEnPeriode(), + brevmetadata = metadata, + varsletDato = LocalDate.of(2023, 9, 26), + varsletBeløp = 5000, + ) + val generertBrev = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument = varselbrevsdokument, erKorrigert = true) + val fasit = les("/varselbrev/OS_en_periode_korrigert_institusjon_nn.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVarselbrevsfritekst skal generere varseltekst for enkelt periode institusjon overgangsstønad nynorsk`() { + val metadata = metadata.copy(institusjon = Institusjon("test", "test"), språkkode = Språkkode.NN) + val varselbrevsdokument = varselbrevsdokument.copy( + feilutbetaltePerioder = lagFeilutbetalingerMedKunEnPeriode(), + brevmetadata = metadata, + ) + val generertBrev = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument, false) + val fasit = les("/varselbrev/OS_en_periode_institusjon_nn.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVarselbrevsfritekst skal generere varseltekst i tredje person ved dødsfall nynorsk`() { + val metadata = metadata.copy(gjelderDødsfall = true, språkkode = Språkkode.NN) + val varselbrevsdokument = + varselbrevsdokument.copy( + brevmetadata = metadata, + feilutbetaltePerioder = lagFeilutbetalingerMedFlerePerioder(), + ) + val generertBrev = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument, false) + val fasit = les("/varselbrev/OS_flere_perioder_dødsfall_nn.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVarselbrevsfritekst skal generere varseltekst i tredje person ved dødsfall bokmål`() { + val metadata = metadata.copy(gjelderDødsfall = true) + val varselbrevsdokument = + varselbrevsdokument.copy( + brevmetadata = metadata, + feilutbetaltePerioder = lagFeilutbetalingerMedKunEnPeriode(), + ) + val generertBrev = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument, false) + val fasit = les("/varselbrev/OS_en_periode_dødsfall.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVarselbrevsfritekst skal generere varseltekst for enkelt periode barnetrygd`() { + val metadata = metadata.copy(ytelsestype = Ytelsestype.BARNETRYGD) + val varselbrevsdokument = varselbrevsdokument.copy( + brevmetadata = metadata, + feilutbetaltePerioder = lagFeilutbetalingerMedKunEnPeriode(), + ) + val generertBrev = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument, false) + val fasit = les("/varselbrev/BA_en_periode.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVarselbrevsfritekst skal generere varseltekst for enkelt periode kontantstøtte`() { + val metadata = metadata.copy(ytelsestype = Ytelsestype.KONTANTSTØTTE) + val varselbrevsdokument = varselbrevsdokument.copy( + brevmetadata = metadata, + feilutbetaltePerioder = lagFeilutbetalingerMedKunEnPeriode(), + ) + val generertBrev = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument, false) + val fasit = les("/varselbrev/KS_en_periode.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVarselbrevsoverskrift skal generere varselbrevsoverskrift`() { + val overskrift = TekstformatererVarselbrev.lagVarselbrevsoverskrift(metadata, false) + val fasit = "NAV vurderer om du må betale tilbake overgangsstønad" + overskrift shouldBe fasit + } + + @Test + fun `lagVarselbrevsoverskrift skal generere varselbrevsoverskrift nynorsk`() { + val brevMetadata = metadata.copy(språkkode = Språkkode.NN) + val overskrift = TekstformatererVarselbrev.lagVarselbrevsoverskrift(brevMetadata, false) + val fasit = "NAV vurderer om du må betale tilbake overgangsstønad" + overskrift shouldBe fasit + } + + @Test + fun `lagVarselbrevsoverskrift skal generere varselbrevsoverskrift institusjon`() { + val brevMetadata = metadata.copy(institusjon = Institusjon("test", "test")) + val overskrift = TekstformatererVarselbrev.lagVarselbrevsoverskrift(brevMetadata, false) + val fasit = "NAV vurderer om institusjonen må betale tilbake overgangsstønad" + overskrift shouldBe fasit + } + + @Test + fun `lagVarselbrevsoverskrift skal generere varselbrevsoverskrift institusjon nynorsk`() { + val brevMetadata = metadata.copy(institusjon = Institusjon("test", "test"), språkkode = Språkkode.NN) + val overskrift = TekstformatererVarselbrev.lagVarselbrevsoverskrift(brevMetadata, false) + val fasit = "NAV vurderer om institusjonen må betale tilbake overgangsstønad" + overskrift shouldBe fasit + } + + @Test + fun `lagKorrigertVarselbrevsoverskrift skal generere korrigert varselbrevsoverskrift`() { + val overskrift = TekstformatererVarselbrev.lagVarselbrevsoverskrift(metadata, true) + val fasit = "Korrigert varsel om feilutbetalt overgangsstønad" + overskrift shouldBe fasit + } + + @Test + fun `lagKorrigertVarselbrevsoverskrift skal generere korrigert varselbrevsoverskrift nynorsk`() { + val brevMetadata = metadata.copy(språkkode = Språkkode.NN) + val overskrift = TekstformatererVarselbrev.lagVarselbrevsoverskrift(brevMetadata, true) + val fasit = "Korrigert varsel om feilutbetalt overgangsstønad" + overskrift shouldBe fasit + } + + @Test + fun `lagVarselbrevsfritekst skal generere varselbrev for verge`() { + val metadata = metadata.copy( + ytelsestype = Ytelsestype.BARNETRYGD, + vergenavn = "John Doe", + finnesVerge = true, + finnesAnnenMottaker = true, + språkkode = Språkkode.NB, + ) + val varselbrevsdokument = varselbrevsdokument.copy( + brevmetadata = metadata, + feilutbetaltePerioder = lagFeilutbetalingerMedKunEnPeriode(), + ) + val generertBrev = TekstformatererVarselbrev.lagFritekst(varselbrevsdokument, false) + val fasit = les("/varselbrev/BA_en_periode.txt") + val vergeTekst = les("/varselbrev/verge.txt") + generertBrev shouldBe "$fasit${System.lineSeparator().repeat(2)}$vergeTekst" + } + + @Test + fun `lagVarselbrevsvedleggHtml skal lage oversikt over varselet uten skatt på bokmål`() { + val vedleggsdata = Vedleggsdata( + Språkkode.NB, + false, + listOf( + FeilutbetaltPeriode( + YearMonth.of(2022, 1), + BigDecimal(1572), + BigDecimal(1573), + BigDecimal(1574), + ), + ), + ) + + val html = TekstformatererVarselbrev.lagVarselbrevsvedleggHtml(vedleggsdata) + + val fasit = les("/varselbrev/vedlegg/vedlegg_nb_uten_skatt.txt") + html shouldBe fasit + } + + @Test + fun `lagVarselbrevsvedleggHtml skal lage oversikt over varselet uten skatt på nynorsk`() { + val vedleggsdata = Vedleggsdata( + Språkkode.NN, + false, + listOf( + FeilutbetaltPeriode( + YearMonth.of(2022, 1), + BigDecimal(1572), + BigDecimal(1573), + BigDecimal(1574), + ), + ), + ) + + val html = TekstformatererVarselbrev.lagVarselbrevsvedleggHtml(vedleggsdata) + + val fasit = les("/varselbrev/vedlegg/vedlegg_nn_uten_skatt.txt") + html shouldBe fasit + } + + @Test + fun `lagVarselbrevsvedleggHtml skal lage oversikt over varselet med skatt på bokmål`() { + val vedleggsdata = Vedleggsdata( + Språkkode.NB, + true, + listOf( + FeilutbetaltPeriode( + YearMonth.of(2022, 1), + BigDecimal(1572), + BigDecimal(1573), + BigDecimal(1574), + ), + ), + ) + + val html = TekstformatererVarselbrev.lagVarselbrevsvedleggHtml(vedleggsdata) + + val fasit = les("/varselbrev/vedlegg/vedlegg_nb_med_skatt.txt") + html shouldBe fasit + } + + @Test + fun `lagVarselbrevsvedleggHtml skal lage oversikt over varselet med skatt på nynorsk`() { + val vedleggsdata = Vedleggsdata( + Språkkode.NN, + true, + listOf( + FeilutbetaltPeriode( + YearMonth.of(2022, 1), + BigDecimal(1572), + BigDecimal(1573), + BigDecimal(1574), + ), + ), + ) + + val html = TekstformatererVarselbrev.lagVarselbrevsvedleggHtml(vedleggsdata) + + val fasit = les("/varselbrev/vedlegg/vedlegg_nn_med_skatt.txt") + html shouldBe fasit + } + + private fun lagFeilutbetalingerMedFlerePerioder(): List { + val periode1 = Datoperiode( + LocalDate.of(2019, 3, 3), + LocalDate.of(2020, 3, 3), + ) + val periode2 = Datoperiode( + LocalDate.of(2022, 3, 3), + LocalDate.of(2024, 3, 3), + ) + return listOf(periode1, periode2) + } + + private fun lagFeilutbetalingerMedKunEnPeriode(): List { + return listOf( + Datoperiode( + LocalDate.of(2019, 3, 3), + LocalDate.of(2020, 3, 3), + ), + ) + } + + private fun les(filnavn: String): String? { + javaClass.getResourceAsStream(filnavn).use { resource -> + Scanner(resource, StandardCharsets.UTF_8).use { scanner -> + scanner.useDelimiter("\\A") + return if (scanner.hasNext()) scanner.next() else null + } + } + } + + private fun lagAdresseinfo(): Adresseinfo { + return Adresseinfo("123456", "Test") + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/VarselbrevServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/VarselbrevServiceTest.kt new file mode 100644 index 000000000..8d6d6becf --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/VarselbrevServiceTest.kt @@ -0,0 +1,90 @@ +package no.nav.familie.tilbake.dokumentbestilling.varsel + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.FeilutbetaltePerioderDto +import no.nav.familie.kontrakter.felles.tilbakekreving.ForhåndsvisVarselbrevRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Periode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Verge +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.DistribusjonshåndteringService +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.EksterneDataForBrevService +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.PdfBrevService +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import no.nav.familie.tilbake.pdfgen.validering.PdfaValidator +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +internal class VarselbrevServiceTest : OppslagSpringRunnerTest() { + + private val fagsakRepository: FagsakRepository = mockk() + private val eksterneDataForBrevService: EksterneDataForBrevService = mockk(relaxed = true) + private val distribusjonshåndteringService: DistribusjonshåndteringService = mockk() + + @Autowired + private lateinit var pdfBrevService: PdfBrevService + + @Autowired + private lateinit var varselbrevUtil: VarselbrevUtil + + private lateinit var varselbrevService: VarselbrevService + + @BeforeEach + fun init() { + varselbrevService = VarselbrevService( + fagsakRepository, + eksterneDataForBrevService, + pdfBrevService, + varselbrevUtil, + distribusjonshåndteringService, + ) + + val personinfo = Personinfo("28056325874", LocalDate.now(), "Fiona") + + every { eksterneDataForBrevService.hentPerson(Testdata.fagsak.bruker.ident, any()) }.returns(personinfo) + every { + eksterneDataForBrevService.hentAdresse(any(), any(), any(), any()) + }.returns(Adresseinfo("12345678901", "Test")) + } + + @Test + fun hentForhåndsvisningVarselbrev() { + val forhåndsvisVarselbrevRequest = + ForhåndsvisVarselbrevRequest( + "Dette er et varsel!", + Ytelsestype.BARNETRYGD, + "1570", + "Bodø", + "321321", + Språkkode.NN, + LocalDate.now(), + FeilutbetaltePerioderDto( + 157468, + listOf( + Periode( + LocalDate.of(2020, 5, 4), + LocalDate.now(), + ), + ), + ), + Fagsystem.EF, + "321654", + Testdata.fagsak.bruker.ident, + null, + fagsystemsbehandlingId = "123", + ) + + val bytes = varselbrevService.hentForhåndsvisningVarselbrev(forhåndsvisVarselbrevRequest) +// File("test.pdf").writeBytes(bytes) + + PdfaValidator.validatePdf(bytes) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/manuelt/ManueltVarselbrevServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/manuelt/ManueltVarselbrevServiceTest.kt new file mode 100644 index 000000000..c328256a1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/varsel/manuelt/ManueltVarselbrevServiceTest.kt @@ -0,0 +1,235 @@ +package no.nav.familie.tilbake.dokumentbestilling.varsel.manuelt + +import io.mockk.every +import io.mockk.excludeRecords +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.FaktaFeilutbetalingDto +import no.nav.familie.tilbake.api.dto.FeilutbetalingsperiodeDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Varsel +import no.nav.familie.tilbake.behandling.domain.Verge +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.DistribusjonshåndteringService +import no.nav.familie.tilbake.dokumentbestilling.brevmaler.Dokumentmalstype +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevmetadataUtil +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.EksterneDataForBrevService +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.PdfBrevService +import no.nav.familie.tilbake.dokumentbestilling.varsel.VarselbrevUtil +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingService +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import no.nav.familie.tilbake.pdfgen.validering.PdfaValidator +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate + +class ManueltVarselbrevServiceTest : OppslagSpringRunnerTest() { + + private val korrigertVarseltekst = "Sender korrigert varselbrev" + private val varseltekst = "Sender manuelt varselbrev" + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var pdfBrevService: PdfBrevService + + @Autowired + private lateinit var varselbrevUtil: VarselbrevUtil + + @Autowired + private lateinit var eksterneDataForBrevService: EksterneDataForBrevService + + private val mockEksterneDataForBrevService: EksterneDataForBrevService = mockk() + private val mockFeilutbetalingService: FaktaFeilutbetalingService = mockk() + private val mockDistribusjonshåndteringService: DistribusjonshåndteringService = mockk() + private lateinit var spyPdfBrevService: PdfBrevService + private lateinit var manueltVarselbrevService: ManueltVarselbrevService + private var behandling = Testdata.behandling + private var fagsak = Testdata.fagsak + private lateinit var brevmetadataUtil: BrevmetadataUtil + private val featureToggleService = mockk(relaxed = true) + + @BeforeEach + fun setup() { + spyPdfBrevService = spyk(pdfBrevService) + + brevmetadataUtil = BrevmetadataUtil( + behandlingRepository = behandlingRepository, + fagsakRepository = fagsakRepository, + manuelleBrevmottakerRepository = mockk(relaxed = true), + eksterneDataForBrevService = mockEksterneDataForBrevService, + organisasjonService = mockk(), + featureToggleService = featureToggleService, + ) + manueltVarselbrevService = ManueltVarselbrevService( + behandlingRepository, + fagsakRepository, + mockEksterneDataForBrevService, + spyPdfBrevService, + mockFeilutbetalingService, + varselbrevUtil, + mockDistribusjonshåndteringService, + brevmetadataUtil, + ) + + every { mockFeilutbetalingService.hentFaktaomfeilutbetaling(any()) } + .returns(lagFeilutbetaling()) + val personinfo = Personinfo("DUMMY_FØDSELSNUMMER", LocalDate.now(), "Fiona") + val ident: String = Testdata.fagsak.bruker.ident + every { mockEksterneDataForBrevService.hentPerson(ident, any()) }.returns(personinfo) + every { + mockEksterneDataForBrevService.hentAdresse(any(), any(), any(), any()) + }.returns(Adresseinfo("12345678901", "Test")) + every { mockEksterneDataForBrevService.hentPåloggetSaksbehandlernavnMedDefault(any()) } returns + eksterneDataForBrevService.hentPåloggetSaksbehandlernavnMedDefault(behandling.ansvarligSaksbehandler) + + fagsak = fagsakRepository.insert(fagsak) + behandling = behandlingRepository.insert(behandling) + } + + @Test + fun `sendManueltVarselBrev skal sende manuelt varselbrev`() { + manueltVarselbrevService.sendManueltVarselBrev(behandling, varseltekst, Brevmottager.BRUKER) + verify { + spyPdfBrevService.sendBrev( + eq(behandling), + eq(fagsak), + eq(Brevtype.VARSEL), + any(), + eq(9000L), + any(), + ) + } + } + + @Test + fun `sendKorrigertVarselBrev skal sende korrigert varselbrev`() { + excludeRecords { spyPdfBrevService.sendBrev(eq(behandling), eq(fagsak), eq(Brevtype.VARSEL), any(), any(), any()) } + manueltVarselbrevService.sendManueltVarselBrev(behandling, varseltekst, Brevmottager.BRUKER) + val behandlingCopy = behandling.copy( + varsler = setOf( + Varsel( + varseltekst = varseltekst, + varselbeløp = 100L, + ), + ), + ) + val behandling = behandlingRepository.update(behandlingCopy) + + manueltVarselbrevService.sendKorrigertVarselBrev(behandling, korrigertVarseltekst, Brevmottager.BRUKER) + + verify { + spyPdfBrevService.sendBrev( + eq(behandling), + eq(fagsak), + eq(Brevtype.KORRIGERT_VARSEL), + any(), + eq(9000L), + any(), + ) + } + } + + @Test + fun `sendKorrigertVarselBrev skal sende korrigert varselbrev med verge`() { + excludeRecords { spyPdfBrevService.sendBrev(eq(behandling), eq(fagsak), eq(Brevtype.VARSEL), any(), any(), any()) } + manueltVarselbrevService.sendManueltVarselBrev(behandling, varseltekst, Brevmottager.BRUKER) + val behandlingCopy = behandling.copy( + varsler = setOf( + Varsel( + varseltekst = varseltekst, + varselbeløp = 100L, + ), + ), + verger = setOf(Testdata.verge), + ) + val behandling = behandlingRepository.update(behandlingCopy) + + manueltVarselbrevService.sendKorrigertVarselBrev(behandling, varseltekst, Brevmottager.VERGE) + + verify { + spyPdfBrevService.sendBrev( + eq(behandling), + eq(fagsak), + eq(Brevtype.KORRIGERT_VARSEL), + any(), + eq(9000L), + any(), + ) + } + } + + @Test + fun `hentForhåndsvisningManueltVarselbrev skal forhåndsvise manuelt varselbrev`() { + val data = manueltVarselbrevService.hentForhåndsvisningManueltVarselbrev( + behandling.id, + Dokumentmalstype.VARSEL, + varseltekst, + ) + + PdfaValidator.validatePdf(data) + } + + @Test + fun `hentForhåndsvisningManueltVarselbrev skal forhåndsvise korrigert varselbrev`() { + val behandlingCopy = behandling.copy( + varsler = setOf( + Varsel( + varseltekst = varseltekst, + varselbeløp = 100L, + ), + ), + ) + behandlingRepository.update(behandlingCopy) + + val data = manueltVarselbrevService.hentForhåndsvisningManueltVarselbrev( + behandling.id, + Dokumentmalstype.KORRIGERT_VARSEL, + varseltekst, + ) + + PdfaValidator.validatePdf(data) + } + + private fun lagFeilutbetaling(): FaktaFeilutbetalingDto { + val periode = Månedsperiode( + LocalDate.of(2019, 10, 1), + LocalDate.of(2019, 10, 30), + ) + + return FaktaFeilutbetalingDto( + totaltFeilutbetaltBeløp = BigDecimal(9000), + totalFeilutbetaltPeriode = periode.toDatoperiode(), + feilutbetaltePerioder = listOf( + FeilutbetalingsperiodeDto( + periode.toDatoperiode(), + BigDecimal(9000), + ), + ), + revurderingsvedtaksdato = LocalDate.now().minusDays(1), + begrunnelse = "", + faktainfo = Faktainfo( + revurderingsårsak = "testverdi", + revurderingsresultat = "testverdi", + tilbakekrevingsvalg = + Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/AvsnittUtilTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/AvsnittUtilTest.kt new file mode 100644 index 000000000..a5a828c5d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/AvsnittUtilTest.kt @@ -0,0 +1,311 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbBehandling +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbHjemmel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbKonfigurasjon +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbPerson +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbTotalresultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVarsel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevsdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbFakta +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbKravgrunnlag +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbResultatTestBuilder +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbSærligeGrunner +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVedtaksbrevsperiode +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVurderinger +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate + +class AvsnittUtilTest { + + private val januar = Datoperiode(LocalDate.of(2019, 1, 1), LocalDate.of(2019, 1, 31)) + private val februar = Datoperiode(LocalDate.of(2019, 2, 1), LocalDate.of(2019, 2, 28)) + + private val brevmetadata = Brevmetadata( + sakspartId = "123456", + sakspartsnavn = "Test", + mottageradresse = Adresseinfo("ident", "bob"), + behandlendeEnhetsNavn = "NAV Familie- og pensjonsytelser Skien", + ansvarligSaksbehandler = "Bob", + saksnummer = "1232456", + språkkode = Språkkode.NB, + ytelsestype = Ytelsestype.BARNETRYGD, + gjelderDødsfall = false, + ) + + private val vedtaksbrevFelles = HbVedtaksbrevFelles( + brevmetadata = brevmetadata, + konfigurasjon = HbKonfigurasjon(klagefristIUker = 4), + søker = HbPerson( + navn = "Søker Søkersen", + ), + fagsaksvedtaksdato = LocalDate.now(), + behandling = HbBehandling(erRevurdering = false), + totalresultat = HbTotalresultat( + Vedtaksresultat.DELVIS_TILBAKEBETALING, + BigDecimal(23002), + BigDecimal(23002), + BigDecimal(23002), + BigDecimal.ZERO, + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + totaltFeilutbetaltBeløp = BigDecimal.valueOf(20000), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ansvarligBeslutter = "ansvarlig person sin signatur", + ) + + @Test + fun `lagVedtaksbrevDeltIAvsnitt skal generere brev delt i avsnitt og underavsnitt`() { + val vedtaksbrevData = vedtaksbrevFelles.copy( + fagsaksvedtaksdato = LocalDate.now(), + behandling = HbBehandling(erRevurdering = false), + totalresultat = HbTotalresultat( + Vedtaksresultat.DELVIS_TILBAKEBETALING, + BigDecimal(23002), + BigDecimal(23002), + BigDecimal(23002), + BigDecimal.ZERO, + ), + fritekstoppsummering = "Her finner du friteksten til oppsummeringen", + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(33001), + varsletDato = LocalDate.of(2020, 4, 4), + ), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + + val perioder = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(30001)), + fakta = HbFakta(Hendelsestype.ANNET, Hendelsesundertype.ANNET_FRITEKST), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat + .MANGELFULLE_OPPLYSNINGER_FRA_BRUKER, + aktsomhetsresultat = Aktsomhet.SIMPEL_UAKTSOMHET, + fritekst = "Du er heldig som slapp å betale alt!", + særligeGrunner = + HbSærligeGrunner( + listOf( + SærligGrunn.TID_FRA_UTBETALING, + SærligGrunn.STØRRELSE_BELØP, + SærligGrunn.ANNET, + ), + "Fritekst særlige grunner", + "Fritekst særlige grunner annet", + ), + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(20002), + førstePeriode = true, + ), + HbVedtaksbrevsperiode( + periode = februar, + kravgrunnlag = HbKravgrunnlag( + feilutbetaltBeløp = BigDecimal(3000), + riktigBeløp = BigDecimal(3000), + utbetaltBeløp = BigDecimal(6000), + ), + fakta = HbFakta(Hendelsestype.BOR_MED_SØKER, Hendelsesundertype.BOR_IKKE_MED_BARN), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat + .FORSTO_BURDE_FORSTÅTT, + aktsomhetsresultat = Aktsomhet.SIMPEL_UAKTSOMHET, + særligeGrunner = + HbSærligeGrunner( + listOf( + SærligGrunn.HELT_ELLER_DELVIS_NAVS_FEIL, + SærligGrunn.STØRRELSE_BELØP, + ), + ), + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(3000), + førstePeriode = true, + ), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val resultat = AvsnittUtil.lagVedtaksbrevDeltIAvsnitt(data, "Du må betale tilbake overgangsstønaden") + + resultat.shouldHaveSize(4) + resultat[0].avsnittstype shouldBe Avsnittstype.OPPSUMMERING + resultat[0].underavsnittsliste.shouldHaveSize(2) + resultat[0].underavsnittsliste[0].fritekstTillatt shouldBe false + resultat[1].underavsnittsliste[0].fritekstTillatt shouldBe true + resultat[1].avsnittstype shouldBe Avsnittstype.PERIODE + resultat[1].underavsnittsliste.shouldHaveSize(7) + resultat[1].underavsnittsliste.filter { it.fritekstTillatt }.size shouldBe 4 + resultat[2].avsnittstype shouldBe Avsnittstype.PERIODE + resultat[2].underavsnittsliste.shouldHaveSize(7) + resultat[2].underavsnittsliste.filter { it.fritekstTillatt }.size shouldBe 3 + resultat[3].avsnittstype shouldBe Avsnittstype.TILLEGGSINFORMASJON + resultat[3].underavsnittsliste.shouldHaveSize(14) + resultat[3].underavsnittsliste.forEach { it.fritekstTillatt shouldBe false } + } + + @Test + fun `parseTekst skal parse tekst til avsnitt`() { + val tekst = "_Hovedoverskrift i brevet\n\n" + + "Brødtekst første avsnitt\n\n" + + "Brødtekst andre avsnitt\n\n" + + "_underoverskrift\n\n" + + "Brødtekst tredje avsnitt\n\n" + + "_Avsluttende overskrift uten etterfølgende tekst\n" + Vedtaksbrevsfritekst.markerValgfriFritekst(null) + + val resultat = AvsnittUtil.parseTekst(tekst, Avsnitt(), null) + + resultat.overskrift shouldBe "Hovedoverskrift i brevet" + val underavsnitt: List = resultat.underavsnittsliste + underavsnitt.shouldHaveSize(4) + underavsnitt[0].overskrift shouldBe null + underavsnitt[0].brødtekst shouldBe "Brødtekst første avsnitt" + underavsnitt[0].fritekstTillatt.shouldBeFalse() + underavsnitt[1].overskrift shouldBe null + underavsnitt[1].brødtekst shouldBe "Brødtekst andre avsnitt" + underavsnitt[1].fritekstTillatt.shouldBeFalse() + underavsnitt[2].overskrift shouldBe "underoverskrift" + underavsnitt[2].brødtekst shouldBe "Brødtekst tredje avsnitt" + underavsnitt[2].fritekstTillatt.shouldBeFalse() + underavsnitt[3].overskrift shouldBe "Avsluttende overskrift uten etterfølgende tekst" + underavsnitt[3].brødtekst shouldBe null + underavsnitt[3].fritekstTillatt.shouldBeTrue() + } + + @Test + fun `parseTekst skal plassere fritekstfelt etter første avsnitt når det er valgt`() { + val tekst = "_Hovedoverskrift i brevet\n\n" + + "Brødtekst første avsnitt\n" + + "${Vedtaksbrevsfritekst.markerValgfriFritekst(null)}\n" + + "_underoverskrift\n\n" + + "Brødtekst andre avsnitt\n\n" + + "_Avsluttende overskrift uten etterfølgende tekst" + + val resultat = AvsnittUtil.parseTekst(tekst, Avsnitt(), null) + + resultat.overskrift shouldBe "Hovedoverskrift i brevet" + val underavsnitt: List = resultat.underavsnittsliste + underavsnitt.shouldHaveSize(4) + underavsnitt[0].overskrift shouldBe null + underavsnitt[0].brødtekst shouldBe "Brødtekst første avsnitt" + underavsnitt[0].fritekstTillatt.shouldBeFalse() + underavsnitt[1].overskrift shouldBe null + underavsnitt[1].brødtekst shouldBe null + underavsnitt[1].fritekstTillatt.shouldBeTrue() + underavsnitt[2].overskrift shouldBe "underoverskrift" + underavsnitt[2].brødtekst shouldBe "Brødtekst andre avsnitt" + underavsnitt[2].fritekstTillatt.shouldBeFalse() + underavsnitt[3].overskrift shouldBe "Avsluttende overskrift uten etterfølgende tekst" + underavsnitt[3].brødtekst shouldBe null + underavsnitt[3].fritekstTillatt.shouldBeFalse() + } + + @Test + fun `parseTekst skal plassere fritekstfelt etter overskriften når det er valgt`() { + val avsnitt = Avsnitt(overskrift = "Hovedoverskrift") + val tekst = "_underoverskrift 1\n" + + "${Vedtaksbrevsfritekst.markerValgfriFritekst(null)}\n" + + "Brødtekst første avsnitt\n\n" + + "_underoverskrift 2\n\n" + + "Brødtekst andre avsnitt" + + val resultat = AvsnittUtil.parseTekst(tekst, avsnitt, null) + + resultat.overskrift shouldBe "Hovedoverskrift" + val underavsnitt: List = resultat.underavsnittsliste + underavsnitt.shouldHaveSize(3) + underavsnitt[0].overskrift shouldBe "underoverskrift 1" + underavsnitt[0].brødtekst shouldBe null + underavsnitt[0].fritekstTillatt.shouldBeTrue() + underavsnitt[1].overskrift shouldBe null + underavsnitt[1].brødtekst shouldBe "Brødtekst første avsnitt" + underavsnitt[1].fritekstTillatt.shouldBeFalse() + underavsnitt[2].overskrift shouldBe "underoverskrift 2" + underavsnitt[2].brødtekst shouldBe "Brødtekst andre avsnitt" + underavsnitt[2].fritekstTillatt.shouldBeFalse() + } + + @Test + fun `parseTekst skal parse fritekstfelt med eksisterende fritekst`() { + val avsnitt = Avsnitt(overskrift = "Hovedoverskrift") + val tekst = "_underoverskrift 1\n${Vedtaksbrevsfritekst.markerValgfriFritekst("fritekst linje 1\nfritekst linje2")}" + + val resultat = AvsnittUtil.parseTekst(tekst, avsnitt, null) + + resultat.overskrift shouldBe "Hovedoverskrift" + val underavsnitt: List = resultat.underavsnittsliste + // underavsnitt.shouldHaveSize(1); + underavsnitt[0].overskrift shouldBe "underoverskrift 1" + underavsnitt[0].brødtekst shouldBe null + underavsnitt[0].fritekstTillatt.shouldBeTrue() + underavsnitt[0].fritekst shouldBe "fritekst linje 1\nfritekst linje2" + } + + @Test + fun `parseTekst skal skille mellom påkrevet og valgfritt fritekstfelt`() { + val avsnitt = Avsnitt(overskrift = "Hovedoverskrift") + val tekst = "_underoverskrift 1\n${Vedtaksbrevsfritekst.markerPåkrevetFritekst(null, null)}\n" + + "_underoverskrift 2\n${Vedtaksbrevsfritekst.markerValgfriFritekst(null)}" + + val resultat = AvsnittUtil.parseTekst(tekst, avsnitt, null) + + resultat.overskrift shouldBe "Hovedoverskrift" + val underavsnitt: List = resultat.underavsnittsliste + underavsnitt.shouldHaveSize(2) + underavsnitt[0].overskrift shouldBe "underoverskrift 1" + underavsnitt[0].brødtekst shouldBe null + underavsnitt[0].fritekstTillatt.shouldBeTrue() + underavsnitt[0].fritekstPåkrevet.shouldBeTrue() + underavsnitt[0].fritekst shouldBe "" + underavsnitt[1].overskrift shouldBe "underoverskrift 2" + underavsnitt[1].brødtekst shouldBe null + underavsnitt[1].fritekstTillatt.shouldBeTrue() + underavsnitt[1].fritekstPåkrevet.shouldBeFalse() + underavsnitt[1].fritekst shouldBe "" + } + + @Test + fun `parseTekst skal utlede underavsnittstype fra fritekstmarkering slik at det er mulig å skille mellom særlige grunner`() { + val avsnitt = Avsnitt(overskrift = "Hovedoverskrift") + val tekst = "_underoverskrift 1\n" + + Vedtaksbrevsfritekst.markerValgfriFritekst(null, Underavsnittstype.SÆRLIGEGRUNNER) + + "\n_underoverskrift 2\n" + + "brødtekst ${Vedtaksbrevsfritekst.markerValgfriFritekst(null, Underavsnittstype.SÆRLIGEGRUNNER_ANNET)}" + + "\n_underoverskrift 3" + + val resultat = AvsnittUtil.parseTekst(tekst, avsnitt, null) + + resultat.overskrift shouldBe "Hovedoverskrift" + val underavsnitt: List = resultat.underavsnittsliste + underavsnitt.shouldHaveSize(3) + underavsnitt[0].underavsnittstype shouldBe Underavsnittstype.SÆRLIGEGRUNNER + underavsnitt[1].underavsnittstype shouldBe Underavsnittstype.SÆRLIGEGRUNNER_ANNET + underavsnitt[1].brødtekst shouldBe "brødtekst " + underavsnitt[1].fritekstTillatt.shouldBeTrue() + underavsnitt[2].underavsnittstype shouldBe Underavsnittstype.SÆRLIGEGRUNNER_ANNET + underavsnitt[2].fritekstTillatt.shouldBeFalse() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/DokprodTilHtmlTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/DokprodTilHtmlTest.kt new file mode 100644 index 000000000..c93c6cd8c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/DokprodTilHtmlTest.kt @@ -0,0 +1,55 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.DokprodTilHtml +import org.junit.jupiter.api.Test + +class DokprodTilHtmlTest { + + @Test + fun `dokprodInnholdTilHtml skal konvertere overskrift og avsnitt`() { + val resultat: String = + DokprodTilHtml.dokprodInnholdTilHtml("_Overskrift\nFørste avsnitt\n\nAndre avsnitt\n\nTredje avsnitt") + + resultat shouldBe "

    Overskrift

    Første avsnitt

    " + + "

    Andre avsnitt

    Tredje avsnitt

    " + } + + @Test + fun `dokprodInnholdTilHtml skal konvertere non break space`() { + // utf8nonBreakingSpace = "\u00A0"; + val resultat: String = DokprodTilHtml.dokprodInnholdTilHtml("10\u00A0000\u00A0kroner") + + resultat shouldBe "

    10 000 kroner

    " + } + + @Test + fun `dokprodInnholdTilHtml skal konvertere bullet points`() { + val resultat: String = DokprodTilHtml.dokprodInnholdTilHtml("*-bulletpoint 1\nbulletpoint 2\nsiste bulletpoint-*") + + resultat shouldBe "
    • bulletpoint 1
    • bulletpoint 2
    • siste bulletpoint
    " + } + + @Test + fun `dokprodInnholdTilHtml skal konvertere bullet points når første linje er tom`() { + val resultat: String = DokprodTilHtml.dokprodInnholdTilHtml("*-\nbulletpoint 1\nbulletpoint 2\nsiste bulletpoint-*") + + resultat shouldBe "
    • bulletpoint 1
    • bulletpoint 2
    • siste bulletpoint
    " + } + + @Test + fun `dokprodInnholdTilHtml skal konvertere halvhjertede avsnitt`() { + // halvhjertet avsnitt er hvor det er tatt kun ett linjeskift. + val resultat: String = DokprodTilHtml.dokprodInnholdTilHtml("Foo\nBar") + + resultat shouldBe "

    Foo
    Bar

    " + } + + @Test + fun `dokprodInnholdTilHtml skal spesialbehandle hilsen`() { + // halvhjertet avsnitt er hvor det er tatt kun ett linjeskift. + val resultat: String = DokprodTilHtml.dokprodInnholdTilHtml("Med vennlig hilsen\nNAV Familie- og pensjonsytelser") + + resultat shouldBe "

    Med vennlig hilsen
    NAV Familie- og pensjonsytelser

    " + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/HbGrunnbel\303\270pUtilTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/HbGrunnbel\303\270pUtilTest.kt" new file mode 100644 index 000000000..4c0118e36 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/HbGrunnbel\303\270pUtilTest.kt" @@ -0,0 +1,51 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.dokumentbestilling.vedtak.HbGrunnbeløpUtil.lagHbGrunnbeløp +import org.junit.jupiter.api.Test +import java.time.YearMonth + +internal class HbGrunnbeløpUtilTest { + + @Test + internal fun `skal bruke fra og til fra perioden hvis de er innenfor perioden sine datoer `() { + val result = lagHbGrunnbeløp(Månedsperiode(YearMonth.of(2021, 3), YearMonth.of(2021, 3))) + + result.tekst6GangerGrunnbeløp shouldBe null + result.grunnbeløpGanger6 shouldBe 608_106.toBigDecimal() + } + + @Test + internal fun `skal bruke periode sin fom hvis den er større enn beløpsperiode sin startdato`() { + val result = lagHbGrunnbeløp(Månedsperiode(YearMonth.of(2021, 3), YearMonth.of(2021, 6))) + + result.tekst6GangerGrunnbeløp shouldBe "608 106 kroner for perioden 1. mars 2021 til 30. april 2021 og 638 394 kroner for perioden 1. mai 2021 til 30. juni 2021" + result.grunnbeløpGanger6 shouldBe null + } + + @Test + internal fun `skal bruke periode sin fom hvis den er større enn beløpsperiode sin startdato 2`() { + val result = lagHbGrunnbeløp(Månedsperiode(YearMonth.of(2021, 4), YearMonth.of(2021, 5))).tekst6GangerGrunnbeløp + + result shouldBe "608 106 kroner for perioden 1. april 2021 til 30. april 2021 " + + "og 638 394 kroner for perioden 1. mai 2021 til 31. mai 2021" + } + + @Test + internal fun `periode og beløpsperiode er lik - gir kun en periode`() { + val result = lagHbGrunnbeløp(Månedsperiode(YearMonth.of(2021, 5), YearMonth.of(2022, 4))) + + result.tekst6GangerGrunnbeløp shouldBe null + result.grunnbeløpGanger6 shouldBe 638_394.toBigDecimal() + } + + @Test + internal fun `perioden går over 3 grunnbeløpsperioder`() { + val result = lagHbGrunnbeløp(Månedsperiode(YearMonth.of(2021, 3), YearMonth.of(2022, 6))).tekst6GangerGrunnbeløp + + result shouldBe "608 106 kroner for perioden 1. mars 2021 til 30. april 2021, " + + "638 394 kroner for perioden 1. mai 2021 til 30. april 2022 " + + "og 668 862 kroner for perioden 1. mai 2022 til 30. juni 2022" + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/HendelseMedUndertype.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/HendelseMedUndertype.kt new file mode 100644 index 000000000..a4572f1ec --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/HendelseMedUndertype.kt @@ -0,0 +1,9 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype + +data class HendelseMedUndertype( + val hendelsestype: Hendelsestype, + val hendelsesundertype: Hendelsesundertype, +) diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrevAllePermutasjonerAvFaktaTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrevAllePermutasjonerAvFaktaTest.kt new file mode 100644 index 000000000..31ed92679 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrevAllePermutasjonerAvFaktaTest.kt @@ -0,0 +1,229 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.handlebars.FellesTekstformaterer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbBehandling +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbHjemmel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbKonfigurasjon +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbPerson +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbTotalresultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVarsel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevDatoer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevPeriodeOgFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbFakta +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbGrunnbeløp +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbKravgrunnlag +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbResultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVedtaksbrevsperiode +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVurderinger +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.HendelsestypePerYtelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.HendelsesundertypePerHendelsestype +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.vilkårsvurdering.domain.AnnenVurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDate +import java.util.TreeMap + +class TekstformatererVedtaksbrevAllePermutasjonerAvFaktaTest { + + private val januar = Datoperiode( + LocalDate.of(2019, 1, 1), + LocalDate.of(2019, 1, 31), + ) + + private val brevmetadata = Brevmetadata( + sakspartId = "123456", + sakspartsnavn = "Test", + mottageradresse = Adresseinfo("ident", "bob"), + behandlendeEnhetsNavn = "NAV Familie- og pensjonsytelser Skien", + ansvarligSaksbehandler = "Bob", + saksnummer = "1232456", + språkkode = Språkkode.NB, + ytelsestype = Ytelsestype.BARNETRYGD, + gjelderDødsfall = false, + ) + + @Test + fun `lagDeltekst skal støtte alle permutasjoner av fakta for EFOG`() { + lagTeksterOgValider( + Ytelsestype.OVERGANGSSTØNAD, + Språkkode.NB, + HendelseMedUndertype(Hendelsestype.STØNADSPERIODE, Hendelsesundertype.UTVIDELSE_UTDANNING), + ) + } + + @Test + fun `lagDeltekst skal støtte alle permutasjoner av fakta for EFOG nynorsk`() { + lagTeksterOgValider( + Ytelsestype.OVERGANGSSTØNAD, + Språkkode.NN, + HendelseMedUndertype(Hendelsestype.STØNADSPERIODE, Hendelsesundertype.UTVIDELSE_UTDANNING), + ) + } + + @Test + fun `lagDeltekst skal støtte alle permutasjoner av fakta for EFBT`() { + lagTeksterOgValider( + Ytelsestype.BARNETILSYN, + Språkkode.NB, + HendelseMedUndertype(Hendelsestype.STØNADSPERIODE, Hendelsesundertype.UTVIDELSE_UTDANNING), + ) + } + + @Test + fun `lagDeltekst skal støtte alle permutasjoner av fakta for EFBT nynorsk`() { + lagTeksterOgValider( + Ytelsestype.BARNETILSYN, + Språkkode.NN, + HendelseMedUndertype(Hendelsestype.STØNADSPERIODE, Hendelsesundertype.UTVIDELSE_UTDANNING), + ) + } + + @Test + fun `lagDeltekst skal støtte alle permutasjoner av fakta for EFSP`() { + lagTeksterOgValider( + Ytelsestype.SKOLEPENGER, + Språkkode.NB, + HendelseMedUndertype(Hendelsestype.STØNADSPERIODE, Hendelsesundertype.UTVIDELSE_UTDANNING), + ) + } + + @Test + fun `lagDeltekst skal støtte alle permutasjoner av fakta for EFSP nynorsk`() { + lagTeksterOgValider( + Ytelsestype.SKOLEPENGER, + Språkkode.NN, + HendelseMedUndertype(Hendelsestype.STØNADSPERIODE, Hendelsesundertype.UTVIDELSE_UTDANNING), + ) + } + + @Test + fun `lagDeltekst skal støtte alle permutasjoner av fakta for BA`() { + lagTeksterOgValider(Ytelsestype.BARNETRYGD, Språkkode.NB) + } + + @Test + fun `lagDeltekst skal støtte alle permutasjoner av fakta for BA nynorsk`() { + lagTeksterOgValider(Ytelsestype.BARNETRYGD, Språkkode.NN) + } + + @SafeVarargs + private fun lagTeksterOgValider( + ytelsestype: Ytelsestype, + språkkode: Språkkode, + vararg unntak: HendelseMedUndertype, + ) { + val felles: HbVedtaksbrevFelles = lagFellesBuilder(språkkode, ytelsestype) + + val resultat = lagFaktatekster(felles, ytelsestype) + sjekkVerdier(resultat, *unntak) + } + + private fun sjekkVerdier(verdier: Map, vararg unntattUnikhet: HendelseMedUndertype) { + val tekstTilHendelsestyper = TreeMap>() + verdier.filter { (key, _) -> key !in unntattUnikhet } + .forEach { (key, value) -> + if (tekstTilHendelsestyper.containsKey(value)) { + tekstTilHendelsestyper[value]!!.add(key) + } else { + val liste: MutableSet = HashSet() + liste.add(key) + tekstTilHendelsestyper[value] = liste + } + } + val feilmelding = tekstTilHendelsestyper.filter { (_, value) -> value.size > 1 }.map { (key, value) -> + """$value mapper alle til "$key""" + }.joinToString("\n") + + if (feilmelding.isNotEmpty()) { + throw AssertionError(feilmelding) + } + } + + private fun lagFaktatekster(felles: HbVedtaksbrevFelles, ytelsestype: Ytelsestype): Map { + val resultat: MutableMap = LinkedHashMap() + for (undertype in getFeilutbetalingsårsaker(ytelsestype)) { + val periode: HbVedtaksbrevsperiode = lagPeriodeBuilder(HbFakta(undertype.hendelsestype, undertype.hendelsesundertype)) + val data = HbVedtaksbrevPeriodeOgFelles(felles, periode) + val tekst = FellesTekstformaterer.lagDeltekst(data, AvsnittUtil.PARTIAL_PERIODE_FAKTA) + resultat[undertype] = tekst + } + return resultat + } + + private fun lagPeriodeBuilder(fakta: HbFakta): HbVedtaksbrevsperiode { + return HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag( + feilutbetaltBeløp = BigDecimal.valueOf(10000), + utbetaltBeløp = BigDecimal.valueOf(33333), + riktigBeløp = BigDecimal.valueOf(23333), + ), + fakta = fakta, + grunnbeløp = HbGrunnbeløp(null, "Seks ganger grunnbeløpet er 741 000 for perioden fra 01.05.2022"), + vurderinger = HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + aktsomhetsresultat = AnnenVurdering.GOD_TRO, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + beløpIBehold = BigDecimal.valueOf(10000), + ), + resultat = HbResultat( + tilbakekrevesBeløp = BigDecimal(10000), + rentebeløp = BigDecimal(1000), + tilbakekrevesBeløpUtenSkattMedRenter = BigDecimal(9000), + ), + førstePeriode = true, + ) + } + + private fun lagFellesBuilder(språkkode: Språkkode, ytelsestype: Ytelsestype) = + HbVedtaksbrevFelles( + brevmetadata = brevmetadata.copy(språkkode = språkkode, ytelsestype = ytelsestype), + hjemmel = HbHjemmel("Folketrygdloven"), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.FULL_TILBAKEBETALING, + totaltRentebeløp = BigDecimal.valueOf(1000), + totaltTilbakekrevesBeløp = BigDecimal.valueOf(10000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal.valueOf(11000), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = + BigDecimal.valueOf(11000), + ), + varsel = HbVarsel( + varsletBeløp = BigDecimal.valueOf(10000), + varsletDato = LocalDate.now().minusDays(100), + ), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 6), + søker = HbPerson( + navn = "Søker Søkersen", + dødsdato = LocalDate.of(2018, 3, 1), + ), + fagsaksvedtaksdato = LocalDate.now(), + behandling = HbBehandling(), + totaltFeilutbetaltBeløp = BigDecimal.valueOf(10000), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ansvarligBeslutter = "ansvarlig person sin signatur", + datoer = HbVedtaksbrevDatoer( + opphørsdatoDødSøker = LocalDate.of(2021, 5, 4), + opphørsdatoDødtBarn = LocalDate.of(2021, 5, 4), + ), + ) + + private fun getFeilutbetalingsårsaker(ytelsestype: Ytelsestype): List { + return HendelsestypePerYtelsestype.getHendelsestyper(ytelsestype).map { hendelsestype -> + HendelsesundertypePerHendelsestype.getHendelsesundertyper(hendelsestype).map { hendelsesundertype -> + HendelseMedUndertype(hendelsestype, hendelsesundertype) + } + }.flatten() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrevInntektOver6GTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrevInntektOver6GTest.kt new file mode 100644 index 000000000..9fbfd3d96 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrevInntektOver6GTest.kt @@ -0,0 +1,178 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.HbGrunnbeløpUtil.lagHbGrunnbeløp +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbBehandling +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbHjemmel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbKonfigurasjon +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbPerson +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbTotalresultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVarsel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevsdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbFakta +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbGrunnbeløp +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbKravgrunnlag +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbResultatTestBuilder +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbSærligeGrunner +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVedtaksbrevsperiode +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVurderinger +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.nio.charset.StandardCharsets +import java.time.LocalDate +import java.util.Scanner + +class TekstformatererVedtaksbrevInntektOver6GTest { + + @Nested + inner class GenererHeltVedtaksbrev { + + @Test + internal fun `en periode, en beløpsperiode`() { + val data = HbVedtaksbrevsdata(felles, listOf(periodeMedEnBeløpsperiode)) + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + val fasit = les("/vedtaksbrev/barnetilsyn/BT_beløp_over_6G_helt_brev_en_periode.txt") + + generertBrev shouldBe fasit + } + + @Test + internal fun `flere perioder, to beløpsperiode og tre beløpsperioder`() { + val data = HbVedtaksbrevsdata(felles, listOf(periodeMedToBeløpsperioder, periodeMedTreBeløpsperioder)) + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + val fasit = les("/vedtaksbrev/barnetilsyn/BT_beløp_over_6G_helt_brev_flere_perioder_flere_beløp.txt") + + generertBrev shouldBe fasit + } + + @Test + internal fun `nynorsk - flere perioder, en beløpsperiode og tre beløpsperioder`() { + val data = HbVedtaksbrevsdata( + felles.copy(brevmetadata.copy(språkkode = Språkkode.NN)), + listOf(periodeMedEnBeløpsperiode, periodeMedTreBeløpsperioder), + ) + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + val fasit = les("/vedtaksbrev/barnetilsyn/BT_beløp_over_6G_helt_brev_flere_perioder_flere_beløp_nn.txt") + + generertBrev shouldBe fasit + } + } + + private val januar = Datoperiode(LocalDate.of(2019, 1, 1), LocalDate.of(2019, 1, 31)) + + private val brevmetadata = Brevmetadata( + sakspartId = "123456", + sakspartsnavn = "Test", + mottageradresse = Adresseinfo("ident", "bob"), + behandlendeEnhetsNavn = "NAV Familie- og pensjonsytelser Skien", + ansvarligSaksbehandler = "Ansvarlig Saksbehandler", + saksnummer = "1232456", + språkkode = Språkkode.NB, + ytelsestype = Ytelsestype.OVERGANGSSTØNAD, + gjelderDødsfall = false, + ) + + private val felles = + HbVedtaksbrevFelles( + brevmetadata = brevmetadata, + hjemmel = HbHjemmel("Folketrygdloven"), + totalresultat = HbTotalresultat( + Vedtaksresultat.FULL_TILBAKEBETALING, + BigDecimal.valueOf(10000), + BigDecimal.valueOf(11000), + BigDecimal.valueOf(11000), + BigDecimal.valueOf(1000), + ), + varsel = HbVarsel( + varsletBeløp = BigDecimal.valueOf(10000), + varsletDato = LocalDate.of(2022, 6, 21), + ), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 6), + søker = HbPerson( + navn = "Søker Søkersen", + ), + fagsaksvedtaksdato = LocalDate.now(), + behandling = HbBehandling(), + totaltFeilutbetaltBeløp = BigDecimal.valueOf(10000), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ansvarligBeslutter = "Ansvarlig Beslutter", + ) + + private val fakta = + HbFakta(Hendelsestype.STØNAD_TIL_BARNETILSYN, Hendelsesundertype.INNTEKT_OVER_6G) + + private fun lagPeriode(grunnbeløp: HbGrunnbeløp): HbVedtaksbrevsperiode = + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(30001)), + fakta = fakta, + vurderinger = HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat + .MANGELFULLE_OPPLYSNINGER_FRA_BRUKER, + aktsomhetsresultat = Aktsomhet.SIMPEL_UAKTSOMHET, + særligeGrunner = + HbSærligeGrunner( + listOf( + SærligGrunn.TID_FRA_UTBETALING, + SærligGrunn.STØRRELSE_BELØP, + ), + ), + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(20002), + grunnbeløp = grunnbeløp, + førstePeriode = true, + ) + + private val periodeMedEnBeløpsperiode = lagPeriode( + lagHbGrunnbeløp( + Månedsperiode( + LocalDate.of(2021, 1, 1), + LocalDate.of(2021, 3, 31), + ), + ), + ) + + private val periodeMedToBeløpsperioder = lagPeriode( + lagHbGrunnbeløp( + Månedsperiode( + LocalDate.of(2020, 1, 1), + LocalDate.of(2021, 4, 30), + ), + ), + ) + + private val periodeMedTreBeløpsperioder = lagPeriode( + lagHbGrunnbeløp( + Månedsperiode( + LocalDate.of(2020, 1, 1), + LocalDate.of(2021, 5, 31), + ), + ), + ) + + private fun les(filnavn: String): String? { + javaClass.getResourceAsStream(filnavn).use { resource -> + Scanner(resource, StandardCharsets.UTF_8).use { scanner -> + scanner.useDelimiter("\\A") + return if (scanner.hasNext()) scanner.next() else null + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrevTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrevTest.kt new file mode 100644 index 000000000..cf1555809 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrevTest.kt @@ -0,0 +1,1456 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotBeEmpty +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.handlebars.FellesTekstformaterer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbBehandling +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbHjemmel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbKonfigurasjon +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbPerson +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbTotalresultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVarsel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevDatoer +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevPeriodeOgFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevsdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbFakta +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbKravgrunnlag +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbResultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbResultatTestBuilder +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbSærligeGrunner +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVedtaksbrevsperiode +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVurderinger +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.AnnenVurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.nio.charset.StandardCharsets +import java.time.LocalDate +import java.util.Scanner + +class TekstformatererVedtaksbrevTest { + + private val januar = Datoperiode(LocalDate.of(2019, 1, 1), LocalDate.of(2019, 1, 31)) + private val februar = Datoperiode(LocalDate.of(2019, 2, 1), LocalDate.of(2019, 2, 28)) + private val mars = Datoperiode(LocalDate.of(2019, 3, 1), LocalDate.of(2019, 3, 31)) + private val april = Datoperiode(LocalDate.of(2019, 4, 1), LocalDate.of(2019, 4, 30)) + private val førsteNyttårsdag = Datoperiode(LocalDate.of(2019, 1, 1), LocalDate.of(2019, 1, 1)) + + private val brevmetadata = Brevmetadata( + sakspartId = "123456", + sakspartsnavn = "Test", + mottageradresse = Adresseinfo("ident", "bob"), + behandlendeEnhetsNavn = "NAV Familie- og pensjonsytelser Skien", + ansvarligSaksbehandler = "Ansvarlig Saksbehandler", + saksnummer = "1232456", + språkkode = Språkkode.NB, + ytelsestype = Ytelsestype.OVERGANGSSTØNAD, + gjelderDødsfall = false, + ) + + private val felles = + HbVedtaksbrevFelles( + brevmetadata = brevmetadata, + hjemmel = HbHjemmel("Folketrygdloven"), + totalresultat = HbTotalresultat( + Vedtaksresultat.FULL_TILBAKEBETALING, + BigDecimal.valueOf(10000), + BigDecimal.valueOf(11000), + BigDecimal.valueOf(11000), + BigDecimal.valueOf(1000), + ), + varsel = HbVarsel( + varsletBeløp = BigDecimal.valueOf(10000), + varsletDato = LocalDate.now().minusDays(100), + ), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 6), + søker = HbPerson( + navn = "Søker Søkersen", + ), + fagsaksvedtaksdato = LocalDate.now(), + behandling = HbBehandling(), + totaltFeilutbetaltBeløp = BigDecimal.valueOf(10000), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ansvarligBeslutter = "Ansvarlig Beslutter", + ) + + @Nested + inner class LagVedtaksbrevFritekst { + + @Test + fun `skal generere vedtaksbrev for OS og god tro uten tilbakekreving uten varsel`() { + val perioder: List = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag(BigDecimal.ZERO, BigDecimal(1000), BigDecimal(1000)), + fakta = HbFakta( + Hendelsestype.DØDSFALL, + Hendelsesundertype.BRUKER_DØD, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + aktsomhetsresultat = AnnenVurdering.GOD_TRO, + beløpIBehold = BigDecimal.ZERO, + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(0), + førstePeriode = true, + ), + ) + val vedtaksbrevData = felles.copy( + fagsaksvedtaksdato = LocalDate.of(2019, 3, 21), + varsel = null, + totalresultat = HbTotalresultat( + Vedtaksresultat.INGEN_TILBAKEBETALING, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + datoer = HbVedtaksbrevDatoer(perioder = perioder), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_ingen_tilbakekreving.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for OS uten tilbakekreving uten varsel i 3dje person`() { + val perioder: List = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag(BigDecimal.ZERO, BigDecimal(1000), BigDecimal(1000)), + fakta = HbFakta( + Hendelsestype.DØDSFALL, + Hendelsesundertype.BRUKER_DØD, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + aktsomhetsresultat = AnnenVurdering.GOD_TRO, + beløpIBehold = BigDecimal.ZERO, + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(0), + førstePeriode = true, + ), + ) + val vedtaksbrevData = felles.copy( + brevmetadata = felles.brevmetadata.copy(gjelderDødsfall = true), + fagsaksvedtaksdato = LocalDate.of(2019, 3, 21), + varsel = null, + totalresultat = HbTotalresultat( + Vedtaksresultat.INGEN_TILBAKEBETALING, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + datoer = HbVedtaksbrevDatoer(perioder = perioder), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_ingen_tilbakekreving_bruker_død.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for OS med tilbakekreving med varsel i 3dje person`() { + val perioder: List = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag(BigDecimal.ZERO, BigDecimal(1000), BigDecimal(1000)), + fakta = HbFakta( + Hendelsestype.DØDSFALL, + Hendelsesundertype.BRUKER_DØD, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + aktsomhetsresultat = Aktsomhet.GROV_UAKTSOMHET, + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(1000), + førstePeriode = true, + ), + ) + val vedtaksbrevData = felles.copy( + brevmetadata = felles.brevmetadata.copy(gjelderDødsfall = true), + fagsaksvedtaksdato = LocalDate.of(2019, 3, 21), + varsel = HbVarsel( + varsletBeløp = BigDecimal(1234567893), + varsletDato = LocalDate.of(2019, 1, 3), + ), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.FULL_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal(10000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal(11000), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal(7011), + totaltRentebeløp = BigDecimal(1000), + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + datoer = HbVedtaksbrevDatoer(perioder = perioder), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_tilbakekreving_bruker_død.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for OS uten tilbakekreving uten varsel i 3dje person nynorsk`() { + val perioder: List = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag(BigDecimal.ZERO, BigDecimal(1000), BigDecimal(1000)), + fakta = HbFakta( + Hendelsestype.DØDSFALL, + Hendelsesundertype.BRUKER_DØD, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + aktsomhetsresultat = AnnenVurdering.GOD_TRO, + beløpIBehold = BigDecimal.ZERO, + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(0), + førstePeriode = true, + ), + ) + val vedtaksbrevData = felles.copy( + brevmetadata = felles.brevmetadata.copy(gjelderDødsfall = true, språkkode = Språkkode.NN), + fagsaksvedtaksdato = LocalDate.of(2019, 3, 21), + varsel = null, + totalresultat = HbTotalresultat( + Vedtaksresultat.INGEN_TILBAKEBETALING, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + ), + søker = HbPerson( + navn = "Søker Søkersen", + dødsdato = LocalDate.of(2018, 3, 1), + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + datoer = HbVedtaksbrevDatoer(perioder = perioder), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_ingen_tilbakekreving_bruker_død_nn.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for OS med tilbakekreving med varsel i 3dje person nynorsk`() { + val perioder: List = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag(BigDecimal.ZERO, BigDecimal(1000), BigDecimal(1000)), + fakta = HbFakta( + Hendelsestype.DØDSFALL, + Hendelsesundertype.BRUKER_DØD, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + aktsomhetsresultat = Aktsomhet.GROV_UAKTSOMHET, + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(1000), + førstePeriode = true, + ), + ) + val vedtaksbrevData = felles.copy( + brevmetadata = felles.brevmetadata.copy(gjelderDødsfall = true, språkkode = Språkkode.NN), + fagsaksvedtaksdato = LocalDate.of(2019, 3, 21), + varsel = HbVarsel( + varsletBeløp = BigDecimal(1234567893), + varsletDato = LocalDate.of(2019, 1, 3), + ), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.FULL_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal(10000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal(11000), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal(7011), + totaltRentebeløp = BigDecimal(1000), + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + datoer = HbVedtaksbrevDatoer(perioder = perioder), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_tilbakekreving_bruker_død_nn.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for OS og god tro uten tilbakekreving uten varsel med verge`() { + val perioder: List = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag(BigDecimal.ZERO, BigDecimal(1000), BigDecimal(1000)), + fakta = HbFakta( + Hendelsestype.DØDSFALL, + Hendelsesundertype.BRUKER_DØD, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + aktsomhetsresultat = AnnenVurdering.GOD_TRO, + beløpIBehold = BigDecimal.ZERO, + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(0), + førstePeriode = true, + ), + ) + val vedtaksbrevData = felles.copy( + fagsaksvedtaksdato = LocalDate.of(2019, 3, 21), + brevmetadata = brevmetadata.copy( + mottageradresse = Adresseinfo( + "12345678901", + "Semba AS c/o John Doe", + ), + sakspartsnavn = "Test", + vergenavn = "John Doe", + finnesVerge = true, + finnesAnnenMottaker = true, + ), + varsel = null, + totalresultat = HbTotalresultat( + Vedtaksresultat.INGEN_TILBAKEBETALING, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + datoer = HbVedtaksbrevDatoer(perioder = perioder), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_ingen_tilbakekreving_med_verge.txt") + generertBrev shouldBe "$fasit" + } + + @Test + fun `skal generere vedtaksbrev for revurdering med OS og mye fritekst`() { + val vedtaksbrevData = felles.copy( + fagsaksvedtaksdato = LocalDate.now(), + behandling = HbBehandling( + erRevurdering = true, + originalBehandlingsdatoFagsakvedtak = LocalDate.of( + 2019, + 1, + 1, + ), + ), + totalresultat = HbTotalresultat( + Vedtaksresultat.DELVIS_TILBAKEBETALING, + BigDecimal(1234567892), + BigDecimal(1234567892), + BigDecimal(1234567000), + BigDecimal.ZERO, + ), + søker = HbPerson( + navn = "Søker Søkersen", + dødsdato = LocalDate.of(2018, 3, 1), + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(1234567893), + varsletDato = LocalDate.of(2019, 1, 3), + ), + fritekstoppsummering = "Skynd deg å betale, vi trenger pengene med en gang!", + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + val perioder = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag(feilutbetaltBeløp = BigDecimal(1234567890)), + fakta = HbFakta( + Hendelsestype.ANNET, + Hendelsesundertype.ANNET_FRITEKST, + "Ingen vet riktig hva som har skjedd, " + + "men du har fått utbetalt alt for mye penger.", + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + aktsomhetsresultat = Aktsomhet.GROV_UAKTSOMHET, + fritekst = "Det er helt utrolig om du ikke har oppdaget dette!", + særligeGrunner = + HbSærligeGrunner( + listOf( + SærligGrunn.HELT_ELLER_DELVIS_NAVS_FEIL, + SærligGrunn.STØRRELSE_BELØP, + SærligGrunn.TID_FRA_UTBETALING, + SærligGrunn.ANNET, + ), + "Gratulerer, du fikk norgesrekord i feilutbetalt" + + " beløp! Du skal slippe å betale renter!", + "at du jobber med OVERGANGSSTØNAD " + + "og dermed vet hvordan dette fungerer!", + ), + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(1234567890), + førstePeriode = true, + ), + HbVedtaksbrevsperiode( + periode = februar, + kravgrunnlag = HbKravgrunnlag( + riktigBeløp = BigDecimal(0), + utbetaltBeløp = BigDecimal(1), + feilutbetaltBeløp = BigDecimal(1), + ), + fakta = HbFakta( + Hendelsestype.ENSLIG_FORSØRGER, + Hendelsesundertype.BARN_FLYTTET, + "Her har økonomisystemet gjort noe helt feil.", + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + aktsomhetsresultat = AnnenVurdering.GOD_TRO, + fritekst = "Vi skjønner at du ikke har oppdaget beløpet, " + + "siden du hadde så mye annet på konto.", + beløpIBehold = BigDecimal(1), + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(1), + førstePeriode = true, + ), + HbVedtaksbrevsperiode( + periode = mars, + kravgrunnlag = HbKravgrunnlag( + riktigBeløp = BigDecimal(0), + utbetaltBeløp = BigDecimal(1), + feilutbetaltBeløp = BigDecimal(1), + ), + fakta = HbFakta( + Hendelsestype.ENSLIG_FORSØRGER, + Hendelsesundertype.BARN_FLYTTET, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + aktsomhetsresultat = Aktsomhet.SIMPEL_UAKTSOMHET, + fritekst = "Her burde du passet mer på!", + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(1), + førstePeriode = true, + ), + HbVedtaksbrevsperiode( + periode = april, + kravgrunnlag = HbKravgrunnlag( + riktigBeløp = BigDecimal(0), + utbetaltBeløp = BigDecimal(1), + feilutbetaltBeløp = BigDecimal(1), + ), + fakta = HbFakta( + Hendelsestype.ENSLIG_FORSØRGER, + Hendelsesundertype.BARN_FLYTTET, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.MANGELFULLE_OPPLYSNINGER_FRA_BRUKER, + aktsomhetsresultat = Aktsomhet.FORSETT, + fritekst = "Dette gjorde du med vilje!", + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(1), + førstePeriode = true, + ), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_fritekst_overalt.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for OS og ett barn og forsett`() { + val vedtaksbrevData = felles + .copy( + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.FULL_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal(10000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal(11000), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal(7011), + totaltRentebeløp = BigDecimal(1000), + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(10000), + varsletDato = LocalDate.of(2020, 4, 4), + ), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 6), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + val perioder = listOf( + HbVedtaksbrevsperiode( + januar, + HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(10000)), + HbFakta( + Hendelsestype.ENSLIG_FORSØRGER, + Hendelsesundertype.BARN_FLYTTET, + ), + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + aktsomhetsresultat = Aktsomhet.FORSETT, + ), + HbResultatTestBuilder.forTilbakekrevesBeløpOgRenter(10000, 1000), + true, + ), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_forsett.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for revurdering med OS og ett barn og forsett og bruker død`() { + val perioder = listOf( + HbVedtaksbrevsperiode( + januar, + HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(10000)), + HbFakta( + Hendelsestype.DØDSFALL, + Hendelsesundertype.BRUKER_DØD, + ), + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + aktsomhetsresultat = Aktsomhet.FORSETT, + ), + HbResultatTestBuilder.forTilbakekrevesBeløpOgRenter(10000, 1000), + true, + ), + ) + val vedtaksbrevData = felles + .copy( + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.FULL_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal(10000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal(11000), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal(7011), + totaltRentebeløp = BigDecimal(1000), + ), + behandling = HbBehandling( + erRevurdering = true, + originalBehandlingsdatoFagsakvedtak = LocalDate.of( + 2019, + 1, + 1, + ), + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(10000), + varsletDato = LocalDate.of(2020, 4, 4), + ), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 6), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + brevmetadata = brevmetadata.copy(gjelderDødsfall = true), + søker = HbPerson( + navn = "Søker Søkersen", + dødsdato = LocalDate.of(2018, 3, 1), + ), + datoer = HbVedtaksbrevDatoer(perioder = perioder), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_revurdering_bruker_død.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for revurdering med OS og ett barn og forsett og bruker død annet annet fritekst er valgt`() { + val perioder = listOf( + HbVedtaksbrevsperiode( + januar, + HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(10000)), + HbFakta( + Hendelsestype.ANNET, + Hendelsesundertype.ANNET_FRITEKST, + "Død bruker annet fritekst er valgt", + ), + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + aktsomhetsresultat = Aktsomhet.FORSETT, + fritekst = "Død bruker annet fritekst er valgt", + ), + HbResultatTestBuilder.forTilbakekrevesBeløpOgRenter(10000, 1000), + true, + ), + ) + val vedtaksbrevData = felles + .copy( + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.FULL_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal(10000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal(11000), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal(7011), + totaltRentebeløp = BigDecimal(1000), + ), + behandling = HbBehandling( + erRevurdering = true, + originalBehandlingsdatoFagsakvedtak = LocalDate.of( + 2019, + 1, + 1, + ), + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(10000), + varsletDato = LocalDate.of(2020, 4, 4), + ), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 6), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + brevmetadata = brevmetadata.copy(gjelderDødsfall = true), + søker = HbPerson( + navn = "Søker Søkersen", + dødsdato = LocalDate.of(2018, 3, 1), + ), + datoer = HbVedtaksbrevDatoer(perioder = perioder), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_revurdering_bruker_død_annet_fritekst.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for revurdering med OS og ett barn og forsett og bruker død nynorsk`() { + val perioder = listOf( + HbVedtaksbrevsperiode( + januar, + HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(10000)), + HbFakta( + Hendelsestype.DØDSFALL, + Hendelsesundertype.BRUKER_DØD, + ), + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + aktsomhetsresultat = Aktsomhet.FORSETT, + ), + HbResultatTestBuilder.forTilbakekrevesBeløpOgRenter(10000, 1000), + true, + ), + ) + val vedtaksbrevData = felles + .copy( + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.FULL_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal(10000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal(11000), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal(7011), + totaltRentebeløp = BigDecimal(1000), + ), + behandling = HbBehandling( + erRevurdering = true, + originalBehandlingsdatoFagsakvedtak = LocalDate.of( + 2019, + 1, + 1, + ), + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(10000), + varsletDato = LocalDate.of(2020, 4, 4), + ), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 6), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + brevmetadata = brevmetadata.copy(språkkode = Språkkode.NN, gjelderDødsfall = true), + søker = HbPerson( + navn = "Søker Søkersen", + dødsdato = LocalDate.of(2018, 3, 1), + ), + datoer = HbVedtaksbrevDatoer(perioder = perioder), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_revurdering_bruker_død_nynorsk.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for revurdering med OS og ett barn og forsett og bruker død nynorsk annet annet fritekst er valgt`() { + val perioder = listOf( + HbVedtaksbrevsperiode( + januar, + HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(10000)), + HbFakta( + Hendelsestype.ANNET, + Hendelsesundertype.ANNET_FRITEKST, + "Død bruker annet fritekst er valgt", + ), + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + aktsomhetsresultat = Aktsomhet.FORSETT, + fritekst = "Død bruker annet fritekst er valgt", + ), + HbResultatTestBuilder.forTilbakekrevesBeløpOgRenter(10000, 1000), + true, + ), + ) + val vedtaksbrevData = felles + .copy( + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.FULL_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal(10000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal(11000), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal(7011), + totaltRentebeløp = BigDecimal(1000), + ), + behandling = HbBehandling( + erRevurdering = true, + originalBehandlingsdatoFagsakvedtak = LocalDate.of( + 2019, + 1, + 1, + ), + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(10000), + varsletDato = LocalDate.of(2020, 4, 4), + ), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 6), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + brevmetadata = brevmetadata.copy(språkkode = Språkkode.NN, gjelderDødsfall = true), + søker = HbPerson( + navn = "Søker Søkersen", + dødsdato = LocalDate.of(2018, 3, 1), + ), + datoer = HbVedtaksbrevDatoer(perioder = perioder), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_revurdering_bruker_død_nynorsk_annet_fritekst.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for_KS_og forsett`() { + val vedtaksbrevData = felles + .copy( + brevmetadata = brevmetadata.copy(ytelsestype = Ytelsestype.KONTANTSTØTTE), + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.FULL_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal(10000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal(10000), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal(10000), + totaltRentebeløp = BigDecimal(0), + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(10000), + varsletDato = LocalDate.of(2020, 4, 4), + ), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 6), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + val perioder: List = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(10000)), + fakta = HbFakta( + hendelsestype = Hendelsestype.ANNET_KS, + hendelsesundertype = Hendelsesundertype.ANNET_FRITEKST, + fritekstFakta = "Dette er svindel!", + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + aktsomhetsresultat = Aktsomhet.FORSETT, + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløpOgRenter(10000, 0), + førstePeriode = true, + ), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/KS_forsett.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for OS med og uten foreldelse og uten skatt`() { + val vedtaksbrevData = felles + .copy( + fagsaksvedtaksdato = LocalDate.of(2019, 11, 12), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.DELVIS_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal(1000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal(1000), + totaltRentebeløp = BigDecimal.ZERO, + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal(1000), + ), + varsel = null, + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + val perioder: List = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag( + BigDecimal.ZERO, + BigDecimal(1000), + BigDecimal(1000), + ), + fakta = HbFakta( + Hendelsestype.ENSLIG_FORSØRGER, + Hendelsesundertype.BARN_FLYTTET, + ), + vurderinger = HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.FORELDET, + aktsomhetsresultat = AnnenVurdering.FORELDET, + foreldelsesfrist = januar.fom.plusMonths(11), + ), + resultat = HbResultat( + tilbakekrevesBeløp = BigDecimal.ZERO, + tilbakekrevesBeløpUtenSkattMedRenter = BigDecimal.ZERO, + rentebeløp = BigDecimal.ZERO, + foreldetBeløp = BigDecimal(1000), + ), + førstePeriode = true, + ), + HbVedtaksbrevsperiode( + periode = februar, + kravgrunnlag = HbKravgrunnlag( + BigDecimal.ZERO, + BigDecimal(1000), + BigDecimal(1000), + ), + fakta = HbFakta( + Hendelsestype.MEDLEMSKAP, + Hendelsesundertype.LOVLIG_OPPHOLD, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.TILLEGGSFRIST, + foreldelsesfrist = januar.fom.plusMonths(11), + oppdagelsesdato = januar.fom.plusMonths(8), + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + aktsomhetsresultat = AnnenVurdering.GOD_TRO, + beløpIBehold = BigDecimal(1000), + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(1000), + førstePeriode = true, + ), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + val fasit = les("/vedtaksbrev/OS_delvis_foreldelse_uten_varsel.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for OS ingen tilbakekreving pga lavt beløp`() { + val vedtaksbrevData = felles + .copy( + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.INGEN_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal.ZERO, + totaltTilbakekrevesBeløpMedRenter = BigDecimal.ZERO, + totaltRentebeløp = BigDecimal.ZERO, + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal.ZERO, + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15 6.ledd"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(500), + varsletDato = LocalDate.of(2020, 4, 4), + ), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + val perioder: List = + listOf( + HbVedtaksbrevsperiode( + periode = førsteNyttårsdag, + kravgrunnlag = HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(500)), + fakta = HbFakta( + Hendelsestype.ANNET, + Hendelsesundertype.ANNET_FRITEKST, + "foo bar baz", + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + aktsomhetsresultat = Aktsomhet.SIMPEL_UAKTSOMHET, + unntasInnkrevingPgaLavtBeløp = true, + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(0), + førstePeriode = true, + ), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_ikke_tilbakekreves_pga_lavt_beløp.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for BA ingen tilbakekreving pga lavt beløp død bruker`() { + val perioder: List = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(500)), + fakta = HbFakta( + Hendelsestype.DØDSFALL, + Hendelsesundertype.BRUKER_DØD, + "foo bar baz", + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + aktsomhetsresultat = Aktsomhet.SIMPEL_UAKTSOMHET, + unntasInnkrevingPgaLavtBeløp = true, + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(0), + førstePeriode = true, + ), + ) + val vedtaksbrevData = felles + .copy( + brevmetadata = brevmetadata.copy(ytelsestype = Ytelsestype.BARNETRYGD, gjelderDødsfall = true), + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.INGEN_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal.ZERO, + totaltTilbakekrevesBeløpMedRenter = BigDecimal.ZERO, + totaltRentebeløp = BigDecimal.ZERO, + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal.ZERO, + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15 6.ledd"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(500), + varsletDato = LocalDate.of(2020, 4, 4), + ), + søker = HbPerson( + navn = "Søker Søkersen", + dødsdato = LocalDate.of(2018, 3, 1), + ), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + datoer = HbVedtaksbrevDatoer(perioder = perioder), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/BA_ikke_tilbakekreves_pga_lavt_beløp_død_bruker.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for BA ingen tilbakekreving pga lavt beløp fritekst død bruker nynorsk`() { + val perioder: List = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(500)), + fakta = HbFakta( + Hendelsestype.DØDSFALL, + Hendelsesundertype.BRUKER_DØD, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + aktsomhetsresultat = Aktsomhet.SIMPEL_UAKTSOMHET, + unntasInnkrevingPgaLavtBeløp = true, + fritekst = "foo bar baz", + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(0), + førstePeriode = true, + ), + ) + val vedtaksbrevData = felles + .copy( + brevmetadata = brevmetadata.copy( + ytelsestype = Ytelsestype.BARNETRYGD, + gjelderDødsfall = true, + språkkode = Språkkode.NN, + ), + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.INGEN_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal.ZERO, + totaltTilbakekrevesBeløpMedRenter = BigDecimal.ZERO, + totaltRentebeløp = BigDecimal.ZERO, + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal.ZERO, + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15 6.ledd"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(500), + varsletDato = LocalDate.of(2020, 4, 4), + ), + søker = HbPerson( + navn = "Søker Søkersen", + dødsdato = LocalDate.of(2018, 3, 1), + ), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + datoer = HbVedtaksbrevDatoer(perioder = perioder), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/BA_ikke_tilbakekreves_pga_lavt_beløp_død_bruker_nynorsk.txt") + generertBrev shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev for OS ingen tilbakekreving pga lavt beløp med korrigert beløp`() { + val vedtaksbrevData = felles + .copy( + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.INGEN_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal.ZERO, + totaltTilbakekrevesBeløpMedRenter = BigDecimal.ZERO, + totaltRentebeløp = BigDecimal.ZERO, + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal.ZERO, + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15 6.ledd"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(15000), + varsletDato = LocalDate.of(2020, 4, 4), + ), + erFeilutbetaltBeløpKorrigertNed = true, + totaltFeilutbetaltBeløp = BigDecimal(1000), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ) + val perioder: List = + listOf( + HbVedtaksbrevsperiode( + periode = førsteNyttårsdag, + kravgrunnlag = HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(500)), + fakta = HbFakta( + Hendelsestype.ANNET, + Hendelsesundertype.ANNET_FRITEKST, + "foo bar baz", + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + aktsomhetsresultat = Aktsomhet.SIMPEL_UAKTSOMHET, + unntasInnkrevingPgaLavtBeløp = true, + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(0), + førstePeriode = true, + ), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(data) + + val fasit = les("/vedtaksbrev/OS_ikke_tilbakekreves_med_korrigert_beløp.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVedtaksbrevFritekst skal generere fritekst og uten perioder vedtaksbrev revurdering for OS med full tilbakebetaling`() { + val fritekstVedtaksbrevsdata: HbVedtaksbrevsdata = + lagFritekstVedtaksbrevData(Ytelsestype.OVERGANGSSTØNAD, Vedtaksresultat.FULL_TILBAKEBETALING) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(fritekstVedtaksbrevsdata) + + generertBrev.shouldNotBeEmpty() + val fasit = les("/vedtaksbrev/Fritekst_Vedtaksbrev_OS_full_tilbakebetaling.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVedtaksbrevFritekst skal generere fritekst og uten perioder vedtaksbrev revurdering for KS med ingen tilbakebetaling`() { + val fritekstVedtaksbrevsdata: HbVedtaksbrevsdata = + lagFritekstVedtaksbrevData(Ytelsestype.KONTANTSTØTTE, Vedtaksresultat.INGEN_TILBAKEBETALING) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsfritekst(fritekstVedtaksbrevsdata) + + generertBrev.shouldNotBeEmpty() + val fasit = les("/vedtaksbrev/Fritekst_Vedtaksbrev_KS_ingen_tilbakebetaling.txt") + generertBrev shouldBe fasit + } + + private fun lagFritekstVedtaksbrevData( + ytelsestype: Ytelsestype, + hovedresultat: Vedtaksresultat, + ): HbVedtaksbrevsdata { + return HbVedtaksbrevsdata( + felles.copy( + brevmetadata = brevmetadata.copy( + språkkode = Språkkode.NB, + ytelsestype = ytelsestype, + ), + totalresultat = felles.totalresultat.copy(hovedresultat = hovedresultat), + behandling = HbBehandling( + erRevurdering = true, + originalBehandlingsdatoFagsakvedtak = LocalDate.now(), + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + fritekstoppsummering = "sender fritekst vedtaksbrev", + vedtaksbrevstype = Vedtaksbrevstype.FRITEKST_FEILUTBETALING_BORTFALT, + ), + emptyList(), + ) + } + } + + @Nested + inner class LagVedtaksbrevOverskrift { + + @Test + fun `skal generere vedtaksbrev overskrift_OVERGANGSSTØNAD_full tilbakebetaling`() { + val data: HbVedtaksbrevsdata = + lagBrevOverskriftTestoppsett(Ytelsestype.OVERGANGSSTØNAD, Vedtaksresultat.FULL_TILBAKEBETALING, Språkkode.NB) + + val overskrift = TekstformatererVedtaksbrev.lagVedtaksbrevsoverskrift(data) + + val fasit = "Du må betale tilbake overgangsstønaden" + overskrift shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev overskrift_kontantstøtte_full tilbakebetaling nynorsk`() { + val data = lagBrevOverskriftTestoppsett( + Ytelsestype.KONTANTSTØTTE, + Vedtaksresultat.FULL_TILBAKEBETALING, + Språkkode.NN, + ) + + val overskrift = TekstformatererVedtaksbrev.lagVedtaksbrevsoverskrift(data) + + val fasit = "Du må betale tilbake kontantstøtta" + overskrift shouldBe fasit + } + + @Test + fun `skal generere vedtaksbrev overskrift_KONTANTSTØTTE_ingen tilbakebetaling`() { + val data: HbVedtaksbrevsdata = + lagBrevOverskriftTestoppsett(Ytelsestype.OVERGANGSSTØNAD, Vedtaksresultat.INGEN_TILBAKEBETALING, Språkkode.NB) + + val overskrift = TekstformatererVedtaksbrev.lagVedtaksbrevsoverskrift(data) + + val fasit = "Du må ikke betale tilbake overgangsstønaden" + overskrift shouldBe fasit + } + + private fun lagBrevOverskriftTestoppsett( + ytelsestype: Ytelsestype, + hovedresultat: Vedtaksresultat, + språkkode: Språkkode, + ): HbVedtaksbrevsdata { + return HbVedtaksbrevsdata( + felles.copy( + brevmetadata = brevmetadata.copy(språkkode = språkkode, ytelsestype = ytelsestype), + totalresultat = felles.totalresultat.copy(hovedresultat = hovedresultat), + ), + emptyList(), + ) + } + } + + @Nested + inner class LagDeltekst { + + @Test + fun `skal ha riktig tekst for særlige grunner når det er reduksjon av beløp`() { + val felles = felles.copy( + brevmetadata = brevmetadata.copy(språkkode = Språkkode.NN), + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + Vedtaksresultat.FULL_TILBAKEBETALING, + BigDecimal(1000), + BigDecimal(1100), + BigDecimal(1100), + BigDecimal(100), + ), + hjemmel = HbHjemmel("foo"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(1000), + varsletDato = LocalDate.of(2020, 4, 4), + ), + ) + val periode = + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(1000)), + fakta = HbFakta(Hendelsestype.ANNET, Hendelsesundertype.ANNET_FRITEKST), + vurderinger = HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat + .FEIL_OPPLYSNINGER_FRA_BRUKER, + aktsomhetsresultat = Aktsomhet.SIMPEL_UAKTSOMHET, + særligeGrunner = + HbSærligeGrunner( + listOf(SærligGrunn.GRAD_AV_UAKTSOMHET), + null, + null, + ), + ), + resultat = HbResultat( + tilbakekrevesBeløp = BigDecimal(500), + rentebeløp = BigDecimal(0), + tilbakekrevesBeløpUtenSkattMedRenter = BigDecimal(500), + ), + førstePeriode = true, + ) + + val generertTekst: String = FellesTekstformaterer.lagDeltekst( + HbVedtaksbrevPeriodeOgFelles(felles, periode), + AvsnittUtil.PARTIAL_PERIODE_SÆRLIGE_GRUNNER, + ) + + generertTekst shouldContain "Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok " + + "til at vi kunne unngå feilutbetalinga. Vi vurderer likevel at aktløysa di har vore så lita at vi har " + + "redusert beløpet du må betale tilbake." + generertTekst shouldContain "Du må betale 500 kroner" + } + + @Test + fun `skal generere tekst for faktaperiode`() { + val felles = felles.copy( + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + Vedtaksresultat.DELVIS_TILBAKEBETALING, + BigDecimal(23002), + BigDecimal(23002), + BigDecimal(23002), + BigDecimal.ZERO, + ), + hjemmel = HbHjemmel("foo"), + datoer = HbVedtaksbrevDatoer( + opphørsdatoIkkeOmsorg = LocalDate.of( + 2020, + 4, + 4, + ), + ), + varsel = HbVarsel( + varsletBeløp = BigDecimal(33001), + varsletDato = LocalDate.of(2020, 4, 4), + ), + ) + val periode = + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(30001)), + fakta = HbFakta(Hendelsestype.ENSLIG_FORSØRGER, Hendelsesundertype.BARN_FLYTTET), + vurderinger = HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat + .MANGELFULLE_OPPLYSNINGER_FRA_BRUKER, + aktsomhetsresultat = Aktsomhet.SIMPEL_UAKTSOMHET, + særligeGrunner = + HbSærligeGrunner( + listOf( + SærligGrunn.TID_FRA_UTBETALING, + SærligGrunn.STØRRELSE_BELØP, + ), + ), + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(20002), + førstePeriode = true, + ) + val data = HbVedtaksbrevPeriodeOgFelles(felles, periode) + + val generertTekst = FellesTekstformaterer.lagDeltekst(data, AvsnittUtil.PARTIAL_PERIODE_FAKTA) + + val fasit = "Du har fått overgangsstønad for barn som ikke bor fast hos deg. Du har derfor fått 30 001 kroner " + + "for mye utbetalt i denne perioden." + generertTekst shouldBe fasit + } + + @Test + fun `skal si at du ikke trenger betale tilbake når det er god tro og beløp ikke er i behold`() { + val felles = felles.copy( + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + Vedtaksresultat.DELVIS_TILBAKEBETALING, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + ), + hjemmel = HbHjemmel("foo"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(1000), + varsletDato = LocalDate.of(2020, 4, 4), + ), + ) + val periode = + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(1000)), + fakta = HbFakta(Hendelsestype.ANNET, Hendelsesundertype.ANNET_FRITEKST), + vurderinger = HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + aktsomhetsresultat = AnnenVurdering.GOD_TRO, + beløpIBehold = BigDecimal.ZERO, + ), + resultat = HbResultatTestBuilder.forTilbakekrevesBeløp(0), + førstePeriode = true, + ) + val data = HbVedtaksbrevPeriodeOgFelles(felles, periode) + + val generertTekst = FellesTekstformaterer.lagDeltekst(data, AvsnittUtil.PARTIAL_PERIODE_VILKÅR) + + generertTekst shouldContain "_Hvordan har vi kommet fram til at du ikke må betale tilbake?" + } + + @Test + fun `skal ha riktig tekst for særlige grunner når det ikke er reduksjon av beløp`() { + val felles = felles.copy( + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + Vedtaksresultat.FULL_TILBAKEBETALING, + BigDecimal(1000), + BigDecimal(1100), + BigDecimal(1100), + BigDecimal(100), + ), + hjemmel = HbHjemmel("foo"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(1000), + varsletDato = LocalDate.of(2020, 4, 4), + ), + ) + val periode = + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(1000)), + fakta = HbFakta(Hendelsestype.ANNET, Hendelsesundertype.ANNET_FRITEKST), + vurderinger = HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat + .FEIL_OPPLYSNINGER_FRA_BRUKER, + aktsomhetsresultat = Aktsomhet.GROV_UAKTSOMHET, + særligeGrunner = + HbSærligeGrunner(listOf(SærligGrunn.GRAD_AV_UAKTSOMHET)), + ), + resultat = HbResultat( + tilbakekrevesBeløp = BigDecimal(1000), + rentebeløp = BigDecimal(100), + tilbakekrevesBeløpUtenSkattMedRenter = BigDecimal(1000), + ), + førstePeriode = true, + ) + + val generertTekst: String = FellesTekstformaterer.lagDeltekst( + HbVedtaksbrevPeriodeOgFelles(felles, periode), + AvsnittUtil.PARTIAL_PERIODE_SÆRLIGE_GRUNNER, + ) + generertTekst shouldContain "Vi har vurdert om det er grunner til å redusere beløpet. " + + "Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok " + + "til at vi kunne unngå feilutbetalingen. Derfor må du betale tilbake hele beløpet." + } + } + + private fun les(filnavn: String): String? { + javaClass.getResourceAsStream(filnavn).use { resource -> + Scanner(resource, StandardCharsets.UTF_8).use { scanner -> + scanner.useDelimiter("\\A") + return if (scanner.hasNext()) scanner.next() else null + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrevVedleggTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrevVedleggTest.kt new file mode 100644 index 000000000..7fc693f10 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/TekstformatererVedtaksbrevVedleggTest.kt @@ -0,0 +1,308 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmetadata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbBehandling +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbHjemmel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbKonfigurasjon +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbPerson +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbTotalresultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVarsel +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevFelles +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.HbVedtaksbrevsdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.Vedtaksbrevstype +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbFakta +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbKravgrunnlag +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbResultat +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbSærligeGrunner +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVedtaksbrevsperiode +import no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode.HbVurderinger +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.AnnenVurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.Test +import java.io.IOException +import java.math.BigDecimal +import java.time.LocalDate +import java.util.Scanner + +class TekstformatererVedtaksbrevVedleggTest { + + private val januar = Datoperiode(LocalDate.of(2019, 1, 1), LocalDate.of(2019, 1, 31)) + private val februar = Datoperiode(LocalDate.of(2019, 2, 1), LocalDate.of(2019, 2, 28)) + private val mars = Datoperiode(LocalDate.of(2019, 3, 1), LocalDate.of(2019, 3, 31)) + + private val brevmetadata = Brevmetadata( + sakspartId = "123456", + sakspartsnavn = "Test", + mottageradresse = Adresseinfo("ident", "bob"), + behandlendeEnhetsNavn = "NAV Familie- og pensjonsytelser Skien", + ansvarligSaksbehandler = "Bob", + saksnummer = "1232456", + språkkode = Språkkode.NB, + ytelsestype = Ytelsestype.OVERGANGSSTØNAD, + gjelderDødsfall = false, + ) + + @Test + fun `lagVedtaksbrevVedleggHtml skal generere vedlegg med en periode uten renter`() { + val data = getVedtaksbrevData(Språkkode.NB) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsvedleggHtml(data) + + val fasit = les("/vedtaksbrev/vedlegg/vedlegg_uten_renter.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVedtaksbrevVedleggHtml skal generere vedlegg med en periode uten renter nynorsk`() { + val data = getVedtaksbrevData(Språkkode.NN) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsvedleggHtml(data) + + val fasit = les("/vedtaksbrev/vedlegg/vedlegg_uten_renter_nn.txt") + generertBrev shouldBe fasit + } + + @Test + fun `lagVedtaksbrevVedleggHtml skal generere vedlegg med en periode uten skatt`() { + val data = getVedtaksbrevData(Språkkode.NB, 10000, 30001, 30001, 0, 0, Ytelsestype.BARNETRYGD) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsvedleggHtml(data) + + val fasit = les("/vedtaksbrev/vedlegg/vedlegg_uten_skatt.txt") + generertBrev shouldBe fasit + } + + private fun getVedtaksbrevData(språkkode: Språkkode): HbVedtaksbrevsdata { + return getVedtaksbrevData(språkkode, 33001, 30001, 20002, 0, 20002 - 16015) + } + + private fun getVedtaksbrevData( + språkkode: Språkkode = Språkkode.NB, + varslet: Int, + feilutbetalt: Int, + tilbakekreves: Int, + renter: Int, + skatt: Int, + ytelsestype: Ytelsestype = Ytelsestype.OVERGANGSSTØNAD, + ): HbVedtaksbrevsdata { + val vedtaksbrevData = + lagTestBuilder() + .copy( + brevmetadata = brevmetadata.copy(språkkode = språkkode, ytelsestype = ytelsestype), + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = + HbTotalresultat( + hovedresultat = Vedtaksresultat.DELVIS_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal.valueOf(tilbakekreves.toLong()), + totaltTilbakekrevesBeløpMedRenter = BigDecimal.valueOf( + (tilbakekreves + renter) + .toLong(), + ), + totaltRentebeløp = BigDecimal.valueOf(renter.toLong()), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal + .valueOf((tilbakekreves + renter - skatt).toLong()), + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + varsel = HbVarsel( + varsletBeløp = BigDecimal.valueOf(varslet.toLong()), + varsletDato = LocalDate.of(2020, 4, 4), + ), + ) + + val perioder = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag + .forFeilutbetaltBeløp(BigDecimal.valueOf(feilutbetalt.toLong())), + fakta = HbFakta( + Hendelsestype.BOSATT_I_RIKET, + Hendelsesundertype.BARN_BOR_IKKE_I_NORGE, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat + .MANGELFULLE_OPPLYSNINGER_FRA_BRUKER, + aktsomhetsresultat = Aktsomhet.SIMPEL_UAKTSOMHET, + fritekst = "Du er heldig som slapp å betale alt!", + særligeGrunner = + HbSærligeGrunner( + listOf( + SærligGrunn.TID_FRA_UTBETALING, + SærligGrunn.STØRRELSE_BELØP, + ), + ), + ), + resultat = + HbResultat( + tilbakekrevesBeløpUtenSkattMedRenter = + BigDecimal.valueOf((tilbakekreves - skatt).toLong()), + tilbakekrevesBeløp = BigDecimal.valueOf(tilbakekreves.toLong()), + rentebeløp = BigDecimal.valueOf(renter.toLong()), + ), + førstePeriode = true, + ), + ) + return HbVedtaksbrevsdata(vedtaksbrevData, perioder) + } + + private fun lagTestBuilder(språkkode: Språkkode = Språkkode.NB, ytelsestype: Ytelsestype = Ytelsestype.OVERGANGSSTØNAD) = + HbVedtaksbrevFelles( + brevmetadata = brevmetadata.copy(språkkode = språkkode, ytelsestype = ytelsestype), + hjemmel = HbHjemmel("Folketrygdloven"), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.FULL_TILBAKEBETALING, + totaltRentebeløp = BigDecimal.valueOf(1000), + totaltTilbakekrevesBeløp = BigDecimal.valueOf(10000), + totaltTilbakekrevesBeløpMedRenter = BigDecimal.valueOf(11000), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = + BigDecimal.valueOf(11000), + ), + varsel = HbVarsel( + varsletBeløp = BigDecimal.valueOf(10000), + varsletDato = LocalDate.now().minusDays(100), + ), + konfigurasjon = HbKonfigurasjon(klagefristIUker = 6), + søker = HbPerson( + navn = "Søker Søkersen", + dødsdato = LocalDate.of(2018, 3, 1), + ), + fagsaksvedtaksdato = LocalDate.now(), + behandling = HbBehandling(), + totaltFeilutbetaltBeløp = BigDecimal.valueOf(10000), + vedtaksbrevstype = Vedtaksbrevstype.ORDINÆR, + ansvarligBeslutter = "ansvarlig person sin signatur", + ) + + @Test + fun `lagVedtaksbrevVedleggHtml skal generere vedlegg med flere perioder og med renter`() { + val vedtaksbrevData = lagTestBuilder() + .copy( + fagsaksvedtaksdato = LocalDate.now(), + totalresultat = HbTotalresultat( + hovedresultat = Vedtaksresultat.DELVIS_TILBAKEBETALING, + totaltTilbakekrevesBeløp = BigDecimal(23002), + totaltTilbakekrevesBeløpMedRenter = BigDecimal(23302), + totaltRentebeløp = BigDecimal(300), + totaltTilbakekrevesBeløpMedRenterUtenSkatt = BigDecimal(18537), + ), + hjemmel = HbHjemmel("Folketrygdloven § 22-15"), + varsel = HbVarsel( + varsletBeløp = BigDecimal(33001), + varsletDato = LocalDate.of(2020, 4, 4), + ), + ) + val perioder = + listOf( + HbVedtaksbrevsperiode( + periode = januar, + kravgrunnlag = HbKravgrunnlag.forFeilutbetaltBeløp(BigDecimal(30001)), + fakta = HbFakta( + Hendelsestype.ANNET, + Hendelsesundertype.ANNET_FRITEKST, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat + .MANGELFULLE_OPPLYSNINGER_FRA_BRUKER, + aktsomhetsresultat = Aktsomhet.SIMPEL_UAKTSOMHET, + fritekst = "Du er heldig som slapp å betale alt!", + særligeGrunner = HbSærligeGrunner( + listOf( + SærligGrunn + .TID_FRA_UTBETALING, + SærligGrunn.STØRRELSE_BELØP, + ), + null, + null, + ), + ), + resultat = HbResultat( + tilbakekrevesBeløpUtenSkattMedRenter = BigDecimal(16015), + tilbakekrevesBeløp = BigDecimal(20002), + rentebeløp = BigDecimal.ZERO, + ), + førstePeriode = true, + ), + HbVedtaksbrevsperiode( + periode = februar, + kravgrunnlag = HbKravgrunnlag( + feilutbetaltBeløp = BigDecimal(3000), + riktigBeløp = BigDecimal(3000), + utbetaltBeløp = BigDecimal(6000), + ), + fakta = HbFakta( + Hendelsestype.BOR_MED_SØKER, + Hendelsesundertype.BOR_IKKE_MED_BARN, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + aktsomhetsresultat = AnnenVurdering.GOD_TRO, + beløpIBehold = BigDecimal.ZERO, + ), + resultat = HbResultat( + tilbakekrevesBeløpUtenSkattMedRenter = BigDecimal.ZERO, + tilbakekrevesBeløp = BigDecimal.ZERO, + rentebeløp = BigDecimal.ZERO, + ), + førstePeriode = true, + ), + HbVedtaksbrevsperiode( + periode = mars, + kravgrunnlag = HbKravgrunnlag( + feilutbetaltBeløp = BigDecimal(3000), + riktigBeløp = BigDecimal(3000), + utbetaltBeløp = BigDecimal(6000), + ), + fakta = HbFakta( + Hendelsestype.BOR_MED_SØKER, + Hendelsesundertype.BOR_IKKE_MED_BARN, + ), + vurderinger = + HbVurderinger( + foreldelsevurdering = Foreldelsesvurderingstype.IKKE_VURDERT, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat + .FORSTO_BURDE_FORSTÅTT, + aktsomhetsresultat = Aktsomhet.FORSETT, + ), + resultat = HbResultat( + tilbakekrevesBeløpUtenSkattMedRenter = BigDecimal(2222), + tilbakekrevesBeløp = BigDecimal(3000), + rentebeløp = BigDecimal(300), + ), + førstePeriode = true, + ), + ) + val data = HbVedtaksbrevsdata(vedtaksbrevData, perioder) + + val generertBrev = TekstformatererVedtaksbrev.lagVedtaksbrevsvedleggHtml(data) + + val fasit = les("/vedtaksbrev/vedlegg/vedlegg_med_og_uten_renter.txt") + generertBrev shouldBe fasit + } + + @Throws(IOException::class) + private fun les(filnavn: String): String? { + javaClass.getResourceAsStream(filnavn).use { resource -> + Scanner(resource, "UTF-8").use { scanner -> + scanner.useDelimiter("\\A") + return if (scanner.hasNext()) scanner.next() else null + } + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtakHjemmelTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtakHjemmelTest.kt new file mode 100644 index 000000000..b21718ee7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtakHjemmelTest.kt @@ -0,0 +1,416 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesperiode +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingAktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsperiode +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.util.UUID + +class VedtakHjemmelTest { + + var periode: Månedsperiode = Månedsperiode(LocalDate.of(2019, 1, 1), LocalDate.of(2019, 1, 31)) + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når det ikke er foreldelse eller renter bokmål`() { + val vurderingPerioder: Set = aktsomhet(periode) { it } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(null, vurderingPerioder), + VedtakHjemmel.EffektForBruker.FØRSTEGANGSVEDTAK, + Språkkode.NB, + visHjemmelForRenter = true, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdloven § 22-15" + hbHjemmel.lovhjemmelFlertall shouldBe false + } + + private fun lagVedtaksbrevgrunnlag( + vurdertForeldelse: VurdertForeldelse?, + vurderingPerioder: Set, + ): Vedtaksbrevgrunnlag { + val behandling = Testdata.vedtaksbrevbehandling + .copy( + vurderteForeldelser = vurdertForeldelse?.let { setOf(it) } ?: setOf(), + vilkårsvurdering = setOf(Testdata.vilkårsvurdering.copy(perioder = vurderingPerioder)), + ) + return Testdata.vedtaksbrevgrunnlag.copy(behandlinger = setOf(behandling), ytelsestype = Ytelsestype.OVERGANGSSTØNAD) + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når det ikke er foreldelse eller renter nynorsk`() { + val vurderingPerioder: Set = aktsomhet(periode) { it } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(null, vurderingPerioder), + VedtakHjemmel.EffektForBruker.FØRSTEGANGSVEDTAK, + Språkkode.NN, + visHjemmelForRenter = true, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdlova § 22-15" + hbHjemmel.lovhjemmelFlertall shouldBe false + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når det er forsto burde forstått og forsett`() { + val vurderingPerioder: Set = aktsomhet(periode) { + it.copy(aktsomhet = Aktsomhet.FORSETT, ileggRenter = false) + } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(null, vurderingPerioder), + VedtakHjemmel.EffektForBruker.FØRSTEGANGSVEDTAK, + Språkkode.NB, + visHjemmelForRenter = true, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdloven § 22-15" + hbHjemmel.lovhjemmelFlertall shouldBe false + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når det er feilaktig opplysninger og forsett`() { + val vurderingPerioder: Set = + aktsomhet(Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, periode) { + it.copy(aktsomhet = Aktsomhet.FORSETT) + } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(null, vurderingPerioder), + VedtakHjemmel.EffektForBruker.FØRSTEGANGSVEDTAK, + Språkkode.NB, + visHjemmelForRenter = true, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdloven §§ 22-15 og 22-17 a" + hbHjemmel.lovhjemmelFlertall shouldBe true + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når det er feilaktig opplysninger og forsett men frisinn og dermed ikke renter`() { + val vurderingPerioder: Set = + aktsomhet(Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, periode) { + it.copy(aktsomhet = Aktsomhet.FORSETT) + } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(null, vurderingPerioder), + VedtakHjemmel.EffektForBruker.FØRSTEGANGSVEDTAK, + Språkkode.NB, + visHjemmelForRenter = false, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdloven § 22-15" + hbHjemmel.lovhjemmelFlertall shouldBe false + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når det ikke kreves tilbake pga lavt beløp bokmål`() { + val vurderingPerioder: Set = aktsomhet(periode) { + it.copy(tilbakekrevSmåbeløp = false) + } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(null, vurderingPerioder), + VedtakHjemmel.EffektForBruker.FØRSTEGANGSVEDTAK, + Språkkode.NB, + visHjemmelForRenter = true, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdloven § 22-15 sjette ledd" + hbHjemmel.lovhjemmelFlertall shouldBe false + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når det ikke kreves tilbake pga lavt beløp nynorsk`() { + val vurderingPerioder: Set = aktsomhet(periode) { + it.copy(tilbakekrevSmåbeløp = false) + } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(null, vurderingPerioder), + VedtakHjemmel.EffektForBruker.FØRSTEGANGSVEDTAK, + Språkkode.NN, + visHjemmelForRenter = true, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdlova § 22-15 sjette ledd" + hbHjemmel.lovhjemmelFlertall shouldBe false + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når alt er foreldet`() { + val vurdertForeldelse = lagForeldelseperiode(periode) { + it.copy( + foreldelsesvurderingstype = Foreldelsesvurderingstype.FORELDET, + foreldelsesfrist = periode.fom.plusMonths(11).atDay(1), + ) + } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(vurdertForeldelse, emptySet()), + VedtakHjemmel.EffektForBruker.FØRSTEGANGSVEDTAK, + Språkkode.NB, + visHjemmelForRenter = true, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "foreldelsesloven §§ 2 og 3" + hbHjemmel.lovhjemmelFlertall shouldBe true + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når noe er foreldet uten tilleggsfrist og ikke renter`() { + val vurdertForeldelse: VurdertForeldelse = lagForeldelseperiode(periode) { + it.copy( + foreldelsesvurderingstype = Foreldelsesvurderingstype.FORELDET, + foreldelsesfrist = periode.fom.plusMonths(11).atDay(1), + ) + } + val vurderingPerioder: Set = aktsomhet(periode) { + it.copy(aktsomhet = Aktsomhet.GROV_UAKTSOMHET, ileggRenter = false) + } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(vurdertForeldelse, vurderingPerioder), + VedtakHjemmel.EffektForBruker.FØRSTEGANGSVEDTAK, + Språkkode.NB, + visHjemmelForRenter = true, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdloven § 22-15 og foreldelsesloven §§ 2 og 3" + hbHjemmel.lovhjemmelFlertall shouldBe true + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når foreldelse er vurdert men ikke ilagt uten tilleggsfrist og renter`() { + val vurdertForeldelse: VurdertForeldelse = lagForeldelseperiode(periode) { + it.copy(foreldelsesvurderingstype = Foreldelsesvurderingstype.IKKE_FORELDET) + } + val vurderingPerioder: Set = aktsomhet(periode) { + it.copy(aktsomhet = Aktsomhet.GROV_UAKTSOMHET, ileggRenter = true) + } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(vurdertForeldelse, vurderingPerioder), + VedtakHjemmel.EffektForBruker.FØRSTEGANGSVEDTAK, + Språkkode.NB, + visHjemmelForRenter = true, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdloven §§ 22-15 og 22-17 a" + hbHjemmel.lovhjemmelFlertall shouldBe true + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når det er både foreldelse med tilleggsfrist og ikke renter`() { + val vurdertForeldelse: VurdertForeldelse = lagForeldelseperiode(periode) { + it.copy( + foreldelsesvurderingstype = Foreldelsesvurderingstype.TILLEGGSFRIST, + foreldelsesfrist = periode.fom.plusMonths(11).atDay(1), + oppdagelsesdato = periode.fom.plusMonths(5).atDay(1), + ) + } + val vurderingPerioder: Set = aktsomhet(periode) { + it.copy(aktsomhet = Aktsomhet.GROV_UAKTSOMHET, ileggRenter = false) + } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(vurdertForeldelse, vurderingPerioder), + VedtakHjemmel.EffektForBruker.FØRSTEGANGSVEDTAK, + Språkkode.NB, + visHjemmelForRenter = true, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdloven § 22-15 og foreldelsesloven §§ 2, 3 og 10" + hbHjemmel.lovhjemmelFlertall shouldBe true + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når det er både foreldelse med tilleggsfrist og renter`() { + val vurdertForeldelse: VurdertForeldelse = lagForeldelseperiode(periode) { + it.copy( + foreldelsesvurderingstype = Foreldelsesvurderingstype.TILLEGGSFRIST, + foreldelsesfrist = periode.fom.plusMonths(11).atDay(1), + oppdagelsesdato = periode.fom.plusMonths(5).atDay(1), + ) + } + val vurderingPerioder: Set = aktsomhet(periode) { + it.copy(aktsomhet = Aktsomhet.GROV_UAKTSOMHET, ileggRenter = true) + } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag( + vurdertForeldelse, + vurderingPerioder, + ), + VedtakHjemmel.EffektForBruker.FØRSTEGANGSVEDTAK, + Språkkode.NB, + visHjemmelForRenter = true, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdloven §§ 22-15 og 22-17 a og foreldelsesloven §§ 2, 3 og 10" + hbHjemmel.lovhjemmelFlertall shouldBe true + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når det ikke er foreldelse eller renter og er klage fra KA`() { + val vurderingPerioder: Set = aktsomhet(periode) { it } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(null, vurderingPerioder), + VedtakHjemmel.EffektForBruker.ENDRET_TIL_UGUNST_FOR_BRUKER, + Språkkode.NB, + visHjemmelForRenter = true, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdloven § 22-15 og forvaltningsloven § 35 c)" + hbHjemmel.lovhjemmelFlertall shouldBe true + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når det er både foreldelse med tilleggsfrist og renter og er klage fra KA`() { + val vurdertForeldelse: VurdertForeldelse = lagForeldelseperiode(periode) { + it.copy( + foreldelsesvurderingstype = Foreldelsesvurderingstype.TILLEGGSFRIST, + foreldelsesfrist = periode.fom.plusMonths(11).atDay(1), + oppdagelsesdato = periode.fom.plusMonths(5).atDay(1), + ) + } + val vurderingPerioder: Set = aktsomhet(periode) { + it.copy(aktsomhet = Aktsomhet.GROV_UAKTSOMHET, ileggRenter = true) + } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(vurdertForeldelse, vurderingPerioder), + VedtakHjemmel.EffektForBruker.ENDRET_TIL_GUNST_FOR_BRUKER, + Språkkode.NB, + visHjemmelForRenter = true, + klagebehandling = false, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdloven §§ 22-15 og 22-17 a, " + + "foreldelsesloven §§ 2, 3 og 10 og forvaltningsloven § 35 a)" + hbHjemmel.lovhjemmelFlertall shouldBe true + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når det ikke er foreldelse eller renter og er klage fra NFP`() { + val vurderingPerioder: Set = aktsomhet(periode) { it } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(null, vurderingPerioder), + VedtakHjemmel.EffektForBruker.ENDRET_TIL_UGUNST_FOR_BRUKER, + Språkkode.NB, + visHjemmelForRenter = true, + klagebehandling = true, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdloven § 22-15" + hbHjemmel.lovhjemmelFlertall shouldBe false + } + + @Test + fun `laghbHjemmel skal gi riktig hjemmel når det er både foreldelse med tilleggsfrist og renter og er klage fra NFP`() { + val vurdertForeldelse: VurdertForeldelse = lagForeldelseperiode(periode) { + it.copy( + foreldelsesvurderingstype = Foreldelsesvurderingstype.TILLEGGSFRIST, + foreldelsesfrist = periode.fom.plusMonths(11).atDay(1), + oppdagelsesdato = periode.fom.plusMonths(5).atDay(1), + ) + } + val vurderingPerioder: Set = aktsomhet(periode) { + it.copy(aktsomhet = Aktsomhet.GROV_UAKTSOMHET, ileggRenter = true) + } + + val hbHjemmel = VedtakHjemmel.lagHjemmel( + Vedtaksresultat.INGEN_TILBAKEBETALING, + lagVedtaksbrevgrunnlag(vurdertForeldelse, vurderingPerioder), + VedtakHjemmel.EffektForBruker.ENDRET_TIL_GUNST_FOR_BRUKER, + Språkkode.NB, + visHjemmelForRenter = true, + klagebehandling = true, + ) + + hbHjemmel.lovhjemmelVedtak shouldBe "folketrygdloven §§ 22-15 og 22-17 a og foreldelsesloven §§ 2, 3 og 10" + hbHjemmel.lovhjemmelFlertall shouldBe true + } + + private fun lagForeldelseperiode( + periode: Månedsperiode, + oppsett: (Foreldelsesperiode) -> Foreldelsesperiode, + ): VurdertForeldelse { + val periodeBuilder = Foreldelsesperiode( + periode = periode, + foreldelsesvurderingstype = Foreldelsesvurderingstype.IKKE_VURDERT, + begrunnelse = "bob", + ) + return VurdertForeldelse( + behandlingId = UUID.randomUUID(), + foreldelsesperioder = setOf(oppsett(periodeBuilder)), + ) + } + + private fun aktsomhet( + periode: Månedsperiode, + oppsett: (VilkårsvurderingAktsomhet) -> VilkårsvurderingAktsomhet, + ): Set { + return aktsomhet(Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, periode, oppsett) + } + + private fun aktsomhet( + resultat: Vilkårsvurderingsresultat, + periode: Månedsperiode, + oppsett: (VilkårsvurderingAktsomhet) -> VilkårsvurderingAktsomhet, + ): Set { + val aktsomhet: VilkårsvurderingAktsomhet = + oppsett(VilkårsvurderingAktsomhet(aktsomhet = Aktsomhet.SIMPEL_UAKTSOMHET, begrunnelse = "foo")) + val vurderingPeriode = Vilkårsvurderingsperiode( + periode = periode, + vilkårsvurderingsresultat = resultat, + begrunnelse = "foo", + aktsomhet = aktsomhet, + ) + + return setOf(vurderingPeriode.copy(aktsomhet = aktsomhet)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevServiceTest.kt new file mode 100644 index 000000000..1eadf7e9e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevServiceTest.kt @@ -0,0 +1,636 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.FritekstavsnittDto +import no.nav.familie.tilbake.api.dto.HentForhåndvisningVedtaksbrevPdfDto +import no.nav.familie.tilbake.api.dto.PeriodeMedTekstDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsak +import no.nav.familie.tilbake.behandling.domain.Behandlingsårsakstype +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandling.domain.Verge +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.DistribusjonshåndteringService +import no.nav.familie.tilbake.dokumentbestilling.felles.Adresseinfo +import no.nav.familie.tilbake.dokumentbestilling.felles.Brevmottager +import no.nav.familie.tilbake.dokumentbestilling.felles.EksterneDataForBrevService +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.Brevdata +import no.nav.familie.tilbake.dokumentbestilling.felles.pdf.PdfBrevService +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingRepository +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingService +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetalingsperiode +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.integration.pdl.internal.Personinfo +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.pdfgen.validering.PdfaValidator +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingRepository +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurdering +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingAktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.VilkårsvurderingSærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsperiode +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.apache.commons.lang3.RandomStringUtils +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.time.YearMonth +import java.util.UUID + +internal class VedtaksbrevServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var vedtaksbrevgeneratorService: VedtaksbrevgeneratorService + + @Autowired + private lateinit var vedtaksbrevgrunnlagService: VedtaksbrevgunnlagService + + @Autowired + private lateinit var faktaRepository: FaktaFeilutbetalingRepository + + @Autowired + private lateinit var vilkårsvurderingRepository: VilkårsvurderingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var vedtaksbrevsoppsummeringRepository: VedtaksbrevsoppsummeringRepository + + @Autowired + private lateinit var vedtaksbrevsperiodeRepository: VedtaksbrevsperiodeRepository + + @Autowired + private lateinit var vilkårsvurderingService: VilkårsvurderingService + + @Autowired + private lateinit var faktaFeilutbetalingService: FaktaFeilutbetalingService + + @Autowired + private lateinit var pdfBrevService: PdfBrevService + + private lateinit var spyPdfBrevService: PdfBrevService + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + private val eksterneDataForBrevService: EksterneDataForBrevService = mockk() + + private lateinit var vedtaksbrevService: VedtaksbrevService + + @Autowired + private lateinit var sendBrevService: DistribusjonshåndteringService + + @Autowired + private lateinit var featureToggleService: FeatureToggleService + + private lateinit var behandling: Behandling + private lateinit var fagsak: Fagsak + + @BeforeEach + fun init() { + spyPdfBrevService = spyk(pdfBrevService) + vedtaksbrevService = VedtaksbrevService( + behandlingRepository, + vedtaksbrevgeneratorService, + vedtaksbrevgrunnlagService, + faktaRepository, + vilkårsvurderingRepository, + fagsakRepository, + vedtaksbrevsoppsummeringRepository, + vedtaksbrevsperiodeRepository, + spyPdfBrevService, + sendBrevService, + featureToggleService, + ) + + fagsak = fagsakRepository.insert(Testdata.fagsak) + behandling = behandlingRepository.insert(Testdata.behandling) + val kravgrunnlagsperiode432 = Testdata.kravgrunnlag431.perioder.first().copy(periode = Månedsperiode(YearMonth.of(2023, 3), YearMonth.of(2023, 4))) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431.copy(perioder = setOf(kravgrunnlagsperiode432))) + vilkårsvurderingRepository.insert( + Testdata.vilkårsvurdering + .copy(perioder = setOf(Testdata.vilkårsperiode.copy(periode = Månedsperiode(YearMonth.of(2023, 3), YearMonth.of(2023, 4)), godTro = null))), + ) + faktaRepository.insert( + Testdata.faktaFeilutbetaling.copy( + perioder = setOf( + FaktaFeilutbetalingsperiode( + periode = Månedsperiode("2020-04" to "2022-08"), + hendelsestype = Hendelsestype.ANNET, + hendelsesundertype = Hendelsesundertype.ANNET_FRITEKST, + ), + FaktaFeilutbetalingsperiode( + periode = Månedsperiode("2023-03" to "2023-04"), + hendelsestype = Hendelsestype.ANNET, + hendelsesundertype = Hendelsesundertype.ANNET_FRITEKST, + ), + ), + ), + ) + + val personinfo = Personinfo("28056325874", LocalDate.now(), "Fiona") + + every { eksterneDataForBrevService.hentPerson(Testdata.fagsak.bruker.ident, any()) }.returns(personinfo) + every { eksterneDataForBrevService.hentSaksbehandlernavn(Testdata.behandling.ansvarligSaksbehandler) } + .returns("Ansvarlig O'Saksbehandler") + every { eksterneDataForBrevService.hentSaksbehandlernavn(Testdata.behandling.ansvarligBeslutter!!) } + .returns("Ansvarlig O'Beslutter") + every { + eksterneDataForBrevService.hentAdresse(any(), any(), any(), any()) + }.returns(Adresseinfo("12345678901", "Test")) + } + + @Test + fun `sendVedtaksbrev skal kalle pfdBrevService med behandling, fagsak og genererte brevdata`() { + val behandlingSlot = slot() + val fagsakSlot = slot() + val brevtypeSlot = slot() + val brevdataSlot = slot() + + vedtaksbrevService.sendVedtaksbrev(Testdata.behandling, Brevmottager.BRUKER) + + verify { + spyPdfBrevService.sendBrev( + capture(behandlingSlot), + capture(fagsakSlot), + capture(brevtypeSlot), + capture(brevdataSlot), + ) + } + behandlingSlot.captured shouldBe Testdata.behandling + fagsakSlot.captured shouldBe fagsak + brevtypeSlot.captured shouldBe Brevtype.VEDTAK + brevdataSlot.captured.overskrift shouldBe "Du må betale tilbake barnetrygden" + } + + @Test + fun `hentForhåndsvisningVedtaksbrevMedVedleggSomPdf skal generere en gyldig pdf`() { + val dto = HentForhåndvisningVedtaksbrevPdfDto( + Testdata.behandling.id, + "Dette er en stor og gild oppsummeringstekst", + listOf( + PeriodeMedTekstDto( + Datoperiode( + LocalDate.now().minusDays(1), + LocalDate.now(), + ), + "Friktekst om fakta", + "Friktekst om foreldelse", + "Friktekst om vilkår", + """Friktekst & > < ' "særligeGrunner""", + "Friktekst om særligeGrunnerAnnet", + ), + ), + ) + + val bytes = vedtaksbrevService.hentForhåndsvisningVedtaksbrevMedVedleggSomPdf(dto) + // File("test.pdf").writeBytes(bytes) + + PdfaValidator.validatePdf(bytes) + } + + @Test + fun `hentForhåndsvisningVedtaksbrevMedVedleggSomPdf skal generere en gyldig pdf med xml-spesialtegn`() { + val bytes = vedtaksbrevService.hentForhåndsvisningVedtaksbrevMedVedleggSomPdf(forhåndvisningDto) + +// File("test.pdf").writeBytes(bytes) + PdfaValidator.validatePdf(bytes) + } + + @Test + fun `hentForhåndsvisningVedtaksbrevSomTekst genererer avsnitt med tekst for forhåndsvisning av vedtaksbrev`() { + val avsnitt = vedtaksbrevService.hentVedtaksbrevSomTekst(Testdata.behandling.id) + + avsnitt.shouldHaveSize(3) + avsnitt.first().overskrift shouldBe "Du må betale tilbake barnetrygden" + } + + @Test + fun `lagreFriteksterFraSaksbehandler skal ikke lagre fritekster når en av de periodene er ugyldig`() { + lagFakta() + val perioderMedTekst = listOf( + PeriodeMedTekstDto( + periode = Datoperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 3)), + faktaAvsnitt = "fakta fritekst", + vilkårAvsnitt = "vilkår fritekst", + ), + PeriodeMedTekstDto( + periode = Datoperiode(YearMonth.of(2021, 10), YearMonth.of(2021, 10)), + faktaAvsnitt = "ugyldig", + vilkårAvsnitt = "ugyldig", + ), + ) + val fritekstAvsnittDto = FritekstavsnittDto( + oppsummeringstekst = "oppsummeringstekst", + perioderMedTekst = perioderMedTekst, + ) + + val exception = shouldThrow { + vedtaksbrevService.lagreFriteksterFraSaksbehandler( + behandlingId = behandling.id, + fritekstAvsnittDto, + ) + } + exception.message shouldBe "Periode 2021-10-01-2021-10-31 er ugyldig for behandling ${behandling.id}" + } + + @Test + fun `lagreFriteksterFraSaksbehandler skal ikke lagre fritekster når oppsummeringstekst er for lang`() { + lagFakta() + val exception = shouldThrow { + vedtaksbrevService.lagreFriteksterFraSaksbehandler( + behandlingId = behandling.id, + lagFritekstAvsnittDto( + "fakta", + RandomStringUtils.random(5000), + ), + ) + } + exception.message shouldBe "Oppsummeringstekst er for lang for behandling ${behandling.id}" + } + + @Test + fun `lagreFriteksterFraSaksbehandler skal ikke lagre når fritekst mangler for ANNET særliggrunner begrunnelse`() { + lagFakta() + lagVilkårsvurdering() + val exception = shouldThrow { + vedtaksbrevService.lagreFriteksterFraSaksbehandler( + behandlingId = behandling.id, + lagFritekstAvsnittDto("fakta", "fakta data"), + ) + } + exception.message shouldBe "Mangler ANNET Særliggrunner fritekst for " + + "${Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 3))}" + } + + @Test + fun `lagreFriteksterFraSaksbehandler skal ikke lagre når fritekst mangler for alle fakta perioder`() { + lagFakta() + val exception = shouldThrow { + vedtaksbrevService.lagreFriteksterFraSaksbehandler( + behandlingId = behandling.id, + lagFritekstAvsnittDto(), + ) + } + exception.message shouldBe "Mangler fakta fritekst for alle fakta perioder" + } + + @Test + fun `lagreFriteksterFraSaksbehandler skal ikke lagre når fritekst mangler for en av fakta perioder`() { + lagFakta() + val perioderMedTekst = listOf( + PeriodeMedTekstDto( + periode = Datoperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)), + faktaAvsnitt = "fakta fritekst", + vilkårAvsnitt = "vilkår fritekst", + ), + PeriodeMedTekstDto( + periode = Datoperiode(YearMonth.of(2021, 2), YearMonth.of(2021, 2)), + faktaAvsnitt = "fakta fritekst", + vilkårAvsnitt = "vilkår fritekst", + ), + PeriodeMedTekstDto( + periode = Datoperiode(YearMonth.of(2021, 3), YearMonth.of(2021, 3)), + vilkårAvsnitt = "vilkår fritekst", + ), + ) + val fritekstAvsnittDto = FritekstavsnittDto( + oppsummeringstekst = "oppsummeringstekst", + perioderMedTekst = perioderMedTekst, + ) + + val exception = shouldThrow { + vedtaksbrevService.lagreFriteksterFraSaksbehandler( + behandlingId = behandling.id, + fritekstavsnittDto = fritekstAvsnittDto, + ) + } + exception.message shouldBe "Mangler fakta fritekst for ${LocalDate.of(2021, 3, 1)}-" + + "${LocalDate.of(2021, 3, 31)}" + } + + @Test + fun `lagreFriteksterFraSaksbehandler skal lagre fritekst`() { + lagFakta() + lagVilkårsvurdering() + + val fritekstAvsnittDto = lagFritekstAvsnittDto( + faktaFritekst = "fakta fritekst", + oppsummeringstekst = "oppsummering fritekst", + særligGrunnerAnnetFritekst = "særliggrunner annet fritekst", + ) + + vedtaksbrevService.lagreFriteksterFraSaksbehandler( + behandlingId = behandling.id, + fritekstavsnittDto = fritekstAvsnittDto, + ) + + val avsnittene = vedtaksbrevService.hentVedtaksbrevSomTekst(behandling.id) + avsnittene.shouldNotBeEmpty() + avsnittene.size shouldBe 3 + + val oppsummeringsavsnitt = avsnittene.firstOrNull { Avsnittstype.OPPSUMMERING == it.avsnittstype } + oppsummeringsavsnitt.shouldNotBeNull() + oppsummeringsavsnitt.underavsnittsliste.size shouldBe 2 + val oppsummeringsunderavsnitt1 = oppsummeringsavsnitt.underavsnittsliste[0] + assertUnderavsnitt( + underavsnitt = oppsummeringsunderavsnitt1, + fritekst = "", + fritekstTillatt = false, + fritekstPåkrevet = false, + ) + val oppsummeringsunderavsnitt2 = oppsummeringsavsnitt.underavsnittsliste[1] + assertUnderavsnitt( + underavsnitt = oppsummeringsunderavsnitt2, + fritekst = "oppsummering fritekst", + fritekstTillatt = true, + fritekstPåkrevet = false, + ) + + val periodeAvsnitter = avsnittene.firstOrNull { Avsnittstype.PERIODE == it.avsnittstype } + periodeAvsnitter.shouldNotBeNull() + periodeAvsnitter.fom shouldBe LocalDate.of(2021, 1, 1) + periodeAvsnitter.tom shouldBe LocalDate.of(2021, 3, 31) + + periodeAvsnitter.underavsnittsliste.size shouldBe 7 + val faktaUnderavsnitt = periodeAvsnitter.underavsnittsliste + .firstOrNull { Underavsnittstype.FAKTA == it.underavsnittstype } + faktaUnderavsnitt.shouldNotBeNull() + assertUnderavsnitt( + underavsnitt = faktaUnderavsnitt, + fritekst = "fakta fritekst", + fritekstTillatt = true, + fritekstPåkrevet = true, + ) + + val foreldelseUnderavsnitt = periodeAvsnitter.underavsnittsliste + .firstOrNull { Underavsnittstype.FORELDELSE == it.underavsnittstype } + foreldelseUnderavsnitt.shouldBeNull() // periodene er ikke foreldet + + val vilkårUnderavsnitter = periodeAvsnitter.underavsnittsliste.filter { Underavsnittstype.VILKÅR == it.underavsnittstype } + vilkårUnderavsnitter.size shouldBe 2 + val vilkårUnderavsnitt1 = vilkårUnderavsnitter[0] + assertUnderavsnitt( + underavsnitt = vilkårUnderavsnitt1, + fritekst = "", + fritekstTillatt = false, + fritekstPåkrevet = false, + ) + val vilkårUnderavsnitt2 = vilkårUnderavsnitter[1] + assertUnderavsnitt( + underavsnitt = vilkårUnderavsnitt2, + fritekst = "vilkår fritekst", + fritekstTillatt = true, + fritekstPåkrevet = false, + ) + + val særligGrunnerUnderavsnitt = periodeAvsnitter.underavsnittsliste + .firstOrNull { Underavsnittstype.SÆRLIGEGRUNNER == it.underavsnittstype } + særligGrunnerUnderavsnitt.shouldNotBeNull() + assertUnderavsnitt( + underavsnitt = særligGrunnerUnderavsnitt, + fritekst = "særliggrunner fritekst", + fritekstTillatt = true, + fritekstPåkrevet = false, + ) + + val særligGrunnerAnnetUnderavsnitt = periodeAvsnitter.underavsnittsliste + .firstOrNull { Underavsnittstype.SÆRLIGEGRUNNER_ANNET == it.underavsnittstype } + særligGrunnerAnnetUnderavsnitt.shouldNotBeNull() + assertUnderavsnitt( + underavsnitt = særligGrunnerAnnetUnderavsnitt, + fritekst = "særliggrunner annet fritekst", + fritekstTillatt = true, + fritekstPåkrevet = true, + ) + + val tilleggsavsnitt = avsnittene.firstOrNull { Avsnittstype.TILLEGGSINFORMASJON == it.avsnittstype } + tilleggsavsnitt.shouldNotBeNull() + } + + @Test + fun `lagreUtkastAvFriteksterFraSaksbehandler skal lagre selv når påkrevet fritekst mangler for alle fakta perioder`() { + lagFakta() + lagVilkårsvurdering() + + val fritekstAvsnittDto = lagFritekstAvsnittDto( + oppsummeringstekst = "oppsummering fritekst", + særligGrunnerAnnetFritekst = "særliggrunner annet fritekst", + ) + vedtaksbrevService.lagreUtkastAvFritekster( + behandlingId = behandling.id, + fritekstAvsnittDto, + ) + + val avsnittene = vedtaksbrevService.hentVedtaksbrevSomTekst(behandling.id) + avsnittene.shouldNotBeEmpty() + avsnittene.size shouldBe 3 + } + + @Test + fun `lagreUtkastAvFriteksterFraSaksbehandler skal lagre selv når påkrevet fritekst mangler for ANNET særliggrunner begrunnelse`() { + lagFakta() + lagVilkårsvurdering() + + val fritekstAvsnittDto = lagFritekstAvsnittDto( + faktaFritekst = "fakta fritekst", + oppsummeringstekst = "oppsummering fritekst", + ) + + vedtaksbrevService.lagreUtkastAvFritekster( + behandlingId = behandling.id, + fritekstavsnittDto = fritekstAvsnittDto, + ) + + val avsnittene = vedtaksbrevService.hentVedtaksbrevSomTekst(behandling.id) + avsnittene.shouldNotBeEmpty() + avsnittene.size shouldBe 3 + } + + @Test + fun `lagreUtkastAvFriteksterFraSaksbehandler skal lagre selv når påkrevet fritekst mangler for oppsummering`() { + var lokalBehandling = Testdata.revurdering.copy( + id = UUID.randomUUID(), + eksternBrukId = UUID.randomUUID(), + årsaker = setOf( + Behandlingsårsak( + originalBehandlingId = behandling.id, + type = Behandlingsårsakstype.REVURDERING_OPPLYSNINGER_OM_VILKÅR, + ), + ), + ) + lokalBehandling = behandlingRepository.insert(lokalBehandling) + + lagFakta(lokalBehandling.id) + lagVilkårsvurdering(lokalBehandling.id) + + val fritekstAvsnittDto = lagFritekstAvsnittDto( + faktaFritekst = "fakta fritekst", + særligGrunnerAnnetFritekst = "særliggrunner annet fritekst", + ) + + vedtaksbrevService.lagreUtkastAvFritekster( + behandlingId = lokalBehandling.id, + fritekstavsnittDto = fritekstAvsnittDto, + ) + + val avsnittene = vedtaksbrevService.hentVedtaksbrevSomTekst(lokalBehandling.id) + avsnittene.shouldNotBeEmpty() + avsnittene.size shouldBe 3 + } + + @Test + fun `lagreFriteksterFraSaksbehandler skal ikke lagre fritekster når påkrevet oppsummeringstekst mangler`() { + var lokalBehandling = Testdata.revurdering.copy( + id = UUID.randomUUID(), + eksternBrukId = UUID.randomUUID(), + årsaker = setOf( + Behandlingsårsak( + originalBehandlingId = behandling.id, + type = Behandlingsårsakstype.REVURDERING_OPPLYSNINGER_OM_VILKÅR, + ), + ), + ) + lokalBehandling = behandlingRepository.insert(lokalBehandling) + lagFakta(lokalBehandling.id) + lagVilkårsvurdering(lokalBehandling.id) + + val exception = shouldThrow { + vedtaksbrevService.lagreFriteksterFraSaksbehandler( + behandlingId = lokalBehandling.id, + lagFritekstAvsnittDto( + faktaFritekst = "fakta", + særligGrunnerAnnetFritekst = "test", + ), + ) + } + exception.message shouldBe "oppsummering fritekst påkrevet for revurdering ${lokalBehandling.id}" + } + + private fun lagFritekstAvsnittDto( + faktaFritekst: String? = null, + oppsummeringstekst: String? = null, + særligGrunnerAnnetFritekst: String? = null, + ): FritekstavsnittDto { + val perioderMedTekst = listOf( + PeriodeMedTekstDto( + periode = Datoperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 3)), + faktaAvsnitt = faktaFritekst, + vilkårAvsnitt = "vilkår fritekst", + foreldelseAvsnitt = "foreldelse fritekst", + særligeGrunnerAvsnitt = "særliggrunner fritekst", + særligeGrunnerAnnetAvsnitt = særligGrunnerAnnetFritekst, + ), + ) + return FritekstavsnittDto( + oppsummeringstekst = oppsummeringstekst, + perioderMedTekst = perioderMedTekst, + ) + } + + private fun lagFakta(behandlingId: UUID = behandling.id) { + val faktaFeilutbetaltePerioder = + setOf( + FaktaFeilutbetalingsperiode( + periode = Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 3)), + hendelsestype = Hendelsestype.ANNET, + hendelsesundertype = Hendelsesundertype.ANNET_FRITEKST, + ), + ) + faktaFeilutbetalingService.deaktiverEksisterendeFaktaOmFeilutbetaling(behandlingId) + faktaRepository.insert( + FaktaFeilutbetaling( + behandlingId = behandlingId, + begrunnelse = "fakta begrrunnelse", + perioder = faktaFeilutbetaltePerioder, + ), + ) + } + + private fun lagVilkårsvurdering(behandlingId: UUID = behandling.id) { + val aktsomhet = + VilkårsvurderingAktsomhet( + aktsomhet = Aktsomhet.GROV_UAKTSOMHET, + særligeGrunnerBegrunnelse = "Særlig grunner begrunnelse", + særligeGrunnerTilReduksjon = false, + vilkårsvurderingSærligeGrunner = + setOf( + VilkårsvurderingSærligGrunn( + særligGrunn = SærligGrunn.ANNET, + begrunnelse = "Annet begrunnelse", + ), + ), + begrunnelse = "aktsomhet begrunnelse", + ) + val vilkårsvurderingPeriode = + Vilkårsvurderingsperiode( + periode = Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 3)), + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + begrunnelse = "Vilkårsvurdering begrunnelse", + aktsomhet = aktsomhet, + ) + vilkårsvurderingService.deaktiverEksisterendeVilkårsvurdering(behandlingId) + vilkårsvurderingRepository.insert( + Vilkårsvurdering( + behandlingId = behandlingId, + perioder = setOf(vilkårsvurderingPeriode), + ), + ) + } + + private fun assertUnderavsnitt( + underavsnitt: Underavsnitt, + fritekst: String, + fritekstTillatt: Boolean, + fritekstPåkrevet: Boolean, + ) { + underavsnitt.fritekst shouldBe fritekst + underavsnitt.fritekstTillatt shouldBe fritekstTillatt + underavsnitt.fritekstPåkrevet shouldBe fritekstPåkrevet + } + + companion object { + + private val forhåndvisningDto = HentForhåndvisningVedtaksbrevPdfDto( + Testdata.behandling.id, + "Dette er en stor og gild oppsummeringstekst", + listOf( + PeriodeMedTekstDto( + Datoperiode( + LocalDate.now().minusDays(1), + LocalDate.now(), + ), + faktaAvsnitt = "&bob", + vilkårAvsnitt = "", + særligeGrunnerAnnetAvsnitt = "'bob' \"bob\"", + ), + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsoppsummeringRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsoppsummeringRepositoryTest.kt new file mode 100644 index 000000000..131c02e6f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsoppsummeringRepositoryTest.kt @@ -0,0 +1,63 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsoppsummering +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class VedtaksbrevsoppsummeringRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var vedtaksbrevsoppsummeringRepository: VedtaksbrevsoppsummeringRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + private val vedtaksbrevsoppsummering = Testdata.vedtaksbrevsoppsummering + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(Testdata.behandling) + } + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av Vedtaksbrevsoppsummering til basen`() { + vedtaksbrevsoppsummeringRepository.insert(vedtaksbrevsoppsummering) + + val lagretVedtaksbrevsoppsummering = vedtaksbrevsoppsummeringRepository.findByIdOrThrow(vedtaksbrevsoppsummering.id) + lagretVedtaksbrevsoppsummering.shouldBeEqualToComparingFieldsExcept( + vedtaksbrevsoppsummering, + Vedtaksbrevsoppsummering::sporbar, + Vedtaksbrevsoppsummering::versjon, + ) + lagretVedtaksbrevsoppsummering.versjon shouldBe 1 + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av Vedtaksbrevsoppsummering i basen`() { + vedtaksbrevsoppsummeringRepository.insert(vedtaksbrevsoppsummering) + var lagretVedtaksbrevsoppsummering = vedtaksbrevsoppsummeringRepository.findByIdOrThrow(vedtaksbrevsoppsummering.id) + val oppdatertVedtaksbrevsoppsummering = lagretVedtaksbrevsoppsummering.copy(oppsummeringFritekst = "bob") + + vedtaksbrevsoppsummeringRepository.update(oppdatertVedtaksbrevsoppsummering) + + lagretVedtaksbrevsoppsummering = vedtaksbrevsoppsummeringRepository.findByIdOrThrow(vedtaksbrevsoppsummering.id) + lagretVedtaksbrevsoppsummering.shouldBeEqualToComparingFieldsExcept( + oppdatertVedtaksbrevsoppsummering, + Vedtaksbrevsoppsummering::sporbar, + Vedtaksbrevsoppsummering::versjon, + ) + lagretVedtaksbrevsoppsummering.versjon shouldBe 2 + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsperiodeRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsperiodeRepositoryTest.kt new file mode 100644 index 000000000..af21e1b6e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/VedtaksbrevsperiodeRepositoryTest.kt @@ -0,0 +1,64 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.domain.Vedtaksbrevsperiode +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class VedtaksbrevsperiodeRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var vedtaksbrevsperiodeRepository: VedtaksbrevsperiodeRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + private val vedtaksbrevsperiode = Testdata.vedtaksbrevsperiode + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(Testdata.behandling) + } + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av Vedtaksbrevsperiode til basen`() { + vedtaksbrevsperiodeRepository.insert(vedtaksbrevsperiode) + + val lagretVedtaksbrevsperiode = vedtaksbrevsperiodeRepository.findByIdOrThrow(vedtaksbrevsperiode.id) + + lagretVedtaksbrevsperiode.shouldBeEqualToComparingFieldsExcept( + vedtaksbrevsperiode, + Vedtaksbrevsperiode::sporbar, + Vedtaksbrevsperiode::versjon, + ) + lagretVedtaksbrevsperiode.versjon shouldBe 1 + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av Vedtaksbrevsperiode i basen`() { + vedtaksbrevsperiodeRepository.insert(vedtaksbrevsperiode) + var lagretVedtaksbrevsperiode = vedtaksbrevsperiodeRepository.findByIdOrThrow(vedtaksbrevsperiode.id) + val oppdatertVedtaksbrevsperiode = lagretVedtaksbrevsperiode.copy(fritekst = "bob") + + vedtaksbrevsperiodeRepository.update(oppdatertVedtaksbrevsperiode) + + lagretVedtaksbrevsperiode = vedtaksbrevsperiodeRepository.findByIdOrThrow(vedtaksbrevsperiode.id) + lagretVedtaksbrevsperiode.shouldBeEqualToComparingFieldsExcept( + oppdatertVedtaksbrevsperiode, + Vedtaksbrevsperiode::sporbar, + Vedtaksbrevsperiode::versjon, + ) + lagretVedtaksbrevsperiode.versjon shouldBe 2 + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbResultatTestBuilder.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbResultatTestBuilder.kt new file mode 100644 index 000000000..fdb03ebc5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/dokumentbestilling/vedtak/handlebars/dto/periode/HbResultatTestBuilder.kt @@ -0,0 +1,18 @@ +package no.nav.familie.tilbake.dokumentbestilling.vedtak.handlebars.dto.periode + +import java.math.BigDecimal + +object HbResultatTestBuilder { + + fun forTilbakekrevesBeløp(tilbakekrevesBeløp: Int): HbResultat { + return forTilbakekrevesBeløpOgRenter(tilbakekrevesBeløp, 0) + } + + fun forTilbakekrevesBeløpOgRenter(tilbakekrevesBeløp: Int, renter: Int): HbResultat { + return HbResultat( + tilbakekrevesBeløp = BigDecimal.valueOf(tilbakekrevesBeløp.toLong()), + rentebeløp = BigDecimal.valueOf(renter.toLong()), + tilbakekrevesBeløpUtenSkattMedRenter = BigDecimal.valueOf(tilbakekrevesBeløp.toLong()), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingRepositoryTest.kt new file mode 100644 index 000000000..913791165 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingRepositoryTest.kt @@ -0,0 +1,79 @@ +package no.nav.familie.tilbake.faktaomfeilutbetaling + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class FaktaFeilutbetalingRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var faktaFeilutbetalingRepository: FaktaFeilutbetalingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + private val fagsak = Testdata.fagsak + private val behandling = Testdata.behandling + private val faktaFeilutbetaling = Testdata.faktaFeilutbetaling + + @BeforeEach + fun init() { + fagsakRepository.insert(fagsak) + behandlingRepository.insert(behandling) + } + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av FaktaFeilutbetaling til basen`() { + faktaFeilutbetalingRepository.insert(faktaFeilutbetaling) + + val lagretFaktaFeilutbetaling = faktaFeilutbetalingRepository.findByIdOrThrow(faktaFeilutbetaling.id) + + lagretFaktaFeilutbetaling.shouldBeEqualToComparingFieldsExcept( + faktaFeilutbetaling, + FaktaFeilutbetaling::sporbar, + FaktaFeilutbetaling::versjon, + ) + lagretFaktaFeilutbetaling.versjon shouldBe 1 + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av FaktaFeilutbetaling i basen`() { + faktaFeilutbetalingRepository.insert(faktaFeilutbetaling) + var lagretFaktaFeilutbetaling = faktaFeilutbetalingRepository.findByIdOrThrow(faktaFeilutbetaling.id) + val oppdatertFaktaFeilutbetaling = lagretFaktaFeilutbetaling.copy(begrunnelse = "bob") + + faktaFeilutbetalingRepository.update(oppdatertFaktaFeilutbetaling) + + lagretFaktaFeilutbetaling = faktaFeilutbetalingRepository.findByIdOrThrow(faktaFeilutbetaling.id) + lagretFaktaFeilutbetaling.shouldBeEqualToComparingFieldsExcept( + oppdatertFaktaFeilutbetaling, + FaktaFeilutbetaling::sporbar, + FaktaFeilutbetaling::versjon, + ) + lagretFaktaFeilutbetaling.versjon shouldBe 2 + } + + @Test + fun `findByBehandlingIdAndAktivIsTrue returnerer resultat når det finnes en forekomst`() { + faktaFeilutbetalingRepository.insert(faktaFeilutbetaling) + + val findByBehandlingId = faktaFeilutbetalingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + + findByBehandlingId?.shouldBeEqualToComparingFieldsExcept( + faktaFeilutbetaling, + FaktaFeilutbetaling::sporbar, + FaktaFeilutbetaling::versjon, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingServiceTest.kt new file mode 100644 index 000000000..9a770e07f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/FaktaFeilutbetalingServiceTest.kt @@ -0,0 +1,162 @@ +package no.nav.familie.tilbake.faktaomfeilutbetaling + +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.FaktaFeilutbetalingDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetalingsperiode +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsperiode432 +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.YearMonth +import java.util.UUID + +internal class FaktaFeilutbetalingServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var faktaFeilutbetalingRepository: FaktaFeilutbetalingRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var faktaFeilutbetalingService: FaktaFeilutbetalingService + + private val behandling = Testdata.behandling + private val periode = Månedsperiode( + fom = YearMonth.now().minusMonths(2), + tom = YearMonth.now(), + ) + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(behandling) + val kravgrunnlag = Testdata.kravgrunnlag431 + .copy( + perioder = setOf( + Kravgrunnlagsperiode432( + periode = periode, + beløp = setOf( + Testdata.feilKravgrunnlagsbeløp433, + Testdata.ytelKravgrunnlagsbeløp433, + ), + månedligSkattebeløp = BigDecimal("123.11"), + ), + ), + ) + kravgrunnlagRepository.insert(kravgrunnlag) + } + + @Test + fun `hentFaktaomfeilutbetaling skal hente fakta om feilutbetaling for en gitt behandling`() { + lagFaktaomfeilutbetaling(behandling.id) + + val faktaFeilutbetalingDto = faktaFeilutbetalingService.hentFaktaomfeilutbetaling(behandlingId = behandling.id) + + faktaFeilutbetalingDto.begrunnelse shouldBe "Fakta begrunnelse" + val varsletData = behandling.aktivtVarsel + faktaFeilutbetalingDto.varsletBeløp shouldBe varsletData?.varselbeløp + + assertFagsystemsbehandling(faktaFeilutbetalingDto, behandling) + assertFeilutbetaltePerioder( + faktaFeilutbetalingDto = faktaFeilutbetalingDto, + hendelsestype = Hendelsestype.ANNET, + hendelsesundertype = Hendelsesundertype.ANNET_FRITEKST, + ) + } + + @Test + fun `hentFaktaomfeilutbetaling skal hente fakta om feilutbetaling for behandling uten varsel`() { + val lagretBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + val oppdatertBehandling = lagretBehandling.copy(varsler = emptySet()) + behandlingRepository.update(oppdatertBehandling) + lagFaktaomfeilutbetaling(behandlingId = oppdatertBehandling.id) + + val faktaFeilutbetalingDto = faktaFeilutbetalingService.hentFaktaomfeilutbetaling(behandlingId = oppdatertBehandling.id) + + faktaFeilutbetalingDto.begrunnelse shouldBe "Fakta begrunnelse" + faktaFeilutbetalingDto.varsletBeløp.shouldBeNull() + assertFagsystemsbehandling(faktaFeilutbetalingDto, behandling) + assertFeilutbetaltePerioder( + faktaFeilutbetalingDto = faktaFeilutbetalingDto, + hendelsestype = Hendelsestype.ANNET, + hendelsesundertype = Hendelsesundertype.ANNET_FRITEKST, + ) + } + + @Test + fun `hentFaktaomfeilutbetaling skal hente fakta om feilutbetaling første gang for en gitt behandling`() { + val faktaFeilutbetalingDto = faktaFeilutbetalingService.hentFaktaomfeilutbetaling(behandlingId = behandling.id) + + faktaFeilutbetalingDto.begrunnelse shouldBe "" + faktaFeilutbetalingDto.varsletBeløp shouldBe behandling.aktivtVarsel?.varselbeløp + assertFagsystemsbehandling(faktaFeilutbetalingDto, behandling) + assertFeilutbetaltePerioder( + faktaFeilutbetalingDto = faktaFeilutbetalingDto, + hendelsestype = null, + hendelsesundertype = null, + ) + } + + private fun lagFaktaomfeilutbetaling(behandlingId: UUID) { + val faktaPerioder = FaktaFeilutbetalingsperiode( + periode = periode, + hendelsestype = Hendelsestype.ANNET, + hendelsesundertype = Hendelsesundertype.ANNET_FRITEKST, + ) + val faktaFeilutbetaling = FaktaFeilutbetaling( + behandlingId = behandlingId, + begrunnelse = "Fakta begrunnelse", + perioder = setOf(faktaPerioder), + ) + faktaFeilutbetalingRepository.insert(faktaFeilutbetaling) + } + + private fun assertFagsystemsbehandling( + faktaFeilutbetalingDto: FaktaFeilutbetalingDto, + behandling: Behandling, + ) { + val fagsystemsbehandling = behandling.aktivFagsystemsbehandling + val faktainfo = faktaFeilutbetalingDto.faktainfo + faktainfo.tilbakekrevingsvalg shouldBe fagsystemsbehandling.tilbakekrevingsvalg + faktaFeilutbetalingDto.revurderingsvedtaksdato shouldBe fagsystemsbehandling.revurderingsvedtaksdato + faktainfo.revurderingsresultat shouldBe fagsystemsbehandling.resultat + faktainfo.revurderingsårsak shouldBe fagsystemsbehandling.årsak + faktainfo.konsekvensForYtelser.shouldBeEmpty() + } + + private fun assertFeilutbetaltePerioder( + faktaFeilutbetalingDto: FaktaFeilutbetalingDto, + hendelsestype: Hendelsestype?, + hendelsesundertype: Hendelsesundertype?, + ) { + faktaFeilutbetalingDto.totalFeilutbetaltPeriode shouldBe periode.toDatoperiode() + faktaFeilutbetalingDto.totaltFeilutbetaltBeløp shouldBe BigDecimal.valueOf(1000000, 2) + + faktaFeilutbetalingDto.feilutbetaltePerioder.size shouldBe 1 + val feilutbetaltePeriode = faktaFeilutbetalingDto.feilutbetaltePerioder.first() + feilutbetaltePeriode.hendelsestype shouldBe hendelsestype + feilutbetaltePeriode.hendelsesundertype shouldBe hendelsesundertype + feilutbetaltePeriode.periode shouldBe periode.toDatoperiode() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/LogiskPeriodeUtilTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/LogiskPeriodeUtilTest.kt new file mode 100644 index 000000000..32497689d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/faktaomfeilutbetaling/LogiskPeriodeUtilTest.kt @@ -0,0 +1,56 @@ +package no.nav.familie.tilbake.faktaomfeilutbetaling + +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Månedsperiode +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.YearMonth + +internal class LogiskPeriodeUtilTest { + + private val januar = YearMonth.of(2021, 1) + private val februar = YearMonth.of(2021, 2) + private val mars = YearMonth.of(2021, 3) + private val april = YearMonth.of(2021, 4) + private val mai = YearMonth.of(2021, 5) + + @Test + fun `utledLogiskPeriode skal returnere én logisk periode når perioder kan slås sammen`() { + val periode1 = Månedsperiode(januar, februar) + val periode2 = Månedsperiode(mars, mai) + + val resultat = LogiskPeriodeUtil.utledLogiskPeriode( + mapOf( + periode1 to BigDecimal.valueOf(100), + periode2 to BigDecimal.valueOf(200), + ).toSortedMap(), + ) + + resultat.size shouldBe 1 + resultat[0].fom shouldBe januar + resultat[0].tom shouldBe mai + resultat[0].feilutbetaltBeløp shouldBe BigDecimal.valueOf(300) + } + + @Test + fun `utledLogiskPeriode skal returner flere logiske periode når perioder som er skilt med måned ikke kan slås sammen`() { + val periode1 = Månedsperiode(januar, februar) + val periode2 = Månedsperiode(april, mai) + + val resultat = LogiskPeriodeUtil.utledLogiskPeriode( + mapOf( + periode1 to BigDecimal.valueOf(100), + periode2 to BigDecimal.valueOf(200), + ).toSortedMap(), + ) + + resultat.size shouldBe 2 + resultat[0].fom shouldBe januar + resultat[0].tom shouldBe februar + resultat[0].feilutbetaltBeløp shouldBe BigDecimal.valueOf(100) + + resultat[1].fom shouldBe april + resultat[1].tom shouldBe mai + resultat[1].feilutbetaltBeløp shouldBe BigDecimal.valueOf(200) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/foreldelse/ForeldelseServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/foreldelse/ForeldelseServiceTest.kt new file mode 100644 index 000000000..05235876f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/foreldelse/ForeldelseServiceTest.kt @@ -0,0 +1,275 @@ +package no.nav.familie.tilbake.foreldelse + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.BehandlingsstegForeldelseDto +import no.nav.familie.tilbake.api.dto.ForeldelsesperiodeDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingRepository +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth +import java.util.UUID + +internal class ForeldelseServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var foreldelsesRepository: VurdertForeldelseRepository + + @Autowired + private lateinit var vilkårsvurderingRepository: VilkårsvurderingRepository + + @Autowired + private lateinit var foreldelseService: ForeldelseService + + private var behandling = Testdata.behandling + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandling = behandlingRepository.insert(Testdata.behandling) + + val kravgrunnlag431 = Testdata.kravgrunnlag431 + val feilkravgrunnlagsbeløp = Testdata.feilKravgrunnlagsbeløp433 + val yteseskravgrunnlagsbeløp = Testdata.ytelKravgrunnlagsbeløp433 + val førsteKravgrunnlagsperiode = Testdata.kravgrunnlagsperiode432 + .copy( + periode = Månedsperiode(YearMonth.of(2017, 1), YearMonth.of(2017, 1)), + beløp = setOf( + feilkravgrunnlagsbeløp.copy(id = UUID.randomUUID()), + yteseskravgrunnlagsbeløp.copy(id = UUID.randomUUID()), + ), + ) + val andreKravgrunnlagsperiode = Testdata.kravgrunnlagsperiode432 + .copy( + id = UUID.randomUUID(), + periode = Månedsperiode(YearMonth.of(2017, 2), YearMonth.of(2017, 2)), + beløp = setOf( + feilkravgrunnlagsbeløp.copy(id = UUID.randomUUID()), + yteseskravgrunnlagsbeløp.copy(id = UUID.randomUUID()), + ), + ) + kravgrunnlagRepository.insert( + kravgrunnlag431.copy( + perioder = setOf( + førsteKravgrunnlagsperiode, + andreKravgrunnlagsperiode, + ), + ), + ) + } + + @Test + fun `hentVurdertForeldelse skal returnere foreldelse data som skal vurderes`() { + val vurdertForeldelseDto = foreldelseService.hentVurdertForeldelse(behandling.id) + + vurdertForeldelseDto.foreldetPerioder.size shouldBe 1 + val foreldetPeriode = vurdertForeldelseDto.foreldetPerioder[0] + foreldetPeriode.periode.fom shouldBe LocalDate.of(2017, 1, 1) + foreldetPeriode.periode.tom shouldBe LocalDate.of(2017, 2, 28) + // feilutbetaltBeløp er 10000.00 i Testdata for hver periode + foreldetPeriode.feilutbetaltBeløp shouldBe BigDecimal("20000") + foreldetPeriode.foreldelsesvurderingstype.shouldBeNull() + foreldetPeriode.begrunnelse.shouldBeNull() + foreldetPeriode.foreldelsesfrist.shouldBeNull() + foreldetPeriode.oppdagelsesdato.shouldBeNull() + } + + @Test + fun `hentVurdertForeldelse skal returnere allerede vurdert foreldelse data`() { + foreldelseService + .lagreVurdertForeldelse( + behandling.id, + BehandlingsstegForeldelseDto( + listOf( + lagForeldelsesperiode( + LocalDate.of(2017, 1, 1), + LocalDate.of(2017, 1, 31), + Foreldelsesvurderingstype + .FORELDET, + ), + lagForeldelsesperiode( + LocalDate.of(2017, 2, 1), + LocalDate.of(2017, 2, 28), + Foreldelsesvurderingstype + .IKKE_FORELDET, + ), + ), + ), + ) + + val vurdertForeldelseDto = foreldelseService.hentVurdertForeldelse(behandling.id) + + vurdertForeldelseDto.foreldetPerioder.size shouldBe 2 + val førstePeriode = vurdertForeldelseDto.foreldetPerioder[0] + førstePeriode.periode.fom shouldBe LocalDate.of(2017, 1, 1) + førstePeriode.periode.tom shouldBe LocalDate.of(2017, 1, 31) + // feilutbetaltBeløp er 10000.00 i Testdata for hver periode + førstePeriode.feilutbetaltBeløp shouldBe BigDecimal("10000") + førstePeriode.foreldelsesvurderingstype shouldBe Foreldelsesvurderingstype.FORELDET + førstePeriode.begrunnelse shouldBe "foreldelses begrunnelse" + førstePeriode.foreldelsesfrist shouldBe LocalDate.of(2017, 2, 28) + førstePeriode.oppdagelsesdato.shouldBeNull() + + val andrePeriode = vurdertForeldelseDto.foreldetPerioder[1] + andrePeriode.periode.fom shouldBe LocalDate.of(2017, 2, 1) + andrePeriode.periode.tom shouldBe LocalDate.of(2017, 2, 28) + // feilutbetaltBeløp er 10000.00 i Testdata for hver periode + andrePeriode.feilutbetaltBeløp shouldBe BigDecimal("10000") + andrePeriode.foreldelsesvurderingstype shouldBe Foreldelsesvurderingstype.IKKE_FORELDET + andrePeriode.begrunnelse shouldBe "foreldelses begrunnelse" + andrePeriode.foreldelsesfrist shouldBe LocalDate.of(2017, 2, 28) + andrePeriode.oppdagelsesdato.shouldBeNull() + } + + @Test + fun `lagreVurdertForeldelse skal lagre foreldelses data for en gitt behandling`() { + foreldelseService + .lagreVurdertForeldelse( + behandling.id, + BehandlingsstegForeldelseDto( + listOf( + lagForeldelsesperiode( + LocalDate.of(2017, 1, 1), + LocalDate.of(2017, 1, 31), + Foreldelsesvurderingstype + .FORELDET, + ), + ), + ), + ) + + val vurdertForeldelse = foreldelsesRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + vurdertForeldelse.shouldNotBeNull() + vurdertForeldelse.foreldelsesperioder.size shouldBe 1 + val vurdertForeldelsesperiode = vurdertForeldelse.foreldelsesperioder.toList()[0] + vurdertForeldelsesperiode.begrunnelse shouldBe "foreldelses begrunnelse" + vurdertForeldelsesperiode.foreldelsesvurderingstype shouldBe Foreldelsesvurderingstype.FORELDET + vurdertForeldelsesperiode.foreldelsesfrist shouldBe LocalDate.of(2017, 2, 28) + vurdertForeldelsesperiode.oppdagelsesdato.shouldBeNull() + vurdertForeldelsesperiode.periode shouldBe Månedsperiode(YearMonth.of(2017, 1), YearMonth.of(2017, 1)) + } + + @Test + fun `lagreVurdertForeldelse skal ikke lagre foreldelses data når periode ikke starter med første dato`() { + val foreldelsesperiode = lagForeldelsesperiode( + LocalDate.of(2017, 1, 10), + LocalDate.of(2017, 1, 31), + Foreldelsesvurderingstype.FORELDET, + ) + val exception = shouldThrow { + foreldelseService + .lagreVurdertForeldelse( + behandling.id, + BehandlingsstegForeldelseDto(listOf(foreldelsesperiode)), + ) + } + exception.message shouldBe "Periode med ${foreldelsesperiode.periode} er ikke i hele måneder" + } + + @Test + fun `lagreVurdertForeldelse skal ikke lagre foreldelses data når periode ikke slutter med siste dato`() { + val foreldelsesperiode = lagForeldelsesperiode( + LocalDate.of(2017, 1, 1), + LocalDate.of(2017, 1, 27), + Foreldelsesvurderingstype.FORELDET, + ) + val exception = shouldThrow { + foreldelseService + .lagreVurdertForeldelse( + behandling.id, + BehandlingsstegForeldelseDto(listOf(foreldelsesperiode)), + ) + } + exception.message shouldBe "Periode med ${foreldelsesperiode.periode} er ikke i hele måneder" + } + + @Test + fun `lagreVurdertForeldelse skal nullstille forrige vurdert vilkårsvurdering når det er endring i foreldesesperiode`() { + val forrigeForeldelsesperiode = lagForeldelsesperiode( + LocalDate.of(2017, 1, 1), + LocalDate.of(2017, 4, 30), + Foreldelsesvurderingstype.IKKE_FORELDET, + ) + foreldelseService.lagreVurdertForeldelse(behandling.id, BehandlingsstegForeldelseDto(listOf(forrigeForeldelsesperiode))) + vilkårsvurderingRepository.insert(Testdata.vilkårsvurdering) + + vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldNotBeNull() + + val nyForeldelsesperiode1 = lagForeldelsesperiode( + LocalDate.of(2017, 1, 1), + LocalDate.of(2017, 2, 28), + Foreldelsesvurderingstype.IKKE_FORELDET, + ) + val nyForeldelsesperiode2 = lagForeldelsesperiode( + LocalDate.of(2017, 3, 1), + LocalDate.of(2017, 4, 30), + Foreldelsesvurderingstype.IKKE_FORELDET, + ) + foreldelseService.lagreVurdertForeldelse( + behandling.id, + BehandlingsstegForeldelseDto( + listOf( + nyForeldelsesperiode1, + nyForeldelsesperiode2, + ), + ), + ) + vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldBeNull() + } + + @Test + fun `lagreVurdertForeldelse skal ikke nullstille vurdert vilkårsvurdering når det er ingen endring i foreldesesperiode`() { + val forrigeForeldelsesperiode = lagForeldelsesperiode( + LocalDate.of(2017, 1, 1), + LocalDate.of(2017, 4, 30), + Foreldelsesvurderingstype.IKKE_FORELDET, + ) + foreldelseService.lagreVurdertForeldelse(behandling.id, BehandlingsstegForeldelseDto(listOf(forrigeForeldelsesperiode))) + vilkårsvurderingRepository.insert(Testdata.vilkårsvurdering) + + vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldNotBeNull() + + val nyForeldelsesperiode = lagForeldelsesperiode( + LocalDate.of(2017, 1, 1), + LocalDate.of(2017, 4, 30), + Foreldelsesvurderingstype.FORELDET, + ) + foreldelseService.lagreVurdertForeldelse(behandling.id, BehandlingsstegForeldelseDto(listOf(nyForeldelsesperiode))) + vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldNotBeNull() + } + + private fun lagForeldelsesperiode( + fom: LocalDate, + tom: LocalDate, + foreldelsesvurderingstype: Foreldelsesvurderingstype, + ): ForeldelsesperiodeDto { + return ForeldelsesperiodeDto( + periode = Datoperiode(fom, tom), + begrunnelse = "foreldelses begrunnelse", + foreldelsesvurderingstype = foreldelsesvurderingstype, + foreldelsesfrist = LocalDate.of(2017, 2, 28), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/foreldelse/VurdertForeldelseRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/foreldelse/VurdertForeldelseRepositoryTest.kt new file mode 100644 index 000000000..7288f1bc8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/foreldelse/VurdertForeldelseRepositoryTest.kt @@ -0,0 +1,64 @@ +package no.nav.familie.tilbake.foreldelse + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class VurdertForeldelseRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var vurdertForeldelseRepository: VurdertForeldelseRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + private val vurdertForeldelse = Testdata.vurdertForeldelse + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(Testdata.behandling) + } + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av VurdertForeldelse til basen`() { + vurdertForeldelseRepository.insert(vurdertForeldelse) + + val lagretVurdertForeldelse = vurdertForeldelseRepository.findByIdOrThrow(vurdertForeldelse.id) + + lagretVurdertForeldelse.shouldBeEqualToComparingFieldsExcept( + vurdertForeldelse, + VurdertForeldelse::sporbar, + VurdertForeldelse::versjon, + ) + lagretVurdertForeldelse.versjon shouldBe 1 + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av VurdertForeldelse i basen`() { + vurdertForeldelseRepository.insert(vurdertForeldelse) + var lagretVurdertForeldelse = vurdertForeldelseRepository.findByIdOrThrow(vurdertForeldelse.id) + val oppdatertVurdertForeldelse = lagretVurdertForeldelse.copy(aktiv = false) + + vurdertForeldelseRepository.update(oppdatertVurdertForeldelse) + + lagretVurdertForeldelse = vurdertForeldelseRepository.findByIdOrThrow(vurdertForeldelse.id) + lagretVurdertForeldelse.shouldBeEqualToComparingFieldsExcept( + oppdatertVurdertForeldelse, + VurdertForeldelse::sporbar, + VurdertForeldelse::versjon, + ) + lagretVurdertForeldelse.versjon shouldBe 2 + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/forvaltning/ForvaltningServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/forvaltning/ForvaltningServiceTest.kt new file mode 100644 index 000000000..5a2146c1b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/forvaltning/ForvaltningServiceTest.kt @@ -0,0 +1,406 @@ +package no.nav.familie.tilbake.forvaltning + +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldHaveSingleElement +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingRequestSendtRepository +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.ContextService +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.datavarehus.saksstatistikk.SendSakshendelseTilDvhTask +import no.nav.familie.tilbake.dokumentbestilling.vedtak.VedtaksbrevsoppsummeringRepository +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingRepository +import no.nav.familie.tilbake.historikkinnslag.LagHistorikkinnslagTask +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.Fagområdekode +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottatt +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattArkivRepository +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattRepository +import no.nav.familie.tilbake.oppgave.FerdigstillOppgaveTask +import no.nav.familie.tilbake.totrinn.TotrinnsvurderingRepository +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingRepository +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigInteger +import java.time.LocalDate + +internal class ForvaltningServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var økonomiXmlMottattRepository: ØkonomiXmlMottattRepository + + @Autowired + private lateinit var økonomiXmlMottattArkivRepository: ØkonomiXmlMottattArkivRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var requestSendtRepository: HentFagsystemsbehandlingRequestSendtRepository + + @Autowired + private lateinit var faktaFeilutbetalingRepository: FaktaFeilutbetalingRepository + + @Autowired + private lateinit var vilkårsvurderingRepository: VilkårsvurderingRepository + + @Autowired + private lateinit var vedtaksbrevsoppsummeringRepository: VedtaksbrevsoppsummeringRepository + + @Autowired + private lateinit var totrinnRepository: TotrinnsvurderingRepository + + @Autowired + private lateinit var forvaltningService: ForvaltningService + + private val behandling = Testdata.behandling + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(behandling) + behandlingsstegstilstandRepository + .insert( + Behandlingsstegstilstand( + behandlingId = behandling.id, + behandlingssteg = Behandlingssteg.GRUNNLAG, + behandlingsstegsstatus = Behandlingsstegstatus.VENTER, + tidsfrist = LocalDate.now().plusWeeks(3), + venteårsak = Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + ), + ) + } + + @Test + fun `korrigerKravgrunnlag skal ikke hente korrigert kravgrunnlag når behandling er avsluttet`() { + behandlingRepository.update( + behandlingRepository.findByIdOrThrow(behandling.id) + .copy(status = Behandlingsstatus.AVSLUTTET), + ) + + val exception = shouldThrow { + forvaltningService.korrigerKravgrunnlag( + behandling.id, + BigInteger.ZERO, + ) + } + exception.message shouldBe "Behandling med id=${behandling.id} er allerede ferdig behandlet." + } + + @Test + fun `korrigerKravgrunnlag skal hente korrigert kravgrunnlag når behandling allerede har et kravgrunnlag`() { + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + forvaltningService.korrigerKravgrunnlag( + behandling.id, + Testdata.kravgrunnlag431.eksternKravgrunnlagId, + ) + + val kravgrunnlagene = kravgrunnlagRepository.findByBehandlingId(behandling.id) + kravgrunnlagene.size shouldBe 2 + kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandling.id).shouldBeTrue() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + } + + @Test + fun `korrigerKravgrunnlag skal hente korrigert kravgrunnlag når behandling ikke har et kravgrunnlag`() { + lagMottattXml() + forvaltningService.korrigerKravgrunnlag(behandling.id, BigInteger.ZERO) + + val kravgrunnlagene = kravgrunnlagRepository.findByBehandlingId(behandling.id) + kravgrunnlagene.size shouldBe 1 + kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandling.id).shouldBeTrue() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + } + + @Test + fun `arkiverMottattKravgrunnlag skal arkivere mottatt xml`() { + val økonomiXmlMottatt = lagMottattXml() + forvaltningService.arkiverMottattKravgrunnlag(økonomiXmlMottatt.id) + + økonomiXmlMottattRepository.existsById(økonomiXmlMottatt.id).shouldBeFalse() + økonomiXmlMottattArkivRepository.findByEksternFagsakIdAndYtelsestype( + økonomiXmlMottatt.eksternFagsakId, + økonomiXmlMottatt.ytelsestype, + ).shouldNotBeEmpty() + } + + @Test + fun `tvingHenleggBehandling skal ikke henlegge behandling når behandling er avsluttet`() { + behandlingRepository.update( + behandlingRepository.findByIdOrThrow(behandling.id) + .copy(status = Behandlingsstatus.AVSLUTTET), + ) + + val exception = shouldThrow { + forvaltningService.tvingHenleggBehandling(behandling.id) + } + exception.message shouldBe "Behandling med id=${behandling.id} er allerede ferdig behandlet." + } + + @Test + fun `tvingHenleggBehandling skal henlegge behandling når behandling ikke er avsluttet`() { + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + forvaltningService.korrigerKravgrunnlag(behandling.id, Testdata.kravgrunnlag431.eksternKravgrunnlagId) + + var behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + + forvaltningService.tvingHenleggBehandling(behandling.id) + + behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.AVBRUTT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.AVBRUTT) + + val oppdatertBehandling = behandlingRepository.findByIdOrThrow(behandling.id) + oppdatertBehandling.erAvsluttet.shouldBeTrue() + oppdatertBehandling.ansvarligSaksbehandler shouldBe ContextService.hentSaksbehandler() + oppdatertBehandling.avsluttetDato shouldBe LocalDate.now() + oppdatertBehandling.sisteResultat!!.type shouldBe Behandlingsresultatstype.HENLAGT_TEKNISK_VEDLIKEHOLD + + val tasker = taskService.findAll() + tasker.shouldHaveSingleElement { + LagHistorikkinnslagTask.TYPE == it.type && + behandling.id.toString() == it.payload && + Aktør.SAKSBEHANDLER.name == it.metadata.getProperty("aktør") && + TilbakekrevingHistorikkinnslagstype.BEHANDLING_HENLAGT.name == it.metadata.getProperty("historikkinnslagstype") + } + tasker.any { + SendSakshendelseTilDvhTask.TASK_TYPE == it.type && + behandling.id.toString() == it.payload + }.shouldBeTrue() + tasker.shouldHaveSingleElement { + FerdigstillOppgaveTask.TYPE == it.type && + behandling.id.toString() == it.payload + } + } + + @Test + fun `flyttBehandlingsstegTilbakeTilFakta skal ikke flytte behandlingssteg når behandling er avsluttet`() { + behandlingRepository.update( + behandlingRepository.findByIdOrThrow(behandling.id) + .copy(status = Behandlingsstatus.AVSLUTTET), + ) + + val exception = shouldThrow { + forvaltningService.flyttBehandlingsstegTilbakeTilFakta(behandling.id) + } + exception.message shouldBe "Behandling med id=${behandling.id} er allerede ferdig behandlet." + } + + @Test + fun `flyttBehandlingsstegTilbakeTilFakta skal flytte behandlingssteg til FAKTA når behandling er i IVERKSETT_VEDTAK steg`() { + behandlingRepository.update( + behandlingRepository.findByIdOrThrow(behandling.id) + .copy(status = Behandlingsstatus.IVERKSETTER_VEDTAK), + ) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + behandlingsstegstilstandRepository + .update( + behandlingsstegstilstandRepository.findByBehandlingIdAndBehandlingssteg( + behandling.id, + Behandlingssteg.GRUNNLAG, + )!! + .copy(behandlingsstegsstatus = Behandlingsstegstatus.UTFØRT), + ) + + faktaFeilutbetalingRepository.insert(Testdata.faktaFeilutbetaling) + lagBehandlingssteg(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingssteg(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + + vilkårsvurderingRepository.insert(Testdata.vilkårsvurdering) + lagBehandlingssteg(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + + vedtaksbrevsoppsummeringRepository.insert(Testdata.vedtaksbrevsoppsummering) + lagBehandlingssteg(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + + totrinnRepository.insert(Testdata.totrinnsvurdering) + lagBehandlingssteg(Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.UTFØRT) + lagBehandlingssteg(Behandlingssteg.IVERKSETT_VEDTAK, Behandlingsstegstatus.KLAR) + + forvaltningService.flyttBehandlingsstegTilbakeTilFakta(behandling.id) + val behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandling.status shouldBe Behandlingsstatus.UTREDES + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FORELDELSE, Behandlingsstegstatus.TILBAKEFØRT) + assertBehandlingssteg( + behandlingsstegstilstand, + Behandlingssteg.VILKÅRSVURDERING, + Behandlingsstegstatus.TILBAKEFØRT, + ) + assertBehandlingssteg( + behandlingsstegstilstand, + Behandlingssteg.FORESLÅ_VEDTAK, + Behandlingsstegstatus.TILBAKEFØRT, + ) + assertBehandlingssteg(behandlingsstegstilstand, Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.TILBAKEFØRT) + assertBehandlingssteg( + behandlingsstegstilstand, + Behandlingssteg.IVERKSETT_VEDTAK, + Behandlingsstegstatus.TILBAKEFØRT, + ) + + faktaFeilutbetalingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldBeNull() + vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldBeNull() + vedtaksbrevsoppsummeringRepository.findByBehandlingId(behandling.id).shouldBeNull() + + taskService.findAll().shouldHaveSingleElement { + it.type == LagHistorikkinnslagTask.TYPE && + it.payload == behandling.id.toString() && + it.metadata["historikkinnslagstype"] == TilbakekrevingHistorikkinnslagstype + .BEHANDLING_FLYTTET_MED_FORVALTNING.name && + it.metadata["aktør"] == Aktør.VEDTAKSLØSNING.name + } + } + + @Test + fun `annulerKravgrunnlag skal annulere kravgrunnlag som er koblet med en behandling`() { + val kravgrunnlag = kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + shouldNotThrowAny { forvaltningService.annulerKravgrunnlag(kravgrunnlag.eksternKravgrunnlagId) } + } + + @Test + fun `annulerKravgrunnlag skal annulere kravgrunnlag som er mottatt i økonomiXmlMottatt`() { + val økonomiXmlMottatt = økonomiXmlMottattRepository.insert(Testdata.økonomiXmlMottatt) + shouldNotThrowAny { forvaltningService.annulerKravgrunnlag(økonomiXmlMottatt.eksternKravgrunnlagId!!) } + } + + @Test + fun `annulerKravgrunnlag skal ikke annulere kravgrunnlag når behandling venter på kravgrunnlag`() { + val eksternKravgrunnlagId = BigInteger.ZERO + val exception = shouldThrow { forvaltningService.annulerKravgrunnlag(eksternKravgrunnlagId) } + exception.message shouldBe "Finnes ikke eksternKravgrunnlagId=$eksternKravgrunnlagId" + } + + @Test + fun `hentForvaltningsinfo skal hente forvaltningsinfo basert på eksternFagsakId og ytelsestype`() { + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val kravgrunnlag = Testdata.kravgrunnlag431 + kravgrunnlagRepository.insert( + kravgrunnlag.copy( + fagsystemId = fagsak.eksternFagsakId, + fagområdekode = Fagområdekode.values() + .first { it.ytelsestype == fagsak.ytelsestype }, + ), + ) + val forvaltningsinfo = + forvaltningService.hentForvaltningsinfo(fagsak.ytelsestype, fagsak.eksternFagsakId).first() + forvaltningsinfo.eksternKravgrunnlagId shouldBe kravgrunnlag.eksternKravgrunnlagId + forvaltningsinfo.mottattXmlId.shouldBeNull() + forvaltningsinfo.eksternId shouldBe kravgrunnlag.referanse + } + + @Test + fun `hentForvaltningsinfo skal hente forvaltningsinfo basert på eksternFagsakId og ytelsestype fra mottattXml`() { + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val mottattXml = Testdata.økonomiXmlMottatt + økonomiXmlMottattRepository.insert( + mottattXml.copy( + eksternFagsakId = fagsak.eksternFagsakId, + ytelsestype = fagsak.ytelsestype, + ), + ) + val forvaltningsinfo = + forvaltningService.hentForvaltningsinfo(fagsak.ytelsestype, fagsak.eksternFagsakId).first() + forvaltningsinfo.eksternKravgrunnlagId shouldBe mottattXml.eksternKravgrunnlagId + forvaltningsinfo.mottattXmlId shouldBe mottattXml.id + forvaltningsinfo.eksternId shouldBe mottattXml.referanse + } + + @Test + fun `hentForvaltningsinfo skal ikke hente forvaltningsinfo når behandling venter på kravgrunnlag`() { + val fagsak = fagsakRepository.findByIdOrThrow(behandling.fagsakId) + val exception = shouldThrow { + forvaltningService.hentForvaltningsinfo( + fagsak.ytelsestype, + fagsak.eksternFagsakId, + ) + } + exception.message shouldBe "Finnes ikke data i systemet for ytelsestype=${fagsak.ytelsestype} " + + "og eksternFagsakId=${fagsak.eksternFagsakId}" + } + + private fun lagMottattXml(): ØkonomiXmlMottatt { + val mottattXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + return økonomiXmlMottattRepository.insert( + ØkonomiXmlMottatt( + melding = mottattXml, + kravstatuskode = Kravstatuskode.NYTT, + eksternFagsakId = "0", + ytelsestype = Ytelsestype.BARNETRYGD, + referanse = "0", + eksternKravgrunnlagId = BigInteger.ZERO, + vedtakId = BigInteger.ZERO, + kontrollfelt = "2021-03-02-18.50.15.236315", + sperret = false, + ), + ) + } + + private fun assertBehandlingssteg( + behandlingsstegstilstand: List, + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ) { + behandlingsstegstilstand.any { + behandlingssteg == it.behandlingssteg && + behandlingsstegstatus == it.behandlingsstegsstatus + }.shouldBeTrue() + } + + private fun lagBehandlingssteg( + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ) { + behandlingsstegstilstandRepository.insert( + Behandlingsstegstilstand( + behandlingId = behandling.id, + behandlingssteg = behandlingssteg, + behandlingsstegsstatus = behandlingsstegstatus, + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/historikkinnslag/HistorikkServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/historikkinnslag/HistorikkServiceTest.kt new file mode 100644 index 000000000..99ec7f3b3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/historikkinnslag/HistorikkServiceTest.kt @@ -0,0 +1,536 @@ +package no.nav.familie.tilbake.historikkinnslag + +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.kontrakter.felles.Applikasjon +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.historikkinnslag.Historikkinnslagstype +import no.nav.familie.kontrakter.felles.historikkinnslag.OpprettHistorikkinnslagRequest +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultat +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.behandling.domain.Behandlingsvedtak +import no.nav.familie.tilbake.behandling.domain.Iverksettingsstatus +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingRepository +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevsporing +import no.nav.familie.tilbake.dokumentbestilling.felles.domain.Brevtype +import no.nav.familie.tilbake.integration.kafka.DefaultKafkaProducer +import no.nav.familie.tilbake.integration.kafka.KafkaProducer +import org.apache.kafka.clients.producer.ProducerRecord +import org.apache.kafka.clients.producer.RecordMetadata +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.support.SendResult +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.UUID + +internal class HistorikkServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var brevsporingRepository: BrevsporingRepository + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + private val mockKafkaTemplate: KafkaTemplate = mockk() + private lateinit var spyKafkaProducer: KafkaProducer + private lateinit var historikkService: HistorikkService + + private val fagsak = Testdata.fagsak + private val behandling = Testdata.behandling + private val behandlingId = behandling.id + private val opprettetTidspunkt = LocalDateTime.now() + + private val behandlingIdSlot = slot() + private val keySlot = slot() + private val historikkinnslagRecordSlot = slot() + + @BeforeEach + fun init() { + fagsakRepository.insert(fagsak) + behandlingRepository.insert(behandling) + + spyKafkaProducer = spyk(DefaultKafkaProducer(mockKafkaTemplate)) + historikkService = HistorikkService( + behandlingRepository, + fagsakRepository, + brevsporingRepository, + spyKafkaProducer, + ) + val recordMetadata = mockk() + every { recordMetadata.offset() } returns 1 + val result = SendResult(mockk(), recordMetadata) + every { mockKafkaTemplate.send(any>()).get() }.returns(result) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når behandling oppretter automatisk`() { + historikkService.lagHistorikkinnslag( + behandlingId, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_OPPRETTET, + Aktør.VEDTAKSLØSNING, + opprettetTidspunkt, + ) + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + assertHistorikkinnslagRequest( + Aktør.VEDTAKSLØSNING, + Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_OPPRETTET.tittel, + Historikkinnslagstype.HENDELSE, + ) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når behandling setter på vent automatisk`() { + behandlingskontrollService.fortsettBehandling(behandlingId) + behandlingskontrollService.settBehandlingPåVent( + behandlingId, + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + LocalDate.now().plusDays(20), + ) + historikkService.lagHistorikkinnslag( + behandlingId, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT, + Aktør.VEDTAKSLØSNING, + opprettetTidspunkt, + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING.beskrivelse, + ) + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + assertHistorikkinnslagRequest( + aktør = Aktør.VEDTAKSLØSNING, + aktørIdent = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + tittel = TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT.tittel, + tekst = "Årsak: Venter på tilbakemelding fra bruker", + type = Historikkinnslagstype.HENDELSE, + ) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når behandling setter på vent manuelt`() { + behandlingskontrollService.fortsettBehandling(behandlingId) + behandlingskontrollService.settBehandlingPåVent( + behandlingId, + Venteårsak.AVVENTER_DOKUMENTASJON, + LocalDate.now().plusDays(20), + ) + historikkService.lagHistorikkinnslag( + behandlingId, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT, + Aktør.SAKSBEHANDLER, + opprettetTidspunkt, + Venteårsak.AVVENTER_DOKUMENTASJON.beskrivelse, + ) + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + assertHistorikkinnslagRequest( + aktør = Aktør.SAKSBEHANDLER, + aktørIdent = behandling.ansvarligSaksbehandler, + tittel = TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT.tittel, + tekst = "Årsak: Avventer dokumentasjon", + type = Historikkinnslagstype.HENDELSE, + ) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når behandling tar av vent manuelt`() { + historikkService.lagHistorikkinnslag( + behandlingId, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_GJENOPPTATT, + Aktør.SAKSBEHANDLER, + opprettetTidspunkt, + ) + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + assertHistorikkinnslagRequest( + aktør = Aktør.SAKSBEHANDLER, + aktørIdent = behandling.ansvarligSaksbehandler, + tittel = TilbakekrevingHistorikkinnslagstype.BEHANDLING_GJENOPPTATT.tittel, + type = Historikkinnslagstype.HENDELSE, + ) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når behandling mottar et kravgrunnlag`() { + historikkService.lagHistorikkinnslag( + behandlingId, + TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT, + Aktør.VEDTAKSLØSNING, + opprettetTidspunkt, + ) + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + assertHistorikkinnslagRequest( + aktør = Aktør.VEDTAKSLØSNING, + aktørIdent = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + tittel = TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT.tittel, + type = Historikkinnslagstype.HENDELSE, + ) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når behandling sender varselbrev`() { + brevsporingRepository.insert( + Brevsporing( + behandlingId = behandlingId, + brevtype = Brevtype.VARSEL, + journalpostId = "testverdi", + dokumentId = "testverdi", + ), + ) + historikkService.lagHistorikkinnslag( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.VARSELBREV_SENDT, + aktør = Aktør.VEDTAKSLØSNING, + opprettetTidspunkt = opprettetTidspunkt, + brevtype = Brevtype.VARSEL.name, + ) + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + assertHistorikkinnslagRequest( + aktør = Aktør.VEDTAKSLØSNING, + aktørIdent = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + tittel = TilbakekrevingHistorikkinnslagstype.VARSELBREV_SENDT.tittel, + tekst = TilbakekrevingHistorikkinnslagstype.VARSELBREV_SENDT.tekst, + type = Historikkinnslagstype.BREV, + dokumentId = "testverdi", + journalpostId = "testverdi", + ) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når behandling er automatisk henlagt`() { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update( + behandling.copy( + resultater = + setOf(Behandlingsresultat(type = Behandlingsresultatstype.HENLAGT_KRAVGRUNNLAG_NULLSTILT)), + ), + ) + historikkService.lagHistorikkinnslag( + behandlingId, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_HENLAGT, + Aktør.VEDTAKSLØSNING, + opprettetTidspunkt, + ) + + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + + assertHistorikkinnslagRequest( + aktør = Aktør.VEDTAKSLØSNING, + aktørIdent = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + tittel = TilbakekrevingHistorikkinnslagstype.BEHANDLING_HENLAGT.tittel, + tekst = "Årsak: Kravgrunnlaget er nullstilt", + type = Historikkinnslagstype.HENDELSE, + ) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når behandling er manuelt henlagt`() { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update( + behandling.copy( + resultater = + setOf(Behandlingsresultat(type = Behandlingsresultatstype.HENLAGT_FEILOPPRETTET)), + ), + ) + historikkService.lagHistorikkinnslag( + behandlingId, + TilbakekrevingHistorikkinnslagstype.BEHANDLING_HENLAGT, + Aktør.VEDTAKSLØSNING, + opprettetTidspunkt, + "testverdi", + ) + + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + + assertHistorikkinnslagRequest( + aktør = Aktør.VEDTAKSLØSNING, + aktørIdent = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + tittel = TilbakekrevingHistorikkinnslagstype.BEHANDLING_HENLAGT.tittel, + tekst = "Årsak: Henlagt, søknaden er feilopprettet, Begrunnelse: testverdi", + type = Historikkinnslagstype.HENDELSE, + ) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når behandling sender henleggelsesbrev`() { + brevsporingRepository.insert( + Brevsporing( + behandlingId = behandlingId, + brevtype = Brevtype.HENLEGGELSE, + journalpostId = "testverdi", + dokumentId = "testverdi", + ), + ) + historikkService.lagHistorikkinnslag( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.HENLEGGELSESBREV_SENDT, + aktør = Aktør.VEDTAKSLØSNING, + opprettetTidspunkt = opprettetTidspunkt, + brevtype = Brevtype.HENLEGGELSE.name, + ) + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + assertHistorikkinnslagRequest( + aktør = Aktør.VEDTAKSLØSNING, + aktørIdent = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + tittel = TilbakekrevingHistorikkinnslagstype.HENLEGGELSESBREV_SENDT.tittel, + tekst = TilbakekrevingHistorikkinnslagstype.HENLEGGELSESBREV_SENDT.tekst, + type = Historikkinnslagstype.BREV, + dokumentId = "testverdi", + journalpostId = "testverdi", + ) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når behandling ikke sender henleggelsesbrev for ukjent adresse`() { + brevsporingRepository.insert( + Brevsporing( + behandlingId = behandlingId, + brevtype = Brevtype.HENLEGGELSE, + journalpostId = "testverdi", + dokumentId = "testverdi", + ), + ) + historikkService.lagHistorikkinnslag( + behandlingId = behandlingId, + historikkinnslagstype = TilbakekrevingHistorikkinnslagstype.BREV_IKKE_SENDT_UKJENT_ADRESSE, + aktør = Aktør.VEDTAKSLØSNING, + opprettetTidspunkt = opprettetTidspunkt, + brevtype = Brevtype.HENLEGGELSE.name, + beskrivelse = TilbakekrevingHistorikkinnslagstype.HENLEGGELSESBREV_SENDT.tekst, + ) + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + assertHistorikkinnslagRequest( + aktør = Aktør.VEDTAKSLØSNING, + aktørIdent = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + tittel = TilbakekrevingHistorikkinnslagstype.BREV_IKKE_SENDT_UKJENT_ADRESSE.tittel, + tekst = TilbakekrevingHistorikkinnslagstype.HENLEGGELSESBREV_SENDT.tekst + " er ikke sendt", + type = Historikkinnslagstype.BREV, + dokumentId = "testverdi", + journalpostId = "testverdi", + ) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når fakta steg er utført for behandling`() { + historikkService.lagHistorikkinnslag( + behandlingId, + TilbakekrevingHistorikkinnslagstype.FAKTA_VURDERT, + Aktør.SAKSBEHANDLER, + opprettetTidspunkt, + ) + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + assertHistorikkinnslagRequest( + aktør = Aktør.SAKSBEHANDLER, + aktørIdent = behandling.ansvarligSaksbehandler, + tittel = TilbakekrevingHistorikkinnslagstype.FAKTA_VURDERT.tittel, + type = Historikkinnslagstype.SKJERMLENKE, + steg = Behandlingssteg.FAKTA.name, + ) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når foreldelse steg er utført for behandling`() { + historikkService.lagHistorikkinnslag( + behandlingId, + TilbakekrevingHistorikkinnslagstype.FORELDELSE_VURDERT, + Aktør.SAKSBEHANDLER, + opprettetTidspunkt, + ) + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + assertHistorikkinnslagRequest( + aktør = Aktør.SAKSBEHANDLER, + aktørIdent = behandling.ansvarligSaksbehandler, + tittel = TilbakekrevingHistorikkinnslagstype.FORELDELSE_VURDERT.tittel, + type = Historikkinnslagstype.SKJERMLENKE, + steg = Behandlingssteg.FORELDELSE.name, + ) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når behandling er fattet`() { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update( + behandling.copy( + resultater = + setOf( + Behandlingsresultat( + type = Behandlingsresultatstype.FULL_TILBAKEBETALING, + behandlingsvedtak = Behandlingsvedtak( + vedtaksdato = LocalDate.now(), + iverksettingsstatus = + Iverksettingsstatus.IVERKSATT, + ), + ), + ), + ), + ) + historikkService.lagHistorikkinnslag( + behandlingId, + TilbakekrevingHistorikkinnslagstype.VEDTAK_FATTET, + Aktør.BESLUTTER, + opprettetTidspunkt, + ) + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + assertHistorikkinnslagRequest( + aktør = Aktør.BESLUTTER, + aktørIdent = requireNotNull(behandling.ansvarligBeslutter), + tittel = TilbakekrevingHistorikkinnslagstype.VEDTAK_FATTET.tittel, + tekst = "Resultat: Full tilbakebetaling", + type = Historikkinnslagstype.HENDELSE, + ) + } + + @Test + fun `lagHistorikkinnslag skal lage historikkinnslag når man bytter enhet på behandling`() { + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update(behandling.copy(behandlendeEnhet = "3434")) + + historikkService.lagHistorikkinnslag( + behandlingId, + TilbakekrevingHistorikkinnslagstype.ENDRET_ENHET, + Aktør.SAKSBEHANDLER, + opprettetTidspunkt, + "begrunnelse for endring", + ) + + verify { + spyKafkaProducer.sendHistorikkinnslag( + capture(behandlingIdSlot), + capture(keySlot), + capture(historikkinnslagRecordSlot), + ) + } + assertHistorikkinnslagRequest( + aktør = Aktør.SAKSBEHANDLER, + aktørIdent = requireNotNull(behandling.ansvarligSaksbehandler), + tittel = TilbakekrevingHistorikkinnslagstype.ENDRET_ENHET.tittel, + tekst = "Ny enhet: 3434, Begrunnelse: begrunnelse for endring", + type = Historikkinnslagstype.HENDELSE, + ) + } + + private fun assertHistorikkinnslagRequest( + aktør: Aktør, + aktørIdent: String, + tittel: String, + type: Historikkinnslagstype, + tekst: String? = null, + steg: String? = null, + dokumentId: String? = null, + journalpostId: String? = null, + ) { + behandlingIdSlot.captured shouldBe behandlingId + val request = historikkinnslagRecordSlot.captured + keySlot.captured shouldBe request.behandlingId + + request.eksternFagsakId shouldBe fagsak.eksternFagsakId + request.aktør shouldBe aktør + request.aktørIdent shouldBe aktørIdent + request.opprettetTidspunkt shouldBe opprettetTidspunkt + request.fagsystem shouldBe Fagsystem.BA + request.applikasjon shouldBe Applikasjon.FAMILIE_TILBAKE + request.tittel shouldBe tittel + request.type shouldBe type + request.tekst shouldBe tekst + request.steg shouldBe steg + request.dokumentId shouldBe dokumentId + request.journalpostId shouldBe journalpostId + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/integration/familie/IntegrasjonerClientTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/integration/familie/IntegrasjonerClientTest.kt new file mode 100644 index 000000000..d1a3dbeee --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/integration/familie/IntegrasjonerClientTest.kt @@ -0,0 +1,157 @@ +package no.nav.familie.tilbake.integration.familie + +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.okJson +import com.github.tomakehurst.wiremock.client.WireMock.post +import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo +import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.nulls.shouldNotBeNull +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Ressurs.Companion.failure +import no.nav.familie.kontrakter.felles.Ressurs.Companion.success +import no.nav.familie.kontrakter.felles.dokarkiv.ArkiverDokumentResponse +import no.nav.familie.kontrakter.felles.dokarkiv.v2.ArkiverDokumentRequest +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstidspunkt +import no.nav.familie.kontrakter.felles.dokdist.Distribusjonstype +import no.nav.familie.kontrakter.felles.organisasjon.Organisasjon +import no.nav.familie.tilbake.config.IntegrasjonerConfig +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.web.client.RestOperations +import java.net.URI + +internal class IntegrasjonerClientTest { + + private val wireMockServer = WireMockServer(wireMockConfig().dynamicPort()) + private val restOperations: RestOperations = RestTemplateBuilder().build() + + private lateinit var integrasjonerClient: IntegrasjonerClient + private val arkiverDokumentRequest = ArkiverDokumentRequest("123456789", true, listOf()) + + @BeforeEach + fun setUp() { + wireMockServer.start() + integrasjonerClient = IntegrasjonerClient( + restOperations, + IntegrasjonerConfig(URI.create(wireMockServer.baseUrl()), "tilbake"), + ) + } + + @AfterEach + fun tearDown() { + wireMockServer.resetAll() + wireMockServer.stop() + } + + @Test + fun `arkiver skal gi vellykket respons hvis integrasjoner gir gyldig svar`() { + val arkiverDokumentResponse = ArkiverDokumentResponse("wer", true) + + wireMockServer.stubFor( + post(urlEqualTo("/${IntegrasjonerConfig.PATH_ARKIVER}")) + .willReturn(okJson(success(arkiverDokumentResponse).toJson())), + ) + + integrasjonerClient.arkiver(arkiverDokumentRequest).shouldNotBeNull() + } + + @Test + fun `arkiver skal kaste feil hvis hvis integrasjoner gir ugyldig svar`() { + wireMockServer.stubFor( + post(urlEqualTo("/${IntegrasjonerConfig.PATH_ARKIVER}")) + .willReturn(okJson(failure("error").toJson())), + ) + + shouldThrow { + integrasjonerClient.arkiver(arkiverDokumentRequest) + } + } + + @Test + fun `distribuerJournalpost skal gi vellykket respons hvis integrasjoner gir gyldig svar`() { + // Gitt + wireMockServer.stubFor( + post(urlEqualTo("/${IntegrasjonerConfig.PATH_DISTRIBUER}")) + .willReturn(okJson(success("id").toJson())), + ) + // Vil gi resultat + integrasjonerClient.distribuerJournalpost( + "3216354", + Fagsystem.EF, + Distribusjonstype.VIKTIG, + Distribusjonstidspunkt.KJERNETID, + ).shouldNotBeNull() + } + + @Test + fun `distribuerJournalpost skal kaste feil hvis hvis integrasjoner gir ugyldig svar`() { + wireMockServer.stubFor( + post(urlEqualTo("/${IntegrasjonerConfig.PATH_DISTRIBUER}")) + .willReturn(okJson(failure("error").toJson())), + ) + + shouldThrow { + integrasjonerClient.distribuerJournalpost( + "3216354", + Fagsystem.EF, + Distribusjonstype.VIKTIG, + Distribusjonstidspunkt.KJERNETID, + ) + } + } + + @Test + fun `hentOrganisasjon skal gi vellykket respons hvis integrasjoner gir gyldig svar`() { + // Gitt + wireMockServer.stubFor( + get(urlEqualTo("/${IntegrasjonerConfig.PATH_ORGANISASJON}/987654321")) + .willReturn( + okJson( + success(Organisasjon("Bob AS", "987654321")) + .toJson(), + ), + ), + ) + // Vil gi resultat + integrasjonerClient.hentOrganisasjon("987654321").shouldNotBeNull() + } + + @Test + fun `hentOrganisasjon skal kaste feil hvis integrasjoner gir ugyldig svar`() { + wireMockServer.stubFor( + get(urlEqualTo("/${IntegrasjonerConfig.PATH_ORGANISASJON}/987654321")) + .willReturn(okJson(failure("error").toJson())), + ) + + shouldThrow { + integrasjonerClient.hentOrganisasjon("987654321") + } + } + + @Test + fun `validerOrganisasjon skal gi vellykket respons hvis organisasjonnr er gyldig`() { + // Gitt + wireMockServer.stubFor( + get(urlEqualTo("/${IntegrasjonerConfig.PATH_ORGANISASJON}/987654321/valider")) + .willReturn(okJson(success(true).toJson())), + ) + // Vil gi resultat + integrasjonerClient.validerOrganisasjon("987654321").shouldBeTrue() + } + + @Test + fun `validerOrganisasjon skal kaste feil hvis organisasjonnr er ugyldig`() { + wireMockServer.stubFor( + get(urlEqualTo("/${IntegrasjonerConfig.PATH_ORGANISASJON}/987654321/valider")) + .willReturn(okJson(success(false).toJson())), + ) + + integrasjonerClient.validerOrganisasjon("987654321").shouldBeFalse() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/integration/pdl/PdlClientTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/integration/pdl/PdlClientTest.kt new file mode 100644 index 000000000..5a6285416 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/integration/pdl/PdlClientTest.kt @@ -0,0 +1,101 @@ +package no.nav.familie.tilbake.integration.pdl + +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock.okJson +import com.github.tomakehurst.wiremock.client.WireMock.post +import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo +import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.tilbake.config.PdlConfig +import no.nav.familie.tilbake.integration.pdl.internal.Kjønn +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.web.client.RestOperations +import java.net.URI +import java.time.LocalDate + +class PdlClientTest { + + companion object { + + private val restOperations: RestOperations = RestTemplateBuilder().build() + lateinit var pdlClient: PdlClient + lateinit var wiremockServerItem: WireMockServer + + @BeforeAll + @JvmStatic + fun initClass() { + wiremockServerItem = WireMockServer(wireMockConfig().dynamicPort()) + wiremockServerItem.start() + + pdlClient = PdlClient(PdlConfig(URI.create(wiremockServerItem.baseUrl())), restOperations) + } + + @AfterAll + @JvmStatic + fun tearDown() { + wiremockServerItem.stop() + } + } + + @AfterEach + fun tearDownEachTest() { + wiremockServerItem.resetAll() + } + + @Test + fun `hentPersoninfo skal hente person info for barnetrygd med ok respons fra PDL`() { + wiremockServerItem.stubFor( + post(urlEqualTo("/${PdlConfig.PATH_GRAPHQL}")) + .willReturn(okJson(readFile("pdlOkResponseEnkel.json"))), + ) + + val respons = pdlClient.hentPersoninfo("11111122222", Fagsystem.BA) + + respons.shouldNotBeNull() + respons.navn shouldBe "ENGASJERT FYR" + respons.kjønn shouldBe Kjønn.MANN + respons.fødselsdato shouldBe LocalDate.of(1955, 9, 13) + respons.dødsdato shouldBe null + } + + @Test + fun `hentPersoninfo skal hente info for en død person`() { + wiremockServerItem.stubFor( + post(urlEqualTo("/${PdlConfig.PATH_GRAPHQL}")) + .willReturn(okJson(readFile("pdlOkResponseDødPerson.json"))), + ) + + val respons = pdlClient.hentPersoninfo("11111122222", Fagsystem.BA) + + respons.shouldNotBeNull() + respons.navn shouldBe "ENGASJERT FYR" + respons.kjønn shouldBe Kjønn.MANN + respons.fødselsdato shouldBe LocalDate.of(1955, 9, 13) + respons.dødsdato shouldBe LocalDate.of(2022, 4, 1) + } + + @Test + fun `hentPersoninfo skal ikke hente person info når person ikke finnes`() { + wiremockServerItem.stubFor( + post(urlEqualTo("/${PdlConfig.PATH_GRAPHQL}")) + .willReturn(okJson(readFile("pdlPersonIkkeFunnetResponse.json"))), + ) + + val exception = shouldThrow( + block = + { pdlClient.hentPersoninfo("11111122222", Fagsystem.BA) }, + ) + exception.message shouldBe "Feil ved oppslag på person: Person ikke funnet" + } + + private fun readFile(filnavn: String): String { + return this::class.java.getResource("/pdl/json/$filnavn").readText() + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/integration/\303\270konomi/OppdragClientTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/integration/\303\270konomi/OppdragClientTest.kt" new file mode 100644 index 000000000..754b98039 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/integration/\303\270konomi/OppdragClientTest.kt" @@ -0,0 +1,327 @@ +package no.nav.familie.tilbake.integration.økonomi + +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock.okJson +import com.github.tomakehurst.wiremock.client.WireMock.post +import com.github.tomakehurst.wiremock.client.WireMock.serviceUnavailable +import com.github.tomakehurst.wiremock.client.WireMock.status +import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo +import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.types.shouldBeInstanceOf +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.simulering.FeilutbetalingerFraSimulering +import no.nav.familie.kontrakter.felles.simulering.FeilutbetaltPeriode +import no.nav.familie.kontrakter.felles.simulering.HentFeilutbetalingerFraSimuleringRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.exceptionhandler.IntegrasjonException +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.iverksettvedtak.TilbakekrevingsvedtakMarshaller +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagUtil +import no.nav.familie.tilbake.kravgrunnlag.domain.KodeAksjon +import no.nav.okonomi.tilbakekrevingservice.KravgrunnlagHentDetaljRequest +import no.nav.okonomi.tilbakekrevingservice.KravgrunnlagHentDetaljResponse +import no.nav.okonomi.tilbakekrevingservice.TilbakekrevingsvedtakRequest +import no.nav.okonomi.tilbakekrevingservice.TilbakekrevingsvedtakResponse +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.DetaljertKravgrunnlagDto +import no.nav.tilbakekreving.kravgrunnlag.detalj.v1.HentKravgrunnlagDetaljDto +import no.nav.tilbakekreving.typer.v1.MmelDto +import org.eclipse.jetty.http.HttpStatus +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.web.client.RestOperations +import java.math.BigDecimal +import java.math.BigInteger +import java.net.URI +import java.time.YearMonth +import java.util.UUID + +internal class OppdragClientTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + private lateinit var oppdragClient: OppdragClient + + private val restOperations: RestOperations = RestTemplateBuilder().build() + private val wireMockServer = WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()) + + private val fagsak = Testdata.fagsak + private val behandling = Testdata.behandling + private lateinit var tilbakekrevingsvedtakRequest: TilbakekrevingsvedtakRequest + private lateinit var hentKravgrunnlagRequest: KravgrunnlagHentDetaljRequest + private val kravgrunnlagId: BigInteger = BigInteger.ZERO + + @BeforeEach + fun init() { + wireMockServer.start() + + fagsakRepository.insert(fagsak) + behandlingRepository.insert(behandling) + oppdragClient = DefaultOppdragClient(restOperations, URI.create(wireMockServer.baseUrl())) + + val tilbakekrevingsvedtakRequestXml = readXml("/tilbakekrevingsvedtak/tilbakekrevingsvedtak.xml") + tilbakekrevingsvedtakRequest = TilbakekrevingsvedtakMarshaller.unmarshall( + tilbakekrevingsvedtakRequestXml, + behandling.id, + UUID.randomUUID(), + ) + hentKravgrunnlagRequest = KravgrunnlagHentDetaljRequest().apply { + hentkravgrunnlag = HentKravgrunnlagDetaljDto().apply { + kravgrunnlagId = kravgrunnlagId + kodeAksjon = KodeAksjon.HENT_KORRIGERT_KRAVGRUNNLAG.kode + saksbehId = "testverdi" + enhetAnsvarlig = "testverdi" + } + } + } + + @AfterEach + fun tearDown() { + wireMockServer.resetAll() + wireMockServer.stop() + } + + @Test + fun `iverksettVedtak skal sende iverksettelse request til oppdrag`() { + wireMockServer.stubFor( + post(urlEqualTo("/${DefaultOppdragClient.IVERKSETTELSE_PATH}/${behandling.id}")) + .willReturn(okJson(Ressurs.success(lagIverksettelseRespons()).toJson())), + ) + val iverksettVedtak = oppdragClient.iverksettVedtak(behandling.id, tilbakekrevingsvedtakRequest) + + iverksettVedtak shouldNotBe null + } + + @Test + fun `iverksettVedtak skal ikke sende iverksettelse request til oppdrag når oppdrag har nedetid`() { + wireMockServer.stubFor( + post(urlEqualTo("/${DefaultOppdragClient.IVERKSETTELSE_PATH}/${behandling.id}")) + .willReturn(status(HttpStatus.REQUEST_TIMEOUT_408)), + ) + + val exception = shouldThrow { + oppdragClient.iverksettVedtak( + behandling.id, + tilbakekrevingsvedtakRequest, + ) + } + exception.shouldNotBeNull() + exception.shouldBeInstanceOf() + exception.message shouldBe "Noe gikk galt ved iverksetting av behandling=${behandling.id}" + } + + @Test + fun `iverksettVedtak skal ikke iverksette behandling til oppdrag når økonomi ikke svarer`() { + wireMockServer.stubFor( + post(urlEqualTo("/${DefaultOppdragClient.IVERKSETTELSE_PATH}/${behandling.id}")) + .willReturn(serviceUnavailable().withStatusMessage("Couldn't send message")), + ) + + val exception = shouldThrow { + oppdragClient.iverksettVedtak( + behandling.id, + tilbakekrevingsvedtakRequest, + ) + } + exception.shouldNotBeNull() + exception.shouldBeInstanceOf() + exception.message shouldBe "Noe gikk galt ved iverksetting av behandling=${behandling.id}" + exception.cause?.message shouldBe "503 Couldn't send message: [no body]" + } + + @Test + fun `hentKravgrunnlag skal hente kravgrunnlag fra oppdrag`() { + wireMockServer.stubFor( + post( + urlEqualTo( + "/${DefaultOppdragClient.HENT_KRAVGRUNNLAG_PATH}/$kravgrunnlagId", + ), + ) + .willReturn( + okJson( + Ressurs.success( + lagHentKravgrunnlagRespons( + "00", + "OK", + ), + ) + .toJson(), + ), + ), + ) + val hentKravgrunnlag = oppdragClient.hentKravgrunnlag(kravgrunnlagId, hentKravgrunnlagRequest) + + hentKravgrunnlag shouldNotBe null + } + + @Test + fun `hentKravgrunnlag skal ikke hente kravgrunnlag fra oppdrag når kravgrunnlag ikke finnes i økonomi`() { + wireMockServer.stubFor( + post( + urlEqualTo( + "/${DefaultOppdragClient.HENT_KRAVGRUNNLAG_PATH}/$kravgrunnlagId", + ), + ) + .willReturn( + okJson( + Ressurs.success( + lagHentKravgrunnlagRespons( + "00", + "B420010I", + ), + ) + .toJson(), + ), + ), + ) + val exception = shouldThrow { + oppdragClient.hentKravgrunnlag(kravgrunnlagId, hentKravgrunnlagRequest) + } + exception.shouldNotBeNull() + exception.shouldBeInstanceOf() + exception.message shouldBe "Noe gikk galt ved henting av kravgrunnlag for kravgrunnlagId=$kravgrunnlagId" + exception.cause?.message shouldBe "Fikk feil respons:{\"systemId\":null,\"kodeMelding\":\"B420010I\"," + + "\"alvorlighetsgrad\":\"00\",\"beskrMelding\":null,\"sqlKode\":null,\"sqlState\":null,\"sqlMelding\":null," + + "\"mqCompletionKode\":null,\"mqReasonKode\":null,\"programId\":null,\"sectionNavn\":null} fra økonomi " + + "ved henting av kravgrunnlag for kravgrunnlagId=$kravgrunnlagId." + } + + @Test + fun `hentKravgrunnlag skal ikke hente kravgrunnlag fra oppdrag når kravgrunnlag er sperret i økonomi`() { + wireMockServer.stubFor( + post( + urlEqualTo( + "/${DefaultOppdragClient.HENT_KRAVGRUNNLAG_PATH}/$kravgrunnlagId", + ), + ) + .willReturn( + okJson( + Ressurs.success( + lagHentKravgrunnlagRespons( + "00", + "B420012I", + ), + ) + .toJson(), + ), + ), + ) + val exception = shouldThrow { + oppdragClient.hentKravgrunnlag(kravgrunnlagId, hentKravgrunnlagRequest) + } + exception.shouldNotBeNull() + exception.message shouldBe "Noe gikk galt ved henting av kravgrunnlag for kravgrunnlagId=$kravgrunnlagId" + exception.cause?.message shouldBe "Hentet kravgrunnlag for kravgrunnlagId=$kravgrunnlagId er sperret" + } + + @Test + fun `hentKravgrunnlag skal ikke hente kravgrunnlag fra oppdrag når økonomi ikke svarer`() { + wireMockServer.stubFor( + post( + urlEqualTo( + "/${DefaultOppdragClient.HENT_KRAVGRUNNLAG_PATH}/$kravgrunnlagId", + ), + ) + .willReturn(serviceUnavailable().withStatusMessage("Couldn't send message")), + ) + val exception = shouldThrow { + oppdragClient.hentKravgrunnlag(kravgrunnlagId, hentKravgrunnlagRequest) + } + exception.shouldNotBeNull() + exception.shouldBeInstanceOf() + exception.message shouldBe "Noe gikk galt ved henting av kravgrunnlag for kravgrunnlagId=$kravgrunnlagId" + exception.cause?.message shouldBe "503 Couldn't send message: [no body]" + } + + @Test + fun `hentFeilutbetalingerFraSimulering skal hente feilutbetalinger fra simulering`() { + val feilutbetaltPeriode = FeilutbetaltPeriode( + fom = YearMonth.now().minusMonths(2).atDay(1), + tom = YearMonth.now().minusMonths(1).atDay(1), + feilutbetaltBeløp = BigDecimal("20000"), + tidligereUtbetaltBeløp = BigDecimal("30000"), + nyttBeløp = BigDecimal("10000"), + ) + val feilutbetaltPerioder = FeilutbetalingerFraSimulering(listOf(feilutbetaltPeriode)) + wireMockServer.stubFor( + post(urlEqualTo("/${DefaultOppdragClient.HENT_FEILUTBETALINGER_PATH}")) + .willReturn(okJson(Ressurs.success(feilutbetaltPerioder).toJson())), + ) + + val respons = oppdragClient + .hentFeilutbetalingerFraSimulering( + HentFeilutbetalingerFraSimuleringRequest( + Ytelsestype.OVERGANGSSTØNAD, + "123", + "1", + ), + ) + respons shouldNotBe null + } + + @Test + fun `hentFeilutbetalingerFraSimulering skal ikke hente feilutbetalinger fra simulering`() { + wireMockServer.stubFor( + post(urlEqualTo("/${DefaultOppdragClient.HENT_FEILUTBETALINGER_PATH}")) + .willReturn(serviceUnavailable().withStatusMessage("Couldn't send message")), + ) + + val exception = shouldThrow { + oppdragClient.hentFeilutbetalingerFraSimulering( + HentFeilutbetalingerFraSimuleringRequest( + Ytelsestype.OVERGANGSSTØNAD, + "123", + "1", + ), + ) + } + exception.shouldNotBeNull() + exception.shouldBeInstanceOf() + exception.message shouldBe "Noe gikk galt ved henting av feilutbetalinger fra simulering" + exception.cause?.message shouldBe "503 Couldn't send message: [no body]" + } + + private fun lagIverksettelseRespons(): TilbakekrevingsvedtakResponse { + val mmelDto = lagMmmelDto("00", "OK") + + val respons = TilbakekrevingsvedtakResponse() + respons.mmel = mmelDto + respons.tilbakekrevingsvedtak = tilbakekrevingsvedtakRequest.tilbakekrevingsvedtak + + return respons + } + + private fun lagHentKravgrunnlagRespons( + alvorlighetsgrad: String, + kodeMelding: String, + ): KravgrunnlagHentDetaljResponse { + val mmelDto = lagMmmelDto(alvorlighetsgrad, kodeMelding) + + val respons = KravgrunnlagHentDetaljResponse() + respons.mmel = mmelDto + respons.detaljertkravgrunnlag = DetaljertKravgrunnlagDto() + respons.detaljertkravgrunnlag = KravgrunnlagUtil + .unmarshalKravgrunnlag(readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml")) + return respons + } + + private fun lagMmmelDto(alvorlighetsgrad: String, kodeMelding: String): MmelDto { + val mmelDto = MmelDto() + mmelDto.alvorlighetsgrad = alvorlighetsgrad + mmelDto.kodeMelding = kodeMelding + return mmelDto + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/AvsluttBehandlingTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/AvsluttBehandlingTaskTest.kt new file mode 100644 index 000000000..83dda94c9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/AvsluttBehandlingTaskTest.kt @@ -0,0 +1,82 @@ +package no.nav.familie.tilbake.iverksettvedtak + +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.historikkinnslag.LagHistorikkinnslagTask +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.iverksettvedtak.task.AvsluttBehandlingTask +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class AvsluttBehandlingTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var avsluttBehandlingTask: AvsluttBehandlingTask + + private val fagsak = Testdata.fagsak + private val behandling = Testdata.behandling + private val behandlingId = behandling.id + + @BeforeEach + fun init() { + fagsakRepository.insert(fagsak) + behandlingRepository.insert(behandling) + } + + @Test + fun `doTask skal avslutte behandling`() { + var behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.IVERKSETTER_VEDTAK)) + behandlingsstegstilstandRepository.insert( + Behandlingsstegstilstand( + behandlingId = behandlingId, + behandlingssteg = Behandlingssteg.AVSLUTTET, + behandlingsstegsstatus = Behandlingsstegstatus.KLAR, + ), + ) + + avsluttBehandlingTask.doTask(Task(type = AvsluttBehandlingTask.TYPE, payload = behandlingId.toString())) + + behandling = behandlingRepository.findByIdOrThrow(behandlingId) + behandling.status shouldBe Behandlingsstatus.AVSLUTTET + + val stegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + stegstilstand.size shouldBe 1 + stegstilstand[0].behandlingssteg shouldBe Behandlingssteg.AVSLUTTET + stegstilstand[0].behandlingsstegsstatus shouldBe Behandlingsstegstatus.UTFØRT + + val tasker = taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)) + val historikkTask = tasker.first { it.type == LagHistorikkinnslagTask.TYPE } + historikkTask.type shouldBe LagHistorikkinnslagTask.TYPE + historikkTask.payload shouldBe behandlingId.toString() + val taskProperty = historikkTask.metadata + taskProperty["aktør"] shouldBe Aktør.VEDTAKSLØSNING.name + taskProperty["historikkinnslagstype"] shouldBe TilbakekrevingHistorikkinnslagstype.BEHANDLING_AVSLUTTET.name + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/IverksettelseServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/IverksettelseServiceTest.kt new file mode 100644 index 000000000..235e53a79 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/IverksettelseServiceTest.kt @@ -0,0 +1,386 @@ +package no.nav.familie.tilbake.iverksettvedtak + +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock +import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldNotBeEmpty +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.mockk +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.kontrakter.felles.Ressurs +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.AktsomhetDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegVilkårsvurderingDto +import no.nav.familie.tilbake.api.dto.SærligGrunnDto +import no.nav.familie.tilbake.api.dto.VilkårsvurderingsperiodeDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.BehandlingsvedtakService +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandlingsresultatstype +import no.nav.familie.tilbake.beregning.TilbakekrevingsberegningService +import no.nav.familie.tilbake.common.exceptionhandler.IntegrasjonException +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.integration.økonomi.DefaultOppdragClient +import no.nav.familie.tilbake.integration.økonomi.OppdragClient +import no.nav.familie.tilbake.iverksettvedtak.domain.KodeResultat +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.Fagområdekode +import no.nav.familie.tilbake.kravgrunnlag.domain.GjelderType +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassekode +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.KodeAksjon +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsperiode432 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import no.nav.okonomi.tilbakekrevingservice.TilbakekrevingsvedtakResponse +import no.nav.tilbakekreving.tilbakekrevingsvedtak.vedtak.v1.TilbakekrevingsbelopDto +import no.nav.tilbakekreving.tilbakekrevingsvedtak.vedtak.v1.TilbakekrevingsvedtakDto +import no.nav.tilbakekreving.typer.v1.MmelDto +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.web.client.RestOperations +import java.math.BigDecimal +import java.math.BigInteger +import java.net.URI +import java.time.YearMonth +import java.util.UUID + +internal class IverksettelseServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var økonomiXmlSendtRepository: ØkonomiXmlSendtRepository + + @Autowired + private lateinit var vilkårsvurderingService: VilkårsvurderingService + + @Autowired + private lateinit var tilbakekrevingsvedtakBeregningService: TilbakekrevingsvedtakBeregningService + + @Autowired + private lateinit var behandlingVedtakService: BehandlingsvedtakService + + @Autowired + private lateinit var beregningService: TilbakekrevingsberegningService + + private lateinit var iverksettelseService: IverksettelseService + private lateinit var oppdragClient: OppdragClient + + private val restOperations: RestOperations = RestTemplateBuilder().build() + private val wireMockServer = WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()) + + private val mockFeatureToggleService: FeatureToggleService = mockk() + private val fagsak = Testdata.fagsak + private val behandling = Testdata.behandling + private val behandlingId = behandling.id + private val perioder = listOf( + Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)), + Månedsperiode(YearMonth.of(2021, 2), YearMonth.of(2021, 2)), + ) + private lateinit var kravgrunnlag431: Kravgrunnlag431 + + @BeforeEach + fun init() { + fagsakRepository.insert(fagsak) + behandlingRepository.insert(behandling) + + kravgrunnlag431 = lagKravgrunnlag() + lagVilkårsvurdering() + + behandlingVedtakService.opprettBehandlingsvedtak(behandlingId) + + wireMockServer.start() + oppdragClient = DefaultOppdragClient(restOperations, URI.create(wireMockServer.baseUrl())) + + iverksettelseService = IverksettelseService( + behandlingRepository, + kravgrunnlagRepository, + økonomiXmlSendtRepository, + tilbakekrevingsvedtakBeregningService, + beregningService, + behandlingVedtakService, + oppdragClient, + mockFeatureToggleService, + ) + } + + @AfterEach + fun tearDown() { + wireMockServer.resetAll() + wireMockServer.stop() + } + + @Test + fun `sendIverksettVedtak skal sende iverksettvedtak til økonomi for suksess respons`() { + wireMockServer.stubFor( + WireMock.post(WireMock.urlEqualTo("/${DefaultOppdragClient.IVERKSETTELSE_PATH}/$behandlingId")) + .willReturn( + WireMock.okJson( + Ressurs.success( + lagRespons( + "00", + "OK", + ), + ).toJson(), + ), + ), + ) + + iverksettelseService.sendIverksettVedtak(behandlingId) + + val økonomiXmlSendt = økonomiXmlSendtRepository.findByBehandlingId(behandlingId) + økonomiXmlSendt.shouldNotBeNull() + assertRequestXml(økonomiXmlSendt.melding, behandlingId, økonomiXmlSendt.id) + assertRespons(økonomiXmlSendt.kvittering, "00", "OK") + + val behandling = behandlingRepository.findByIdOrThrow(behandlingId) + val aktivBehandlingsresultat = behandling.sisteResultat + aktivBehandlingsresultat.shouldNotBeNull() + aktivBehandlingsresultat.type shouldBe Behandlingsresultatstype.FULL_TILBAKEBETALING + } + + @Test + fun `sendIverksettVedtak skal sende iverksettvedtak til økonomi for feil respons`() { + wireMockServer.stubFor( + WireMock.post(WireMock.urlEqualTo("/${DefaultOppdragClient.IVERKSETTELSE_PATH}/$behandlingId")) + .willReturn( + WireMock.okJson( + Ressurs.success( + lagRespons( + "10", + "feil", + ), + ).toJson(), + ), + ), + ) + + val exception = shouldThrow { iverksettelseService.sendIverksettVedtak(behandlingId) } + exception.shouldBeInstanceOf() + exception.message shouldBe "Noe gikk galt ved iverksetting av behandling=$behandlingId" + exception.cause!!.message shouldBe "Fikk feil respons fra økonomi ved iverksetting av behandling=$behandlingId." + + "Mottatt respons:${objectMapper.writeValueAsString(lagMmmelDto("10", "feil"))}" + + val økonomiXmlSendt = økonomiXmlSendtRepository.findByBehandlingId(behandlingId) + økonomiXmlSendt.shouldNotBeNull() + assertRequestXml(økonomiXmlSendt.melding, behandlingId, økonomiXmlSendt.id) + økonomiXmlSendt.kvittering.shouldBeNull() + } + + private fun lagKravgrunnlag(): Kravgrunnlag431 { + val feilPostering = lagKravgrunnlagsbeløp( + klassetype = Klassetype.FEIL, + klassekode = Klassekode.KL_KODE_FEIL_BA, + nyttBeløp = BigDecimal(5000), + ) + + val ytelPostering = lagKravgrunnlagsbeløp( + klassetype = Klassetype.YTEL, + klassekode = Klassekode.BATR, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(5000), + ) + + val kravgrunnlagsperioder = perioder.map { + Kravgrunnlagsperiode432( + periode = it, + månedligSkattebeløp = BigDecimal.ZERO, + beløp = setOf( + feilPostering.copy(id = UUID.randomUUID()), + ytelPostering.copy(id = UUID.randomUUID()), + ), + ) + }.toSet() + + val kravgrunnlag = Kravgrunnlag431( + behandlingId = behandlingId, + vedtakId = BigInteger.ZERO, + kravstatuskode = Kravstatuskode.NYTT, + fagområdekode = Fagområdekode.BA, + fagsystemId = fagsak.eksternFagsakId, + gjelderVedtakId = "testverdi", + gjelderType = GjelderType.PERSON, + utbetalesTilId = "testverdi", + utbetIdType = GjelderType.PERSON, + ansvarligEnhet = "testverdi", + bostedsenhet = "testverdi", + behandlingsenhet = "testverdi", + kontrollfelt = "testverdi", + referanse = behandling.aktivFagsystemsbehandling.eksternId, + eksternKravgrunnlagId = BigInteger.ZERO, + saksbehandlerId = "testverdi", + perioder = kravgrunnlagsperioder, + ) + kravgrunnlagRepository.insert(kravgrunnlag) + + return kravgrunnlag + } + + private fun lagKravgrunnlagsbeløp( + klassetype: Klassetype, + klassekode: Klassekode, + nyttBeløp: BigDecimal = BigDecimal.ZERO, + utbetaltBeløp: BigDecimal = BigDecimal.ZERO, + tilbakekrevesBeløp: BigDecimal = BigDecimal.ZERO, + ): Kravgrunnlagsbeløp433 { + return Kravgrunnlagsbeløp433( + klassetype = klassetype, + klassekode = klassekode, + nyttBeløp = nyttBeløp, + opprinneligUtbetalingsbeløp = utbetaltBeløp, + tilbakekrevesBeløp = tilbakekrevesBeløp, + skatteprosent = BigDecimal.ZERO, + ) + } + + private fun lagVilkårsvurdering() { + val vilkårsperioder = perioder.map { + VilkårsvurderingsperiodeDto( + periode = it.toDatoperiode(), + begrunnelse = "testverdi", + aktsomhetDto = AktsomhetDto( + aktsomhet = Aktsomhet.GROV_UAKTSOMHET, + begrunnelse = "testverdi", + særligeGrunnerTilReduksjon = false, + tilbakekrevSmåbeløp = true, + særligeGrunnerBegrunnelse = "testverdi", + særligeGrunner = listOf( + SærligGrunnDto( + særligGrunn = SærligGrunn.ANNET, + begrunnelse = "testverdi", + ), + ), + ), + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + ) + } + vilkårsvurderingService.lagreVilkårsvurdering(behandling.id, BehandlingsstegVilkårsvurderingDto(vilkårsperioder)) + } + + private fun lagRespons( + alvorlighetsgrad: String, + kodeMelding: String, + ): TilbakekrevingsvedtakResponse { + val mmelDto = lagMmmelDto(alvorlighetsgrad, kodeMelding) + + val respons = TilbakekrevingsvedtakResponse() + respons.mmel = mmelDto + respons.tilbakekrevingsvedtak = TilbakekrevingsvedtakDto() + + return respons + } + + private fun lagMmmelDto(alvorlighetsgrad: String, kodeMelding: String): MmelDto { + val mmelDto = MmelDto() + mmelDto.alvorlighetsgrad = alvorlighetsgrad + mmelDto.kodeMelding = kodeMelding + return mmelDto + } + + private fun assertRespons( + kvittering: String?, + alvorlighetsgrad: String, + kodeMelding: String, + ) { + kvittering.shouldNotBeEmpty() + val mmelDto = objectMapper.readValue(kvittering, MmelDto::class.java) + mmelDto.alvorlighetsgrad shouldBe alvorlighetsgrad + mmelDto.kodeMelding shouldBe kodeMelding + } + + private fun assertRequestXml(melding: String, behandlingId: UUID, xmlId: UUID) { + val request = TilbakekrevingsvedtakMarshaller.unmarshall(melding, behandlingId, xmlId) + request.shouldNotBeNull() + + val tilbakekrevingsvedtak = request.tilbakekrevingsvedtak + tilbakekrevingsvedtak.kodeAksjon shouldBe KodeAksjon.FATTE_VEDTAK.kode + tilbakekrevingsvedtak.datoVedtakFagsystem.shouldNotBeNull() + tilbakekrevingsvedtak.vedtakId shouldBe BigInteger.ZERO + tilbakekrevingsvedtak.kodeHjemmel shouldBe "22-15" + tilbakekrevingsvedtak.enhetAnsvarlig shouldBe kravgrunnlag431.ansvarligEnhet + + val førstePeriode = tilbakekrevingsvedtak.tilbakekrevingsperiode[0] + førstePeriode.periode.shouldNotBeNull() + førstePeriode.belopRenter shouldBe BigDecimal.ZERO + førstePeriode.tilbakekrevingsbelop.size shouldBe 2 + assertBeløp( + beløpene = førstePeriode.tilbakekrevingsbelop, + klassekode = Klassekode.KL_KODE_FEIL_BA, + nyttBeløp = BigDecimal(5000), + ) + assertBeløp( + beløpene = førstePeriode.tilbakekrevingsbelop, + klassekode = Klassekode.BATR, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(5000), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsvedtak.tilbakekrevingsperiode[0] + andrePeriode.periode.shouldNotBeNull() + andrePeriode.belopRenter shouldBe BigDecimal.ZERO + andrePeriode.tilbakekrevingsbelop.size shouldBe 2 + assertBeløp( + beløpene = andrePeriode.tilbakekrevingsbelop, + klassekode = Klassekode.KL_KODE_FEIL_BA, + nyttBeløp = BigDecimal(5000), + ) + assertBeløp( + beløpene = andrePeriode.tilbakekrevingsbelop, + klassekode = Klassekode.BATR, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(5000), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + } + + private fun assertBeløp( + beløpene: List, + klassekode: Klassekode, + nyttBeløp: BigDecimal = BigDecimal.ZERO, + utbetaltBeløp: BigDecimal = BigDecimal.ZERO, + tilbakekrevesBeløp: BigDecimal = BigDecimal.ZERO, + uinnkrevdBeløp: BigDecimal = BigDecimal.ZERO, + skattBeløp: BigDecimal = BigDecimal.ZERO, + kodeResultat: KodeResultat? = null, + ) { + beløpene.any { + klassekode.name == it.kodeKlasse && + nyttBeløp == it.belopNy && + utbetaltBeløp == it.belopOpprUtbet && + tilbakekrevesBeløp == it.belopTilbakekreves && + uinnkrevdBeløp == it.belopUinnkrevd + skattBeløp == it.belopSkatt + }.shouldBeTrue() + + beløpene.any { + kodeResultat?.kode == it.kodeResultat && + "ANNET" == it.kodeAarsak && + "IKKE_FORDELT" == it.kodeSkyld + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/IverksettelseServiceUnitTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/IverksettelseServiceUnitTest.kt new file mode 100644 index 000000000..b15e38ccb --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/IverksettelseServiceUnitTest.kt @@ -0,0 +1,203 @@ +package no.nav.familie.tilbake.iverksettvedtak + +import io.mockk.CapturingSlot +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.BehandlingsvedtakService +import no.nav.familie.tilbake.beregning.TilbakekrevingsberegningService +import no.nav.familie.tilbake.beregning.modell.Beregningsresultat +import no.nav.familie.tilbake.beregning.modell.Beregningsresultatsperiode +import no.nav.familie.tilbake.beregning.modell.Vedtaksresultat +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.integration.økonomi.OppdragClient +import no.nav.familie.tilbake.iverksettvedtak.domain.KodeResultat.DELVIS_TILBAKEKREVING +import no.nav.familie.tilbake.iverksettvedtak.domain.KodeResultat.FULL_TILBAKEKREVING +import no.nav.familie.tilbake.iverksettvedtak.domain.Tilbakekrevingsbeløp +import no.nav.familie.tilbake.iverksettvedtak.domain.Tilbakekrevingsperiode +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassekode +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsperiode432 +import no.nav.okonomi.tilbakekrevingservice.TilbakekrevingsvedtakRequest +import no.nav.okonomi.tilbakekrevingservice.TilbakekrevingsvedtakResponse +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.math.RoundingMode +import java.time.YearMonth + +class IverksettelseServiceUnitTest { + + val behandlingRepository = mockk() + val kravgrunnlagRepository = mockk() + val økonomiXmlSendtRepository = mockk<ØkonomiXmlSendtRepository>() + val tilbakekrevingsvedtakBeregningService = mockk() + val beregningService = mockk() + val behandlingVedtakService = mockk() + val oppdragClient = mockk() + val featureToggleService = mockk() + + val behandling = Testdata.behandling + + val iverksettelseService = IverksettelseService( + behandlingRepository, + kravgrunnlagRepository, + økonomiXmlSendtRepository, + tilbakekrevingsvedtakBeregningService, + beregningService, + behandlingVedtakService, + oppdragClient, + featureToggleService, + ) + + @Test + fun `skal endre fra delvis til full tilbakekreving dersom utestående beløp er 0 og featuretoggle skrudd på`() { + val requestSlot = settOppMockDataSomGirUriktigDelvisTilbakekrevingForEnPeriode() + + every { featureToggleService.isEnabled(any()) } returns true + iverksettelseService.sendIverksettVedtak(behandling.id) + + val tilbakekrevingsperioder = requestSlot.captured.tilbakekrevingsvedtak.tilbakekrevingsperiode + + assertThat(tilbakekrevingsperioder).hasSize(2) + assertThat(tilbakekrevingsperioder.first().tilbakekrevingsbelop.first().kodeResultat).isEqualTo(DELVIS_TILBAKEKREVING.kode) + assertThat(tilbakekrevingsperioder.last().tilbakekrevingsbelop.first().kodeResultat).isEqualTo(FULL_TILBAKEKREVING.kode) + } + + @Test + fun `skal beholde delvis tilbakekreving selv om utestående beløp er 0 når featuretoggle ikke er på`() { + val requestSlot = settOppMockDataSomGirUriktigDelvisTilbakekrevingForEnPeriode() + + every { featureToggleService.isEnabled(any()) } returns false + iverksettelseService.sendIverksettVedtak(behandling.id) + + val tilbakekrevingsperioder = requestSlot.captured.tilbakekrevingsvedtak.tilbakekrevingsperiode + + assertThat(tilbakekrevingsperioder).hasSize(2) + assertThat(tilbakekrevingsperioder.first().tilbakekrevingsbelop.first().kodeResultat).isEqualTo(DELVIS_TILBAKEKREVING.kode) + assertThat(tilbakekrevingsperioder.last().tilbakekrevingsbelop.first().kodeResultat).isEqualTo(DELVIS_TILBAKEKREVING.kode) + } + + private fun settOppMockDataSomGirUriktigDelvisTilbakekrevingForEnPeriode(): CapturingSlot { + val requestSlot = slot() + val tilbakekrevingsperioder = lagTilbakekrevingsperiode() + val kravgrunnlag = lagKravgrunnlag() + every { behandlingRepository.findByIdOrThrow(any()) } returns behandling + every { kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(any()) } returns kravgrunnlag + every { tilbakekrevingsvedtakBeregningService.beregnVedtaksperioder(any(), any()) } returns tilbakekrevingsperioder + every { økonomiXmlSendtRepository.insert(any()) } returns Testdata.økonomiXmlSendt + every { oppdragClient.iverksettVedtak(any(), capture(requestSlot)) } returns TilbakekrevingsvedtakResponse() + every { økonomiXmlSendtRepository.findByIdOrThrow(any()) } returns Testdata.økonomiXmlSendt + every { økonomiXmlSendtRepository.update(any()) } returns Testdata.økonomiXmlSendt + every { beregningService.beregn(any()) } returns lagBeregningsresultat() + every { behandlingVedtakService.oppdaterBehandlingsvedtak(any(), any()) } returns behandling + return requestSlot + } + + private fun lagBeregningsresultat() = Beregningsresultat( + beregningsresultatsperioder = listOf( + Beregningsresultatsperiode( + periode = Månedsperiode(YearMonth.now().minusMonths(2), YearMonth.now().minusMonths(1)), + vurdering = null, + feilutbetaltBeløp = BigDecimal(10000), + andelAvBeløp = BigDecimal(9983).divide(BigDecimal(10000), 2, RoundingMode.HALF_UP), + renteprosent = null, + manueltSattTilbakekrevingsbeløp = BigDecimal(9983), + tilbakekrevingsbeløpUtenRenter = BigDecimal(9983), + rentebeløp = BigDecimal.ZERO, + tilbakekrevingsbeløp = BigDecimal(9983), + skattebeløp = BigDecimal.ZERO, + tilbakekrevingsbeløpEtterSkatt = BigDecimal(9983), + utbetaltYtelsesbeløp = BigDecimal.ZERO, + riktigYtelsesbeløp = BigDecimal.ZERO, + ), + Beregningsresultatsperiode( + periode = Månedsperiode(YearMonth.now().minusMonths(1), YearMonth.now()), + vurdering = null, + feilutbetaltBeløp = BigDecimal(47), + andelAvBeløp = BigDecimal.ONE, + renteprosent = null, + manueltSattTilbakekrevingsbeløp = BigDecimal(47), + tilbakekrevingsbeløpUtenRenter = BigDecimal(47), + rentebeløp = BigDecimal.ZERO, + tilbakekrevingsbeløp = BigDecimal(47), + skattebeløp = BigDecimal.ZERO, + tilbakekrevingsbeløpEtterSkatt = BigDecimal(47), + utbetaltYtelsesbeløp = BigDecimal.ZERO, + riktigYtelsesbeløp = BigDecimal.ZERO, + ), + ), + vedtaksresultat = Vedtaksresultat.DELVIS_TILBAKEBETALING, + ) + + private fun lagTilbakekrevingsperiode() = + listOf( + Tilbakekrevingsperiode( + periode = Månedsperiode(YearMonth.now().minusMonths(2), YearMonth.now().minusMonths(1)), + renter = BigDecimal.ZERO, + beløp = listOf( + Tilbakekrevingsbeløp( + klassetype = Klassetype.YTEL, + klassekode = Klassekode.EFOG, + nyttBeløp = BigDecimal.ZERO, + utbetaltBeløp = BigDecimal.ZERO, + tilbakekrevesBeløp = BigDecimal(9983), + uinnkrevdBeløp = BigDecimal(17), + skattBeløp = BigDecimal.ZERO, + kodeResultat = DELVIS_TILBAKEKREVING, + ), + ), + ), + Tilbakekrevingsperiode( + periode = Månedsperiode(YearMonth.now().minusMonths(1), YearMonth.now()), + renter = BigDecimal.ZERO, + beløp = listOf( + Tilbakekrevingsbeløp( + klassetype = Klassetype.YTEL, + klassekode = Klassekode.EFOG, + nyttBeløp = BigDecimal.ZERO, + utbetaltBeløp = BigDecimal.ZERO, + tilbakekrevesBeløp = BigDecimal(47), + uinnkrevdBeløp = BigDecimal(0), + skattBeløp = BigDecimal.ZERO, + kodeResultat = DELVIS_TILBAKEKREVING, + ), + ), + ), + ) + + private fun lagKravgrunnlag() = Testdata.kravgrunnlag431.copy( + perioder = setOf( + Kravgrunnlagsperiode432( + periode = Månedsperiode( + YearMonth.now().minusMonths(2), + YearMonth.now().minusMonths(1), + ), + beløp = setOf( + Testdata.feilKravgrunnlagsbeløp433, + Testdata.ytelKravgrunnlagsbeløp433, + ), + månedligSkattebeløp = BigDecimal.ZERO, + ), + Kravgrunnlagsperiode432( + periode = Månedsperiode( + YearMonth.now().minusMonths(1), + YearMonth.now(), + ), + beløp = setOf( + Testdata.feilKravgrunnlagsbeløp433, + Testdata.ytelKravgrunnlagsbeløp433.copy( + tilbakekrevesBeløp = BigDecimal(47), + opprinneligUtbetalingsbeløp = BigDecimal(47), + ), + ), + månedligSkattebeløp = BigDecimal.ZERO, + ), + ), + ) +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/TilbakekrevingsvedtakBeregningServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/TilbakekrevingsvedtakBeregningServiceTest.kt new file mode 100644 index 000000000..014b8528e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/TilbakekrevingsvedtakBeregningServiceTest.kt @@ -0,0 +1,1657 @@ +package no.nav.familie.tilbake.iverksettvedtak + +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.AktsomhetDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegForeldelseDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegVilkårsvurderingDto +import no.nav.familie.tilbake.api.dto.ForeldelsesperiodeDto +import no.nav.familie.tilbake.api.dto.GodTroDto +import no.nav.familie.tilbake.api.dto.SærligGrunnDto +import no.nav.familie.tilbake.api.dto.VilkårsvurderingsperiodeDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.foreldelse.ForeldelseService +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.iverksettvedtak.VilkårsvurderingsPeriodeDomainUtil.lagGrovtUaktsomVilkårsvurderingsperiode +import no.nav.familie.tilbake.iverksettvedtak.domain.KodeResultat +import no.nav.familie.tilbake.iverksettvedtak.domain.Tilbakekrevingsbeløp +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagMapper +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagUtil +import no.nav.familie.tilbake.kravgrunnlag.domain.Fagområdekode +import no.nav.familie.tilbake.kravgrunnlag.domain.GjelderType +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassekode +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsperiode432 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingService +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn.ANNET +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn.GRAD_AV_UAKTSOMHET +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn.HELT_ELLER_DELVIS_NAVS_FEIL +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.math.BigInteger +import java.time.YearMonth + +internal class TilbakekrevingsvedtakBeregningServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var vilkårsvurderingService: VilkårsvurderingService + + @Autowired + private lateinit var foreldelsesService: ForeldelseService + + @Autowired + private lateinit var vedtakBeregningService: TilbakekrevingsvedtakBeregningService + + @Autowired + private lateinit var iverksettelseService: IverksettelseService + + private val fagsak = Testdata.fagsak + private val behandling = Testdata.behandling + + private val perioder = listOf( + Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)), + Månedsperiode(YearMonth.of(2021, 2), YearMonth.of(2021, 2)), + ) + + private lateinit var kravgrunnlag: Kravgrunnlag431 + + @BeforeEach + fun init() { + fagsakRepository.insert(fagsak) + behandlingRepository.insert(behandling) + + val månedligSkattBeløp = BigDecimal.ZERO + val kravgrunnlagsbeløpene = listOf( + Kravgrunnlagsbeløp(klassetype = Klassetype.FEIL, nyttBeløp = BigDecimal(5000)), + Kravgrunnlagsbeløp( + klassetype = Klassetype.YTEL, + opprinneligUtbetalingsbeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(5000), + ), + ) + + kravgrunnlag = lagKravgrunnlag(perioder, månedligSkattBeløp, kravgrunnlagsbeløpene) + kravgrunnlagRepository.insert(kravgrunnlag) + } + + @Test + fun `beregnVedtaksperioder skal beregne når vilkårsvurderte med 50 prosent andel tilbakekrevesbeløp`() { + lagAktsomhetVilkårsvurdering( + perioder = listOf( + Månedsperiode( + YearMonth.of(2021, 1), + YearMonth.of(2021, 2), + ), + ), + aktsomhet = Aktsomhet.SIMPEL_UAKTSOMHET, + andelTilbakreves = BigDecimal(50), + særligeGrunnerTilReduksjon = true, + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 2 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)) + førstePeriode.renter shouldBe BigDecimal.ZERO + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(2500), + uinnkrevdBeløp = BigDecimal(2500), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 2), YearMonth.of(2021, 2)) + andrePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(2500), + uinnkrevdBeløp = BigDecimal(2500), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne når vilkårsvurderte med 33 prosent andel tilbakekrevesbeløp`() { + lagAktsomhetVilkårsvurdering( + perioder = listOf( + Månedsperiode( + YearMonth.of(2021, 1), + YearMonth.of(2021, 2), + ), + ), + aktsomhet = Aktsomhet.SIMPEL_UAKTSOMHET, + andelTilbakreves = BigDecimal(33), + særligeGrunnerTilReduksjon = true, + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 2 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)) + førstePeriode.renter shouldBe BigDecimal.ZERO + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(1650), + uinnkrevdBeløp = BigDecimal(3350), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 2), YearMonth.of(2021, 2)) + andrePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(1650), + uinnkrevdBeløp = BigDecimal(3350), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne når vilkårsvurderte med Forsett aktsomhet`() { + lagAktsomhetVilkårsvurdering( + perioder = listOf( + Månedsperiode( + YearMonth.of(2021, 1), + YearMonth.of(2021, 2), + ), + ), + aktsomhet = Aktsomhet.FORSETT, + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 2 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)) + førstePeriode.renter shouldBe BigDecimal.ZERO + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(5000), + uinnkrevdBeløp = BigDecimal.ZERO, + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 2), YearMonth.of(2021, 2)) + andrePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(5000), + uinnkrevdBeløp = BigDecimal.ZERO, + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne når vilkårsvurderte med God tro og ingen tilbakekreving`() { + lagGodTroVilkårsvurdering( + perioder = listOf( + Månedsperiode( + YearMonth.of(2021, 1), + YearMonth.of(2021, 2), + ), + ), + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 2 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)) + førstePeriode.renter shouldBe BigDecimal.ZERO + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.INGEN_TILBAKEKREVING) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal.ZERO, + uinnkrevdBeløp = BigDecimal(5000), + kodeResultat = KodeResultat.INGEN_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 2), YearMonth.of(2021, 2)) + andrePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.INGEN_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal.ZERO, + uinnkrevdBeløp = BigDecimal(5000), + kodeResultat = KodeResultat.INGEN_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne når vilkårsvurderte med God tro og bestemt tilbakekrevesbeløp`() { + lagGodTroVilkårsvurdering( + perioder = listOf( + Månedsperiode( + YearMonth.of(2021, 1), + YearMonth.of(2021, 2), + ), + ), + beløpErIBehold = true, + beløpTilbakekreves = BigDecimal(3000), + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 2 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)) + førstePeriode.renter shouldBe BigDecimal.ZERO + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(1500), + uinnkrevdBeløp = BigDecimal(3500), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 2), YearMonth.of(2021, 2)) + andrePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(1500), + uinnkrevdBeløp = BigDecimal(3500), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne når vilkårsvurderte med God tro og bestemt tilbakekrevesbeløp med avrunding`() { + lagGodTroVilkårsvurdering( + perioder = listOf( + Månedsperiode( + YearMonth.of(2021, 1), + YearMonth.of(2021, 2), + ), + ), + beløpErIBehold = true, + beløpTilbakekreves = BigDecimal(1999), + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 2 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)) + førstePeriode.renter shouldBe BigDecimal.ZERO + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(999), + uinnkrevdBeløp = BigDecimal(4001), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 2), YearMonth.of(2021, 2)) + andrePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(1000), + uinnkrevdBeløp = BigDecimal(4000), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne når vilkårsvurderte med 50 prosent andeltilbakekrevesbeløp med skatt beløp`() { + val månedligSkattBeløp = BigDecimal(500) + + kravgrunnlagRepository.deleteById(kravgrunnlag.id) + val kravgrunnlagsbeløpene = listOf( + Kravgrunnlagsbeløp( + klassetype = Klassetype.FEIL, + nyttBeløp = BigDecimal(5000), + skatteprosent = BigDecimal(10), + ), + Kravgrunnlagsbeløp( + klassetype = Klassetype.YTEL, + opprinneligUtbetalingsbeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(5000), + skatteprosent = BigDecimal(10), + ), + ) + + val kravgrunnlag = lagKravgrunnlag(perioder, månedligSkattBeløp, kravgrunnlagsbeløpene) + kravgrunnlagRepository.insert(kravgrunnlag) + + lagAktsomhetVilkårsvurdering( + perioder = listOf( + Månedsperiode( + YearMonth.of(2021, 1), + YearMonth.of(2021, 2), + ), + ), + aktsomhet = Aktsomhet.SIMPEL_UAKTSOMHET, + andelTilbakreves = BigDecimal(50), + særligeGrunnerTilReduksjon = true, + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 2 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)) + førstePeriode.renter shouldBe BigDecimal.ZERO + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(2500), + uinnkrevdBeløp = BigDecimal(2500), + skattBeløp = BigDecimal(250), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 2), YearMonth.of(2021, 2)) + andrePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(2500), + uinnkrevdBeløp = BigDecimal(2500), + skattBeløp = BigDecimal(250), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne med en foreldet periode,en vilkårsvurdert periode med 100 prosent tilbakekreving`() { + lagForeldelse(listOf(perioder[0])) + + lagAktsomhetVilkårsvurdering(listOf(perioder[1]), Aktsomhet.GROV_UAKTSOMHET) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 2 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)) + førstePeriode.renter shouldBe BigDecimal.ZERO + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.FORELDET) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(0), + uinnkrevdBeløp = BigDecimal(5000), + skattBeløp = BigDecimal(0), + kodeResultat = KodeResultat.FORELDET, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 2), YearMonth.of(2021, 2)) + andrePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(5000), + uinnkrevdBeløp = BigDecimal(0), + skattBeløp = BigDecimal(0), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne med tre vilkårsvurdert periode med 100 prosent tilbakekreving`() { + kravgrunnlagRepository.deleteById(kravgrunnlag.id) + + val månedligSkattBeløp = BigDecimal(750) + val perioder = listOf( + Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)), + Månedsperiode(YearMonth.of(2021, 2), YearMonth.of(2021, 2)), + Månedsperiode(YearMonth.of(2021, 3), YearMonth.of(2021, 3)), + ) + + val kravgrunnlagsbeløpene = listOf( + Kravgrunnlagsbeløp( + klassetype = Klassetype.FEIL, + nyttBeløp = BigDecimal(5000), + skatteprosent = BigDecimal(15), + ), + Kravgrunnlagsbeløp( + klassetype = Klassetype.YTEL, + opprinneligUtbetalingsbeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(5000), + skatteprosent = BigDecimal(15), + ), + ) + val kravgrunnlag = lagKravgrunnlag(perioder, månedligSkattBeløp, kravgrunnlagsbeløpene) + kravgrunnlagRepository.insert(kravgrunnlag) + + // en beregnet periode med 100 prosent tilbakekreving + lagAktsomhetVilkårsvurdering( + listOf(Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 3))), + Aktsomhet.GROV_UAKTSOMHET, + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 3 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)) + førstePeriode.renter shouldBe BigDecimal.ZERO + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(5000), + uinnkrevdBeløp = BigDecimal(0), + skattBeløp = BigDecimal(750), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 2), YearMonth.of(2021, 2)) + andrePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(5000), + uinnkrevdBeløp = BigDecimal(0), + skattBeløp = BigDecimal(750), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val tredjePeriode = tilbakekrevingsperioder[2] + tredjePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 3), YearMonth.of(2021, 3)) + tredjePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = tredjePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(5000), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = tredjePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(5000), + tilbakekrevesBeløp = BigDecimal(5000), + uinnkrevdBeløp = BigDecimal(0), + skattBeløp = BigDecimal(750), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne med 2 foreldet, 2 god tro og 3 periode med 100 prosent tilbakekreving`() { + kravgrunnlagRepository.deleteById(kravgrunnlag.id) + + val perioder = listOf( + Månedsperiode(YearMonth.of(2020, 1), YearMonth.of(2020, 1)), + Månedsperiode(YearMonth.of(2020, 3), YearMonth.of(2020, 3)), + Månedsperiode(YearMonth.of(2020, 5), YearMonth.of(2020, 5)), + Månedsperiode(YearMonth.of(2020, 7), YearMonth.of(2020, 7)), + Månedsperiode(YearMonth.of(2020, 9), YearMonth.of(2020, 9)), + Månedsperiode(YearMonth.of(2020, 11), YearMonth.of(2020, 11)), + Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)), + ) + + val kravgrunnlagsbeløpene = listOf( + Kravgrunnlagsbeløp( + klassetype = Klassetype.FEIL, + nyttBeløp = BigDecimal(952.38), + ), + Kravgrunnlagsbeløp( + klassetype = Klassetype.YTEL, + opprinneligUtbetalingsbeløp = BigDecimal(952.38), + tilbakekrevesBeløp = BigDecimal(952.38), + ), + ) + val kravgrunnlag = lagKravgrunnlag(perioder, BigDecimal.ZERO, kravgrunnlagsbeløpene) + kravgrunnlagRepository.insert(kravgrunnlag) + + // 1,2 beregnet periode er foreldet + lagForeldelse(listOf(perioder[0], perioder[1])) + + // 3,4 beregnet periode er godtro med ingen tilbakekreving + val godtroPerioder = listOf(perioder[2], perioder[3]).map { + VilkårsvurderingsperiodeDto( + periode = it.toDatoperiode(), + begrunnelse = "testverdi", + godTroDto = GodTroDto(begrunnelse = "testverdi", beløpErIBehold = false), + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + ) + } + + // 5,6,7 beregnet periode er Forsett aktsomhet med Full tilbakekreving + val aktsomhetPerioder = listOf(perioder[4], perioder[5], perioder[6]).map { + VilkårsvurderingsperiodeDto( + periode = it.toDatoperiode(), + begrunnelse = "testverdi", + aktsomhetDto = AktsomhetDto(aktsomhet = Aktsomhet.FORSETT, begrunnelse = "testverdi"), + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + ) + } + vilkårsvurderingService.lagreVilkårsvurdering( + behandling.id, + BehandlingsstegVilkårsvurderingDto( + godtroPerioder + + aktsomhetPerioder, + ), + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 7 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe Månedsperiode(YearMonth.of(2020, 1), YearMonth.of(2020, 1)) + førstePeriode.renter shouldBe BigDecimal.ZERO + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(952), kodeResultat = KodeResultat.FORELDET) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(952), + tilbakekrevesBeløp = BigDecimal.ZERO, + uinnkrevdBeløp = BigDecimal(952), + kodeResultat = KodeResultat.FORELDET, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe Månedsperiode(YearMonth.of(2020, 3), YearMonth.of(2020, 3)) + andrePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(952), kodeResultat = KodeResultat.FORELDET) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(952), + tilbakekrevesBeløp = BigDecimal.ZERO, + uinnkrevdBeløp = BigDecimal(952), + kodeResultat = KodeResultat.FORELDET, + ) + + val tredjePeriode = tilbakekrevingsperioder[2] + tredjePeriode.periode shouldBe Månedsperiode(YearMonth.of(2020, 5), YearMonth.of(2020, 5)) + tredjePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = tredjePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(952), kodeResultat = KodeResultat.INGEN_TILBAKEKREVING) + ytelsePostering = tredjePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(952), + tilbakekrevesBeløp = BigDecimal.ZERO, + uinnkrevdBeløp = BigDecimal(952), + kodeResultat = KodeResultat.INGEN_TILBAKEKREVING, + ) + + val fjerdePeriode = tilbakekrevingsperioder[3] + fjerdePeriode.periode shouldBe Månedsperiode(YearMonth.of(2020, 7), YearMonth.of(2020, 7)) + fjerdePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = fjerdePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(952), kodeResultat = KodeResultat.INGEN_TILBAKEKREVING) + ytelsePostering = fjerdePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(952), + tilbakekrevesBeløp = BigDecimal.ZERO, + uinnkrevdBeløp = BigDecimal(952), + kodeResultat = KodeResultat.INGEN_TILBAKEKREVING, + ) + + val femtePeriode = tilbakekrevingsperioder[4] + femtePeriode.periode shouldBe Månedsperiode(YearMonth.of(2020, 9), YearMonth.of(2020, 9)) + femtePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = femtePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(952), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = femtePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(952), + tilbakekrevesBeløp = BigDecimal(952), + uinnkrevdBeløp = BigDecimal.ZERO, + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val sjettePeriode = tilbakekrevingsperioder[5] + sjettePeriode.periode shouldBe Månedsperiode(YearMonth.of(2020, 11), YearMonth.of(2020, 11)) + sjettePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = sjettePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(952), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = sjettePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(952), + tilbakekrevesBeløp = BigDecimal(952), + uinnkrevdBeløp = BigDecimal.ZERO, + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val sjuendePeriode = tilbakekrevingsperioder[6] + sjuendePeriode.periode shouldBe Månedsperiode(YearMonth.of(2021, 1), YearMonth.of(2021, 1)) + sjuendePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = sjuendePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(952), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = sjuendePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(952), + tilbakekrevesBeløp = BigDecimal(952), + uinnkrevdBeløp = BigDecimal.ZERO, + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne perioder med 2 god tro, 3 50 prosent og 3 100 prosent tilbakekreving med renter`() { + kravgrunnlagRepository.deleteById(kravgrunnlag.id) + fagsakRepository.update(fagsakRepository.findByIdOrThrow(fagsak.id).copy(fagsystem = Fagsystem.EF)) + + val kravgrunnlagxml = readXml("/kravgrunnlagxml/kravgrunnlag_EF_med_renter.xml") + val kravgrunnlag = KravgrunnlagMapper.tilKravgrunnlag431( + KravgrunnlagUtil.unmarshalKravgrunnlag(kravgrunnlagxml), + behandling.id, + ) + kravgrunnlagRepository.insert(kravgrunnlag) + + val sortedPerioder = kravgrunnlag.perioder.map { it.periode }.sortedBy { it.fom } + + // 1,2 perioder er vilkårsvurdert med god tro(ingen tilbakebetaling) + val godTroPeriode = VilkårsvurderingsperiodeDto( + periode = Datoperiode(sortedPerioder[0].fom, sortedPerioder[1].tom), + begrunnelse = "testverdi", + godTroDto = GodTroDto( + begrunnelse = "testverdi", + beløpErIBehold = false, + ), + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + ) + + val særligGrunner = listOf(SærligGrunnDto(ANNET, "testverdi")) + // 3,4 perioder er vilkårsvurdert med SIMPEL UAKTSOMHET(50 prosent tilbakebetaling) + val simpelUaktsomhetPeriode = VilkårsvurderingsperiodeDto( + periode = Datoperiode( + sortedPerioder[2].fom, + sortedPerioder[3].tom, + ), + begrunnelse = "testverdi", + aktsomhetDto = + AktsomhetDto( + aktsomhet = Aktsomhet.SIMPEL_UAKTSOMHET, + ileggRenter = false, + andelTilbakekreves = BigDecimal(50), + begrunnelse = "testverdi", + særligeGrunnerTilReduksjon = true, + tilbakekrevSmåbeløp = true, + særligeGrunnerBegrunnelse = "testverdi", + særligeGrunner = særligGrunner, + ), + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + ) + + // 5,6,7 perioder er vilkårsvurdert med GROV UAKTSOMHET(100 prosent tilbakebetaling) + val grovUaktsomhetPeriode = VilkårsvurderingsperiodeDto( + periode = Datoperiode( + sortedPerioder[4].fom, + sortedPerioder[6].tom, + ), + begrunnelse = "testverdi", + aktsomhetDto = + AktsomhetDto( + aktsomhet = Aktsomhet.GROV_UAKTSOMHET, + ileggRenter = true, + andelTilbakekreves = null, + begrunnelse = "testverdi", + særligeGrunnerTilReduksjon = false, + tilbakekrevSmåbeløp = true, + særligeGrunnerBegrunnelse = "testverdi", + særligeGrunner = særligGrunner, + ), + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + ) + vilkårsvurderingService.lagreVilkårsvurdering( + behandling.id, + BehandlingsstegVilkårsvurderingDto( + listOf( + godTroPeriode, + simpelUaktsomhetPeriode, + grovUaktsomhetPeriode, + ), + ), + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 7 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe sortedPerioder[0] + førstePeriode.renter shouldBe BigDecimal.ZERO + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(486), kodeResultat = KodeResultat.INGEN_TILBAKEKREVING) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(1544), + utbetaltBeløp = BigDecimal(2030), + tilbakekrevesBeløp = BigDecimal.ZERO, + uinnkrevdBeløp = BigDecimal(486), + kodeResultat = KodeResultat.INGEN_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe sortedPerioder[1] + andrePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(17336), kodeResultat = KodeResultat.INGEN_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(3574), + utbetaltBeløp = BigDecimal(20910), + tilbakekrevesBeløp = BigDecimal.ZERO, + uinnkrevdBeløp = BigDecimal(17336), + kodeResultat = KodeResultat.INGEN_TILBAKEKREVING, + ) + + val tredjePeriode = tilbakekrevingsperioder[2] + tredjePeriode.periode shouldBe sortedPerioder[2] + tredjePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = tredjePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(17241), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + ytelsePostering = tredjePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(5658), + utbetaltBeløp = BigDecimal(22899), + tilbakekrevesBeløp = BigDecimal(8620), + uinnkrevdBeløp = BigDecimal(8621), + skattBeløp = BigDecimal(4310), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + + val fjerdePeriode = tilbakekrevingsperioder[3] + fjerdePeriode.periode shouldBe sortedPerioder[3] + fjerdePeriode.renter shouldBe BigDecimal.ZERO + feilPostering = fjerdePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(17241), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + ytelsePostering = fjerdePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(5658), + utbetaltBeløp = BigDecimal(22899), + tilbakekrevesBeløp = BigDecimal(8621), + uinnkrevdBeløp = BigDecimal(8620), + skattBeløp = BigDecimal.ZERO, + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + + val femtePeriode = tilbakekrevingsperioder[4] + femtePeriode.periode shouldBe sortedPerioder[4] + femtePeriode.renter shouldBe BigDecimal(1724) + feilPostering = femtePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(17241), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = femtePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(5658), + utbetaltBeløp = BigDecimal(22899), + tilbakekrevesBeløp = BigDecimal(17241), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(8620), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val sjettePeriode = tilbakekrevingsperioder[5] + sjettePeriode.periode shouldBe sortedPerioder[5] + sjettePeriode.renter shouldBe BigDecimal(1724) + feilPostering = sjettePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(17241), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = sjettePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(5658), + utbetaltBeløp = BigDecimal(22899), + tilbakekrevesBeløp = BigDecimal(17241), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(8620), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val sjuendePeriode = tilbakekrevingsperioder[6] + sjuendePeriode.periode shouldBe sortedPerioder[6] + sjuendePeriode.renter shouldBe BigDecimal(1736) + feilPostering = sjuendePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(17364), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = sjuendePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(25485), + utbetaltBeløp = BigDecimal(42849), + tilbakekrevesBeløp = BigDecimal(17364), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(8682), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne periode med 100 prosent tilbakekreving og renter skal rundes ned`() { + kravgrunnlagRepository.deleteById(kravgrunnlag.id) + fagsakRepository.update(fagsakRepository.findByIdOrThrow(fagsak.id).copy(fagsystem = Fagsystem.EF)) + + val kravgrunnlagxml = readXml("/kravgrunnlagxml/kravgrunnlag_EF_med_renter_avrundingsfeil_ned.xml") + val kravgrunnlag = KravgrunnlagMapper.tilKravgrunnlag431( + KravgrunnlagUtil.unmarshalKravgrunnlag(kravgrunnlagxml), + behandling.id, + ) + kravgrunnlagRepository.insert(kravgrunnlag) + + val periode = kravgrunnlag.perioder.first().periode + + val grovUaktsomhetPeriode = lagGrovtUaktsomVilkårsvurderingsperiode(periode.fom, periode.tom) + + vilkårsvurderingService.lagreVilkårsvurdering( + behandling.id, + BehandlingsstegVilkårsvurderingDto( + listOf(grovUaktsomhetPeriode), + ), + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.size shouldBe 1 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val tilbakekrevingsperiode = tilbakekrevingsperioder[0] + tilbakekrevingsperiode.periode shouldBe periode + tilbakekrevingsperiode.renter shouldBe BigDecimal(1860) + } + + @Test + fun `beregnVedtaksperioder som beregner flere perioder i samme vilkårsperiode med 100 prosent tilbakekreving og renter skal aldri overstige 10%`() { + kravgrunnlagRepository.deleteById(kravgrunnlag.id) + fagsakRepository.update(fagsakRepository.findByIdOrThrow(fagsak.id).copy(fagsystem = Fagsystem.EF)) + + val kravgrunnlagxml = readXml("/kravgrunnlagxml/kravgrunnlag_EF_med_3_perioder_med_renter_avrunding_ned.xml") + val kravgrunnlag = KravgrunnlagMapper.tilKravgrunnlag431( + KravgrunnlagUtil.unmarshalKravgrunnlag(kravgrunnlagxml), + behandling.id, + ) + kravgrunnlagRepository.insert(kravgrunnlag) + + val sortedPerioder = kravgrunnlag.perioder.map { it.periode }.sortedBy { it.fom } + + val grovUaktsomhetPeriode = lagGrovtUaktsomVilkårsvurderingsperiode(sortedPerioder.first().fom, sortedPerioder.last().tom) + + vilkårsvurderingService.lagreVilkårsvurdering( + behandling.id, + BehandlingsstegVilkårsvurderingDto( + listOf(grovUaktsomhetPeriode), + ), + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.size shouldBe 3 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + tilbakekrevingsperioder[0].periode shouldBe sortedPerioder[0] + tilbakekrevingsperioder[0].renter shouldBe BigDecimal(1860) + + tilbakekrevingsperioder[1].periode shouldBe sortedPerioder[1] + tilbakekrevingsperioder[1].renter shouldBe BigDecimal(1861) + + tilbakekrevingsperioder[2].periode shouldBe sortedPerioder[2] + tilbakekrevingsperioder[2].renter shouldBe BigDecimal(1861) + + tilbakekrevingsperioder.sumOf { it.renter } shouldBe BigDecimal(5582) + } + + @Test + fun `beregnVedtaksperioder som beregner flere perioder i separate vilkårsperioder med 100 prosent tilbakekreving og renter skal skal avrunde hver renteperiode ned`() { + kravgrunnlagRepository.deleteById(kravgrunnlag.id) + fagsakRepository.update(fagsakRepository.findByIdOrThrow(fagsak.id).copy(fagsystem = Fagsystem.EF)) + + val kravgrunnlagxml = readXml("/kravgrunnlagxml/kravgrunnlag_EF_med_3_perioder_med_renter_avrunding_ned.xml") + val kravgrunnlag = KravgrunnlagMapper.tilKravgrunnlag431( + KravgrunnlagUtil.unmarshalKravgrunnlag(kravgrunnlagxml), + behandling.id, + ) + kravgrunnlagRepository.insert(kravgrunnlag) + + val sortedPerioder = kravgrunnlag.perioder.map { it.periode }.sortedBy { it.fom } + + val grovUaktsomhetPeriode1 = lagGrovtUaktsomVilkårsvurderingsperiode(sortedPerioder[0].fom, sortedPerioder[0].tom) + val grovUaktsomhetPeriode2 = lagGrovtUaktsomVilkårsvurderingsperiode(sortedPerioder[1].fom, sortedPerioder[1].tom) + val grovUaktsomhetPeriode3 = lagGrovtUaktsomVilkårsvurderingsperiode(sortedPerioder[2].fom, sortedPerioder[2].tom) + + vilkårsvurderingService.lagreVilkårsvurdering( + behandling.id, + BehandlingsstegVilkårsvurderingDto( + listOf(grovUaktsomhetPeriode1, grovUaktsomhetPeriode2, grovUaktsomhetPeriode3), + ), + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.size shouldBe 3 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + tilbakekrevingsperioder.forEachIndexed { index, tilbakekrevingsperiode -> + tilbakekrevingsperiode.periode shouldBe sortedPerioder[index] + tilbakekrevingsperiode.renter shouldBe BigDecimal(1860) + } + } + + @Test + fun `beregnVedtaksperioder skal beregne EF perioder med FORTSETT aktsomhet med full tilbakekreving med 10 prosent renter`() { + kravgrunnlagRepository.deleteById(kravgrunnlag.id) + fagsakRepository.update(fagsakRepository.findByIdOrThrow(fagsak.id).copy(fagsystem = Fagsystem.EF)) + + val kravgrunnlagxml = readXml("/kravgrunnlagxml/kravgrunnlag_EF_med_renter_avrunding.xml") + val kravgrunnlag = KravgrunnlagMapper.tilKravgrunnlag431( + KravgrunnlagUtil.unmarshalKravgrunnlag(kravgrunnlagxml), + behandling.id, + ) + kravgrunnlagRepository.insert(kravgrunnlag) + + val sortedPerioder = kravgrunnlag.perioder.map { it.periode }.sortedBy { it.fom } + + val aktsomhetPeriode = VilkårsvurderingsperiodeDto( + periode = Datoperiode( + sortedPerioder[0].fom, + sortedPerioder[6].tom, + ), + begrunnelse = "testverdi", + aktsomhetDto = AktsomhetDto( + aktsomhet = Aktsomhet.FORSETT, + begrunnelse = "fortsett begrunnelse", + ), + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + ) + + vilkårsvurderingService.lagreVilkårsvurdering(behandling.id, BehandlingsstegVilkårsvurderingDto(listOf(aktsomhetPeriode))) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 7 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe sortedPerioder[0] + førstePeriode.renter shouldBe BigDecimal(22) + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(209), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(4028), + utbetaltBeløp = BigDecimal(4237), + tilbakekrevesBeløp = BigDecimal(209), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(104), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe sortedPerioder[1] + andrePeriode.renter shouldBe BigDecimal(21) + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(208), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(5070), + utbetaltBeløp = BigDecimal(5278), + tilbakekrevesBeløp = BigDecimal(208), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(104), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val tredjePeriode = tilbakekrevingsperioder[2] + tredjePeriode.periode shouldBe sortedPerioder[2] + tredjePeriode.renter shouldBe BigDecimal(437) + feilPostering = tredjePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(4375), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = tredjePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(5070), + utbetaltBeløp = BigDecimal(9445), + tilbakekrevesBeløp = BigDecimal(4375), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal.ZERO, + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val fjerdePeriode = tilbakekrevingsperioder[3] + fjerdePeriode.periode shouldBe sortedPerioder[3] + fjerdePeriode.renter shouldBe BigDecimal(437) + feilPostering = fjerdePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(4375), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = fjerdePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(5070), + utbetaltBeløp = BigDecimal(9445), + tilbakekrevesBeløp = BigDecimal(4375), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(2187), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val femtePeriode = tilbakekrevingsperioder[4] + femtePeriode.periode shouldBe sortedPerioder[4] + femtePeriode.renter shouldBe BigDecimal(437) + feilPostering = femtePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(4375), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = femtePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(5070), + utbetaltBeløp = BigDecimal(9445), + tilbakekrevesBeløp = BigDecimal(4375), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(2187), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val sjettePeriode = tilbakekrevingsperioder[5] + sjettePeriode.periode shouldBe sortedPerioder[5] + sjettePeriode.renter shouldBe BigDecimal(375) + feilPostering = sjettePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(3750), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = sjettePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(10695), + utbetaltBeløp = BigDecimal(14445), + tilbakekrevesBeløp = BigDecimal(3750), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(1874), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val sjuendePeriode = tilbakekrevingsperioder[6] + sjuendePeriode.periode shouldBe sortedPerioder[6] + sjuendePeriode.renter shouldBe BigDecimal(375) + feilPostering = sjuendePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(3750), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = sjuendePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(10695), + utbetaltBeløp = BigDecimal(14445), + tilbakekrevesBeløp = BigDecimal(3750), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(1874), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne EF perioder med 50 prosent tilbakekreving og skatt avrunding`() { + kravgrunnlagRepository.deleteById(kravgrunnlag.id) + fagsakRepository.update(fagsakRepository.findByIdOrThrow(fagsak.id).copy(fagsystem = Fagsystem.EF)) + + val kravgrunnlagxml = readXml("/kravgrunnlagxml/kravgrunnlag_EF_med_skatt_avrunding.xml") + val kravgrunnlag = KravgrunnlagMapper.tilKravgrunnlag431( + KravgrunnlagUtil.unmarshalKravgrunnlag(kravgrunnlagxml), + behandling.id, + ) + kravgrunnlagRepository.insert(kravgrunnlag) + + val sortedPerioder = kravgrunnlag.perioder.map { it.periode }.sortedBy { it.fom } + + val aktsomhetPeriode = VilkårsvurderingsperiodeDto( + periode = Datoperiode( + sortedPerioder[0].fom, + sortedPerioder[1].tom, + ), + begrunnelse = "testverdi", + aktsomhetDto = AktsomhetDto( + aktsomhet = Aktsomhet.SIMPEL_UAKTSOMHET, + begrunnelse = "simpel uaktsomhet begrunnelse", + tilbakekrevSmåbeløp = true, + særligeGrunnerBegrunnelse = "test", + særligeGrunnerTilReduksjon = true, + særligeGrunner = listOf( + SærligGrunnDto( + HELT_ELLER_DELVIS_NAVS_FEIL, + ), + ), + andelTilbakekreves = BigDecimal(50), + ), + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + ) + + vilkårsvurderingService.lagreVilkårsvurdering(behandling.id, BehandlingsstegVilkårsvurderingDto(listOf(aktsomhetPeriode))) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 2 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe sortedPerioder[0] + førstePeriode.renter shouldBe BigDecimal(0) + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(1755), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(18195), + utbetaltBeløp = BigDecimal(19950), + tilbakekrevesBeløp = BigDecimal(877), + uinnkrevdBeløp = BigDecimal(878), + skattBeløp = BigDecimal(385), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe sortedPerioder[1] + andrePeriode.renter shouldBe BigDecimal(0) + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(1755), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + nyttBeløp = BigDecimal(18195), + utbetaltBeløp = BigDecimal(19950), + tilbakekrevesBeløp = BigDecimal(878), + uinnkrevdBeløp = BigDecimal(877), + skattBeløp = BigDecimal(439), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne EF perioder med delvis tilbakekreving og skatt avrunding`() { + kravgrunnlagRepository.deleteById(kravgrunnlag.id) + fagsakRepository.update(fagsakRepository.findByIdOrThrow(fagsak.id).copy(fagsystem = Fagsystem.EF)) + + val kravgrunnlagxml = readXml("/kravgrunnlagxml/kravgrunnlag_EF_med_skatt_avrunding_3.xml") + val kravgrunnlag = KravgrunnlagMapper.tilKravgrunnlag431( + KravgrunnlagUtil.unmarshalKravgrunnlag(kravgrunnlagxml), + behandling.id, + ) + kravgrunnlagRepository.insert(kravgrunnlag) + + val sortedPerioder = kravgrunnlag.perioder.map { it.periode }.sortedBy { it.fom } + + val aktsomhetPeriode = VilkårsvurderingsperiodeDto( + periode = Datoperiode( + sortedPerioder[0].fom, + sortedPerioder[2].tom, + ), + begrunnelse = "testverdi", + aktsomhetDto = AktsomhetDto( + aktsomhet = Aktsomhet.SIMPEL_UAKTSOMHET, + begrunnelse = "simpel uaktsomhet begrunnelse", + tilbakekrevSmåbeløp = true, + særligeGrunnerBegrunnelse = "test", + særligeGrunnerTilReduksjon = false, + særligeGrunner = listOf(SærligGrunnDto(GRAD_AV_UAKTSOMHET), SærligGrunnDto(HELT_ELLER_DELVIS_NAVS_FEIL)), + andelTilbakekreves = BigDecimal(100), + ), + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + ) + + val aktsomhetPeriode1 = VilkårsvurderingsperiodeDto( + periode = Datoperiode( + sortedPerioder[3].fom, + sortedPerioder[3].tom, + ), + begrunnelse = "testverdi", + aktsomhetDto = AktsomhetDto( + aktsomhet = Aktsomhet.SIMPEL_UAKTSOMHET, + begrunnelse = "simpel uaktsomhet begrunnelse", + tilbakekrevSmåbeløp = true, + særligeGrunnerBegrunnelse = "test", + særligeGrunnerTilReduksjon = true, + særligeGrunner = listOf(SærligGrunnDto(HELT_ELLER_DELVIS_NAVS_FEIL)), + andelTilbakekreves = BigDecimal(68), + ), + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + ) + + vilkårsvurderingService.lagreVilkårsvurdering( + behandling.id, + BehandlingsstegVilkårsvurderingDto(listOf(aktsomhetPeriode, aktsomhetPeriode1)), + ) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 4 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe sortedPerioder[0] + førstePeriode.renter shouldBe BigDecimal(0) + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(637), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(8782), + tilbakekrevesBeløp = BigDecimal(637), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(216), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe sortedPerioder[1] + andrePeriode.renter shouldBe BigDecimal(0) + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(1087), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(8782), + tilbakekrevesBeløp = BigDecimal(1087), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(369), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val tredjePeriode = tilbakekrevingsperioder[2] + tredjePeriode.periode shouldBe sortedPerioder[2] + tredjePeriode.renter shouldBe BigDecimal(0) + feilPostering = tredjePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(2250), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = tredjePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(8782), + tilbakekrevesBeløp = BigDecimal(2250), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(382), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val fjerdePeriode = tilbakekrevingsperioder[3] + fjerdePeriode.periode shouldBe sortedPerioder[3] + fjerdePeriode.renter shouldBe BigDecimal(0) + feilPostering = fjerdePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(8782), kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING) + ytelsePostering = fjerdePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(8782), + tilbakekrevesBeløp = BigDecimal(5972), + uinnkrevdBeløp = BigDecimal(2810), + skattBeløp = BigDecimal(2029), + kodeResultat = KodeResultat.DELVIS_TILBAKEKREVING, + ) + } + + @Test + fun `beregnVedtaksperioder skal beregne EF perioder med full tilbakekreving og skatt avrunding`() { + kravgrunnlagRepository.deleteById(kravgrunnlag.id) + fagsakRepository.update(fagsakRepository.findByIdOrThrow(fagsak.id).copy(fagsystem = Fagsystem.EF)) + + val kravgrunnlagxml = readXml("/kravgrunnlagxml/kravgrunnlag_EF_med_skatt_avrunding_2.xml") + val kravgrunnlag = KravgrunnlagMapper.tilKravgrunnlag431( + KravgrunnlagUtil.unmarshalKravgrunnlag(kravgrunnlagxml), + behandling.id, + ) + kravgrunnlagRepository.insert(kravgrunnlag) + + val sortedPerioder = kravgrunnlag.perioder.map { it.periode }.sortedBy { it.fom } + + val aktsomhetPeriode = VilkårsvurderingsperiodeDto( + periode = Datoperiode( + sortedPerioder[0].fom, + sortedPerioder[3].tom, + ), + begrunnelse = "testverdi", + aktsomhetDto = AktsomhetDto( + aktsomhet = Aktsomhet.SIMPEL_UAKTSOMHET, + begrunnelse = "simpel uaktsomhet begrunnelse", + tilbakekrevSmåbeløp = true, + særligeGrunnerBegrunnelse = "test", + særligeGrunnerTilReduksjon = true, + særligeGrunner = listOf( + SærligGrunnDto( + HELT_ELLER_DELVIS_NAVS_FEIL, + ), + ), + andelTilbakekreves = BigDecimal(100), + ), + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FEIL_OPPLYSNINGER_FRA_BRUKER, + ) + + vilkårsvurderingService.lagreVilkårsvurdering(behandling.id, BehandlingsstegVilkårsvurderingDto(listOf(aktsomhetPeriode))) + + val tilbakekrevingsperioder = vedtakBeregningService.beregnVedtaksperioder(behandling.id, kravgrunnlag) + .sortedBy { it.periode.fom } + tilbakekrevingsperioder.shouldNotBeNull() + tilbakekrevingsperioder.size shouldBe 4 + shouldNotThrowAny { iverksettelseService.validerBeløp(behandling.id, tilbakekrevingsperioder) } + + val førstePeriode = tilbakekrevingsperioder[0] + førstePeriode.periode shouldBe sortedPerioder[0] + førstePeriode.renter shouldBe BigDecimal(0) + var feilPostering = førstePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(2962), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + var ytelsePostering = førstePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(12607), + tilbakekrevesBeløp = BigDecimal(2962), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(429), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val andrePeriode = tilbakekrevingsperioder[1] + andrePeriode.periode shouldBe sortedPerioder[1] + andrePeriode.renter shouldBe BigDecimal(0) + feilPostering = andrePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(1725), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = andrePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(12607), + tilbakekrevesBeløp = BigDecimal(1725), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(431), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val tredjePeriode = tilbakekrevingsperioder[2] + tredjePeriode.periode shouldBe sortedPerioder[2] + tredjePeriode.renter shouldBe BigDecimal(0) + feilPostering = tredjePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(1050), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = tredjePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(12607), + tilbakekrevesBeløp = BigDecimal(1050), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(262), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + + val fjerdePeriode = tilbakekrevingsperioder[3] + fjerdePeriode.periode shouldBe sortedPerioder[3] + fjerdePeriode.renter shouldBe BigDecimal(0) + feilPostering = fjerdePeriode.beløp.first { Klassetype.FEIL == it.klassetype } + assertBeløp(beløp = feilPostering, nyttBeløp = BigDecimal(150), kodeResultat = KodeResultat.FULL_TILBAKEKREVING) + ytelsePostering = fjerdePeriode.beløp.first { Klassetype.YTEL == it.klassetype } + assertBeløp( + beløp = ytelsePostering, + utbetaltBeløp = BigDecimal(12607), + tilbakekrevesBeløp = BigDecimal(150), + uinnkrevdBeløp = BigDecimal.ZERO, + skattBeløp = BigDecimal(37), + kodeResultat = KodeResultat.FULL_TILBAKEKREVING, + ) + } + + private fun lagForeldelse(perioder: List) { + val foreldelsesdata = BehandlingsstegForeldelseDto( + perioder.map { + ForeldelsesperiodeDto( + periode = it.toDatoperiode(), + begrunnelse = "testverdi", + foreldelsesvurderingstype = + Foreldelsesvurderingstype.FORELDET, + ) + }, + ) + foreldelsesService.lagreVurdertForeldelse(behandling.id, foreldelsesdata) + } + + private fun lagAktsomhetVilkårsvurdering( + perioder: List, + aktsomhet: Aktsomhet, + andelTilbakreves: BigDecimal? = null, + beløpTilbakekreves: BigDecimal? = null, + særligeGrunnerTilReduksjon: Boolean = false, + ) { + val vilkårsperioder = perioder.map { + VilkårsvurderingsperiodeDto( + periode = it.toDatoperiode(), + begrunnelse = "testverdi", + aktsomhetDto = AktsomhetDto( + aktsomhet = aktsomhet, + andelTilbakekreves = andelTilbakreves, + beløpTilbakekreves = beløpTilbakekreves, + begrunnelse = "testverdi", + særligeGrunnerTilReduksjon = særligeGrunnerTilReduksjon, + tilbakekrevSmåbeløp = true, + særligeGrunnerBegrunnelse = "testverdi", + særligeGrunner = listOf( + SærligGrunnDto( + ANNET, + "testverdi", + ), + ), + ), + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + ) + } + vilkårsvurderingService.lagreVilkårsvurdering(behandling.id, BehandlingsstegVilkårsvurderingDto(vilkårsperioder)) + } + + private fun lagGodTroVilkårsvurdering( + perioder: List, + beløpErIBehold: Boolean = false, + beløpTilbakekreves: BigDecimal? = null, + ) { + val vilkårsperioder = perioder.map { + VilkårsvurderingsperiodeDto( + periode = it.toDatoperiode(), + begrunnelse = "testverdi", + godTroDto = GodTroDto( + begrunnelse = "testverdi", + beløpErIBehold = beløpErIBehold, + beløpTilbakekreves = beløpTilbakekreves, + ), + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + ) + } + vilkårsvurderingService.lagreVilkårsvurdering(behandling.id, BehandlingsstegVilkårsvurderingDto(vilkårsperioder)) + } + + private fun lagKravgrunnlag( + perioder: List, + månedligSkattBeløp: BigDecimal, + kravgrunnlagsbeløpene: List, + ): Kravgrunnlag431 { + return Kravgrunnlag431( + behandlingId = behandling.id, + vedtakId = BigInteger.ZERO, + kravstatuskode = Kravstatuskode.NYTT, + fagområdekode = Fagområdekode.BA, + fagsystemId = fagsak.eksternFagsakId, + gjelderVedtakId = "testverdi", + gjelderType = GjelderType.PERSON, + utbetalesTilId = "testverdi", + utbetIdType = GjelderType.PERSON, + ansvarligEnhet = "testverdi", + bostedsenhet = "testverdi", + behandlingsenhet = "testverdi", + kontrollfelt = "testverdi", + referanse = behandling.aktivFagsystemsbehandling.eksternId, + eksternKravgrunnlagId = BigInteger.ZERO, + saksbehandlerId = "testverdi", + perioder = lagKravgrunnlagsperiode(perioder, månedligSkattBeløp, kravgrunnlagsbeløpene), + ) + } + + private fun lagKravgrunnlagsperiode( + perioder: List, + månedligSkattBeløp: BigDecimal, + kravgrunnlagsbeløpene: List, + ): Set { + return perioder.map { + Kravgrunnlagsperiode432( + periode = it, + månedligSkattebeløp = månedligSkattBeløp, + beløp = lagKravgrunnlagsbeløp(kravgrunnlagsbeløpene), + ) + }.toSet() + } + + private fun lagKravgrunnlagsbeløp(kravgrunnlagsbeløpene: List): Set { + return kravgrunnlagsbeløpene.map { + Kravgrunnlagsbeløp433( + klassekode = Klassekode.BATR, + klassetype = it.klassetype, + opprinneligUtbetalingsbeløp = it.opprinneligUtbetalingsbeløp, + nyttBeløp = it.nyttBeløp, + tilbakekrevesBeløp = it.tilbakekrevesBeløp, + uinnkrevdBeløp = it.uinnkrevdBeløp, + skatteprosent = it.skatteprosent, + ) + }.toSet() + } + + private fun assertBeløp( + beløp: Tilbakekrevingsbeløp, + nyttBeløp: BigDecimal = BigDecimal.ZERO, + utbetaltBeløp: BigDecimal = BigDecimal.ZERO, + tilbakekrevesBeløp: BigDecimal = BigDecimal.ZERO, + uinnkrevdBeløp: BigDecimal = BigDecimal.ZERO, + skattBeløp: BigDecimal = BigDecimal.ZERO, + kodeResultat: KodeResultat, + ) { + beløp.nyttBeløp shouldBe nyttBeløp + beløp.utbetaltBeløp shouldBe utbetaltBeløp + beløp.utbetaltBeløp shouldBe utbetaltBeløp + beløp.tilbakekrevesBeløp shouldBe tilbakekrevesBeløp + beløp.uinnkrevdBeløp shouldBe uinnkrevdBeløp + beløp.skattBeløp shouldBe skattBeløp + beløp.kodeResultat shouldBe kodeResultat + } + + internal data class Kravgrunnlagsbeløp( + val klassetype: Klassetype, + val opprinneligUtbetalingsbeløp: BigDecimal = BigDecimal.ZERO, + val nyttBeløp: BigDecimal = BigDecimal.ZERO, + val tilbakekrevesBeløp: BigDecimal = BigDecimal.ZERO, + val uinnkrevdBeløp: BigDecimal = BigDecimal.ZERO, + val skatteprosent: BigDecimal = BigDecimal.ZERO, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/Vilk\303\245rsvurderingsPeriodeDomainUtil.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/Vilk\303\245rsvurderingsPeriodeDomainUtil.kt" new file mode 100644 index 000000000..cf84bcc89 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/Vilk\303\245rsvurderingsPeriodeDomainUtil.kt" @@ -0,0 +1,31 @@ +package no.nav.familie.tilbake.iverksettvedtak + +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.tilbake.api.dto.AktsomhetDto +import no.nav.familie.tilbake.api.dto.SærligGrunnDto +import no.nav.familie.tilbake.api.dto.VilkårsvurderingsperiodeDto +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import java.time.YearMonth + +object VilkårsvurderingsPeriodeDomainUtil { + + fun lagGrovtUaktsomVilkårsvurderingsperiode(fom: YearMonth, tom: YearMonth) = VilkårsvurderingsperiodeDto( + periode = Datoperiode(fom, tom), + begrunnelse = "testverdi", + aktsomhetDto = + AktsomhetDto( + aktsomhet = Aktsomhet.GROV_UAKTSOMHET, + ileggRenter = true, + andelTilbakekreves = null, + begrunnelse = "testverdi", + særligeGrunnerTilReduksjon = false, + tilbakekrevSmåbeløp = true, + særligeGrunnerBegrunnelse = "testverdi", + særligeGrunner = listOf(SærligGrunnDto(SærligGrunn.ANNET, "testverdi")), + ), + vilkårsvurderingsresultat = + Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + ) +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/\303\230konomiXmlSendtRepositoryTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/\303\230konomiXmlSendtRepositoryTest.kt" new file mode 100644 index 000000000..0170761cc --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/iverksettvedtak/\303\230konomiXmlSendtRepositoryTest.kt" @@ -0,0 +1,87 @@ +package no.nav.familie.tilbake.iverksettvedtak + +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.iverksettvedtak.domain.ØkonomiXmlSendt +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +internal class ØkonomiXmlSendtRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var økonomiXmlSendtRepository: ØkonomiXmlSendtRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + private val økonomiXmlSendt = Testdata.økonomiXmlSendt + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(Testdata.behandling) + } + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av ØkonomiXmlSendt til basen`() { + økonomiXmlSendtRepository.insert(økonomiXmlSendt) + + val lagretØkonomiXmlSendt = økonomiXmlSendtRepository.findByIdOrThrow(økonomiXmlSendt.id) + + lagretØkonomiXmlSendt.shouldBeEqualToComparingFieldsExcept( + økonomiXmlSendt, + ØkonomiXmlSendt::sporbar, + ØkonomiXmlSendt::versjon, + ) + lagretØkonomiXmlSendt.versjon shouldBe 1 + } + + @Test + fun `findByMeldingstypeAndSporbarOpprettetTidAfter skal finne forekomster hvis det finnes for søkekriterier`() { + økonomiXmlSendtRepository.insert(økonomiXmlSendt) + + val lagretØkonomiXmlSendt = + økonomiXmlSendtRepository.findByOpprettetPåDato(LocalDate.now()) + + lagretØkonomiXmlSendt.shouldNotBeEmpty() + } + + @Test + fun `findByMeldingstypeAndSporbarOpprettetTidAfter skal ikke finne forekomster hvis det ikke finnes for søkekriterier`() { + økonomiXmlSendtRepository.insert(økonomiXmlSendt) + + val lagretØkonomiXmlSendt = + økonomiXmlSendtRepository.findByOpprettetPåDato(LocalDate.now().plusDays(1)) + + lagretØkonomiXmlSendt.shouldBeEmpty() + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av ØkonomiXmlSendt i basen`() { + økonomiXmlSendtRepository.insert(økonomiXmlSendt) + var lagretØkonomiXmlSendt = økonomiXmlSendtRepository.findByIdOrThrow(økonomiXmlSendt.id) + val oppdatertØkonomiXmlSendt = lagretØkonomiXmlSendt.copy(melding = "bob") + + økonomiXmlSendtRepository.update(oppdatertØkonomiXmlSendt) + + lagretØkonomiXmlSendt = økonomiXmlSendtRepository.findByIdOrThrow(økonomiXmlSendt.id) + lagretØkonomiXmlSendt.shouldBeEqualToComparingFieldsExcept( + oppdatertØkonomiXmlSendt, + ØkonomiXmlSendt::sporbar, + ØkonomiXmlSendt::versjon, + ) + lagretØkonomiXmlSendt.versjon shouldBe 2 + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/BehandleKravgrunnlagTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/BehandleKravgrunnlagTaskTest.kt new file mode 100644 index 000000000..d7c9a1728 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/BehandleKravgrunnlagTaskTest.kt @@ -0,0 +1,837 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.BehandlingsstegFaktaDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegForeldelseDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegForeslåVedtaksstegDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegVilkårsvurderingDto +import no.nav.familie.tilbake.api.dto.FaktaFeilutbetalingsperiodeDto +import no.nav.familie.tilbake.api.dto.ForeldelsesperiodeDto +import no.nav.familie.tilbake.api.dto.FritekstavsnittDto +import no.nav.familie.tilbake.api.dto.GodTroDto +import no.nav.familie.tilbake.api.dto.VilkårsvurderingsperiodeDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Fagsystemsbehandling +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.behandling.task.OppdaterFaktainfoTask +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.Behandlingsstegsinfo +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.vedtak.VedtaksbrevsoppsummeringRepository +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingRepository +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.VurdertForeldelseRepository +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.historikkinnslag.LagHistorikkinnslagTask +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottatt +import no.nav.familie.tilbake.kravgrunnlag.task.BehandleKravgrunnlagTask +import no.nav.familie.tilbake.oppgave.OppdaterOppgaveTask +import no.nav.familie.tilbake.oppgave.OppdaterPrioritetTask +import no.nav.familie.tilbake.vilkårsvurdering.VilkårsvurderingRepository +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.math.BigInteger +import java.time.LocalDate +import java.time.YearMonth +import java.util.UUID + +internal class BehandleKravgrunnlagTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var mottattXmlRepository: ØkonomiXmlMottattRepository + + @Autowired + private lateinit var mottattXmlArkivRepository: ØkonomiXmlMottattArkivRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var faktaFeilutbetalingRepository: FaktaFeilutbetalingRepository + + @Autowired + private lateinit var foreldelseRepository: VurdertForeldelseRepository + + @Autowired + private lateinit var vilkårsvurderingRepository: VilkårsvurderingRepository + + @Autowired + private lateinit var vedtaksbrevsoppsummeringRepository: VedtaksbrevsoppsummeringRepository + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + @Autowired + private lateinit var stegService: StegService + + @Autowired + private lateinit var behandleKravgrunnlagTask: BehandleKravgrunnlagTask + + private val fagsak = Testdata.fagsak + private val behandling = Testdata.behandling + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(behandling) + } + + @Test + fun `doTask skal lagre mottatt kravgrunnlag i Kravgrunnlag431 når behandling finnes`() { + lagGrunnlagssteg() + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + val task = opprettTask(kravgrunnlagXml) + + behandleKravgrunnlagTask.doTask(task) + + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + kravgrunnlag.shouldNotBeNull() + kravgrunnlag.kravstatuskode shouldBe Kravstatuskode.NYTT + kravgrunnlag.fagsystemId shouldBe fagsak.eksternFagsakId + KravgrunnlagUtil.tilYtelsestype(kravgrunnlag.fagområdekode.name) shouldBe Ytelsestype.BARNETRYGD + + assertPerioder(kravgrunnlag) + assertBeløp(kravgrunnlag) + + mottattXmlRepository.findByEksternKravgrunnlagIdAndVedtakId( + kravgrunnlag.eksternKravgrunnlagId, + kravgrunnlag.vedtakId, + ).shouldBeEmpty() + mottattXmlArkivRepository.findAll().toList().shouldNotBeEmpty() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT, Aktør.VEDTAKSLØSNING) + assertOppgaveTask( + "Behandling er tatt av vent, " + + "men revurderingsvedtaksdato er mindre enn 10 dager fra dagens dato." + + "Fristen settes derfor 10 dager fra revurderingsvedtaksdato " + + "for å sikre at behandlingen har mottatt oppdatert kravgrunnlag", + behandling.aktivFagsystemsbehandling.revurderingsvedtaksdato.plusDays(10), + ) + assertIkkeOpprettOppdaterPrioritetTask() + } + + @Test + fun `doTask skal lagre mottatt kravgrunnlag i Kravgrunnlag431 når behandling finnes med revurdering gamle enn 10 dager`() { + val fagsystemsbehandling = Fagsystemsbehandling( + eksternId = UUID.randomUUID().toString(), + tilbakekrevingsvalg = Tilbakekrevingsvalg + .OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + revurderingsvedtaksdato = LocalDate.now().minusDays(10), + resultat = "OPPHØR", + årsak = "testverdi", + ) + behandlingRepository.update( + behandlingRepository.findByIdOrThrow(behandling.id) + .copy(fagsystemsbehandling = setOf(fagsystemsbehandling)), + ) + lagGrunnlagssteg() + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + val task = opprettTask(kravgrunnlagXml) + + behandleKravgrunnlagTask.doTask(task) + + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + kravgrunnlag.shouldNotBeNull() + kravgrunnlag.kravstatuskode shouldBe Kravstatuskode.NYTT + kravgrunnlag.fagsystemId shouldBe fagsak.eksternFagsakId + KravgrunnlagUtil.tilYtelsestype(kravgrunnlag.fagområdekode.name) shouldBe Ytelsestype.BARNETRYGD + + assertPerioder(kravgrunnlag) + assertBeløp(kravgrunnlag) + + mottattXmlRepository.findByEksternKravgrunnlagIdAndVedtakId( + kravgrunnlag.eksternKravgrunnlagId, + kravgrunnlag.vedtakId, + ).shouldBeEmpty() + mottattXmlArkivRepository.findAll().toList().shouldNotBeEmpty() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT, Aktør.VEDTAKSLØSNING) + assertOppgaveTask( + "Behandling er tatt av vent, pga mottatt kravgrunnlag", + LocalDate.now().plusDays(1), + ) + assertIkkeOpprettOppdaterPrioritetTask() + } + + @Test + fun `doTask skal lagre mottatt kravgrunnlag i Kravgrunnlag431 mens behandling venter på brukerstilbakemelding`() { + behandlingsstegstilstandRepository.insert( + Behandlingsstegstilstand( + behandlingId = behandling.id, + behandlingssteg = Behandlingssteg.VARSEL, + behandlingsstegsstatus = Behandlingsstegstatus.VENTER, + venteårsak = Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + tidsfrist = LocalDate.now().plusWeeks(3), + ), + ) + + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + val task = opprettTask(kravgrunnlagXml) + + behandleKravgrunnlagTask.doTask(task) + + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + kravgrunnlag.shouldNotBeNull() + kravgrunnlag.kravstatuskode shouldBe Kravstatuskode.NYTT + kravgrunnlag.fagsystemId shouldBe fagsak.eksternFagsakId + KravgrunnlagUtil.tilYtelsestype(kravgrunnlag.fagområdekode.name) shouldBe Ytelsestype.BARNETRYGD + + assertPerioder(kravgrunnlag) + assertBeløp(kravgrunnlag) + + mottattXmlRepository.findByEksternKravgrunnlagIdAndVedtakId( + kravgrunnlag.eksternKravgrunnlagId, + kravgrunnlag.vedtakId, + ).shouldBeEmpty() + mottattXmlArkivRepository.findAll().toList().shouldNotBeEmpty() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + // behandling venter fortsatt på brukerstilbakemelding, oppretter ikke grunnlagssteg + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.VARSEL, Behandlingsstegstatus.VENTER) + behandlingsstegstilstand.any { it.behandlingssteg == Behandlingssteg.GRUNNLAG }.shouldBeFalse() + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT, Aktør.VEDTAKSLØSNING) + taskService.findAll().none { it.type == OppdaterOppgaveTask.TYPE }.shouldBeTrue() + assertOpprettOppdaterPrioritetTask() + } + + @Test + fun `doTask skal lagre mottatt kravgrunnlag i Kravgrunnlag431 med en YTEL postering som er større enn differansen`() { + behandlingsstegstilstandRepository.insert( + Behandlingsstegstilstand( + behandlingId = behandling.id, + behandlingssteg = Behandlingssteg.VARSEL, + behandlingsstegsstatus = Behandlingsstegstatus.VENTER, + venteårsak = Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + tidsfrist = LocalDate.now().plusWeeks(3), + ), + ) + + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_belop_storre_enn_diff.xml") + val task = opprettTask(kravgrunnlagXml) + + behandleKravgrunnlagTask.doTask(task) + + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + kravgrunnlag.shouldNotBeNull() + kravgrunnlag.kravstatuskode shouldBe Kravstatuskode.NYTT + kravgrunnlag.fagsystemId shouldBe fagsak.eksternFagsakId + KravgrunnlagUtil.tilYtelsestype(kravgrunnlag.fagområdekode.name) shouldBe Ytelsestype.BARNETRYGD + + assertPerioder(kravgrunnlag) + + val kravgrunnlagsbeløp = kravgrunnlag.perioder.toList()[0].beløp + kravgrunnlagsbeløp.size shouldBe 3 + kravgrunnlagsbeløp.any { Klassetype.YTEL == it.klassetype }.shouldBeTrue() + kravgrunnlagsbeløp.any { Klassetype.FEIL == it.klassetype }.shouldBeTrue() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + // behandling venter fortsatt på brukerstilbakemelding, oppretter ikke grunnlagssteg + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.VARSEL, Behandlingsstegstatus.VENTER) + behandlingsstegstilstand.any { it.behandlingssteg == Behandlingssteg.GRUNNLAG }.shouldBeFalse() + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT, Aktør.VEDTAKSLØSNING) + taskService.findAll().none { it.type == OppdaterOppgaveTask.TYPE }.shouldBeTrue() + assertOpprettOppdaterPrioritetTask() + } + + @Test + fun `doTask skal lagre mottatt ENDR kravgrunnlag i Kravgrunnlag431 når behandling finnes`() { + lagGrunnlagssteg() + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) + + behandlingskontrollService + .tilbakehoppBehandlingssteg( + behandlingId = behandling.id, + behandlingsstegsinfo = + Behandlingsstegsinfo( + Behandlingssteg.GRUNNLAG, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + LocalDate.now().plusWeeks(4), + ), + ) + val endretKravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_ENDR.xml") + + behandleKravgrunnlagTask.doTask(opprettTask(endretKravgrunnlagXml)) + + val alleKravgrunnlag = kravgrunnlagRepository.findByBehandlingId(behandling.id) + alleKravgrunnlag.size shouldBe 2 + alleKravgrunnlag.any { Kravstatuskode.NYTT == it.kravstatuskode && !it.aktiv }.shouldBeTrue() + + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + kravgrunnlag.shouldNotBeNull() + kravgrunnlag.kravstatuskode shouldBe Kravstatuskode.ENDRET + kravgrunnlag.fagsystemId shouldBe fagsak.eksternFagsakId + KravgrunnlagUtil.tilYtelsestype(kravgrunnlag.fagområdekode.name) shouldBe Ytelsestype.BARNETRYGD + + assertPerioder(kravgrunnlag) + assertBeløp(kravgrunnlag) + + mottattXmlRepository.findByEksternKravgrunnlagIdAndVedtakId( + kravgrunnlag.eksternKravgrunnlagId, + kravgrunnlag.vedtakId, + ).shouldBeEmpty() + mottattXmlArkivRepository.findAll().toList().shouldNotBeEmpty() + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT, Aktør.VEDTAKSLØSNING) + assertOppdaterFaktainfoTask(kravgrunnlag.referanse) + assertOppgaveTask( + "Behandling er tatt av vent, " + + "men revurderingsvedtaksdato er mindre enn 10 dager fra dagens dato." + + "Fristen settes derfor 10 dager fra revurderingsvedtaksdato " + + "for å sikre at behandlingen har mottatt oppdatert kravgrunnlag", + behandling.aktivFagsystemsbehandling.revurderingsvedtaksdato.plusDays(10), + ) + assertIkkeOpprettOppdaterPrioritetTask() + } + + @Test + fun `doTask skal lagre mottatt ENDR kravgrunnlag og slette behandlet data når behandling er på vilkårsvurdering steg`() { + lagGrunnlagssteg() + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) + // Håndter fakta steg + stegService.håndterSteg( + behandling.id, + BehandlingsstegFaktaDto( + listOf( + FaktaFeilutbetalingsperiodeDto( + Datoperiode( + YearMonth.of(2020, 8), + YearMonth.of(2020, 8), + ), + Hendelsestype.ANNET, + Hendelsesundertype.ANNET_FRITEKST, + ), + ), + "Fakta begrunnelse", + ), + ) + // Håndter foreldelse steg + stegService.håndterSteg( + behandling.id, + BehandlingsstegForeldelseDto( + listOf( + ForeldelsesperiodeDto( + Datoperiode( + YearMonth.of(2020, 8), + YearMonth.of(2020, 8), + ), + "Foreldelse begrunnelse", + Foreldelsesvurderingstype + .IKKE_FORELDET, + ), + ), + ), + ) + + faktaFeilutbetalingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldNotBeNull() + foreldelseRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldNotBeNull() + + var behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.FORELDELSE, + Behandlingsstegstatus.UTFØRT, + ) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.VILKÅRSVURDERING, + Behandlingsstegstatus.KLAR, + ) + + val endretKravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_ENDR.xml") + behandleKravgrunnlagTask.doTask(opprettTask(endretKravgrunnlagXml)) + + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + kravgrunnlag.shouldNotBeNull() + kravgrunnlag.kravstatuskode shouldBe Kravstatuskode.ENDRET + kravgrunnlag.fagsystemId shouldBe fagsak.eksternFagsakId + KravgrunnlagUtil.tilYtelsestype(kravgrunnlag.fagområdekode.name) shouldBe Ytelsestype.BARNETRYGD + + assertPerioder(kravgrunnlag) + assertBeløp(kravgrunnlag) + + mottattXmlRepository.findByEksternKravgrunnlagIdAndVedtakId( + kravgrunnlag.eksternKravgrunnlagId, + kravgrunnlag.vedtakId, + ).shouldBeEmpty() + mottattXmlArkivRepository.findAll().toList().shouldNotBeEmpty() + + behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.FORELDELSE, + Behandlingsstegstatus.TILBAKEFØRT, + ) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.VILKÅRSVURDERING, + Behandlingsstegstatus.TILBAKEFØRT, + ) + + faktaFeilutbetalingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldBeNull() + foreldelseRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldBeNull() + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT, Aktør.VEDTAKSLØSNING) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.FAKTA_VURDERT, Aktør.SAKSBEHANDLER) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.FORELDELSE_VURDERT, Aktør.SAKSBEHANDLER) + + assertOppdaterFaktainfoTask(kravgrunnlag.referanse) + assertOpprettOppdaterPrioritetTask() + } + + @Test + fun `doTask skal lagre mottatt ENDR kravgrunnlag og slette behandlet data når behandling er på fatte vedtak steg`() { + lagGrunnlagssteg() + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) + + val periode = Datoperiode(YearMonth.of(2020, 8), YearMonth.of(2020, 8)) + // Håndter fakta steg + stegService.håndterSteg( + behandling.id, + BehandlingsstegFaktaDto( + listOf( + FaktaFeilutbetalingsperiodeDto( + periode, + Hendelsestype.BARNS_ALDER, + Hendelsesundertype.BARN_DØD, + ), + ), + "Fakta begrunnelse", + ), + ) + // Håndter foreldelse steg + stegService.håndterSteg( + behandling.id, + BehandlingsstegForeldelseDto( + listOf( + ForeldelsesperiodeDto( + periode, + "Foreldelse begrunnelse", + Foreldelsesvurderingstype + .IKKE_FORELDET, + ), + ), + ), + ) + + // Håndter Vilkårsvurdering steg + val periodeMedGodTro = VilkårsvurderingsperiodeDto( + periode, + Vilkårsvurderingsresultat.GOD_TRO, + "Vilkårs begrunnelse", + GodTroDto(begrunnelse = "god tro", beløpErIBehold = false), + ) + stegService.håndterSteg( + behandling.id, + BehandlingsstegVilkårsvurderingDto(listOf(periodeMedGodTro)), + ) + + // Håndter Foreslå Vedtak steg + stegService.håndterSteg( + behandling.id, + BehandlingsstegForeslåVedtaksstegDto( + FritekstavsnittDto( + "oppsummeringstekst", + emptyList(), + ), + ), + ) + + faktaFeilutbetalingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldNotBeNull() + foreldelseRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldNotBeNull() + vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldNotBeNull() + vedtaksbrevsoppsummeringRepository.findByBehandlingId(behandling.id).shouldNotBeNull() + + var behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FORELDELSE, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + + val endretKravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_ENDR.xml") + behandleKravgrunnlagTask.doTask(opprettTask(endretKravgrunnlagXml)) + + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + kravgrunnlag.shouldNotBeNull() + kravgrunnlag.kravstatuskode shouldBe Kravstatuskode.ENDRET + kravgrunnlag.fagsystemId shouldBe fagsak.eksternFagsakId + KravgrunnlagUtil.tilYtelsestype(kravgrunnlag.fagområdekode.name) shouldBe Ytelsestype.BARNETRYGD + + assertPerioder(kravgrunnlag) + assertBeløp(kravgrunnlag) + + mottattXmlRepository.findByEksternKravgrunnlagIdAndVedtakId( + kravgrunnlag.eksternKravgrunnlagId, + kravgrunnlag.vedtakId, + ).shouldBeEmpty() + mottattXmlArkivRepository.findAll().toList().shouldNotBeEmpty() + + behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FORELDELSE, Behandlingsstegstatus.TILBAKEFØRT) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.VILKÅRSVURDERING, + Behandlingsstegstatus.TILBAKEFØRT, + ) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.FORESLÅ_VEDTAK, + Behandlingsstegstatus.TILBAKEFØRT, + ) + assertBehandlingsstegstilstand( + behandlingsstegstilstand, + Behandlingssteg.FATTE_VEDTAK, + Behandlingsstegstatus.TILBAKEFØRT, + ) + + faktaFeilutbetalingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldBeNull() + foreldelseRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldBeNull() + vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id).shouldBeNull() + vedtaksbrevsoppsummeringRepository.findByBehandlingId(behandling.id).shouldBeNull() + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT, Aktør.VEDTAKSLØSNING) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.FAKTA_VURDERT, Aktør.SAKSBEHANDLER) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.FORELDELSE_VURDERT, Aktør.SAKSBEHANDLER) + + assertOppdaterFaktainfoTask(kravgrunnlag.referanse) + assertOpprettOppdaterPrioritetTask() + } + + @Test + fun `doTask skal ikke lagre mottatt kravgrunnlag når mottatt xml er ugyldig`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_ugyldig_struktur.xml") + + val exception = shouldThrow { behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) } + exception.message shouldBe "Mottatt kravgrunnlagXML er ugyldig! Den feiler med jakarta.xml.bind.UnmarshalException\n" + + " - with linked exception:\n" + + "[org.xml.sax.SAXParseException; lineNumber: 21; columnNumber: 33; " + + "cvc-complex-type.2.4.b: The content of element 'urn:detaljertKravgrunnlag' " + + "is not complete. One of " + + "'{\"urn:no:nav:tilbakekreving:kravgrunnlag:detalj:v1\":tilbakekrevingsPeriode}'" + + " is expected.]" + } + + @Test + fun `doTask skal ikke lagre mottatt kravgrunnlag når mottatt xml ikke har referanse`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_tomt_referanse.xml") + + val exception = shouldThrow { behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) } + exception.message shouldBe "Ugyldig kravgrunnlag for kravgrunnlagId 0. Mangler referanse." + } + + @Test + fun `doTask skal ikke lagre mottatt kravgrunnlag når mottatt xml periode ikke er innenfor måned`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_periode_utenfor_kalendermåned.xml") + + val exception = shouldThrow { behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) } + exception.message shouldBe "Ugyldig kravgrunnlag for kravgrunnlagId 0. " + + "Perioden 2020-08-01-2020-09-30 er ikke innenfor en kalendermåned." + } + + @Test + fun `doTask skal ikke lagre mottatt kravgrunnlag når mottatt xml periode ikke starter første dag i måned`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_periode_starter_ikke_første_dag.xml") + + val exception = shouldThrow { behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) } + exception.message shouldBe "Ugyldig kravgrunnlag for kravgrunnlagId 0. " + + "Perioden 2020-08-15-2020-08-31 starter ikke første dag i måned." + } + + @Test + fun `doTask skal ikke lagre mottatt kravgrunnlag når mottatt xml periode ikke slutter siste dag i måned`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_periode_slutter_ikke_siste_dag.xml") + + val exception = shouldThrow { behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) } + exception.message shouldBe "Ugyldig kravgrunnlag for kravgrunnlagId 0. " + + "Perioden 2020-08-01-2020-08-28 slutter ikke siste dag i måned." + } + + @Test + fun `doTask skal ikke lagre mottatt kravgrunnlag når mottatt xml mangler FEIL postering`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_uten_FEIL_postering.xml") + + val exception = shouldThrow { behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) } + exception.message shouldBe "Ugyldig kravgrunnlag for kravgrunnlagId 0. " + + "Perioden 2020-08-01-2020-08-31 mangler postering med klassetype=FEIL." + } + + @Test + fun `doTask skal ikke lagre mottatt kravgrunnlag når mottatt xml mangler YTEL postering`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_uten_YTEL_postering.xml") + + val exception = shouldThrow { behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) } + exception.message shouldBe "Ugyldig kravgrunnlag for kravgrunnlagId 0. " + + "Perioden 2020-08-01-2020-08-31 mangler postering med klassetype=YTEL." + } + + @Test + fun `doTask skal ikke lagre mottatt kravgrunnlag når mottatt xml har overlappende perioder`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_overlappende_perioder.xml") + + val exception = shouldThrow { behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) } + exception.message shouldBe "Ugyldig kravgrunnlag for kravgrunnlagId 0. " + + "Overlappende perioder Månedsperiode(fom=2020-08, tom=2020-08) og Månedsperiode(fom=2020-08, tom=2020-08)." + } + + @Test + fun `doTask skal ikke lagre mottatt kravgrunnlag når xml har posteringsskatt som ikke matcher månedlig skatt beløp`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_posteringsskatt_matcher_ikke_med_månedlig_skatt_beløp.xml") + + val exception = shouldThrow { behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) } + exception.message shouldBe "Ugyldig kravgrunnlag for kravgrunnlagId 0. " + + "For måned 2020-08 er maks skatt 0.00, men maks tilbakekreving ganget med skattesats blir 210" + } + + @Test + fun `doTask skal ikke lagre mottatt kravgrunnlag når mottatt xml har FEIL postering med negativt beløp`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_FEIL_postering_med_negativ_beløp.xml") + + val exception = shouldThrow { behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) } + exception.message shouldBe "Ugyldig kravgrunnlag for kravgrunnlagId 0. " + + "Perioden 2020-08-01-2020-08-31 har FEIL postering med negativ beløp" + } + + @Test + fun `doTask skal ikke lagre mottatt kravgrunnlag når mottatt xml har ulike total tilbakekrevesbeløp og total nybeløp`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_ulike_total_tilbakekrevesbeløp_total_nybeløp.xml") + + val exception = shouldThrow { behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) } + exception.message shouldBe "Ugyldig kravgrunnlag for kravgrunnlagId 0. " + + "For perioden 2020-08-01-2020-08-31 " + + "total tilkakekrevesBeløp i YTEL posteringer er 1500.00, " + + "mens total nytt beløp i FEIL posteringer er 2108.00. " + + "Det er forventet at disse er like." + } + + @Test + fun `doTask skal ikke lagre mottatt kravgrunnlag når mottatt xml har YTEL postering som ikke matcher beregning`() { + val kravgrunnlagXml = readXml( + "/kravgrunnlagxml/" + + "kravgrunnlag_YTEL_postering_som_ikke_matcher_beregning.xml", + ) + + val exception = shouldThrow { behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) } + exception.message shouldBe "Ugyldig kravgrunnlag for kravgrunnlagId 0. " + + "Har en eller flere perioder med YTEL-postering med tilbakekrevesBeløp " + + "som er større enn differanse mellom nyttBeløp og opprinneligBeløp" + } + + @Test + fun `doTask skal lagre mottatt kravgrunnlag i oko xml mottatt når behandling ikke finnes`() { + val behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + val task = opprettTask(kravgrunnlagXml) + + behandleKravgrunnlagTask.doTask(task) + kravgrunnlagRepository.existsByBehandlingIdAndAktivTrueAndSperretFalse(behandling.id).shouldBeFalse() + + val mottattKravgrunnlagListe = mottattXmlRepository.findByEksternKravgrunnlagIdAndVedtakId( + BigInteger.ZERO, + BigInteger.ZERO, + ) + assertOkoXmlMottattData(mottattKravgrunnlagListe, kravgrunnlagXml, Kravstatuskode.NYTT, "0") + + mottattXmlArkivRepository.findAll().toList().shouldBeEmpty() + } + + @Test + fun `doTask skal lagre mottatt ENDR kravgrunnlag i oko xml mottatt når tabellen allerede har NYTT kravgrunnlag`() { + val behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + behandleKravgrunnlagTask.doTask(opprettTask(kravgrunnlagXml)) + val endretKravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_ENDR.xml") + + behandleKravgrunnlagTask.doTask(opprettTask(endretKravgrunnlagXml)) + + val mottattKravgrunnlagListe = mottattXmlRepository.findByEksternKravgrunnlagIdAndVedtakId( + BigInteger.ZERO, + BigInteger.ZERO, + ) + assertOkoXmlMottattData(mottattKravgrunnlagListe, endretKravgrunnlagXml, Kravstatuskode.ENDRET, "1") + + mottattXmlArkivRepository.findAll().toList().shouldNotBeEmpty() + } + + private fun opprettTask(kravgrunnlagXml: String): Task { + return taskService.save( + Task( + type = BehandleKravgrunnlagTask.TYPE, + payload = kravgrunnlagXml, + ), + ) + } + + private fun assertOkoXmlMottattData( + mottattKravgrunnlagListe: List<ØkonomiXmlMottatt>, + kravgrunnlagXml: String, + kravstatuskode: Kravstatuskode, + referanse: String, + ) { + mottattKravgrunnlagListe.shouldNotBeEmpty() + mottattKravgrunnlagListe.size shouldBe 1 + val mottattKravgrunnlag = mottattKravgrunnlagListe[0] + mottattKravgrunnlag.kravstatuskode shouldBe kravstatuskode + mottattKravgrunnlag.eksternFagsakId shouldBe fagsak.eksternFagsakId + mottattKravgrunnlag.referanse shouldBe referanse + mottattKravgrunnlag.kontrollfelt shouldBe "2021-03-02-18.50.15.236315" + mottattKravgrunnlag.melding shouldBe kravgrunnlagXml + mottattKravgrunnlag.eksternKravgrunnlagId shouldBe BigInteger.ZERO + mottattKravgrunnlag.vedtakId shouldBe BigInteger.ZERO + mottattKravgrunnlag.ytelsestype shouldBe Ytelsestype.BARNETRYGD + } + + private fun assertPerioder(kravgrunnlag: Kravgrunnlag431) { + val perioder = kravgrunnlag.perioder + perioder.shouldNotBeNull() + perioder.size shouldBe 1 + (perioder.toList()[0].månedligSkattebeløp == BigDecimal("0.00")).shouldBeTrue() + } + + private fun assertBeløp(kravgrunnlag: Kravgrunnlag431) { + val kravgrunnlagsbeløp = kravgrunnlag.perioder.toList()[0].beløp + kravgrunnlagsbeløp.size shouldBe 2 + kravgrunnlagsbeløp.any { Klassetype.YTEL == it.klassetype }.shouldBeTrue() + kravgrunnlagsbeløp.any { Klassetype.FEIL == it.klassetype }.shouldBeTrue() + } + + private fun lagGrunnlagssteg() { + behandlingsstegstilstandRepository + .insert( + Behandlingsstegstilstand( + behandlingId = behandling.id, + behandlingssteg = Behandlingssteg.GRUNNLAG, + behandlingsstegsstatus = Behandlingsstegstatus.VENTER, + venteårsak = Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + tidsfrist = LocalDate.now().plusWeeks(4), + ), + ) + } + + private fun assertBehandlingsstegstilstand( + behandlingsstegstilstand: List, + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ) { + behandlingsstegstilstand.any { + behandlingssteg == it.behandlingssteg && + behandlingsstegstatus == it.behandlingsstegsstatus + }.shouldBeTrue() + } + + private fun assertHistorikkTask( + historikkinnslagstype: TilbakekrevingHistorikkinnslagstype, + aktør: Aktør, + ) { + taskService.findAll().any { + LagHistorikkinnslagTask.TYPE == it.type && + historikkinnslagstype.name == it.metadata["historikkinnslagstype"] && + aktør.name == it.metadata["aktør"] && + behandling.id.toString() == it.payload + }.shouldBeTrue() + } + + private fun assertOppdaterFaktainfoTask(referanse: String) { + taskService.findAll().any { + OppdaterFaktainfoTask.TYPE == it.type && + fagsak.eksternFagsakId == it.metadata["eksternFagsakId"] && + fagsak.ytelsestype.name == it.metadata["ytelsestype"] && + referanse == it.metadata["eksternId"] + }.shouldBeTrue() + } + + private fun assertOppgaveTask( + beskrivelse: String, + fristDato: LocalDate, + ) { + taskService.findAll().any { + OppdaterOppgaveTask.TYPE == it.type && + behandling.id.toString() == it.payload + beskrivelse == it.metadata["beskrivelse"] && + fristDato.toString() == it.metadata["frist"] + }.shouldBeTrue() + } + + private fun assertOpprettOppdaterPrioritetTask() { + taskService.findAll().any { + OppdaterPrioritetTask.TYPE == it.type && + behandling.id.toString() == it.payload + }.shouldBeTrue() + } + + private fun assertIkkeOpprettOppdaterPrioritetTask() { + taskService.findAll().any { + OppdaterPrioritetTask.TYPE == it.type && + behandling.id.toString() == it.payload + }.shouldBeFalse() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/BehandleStatusmeldingTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/BehandleStatusmeldingTaskTest.kt new file mode 100644 index 000000000..221dc7a90 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/BehandleStatusmeldingTaskTest.kt @@ -0,0 +1,481 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSingleElement +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg.FAKTA +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg.FORELDELSE +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg.FORESLÅ_VEDTAK +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg.GRUNNLAG +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg.VARSEL +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg.VILKÅRSVURDERING +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.exceptionhandler.Feil +import no.nav.familie.tilbake.common.exceptionhandler.UgyldigStatusmeldingFeil +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.historikkinnslag.LagHistorikkinnslagTask +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.familie.tilbake.kravgrunnlag.task.BehandleKravgrunnlagTask +import no.nav.familie.tilbake.kravgrunnlag.task.BehandleStatusmeldingTask +import no.nav.familie.tilbake.oppgave.OppdaterOppgaveTask +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.domain.Pageable +import java.time.LocalDate + +internal class BehandleStatusmeldingTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var mottattXmlRepository: ØkonomiXmlMottattRepository + + @Autowired + private lateinit var mottattXmlArkivRepository: ØkonomiXmlMottattArkivRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var behandleStatusmeldingTask: BehandleStatusmeldingTask + + @Autowired + private lateinit var behandleKravgrunnlagTask: BehandleKravgrunnlagTask + + private val fagsak = Testdata.fagsak + private val behandling = Testdata.behandling + + @BeforeEach + fun init() { + fagsakRepository.insert(fagsak) + } + + @Test + fun `doTask skal ikke prosessere SPER melding når det ikke finnes et kravgrunnlag`() { + val statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_SPER_BA.xml") + val task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + + val exception = shouldThrow { behandleStatusmeldingTask.doTask(task) } + exception.message shouldBe "Det finnes intet kravgrunnlag for fagsystemId=${fagsak.eksternFagsakId} " + + "og ytelsestype=${fagsak.ytelsestype}" + } + + @Test + fun `doTask skal ikke prosessere ugyldig SPER melding`() { + val statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_SPER_ugyldig.xml") + val task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + + val exception = shouldThrow { behandleStatusmeldingTask.doTask(task) } + exception.message shouldBe "Ugyldig statusmelding for vedtakId=0, Mangler referanse." + } + + @Test + fun `doTask skal prosessere SPER melding uten behandling`() { + opprettGrunnlag() + + val statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_SPER_BA.xml") + val task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + + behandleStatusmeldingTask.doTask(task) + kravgrunnlagRepository.findAll().toList().shouldBeEmpty() + + val mottattXmlListe = mottattXmlRepository.findByEksternFagsakIdAndYtelsestype(fagsak.eksternFagsakId, fagsak.ytelsestype) + mottattXmlListe.size shouldBe 1 + val mottattXml = mottattXmlListe[0] + mottattXml.melding.contains(Constants.kravgrunnlagXmlRootElement).shouldBeTrue() + mottattXml.sperret.shouldBeTrue() + + assertArkivertXml(1, false, Kravstatuskode.SPERRET) + } + + @Test + fun `doTask skal prosessere ENDR melding uten behandling`() { + opprettGrunnlag() + + var statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_SPER_BA.xml") + var task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + behandleStatusmeldingTask.doTask(task) + + statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_ENDR_BA.xml") + task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + + behandleStatusmeldingTask.doTask(task) + kravgrunnlagRepository.findAll().toList().shouldBeEmpty() + + val mottattXmlListe = mottattXmlRepository.findByEksternFagsakIdAndYtelsestype(fagsak.eksternFagsakId, fagsak.ytelsestype) + mottattXmlListe.size shouldBe 1 + val mottattXml = mottattXmlListe[0] + mottattXml.melding.contains(Constants.kravgrunnlagXmlRootElement).shouldBeTrue() + mottattXml.sperret.shouldBeFalse() + + assertArkivertXml(2, false, Kravstatuskode.SPERRET, Kravstatuskode.ENDRET) + } + + @Test + fun `doTask skal prosessere AVSL melding uten behandling`() { + opprettGrunnlag() + + val statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_AVSL_BA.xml") + val task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + + behandleStatusmeldingTask.doTask(task) + kravgrunnlagRepository.findAll().toList().shouldBeEmpty() + mottattXmlRepository.findByEksternFagsakIdAndYtelsestype(fagsak.eksternFagsakId, fagsak.ytelsestype).shouldBeEmpty() + + assertArkivertXml(2, true, Kravstatuskode.AVSLUTTET) + } + + @Test + fun `doTask skal prosessere SPER melding med behandling på VARSEL steg`() { + behandlingRepository.insert(behandling) + lagBehandlingsstegstilstand(VARSEL, Behandlingsstegstatus.VENTER) + + opprettGrunnlag() + + val statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_SPER_BA.xml") + val task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + + behandleStatusmeldingTask.doTask(task) + kravgrunnlagRepository.findByBehandlingId(behandling.id).shouldNotBeEmpty() + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + kravgrunnlag.sperret.shouldBeTrue() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingstegstilstand(behandlingsstegstilstand, VARSEL, Behandlingsstegstatus.VENTER) + + val venteårsak = Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT, venteårsak.beskrivelse) + taskService.findAll().none { + it.type == OppdaterOppgaveTask.TYPE && + it.payload == behandling.id.toString() && + it.metadata["beskrivelse"] == venteårsak.beskrivelse && + it.metadata["frist"] == LocalDate.now().plusWeeks(venteårsak.defaultVenteTidIUker).toString() + }.shouldBeTrue() + } + + @Test + fun `doTask skal prosessere SPER melding med behandling på FAKTA steg`() { + behandlingRepository.insert(behandling) + lagBehandlingsstegstilstand(VARSEL, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(GRUNNLAG, Behandlingsstegstatus.VENTER) + + opprettGrunnlag() + var behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingstegstilstand(behandlingsstegstilstand, FAKTA, Behandlingsstegstatus.KLAR) + + val statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_SPER_BA.xml") + val task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + + behandleStatusmeldingTask.doTask(task) + kravgrunnlagRepository.findByBehandlingId(behandling.id).shouldNotBeEmpty() + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + kravgrunnlag.sperret.shouldBeTrue() + + behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingstegstilstand(behandlingsstegstilstand, FAKTA, Behandlingsstegstatus.AVBRUTT) + assertBehandlingstegstilstand(behandlingsstegstilstand, GRUNNLAG, Behandlingsstegstatus.VENTER) + assertBehandlingstegstilstand(behandlingsstegstilstand, VARSEL, Behandlingsstegstatus.UTFØRT) + + assertArkivertXml(2, true, Kravstatuskode.SPERRET) + mottattXmlRepository.findByEksternFagsakIdAndYtelsestype(fagsak.eksternFagsakId, fagsak.ytelsestype).shouldBeEmpty() + + val venteårsak = Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT, venteårsak.beskrivelse) + assertOppgaveTask(venteårsak.beskrivelse, LocalDate.now().plusWeeks(venteårsak.defaultVenteTidIUker)) + } + + @Test + fun `doTask skal prosessere SPER melding med behandling på FORESLÅ VEDTAK steg`() { + behandlingRepository.insert(behandling) + settBehandlingTilForeslåVedtakSteg() + + val statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_SPER_BA.xml") + val task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + + behandleStatusmeldingTask.doTask(task) + kravgrunnlagRepository.findByBehandlingId(behandling.id).shouldNotBeEmpty() + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + kravgrunnlag.sperret.shouldBeTrue() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingstegstilstand(behandlingsstegstilstand, FAKTA, Behandlingsstegstatus.UTFØRT) + assertBehandlingstegstilstand(behandlingsstegstilstand, GRUNNLAG, Behandlingsstegstatus.VENTER) + assertBehandlingstegstilstand(behandlingsstegstilstand, VARSEL, Behandlingsstegstatus.UTFØRT) + assertBehandlingstegstilstand(behandlingsstegstilstand, FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + assertBehandlingstegstilstand(behandlingsstegstilstand, VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + assertBehandlingstegstilstand(behandlingsstegstilstand, FORESLÅ_VEDTAK, Behandlingsstegstatus.AVBRUTT) + + assertArkivertXml(2, true, Kravstatuskode.SPERRET) + mottattXmlRepository.findByEksternFagsakIdAndYtelsestype(fagsak.eksternFagsakId, fagsak.ytelsestype).shouldBeEmpty() + + val venteårsak = Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT, venteårsak.beskrivelse) + assertOppgaveTask(venteårsak.beskrivelse, LocalDate.now().plusWeeks(venteårsak.defaultVenteTidIUker)) + } + + @Test + fun `doTask skal prosessere ENDR melding med behandling på FAKTA steg`() { + behandlingRepository.insert(behandling) + lagBehandlingsstegstilstand(VARSEL, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(GRUNNLAG, Behandlingsstegstatus.VENTER) + opprettGrunnlag() + + var behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingstegstilstand(behandlingsstegstilstand, GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingstegstilstand(behandlingsstegstilstand, FAKTA, Behandlingsstegstatus.KLAR) + + var statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_SPER_BA.xml") + var task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + behandleStatusmeldingTask.doTask(task) + + behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingstegstilstand(behandlingsstegstilstand, FAKTA, Behandlingsstegstatus.AVBRUTT) + assertBehandlingstegstilstand(behandlingsstegstilstand, GRUNNLAG, Behandlingsstegstatus.VENTER) + + statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_ENDR_BA.xml") + task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + + behandleStatusmeldingTask.doTask(task) + kravgrunnlagRepository.findByBehandlingId(behandling.id).shouldNotBeEmpty() + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + kravgrunnlag.sperret.shouldBeFalse() + + behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingstegstilstand(behandlingsstegstilstand, FAKTA, Behandlingsstegstatus.KLAR) + assertBehandlingstegstilstand(behandlingsstegstilstand, GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingstegstilstand(behandlingsstegstilstand, VARSEL, Behandlingsstegstatus.UTFØRT) + + assertArkivertXml(3, true, Kravstatuskode.SPERRET, Kravstatuskode.ENDRET) + + mottattXmlRepository.findByEksternFagsakIdAndYtelsestype(fagsak.eksternFagsakId, fagsak.ytelsestype).shouldBeEmpty() + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT) + assertHistorikkTask( + TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG.beskrivelse, + ) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.BEHANDLING_GJENOPPTATT) + assertOppgaveTask("Behandling er tatt av vent, pga mottatt ENDR melding", LocalDate.now()) + } + + @Test + fun `doTask skal prosessere ENDR melding med behandling på FORESLÅ VEDTAK steg`() { + behandlingRepository.insert(behandling) + settBehandlingTilForeslåVedtakSteg() + + var statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_SPER_BA.xml") + var task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + behandleStatusmeldingTask.doTask(task) + + statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_ENDR_BA.xml") + task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + + behandleStatusmeldingTask.doTask(task) + kravgrunnlagRepository.findByBehandlingId(behandling.id).shouldNotBeEmpty() + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + kravgrunnlag.sperret.shouldBeFalse() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + assertBehandlingstegstilstand(behandlingsstegstilstand, FAKTA, Behandlingsstegstatus.UTFØRT) + assertBehandlingstegstilstand(behandlingsstegstilstand, GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingstegstilstand(behandlingsstegstilstand, VARSEL, Behandlingsstegstatus.UTFØRT) + assertBehandlingstegstilstand(behandlingsstegstilstand, FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + assertBehandlingstegstilstand(behandlingsstegstilstand, VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + assertBehandlingstegstilstand(behandlingsstegstilstand, FORESLÅ_VEDTAK, Behandlingsstegstatus.KLAR) + + assertArkivertXml(3, true, Kravstatuskode.SPERRET, Kravstatuskode.ENDRET) + + mottattXmlRepository.findByEksternFagsakIdAndYtelsestype(fagsak.eksternFagsakId, fagsak.ytelsestype).shouldBeEmpty() + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT) + assertHistorikkTask( + TilbakekrevingHistorikkinnslagstype.BEHANDLING_PÅ_VENT, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG.beskrivelse, + ) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.BEHANDLING_GJENOPPTATT) + assertOppgaveTask("Behandling er tatt av vent, pga mottatt ENDR melding", LocalDate.now()) + } + + @Test + fun `doTask skal prosessere AVSL melding med behandling på FORESLÅ VEDTAK steg`() { + behandlingRepository.insert(behandling) + settBehandlingTilForeslåVedtakSteg() + + val statusmeldingXml = readXml("/kravvedtakstatusxml/statusmelding_AVSL_BA.xml") + val task = opprettTask(statusmeldingXml, BehandleStatusmeldingTask.TYPE) + + behandleStatusmeldingTask.doTask(task) + kravgrunnlagRepository.findByBehandlingId(behandling.id).shouldNotBeEmpty() + val kravgrunnlag = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + kravgrunnlag.sperret.shouldBeFalse() + kravgrunnlag.avsluttet.shouldBeTrue() + + assertArkivertXml(2, true, Kravstatuskode.AVSLUTTET) + + mottattXmlRepository.findByEksternFagsakIdAndYtelsestype(fagsak.eksternFagsakId, fagsak.ytelsestype).shouldBeEmpty() + + val behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandling.status shouldBe Behandlingsstatus.AVSLUTTET + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandling.id) + behandlingsstegstilstand.shouldHaveSingleElement { Behandlingsstegstatus.AVBRUTT != it.behandlingsstegsstatus } + behandlingsstegstilstand.shouldHaveSingleElement { + Behandlingsstegstatus.AVBRUTT != it.behandlingsstegsstatus && + it.behandlingssteg == VARSEL + } + assertBehandlingstegstilstand(behandlingsstegstilstand, VARSEL, Behandlingsstegstatus.UTFØRT) + + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_MOTTATT) + assertHistorikkTask(TilbakekrevingHistorikkinnslagstype.BEHANDLING_HENLAGT, "") + } + + private fun settBehandlingTilForeslåVedtakSteg() { + lagBehandlingsstegstilstand(VARSEL, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(GRUNNLAG, Behandlingsstegstatus.VENTER) + + opprettGrunnlag() + + // oppdater FAKTA steg manuelt til UTFØRT + val aktivtBehandlingsstegstilstand = behandlingsstegstilstandRepository + .findByBehandlingIdAndBehandlingssteg(behandling.id, FAKTA) + aktivtBehandlingsstegstilstand.shouldNotBeNull() + aktivtBehandlingsstegstilstand.let { + behandlingsstegstilstandRepository.update(it.copy(behandlingsstegsstatus = Behandlingsstegstatus.UTFØRT)) + } + // sett aktivt behandlingssteg til FORESLÅ_VEDTAK + lagBehandlingsstegstilstand(FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(FORESLÅ_VEDTAK, Behandlingsstegstatus.KLAR) + } + + private fun opprettGrunnlag() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + val task = opprettTask(kravgrunnlagXml, BehandleKravgrunnlagTask.TYPE) + behandleKravgrunnlagTask.doTask(task) + + kravgrunnlagRepository.findByBehandlingId(behandling.id) // skrevet for å fikse Optimistic Lock Exception + } + + private fun lagBehandlingsstegstilstand( + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + venteårsak: Venteårsak? = null, + tidsfrist: LocalDate? = null, + ) { + behandlingsstegstilstandRepository.insert( + Behandlingsstegstilstand( + behandlingId = behandling.id, + behandlingssteg = behandlingssteg, + behandlingsstegsstatus = behandlingsstegstatus, + venteårsak = venteårsak, + tidsfrist = tidsfrist, + ), + ) + } + + private fun assertBehandlingstegstilstand( + behandlingsstegstilstand: List, + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ) { + behandlingsstegstilstand.shouldHaveSingleElement { + behandlingssteg == it.behandlingssteg && + behandlingsstegstatus == it.behandlingsstegsstatus + } + } + + private fun assertArkivertXml( + size: Int, + finnesKravgrunnlag: Boolean, + vararg statusmeldingKravstatuskode: Kravstatuskode, + ) { + val arkivertXmlListe = mottattXmlArkivRepository.findByEksternFagsakIdAndYtelsestype( + fagsak.eksternFagsakId, + fagsak.ytelsestype, + ) + arkivertXmlListe.size shouldBe size + + if (finnesKravgrunnlag) { + arkivertXmlListe.any { it.melding.contains(Constants.kravgrunnlagXmlRootElement) }.shouldBeTrue() + } + statusmeldingKravstatuskode.forEach { kravstatuskode -> + arkivertXmlListe.shouldHaveSingleElement { + it.melding.contains(Constants.statusmeldingXmlRootElement) && + it.melding.contains(kravstatuskode.kode) + } + } + } + + private fun opprettTask(xml: String, taskType: String): Task { + return taskService.save( + Task( + type = taskType, + payload = xml, + ), + ) + } + + private fun assertHistorikkTask( + historikkinnslagstype: TilbakekrevingHistorikkinnslagstype, + beskrivelse: String? = null, + ) { + taskService.finnTasksMedStatus( + listOf( + Status.KLAR_TIL_PLUKK, + Status.UBEHANDLET, + Status.BEHANDLER, + Status.FERDIG, + ), + page = Pageable.unpaged(), + ).any { + LagHistorikkinnslagTask.TYPE == it.type && + historikkinnslagstype.name == it.metadata["historikkinnslagstype"] && + Aktør.VEDTAKSLØSNING.name == it.metadata["aktør"] && + beskrivelse == it.metadata["beskrivelse"] && + behandling.id.toString() == it.payload + }.shouldBeTrue() + } + + private fun assertOppgaveTask(beskrivelse: String, fristTid: LocalDate) { + taskService.findAll().any { + it.type == OppdaterOppgaveTask.TYPE && + it.payload == behandling.id.toString() && + it.metadata["beskrivelse"] == beskrivelse && + it.metadata["frist"] == fristTid.toString() + }.shouldBeTrue() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/FinnKravgrunnlagTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/FinnKravgrunnlagTaskTest.kt new file mode 100644 index 000000000..3b46437b9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/FinnKravgrunnlagTaskTest.kt @@ -0,0 +1,290 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Vergetype +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.BehandlingService +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingRequestSendtRepository +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingService +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.historikkinnslag.HistorikkTaskService +import no.nav.familie.tilbake.integration.kafka.KafkaProducer +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravstatuskode +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottatt +import no.nav.familie.tilbake.kravgrunnlag.event.EndretKravgrunnlagEventPublisher +import no.nav.familie.tilbake.kravgrunnlag.task.FinnKravgrunnlagTask +import no.nav.familie.tilbake.micrometer.TellerService +import no.nav.familie.tilbake.oppgave.OppgaveTaskService +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigInteger +import java.time.LocalDate +import java.util.UUID + +internal class FinnKravgrunnlagTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var økonomiXmlMottattRepository: ØkonomiXmlMottattRepository + + @Autowired + private lateinit var økonomiXmlMottattArkivRepository: ØkonomiXmlMottattArkivRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var requestSendtRepository: HentFagsystemsbehandlingRequestSendtRepository + + @Autowired + private lateinit var behandlingService: BehandlingService + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + @Autowired + private lateinit var stegService: StegService + + @Autowired + private lateinit var oppgaveTaskService: OppgaveTaskService + + @Autowired + private lateinit var mottattXmlService: ØkonomiXmlMottattService + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var historikkTaskService: HistorikkTaskService + + @Autowired + private lateinit var kravvedtakstatusService: KravvedtakstatusService + + @Autowired + private lateinit var tellerService: TellerService + + @Autowired + private lateinit var endretKravgrunnlagEventPublisher: EndretKravgrunnlagEventPublisher + + private val kafkaProducer: KafkaProducer = mockk() + + private lateinit var kravgrunnlagService: KravgrunnlagService + private lateinit var hentFagsystemsbehandlingService: HentFagsystemsbehandlingService + private lateinit var finnKravgrunnlagTask: FinnKravgrunnlagTask + + private lateinit var behandling: Behandling + private lateinit var behandlingId: UUID + + private val eksternFagsakId = "testverdi" + + @BeforeEach + fun init() { + hentFagsystemsbehandlingService = HentFagsystemsbehandlingService(requestSendtRepository, kafkaProducer) + kravgrunnlagService = KravgrunnlagService( + kravgrunnlagRepository, + behandlingRepository, + mottattXmlService, + stegService, + behandlingskontrollService, + taskService, + tellerService, + oppgaveTaskService, + historikkTaskService, + hentFagsystemsbehandlingService, + endretKravgrunnlagEventPublisher, + ) + + finnKravgrunnlagTask = FinnKravgrunnlagTask( + behandlingRepository, + fagsakRepository, + økonomiXmlMottattRepository, + kravgrunnlagRepository, + kravgrunnlagService, + kravvedtakstatusService, + ) + + every { kafkaProducer.sendHentFagsystemsbehandlingRequest(any(), any()) } returns Unit + } + + @Test + fun `doTask skal finne og koble grunnlag med behandling`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + lagreMottattKravgrunnlag(kravgrunnlagXml) + + behandling = opprettBehandling(finnesVerge = true) + behandlingId = behandling.id + + finnKravgrunnlagTask.doTask(Task(type = FinnKravgrunnlagTask.TYPE, payload = behandlingId.toString())) + + val arkivXmlene = økonomiXmlMottattArkivRepository.findByEksternFagsakIdAndYtelsestype( + eksternFagsakId, + Ytelsestype.BARNETRYGD, + ) + arkivXmlene.shouldNotBeEmpty() + + (økonomiXmlMottattRepository.findAll() as List<*>).shouldBeEmpty() + + kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandlingId).shouldBeTrue() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.VERGE, Behandlingsstegstatus.AUTOUTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + } + + @Test + fun `doTask skal finne og koble grunnlag med behandling når grunnlag er sperret`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + lagreMottattKravgrunnlag(kravgrunnlagXml, true) + + behandling = opprettBehandling(finnesVerge = true) + behandlingId = behandling.id + + finnKravgrunnlagTask.doTask(Task(type = FinnKravgrunnlagTask.TYPE, payload = behandlingId.toString())) + + val arkivXmlene = økonomiXmlMottattArkivRepository.findByEksternFagsakIdAndYtelsestype( + eksternFagsakId, + Ytelsestype.BARNETRYGD, + ) + arkivXmlene.shouldNotBeEmpty() + + (økonomiXmlMottattRepository.findAll() as List<*>).shouldBeEmpty() + + kravgrunnlagRepository.existsByBehandlingIdAndAktivTrueAndSperretTrue(behandlingId).shouldBeTrue() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.VENTER) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.VERGE, Behandlingsstegstatus.AUTOUTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.AVBRUTT) + } + + @Test + fun `doTask skal finne og koble grunnlag med behandling når det finnes et NY og et ENDR grunnlag`() { + val kravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + lagreMottattKravgrunnlag(kravgrunnlagXml, true) + + behandling = opprettBehandling(finnesVerge = false) + behandlingId = behandling.id + + val endretKravgrunnlagXml = readXml("/kravgrunnlagxml/kravgrunnlag_BA_ENDR.xml") + lagreMottattKravgrunnlag(endretKravgrunnlagXml) + + finnKravgrunnlagTask.doTask(Task(type = FinnKravgrunnlagTask.TYPE, payload = behandlingId.toString())) + + val arkivXmlene = økonomiXmlMottattArkivRepository.findByEksternFagsakIdAndYtelsestype( + eksternFagsakId, + Ytelsestype.BARNETRYGD, + ) + arkivXmlene.shouldNotBeEmpty() + arkivXmlene.size shouldBe 2 + + (økonomiXmlMottattRepository.findAll() as List<*>).shouldBeEmpty() + + kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandlingId).shouldBeTrue() + val kravgrunnlagene = kravgrunnlagRepository.findByBehandlingId(behandlingId) + kravgrunnlagene.any { it.aktiv && it.kravstatuskode == Kravstatuskode.ENDRET }.shouldBeTrue() + kravgrunnlagene.any { !it.aktiv && it.sperret && it.kravstatuskode == Kravstatuskode.NYTT }.shouldBeTrue() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(behandlingId) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertBehandlingsstegstilstand(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + behandlingsstegstilstand.any { it.behandlingssteg == Behandlingssteg.VERGE }.shouldBeFalse() + } + + private fun opprettBehandling(finnesVerge: Boolean): Behandling { + val faktainfo = Faktainfo( + revurderingsårsak = "testverdi", + revurderingsresultat = "testresultat", + tilbakekrevingsvalg = Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_UTEN_VARSEL, + ) + + val verge = if (finnesVerge) { + no.nav.familie.kontrakter.felles.tilbakekreving.Verge( + vergetype = Vergetype.VERGE_FOR_BARN, + navn = "Andy", + personIdent = "321321321", + ) + } else { + null + } + + val request = OpprettTilbakekrevingRequest( + ytelsestype = Ytelsestype.BARNETRYGD, + fagsystem = Fagsystem.BA, + eksternFagsakId = eksternFagsakId, + personIdent = "321321322", + eksternId = "0", + manueltOpprettet = false, + språkkode = Språkkode.NB, + enhetId = "8020", + enhetsnavn = "Oslo", + varsel = null, + verge = verge, + revurderingsvedtaksdato = LocalDate.now(), + faktainfo = faktainfo, + saksbehandlerIdent = "Z0000", + ) + return behandlingService.opprettBehandling(request) + } + + private fun lagreMottattKravgrunnlag( + kravgrunnlagXml: String, + sperret: Boolean = false, + ) { + økonomiXmlMottattRepository.insert( + ØkonomiXmlMottatt( + melding = kravgrunnlagXml, + kravstatuskode = Kravstatuskode.NYTT, + eksternFagsakId = eksternFagsakId, + ytelsestype = Ytelsestype.BARNETRYGD, + referanse = "0", + eksternKravgrunnlagId = BigInteger.ZERO, + vedtakId = BigInteger.ZERO, + kontrollfelt = "2021-03-02-18.50.15.236315", + sperret = sperret, + ), + ) + } + + private fun assertBehandlingsstegstilstand( + behandlingsstegstilstand: List, + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ) { + behandlingsstegstilstand.any { + it.behandlingssteg == behandlingssteg && + it.behandlingsstegsstatus == behandlingsstegstatus + }.shouldBeTrue() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/HentFagsystemsbehandlingTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/HentFagsystemsbehandlingTaskTest.kt new file mode 100644 index 000000000..132290970 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/HentFagsystemsbehandlingTaskTest.kt @@ -0,0 +1,164 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveSingleElement +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.BehandlingService +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingRequestSendtRepository +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingService +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.common.exceptionhandler.UgyldigKravgrunnlagFeil +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.historikkinnslag.HistorikkService +import no.nav.familie.tilbake.integration.kafka.KafkaProducer +import no.nav.familie.tilbake.kravgrunnlag.batch.HentFagsystemsbehandlingTask +import no.nav.familie.tilbake.kravgrunnlag.batch.HåndterGamleKravgrunnlagService +import no.nav.familie.tilbake.kravgrunnlag.batch.HåndterGammelKravgrunnlagTask +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottatt +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.util.UUID + +internal class HentFagsystemsbehandlingTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var xmlMottattRepository: ØkonomiXmlMottattRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var requestSendtRepository: HentFagsystemsbehandlingRequestSendtRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var behandlingService: BehandlingService + + @Autowired + private lateinit var økonomiXmlMottattService: ØkonomiXmlMottattService + + @Autowired + private lateinit var stegService: StegService + + @Autowired + private lateinit var historikkService: HistorikkService + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + private val mockHentKravgrunnlagService: HentKravgrunnlagService = mockk() + + private lateinit var håndterGamleKravgrunnlagService: HåndterGamleKravgrunnlagService + private lateinit var hentFagsystemsbehandlingService: HentFagsystemsbehandlingService + private lateinit var hentFagsystemsbehandlingTask: HentFagsystemsbehandlingTask + + private var xmlMottatt: ØkonomiXmlMottatt = Testdata.økonomiXmlMottatt + private lateinit var mottattXMl: String + private lateinit var mottattXmlId: UUID + + private val eksternFagsakIdSlot = slot() + private val ytelsestypeSlot = slot() + private val eksternIdSlot = slot() + + @BeforeEach + fun init() { + mottattXMl = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + xmlMottatt = xmlMottattRepository.insert(Testdata.økonomiXmlMottatt.copy(melding = mottattXMl)) + mottattXmlId = xmlMottatt.id + + håndterGamleKravgrunnlagService = HåndterGamleKravgrunnlagService( + behandlingRepository, + kravgrunnlagRepository, + behandlingService, + behandlingskontrollService, + økonomiXmlMottattService, + mockHentKravgrunnlagService, + stegService, + historikkService, + ) + val kafkaProducer: KafkaProducer = mockk() + hentFagsystemsbehandlingService = spyk(HentFagsystemsbehandlingService(requestSendtRepository, kafkaProducer)) + hentFagsystemsbehandlingTask = + HentFagsystemsbehandlingTask(håndterGamleKravgrunnlagService, hentFagsystemsbehandlingService, taskService) + + every { kafkaProducer.sendHentFagsystemsbehandlingRequest(any(), any()) } returns Unit + } + + @AfterEach + fun tearDown() { + requestSendtRepository.deleteAll() + } + + @Test + fun `doTask skal kaste exception når det allerede finnes en behandling på samme fagsak`() { + fagsakRepository.insert(Testdata.fagsak.copy(eksternFagsakId = xmlMottatt.eksternFagsakId)) + behandlingRepository.insert(Testdata.behandling) + + val exception = shouldThrow { hentFagsystemsbehandlingTask.doTask(lagTask()) } + exception.message shouldBe "Kravgrunnlag med $mottattXmlId er ugyldig." + + "Det finnes allerede en åpen behandling for " + + "fagsak=${xmlMottatt.eksternFagsakId} og ytelsestype=${xmlMottatt.ytelsestype}. " + + "Kravgrunnlaget skulle være koblet. Kravgrunnlaget arkiveres manuelt" + + "ved å bruke forvaltningsrutine etter feilundersøkelse." + } + + @Test + fun `doTask skal sende hentFagsystemsbehandling request når det ikke finnes en behandling på samme fagsak`() { + hentFagsystemsbehandlingTask.doTask(lagTask()) + + verify { + hentFagsystemsbehandlingService.sendHentFagsystemsbehandlingRequest( + capture(eksternFagsakIdSlot), + capture(ytelsestypeSlot), + capture(eksternIdSlot), + ) + } + eksternFagsakIdSlot.captured shouldBe xmlMottatt.eksternFagsakId + ytelsestypeSlot.captured shouldBe xmlMottatt.ytelsestype + eksternIdSlot.captured shouldBe xmlMottatt.referanse + + requestSendtRepository.findByEksternFagsakIdAndYtelsestypeAndEksternId( + xmlMottatt.eksternFagsakId, + xmlMottatt.ytelsestype, + xmlMottatt.referanse, + ).shouldNotBeNull() + } + + @Test + fun `onCompletion skal opprette task for å håndtere gammel kravgrunnlag`() { + hentFagsystemsbehandlingTask.onCompletion(lagTask()) + + taskService.finnTasksMedStatus(listOf(Status.UBEHANDLET)).shouldHaveSingleElement { + it.type == HåndterGammelKravgrunnlagTask.TYPE && + it.payload == xmlMottatt.id.toString() + } + } + + private fun lagTask(): Task { + return taskService.save(Task(type = HentFagsystemsbehandlingTask.TYPE, payload = mottattXmlId.toString())) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/HentKravgrunnlagTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/HentKravgrunnlagTaskTest.kt new file mode 100644 index 000000000..d3a6e7507 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/HentKravgrunnlagTaskTest.kt @@ -0,0 +1,147 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import no.nav.familie.kontrakter.felles.Applikasjon +import no.nav.familie.kontrakter.felles.historikkinnslag.Aktør +import no.nav.familie.kontrakter.felles.historikkinnslag.Historikkinnslagstype +import no.nav.familie.kontrakter.felles.historikkinnslag.OpprettHistorikkinnslagRequest +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingsstatus +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingRepository +import no.nav.familie.tilbake.historikkinnslag.HistorikkService +import no.nav.familie.tilbake.historikkinnslag.TilbakekrevingHistorikkinnslagstype +import no.nav.familie.tilbake.integration.kafka.DefaultKafkaProducer +import no.nav.familie.tilbake.integration.kafka.KafkaProducer +import no.nav.familie.tilbake.integration.økonomi.MockOppdragClient +import no.nav.familie.tilbake.integration.økonomi.OppdragClient +import no.nav.familie.tilbake.kravgrunnlag.task.HentKravgrunnlagTask +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.kafka.core.KafkaTemplate +import java.time.LocalDate +import java.util.UUID + +internal class HentKravgrunnlagTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var mottattXmlRepository: ØkonomiXmlMottattRepository + + @Autowired + private lateinit var brevsporingRepository: BrevsporingRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var stegService: StegService + + private lateinit var kafkaProducer: KafkaProducer + private lateinit var historikkService: HistorikkService + private lateinit var oppdragClient: OppdragClient + private lateinit var hentKravgrunnlagService: HentKravgrunnlagService + private lateinit var hentKravgrunnlagTask: HentKravgrunnlagTask + + private lateinit var fagsak: Fagsak + private lateinit var behandling: Behandling + + private val behandlingSlot = slot() + private val historikkinnslagSlot = slot() + + @BeforeEach + fun init() { + fagsak = fagsakRepository.insert(Testdata.fagsak) + behandling = behandlingRepository.insert(Testdata.behandling) + kravgrunnlagRepository.insert(Testdata.kravgrunnlag431) + + behandling = behandlingRepository.findByIdOrThrow(behandling.id) + behandlingRepository.update(behandling.copy(status = Behandlingsstatus.AVSLUTTET)) + + val kafkaTemplate: KafkaTemplate = mockk() + kafkaProducer = spyk(DefaultKafkaProducer(kafkaTemplate)) + historikkService = HistorikkService(behandlingRepository, fagsakRepository, brevsporingRepository, kafkaProducer) + oppdragClient = MockOppdragClient(kravgrunnlagRepository, mottattXmlRepository) + hentKravgrunnlagService = HentKravgrunnlagService(kravgrunnlagRepository, oppdragClient, historikkService) + hentKravgrunnlagTask = HentKravgrunnlagTask(behandlingRepository, hentKravgrunnlagService, stegService) + + every { kafkaProducer.sendHistorikkinnslag(any(), any(), any()) } returns Unit + } + + @Test + fun `doTask skal hente kravgrunnlag for revurderingstilbakekreving`() { + val revurdering = behandlingRepository.insert(Testdata.revurdering) + behandlingsstegstilstandRepository + .insert( + Behandlingsstegstilstand( + behandlingId = revurdering.id, + behandlingssteg = Behandlingssteg.GRUNNLAG, + behandlingsstegsstatus = Behandlingsstegstatus.VENTER, + venteårsak = Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + tidsfrist = LocalDate.now().plusWeeks(3), + ), + ) + + hentKravgrunnlagTask.doTask(lagTask(revurdering.id)) + kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(revurdering.id).shouldBeTrue() + + verify { kafkaProducer.sendHistorikkinnslag(capture(behandlingSlot), any(), capture(historikkinnslagSlot)) } + behandlingSlot.captured shouldBe revurdering.id + + val historikkinnslagRequest = historikkinnslagSlot.captured + historikkinnslagRequest.type shouldBe Historikkinnslagstype.HENDELSE + historikkinnslagRequest.behandlingId shouldBe revurdering.eksternBrukId.toString() + historikkinnslagRequest.eksternFagsakId shouldBe fagsak.eksternFagsakId + historikkinnslagRequest.aktør shouldBe Aktør.VEDTAKSLØSNING + historikkinnslagRequest.aktørIdent shouldBe Constants.BRUKER_ID_VEDTAKSLØSNINGEN + historikkinnslagRequest.applikasjon shouldBe Applikasjon.FAMILIE_TILBAKE + historikkinnslagRequest.tittel shouldBe TilbakekrevingHistorikkinnslagstype.KRAVGRUNNLAG_HENT.tittel + historikkinnslagRequest.opprettetTidspunkt.toLocalDate() shouldBe LocalDate.now() + + val behandlingsstegstilstand = behandlingsstegstilstandRepository.findByBehandlingId(revurdering.id) + behandlingsstegstilstand.any { + Behandlingssteg.GRUNNLAG == it.behandlingssteg && + Behandlingsstegstatus.UTFØRT == it.behandlingsstegsstatus + }.shouldBeTrue() + + behandlingsstegstilstand.any { + Behandlingssteg.FAKTA == it.behandlingssteg && + Behandlingsstegstatus.KLAR == it.behandlingsstegsstatus + }.shouldBeTrue() + } + + private fun lagTask(behandlingId: UUID): Task { + return Task( + type = HentKravgrunnlagTask.TYPE, + payload = behandlingId.toString(), + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/H\303\245ndterGamleKravgrunnlagBatchTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/H\303\245ndterGamleKravgrunnlagBatchTest.kt" new file mode 100644 index 000000000..aff94cb8b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/H\303\245ndterGamleKravgrunnlagBatchTest.kt" @@ -0,0 +1,74 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.prosessering.domene.Status +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.avstemming.task.AvstemmingTask +import no.nav.familie.tilbake.common.repository.Sporbar +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.kravgrunnlag.batch.HentFagsystemsbehandlingTask +import no.nav.familie.tilbake.kravgrunnlag.batch.HåndterGamleKravgrunnlagBatch +import no.nav.familie.tilbake.kravgrunnlag.batch.HåndterGammelKravgrunnlagTask +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDateTime +import java.util.UUID + +internal class HåndterGamleKravgrunnlagBatchTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var mottattXmlRepository: ØkonomiXmlMottattRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var håndterGamleKravgrunnlagBatch: HåndterGamleKravgrunnlagBatch + + @Test + fun `utfør skal ikke opprette tasker når det ikke finnes noen kravgrunnlag som er gamle enn bestemte uker`() { + mottattXmlRepository.insert(Testdata.økonomiXmlMottatt) + + håndterGamleKravgrunnlagBatch.utfør() + (taskService.findAll().filter { it.type != AvstemmingTask.TYPE }).shouldBeEmpty() + } + + @Test + fun `utfør skal ikke opprette tasker når det allerede finnes en feilet task på det samme kravgrunnlag`() { + val mottattXml = mottattXmlRepository.insert(Testdata.økonomiXmlMottatt) + val task = taskService.save(Task(type = HåndterGammelKravgrunnlagTask.TYPE, payload = mottattXml.id.toString())) + taskService.save(taskService.findById(task.id).copy(status = Status.FEILET)) + + håndterGamleKravgrunnlagBatch.utfør() + taskService.findAll().any { it.type == HentFagsystemsbehandlingTask.TYPE }.shouldBeFalse() + } + + @Test + fun `utfør skal opprette tasker når det finnes noen kravgrunnlag som er gamle enn bestemte uker`() { + val førsteXml = Testdata.økonomiXmlMottatt.copy( + id = UUID.randomUUID(), + sporbar = Sporbar(opprettetTid = LocalDateTime.now().minusWeeks(9)), + ) + mottattXmlRepository.insert(førsteXml) + + val andreXml = Testdata.økonomiXmlMottatt.copy( + id = UUID.randomUUID(), + sporbar = Sporbar(opprettetTid = LocalDateTime.now().minusWeeks(9)), + ytelsestype = Ytelsestype.SKOLEPENGER, + ) + mottattXmlRepository.insert(andreXml) + + val tredjeXml = Testdata.økonomiXmlMottatt + mottattXmlRepository.insert(tredjeXml) + + håndterGamleKravgrunnlagBatch.utfør() + (taskService.findAll() as List<*>).shouldNotBeEmpty() + taskService.findAll().count { it.type == HentFagsystemsbehandlingTask.TYPE } shouldBe 2 + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/H\303\245ndterGammelKravgrunnlagTaskTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/H\303\245ndterGammelKravgrunnlagTaskTest.kt" new file mode 100644 index 000000000..2598023c5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/H\303\245ndterGammelKravgrunnlagTaskTest.kt" @@ -0,0 +1,315 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldHaveSingleElement +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.objectMapper +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandling +import no.nav.familie.kontrakter.felles.tilbakekreving.HentFagsystemsbehandlingRespons +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.BehandlingService +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingRequestSendtRepository +import no.nav.familie.tilbake.behandling.HentFagsystemsbehandlingService +import no.nav.familie.tilbake.behandling.domain.HentFagsystemsbehandlingRequestSendt +import no.nav.familie.tilbake.behandling.steg.StegService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.common.exceptionhandler.IntegrasjonException +import no.nav.familie.tilbake.common.exceptionhandler.KravgrunnlagIkkeFunnetFeil +import no.nav.familie.tilbake.common.exceptionhandler.SperretKravgrunnlagFeil +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.dokumentbestilling.felles.BrevsporingRepository +import no.nav.familie.tilbake.historikkinnslag.HistorikkService +import no.nav.familie.tilbake.integration.kafka.KafkaProducer +import no.nav.familie.tilbake.kravgrunnlag.batch.HåndterGamleKravgrunnlagService +import no.nav.familie.tilbake.kravgrunnlag.batch.HåndterGammelKravgrunnlagTask +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottatt +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.repository.findByIdOrNull +import java.time.LocalDate +import java.util.UUID + +internal class HåndterGammelKravgrunnlagTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var brevSporingRepository: BrevsporingRepository + + @Autowired + private lateinit var xmlMottattRepository: ØkonomiXmlMottattRepository + + @Autowired + private lateinit var xmlMottattArkivRepository: ØkonomiXmlMottattArkivRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var taskService: TaskService + + @Autowired + private lateinit var requestSendtRepository: HentFagsystemsbehandlingRequestSendtRepository + + @Autowired + private lateinit var behandlingstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var behandlingService: BehandlingService + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + @Autowired + private lateinit var økonomiXmlMottattService: ØkonomiXmlMottattService + + @Autowired + private lateinit var stegService: StegService + + private val mockHentKravgrunnlagService: HentKravgrunnlagService = mockk() + + private lateinit var historikkService: HistorikkService + private lateinit var håndterGamleKravgrunnlagService: HåndterGamleKravgrunnlagService + private lateinit var hentFagsystemsbehandlingService: HentFagsystemsbehandlingService + private lateinit var håndterGammelKravgrunnlagTask: HåndterGammelKravgrunnlagTask + + private var xmlMottatt: ØkonomiXmlMottatt = Testdata.økonomiXmlMottatt + private lateinit var mottattXMl: String + private lateinit var mottattXmlId: UUID + + @BeforeEach + fun init() { + mottattXMl = readXml("/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml") + xmlMottatt = xmlMottattRepository.insert(Testdata.økonomiXmlMottatt.copy(melding = mottattXMl)) + mottattXmlId = xmlMottatt.id + + val kafkaProducer: KafkaProducer = mockk() + historikkService = HistorikkService(behandlingRepository, fagsakRepository, brevSporingRepository, kafkaProducer) + håndterGamleKravgrunnlagService = HåndterGamleKravgrunnlagService( + behandlingRepository, + kravgrunnlagRepository, + behandlingService, + behandlingskontrollService, + økonomiXmlMottattService, + mockHentKravgrunnlagService, + stegService, + historikkService, + ) + hentFagsystemsbehandlingService = spyk(HentFagsystemsbehandlingService(requestSendtRepository, kafkaProducer)) + håndterGammelKravgrunnlagTask = + HåndterGammelKravgrunnlagTask(håndterGamleKravgrunnlagService, hentFagsystemsbehandlingService) + + every { kafkaProducer.sendHentFagsystemsbehandlingRequest(any(), any()) } returns Unit + every { kafkaProducer.sendHistorikkinnslag(any(), any(), any()) } returns Unit + } + + @AfterEach + fun tearDown() { + requestSendtRepository.deleteAll() + } + + @Test + fun `doTask skal kaste exception når fagsystemsbehandling ikke finnes i fagsystem`() { + requestSendtRepository.insert( + HentFagsystemsbehandlingRequestSendt( + eksternFagsakId = xmlMottatt.eksternFagsakId, + ytelsestype = xmlMottatt.ytelsestype, + eksternId = xmlMottatt.referanse, + ), + ) + val exception = shouldThrow { håndterGammelKravgrunnlagTask.doTask(lagTask()) } + exception.message shouldBe "HentFagsystemsbehandling respons-en har ikke mottatt fra fagsystem for " + + "eksternFagsakId=${xmlMottatt.eksternFagsakId},ytelsestype=${xmlMottatt.ytelsestype}," + + "eksternId=${xmlMottatt.referanse}.Task-en kan kjøre på nytt manuelt når respons-en er mottatt." + } + + @Test + fun `doTask skal opprette en behandling og koble kravgrunnlag med behandlingen`() { + requestSendtRepository + .insert( + HentFagsystemsbehandlingRequestSendt( + eksternFagsakId = xmlMottatt.eksternFagsakId, + ytelsestype = xmlMottatt.ytelsestype, + eksternId = xmlMottatt.referanse, + respons = lagHentFagsystemsbehandlingRespons(xmlMottatt), + ), + ) + + val hentetKravgrunnlag = KravgrunnlagUtil.unmarshalKravgrunnlag(mottattXMl) + + every { mockHentKravgrunnlagService.hentKravgrunnlagFraØkonomi(any(), any()) } returns hentetKravgrunnlag + + håndterGammelKravgrunnlagTask.doTask(lagTask()) + + val behandling = behandlingRepository.finnÅpenTilbakekrevingsbehandling( + xmlMottatt.ytelsestype, + hentetKravgrunnlag.fagsystemId, + ) + behandling.shouldNotBeNull() + kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandling.id).shouldBeTrue() + + val behandlingsstegstilstand = behandlingstilstandRepository.findByBehandlingId(behandling.id) + assertSteg(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + assertSteg(behandlingsstegstilstand, Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + + xmlMottattRepository.findByIdOrNull(mottattXmlId).shouldBeNull() + xmlMottattArkivRepository.findByEksternFagsakIdAndYtelsestype( + xmlMottatt.eksternFagsakId, + xmlMottatt.ytelsestype, + ).shouldNotBeNull() + } + + @Test + fun `doTask skal opprette en behandling og venter på kravgrunnlag når hentet kravgrunnlag er sperret hos økonomi`() { + requestSendtRepository + .insert( + HentFagsystemsbehandlingRequestSendt( + eksternFagsakId = xmlMottatt.eksternFagsakId, + ytelsestype = xmlMottatt.ytelsestype, + eksternId = xmlMottatt.referanse, + respons = lagHentFagsystemsbehandlingRespons(xmlMottatt), + ), + ) + + val hentetKravgrunnlag = KravgrunnlagUtil.unmarshalKravgrunnlag(mottattXMl) + + every { mockHentKravgrunnlagService.hentKravgrunnlagFraØkonomi(any(), any()) } throws + SperretKravgrunnlagFeil("Hentet kravgrunnlag er sperret") + + håndterGammelKravgrunnlagTask.doTask(lagTask()) + val behandling = behandlingRepository.finnÅpenTilbakekrevingsbehandling( + xmlMottatt.ytelsestype, + hentetKravgrunnlag.fagsystemId, + ) + behandling.shouldNotBeNull() + kravgrunnlagRepository.existsByBehandlingIdAndAktivTrueAndSperretTrue(behandling.id).shouldBeTrue() + + val behandlingsstegstilstand = behandlingstilstandRepository.findByBehandlingId(behandling.id) + assertSteg(behandlingsstegstilstand, Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.VENTER) + + xmlMottattRepository.findByIdOrNull(mottattXmlId).shouldBeNull() + xmlMottattArkivRepository.findByEksternFagsakIdAndYtelsestype( + xmlMottatt.eksternFagsakId, + xmlMottatt.ytelsestype, + ).shouldNotBeNull() + } + + @Test + fun `doTask skal kaste exception når kravgrunnlag ikke finnes hos økonomi`() { + requestSendtRepository + .insert( + HentFagsystemsbehandlingRequestSendt( + eksternFagsakId = xmlMottatt.eksternFagsakId, + ytelsestype = xmlMottatt.ytelsestype, + eksternId = xmlMottatt.referanse, + respons = lagHentFagsystemsbehandlingRespons(xmlMottatt), + ), + ) + + every { mockHentKravgrunnlagService.hentKravgrunnlagFraØkonomi(any(), any()) } throws + IntegrasjonException("Kravgrunnlag finnes ikke i økonomi") + + val exception = shouldThrow { håndterGammelKravgrunnlagTask.doTask(lagTask()) } + exception.message shouldBe "Kravgrunnlag finnes ikke i økonomi" + } + + @Test + fun `doTask skal arkivere kravgrunnlag som ikke finnes hos økonomi dersom det finnes nyere duplikat med kravstatus AVSL`() { + requestSendtRepository + .insert( + HentFagsystemsbehandlingRequestSendt( + eksternFagsakId = xmlMottatt.eksternFagsakId, + ytelsestype = xmlMottatt.ytelsestype, + eksternId = xmlMottatt.referanse, + respons = lagHentFagsystemsbehandlingRespons(xmlMottatt), + ), + ) + + økonomiXmlMottattService.arkiverMottattXml( + mottattXml = mottattXMl + .replace("", "2") + .replace("", "2") + .replace("", "2"), + fagsystemId = xmlMottatt.eksternFagsakId, + ytelsestype = xmlMottatt.ytelsestype, + ) + + økonomiXmlMottattService.arkiverMottattXml( + mottattXml = readXml("/kravvedtakstatusxml/statusmelding_AVSL_BA.xml") + .replace("", "2"), + fagsystemId = xmlMottatt.eksternFagsakId, + ytelsestype = xmlMottatt.ytelsestype, + ) + + every { mockHentKravgrunnlagService.hentKravgrunnlagFraØkonomi(any(), any()) } throws + KravgrunnlagIkkeFunnetFeil(melding = "Noe gikk galt") + + val task = lagTask() + håndterGammelKravgrunnlagTask.doTask(task) + + val arkiverteKravgrunnlag = + økonomiXmlMottattService.hentArkiverteMottattXml(xmlMottatt.eksternFagsakId, xmlMottatt.ytelsestype) + + arkiverteKravgrunnlag.size shouldBe 3 + arkiverteKravgrunnlag.shouldHaveSingleElement { it.melding == mottattXMl } + } + + private fun lagTask(): Task { + return taskService.save(Task(type = HåndterGammelKravgrunnlagTask.TYPE, payload = mottattXmlId.toString())) + } + + private fun lagHentFagsystemsbehandlingRespons(xmlMottatt: ØkonomiXmlMottatt): String { + val fagsystemsbehandling = HentFagsystemsbehandling( + eksternFagsakId = xmlMottatt.eksternFagsakId, + ytelsestype = xmlMottatt.ytelsestype, + eksternId = xmlMottatt.referanse, + personIdent = "testverdi", + språkkode = Språkkode.NB, + enhetId = "8020", + enhetsnavn = "testverdi", + revurderingsvedtaksdato = LocalDate.now(), + faktainfo = Faktainfo( + revurderingsårsak = "testverdi", + revurderingsresultat = "OPPHØR", + tilbakekrevingsvalg = Tilbakekrevingsvalg + .IGNORER_TILBAKEKREVING, + ), + ) + + return objectMapper.writeValueAsString(HentFagsystemsbehandlingRespons(hentFagsystemsbehandling = fagsystemsbehandling)) + } + + private fun assertSteg( + behandlingsstegstilstand: List, + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ) { + behandlingsstegstilstand.shouldHaveSingleElement { + it.behandlingssteg == behandlingssteg && + behandlingsstegstatus == it.behandlingsstegsstatus + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagMottakerTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagMottakerTest.kt new file mode 100644 index 000000000..51595df21 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagMottakerTest.kt @@ -0,0 +1,25 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.kravgrunnlag.task.BehandleKravgrunnlagTask +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class KravgrunnlagMottakerTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var taskService: TaskService + + @Test + fun `verifiser at det er mulig å lagre en task`() { + taskService.save( + Task( + type = BehandleKravgrunnlagTask.TYPE, + payload = "kravgrunnlagFraOppdrag", + ), + ) + taskService.findAll().filter { it.type == BehandleKravgrunnlagTask.TYPE }.isNotEmpty() + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagRepositoryTest.kt new file mode 100644 index 000000000..3f653c7b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/KravgrunnlagRepositoryTest.kt @@ -0,0 +1,82 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class KravgrunnlagRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + private val fagsak = Testdata.fagsak + private val kravgrunnlag431 = Testdata.kravgrunnlag431 + private val behandling = Testdata.behandling + + @BeforeEach + fun init() { + fagsakRepository.insert(fagsak) + behandlingRepository.insert(behandling) + } + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av Kravgrunnlag431 til basen`() { + kravgrunnlagRepository.insert(kravgrunnlag431) + + val lagretKravgrunnlag431 = kravgrunnlagRepository.findByIdOrThrow(kravgrunnlag431.id) + + lagretKravgrunnlag431.shouldBeEqualToComparingFieldsExcept( + kravgrunnlag431, + Kravgrunnlag431::sporbar, + Kravgrunnlag431::perioder, + Kravgrunnlag431::versjon, + ) + lagretKravgrunnlag431.versjon shouldBe 1 + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av Kravgrunnlag431 i basen`() { + kravgrunnlagRepository.insert(kravgrunnlag431) + var lagretKravgrunnlag431 = kravgrunnlagRepository.findByIdOrThrow(kravgrunnlag431.id) + val oppdatertKravgrunnlag431 = lagretKravgrunnlag431.copy(sperret = true) + + kravgrunnlagRepository.update(oppdatertKravgrunnlag431) + + lagretKravgrunnlag431 = kravgrunnlagRepository.findByIdOrThrow(kravgrunnlag431.id) + lagretKravgrunnlag431.shouldBeEqualToComparingFieldsExcept( + oppdatertKravgrunnlag431, + Kravgrunnlag431::sporbar, + Kravgrunnlag431::perioder, + Kravgrunnlag431::versjon, + ) + lagretKravgrunnlag431.versjon shouldBe 2 + } + + @Test + fun `findByBehandlingId returnerer resultat når det finnes en forekomst`() { + kravgrunnlagRepository.insert(kravgrunnlag431) + + val findByBehandlingId = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + + kravgrunnlag431.shouldBeEqualToComparingFieldsExcept( + findByBehandlingId, + Kravgrunnlag431::sporbar, + Kravgrunnlag431::perioder, + Kravgrunnlag431::versjon, + ) + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattArkivRepositoryTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattArkivRepositoryTest.kt" new file mode 100644 index 000000000..ff48c29ea --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattArkivRepositoryTest.kt" @@ -0,0 +1,49 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottattArkiv +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class ØkonomiXmlMottattArkivRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var økonomiXmlMottattArkivRepository: ØkonomiXmlMottattArkivRepository + + private val økonomiXmlMottattArkiv = Testdata.økonomiXmlMottattArkiv + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av ØkonomiXmlMottattArkiv til basen`() { + økonomiXmlMottattArkivRepository.insert(økonomiXmlMottattArkiv) + + val lagretØkonomiXmlMottattArkiv = økonomiXmlMottattArkivRepository.findByIdOrThrow(økonomiXmlMottattArkiv.id) + + lagretØkonomiXmlMottattArkiv.shouldBeEqualToComparingFieldsExcept( + økonomiXmlMottattArkiv, + ØkonomiXmlMottattArkiv::sporbar, + ØkonomiXmlMottattArkiv::versjon, + ) + lagretØkonomiXmlMottattArkiv.versjon shouldBe 1 + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av ØkonomiXmlMottattArkiv i basen`() { + økonomiXmlMottattArkivRepository.insert(økonomiXmlMottattArkiv) + var lagretØkonomiXmlMottattArkiv = økonomiXmlMottattArkivRepository.findByIdOrThrow(økonomiXmlMottattArkiv.id) + val oppdatertØkonomiXmlMottattArkiv = lagretØkonomiXmlMottattArkiv.copy(melding = "bob") + + økonomiXmlMottattArkivRepository.update(oppdatertØkonomiXmlMottattArkiv) + + lagretØkonomiXmlMottattArkiv = økonomiXmlMottattArkivRepository.findByIdOrThrow(økonomiXmlMottattArkiv.id) + lagretØkonomiXmlMottattArkiv.shouldBeEqualToComparingFieldsExcept( + oppdatertØkonomiXmlMottattArkiv, + ØkonomiXmlMottattArkiv::sporbar, + ØkonomiXmlMottattArkiv::versjon, + ) + lagretØkonomiXmlMottattArkiv.versjon shouldBe 2 + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattRepositoryTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattRepositoryTest.kt" new file mode 100644 index 000000000..2067f379f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/kravgrunnlag/\303\230konomiXmlMottattRepositoryTest.kt" @@ -0,0 +1,49 @@ +package no.nav.familie.tilbake.kravgrunnlag + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.kravgrunnlag.domain.ØkonomiXmlMottatt +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class ØkonomiXmlMottattRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var økonomiXmlMottattRepository: ØkonomiXmlMottattRepository + + private val økonomiXmlMottatt = Testdata.økonomiXmlMottatt + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av ØkonomiXmlMottatt til basen`() { + økonomiXmlMottattRepository.insert(økonomiXmlMottatt) + + val lagretØkonomiXmlMottatt = økonomiXmlMottattRepository.findByIdOrThrow(økonomiXmlMottatt.id) + + lagretØkonomiXmlMottatt.shouldBeEqualToComparingFieldsExcept( + økonomiXmlMottatt, + ØkonomiXmlMottatt::sporbar, + ØkonomiXmlMottatt::versjon, + ) + lagretØkonomiXmlMottatt.versjon shouldBe 1 + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av ØkonomiXmlMottatt i basen`() { + økonomiXmlMottattRepository.insert(økonomiXmlMottatt) + var lagretØkonomiXmlMottatt = økonomiXmlMottattRepository.findByIdOrThrow(økonomiXmlMottatt.id) + val oppdatertØkonomiXmlMottatt = lagretØkonomiXmlMottatt.copy(eksternFagsakId = "bob") + + økonomiXmlMottattRepository.update(oppdatertØkonomiXmlMottatt) + + lagretØkonomiXmlMottatt = økonomiXmlMottattRepository.findByIdOrThrow(økonomiXmlMottatt.id) + lagretØkonomiXmlMottatt.shouldBeEqualToComparingFieldsExcept( + oppdatertØkonomiXmlMottatt, + ØkonomiXmlMottatt::sporbar, + ØkonomiXmlMottatt::versjon, + ) + lagretØkonomiXmlMottatt.versjon shouldBe 2 + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/micrometer/TellerServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/micrometer/TellerServiceTest.kt new file mode 100644 index 000000000..d95bd709a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/micrometer/TellerServiceTest.kt @@ -0,0 +1,127 @@ +package no.nav.familie.tilbake.micrometer + +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.micrometer.domain.MeldingstellingRepository +import no.nav.familie.tilbake.micrometer.domain.Meldingstype +import no.nav.familie.tilbake.micrometer.domain.Mottaksstatus +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class TellerServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var tellerService: TellerService + + @Autowired + private lateinit var meldingstellingRepository: MeldingstellingRepository + + @Test + fun `tellKobletKravgrunnlag oppretter ny forekomst ved dagnes første telling`() { + tellerService.tellKobletKravgrunnlag(fagsystem = Fagsystem.EF) + + val meldingstelling = meldingstellingRepository.findByFagsystemAndTypeAndStatusAndDato( + Fagsystem.EF, + Meldingstype.KRAVGRUNNLAG, + Mottaksstatus.KOBLET, + ) + + meldingstelling!!.antall shouldBe 1 + } + + @Test + fun `tellUkobletKravgrunnlag oppretter ny forekomst ved dagnes første telling`() { + tellerService.tellUkobletKravgrunnlag(fagsystem = Fagsystem.EF) + + val meldingstelling = meldingstellingRepository.findByFagsystemAndTypeAndStatusAndDato( + Fagsystem.EF, + Meldingstype.KRAVGRUNNLAG, + Mottaksstatus.UKOBLET, + ) + + meldingstelling!!.antall shouldBe 1 + } + + @Test + fun `tellKobletStatusmelding oppretter ny forekomst ved dagnes første telling`() { + tellerService.tellKobletStatusmelding(fagsystem = Fagsystem.EF) + + val meldingstelling = meldingstellingRepository.findByFagsystemAndTypeAndStatusAndDato( + Fagsystem.EF, + Meldingstype.STATUSMELDING, + Mottaksstatus.KOBLET, + ) + + meldingstelling!!.antall shouldBe 1 + } + + @Test + fun `tellUkobletStatusmelding oppretter ny forekomst ved dagnes første telling`() { + tellerService.tellUkobletStatusmelding(fagsystem = Fagsystem.EF) + + val meldingstelling = meldingstellingRepository.findByFagsystemAndTypeAndStatusAndDato( + Fagsystem.EF, + Meldingstype.STATUSMELDING, + Mottaksstatus.UKOBLET, + ) + + meldingstelling!!.antall shouldBe 1 + } + + @Test + fun `tellKobletKravgrunnlag oppdaterer eksisterende teller ved påfølgende tellinger`() { + tellerService.tellKobletKravgrunnlag(fagsystem = Fagsystem.EF) + tellerService.tellKobletKravgrunnlag(fagsystem = Fagsystem.EF) + + val meldingstelling = meldingstellingRepository.findByFagsystemAndTypeAndStatusAndDato( + Fagsystem.EF, + Meldingstype.KRAVGRUNNLAG, + Mottaksstatus.KOBLET, + ) + + meldingstelling!!.antall shouldBe 2 + } + + @Test + fun `tellUkobletKravgrunnlag oppdaterer eksisterende teller ved påfølgende tellinger`() { + tellerService.tellUkobletKravgrunnlag(fagsystem = Fagsystem.EF) + tellerService.tellUkobletKravgrunnlag(fagsystem = Fagsystem.EF) + + val meldingstelling = meldingstellingRepository.findByFagsystemAndTypeAndStatusAndDato( + Fagsystem.EF, + Meldingstype.KRAVGRUNNLAG, + Mottaksstatus.UKOBLET, + ) + + meldingstelling!!.antall shouldBe 2 + } + + @Test + fun `tellKobletStatusmelding oppdaterer eksisterende teller ved påfølgende tellinger`() { + tellerService.tellKobletStatusmelding(fagsystem = Fagsystem.EF) + tellerService.tellKobletStatusmelding(fagsystem = Fagsystem.EF) + + val meldingstelling = meldingstellingRepository.findByFagsystemAndTypeAndStatusAndDato( + Fagsystem.EF, + Meldingstype.STATUSMELDING, + Mottaksstatus.KOBLET, + ) + + meldingstelling!!.antall shouldBe 2 + } + + @Test + fun `tellUkobletStatusmelding oppdaterer eksisterende teller ved påfølgende tellinger`() { + tellerService.tellUkobletStatusmelding(fagsystem = Fagsystem.EF) + tellerService.tellUkobletStatusmelding(fagsystem = Fagsystem.EF) + + val meldingstelling = meldingstellingRepository.findByFagsystemAndTypeAndStatusAndDato( + Fagsystem.EF, + Meldingstype.STATUSMELDING, + Mottaksstatus.UKOBLET, + ) + + meldingstelling!!.antall shouldBe 2 + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/LagOppgaveTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/LagOppgaveTaskTest.kt new file mode 100644 index 000000000..87dc324f1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/LagOppgaveTaskTest.kt @@ -0,0 +1,176 @@ +package no.nav.familie.tilbake.oppgave + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.kontrakter.felles.oppgave.OppgavePrioritet +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandlingskontroll.BehandlingskontrollService +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate +import java.util.Properties + +internal class LagOppgaveTaskTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var behandlingskontrollService: BehandlingskontrollService + + private val mockOppgaveService: OppgaveService = mockk(relaxed = true) + private val mockIntegrasjonerClient = mockk(relaxed = true) + private val oppgavePrioritetService = mockk() + + private lateinit var lagOppgaveTask: LagOppgaveTask + + private val behandling: Behandling = Testdata.behandling + + private val dagensDato = LocalDate.now() + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(behandling) + + every { oppgavePrioritetService.utledOppgaveprioritet(any(), any()) } returns OppgavePrioritet.NORM + + lagOppgaveTask = LagOppgaveTask(mockOppgaveService, behandlingskontrollService, oppgavePrioritetService) + } + + @Test + fun `doTask skal lage oppgave når behandling venter på varsel steg`() { + lagBehandlingsstegstilstand(Behandlingssteg.VARSEL, Behandlingsstegstatus.VENTER, Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING) + val fristForFerdigstillelse = dagensDato.plusWeeks(Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING.defaultVenteTidIUker) + + lagOppgaveTask.doTask(lagTask()) + + verify { + mockOppgaveService.opprettOppgave( + behandling.id, + Oppgavetype.BehandleSak, + "enhet", + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING.beskrivelse, + fristForFerdigstillelse, + null, + OppgavePrioritet.NORM, + ) + } + } + + @Test + fun `doTask skal lage oppgave når behandling venter på grunnlag steg`() { + lagBehandlingsstegstilstand( + Behandlingssteg.GRUNNLAG, + Behandlingsstegstatus.VENTER, + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG, + ) + val fristForFerdigstillelse = dagensDato.plusWeeks(Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG.defaultVenteTidIUker) + + lagOppgaveTask.doTask(lagTask()) + + verify { + mockOppgaveService.opprettOppgave( + behandling.id, + Oppgavetype.BehandleSak, + "enhet", + Venteårsak.VENT_PÅ_TILBAKEKREVINGSGRUNNLAG.beskrivelse, + fristForFerdigstillelse, + null, + OppgavePrioritet.NORM, + ) + } + } + + @Test + fun `doTask skal lage oppgave når behandling er på FAKTA steg`() { + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.KLAR) + + lagOppgaveTask.doTask(lagTask()) + + verify { + mockOppgaveService.opprettOppgave( + behandlingId = behandling.id, + oppgavetype = Oppgavetype.BehandleSak, + enhet = "enhet", + beskrivelse = "", + fristForFerdigstillelse = dagensDato, + saksbehandler = null, + OppgavePrioritet.NORM, + ) + } + } + + @Test + fun `doTask skal lage oppgave med saksbehandler som sendte til beslutter i beskrivelse`() { + lagBehandlingsstegstilstand(Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + + val opprettetAv = "Saksbehandler Saksbehandlersen" + lagOppgaveTask.doTask(lagTask(opprettetAv)) + + verify { + mockOppgaveService.opprettOppgave( + behandlingId = behandling.id, + oppgavetype = Oppgavetype.BehandleSak, + enhet = "enhet", + beskrivelse = "Sendt til godkjenning av Saksbehandler Saksbehandlersen ", + fristForFerdigstillelse = dagensDato, + saksbehandler = null, + OppgavePrioritet.NORM, + ) + } + } + + private fun lagBehandlingsstegstilstand( + behandlingssteg: Behandlingssteg, + behandlingsstegsstatus: Behandlingsstegstatus, + venteårsak: Venteårsak? = null, + ) { + behandlingsstegstilstandRepository.insert( + Behandlingsstegstilstand( + behandlingId = behandling.id, + behandlingssteg = behandlingssteg, + behandlingsstegsstatus = behandlingsstegsstatus, + venteårsak = venteårsak, + tidsfrist = venteårsak?.let { + dagensDato.plusWeeks(it.defaultVenteTidIUker) + }, + ), + ) + } + + private fun lagTask(opprettetAv: String? = null): Task { + return Task( + type = LagOppgaveTask.TYPE, + payload = behandling.id.toString(), + properties = Properties().apply { + setProperty("oppgavetype", Oppgavetype.BehandleSak.name) + setProperty(PropertyName.ENHET, "enhet") + if (opprettetAv != null) { + setProperty("opprettetAv", opprettetAv) + } + }, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/OppdaterAnsvarligSaksbehandlerTaskTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/OppdaterAnsvarligSaksbehandlerTaskTest.kt new file mode 100644 index 000000000..e05998e8e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/OppdaterAnsvarligSaksbehandlerTaskTest.kt @@ -0,0 +1,102 @@ +package no.nav.familie.tilbake.oppgave + +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import no.nav.familie.kontrakter.felles.oppgave.OppgavePrioritet +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.config.PropertyName +import no.nav.familie.tilbake.data.Testdata +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.Properties + +internal class OppdaterAnsvarligSaksbehandlerTaskTest { + + private val behandlingRepository: BehandlingRepository = mockk(relaxed = true) + private val fagsakRepository: FagsakRepository = mockk(relaxed = true) + private val mockOppgaveService: OppgaveService = mockk(relaxed = true) + private val oppgavePrioritetService = mockk() + private val behandling: Behandling = Testdata.behandling + + private val oppdaterAnsvarligSaksbehandlerTask = + OppdaterAnsvarligSaksbehandlerTask(mockOppgaveService, behandlingRepository, oppgavePrioritetService) + + @BeforeEach + fun init() { + clearMocks(mockOppgaveService) + every { fagsakRepository.findByIdOrThrow(Testdata.fagsak.id) } returns Testdata.fagsak + every { behandlingRepository.findByIdOrThrow(Testdata.behandling.id) } returns Testdata.behandling + every { oppgavePrioritetService.utledOppgaveprioritet(any(), any()) } returns OppgavePrioritet.NORM + } + + @Test + fun `doTask skal oppdatere oppgave når prioritet endret`() { + val oppgave = Oppgave(tilordnetRessurs = behandling.ansvarligSaksbehandler, prioritet = OppgavePrioritet.NORM) + + every { oppgavePrioritetService.utledOppgaveprioritet(any(), any()) } returns OppgavePrioritet.HOY + every { mockOppgaveService.finnOppgaveForBehandlingUtenOppgaveType(behandling.id) } returns oppgave + + oppdaterAnsvarligSaksbehandlerTask.doTask(lagTask()) + + verify { + mockOppgaveService.patchOppgave( + oppgave.copy( + tilordnetRessurs = behandling.ansvarligSaksbehandler, + prioritet = OppgavePrioritet.HOY, + ), + ) + } + } + + @Test + fun `doTask skal oppdatere oppgave når saksbehandler endret`() { + val oppgave = Oppgave(tilordnetRessurs = "TIDLIGERE saksbehandler", prioritet = OppgavePrioritet.NORM) + every { oppgavePrioritetService.utledOppgaveprioritet(any(), any()) } returns OppgavePrioritet.NORM + every { mockOppgaveService.finnOppgaveForBehandlingUtenOppgaveType(behandling.id) } returns oppgave + + oppdaterAnsvarligSaksbehandlerTask.doTask(lagTask()) + + verify(atLeast = 1) { + mockOppgaveService.patchOppgave( + oppgave.copy( + tilordnetRessurs = behandling.ansvarligSaksbehandler, + prioritet = OppgavePrioritet.NORM, + ), + ) + } + } + + @Test + fun `Skal ikke oppdatere oppgave når ingenting er endret`() { + val oppgave = Oppgave(tilordnetRessurs = behandling.ansvarligSaksbehandler, prioritet = OppgavePrioritet.NORM) + + every { oppgavePrioritetService.utledOppgaveprioritet(any(), any()) } returns OppgavePrioritet.NORM + every { mockOppgaveService.finnOppgaveForBehandlingUtenOppgaveType(behandling.id) } returns oppgave + + oppdaterAnsvarligSaksbehandlerTask.doTask(lagTask()) + + verify(exactly = 0) { mockOppgaveService.patchOppgave(any()) } + } + + private fun lagTask(opprettetAv: String? = null): Task { + return Task( + type = OppdaterAnsvarligSaksbehandlerTask.TYPE, + payload = behandling.id.toString(), + properties = Properties().apply { + setProperty("oppgavetype", Oppgavetype.BehandleSak.name) + setProperty(PropertyName.ENHET, "enhet") + if (opprettetAv != null) { + setProperty("opprettetAv", opprettetAv) + } + }, + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/OppgavePrioritetServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/OppgavePrioritetServiceTest.kt new file mode 100644 index 000000000..1f8f50ac7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/OppgavePrioritetServiceTest.kt @@ -0,0 +1,90 @@ +package no.nav.familie.tilbake.oppgave + +import io.mockk.every +import io.mockk.mockk +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import no.nav.familie.kontrakter.felles.oppgave.OppgavePrioritet +import no.nav.familie.tilbake.config.FeatureToggleService +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.data.Testdata.lagFeilBeløp +import no.nav.familie.tilbake.data.Testdata.lagYtelBeløp +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlag431 +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.YearMonth +import java.util.UUID + +internal class OppgavePrioritetServiceTest { + + val kravgrunnlagRepository = mockk() + val featureToggleService = mockk() + val oppgavePrioritetService = OppgavePrioritetService(kravgrunnlagRepository, featureToggleService) + + @Test + fun `skal gi prioritet LAV for feilutbetaling på 9999`() { + val behandlingId = UUID.randomUUID() + val oppgave = Oppgave() + + every { kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandlingId) } returns true + + every { kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) } returns lagKravgrunnlagMedFeilutbetaling( + 9999, + ) + + assertThat(oppgavePrioritetService.utledOppgaveprioritet(behandlingId, oppgave)).isEqualTo(OppgavePrioritet.LAV) + } + + @Test + fun `skal gi prioritet NORM for feilutbetaling på 30 000`() { + val behandlingId = UUID.randomUUID() + val oppgave = Oppgave() + + every { kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandlingId) } returns true + + every { kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) } returns lagKravgrunnlagMedFeilutbetaling( + 30_000, + ) + + assertThat(oppgavePrioritetService.utledOppgaveprioritet(behandlingId, oppgave)).isEqualTo(OppgavePrioritet.NORM) + } + + @Test + fun `skal gi prioritet HØY for feilutbetaling på 75 000`() { + val behandlingId = UUID.randomUUID() + val oppgave = Oppgave() + + every { kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandlingId) } returns true + + every { kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandlingId) } returns lagKravgrunnlagMedFeilutbetaling( + 75_000, + ) + + assertThat(oppgavePrioritetService.utledOppgaveprioritet(behandlingId, oppgave)).isEqualTo(OppgavePrioritet.HOY) + } + + @Test + fun `skal gi oppgavens eksisterende prioritet dersom det ikke finnes kravgrunnlag`() { + val behandlingId = UUID.randomUUID() + val oppgave = Oppgave(prioritet = OppgavePrioritet.HOY) + + every { kravgrunnlagRepository.existsByBehandlingIdAndAktivTrue(behandlingId) } returns false + + assertThat(oppgavePrioritetService.utledOppgaveprioritet(behandlingId, oppgave)).isEqualTo(OppgavePrioritet.HOY) + } + + private fun lagKravgrunnlagMedFeilutbetaling(feilutbetaling: Int): Kravgrunnlag431 { + val periode = Testdata.kravgrunnlagsperiode432.copy( + id = UUID.randomUUID(), + periode = Månedsperiode(YearMonth.of(2020, 1), YearMonth.of(2023, 1)), + beløp = setOf( + lagFeilBeløp(BigDecimal(feilutbetaling)), + lagYtelBeløp(BigDecimal(feilutbetaling), BigDecimal(10)), + ), + ) + + return Testdata.kravgrunnlag431.copy(perioder = setOf(periode)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/OppgaveServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/OppgaveServiceTest.kt new file mode 100644 index 000000000..3f9dbbbe4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/OppgaveServiceTest.kt @@ -0,0 +1,243 @@ +package no.nav.familie.tilbake.oppgave + +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.CapturingSlot +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.kontrakter.felles.oppgave.FinnOppgaveResponseDto +import no.nav.familie.kontrakter.felles.oppgave.MappeDto +import no.nav.familie.kontrakter.felles.oppgave.Oppgave +import no.nav.familie.kontrakter.felles.oppgave.OppgavePrioritet +import no.nav.familie.kontrakter.felles.oppgave.Oppgavetype +import no.nav.familie.kontrakter.felles.oppgave.OpprettOppgaveRequest +import no.nav.familie.prosessering.domene.Task +import no.nav.familie.prosessering.internal.TaskService +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata.behandling +import no.nav.familie.tilbake.data.Testdata.fagsak +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import no.nav.familie.tilbake.person.PersonService +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.core.env.Environment +import java.time.LocalDate +import java.util.Properties + +class OppgaveServiceTest { + + private val behandlingRepository: BehandlingRepository = mockk(relaxed = true) + private val fagsakRepository: FagsakRepository = mockk(relaxed = true) + private val integrasjonerClient: IntegrasjonerClient = mockk(relaxed = true) + private val personService: PersonService = mockk(relaxed = true) + private val environment: Environment = mockk(relaxed = true) + private val taskService: TaskService = mockk(relaxed = true) + + private val mappeIdGodkjenneVedtak = 100 + private val mappeIdBehandleSak = 200 + private val finnMappeResponseDto = listOf( + MappeDto(300, "EF Sak - 50 Behandle sak", enhetsnr = "4489"), + MappeDto(mappeIdBehandleSak, "50 Tilbakekreving - Klar til behandling", enhetsnr = "4489"), + MappeDto(mappeIdGodkjenneVedtak, "70 Godkjennevedtak", enhetsnr = "4489"), + MappeDto(400, "EF Sak - 70 Godkjenne vedtak", enhetsnr = "4489"), + ) + + private lateinit var oppgaveService: OppgaveService + + @BeforeEach + fun setUp() { + clearMocks(integrasjonerClient) + oppgaveService = OppgaveService( + behandlingRepository, + fagsakRepository, + integrasjonerClient, + personService, + taskService, + environment, + ) + every { fagsakRepository.findByIdOrThrow(fagsak.id) } returns fagsak + every { behandlingRepository.findByIdOrThrow(behandling.id) } returns behandling + every { taskService.finnTasksMedStatus(any(), any(), any()) } returns emptyList() + } + + @Nested + inner class OpprettOppgave { + + @Test + fun `skal legge godkjenneVedtak i EF-Sak-70-mappe for enhet 4489`() { + val slot = CapturingSlot() + every { integrasjonerClient.finnMapper(any()) } returns finnMappeResponseDto + + oppgaveService.opprettOppgave( + behandling.id, + Oppgavetype.GodkjenneVedtak, + "4489", + "", + LocalDate.now().plusDays(5), + "bob", + OppgavePrioritet.NORM, + ) + + verify { integrasjonerClient.opprettOppgave(capture(slot)) } + slot.captured.mappeId shouldBe mappeIdGodkjenneVedtak + } + + @Test + fun `skal ikke legge oppgave for enhet 4483 i mappe`() { + val slot = CapturingSlot() + + oppgaveService.opprettOppgave( + behandling.id, + Oppgavetype.GodkjenneVedtak, + "4483", + "", + LocalDate.now().plusDays(5), + "bob", + OppgavePrioritet.NORM, + ) + + verify { integrasjonerClient.opprettOppgave(capture(slot)) } + slot.captured.mappeId shouldBe null + } + + @Test + fun `skal legge behandleSak i EF-Sak-50-mappe for 4489`() { + val slot = CapturingSlot() + every { integrasjonerClient.finnMapper("4489") } returns finnMappeResponseDto + + oppgaveService.opprettOppgave( + behandling.id, + Oppgavetype.BehandleSak, + "4489", + "", + LocalDate.now().plusDays(5), + "bob", + OppgavePrioritet.NORM, + ) + verify { integrasjonerClient.opprettOppgave(capture(slot)) } + + slot.captured.mappeId shouldBe mappeIdBehandleSak + } + + @Test + fun `skal ikke legge behandleSak i EF-Sak-50-mappe for verdi ulik 4489`() { + val slot = CapturingSlot() + every { integrasjonerClient.finnMapper("4489") } returns finnMappeResponseDto + + oppgaveService.opprettOppgave( + behandling.id, + Oppgavetype.BehandleSak, + "1578", + "", + LocalDate.now().plusDays(5), + "bob", + OppgavePrioritet.NORM, + ) + verify { integrasjonerClient.opprettOppgave(capture(slot)) } + + slot.captured.mappeId shouldBe null + } + + @Test + fun `skal ikke legge behandleSak i noen mappe når ingen mapper matcher`() { + val kunMapperSomIkkeKanBrukes = listOf( + MappeDto(300, "EF Sak - 50 Behandle sak", enhetsnr = "4489"), + MappeDto(400, "EF Sak - 70 Godkjenne vedtak", enhetsnr = "4489"), + ) + + val slot = CapturingSlot() + every { integrasjonerClient.finnMapper("4489") } returns kunMapperSomIkkeKanBrukes + + oppgaveService.opprettOppgave( + behandling.id, + Oppgavetype.BehandleSak, + "4489", + "", + LocalDate.now().plusDays(5), + "bob", + OppgavePrioritet.NORM, + ) + verify { integrasjonerClient.opprettOppgave(capture(slot)) } + + slot.captured.mappeId shouldBe null + } + + @Test + fun `skal fungere også etter rettet skrivefeil i gosys `() { + val mapperMedOrdelingsfeilRettet = listOf( + MappeDto(300, "50 Behandle sak", enhetsnr = "4489"), + MappeDto(400, "70 Godkjenne vedtak ", enhetsnr = "4489"), // ligger i gosys som Godkjennevedtak 2022-09-01 + ) + + val slot = CapturingSlot() + every { integrasjonerClient.finnMapper("4489") } returns mapperMedOrdelingsfeilRettet + + oppgaveService.opprettOppgave( + behandling.id, + Oppgavetype.GodkjenneVedtak, + "4489", + "", + LocalDate.now().plusDays(5), + "bob", + OppgavePrioritet.NORM, + ) + verify { integrasjonerClient.opprettOppgave(capture(slot)) } + + slot.captured.mappeId shouldBe 400 + } + + @Test + fun `skal ikke legge godkjenneVedtak oppgaver i EF-Sak-50-mappe når det allerede finnes en`() { + every { integrasjonerClient.finnMapper("4489") } returns finnMappeResponseDto + every { integrasjonerClient.finnOppgaver(any()) } returns FinnOppgaveResponseDto(1L, listOf(Oppgave())) + + val exception = shouldThrow { + oppgaveService.opprettOppgave( + behandling.id, + Oppgavetype.GodkjenneVedtak, + "4483", + "", + LocalDate.now().plusDays(5), + "bob", + OppgavePrioritet.NORM, + ) + } + exception.message shouldBe "Det finnes allerede en oppgave ${Oppgavetype.GodkjenneVedtak} " + + "for behandling ${behandling.id} og finnes ikke noen ferdigstilleoppgaver. " + + "Eksisterende oppgaven ${Oppgavetype.GodkjenneVedtak} må lukke først." + } + + @Test + fun `skal legge godkjenneVedtak oppgaver når det allerede finnes en og har en åpen ferdigstilloppgave task`() { + val slot = CapturingSlot() + + every { integrasjonerClient.finnMapper("4489") } returns finnMappeResponseDto + every { integrasjonerClient.finnOppgaver(any()) } returns FinnOppgaveResponseDto(1L, listOf(Oppgave())) + val properties = Properties().apply { setProperty("oppgavetype", Oppgavetype.GodkjenneVedtak.name) } + every { taskService.finnTasksMedStatus(any(), any(), any()) } returns + listOf(Task(type = FerdigstillOppgaveTask.TYPE, payload = behandling.id.toString(), properties = properties)) + + shouldNotThrow { + oppgaveService.opprettOppgave( + behandling.id, + Oppgavetype.GodkjenneVedtak, + "4489", + "", + LocalDate.now().plusDays(5), + "bob", + OppgavePrioritet.NORM, + ) + } + + verify { integrasjonerClient.opprettOppgave(capture(slot)) } + + slot.captured.mappeId shouldBe mappeIdGodkjenneVedtak + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/OppgaveServiceTestConfig.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/OppgaveServiceTestConfig.kt new file mode 100644 index 000000000..105b7fe85 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/oppgave/OppgaveServiceTestConfig.kt @@ -0,0 +1,16 @@ +package no.nav.familie.tilbake.oppgave + +import io.mockk.mockk +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile + +class OppgaveServiceTestConfig { + + @Bean + @Profile("mock-oppgave") + @Primary + fun mockArbeidsfordelingService(): OppgaveService { + return mockk(relaxed = true) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/sikkerhet/TilgangAdviceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/sikkerhet/TilgangAdviceTest.kt new file mode 100644 index 000000000..438574917 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/sikkerhet/TilgangAdviceTest.kt @@ -0,0 +1,656 @@ +package no.nav.familie.tilbake.sikkerhet + +import io.jsonwebtoken.Jwts +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import no.nav.familie.kontrakter.felles.Fagsystem +import no.nav.familie.kontrakter.felles.Språkkode +import no.nav.familie.kontrakter.felles.tilbakekreving.Faktainfo +import no.nav.familie.kontrakter.felles.tilbakekreving.OpprettTilbakekrevingRequest +import no.nav.familie.kontrakter.felles.tilbakekreving.Tilbakekrevingsvalg +import no.nav.familie.kontrakter.felles.tilbakekreving.Varsel +import no.nav.familie.kontrakter.felles.tilbakekreving.Ytelsestype +import no.nav.familie.kontrakter.felles.tilgangskontroll.Tilgang +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.BehandlingPåVentDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegFaktaDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegFatteVedtaksstegDto +import no.nav.familie.tilbake.api.dto.BrukerDto +import no.nav.familie.tilbake.api.dto.FagsakDto +import no.nav.familie.tilbake.api.dto.HentFagsystemsbehandlingRequestDto +import no.nav.familie.tilbake.api.dto.HentForhåndvisningVedtaksbrevPdfDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandling.domain.Behandling +import no.nav.familie.tilbake.behandling.domain.Behandlingstype +import no.nav.familie.tilbake.behandling.domain.Bruker +import no.nav.familie.tilbake.behandling.domain.Fagsak +import no.nav.familie.tilbake.behandlingskontroll.domain.Venteårsak +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.config.RolleConfig +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.integration.familie.IntegrasjonerClient +import no.nav.familie.tilbake.integration.pdl.internal.Kjønn +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.ØkonomiXmlMottattRepository +import no.nav.security.token.support.core.context.TokenValidationContext +import no.nav.security.token.support.core.jwt.JwtToken +import no.nav.security.token.support.spring.SpringTokenValidationContextHolder +import org.aspectj.lang.JoinPoint +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpMethod +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.test.context.TestPropertySource +import org.springframework.web.context.request.RequestContextHolder +import org.springframework.web.context.request.ServletRequestAttributes +import java.math.BigDecimal +import java.time.LocalDate +import java.util.Calendar + +@TestPropertySource( + properties = [ + "rolle.barnetrygd.beslutter=bb123", + "rolle.barnetrygd.saksbehandler=bs123", + "rolle.barnetrygd.veileder=bv123", + "rolle.enslig.beslutter=eb123", + "rolle.enslig.saksbehandler=es123", + "rolle.enslig.veileder=ev123", + "rolle.kontantstøtte.beslutter = kb123", + "rolle.kontantstøtte.saksbehandler = ks123", + "rolle.kontantstøtte.veileder = kv123", + "rolle.teamfamilie.forvalter = familie123", + ], +) +internal class TilgangAdviceTest : OppslagSpringRunnerTest() { + + companion object { + + const val BARNETRYGD_BESLUTTER_ROLLE = "bb123" + const val BARNETRYGD_SAKSBEHANDLER_ROLLE = "bs123" + const val BARNETRYGD_VEILEDER_ROLLE = "bv123" + + const val ENSLIG_BESLUTTER_ROLLE = "eb123" + const val ENSLIG_SAKSBEHANDLER_ROLLE = "es123" + + const val TEAMFAMILIE_FORVALTER_ROLLE = "familie123" + } + + @AfterEach + fun tearDown() { + RequestContextHolder.currentRequestAttributes().removeAttribute(SpringTokenValidationContextHolder::class.java.name, 0) + } + + private lateinit var tilgangAdvice: TilgangAdvice + + @Autowired + private lateinit var rolleConfig: RolleConfig + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var mottattXmlRepository: ØkonomiXmlMottattRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + private val auditLogger: AuditLogger = mockk(relaxed = true) + private val personIdent: String = "1232" + private val mockJoinpoint: JoinPoint = mockk() + private val mockIntegrasjonerClient: IntegrasjonerClient = mockk() + + private lateinit var fagsak: Fagsak + private lateinit var behandling: Behandling + + val opprettTilbakekrevingRequest = + OpprettTilbakekrevingRequest( + ytelsestype = Ytelsestype.BARNETRYGD, + fagsystem = Fagsystem.BA, + eksternFagsakId = "123", + personIdent = "123434", + eksternId = "123", + manueltOpprettet = false, + enhetId = "8020", + enhetsnavn = "Oslo", + revurderingsvedtaksdato = LocalDate.now(), + varsel = Varsel("hello", BigDecimal.valueOf(1000), emptyList()), + faktainfo = Faktainfo( + "testårsak", + "testresultat", + Tilbakekrevingsvalg.OPPRETT_TILBAKEKREVING_MED_VARSEL, + ), + saksbehandlerIdent = "bob", + ) + + @BeforeEach + fun init() { + tilgangAdvice = TilgangAdvice( + rolleConfig, + fagsakRepository, + behandlingRepository, + kravgrunnlagRepository, + auditLogger, + mottattXmlRepository, + mockIntegrasjonerClient, + ) + + fagsak = fagsakRepository.insert( + Fagsak( + bruker = Bruker("1232"), + eksternFagsakId = "123", + fagsystem = Fagsystem.BA, + ytelsestype = Ytelsestype.BARNETRYGD, + ), + ) + behandling = behandlingRepository.insert( + Behandling( + fagsakId = fagsak.id, + type = Behandlingstype.TILBAKEKREVING, + ansvarligSaksbehandler = Constants.BRUKER_ID_VEDTAKSLØSNINGEN, + behandlendeEnhet = "8020", + behandlendeEnhetsNavn = "Oslo", + manueltOpprettet = false, + ), + ) + } + + @Test + fun `sjekkTilgang skal sperre tilgang hvis person er kode 6`() { + every { mockJoinpoint.args } returns arrayOf(behandling.id) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, false, null)) + val rolletilgangssjekk = + Rolletilgangssjekk(Behandlerrolle.VEILEDER, "hent behandling", AuditLoggerEvent.ACCESS, HenteParam.BEHANDLING_ID) + val token = opprettToken("abc", listOf(BARNETRYGD_BESLUTTER_ROLLE)) + opprettRequestContext("/api/behandling/v1/$behandling.id", HttpMethod.GET, token) + + shouldThrow { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + .message shouldBe "abc har ikke tilgang til person i hent behandling" + } + + @Test + fun `sjekkTilgang skal gi tilgang hvis person ikke er kode 6`() { + every { mockJoinpoint.args } returns arrayOf(behandling.id) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + val rolletilgangssjekk = + Rolletilgangssjekk(Behandlerrolle.VEILEDER, "hent behandling", AuditLoggerEvent.ACCESS, HenteParam.BEHANDLING_ID) + val token = opprettToken("abc", listOf(BARNETRYGD_BESLUTTER_ROLLE)) + opprettRequestContext("/api/behandling/v1/$behandling.id", HttpMethod.GET, token) + + shouldNotThrowAny { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + } + + @Test + fun `sjekkTilgang skal ha tilgang for barnetrygd beslutter i barnetrygd hent behandling request`() { + val token = opprettToken("abc", listOf(BARNETRYGD_BESLUTTER_ROLLE)) + opprettRequestContext("/api/behandling/v1/$behandling.id", HttpMethod.GET, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf(behandling.id) + val rolletilgangssjekk = + Rolletilgangssjekk(Behandlerrolle.VEILEDER, "hent behandling", AuditLoggerEvent.ACCESS, HenteParam.BEHANDLING_ID) + + shouldNotThrowAny { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + } + + @Test + fun `sjekkTilgang skal ikke ha tilgang for enslig beslutter i barnetrygd hent behandling request`() { + val token = opprettToken("abc", listOf(ENSLIG_BESLUTTER_ROLLE)) + opprettRequestContext("/api/behandling/v1/$behandling.id", HttpMethod.GET, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true)) + every { mockJoinpoint.args } returns arrayOf(behandling.id) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.VEILEDER, + "barnetrygd hent behandling", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + + val exception = shouldThrow(block = { + tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) + }) + + exception.message shouldBe "abc har ikke tilgang til ${rolletilgangssjekk.handling}" + } + + @Test + fun `sjekkTilgang skal ikke ha tilgang for barnetrygd veileder i barnetrygd opprett behandling request`() { + val token = opprettToken("abc", listOf(BARNETRYGD_VEILEDER_ROLLE)) + opprettRequestContext("/api/behandling/v1", HttpMethod.POST, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true)) + every { mockJoinpoint.args } returns arrayOf(opprettTilbakekrevingRequest) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "barnetrygd opprett behandling", + AuditLoggerEvent.ACCESS, + HenteParam.INGEN, + ) + + val exception = shouldThrow(block = { + tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) + }) + + exception.message shouldBe "abc med rolle VEILEDER har ikke tilgang til å barnetrygd opprett behandling. " + + "Krever ${rolletilgangssjekk.minimumBehandlerrolle}." + } + + @Test + fun `sjekkTilgang skal ha tilgang i barnetrygd opprett behandling request når bruker både er beslutter og veileder`() { + val token = opprettToken("abc", listOf(BARNETRYGD_BESLUTTER_ROLLE, BARNETRYGD_VEILEDER_ROLLE)) + opprettRequestContext("/api/behandling/v1", HttpMethod.POST, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(emptyList()) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf(opprettTilbakekrevingRequest) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "barnetrygd opprett behandling", + AuditLoggerEvent.ACCESS, + HenteParam.INGEN, + ) + + shouldNotThrowAny { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + } + + @Test + fun `sjekkTilgang skal ha tilgang i hent behandling request når saksbehandler har tilgang til enslig og barnetrygd`() { + val token = opprettToken("abc", listOf(ENSLIG_SAKSBEHANDLER_ROLLE, BARNETRYGD_SAKSBEHANDLER_ROLLE)) + opprettRequestContext("/api/behandling/v1/$behandling.id", HttpMethod.GET, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf(behandling.id) + val rolletilgangssjekk = + Rolletilgangssjekk(Behandlerrolle.VEILEDER, "hent behandling", AuditLoggerEvent.ACCESS, HenteParam.BEHANDLING_ID) + + shouldNotThrowAny { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + } + + @Test + fun `sjekkTilgang skal ha tilgang i hent behandling request når bruker er fagsystem`() { + val token = opprettToken(Constants.BRUKER_ID_VEDTAKSLØSNINGEN, listOf()) + opprettRequestContext("/api/behandling/v1/$behandling.id", HttpMethod.GET, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf(behandling.id) + val rolletilgangssjekk = + Rolletilgangssjekk(Behandlerrolle.VEILEDER, "hent behandling", AuditLoggerEvent.ACCESS, HenteParam.BEHANDLING_ID) + + shouldNotThrowAny { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + } + + @Test + fun `sjekkTilgang skal ikke ha tilgang i hent behandling request når bruker er ukjent`() { + val token = opprettToken("abc", listOf()) + opprettRequestContext("/api/behandling/v1/$behandling.id", HttpMethod.GET, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf(behandling.id) + val rolletilgangssjekk = + Rolletilgangssjekk(Behandlerrolle.VEILEDER, "hent behandling", AuditLoggerEvent.ACCESS, HenteParam.BEHANDLING_ID) + + val exception = shouldThrow(block = { + tilgangAdvice.sjekkTilgang( + mockJoinpoint, + rolletilgangssjekk, + ) + }) + + exception.message shouldBe "Bruker har mangler tilgang til hent behandling" + } + + @Test + fun `sjekkTilgang skal saksbehandler ha tilgang i Fakta utførBehandlingssteg POST request`() { + val token = opprettToken("abc", listOf(BARNETRYGD_SAKSBEHANDLER_ROLLE)) + // POST request uten body + opprettRequestContext("/api/behandling/$behandling.id/steg/v1/", HttpMethod.POST, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf(behandling.id, BehandlingsstegFaktaDto(emptyList(), "testverdi")) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Håndterer behandlingens aktiv steg og fortsetter den til neste steg", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + + shouldNotThrowAny { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + } + + @Test + fun `sjekkTilgang skal saksbehandler ikke ha tilgang i Fattevedtak utførBehandlingssteg POST request`() { + val token = opprettToken("abc", listOf(BARNETRYGD_SAKSBEHANDLER_ROLLE)) + // POST request uten body + opprettRequestContext("/api/behandling/$behandling.id/steg/v1/", HttpMethod.POST, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf(behandling.id, BehandlingsstegFatteVedtaksstegDto(emptyList())) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Håndterer behandlingens aktiv steg og fortsetter den til neste steg", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + + val exception = shouldThrow { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + + exception.message shouldBe "abc med rolle SAKSBEHANDLER har ikke tilgang til å Håndterer behandlingens aktiv " + + "steg og fortsetter den til neste steg. Krever BESLUTTER." + } + + @Test + fun `sjekkTilgang skal beslutter ha tilgang i Fattevedtak utførBehandlingssteg POST request`() { + val token = opprettToken("abc", listOf(BARNETRYGD_BESLUTTER_ROLLE)) + // POST request uten body + opprettRequestContext("/api/behandling/$behandling.id/steg/v1/", HttpMethod.POST, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf(behandling.id, BehandlingsstegFatteVedtaksstegDto(emptyList())) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Håndterer behandlingens aktiv steg og fortsetter den til neste steg", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + + shouldNotThrowAny { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + } + + @Test + fun `sjekkTilgang skal ha tilgang i sett behandling på vent PUT request`() { + val token = opprettToken("abc", listOf(BARNETRYGD_SAKSBEHANDLER_ROLLE)) + opprettRequestContext("/api/behandling/$behandling.id/vent/v1/", HttpMethod.PUT, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf( + behandling.id, + BehandlingPåVentDto( + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + LocalDate.now().plusWeeks(2), + ), + ) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Setter behandling på vent", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + + shouldNotThrowAny { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + } + + @Test + fun `sjekkTilgang skal forvalter ikke ha tilgang til vanlig tjenester`() { + val token = opprettToken("abc", listOf(TEAMFAMILIE_FORVALTER_ROLLE)) + opprettRequestContext("/api/behandling/$behandling.id/vent/v1/", HttpMethod.PUT, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf( + behandling.id, + BehandlingPåVentDto( + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + LocalDate.now().plusWeeks(2), + ), + ) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Setter behandling på vent", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + + val exception = shouldThrow { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + + exception.message shouldBe "abc med rolle FORVALTER har ikke tilgang til å Setter behandling på vent." + + " Krever SAKSBEHANDLER." + } + + @Test + fun `sjekkTilgang skal saksbehandler ikke ha tilgang til forvaltningstjenester`() { + val token = opprettToken("abc", listOf(BARNETRYGD_SAKSBEHANDLER_ROLLE)) + opprettRequestContext("/api/forvaltning//behandling/$behandling.id/tving-henleggelse/v1", HttpMethod.PUT, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf(behandling.id) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.FORVALTER, + "Tving henlegger behandling", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + + val exception = shouldThrow { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + + exception.message shouldBe "abc med rolle SAKSBEHANDLER har ikke tilgang til å kalle forvaltningstjeneste " + + "Tving henlegger behandling. Krever FORVALTER." + } + + @Test + fun `sjekkTilgang skal saksbehandler ha tilgang til forvaltningstjenester hvis saksbehandler har forvalter rolle også`() { + val token = opprettToken("abc", listOf(BARNETRYGD_SAKSBEHANDLER_ROLLE, TEAMFAMILIE_FORVALTER_ROLLE)) + opprettRequestContext("/api/forvaltning//behandling/$behandling.id/tving-henleggelse/v1", HttpMethod.PUT, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf(behandling.id) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.FORVALTER, + "Tving henlegger behandling", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + + shouldNotThrowAny { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + } + + @Test + fun `sjekkTilgang skal saksbehandler ha tilgang til vanlig tjenester selv om saksbehandler har forvalter rolle også`() { + val token = opprettToken("abc", listOf(BARNETRYGD_SAKSBEHANDLER_ROLLE, TEAMFAMILIE_FORVALTER_ROLLE)) + opprettRequestContext("/api/behandling/$behandling.id/vent/v1/", HttpMethod.PUT, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf( + behandling.id, + BehandlingPåVentDto( + Venteårsak.VENT_PÅ_BRUKERTILBAKEMELDING, + LocalDate.now().plusWeeks(2), + ), + ) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Setter behandling på vent", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + + shouldNotThrowAny { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + } + + @Test + fun `sjekkTilgang skal forvalter ha tilgang til forvaltningstjenester`() { + val token = opprettToken("abc", listOf(TEAMFAMILIE_FORVALTER_ROLLE)) + // POST request uten body + opprettRequestContext("/api/forvaltning//behandling/$behandling.id/tving-henleggelse/v1", HttpMethod.PUT, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf(behandling.id) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.FORVALTER, + "Tving henlegger behandling", + AuditLoggerEvent.ACCESS, + HenteParam.BEHANDLING_ID, + ) + + shouldNotThrowAny { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + } + + @Test + fun `sjekkTilgang skal forvalter ha tilgang til forvaltningstjeneste arkiver mottattXml med input som mottattXmlId`() { + val token = opprettToken("abc", listOf(TEAMFAMILIE_FORVALTER_ROLLE)) + val økonomiXmlMottatt = mottattXmlRepository.insert(Testdata.økonomiXmlMottatt) + // PUT request uten body + opprettRequestContext("/arkiver/kravgrunnlag/${økonomiXmlMottatt.id}/v1", HttpMethod.PUT, token) + every { mockJoinpoint.args } returns arrayOf(økonomiXmlMottatt.id) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.FORVALTER, + "Arkiverer mottatt kravgrunnlag", + AuditLoggerEvent.ACCESS, + HenteParam.MOTTATT_XML_ID, + ) + + shouldNotThrowAny { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + } + + @Test + fun `sjekkTilgang skal forvalter ha tilgang til forvaltningstjeneste annuler kravgrunnlag med input som eksternKravgrunnlagId`() { + val token = opprettToken("abc", listOf(TEAMFAMILIE_FORVALTER_ROLLE)) + val økonomiXmlMottatt = mottattXmlRepository.insert(Testdata.økonomiXmlMottatt) + // PUT request uten body + opprettRequestContext("/annuler/kravgrunnlag/${økonomiXmlMottatt.eksternKravgrunnlagId}/v1", HttpMethod.PUT, token) + every { mockJoinpoint.args } returns arrayOf(økonomiXmlMottatt.eksternKravgrunnlagId) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.FORVALTER, + "Annulerer mottatt kravgrunnlag", + AuditLoggerEvent.ACCESS, + HenteParam.EKSTERN_KRAVGRUNNLAG_ID, + ) + + shouldNotThrowAny { tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) } + } + + @Test + fun `sjekkTilgang skal finne personer basert på ytelsestype og eksternFagsakId for henteparam`() { + val token = opprettToken("abc", listOf(BARNETRYGD_SAKSBEHANDLER_ROLLE)) + opprettRequestContext("dummy", HttpMethod.PUT, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf(Ytelsestype.BARNETRYGD, fagsak.eksternFagsakId) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Setter behandling på vent", + AuditLoggerEvent.ACCESS, + HenteParam.YTELSESTYPE_OG_EKSTERN_FAGSAK_ID, + ) + + tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) + + verify { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } + } + + @Test + fun `sjekkTilgang skal finne personer basert på dto med ytelsestype og eksternFagsakId`() { + val token = opprettToken("abc", listOf(BARNETRYGD_SAKSBEHANDLER_ROLLE)) + opprettRequestContext("dummy", HttpMethod.PUT, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns + arrayOf(HentFagsystemsbehandlingRequestDto(Ytelsestype.BARNETRYGD, fagsak.eksternFagsakId, "")) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Setter behandling på vent", + AuditLoggerEvent.ACCESS, + HenteParam.INGEN, + ) + + tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) + + verify { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } + } + + @Test + fun `sjekkTilgang skal finne personer basert på fagsystem og eksternFagsakId for henteparam`() { + val token = opprettToken("abc", listOf(BARNETRYGD_SAKSBEHANDLER_ROLLE)) + opprettRequestContext("dummy", HttpMethod.PUT, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns arrayOf(Fagsystem.BA, fagsak.eksternFagsakId) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Setter behandling på vent", + AuditLoggerEvent.ACCESS, + HenteParam.FAGSYSTEM_OG_EKSTERN_FAGSAK_ID, + ) + + tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) + + verify { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } + } + + @Test + fun `sjekkTilgang skal finne personer basert på dto med fagsystem og eksternFagsakId`() { + val token = opprettToken("abc", listOf(BARNETRYGD_SAKSBEHANDLER_ROLLE)) + opprettRequestContext("dummy", HttpMethod.PUT, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns + arrayOf( + FagsakDto( + fagsak.eksternFagsakId, + Ytelsestype.BARNETRYGD, + Fagsystem.BA, + Språkkode.NB, + BrukerDto("", "", LocalDate.now(), Kjønn.KVINNE), + listOf(), + ), + ) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Setter behandling på vent", + AuditLoggerEvent.ACCESS, + HenteParam.INGEN, + ) + + tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) + + verify { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } + } + + @Test + fun `sjekkTilgang skal finne personer basert på dto med behandlingId`() { + val token = opprettToken("abc", listOf(BARNETRYGD_SAKSBEHANDLER_ROLLE)) + opprettRequestContext("dummy", HttpMethod.PUT, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns + arrayOf(HentForhåndvisningVedtaksbrevPdfDto(behandlingId = behandling.id, perioderMedTekst = listOf())) + val rolletilgangssjekk = Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Setter behandling på vent", + AuditLoggerEvent.ACCESS, + HenteParam.INGEN, + ) + + tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) + + verify { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } + } + + @Test + fun `sjekkTilgang skal finne personer basert på dto med eksternBrukId`() { + val token = opprettToken("abc", listOf(BARNETRYGD_SAKSBEHANDLER_ROLLE)) + opprettRequestContext("dummy", HttpMethod.PUT, token) + every { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } returns listOf(Tilgang(personIdent, true, null)) + every { mockJoinpoint.args } returns + arrayOf(object { + @Suppress("unused") + val eksternBrukId = behandling.eksternBrukId + }) + val rolletilgangssjekk = + Rolletilgangssjekk( + Behandlerrolle.SAKSBEHANDLER, + "Setter behandling på vent", + AuditLoggerEvent.ACCESS, + HenteParam.INGEN, + ) + + tilgangAdvice.sjekkTilgang(mockJoinpoint, rolletilgangssjekk) + + verify { mockIntegrasjonerClient.sjekkTilgangTilPersoner(listOf("1232")) } + } + + private fun opprettToken(behandlerNavn: String, gruppeNavn: List): String { + val additionalParameters = mapOf("NAVident" to behandlerNavn, "groups" to gruppeNavn) + val calendar = Calendar.getInstance() + calendar.add(Calendar.SECOND, 60) + return Jwts.builder().setExpiration(calendar.time) + .setIssuer("azuread") + .addClaims(additionalParameters).compact() + } + + private fun opprettRequestContext(requestUri: String, requestMethod: HttpMethod, token: String) { + val mockHttpServletRequest = + MockHttpServletRequest(requestMethod.name(), requestUri) + + RequestContextHolder.setRequestAttributes(ServletRequestAttributes(mockHttpServletRequest)) + val tokenValidationContext = TokenValidationContext(mapOf("azuread" to JwtToken(token))) + RequestContextHolder.currentRequestAttributes() + .setAttribute(SpringTokenValidationContextHolder::class.java.name, tokenValidationContext, 0) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/totrinn/TotrinnServiceTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/totrinn/TotrinnServiceTest.kt new file mode 100644 index 000000000..39e9f1db9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/totrinn/TotrinnServiceTest.kt @@ -0,0 +1,199 @@ +package no.nav.familie.tilbake.totrinn + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.Totrinnsstegsinfo +import no.nav.familie.tilbake.api.dto.VurdertTotrinnDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.data.Testdata +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class TotrinnServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var totrinnService: TotrinnService + + private val fagsak = Testdata.fagsak + private val behandling = Testdata.behandling + private val behandlingId = behandling.id + + @BeforeEach + fun init() { + fagsakRepository.insert(fagsak) + behandlingRepository.insert(behandling) + } + + @Test + fun `hentTotrinnsvurderinger skal hente totrinnsvurdering for det første gang`() { + lagBehandlingsstegstilstand(Behandlingssteg.VARSEL, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + + val totrinnsvurderingDto = totrinnService.hentTotrinnsvurderinger(behandlingId) + + val totrinnsstegsinfo = totrinnsvurderingDto.totrinnsstegsinfo + totrinnsstegsinfo.shouldContainExactly( + Totrinnsstegsinfo(Behandlingssteg.FAKTA, null, null), + Totrinnsstegsinfo(Behandlingssteg.FORELDELSE, null, null), + Totrinnsstegsinfo(Behandlingssteg.VILKÅRSVURDERING, null, null), + Totrinnsstegsinfo(Behandlingssteg.FORESLÅ_VEDTAK, null, null), + ) + } + + @Test + fun `hentTotrinnsvurderinger skal hente totrinnsvurdering etter beslutters vurdering`() { + lagBehandlingsstegstilstand(Behandlingssteg.VARSEL, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + + totrinnService.lagreTotrinnsvurderinger( + behandlingId, + listOf( + VurdertTotrinnDto( + behandlingssteg = Behandlingssteg.FAKTA, + godkjent = true, + begrunnelse = "testverdi", + ), + VurdertTotrinnDto( + behandlingssteg = Behandlingssteg.FORELDELSE, + godkjent = true, + begrunnelse = "testverdi", + ), + VurdertTotrinnDto( + behandlingssteg = Behandlingssteg.VILKÅRSVURDERING, + godkjent = false, + begrunnelse = "testverdi", + ), + VurdertTotrinnDto( + behandlingssteg = Behandlingssteg.FORESLÅ_VEDTAK, + godkjent = false, + begrunnelse = "testverdi", + ), + ), + ) + + val totrinnsvurderingDto = totrinnService.hentTotrinnsvurderinger(behandlingId) + + val totrinnsstegsinfo = totrinnsvurderingDto.totrinnsstegsinfo + totrinnsstegsinfo.shouldContainExactly( + Totrinnsstegsinfo(Behandlingssteg.FAKTA, true, "testverdi"), + Totrinnsstegsinfo(Behandlingssteg.VILKÅRSVURDERING, false, "testverdi"), + Totrinnsstegsinfo(Behandlingssteg.FORESLÅ_VEDTAK, false, "testverdi"), + ) + } + + @Test + fun `hentTotrinnsvurderinger skal hente totrinnsvurdering etter beslutters vurdering med nytt behandlingssteg`() { + lagBehandlingsstegstilstand(Behandlingssteg.VARSEL, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + + totrinnService.lagreTotrinnsvurderinger( + behandlingId, + listOf( + VurdertTotrinnDto( + behandlingssteg = Behandlingssteg.FAKTA, + godkjent = true, + begrunnelse = "testverdi", + ), + VurdertTotrinnDto( + behandlingssteg = Behandlingssteg.VILKÅRSVURDERING, + godkjent = false, + begrunnelse = "testverdi", + ), + VurdertTotrinnDto( + behandlingssteg = Behandlingssteg.FORESLÅ_VEDTAK, + godkjent = false, + begrunnelse = "testverdi", + ), + ), + ) + + // Dette steget var ikke behandlet med første omgang + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.UTFØRT) + + val totrinnsvurderingDto = totrinnService.hentTotrinnsvurderinger(behandlingId) + + val totrinnsstegsinfo = totrinnsvurderingDto.totrinnsstegsinfo + totrinnsstegsinfo.shouldContainExactly( + Totrinnsstegsinfo(Behandlingssteg.FAKTA, true, "testverdi"), + Totrinnsstegsinfo(Behandlingssteg.FORELDELSE, null, null), + Totrinnsstegsinfo(Behandlingssteg.VILKÅRSVURDERING, false, "testverdi"), + Totrinnsstegsinfo(Behandlingssteg.FORESLÅ_VEDTAK, false, "testverdi"), + ) + } + + @Test + fun `lagreTotrinnsvurderinger skal ikke lagre når det mangler steg i request som kan besluttes`() { + lagBehandlingsstegstilstand(Behandlingssteg.VARSEL, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FATTE_VEDTAK, Behandlingsstegstatus.KLAR) + + val exception = shouldThrow { + totrinnService.lagreTotrinnsvurderinger( + behandlingId, + listOf( + VurdertTotrinnDto( + behandlingssteg = Behandlingssteg.FAKTA, + godkjent = true, + begrunnelse = "testverdi", + ), + VurdertTotrinnDto( + behandlingssteg = Behandlingssteg.FORESLÅ_VEDTAK, + godkjent = false, + begrunnelse = "testverdi", + ), + ), + ) + } + + exception.message shouldBe "Stegene [FORELDELSE, VILKÅRSVURDERING] mangler totrinnsvurdering" + } + + private fun lagBehandlingsstegstilstand( + behandlingssteg: Behandlingssteg, + behandlingsstegstatus: Behandlingsstegstatus, + ) { + behandlingsstegstilstandRepository.insert( + Behandlingsstegstilstand( + behandlingId = behandlingId, + behandlingssteg = behandlingssteg, + behandlingsstegsstatus = behandlingsstegstatus, + ), + ) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/totrinn/TotrinnsvurderingRepositoryTest.kt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/totrinn/TotrinnsvurderingRepositoryTest.kt new file mode 100644 index 000000000..9dd618003 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/totrinn/TotrinnsvurderingRepositoryTest.kt @@ -0,0 +1,64 @@ +package no.nav.familie.tilbake.totrinn + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.totrinn.domain.Totrinnsvurdering +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class TotrinnsvurderingRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var totrinnsvurderingRepository: TotrinnsvurderingRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + private val totrinnsvurdering = Testdata.totrinnsvurdering + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(Testdata.behandling) + } + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av Totrinnsvurdering til basen`() { + totrinnsvurderingRepository.insert(totrinnsvurdering) + + val lagretTotrinnsvurdering = totrinnsvurderingRepository.findByIdOrThrow(totrinnsvurdering.id) + + lagretTotrinnsvurdering.shouldBeEqualToComparingFieldsExcept( + totrinnsvurdering, + Totrinnsvurdering::sporbar, + Totrinnsvurdering::versjon, + ) + lagretTotrinnsvurdering.versjon shouldBe 1 + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av Totrinnsvurdering i basen`() { + totrinnsvurderingRepository.insert(totrinnsvurdering) + var lagretTotrinnsvurdering = totrinnsvurderingRepository.findByIdOrThrow(totrinnsvurdering.id) + val oppdatertTotrinnsvurdering = lagretTotrinnsvurdering.copy(begrunnelse = "bob") + + totrinnsvurderingRepository.update(oppdatertTotrinnsvurdering) + + lagretTotrinnsvurdering = totrinnsvurderingRepository.findByIdOrThrow(totrinnsvurdering.id) + lagretTotrinnsvurdering.shouldBeEqualToComparingFieldsExcept( + oppdatertTotrinnsvurdering, + Totrinnsvurdering::sporbar, + Totrinnsvurdering::versjon, + ) + lagretTotrinnsvurdering.versjon shouldBe 2 + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingRepositoryTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingRepositoryTest.kt" new file mode 100644 index 000000000..f6dc7360d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingRepositoryTest.kt" @@ -0,0 +1,40 @@ +package no.nav.familie.tilbake.vilkårsvurdering + +import io.kotest.matchers.equality.shouldBeEqualToComparingFieldsExcept +import io.kotest.matchers.shouldBe +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.common.repository.findByIdOrThrow +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurdering +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +internal class VilkårsvurderingRepositoryTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var vilkårsvurderingRepository: VilkårsvurderingRepository + + private val vilkår = Testdata.vilkårsvurdering + + @Test + fun `insert med gyldige verdier skal persistere en forekomst av Vilkårsvurdering til basen`() { + vilkårsvurderingRepository.insert(vilkår) + + val lagretVilkår = vilkårsvurderingRepository.findByIdOrThrow(vilkår.id) + lagretVilkår.shouldBeEqualToComparingFieldsExcept(vilkår, Vilkårsvurdering::sporbar, Vilkårsvurdering::versjon) + lagretVilkår.versjon shouldBe 1 + } + + @Test + fun `update med gyldige verdier skal oppdatere en forekomst av Vilkårsvurdering i basen`() { + vilkårsvurderingRepository.insert(vilkår) + var lagretVilkår = vilkårsvurderingRepository.findByIdOrThrow(vilkår.id) + val oppdatertVilkår = lagretVilkår.copy(aktiv = false) + + vilkårsvurderingRepository.update(oppdatertVilkår) + + lagretVilkår = vilkårsvurderingRepository.findByIdOrThrow(vilkår.id) + lagretVilkår.shouldBeEqualToComparingFieldsExcept(oppdatertVilkår, Vilkårsvurdering::sporbar, Vilkårsvurdering::versjon) + lagretVilkår.versjon shouldBe 2 + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingServiceTest.kt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingServiceTest.kt" new file mode 100644 index 000000000..7398e051f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/kotlin/no/nav/familie/tilbake/vilk\303\245rsvurdering/Vilk\303\245rsvurderingServiceTest.kt" @@ -0,0 +1,712 @@ +package no.nav.familie.tilbake.vilkårsvurdering + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import no.nav.familie.kontrakter.felles.Datoperiode +import no.nav.familie.kontrakter.felles.Månedsperiode +import no.nav.familie.tilbake.OppslagSpringRunnerTest +import no.nav.familie.tilbake.api.dto.AktivitetDto +import no.nav.familie.tilbake.api.dto.AktsomhetDto +import no.nav.familie.tilbake.api.dto.BehandlingsstegVilkårsvurderingDto +import no.nav.familie.tilbake.api.dto.GodTroDto +import no.nav.familie.tilbake.api.dto.SærligGrunnDto +import no.nav.familie.tilbake.api.dto.VilkårsvurderingsperiodeDto +import no.nav.familie.tilbake.behandling.BehandlingRepository +import no.nav.familie.tilbake.behandling.FagsakRepository +import no.nav.familie.tilbake.behandlingskontroll.BehandlingsstegstilstandRepository +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingssteg +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstatus +import no.nav.familie.tilbake.behandlingskontroll.domain.Behandlingsstegstilstand +import no.nav.familie.tilbake.config.Constants +import no.nav.familie.tilbake.data.Testdata +import no.nav.familie.tilbake.faktaomfeilutbetaling.FaktaFeilutbetalingRepository +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetaling +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.FaktaFeilutbetalingsperiode +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsestype +import no.nav.familie.tilbake.faktaomfeilutbetaling.domain.Hendelsesundertype +import no.nav.familie.tilbake.foreldelse.VurdertForeldelseRepository +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesperiode +import no.nav.familie.tilbake.foreldelse.domain.Foreldelsesvurderingstype +import no.nav.familie.tilbake.foreldelse.domain.VurdertForeldelse +import no.nav.familie.tilbake.kravgrunnlag.KravgrunnlagRepository +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassekode +import no.nav.familie.tilbake.kravgrunnlag.domain.Klassetype +import no.nav.familie.tilbake.kravgrunnlag.domain.Kravgrunnlagsbeløp433 +import no.nav.familie.tilbake.vilkårsvurdering.domain.Aktsomhet +import no.nav.familie.tilbake.vilkårsvurdering.domain.SærligGrunn +import no.nav.familie.tilbake.vilkårsvurdering.domain.Vilkårsvurderingsresultat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.math.BigDecimal +import java.time.LocalDate +import java.time.YearMonth +import java.util.UUID + +internal class VilkårsvurderingServiceTest : OppslagSpringRunnerTest() { + + @Autowired + private lateinit var fagsakRepository: FagsakRepository + + @Autowired + private lateinit var behandlingRepository: BehandlingRepository + + @Autowired + private lateinit var kravgrunnlagRepository: KravgrunnlagRepository + + @Autowired + private lateinit var behandlingsstegstilstandRepository: BehandlingsstegstilstandRepository + + @Autowired + private lateinit var faktaFeilutbetalingRepository: FaktaFeilutbetalingRepository + + @Autowired + private lateinit var foreldelseRepository: VurdertForeldelseRepository + + @Autowired + private lateinit var vilkårsvurderingRepository: VilkårsvurderingRepository + + @Autowired + private lateinit var vilkårsvurderingService: VilkårsvurderingService + + private val behandling = Testdata.behandling + + @BeforeEach + fun init() { + fagsakRepository.insert(Testdata.fagsak) + behandlingRepository.insert(behandling) + val førstePeriode = Testdata.kravgrunnlagsperiode432 + .copy( + id = UUID.randomUUID(), + periode = Månedsperiode(fom = YearMonth.of(2020, 1), tom = YearMonth.of(2020, 1)), + beløp = setOf( + Testdata.feilKravgrunnlagsbeløp433.copy(id = UUID.randomUUID()), + Testdata.ytelKravgrunnlagsbeløp433.copy(id = UUID.randomUUID()), + ), + ) + val andrePeriode = Testdata.kravgrunnlagsperiode432 + .copy( + id = UUID.randomUUID(), + periode = Månedsperiode(fom = YearMonth.of(2020, 2), tom = YearMonth.of(2020, 2)), + beløp = setOf( + Testdata.feilKravgrunnlagsbeløp433.copy(id = UUID.randomUUID()), + Testdata.ytelKravgrunnlagsbeløp433.copy(id = UUID.randomUUID()), + ), + ) + + val kravgrunnlag431 = Testdata.kravgrunnlag431.copy(perioder = setOf(førstePeriode, andrePeriode)) + kravgrunnlagRepository.insert(kravgrunnlag431) + + val periode = FaktaFeilutbetalingsperiode( + periode = Månedsperiode(førstePeriode.periode.fom, andrePeriode.periode.tom), + hendelsestype = Hendelsestype.ANNET, + hendelsesundertype = Hendelsesundertype.ANNET_FRITEKST, + ) + faktaFeilutbetalingRepository.insert( + FaktaFeilutbetaling( + behandlingId = behandling.id, + begrunnelse = "fakta begrunnelse", + perioder = setOf(periode), + ), + ) + + lagBehandlingsstegstilstand(Behandlingssteg.GRUNNLAG, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FAKTA, Behandlingsstegstatus.UTFØRT) + } + + @Test + fun `hentVilkårsvurdering skal hente vilkårsvurdering fra fakta perioder`() { + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.KLAR) + + val vurdertVilkårsvurderingDto = vilkårsvurderingService.hentVilkårsvurdering(behandling.id) + vurdertVilkårsvurderingDto.rettsgebyr shouldBe Constants.rettsgebyr + vurdertVilkårsvurderingDto.perioder.shouldNotBeEmpty() + vurdertVilkårsvurderingDto.perioder.size shouldBe 1 + val vurdertPeriode = vurdertVilkårsvurderingDto.perioder[0] + vurdertPeriode.periode shouldBe Datoperiode(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 2, 29)) + vurdertPeriode.hendelsestype shouldBe Hendelsestype.ANNET + vurdertPeriode.feilutbetaltBeløp shouldBe BigDecimal("20000") + vurdertPeriode.reduserteBeløper.shouldBeEmpty() + assertAktiviteter(vurdertPeriode.aktiviteter) + vurdertPeriode.aktiviteter[0].beløp shouldBe BigDecimal(20000) + vurdertPeriode.foreldet.shouldBeFalse() + vurdertPeriode.foreldet.shouldBeFalse() + vurdertPeriode.begrunnelse.shouldBeNull() + vurdertPeriode.vilkårsvurderingsresultatInfo.shouldBeNull() + } + + @Test + fun `hentVilkårsvurdering skal hente vilkårsvurdering fra foreldelse perioder som ikke er foreldet`() { + lagForeldese(Foreldelsesvurderingstype.FORELDET, Foreldelsesvurderingstype.IKKE_FORELDET) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.KLAR) + + val vurdertVilkårsvurderingDto = vilkårsvurderingService.hentVilkårsvurdering(behandling.id) + vurdertVilkårsvurderingDto.rettsgebyr shouldBe Constants.rettsgebyr + vurdertVilkårsvurderingDto.perioder.shouldNotBeEmpty() + vurdertVilkårsvurderingDto.perioder.size shouldBe 2 + + val foreldetPeriode = vurdertVilkårsvurderingDto.perioder[0] + foreldetPeriode.periode shouldBe Datoperiode(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 1, 31)) + foreldetPeriode.hendelsestype shouldBe Hendelsestype.ANNET + foreldetPeriode.feilutbetaltBeløp shouldBe BigDecimal("10000") + foreldetPeriode.foreldet.shouldBeTrue() + foreldetPeriode.reduserteBeløper.shouldBeEmpty() + assertAktiviteter(foreldetPeriode.aktiviteter) + foreldetPeriode.aktiviteter[0].beløp shouldBe BigDecimal(10000) + foreldetPeriode.begrunnelse shouldBe "foreldelse begrunnelse 1" + foreldetPeriode.vilkårsvurderingsresultatInfo.shouldBeNull() + + val ikkeForeldetPeriode = vurdertVilkårsvurderingDto.perioder[1] + ikkeForeldetPeriode.periode shouldBe Datoperiode(LocalDate.of(2020, 2, 1), LocalDate.of(2020, 2, 29)) + ikkeForeldetPeriode.hendelsestype shouldBe Hendelsestype.ANNET + ikkeForeldetPeriode.foreldet.shouldBeFalse() + ikkeForeldetPeriode.feilutbetaltBeløp shouldBe BigDecimal("10000") + ikkeForeldetPeriode.aktiviteter[0].beløp shouldBe BigDecimal(10000) + ikkeForeldetPeriode.reduserteBeløper.shouldBeEmpty() + assertAktiviteter(ikkeForeldetPeriode.aktiviteter) + ikkeForeldetPeriode.begrunnelse.shouldBeNull() + ikkeForeldetPeriode.vilkårsvurderingsresultatInfo.shouldBeNull() + } + + @Test + fun `hentVilkårsvurdering skal hente vilkårsvurdering når perioder er delt opp`() { + // delt opp i to perioder + val periode1 = Datoperiode(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 1, 31)) + val periode2 = Datoperiode(LocalDate.of(2020, 2, 1), LocalDate.of(2020, 2, 29)) + val behandlingsstegVilkårsvurderingDto = lagVilkårsvurderingMedGodTro(perioder = listOf(periode1, periode2)) + vilkårsvurderingService.lagreVilkårsvurdering(behandling.id, behandlingsstegVilkårsvurderingDto) + + val vurdertVilkårsvurderingDto = vilkårsvurderingService.hentVilkårsvurdering(behandling.id) + vurdertVilkårsvurderingDto.rettsgebyr shouldBe Constants.rettsgebyr + vurdertVilkårsvurderingDto.perioder.shouldNotBeEmpty() + vurdertVilkårsvurderingDto.perioder.size shouldBe 2 + + val førstePeriode = vurdertVilkårsvurderingDto.perioder[0] + førstePeriode.periode shouldBe periode1 + førstePeriode.hendelsestype shouldBe Hendelsestype.ANNET + førstePeriode.feilutbetaltBeløp shouldBe BigDecimal(10000) + førstePeriode.foreldet.shouldBeFalse() + assertAktiviteter(førstePeriode.aktiviteter) + førstePeriode.aktiviteter[0].beløp shouldBe BigDecimal(10000) + var vilkårsvurderingsresultatDto = førstePeriode.vilkårsvurderingsresultatInfo + vilkårsvurderingsresultatDto.shouldNotBeNull() + vilkårsvurderingsresultatDto.vilkårsvurderingsresultat shouldBe Vilkårsvurderingsresultat.GOD_TRO + + val andrePeriode = vurdertVilkårsvurderingDto.perioder[1] + andrePeriode.periode shouldBe periode2 + andrePeriode.hendelsestype shouldBe Hendelsestype.ANNET + andrePeriode.feilutbetaltBeløp shouldBe BigDecimal(10000) + andrePeriode.foreldet.shouldBeFalse() + assertAktiviteter(andrePeriode.aktiviteter) + andrePeriode.aktiviteter[0].beløp shouldBe BigDecimal(10000) + vilkårsvurderingsresultatDto = andrePeriode.vilkårsvurderingsresultatInfo + vilkårsvurderingsresultatDto.shouldNotBeNull() + vilkårsvurderingsresultatDto.vilkårsvurderingsresultat shouldBe Vilkårsvurderingsresultat.GOD_TRO + } + + @Test + fun `hentVilkårsvurdering skal hente vilkårsvurdering med reduserte beløper`() { + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.AUTOUTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.KLAR) + + val kravgrunnlag431 = kravgrunnlagRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + val justBeløp = lagKravgrunnlagsbeløp( + klassetype = Klassetype.JUST, + nyttBeløp = BigDecimal(5000), + opprinneligUtbetalingsbeløp = BigDecimal.ZERO, + ) + val trekBeløp = lagKravgrunnlagsbeløp( + klassetype = Klassetype.TREK, + nyttBeløp = BigDecimal.ZERO, + opprinneligUtbetalingsbeløp = BigDecimal(-2000), + ) + val skatBeløp = lagKravgrunnlagsbeløp( + klassetype = Klassetype.SKAT, + nyttBeløp = BigDecimal.ZERO, + opprinneligUtbetalingsbeløp = BigDecimal(-2000), + ) + val førstePeriode = kravgrunnlag431.perioder + .toList()[0] + .copy( + beløp = setOf( + Testdata.feilKravgrunnlagsbeløp433.copy(id = UUID.randomUUID()), + Testdata.ytelKravgrunnlagsbeløp433.copy(id = UUID.randomUUID()), + justBeløp, + ), + ) + val andrePeriode = kravgrunnlag431.perioder + .toList()[1] + .copy( + beløp = setOf( + Testdata.feilKravgrunnlagsbeløp433.copy(id = UUID.randomUUID()), + Testdata.ytelKravgrunnlagsbeløp433.copy(id = UUID.randomUUID()), + trekBeløp, + skatBeløp, + ), + ) + kravgrunnlagRepository.update(kravgrunnlag431.copy(perioder = setOf(førstePeriode, andrePeriode))) + + val vurdertVilkårsvurderingDto = vilkårsvurderingService.hentVilkårsvurdering(behandling.id) + vurdertVilkårsvurderingDto.rettsgebyr shouldBe Constants.rettsgebyr + vurdertVilkårsvurderingDto.perioder.shouldNotBeEmpty() + vurdertVilkårsvurderingDto.perioder.size shouldBe 1 + val vurdertPeriode = vurdertVilkårsvurderingDto.perioder[0] + vurdertPeriode.periode shouldBe Datoperiode(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 2, 29)) + vurdertPeriode.hendelsestype shouldBe Hendelsestype.ANNET + vurdertPeriode.feilutbetaltBeløp shouldBe BigDecimal("20000") + assertAktiviteter(vurdertPeriode.aktiviteter) + vurdertPeriode.aktiviteter[0].beløp shouldBe BigDecimal(20000) + vurdertPeriode.foreldet.shouldBeFalse() + + vurdertPeriode.reduserteBeløper.shouldNotBeEmpty() + vurdertPeriode.reduserteBeløper.size shouldBe 3 + var redusertBeløp = vurdertPeriode.reduserteBeløper[0] + redusertBeløp.trekk.shouldBeTrue() + redusertBeløp.beløp shouldBe BigDecimal("2000.00") + redusertBeløp = vurdertPeriode.reduserteBeløper[1] + redusertBeløp.trekk.shouldBeTrue() + redusertBeløp.beløp shouldBe BigDecimal("2000.00") + redusertBeløp = vurdertPeriode.reduserteBeløper[2] + redusertBeløp.trekk.shouldBeFalse() + redusertBeløp.beløp shouldBe BigDecimal("5000.00") + + vurdertPeriode.vilkårsvurderingsresultatInfo.shouldBeNull() + vurdertPeriode.begrunnelse.shouldBeNull() + } + + @Test + fun `hentVilkårsvurdering skal hente allerede lagret simpel aktsomhet vilkårsvurdering`() { + val behandlingsstegVilkårsvurderingDto = + lagVilkårsvurderingMedSimpelAktsomhet(særligGrunn = SærligGrunnDto(SærligGrunn.GRAD_AV_UAKTSOMHET)) + vilkårsvurderingService.lagreVilkårsvurdering( + behandlingId = behandling.id, + behandlingsstegVilkårsvurderingDto = behandlingsstegVilkårsvurderingDto, + ) + + val vurdertVilkårsvurderingDto = vilkårsvurderingService.hentVilkårsvurdering(behandling.id) + vurdertVilkårsvurderingDto.rettsgebyr shouldBe Constants.rettsgebyr + vurdertVilkårsvurderingDto.perioder.shouldNotBeEmpty() + vurdertVilkårsvurderingDto.perioder.size shouldBe 1 + val vurdertPeriode = vurdertVilkårsvurderingDto.perioder[0] + vurdertPeriode.periode shouldBe Datoperiode(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 2, 29)) + vurdertPeriode.hendelsestype shouldBe Hendelsestype.ANNET + vurdertPeriode.feilutbetaltBeløp shouldBe BigDecimal("20000") + assertAktiviteter(vurdertPeriode.aktiviteter) + vurdertPeriode.aktiviteter[0].beløp shouldBe BigDecimal(20000) + vurdertPeriode.foreldet.shouldBeFalse() + vurdertPeriode.begrunnelse shouldBe "Vilkårsvurdering begrunnelse" + + val vilkårsvurderingsresultatDto = vurdertPeriode.vilkårsvurderingsresultatInfo + vilkårsvurderingsresultatDto.shouldNotBeNull() + vilkårsvurderingsresultatDto.godTro.shouldBeNull() + vilkårsvurderingsresultatDto.vilkårsvurderingsresultat shouldBe Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT + val aktsomhetDto = vilkårsvurderingsresultatDto.aktsomhet + aktsomhetDto.shouldNotBeNull() + aktsomhetDto.aktsomhet shouldBe Aktsomhet.SIMPEL_UAKTSOMHET + aktsomhetDto.begrunnelse shouldBe "Aktsomhet begrunnelse" + aktsomhetDto.tilbakekrevSmåbeløp.shouldBeFalse() + aktsomhetDto.særligeGrunnerTilReduksjon.shouldBeFalse() + aktsomhetDto.andelTilbakekreves.shouldBeNull() + aktsomhetDto.ileggRenter.shouldBeNull() + aktsomhetDto.beløpTilbakekreves.shouldBeNull() + aktsomhetDto.særligeGrunnerBegrunnelse shouldBe "Særlig grunner begrunnelse" + val særligGrunner = aktsomhetDto.særligeGrunner + særligGrunner.shouldNotBeNull() + særligGrunner.any { SærligGrunn.GRAD_AV_UAKTSOMHET == it.særligGrunn }.shouldBeTrue() + særligGrunner.all { it.begrunnelse == null }.shouldBeTrue() + } + + @Test + fun `hentVilkårsvurdering skal hente allerede lagret god tro vilkårsvurdering`() { + val behandlingsstegVilkårsvurderingDto = + lagVilkårsvurderingMedGodTro(perioder = listOf(Datoperiode(YearMonth.of(2020, 1), YearMonth.of(2020, 2)))) + vilkårsvurderingService.lagreVilkårsvurdering( + behandlingId = behandling.id, + behandlingsstegVilkårsvurderingDto = behandlingsstegVilkårsvurderingDto, + ) + + val vurdertVilkårsvurderingDto = vilkårsvurderingService.hentVilkårsvurdering(behandling.id) + vurdertVilkårsvurderingDto.rettsgebyr shouldBe Constants.rettsgebyr + vurdertVilkårsvurderingDto.perioder.shouldNotBeEmpty() + vurdertVilkårsvurderingDto.perioder.size shouldBe 1 + val vurdertPeriode = vurdertVilkårsvurderingDto.perioder[0] + vurdertPeriode.periode shouldBe Datoperiode(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 2, 29)) + vurdertPeriode.hendelsestype shouldBe Hendelsestype.ANNET + vurdertPeriode.feilutbetaltBeløp shouldBe BigDecimal("20000") + assertAktiviteter(vurdertPeriode.aktiviteter) + vurdertPeriode.aktiviteter[0].beløp shouldBe BigDecimal(20000) + vurdertPeriode.foreldet.shouldBeFalse() + vurdertPeriode.begrunnelse shouldBe "Vilkårsvurdering begrunnelse" + + val vilkårsvurderingsresultatDto = vurdertPeriode.vilkårsvurderingsresultatInfo + vilkårsvurderingsresultatDto.shouldNotBeNull() + vilkårsvurderingsresultatDto.aktsomhet.shouldBeNull() + vilkårsvurderingsresultatDto.vilkårsvurderingsresultat shouldBe Vilkårsvurderingsresultat.GOD_TRO + val godTroDto = vilkårsvurderingsresultatDto.godTro + godTroDto.shouldNotBeNull() + godTroDto.beløpErIBehold.shouldBeTrue() + godTroDto.begrunnelse shouldBe "God tro begrunnelse" + godTroDto.beløpTilbakekreves.shouldBeNull() + } + + @Test + fun `hentVilkårsvurdering skal hente foreldelse perioder som endret til IKKE_FORELDET`() { + // en periode med FORELDET og andre er IKKE_FORELDET + lagForeldese(Foreldelsesvurderingstype.FORELDET, Foreldelsesvurderingstype.IKKE_FORELDET) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.KLAR) + + var vurdertVilkårsvurderingDto = vilkårsvurderingService.hentVilkårsvurdering(behandling.id) + vurdertVilkårsvurderingDto.perioder.shouldNotBeEmpty() + vurdertVilkårsvurderingDto.perioder.size shouldBe 2 + vurdertVilkårsvurderingDto.perioder.count { it.foreldet } shouldBe 1 + vurdertVilkårsvurderingDto.perioder.count { !it.foreldet } shouldBe 1 + + // behandle vilkårsvurdering + vilkårsvurderingService + .lagreVilkårsvurdering( + behandling.id, + lagVilkårsvurderingMedGodTro( + perioder = listOf( + Datoperiode( + YearMonth.of(2020, 2), + YearMonth.of(2020, 2), + ), + ), + ), + ) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.KLAR) + + // endret begge perioder til IKKE_FORELDET + val vurdertForeldelse = foreldelseRepository.findByBehandlingIdAndAktivIsTrue(behandlingId = behandling.id)!! + oppdaterForeldelsesvurdering( + vurdertForeldelse, + Foreldelsesvurderingstype.IKKE_FORELDET, + Foreldelsesvurderingstype.IKKE_FORELDET, + ) + + vurdertVilkårsvurderingDto = vilkårsvurderingService.hentVilkårsvurdering(behandling.id) + vurdertVilkårsvurderingDto.perioder.shouldNotBeEmpty() + vurdertVilkårsvurderingDto.perioder.size shouldBe 2 + vurdertVilkårsvurderingDto.perioder.count { !it.foreldet } shouldBe 2 + + val ikkeVurdertPeriode = vurdertVilkårsvurderingDto.perioder[0] + ikkeVurdertPeriode.periode shouldBe Datoperiode(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 1, 31)) + ikkeVurdertPeriode.hendelsestype shouldBe Hendelsestype.ANNET + ikkeVurdertPeriode.feilutbetaltBeløp shouldBe BigDecimal("10000") + ikkeVurdertPeriode.reduserteBeløper.shouldBeEmpty() + assertAktiviteter(ikkeVurdertPeriode.aktiviteter) + ikkeVurdertPeriode.aktiviteter[0].beløp shouldBe BigDecimal(10000) + ikkeVurdertPeriode.vilkårsvurderingsresultatInfo.shouldBeNull() + ikkeVurdertPeriode.begrunnelse.shouldBeNull() + + val vurdertPeriode = vurdertVilkårsvurderingDto.perioder[1] + vurdertPeriode.periode shouldBe Datoperiode(LocalDate.of(2020, 2, 1), LocalDate.of(2020, 2, 29)) + vurdertPeriode.hendelsestype shouldBe Hendelsestype.ANNET + vurdertPeriode.feilutbetaltBeløp shouldBe BigDecimal("10000") + assertAktiviteter(vurdertPeriode.aktiviteter) + vurdertPeriode.aktiviteter[0].beløp shouldBe BigDecimal(10000) + vurdertPeriode.begrunnelse shouldBe "Vilkårsvurdering begrunnelse" + + val vilkårsvurderingsresultatDto = vurdertPeriode.vilkårsvurderingsresultatInfo + vilkårsvurderingsresultatDto.shouldNotBeNull() + vilkårsvurderingsresultatDto.aktsomhet.shouldBeNull() + vilkårsvurderingsresultatDto.vilkårsvurderingsresultat shouldBe Vilkårsvurderingsresultat.GOD_TRO + val godTroDto = vilkårsvurderingsresultatDto.godTro + godTroDto.shouldNotBeNull() + godTroDto.beløpErIBehold.shouldBeTrue() + godTroDto.begrunnelse shouldBe "God tro begrunnelse" + godTroDto.beløpTilbakekreves.shouldBeNull() + } + + @Test + fun `hentVilkårsvurdering skal hente perioder som endret til FORELDET`() { + // en periode med FORELDET og andre er IKKE_FORELDET + lagForeldese(Foreldelsesvurderingstype.FORELDET, Foreldelsesvurderingstype.IKKE_FORELDET) + lagBehandlingsstegstilstand(Behandlingssteg.FORELDELSE, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.KLAR) + + var vurdertVilkårsvurderingDto = vilkårsvurderingService.hentVilkårsvurdering(behandling.id) + vurdertVilkårsvurderingDto.perioder.shouldNotBeEmpty() + vurdertVilkårsvurderingDto.perioder.size shouldBe 2 + vurdertVilkårsvurderingDto.perioder.count { it.foreldet } shouldBe 1 + vurdertVilkårsvurderingDto.perioder.count { !it.foreldet } shouldBe 1 + + // behandle vilkårsvurdering + vilkårsvurderingService + .lagreVilkårsvurdering( + behandling.id, + lagVilkårsvurderingMedGodTro( + perioder = listOf( + Datoperiode( + YearMonth.of(2020, 2), + YearMonth.of(2020, 2), + ), + ), + ), + ) + lagBehandlingsstegstilstand(Behandlingssteg.VILKÅRSVURDERING, Behandlingsstegstatus.UTFØRT) + lagBehandlingsstegstilstand(Behandlingssteg.FORESLÅ_VEDTAK, Behandlingsstegstatus.KLAR) + + // endret begge perioder til FORELDET + val vurdertForeldelse = foreldelseRepository.findByBehandlingIdAndAktivIsTrue(behandlingId = behandling.id)!! + oppdaterForeldelsesvurdering(vurdertForeldelse, Foreldelsesvurderingstype.FORELDET, Foreldelsesvurderingstype.FORELDET) + + vurdertVilkårsvurderingDto = vilkårsvurderingService.hentVilkårsvurdering(behandling.id) + vurdertVilkårsvurderingDto.perioder.shouldNotBeEmpty() + vurdertVilkårsvurderingDto.perioder.size shouldBe 2 + vurdertVilkårsvurderingDto.perioder.count { it.foreldet } shouldBe 2 + + val førsteForeldetPeriode = vurdertVilkårsvurderingDto.perioder[0] + førsteForeldetPeriode.periode shouldBe Datoperiode(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 1, 31)) + førsteForeldetPeriode.hendelsestype shouldBe Hendelsestype.ANNET + førsteForeldetPeriode.feilutbetaltBeløp shouldBe BigDecimal("10000") + førsteForeldetPeriode.reduserteBeløper.shouldBeEmpty() + assertAktiviteter(førsteForeldetPeriode.aktiviteter) + førsteForeldetPeriode.aktiviteter[0].beløp shouldBe BigDecimal(10000) + førsteForeldetPeriode.vilkårsvurderingsresultatInfo.shouldBeNull() + førsteForeldetPeriode.begrunnelse shouldBe "foreldelse begrunnelse 1" + + val andreForeldetPeriode = vurdertVilkårsvurderingDto.perioder[1] + andreForeldetPeriode.periode shouldBe Datoperiode(LocalDate.of(2020, 2, 1), LocalDate.of(2020, 2, 29)) + andreForeldetPeriode.hendelsestype shouldBe Hendelsestype.ANNET + andreForeldetPeriode.feilutbetaltBeløp shouldBe BigDecimal("10000") + andreForeldetPeriode.reduserteBeløper.shouldBeEmpty() + assertAktiviteter(andreForeldetPeriode.aktiviteter) + andreForeldetPeriode.aktiviteter[0].beløp shouldBe BigDecimal(10000) + andreForeldetPeriode.vilkårsvurderingsresultatInfo.shouldBeNull() + andreForeldetPeriode.begrunnelse shouldBe "foreldelse begrunnelse 2" + } + + @Test + fun `lagreVilkårsvurdering skal ikke lagre vilkårsvurdering når andelTilbakekreves er mer enn 100 prosent `() { + val exception = shouldThrow { + vilkårsvurderingService + .lagreVilkårsvurdering( + behandling.id, + lagVilkårsvurderingMedSimpelAktsomhet( + andelTilbakekreves = BigDecimal(120), + særligGrunn = + SærligGrunnDto(SærligGrunn.GRAD_AV_UAKTSOMHET), + ), + ) + } + exception.message shouldBe "Andel som skal tilbakekreves kan ikke være mer enn 100 prosent" + } + + @Test + fun `lagreVilkårsvurdering skal ikke lagre vilkårsvurdering når ANNET særlig grunner mangler ANNET begrunnelse`() { + val exception = shouldThrow { + vilkårsvurderingService + .lagreVilkårsvurdering( + behandling.id, + lagVilkårsvurderingMedSimpelAktsomhet(særligGrunn = SærligGrunnDto(SærligGrunn.ANNET)), + ) + } + exception.message shouldBe "ANNET særlig grunner må ha ANNET begrunnelse" + } + + @Test + fun `lagreVilkårsvurdering skal ikke lagre vilkårsvurdering når manueltSattBeløp er mer enn feilutbetalt beløp`() { + // forutsetter at kravgrunnlag har 20000 som feilutbetalt beløp fra Testdata + val behandlingsstegVilkårsvurderingDto = + lagVilkårsvurderingMedSimpelAktsomhet( + manueltSattBeløp = BigDecimal(30000), + særligGrunn = SærligGrunnDto(SærligGrunn.GRAD_AV_UAKTSOMHET), + ) + val exception = shouldThrow { + vilkårsvurderingService.lagreVilkårsvurdering(behandling.id, behandlingsstegVilkårsvurderingDto) + } + exception.message shouldBe "Beløp som skal tilbakekreves kan ikke være mer enn feilutbetalt beløp" + } + + @Test + fun `lagreVilkårsvurdering skal ikke lagre vilkårsvurdering når tilbakekrevesBeløp er mer enn feilutbetalt beløp`() { + // forutsetter at kravgrunnlag har 20000 som feilutbetalt beløp fra Testdata + val exception = shouldThrow { + vilkårsvurderingService.lagreVilkårsvurdering( + behandling.id, + lagVilkårsvurderingMedGodTro( + listOf( + Datoperiode( + YearMonth.of(2020, 1), + YearMonth.of(2020, 2), + ), + ), + BigDecimal(30000), + ), + ) + } + exception.message shouldBe "Beløp som skal tilbakekreves kan ikke være mer enn feilutbetalt beløp" + } + + @Test + fun `lagreVilkårsvurdering skal lagre vilkårsvurdering med false ileggRenter for barnetrygd behandling`() { + // forutsetter at behandling opprettet for barnetrygd fra Testdata + vilkårsvurderingService + .lagreVilkårsvurdering( + behandling.id, + lagVilkårsvurderingMedSimpelAktsomhet( + ileggRenter = true, + særligGrunn = + SærligGrunnDto(SærligGrunn.GRAD_AV_UAKTSOMHET), + ), + ) + + val vilkårsvurdering = vilkårsvurderingRepository.findByBehandlingIdAndAktivIsTrue(behandling.id) + vilkårsvurdering.shouldNotBeNull() + + vilkårsvurdering.perioder.shouldNotBeEmpty() + vilkårsvurdering.perioder.size shouldBe 1 + val vurdertPeriode = vilkårsvurdering.perioder.toList()[0] + vurdertPeriode.periode shouldBe Månedsperiode(YearMonth.of(2020, 1), YearMonth.of(2020, 2)) + vurdertPeriode.begrunnelse shouldBe "Vilkårsvurdering begrunnelse" + + vurdertPeriode.aktsomhet.shouldNotBeNull() + vurdertPeriode.godTro.shouldBeNull() + vurdertPeriode.vilkårsvurderingsresultat shouldBe Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT + + val aktsomhet = vurdertPeriode.aktsomhet + aktsomhet.shouldNotBeNull() + aktsomhet.aktsomhet shouldBe Aktsomhet.SIMPEL_UAKTSOMHET + aktsomhet.begrunnelse shouldBe "Aktsomhet begrunnelse" + aktsomhet.tilbakekrevSmåbeløp.shouldBeFalse() + aktsomhet.særligeGrunnerTilReduksjon.shouldBeFalse() + aktsomhet.andelTilbakekreves.shouldBeNull() + aktsomhet.andelTilbakekreves.shouldBeNull() + aktsomhet.ileggRenter shouldBe false + aktsomhet.manueltSattBeløp.shouldBeNull() + aktsomhet.særligeGrunnerBegrunnelse shouldBe "Særlig grunner begrunnelse" + + val særligGrunner = aktsomhet.vilkårsvurderingSærligeGrunner + særligGrunner.shouldNotBeNull() + særligGrunner.any { SærligGrunn.GRAD_AV_UAKTSOMHET == it.særligGrunn }.shouldBeTrue() + særligGrunner.all { it.begrunnelse == null }.shouldBeTrue() + } + + private fun lagBehandlingsstegstilstand(behandlingssteg: Behandlingssteg, behandlingsstegstatus: Behandlingsstegstatus) { + behandlingsstegstilstandRepository.insert( + Behandlingsstegstilstand( + behandlingssteg = behandlingssteg, + behandlingsstegsstatus = behandlingsstegstatus, + behandlingId = behandling.id, + ), + ) + } + + private fun lagKravgrunnlagsbeløp( + klassetype: Klassetype, + nyttBeløp: BigDecimal, + opprinneligUtbetalingsbeløp: BigDecimal, + ): Kravgrunnlagsbeløp433 { + return Kravgrunnlagsbeløp433( + id = UUID.randomUUID(), + klassetype = klassetype, + klassekode = Klassekode.BATR, + opprinneligUtbetalingsbeløp = opprinneligUtbetalingsbeløp, + nyttBeløp = nyttBeløp, + tilbakekrevesBeløp = BigDecimal.ZERO, + skatteprosent = BigDecimal.ZERO, + uinnkrevdBeløp = BigDecimal.ZERO, + resultatkode = "testverdi", + årsakskode = "testverdi", + skyldkode = "testverdi", + ) + } + + private fun assertAktiviteter(aktiviteter: List) { + aktiviteter.shouldNotBeEmpty() + aktiviteter.size shouldBe 1 + aktiviteter[0].aktivitet shouldBe Klassekode.BATR.aktivitet + } + + private fun lagVilkårsvurderingMedSimpelAktsomhet( + andelTilbakekreves: BigDecimal? = null, + manueltSattBeløp: BigDecimal? = null, + ileggRenter: Boolean? = null, + særligGrunn: SærligGrunnDto, + ): BehandlingsstegVilkårsvurderingDto { + val periode = VilkårsvurderingsperiodeDto( + periode = Datoperiode(YearMonth.of(2020, 1), YearMonth.of(2020, 2)), + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.FORSTO_BURDE_FORSTÅTT, + begrunnelse = "Vilkårsvurdering begrunnelse", + aktsomhetDto = AktsomhetDto( + aktsomhet = Aktsomhet.SIMPEL_UAKTSOMHET, + andelTilbakekreves = andelTilbakekreves, + beløpTilbakekreves = manueltSattBeløp, + ileggRenter = ileggRenter, + begrunnelse = "Aktsomhet begrunnelse", + særligeGrunner = listOf(særligGrunn), + tilbakekrevSmåbeløp = false, + særligeGrunnerBegrunnelse = + "Særlig grunner begrunnelse", + ), + ) + return BehandlingsstegVilkårsvurderingDto(listOf(periode)) + } + + private fun lagVilkårsvurderingMedGodTro( + perioder: List, + beløpTilbakekreves: BigDecimal? = null, + ): BehandlingsstegVilkårsvurderingDto { + return BehandlingsstegVilkårsvurderingDto( + vilkårsvurderingsperioder = perioder.map { + VilkårsvurderingsperiodeDto( + periode = it, + vilkårsvurderingsresultat = Vilkårsvurderingsresultat.GOD_TRO, + begrunnelse = "Vilkårsvurdering begrunnelse", + godTroDto = GodTroDto( + begrunnelse = "God tro begrunnelse", + beløpErIBehold = true, + beløpTilbakekreves = beløpTilbakekreves, + ), + ) + }, + ) + } + + private fun lagForeldese(vararg foreldelsesvurderingstyper: Foreldelsesvurderingstype) { + val foreldelsesperioder = + setOf( + Foreldelsesperiode( + periode = Månedsperiode(YearMonth.of(2020, 1), YearMonth.of(2020, 1)), + foreldelsesvurderingstype = foreldelsesvurderingstyper[0], + begrunnelse = "foreldelse begrunnelse 1", + ), + Foreldelsesperiode( + periode = Månedsperiode(YearMonth.of(2020, 2), YearMonth.of(2020, 2)), + foreldelsesvurderingstype = foreldelsesvurderingstyper[1], + begrunnelse = "foreldelse begrunnelse 2", + ), + ) + foreldelseRepository.insert(VurdertForeldelse(behandlingId = behandling.id, foreldelsesperioder = foreldelsesperioder)) + } + + private fun oppdaterForeldelsesvurdering( + vurdertForeldelse: VurdertForeldelse, + vararg foreldelsesvurderingstyper: Foreldelsesvurderingstype, + ) { + val foreldelsesperioder = setOf( + Foreldelsesperiode( + periode = Månedsperiode(YearMonth.of(2020, 1), YearMonth.of(2020, 1)), + foreldelsesvurderingstype = foreldelsesvurderingstyper[0], + begrunnelse = "foreldelse begrunnelse 1", + ), + Foreldelsesperiode( + periode = Månedsperiode(YearMonth.of(2020, 2), YearMonth.of(2020, 2)), + foreldelsesvurderingstype = foreldelsesvurderingstyper[1], + begrunnelse = "foreldelse begrunnelse 2", + ), + ) + foreldelseRepository.update(vurdertForeldelse.copy(foreldelsesperioder = foreldelsesperioder)) + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/application-integrasjonstest.yaml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/application-integrasjonstest.yaml new file mode 100644 index 000000000..7edd47dc5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/application-integrasjonstest.yaml @@ -0,0 +1,27 @@ +server: + port: 9093 + + +prosessering: + enabled: false + +no.nav.security.jwt: + issuer.azuread: + discoveryurl: http://localhost:${mock-oauth2-server.port}/issuer1/.well-known/openid-configuration + accepted_audience: aud-localhost + cookie_name: localhost-idtoken + +AZURE_APP_WELL_KNOWN_URL: testverdi +AZURE_OPENID_CONFIG_TOKEN_ENDPOINT: testverdi +FAMILIE_INTEGRASJONER_URL: http://localhost:8085 +FAMILIE_OPPDRAG_URL: http://localhost:8087 + +CREDENTIAL_USERNAME: not-a-real-srvuser +CREDENTIAL_PASSWORD: not-a-real-pw +API_SCOPE: dummy +AUTHORIZATION_URL: testverdi + +NAIS_APP_NAME: familie-klage +UNLEASH_SERVER_API_URL: http://localhost:4242/api +UNLEASH_SERVER_API_TOKEN: token +NAIS_CLUSTER_NAME: dev-gcp \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/application-local.yaml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/application-local.yaml new file mode 100644 index 000000000..649ed5f40 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/application-local.yaml @@ -0,0 +1,50 @@ +spring: + datasource: + username: postgres + password: test + url: jdbc:postgresql://localhost:5432/familie-tilbake + driver-class-name: org.postgresql.Driver + +rolle: + barnetrygd: + veileder: "93a26831-9866-4410-927b-74ff51a9107c" + saksbehandler: "d21e00a4-969d-4b28-8782-dc818abfae65" + beslutter: "9449c153-5a1e-44a7-84c6-7cc7a8867233" + enslig: + veileder: "19dcbfde-4cdb-4c64-a1ea-ac9802b03339" + saksbehandler: "ee5e0b5e-454c-4612-b931-1fe363df7c2c" + beslutter: "01166863-22f1-4e16-9785-d7a05a22df74" + kontantstøtte: + veileder: "" + saksbehandler: "" + beslutter: "" + teamfamilie: + forvalter: "c62e908a-cf20-4ad0-b7b3-3ff6ca4bf38b" + prosessering: "928636f4-fd0d-4149-978e-a6fb68bb19de" # 0000-GA-STDAPPS + + +prosessering: + fixedDelayString: + in: + milliseconds: 1000 + +unleash: + enabled: true + +AZURE_APP_WELL_KNOWN_URL: https://login.microsoftonline.com/navq.onmicrosoft.com/v2.0/.well-known/openid-configuration +AZURE_OPENID_CONFIG_TOKEN_ENDPOINT: https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token +FAMILIE_INTEGRASJONER_URL: http://localhost:8085 +FAMILIE_OPPDRAG_URL: #Mockes ut lokalt +SECURITYTOKENSERVICE_URL: https://localhost:8063/soap/SecurityTokenServiceProvider/ +CREDENTIAL_USERNAME: not-a-real-srvuser +CREDENTIAL_PASSWORD: not-a-real-pw +PDL_URL: #Mockes ut lokalt +OPPRETTELSE_DAGER_BEGRENSNING: 1 +CRON_HÅNDTER_GAMMEL_KRAVGRUNNLAG: 0 0/5 * ? * MON-FRI +CRON_AUTOMATISK_SAKSBEHANDLING: 0 0/3 * ? * MON-FRI +CRON_AUTOMATISK_GJENOPPTA: 0 0/7 * ? * MON-FRI + +NAIS_APP_NAME: familie-klage +UNLEASH_SERVER_API_URL: http://localhost:4242/api +UNLEASH_SERVER_API_TOKEN: token +NAIS_CLUSTER_NAME: dev-gcp \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/bootstrap-integrasjonstest.yaml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/bootstrap-integrasjonstest.yaml new file mode 100644 index 000000000..9bb1f742e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/bootstrap-integrasjonstest.yaml @@ -0,0 +1,4 @@ +spring: + cloud: + vault: + enabled: false diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/bootstrap-local.yaml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/bootstrap-local.yaml new file mode 100644 index 000000000..9bb1f742e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/bootstrap-local.yaml @@ -0,0 +1,4 @@ +spring: + cloud: + vault: + enabled: false diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/.editorconfig b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/.editorconfig new file mode 100644 index 000000000..88d1d56a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/.editorconfig @@ -0,0 +1,3 @@ +# unngår at editor auto-inserter newline i fasiten +[*.txt] +insert_final_newline = false diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev.txt new file mode 100644 index 000000000..24b19d620 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev.txt @@ -0,0 +1,14 @@ +Vi sendte deg et varsel 9. mars 2019 om at du hadde fått for mye utbetalt i stønad til barnetilsyn. Vi har i ettertid avsluttet saken om tilbakebetaling fordi det ikke lenger er et feilutbetalt beløp å betale tilbake. + +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +Bob \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_d\303\270d_bruker.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_d\303\270d_bruker.txt" new file mode 100644 index 000000000..a2fc695ce --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_d\303\270d_bruker.txt" @@ -0,0 +1,14 @@ +Vi sendte et varsel 9. mars 2019 om at det var utbetalt for mye i stønad til barnetilsyn. Vi har i ettertid avsluttet saken om tilbakebetaling fordi det ikke lenger er et feilutbetalt beløp å betale tilbake. + +_Har dere spørsmål? +Dere finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan dere chatte eller skrive til oss. + +Hvis dere ikke finner svar på nav.no kan dere ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +Bob \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_institusjon.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_institusjon.txt new file mode 100644 index 000000000..9203ffa11 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_institusjon.txt @@ -0,0 +1,14 @@ +Vi sendte dere et varsel 9. mars 2019 om at dere hadde fått for mye utbetalt i stønad til barnetilsyn. Vi har i ettertid avsluttet saken om tilbakebetaling fordi det ikke lenger er et feilutbetalt beløp å betale tilbake. + +_Har dere spørsmål? +Dere finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan dere chatte eller skrive til oss. + +Hvis dere ikke finner svar på nav.no kan dere ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +Bob \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_nn.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_nn.txt new file mode 100644 index 000000000..00c21891e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_nn.txt @@ -0,0 +1,14 @@ +Vi sende deg eit varsel 9. mars 2019 om at du hadde fått for mykje utbetalt i stønad til barnetilsyn. Vi har i ettertid avslutta saka om tilbakebetaling fordi det ikkje lenger er eit feilutbetalt beløp å betale tilbake. + +_Har du spørsmål? +Du finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Om du ikkje finn svar på nav.no, kan du ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +Bob \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_nn_d\303\270d_bruker.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_nn_d\303\270d_bruker.txt" new file mode 100644 index 000000000..186b9fff6 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_nn_d\303\270d_bruker.txt" @@ -0,0 +1,14 @@ +Vi sende eit varsel 9. mars 2019 om at det var utbetalt for mykje i stønad til barnetilsyn. Vi har i ettertid avslutta saka om tilbakebetaling fordi det ikkje lenger er eit feilutbetalt beløp å betale tilbake. + +_Har de spørsmål? +De finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan de chatte eller skrive til oss. + +Om de ikkje finn svar på nav.no, kan de ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +Bob \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_nn_institusjon.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_nn_institusjon.txt new file mode 100644 index 000000000..52fa8e76e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_nn_institusjon.txt @@ -0,0 +1,14 @@ +Vi sende dykk eit varsel 9. mars 2019 om at de hadde fått for mykje utbetalt i stønad til barnetilsyn. Vi har i ettertid avslutta saka om tilbakebetaling fordi det ikkje lenger er eit feilutbetalt beløp å betale tilbake. + +_Har de spørsmål? +De finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan de chatte eller skrive til oss. + +Om de ikkje finn svar på nav.no, kan de ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +Bob \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering.txt new file mode 100644 index 000000000..2ece32343 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering.txt @@ -0,0 +1,14 @@ +Revurderingen ble henlagt + +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +Bob \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering_d\303\270d_bruker.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering_d\303\270d_bruker.txt" new file mode 100644 index 000000000..4f765b130 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering_d\303\270d_bruker.txt" @@ -0,0 +1,14 @@ +Revurderingen ble henlagt + +_Har dere spørsmål? +Dere finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan dere chatte eller skrive til oss. + +Hvis dere ikke finner svar på nav.no kan dere ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +Bob \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering_frisinn.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering_frisinn.txt new file mode 100644 index 000000000..c14df500e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering_frisinn.txt @@ -0,0 +1,12 @@ +Revurderingen ble henlagt + +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Familie- og pensjonsytelser \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering_nn.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering_nn.txt new file mode 100644 index 000000000..02a848291 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering_nn.txt @@ -0,0 +1,14 @@ +Revurderingen ble henlagt + +_Har du spørsmål? +Du finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Om du ikkje finn svar på nav.no, kan du ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +Bob \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering_nn_d\303\270d_bruker.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering_nn_d\303\270d_bruker.txt" new file mode 100644 index 000000000..3cb1da13d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/henleggelsesbrev/henleggelsesbrev_revurdering_nn_d\303\270d_bruker.txt" @@ -0,0 +1,14 @@ +Revurderingen ble henlagt + +_Har de spørsmål? +De finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan de chatte eller skrive til oss. + +Om de ikkje finn svar på nav.no, kan de ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +Bob \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/.editorconfig b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/.editorconfig new file mode 100644 index 000000000..88d1d56a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/.editorconfig @@ -0,0 +1,3 @@ +# unngår at editor auto-inserter newline i fasiten +[*.txt] +insert_final_newline = false diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev.txt new file mode 100644 index 000000000..307dd4a2a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev.txt @@ -0,0 +1,22 @@ +Vi har fått nye opplysninger i saken om tilbakebetaling av stønad til barnetilsyn, men trenger mer dokumentasjon for å behandle saken. +_Du må sende oss +Dette er ein fritekst. +_Saken blir behandlet etter frist har gått ut +Hvis vi ikke får opplysningene innen 2. mars 2020, behandler vi saken med de opplysningene vi har. + +Dette går fram av folketrygdloven § 21-3. +_Slik uttaler du deg +Du kan sende uttalelsen din ved å logge deg inn på nav.no/beskjedtilnav og velge «Send beskjed til NAV». Du kan også sende uttalelsen din til oss i posten. Adressen finner du på +nav.no/ettersendelser. +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +Bob \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_d\303\270d_bruker.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_d\303\270d_bruker.txt" new file mode 100644 index 000000000..566c0ca23 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_d\303\270d_bruker.txt" @@ -0,0 +1,22 @@ +Vi har fått nye opplysninger i saken om tilbakebetaling av stønad til barnetilsyn, men trenger mer dokumentasjon for å behandle saken. +_Dere må sende oss +Dette er ein fritekst. +_Saken blir behandlet etter frist har gått ut +Hvis vi ikke får opplysningene innen 2. mars 2020, behandler vi saken med de opplysningene vi har. + +Dette går fram av folketrygdloven § 21-3. +_Rett til uttalelse +Dere kan sende uttalelsen til oss i posten. Adressen finner dere på nav.no/ettersendelser. +Dere kan også ta kontakt med oss på nav.no/kontakt. +_Har dere spørsmål? +Dere finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan dere chatte eller skrive til oss. + +Hvis dere ikke finner svar på nav.no kan dere ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +Bob \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_institusjon.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_institusjon.txt new file mode 100644 index 000000000..566c0ca23 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_institusjon.txt @@ -0,0 +1,22 @@ +Vi har fått nye opplysninger i saken om tilbakebetaling av stønad til barnetilsyn, men trenger mer dokumentasjon for å behandle saken. +_Dere må sende oss +Dette er ein fritekst. +_Saken blir behandlet etter frist har gått ut +Hvis vi ikke får opplysningene innen 2. mars 2020, behandler vi saken med de opplysningene vi har. + +Dette går fram av folketrygdloven § 21-3. +_Rett til uttalelse +Dere kan sende uttalelsen til oss i posten. Adressen finner dere på nav.no/ettersendelser. +Dere kan også ta kontakt med oss på nav.no/kontakt. +_Har dere spørsmål? +Dere finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan dere chatte eller skrive til oss. + +Hvis dere ikke finner svar på nav.no kan dere ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +Bob \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_nn.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_nn.txt new file mode 100644 index 000000000..a3078a413 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_nn.txt @@ -0,0 +1,22 @@ +Vi har fått nye opplysningar i saka om tilbakebetaling av barnetrygd, men trenger meir dokumentasjon for å kunne behandla saka. +_Du må sende oss +Dette er ein fritekst. +_Saka blir behandla etter frist har gått ut +Dersom vi ikkje får opplysningane innan 2. mars 2020, behandlar vi saka med dei opplysningane vi har. + +Dette går fram av barnetrygdlova §§ 17 og 18. +_Slik uttaler du deg +Du kan sende uttalen din ved å logge deg inn på nav.no/beskjedtilnav og velje «Send beskjed til NAV». Du kan også sende uttalen din til oss i posten. Adressa finn du på +nav.no/ettersendelser. +_Har du spørsmål? +Du finn meir informasjon på nav.no/barnetrygd. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Om du ikkje finn svar på nav.no, kan du ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Familie- og pensjonsytelser Skien + +Bob \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_nn_d\303\270d_bruker.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_nn_d\303\270d_bruker.txt" new file mode 100644 index 000000000..77251f5ed --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_nn_d\303\270d_bruker.txt" @@ -0,0 +1,22 @@ +Vi har fått nye opplysningar i saka om tilbakebetaling av barnetrygd, men trenger meir dokumentasjon for å kunne behandla saka. +_De må sende oss +Dette er ein fritekst. +_Saka blir behandla etter frist har gått ut +Dersom vi ikkje får opplysningane innan 2. mars 2020, behandlar vi saka med dei opplysningane vi har. + +Dette går fram av barnetrygdlova §§ 17 og 18. +_Rett til uttale +De kan sende uttalen til oss i posten. Adressa finn de på nav.no/ettersendelser. +De kan også ta kontakt med oss på nav.no/kontakt. +_Har de spørsmål? +De finn meir informasjon på nav.no/barnetrygd. + +På nav.no/kontakt kan de chatte eller skrive til oss. + +Om de ikkje finn svar på nav.no, kan de ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Familie- og pensjonsytelser Skien + +Bob \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_nn_institusjon.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_nn_institusjon.txt new file mode 100644 index 000000000..77251f5ed --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/innhentdokumentasjonbrev/innhentdokumentasjonbrev_nn_institusjon.txt @@ -0,0 +1,22 @@ +Vi har fått nye opplysningar i saka om tilbakebetaling av barnetrygd, men trenger meir dokumentasjon for å kunne behandla saka. +_De må sende oss +Dette er ein fritekst. +_Saka blir behandla etter frist har gått ut +Dersom vi ikkje får opplysningane innan 2. mars 2020, behandlar vi saka med dei opplysningane vi har. + +Dette går fram av barnetrygdlova §§ 17 og 18. +_Rett til uttale +De kan sende uttalen til oss i posten. Adressa finn de på nav.no/ettersendelser. +De kan også ta kontakt med oss på nav.no/kontakt. +_Har de spørsmål? +De finn meir informasjon på nav.no/barnetrygd. + +På nav.no/kontakt kan de chatte eller skrive til oss. + +Om de ikkje finn svar på nav.no, kan de ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Familie- og pensjonsytelser Skien + +Bob \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/junit-platform.properties b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..d265fd838 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.testinstance.lifecycle.default = per_class diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kotest.properties b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kotest.properties new file mode 100644 index 000000000..c6a0dcaea --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kotest.properties @@ -0,0 +1,4 @@ +# Noe skjedde i 5.7.0, kommer bli disabled by default i 6.0 +# https://github.com/kotest/kotest/issues/3126#issuecomment-1517219616 +kotest.framework.classpath.scanning.autoscan.disable=true +kotest.framework.classpath.scanning.config.disable=true diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_BA_ENDR.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_BA_ENDR.xml new file mode 100644 index 000000000..108f6e9bd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_BA_ENDR.xml @@ -0,0 +1,47 @@ + + + + 0 + 0 + ENDR + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 1 + + + 2020-08-01 + 2020-08-31 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_BA_belop_storre_enn_diff.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_BA_belop_storre_enn_diff.xml new file mode 100644 index 000000000..0cfc4d820 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_BA_belop_storre_enn_diff.xml @@ -0,0 +1,56 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2022-06-30-21.22.15.613021 + K231B433 + 0 + + + 2022-06-01 + 2022-06-30 + + 0.00 + + BATR + YTEL + 1054.00 + 1676.00 + 0.00 + 0.00 + 0.0000 + + + BATRSMA + YTEL + 660.00 + 0.00 + 38.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 38.00 + 0.00 + 0.00 + 0.0000 + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml new file mode 100644 index 000000000..59028b67e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_BA_riktig_eksternfagsakId_ytelsestype.xml @@ -0,0 +1,47 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 0 + + + 2020-08-01 + 2020-08-31 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_3_perioder_med_renter_avrunding_ned.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_3_perioder_med_renter_avrunding_ned.xml new file mode 100644 index 000000000..bcec797c8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_3_perioder_med_renter_avrunding_ned.xml @@ -0,0 +1,96 @@ + + + + 0 + 0 + NY + EFOG + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-11-01-18.40.23.787834 + K231B433 + 1 + + + 2021-07-01 + 2021-07-31 + + 9304.00 + + EFOG + YTEL + 44093.00 + 25484.00 + 18609.00 + 0.00 + 50.0000 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 18609.00 + 0.00 + 0.00 + 50.0000 + + + + + 2021-08-01 + 2021-08-31 + + 9304.00 + + EFOG + YTEL + 44093.00 + 25484.00 + 18609.00 + 0.00 + 50.0000 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 18609.00 + 0.00 + 0.00 + 50.0000 + + + + + 2021-09-01 + 2021-09-30 + + 9304.00 + + EFOG + YTEL + 44093.00 + 25484.00 + 18609.00 + 0.00 + 50.0000 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 18609.00 + 0.00 + 0.00 + 50.0000 + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_renter.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_renter.xml new file mode 100644 index 000000000..a64d5d863 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_renter.xml @@ -0,0 +1,196 @@ + + + + 0 + 0 + NY + EFOG + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-11-01-18.40.23.787834 + K231B433 + 1 + + + 2021-03-01 + 2021-03-31 + + 243.00 + + EFOG + YTEL + 2030.00 + 1544.00 + 486.00 + 0.00 + 50.0000 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 486.00 + 0.00 + 0.00 + 50.0000 + + + + + 2021-04-01 + 2021-04-30 + + 8668.00 + + EFOG + YTEL + 20910.00 + 3574.00 + 17336.00 + 0.00 + 50.0000 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 17336.00 + 0.00 + 0.00 + 50.0000 + + + + + 2021-05-01 + 2021-05-31 + + 8620.00 + + EFOG + YTEL + 22899.00 + 5658.00 + 17241.00 + 0.00 + 49.9974 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 17241.00 + 0.00 + 0.00 + 49.9974 + + + + + 2021-06-01 + 2021-06-30 + + 0.00 + + EFOG + YTEL + 22899.00 + 5658.00 + 17241.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 17241.00 + 0.00 + 0.00 + 0.0000 + + + + + 2021-07-01 + 2021-07-31 + + 8620.00 + + EFOG + YTEL + 22899.00 + 5658.00 + 17241.00 + 0.00 + 49.9974 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 17241.00 + 0.00 + 0.00 + 49.9974 + + + + + 2021-08-01 + 2021-08-31 + + 8620.00 + + EFOG + YTEL + 22899.00 + 5658.00 + 17241.00 + 0.00 + 49.9974 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 17241.00 + 0.00 + 0.00 + 49.9974 + + + + + 2021-09-01 + 2021-09-30 + + 8682.00 + + EFOG + YTEL + 42849.00 + 25485.00 + 17364.00 + 0.00 + 50.0000 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 17364.00 + 0.00 + 0.00 + 50.0000 + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_renter_avrunding.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_renter_avrunding.xml new file mode 100644 index 000000000..94d820830 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_renter_avrunding.xml @@ -0,0 +1,196 @@ + + + + 0 + 0 + NY + EFOG + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-11-18-20.31.20.690336 + K231B433 + 1 + + + 2021-04-01 + 2021-04-30 + + 104.00 + + EFOG + YTEL + 4237.00 + 4028.00 + 209.00 + 0.00 + 49.9881 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 209.00 + 0.00 + 0.00 + 49.9881 + + + + + 2021-05-01 + 2021-05-31 + + 104.00 + + EFOG + YTEL + 5278.00 + 5070.00 + 208.00 + 0.00 + 50.0000 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 208.00 + 0.00 + 0.00 + 50.0000 + + + + + 2021-06-01 + 2021-06-30 + + 0.00 + + EFOG + YTEL + 9445.00 + 5070.00 + 4375.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 4375.00 + 0.00 + 0.00 + 0.0000 + + + + + 2021-07-01 + 2021-07-31 + + 2187.00 + + EFOG + YTEL + 9445.00 + 5070.00 + 4375.00 + 0.00 + 49.9947 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 4375.00 + 0.00 + 0.00 + 49.9947 + + + + + 2021-08-01 + 2021-08-31 + + 2187.00 + + EFOG + YTEL + 9445.00 + 5070.00 + 4375.00 + 0.00 + 49.9947 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 4375.00 + 0.00 + 0.00 + 49.9947 + + + + + 2021-09-01 + 2021-09-30 + + 1875.00 + + EFOG + YTEL + 14445.00 + 10695.00 + 3750.00 + 0.00 + 49.9965 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 3750.00 + 0.00 + 0.00 + 49.9965 + + + + + 2021-10-01 + 2021-10-31 + + 1875.00 + + EFOG + YTEL + 14445.00 + 10695.00 + 3750.00 + 0.00 + 49.9965 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 3750.00 + 0.00 + 0.00 + 49.9965 + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_renter_avrundingsfeil_ned.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_renter_avrundingsfeil_ned.xml new file mode 100644 index 000000000..f31b8709f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_renter_avrundingsfeil_ned.xml @@ -0,0 +1,46 @@ + + + + 0 + 0 + NY + EFOG + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-11-01-18.40.23.787834 + K231B433 + 1 + + + 2021-09-01 + 2021-09-30 + + 9304.00 + + EFOG + YTEL + 44093.00 + 25485.00 + 18608.00 + 0.00 + 50.0000 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 18608.00 + 0.00 + 0.00 + 50.0000 + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_skatt_avrunding.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_skatt_avrunding.xml new file mode 100644 index 000000000..6dd0fcdd7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_skatt_avrunding.xml @@ -0,0 +1,72 @@ + + + + 0 + 0 + NY + EFOG + 200000480 + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2022-01-25-21.59.44.596277 + K231B433 + 1 + + + 2021-12-01 + 2021-12-31 + + 772.00 + + EFOG + YTEL + 19950.00 + 18195.00 + 1755.00 + 0.00 + 44.0000 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 1755.00 + 0.00 + 0.00 + 44.0000 + + + + + 2022-01-01 + 2022-01-31 + + 878.00 + + EFOG + YTEL + 19950.00 + 18195.00 + 1755.00 + 0.00 + 50.0000 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 1755.00 + 0.00 + 0.00 + 50.0000 + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_skatt_avrunding_2.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_skatt_avrunding_2.xml new file mode 100644 index 000000000..91aa8a37b --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_skatt_avrunding_2.xml @@ -0,0 +1,157 @@ + + + + 0 + 0 + NY + EFOG + 2494 + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2022-03-17-23.08.37.792887 + K231B433 + 1 + + + 2021-12-01 + 2021-12-31 + + 429.00 + + EFOG + YTEL + 12607.00 + 0.00 + 2962.00 + 0.00 + 14.4998 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 2962.00 + 0.00 + 0.00 + 0.0000 + + + KL_KODE_JUST_EFOG + JUST + 0.00 + 9645.00 + 0.00 + 0.00 + 0.0000 + + + + + 2022-01-01 + 2022-01-31 + + 431.00 + + EFOG + YTEL + 12607.00 + 0.00 + 1725.00 + 0.00 + 24.9940 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 1725.00 + 0.00 + 0.00 + 0.0000 + + + KL_KODE_JUST_EFOG + JUST + 0.00 + 10882.00 + 0.00 + 0.00 + 0.0000 + + + + + 2022-02-01 + 2022-02-28 + + 262.00 + + EFOG + YTEL + 12607.00 + 0.00 + 1050.00 + 0.00 + 24.9940 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 1050.00 + 0.00 + 0.00 + 0.0000 + + + KL_KODE_JUST_EFOG + JUST + 0.00 + 11557.00 + 0.00 + 0.00 + 0.0000 + + + + + 2022-03-01 + 2022-03-31 + + 37.00 + + EFOG + YTEL + 12607.00 + 0.00 + 150.00 + 0.00 + 24.9940 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 150.00 + 0.00 + 0.00 + 0.0000 + + + KL_KODE_JUST_EFOG + JUST + 0.00 + 12457.00 + 0.00 + 0.00 + 0.0000 + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_skatt_avrunding_3.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_skatt_avrunding_3.xml new file mode 100644 index 000000000..5c0e7750e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_EF_med_skatt_avrunding_3.xml @@ -0,0 +1,148 @@ + + + + 0 + 0 + ENDR + EFOG + 1793 + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2022-06-16-22.59.46.705715 + K231B433 + 15 + + + 2021-10-01 + 2021-10-31 + + 216.00 + + EFOG + YTEL + 8782.00 + 0.00 + 637.00 + 0.00 + 33.9899 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 637.00 + 0.00 + 0.00 + 0.0000 + + + KL_KODE_JUST_EFOG + JUST + 0.00 + 8145.00 + 0.00 + 0.00 + 0.0000 + + + + + 2021-11-01 + 2021-11-30 + + 369.00 + + EFOG + YTEL + 8782.00 + 0.00 + 1087.00 + 0.00 + 33.9899 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 1087.00 + 0.00 + 0.00 + 0.0000 + + + KL_KODE_JUST_EFOG + JUST + 0.00 + 7695.00 + 0.00 + 0.00 + 0.0000 + + + + + 2021-12-01 + 2021-12-31 + + 382.00 + + EFOG + YTEL + 8782.00 + 0.00 + 2250.00 + 0.00 + 16.9892 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 2250.00 + 0.00 + 0.00 + 0.0000 + + + KL_KODE_JUST_EFOG + JUST + 0.00 + 6532.00 + 0.00 + 0.00 + 0.0000 + + + + + 2022-02-01 + 2022-02-28 + + 2985.00 + + EFOG + YTEL + 8782.00 + 0.00 + 8782.00 + 0.00 + 33.9899 + + + KL_KODE_FEIL_EFOG + FEIL + 0.00 + 8782.00 + 0.00 + 0.00 + 0.0000 + + + + \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_FEIL_postering_med_negativ_bel\303\270p.xml" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_FEIL_postering_med_negativ_bel\303\270p.xml" new file mode 100644 index 000000000..f2ca59eb5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_FEIL_postering_med_negativ_bel\303\270p.xml" @@ -0,0 +1,47 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 0 + + + 2020-08-01 + 2020-08-31 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + -2108.00 + 0.00 + 0.00 + 0.0000 + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_YTEL_postering_som_ikke_matcher_beregning.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_YTEL_postering_som_ikke_matcher_beregning.xml new file mode 100644 index 000000000..c9eb08e85 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_YTEL_postering_som_ikke_matcher_beregning.xml @@ -0,0 +1,47 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 0 + + + 2020-08-01 + 2020-08-31 + + 0.00 + + BATR + YTEL + 1000.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_overlappende_perioder.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_overlappende_perioder.xml new file mode 100644 index 000000000..3361c7750 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_overlappende_perioder.xml @@ -0,0 +1,72 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 0 + + + 2020-08-01 + 2020-08-31 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + + 2020-08-01 + 2020-08-31 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_periode_slutter_ikke_siste_dag.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_periode_slutter_ikke_siste_dag.xml new file mode 100644 index 000000000..b32a363ac --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_periode_slutter_ikke_siste_dag.xml @@ -0,0 +1,72 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 0 + + + 2020-08-01 + 2020-08-28 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + + 2020-09-01 + 2020-09-30 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_periode_starter_ikke_f\303\270rste_dag.xml" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_periode_starter_ikke_f\303\270rste_dag.xml" new file mode 100644 index 000000000..6957b60d5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_periode_starter_ikke_f\303\270rste_dag.xml" @@ -0,0 +1,72 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 0 + + + 2020-08-15 + 2020-08-31 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + + 2020-09-01 + 2020-09-30 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_periode_utenfor_kalenderm\303\245ned.xml" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_periode_utenfor_kalenderm\303\245ned.xml" new file mode 100644 index 000000000..67718779a --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_periode_utenfor_kalenderm\303\245ned.xml" @@ -0,0 +1,47 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 0 + + + 2020-08-01 + 2020-09-30 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_posteringsskatt_matcher_ikke_med_m\303\245nedlig_skatt_bel\303\270p.xml" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_posteringsskatt_matcher_ikke_med_m\303\245nedlig_skatt_bel\303\270p.xml" new file mode 100644 index 000000000..e95364a25 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_posteringsskatt_matcher_ikke_med_m\303\245nedlig_skatt_bel\303\270p.xml" @@ -0,0 +1,47 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 0 + + + 2020-08-01 + 2020-08-31 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 10.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 10.0000 + + + + diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_samme_m\303\245ned_har_ulike_m\303\245nedlig_skatt_bel\303\270p.xml" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_samme_m\303\245ned_har_ulike_m\303\245nedlig_skatt_bel\303\270p.xml" new file mode 100644 index 000000000..7edd2973e --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_samme_m\303\245ned_har_ulike_m\303\245nedlig_skatt_bel\303\270p.xml" @@ -0,0 +1,72 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 0 + + + 2020-08-01 + 2020-08-14 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + + 2020-08-18 + 2020-08-31 + + 210.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 10.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 10.0000 + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_tomt_referanse.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_tomt_referanse.xml new file mode 100644 index 000000000..3b7ac369a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_tomt_referanse.xml @@ -0,0 +1,46 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + + + 2020-08-01 + 2020-08-31 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_ugyldig_struktur.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_ugyldig_struktur.xml new file mode 100644 index 000000000..02d44b1fd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_ugyldig_struktur.xml @@ -0,0 +1,22 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 0 + + diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_ulike_total_tilbakekrevesbel\303\270p_total_nybel\303\270p.xml" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_ulike_total_tilbakekrevesbel\303\270p_total_nybel\303\270p.xml" new file mode 100644 index 000000000..c134c756b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_ulike_total_tilbakekrevesbel\303\270p_total_nybel\303\270p.xml" @@ -0,0 +1,47 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 0 + + + 2020-08-01 + 2020-08-31 + + 0.00 + + BATR + YTEL + 1500.00 + 0.00 + 1500.00 + 0.00 + 0.0000 + + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_uten_FEIL_postering.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_uten_FEIL_postering.xml new file mode 100644 index 000000000..e6dc1be06 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_uten_FEIL_postering.xml @@ -0,0 +1,38 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 0 + + + 2020-08-01 + 2020-08-31 + + 0.00 + + BATR + YTEL + 2108.00 + 0.00 + 2108.00 + 0.00 + 0.0000 + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_uten_YTEL_postering.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_uten_YTEL_postering.xml new file mode 100644 index 000000000..5e75f8715 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravgrunnlagxml/kravgrunnlag_uten_YTEL_postering.xml @@ -0,0 +1,38 @@ + + + + 0 + 0 + NY + BA + testverdi + 0 + testverdi + PERSON + testverdi + PERSON + 8020 + 8020 + 8020 + 2021-03-02-18.50.15.236315 + K231B433 + 0 + + + 2020-08-01 + 2020-08-31 + + 0.00 + + KL_KODE_FEIL_BA + FEIL + 0.00 + 2108.00 + 0.00 + 0.00 + 0.0000 + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravvedtakstatusxml/statusmelding_AVSL_BA.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravvedtakstatusxml/statusmelding_AVSL_BA.xml new file mode 100644 index 000000000..08b8bba03 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravvedtakstatusxml/statusmelding_AVSL_BA.xml @@ -0,0 +1,12 @@ + + + + 0 + AVSL + BA + testverdi + testverdi + PERSON + 0 + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravvedtakstatusxml/statusmelding_ENDR_BA.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravvedtakstatusxml/statusmelding_ENDR_BA.xml new file mode 100644 index 000000000..0004a2dd9 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravvedtakstatusxml/statusmelding_ENDR_BA.xml @@ -0,0 +1,12 @@ + + + + 0 + ENDR + BA + testverdi + testverdi + PERSON + 0 + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravvedtakstatusxml/statusmelding_SPER_BA.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravvedtakstatusxml/statusmelding_SPER_BA.xml new file mode 100644 index 000000000..f358357c7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravvedtakstatusxml/statusmelding_SPER_BA.xml @@ -0,0 +1,12 @@ + + + + 0 + SPER + BA + testverdi + testverdi + PERSON + 0 + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravvedtakstatusxml/statusmelding_SPER_ugyldig.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravvedtakstatusxml/statusmelding_SPER_ugyldig.xml new file mode 100644 index 000000000..02311cda7 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/kravvedtakstatusxml/statusmelding_SPER_ugyldig.xml @@ -0,0 +1,11 @@ + + + + 0 + SPER + BA + testverdi + testverdi + PERSON + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/logback.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/logback.xml new file mode 100644 index 000000000..0c5dd7306 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/logback.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/mockpdf/mocktest.pdf b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/mockpdf/mocktest.pdf new file mode 100644 index 000000000..7902c7bea Binary files /dev/null and b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/mockpdf/mocktest.pdf differ diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlAktorIdResponse.json b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlAktorIdResponse.json new file mode 100644 index 000000000..54927a1b5 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlAktorIdResponse.json @@ -0,0 +1,23 @@ +{ + "data": { + "pdlIdenter": { + "identer": [ + { + "ident": "21127725540", + "historisk": false, + "gruppe": "FOLKEREGISTERIDENT" + }, + { + "ident": "2872543507203", + "historisk": false, + "gruppe": "AKTORID" + }, + { + "ident": "2872543000000", + "historisk": true, + "gruppe": "AKTORID" + } + ] + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlIkkeTilgangResponse.json b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlIkkeTilgangResponse.json new file mode 100644 index 000000000..998205064 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlIkkeTilgangResponse.json @@ -0,0 +1,28 @@ +{ + "errors": [ + { + "message": "Ikke tilgang til å se person", + "locations": [ + { + "line": 2, + "column": 5 + } + ], + "path": [ + "hentPerson" + ], + "extensions": { + "code": "unauthorized", + "details": { + "type": "abac-deny", + "cause": "cause-0001-manglerrolle", + "policy": "adressebeskyttelse_strengt_fortrolig_adresse" + }, + "classification": "ExecutionAborted" + } + } + ], + "data": { + "hentPerson": null + } +} diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlOkResponseD\303\270dPerson.json" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlOkResponseD\303\270dPerson.json" new file mode 100644 index 000000000..93b8575af --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlOkResponseD\303\270dPerson.json" @@ -0,0 +1,34 @@ +{ + "data": { + "person": { + "navn": [ + { + "fornavn": "ENGASJERT", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "1955-09-13" + } + ], + "kjoenn": [ + { + "kjoenn": "MANN" + } + ], + "doedsfall": [ + { + "doedsdato": "2022-04-01" + } + ], + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "12345678910", + "status": "OPPHOERT", + "type": "FNR" + } + ] + } + } +} diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlOkResponseEnkel.json b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlOkResponseEnkel.json new file mode 100644 index 000000000..de3c2e22a --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlOkResponseEnkel.json @@ -0,0 +1,55 @@ +{ + "data": { + "person": { + "navn": [ + { + "fornavn": "ENGASJERT", + "etternavn": "FYR" + } + ], + "foedsel": [ + { + "foedselsdato": "1955-09-13" + } + ], + "kjoenn": [ + { + "kjoenn": "MANN" + } + ], + "bostedsadresse": [ + { + "vegadresse": { + "matrikkelId": "1234", + "husnummer": "3", + "husbokstav": null, + "bruksenhetsnummer": null, + "adressenavn": "OTTO SVERDRUPS VEG", + "kommunenummer": "1566", + "tilleggsnavn": null, + "postnummer": "6650" + }, + "matrikkeladresse": null, + "ukjentBosted": null + } + ], + "sivilstand": [ + { + "type": "UGIFT" + } + ], + "adressebeskyttelse": [ + { + "gradering": "UGRADERT" + } + ], + "folkeregisteridentifikator": [ + { + "identifikasjonsnummer": "12345678910", + "status": "I_BRUK", + "type": "DNR" + } + ] + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlPersonIkkeFunnetResponse.json b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlPersonIkkeFunnetResponse.json new file mode 100644 index 000000000..6bdd8af06 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/pdl/json/pdlPersonIkkeFunnetResponse.json @@ -0,0 +1,28 @@ +{ + "errors": [ + { + "message": "Person ikke funnet", + "locations": [ + { + "line": 2, + "column": 5 + } + ], + "path": [ + "hentPerson" + ], + "extensions": { + "code": "Not found", + "details": { + "type": "not-found", + "cause": "cause-0001-ikkefunnet", + "policy": "ikke-funnet" + }, + "classification": "ExecutionAborted" + } + } + ], + "data": { + "hentPerson": null + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/tilbakekrevingsvedtak/tilbakekrevingsvedtak.xml b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/tilbakekrevingsvedtak/tilbakekrevingsvedtak.xml new file mode 100644 index 000000000..660aaaee1 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/tilbakekrevingsvedtak/tilbakekrevingsvedtak.xml @@ -0,0 +1,66 @@ + + + + 8 + 0 + 2021-06-14 + 22-15 + 8020 + 2021-06-02-18.59.19.905870 + Z994619 + + + 2021-04-01 + 2021-04-30 + + 0 + + BATR + 1054 + 0 + 1054 + 0 + 0 + FULL_TILBAKEKREV + ANNET + IKKE_FORDELT + + + KL_KODE_FEIL_BA + 0 + 1054 + 0 + 0 + 0 + + + + + 2021-05-01 + 2021-05-31 + + 0 + + BATR + 1054 + 0 + 1054 + 0 + 0 + FULL_TILBAKEKREV + ANNET + IKKE_FORDELT + + + KL_KODE_FEIL_BA + 0 + 1054 + 0 + 0 + 0 + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/.editorconfig b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/.editorconfig new file mode 100644 index 000000000..88d1d56a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/.editorconfig @@ -0,0 +1,3 @@ +# unngår at editor auto-inserter newline i fasiten +[*.txt] +insert_final_newline = false diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/BA_en_periode.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/BA_en_periode.txt new file mode 100644 index 000000000..1fb08bc14 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/BA_en_periode.txt @@ -0,0 +1,39 @@ +Du har fått 595 959 kroner for mye utbetalt i barnetrygd fra og med 3. mars 2019 til og med 3. mars 2020. + +Før vi avgjør om du skal betale tilbake, kan du uttale deg innen 4. april 2020. +_Dette har skjedd +Barnetrygden din ble endret 18. desember 2019, og endringen har ført til at du har fått utbetalt for mye. + +Dette er fritekst skrevet av saksbehandler. +_Dette legger vi vekt på i vurderingen vår +For å avgjøre om vi kan kreve tilbake, tar vi først stilling til de tre øverste punktene i denne listen. Hvis resultatet blir at vi kan kreve tilbake, vurderer vi om du skal betale tilbake hele eller deler av beløpet. + +Vi legger vekt på + +*-om du forstod eller burde forstått at beløpet du fikk utbetalt var feil +om du har gitt riktig informasjon til NAV +om du har gitt all informasjon til NAV i rett tid +hvor lang tid det har gått siden barnetrygden ble feilutbetalt +hvor stort det feilutbetalte beløpet er +om feilen kan skyldes NAV-* + +Selv om det er NAV som er skyld i feilutbetalingen, kan vi likevel kreve at du betaler tilbake pengene. + +Dette går fram av barnetrygdloven § 13. +_Slik uttaler du deg +Du kan sende uttalelsen din ved å logge deg inn på nav.no/beskjedtilnav og velge «Send beskjed til NAV». Du kan også sende uttalelsen din til oss i posten. Adressen finner du på +nav.no/ettersendelser. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/barnetrygd. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Familie- og pensjonsytelser Skien + +Bob \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/KS_en_periode.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/KS_en_periode.txt new file mode 100644 index 000000000..237560189 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/KS_en_periode.txt @@ -0,0 +1,39 @@ +Du har fått 595 959 kroner for mye utbetalt i kontantstøtte fra og med 3. mars 2019 til og med 3. mars 2020. + +Før vi avgjør om du skal betale tilbake, kan du uttale deg innen 4. april 2020. +_Dette har skjedd +Kontantstøtten din ble endret 18. desember 2019, og endringen har ført til at du har fått utbetalt for mye. + +Dette er fritekst skrevet av saksbehandler. +_Dette legger vi vekt på i vurderingen vår +For å avgjøre om vi kan kreve tilbake, tar vi først stilling til de tre øverste punktene i denne listen. Hvis resultatet blir at vi kan kreve tilbake, vurderer vi om du skal betale tilbake hele eller deler av beløpet. + +Vi legger vekt på + +*-om du forstod eller burde forstått at beløpet du fikk utbetalt var feil +om du har gitt riktig informasjon til NAV +om du har gitt all informasjon til NAV i rett tid +hvor lang tid det har gått siden kontantstøtten ble feilutbetalt +hvor stort det feilutbetalte beløpet er +om feilen kan skyldes NAV-* + +Selv om det er NAV som er skyld i feilutbetalingen, kan vi likevel kreve at du betaler tilbake pengene. + +Dette går fram av kontantstøtteloven § 11. +_Slik uttaler du deg +Du kan sende uttalelsen din ved å logge deg inn på nav.no/beskjedtilnav og velge «Send beskjed til NAV». Du kan også sende uttalelsen din til oss i posten. Adressen finner du på +nav.no/ettersendelser. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/kontantstotte. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Familie- og pensjonsytelser Skien + +Bob \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode.txt new file mode 100644 index 000000000..abde96f21 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode.txt @@ -0,0 +1,46 @@ +Du har fått 595 959 kroner for mye utbetalt i overgangsstønad fra og med 3. mars 2019 til og med 3. mars 2020. Dette er før skatt. + +Før vi avgjør om du skal betale tilbake, kan du uttale deg innen 4. april 2020. + +Hvis du må betale tilbake, reduserer vi beløpet med trukket skatt. +_Dette har skjedd +Overgangsstønaden din ble endret 18. desember 2019, og endringen har ført til at du har fått utbetalt for mye. + +Dette er fritekst skrevet av saksbehandler. +_Dette legger vi vekt på i vurderingen vår +For å avgjøre om vi kan kreve tilbake, tar vi først stilling til de tre øverste punktene i denne listen. Hvis resultatet blir at vi kan kreve tilbake, vurderer vi om du skal betale tilbake hele eller deler av beløpet. + +Vi legger vekt på + +*-om du forstod eller burde forstått at beløpet du fikk utbetalt var feil +om du har gitt riktig informasjon til NAV +om du har gitt all informasjon til NAV i rett tid +hvor lang tid det har gått siden overgangsstønaden ble feilutbetalt +hvor stort det feilutbetalte beløpet er +om feilen kan skyldes NAV-* + +Selv om det er NAV som er skyld i feilutbetalingen, kan vi likevel kreve at du betaler tilbake pengene. + +Hvis du må betale tilbake, og du har gitt oss feil eller mangelfull informasjon, kan vi kreve at du betaler et rentetillegg på ti prosent av beløpet. + +Dette går fram av folketrygdloven §§ 22-15 og 22-17a. +_Slik uttaler du deg +Du kan sende uttalelsen din ved å logge deg inn på nav.no/beskjedtilnav og velge «Send beskjed til NAV». Du kan også sende uttalelsen din til oss i posten. Adressen finner du på +nav.no/ettersendelser. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +Bob + + +Vedlegg: Oversikt feilutbetalt beløp per måned \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_d\303\270dsfall.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_d\303\270dsfall.txt" new file mode 100644 index 000000000..1337fdda7 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_d\303\270dsfall.txt" @@ -0,0 +1,45 @@ +Det er utbetalt 595 959 kroner for mye i overgangsstønad fra og med 3. mars 2019 til og med 3. mars 2020. Dette er før skatt. + +Før vi avgjør om pengene skal betales tilbake, kan dere uttale dere innen 4. april 2020. + +Hvis det må betales tilbake, reduserer vi beløpet med trukket skatt. +_Dette har skjedd +Overgangsstønaden ble endret 18. desember 2019, og endringen har ført til at det er utbetalt for mye. + +Dette er fritekst skrevet av saksbehandler. +_Dette legger vi vekt på i vurderingen vår +For å avgjøre om vi kan kreve tilbake, tar vi først stilling til de to øverste punktene i denne listen. Hvis resultatet blir at vi kan kreve tilbake, vurderer vi om hele eller deler av beløpet skal betales tilbake. + +Vi legger vekt på + +*-om dere forstod eller burde forstått at beløpet som er utbetalt var feil +om vi har fått informasjon fra dere +hvor lang tid det har gått siden overgangsstønaden ble feilutbetalt +hvor stort det feilutbetalte beløpet er +om feilen kan skyldes NAV-* + +Selv om det er NAV som er skyld i feilutbetalingen, kan vi likevel kreve at feilutbetalt beløp betales tilbake. + +Hvis dere må betale tilbake, og dere har gitt oss feil eller mangelfull informasjon, kan vi kreve at dere betaler et rentetillegg på ti prosent av beløpet. + +Dette går fram av folketrygdloven §§ 22-15 og 22-17a. +_Rett til uttalelse +Dere kan sende uttalelsen til oss i posten. Adressen finner dere på nav.no/ettersendelser. +Dere kan også ta kontakt med oss på nav.no/kontakt. +_Rett til innsyn +Dere kan be om innsyn ved å ta kontakt med oss. +_Har dere spørsmål? +Dere finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan dere chatte eller skrive til oss. + +Hvis dere ikke finner svar på nav.no kan dere ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +Bob + + +Vedlegg: Oversikt feilutbetalt beløp per måned \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_institusjon.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_institusjon.txt new file mode 100644 index 000000000..f269a036e --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_institusjon.txt @@ -0,0 +1,46 @@ +Institusjonen har fått 595 959 kroner for mye utbetalt i overgangsstønad fra og med 3. mars 2019 til og med 3. mars 2020. Dette er før skatt. + +Før vi avgjør om pengene skal betales tilbake, kan dere uttale dere innen 4. april 2020. + +Hvis det må betales tilbake, reduserer vi beløpet med trukket skatt. +_Dette har skjedd +Overgangsstønaden ble endret 18. desember 2019, og endringen har ført til at institusjonen har fått utbetalt for mye. + +Dette er fritekst skrevet av saksbehandler. +_Dette legger vi vekt på i vurderingen vår +For å avgjøre om vi kan kreve tilbake, tar vi først stilling til de tre øverste punktene i denne listen. Hvis resultatet blir at vi kan kreve tilbake, vurderer vi om dere skal betale tilbake hele eller deler av beløpet. + +Vi legger vekt på + +*-om dere forstod eller burde forstått at beløpet dere fikk utbetalt var feil +om dere har gitt riktig informasjon til NAV +om dere har gitt all informasjon til NAV i rett tid +hvor lang tid det har gått siden overgangsstønaden ble feilutbetalt +hvor stort det feilutbetalte beløpet er +om feilen kan skyldes NAV-* + +Selv om det er NAV som er skyld i feilutbetalingen, kan vi likevel kreve at feilutbetalt beløp betales tilbake. + +Hvis dere må betale tilbake, og dere har gitt oss feil eller mangelfull informasjon, kan vi kreve at dere betaler et rentetillegg på ti prosent av beløpet. + +Dette går fram av folketrygdloven §§ 22-15 og 22-17a. +_Rett til uttalelse +Dere kan sende uttalelsen til oss i posten. Adressen finner dere på nav.no/ettersendelser. +Dere kan også ta kontakt med oss på nav.no/kontakt. +_Rett til innsyn +Dere kan be om innsyn ved å ta kontakt med oss. +_Har dere spørsmål? +Dere finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan dere chatte eller skrive til oss. + +Hvis dere ikke finner svar på nav.no kan dere ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +Bob + + +Vedlegg: Oversikt feilutbetalt beløp per måned \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_institusjon_nn.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_institusjon_nn.txt new file mode 100644 index 000000000..5139a0d0d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_institusjon_nn.txt @@ -0,0 +1,46 @@ +Institusjonen har fått 595 959 kroner for mykje utbetalt i overgangsstønad frå og med 3. mars 2019 til og med 3. mars 2020. Dette er før skatt. + +Før vi avgjer om pengane skal betalast tilbake, kan de uttale dykk innan 4. april 2020. + +Dersom det må betalast tilbake, reduserer vi beløpet med trekt skatt. +_Dette har skjedd +Overgangsstønaden blei endra 18. desember 2019, og endringa har ført til at institusjonen har fått utbetalt for mykje. + +Dette er fritekst skrevet av saksbehandler. +_Dette legg vi vekt på i vurderinga vår +For å avgjere om vi kan krevje tilbake, tek vi først stilling til dei tre øvste punkta i denne lista. Dersom resultatet blir at vi kan krevje tilbake, vurderer vi om de skal betale tilbake heile eller delar av beløpet. + +Vi legg vekt på + +*-om de forstod eller burde forstått at beløpet som er utbetalt var feil +om de har gitt riktig informasjon til NAV +om de har gitt all informasjon til NAV i rett tid +kor lang tid det har gått sidan overgangsstønaden blei feilutbetalt +kor stort det feilutbetalte beløpet er +om feilen kan vere NAV si skuld-* + +Sjølv om det er NAV som er skuld i feilutbetalinga, kan vi likevel krevje at feilutbetalt beløp vert betalt tilbake. + +Dersom institusjonen må betale tilbake, og institusjonen har gitt oss feil eller mangelfull informasjon, kan vi krevje at institusjonen betaler eit rentetillegg på ti prosent av beløpet institusjonen skuldar. + +Dette går fram av folketrygdlova §§ 22-15 og 22-17a. +_Rett til uttale +De kan sende uttalen til oss i posten. Adressa finn de på nav.no/ettersendelser. +De kan også ta kontakt med oss på nav.no/kontakt. +_Rett til innsyn +De kan be om innsyn ved å ta kontakt med oss. +_Har de spørsmål? +De finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan de chatte eller skrive til oss. + +Om de ikkje finn svar på nav.no, kan de ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +Bob + + +Vedlegg: Oversikt feilutbetalt beløp per måned \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_korrigert_institusjon.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_korrigert_institusjon.txt new file mode 100644 index 000000000..10957bace --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_korrigert_institusjon.txt @@ -0,0 +1,51 @@ +Vi varslet 26. september 2023 om at det var utbetalt 5 000 kroner for mye i overgangsstønad før skatt. + +Riktig beløp som er utbetalt for mye, er 595 959 kroner før skatt. + +Perioden dere har fått for mye utbetalt for, er: +*- +Fra og med 3. mars 2019 til og med 3. mars 2020.-* +Før vi avgjør om pengene skal betales tilbake, kan dere uttale dere innen 4. april 2020. + +Hvis det må betales tilbake, reduserer vi beløpet med trukket skatt. +_Dette har skjedd +Overgangsstønaden ble endret 18. desember 2019, og endringen har ført til at institusjonen har fått utbetalt for mye. + +Dette er fritekst skrevet av saksbehandler. +_Dette legger vi vekt på i vurderingen vår +For å avgjøre om vi kan kreve tilbake, tar vi først stilling til de tre øverste punktene i denne listen. Hvis resultatet blir at vi kan kreve tilbake, vurderer vi om dere skal betale tilbake hele eller deler av beløpet. + +Vi legger vekt på + +*-om dere forstod eller burde forstått at beløpet dere fikk utbetalt var feil +om dere har gitt riktig informasjon til NAV +om dere har gitt all informasjon til NAV i rett tid +hvor lang tid det har gått siden overgangsstønaden ble feilutbetalt +hvor stort det feilutbetalte beløpet er +om feilen kan skyldes NAV-* + +Selv om det er NAV som er skyld i feilutbetalingen, kan vi likevel kreve at feilutbetalt beløp betales tilbake. + +Hvis dere må betale tilbake, og dere har gitt oss feil eller mangelfull informasjon, kan vi kreve at dere betaler et rentetillegg på ti prosent av beløpet. + +Dette går fram av folketrygdloven §§ 22-15 og 22-17a. +_Rett til uttalelse +Dere kan sende uttalelsen til oss i posten. Adressen finner dere på nav.no/ettersendelser. +Dere kan også ta kontakt med oss på nav.no/kontakt. +_Rett til innsyn +Dere kan be om innsyn ved å ta kontakt med oss. +_Har dere spørsmål? +Dere finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan dere chatte eller skrive til oss. + +Hvis dere ikke finner svar på nav.no kan dere ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +Bob + + +Vedlegg: Oversikt feilutbetalt beløp per måned \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_korrigert_institusjon_nn.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_korrigert_institusjon_nn.txt new file mode 100644 index 000000000..5c0e6cd56 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_en_periode_korrigert_institusjon_nn.txt @@ -0,0 +1,51 @@ +Vi varslet 26. september 2023 om at det var utbetalt 5 000 kroner for mykje i overgangsstønad før skatt. + +Riktig beløp som er utbetalt for mykje, er 595 959 kroner før skatt. + +Perioden de har fått for mykje utbetalt for, er: +*- +Frå og med 3. mars 2019 til og med 3. mars 2020.-* +Før vi avgjer om pengane skal betalast tilbake, kan de uttale dykk innan 4. april 2020. + +Dersom det må betalast tilbake, reduserer vi beløpet med trekt skatt. +_Dette har skjedd +Overgangsstønaden blei endra 18. desember 2019, og endringa har ført til at institusjonen har fått utbetalt for mykje. + +Dette er fritekst skrevet av saksbehandler. +_Dette legg vi vekt på i vurderinga vår +For å avgjere om vi kan krevje tilbake, tek vi først stilling til dei tre øvste punkta i denne lista. Dersom resultatet blir at vi kan krevje tilbake, vurderer vi om de skal betale tilbake heile eller delar av beløpet. + +Vi legg vekt på + +*-om de forstod eller burde forstått at beløpet som er utbetalt var feil +om de har gitt riktig informasjon til NAV +om de har gitt all informasjon til NAV i rett tid +kor lang tid det har gått sidan overgangsstønaden blei feilutbetalt +kor stort det feilutbetalte beløpet er +om feilen kan vere NAV si skuld-* + +Sjølv om det er NAV som er skuld i feilutbetalinga, kan vi likevel krevje at feilutbetalt beløp vert betalt tilbake. + +Dersom institusjonen må betale tilbake, og institusjonen har gitt oss feil eller mangelfull informasjon, kan vi krevje at institusjonen betaler eit rentetillegg på ti prosent av beløpet institusjonen skuldar. + +Dette går fram av folketrygdlova §§ 22-15 og 22-17a. +_Rett til uttale +De kan sende uttalen til oss i posten. Adressa finn de på nav.no/ettersendelser. +De kan også ta kontakt med oss på nav.no/kontakt. +_Rett til innsyn +De kan be om innsyn ved å ta kontakt med oss. +_Har de spørsmål? +De finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan de chatte eller skrive til oss. + +Om de ikkje finn svar på nav.no, kan de ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +Bob + + +Vedlegg: Oversikt feilutbetalt beløp per måned \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_flere_perioder_d\303\270dsfall_nn.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_flere_perioder_d\303\270dsfall_nn.txt" new file mode 100644 index 000000000..423bfb97b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_flere_perioder_d\303\270dsfall_nn.txt" @@ -0,0 +1,49 @@ +Det er utbetalt 595 959 kroner for mykje i overgangsstønad. Dette er før skatt. + +Periodane det er for mykje utbetalt for, er: +*- +Frå og med 3. mars 2019 til og med 3. mars 2020. +Frå og med 3. mars 2022 til og med 3. mars 2024.-* +Før vi avgjer om pengane skal betalast tilbake, kan de uttale dykk innan 4. april 2020. + +Dersom det må betalast tilbake, reduserer vi beløpet med trekt skatt. +_Dette har skjedd +Overgangsstønaden blei endra 18. desember 2019, og endringa har ført til at det er utbetalt for mykje. + +Dette er fritekst skrevet av saksbehandler. +_Dette legg vi vekt på i vurderinga vår +For å avgjere om vi kan krevje tilbake, tek vi først stilling til dei to øvste punkta i denne lista. Dersom resultatet blir at vi kan krevje tilbake, vurderer vi om heile eller delar av beløpet skal betalast tilbake. + +Vi legg vekt på + +*-om de forstod eller burde forstått at beløpet som er utbetalt var feil +om vi har fått informasjon frå dykk +kor lang tid det har gått sidan overgangsstønaden blei feilutbetalt +kor stort det feilutbetalte beløpet er +om feilen kan vere NAV si skuld-* + +Sjølv om det er NAV som er skuld i feilutbetalinga, kan vi likevel krevje at feilutbetalt beløp vert betalt tilbake. + +Dersom de må betale tilbake, og de har gitt oss feil eller mangelfull informasjon, kan vi krevje at de betaler eit rentetillegg på ti prosent av beløpet de skuldar. + +Dette går fram av folketrygdlova §§ 22-15 og 22-17a. +_Rett til uttale +De kan sende uttalen til oss i posten. Adressa finn de på nav.no/ettersendelser. +De kan også ta kontakt med oss på nav.no/kontakt. +_Rett til innsyn +De kan be om innsyn ved å ta kontakt med oss. +_Har de spørsmål? +De finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan de chatte eller skrive til oss. + +Om de ikkje finn svar på nav.no, kan de ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +Bob + + +Vedlegg: Oversikt feilutbetalt beløp per måned \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_flere_perioder_nn.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_flere_perioder_nn.txt new file mode 100644 index 000000000..fa6a095ce --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/OS_flere_perioder_nn.txt @@ -0,0 +1,50 @@ +Du har fått 595 959 kroner for mykje utbetalt i overgangsstønad. Dette er før skatt. + +Periodane du har fått for mykje utbetalt for, er: +*- +Frå og med 3. mars 2019 til og med 3. mars 2020. +Frå og med 3. mars 2022 til og med 3. mars 2024.-* +Før vi avgjer om du skal betale tilbake, kan du uttale deg innan 4. april 2020. + +Dersom du må betale tilbake, reduserer vi beløpet med trekt skatt. +_Dette har skjedd +Overgangsstønaden din blei endra 18. desember 2019, og endringa har ført til at du har fått utbetalt for mykje. + +Dette er fritekst skrevet av saksbehandler. +_Dette legg vi vekt på i vurderinga vår +For å avgjere om vi kan krevje tilbake, tek vi først stilling til dei tre øvste punkta i denne lista. Dersom resultatet blir at vi kan krevje tilbake, vurderer vi om du skal betale tilbake heile eller delar av beløpet. + +Vi legg vekt på + +*-om du forstod eller burde forstått at beløpet du fekk utbetalt var feil +om du har gitt riktig informasjon til NAV +om du har gitt all informasjon til NAV i rett tid +kor lang tid det har gått sidan overgangsstønaden blei feilutbetalt +kor stort det feilutbetalte beløpet er +om feilen kan vere NAV si skuld-* + +Sjølv om det er NAV som er skuld i feilutbetalinga, kan vi likevel krevje at du betaler tilbake pengane. + +Dersom du må betale tilbake, og du har gitt oss feil eller mangelfull informasjon, kan vi krevje at du betaler eit rentetillegg på ti prosent av beløpet du skuldar. + +Dette går fram av folketrygdlova §§ 22-15 og 22-17a. +_Slik uttaler du deg +Du kan sende uttalen din ved å logge deg inn på nav.no/beskjedtilnav og velje «Send beskjed til NAV». Du kan også sende uttalen din til oss i posten. Adressa finn du på +nav.no/ettersendelser. +_Du har rett til innsyn +På nav.no/dittnav kan du sjå dokumenta i saka di. +_Har du spørsmål? +Du finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Om du ikkje finn svar på nav.no, kan du ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +Bob + + +Vedlegg: Oversikt feilutbetalt beløp per måned \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/vedlegg/vedlegg_nb_med_skatt.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/vedlegg/vedlegg_nb_med_skatt.txt new file mode 100644 index 000000000..f423be99d --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/vedlegg/vedlegg_nb_med_skatt.txt @@ -0,0 +1,19 @@ +

    Feilutbetalt beløp per måned

    + + + + + + + + + + + + + + + + + +
    PeriodeUtbetalt beløp før skatt er trukket fraNytt beregnet beløp før skatt er trukket fraFeilutbetalt beløp før skatt er trukket fra
    januar 20221 573 kroner1 572 kroner1 574 kroner
    \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/vedlegg/vedlegg_nb_uten_skatt.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/vedlegg/vedlegg_nb_uten_skatt.txt new file mode 100644 index 000000000..9d8171a25 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/vedlegg/vedlegg_nb_uten_skatt.txt @@ -0,0 +1,19 @@ +

    Feilutbetalt beløp per måned

    + + + + + + + + + + + + + + + + + +
    PeriodeUtbetalt beløpNytt beregnet beløpFeilutbetalt beløp
    januar 20221 573 kroner1 572 kroner1 574 kroner
    \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/vedlegg/vedlegg_nn_med_skatt.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/vedlegg/vedlegg_nn_med_skatt.txt new file mode 100644 index 000000000..bd04ca70f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/vedlegg/vedlegg_nn_med_skatt.txt @@ -0,0 +1,19 @@ +

    Feilutbetalt beløp per måned

    + + + + + + + + + + + + + + + + + +
    PeriodeUtbetalt beløp før skatt er trekt fråNytt beregna beløp før skatt er trekt fråFeilutbetalt beløp før skatt er trekt frå
    januar 20221 573 kroner1 572 kroner1 574 kroner
    \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/vedlegg/vedlegg_nn_uten_skatt.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/vedlegg/vedlegg_nn_uten_skatt.txt new file mode 100644 index 000000000..6478d954f --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/vedlegg/vedlegg_nn_uten_skatt.txt @@ -0,0 +1,19 @@ +

    Feilutbetalt beløp per måned

    + + + + + + + + + + + + + + + + + +
    PeriodeUtbetalt beløpNytt beregna beløpFeilutbetalt beløp
    januar 20221 573 kroner1 572 kroner1 574 kroner
    \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/verge.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/verge.txt new file mode 100644 index 000000000..859264626 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/varselbrev/verge.txt @@ -0,0 +1 @@ +Brev med likt innhold er sendt til John Doe \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/.editorconfig b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/.editorconfig new file mode 100644 index 000000000..88d1d56a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/.editorconfig @@ -0,0 +1,3 @@ +# unngår at editor auto-inserter newline i fasiten +[*.txt] +insert_final_newline = false diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/BA_ikke_tilbakekreves_pga_lavt_bel\303\270p_d\303\270d_bruker.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/BA_ikke_tilbakekreves_pga_lavt_bel\303\270p_d\303\270d_bruker.txt" new file mode 100644 index 000000000..0b2b8d934 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/BA_ikke_tilbakekreves_pga_lavt_bel\303\270p_d\303\270d_bruker.txt" @@ -0,0 +1,30 @@ +Vi varslet 4. april 2020 om at det er utbetalt 500 kroner for mye. Det feilutbetalte beløpet må ikke betales tilbake. +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Vi har fått melding om at Søker Søkersen døde. Barnetrygden skulle vært stanset fra og med 1. januar 2019. + +Fordi barnetrygden er utbetalt etter denne datoen er det utbetalt 500 kroner for mye. + +foo bar baz +__Hvordan har vi kommet fram til at barnetrygden ikke må betales tilbake? +Selv om dere burde forstått at barnetrygden er utbetalt ved en feil, kreves ikke pengene tilbake. Det er fordi feilutbetalt beløp er lavere enn 4 972 kroner. + +Vedtaket er gjort etter Folketrygdloven § 22-15 6.ledd. +_Rett til å klage +Dere kan klage innen 6 uker fra den datoen dere mottok vedtaket. Dere finner skjema og informasjon på nav.no/klage. +_Rett til innsyn +Dere kan be om innsyn ved å ta kontakt med oss. +_Har dere spørsmål? +Dere finner mer informasjon på nav.no/barnetrygd. + +På nav.no/kontakt kan dere chatte eller skrive til oss. + +Hvis dere ikke finner svar på nav.no kan dere ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Familie- og pensjonsytelser Skien + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/BA_ikke_tilbakekreves_pga_lavt_bel\303\270p_d\303\270d_bruker_nynorsk.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/BA_ikke_tilbakekreves_pga_lavt_bel\303\270p_d\303\270d_bruker_nynorsk.txt" new file mode 100644 index 000000000..20f76977f --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/BA_ikke_tilbakekreves_pga_lavt_bel\303\270p_d\303\270d_bruker_nynorsk.txt" @@ -0,0 +1,30 @@ +Vi varsla 4. april 2020 om at det er utbetalt 500 kroner for mykje. Det feilutbetalte beløpet må ikkje betalast tilbake. +_Perioden frå og med 1. januar 2019 til og med 31. januar 2019 +Vi har fått melding om at Søker Søkersen døydde. Barnetrygda skulle vore stansa frå og med 1. januar 2019. + +Fordi barnetrygda er utbetalt etter denne datoen er det utbetalt 500 kroner for mykje. +__Korleis har vi kome fram til at barnetrygda ikkje må betalast tilbake? +Sjølv om de burde ha forstått at barnetrygda er utbetalt ved ein feil, vert ikkje pengane krevd tilbake. Det er fordi feilutbetalt beløp er lågare enn 4 972 kroner. + +foo bar baz + +Vedtaket er gjort etter Folketrygdloven § 22-15 6.ledd. +_Rett til å klage +De kan klage innan 6 veker frå den datoen de fekk vedtaket. De finn skjema og informasjon på nav.no/klage. +_Rett til innsyn +De kan be om innsyn ved å ta kontakt med oss. +_Har de spørsmål? +De finn meir informasjon på nav.no/barnetrygd. + +På nav.no/kontakt kan de chatte eller skrive til oss. + +Om de ikkje finn svar på nav.no, kan de ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Familie- og pensjonsytelser Skien + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaka \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/Fritekst_Vedtaksbrev_KS_ingen_tilbakebetaling.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/Fritekst_Vedtaksbrev_KS_ingen_tilbakebetaling.txt new file mode 100644 index 000000000..946e4c5b4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/Fritekst_Vedtaksbrev_KS_ingen_tilbakebetaling.txt @@ -0,0 +1,20 @@ +sender fritekst vedtaksbrev + +_Lovhjemmelen vi har brukt +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Du har rett til å klage +Du kan klage innen 6 uker fra den datoen du mottok vedtaket. Du finner skjema og informasjon på nav.no/klage. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/kontantstotte. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Familie- og pensjonsytelser Skien + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/Fritekst_Vedtaksbrev_OS_full_tilbakebetaling.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/Fritekst_Vedtaksbrev_OS_full_tilbakebetaling.txt new file mode 100644 index 000000000..74f54a6d3 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/Fritekst_Vedtaksbrev_OS_full_tilbakebetaling.txt @@ -0,0 +1,27 @@ +sender fritekst vedtaksbrev + +_Lovhjemmelen vi har brukt +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Skatt og skatteoppgjør +NAV gir opplysninger til Skatteetaten. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjør. +_Hvordan betaler du tilbake? +Skatteetaten vil korrigere beløpet du skal betale tilbake. Du finner mer informasjon på +skatteetaten.no/betale. +_Du har rett til å klage +Du kan klage innen 6 uker fra den datoen du mottok vedtaket. Du finner skjema og informasjon på nav.no/klage. + +Du må som hovedregel begynne å betale tilbake beløpet når du får fakturaen, selv om du klager på dette vedtaket. Dette følger av forvaltningsloven § 42. Hvis du får vedtak om at du ikke trengte å betale tilbake hele eller deler av beløpet du skyldte, betaler vi pengene tilbake til deg. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/KS_forsett.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/KS_forsett.txt new file mode 100644 index 000000000..9f2e847a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/KS_forsett.txt @@ -0,0 +1,39 @@ +Du fikk varsel fra oss 4. april 2020 om at du har fått utbetalt for mye. Du må betale tilbake 10 000 kroner, som er hele det feilutbetalte beløpet. + +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Dette er svindel! + +Du har fått 10 000 kroner for mye utbetalt. +__Hvordan har vi kommet fram til at du må betale tilbake? +Du har fått vite om du har rett til kontantstøtte og hvor mye du har rett til. Selv hvis du har meldt fra til oss, kan vi kreve tilbake det du har fått for mye hvis du forsto at beløpet var feil. At du må betale tilbake, betyr ikke at du selv har skyld i feilutbetalingen. + +Ut fra informasjonen du har fått, legger vi til grunn at du forsto at du fikk utbetalt for mye. Derfor kan vi kreve tilbake. + +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Hvordan betaler du tilbake? +Du vil få faktura fra Skatteetaten på det beløpet du skal betale tilbake. + +På fakturaen vil det stå informasjon om nøyaktig beløp, kontonummer og forfallsdato. Du trenger ikke å gjøre noe før du får fakturaen. + +Du finner mer informasjon på skatteetaten.no/betale. +_Du har rett til å klage +Du kan klage innen 6 uker fra den datoen du mottok vedtaket. Du finner skjema og informasjon på nav.no/klage. + +Du må som hovedregel begynne å betale tilbake beløpet når du får fakturaen, selv om du klager på dette vedtaket. Dette følger av forvaltningsloven § 42. Hvis du får vedtak om at du ikke trengte å betale tilbake hele eller deler av beløpet du skyldte, betaler vi pengene tilbake til deg. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/kontantstotte. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Familie- og pensjonsytelser Skien + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_delvis_foreldelse_uten_varsel.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_delvis_foreldelse_uten_varsel.txt new file mode 100644 index 000000000..8d1a814bd --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_delvis_foreldelse_uten_varsel.txt @@ -0,0 +1,51 @@ +I brev 12. november 2019 fikk du melding om at overgangsstønaden din er endret. Endringen førte til at du har fått utbetalt for mye. Du må betale tilbake 1 000 kroner, som er deler av det feilutbetalte beløpet. +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Du har fått overgangsstønad for barn som ikke bor fast hos deg. Du har derfor fått 1 000 kroner for mye utbetalt i denne perioden. +__Hvordan har vi kommet fram til at du ikke må betale tilbake? +Fristen for å kreve tilbake overgangsstønaden er tre år og starter fra det tidspunktet feilutbetalingen skjedde. + +Foreldelsesfristen er 1. desember 2019. Vi kan ikke kreve tilbake penger som er blitt utbetalt mer enn tre år før denne datoen. + +Du skal derfor ikke betale tilbake 1 000 kroner, som er feilutbetalt fra og med 1. januar 2019 til og med 31. januar 2019. + +_Perioden fra og med 1. februar 2019 til og med 28. februar 2019 +Du er ikke medlem av folketrygden fordi du ikke har oppholdstillatelse i Norge. Derfor har du ikke rett til overgangsstønad. + +Du har fått 1 000 kroner for mye utbetalt. +__Hvordan har vi kommet fram til at du må betale tilbake? +Fristen for å kreve tilbake overgangsstønaden er tre år fra det tidspunktet feilutbetalingen skjedde. Vi kan utvide fristen med ytterligere 10 år hvis det ikke var mulig for NAV å oppdage feilen tidligere. Fra tidspunktet vi oppdaget feilen, har vi ett år på å kreve pengene tilbake. + +Vi har oppdaget feilutbetalingen 1. september 2019, og har behandlet saken din innen ett år. Derfor må du betale tilbake overgangsstønaden du har fått fra og med 1. februar 2019 til og med 28. februar 2019. + +Vi har ikke gitt deg den informasjonen du trengte for å forstå at beløpet du fikk utbetalt var feil. Du har opplyst at du ikke har brukt 1 000 kroner. Disse kan vi kreve betale, selv om du ikke forstod at utbetalingen var feil. +_Lovhjemmelen vi har brukt +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Skatt og skatteoppgjør +NAV gir opplysninger til Skatteetaten. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjør. +_Hvordan betaler du tilbake? +Du vil få faktura fra Skatteetaten på det beløpet du skal betale tilbake. + +På fakturaen vil det stå informasjon om nøyaktig beløp, kontonummer og forfallsdato. Du trenger ikke å gjøre noe før du får fakturaen. + +Du finner mer informasjon på skatteetaten.no/betale. +_Du har rett til å klage +Du kan klage innen 6 uker fra den datoen du mottok vedtaket. Du finner skjema og informasjon på nav.no/klage. + +Du må som hovedregel begynne å betale tilbake beløpet når du får fakturaen, selv om du klager på dette vedtaket. Dette følger av forvaltningsloven § 42. Hvis du får vedtak om at du ikke trengte å betale tilbake hele eller deler av beløpet du skyldte, betaler vi pengene tilbake til deg. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_forsett.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_forsett.txt new file mode 100644 index 000000000..0cc883fec --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_forsett.txt @@ -0,0 +1,45 @@ +Du fikk varsel fra oss 4. april 2020 om at du har fått utbetalt for mye. Beløpet du skylder før skatt er 11 000 kroner. Dette er hele det feilutbetalte beløpet. Dette beløpet er med renter. + +Det du skal betale tilbake etter at skatten er trukket fra, er 7 011 kroner. + +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Du har fått overgangsstønad for barn som ikke bor fast hos deg. Du har derfor fått 10 000 kroner for mye utbetalt i denne perioden. +__Hvordan har vi kommet fram til at du må betale tilbake? +Du har fått vite om du har rett til overgangsstønad og hvor mye du har rett til. Selv hvis du har meldt fra til oss, kan vi kreve tilbake det du har fått for mye hvis du forsto at beløpet var feil. At du må betale tilbake, betyr ikke at du selv har skyld i feilutbetalingen. + +Ut fra informasjonen du har fått, legger vi til grunn at du forsto at du fikk utbetalt for mye. Derfor kan vi kreve tilbake. +__Renter +Etter vår vurdering forsto du at det du fikk utbetalt var feil. Derfor må du betale et rentetillegg på 10 prosent. Det vil si 1 000 kroner før skatt. Dette er i tillegg til det feilutbetalte beløpet. + +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Skatt og skatteoppgjør +Skatten som er trukket fra beløpet du skal betale tilbake, er beregnet etter det du har blitt trukket i skatt i gjennomsnitt per måned. Det betyr at beløpet du skal betale tilbake etter skatt, ikke alltid er likt med det beløpet du fikk inn på konto. + +NAV gir opplysninger til Skatteetaten om skattebeløpet og om beløpet du skal betale tilbake før skatt er trukket fra. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjør. +_Hvordan betaler du tilbake? +Du vil få faktura fra Skatteetaten på det beløpet du skal betale tilbake. + +På fakturaen vil det stå informasjon om nøyaktig beløp, kontonummer og forfallsdato. Du trenger ikke å gjøre noe før du får fakturaen. + +Du finner mer informasjon på skatteetaten.no/betale. +_Du har rett til å klage +Du kan klage innen 6 uker fra den datoen du mottok vedtaket. Du finner skjema og informasjon på nav.no/klage. + +Du må som hovedregel begynne å betale tilbake beløpet når du får fakturaen, selv om du klager på dette vedtaket. Dette følger av forvaltningsloven § 42. Hvis du får vedtak om at du ikke trengte å betale tilbake hele eller deler av beløpet du skyldte, betaler vi pengene tilbake til deg. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_fritekst_overalt.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_fritekst_overalt.txt new file mode 100644 index 000000000..1c6647d23 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_fritekst_overalt.txt @@ -0,0 +1,73 @@ +Vi har vurdert saken din om tilbakebetaling på nytt. Derfor gjelder ikke det tidligere vedtaket av 1. januar 2019 om tilbakebetaling av overgangsstønad. + +Du fikk varsel fra oss 3. januar 2019 om at du har fått utbetalt 1 234 567 893 kroner for mye. Beløpet du skylder før skatt er 1 234 567 892 kroner. Dette er deler av det feilutbetalte beløpet. + +Det du skal betale tilbake etter at skatten er trukket fra, er 1 234 567 000 kroner. + +Skynd deg å betale, vi trenger pengene med en gang! +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Ingen vet riktig hva som har skjedd, men du har fått utbetalt alt for mye penger. + +Du har fått 1 234 567 890 kroner for mye utbetalt. +__Hvordan har vi kommet fram til at du må betale tilbake? +Du har fått vite om du har rett til overgangsstønad og hvor mye du har rett til. Selv hvis du har meldt fra til oss, kan vi kreve tilbake det du har fått for mye hvis du må ha forstått at beløpet var feil. At du må betale tilbake, betyr ikke at du selv har skyld i feilutbetalingen. + +Det er helt utrolig om du ikke har oppdaget dette! + +Ut fra informasjonen du har fått, må du etter vår vurdering ha forstått at du fikk for mye utbetalt. Derfor kan vi kreve tilbake. +__Er det særlige grunner til å redusere beløpet? +Gratulerer, du fikk norgesrekord i feilutbetalt beløp! Du skal slippe å betale renter! + +Vi har vurdert om det er grunner til å redusere beløpet. Selv om vi kunne unngått at du fikk for mye utbetalt, har vi lagt vekt på at du må ha forstått at du fikk penger du ikke har rett til og at det feilutbetalte beløpet er høyt. Vi har også lagt vekt på at det er kort tid siden utbetalingen skjedde og at du jobber med OVERGANGSSTØNAD og dermed vet hvordan dette fungerer! + +Derfor må du betale tilbake hele beløpet. +_Perioden fra og med 1. februar 2019 til og med 28. februar 2019 +Du har fått overgangsstønad for barn som ikke bor fast hos deg. Du har derfor fått 1 krone for mye utbetalt i denne perioden. + +Her har økonomisystemet gjort noe helt feil. +__Hvordan har vi kommet fram til at du må betale tilbake? +Vi har ikke gitt deg den informasjonen du trengte for å forstå at beløpet du fikk utbetalt var feil. Du har opplyst at du ikke har brukt 1 krone. Disse kan vi kreve betale, selv om du ikke forstod at utbetalingen var feil. + +Vi skjønner at du ikke har oppdaget beløpet, siden du hadde så mye annet på konto. +_Perioden fra og med 1. mars 2019 til og med 31. mars 2019 +Du har fått overgangsstønad for barn som ikke bor fast hos deg. Du har derfor fått 1 krone for mye utbetalt i denne perioden. +__Hvordan har vi kommet fram til at du må betale tilbake? +Du har fått vite om du har rett til overgangsstønad og hvor mye du har rett til. Etter vår vurdering burde du forstått at opplysningene du ga oss var uriktige. Derfor kan vi kreve pengene tilbake. + +Her burde du passet mer på! +_Perioden fra og med 1. april 2019 til og med 30. april 2019 +Du har fått overgangsstønad for barn som ikke bor fast hos deg. Du har derfor fått 1 krone for mye utbetalt i denne perioden. +__Hvordan har vi kommet fram til at du må betale tilbake? +Du har fått vite om du har rett til overgangsstønad og hvor mye du har rett til. Etter vår vurdering forsto du at du ikke ga oss alle opplysningene vi trengte tidsnok for å sikre at du fikk riktig utbetaling. Derfor kan vi kreve pengene tilbake. + +Dette gjorde du med vilje! +_Lovhjemmelen vi har brukt +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Skatt og skatteoppgjør +Skatten som er trukket fra beløpet du skal betale tilbake, er beregnet etter det du har blitt trukket i skatt i gjennomsnitt per måned. Det betyr at beløpet du skal betale tilbake etter skatt, ikke alltid er likt med det beløpet du fikk inn på konto. + +NAV gir opplysninger til Skatteetaten om skattebeløpet og om beløpet du skal betale tilbake før skatt er trukket fra. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjør. +_Hvordan betaler du tilbake? +Skatteetaten vil korrigere beløpet du skal betale tilbake. Du finner mer informasjon på +skatteetaten.no/betale. +_Du har rett til å klage +Du kan klage innen 6 uker fra den datoen du mottok vedtaket. Du finner skjema og informasjon på nav.no/klage. + +Du må som hovedregel begynne å betale tilbake beløpet når du får fakturaen, selv om du klager på dette vedtaket. Dette følger av forvaltningsloven § 42. Hvis du får vedtak om at du ikke trengte å betale tilbake hele eller deler av beløpet du skyldte, betaler vi pengene tilbake til deg. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ikke_tilbakekreves_med_korrigert_bel\303\270p.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ikke_tilbakekreves_med_korrigert_bel\303\270p.txt" new file mode 100644 index 000000000..b1a86a3ee --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ikke_tilbakekreves_med_korrigert_bel\303\270p.txt" @@ -0,0 +1,28 @@ +Du fikk varsel fra oss 4. april 2020 om at du har fått utbetalt 15 000 kroner for mye. Beløpet har blitt endret. Nytt feilutbetalt beløp utgjør 1 000 kroner. Vi har behandlet saken og du må ikke betale tilbake det feilutbetalte beløpet. +_Perioden fra og med 1. januar 2019 til og med 1. januar 2019 +foo bar baz + +Du har fått 500 kroner for mye utbetalt. +__Hvordan har vi kommet fram til at du ikke må betale tilbake? +Du har fått vite om du har rett til overgangsstønad og hvor mye du har rett til. Selv om du burde forstått at beløpet var feil, er beløpet lavere enn 4 972 kroner. Du må derfor ikke betale tilbake beløpet. + +Vedtaket er gjort etter Folketrygdloven § 22-15 6.ledd. +_Du har rett til å klage +Du kan klage innen 6 uker fra den datoen du mottok vedtaket. Du finner skjema og informasjon på nav.no/klage. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ikke_tilbakekreves_pga_lavt_bel\303\270p.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ikke_tilbakekreves_pga_lavt_bel\303\270p.txt" new file mode 100644 index 000000000..de488e36c --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ikke_tilbakekreves_pga_lavt_bel\303\270p.txt" @@ -0,0 +1,28 @@ +Du fikk varsel fra oss 4. april 2020 om at du har fått utbetalt 500 kroner for mye. Vi har behandlet saken og du må ikke betale tilbake det feilutbetalte beløpet. +_Perioden fra og med 1. januar 2019 til og med 1. januar 2019 +foo bar baz + +Du har fått 500 kroner for mye utbetalt. +__Hvordan har vi kommet fram til at du ikke må betale tilbake? +Du har fått vite om du har rett til overgangsstønad og hvor mye du har rett til. Selv om du burde forstått at beløpet var feil, er beløpet lavere enn 4 972 kroner. Du må derfor ikke betale tilbake beløpet. + +Vedtaket er gjort etter Folketrygdloven § 22-15 6.ledd. +_Du har rett til å klage +Du kan klage innen 6 uker fra den datoen du mottok vedtaket. Du finner skjema og informasjon på nav.no/klage. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ingen_tilbakekreving.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ingen_tilbakekreving.txt new file mode 100644 index 000000000..5bd376568 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ingen_tilbakekreving.txt @@ -0,0 +1,28 @@ +I brev 21. mars 2019 fikk du melding om at overgangsstønaden din er endret. Endringen førte til at du har fått utbetalt for mye. Du må ikke betale tilbake det du har fått for mye. +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Vi har fått melding om at Søker Søkersen døde. Overgangsstønaden skulle vært stanset fra og med 1. januar 2019. + +Fordi overgangsstønaden er utbetalt etter denne datoen er det utbetalt 1 000 kroner for mye. +__Hvordan har vi kommet fram til at du ikke må betale tilbake? +Vi har ikke gitt deg den informasjonen du trengte for å forstå at beløpet du fikk utbetalt var feil. Du må derfor ikke betale tilbake. + +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Du har rett til å klage +Du kan klage innen 6 uker fra den datoen du mottok vedtaket. Du finner skjema og informasjon på nav.no/klage. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ingen_tilbakekreving_bruker_d\303\270d.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ingen_tilbakekreving_bruker_d\303\270d.txt" new file mode 100644 index 000000000..9bdbcfec5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ingen_tilbakekreving_bruker_d\303\270d.txt" @@ -0,0 +1,28 @@ +I brev 21. mars 2019 sendte vi melding om at overgangsstønaden er endret. Endringen førte til at det ble utbetalt for mye. Det feilutbetalte beløpet må ikke betales tilbake. +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Vi har fått melding om at Søker Søkersen døde. Overgangsstønaden skulle vært stanset fra og med 1. januar 2019. + +Fordi overgangsstønaden er utbetalt etter denne datoen er det utbetalt 1 000 kroner for mye. +__Hvordan har vi kommet fram til at overgangsstønaden ikke må betales tilbake? +Dere har ikke fått den informasjonen dere trengte for å forstå at beløpet som ble utbetalt var feil. 1 000 kroner kroner skal derfor ikke betales tilbake. + +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Rett til å klage +Dere kan klage innen 6 uker fra den datoen dere mottok vedtaket. Dere finner skjema og informasjon på nav.no/klage. +_Rett til innsyn +Dere kan be om innsyn ved å ta kontakt med oss. +_Har dere spørsmål? +Dere finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan dere chatte eller skrive til oss. + +Hvis dere ikke finner svar på nav.no kan dere ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ingen_tilbakekreving_bruker_d\303\270d_nn.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ingen_tilbakekreving_bruker_d\303\270d_nn.txt" new file mode 100644 index 000000000..8b40290e8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ingen_tilbakekreving_bruker_d\303\270d_nn.txt" @@ -0,0 +1,28 @@ +I brev 21. mars 2019 sende vi melding om at overgangsstønaden er endra. Endringa førte til at det blei utbetalt for mykje. Det feilutbetalte beløpet må ikkje betalast tilbake. +_Perioden frå og med 1. januar 2019 til og med 31. januar 2019 +Vi har fått melding om at Søker Søkersen døydde. Overgangsstønaden skulle vore stansa frå og med 1. januar 2019. + +Fordi overgangsstønaden er utbetalt etter denne datoen er det utbetalt 1 000 kroner for mykje. +__Korleis har vi kome fram til at overgangsstønaden ikkje må betalast tilbake? +De har ikkje fått den informasjonen de trong for å forstå at beløpet som blei utbetalt var feil. 1 000 kroner kroner skal derfor ikkje betalast tilbake. + +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Rett til å klage +De kan klage innan 6 veker frå den datoen de fekk vedtaket. De finn skjema og informasjon på nav.no/klage. +_Rett til innsyn +De kan be om innsyn ved å ta kontakt med oss. +_Har de spørsmål? +De finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan de chatte eller skrive til oss. + +Om de ikkje finn svar på nav.no, kan de ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaka \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ingen_tilbakekreving_med_verge.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ingen_tilbakekreving_med_verge.txt new file mode 100644 index 000000000..1c0476e96 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_ingen_tilbakekreving_med_verge.txt @@ -0,0 +1,31 @@ +I brev 21. mars 2019 fikk du melding om at overgangsstønaden din er endret. Endringen førte til at du har fått utbetalt for mye. Du må ikke betale tilbake det du har fått for mye. +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Vi har fått melding om at Søker Søkersen døde. Overgangsstønaden skulle vært stanset fra og med 1. januar 2019. + +Fordi overgangsstønaden er utbetalt etter denne datoen er det utbetalt 1 000 kroner for mye. +__Hvordan har vi kommet fram til at du ikke må betale tilbake? +Vi har ikke gitt deg den informasjonen du trengte for å forstå at beløpet du fikk utbetalt var feil. Du må derfor ikke betale tilbake. + +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Du har rett til å klage +Du kan klage innen 6 uker fra den datoen du mottok vedtaket. Du finner skjema og informasjon på nav.no/klage. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Brev med likt innhold er sendt til Test + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_revurdering_bruker_d\303\270d.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_revurdering_bruker_d\303\270d.txt" new file mode 100644 index 000000000..62a50f108 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_revurdering_bruker_d\303\270d.txt" @@ -0,0 +1,44 @@ +Vi har vurdert saken om tilbakebetaling på nytt. Derfor gjelder ikke det tidligere vedtaket av 1. januar 2019 om tilbakebetaling av overgangsstønad. + +Vi varslet 4. april 2020 om at det er utbetalt for mye. Utestående beløp før skatt er 11 000 kroner. Dette er hele det feilutbetalte beløpet. Dette beløpet er med renter. + +Det som skal betales tilbake etter at skatten er trukket fra, er 7 011 kroner. + +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Vi har fått melding om at Søker Søkersen døde. Overgangsstønaden skulle vært stanset fra og med 1. januar 2019. + +Fordi overgangsstønaden er utbetalt etter denne datoen er det utbetalt 10 000 kroner for mye. +__Hvordan har vi kommet fram til at overgangsstønaden må betales tilbake? +Det burde vært oppdaget og meldt ifra om at overgangsstønad ble utbetalt etter at Søker Søkersen døde. +__Renter +Etter vår vurdering forsto dere at det dere fikk utbetalt var feil. Derfor må dere betale et rentetillegg på 10 prosent. Det vil si 1 000 kroner før skatt. Dette er i tillegg til det feilutbetalte beløpet. + +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Skatt og skatteoppgjør +Skatten som er trukket fra beløpet som skal betales tilbake, er beregnet etter det som har blitt trukket i skatt i gjennomsnitt per måned. + +NAV gir opplysninger til Skatteetaten om skattebeløpet og om beløpet som skal betales tilbake før skatt er trukket fra. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjør. +_Hvordan betaler du tilbake? +Skatteetaten vil korrigere beløpet dere skal betale tilbake. Dere finner mer informasjon på +skatteetaten.no/betale. +_Rett til å klage +Dere kan klage innen 6 uker fra den datoen dere mottok vedtaket. Dere finner skjema og informasjon på nav.no/klage. + +Beløpet må betales selv om det klages dette vedtaket. Dette følger av forvaltningsloven § 42. Hvis vedtaket blir gjort om slik at hele eller deler av beløpet likevel ikke skal betales tilbake, betaler vi pengene tilbake. +_Rett til innsyn +Dere kan be om innsyn ved å ta kontakt med oss. +_Har dere spørsmål? +Dere finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan dere chatte eller skrive til oss. + +Hvis dere ikke finner svar på nav.no kan dere ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_revurdering_bruker_d\303\270d_annet_fritekst.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_revurdering_bruker_d\303\270d_annet_fritekst.txt" new file mode 100644 index 000000000..c62263e72 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_revurdering_bruker_d\303\270d_annet_fritekst.txt" @@ -0,0 +1,44 @@ +Vi har vurdert saken om tilbakebetaling på nytt. Derfor gjelder ikke det tidligere vedtaket av 1. januar 2019 om tilbakebetaling av overgangsstønad. + +Vi varslet 4. april 2020 om at det er utbetalt for mye. Utestående beløp før skatt er 11 000 kroner. Dette er hele det feilutbetalte beløpet. Dette beløpet er med renter. + +Det som skal betales tilbake etter at skatten er trukket fra, er 7 011 kroner. + +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Død bruker annet fritekst er valgt + +Det er utbetalt 10 000 kroner for mye. +__Hvordan har vi kommet fram til at overgangsstønaden må betales tilbake? +Død bruker annet fritekst er valgt +__Renter +Etter vår vurdering forsto dere at det dere fikk utbetalt var feil. Derfor må dere betale et rentetillegg på 10 prosent. Det vil si 1 000 kroner før skatt. Dette er i tillegg til det feilutbetalte beløpet. + +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Skatt og skatteoppgjør +Skatten som er trukket fra beløpet som skal betales tilbake, er beregnet etter det som har blitt trukket i skatt i gjennomsnitt per måned. + +NAV gir opplysninger til Skatteetaten om skattebeløpet og om beløpet som skal betales tilbake før skatt er trukket fra. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjør. +_Hvordan betaler du tilbake? +Skatteetaten vil korrigere beløpet dere skal betale tilbake. Dere finner mer informasjon på +skatteetaten.no/betale. +_Rett til å klage +Dere kan klage innen 6 uker fra den datoen dere mottok vedtaket. Dere finner skjema og informasjon på nav.no/klage. + +Beløpet må betales selv om det klages dette vedtaket. Dette følger av forvaltningsloven § 42. Hvis vedtaket blir gjort om slik at hele eller deler av beløpet likevel ikke skal betales tilbake, betaler vi pengene tilbake. +_Rett til innsyn +Dere kan be om innsyn ved å ta kontakt med oss. +_Har dere spørsmål? +Dere finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan dere chatte eller skrive til oss. + +Hvis dere ikke finner svar på nav.no kan dere ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_revurdering_bruker_d\303\270d_nynorsk.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_revurdering_bruker_d\303\270d_nynorsk.txt" new file mode 100644 index 000000000..31ccf2604 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_revurdering_bruker_d\303\270d_nynorsk.txt" @@ -0,0 +1,44 @@ +Vi har vurdert saka om tilbakebetaling på nytt. Derfor gjeld ikkje det tidlegare vedtaket av 1. januar 2019 om tilbakebetaling av overgangsstønad. + +Vi varsla 4. april 2020 om at det er utbetalt for mykje. Utestående beløp før skatt er 11 000 kroner. Dette er heile det feilutbetalte beløpet. Dette beløpet er med renter. + +Det som skal betalast tilbake etter skatten er trekt frå er 7 011 kroner. + +_Perioden frå og med 1. januar 2019 til og med 31. januar 2019 +Vi har fått melding om at Søker Søkersen døydde. Overgangsstønaden skulle vore stansa frå og med 1. januar 2019. + +Fordi overgangsstønaden er utbetalt etter denne datoen er det utbetalt 10 000 kroner for mykje. +__Korleis har vi kome fram til at overgangsstønaden må betalast tilbake? +Det burde blitt oppdaga og meldt ifrå om at overgangsstønad blei utbetalt etter at Søker Søkersen døydde. +__Renter +Etter vår vurdering forstod de at det de fekk utbetalt var feil. Derfor må de betale eit rentetillegg på 10 prosent. Det vil seie 1 000 kroner før skatt. Dette er i tillegg til det feilutbetalte beløpet. + +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Skatt og skatteoppgjer +Skatten som er trekt frå beløpet som skal betalast tilbake, er berekna etter det som har blitt trekt i skatt i gjennomsnitt per månad. + +NAV gjev opplysningar til Skatteetaten om skattebeløpet og om beløpet som skal betalast tilbake før skatt er trekt frå. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjer. +_Korleis betalar du tilbake? +Skatteetaten vil korrigere beløpet de skal betale tilbake. De finn meir informasjon på +skatteetaten.no/betale. +_Rett til å klage +De kan klage innan 6 veker frå den datoen de fekk vedtaket. De finn skjema og informasjon på nav.no/klage. + +Beløpet må betalast sjølv om det klagast dette vedtaket. Dette følgjer av forvaltningslova § 42. Om vedtaket blir gjort om slik at heile eller delar av beløpet likevel ikke skal betalast tilbake, betalar vi pengane tilbake. +_Rett til innsyn +De kan be om innsyn ved å ta kontakt med oss. +_Har de spørsmål? +De finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan de chatte eller skrive til oss. + +Om de ikkje finn svar på nav.no, kan de ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaka \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_revurdering_bruker_d\303\270d_nynorsk_annet_fritekst.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_revurdering_bruker_d\303\270d_nynorsk_annet_fritekst.txt" new file mode 100644 index 000000000..777548cf6 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_revurdering_bruker_d\303\270d_nynorsk_annet_fritekst.txt" @@ -0,0 +1,44 @@ +Vi har vurdert saka om tilbakebetaling på nytt. Derfor gjeld ikkje det tidlegare vedtaket av 1. januar 2019 om tilbakebetaling av overgangsstønad. + +Vi varsla 4. april 2020 om at det er utbetalt for mykje. Utestående beløp før skatt er 11 000 kroner. Dette er heile det feilutbetalte beløpet. Dette beløpet er med renter. + +Det som skal betalast tilbake etter skatten er trekt frå er 7 011 kroner. + +_Perioden frå og med 1. januar 2019 til og med 31. januar 2019 +Død bruker annet fritekst er valgt + +Det er utbetalt 10 000 kroner for mykje. +__Korleis har vi kome fram til at overgangsstønaden må betalast tilbake? +Død bruker annet fritekst er valgt +__Renter +Etter vår vurdering forstod de at det de fekk utbetalt var feil. Derfor må de betale eit rentetillegg på 10 prosent. Det vil seie 1 000 kroner før skatt. Dette er i tillegg til det feilutbetalte beløpet. + +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Skatt og skatteoppgjer +Skatten som er trekt frå beløpet som skal betalast tilbake, er berekna etter det som har blitt trekt i skatt i gjennomsnitt per månad. + +NAV gjev opplysningar til Skatteetaten om skattebeløpet og om beløpet som skal betalast tilbake før skatt er trekt frå. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjer. +_Korleis betalar du tilbake? +Skatteetaten vil korrigere beløpet de skal betale tilbake. De finn meir informasjon på +skatteetaten.no/betale. +_Rett til å klage +De kan klage innan 6 veker frå den datoen de fekk vedtaket. De finn skjema og informasjon på nav.no/klage. + +Beløpet må betalast sjølv om det klagast dette vedtaket. Dette følgjer av forvaltningslova § 42. Om vedtaket blir gjort om slik at heile eller delar av beløpet likevel ikke skal betalast tilbake, betalar vi pengane tilbake. +_Rett til innsyn +De kan be om innsyn ved å ta kontakt med oss. +_Har de spørsmål? +De finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan de chatte eller skrive til oss. + +Om de ikkje finn svar på nav.no, kan de ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaka \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_tilbakekreving_bruker_d\303\270d.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_tilbakekreving_bruker_d\303\270d.txt" new file mode 100644 index 000000000..9a33c753b --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_tilbakekreving_bruker_d\303\270d.txt" @@ -0,0 +1,43 @@ +Vi varslet 3. januar 2019 om at det er utbetalt for mye. Utestående beløp før skatt er 11 000 kroner. Dette er hele det feilutbetalte beløpet. Dette beløpet er med renter. + +Det som skal betales tilbake etter at skatten er trukket fra, er 7 011 kroner. + +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Vi har fått melding om at Søker Søkersen døde. Overgangsstønaden skulle vært stanset fra og med 1. januar 2019. + +Fordi overgangsstønaden er utbetalt etter denne datoen er det utbetalt 1 000 kroner for mye. +__Hvordan har vi kommet fram til at overgangsstønaden må betales tilbake? +Det burde vært oppdaget og meldt ifra om at overgangsstønad ble utbetalt etter at Søker Søkersen døde. + +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Skatt og skatteoppgjør +Skatten som er trukket fra beløpet som skal betales tilbake, er beregnet etter det som har blitt trukket i skatt i gjennomsnitt per måned. + +NAV gir opplysninger til Skatteetaten om skattebeløpet og om beløpet som skal betales tilbake før skatt er trukket fra. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjør. +_Hvordan betaler du tilbake? +Dere vil få faktura fra Skatteetaten på det beløpet som skal betales tilbake. + +På fakturaen vil det stå informasjon om nøyaktig beløp, kontonummer og forfallsdato. Dere trenger ikke å gjøre noe før du får fakturaen. + +Dere finner mer informasjon på skatteetaten.no/betale. +_Rett til å klage +Dere kan klage innen 6 uker fra den datoen dere mottok vedtaket. Dere finner skjema og informasjon på nav.no/klage. + +Beløpet må betales selv om det klages dette vedtaket. Dette følger av forvaltningsloven § 42. Hvis vedtaket blir gjort om slik at hele eller deler av beløpet likevel ikke skal betales tilbake, betaler vi pengene tilbake. +_Rett til innsyn +Dere kan be om innsyn ved å ta kontakt med oss. +_Har dere spørsmål? +Dere finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan dere chatte eller skrive til oss. + +Hvis dere ikke finner svar på nav.no kan dere ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_tilbakekreving_bruker_d\303\270d_nn.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_tilbakekreving_bruker_d\303\270d_nn.txt" new file mode 100644 index 000000000..583574a97 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/OS_tilbakekreving_bruker_d\303\270d_nn.txt" @@ -0,0 +1,43 @@ +Vi varsla 3. januar 2019 om at det er utbetalt for mykje. Utestående beløp før skatt er 11 000 kroner. Dette er heile det feilutbetalte beløpet. Dette beløpet er med renter. + +Det som skal betalast tilbake etter skatten er trekt frå er 7 011 kroner. + +_Perioden frå og med 1. januar 2019 til og med 31. januar 2019 +Vi har fått melding om at Søker Søkersen døydde. Overgangsstønaden skulle vore stansa frå og med 1. januar 2019. + +Fordi overgangsstønaden er utbetalt etter denne datoen er det utbetalt 1 000 kroner for mykje. +__Korleis har vi kome fram til at overgangsstønaden må betalast tilbake? +Det burde blitt oppdaga og meldt ifrå om at overgangsstønad blei utbetalt etter at Søker Søkersen døydde. + +Vedtaket er gjort etter Folketrygdloven § 22-15. +_Skatt og skatteoppgjer +Skatten som er trekt frå beløpet som skal betalast tilbake, er berekna etter det som har blitt trekt i skatt i gjennomsnitt per månad. + +NAV gjev opplysningar til Skatteetaten om skattebeløpet og om beløpet som skal betalast tilbake før skatt er trekt frå. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjer. +_Korleis betalar du tilbake? +De vil få faktura frå Skatteetaten på det beløpet som skal betalast tilbake. + +På fakturaen vil det stå informasjon om nøyaktig beløp, kontonummer og forfallsdato. De treng ikkje å gjere noko før du får fakturaen. + +De finn meir informasjon på skatteetaten.no/betale. +_Rett til å klage +De kan klage innan 6 veker frå den datoen de fekk vedtaket. De finn skjema og informasjon på nav.no/klage. + +Beløpet må betalast sjølv om det klagast dette vedtaket. Dette følgjer av forvaltningslova § 42. Om vedtaket blir gjort om slik at heile eller delar av beløpet likevel ikke skal betalast tilbake, betalar vi pengane tilbake. +_Rett til innsyn +De kan be om innsyn ved å ta kontakt med oss. +_Har de spørsmål? +De finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan de chatte eller skrive til oss. + +Om de ikkje finn svar på nav.no, kan de ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaka \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/barnetilsyn/BT_bel\303\270p_over_6G_helt_brev_en_periode.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/barnetilsyn/BT_bel\303\270p_over_6G_helt_brev_en_periode.txt" new file mode 100644 index 000000000..caef3add9 --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/barnetilsyn/BT_bel\303\270p_over_6G_helt_brev_en_periode.txt" @@ -0,0 +1,41 @@ +Du fikk varsel fra oss 21. juni 2022 om at du har fått utbetalt for mye. Du må betale tilbake 11 000 kroner, som er hele det feilutbetalte beløpet. Dette beløpet er med renter. + +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Du har fått stønad til barnetilsyn. Fordi inntekten din er over 608 106 kroner, har du ikke lenger rett til stønaden. Derfor har du fått 30 001 kroner for mye utbetalt i denne perioden. +__Hvordan har vi kommet fram til at du må betale tilbake? +Du har fått vite om du har rett til overgangsstønad og hvor mye du har rett til. Etter vår vurdering burde du forstått at du ikke ga oss alle opplysningene vi trengte tidsnok for å sikre at du fikk riktig utbetaling. Derfor kan vi kreve pengene tilbake. +__Er det særlige grunner til å redusere beløpet? +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake fordi det har gått lang tid siden feilutbetalingen skjedde og beløpet er lavt. + +Du må betale 20 002 kroner. + +Vedtaket er gjort etter Folketrygdloven. +_Skatt og skatteoppgjør +NAV gir opplysninger til Skatteetaten. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjør. +_Hvordan betaler du tilbake? +Du vil få faktura fra Skatteetaten på det beløpet du skal betale tilbake. + +På fakturaen vil det stå informasjon om nøyaktig beløp, kontonummer og forfallsdato. Du trenger ikke å gjøre noe før du får fakturaen. + +Du finner mer informasjon på skatteetaten.no/betale. +_Du har rett til å klage +Du kan klage innen 6 uker fra den datoen du mottok vedtaket. Du finner skjema og informasjon på nav.no/klage. + +Du må som hovedregel begynne å betale tilbake beløpet når du får fakturaen, selv om du klager på dette vedtaket. Dette følger av forvaltningsloven § 42. Hvis du får vedtak om at du ikke trengte å betale tilbake hele eller deler av beløpet du skyldte, betaler vi pengene tilbake til deg. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/barnetilsyn/BT_bel\303\270p_over_6G_helt_brev_flere_perioder_flere_bel\303\270p.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/barnetilsyn/BT_bel\303\270p_over_6G_helt_brev_flere_perioder_flere_bel\303\270p.txt" new file mode 100644 index 000000000..32bb365df --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/barnetilsyn/BT_bel\303\270p_over_6G_helt_brev_flere_perioder_flere_bel\303\270p.txt" @@ -0,0 +1,53 @@ +Du fikk varsel fra oss 21. juni 2022 om at du har fått utbetalt for mye. Du må betale tilbake 11 000 kroner, som er hele det feilutbetalte beløpet. Dette beløpet er med renter. + +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Du har fått stønad til barnetilsyn. Fordi inntekten din er over seks ganger grunnbeløpet, har du ikke lenger rett til stønaden. Derfor har du fått 30 001 kroner for mye utbetalt i denne perioden. + +Seks ganger grunnbeløpet er 599 148 kroner for perioden 1. januar 2020 til 30. april 2020 og 608 106 kroner for perioden 1. mai 2020 til 30. april 2021. +__Hvordan har vi kommet fram til at du må betale tilbake? +Du har fått vite om du har rett til overgangsstønad og hvor mye du har rett til. Etter vår vurdering burde du forstått at du ikke ga oss alle opplysningene vi trengte tidsnok for å sikre at du fikk riktig utbetaling. Derfor kan vi kreve pengene tilbake. +__Er det særlige grunner til å redusere beløpet? +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake fordi det har gått lang tid siden feilutbetalingen skjedde og beløpet er lavt. + +Du må betale 20 002 kroner. +_Perioden fra og med 1. januar 2019 til og med 31. januar 2019 +Du har fått stønad til barnetilsyn. Fordi inntekten din er over seks ganger grunnbeløpet, har du ikke lenger rett til stønaden. Derfor har du fått 30 001 kroner for mye utbetalt i denne perioden. + +Seks ganger grunnbeløpet er 599 148 kroner for perioden 1. januar 2020 til 30. april 2020, 608 106 kroner for perioden 1. mai 2020 til 30. april 2021 og 638 394 kroner for perioden 1. mai 2021 til 31. mai 2021. +__Hvordan har vi kommet fram til at du må betale tilbake? +Du har fått vite om du har rett til overgangsstønad og hvor mye du har rett til. Etter vår vurdering burde du forstått at du ikke ga oss alle opplysningene vi trengte tidsnok for å sikre at du fikk riktig utbetaling. Derfor kan vi kreve pengene tilbake. +__Er det særlige grunner til å redusere beløpet? +Vi har lagt vekt på at du ikke har gitt oss alle nødvendige opplysninger tidsnok til at vi kunne unngå feilutbetalingen. Vi har likevel redusert beløpet du må betale tilbake fordi det har gått lang tid siden feilutbetalingen skjedde og beløpet er lavt. + +Du må betale 20 002 kroner. +_Lovhjemmelen vi har brukt +Vedtaket er gjort etter Folketrygdloven. +_Skatt og skatteoppgjør +NAV gir opplysninger til Skatteetaten. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjør. +_Hvordan betaler du tilbake? +Du vil få faktura fra Skatteetaten på det beløpet du skal betale tilbake. + +På fakturaen vil det stå informasjon om nøyaktig beløp, kontonummer og forfallsdato. Du trenger ikke å gjøre noe før du får fakturaen. + +Du finner mer informasjon på skatteetaten.no/betale. +_Du har rett til å klage +Du kan klage innen 6 uker fra den datoen du mottok vedtaket. Du finner skjema og informasjon på nav.no/klage. + +Du må som hovedregel begynne å betale tilbake beløpet når du får fakturaen, selv om du klager på dette vedtaket. Dette følger av forvaltningsloven § 42. Hvis du får vedtak om at du ikke trengte å betale tilbake hele eller deler av beløpet du skyldte, betaler vi pengene tilbake til deg. +_Du har rett til innsyn +På nav.no/dittnav kan du se dokumentene i saken din. +_Har du spørsmål? +Du finner mer informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Hvis du ikke finner svar på nav.no kan du ringe oss på telefon 55 55 33 33, hverdager 09.00-15.00. + + +Med vennlig hilsen +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaken \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/barnetilsyn/BT_bel\303\270p_over_6G_helt_brev_flere_perioder_flere_bel\303\270p_nn.txt" "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/barnetilsyn/BT_bel\303\270p_over_6G_helt_brev_flere_perioder_flere_bel\303\270p_nn.txt" new file mode 100644 index 000000000..58ec7d09d --- /dev/null +++ "b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/barnetilsyn/BT_bel\303\270p_over_6G_helt_brev_flere_perioder_flere_bel\303\270p_nn.txt" @@ -0,0 +1,51 @@ +Du fekk varsel fra oss 21. juni 2022 om at du har fått utbetalt for mykje. Du må betale tilbake 11 000 kroner, som er heile det feilutbetalte beløpet. Dette beløpet er med renter. + +_Perioden frå og med 1. januar 2019 til og med 31. januar 2019 +Du har fått stønad til barnetilsyn. Fordi inntekta di er over 608 106 kroner, har du ikkje lengre rett til stønaden. Derfor har du fått 30 001 kroner for mykje utbetalt i denne perioden. +__Korleis har vi kome fram til at du må betale tilbake? +Du har fått vite om du har rett til overgangsstønad og kor mykje du har rett til. Vi vurderer det slik at du burde ha forstått at du ikkje gav oss alle opplysningane vi trong, tidsnok for å sikre at du fekk riktig utbetaling. Derfor kan vi krevje pengane tilbake. +__Er det særlege grunnar til å redusere beløpet? +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det har gått lang tid sidan feilutbetalinga skjedde, og beløpet er lågt. + +Du må betale 20 002 kroner. +_Perioden frå og med 1. januar 2019 til og med 31. januar 2019 +Du har fått stønad til barnetilsyn. Fordi inntekta di er over seks gonger grunnbeløpet, har du ikkje lengre rett til stønaden. Derfor har du fått 30 001 kroner for mykje utbetalt i denne perioden. + +Seks gonger grunnbeløpet er 599 148 kroner for perioden 1. januar 2020 til 30. april 2020, 608 106 kroner for perioden 1. mai 2020 til 30. april 2021 og 638 394 kroner for perioden 1. mai 2021 til 31. mai 2021. +__Korleis har vi kome fram til at du må betale tilbake? +Du har fått vite om du har rett til overgangsstønad og kor mykje du har rett til. Vi vurderer det slik at du burde ha forstått at du ikkje gav oss alle opplysningane vi trong, tidsnok for å sikre at du fekk riktig utbetaling. Derfor kan vi krevje pengane tilbake. +__Er det særlege grunnar til å redusere beløpet? +Vi har lagt vekt på at du ikkje har gitt oss alle nødvendige opplysningar tidsnok til at vi kunne unngå feilutbetalinga. Vi har likevel redusert beløpet du må betale tilbake, fordi det har gått lang tid sidan feilutbetalinga skjedde, og beløpet er lågt. + +Du må betale 20 002 kroner. +_Lovheimelen vi har brukt +Vedtaket er gjort etter Folketrygdloven. +_Skatt og skatteoppgjer +NAV gjev opplysningar til Skatteetaten. Skatteetaten vil vurdere om det er grunnlag for å endre skatteoppgjer. +_Korleis betalar du tilbake? +Du vil få faktura frå Skatteetaten på det beløpet du skal betale tilbake. + +På fakturaen vil det stå informasjon om nøyaktig beløp, kontonummer og forfallsdato. Du treng ikkje å gjere noko før du får fakturaen. + +Du finn meir informasjon på skatteetaten.no/betale. +_Du har rett til å klage +Du kan klage innan 6 veker frå den datoen du fekk vedtaket. Du finn skjema og informasjon på nav.no/klage. + +Du må som hovudregel begynne å betale tilbake beløpet når du får fakturaen, sjølv om du klagar på dette vedtaket. Dette følgjer av forvaltningslova § 42. Vi vil betale tilbake pengane du har betalt inn, om du får vedtak om at du ikkje trong å betale tilbake heile eller delar av beløpet du skulda. +_Du har rett til innsyn +På nav.no/dittnav kan du sjå dokumenta i saka di. +_Har du spørsmål? +Du finn meir informasjon på nav.no/alene-med-barn. + +På nav.no/kontakt kan du chatte eller skrive til oss. + +Om du ikkje finn svar på nav.no, kan du ringe oss på telefon 55 55 33 33, kvardagar 09.00-15.00. + + +Med venleg helsing +NAV Arbeid og ytelser + +{venstrejustert}Ansvarlig Saksbehandler{høyrejustert}Ansvarlig Beslutter + + +Vedlegg: Resultatet av tilbakebetalingssaka \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/vedlegg/vedlegg_med_og_uten_renter.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/vedlegg/vedlegg_med_og_uten_renter.txt new file mode 100644 index 000000000..594cc0f9c --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/vedlegg/vedlegg_med_og_uten_renter.txt @@ -0,0 +1,47 @@ +

    Oversikt over resultatet av tilbakebetalingssaken

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PeriodeFeilutbetalt beløpTilbakebetalingRenterBeløp før skattBeløp du skal betale tilbake etter skatt er trukket fra
    01.01.2019 – 31.01.201930 001 kronerDeler av beløpet20 002 kroner16 015 kroner
    01.02.2019 – 28.02.20193 000 kronerIngen tilbakebetaling0 kroner0 kroner
    01.03.2019 – 31.03.20193 000 kronerHele beløpet300 kroner3 300 kroner2 222 kroner
    Sum23 302 kroner18 537 kroner
    \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/vedlegg/vedlegg_uten_renter.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/vedlegg/vedlegg_uten_renter.txt new file mode 100644 index 000000000..d28dea1b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/vedlegg/vedlegg_uten_renter.txt @@ -0,0 +1,28 @@ +

    Oversikt over resultatet av tilbakebetalingssaken

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PeriodeFeilutbetalt beløpTilbakebetalingBeløp før skattBeløp du skal betale tilbake etter skatt er trukket fra
    01.01.2019 – 31.01.201930 001 kronerDeler av beløpet20 002 kroner16 015 kroner
    Sum20 002 kroner16 015 kroner
    \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/vedlegg/vedlegg_uten_renter_nn.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/vedlegg/vedlegg_uten_renter_nn.txt new file mode 100644 index 000000000..186744215 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/vedlegg/vedlegg_uten_renter_nn.txt @@ -0,0 +1,28 @@ +

    Oversikt over resultatet av tilbakebetalingssaka

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PeriodeFeilutbetalt beløpTilbakebetalingBeløp før skattBeløp du skal betale tilbake etter at skatt er trekt frå
    01.01.2019 – 31.01.201930 001 kronerDelar av beløpet20 002 kroner16 015 kroner
    Sum20 002 kroner16 015 kroner
    \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/vedlegg/vedlegg_uten_skatt.txt b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/vedlegg/vedlegg_uten_skatt.txt new file mode 100644 index 000000000..8829fd529 --- /dev/null +++ b/jdk_17_maven/cs/rest/familie-tilbake/src/test/resources/vedtaksbrev/vedlegg/vedlegg_uten_skatt.txt @@ -0,0 +1,25 @@ +

    Oversikt over resultatet av tilbakebetalingssaken

    + + + + + + + + + + + + + + + + + + + + + + + +
    PeriodeFeilutbetalt beløpTilbakebetalingBeløp du skal betale tilbake
    01.01.2019 – 31.01.201930 001 kronerHele beløpet30 001 kroner
    Sum30 001 kroner
    \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/pom.xml b/jdk_17_maven/cs/rest/pom.xml new file mode 100644 index 000000000..839b137b7 --- /dev/null +++ b/jdk_17_maven/cs/rest/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + + org.evomaster + evomaster-benchmark-jdk17-cs + 3.0.1-SNAPSHOT + + + evomaster-benchmark-jdk17-cs-rest + pom + + + + familie-ba-sak + + + + + + + + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/signal-server/LICENSE b/jdk_17_maven/cs/rest/signal-server/LICENSE new file mode 100644 index 000000000..33fd343a7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keysManager, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/jdk_17_maven/cs/rest/signal-server/README.md b/jdk_17_maven/cs/rest/signal-server/README.md new file mode 100644 index 000000000..14b67d673 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/README.md @@ -0,0 +1,26 @@ +Signal-Server +================= + +Documentation +------------- + +Looking for protocol documentation? Check out the website! + +https://signal.org/docs/ + +Cryptography Notice +------------ + +This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software. +BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. +See for more information. + +The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms. +The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code. + +License +--------------------- + +Copyright 2013-2023 Signal Messenger, LLC + +Licensed under the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html diff --git a/jdk_17_maven/cs/rest/signal-server/api-doc/pom.xml b/jdk_17_maven/cs/rest/signal-server/api-doc/pom.xml new file mode 100644 index 000000000..d1a52d712 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/api-doc/pom.xml @@ -0,0 +1,54 @@ + + + + TextSecureServer + org.whispersystems.textsecure + + 10.3.0 + + 4.0.0 + api-doc + + + + org.whispersystems.textsecure + service + ${project.version} + + + + + + + io.swagger.core.v3 + swagger-maven-plugin + 2.2.8 + + signal-server-openapi + ${project.build.directory}/openapi + YAML + ${project.basedir}/src/main/resources/openapi/openapi-configuration.yaml + + + + + compile + + resolve + + + + + + com.google.cloud.tools + jib-maven-plugin + + + true + + + + + diff --git a/jdk_17_maven/cs/rest/signal-server/api-doc/src/main/java/org/signal/openapi/OpenApiExtension.java b/jdk_17_maven/cs/rest/signal-server/api-doc/src/main/java/org/signal/openapi/OpenApiExtension.java new file mode 100644 index 000000000..136cbcf62 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/api-doc/src/main/java/org/signal/openapi/OpenApiExtension.java @@ -0,0 +1,110 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.openapi; + +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.SimpleType; +import io.dropwizard.auth.Auth; +import io.swagger.v3.jaxrs2.ResolvedParameter; +import io.swagger.v3.jaxrs2.ext.AbstractOpenAPIExtension; +import io.swagger.v3.jaxrs2.ext.OpenAPIExtension; +import io.swagger.v3.oas.models.Components; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Set; +import javax.ws.rs.Consumes; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; + +/** + * One of the extension mechanisms of Swagger Core library (OpenAPI processor) is via custom implementations + * of the {@link AbstractOpenAPIExtension} class. + *

    + * The purpose of this extension is to customize certain aspects of the OpenAPI model generation on a lower level. + * This extension works in coordination with {@link OpenApiReader} that has access to the model on a higher level. + *

    + * The extension is enabled by being listed in {@code META-INF/services/io.swagger.v3.jaxrs2.ext.OpenAPIExtension} file. + * @see ServiceLoader + * @see OpenApiReader + * @see Swagger 2.X Extensions + */ +public class OpenApiExtension extends AbstractOpenAPIExtension { + + public static final ResolvedParameter AUTHENTICATED_ACCOUNT = new ResolvedParameter(); + + public static final ResolvedParameter OPTIONAL_AUTHENTICATED_ACCOUNT = new ResolvedParameter(); + + public static final ResolvedParameter DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT = new ResolvedParameter(); + + public static final ResolvedParameter OPTIONAL_DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT = new ResolvedParameter(); + + /** + * When parsing endpoint methods, Swagger will treat the first parameter not annotated as header/path/query param + * as a request body (and will ignore other not annotated parameters). In our case, this behavior conflicts with + * the {@code @Auth}-annotated parameters. Here we're checking if parameters are known to be anything other than + * a body and return an appropriate {@link ResolvedParameter} representation. + */ + @Override + public ResolvedParameter extractParameters( + final List annotations, + final Type type, + final Set typesToSkip, + final Components components, + final Consumes classConsumes, + final Consumes methodConsumes, + final boolean includeRequestBody, + final JsonView jsonViewAnnotation, + final Iterator chain) { + + if (annotations.stream().anyMatch(a -> a.annotationType().equals(Auth.class))) { + // this is the case of authenticated endpoint, + if (type instanceof SimpleType simpleType + && simpleType.getRawClass().equals(AuthenticatedAccount.class)) { + return AUTHENTICATED_ACCOUNT; + } + if (type instanceof SimpleType simpleType + && simpleType.getRawClass().equals(DisabledPermittedAuthenticatedAccount.class)) { + return DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT; + } + if (type instanceof SimpleType simpleType + && isOptionalOfType(simpleType, AuthenticatedAccount.class)) { + return OPTIONAL_AUTHENTICATED_ACCOUNT; + } + if (type instanceof SimpleType simpleType + && isOptionalOfType(simpleType, DisabledPermittedAuthenticatedAccount.class)) { + return OPTIONAL_DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT; + } + } + + return super.extractParameters( + annotations, + type, + typesToSkip, + components, + classConsumes, + methodConsumes, + includeRequestBody, + jsonViewAnnotation, + chain); + } + + private static boolean isOptionalOfType(final SimpleType simpleType, final Class expectedType) { + if (!simpleType.getRawClass().equals(Optional.class)) { + return false; + } + final List typeParameters = simpleType.getBindings().getTypeParameters(); + if (typeParameters.isEmpty()) { + return false; + } + return typeParameters.get(0) instanceof SimpleType optionalParameterType + && optionalParameterType.getRawClass().equals(expectedType); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/api-doc/src/main/java/org/signal/openapi/OpenApiReader.java b/jdk_17_maven/cs/rest/signal-server/api-doc/src/main/java/org/signal/openapi/OpenApiReader.java new file mode 100644 index 000000000..61585efe1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/api-doc/src/main/java/org/signal/openapi/OpenApiReader.java @@ -0,0 +1,73 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.openapi; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static org.signal.openapi.OpenApiExtension.AUTHENTICATED_ACCOUNT; +import static org.signal.openapi.OpenApiExtension.DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT; +import static org.signal.openapi.OpenApiExtension.OPTIONAL_AUTHENTICATED_ACCOUNT; +import static org.signal.openapi.OpenApiExtension.OPTIONAL_DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT; + +import com.fasterxml.jackson.annotation.JsonView; +import com.google.common.collect.ImmutableList; +import io.swagger.v3.jaxrs2.Reader; +import io.swagger.v3.jaxrs2.ResolvedParameter; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; +import javax.ws.rs.Consumes; + +/** + * One of the extension mechanisms of Swagger Core library (OpenAPI processor) is via custom implementations + * of the {@link Reader} class. + *

    + * The purpose of this extension is to customize certain aspects of the OpenAPI model generation on a higher level. + * This extension works in coordination with {@link OpenApiExtension} that has access to the model on a lower level. + *

    + * The extension is enabled by being listed in {@code resources/openapi/openapi-configuration.yaml} file. + * @see OpenApiExtension + * @see Swagger 2.X Extensions + */ +public class OpenApiReader extends Reader { + + private static final String AUTHENTICATED_ACCOUNT_AUTH_SCHEMA = "authenticatedAccount"; + + + /** + * Overriding this method allows converting a resolved parameter into other operation entities, + * in this case, into security requirements. + */ + @Override + protected ResolvedParameter getParameters( + final Type type, + final List annotations, + final Operation operation, + final Consumes classConsumes, + final Consumes methodConsumes, + final JsonView jsonViewAnnotation) { + final ResolvedParameter resolved = super.getParameters( + type, annotations, operation, classConsumes, methodConsumes, jsonViewAnnotation); + + if (resolved == AUTHENTICATED_ACCOUNT || resolved == DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT) { + operation.setSecurity(ImmutableList.builder() + .addAll(firstNonNull(operation.getSecurity(), Collections.emptyList())) + .add(new SecurityRequirement().addList(AUTHENTICATED_ACCOUNT_AUTH_SCHEMA)) + .build()); + } + if (resolved == OPTIONAL_AUTHENTICATED_ACCOUNT || resolved == OPTIONAL_DISABLED_PERMITTED_AUTHENTICATED_ACCOUNT) { + operation.setSecurity(ImmutableList.builder() + .addAll(firstNonNull(operation.getSecurity(), Collections.emptyList())) + .add(new SecurityRequirement().addList(AUTHENTICATED_ACCOUNT_AUTH_SCHEMA)) + .add(new SecurityRequirement()) + .build()); + } + + return resolved; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/api-doc/src/main/resources/META-INF/services/io.swagger.v3.jaxrs2.ext.OpenAPIExtension b/jdk_17_maven/cs/rest/signal-server/api-doc/src/main/resources/META-INF/services/io.swagger.v3.jaxrs2.ext.OpenAPIExtension new file mode 100644 index 000000000..d720ba4fb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/api-doc/src/main/resources/META-INF/services/io.swagger.v3.jaxrs2.ext.OpenAPIExtension @@ -0,0 +1 @@ +org.signal.openapi.OpenApiExtension diff --git a/jdk_17_maven/cs/rest/signal-server/api-doc/src/main/resources/openapi/openapi-configuration.yaml b/jdk_17_maven/cs/rest/signal-server/api-doc/src/main/resources/openapi/openapi-configuration.yaml new file mode 100644 index 000000000..ff49154c6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/api-doc/src/main/resources/openapi/openapi-configuration.yaml @@ -0,0 +1,25 @@ +resourcePackages: + - org.whispersystems.textsecuregcm +prettyPrint: true +cacheTTL: 0 +readerClass: org.signal.openapi.OpenApiReader +openAPI: + info: + title: Signal Server API + license: + name: AGPL-3.0-only + url: https://www.gnu.org/licenses/agpl-3.0.txt + servers: + - url: https://chat.signal.org + description: Production service + - url: https://chat.staging.signal.org + description: Staging service + components: + securitySchemes: + authenticatedAccount: + type: http + scheme: basic + description: | + Account authentication is based on Basic authentication schema, + where `username` has a format of `[.]`. If `device_id` is not specified, + user's `main` device is assumed. diff --git a/jdk_17_maven/cs/rest/signal-server/event-logger/pom.xml b/jdk_17_maven/cs/rest/signal-server/event-logger/pom.xml new file mode 100644 index 000000000..69793486d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/event-logger/pom.xml @@ -0,0 +1,95 @@ + + + + + + TextSecureServer + org.whispersystems.textsecure + + 10.3.0 + + + 4.0.0 + event-logger + + + + com.google.cloud + google-cloud-logging + + + org.jetbrains.kotlin + kotlin-stdlib + + + org.jetbrains + + annotations + + + + + org.jetbrains.kotlinx + kotlinx-serialization-json + ${kotlinx-serialization.version} + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + + compile + + compile + + + + + test-compile + + test-compile + + + + + + kotlinx-serialization + + + + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin.version} + + + + + com.google.cloud.tools + jib-maven-plugin + + + true + + + + + + diff --git a/jdk_17_maven/cs/rest/signal-server/event-logger/src/main/kotlin/events.kt b/jdk_17_maven/cs/rest/signal-server/event-logger/src/main/kotlin/events.kt new file mode 100644 index 000000000..b1bbbc5a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/event-logger/src/main/kotlin/events.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.event + +import java.util.Collections +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass + +val module = SerializersModule { + polymorphic(Event::class) { + subclass(RemoteConfigSetEvent::class) + subclass(RemoteConfigDeleteEvent::class) + } +} +val jsonFormat = Json { serializersModule = module } + +sealed interface Event + +@Serializable +data class RemoteConfigSetEvent( + val identity: String, + val name: String, + val percentage: Int, + val defaultValue: String? = null, + val value: String? = null, + val hashKey: String? = null, + val uuids: Collection = Collections.emptyList(), +) : Event + +@Serializable +data class RemoteConfigDeleteEvent( + val identity: String, + val name: String, +) : Event diff --git a/jdk_17_maven/cs/rest/signal-server/event-logger/src/main/kotlin/loggers.kt b/jdk_17_maven/cs/rest/signal-server/event-logger/src/main/kotlin/loggers.kt new file mode 100644 index 000000000..4e87f7ea8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/event-logger/src/main/kotlin/loggers.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.event + +import com.google.cloud.logging.LogEntry +import com.google.cloud.logging.Logging +import com.google.cloud.logging.MonitoredResourceUtil +import com.google.cloud.logging.Payload.JsonPayload +import com.google.cloud.logging.Severity +import com.google.protobuf.Struct +import com.google.protobuf.util.JsonFormat +import kotlinx.serialization.encodeToString + +interface AdminEventLogger { + fun logEvent(event: Event, labels: Map?) + fun logEvent(event: Event) = logEvent(event, null) +} + +class NoOpAdminEventLogger : AdminEventLogger { + override fun logEvent(event: Event, labels: Map?) {} +} + +class GoogleCloudAdminEventLogger(private val logging: Logging, private val projectId: String, private val logName: String) : AdminEventLogger { + override fun logEvent(event: Event, labels: Map?) { + val structBuilder = Struct.newBuilder() + JsonFormat.parser().merge(jsonFormat.encodeToString(event), structBuilder) + val struct = structBuilder.build() + + val logEntryBuilder = LogEntry.newBuilder(JsonPayload.of(struct)) + .setLogName(logName) + .setSeverity(Severity.NOTICE) + .setResource(MonitoredResourceUtil.getResource(projectId, "project")); + if (labels != null) { + logEntryBuilder.setLabels(labels); + } + logging.write(listOf(logEntryBuilder.build())) + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/event-logger/src/test/kotlin/org/signal/event/GoogleCloudAdminEventLoggerTest.kt b/jdk_17_maven/cs/rest/signal-server/event-logger/src/test/kotlin/org/signal/event/GoogleCloudAdminEventLoggerTest.kt new file mode 100644 index 000000000..0ac27778f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/event-logger/src/test/kotlin/org/signal/event/GoogleCloudAdminEventLoggerTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.event + +import com.google.cloud.logging.Logging +import com.google.cloud.logging.LoggingOptions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.mockito.Mockito.mock + +class GoogleCloudAdminEventLoggerTest { + + @Test + fun logEvent() { + val logging = mock(Logging::class.java) + val logger = GoogleCloudAdminEventLogger(logging, "my-project", "test") + + val event = RemoteConfigDeleteEvent("token", "test") + logger.logEvent(event) + } + + @Test + fun testGetService() { + assertDoesNotThrow { + // This is a canary for version conflicts between the cloud logging library and protobuf-java + LoggingOptions.newBuilder() + .setProjectId("test") + .build() + .getService() + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/integration-tests/.gitignore b/jdk_17_maven/cs/rest/signal-server/integration-tests/.gitignore new file mode 100644 index 000000000..9637f0290 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/integration-tests/.gitignore @@ -0,0 +1,2 @@ +.libs +src/main/resources/config.yml diff --git a/jdk_17_maven/cs/rest/signal-server/integration-tests/pom.xml b/jdk_17_maven/cs/rest/signal-server/integration-tests/pom.xml new file mode 100644 index 000000000..612fb0553 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/integration-tests/pom.xml @@ -0,0 +1,63 @@ + + + + TextSecureServer + org.whispersystems.textsecure + + 10.3.0 + + 4.0.0 + integration-tests + + + + org.whispersystems.textsecure + service + ${project.version} + + + + software.amazon.awssdk + dynamodb + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + ** + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.0.0 + + + ${project.basedir}/.libs/software.amazon.awssdk-sso.jar + + + **/*.java + + + + + com.google.cloud.tools + jib-maven-plugin + + + true + + + + + diff --git a/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/Codecs.java b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/Codecs.java new file mode 100644 index 000000000..db985945c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/Codecs.java @@ -0,0 +1,102 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.integration; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.util.Base64; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECPublicKey; + +public final class Codecs { + + private Codecs() { + // utility class + } + + @FunctionalInterface + public interface CheckedFunction { + R apply(T t) throws Exception; + } + + public static class Base64BasedSerializer extends JsonSerializer { + + private final CheckedFunction mapper; + + public Base64BasedSerializer(final CheckedFunction mapper) { + this.mapper = mapper; + } + + @Override + public void serialize(final T value, final JsonGenerator gen, final SerializerProvider serializers) throws IOException { + try { + gen.writeString(Base64.getEncoder().withoutPadding().encodeToString(mapper.apply(value))); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public static class Base64BasedDeserializer extends JsonDeserializer { + + private final CheckedFunction mapper; + + public Base64BasedDeserializer(final CheckedFunction mapper) { + this.mapper = mapper; + } + + @Override + public T deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException { + try { + return mapper.apply(Base64.getDecoder().decode(p.getValueAsString())); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public static class ByteArraySerializer extends Base64BasedSerializer { + public ByteArraySerializer() { + super(bytes -> bytes); + } + } + + public static class ByteArrayDeserializer extends Base64BasedDeserializer { + public ByteArrayDeserializer() { + super(bytes -> bytes); + } + } + + public static class ECPublicKeySerializer extends Base64BasedSerializer { + public ECPublicKeySerializer() { + super(ECPublicKey::serialize); + } + } + + public static class ECPublicKeyDeserializer extends Base64BasedDeserializer { + public ECPublicKeyDeserializer() { + super(bytes -> Curve.decodePoint(bytes, 0)); + } + } + + public static class IdentityKeySerializer extends Base64BasedSerializer { + public IdentityKeySerializer() { + super(IdentityKey::serialize); + } + } + + public static class IdentityKeyDeserializer extends Base64BasedDeserializer { + public IdentityKeyDeserializer() { + super(bytes -> new IdentityKey(bytes, 0)); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/IntegrationTools.java b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/IntegrationTools.java new file mode 100644 index 000000000..109a84805 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/IntegrationTools.java @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.integration; + +import java.time.Clock; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import org.signal.integration.config.Config; +import org.whispersystems.textsecuregcm.registration.VerificationSession; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; +import org.whispersystems.textsecuregcm.storage.VerificationSessionManager; +import org.whispersystems.textsecuregcm.storage.VerificationSessions; +import org.whispersystems.textsecuregcm.util.DynamoDbFromConfig; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +public class IntegrationTools { + + private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager; + + private final VerificationSessionManager verificationSessionManager; + + + public static IntegrationTools create(final Config config) { + final AwsCredentialsProvider credentialsProvider = DefaultCredentialsProvider.builder().build(); + + final DynamoDbAsyncClient dynamoDbAsyncClient = DynamoDbFromConfig.asyncClient( + config.dynamoDbClientConfiguration(), + credentialsProvider); + + final DynamoDbClient dynamoDbClient = DynamoDbFromConfig.client( + config.dynamoDbClientConfiguration(), + credentialsProvider); + + final RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords( + config.dynamoDbTables().registrationRecovery(), Duration.ofDays(1), dynamoDbClient, dynamoDbAsyncClient); + + final VerificationSessions verificationSessions = new VerificationSessions( + dynamoDbAsyncClient, config.dynamoDbTables().verificationSessions(), Clock.systemUTC()); + + return new IntegrationTools( + new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords), + new VerificationSessionManager(verificationSessions) + ); + } + + private IntegrationTools( + final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager, + final VerificationSessionManager verificationSessionManager) { + this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager; + this.verificationSessionManager = verificationSessionManager; + } + + public CompletableFuture populateRecoveryPassword(final String e164, final byte[] password) { + return registrationRecoveryPasswordsManager.storeForCurrentNumber(e164, password); + } + + public CompletableFuture> peekVerificationSessionPushChallenge(final String sessionId) { + return verificationSessionManager.findForId(sessionId) + .thenApply(maybeSession -> maybeSession.map(VerificationSession::pushChallenge)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/Operations.java b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/Operations.java new file mode 100644 index 000000000..88c037bea --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/Operations.java @@ -0,0 +1,344 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.integration; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.io.Resources; +import com.google.common.net.HttpHeaders; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.net.URI; +import java.net.URL; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executors; +import org.apache.commons.lang3.RandomUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.tuple.Pair; +import org.signal.integration.config.Config; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.signal.libsignal.protocol.kem.KEMKeyPair; +import org.signal.libsignal.protocol.kem.KEMKeyType; +import org.signal.libsignal.protocol.kem.KEMPublicKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.entities.RegistrationRequest; +import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.HeaderUtils; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +public final class Operations { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final Config CONFIG = loadConfigFromClasspath("config.yml"); + + private static final IntegrationTools INTEGRATION_TOOLS = IntegrationTools.create(CONFIG); + + private static final String USER_AGENT = "integration-test"; + + private static final FaultTolerantHttpClient CLIENT = buildClient(); + + + private Operations() { + // utility class + } + + public static TestUser newRegisteredUser(final String number) { + final byte[] registrationPassword = RandomUtils.nextBytes(32); + final String accountPassword = Base64.getEncoder().encodeToString(RandomUtils.nextBytes(32)); + + final TestUser user = TestUser.create(number, accountPassword, registrationPassword); + final AccountAttributes accountAttributes = user.accountAttributes(); + + INTEGRATION_TOOLS.populateRecoveryPassword(number, registrationPassword).join(); + + // register account + final RegistrationRequest registrationRequest = new RegistrationRequest( + null, registrationPassword, accountAttributes, true, false, + Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()); + + final AccountIdentityResponse registrationResponse = apiPost("/v1/registration", registrationRequest) + .authorized(number, accountPassword) + .executeExpectSuccess(AccountIdentityResponse.class); + + user.setAciUuid(registrationResponse.uuid()); + user.setPniUuid(registrationResponse.pni()); + + // upload pre-key + final TestUser.PreKeySetPublicView preKeySetPublicView = user.preKeys(Device.MASTER_ID, false); + apiPut("/v2/keys", preKeySetPublicView) + .authorized(user, Device.MASTER_ID) + .executeExpectSuccess(); + + return user; + } + + public static TestUser newRegisteredUserAtomic(final String number) { + final byte[] registrationPassword = RandomUtils.nextBytes(32); + final String accountPassword = Base64.getEncoder().encodeToString(RandomUtils.nextBytes(32)); + + final TestUser user = TestUser.create(number, accountPassword, registrationPassword); + final AccountAttributes accountAttributes = user.accountAttributes(); + + INTEGRATION_TOOLS.populateRecoveryPassword(number, registrationPassword).join(); + + final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair(); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + + // register account + final RegistrationRequest registrationRequest = new RegistrationRequest(null, + registrationPassword, + accountAttributes, + true, + true, + Optional.of(new IdentityKey(aciIdentityKeyPair.getPublicKey())), + Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey())), + Optional.of(generateSignedECPreKey(1, aciIdentityKeyPair)), + Optional.of(generateSignedECPreKey(2, pniIdentityKeyPair)), + Optional.of(generateSignedKEMPreKey(3, aciIdentityKeyPair)), + Optional.of(generateSignedKEMPreKey(4, pniIdentityKeyPair)), + Optional.empty(), + Optional.empty()); + + final AccountIdentityResponse registrationResponse = apiPost("/v1/registration", registrationRequest) + .authorized(number, accountPassword) + .executeExpectSuccess(AccountIdentityResponse.class); + + user.setAciUuid(registrationResponse.uuid()); + user.setPniUuid(registrationResponse.pni()); + + return user; + } + + public static void deleteUser(final TestUser user) { + apiDelete("/v1/accounts/me").authorized(user).executeExpectSuccess(); + } + + public static String peekVerificationSessionPushChallenge(final String sessionId) { + return INTEGRATION_TOOLS.peekVerificationSessionPushChallenge(sessionId).join() + .orElseThrow(() -> new RuntimeException("push challenge not found for the verification session")); + } + + public static T sendEmptyRequestAuthenticated( + final String endpoint, + final String method, + final String username, + final String password, + final Class outputType) { + try { + final HttpRequest request = HttpRequest.newBuilder() + .uri(serverUri(endpoint, Collections.emptyList())) + .method(method, HttpRequest.BodyPublishers.noBody()) + .header(HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(username, password)) + .header(HttpHeaders.CONTENT_TYPE, "application/json") + .build(); + return CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) + .whenComplete((response, error) -> { + if (error != null) { + logger.error("request error", error); + error.printStackTrace(); + } else { + logger.info("response: {}", response.statusCode()); + System.out.println("response: " + response.statusCode() + ", " + response.body()); + } + }) + .thenApply(response -> { + try { + return outputType.equals(Void.class) + ? null + : SystemMapper.jsonMapper().readValue(response.body(), outputType); + } catch (final IOException e) { + throw new RuntimeException(e); + } + }) + .get(); + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + + public static RequestBuilder apiGet(final String endpoint) { + return new RequestBuilder(HttpRequest.newBuilder().GET(), endpoint); + } + + public static RequestBuilder apiDelete(final String endpoint) { + return new RequestBuilder(HttpRequest.newBuilder().DELETE(), endpoint); + } + + public static RequestBuilder apiPost(final String endpoint, final R input) { + return RequestBuilder.withJsonBody(endpoint, "POST", input); + } + + public static RequestBuilder apiPut(final String endpoint, final R input) { + return RequestBuilder.withJsonBody(endpoint, "PUT", input); + } + + public static RequestBuilder apiPatch(final String endpoint, final R input) { + return RequestBuilder.withJsonBody(endpoint, "PATCH", input); + } + + private static URI serverUri(final String endpoint, final List queryParams) { + final String query = queryParams.isEmpty() + ? StringUtils.EMPTY + : "?" + String.join("&", queryParams); + return URI.create("https://" + CONFIG.domain() + endpoint + query); + } + + public static class RequestBuilder { + + private final HttpRequest.Builder builder; + + private final String endpoint; + + private final List queryParams = new ArrayList<>(); + + + private RequestBuilder(final HttpRequest.Builder builder, final String endpoint) { + this.builder = builder; + this.endpoint = endpoint; + } + + private static RequestBuilder withJsonBody(final String endpoint, final String method, final R input) { + try { + final byte[] body = SystemMapper.jsonMapper().writeValueAsBytes(input); + return new RequestBuilder(HttpRequest.newBuilder() + .header(HttpHeaders.CONTENT_TYPE, "application/json") + .method(method, HttpRequest.BodyPublishers.ofByteArray(body)), endpoint); + } catch (final JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public RequestBuilder authorized(final TestUser user) { + return authorized(user, Device.MASTER_ID); + } + + public RequestBuilder authorized(final TestUser user, final long deviceId) { + final String username = "%s.%d".formatted(user.aciUuid().toString(), deviceId); + return authorized(username, user.accountPassword()); + } + + public RequestBuilder authorized(final String username, final String password) { + builder.header(HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(username, password)); + return this; + } + + public RequestBuilder queryParam(final String key, final String value) { + queryParams.add("%s=%s".formatted(key, value)); + return this; + } + + public RequestBuilder header(final String name, final String value) { + builder.header(name, value); + return this; + } + + public Pair execute() { + return execute(Void.class); + } + + public Pair executeExpectSuccess() { + final Pair execute = execute(); + Validate.isTrue( + execute.getLeft() >= 200 && execute.getLeft() < 300, + "Unexpected response code: %d", + execute.getLeft()); + return execute; + } + + public T executeExpectSuccess(final Class expectedType) { + final Pair execute = execute(expectedType); + return requireNonNull(execute.getRight()); + } + + public void executeExpectStatusCode(final int expectedStatusCode) { + final Pair execute = execute(Void.class); + Validate.isTrue( + execute.getLeft() == expectedStatusCode, + "Unexpected response code: %d", + execute.getLeft() + ); + } + + public Pair execute(final Class expectedType) { + builder.uri(serverUri(endpoint, queryParams)) + .header(HttpHeaders.USER_AGENT, USER_AGENT); + return CLIENT.sendAsync(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) + .whenComplete((response, error) -> { + if (error != null) { + logger.error("request error", error); + error.printStackTrace(); + } + }) + .thenApply(response -> { + try { + final T result = expectedType.equals(Void.class) + ? null + : SystemMapper.jsonMapper().readValue(response.body(), expectedType); + return Pair.of(response.statusCode(), result); + } catch (final IOException e) { + throw new RuntimeException(e); + } + }) + .join(); + } + } + + private static FaultTolerantHttpClient buildClient() { + try { + return FaultTolerantHttpClient.newBuilder() + .withName("integration-test") + .withExecutor(Executors.newFixedThreadPool(16)) + .withRetryExecutor(Executors.newSingleThreadScheduledExecutor()) + .withCircuitBreaker(new CircuitBreakerConfiguration()) + .withTrustedServerCertificates(CONFIG.rootCert()) + .build(); + } catch (final CertificateException e) { + throw new RuntimeException(e); + } + } + + private static Config loadConfigFromClasspath(final String filename) { + try { + final URL configFileUrl = Resources.getResource(filename); + return SystemMapper.yamlMapper().readValue(Resources.toByteArray(configFileUrl), Config.class); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + private static ECSignedPreKey generateSignedECPreKey(long id, final ECKeyPair identityKeyPair) { + final ECPublicKey pubKey = Curve.generateKeyPair().getPublicKey(); + final byte[] sig = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize()); + return new ECSignedPreKey(id, pubKey, sig); + } + + private static KEMSignedPreKey generateSignedKEMPreKey(long id, final ECKeyPair identityKeyPair) { + final KEMPublicKey pubKey = KEMKeyPair.generate(KEMKeyType.KYBER_1024).getPublicKey(); + final byte[] sig = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize()); + return new KEMSignedPreKey(id, pubKey, sig); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/TestDevice.java b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/TestDevice.java new file mode 100644 index 000000000..59c5de53a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/TestDevice.java @@ -0,0 +1,64 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.integration; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.lang3.tuple.Pair; +import org.signal.libsignal.protocol.IdentityKeyPair; +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.signal.libsignal.protocol.state.SignedPreKeyRecord; + +public class TestDevice { + + private final long deviceId; + + private final Map> signedPreKeys = new ConcurrentHashMap<>(); + + + public static TestDevice create( + final long deviceId, + final IdentityKeyPair aciIdentityKeyPair, + final IdentityKeyPair pniIdentityKeyPair) { + final TestDevice device = new TestDevice(deviceId); + device.addSignedPreKey(aciIdentityKeyPair); + device.addSignedPreKey(pniIdentityKeyPair); + return device; + } + + public TestDevice(final long deviceId) { + this.deviceId = deviceId; + } + + public long deviceId() { + return deviceId; + } + + public SignedPreKeyRecord latestSignedPreKey(final IdentityKeyPair identity) { + final int id = signedPreKeys.entrySet() + .stream() + .filter(p -> p.getValue().getLeft().equals(identity)) + .mapToInt(Map.Entry::getKey) + .max() + .orElseThrow(); + return signedPreKeys.get(id).getRight(); + } + + public SignedPreKeyRecord addSignedPreKey(final IdentityKeyPair identity) { + try { + final int nextId = signedPreKeys.keySet().stream().mapToInt(k -> k + 1).max().orElse(0); + final ECKeyPair keyPair = Curve.generateKeyPair(); + final byte[] signature = Curve.calculateSignature(identity.getPrivateKey(), keyPair.getPublicKey().serialize()); + final SignedPreKeyRecord signedPreKeyRecord = new SignedPreKeyRecord(nextId, System.currentTimeMillis(), keyPair, signature); + signedPreKeys.put(nextId, Pair.of(identity, signedPreKeyRecord)); + return signedPreKeyRecord; + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/TestUser.java b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/TestUser.java new file mode 100644 index 000000000..9abfcb7ea --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/TestUser.java @@ -0,0 +1,183 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.integration; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.lang3.RandomUtils; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.IdentityKeyPair; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.signal.libsignal.protocol.state.SignedPreKeyRecord; +import org.signal.libsignal.protocol.util.KeyHelper; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.storage.Device; + +public class TestUser { + + private final int registrationId; + + private final IdentityKeyPair aciIdentityKey; + + private final Map devices = new ConcurrentHashMap<>(); + + private final byte[] unidentifiedAccessKey; + + private String phoneNumber; + + private IdentityKeyPair pniIdentityKey; + + private String accountPassword; + + private byte[] registrationPassword; + + private UUID aciUuid; + + private UUID pniUuid; + + + public static TestUser create(final String phoneNumber, final String accountPassword, final byte[] registrationPassword) { + // ACI identity key pair + final IdentityKeyPair aciIdentityKey = IdentityKeyPair.generate(); + // PNI identity key pair + final IdentityKeyPair pniIdentityKey = IdentityKeyPair.generate(); + // registration id + final int registrationId = KeyHelper.generateRegistrationId(false); + // uak + final byte[] unidentifiedAccessKey = RandomUtils.nextBytes(16); + + return new TestUser( + registrationId, + aciIdentityKey, + phoneNumber, + pniIdentityKey, + unidentifiedAccessKey, + accountPassword, + registrationPassword); + } + + public TestUser( + final int registrationId, + final IdentityKeyPair aciIdentityKey, + final String phoneNumber, + final IdentityKeyPair pniIdentityKey, + final byte[] unidentifiedAccessKey, + final String accountPassword, + final byte[] registrationPassword) { + this.registrationId = registrationId; + this.aciIdentityKey = aciIdentityKey; + this.phoneNumber = phoneNumber; + this.pniIdentityKey = pniIdentityKey; + this.unidentifiedAccessKey = unidentifiedAccessKey; + this.accountPassword = accountPassword; + this.registrationPassword = registrationPassword; + devices.put(Device.MASTER_ID, TestDevice.create(Device.MASTER_ID, aciIdentityKey, pniIdentityKey)); + } + + public int registrationId() { + return registrationId; + } + + public IdentityKeyPair aciIdentityKey() { + return aciIdentityKey; + } + + public String phoneNumber() { + return phoneNumber; + } + + public IdentityKeyPair pniIdentityKey() { + return pniIdentityKey; + } + + public String accountPassword() { + return accountPassword; + } + + public byte[] registrationPassword() { + return registrationPassword; + } + + public UUID aciUuid() { + return aciUuid; + } + + public UUID pniUuid() { + return pniUuid; + } + + public AccountAttributes accountAttributes() { + return new AccountAttributes(true, registrationId, "", "", true, new Device.DeviceCapabilities(false, false, false, false)) + .withUnidentifiedAccessKey(unidentifiedAccessKey) + .withRecoveryPassword(registrationPassword); + } + + public void setAciUuid(final UUID aciUuid) { + this.aciUuid = aciUuid; + } + + public void setPniUuid(final UUID pniUuid) { + this.pniUuid = pniUuid; + } + + public void setPhoneNumber(final String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public void setPniIdentityKey(final IdentityKeyPair pniIdentityKey) { + this.pniIdentityKey = pniIdentityKey; + } + + public void setAccountPassword(final String accountPassword) { + this.accountPassword = accountPassword; + } + + public void setRegistrationPassword(final byte[] registrationPassword) { + this.registrationPassword = registrationPassword; + } + + public PreKeySetPublicView preKeys(final long deviceId, final boolean pni) { + final IdentityKeyPair identity = pni + ? pniIdentityKey + : aciIdentityKey; + final TestDevice device = requireNonNull(devices.get(deviceId)); + final SignedPreKeyRecord signedPreKeyRecord = device.latestSignedPreKey(identity); + return new PreKeySetPublicView( + Collections.emptyList(), + identity.getPublicKey(), + new SignedPreKeyPublicView( + signedPreKeyRecord.getId(), + signedPreKeyRecord.getKeyPair().getPublicKey(), + signedPreKeyRecord.getSignature() + ) + ); + } + + public record SignedPreKeyPublicView( + int keyId, + @JsonSerialize(using = Codecs.ECPublicKeySerializer.class) + @JsonDeserialize(using = Codecs.ECPublicKeyDeserializer.class) + ECPublicKey publicKey, + @JsonSerialize(using = Codecs.ByteArraySerializer.class) + @JsonDeserialize(using = Codecs.ByteArrayDeserializer.class) + byte[] signature) { + } + + public record PreKeySetPublicView( + List preKeys, + @JsonSerialize(using = Codecs.IdentityKeySerializer.class) + @JsonDeserialize(using = Codecs.IdentityKeyDeserializer.class) + IdentityKey identityKey, + SignedPreKeyPublicView signedPreKey) { + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/config/Config.java b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/config/Config.java new file mode 100644 index 000000000..6c7b4a77a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/config/Config.java @@ -0,0 +1,14 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.integration.config; + +import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration; + +public record Config(String domain, + String rootCert, + DynamoDbClientConfiguration dynamoDbClientConfiguration, + DynamoDbTables dynamoDbTables) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/config/DynamoDbTables.java b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/config/DynamoDbTables.java new file mode 100644 index 000000000..9a1843ad2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/main/java/org/signal/integration/config/DynamoDbTables.java @@ -0,0 +1,10 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.integration.config; + +public record DynamoDbTables(String registrationRecovery, + String verificationSessions) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/integration-tests/src/test/java/org/signal/integration/AccountTest.java b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/test/java/org/signal/integration/AccountTest.java new file mode 100644 index 000000000..7b6e6c703 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/test/java/org/signal/integration/AccountTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.integration; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; +import org.signal.libsignal.usernames.BaseUsernameException; +import org.signal.libsignal.usernames.Username; +import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse; +import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse; +import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest; +import org.whispersystems.textsecuregcm.entities.EncryptedUsername; +import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest; +import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse; +import org.whispersystems.textsecuregcm.entities.UsernameHashResponse; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; + +public class AccountTest { + + @Test + public void testCreateAccount() throws Exception { + final TestUser user = Operations.newRegisteredUser("+19995550101"); + try { + final Pair execute = Operations.apiGet("/v1/accounts/whoami") + .authorized(user) + .execute(AccountIdentityResponse.class); + assertEquals(HttpStatus.SC_OK, execute.getLeft()); + } finally { + Operations.deleteUser(user); + } + } + + @Test + public void testCreateAccountAtomic() throws Exception { + final TestUser user = Operations.newRegisteredUserAtomic("+19995550201"); + try { + final Pair execute = Operations.apiGet("/v1/accounts/whoami") + .authorized(user) + .execute(AccountIdentityResponse.class); + assertEquals(HttpStatus.SC_OK, execute.getLeft()); + } finally { + Operations.deleteUser(user); + } + } + + @Test + public void testUsernameOperations() throws Exception { + final TestUser user = Operations.newRegisteredUser("+19995550102"); + try { + verifyFullUsernameLifecycle(user); + // no do it again to check changing usernames + verifyFullUsernameLifecycle(user); + } finally { + Operations.deleteUser(user); + } + } + + private static void verifyFullUsernameLifecycle(final TestUser user) throws BaseUsernameException { + final String preferred = "test"; + final List candidates = Username.candidatesFrom(preferred, preferred.length(), preferred.length() + 1); + + // reserve a username + final ReserveUsernameHashRequest reserveUsernameHashRequest = new ReserveUsernameHashRequest( + candidates.stream().map(Username::getHash).toList()); + // try unauthorized + Operations + .apiPut("/v1/accounts/username_hash/reserve", reserveUsernameHashRequest) + .executeExpectStatusCode(HttpStatus.SC_UNAUTHORIZED); + + final ReserveUsernameHashResponse reserveUsernameHashResponse = Operations + .apiPut("/v1/accounts/username_hash/reserve", reserveUsernameHashRequest) + .authorized(user) + .executeExpectSuccess(ReserveUsernameHashResponse.class); + + // find which one is the reserved username + final byte[] reservedHash = reserveUsernameHashResponse.usernameHash(); + final Username reservedUsername = candidates.stream() + .filter(u -> Arrays.equals(u.getHash(), reservedHash)) + .findAny() + .orElseThrow(); + + // confirm a username + final ConfirmUsernameHashRequest confirmUsernameHashRequest = new ConfirmUsernameHashRequest( + reservedUsername.getHash(), + reservedUsername.generateProof(), + "cluck cluck i'm a parrot".getBytes() + ); + // try unauthorized + Operations + .apiPut("/v1/accounts/username_hash/confirm", confirmUsernameHashRequest) + .executeExpectStatusCode(HttpStatus.SC_UNAUTHORIZED); + Operations + .apiPut("/v1/accounts/username_hash/confirm", confirmUsernameHashRequest) + .authorized(user) + .executeExpectSuccess(UsernameHashResponse.class); + + + // lookup username + final AccountIdentifierResponse accountIdentifierResponse = Operations + .apiGet("/v1/accounts/username_hash/" + Base64.getUrlEncoder().encodeToString(reservedHash)) + .executeExpectSuccess(AccountIdentifierResponse.class); + assertEquals(new AciServiceIdentifier(user.aciUuid()), accountIdentifierResponse.uuid()); + // try authorized + Operations + .apiGet("/v1/accounts/username_hash/" + Base64.getUrlEncoder().encodeToString(reservedHash)) + .authorized(user) + .executeExpectStatusCode(HttpStatus.SC_BAD_REQUEST); + + // delete username + Operations + .apiDelete("/v1/accounts/username_hash") + .authorized(user) + .executeExpectSuccess(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/integration-tests/src/test/java/org/signal/integration/MessagingTest.java b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/test/java/org/signal/integration/MessagingTest.java new file mode 100644 index 000000000..634e55066 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/test/java/org/signal/integration/MessagingTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.integration; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.whispersystems.textsecuregcm.entities.IncomingMessage; +import org.whispersystems.textsecuregcm.entities.IncomingMessageList; +import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList; +import org.whispersystems.textsecuregcm.entities.SendMessageResponse; +import org.whispersystems.textsecuregcm.storage.Device; + +public class MessagingTest { + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testSendMessageUnsealed(final boolean atomicAccountCreation) throws Exception { + final TestUser userA; + final TestUser userB; + + if (atomicAccountCreation) { + userA = Operations.newRegisteredUserAtomic("+19995550102"); + userB = Operations.newRegisteredUserAtomic("+19995550103"); + } else { + userA = Operations.newRegisteredUser("+19995550104"); + userB = Operations.newRegisteredUser("+19995550105"); + } + + try { + final byte[] expectedContent = "Hello, World!".getBytes(StandardCharsets.UTF_8); + final String contentBase64 = Base64.getEncoder().encodeToString(expectedContent); + final IncomingMessage message = new IncomingMessage(1, Device.MASTER_ID, userB.registrationId(), contentBase64); + final IncomingMessageList messages = new IncomingMessageList(List.of(message), false, true, System.currentTimeMillis()); + + final Pair sendMessage = Operations + .apiPut("/v1/messages/%s".formatted(userB.aciUuid().toString()), messages) + .authorized(userA) + .execute(SendMessageResponse.class); + + final Pair receiveMessages = Operations.apiGet("/v1/messages") + .authorized(userB) + .execute(OutgoingMessageEntityList.class); + + final byte[] actualContent = receiveMessages.getRight().messages().get(0).content(); + assertArrayEquals(expectedContent, actualContent); + } finally { + Operations.deleteUser(userA); + Operations.deleteUser(userB); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/integration-tests/src/test/java/org/signal/integration/RegistrationTest.java b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/test/java/org/signal/integration/RegistrationTest.java new file mode 100644 index 000000000..b27faf800 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/integration-tests/src/test/java/org/signal/integration/RegistrationTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.integration; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.entities.CreateVerificationSessionRequest; +import org.whispersystems.textsecuregcm.entities.SubmitVerificationCodeRequest; +import org.whispersystems.textsecuregcm.entities.UpdateVerificationSessionRequest; +import org.whispersystems.textsecuregcm.entities.VerificationCodeRequest; +import org.whispersystems.textsecuregcm.entities.VerificationSessionResponse; + +public class RegistrationTest { + + @Test + public void testRegistration() throws Exception { + final UpdateVerificationSessionRequest originalRequest = new UpdateVerificationSessionRequest( + "test", UpdateVerificationSessionRequest.PushTokenType.FCM, null, null, null, null); + final CreateVerificationSessionRequest input = new CreateVerificationSessionRequest("+19995550102", originalRequest); + + final VerificationSessionResponse verificationSessionResponse = Operations + .apiPost("/v1/verification/session", input) + .executeExpectSuccess(VerificationSessionResponse.class); + + final String sessionId = verificationSessionResponse.id(); + final String pushChallenge = Operations.peekVerificationSessionPushChallenge(sessionId); + + // supply push challenge + final UpdateVerificationSessionRequest updatedRequest = new UpdateVerificationSessionRequest( + "test", UpdateVerificationSessionRequest.PushTokenType.FCM, pushChallenge, null, null, null); + final VerificationSessionResponse pushChallengeSupplied = Operations + .apiPatch("/v1/verification/session/%s".formatted(sessionId), updatedRequest) + .executeExpectSuccess(VerificationSessionResponse.class); + + Assertions.assertTrue(pushChallengeSupplied.allowedToRequestCode()); + + // request code + final VerificationCodeRequest verificationCodeRequest = new VerificationCodeRequest( + VerificationCodeRequest.Transport.SMS, "android-ng"); + + final VerificationSessionResponse codeRequested = Operations + .apiPost("/v1/verification/session/%s/code".formatted(sessionId), verificationCodeRequest) + .executeExpectSuccess(VerificationSessionResponse.class); + + // verify code + final SubmitVerificationCodeRequest submitVerificationCodeRequest = new SubmitVerificationCodeRequest("265402"); + final VerificationSessionResponse codeVerified = Operations + .apiPut("/v1/verification/session/%s/code".formatted(sessionId), submitVerificationCodeRequest) + .executeExpectSuccess(VerificationSessionResponse.class); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/pom.xml b/jdk_17_maven/cs/rest/signal-server/pom.xml new file mode 100644 index 000000000..066bd5003 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/pom.xml @@ -0,0 +1,506 @@ + + + 4.0.0 + pom + + + + central + Central Repository + https://repo.maven.apache.org/maven2 + + false + + + + + + + ossrh-snapshots + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + + + api-doc + event-logger + integration-tests + service + websocket-resources + + + + 2.20.130 + 3.25.0 + 1.9.0 + 2.13.0 + 2.1.7 + 1.1.13 + 26.22.0 + 1.56.1 + 2.10.1 + 2.13.5 + 2.3.1 + 1.9.0 + 1.5.1 + 6.2.6.RELEASE + 8.13.19 + 7.3 + 3.4.0 + 1.10.10 + 4.1.96.Final + 1.3.0 + 3.23.2 + 0.15.2 + 1.2.4 + 1.7.0 + 3.1.0 + 1.7.36 + 23.1.1 + 0.10.4 + + + b8af44d6a7e0615a7486d7307dd54bba23ff24e3aea14893fd2795e8c436d44e + + UTF-8 + + + org.whispersystems.textsecure + TextSecureServer + 10.3.0 + + + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + io.dropwizard + dropwizard-dependencies + ${dropwizard.version} + pom + import + + + + org.apache.tomcat + annotations-api + 6.0.53 + provided + + + io.netty + netty-bom + ${netty.version} + pom + import + + + software.amazon.awssdk + bom + ${aws.sdk2.version} + pom + import + + + com.google.cloud + libraries-bom + ${google-cloud-libraries.version} + pom + import + + + com.salesforce.servicelibs + reactor-grpc-stub + ${reactive.grpc.version} + + + io.github.resilience4j + resilience4j-bom + ${resilience4j.version} + pom + import + + + io.micrometer + micrometer-bom + ${micrometer.version} + pom + import + + + io.projectreactor + reactor-bom + 2022.0.10 + pom + import + + + org.jetbrains.kotlin + kotlin-bom + ${kotlin.version} + pom + import + + + com.eatthepath + pushy + ${pushy.version} + + + com.eatthepath + pushy-dropwizard-metrics-listener + ${pushy.version} + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + com.googlecode.libphonenumber + libphonenumber + ${libphonenumber.version} + + + com.vdurmont + semver4j + ${semver4j.version} + + + commons-io + commons-io + ${commons-io.version} + + + io.lettuce + lettuce-core + ${lettuce.version} + + + io.vavr + vavr + ${vavr.version} + + + javax.xml.bind + jaxb-api + ${jaxb.version} + + + net.logstash.logback + logstash-logback-encoder + ${logstash.logback.version} + + + org.apache.commons + commons-csv + ${commons-csv.version} + + + org.coursera + dropwizard-metrics-datadog + ${dropwizard-metrics-datadog.version} + + + org.glassfish.jaxb + jaxb-runtime + ${jaxb.version} + runtime + + + org.opentest4j + opentest4j + ${opentest4j.version} + test + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-nop + ${slf4j.version} + test + + + commons-logging + commons-logging + 1.2 + + + org.ow2.asm + asm + 9.5 + test + + + com.stripe + stripe-java + ${stripe.version} + + + com.braintreepayments.gateway + braintree-java + ${braintree.version} + + + com.google.code.gson + gson + ${gson.version} + + + org.signal + embedded-redis + 0.8.3 + test + + + org.signal + libsignal-server + 0.30.0 + + + org.apache.logging.log4j + log4j-bom + 2.17.1 + pom + import + + + + + + + org.hamcrest + hamcrest-all + 1.3 + test + + + com.github.tomakehurst + wiremock-jre8 + 2.35.1 + test + + + org.hamcrest + hamcrest-core + + + javax.xml.bind + jaxb-api + + + + + org.mockito + mockito-core + test + + + org.assertj + assertj-core + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit-pioneer + junit-pioneer + 2.0.1 + test + + + + + + + include-spam-filter + + + spam-filter/pom.xml + + + + spam-filter + + + + + exclude-spam-filter + + + spam-filter/pom.xml + + + + + + + + + kr.motd.maven + os-maven-plugin + 1.7.0 + + + + + + com.google.cloud.tools + jib-maven-plugin + 3.3.1 + + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + false + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + reactor-grpc + com.salesforce.servicelibs + reactor-grpc + ${reactive.grpc.version} + com.salesforce.reactorgrpc.ReactorGrpcGenerator + + + + + + + compile + compile-custom + test-compile + test-compile-custom + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 17 + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + true + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.2 + + + copy + test-compile + + copy-dependencies + + + test + so,dll,dylib + ${project.build.directory}/lib + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + + sqlite4java.library.path + ${project.build.directory}/lib + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + enforce + + + + + + 3.8.6 + + + + + + + + + org.apache.maven.plugins + maven-install-plugin + 3.0.0-M1 + + true + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.0.0-M1 + + true + + + + + + + diff --git a/jdk_17_maven/cs/rest/signal-server/service/assembly.xml b/jdk_17_maven/cs/rest/signal-server/service/assembly.xml new file mode 100644 index 000000000..642e2cc96 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/assembly.xml @@ -0,0 +1,25 @@ + + bin + false + + tar.gz + + + + ${project.basedir}/config + /config + + * + + + + ${project.build.directory} + / + + ${parent.artifactId}-${project.version}.jar + + + + diff --git a/jdk_17_maven/cs/rest/signal-server/service/config/sample-secrets-bundle.yml b/jdk_17_maven/cs/rest/signal-server/service/config/sample-secrets-bundle.yml new file mode 100644 index 000000000..123704c88 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/config/sample-secrets-bundle.yml @@ -0,0 +1,88 @@ +datadog.apiKey: unset + +stripe.apiKey: unset +stripe.idempotencyKeyGenerator: abcdefg12345678= # base64 for creating request idempotency hash + +braintree.privateKey: unset + +directoryV2.client.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth tokens for Signal users +directoryV2.client.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth identity tokens for Signal users + +svr2.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth tokens for Signal users +svr2.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth identity tokens for Signal users + +tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= + +awsAttachments.accessKey: test +awsAttachments.accessSecret: test + +gcpAttachments.rsaSigningKey: | + -----BEGIN PRIVATE KEY----- + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + AAAAAAAA + -----END PRIVATE KEY----- + +apn.teamId: team-id +apn.keyId: key-id +apn.signingKey: | + -----BEGIN PRIVATE KEY----- + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + AAAAAAAA + -----END PRIVATE KEY----- + +fcm.credentials: | + { "json": true } + +cdn.accessKey: test # AWS Access Key ID +cdn.accessSecret: test # AWS Access Secret + +unidentifiedDelivery.certificate: ABCD1234 +unidentifiedDelivery.privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA + +hCaptcha.apiKey: unset + +storageService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + +zkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA== + +genericZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA== + +paymentsService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # base64-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users +paymentsService.fixerApiKey: unset +paymentsService.coinMarketCapApiKey: unset + +artService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # base64-encoded 32-byte secret not shared with any external service, but used in ArtController +artService.userAuthenticationTokenUserIdSecret: AAAAAAAAAAA= # base64-encoded secret to obscure user phone numbers from Sticker Creator + +currentReportingKey.secret: AAAAAAAAAAA= +currentReportingKey.salt: AAAAAAAAAAA= + +turn.secret: AAAAAAAAAAA= + +linkDevice.secret: AAAAAAAAAAA= diff --git a/jdk_17_maven/cs/rest/signal-server/service/config/sample.yml b/jdk_17_maven/cs/rest/signal-server/service/config/sample.yml new file mode 100644 index 000000000..38c21ce1b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/config/sample.yml @@ -0,0 +1,404 @@ +# Example, relatively minimal, configuration that passes validation (see `io.dropwizard.cli.CheckCommand`) +# +# `unset` values will need to be set to work properly. +# Most other values are technically valid for a local/demonstration environment, but are probably not production-ready. + +logging: + level: INFO + appenders: + - type: console + threshold: ALL + timeZone: UTC + target: stdout + - type: logstashtcpsocket + destination: example.com:10516 + apiKey: secret://datadog.apiKey + environment: staging + +metrics: + reporters: + - type: signal-datadog + frequency: 10 seconds + tags: + - "env:staging" + - "service:chat" + udpTransport: + statsdHost: localhost + port: 8125 + excludesAttributes: + - m1_rate + - m5_rate + - m15_rate + - mean_rate + - stddev + useRegexFilters: true + excludes: + - ^.+\.total$ + - ^.+\.request\.filtering$ + - ^.+\.response\.filtering$ + - ^executor\..+$ + - ^lettuce\..+$ + reportOnStop: true + +adminEventLoggingConfiguration: + credentials: | + { + "key": "value" + } + projectId: some-project-id + logName: some-log-name + +grpcPort: 8080 + +stripe: + apiKey: secret://stripe.apiKey + idempotencyKeyGenerator: secret://stripe.idempotencyKeyGenerator + boostDescription: > + Example + supportedCurrenciesByPaymentMethod: + CARD: + - usd + - eur + SEPA_DEBIT: + - eur + + +braintree: + merchantId: unset + publicKey: unset + privateKey: secret://braintree.privateKey + environment: unset + graphqlUrl: unset + merchantAccounts: + # ISO 4217 currency code and its corresponding sub-merchant account + 'xts': unset + supportedCurrenciesByPaymentMethod: + PAYPAL: + - usd + +dynamoDbClientConfiguration: + region: us-west-2 # AWS Region + +dynamoDbTables: + accounts: + tableName: Example_Accounts + phoneNumberTableName: Example_Accounts_PhoneNumbers + phoneNumberIdentifierTableName: Example_Accounts_PhoneNumberIdentifiers + usernamesTableName: Example_Accounts_Usernames + scanPageSize: 100 + clientReleases: + tableName: Example_ClientReleases + deletedAccounts: + tableName: Example_DeletedAccounts + deletedAccountsLock: + tableName: Example_DeletedAccountsLock + issuedReceipts: + tableName: Example_IssuedReceipts + expiration: P30D # Duration of time until rows expire + generator: abcdefg12345678= # random base64-encoded binary sequence + ecKeys: + tableName: Example_Keys + ecSignedPreKeys: + tableName: Example_EC_Signed_Pre_Keys + pqKeys: + tableName: Example_PQ_Keys + pqLastResortKeys: + tableName: Example_PQ_Last_Resort_Keys + messages: + tableName: Example_Messages + expiration: P30D # Duration of time until rows expire + phoneNumberIdentifiers: + tableName: Example_PhoneNumberIdentifiers + profiles: + tableName: Example_Profiles + pushChallenge: + tableName: Example_PushChallenge + redeemedReceipts: + tableName: Example_RedeemedReceipts + expiration: P30D # Duration of time until rows expire + registrationRecovery: + tableName: Example_RegistrationRecovery + expiration: P300D # Duration of time until rows expire + remoteConfig: + tableName: Example_RemoteConfig + reportMessage: + tableName: Example_ReportMessage + subscriptions: + tableName: Example_Subscriptions + verificationSessions: + tableName: Example_VerificationSessions + +cacheCluster: # Redis server configuration for cache cluster + configurationUri: redis://redis.example.com:6379/ + +clientPresenceCluster: # Redis server configuration for client presence cluster + configurationUri: redis://redis.example.com:6379/ + +pubsub: # Redis server configuration for pubsub cluster + uri: redis://redis.example.com:6379/ + +pushSchedulerCluster: # Redis server configuration for push scheduler cluster + configurationUri: redis://redis.example.com:6379/ + +rateLimitersCluster: # Redis server configuration for rate limiters cluster + configurationUri: redis://redis.example.com:6379/ + +directoryV2: + client: # Configuration for interfacing with Contact Discovery Service v2 cluster + userAuthenticationTokenSharedSecret: secret://directoryV2.client.userAuthenticationTokenSharedSecret + userIdTokenSharedSecret: secret://directoryV2.client.userIdTokenSharedSecret + +svr2: + uri: svr2.example.com + userAuthenticationTokenSharedSecret: secret://svr2.userAuthenticationTokenSharedSecret + userIdTokenSharedSecret: secret://svr2.userIdTokenSharedSecret + svrCaCertificates: + - | + -----BEGIN CERTIFICATE----- + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + AAAAAAAAAAAAAAAAAAAA + -----END CERTIFICATE----- + + +messageCache: # Redis server configuration for message store cache + persistDelayMinutes: 1 + cluster: + configurationUri: redis://redis.example.com:6379/ + +metricsCluster: + configurationUri: redis://redis.example.com:6379/ + +awsAttachments: # AWS S3 configuration + accessKey: secret://awsAttachments.accessKey + accessSecret: secret://awsAttachments.accessSecret + bucket: aws-attachments + region: us-west-2 + +gcpAttachments: # GCP Storage configuration + domain: example.com + email: user@example.cocm + maxSizeInBytes: 1024 + pathPrefix: + rsaSigningKey: secret://gcpAttachments.rsaSigningKey + +tus: + uploadUri: https://example.org/upload + userAuthenticationTokenSharedSecret: secret://tus.userAuthenticationTokenSharedSecret + +accountDatabaseCrawler: + chunkSize: 10 # accounts per run + +apn: # Apple Push Notifications configuration + sandbox: true + bundleId: com.example.textsecuregcm + keyId: secret://apn.keyId + teamId: secret://apn.teamId + signingKey: secret://apn.signingKey + +fcm: # FCM configuration + credentials: secret://fcm.credentials + +cdn: + accessKey: secret://cdn.accessKey + accessSecret: secret://cdn.accessSecret + bucket: cdn # S3 Bucket name + region: us-west-2 # AWS region + +dogstatsd: + environment: dev + +unidentifiedDelivery: + certificate: secret://unidentifiedDelivery.certificate + privateKey: secret://unidentifiedDelivery.privateKey + expiresDays: 7 + +recaptcha: + projectPath: projects/example + credentialConfigurationJson: "{ }" # service account configuration for backend authentication + +hCaptcha: + apiKey: secret://hCaptcha.apiKey + +shortCode: + baseUrl: https://example.com/shortcodes/ + +storageService: + uri: storage.example.com + userAuthenticationTokenSharedSecret: secret://storageService.userAuthenticationTokenSharedSecret + storageCaCertificates: + - | + -----BEGIN CERTIFICATE----- + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + AAAAAAAAAAAAAAAAAAAA + -----END CERTIFICATE----- + +zkConfig: + serverPublic: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + serverSecret: secret://zkConfig.serverSecret + +genericZkConfig: + serverSecret: secret://genericZkConfig.serverSecret + +appConfig: + application: example + environment: example + configuration: example + +remoteConfig: + authorizedUsers: + - # 1st authorized user + - # 2nd authorized user + - # ... + - # Nth authorized user + requiredHostedDomain: example.com + audiences: + - # 1st audience + - # 2nd audience + - # ... + - # Nth audience + globalConfig: # keys and values that are given to clients on GET /v1/config + EXAMPLE_KEY: VALUE + +paymentsService: + userAuthenticationTokenSharedSecret: secret://paymentsService.userAuthenticationTokenSharedSecret + fixerApiKey: secret://paymentsService.fixerApiKey + coinMarketCapApiKey: secret://paymentsService.coinMarketCapApiKey + coinMarketCapCurrencyIds: + MOB: 7878 + paymentCurrencies: + # list of symbols for supported currencies + - MOB + +artService: + userAuthenticationTokenSharedSecret: secret://artService.userAuthenticationTokenSharedSecret + userAuthenticationTokenUserIdSecret: secret://artService.userAuthenticationTokenUserIdSecret + +badges: + badges: + - id: TEST + category: other + sprites: # exactly 6 + - sprite-1.png + - sprite-2.png + - sprite-3.png + - sprite-4.png + - sprite-5.png + - sprite-6.png + svg: example.svg + svgs: + - light: example-light.svg + dark: example-dark.svg + badgeIdsEnabledForAll: + - TEST + receiptLevels: + '1': TEST + +subscription: # configuration for Stripe subscriptions + badgeGracePeriod: P15D + levels: + 500: + badge: EXAMPLE + prices: + # list of ISO 4217 currency codes and amounts for the given badge level + xts: + amount: '10' + processorIds: + STRIPE: price_example # stripe Price ID + BRAINTREE: plan_example # braintree Plan ID + +oneTimeDonations: + sepaMaxTransactionSizeEuros: '10000' + boost: + level: 1 + expiration: P90D + badge: EXAMPLE + gift: + level: 10 + expiration: P90D + badge: EXAMPLE + currencies: + # ISO 4217 currency codes and amounts in those currencies + xts: + minimum: '0.5' + gift: '2' + boosts: + - '1' + - '2' + - '4' + - '8' + - '20' + - '40' + +registrationService: + host: registration.example.com + port: 443 + credentialConfigurationJson: | + { + "example": "example" + } + identityTokenAudience: https://registration.example.com + registrationCaCertificate: | # Registration service TLS certificate trust root + -----BEGIN CERTIFICATE----- + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + AAAAAAAAAAAAAAAAAAAA + -----END CERTIFICATE----- + +turn: + secret: secret://turn.secret + +commandStopListener: + path: /example/path + +linkDevice: + secret: secret://linkDevice.secret diff --git a/jdk_17_maven/cs/rest/signal-server/service/pom.xml b/jdk_17_maven/cs/rest/signal-server/service/pom.xml new file mode 100644 index 000000000..b60651ee9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/pom.xml @@ -0,0 +1,709 @@ + + + + TextSecureServer + org.whispersystems.textsecure + + 10.3.0 + + 4.0.0 + service + + + + io.swagger.core.v3 + swagger-jaxrs2 + 2.2.8 + + + + org.yaml + snakeyaml + + + + + jakarta.servlet + jakarta.servlet-api + + + jakarta.validation + jakarta.validation-api + + + jakarta.ws.rs + jakarta.ws.rs-api + + + + org.whispersystems.textsecure + event-logger + ${project.version} + + + org.whispersystems.textsecure + websocket-resources + ${project.version} + + + org.signal + libsignal-server + + + + io.dropwizard + dropwizard-core + + + io.dropwizard + dropwizard-auth + + + io.dropwizard + dropwizard-client + + + io.dropwizard + dropwizard-db + + + io.dropwizard + dropwizard-logging + + + io.dropwizard + dropwizard-metrics + + + io.dropwizard + dropwizard-util + + + io.dropwizard + dropwizard-servlets + + + io.dropwizard + dropwizard-lifecycle + + + io.dropwizard + dropwizard-jersey + + + io.dropwizard + dropwizard-jetty + + + io.dropwizard + dropwizard-validation + + + io.dropwizard + dropwizard-migrations + runtime + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-access + + + ch.qos.logback + logback-classic + + + net.logstash.logback + logstash-logback-encoder + + + + io.dropwizard.metrics + metrics-core + + + io.dropwizard.metrics + metrics-healthchecks + + + io.dropwizard.metrics + metrics-annotation + + + org.glassfish.jersey.core + jersey-common + + + org.glassfish.jersey.core + jersey-server + + + org.glassfish.jersey.core + jersey-client + + + org.glassfish.jaxb + jaxb-runtime + + + + io.dropwizard + dropwizard-testing + test + + + junit + junit + + + + + + party.iroiro.luajava + luajava + ${luajava.version} + test + + + party.iroiro.luajava + lua51 + ${luajava.version} + test + + + party.iroiro.luajava + lua51-platform + ${luajava.version} + natives-desktop + runtime + + + + org.eclipse.jetty.websocket + websocket-api + + + org.eclipse.jetty + jetty-servlets + + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-csv + + + + com.google.firebase + firebase-admin + 9.1.1 + + + + com.google.code.findbugs + jsr305 + + + + io.github.resilience4j + resilience4j-circuitbreaker + + + io.github.resilience4j + resilience4j-retry + + + io.github.resilience4j + resilience4j-reactor + + + + io.grpc + grpc-netty-shaded + runtime + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + + org.apache.tomcat + annotations-api + provided + + + + io.micrometer + micrometer-core + + + io.micrometer + micrometer-registry-statsd + + + org.coursera + dropwizard-metrics-datadog + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + + + + com.salesforce.servicelibs + reactor-grpc-stub + + + + software.amazon.awssdk + sts + + + software.amazon.awssdk + s3 + + + software.amazon.awssdk + dynamodb + + + software.amazon.awssdk + appconfig + + + software.amazon.awssdk + appconfigdata + + + com.amazonaws + dynamodb-lock-client + 1.2.0 + + + commons-logging + commons-logging + + + + + + io.lettuce + lettuce-core + + + + com.eatthepath + pushy + + + com.eatthepath + pushy-dropwizard-metrics-listener + + + + com.vdurmont + semver4j + + + + com.google.guava + guava + + + + com.google.protobuf + protobuf-java + + + + com.googlecode.libphonenumber + libphonenumber + + + + net.sourceforge.argparse4j + argparse4j + + + + org.glassfish.jersey.test-framework + jersey-test-framework-core + test + + + junit + junit + + + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + test + + + javax.servlet + javax.servlet-api + + + junit + junit + + + + + + com.almworks.sqlite4java + sqlite4java + 1.0.392 + test + + + + io.projectreactor + reactor-core + + + io.projectreactor + reactor-core-micrometer + + + io.vavr + vavr + + + + org.junit.jupiter + junit-jupiter-params + test + + + + io.projectreactor + reactor-test + + + + org.signal + embedded-redis + test + + + + com.fasterxml.uuid + java-uuid-generator + 4.2.0 + test + + + + com.amazonaws + DynamoDBLocal + 1.23.0 + test + + + io.github.ganadist.sqlite4java + libsqlite4java-osx-aarch64 + 1.0.392 + dylib + test + + + + com.google.auth + google-auth-library-oauth2-http + + + + com.google.cloud + google-cloud-recaptchaenterprise + + + + com.stripe + stripe-java + + + + com.braintreepayments.gateway + braintree-java + + + + com.apollographql.apollo3 + apollo-api-jvm + 3.8.2 + + + + + + + exclude-spam-filter + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + + org.whispersystems.textsecuregcm.WhisperServerService + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.6.0 + + + assembly.xml + + + + + make-assembly + package + + single + + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.2.0 + + + read-deploy-configuration + deploy + + read-project-properties + + + ${project.basedir}/config/deploy.properties + + + + + + + com.google.cloud.tools + jib-maven-plugin + + + deploy + + build + + + + + + eclipse-temurin@sha256:${docker.image.sha256} + + + ${docker.repo}:${project.version} + + + org.whispersystems.textsecuregcm.WhisperServerService + + -server + -Djava.awt.headless=true + -Djdk.nio.maxCachedBufferSize=262144 + -Dlog4j2.formatMsgNoLookups=true + -XX:MaxRAMPercentage=75 + -XX:+HeapDumpOnOutOfMemoryError + -XX:HeapDumpPath=/tmp/heapdump.bin + + + 8080 + + USE_CURRENT_TIMESTAMP + + + + + ${project.basedir}/config + *.yml + /usr/share/signal/ + + + + + + + + + + include-spam-filter + + + + com.google.cloud.tools + jib-maven-plugin + + + true + + + + + + + + + ${project.parent.artifactId}-${project.version} + + + org.codehaus.mojo + templating-maven-plugin + 1.0.0 + + + filter-src + + filter-sources + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + --add-opens=java.base/java.net=ALL-UNNAMED + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + + check-all-service-config + verify + + java + + + + + org.whispersystems.textsecuregcm.CheckServiceConfigurations + test + + ${project.basedir}/config + + + + + + com.github.aoudiamoncef + apollo-client-maven-plugin + 5.0.0 + + + + generate + + + + + + braintree + + com.braintree.graphql.client + + + + + + + + + + + diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/ChargePayPalOneTimePayment.graphql b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/ChargePayPalOneTimePayment.graphql new file mode 100644 index 000000000..2007a93be --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/ChargePayPalOneTimePayment.graphql @@ -0,0 +1,9 @@ +# https://graphql.braintreepayments.com/reference/#Mutation--chargePaymentMethod +mutation ChargePayPalOneTimePayment($input: ChargePaymentMethodInput!) { + chargePaymentMethod(input: $input) { + transaction { + id, + status + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/CreatePayPalBillingAgreement.graphql b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/CreatePayPalBillingAgreement.graphql new file mode 100644 index 000000000..7d1887334 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/CreatePayPalBillingAgreement.graphql @@ -0,0 +1,6 @@ +mutation CreatePayPalBillingAgreement($input: CreatePayPalBillingAgreementInput!) { + createPayPalBillingAgreement(input: $input) { + approvalUrl, + billingAgreementToken + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/CreatePayPalOneTimePayment.graphql b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/CreatePayPalOneTimePayment.graphql new file mode 100644 index 000000000..58ee6b6e9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/CreatePayPalOneTimePayment.graphql @@ -0,0 +1,7 @@ +# https://graphql.braintreepayments.com/reference/#Mutation--createPayPalOneTimePayment +mutation CreatePayPalOneTimePayment($input: CreatePayPalOneTimePaymentInput!) { + createPayPalOneTimePayment(input: $input) { + approvalUrl, + paymentId + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/TokenizePayPalBillingAgreement.graphql b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/TokenizePayPalBillingAgreement.graphql new file mode 100644 index 000000000..ca07fa375 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/TokenizePayPalBillingAgreement.graphql @@ -0,0 +1,7 @@ +mutation TokenizePayPalBillingAgreement($input: TokenizePayPalBillingAgreementInput!) { + tokenizePayPalBillingAgreement(input: $input) { + paymentMethod { + id + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/TokenizePayPalOneTimePayment.graphql b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/TokenizePayPalOneTimePayment.graphql new file mode 100644 index 000000000..ae0d5b72f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/TokenizePayPalOneTimePayment.graphql @@ -0,0 +1,8 @@ +# https://graphql.braintreepayments.com/reference/#Mutation--tokenizePayPalOneTimePayment +mutation TokenizePayPalOneTimePayment($input: TokenizePayPalOneTimePaymentInput!) { + tokenizePayPalOneTimePayment(input: $input) { + paymentMethod { + id + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/VaultPaymentMethod.graphql b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/VaultPaymentMethod.graphql new file mode 100644 index 000000000..bcce3f7ea --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/VaultPaymentMethod.graphql @@ -0,0 +1,7 @@ +mutation VaultPaymentMethod($input: VaultPaymentMethodInput!) { + vaultPaymentMethod(input: $input) { + paymentMethod { + id + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/schema.json b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/schema.json new file mode 100644 index 000000000..32e2145da --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/graphql/braintree/schema.json @@ -0,0 +1,35093 @@ +{ + "data": { + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": { + "name": "Mutation" + }, + "subscriptionType": null, + "types": [ + { + "kind": "ENUM", + "name": "ACHStandardEntryClassCode", + "description": "A NACHA standard entry class (SEC) code, which designates how an ACH transaction was authorized.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CCD", + "description": "Corporate credit or debit.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PPD", + "description": "Prearranged payment and deposit.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TEL", + "description": "Telephone-initiated.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "WEB", + "description": "Internet-initiated/mobile.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ACRType", + "description": "The authentication context class reference that indcates how a universal access token can be used.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CLIENT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SERVER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "AcceptDisputeInput", + "description": "Top-level input fields for accepting a dispute.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "disputeId", + "description": "The ID of the dispute to be accepted.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "AcceptDisputePayload", + "description": "Top-level field returned when accepting a dispute.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dispute", + "description": "Information about the dispute that was accepted.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Dispute", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "AccessToken", + "description": "An OAuth access token.", + "fields": [ + { + "name": "accessToken", + "description": "The access token.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refreshToken", + "description": "The refresh token for getting a new access token.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tokenType", + "description": "The type of token.", + "args": [], + "type": { + "kind": "ENUM", + "name": "OAuthTokenType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expiresAt", + "description": "Expiration in ISO time format.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "AccountCreationStatus", + "description": "The status of the business account creation request.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "COMPLETED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DECLINED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "IN_SETUP", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "IN_VETTING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBMITTED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "AccountCreationStatusSearchInput", + "description": "Input fields for searching for BusinessAccountCreationRequests by their `AccountCreationStatus`.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "The creation status is exactly this value.", + "type": { + "kind": "ENUM", + "name": "AccountCreationStatus", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "in", + "description": "The creation status is one of these values.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "AccountCreationStatus", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Address", + "description": "Representation of an address.", + "fields": [ + { + "name": "company", + "description": "Company name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "streetAddress", + "description": "The street address.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `addressLine1` instead." + }, + { + "name": "addressLine1", + "description": "The first line of the street address, such as street number, street name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "extendedAddress", + "description": "Extended address information, such as an apartment or suite number.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `addressLine2` instead." + }, + { + "name": "addressLine2", + "description": "Extended address information, such as an apartment number or suite number.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "firstName", + "description": "First name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `fullName` instead." + }, + { + "name": "lastName", + "description": "Last name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `fullName` instead." + }, + { + "name": "fullName", + "description": "Full name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locality", + "description": "Locality/city.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `adminArea2` instead." + }, + { + "name": "adminArea2", + "description": "A city, town, or village.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "region", + "description": "State or province.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `adminArea1` instead." + }, + { + "name": "adminArea1", + "description": "Highest level subdivision, such as state, province, or ISO-3166-2 subdivison.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postalCode", + "description": "Postal code, otherwise known as CAP, CEP, Eircode, NPA, PIN, PLZ, or ZIP code.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "countryCode", + "description": "Country code for the address.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CountryCode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "phoneNumber", + "description": "Phone number.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "AddressInput", + "description": "Input fields for an Address.", + "fields": null, + "inputFields": [ + { + "name": "company", + "description": "Company name. 255 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "streetAddress", + "description": "The street address. 255 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "addressLine1", + "description": "The first line of the street address, such as street number, street name. 255 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "extendedAddress", + "description": "Extended address information, such as an apartment or suite number. 255 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "addressLine2", + "description": "Extended address information, such as apartment number or suite number. 255 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "firstName", + "description": "First name. 255 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "lastName", + "description": "Last name. 255 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "locality", + "description": "Locality/city. 255 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "adminArea2", + "description": "A city, town or village. 255 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "region", + "description": "State or province. 255 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "adminArea1", + "description": "Highest level subdivision, such as state, province or ISO-3166-2 subdivision. 255 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "postalCode", + "description": "Postal code in any country's format, otherwise known as CAP, CEP, Eircode, NPA, PIN, PLZ, or ZIP code. Nine alphanumeric characters maximum, may also contain spaces and hyphens.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "countryCode", + "description": "Country code for the address.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "CountryCode", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "countryCodeAlpha3", + "description": "Deprecated: This field is included for supporting legacy clients. Please use `countryCode` instead.\n\nCountry code for the address in ISO 3166-1 alpha-3 format.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "countryCodeAlpha2", + "description": "Deprecated: This field is included for supporting legacy clients. Please use `countryCode` instead.\n\nCountry code for the address in ISO 3166-1 alpha-2 format.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "countryCodeNumeric", + "description": "Deprecated: This field is included for supporting legacy clients. Please use `countryCode` instead.\n\nCountry code for the address in ISO 3166-1 numeric format.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "countryName", + "description": "Deprecated: This field is included for supporting legacy clients. Please use `countryCode` instead.\n\nCountry name for the address.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Amount", + "description": "A monetary amount, either a whole number or a number with exactly two or three decimal places.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ApplePayConfiguration", + "description": "Configuration for Apple Pay on iOS.", + "fields": [ + { + "name": "status", + "description": "The environment being used for Apple Pay.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ApplePayStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "countryCode", + "description": "The country code of the acquiring bank where the transaction is likely to be processed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CountryCodeAlpha2", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "currencyCode", + "description": "The merchant's Apple Pay currency code.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CurrencyCodeAlpha", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchantIdentifier", + "description": "The merchant identifier that must be supplied when making an Apple Pay request.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "supportedCardBrands", + "description": "A list of card brands supported by the merchant for Apple Pay.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ApplePayOriginDetails", + "description": "Additional information about the payment method specific to Apple Pay.", + "fields": [ + { + "name": "paymentInstrumentName", + "description": "A human-readable description of the Apple Pay payment method. This usually consists of the Apple Pay card type and its last four digits. If there is no underlying credit card, this will describe the customer's payment method and the parent CreditCardDetail object's last4 field will be null.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bin", + "description": "The first 6 digits of the credit card, known as the Bank Identification Number. This BIN may differ from the BIN of the customer's actual card.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ApplePayStatus", + "description": "The environment being used for Apple Pay.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "MOCK", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OFF", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRODUCTION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mock", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "off", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "production", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ApplePayWebConfiguration", + "description": "Configuration for Apple Pay on web.", + "fields": [ + { + "name": "countryCode", + "description": "The merchant's Apple Pay country code.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CountryCodeAlpha2", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "currencyCode", + "description": "The merchant's Apple Pay currency code.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CurrencyCodeAlpha", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchantIdentifier", + "description": "The merchant identifier that must be supplied when making an Apple Pay request.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "supportedCardBrands", + "description": "A list of card brands supported by the merchant for Apple Pay.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ApplicationBankAccountPurpose", + "description": "The purpose of the merchant application bank account.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CHECKING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SAVINGS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ApplicationStatus", + "description": "The status of a merchant account application.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "APPROVED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROCESSING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "REJECTED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "AuthenticationInsight", + "description": "Information about the [customer authentication regulation environment](https://developers.braintreepayments.com/guides/3d-secure/migration/javascript/v3#authentication-insight) that applies to the payment method when processed with the provided merchant account.", + "fields": [ + { + "name": "merchantAccountId", + "description": "The merchant account used to determine authentication insight.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customerAuthenticationRegulationEnvironment", + "description": "The customer authentication regulation environment that applies when transacting with this payment method and merchant account.", + "args": [], + "type": { + "kind": "ENUM", + "name": "CustomerAuthenticationRegulationEnvironment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customerAuthenticationIndicator", + "description": "A value indicating when to perform further customer authentication.", + "args": [], + "type": { + "kind": "ENUM", + "name": "CustomerAuthenticationIndicator", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "AuthenticationInsightInput", + "description": "Input fields when requesting authentication insight for a payment method.", + "fields": null, + "inputFields": [ + { + "name": "merchantAccountId", + "description": "ID of the merchant account that will be used when charging this payment method.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "The intended transaction amount to be authorized on this payment method.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "recurringCustomerConsent", + "description": "A flag indicating whether the customer has consented to further recurring transactions.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "recurringMaxAmount", + "description": "The maximum amount permitted for recurring transactions set by the customer.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "AuthorizationAdjustment", + "description": "Records of authorization adjustments performed when a transaction is captured for less or more than its original authorization amount.", + "fields": [ + { + "name": "amount", + "description": "Difference between the authorized amount and the amount captured. Negative values indicate the authorized amount was adjusted down.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "successful", + "description": "Indicates if the adjustment was successful or not.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when this adjustment was performed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processorResponse", + "description": "Processor response from this adjustment.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionAuthorizationAdjustmentProcessorResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "AuthorizationExpiredEvent", + "description": "Accompanying information for an authorization expired transaction.", + "fields": [ + { + "name": "status", + "description": "The new status of the transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the authorization for this transaction was marked expired.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount of the transaction for this status event.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "The source for the transaction change to the new status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminal", + "description": "Whether or not this is the final state for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "AuthorizeCreditCardInput", + "description": "Top-level input fields for creating a transaction by authorizing a credit card.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of a credit card payment method to be authorized.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "options", + "description": "Input fields related to the credit card being authorized.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CreditCardTransactionOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transaction", + "description": "Input fields for the authorization, with details that will define the resulting transaction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "AuthorizePayPalAccountInput", + "description": "Top-level input fields for creating a transaction by authorizing a PayPal account.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of a PayPal payment method to be authorized.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "options", + "description": "Input fields related to the PayPal account being authorized.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AuthorizePayPalAccountOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transaction", + "description": "Input fields for the authorization, with details that will define the resulting transaction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "AuthorizePayPalAccountOptionsInput", + "description": "Input fields for authorizing a PayPal account.", + "fields": null, + "inputFields": [ + { + "name": "customField", + "description": "Variable passed directly to PayPal for your own tracking purposes. Customers do not see this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "description", + "description": "Description of the transaction that is displayed to customers in PayPal email receipts.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "payee", + "description": "Deprecated: This field is no longer supported.", + "type": { + "kind": "INPUT_OBJECT", + "name": "PayPalPayeeOptionsInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "AuthorizePaymentMethodInput", + "description": "Top-level input fields for creating a transaction by authorizing a payment method.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of a payment method to be authorized.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "transaction", + "description": "Input fields for the authorization, with details that will define the resulting transaction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "AuthorizeVenmoAccountInput", + "description": "Top-level input fields for creating a transaction by authorizing a Venmo account.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of a Venmo payment method to be authorized.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "options", + "description": "Input fields related to the Venmo account being authorized.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AuthorizeVenmoAccountOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transaction", + "description": "Input fields for the authorization, with details that will define the resulting transaction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "AuthorizeVenmoAccountOptionsInput", + "description": "Input fields for authorizing a Venmo account.", + "fields": null, + "inputFields": [ + { + "name": "profileId", + "description": "Specifies which Venmo business profile to use for the transaction.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "AuthorizedEvent", + "description": "Accompanying information for an authorized transaction.", + "fields": [ + { + "name": "status", + "description": "The new status of the transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the transaction was authorized.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount the transaction was authorized for. This will match the amount on the transaction itself. In most cases, you can't request to settle more than this amount.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "The source for the transaction change to the new status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processorResponse", + "description": "Fields describing the payment processor response to the authorization request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionAuthorizationProcessorResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "networkResponse", + "description": "Fields describing the network response to the authorization request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentNetworkResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "riskDecision", + "description": "Risk decision for this transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "RiskDecision", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminal", + "description": "Whether or not this is the final state for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authorizationExpiresAt", + "description": "The date/time the transaction will expire if it has the authorized status. For more details on authorization expiration timeframes, see the [Statuses reference](https://developers.braintreepayments.com/reference/general/statuses#authorization-expired).", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "AvsCvvResponseCode", + "description": "Response codes from the processing bank's Address Verification System (AVS) and CVV verification.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "BYPASS", + "description": "AVS or CVV checks were skipped via the API.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DOES_NOT_MATCH", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ISSUER_DOES_NOT_PARTICIPATE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MATCHES", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NOT_APPLICABLE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NOT_PROVIDED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NOT_VERIFIED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SYSTEM_ERROR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "BinRecord", + "description": "Information about the credit card based on its BIN.", + "fields": [ + { + "name": "prepaid", + "description": "Whether or not the card is prepaid, such as a gift card.", + "args": [], + "type": { + "kind": "ENUM", + "name": "BinRecordValue", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "healthcare", + "description": "Whether the card is designated only to be used for healthcare expenses.", + "args": [], + "type": { + "kind": "ENUM", + "name": "BinRecordValue", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "debit", + "description": "Whether or not the card is a debit card.", + "args": [], + "type": { + "kind": "ENUM", + "name": "BinRecordValue", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "durbinRegulated", + "description": "Whether the card is regulated by the Durbin Amendment due to the bank's assets, and therefore has a maximum interchange rate.", + "args": [], + "type": { + "kind": "ENUM", + "name": "BinRecordValue", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "commercial", + "description": "Whether or not the card is a commercial card and capable of processing Level 2 transactions.", + "args": [], + "type": { + "kind": "ENUM", + "name": "BinRecordValue", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payroll", + "description": "Whether or not the card is designated for employee wages.", + "args": [], + "type": { + "kind": "ENUM", + "name": "BinRecordValue", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issuingBank", + "description": "The name of the bank that issued the card.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "countryOfIssuance", + "description": "The country code of the country that issued the card.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CountryCode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "productId", + "description": "A code representing any special program from the card issuer the card is part of.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "BinRecordValue", + "description": "A boolean-like value that includes `UNKNOWN` in the case where the information isn't available.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "NO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNKNOWN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "YES", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "No", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Unknown", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Yes", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "Built-in Boolean", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "BraintreeApiConfiguration", + "description": "Configuration for payment methods in legacy clients.", + "fields": [ + { + "name": "url", + "description": "The URL for tokenizing payment methods.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "accessToken", + "description": "The authentication for tokenizing payment methods.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "BusinessAccountCreationRequest", + "description": "Record of onboarding request.", + "fields": [ + { + "name": "id", + "description": "Unique identifier generated by PayPal for the onboarding request.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchantAccount", + "description": "Information about the merchant account that is being created as a result of the request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MerchantAccount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "creationStatus", + "description": "The account creation status for this account.", + "args": [], + "type": { + "kind": "ENUM", + "name": "AccountCreationStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "BusinessAccountCreationRequestConnection", + "description": "A paginated list of BusinessAccountCreationRequests.", + "fields": [ + { + "name": "edges", + "description": "A list of BusinessAccountCreationRequests.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "BusinessAccountCreationRequestConnectionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information about the page of BusinessAccountCreationRequests contained in `edges`.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "BusinessAccountCreationRequestConnectionEdge", + "description": "A BusinessAccountCreationRequest within a BusinessAccountCreationRequestConnection.", + "fields": [ + { + "name": "cursor", + "description": "This BusinessAccountCreationRequest's location within the BusinessAccountCreationRequestConnection. Used for requesting additional pages.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The business account creation request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "BusinessAccountCreationRequest", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "BusinessAccountCreationRequestSearchInput", + "description": "Input fields for searching for BusinessAccountCreationRequests.", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "Find BusinessAccountCreationRequests with an ID or IDs.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "externalId", + "description": "Find BusinessAccountCreationRequests by their external ID or IDs.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "status", + "description": "Find BusinessAccountCreationRequests by their creation status.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AccountCreationStatusSearchInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "BusinessType", + "description": "The type of the business.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "GOVERNMENT_AGENCY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIMITED_LIABILITY_CORPORATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NONPROFIT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PARTNERSHIP", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PARTNERSHIP_LLP", + "description": null, + "isDeprecated": true, + "deprecationReason": "No longer applicable, use PARTNERSHIP instead." + }, + { + "name": "PRIVATE_CORPORATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PUBLIC_CORPORATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SOLE_PROPRIETORSHIP", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TAX_EXEMPT", + "description": null, + "isDeprecated": true, + "deprecationReason": "No longer applicable, use NONPROFIT instead." + } + ], + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "CVV", + "description": "A three- or four-digit string CVV (card verification value), otherwise known as CSC or CVC.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CaptureTransactionInput", + "description": "Top-level input fields for capturing an authorized transaction.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transactionId", + "description": "ID of the transaction to be captured.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "Deprecated: This field is included for supporting legacy clients. Please use `transaction.amount` instead.\n\nThe amount to capture on the transaction. Must be greater than 0. You can't capture more than the authorized amount unless your industry and processor support settlement adjustment (capturing a certain percentage over the authorized amount); [contact us for assistance](https://help.braintreepayments.com?issue=TransactionProcessingQuestion). If you capture an amount that is less than what was authorized, the transaction object will return the amount captured.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transaction", + "description": "Input fields for the capture, with details that will define the resulting transaction.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CaptureTransactionOptionsInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CaptureTransactionOptionsInput", + "description": "Input fields for a capture, with details that will define the resulting transaction.", + "fields": null, + "inputFields": [ + { + "name": "amount", + "description": "The amount to capture on the transaction. Must be greater than 0. You can't capture more than the authorized amount unless your industry and processor support settlement adjustment (capturing a certain percentage over the authorized amount); [contact us for assistance](https://help.braintreepayments.com?issue=TransactionProcessingQuestion). If you capture an amount that is less than what was authorized, the transaction object will return the amount captured.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "descriptor", + "description": "Fields used to define what will appear on a customer's bank statement for a specific purchase. If specified, this will update the existing descriptor on the transaction.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionDescriptorInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "discountAmount", + "description": "Discount amount that was included in the total transaction amount. Does not add to the total amount the payment method will be charged. This value can't be negative. Please note that this field is not used on PayPal transactions.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "lineItems", + "description": "Line items for this transaction. Up to 249 line items may be specified.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionLineItemInput", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "orderId", + "description": "Additional information about the transaction. On PayPal transactions, this field maps to the PayPal invoice number. PayPal invoice numbers must be unique in your PayPal business account. Maximum 255 characters or 127 for PayPal transactions. If specified, this will update the existing order ID on the transaction.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "purchaseOrderNumber", + "description": "A purchase order identification value you associate with this transaction.\n\n*Required for Level 2 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "shipping", + "description": "Shipping information.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionShippingInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "tax", + "description": "Tax information about the transaction.\n\n*Required for Level 2 processing*.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionTaxInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "CardAccountType", + "description": "The type of account to be used when transacting with a combo card.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CREDIT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DEBIT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CardPresentOriginDetails", + "description": "Additional information about a card present payment method supplied by an in-store payment reader.", + "fields": [ + { + "name": "authorizationMode", + "description": "The authorization mode used to perform the transaction on the payment reader.", + "args": [], + "type": { + "kind": "ENUM", + "name": "InStoreReaderAuthorizationMode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pinVerified", + "description": "An indicator for whether the transaction was verified via pin.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputMode", + "description": "The input mode used on the payment reader to facilitate an in-store transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentReaderInputMode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminalId", + "description": "The ID of the terminal that was processed this transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "InStoreReaderOriginDetails", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "Challenge", + "description": "A list of challenges that are required by the current merchant to process a given credit card.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CVV", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "POSTAL_CODE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cvv", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postal_code", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ChargeCreditCardInput", + "description": "Top-level input fields for creating a transaction by charging a credit card.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of a credit card payment method to be charged.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "options", + "description": "Input fields for creating a credit card transaction.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CreditCardTransactionOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transaction", + "description": "Input fields for the charge, with details that will define the resulting transaction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ChargePayPalAccountInput", + "description": "Top-level input fields for creating a transaction by charging a PayPal account.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "The ID of an existing PayPal account.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "options", + "description": "Input fields related to the PayPal account being charged.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ChargePayPalAccountOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transaction", + "description": "Input fields for the charge, with details that will define the resulting transaction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ChargePayPalAccountOptionsInput", + "description": "Input fields for creating a transaction with a PayPal account.", + "fields": null, + "inputFields": [ + { + "name": "customField", + "description": "Variable passed directly to PayPal for your own tracking purposes. Customers do not see this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "description", + "description": "Description of the transaction that is displayed to customers in PayPal email receipts.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "payee", + "description": "Deprecated: This field is no longer supported.", + "type": { + "kind": "INPUT_OBJECT", + "name": "PayPalPayeeOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "selectedFinancingOption", + "description": "Buyer selected PayPal financing option.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SelectedPayPalFinancingOptionInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ChargePaymentMethodInput", + "description": "Top-level input fields for creating a transaction by charging a payment method.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of a payment method to be charged.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "transaction", + "description": "Input fields for the charge, with details that will define the resulting transaction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ChargeUsBankAccountInput", + "description": "Top-level input fields for creating a transaction by charging a US bank account.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "The ID of an existing US bank account.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "options", + "description": "Input fields related to the US bank account being charged.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ChargeUsBankAccountOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transaction", + "description": "Input fields for the charge, with details that will define the resulting transaction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ChargeUsBankAccountOptionsInput", + "description": "Input fields for creating a transaction with a US bank account.", + "fields": null, + "inputFields": [ + { + "name": "standardEntryClassCode", + "description": "A NACHA standard entry class (SEC) code, which designates how the transaction was authorized. Most internet-based sales should use the `WEB` code.", + "type": { + "kind": "ENUM", + "name": "ACHStandardEntryClassCode", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ChargeVenmoAccountInput", + "description": "Top-level input fields for creating a transaction by charging a Venmo account.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "The ID of an existing Venmo account.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "options", + "description": "Input fields for creating a Pay with Venmo transaction.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ChargeVenmoAccountOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transaction", + "description": "Input fields for the charge, with details that will define the resulting transaction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ChargeVenmoAccountOptionsInput", + "description": "Input fields for creating a Pay with Venmo transaction.", + "fields": null, + "inputFields": [ + { + "name": "profileId", + "description": "Specifies which Venmo business profile to use for the transaction.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ChargebackProtectionLevel", + "description": "The chargeback protection level indicates the transaction or dispute's protection status.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "EFFORTLESS", + "description": "The transaction or dispute is protected by the effortless chargeback protection product.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NOT_PROTECTED", + "description": "The merchant has not enrolled any chargeback protection products, or the merchant is registered, but the transaction or dispute is not protected.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "STANDARD", + "description": "The transaction or dispute is protected by the standard chargeback protection product.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ChildCapture", + "description": "A partial capture's relationship to its original authorization transaction.", + "fields": [ + { + "name": "parentAuthorization", + "description": "The original authorization whose funds have been partially captured.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ClientConfiguration", + "description": "Top-level fields returned from the client configuration query.", + "fields": [ + { + "name": "analyticsUrl", + "description": "URL to send analytics.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "This field is included for supporting SDKs that send analytics." + }, + { + "name": "applePay", + "description": "Configuration for Apple Pay on iOS.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ApplePayConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "applePayWeb", + "description": "Configuration for Apple Pay on the web.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ApplePayWebConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assetsUrl", + "description": "A URL pointing to the base path of Braintree's web pages used for various browser switches and popups.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientApiUrl", + "description": "A URL pointing to the base path of Braintree's client API.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "This field is included for supporting legacy clients." + }, + { + "name": "supportedFeatures", + "description": "A list of client features the merchant supports.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "ClientFeature", + "ofType": null + } + } + }, + "isDeprecated": true, + "deprecationReason": "This field is included for supporting legacy clients." + }, + { + "name": "braintreeApi", + "description": "Configuration for payment methods in legacy clients.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "BraintreeApiConfiguration", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "This field is included for supporting legacy clients." + }, + { + "name": "creditCard", + "description": "Configuration for credit card tokenization.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "CreditCardConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "environment", + "description": "The enum of the current environment.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ClientConfigurationEnvironment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fraudProvider", + "description": "Configuration for fraud protection provider.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "FraudProviderConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "googlePay", + "description": "Configuration for Google Pay on Android and the web.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "GooglePayConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ideal", + "description": "Deprecated, this field will always be null.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "IDealConfiguration", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "This field is included for supporting legacy clients." + }, + { + "name": "kount", + "description": "Deprecated, formerly configuration for Kount fraud tools, now this configuration lives under fraudProvider.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "KountConfiguration", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "This field is included for supporting legacy clients." + }, + { + "name": "masterpass", + "description": "Configuration for Masterpass.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MasterpassConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchantId", + "description": "The merchant ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paypal", + "description": "Configuration for PayPal.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PayPalConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "samsungPay", + "description": "Configuration for Samsung Pay.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "SamsungPayConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unionPay", + "description": "Configuration for UnionPay cards.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "UnionPayConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "usBankAccount", + "description": "Configuration for US bank account processing.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "UsBankAccountConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "venmo", + "description": "Configuration for Pay with Venmo.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "VenmoConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "visaCheckout", + "description": "Configuration for Visa Checkout.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "VisaCheckoutConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "challenges", + "description": "A list of challenges that are required by the current merchant to process a given credit card.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "Challenge", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ClientConfigurationEnvironment", + "description": "The client configuration environment being used.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "DEVELOPMENT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRODUCTION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "QA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SANDBOX", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TEST", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "development", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "production", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "qa", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sandbox", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "test", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ClientFeature", + "description": "A value used by Braintree client SDKs to determine what operations are supported through this GraphQL API.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "TOKENIZE_CREDIT_CARDS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tokenize_credit_cards", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ClientTokenInput", + "description": "Input fields for creating a client token.", + "fields": null, + "inputFields": [ + { + "name": "merchantAccountId", + "description": "The merchant account ID used to create the client token. Defaults to your default merchant account ID.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customerId", + "description": "The ID of an existing customer. Including this will allow your customer to vault and manage their payment methods.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ConfirmMicroTransferAmountsInput", + "description": "Top-level input field for confirming micro-transfer values.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "verificationId", + "description": "The ID of the verification from vaulting the bank account.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "amountsInCents", + "description": "The amounts, in cents, of two deposits made into the customer's bank account after initiating a MICRO_TRANSFERS verification. These values should be collected from your customer.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ConfirmMicroTransferAmountsPayload", + "description": "Top-level output field from confirming micro-transfer amounts on bank account.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "verification", + "description": "The verification that was run on the payment method prior to vaulting.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Verification", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of the micro-transfer amounts confirmation.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ConfirmMicroTransferAmountsStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ConfirmMicroTransferAmountsStatus", + "description": "The status of a micro-transfer amount confirmation.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "AMOUNTS_DO_NOT_MATCH", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CONFIRMED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TOO_MANY_ATTEMPTS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ConfirmationPromptAlignment", + "description": "The alignment of the confirmation prompt text when displayed on the in-store reader.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CENTER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LEFT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "CountryCode", + "description": "An [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country code. Braintree only accepts [specific alpha-2 values](https://developers.braintreepayments.com/reference/general/countries#list-of-countries). Clients using a Braintree version prior to 2021-02-01 should use an [ISO 3166-1 alpha-3](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) country code.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "CountryCodeAlpha2", + "description": "An [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country code. Braintree only accepts [specific alpha-2 values](https://developers.braintreepayments.com/reference/general/countries#list-of-countries).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateClientTokenInput", + "description": "Top-level input field for generating a client token.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "clientToken", + "description": "Input fields for creating a client token.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ClientTokenInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateClientTokenPayload", + "description": "Top-level fields returned when creating a client token.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientToken", + "description": "A Base64 encoded string used to initialize client SDKs.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateCustomerInput", + "description": "Top-level field for creating a customer.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customer", + "description": "Input fields for creating a customer.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CustomerInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateCustomerPayload", + "description": "Top-level fields returned when creating a customer.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customer", + "description": "Information about the customer that was created. Can be used when vaulting payment methods or creating transactions to associate those objects.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Customer", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateDisputeFileEvidenceInput", + "description": "Top-level input fields for adding file evidence to a dispute.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "disputeId", + "description": "The ID of the dispute to be accepted.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "category", + "description": "The category for the evidence file.", + "type": { + "kind": "ENUM", + "name": "DisputeFileEvidenceCategory", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateDisputeFileEvidencePayload", + "description": "Top-level field returned when creating file evidence for a dispute.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "evidence", + "description": "The evidence object created.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "DisputeFileEvidence", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dispute", + "description": "Information about the dispute the evidence is attached to.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Dispute", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateDisputeTextEvidenceInput", + "description": "Top-level input fields for creating text evidence for a dispute.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "disputeId", + "description": "The ID of the dispute to create the evidence for.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "category", + "description": "The category of the text evidence.", + "type": { + "kind": "ENUM", + "name": "DisputeTextEvidenceCategory", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "content", + "description": "The content of the text evidence.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateDisputeTextEvidencePayload", + "description": "Top-level field returned when creating text evidence for a dispute.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "evidence", + "description": "The evidence object created.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "DisputeTextEvidence", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateInStoreLocationInput", + "description": "Input fields for creating an in store location.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "location", + "description": "Input fields to create an in-store Location.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InStoreLocationInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateInStoreLocationPayload", + "description": "Top-level fields returned when creating an in-store location.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "location", + "description": "The in-store location.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreLocation", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateNonInstantLocalPaymentContextInput", + "description": "Top-level input fields for creating a non-instant local payment context.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentContext", + "description": "Input fields for creating a non-instant local payment context.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "NonInstantLocalPaymentContextInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateNonInstantLocalPaymentContextPayload", + "description": "The result of a request to make a local payment context.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentContext", + "description": "Details about the local payment context.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "LocalPaymentContext", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreatePayPalBillingAgreementInput", + "description": "Top-level input field for creating a PayPal Billing Agreement Token.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "Braintree merchant account ID associated with the PayPal account to be used for the Billing Agreement creation.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "returnUrl", + "description": "URL for redirect back to merchant app on the client indicating successful approval.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "cancelUrl", + "description": "URL for redirect back to merchant app on the client indicating unsuccessful approval.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "description", + "description": "Description of the PayPal Billing Agreement, displayed to the PayPal user on paypal.com and other PayPal user experiences.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "email", + "description": "Email of the payer (if known). This will prepopulate the input field in the PayPal approval page.", + "type": { + "kind": "SCALAR", + "name": "EmailAddress", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "offerPayPalCredit", + "description": "Indicates whether PayPal Credit should be offered in the PayPal approval flow.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paypalRiskCorrelationId", + "description": "PayPal Risk correlation ID (also known as the Client Metadata ID).", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name" : "paypalExperienceProfile", + "description" : "Defines the experience profile used to render the billing agreement approval flow.", + "type" : { + "kind" : "INPUT_OBJECT", + "name" : "PayPalBillingAgreementExperienceProfileInput", + "ofType" : null + }, + "defaultValue" : null + }, + { + "name" : "shippingAddress", + "description" : "Merchant-provided shipping address. Fields addressLine1, adminArea2, and countryCode are required for Billing Agreements.", + "type" : { + "kind" : "INPUT_OBJECT", + "name" : "AddressInput", + "ofType" : null + }, + "defaultValue" : null + }, + { + "name" : "paypalProductAttributes", + "description" : "Product attributes input for PayPal billing agreement.", + "type" : { + "kind" : "INPUT_OBJECT", + "name" : "PayPalProductAttributesInput", + "ofType" : null + }, + "defaultValue" : null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreatePayPalBillingAgreementPayload", + "description": "Top-level fields returned from setting up a PayPal Billing Agreement Token.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "billingAgreementToken", + "description": "The Billing Agreement token.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "approvalUrl", + "description": "The URL for getting user approval of the PayPal Billing Agreement.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreatePayPalOneTimePaymentInput", + "description": "Top-level input field for creating a PayPal One-Time Payment.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "Braintree merchant account ID associated with the PayPal account to be used for the One-Time payment creation.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "Total amount for payment to be charged to consumer.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "MonetaryAmountInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "cancelUrl", + "description": "URL for redirect back to merchant app on the client indicating unsuccessful approval.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "email", + "description": "Email of the payer. This will prepopulate the input field in the PayPal approval login page.", + "type": { + "kind": "SCALAR", + "name": "EmailAddress", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "intent", + "description": "The payment intent.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PayPalIntent", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "lineItems", + "description": "The line items for this transaction. Maximum 249 line items.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PayPalLineItemInput", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "offerPayLater", + "description": "Indicates whether PayPal Pay Later should be offered in the PayPal approval flow.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paypalRiskCorrelationId", + "description": "PayPal Risk correlation ID (also known as the Client Metadata ID).", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paypalExperienceProfile", + "description": "Defines the experience profile used to render the approval flow.", + "type": { + "kind": "INPUT_OBJECT", + "name": "PayPalExperienceProfileInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "requestBillingAgreement", + "description": "Indicates whether this payment uses the [Billing Agreement with Purchase flow](https://developers.braintreepayments.com/guides/paypal/checkout-with-paypal/javascript/v3#checkout-using-paypal-billing-agreement-with-purchase-flow). This will request Billing Agreement approval from the customer, and a multi-use PayPal payment method will be created alongside the transaction.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "billingAgreementDescription", + "description": "A description of the Billing Agreement being requested. This is displayed to the customer on paypal.com when `requestBillingAgreement` is true. Maximum 127 characters.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "returnUrl", + "description": "URL for redirect back to merchant app on the client indicating successful approval.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "shippingAddress", + "description": "Merchant-provided shipping address. If passing a shipping address, fields addressLine1, adminArea2, and countryCode are required.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AddressInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "shippingOptions", + "description": "List of shipping options offered by the payee or merchant to the payer to ship or pick up their items. **Note:** `shippingOptions` may not be passed with intent `ORDER` payments.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PayPalShippingOptionInput", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreatePayPalOneTimePaymentPayload", + "description": "Top-level fields returned from setting up a PayPal One-Time Payment.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "approvalUrl", + "description": "The URL for getting user approval of the PayPal payment.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentId", + "description": "The PayPal payment ID. This ID is prefixed with \"PAYID-\".", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateUniversalAccessTokenInput", + "description": "Top-level input field for generating a PayPal access token.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customerId", + "description": "The ID of an existing customer. Including this will allow the access token to interact with this customer's data.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "type", + "description": "Authentication context class reference for the universal access token.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "ACRType", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateUniversalAccessTokenPayload", + "description": "Top-level fields returned when creating a universal access token.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "accessToken", + "description": "The created universal access token.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "AccessToken", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "description": "A code identifying the card brand.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "AMERICAN_EXPRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CITI", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DINERS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DISCOVER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ELO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "HIPER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "HIPERCARD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERNATIONAL_MAESTRO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "JCB", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MASTERCARD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SOLO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SWITCH", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UK_MAESTRO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION_PAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNKNOWN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VISA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "american_express", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "citi", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "diners", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discover", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "elo", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hiper", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hipercard", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "international_maestro", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "jcb", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mastercard", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "solo", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "switch", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uk_maestro", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "union_pay", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unknown", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "visa", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreditCardConfiguration", + "description": "Configuration for credit card tokenization.", + "fields": [ + { + "name": "supportedCardBrands", + "description": "A list of card brands supported by the merchant for credit card processing.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "challenges", + "description": "A list of challenges that are required by the merchant to process a given credit card.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "Challenge", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "threeDSecureEnabled", + "description": "Whether or not the merchant supports 3D Secure.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `threeDSecure` instead." + }, + { + "name": "threeDSecure", + "description": "Configuration for 3D Secure.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ThreeDSecureConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fraudDataCollectionEnabled", + "description": "Whether or not fraud data collection is enabled for the merchant.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreditCardDetails", + "description": "Details about a credit card.", + "fields": [ + { + "name": "brandCode", + "description": "A static code identifying the card brand.", + "args": [], + "type": { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last4", + "description": "The last four digits of the card number.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bin", + "description": "The first 6 digits of the credit card number, known as the Bank Identification Number. If this card originates from a third party such as a wallet provider, this BIN may not be present and the PaymentMethodOriginDetails will contain a BIN instead.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "binData", + "description": "Information about the card based on its BIN.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "BinRecord", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expirationMonth", + "description": "The month of the expiration date, formatted MM.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expirationYear", + "description": "The year of the expiration date, formatted YYYY.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cardholderName", + "description": "The cardholder's name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uniqueNumberIdentifier", + "description": "An identifier that uniquely represents any credit card number, for cards stored in a merchant's vault. If the same credit card is added to a merchant's vault multiple times, each will have the same identifier. This identifier will only be returned if the field \"origin\" is null.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "origin", + "description": "Additional information if the credit card was provided from a third-party origin, such as Apple Pay, Google Pay, or another digital wallet.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethodOrigin", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "billingAddress", + "description": "The billing address associated with the credit card.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Address", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "threeDSecure", + "description": "3D Secure information for the payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ThreeDSecureDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "imageUrl", + "description": "A URL to an image logo representing the card brand.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "This field is included for supporting legacy clients." + }, + { + "name": "brand", + "description": "The display name of the card brand, e.g. \"Visa\" or \"American Express\".", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `brandCode` instead." + }, + { + "name": "cardOnFileNetworkTokenized", + "description": "Indicates whether the card on file is network tokenized.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreditCardFraudToolsOptionsInput", + "description": "Input fields that allow you to skip certain fraud checks. These will override Control Panel settings.", + "fields": null, + "inputFields": [ + { + "name": "skipCvv", + "description": "Skip CVV checks. Will result in a `cvvResponse` of `BYPASS` in the response from the processor.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "skipAvs", + "description": "Skip AVS checks. Will result in an `avsPostalCodeResponse` of `BYPASS` in the response from the processor.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "skipAdvancedFraudChecking", + "description": "Skip [advanced fraud checks](https://developers.braintreepayments.com/guides/advanced-fraud-management-tools/overview).", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreditCardInput", + "description": "Input fields for a credit card.", + "fields": null, + "inputFields": [ + { + "name": "number", + "description": "The 12-to-19-digit value that uniquely identifies this credit card, also known as the primary account number or PAN.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "expirationYear", + "description": "The two- or four-digit year associated with a credit card, formatted `YYYY` or `YY`.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "expirationMonth", + "description": "The expiration month of a credit card, formatted `MM`.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "cvv", + "description": "A three- or four-digit card verification value assigned to credit cards. The CVV will never be stored, but it can be provided with one-time requests to verify the card.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "cardholderName", + "description": "When supplied, the cardholder name that will be tokenized with the contents of the fields.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "billingAddress", + "description": "The billing address for the credit card.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AddressInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "CreditCardLast4", + "description": "A four-digit string.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "CreditCardNumber", + "description": "A number that passes Luhn validation.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreditCardTransactionDetails", + "description": "Credit card specific details on a transaction or verification.", + "fields": [ + { + "name": "creditCard", + "description": "The details of the credit card itself.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "CreditCardDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "networkTransactionId", + "description": "The network transaction identifier provided by the payment network. If this transaction was created in order to verify a payment method before storing it in an external vault, then this value can be pased when creating subsequent transactions with the same payment method.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "accountType", + "description": "For combo cards, what account type was used for this specific transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "CardAccountType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "acquirerReferenceNumber", + "description": "Reference value assigned to a card transaction once it has been processed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processedWithCardOnFileNetworkToken", + "description": "Indicates whether the transaction was processed with a card on file network token.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "accountBalance", + "description": "The remaining balance in the account after this transaction. This field is only returned for payment methods such as prepaid cards.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreditCardTransactionOptionsInput", + "description": "Input fields for creating a transaction by authorizing or charging a credit card.", + "fields": null, + "inputFields": [ + { + "name": "externalVault", + "description": "Details about this transaction if it's being created from a credit card that is or will be stored in an non-Braintree vault.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionExternalVaultOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "billingAddress", + "description": "A billing address to use for the transaction. If a billing address was provided when tokenizing or is present on the vaulted credit card, it will be *merged* with this input value, with priority given to this input value.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AddressInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "accountType", + "description": "The type of account to be used when transacting with a combo card.", + "type": { + "kind": "ENUM", + "name": "CardAccountType", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "tokenizedCvv", + "description": "The CVV for the credit card to be used when creating this transction, securely tokenized with the `tokenizeCvv` mutation.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "fraudTools", + "description": "Control which fraud tools will be applied to this transaction. Fraud tools cannot be retroactively applied to a transaction if skipped.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CreditCardFraudToolsOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "threeDSecureAuthentication", + "description": "3D Secure authentication information performed for this transaction. Only use these fields if you are charging or authorizing a single-use payment method ID that was *not* generated by a 3DS flow on on the client.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecureAuthenticationInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "scaExemption", + "description": "The type of Strong Customer Authentication Exemption requested.", + "type": { + "kind": "ENUM", + "name": "ScaExemptionType", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "installmentCount", + "description": "Number of monthly installments (can be anywhere between 2 and 12).", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreditCardVerificationDetails", + "description": "Information specific to verifications of credit card payment methods.", + "fields": [ + { + "name": "amount", + "description": "The amount used when performing the verification. May be 0.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreditCardVerificationOptionsInput", + "description": "Input fields that specify options for verifying the credit card.", + "fields": null, + "inputFields": [ + { + "name": "merchantAccountId", + "description": "Deprecated: Please use `merchantAccountId` in the base input instead.\n\nID of the merchant account to use when verifying the credit card.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "accountType", + "description": "The type of account to be used when verifying a combo card.", + "type": { + "kind": "ENUM", + "name": "CardAccountType", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "riskData", + "description": "Customer device information, which is sent directly to supported processors for fraud analysis.", + "type": { + "kind": "INPUT_OBJECT", + "name": "RiskDataInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "fraudTools", + "description": "Control which fraud tools will be applied to this verification. Fraud tools cannot be retroactively applied to a verification if skipped.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CreditCardFraudToolsOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "tokenizedCvv", + "description": "The CVV for the credit card to be used when verifying the credit card, securely tokenized with the `tokenizeCvv` mutation.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "The amount to use to verify the credit card.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "skip", + "description": "Whether to opt out of verifying the credit card. Defaults to `false`. Clients should only pass `true` in the uncommon scenario that the credit card has been verified externally to Braintree.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "CurrencyCodeAlpha", + "description": "An [ISO 4217 alpha](https://en.wikipedia.org/wiki/ISO_4217) currency code. Braintree only accepts [specific alpha values](https://developers.braintreepayments.com/reference/general/currencies).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CustomActionsPaymentContext", + "description": "Top-level fields returned from a Custom Actions payment context.", + "fields": [ + { + "name": "id", + "description": "The identifier of the payment context.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date and time when the payment context was created.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": "Date and time when the payment context was updated.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customFields", + "description": "A list of fields stored on a PaymentContext during execution of a Custom Actions handler (Five (5) entries maximum).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CustomActionsPaymentContextField", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "PaymentContext", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CustomActionsPaymentContextField", + "description": "Fields returned by the createPaymentContext custom actions event handler.", + "fields": [ + { + "name": "name", + "description": "An alphanumeric string used as a key to lookup a CustomField value (255 characters maximum).", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "value", + "description": "An alphanumeric string used to store a CustomField value (7168 characters maximum).", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CustomActionsPaymentContextFieldInput", + "description": "Fields that are provided when creating the payment context.", + "fields": null, + "inputFields": [ + { + "name": "name", + "description": "An alphanumeric string used as a key to lookup a CustomField value (255 characters maximum).", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "value", + "description": "An alphanumeric string used to store a CustomField value (7168 characters maximum).", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CustomActionsPaymentMethodDetails", + "description": "Details about a custom actions payment method.", + "fields": [ + { + "name": "actionName", + "description": "The action to be invoked when using the payment method.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": "Fields that your action requires.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CustomActionsPaymentMethodField", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CustomActionsPaymentMethodField", + "description": "Fields that are provided during tokenization and are presented to the invoked action to be consumed.", + "fields": [ + { + "name": "name", + "description": "The name of this field, e.g. \"accountNumber\".", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "displayValue", + "description": "The value displayed in the Control Panel or API, e.g. \"*****6789\".", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CustomActionsPaymentMethodFieldInput", + "description": "Fields that are provided during tokenization and are presented to the invoked action to be consumed.", + "fields": null, + "inputFields": [ + { + "name": "name", + "description": "The name of this field. e.g. \"accountNumber\".", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "value", + "description": "The value of this field. e.g. \"123456789\".", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "displayValue", + "description": "The value displayed in the Control Panel or API. e.g. \"*****6789\".", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CustomActionsPaymentMethodInput", + "description": "Input fields for a Custom Actions payment method.", + "fields": null, + "inputFields": [ + { + "name": "actionName", + "description": "The action you wish to invoke when using the tokenized payment method.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "fields", + "description": "Fields that your action requires.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CustomActionsPaymentMethodFieldInput", + "ofType": null + } + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CustomField", + "description": "A merchant-defined custom field to store additional information.", + "fields": [ + { + "name": "name", + "description": "The name of the custom field.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "value", + "description": "The value of the custom field.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CustomFieldInput", + "description": "Custom field name/value pairs. Maximum 255 characters. You must [set up each custom field in the Control Panel](https://articles.braintreepayments.com/control-panel/custom-fields#creating-a-custom-field) prior to passing it with a request.", + "fields": null, + "inputFields": [ + { + "name": "name", + "description": "Name of the custom field as defined in the Control Panel.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CustomFieldName", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "value", + "description": "Value for the named custom field. A null value will ignore (on create) or remove (on update) the custom field.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "CustomFieldName", + "description": "A string representing a custom field value. Contains letters, numbers, and underscores.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Customer", + "description": "Information about a customer and their associated payment methods and transactions.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legacyId", + "description": "Legacy unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "company", + "description": "Company or business name associated with this customer.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date and time at which the customer was created.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customFields", + "description": "Collection of custom field/value pairs. Custom fields are [defined in the Control Panel](https://articles.braintreepayments.com/control-panel/custom-fields#store-and-pass-back-fields).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CustomField", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultPaymentMethod", + "description": "Customer's default payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": "Email address for this customer.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "firstName", + "description": "Customer's first name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastName", + "description": "Customer's last name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "phoneNumber", + "description": "The phone number for this customer.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethods", + "description": "Payment methods belonging to this customer.", + "args": [ + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PaymentMethodConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "transactions", + "description": "Transactions associated with this customer. This includes transactions created by charging a vaulted payment method that belongs or belonged to the customer, or by passing a customer ID when charging a single-use payment method.", + "args": [ + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TransactionConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "CustomerAuthenticationIndicator", + "description": "A value indicating when to perform further customer authentication.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "OPTIONAL", + "description": "Indicates further authentication is optional.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "REQUIRED", + "description": "Indicates further authentication should be performed.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNAVAILABLE", + "description": "Customer authentication indicator information is unavailable at this time.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "CustomerAuthenticationRegulationEnvironment", + "description": "The customer authentication regulation environment that applies to the transaction, such as [PSD2](https://www.braintreepayments.com/blog/understanding-and-preparing-for-psd2-strong-customer-authentication/).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "PSDTWO", + "description": "EU Regulation [PSD2 Strong Customer Authentication](https://www.braintreepayments.com/blog/understanding-and-preparing-for-psd2-strong-customer-authentication/) applies to this transaction.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RBI", + "description": "Reserve Bank of India regulations apply to this transactions.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNAVAILABLE", + "description": "Customer authentication regulation environment information is unavailable for this transaction at this time.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNREGULATED", + "description": "No customer authentication regulations apply to this transaction.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CustomerConnection", + "description": "A paginated list of customers.", + "fields": [ + { + "name": "edges", + "description": "A list of customers.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CustomerConnectionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information about the page of customers contained in `edges`.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CustomerConnectionEdge", + "description": "A customer within a CustomerConnection.", + "fields": [ + { + "name": "cursor", + "description": "This customer's location within the CustomerConnection. Used for requesting additional pages.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The customer.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Customer", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CustomerInput", + "description": "Input fields for creating or updating a customer. On update, omitted fields will not be updated. Passing a null value will assign null to that field.", + "fields": null, + "inputFields": [ + { + "name": "company", + "description": "Company or business name associated with the customer.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customFields", + "description": "Collection of custom field/value pairs. You must [set up each custom field in the Control Panel](https://articles.braintreepayments.com/control-panel/custom-fields#creating-a-custom-field) prior to passing it with a request.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CustomFieldInput", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "email", + "description": "Email address for the customer.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "firstName", + "description": "Customer's first name.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "lastName", + "description": "Customer's last name.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "phoneNumber", + "description": "The customer's phone number.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "taxIdentifiers", + "description": "A set of country code ID pairs, analogous to Social Security numbers in the United States.\n\nA customer may have multiple tax identifiers, but only one per tax jurisdiction. The values provided for an update will be stored and previous entries will be updated.\n\n**Note:** You will only need to use these fields for processing in certain countries.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CustomerTaxIdentifierInput", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CustomerSearchInput", + "description": "Input fields for searching for customers.", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "Find customers with an id or ids.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "company", + "description": "Find customers with a given company or business name.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "createdAt", + "description": "Find customers with a given created at time.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTimestampInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "email", + "description": "Find customers with a given email address.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "firstName", + "description": "Find customers with a given first name.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "lastName", + "description": "Find customers with a given last name.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "phoneNumber", + "description": "Find customers with a given phone number.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CustomerTaxIdentifierInput", + "description": "The customer's tax identifer for a given tax jurisdiction.", + "fields": null, + "inputFields": [ + { + "name": "identifier", + "description": "The identifier provided in the format required for the given tax jurisdiction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "countryCode", + "description": "The country code of the tax jurisdiction for this tax identifier.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CountryCode", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Date", + "description": "A date in the format YYYY-MM-DD.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteCustomerInput", + "description": "Top-level input fields for deleting a customer.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customerId", + "description": "The ID of the customer to be deleted.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DeleteCustomerPayload", + "description": "Top-level output field from deleting a customer.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DeleteDisputeEvidenceInput", + "description": "Input fields for deleting dispute evidence.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "evidenceId", + "description": "The ID of the evidence to be deleted.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "disputeId", + "description": "The ID of the dispute that the evidence belongs to.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DeleteDisputeEvidencePayload", + "description": "Top-level field returned when deleting evidence from a dispute.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DeletePaymentMethodFromSingleUseTokenInput", + "description": "Top-level input fields for deleting a payment method referenced by a single-use token.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "singleUseTokenId", + "description": "A single-use token ID referencing a payment method.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DeletePaymentMethodFromSingleUseTokenPayload", + "description": "Top-level output field from deleting a payment method referenced by a single-use token.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DeletePaymentMethodFromVaultInput", + "description": "Top-level input fields for deleting a multi-use payment method from the vault.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "The ID of the multi-use payment method to be deleted.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "initiatedBy", + "description": "Indicates whether this deletion was initiated by the merchant or the customer (via the merchant site/app).", + "type": { + "kind": "ENUM", + "name": "PaymentMethodDeletionInitiator", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "deleteRelatedPaymentMethods", + "description": "Additionally request deletion of all related payment methods (ones that store the same underlying payment instrument as the one specified by `paymentMethodId`) across all customers for current merchant.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "fraudRelated", + "description": "Indicates if this deletion is related to suspected fraud, as determined by the merchant.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DeletePaymentMethodFromVaultPayload", + "description": "Top-level output field from deleting a multi-use payment method.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DetachedRefundInput", + "description": "Specific input fields for describing a detached refund.", + "fields": null, + "inputFields": [ + { + "name": "amount", + "description": "The amount to refund.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "orderId", + "description": "The refund's order ID.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "ID of the merchant account that will be used when performing the refund.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customFields", + "description": "Collection of custom field/value pairs. You must [set up each custom field in the Control Panel](https://articles.braintreepayments.com/control-panel/custom-fields#creating-a-custom-field) prior to passing it with a request.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CustomFieldInput", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "descriptor", + "description": "Fields used to define what will appear on a customer's statement (for instance, credit card or bank statement) for this refund. This should match the original transaction if possible.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionDescriptorInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DisbursementBankAccount", + "description": "Details about the disbursement bank account.", + "fields": [ + { + "name": "last4", + "description": "The last four digits of the bank account number.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "routingNumber", + "description": "The routing number of the bank.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DisbursementDetails", + "description": "Disbursement details contain information about how and when the transaction was disbursed, including timing and currency information. This field is only available if you have an eligible merchant account.", + "fields": [ + { + "name": "date", + "description": "The date that the funds associated with this transaction were disbursed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "Amount of money disbursed in the settlement currency, which may be different than the transaction's [presentment currency](https://articles.braintreepayments.com/get-started/currencies).", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "exchangeRate", + "description": "The exchange rate from the presentment currency to the settlement currency. If the currencies are the same, this will be 1.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fundsHeld", + "description": "Indicates whether funds have been withheld from a disbursement to the merchant's account.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "DisplayItemType", + "description": "The display item type to be displayed on the in-store reader.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CHARGE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DISCOUNT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LINE_BREAK", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TEXT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Dispute", + "description": "[A case raised by a customer to either request information about or to challenge a charge](https://articles.braintreepayments.com/risk-and-security/chargebacks-retrievals/overview). These are initiated via a customer's payment provider, such as their bank, and require a merchant to provide evidence or further information.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legacyId", + "description": "Legacy unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amountDisputed", + "description": "The amount of money from the original charge that the customer is disputing. Can be 0. This amount is debited from a merchant's account and held in a third-party account until the dispute is resolved, at which time it is sent to either the merchant or customer.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amountWon", + "description": "If an amount was disputed, the amount of money awarded back to the merchant if the dispute was reversed.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "caseNumber", + "description": "The case number for the dispute.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date and time at which the dispute was created.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "receivedDate", + "description": "Date the dispute was received by the merchant.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "referenceNumber", + "description": "The transaction reference number for the dispute.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "responseDeadline", + "description": "The deadline for the merchant to submit a response to the dispute.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "replyByDate", + "description": "The reply by date for the merchant to submit a response to the dispute.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": "The type of dispute.", + "args": [], + "type": { + "kind": "ENUM", + "name": "DisputeType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "evidence", + "description": "Evidence records submitted by the merchant for the dispute.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INTERFACE", + "name": "DisputeEvidence", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "originalDispute", + "description": "If this dispute is a follow-up to a previous chargeback or retrieval, the original dispute.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Dispute", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processorResponse", + "description": "Additional information from the payment processor.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "DisputeProcessorResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of the dispute.", + "args": [], + "type": { + "kind": "ENUM", + "name": "DisputeStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "statusHistory", + "description": "A log of history events containing status changes by date for this dispute.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DisputeStatusEvent", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "transaction", + "description": "The disputed transaction which the customer is either requesting further information on or challenging.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "chargebackProtectionLevel", + "description": "The chargeback protection status of the dispute.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ChargebackProtectionLevel", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `protectionLevel` instead." + }, + { + "name" : "protectionLevel", + "description" : "The protection level of the dispute.", + "args" : [], + "type" : { + "kind" : "ENUM", + "name" : "DisputeProtectionLevel", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, + { + "name" : "preDisputeProgram", + "description" : "The pre-dispute program of the dispute.", + "args" : [], + "type" : { + "kind" : "ENUM", + "name" : "PreDisputeProgram", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DisputeConnection", + "description": "A paginated list of disputes.", + "fields": [ + { + "name": "edges", + "description": "A list of disputes.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DisputeConnectionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information about the page of disputes contained in `edges`.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DisputeConnectionEdge", + "description": "A dispute within a DisputeConnection.", + "fields": [ + { + "name": "cursor", + "description": "This dispute's location within the DisputeConnection. Used for requesting additional pages.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The dispute.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Dispute", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "DisputeEvidence", + "description": "Evidence provided by a merchant to respond to a dispute.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legacyId", + "description": "Legacy unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date and time when the evidence was created with Braintree.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sentToProcessorAt", + "description": "Date and time when the evidence was sent to the processor.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "category", + "description": "The evidence category.", + "args": [], + "type": { + "kind": "ENUM", + "name": "DisputeEvidenceCategory", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "DisputeFileEvidence", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "DisputeTextEvidence", + "ofType": null + } + ] + }, + { + "kind": "ENUM", + "name": "DisputeEvidenceCategory", + "description": "The evidence category that specifies which requirement it satisfies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "AVS_RESPONSE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CARRIER_NAME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CARRIER_NAME_OTHER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_ISSUED_AMOUNT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_ISSUED_DATE_TIME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DEVICE_ID", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DEVICE_NAME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DOWNLOAD_DATE_TIME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "EVIDENCE_TYPE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GENERAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GEOGRAPHICAL_LOCATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LEGIT_PAYMENTS_FOR_SAME_MERCHANDISE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MERCHANT_WEBSITE_OR_APP_ACCESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_DIGITAL_GOODS_TRANSACTION_ARN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_DIGITAL_GOODS_TRANSACTION_DATE_TIME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_DIGITAL_GOODS_TRANSACTION_ID", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_ARN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_DATE_TIME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_EMAIL_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_ID", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_IP_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_PHONE_NUMBER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_PHYSICAL_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROFILE_SETUP_OR_APP_ACCESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROOF_OF_3D_SECURE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROOF_OF_AUTHORIZED_SIGNER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROOF_OF_DELIVERY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROOF_OF_DELIVERY_EMP_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROOF_OF_POSSESSION_OR_USAGE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PURCHASER_EMAIL_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PURCHASER_IP_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PURCHASER_NAME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RECURRING_TRANSACTION_ARN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RECURRING_TRANSACTION_DATE_TIME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RECURRING_TRANSACTION_ID", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "REFUND_ID", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SIGNED_DELIVERY_FORM", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SIGNED_ORDER_FORM", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TICKET_PROOF", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TRACKING_NUMBER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TRACKING_URL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DisputeFileEvidence", + "description": "Images, files, or other evidence supporting a dispute case.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legacyId", + "description": "Legacy unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date and time at which the evidence was created with Braintree.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sentToProcessorAt", + "description": "Date and time at which the evidence was sent to the processor.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": "A URL where you can retrieve the dispute evidence.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "category", + "description": "The evidence category.", + "args": [], + "type": { + "kind": "ENUM", + "name": "DisputeEvidenceCategory", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "DisputeEvidence", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "DisputeFileEvidenceCategory", + "description": "For file evidence: the evidence category that specifies which requirement it satisfies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "GENERAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LEGIT_PAYMENTS_FOR_SAME_MERCHANDISE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MERCHANT_WEBSITE_OR_APP_ACCESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROFILE_SETUP_OR_APP_ACCESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROOF_OF_3D_SECURE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROOF_OF_AUTHORIZED_SIGNER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROOF_OF_DELIVERY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROOF_OF_DELIVERY_EMP_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROOF_OF_POSSESSION_OR_USAGE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SIGNED_DELIVERY_FORM", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SIGNED_ORDER_FORM", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TICKET_PROOF", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DisputeProcessorResponse", + "description": "Information about the dispute provided by the processor.", + "fields": [ + { + "name": "processorComments", + "description": "Additional comments forwarded by the processor.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reason", + "description": "The reason the dispute was created.", + "args": [], + "type": { + "kind": "ENUM", + "name": "DisputeReason", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reasonCode", + "description": "The reason code provided by the processor.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reasonDescription", + "description": "The reason code description based on the `reasonCode`.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "receivedDate", + "description": "Date the dispute was received by the merchant.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "referenceNumber", + "description": "The string value representing the reference number provided by the processor (if any).", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "DisputeProtectionLevel", + "description": "The Protection level indicates if dispute is eligible for protection through any feature enabled on your account.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CHARGEBACK_PROTECTION_TOOL", + "description": "The dispute is protected by the standard chargeback protection product.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "EFFORTLESS_CHARGEBACK_PROTECTION_TOOL", + "description": "The dispute is protected by the effortless chargeback protection product.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NO_PROTECTION", + "description": "The merchant has not enrolled in any chargeback protection products, or the merchant is enrolled, but the dispute is not protected.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "DisputeReason", + "description": "The reason a customer opened a chargeback, pre-arbitration, or retrieval.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CANCELLED_RECURRING_TRANSACTION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_NOT_PROCESSED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DUPLICATE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAUD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GENERAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INVALID_ACCOUNT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NOT_RECOGNIZED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRODUCT_NOT_RECEIVED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRODUCT_UNSATISFACTORY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RETRIEVAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TRANSACTION_AMOUNT_DIFFERS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DisputeSearchInput", + "description": "Input fields for searching for Disputes.", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "Find disputes with an id or ids.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "status", + "description": "Find disputes with a given status.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchDisputeStatusInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "type", + "description": "Find disputes with a given type.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchDisputeTypeInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "reason", + "description": "Find disputes with a given reason description.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchDisputeReasonInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "caseNumber", + "description": "Find disputes with a given processor's caseNumber.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "referenceNumber", + "description": "Find disputes with a given transaction referenceNumber.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "amountDisputed", + "description": "Find disputes for a given amount or currency.", + "type": { + "kind": "INPUT_OBJECT", + "name": "MonetaryAmountSearchInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "amountWon", + "description": "Find disputes by the amount won.", + "type": { + "kind": "INPUT_OBJECT", + "name": "MonetaryAmountSearchInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "receivedDate", + "description": "Find disputes by the date received.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchDateInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "replyByDate", + "description": "Find disputes by the reply by date.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchDateInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "effectiveDate", + "description": "Find disputes by the date a status change history event took effect.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchDateInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transaction", + "description": "Find disputes based on a set of transaction criteria.", + "type": { + "kind": "INPUT_OBJECT", + "name": "DisputeTransactionSearchInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "chargebackProtectionLevel", + "description": "Deprecated: Please use `protectionLevel` instead.\n\nFind disputes with a given computed chargeback protection level.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchChargebackProtectionLevelInput", + "ofType": null + }, + "defaultValue" : null + }, + { + "name" : "protectionLevel", + "description" : "Find disputes with a given protection level.", + "type" : { + "kind" : "INPUT_OBJECT", + "name" : "SearchDisputeProtectionLevelInput", + "ofType" : null + }, + "defaultValue" : null + }, + { + "name" : "preDisputeProgram", + "description" : "Find disputes with a given pre-dispute program.", + "type" : { + "kind" : "INPUT_OBJECT", + "name" : "SearchPreDisputeProgramInput", + "ofType" : null + }, + "defaultValue" : null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "DisputeStatus", + "description": "The status of the dispute.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ACCEPTED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DISPUTED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "EXPIRED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LOST", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OPEN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "WON", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DisputeStatusEvent", + "description": "A record of a status the dispute has passed through.", + "fields": [ + { + "name": "disbursementDate", + "description": "The date any funds associated with this event were disbursed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of the dispute.", + "args": [], + "type": { + "kind": "ENUM", + "name": "DisputeStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the status event occurred.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "effectiveDate", + "description": "The date the status event took effect.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DisputeTextEvidence", + "description": "Text evidence supporting a dispute case.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legacyId", + "description": "Legacy unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date and time at which the evidence was created with Braintree.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sentToProcessorAt", + "description": "Date and time at which the evidence was sent to the processor.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "comment", + "description": "The body for text evidence.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `content` for name instead." + }, + { + "name": "content", + "description": "The body for text evidence.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "category", + "description": "The evidence category.", + "args": [], + "type": { + "kind": "ENUM", + "name": "DisputeEvidenceCategory", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "DisputeEvidence", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "DisputeTextEvidenceCategory", + "description": "For text evidence: the evidence category that specifies which requirement it satisfies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "AVS_RESPONSE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_ISSUED_AMOUNT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_ISSUED_DATE_TIME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DEVICE_ID", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DEVICE_NAME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DOWNLOAD_DATE_TIME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GEOGRAPHICAL_LOCATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_DIGITAL_GOODS_TRANSACTION_ARN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_DIGITAL_GOODS_TRANSACTION_DATE_TIME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_DIGITAL_GOODS_TRANSACTION_ID", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_ARN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_DATE_TIME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_EMAIL_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_ID", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_IP_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_PHONE_NUMBER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIOR_NON_DISPUTED_TRANSACTION_PHYSICAL_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PURCHASER_EMAIL_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PURCHASER_IP_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PURCHASER_NAME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RECURRING_TRANSACTION_ARN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RECURRING_TRANSACTION_DATE_TIME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RECURRING_TRANSACTION_ID", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DisputeTransactionSearchInput", + "description": "Transaction input fields for searching for disputes.", + "fields": null, + "inputFields": [ + { + "name": "transactionId", + "description": "Find disputes for a transaction id or ids.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customerId", + "description": "Find disputes for a customer id or ids.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transactionSource", + "description": "Find disputes with a given transaction source.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTransactionSourceInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodSnapshotType", + "description": "Find disputes on transactions charging payment methods of the given type.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentMethodSnapshotTypeInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "facilitatorOAuthApplicationClientId", + "description": "Find disputes on transactions created by a third party via the Grant API using a given OAuth application client ID.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "disbursementDate", + "description": "Find disputes by the transaction's disbursement date.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchDateInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "Find disputes on transactions associated with a merchant account ID or IDs.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "DisputeType", + "description": "Type of dispute.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CHARGEBACK", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRE_ARBITRATION", + "description": "A [second challenge to a charge](https://articles.braintreepayments.com/risk-and-security/chargebacks-retrievals/overview#pre-arbitrations), in the case that you have won an initial chargeback.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RETRIEVAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Duration", + "description": "An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) Duration that accepts Days, Hours, Minutes and Seconds.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ECommerceIndicator", + "description": "A card brand-specific two-digit string describing the mode of the transaction.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "EmailAddress", + "description": "The internationalized email address.

    Note: Up to 64 characters are allowed before and 255 characters are allowed after the @ sign.\nHowever, the generally accepted maximum length for an email address is 254 characters.\nThe pattern verifies that an unquoted @ sign exists.
    \n\nminLength: 3\nmaxLength: 254\npattern: ^.+@[^\\\"\\\\-].+$.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "EmvCardOriginDetails", + "description": "Additional information about an integrated circuit card (ICC) payment method supplied by an in-store payment reader.", + "fields": [ + { + "name": "authorizationMode", + "description": "The authorization mode used to perform the transaction on the payment reader.", + "args": [], + "type": { + "kind": "ENUM", + "name": "InStoreReaderAuthorizationMode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pinVerified", + "description": "An indicator for whether the transaction was verified via pin.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputMode", + "description": "The input mode used on the payment reader to facilitate an in-store transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentReaderInputMode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminalId", + "description": "The ID of the terminal that was processed this transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "applicationPreferredName", + "description": "The preferred name associated with the application used to process an EMV transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "applicationIdentifier", + "description": "The identifier specifying which EMV application was used to process the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminalVerificationResult", + "description": "A status code representing the result of a series of validations performed against an EMV enabled credit card.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cardSequenceNumber", + "description": "A unique identifier for credit cards that share the same PAN.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "applicationInterchangeProfile", + "description": "An indicator of the credit card's capabilities within the processing application.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminalTransactionDate", + "description": "The local date that the transaction requested authorization from the payment reader, formatted YYMMDD.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminalTransactionType", + "description": "An indicator of the type of transaction specified during authorization processing.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cashbackAmount", + "description": "An additional amount associated with the transaction that represents the cashback amount requested by the cardholder.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "applicationUsageControl", + "description": "An indicator used to specify an issuer's restrictions for processing in a geographic region.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminalCountryCode", + "description": "The country code indicated by the payment reader to process the transaction with.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "applicationCryptogram", + "description": "The cryptogram provided by an integrated circuit card (ICC) used for processing the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cryptogramInformationData", + "description": "An indicator for the type of application cryptogram provided by an integrated circuit card (ICC) to process the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cardholderVerificationMethodResults", + "description": "An indicator of the cardholder verification method and if it was successful or unsuccessful.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "applicationTransactionCounter", + "description": "A counter managed by an integrated circuit card (ICC) that provides a reference to each transaction using that card.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unpredictableNumber", + "description": "A value used to uniquely differentiate an application cryptogram used during authorization processing.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issuerActionCodeDefault", + "description": "An indicator of the conditions that caused a transaction to be offline declined by the issuer, in a scenario where the transaction may have authorized if the payment reader made a processor request but was unable to.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issuerActionCodeDenial", + "description": "An indicator of the conditions that caused a transaction to be offline declined by the issuer, in a scenario where the payment reader did not attempt to make a processor request.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issuerActionCodeOnline", + "description": "An indicator of the conditions that caused the payment reader to attempt to make a processor request.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "InStoreReaderOriginDetails", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ExchangeRate", + "description": "A value with more than one decimal place, representing an exchange rate between currencies. For example, `0.93014065558374`.\nminLength: 3\npattern: ^\\\\d+[.]\\\\d+$", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ExchangeRateQuote", + "description": "Details of the generated exchange rate quote.", + "fields": [ + { + "name": "id", + "description": "Unique identifier, which must be passed in the payment request in order to honor the exchange rate during settlement.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "baseAmount", + "description": "The amount in the `baseCurrency` to be converted to the `quoteCurrency`. If no amount was provided, then this amount is 1 unit of `baseCurrency`.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "quoteAmount", + "description": "The amount in the `quoteCurrency` converted from the `baseCurrency`.\nIf no amount was provided, then this amount is converted from 1 unit of `baseCurrency`, which will be the same as `exchangeRate` after rounding-off.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "exchangeRate", + "description": "This much of `quoteCurrency` is required to buy 1 unit of `baseCurrency`. This includes merchant `markupPercentage` if any.\nIf a `markupPercentage` is specified, this field will be the sum of that percentage and the `tradeRate`.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ExchangeRate", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tradeRate", + "description": "This is the rate at which PayPal will settle with the merchant.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ExchangeRate", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expiresAt", + "description": "When the exchange rate quote represents expires.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refreshesAt", + "description": "When the exchange rate quote represents will be refreshed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ExchangeRateQuoteInput", + "description": "Input to generate the exchange rate quote.", + "fields": null, + "inputFields": [ + { + "name": "baseCurrency", + "description": "The currency code from which the exchange rate will be used to convert to the `quoteCurrency`.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CurrencyCodeAlpha", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "quoteCurrency", + "description": "The currency code to which the exchange rate will be used to convert from `baseCurrency`.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CurrencyCodeAlpha", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "baseAmount", + "description": "The amount in the `baseCurrency` to be converted to the `quoteCurrency`. If this is provided, the result will include the converted amount properly rounded.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "markup", + "description": "A percentage added into the exchange rate. This allows the merchant to settle for more than the quoted `tradeRate`.", + "type": { + "kind": "SCALAR", + "name": "Percentage", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ExchangeRateQuotePayload", + "description": "Exchange rate quotes for a specific customer.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "quotes", + "description": "Exchange rate quote details for each base and quote currency combination.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ExchangeRateQuote", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ExternalVaultStatus", + "description": "A credit card's assocation with an external vault.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "VAULTED", + "description": "The payment method for this transaction has been vaulted in an external vault.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "WILL_VAULT", + "description": "The payment method has not been vaulted in an exernal vault, but it will be if this transaction is successfully processed.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FacilitatorDetails", + "description": "Fields capturing information about a third party that provided payment information for this transaction via the Grant API, Shared Vault, or Google Pay.", + "fields": [ + { + "name": "oauthApplication", + "description": "The OAuth application that owns the payment information used to create the transaction.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "OAuthApplication", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FailedEvent", + "description": "Accompanying information for a transaction that failed because it could not be successfully sent to the processor.", + "fields": [ + { + "name": "status", + "description": "The new status of the transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the transaction failed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount of the transaction for this status event.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "The source for the transaction change to the new status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processorResponse", + "description": "Fields describing the payment processor response, or an explanation for the lack thereof.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionAuthorizationProcessorResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "networkResponse", + "description": "Fields describing the network response to the authorization request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentNetworkResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "riskDecision", + "description": "Risk decision for this transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "RiskDecision", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminal", + "description": "Whether or not this is the final state for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "FinalizeDisputeInput", + "description": "Top-level input fields for finalizing a dispute.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "disputeId", + "description": "The ID of the dispute to be finalized.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FinalizeDisputePayload", + "description": "Top-level field returned when finalizing a dispute.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dispute", + "description": "Information about the dispute that was finalized.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Dispute", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Float", + "description": "Built-in Float", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FraudProviderConfiguration", + "description": "Configuration for fraud protection provider.", + "fields": [ + { + "name": "merchantId", + "description": "The merchant ID used by the fraud protection provider to identify the fraud data collection request.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "The name of the fraud provider.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "FraudServiceProvider", + "description": "The fraud service provider used to generate the risk decision.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CHARGEBACK_PROTECTION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "EFFORTLESS_CHARGEBACK_PROTECTION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAUD_PROTECTION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAUD_PROTECTION_ADVANCED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAUD_PROTECTION_ENTERPRISE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "KOUNT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "GatewayRejectedEvent", + "description": "Accompanying information for a gateway rejected transaction.", + "fields": [ + { + "name": "status", + "description": "The new status of the transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the transaction was rejected by the gateway.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount of the transaction for this status event.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "The source for the transaction change to the new status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gatewayRejectionReason", + "description": "The reason the transaction was rejected, based on your gateway settings.", + "args": [], + "type": { + "kind": "ENUM", + "name": "GatewayRejectionReason", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processorResponse", + "description": "Fields describing the payment processor response. Depending on your gateway settings, the AVS and CVV responses may be the reason for the rejection.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionAuthorizationProcessorResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "networkResponse", + "description": "Fields describing the network response to the authorization request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentNetworkResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "riskDecision", + "description": "Risk decision for this transaction. If the gatewayRejectionReason is fraud, this may be the reason for the rejection.", + "args": [], + "type": { + "kind": "ENUM", + "name": "RiskDecision", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminal", + "description": "Whether or not this is the final state for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "duplicateOf", + "description": "The original transaction if the gateway rejection reason was `DUPLICATE`.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "GatewayRejectionReason", + "description": "Possible reasons why a transaction was rejected by the gateway.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "APPLICATION_INCOMPLETE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AVS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AVS_AND_CVV", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CVV", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DUPLICATE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "EXCESSIVE_RETRY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAUD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MANUAL_TRANSACTIONS_DISABLED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAYMENT_METHOD_BLOCKED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RISK_THRESHOLD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "THREE_D_SECURE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TOKEN_ISSUANCE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TOO_MANY_CONFIRMATION_ATTEMPTS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION_PAY_ENROLLMENT_REQUIRED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "GenerateExchangeRateQuoteInput", + "description": "Input to generate a list of exchange rate quotes.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "quotes", + "description": "Base and quote currency combinations for which the quote will be generated.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ExchangeRateQuoteInput", + "ofType": null + } + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "GeoCoordinates", + "description": "Coordinates describing a geographic position.", + "fields": [ + { + "name": "latitude", + "description": "The angular distance of a place north or south of the earth's equator.\nA positive value is north of the equator, a negative value is south of the equator.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "longitude", + "description": "The angular distance of a place east or west of the meridian at Greenwich, England.\nA positive value is east of the prime meridian, a negative value is west of the prime meridian.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "GeoCoordinatesInput", + "description": "Coordinates describing a geographic position.", + "fields": null, + "inputFields": [ + { + "name": "latitude", + "description": "The angular distance of a place north or south of the earth's equator.\nA positive value is north of the equator, a negative value is south of the equator.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "longitude", + "description": "The angular distance of a place east or west of the meridian at Greenwich, England.\nA positive value is east of the prime meridian, a negative value is west of the prime meridian.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "GooglePayConfiguration", + "description": "Configuration for Google Pay on Android and the web.", + "fields": [ + { + "name": "countryCode", + "description": "The country code of the acquiring bank where the transaction is likely to be processed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CountryCodeAlpha2", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "displayName", + "description": "A string used to identify the merchant to the customer.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "environment", + "description": "The environment being used for Google Pay.", + "args": [], + "type": { + "kind": "ENUM", + "name": "GooglePayEnvironment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "googleAuthorization", + "description": "Authorization to use when tokenizing a Google Pay payment method.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "This field is included for supporting legacy clients." + }, + { + "name": "paypalClientId", + "description": "A string used to identify the merchant's PayPal account when generating a PayPal Closed Loop Token.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "supportedCardBrands", + "description": "A list of card brands supported by the merchant for Google Pay.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "GooglePayEnvironment", + "description": "The environment being used for Google Pay.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "PRODUCTION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SANDBOX", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "production", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sandbox", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "GooglePayOriginDetails", + "description": "Additional information about the payment method specific to Google Pay.", + "fields": [ + { + "name": "googleTransactionId", + "description": "A reference ID for the Google transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bin", + "description": "The first 6 digits of the credit card, known as the Bank Identification Number. This BIN may differ from the BIN of the customer's actual card.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "HyperwalletAccountDetails", + "description": "Details about a Hyperwallet account.", + "fields": [ + { + "name": "userId", + "description": "The ID of the Hyperwallet account.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "IDealConfiguration", + "description": "Configuration for iDEAL.", + "fields": [ + { + "name": "routeId", + "description": "The route ID used to process an iDEAL payment.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assetsUrl", + "description": "A URL used to redirect the customer to the bank's web page.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InStoreContext", + "description": "Reference object for an in-store request.", + "fields": [ + { + "name": "id", + "description": "A unique ID for this in-store request.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": true, + "deprecationReason": "Use the id field from the InStoreContextPayload" + }, + { + "name": "transaction", + "description": "The transaction representing the charge on the payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use a Node query for a RequestTransactionInStoreContext" + }, + { + "name": "refund", + "description": "The refund representing the refund on the payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Refund", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use a Node query for a RequestRefundInStoreContext" + }, + { + "name": "reader", + "description": "The reader associated with the in-store request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreReader", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use the reader field from the InStoreContextPayload" + }, + { + "name": "status", + "description": "The status of the context created.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "InStoreContextStatus", + "ofType": null + } + }, + "isDeprecated": true, + "deprecationReason": "Use the status field from the InStoreContextPayload" + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "InStoreContextResult", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InStoreContextPayload", + "description": "Top-level fields returned when requesting a state change on an in-store reader.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inStoreContext", + "description": "The in-store context created when an in-store flow is initiated.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreContext", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use top-level fields" + }, + { + "name": "id", + "description": "A unique ID for this in-store context request.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reader", + "description": "The reader associated with the in-store request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreReader", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of the context created.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "InStoreContextStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "InStoreContextResult", + "description": "Reference object for an in-store request.", + "fields": [ + { + "name": "id", + "description": "A unique ID for this in-store request.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reader", + "description": "The reader associated with the in-store request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreReader", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "InStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestChargeInStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestConfirmationPromptInStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestDisplayInStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestFirmwareUpdateInStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestRefundInStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestSignaturePromptInStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestVaultInStoreContext", + "ofType": null + } + ] + }, + { + "kind": "ENUM", + "name": "InStoreContextStatus", + "description": "Potential statuses of a context created as part of an in-store request.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CANCELLED", + "description": "The context was successfully canceled.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "COMPLETE", + "description": "Successful. The context was ended.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FAILED", + "description": "Not successful. The context was ended.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PENDING", + "description": "Flow in-progress. Waiting for reader or point of sale interaction.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROCESSING", + "description": "Payment flow in-progress. Customer payment method submitted for transaction processing.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InStoreDisplayItemInput", + "description": "Input fields for an individual display item on an in-store reader.", + "fields": null, + "inputFields": [ + { + "name": "kind", + "description": "The display item type to be displayed on the in-store reader.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "DisplayItemType", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "description", + "description": "The display item text to be displayed on the in-store reader. 35 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "quantity", + "description": "The number of units for a CHARGE or DISCOUNT item. Must be greater than 0.", + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "The total amount of a CHARGE or DISCOUNT item.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InStoreLocation", + "description": "An in-store location.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "Name of the in-store location.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "internalName", + "description": "A merchant-assigned internal name of this location, unique to this merchant.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "address", + "description": "The address of the in-store location.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreLocationAddress", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "geoCoordinates", + "description": "The coordinates of this location.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "GeoCoordinates", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payerId", + "description": "The PayPal account ID to which this location was added.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "qrCodePaymentsEnabled", + "description": "Whether QR code payments will be enabled for this location.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InStoreLocationAddress", + "description": "Input fields for an in-store location address.", + "fields": [ + { + "name": "streetAddress", + "description": "The street address.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "extendedAddress", + "description": "Extended address information, such as an apartment or suite number.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locality", + "description": "Locality/city.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "region", + "description": "State or province.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "postalCode", + "description": "Postal code in any country's format, otherwise known as CAP, CEP, Eircode, NPA, PIN, PLZ, or ZIP code.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "countryCode", + "description": "Country code for the address.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CountryCode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InStoreLocationAddressInput", + "description": "Input fields for an in-store Location Address.", + "fields": null, + "inputFields": [ + { + "name": "streetAddress", + "description": "The street address.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "extendedAddress", + "description": "Extended address information, such as an apartment or suite number.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "locality", + "description": "Locality/city.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "region", + "description": "State or province.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "postalCode", + "description": "Postal code in any country's format, otherwise known as CAP, CEP, Eircode, NPA, PIN, PLZ, or ZIP code.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "countryCode", + "description": "Country code for the address.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CountryCode", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InStoreLocationAddressUpdateInput", + "description": "Input fields for an in-store Location Address update.", + "fields": null, + "inputFields": [ + { + "name": "streetAddress", + "description": "The street address.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "extendedAddress", + "description": "Extended address information, such as an apartment or suite number.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "locality", + "description": "Locality/city.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "region", + "description": "State or province.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "postalCode", + "description": "Postal code in any country's format, otherwise known as CAP, CEP, Eircode, NPA, PIN, PLZ, or ZIP code.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "countryCode", + "description": "Country code for the address.", + "type": { + "kind": "SCALAR", + "name": "CountryCode", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InStoreLocationConnection", + "description": "A paginated list of in-store locations.", + "fields": [ + { + "name": "edges", + "description": "A list of in-store locations.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "InStoreLocationConnectionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information about the page of in-store locations contained in `edges`.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InStoreLocationConnectionEdge", + "description": "An in-store location within an InStoreLocationConnection.", + "fields": [ + { + "name": "cursor", + "description": "The in-store locations's location within the InStoreLocationConnection. Used for requesting additional pages.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The in-store location.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreLocation", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InStoreLocationInput", + "description": "Fields required for an instore location.", + "fields": null, + "inputFields": [ + { + "name": "name", + "description": "The publicly visible label of this Location.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "internalName", + "description": "Name assigned by the merchant to uniquely identify this Location.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "address", + "description": "The address of the in-store Location.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InStoreLocationAddressInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "geoCoordinates", + "description": "The coordinates of this location.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "GeoCoordinatesInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "payerId", + "description": "The PayPal account ID to which this Location will be added.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "enableQRCodePayments", + "description": "Whether QR code payments will be enabled for this location.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InStoreLocationUpdateInput", + "description": "Fields required to update an in-store location.", + "fields": null, + "inputFields": [ + { + "name": "name", + "description": "The publicly visible label of this location.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "internalName", + "description": "Name assigned by the merchant to uniquely identify this location.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "address", + "description": "The address of the location.", + "type": { + "kind": "INPUT_OBJECT", + "name": "InStoreLocationAddressUpdateInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "geoCoordinates", + "description": "The coordinates of this location.", + "type": { + "kind": "INPUT_OBJECT", + "name": "GeoCoordinatesInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "payerId", + "description": "The PayPal account ID to which this location will be added.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "enableQRCodePayments", + "description": "Whether QR code payments will be enabled for this location.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InStoreReader", + "description": "An in-store payment card reader.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "Name given to the reader.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "vendor", + "description": "Vendor-specific information about the reader.", + "args": [], + "type": { + "kind": "UNION", + "name": "InStoreReaderVendor", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "location", + "description": "The in-store location the reader is attached to.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreLocation", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "Current status of the reader.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ReaderStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pairedAt", + "description": "Date and time when the reader was paired.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastSeenAt", + "description": "Date and time when the reader last established a connection.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "offlineSince", + "description": "Date and time when the reader last disconnected.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "softwareVersion", + "description": "The version of the payment application running on the Reader.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "InStoreReaderAuthorizationMode", + "description": "The authorization mode used to perform the transaction.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CARD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ISSUER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InStoreReaderConnection", + "description": "A paginated list of in-store readers.", + "fields": [ + { + "name": "edges", + "description": "A list of in-store readers.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "InStoreReaderConnectionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information about the page of in-store readers contained in `edges`.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InStoreReaderConnectionEdge", + "description": "An in-store reader within an InStoreReaderConnection.", + "fields": [ + { + "name": "cursor", + "description": "The in-store reader's location within the InStoreReaderConnection. Used for requesting additional pages.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The in-store reader.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreReader", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "InStoreReaderOriginDetails", + "description": "Additional information about the payment method supplied by an in-store payment reader.", + "fields": [ + { + "name": "authorizationMode", + "description": "The authorization mode used to perform the transaction on the payment reader.", + "args": [], + "type": { + "kind": "ENUM", + "name": "InStoreReaderAuthorizationMode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pinVerified", + "description": "An indicator for whether the transaction was verified via pin.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputMode", + "description": "The input mode used on the payment reader to facilitate an in-store transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentReaderInputMode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminalId", + "description": "The ID of the terminal that was processed this transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "CardPresentOriginDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "EmvCardOriginDetails", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "InStoreReaderPayload", + "description": "Top-level fields returned for an in-store reader.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reader", + "description": "The reader.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreReader", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InStoreReaderSearchInput", + "description": "Input fields for searching for in-store readers.", + "fields": null, + "inputFields": [ + { + "name": "locationId", + "description": "Find in-store readers with location ID or IDs.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "softwareVersion", + "description": "Find in-store readers with software version.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchSoftwareVersionInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "readerStatus", + "description": "Find in-store readers with reader status.", + "type": { + "kind": "ENUM", + "name": "ReaderStatus", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InStoreReaderSetupInput", + "description": "Fields that are reader specific for pairing a reader.", + "fields": null, + "inputFields": [ + { + "name": "locationId", + "description": "In-Store Location to attach Reader to.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "name", + "description": "Name given to the Reader.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "InStoreReaderVendor", + "description": "A union of all possible in-store reader vendors.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "VerifoneVendor", + "ofType": null + } + ] + }, + { + "kind": "INPUT_OBJECT", + "name": "InStoreRefundInput", + "description": "Input fields for creating an in-store transaction.", + "fields": null, + "inputFields": [ + { + "name": "amount", + "description": "Refund amount of the request. This value must be greater than 0, and must match the currency format of the merchant account. This can only contain numbers and one decimal point (e.g. x.xx). Can't be greater than the maximum allowed by the processor.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "Merchant account ID used to process the refund. Currency is also determined by merchant account ID. If no merchant account ID is specified, we will use your default merchant account.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "orderId", + "description": "Additional information about the refund. On PayPal refunds, this field maps to the PayPal invoice number. PayPal invoice numbers must be unique in your PayPal business account. Maximum 255 characters or 127 for PayPal refunds.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customFields", + "description": "Collection of custom field/value pairs. You must [set up each custom field in the Control Panel](https://articles.braintreepayments.com/control-panel/custom-fields#creating-a-custom-field) prior to passing it with a request.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CustomFieldInput", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "descriptor", + "description": "Fields used to define what will appear on a customer's bank statement for a specific purchase.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionDescriptorInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InStoreTransactionInput", + "description": "Input fields for creating an in-store transaction.", + "fields": null, + "inputFields": [ + { + "name": "amount", + "description": "Billing amount of the request. This value must be greater than 0, and must match the currency format of the merchant account. This can only contain numbers and one decimal point (e.g. x.xx). Can't be greater than the maximum allowed by the processor.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "Merchant account ID used to process the transaction. Currency is also determined by merchant account ID. If no merchant account ID is specified, we will use your default merchant account.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "orderId", + "description": "Additional information about the transaction. On PayPal transactions, this field maps to the PayPal invoice number. PayPal invoice numbers must be unique in your PayPal business account. Maximum 255 characters or 127 for PayPal transactions.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customFields", + "description": "Collection of custom field/value pairs. You must [set up each custom field in the Control Panel](https://articles.braintreepayments.com/control-panel/custom-fields#creating-a-custom-field) prior to passing it with a request.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CustomFieldInput", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "descriptor", + "description": "Fields used to define what will appear on a customer's bank statement for a specific purchase.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionDescriptorInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customerId", + "description": "If charging a single-use payment method, optional ID of a customer to associate the transaction with. If vaulting the single-use payment method, this customer will be associated with the resulting multi-use payment method.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "vaultPaymentMethodAfterTransacting", + "description": "When a single-use payment method is used to create this transaction, it can be automatically stored in the vault after transacting. If this field is left blank, the single-use payment method will not be vaulted.", + "type": { + "kind": "INPUT_OBJECT", + "name": "VaultInStorePaymentMethodAfterTransactingInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "channel", + "description": "For partners and shopping carts only. If you are a shopping cart provider or other Braintree partner, pass a string identifier for your service. For PayPal transactions, this maps to paypal.bn_code.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "Built-in Int", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "KountConfiguration", + "description": "Configuration for Kount fraud tools.", + "fields": [ + { + "name": "merchantId", + "description": "The Kount merchant ID used to identify the fraud data collection request.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Language", + "description": "The [language tag](https://tools.ietf.org/html/bcp47#section-2) for the language in which to localize the error-related strings, such as messages, issues, and suggested actions.\nThe tag is made up of the [ISO 639-2 language code](https://www.loc.gov/standards/iso639-2/php/code_list.php), the optional [ISO-15924 script tag](http://www.unicode.org/iso15924/codelists.html), and the [ISO-3166 alpha-2 country code](https://developer.paypal.com/braintree/docs/reference/general/countries).\nmaxLength: 10\nminLength: 2\npattern: ^[a-z]{2}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}))?$", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "LegacyIdType", + "description": "The type of object the legacy ID represents when converting it to a global ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CUSTOMER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DISPUTE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MERCHANT_ACCOUNT_APPLICATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAYMENT_CONTEXT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAYMENT_METHOD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "REFUND", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TRANSACTION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "US_BANK_ACCOUNT_VERIFICATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VERIFICATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "LiabilityShift", + "description": "A scenario detailing which party assumes liability for certain conditions in the event of a transaction being disputed.", + "fields": [ + { + "name": "responsibleParty", + "description": "The party taking responsibility for liability.", + "args": [], + "type": { + "kind": "ENUM", + "name": "LiabilityShiftResponsibleParty", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "conditions", + "description": "The specific conditions under which the responsible party assumes liability, in the event of a chargeback.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "LiabilityShiftCondition", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "LiabilityShiftCondition", + "description": "If enrolled in Effortless Chargeback Protection, and in the event the transaction is disputed, these are the specific conditions under which the responsible party assumes liability for that chargeback.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ITEM_NOT_RECEIVED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNAUTHORIZED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "LiabilityShiftResponsibleParty", + "description": "If enrolled in Effortless Chargeback Protection, and in the event the transaction is disputed, these are the possible parties which can assume liability.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ISSUER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "LocalPaymentAddressInput", + "description": "Input fields for local payment addresses.", + "fields": null, + "inputFields": [ + { + "name": "streetAddress", + "description": "The street address.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "extendedAddress", + "description": "Extended address information, such as an apartment or suite number.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "locality", + "description": "Locality/city.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "region", + "description": "State or province.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "postalCode", + "description": "Postal code in any country's format, otherwise known as CAP, CEP, Eircode, NPA, PIN, PLZ, or ZIP code.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "countryCode", + "description": "Country code for the address.", + "type": { + "kind": "SCALAR", + "name": "CountryCode", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "LocalPaymentContext", + "description": "The LocalPayment object.", + "fields": [ + { + "name": "id", + "description": "Unique identifier for the payment context.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legacyId", + "description": "Legacy unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": "The type of the local payment.", + "args": [], + "type": { + "kind": "ENUM", + "name": "LocalPaymentMethodType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "approvalUrl", + "description": "The URL to which a customer should be redirected to approve the local payment.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount charged in this local payment.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchantAccountId", + "description": "The merchant account used to create the payment context.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "transactedAt", + "description": "Date and time when the local payment context was used to create a transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "approvedAt", + "description": "Date and time when the local payment context was approved by the customer.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date and time when the local payment context was created.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": "Date and time when the local payment context was updated.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "expiredAt", + "description": "Date and time when the local payment context was expired.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentId", + "description": "Unique identifier for the local payment.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "orderId", + "description": "The PayPal Invoice ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "PaymentContext", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "LocalPaymentDetails", + "description": "Local payment specific details on a transaction.", + "fields": [ + { + "name": "origin", + "description": "Additional information about the local payment method provided from a third-party origin, such as PayPal or another regional payment method provider.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethodOrigin", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": "Regional payment method selected by the customer.", + "args": [], + "type": { + "kind": "ENUM", + "name": "LocalPaymentMethodType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "displayName", + "description": "Description of the payment method that can be displayed to customers.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "LocalPaymentMethodType", + "description": "A value identifying the type of regional payment method.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ALIPAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "BANCONTACT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "BLIK", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "BOLETOBANCARIO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "EPS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GIROPAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GRABPAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "IDEAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MULTIBANCO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MYBANK", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OXXO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "P24", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAYU", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAY_UPON_INVOICE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SATISPAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SEPA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SOFORT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SWISH", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TRUSTLY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VERKKOPANKKI", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VIPPS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "WECHAT_PAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "LocalPaymentPayerInfoInput", + "description": "Input fields for the payer of a local payment.", + "fields": null, + "inputFields": [ + { + "name": "givenName", + "description": "The payer's given (first) name.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "surname", + "description": "The payer's surname (last name).", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "email", + "description": "The payer's email.", + "type": { + "kind": "SCALAR", + "name": "EmailAddress", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "phoneNumber", + "description": "The payer's phone number.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "shippingAddress", + "description": "The payer's shipping address.", + "type": { + "kind": "INPUT_OBJECT", + "name": "LocalPaymentAddressInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "billingAddress", + "description": "The payer's billing address.", + "type": { + "kind": "INPUT_OBJECT", + "name": "LocalPaymentAddressInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "taxInfo", + "description": "The payer's tax information. This is only required for Boleto Bancário payments.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TaxInfoInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "MVVAcceptanceChannel", + "description": "Means by which customers by their bills.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "FACE_TO_FACE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MAIL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PHONE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "WEB", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "MVVRegistrationType", + "description": "Supported MVV (Merchant Verification Value) programs.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "LOAN_VPP", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TAX_DEBIT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UTIL_RATE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UTIL_VPP", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "MVVUtilityType", + "description": "Supported MVV (Merchant Verification Value) utility types.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ELECTRIC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GAS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TRASH", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "WATER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "MandateType", + "description": "Mandate type for SEPA Direct Debit Account.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ONE_OFF", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RECURRENT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MasterpassConfiguration", + "description": "Configuration for Masterpass.", + "fields": [ + { + "name": "merchantCheckoutId", + "description": "The Masterpass merchant checkout ID used to identify the merchant in Masterpass requests.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "supportedCardBrands", + "description": "A list of card brands supported by the merchant for Masterpass.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MasterpassOriginDetails", + "description": "Additional information about the payment method specific to Masterpass.", + "fields": [ + { + "name": "bin", + "description": "The first 6 digits of the credit card, known as the Bank Identification Number. This BIN may differ from the BIN of the customer's actual card.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Merchant", + "description": "Details about a merchant and its current settings.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "Current status.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "companyName", + "description": "Company name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "website", + "description": "The merchant's main website.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timezone", + "description": "The timezone that the merchant operates in.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchantAccounts", + "description": "A paginated list of merchant accounts that belong to this merchant. Filtered by search criteria, if provided.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "MerchantAccountSearchInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "MerchantAccountConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MerchantAccount", + "description": "Information about a merchant account associated with a merchant.", + "fields": [ + { + "name": "id", + "description": "Unique identifier for the merchant account. Used to determine what merchant account processed or will process a given Payment.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bankAccount", + "description": "The disbursement bank account linked with the merchant account.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "DisbursementBankAccount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "currencyCode", + "description": "The ISO code for the currency the merchant account uses.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CurrencyCodeAlpha", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dbaName", + "description": "Business name of the account.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "externalId", + "description": "A unique identifier for this account in external systems.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of a merchant account. This determines whether the merchant account can be used to create a Payment.", + "args": [], + "type": { + "kind": "ENUM", + "name": "MerchantAccountStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDefault", + "description": "Whether this merchant account is the default for this merchant. The default merchant account is used to process all Payments where a merchant account ID is not specified.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paypalAccount", + "description": "The PayPal account linked with the merchant account.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PayPalAccountDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hyperwalletAccount", + "description": "The Hyperwallet account linked with the merchant account.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "HyperwalletAccountDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "venmoAccount", + "description": "The Venmo account linked with the merchant account.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "VenmoAccountDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "threeDSecure", + "description": "The 3D Secure configuration for the merchant account.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MerchantAccountThreeDSecureConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MerchantAccountApplication", + "description": "A record of a merchant account application.", + "fields": [ + { + "name": "id", + "description": "A unique ID for the account application. Can be used to query the status of the onboarding request in the future.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legacyId", + "description": "Legacy unique ID.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of the application.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ApplicationStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MerchantAccountConnection", + "description": "A paginated list of merchant accounts.", + "fields": [ + { + "name": "edges", + "description": "A list of merchant accounts.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MerchantAccountConnectionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information about the page of merchant accounts contained in `edges`.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MerchantAccountConnectionEdge", + "description": "A merchant account within a MerchantAccountConnection.", + "fields": [ + { + "name": "cursor", + "description": "This merchant account's location within the MerchantAccountConnection. Used for requesting additional pages.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The merchant account.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MerchantAccount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "MerchantAccountSearchInput", + "description": "Input fields for searching for merchant accounts.", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "Find merchant accounts with an id or ids.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paypalAccountId", + "description": "Find merchant accounts associated with a given PayPal account ID.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "MerchantAccountStatus", + "description": "The status of a merchant account. This determines whether the merchant account can be used to create a Payment, and whether funds can continue to flow to the associated bank account.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ACTIVE", + "description": "The merchant account can be used to create transactions and refunds.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PENDING", + "description": "The merchant account is still being set up, and cannot be used to create transactions or refunds yet.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUSPENDED", + "description": "The merchant account cannot be used to process transactions or refunds.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MerchantAccountThreeDSecureConfiguration", + "description": "Details about the 3D Secure configuration of the merchant account.", + "fields": [ + { + "name": "v1", + "description": "Configuration for 3D Secure v1.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MerchantAccountThreeDSecureVersionConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "v2", + "description": "Configuration for 3D Secure v2.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MerchantAccountThreeDSecureVersionConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MerchantAccountThreeDSecureVersionConfiguration", + "description": "Details about the configuration of a version of 3D Secure for the merchant account.", + "fields": [ + { + "name": "supportedCardBrands", + "description": "Card types enabled for this 3D Secure version.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MonetaryAmount", + "description": "A monetary amount with currency.", + "fields": [ + { + "name": "value", + "description": "The amount of money, either a whole number or a number with up to 3 decimal places.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "currencyIsoCode", + "description": "The ISO code for the money's currency.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CurrencyCodeAlpha", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `currencyCode` instead." + }, + { + "name": "currencyCode", + "description": "The currency code for the monetary amount.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CurrencyCodeAlpha", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "MonetaryAmountInput", + "description": "Input fields representing an amount with currency.", + "fields": null, + "inputFields": [ + { + "name": "value", + "description": "The amount of money, either a whole number or a number with up to 3 decimal places.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "currencyCode", + "description": "The currency code for the monetary amount.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CurrencyCodeAlpha", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "MonetaryAmountSearchInput", + "description": "Input fields for searching for a transaction or refund amount.", + "fields": null, + "inputFields": [ + { + "name": "value", + "description": "Find transactions for a given amount.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchRangeInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "currencyIsoCode", + "description": "Deprecated: Please use `currencyCode` instead.\n\nFind transactions with a given currency.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "currencyCode", + "description": "Find transactions with a given currency.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Month", + "description": "A two-digit, zero-padded month.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Mutation", + "description": "The top-level Mutation type. Mutations are used to make requests that create or modify data.", + "fields": [ + { + "name": "authorizePaymentMethod", + "description": "Authorize an eligible payment method and return a payload that includes details of the resulting transaction.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AuthorizePaymentMethodInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authorizePayPalAccount", + "description": "Authorize an eligible PayPal account and return a payload that includes details of the resulting transaction.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AuthorizePayPalAccountInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PayPalTransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authorizeVenmoAccount", + "description": "Authorize an eligible Venmo account and return a payload that includes details of the resulting transaction.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AuthorizeVenmoAccountInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authorizeCreditCard", + "description": "Authorize a credit card of any origin and return a payload that includes details of the resulting transaction.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AuthorizeCreditCardInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "captureTransaction", + "description": "Capture an authorized transaction and return a payload that includes details of the transaction.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CaptureTransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "chargePaymentMethod", + "description": "Charge any payment method and return a payload that includes details of the resulting transaction.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ChargePaymentMethodInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "chargeUsBankAccount", + "description": "Charge a US bank account and return a payload that includes details of the resulting transaction. See https://developers.braintreepayments.com/guides/ach/configuration for information on eligibility and setup.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ChargeUsBankAccountInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "chargePayPalAccount", + "description": "Charge a PayPal account and return a payload that includes details of the resulting transaction.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ChargePayPalAccountInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PayPalTransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "chargeVenmoAccount", + "description": "Charge a Venmo account and return a payload that includes details of the resulting transaction. See https://articles.braintreepayments.com/guides/payment-methods/venmo for information on eligibility and setup.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ChargeVenmoAccountInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "chargeCreditCard", + "description": "Charge a credit card of any origin and return a payload that includes details of the resulting transaction.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ChargeCreditCardInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "vaultPaymentMethod", + "description": "Vault payment information from a single-use payment method and return a payload that includes a new multi-use payment method. When vaulting a credit card, by default, this mutation will also verify that card before vaulting.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "VaultPaymentMethodInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VaultPaymentMethodPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "vaultUsBankAccount", + "description": "Vault payment information from a single-use US bank account payment method and return a payload that includes a new multi-use payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "VaultUsBankAccountInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VaultPaymentMethodPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "vaultCreditCard", + "description": "Vault payment information from a single-use credit card and return a payload that includes a new multi-use payment method. By default, this mutation will also verify the card before vaulting.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "VaultCreditCardInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VaultPaymentMethodPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refundTransaction", + "description": "Refund a settled transaction and return a payload that includes details of the refund.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RefundTransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RefundTransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reverseTransaction", + "description": "Reverse a transaction and return a payload that includes either the voided transaction or a refund.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ReverseTransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ReverseTransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reverseRefund", + "description": "Reverse a refund and return a payload that includes voided refund.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ReverseRefundInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RefundTransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refundCreditCard", + "description": "Create a detached refund (unassociated with any previous Braintree payment) to a credit card and return a payload that includes details of the refund.\n\nWe have previously referred to this as issuing a \"detached credit,\" and it is disallowed by default. See the [documentation](https://articles.braintreepayments.com/control-panel/transactions/refunds-voids-credits#detached-credits) for more information regarding eligibility and configuration.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RefundCreditCardInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RefundCreditCardPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refundUsBankAccount", + "description": "Create a detached refund (unassociated with any previous Braintree payment) to a US Bank Account and return a payload that includes details of the refund.\n\nWe have previously referred to this as issuing a \"detached credit,\" and it is disallowed by default. See the [documentation](https://articles.braintreepayments.com/control-panel/transactions/refunds-voids-credits#detached-credits) for more information regarding eligibility and configuration.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RefundUsBankAccountInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RefundUsBankAccountPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateTransactionCustomFields", + "description": "Update custom fields on a transaction. Custom fields are [defined in the Control Panel](https://articles.braintreepayments.com/control-panel/custom-fields#store-and-pass-back-fields).", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateTransactionCustomFieldsInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateTransactionCustomFieldsPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "verifyPaymentMethod", + "description": "Run a verification on a multi-use payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "VerifyPaymentMethodInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VerifyPaymentMethodPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "verifyCreditCard", + "description": "Run a verification on a multi-use credit card payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "VerifyCreditCardInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VerifyPaymentMethodPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "verifyUsBankAccount", + "description": "Run a verification on a multi-use US bank account payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "VerifyUsBankAccountInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VerifyPaymentMethodPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "confirmMicroTransferAmounts", + "description": "Confirm micro-transfer amounts initiated by vaultUsBankAccount or verifyUsBankAccount, completing the verification process for a US Bank Account via micro-transfer.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ConfirmMicroTransferAmountsInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ConfirmMicroTransferAmountsPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deletePaymentMethodFromVault", + "description": "Delete a multi-use payment method from the vault.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeletePaymentMethodFromVaultInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeletePaymentMethodFromVaultPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createClientToken", + "description": "Create a client token that can be used to initialize a client in order to tokenize payment information.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "CreateClientTokenInput", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateClientTokenPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createUniversalAccessToken", + "description": "Create a PayPal access token that can be used to make additional API calls or initialize a client.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateUniversalAccessTokenInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateUniversalAccessTokenPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "partialCaptureTransaction", + "description": "Partially capture funds from a transaction that was successfully authorized and return a payload that includes a new transaction with information about the capture. This is available for [Venmo](https://developers.braintreepayments.com/guides/venmo/submit-for-partial-settlement) and [PayPal](https://articles.braintreepayments.com/guides/payment-methods/paypal/processing#multiple-partial-settlements) transactions.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PartialCaptureTransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PartialCaptureTransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tokenizeCustomActionsPaymentMethod", + "description": "Tokenize Custom Actions fields and return a payload that includes a single-use payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TokenizeCustomActionsPaymentMethodInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TokenizeCustomActionsPaymentMethodPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tokenizeCreditCard", + "description": "Tokenize credit card fields and return a payload that includes a single-use payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TokenizeCreditCardInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TokenizeCreditCardPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tokenizeCvv", + "description": "Tokenize a credit card's CVV and return a payload that includes a single-use payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TokenizeCvvInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TokenizeCvvPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tokenizeNetworkToken", + "description": "Tokenize a network tokenized payment instrument and return a payload that includes a single-use payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TokenizeNetworkTokenInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TokenizeNetworkTokenPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tokenizeSamsungPayCard", + "description": "Tokenize Samsung Pay card fields and return a payload that includes a single-use payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TokenizeSamsungPayCardInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TokenizeSamsungPayCardPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tokenizeUsBankAccount", + "description": "Tokenize US bank account fields and return a payload that includes a single-use payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TokenizeUsBankAccountInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TokenizeUsBankAccountPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tokenizeUsBankLogin", + "description": "Tokenize US bank login fields and return a payload that includes a single-use payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TokenizeUsBankLoginInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TokenizeUsBankAccountPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tokenizePayPalOneTimePayment", + "description": "Tokenize PayPal One-Time Payment and return a payload that includes a single-use payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TokenizePayPalOneTimePaymentInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TokenizePayPalOneTimePaymentPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createPayPalOneTimePayment", + "description": "Set up a PayPal One-Time Payment for approval by a PayPal user. See [documentation](https://developer.paypal.com/braintree/docs/guides/paypal/checkout-with-paypal) for more information. Your account must be enabled for this feature.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreatePayPalOneTimePaymentInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreatePayPalOneTimePaymentPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tokenizePayPalBillingAgreement", + "description": "Tokenize PayPal account and return a payload that includes a single-use payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TokenizePayPalBillingAgreementInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TokenizePayPalBillingAgreementPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createPayPalBillingAgreement", + "description": "Set up a PayPal Billing Agreement Token for approval by a PayPal user.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreatePayPalBillingAgreementInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreatePayPalBillingAgreementPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createCustomer", + "description": "Create a customer for storing individual customer information and/or grouping transactions and multi-use payment methods.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "CreateCustomerInput", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateCustomerPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateCustomer", + "description": "Update a customer's information.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateCustomerInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateCustomerPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteCustomer", + "description": "Delete a customer, breaking association between any of the customer's transactions. Will not delete if the customer has existing payment methods.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteCustomerInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteCustomerPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deletePaymentMethodFromSingleUseToken", + "description": "Delete a payment method referenced by a single-use token.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeletePaymentMethodFromSingleUseTokenInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeletePaymentMethodFromSingleUseTokenPayload", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `deletePaymentMethodFromVault` instead." + }, + { + "name": "updateCreditCardBillingAddress", + "description": "Set a new billing address for a multi-use credit card payment method. By default, this mutation will also verify the card with the new billing address before updating.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateCreditCardBillingAddressInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateCreditCardBillingAddressPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "performThreeDSecureLookup", + "description": "Attempt to perform 3D Secure Authentication on credit card payment method. This may consume the payment method and return a new single-use payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PerformThreeDSecureLookupInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PerformThreeDSecureLookupPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "acceptDispute", + "description": "Accepts a dispute and returns a payload that includes the dispute that was accepted. Only disputes with a status of OPEN can be accepted.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AcceptDisputeInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AcceptDisputePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "finalizeDispute", + "description": "Finalizes a dispute and returns a payload that includes the dispute that was finalized. Only disputes with a status of OPEN can be finalized.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "FinalizeDisputeInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "FinalizeDisputePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createDisputeTextEvidence", + "description": "Creates text evidence to a dispute and returns a payload that includes the evidence that was created. Only disputes with a status of OPEN can have text evidence created for them.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateDisputeTextEvidenceInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateDisputeTextEvidencePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deleteDisputeEvidence", + "description": "Deletes evidence from a dispute.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DeleteDisputeEvidenceInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DeleteDisputeEvidencePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createDisputeFileEvidence", + "description": "Uploads an evidence file and associates it with a dispute. **Note:**: file upload requires a special request format. See the ['Uploading Files' integration guide](https://graphql.braintreepayments.com/integration_guides/uploading_files) for instructions on how to perform this mutation.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateDisputeFileEvidenceInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateDisputeFileEvidencePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "vaultPayPalBillingAgreement", + "description": "Vault an existing PayPal Billing Agreement that was not created through Braintree. Only use this mutation if you need to import PayPal Billing Agreements from an existing PayPal integration into your Braintree account.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "VaultPayPalBillingAgreementInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VaultPayPalBillingAgreementPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sandboxSettleTransaction", + "description": "Force a transaction to settle in the sandbox environment. Generates an error elsewhere.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SandboxSettleTransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createInStoreLocation", + "description": "Creates a new In-Store Location to associate Readers.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateInStoreLocationInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateInStoreLocationPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateInStoreLocation", + "description": "Updates an In-Store Location.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateInStoreLocationInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateInStoreLocationPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pairInStoreReader", + "description": "Pairs a Reader to an account and In-Store Location.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PairInStoreReaderInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreReaderPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateInStoreReader", + "description": "Updates an In-Store Reader.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateInStoreReaderInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreReaderPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requestChargeFromInStoreReader", + "description": "Request an in-store reader to begin the charge flow.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RequestChargeFromInStoreReaderInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreContextPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requestCancelFromInStoreReader", + "description": "Request an in-store reader to cancel the charge flow.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RequestCancelFromInStoreReaderInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreContextPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requestRefundFromInStoreReader", + "description": "Request an in-store reader to start an unreferenced refund flow.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RequestRefundFromInStoreReaderInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreContextPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requestVaultFromInStoreReader", + "description": "Request an in-store reader to vault a payment method.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RequestVaultFromInStoreReaderInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreContextPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requestTextDisplayFromInStoreReader", + "description": "Request an in-store reader to display text.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RequestTextDisplayFromInStoreReaderInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreContextPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requestItemDisplayFromInStoreReader", + "description": "Request an in-store reader to display line items.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RequestItemDisplayFromInStoreReaderInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreContextPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requestFirmwareUpdateFromInStoreReader", + "description": "Request an in-store reader to update to the latest version of software.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RequestFirmwareUpdateFromInStoreReaderInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreContextPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requestSignaturePromptFromInStoreReader", + "description": "Request an in-store reader to display a signature prompt.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RequestSignaturePromptFromInStoreReaderInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreContextPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "requestConfirmationPromptFromInStoreReader", + "description": "Request an in-store reader to display a confirmation prompt.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RequestConfirmationPromptFromInStoreReaderInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreContextPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateTransactionAmount", + "description": "Updates the authorization amount of the transaction.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateTransactionAmountInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TransactionPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "generateExchangeRateQuote", + "description": "Generate a customized currency exchange rate quote for items on a merchant's page. This allows merchants to advertise products in their customer's currency. Your account must be enabled to use this feature.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "GenerateExchangeRateQuoteInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ExchangeRateQuotePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createNonInstantLocalPaymentContext", + "description": "Creates a non-instant local payment context. Your account must be enabled to use this feature.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateNonInstantLocalPaymentContextInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateNonInstantLocalPaymentContextPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "NameInput", + "description": "The name of the party.", + "fields": null, + "inputFields": [ + { + "name": "prefix", + "description": "The prefix, or title, to the party name.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "givenName", + "description": "The party's given, or first, name. Required if the party is a person.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "surname", + "description": "The party's surname or family name. Also known as the last name. Required if\nthe party is a person. Use also to store multiple surnames including the\nmatronymic, or mother's, surname.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "middleName", + "description": "The party's middle name. Use also to store multiple middle names including the patronymic, or father's, middle name.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "suffix", + "description": "The suffix for the party's name.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "alternateFullName", + "description": "The party's alternate name. Can be a business name, nickname, or any other\nname that cannot be split into first, last name. Required for a business party name.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "NetworkTokenInput", + "description": "Input fields for a network tokenized card.", + "fields": null, + "inputFields": [ + { + "name": "cryptogram", + "description": "A one-time-use string generated by the token requester to validate the transaction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "eCommerceIndicator", + "description": "A two-digit string that should be passed along in the authorization message.", + "type": { + "kind": "SCALAR", + "name": "ECommerceIndicator", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "expirationMonth", + "description": "A two-digit string representing the expiration month of the DPAN.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Month", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "expirationYear", + "description": "A four-digit string representing the expiration year of the DPAN.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Year", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "number", + "description": "The card number used in processing. This is a device PAN (DPAN), not the backing card number (FPAN).", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CreditCardNumber", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "originDetails", + "description": "Additional information about a network token.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "NetworkTokenOriginDetailsInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "NetworkTokenOrigin", + "description": "The source of the network token.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "APPLE_PAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GOOGLE_PAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NETWORK_TOKEN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "NetworkTokenOriginDetails", + "description": "Additional information about the payment method specific to Network Token.", + "fields": [ + { + "name": "bin", + "description": "The first 6 digits of the credit card, known as the Bank Identification Number. This BIN may differ from the BIN of the customer's actual card.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "NetworkTokenOriginDetailsInput", + "description": "Information about the network token, such as the origin of the network token, source card details, and other token requestor data.", + "fields": null, + "inputFields": [ + { + "name": "origin", + "description": "The origin of the network token.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "NetworkTokenOrigin", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "sourceCardDescription", + "description": "A string, suitable for display, that describes the backing card.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "sourceCardLast4", + "description": "The last 4 digits of the backing card number (FPAN).", + "type": { + "kind": "SCALAR", + "name": "CreditCardLast4", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "sourceCardType", + "description": "The card type of the backing card.", + "type": { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "tokenRequestorId", + "description": "The token requestor ID of the entity that generated this network token.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transactionId", + "description": "The transaction ID for this network token.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "description": "Relay compatible Node interface.", + "fields": [ + { + "name": "id", + "description": "Global ID for a given object.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "BusinessAccountCreationRequest", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CustomActionsPaymentContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Customer", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Dispute", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "InStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "LocalPaymentContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Refund", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestChargeInStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestConfirmationPromptInStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestDisplayInStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestFirmwareUpdateInStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestRefundInStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestSignaturePromptInStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "RequestVaultInStoreContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Verification", + "ofType": null + } + ] + }, + { + "kind": "INPUT_OBJECT", + "name": "NonInstantLocalPaymentContextInput", + "description": "Input fields for non-instant local payment context.", + "fields": null, + "inputFields": [ + { + "name": "orderId", + "description": "The order id of the eventual Braintree transaction and the invoice number of the local payment context. Maximum 127 characters.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "The amount of the local payment.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "MonetaryAmountInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "type", + "description": "The type of the non-instant local payment.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "NonInstantLocalPaymentMethodType", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "countryCode", + "description": "The country code of the local payment. For local payments supported in multiple countries, this value may determine which banks are presented to the customer.", + "type": { + "kind": "SCALAR", + "name": "CountryCode", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "locale", + "description": "The language tag for the language in which to localize the error-related strings.", + "type": { + "kind": "SCALAR", + "name": "Language", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "returnUrl", + "description": "The URL where the customer is redirected after the customer approves the payment.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "cancelUrl", + "description": "The URL where the customer is redirected after the customer cancels the payment.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "ID of the PayPal merchant account that will be used when charging this payment method.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "payerInfo", + "description": "The payer's information.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "LocalPaymentPayerInfoInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "expiryDate", + "description": "Overrides the default date at which the local payment context will expire. MULTIBANCO is not overridable.", + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "NonInstantLocalPaymentMethodType", + "description": "A value identifying the type of non-instant regional payment method.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "BOLETOBANCARIO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MULTIBANCO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OXXO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "OAuthApplication", + "description": "Information about an OAuth Application.", + "fields": [ + { + "name": "clientId", + "description": "The unique identifier of the OAuth application.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "The name of the OAuth application.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "OAuthTokenType", + "description": "OAuth access token type.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "BEARER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "OwnerAddressType", + "description": "The owner's address type.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "HOME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MAILING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "OwnerIDType", + "description": "The type of identity number provided for the owner.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SOCIAL_SECURITY_NUMBER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "OwnerPhoneType", + "description": "The owner's phone type.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "HOME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MOBILE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "OwnerPosition", + "description": "The position that the owner holds in the business.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "BENEFICIAL_OWNER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CHAIRMAN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DIRECTOR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PARTNER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SECRETARY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TREASURER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "OwnerRole", + "description": "The role that the owner holds in the business.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "BENEFICIAL_OWNER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SIGNIFICANT_RESPONSIBILITY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PageInfo", + "description": "The page information for a connection.", + "fields": [ + { + "name": "hasNextPage", + "description": "Whether or not there is a next page available.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasPreviousPage", + "description": "Always false; backwards pagination is not supported. Present to comply with Relay specifications.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "startCursor", + "description": "The cursor for the first item in the connection page.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "endCursor", + "description": "The cursor for the last item in the connection page.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PairInStoreReaderInput", + "description": "Input fields for pairing an in store reader.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "userCode", + "description": "Code displayed on Reader during pairing.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "reader", + "description": "Inputs for Reader.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InStoreReaderSetupInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ParentAuthorization", + "description": "An original authorization's relationship to all its partial capture transactions.", + "fields": [ + { + "name": "childCaptures", + "description": "The captures on a partially captured authorization.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalAmountAuthorized", + "description": "The total amount authorized by this transaction. This amount will not change as this transaction is partially captured.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "PartialCaptureDetails", + "description": "A union of all possible relationships of transactions involved in partial captures. If the transaction has been partially captured, this links to all its partial capture children; if the transaction represents a partial capture attempt, this links to the original parent authorization.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "ChildCapture", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ParentAuthorization", + "ofType": null + } + ] + }, + { + "kind": "INPUT_OBJECT", + "name": "PartialCaptureTransactionInput", + "description": "Top-level input fields for capturing outstanding funds authorized by a transaction.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transactionId", + "description": "ID of the original authorized transaction to be partially captured.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "transaction", + "description": "Input fields for the capture, with details that will define the resulting capture transaction.", + "type": { + "kind": "INPUT_OBJECT", + "name": "PartialCaptureTransactionOptionsInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PartialCaptureTransactionOptionsInput", + "description": "Input fields for the capture, with details that will define the resulting capture transaction.", + "fields": null, + "inputFields": [ + { + "name": "amount", + "description": "The amount to capture on the transaction against the parent authorization transaction. Must be greater than 0. You can perform multiple partial capture transactions as long as the cumulative amount of those transactions is less than or equal to the amount authorized by the parent transaction. You can't capture more than the authorized amount unless your industry and processor support settlement adjustment (capturing a certain percentage over the authorized amount); [contact us for assistance](https://help.braintreepayments.com?issue=TransactionProcessingQuestion).", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "discountAmount", + "description": "Discount amount that was included in the total transaction amount. Does not add to the total amount the payment method will be charged. This value can't be negative. Please note that this field is not on PayPal transactions.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "lineItems", + "description": "Line items for this transaction. Up to 249 line items may be specified.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionLineItemInput", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "orderId", + "description": "Additional information about the transaction. On PayPal transactions, this field maps to the PayPal invoice number. PayPal invoice numbers must be unique in your PayPal business account. Maximum 255 characters or 127 for PayPal transactions. If specified, this will update the existing order ID on the transaction.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "purchaseOrderNumber", + "description": "A purchase order identification value you associate with this transaction.\n\n*Required for Level 2 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "shipping", + "description": "Shipping information.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionShippingInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "tax", + "description": "Tax information about the transaction.\n\n*Required for Level 2 processing*.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionTaxInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "descriptor", + "description": "Fields used to define what will appear on a customer's bank statement for a specific purchase. If specified, this will update the existing descriptor on the transaction.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionDescriptorInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PartialCaptureTransactionPayload", + "description": "Top-level output field from partially capturing a transaction.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "capture", + "description": "The transaction representing the partial capture.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PayPalAccountDetails", + "description": "Details about a PayPal account.", + "fields": [ + { + "name": "billingAgreementId", + "description": "The ID of the billing agreement for this PayPal account.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "billingAddress", + "description": "The billing address associated with the PayPal account.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Address", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "shippingAddress", + "description": "The shipping address associated with the PayPal account.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Address", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": "The email address associated with the PayPal account.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "phone", + "description": "The primary phone number associated with the PayPal account.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payerId", + "description": "The PayPal ID of the PayPal account.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "firstName", + "description": "The first name on the PayPal account.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastName", + "description": "The last name on the PayPal account.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cobrandedCardLabel", + "description": "The label of the co-branded card used as a funding source.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "origin", + "description": "Additional information if the PayPal account was provided from a third-party origin, such as Apple Pay, Google Pay, or another digital wallet.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethodOrigin", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "limitedUseOrderId", + "description": "Limited use PayPal provided Order ID (starts with O-).", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PayPalAccountInput", + "description": "Input for identifying a PayPal account.", + "fields": null, + "inputFields": [ + { + "name": "payerId", + "description": "The unique PayPal ID of the PayPal account.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType" : null + }, + "defaultValue" : null + } + ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, + { + "kind" : "ENUM", + "name" : "PayPalBillingAgreementChargePattern", + "description" : "Expected business/pricing model for a billing agreement (Charge Patterns).", + "fields" : null, + "inputFields" : null, + "interfaces" : null, + "enumValues" : [ + { + "name" : "DEFERRED", + "description" : "Pay after use, non-recurring post-paid, variable amount, irregular.", + "isDeprecated" : false, + "deprecationReason" : null + }, + { + "name" : "IMMEDIATE", + "description" : "On-demand instant payments - non-recurring, pre-paid, variable amount.", + "isDeprecated" : false, + "deprecationReason" : null + }, + { + "name" : "RECURRING_POSTPAID", + "description" : "Pay on a fixed date based on usage or consumption after the goods/service is delivered.", + "isDeprecated" : false, + "deprecationReason" : null + }, + { + "name" : "RECURRING_PREPAID", + "description" : "Pay upfront fixed or variable amount on a fixed date before the goods/service is delivered.", + "isDeprecated" : false, + "deprecationReason" : null + }, + { + "name" : "THRESHOLD_POSTPAID", + "description" : "Charge payer when the set amount is reached or monthly billing cycle, whichever comes first, after the goods/service is delivered.", + "isDeprecated" : false, + "deprecationReason" : null + }, + { + "name" : "THRESHOLD_PREPAID", + "description" : "Charge payer when the set amount is reached or monthly billing cycle, whichever comes first, before the goods/service is delivered.", + "isDeprecated" : false, + "deprecationReason" : null + } + ], + "possibleTypes" : null + }, + { + "kind" : "INPUT_OBJECT", + "name" : "PayPalBillingAgreementExperienceProfileInput", + "description" : "Controls the experience in a PayPal billing agreement approval flow.", + "fields" : null, + "inputFields" : [ + { + "name" : "brandName", + "description" : "Merchant brand name to be displayed on the PayPal approval pages.", + "type" : { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "defaultValue" : null + }, + { + "name" : "collectShippingAddress", + "description" : "Indicates whether a shipping address will be collected from the customer during the agreement approval flow.", + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "defaultValue" : null + }, + { + "name" : "landingPageType", + "description" : "Specifies the PayPal page to display when a user lands on the PayPal site to complete the payment.", + "type" : { + "kind" : "ENUM", + "name" : "PayPalLandingPageType", + "ofType" : null + }, + "defaultValue" : null + }, + { + "name" : "locale", + "description" : "Locale of the PayPal payment approval experience.", + "type" : { + "kind" : "SCALAR", + "name" : "Language", + "ofType" : null + }, + "defaultValue" : null + }, + { + "name" : "shippingAddressEditable", + "description" : "Indicates whether to enable user editing of the shipping address. Only applies when shipping address is provided by merchant.", + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "defaultValue" : null + } + ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, + { + "kind" : "INPUT_OBJECT", + "name" : "PayPalBillingAgreementInput", + "description" : "Input fields for a PayPal account to be vaulted.", + "fields" : null, + "inputFields" : [ + { + "name" : "billingAgreementToken", + "description" : "The Billing Agreement token.", + "type" : { + "kind" : "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PayPalConfiguration", + "description": "Configuration for PayPal.", + "fields": [ + { + "name": "displayName", + "description": "The merchant's company name for displaying to customers in the PayPal UI.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientId", + "description": "The merchant's PayPal client ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "privacyUrl", + "description": "The merchant's privacy policy URL.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userAgreementUrl", + "description": "The merchant's user agreement URL.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "assetsUrl", + "description": "A URL pointing to the base path of Braintree's web pages used for various browser switches and popups.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "environment", + "description": "The PayPal environment.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PayPalEnvironment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "environmentNoNetwork", + "description": "For internal use only.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "This field is only included for internal testing purposes." + }, + { + "name": "unvettedMerchant", + "description": "Whether or not the merchant has been vetted.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "braintreeClientId", + "description": "Braintree's PayPal client ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "billingAgreementsEnabled", + "description": "Whether billing agreements are enabled and should be used instead of future payments.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchantAccountId", + "description": "The merchant account being used. This affects the currency code and other options.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "currencyCode", + "description": "The currency code to use.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CurrencyCodeAlpha", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payeeEmail", + "description": "The email address of the PayPal account that will receive the funds when a transaction is created.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directBaseUrl", + "description": "For internal use only.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "This field is only included for internal testing purposes." + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PayPalEnvironment", + "description": "The environment being used for PayPal.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CUSTOM", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIVE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OFFLINE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "custom", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "live", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "offline", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PayPalExperienceProfileInput", + "description": "Controls the experience in a PayPal approval flow.", + "fields": null, + "inputFields": [ + { + "name": "brandName", + "description": "Merchant brand name to be displayed on the PayPal approval pages.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "collectShippingAddress", + "description": "Indicates whether a shipping address will be collected from the customer during the agreement approval flow.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "landingPageType", + "description": "Specifies the PayPal page to display when a user lands on the PayPal site to complete the payment.", + "type": { + "kind": "ENUM", + "name": "PayPalLandingPageType", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "locale", + "description": "Locale of the PayPal payment approval experience.", + "type": { + "kind": "SCALAR", + "name": "Language", + "ofType": null + }, + "defaultValue" : null + }, + { + "name" : "shippingAddressEditable", + "description" : "Indicates whether to enable user editing of the shipping address. Only applies when shipping address is provided by merchant.", + "type" : { + "kind" : "SCALAR", + "name" : "Boolean", + "ofType" : null + }, + "defaultValue" : null + }, + { + "name" : "userAction", + "description" : "Presents the customer with either the Continue or Pay Now (COMMIT) checkout flow. Default is Continue flow if the field is not provided.", + "type" : { + "kind" : "ENUM", + "name" : "PayPalUserAction", + "ofType" : null + }, + "defaultValue" : null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PayPalFinancingCreditProductIdentifier", + "description": "Possible identifiers for credit products provided via PayPal.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CREDIT_CARD_INSTALLMENTS_BR", + "description": "Brazil Credit Card Installments.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_CARD_INSTALLMENTS_MX", + "description": "Mexico Credit Card Installments.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_CARD_US", + "description": "United States Credit Card.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAYPAL_CREDIT_DE", + "description": "Germany PayPal Credit.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAYPAL_CREDIT_UK", + "description": "United Kingdom PayPal Credit.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAYPAL_CREDIT_US", + "description": "United States PayPal Credit.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAY_LATER_FR", + "description": "France Pay Later.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAY_LATER_GB", + "description": "Great Britain Pay Later.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAY_LATER_US", + "description": "United States Pay Later.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAY_UPON_INVOICE_DE", + "description": "Germany Pay Upon Invoice.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PayPalFinancingOption", + "description": "PayPal financing options available for a transaction.", + "fields": [ + { + "name": "creditProductIdentifier", + "description": "The credit product identifier.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PayPalFinancingCreditProductIdentifier", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "qualifyingFinancingOptions", + "description": "Financing options the transaction qualifies for.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PayPalQualifyingFinancingOption", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PayPalFinancingOptionCreditType", + "description": "PayPal Financing option credit type.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "INSTALLMENT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NO_INTEREST", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAY_UPON_INVOICE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SAME_AS_CASH", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PayPalFinancingOptionsInput", + "description": "Input fields for requesting information about PayPal financing options.", + "fields": null, + "inputFields": [ + { + "name": "paymentMethodId", + "description": "ID of an existing multi-use PayPal payment method to request financing options for.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "The transaction currency and total amount to finance.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "MonetaryAmountInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "countryCode", + "description": "The financing country code.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CountryCodeAlpha2", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PayPalFinancingOptionsPayload", + "description": "PayPal financing options response payload.", + "fields": [ + { + "name": "financingOptions", + "description": "PayPal financing options.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PayPalFinancingOption", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PayPalIntent", + "description": "The intent for PayPal payments.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "AUTHORIZE", + "description": "Merchant will authorize the payment, but the funds will be captured separately.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ORDER", + "description": "Merchant will create a PayPal Order. This validates the transaction without an authorization (i.e. without holding funds). Useful for authorizing and capturing funds up to 90 days after the order has been placed.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SALE", + "description": "Merchant will authorize and captures funds simultaneously.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PayPalLandingPageType", + "description": "The type of landing page to display on the PayPal site for user checkout.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "BILLING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DEFAULT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LOGIN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PayPalLineItemInput", + "description": "Line items for a PayPal payment.", + "fields": null, + "inputFields": [ + { + "name": "name", + "description": "Item name. Maximum 127 characters.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "quantity", + "description": "Number of units of the item purchased. This value can't be negative or zero.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "unitAmount", + "description": "Per-unit price of the item. Can include up to 2 decimal places. This value can't be negative or zero.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "type", + "description": "Indicates whether the line item is a debit (sale) or credit (refund or discount) to the customer.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "TransactionLineItemType", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "description", + "description": "Item description. Maximum 127 characters.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "productCode", + "description": "Product or UPC code for the item. Maximum 127 characters.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "unitTaxAmount", + "description": "Per-unit tax price of the item. Can include up to 2 decimal places. This value can't be negative or zero.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "url", + "description": "The URL to product information.", + "type": { + "kind": "SCALAR", + "name": "URL", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PayPalLocalPaymentOriginDetails", + "description": "Additional information about the local payment method specific to PayPal.", + "fields": [ + { + "name": "captureId", + "description": "If funds for the transaction have settled, the PayPal ID for the capture of funds.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customField", + "description": "A string of field/value pairs passed directly to PayPal.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentId", + "description": "The identification value of the payment within PayPal's API.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "transactionFee", + "description": "The fee charged by PayPal for the transaction.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PayPalLocalPaymentRefundDetails", + "description": "PayPal local payment specific refund details.", + "fields": [ + { + "name": "refundId", + "description": "The PayPal refund ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refundedFee", + "description": "Refunded transaction fee charged by PayPal.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PayPalOneTimePaymentInput", + "description": "Input fields for a PayPal account for a One-Time payment.", + "fields": null, + "inputFields": [ + { + "name": "payerId", + "description": "The PayPal payer ID.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "paymentId", + "description": "The PayPal payment ID. This ID is prefixed with \"PAYID-\".", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "paymentToken", + "description": "The PayPal payment token, also known as an Express Checkout token. This token is prefixed with \"EC-\".", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PayPalPayeeOptionsInput", + "description": "Input fields for a PayPal account receiving transaction funds.", + "fields": null, + "inputFields": [ + { + "name": "email", + "description": "The email address associated with the payee PayPal account.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType" : null + }, + "defaultValue" : null + } + ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, + { + "kind" : "INPUT_OBJECT", + "name" : "PayPalProductAttributesInput", + "description" : "Product attributes input for PayPal billing agreement.", + "fields" : null, + "inputFields" : [ + { + "name" : "paypalBillingAgreementChargePattern", + "description" : "Expected business/pricing model for a billing agreement (Charge Patterns).", + "type" : { + "kind" : "ENUM", + "name" : "PayPalBillingAgreementChargePattern", + "ofType" : null + }, + "defaultValue" : null + } + ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, + { + "kind" : "OBJECT", + "name" : "PayPalQualifyingFinancingOption", + "description" : "PayPal qualifying financing options for a product.", + "fields" : [ + { + "name" : "apr", + "description" : "APR percentage.", + "args" : [], + "type" : { + "kind" : "SCALAR", + "name": "Percentage", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nominalRate", + "description": "Nominal rate percentage.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Percentage", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "term", + "description": "Total number of payments over which to finance the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "intervalDuration", + "description": "The duration between each interval or payment.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Duration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "countryCode", + "description": "The country or region for the financing option.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CountryCodeAlpha2", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "creditType", + "description": "Credit type.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PayPalFinancingOptionCreditType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "minimumAmount", + "description": "The minimum qualifying amount for a transaction.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "monthlyInterestRate", + "description": "The monthly interest rate for this financing option.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Percentage", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "periodicPayment", + "description": "The amount for transaction periodic payments.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "monthlyPayment", + "description": "The amount for transaction monthly payments.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discountAmount", + "description": "The discount amount on the transaction for this financing option.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discountPercentage", + "description": "The discount percentage for this financing option.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Percentage", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalInterest", + "description": "The total interest cost for this financing option.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCost", + "description": "The total amount for the transaction, including interest.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paypalSubsidized", + "description": "Indicates whether the financing option's credit fee is funded by PayPal.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PayPalRefundDetails", + "description": "PayPal-specific refund details.", + "fields": [ + { + "name": "refundId", + "description": "The PayPal refund ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refundedFee", + "description": "Refunded transaction fee charged by PayPal.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": "The description of this refund.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reason", + "description": "The reason this refund was created.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PayPalRetailAppUsedForScanning", + "description": "The app used to scan an in-store QR code.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VENMO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PayPalShippingOptionInput", + "description": "A shipping option for a PayPal One-Time payment.", + "fields": null, + "inputFields": [ + { + "name": "amount", + "description": "The cost for this shipping option.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "MonetaryAmountInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "id", + "description": "A unique ID that identifies a shipping option.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "description", + "description": "The shipping option description. Localize this description to the payer's locale. For example, `Free Shipping`, `USPS Priority Shipping`, `Expédition prioritaire USPS`, or `USPS yōuxiān fā huò`.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "selected", + "description": "Indicates which shipping option is selected by default when the payer views the shipping options within the PayPal checkout experience. Only one shipping option can be selected at a time.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "type", + "description": "The method by which the payer wants to receive their items.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PayPalShippingOptionType", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PayPalShippingOptionType", + "description": "The method by which the payer wants to receive their items.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "PICKUP", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SHIPPING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PayPalTransactionDetails", + "description": "PayPal-specific details on a transaction.", + "fields": [ + { + "name": "authorizationId", + "description": "If the transaction was successfully authorized, the PayPal ID for the authorization.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "captureId", + "description": "If funds for the transaction have settled, the PayPal ID for the capture of funds.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customField", + "description": "A string of field/value pairs passed directly to PayPal.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payer", + "description": "Details about the payer or owner of the PayPal account.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PayPalAccountDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payee", + "description": "Details about the PayPal account that received the funds.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PayPalAccountDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payerStatus", + "description": "Whether or not the PayPal account has been verified by PayPal.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentId", + "description": "The identification value of the payment within PayPal's API.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refundId", + "description": "If the transaction is a refund, the PayPal refund ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "This field will never be populated as it only appears on refunds. Use `details.paypalId` on a refund instead." + }, + { + "name": "sellerProtectionStatus", + "description": "Whether or not the transaction qualifies for PayPal Seller Protection.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "taxId", + "description": "Payer's tax ID. Only returned for payments from Brazilian accounts.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "taxIdType", + "description": "Payer's tax ID type. Only returned for payments from Brazilian accounts. Allowed values BR_CPF or BR_CNPJ.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "transactionFee", + "description": "The fee charged by PayPal for the transaction.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "transactionFeeAmount", + "description": "The fee charged by PayPal for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `transactionFee.value` instead." + }, + { + "name": "transactionFeeCurrencyIsoCode", + "description": "The currency code for the currency of the PayPal transaction fee.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `transactionFee.currencyCode` instead." + }, + { + "name": "description", + "description": "Description of the transaction that is displayed to customers in PayPal email receipts.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "origin", + "description": "Additional information if the credit card was provided from a third-party origin, such as Google Pay.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethodOrigin", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "selectedFinancingOption", + "description": "Buyer selected financing option at the time of creating a transaction.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "SelectedPayPalFinancingOptionDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "appUsedForScanning", + "description": "The application used by the payer to scan the QR code.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PayPalRetailAppUsedForScanning", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PayPalTransactionPayload", + "description": "Top-level output field from creating a PayPal transaction.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "transaction", + "description": "The transaction representing the charge on the payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "billingAgreementWithPurchasePaymentMethod", + "description": "If the paymentMethodId passed to this mutation was a single-use PayPal payment method created with the [Billing Agreement with Purchase flow](https://developers.braintreepayments.com/guides/paypal/checkout-with-paypal/javascript/v3#checkout-using-paypal-billing-agreement-with-purchase-flow), then this field will be populated with a multi-use PayPal payment method created alongside the transaction. Otherwise, this will be null.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } + ], + "inputFields" : null, + "interfaces" : [], + "enumValues" : null, + "possibleTypes" : null + }, + { + "kind" : "ENUM", + "name" : "PayPalUserAction", + "description" : "PayPal User action type.", + "fields" : null, + "inputFields" : null, + "interfaces" : null, + "enumValues" : [ + { + "name" : "COMMIT", + "description" : null, + "isDeprecated" : false, + "deprecationReason" : null + }, + { + "name" : "CONTINUE", + "description" : null, + "isDeprecated" : false, + "deprecationReason" : null + } + ], + "possibleTypes" : null + }, + { + "kind" : "INTERFACE", + "name" : "Payment", + "description" : "A merchant-initiated movement of money between the merchant and a customer, by way of a payment method. Payments can represent money moving either from a customer to the merchant by charging a payment method (a Transaction), or from the merchant back to a customer by refunding a previous transaction (a Refund).", + "fields" : [ + { + "name" : "id", + "description" : "Unique identifier.", + "args" : [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legacyId", + "description": "Legacy unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date and time when the payment was created.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount charged or credited to the payment method. Note that in the case of a Transaction, this amount will represent the amount moving from the customer to the merchant, and in the case of a Refund, will represent the amount moving from the merchant back to the customer.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "orderId", + "description": "The order ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The current status of this payment.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "statusHistory", + "description": "The records of all statuses this payment has passed through, with additional information on why each status occurred. Returned in reverse chronological order, with the most recent event first in the list.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchantAccountId", + "description": "The ID of the merchant account that processed this payment.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "How the payment was created.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethodSnapshot", + "description": "Snapshot of payment method details used to create the payment, preserved at the time the transaction was created. This will always be present.", + "args": [], + "type": { + "kind": "UNION", + "name": "PaymentMethodSnapshot", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Refund", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "PaymentConnection", + "description": "A paginated list of transactions and refunds.", + "fields": [ + { + "name": "edges", + "description": "A list of transactions and refunds.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PaymentConnectionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information about the page of transactions and refunds contained in `edges`.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PaymentConnectionEdge", + "description": "A transaction or refund within a PaymentConnection.", + "fields": [ + { + "name": "cursor", + "description": "This transaction or refund's location within the PaymentConnection. Used for requesting additional pages.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The transaction or refund.", + "args": [], + "type": { + "kind": "INTERFACE", + "name": "Payment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "PaymentContext", + "description": "Context associated with a transaction.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date and time when the payment context was created.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": "Date and time when the payment context was updated.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "CustomActionsPaymentContext", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "LocalPaymentContext", + "ofType": null + } + ] + }, + { + "kind": "ENUM", + "name": "PaymentInitiator", + "description": "The initiator of the payment. Payment can either be merchant-initiated or customer-initiated.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "MOTO", + "description": "Transactions that are initiated by the customer via the merchant by mail or telephone.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RECURRING", + "description": "Transactions that are initiated by the merchant for subsequent recurring payments (e.g. subscriptions with a fixed amount on a predefined schedule).", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RECURRING_FIRST", + "description": "Transactions initiated by the customer that represent the first in a series of recurring payments or subscription.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNSCHEDULED", + "description": "Transactions that are initiated by the merchant for unscheduled payments that are not recurring on a predefined schedule or amount (e.g. balance top-up).", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PaymentLevelFeeReport", + "description": "The [payment-level fee report (formerly known as the transaction-level fee report)](https://articles.braintreepayments.com/control-panel/reporting/transaction-level-fee-report) provides a breakdown of fees per individual payments (encompassing transactions and refunds).", + "fields": [ + { + "name": "url", + "description": "The URL where the generated report is stored. Download the report from this URL.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PaymentMethod", + "description": "Top-level field representing a payment method.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legacyId", + "description": "Legacy unique identifier. May be the same as ID for single-use payment methods.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "usage", + "description": "Whether a payment method may be used only once or multiple times.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentMethodUsage", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date and time when the payment method was vaulted.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "details", + "description": "Details about the payment method specific to the type (e.g. credit card, PayPal account).", + "args": [], + "type": { + "kind": "UNION", + "name": "PaymentMethodDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "verifications", + "description": "A paginated list of verifications that have been run against the payment method.", + "args": [ + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VerificationConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customer", + "description": "The customer that the payment method belongs to.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Customer", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PaymentMethodConnection", + "description": "A paginated list of payment methods.", + "fields": [ + { + "name": "edges", + "description": "A list of payment methods.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PaymentMethodConnectionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information about the page of payment methods contained in `edges`.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PaymentMethodConnectionEdge", + "description": "A payment method within a PaymentMethodConnection.", + "fields": [ + { + "name": "cursor", + "description": "This payment method's location within the PaymentMethodConnection. Used for requesting additional pages.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PaymentMethodDeletionInitiator", + "description": "Initiator of a payment method delete request.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CUSTOMER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MERCHANT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "PaymentMethodDetails", + "description": "A union of all possible payment method details. PaymentMethodDetails contain information for display purposes, payment method management, and processing.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "CustomActionsPaymentMethodDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CreditCardDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "PayPalAccountDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SamsungPayCardDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "VenmoAccountDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "UsBankAccountDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SEPADirectDebitAccountDetails", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "PaymentMethodOrigin", + "description": "Information about how the customer provided a payment method, such as via a digital wallet.", + "fields": [ + { + "name": "type", + "description": "An enum identifying the origin of the payment method.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentMethodOriginType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "details", + "description": "When available, additional details specific to the origin.", + "args": [], + "type": { + "kind": "UNION", + "name": "PaymentMethodOriginDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "PaymentMethodOriginDetails", + "description": "A union of all possible payment method origin details. PaymentMethodOriginDetails contain additional information specific to the third party the payment method was provided by.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "ApplePayOriginDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GooglePayOriginDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "MasterpassOriginDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "NetworkTokenOriginDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SamsungPayOriginDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "VisaCheckoutOriginDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "PayPalLocalPaymentOriginDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CardPresentOriginDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "EmvCardOriginDetails", + "ofType": null + } + ] + }, + { + "kind": "ENUM", + "name": "PaymentMethodOriginType", + "description": "A value identifying the third-party origin from which a customer provided their payment method.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "APPLE_PAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GOOGLE_PAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "IN_STORE_READER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MASTERPASS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NETWORK_TOKEN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SAMSUNG_PAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VISA_CHECKOUT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "PaymentMethodSnapshot", + "description": "A union of all possible payment method details as they were used in a transaction or verification. PaymentMethodSnapshot preserves values used to create a given transaction or verify a payment method at that moment in time.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "CustomActionsPaymentMethodDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CreditCardDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "PayPalTransactionDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "VenmoAccountDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "UsBankAccountDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "LocalPaymentDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CreditCardTransactionDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SEPADirectDebitTransactionDetails", + "ofType": null + } + ] + }, + { + "kind": "ENUM", + "name": "PaymentMethodSnapshotSearchType", + "description": "A value identifying the type of payment method used for a transaction. For certain payment methods such as credit cards, this value also encodes the origin from which a customer provided that payment method.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ALIPAY_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "BANCONTACT_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "BLIK_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "BOLETOBANCARIO_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_CARD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_CARD_VIA_APPLE_PAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_CARD_VIA_GOOGLE_PAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_CARD_VIA_MASTERPASS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_CARD_VIA_NETWORK_TOKEN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_CARD_VIA_SAMSUNG_PAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CREDIT_CARD_VIA_VISA_CHECKOUT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "EPS_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GIROPAY_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GRABPAY_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "IDEAL_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LOCAL_PAYMENT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MULTIBANCO_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MYBANK_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OXXO_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "P24_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAYU_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAY_UPON_INVOICE_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SATISPAY_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SEPA_DIRECT_DEBIT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SEPA_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SOFORT_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SWISH_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TRUSTLY_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "US_BANK_ACCOUNT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VENMO_ACCOUNT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VERKKOPANKKI_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VIPPS_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "WECHAT_PAY_VIA_PAYPAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PaymentMethodUsage", + "description": "Possible usages for payment methods.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "MULTI_USE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SINGLE_USE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PaymentMethodVerificationOptionsInput", + "description": "Input fields that specify options for verifying the vaulted payment method. Only applicable for payment method types that suport verification.", + "fields": null, + "inputFields": [ + { + "name": "merchantAccountId", + "description": "ID of the merchant account to use when verifying the payment method. The verification will use the default merchant account if this field is left blank.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "skip", + "description": "Whether to opt out of verifying the payment method. Defaults to `false` for payment methods that support verification. Clients should only pass `true` in the uncommon scenario that the payment method has been verified externally to Braintree.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PaymentNetworkResponse", + "description": "The network response. When present, this field can provide additional detail about why an authorization or verification was declined, but the processorResponse should be considered the source of truth.", + "fields": [ + { + "name": "code", + "description": "The network response code for [authorizations](https://developers.braintreepayments.com/reference/response/transaction/#network-response-codes) or [verifications](https://developers.braintreepayments.com/reference/response/credit-card-verification#network-response-codes).", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": "The network response text.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PaymentReaderInputMode", + "description": "The input mode used on the payment reader to facilitate an in-store transaction.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CONTACT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CONTACTLESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MAGSTRIPE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MAGSTRIPE_FALLBACK", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MANUAL_KEY_ENTRY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VAULT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PaymentSearchInput", + "description": "Input fields for searching for any type of Payment.", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "Find payments with an ID or IDs.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "type", + "description": "Find payments by their type. Use this field to search for payments by the direction of money movement.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentTypeInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "status", + "description": "Find payments with a given status.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentStatusInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "statusTransition", + "description": "Find payments based on the time at which they transitioned to a given status.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentStatusTransitionInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "createdAt", + "description": "Find payments based on the time they were created.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTimestampInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "Find payments for a given amount or currency.", + "type": { + "kind": "INPUT_OBJECT", + "name": "MonetaryAmountSearchInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "orderId", + "description": "Find payments with a given orderId.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "Find payments processed through a merchant account ID or IDs.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customer", + "description": "Find payments with a given customer.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentCustomerInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "disbursementDate", + "description": "Find payments by their disbursement date. Only use this search criteria if you have an eligible merchant account. Note that payments can only be disbursed after they reach the SETTLED status.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchDateInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "source", + "description": "Find payments created with a given source.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentSourceInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "settlementBatchId", + "description": "Find payments by the batch ID under which the payment was submitted for settlement.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethod", + "description": "Find payments based on information about the payment method used for the payment.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentPaymentMethodInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "facilitatorOAuthApplicationClientId", + "description": "Find payments created by a third party via the Grant API using a given OAuth application client ID.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "userId", + "description": "Find payments with a user ID or IDs.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "storeId", + "description": "Find payments by the ID of the store that the transaction was processed in.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PaymentSearchType", + "description": "The type of a Payment, based primarily on implementing type.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "DETACHED_REFUND", + "description": "Only use this field if you have processed [detached credits](https://articles.braintreepayments.com/control-panel/transactions/refunds-voids-credits#detached-credits). The payment is a Refund, and represents a refund of a transaction not processed through your Braintree account.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "REFUND", + "description": "The payment is a Refund, and represents a refund of a transaction present in this Braintree account. Unless you have processed any [detached credits](https://articles.braintreepayments.com/control-panel/transactions/refunds-voids-credits#detached-credits), this type encompasses all refunds.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TRANSACTION", + "description": "The payment is a Transaction.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PaymentSource", + "description": "The origin of a request that created or changed a transaction or refund.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "API", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CONTROL_PANEL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAYMENT_READER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RECURRING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNKNOWN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "PaymentStatus", + "description": "The status of the payment, indicating its success or failure, and where it is in its [lifecycle](https://articles.braintreepayments.com/get-started/transaction-lifecycle). For further details on why any given status occurred, consult the corresponding `PaymentStatusEvent` in the payment's `statusHistory`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "AUTHORIZATION_EXPIRED", + "description": "The transaction spent too much time in the `AUTHORIZED` status and was marked as expired. Expiration [time frames](https://developers.braintreepayments.com/reference/general/statuses#authorization-expired) differ by card type, transaction type, and, in some cases, merchant category.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AUTHORIZED", + "description": "The processor authorized the transaction, putting your customer's funds on hold. Your customer may see a pending charge on his or her account. However, before the customer is actually charged and before you receive the funds, you must use the `captureTransaction` mutation. If you do not want to capture the transaction, you should use the `reverseTransaction` mutation to avoid a misuse of authorization fee.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AUTHORIZING", + "description": "If a payment remains in a status of `AUTHORIZING`, [contact us for assistance](https://help.braintreepayments.com?issue=TransactionProcessingQuestion).", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FAILED", + "description": "An error occurred when sending the payment to the downstream processor. See the payment's `statusHistory` for the exact error.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GATEWAY_REJECTED", + "description": "The transaction was [rejected](https://articles.braintreepayments.com/control-panel/transactions/gateway-rejections) based on one or more settings or rules in your Braintree gateway. See the transaction's `statusHistory` to determine which resulted in the decline.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROCESSOR_DECLINED", + "description": "The processor declined the transaction while attempting to authorize it. See the transaction's `statusHistory` to determine what reason the processor gave for the decline.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SETTLED", + "description": "The payment has been settled. For transactions, this means your customer has been charged and the process of disbursing the funds to your bank account has begun. For refunds, it means that the process of disbursing funds back to the customer has begun.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SETTLEMENT_CONFIRMED", + "description": "The transaction was captured partially and will not be submitted to processor for settling. Its child transaction(s) has been successfully captured and will be included in the next settlement batch.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SETTLEMENT_DECLINED", + "description": "The processor declined the payment while attempting to capture it. See the payment's `statusHistory` to determine why it wasn't settled. This status is rare, and only certain types of transactions can be affected.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SETTLEMENT_PENDING", + "description": "The transaction has not yet fully settled. This status is rare, and will generally resolve to a status of `SETTLED`. Only certain types of transactions can be affected.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SETTLING", + "description": "The payment is in the process of being settled. This is a transitory state, and will resolve to a status of `SETTLED`.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBMITTED_FOR_SETTLEMENT", + "description": "The payment has been successfully captured, and will be included in the next settlement batch, at which time it will become settled.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VOIDED", + "description": "The payment has been voided or canceled. For transactions, this means it's no longer authorized, your customer's funds are no longer on hold, and you can't use the `captureTransaction` mutation on this transaction. For refunds, it means the customer will not receive the funds from the refund.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "description": "Status event in the [lifecycle of a payment](https://articles.braintreepayments.com/get-started/transaction-lifecycle).", + "fields": [ + { + "name": "status", + "description": "New status of the payment.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the status event occurred.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The payment amount applicable to the status. For instance, the amount when a transaction is `SUBMITTED_FOR_SETTLEMENT` might be less than the amount for which it was `AUTHORIZED`.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "Source that caused the status event to occur.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminal", + "description": "Whether this is the final state for the payment. If false, this transaction will pass into another subsequent state.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "AuthorizationExpiredEvent", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "AuthorizedEvent", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "FailedEvent", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "GatewayRejectedEvent", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "ProcessorDeclinedEvent", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SettledEvent", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SettlementConfirmedEvent", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SettlementDeclinedEvent", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SettlementPendingEvent", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SettlingEvent", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SubmittedForSettlementEvent", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "VoidedEvent", + "ofType": null + } + ] + }, + { + "kind": "SCALAR", + "name": "Percentage", + "description": "The percentage, as a fixed-point, signed decimal number. For example, define a 19.99% interest rate as `19.99`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PerformThreeDSecureLookupInput", + "description": "Top-level fields for performing a 3D Secure Lookup.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "ID of the merchant account that will be used when charging the payment method.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "dfReferenceId", + "description": "Reference ID used by our MPI provider CardinalCommerce to connect the lookup request to the device data that was previously collected.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of a payment method to perform the lookup on.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "The amount you plan to charge the payment method after the 3D Secure authentication.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "transactionInformation", + "description": "Additional information about the transaction when authenticating through 3D Secure.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecureLookupTransactionInformationInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "cardholderInformation", + "description": "Additional information about the cardholder when authenticating through 3D Secure.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecureLookupCardholderInformationInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "requestAuthenticationChallenge", + "description": "When set to true, requests a 3D Secure authentication challenge from the issuer. A challenge will result in the acsUrl field being populated on the response, requiring you to open the challenge on the client side.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "clientInformation", + "description": "Information about the client-side lookup process.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecureLookupClientInformationInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "dataOnlyRequested", + "description": "When set to true, the data-only 3D Secure call will be created. The status of [DATA_ONLY_SUCCESSFUL](https://developers.braintreepayments.com/guides/3d-secure/advanced-options#using-data-only-3d-secure) will be returned as `ThreeDSecureAuthenticationStatus` for a successful response.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "cardAdd", + "description": "If set to true, a card-add challenge will be requested from the issuer to confirm adding new card to the merchant's vault. This flag should only be used when adding a card to a merchant’s vault and not for creating transactions.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PerformThreeDSecureLookupPayload", + "description": "Top-level fields returned when performing a 3D Secure Lookup.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "threeDSecureLookupData", + "description": "Data fields containing information from the MPI provider about the 3D Secure Lookup result.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ThreeDSecureLookupData", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethod", + "description": "A single-use payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "PhoneInput", + "description": "The phone number in its international [E.164 numbering plan format](https://www.itu.int/rec/T-REC-E.164/en).", + "fields": null, + "inputFields": [ + { + "name": "countryPhoneCode", + "description": "The country calling code (CC), in its canonical international [E.164 numbering\nplan format](https://www.itu.int/rec/T-REC-E.164/en). The combined length of\nthe CC and the national number must not be greater than 15 digits. The\nnational number consists of a national destination code (NDC) and subscriber number (SN).", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "phoneNumber", + "description": "The phone number, in its canonical international [E.164 numbering plan\nformat](https://www.itu.int/rec/T-REC-E.164/en). The combined length of the\ncountry calling code (CC) and the national number must not be greater than 15\ndigits. The national number consists of a national destination code (NDC) and\nsubscriber number (SN).", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "extensionNumber", + "description": "The extension number.", + "type": { + "kind" : "SCALAR", + "name" : "String", + "ofType" : null + }, + "defaultValue" : null + } + ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, + { + "kind" : "ENUM", + "name" : "PreDisputeProgram", + "description" : "The pre-dispute program of the dispute.", + "fields" : null, + "inputFields" : null, + "interfaces" : null, + "enumValues" : [ + { + "name" : "NONE", + "description" : "The dispute does not have a pre-dispute program.", + "isDeprecated" : false, + "deprecationReason" : null + }, + { + "name" : "VISA_RDR", + "description" : "The dispute is part of the Visa Rapid Dispute Resolution (RDR) program.", + "isDeprecated" : false, + "deprecationReason" : null + } + ], + "possibleTypes" : null + }, + { + "kind" : "ENUM", + "name" : "ProcessorDeclineType", + "description" : "Whether the decline is likely to be temporary or persistent. Can be taken into consideration when determining whether to retry a declined charge.", + "fields" : null, + "inputFields" : null, + "interfaces" : null, + "enumValues" : [ + { + "name": "HARD", + "description": "Hard declines are the result of an error or issue which can't be resolved immediately; the decline is not temporary and subsequent charges on the same payment method will likely not be successful.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SOFT", + "description": "Soft declines result from a temporary issue and can be retried; subsequent charges on the same payment method may be successful.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ProcessorDeclinedEvent", + "description": "Accompanying information for a processor declined transaction.", + "fields": [ + { + "name": "status", + "description": "The new status of the transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the transaction was declined by the processor.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount of the transaction for this status event.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "The source for the transaction change to the new status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "declineType", + "description": "Whether or not the decline is the result of a temporary issue.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ProcessorDeclineType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processorResponse", + "description": "Fields describing the payment processor response and why they declined the transaction.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionAuthorizationProcessorResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "networkResponse", + "description": "Fields describing the network response to the authorization request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentNetworkResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "riskDecision", + "description": "Risk decision for this transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "RiskDecision", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminal", + "description": "Whether or not this is the final state for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Query", + "description": "The top-level Query type. Queries are used to fetch data.", + "fields": [ + { + "name": "ping", + "description": "Returns the literal string 'pong'.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pingInStoreReader", + "description": "Triggers a beep on a connected Reader and returns the Reader information or an error if unable to ping the device.", + "args": [ + { + "name": "readerId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreReader", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "viewer", + "description": "The currently authenticated viewer.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Viewer", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientConfiguration", + "description": "The client-side environment and payment method configuration.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ClientConfiguration", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "Fetch any object that extends the Node interface using its ID.", + "args": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "idFromLegacyId", + "description": "Get a GraphQL ID from a legacy ID that was returned from an SDK or a legacyId field. Does not verify existence except for payment methods.", + "args": [ + { + "name": "legacyId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "type", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "LegacyIdType", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "report", + "description": "A collection of the available reports. Each field on the `Report` type is a different report that can be queried with its own input parameters.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Report", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "search", + "description": "A collection of the available searches. Each field on the `Search` type is a different search that can be queried with its own input parameters.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Search", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paypalFinancingOptions", + "description": "Retrieve PayPal financing options that include payment installment plans.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PayPalFinancingOptionsInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PayPalFinancingOptionsPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inStoreLocations", + "description": "Retrieve a paginated list of all in-store locations.", + "args": [ + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreLocationConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ReaderStatus", + "description": "Indicates the status of a Reader.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "OFFLINE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ONLINE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "RecurringType", + "description": "The type of recurring payment a transaction represents.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "FIRST", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSEQUENT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNSCHEDULED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Refund", + "description": "A refund of a charge on a payment method, representing an attempt to send money from a previous transaction back to the customer.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legacyId", + "description": "Legacy unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date and time when the refund was created.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount that will be refunded, which can be less than or equal to the original charge amount.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "orderId", + "description": "The order ID for this refund. For PayPal transactions, the PayPal Invoice ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The current status of this refund.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "statusHistory", + "description": "The records of all statuses this refund has passed through, with additional information on why each status occurred. Returned in reverse chronological order, with the most recent event first in the list.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "details", + "description": "Payment method specific details about the refund.", + "args": [], + "type": { + "kind": "UNION", + "name": "RefundPaymentMethodDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchantAccountId", + "description": "The ID of the merchant account that processed this refund.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "How the refund was created.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refundedTransaction", + "description": "The original transaction that this refunds. If this is not present, then this refund represents a refund of a transaction that does not belong to this Braintree gateway account.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethodSnapshot", + "description": "Snapshot of payment method details that will receive the refund, typically based on the original transaction. This will always be present. Equivalent to `refundedTransaction.paymentMethodSnapshot`.", + "args": [], + "type": { + "kind": "UNION", + "name": "PaymentMethodSnapshot", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethod", + "description": "The multi-use payment method that will receive the refund. Only present if a multi-use payment method was used to create the original transaction and it has not been since deleted. The details of this PaymentMethod may have changed since the transaction was created; details used for the transaction can be found in the `paymentMethodSnapshot` field. Equivalent to `refundedTransaction.paymentMethod` (if present).", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customer", + "description": "The customer that the vaulted payment method (if it exists) belongs to. Equivalent to `refundedTransaction.customer` (if present).", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Customer", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lineItems", + "description": "Line items for this refund.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TransactionLineItem", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customFields", + "description": "Collection of custom field/value pairs passed when creating the refund. Custom fields are [defined in the Control Panel](https://articles.braintreepayments.com/control-panel/custom-fields#store-and-pass-back-fields). For all refunds except \"detached refunds\", these will always be null.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CustomField", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "descriptor", + "description": "Fields used to define what will appear on a customer's statement (for instance, credit card or bank statement) for this refund. This will always match the descriptor from the refunded transaction (if present).", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionDescriptor", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason" : null + }, + { + "name" : "disbursementDetails", + "description" : "The disbursement details associated with this refund. This field is only available after the refund is SETTLED and if you have an eligible merchant account.", + "args" : [], + "type" : { + "kind" : "OBJECT", + "name" : "DisbursementDetails", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + }, + { + "name" : "paymentInitiatedAt", + "description" : "The refund date and time as reported by the in-store payment terminal.", + "args" : [], + "type" : { + "kind" : "SCALAR", + "name" : "Timestamp", + "ofType" : null + }, + "isDeprecated" : false, + "deprecationReason" : null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Payment", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RefundConnection", + "description": "A paginated list of refunds.", + "fields": [ + { + "name": "edges", + "description": "A list of refunds.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RefundConnectionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information about the page of refunds contained in `edges`.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RefundConnectionEdge", + "description": "A transaction within a RefundConnection.", + "fields": [ + { + "name": "cursor", + "description": "This refund's location within the RefundConnection. Used for requesting additional pages.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The refund.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Refund", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RefundCreditCardInput", + "description": "Top-level input fields for creating a detached refund on a credit card.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of the credit card to be refunded.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "refund", + "description": "Input fields containing details about the refund.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DetachedRefundInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RefundCreditCardPayload", + "description": "Top-level output field from creating a detached refund for a credit card.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refund", + "description": "The information about the created refund.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Refund", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RefundInput", + "description": "Specific input fields for describing a refund.", + "fields": null, + "inputFields": [ + { + "name": "amount", + "description": "The amount to refund. Must be less than or equal to the amount of the original transaction. Defaults to the total amount of the original transaction.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "orderId", + "description": "The refund's order ID. Defaults to the order ID of the original transaction.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "ID of the merchant account that will be used when performing the refund.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "description", + "description": "Description of the refund that is displayed to customers in PayPal email receipts.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "reason", + "description": "Reason of the refund transaction. This field maps to the PayPal refund reason.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "lineItems", + "description": "Line items for this refund. Up to 249 line items may be specified.\n\nOnly allowed for Custom Actions transactions.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionLineItemInput", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "RefundPaymentMethodDetails", + "description": "A union of all possible payment method refund details.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "PayPalRefundDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "PayPalLocalPaymentRefundDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "SEPADirectDebitRefundDetails", + "ofType": null + } + ] + }, + { + "kind": "ENUM", + "name": "RefundPolicy", + "description": "Supported refund policies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "EXCHANGE_ONLY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NO_REFUND_OR_EXCHANGE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "REFUND_CARDHOLDER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RefundSearchInput", + "description": "Input fields for searching for refunds.", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "Find refunds with an ID or IDs.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "status", + "description": "Find refunds with the given status.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTransactionStatusInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "statusTransition", + "description": "Find payments based on the time at which they transitioned to a given status.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentStatusTransitionInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "createdAt", + "description": "Find refunds based on the time they were created.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTimestampInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "Find refunds with a given amount or currency.", + "type": { + "kind": "INPUT_OBJECT", + "name": "MonetaryAmountSearchInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "orderId", + "description": "Find refunds with a given orderId.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "Find refunds processed through a merchant account ID or IDs. In most cases, this will be the merchant account of the original refunded transaction.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customer", + "description": "Find refunds with a given customer.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentCustomerInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "disbursementDate", + "description": "Find refunds by their disbursement date. Only use this search criteria if you have an eligible merchant account. Note that refunds can only be disbursed after they reach the SETTLED status.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchDateInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "source", + "description": "Find refunds created with a given source.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentSourceInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "settlementBatchId", + "description": "Find refunds by the batch ID under which the refund was submitted for settlement.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethod", + "description": "Find refunds based on information about the payment method used for the refund.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentPaymentMethodInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "facilitatorOAuthApplicationClientId", + "description": "Find refunds created by a third party via the Grant API using a given OAuth application client ID.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "userId", + "description": "Find refunds with a user ID or IDs.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "storeId", + "description": "Find refunds by the ID of the store that the transaction was processed in.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RefundTransactionInput", + "description": "Top-level input fields for refunding a transaction.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transactionId", + "description": "The ID of a transaction to be refunded.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "refund", + "description": "Input fields for the details of the refund.", + "type": { + "kind": "INPUT_OBJECT", + "name": "RefundInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RefundTransactionPayload", + "description": "Top-level output field from refunding a transaction.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refund", + "description": "The information about the created refund.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Refund", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RefundUsBankAccountInput", + "description": "Top-level input fields for creating a detached refund on a US Bank Account.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of the US Bank Account to be refunded.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "options", + "description": "Input fields related to the US bank account being charged.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ChargeUsBankAccountOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "refund", + "description": "Input fields containing details about the refund.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DetachedRefundInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RefundUsBankAccountPayload", + "description": "Top-level output field from creating a detached refund for a US Bank Account.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refund", + "description": "The information about the created refund.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Refund", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Report", + "description": "Top-level fields returned for a report query.", + "fields": [ + { + "name": "transactionLevelFees", + "description": "Top-level fields returned in the transaction-level fee report query.", + "args": [ + { + "name": "date", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TransactionLevelFeeReport", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "This report has been renamed `paymentLevelFees`, since it applies to all types in the Payment interface, including transactions and refunds. Use the `paymentLevelFees` field instead, which returns the same report." + }, + { + "name": "paymentLevelFees", + "description": "Top-level fields returned in the payment-level fee report query.", + "args": [ + { + "name": "date", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PaymentLevelFeeReport", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RequestCancelFromInStoreReaderInput", + "description": "Input fields for requesting a cancel during an in-store charge flow.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "inStoreContextId", + "description": "Unique ID for the charge flow.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RequestChargeFromInStoreReaderInput", + "description": "Input fields for beginning the in-store charge flow.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "readerId", + "description": "ID of the Reader to request a charge from.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "transaction", + "description": "Information about the requested in-store transaction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InStoreTransactionInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RequestChargeInStoreContext", + "description": "Reference object for an in-store charge request.", + "fields": [ + { + "name": "id", + "description": "A unique ID for this charge request.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reader", + "description": "The reader from which the charge was requested.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreReader", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of the context created when the charge was requested. A status of COMPLETE does not indicate a successful payment.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "InStoreContextStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "transaction", + "description": "The transaction representing the charge on the payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "InStoreContextResult", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RequestConfirmationPromptFromInStoreReaderInput", + "description": "Input fields for requesting a confirmation prompt on an in-store reader.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "readerId", + "description": "ID of the Reader to request a confirmation prompt from.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "title", + "description": "Title to be displayed on the in-store reader. 50 character maximum.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "text", + "description": "Text to be displayed on the in-store reader. 65536 character maximum. '\\n' line breaks will be respected.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "alignment", + "description": "The way the text is aligned when displayed on an in-store reader. Defaults to CENTER.", + "type": { + "kind": "ENUM", + "name": "ConfirmationPromptAlignment", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "cancellationText", + "description": "Text for the cancellation option to be displayed on the in-store reader. 20 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "confirmationText", + "description": "Text for the confirmation option to be displayed on the in-store reader. 20 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RequestConfirmationPromptInStoreContext", + "description": "Reference object for an in-store reader confirmation prompt.", + "fields": [ + { + "name": "id", + "description": "A unique ID for this confirmation prompt request.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reader", + "description": "The reader from which the confirmation prompt was requested.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreReader", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of the context created when the confirmation prompt was requested.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "InStoreContextStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "confirmed", + "description": "The confirmation response collected by the in-store reader.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "InStoreContextResult", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RequestDisplayInStoreContext", + "description": "Reference object for an in-store display request.", + "fields": [ + { + "name": "id", + "description": "A unique ID for this display request.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reader", + "description": "The reader from which the display was requested.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreReader", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of the context created when the display was requested.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "InStoreContextStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "InStoreContextResult", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RequestFirmwareUpdateFromInStoreReaderInput", + "description": "Input fields for requesting a firmware update for an in-store reader.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "readerId", + "description": "The in-store reader to receive the firmware update.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RequestFirmwareUpdateInStoreContext", + "description": "Reference object for an in-store reader firmware update.", + "fields": [ + { + "name": "id", + "description": "A unique ID for this firmware update request.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reader", + "description": "The reader for which the firmware update was requested.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreReader", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of the context created when the firmware update was requested.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "InStoreContextStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "InStoreContextResult", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RequestItemDisplayFromInStoreReaderInput", + "description": "Input fields for beginning the in-store display line items flow.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "readerId", + "description": "ID of the Reader to display items on.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "displayItems", + "description": "Items to be displayed on the in-store reader. Up to 249 items may be specified.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InStoreDisplayItemInput", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "tax", + "description": "The total tax amount for the entire transaction, including all display line items.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "The total amount for the entire transaction, including tax.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "discountAmount", + "description": "The total discount amount for the entire transaction.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RequestRefundFromInStoreReaderInput", + "description": "Input fields for beginning the in-store refund flow.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "readerId", + "description": "ID of the Reader to request a refund from.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "refund", + "description": "Information about the requested in-store refund.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InStoreRefundInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RequestRefundInStoreContext", + "description": "Reference object for an in-store refund request.", + "fields": [ + { + "name": "id", + "description": "A unique ID for this refund request.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reader", + "description": "The reader from which the refund was requested.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreReader", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of the context created when the refund was requested. A status of COMPLETE does not indicate a successful payment.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "InStoreContextStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refund", + "description": "The refund representing the refund on the payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Refund", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "InStoreContextResult", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RequestSignaturePromptFromInStoreReaderInput", + "description": "Input fields for requesting a signature prompt on an in-store reader.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "readerId", + "description": "ID of the Reader to request a signature prompt from.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "title", + "description": "Title to be displayed on the in-store reader. 50 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "cancellationText", + "description": "Text for the cancellation option to be displayed on the in-store reader. 20 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "confirmationText", + "description": "Text for the confirmation option to be displayed on the in-store reader. 20 character maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RequestSignaturePromptInStoreContext", + "description": "Reference object for an in-store reader signature prompt.", + "fields": [ + { + "name": "id", + "description": "A unique ID for this signature prompt request.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reader", + "description": "The reader from which the signature prompt was requested.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreReader", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of the context created when the signature prompt was requested.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "InStoreContextStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "signatureData", + "description": "The signature data collected by the in-store reader. Base64 encoded PNG image.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "InStoreContextResult", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RequestTextDisplayFromInStoreReaderInput", + "description": "Input fields for beginning the in-store display text flow.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "readerId", + "description": "ID of the Reader to request a text display from.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "text", + "description": "Text to be displayed on the in-store reader. 255 character maximum. '\\n' line breaks will be respected.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RequestVaultFromInStoreReaderInput", + "description": "Input fields for beginning the in-store charge flow.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "readerId", + "description": "ID of the Reader to request a vault from.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "verification", + "description": "Input fields that specify options for verifying the payment method before vaulting. Only applicable if the payment method is of a type that supports verification. For supported types, verification is performed by default. If the verification fails, the payment method will not be vaulted.", + "type": { + "kind": "INPUT_OBJECT", + "name": "PaymentMethodVerificationOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customerId", + "description": "ID of the customer to associate the resulting multi-use payment method with.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RequestVaultInStoreContext", + "description": "Reference object for an in-store vault request.", + "fields": [ + { + "name": "id", + "description": "A unique ID for this vault request.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reader", + "description": "The reader from which the vault was requested.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreReader", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of the context created when the vault was requested.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "InStoreContextStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethod", + "description": "A payment method that has been stored in a merchant's vault and can be reused.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "verification", + "description": "The verification that was run on the payment method prior to vaulting.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Verification", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "InStoreContextResult", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ReverseRefundInput", + "description": "Input fields for reversing a refund.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "refundId", + "description": "The ID of the refund to reverse.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ReverseTransactionInput", + "description": "Input fields for reversing a transaction.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transactionId", + "description": "The ID of the transaction to reverse.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ReverseTransactionPayload", + "description": "Top-level output field for reversing a transaction.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reversal", + "description": "A transaction (if the original transaction was voided) or refund (if the original transaction was refunded). A reversal will attempt to void the original transaction if it has not yet settled. If the original transaction has settled, a reversal will create a refund for the full amount.", + "args": [], + "type": { + "kind": "UNION", + "name": "TransactionReversal", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Right", + "description": "A right assigned to a user.", + "fields": [ + { + "name": "name", + "description": "A human-readable name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RiskData", + "description": "Data from advanced risk evaluations.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "decision", + "description": "The risk decision on whether the transaction should be permitted.", + "args": [], + "type": { + "kind": "ENUM", + "name": "RiskDecision", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "decisionReasons", + "description": "The reasons for the decision from the fraud service provider.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deviceDataCaptured", + "description": "Whether data associated with the customer's device was captured and used in the decision process.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fraudServiceProvider", + "description": "The fraud service provider used to generate the risk decision.", + "args": [], + "type": { + "kind": "ENUM", + "name": "FraudServiceProvider", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "liabilityShift", + "description": "Liability Shift information in the event of a chargeback.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "LiabilityShift", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "score", + "description": "The numeric risk score assigned by the fraud service provider.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "RiskDataInput", + "description": "Input fields for data used by processors for risk analysis.", + "fields": null, + "inputFields": [ + { + "name": "customerBrowser", + "description": "The User-Agent header provided by the customer's browser, which gives information about the browser. Maximum 255 characters.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customerIp", + "description": "The customer's IP address.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "deviceData", + "description": "Customer device information. Required when creating transactions using cards (only if using Advanced Fraud Tools), PayPal (only for one-time Vaulted PayPal transactions), and Venmo payment method types. This value will contain a Fraud Merchant ID as the unique, numeric identifier for a Kount account and a Device Session ID as the unique identifier for a customer device. For PayPal and Venmo transactions, this value will also include a PayPal Correlation ID.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "RiskDecision", + "description": "The risk decision provides further context on how a transaction was scored for risk by Braintree.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "APPROVE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DECLINE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NOT_EVALUATED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "REVIEW", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Role", + "description": "Groups of rights assigned to the user.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "A human-readable name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isAccountAdmin", + "description": "Whether the role grants account admin status.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rights", + "description": "The rights associated with the role.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Right", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SEPADirectDebitAccountDetails", + "description": "Details about a SEPA Direct Debit account.", + "fields": [ + { + "name": "merchantOrPartnerCustomerId", + "description": "Merchant or Partner Customer ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last4", + "description": "Last 4 characters of IBAN number.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bankReferenceToken", + "description": "Bank reference token.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mandateType", + "description": "Mandate type.", + "args": [], + "type": { + "kind": "ENUM", + "name": "MandateType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SEPADirectDebitRefundDetails", + "description": "Refund-related details for SEPA Direct Debit transactions.", + "fields": [ + { + "name": "refundId", + "description": "The SEPA Direct Debit refund ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refundedFee", + "description": "Refunded transaction fee charged by SEPA Direct Debit.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentId", + "description": "PayPal V2 OrderId.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SEPADirectDebitTransactionDetails", + "description": "Details about a SEPA Direct Debit account.", + "fields": [ + { + "name": "captureId", + "description": "If funds for the transaction have settled, the PayPal ID for the capture of funds.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "transactionFee", + "description": "The fee charged by PayPal for the transaction.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentId", + "description": "PayPal V2 OrderId.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SamsungPayCardDetails", + "description": "Details about a Samsung Pay card.", + "fields": [ + { + "name": "brand", + "description": "The display name of the card brand, e.g. \"Visa\" or \"American Express\".", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "brandCode", + "description": "A static code identifying the card brand of the FPAN (the customer's actual backing card).", + "args": [], + "type": { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bin", + "description": "The first 6 digits of the credit card, known as the Bank Identification Number. This BIN will differ from the BIN of the source (customer's actual) card.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "binData", + "description": "Information about the card based on its BIN. This BIN will differ from the BIN of the source (customer's actual) card.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "BinRecord", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sourceCardLast4", + "description": "The last four digits of the FPAN (the customer's actual backing card).", + "args": [], + "type": { + "kind": "SCALAR", + "name": "CreditCardLast4", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SamsungPayCardInput", + "description": "Input fields for a Samsung Pay card.", + "fields": null, + "inputFields": [ + { + "name": "cryptogram", + "description": "A one-time-use string generated by the token requester to validate the transaction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "eCommerceIndicator", + "description": "A two-digit string that should be passed along in the authorization message.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ECommerceIndicator", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "expirationMonth", + "description": "A two-digit string representing the expiration month of the DPAN.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Month", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "expirationYear", + "description": "A four-digit string representing the expiration year of the DPAN.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Year", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "number", + "description": "The card number provided by Samsung and used in processing. This is a digitized PAN (DPAN), not the backing card number (FPAN).", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CreditCardNumber", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "sourceCardLast4", + "description": "The last four digits of the FPAN (the cardholder's backing card).", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CreditCardLast4", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SamsungPayConfiguration", + "description": "Configuration for Samsung Pay on Android.", + "fields": [ + { + "name": "displayName", + "description": "A string used to identify the merchant to the customer.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "environment", + "description": "The Samsung Pay environment.", + "args": [], + "type": { + "kind": "ENUM", + "name": "SamsungPayEnvironment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "serviceId", + "description": "The Samsung Pay service ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "samsungAuthorization", + "description": "Authorization to use when tokenizing Samsung Pay.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "supportedCardBrands", + "description": "A list of card brands supported by the merchant for Samsung Pay.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "SamsungPayEnvironment", + "description": "The environment being used for Samsung Pay.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "PRODUCTION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SANDBOX", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SamsungPayOriginDetails", + "description": "Additional information about the payment method specific to Samsung Pay.", + "fields": [ + { + "name": "bin", + "description": "The first 6 digits of the credit card, known as the Bank Identification Number. This BIN may differ from the BIN of the customer's actual card.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SandboxSettleTransactionInput", + "description": "Top-level input fields for settling a transaction in the sandbox environment.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transactionId", + "description": "Id of the transaction to force settlement in the sandbox environment.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "settlementState", + "description": "The target settlement state for the transaction in the sandbox environment.", + "type": { + "kind": "ENUM", + "name": "SandboxSettlementState", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "SandboxSettlementState", + "description": "The settlement state when forcing transaction settlement in the sandbox environment.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SETTLED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SETTLEMENT_DECLINED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ScaExemptionType", + "description": "The type of Strong Customer Authentication Exemption.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "LOW_VALUE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SECURE_CORPORATE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TRANSACTION_RISK_ANALYSIS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TRUSTED_BENEFICIARY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Search", + "description": "Top-level fields returned for a search query.", + "fields": [ + { + "name": "transactions", + "description": "A paginated list of transactions that match the TransactionSearchInput.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionSearchInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TransactionConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refunds", + "description": "A paginated list of refunds that match the RefundSearchInput.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RefundSearchInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RefundConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payments", + "description": "A paginated list of all types of Payment that match the PaymentSearchInput.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PaymentSearchInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PaymentConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "disputes", + "description": "A paginated list of disputes that match the DisputeSearchInput.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DisputeSearchInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "DisputeConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "verifications", + "description": "A paginated list of verifications that match the VerificationSearchInput.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "VerificationSearchInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VerificationConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customers", + "description": "A paginated list of customers that match the CustomerSearchInput.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CustomerSearchInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CustomerConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "businessAccountCreationRequests", + "description": "A paginated list of business account creation requests that match the BusinessAccountCreationRequestSearchInput.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "BusinessAccountCreationRequestSearchInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "BusinessAccountCreationRequestConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inStoreReaders", + "description": "A paginated list of in-store readers that match the InStoreReaderSearchInput.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InStoreReaderSearchInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "InStoreReaderConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchChargebackProtectionLevelInput", + "description": "Deprecated: Please use `SearchDisputeProtectionLevelInput` instead.\n\nInput fields for searching for a dispute with a given chargeback protection level.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "The dispute's chargeback protection level is exactly this value.", + "type": { + "kind": "ENUM", + "name": "ChargebackProtectionLevel", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "in", + "description": "Dispute's chargeback protection level is one of these values.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "ChargebackProtectionLevel", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchCreditCardBrandCodeInput", + "description": "Input fields for searching for payments by credit card brand.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "Credit card brand code is exactly this value.", + "type": { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "in", + "description": "Credit card brand code is one of these values.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchCreditCardExpirationDateInput", + "description": "Input fields for searching for payments by payment method snapshot credit card expiration date criteria.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "Field is exactly this value.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchCreditCardExpirationMonthYearInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "isNot", + "description": "Field is not this value.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchCreditCardExpirationMonthYearInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchCreditCardExpirationMonthYearInput", + "description": "Input fields for searching for payments by payment method snapshot credit card expiration date criteria.", + "fields": null, + "inputFields": [ + { + "name": "expirationMonth", + "description": "The month of the credit card expiration as MM.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "expirationYear", + "description": "The year of the credit card expiration as YYYY.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchCreditCardNumberInput", + "description": "Input fields for searching for payments by payment method snapshot credit card number criteria.", + "fields": null, + "inputFields": [ + { + "name": "startsWith", + "description": "Up to the first six digits of the credit card number (the credit card's BIN).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "endsWith", + "description": "Up to four digits of the last four digits of the credit card number.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchDateInput", + "description": "Input fields for searching for a date. These ranges are precise to the day.", + "fields": null, + "inputFields": [ + { + "name": "greaterThanOrEqualTo", + "description": "Date is greater than or equal to this value.", + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "lessThanOrEqualTo", + "description": "Date is less than or equal to this value.", + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchDisputeProtectionLevelInput", + "description": "Input fields for searching for a dispute with a given protection level.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "The dispute's protection level is exactly this value.", + "type": { + "kind": "ENUM", + "name": "DisputeProtectionLevel", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "in", + "description": "The dispute's protection level is one of these values.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "DisputeProtectionLevel", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchDisputeReasonInput", + "description": "Input fields for searching for a dispute with a given reason description.", + "fields": null, + "inputFields": [ + { + "name": "in", + "description": "The dispute reason is one of these values.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "DisputeReason", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchDisputeStatusInput", + "description": "Input fields for searching for a dispute with a given status.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "The dispute status is exactly this value.", + "type": { + "kind": "ENUM", + "name": "DisputeStatus", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "in", + "description": "The dispute status is one of these values.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "DisputeStatus", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchDisputeTypeInput", + "description": "Input fields for searching for a dispute with a given type.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "The dispute type is exactly this value.", + "type": { + "kind": "ENUM", + "name": "DisputeType", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "in", + "description": "The dispute type is one of these values.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "DisputeType", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentCreditCardDetailsInput", + "description": "Input fields for searching for payments by payment method snapshot credit card details criteria.", + "fields": null, + "inputFields": [ + { + "name": "number", + "description": "The credit card number used for the payment.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchCreditCardNumberInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "expirationDate", + "description": "Find payments based on the expiration date of the credit card used for the payment.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchCreditCardExpirationDateInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "uniqueNumberIdentifier", + "description": "The unique identifier of the credit card number used for the payment.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "cardholderName", + "description": "The card holder name of the credit card number used for the payment.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "brandCode", + "description": "The brand code of the credit card number used for the payment.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchCreditCardBrandCodeInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentCustomerInput", + "description": "Input fields for searching payments by customer.", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "Find payments with a given customer ID.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "firstName", + "description": "Find payments with a given first name.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "lastName", + "description": "Find payments with a given last name.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "company", + "description": "Find payments with a given customer company.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "email", + "description": "Find payments with a customer email.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentMethodSnapshotTypeInput", + "description": "Input fields for searching transactions by payment method snapshot type.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "This value represents the payment method type used to create a transaction. In the case of credit cards, this value also encode the origin from which a customer provided that payment method.", + "type": { + "kind": "ENUM", + "name": "PaymentMethodSnapshotSearchType", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "in", + "description": "These values represent the payment method type used to create a transaction. In the case of credit cards, these values also encode the origin from which a customer provided that payment method.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PaymentMethodSnapshotSearchType", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentPayPalDetailsInput", + "description": "Input fields for searching for payments by payment method snapshot PayPal details criteria.", + "fields": null, + "inputFields": [ + { + "name": "email", + "description": "\"The email address of the PayPal payer.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentPaymentMethodInput", + "description": "Input fields for searching for payments by payment method criteria.", + "fields": null, + "inputFields": [ + { + "name": "paymentMethodId", + "description": "The ID of the vaulted payment method used for the payment.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodSnapshot", + "description": "The snapshot of the payment method at the time of payment creation.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentPaymentMethodSnapshotInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentPaymentMethodSnapshotInput", + "description": "Input fields for searching for payments by payment method snapshot criteria.", + "fields": null, + "inputFields": [ + { + "name": "type", + "description": "Find payments based on the payment instrument type.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentMethodSnapshotTypeInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "creditCardDetails", + "description": "Find payments made with credit cards, based on the details of the credit card used for the payment. Passing an object with non-empty, non-null fields will scope your search to *only* credit card payment methods. This overrides the `type` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentCreditCardDetailsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "payPalDetails", + "description": "Find payments made with PayPal, based on the PayPal details used for the payment. Passing a value here will scope your search to *only* PayPal payment methods. This overrides the `type` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentPayPalDetailsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "sepaDirectDebitDetails", + "description": "Find SEPA payments with SEPA details. Passing a value here will scope your search to *only* SEPA Direct Debit payment methods. This overrides the `type` field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentSEPADirectDebitDetailsInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentSEPADirectDebitDetailsInput", + "description": "Input field for searching for payments by payment method snapshot SEPA Direct Debit details criteria.", + "fields": null, + "inputFields": [ + { + "name": "paymentId", + "description": "PayPal V2 OrderId.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentSourceInput", + "description": "Input fields for searching for a transaction or refund created with a given source.", + "fields": null, + "inputFields": [ + { + "name": "in", + "description": "The transaction source is one of these values.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentStatusInput", + "description": "Input fields for searching for a transaction or refund with a given status.", + "fields": null, + "inputFields": [ + { + "name": "in", + "description": "The transaction status is one of these values.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentStatusTransitionInput", + "description": "Payment status transition times.", + "fields": null, + "inputFields": [ + { + "name": "failedAt", + "description": "Find transactions with a given failed at time.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTimestampInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "settledAt", + "description": "Find transactions with a given settled at time.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTimestampInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "submittedForSettlementAt", + "description": "Find transactions with a given submitted for settlement time.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTimestampInput", + "ofType": null + }, + "defaultValue" : null + }, + { + "name" : "voidedAt", + "description" : "Find transactions with a given voided at time.", + "type" : { + "kind" : "INPUT_OBJECT", + "name" : "SearchTimestampInput", + "ofType" : null + }, + "defaultValue" : null + }, + { + "name" : "authorizationExpiredAt", + "description" : "Find transactions with a given authorization expired at time.", + "type" : { + "kind" : "INPUT_OBJECT", + "name" : "SearchTimestampInput", + "ofType" : null + }, + "defaultValue" : null + }, + { + "name" : "authorizedAt", + "description" : "Find transactions with a given authorized at time.", + "type" : { + "kind" : "INPUT_OBJECT", + "name" : "SearchTimestampInput", + "ofType" : null + }, + "defaultValue" : null + }, + { + "name" : "gatewayRejectedAt", + "description" : "Find transactions with a given gateway rejected at time.", + "type" : { + "kind" : "INPUT_OBJECT", + "name" : "SearchTimestampInput", + "ofType" : null + }, + "defaultValue" : null + }, + { + "name" : "processorDeclinedAt", + "description" : "Find transactions with a given processor declined at time.", + "type" : { + "kind" : "INPUT_OBJECT", + "name" : "SearchTimestampInput", + "ofType" : null + }, + "defaultValue" : null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentTypeInput", + "description": "Input fields for searching for payments by implementing type.", + "fields": null, + "inputFields": [ + { + "name": "in", + "description": "The payment is a transaction and/or a refund.", + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "ENUM", + "name" : "PaymentSearchType", + "ofType" : null + } + } + }, + "defaultValue" : null + } + ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, + { + "kind" : "INPUT_OBJECT", + "name" : "SearchPreDisputeProgramInput", + "description" : "Input fields for searching for a dispute with a given pre-dispute program.", + "fields" : null, + "inputFields" : [ + { + "name" : "is", + "description" : "The dispute's pre-dispute program is exactly this value.", + "type" : { + "kind" : "ENUM", + "name" : "PreDisputeProgram", + "ofType" : null + }, + "defaultValue" : null + }, + { + "name" : "in", + "description" : "The dispute's pre-dispute program is one of these values.", + "type" : { + "kind" : "LIST", + "name" : null, + "ofType" : { + "kind" : "NON_NULL", + "name" : null, + "ofType" : { + "kind" : "ENUM", + "name" : "PreDisputeProgram", + "ofType" : null + } + } + }, + "defaultValue" : null + } + ], + "interfaces" : null, + "enumValues" : null, + "possibleTypes" : null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchRangeInput", + "description": "Input fields for searching for a range.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "Field is exactly this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "greaterThanOrEqualTo", + "description": "Field is greater than or equal to this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "lessThanOrEqualTo", + "description": "Field is less than or equal to this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchSoftwareVersionInput", + "description": "Input fields for searching for a version number.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "Field is exactly this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "isNot", + "description": "Field is not this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "startsWith", + "description": "Field starts with this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "contains", + "description": "Field contains this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "description": "Input fields for searching text fields.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "Field is exactly this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "isNot", + "description": "Field is not this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "startsWith", + "description": "Field starts with this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "endsWith", + "description": "Field ends with this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "contains", + "description": "Field contains this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchTimestampInput", + "description": "Input fields for searching by timestamp. These ranges are precise to the minute; the results of searching for an object created between 12/17/2015 17:00 and 12/17/2015 17:00 (i.e., the same minute) will include objects created at 12/17/2015 17:00:59. If no timezone is provided, it will be assumed to be UTC.", + "fields": null, + "inputFields": [ + { + "name": "greaterThanOrEqualTo", + "description": "Timestamp is greater than or equal to this value.", + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "lessThanOrEqualTo", + "description": "Timestamp is less than or equal to this value.", + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchTransactionSourceInput", + "description": "Input fields for searching for a transaction created with a given source.", + "fields": null, + "inputFields": [ + { + "name": "in", + "description": "The transaction source is one of these values.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchTransactionStatusInput", + "description": "Input fields for searching for a transaction with a given status.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "The transaction status is exactly this value.", + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "in", + "description": "The transaction status is one of these values.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchTransactionStatusTransitionInput", + "description": "Transaction status transition times.", + "fields": null, + "inputFields": [ + { + "name": "failedAt", + "description": "Find transactions with a given failed at time.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTimestampInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "settledAt", + "description": "Find transactions with a given settled at time.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTimestampInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "submittedForSettlementAt", + "description": "Find transactions with a given submitted for settlement time.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTimestampInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "voidedAt", + "description": "Find transactions with a given voided at time.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTimestampInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "description": "Input fields for searching for specific values.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "Field is exactly this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "in", + "description": "Field is one of these values.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SearchVerificationStatusInput", + "description": "Input fields for searching for a verification with a given status.", + "fields": null, + "inputFields": [ + { + "name": "is", + "description": "The verification status is exactly this value.", + "type": { + "kind": "ENUM", + "name": "VerificationStatus", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "in", + "description": "The verification status is one of these values.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VerificationStatus", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SelectedPayPalFinancingOptionDetails", + "description": "Details about a selected financing option by a PayPal buyer.", + "fields": [ + { + "name": "term", + "description": "Total number of payments over which to finance the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "monthlyPayment", + "description": "The amount for each monthly payment.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discountPercentage", + "description": "The percent discount off the total transaction amount due to the selected financing option.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Percentage", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discountAmount", + "description": "The amount reduced from the total transaction amount.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "SelectedPayPalFinancingOptionInput", + "description": "Input fields indicating a selected financing option by a PayPal buyer.", + "fields": null, + "inputFields": [ + { + "name": "term", + "description": "Total number of payments over which to finance the transaction.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "currencyCode", + "description": "The currency code for the monthly payment and discount amount.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CurrencyCodeAlpha", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "monthlyPayment", + "description": "The amount for each monthly payment.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "discountPercentage", + "description": "The percent discount off the total transaction amount due to the selected financing option.", + "type": { + "kind": "SCALAR", + "name": "Percentage", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "discountAmount", + "description": "The amount reduced from the total transaction amount.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SettledEvent", + "description": "Accompanying information for a settled transaction.", + "fields": [ + { + "name": "status", + "description": "The new status of the transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the transaction was settled.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount the transaction was settled for, in the same currency as the original authorization (aka the \"presentment\" currency.) If you have elected to settle the transaction into a bank account with a different currency, this will not reflect that.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "The source for the transaction change to the new status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processorResponse", + "description": "Fields describing the payment processor response.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionSettlementProcessorResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminal", + "description": "Whether or not this is the final state for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "settlementBatchId", + "description": "The ID of the settlement batch in which the transaction was processed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SettlementConfirmedEvent", + "description": "Accompanying information for a settlement confirmed transaction.", + "fields": [ + { + "name": "status", + "description": "The new status of the transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the transaction became settlement confirmed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount of the transaction for this status event.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "The source for the transaction change to the new status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processorResponse", + "description": "Fields describing the payment processor response to the settlement request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionSettlementProcessorResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminal", + "description": "Whether or not this is the final state for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SettlementDeclinedEvent", + "description": "Accompanying information for a settlement declined transaction.", + "fields": [ + { + "name": "status", + "description": "The new status of the transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the processor declined to settle this transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount of the transaction for this status event.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "The source for the transaction change to the new status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processorResponse", + "description": "Fields describing the payment processor response to the settlement request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionSettlementProcessorResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminal", + "description": "Whether or not this is the final state for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SettlementPendingEvent", + "description": "Accompanying information for a settlement pending transaction. This typically only occurs for PayPal transactions.", + "fields": [ + { + "name": "status", + "description": "The new status of the transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the transaction became settlement pending.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount of the transaction for this status event.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "The source for the transaction change to the new status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processorResponse", + "description": "Fields describing the payment processor response to the settlement request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionSettlementProcessorResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminal", + "description": "Whether or not this is the final state for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SettlingEvent", + "description": "Accompanying information for a transaction that is settling. This is typically a transient state during which the transaction is being settled with the processor.", + "fields": [ + { + "name": "status", + "description": "The new status of the transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the transaction began settling.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount of the transaction for this status event. This should match the amount submitted for settlement.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "The source for the transaction change to the new status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminal", + "description": "Whether or not this is the final state for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "Built-in String", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SubmittedForSettlementEvent", + "description": "Accompanying information for a transaction that is submitted for settlement. This status indicates that the transaction is scheduled to be settled.", + "fields": [ + { + "name": "status", + "description": "The new status of the transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the transaction was submitted for settlement.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount that was submitted for settlement. This can differ from the authorized amount, but by default is the same.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "The source for the transaction change to the new status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminal", + "description": "Whether or not this is the final state for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TaxInfoInput", + "description": "Input fields for local payment tax information.", + "fields": null, + "inputFields": [ + { + "name": "identifier", + "description": "The payer's tax identifier value.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "type", + "description": "The payer's tax identifier type.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "TaxInfoType", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "TaxInfoType", + "description": "The type of tax identifier.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "BR_CNPJ", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "BR_CPF", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ThreeDSecureAuthentication", + "description": "Information about the 3D Secure authentication for a payment method.", + "fields": [ + { + "name": "cavv", + "description": "The cardholder authentication verification value. This value should be appended to the authorization message signifying that the transaction has been successfully authenticated with 3D Secure. This value will be encoded according to the merchant's configuration with CardinalCommerce, with either Base64 or Hex encoding. The decoded value will be of different length and format per card scheme.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directoryServerTransactionId", + "description": "A unique identifier for the 3D Secure interaction with the card brand directory server.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "eciFlag", + "description": "The electronic commerce indicator.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ECommerceIndicator", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "liabilityShifted", + "description": "A boolean indicating if the card has received liability shift.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "liabilityShiftPossible", + "description": "A boolean indicating if the card is eligible for liability shift.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cardEnrolled", + "description": "Indicates whether the card is enrolled in a 3D Secure program.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ThreeDSecureCardEnrolled", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authenticationStatus", + "description": "The 3D Secure authentication status of the card.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "version", + "description": "The version of the 3D Secure protocol used during authentication.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "xId", + "description": "A unique identifier for the 3D Secure interaction with the provider.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "threeDSecureServerTransactionId", + "description": "A unique identifier for the 3D Secure interaction with the 3D Secure server.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "acsTransactionId", + "description": "A unique identifier for the 3D Secure interaction with the access control server.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paresStatus", + "description": "Indicates the current status of the 3D Secure authentication from the 3D Secure server for 3D Secure 1.0 authentications.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationStatusIndicator", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "transactionStatus", + "description": "Indicates the current status of the 3D Secure authentication from the 3D Secure server for 3D Secure 2.0 authentications.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationStatusIndicator", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "transactionStatusReason", + "description": "Indicates the reason for the transaction status. This will be null if status is `SUCCESSFUL_AUTHENTICATION`.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationAcsWindowSize", + "description": "An override field that a merchant can pass in to set the challenge window size to display to the end cardholder. The ACS will reply with content that is formatted appropriately to this window size to allow for the best user experience. The sizes are width x height in pixels of the window displayed in the cardholder browser window.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "FULL_PAGE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "W250_H400", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "W390_H400", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "W500_H600", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "W600_H400", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationDeliveryTimeframe", + "description": "Indicates the delivery timeframe if applicable.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ELECTRONIC_DELIVERY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OVERNIGHT_SHIPPING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SAME_DAY_SHIPPING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TWO_OR_MORE_DAY_SHIPPING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecureAuthenticationInput", + "description": "Input fields for passing auxillary 3D Secure information manually, as opposed to tokenized on a single-use payment method ID.", + "fields": null, + "inputFields": [ + { + "name": "authenticationId", + "description": "Braintree unique ID of the 3D Secure authentication performed for this transaction. You will only need to use this field if you are charging or authorizing a vaulted payment method ID.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "passThrough", + "description": "Results of a merchant-performed 3D Secure authentication. You will only need to use these fields if you've performed your own integration with a 3D Secure MPI provider (e.g. Cardinal Centinel). Otherwise, Braintree's SDKs handle this for you in our standard 3D Secure integration.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecurePassThroughInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationMerchantProductCode", + "description": "Merchant product code.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ACCOMMODATION_RETAIL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AIRLINE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CAR_RENTAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CASH_DISPENSING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DIGITAL_GOODS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FUEL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GENERAL_RETAIL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LUXURY_RETAIL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OTHER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RESTAURANT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SERVICES", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TRAVEL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationShippingType", + "description": "Indicates the shipping type for the transaction.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "DIGITAL_GOODS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OTHER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SHIP_TO_ADDRESS_ON_FILE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SHIP_TO_BILLING_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SHIP_TO_OTHER_ADDRESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SHIP_TO_STORE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TICKETS_NOT_SHIPPED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationStatus", + "description": "The 3D Secure authentication status of the card.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "AUTHENTICATE_ATTEMPT_SUCCESSFUL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AUTHENTICATE_ERROR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AUTHENTICATE_FAILED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AUTHENTICATE_FAILED_ACS_ERROR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AUTHENTICATE_FRICTIONLESS_FAILED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AUTHENTICATE_REJECTED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AUTHENTICATE_SIGNATURE_VERIFICATION_FAILED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AUTHENTICATE_SUCCESSFUL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AUTHENTICATE_SUCCESSFUL_ISSUER_NOT_PARTICIPATING", + "description": null, + "isDeprecated": true, + "deprecationReason": "No longer applicable." + }, + { + "name": "AUTHENTICATE_UNABLE_TO_AUTHENTICATE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AUTHENTICATION_BYPASSED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AUTHENTICATION_UNAVAILABLE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CHALLENGE_REQUIRED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DATA_ONLY_SUCCESSFUL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LOOKUP_BYPASSED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LOOKUP_CARD_ERROR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LOOKUP_ENROLLED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LOOKUP_ERROR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LOOKUP_FAILED_ACS_ERROR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LOOKUP_NOT_ENROLLED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LOOKUP_SERVER_ERROR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNSUPPORTED_ACCOUNT_TYPE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNSUPPORTED_CARD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNSUPPORTED_THREE_D_SECURE_VERSION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationStatusIndicator", + "description": "Indicates the current status of the 3D Secure authentication.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "AUTHENTICATION_REJECTED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CHALLENGE_REQUIRED_DECOUPLED_AUTHENTICATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CHALLENGE_REQUIRED_FOR_AUTHENTICATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FAILED_AUTHENTICATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INFORMATIONAL_CHALLENGE_PREFERENCE_ACKNOWLEDGED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUCCESSFUL_ATTEMPTS_TRANSACTION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUCCESSFUL_AUTHENTICATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNABLE_TO_COMPLETE_AUTHENTICATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationTransactionType", + "description": "Indicates the type of transaction for 3D Secure authentication.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ADD_CARD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CARDHOLDER_VERIFICATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INSTALLMENT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MAINTAIN_CARD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PAYMENT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RECURRING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ThreeDSecureCardEnrolled", + "description": "Indicates whether the card is enrolled in a 3D Secure program.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "BYPASS", + "description": "Authentication has been bypassed. This status will be returned if you set up bypass rules with CardinalCommerce, and they are triggered.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ERROR", + "description": "There was an error in determining whether the card is enrolled in a 3D Secure program.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NO", + "description": "The card is not enrolled.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNAVAILABLE", + "description": "The DS (directory server) or ACS (access control server) is not available for authentication at the time of the request.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "YES", + "description": "The card is enrolled.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ThreeDSecureCavvAlgorithm", + "description": "A 3D Secure CAVV algorithm. Possible Values: 2 - CVV with ATN, 3 - Mastercard SPA algorithm.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ThreeDSecureConfiguration", + "description": "Configuration for 3D Secure.", + "fields": [ + { + "name": "cardinalAuthenticationJWT", + "description": "Authentication information for initializing Cardinal's songbird.js library.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ThreeDSecureDetails", + "description": "3D Secure information for the payment method.", + "fields": [ + { + "name": "authentication", + "description": "Contains relevant data fields if the payment method has been authenticated using 3D Secure. Only available on 3D Secure authenticated single-use payment methods and 3D Secure paymentMethodSnapshots.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ThreeDSecureAuthentication", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authenticationInsight", + "description": "Information about the [customer authentication regulation environment](https://developers.braintreepayments.com/guides/3d-secure/migration/javascript/v3#authentication-insight) that applies to the payment method when processed with the provided merchant account. This can be used to determine whether to perform 3D Secure authentication.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AuthenticationInsightInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AuthenticationInsight", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cavv", + "description": "The cardholder authentication verification value. This value should be appended to the authorization message signifying that the transaction has been successfully authenticated with 3D Secure. This value will be encoded according to the merchant's configuration with CardinalCommerce, with either Base64 or Hex encoding. The decoded value will be of different length and format per card scheme.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.cavv instead." + }, + { + "name": "directoryServerTransactionId", + "description": "A unique identifier for the 3D Secure interaction with the card brand directory server.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.directoryServerTransactionId instead." + }, + { + "name": "eciFlag", + "description": "The electronic commerce indicator.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ECommerceIndicator", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.eciFlag instead." + }, + { + "name": "liabilityShifted", + "description": "A boolean indicating if the card has received liability shift.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.liabilityShifted instead." + }, + { + "name": "liabilityShiftPossible", + "description": "A boolean indicating if the card is eligible for liability shift.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.liabilityShiftPossible instead." + }, + { + "name": "cardEnrolled", + "description": "Indicates whether the card is enrolled in a 3D Secure program.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ThreeDSecureCardEnrolled", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.cardEnrolled instead." + }, + { + "name": "authenticationStatus", + "description": "The 3D Secure authentication status of the card.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationStatus", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.authenticationStatus instead." + }, + { + "name": "version", + "description": "The version of the 3D Secure protocol used during authentication.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.version instead." + }, + { + "name": "xId", + "description": "A unique identifier for the 3D Secure interaction with the provider.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.xId instead." + }, + { + "name": "threeDSecureServerTransactionId", + "description": "A unique identifier for the 3D Secure interaction with the 3D Secure server.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.threeDSecureServerTransactionId instead." + }, + { + "name": "acsTransactionId", + "description": "A unique identifier for the 3D Secure interaction with the access control server.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.acsTransactionId instead." + }, + { + "name": "paresStatus", + "description": "Indicates the current status of the 3D Secure authentication from the 3D Secure server for 3D Secure 1.0 authentications.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationStatusIndicator", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.paresStatus instead." + }, + { + "name": "transactionStatus", + "description": "Indicates the current status of the 3D Secure authentication from the 3D Secure server for 3D Secure 2.0 authentications.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationStatusIndicator", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.transactionStatus instead." + }, + { + "name": "transactionStatusReason", + "description": "Indicates the reason for the transaction status. This will be null if status is `SUCCESSFUL_AUTHENTICATION`.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use ThreeDSecureDetails.authentication.transactionStatusReason instead." + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecureLookupBillingAddressInput", + "description": " The billing address of the cardholder sent with 3D Secure Lookup requests.", + "fields": null, + "inputFields": [ + { + "name": "givenName", + "description": "The given (first) name associated with the billing address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "surname", + "description": "The surname (last name) associated with the billing address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "phoneNumber", + "description": "The billing phone number used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "line1", + "description": "Line 1 of the billing address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "line2", + "description": "Line 2 of the billing address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "line3", + "description": "Line 3 of the billing address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "locality", + "description": "City or locality of billing address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "region", + "description": "State or region of billing address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "countryCode", + "description": "Country code of billing address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "postalCode", + "description": "Postal code of billing address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecureLookupCardholderInformationInput", + "description": "Additional information about the cardholder when authenticating through 3D Secure.", + "fields": null, + "inputFields": [ + { + "name": "billingAddress", + "description": "The billing address of the cardholder.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecureLookupBillingAddressInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecureLookupClientInformationInput", + "description": "Information about the client side lookup process.", + "fields": null, + "inputFields": [ + { + "name": "sdkVersion", + "description": "Version of the Braintree client-side SDK being used.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "requestedThreeDSecureVersion", + "description": "Version of 3D Secure requested when performing the lookup.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "issuerDeviceDataCollectionMillisecondsElapsed", + "description": "Number of milliseconds taken for the issuer to collect device data.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "issuerDeviceDataCollectionResult", + "description": "Whether device data collection by the issuer succeeded.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "threeDSecureServerDeviceDataCollectionMillisecondsElapsed", + "description": "Number of milliseconds taken for the 3D Secure server to collect device data.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ThreeDSecureLookupData", + "description": "Data fields containing information from the MPI provider about the 3D Secure Lookup result.", + "fields": [ + { + "name": "acsUrl", + "description": "The URL to use to issue a challenge to the cardholder for 3D Secure authentication.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authenticationId", + "description": "Braintree unique ID of the 3D Secure authentication performed for this transaction. You will only need to use this field if you are charging or authorizing a vaulted payment method ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "version", + "description": "The version of the 3D Secure protocol used in the authentication.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pareq", + "description": "The \"PAReq\" or \"Payment Authentication Request\" is the encoded request message used to initiate authentication.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "md", + "description": "The unique 3D Secure identifier assigned by Braintree to track the 3D Secure call as it progresses.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "termUrl", + "description": "A fully qualified URL that the customer will be redirected to once the authentication completes.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "transactionId", + "description": "A unique identifier used by the MPI provider to identify the 3D Secure interaction. The MPI provider provides the framework for determining if a card is enrolled in a 3D Secure program and for facilitating interactions with the issuer.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecureLookupShippingAddressInput", + "description": " The shipping address of the transaction to be sent with 3D Secure Lookup requests.", + "fields": null, + "inputFields": [ + { + "name": "givenName", + "description": "The given (first) name associated with the shipping address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "surname", + "description": "The surname (last name) associated with the shipping address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "phoneNumber", + "description": "The shipping phone number used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "line1", + "description": "Line 1 of the shipping address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "line2", + "description": "Line 2 of the shipping address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "line3", + "description": "Line 3 of the shipping address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "locality", + "description": "City or locality of shipping address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "region", + "description": "State or region of shipping address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "countryCode", + "description": "Country code of shipping address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "postalCode", + "description": "Postal code of shipping address used for verification.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ThreeDSecureLookupShippingMethod", + "description": "Indicates the shipping method chosen for the transaction in the 3D Secure lookup.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ELECTRONIC_DELIVERY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GROUND", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OVERNIGHT_EXPEDITED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PRIORITY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SAME_DAY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SHIP_TO_STORE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecureLookupTransactionInformationInput", + "description": "Additional information about the transaction when authenticating through 3D Secure.", + "fields": null, + "inputFields": [ + { + "name": "email", + "description": "The email associated with the transaction.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "shippingMethod", + "description": "Indicates the shipping method chosen for the transaction in the 3D Secure lookup.", + "type": { + "kind": "ENUM", + "name": "ThreeDSecureLookupShippingMethod", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "phoneNumber", + "description": "The phone number associated with the transaction.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "shippingAddress", + "description": "The shipping address for the transaction.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecureLookupShippingAddressInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "workPhoneNumber", + "description": "The work phone number associated with the transaction.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transactionType", + "description": "Indicates the type of transaction.", + "type": { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationTransactionType", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "deliveryTimeframe", + "description": "Indicates the delivery timeframe if applicable.", + "type": { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationDeliveryTimeframe", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "deliveryEmail", + "description": "For electronic delivery, email address to which the product was delivered.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "shippingType", + "description": "Indicates shipping type chosen for the transaction.", + "type": { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationShippingType", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "productCode", + "description": "Merchant product code.", + "type": { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationMerchantProductCode", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "reorderIndicator", + "description": "Indicates whether the cardholder is reordering merchandise purchased in a previous order.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "preorderIndicator", + "description": "Indicates whether cardholder is placing an order with a future availability or release date.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "preorderDate", + "description": "Expected date that a pre-ordered purchase will be available.", + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "giftCardAmount", + "description": "The purchase amount total for prepaid gift cards.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "giftCardCurrencyCode", + "description": "ISO 4217 currency code for the gift card purchased.", + "type": { + "kind": "SCALAR", + "name": "CurrencyCodeAlpha", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "giftCardCount", + "description": "Total count of individual prepaid gift cards purchased.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "accountCreatedDuringTransaction", + "description": "Indicates whether the cardholder created the account during this transaction.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "accountCreateDate", + "description": "Date the cardholder opened the account.", + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "accountChangedDuringTransaction", + "description": "Indicates whether the cardholder changed the account during this transaction. This includes changes to the billing or shipping address, new payment accounts or new users added.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "accountChangeDate", + "description": "Date the cardholder's account was last changed. This includes changes to the billing or shipping address, new payment accounts or new users added.", + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "accountPasswordChangedDuringTransaction", + "description": "Indicates whether the cardholder changed or reset the password on the account during this transaction.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "accountPasswordChangeDate", + "description": "Date the cardholder changed or reset the password on the account.", + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "firstUseOfShippingAddress", + "description": "Indicates whether this transaction represents the first use of this shipping address.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "shippingAddressFirstUsageDate", + "description": "Date when the shipping address used for this transaction was first used.", + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transactionCountDay", + "description": "Number of transactions (successful or incomplete) for this cardholder account within the last 24 hours.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transactionCountYear", + "description": "Number of transactions (successful or incomplete) for this cardholder account within the last year.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "addCardAttempts", + "description": "Number of attempts that have been made to add a card to this account in the last 24 hours.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "accountPurchases", + "description": "Number of purchases with this cardholder account during the previous six months.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "suspiciousActivityObserved", + "description": "Indicates whether the merchant experienced suspicious activity (including previous fraud) on the account.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "accountNameMatchesShippingName", + "description": "Indicates if the cardholder name on the account is identical to the shipping name used for the transaction.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodAddedDuringTransaction", + "description": "Indicates whether the payment method was added to the cardholder account during this transaction.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodAddedToAccountDate", + "description": "Date the payment method was added to the cardholder account.", + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "acsWindowSize", + "description": "An override field that a merchant can pass in to set the challenge window size to display to the end cardholder. The ACS will reply with content that is formatted appropriately to this window size to allow for the best user experience. The sizes are width x height in pixels of the window displayed in the cardholder browser window.", + "type": { + "kind": "ENUM", + "name": "ThreeDSecureAuthenticationAcsWindowSize", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "sdkMaxTimeout", + "description": "This field indicates the maximum amount of time for all 3DS 2.0 messages to be communicated between all components (in minutes). Minimum is 05. Defaults to 15.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "billingAddressMatchesShippingAddress", + "description": "Indicates whether cardholder billing and shipping addresses match.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "accountId", + "description": "Additional cardholder account information.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "ipAddress", + "description": "The IP address of the cardholder. Both IPv4 and IPv6 formats are supported.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "orderDescription", + "description": "Brief Description of items purchased.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "taxAmount", + "description": "Tax amount.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "userAgent", + "description": "The exact content of the HTTP user agent header.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "installment", + "description": "Indicates the maximum number of authorizations for installment payments. An integer value greater than 1 indicating the maximum number of permitted authorizations for installment payments.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "purchaseDate", + "description": "Datetime of original purchase.", + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "recurringEnd", + "description": "The date after which no further recurring authorizations should be performed.", + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "recurringFrequency", + "description": "Integer value indicating the minimum number of days between recurring authorizations. A frequency of monthly is indicated by the value 28. Multiple of 28 days will be used to indicate months. Example: 6 months = 168.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecurePassThroughInput", + "description": "Results of a merchant-performed 3D Secure authentication.", + "fields": null, + "inputFields": [ + { + "name": "eciFlag", + "description": "The value of the electronic commerce indicator (ECI) flag, which indicates the outcome of the 3D Secure authentication.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ECommerceIndicator", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "cavv", + "description": "Cardholder authentication verification value or CAVV. The main encrypted message issuers and card networks use to verify authentication has occurred. Mastercard uses an AVV (Authentication Verification Value) message and American Express uses an AEVV (American Express Verification Value) message, each of which should also be passed in the cavv parameter.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "xId", + "description": "Transaction identifier resulting from 3D Secure authentication. Uniquely identifies the transaction and sometimes required in the authorization message. Must be base64-encoded. This field will no longer be used in 3D Secure 2 authentications.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "threeDSecureServerTransactionId", + "description": "3D Secure server transaction identifier resulting from 3D Secure authentication.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "version", + "description": "The version of 3D Secure authentication used for the transaction. Required on Visa and Mastercard authentications.", + "type": { + "kind": "SCALAR", + "name": "ThreeDSecureVersion", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "authenticationResponse", + "description": "The 3D Secure authentication response status code.", + "type": { + "kind": "SCALAR", + "name": "ThreeDSecureStatusCode", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "directoryServerResponse", + "description": "The 3D Secure directory server response.", + "type": { + "kind": "SCALAR", + "name": "ThreeDSecureStatusCode", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "cavvAlgorithm", + "description": "The algorithm used to generate the CAVV value. This is only returned for Mastercard SecureCode transactions (3DS 1.0).", + "type": { + "kind": "SCALAR", + "name": "ThreeDSecureCavvAlgorithm", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "directoryServerTransactionId", + "description": "A unique identifier for the 3D Secure 2 interaction with the card brand directory server. This field must be supplied for Mastercard Identity Check.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ThreeDSecureStatusCode", + "description": "A raw 3D Secure PARes or VARes response code (e.g. 'Y').", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ThreeDSecureVersion", + "description": "A 3D Secure authentication version. Must be composed of digits separated by periods (e.g. '1.0.2').", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Timestamp", + "description": "An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Times) timestamp with microsecond precision, in UTC.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TokenizeCreditCardInput", + "description": "Top-level input fields for tokenizing a credit card.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "creditCard", + "description": "Input fields for a credit card.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreditCardInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "options", + "description": "Credit card tokenization options.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TokenizeCreditCardOptionsInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TokenizeCreditCardOptionsInput", + "description": "Credit card tokenization options.", + "fields": null, + "inputFields": [ + { + "name": "validate", + "description": "Whether to run validations on credit card fields. Validations are not run by default.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TokenizeCreditCardPayload", + "description": "Top-level fields returned from a tokenized credit card.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "token", + "description": "A one-time-use reference to tokenized sensitive information.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `paymentMethod.id` instead." + }, + { + "name": "creditCard", + "description": "Details about the tokenized card.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "CreditCardDetails", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `paymentMethod.details` instead." + }, + { + "name": "singleUseToken", + "description": "A single-use payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `paymentMethod` instead." + }, + { + "name": "paymentMethod", + "description": "A single-use payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authenticationInsight", + "description": "Information about the [customer authentication regulation environment](https://developers.braintreepayments.com/guides/3d-secure/migration/javascript/v3#authentication-insight) that applies to the payment method when processed with the provided merchant account. This can be used to determine whether to perform 3D Secure authentication.", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AuthenticationInsightInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AuthenticationInsight", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use paymentMethod.details.threeDSecure.authenticationInsight instead." + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TokenizeCustomActionsPaymentMethodInput", + "description": "Top-level input fields for tokenizing Custom Actions.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customActionsPaymentMethod", + "description": "Input fields for a Custom Actions payment method.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CustomActionsPaymentMethodInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TokenizeCustomActionsPaymentMethodPayload", + "description": "Top-level fields returned from tokenizing a CustomActionsPaymentMethod.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethod", + "description": "A single-use payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TokenizeCvvInput", + "description": "Top-level input fields for tokenizing a CVV, otherwise known as CSC or CVC.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "cvv", + "description": "A 3 or 4 digit card verification value assigned to credit cards. The CVV will never be stored, but it can be provided with one-time requests to verify the card.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CVV", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TokenizeCvvPayload", + "description": "Top-level fields returned from a tokenized CVV.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tokenizedCvv", + "description": "A single-use tokenized CVV.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TokenizedCvv", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "singleUseToken", + "description": "A single-use payment method representing just a CVV.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "This mutation does not create a full PaymentMethod. Use `tokenizedCvv` instead." + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TokenizeNetworkTokenInput", + "description": "Top-level input field for tokenizing a network token.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "networkToken", + "description": "Input fields for a network token object.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "NetworkTokenInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TokenizeNetworkTokenPayload", + "description": "Top-level fields returned from a tokenized Network Token.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethod", + "description": "A single-use payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TokenizePayPalBillingAgreementInput", + "description": "Top-level input fields for tokenizing a PayPal account.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "billingAgreement", + "description": "Input fields for a PayPal Billing Agreement.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PayPalBillingAgreementInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TokenizePayPalBillingAgreementPayload", + "description": "Top-level fields returned from a tokenized PayPal account.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethod", + "description": "A single-use payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TokenizePayPalOneTimePaymentInput", + "description": "Top-level input fields for tokenizing a PayPal account.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "Braintree merchant account ID associated with the PayPal account to be used for the One-Time payment tokenization.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paypalOneTimePayment", + "description": "Input fields for a PayPal One-Time Payment.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PayPalOneTimePaymentInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TokenizePayPalOneTimePaymentPayload", + "description": "Top-level fields returned from a tokenized PayPal account.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethod", + "description": "A single-use payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TokenizeSamsungPayCardInput", + "description": "Top-level input field for tokenizing a Samsung Pay card.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "samsungPayCard", + "description": "Input fields for a Samsung Pay card.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SamsungPayCardInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TokenizeSamsungPayCardPayload", + "description": "Top-level fields returned from a tokenized Samsung Pay card.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "singleUseToken", + "description": "A one-time-use reference to tokenized sensitive information.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `paymentMethod` instead." + }, + { + "name": "paymentMethod", + "description": "A single-use payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TokenizeUsBankAccountInput", + "description": "Top-level input fields for tokenizing a US bank account.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "usBankAccount", + "description": "Input fields for a US bank account object.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UsBankAccountInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TokenizeUsBankAccountPayload", + "description": "Top-level fields returned from a tokenized US bank account.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethod", + "description": "A single-use payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TokenizeUsBankLoginInput", + "description": "Top-level input fields for tokenizing a US bank login.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "usBankLogin", + "description": "Input fields for a US bank login.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UsBankLoginInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TokenizedCvv", + "description": "A single-use, tokenized value representing a CVV (card verification value), otherwise known as CSC or CVC. This cannot be charged or authorized, since it is not a payment method, but it can be used alongside a multi-use credit card payment method.", + "fields": [ + { + "name": "id", + "description": "Unique identifier for the tokenized CVV.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Transaction", + "description": "A charge on a payment method.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legacyId", + "description": "Legacy unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date and time when the transaction was created.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethodSnapshot", + "description": "Snapshot of payment method details used to create the transaction, preserved at the time the transaction was created. This will always be present.", + "args": [], + "type": { + "kind": "UNION", + "name": "PaymentMethodSnapshot", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethod", + "description": "The multi-use payment method associated with the transaction. Only present if a multi-use payment method was used to create the transaction and it has not been deleted. The details of this PaymentMethod may have changed since the transaction was created; details used for the transaction can be found in the `paymentMethodSnapshot` field.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount charged in this transaction. For transactions that are partially captured, this amount will be the cummulative amount captured on this transaction. For transactions that are partially authorized, the amount will be less than the `initialRequestedAuthorizationAmount`.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "initialRequestedAuthorizationAmount", + "description": "The initial requested authorization amount for this transaction.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customFields", + "description": "Collection of custom field/value pairs. Custom fields are [defined in the Control Panel](https://articles.braintreepayments.com/control-panel/custom-fields#store-and-pass-back-fields).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CustomField", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchantId", + "description": "The ID of the merchant that processed this transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchantAccountId", + "description": "The ID of the merchant account that processed this transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchantName", + "description": "The display name of the merchant that processed this transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchantAddress", + "description": "The address of the merchant that processed this transaction.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Address", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "orderId", + "description": "The order ID for this transaction. For PayPal transactions, the PayPal Invoice ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "purchaseOrderNumber", + "description": "A purchase order identification value you associate with this transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The current status of this transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processorResponse", + "description": "Fields describing the payment processor response.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionAuthorizationProcessorResponse", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use relevant events in `statusHistory` instead." + }, + { + "name": "riskData", + "description": "Risk data evaluated for this transaction.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "RiskData", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "descriptor", + "description": "Fields used to define what will appear on customers' credit card statements for a specific purchase.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionDescriptor", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "statusHistory", + "description": "The records of all statuses this transaction has passed through, with additional information on why each status occurred. Returned in reverse chronological order, with the most recent event first in the list.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "channel", + "description": "If the transaction request was performed through a shopping cart provider or Braintree partner, this field will have a string identifier for that shopping cart provider or partner. For PayPal transactions, this maps to the PayPal account's bn_code.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "How the transaction was created.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customer", + "description": "Customer associated with the transaction, if applicable.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Customer", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "shipping", + "description": "Shipping information.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionShipping", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tax", + "description": "Tax information.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionTaxInformation", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "scaExemptionRequested", + "description": "The type of Strong Customer Authentication Exemption that was requested for this transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ScaExemptionType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discountAmount", + "description": "Discount amount that was included in the total transaction amount.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lineItems", + "description": "Line items for this transaction.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TransactionLineItem", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "refunds", + "description": "The list of refunds issued against this transaction.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Refund", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "partialCaptureDetails", + "description": "For transactions created or captured using the `partialCaptureTransaction` mutation. This field links a given transaction to its original authorization or all its partial captures.", + "args": [], + "type": { + "kind": "UNION", + "name": "PartialCaptureDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "disputes", + "description": "A collection of disputes associated with the transaction.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Dispute", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "facilitatorDetails", + "description": "If the transaction request was performed using payment information from a third party via the Grant API, Shared Vault or Google Pay, these fields will capture information about the third party. These fields are primarily useful for the merchant of record.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "FacilitatorDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "disbursementDetails", + "description": "The disbursement details associated with this transaction. This field is only available after the transaction is SETTLED and if you have an eligible merchant account.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "DisbursementDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "billingAddress", + "description": "The billing address associated with the transaction.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Address", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authorizationAdjustments", + "description": "A collection of AuthorizationAdjustments associated with the transaction.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "AuthorizationAdjustment", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "retried", + "description": "Whether or not the transaction was automatically retried by Braintree's internal systems.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "installmentDetails", + "description": "Installment details associated with the transaction.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "TransactionInstallmentDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentInitiatedAt", + "description": "The transaction date and time as reported by the in-store payment terminal.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "Payment", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionAuthorizationAdjustmentProcessorResponse", + "description": "Record of processor response data received in response to authorization adjustment requests.", + "fields": [ + { + "name": "legacyCode", + "description": "The [processor response code](https://developers.braintreepayments.com/reference/general/processor-responses/authorization-responses) indicating the result of attempting the adjustment.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": "The text explanation of the processor response code.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "declineType", + "description": "Whether or not the decline is the result of a temporary issue. Only present if adjustment is declined.", + "args": [], + "type": { + "kind": "ENUM", + "name": "ProcessorDeclineType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionAuthorizationProcessorResponse", + "description": "Detailed response information from the processor when attempting to authorize a transaction.", + "fields": [ + { + "name": "legacyCode", + "description": "A code based on the response from the processor, indicating the result of attempting to authorize this transaction. See the [list of possible processor response codes for authorization](https://developers.braintreepayments.com/reference/general/processor-responses/authorization-responses).", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": "The text explanation of the processor response legacyCode.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cvvResponse", + "description": "The processing bank's response to the provided CVV.", + "args": [], + "type": { + "kind": "ENUM", + "name": "AvsCvvResponseCode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "avsPostalCodeResponse", + "description": "The processing bank's response to the provided billing postal or zip code.", + "args": [], + "type": { + "kind": "ENUM", + "name": "AvsCvvResponseCode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "avsStreetAddressResponse", + "description": "The processing bank's response to the provided billing street address.", + "args": [], + "type": { + "kind": "ENUM", + "name": "AvsCvvResponseCode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "authorizationId", + "description": "The processor's unique ID or \"code\" for the authorization.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "additionalInformation", + "description": "If present, any additional information recieved from the processor. May provide further insight into the `legacyCode`.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "retrievalReferenceNumber", + "description": "The processor's reference number for the authorization.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "emvData", + "description": "Response EMV data provided by the processor if this was an EMV transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionConnection", + "description": "A paginated list of transactions.", + "fields": [ + { + "name": "edges", + "description": "A list of transactions.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TransactionConnectionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information about the page of transactions contained in `edges`.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionConnectionEdge", + "description": "A transaction within a TransactionConnection.", + "fields": [ + { + "name": "cursor", + "description": "This transaction's location within the TransactionConnection. Used for requesting additional pages.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The transaction.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TransactionCustomerDetailsInput", + "description": "Customer details to be stored on the transaction itself, if the transaction is not associated with a customer. Used for fraud detection purposes.", + "fields": null, + "inputFields": [ + { + "name": "email", + "description": "Email address for the customer.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "phoneNumber", + "description": "Phone number for the customer.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionDescriptor", + "description": "Fields used to define what will appear on a customer's bank statement for a specific purchase.", + "fields": [ + { + "name": "name", + "description": "The value in the business name field of a customer's statement.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "phone", + "description": "The value in the phone number field of a customer's statement.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": "The value in the URL/web address field of a customer's statement.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TransactionDescriptorInput", + "description": "Fields used to define what will appear on a customer's bank statement for a specific purchase.", + "fields": null, + "inputFields": [ + { + "name": "name", + "description": "The value in the business name field of a customer's statement.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "phone", + "description": "The value in the phone number field of a customer's statement.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "url", + "description": "The value in the URL/web address field of a customer's statement.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TransactionExternalVaultOptionsInput", + "description": "Input for transactions created with credit cards vaulted in an external vault, not the Braintree Vault. Do not use for transactions created from Braintree multi-use payment methods, or from single-use payment methods which will not be stored in an external vault.", + "fields": null, + "inputFields": [ + { + "name": "status", + "description": "The credit card's assocation with an external vault.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "ExternalVaultStatus", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "verifyingNetworkTransactionId", + "description": "The network transaction ID of the first _transaction_ after which this payment method was stored in the external vault. If the `status` is `WILL_VAULT`, do not pass this value; the network transaction ID of the resulting transaction can be passed in this field for _subsequent_ transactions. If the `status` is `VAULTED`, but the customer is directly initiating the charge, do not pass this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TransactionInput", + "description": "Input fields for creating a transaction.", + "fields": null, + "inputFields": [ + { + "name": "amount", + "description": "Billing amount of the request. This value must be greater than 0, and must match the currency format of the merchant account. This can only contain numbers and one decimal point (e.g. x.xx). Can't be greater than the maximum allowed by the processor.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "Merchant account ID used to process the transaction. Currency is also determined by merchant account ID. If no merchant account ID is specified, we will use your default merchant account.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "orderId", + "description": "Additional information about the transaction. On PayPal transactions, this field maps to the PayPal invoice number. PayPal invoice numbers must be unique in your PayPal business account. Maximum 255 characters or 127 for PayPal transactions.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "exchangeRateQuoteId", + "description": "ID of exchange rate quote to be used for the transaction.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "purchaseOrderNumber", + "description": "A purchase order identification value you associate with this transaction.\n\n*Required for Level 2 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "riskData", + "description": "Customer device information, which is sent directly to supported processors for fraud analysis.", + "type": { + "kind": "INPUT_OBJECT", + "name": "RiskDataInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customFields", + "description": "Collection of custom field/value pairs. You must [set up each custom field in the Control Panel](https://articles.braintreepayments.com/control-panel/custom-fields#creating-a-custom-field) prior to passing it with a request.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CustomFieldInput", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "descriptor", + "description": "Fields used to define what will appear on a customer's bank statement for a specific purchase.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionDescriptorInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "recurring", + "description": "Deprecated: This field is included for supporting legacy clients. Please use `paymentInitiator` instead.", + "type": { + "kind": "ENUM", + "name": "RecurringType", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentInitiator", + "description": "The initiator of the payment. Payment can either be merchant-initiated or customer-initiated. If the transaction is an ecommerce transaction initiated by the customer, no value is passed.", + "type": { + "kind": "ENUM", + "name": "PaymentInitiator", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "channel", + "description": "For partners and shopping carts only. If you are a shopping cart provider or other Braintree partner, pass a string identifier for your service. For PayPal transactions, this maps to paypal.bn_code.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customerId", + "description": "If charging a single-use payment method, optional ID of a customer to associate the transaction with. If vaulting the single-use payment method, this customer will be associated with the resulting multi-use payment method.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "shipping", + "description": "Shipping information.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionShippingInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "tax", + "description": "Tax information about the transaction.\n\n*Required for Level 2 processing*.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionTaxInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "discountAmount", + "description": "Discount amount that was included in the total transaction amount. Does not add to the total amount the payment method will be charged. This value can't be negative. Please note that this field is not used on PayPal transactions.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "lineItems", + "description": "Line items for this transaction. Up to 249 line items may be specified.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TransactionLineItemInput", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "threeDSecurePassThrough", + "description": "Deprecated: This field is included for supporting legacy clients. This field is specific to credit card payment methods only, and cannot be applied to transactions with other payment method types. If you need to pass this field, please use `authorizeCreditCard` or `chargeCreditCard`. See the `CreditCardTransactionOptionsInput` type for details.\n\nResults of a merchant-performed 3D Secure authentication. You will only need to use these fields if you've performed your own integration with a 3D Secure MPI provider (e.g. Cardinal Centinel). Otherwise, Braintree's SDKs handle this for you in our standard 3D Secure integration.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecurePassThroughInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "vaultPaymentMethodAfterTransacting", + "description": "When a single-use payment method is used to create this transaction, it can be automatically stored in the vault after transacting. If this field is left blank, the single-use payment method will not be vaulted.", + "type": { + "kind": "INPUT_OBJECT", + "name": "VaultPaymentMethodAfterTransactingInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customerDetails", + "description": "Customer information to be stored on the transaction and used for fraud protection. Use this if you wish to pass customer information on a transaction without creating an independent stored customer record in the vault.\n\nThis parameter can only be used if you do not pass `customerId`, and if you are not using a vaulted/multi-use payment method. In other words, this field is only valid when the transaction will not be associated with an existing customer.\n\nIf `vaultPaymentMethodAfterTransacting` is also passed, these values will be used when creating a new customer for the newly-vaulted payment method.", + "type": { + "kind": "INPUT_OBJECT", + "name": "TransactionCustomerDetailsInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionInstallment", + "description": "Transaction Installment information.", + "fields": [ + { + "name": "id", + "description": "Installment ID.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "projectedDisbursementDate", + "description": "The projected date for the funds associated with this installment to be disbursed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "actualDisbursementDate", + "description": "The date that the funds associated with this installment were actually disbursed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "Installment amount.The total transaction amount is split equally into each installment.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "adjustments", + "description": "List of adjustments associated with the installment.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TransactionInstallmentAdjustment", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionInstallmentAdjustment", + "description": "Adjustment information.", + "fields": [ + { + "name": "projectedDisbursementDate", + "description": "The projected date for the funds associated with the adjustements to be disbursed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "actualDisbursementDate", + "description": "The date that the funds associated with this adjustments were actually disbursed.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "Adjustment amount for the installment.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": "Transaction Installment Adjustment type.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "TransactionInstallmentAdjustmentType", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "TransactionInstallmentAdjustmentType", + "description": "Transaction Installment Adjustment type to indicate the reason for the adjustment.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "DISPUTE", + "description": "Dispute.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "REFUND", + "description": "Refund.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionInstallmentDetails", + "description": "Installment details for the transaction.", + "fields": [ + { + "name": "count", + "description": "The installment count associated with the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "installments", + "description": "List of installments associated with the transaction.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TransactionInstallment", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionLevelFeeReport", + "description": "The [transaction-level fee report](https://articles.braintreepayments.com/control-panel/reporting/transaction-level-fee-report) provides a breakdown of fees per individual transactions and refunds. This type is no longer in use; see `PaymentLevelFeeReport` instead.", + "fields": [ + { + "name": "url", + "description": "The URL where you can access the requested report.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionLineItem", + "description": "Data for individual line items on a transaction.", + "fields": [ + { + "name": "name", + "description": "Item name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "kind", + "description": "Indicates whether the line item is a sale or refund.", + "args": [], + "type": { + "kind": "ENUM", + "name": "TransactionLineItemType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "quantity", + "description": "Number of units of the item purchased.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unitAmount", + "description": "Per-unit price of the item.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalAmount", + "description": "Total price amount for the line item, i.e. quantity multiplied by unit amount.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unitTaxAmount", + "description": "Per-unit tax price of the item.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "taxAmount", + "description": "Tax amount for the line item.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discountAmount", + "description": "The discount amount of the line item.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unitOfMeasure", + "description": "The unit of measure or the unit of measure code.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "productCode", + "description": "Product or UPC code for the item.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "commodityCode", + "description": "Code used to classify items purchased and track the total amount spent across various categories of products and services. Different corporate purchasing organizations may use different standards, but the [United Nations Standard Products and Services Code (UNSPSC)](https://www.unspsc.org/) is frequently used.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": "Item description.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": "The URL to product information.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "itemType", + "description": "The type of the line item, i.e., physical, digital etc.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "imageUrl", + "description": "URL to an image that represents the product. Max 1024 characters.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TransactionLineItemInput", + "description": "Data for individual line items on a transaction.", + "fields": null, + "inputFields": [ + { + "name": "name", + "description": "Item name. Maximum 35 characters, or 127 characters for PayPal transactions.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "kind", + "description": "Indicates whether the line item is a sale or refund.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "TransactionLineItemType", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "quantity", + "description": "Number of units of the item purchased. Can include up to 4 decimal places. This value can't be negative or zero.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "unitAmount", + "description": "Per-unit price of the item. Maximum 4 decimal places, or 2 decimal places for PayPal transactions. This value can't be negative or zero.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "totalAmount", + "description": "Total price amount for the line item: quantity multiplied by unitAmount. Can include up to 2 decimal places.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "unitTaxAmount", + "description": "Per-unit tax price of the item. Can include up to 2 decimal places. This value can't be negative or zero.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "taxAmount", + "description": "Tax amount for the line item. Can include up to 2 decimal places. This value can't be negative.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "discountAmount", + "description": "Amount of discount for the line item. Can include up to 2 decimal places. This value can't be negative. Please note that this field is not used on PayPal transactions.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "unitOfMeasure", + "description": "The unit of measure or the unit of measure code. Maximum 12 characters.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "productCode", + "description": "Product or UPC code for the item. Maximum 12 characters, or 127 characters for PayPal transactions.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "commodityCode", + "description": "Code used to classify items purchased and track the total amount spent across various categories of products and services. Different corporate purchasing organizations may use different standards, but the [United Nations Standard Products and Services Code (UNSPSC)](https://www.unspsc.org/) is frequently used. Maximum 12 characters.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "description", + "description": "Item description. Maximum 127 characters.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "url", + "description": "A URL to information about the product.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "itemType", + "description": "The type of the line item, i.e., physical, digital etc.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "imageUrl", + "description": "URL to an image that represents the product. Max 1024 characters.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "TransactionLineItemType", + "description": "Indicates whether a transaction line item is a debit (sale) or credit (refund).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CREDIT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DEBIT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionPayload", + "description": "Top-level output field from creating a transaction.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "transaction", + "description": "The transaction representing the charge on the payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "TransactionReversal", + "description": "A union of all possible results of a transaction reversal. If the transaction is settled, a refund will be issued and a Refund object will be returned. Otherwise, the transaction will be voided and a Transaction object will be returned.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Refund", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Transaction", + "ofType": null + } + ] + }, + { + "kind": "INPUT_OBJECT", + "name": "TransactionSearchInput", + "description": "Input fields for searching for transactions.", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "Find transactions with an ID or IDs.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "status", + "description": "Find transactions with a given transaction status.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTransactionStatusInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "statusTransition", + "description": "Find transactions based on the given transaction status transition times.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTransactionStatusTransitionInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "createdAt", + "description": "Find transactions based on the time they were created.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTimestampInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "Find transactions for a given amount or currency.", + "type": { + "kind": "INPUT_OBJECT", + "name": "MonetaryAmountSearchInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "orderId", + "description": "Find transactions with a given orderId.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "Find payments processed through a merchant account ID or IDs.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customer", + "description": "Find transactions with a given customer.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentCustomerInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodSnapshotType", + "description": "Find transactions created by charging payment methods of the given type.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentMethodSnapshotTypeInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "disbursementDate", + "description": "Find transactions by their disbursement date. Only use this search criteria if you have an eligible merchant account. Note that transactions can only be disbursed after they reach the SETTLED status.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchDateInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "source", + "description": "Find transactions created with a given transaction source.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTransactionSourceInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "settlementBatchId", + "description": "Find transactions by the batch ID under which the transaction was submitted for settlement.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTextInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethod", + "description": "Find transactions based on information about the payment method used for the transaction.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchPaymentPaymentMethodInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "facilitatorOAuthApplicationClientId", + "description": "Find transactions created by a third party via the Grant API using a given OAuth application client ID.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "userId", + "description": "Find transactions with a user ID or IDs.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "storeId", + "description": "Find transactions by the ID of the store that the transaction was processed in.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionSettlementProcessorResponse", + "description": "Detailed response information from the processor when attempting to settle a transaction.", + "fields": [ + { + "name": "legacyCode", + "description": "A code based on the response from the processor, indicating the result of attempting to settle this transaction. See the [list of possible processor response codes for settlement](https://developers.braintreepayments.com/reference/general/processor-responses/settlement-responses).", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": "The text explanation of the processor response legacyCode.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cvvResponse", + "description": "The processing bank's response to the provided CVV.", + "args": [], + "type": { + "kind": "ENUM", + "name": "AvsCvvResponseCode", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "AVS and CVV checks do not take place when capturing a transaction, only when authorizing. Use the `processorResponse` on an authorization-related `PaymentStatusEvent` instead." + }, + { + "name": "avsPostalCodeResponse", + "description": "The processing bank's response to the provided billing postal or zip code.", + "args": [], + "type": { + "kind": "ENUM", + "name": "AvsCvvResponseCode", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "AVS and CVV checks do not take place when capturing a transaction, only when authorizing. Use the `processorResponse` on an authorization-related `PaymentStatusEvent` instead." + }, + { + "name": "avsStreetAddressResponse", + "description": "The processing bank's response to the provided billing street address.", + "args": [], + "type": { + "kind": "ENUM", + "name": "AvsCvvResponseCode", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "AVS and CVV checks do not take place when capturing a transaction, only when authorizing. Use the `processorResponse` on an authorization-related `PaymentStatusEvent` instead." + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionShipping", + "description": "Information related to shipping a physical product.", + "fields": [ + { + "name": "shippingAddress", + "description": "Shipping address information.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Address", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "shippingAmount", + "description": "The shipping cost of the entire transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "shipsFromPostalCode", + "description": "The postal code of the source shipping location.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TransactionShippingInput", + "description": "Information related to shipping a physical product.", + "fields": null, + "inputFields": [ + { + "name": "shippingAddress", + "description": "Shipping destination address information.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AddressInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "shippingAmount", + "description": "Shipping cost on the entire transaction.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "shipsFromPostalCode", + "description": "The postal code of the source shipping location, in any country's format.\n\n*Required for Level 3 processing*.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TransactionTaxInformation", + "description": "Information related to taxes on the transaction.", + "fields": [ + { + "name": "taxAmount", + "description": "The amount of tax that was included in the total transaction amount.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "taxExempt", + "description": "Whether the transaction should be considered eligible for tax exemption.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "TransactionTaxInput", + "description": "Information related to taxes on the transaction.", + "fields": null, + "inputFields": [ + { + "name": "taxAmount", + "description": "Amount of tax that was included in the total transaction amount. Does not add to the total amount the payment method will be charged.\n\n*Required for Level 2 processing* unless `taxExempt` is `true`.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "taxExempt", + "description": "Whether the transaction should be considered eligible for tax exemption.\n\n*Required for Level 2 processing*.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "URL", + "description": "A URL string\npattern: [a-zA-Z0-9+-.]:\\\\/\\\\/([-a-zA-Z0-9@:%._\\\\+~#=]{2,256}\\\\.?[a-z]{2,4}\\\\b([-a-zA-Z0-9@:%_\\\\+.~#?&//=]*))?", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UnionPayConfiguration", + "description": "Configuration for UnionPay cards.", + "fields": [ + { + "name": "merchantAccountId", + "description": "The Braintree merchant account ID with UnionPay processing enabled.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateCreditCardBillingAddressInput", + "description": "Top-level input fields for updating a multi-use credit card to use a new billing address.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "The multi-use credit card for which the billing address will be updated or added.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "billingAddress", + "description": "The new billing address.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AddressInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "ID of the merchant account that will be used when verifying the credit card with the new billing address.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "verification", + "description": "Input fields that specify options for verifying the credit card with the new billing address. By default, a verification will be performed. If the verification fails, the update will not be performed.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CreditCardVerificationOptionsInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UpdateCreditCardBillingAddressPayload", + "description": "Top-level fields returned when updating a multi-use credit card to a new billing address.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "billingAddress", + "description": "The new billing address. Will be `null` if a failed verification prevented the update.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Address", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "verification", + "description": "The verification that was run on the payment method prior to updating the billing address, if present.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Verification", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateCustomerInput", + "description": "Top-level field for updating a customer.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customerId", + "description": "ID of the customer to be updated.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "customer", + "description": "Input fields for the updates to be made on the customer.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CustomerInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UpdateCustomerPayload", + "description": "Top-level fields returned when updating a customer.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customer", + "description": "Information about the customer that was updated.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Customer", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateInStoreLocationInput", + "description": "Input fields for updating an in-store location.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "locationId", + "description": "ID of the location to be updated.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "location", + "description": "Input fields to update an in-store location.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InStoreLocationUpdateInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UpdateInStoreLocationPayload", + "description": "Top-level fields returned when creating an in-store location.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "location", + "description": "The in-store location.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "InStoreLocation", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateInStoreReaderInput", + "description": "Input fields for updating an in-store reader.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "readerId", + "description": "The ID of the in-store reader to update.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "name", + "description": "The new name for the in-store reader.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "locationId", + "description": "The new location ID for the in-store reader.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateTransactionAmountInput", + "description": "Top-level input fields for a updating a transaction's amount.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transactionId", + "description": "ID of the transaction on which to perform the adjustment.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "The new total amount to be authorized on a transaction. This value must be greater than 0, and must match the currency format of the merchant account, and cannot be greater than the maximum allowed by the processor.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateTransactionCustomFieldsInput", + "description": "Input for creating or updating custom fields on a transaction.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "transactionId", + "description": "The ID of the transaction to update.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "customFields", + "description": "The list of custom fields to update. You must [set up each custom field in the Control Panel](https://articles.braintreepayments.com/control-panel/custom-fields#creating-a-custom-field) prior to passing it with a request.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CustomFieldInput", + "ofType": null + } + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UpdateTransactionCustomFieldsPayload", + "description": "Top-level output field from updating custom fields for a specific transaction.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customFields", + "description": "A list of all custom fields on the updated transaction. Custom fields are [defined in the Control Panel](https://articles.braintreepayments.com/control-panel/custom-fields#store-and-pass-back-fields).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CustomField", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UsBankAccountAchMandate", + "description": "Details about the customer's acceptance of ACH terms.", + "fields": [ + { + "name": "acceptanceText", + "description": "The text the customer agreed to when setting up ACH.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "acceptedAt", + "description": "Date and time when the text terms were accepted.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UsBankAccountBillingAddressInput", + "description": "A billing address for a US bank account. This is a subset of the fields required on `AddressInput`.", + "fields": null, + "inputFields": [ + { + "name": "streetAddress", + "description": "The street address.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "extendedAddress", + "description": "The extended address information—such as an apartment or suite number.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "city", + "description": "The city.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "state", + "description": "The state.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "UsStateCode", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "zipCode", + "description": "The ZIP code.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UsZipCode", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UsBankAccountBusinessOwnerInput", + "description": "The name of the owner of a business US bank account.", + "fields": null, + "inputFields": [ + { + "name": "businessName", + "description": "The name of the business that owns the account.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UsBankAccountConfiguration", + "description": "Configuration for US bank account processing.", + "fields": [ + { + "name": "routeId", + "description": "The route ID used to process a US bank account payment.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "plaidPublicKey", + "description": "The public key for Plaid to use to log in to a bank account.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UsBankAccountDetails", + "description": "Details about a US bank account.", + "fields": [ + { + "name": "accountholderName", + "description": "The name of the accountholder. This is either the business name for a business account, or the owner's full name for an individual account.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "accountType", + "description": "The bank account type.", + "args": [], + "type": { + "kind": "ENUM", + "name": "UsBankAccountType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ownershipType", + "description": "The ownership type of the account, i.e. business or personal.", + "args": [], + "type": { + "kind": "ENUM", + "name": "UsBankAccountOwnershipType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bankName", + "description": "The name of the bank at which the account exists.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last4", + "description": "The last four digits of the bank account number.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "routingNumber", + "description": "The routing number of the bank.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "verified", + "description": "Whether or not the bank account has been verified and can be transacted on.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "achMandate", + "description": "NACHA-mandated proof of acceptance of ACH terms.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "UsBankAccountAchMandate", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UsBankAccountIndividualOwnerInput", + "description": "The name of the owner of a personal US bank account.", + "fields": null, + "inputFields": [ + { + "name": "firstName", + "description": "The first name of the accountholder.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "lastName", + "description": "The last name of the accountholder.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UsBankAccountInput", + "description": "Input fields for a US bank account object.", + "fields": null, + "inputFields": [ + { + "name": "accountNumber", + "description": "The account number of the bank account.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UsBankAccountNumber", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "routingNumber", + "description": "The routing number of the bank that holds the account.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UsBankRoutingNumber", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "accountType", + "description": "The type of account.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "UsBankAccountType", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "businessOwner", + "description": "Information about the business that owns the account. This should only be specified for business accounts.", + "type": { + "kind": "INPUT_OBJECT", + "name": "UsBankAccountBusinessOwnerInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "individualOwner", + "description": "Information about the individual that owns the account. This should only be specified for individual accounts.", + "type": { + "kind": "INPUT_OBJECT", + "name": "UsBankAccountIndividualOwnerInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "billingAddress", + "description": "The billing address of the account.", + "type": { + "kind": "INPUT_OBJECT", + "name": "UsBankAccountBillingAddressInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "achMandate", + "description": "Language used to prove that you have the customer's explicit permission to debit their bank account.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "UsBankAccountNumber", + "description": "An account number containing 1-17 digits.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "UsBankAccountOwnershipType", + "description": "The ownership type of US Bank Account.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "BUSINESS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PERSONAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "UsBankAccountType", + "description": "The type of US Bank Account.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "CHECKING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SAVINGS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNKNOWN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UsBankAccountVerificationDetails", + "description": "Information specific to verifications of US bank account payment methods.", + "fields": [ + { + "name": "method", + "description": "Type of US bank account verification performed.", + "args": [], + "type": { + "kind": "ENUM", + "name": "UsBankAccountVerificationMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "verificationDeterminedAt", + "description": "Time at which the verification was determined to be successful or not. If successful, at this time the payment method will be marked `verified` and you will be able to charge it.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "UsBankAccountVerificationMethod", + "description": "The type of verification on a US bank account payment method. See our [ACH guide](https://articles.braintreepayments.com/guides/payment-methods/ach#verification-methods).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "INDEPENDENT_CHECK", + "description": "Verification conducted independently by the merchant, not through Braintree.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MICRO_TRANSFERS", + "description": "Verification by micro-deposits transferred to the bank account, which the customer must then confirm. The most reliable method, but takes additional time.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NETWORK_CHECK", + "description": "Verification via account information. Will complete the verification process immediately, but is not supported by all banks.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TOKENIZED_CHECK", + "description": "Verification at the point of tokenization. Requires integration with a third-party provider. Because this requires a different tokenization flow, this method of verification is only supported for vaulting tokenized US bank account logins, and is not supported when re-verifying a US bank account payment method.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UsBankLoginInput", + "description": "Input fields for a US bank login object.", + "fields": null, + "inputFields": [ + { + "name": "publicToken", + "description": "The public token returned from the bank login.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "accountId", + "description": "The login provider account ID used for the bank login.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "accountType", + "description": "The type of account.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "UsBankAccountType", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "businessOwner", + "description": "Information about the business that owns the account. This should only be specified for business accounts.", + "type": { + "kind": "INPUT_OBJECT", + "name": "UsBankAccountBusinessOwnerInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "individualOwner", + "description": "Information about the individual that owns the account. This should only be specified for individual accounts.", + "type": { + "kind": "INPUT_OBJECT", + "name": "UsBankAccountIndividualOwnerInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "billingAddress", + "description": "The billing address of the account.", + "type": { + "kind": "INPUT_OBJECT", + "name": "UsBankAccountBillingAddressInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "achMandate", + "description": "Language used to prove that you have the customer's explicit permission to debit their bank account.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "UsBankRoutingNumber", + "description": "A routing number containing 8 or 9 digits.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "UsStateCode", + "description": "A two-letter code representing a US state or territory.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "AK", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AZ", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "CT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GU", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "HI", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "IA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ID", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "IL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "IN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "KS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "KY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ME", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MI", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MO", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MP", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ND", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NH", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NJ", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NM", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NV", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OH", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OK", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RI", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TX", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UM", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VI", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "WA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "WI", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "WV", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "WY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "UsZipCode", + "description": "A US ZIP code. Supports DDDDD and DDDDD-DDDD formats.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "User", + "description": "Details about the user.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": "Email address.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "Current status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "UserStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "Full name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "roles", + "description": "Associated roles.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Role", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "UserStatus", + "description": "The status of the user.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ACTIVE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DELETED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PASSIVE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PENDING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUSPENDED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VaultCreditCardExternalVaultOptionsInput", + "description": "Options used to indicate when a credit card is externally vaulted.", + "fields": null, + "inputFields": [ + { + "name": "verifyingNetworkTransactionId", + "description": "For use if this credit card is stored in an external vault. The network transaction ID of the first _transaction_ after which this credit card was stored in the external vault.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VaultCreditCardInput", + "description": "Top-level input field for vaulting a credit card so it can be used multiple times.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of an existing single-use credit card payment method to be vaulted.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "verification", + "description": "Input fields that specify options for verifying the credit card before vaulting. By default, a verification will be performed. If the verification fails, the credit card will not be vaulted.", + "type": { + "kind": "INPUT_OBJECT", + "name": "VaultCreditCardVerificationOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "externalVault", + "description": "Options used to indicate when a credit card is externally vaulted.", + "type": { + "kind": "INPUT_OBJECT", + "name": "VaultCreditCardExternalVaultOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customerId", + "description": "ID of the customer to associate the resulting multi-use payment method with.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "accountType", + "description": "The type of account to be used when verifying a combo card.", + "type": { + "kind": "ENUM", + "name": "CardAccountType", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "billingAddress", + "description": "A billing address to associate with the vaulted credit card. If billing address data was included when tokenizing the credit card, it will be *merged* with this input value.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AddressInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "threeDSecurePassThrough", + "description": "Results of a merchant-performed 3D Secure authentication. You will only need to use these fields if you've performed your own integration with a 3D Secure MPI provider (e.g. Cardinal Centinel). Otherwise, Braintree's SDKs handle this for you in our standard 3D Secure integration.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecurePassThroughInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "riskData", + "description": "Customer device information, which is sent directly to supported processors for fraud analysis.", + "type": { + "kind": "INPUT_OBJECT", + "name": "RiskDataInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VaultCreditCardVerificationOptionsInput", + "description": "Input fields that specify options for verifying the vaulted credit card.", + "fields": null, + "inputFields": [ + { + "name": "merchantAccountId", + "description": "ID of the merchant account to use when verifying the credit card. The verification will use the default merchant account if this field is left blank.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "skip", + "description": "Whether to opt out of verifying the credit card. Defaults to `false` for credit cards that support verification. Clients should only pass `true` in the uncommon scenario that the credit card has been verified externally to Braintree.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "amount", + "description": "The amount to use to verify the credit card.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "fraudTools", + "description": "Control which fraud tools will be applied to this transaction. Fraud tools cannot be retroactively applied to a transaction if skipped.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CreditCardFraudToolsOptionsInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VaultInStorePaymentMethodAfterTransactingInput", + "description": " Specifies behavior for vaulting a single-use payment method for an in-store transaction.", + "fields": null, + "inputFields": [ + { + "name": "when", + "description": "Specifies the criteria which must be met to vault this payment method.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VaultPaymentMethodCriteria", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "qrcOverride", + "description": "Vaulting behavior override for QR code payments.", + "type": { + "kind": "ENUM", + "name": "VaultQRCOverride", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VaultLimitedUsePayPalAccountOptionsInput", + "description": "Input fields that provide information about the resulting PayPal account.", + "fields": null, + "inputFields": [ + { + "name": "amount", + "description": "The total amount of the order. This will be the limit to how much may be captured on the resulting payment method.", + "type": { + "kind": "SCALAR", + "name": "Amount", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customField", + "description": "Variable passed directly to PayPal for your own tracking purposes. Customers do not see this value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "description", + "description": "Description of the transaction that is displayed to customers in PayPal email receipts.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "orderId", + "description": "The PayPal invoice number. It must be unique in your PayPal business account and can contain a maximum of 127 characters. If specified, transactions created from the resulting payment method will have this orderId.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "shippingAddress", + "description": "Shipping destination address information.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AddressInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VaultPayPalBillingAgreementInput", + "description": "Top-level input fields for importing and vaulting a PayPal Billing Agreement.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "billingAgreementId", + "description": "ID of a PayPal Billing Agreement, that was not created through Braintree, to import and vault.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "customerId", + "description": "Optional ID of the customer to associate the resulting payment method with.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "Optional ID of the merchant account associated with the linked PayPal account to be used to retrieve billing agreement details from PayPal. Only used for merchants with the PayPal multi-account feature enabled in Braintree.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "indirectPayee", + "description": "The merchant (payee) PayPal account associated with the PayPal Billing Agreement being vaulted. Only used when the specified merchant account is specially configured to handle indirect PayPal accounts.", + "type": { + "kind": "INPUT_OBJECT", + "name": "PayPalAccountInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VaultPayPalBillingAgreementPayload", + "description": "Top-level fields returned when importing and vaulting a PayPal Billing Agreement.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethod", + "description": "The vaulted payment method containing the imported PayPal Billing Agreement.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VaultPaymentMethodAfterTransactingInput", + "description": " Specifies behavior for vaulting a single-use payment method after transacting with it.", + "fields": null, + "inputFields": [ + { + "name": "when", + "description": "Specifies the criteria which must be met to vault this payment method.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VaultPaymentMethodCriteria", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "VaultPaymentMethodCriteria", + "description": "Defines criteria for vaulting a single-use payment method after transacting with it.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ALWAYS", + "description": "Always store the single-use payment method after transacting, regardless of the status of the transaction.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ON_SUCCESSFUL_TRANSACTION", + "description": "Only store the single-use payment method if it was successfully authorized.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VaultPaymentMethodInput", + "description": "Top-level input field for vaulting a payment method so it can be used multiple times.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of an existing single-use payment method to be vaulted.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "verificationMerchantAccountId", + "description": "Deprecated: This field is included for supporting legacy clients. Please use `verification.merchantAccountId` instead.\n\nID of the merchant account to use when verifying the payment method.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "verification", + "description": "Input fields that specify options for verifying the payment method before vaulting. Only applicable if the payment method is of a type that supports verification. For supported types, verification is performed by default. If the verification fails, the payment method will not be vaulted. For additional, payment method-specific verification options, please see other verification mutations such as `verifyCreditCard` or `verifyUsBankAccount`.", + "type": { + "kind": "INPUT_OBJECT", + "name": "PaymentMethodVerificationOptionsInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customerId", + "description": "ID of the customer to associate the resulting multi-use payment method with.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "threeDSecurePassThrough", + "description": "Results of a merchant-performed 3D Secure authentication. You will only need to use these fields if you've performed your own integration with a 3D Secure MPI provider (e.g. Cardinal Centinel). Otherwise, Braintree's SDKs handle this for you in our standard 3D Secure integration.", + "type": { + "kind": "INPUT_OBJECT", + "name": "ThreeDSecurePassThroughInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "riskData", + "description": "Customer device information, which is sent directly to supported processors for fraud analysis.", + "type": { + "kind": "INPUT_OBJECT", + "name": "RiskDataInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VaultPaymentMethodPayload", + "description": "Top-level output field from vaulting a payment method.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethod", + "description": "A payment method that has been stored in a merchant's vault and can be reused.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "verification", + "description": "The verification that was run on the payment method prior to vaulting.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Verification", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "VaultQRCOverride", + "description": "The override options for QR code vaulting.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "HIDE_QRC", + "description": "Do not show QR code as a payment option, even if it is enabled.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SHOW_QRC_NO_VAULT", + "description": "If QR codes are enabled, show as a payment option, but do not vault.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VaultUsBankAccountInput", + "description": "Top-level input field for vaulting a bank account so it can be used multiple times.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of an existing single-use payment method to be vaulted.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "verificationMerchantAccountId", + "description": "ID of the merchant account to use when verifying the payment method.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "customerId", + "description": "ID of the customer to associate the resulting multi-use payment method with.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "verificationMethod", + "description": "Type of US bank account verification to perform.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "UsBankAccountVerificationMethod", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VenmoAccountDetails", + "description": "Details about a Venmo Account.", + "fields": [ + { + "name": "username", + "description": "The Venmo username, as chosen by the user.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "venmoUserId", + "description": "The Venmo user ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VenmoConfiguration", + "description": "Configuration for Pay with Venmo.", + "fields": [ + { + "name": "merchantId", + "description": "The Venmo merchant ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "accessToken", + "description": "Authorization to use when tokenizing a Venmo payment method.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "environment", + "description": "The Venmo environment.", + "args": [], + "type": { + "kind": "ENUM", + "name": "VenmoEnvironment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "VenmoEnvironment", + "description": "The environment being used for Venmo.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "PRODUCTION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SANDBOX", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "production", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sandbox", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VenmoPayerInfo", + "description": "Information about a payer's Venmo account.", + "fields": [ + { + "name": "firstName", + "description": "The payer's first name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastName", + "description": "The payer's last name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "phoneNumber", + "description": "The payer's phone number.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "email", + "description": "The payer's email address.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "EmailAddress", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "externalId", + "description": "The external ID of the payer's Venmo account.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userName", + "description": "The username of the payer's Venmo account.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "billingAddress", + "description": "The payer's billing address.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Address", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "shippingAddress", + "description": "The payer's shipping address.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Address", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VenmoPayerInfoInput", + "description": "Information about a payer's Venmo account.", + "fields": null, + "inputFields": [ + { + "name": "firstName", + "description": "The payer's first name.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "lastName", + "description": "The payer's last name.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "phoneNumber", + "description": "The payer's phone number.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "email", + "description": "The payer's email address.", + "type": { + "kind": "SCALAR", + "name": "EmailAddress", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "externalId", + "description": "The external ID of the payer's Venmo account.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "userName", + "description": "The username of the payer's Venmo account.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "billingAddress", + "description": "The payer's billing address.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AddressInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "shippingAddress", + "description": "The payer's shipping address.", + "type": { + "kind": "INPUT_OBJECT", + "name": "AddressInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Verification", + "description": "A verification reporting whether the payment method has passed your fraud rules and the issuer has ensured it is associated with a valid account.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "legacyId", + "description": "Legacy unique identifier.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethodSnapshot", + "description": "Snapshot of payment method details that were verified. This will always be present.", + "args": [], + "type": { + "kind": "UNION", + "name": "PaymentMethodSnapshot", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethod", + "description": "The multi-use payment method that was verified, if it was vaulted. The details of this PaymentMethod may have changed since it was verified.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentMethod", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "For a credit card, the amount used when performing the verification.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Depending on the type of payment method being verified, some verifications do not have an amount. On a credit card verification, use `paymentMethodVerificationDetails.amount` instead." + }, + { + "name": "merchantAccountId", + "description": "The merchant account used for the verification.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The current status of this verification, indicating whether the verification was successful. Braintree recommends only vaulting payment methods that are successfully verified.", + "args": [], + "type": { + "kind": "ENUM", + "name": "VerificationStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "processorResponse", + "description": "Detailed response information from the processor. Will not be present if the verification was rejected prior to contacting the processor.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "VerificationProcessorResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "networkResponse", + "description": "Fields describing the network response to the verification request.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaymentNetworkResponse", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date and time at which the verification was created.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gatewayRejectionReason", + "description": "The reason the verification was rejected. This will only be set if status is GATEWAY_REJECTED.", + "args": [], + "type": { + "kind": "ENUM", + "name": "GatewayRejectionReason", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "riskData", + "description": "Risk data evaluated for this verification.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "RiskData", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "paymentMethodVerificationDetails", + "description": "Details unique to the verification based on payment method type being verified.", + "args": [], + "type": { + "kind": "UNION", + "name": "VerificationDetails", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VerificationConnection", + "description": "A paginated list of verifications.", + "fields": [ + { + "name": "edges", + "description": "A list of verifications.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "VerificationConnectionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information about the page of verifications contained in `edges`.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VerificationConnectionEdge", + "description": "A verification within a VerificationConnection.", + "fields": [ + { + "name": "cursor", + "description": "The verification's location within the VerificationConnection. Used for requesting additional pages.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The verification.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Verification", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "VerificationDetails", + "description": "A union of all possible verification details specific to the type of payment method being verified.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "UsBankAccountVerificationDetails", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "CreditCardVerificationDetails", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "VerificationProcessorResponse", + "description": "Detailed response information from the processor.", + "fields": [ + { + "name": "legacyCode", + "description": "The [processor response code](https://developers.braintreepayments.com/reference/general/processor-responses/authorization-responses) indicating the result of attempting the verification.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": "The text explanation of the processor response code.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cvvResponse", + "description": "The processing bank's response to the provided CVV.", + "args": [], + "type": { + "kind": "ENUM", + "name": "AvsCvvResponseCode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "avsPostalCodeResponse", + "description": "The processing bank's response to the provided billing postal or zip code.", + "args": [], + "type": { + "kind": "ENUM", + "name": "AvsCvvResponseCode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "avsStreetAddressResponse", + "description": "The processing bank's response to the provided billing street address.", + "args": [], + "type": { + "kind": "ENUM", + "name": "AvsCvvResponseCode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "additionalInformation", + "description": "If present, any additional information recieved from the processor. May provide further insight into the `legacyCode`.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VerificationSearchInput", + "description": "Input fields for searching for verifications.", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "Find verifications with an ID or IDs.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchValueInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "status", + "description": "Find verifications with a given status.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchVerificationStatusInput", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "createdAt", + "description": "Find verifications with a given created at time.", + "type": { + "kind": "INPUT_OBJECT", + "name": "SearchTimestampInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "VerificationStatus", + "description": "The status of the verification, indicating whether the payment method was successfully verified. Braintree recommends only vaulting payment methods with successful verifications.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "FAILED", + "description": "Indicates the verification was unsuccessful because of an issue communicating with the processor.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GATEWAY_REJECTED", + "description": "Indicates that the verification was unsuccessful because the payment method failed one or more fraud checks. In this case, the `gatewayRejectionReason` will indicate which fraud check failed.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PENDING", + "description": "Indicates that the verification is pending.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PROCESSOR_DECLINED", + "description": "Indicates that the verification was unsuccessful based on the response from the processor.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VERIFIED", + "description": "Indicates that the verification was successful.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VERIFYING", + "description": "Indicates that the verification is in the process of verifying.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VerifoneVendor", + "description": "Verifone specific in-store reader information.", + "fields": [ + { + "name": "model", + "description": "Model name or number of reader.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "osVersion", + "description": "Current OS version running on the reader.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "serialNumber", + "description": "Vendor-specific device serial number.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VerifyCreditCardInput", + "description": "Top-level input field for verifying a multi-use credit card.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of an existing multi-use payment method to be vaulted.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "ID of the merchant account to use when verifying the credit card.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "options", + "description": "Input fields for verifying a credit card.", + "type": { + "kind": "INPUT_OBJECT", + "name": "CreditCardVerificationOptionsInput", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VerifyPaymentMethodInput", + "description": "Top-level input field for verifying a multi-use payment method.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of an existing multi-use payment method to be verified.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "ID of the merchant account to use when verifying the payment method.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VerifyPaymentMethodPayload", + "description": "Top-level output field from verifying a payment method.", + "fields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "verification", + "description": "The verification that was run on the payment method.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Verification", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "VerifyUsBankAccountInput", + "description": "Top-level input field for retrying a verification on a bank account.", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "An identifier used to reconcile requests and responses. 255 characters maximum.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "paymentMethodId", + "description": "ID of an existing multi-use payment method to be vaulted.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "merchantAccountId", + "description": "ID of the merchant account to use when verifying the payment method.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "verificationMethod", + "description": "Type of US bank account verification to perform.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "UsBankAccountVerificationMethod", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Viewer", + "description": "Details about the user and merchant authenticated in this request.", + "fields": [ + { + "name": "id", + "description": "Unique identifier.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `user` for id instead." + }, + { + "name": "email", + "description": "Email address.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `user` for email instead." + }, + { + "name": "status", + "description": "Current status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "UserStatus", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `user` for status instead." + }, + { + "name": "name", + "description": "Full name.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `user` for name instead." + }, + { + "name": "roles", + "description": "Associated roles.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Role", + "ofType": null + } + } + }, + "isDeprecated": true, + "deprecationReason": "Use `user` for roles instead." + }, + { + "name": "user", + "description": "Details about the authenticated user.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merchant", + "description": "Details about the authenticated merchant.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Merchant", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rights", + "description": "Associated rights based on authentication.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Right", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VisaCheckoutConfiguration", + "description": "Configuration for Visa Checkout.", + "fields": [ + { + "name": "apiKey", + "description": "The Visa Checkout API key.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "encryptionKey", + "description": "The Visa Checkout encryption key.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "externalClientId", + "description": "The Visa Checkout external client ID.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "supportedCardBrands", + "description": "A list of card brands supported by the merchant for Visa Checkout.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "CreditCardBrandCode", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VisaCheckoutOriginDetails", + "description": "Additional information about the payment method specific to Visa Checkout.", + "fields": [ + { + "name": "callId", + "description": "The Visa assigned identifier for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bin", + "description": "The first 6 digits of the credit card, known as the Bank Identification Number. This BIN may differ from the BIN of the customer's actual card.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VoidedEvent", + "description": "Accompanying information for a transaction that has been voided.", + "fields": [ + { + "name": "status", + "description": "The new status of the transaction.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "Date and time when the transaction was voided.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Timestamp", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "amount", + "description": "The amount of the voided transaction. This should match the authorization amount.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "MonetaryAmount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "The source for the transaction change to the new status.", + "args": [], + "type": { + "kind": "ENUM", + "name": "PaymentSource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "terminal", + "description": "Whether or not this is the final state for the transaction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "PaymentStatusEvent", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Year", + "description": "A four-digit year.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": null, + "fields": [ + { + "name": "name", + "description": "The __Directive type represents a Directive that a server supports.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isRepeatable", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onOperation", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onFragment", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onField", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "An enum describing valid locations where a directive can be placed", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Indicates the directive is valid on queries.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Indicates the directive is valid on mutations.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Indicates the directive is valid on subscriptions.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Indicates the directive is valid on fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Indicates the directive is valid on fragment definitions.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Indicates the directive is valid on fragment spreads.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Indicates the directive is valid on inline fragments.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VARIABLE_DEFINITION", + "description": "Indicates the directive is valid on variable definitions.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Indicates the directive is valid on a schema SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Indicates the directive is valid on a scalar SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates the directive is valid on an object SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Indicates the directive is valid on a field SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Indicates the directive is valid on a field argument SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates the directive is valid on an interface SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates the directive is valid on an union SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates the directive is valid on an enum SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Indicates the directive is valid on an enum value SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates the directive is valid on an input object SDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Indicates the directive is valid on an input object field SDL definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Introspection defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "'A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "'If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": null, + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "specifiedByUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given __Type is", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar. 'specifiedByUrl' is a valid field", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if`'argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "deprecated", + "description": "Marks the field, argument, input field or enum value as deprecated", + "locations": [ + "FIELD_DEFINITION", + "ARGUMENT_DEFINITION", + "ENUM_VALUE", + "INPUT_FIELD_DEFINITION" + ], + "args": [ + { + "name": "reason", + "description": "The reason for the deprecation", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"" + } + ] + }, + { + "name": "specifiedBy", + "description": "Exposes a URL that specifies the behaviour of this scalar.", + "locations": [ + "SCALAR" + ], + "args": [ + { + "name": "url", + "description": "The URL that specifies the behaviour of this scalar.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ] + } + ] + } + }, + "extensions": { + "requestId" : "1773a68c-af86-410b-aea2-e03390380697" + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java-templates/org/whispersystems/textsecuregcm/WhisperServerVersion.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java-templates/org/whispersystems/textsecuregcm/WhisperServerVersion.java new file mode 100644 index 000000000..fa4c40bd9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java-templates/org/whispersystems/textsecuregcm/WhisperServerVersion.java @@ -0,0 +1,15 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm; + +public class WhisperServerVersion { + + private static final String VERSION = "${project.version}"; + + public static String getServerVersion() { + return VERSION; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/signal/i18n/HeaderControlledResourceBundleLookup.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/signal/i18n/HeaderControlledResourceBundleLookup.java new file mode 100644 index 000000000..7e2660938 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/signal/i18n/HeaderControlledResourceBundleLookup.java @@ -0,0 +1,67 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.i18n; + +import com.google.common.annotations.VisibleForTesting; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.ResourceBundle.Control; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; + +public class HeaderControlledResourceBundleLookup { + + private static final int MAX_LOCALES = 15; + + private final ResourceBundleFactory resourceBundleFactory; + + public HeaderControlledResourceBundleLookup() { + this(ResourceBundle::getBundle); + } + + @VisibleForTesting + public HeaderControlledResourceBundleLookup( + @Nonnull final ResourceBundleFactory resourceBundleFactory) { + this.resourceBundleFactory = Objects.requireNonNull(resourceBundleFactory); + } + + @Nonnull + private List getAcceptableLocales(final List acceptableLanguages) { + return acceptableLanguages.stream().limit(MAX_LOCALES).distinct().collect(Collectors.toList()); + } + + @Nonnull + public ResourceBundle getResourceBundle(final String baseName, final List acceptableLocales) { + final List deduplicatedLocales = getAcceptableLocales(acceptableLocales); + final Locale desiredLocale = deduplicatedLocales.isEmpty() ? Locale.getDefault() : deduplicatedLocales.get(0); + // define a control with a fallback order as specified in the header + Control control = new Control() { + @Override + public List getFormats(final String baseName) { + Objects.requireNonNull(baseName); + return Control.FORMAT_PROPERTIES; + } + + @Override + public Locale getFallbackLocale(final String baseName, final Locale locale) { + Objects.requireNonNull(baseName); + if (locale.equals(Locale.getDefault())) { + return null; + } + final int localeIndex = deduplicatedLocales.indexOf(locale); + if (localeIndex < 0 || localeIndex >= deduplicatedLocales.size() - 1) { + return Locale.getDefault(); + } + // [0, deduplicatedLocales.size() - 2] is now the possible range for localeIndex + return deduplicatedLocales.get(localeIndex + 1); + } + }; + + return resourceBundleFactory.createBundle(baseName, desiredLocale, control); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/signal/i18n/ResourceBundleFactory.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/signal/i18n/ResourceBundleFactory.java new file mode 100644 index 000000000..83bc7f283 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/signal/i18n/ResourceBundleFactory.java @@ -0,0 +1,13 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.i18n; + +import java.util.Locale; +import java.util.ResourceBundle; + +public interface ResourceBundleFactory { + ResourceBundle createBundle(String baseName, Locale locale, ResourceBundle.Control control); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java new file mode 100644 index 000000000..58f13a318 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java @@ -0,0 +1,511 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.Configuration; +import java.time.Duration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.attachments.TusConfiguration; +import org.whispersystems.textsecuregcm.configuration.AccountDatabaseCrawlerConfiguration; +import org.whispersystems.textsecuregcm.configuration.AdminEventLoggingConfiguration; +import org.whispersystems.textsecuregcm.configuration.ApnConfiguration; +import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration; +import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration; +import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration; +import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; +import org.whispersystems.textsecuregcm.configuration.BraintreeConfiguration; +import org.whispersystems.textsecuregcm.configuration.CdnConfiguration; +import org.whispersystems.textsecuregcm.configuration.ClientReleaseConfiguration; +import org.whispersystems.textsecuregcm.configuration.CommandStopListenerConfiguration; +import org.whispersystems.textsecuregcm.configuration.DogstatsdConfiguration; +import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration; +import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration; +import org.whispersystems.textsecuregcm.configuration.DynamoDbTables; +import org.whispersystems.textsecuregcm.configuration.FcmConfiguration; +import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration; +import org.whispersystems.textsecuregcm.configuration.GenericZkConfig; +import org.whispersystems.textsecuregcm.configuration.HCaptchaConfiguration; +import org.whispersystems.textsecuregcm.configuration.LinkDeviceSecretConfiguration; +import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration; +import org.whispersystems.textsecuregcm.configuration.MessageByteLimitCardinalityEstimatorConfiguration; +import org.whispersystems.textsecuregcm.configuration.MessageCacheConfiguration; +import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration; +import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration; +import org.whispersystems.textsecuregcm.configuration.RecaptchaConfiguration; +import org.whispersystems.textsecuregcm.configuration.RedisClusterConfiguration; +import org.whispersystems.textsecuregcm.configuration.RedisConfiguration; +import org.whispersystems.textsecuregcm.configuration.RegistrationServiceConfiguration; +import org.whispersystems.textsecuregcm.configuration.RemoteConfigConfiguration; +import org.whispersystems.textsecuregcm.configuration.ReportMessageConfiguration; +import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration; +import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration; +import org.whispersystems.textsecuregcm.configuration.ShortCodeExpanderConfiguration; +import org.whispersystems.textsecuregcm.configuration.SpamFilterConfiguration; +import org.whispersystems.textsecuregcm.configuration.StripeConfiguration; +import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; +import org.whispersystems.textsecuregcm.configuration.TurnSecretConfiguration; +import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration; +import org.whispersystems.textsecuregcm.configuration.ZkConfig; +import org.whispersystems.textsecuregcm.limits.RateLimiterConfig; +import org.whispersystems.websocket.configuration.WebSocketConfiguration; + +/** @noinspection MismatchedQueryAndUpdateOfCollection, WeakerAccess */ +public class WhisperServerConfiguration extends Configuration { + + @NotNull + @Valid + @JsonProperty + private AdminEventLoggingConfiguration adminEventLoggingConfiguration; + + @NotNull + @Valid + @JsonProperty + private StripeConfiguration stripe; + + @NotNull + @Valid + @JsonProperty + private BraintreeConfiguration braintree; + + @NotNull + @Valid + @JsonProperty + private DynamoDbClientConfiguration dynamoDbClientConfiguration; + + @NotNull + @Valid + @JsonProperty + private DynamoDbTables dynamoDbTables; + + @NotNull + @Valid + @JsonProperty + private AwsAttachmentsConfiguration awsAttachments; + + @NotNull + @Valid + @JsonProperty + private GcpAttachmentsConfiguration gcpAttachments; + + @NotNull + @Valid + @JsonProperty + private CdnConfiguration cdn; + + @NotNull + @Valid + @JsonProperty + private DogstatsdConfiguration dogstatsd = new DogstatsdConfiguration(); + + @NotNull + @Valid + @JsonProperty + private RedisClusterConfiguration cacheCluster; + + @NotNull + @Valid + @JsonProperty + private RedisConfiguration pubsub; + + @NotNull + @Valid + @JsonProperty + private RedisClusterConfiguration metricsCluster; + + @NotNull + @Valid + @JsonProperty + private DirectoryV2Configuration directoryV2; + + @NotNull + @Valid + @JsonProperty + private SecureValueRecovery2Configuration svr2; + + @NotNull + @Valid + @JsonProperty + private AccountDatabaseCrawlerConfiguration accountDatabaseCrawler; + + @NotNull + @Valid + @JsonProperty + private RedisClusterConfiguration pushSchedulerCluster; + + @NotNull + @Valid + @JsonProperty + private RedisClusterConfiguration rateLimitersCluster; + + @NotNull + @Valid + @JsonProperty + private MessageCacheConfiguration messageCache; + + @NotNull + @Valid + @JsonProperty + private RedisClusterConfiguration clientPresenceCluster; + + @Valid + @NotNull + @JsonProperty + private Set testDevices = new HashSet<>(); + + @Valid + @NotNull + @JsonProperty + private List maxDevices = new LinkedList<>(); + + @Valid + @NotNull + @JsonProperty + private Map limits = new HashMap<>(); + + @Valid + @NotNull + @JsonProperty + private WebSocketConfiguration webSocket = new WebSocketConfiguration(); + + @Valid + @NotNull + @JsonProperty + private FcmConfiguration fcm; + + @Valid + @NotNull + @JsonProperty + private ApnConfiguration apn; + + @Valid + @NotNull + @JsonProperty + private UnidentifiedDeliveryConfiguration unidentifiedDelivery; + + @Valid + @NotNull + @JsonProperty + private RecaptchaConfiguration recaptcha; + + @Valid + @NotNull + @JsonProperty + private HCaptchaConfiguration hCaptcha; + + @Valid + @NotNull + @JsonProperty + private ShortCodeExpanderConfiguration shortCode; + + @Valid + @NotNull + @JsonProperty + private SecureStorageServiceConfiguration storageService; + + @Valid + @NotNull + @JsonProperty + private PaymentsServiceConfiguration paymentsService; + + @Valid + @NotNull + @JsonProperty + private ArtServiceConfiguration artService; + + @Valid + @NotNull + @JsonProperty + private ZkConfig zkConfig; + + @Valid + @NotNull + @JsonProperty + private GenericZkConfig genericZkConfig; + + @Valid + @NotNull + @JsonProperty + private RemoteConfigConfiguration remoteConfig; + + @Valid + @NotNull + @JsonProperty + private AppConfigConfiguration appConfig; + + @Valid + @NotNull + @JsonProperty + private BadgesConfiguration badges; + + @Valid + @JsonProperty + @NotNull + private SubscriptionConfiguration subscription; + + @Valid + @JsonProperty + @NotNull + private OneTimeDonationConfiguration oneTimeDonations; + + @Valid + @NotNull + @JsonProperty + private ReportMessageConfiguration reportMessage = new ReportMessageConfiguration(); + + @Valid + @JsonProperty + private SpamFilterConfiguration spamFilterConfiguration; + + @Valid + @NotNull + @JsonProperty + private RegistrationServiceConfiguration registrationService; + + @Valid + @NotNull + @JsonProperty + private TurnSecretConfiguration turn; + + @Valid + @NotNull + @JsonProperty + private TusConfiguration tus; + + @Valid + @NotNull + @JsonProperty + private int grpcPort; + + @Valid + @NotNull + @JsonProperty + private ClientReleaseConfiguration clientRelease = new ClientReleaseConfiguration(Duration.ofHours(4)); + + @Valid + @NotNull + @JsonProperty + private MessageByteLimitCardinalityEstimatorConfiguration messageByteLimitCardinalityEstimator = new MessageByteLimitCardinalityEstimatorConfiguration(Duration.ofDays(1)); + + @Valid + @NotNull + @JsonProperty + private CommandStopListenerConfiguration commandStopListener; + + @Valid + @NotNull + @JsonProperty + private LinkDeviceSecretConfiguration linkDevice; + + public AdminEventLoggingConfiguration getAdminEventLoggingConfiguration() { + return adminEventLoggingConfiguration; + } + + public StripeConfiguration getStripe() { + return stripe; + } + + public BraintreeConfiguration getBraintree() { + return braintree; + } + + public DynamoDbClientConfiguration getDynamoDbClientConfiguration() { + return dynamoDbClientConfiguration; + } + + public DynamoDbTables getDynamoDbTables() { + return dynamoDbTables; + } + + public RecaptchaConfiguration getRecaptchaConfiguration() { + return recaptcha; + } + + public HCaptchaConfiguration getHCaptchaConfiguration() { + return hCaptcha; + } + + public ShortCodeExpanderConfiguration getShortCodeRetrieverConfiguration() { + return shortCode; + } + + public WebSocketConfiguration getWebSocketConfiguration() { + return webSocket; + } + + public AwsAttachmentsConfiguration getAwsAttachmentsConfiguration() { + return awsAttachments; + } + + public GcpAttachmentsConfiguration getGcpAttachmentsConfiguration() { + return gcpAttachments; + } + + public RedisClusterConfiguration getCacheClusterConfiguration() { + return cacheCluster; + } + + public RedisConfiguration getPubsubCacheConfiguration() { + return pubsub; + } + + public RedisClusterConfiguration getMetricsClusterConfiguration() { + return metricsCluster; + } + + public SecureValueRecovery2Configuration getSvr2Configuration() { + return svr2; + } + + public DirectoryV2Configuration getDirectoryV2Configuration() { + return directoryV2; + } + + public SecureStorageServiceConfiguration getSecureStorageServiceConfiguration() { + return storageService; + } + + public AccountDatabaseCrawlerConfiguration getAccountDatabaseCrawlerConfiguration() { + return accountDatabaseCrawler; + } + + public MessageCacheConfiguration getMessageCacheConfiguration() { + return messageCache; + } + + public RedisClusterConfiguration getClientPresenceClusterConfiguration() { + return clientPresenceCluster; + } + + public RedisClusterConfiguration getPushSchedulerCluster() { + return pushSchedulerCluster; + } + + public RedisClusterConfiguration getRateLimitersCluster() { + return rateLimitersCluster; + } + + public Map getLimitsConfiguration() { + return limits; + } + + public FcmConfiguration getFcmConfiguration() { + return fcm; + } + + public ApnConfiguration getApnConfiguration() { + return apn; + } + + public CdnConfiguration getCdnConfiguration() { + return cdn; + } + + public DogstatsdConfiguration getDatadogConfiguration() { + return dogstatsd; + } + + public UnidentifiedDeliveryConfiguration getDeliveryCertificate() { + return unidentifiedDelivery; + } + + public Set getTestDevices() { + return testDevices; + } + + public Map getMaxDevices() { + Map results = new HashMap<>(); + + for (MaxDeviceConfiguration maxDeviceConfiguration : maxDevices) { + results.put(maxDeviceConfiguration.getNumber(), + maxDeviceConfiguration.getCount()); + } + + return results; + } + + public PaymentsServiceConfiguration getPaymentsServiceConfiguration() { + return paymentsService; + } + + public ArtServiceConfiguration getArtServiceConfiguration() { + return artService; + } + + public ZkConfig getZkConfig() { + return zkConfig; + } + + public GenericZkConfig getGenericZkConfig() { + return genericZkConfig; + } + + public RemoteConfigConfiguration getRemoteConfigConfiguration() { + return remoteConfig; + } + + public AppConfigConfiguration getAppConfig() { + return appConfig; + } + + public BadgesConfiguration getBadges() { + return badges; + } + + public SubscriptionConfiguration getSubscription() { + return subscription; + } + + public OneTimeDonationConfiguration getOneTimeDonations() { + return oneTimeDonations; + } + + public ReportMessageConfiguration getReportMessageConfiguration() { + return reportMessage; + } + + public SpamFilterConfiguration getSpamFilterConfiguration() { + return spamFilterConfiguration; + } + + public RegistrationServiceConfiguration getRegistrationServiceConfiguration() { + return registrationService; + } + + public TurnSecretConfiguration getTurnSecretConfiguration() { + return turn; + } + + public TusConfiguration getTus() { + return tus; + } + + public int getGrpcPort() { + return grpcPort; + } + + public ClientReleaseConfiguration getClientReleaseConfiguration() { + return clientRelease; + } + + public MessageByteLimitCardinalityEstimatorConfiguration getMessageByteLimitCardinalityEstimator() { + return messageByteLimitCardinalityEstimator; + } + + public CommandStopListenerConfiguration getCommandStopListener() { + return commandStopListener; + } + + public LinkDeviceSecretConfiguration getLinkDeviceSecretConfiguration() { + return linkDevice; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java new file mode 100644 index 000000000..5806617c7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -0,0 +1,907 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm; + +import static com.codahale.metrics.MetricRegistry.name; +import static java.util.Objects.requireNonNull; + +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; +import com.google.api.client.http.apache.v2.ApacheHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.logging.LoggingOptions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import io.dropwizard.Application; +import io.dropwizard.auth.AuthFilter; +import io.dropwizard.auth.PolymorphicAuthDynamicFeature; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.auth.basic.BasicCredentialAuthFilter; +import io.dropwizard.auth.basic.BasicCredentials; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import io.grpc.ServerBuilder; +import io.grpc.ServerInterceptors; +import io.lettuce.core.metrics.MicrometerCommandLatencyRecorder; +import io.lettuce.core.metrics.MicrometerOptions; +import io.lettuce.core.resource.ClientResources; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.binder.grpc.MetricCollectingServerInterceptor; +import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics; +import java.io.ByteArrayInputStream; +import java.net.http.HttpClient; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import javax.servlet.DispatcherType; +import javax.servlet.FilterRegistration; +import javax.servlet.ServletRegistration; +import org.eclipse.jetty.servlets.CrossOriginFilter; +import org.glassfish.jersey.server.ServerProperties; +import org.signal.event.AdminEventLogger; +import org.signal.event.GoogleCloudAdminEventLogger; +import org.signal.i18n.HeaderControlledResourceBundleLookup; +import org.signal.libsignal.zkgroup.GenericServerSecretParams; +import org.signal.libsignal.zkgroup.ServerSecretParams; +import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations; +import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; +import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation; +import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.attachments.GcsAttachmentGenerator; +import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator; +import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator; +import org.whispersystems.textsecuregcm.auth.CertificateGenerator; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccountAuthenticator; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager; +import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager; +import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; +import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener; +import org.whispersystems.textsecuregcm.auth.grpc.BasicCredentialAuthenticationInterceptor; +import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter; +import org.whispersystems.textsecuregcm.badges.ResourceBundleLevelTranslator; +import org.whispersystems.textsecuregcm.captcha.CaptchaChecker; +import org.whispersystems.textsecuregcm.captcha.HCaptchaClient; +import org.whispersystems.textsecuregcm.captcha.RecaptchaClient; +import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager; +import org.whispersystems.textsecuregcm.captcha.ShortCodeExpander; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretStore; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretsModule; +import org.whispersystems.textsecuregcm.controllers.AccountController; +import org.whispersystems.textsecuregcm.controllers.AccountControllerV2; +import org.whispersystems.textsecuregcm.controllers.ArtController; +import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2; +import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV3; +import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV4; +import org.whispersystems.textsecuregcm.controllers.CallLinkController; +import org.whispersystems.textsecuregcm.controllers.CertificateController; +import org.whispersystems.textsecuregcm.controllers.ChallengeController; +import org.whispersystems.textsecuregcm.controllers.DeviceController; +import org.whispersystems.textsecuregcm.controllers.DirectoryV2Controller; +import org.whispersystems.textsecuregcm.controllers.DonationController; +import org.whispersystems.textsecuregcm.controllers.KeepAliveController; +import org.whispersystems.textsecuregcm.controllers.KeysController; +import org.whispersystems.textsecuregcm.controllers.MessageController; +import org.whispersystems.textsecuregcm.controllers.PaymentsController; +import org.whispersystems.textsecuregcm.controllers.ProfileController; +import org.whispersystems.textsecuregcm.controllers.ProvisioningController; +import org.whispersystems.textsecuregcm.controllers.RegistrationController; +import org.whispersystems.textsecuregcm.controllers.RemoteConfigController; +import org.whispersystems.textsecuregcm.controllers.SecureStorageController; +import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller; +import org.whispersystems.textsecuregcm.controllers.StickerController; +import org.whispersystems.textsecuregcm.controllers.SubscriptionController; +import org.whispersystems.textsecuregcm.controllers.VerificationController; +import org.whispersystems.textsecuregcm.currency.CoinMarketCapClient; +import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager; +import org.whispersystems.textsecuregcm.currency.FixerClient; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; +import org.whispersystems.textsecuregcm.filters.RemoteDeprecationFilter; +import org.whispersystems.textsecuregcm.filters.RequestStatisticsFilter; +import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter; +import org.whispersystems.textsecuregcm.grpc.AcceptLanguageInterceptor; +import org.whispersystems.textsecuregcm.grpc.ErrorMappingInterceptor; +import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsAnonymousGrpcService; +import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsGrpcService; +import org.whispersystems.textsecuregcm.grpc.GrpcServerManagedWrapper; +import org.whispersystems.textsecuregcm.grpc.KeysAnonymousGrpcService; +import org.whispersystems.textsecuregcm.grpc.KeysGrpcService; +import org.whispersystems.textsecuregcm.grpc.PaymentsGrpcService; +import org.whispersystems.textsecuregcm.grpc.ProfileAnonymousGrpcService; +import org.whispersystems.textsecuregcm.grpc.ProfileGrpcService; +import org.whispersystems.textsecuregcm.grpc.UserAgentInterceptor; +import org.whispersystems.textsecuregcm.limits.CardinalityEstimator; +import org.whispersystems.textsecuregcm.limits.PushChallengeManager; +import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.JsonMappingExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.SubscriptionProcessorExceptionMapper; +import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.metrics.ReportedMessageMetricsListener; +import org.whispersystems.textsecuregcm.metrics.TrafficSource; +import org.whispersystems.textsecuregcm.providers.MultiRecipientMessageProvider; +import org.whispersystems.textsecuregcm.providers.RedisClusterHealthCheck; +import org.whispersystems.textsecuregcm.push.APNSender; +import org.whispersystems.textsecuregcm.push.ApnPushNotificationScheduler; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.push.FcmSender; +import org.whispersystems.textsecuregcm.push.MessageSender; +import org.whispersystems.textsecuregcm.push.ProvisioningManager; +import org.whispersystems.textsecuregcm.push.PushLatencyManager; +import org.whispersystems.textsecuregcm.push.PushNotificationManager; +import org.whispersystems.textsecuregcm.push.ReceiptSender; +import org.whispersystems.textsecuregcm.redis.ConnectionEventLogger; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; +import org.whispersystems.textsecuregcm.s3.PolicySigner; +import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator; +import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; +import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client; +import org.whispersystems.textsecuregcm.spam.FilterSpam; +import org.whispersystems.textsecuregcm.spam.RateLimitChallengeListener; +import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider; +import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider; +import org.whispersystems.textsecuregcm.spam.SpamFilter; +import org.whispersystems.textsecuregcm.storage.AccountLockManager; +import org.whispersystems.textsecuregcm.storage.Accounts; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.ChangeNumberManager; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; +import org.whispersystems.textsecuregcm.storage.ClientReleases; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import org.whispersystems.textsecuregcm.storage.MessagesCache; +import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb; +import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers; +import org.whispersystems.textsecuregcm.storage.Profiles; +import org.whispersystems.textsecuregcm.storage.ProfilesManager; +import org.whispersystems.textsecuregcm.storage.PushChallengeDynamoDb; +import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; +import org.whispersystems.textsecuregcm.storage.RemoteConfigs; +import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager; +import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb; +import org.whispersystems.textsecuregcm.storage.ReportMessageManager; +import org.whispersystems.textsecuregcm.storage.SubscriptionManager; +import org.whispersystems.textsecuregcm.storage.VerificationSessionManager; +import org.whispersystems.textsecuregcm.storage.VerificationSessions; +import org.whispersystems.textsecuregcm.subscriptions.BankMandateTranslator; +import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager; +import org.whispersystems.textsecuregcm.subscriptions.StripeManager; +import org.whispersystems.textsecuregcm.util.DynamoDbFromConfig; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.UsernameHashZkProofVerifier; +import org.whispersystems.textsecuregcm.util.logging.LoggingUnhandledExceptionMapper; +import org.whispersystems.textsecuregcm.util.logging.UncaughtExceptionHandler; +import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener; +import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener; +import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator; +import org.whispersystems.textsecuregcm.workers.AssignUsernameCommand; +import org.whispersystems.textsecuregcm.workers.CertificateCommand; +import org.whispersystems.textsecuregcm.workers.CheckDynamicConfigurationCommand; +import org.whispersystems.textsecuregcm.workers.CrawlAccountsCommand; +import org.whispersystems.textsecuregcm.workers.DeleteUserCommand; +import org.whispersystems.textsecuregcm.workers.MessagePersisterServiceCommand; +import org.whispersystems.textsecuregcm.workers.MigrateSignedECPreKeysCommand; +import org.whispersystems.textsecuregcm.workers.ScheduledApnPushNotificationSenderServiceCommand; +import org.whispersystems.textsecuregcm.workers.ServerVersionCommand; +import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask; +import org.whispersystems.textsecuregcm.workers.SetUserDiscoverabilityCommand; +import org.whispersystems.textsecuregcm.workers.UnlinkDeviceCommand; +import org.whispersystems.textsecuregcm.workers.ZkParamsCommand; +import org.whispersystems.websocket.WebSocketResourceProviderFactory; +import org.whispersystems.websocket.setup.WebSocketEnvironment; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain; +import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3Client; + +import org.eclipse.jetty.server.AbstractNetworkConnector; +import org.eclipse.jetty.server.Server; + +public class WhisperServerService extends Application { + + private static final Logger log = LoggerFactory.getLogger(WhisperServerService.class); + + public static final String SECRETS_BUNDLE_FILE_NAME_PROPERTY = "secrets.bundle.filename"; + + private Server jettyServer; + + public int getJettyPort() { + return ((AbstractNetworkConnector)jettyServer.getConnectors()[0]).getLocalPort(); + } + + public Server getJettyServer() { + return jettyServer; + } + + public static final software.amazon.awssdk.auth.credentials.AwsCredentialsProvider AWSSDK_CREDENTIALS_PROVIDER = + AwsCredentialsProviderChain.of( + InstanceProfileCredentialsProvider.create(), + WebIdentityTokenFileCredentialsProvider.create()); + + @Override + public void initialize(final Bootstrap bootstrap) { + // `SecretStore` needs to be initialized before Dropwizard reads the main application config file. + final String secretsBundleFileName = requireNonNull( + System.getProperty(SECRETS_BUNDLE_FILE_NAME_PROPERTY), + "Application requires property [%s] to be provided".formatted(SECRETS_BUNDLE_FILE_NAME_PROPERTY)); + final SecretStore secretStore = SecretStore.fromYamlFileSecretsBundle(secretsBundleFileName); + SecretsModule.INSTANCE.setSecretStore(secretStore); + + // Initializing SystemMapper here because parsing of the main application config happens before `run()` method is called. + SystemMapper.configureMapper(bootstrap.getObjectMapper()); + + bootstrap.addCommand(new DeleteUserCommand()); + bootstrap.addCommand(new CertificateCommand()); + bootstrap.addCommand(new ZkParamsCommand()); + bootstrap.addCommand(new ServerVersionCommand()); + bootstrap.addCommand(new CheckDynamicConfigurationCommand()); + bootstrap.addCommand(new SetUserDiscoverabilityCommand()); + bootstrap.addCommand(new AssignUsernameCommand()); + bootstrap.addCommand(new UnlinkDeviceCommand()); + bootstrap.addCommand(new CrawlAccountsCommand()); + bootstrap.addCommand(new ScheduledApnPushNotificationSenderServiceCommand()); + bootstrap.addCommand(new MessagePersisterServiceCommand()); + bootstrap.addCommand(new MigrateSignedECPreKeysCommand()); + } + + @Override + public String getName() { + return "whisper-server"; + } + + @Override + public void run(WhisperServerConfiguration config, Environment environment) throws Exception { + final Clock clock = Clock.systemUTC(); + final int availableProcessors = Runtime.getRuntime().availableProcessors(); + + UncaughtExceptionHandler.register(); + + MetricsUtil.configureRegistries(config, environment); + + HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup = + new HeaderControlledResourceBundleLookup(); + ConfiguredProfileBadgeConverter profileBadgeConverter = new ConfiguredProfileBadgeConverter( + clock, config.getBadges(), headerControlledResourceBundleLookup); + ResourceBundleLevelTranslator resourceBundleLevelTranslator = new ResourceBundleLevelTranslator( + headerControlledResourceBundleLookup); + BankMandateTranslator bankMandateTranslator = new BankMandateTranslator(headerControlledResourceBundleLookup); + + DynamoDbAsyncClient dynamoDbAsyncClient = DynamoDbFromConfig.asyncClient(config.getDynamoDbClientConfiguration(), + AWSSDK_CREDENTIALS_PROVIDER); + + DynamoDbClient dynamoDbClient = DynamoDbFromConfig.client(config.getDynamoDbClientConfiguration(), + AWSSDK_CREDENTIALS_PROVIDER); + + DynamicConfigurationManager dynamicConfigurationManager = + new DynamicConfigurationManager<>(config.getAppConfig().getApplication(), + config.getAppConfig().getEnvironment(), + config.getAppConfig().getConfigurationName(), + DynamicConfiguration.class); + + BlockingQueue messageDeletionQueue = new LinkedBlockingQueue<>(); + Metrics.gaugeCollectionSize(name(getClass(), "messageDeletionQueueSize"), Collections.emptyList(), + messageDeletionQueue); + ExecutorService messageDeletionAsyncExecutor = environment.lifecycle() + .executorService(name(getClass(), "messageDeletionAsyncExecutor-%d")) + .minThreads(2) + .maxThreads(2) + .allowCoreThreadTimeOut(true) + .workQueue(messageDeletionQueue).build(); + + Accounts accounts = new Accounts( + dynamoDbClient, + dynamoDbAsyncClient, + config.getDynamoDbTables().getAccounts().getTableName(), + config.getDynamoDbTables().getAccounts().getPhoneNumberTableName(), + config.getDynamoDbTables().getAccounts().getPhoneNumberIdentifierTableName(), + config.getDynamoDbTables().getAccounts().getUsernamesTableName(), + config.getDynamoDbTables().getDeletedAccounts().getTableName(), + config.getDynamoDbTables().getAccounts().getScanPageSize()); + ClientReleases clientReleases = new ClientReleases(dynamoDbAsyncClient, + config.getDynamoDbTables().getClientReleases().getTableName()); + PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(dynamoDbClient, + config.getDynamoDbTables().getPhoneNumberIdentifiers().getTableName()); + Profiles profiles = new Profiles(dynamoDbClient, dynamoDbAsyncClient, + config.getDynamoDbTables().getProfiles().getTableName()); + KeysManager keys = new KeysManager( + dynamoDbAsyncClient, + config.getDynamoDbTables().getEcKeys().getTableName(), + config.getDynamoDbTables().getKemKeys().getTableName(), + config.getDynamoDbTables().getEcSignedPreKeys().getTableName(), + config.getDynamoDbTables().getKemLastResortKeys().getTableName(), + dynamicConfigurationManager); + MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(dynamoDbClient, dynamoDbAsyncClient, + config.getDynamoDbTables().getMessages().getTableName(), + config.getDynamoDbTables().getMessages().getExpiration(), + messageDeletionAsyncExecutor); + RemoteConfigs remoteConfigs = new RemoteConfigs(dynamoDbClient, + config.getDynamoDbTables().getRemoteConfig().getTableName()); + PushChallengeDynamoDb pushChallengeDynamoDb = new PushChallengeDynamoDb(dynamoDbClient, + config.getDynamoDbTables().getPushChallenge().getTableName()); + ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(dynamoDbClient, + config.getDynamoDbTables().getReportMessage().getTableName(), + config.getReportMessageConfiguration().getReportTtl()); + RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords( + config.getDynamoDbTables().getRegistrationRecovery().getTableName(), + config.getDynamoDbTables().getRegistrationRecovery().getExpiration(), + dynamoDbClient, + dynamoDbAsyncClient + ); + + final VerificationSessions verificationSessions = new VerificationSessions(dynamoDbAsyncClient, + config.getDynamoDbTables().getVerificationSessions().getTableName(), clock); + + final ClientResources redisClientResources = ClientResources.builder() + .commandLatencyRecorder(new MicrometerCommandLatencyRecorder(Metrics.globalRegistry, MicrometerOptions.builder().build())) + .build(); + + ConnectionEventLogger.logConnectionEvents(redisClientResources); + + FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", config.getCacheClusterConfiguration(), redisClientResources); + FaultTolerantRedisCluster messagesCluster = new FaultTolerantRedisCluster("messages_cluster", config.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClientResources); + FaultTolerantRedisCluster clientPresenceCluster = new FaultTolerantRedisCluster("client_presence_cluster", config.getClientPresenceClusterConfiguration(), redisClientResources); + FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", config.getMetricsClusterConfiguration(), redisClientResources); + FaultTolerantRedisCluster pushSchedulerCluster = new FaultTolerantRedisCluster("push_scheduler", config.getPushSchedulerCluster(), redisClientResources); + FaultTolerantRedisCluster rateLimitersCluster = new FaultTolerantRedisCluster("rate_limiters", config.getRateLimitersCluster(), redisClientResources); + + final BlockingQueue keyspaceNotificationDispatchQueue = new ArrayBlockingQueue<>(100_000); + Metrics.gaugeCollectionSize(name(getClass(), "keyspaceNotificationDispatchQueueSize"), Collections.emptyList(), + keyspaceNotificationDispatchQueue); + final BlockingQueue receiptSenderQueue = new LinkedBlockingQueue<>(); + Metrics.gaugeCollectionSize(name(getClass(), "receiptSenderQueue"), Collections.emptyList(), receiptSenderQueue); + final BlockingQueue fcmSenderQueue = new LinkedBlockingQueue<>(); + Metrics.gaugeCollectionSize(name(getClass(), "fcmSenderQueue"), Collections.emptyList(), fcmSenderQueue); + final BlockingQueue messageDeliveryQueue = new LinkedBlockingQueue<>(); + Metrics.gaugeCollectionSize(MetricsUtil.name(getClass(), "messageDeliveryQueue"), Collections.emptyList(), + messageDeliveryQueue); + + ScheduledExecutorService recurringJobExecutor = environment.lifecycle() + .scheduledExecutorService(name(getClass(), "recurringJob-%d")).threads(6).build(); + ScheduledExecutorService websocketScheduledExecutor = environment.lifecycle() + .scheduledExecutorService(name(getClass(), "websocket-%d")).threads(8).build(); + ExecutorService keyspaceNotificationDispatchExecutor = ExecutorServiceMetrics.monitor(Metrics.globalRegistry, + environment.lifecycle() + .executorService(name(getClass(), "keyspaceNotification-%d")) + .maxThreads(16) + .workQueue(keyspaceNotificationDispatchQueue) + .build(), + MetricsUtil.name(getClass(), "keyspaceNotificationExecutor"), + MetricsUtil.PREFIX); + ExecutorService apnSenderExecutor = environment.lifecycle().executorService(name(getClass(), "apnSender-%d")) + .maxThreads(1).minThreads(1).build(); + ExecutorService fcmSenderExecutor = environment.lifecycle().executorService(name(getClass(), "fcmSender-%d")) + .maxThreads(32).minThreads(32).workQueue(fcmSenderQueue).build(); + ExecutorService secureValueRecoveryServiceExecutor = environment.lifecycle() + .executorService(name(getClass(), "secureValueRecoveryService-%d")).maxThreads(1).minThreads(1).build(); + ExecutorService storageServiceExecutor = environment.lifecycle() + .executorService(name(getClass(), "storageService-%d")).maxThreads(1).minThreads(1).build(); + ScheduledExecutorService secureValueRecoveryServiceRetryExecutor = environment.lifecycle() + .scheduledExecutorService(name(getClass(), "secureValueRecoveryServiceRetry-%d")).threads(1).build(); + ScheduledExecutorService storageServiceRetryExecutor = environment.lifecycle() + .scheduledExecutorService(name(getClass(), "storageServiceRetry-%d")).threads(1).build(); + ScheduledExecutorService hcaptchaRetryExecutor = environment.lifecycle() + .scheduledExecutorService(name(getClass(), "hCaptchaRetry-%d")).threads(1).build(); + + Scheduler messageDeliveryScheduler = Schedulers.fromExecutorService( + ExecutorServiceMetrics.monitor(Metrics.globalRegistry, + environment.lifecycle().executorService(name(getClass(), "messageDelivery-%d")) + .minThreads(20) + .maxThreads(20) + .workQueue(messageDeliveryQueue) + .build(), + MetricsUtil.name(getClass(), "messageDeliveryExecutor"), MetricsUtil.PREFIX), + "messageDelivery"); + + // TODO: generally speaking this is a DynamoDB I/O executor for the accounts table; we should eventually have a general executor for speaking to the accounts table, but most of the server is still synchronous so this isn't widely useful yet + ExecutorService batchIdentityCheckExecutor = environment.lifecycle().executorService(name(getClass(), "batchIdentityCheck-%d")).minThreads(32).maxThreads(32).build(); + ExecutorService multiRecipientMessageExecutor = environment.lifecycle() + .executorService(name(getClass(), "multiRecipientMessage-%d")).minThreads(64).maxThreads(64).build(); + ExecutorService subscriptionProcessorExecutor = environment.lifecycle() + .executorService(name(getClass(), "subscriptionProcessor-%d")) + .maxThreads(availableProcessors) // mostly this is IO bound so tying to number of processors is tenuous at best + .minThreads(availableProcessors) // mostly this is IO bound so tying to number of processors is tenuous at best + .allowCoreThreadTimeOut(true). + build(); + ExecutorService receiptSenderExecutor = environment.lifecycle() + .executorService(name(getClass(), "receiptSender-%d")) + .maxThreads(2) + .minThreads(2) + .workQueue(receiptSenderQueue) + .rejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()) + .build(); + ExecutorService registrationCallbackExecutor = environment.lifecycle() + .executorService(name(getClass(), "registration-%d")) + .maxThreads(2) + .minThreads(2) + .build(); + ExecutorService accountLockExecutor = environment.lifecycle() + .executorService(name(getClass(), "accountLock-%d")) + .minThreads(8) + .maxThreads(8) + .build(); + ScheduledExecutorService subscriptionProcessorRetryExecutor = environment.lifecycle() + .scheduledExecutorService(name(getClass(), "subscriptionProcessorRetry-%d")).threads(1).build(); + + final AdminEventLogger adminEventLogger = new GoogleCloudAdminEventLogger( + LoggingOptions.newBuilder().setProjectId(config.getAdminEventLoggingConfiguration().projectId()) + .setCredentials(GoogleCredentials.fromStream(new ByteArrayInputStream( + config.getAdminEventLoggingConfiguration().credentials().getBytes(StandardCharsets.UTF_8)))) + .build().getService(), + config.getAdminEventLoggingConfiguration().projectId(), + config.getAdminEventLoggingConfiguration().logName()); + + StripeManager stripeManager = new StripeManager(config.getStripe().apiKey().value(), subscriptionProcessorExecutor, + config.getStripe().idempotencyKeyGenerator().value(), config.getStripe().boostDescription(), config.getStripe().supportedCurrenciesByPaymentMethod()); + BraintreeManager braintreeManager = new BraintreeManager(config.getBraintree().merchantId(), + config.getBraintree().publicKey(), config.getBraintree().privateKey().value(), + config.getBraintree().environment(), + config.getBraintree().supportedCurrenciesByPaymentMethod(), config.getBraintree().merchantAccounts(), + config.getBraintree().graphqlUrl(), config.getBraintree().circuitBreaker(), subscriptionProcessorExecutor, + subscriptionProcessorRetryExecutor); + + ExternalServiceCredentialsGenerator directoryV2CredentialsGenerator = DirectoryV2Controller.credentialsGenerator( + config.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration()); + ExternalServiceCredentialsGenerator storageCredentialsGenerator = SecureStorageController.credentialsGenerator( + config.getSecureStorageServiceConfiguration()); + ExternalServiceCredentialsGenerator paymentsCredentialsGenerator = PaymentsController.credentialsGenerator( + config.getPaymentsServiceConfiguration()); + ExternalServiceCredentialsGenerator artCredentialsGenerator = ArtController.credentialsGenerator( + config.getArtServiceConfiguration()); + ExternalServiceCredentialsGenerator svr2CredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator( + config.getSvr2Configuration()); + + dynamicConfigurationManager.start(); + + ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager( + dynamicConfigurationManager); + RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = new RegistrationRecoveryPasswordsManager( + registrationRecoveryPasswords); + UsernameHashZkProofVerifier usernameHashZkProofVerifier = new UsernameHashZkProofVerifier(); + + RegistrationServiceClient registrationServiceClient = new RegistrationServiceClient( + config.getRegistrationServiceConfiguration().host(), + config.getRegistrationServiceConfiguration().port(), + config.getRegistrationServiceConfiguration().credentialConfigurationJson(), + config.getRegistrationServiceConfiguration().identityTokenAudience(), + config.getRegistrationServiceConfiguration().registrationCaCertificate(), + registrationCallbackExecutor); + SecureValueRecovery2Client secureValueRecovery2Client = new SecureValueRecovery2Client(svr2CredentialsGenerator, + secureValueRecoveryServiceExecutor, secureValueRecoveryServiceRetryExecutor, config.getSvr2Configuration()); + SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator, + storageServiceExecutor, storageServiceRetryExecutor, config.getSecureStorageServiceConfiguration()); + ClientPresenceManager clientPresenceManager = new ClientPresenceManager(clientPresenceCluster, recurringJobExecutor, + keyspaceNotificationDispatchExecutor); + ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster); + MessagesCache messagesCache = new MessagesCache(messagesCluster, messagesCluster, + keyspaceNotificationDispatchExecutor, messageDeliveryScheduler, messageDeletionAsyncExecutor, clock); + ClientReleaseManager clientReleaseManager = new ClientReleaseManager(clientReleases, + recurringJobExecutor, + config.getClientReleaseConfiguration().refreshInterval(), + Clock.systemUTC()); + PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, clientReleaseManager); + ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, rateLimitersCluster, + config.getReportMessageConfiguration().getCounterTtl()); + MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager, + messageDeletionAsyncExecutor); + AccountLockManager accountLockManager = new AccountLockManager(dynamoDbClient, + config.getDynamoDbTables().getDeletedAccountsLock().getTableName()); + AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster, + accountLockManager, keys, messagesManager, profilesManager, + secureStorageClient, secureValueRecovery2Client, + clientPresenceManager, + experimentEnrollmentManager, registrationRecoveryPasswordsManager, accountLockExecutor, clock); + RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs); + APNSender apnSender = new APNSender(apnSenderExecutor, config.getApnConfiguration()); + FcmSender fcmSender = new FcmSender(fcmSenderExecutor, config.getFcmConfiguration().credentials().value()); + ApnPushNotificationScheduler apnPushNotificationScheduler = new ApnPushNotificationScheduler(pushSchedulerCluster, + apnSender, accountsManager, 0); + PushNotificationManager pushNotificationManager = new PushNotificationManager(accountsManager, apnSender, fcmSender, + apnPushNotificationScheduler, pushLatencyManager); + RateLimiters rateLimiters = RateLimiters.createAndValidate(config.getLimitsConfiguration(), + dynamicConfigurationManager, rateLimitersCluster); + ProvisioningManager provisioningManager = new ProvisioningManager(config.getPubsubCacheConfiguration().getUri(), + redisClientResources, config.getPubsubCacheConfiguration().getTimeout(), + config.getPubsubCacheConfiguration().getCircuitBreakerConfiguration()); + IssuedReceiptsManager issuedReceiptsManager = new IssuedReceiptsManager( + config.getDynamoDbTables().getIssuedReceipts().getTableName(), + config.getDynamoDbTables().getIssuedReceipts().getExpiration(), + dynamoDbAsyncClient, + config.getDynamoDbTables().getIssuedReceipts().getGenerator()); + RedeemedReceiptsManager redeemedReceiptsManager = new RedeemedReceiptsManager(clock, + config.getDynamoDbTables().getRedeemedReceipts().getTableName(), + dynamoDbAsyncClient, + config.getDynamoDbTables().getRedeemedReceipts().getExpiration()); + SubscriptionManager subscriptionManager = new SubscriptionManager( + config.getDynamoDbTables().getSubscriptions().getTableName(), dynamoDbAsyncClient); + + final RegistrationLockVerificationManager registrationLockVerificationManager = new RegistrationLockVerificationManager( + accountsManager, clientPresenceManager, svr2CredentialsGenerator, registrationRecoveryPasswordsManager, pushNotificationManager, rateLimiters); + final PhoneVerificationTokenManager phoneVerificationTokenManager = new PhoneVerificationTokenManager( + registrationServiceClient, registrationRecoveryPasswordsManager); + + final ReportedMessageMetricsListener reportedMessageMetricsListener = new ReportedMessageMetricsListener( + accountsManager); + reportMessageManager.addListener(reportedMessageMetricsListener); + + final AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager); + final DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator( + accountsManager); + + final MessageSender messageSender = new MessageSender(clientPresenceManager, messagesManager, + pushNotificationManager, + pushLatencyManager); + final ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor); + final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager, + config.getTurnSecretConfiguration().secret().value()); + + final CardinalityEstimator messageByteLimitCardinalityEstimator = new CardinalityEstimator( + rateLimitersCluster, + "message_byte_limit", + config.getMessageByteLimitCardinalityEstimator().period()); + + RecaptchaClient recaptchaClient = new RecaptchaClient( + config.getRecaptchaConfiguration().projectPath(), + config.getRecaptchaConfiguration().credentialConfigurationJson(), + dynamicConfigurationManager); + HCaptchaClient hCaptchaClient = new HCaptchaClient( + config.getHCaptchaConfiguration().getApiKey().value(), + hcaptchaRetryExecutor, + config.getHCaptchaConfiguration().getCircuitBreaker(), + config.getHCaptchaConfiguration().getRetry(), + dynamicConfigurationManager); + HttpClient shortCodeRetrieverHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2) + .connectTimeout(Duration.ofSeconds(10)).build(); + ShortCodeExpander shortCodeRetriever = new ShortCodeExpander(shortCodeRetrieverHttpClient, config.getShortCodeRetrieverConfiguration().baseUrl()); + CaptchaChecker captchaChecker = new CaptchaChecker(shortCodeRetriever, List.of(recaptchaClient, hCaptchaClient)); + + PushChallengeManager pushChallengeManager = new PushChallengeManager(pushNotificationManager, + pushChallengeDynamoDb); + RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager, + captchaChecker, rateLimiters); + + ChangeNumberManager changeNumberManager = new ChangeNumberManager(messageSender, accountsManager); + + HttpClient currencyClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).connectTimeout(Duration.ofSeconds(10)).build(); + FixerClient fixerClient = new FixerClient(currencyClient, config.getPaymentsServiceConfiguration().fixerApiKey().value()); + CoinMarketCapClient coinMarketCapClient = new CoinMarketCapClient(currencyClient, config.getPaymentsServiceConfiguration().coinMarketCapApiKey().value(), config.getPaymentsServiceConfiguration().coinMarketCapCurrencyIds()); + CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, + cacheCluster, config.getPaymentsServiceConfiguration().paymentCurrencies(), recurringJobExecutor, Clock.systemUTC()); + + environment.lifecycle().manage(apnSender); + environment.lifecycle().manage(apnPushNotificationScheduler); + environment.lifecycle().manage(provisioningManager); + environment.lifecycle().manage(messagesCache); + environment.lifecycle().manage(clientPresenceManager); + environment.lifecycle().manage(currencyManager); + environment.lifecycle().manage(registrationServiceClient); + environment.lifecycle().manage(clientReleaseManager); + + final RegistrationCaptchaManager registrationCaptchaManager = new RegistrationCaptchaManager(captchaChecker, + rateLimiters, config.getTestDevices(), dynamicConfigurationManager); + + StaticCredentialsProvider cdnCredentialsProvider = StaticCredentialsProvider + .create(AwsBasicCredentials.create( + config.getCdnConfiguration().accessKey().value(), + config.getCdnConfiguration().accessSecret().value())); + S3Client cdnS3Client = S3Client.builder() + .credentialsProvider(cdnCredentialsProvider) + .region(Region.of(config.getCdnConfiguration().region())) + .build(); + S3AsyncClient asyncCdnS3Client = S3AsyncClient.builder() + .credentialsProvider(cdnCredentialsProvider) + .region(Region.of(config.getCdnConfiguration().region())) + .build(); + + final GcsAttachmentGenerator gcsAttachmentGenerator = new GcsAttachmentGenerator( + config.getGcpAttachmentsConfiguration().domain(), + config.getGcpAttachmentsConfiguration().email(), + config.getGcpAttachmentsConfiguration().maxSizeInBytes(), + config.getGcpAttachmentsConfiguration().pathPrefix(), + config.getGcpAttachmentsConfiguration().rsaSigningKey().value()); + + PostPolicyGenerator profileCdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().region(), + config.getCdnConfiguration().bucket(), config.getCdnConfiguration().accessKey().value()); + PolicySigner profileCdnPolicySigner = new PolicySigner(config.getCdnConfiguration().accessSecret().value(), + config.getCdnConfiguration().region()); + + ServerSecretParams zkSecretParams = new ServerSecretParams(config.getZkConfig().serverSecret().value()); + GenericServerSecretParams genericZkSecretParams = new GenericServerSecretParams(config.getGenericZkConfig().serverSecret().value()); + ServerZkProfileOperations zkProfileOperations = new ServerZkProfileOperations(zkSecretParams); + ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams); + ServerZkReceiptOperations zkReceiptOperations = new ServerZkReceiptOperations(zkSecretParams); + + AuthFilter accountAuthFilter = new BasicCredentialAuthFilter.Builder().setAuthenticator( + accountAuthenticator).buildAuthFilter(); + AuthFilter disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder().setAuthenticator( + disabledPermittedAccountAuthenticator).buildAuthFilter(); + + final BasicCredentialAuthenticationInterceptor basicCredentialAuthenticationInterceptor = + new BasicCredentialAuthenticationInterceptor(new BaseAccountAuthenticator(accountsManager)); + + final ServerBuilder grpcServer = ServerBuilder.forPort(config.getGrpcPort()) + .addService(ExternalServiceCredentialsGrpcService.createForAllExternalServices(config, rateLimiters)) + .addService(ExternalServiceCredentialsAnonymousGrpcService.create(accountsManager, config)) + .addService(ServerInterceptors.intercept(new KeysGrpcService(accountsManager, keys, rateLimiters), basicCredentialAuthenticationInterceptor)) + .addService(new KeysAnonymousGrpcService(accountsManager, keys)) + .addService(new PaymentsGrpcService(currencyManager)) + .addService(ServerInterceptors.intercept(new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager, + config.getBadges(), asyncCdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, rateLimiters, zkProfileOperations, config.getCdnConfiguration().bucket()), basicCredentialAuthenticationInterceptor)) + .addService(new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter, zkProfileOperations)); + + RemoteDeprecationFilter remoteDeprecationFilter = new RemoteDeprecationFilter(dynamicConfigurationManager); + environment.servlets() + .addFilter("RemoteDeprecationFilter", remoteDeprecationFilter) + .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*"); + + // Note: interceptors run in the reverse order they are added; the remote deprecation filter + // depends on the user-agent context so it has to come first here! + // http://grpc.github.io/grpc-java/javadoc/io/grpc/ServerBuilder.html#intercept-io.grpc.ServerInterceptor- + grpcServer + // TODO: specialize metrics with user-agent platform + .intercept(new MetricCollectingServerInterceptor(Metrics.globalRegistry)) + .intercept(new ErrorMappingInterceptor()) + .intercept(new AcceptLanguageInterceptor()) + .intercept(remoteDeprecationFilter) + .intercept(new UserAgentInterceptor()); + + environment.lifecycle().manage(new GrpcServerManagedWrapper(grpcServer.build())); + + environment.jersey().register(new RequestStatisticsFilter(TrafficSource.HTTP)); + environment.jersey().register(MultiRecipientMessageProvider.class); + environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP, clientReleaseManager)); + environment.jersey() + .register(new PolymorphicAuthDynamicFeature<>(ImmutableMap.of(AuthenticatedAccount.class, accountAuthFilter, + DisabledPermittedAuthenticatedAccount.class, disabledPermittedAccountAuthFilter))); + environment.jersey().register(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))); + environment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager)); + environment.jersey().register(new TimestampResponseFilter()); + + /// + WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment<>(environment, + config.getWebSocketConfiguration(), 90000); + webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(accountAuthenticator)); + webSocketEnvironment.setConnectListener( + new AuthenticatedConnectListener(receiptSender, messagesManager, pushNotificationManager, + clientPresenceManager, websocketScheduledExecutor, messageDeliveryScheduler, clientReleaseManager)); + webSocketEnvironment.jersey() + .register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager)); + webSocketEnvironment.jersey().register(new RequestStatisticsFilter(TrafficSource.WEBSOCKET)); + webSocketEnvironment.jersey().register(MultiRecipientMessageProvider.class); + webSocketEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET, clientReleaseManager)); + webSocketEnvironment.jersey().register(new KeepAliveController(clientPresenceManager)); + + // these should be common, but use @Auth DisabledPermittedAccount, which isn’t supported yet on websocket + environment.jersey().register( + new AccountController(accountsManager, rateLimiters, + turnTokenGenerator, + registrationRecoveryPasswordsManager, usernameHashZkProofVerifier)); + + environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager)); + + boolean registeredSpamFilter = false; + ReportSpamTokenProvider reportSpamTokenProvider = null; + + for (final SpamFilter filter : ServiceLoader.load(SpamFilter.class)) { + if (filter.getClass().isAnnotationPresent(FilterSpam.class)) { + try { + filter.configure(config.getSpamFilterConfiguration().getEnvironment()); + + ReportSpamTokenProvider thisProvider = filter.getReportSpamTokenProvider(); + if (reportSpamTokenProvider == null) { + reportSpamTokenProvider = thisProvider; + } else if (thisProvider != null) { + log.info("Multiple spam report token providers found. Using the first."); + } + + filter.getReportedMessageListeners().forEach(reportMessageManager::addListener); + + environment.lifecycle().manage(filter); + environment.jersey().register(filter); + webSocketEnvironment.jersey().register(filter); + + log.info("Registered spam filter: {}", filter.getClass().getName()); + registeredSpamFilter = true; + } catch (final Exception e) { + log.warn("Failed to register spam filter: {}", filter.getClass().getName(), e); + } + } else { + log.warn("Spam filter {} not annotated with @FilterSpam and will not be installed", + filter.getClass().getName()); + } + + if (filter instanceof RateLimitChallengeListener) { + log.info("Registered rate limit challenge listener: {}", filter.getClass().getName()); + rateLimitChallengeManager.addListener((RateLimitChallengeListener) filter); + } + } + + if (!registeredSpamFilter) { + log.warn("No spam filters installed"); + } + + if (reportSpamTokenProvider == null) { + log.warn("No spam-reporting token providers found; using default (no-op) provider as a default"); + reportSpamTokenProvider = ReportSpamTokenProvider.noop(); + } + + final List commonControllers = Lists.newArrayList( + new AccountControllerV2(accountsManager, changeNumberManager, phoneVerificationTokenManager, + registrationLockVerificationManager, rateLimiters), + new ArtController(rateLimiters, artCredentialsGenerator), + new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().accessKey().value(), config.getAwsAttachmentsConfiguration().accessSecret().value(), config.getAwsAttachmentsConfiguration().region(), config.getAwsAttachmentsConfiguration().bucket()), + new AttachmentControllerV3(rateLimiters, gcsAttachmentGenerator), + new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, new TusAttachmentGenerator(config.getTus()), experimentEnrollmentManager), + new CallLinkController(rateLimiters, genericZkSecretParams), + new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(), config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()), zkAuthOperations, genericZkSecretParams, clock), + new ChallengeController(rateLimitChallengeManager), + new DeviceController(config.getLinkDeviceSecretConfiguration().secret().value(), accountsManager, messagesManager, keys, rateLimiters, + rateLimitersCluster, config.getMaxDevices(), clock), + new DirectoryV2Controller(directoryV2CredentialsGenerator), + new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(), + ReceiptCredentialPresentation::new), + new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender, receiptSender, + accountsManager, messagesManager, pushNotificationManager, reportMessageManager, + multiRecipientMessageExecutor, messageDeliveryScheduler, reportSpamTokenProvider, clientReleaseManager, + dynamicConfigurationManager), + new PaymentsController(currencyManager, paymentsCredentialsGenerator), + new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager, + profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, + config.getCdnConfiguration().bucket(), zkProfileOperations, batchIdentityCheckExecutor), + new ProvisioningController(rateLimiters, provisioningManager), + new RegistrationController(accountsManager, phoneVerificationTokenManager, registrationLockVerificationManager, + keys, rateLimiters), + new RemoteConfigController(remoteConfigsManager, adminEventLogger, + config.getRemoteConfigConfiguration().authorizedUsers(), + config.getRemoteConfigConfiguration().requiredHostedDomain(), + config.getRemoteConfigConfiguration().audiences(), + new GoogleIdTokenVerifier.Builder(new ApacheHttpTransport(), new GsonFactory()), + config.getRemoteConfigConfiguration().globalConfig()), + new SecureStorageController(storageCredentialsGenerator), + new SecureValueRecovery2Controller(svr2CredentialsGenerator, accountsManager), + new StickerController(rateLimiters, config.getCdnConfiguration().accessKey().value(), + config.getCdnConfiguration().accessSecret().value(), config.getCdnConfiguration().region(), + config.getCdnConfiguration().bucket()), + new VerificationController(registrationServiceClient, new VerificationSessionManager(verificationSessions), + pushNotificationManager, registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters, + accountsManager, clock) + ); + if (config.getSubscription() != null && config.getOneTimeDonations() != null) { + commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getOneTimeDonations(), + subscriptionManager, stripeManager, braintreeManager, zkReceiptOperations, issuedReceiptsManager, profileBadgeConverter, + resourceBundleLevelTranslator, bankMandateTranslator)); + } + + for (Object controller : commonControllers) { + environment.jersey().register(controller); + webSocketEnvironment.jersey().register(controller); + } + + WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment<>(environment, + webSocketEnvironment.getRequestLog(), 60000); + provisioningEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager)); + provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(provisioningManager)); + provisioningEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET, clientReleaseManager)); + provisioningEnvironment.jersey().register(new KeepAliveController(clientPresenceManager)); + + registerCorsFilter(environment); + registerExceptionMappers(environment, webSocketEnvironment, provisioningEnvironment); + registerProviders(environment, webSocketEnvironment, provisioningEnvironment); + + environment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE); + webSocketEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE); + provisioningEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE); + + WebSocketResourceProviderFactory webSocketServlet = new WebSocketResourceProviderFactory<>( + webSocketEnvironment, AuthenticatedAccount.class, config.getWebSocketConfiguration()); + WebSocketResourceProviderFactory provisioningServlet = new WebSocketResourceProviderFactory<>( + provisioningEnvironment, AuthenticatedAccount.class, config.getWebSocketConfiguration()); + + ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", webSocketServlet); + ServletRegistration.Dynamic provisioning = environment.servlets().addServlet("Provisioning", provisioningServlet); + + websocket.addMapping("/v1/websocket/"); + websocket.setAsyncSupported(true); + + provisioning.addMapping("/v1/websocket/provisioning/"); + provisioning.setAsyncSupported(true); + + environment.admin().addTask(new SetRequestLoggingEnabledTask()); + + environment.healthChecks().register("cacheCluster", new RedisClusterHealthCheck(cacheCluster)); + + MetricsUtil.registerSystemResourceMetrics(environment); + + // Note: Custom code + environment.lifecycle().addServerLifecycleListener(server -> jettyServer = server); + } + + + private void registerProviders(Environment environment, + WebSocketEnvironment webSocketEnvironment, + WebSocketEnvironment provisioningEnvironment) { + environment.jersey().register(ScoreThresholdProvider.ScoreThresholdFeature.class); + webSocketEnvironment.jersey().register(ScoreThresholdProvider.ScoreThresholdFeature.class); + provisioningEnvironment.jersey().register(ScoreThresholdProvider.ScoreThresholdFeature.class); + } + + private void registerExceptionMappers(Environment environment, + WebSocketEnvironment webSocketEnvironment, + WebSocketEnvironment provisioningEnvironment) { + + List.of( + new LoggingUnhandledExceptionMapper(), + new CompletionExceptionMapper(), + new IOExceptionMapper(), + new RateLimitExceededExceptionMapper(), + new InvalidWebsocketAddressExceptionMapper(), + new DeviceLimitExceededExceptionMapper(), + new ServerRejectedExceptionMapper(), + new ImpossiblePhoneNumberExceptionMapper(), + new NonNormalizedPhoneNumberExceptionMapper(), + new RegistrationServiceSenderExceptionMapper(), + new SubscriptionProcessorExceptionMapper(), + new JsonMappingExceptionMapper() + ).forEach(exceptionMapper -> { + environment.jersey().register(exceptionMapper); + webSocketEnvironment.jersey().register(exceptionMapper); + provisioningEnvironment.jersey().register(exceptionMapper); + }); + } + + private void registerCorsFilter(Environment environment) { + FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class); + filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*"); + filter.setInitParameter("allowedOrigins", "*"); + filter.setInitParameter("allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin,X-Signal-Agent"); + filter.setInitParameter("allowedMethods", "GET,PUT,POST,DELETE,OPTIONS"); + filter.setInitParameter("preflightMaxAge", "5184000"); + filter.setInitParameter("allowCredentials", "true"); + } + + public static void main(String[] args) throws Exception { + new WhisperServerService().run(args); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/attachments/AttachmentGenerator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/attachments/AttachmentGenerator.java new file mode 100644 index 000000000..e0525aa75 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/attachments/AttachmentGenerator.java @@ -0,0 +1,15 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.attachments; +import java.util.Map; + +public interface AttachmentGenerator { + + record Descriptor(Map headers, String signedUploadLocation) {} + + Descriptor generateAttachment(final String key); + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/attachments/GcsAttachmentGenerator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/attachments/GcsAttachmentGenerator.java new file mode 100644 index 000000000..177a5e03b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/attachments/GcsAttachmentGenerator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.attachments; + +import org.whispersystems.textsecuregcm.gcp.CanonicalRequest; +import org.whispersystems.textsecuregcm.gcp.CanonicalRequestGenerator; +import org.whispersystems.textsecuregcm.gcp.CanonicalRequestSigner; +import javax.annotation.Nonnull; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.spec.InvalidKeySpecException; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Map; + +public class GcsAttachmentGenerator implements AttachmentGenerator { + @Nonnull + private final CanonicalRequestGenerator canonicalRequestGenerator; + + @Nonnull + private final CanonicalRequestSigner canonicalRequestSigner; + + public GcsAttachmentGenerator(@Nonnull String domain, @Nonnull String email, + int maxSizeInBytes, @Nonnull String pathPrefix, @Nonnull String rsaSigningKey) + throws IOException, InvalidKeyException, InvalidKeySpecException { + this.canonicalRequestGenerator = new CanonicalRequestGenerator(domain, email, maxSizeInBytes, pathPrefix); + this.canonicalRequestSigner = new CanonicalRequestSigner(rsaSigningKey); + } + + @Override + public Descriptor generateAttachment(final String key) { + final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); + final CanonicalRequest canonicalRequest = canonicalRequestGenerator.createFor(key, now); + return new Descriptor(getHeaderMap(canonicalRequest), getSignedUploadLocation(canonicalRequest)); + } + + private String getSignedUploadLocation(@Nonnull CanonicalRequest canonicalRequest) { + return "https://" + canonicalRequest.getDomain() + canonicalRequest.getResourcePath() + + '?' + canonicalRequest.getCanonicalQuery() + + "&X-Goog-Signature=" + canonicalRequestSigner.sign(canonicalRequest); + } + + private static Map getHeaderMap(@Nonnull CanonicalRequest canonicalRequest) { + return Map.of( + "host", canonicalRequest.getDomain(), + "x-goog-content-length-range", "1," + canonicalRequest.getMaxSizeInBytes(), + "x-goog-resumable", "start"); + } + + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/attachments/TusAttachmentGenerator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/attachments/TusAttachmentGenerator.java new file mode 100644 index 000000000..9fc7a71c2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/attachments/TusAttachmentGenerator.java @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.attachments; + +import org.apache.http.HttpHeaders; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.util.HeaderUtils; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.util.Base64; +import java.util.Map; + +public class TusAttachmentGenerator implements AttachmentGenerator { + + private static final String ATTACHMENTS = "attachments"; + + final ExternalServiceCredentialsGenerator credentialsGenerator; + final String tusUri; + + public TusAttachmentGenerator(final TusConfiguration cfg) { + this.tusUri = cfg.uploadUri(); + this.credentialsGenerator = credentialsGenerator(Clock.systemUTC(), cfg); + } + + private static ExternalServiceCredentialsGenerator credentialsGenerator(final Clock clock, final TusConfiguration cfg) { + return ExternalServiceCredentialsGenerator + .builder(cfg.userAuthenticationTokenSharedSecret()) + .prependUsername(false) + .withClock(clock) + .build(); + } + + @Override + public Descriptor generateAttachment(final String key) { + final ExternalServiceCredentials credentials = credentialsGenerator.generateFor(ATTACHMENTS + "/" + key); + final String b64Key = Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8)); + final Map headers = Map.of( + HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(credentials), + "Upload-Metadata", String.format("filename %s", b64Key) + ); + return new Descriptor(headers, tusUri + "/" + ATTACHMENTS); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/attachments/TusConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/attachments/TusConfiguration.java new file mode 100644 index 000000000..5991c8ab3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/attachments/TusConfiguration.java @@ -0,0 +1,15 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.attachments; + +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; +import org.whispersystems.textsecuregcm.util.ExactlySize; +import javax.validation.constraints.NotEmpty; + +public record TusConfiguration( + @ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret, + @NotEmpty String uploadUri +){} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAndAuthenticatedDeviceHolder.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAndAuthenticatedDeviceHolder.java new file mode 100644 index 000000000..bf10bd657 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAndAuthenticatedDeviceHolder.java @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; + +public interface AccountAndAuthenticatedDeviceHolder { + + Account getAccount(); + + Device getAuthenticatedDevice(); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAuthenticator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAuthenticator.java new file mode 100644 index 000000000..a1e9ffa7e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAuthenticator.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.auth; + +import io.dropwizard.auth.Authenticator; +import io.dropwizard.auth.basic.BasicCredentials; +import java.util.Optional; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; +import org.whispersystems.textsecuregcm.storage.AccountsManager; + +public class AccountAuthenticator extends BaseAccountAuthenticator implements + Authenticator { + + public AccountAuthenticator(AccountsManager accountsManager) { + super(accountsManager); + } + + @Override + public Optional authenticate(BasicCredentials basicCredentials) { + return super.authenticate(basicCredentials, true); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/Anonymous.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/Anonymous.java new file mode 100644 index 000000000..fa8a8190e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/Anonymous.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import java.util.Base64; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; + +public class Anonymous { + + private final byte[] unidentifiedSenderAccessKey; + + public Anonymous(String header) { + try { + this.unidentifiedSenderAccessKey = Base64.getDecoder().decode(header); + } catch (IllegalArgumentException e) { + throw new WebApplicationException(e, Response.Status.UNAUTHORIZED); + } + } + + public byte[] getAccessKey() { + return unidentifiedSenderAccessKey; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRefreshRequirementProvider.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRefreshRequirementProvider.java new file mode 100644 index 000000000..1e3e5b22a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRefreshRequirementProvider.java @@ -0,0 +1,100 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.Pair; + +/** + * This {@link WebsocketRefreshRequirementProvider} observes intra-request changes in {@link Account#isEnabled()} and + * {@link Device#isEnabled()}. + *

    + * If a change in {@link Account#isEnabled()} or any associated {@link Device#isEnabled()} is observed, then any active + * WebSocket connections for the account must be closed in order for clients to get a refreshed + * {@link io.dropwizard.auth.Auth} object with a current device list. + * + * @see AuthenticatedAccount + * @see DisabledPermittedAuthenticatedAccount + */ +public class AuthEnablementRefreshRequirementProvider implements WebsocketRefreshRequirementProvider { + + private final AccountsManager accountsManager; + + private static final Logger logger = LoggerFactory.getLogger(AuthEnablementRefreshRequirementProvider.class); + + private static final String ACCOUNT_UUID = AuthEnablementRefreshRequirementProvider.class.getName() + ".accountUuid"; + private static final String DEVICES_ENABLED = AuthEnablementRefreshRequirementProvider.class.getName() + ".devicesEnabled"; + + public AuthEnablementRefreshRequirementProvider(final AccountsManager accountsManager) { + this.accountsManager = accountsManager; + } + + @VisibleForTesting + static Map buildDevicesEnabledMap(final Account account) { + return account.getDevices().stream().collect(Collectors.toMap(Device::getId, Device::isEnabled)); + } + + @Override + public void handleRequestFiltered(final RequestEvent requestEvent) { + if (requestEvent.getUriInfo().getMatchedResourceMethod().getInvocable().getHandlingMethod().getAnnotation(ChangesDeviceEnabledState.class) != null) { + // The authenticated principal, if any, will be available after filters have run. + // Now that the account is known, capture a snapshot of `isEnabled` for the account's devices before carrying out + // the request’s business logic. + ContainerRequestUtil.getAuthenticatedAccount(requestEvent.getContainerRequest()).ifPresent(account -> + setAccount(requestEvent.getContainerRequest(), account)); + } + } + + public static void setAccount(final ContainerRequest containerRequest, final Account account) { + containerRequest.setProperty(ACCOUNT_UUID, account.getUuid()); + containerRequest.setProperty(DEVICES_ENABLED, buildDevicesEnabledMap(account)); + } + + @Override + public List> handleRequestFinished(final RequestEvent requestEvent) { + // Now that the request is finished, check whether `isEnabled` changed for any of the devices. If the value did + // change or if a devices was added or removed, all devices must disconnect and reauthenticate. + if (requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED) != null) { + + @SuppressWarnings("unchecked") final Map initialDevicesEnabled = + (Map) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED); + + return accountsManager.getByAccountIdentifier((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> { + final Set deviceIdsToDisplace; + final Map currentDevicesEnabled = buildDevicesEnabledMap(account); + + if (!initialDevicesEnabled.equals(currentDevicesEnabled)) { + deviceIdsToDisplace = new HashSet<>(initialDevicesEnabled.keySet()); + deviceIdsToDisplace.addAll(currentDevicesEnabled.keySet()); + } else { + deviceIdsToDisplace = Collections.emptySet(); + } + + return deviceIdsToDisplace.stream() + .map(deviceId -> new Pair<>(account.getUuid(), deviceId)) + .collect(Collectors.toList()); + }).orElseGet(() -> { + logger.error("Request had account, but it is no longer present"); + return Collections.emptyList(); + }); + } else + return Collections.emptyList(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticatedAccount.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticatedAccount.java new file mode 100644 index 000000000..13c9e504c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticatedAccount.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import java.security.Principal; +import java.util.function.Supplier; +import javax.security.auth.Subject; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.Pair; + +public class AuthenticatedAccount implements Principal, AccountAndAuthenticatedDeviceHolder { + + private final Supplier> accountAndDevice; + + public AuthenticatedAccount(final Supplier> accountAndDevice) { + this.accountAndDevice = accountAndDevice; + } + + @Override + public Account getAccount() { + return accountAndDevice.get().first(); + } + + @Override + public Device getAuthenticatedDevice() { + return accountAndDevice.get().second(); + } + + // Principal implementation + + @Override + public String getName() { + return null; + } + + @Override + public boolean implies(final Subject subject) { + return false; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticator.java new file mode 100644 index 000000000..be4ee98a5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticator.java @@ -0,0 +1,173 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.auth.basic.BasicCredentials; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import java.time.Clock; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.UUID; +import org.apache.commons.lang3.StringUtils; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.RefreshingAccountAndDeviceSupplier; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.Util; + +public class BaseAccountAuthenticator { + + private static final String AUTHENTICATION_COUNTER_NAME = name(BaseAccountAuthenticator.class, "authentication"); + private static final String ENABLED_NOT_REQUIRED_AUTHENTICATION_COUNTER_NAME = name(BaseAccountAuthenticator.class, + "enabledNotRequiredAuthentication"); + private static final String AUTHENTICATION_SUCCEEDED_TAG_NAME = "succeeded"; + private static final String AUTHENTICATION_FAILURE_REASON_TAG_NAME = "reason"; + private static final String ENABLED_TAG_NAME = "enabled"; + + private static final String DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME = name(BaseAccountAuthenticator.class, "daysSinceLastSeen"); + private static final String IS_PRIMARY_DEVICE_TAG = "isPrimary"; + + @VisibleForTesting + static final char DEVICE_ID_SEPARATOR = '.'; + + private final AccountsManager accountsManager; + private final Clock clock; + + public BaseAccountAuthenticator(AccountsManager accountsManager) { + this(accountsManager, Clock.systemUTC()); + } + + @VisibleForTesting + public BaseAccountAuthenticator(AccountsManager accountsManager, Clock clock) { + this.accountsManager = accountsManager; + this.clock = clock; + } + + static Pair getIdentifierAndDeviceId(final String basicUsername) { + final String identifier; + final long deviceId; + + final int deviceIdSeparatorIndex = basicUsername.indexOf(DEVICE_ID_SEPARATOR); + + if (deviceIdSeparatorIndex == -1) { + identifier = basicUsername; + deviceId = Device.MASTER_ID; + } else { + identifier = basicUsername.substring(0, deviceIdSeparatorIndex); + deviceId = Long.parseLong(basicUsername.substring(deviceIdSeparatorIndex + 1)); + } + + return new Pair<>(identifier, deviceId); + } + + public Optional authenticate(BasicCredentials basicCredentials, boolean enabledRequired) { + boolean succeeded = false; + String failureReason = null; + + try { + final UUID accountUuid; + final long deviceId; + { + final Pair identifierAndDeviceId = getIdentifierAndDeviceId(basicCredentials.getUsername()); + + accountUuid = UUID.fromString(identifierAndDeviceId.first()); + deviceId = identifierAndDeviceId.second(); + } + + Optional account = accountsManager.getByAccountIdentifier(accountUuid); + + if (account.isEmpty()) { + failureReason = "noSuchAccount"; + return Optional.empty(); + } + + Optional device = account.get().getDevice(deviceId); + + if (device.isEmpty()) { + failureReason = "noSuchDevice"; + return Optional.empty(); + } + + if (enabledRequired) { + final boolean deviceDisabled = !device.get().isEnabled(); + if (deviceDisabled) { + failureReason = "deviceDisabled"; + } + + final boolean accountDisabled = !account.get().isEnabled(); + if (accountDisabled) { + failureReason = "accountDisabled"; + } + if (accountDisabled || deviceDisabled) { + return Optional.empty(); + } + } else { + Metrics.counter(ENABLED_NOT_REQUIRED_AUTHENTICATION_COUNTER_NAME, + ENABLED_TAG_NAME, String.valueOf(device.get().isEnabled() && account.get().isEnabled()), + IS_PRIMARY_DEVICE_TAG, String.valueOf(device.get().isMaster())) + .increment(); + } + + SaltedTokenHash deviceSaltedTokenHash = device.get().getAuthTokenHash(); + if (deviceSaltedTokenHash.verify(basicCredentials.getPassword())) { + succeeded = true; + Account authenticatedAccount = updateLastSeen(account.get(), device.get()); + if (deviceSaltedTokenHash.getVersion() != SaltedTokenHash.CURRENT_VERSION) { + authenticatedAccount = accountsManager.updateDeviceAuthentication( + authenticatedAccount, + device.get(), + SaltedTokenHash.generateFor(basicCredentials.getPassword())); // new credentials have current version + } + return Optional.of(new AuthenticatedAccount( + new RefreshingAccountAndDeviceSupplier(authenticatedAccount, device.get().getId(), accountsManager))); + } + + return Optional.empty(); + } catch (IllegalArgumentException | InvalidAuthorizationHeaderException iae) { + failureReason = "invalidHeader"; + return Optional.empty(); + } finally { + Tags tags = Tags.of( + AUTHENTICATION_SUCCEEDED_TAG_NAME, String.valueOf(succeeded)); + + if (StringUtils.isNotBlank(failureReason)) { + tags = tags.and(AUTHENTICATION_FAILURE_REASON_TAG_NAME, failureReason); + } + + Metrics.counter(AUTHENTICATION_COUNTER_NAME, tags).increment(); + } + } + + @VisibleForTesting + public Account updateLastSeen(Account account, Device device) { + // compute a non-negative integer between 0 and 86400. + long n = Util.ensureNonNegativeLong(account.getUuid().getLeastSignificantBits()); + final long lastSeenOffsetSeconds = n % ChronoUnit.DAYS.getDuration().toSeconds(); + + // produce a truncated timestamp which is either today at UTC midnight + // or yesterday at UTC midnight, based on per-user randomized offset used. + final long todayInMillisWithOffset = Util.todayInMillisGivenOffsetFromNow(clock, Duration.ofSeconds(lastSeenOffsetSeconds).negated()); + + // only update the device's last seen time when it falls behind the truncated timestamp. + // this ensure a few things: + // (1) each account will only update last-seen at most once per day + // (2) these updates will occur throughout the day rather than all occurring at UTC midnight. + if (device.getLastSeen() < todayInMillisWithOffset) { + Metrics.summary(DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME, IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isMaster())) + .record(Duration.ofMillis(todayInMillisWithOffset - device.getLastSeen()).toDays()); + + return accountsManager.updateDeviceLastSeen(account, device, Util.todayInMillis(clock)); + } + + return account; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/BasicAuthorizationHeader.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/BasicAuthorizationHeader.java new file mode 100644 index 000000000..05e4f4f27 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/BasicAuthorizationHeader.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.auth; + +import java.util.Base64; +import org.apache.commons.lang3.StringUtils; +import org.whispersystems.textsecuregcm.util.Pair; + +public class BasicAuthorizationHeader { + + private final String username; + private final long deviceId; + private final String password; + + private BasicAuthorizationHeader(final String username, final long deviceId, final String password) { + this.username = username; + this.deviceId = deviceId; + this.password = password; + } + + public static BasicAuthorizationHeader fromString(final String header) throws InvalidAuthorizationHeaderException { + try { + if (StringUtils.isBlank(header)) { + throw new InvalidAuthorizationHeaderException("Blank header"); + } + + final int spaceIndex = header.indexOf(' '); + + if (spaceIndex == -1) { + throw new InvalidAuthorizationHeaderException("Invalid authorization header: " + header); + } + + final String authorizationType = header.substring(0, spaceIndex); + + if (!"Basic".equals(authorizationType)) { + throw new InvalidAuthorizationHeaderException("Unsupported authorization method: " + authorizationType); + } + + final String credentials; + + try { + credentials = new String(Base64.getDecoder().decode(header.substring(spaceIndex + 1))); + } catch (final IndexOutOfBoundsException e) { + throw new InvalidAuthorizationHeaderException("Missing credentials"); + } + + if (StringUtils.isEmpty(credentials)) { + throw new InvalidAuthorizationHeaderException("Bad decoded value: " + credentials); + } + + final int credentialSeparatorIndex = credentials.indexOf(':'); + + if (credentialSeparatorIndex == -1) { + throw new InvalidAuthorizationHeaderException("Badly-formatted credentials: " + credentials); + } + + final String usernameComponent = credentials.substring(0, credentialSeparatorIndex); + + final String username; + final long deviceId; + { + final Pair identifierAndDeviceId = + BaseAccountAuthenticator.getIdentifierAndDeviceId(usernameComponent); + + username = identifierAndDeviceId.first(); + deviceId = identifierAndDeviceId.second(); + } + + final String password = credentials.substring(credentialSeparatorIndex + 1); + + if (StringUtils.isAnyBlank(username, password)) { + throw new InvalidAuthorizationHeaderException("Username or password were blank"); + } + + return new BasicAuthorizationHeader(username, deviceId, password); + } catch (final IllegalArgumentException | IndexOutOfBoundsException e) { + throw new InvalidAuthorizationHeaderException(e); + } + } + + public String getUsername() { + return username; + } + + public long getDeviceId() { + return deviceId; + } + + public String getPassword() { + return password; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/CertificateGenerator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/CertificateGenerator.java new file mode 100644 index 000000000..93a568ae6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/CertificateGenerator.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import java.security.InvalidKeyException; +import java.util.concurrent.TimeUnit; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECPrivateKey; +import org.whispersystems.textsecuregcm.entities.MessageProtos.SenderCertificate; +import org.whispersystems.textsecuregcm.entities.MessageProtos.ServerCertificate; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; + +public class CertificateGenerator { + + private final ECPrivateKey privateKey; + private final int expiresDays; + private final ServerCertificate serverCertificate; + + public CertificateGenerator(byte[] serverCertificate, ECPrivateKey privateKey, int expiresDays) + throws InvalidProtocolBufferException + { + this.privateKey = privateKey; + this.expiresDays = expiresDays; + this.serverCertificate = ServerCertificate.parseFrom(serverCertificate); + } + + public byte[] createFor(Account account, Device device, boolean includeE164) throws InvalidKeyException { + SenderCertificate.Certificate.Builder builder = SenderCertificate.Certificate.newBuilder() + .setSenderDevice(Math.toIntExact(device.getId())) + .setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays)) + .setIdentityKey(ByteString.copyFrom(account.getIdentityKey(IdentityType.ACI).serialize())) + .setSigner(serverCertificate) + .setSenderUuid(account.getUuid().toString()); + + if (includeE164) { + builder.setSender(account.getNumber()); + } + + byte[] certificate = builder.build().toByteArray(); + byte[] signature; + try { + signature = Curve.calculateSignature(privateKey, certificate); + } catch (org.signal.libsignal.protocol.InvalidKeyException e) { + throw new InvalidKeyException(e); + } + + return SenderCertificate.newBuilder() + .setCertificate(ByteString.copyFrom(certificate)) + .setSignature(ByteString.copyFrom(signature)) + .build() + .toByteArray(); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ChangesDeviceEnabledState.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ChangesDeviceEnabledState.java new file mode 100644 index 000000000..dc4911cdb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ChangesDeviceEnabledState.java @@ -0,0 +1,20 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that an endpoint may change the "enabled" state of one or more devices associated with an account, and that + * any websockets associated with the account may need to be refreshed after a call to that endpoint. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ChangesDeviceEnabledState { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/CombinedUnidentifiedSenderAccessKeys.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/CombinedUnidentifiedSenderAccessKeys.java new file mode 100644 index 000000000..f101692bf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/CombinedUnidentifiedSenderAccessKeys.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import java.util.Base64; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +public class CombinedUnidentifiedSenderAccessKeys { + private final byte[] combinedUnidentifiedSenderAccessKeys; + + public CombinedUnidentifiedSenderAccessKeys(String header) { + try { + this.combinedUnidentifiedSenderAccessKeys = Base64.getDecoder().decode(header); + if (this.combinedUnidentifiedSenderAccessKeys == null || this.combinedUnidentifiedSenderAccessKeys.length != 16) { + throw new WebApplicationException("Invalid combined unidentified sender access keys", Status.UNAUTHORIZED); + } + } catch (IllegalArgumentException e) { + throw new WebApplicationException(e, Response.Status.UNAUTHORIZED); + } + } + + public byte[] getAccessKeys() { + return combinedUnidentifiedSenderAccessKeys; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ContainerRequestUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ContainerRequestUtil.java new file mode 100644 index 000000000..f551b9761 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ContainerRequestUtil.java @@ -0,0 +1,21 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import org.glassfish.jersey.server.ContainerRequest; +import org.whispersystems.textsecuregcm.storage.Account; +import javax.ws.rs.core.SecurityContext; +import java.util.Optional; + +class ContainerRequestUtil { + + static Optional getAuthenticatedAccount(final ContainerRequest request) { + return Optional.ofNullable(request.getSecurityContext()) + .map(SecurityContext::getUserPrincipal) + .map(principal -> principal instanceof AccountAndAuthenticatedDeviceHolder + ? ((AccountAndAuthenticatedDeviceHolder) principal).getAccount() : null); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/DisabledPermittedAccountAuthenticator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/DisabledPermittedAccountAuthenticator.java new file mode 100644 index 000000000..03a365ad8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/DisabledPermittedAccountAuthenticator.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import io.dropwizard.auth.Authenticator; +import io.dropwizard.auth.basic.BasicCredentials; +import java.util.Optional; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; +import org.whispersystems.textsecuregcm.storage.AccountsManager; + +public class DisabledPermittedAccountAuthenticator extends BaseAccountAuthenticator implements + Authenticator { + + public DisabledPermittedAccountAuthenticator(AccountsManager accountsManager) { + super(accountsManager); + } + + @Override + public Optional authenticate(BasicCredentials credentials) { + Optional account = super.authenticate(credentials, false); + return account.map(DisabledPermittedAuthenticatedAccount::new); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/DisabledPermittedAuthenticatedAccount.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/DisabledPermittedAuthenticatedAccount.java new file mode 100644 index 000000000..2b4fd73f1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/DisabledPermittedAuthenticatedAccount.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import java.security.Principal; +import javax.security.auth.Subject; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; + +public class DisabledPermittedAuthenticatedAccount implements Principal, AccountAndAuthenticatedDeviceHolder { + + private final AuthenticatedAccount authenticatedAccount; + + public DisabledPermittedAuthenticatedAccount(final AuthenticatedAccount authenticatedAccount) { + this.authenticatedAccount = authenticatedAccount; + } + + @Override + public Account getAccount() { + return authenticatedAccount.getAccount(); + } + + @Override + public Device getAuthenticatedDevice() { + return authenticatedAccount.getAuthenticatedDevice(); + } + + // Principal implementation + + @Override + public String getName() { + return null; + } + + @Override + public boolean implies(Subject subject) { + return false; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentials.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentials.java new file mode 100644 index 000000000..5364d460e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentials.java @@ -0,0 +1,11 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + + +public record ExternalServiceCredentials(String username, String password) { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialsGenerator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialsGenerator.java new file mode 100644 index 000000000..38c9958b1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialsGenerator.java @@ -0,0 +1,293 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static java.util.Objects.requireNonNull; +import static org.whispersystems.textsecuregcm.util.HmacUtils.hmac256ToHexString; +import static org.whispersystems.textsecuregcm.util.HmacUtils.hmac256TruncatedToHexString; +import static org.whispersystems.textsecuregcm.util.HmacUtils.hmacHexStringsEqual; + +import com.google.common.annotations.VisibleForTesting; +import java.time.Clock; +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Function; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; + +public class ExternalServiceCredentialsGenerator { + + private static final String DELIMITER = ":"; + + private static final int TRUNCATED_SIGNATURE_LENGTH = 10; + + private final byte[] key; + + private final byte[] userDerivationKey; + + private final boolean prependUsername; + + private final boolean truncateSignature; + + private final String usernameTimestampPrefix; + + private final Function usernameTimestampTruncator; + + private final Clock clock; + + private final int derivedUsernameTruncateLength; + + + public static ExternalServiceCredentialsGenerator.Builder builder(final SecretBytes key) { + return builder(key.value()); + } + + @VisibleForTesting + public static ExternalServiceCredentialsGenerator.Builder builder(final byte[] key) { + return new Builder(key); + } + + private ExternalServiceCredentialsGenerator( + final byte[] key, + final byte[] userDerivationKey, + final boolean prependUsername, + final boolean truncateSignature, + final int derivedUsernameTruncateLength, + final String usernameTimestampPrefix, + final Function usernameTimestampTruncator, + final Clock clock) { + this.key = requireNonNull(key); + this.userDerivationKey = requireNonNull(userDerivationKey); + this.prependUsername = prependUsername; + this.truncateSignature = truncateSignature; + this.usernameTimestampPrefix = usernameTimestampPrefix; + this.usernameTimestampTruncator = usernameTimestampTruncator; + this.clock = requireNonNull(clock); + this.derivedUsernameTruncateLength = derivedUsernameTruncateLength; + + if (hasUsernameTimestampPrefix() ^ hasUsernameTimestampTruncator()) { + throw new RuntimeException("Configured to have only one of (usernameTimestampPrefix, usernameTimestampTruncator)"); + } + } + + /** + * A convenience method for the case of identity in the form of {@link UUID}. + * @param uuid identity to generate credentials for + * @return an instance of {@link ExternalServiceCredentials} + */ + public ExternalServiceCredentials generateForUuid(final UUID uuid) { + return generateFor(uuid.toString()); + } + + /** + * Generates `ExternalServiceCredentials` for the given identity following this generator's configuration. + * @param identity identity string to generate credentials for + * @return an instance of {@link ExternalServiceCredentials} + */ + public ExternalServiceCredentials generateFor(final String identity) { + if (usernameIsTimestamp()) { + throw new RuntimeException("Configured to use timestamp as username"); + } + + return generate(identity); + } + + /** + * Generates `ExternalServiceCredentials` using a prefix concatenated with a truncated timestamp as the username, following this generator's configuration. + * @return an instance of {@link ExternalServiceCredentials} + */ + public ExternalServiceCredentials generateWithTimestampAsUsername() { + if (!usernameIsTimestamp()) { + throw new RuntimeException("Not configured to use timestamp as username"); + } + + final String truncatedTimestampSeconds = String.valueOf(usernameTimestampTruncator.apply(clock.instant()).getEpochSecond()); + return generate(usernameTimestampPrefix + DELIMITER + truncatedTimestampSeconds); + } + + private ExternalServiceCredentials generate(final String identity) { + final String username = shouldDeriveUsername() + ? hmac256TruncatedToHexString(userDerivationKey, identity, derivedUsernameTruncateLength) + : identity; + + final long currentTimeSeconds = currentTimeSeconds(); + + final String dataToSign = usernameIsTimestamp() ? username : username + DELIMITER + currentTimeSeconds; + + final String signature = truncateSignature + ? hmac256TruncatedToHexString(key, dataToSign, TRUNCATED_SIGNATURE_LENGTH) + : hmac256ToHexString(key, dataToSign); + + final String token = (prependUsername ? dataToSign : currentTimeSeconds) + DELIMITER + signature; + + return new ExternalServiceCredentials(username, token); + } + + /** + * In certain cases, identity (as it was passed to `generate` method) + * is a part of the signature (`password`, in terms of `ExternalServiceCredentials`) string itself. + * For such cases, this method returns the value of the identity string. + * @param password `password` part of `ExternalServiceCredentials` + * @return non-empty optional with an identity string value, or empty if value can't be extracted. + */ + public Optional identityFromSignature(final String password) { + // for some generators, identity in the clear is just not a part of the password + if (!prependUsername || shouldDeriveUsername() || StringUtils.isBlank(password)) { + return Optional.empty(); + } + // checking for the case of unexpected format + if (StringUtils.countMatches(password, DELIMITER) == 2) { + if (usernameIsTimestamp()) { + final int indexOfSecondDelimiter = password.indexOf(DELIMITER, password.indexOf(DELIMITER) + 1); + return Optional.of(password.substring(0, indexOfSecondDelimiter)); + } else { + return Optional.of(password.substring(0, password.indexOf(DELIMITER))); + } + } + return Optional.empty(); + } + + /** + * Given an instance of {@link ExternalServiceCredentials} object, checks that the password + * matches the username taking into accound this generator's configuration. + * @param credentials an instance of {@link ExternalServiceCredentials} + * @return An optional with a timestamp (seconds) of when the credentials were generated, + * or an empty optional if the password doesn't match the username for any reason (including malformed data) + */ + public Optional validateAndGetTimestamp(final ExternalServiceCredentials credentials) { + final String[] parts = requireNonNull(credentials).password().split(DELIMITER); + final String timestampSeconds; + final String actualSignature; + + // making sure password format matches our expectations based on the generator configuration + if (parts.length == 3 && prependUsername) { + final String username = usernameIsTimestamp() ? parts[0] + DELIMITER + parts[1] : parts[0]; + // username has to match the one from `credentials` + if (!credentials.username().equals(username)) { + return Optional.empty(); + } + timestampSeconds = parts[1]; + actualSignature = parts[2]; + } else if (parts.length == 2 && !prependUsername) { + timestampSeconds = parts[0]; + actualSignature = parts[1]; + } else { + // unexpected password format + return Optional.empty(); + } + + final String signedData = usernameIsTimestamp() ? credentials.username() : credentials.username() + DELIMITER + timestampSeconds; + final String expectedSignature = truncateSignature + ? hmac256TruncatedToHexString(key, signedData, TRUNCATED_SIGNATURE_LENGTH) + : hmac256ToHexString(key, signedData); + + // if the signature is valid it's safe to parse the `timestampSeconds` string into Long + return hmacHexStringsEqual(expectedSignature, actualSignature) + ? Optional.of(Long.valueOf(timestampSeconds)) + : Optional.empty(); + } + + /** + * Given an instance of {@link ExternalServiceCredentials} object and the max allowed age for those credentials, + * checks if credentials are valid and not expired. + * @param credentials an instance of {@link ExternalServiceCredentials} + * @param maxAgeSeconds age in seconds + * @return An optional with a timestamp (seconds) of when the credentials were generated, + * or an empty optional if the password doesn't match the username for any reason (including malformed data) + */ + public Optional validateAndGetTimestamp(final ExternalServiceCredentials credentials, final long maxAgeSeconds) { + return validateAndGetTimestamp(credentials) + .filter(ts -> currentTimeSeconds() - ts <= maxAgeSeconds); + } + + private boolean shouldDeriveUsername() { + return userDerivationKey.length > 0; + } + + private boolean hasUsernameTimestampPrefix() { + return usernameTimestampPrefix != null; + } + + private boolean hasUsernameTimestampTruncator() { + return usernameTimestampTruncator != null; + } + + private boolean usernameIsTimestamp() { + return hasUsernameTimestampPrefix() && hasUsernameTimestampTruncator(); + } + + private long currentTimeSeconds() { + return clock.instant().getEpochSecond(); + } + + public static class Builder { + + private final byte[] key; + + private byte[] userDerivationKey = new byte[0]; + + private boolean prependUsername = true; + + private boolean truncateSignature = true; + + private int derivedUsernameTruncateLength = 10; + + private String usernameTimestampPrefix = null; + + private Function usernameTimestampTruncator = null; + + private Clock clock = Clock.systemUTC(); + + + private Builder(final byte[] key) { + this.key = requireNonNull(key); + } + + public Builder withUserDerivationKey(final SecretBytes userDerivationKey) { + return withUserDerivationKey(userDerivationKey.value()); + } + + public Builder withUserDerivationKey(final byte[] userDerivationKey) { + Validate.isTrue(requireNonNull(userDerivationKey).length > 0, "userDerivationKey must not be empty"); + this.userDerivationKey = userDerivationKey; + return this; + } + + public Builder withClock(final Clock clock) { + this.clock = requireNonNull(clock); + return this; + } + + public Builder withDerivedUsernameTruncateLength(int truncateLength) { + Validate.inclusiveBetween(10, 32, truncateLength); + this.derivedUsernameTruncateLength = truncateLength; + return this; + } + + public Builder prependUsername(final boolean prependUsername) { + this.prependUsername = prependUsername; + return this; + } + + public Builder truncateSignature(final boolean truncateSignature) { + this.truncateSignature = truncateSignature; + return this; + } + + public Builder withUsernameTimestampTruncatorAndPrefix(final Function truncator, final String prefix) { + this.usernameTimestampTruncator = truncator; + this.usernameTimestampPrefix = prefix; + return this; + } + + public ExternalServiceCredentialsGenerator build() { + return new ExternalServiceCredentialsGenerator( + key, userDerivationKey, prependUsername, truncateSignature, derivedUsernameTruncateLength, usernameTimestampPrefix, usernameTimestampTruncator, clock); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialsSelector.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialsSelector.java new file mode 100644 index 000000000..9c42e8b2c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialsSelector.java @@ -0,0 +1,84 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class ExternalServiceCredentialsSelector { + + private ExternalServiceCredentialsSelector() {} + + public record CredentialInfo(String token, boolean valid, ExternalServiceCredentials credentials, long timestamp) { + /** + * @return a copy of this record with valid=false + */ + private CredentialInfo invalidate() { + return new CredentialInfo(token, false, credentials, timestamp); + } + } + + /** + * Validate a list of username:password credentials. + * A credential is valid if it passes validation by the provided credentialsGenerator AND it is the most recent + * credential in the provided list for a username. + * + * @param tokens A list of credentials, potentially with different usernames + * @param credentialsGenerator To validate these credentials + * @param maxAgeSeconds The maximum allowable age of the credential + * @return A {@link CredentialInfo} for each provided token + */ + public static List check( + final List tokens, + final ExternalServiceCredentialsGenerator credentialsGenerator, + final long maxAgeSeconds) { + + // the credential for the username with the latest timestamp (so far) + final Map bestForUsername = new HashMap<>(); + final List results = new ArrayList<>(); + for (String token : tokens) { + // each token is supposed to be in a "${username}:${password}" form, + // (note that password part may also contain ':' characters) + final String[] parts = token.split(":", 2); + if (parts.length != 2) { + results.add(new CredentialInfo(token, false, null, 0L)); + continue; + } + final ExternalServiceCredentials credentials = new ExternalServiceCredentials(parts[0], parts[1]); + final Optional maybeTimestamp = credentialsGenerator.validateAndGetTimestamp(credentials, maxAgeSeconds); + if (maybeTimestamp.isEmpty()) { + results.add(new CredentialInfo(token, false, null, 0L)); + continue; + } + + // now that we validated signature and token age, we will also find the latest of the tokens + // for each username + final long timestamp = maybeTimestamp.get(); + final CredentialInfo best = bestForUsername.get(credentials.username()); + if (best == null) { + bestForUsername.put(credentials.username(), new CredentialInfo(token, true, credentials, timestamp)); + continue; + } + if (best.timestamp() < timestamp) { + // we found a better credential for the username + bestForUsername.put(credentials.username(), new CredentialInfo(token, true, credentials, timestamp)); + // mark the previous best as an invalid credential, since we have a better credential now + results.add(best.invalidate()); + } else { + // the credential we already had was more recent, this one can be marked invalid + results.add(new CredentialInfo(token, false, null, 0L)); + } + } + + // all invalid tokens should be in results, just add the valid ones + results.addAll(bestForUsername.values()); + return results; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/InvalidAuthorizationHeaderException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/InvalidAuthorizationHeaderException.java new file mode 100644 index 000000000..d5b85daa9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/InvalidAuthorizationHeaderException.java @@ -0,0 +1,19 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.auth; + + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response.Status; + +public class InvalidAuthorizationHeaderException extends WebApplicationException { + public InvalidAuthorizationHeaderException(String s) { + super(s, Status.UNAUTHORIZED); + } + + public InvalidAuthorizationHeaderException(Exception e) { + super(e, Status.UNAUTHORIZED); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/OptionalAccess.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/OptionalAccess.java new file mode 100644 index 000000000..d6b6346f0 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/OptionalAccess.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; + +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.security.MessageDigest; +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class OptionalAccess { + + public static final String UNIDENTIFIED = "Unidentified-Access-Key"; + + public static void verify(Optional requestAccount, + Optional accessKey, + Optional targetAccount, + String deviceSelector) + { + try { + verify(requestAccount, accessKey, targetAccount); + + if (!deviceSelector.equals("*")) { + long deviceId = Long.parseLong(deviceSelector); + + Optional targetDevice = targetAccount.get().getDevice(deviceId); + + if (targetDevice.isPresent() && targetDevice.get().isEnabled()) { + return; + } + + if (requestAccount.isPresent()) { + throw new NotFoundException(); + } else { + throw new NotAuthorizedException(Response.Status.UNAUTHORIZED); + } + } + } catch (NumberFormatException e) { + throw new WebApplicationException(Response.status(422).build()); + } + } + + public static void verify(Optional requestAccount, + Optional accessKey, + Optional targetAccount) + { + if (requestAccount.isPresent() && targetAccount.isPresent() && targetAccount.get().isEnabled()) { + return; + } + + //noinspection ConstantConditions + if (requestAccount.isPresent() && (targetAccount.isEmpty() || (targetAccount.isPresent() && !targetAccount.get().isEnabled()))) { + throw new NotFoundException(); + } + + if (accessKey.isPresent() && targetAccount.isPresent() && targetAccount.get().isEnabled() && targetAccount.get().isUnrestrictedUnidentifiedAccess()) { + return; + } + + if (accessKey.isPresent() && + targetAccount.isPresent() && + targetAccount.get().getUnidentifiedAccessKey().isPresent() && + targetAccount.get().isEnabled() && + MessageDigest.isEqual(accessKey.get().getAccessKey(), targetAccount.get().getUnidentifiedAccessKey().get())) + { + return; + } + + throw new NotAuthorizedException(Response.Status.UNAUTHORIZED); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/PhoneNumberChangeRefreshRequirementProvider.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/PhoneNumberChangeRefreshRequirementProvider.java new file mode 100644 index 000000000..4ba8db0a5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/PhoneNumberChangeRefreshRequirementProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.util.Pair; + +public class PhoneNumberChangeRefreshRequirementProvider implements WebsocketRefreshRequirementProvider { + + private static final String INITIAL_NUMBER_KEY = + PhoneNumberChangeRefreshRequirementProvider.class.getName() + ".initialNumber"; + + @Override + public void handleRequestFiltered(final RequestEvent requestEvent) { + ContainerRequestUtil.getAuthenticatedAccount(requestEvent.getContainerRequest()) + .ifPresent(account -> requestEvent.getContainerRequest().setProperty(INITIAL_NUMBER_KEY, account.getNumber())); + } + + @Override + public List> handleRequestFinished(final RequestEvent requestEvent) { + final String initialNumber = (String) requestEvent.getContainerRequest().getProperty(INITIAL_NUMBER_KEY); + + if (initialNumber != null) { + final Optional maybeAuthenticatedAccount = + ContainerRequestUtil.getAuthenticatedAccount(requestEvent.getContainerRequest()); + + return maybeAuthenticatedAccount + .filter(account -> !initialNumber.equals(account.getNumber())) + .map(account -> account.getDevices().stream() + .map(device -> new Pair<>(account.getUuid(), device.getId())) + .collect(Collectors.toList())) + .orElse(Collections.emptyList()); + } else { + return Collections.emptyList(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/PhoneVerificationTokenManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/PhoneVerificationTokenManager.java new file mode 100644 index 000000000..d09469306 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/PhoneVerificationTokenManager.java @@ -0,0 +1,113 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import java.security.MessageDigest; +import java.time.Duration; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.ServerErrorException; +import javax.ws.rs.core.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest; +import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; +import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; + +public class PhoneVerificationTokenManager { + + private static final Logger logger = LoggerFactory.getLogger(PhoneVerificationTokenManager.class); + private static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15); + private static final long VERIFICATION_TIMEOUT_SECONDS = REGISTRATION_RPC_TIMEOUT.plusSeconds(1).getSeconds(); + + private final RegistrationServiceClient registrationServiceClient; + private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager; + + public PhoneVerificationTokenManager(final RegistrationServiceClient registrationServiceClient, + final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager) { + this.registrationServiceClient = registrationServiceClient; + this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager; + } + + /** + * Checks if a {@link PhoneVerificationRequest} has a token that verifies the caller has confirmed access to the e164 + * number + * + * @param number the e164 presented for verification + * @param request the request with exactly one verification token (RegistrationService sessionId or registration + * recovery password) + * @return if verification was successful, returns the verification type + * @throws BadRequestException if the number does not match the sessionId’s number, or the remote service rejects + * the session ID as invalid + * @throws NotAuthorizedException if the session is not verified + * @throws ForbiddenException if the recovery password is not valid + * @throws InterruptedException if verification did not complete before a timeout + */ + public PhoneVerificationRequest.VerificationType verify(final String number, final PhoneVerificationRequest request) + throws InterruptedException { + + final PhoneVerificationRequest.VerificationType verificationType = request.verificationType(); + switch (verificationType) { + case SESSION -> verifyBySessionId(number, request.decodeSessionId()); + case RECOVERY_PASSWORD -> verifyByRecoveryPassword(number, request.recoveryPassword()); + } + + return verificationType; + } + + private void verifyBySessionId(final String number, final byte[] sessionId) throws InterruptedException { + try { + final RegistrationServiceSession session = registrationServiceClient + .getSession(sessionId, REGISTRATION_RPC_TIMEOUT) + .get(VERIFICATION_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .orElseThrow(() -> new NotAuthorizedException("session not verified")); + + if (!MessageDigest.isEqual(number.getBytes(), session.number().getBytes())) { + throw new BadRequestException("number does not match session"); + } + if (!session.verified()) { + throw new NotAuthorizedException("session not verified"); + } + } catch (final ExecutionException e) { + + if (e.getCause() instanceof StatusRuntimeException grpcRuntimeException) { + if (grpcRuntimeException.getStatus().getCode() == Status.Code.INVALID_ARGUMENT) { + throw new BadRequestException(); + } + } + + logger.error("Registration service failure", e); + throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE); + + } catch (final CancellationException | TimeoutException e) { + + logger.error("Registration service failure", e); + throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE); + } + } + + private void verifyByRecoveryPassword(final String number, final byte[] recoveryPassword) + throws InterruptedException { + try { + final boolean verified = registrationRecoveryPasswordsManager.verify(number, recoveryPassword) + .get(VERIFICATION_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!verified) { + throw new ForbiddenException("recoveryPassword couldn't be verified"); + } + } catch (final ExecutionException | TimeoutException e) { + throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE); + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/RegistrationLockVerificationManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/RegistrationLockVerificationManager.java new file mode 100644 index 000000000..0f80ca6d6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/RegistrationLockVerificationManager.java @@ -0,0 +1,178 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.common.annotations.VisibleForTesting; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import javax.annotation.Nullable; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest; +import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; +import org.whispersystems.textsecuregcm.push.PushNotificationManager; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; +import org.whispersystems.textsecuregcm.util.Util; + +public class RegistrationLockVerificationManager { + public enum Flow { + REGISTRATION, + CHANGE_NUMBER + } + + @VisibleForTesting + public static final int FAILURE_HTTP_STATUS = 423; + + private static final String EXPIRED_REGISTRATION_LOCK_COUNTER_NAME = + name(RegistrationLockVerificationManager.class, "expiredRegistrationLock"); + private static final String REQUIRED_REGISTRATION_LOCK_COUNTER_NAME = + name(RegistrationLockVerificationManager.class, "requiredRegistrationLock"); + private static final String CHALLENGED_DEVICE_NOT_PUSH_REGISTERED_COUNTER_NAME = + name(RegistrationLockVerificationManager.class, "challengedDeviceNotPushRegistered"); + private static final String ALREADY_LOCKED_TAG_NAME = "alreadyLocked"; + private static final String REGISTRATION_LOCK_VERIFICATION_FLOW_TAG_NAME = "flow"; + private static final String REGISTRATION_LOCK_MATCHES_TAG_NAME = "registrationLockMatches"; + private static final String PHONE_VERIFICATION_TYPE_TAG_NAME = "phoneVerificationType"; + + private final AccountsManager accounts; + private final ClientPresenceManager clientPresenceManager; + private final ExternalServiceCredentialsGenerator svr2CredentialGenerator; + private final RateLimiters rateLimiters; + private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager; + private final PushNotificationManager pushNotificationManager; + + public RegistrationLockVerificationManager( + final AccountsManager accounts, final ClientPresenceManager clientPresenceManager, + final ExternalServiceCredentialsGenerator svr2CredentialGenerator, + final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager, + final PushNotificationManager pushNotificationManager, + final RateLimiters rateLimiters) { + this.accounts = accounts; + this.clientPresenceManager = clientPresenceManager; + this.svr2CredentialGenerator = svr2CredentialGenerator; + this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager; + this.pushNotificationManager = pushNotificationManager; + this.rateLimiters = rateLimiters; + } + + /** + * Verifies the given registration lock credentials against the account’s current registration lock, if any + * + * @param account + * @param clientRegistrationLock + * @throws RateLimitExceededException + * @throws WebApplicationException + */ + public void verifyRegistrationLock(final Account account, @Nullable final String clientRegistrationLock, + final String userAgent, + final Flow flow, + final PhoneVerificationRequest.VerificationType phoneVerificationType + ) throws RateLimitExceededException, WebApplicationException { + + final Tags expiredTags = Tags.of(UserAgentTagUtil.getPlatformTag(userAgent), + Tag.of(REGISTRATION_LOCK_VERIFICATION_FLOW_TAG_NAME, flow.name()), + Tag.of(PHONE_VERIFICATION_TYPE_TAG_NAME, phoneVerificationType.name()) + ); + + final StoredRegistrationLock existingRegistrationLock = account.getRegistrationLock(); + + switch (existingRegistrationLock.getStatus()) { + case EXPIRED: + Metrics.counter(EXPIRED_REGISTRATION_LOCK_COUNTER_NAME, expiredTags).increment(); + return; + case ABSENT: + return; + case REQUIRED: + break; + default: + throw new RuntimeException("Unexpected status: " + existingRegistrationLock.getStatus()); + } + + if (!Util.isEmpty(clientRegistrationLock)) { + rateLimiters.getPinLimiter().validate(account.getNumber()); + } + + final String phoneNumber = account.getNumber(); + final boolean registrationLockMatches = existingRegistrationLock.verify(clientRegistrationLock); + final boolean alreadyLocked = account.hasLockedCredentials(); + + final Tags additionalTags = expiredTags.and( + REGISTRATION_LOCK_MATCHES_TAG_NAME, Boolean.toString(registrationLockMatches), + ALREADY_LOCKED_TAG_NAME, Boolean.toString(alreadyLocked) + ); + + Metrics.counter(REQUIRED_REGISTRATION_LOCK_COUNTER_NAME, additionalTags).increment(); + + final DistributionSummary registrationLockIdleDays = DistributionSummary + .builder(name(RegistrationLockVerificationManager.class, "registrationLockIdleDays")) + .tags(additionalTags) + .publishPercentiles(0.75, 0.95, 0.99, 0.999) + .distributionStatisticExpiry(Duration.ofHours(2)) + .register(Metrics.globalRegistry); + + final Instant accountLastSeen = Instant.ofEpochMilli(account.getLastSeen()); + final Duration timeSinceLastSeen = Duration.between(accountLastSeen, Instant.now()); + + registrationLockIdleDays.record(timeSinceLastSeen.toDays()); + + if (!registrationLockMatches) { + // At this point, the client verified ownership of the phone number but doesn’t have the reglock PIN. + // Freezing the existing account credentials will definitively start the reglock timeout. + // Until the timeout, the current reglock can still be supplied, + // along with phone number verification, to restore access. + final ExternalServiceCredentials existingSvr2Credentials = svr2CredentialGenerator.generateForUuid(account.getUuid()); + + final Account updatedAccount; + if (!alreadyLocked) { + updatedAccount = accounts.update(account, Account::lockAuthTokenHash); + } else { + updatedAccount = account; + } + + // The client often sends an empty registration lock token on the first request + // and sends an actual token if the server returns a 423 indicating that one is required. + // This logic accounts for that behavior by not deleting the registration recovery password + // if the user verified correctly via registration recovery password and sent an empty token. + // This allows users to re-register via registration recovery password + // instead of always being forced to fall back to SMS verification. + if (!phoneVerificationType.equals(PhoneVerificationRequest.VerificationType.RECOVERY_PASSWORD) || clientRegistrationLock != null) { + registrationRecoveryPasswordsManager.removeForNumber(updatedAccount.getNumber()); + } + + final List deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList(); + clientPresenceManager.disconnectAllPresences(updatedAccount.getUuid(), deviceIds); + + try { + // Send a push notification that prompts the client to attempt login and fail due to locked credentials + pushNotificationManager.sendAttemptLoginNotification(updatedAccount, "failedRegistrationLock"); + } catch (final NotPushRegisteredException e) { + Metrics.counter(CHALLENGED_DEVICE_NOT_PUSH_REGISTERED_COUNTER_NAME).increment(); + } + + throw new WebApplicationException(Response.status(FAILURE_HTTP_STATUS) + .entity(new RegistrationLockFailure(existingRegistrationLock.getTimeRemaining().toMillis(), + existingRegistrationLock.needsFailureCredentials() ? existingSvr2Credentials : null)) + .build()); + } + + rateLimiters.getPinLimiter().clear(phoneNumber); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/SaltedTokenHash.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/SaltedTokenHash.java new file mode 100644 index 000000000..8cfecfe7f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/SaltedTokenHash.java @@ -0,0 +1,75 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.auth; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.HexFormat; +import org.signal.libsignal.protocol.kdf.HKDF; + +public record SaltedTokenHash(String hash, String salt) { + + public enum Version { + V1, + V2, + } + + public static final Version CURRENT_VERSION = Version.V2; + + private static final String V2_PREFIX = "2."; + + private static final byte[] AUTH_TOKEN_HKDF_INFO = "authtoken".getBytes(StandardCharsets.UTF_8); + + private static final int SALT_SIZE = 16; + + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + + public static SaltedTokenHash generateFor(final String token) { + final String salt = generateSalt(); + final String hash = calculateV2Hash(salt, token); + return new SaltedTokenHash(hash, salt); + } + + public Version getVersion() { + return hash.startsWith(V2_PREFIX) ? Version.V2 : Version.V1; + } + + public boolean verify(final String token) { + final String theirValue = switch (getVersion()) { + case V1 -> calculateV1Hash(salt, token); + case V2 -> calculateV2Hash(salt, token); + }; + return MessageDigest.isEqual( + theirValue.getBytes(StandardCharsets.UTF_8), + hash.getBytes(StandardCharsets.UTF_8)); + } + + private static String generateSalt() { + final byte[] salt = new byte[SALT_SIZE]; + SECURE_RANDOM.nextBytes(salt); + return HexFormat.of().formatHex(salt); + } + + private static String calculateV1Hash(final String salt, final String token) { + try { + return HexFormat.of() + .formatHex(MessageDigest.getInstance("SHA1").digest((salt + token).getBytes(StandardCharsets.UTF_8))); + } catch (final NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + private static String calculateV2Hash(final String salt, final String token) { + final byte[] secret = HKDF.deriveSecrets( + token.getBytes(StandardCharsets.UTF_8), // key + salt.getBytes(StandardCharsets.UTF_8), // salt + AUTH_TOKEN_HKDF_INFO, + 32); + return V2_PREFIX + HexFormat.of().formatHex(secret); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredRegistrationLock.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredRegistrationLock.java new file mode 100644 index 000000000..276a4d6f7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredRegistrationLock.java @@ -0,0 +1,88 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import com.google.common.annotations.VisibleForTesting; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import javax.annotation.Nullable; +import org.whispersystems.textsecuregcm.util.Util; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class StoredRegistrationLock { + public enum Status { + REQUIRED, + EXPIRED, + ABSENT + } + + @VisibleForTesting + static final Duration REGISTRATION_LOCK_EXPIRATION_DAYS = Duration.ofDays(7); + + private final Optional registrationLock; + + private final Optional registrationLockSalt; + + private final Instant lastSeen; + + /** + * @return milliseconds since the last time the account was seen. + */ + private long timeSinceLastSeen() { + return System.currentTimeMillis() - lastSeen.toEpochMilli(); + } + + /** + * @return true if the registration lock and salt are both set. + */ + private boolean hasLockAndSalt() { + return registrationLock.isPresent() && registrationLockSalt.isPresent(); + } + + public boolean isPresent() { + return hasLockAndSalt(); + } + + public StoredRegistrationLock(Optional registrationLock, Optional registrationLockSalt, Instant lastSeen) { + this.registrationLock = registrationLock; + this.registrationLockSalt = registrationLockSalt; + this.lastSeen = lastSeen; + } + + public Status getStatus() { + if (!isPresent()) { + return Status.ABSENT; + } + if (getTimeRemaining().toMillis() > 0) { + return Status.REQUIRED; + } + return Status.EXPIRED; + } + + public boolean needsFailureCredentials() { + return hasLockAndSalt(); + } + + public Duration getTimeRemaining() { + return REGISTRATION_LOCK_EXPIRATION_DAYS.minus(timeSinceLastSeen(), ChronoUnit.MILLIS); + } + + public boolean verify(@Nullable String clientRegistrationLock) { + if (hasLockAndSalt() && Util.nonEmpty(clientRegistrationLock)) { + SaltedTokenHash credentials = new SaltedTokenHash(registrationLock.get(), registrationLockSalt.get()); + return credentials.verify(clientRegistrationLock); + } else { + return false; + } + } + + @VisibleForTesting + public StoredRegistrationLock forTime(long timestamp) { + return new StoredRegistrationLock(registrationLock, registrationLockSalt, Instant.ofEpochMilli(timestamp)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnToken.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnToken.java new file mode 100644 index 000000000..65682ee43 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnToken.java @@ -0,0 +1,11 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import java.util.List; + +public record TurnToken(String username, String password, List urls) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnTokenGenerator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnTokenGenerator.java new file mode 100644 index 000000000..6f2b378db --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnTokenGenerator.java @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import org.whispersystems.textsecuregcm.configuration.TurnUriConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicTurnConfiguration; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.Util; +import org.whispersystems.textsecuregcm.util.WeightedRandomSelect; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public class TurnTokenGenerator { + + private final DynamicConfigurationManager dynamicConfigurationManager; + + private final byte[] turnSecret; + + private static final String ALGORITHM = "HmacSHA1"; + + public TurnTokenGenerator(final DynamicConfigurationManager dynamicConfigurationManager, + final byte[] turnSecret) { + + this.dynamicConfigurationManager = dynamicConfigurationManager; + this.turnSecret = turnSecret; + } + + public TurnToken generate(final UUID aci) { + try { + final List urls = urls(aci); + final Mac mac = Mac.getInstance(ALGORITHM); + final long validUntilSeconds = Instant.now().plus(Duration.ofDays(1)).getEpochSecond(); + final long user = Util.ensureNonNegativeInt(new SecureRandom().nextInt()); + final String userTime = validUntilSeconds + ":" + user; + + mac.init(new SecretKeySpec(turnSecret, ALGORITHM)); + final String password = Base64.getEncoder().encodeToString(mac.doFinal(userTime.getBytes())); + + return new TurnToken(userTime, password, urls); + } catch (final NoSuchAlgorithmException | InvalidKeyException e) { + throw new AssertionError(e); + } + } + + private List urls(final UUID aci) { + final DynamicTurnConfiguration turnConfig = dynamicConfigurationManager.getConfiguration().getTurnConfiguration(); + + // Check if number is enrolled to test out specific turn servers + final Optional enrolled = turnConfig.getUriConfigs().stream() + .filter(config -> config.getEnrolledAcis().contains(aci)) + .findFirst(); + + if (enrolled.isPresent()) { + return enrolled.get().getUris(); + } + + // Otherwise, select from turn server sets by weighted choice + return WeightedRandomSelect.select(turnConfig + .getUriConfigs() + .stream() + .map(c -> new Pair<>(c.getUris(), c.getWeight())).toList()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/UnidentifiedAccessChecksum.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/UnidentifiedAccessChecksum.java new file mode 100644 index 000000000..3124e2713 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/UnidentifiedAccessChecksum.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +public class UnidentifiedAccessChecksum { + + public static byte[] generateFor(byte[] unidentifiedAccessKey) { + try { + if (unidentifiedAccessKey.length != 16) { + throw new IllegalArgumentException("Invalid UAK length: " + unidentifiedAccessKey.length); + } + + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(unidentifiedAccessKey, "HmacSHA256")); + + return mac.doFinal(new byte[32]); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new AssertionError(e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/UnidentifiedAccessUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/UnidentifiedAccessUtil.java new file mode 100644 index 000000000..32283170a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/UnidentifiedAccessUtil.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import org.whispersystems.textsecuregcm.storage.Account; +import java.security.MessageDigest; + +public class UnidentifiedAccessUtil { + + private UnidentifiedAccessUtil() { + } + + /** + * Checks whether an action (e.g. sending a message or retrieving pre-keys) may be taken on the target account by an + * actor presenting the given unidentified access key. + * + * @param targetAccount the account on which an actor wishes to take an action + * @param unidentifiedAccessKey the unidentified access key presented by the actor + * + * @return {@code true} if an actor presenting the given unidentified access key has permission to take an action on + * the target account or {@code false} otherwise + */ + public static boolean checkUnidentifiedAccess(final Account targetAccount, final byte[] unidentifiedAccessKey) { + return targetAccount.isUnrestrictedUnidentifiedAccess() + || targetAccount.getUnidentifiedAccessKey() + .map(targetUnidentifiedAccessKey -> MessageDigest.isEqual(targetUnidentifiedAccessKey, unidentifiedAccessKey)) + .orElse(false); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshApplicationEventListener.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshApplicationEventListener.java new file mode 100644 index 000000000..ad7ffeb9c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshApplicationEventListener.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.storage.AccountsManager; + +/** + * Delegates request events to a listener that watches for intra-request changes that require websocket refreshes + */ +public class WebsocketRefreshApplicationEventListener implements ApplicationEventListener { + + private final WebsocketRefreshRequestEventListener websocketRefreshRequestEventListener; + + public WebsocketRefreshApplicationEventListener(final AccountsManager accountsManager, + final ClientPresenceManager clientPresenceManager) { + + this.websocketRefreshRequestEventListener = new WebsocketRefreshRequestEventListener(clientPresenceManager, + new AuthEnablementRefreshRequirementProvider(accountsManager), + new PhoneNumberChangeRefreshRequirementProvider()); + } + + @Override + public void onEvent(final ApplicationEvent event) { + } + + @Override + public RequestEventListener onRequest(final RequestEvent requestEvent) { + return websocketRefreshRequestEventListener; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequestEventListener.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequestEventListener.java new file mode 100644 index 000000000..9fbb84fab --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequestEventListener.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Context; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEvent.Type; +import org.glassfish.jersey.server.monitoring.RequestEventListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; + +public class WebsocketRefreshRequestEventListener implements RequestEventListener { + + private final ClientPresenceManager clientPresenceManager; + private final WebsocketRefreshRequirementProvider[] providers; + + private static final Counter DISPLACED_ACCOUNTS = Metrics.counter( + name(WebsocketRefreshRequestEventListener.class, "displacedAccounts")); + + private static final Counter DISPLACED_DEVICES = Metrics.counter( + name(WebsocketRefreshRequestEventListener.class, "displacedDevices")); + + private static final Logger logger = LoggerFactory.getLogger(WebsocketRefreshRequestEventListener.class); + + public WebsocketRefreshRequestEventListener( + final ClientPresenceManager clientPresenceManager, + final WebsocketRefreshRequirementProvider... providers) { + + this.clientPresenceManager = clientPresenceManager; + this.providers = providers; + } + + @Context + private ResourceInfo resourceInfo; + + @Override + public void onEvent(final RequestEvent event) { + if (event.getType() == Type.REQUEST_FILTERED) { + for (final WebsocketRefreshRequirementProvider provider : providers) { + provider.handleRequestFiltered(event); + } + } else if (event.getType() == Type.FINISHED) { + final AtomicInteger displacedDevices = new AtomicInteger(0); + + Arrays.stream(providers) + .flatMap(provider -> provider.handleRequestFinished(event).stream()) + .distinct() + .forEach(pair -> { + try { + displacedDevices.incrementAndGet(); + clientPresenceManager.disconnectPresence(pair.first(), pair.second()); + } catch (final Exception e) { + logger.error("Could not displace device presence", e); + } + }); + + if (displacedDevices.get() > 0) { + DISPLACED_ACCOUNTS.increment(); + DISPLACED_DEVICES.increment(displacedDevices.get()); + } + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequirementProvider.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequirementProvider.java new file mode 100644 index 000000000..2f75127ec --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/WebsocketRefreshRequirementProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import java.util.List; +import java.util.UUID; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.whispersystems.textsecuregcm.util.Pair; + +/** + * A websocket refresh requirement provider watches for intra-request changes (e.g. to authentication status) that + * require a websocket refresh. + */ +public interface WebsocketRefreshRequirementProvider { + + /** + * Processes a request after filters have run and the request has been mapped to a destination controller. + * + * @param requestEvent the request event to observe + */ + void handleRequestFiltered(RequestEvent requestEvent); + + /** + * Processes a request after all normal request handling has been completed. + * + * @param requestEvent the request event to observe + * @return a list of pairs of account UUID/device ID pairs identifying websockets that need to be refreshed as a + * result of the observed request + */ + List> handleRequestFinished(RequestEvent requestEvent); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/grpc/AuthenticatedDevice.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/grpc/AuthenticatedDevice.java new file mode 100644 index 000000000..906056986 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/grpc/AuthenticatedDevice.java @@ -0,0 +1,11 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth.grpc; + +import java.util.UUID; + +public record AuthenticatedDevice(UUID accountIdentifier, long deviceId) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/grpc/AuthenticationUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/grpc/AuthenticationUtil.java new file mode 100644 index 000000000..424b1ab2e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/grpc/AuthenticationUtil.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth.grpc; + +import io.grpc.Context; +import io.grpc.Status; +import java.util.UUID; +import javax.annotation.Nullable; +import org.whispersystems.textsecuregcm.storage.Device; + +/** + * Provides utility methods for working with authentication in the context of gRPC calls. + */ +public class AuthenticationUtil { + + static final Context.Key CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY = Context.key("authenticated-aci"); + static final Context.Key CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY = Context.key("authenticated-device-id"); + + /** + * Returns the account/device authenticated in the current gRPC context or throws an "unauthenticated" exception if + * no authenticated account/device is available. + * + * @return the account/device identifier authenticated in the current gRPC context + * + * @throws io.grpc.StatusRuntimeException with a status of {@code UNAUTHENTICATED} if no authenticated account/device + * could be retrieved from the current gRPC context + */ + public static AuthenticatedDevice requireAuthenticatedDevice() { + @Nullable final UUID accountIdentifier = CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY.get(); + @Nullable final Long deviceId = CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY.get(); + + if (accountIdentifier != null && deviceId != null) { + return new AuthenticatedDevice(accountIdentifier, deviceId); + } + + throw Status.UNAUTHENTICATED.asRuntimeException(); + } + + /** + * Returns the account/device authenticated in the current gRPC context or throws an "unauthenticated" exception if + * no authenticated account/device is available or "permission denied" if the authenticated device is not the primary + * device for the account. + * + * @return the account/device identifier authenticated in the current gRPC context + * + * @throws io.grpc.StatusRuntimeException with a status of {@code UNAUTHENTICATED} if no authenticated account/device + * could be retrieved from the current gRPC context or a status of {@code PERMISSION_DENIED} if the authenticated + * device is not the primary device for the authenticated account + */ + public static AuthenticatedDevice requireAuthenticatedPrimaryDevice() { + final AuthenticatedDevice authenticatedDevice = requireAuthenticatedDevice(); + + if (authenticatedDevice.deviceId() != Device.MASTER_ID) { + throw Status.PERMISSION_DENIED.asRuntimeException(); + } + + return authenticatedDevice; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/grpc/BasicCredentialAuthenticationInterceptor.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/grpc/BasicCredentialAuthenticationInterceptor.java new file mode 100644 index 000000000..95b66b18a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/auth/grpc/BasicCredentialAuthenticationInterceptor.java @@ -0,0 +1,85 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth.grpc; + +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.auth.basic.BasicCredentials; +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator; +import org.whispersystems.textsecuregcm.util.HeaderUtils; + +/** + * A basic credential authentication interceptor enforces the presence of a valid username and password on every call. + * Callers supply credentials by providing a username (UUID and optional device ID) and password pair in the + * {@code x-signal-basic-auth-credentials} call header. + *

    + * Downstream services can retrieve the identity of the authenticated caller using methods in + * {@link AuthenticationUtil}. + *

    + * Note that this authentication, while fully functional, is intended only for development and testing purposes and is + * intended to be replaced with a more robust and efficient strategy before widespread client adoption. + * + * @see AuthenticationUtil + * @see BaseAccountAuthenticator + */ +public class BasicCredentialAuthenticationInterceptor implements ServerInterceptor { + + private final BaseAccountAuthenticator baseAccountAuthenticator; + + @VisibleForTesting + static final Metadata.Key BASIC_CREDENTIALS = + Metadata.Key.of("x-signal-auth", Metadata.ASCII_STRING_MARSHALLER); + + private static final Metadata EMPTY_TRAILERS = new Metadata(); + + public BasicCredentialAuthenticationInterceptor(final BaseAccountAuthenticator baseAccountAuthenticator) { + this.baseAccountAuthenticator = baseAccountAuthenticator; + } + + @Override + public ServerCall.Listener interceptCall( + final ServerCall call, + final Metadata headers, + final ServerCallHandler next) { + + final String authHeader = headers.get(BASIC_CREDENTIALS); + + if (StringUtils.isNotBlank(authHeader)) { + final Optional maybeCredentials = HeaderUtils.basicCredentialsFromAuthHeader(authHeader); + if (maybeCredentials.isEmpty()) { + call.close(Status.UNAUTHENTICATED.withDescription("Could not parse credentials"), EMPTY_TRAILERS); + } else { + final Optional maybeAuthenticatedAccount = + baseAccountAuthenticator.authenticate(maybeCredentials.get(), false); + + if (maybeAuthenticatedAccount.isPresent()) { + final AuthenticatedAccount authenticatedAccount = maybeAuthenticatedAccount.get(); + + final Context context = Context.current() + .withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY, authenticatedAccount.getAccount().getUuid()) + .withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY, authenticatedAccount.getAuthenticatedDevice().getId()); + + return Contexts.interceptCall(context, call, headers, next); + } else { + call.close(Status.UNAUTHENTICATED.withDescription("Credentials not accepted"), EMPTY_TRAILERS); + } + } + } else { + call.close(Status.UNAUTHENTICATED.withDescription("No credentials provided"), EMPTY_TRAILERS); + } + + return new ServerCall.Listener<>() {}; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/BadgeTranslator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/BadgeTranslator.java new file mode 100644 index 000000000..0e5d7cf6a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/BadgeTranslator.java @@ -0,0 +1,14 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.badges; + +import java.util.List; +import java.util.Locale; +import org.whispersystems.textsecuregcm.entities.Badge; + +public interface BadgeTranslator { + Badge translate(List acceptableLanguages, String badgeId); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverter.java new file mode 100644 index 000000000..40a9f2dcc --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverter.java @@ -0,0 +1,131 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.badges; + +import com.google.common.annotations.VisibleForTesting; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.signal.i18n.HeaderControlledResourceBundleLookup; +import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; +import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; +import org.whispersystems.textsecuregcm.entities.Badge; +import org.whispersystems.textsecuregcm.entities.BadgeSvg; +import org.whispersystems.textsecuregcm.entities.SelfBadge; +import org.whispersystems.textsecuregcm.storage.AccountBadge; + +public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter, BadgeTranslator { + + @VisibleForTesting + static final String BASE_NAME = "org.signal.badges.Badges"; + + private final Clock clock; + private final Map knownBadges; + private final List badgeIdsEnabledForAll; + private final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup; + + public ConfiguredProfileBadgeConverter( + final Clock clock, + final BadgesConfiguration badgesConfiguration, + final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup) { + this.clock = clock; + this.knownBadges = badgesConfiguration.getBadges().stream() + .collect(Collectors.toMap(BadgeConfiguration::getId, Function.identity())); + this.badgeIdsEnabledForAll = badgesConfiguration.getBadgeIdsEnabledForAll(); + this.headerControlledResourceBundleLookup = headerControlledResourceBundleLookup; + } + + @Override + public Badge translate(final List acceptableLanguages, final String badgeId) { + final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME, + acceptableLanguages); + final BadgeConfiguration configuration = knownBadges.get(badgeId); + return newBadge( + false, + configuration.getId(), + configuration.getCategory(), + resourceBundle.getString(configuration.getId() + "_name"), + resourceBundle.getString(configuration.getId() + "_description"), + configuration.getSprites(), + configuration.getSvg(), + configuration.getSvgs(), + null, + false); + } + + @Override + public List convert( + final List acceptableLanguages, + final List accountBadges, + final boolean isSelf) { + if (accountBadges.isEmpty() && badgeIdsEnabledForAll.isEmpty()) { + return List.of(); + } + + final Instant now = clock.instant(); + final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME, + acceptableLanguages); + List badges = accountBadges.stream() + .filter(accountBadge -> (isSelf || accountBadge.isVisible()) + && now.isBefore(accountBadge.getExpiration()) + && knownBadges.containsKey(accountBadge.getId())) + .map(accountBadge -> { + BadgeConfiguration configuration = knownBadges.get(accountBadge.getId()); + return newBadge( + isSelf, + accountBadge.getId(), + configuration.getCategory(), + resourceBundle.getString(accountBadge.getId() + "_name"), + resourceBundle.getString(accountBadge.getId() + "_description"), + configuration.getSprites(), + configuration.getSvg(), + configuration.getSvgs(), + accountBadge.getExpiration(), + accountBadge.isVisible()); + }) + .collect(Collectors.toCollection(ArrayList::new)); + badges.addAll(badgeIdsEnabledForAll.stream().filter(knownBadges::containsKey).map(id -> { + BadgeConfiguration configuration = knownBadges.get(id); + return newBadge( + isSelf, + id, + configuration.getCategory(), + resourceBundle.getString(id + "_name"), + resourceBundle.getString(id + "_description"), + configuration.getSprites(), + configuration.getSvg(), + configuration.getSvgs(), + now.plus(Duration.ofDays(1)), + true); + }).collect(Collectors.toList())); + return badges; + } + + private Badge newBadge( + final boolean isSelf, + final String id, + final String category, + final String name, + final String description, + final List sprites, + final String svg, + final List svgs, + final Instant expiration, + final boolean visible) { + if (isSelf) { + return new SelfBadge(id, category, name, description, sprites, svg, svgs, expiration, visible); + } else { + return new Badge(id, category, name, description, sprites, svg, svgs); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/LevelTranslator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/LevelTranslator.java new file mode 100644 index 000000000..a9529b88d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/LevelTranslator.java @@ -0,0 +1,13 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.badges; + +import java.util.List; +import java.util.Locale; + +public interface LevelTranslator { + String translate(List acceptableLanguages, String badgeId); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/ProfileBadgeConverter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/ProfileBadgeConverter.java new file mode 100644 index 000000000..b36f8946f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/ProfileBadgeConverter.java @@ -0,0 +1,20 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.badges; + +import java.util.List; +import java.util.Locale; +import org.whispersystems.textsecuregcm.entities.Badge; +import org.whispersystems.textsecuregcm.storage.AccountBadge; + +public interface ProfileBadgeConverter { + + /** + * Converts the {@link AccountBadge}s for an account into the objects + * that can be returned on a profile fetch. + */ + List convert(List acceptableLanguages, List accountBadges, boolean isSelf); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/ResourceBundleLevelTranslator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/ResourceBundleLevelTranslator.java new file mode 100644 index 000000000..807a1d537 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/badges/ResourceBundleLevelTranslator.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.badges; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.ResourceBundle; +import javax.annotation.Nonnull; +import org.signal.i18n.HeaderControlledResourceBundleLookup; + +public class ResourceBundleLevelTranslator implements LevelTranslator { + + private static final String BASE_NAME = "org.signal.subscriptions.Subscriptions"; + + private final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup; + + public ResourceBundleLevelTranslator( + @Nonnull final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup) { + this.headerControlledResourceBundleLookup = Objects.requireNonNull(headerControlledResourceBundleLookup); + } + + @Override + public String translate(final List acceptableLanguages, final String badgeId) { + final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME, + acceptableLanguages); + return resourceBundle.getString(badgeId); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/Action.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/Action.java new file mode 100644 index 000000000..26f4889e6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/Action.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.captcha; + +import com.fasterxml.jackson.annotation.JsonCreator; +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +public enum Action { + CHALLENGE("challenge"), + REGISTRATION("registration"); + + private final String actionName; + + Action(String actionName) { + this.actionName = actionName; + } + + public String getActionName() { + return actionName; + } + + private static final Map ENUM_MAP = Arrays + .stream(Action.values()) + .collect(Collectors.toMap( + a -> a.actionName, + Function.identity())); + @JsonCreator + public static Action fromString(String key) { + return ENUM_MAP.get(key.toLowerCase(Locale.ROOT).strip()); + } + + static Optional parse(final String action) { + return Optional.ofNullable(fromString(action)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/AssessmentResult.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/AssessmentResult.java new file mode 100644 index 000000000..0db5d0753 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/AssessmentResult.java @@ -0,0 +1,115 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.captcha; + +import java.util.Objects; +import java.util.Optional; + +public class AssessmentResult { + + private final boolean solved; + private final float actualScore; + private final float defaultScoreThreshold; + private final String scoreString; + + /** + * A captcha assessment + * + * @param solved if false, the captcha was not successfully completed + * @param actualScore float representation of the risk level from [0, 1.0], with 1.0 being the least risky + * @param defaultScoreThreshold the score threshold which the score will be evaluated against by default + * @param scoreString a quantized string representation of the risk level, suitable for use in metrics + */ + private AssessmentResult(boolean solved, float actualScore, float defaultScoreThreshold, final String scoreString) { + this.solved = solved; + this.actualScore = actualScore; + this.defaultScoreThreshold = defaultScoreThreshold; + this.scoreString = scoreString; + } + + /** + * Construct an {@link AssessmentResult} from a captcha evaluation score + * + * @param actualScore the score + * @param defaultScoreThreshold the threshold to compare the score against by default + */ + public static AssessmentResult fromScore(float actualScore, float defaultScoreThreshold) { + if (actualScore < 0 || actualScore > 1.0 || defaultScoreThreshold < 0 || defaultScoreThreshold > 1.0) { + throw new IllegalArgumentException("invalid captcha score"); + } + return new AssessmentResult(true, actualScore, defaultScoreThreshold, AssessmentResult.scoreString(actualScore)); + } + + /** + * Construct a captcha assessment that will always be invalid + */ + public static AssessmentResult invalid() { + return new AssessmentResult(false, 0.0f, 0.0f, ""); + } + + /** + * Construct a captcha assessment that will always be valid + */ + public static AssessmentResult alwaysValid() { + return new AssessmentResult(true, 1.0f, 0.0f, "1.0"); + } + + /** + * Check if the captcha assessment should be accepted using the default score threshold + * + * @return true if this assessment should be accepted under the default score threshold + */ + public boolean isValid() { + return isValid(Optional.empty()); + } + + /** + * Check if the captcha assessment should be accepted + * + * @param scoreThreshold the minimum score the assessment requires to pass, uses default if empty + * @return true if the assessment scored higher than the provided scoreThreshold + */ + public boolean isValid(Optional scoreThreshold) { + if (!solved) { + return false; + } + return this.actualScore >= scoreThreshold.orElse(this.defaultScoreThreshold); + } + + public String getScoreString() { + return scoreString; + } + + public float getScore() { + return this.actualScore; + } + + + /** + * Map a captcha score in [0.0, 1.0] to a low cardinality discrete space in [0, 100] suitable for use in metrics + */ + private static String scoreString(final float score) { + final int x = Math.round(score * 10); // [0, 10] + return Integer.toString(x * 10); // [0, 100] in increments of 10 + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + AssessmentResult that = (AssessmentResult) o; + return solved == that.solved && Float.compare(that.actualScore, actualScore) == 0 + && Float.compare(that.defaultScoreThreshold, defaultScoreThreshold) == 0 && Objects.equals(scoreString, + that.scoreString); + } + + @Override + public int hashCode() { + return Objects.hash(solved, actualScore, defaultScoreThreshold, scoreString); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaChecker.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaChecker.java new file mode 100644 index 000000000..0d7bed762 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaChecker.java @@ -0,0 +1,113 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.captcha; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.common.annotations.VisibleForTesting; +import io.micrometer.core.instrument.Metrics; +import java.io.IOException; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.ws.rs.BadRequestException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CaptchaChecker { + private static final Logger logger = LoggerFactory.getLogger(CaptchaChecker.class); + private static final String INVALID_SITEKEY_COUNTER_NAME = name(CaptchaChecker.class, "invalidSiteKey"); + private static final String ASSESSMENTS_COUNTER_NAME = name(RecaptchaClient.class, "assessments"); + private static final String INVALID_ACTION_COUNTER_NAME = name(CaptchaChecker.class, "invalidActions"); + + @VisibleForTesting + static final String SEPARATOR = "."; + + private static final String SHORT_SUFFIX = "-short"; + + private final ShortCodeExpander shortCodeExpander; + private final Map captchaClientMap; + + public CaptchaChecker( + final ShortCodeExpander shortCodeRetriever, + final List captchaClients) { + this.shortCodeExpander = shortCodeRetriever; + this.captchaClientMap = captchaClients.stream() + .collect(Collectors.toMap(CaptchaClient::scheme, Function.identity())); + } + + + /** + * Check if a solved captcha should be accepted + * + * @param expectedAction the {@link Action} for which this captcha solution is intended + * @param input expected to contain a prefix indicating the captcha scheme, sitekey, token, and action. The + * expected format is {@code version-prefix.sitekey.action.token} + * @param ip IP of the solver + * @return An {@link AssessmentResult} indicating whether the solution should be accepted, and a score that can be + * used for metrics + * @throws IOException if there is an error validating the captcha with the underlying service + * @throws BadRequestException if input is not in the expected format + */ + public AssessmentResult verify( + final Action expectedAction, + final String input, + final String ip) throws IOException { + final String[] parts = input.split("\\" + SEPARATOR, 4); + + // we allow missing actions, if we're missing 1 part, assume it's the action + if (parts.length < 4) { + throw new BadRequestException("too few parts"); + } + + final String prefix = parts[0]; + final String siteKey = parts[1].toLowerCase(Locale.ROOT).strip(); + final String action = parts[2]; + String token = parts[3]; + + String provider = prefix; + if (prefix.endsWith(SHORT_SUFFIX)) { + // This is a "short" solution that points to the actual solution. We need to fetch the + // full solution before proceeding + provider = prefix.substring(0, prefix.length() - SHORT_SUFFIX.length()); + token = shortCodeExpander.retrieve(token).orElseThrow(() -> new BadRequestException("invalid shortcode")); + } + + final CaptchaClient client = this.captchaClientMap.get(provider); + if (client == null) { + throw new BadRequestException("invalid captcha scheme"); + } + + final Action parsedAction = Action.parse(action) + .orElseThrow(() -> { + Metrics.counter(INVALID_ACTION_COUNTER_NAME, "action", action).increment(); + throw new BadRequestException("invalid captcha action"); + }); + + if (!parsedAction.equals(expectedAction)) { + Metrics.counter(INVALID_ACTION_COUNTER_NAME, "action", action).increment(); + throw new BadRequestException("invalid captcha action"); + } + + final Set allowedSiteKeys = client.validSiteKeys(parsedAction); + if (!allowedSiteKeys.contains(siteKey)) { + logger.debug("invalid site-key {}, action={}, token={}", siteKey, action, token); + Metrics.counter(INVALID_SITEKEY_COUNTER_NAME, "action", action).increment(); + throw new BadRequestException("invalid captcha site-key"); + } + + final AssessmentResult result = client.verify(siteKey, parsedAction, token, ip); + Metrics.counter(ASSESSMENTS_COUNTER_NAME, + "action", action, + "score", result.getScoreString(), + "provider", provider) + .increment(); + return result; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaClient.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaClient.java new file mode 100644 index 000000000..f22a52ab1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/CaptchaClient.java @@ -0,0 +1,41 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.captcha; + +import java.io.IOException; +import java.util.Optional; +import java.util.Set; + +public interface CaptchaClient { + + + /** + * @return the identifying captcha scheme that this CaptchaClient handles + */ + String scheme(); + + /** + * @param action the action to retrieve site keys for + * @return siteKeys this client is willing to accept + */ + Set validSiteKeys(final Action action); + + /** + * Verify a provided captcha solution + * + * @param siteKey identifying string for the captcha service + * @param action an action indicating the purpose of the captcha + * @param token the captcha solution that will be verified + * @param ip the ip of the captcha solver + * @return An {@link AssessmentResult} indicating whether the solution should be accepted + * @throws IOException if the underlying captcha provider returns an error + */ + AssessmentResult verify( + final String siteKey, + final Action action, + final String token, + final String ip) throws IOException; +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClient.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClient.java new file mode 100644 index 000000000..b78fa60ad --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClient.java @@ -0,0 +1,160 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.captcha; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.common.annotations.VisibleForTesting; +import io.micrometer.core.instrument.Metrics; +import java.io.IOException; +import java.math.BigDecimal; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import javax.ws.rs.core.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; +import org.whispersystems.textsecuregcm.configuration.RetryConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.util.ExceptionUtils; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +public class HCaptchaClient implements CaptchaClient { + + private static final Logger logger = LoggerFactory.getLogger(HCaptchaClient.class); + private static final String PREFIX = "signal-hcaptcha"; + private static final String ASSESSMENT_REASON_COUNTER_NAME = name(HCaptchaClient.class, "assessmentReason"); + private static final String INVALID_REASON_COUNTER_NAME = name(HCaptchaClient.class, "invalidReason"); + private final String apiKey; + private final FaultTolerantHttpClient client; + private final DynamicConfigurationManager dynamicConfigurationManager; + + @VisibleForTesting + HCaptchaClient(final String apiKey, + final FaultTolerantHttpClient faultTolerantHttpClient, + final DynamicConfigurationManager dynamicConfigurationManager) { + this.apiKey = apiKey; + this.client = faultTolerantHttpClient; + this.dynamicConfigurationManager = dynamicConfigurationManager; + } + + public HCaptchaClient( + final String apiKey, + final ScheduledExecutorService retryExecutor, + final CircuitBreakerConfiguration circuitBreakerConfiguration, + final RetryConfiguration retryConfiguration, + final DynamicConfigurationManager dynamicConfigurationManager) { + this(apiKey, + FaultTolerantHttpClient.newBuilder() + .withName("hcaptcha") + .withCircuitBreaker(circuitBreakerConfiguration) + .withExecutor(Executors.newCachedThreadPool()) + .withRetryExecutor(retryExecutor) + .withRetry(retryConfiguration) + .withRetryOnException(ex -> ex instanceof IOException) + .withConnectTimeout(Duration.ofSeconds(10)) + .withVersion(HttpClient.Version.HTTP_2) + .build(), + dynamicConfigurationManager); + } + + @Override + public String scheme() { + return PREFIX; + } + + @Override + public Set validSiteKeys(final Action action) { + final DynamicCaptchaConfiguration config = dynamicConfigurationManager.getConfiguration().getCaptchaConfiguration(); + if (!config.isAllowHCaptcha()) { + logger.warn("Received request to verify an hCaptcha, but hCaptcha is not enabled"); + return Collections.emptySet(); + } + return Optional + .ofNullable(config.getHCaptchaSiteKeys().get(action)) + .orElse(Collections.emptySet()); + } + + @Override + public AssessmentResult verify( + final String siteKey, + final Action action, + final String token, + final String ip) + throws IOException { + + final DynamicCaptchaConfiguration config = dynamicConfigurationManager.getConfiguration().getCaptchaConfiguration(); + final String body = String.format("response=%s&secret=%s&remoteip=%s", + URLEncoder.encode(token, StandardCharsets.UTF_8), + URLEncoder.encode(this.apiKey, StandardCharsets.UTF_8), + ip); + final HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://hcaptcha.com/siteverify")) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + final HttpResponse response; + try { + response = this.client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join(); + } catch (CompletionException e) { + logger.warn("failed to make http request to hCaptcha: {}", e.getMessage()); + throw new IOException(ExceptionUtils.unwrap(e)); + } + + if (response.statusCode() != Response.Status.OK.getStatusCode()) { + logger.warn("failure submitting token to hCaptcha (code={}): {}", response.statusCode(), response); + throw new IOException("hCaptcha http failure : " + response.statusCode()); + } + + final HCaptchaResponse hCaptchaResponse = SystemMapper.jsonMapper() + .readValue(response.body(), HCaptchaResponse.class); + + logger.debug("received hCaptcha response: {}", hCaptchaResponse); + + if (!hCaptchaResponse.success) { + for (String errorCode : hCaptchaResponse.errorCodes) { + Metrics.counter(INVALID_REASON_COUNTER_NAME, + "action", action.getActionName(), + "reason", errorCode).increment(); + } + return AssessmentResult.invalid(); + } + + // hcaptcha uses the inverse scheme of recaptcha (for hcaptcha, a low score is less risky) + final float score = 1.0f - hCaptchaResponse.score; + if (score < 0.0f || score > 1.0f) { + logger.error("Invalid score {} from hcaptcha response {}", hCaptchaResponse.score, hCaptchaResponse); + return AssessmentResult.invalid(); + } + final BigDecimal threshold = config.getScoreFloorByAction().getOrDefault(action, config.getScoreFloor()); + final AssessmentResult assessmentResult = AssessmentResult.fromScore(score, threshold.floatValue()); + + for (String reason : hCaptchaResponse.scoreReasons) { + Metrics.counter(ASSESSMENT_REASON_COUNTER_NAME, + "action", action.getActionName(), + "reason", reason, + "score", assessmentResult.getScoreString()).increment(); + } + return assessmentResult; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaResponse.java new file mode 100644 index 000000000..39db8c755 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/HCaptchaResponse.java @@ -0,0 +1,57 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.captcha; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +/** + * Verify response returned by hcaptcha + *

    + * see ... + */ +public class HCaptchaResponse { + + @JsonProperty + boolean success; + + @JsonProperty(value = "challenge-ts") + Duration challengeTs; + + @JsonProperty + String hostname; + + @JsonProperty + boolean credit; + + @JsonProperty(value = "error-codes") + List errorCodes = Collections.emptyList(); + + @JsonProperty + float score; + + @JsonProperty(value = "score-reasons") + List scoreReasons = Collections.emptyList(); + + public HCaptchaResponse() { + } + + @Override + public String toString() { + return "HCaptchaResponse{" + + "success=" + success + + ", challengeTs=" + challengeTs + + ", hostname='" + hostname + '\'' + + ", credit=" + credit + + ", errorCodes=" + errorCodes + + ", score=" + score + + ", scoreReasons=" + scoreReasons + + '}'; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/RecaptchaClient.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/RecaptchaClient.java new file mode 100644 index 000000000..216901365 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/RecaptchaClient.java @@ -0,0 +1,134 @@ +/* + * Copyright 2021-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.captcha; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.api.gax.rpc.ApiException; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.recaptchaenterprise.v1.RecaptchaEnterpriseServiceClient; +import com.google.cloud.recaptchaenterprise.v1.RecaptchaEnterpriseServiceSettings; +import com.google.recaptchaenterprise.v1.Assessment; +import com.google.recaptchaenterprise.v1.Event; +import com.google.recaptchaenterprise.v1.RiskAnalysis; +import io.micrometer.core.instrument.Metrics; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; + +public class RecaptchaClient implements CaptchaClient { + + private static final Logger log = LoggerFactory.getLogger(RecaptchaClient.class); + + private static final String V2_PREFIX = "signal-recaptcha-v2"; + private static final String INVALID_REASON_COUNTER_NAME = name(RecaptchaClient.class, "invalidReason"); + private static final String INVALID_SITEKEY_COUNTER_NAME = name(RecaptchaClient.class, "invalidSiteKey"); + private static final String ASSESSMENT_REASON_COUNTER_NAME = name(RecaptchaClient.class, "assessmentReason"); + + + private final String projectPath; + private final RecaptchaEnterpriseServiceClient client; + private final DynamicConfigurationManager dynamicConfigurationManager; + + public RecaptchaClient( + @Nonnull final String projectPath, + @Nonnull final String recaptchaCredentialConfigurationJson, + final DynamicConfigurationManager dynamicConfigurationManager) { + try { + this.projectPath = Objects.requireNonNull(projectPath); + this.client = RecaptchaEnterpriseServiceClient.create(RecaptchaEnterpriseServiceSettings.newBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(GoogleCredentials.fromStream( + new ByteArrayInputStream(recaptchaCredentialConfigurationJson.getBytes(StandardCharsets.UTF_8))))) + .build()); + + this.dynamicConfigurationManager = dynamicConfigurationManager; + } catch (IOException e) { + throw new AssertionError(e); + } + } + + @Override + public String scheme() { + return V2_PREFIX; + } + + @Override + public Set validSiteKeys(final Action action) { + final DynamicCaptchaConfiguration config = dynamicConfigurationManager.getConfiguration().getCaptchaConfiguration(); + if (!config.isAllowRecaptcha()) { + log.warn("Received request to verify a recaptcha, but recaptcha is not enabled"); + return Collections.emptySet(); + } + return Optional + .ofNullable(config.getRecaptchaSiteKeys().get(action)) + .orElse(Collections.emptySet()); + } + + @Override + public org.whispersystems.textsecuregcm.captcha.AssessmentResult verify( + final String sitekey, + final Action action, + final String token, + final String ip) throws IOException { + final DynamicCaptchaConfiguration config = dynamicConfigurationManager.getConfiguration().getCaptchaConfiguration(); + final Set allowedSiteKeys = config.getRecaptchaSiteKeys().get(action); + if (allowedSiteKeys != null && !allowedSiteKeys.contains(sitekey)) { + log.info("invalid recaptcha sitekey {}, action={}, token={}", action, token); + Metrics.counter(INVALID_SITEKEY_COUNTER_NAME, "action", action.getActionName()).increment(); + return AssessmentResult.invalid(); + } + + Event.Builder eventBuilder = Event.newBuilder() + .setSiteKey(sitekey) + .setToken(token) + .setUserIpAddress(ip); + + if (action != null) { + eventBuilder.setExpectedAction(action.getActionName()); + } + + final Event event = eventBuilder.build(); + final Assessment assessment; + try { + assessment = client.createAssessment(projectPath, Assessment.newBuilder().setEvent(event).build()); + } catch (ApiException e) { + throw new IOException(e); + } + + if (assessment.getTokenProperties().getValid()) { + final float score = assessment.getRiskAnalysis().getScore(); + log.debug("assessment for {} was valid, score: {}", action.getActionName(), score); + final BigDecimal threshold = config.getScoreFloorByAction().getOrDefault(action, config.getScoreFloor()); + final AssessmentResult assessmentResult = AssessmentResult.fromScore(score, threshold.floatValue()); + for (RiskAnalysis.ClassificationReason reason : assessment.getRiskAnalysis().getReasonsList()) { + Metrics.counter(ASSESSMENT_REASON_COUNTER_NAME, + "action", action.getActionName(), + "score", assessmentResult.getScoreString(), + "reason", reason.name()) + .increment(); + } + return assessmentResult; + } else { + Metrics.counter(INVALID_REASON_COUNTER_NAME, + "action", action.getActionName(), + "reason", assessment.getTokenProperties().getInvalidReason().name()) + .increment(); + return AssessmentResult.invalid(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/RegistrationCaptchaManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/RegistrationCaptchaManager.java new file mode 100644 index 000000000..3eedcd193 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/RegistrationCaptchaManager.java @@ -0,0 +1,61 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.captcha; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.SharedMetricRegistries; +import java.io.IOException; +import java.util.Optional; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.controllers.AccountController; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.util.Constants; +import org.whispersystems.textsecuregcm.util.Util; + +public class RegistrationCaptchaManager { + + private static final Logger logger = LoggerFactory.getLogger(RegistrationCaptchaManager.class); + + private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); + private final Meter countryFilteredHostMeter = metricRegistry.meter( + name(AccountController.class, "country_limited_host")); + private final Meter rateLimitedHostMeter = metricRegistry.meter(name(AccountController.class, "rate_limited_host")); + private final Meter rateLimitedPrefixMeter = metricRegistry.meter( + name(AccountController.class, "rate_limited_prefix")); + + private final CaptchaChecker captchaChecker; + private final RateLimiters rateLimiters; + private final Set testDevices; + private final DynamicConfigurationManager dynamicConfigurationManager; + + + public RegistrationCaptchaManager(final CaptchaChecker captchaChecker, final RateLimiters rateLimiters, + final Set testDevices, + final DynamicConfigurationManager dynamicConfigurationManager) { + this.captchaChecker = captchaChecker; + this.rateLimiters = rateLimiters; + this.testDevices = testDevices; + this.dynamicConfigurationManager = dynamicConfigurationManager; + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public Optional assessCaptcha(final Optional captcha, final String sourceHost) + throws IOException { + return captcha.isPresent() + ? Optional.of(captchaChecker.verify(Action.REGISTRATION, captcha.get(), sourceHost)) + : Optional.empty(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/ShortCodeExpander.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/ShortCodeExpander.java new file mode 100644 index 000000000..546b6b3eb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/captcha/ShortCodeExpander.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.captcha; + +import io.micrometer.core.instrument.Metrics; +import org.apache.http.HttpStatus; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Optional; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +public class ShortCodeExpander { + private static final String EXPAND_COUNTER_NAME = name(ShortCodeExpander.class, "expand"); + + private final HttpClient client; + private final URI shortenerHost; + + public ShortCodeExpander(final HttpClient client, final String shortenerHost) { + this.client = client; + this.shortenerHost = URI.create(shortenerHost); + } + + public Optional retrieve(final String shortCode) throws IOException { + final URI uri = shortenerHost.resolve(shortCode); + final HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build(); + + try { + final HttpResponse response = this.client.send(request, HttpResponse.BodyHandlers.ofString()); + Metrics.counter(EXPAND_COUNTER_NAME, "responseCode", Integer.toString(response.statusCode())).increment(); + return switch (response.statusCode()) { + case HttpStatus.SC_OK -> Optional.of(response.body()); + case HttpStatus.SC_NOT_FOUND -> Optional.empty(); + default -> throw new IOException("Failed to look up shortcode"); + }; + } catch (InterruptedException e) { + throw new IOException(e); + } + } + + + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AccountDatabaseCrawlerConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AccountDatabaseCrawlerConfiguration.java new file mode 100644 index 000000000..e920bc9fc --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AccountDatabaseCrawlerConfiguration.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class AccountDatabaseCrawlerConfiguration { + + @JsonProperty + private int chunkSize = 1000; + + public int getChunkSize() { + return chunkSize; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AccountsTableConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AccountsTableConfiguration.java new file mode 100644 index 000000000..21eee6c8d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AccountsTableConfiguration.java @@ -0,0 +1,49 @@ +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.whispersystems.textsecuregcm.configuration.DynamoDbTables.Table; +import javax.validation.constraints.NotBlank; + +public class AccountsTableConfiguration extends Table { + + private final String phoneNumberTableName; + private final String phoneNumberIdentifierTableName; + private final String usernamesTableName; + private final int scanPageSize; + + @JsonCreator + public AccountsTableConfiguration( + @JsonProperty("tableName") final String tableName, + @JsonProperty("phoneNumberTableName") final String phoneNumberTableName, + @JsonProperty("phoneNumberIdentifierTableName") final String phoneNumberIdentifierTableName, + @JsonProperty("usernamesTableName") final String usernamesTableName, + @JsonProperty("scanPageSize") final int scanPageSize) { + + super(tableName); + + this.phoneNumberTableName = phoneNumberTableName; + this.phoneNumberIdentifierTableName = phoneNumberIdentifierTableName; + this.usernamesTableName = usernamesTableName; + this.scanPageSize = scanPageSize; + } + + @NotBlank + public String getPhoneNumberTableName() { + return phoneNumberTableName; + } + + @NotBlank + public String getPhoneNumberIdentifierTableName() { + return phoneNumberIdentifierTableName; + } + + @NotBlank + public String getUsernamesTableName() { + return usernamesTableName; + } + + public int getScanPageSize() { + return scanPageSize; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AdminEventLoggingConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AdminEventLoggingConfiguration.java new file mode 100644 index 000000000..7a13fa971 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AdminEventLoggingConfiguration.java @@ -0,0 +1,15 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; + +public record AdminEventLoggingConfiguration( + @NotBlank String credentials, + @NotEmpty String projectId, + @NotEmpty String logName) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ApnConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ApnConfiguration.java new file mode 100644 index 000000000..440c6423f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ApnConfiguration.java @@ -0,0 +1,17 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.configuration; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; + + +public record ApnConfiguration(@NotNull SecretString teamId, + @NotNull SecretString keyId, + @NotNull SecretString signingKey, + @NotBlank String bundleId, + boolean sandbox) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AppConfigConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AppConfigConfiguration.java new file mode 100644 index 000000000..1f4d08c0e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AppConfigConfiguration.java @@ -0,0 +1,32 @@ +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.validation.constraints.NotEmpty; + +public class AppConfigConfiguration { + + @JsonProperty + @NotEmpty + private String application; + + @JsonProperty + @NotEmpty + private String environment; + + @JsonProperty + @NotEmpty + private String configuration; + + public String getApplication() { + return application; + } + + public String getEnvironment() { + return environment; + } + + public String getConfigurationName() { + return configuration; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ArtServiceConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ArtServiceConfiguration.java new file mode 100644 index 000000000..ce2c79b47 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ArtServiceConfiguration.java @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import static org.apache.commons.lang3.ObjectUtils.firstNonNull; + +import java.time.Duration; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; +import org.whispersystems.textsecuregcm.util.ExactlySize; + +public record ArtServiceConfiguration(@ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret, + @NotNull SecretBytes userAuthenticationTokenUserIdSecret, + @NotNull Duration tokenExpiration) { + public ArtServiceConfiguration { + tokenExpiration = firstNonNull(tokenExpiration, Duration.ofDays(1)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AwsAttachmentsConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AwsAttachmentsConfiguration.java new file mode 100644 index 000000000..3bf3f3af1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AwsAttachmentsConfiguration.java @@ -0,0 +1,15 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.configuration; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; + +public record AwsAttachmentsConfiguration(@NotNull SecretString accessKey, + @NotNull SecretString accessSecret, + @NotBlank String bucket, + @NotBlank String region) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/BadgeConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/BadgeConfiguration.java new file mode 100644 index 000000000..8fe806b74 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/BadgeConfiguration.java @@ -0,0 +1,68 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.entities.BadgeSvg; +import org.whispersystems.textsecuregcm.util.ExactlySize; + +public class BadgeConfiguration { + public static final String CATEGORY_TESTING = "testing"; + + private final String id; + private final String category; + private final List sprites; + private final String svg; + private final List svgs; + + @JsonCreator + public BadgeConfiguration( + @JsonProperty("id") final String id, + @JsonProperty("category") final String category, + @JsonProperty("sprites") final List sprites, + @JsonProperty("svg") final String svg, + @JsonProperty("svgs") final List svgs) { + this.id = id; + this.category = category; + this.sprites = sprites; + this.svg = svg; + this.svgs = svgs; + } + + @NotEmpty + public String getId() { + return id; + } + + @NotEmpty + public String getCategory() { + return category; + } + + @NotNull + @ExactlySize(6) + public List getSprites() { + return sprites; + } + + @NotEmpty + public String getSvg() { + return svg; + } + + @NotNull + public List getSvgs() { + return svgs; + } + + public boolean isTestBadge() { + return CATEGORY_TESTING.equals(category); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/BadgesConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/BadgesConfiguration.java new file mode 100644 index 000000000..dd067bf7a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/BadgesConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import io.dropwizard.validation.ValidationMethod; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +public class BadgesConfiguration { + private final List badges; + private final List badgeIdsEnabledForAll; + private final Map receiptLevels; + + @JsonCreator + public BadgesConfiguration( + @JsonProperty("badges") @JsonSetter(nulls = Nulls.AS_EMPTY) final List badges, + @JsonProperty("badgeIdsEnabledForAll") @JsonSetter(nulls = Nulls.AS_EMPTY) final List badgeIdsEnabledForAll, + @JsonProperty("receiptLevels") @JsonSetter(nulls = Nulls.AS_EMPTY) final Map receiptLevels) { + this.badges = Objects.requireNonNull(badges); + this.badgeIdsEnabledForAll = Objects.requireNonNull(badgeIdsEnabledForAll); + this.receiptLevels = Objects.requireNonNull(receiptLevels); + } + + @Valid + @NotNull + public List getBadges() { + return badges; + } + + @Valid + @NotNull + public List getBadgeIdsEnabledForAll() { + return badgeIdsEnabledForAll; + } + + @Valid + @NotNull + public Map getReceiptLevels() { + return receiptLevels; + } + + @JsonIgnore + @ValidationMethod(message = "contains receipt level mappings that are not configured badges") + public boolean isAllReceiptLevelsConfigured() { + final Set badgeNames = badges.stream().map(BadgeConfiguration::getId).collect(Collectors.toSet()); + return badgeNames.containsAll(receiptLevels.values()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/BraintreeConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/BraintreeConfiguration.java new file mode 100644 index 000000000..32291b3e3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/BraintreeConfiguration.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import java.util.Map; +import java.util.Set; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; +import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod; + +/** + * @param merchantId the Braintree merchant ID + * @param publicKey the Braintree API public key + * @param privateKey the Braintree API private key + * @param environment the Braintree environment ("production" or "sandbox") + * @param supportedCurrencies the set of supported currencies + * @param graphqlUrl the Braintree GraphQL URl to use (this must match the environment) + * @param merchantAccounts merchant account within the merchant for processing individual currencies + * @param circuitBreaker configuration for the circuit breaker used by the GraphQL HTTP client + */ +public record BraintreeConfiguration(@NotBlank String merchantId, + @NotBlank String publicKey, + @NotNull SecretString privateKey, + @NotBlank String environment, + @Valid @NotEmpty Map> supportedCurrenciesByPaymentMethod, + @NotBlank String graphqlUrl, + @NotEmpty Map merchantAccounts, + @NotNull + @Valid + CircuitBreakerConfiguration circuitBreaker) { + + public BraintreeConfiguration { + if (circuitBreaker == null) { + // It’s a little counter-intuitive, but this compact constructor allows a default value + // to be used when one isn’t specified (e.g. in YAML), allowing the field to still be + // validated as @NotNull + circuitBreaker = new CircuitBreakerConfiguration(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/CdnConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/CdnConfiguration.java new file mode 100644 index 000000000..e3bb81ccc --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/CdnConfiguration.java @@ -0,0 +1,16 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; + +public record CdnConfiguration(@NotNull SecretString accessKey, + @NotNull SecretString accessSecret, + @NotBlank String bucket, + @NotBlank String region) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/CircuitBreakerConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/CircuitBreakerConfiguration.java new file mode 100644 index 000000000..2fd70bec1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/CircuitBreakerConfiguration.java @@ -0,0 +1,123 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +public class CircuitBreakerConfiguration { + + @JsonProperty + @NotNull + @Min(1) + @Max(100) + private int failureRateThreshold = 50; + + @JsonProperty + @NotNull + @Min(1) + private int permittedNumberOfCallsInHalfOpenState = 10; + + @JsonProperty + @NotNull + @Min(1) + private int slidingWindowSize = 100; + + @JsonProperty + @NotNull + @Min(1) + private int slidingWindowMinimumNumberOfCalls = 100; + + @JsonProperty + @NotNull + @Min(1) + private long waitDurationInOpenStateInSeconds = 10; + + @JsonProperty + private List ignoredExceptions = Collections.emptyList(); + + + public int getFailureRateThreshold() { + return failureRateThreshold; + } + + public int getPermittedNumberOfCallsInHalfOpenState() { + return permittedNumberOfCallsInHalfOpenState; + } + + public int getSlidingWindowSize() { + return slidingWindowSize; + } + + public int getSlidingWindowMinimumNumberOfCalls() { + return slidingWindowMinimumNumberOfCalls; + } + + public long getWaitDurationInOpenStateInSeconds() { + return waitDurationInOpenStateInSeconds; + } + + public List> getIgnoredExceptions() { + return ignoredExceptions.stream() + .map(name -> { + try { + return Class.forName(name); + } catch (final ClassNotFoundException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()); + } + + @VisibleForTesting + public void setFailureRateThreshold(int failureRateThreshold) { + this.failureRateThreshold = failureRateThreshold; + } + + @VisibleForTesting + public void setSlidingWindowSize(int size) { + this.slidingWindowSize = size; + } + + @VisibleForTesting + public void setSlidingWindowMinimumNumberOfCalls(int size) { + this.slidingWindowMinimumNumberOfCalls = size; + } + + @VisibleForTesting + public void setPermittedNumberOfCallsInHalfOpenState(int size) { + this.permittedNumberOfCallsInHalfOpenState = size; + } + + @VisibleForTesting + public void setWaitDurationInOpenStateInSeconds(int seconds) { + this.waitDurationInOpenStateInSeconds = seconds; + } + + @VisibleForTesting + public void setIgnoredExceptions(final List ignoredExceptions) { + this.ignoredExceptions = ignoredExceptions; + } + + public CircuitBreakerConfig toCircuitBreakerConfig() { + return CircuitBreakerConfig.custom() + .failureRateThreshold(getFailureRateThreshold()) + .ignoreExceptions(getIgnoredExceptions().toArray(new Class[0])) + .permittedNumberOfCallsInHalfOpenState(getPermittedNumberOfCallsInHalfOpenState()) + .waitDurationInOpenState(Duration.ofSeconds(getWaitDurationInOpenStateInSeconds())) + .slidingWindow(getSlidingWindowSize(), getSlidingWindowMinimumNumberOfCalls(), + CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) + .build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ClientReleaseConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ClientReleaseConfiguration.java new file mode 100644 index 000000000..890ad2bc2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ClientReleaseConfiguration.java @@ -0,0 +1,12 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import javax.validation.constraints.NotNull; +import java.time.Duration; + +public record ClientReleaseConfiguration(@NotNull Duration refreshInterval) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/CommandStopListenerConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/CommandStopListenerConfiguration.java new file mode 100644 index 000000000..0b43720aa --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/CommandStopListenerConfiguration.java @@ -0,0 +1,12 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import javax.validation.constraints.NotNull; + +public record CommandStopListenerConfiguration(@NotNull String path) { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DatabaseConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DatabaseConfiguration.java new file mode 100644 index 000000000..0b7cfcb17 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DatabaseConfiguration.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.validation.constraints.NotNull; + +import io.dropwizard.db.DataSourceFactory; + +public class DatabaseConfiguration extends DataSourceFactory { + + @NotNull + @JsonProperty + private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration(); + + public CircuitBreakerConfiguration getCircuitBreakerConfiguration() { + return circuitBreaker; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DirectoryV2ClientConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DirectoryV2ClientConfiguration.java new file mode 100644 index 000000000..58991a71e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DirectoryV2ClientConfiguration.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.configuration; + +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; +import org.whispersystems.textsecuregcm.util.ExactlySize; + +public record DirectoryV2ClientConfiguration(@ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret, + @ExactlySize(32) SecretBytes userIdTokenSharedSecret) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DirectoryV2Configuration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DirectoryV2Configuration.java new file mode 100644 index 000000000..afe069ca9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DirectoryV2Configuration.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.Valid; + +public class DirectoryV2Configuration { + + private final DirectoryV2ClientConfiguration clientConfiguration; + + @JsonCreator + public DirectoryV2Configuration(@JsonProperty("client") DirectoryV2ClientConfiguration clientConfiguration) { + this.clientConfiguration = clientConfiguration; + } + + @Valid + public DirectoryV2ClientConfiguration getDirectoryV2ClientConfiguration() { + return clientConfiguration; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DogstatsdConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DogstatsdConfiguration.java new file mode 100644 index 000000000..b2d8e0d92 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DogstatsdConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micrometer.statsd.StatsdConfig; +import io.micrometer.statsd.StatsdFlavor; +import java.time.Duration; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +public class DogstatsdConfiguration implements StatsdConfig { + + @JsonProperty + @NotNull + private Duration step = Duration.ofSeconds(10); + + @JsonProperty + @NotBlank + private String environment; + + @Override + public Duration step() { + return step; + } + + public String getEnvironment() { + return environment; + } + + @Override + public StatsdFlavor flavor() { + return StatsdFlavor.DATADOG; + } + + @Override + public String get(final String key) { + // We have no Micrometer key/value pairs to report, so always return `null` + return null; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DynamoDbClientConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DynamoDbClientConfiguration.java new file mode 100644 index 000000000..3a7c84a7c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DynamoDbClientConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Duration; +import javax.validation.constraints.NotEmpty; + +public class DynamoDbClientConfiguration { + + private final String region; + private final Duration clientExecutionTimeout; + private final Duration clientRequestTimeout; + + @JsonCreator + public DynamoDbClientConfiguration( + @JsonProperty("region") final String region, + @JsonProperty("clientExcecutionTimeout") final Duration clientExecutionTimeout, + @JsonProperty("clientRequestTimeout") final Duration clientRequestTimeout) { + this.region = region; + this.clientExecutionTimeout = clientExecutionTimeout != null ? clientExecutionTimeout : Duration.ofSeconds(30); + this.clientRequestTimeout = clientRequestTimeout != null ? clientRequestTimeout : Duration.ofSeconds(10); + } + + @NotEmpty + public String getRegion() { + return region; + } + + public Duration getClientExecutionTimeout() { + return clientExecutionTimeout; + } + + public Duration getClientRequestTimeout() { + return clientRequestTimeout; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DynamoDbTables.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DynamoDbTables.java new file mode 100644 index 000000000..133e74eb4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/DynamoDbTables.java @@ -0,0 +1,224 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Duration; +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +public class DynamoDbTables { + + public static class Table { + private final String tableName; + + @JsonCreator + public Table( + @JsonProperty("tableName") final String tableName) { + this.tableName = tableName; + } + + @NotEmpty + public String getTableName() { + return tableName; + } + } + + public static class TableWithExpiration extends Table { + private final Duration expiration; + + @JsonCreator + public TableWithExpiration( + @JsonProperty("tableName") final String tableName, + @JsonProperty("expiration") final Duration expiration) { + super(tableName); + this.expiration = expiration; + } + + @NotNull + public Duration getExpiration() { + return expiration; + } + } + + private final AccountsTableConfiguration accounts; + private final Table clientReleases; + private final Table deletedAccounts; + private final Table deletedAccountsLock; + private final IssuedReceiptsTableConfiguration issuedReceipts; + private final Table ecKeys; + private final Table ecSignedPreKeys; + private final Table kemKeys; + private final Table kemLastResortKeys; + private final TableWithExpiration messages; + private final Table phoneNumberIdentifiers; + private final Table profiles; + private final Table pushChallenge; + private final TableWithExpiration redeemedReceipts; + private final TableWithExpiration registrationRecovery; + private final Table remoteConfig; + private final Table reportMessage; + private final Table subscriptions; + private final Table verificationSessions; + + public DynamoDbTables( + @JsonProperty("accounts") final AccountsTableConfiguration accounts, + @JsonProperty("clientReleases") final Table clientReleases, + @JsonProperty("deletedAccounts") final Table deletedAccounts, + @JsonProperty("deletedAccountsLock") final Table deletedAccountsLock, + @JsonProperty("issuedReceipts") final IssuedReceiptsTableConfiguration issuedReceipts, + @JsonProperty("ecKeys") final Table ecKeys, + @JsonProperty("ecSignedPreKeys") final Table ecSignedPreKeys, + @JsonProperty("pqKeys") final Table kemKeys, + @JsonProperty("pqLastResortKeys") final Table kemLastResortKeys, + @JsonProperty("messages") final TableWithExpiration messages, + @JsonProperty("phoneNumberIdentifiers") final Table phoneNumberIdentifiers, + @JsonProperty("profiles") final Table profiles, + @JsonProperty("pushChallenge") final Table pushChallenge, + @JsonProperty("redeemedReceipts") final TableWithExpiration redeemedReceipts, + @JsonProperty("registrationRecovery") final TableWithExpiration registrationRecovery, + @JsonProperty("remoteConfig") final Table remoteConfig, + @JsonProperty("reportMessage") final Table reportMessage, + @JsonProperty("subscriptions") final Table subscriptions, + @JsonProperty("verificationSessions") final Table verificationSessions) { + + this.accounts = accounts; + this.clientReleases = clientReleases; + this.deletedAccounts = deletedAccounts; + this.deletedAccountsLock = deletedAccountsLock; + this.issuedReceipts = issuedReceipts; + this.ecKeys = ecKeys; + this.ecSignedPreKeys = ecSignedPreKeys; + this.kemKeys = kemKeys; + this.kemLastResortKeys = kemLastResortKeys; + this.messages = messages; + this.phoneNumberIdentifiers = phoneNumberIdentifiers; + this.profiles = profiles; + this.pushChallenge = pushChallenge; + this.redeemedReceipts = redeemedReceipts; + this.registrationRecovery = registrationRecovery; + this.remoteConfig = remoteConfig; + this.reportMessage = reportMessage; + this.subscriptions = subscriptions; + this.verificationSessions = verificationSessions; + } + + @NotNull + @Valid + public AccountsTableConfiguration getAccounts() { + return accounts; + } + + @NotNull + @Valid + public Table getClientReleases() { + return clientReleases; + } + + @NotNull + @Valid + public Table getDeletedAccounts() { + return deletedAccounts; + } + + @NotNull + @Valid + public Table getDeletedAccountsLock() { + return deletedAccountsLock; + } + + @NotNull + @Valid + public IssuedReceiptsTableConfiguration getIssuedReceipts() { + return issuedReceipts; + } + + @NotNull + @Valid + public Table getEcKeys() { + return ecKeys; + } + + @NotNull + @Valid + public Table getEcSignedPreKeys() { + return ecSignedPreKeys; + } + + @NotNull + @Valid + public Table getKemKeys() { + return kemKeys; + } + + @NotNull + @Valid + public Table getKemLastResortKeys() { + return kemLastResortKeys; + } + + @NotNull + @Valid + public TableWithExpiration getMessages() { + return messages; + } + + @NotNull + @Valid + public Table getPhoneNumberIdentifiers() { + return phoneNumberIdentifiers; + } + + @NotNull + @Valid + public Table getProfiles() { + return profiles; + } + + @NotNull + @Valid + public Table getPushChallenge() { + return pushChallenge; + } + + @NotNull + @Valid + public TableWithExpiration getRedeemedReceipts() { + return redeemedReceipts; + } + + @NotNull + @Valid + public TableWithExpiration getRegistrationRecovery() { + return registrationRecovery; + } + + @NotNull + @Valid + public Table getRemoteConfig() { + return remoteConfig; + } + + @NotNull + @Valid + public Table getReportMessage() { + return reportMessage; + } + + @NotNull + @Valid + public Table getSubscriptions() { + return subscriptions; + } + + @NotNull + @Valid + public Table getVerificationSessions() { + return verificationSessions; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/FcmConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/FcmConfiguration.java new file mode 100644 index 000000000..bd6b3e5e0 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/FcmConfiguration.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; + +public record FcmConfiguration(@NotNull SecretString credentials) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GcpAttachmentsConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GcpAttachmentsConfiguration.java new file mode 100644 index 000000000..fba253e63 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GcpAttachmentsConfiguration.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import io.dropwizard.util.Strings; +import io.dropwizard.validation.ValidationMethod; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; + +public record GcpAttachmentsConfiguration(@NotBlank String domain, + @NotBlank String email, + @Min(1) int maxSizeInBytes, + String pathPrefix, + @NotNull SecretString rsaSigningKey) { + @SuppressWarnings("unused") + @ValidationMethod(message = "pathPrefix must be empty or start with /") + public boolean isPathPrefixValid() { + return Strings.isNullOrEmpty(pathPrefix) || pathPrefix.startsWith("/"); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GenericZkConfig.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GenericZkConfig.java new file mode 100644 index 000000000..2ebcace88 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GenericZkConfig.java @@ -0,0 +1,12 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; + +public record GenericZkConfig(@NotNull SecretBytes serverSecret) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GraphiteConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GraphiteConfiguration.java new file mode 100644 index 000000000..8d4ea3947 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/GraphiteConfiguration.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class GraphiteConfiguration { + @JsonProperty + private String host; + + @JsonProperty + private int port; + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public boolean isEnabled() { + return host != null && port != 0; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/HCaptchaConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/HCaptchaConfiguration.java new file mode 100644 index 000000000..d5a334e3d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/HCaptchaConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; + +public class HCaptchaConfiguration { + + @JsonProperty + @NotNull + SecretString apiKey; + + @JsonProperty + @NotNull + CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration(); + + @JsonProperty + @NotNull + RetryConfiguration retry = new RetryConfiguration(); + + + public SecretString getApiKey() { + return apiKey; + } + + public CircuitBreakerConfiguration getCircuitBreaker() { + return circuitBreaker; + } + + public RetryConfiguration getRetry() { + return retry; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/IssuedReceiptsTableConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/IssuedReceiptsTableConfiguration.java new file mode 100644 index 000000000..e7969f521 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/IssuedReceiptsTableConfiguration.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Duration; +import javax.validation.constraints.NotEmpty; + +public class IssuedReceiptsTableConfiguration extends DynamoDbTables.TableWithExpiration { + + private final byte[] generator; + + public IssuedReceiptsTableConfiguration( + @JsonProperty("tableName") final String tableName, + @JsonProperty("expiration") final Duration expiration, + @JsonProperty("generator") final byte[] generator) { + super(tableName, expiration); + this.generator = generator; + } + + @NotEmpty + public byte[] getGenerator() { + return generator; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/LinkDeviceSecretConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/LinkDeviceSecretConfiguration.java new file mode 100644 index 000000000..648d768d8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/LinkDeviceSecretConfiguration.java @@ -0,0 +1,11 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; + +public record LinkDeviceSecretConfiguration(SecretBytes secret) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/MaxDeviceConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/MaxDeviceConfiguration.java new file mode 100644 index 000000000..b9b26ece8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/MaxDeviceConfiguration.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +public class MaxDeviceConfiguration { + + @JsonProperty + @NotEmpty + private String number; + + @JsonProperty + @NotNull + private int count; + + public String getNumber() { + return number; + } + + public int getCount() { + return count; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/MessageByteLimitCardinalityEstimatorConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/MessageByteLimitCardinalityEstimatorConfiguration.java new file mode 100644 index 000000000..1630c5698 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/MessageByteLimitCardinalityEstimatorConfiguration.java @@ -0,0 +1,11 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import javax.validation.constraints.NotNull; +import java.time.Duration; + +public record MessageByteLimitCardinalityEstimatorConfiguration(@NotNull Duration period) {} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/MessageCacheConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/MessageCacheConfiguration.java new file mode 100644 index 000000000..8f967b334 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/MessageCacheConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +public class MessageCacheConfiguration { + + @JsonProperty + @NotNull + @Valid + private RedisClusterConfiguration cluster; + + @JsonProperty + private int persistDelayMinutes = 10; + + public RedisClusterConfiguration getRedisClusterConfiguration() { + return cluster; + } + + public int getPersistDelayMinutes() { + return persistDelayMinutes; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/OneTimeDonationConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/OneTimeDonationConfiguration.java new file mode 100644 index 000000000..a128b0270 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/OneTimeDonationConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import java.math.BigDecimal; +import java.time.Duration; +import java.util.Map; +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Positive; + +/** + * @param boost configuration for individual donations + * @param gift configuration for gift donations + * @param currencies map of lower-cased ISO 3 currency codes and the suggested donation amounts in that currency + */ +public record OneTimeDonationConfiguration(@Valid ExpiringLevelConfiguration boost, + @Valid ExpiringLevelConfiguration gift, + Map currencies, + BigDecimal sepaMaxTransactionSizeEuros) { + + /** + * @param badge the numeric donation level ID + * @param level the badge ID associated with the level + * @param expiration the duration after which the level expires + */ + public record ExpiringLevelConfiguration(@NotEmpty String badge, @Positive long level, Duration expiration) { + + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/OneTimeDonationCurrencyConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/OneTimeDonationCurrencyConfiguration.java new file mode 100644 index 000000000..9fa696b3b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/OneTimeDonationCurrencyConfiguration.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import java.math.BigDecimal; +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.util.ExactlySize; + +/** + * One-time donation configuration for a given currency + * + * @param minimum the minimum amount permitted to be charged in this currency + * @param gift the suggested gift donation amount + * @param boosts the list of suggested one-time donation amounts + */ +public record OneTimeDonationCurrencyConfiguration( + @NotNull @DecimalMin("0.01") BigDecimal minimum, + @NotNull @DecimalMin("0.01") BigDecimal gift, + @Valid + @ExactlySize(6) + @NotNull + List<@NotNull @DecimalMin("0.01") BigDecimal> boosts) { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/PaymentsServiceConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/PaymentsServiceConfiguration.java new file mode 100644 index 000000000..06dc78e7a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/PaymentsServiceConfiguration.java @@ -0,0 +1,21 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import java.util.List; +import java.util.Map; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; + +public record PaymentsServiceConfiguration(@NotNull SecretBytes userAuthenticationTokenSharedSecret, + @NotNull SecretString coinMarketCapApiKey, + @NotNull SecretString fixerApiKey, + @NotEmpty Map<@NotBlank String, Integer> coinMarketCapCurrencyIds, + @NotEmpty List paymentCurrencies) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RecaptchaConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RecaptchaConfiguration.java new file mode 100644 index 000000000..948fee148 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RecaptchaConfiguration.java @@ -0,0 +1,12 @@ +/* + * Copyright 2021-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import javax.validation.constraints.NotEmpty; + +public record RecaptchaConfiguration(@NotEmpty String projectPath, @NotEmpty String credentialConfigurationJson) { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RedisClusterConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RedisClusterConfiguration.java new file mode 100644 index 000000000..07d1682cf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RedisClusterConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.Duration; + +public class RedisClusterConfiguration { + + @JsonProperty + @NotEmpty + private String configurationUri; + + @JsonProperty + @NotNull + private Duration timeout = Duration.ofMillis(3_000); + + @JsonProperty + @NotNull + @Valid + private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration(); + + @JsonProperty + @NotNull + @Valid + private RetryConfiguration retry = new RetryConfiguration(); + + public String getConfigurationUri() { + return configurationUri; + } + + public Duration getTimeout() { + return timeout; + } + + public CircuitBreakerConfiguration getCircuitBreakerConfiguration() { + return circuitBreaker; + } + + public RetryConfiguration getRetryConfiguration() { + return retry; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RedisConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RedisConfiguration.java new file mode 100644 index 000000000..07824b19e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RedisConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.configuration; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Duration; +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +public class RedisConfiguration { + + @JsonProperty + @NotEmpty + private String uri; + + @JsonProperty + @NotNull + private Duration timeout = Duration.ofSeconds(10); + + @JsonProperty + @NotNull + @Valid + private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration(); + + public String getUri() { + return uri; + } + + public Duration getTimeout() { + return timeout; + } + + public CircuitBreakerConfiguration getCircuitBreakerConfiguration() { + return circuitBreaker; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RegistrationServiceConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RegistrationServiceConfiguration.java new file mode 100644 index 000000000..cb50f6f2d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RegistrationServiceConfiguration.java @@ -0,0 +1,10 @@ +package org.whispersystems.textsecuregcm.configuration; + +import javax.validation.constraints.NotBlank; + +public record RegistrationServiceConfiguration(@NotBlank String host, + int port, + @NotBlank String credentialConfigurationJson, + @NotBlank String identityTokenAudience, + @NotBlank String registrationCaCertificate) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RemoteConfigConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RemoteConfigConfiguration.java new file mode 100644 index 000000000..fad24d277 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RemoteConfigConfiguration.java @@ -0,0 +1,19 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +public record RemoteConfigConfiguration(@NotNull Set authorizedUsers, + @NotNull String requiredHostedDomain, + @NotNull @NotEmpty List audiences, + @NotNull Map globalConfig) { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ReportMessageConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ReportMessageConfiguration.java new file mode 100644 index 000000000..98d2f0061 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ReportMessageConfiguration.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.NotNull; +import java.time.Duration; + +public class ReportMessageConfiguration { + + @JsonProperty + @NotNull + private final Duration reportTtl = Duration.ofDays(7); + + @JsonProperty + @NotNull + private final Duration counterTtl = Duration.ofDays(1); + + public Duration getReportTtl() { + return reportTtl; + } + + public Duration getCounterTtl() { + return counterTtl; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RetryConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RetryConfiguration.java new file mode 100644 index 000000000..9110b05d7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RetryConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.validation.constraints.Min; + +import java.time.Duration; + +import io.github.resilience4j.retry.RetryConfig; + +public class RetryConfiguration { + + @JsonProperty + @Min(1) + private int maxAttempts = 3; + + @JsonProperty + @Min(1) + private long waitDuration = RetryConfig.DEFAULT_WAIT_DURATION; + + public int getMaxAttempts() { + return maxAttempts; + } + + public void setMaxAttempts(final int maxAttempts) { + this.maxAttempts = maxAttempts; + } + + public long getWaitDuration() { + return waitDuration; + } + + public void setWaitDuration(final long waitDuration) { + this.waitDuration = waitDuration; + } + + public RetryConfig toRetryConfig() { + return toRetryConfigBuilder().build(); + } + + public RetryConfig.Builder toRetryConfigBuilder() { + return RetryConfig.custom() + .maxAttempts(getMaxAttempts()) + .waitDuration(Duration.ofMillis(getWaitDuration())); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureStorageServiceConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureStorageServiceConfiguration.java new file mode 100644 index 000000000..ebf6c2349 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureStorageServiceConfiguration.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; + +public record SecureStorageServiceConfiguration(@NotNull SecretBytes userAuthenticationTokenSharedSecret, + @NotBlank String uri, + @NotEmpty List<@NotBlank String> storageCaCertificates, + @Valid CircuitBreakerConfiguration circuitBreaker, + @Valid RetryConfiguration retry) { + public SecureStorageServiceConfiguration { + if (circuitBreaker == null) { + circuitBreaker = new CircuitBreakerConfiguration(); + } + if (retry == null) { + retry = new RetryConfiguration(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureValueRecovery2Configuration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureValueRecovery2Configuration.java new file mode 100644 index 000000000..a0a333195 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureValueRecovery2Configuration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.configuration; + +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; +import org.whispersystems.textsecuregcm.util.ExactlySize; + +public record SecureValueRecovery2Configuration( + @NotBlank String uri, + @ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret, + @ExactlySize(32) SecretBytes userIdTokenSharedSecret, + @NotEmpty List<@NotBlank String> svrCaCertificates, + @NotNull @Valid CircuitBreakerConfiguration circuitBreaker, + @NotNull @Valid RetryConfiguration retry) { + + public SecureValueRecovery2Configuration { + if (circuitBreaker == null) { + circuitBreaker = new CircuitBreakerConfiguration(); + } + + if (retry == null) { + retry = new RetryConfiguration(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ShortCodeExpanderConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ShortCodeExpanderConfiguration.java new file mode 100644 index 000000000..918fdd9cb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ShortCodeExpanderConfiguration.java @@ -0,0 +1,9 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +public record ShortCodeExpanderConfiguration(String baseUrl) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SpamFilterConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SpamFilterConfiguration.java new file mode 100644 index 000000000..c30028c35 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SpamFilterConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.NotBlank; + +public class SpamFilterConfiguration { + + @JsonProperty + @NotBlank + private final String environment; + + @JsonCreator + public SpamFilterConfiguration(@JsonProperty("environment") final String environment) { + this.environment = environment; + } + + public String getEnvironment() { + return environment; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SqsConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SqsConfiguration.java new file mode 100644 index 000000000..9db5343c8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SqsConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.validation.constraints.NotEmpty; + +public class SqsConfiguration { + @NotEmpty + @JsonProperty + private String accessKey; + + @NotEmpty + @JsonProperty + private String accessSecret; + + @NotEmpty + @JsonProperty + private List queueUrls; + + @NotEmpty + @JsonProperty + private String region = "us-east-1"; + + public String getAccessKey() { + return accessKey; + } + + public String getAccessSecret() { + return accessSecret; + } + + public List getQueueUrls() { + return queueUrls; + } + + public String getRegion() { + return region; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/StripeConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/StripeConfiguration.java new file mode 100644 index 000000000..cef389757 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/StripeConfiguration.java @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import java.util.Map; +import java.util.Set; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; +import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod; + +public record StripeConfiguration(@NotNull SecretString apiKey, + @NotNull SecretBytes idempotencyKeyGenerator, + @NotBlank String boostDescription, + @Valid @NotEmpty Map> supportedCurrenciesByPaymentMethod) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionConfiguration.java new file mode 100644 index 000000000..a9115ab55 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.validation.ValidationMethod; +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +public class SubscriptionConfiguration { + + private final Duration badgeGracePeriod; + private final Map levels; + + @JsonCreator + public SubscriptionConfiguration( + @JsonProperty("badgeGracePeriod") @Valid Duration badgeGracePeriod, + @JsonProperty("levels") @Valid Map<@NotNull @Min(1) Long, @NotNull @Valid SubscriptionLevelConfiguration> levels) { + this.badgeGracePeriod = badgeGracePeriod; + this.levels = levels; + } + + public Duration getBadgeGracePeriod() { + return badgeGracePeriod; + } + + public Map getLevels() { + return levels; + } + + @JsonIgnore + @ValidationMethod(message = "has a mismatch between the levels supported currencies") + public boolean isCurrencyListSameAcrossAllLevels() { + Optional any = levels.values().stream().findAny(); + if (any.isEmpty()) { + return true; + } + + Set currencies = any.get().getPrices().keySet(); + return levels.values().stream().allMatch(level -> currencies.equals(level.getPrices().keySet())); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionLevelConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionLevelConfiguration.java new file mode 100644 index 000000000..c410295b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionLevelConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +public class SubscriptionLevelConfiguration { + + private final String badge; + private final Map prices; + + @JsonCreator + public SubscriptionLevelConfiguration( + @JsonProperty("badge") @NotEmpty String badge, + @JsonProperty("prices") @Valid Map<@NotEmpty String, @NotNull @Valid SubscriptionPriceConfiguration> prices) { + this.badge = badge; + this.prices = prices; + } + + public String getBadge() { + return badge; + } + + public Map getPrices() { + return prices; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionPriceConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionPriceConfiguration.java new file mode 100644 index 000000000..21d3d0a2c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionPriceConfiguration.java @@ -0,0 +1,20 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import java.math.BigDecimal; +import java.util.Map; +import javax.validation.Valid; +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor; + +public record SubscriptionPriceConfiguration(@Valid @NotEmpty Map processorIds, + @NotNull @DecimalMin("0.01") BigDecimal amount) { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnSecretConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnSecretConfiguration.java new file mode 100644 index 000000000..496780e93 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnSecretConfiguration.java @@ -0,0 +1,11 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; + +public record TurnSecretConfiguration(SecretBytes secret) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnUriConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnUriConfiguration.java new file mode 100644 index 000000000..913d49790 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnUriConfiguration.java @@ -0,0 +1,40 @@ +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public class TurnUriConfiguration { + @JsonProperty + @NotNull + private List uris; + + /** + * The weight of this entry for weighted random selection + */ + @JsonProperty + @Min(0) + private long weight = 1; + + /** + * Enrolled numbers will always get this uri list + */ + @JsonProperty + private Set enrolledAcis = Collections.emptySet(); + + public List getUris() { + return uris; + } + + public long getWeight() { + return weight; + } + + public Set getEnrolledAcis() { + return Collections.unmodifiableSet(enrolledAcis); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/URLDeserializationConverter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/URLDeserializationConverter.java new file mode 100644 index 000000000..d6f137e50 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/URLDeserializationConverter.java @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.databind.util.StdConverter; +import java.net.MalformedURLException; +import java.net.URL; + +final class URLDeserializationConverter extends StdConverter { + + @Override + public URL convert(final String value) { + try { + return new URL(value); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/URLSerializationConverter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/URLSerializationConverter.java new file mode 100644 index 000000000..a557d6ca5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/URLSerializationConverter.java @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.databind.util.StdConverter; +import java.net.URL; + +final class URLSerializationConverter extends StdConverter { + + @Override + public String convert(final URL value) { + return value.toString(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/UnidentifiedDeliveryConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/UnidentifiedDeliveryConfiguration.java new file mode 100644 index 000000000..18971349e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/UnidentifiedDeliveryConfiguration.java @@ -0,0 +1,21 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import javax.validation.constraints.NotNull; +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECPrivateKey; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; +import org.whispersystems.textsecuregcm.util.ExactlySize; + +public record UnidentifiedDeliveryConfiguration(@NotNull SecretBytes certificate, + @ExactlySize(32) SecretBytes privateKey, + int expiresDays) { + public ECPrivateKey ecPrivateKey() throws InvalidKeyException { + return Curve.decodePrivatePoint(privateKey.value()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ZkConfig.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ZkConfig.java new file mode 100644 index 000000000..a0aebf91f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/ZkConfig.java @@ -0,0 +1,14 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; + +public record ZkConfig(@NotNull SecretBytes serverSecret, + @NotEmpty byte[] serverPublic) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicCaptchaConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicCaptchaConfiguration.java new file mode 100644 index 000000000..636031f86 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicCaptchaConfiguration.java @@ -0,0 +1,91 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.dynamic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import org.whispersystems.textsecuregcm.captcha.Action; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.validation.constraints.DecimalMax; +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotNull; + +public class DynamicCaptchaConfiguration { + + @JsonProperty + @DecimalMin("0") + @DecimalMax("1") + @NotNull + private BigDecimal scoreFloor; + + @JsonProperty + private boolean allowHCaptcha = false; + + @JsonProperty + private boolean allowRecaptcha = true; + + @JsonProperty + @NotNull + private Map> hCaptchaSiteKeys = Collections.emptyMap(); + + @JsonProperty + @NotNull + private Map> recaptchaSiteKeys = Collections.emptyMap(); + + @JsonProperty + @NotNull + private Map scoreFloorByAction = Collections.emptyMap(); + + public BigDecimal getScoreFloor() { + return scoreFloor; + } + + public boolean isAllowHCaptcha() { + return allowHCaptcha; + } + + public boolean isAllowRecaptcha() { + return allowRecaptcha; + } + + public Map getScoreFloorByAction() { + return scoreFloorByAction; + } + + @VisibleForTesting + public void setAllowHCaptcha(final boolean allowHCaptcha) { + this.allowHCaptcha = allowHCaptcha; + } + + @VisibleForTesting + public void setScoreFloor(final BigDecimal scoreFloor) { + this.scoreFloor = scoreFloor; + } + + public Map> getHCaptchaSiteKeys() { + return hCaptchaSiteKeys; + } + + @VisibleForTesting + public void setHCaptchaSiteKeys(final Map> hCaptchaSiteKeys) { + this.hCaptchaSiteKeys = hCaptchaSiteKeys; + } + + public Map> getRecaptchaSiteKeys() { + return recaptchaSiteKeys; + } + + @VisibleForTesting + public void setRecaptchaSiteKeys(final Map> recaptchaSiteKeys) { + this.recaptchaSiteKeys = recaptchaSiteKeys; + } + + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfiguration.java new file mode 100644 index 000000000..8c663ef11 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfiguration.java @@ -0,0 +1,107 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.dynamic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import javax.validation.Valid; +import org.whispersystems.textsecuregcm.limits.RateLimiterConfig; + +public class DynamicConfiguration { + + @JsonProperty + @Valid + private Map experiments = Collections.emptyMap(); + + @JsonProperty + @Valid + private Map preRegistrationExperiments = Collections.emptyMap(); + + @JsonProperty + @Valid + private Map limits = new HashMap<>(); + + @JsonProperty + @Valid + private DynamicRemoteDeprecationConfiguration remoteDeprecation = new DynamicRemoteDeprecationConfiguration(); + + @JsonProperty + @Valid + private DynamicPaymentsConfiguration payments = new DynamicPaymentsConfiguration(); + + @JsonProperty + @Valid + private DynamicCaptchaConfiguration captcha = new DynamicCaptchaConfiguration(); + + @JsonProperty + @Valid + private DynamicTurnConfiguration turn = new DynamicTurnConfiguration(); + + @JsonProperty + @Valid + DynamicMessagePersisterConfiguration messagePersister = new DynamicMessagePersisterConfiguration(); + + @JsonProperty + @Valid + DynamicRateLimitPolicy rateLimitPolicy = new DynamicRateLimitPolicy(false); + + @JsonProperty + @Valid + DynamicECPreKeyMigrationConfiguration ecPreKeyMigration = new DynamicECPreKeyMigrationConfiguration(true, false); + + @JsonProperty + @Valid + DynamicInboundMessageByteLimitConfiguration inboundMessageByteLimit = new DynamicInboundMessageByteLimitConfiguration(true); + + public Optional getExperimentEnrollmentConfiguration( + final String experimentName) { + return Optional.ofNullable(experiments.get(experimentName)); + } + + public Optional getPreRegistrationEnrollmentConfiguration( + final String experimentName) { + return Optional.ofNullable(preRegistrationExperiments.get(experimentName)); + } + + public Map getLimits() { + return limits; + } + + public DynamicRemoteDeprecationConfiguration getRemoteDeprecationConfiguration() { + return remoteDeprecation; + } + + public DynamicPaymentsConfiguration getPaymentsConfiguration() { + return payments; + } + + public DynamicCaptchaConfiguration getCaptchaConfiguration() { + return captcha; + } + + public DynamicTurnConfiguration getTurnConfiguration() { + return turn; + } + + public DynamicMessagePersisterConfiguration getMessagePersisterConfiguration() { + return messagePersister; + } + + public DynamicRateLimitPolicy getRateLimitPolicy() { + return rateLimitPolicy; + } + + public DynamicECPreKeyMigrationConfiguration getEcPreKeyMigrationConfiguration() { + return ecPreKeyMigration; + } + + public DynamicInboundMessageByteLimitConfiguration getInboundMessageByteLimitConfiguration() { + return inboundMessageByteLimit; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicECPreKeyMigrationConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicECPreKeyMigrationConfiguration.java new file mode 100644 index 000000000..fedc87ca7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicECPreKeyMigrationConfiguration.java @@ -0,0 +1,9 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.dynamic; + +public record DynamicECPreKeyMigrationConfiguration(boolean deleteEcSignedPreKeys, boolean storeEcSignedPreKeys) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicExperimentEnrollmentConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicExperimentEnrollmentConfiguration.java new file mode 100644 index 000000000..bd4b0b699 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicExperimentEnrollmentConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.dynamic; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import java.util.Collections; +import java.util.Set; +import java.util.UUID; + +public class DynamicExperimentEnrollmentConfiguration { + + @JsonProperty + @Valid + private Set enrolledUuids = Collections.emptySet(); + + @JsonProperty + @Valid + @Min(0) + @Max(100) + private int enrollmentPercentage = 0; + + public Set getEnrolledUuids() { + return enrolledUuids; + } + + public int getEnrollmentPercentage() { + return enrollmentPercentage; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicInboundMessageByteLimitConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicInboundMessageByteLimitConfiguration.java new file mode 100644 index 000000000..00475f1f9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicInboundMessageByteLimitConfiguration.java @@ -0,0 +1,9 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.dynamic; + +public record DynamicInboundMessageByteLimitConfiguration(boolean enforceInboundLimit) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicMessagePersisterConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicMessagePersisterConfiguration.java new file mode 100644 index 000000000..d74cac20d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicMessagePersisterConfiguration.java @@ -0,0 +1,18 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.dynamic; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class DynamicMessagePersisterConfiguration { + + @JsonProperty + private boolean persistenceEnabled = true; + + public boolean isPersistenceEnabled() { + return persistenceEnabled; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicPaymentsConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicPaymentsConfiguration.java new file mode 100644 index 000000000..01923b182 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicPaymentsConfiguration.java @@ -0,0 +1,20 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.dynamic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.List; + +public class DynamicPaymentsConfiguration { + + @JsonProperty + private List disallowedPrefixes = Collections.emptyList(); + + public List getDisallowedPrefixes() { + return disallowedPrefixes; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicPreRegistrationExperimentEnrollmentConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicPreRegistrationExperimentEnrollmentConfiguration.java new file mode 100644 index 000000000..d81b1595e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicPreRegistrationExperimentEnrollmentConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.dynamic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.Set; +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; + +public class DynamicPreRegistrationExperimentEnrollmentConfiguration { + + @JsonProperty + @Valid + private Set enrolledE164s = Collections.emptySet(); + + @JsonProperty + @Valid + private Set excludedE164s = Collections.emptySet(); + + @JsonProperty + @Valid + private Set includedCountryCodes = Collections.emptySet(); + + @JsonProperty + @Valid + private Set excludedCountryCodes = Collections.emptySet(); + + @JsonProperty + @Valid + @Min(0) + @Max(100) + private int enrollmentPercentage = 0; + + public Set getEnrolledE164s() { + return enrolledE164s; + } + + public Set getExcludedE164s() { + return excludedE164s; + } + + public Set getIncludedCountryCodes() { + return includedCountryCodes; + } + + public Set getExcludedCountryCodes() { + return excludedCountryCodes; + } + + public int getEnrollmentPercentage() { + return enrollmentPercentage; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicRateLimitPolicy.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicRateLimitPolicy.java new file mode 100644 index 000000000..3fb0172ea --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicRateLimitPolicy.java @@ -0,0 +1,8 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.dynamic; + +public record DynamicRateLimitPolicy(boolean failOpen) {} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicRemoteDeprecationConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicRemoteDeprecationConfiguration.java new file mode 100644 index 000000000..428532503 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicRemoteDeprecationConfiguration.java @@ -0,0 +1,78 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.dynamic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import com.vdurmont.semver4j.Semver; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +public class DynamicRemoteDeprecationConfiguration { + + @JsonProperty + private Map minimumVersions = Collections.emptyMap(); + + @JsonProperty + private Map versionsPendingDeprecation = Collections.emptyMap(); + + @JsonProperty + private Map> blockedVersions = Collections.emptyMap(); + + @JsonProperty + private Map> versionsPendingBlock = Collections.emptyMap(); + + @JsonProperty + private boolean unrecognizedUserAgentAllowed = true; + + @VisibleForTesting + public void setMinimumVersions(final Map minimumVersions) { + this.minimumVersions = minimumVersions; + } + + public Map getMinimumVersions() { + return minimumVersions; + } + + @VisibleForTesting + public void setVersionsPendingDeprecation(final Map versionsPendingDeprecation) { + this.versionsPendingDeprecation = versionsPendingDeprecation; + } + + public Map getVersionsPendingDeprecation() { + return versionsPendingDeprecation; + } + + @VisibleForTesting + public void setUnrecognizedUserAgentAllowed(final boolean allowUnrecognizedUserAgents) { + this.unrecognizedUserAgentAllowed = allowUnrecognizedUserAgents; + } + + public boolean isUnrecognizedUserAgentAllowed() { + return unrecognizedUserAgentAllowed; + } + + @VisibleForTesting + public void setBlockedVersions(final Map> blockedVersions) { + this.blockedVersions = blockedVersions; + } + + public Map> getBlockedVersions() { + return blockedVersions; + } + + @VisibleForTesting + public void setVersionsPendingBlock(final Map> versionsPendingBlock) { + this.versionsPendingBlock = versionsPendingBlock; + } + + public Map> getVersionsPendingBlock() { + return versionsPendingBlock; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicTurnConfiguration.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicTurnConfiguration.java new file mode 100644 index 000000000..e34a2143a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicTurnConfiguration.java @@ -0,0 +1,22 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.dynamic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.List; +import javax.validation.Valid; +import org.whispersystems.textsecuregcm.configuration.TurnUriConfiguration; + +public class DynamicTurnConfiguration { + + @JsonProperty + private List<@Valid TurnUriConfiguration> uriConfigs = Collections.emptyList(); + + public List getUriConfigs() { + return uriConfigs; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/BaseSecretValidator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/BaseSecretValidator.java new file mode 100644 index 000000000..d7887535e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/BaseSecretValidator.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.secrets; + +import static java.util.Objects.requireNonNull; + +import java.lang.annotation.Annotation; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public abstract class BaseSecretValidator> implements ConstraintValidator { + + private final ConstraintValidator validator; + + + protected BaseSecretValidator(final ConstraintValidator validator) { + this.validator = requireNonNull(validator); + } + + @Override + public void initialize(final A constraintAnnotation) { + validator.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(final S value, final ConstraintValidatorContext context) { + return validator.isValid(value.value(), context); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/Secret.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/Secret.java new file mode 100644 index 000000000..5f0b82921 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/Secret.java @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.secrets; + +public class Secret { + + private final T value; + + + public Secret(final T value) { + this.value = value; + } + + public T value() { + return value; + } + + @Override + public String toString() { + return "[REDACTED]"; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretBytes.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretBytes.java new file mode 100644 index 000000000..46205c43b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretBytes.java @@ -0,0 +1,20 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.secrets; + +import org.apache.commons.lang3.Validate; + +public class SecretBytes extends Secret { + + public SecretBytes(final byte[] value) { + super(requireNotEmpty(value)); + } + + private static byte[] requireNotEmpty(final byte[] value) { + Validate.isTrue(value.length > 0, "SecretBytes value must not be empty"); + return value; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretBytesList.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretBytesList.java new file mode 100644 index 000000000..88dec09b2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretBytesList.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.secrets; + +import com.google.common.collect.ImmutableList; +import java.util.Collection; +import java.util.List; +import javax.validation.constraints.NotEmpty; +import org.hibernate.validator.internal.constraintvalidators.bv.notempty.NotEmptyValidatorForCollection; + +public class SecretBytesList extends Secret> { + + @SuppressWarnings("rawtypes") + public static class ValidatorNotEmpty extends BaseSecretValidator { + public ValidatorNotEmpty() { + super(new NotEmptyValidatorForCollection()); + } + } + + public SecretBytesList(final List value) { + super(ImmutableList.copyOf(value)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretStore.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretStore.java new file mode 100644 index 000000000..03f3c0788 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretStore.java @@ -0,0 +1,106 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.secrets; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.annotations.VisibleForTesting; +import java.io.File; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +public class SecretStore { + + private final Map> secrets; + + + public static SecretStore fromYamlFileSecretsBundle(final String filename) { + try { + @SuppressWarnings("unchecked") + final Map secretsBundle = SystemMapper.yamlMapper().readValue(new File(filename), Map.class); + return fromSecretsBundle(secretsBundle); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to parse YAML file [%s]".formatted(filename), e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public SecretStore(final Map> secrets) { + this.secrets = Map.copyOf(secrets); + } + + public SecretString secretString(final String reference) { + return fromStore(reference, SecretString.class); + } + + public SecretBytes secretBytesFromBase64String(final String reference) { + final SecretString secret = fromStore(reference, SecretString.class); + return new SecretBytes(decodeBase64(secret.value())); + } + + public SecretStringList secretStringList(final String reference) { + return fromStore(reference, SecretStringList.class); + } + + public SecretBytesList secretBytesListFromBase64Strings(final String reference) { + final List secrets = secretStringList(reference).value(); + final List byteSecrets = secrets.stream().map(SecretStore::decodeBase64).toList(); + return new SecretBytesList(byteSecrets); + } + + private > T fromStore(final String name, final Class expected) { + final Secret secret = secrets.get(name); + if (secret == null) { + throw new IllegalArgumentException("Secret [%s] is not present in the secrets bundle".formatted(name)); + } + if (!expected.isInstance(secret)) { + throw new IllegalArgumentException("Secret [%s] is of type [%s] but caller expects type [%s]".formatted( + name, secret.getClass().getSimpleName(), expected.getSimpleName())); + } + return expected.cast(secret); + } + + @VisibleForTesting + public static SecretStore fromYamlStringSecretsBundle(final String secretsBundleYaml) { + try { + @SuppressWarnings("unchecked") + final Map secretsBundle = SystemMapper.yamlMapper().readValue(secretsBundleYaml, Map.class); + return fromSecretsBundle(secretsBundle); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to parse JSON", e); + } + } + + private static SecretStore fromSecretsBundle(final Map secretsBundle) { + final Map> store = new HashMap<>(); + secretsBundle.forEach((k, v) -> { + if (v instanceof final String str) { + store.put(k, new SecretString(str)); + return; + } + if (v instanceof final List list) { + final List secrets = list.stream().map(o -> { + if (o instanceof final String s) { + return s; + } + throw new IllegalArgumentException("Secrets bundle JSON object is only supposed to have values of types String and list of Strings"); + }).toList(); + store.put(k, new SecretStringList(secrets)); + return; + } + throw new IllegalArgumentException("Secrets bundle JSON object is only supposed to have values of types String and list of Strings"); + }); + return new SecretStore(store); + } + + private static byte[] decodeBase64(final String str) { + return Base64.getDecoder().decode(str); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretString.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretString.java new file mode 100644 index 000000000..55591092b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretString.java @@ -0,0 +1,14 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.secrets; + +import org.apache.commons.lang3.Validate; + +public class SecretString extends Secret { + public SecretString(final String value) { + super(Validate.notBlank(value, "SecretString value must not be blank")); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretStringList.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretStringList.java new file mode 100644 index 000000000..e08fe5b54 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretStringList.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.secrets; + +import com.google.common.collect.ImmutableList; +import java.util.Collection; +import java.util.List; +import javax.validation.constraints.NotEmpty; +import org.hibernate.validator.internal.constraintvalidators.bv.notempty.NotEmptyValidatorForCollection; + +public class SecretStringList extends Secret> { + + @SuppressWarnings("rawtypes") + public static class ValidatorNotEmpty extends BaseSecretValidator { + public ValidatorNotEmpty() { + super(new NotEmptyValidatorForCollection()); + } + } + + public SecretStringList(final List value) { + super(ImmutableList.copyOf(value)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretsModule.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretsModule.java new file mode 100644 index 000000000..e395b6573 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretsModule.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.secrets; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; + +public class SecretsModule extends SimpleModule { + + public static final SecretsModule INSTANCE = new SecretsModule(); + + public static final String PREFIX = "secret://"; + + private final AtomicReference secretStoreHolder = new AtomicReference<>(null); + + + private SecretsModule() { + addDeserializer(SecretString.class, createDeserializer(SecretStore::secretString)); + addDeserializer(SecretBytes.class, createDeserializer(SecretStore::secretBytesFromBase64String)); + addDeserializer(SecretStringList.class, createDeserializer(SecretStore::secretStringList)); + addDeserializer(SecretBytesList.class, createDeserializer(SecretStore::secretBytesListFromBase64Strings)); + } + + public void setSecretStore(final SecretStore secretStore) { + this.secretStoreHolder.set(requireNonNull(secretStore)); + } + + private JsonDeserializer createDeserializer(final BiFunction constructor) { + return new JsonDeserializer<>() { + @Override + public T deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException, JacksonException { + final SecretStore secretStore = secretStoreHolder.get(); + if (secretStore == null) { + throw new IllegalStateException( + "An instance of a SecretStore must be set for the SecretsModule via setSecretStore() method"); + } + final String reference = p.getValueAsString(); + if (!reference.startsWith(PREFIX) || reference.length() <= PREFIX.length()) { + throw new IllegalArgumentException( + "Value of a secret field must start with a [%s] prefix and refer to an entry in a secrets bundle".formatted(PREFIX)); + } + return constructor.apply(secretStore, reference.substring(PREFIX.length())); + } + }; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java new file mode 100644 index 000000000..17dc5c497 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -0,0 +1,538 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.controllers; + +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import java.util.Base64; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HEAD; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.signal.libsignal.usernames.BaseUsernameException; +import org.whispersystems.textsecuregcm.auth.AccountAndAuthenticatedDeviceHolder; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; +import org.whispersystems.textsecuregcm.auth.TurnToken; +import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse; +import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse; +import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; +import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest; +import org.whispersystems.textsecuregcm.entities.DeviceName; +import org.whispersystems.textsecuregcm.entities.EncryptedUsername; +import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; +import org.whispersystems.textsecuregcm.entities.RegistrationLock; +import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest; +import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse; +import org.whispersystems.textsecuregcm.entities.UsernameHashResponse; +import org.whispersystems.textsecuregcm.entities.UsernameLinkHandle; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.limits.RateLimitedByIp; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; +import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableException; +import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException; +import org.whispersystems.textsecuregcm.util.ExceptionUtils; +import org.whispersystems.textsecuregcm.util.HeaderUtils; +import org.whispersystems.textsecuregcm.util.UsernameHashZkProofVerifier; +import org.whispersystems.textsecuregcm.util.Util; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +@Path("/v1/accounts") +@io.swagger.v3.oas.annotations.tags.Tag(name = "Account") +public class AccountController { + public static final int MAXIMUM_USERNAME_HASHES_LIST_LENGTH = 20; + public static final int USERNAME_HASH_LENGTH = 32; + + private final AccountsManager accounts; + private final RateLimiters rateLimiters; + private final TurnTokenGenerator turnTokenGenerator; + private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager; + private final UsernameHashZkProofVerifier usernameHashZkProofVerifier; + + public AccountController( + AccountsManager accounts, + RateLimiters rateLimiters, + TurnTokenGenerator turnTokenGenerator, + RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager, + UsernameHashZkProofVerifier usernameHashZkProofVerifier) { + this.accounts = accounts; + this.rateLimiters = rateLimiters; + this.turnTokenGenerator = turnTokenGenerator; + this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager; + this.usernameHashZkProofVerifier = usernameHashZkProofVerifier; + } + + @GET + @Path("/turn/") + @Produces(MediaType.APPLICATION_JSON) + public TurnToken getTurnToken(@Auth AuthenticatedAccount auth) throws RateLimitExceededException { + rateLimiters.getTurnLimiter().validate(auth.getAccount().getUuid()); + return turnTokenGenerator.generate(auth.getAccount().getUuid()); + } + + @PUT + @Path("/gcm/") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ChangesDeviceEnabledState + public void setGcmRegistrationId(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth, + @NotNull @Valid GcmRegistrationId registrationId) { + + final Account account = disabledPermittedAuth.getAccount(); + final Device device = disabledPermittedAuth.getAuthenticatedDevice(); + + if (Objects.equals(device.getGcmId(), registrationId.gcmRegistrationId())) { + return; + } + + accounts.updateDevice(account, device.getId(), d -> { + d.setApnId(null); + d.setVoipApnId(null); + d.setGcmId(registrationId.gcmRegistrationId()); + d.setFetchesMessages(false); + }); + } + + @DELETE + @Path("/gcm/") + @ChangesDeviceEnabledState + public void deleteGcmRegistrationId(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth) { + Account account = disabledPermittedAuth.getAccount(); + Device device = disabledPermittedAuth.getAuthenticatedDevice(); + + accounts.updateDevice(account, device.getId(), d -> { + d.setGcmId(null); + d.setFetchesMessages(false); + d.setUserAgent("OWA"); + }); + } + + @PUT + @Path("/apn/") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ChangesDeviceEnabledState + public void setApnRegistrationId(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth, + @NotNull @Valid ApnRegistrationId registrationId) { + + final Account account = disabledPermittedAuth.getAccount(); + final Device device = disabledPermittedAuth.getAuthenticatedDevice(); + + if (Objects.equals(device.getApnId(), registrationId.apnRegistrationId()) && + Objects.equals(device.getVoipApnId(), registrationId.voipRegistrationId())) { + + return; + } + + accounts.updateDevice(account, device.getId(), d -> { + d.setApnId(registrationId.apnRegistrationId()); + d.setVoipApnId(registrationId.voipRegistrationId()); + d.setGcmId(null); + d.setFetchesMessages(false); + }); + } + + @DELETE + @Path("/apn/") + @ChangesDeviceEnabledState + public void deleteApnRegistrationId(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth) { + Account account = disabledPermittedAuth.getAccount(); + Device device = disabledPermittedAuth.getAuthenticatedDevice(); + + accounts.updateDevice(account, device.getId(), d -> { + d.setApnId(null); + d.setVoipApnId(null); + d.setFetchesMessages(false); + if (d.getId() == 1) { + d.setUserAgent("OWI"); + } else { + d.setUserAgent("OWP"); + } + }); + } + + @PUT + @Produces(MediaType.APPLICATION_JSON) + @Path("/registration_lock") + public void setRegistrationLock(@Auth AuthenticatedAccount auth, @NotNull @Valid RegistrationLock accountLock) { + SaltedTokenHash credentials = SaltedTokenHash.generateFor(accountLock.getRegistrationLock()); + + accounts.update(auth.getAccount(), + a -> a.setRegistrationLock(credentials.hash(), credentials.salt())); + } + + @DELETE + @Path("/registration_lock") + public void removeRegistrationLock(@Auth AuthenticatedAccount auth) { + accounts.update(auth.getAccount(), a -> a.setRegistrationLock(null, null)); + } + + @PUT + @Path("/name/") + public void setName(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth, @NotNull @Valid DeviceName deviceName) { + Account account = disabledPermittedAuth.getAccount(); + Device device = disabledPermittedAuth.getAuthenticatedDevice(); + accounts.updateDevice(account, device.getId(), d -> d.setName(deviceName.getDeviceName())); + } + + @PUT + @Path("/attributes/") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ChangesDeviceEnabledState + public void setAccountAttributes( + @Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth, + @HeaderParam(HeaderUtils.X_SIGNAL_AGENT) String userAgent, + @NotNull @Valid AccountAttributes attributes) { + final Account account = disabledPermittedAuth.getAccount(); + final long deviceId = disabledPermittedAuth.getAuthenticatedDevice().getId(); + + final Account updatedAccount = accounts.update(account, a -> { + a.getDevice(deviceId).ifPresent(d -> { + d.setFetchesMessages(attributes.getFetchesMessages()); + d.setName(attributes.getName()); + d.setLastSeen(Util.todayInMillis()); + d.setCapabilities(attributes.getCapabilities()); + d.setRegistrationId(attributes.getRegistrationId()); + attributes.getPhoneNumberIdentityRegistrationId().ifPresent(d::setPhoneNumberIdentityRegistrationId); + d.setUserAgent(userAgent); + }); + + a.setRegistrationLockFromAttributes(attributes); + a.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey()); + a.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess()); + a.setDiscoverableByPhoneNumber(attributes.isDiscoverableByPhoneNumber()); + }); + + // if registration recovery password was sent to us, store it (or refresh its expiration) + attributes.recoveryPassword().ifPresent(registrationRecoveryPassword -> + registrationRecoveryPasswordsManager.storeForCurrentNumber(updatedAccount.getNumber(), registrationRecoveryPassword)); + } + + @GET + @Path("/me") + @Produces(MediaType.APPLICATION_JSON) + public AccountIdentityResponse getMe(@Auth DisabledPermittedAuthenticatedAccount auth) { + return buildAccountIdentityResponse(auth); + } + + @GET + @Path("/whoami") + @Produces(MediaType.APPLICATION_JSON) + public AccountIdentityResponse whoAmI(@Auth AuthenticatedAccount auth) { + return buildAccountIdentityResponse(auth); + } + + private AccountIdentityResponse buildAccountIdentityResponse(AccountAndAuthenticatedDeviceHolder auth) { + return new AccountIdentityResponse(auth.getAccount().getUuid(), + auth.getAccount().getNumber(), + auth.getAccount().getPhoneNumberIdentifier(), + auth.getAccount().getUsernameHash().filter(h -> h.length > 0).orElse(null), + auth.getAccount().isStorageSupported()); + } + + @DELETE + @Path("/username_hash") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Delete username hash", + description = """ + Authenticated endpoint. Deletes previously stored username for the account. + """ + ) + @ApiResponse(responseCode = "204", description = "Username successfully deleted.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "401", description = "Account authentication check failed.") + public CompletableFuture deleteUsernameHash(@Auth final AuthenticatedAccount auth) { + clearUsernameLink(auth.getAccount()); + return accounts.clearUsernameHash(auth.getAccount()) + .thenRun(Util.NOOP); + } + + @PUT + @Path("/username_hash/reserve") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Operation( + summary = "Reserve username hash", + description = """ + Authenticated endpoint. Takes in a list of hashes of potential username hashes, finds one that is not taken, + and reserves it for the current account. + """ + ) + @ApiResponse(responseCode = "200", description = "Username hash reserved successfully.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "401", description = "Account authentication check failed.") + @ApiResponse(responseCode = "409", description = "All username hashes from the list are taken.") + @ApiResponse(responseCode = "422", description = "Invalid request format.") + @ApiResponse(responseCode = "429", description = "Ratelimited.") + public CompletableFuture reserveUsernameHash( + @Auth final AuthenticatedAccount auth, + @NotNull @Valid final ReserveUsernameHashRequest usernameRequest) throws RateLimitExceededException { + + rateLimiters.getUsernameReserveLimiter().validate(auth.getAccount().getUuid()); + + for (final byte[] hash : usernameRequest.usernameHashes()) { + if (hash.length != USERNAME_HASH_LENGTH) { + throw new WebApplicationException(Response.status(422).build()); + } + } + + return accounts.reserveUsernameHash(auth.getAccount(), usernameRequest.usernameHashes()) + .thenApply(reservation -> new ReserveUsernameHashResponse(reservation.reservedUsernameHash())) + .exceptionally(throwable -> { + if (ExceptionUtils.unwrap(throwable) instanceof UsernameHashNotAvailableException) { + throw new WebApplicationException(Status.CONFLICT); + } + + throw ExceptionUtils.wrap(throwable); + }); + } + + @PUT + @Path("/username_hash/confirm") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Operation( + summary = "Confirm username hash", + description = """ + Authenticated endpoint. For a previously reserved username hash, confirm that this username hash is now taken + by this account. + """ + ) + @ApiResponse(responseCode = "200", description = "Username hash confirmed successfully.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "401", description = "Account authentication check failed.") + @ApiResponse(responseCode = "409", description = "Given username hash doesn't match the reserved one or no reservation found.") + @ApiResponse(responseCode = "410", description = "Username hash not available (username can't be used).") + @ApiResponse(responseCode = "422", description = "Invalid request format.") + @ApiResponse(responseCode = "429", description = "Ratelimited.") + public CompletableFuture confirmUsernameHash( + @Auth final AuthenticatedAccount auth, + @NotNull @Valid final ConfirmUsernameHashRequest confirmRequest) { + + try { + usernameHashZkProofVerifier.verifyProof(confirmRequest.zkProof(), confirmRequest.usernameHash()); + } catch (final BaseUsernameException e) { + throw new WebApplicationException(Response.status(422).build()); + } + + return rateLimiters.getUsernameSetLimiter().validateAsync(auth.getAccount().getUuid()) + .thenCompose(ignored -> accounts.confirmReservedUsernameHash( + auth.getAccount(), + confirmRequest.usernameHash(), + confirmRequest.encryptedUsername())) + .thenApply(updatedAccount -> new UsernameHashResponse(updatedAccount.getUsernameHash() + .orElseThrow(() -> new IllegalStateException("Could not get username after setting")), + updatedAccount.getUsernameLinkHandle())) + .exceptionally(throwable -> { + if (ExceptionUtils.unwrap(throwable) instanceof UsernameReservationNotFoundException) { + throw new WebApplicationException(Status.CONFLICT); + } + + if (ExceptionUtils.unwrap(throwable) instanceof UsernameHashNotAvailableException) { + throw new WebApplicationException(Status.GONE); + } + + throw ExceptionUtils.wrap(throwable); + }) + .toCompletableFuture(); + } + + @GET + @Path("/username_hash/{usernameHash}") + @Produces(MediaType.APPLICATION_JSON) + @RateLimitedByIp(RateLimiters.For.USERNAME_LOOKUP) + @Operation( + summary = "Lookup username hash", + description = """ + Forced unauthenticated endpoint. For the given username hash, look up a user ID. + """ + ) + @ApiResponse(responseCode = "200", description = "Account found for the given username.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "400", description = "Request must not be authenticated.") + @ApiResponse(responseCode = "404", description = "Account not fount for the given username.") + public CompletableFuture lookupUsernameHash( + @Auth final Optional maybeAuthenticatedAccount, + @PathParam("usernameHash") final String usernameHash) { + + requireNotAuthenticated(maybeAuthenticatedAccount); + final byte[] hash; + try { + hash = Base64.getUrlDecoder().decode(usernameHash); + } catch (IllegalArgumentException | AssertionError e) { + throw new WebApplicationException(Response.status(422).build()); + } + + if (hash.length != USERNAME_HASH_LENGTH) { + throw new WebApplicationException(Response.status(422).build()); + } + + return accounts.getByUsernameHash(hash).thenApply(maybeAccount -> maybeAccount.map(Account::getUuid) + .map(AciServiceIdentifier::new) + .map(AccountIdentifierResponse::new) + .orElseThrow(() -> new WebApplicationException(Status.NOT_FOUND))); + } + + @PUT + @Path("/username_link") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Operation( + summary = "Set username link", + description = """ + Authenticated endpoint. For the given encrypted username generates a username link handle. + Username link handle could be used to lookup the encrypted username. + An account can only have one username link at a time. Calling this endpoint will reset previously stored + encrypted username and deactivate previous link handle. + """ + ) + @ApiResponse(responseCode = "200", description = "Username Link updated successfully.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "401", description = "Account authentication check failed.") + @ApiResponse(responseCode = "409", description = "Username is not set for the account.") + @ApiResponse(responseCode = "422", description = "Invalid request format.") + @ApiResponse(responseCode = "429", description = "Ratelimited.") + public UsernameLinkHandle updateUsernameLink( + @Auth final AuthenticatedAccount auth, + @NotNull @Valid final EncryptedUsername encryptedUsername) throws RateLimitExceededException { + // check ratelimiter for username link operations + rateLimiters.forDescriptor(RateLimiters.For.USERNAME_LINK_OPERATION).validate(auth.getAccount().getUuid()); + + // check if username hash is set for the account + if (auth.getAccount().getUsernameHash().isEmpty()) { + throw new WebApplicationException(Status.CONFLICT); + } + + final UUID usernameLinkHandle = UUID.randomUUID(); + updateUsernameLink(auth.getAccount(), usernameLinkHandle, encryptedUsername.usernameLinkEncryptedValue()); + return new UsernameLinkHandle(usernameLinkHandle); + } + + @DELETE + @Path("/username_link") + @Operation( + summary = "Delete username link", + description = """ + Authenticated endpoint. Deletes username link for the given account: previously store encrypted username is deleted + and username link handle is deactivated. + """ + ) + @ApiResponse(responseCode = "204", description = "Username Link successfully deleted.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "401", description = "Account authentication check failed.") + @ApiResponse(responseCode = "429", description = "Ratelimited.") + public void deleteUsernameLink(@Auth final AuthenticatedAccount auth) throws RateLimitExceededException { + // check ratelimiter for username link operations + rateLimiters.forDescriptor(RateLimiters.For.USERNAME_LINK_OPERATION).validate(auth.getAccount().getUuid()); + clearUsernameLink(auth.getAccount()); + } + + @GET + @Path("/username_link/{uuid}") + @Produces(MediaType.APPLICATION_JSON) + @RateLimitedByIp(RateLimiters.For.USERNAME_LINK_LOOKUP_PER_IP) + @Operation( + summary = "Lookup username link", + description = """ + Enforced unauthenticated endpoint. For the given username link handle, looks up the database for an associated encrypted username. + If found, encrypted username is returned, otherwise responds with 404 Not Found. + """ + ) + @ApiResponse(responseCode = "200", description = "Username link with the given handle was found.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "400", description = "Request must not be authenticated.") + @ApiResponse(responseCode = "404", description = "Username link was not found for the given handle.") + @ApiResponse(responseCode = "422", description = "Invalid request format.") + @ApiResponse(responseCode = "429", description = "Ratelimited.") + public CompletableFuture lookupUsernameLink( + @Auth final Optional maybeAuthenticatedAccount, + @PathParam("uuid") final UUID usernameLinkHandle) { + + requireNotAuthenticated(maybeAuthenticatedAccount); + + return accounts.getByUsernameLinkHandle(usernameLinkHandle) + .thenApply(maybeAccount -> maybeAccount.flatMap(Account::getEncryptedUsername) + .map(EncryptedUsername::new) + .orElseThrow(NotFoundException::new)); + } + + @Operation( + summary = "Check whether an account exists", + description = """ + Enforced unauthenticated endpoint. Checks whether an account with a given identifier exists. + """ + ) + @ApiResponse(responseCode = "200", description = "An account with the given identifier was found.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "400", description = "Request must not be authenticated.") + @ApiResponse(responseCode = "404", description = "An account was not found for the given identifier.") + @ApiResponse(responseCode = "422", description = "Invalid request format.") + @ApiResponse(responseCode = "429", description = "Rate-limited.") + @HEAD + @Path("/account/{identifier}") + @RateLimitedByIp(RateLimiters.For.CHECK_ACCOUNT_EXISTENCE) + public Response accountExists( + @Auth final Optional authenticatedAccount, + + @Parameter(description = "An ACI or PNI account identifier to check") + @PathParam("identifier") final ServiceIdentifier accountIdentifier) { + + // Disallow clients from making authenticated requests to this endpoint + requireNotAuthenticated(authenticatedAccount); + + final Optional maybeAccount = accounts.getByServiceIdentifier(accountIdentifier); + + return Response.status(maybeAccount.map(ignored -> Status.OK).orElse(Status.NOT_FOUND)).build(); + } + + @DELETE + @Path("/me") + public CompletableFuture deleteAccount(@Auth DisabledPermittedAuthenticatedAccount auth) throws InterruptedException { + return accounts.delete(auth.getAccount(), AccountsManager.DeletionReason.USER_REQUEST); + } + + private void clearUsernameLink(final Account account) { + updateUsernameLink(account, null, null); + } + + private void updateUsernameLink( + final Account account, + @Nullable final UUID usernameLinkHandle, + @Nullable final byte[] encryptedUsername) { + if ((encryptedUsername == null) ^ (usernameLinkHandle == null)) { + throw new IllegalStateException("Both or neither arguments must be null"); + } + accounts.update(account, a -> a.setUsernameLinkDetails(usernameLinkHandle, encryptedUsername)); + } + + private void requireNotAuthenticated(final Optional authenticatedAccount) { + if (authenticatedAccount.isPresent()) { + throw new BadRequestException("Operation requires unauthenticated access"); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2.java new file mode 100644 index 000000000..6051f81ce --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2.java @@ -0,0 +1,245 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.common.net.HttpHeaders; +import io.dropwizard.auth.Auth; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager; +import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager; +import org.whispersystems.textsecuregcm.entities.AccountDataReportResponse; +import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse; +import org.whispersystems.textsecuregcm.entities.ChangeNumberRequest; +import org.whispersystems.textsecuregcm.entities.MismatchedDevices; +import org.whispersystems.textsecuregcm.entities.PhoneNumberDiscoverabilityRequest; +import org.whispersystems.textsecuregcm.entities.PhoneNumberIdentityKeyDistributionRequest; +import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest; +import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure; +import org.whispersystems.textsecuregcm.entities.StaleDevices; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.ChangeNumberManager; + +@Path("/v2/accounts") +@io.swagger.v3.oas.annotations.tags.Tag(name = "Account") +public class AccountControllerV2 { + + private static final String CHANGE_NUMBER_COUNTER_NAME = name(AccountControllerV2.class, "changeNumber"); + private static final String VERIFICATION_TYPE_TAG_NAME = "verification"; + + private final AccountsManager accountsManager; + private final ChangeNumberManager changeNumberManager; + private final PhoneVerificationTokenManager phoneVerificationTokenManager; + private final RegistrationLockVerificationManager registrationLockVerificationManager; + private final RateLimiters rateLimiters; + + public AccountControllerV2(final AccountsManager accountsManager, final ChangeNumberManager changeNumberManager, + final PhoneVerificationTokenManager phoneVerificationTokenManager, + final RegistrationLockVerificationManager registrationLockVerificationManager, final RateLimiters rateLimiters) { + this.accountsManager = accountsManager; + this.changeNumberManager = changeNumberManager; + this.phoneVerificationTokenManager = phoneVerificationTokenManager; + this.registrationLockVerificationManager = registrationLockVerificationManager; + this.rateLimiters = rateLimiters; + } + + @PUT + @Path("/number") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Change number", description = "Changes a phone number for an existing account.") + @ApiResponse(responseCode = "200", description = "The phone number associated with the authenticated account was changed successfully", useReturnTypeSchema = true) + @ApiResponse(responseCode = "401", description = "Account authentication check failed.") + @ApiResponse(responseCode = "403", description = "Verification failed for the provided Registration Recovery Password") + @ApiResponse(responseCode = "409", description = "Mismatched number of devices or device ids in 'devices to notify' list", content = @Content(schema = @Schema(implementation = MismatchedDevices.class))) + @ApiResponse(responseCode = "410", description = "Mismatched registration ids in 'devices to notify' list", content = @Content(schema = @Schema(implementation = StaleDevices.class))) + @ApiResponse(responseCode = "422", description = "The request did not pass validation") + @ApiResponse(responseCode = "423", content = @Content(schema = @Schema(implementation = RegistrationLockFailure.class))) + @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header( + name = "Retry-After", + description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed")) + public AccountIdentityResponse changeNumber(@Auth final AuthenticatedAccount authenticatedAccount, + @NotNull @Valid final ChangeNumberRequest request, @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent) + throws RateLimitExceededException, InterruptedException { + + if (!authenticatedAccount.getAuthenticatedDevice().isMaster()) { + throw new ForbiddenException(); + } + + final String number = request.number(); + + // Only verify and check reglock if there's a data change to be made... + if (!authenticatedAccount.getAccount().getNumber().equals(number)) { + + RateLimiter.adaptLegacyException(() -> rateLimiters.getRegistrationLimiter().validate(number)); + + final PhoneVerificationRequest.VerificationType verificationType = phoneVerificationTokenManager.verify(number, + request); + + final Optional existingAccount = accountsManager.getByE164(number); + + if (existingAccount.isPresent()) { + registrationLockVerificationManager.verifyRegistrationLock(existingAccount.get(), request.registrationLock(), + userAgent, RegistrationLockVerificationManager.Flow.CHANGE_NUMBER, verificationType); + } + + Metrics.counter(CHANGE_NUMBER_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent), + Tag.of(VERIFICATION_TYPE_TAG_NAME, verificationType.name()))) + .increment(); + } + + // ...but always attempt to make the change in case a client retries and needs to re-send messages + try { + final Account updatedAccount = changeNumberManager.changeNumber( + authenticatedAccount.getAccount(), + request.number(), + request.pniIdentityKey(), + request.devicePniSignedPrekeys(), + request.devicePniPqLastResortPrekeys(), + request.deviceMessages(), + request.pniRegistrationIds()); + + return new AccountIdentityResponse( + updatedAccount.getUuid(), + updatedAccount.getNumber(), + updatedAccount.getPhoneNumberIdentifier(), + updatedAccount.getUsernameHash().orElse(null), + updatedAccount.isStorageSupported()); + } catch (MismatchedDevicesException e) { + throw new WebApplicationException(Response.status(409) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(new MismatchedDevices(e.getMissingDevices(), + e.getExtraDevices())) + .build()); + } catch (StaleDevicesException e) { + throw new WebApplicationException(Response.status(410) + .type(MediaType.APPLICATION_JSON) + .entity(new StaleDevices(e.getStaleDevices())) + .build()); + } catch (IllegalArgumentException e) { + throw new BadRequestException(e); + } + } + + @PUT + @Path("/phone_number_identity_key_distribution") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Set phone-number identity keys", + description = "Updates key material for the phone-number identity for all devices and sends a synchronization message to companion devices") + @ApiResponse(responseCode = "200", description = "Indicates the transaction was successful and returns basic information about this account.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "401", description = "Account authentication check failed.") + @ApiResponse(responseCode = "403", description = "This endpoint can only be invoked from the account's primary device.") + @ApiResponse(responseCode = "422", description = "The request body failed validation.") + @ApiResponse(responseCode = "409", description = "The set of devices specified in the request does not match the set of devices active on the account.", + content = @Content(schema = @Schema(implementation = MismatchedDevices.class))) + @ApiResponse(responseCode = "410", description = "The registration IDs provided for some devices do not match those stored on the server.", + content = @Content(schema = @Schema(implementation = StaleDevices.class))) + public AccountIdentityResponse distributePhoneNumberIdentityKeys(@Auth final AuthenticatedAccount authenticatedAccount, + @NotNull @Valid final PhoneNumberIdentityKeyDistributionRequest request) { + + if (!authenticatedAccount.getAuthenticatedDevice().isMaster()) { + throw new ForbiddenException(); + } + + try { + final Account updatedAccount = changeNumberManager.updatePniKeys( + authenticatedAccount.getAccount(), + request.pniIdentityKey(), + request.devicePniSignedPrekeys(), + request.devicePniPqLastResortPrekeys(), + request.deviceMessages(), + request.pniRegistrationIds()); + + return new AccountIdentityResponse( + updatedAccount.getUuid(), + updatedAccount.getNumber(), + updatedAccount.getPhoneNumberIdentifier(), + updatedAccount.getUsernameHash().orElse(null), + updatedAccount.isStorageSupported()); + } catch (MismatchedDevicesException e) { + throw new WebApplicationException(Response.status(409) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(new MismatchedDevices(e.getMissingDevices(), + e.getExtraDevices())) + .build()); + } catch (StaleDevicesException e) { + throw new WebApplicationException(Response.status(410) + .type(MediaType.APPLICATION_JSON) + .entity(new StaleDevices(e.getStaleDevices())) + .build()); + } catch (IllegalArgumentException e) { + throw new BadRequestException(e); + } + } + + @PUT + @Path("/phone_number_discoverability") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public void setPhoneNumberDiscoverability( + @Auth AuthenticatedAccount auth, + @NotNull @Valid PhoneNumberDiscoverabilityRequest phoneNumberDiscoverability + ) { + accountsManager.update(auth.getAccount(), a -> a.setDiscoverableByPhoneNumber( + phoneNumberDiscoverability.discoverableByPhoneNumber())); + } + + @GET + @Path("/data_report") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Produces a report of non-ephemeral account data stored by the service") + @ApiResponse(responseCode = "200", + description = "Response with data report. A plain text representation is a field in the response.", + useReturnTypeSchema = true) + public AccountDataReportResponse getAccountDataReport(@Auth final AuthenticatedAccount auth) { + + final Account account = auth.getAccount(); + + return new AccountDataReportResponse(UUID.randomUUID(), Instant.now(), + new AccountDataReportResponse.AccountAndDevicesDataReport( + new AccountDataReportResponse.AccountDataReport( + account.getNumber(), + account.getBadges().stream().map(AccountDataReportResponse.BadgeDataReport::new).toList(), + account.isUnrestrictedUnidentifiedAccess(), + account.isDiscoverableByPhoneNumber()), + account.getDevices().stream().map(device -> + new AccountDataReportResponse.DeviceDataReport( + device.getId(), + Instant.ofEpochMilli(device.getLastSeen()), + Instant.ofEpochMilli(device.getCreated()), + device.getUserAgent())).toList())); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArtController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArtController.java new file mode 100644 index 000000000..7178fa76d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArtController.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.UUID; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration; +import org.whispersystems.textsecuregcm.limits.RateLimiters; + +@Path("/v1/art") +@Tag(name = "Art") +public class ArtController { + private final ExternalServiceCredentialsGenerator artServiceCredentialsGenerator; + private final RateLimiters rateLimiters; + + public static ExternalServiceCredentialsGenerator credentialsGenerator(final ArtServiceConfiguration cfg) { + return ExternalServiceCredentialsGenerator + .builder(cfg.userAuthenticationTokenSharedSecret()) + .withUserDerivationKey(cfg.userAuthenticationTokenUserIdSecret()) + .prependUsername(false) + .truncateSignature(false) + .build(); + } + + public ArtController(final RateLimiters rateLimiters, + final ExternalServiceCredentialsGenerator artServiceCredentialsGenerator) { + this.artServiceCredentialsGenerator = artServiceCredentialsGenerator; + this.rateLimiters = rateLimiters; + } + + @GET + @Path("/auth") + @Produces(MediaType.APPLICATION_JSON) + public ExternalServiceCredentials getAuth(final @Auth AuthenticatedAccount auth) + throws RateLimitExceededException { + final UUID uuid = auth.getAccount().getUuid(); + rateLimiters.forDescriptor(RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS).validate(uuid); + return artServiceCredentialsGenerator.generateForUuid(uuid); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV2.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV2.java new file mode 100644 index 000000000..e15befe4b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV2.java @@ -0,0 +1,81 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.common.net.HttpHeaders; +import io.dropwizard.auth.Auth; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.security.SecureRandom; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV2; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.s3.PolicySigner; +import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator; +import org.whispersystems.textsecuregcm.util.Conversions; +import org.whispersystems.textsecuregcm.util.Pair; + +@Path("/v2/attachments") +@Tag(name = "Attachments") +public class AttachmentControllerV2 { + + private final PostPolicyGenerator policyGenerator; + private final PolicySigner policySigner; + private final RateLimiter rateLimiter; + + private static final String CREATE_UPLOAD_COUNTER_NAME = name(AttachmentControllerV2.class, "uploadForm"); + + public AttachmentControllerV2(RateLimiters rateLimiters, String accessKey, String accessSecret, String region, + String bucket) { + this.rateLimiter = rateLimiters.getAttachmentLimiter(); + this.policyGenerator = new PostPolicyGenerator(region, bucket, accessKey); + this.policySigner = new PolicySigner(accessSecret, region); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/form/upload") + public AttachmentDescriptorV2 getAttachmentUploadForm( + @Auth AuthenticatedAccount auth, + @HeaderParam(HttpHeaders.USER_AGENT) String userAgent) + throws RateLimitExceededException { + rateLimiter.validate(auth.getAccount().getUuid()); + + ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); + long attachmentId = generateAttachmentId(); + String objectName = String.valueOf(attachmentId); + Pair policy = policyGenerator.createFor(now, objectName, 100 * 1024 * 1024); + String signature = policySigner.getSignature(now, policy.second()); + + Metrics.counter(CREATE_UPLOAD_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment(); + + return new AttachmentDescriptorV2(attachmentId, objectName, policy.first(), + "private", "AWS4-HMAC-SHA256", + now.format(PostPolicyGenerator.AWS_DATE_TIME), + policy.second(), signature); + } + + private long generateAttachmentId() { + byte[] attachmentBytes = new byte[8]; + new SecureRandom().nextBytes(attachmentBytes); + + attachmentBytes[0] = (byte) (attachmentBytes[0] & 0x7F); + return Conversions.byteArrayToLong(attachmentBytes); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV3.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV3.java new file mode 100644 index 000000000..2d2c03c5c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV3.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; +import javax.annotation.Nonnull; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.whispersystems.textsecuregcm.attachments.AttachmentGenerator; +import org.whispersystems.textsecuregcm.attachments.GcsAttachmentGenerator; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV3; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; + +@Path("/v3/attachments") +@Tag(name = "Attachments") +public class AttachmentControllerV3 { + + @Nonnull + private final RateLimiter rateLimiter; + + @Nonnull + private final GcsAttachmentGenerator gcsAttachmentGenerator; + + @Nonnull + private final SecureRandom secureRandom; + + public AttachmentControllerV3(@Nonnull RateLimiters rateLimiters, @Nonnull GcsAttachmentGenerator gcsAttachmentGenerator) + throws IOException, InvalidKeyException, InvalidKeySpecException { + this.rateLimiter = rateLimiters.getAttachmentLimiter(); + this.gcsAttachmentGenerator = gcsAttachmentGenerator; + this.secureRandom = new SecureRandom(); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/form/upload") + public AttachmentDescriptorV3 getAttachmentUploadForm(@Auth AuthenticatedAccount auth) + throws RateLimitExceededException { + rateLimiter.validate(auth.getAccount().getUuid()); + final String key = generateAttachmentKey(); + final AttachmentGenerator.Descriptor descriptor = this.gcsAttachmentGenerator.generateAttachment(key); + return new AttachmentDescriptorV3(2, key, descriptor.headers(), descriptor.signedUploadLocation()); + } + + private String generateAttachmentKey() { + final byte[] bytes = new byte[15]; + secureRandom.nextBytes(bytes); + return Base64.getUrlEncoder().encodeToString(bytes); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV4.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV4.java new file mode 100644 index 000000000..4ec3f2ca9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV4.java @@ -0,0 +1,95 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.whispersystems.textsecuregcm.attachments.AttachmentGenerator; +import org.whispersystems.textsecuregcm.attachments.GcsAttachmentGenerator; +import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV3; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; + + +/** + * The V4 API is identical to the {@link AttachmentControllerV3} API, but supports an additional TUS based cdn type (cdn3) + */ +@Path("/v4/attachments") +@Tag(name = "Attachments") +public class AttachmentControllerV4 { + + public static final String CDN3_EXPERIMENT_NAME = "cdn3"; + + private final ExperimentEnrollmentManager experimentEnrollmentManager; + private final RateLimiter rateLimiter; + + private final Map attachmentGenerators; + + @Nonnull + private final SecureRandom secureRandom; + + public AttachmentControllerV4( + final RateLimiters rateLimiters, + final GcsAttachmentGenerator gcsAttachmentGenerator, + final TusAttachmentGenerator tusAttachmentGenerator, + final ExperimentEnrollmentManager experimentEnrollmentManager) { + this.rateLimiter = rateLimiters.getAttachmentLimiter(); + this.experimentEnrollmentManager = experimentEnrollmentManager; + this.secureRandom = new SecureRandom(); + this.attachmentGenerators = Map.of( + 2, gcsAttachmentGenerator, + 3, tusAttachmentGenerator + ); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/form/upload") + @Operation( + summary = "Get an upload form", + description = """ + Retrieve an upload form that can be used to perform a resumable upload. The response will include a cdn number + indicating what protocol should be used to perform the upload. + """ + ) + @ApiResponse(responseCode = "200", description = "Success, response body includes upload form", useReturnTypeSchema = true) + @ApiResponse(responseCode = "413", description = "Too many attempts", headers = @Header( + name = "Retry-After", + description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed")) + @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header( + name = "Retry-After", + description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed")) + public AttachmentDescriptorV3 getAttachmentUploadForm(@Auth AuthenticatedAccount auth) + throws RateLimitExceededException { + rateLimiter.validate(auth.getAccount().getUuid()); + final String key = generateAttachmentKey(); + final boolean useCdn3 = this.experimentEnrollmentManager.isEnrolled(auth.getAccount().getUuid(), CDN3_EXPERIMENT_NAME); + int cdn = useCdn3 ? 3 : 2; + final AttachmentGenerator.Descriptor descriptor = this.attachmentGenerators.get(cdn).generateAttachment(key); + return new AttachmentDescriptorV3(cdn, key, descriptor.headers(), descriptor.signedUploadLocation()); + } + + private String generateAttachmentKey() { + final byte[] bytes = new byte[15]; + secureRandom.nextBytes(bytes); + return Base64.getUrlEncoder().encodeToString(bytes); + } +} + diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallLinkController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallLinkController.java new file mode 100644 index 000000000..0c55bcd05 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallLinkController.java @@ -0,0 +1,74 @@ +package org.whispersystems.textsecuregcm.controllers; + +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.signal.libsignal.protocol.ServiceId; +import org.signal.libsignal.zkgroup.GenericServerSecretParams; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialRequest; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.entities.CreateCallLinkCredential; +import org.whispersystems.textsecuregcm.entities.GetCreateCallLinkCredentialsRequest; +import org.whispersystems.textsecuregcm.limits.RateLimiters; + +@Path("/v1/call-link") +@io.swagger.v3.oas.annotations.tags.Tag(name = "CallLink") +public class CallLinkController { + private final RateLimiters rateLimiters; + private final GenericServerSecretParams genericServerSecretParams; + + public CallLinkController( + final RateLimiters rateLimiters, + final GenericServerSecretParams genericServerSecretParams + ) { + this.rateLimiters = rateLimiters; + this.genericServerSecretParams = genericServerSecretParams; + } + + @POST + @Path("/create-auth") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Generate a credential for creating call links", + description = """ + Generate a credential over a truncated timestamp, room ID, and account UUID. With zero knowledge + group infrastructure, the server does not know the room ID. + """ + ) + @ApiResponse(responseCode = "200", description = "`JSON` with generated credentials.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "400", description = "Invalid create call link credential request.") + @ApiResponse(responseCode = "401", description = "Account authentication check failed.") + @ApiResponse(responseCode = "422", description = "Invalid request format.") + @ApiResponse(responseCode = "429", description = "Ratelimited.") + public CreateCallLinkCredential getCreateAuth( + final @Auth AuthenticatedAccount auth, + final @NotNull @Valid GetCreateCallLinkCredentialsRequest request + ) throws RateLimitExceededException { + + rateLimiters.getCreateCallLinkLimiter().validate(auth.getAccount().getUuid()); + + final Instant truncatedDayTimestamp = Instant.now().truncatedTo(ChronoUnit.DAYS); + + CreateCallLinkCredentialRequest createCallLinkCredentialRequest; + try { + createCallLinkCredentialRequest = new CreateCallLinkCredentialRequest(request.createCallLinkCredentialRequest()); + } catch (InvalidInputException e) { + throw new BadRequestException("Invalid create call link credential request", e); + } + + return new CreateCallLinkCredential( + createCallLinkCredentialRequest.issueCredential(new ServiceId.Aci(auth.getAccount().getUuid()), truncatedDayTimestamp, genericServerSecretParams).serialize(), + truncatedDayTimestamp.getEpochSecond() + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CertificateController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CertificateController.java new file mode 100644 index 000000000..d4568216f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CertificateController.java @@ -0,0 +1,138 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.auth.Auth; +import io.micrometer.core.instrument.Metrics; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.security.InvalidKeyException; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.signal.libsignal.protocol.ServiceId; +import org.signal.libsignal.zkgroup.GenericServerSecretParams; +import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse; +import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations; +import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialResponse; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.CertificateGenerator; +import org.whispersystems.textsecuregcm.entities.DeliveryCertificate; +import org.whispersystems.textsecuregcm.entities.GroupCredentials; +import org.whispersystems.textsecuregcm.identity.IdentityType; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +@Path("/v1/certificate") +@Tag(name = "Certificate") +public class CertificateController { + + private final CertificateGenerator certificateGenerator; + private final ServerZkAuthOperations serverZkAuthOperations; + private final GenericServerSecretParams genericServerSecretParams; + private final Clock clock; + + @VisibleForTesting + public static final Duration MAX_REDEMPTION_DURATION = Duration.ofDays(7); + private static final String GENERATE_DELIVERY_CERTIFICATE_COUNTER_NAME = name(CertificateGenerator.class, "generateCertificate"); + private static final String INCLUDE_E164_TAG_NAME = "includeE164"; + + public CertificateController( + @Nonnull CertificateGenerator certificateGenerator, + @Nonnull ServerZkAuthOperations serverZkAuthOperations, + @Nonnull GenericServerSecretParams genericServerSecretParams, + @Nonnull Clock clock) { + this.certificateGenerator = Objects.requireNonNull(certificateGenerator); + this.serverZkAuthOperations = Objects.requireNonNull(serverZkAuthOperations); + this.genericServerSecretParams = genericServerSecretParams; + this.clock = Objects.requireNonNull(clock); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/delivery") + public DeliveryCertificate getDeliveryCertificate(@Auth AuthenticatedAccount auth, + @QueryParam("includeE164") @DefaultValue("true") boolean includeE164) + throws InvalidKeyException { + + if (auth.getAccount().getIdentityKey(IdentityType.ACI) == null) { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + + Metrics.counter(GENERATE_DELIVERY_CERTIFICATE_COUNTER_NAME, INCLUDE_E164_TAG_NAME, String.valueOf(includeE164)) + .increment(); + + return new DeliveryCertificate( + certificateGenerator.createFor(auth.getAccount(), auth.getAuthenticatedDevice(), includeE164)); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/auth/group") + public GroupCredentials getGroupAuthenticationCredentials( + @Auth AuthenticatedAccount auth, + @QueryParam("redemptionStartSeconds") int startSeconds, + @QueryParam("redemptionEndSeconds") int endSeconds, + @QueryParam("pniAsServiceId") boolean pniAsServiceId) { + + final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS); + final Instant redemptionStart = Instant.ofEpochSecond(startSeconds); + final Instant redemptionEnd = Instant.ofEpochSecond(endSeconds); + + if (redemptionStart.isAfter(redemptionEnd) || + redemptionStart.isBefore(startOfDay) || + redemptionEnd.isAfter(startOfDay.plus(MAX_REDEMPTION_DURATION)) || + !redemptionStart.equals(redemptionStart.truncatedTo(ChronoUnit.DAYS)) || + !redemptionEnd.equals(redemptionEnd.truncatedTo(ChronoUnit.DAYS))) { + + throw new BadRequestException(); + } + + final List credentials = new ArrayList<>(); + final List callLinkAuthCredentials = new ArrayList<>(); + + Instant redemption = redemptionStart; + + ServiceId.Aci aci = new ServiceId.Aci(auth.getAccount().getUuid()); + ServiceId.Pni pni = new ServiceId.Pni(auth.getAccount().getPhoneNumberIdentifier()); + + while (!redemption.isAfter(redemptionEnd)) { + AuthCredentialWithPniResponse authCredentialWithPni; + if (pniAsServiceId) { + authCredentialWithPni = serverZkAuthOperations.issueAuthCredentialWithPniAsServiceId(aci, pni, redemption); + } else { + authCredentialWithPni = serverZkAuthOperations.issueAuthCredentialWithPniAsAci(aci, pni, redemption); + } + credentials.add(new GroupCredentials.GroupCredential( + authCredentialWithPni.serialize(), + (int) redemption.getEpochSecond())); + + callLinkAuthCredentials.add(new GroupCredentials.CallLinkAuthCredential( + CallLinkAuthCredentialResponse.issueCredential(aci, redemption, genericServerSecretParams).serialize(), + redemption.getEpochSecond())); + + redemption = redemption.plus(Duration.ofDays(1)); + } + + + return new GroupCredentials(credentials, callLinkAuthCredentials, pni.getRawUUID()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ChallengeController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ChallengeController.java new file mode 100644 index 000000000..c71933db6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ChallengeController.java @@ -0,0 +1,161 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.common.net.HttpHeaders; +import io.dropwizard.auth.Auth; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.io.IOException; +import javax.validation.Valid; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.entities.AnswerChallengeRequest; +import org.whispersystems.textsecuregcm.entities.AnswerPushChallengeRequest; +import org.whispersystems.textsecuregcm.entities.AnswerRecaptchaChallengeRequest; +import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; +import org.whispersystems.textsecuregcm.util.HeaderUtils; + +@Path("/v1/challenge") +@Tag(name = "Challenge") +public class ChallengeController { + + private final RateLimitChallengeManager rateLimitChallengeManager; + + private static final String CHALLENGE_RESPONSE_COUNTER_NAME = name(ChallengeController.class, "challengeResponse"); + private static final String CHALLENGE_TYPE_TAG = "type"; + + public ChallengeController(final RateLimitChallengeManager rateLimitChallengeManager) { + this.rateLimitChallengeManager = rateLimitChallengeManager; + } + + @PUT + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Operation( + summary = "Submit proof of a challenge completion", + description = """ + Some server endpoints (the "send message" endpoint, for example) may return a 428 response indicating the client must complete a challenge before continuing. + Clients may use this endpoint to provide proof of a completed challenge. If successful, the client may then + continue their original operation. + """, + requestBody = @RequestBody(content = {@Content(schema = @Schema(oneOf = {AnswerPushChallengeRequest.class, + AnswerRecaptchaChallengeRequest.class}))}) + ) + @ApiResponse(responseCode = "200", description = "Indicates the challenge proof was accepted") + @ApiResponse(responseCode = "413", description = "Too many attempts", headers = @Header( + name = "Retry-After", + description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed")) + @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header( + name = "Retry-After", + description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed")) + public Response handleChallengeResponse(@Auth final AuthenticatedAccount auth, + @Valid final AnswerChallengeRequest answerRequest, + @HeaderParam(HttpHeaders.X_FORWARDED_FOR) final String forwardedFor, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent) throws RateLimitExceededException, IOException { + + Tags tags = Tags.of(UserAgentTagUtil.getPlatformTag(userAgent)); + + try { + if (answerRequest instanceof final AnswerPushChallengeRequest pushChallengeRequest) { + tags = tags.and(CHALLENGE_TYPE_TAG, "push"); + + rateLimitChallengeManager.answerPushChallenge(auth.getAccount(), pushChallengeRequest.getChallenge()); + } else if (answerRequest instanceof AnswerRecaptchaChallengeRequest recaptchaChallengeRequest) { + tags = tags.and(CHALLENGE_TYPE_TAG, "recaptcha"); + + final String mostRecentProxy = HeaderUtils.getMostRecentProxy(forwardedFor).orElseThrow(() -> new BadRequestException()); + boolean success = rateLimitChallengeManager.answerRecaptchaChallenge( + auth.getAccount(), + recaptchaChallengeRequest.getCaptcha(), + mostRecentProxy, + userAgent); + + if (!success) { + return Response.status(428).build(); + } + + } else { + tags = tags.and(CHALLENGE_TYPE_TAG, "unrecognized"); + } + } finally { + Metrics.counter(CHALLENGE_RESPONSE_COUNTER_NAME, tags).increment(); + } + + return Response.status(200).build(); + } + + @POST + @Path("/push") + @Operation( + summary = "Request a push challenge", + description = """ + Clients may proactively request a push challenge by making an empty POST request. Push challenges will only be + sent to the requesting account’s main device. When the push is received it may be provided as proof of completed + challenge to /v1/challenge. + APNs challenge payloads will be formatted as follows: + ``` + { + "aps": { + "sound": "default", + "alert": { + "loc-key": "APN_Message" + } + }, + "rateLimitChallenge": "{CHALLENGE_TOKEN}" + } + ``` + FCM challenge payloads will be formatted as follows: + ``` + {"rateLimitChallenge": "{CHALLENGE_TOKEN}"} + ``` + + Clients may retry the PUT in the event of an HTTP/5xx response (except HTTP/508) from the server, but must + implement an exponential back-off system and limit the total number of retries. + """ + ) + @ApiResponse(responseCode = "200", description = """ + Indicates a payload to the account's primary device has been attempted. When clients receive a challenge push + notification, they may issue a PUT request to /v1/challenge. + """) + @ApiResponse(responseCode = "404", description = """ + The server does not have a push notification token for the authenticated account’s main device; clients may add a push + token and try again + """) + @ApiResponse(responseCode = "413", description = "Too many attempts", headers = @Header( + name = "Retry-After", + description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed")) + @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header( + name = "Retry-After", + description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed")) + public Response requestPushChallenge(@Auth final AuthenticatedAccount auth) { + try { + rateLimitChallengeManager.sendPushChallenge(auth.getAccount()); + return Response.status(200).build(); + } catch (final NotPushRegisteredException e) { + return Response.status(404).build(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java new file mode 100644 index 000000000..e0c424fe5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java @@ -0,0 +1,457 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.controllers; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.HttpHeaders; +import io.dropwizard.auth.Auth; +import io.lettuce.core.SetArgs; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.server.ContainerRequest; +import org.whispersystems.textsecuregcm.auth.AuthEnablementRefreshRequirementProvider; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader; +import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.entities.DeviceActivationRequest; +import org.whispersystems.textsecuregcm.entities.DeviceInfo; +import org.whispersystems.textsecuregcm.entities.DeviceInfoList; +import org.whispersystems.textsecuregcm.entities.DeviceResponse; +import org.whispersystems.textsecuregcm.entities.LinkDeviceRequest; +import org.whispersystems.textsecuregcm.entities.PreKeySignatureValidator; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.Util; +import org.whispersystems.textsecuregcm.util.VerificationCode; + +@Path("/v1/devices") +@Tag(name = "Devices") +public class DeviceController { + + static final int MAX_DEVICES = 6; + + private final Key verificationTokenKey; + private final AccountsManager accounts; + private final MessagesManager messages; + private final KeysManager keys; + private final RateLimiters rateLimiters; + private final FaultTolerantRedisCluster usedTokenCluster; + private final Map maxDeviceConfiguration; + + private final Clock clock; + + private static final String VERIFICATION_TOKEN_ALGORITHM = "HmacSHA256"; + + @VisibleForTesting + static final Duration TOKEN_EXPIRATION_DURATION = Duration.ofMinutes(10); + + public DeviceController(byte[] linkDeviceSecret, + AccountsManager accounts, + MessagesManager messages, + KeysManager keys, + RateLimiters rateLimiters, + FaultTolerantRedisCluster usedTokenCluster, + Map maxDeviceConfiguration, final Clock clock) { + this.verificationTokenKey = new SecretKeySpec(linkDeviceSecret, VERIFICATION_TOKEN_ALGORITHM); + this.accounts = accounts; + this.messages = messages; + this.keys = keys; + this.rateLimiters = rateLimiters; + this.usedTokenCluster = usedTokenCluster; + this.maxDeviceConfiguration = maxDeviceConfiguration; + this.clock = clock; + + // Fail fast: reject bad keys + try { + final Mac mac = Mac.getInstance(VERIFICATION_TOKEN_ALGORITHM); + mac.init(verificationTokenKey); + } catch (final NoSuchAlgorithmException e) { + throw new AssertionError("All Java implementations must support HmacSHA256", e); + } catch (final InvalidKeyException e) { + throw new IllegalArgumentException(e); + } + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public DeviceInfoList getDevices(@Auth AuthenticatedAccount auth) { + List devices = new LinkedList<>(); + + for (Device device : auth.getAccount().getDevices()) { + devices.add(new DeviceInfo(device.getId(), device.getName(), + device.getLastSeen(), device.getCreated())); + } + + return new DeviceInfoList(devices); + } + + @DELETE + @Produces(MediaType.APPLICATION_JSON) + @Path("/{device_id}") + @ChangesDeviceEnabledState + public void removeDevice(@Auth AuthenticatedAccount auth, @PathParam("device_id") long deviceId) { + Account account = auth.getAccount(); + if (auth.getAuthenticatedDevice().getId() != Device.MASTER_ID) { + throw new WebApplicationException(Response.Status.UNAUTHORIZED); + } + + if (deviceId == Device.MASTER_ID) { + throw new ForbiddenException(); + } + + final CompletableFuture deleteKeysFuture = keys.delete(account.getUuid(), deviceId); + + messages.clear(account.getUuid(), deviceId).join(); + account = accounts.update(account, a -> a.removeDevice(deviceId)); + // ensure any messages that came in after the first clear() are also removed + messages.clear(account.getUuid(), deviceId).join(); + + deleteKeysFuture.join(); + } + + @GET + @Path("/provisioning/code") + @Produces(MediaType.APPLICATION_JSON) + public VerificationCode createDeviceToken(@Auth AuthenticatedAccount auth) + throws RateLimitExceededException, DeviceLimitExceededException { + + final Account account = auth.getAccount(); + + rateLimiters.getAllocateDeviceLimiter().validate(account.getUuid()); + + int maxDeviceLimit = MAX_DEVICES; + + if (maxDeviceConfiguration.containsKey(account.getNumber())) { + maxDeviceLimit = maxDeviceConfiguration.get(account.getNumber()); + } + + if (account.getEnabledDeviceCount() >= maxDeviceLimit) { + throw new DeviceLimitExceededException(account.getDevices().size(), MAX_DEVICES); + } + + if (auth.getAuthenticatedDevice().getId() != Device.MASTER_ID) { + throw new WebApplicationException(Response.Status.UNAUTHORIZED); + } + + return new VerificationCode(generateVerificationToken(account.getUuid())); + } + + /** + * @deprecated callers should use {@link #linkDevice(BasicAuthorizationHeader, LinkDeviceRequest, ContainerRequest)} + * instead + */ + @PUT + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Path("/{verification_code}") + @ChangesDeviceEnabledState + @Deprecated(forRemoval = true) + public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode, + @HeaderParam(HttpHeaders.AUTHORIZATION) BasicAuthorizationHeader authorizationHeader, + @NotNull @Valid AccountAttributes accountAttributes, + @Context ContainerRequest containerRequest) + throws RateLimitExceededException, DeviceLimitExceededException { + + final Pair accountAndDevice = createDevice(authorizationHeader.getPassword(), + verificationCode, + accountAttributes, + containerRequest, + Optional.empty()); + + final Account account = accountAndDevice.first(); + final Device device = accountAndDevice.second(); + + return new DeviceResponse(account.getUuid(), account.getPhoneNumberIdentifier(), device.getId()); + } + + @PUT + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Path("/link") + @ChangesDeviceEnabledState + @Operation(summary = "Link a device to an account", + description = """ + Links a device to an account identified by a given phone number. + """) + @ApiResponse(responseCode = "200", description = "The new device was linked to the calling account", useReturnTypeSchema = true) + @ApiResponse(responseCode = "403", description = "The given account was not found or the given verification code was incorrect") + @ApiResponse(responseCode = "411", description = "The given account already has its maximum number of linked devices") + @ApiResponse(responseCode = "422", description = "The request did not pass validation") + @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header( + name = "Retry-After", + description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed")) + public DeviceResponse linkDevice(@HeaderParam(HttpHeaders.AUTHORIZATION) BasicAuthorizationHeader authorizationHeader, + @NotNull @Valid LinkDeviceRequest linkDeviceRequest, + @Context ContainerRequest containerRequest) + throws RateLimitExceededException, DeviceLimitExceededException { + + final Pair accountAndDevice = createDevice(authorizationHeader.getPassword(), + linkDeviceRequest.verificationCode(), + linkDeviceRequest.accountAttributes(), + containerRequest, + Optional.of(linkDeviceRequest.deviceActivationRequest())); + + final Account account = accountAndDevice.first(); + final Device device = accountAndDevice.second(); + + return new DeviceResponse(account.getUuid(), account.getPhoneNumberIdentifier(), device.getId()); + } + + @PUT + @Produces(MediaType.APPLICATION_JSON) + @Path("/unauthenticated_delivery") + public void setUnauthenticatedDelivery(@Auth AuthenticatedAccount auth) { + assert (auth.getAuthenticatedDevice() != null); + // Deprecated + } + + @PUT + @Produces(MediaType.APPLICATION_JSON) + @Path("/capabilities") + public void setCapabilities(@Auth AuthenticatedAccount auth, @NotNull @Valid DeviceCapabilities capabilities) { + assert (auth.getAuthenticatedDevice() != null); + final long deviceId = auth.getAuthenticatedDevice().getId(); + accounts.updateDevice(auth.getAccount(), deviceId, d -> d.setCapabilities(capabilities)); + } + + private Mac getInitializedMac() { + try { + final Mac mac = Mac.getInstance(VERIFICATION_TOKEN_ALGORITHM); + mac.init(verificationTokenKey); + + return mac; + } catch (final NoSuchAlgorithmException | InvalidKeyException e) { + // All Java implementations must support HmacSHA256 and we checked the key at construction time, so this can never + // happen + throw new AssertionError(e); + } + } + + @VisibleForTesting + String generateVerificationToken(final UUID aci) { + final String claims = aci + "." + clock.instant().toEpochMilli(); + final byte[] signature = getInitializedMac().doFinal(claims.getBytes(StandardCharsets.UTF_8)); + + return claims + ":" + Base64.getUrlEncoder().encodeToString(signature); + } + + @VisibleForTesting + Optional checkVerificationToken(final String verificationToken) { + final boolean tokenUsed = usedTokenCluster.withCluster(connection -> + connection.sync().get(getUsedTokenKey(verificationToken)) != null); + + if (tokenUsed) { + return Optional.empty(); + } + + final String[] claimsAndSignature = verificationToken.split(":", 2); + + if (claimsAndSignature.length != 2) { + return Optional.empty(); + } + + final byte[] expectedSignature = getInitializedMac().doFinal(claimsAndSignature[0].getBytes(StandardCharsets.UTF_8)); + final byte[] providedSignature; + + try { + providedSignature = Base64.getUrlDecoder().decode(claimsAndSignature[1]); + } catch (final IllegalArgumentException e) { + return Optional.empty(); + } + + if (!MessageDigest.isEqual(expectedSignature, providedSignature)) { + return Optional.empty(); + } + + final String[] aciAndTimestamp = claimsAndSignature[0].split("\\.", 2); + + if (aciAndTimestamp.length != 2) { + return Optional.empty(); + } + + final UUID aci; + + try { + aci = UUID.fromString(aciAndTimestamp[0]); + } catch (final IllegalArgumentException e) { + return Optional.empty(); + } + + final Instant timestamp; + + try { + timestamp = Instant.ofEpochMilli(Long.parseLong(aciAndTimestamp[1])); + } catch (final NumberFormatException e) { + return Optional.empty(); + } + + final Instant tokenExpiration = timestamp.plus(TOKEN_EXPIRATION_DURATION); + + if (tokenExpiration.isBefore(clock.instant())) { + return Optional.empty(); + } + + return Optional.of(aci); + } + + static boolean isCapabilityDowngrade(Account account, DeviceCapabilities capabilities) { + return account.isPniSupported() && !capabilities.pni(); + } + + private Pair createDevice(final String password, + final String verificationCode, + final AccountAttributes accountAttributes, + final ContainerRequest containerRequest, + final Optional maybeDeviceActivationRequest) + throws RateLimitExceededException, DeviceLimitExceededException { + + final Optional maybeAciFromToken = checkVerificationToken(verificationCode); + + final Account account = maybeAciFromToken.flatMap(accounts::getByAccountIdentifier) + .orElseThrow(ForbiddenException::new); + + rateLimiters.getVerifyDeviceLimiter().validate(account.getUuid()); + + maybeDeviceActivationRequest.ifPresent(deviceActivationRequest -> { + assert deviceActivationRequest.aciSignedPreKey().isPresent(); + assert deviceActivationRequest.pniSignedPreKey().isPresent(); + assert deviceActivationRequest.aciPqLastResortPreKey().isPresent(); + assert deviceActivationRequest.pniPqLastResortPreKey().isPresent(); + + final boolean allKeysValid = PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey( + IdentityType.ACI), + List.of(deviceActivationRequest.aciSignedPreKey().get(), deviceActivationRequest.aciPqLastResortPreKey().get())) + && PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.PNI), + List.of(deviceActivationRequest.pniSignedPreKey().get(), deviceActivationRequest.pniPqLastResortPreKey().get())); + + if (!allKeysValid) { + throw new WebApplicationException(Response.status(422).build()); + } + }); + + // Normally, the "do we need to refresh somebody's websockets" listener can do this on its own. In this case, + // we're not using the conventional authentication system, and so we need to give it a hint so it knows who the + // active user is and what their device states look like. + AuthEnablementRefreshRequirementProvider.setAccount(containerRequest, account); + + int maxDeviceLimit = MAX_DEVICES; + + if (maxDeviceConfiguration.containsKey(account.getNumber())) { + maxDeviceLimit = maxDeviceConfiguration.get(account.getNumber()); + } + + if (account.getEnabledDeviceCount() >= maxDeviceLimit) { + throw new DeviceLimitExceededException(account.getDevices().size(), MAX_DEVICES); + } + + final DeviceCapabilities capabilities = accountAttributes.getCapabilities(); + if (capabilities != null && isCapabilityDowngrade(account, capabilities)) { + throw new WebApplicationException(Response.status(409).build()); + } + + final Device device = new Device(); + device.setName(accountAttributes.getName()); + device.setAuthTokenHash(SaltedTokenHash.generateFor(password)); + device.setFetchesMessages(accountAttributes.getFetchesMessages()); + device.setRegistrationId(accountAttributes.getRegistrationId()); + accountAttributes.getPhoneNumberIdentityRegistrationId().ifPresent(device::setPhoneNumberIdentityRegistrationId); + device.setLastSeen(Util.todayInMillis()); + device.setCreated(System.currentTimeMillis()); + device.setCapabilities(accountAttributes.getCapabilities()); + + maybeDeviceActivationRequest.ifPresent(deviceActivationRequest -> { + device.setSignedPreKey(deviceActivationRequest.aciSignedPreKey().get()); + device.setPhoneNumberIdentitySignedPreKey(deviceActivationRequest.pniSignedPreKey().get()); + + deviceActivationRequest.apnToken().ifPresent(apnRegistrationId -> { + device.setApnId(apnRegistrationId.apnRegistrationId()); + device.setVoipApnId(apnRegistrationId.voipRegistrationId()); + }); + + deviceActivationRequest.gcmToken().ifPresent(gcmRegistrationId -> + device.setGcmId(gcmRegistrationId.gcmRegistrationId())); + }); + + final Account updatedAccount = accounts.update(account, a -> { + device.setId(a.getNextDeviceId()); + + final CompletableFuture deleteKeysFuture = CompletableFuture.allOf( + keys.delete(a.getUuid(), device.getId()), + keys.delete(a.getPhoneNumberIdentifier(), device.getId())); + + messages.clear(a.getUuid(), device.getId()).join(); + + deleteKeysFuture.join(); + + maybeDeviceActivationRequest.ifPresent(deviceActivationRequest -> CompletableFuture.allOf( + keys.storeEcSignedPreKeys(a.getUuid(), + Map.of(device.getId(), deviceActivationRequest.aciSignedPreKey().get())), + keys.storePqLastResort(a.getUuid(), + Map.of(device.getId(), deviceActivationRequest.aciPqLastResortPreKey().get())), + keys.storeEcSignedPreKeys(a.getPhoneNumberIdentifier(), + Map.of(device.getId(), deviceActivationRequest.pniSignedPreKey().get())), + keys.storePqLastResort(a.getPhoneNumberIdentifier(), + Map.of(device.getId(), deviceActivationRequest.pniPqLastResortPreKey().get()))) + .join()); + + a.addDevice(device); + }); + + if (maybeAciFromToken.isPresent()) { + usedTokenCluster.useCluster(connection -> + connection.sync().set(getUsedTokenKey(verificationCode), "", new SetArgs().ex(TOKEN_EXPIRATION_DURATION))); + } + + return new Pair<>(updatedAccount, device); + } + + private static String getUsedTokenKey(final String token) { + return "usedToken::" + token; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceLimitExceededException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceLimitExceededException.java new file mode 100644 index 000000000..457c5d2cf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceLimitExceededException.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + + +public class DeviceLimitExceededException extends Exception { + + private final int currentDevices; + private final int maxDevices; + + public DeviceLimitExceededException(int currentDevices, int maxDevices) { + this.currentDevices = currentDevices; + this.maxDevices = maxDevices; + } + + public int getCurrentDevices() { + return currentDevices; + } + + public int getMaxDevices() { + return maxDevices; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryV2Controller.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryV2Controller.java new file mode 100644 index 000000000..5bd862cda --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryV2Controller.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.controllers; + +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.time.Clock; +import java.util.UUID; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.DirectoryV2ClientConfiguration; + +@Path("/v2/directory") +@Tag(name = "Directory") +public class DirectoryV2Controller { + + private final ExternalServiceCredentialsGenerator directoryServiceTokenGenerator; + + @VisibleForTesting + public static ExternalServiceCredentialsGenerator credentialsGenerator(final DirectoryV2ClientConfiguration cfg, + final Clock clock) { + return ExternalServiceCredentialsGenerator + .builder(cfg.userAuthenticationTokenSharedSecret()) + .withUserDerivationKey(cfg.userIdTokenSharedSecret()) + .prependUsername(false) + .withClock(clock) + .build(); + } + + public static ExternalServiceCredentialsGenerator credentialsGenerator(final DirectoryV2ClientConfiguration cfg) { + return credentialsGenerator(cfg, Clock.systemUTC()); + } + + public DirectoryV2Controller(final ExternalServiceCredentialsGenerator userTokenGenerator) { + this.directoryServiceTokenGenerator = userTokenGenerator; + } + + @GET + @Path("/auth") + @Produces(MediaType.APPLICATION_JSON) + public Response getAuthToken(final @Auth AuthenticatedAccount auth) { + final UUID uuid = auth.getAccount().getUuid(); + final ExternalServiceCredentials credentials = directoryServiceTokenGenerator.generateForUuid(uuid); + return Response.ok().entity(credentials).build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DonationController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DonationController.java new file mode 100644 index 000000000..a4282c6e8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DonationController.java @@ -0,0 +1,145 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.time.Clock; +import java.time.Instant; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinPool.ManagedBlocker; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.VerificationFailedException; +import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation; +import org.signal.libsignal.zkgroup.receipts.ReceiptSerial; +import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; +import org.whispersystems.textsecuregcm.entities.RedeemReceiptRequest; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountBadge; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager; + +@Path("/v1/donation") +@Tag(name = "Donations") +public class DonationController { + + public interface ReceiptCredentialPresentationFactory { + ReceiptCredentialPresentation build(byte[] bytes) throws InvalidInputException; + } + + private static final Logger logger = LoggerFactory.getLogger(DonationController.class); + + private final Clock clock; + private final ServerZkReceiptOperations serverZkReceiptOperations; + private final RedeemedReceiptsManager redeemedReceiptsManager; + private final AccountsManager accountsManager; + private final BadgesConfiguration badgesConfiguration; + private final ReceiptCredentialPresentationFactory receiptCredentialPresentationFactory; + + public DonationController( + @Nonnull final Clock clock, + @Nonnull final ServerZkReceiptOperations serverZkReceiptOperations, + @Nonnull final RedeemedReceiptsManager redeemedReceiptsManager, + @Nonnull final AccountsManager accountsManager, + @Nonnull final BadgesConfiguration badgesConfiguration, + @Nonnull final ReceiptCredentialPresentationFactory receiptCredentialPresentationFactory) { + this.clock = Objects.requireNonNull(clock); + this.serverZkReceiptOperations = Objects.requireNonNull(serverZkReceiptOperations); + this.redeemedReceiptsManager = Objects.requireNonNull(redeemedReceiptsManager); + this.accountsManager = Objects.requireNonNull(accountsManager); + this.badgesConfiguration = Objects.requireNonNull(badgesConfiguration); + this.receiptCredentialPresentationFactory = Objects.requireNonNull(receiptCredentialPresentationFactory); + } + + @POST + @Path("/redeem-receipt") + @Consumes(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) + public CompletionStage redeemReceipt( + @Auth final AuthenticatedAccount auth, + @NotNull @Valid final RedeemReceiptRequest request) { + return CompletableFuture.supplyAsync(() -> { + ReceiptCredentialPresentation receiptCredentialPresentation; + try { + receiptCredentialPresentation = receiptCredentialPresentationFactory.build( + request.getReceiptCredentialPresentation()); + } catch (InvalidInputException e) { + return CompletableFuture.completedFuture(Response.status(Status.BAD_REQUEST).entity("invalid receipt credential presentation").type(MediaType.TEXT_PLAIN_TYPE).build()); + } + try { + serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation); + } catch (VerificationFailedException e) { + return CompletableFuture.completedFuture(Response.status(Status.BAD_REQUEST).entity("receipt credential presentation verification failed").type(MediaType.TEXT_PLAIN_TYPE).build()); + } + + final ReceiptSerial receiptSerial = receiptCredentialPresentation.getReceiptSerial(); + final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime()); + final long receiptLevel = receiptCredentialPresentation.getReceiptLevel(); + final String badgeId = badgesConfiguration.getReceiptLevels().get(receiptLevel); + if (badgeId == null) { + return CompletableFuture.completedFuture(Response.serverError().entity("server does not recognize the requested receipt level").type(MediaType.TEXT_PLAIN_TYPE).build()); + } + final CompletionStage putStage = redeemedReceiptsManager.put( + receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, auth.getAccount().getUuid()); + return putStage.thenApplyAsync(receiptMatched -> { + if (!receiptMatched) { + return Response.status(Status.BAD_REQUEST).entity("receipt serial is already redeemed").type(MediaType.TEXT_PLAIN_TYPE).build(); + } + + try { + ForkJoinPool.managedBlock(new ManagedBlocker() { + boolean done = false; + + @Override + public boolean block() { + final Optional optionalAccount = accountsManager.getByAccountIdentifier(auth.getAccount().getUuid()); + optionalAccount.ifPresent(account -> { + accountsManager.update(account, a -> { + a.addBadge(clock, new AccountBadge(badgeId, receiptExpiration, request.isVisible())); + if (request.isPrimary()) { + a.makeBadgePrimaryIfExists(clock, badgeId); + } + }); + }); + done = true; + return true; + } + + @Override + public boolean isReleasable() { + return done; + } + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return Response.serverError().build(); + } + + return Response.ok().build(); + }); + }).thenCompose(Function.identity()); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeepAliveController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeepAliveController.java new file mode 100644 index 000000000..96849ab62 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeepAliveController.java @@ -0,0 +1,70 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static com.codahale.metrics.MetricRegistry.name; + +import io.dropwizard.auth.Auth; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Optional; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.websocket.session.WebSocketSession; +import org.whispersystems.websocket.session.WebSocketSessionContext; + + +@Path("/v1/keepalive") +@Tag(name = "Keep Alive") +public class KeepAliveController { + + private final Logger logger = LoggerFactory.getLogger(KeepAliveController.class); + + private final ClientPresenceManager clientPresenceManager; + + private static final String NO_LOCAL_SUBSCRIPTION_COUNTER_NAME = name(KeepAliveController.class, + "noLocalSubscription"); + + public KeepAliveController(final ClientPresenceManager clientPresenceManager) { + this.clientPresenceManager = clientPresenceManager; + } + + @GET + public Response getKeepAlive(@Auth Optional maybeAuth, + @WebSocketSession WebSocketSessionContext context) { + + maybeAuth.ifPresent(auth -> { + if (!clientPresenceManager.isLocallyPresent(auth.getAccount().getUuid(), auth.getAuthenticatedDevice().getId())) { + logger.debug("***** No local subscription found for {}::{}; age = {}ms, User-Agent = {}", + auth.getAccount().getUuid(), auth.getAuthenticatedDevice().getId(), + System.currentTimeMillis() - context.getClient().getCreatedTimestamp(), + context.getClient().getUserAgent()); + + context.getClient().close(1000, "OK"); + + Metrics.counter(NO_LOCAL_SUBSCRIPTION_COUNTER_NAME, + Tags.of(UserAgentTagUtil.getPlatformTag(context.getClient().getUserAgent()))) + .increment(); + } + }); + + return Response.ok().build(); + } + + @GET + @Path("/provisioning") + public Response getProvisioningKeepAlive() { + return Response.ok().build(); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java new file mode 100644 index 000000000..11a2f4bb4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java @@ -0,0 +1,341 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.controllers; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.google.common.net.HttpHeaders; +import io.dropwizard.auth.Auth; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.ws.rs.Consumes; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.signal.libsignal.protocol.IdentityKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.Anonymous; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.OptionalAccess; +import org.whispersystems.textsecuregcm.entities.ECPreKey; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.entities.PreKeyCount; +import org.whispersystems.textsecuregcm.entities.PreKeyResponse; +import org.whispersystems.textsecuregcm.entities.PreKeyResponseItem; +import org.whispersystems.textsecuregcm.entities.PreKeyState; +import org.whispersystems.textsecuregcm.experiment.Experiment; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import org.whispersystems.textsecuregcm.util.Util; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +@Path("/v2/keys") +@Tag(name = "Keys") +public class KeysController { + + private final RateLimiters rateLimiters; + private final KeysManager keys; + private final AccountsManager accounts; + private final Experiment compareSignedEcPreKeysExperiment = new Experiment("compareSignedEcPreKeys"); + + private static final String IDENTITY_KEY_CHANGE_COUNTER_NAME = name(KeysController.class, "identityKeyChange"); + private static final String IDENTITY_KEY_CHANGE_FORBIDDEN_COUNTER_NAME = name(KeysController.class, "identityKeyChangeForbidden"); + + private static final String IDENTITY_TYPE_TAG_NAME = "identityType"; + private static final String HAS_IDENTITY_KEY_TAG_NAME = "hasIdentityKey"; + + private static final Logger logger = LoggerFactory.getLogger(KeysController.class); + + public KeysController(RateLimiters rateLimiters, KeysManager keys, AccountsManager accounts) { + this.rateLimiters = rateLimiters; + this.keys = keys; + this.accounts = accounts; + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Get prekey count", + description = "Gets the number of one-time prekeys uploaded for this device and still available") + @ApiResponse(responseCode = "200", description = "Body contains the number of available one-time prekeys for the device.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "401", description = "Account authentication check failed.") + public CompletableFuture getStatus(@Auth final AuthenticatedAccount auth, + @QueryParam("identity") final Optional identityType) { + + final CompletableFuture ecCountFuture = + keys.getEcCount(getIdentifier(auth.getAccount(), identityType), auth.getAuthenticatedDevice().getId()); + + final CompletableFuture pqCountFuture = + keys.getPqCount(getIdentifier(auth.getAccount(), identityType), auth.getAuthenticatedDevice().getId()); + + return ecCountFuture.thenCombine(pqCountFuture, PreKeyCount::new); + } + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ChangesDeviceEnabledState + @Operation(summary = "Upload new prekeys", + description = """ + Upload new prekeys for this device. Can also be used, from the primary device only, to set the account's identity + key, but this is deprecated now that accounts can be created atomically. + """) + @ApiResponse(responseCode = "200", description = "Indicates that new keys were successfully stored.") + @ApiResponse(responseCode = "401", description = "Account authentication check failed.") + @ApiResponse(responseCode = "403", description = "Attempt to change identity key from a non-primary device.") + @ApiResponse(responseCode = "422", description = "Invalid request format.") + public CompletableFuture setKeys(@Auth final DisabledPermittedAuthenticatedAccount disabledPermittedAuth, + @RequestBody @NotNull @Valid final PreKeyState preKeys, + + @Parameter(allowEmptyValue=true) + @Schema( + allowableValues={"aci", "pni"}, + defaultValue="aci", + description="whether this operation applies to the account (aci) or phone-number (pni) identity") + @QueryParam("identity") final Optional identityType, + + @HeaderParam(HttpHeaders.USER_AGENT) String userAgent) { + Account account = disabledPermittedAuth.getAccount(); + Device device = disabledPermittedAuth.getAuthenticatedDevice(); + boolean updateAccount = false; + + final boolean usePhoneNumberIdentity = usePhoneNumberIdentity(identityType); + + if (preKeys.getSignedPreKey() != null && + !preKeys.getSignedPreKey().equals(usePhoneNumberIdentity ? device.getSignedPreKey(IdentityType.PNI) + : device.getSignedPreKey(IdentityType.ACI))) { + updateAccount = true; + } + + final IdentityKey oldIdentityKey = + usePhoneNumberIdentity ? account.getIdentityKey(IdentityType.PNI) : account.getIdentityKey(IdentityType.ACI); + if (!Objects.equals(preKeys.getIdentityKey(), oldIdentityKey)) { + updateAccount = true; + + final boolean hasIdentityKey = oldIdentityKey != null; + final Tags tags = Tags.of(UserAgentTagUtil.getPlatformTag(userAgent)) + .and(HAS_IDENTITY_KEY_TAG_NAME, String.valueOf(hasIdentityKey)) + .and(IDENTITY_TYPE_TAG_NAME, usePhoneNumberIdentity ? "pni" : "aci"); + + if (!device.isMaster()) { + Metrics.counter(IDENTITY_KEY_CHANGE_FORBIDDEN_COUNTER_NAME, tags).increment(); + + throw new ForbiddenException(); + } + Metrics.counter(IDENTITY_KEY_CHANGE_COUNTER_NAME, tags).increment(); + + if (hasIdentityKey) { + logger.warn("Existing {} identity key changed; account age is {} days", + identityType.orElse("aci"), + Duration.between(Instant.ofEpochMilli(device.getCreated()), Instant.now()).toDays()); + } + } + + if (updateAccount) { + account = accounts.update(account, a -> { + if (preKeys.getSignedPreKey() != null) { + a.getDevice(device.getId()).ifPresent(d -> { + if (usePhoneNumberIdentity) { + d.setPhoneNumberIdentitySignedPreKey(preKeys.getSignedPreKey()); + } else { + d.setSignedPreKey(preKeys.getSignedPreKey()); + } + }); + } + + if (usePhoneNumberIdentity) { + a.setPhoneNumberIdentityKey(preKeys.getIdentityKey()); + } else { + a.setIdentityKey(preKeys.getIdentityKey()); + } + }); + } + + return keys.store(getIdentifier(account, identityType), device.getId(), + preKeys.getPreKeys(), preKeys.getPqPreKeys(), preKeys.getSignedPreKey(), preKeys.getPqLastResortPreKey()); + } + + @GET + @Path("/{identifier}/{device_id}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Fetch public keys for another user", + description = "Retrieves the public identity key and available device prekeys for a specified account or phone-number identity") + @ApiResponse(responseCode = "200", description = "Indicates at least one prekey was available for at least one requested device.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "401", description = "Account authentication check failed and unidentified-access key was not supplied or invalid.") + @ApiResponse(responseCode = "404", description = "Requested identity or device does not exist, is not active, or has no available prekeys.") + @ApiResponse(responseCode = "429", description = "Rate limit exceeded.", headers = @Header( + name = "Retry-After", + description = "If present, a positive integer indicating the number of seconds before a subsequent attempt could succeed")) + public PreKeyResponse getDeviceKeys(@Auth Optional auth, + @HeaderParam(OptionalAccess.UNIDENTIFIED) Optional accessKey, + + @Parameter(description="the account or phone-number identifier to retrieve keys for") + @PathParam("identifier") ServiceIdentifier targetIdentifier, + + @Parameter(description="the device id of a single device to retrieve prekeys for, or `*` for all enabled devices") + @PathParam("device_id") String deviceId, + + @Parameter(allowEmptyValue=true, description="whether to retrieve post-quantum prekeys") + @Schema(defaultValue="false") + @QueryParam("pq") boolean returnPqKey, + + @HeaderParam(HttpHeaders.USER_AGENT) String userAgent) + throws RateLimitExceededException { + + if (auth.isEmpty() && accessKey.isEmpty()) { + throw new WebApplicationException(Response.Status.UNAUTHORIZED); + } + + final Optional account = auth.map(AuthenticatedAccount::getAccount); + + final Account target; + { + final Optional maybeTarget = accounts.getByServiceIdentifier(targetIdentifier); + + OptionalAccess.verify(account, accessKey, maybeTarget, deviceId); + + target = maybeTarget.orElseThrow(); + } + + if (account.isPresent()) { + rateLimiters.getPreKeysLimiter().validate( + account.get().getUuid() + "." + auth.get().getAuthenticatedDevice().getId() + "__" + targetIdentifier.uuid() + + "." + deviceId); + } + + final List devices = parseDeviceId(deviceId, target); + final List responseItems = new ArrayList<>(devices.size()); + + final List> tasks = devices.stream().map(device -> { + + ECSignedPreKey signedECPreKey = device.getSignedPreKey(targetIdentifier.identityType()); + + final CompletableFuture> unsignedEcPreKeyFuture = keys.takeEC(targetIdentifier.uuid(), + device.getId()); + final CompletableFuture> pqPreKeyFuture = returnPqKey + ? keys.takePQ(targetIdentifier.uuid(), device.getId()) + : CompletableFuture.completedFuture(Optional.empty()); + + return pqPreKeyFuture.thenCombine(unsignedEcPreKeyFuture, + (maybePqPreKey, maybeUnsignedEcPreKey) -> { + + KEMSignedPreKey pqPreKey = pqPreKeyFuture.join().orElse(null); + ECPreKey unsignedECPreKey = unsignedEcPreKeyFuture.join().orElse(null); + + compareSignedEcPreKeysExperiment.compareFutureResult(Optional.ofNullable(signedECPreKey), + keys.getEcSignedPreKey(targetIdentifier.uuid(), device.getId())); + + if (signedECPreKey != null || unsignedECPreKey != null || pqPreKey != null) { + final int registrationId = switch (targetIdentifier.identityType()) { + case ACI -> device.getRegistrationId(); + case PNI -> device.getPhoneNumberIdentityRegistrationId().orElse(device.getRegistrationId()); + }; + + responseItems.add( + new PreKeyResponseItem(device.getId(), registrationId, signedECPreKey, unsignedECPreKey, + pqPreKey)); + } + + return null; + }).thenRun(Util.NOOP); + }) + .toList(); + + CompletableFuture.allOf(tasks.toArray(new CompletableFuture[0])).join(); + + final IdentityKey identityKey = target.getIdentityKey(targetIdentifier.identityType()); + + if (responseItems.isEmpty()) { + throw new WebApplicationException(Response.Status.NOT_FOUND); + } + return new PreKeyResponse(identityKey, responseItems); + } + + @PUT + @Path("/signed") + @Consumes(MediaType.APPLICATION_JSON) + @ChangesDeviceEnabledState + @Operation(summary = "Upload a new signed prekey", + description = """ + Upload a new signed elliptic-curve prekey for this device. Deprecated; use PUT /v2/keys with instead. + """) + @ApiResponse(responseCode = "200", description = "Indicates that new prekey was successfully stored.") + @ApiResponse(responseCode = "401", description = "Account authentication check failed.") + @ApiResponse(responseCode = "422", description = "Invalid request format.") + public CompletableFuture setSignedKey(@Auth final AuthenticatedAccount auth, + @Valid final ECSignedPreKey signedPreKey, + @QueryParam("identity") final Optional identityType) { + + Device device = auth.getAuthenticatedDevice(); + + accounts.updateDevice(auth.getAccount(), device.getId(), d -> { + if (usePhoneNumberIdentity(identityType)) { + d.setPhoneNumberIdentitySignedPreKey(signedPreKey); + } else { + d.setSignedPreKey(signedPreKey); + } + }); + + return keys.storeEcSignedPreKeys(getIdentifier(auth.getAccount(), identityType), + Map.of(device.getId(), signedPreKey)); + } + + private static boolean usePhoneNumberIdentity(final Optional identityType) { + return "pni".equals(identityType.map(String::toLowerCase).orElse("aci")); + } + + private static UUID getIdentifier(final Account account, final Optional identityType) { + return usePhoneNumberIdentity(identityType) ? + account.getPhoneNumberIdentifier() : + account.getUuid(); + } + + private List parseDeviceId(String deviceId, Account account) { + if (deviceId.equals("*")) { + return account.getDevices().stream().filter(Device::isEnabled).toList(); + } + try { + long id = Long.parseLong(deviceId); + return account.getDevice(id).filter(Device::isEnabled).map(List::of).orElse(List.of()); + } catch (NumberFormatException e) { + throw new WebApplicationException(Response.status(422).build()); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java new file mode 100644 index 000000000..85f4ab601 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java @@ -0,0 +1,793 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.controllers; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.codahale.metrics.annotation.Timed; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.HttpHeaders; +import com.google.protobuf.ByteString; +import io.dropwizard.auth.Auth; +import io.dropwizard.util.DataSize; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import java.security.MessageDigest; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.Anonymous; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.CombinedUnidentifiedSenderAccessKeys; +import org.whispersystems.textsecuregcm.auth.OptionalAccess; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.entities.AccountMismatchedDevices; +import org.whispersystems.textsecuregcm.entities.AccountStaleDevices; +import org.whispersystems.textsecuregcm.entities.IncomingMessage; +import org.whispersystems.textsecuregcm.entities.IncomingMessageList; +import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope; +import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type; +import org.whispersystems.textsecuregcm.entities.MismatchedDevices; +import org.whispersystems.textsecuregcm.entities.MultiRecipientMessage; +import org.whispersystems.textsecuregcm.entities.MultiRecipientMessage.Recipient; +import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; +import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList; +import org.whispersystems.textsecuregcm.entities.SendMessageResponse; +import org.whispersystems.textsecuregcm.entities.SendMultiRecipientMessageResponse; +import org.whispersystems.textsecuregcm.entities.SpamReport; +import org.whispersystems.textsecuregcm.entities.StaleDevices; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.limits.CardinalityEstimator; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.metrics.MessageMetrics; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.providers.MultiRecipientMessageProvider; +import org.whispersystems.textsecuregcm.push.MessageSender; +import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; +import org.whispersystems.textsecuregcm.push.PushNotificationManager; +import org.whispersystems.textsecuregcm.push.ReceiptSender; +import org.whispersystems.textsecuregcm.spam.FilterSpam; +import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.storage.ReportMessageManager; +import org.whispersystems.textsecuregcm.util.DestinationDeviceValidator; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.Util; +import org.whispersystems.textsecuregcm.websocket.WebSocketConnection; +import org.whispersystems.websocket.Stories; +import reactor.core.scheduler.Scheduler; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +@Path("/v1/messages") +@io.swagger.v3.oas.annotations.tags.Tag(name = "Messages") +public class MessageController { + + private static final Logger logger = LoggerFactory.getLogger(MessageController.class); + + private final RateLimiters rateLimiters; + private final CardinalityEstimator messageByteLimitEstimator; + private final MessageSender messageSender; + private final ReceiptSender receiptSender; + private final AccountsManager accountsManager; + private final MessagesManager messagesManager; + private final PushNotificationManager pushNotificationManager; + private final ReportMessageManager reportMessageManager; + private final ExecutorService multiRecipientMessageExecutor; + private final Scheduler messageDeliveryScheduler; + private final ReportSpamTokenProvider reportSpamTokenProvider; + private final ClientReleaseManager clientReleaseManager; + private final DynamicConfigurationManager dynamicConfigurationManager; + + private static final String REJECT_OVERSIZE_MESSAGE_COUNTER = name(MessageController.class, "rejectOversizeMessage"); + private static final String SENT_MESSAGE_COUNTER_NAME = name(MessageController.class, "sentMessages"); + private static final String CONTENT_SIZE_DISTRIBUTION_NAME = name(MessageController.class, "messageContentSize"); + private static final String OUTGOING_MESSAGE_LIST_SIZE_BYTES_DISTRIBUTION_NAME = name(MessageController.class, "outgoingMessageListSizeBytes"); + private static final String RATE_LIMITED_MESSAGE_COUNTER_NAME = name(MessageController.class, "rateLimitedMessage"); + private static final String RATE_LIMITED_STORIES_COUNTER_NAME = name(MessageController.class, "rateLimitedStory"); + + private static final String REJECT_INVALID_ENVELOPE_TYPE = name(MessageController.class, "rejectInvalidEnvelopeType"); + + private static final String EPHEMERAL_TAG_NAME = "ephemeral"; + private static final String SENDER_TYPE_TAG_NAME = "senderType"; + private static final String SENDER_COUNTRY_TAG_NAME = "senderCountry"; + private static final String RATE_LIMIT_REASON_TAG_NAME = "rateLimitReason"; + private static final String ENVELOPE_TYPE_TAG_NAME = "envelopeType"; + + private static final String SENDER_TYPE_IDENTIFIED = "identified"; + private static final String SENDER_TYPE_UNIDENTIFIED = "unidentified"; + private static final String SENDER_TYPE_SELF = "self"; + + @VisibleForTesting + static final long MAX_MESSAGE_SIZE = DataSize.kibibytes(256).toBytes(); + + public MessageController( + RateLimiters rateLimiters, + CardinalityEstimator messageByteLimitEstimator, + MessageSender messageSender, + ReceiptSender receiptSender, + AccountsManager accountsManager, + MessagesManager messagesManager, + PushNotificationManager pushNotificationManager, + ReportMessageManager reportMessageManager, + @Nonnull ExecutorService multiRecipientMessageExecutor, + Scheduler messageDeliveryScheduler, + @Nonnull ReportSpamTokenProvider reportSpamTokenProvider, + final ClientReleaseManager clientReleaseManager, + final DynamicConfigurationManager dynamicConfigurationManager) { + this.rateLimiters = rateLimiters; + this.messageByteLimitEstimator = messageByteLimitEstimator; + this.messageSender = messageSender; + this.receiptSender = receiptSender; + this.accountsManager = accountsManager; + this.messagesManager = messagesManager; + this.pushNotificationManager = pushNotificationManager; + this.reportMessageManager = reportMessageManager; + this.multiRecipientMessageExecutor = Objects.requireNonNull(multiRecipientMessageExecutor); + this.messageDeliveryScheduler = messageDeliveryScheduler; + this.reportSpamTokenProvider = reportSpamTokenProvider; + this.clientReleaseManager = clientReleaseManager; + this.dynamicConfigurationManager = dynamicConfigurationManager; + } + + @Timed + @Path("/{destination}") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @FilterSpam + public Response sendMessage(@Auth Optional source, + @HeaderParam(OptionalAccess.UNIDENTIFIED) Optional accessKey, + @HeaderParam(HttpHeaders.USER_AGENT) String userAgent, + @HeaderParam(HttpHeaders.X_FORWARDED_FOR) String forwardedFor, + @PathParam("destination") ServiceIdentifier destinationIdentifier, + @QueryParam("story") boolean isStory, + @NotNull @Valid IncomingMessageList messages, + @Context ContainerRequestContext context) throws RateLimitExceededException { + + if (source.isEmpty() && accessKey.isEmpty() && !isStory) { + throw new WebApplicationException(Response.Status.UNAUTHORIZED); + } + + final String senderType; + + if (source.isPresent()) { + if (source.get().getAccount().isIdentifiedBy(destinationIdentifier)) { + senderType = SENDER_TYPE_SELF; + } else { + senderType = SENDER_TYPE_IDENTIFIED; + } + } else { + senderType = SENDER_TYPE_UNIDENTIFIED; + } + + final Optional spamReportToken; + if (senderType.equals(SENDER_TYPE_IDENTIFIED)) { + spamReportToken = reportSpamTokenProvider.makeReportSpamToken(context); + } else { + spamReportToken = Optional.empty(); + } + + int totalContentLength = 0; + + for (final IncomingMessage message : messages.messages()) { + int contentLength = 0; + + if (!Util.isEmpty(message.content())) { + contentLength += message.content().length(); + } + + validateContentLength(contentLength, userAgent); + validateEnvelopeType(message.type(), userAgent); + + totalContentLength += contentLength; + } + + try { + rateLimiters.getInboundMessageBytes().validate(destinationIdentifier.uuid(), totalContentLength); + } catch (final RateLimitExceededException e) { + if (dynamicConfigurationManager.getConfiguration().getInboundMessageByteLimitConfiguration().enforceInboundLimit()) { + messageByteLimitEstimator.add(destinationIdentifier.uuid().toString()); + throw e; + } + } + + try { + boolean isSyncMessage = source.isPresent() && source.get().getAccount().isIdentifiedBy(destinationIdentifier); + + Optional destination; + + if (!isSyncMessage) { + destination = accountsManager.getByServiceIdentifier(destinationIdentifier); + } else { + destination = source.map(AuthenticatedAccount::getAccount); + } + + // Stories will be checked by the client; we bypass access checks here for stories. + if (!isStory) { + OptionalAccess.verify(source.map(AuthenticatedAccount::getAccount), accessKey, destination); + } + + boolean needsSync = !isSyncMessage && source.isPresent() && source.get().getAccount().getEnabledDeviceCount() > 1; + + // We return 200 when stories are sent to a non-existent account. Since story sends bypass OptionalAccess.verify + // we leak information about whether a destination UUID exists if we return any other code (e.g. 404) from + // these requests. + if (isStory && destination.isEmpty()) { + return Response.ok(new SendMessageResponse(needsSync)).build(); + } + + // if destination is empty we would either throw an exception in OptionalAccess.verify when isStory is false + // or else return a 200 response when isStory is true. + assert destination.isPresent(); + + if (source.isPresent() && !isSyncMessage) { + checkMessageRateLimit(source.get(), destination.get(), userAgent); + } + + if (isStory) { + checkStoryRateLimit(destination.get(), userAgent); + } + + final Set excludedDeviceIds; + + if (isSyncMessage) { + excludedDeviceIds = Set.of(source.get().getAuthenticatedDevice().getId()); + } else { + excludedDeviceIds = Collections.emptySet(); + } + + DestinationDeviceValidator.validateCompleteDeviceList(destination.get(), + messages.messages().stream().map(IncomingMessage::destinationDeviceId).collect(Collectors.toSet()), + excludedDeviceIds); + + DestinationDeviceValidator.validateRegistrationIds(destination.get(), + messages.messages(), + IncomingMessage::destinationDeviceId, + IncomingMessage::destinationRegistrationId, + destination.get().getPhoneNumberIdentifier().equals(destinationIdentifier.uuid())); + + final List tags = List.of(UserAgentTagUtil.getPlatformTag(userAgent), + Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(messages.online())), + Tag.of(SENDER_TYPE_TAG_NAME, senderType)); + + for (IncomingMessage incomingMessage : messages.messages()) { + Optional destinationDevice = destination.get().getDevice(incomingMessage.destinationDeviceId()); + + if (destinationDevice.isPresent()) { + Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags).increment(); + sendIndividualMessage( + source, + destination.get(), + destinationDevice.get(), + destinationIdentifier, + messages.timestamp(), + messages.online(), + isStory, + messages.urgent(), + incomingMessage, + userAgent, + spamReportToken); + } + } + + return Response.ok(new SendMessageResponse(needsSync)).build(); + } catch (NoSuchUserException e) { + throw new WebApplicationException(Response.status(404).build()); + } catch (MismatchedDevicesException e) { + throw new WebApplicationException(Response.status(409) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(new MismatchedDevices(e.getMissingDevices(), + e.getExtraDevices())) + .build()); + } catch (StaleDevicesException e) { + throw new WebApplicationException(Response.status(410) + .type(MediaType.APPLICATION_JSON) + .entity(new StaleDevices(e.getStaleDevices())) + .build()); + } + } + + + /** + * Build mapping of accounts to devices/registration IDs. + */ + private Map>> buildDeviceIdAndRegistrationIdMap( + MultiRecipientMessage multiRecipientMessage, + Map accountsByServiceIdentifier) { + + return Arrays.stream(multiRecipientMessage.recipients()) + // for normal messages, all recipients UUIDs are in the map, + // but story messages might specify inactive UUIDs, which we + // have previously filtered + .filter(r -> accountsByServiceIdentifier.containsKey(r.uuid())) + .collect(Collectors.toMap( + recipient -> accountsByServiceIdentifier.get(recipient.uuid()), + recipient -> new HashSet<>( + Collections.singletonList(new Pair<>(recipient.deviceId(), recipient.registrationId()))), + (a, b) -> { + a.addAll(b); + return a; + } + )); + } + + @Timed + @Path("/multi_recipient") + @PUT + @Consumes(MultiRecipientMessageProvider.MEDIA_TYPE) + @Produces(MediaType.APPLICATION_JSON) + @FilterSpam + public Response sendMultiRecipientMessage( + @HeaderParam(OptionalAccess.UNIDENTIFIED) @Nullable CombinedUnidentifiedSenderAccessKeys accessKeys, + @HeaderParam(HttpHeaders.USER_AGENT) String userAgent, + @HeaderParam(HttpHeaders.X_FORWARDED_FOR) String forwardedFor, + @QueryParam("online") boolean online, + @QueryParam("ts") long timestamp, + @QueryParam("urgent") @DefaultValue("true") final boolean isUrgent, + @QueryParam("story") boolean isStory, + @NotNull @Valid MultiRecipientMessage multiRecipientMessage) { + + final Map accountsByServiceIdentifier = new HashMap<>(); + + for (final Recipient recipient : multiRecipientMessage.recipients()) { + if (!accountsByServiceIdentifier.containsKey(recipient.uuid())) { + final Optional maybeAccount = accountsManager.getByServiceIdentifier(recipient.uuid()); + + if (maybeAccount.isPresent()) { + accountsByServiceIdentifier.put(recipient.uuid(), maybeAccount.get()); + } else { + if (!isStory) { + throw new NotFoundException(); + } + } + } + } + + // Stories will be checked by the client; we bypass access checks here for stories. + if (!isStory) { + checkAccessKeys(accessKeys, accountsByServiceIdentifier.values()); + } + + final Map>> accountToDeviceIdAndRegistrationIdMap = + buildDeviceIdAndRegistrationIdMap(multiRecipientMessage, accountsByServiceIdentifier); + + // We might filter out all the recipients of a story (if none have enabled stories). + // In this case there is no error so we should just return 200 now. + if (isStory && accountToDeviceIdAndRegistrationIdMap.isEmpty()) { + return Response.ok(new SendMultiRecipientMessageResponse(new LinkedList<>())).build(); + } + + Collection accountMismatchedDevices = new ArrayList<>(); + Collection accountStaleDevices = new ArrayList<>(); + accountsByServiceIdentifier.forEach((serviceIdentifier, account) -> { + + if (isStory) { + checkStoryRateLimit(account, userAgent); + } + + Set deviceIds = accountToDeviceIdAndRegistrationIdMap + .getOrDefault(account, Collections.emptySet()) + .stream() + .map(Pair::first) + .collect(Collectors.toSet()); + + try { + DestinationDeviceValidator.validateCompleteDeviceList(account, deviceIds, Collections.emptySet()); + + // Multi-recipient messages are always sealed-sender messages, and so can never be sent to a phone number + // identity + DestinationDeviceValidator.validateRegistrationIds( + account, + accountToDeviceIdAndRegistrationIdMap.get(account).stream(), + false); + } catch (MismatchedDevicesException e) { + accountMismatchedDevices.add(new AccountMismatchedDevices(serviceIdentifier, + new MismatchedDevices(e.getMissingDevices(), e.getExtraDevices()))); + } catch (StaleDevicesException e) { + accountStaleDevices.add(new AccountStaleDevices(serviceIdentifier, new StaleDevices(e.getStaleDevices()))); + } + }); + if (!accountMismatchedDevices.isEmpty()) { + return Response + .status(409) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(accountMismatchedDevices) + .build(); + } + if (!accountStaleDevices.isEmpty()) { + return Response + .status(410) + .type(MediaType.APPLICATION_JSON) + .entity(accountStaleDevices) + .build(); + } + + List uuids404 = Collections.synchronizedList(new ArrayList<>()); + + try { + final Counter sentMessageCounter = Metrics.counter(SENT_MESSAGE_COUNTER_NAME, Tags.of( + UserAgentTagUtil.getPlatformTag(userAgent), + Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)), + Tag.of(SENDER_TYPE_TAG_NAME, SENDER_TYPE_UNIDENTIFIED))); + + multiRecipientMessageExecutor.invokeAll(Arrays.stream(multiRecipientMessage.recipients()) + .map(recipient -> (Callable) () -> { + Account destinationAccount = accountsByServiceIdentifier.get(recipient.uuid()); + + // we asserted this must exist in validateCompleteDeviceList + Device destinationDevice = destinationAccount.getDevice(recipient.deviceId()).orElseThrow(); + sentMessageCounter.increment(); + try { + sendCommonPayloadMessage(destinationAccount, destinationDevice, timestamp, online, isStory, isUrgent, + recipient, multiRecipientMessage.commonPayload()); + } catch (NoSuchUserException e) { + uuids404.add(recipient.uuid()); + } + return null; + }) + .collect(Collectors.toList())); + } catch (InterruptedException e) { + logger.error("interrupted while delivering multi-recipient messages", e); + return Response.serverError().entity("interrupted during delivery").build(); + } + return Response.ok(new SendMultiRecipientMessageResponse(uuids404)).build(); + } + + private void checkAccessKeys(final CombinedUnidentifiedSenderAccessKeys accessKeys, final Collection destinationAccounts) { + // We should not have null access keys when checking access; bail out early. + if (accessKeys == null) { + throw new WebApplicationException(Status.UNAUTHORIZED); + } + AtomicBoolean throwUnauthorized = new AtomicBoolean(false); + byte[] empty = new byte[16]; + final Optional UNRESTRICTED_UNIDENTIFIED_ACCESS_KEY = Optional.of(new byte[16]); + byte[] combinedUnknownAccessKeys = destinationAccounts.stream() + .map(account -> { + if (account.isUnrestrictedUnidentifiedAccess()) { + return UNRESTRICTED_UNIDENTIFIED_ACCESS_KEY; + } else { + return account.getUnidentifiedAccessKey(); + } + }) + .map(accessKey -> { + if (accessKey.isEmpty()) { + throwUnauthorized.set(true); + return empty; + } + return accessKey.get(); + }) + .reduce(new byte[16], (bytes, bytes2) -> { + if (bytes.length != bytes2.length) { + throwUnauthorized.set(true); + return bytes; + } + for (int i = 0; i < bytes.length; i++) { + bytes[i] ^= bytes2[i]; + } + return bytes; + }); + if (throwUnauthorized.get() + || !MessageDigest.isEqual(combinedUnknownAccessKeys, accessKeys.getAccessKeys())) { + throw new WebApplicationException(Status.UNAUTHORIZED); + } + } + + @Timed + @GET + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture getPendingMessages(@Auth AuthenticatedAccount auth, + @HeaderParam(Stories.X_SIGNAL_RECEIVE_STORIES) String receiveStoriesHeader, + @HeaderParam(HttpHeaders.USER_AGENT) String userAgent) { + + boolean shouldReceiveStories = Stories.parseReceiveStoriesHeader(receiveStoriesHeader); + + pushNotificationManager.handleMessagesRetrieved(auth.getAccount(), auth.getAuthenticatedDevice(), userAgent); + + return messagesManager.getMessagesForDevice( + auth.getAccount().getUuid(), + auth.getAuthenticatedDevice().getId(), + false) + .map(messagesAndHasMore -> { + Stream envelopes = messagesAndHasMore.first().stream(); + if (!shouldReceiveStories) { + envelopes = envelopes.filter(e -> !e.getStory()); + } + + final OutgoingMessageEntityList messages = new OutgoingMessageEntityList(envelopes + .map(OutgoingMessageEntity::fromEnvelope) + .peek(outgoingMessageEntity -> { + MessageMetrics.measureAccountOutgoingMessageUuidMismatches(auth.getAccount(), outgoingMessageEntity); + MessageMetrics.measureOutgoingMessageLatency(outgoingMessageEntity.serverTimestamp(), "rest", userAgent, clientReleaseManager); + }) + .collect(Collectors.toList()), + messagesAndHasMore.second()); + + Metrics.summary(OUTGOING_MESSAGE_LIST_SIZE_BYTES_DISTRIBUTION_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))) + .record(estimateMessageListSizeBytes(messages)); + + return messages; + }) + .timeout(Duration.ofSeconds(5)) + .subscribeOn(messageDeliveryScheduler) + .toFuture(); + } + + private static long estimateMessageListSizeBytes(final OutgoingMessageEntityList messageList) { + long size = 0; + + for (final OutgoingMessageEntity message : messageList.messages()) { + size += message.content() == null ? 0 : message.content().length; + size += message.sourceUuid() == null ? 0 : 36; + } + + return size; + } + + @Timed + @DELETE + @Path("/uuid/{uuid}") + public CompletableFuture removePendingMessage(@Auth AuthenticatedAccount auth, @PathParam("uuid") UUID uuid) { + return messagesManager.delete( + auth.getAccount().getUuid(), + auth.getAuthenticatedDevice().getId(), + uuid, + null) + .thenAccept(maybeDeletedMessage -> { + maybeDeletedMessage.ifPresent(deletedMessage -> { + + WebSocketConnection.recordMessageDeliveryDuration(deletedMessage.getTimestamp(), + auth.getAuthenticatedDevice()); + + if (deletedMessage.hasSourceUuid() && deletedMessage.getType() != Type.SERVER_DELIVERY_RECEIPT) { + try { + receiptSender.sendReceipt( + ServiceIdentifier.valueOf(deletedMessage.getDestinationUuid()), auth.getAuthenticatedDevice().getId(), + AciServiceIdentifier.valueOf(deletedMessage.getSourceUuid()), deletedMessage.getTimestamp()); + } catch (Exception e) { + logger.warn("Failed to send delivery receipt", e); + } + } + }); + }); + } + + @Timed + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Path("/report/{source}/{messageGuid}") + public Response reportSpamMessage( + @Auth AuthenticatedAccount auth, + @PathParam("source") String source, + @PathParam("messageGuid") UUID messageGuid, + @Nullable @Valid SpamReport spamReport, + @HeaderParam(HttpHeaders.USER_AGENT) String userAgent + ) { + + final Optional sourceNumber; + final Optional sourceAci; + final Optional sourcePni; + if (source.startsWith("+")) { + sourceNumber = Optional.of(source); + final Optional maybeAccount = accountsManager.getByE164(source); + if (maybeAccount.isPresent()) { + sourceAci = maybeAccount.map(Account::getUuid); + sourcePni = maybeAccount.map(Account::getPhoneNumberIdentifier); + } else { + sourceAci = accountsManager.findRecentlyDeletedAccountIdentifier(source); + sourcePni = Optional.ofNullable(accountsManager.getPhoneNumberIdentifier(source)); + } + } else { + sourceAci = Optional.of(UUID.fromString(source)); + + final Optional sourceAccount = accountsManager.getByAccountIdentifier(sourceAci.get()); + + if (sourceAccount.isEmpty()) { + logger.warn("Could not find source: {}", sourceAci.get()); + sourceNumber = accountsManager.findRecentlyDeletedE164(sourceAci.get()); + sourcePni = sourceNumber.map(accountsManager::getPhoneNumberIdentifier); + } else { + sourceNumber = sourceAccount.map(Account::getNumber); + sourcePni = sourceAccount.map(Account::getPhoneNumberIdentifier); + } + } + + UUID spamReporterUuid = auth.getAccount().getUuid(); + + // spam report token is optional, but if provided ensure it is valid base64 and non-empty. + final Optional maybeSpamReportToken = + Optional.ofNullable(spamReport) + .flatMap(r -> Optional.ofNullable(r.token())) + .filter(t -> t.length > 0); + + reportMessageManager.report(sourceNumber, sourceAci, sourcePni, messageGuid, spamReporterUuid, maybeSpamReportToken, userAgent); + + return Response.status(Status.ACCEPTED) + .build(); + } + + private void sendIndividualMessage( + Optional source, + Account destinationAccount, + Device destinationDevice, + ServiceIdentifier destinationIdentifier, + long timestamp, + boolean online, + boolean story, + boolean urgent, + IncomingMessage incomingMessage, + String userAgentString, + Optional spamReportToken) + throws NoSuchUserException { + try { + final Envelope envelope; + + try { + Account sourceAccount = source.map(AuthenticatedAccount::getAccount).orElse(null); + Long sourceDeviceId = source.map(account -> account.getAuthenticatedDevice().getId()).orElse(null); + envelope = incomingMessage.toEnvelope( + destinationIdentifier, + sourceAccount, + sourceDeviceId, + timestamp == 0 ? System.currentTimeMillis() : timestamp, + story, + urgent, + spamReportToken.orElse(null)); + } catch (final IllegalArgumentException e) { + logger.warn("Received bad envelope type {} from {}", incomingMessage.type(), userAgentString); + throw new BadRequestException(e); + } + + messageSender.sendMessage(destinationAccount, destinationDevice, envelope, online); + } catch (NotPushRegisteredException e) { + if (destinationDevice.isMaster()) throw new NoSuchUserException(e); + else logger.debug("Not registered", e); + } + } + + private void sendCommonPayloadMessage(Account destinationAccount, + Device destinationDevice, + long timestamp, + boolean online, + boolean story, + boolean urgent, + Recipient recipient, + byte[] commonPayload) throws NoSuchUserException { + try { + Envelope.Builder messageBuilder = Envelope.newBuilder(); + long serverTimestamp = System.currentTimeMillis(); + byte[] recipientKeyMaterial = recipient.perRecipientKeyMaterial(); + + byte[] payload = new byte[1 + recipientKeyMaterial.length + commonPayload.length]; + payload[0] = MultiRecipientMessageProvider.AMBIGUOUS_ID_VERSION_IDENTIFIER; + System.arraycopy(recipientKeyMaterial, 0, payload, 1, recipientKeyMaterial.length); + System.arraycopy(commonPayload, 0, payload, 1 + recipientKeyMaterial.length, commonPayload.length); + + messageBuilder + .setType(Type.UNIDENTIFIED_SENDER) + .setTimestamp(timestamp == 0 ? serverTimestamp : timestamp) + .setServerTimestamp(serverTimestamp) + .setContent(ByteString.copyFrom(payload)) + .setStory(story) + .setUrgent(urgent) + .setDestinationUuid(new AciServiceIdentifier(destinationAccount.getUuid()).toServiceIdentifierString()); + + messageSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build(), online); + } catch (NotPushRegisteredException e) { + if (destinationDevice.isMaster()) { + throw new NoSuchUserException(e); + } else { + logger.debug("Not registered", e); + } + } + } + + private void checkStoryRateLimit(Account destination, String userAgent) { + try { + rateLimiters.getStoriesLimiter().validate(destination.getUuid()); + } catch (final RateLimitExceededException e) { + Metrics.counter(RATE_LIMITED_STORIES_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment(); + } + } + + private void checkMessageRateLimit(AuthenticatedAccount source, Account destination, String userAgent) + throws RateLimitExceededException { + final String senderCountryCode = Util.getCountryCode(source.getAccount().getNumber()); + + try { + rateLimiters.getMessagesLimiter().validate(source.getAccount().getUuid(), destination.getUuid()); + } catch (final RateLimitExceededException e) { + Metrics.counter(RATE_LIMITED_MESSAGE_COUNTER_NAME, + Tags.of( + UserAgentTagUtil.getPlatformTag(userAgent), + Tag.of(SENDER_COUNTRY_TAG_NAME, senderCountryCode), + Tag.of(RATE_LIMIT_REASON_TAG_NAME, "singleDestinationRate"))).increment(); + + throw e; + } + } + + private void validateContentLength(final int contentLength, final String userAgent) { + Metrics.summary(CONTENT_SIZE_DISTRIBUTION_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))) + .record(contentLength); + + if (contentLength > MAX_MESSAGE_SIZE) { + Metrics.counter(REJECT_OVERSIZE_MESSAGE_COUNTER, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))) + .increment(); + throw new WebApplicationException(Status.REQUEST_ENTITY_TOO_LARGE); + } + + } + + private void validateEnvelopeType(final int type, final String userAgent) { + if (type == Type.SERVER_DELIVERY_RECEIPT_VALUE) { + Metrics.counter(REJECT_INVALID_ENVELOPE_TYPE, + Tags.of(UserAgentTagUtil.getPlatformTag(userAgent), Tag.of(ENVELOPE_TYPE_TAG_NAME, String.valueOf(type)))) + .increment(); + throw new BadRequestException("reserved envelope type"); + } + } + + public static Optional getMessageContent(IncomingMessage message) { + if (Util.isEmpty(message.content())) return Optional.empty(); + + try { + return Optional.of(Base64.getDecoder().decode(message.content())); + } catch (IllegalArgumentException e) { + logger.debug("Bad B64", e); + return Optional.empty(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MismatchedDevicesException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MismatchedDevicesException.java new file mode 100644 index 000000000..4f8745e1c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MismatchedDevicesException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import java.util.List; + +public class MismatchedDevicesException extends Exception { + + private final List missingDevices; + private final List extraDevices; + + public MismatchedDevicesException(List missingDevices, List extraDevices) { + this.missingDevices = missingDevices; + this.extraDevices = extraDevices; + } + + public List getMissingDevices() { + return missingDevices; + } + + public List getExtraDevices() { + return extraDevices; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/NoSuchUserException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/NoSuchUserException.java new file mode 100644 index 000000000..68c3c58b0 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/NoSuchUserException.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.controllers; + +import java.util.UUID; + +public class NoSuchUserException extends Exception { + + public NoSuchUserException(final UUID uuid) { + super(uuid.toString()); + } + + public NoSuchUserException(Exception e) { + super(e); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/PaymentsController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/PaymentsController.java new file mode 100644 index 000000000..bd9946d1d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/PaymentsController.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration; +import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager; +import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntityList; + +@Path("/v1/payments") +@Tag(name = "Payments") +public class PaymentsController { + + private final ExternalServiceCredentialsGenerator paymentsServiceCredentialsGenerator; + private final CurrencyConversionManager currencyManager; + + + public static ExternalServiceCredentialsGenerator credentialsGenerator(final PaymentsServiceConfiguration cfg) { + return ExternalServiceCredentialsGenerator + .builder(cfg.userAuthenticationTokenSharedSecret()) + .prependUsername(true) + .build(); + } + + public PaymentsController(final CurrencyConversionManager currencyManager, + final ExternalServiceCredentialsGenerator paymentsServiceCredentialsGenerator) { + this.currencyManager = currencyManager; + this.paymentsServiceCredentialsGenerator = paymentsServiceCredentialsGenerator; + } + + @GET + @Path("/auth") + @Produces(MediaType.APPLICATION_JSON) + public ExternalServiceCredentials getAuth(final @Auth AuthenticatedAccount auth) { + return paymentsServiceCredentialsGenerator.generateForUuid(auth.getAccount().getUuid()); + } + + @GET + @Path("/conversions") + @Produces(MediaType.APPLICATION_JSON) + public CurrencyConversionEntityList getConversions(final @Auth AuthenticatedAccount auth) { + return currencyManager.getCurrencyConversions().orElseThrow(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java new file mode 100644 index 000000000..6e2db8eb8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java @@ -0,0 +1,526 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.common.base.Preconditions; +import io.dropwizard.auth.Auth; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.vavr.Tuple; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.HexFormat; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ServiceId; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.VerificationFailedException; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; +import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.Anonymous; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.OptionalAccess; +import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum; +import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; +import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; +import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.entities.BaseProfileResponse; +import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckRequest; +import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckResponse; +import org.whispersystems.textsecuregcm.entities.CreateProfileRequest; +import org.whispersystems.textsecuregcm.entities.CredentialProfileResponse; +import org.whispersystems.textsecuregcm.entities.ExpiringProfileKeyCredentialProfileResponse; +import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes; +import org.whispersystems.textsecuregcm.entities.UserCapabilities; +import org.whispersystems.textsecuregcm.entities.VersionedProfileResponse; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.s3.PolicySigner; +import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountBadge; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.storage.ProfilesManager; +import org.whispersystems.textsecuregcm.storage.VersionedProfile; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.ProfileHelper; +import org.whispersystems.textsecuregcm.util.Util; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +@Path("/v1/profile") +@Tag(name = "Profile") +public class ProfileController { + private final Logger logger = LoggerFactory.getLogger(ProfileController.class); + private final Clock clock; + private final RateLimiters rateLimiters; + private final ProfilesManager profilesManager; + private final AccountsManager accountsManager; + private final DynamicConfigurationManager dynamicConfigurationManager; + private final ProfileBadgeConverter profileBadgeConverter; + private final Map badgeConfigurationMap; + + private final PolicySigner policySigner; + private final PostPolicyGenerator policyGenerator; + private final ServerZkProfileOperations zkProfileOperations; + + private final S3Client s3client; + private final String bucket; + + private final Executor batchIdentityCheckExecutor; + + private static final String EXPIRING_PROFILE_KEY_CREDENTIAL_TYPE = "expiringProfileKey"; + + private static final Counter VERSION_NOT_FOUND_COUNTER = Metrics.counter(name(ProfileController.class, "versionNotFound")); + private static final String INVALID_ACCEPT_LANGUAGE_COUNTER_NAME = name(ProfileController.class, "invalidAcceptLanguage"); + + public ProfileController( + Clock clock, + RateLimiters rateLimiters, + AccountsManager accountsManager, + ProfilesManager profilesManager, + DynamicConfigurationManager dynamicConfigurationManager, + ProfileBadgeConverter profileBadgeConverter, + BadgesConfiguration badgesConfiguration, + S3Client s3client, + PostPolicyGenerator policyGenerator, + PolicySigner policySigner, + String bucket, + ServerZkProfileOperations zkProfileOperations, + Executor batchIdentityCheckExecutor) { + this.clock = clock; + this.rateLimiters = rateLimiters; + this.accountsManager = accountsManager; + this.profilesManager = profilesManager; + this.dynamicConfigurationManager = dynamicConfigurationManager; + this.profileBadgeConverter = profileBadgeConverter; + this.badgeConfigurationMap = badgesConfiguration.getBadges().stream().collect(Collectors.toMap( + BadgeConfiguration::getId, Function.identity())); + this.zkProfileOperations = zkProfileOperations; + this.bucket = bucket; + this.s3client = s3client; + this.policyGenerator = policyGenerator; + this.policySigner = policySigner; + this.batchIdentityCheckExecutor = Preconditions.checkNotNull(batchIdentityCheckExecutor); + } + + @PUT + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public Response setProfile(@Auth AuthenticatedAccount auth, @NotNull @Valid CreateProfileRequest request) { + + final Optional currentProfile = profilesManager.get(auth.getAccount().getUuid(), + request.getVersion()); + + if (request.getPaymentAddress() != null && request.getPaymentAddress().length != 0) { + final boolean hasDisallowedPrefix = + dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getDisallowedPrefixes().stream() + .anyMatch(prefix -> auth.getAccount().getNumber().startsWith(prefix)); + + if (hasDisallowedPrefix && currentProfile.map(VersionedProfile::paymentAddress).isEmpty()) { + return Response.status(Response.Status.FORBIDDEN).build(); + } + } + + Optional currentAvatar = Optional.empty(); + if (currentProfile.isPresent() && currentProfile.get().avatar() != null && currentProfile.get().avatar() + .startsWith("profiles/")) { + currentAvatar = Optional.of(currentProfile.get().avatar()); + } + + final String avatar = switch (request.getAvatarChange()) { + case UNCHANGED -> currentAvatar.orElse(null); + case CLEAR -> null; + case UPDATE -> ProfileHelper.generateAvatarObjectName(); + }; + + profilesManager.set(auth.getAccount().getUuid(), + new VersionedProfile( + request.getVersion(), + request.getName(), + avatar, + request.getAboutEmoji(), + request.getAbout(), + request.getPaymentAddress(), + request.getCommitment().serialize())); + + if (request.getAvatarChange() != CreateProfileRequest.AvatarChange.UNCHANGED) { + currentAvatar.ifPresent(s -> s3client.deleteObject(DeleteObjectRequest.builder() + .bucket(bucket) + .key(s) + .build())); + } + + final List updatedBadges = request.getBadges() + .map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges, auth.getAccount().getBadges())) + .orElseGet(() -> auth.getAccount().getBadges()); + + accountsManager.update(auth.getAccount(), a -> { + a.setBadges(clock, updatedBadges); + a.setCurrentProfileVersion(request.getVersion()); + }); + + if (request.getAvatarChange() == CreateProfileRequest.AvatarChange.UPDATE) { + return Response.ok(generateAvatarUploadForm(avatar)).build(); + } else { + return Response.ok().build(); + } + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/{identifier}/{version}") + public VersionedProfileResponse getProfile( + @Auth Optional auth, + @HeaderParam(OptionalAccess.UNIDENTIFIED) Optional accessKey, + @Context ContainerRequestContext containerRequestContext, + @PathParam("identifier") AciServiceIdentifier accountIdentifier, + @PathParam("version") String version) + throws RateLimitExceededException { + + final Optional maybeRequester = auth.map(AuthenticatedAccount::getAccount); + final Account targetAccount = verifyPermissionToReceiveAccountIdentityProfile(maybeRequester, accessKey, accountIdentifier); + + return buildVersionedProfileResponse(targetAccount, + version, + maybeRequester.map(requester -> ProfileHelper.isSelfProfileRequest(requester.getUuid(), accountIdentifier)).orElse(false), + containerRequestContext); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/{identifier}/{version}/{credentialRequest}") + public CredentialProfileResponse getProfile( + @Auth Optional auth, + @HeaderParam(OptionalAccess.UNIDENTIFIED) Optional accessKey, + @Context ContainerRequestContext containerRequestContext, + @PathParam("identifier") AciServiceIdentifier accountIdentifier, + @PathParam("version") String version, + @PathParam("credentialRequest") String credentialRequest, + @QueryParam("credentialType") String credentialType) + throws RateLimitExceededException { + + if (!EXPIRING_PROFILE_KEY_CREDENTIAL_TYPE.equals(credentialType)) { + throw new BadRequestException(); + } + + final Optional maybeRequester = auth.map(AuthenticatedAccount::getAccount); + final Account targetAccount = verifyPermissionToReceiveAccountIdentityProfile(maybeRequester, accessKey, accountIdentifier); + final boolean isSelf = maybeRequester.map(requester -> ProfileHelper.isSelfProfileRequest(requester.getUuid(), accountIdentifier)).orElse(false); + + return buildExpiringProfileKeyCredentialProfileResponse(targetAccount, + version, + credentialRequest, + isSelf, + containerRequestContext); + } + + // Although clients should generally be using versioned profiles wherever possible, there are still a few lingering + // use cases for getting profiles without a version (e.g. getting a contact's unidentified access key checksum). + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/{identifier}") + public BaseProfileResponse getUnversionedProfile( + @Auth Optional auth, + @HeaderParam(OptionalAccess.UNIDENTIFIED) Optional accessKey, + @Context ContainerRequestContext containerRequestContext, + @HeaderParam(HttpHeaders.USER_AGENT) String userAgent, + @PathParam("identifier") ServiceIdentifier identifier, + @QueryParam("ca") boolean useCaCertificate) + throws RateLimitExceededException { + + final Optional maybeRequester = auth.map(AuthenticatedAccount::getAccount); + + return switch (identifier.identityType()) { + case ACI -> { + final AciServiceIdentifier aciServiceIdentifier = (AciServiceIdentifier) identifier; + + final Account targetAccount = + verifyPermissionToReceiveAccountIdentityProfile(maybeRequester, accessKey, aciServiceIdentifier); + + yield buildBaseProfileResponseForAccountIdentity(targetAccount, + maybeRequester.map(requester -> ProfileHelper.isSelfProfileRequest(requester.getUuid(), aciServiceIdentifier)).orElse(false), + containerRequestContext); + } + case PNI -> { + final Optional maybeAccountByPni = accountsManager.getByPhoneNumberIdentifier(identifier.uuid()); + + if (maybeRequester.isEmpty()) { + throw new WebApplicationException(Response.Status.UNAUTHORIZED); + } else { + rateLimiters.getProfileLimiter().validate(maybeRequester.get().getUuid()); + } + + OptionalAccess.verify(maybeRequester, Optional.empty(), maybeAccountByPni); + + assert maybeAccountByPni.isPresent(); + yield buildBaseProfileResponseForPhoneNumberIdentity(maybeAccountByPni.get()); + } + }; + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("/identity_check/batch") + public CompletableFuture runBatchIdentityCheck(@NotNull @Valid BatchIdentityCheckRequest request) { + return CompletableFuture.supplyAsync(() -> { + List responseElements = Collections.synchronizedList(new ArrayList<>()); + + final int targetBatchCount = 10; + // clamp the amount per batch to be in the closed range [30, 100] + final int batchSize = Math.min(Math.max(request.elements().size() / targetBatchCount, 30), 100); + // add 1 extra batch if there is any remainder to consume the final non-full batch + final int batchCount = + request.elements().size() / batchSize + (request.elements().size() % batchSize != 0 ? 1 : 0); + + @SuppressWarnings("rawtypes") CompletableFuture[] futures = new CompletableFuture[batchCount]; + for (int i = 0; i < batchCount; ++i) { + List batch = request.elements() + .subList(i * batchSize, Math.min((i + 1) * batchSize, request.elements().size())); + futures[i] = CompletableFuture.runAsync(() -> { + MessageDigest sha256; + try { + sha256 = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + for (final BatchIdentityCheckRequest.Element element : batch) { + checkFingerprintAndAdd(element, responseElements, sha256); + } + }, batchIdentityCheckExecutor); + } + + return Tuple.of(futures, responseElements); + }).thenCompose(tuple2 -> CompletableFuture.allOf(tuple2._1).thenApply((ignored) -> new BatchIdentityCheckResponse(tuple2._2))); + } + + private void checkFingerprintAndAdd(BatchIdentityCheckRequest.Element element, + Collection responseElements, MessageDigest md) { + + final ServiceIdentifier identifier = Objects.requireNonNullElse(element.uuid(), element.aci()); + final Optional maybeAccount = accountsManager.getByServiceIdentifier(identifier); + + maybeAccount.ifPresent(account -> { + final IdentityKey identityKey = account.getIdentityKey(identifier.identityType()); + if (identityKey == null) { + return; + } + + md.reset(); + byte[] digest = md.digest(identityKey.serialize()); + byte[] fingerprint = Util.truncate(digest, 4); + + if (!Arrays.equals(fingerprint, element.fingerprint())) { + responseElements.add(new BatchIdentityCheckResponse.Element(element.uuid(), element.aci(), identityKey)); + } + }); + } + + private ExpiringProfileKeyCredentialProfileResponse buildExpiringProfileKeyCredentialProfileResponse( + final Account account, + final String version, + final String encodedCredentialRequest, + final boolean isSelf, + final ContainerRequestContext containerRequestContext) { + + final ExpiringProfileKeyCredentialResponse expiringProfileKeyCredentialResponse = profilesManager.get(account.getUuid(), version) + .map(profile -> { + final ExpiringProfileKeyCredentialResponse profileKeyCredentialResponse; + try { + profileKeyCredentialResponse = ProfileHelper.getExpiringProfileKeyCredential(HexFormat.of().parseHex(encodedCredentialRequest), + profile, new ServiceId.Aci(account.getUuid()), zkProfileOperations); + } catch (VerificationFailedException | InvalidInputException e) { + throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST).build(), e); + } + return profileKeyCredentialResponse; + }) + .orElse(null); + + return new ExpiringProfileKeyCredentialProfileResponse( + buildVersionedProfileResponse(account, version, isSelf, containerRequestContext), + expiringProfileKeyCredentialResponse); + } + + private VersionedProfileResponse buildVersionedProfileResponse(final Account account, + final String version, + final boolean isSelf, + final ContainerRequestContext containerRequestContext) { + + final Optional maybeProfile = profilesManager.get(account.getUuid(), version); + + if (maybeProfile.isEmpty()) { + // Hypothesis: this should basically never happen since clients can't delete versions + VERSION_NOT_FOUND_COUNTER.increment(); + } + + final byte[] name = maybeProfile.map(VersionedProfile::name).orElse(null); + final byte[] about = maybeProfile.map(VersionedProfile::about).orElse(null); + final byte[] aboutEmoji = maybeProfile.map(VersionedProfile::aboutEmoji).orElse(null); + final String avatar = maybeProfile.map(VersionedProfile::avatar).orElse(null); + + // Allow requests where either the version matches the latest version on Account or the latest version on Account + // is empty to read the payment address. + final byte[] paymentAddress = maybeProfile + .filter(p -> account.getCurrentProfileVersion().map(v -> v.equals(version)).orElse(true)) + .map(VersionedProfile::paymentAddress) + .orElse(null); + + return new VersionedProfileResponse( + buildBaseProfileResponseForAccountIdentity(account, isSelf, containerRequestContext), + name, about, aboutEmoji, avatar, paymentAddress); + } + + private BaseProfileResponse buildBaseProfileResponseForAccountIdentity(final Account account, + final boolean isSelf, + final ContainerRequestContext containerRequestContext) { + + return new BaseProfileResponse(account.getIdentityKey(IdentityType.ACI), + account.getUnidentifiedAccessKey().map(UnidentifiedAccessChecksum::generateFor).orElse(null), + account.isUnrestrictedUnidentifiedAccess(), + UserCapabilities.createForAccount(account), + profileBadgeConverter.convert( + getAcceptableLanguagesForRequest(containerRequestContext), + account.getBadges(), + isSelf), + new AciServiceIdentifier(account.getUuid())); + } + + private BaseProfileResponse buildBaseProfileResponseForPhoneNumberIdentity(final Account account) { + return new BaseProfileResponse(account.getIdentityKey(IdentityType.PNI), + null, + false, + UserCapabilities.createForAccount(account), + Collections.emptyList(), + new PniServiceIdentifier(account.getPhoneNumberIdentifier())); + } + + private List getAcceptableLanguagesForRequest(final ContainerRequestContext containerRequestContext) { + try { + return containerRequestContext.getAcceptableLanguages(); + } catch (final ProcessingException e) { + final String userAgent = containerRequestContext.getHeaderString(HttpHeaders.USER_AGENT); + Metrics.counter(INVALID_ACCEPT_LANGUAGE_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment(); + logger.debug("Could not get acceptable languages; Accept-Language: {}; User-Agent: {}", + containerRequestContext.getHeaderString(HttpHeaders.ACCEPT_LANGUAGE), + userAgent, + e); + + return List.of(); + } + } + + /** + * Verifies that the requester has permission to view the profile of the account identified by the given ACI. + * + * @param maybeRequester the authenticated account requesting the profile, if any + * @param maybeAccessKey an anonymous access key for the target account + * @param accountIdentifier the ACI of the target account + * + * @return the target account + * + * @throws RateLimitExceededException if the requester must wait before requesting the target account's profile + * @throws NotFoundException if no account was found for the target ACI + * @throws NotAuthorizedException if the requester is not authorized to receive the target account's profile or if the + * requester was not authenticated and did not present an anonymous access key + */ + private Account verifyPermissionToReceiveAccountIdentityProfile(final Optional maybeRequester, + final Optional maybeAccessKey, + final AciServiceIdentifier accountIdentifier) throws RateLimitExceededException { + + if (maybeRequester.isEmpty() && maybeAccessKey.isEmpty()) { + throw new WebApplicationException(Response.Status.UNAUTHORIZED); + } + + if (maybeRequester.isPresent()) { + rateLimiters.getProfileLimiter().validate(maybeRequester.get().getUuid()); + } + + final Optional maybeTargetAccount = accountsManager.getByAccountIdentifier(accountIdentifier.uuid()); + + OptionalAccess.verify(maybeRequester, maybeAccessKey, maybeTargetAccount); + assert maybeTargetAccount.isPresent(); + + return maybeTargetAccount.get(); + } + + private ProfileAvatarUploadAttributes generateAvatarUploadForm( + final String objectName) { + ZonedDateTime now = ZonedDateTime.now(clock); + Pair policy = policyGenerator.createFor(now, objectName, ProfileHelper.MAX_PROFILE_AVATAR_SIZE_BYTES); + String signature = policySigner.getSignature(now, policy.second()); + + return new ProfileAvatarUploadAttributes(objectName, policy.first(), + "private", "AWS4-HMAC-SHA256", + now.format(PostPolicyGenerator.AWS_DATE_TIME), policy.second(), signature); + } + + @Nullable + private static byte[] decodeFromBase64(@Nullable final String input) { + if (input == null) { + return null; + } + return Base64.getDecoder().decode(input); + } + + @Nullable + private static String encodeToBase64(@Nullable final byte[] input) { + if (input == null) { + return null; + } + return Base64.getEncoder().encodeToString(input); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProvisioningController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProvisioningController.java new file mode 100644 index 000000000..e29e89c4e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProvisioningController.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Base64; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.ws.rs.Consumes; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.entities.ProvisioningMessage; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.push.ProvisioningManager; +import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress; + +@Path("/v1/provisioning") +@Tag(name = "Provisioning") +public class ProvisioningController { + + private final RateLimiters rateLimiters; + private final ProvisioningManager provisioningManager; + + public ProvisioningController(RateLimiters rateLimiters, ProvisioningManager provisioningManager) { + this.rateLimiters = rateLimiters; + this.provisioningManager = provisioningManager; + } + + @Path("/{destination}") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public void sendProvisioningMessage(@Auth AuthenticatedAccount auth, + @PathParam("destination") String destinationName, + @NotNull @Valid ProvisioningMessage message) + throws RateLimitExceededException { + + rateLimiters.getMessagesLimiter().validate(auth.getAccount().getUuid()); + + if (!provisioningManager.sendProvisioningMessage(new ProvisioningAddress(destinationName, 0), + Base64.getMimeDecoder().decode(message.body()))) { + throw new WebApplicationException(Response.Status.NOT_FOUND); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RateLimitExceededException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RateLimitExceededException.java new file mode 100644 index 000000000..f42441dca --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RateLimitExceededException.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.controllers; + +import io.grpc.Metadata; +import io.grpc.Status; +import java.time.Duration; +import java.util.Optional; +import javax.annotation.Nullable; +import org.whispersystems.textsecuregcm.grpc.ConvertibleToGrpcStatus; + +public class RateLimitExceededException extends Exception implements ConvertibleToGrpcStatus { + + public static final Metadata.Key RETRY_AFTER_DURATION_KEY = + Metadata.Key.of("retry-after", new Metadata.AsciiMarshaller<>() { + @Override + public String toAsciiString(final Duration value) { + return value.toString(); + } + + @Override + public Duration parseAsciiString(final String serialized) { + return Duration.parse(serialized); + } + }); + + @Nullable + private final Duration retryDuration; + private final boolean legacy; + + /** + * Constructs a new exception indicating when it may become safe to retry + * + * @param retryDuration A duration to wait before retrying, null if no duration can be indicated + * @param legacy whether to use a legacy status code when mapping the exception to an HTTP response + */ + public RateLimitExceededException(@Nullable final Duration retryDuration, final boolean legacy) { + super(null, null, true, false); + this.retryDuration = retryDuration; + this.legacy = legacy; + } + + public Optional getRetryDuration() { + return Optional.ofNullable(retryDuration); + } + + public boolean isLegacy() { + return legacy; + } + + @Override + public Status grpcStatus() { + return Status.RESOURCE_EXHAUSTED; + } + + @Override + public Optional grpcMetadata() { + return getRetryDuration() + .map(duration -> { + final Metadata metadata = new Metadata(); + metadata.put(RETRY_AFTER_DURATION_KEY, duration); + return metadata; + }); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java new file mode 100644 index 000000000..0ef2eb5e6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java @@ -0,0 +1,201 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.common.net.HttpHeaders; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.ws.rs.Consumes; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader; +import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager; +import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager; +import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse; +import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest; +import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure; +import org.whispersystems.textsecuregcm.entities.RegistrationRequest; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import org.whispersystems.textsecuregcm.util.HeaderUtils; +import org.whispersystems.textsecuregcm.util.Util; + +@Path("/v1/registration") +@io.swagger.v3.oas.annotations.tags.Tag(name = "Registration") +public class RegistrationController { + + private static final DistributionSummary REREGISTRATION_IDLE_DAYS_DISTRIBUTION = DistributionSummary + .builder(name(RegistrationController.class, "reregistrationIdleDays")) + .publishPercentiles(0.75, 0.95, 0.99, 0.999) + .distributionStatisticExpiry(Duration.ofHours(2)) + .register(Metrics.globalRegistry); + + private static final String ACCOUNT_CREATED_COUNTER_NAME = name(RegistrationController.class, "accountCreated"); + private static final String COUNTRY_CODE_TAG_NAME = "countryCode"; + private static final String REGION_CODE_TAG_NAME = "regionCode"; + private static final String VERIFICATION_TYPE_TAG_NAME = "verification"; + private static final String ACCOUNT_ACTIVATED_TAG_NAME = "accountActivated"; + private static final String INVALID_ACCOUNT_ATTRS_COUNTER_NAME = name(RegistrationController.class, "invalidAccountAttrs"); + + private final AccountsManager accounts; + private final PhoneVerificationTokenManager phoneVerificationTokenManager; + private final RegistrationLockVerificationManager registrationLockVerificationManager; + private final KeysManager keysManager; + private final RateLimiters rateLimiters; + + public RegistrationController(final AccountsManager accounts, + final PhoneVerificationTokenManager phoneVerificationTokenManager, + final RegistrationLockVerificationManager registrationLockVerificationManager, + final KeysManager keysManager, + final RateLimiters rateLimiters) { + this.accounts = accounts; + this.phoneVerificationTokenManager = phoneVerificationTokenManager; + this.registrationLockVerificationManager = registrationLockVerificationManager; + this.keysManager = keysManager; + this.rateLimiters = rateLimiters; + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Registers an account", + description = """ + Registers a new account or attempts to “re-register” an existing account. It is expected that a well-behaved client + could make up to three consecutive calls to this API: + 1. gets 423 from existing registration lock \n + 2. gets 409 from device available for transfer \n + 3. success \n + """) + @ApiResponse(responseCode = "200", description = "The phone number associated with the authenticated account was changed successfully", useReturnTypeSchema = true) + @ApiResponse(responseCode = "403", description = "Verification failed for the provided Registration Recovery Password") + @ApiResponse(responseCode = "409", description = "The caller has not explicitly elected to skip transferring data from another device, but a device transfer is technically possible") + @ApiResponse(responseCode = "422", description = "The request did not pass validation") + @ApiResponse(responseCode = "423", content = @Content(schema = @Schema(implementation = RegistrationLockFailure.class))) + @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header( + name = "Retry-After", + description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed")) + public AccountIdentityResponse register( + @HeaderParam(HttpHeaders.AUTHORIZATION) @NotNull final BasicAuthorizationHeader authorizationHeader, + @HeaderParam(HeaderUtils.X_SIGNAL_AGENT) final String signalAgent, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, + @NotNull @Valid final RegistrationRequest registrationRequest) throws RateLimitExceededException, InterruptedException { + + final String number = authorizationHeader.getUsername(); + final String password = authorizationHeader.getPassword(); + + RateLimiter.adaptLegacyException(() -> rateLimiters.getRegistrationLimiter().validate(number)); + if (!AccountsManager.validNewAccountAttributes(registrationRequest.accountAttributes())) { + Metrics.counter(INVALID_ACCOUNT_ATTRS_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment(); + throw new WebApplicationException(Response.status(422, "account attributes invalid").build()); + } + + final PhoneVerificationRequest.VerificationType verificationType = phoneVerificationTokenManager.verify(number, + registrationRequest); + + final Optional existingAccount = accounts.getByE164(number); + + existingAccount.ifPresent(account -> { + final Instant accountLastSeen = Instant.ofEpochMilli(account.getLastSeen()); + final Duration timeSinceLastSeen = Duration.between(accountLastSeen, Instant.now()); + REREGISTRATION_IDLE_DAYS_DISTRIBUTION.record(timeSinceLastSeen.toDays()); + }); + + if (!registrationRequest.skipDeviceTransfer() && existingAccount.map(Account::isTransferSupported).orElse(false)) { + // If a device transfer is possible, clients must explicitly opt out of a transfer (i.e. after prompting the user) + // before we'll let them create a new account "from scratch" + throw new WebApplicationException(Response.status(409, "device transfer available").build()); + } + + if (existingAccount.isPresent()) { + registrationLockVerificationManager.verifyRegistrationLock(existingAccount.get(), + registrationRequest.accountAttributes().getRegistrationLock(), + userAgent, RegistrationLockVerificationManager.Flow.REGISTRATION, verificationType); + } + + Account account = accounts.create(number, password, signalAgent, registrationRequest.accountAttributes(), + existingAccount.map(Account::getBadges).orElseGet(ArrayList::new)); + + // If the request includes all the information we need to fully "activate" the account, we should do so + if (registrationRequest.supportsAtomicAccountCreation()) { + assert registrationRequest.aciIdentityKey().isPresent(); + assert registrationRequest.pniIdentityKey().isPresent(); + assert registrationRequest.deviceActivationRequest().aciSignedPreKey().isPresent(); + assert registrationRequest.deviceActivationRequest().pniSignedPreKey().isPresent(); + assert registrationRequest.deviceActivationRequest().aciPqLastResortPreKey().isPresent(); + assert registrationRequest.deviceActivationRequest().pniPqLastResortPreKey().isPresent(); + + account = accounts.update(account, a -> { + a.setIdentityKey(registrationRequest.aciIdentityKey().get()); + a.setPhoneNumberIdentityKey(registrationRequest.pniIdentityKey().get()); + + final Device device = a.getMasterDevice().orElseThrow(); + + device.setSignedPreKey(registrationRequest.deviceActivationRequest().aciSignedPreKey().get()); + device.setPhoneNumberIdentitySignedPreKey(registrationRequest.deviceActivationRequest().pniSignedPreKey().get()); + + registrationRequest.deviceActivationRequest().apnToken().ifPresent(apnRegistrationId -> { + device.setApnId(apnRegistrationId.apnRegistrationId()); + device.setVoipApnId(apnRegistrationId.voipRegistrationId()); + }); + + registrationRequest.deviceActivationRequest().gcmToken().ifPresent(gcmRegistrationId -> + device.setGcmId(gcmRegistrationId.gcmRegistrationId())); + + CompletableFuture.allOf( + keysManager.storeEcSignedPreKeys(a.getUuid(), + Map.of(Device.MASTER_ID, registrationRequest.deviceActivationRequest().aciSignedPreKey().get())), + keysManager.storePqLastResort(a.getUuid(), + Map.of(Device.MASTER_ID, registrationRequest.deviceActivationRequest().aciPqLastResortPreKey().get())), + keysManager.storeEcSignedPreKeys(a.getPhoneNumberIdentifier(), + Map.of(Device.MASTER_ID, registrationRequest.deviceActivationRequest().pniSignedPreKey().get())), + keysManager.storePqLastResort(a.getPhoneNumberIdentifier(), + Map.of(Device.MASTER_ID, registrationRequest.deviceActivationRequest().pniPqLastResortPreKey().get()))) + .join(); + }); + } + + Metrics.counter(ACCOUNT_CREATED_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent), + Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)), + Tag.of(REGION_CODE_TAG_NAME, Util.getRegion(number)), + Tag.of(VERIFICATION_TYPE_TAG_NAME, verificationType.name()), + Tag.of(ACCOUNT_ACTIVATED_TAG_NAME, String.valueOf(account.isEnabled())))) + .increment(); + + return new AccountIdentityResponse(account.getUuid(), + account.getNumber(), + account.getPhoneNumberIdentifier(), + account.getUsernameHash().orElse(null), + existingAccount.map(Account::isStorageSupported).orElse(false)); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigController.java new file mode 100644 index 000000000..899b1b57f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigController.java @@ -0,0 +1,180 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Clock; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.signal.event.AdminEventLogger; +import org.signal.event.RemoteConfigDeleteEvent; +import org.signal.event.RemoteConfigSetEvent; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.entities.UserRemoteConfig; +import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList; +import org.whispersystems.textsecuregcm.storage.RemoteConfig; +import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager; +import org.whispersystems.textsecuregcm.util.Conversions; +import org.whispersystems.textsecuregcm.util.Util; + +@Path("/v1/config") +@Tag(name = "Remote Config") +public class RemoteConfigController { + + private final RemoteConfigsManager remoteConfigsManager; + private final AdminEventLogger adminEventLogger; + private final Set configAuthUsers; + private final Map globalConfig; + + private final String requiredHostedDomain; + + private final GoogleIdTokenVerifier googleIdTokenVerifier; + + private static final String GLOBAL_CONFIG_PREFIX = "global."; + + public RemoteConfigController(RemoteConfigsManager remoteConfigsManager, AdminEventLogger adminEventLogger, + Set configAuthUsers, String requiredHostedDomain, List audience, + final GoogleIdTokenVerifier.Builder googleIdTokenVerifierBuilder, Map globalConfig) { + this.remoteConfigsManager = remoteConfigsManager; + this.adminEventLogger = Objects.requireNonNull(adminEventLogger); + this.configAuthUsers = configAuthUsers; + this.globalConfig = globalConfig; + + this.requiredHostedDomain = requiredHostedDomain; + this.googleIdTokenVerifier = googleIdTokenVerifierBuilder.setAudience(audience).build(); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public UserRemoteConfigList getAll(@Auth AuthenticatedAccount auth) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA1"); + + final Stream globalConfigStream = globalConfig.entrySet().stream() + .map(entry -> new UserRemoteConfig(GLOBAL_CONFIG_PREFIX + entry.getKey(), true, entry.getValue())); + return new UserRemoteConfigList(Stream.concat(remoteConfigsManager.getAll().stream().map(config -> { + final byte[] hashKey = config.getHashKey() != null ? config.getHashKey().getBytes(StandardCharsets.UTF_8) + : config.getName().getBytes(StandardCharsets.UTF_8); + boolean inBucket = isInBucket(digest, auth.getAccount().getUuid(), hashKey, config.getPercentage(), + config.getUuids()); + return new UserRemoteConfig(config.getName(), inBucket, + inBucket ? config.getValue() : config.getDefaultValue()); + }), globalConfigStream).collect(Collectors.toList()), Clock.systemUTC().instant()); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public void set(@HeaderParam("Config-Token") String configToken, @NotNull @Valid RemoteConfig config) { + + final String authIdentity = getAuthIdentity(configToken) + .orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED)); + + if (config.getName().startsWith(GLOBAL_CONFIG_PREFIX)) { + throw new WebApplicationException(Response.Status.FORBIDDEN); + } + + adminEventLogger.logEvent( + new RemoteConfigSetEvent( + authIdentity, + config.getName(), + config.getPercentage(), + config.getDefaultValue(), + config.getValue(), + config.getHashKey(), + config.getUuids().stream().map(UUID::toString).collect(Collectors.toList()))); + remoteConfigsManager.set(config); + } + + @DELETE + @Path("/{name}") + public void delete(@HeaderParam("Config-Token") String configToken, @PathParam("name") String name) { + final String authIdentity = getAuthIdentity(configToken) + .orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED)); + + if (name.startsWith(GLOBAL_CONFIG_PREFIX)) { + throw new WebApplicationException(Response.Status.FORBIDDEN); + } + + adminEventLogger.logEvent(new RemoteConfigDeleteEvent(authIdentity, name)); + remoteConfigsManager.delete(name); + } + + private Optional getAuthIdentity(String token) { + return getAuthorizedGoogleIdentity(token) + .map(googleIdToken -> googleIdToken.getPayload().getEmail()); + } + + private Optional getAuthorizedGoogleIdentity(String token) { + try { + final @Nullable GoogleIdToken googleIdToken = googleIdTokenVerifier.verify(token); + + if (googleIdToken != null + && googleIdToken.getPayload().getHostedDomain().equals(requiredHostedDomain) + && googleIdToken.getPayload().getEmailVerified() + && configAuthUsers.contains(googleIdToken.getPayload().getEmail())) { + + return Optional.of(googleIdToken); + } + + return Optional.empty(); + + } catch (final Exception ignored) { + return Optional.empty(); + } + } + + @VisibleForTesting + public static boolean isInBucket(MessageDigest digest, UUID uid, byte[] hashKey, int configPercentage, + Set uuidsInBucket) { + if (uuidsInBucket.contains(uid)) { + return true; + } + + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + bb.putLong(uid.getMostSignificantBits()); + bb.putLong(uid.getLeastSignificantBits()); + + digest.update(bb.array()); + + byte[] hash = digest.digest(hashKey); + int bucket = (int) (Util.ensureNonNegativeLong(Conversions.byteArrayToLong(hash)) % 100); + + return bucket < configPercentage; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureStorageController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureStorageController.java new file mode 100644 index 000000000..a65f0aeb8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureStorageController.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration; + +@Path("/v1/storage") +@Tag(name = "Secure Storage") +public class SecureStorageController { + + private final ExternalServiceCredentialsGenerator storageServiceCredentialsGenerator; + + public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureStorageServiceConfiguration cfg) { + return ExternalServiceCredentialsGenerator + .builder(cfg.userAuthenticationTokenSharedSecret()) + .prependUsername(true) + .build(); + } + + public SecureStorageController(ExternalServiceCredentialsGenerator storageServiceCredentialsGenerator) { + this.storageServiceCredentialsGenerator = storageServiceCredentialsGenerator; + } + + @GET + @Path("/auth") + @Produces(MediaType.APPLICATION_JSON) + public ExternalServiceCredentials getAuth(@Auth AuthenticatedAccount auth) { + return storageServiceCredentialsGenerator.generateForUuid(auth.getAccount().getUuid()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2Controller.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2Controller.java new file mode 100644 index 000000000..7e1fb6409 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2Controller.java @@ -0,0 +1,128 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.time.Clock; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.jetbrains.annotations.TestOnly; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsSelector; +import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration; +import org.whispersystems.textsecuregcm.entities.AuthCheckRequest; +import org.whispersystems.textsecuregcm.entities.AuthCheckResponse; +import org.whispersystems.textsecuregcm.limits.RateLimitedByIp; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; + +@Path("/v2/backup") +@Tag(name = "Secure Value Recovery") +public class SecureValueRecovery2Controller { + + private static final long MAX_AGE_SECONDS = TimeUnit.DAYS.toSeconds(30); + + public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureValueRecovery2Configuration cfg) { + return credentialsGenerator(cfg, Clock.systemUTC()); + } + + @TestOnly + public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureValueRecovery2Configuration cfg, final Clock clock) { + return ExternalServiceCredentialsGenerator + .builder(cfg.userAuthenticationTokenSharedSecret()) + .withUserDerivationKey(cfg.userIdTokenSharedSecret().value()) + .prependUsername(false) + .withDerivedUsernameTruncateLength(16) + .withClock(clock) + .build(); + } + + private final ExternalServiceCredentialsGenerator backupServiceCredentialGenerator; + private final AccountsManager accountsManager; + + public SecureValueRecovery2Controller(final ExternalServiceCredentialsGenerator backupServiceCredentialGenerator, + final AccountsManager accountsManager) { + this.backupServiceCredentialGenerator = backupServiceCredentialGenerator; + this.accountsManager = accountsManager; + } + + @GET + @Path("/auth") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Generate credentials for SVR2", + description = """ + Generate SVR2 service credentials. Generated credentials have an expiration time of 30 days + (however, the TTL is fully controlled by the server side and may change even for already generated credentials). + """ + ) + @ApiResponse(responseCode = "200", description = "`JSON` with generated credentials.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "401", description = "Account authentication check failed.") + public ExternalServiceCredentials getAuth(@Auth final AuthenticatedAccount auth) { + return backupServiceCredentialGenerator.generateFor(auth.getAccount().getUuid().toString()); + } + + + @POST + @Path("/auth/check") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @RateLimitedByIp(RateLimiters.For.BACKUP_AUTH_CHECK) + @Operation( + summary = "Check SVR2 credentials", + description = """ + Over time, clients may wind up with multiple sets of SVR2 authentication credentials in cloud storage. + To determine which set is most current and should be used to communicate with SVR2 to retrieve a master key + (from which a registration recovery password can be derived), clients should call this endpoint + with a list of stored credentials. The response will identify which (if any) set of credentials are appropriate for communicating with SVR2. + """ + ) + @ApiResponse(responseCode = "200", description = "`JSON` with the check results.", useReturnTypeSchema = true) + @ApiResponse(responseCode = "422", description = "Provided list of SVR2 credentials could not be parsed") + @ApiResponse(responseCode = "400", description = "`POST` request body is not a valid `JSON`") + public AuthCheckResponse authCheck(@NotNull @Valid final AuthCheckRequest request) { + final List credentials = ExternalServiceCredentialsSelector.check( + request.passwords(), + backupServiceCredentialGenerator, + MAX_AGE_SECONDS); + + // the username associated with the provided number + final Optional matchingUsername = accountsManager + .getByE164(request.number()) + .map(Account::getUuid) + .map(backupServiceCredentialGenerator::generateForUuid) + .map(ExternalServiceCredentials::username); + + return new AuthCheckResponse(credentials.stream().collect(Collectors.toMap( + ExternalServiceCredentialsSelector.CredentialInfo::token, + info -> { + if (!info.valid()) { + return AuthCheckResponse.Result.INVALID; + } + final String username = info.credentials().username(); + // does this credential match the account id for the e164 provided in the request? + boolean match = matchingUsername.filter(username::equals).isPresent(); + return match ? AuthCheckResponse.Result.MATCH : AuthCheckResponse.Result.NO_MATCH; + } + ))); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ServerRejectedException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ServerRejectedException.java new file mode 100644 index 000000000..565d77667 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ServerRejectedException.java @@ -0,0 +1,10 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +public class ServerRejectedException extends Exception { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/StaleDevicesException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/StaleDevicesException.java new file mode 100644 index 000000000..7e914f176 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/StaleDevicesException.java @@ -0,0 +1,21 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import java.util.List; + + +public class StaleDevicesException extends Exception { + private final List staleDevices; + + public StaleDevicesException(List staleDevices) { + this.staleDevices = staleDevices; + } + + public List getStaleDevices() { + return staleDevices; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/StickerController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/StickerController.java new file mode 100644 index 000000000..86ffec4de --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/StickerController.java @@ -0,0 +1,86 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.security.SecureRandom; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.HexFormat; +import java.util.LinkedList; +import java.util.List; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.entities.StickerPackFormUploadAttributes; +import org.whispersystems.textsecuregcm.entities.StickerPackFormUploadAttributes.StickerPackFormUploadItem; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.s3.PolicySigner; +import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator; +import org.whispersystems.textsecuregcm.util.Constants; +import org.whispersystems.textsecuregcm.util.Pair; + +@Path("/v1/sticker") +@Tag(name = "Stickers") +public class StickerController { + + private final RateLimiters rateLimiters; + private final PolicySigner policySigner; + private final PostPolicyGenerator policyGenerator; + + public StickerController(RateLimiters rateLimiters, String accessKey, String accessSecret, String region, String bucket) { + this.rateLimiters = rateLimiters; + this.policySigner = new PolicySigner(accessSecret, region); + this.policyGenerator = new PostPolicyGenerator(region, bucket, accessKey); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/pack/form/{count}") + public StickerPackFormUploadAttributes getStickersForm(@Auth AuthenticatedAccount auth, + @PathParam("count") @Min(1) @Max(201) int stickerCount) + throws RateLimitExceededException { + rateLimiters.getStickerPackLimiter().validate(auth.getAccount().getUuid()); + + ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); + String packId = generatePackId(); + String packLocation = "stickers/" + packId; + String manifestKey = packLocation + "/manifest.proto"; + Pair manifestPolicy = policyGenerator.createFor(now, manifestKey, + Constants.MAXIMUM_STICKER_MANIFEST_SIZE_BYTES); + String manifestSignature = policySigner.getSignature(now, manifestPolicy.second()); + StickerPackFormUploadItem manifest = new StickerPackFormUploadItem(-1, manifestKey, manifestPolicy.first(), + "private", "AWS4-HMAC-SHA256", + now.format(PostPolicyGenerator.AWS_DATE_TIME), manifestPolicy.second(), manifestSignature); + + List stickers = new LinkedList<>(); + + for (int i = 0; i < stickerCount; i++) { + String stickerKey = packLocation + "/full/" + i; + Pair stickerPolicy = policyGenerator.createFor(now, stickerKey, + Constants.MAXIMUM_STICKER_SIZE_BYTES); + String stickerSignature = policySigner.getSignature(now, stickerPolicy.second()); + stickers.add(new StickerPackFormUploadItem(i, stickerKey, stickerPolicy.first(), "private", "AWS4-HMAC-SHA256", + now.format(PostPolicyGenerator.AWS_DATE_TIME), stickerPolicy.second(), stickerSignature)); + } + + return new StickerPackFormUploadAttributes(packId, manifest, stickers); + } + + private String generatePackId() { + byte[] object = new byte[16]; + new SecureRandom().nextBytes(object); + + return HexFormat.of().formatHex(object); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java new file mode 100644 index 000000000..475a90a33 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java @@ -0,0 +1,1093 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.HttpHeaders; +import com.stripe.exception.StripeException; +import com.vdurmont.semver4j.Semver; +import io.dropwizard.auth.Auth; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import java.math.BigDecimal; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.VerificationFailedException; +import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequest; +import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse; +import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.badges.BadgeTranslator; +import org.whispersystems.textsecuregcm.badges.LevelTranslator; +import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration; +import org.whispersystems.textsecuregcm.configuration.OneTimeDonationCurrencyConfiguration; +import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; +import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration; +import org.whispersystems.textsecuregcm.entities.Badge; +import org.whispersystems.textsecuregcm.entities.PurchasableBadge; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager; +import org.whispersystems.textsecuregcm.storage.SubscriptionManager; +import org.whispersystems.textsecuregcm.storage.SubscriptionManager.GetResult; +import org.whispersystems.textsecuregcm.subscriptions.BankMandateTranslator; +import org.whispersystems.textsecuregcm.subscriptions.BankTransferType; +import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager; +import org.whispersystems.textsecuregcm.subscriptions.ChargeFailure; +import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod; +import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer; +import org.whispersystems.textsecuregcm.subscriptions.StripeManager; +import org.whispersystems.textsecuregcm.subscriptions.SubscriptionCurrencyUtil; +import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor; +import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorManager; +import org.whispersystems.textsecuregcm.util.ExactlySize; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; +import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; +import org.whispersystems.textsecuregcm.util.ua.UserAgent; +import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; + +@Path("/v1/subscription") +@io.swagger.v3.oas.annotations.tags.Tag(name = "Subscriptions") +public class SubscriptionController { + + private static final Logger logger = LoggerFactory.getLogger(SubscriptionController.class); + + private final Clock clock; + private final SubscriptionConfiguration subscriptionConfiguration; + private final OneTimeDonationConfiguration oneTimeDonationConfiguration; + private final SubscriptionManager subscriptionManager; + private final StripeManager stripeManager; + private final BraintreeManager braintreeManager; + private final ServerZkReceiptOperations zkReceiptOperations; + private final IssuedReceiptsManager issuedReceiptsManager; + private final BadgeTranslator badgeTranslator; + private final LevelTranslator levelTranslator; + private final BankMandateTranslator bankMandateTranslator; + private static final String INVALID_ACCEPT_LANGUAGE_COUNTER_NAME = MetricsUtil.name(SubscriptionController.class, + "invalidAcceptLanguage"); + private static final String RECEIPT_ISSUED_COUNTER_NAME = MetricsUtil.name(SubscriptionController.class, "receiptIssued"); + private static final String PROCESSOR_TAG_NAME = "processor"; + private static final String TYPE_TAG_NAME = "type"; + private static final String EURO_CURRENCY_CODE = "EUR"; + private static final Semver LAST_PROBLEMATIC_IOS_VERSION = new Semver("6.44.0"); + + public SubscriptionController( + @Nonnull Clock clock, + @Nonnull SubscriptionConfiguration subscriptionConfiguration, + @Nonnull OneTimeDonationConfiguration oneTimeDonationConfiguration, + @Nonnull SubscriptionManager subscriptionManager, + @Nonnull StripeManager stripeManager, + @Nonnull BraintreeManager braintreeManager, + @Nonnull ServerZkReceiptOperations zkReceiptOperations, + @Nonnull IssuedReceiptsManager issuedReceiptsManager, + @Nonnull BadgeTranslator badgeTranslator, + @Nonnull LevelTranslator levelTranslator, + @Nonnull BankMandateTranslator bankMandateTranslator) { + this.clock = Objects.requireNonNull(clock); + this.subscriptionConfiguration = Objects.requireNonNull(subscriptionConfiguration); + this.oneTimeDonationConfiguration = Objects.requireNonNull(oneTimeDonationConfiguration); + this.subscriptionManager = Objects.requireNonNull(subscriptionManager); + this.stripeManager = Objects.requireNonNull(stripeManager); + this.braintreeManager = Objects.requireNonNull(braintreeManager); + this.zkReceiptOperations = Objects.requireNonNull(zkReceiptOperations); + this.issuedReceiptsManager = Objects.requireNonNull(issuedReceiptsManager); + this.badgeTranslator = Objects.requireNonNull(badgeTranslator); + this.levelTranslator = Objects.requireNonNull(levelTranslator); + this.bankMandateTranslator = Objects.requireNonNull(bankMandateTranslator); + } + + private Map buildCurrencyConfiguration(@Nullable final UserAgent userAgent) { + final List subscriptionProcessorManagers = List.of(stripeManager, braintreeManager); + return oneTimeDonationConfiguration.currencies() + .entrySet().stream() + .collect(Collectors.toMap(Entry::getKey, currencyAndConfig -> { + final String currency = currencyAndConfig.getKey(); + final OneTimeDonationCurrencyConfiguration currencyConfig = currencyAndConfig.getValue(); + + final Map> oneTimeLevelsToSuggestedAmounts = Map.of( + String.valueOf(oneTimeDonationConfiguration.boost().level()), currencyConfig.boosts(), + String.valueOf(oneTimeDonationConfiguration.gift().level()), List.of(currencyConfig.gift()) + ); + + final Map subscriptionLevelsToAmounts = subscriptionConfiguration.getLevels() + .entrySet().stream() + .filter(levelIdAndConfig -> levelIdAndConfig.getValue().getPrices().containsKey(currency)) + .collect(Collectors.toMap( + levelIdAndConfig -> String.valueOf(levelIdAndConfig.getKey()), + levelIdAndConfig -> levelIdAndConfig.getValue().getPrices().get(currency).amount())); + + final List supportedPaymentMethods = Arrays.stream(PaymentMethod.values()) + .filter(paymentMethod -> !excludePaymentMethod(userAgent, paymentMethod)) + .filter(paymentMethod -> subscriptionProcessorManagers.stream() + .anyMatch(manager -> manager.supportsPaymentMethod(paymentMethod) + && manager.getSupportedCurrenciesForPaymentMethod(paymentMethod).contains(currency))) + .map(PaymentMethod::name) + .collect(Collectors.toList()); + + if (supportedPaymentMethods.isEmpty()) { + throw new RuntimeException("Configuration has currency with no processor support: " + currency); + } + + return new CurrencyConfiguration(currencyConfig.minimum(), oneTimeLevelsToSuggestedAmounts, + subscriptionLevelsToAmounts, supportedPaymentMethods); + })); + } + + // This logic to exclude some iOS client versions from receiving SEPA_DEBIT or IDEAL + // as a supported payment method can be removed after 01-23-24. + private boolean excludePaymentMethod(@Nullable final UserAgent userAgent, final PaymentMethod paymentMethod) { + return (paymentMethod == PaymentMethod.SEPA_DEBIT || paymentMethod == PaymentMethod.IDEAL) + && userAgent != null + && userAgent.getPlatform() == ClientPlatform.IOS + && userAgent.getVersion().isLowerThanOrEqualTo(LAST_PROBLEMATIC_IOS_VERSION); + } + + @VisibleForTesting + GetSubscriptionConfigurationResponse buildGetSubscriptionConfigurationResponse(final List acceptableLanguages, + final UserAgent userAgent) { + final Map levels = new HashMap<>(); + + subscriptionConfiguration.getLevels().forEach((levelId, levelConfig) -> { + final LevelConfiguration levelConfiguration = new LevelConfiguration( + levelTranslator.translate(acceptableLanguages, levelConfig.getBadge()), + badgeTranslator.translate(acceptableLanguages, levelConfig.getBadge())); + levels.put(String.valueOf(levelId), levelConfiguration); + }); + + final Badge boostBadge = badgeTranslator.translate(acceptableLanguages, + oneTimeDonationConfiguration.boost().badge()); + levels.put(String.valueOf(oneTimeDonationConfiguration.boost().level()), + new LevelConfiguration( + boostBadge.getName(), + // NB: the one-time badges are PurchasableBadge, which has a `duration` field + new PurchasableBadge( + boostBadge, + oneTimeDonationConfiguration.boost().expiration()))); + + final Badge giftBadge = badgeTranslator.translate(acceptableLanguages, oneTimeDonationConfiguration.gift().badge()); + levels.put(String.valueOf(oneTimeDonationConfiguration.gift().level()), + new LevelConfiguration( + giftBadge.getName(), + new PurchasableBadge( + giftBadge, + oneTimeDonationConfiguration.gift().expiration()))); + + return new GetSubscriptionConfigurationResponse(buildCurrencyConfiguration(userAgent), levels); + } + + @DELETE + @Path("/{subscriberId}") + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture deleteSubscriber( + @Auth Optional authenticatedAccount, + @PathParam("subscriberId") String subscriberId) { + RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock); + return subscriptionManager.get(requestData.subscriberUser, requestData.hmac) + .thenCompose(getResult -> { + if (getResult == GetResult.NOT_STORED || getResult == GetResult.PASSWORD_MISMATCH) { + throw new NotFoundException(); + } + return getResult.record.getProcessorCustomer() + .map(processorCustomer -> getManagerForProcessor(processorCustomer.processor()).cancelAllActiveSubscriptions(processorCustomer.customerId())) + // a missing customer ID is OK; it means the subscriber never started to add a payment method + .orElseGet(() -> CompletableFuture.completedFuture(null)); + }) + .thenCompose(unused -> subscriptionManager.canceledAt(requestData.subscriberUser, requestData.now)) + .thenApply(unused -> Response.ok().build()); + } + + @PUT + @Path("/{subscriberId}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture updateSubscriber( + @Auth Optional authenticatedAccount, + @PathParam("subscriberId") String subscriberId) { + RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock); + return subscriptionManager.get(requestData.subscriberUser, requestData.hmac) + .thenCompose(getResult -> { + if (getResult == GetResult.PASSWORD_MISMATCH) { + throw new ForbiddenException("subscriberId mismatch"); + } else if (getResult == GetResult.NOT_STORED) { + // create a customer and write it to ddb + return subscriptionManager.create(requestData.subscriberUser, requestData.hmac, requestData.now) + .thenApply(updatedRecord -> { + if (updatedRecord == null) { + throw new ForbiddenException(); + } + return updatedRecord; + }); + } else { + // already exists so just touch access time and return + return subscriptionManager.accessedAt(requestData.subscriberUser, requestData.now) + .thenApply(unused -> getResult.record); + } + }) + .thenApply(record -> Response.ok().build()); + } + + record CreatePaymentMethodResponse(String clientSecret, SubscriptionProcessor processor) { + + } + + @POST + @Path("/{subscriberId}/create_payment_method") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture createPaymentMethod( + @Auth Optional authenticatedAccount, + @PathParam("subscriberId") String subscriberId, + @QueryParam("type") @DefaultValue("CARD") PaymentMethod paymentMethodType) { + + RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock); + + final SubscriptionProcessorManager subscriptionProcessorManager = getManagerForPaymentMethod(paymentMethodType); + + return subscriptionManager.get(requestData.subscriberUser, requestData.hmac) + .thenApply(this::requireRecordFromGetResult) + .thenCompose(record -> { + final CompletableFuture updatedRecordFuture = + record.getProcessorCustomer() + .map(ProcessorCustomer::processor) + .map(processor -> { + if (processor != subscriptionProcessorManager.getProcessor()) { + throw new ClientErrorException("existing processor does not match", Status.CONFLICT); + } + + return CompletableFuture.completedFuture(record); + }) + .orElseGet(() -> subscriptionProcessorManager.createCustomer(requestData.subscriberUser) + .thenApply(ProcessorCustomer::customerId) + .thenCompose(customerId -> subscriptionManager.setProcessorAndCustomerId(record, + new ProcessorCustomer(customerId, subscriptionProcessorManager.getProcessor()), + Instant.now()))); + + return updatedRecordFuture.thenCompose( + updatedRecord -> { + final String customerId = updatedRecord.getProcessorCustomer() + .filter(pc -> pc.processor().equals(subscriptionProcessorManager.getProcessor())) + .orElseThrow(() -> new InternalServerErrorException("record should not be missing customer")) + .customerId(); + return subscriptionProcessorManager.createPaymentMethodSetupToken(customerId); + }); + }) + .thenApply( + token -> Response.ok(new CreatePaymentMethodResponse(token, subscriptionProcessorManager.getProcessor())) + .build()); + } + + public record CreatePayPalBillingAgreementRequest(@NotBlank String returnUrl, @NotBlank String cancelUrl) { + + } + + public record CreatePayPalBillingAgreementResponse(@NotBlank String approvalUrl, @NotBlank String token) { + + } + + @POST + @Path("/{subscriberId}/create_payment_method/paypal") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture createPayPalPaymentMethod( + @Auth Optional authenticatedAccount, + @PathParam("subscriberId") String subscriberId, + @NotNull @Valid CreatePayPalBillingAgreementRequest request, + @Context ContainerRequestContext containerRequestContext) { + + RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock); + + return subscriptionManager.get(requestData.subscriberUser, requestData.hmac) + .thenApply(this::requireRecordFromGetResult) + .thenCompose(record -> { + + final CompletableFuture updatedRecordFuture = + record.getProcessorCustomer() + .map(ProcessorCustomer::processor) + .map(processor -> { + if (processor != braintreeManager.getProcessor()) { + throw new ClientErrorException("existing processor does not match", Status.CONFLICT); + } + return CompletableFuture.completedFuture(record); + }) + .orElseGet(() -> braintreeManager.createCustomer(requestData.subscriberUser) + .thenApply(ProcessorCustomer::customerId) + .thenCompose(customerId -> subscriptionManager.setProcessorAndCustomerId(record, + new ProcessorCustomer(customerId, braintreeManager.getProcessor()), + Instant.now()))); + + return updatedRecordFuture.thenCompose( + updatedRecord -> { + final Locale locale = getAcceptableLanguagesForRequest(containerRequestContext).stream() + .filter(l -> !"*".equals(l.getLanguage())) + .findFirst() + .orElse(Locale.US); + + return braintreeManager.createPayPalBillingAgreement(request.returnUrl, request.cancelUrl, + locale.toLanguageTag()); + }); + }) + .thenApply( + billingAgreementApprovalDetails -> Response.ok( + new CreatePayPalBillingAgreementResponse(billingAgreementApprovalDetails.approvalUrl(), + billingAgreementApprovalDetails.billingAgreementToken())) + .build()); + } + + private SubscriptionProcessorManager getManagerForPaymentMethod(PaymentMethod paymentMethod) { + return switch (paymentMethod) { + case CARD, SEPA_DEBIT, IDEAL -> stripeManager; + case PAYPAL -> braintreeManager; + case UNKNOWN -> throw new BadRequestException("Invalid payment method"); + }; + } + + private SubscriptionProcessorManager getManagerForProcessor(SubscriptionProcessor processor) { + return switch (processor) { + case STRIPE -> stripeManager; + case BRAINTREE -> braintreeManager; + }; + } + + @POST + @Path("/{subscriberId}/default_payment_method/{paymentMethodId}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Deprecated // use /{subscriberId}/default_payment_method/{processor}/{paymentMethodId} + public CompletableFuture setDefaultPaymentMethod( + @Auth Optional authenticatedAccount, + @PathParam("subscriberId") String subscriberId, + @PathParam("paymentMethodId") @NotEmpty String paymentMethodId) { + RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock); + return subscriptionManager.get(requestData.subscriberUser, requestData.hmac) + .thenApply(this::requireRecordFromGetResult) + .thenCompose(record -> stripeManager.setDefaultPaymentMethodForCustomer( + record.getProcessorCustomer().orElseThrow().customerId(), paymentMethodId, record.subscriptionId)) + .thenApply(customer -> Response.ok().build()); + } + + @POST + @Path("/{subscriberId}/default_payment_method/{processor}/{paymentMethodToken}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture setDefaultPaymentMethodWithProcessor( + @Auth Optional authenticatedAccount, + @PathParam("subscriberId") String subscriberId, + @PathParam("processor") SubscriptionProcessor processor, + @PathParam("paymentMethodToken") @NotEmpty String paymentMethodToken) { + RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock); + + final SubscriptionProcessorManager manager = getManagerForProcessor(processor); + + return subscriptionManager.get(requestData.subscriberUser, requestData.hmac) + .thenApply(this::requireRecordFromGetResult) + .thenCompose(record -> record.getProcessorCustomer() + .map(processorCustomer -> manager.setDefaultPaymentMethodForCustomer(processorCustomer.customerId(), + paymentMethodToken, record.subscriptionId)) + .orElseThrow(() -> + // a missing customer ID indicates the client made requests out of order, + // and needs to call create_payment_method to create a customer for the given payment method + new ClientErrorException(Status.CONFLICT))) + .thenApply(customer -> Response.ok().build()); + } + + public record SetSubscriptionLevelSuccessResponse(long level) { + } + + public record SetSubscriptionLevelErrorResponse(List errors) { + + public record Error(SetSubscriptionLevelErrorResponse.Error.Type type, String message) { + + public enum Type { + UNSUPPORTED_LEVEL, + UNSUPPORTED_CURRENCY, + PAYMENT_REQUIRES_ACTION, + } + } + } + + @PUT + @Path("/{subscriberId}/level/{level}/{currency}/{idempotencyKey}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture setSubscriptionLevel( + @Auth Optional authenticatedAccount, + @PathParam("subscriberId") String subscriberId, + @PathParam("level") long level, + @PathParam("currency") String currency, + @PathParam("idempotencyKey") String idempotencyKey) { + RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock); + return subscriptionManager.get(requestData.subscriberUser, requestData.hmac) + .thenApply(this::requireRecordFromGetResult) + .thenCompose(record -> { + + final ProcessorCustomer processorCustomer = record.getProcessorCustomer() + .orElseThrow(() -> + // a missing customer ID indicates the client made requests out of order, + // and needs to call create_payment_method to create a customer for the given payment method + new ClientErrorException(Status.CONFLICT)); + + final String subscriptionTemplateId = getSubscriptionTemplateId(level, currency, + processorCustomer.processor()); + + final SubscriptionProcessorManager manager = getManagerForProcessor(processorCustomer.processor()); + + return Optional.ofNullable(record.subscriptionId).map(subId -> { + // we already have a subscription in our records so let's check the level and currency, + // and only change it if needed + return manager.getSubscription(subId).thenCompose( + subscription -> manager.getLevelAndCurrencyForSubscription(subscription) + .thenCompose(existingLevelAndCurrency -> { + if (existingLevelAndCurrency.equals(new SubscriptionProcessorManager.LevelAndCurrency(level, + currency.toLowerCase(Locale.ROOT)))) { + return CompletableFuture.completedFuture(subscription); + } + return manager.updateSubscription( + subscription, subscriptionTemplateId, level, idempotencyKey) + .thenCompose(updatedSubscription -> + subscriptionManager.subscriptionLevelChanged(requestData.subscriberUser, + requestData.now, + level, updatedSubscription.id()) + .thenApply(unused -> updatedSubscription)); + })); + }).orElseGet(() -> { + long lastSubscriptionCreatedAt = + record.subscriptionCreatedAt != null ? record.subscriptionCreatedAt.getEpochSecond() : 0; + + // we don't have a subscription yet so create it and then record the subscription id + return manager.createSubscription(processorCustomer.customerId(), + subscriptionTemplateId, + level, + lastSubscriptionCreatedAt) + .exceptionally(e -> { + if (e.getCause() instanceof StripeException stripeException + && "subscription_payment_intent_requires_action".equals(stripeException.getCode())) { + throw new BadRequestException(Response.status(Status.BAD_REQUEST) + .entity(new SetSubscriptionLevelErrorResponse(List.of( + new SetSubscriptionLevelErrorResponse.Error( + SetSubscriptionLevelErrorResponse.Error.Type.PAYMENT_REQUIRES_ACTION, null + ) + ))).build()); + } + if (e instanceof RuntimeException re) { + throw re; + } + + throw new CompletionException(e); + }) + .thenCompose(subscription -> subscriptionManager.subscriptionCreated( + requestData.subscriberUser, subscription.id(), requestData.now, level) + .thenApply(unused -> subscription)); + }); + }) + .thenApply(unused -> Response.ok(new SetSubscriptionLevelSuccessResponse(level)).build()); + } + + /** + * Comprehensive configuration for subscriptions and one-time donations + * + * @param currencies map of lower-cased ISO 3 currency codes to minimums and level-specific scalar amounts + * @param levels map of numeric level IDs to level-specific configuration + */ + public record GetSubscriptionConfigurationResponse(Map currencies, + Map levels) { + + } + + /** + * Configuration for a currency - use to present appropriate client interfaces + * + * @param minimum the minimum amount that may be submitted for a one-time donation in the currency + * @param oneTime map of numeric one-time donation level IDs to the list of default amounts to be + * presented + * @param subscription map of numeric subscription level IDs to the amount charged for that level + * @param supportedPaymentMethods the payment methods that support the given currency + */ + public record CurrencyConfiguration(BigDecimal minimum, Map> oneTime, + Map subscription, + List supportedPaymentMethods) { + + } + + /** + * Configuration for a donation level - use to present appropriate client interfaces + * + * @param name the localized name for the level + * @param badge the displayable badge associated with the level + */ + public record LevelConfiguration(String name, Badge badge) { + + } + + @GET + @Path("/configuration") + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture getConfiguration(@Context ContainerRequestContext containerRequestContext, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgentString) { + return CompletableFuture.supplyAsync(() -> { + List acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext); + + UserAgent userAgent; + try { + userAgent = UserAgentUtil.parseUserAgentString(userAgentString); + } catch (UnrecognizedUserAgentException e) { + userAgent = null; + } + return Response.ok(buildGetSubscriptionConfigurationResponse(acceptableLanguages, userAgent)).build(); + }); + } + + @GET + @Path("/bank_mandate/{bankTransferType}") + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture getBankMandate(final @Context ContainerRequestContext containerRequestContext, + final @PathParam("bankTransferType") BankTransferType bankTransferType) { + return CompletableFuture.supplyAsync(() -> { + List acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext); + return Response.ok(new GetBankMandateResponse( + bankMandateTranslator.translate(acceptableLanguages, bankTransferType))).build(); + }); + } + + public record GetBankMandateResponse(String mandate) {} + + public record GetBoostBadgesResponse(Map levels) { + public record Level(PurchasableBadge badge) { + } + } + + @GET + @Path("/boost/badges") + @Produces(MediaType.APPLICATION_JSON) + @Deprecated // use /configuration + public CompletableFuture getBoostBadges(@Context ContainerRequestContext containerRequestContext) { + return CompletableFuture.supplyAsync(() -> { + long boostLevel = oneTimeDonationConfiguration.boost().level(); + String boostBadge = oneTimeDonationConfiguration.boost().badge(); + long giftLevel = oneTimeDonationConfiguration.gift().level(); + String giftBadge = oneTimeDonationConfiguration.gift().badge(); + List acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext); + GetBoostBadgesResponse getBoostBadgesResponse = new GetBoostBadgesResponse(Map.of( + boostLevel, new GetBoostBadgesResponse.Level( + new PurchasableBadge(badgeTranslator.translate(acceptableLanguages, boostBadge), + oneTimeDonationConfiguration.boost().expiration())), + giftLevel, new GetBoostBadgesResponse.Level( + new PurchasableBadge(badgeTranslator.translate(acceptableLanguages, giftBadge), + oneTimeDonationConfiguration.gift().expiration())))); + return Response.ok(getBoostBadgesResponse).build(); + }); + } + + public static class CreateBoostRequest { + + @NotEmpty + @ExactlySize(3) + public String currency; + @Min(1) + public long amount; + public Long level; + public PaymentMethod paymentMethod = PaymentMethod.CARD; + } + + public static class CreatePayPalBoostRequest extends CreateBoostRequest { + + @NotEmpty + public String returnUrl; + @NotEmpty + public String cancelUrl; + + public CreatePayPalBoostRequest() { + super.paymentMethod = PaymentMethod.PAYPAL; + } + } + + record CreatePayPalBoostResponse(String approvalUrl, String paymentId) { + + } + + public record CreateBoostResponse(String clientSecret) { + } + + /** + * Creates a Stripe PaymentIntent with the requested amount and currency + */ + @POST + @Path("/boost/create") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture createBoostPaymentIntent(@NotNull @Valid CreateBoostRequest request) { + return CompletableFuture.runAsync(() -> { + if (request.level == null) { + request.level = oneTimeDonationConfiguration.boost().level(); + } + BigDecimal amount = BigDecimal.valueOf(request.amount); + if (request.level == oneTimeDonationConfiguration.gift().level()) { + BigDecimal amountConfigured = oneTimeDonationConfiguration.currencies() + .get(request.currency.toLowerCase(Locale.ROOT)).gift(); + if (amountConfigured == null || + SubscriptionCurrencyUtil.convertConfiguredAmountToStripeAmount(request.currency, amountConfigured) + .compareTo(amount) != 0) { + throw new WebApplicationException( + Response.status(Status.CONFLICT).entity(Map.of("error", "level_amount_mismatch")).build()); + } + } + validateRequestCurrencyAmount(request, amount, stripeManager); + }) + .thenCompose(unused -> stripeManager.createPaymentIntent(request.currency, request.amount, request.level)) + .thenApply(paymentIntent -> Response.ok(new CreateBoostResponse(paymentIntent.getClientSecret())).build()); + } + + /** + * Validates that the currency is supported by the {@code manager} and {@code request.paymentMethod} + * and that the amount meets minimum and maximum constraints. + * + * @throws BadRequestException indicates validation failed. Inspect {@code response.error} for details + */ + private void validateRequestCurrencyAmount(CreateBoostRequest request, BigDecimal amount, + SubscriptionProcessorManager manager) { + if (!manager.getSupportedCurrenciesForPaymentMethod(request.paymentMethod).contains(request.currency.toLowerCase(Locale.ROOT))) { + throw new BadRequestException(Response.status(Status.BAD_REQUEST) + .entity(Map.of("error", "unsupported_currency")).build()); + } + + BigDecimal minCurrencyAmountMajorUnits = oneTimeDonationConfiguration.currencies() + .get(request.currency.toLowerCase(Locale.ROOT)).minimum(); + BigDecimal minCurrencyAmountMinorUnits = SubscriptionCurrencyUtil.convertConfiguredAmountToApiAmount( + request.currency, + minCurrencyAmountMajorUnits); + if (minCurrencyAmountMinorUnits.compareTo(amount) > 0) { + throw new BadRequestException(Response.status(Status.BAD_REQUEST) + .entity(Map.of( + "error", "amount_below_currency_minimum", + "minimum", minCurrencyAmountMajorUnits.toString())).build()); + } + + if (request.paymentMethod == PaymentMethod.SEPA_DEBIT && + amount.compareTo(SubscriptionCurrencyUtil.convertConfiguredAmountToApiAmount( + EURO_CURRENCY_CODE, + oneTimeDonationConfiguration.sepaMaxTransactionSizeEuros())) > 0) { + throw new BadRequestException(Response.status(Status.BAD_REQUEST) + .entity(Map.of( + "error", "amount_above_sepa_limit", + "maximum", oneTimeDonationConfiguration.sepaMaxTransactionSizeEuros().toString())).build()); + } + } + + @POST + @Path("/boost/paypal/create") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture createPayPalBoost(@NotNull @Valid CreatePayPalBoostRequest request, + @Context ContainerRequestContext containerRequestContext) { + + return CompletableFuture.runAsync(() -> { + if (request.level == null) { + request.level = oneTimeDonationConfiguration.boost().level(); + } + + validateRequestCurrencyAmount(request, BigDecimal.valueOf(request.amount), braintreeManager); + }) + .thenCompose(unused -> { + final Locale locale = getAcceptableLanguagesForRequest(containerRequestContext).stream() + .filter(l -> !"*".equals(l.getLanguage())) + .findFirst() + .orElse(Locale.US); + + return braintreeManager.createOneTimePayment(request.currency.toUpperCase(Locale.ROOT), request.amount, + locale.toLanguageTag(), + request.returnUrl, request.cancelUrl); + }) + .thenApply(approvalDetails -> Response.ok( + new CreatePayPalBoostResponse(approvalDetails.approvalUrl(), approvalDetails.paymentId())).build()); + } + + public static class ConfirmPayPalBoostRequest extends CreateBoostRequest { + + @NotEmpty + public String payerId; + @NotEmpty + public String paymentId; // PAYID-… + @NotEmpty + public String paymentToken; // EC-… + } + + record ConfirmPayPalBoostResponse(String paymentId) { + + } + + @POST + @Path("/boost/paypal/confirm") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture confirmPayPalBoost(@NotNull @Valid ConfirmPayPalBoostRequest request) { + + return CompletableFuture.runAsync(() -> { + if (request.level == null) { + request.level = oneTimeDonationConfiguration.boost().level(); + } + }) + .thenCompose(unused -> braintreeManager.captureOneTimePayment(request.payerId, request.paymentId, + request.paymentToken, request.currency, request.amount, request.level)) + .thenApply(chargeSuccessDetails -> Response.ok( + new ConfirmPayPalBoostResponse(chargeSuccessDetails.paymentId())).build()); + } + + public static class CreateBoostReceiptCredentialsRequest { + + /** + * a payment ID from {@link #processor} + */ + @NotNull + public String paymentIntentId; + @NotNull + public byte[] receiptCredentialRequest; + + @NotNull + public SubscriptionProcessor processor = SubscriptionProcessor.STRIPE; + } + + public record CreateBoostReceiptCredentialsResponse(byte[] receiptCredentialResponse) { + } + + @POST + @Path("/boost/receipt_credentials") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture createBoostReceiptCredentials( + @NotNull @Valid final CreateBoostReceiptCredentialsRequest request, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent) { + + final SubscriptionProcessorManager manager = getManagerForProcessor(request.processor); + + return manager.getPaymentDetails(request.paymentIntentId) + .thenCompose(paymentDetails -> { + if (paymentDetails == null) { + throw new WebApplicationException(Status.NOT_FOUND); + } + switch (paymentDetails.status()) { + case PROCESSING -> throw new WebApplicationException(Status.NO_CONTENT); + case SUCCEEDED -> { + } + default -> throw new WebApplicationException(Status.PAYMENT_REQUIRED); + } + + long level = oneTimeDonationConfiguration.boost().level(); + if (paymentDetails.customMetadata() != null) { + String levelMetadata = paymentDetails.customMetadata() + .getOrDefault("level", Long.toString(oneTimeDonationConfiguration.boost().level())); + try { + level = Long.parseLong(levelMetadata); + } catch (NumberFormatException e) { + logger.error("failed to parse level metadata ({}) on payment intent {}", levelMetadata, + paymentDetails.id(), e); + throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); + } + } + Duration levelExpiration; + if (oneTimeDonationConfiguration.boost().level() == level) { + levelExpiration = oneTimeDonationConfiguration.boost().expiration(); + } else if (oneTimeDonationConfiguration.gift().level() == level) { + levelExpiration = oneTimeDonationConfiguration.gift().expiration(); + } else { + logger.error("level ({}) returned from payment intent that is unknown to the server", level); + throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); + } + ReceiptCredentialRequest receiptCredentialRequest; + try { + receiptCredentialRequest = new ReceiptCredentialRequest(request.receiptCredentialRequest); + } catch (InvalidInputException e) { + throw new BadRequestException("invalid receipt credential request", e); + } + final long finalLevel = level; + return issuedReceiptsManager.recordIssuance(paymentDetails.id(), manager.getProcessor(), + receiptCredentialRequest, clock.instant()) + .thenApply(unused -> { + Instant expiration = paymentDetails.created() + .plus(levelExpiration) + .truncatedTo(ChronoUnit.DAYS) + .plus(1, ChronoUnit.DAYS); + ReceiptCredentialResponse receiptCredentialResponse; + try { + receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential( + receiptCredentialRequest, expiration.getEpochSecond(), finalLevel); + } catch (VerificationFailedException e) { + throw new BadRequestException("receipt credential request failed verification", e); + } + Metrics.counter(RECEIPT_ISSUED_COUNTER_NAME, + Tags.of( + Tag.of(PROCESSOR_TAG_NAME, manager.getProcessor().toString()), + Tag.of(TYPE_TAG_NAME, "boost"), + UserAgentTagUtil.getPlatformTag(userAgent))) + .increment(); + return Response.ok(new CreateBoostReceiptCredentialsResponse(receiptCredentialResponse.serialize())) + .build(); + }); + }); + } + + public record GetSubscriptionInformationResponse( + SubscriptionController.GetSubscriptionInformationResponse.Subscription subscription, + @JsonInclude(Include.NON_NULL) ChargeFailure chargeFailure) { + + public record Subscription(long level, Instant billingCycleAnchor, Instant endOfCurrentPeriod, boolean active, + boolean cancelAtPeriodEnd, String currency, BigDecimal amount, String status, + SubscriptionProcessor processor, PaymentMethod paymentMethod, boolean paymentProcessing) { + + } + } + + @GET + @Path("/{subscriberId}") + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture getSubscriptionInformation( + @Auth Optional authenticatedAccount, + @PathParam("subscriberId") String subscriberId) { + RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock); + return subscriptionManager.get(requestData.subscriberUser, requestData.hmac) + .thenApply(this::requireRecordFromGetResult) + .thenCompose(record -> { + if (record.subscriptionId == null) { + return CompletableFuture.completedFuture(Response.ok(new GetSubscriptionInformationResponse(null, null)).build()); + } + + final SubscriptionProcessorManager manager = getManagerForProcessor(record.getProcessorCustomer().orElseThrow().processor()); + + return manager.getSubscription(record.subscriptionId).thenCompose(subscription -> + manager.getSubscriptionInformation(subscription).thenApply(subscriptionInformation -> Response.ok( + new GetSubscriptionInformationResponse( + new GetSubscriptionInformationResponse.Subscription( + subscriptionInformation.level(), + subscriptionInformation.billingCycleAnchor(), + subscriptionInformation.endOfCurrentPeriod(), + subscriptionInformation.active(), + subscriptionInformation.cancelAtPeriodEnd(), + subscriptionInformation.price().currency(), + subscriptionInformation.price().amount(), + subscriptionInformation.status().getApiValue(), + manager.getProcessor(), + subscriptionInformation.paymentMethod(), + subscriptionInformation.paymentProcessing()), + subscriptionInformation.chargeFailure() + )).build())); + }); + } + + public record GetReceiptCredentialsRequest(@NotEmpty byte[] receiptCredentialRequest) { + } + + public record GetReceiptCredentialsResponse(@NotEmpty byte[] receiptCredentialResponse) { + } + + @POST + @Path("/{subscriberId}/receipt_credentials") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture createSubscriptionReceiptCredentials( + @Auth Optional authenticatedAccount, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, + @PathParam("subscriberId") String subscriberId, + @NotNull @Valid GetReceiptCredentialsRequest request) { + RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock); + return subscriptionManager.get(requestData.subscriberUser, requestData.hmac) + .thenApply(this::requireRecordFromGetResult) + .thenCompose(record -> { + if (record.subscriptionId == null) { + return CompletableFuture.completedFuture(Response.status(Status.NOT_FOUND).build()); + } + ReceiptCredentialRequest receiptCredentialRequest; + try { + receiptCredentialRequest = new ReceiptCredentialRequest(request.receiptCredentialRequest()); + } catch (InvalidInputException e) { + throw new BadRequestException("invalid receipt credential request", e); + } + + final SubscriptionProcessorManager manager = getManagerForProcessor(record.getProcessorCustomer().orElseThrow().processor()); + return manager.getReceiptItem(record.subscriptionId) + .thenCompose(receipt -> issuedReceiptsManager.recordIssuance( + receipt.itemId(), manager.getProcessor(), receiptCredentialRequest, + requestData.now) + .thenApply(unused -> receipt)) + .thenApply(receipt -> { + ReceiptCredentialResponse receiptCredentialResponse; + try { + receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential( + receiptCredentialRequest, + receiptExpirationWithGracePeriod(receipt.expiration()).getEpochSecond(), receipt.level()); + } catch (VerificationFailedException e) { + throw new BadRequestException("receipt credential request failed verification", e); + } + Metrics.counter(RECEIPT_ISSUED_COUNTER_NAME, + Tags.of( + Tag.of(PROCESSOR_TAG_NAME, manager.getProcessor().toString()), + Tag.of(TYPE_TAG_NAME, "subscription"), + UserAgentTagUtil.getPlatformTag(userAgent))) + .increment(); + return Response.ok(new GetReceiptCredentialsResponse(receiptCredentialResponse.serialize())) + .build(); + }); + }); + } + + private Instant receiptExpirationWithGracePeriod(Instant itemExpiration) { + return itemExpiration.plus(subscriptionConfiguration.getBadgeGracePeriod()) + .truncatedTo(ChronoUnit.DAYS) + .plus(1, ChronoUnit.DAYS); + } + + private String getSubscriptionTemplateId(long level, String currency, SubscriptionProcessor processor) { + SubscriptionLevelConfiguration levelConfiguration = subscriptionConfiguration.getLevels().get(level); + if (levelConfiguration == null) { + throw new BadRequestException(Response.status(Status.BAD_REQUEST) + .entity(new SetSubscriptionLevelErrorResponse(List.of( + new SetSubscriptionLevelErrorResponse.Error( + SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_LEVEL, null)))) + .build()); + } + + return Optional.ofNullable(levelConfiguration.getPrices() + .get(currency.toLowerCase(Locale.ROOT))) + .map(priceConfiguration -> priceConfiguration.processorIds().get(processor)) + .orElseThrow(() -> new BadRequestException(Response.status(Status.BAD_REQUEST) + .entity(new SetSubscriptionLevelErrorResponse(List.of( + new SetSubscriptionLevelErrorResponse.Error( + SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_CURRENCY, null)))) + .build())); + } + + private SubscriptionManager.Record requireRecordFromGetResult(SubscriptionManager.GetResult getResult) { + if (getResult == GetResult.PASSWORD_MISMATCH) { + throw new ForbiddenException("subscriberId mismatch"); + } else if (getResult == GetResult.NOT_STORED) { + throw new NotFoundException(); + } else { + return getResult.record; + } + } + + private List getAcceptableLanguagesForRequest(ContainerRequestContext containerRequestContext) { + try { + return containerRequestContext.getAcceptableLanguages(); + } catch (final ProcessingException e) { + final String userAgent = containerRequestContext.getHeaderString(HttpHeaders.USER_AGENT); + Metrics.counter(INVALID_ACCEPT_LANGUAGE_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment(); + logger.debug("Could not get acceptable languages; Accept-Language: {}; User-Agent: {}", + containerRequestContext.getHeaderString(HttpHeaders.ACCEPT_LANGUAGE), + userAgent, + e); + + return List.of(); + } + } + + private record RequestData(@Nonnull byte[] subscriberBytes, + @Nonnull byte[] subscriberUser, + @Nonnull byte[] subscriberKey, + @Nonnull byte[] hmac, + @Nonnull Instant now) { + + public static RequestData process( + Optional authenticatedAccount, + String subscriberId, + Clock clock) { + Instant now = clock.instant(); + if (authenticatedAccount.isPresent()) { + throw new ForbiddenException("must not use authenticated connection for subscriber operations"); + } + byte[] subscriberBytes = convertSubscriberIdStringToBytes(subscriberId); + byte[] subscriberUser = getUser(subscriberBytes); + byte[] subscriberKey = getKey(subscriberBytes); + byte[] hmac = computeHmac(subscriberUser, subscriberKey); + return new RequestData(subscriberBytes, subscriberUser, subscriberKey, hmac, now); + } + + private static byte[] convertSubscriberIdStringToBytes(String subscriberId) { + try { + byte[] bytes = Base64.getUrlDecoder().decode(subscriberId); + if (bytes.length != 32) { + throw new NotFoundException(); + } + return bytes; + } catch (IllegalArgumentException e) { + throw new NotFoundException(e); + } + } + + private static byte[] getUser(byte[] subscriberBytes) { + byte[] user = new byte[16]; + System.arraycopy(subscriberBytes, 0, user, 0, user.length); + return user; + } + + private static byte[] getKey(byte[] subscriberBytes) { + byte[] key = new byte[16]; + System.arraycopy(subscriberBytes, 16, key, 0, key.length); + return key; + } + + private static byte[] computeHmac(byte[] subscriberUser, byte[] subscriberKey) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(subscriberKey, "HmacSHA256")); + return mac.doFinal(subscriberUser); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new InternalServerErrorException(e); + } + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java new file mode 100644 index 000000000..8b1771695 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java @@ -0,0 +1,688 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.Phonenumber; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.time.Clock; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.HexFormat; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletionException; +import java.util.concurrent.TimeUnit; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.Consumes; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.PATCH; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.ServerErrorException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.captcha.AssessmentResult; +import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager; +import org.whispersystems.textsecuregcm.entities.CreateVerificationSessionRequest; +import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; +import org.whispersystems.textsecuregcm.entities.SubmitVerificationCodeRequest; +import org.whispersystems.textsecuregcm.entities.UpdateVerificationSessionRequest; +import org.whispersystems.textsecuregcm.entities.VerificationCodeRequest; +import org.whispersystems.textsecuregcm.entities.VerificationSessionResponse; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.push.PushNotification; +import org.whispersystems.textsecuregcm.push.PushNotificationManager; +import org.whispersystems.textsecuregcm.registration.ClientType; +import org.whispersystems.textsecuregcm.registration.MessageTransport; +import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; +import org.whispersystems.textsecuregcm.registration.RegistrationServiceException; +import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException; +import org.whispersystems.textsecuregcm.registration.TransportNotAllowedException; +import org.whispersystems.textsecuregcm.registration.VerificationSession; +import org.whispersystems.textsecuregcm.spam.Extract; +import org.whispersystems.textsecuregcm.spam.FilterSpam; +import org.whispersystems.textsecuregcm.spam.ScoreThreshold; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; +import org.whispersystems.textsecuregcm.storage.VerificationSessionManager; +import org.whispersystems.textsecuregcm.util.ExceptionUtils; +import org.whispersystems.textsecuregcm.util.HeaderUtils; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.Util; + +@Path("/v1/verification") +@io.swagger.v3.oas.annotations.tags.Tag(name = "Verification") +public class VerificationController { + + private static final Logger logger = LoggerFactory.getLogger(VerificationController.class); + private static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15); + private static final Duration DYNAMODB_TIMEOUT = Duration.ofSeconds(5); + + private static final SecureRandom RANDOM = new SecureRandom(); + + private static final String PUSH_CHALLENGE_COUNTER_NAME = name(VerificationController.class, "pushChallenge"); + private static final String CHALLENGE_PRESENT_TAG_NAME = "present"; + private static final String CHALLENGE_MATCH_TAG_NAME = "matches"; + private static final String CAPTCHA_ATTEMPT_COUNTER_NAME = name(VerificationController.class, "captcha"); + private static final String COUNTRY_CODE_TAG_NAME = "countryCode"; + private static final String REGION_CODE_TAG_NAME = "regionCode"; + private static final String SCORE_TAG_NAME = "score"; + private static final String CODE_REQUESTED_COUNTER_NAME = name(VerificationController.class, "codeRequested"); + private static final String VERIFICATION_TRANSPORT_TAG_NAME = "transport"; + private static final String VERIFIED_COUNTER_NAME = name(VerificationController.class, "verified"); + private static final String SUCCESS_TAG_NAME = "success"; + + private final RegistrationServiceClient registrationServiceClient; + private final VerificationSessionManager verificationSessionManager; + private final PushNotificationManager pushNotificationManager; + private final RegistrationCaptchaManager registrationCaptchaManager; + private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager; + private final RateLimiters rateLimiters; + private final AccountsManager accountsManager; + + private final Clock clock; + + public VerificationController(final RegistrationServiceClient registrationServiceClient, + final VerificationSessionManager verificationSessionManager, + final PushNotificationManager pushNotificationManager, + final RegistrationCaptchaManager registrationCaptchaManager, + final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager, + final RateLimiters rateLimiters, + final AccountsManager accountsManager, + final Clock clock) { + this.registrationServiceClient = registrationServiceClient; + this.verificationSessionManager = verificationSessionManager; + this.pushNotificationManager = pushNotificationManager; + this.registrationCaptchaManager = registrationCaptchaManager; + this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager; + this.rateLimiters = rateLimiters; + this.accountsManager = accountsManager; + this.clock = clock; + } + + @POST + @Path("/session") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public VerificationSessionResponse createSession(@NotNull @Valid CreateVerificationSessionRequest request) + throws RateLimitExceededException { + + final Pair pushTokenAndType = validateAndExtractPushToken( + request.getUpdateVerificationSessionRequest()); + + final Phonenumber.PhoneNumber phoneNumber; + try { + phoneNumber = PhoneNumberUtil.getInstance().parse(request.getNumber(), null); + } catch (final NumberParseException e) { + throw new ServerErrorException("could not parse already validated number", Response.Status.INTERNAL_SERVER_ERROR); + } + + final RegistrationServiceSession registrationServiceSession; + try { + registrationServiceSession = registrationServiceClient.createRegistrationSession(phoneNumber, + accountsManager.getByE164(request.getNumber()).isPresent(), + REGISTRATION_RPC_TIMEOUT).join(); + } catch (final CancellationException e) { + + throw new ServerErrorException("registration service unavailable", Response.Status.SERVICE_UNAVAILABLE); + } catch (final CompletionException e) { + + if (ExceptionUtils.unwrap(e) instanceof RateLimitExceededException re) { + RateLimiter.adaptLegacyException(() -> { + throw re; + }); + } + + throw new ServerErrorException(Response.Status.INTERNAL_SERVER_ERROR, e); + } + + VerificationSession verificationSession = new VerificationSession(null, new ArrayList<>(), + Collections.emptyList(), false, + clock.millis(), clock.millis(), registrationServiceSession.expiration()); + + verificationSession = handlePushToken(pushTokenAndType, verificationSession); + // unconditionally request a captcha -- it will either be the only requested information, or a fallback + // if a push challenge sent in `handlePushToken` doesn't arrive in time + verificationSession.requestedInformation().add(VerificationSession.Information.CAPTCHA); + + storeVerificationSession(registrationServiceSession, verificationSession); + + return buildResponse(registrationServiceSession, verificationSession); + } + + @FilterSpam + @PATCH + @Path("/session/{sessionId}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public VerificationSessionResponse updateSession(@PathParam("sessionId") final String encodedSessionId, + @HeaderParam(com.google.common.net.HttpHeaders.X_FORWARDED_FOR) String forwardedFor, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, + @NotNull @Valid final UpdateVerificationSessionRequest updateVerificationSessionRequest, + @NotNull @Extract final ScoreThreshold captchaScoreThreshold) { + + final String sourceHost = HeaderUtils.getMostRecentProxy(forwardedFor).orElseThrow(); + + final Pair pushTokenAndType = validateAndExtractPushToken( + updateVerificationSessionRequest); + + final RegistrationServiceSession registrationServiceSession = retrieveRegistrationServiceSession(encodedSessionId); + VerificationSession verificationSession = retrieveVerificationSession(registrationServiceSession); + + try { + // these handle* methods ordered from least likely to fail to most, so take care when considering a change + verificationSession = handlePushToken(pushTokenAndType, verificationSession); + + verificationSession = handlePushChallenge(updateVerificationSessionRequest, registrationServiceSession, + verificationSession); + + verificationSession = handleCaptcha(sourceHost, updateVerificationSessionRequest, registrationServiceSession, + verificationSession, userAgent, captchaScoreThreshold.getScoreThreshold()); + } catch (final RateLimitExceededException e) { + + final Response response = buildResponseForRateLimitExceeded(verificationSession, registrationServiceSession, + e.getRetryDuration()); + throw new ClientErrorException(response); + + } catch (final ForbiddenException e) { + + throw new ClientErrorException(Response.status(Response.Status.FORBIDDEN) + .entity(buildResponse(registrationServiceSession, verificationSession)) + .build()); + + } finally { + // Each of the handle* methods may update requestedInformation, submittedInformation, and allowedToRequestCode, + // and we want to be sure to store a changes, even if a later method throws + updateStoredVerificationSession(registrationServiceSession, verificationSession); + } + + return buildResponse(registrationServiceSession, verificationSession); + } + + private void storeVerificationSession(final RegistrationServiceSession registrationServiceSession, + final VerificationSession verificationSession) { + verificationSessionManager.insert(registrationServiceSession.encodedSessionId(), verificationSession) + .orTimeout(DYNAMODB_TIMEOUT.toSeconds(), TimeUnit.SECONDS) + .join(); + } + + private void updateStoredVerificationSession(final RegistrationServiceSession registrationServiceSession, + final VerificationSession verificationSession) { + verificationSessionManager.update(registrationServiceSession.encodedSessionId(), verificationSession) + .orTimeout(DYNAMODB_TIMEOUT.toSeconds(), TimeUnit.SECONDS) + .join(); + } + + /** + * If {@code pushTokenAndType} values are not {@code null}, sends a push challenge. If there is no existing push + * challenge in the session, one will be created, set on the returned session record, and + * {@link VerificationSession#requestedInformation()} will be updated. + */ + private VerificationSession handlePushToken( + final Pair pushTokenAndType, VerificationSession verificationSession) { + + if (pushTokenAndType.first() != null) { + + if (verificationSession.pushChallenge() == null) { + + final List requestedInformation = new ArrayList<>(); + requestedInformation.add(VerificationSession.Information.PUSH_CHALLENGE); + requestedInformation.addAll(verificationSession.requestedInformation()); + + verificationSession = new VerificationSession(generatePushChallenge(), requestedInformation, + verificationSession.submittedInformation(), verificationSession.allowedToRequestCode(), + verificationSession.createdTimestamp(), clock.millis(), verificationSession.remoteExpirationSeconds() + ); + } + + pushNotificationManager.sendRegistrationChallengeNotification(pushTokenAndType.first(), pushTokenAndType.second(), + verificationSession.pushChallenge()); + } + + return verificationSession; + } + + /** + * If a push challenge value is present, compares against the stored value. If they match, then + * {@link VerificationSession.Information#PUSH_CHALLENGE} is removed from requested information, added to submitted + * information, and {@link VerificationSession#allowedToRequestCode()} is re-evaluated. + * + * @throws ForbiddenException if values to not match. + * @throws RateLimitExceededException if too many push challenges have been submitted + */ + private VerificationSession handlePushChallenge( + final UpdateVerificationSessionRequest updateVerificationSessionRequest, + final RegistrationServiceSession registrationServiceSession, + VerificationSession verificationSession) throws RateLimitExceededException { + + if (verificationSession.submittedInformation() + .contains(VerificationSession.Information.PUSH_CHALLENGE)) { + // skip if a challenge has already been submitted + return verificationSession; + } + + final boolean pushChallengePresent = updateVerificationSessionRequest.pushChallenge() != null; + if (pushChallengePresent) { + RateLimiter.adaptLegacyException( + () -> rateLimiters.getVerificationPushChallengeLimiter() + .validate(registrationServiceSession.encodedSessionId())); + } + + final boolean pushChallengeMatches; + if (pushChallengePresent && verificationSession.pushChallenge() != null) { + pushChallengeMatches = MessageDigest.isEqual( + updateVerificationSessionRequest.pushChallenge().getBytes(StandardCharsets.UTF_8), + verificationSession.pushChallenge().getBytes(StandardCharsets.UTF_8)); + } else { + pushChallengeMatches = false; + } + + Metrics.counter(PUSH_CHALLENGE_COUNTER_NAME, + COUNTRY_CODE_TAG_NAME, Util.getCountryCode(registrationServiceSession.number()), + REGION_CODE_TAG_NAME, Util.getRegion(registrationServiceSession.number()), + CHALLENGE_PRESENT_TAG_NAME, Boolean.toString(pushChallengePresent), + CHALLENGE_MATCH_TAG_NAME, Boolean.toString(pushChallengeMatches)) + .increment(); + + if (pushChallengeMatches) { + final List submittedInformation = new ArrayList<>( + verificationSession.submittedInformation()); + submittedInformation.add(VerificationSession.Information.PUSH_CHALLENGE); + + final List requestedInformation = new ArrayList<>( + verificationSession.requestedInformation()); + // a push challenge satisfies a requested captcha + requestedInformation.remove(VerificationSession.Information.CAPTCHA); + final boolean allowedToRequestCode = (verificationSession.allowedToRequestCode() + || requestedInformation.remove(VerificationSession.Information.PUSH_CHALLENGE)) + && requestedInformation.isEmpty(); + + verificationSession = new VerificationSession(verificationSession.pushChallenge(), requestedInformation, + submittedInformation, allowedToRequestCode, verificationSession.createdTimestamp(), clock.millis(), + verificationSession.remoteExpirationSeconds()); + + } else if (pushChallengePresent) { + throw new ForbiddenException(); + } + return verificationSession; + } + + /** + * If a captcha value is present, it is assessed. If it is valid, then {@link VerificationSession.Information#CAPTCHA} + * is removed from requested information, added to submitted information, and + * {@link VerificationSession#allowedToRequestCode()} is re-evaluated. + * + * @throws ForbiddenException if assessment is not valid. + * @throws RateLimitExceededException if too many captchas have been submitted + */ + private VerificationSession handleCaptcha( + final String sourceHost, + final UpdateVerificationSessionRequest updateVerificationSessionRequest, + final RegistrationServiceSession registrationServiceSession, + VerificationSession verificationSession, + final String userAgent, + final Optional captchaScoreThreshold) throws RateLimitExceededException { + + if (updateVerificationSessionRequest.captcha() == null) { + return verificationSession; + } + + RateLimiter.adaptLegacyException( + () -> rateLimiters.getVerificationCaptchaLimiter().validate(registrationServiceSession.encodedSessionId())); + + final AssessmentResult assessmentResult; + try { + + assessmentResult = registrationCaptchaManager.assessCaptcha( + Optional.of(updateVerificationSessionRequest.captcha()), sourceHost) + .orElseThrow(() -> new ServerErrorException(Response.Status.INTERNAL_SERVER_ERROR)); + + Metrics.counter(CAPTCHA_ATTEMPT_COUNTER_NAME, Tags.of( + Tag.of(SUCCESS_TAG_NAME, String.valueOf(assessmentResult.isValid(captchaScoreThreshold))), + UserAgentTagUtil.getPlatformTag(userAgent), + Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(registrationServiceSession.number())), + Tag.of(REGION_CODE_TAG_NAME, Util.getRegion(registrationServiceSession.number())), + Tag.of(SCORE_TAG_NAME, assessmentResult.getScoreString()))) + .increment(); + + } catch (IOException e) { + throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE); + } + + if (assessmentResult.isValid(captchaScoreThreshold)) { + final List submittedInformation = new ArrayList<>( + verificationSession.submittedInformation()); + submittedInformation.add(VerificationSession.Information.CAPTCHA); + + final List requestedInformation = new ArrayList<>( + verificationSession.requestedInformation()); + // a captcha satisfies a push challenge, in case of push deliverability issues + requestedInformation.remove(VerificationSession.Information.PUSH_CHALLENGE); + final boolean allowedToRequestCode = (verificationSession.allowedToRequestCode() + || requestedInformation.remove(VerificationSession.Information.CAPTCHA)) + && requestedInformation.isEmpty(); + + verificationSession = new VerificationSession(verificationSession.pushChallenge(), requestedInformation, + submittedInformation, allowedToRequestCode, verificationSession.createdTimestamp(), clock.millis(), + verificationSession.remoteExpirationSeconds()); + } else { + throw new ForbiddenException(); + } + + return verificationSession; + } + + @GET + @Path("/session/{sessionId}") + @Produces(MediaType.APPLICATION_JSON) + public VerificationSessionResponse getSession(@PathParam("sessionId") final String encodedSessionId) { + + final RegistrationServiceSession registrationServiceSession = retrieveRegistrationServiceSession(encodedSessionId); + final VerificationSession verificationSession = retrieveVerificationSession(registrationServiceSession); + + return buildResponse(registrationServiceSession, verificationSession); + } + + @POST + @Path("/session/{sessionId}/code") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public VerificationSessionResponse requestVerificationCode(@PathParam("sessionId") final String encodedSessionId, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) Optional acceptLanguage, + @NotNull @Valid VerificationCodeRequest verificationCodeRequest) throws Throwable { + + final RegistrationServiceSession registrationServiceSession = retrieveRegistrationServiceSession(encodedSessionId); + final VerificationSession verificationSession = retrieveVerificationSession(registrationServiceSession); + + if (registrationServiceSession.verified()) { + throw new ClientErrorException( + Response.status(Response.Status.CONFLICT) + .entity(buildResponse(registrationServiceSession, verificationSession)) + .build()); + } + + if (!verificationSession.allowedToRequestCode()) { + final Response.Status status = verificationSession.requestedInformation().isEmpty() + ? Response.Status.TOO_MANY_REQUESTS + : Response.Status.CONFLICT; + + throw new ClientErrorException( + Response.status(status) + .entity(buildResponse(registrationServiceSession, verificationSession)) + .build()); + } + + final MessageTransport messageTransport = verificationCodeRequest.transport().toMessageTransport(); + + final ClientType clientType = switch (verificationCodeRequest.client()) { + case "ios" -> ClientType.IOS; + case "android-2021-03" -> ClientType.ANDROID_WITH_FCM; + default -> { + if (StringUtils.startsWithIgnoreCase(verificationCodeRequest.client(), "android")) { + yield ClientType.ANDROID_WITHOUT_FCM; + } + yield ClientType.UNKNOWN; + } + }; + + final RegistrationServiceSession resultSession; + try { + resultSession = registrationServiceClient.sendVerificationCode(registrationServiceSession.id(), + messageTransport, + clientType, + acceptLanguage.orElse(null), REGISTRATION_RPC_TIMEOUT).join(); + } catch (final CancellationException e) { + throw new ServerErrorException("registration service unavailable", Response.Status.SERVICE_UNAVAILABLE); + } catch (final CompletionException e) { + final Throwable unwrappedException = ExceptionUtils.unwrap(e); + if (unwrappedException instanceof RateLimitExceededException rateLimitExceededException) { + if (rateLimitExceededException instanceof VerificationSessionRateLimitExceededException ve) { + final Response response = buildResponseForRateLimitExceeded(verificationSession, ve.getRegistrationSession(), + ve.getRetryDuration()); + throw new ClientErrorException(response); + } + + throw new RateLimitExceededException(rateLimitExceededException.getRetryDuration().orElse(null), false); + } else if (unwrappedException instanceof RegistrationServiceException registrationServiceException) { + + throw registrationServiceException.getRegistrationSession() + .map(s -> buildResponse(s, verificationSession)) + .map(verificationSessionResponse -> { + final Response response = registrationServiceException instanceof TransportNotAllowedException + ? Response.status(418).entity(verificationSessionResponse).build() + : Response.status(Response.Status.CONFLICT).entity(verificationSessionResponse).build(); + + return new ClientErrorException(response); + }) + .orElseGet(NotFoundException::new); + + } else if (unwrappedException instanceof RegistrationServiceSenderException) { + + throw unwrappedException; + + } else { + logger.error("Registration service failure", unwrappedException); + throw new ServerErrorException(Response.Status.INTERNAL_SERVER_ERROR); + } + } + + Metrics.counter(CODE_REQUESTED_COUNTER_NAME, Tags.of( + UserAgentTagUtil.getPlatformTag(userAgent), + Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(registrationServiceSession.number())), + Tag.of(REGION_CODE_TAG_NAME, Util.getRegion(registrationServiceSession.number())), + Tag.of(VERIFICATION_TRANSPORT_TAG_NAME, verificationCodeRequest.transport().toString()))) + .increment(); + + return buildResponse(resultSession, verificationSession); + } + + @PUT + @Path("/session/{sessionId}/code") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public VerificationSessionResponse verifyCode(@PathParam("sessionId") final String encodedSessionId, + @HeaderParam(HttpHeaders.USER_AGENT) String userAgent, + @NotNull @Valid final SubmitVerificationCodeRequest submitVerificationCodeRequest) + throws RateLimitExceededException { + + final RegistrationServiceSession registrationServiceSession = retrieveRegistrationServiceSession(encodedSessionId); + final VerificationSession verificationSession = retrieveVerificationSession(registrationServiceSession); + + if (registrationServiceSession.verified()) { + final VerificationSessionResponse verificationSessionResponse = buildResponse(registrationServiceSession, + verificationSession); + + throw new ClientErrorException( + Response.status(Response.Status.CONFLICT).entity(verificationSessionResponse).build()); + } + + final RegistrationServiceSession resultSession; + try { + resultSession = registrationServiceClient.checkVerificationCode(registrationServiceSession.id(), + submitVerificationCodeRequest.code(), + REGISTRATION_RPC_TIMEOUT) + .join(); + } catch (final CancellationException e) { + logger.warn("Unexpected cancellation from registration service", e); + throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE); + } catch (final CompletionException e) { + final Throwable unwrappedException = ExceptionUtils.unwrap(e); + if (unwrappedException instanceof RateLimitExceededException rateLimitExceededException) { + + if (rateLimitExceededException instanceof VerificationSessionRateLimitExceededException ve) { + final Response response = buildResponseForRateLimitExceeded(verificationSession, ve.getRegistrationSession(), + ve.getRetryDuration()); + throw new ClientErrorException(response); + } + + throw new RateLimitExceededException(rateLimitExceededException.getRetryDuration().orElse(null), false); + + } else if (unwrappedException instanceof RegistrationServiceException registrationServiceException) { + + throw registrationServiceException.getRegistrationSession() + .map(s -> buildResponse(s, verificationSession)) + .map(verificationSessionResponse -> new ClientErrorException( + Response.status(Response.Status.CONFLICT).entity(verificationSessionResponse).build())) + .orElseGet(NotFoundException::new); + + } else { + logger.error("Registration service failure", unwrappedException); + throw new ServerErrorException(Response.Status.INTERNAL_SERVER_ERROR); + } + } + + if (resultSession.verified()) { + registrationRecoveryPasswordsManager.removeForNumber(registrationServiceSession.number()); + } + + Metrics.counter(VERIFIED_COUNTER_NAME, Tags.of( + UserAgentTagUtil.getPlatformTag(userAgent), + Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(registrationServiceSession.number())), + Tag.of(REGION_CODE_TAG_NAME, Util.getRegion(registrationServiceSession.number())), + Tag.of(SUCCESS_TAG_NAME, Boolean.toString(resultSession.verified())))) + .increment(); + + return buildResponse(resultSession, verificationSession); + } + + private Response buildResponseForRateLimitExceeded(final VerificationSession verificationSession, + final RegistrationServiceSession registrationServiceSession, + final Optional retryDuration) { + + final Response.ResponseBuilder responseBuilder = Response.status(Response.Status.TOO_MANY_REQUESTS) + .entity(buildResponse(registrationServiceSession, verificationSession)); + + retryDuration + .filter(d -> !d.isNegative()) + .ifPresent(d -> responseBuilder.header(HttpHeaders.RETRY_AFTER, d.toSeconds())); + + return responseBuilder.build(); + } + + /** + * @throws ClientErrorException with {@code 422} status if the ID cannot be decoded + * @throws javax.ws.rs.NotFoundException if the ID cannot be found + */ + private RegistrationServiceSession retrieveRegistrationServiceSession(final String encodedSessionId) { + final byte[] sessionId; + + try { + sessionId = decodeSessionId(encodedSessionId); + } catch (final IllegalArgumentException e) { + throw new ClientErrorException("Malformed session ID", HttpStatus.SC_UNPROCESSABLE_ENTITY); + } + + try { + final RegistrationServiceSession registrationServiceSession = registrationServiceClient.getSession(sessionId, + REGISTRATION_RPC_TIMEOUT).join() + .orElseThrow(NotFoundException::new); + + if (registrationServiceSession.verified()) { + registrationRecoveryPasswordsManager.removeForNumber(registrationServiceSession.number()); + } + + return registrationServiceSession; + + } catch (final CompletionException | CancellationException e) { + final Throwable unwrapped = ExceptionUtils.unwrap(e); + + if (unwrapped instanceof StatusRuntimeException grpcRuntimeException) { + if (grpcRuntimeException.getStatus().getCode() == Status.Code.INVALID_ARGUMENT) { + throw new BadRequestException(); + } + } + logger.error("Registration service failure", e); + throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE, e); + } + } + + /** + * @throws NotFoundException if the session is has no record + */ + private VerificationSession retrieveVerificationSession(final RegistrationServiceSession registrationServiceSession) { + + return verificationSessionManager.findForId(registrationServiceSession.encodedSessionId()) + .orTimeout(5, TimeUnit.SECONDS) + .join().orElseThrow(NotFoundException::new); + } + + /** + * @throws ClientErrorException with {@code 422} status if the only one of token and type are present + */ + private Pair validateAndExtractPushToken( + final UpdateVerificationSessionRequest request) { + + final String pushToken; + final PushNotification.TokenType pushTokenType; + if (Objects.isNull(request.pushToken()) + != Objects.isNull(request.pushTokenType())) { + throw new WebApplicationException("must specify both pushToken and pushTokenType or neither", + HttpStatus.SC_UNPROCESSABLE_ENTITY); + } else { + pushToken = request.pushToken(); + pushTokenType = pushToken == null + ? null + : request.pushTokenType().toTokenType(); + } + + return new Pair<>(pushToken, pushTokenType); + } + + private VerificationSessionResponse buildResponse(final RegistrationServiceSession registrationServiceSession, + final VerificationSession verificationSession) { + return new VerificationSessionResponse(registrationServiceSession.encodedSessionId(), + registrationServiceSession.nextSms(), + registrationServiceSession.nextVoiceCall(), registrationServiceSession.nextVerificationAttempt(), + verificationSession.allowedToRequestCode(), verificationSession.requestedInformation(), + registrationServiceSession.verified()); + } + + public static byte[] decodeSessionId(final String sessionId) { + return Base64.getUrlDecoder().decode(sessionId); + } + + private static String generatePushChallenge() { + final byte[] challenge = new byte[16]; + RANDOM.nextBytes(challenge); + + return HexFormat.of().formatHex(challenge); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationSessionRateLimitExceededException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationSessionRateLimitExceededException.java new file mode 100644 index 000000000..8841a44b1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationSessionRateLimitExceededException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import org.jetbrains.annotations.Nullable; +import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; +import java.time.Duration; + +public class VerificationSessionRateLimitExceededException extends RateLimitExceededException { + + private final RegistrationServiceSession registrationServiceSession; + + /** + * Constructs a new exception indicating when it may become safe to retry + * + * @param registrationServiceSession the associated registration session + * @param retryDuration A duration to wait before retrying, null if no duration can be indicated + * @param legacy whether to use a legacy status code when mapping the exception to an HTTP + * response + */ + public VerificationSessionRateLimitExceededException( + final RegistrationServiceSession registrationServiceSession, @Nullable final Duration retryDuration, + final boolean legacy) { + super(retryDuration, legacy); + this.registrationServiceSession = registrationServiceSession; + } + + public RegistrationServiceSession getRegistrationSession() { + return registrationServiceSession; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/currency/CoinMarketCapClient.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/currency/CoinMarketCapClient.java new file mode 100644 index 000000000..9606e4d6a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/currency/CoinMarketCapClient.java @@ -0,0 +1,84 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.currency; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; +import java.math.BigDecimal; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +public class CoinMarketCapClient { + + private final HttpClient httpClient; + private final String apiKey; + private final Map currencyIdsBySymbol; + + private static final Logger logger = LoggerFactory.getLogger(CoinMarketCapClient.class); + + record CoinMarketCapResponse(@JsonProperty("data") PriceConversionResponse priceConversionResponse) {}; + + record PriceConversionResponse(int id, String symbol, Map quote) {}; + + record PriceConversionQuote(BigDecimal price) {}; + + public CoinMarketCapClient(final HttpClient httpClient, final String apiKey, final Map currencyIdsBySymbol) { + this.httpClient = httpClient; + this.apiKey = apiKey; + this.currencyIdsBySymbol = currencyIdsBySymbol; + } + + public BigDecimal getSpotPrice(final String currency, final String base) throws IOException { + if (!currencyIdsBySymbol.containsKey(currency)) { + throw new IllegalArgumentException("No currency ID found for " + currency); + } + + final URI quoteUri = URI.create( + String.format("https://pro-api.coinmarketcap.com/v2/tools/price-conversion?amount=1&id=%d&convert=%s", + currencyIdsBySymbol.get(currency), base)); + + try { + final HttpResponse response = httpClient.send(HttpRequest.newBuilder() + .GET() + .uri(quoteUri) + .header("X-CMC_PRO_API_KEY", apiKey) + .build(), + HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() < 200 || response.statusCode() >= 300) { + logger.warn("CoinMarketCapRequest failed with response: {}", response); + throw new IOException("CoinMarketCap request failed with status code " + response.statusCode()); + } + + return extractConversionRate(parseResponse(response.body()), base); + } catch (final InterruptedException e) { + throw new IOException("Interrupted while waiting for a response", e); + } + } + + @VisibleForTesting + static CoinMarketCapResponse parseResponse(final String responseJson) throws JsonProcessingException { + return SystemMapper.jsonMapper().readValue(responseJson, CoinMarketCapResponse.class); + } + + @VisibleForTesting + static BigDecimal extractConversionRate(final CoinMarketCapResponse response, final String destinationCurrency) + throws IOException { + if (!response.priceConversionResponse().quote.containsKey(destinationCurrency)) { + throw new IOException("Response does not contain conversion rate for " + destinationCurrency); + } + + return response.priceConversionResponse().quote.get(destinationCurrency).price(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManager.java new file mode 100644 index 000000000..f33170a3d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManager.java @@ -0,0 +1,169 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.currency; + +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.lifecycle.Managed; +import io.lettuce.core.SetArgs; +import java.io.IOException; +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntity; +import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntityList; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; + +public class CurrencyConversionManager implements Managed { + + private static final Logger logger = LoggerFactory.getLogger(CurrencyConversionManager.class); + + @VisibleForTesting + static final Duration FIXER_REFRESH_INTERVAL = Duration.ofHours(2); + + private static final Duration COIN_MARKET_CAP_REFRESH_INTERVAL = Duration.ofMinutes(5); + + @VisibleForTesting + static final String COIN_MARKET_CAP_SHARED_CACHE_CURRENT_KEY = "CurrencyConversionManager::CoinMarketCapCacheCurrent"; + + private static final String COIN_MARKET_CAP_SHARED_CACHE_DATA_KEY = "CurrencyConversionManager::CoinMarketCapCacheData"; + + private final FixerClient fixerClient; + + private final CoinMarketCapClient coinMarketCapClient; + + private final FaultTolerantRedisCluster cacheCluster; + + private final Clock clock; + + private final List currencies; + + private final ScheduledExecutorService executor; + + private final AtomicReference cached = new AtomicReference<>(null); + + private Instant fixerUpdatedTimestamp = Instant.MIN; + + private Map cachedFixerValues; + + private Map cachedCoinMarketCapValues; + + + public CurrencyConversionManager( + final FixerClient fixerClient, + final CoinMarketCapClient coinMarketCapClient, + final FaultTolerantRedisCluster cacheCluster, + final List currencies, + final ScheduledExecutorService executor, + final Clock clock) { + this.fixerClient = fixerClient; + this.coinMarketCapClient = coinMarketCapClient; + this.cacheCluster = cacheCluster; + this.currencies = currencies; + this.executor = executor; + this.clock = clock; + } + + public Optional getCurrencyConversions() { + return Optional.ofNullable(cached.get()); + } + + @Override + public void start() throws Exception { + executor.scheduleAtFixedRate(() -> { + try { + updateCacheIfNecessary(); + } catch (Throwable t) { + logger.warn("Error updating currency conversions", t); + } + }, 0, 15, TimeUnit.SECONDS); + } + + @VisibleForTesting + void updateCacheIfNecessary() throws IOException { + if (Duration.between(fixerUpdatedTimestamp, clock.instant()).abs().compareTo(FIXER_REFRESH_INTERVAL) >= 0 || cachedFixerValues == null) { + this.cachedFixerValues = new HashMap<>(fixerClient.getConversionsForBase("USD")); + this.fixerUpdatedTimestamp = clock.instant(); + } + + { + final Map coinMarketCapValuesFromSharedCache = cacheCluster.withCluster(connection -> { + final Map parsedSharedCacheData = new HashMap<>(); + + connection.sync().hgetall(COIN_MARKET_CAP_SHARED_CACHE_DATA_KEY).forEach((currency, conversionRate) -> + parsedSharedCacheData.put(currency, new BigDecimal(conversionRate))); + + return parsedSharedCacheData; + }); + + if (coinMarketCapValuesFromSharedCache != null && !coinMarketCapValuesFromSharedCache.isEmpty()) { + cachedCoinMarketCapValues = coinMarketCapValuesFromSharedCache; + } + } + + final boolean shouldUpdateSharedCache = cacheCluster.withCluster(connection -> + "OK".equals(connection.sync().set(COIN_MARKET_CAP_SHARED_CACHE_CURRENT_KEY, + "true", + SetArgs.Builder.nx().ex(COIN_MARKET_CAP_REFRESH_INTERVAL)))); + + if (shouldUpdateSharedCache || cachedCoinMarketCapValues == null) { + final Map conversionRatesFromCoinMarketCap = new HashMap<>(currencies.size()); + + for (final String currency : currencies) { + conversionRatesFromCoinMarketCap.put(currency, coinMarketCapClient.getSpotPrice(currency, "USD")); + } + + cachedCoinMarketCapValues = conversionRatesFromCoinMarketCap; + + if (shouldUpdateSharedCache) { + cacheCluster.useCluster(connection -> { + final Map sharedCoinMarketCapValues = new HashMap<>(); + + cachedCoinMarketCapValues.forEach((currency, conversionRate) -> + sharedCoinMarketCapValues.put(currency, conversionRate.toString())); + + connection.sync().hset(COIN_MARKET_CAP_SHARED_CACHE_DATA_KEY, sharedCoinMarketCapValues); + }); + } + } + + List entities = new LinkedList<>(); + + for (Map.Entry currency : cachedCoinMarketCapValues.entrySet()) { + BigDecimal usdValue = stripTrailingZerosAfterDecimal(currency.getValue()); + + Map values = new HashMap<>(); + values.put("USD", usdValue); + + for (Map.Entry conversion : cachedFixerValues.entrySet()) { + values.put(conversion.getKey(), stripTrailingZerosAfterDecimal(conversion.getValue().multiply(usdValue))); + } + + entities.add(new CurrencyConversionEntity(currency.getKey(), values)); + } + + this.cached.set(new CurrencyConversionEntityList(entities, clock.millis())); + } + + private BigDecimal stripTrailingZerosAfterDecimal(BigDecimal bigDecimal) { + BigDecimal n = bigDecimal.stripTrailingZeros(); + if (n.scale() < 0) { + return n.setScale(0); + } else { + return n; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/currency/FixerClient.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/currency/FixerClient.java new file mode 100644 index 000000000..ffc4fd318 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/currency/FixerClient.java @@ -0,0 +1,80 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.currency; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.IOException; +import java.math.BigDecimal; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Map; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +public class FixerClient { + + private final String apiKey; + private final HttpClient client; + + public FixerClient(HttpClient client, String apiKey) { + this.apiKey = apiKey; + this.client = client; + } + + public Map getConversionsForBase(String base) throws FixerException { + try { + URI uri = URI.create("https://data.fixer.io/api/latest?access_key=" + apiKey + "&base=" + base); + + HttpResponse response = client.send(HttpRequest.newBuilder() + .GET() + .uri(uri) + .build(), + HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new FixerException("Bad response: " + response.statusCode() + " " + response.toString()); + } + + FixerResponse parsedResponse = SystemMapper.jsonMapper().readValue(response.body(), FixerResponse.class); + + if (parsedResponse.success) return parsedResponse.rates; + else throw new FixerException("Got failed response!"); + } catch (IOException | InterruptedException e) { + throw new FixerException(e); + } + } + + private static class FixerResponse { + + @JsonProperty + private boolean success; + + @JsonProperty + private long timestamp; + + @JsonProperty + private String base; + + @JsonProperty + private String date; + + @JsonProperty + private Map rates; + + } + + public static class FixerException extends IOException { + public FixerException(String message) { + super(message); + } + + public FixerException(Exception exception) { + super(exception); + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java new file mode 100644 index 000000000..6f256c1a9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java @@ -0,0 +1,124 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.common.annotations.VisibleForTesting; +import java.util.Optional; +import java.util.OptionalInt; +import javax.annotation.Nullable; +import javax.validation.constraints.Size; +import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; +import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; +import org.whispersystems.textsecuregcm.util.ExactlySize; + +public class AccountAttributes { + + @JsonProperty + private boolean fetchesMessages; + + @JsonProperty + private int registrationId; + + @JsonProperty("pniRegistrationId") + private Integer phoneNumberIdentityRegistrationId; + + @JsonProperty + @Size(max = 204, message = "This field must be less than 50 characters") + private String name; + + @JsonProperty + private String registrationLock; + + @JsonProperty + @ExactlySize({0, 16}) + private byte[] unidentifiedAccessKey; + + @JsonProperty + private boolean unrestrictedUnidentifiedAccess; + + @JsonProperty + private DeviceCapabilities capabilities; + + @JsonProperty + private boolean discoverableByPhoneNumber = true; + + @JsonProperty + @Nullable + @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) + private byte[] recoveryPassword = null; + + public AccountAttributes() { + } + + @VisibleForTesting + public AccountAttributes( + final boolean fetchesMessages, + final int registrationId, + final String name, + final String registrationLock, + final boolean discoverableByPhoneNumber, + final DeviceCapabilities capabilities) { + this.fetchesMessages = fetchesMessages; + this.registrationId = registrationId; + this.name = name; + this.registrationLock = registrationLock; + this.discoverableByPhoneNumber = discoverableByPhoneNumber; + this.capabilities = capabilities; + } + + public boolean getFetchesMessages() { + return fetchesMessages; + } + + public int getRegistrationId() { + return registrationId; + } + + public OptionalInt getPhoneNumberIdentityRegistrationId() { + return phoneNumberIdentityRegistrationId != null ? OptionalInt.of(phoneNumberIdentityRegistrationId) : OptionalInt.empty(); + } + + public String getName() { + return name; + } + + public String getRegistrationLock() { + return registrationLock; + } + + public byte[] getUnidentifiedAccessKey() { + return unidentifiedAccessKey; + } + + public boolean isUnrestrictedUnidentifiedAccess() { + return unrestrictedUnidentifiedAccess; + } + + public DeviceCapabilities getCapabilities() { + return capabilities; + } + + public boolean isDiscoverableByPhoneNumber() { + return discoverableByPhoneNumber; + } + + public Optional recoveryPassword() { + return Optional.ofNullable(recoveryPassword); + } + + @VisibleForTesting + public AccountAttributes withUnidentifiedAccessKey(final byte[] unidentifiedAccessKey) { + this.unidentifiedAccessKey = unidentifiedAccessKey; + return this; + } + + @VisibleForTesting + public AccountAttributes withRecoveryPassword(final byte[] recoveryPassword) { + this.recoveryPassword = recoveryPassword; + return this; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountCount.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountCount.java new file mode 100644 index 000000000..c1caf6fb5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountCount.java @@ -0,0 +1,23 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class AccountCount { + + @JsonProperty + private int count; + + public AccountCount(int count) { + this.count = count; + } + + public AccountCount() {} + + public int getCount() { + return count; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountDataReportResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountDataReportResponse.java new file mode 100644 index 000000000..e6276a6e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountDataReportResponse.java @@ -0,0 +1,122 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nullable; +import org.whispersystems.textsecuregcm.storage.AccountBadge; + +public record AccountDataReportResponse(UUID reportId, + @JsonSerialize(using = InstantSerializer.class) + @JsonFormat(pattern = DATE_FORMAT, timezone = UTC) + Instant reportTimestamp, + AccountAndDevicesDataReport data) { + + private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + private static final String UTC = "Etc/UTC"; + + @JsonProperty + @Schema(description = "A plaintext representation of the data report") + String text() { + + final StringBuilder builder = new StringBuilder(); + + // header + builder.append(String.format(""" + Report ID: %s + Report timestamp: %s + + """, + reportId, + reportTimestamp.truncatedTo(ChronoUnit.SECONDS))); + + // account + builder.append(String.format(""" + # Account + Phone number: %s + Allow sealed sender from anyone: %s + Find account by phone number: %s + """, + data.account.phoneNumber(), + data.account.allowSealedSenderFromAnyone(), + data.account.findAccountByPhoneNumber())); + + // badges + builder.append("Badges:"); + + if (data.account.badges().isEmpty()) { + builder.append(" None\n"); + } else { + builder.append("\n"); + data.account.badges().forEach(badgeDataReport -> builder.append(String.format(""" + - ID: %s + Expiration: %s + Visible: %s + """, + badgeDataReport.id(), + badgeDataReport.expiration().truncatedTo(ChronoUnit.SECONDS), + badgeDataReport.visible()))); + } + + // devices + builder.append("\n# Devices\n"); + + data.devices().forEach(deviceDataReport -> + builder.append(String.format(""" + - ID: %s + Created: %s + Last seen: %s + User-agent: %s + """, + deviceDataReport.id(), + deviceDataReport.created().truncatedTo(ChronoUnit.SECONDS), + deviceDataReport.lastSeen().truncatedTo(ChronoUnit.SECONDS), + deviceDataReport.userAgent()))); + + return builder.toString(); + } + + + public record AccountAndDevicesDataReport(AccountDataReport account, + List devices) { + + } + + public record AccountDataReport(String phoneNumber, List badges, boolean allowSealedSenderFromAnyone, + boolean findAccountByPhoneNumber) { + + } + + public record DeviceDataReport(long id, + @JsonFormat(pattern = DATE_FORMAT, timezone = UTC) + Instant lastSeen, + @JsonFormat(pattern = DATE_FORMAT, timezone = UTC) + Instant created, + @Nullable String userAgent) { + + + } + + public record BadgeDataReport(String id, + @JsonFormat(pattern = DATE_FORMAT, timezone = UTC) + Instant expiration, + boolean visible) { + + public BadgeDataReport(AccountBadge badge) { + this(badge.getId(), badge.getExpiration(), badge.isVisible()); + } + + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountIdentifierResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountIdentifierResponse.java new file mode 100644 index 000000000..e3067d628 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountIdentifierResponse.java @@ -0,0 +1,17 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter; + +public record AccountIdentifierResponse(@NotNull + @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.AciServiceIdentifierDeserializer.class) + AciServiceIdentifier uuid) {} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountIdentityResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountIdentityResponse.java new file mode 100644 index 000000000..ec9e186aa --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountIdentityResponse.java @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.UUID; +import javax.annotation.Nullable; + +public record AccountIdentityResponse( + @Schema(description="the account identifier for this account") + UUID uuid, + + @Schema(description="the phone number associated with this account") + String number, + + @Schema(description="the account identifier for this account's phone-number identity") + UUID pni, + + @Schema(description="a hash of this account's username, if set") + @JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class) + @Nullable byte[] usernameHash, + + @Schema(description="whether any of this account's devices support storage") + boolean storageCapable) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountMismatchedDevices.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountMismatchedDevices.java new file mode 100644 index 000000000..55e939ebe --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountMismatchedDevices.java @@ -0,0 +1,18 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter; + +public record AccountMismatchedDevices(@JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) + ServiceIdentifier uuid, + + MismatchedDevices devices) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountStaleDevices.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountStaleDevices.java new file mode 100644 index 000000000..031046d8c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountStaleDevices.java @@ -0,0 +1,18 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter; + +public record AccountStaleDevices(@JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) + ServiceIdentifier uuid, + + StaleDevices devices) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AcknowledgeWebsocketMessage.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AcknowledgeWebsocketMessage.java new file mode 100644 index 000000000..8393ba342 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AcknowledgeWebsocketMessage.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class AcknowledgeWebsocketMessage extends IncomingWebsocketMessage { + + @JsonProperty + private long id; + + public AcknowledgeWebsocketMessage() {} + + public AcknowledgeWebsocketMessage(long id) { + this.type = TYPE_ACKNOWLEDGE_MESSAGE; + this.id = id; + } + + public long getId() { + return id; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AnswerChallengeRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AnswerChallengeRequest.java new file mode 100644 index 000000000..c7ec4782e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AnswerChallengeRequest.java @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = AnswerPushChallengeRequest.class, name = "rateLimitPushChallenge"), + @JsonSubTypes.Type(value = AnswerRecaptchaChallengeRequest.class, name = "recaptcha") +}) +public abstract class AnswerChallengeRequest { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AnswerPushChallengeRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AnswerPushChallengeRequest.java new file mode 100644 index 000000000..97177f453 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AnswerPushChallengeRequest.java @@ -0,0 +1,20 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotBlank; + +public class AnswerPushChallengeRequest extends AnswerChallengeRequest { + + @Schema(description = "A token provided to the client via a push payload") + @NotBlank + private String challenge; + + public String getChallenge() { + return challenge; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AnswerRecaptchaChallengeRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AnswerRecaptchaChallengeRequest.java new file mode 100644 index 000000000..e65dbd43b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AnswerRecaptchaChallengeRequest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotBlank; + +public class AnswerRecaptchaChallengeRequest extends AnswerChallengeRequest { + + @Schema(description = "The value of the token field from the server's 428 response") + @NotBlank + private String token; + + @Schema( + description = "A string representing a solved captcha", + example = "signal-hcaptcha.30b01b46-d8c9-4c30-bbd7-9719acfe0c10.challenge.abcdefg1345") + @NotBlank + private String captcha; + + public String getToken() { + return token; + } + + public String getCaptcha() { + return captcha; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ApnRegistrationId.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ApnRegistrationId.java new file mode 100644 index 000000000..0741a9599 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ApnRegistrationId.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.entities; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotEmpty; + +public record ApnRegistrationId(@NotEmpty String apnRegistrationId, + @Nullable String voipRegistrationId) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptorV2.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptorV2.java new file mode 100644 index 000000000..67d1f5f4f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptorV2.java @@ -0,0 +1,23 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record AttachmentDescriptorV2(long attachmentId, + String key, + String credential, + String acl, + String algorithm, + String date, + String policy, + String signature) { + + @JsonProperty + public String attachmentIdString() { + return String.valueOf(attachmentId); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptorV3.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptorV3.java new file mode 100644 index 000000000..c0aec7388 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptorV3.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; + +public record AttachmentDescriptorV3( + @Schema(description = """ + Indicates the CDN type. 2 in the v3 API, 2 or 3 in the v4 API. + 2 indicates resumable uploads using GCS, + 3 indicates resumable uploads using TUS + """) + int cdn, + @Schema(description = "The location within the specified cdn where the finished upload can be found") + String key, + @Schema(description = "A map of headers to include with all upload requests. Potentially contains time-limited upload credentials") + Map headers, + + @Schema(description = "The URL to upload to with the appropriate protocol") + String signedUploadLocation) { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentUri.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentUri.java new file mode 100644 index 000000000..0531c76c5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentUri.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; + +public class AttachmentUri { + + @JsonProperty + private String location; + + public AttachmentUri(URL uri) { + this.location = uri.toString(); + } + + public AttachmentUri() {} + + public URL getLocation() throws MalformedURLException { + return URI.create(location).toURL(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckRequest.java new file mode 100644 index 000000000..1b493e681 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckRequest.java @@ -0,0 +1,19 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.whispersystems.textsecuregcm.util.E164; + +public record AuthCheckRequest(@Schema(description = "The e164-formatted phone number.") + @NotNull @E164 String number, + @Schema(description = "A list of SVR auth values, previously retrieved from `/v1/backup/auth`; may contain at most 10.") + @NotEmpty @Size(max = 10) List passwords) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckResponse.java new file mode 100644 index 000000000..e0f94a75a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AuthCheckResponse.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonValue; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Map; +import javax.validation.constraints.NotNull; + +public record AuthCheckResponse(@Schema(description = "A dictionary with the auth check results: `KBS Credentials -> 'match'/'no-match'/'invalid'`") + @NotNull Map matches) { + + public enum Result { + MATCH("match"), + NO_MATCH("no-match"), + INVALID("invalid"); + + private final String clientCode; + + Result(final String clientCode) { + this.clientCode = clientCode; + } + + @JsonValue + public String clientCode() { + return clientCode; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AvatarChange.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AvatarChange.java new file mode 100644 index 000000000..29c218ffb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/AvatarChange.java @@ -0,0 +1,7 @@ +package org.whispersystems.textsecuregcm.entities; + +public enum AvatarChange { + AVATAR_CHANGE_UNCHANGED, + AVATAR_CHANGE_CLEAR, + AVATAR_CHANGE_UPDATE +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/Badge.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/Badge.java new file mode 100644 index 000000000..ff4441fb8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/Badge.java @@ -0,0 +1,119 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Strings; +import java.util.List; +import java.util.Objects; + +public class Badge { + private final String id; + private final String category; + private final String name; + private final String description; + private final List sprites6; + private final String svg; + private final List svgs; + + @JsonCreator + public Badge( + @JsonProperty("id") final String id, + @JsonProperty("category") final String category, + @JsonProperty("name") final String name, + @JsonProperty("description") final String description, + @JsonProperty("sprites6") final List sprites6, + @JsonProperty("svg") final String svg, + @JsonProperty("svgs") final List svgs) { + this.id = id; + this.category = category; + this.name = name; + this.description = description; + this.sprites6 = Objects.requireNonNull(sprites6); + if (sprites6.size() != 6) { + throw new IllegalArgumentException("sprites must have size 6"); + } + if (Strings.isNullOrEmpty(svg)) { + throw new IllegalArgumentException("svg cannot be empty"); + } + this.svg = svg; + this.svgs = Objects.requireNonNull(svgs); + } + + public String getId() { + return id; + } + + public String getCategory() { + return category; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public List getSprites6() { + return sprites6; + } + + public String getSvg() { + return svg; + } + + public List getSvgs() { + return svgs; + } + + /** + * Workaround for old Android builds that expect this field to exist but don't care it's an empty string. + */ + @Deprecated + @JsonProperty + public String getImageUrl() { + return ""; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Badge badge = (Badge) o; + return Objects.equals(id, badge.id) + && Objects.equals(category, badge.category) + && Objects.equals(name, badge.name) + && Objects.equals(description, badge.description) + && Objects.equals(sprites6, badge.sprites6) + && Objects.equals(svg, badge.svg) + && Objects.equals(svgs, badge.svgs); + } + + @Override + public int hashCode() { + return Objects.hash(id, category, name, description, sprites6, svg, svgs); + } + + @Override + public String toString() { + return "Badge{" + + "id='" + id + '\'' + + ", category='" + category + '\'' + + ", name='" + name + '\'' + + ", description='" + description + '\'' + + ", sprites6=" + sprites6 + + ", svg='" + svg + '\'' + + ", svgs=" + svgs + + '}'; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/BadgeSvg.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/BadgeSvg.java new file mode 100644 index 000000000..fef8cda78 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/BadgeSvg.java @@ -0,0 +1,67 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Strings; +import java.util.Objects; +import javax.validation.constraints.NotEmpty; + +public class BadgeSvg { + private final String light; + private final String dark; + + @JsonCreator + public BadgeSvg( + @JsonProperty("light") final String light, + @JsonProperty("dark") final String dark) { + if (Strings.isNullOrEmpty(light)) { + throw new IllegalArgumentException("light cannot be empty"); + } + this.light = light; + if (Strings.isNullOrEmpty(dark)) { + throw new IllegalArgumentException("dark cannot be empty"); + } + this.dark = dark; + } + + @NotEmpty + public String getLight() { + return light; + } + + @NotEmpty + public String getDark() { + return dark; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BadgeSvg badgeSvg = (BadgeSvg) o; + return Objects.equals(light, badgeSvg.light) + && Objects.equals(dark, badgeSvg.dark); + } + + @Override + public int hashCode() { + return Objects.hash(light, dark); + } + + @Override + public String toString() { + return "BadgeSvg{" + + "light='" + light + '\'' + + ", dark='" + dark + '\'' + + '}'; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/BaseProfileResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/BaseProfileResponse.java new file mode 100644 index 000000000..c3414f628 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/BaseProfileResponse.java @@ -0,0 +1,86 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.signal.libsignal.protocol.IdentityKey; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter; +import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter; +import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter; + +import java.util.List; + +public class BaseProfileResponse { + + @JsonProperty + @JsonSerialize(using = IdentityKeyAdapter.Serializer.class) + @JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class) + private IdentityKey identityKey; + + @JsonProperty + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + private byte[] unidentifiedAccess; + + @JsonProperty + private boolean unrestrictedUnidentifiedAccess; + + @JsonProperty + private UserCapabilities capabilities; + + @JsonProperty + private List badges; + + @JsonProperty + @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) + private ServiceIdentifier uuid; + + public BaseProfileResponse() { + } + + public BaseProfileResponse(final IdentityKey identityKey, + final byte[] unidentifiedAccess, + final boolean unrestrictedUnidentifiedAccess, + final UserCapabilities capabilities, + final List badges, + final ServiceIdentifier uuid) { + + this.identityKey = identityKey; + this.unidentifiedAccess = unidentifiedAccess; + this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; + this.capabilities = capabilities; + this.badges = badges; + this.uuid = uuid; + } + + public IdentityKey getIdentityKey() { + return identityKey; + } + + public byte[] getUnidentifiedAccess() { + return unidentifiedAccess; + } + + public boolean isUnrestrictedUnidentifiedAccess() { + return unrestrictedUnidentifiedAccess; + } + + public UserCapabilities getCapabilities() { + return capabilities; + } + + public List getBadges() { + return badges; + } + + public ServiceIdentifier getUuid() { + return uuid; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckRequest.java new file mode 100644 index 000000000..45ab13963 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckRequest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.util.List; +import javax.annotation.Nullable; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.util.ExactlySize; +import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter; + +public record BatchIdentityCheckRequest(@Valid @NotNull @Size(max = 1000) List elements) { + + /** + * @param uuid account id or phone number id + * @param fingerprint most significant 4 bytes of SHA-256 of the 33-byte identity key field (32-byte curve25519 public + * key prefixed with 0x05) + */ + public record Element(@Nullable + @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) + ServiceIdentifier uuid, + + @Nullable + @Deprecated // remove after 2023-11-01 + @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.AciServiceIdentifierDeserializer.class) + AciServiceIdentifier aci, + + @NotNull + @ExactlySize(4) + byte[] fingerprint) { + + public Element { + if (aci == null && uuid == null) { + throw new IllegalArgumentException("aci and uuid cannot both be null"); + } + + if (aci != null && uuid != null) { + throw new IllegalArgumentException("aci and uuid cannot both be non-null"); + } + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckResponse.java new file mode 100644 index 000000000..38ba7585c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/BatchIdentityCheckResponse.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.util.List; +import javax.annotation.Nullable; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import org.signal.libsignal.protocol.IdentityKey; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter; +import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter; + +public record BatchIdentityCheckResponse(@Valid List elements) { + + public record Element(@JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) + @Nullable + ServiceIdentifier uuid, + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.AciServiceIdentifierDeserializer.class) + @Nullable + @Deprecated // remove after 2023-11-01 + ServiceIdentifier aci, + + @NotNull + @JsonSerialize(using = IdentityKeyAdapter.Serializer.class) + @JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class) + IdentityKey identityKey) { + + public Element { + if (aci == null && uuid == null) { + throw new IllegalArgumentException("aci and uuid cannot both be null"); + } + + if (aci != null && uuid != null) { + throw new IllegalArgumentException("aci and uuid cannot both be non-null"); + } + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangeNumberRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangeNumberRequest.java new file mode 100644 index 000000000..7f37aa404 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangeNumberRequest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import org.signal.libsignal.protocol.IdentityKey; +import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; +import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter; + +public record ChangeNumberRequest( + @Schema(description=""" + A session ID from registration service, if using session id to authenticate this request. + Must not be combined with `recoveryPassword`.""") + String sessionId, + + @Schema(type="string", description=""" + The base64-encoded recovery password for the new phone number, if using a recovery password to authenticate this request. + Must not be combined with `sessionId`.""") + @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) byte[] recoveryPassword, + + @Schema(description="the new phone number for this account") + @NotBlank String number, + + @Schema(description="the registration lock password for the new phone number, if necessary") + @JsonProperty("reglock") @Nullable String registrationLock, + + @Schema(description="the new public identity key to use for the phone-number identity associated with the new phone number") + @JsonSerialize(using = IdentityKeyAdapter.Serializer.class) + @JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class) + @NotNull IdentityKey pniIdentityKey, + + @ArraySchema( + arraySchema=@Schema(description=""" + A list of synchronization messages to send to companion devices to supply the private keysManager + associated with the new identity key and their new prekeys. + Exactly one message must be supplied for each enabled device other than the sending (primary) device.""")) + @NotNull @Valid List<@NotNull @Valid IncomingMessage> deviceMessages, + + @Schema(description=""" + A new signed elliptic-curve prekey for each enabled device on the account, including this one. + Each must be accompanied by a valid signature from the new identity key in this request.""") + @NotNull @Valid Map devicePniSignedPrekeys, + + @Schema(description=""" + A new signed post-quantum last-resort prekey for each enabled device on the account, including this one. + May be absent, in which case the last resort PQ prekeys for each device will be deleted if any had been stored. + If present, must contain one prekey per enabled device including this one. + Prekeys for devices that did not previously have any post-quantum prekeys stored will be silently dropped. + Each must be accompanied by a valid signature from the new identity key in this request.""") + @Valid Map devicePniPqLastResortPrekeys, + + @Schema(description="the new phone-number-identity registration ID for each enabled device on the account, including this one") + @NotNull Map pniRegistrationIds) implements PhoneVerificationRequest { + + @AssertTrue + public boolean isSignatureValidOnEachSignedPreKey() { + List> spks = new ArrayList<>(); + if (devicePniSignedPrekeys != null) { + spks.addAll(devicePniSignedPrekeys.values()); + } + if (devicePniPqLastResortPrekeys != null) { + spks.addAll(devicePniPqLastResortPrekeys.values()); + } + return spks.isEmpty() || PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey, spks); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangePhoneNumberRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangePhoneNumberRequest.java new file mode 100644 index 000000000..88df907ee --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ChangePhoneNumberRequest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import org.signal.libsignal.protocol.IdentityKey; +import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter; + +public record ChangePhoneNumberRequest( + @Schema(description="the new phone number for this account") + @NotBlank String number, + + @Schema(description="the registration verification code to authenticate this request") + @NotBlank String code, + + @Schema(description="the registration lock password for the new phone number, if necessary") + @JsonProperty("reglock") @Nullable String registrationLock, + + @Schema(description="the new public identity key to use for the phone-number identity associated with the new phone number") + @JsonSerialize(using = IdentityKeyAdapter.Serializer.class) + @JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class) + @Nullable IdentityKey pniIdentityKey, + + @Schema(description=""" + A list of synchronization messages to send to companion devices to supply the private keysManager + associated with the new identity key and their new prekeys. + Exactly one message must be supplied for each enabled device other than the sending (primary) device.""") + @Nullable List deviceMessages, + + @Schema(description=""" + A new signed elliptic-curve prekey for each enabled device on the account, including this one. + Each must be accompanied by a valid signature from the new identity key in this request.""") + @Nullable Map devicePniSignedPrekeys, + + @Schema(description=""" + A new signed post-quantum last-resort prekey for each enabled device on the account, including this one. + May be absent, in which case the last resort PQ prekeys for each device will be deleted if any had been stored. + If present, must contain one prekey per enabled device including this one. + Prekeys for devices that did not previously have any post-quantum prekeys stored will be silently dropped. + Each must be accompanied by a valid signature from the new identity key in this request.""") + @Nullable @Valid Map devicePniPqLastResortPrekeys, + + @Schema(description="the new phone-number-identity registration ID for each enabled device on the account, including this one") + @Nullable Map pniRegistrationIds) { + + @AssertTrue + public boolean isSignatureValidOnEachSignedPreKey() { + List> spks = new ArrayList<>(); + if (devicePniSignedPrekeys != null) { + spks.addAll(devicePniSignedPrekeys.values()); + } + if (devicePniPqLastResortPrekeys != null) { + spks.addAll(devicePniPqLastResortPrekeys.values()); + } + return spks.isEmpty() || PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey, spks); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ConfirmUsernameHashRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ConfirmUsernameHashRequest.java new file mode 100644 index 000000000..a38a6efbc --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ConfirmUsernameHashRequest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import javax.annotation.Nullable; +import javax.validation.Valid; +import javax.validation.constraints.Size; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.whispersystems.textsecuregcm.controllers.AccountController; +import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter; +import org.whispersystems.textsecuregcm.util.ExactlySize; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record ConfirmUsernameHashRequest( + @JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class) + @ExactlySize(AccountController.USERNAME_HASH_LENGTH) + byte[] usernameHash, + + @JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class) + byte[] zkProof, + + @Schema(type = "string", description = "The url-safe base64-encoded encrypted username to be stored for username links") + @Nullable + @JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class) + @Size(min = 1, max = 128) + byte[] encryptedUsername +) {} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateCallLinkCredential.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateCallLinkCredential.java new file mode 100644 index 000000000..4532e7387 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateCallLinkCredential.java @@ -0,0 +1,3 @@ +package org.whispersystems.textsecuregcm.entities; + +public record CreateCallLinkCredential(byte[] credential, long redemptionTime){} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java new file mode 100644 index 000000000..068a6a5a2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java @@ -0,0 +1,130 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; +import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter; +import org.whispersystems.textsecuregcm.util.ExactlySize; + +public class CreateProfileRequest { + + @JsonProperty + @NotEmpty + private String version; + + @JsonProperty + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + @ExactlySize({81, 285}) + private byte[] name; + + @JsonProperty + private boolean avatar; + + @JsonProperty + private boolean sameAvatar; + + @JsonProperty + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + @ExactlySize({0, 60}) + private byte[] aboutEmoji; + + @JsonProperty + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + @ExactlySize({0, 156, 282, 540}) + private byte[] about; + + @JsonProperty + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + @ExactlySize({0, 582}) + private byte[] paymentAddress; + + @JsonProperty + @Nullable + private List badgeIds; + + @JsonProperty + @NotNull + @JsonDeserialize(using = ProfileKeyCommitmentAdapter.Deserializing.class) + @JsonSerialize(using = ProfileKeyCommitmentAdapter.Serializing.class) + private ProfileKeyCommitment commitment; + + public CreateProfileRequest() { + } + + public CreateProfileRequest( + final ProfileKeyCommitment commitment, final String version, final byte[] name, final byte[] aboutEmoji, final byte[] about, + final byte[] paymentAddress, final boolean wantsAvatar, final boolean sameAvatar, final List badgeIds) { + this.commitment = commitment; + this.version = version; + this.name = name; + this.aboutEmoji = aboutEmoji; + this.about = about; + this.paymentAddress = paymentAddress; + this.avatar = wantsAvatar; + this.sameAvatar = sameAvatar; + this.badgeIds = badgeIds; + } + + public ProfileKeyCommitment getCommitment() { + return commitment; + } + + public String getVersion() { + return version; + } + + public byte[] getName() { + return name; + } + + public boolean hasAvatar() { + return avatar; + } + + public enum AvatarChange { + UNCHANGED, + CLEAR, + UPDATE; + } + + public AvatarChange getAvatarChange() { + if (!hasAvatar()) { + return AvatarChange.CLEAR; + } + if (!sameAvatar) { + return AvatarChange.UPDATE; + } + return AvatarChange.UNCHANGED; + } + + public byte[] getAboutEmoji() { + return aboutEmoji; + } + + public byte[] getAbout() { + return about; + } + + public byte[] getPaymentAddress() { + return paymentAddress; + } + + public Optional> getBadges() { + return Optional.ofNullable(badgeIds); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateVerificationSessionRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateVerificationSessionRequest.java new file mode 100644 index 000000000..e3011c8af --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateVerificationSessionRequest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.google.common.annotations.VisibleForTesting; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import org.whispersystems.textsecuregcm.util.E164; + +// Not a record, because Jackson does not support @JsonUnwrapped with records +// https://github.com/FasterXML/jackson-databind/issues/1497 +public final class CreateVerificationSessionRequest { + + @E164 + @NotBlank + @JsonProperty + private String number; + + @Valid + @JsonUnwrapped + private UpdateVerificationSessionRequest updateVerificationSessionRequest; + + public CreateVerificationSessionRequest() { + } + + @VisibleForTesting + public CreateVerificationSessionRequest(final String number, final UpdateVerificationSessionRequest updateVerificationSessionRequest) { + this.number = number; + this.updateVerificationSessionRequest = updateVerificationSessionRequest; + } + + public String getNumber() { + return number; + } + + public UpdateVerificationSessionRequest getUpdateVerificationSessionRequest() { + return updateVerificationSessionRequest; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CredentialProfileResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CredentialProfileResponse.java new file mode 100644 index 000000000..01329d466 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CredentialProfileResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; + +public abstract class CredentialProfileResponse { + + @JsonUnwrapped + private VersionedProfileResponse versionedProfileResponse; + + protected CredentialProfileResponse() { + } + + protected CredentialProfileResponse(final VersionedProfileResponse versionedProfileResponse) { + this.versionedProfileResponse = versionedProfileResponse; + } + + public VersionedProfileResponse getVersionedProfileResponse() { + return versionedProfileResponse; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CurrencyConversionEntity.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CurrencyConversionEntity.java new file mode 100644 index 000000000..c937b16fc --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CurrencyConversionEntity.java @@ -0,0 +1,31 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.math.BigDecimal; +import java.util.Map; + +public class CurrencyConversionEntity { + + @JsonProperty + private String base; + + @JsonProperty + private Map conversions; + + public CurrencyConversionEntity(String base, Map conversions) { + this.base = base; + this.conversions = conversions; + } + + public CurrencyConversionEntity() {} + + public String getBase() { + return base; + } + + public Map getConversions() { + return conversions; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CurrencyConversionEntityList.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CurrencyConversionEntityList.java new file mode 100644 index 000000000..91c101615 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/CurrencyConversionEntityList.java @@ -0,0 +1,29 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class CurrencyConversionEntityList { + + @JsonProperty + private List currencies; + + @JsonProperty + private long timestamp; + + public CurrencyConversionEntityList(List currencies, long timestamp) { + this.currencies = currencies; + this.timestamp = timestamp; + } + + public CurrencyConversionEntityList() {} + + public List getCurrencies() { + return currencies; + } + + public long getTimestamp() { + return timestamp; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeliveryCertificate.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeliveryCertificate.java new file mode 100644 index 000000000..49dd46503 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeliveryCertificate.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class DeliveryCertificate { + + private final byte[] certificate; + + @JsonCreator + public DeliveryCertificate( + @JsonProperty("certificate") byte[] certificate) { + this.certificate = certificate; + } + + public byte[] getCertificate() { + return certificate; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java new file mode 100644 index 000000000..57be1f6b7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java @@ -0,0 +1,54 @@ +package org.whispersystems.textsecuregcm.entities; + +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.validation.Valid; + +import java.util.Optional; + +public record DeviceActivationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """ + A signed EC pre-key to be associated with this account's ACI. If provided, an account + will be created "atomically," and all other properties needed for atomic account + creation must also be present. + """) + Optional<@Valid ECSignedPreKey> aciSignedPreKey, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """ + A signed EC pre-key to be associated with this account's PNI. If provided, an account + will be created "atomically," and all other properties needed for atomic account + creation must also be present. + """) + Optional<@Valid ECSignedPreKey> pniSignedPreKey, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """ + A signed Kyber-1024 "last resort" pre-key to be associated with this account's ACI. If + provided, an account will be created "atomically," and all other properties needed for + atomic account creation must also be present. + """) + Optional<@Valid KEMSignedPreKey> aciPqLastResortPreKey, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """ + A signed Kyber-1024 "last resort" pre-key to be associated with this account's PNI. If + provided, an account will be created "atomically," and all other properties needed for + atomic account creation must also be present. + """) + Optional<@Valid KEMSignedPreKey> pniPqLastResortPreKey, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """ + An APNs token set for the account's primary device. If provided, the account's primary + device will be notified of new messages via push notifications to the given token. If + creating an account "atomically," callers must provide exactly one of an APNs token + set, an FCM token, or an `AccountAttributes` entity with `fetchesMessages` set to + `true`. + """) + Optional<@Valid ApnRegistrationId> apnToken, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """ + An FCM/GCM token for the account's primary device. If provided, the account's primary + device will be notified of new messages via push notifications to the given token. If + creating an account "atomically," callers must provide exactly one of an APNs token + set, an FCM token, or an `AccountAttributes` entity with `fetchesMessages` set to + `true`. + """) + Optional<@Valid GcmRegistrationId> gcmToken) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceInfo.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceInfo.java new file mode 100644 index 000000000..189a0b39b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceInfo.java @@ -0,0 +1,9 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +public record DeviceInfo(long id, String name, long lastSeen, long created) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceInfoList.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceInfoList.java new file mode 100644 index 000000000..a67e577f5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceInfoList.java @@ -0,0 +1,11 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import java.util.List; + +public record DeviceInfoList(List devices) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceName.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceName.java new file mode 100644 index 000000000..498c4032b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceName.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Size; + +public class DeviceName { + + @JsonProperty + @NotEmpty + @Size(max = 300, message = "This field must be less than 300 characters") + private String deviceName; + + public DeviceName() {} + + public String getDeviceName() { + return deviceName; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceResponse.java new file mode 100644 index 000000000..4539ae2a0 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceResponse.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; + +import java.util.UUID; + +public class DeviceResponse { + @JsonProperty + private UUID uuid; + + @JsonProperty + private UUID pni; + + @JsonProperty + private long deviceId; + + @VisibleForTesting + public DeviceResponse() {} + + public DeviceResponse(UUID uuid, UUID pni, long deviceId) { + this.uuid = uuid; + this.pni = pni; + this.deviceId = deviceId; + } + + public UUID getUuid() { + return uuid; + } + + public UUID getPni() { + return pni; + } + + public long getDeviceId() { + return deviceId; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ECPreKey.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ECPreKey.java new file mode 100644 index 000000000..e638183d6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ECPreKey.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.whispersystems.textsecuregcm.util.ECPublicKeyAdapter; + +public record ECPreKey( + @Schema(description=""" + An arbitrary ID for this key, which will be provided by peers using this key to encrypt messages so the private key can be looked up. + Should not be zero. Should be less than 2^24. + """) + long keyId, + + @JsonSerialize(using = ECPublicKeyAdapter.Serializer.class) + @JsonDeserialize(using = ECPublicKeyAdapter.Deserializer.class) + @Schema(type="string", description=""" + The public key, serialized in libsignal's elliptic-curve public key format and then base64-encoded. + """) + ECPublicKey publicKey) implements PreKey { + + @Override + public byte[] serializedPublicKey() { + return publicKey().serialize(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ECSignedPreKey.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ECSignedPreKey.java new file mode 100644 index 000000000..b585286bc --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ECSignedPreKey.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; +import org.whispersystems.textsecuregcm.util.ECPublicKeyAdapter; +import java.util.Arrays; +import java.util.Objects; + +public record ECSignedPreKey( + @Schema(description=""" + An arbitrary ID for this key, which will be provided by peers using this key to encrypt messages so the private key can be looked up. + Should not be zero. Should be less than 2^24. + """) + long keyId, + + @JsonSerialize(using = ECPublicKeyAdapter.Serializer.class) + @JsonDeserialize(using = ECPublicKeyAdapter.Deserializer.class) + @Schema(type="string", description=""" + The public key, serialized in libsignal's elliptic-curve public key format and then base64-encoded. + """) + ECPublicKey publicKey, + + @JsonSerialize(using = ByteArrayAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) + @Schema(type="string", description=""" + The signature of the serialized `publicKey` with the account (or phone-number identity)'s identity key, base64-encoded. + """) + byte[] signature) implements SignedPreKey { + + @Override + public byte[] serializedPublicKey() { + return publicKey().serialize(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ECSignedPreKey that = (ECSignedPreKey) o; + return keyId == that.keyId && publicKey.equals(that.publicKey) && Arrays.equals(signature, that.signature); + } + + @Override + public int hashCode() { + int result = Objects.hash(keyId, publicKey); + result = 31 * result + Arrays.hashCode(signature); + return result; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/EncryptedUsername.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/EncryptedUsername.java new file mode 100644 index 000000000..93f257964 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/EncryptedUsername.java @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter; + +public record EncryptedUsername( + @JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class) + @NotNull + @Size(min = 1, max = 128) + @Schema(type = "string", description = "the URL-safe base64 encoding of the encrypted username") + byte[] usernameLinkEncryptedValue) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialProfileResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialProfileResponse.java new file mode 100644 index 000000000..5ac0566a1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialProfileResponse.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import javax.annotation.Nullable; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; + +public class ExpiringProfileKeyCredentialProfileResponse extends CredentialProfileResponse { + + @JsonProperty + @JsonSerialize(using = ExpiringProfileKeyCredentialResponseAdapter.Serializing.class) + @JsonDeserialize(using = ExpiringProfileKeyCredentialResponseAdapter.Deserializing.class) + @Nullable + private ExpiringProfileKeyCredentialResponse credential; + + public ExpiringProfileKeyCredentialProfileResponse() { + } + + public ExpiringProfileKeyCredentialProfileResponse(final VersionedProfileResponse versionedProfileResponse, + @Nullable final ExpiringProfileKeyCredentialResponse credential) { + + super(versionedProfileResponse); + this.credential = credential; + } + + @Nullable + public ExpiringProfileKeyCredentialResponse getCredential() { + return credential; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialResponseAdapter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialResponseAdapter.java new file mode 100644 index 000000000..acca13d68 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialResponseAdapter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.util.Base64; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; + +public class ExpiringProfileKeyCredentialResponseAdapter { + + public static class Serializing extends JsonSerializer { + @Override + public void serialize(ExpiringProfileKeyCredentialResponse response, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + if (response == null) jsonGenerator.writeNull(); + else jsonGenerator.writeString(Base64.getEncoder().encodeToString(response.serialize())); + } + } + + public static class Deserializing extends JsonDeserializer { + @Override + public ExpiringProfileKeyCredentialResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + try { + return new ExpiringProfileKeyCredentialResponse(Base64.getDecoder().decode(jsonParser.getValueAsString())); + } catch (InvalidInputException e) { + throw new IOException(e); + } + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/GcmRegistrationId.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/GcmRegistrationId.java new file mode 100644 index 000000000..f715301a6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/GcmRegistrationId.java @@ -0,0 +1,10 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.entities; + +import javax.validation.constraints.NotEmpty; + +public record GcmRegistrationId(@NotEmpty String gcmRegistrationId) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/GetCreateCallLinkCredentialsRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/GetCreateCallLinkCredentialsRequest.java new file mode 100644 index 000000000..f3665f49a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/GetCreateCallLinkCredentialsRequest.java @@ -0,0 +1,6 @@ +package org.whispersystems.textsecuregcm.entities; + +import javax.validation.constraints.NotEmpty; + + +public record GetCreateCallLinkCredentialsRequest(@NotEmpty byte[] createCallLinkCredentialRequest) {} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/GroupCredentials.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/GroupCredentials.java new file mode 100644 index 000000000..e0e8c3ff4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/GroupCredentials.java @@ -0,0 +1,19 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import java.util.List; +import java.util.UUID; +import javax.annotation.Nullable; + +public record GroupCredentials(List credentials, List callLinkAuthCredentials, @Nullable UUID pni) { + + public record GroupCredential(byte[] credential, long redemptionTime) { + } + + public record CallLinkAuthCredential(byte[] credential, long redemptionTime) { + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java new file mode 100644 index 000000000..edcafba94 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.entities; + +import com.google.protobuf.ByteString; +import java.util.Base64; +import javax.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.storage.Account; + +public record IncomingMessage(int type, long destinationDeviceId, int destinationRegistrationId, String content) { + + public MessageProtos.Envelope toEnvelope(final ServiceIdentifier destinationIdentifier, + @Nullable Account sourceAccount, + @Nullable Long sourceDeviceId, + final long timestamp, + final boolean story, + final boolean urgent, + @Nullable byte[] reportSpamToken) { + + final MessageProtos.Envelope.Type envelopeType = MessageProtos.Envelope.Type.forNumber(type()); + + if (envelopeType == null) { + throw new IllegalArgumentException("Bad envelope type: " + type()); + } + + final MessageProtos.Envelope.Builder envelopeBuilder = MessageProtos.Envelope.newBuilder(); + + envelopeBuilder.setType(envelopeType) + .setTimestamp(timestamp) + .setServerTimestamp(System.currentTimeMillis()) + .setDestinationUuid(destinationIdentifier.toServiceIdentifierString()) + .setStory(story) + .setUrgent(urgent); + + if (sourceAccount != null && sourceDeviceId != null) { + envelopeBuilder + .setSourceUuid(new AciServiceIdentifier(sourceAccount.getUuid()).toServiceIdentifierString()) + .setSourceDevice(sourceDeviceId.intValue()); + } + + if (reportSpamToken != null) { + envelopeBuilder.setReportSpamToken(ByteString.copyFrom(reportSpamToken)); + } + + if (StringUtils.isNotEmpty(content())) { + envelopeBuilder.setContent(ByteString.copyFrom(Base64.getDecoder().decode(content()))); + } + + return envelopeBuilder.build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessageList.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessageList.java new file mode 100644 index 000000000..50113db8a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessageList.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.entities; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.whispersystems.textsecuregcm.controllers.MessageController; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; + +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; + +public record IncomingMessageList(@NotNull @Valid List<@NotNull IncomingMessage> messages, + boolean online, boolean urgent, long timestamp) { + + private static final Counter REJECT_DUPLICATE_RECIPIENT_COUNTER = + Metrics.counter( + name(MessageController.class, "rejectDuplicateRecipients"), + "multiRecipient", "false"); + + @JsonCreator + public IncomingMessageList(@JsonProperty("messages") @NotNull @Valid List<@NotNull IncomingMessage> messages, + @JsonProperty("online") boolean online, + @JsonProperty("urgent") Boolean urgent, + @JsonProperty("timestamp") long timestamp) { + + this(messages, online, urgent == null || urgent, timestamp); + } + + @AssertTrue + public boolean hasNoDuplicateRecipients() { + boolean valid = messages.stream().filter(m -> m != null).map(IncomingMessage::destinationDeviceId).distinct().count() == messages.size(); + if (!valid) { + REJECT_DUPLICATE_RECIPIENT_COUNTER.increment(); + } + return valid; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingWebsocketMessage.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingWebsocketMessage.java new file mode 100644 index 000000000..bc2ff8945 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingWebsocketMessage.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class IncomingWebsocketMessage { + + public static final int TYPE_ACKNOWLEDGE_MESSAGE = 1; + public static final int TYPE_PING_MESSAGE = 2; + public static final int TYPE_PONG_MESSAGE = 3; + + @JsonProperty + protected int type; + + public IncomingWebsocketMessage() {} + + public IncomingWebsocketMessage(int type) { + this.type = type; + } + + public int getType() { + return type; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/KEMSignedPreKey.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/KEMSignedPreKey.java new file mode 100644 index 000000000..5e2ff6eca --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/KEMSignedPreKey.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; +import org.signal.libsignal.protocol.kem.KEMPublicKey; +import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; +import org.whispersystems.textsecuregcm.util.KEMPublicKeyAdapter; +import java.util.Arrays; +import java.util.Objects; + +public record KEMSignedPreKey( + @Schema(description=""" + An arbitrary ID for this key, which will be provided by peers using this key to encrypt messages so the private key can be looked up. + Should not be zero. Should be less than 2^24. The owner of this key must be able to determine from the key ID whether this represents + a single-use or last-resort key, but another party should *not* be able to tell. + """) + long keyId, + + @JsonSerialize(using = KEMPublicKeyAdapter.Serializer.class) + @JsonDeserialize(using = KEMPublicKeyAdapter.Deserializer.class) + @Schema(type="string", description=""" + The public key, serialized in libsignal's Kyber1024 public key format and then base64-encoded. + """) + KEMPublicKey publicKey, + + @JsonSerialize(using = ByteArrayAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) + @Schema(type="string", description=""" + The signature of the serialized `publicKey` with the account (or phone-number identity)'s identity key, base64-encoded. + """) + byte[] signature) implements SignedPreKey { + + @Override + public byte[] serializedPublicKey() { + return publicKey().serialize(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + KEMSignedPreKey that = (KEMSignedPreKey) o; + return keyId == that.keyId && publicKey.equals(that.publicKey) && Arrays.equals(signature, that.signature); + } + + @Override + public int hashCode() { + int result = Objects.hash(keyId, publicKey); + result = 31 * result + Arrays.hashCode(signature); + return result; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/LinkDeviceRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/LinkDeviceRequest.java new file mode 100644 index 000000000..b12fb36a5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/LinkDeviceRequest.java @@ -0,0 +1,55 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import java.util.Optional; + +public record LinkDeviceRequest(@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """ + The verification code associated with this device. Must match the verification code + provided by the server when provisioning this device. + """) + String verificationCode, + + AccountAttributes accountAttributes, + + @JsonUnwrapped + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + DeviceActivationRequest deviceActivationRequest) { + + @JsonCreator + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public LinkDeviceRequest(@JsonProperty("verificationCode") String verificationCode, + @JsonProperty("accountAttributes") AccountAttributes accountAttributes, + @JsonProperty("aciSignedPreKey") Optional<@Valid ECSignedPreKey> aciSignedPreKey, + @JsonProperty("pniSignedPreKey") Optional<@Valid ECSignedPreKey> pniSignedPreKey, + @JsonProperty("aciPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> aciPqLastResortPreKey, + @JsonProperty("pniPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> pniPqLastResortPreKey, + @JsonProperty("apnToken") Optional<@Valid ApnRegistrationId> apnToken, + @JsonProperty("gcmToken") Optional<@Valid GcmRegistrationId> gcmToken) { + + this(verificationCode, accountAttributes, + new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnToken, gcmToken)); + } + + @AssertTrue + public boolean hasAllRequiredFields() { + return deviceActivationRequest().aciSignedPreKey().isPresent() + && deviceActivationRequest().pniSignedPreKey().isPresent() + && deviceActivationRequest().aciPqLastResortPreKey().isPresent() + && deviceActivationRequest().pniPqLastResortPreKey().isPresent(); + } + + @AssertTrue + public boolean hasExactlyOneMessageDeliveryChannel() { + if (accountAttributes.getFetchesMessages()) { + return deviceActivationRequest().apnToken().isEmpty() && deviceActivationRequest().gcmToken().isEmpty(); + } else { + return deviceActivationRequest().apnToken().isPresent() ^ deviceActivationRequest().gcmToken().isPresent(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/MessageResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/MessageResponse.java new file mode 100644 index 000000000..a5c04df3a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/MessageResponse.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.entities; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +public class MessageResponse { + private List success; + private List failure; + private Set missingDeviceIds; + + public MessageResponse(List success, List failure) { + this.success = success; + this.failure = failure; + this.missingDeviceIds = new HashSet<>(); + } + + public MessageResponse(Set missingDeviceIds) { + this.success = new LinkedList<>(); + this.failure = new LinkedList<>(missingDeviceIds); + this.missingDeviceIds = missingDeviceIds; + } + + public MessageResponse() {} + + public List getSuccess() { + return success; + } + + public void setSuccess(List success) { + this.success = success; + } + + public List getFailure() { + return failure; + } + + public void setFailure(List failure) { + this.failure = failure; + } + + public Set getNumbersMissingDevices() { + return missingDeviceIds; + } + + public void setNumbersMissingDevices(Set numbersMissingDevices) { + this.missingDeviceIds = numbersMissingDevices; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/MismatchedDevices.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/MismatchedDevices.java new file mode 100644 index 000000000..ef6b6eda4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/MismatchedDevices.java @@ -0,0 +1,20 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +public record MismatchedDevices(@JsonProperty + @Schema(description = "Devices present on the account but absent in the request") + List missingDevices, + + @JsonProperty + @Schema(description = "Devices absent on the request but present in the account") + List extraDevices) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/MultiRecipientMessage.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/MultiRecipientMessage.java new file mode 100644 index 000000000..ba6421c61 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/MultiRecipientMessage.java @@ -0,0 +1,97 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import static com.codahale.metrics.MetricRegistry.name; + +import java.util.Arrays; +import java.util.Objects; +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.whispersystems.textsecuregcm.controllers.MessageController; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.providers.MultiRecipientMessageProvider; +import org.whispersystems.textsecuregcm.util.Pair; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter; + +public record MultiRecipientMessage( + @NotNull @Size(min = 1, max = MultiRecipientMessageProvider.MAX_RECIPIENT_COUNT) @Valid Recipient[] recipients, + @NotNull @Size(min = 32) byte[] commonPayload) { + + private static final Counter REJECT_DUPLICATE_RECIPIENT_COUNTER = + Metrics.counter( + name(MessageController.class, "rejectDuplicateRecipients"), + "multiRecipient", "false"); + + public record Recipient(@NotNull + @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) + ServiceIdentifier uuid, + @Min(1) long deviceId, + @Min(0) @Max(65535) int registrationId, + @Size(min = 48, max = 48) @NotNull byte[] perRecipientKeyMaterial) { + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Recipient recipient = (Recipient) o; + return deviceId == recipient.deviceId && registrationId == recipient.registrationId && uuid.equals(recipient.uuid) + && Arrays.equals(perRecipientKeyMaterial, recipient.perRecipientKeyMaterial); + } + + @Override + public int hashCode() { + int result = Objects.hash(uuid, deviceId, registrationId); + result = 31 * result + Arrays.hashCode(perRecipientKeyMaterial); + return result; + } + } + + public MultiRecipientMessage(Recipient[] recipients, byte[] commonPayload) { + this.recipients = recipients; + this.commonPayload = commonPayload; + } + + @AssertTrue + public boolean hasNoDuplicateRecipients() { + boolean valid = + Arrays.stream(recipients).map(r -> new Pair<>(r.uuid(), r.deviceId())).distinct().count() == recipients.length; + if (!valid) { + REJECT_DUPLICATE_RECIPIENT_COUNTER.increment(); + } + return valid; + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + MultiRecipientMessage that = (MultiRecipientMessage) o; + return Arrays.equals(recipients, that.recipients) && Arrays.equals(commonPayload, that.commonPayload); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(recipients); + result = 31 * result + Arrays.hashCode(commonPayload); + return result; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntity.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntity.java new file mode 100644 index 000000000..226d243a1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntity.java @@ -0,0 +1,118 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.protobuf.ByteString; +import java.util.Arrays; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nullable; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter; + +public record OutgoingMessageEntity(UUID guid, + int type, + long timestamp, + + @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) + @Nullable + ServiceIdentifier sourceUuid, + + int sourceDevice, + + @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) + ServiceIdentifier destinationUuid, + + @Nullable UUID updatedPni, + byte[] content, + long serverTimestamp, + boolean urgent, + boolean story, + @Nullable byte[] reportSpamToken) { + + public MessageProtos.Envelope toEnvelope() { + final MessageProtos.Envelope.Builder builder = MessageProtos.Envelope.newBuilder() + .setType(MessageProtos.Envelope.Type.forNumber(type())) + .setTimestamp(timestamp()) + .setServerTimestamp(serverTimestamp()) + .setDestinationUuid(destinationUuid().toServiceIdentifierString()) + .setServerGuid(guid().toString()) + .setStory(story) + .setUrgent(urgent); + + if (sourceUuid() != null) { + builder.setSourceUuid(sourceUuid().toServiceIdentifierString()); + builder.setSourceDevice(sourceDevice()); + } + + if (content() != null) { + builder.setContent(ByteString.copyFrom(content())); + } + + if (updatedPni() != null) { + builder.setUpdatedPni(updatedPni().toString()); + } + + if (reportSpamToken != null) { + builder.setReportSpamToken(ByteString.copyFrom(reportSpamToken)); + } + + return builder.build(); + } + + public static OutgoingMessageEntity fromEnvelope(final MessageProtos.Envelope envelope) { + ByteString token = envelope.getReportSpamToken(); + return new OutgoingMessageEntity( + UUID.fromString(envelope.getServerGuid()), + envelope.getType().getNumber(), + envelope.getTimestamp(), + envelope.hasSourceUuid() ? ServiceIdentifier.valueOf(envelope.getSourceUuid()) : null, + envelope.getSourceDevice(), + envelope.hasDestinationUuid() ? ServiceIdentifier.valueOf(envelope.getDestinationUuid()) : null, + envelope.hasUpdatedPni() ? UUID.fromString(envelope.getUpdatedPni()) : null, + envelope.getContent().toByteArray(), + envelope.getServerTimestamp(), + envelope.getUrgent(), + envelope.getStory(), + token.isEmpty() ? null : token.toByteArray()); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final OutgoingMessageEntity that = (OutgoingMessageEntity) o; + return guid.equals(that.guid) && + type == that.type && + timestamp == that.timestamp && + Objects.equals(sourceUuid, that.sourceUuid) && + sourceDevice == that.sourceDevice && + destinationUuid.equals(that.destinationUuid) && + Objects.equals(updatedPni, that.updatedPni) && + Arrays.equals(content, that.content) && + serverTimestamp == that.serverTimestamp && + urgent == that.urgent && + story == that.story && + Arrays.equals(reportSpamToken, that.reportSpamToken); + } + + @Override + public int hashCode() { + int result = Objects.hash( + guid, type, timestamp, sourceUuid, sourceDevice, destinationUuid, updatedPni, serverTimestamp, urgent, story); + result = 31 * result + Arrays.hashCode(content); + result = 71 * result + Arrays.hashCode(reportSpamToken); + return result; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntityList.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntityList.java new file mode 100644 index 000000000..40447431d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntityList.java @@ -0,0 +1,11 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import java.util.List; + +public record OutgoingMessageEntityList(List messages, boolean more) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PhoneNumberDiscoverabilityRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PhoneNumberDiscoverabilityRequest.java new file mode 100644 index 000000000..ba0e52a2f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PhoneNumberDiscoverabilityRequest.java @@ -0,0 +1,5 @@ +package org.whispersystems.textsecuregcm.entities; + +import javax.validation.constraints.NotNull; + +public record PhoneNumberDiscoverabilityRequest(@NotNull Boolean discoverableByPhoneNumber) {} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PhoneNumberIdentityKeyDistributionRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PhoneNumberIdentityKeyDistributionRequest.java new file mode 100644 index 000000000..3db331a18 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PhoneNumberIdentityKeyDistributionRequest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; +import org.signal.libsignal.protocol.IdentityKey; +import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter; + +public record PhoneNumberIdentityKeyDistributionRequest( + @NotNull + @JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class) + @Schema(description="the new identity key for this account's phone-number identity") + IdentityKey pniIdentityKey, + + @NotNull + @Valid + @ArraySchema( + arraySchema=@Schema(description=""" + A list of synchronization messages to send to companion devices to supply the private keys + associated with the new identity key and their new prekeys. + Exactly one message must be supplied for each enabled device other than the sending (primary) device. + """)) + List<@NotNull @Valid IncomingMessage> deviceMessages, + + @NotNull + @Valid + @Schema(description=""" + A new signed elliptic-curve prekey for each enabled device on the account, including this one. + Each must be accompanied by a valid signature from the new identity key in this request.""") + Map devicePniSignedPrekeys, + + @Schema(description=""" + A new signed post-quantum last-resort prekey for each enabled device on the account, including this one. + May be absent, in which case the last resort PQ prekeys for each device will be deleted if any had been stored. + If present, must contain one prekey per enabled device including this one. + Prekeys for devices that did not previously have any post-quantum prekeys stored will be silently dropped. + Each must be accompanied by a valid signature from the new identity key in this request.""") + @Valid Map devicePniPqLastResortPrekeys, + + @NotNull + @Valid + @Schema(description="The new registration ID to use for the phone-number identity of each device, including this one.") + Map pniRegistrationIds) { + + @AssertTrue + public boolean isSignatureValidOnEachSignedPreKey() { + List> spks = new ArrayList<>(devicePniSignedPrekeys.values()); + if (devicePniPqLastResortPrekeys != null) { + spks.addAll(devicePniPqLastResortPrekeys.values()); + } + return spks.isEmpty() || PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey, spks); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PhoneVerificationRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PhoneVerificationRequest.java new file mode 100644 index 000000000..c471936cf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PhoneVerificationRequest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +import java.util.Base64; +import javax.validation.constraints.AssertTrue; +import javax.ws.rs.ClientErrorException; +import org.apache.http.HttpStatus; + +public interface PhoneVerificationRequest { + + enum VerificationType { + SESSION, + RECOVERY_PASSWORD + } + + String sessionId(); + + byte[] recoveryPassword(); + + // for the @AssertTrue to work with bean validation, method name must follow 'isSmth()'/'getSmth()' naming convention + @AssertTrue + default boolean isValid() { + // checking that exactly one of sessionId/recoveryPassword is non-empty + return isNotBlank(sessionId()) ^ (recoveryPassword() != null && recoveryPassword().length > 0); + } + + default PhoneVerificationRequest.VerificationType verificationType() { + return isNotBlank(sessionId()) ? PhoneVerificationRequest.VerificationType.SESSION + : PhoneVerificationRequest.VerificationType.RECOVERY_PASSWORD; + } + + default byte[] decodeSessionId() { + try { + return Base64.getUrlDecoder().decode(sessionId()); + } catch (final IllegalArgumentException e) { + throw new ClientErrorException("Malformed session ID", HttpStatus.SC_UNPROCESSABLE_ENTITY); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java new file mode 100644 index 000000000..0ebe89cd4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java @@ -0,0 +1,15 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +public interface PreKey { + + long keyId(); + + K publicKey(); + + byte[] serializedPublicKey(); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyCount.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyCount.java new file mode 100644 index 000000000..17bcee889 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyCount.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +public class PreKeyCount { + + @Schema(description="the number of stored unsigned elliptic-curve prekeys for this device") + @JsonProperty + private int count; + + @Schema(description="the number of stored one-time post-quantum prekeys for this device") + @JsonProperty + private int pqCount; + + public PreKeyCount(int ecCount, int pqCount) { + this.count = ecCount; + this.pqCount = pqCount; + } + + public PreKeyCount() {} + + public int getCount() { + return count; + } + + public int getPqCount() { + return pqCount; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponse.java new file mode 100644 index 000000000..c818746ec --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponse.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.VisibleForTesting; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import org.signal.libsignal.protocol.IdentityKey; +import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter; + +public class PreKeyResponse { + + @JsonProperty + @JsonSerialize(using = IdentityKeyAdapter.Serializer.class) + @JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class) + @Schema(description="the public identity key for the requested identity") + private IdentityKey identityKey; + + @JsonProperty + @Schema(description="information about each requested device") + private List devices; + + public PreKeyResponse() {} + + public PreKeyResponse(IdentityKey identityKey, List devices) { + this.identityKey = identityKey; + this.devices = devices; + } + + @VisibleForTesting + public IdentityKey getIdentityKey() { + return identityKey; + } + + @VisibleForTesting + @JsonIgnore + public PreKeyResponseItem getDevice(int deviceId) { + for (PreKeyResponseItem device : devices) { + if (device.getDeviceId() == deviceId) return device; + } + + return null; + } + + @VisibleForTesting + @JsonIgnore + public int getDevicesCount() { + return devices.size(); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseItem.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseItem.java new file mode 100644 index 000000000..0cf519c9f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseItem.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import io.swagger.v3.oas.annotations.media.Schema; + +public class PreKeyResponseItem { + + @JsonProperty + @Schema(description="the device ID of the device to which this item pertains") + private long deviceId; + + @JsonProperty + @Schema(description="the registration ID for the device") + private int registrationId; + + @JsonProperty + @Schema(description="the signed elliptic-curve prekey for the device, if one has been set") + private ECSignedPreKey signedPreKey; + + @JsonProperty + @Schema(description="an unsigned elliptic-curve prekey for the device, if any remain") + private ECPreKey preKey; + + @JsonProperty + @Schema(description="a signed post-quantum prekey for the device " + + "(a one-time prekey if any remain, otherwise the last-resort prekey if one has been set)") + private KEMSignedPreKey pqPreKey; + + public PreKeyResponseItem() {} + + public PreKeyResponseItem(long deviceId, int registrationId, ECSignedPreKey signedPreKey, ECPreKey preKey, KEMSignedPreKey pqPreKey) { + this.deviceId = deviceId; + this.registrationId = registrationId; + this.signedPreKey = signedPreKey; + this.preKey = preKey; + this.pqPreKey = pqPreKey; + } + + @VisibleForTesting + public ECSignedPreKey getSignedPreKey() { + return signedPreKey; + } + + @VisibleForTesting + public ECPreKey getPreKey() { + return preKey; + } + + @VisibleForTesting + public KEMSignedPreKey getPqPreKey() { + return pqPreKey; + } + + @VisibleForTesting + public int getRegistrationId() { + return registrationId; + } + + @VisibleForTesting + public long getDeviceId() { + return deviceId; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeySignatureValidator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeySignatureValidator.java new file mode 100644 index 000000000..67a712426 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeySignatureValidator.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.entities; + +import static com.codahale.metrics.MetricRegistry.name; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import java.util.Collection; +import org.signal.libsignal.protocol.IdentityKey; + +public abstract class PreKeySignatureValidator { + public static final Counter INVALID_SIGNATURE_COUNTER = + Metrics.counter(name(PreKeySignatureValidator.class, "invalidPreKeySignature")); + + public static boolean validatePreKeySignatures(final IdentityKey identityKey, final Collection> spks) { + final boolean success = spks.stream().allMatch(spk -> spk.signatureValid(identityKey)); + + if (!success) { + INVALID_SIGNATURE_COUNTER.increment(); + } + + return success; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyState.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyState.java new file mode 100644 index 000000000..8b17d4da7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyState.java @@ -0,0 +1,114 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.VisibleForTesting; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.ArrayList; +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; +import org.signal.libsignal.protocol.IdentityKey; +import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter; + +public class PreKeyState { + + @JsonProperty + @Valid + @Schema(description="A list of unsigned elliptic-curve prekeys to use for this device. " + + "If present and not empty, replaces all stored unsigned EC prekeys for the device; " + + "if absent or empty, any stored unsigned EC prekeys for the device are not deleted.") + private List<@Valid ECPreKey> preKeys; + + @JsonProperty + @Valid + @Schema(description="An optional signed elliptic-curve prekey to use for this device. " + + "If present, replaces the stored signed elliptic-curve prekey for the device; " + + "if absent, the stored signed prekey is not deleted. " + + "If present, must have a valid signature from the identity key in this request.") + private ECSignedPreKey signedPreKey; + + @JsonProperty + @Valid + @Schema(description="A list of signed post-quantum one-time prekeys to use for this device. " + + "Each key must have a valid signature from the identity key in this request. " + + "If present and not empty, replaces all stored unsigned PQ prekeys for the device; " + + "if absent or empty, any stored unsigned PQ prekeys for the device are not deleted.") + private List<@Valid KEMSignedPreKey> pqPreKeys; + + @JsonProperty + @Valid + @Schema(description="An optional signed last-resort post-quantum prekey to use for this device. " + + "If present, replaces the stored signed post-quantum last-resort prekey for the device; " + + "if absent, a stored last-resort prekey will *not* be deleted. " + + "If present, must have a valid signature from the identity key in this request.") + private KEMSignedPreKey pqLastResortPreKey; + + @JsonProperty + @JsonSerialize(using = IdentityKeyAdapter.Serializer.class) + @JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class) + @NotNull + @Schema(description="Required. " + + "The public identity key for this identity (account or phone-number identity). " + + "If this device is not the primary device for the account, " + + "must match the existing stored identity key for this identity.") + private IdentityKey identityKey; + + public PreKeyState() {} + + @VisibleForTesting + public PreKeyState(IdentityKey identityKey, ECSignedPreKey signedPreKey, List keys) { + this(identityKey, signedPreKey, keys, null, null); + } + + @VisibleForTesting + public PreKeyState(IdentityKey identityKey, ECSignedPreKey signedPreKey, List keys, List pqKeys, KEMSignedPreKey pqLastResortKey) { + this.identityKey = identityKey; + this.signedPreKey = signedPreKey; + this.preKeys = keys; + this.pqPreKeys = pqKeys; + this.pqLastResortPreKey = pqLastResortKey; + } + + public List getPreKeys() { + return preKeys; + } + + public ECSignedPreKey getSignedPreKey() { + return signedPreKey; + } + + public List getPqPreKeys() { + return pqPreKeys; + } + + public KEMSignedPreKey getPqLastResortPreKey() { + return pqLastResortPreKey; + } + + public IdentityKey getIdentityKey() { + return identityKey; + } + + @AssertTrue + public boolean isSignatureValidOnEachSignedKey() { + List> spks = new ArrayList<>(); + if (pqPreKeys != null) { + spks.addAll(pqPreKeys); + } + if (pqLastResortPreKey != null) { + spks.add(pqLastResortPreKey); + } + if (signedPreKey != null) { + spks.add(signedPreKey); + } + return spks.isEmpty() || PreKeySignatureValidator.validatePreKeySignatures(identityKey, spks); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ProfileAvatarUploadAttributes.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ProfileAvatarUploadAttributes.java new file mode 100644 index 000000000..77b1016fa --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ProfileAvatarUploadAttributes.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ProfileAvatarUploadAttributes { + + @JsonProperty + private String key; + + @JsonProperty + private String credential; + + @JsonProperty + private String acl; + + @JsonProperty + private String algorithm; + + @JsonProperty + private String date; + + @JsonProperty + private String policy; + + @JsonProperty + private String signature; + + public ProfileAvatarUploadAttributes() {} + + public ProfileAvatarUploadAttributes(String key, String credential, + String acl, String algorithm, + String date, String policy, + String signature) + { + this.key = key; + this.credential = credential; + this.acl = acl; + this.algorithm = algorithm; + this.date = date; + this.policy = policy; + this.signature = signature; + } + + public String getKey() { + return key; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ProfileKeyCommitmentAdapter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ProfileKeyCommitmentAdapter.java new file mode 100644 index 000000000..29fd82e7c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ProfileKeyCommitmentAdapter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.util.Base64; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; + +public class ProfileKeyCommitmentAdapter { + + public static class Serializing extends JsonSerializer { + @Override + public void serialize(ProfileKeyCommitment value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(Base64.getEncoder().encodeToString(value.serialize())); + } + } + + public static class Deserializing extends JsonDeserializer { + + @Override + public ProfileKeyCommitment deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + try { + return new ProfileKeyCommitment(Base64.getDecoder().decode(p.getValueAsString())); + } catch (InvalidInputException e) { + throw new IOException(e); + } + } + } +} + diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ProvisioningMessage.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ProvisioningMessage.java new file mode 100644 index 000000000..b6c6dff8b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ProvisioningMessage.java @@ -0,0 +1,11 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import javax.validation.constraints.NotEmpty; + +public record ProvisioningMessage(@NotEmpty String body) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PurchasableBadge.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PurchasableBadge.java new file mode 100644 index 000000000..e72bb53d1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/PurchasableBadge.java @@ -0,0 +1,78 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Objects; + +public class PurchasableBadge extends Badge { + private final Duration duration; + + @JsonCreator + public PurchasableBadge( + @JsonProperty("id") final String id, + @JsonProperty("category") final String category, + @JsonProperty("name") final String name, + @JsonProperty("description") final String description, + @JsonProperty("sprites6") final List sprites6, + @JsonProperty("svg") final String svg, + @JsonProperty("svgs") final List svgs, + @JsonProperty("duration") final Duration duration) { + super(id, category, name, description, sprites6, svg, svgs); + this.duration = duration != null ? duration.truncatedTo(ChronoUnit.SECONDS) : null; + } + + public PurchasableBadge(final Badge badge, final Duration duration) { + super( + badge.getId(), + badge.getCategory(), + badge.getName(), + badge.getDescription(), + badge.getSprites6(), + badge.getSvg(), + badge.getSvgs()); + this.duration = duration != null ? duration.truncatedTo(ChronoUnit.SECONDS) : null; + } + + @JsonFormat(shape = Shape.NUMBER_INT) + public Duration getDuration() { + return duration; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + PurchasableBadge that = (PurchasableBadge) o; + return Objects.equals(duration, that.duration); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), duration); + } + + @Override + public String toString() { + return "PurchasableBadge{" + + "super=" + super.toString() + + ", duration=" + duration + + '}'; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RateLimitChallenge.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RateLimitChallenge.java new file mode 100644 index 000000000..bb396ea42 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RateLimitChallenge.java @@ -0,0 +1,32 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.validation.constraints.NotNull; + +public class RateLimitChallenge { + + @JsonProperty + @NotNull + private final String token; + + @JsonProperty + @NotNull + private final List options; + + @JsonCreator + public RateLimitChallenge(@JsonProperty("token") final String token, @JsonProperty("options") final List options) { + + this.token = token; + this.options = options; + } + + public String getToken() { + return token; + } + + public List getOptions() { + return options; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RedeemReceiptRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RedeemReceiptRequest.java new file mode 100644 index 000000000..c4fa3a74c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RedeemReceiptRequest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.NotEmpty; + +public class RedeemReceiptRequest { + + private final byte[] receiptCredentialPresentation; + private final boolean visible; + private final boolean primary; + + @JsonCreator + public RedeemReceiptRequest( + @JsonProperty("receiptCredentialPresentation") byte[] receiptCredentialPresentation, + @JsonProperty("visible") boolean visible, + @JsonProperty("primary") boolean primary) { + this.receiptCredentialPresentation = receiptCredentialPresentation; + this.visible = visible; + this.primary = primary; + } + + @NotEmpty + public byte[] getReceiptCredentialPresentation() { + return receiptCredentialPresentation; + } + + public boolean isVisible() { + return visible; + } + + public boolean isPrimary() { + return primary; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLock.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLock.java new file mode 100644 index 000000000..f08ecc7c7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLock.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Size; + +public class RegistrationLock { + + @JsonProperty + @Size(min=64, max=64) + @NotEmpty + private String registrationLock; + + public RegistrationLock() {} + + @VisibleForTesting + public RegistrationLock(String registrationLock) { + this.registrationLock = registrationLock; + } + + public String getRegistrationLock() { + return registrationLock; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLockFailure.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLockFailure.java new file mode 100644 index 000000000..3a4d7aa77 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLockFailure.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; + +@Schema(description = "A token provided to the client via a push payload") + +public record RegistrationLockFailure( + @Schema(description = "Time remaining in milliseconds before the existing registration lock expires") + long timeRemaining, + @Schema(description = "Credentials that can be used with SVR2") + ExternalServiceCredentials svr2Credentials) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationRequest.java new file mode 100644 index 000000000..68cc6b006 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationRequest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.VisibleForTesting; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import java.util.Optional; +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; +import org.signal.libsignal.protocol.IdentityKey; +import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; +import org.whispersystems.textsecuregcm.util.OptionalIdentityKeyAdapter; + +public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """ + The ID of an existing verification session as it appears in a verification session + metadata object. Must be provided if `recoveryPassword` is not provided; must not be + provided if `recoveryPassword` is provided. + """) + String sessionId, + + @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """ + A base64-encoded registration recovery password. Must be provided if `sessionId` is + not provided; must not be provided if `sessionId` is provided + """) + byte[] recoveryPassword, + + @NotNull + @Valid + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + AccountAttributes accountAttributes, + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """ + If true, indicates that the end user has elected not to transfer data from another + device even though a device transfer is technically possible given the capabilities of + the calling device and the device associated with the existing account (if any). If + false and if a device transfer is technically possible, the registration request will + fail with an HTTP/409 response indicating that the client should prompt the user to + transfer data from an existing device. + """) + boolean skipDeviceTransfer, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """ + If true, indicates that this is a request for "atomic" registration. If any properties + needed for atomic account creation are not present, the request will fail. If false, + atomic account creation can still occur, but only if all required fields are present. + """) + boolean requireAtomic, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """ + The ACI-associated identity key for the account, encoded as a base64 string. If + provided, an account will be created "atomically," and all other properties needed for + atomic account creation must also be present. + """) + @JsonSerialize(using = OptionalIdentityKeyAdapter.Serializer.class) + @JsonDeserialize(using = OptionalIdentityKeyAdapter.Deserializer.class) + Optional aciIdentityKey, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """ + The PNI-associated identity key for the account, encoded as a base64 string. If + provided, an account will be created "atomically," and all other properties needed for + atomic account creation must also be present. + """) + @JsonSerialize(using = OptionalIdentityKeyAdapter.Serializer.class) + @JsonDeserialize(using = OptionalIdentityKeyAdapter.Deserializer.class) + Optional pniIdentityKey, + + @JsonUnwrapped + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + DeviceActivationRequest deviceActivationRequest) implements PhoneVerificationRequest { + + @JsonCreator + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public RegistrationRequest(@JsonProperty("sessionId") String sessionId, + @JsonProperty("recoveryPassword") byte[] recoveryPassword, + @JsonProperty("accountAttributes") AccountAttributes accountAttributes, + @JsonProperty("skipDeviceTransfer") boolean skipDeviceTransfer, + @JsonProperty("requireAtomic") boolean requireAtomic, + @JsonProperty("aciIdentityKey") Optional aciIdentityKey, + @JsonProperty("pniIdentityKey") Optional pniIdentityKey, + @JsonProperty("aciSignedPreKey") Optional<@Valid ECSignedPreKey> aciSignedPreKey, + @JsonProperty("pniSignedPreKey") Optional<@Valid ECSignedPreKey> pniSignedPreKey, + @JsonProperty("aciPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> aciPqLastResortPreKey, + @JsonProperty("pniPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> pniPqLastResortPreKey, + @JsonProperty("apnToken") Optional<@Valid ApnRegistrationId> apnToken, + @JsonProperty("gcmToken") Optional<@Valid GcmRegistrationId> gcmToken) { + + // This may seem a little verbose, but at the time of writing, Jackson struggles with `@JsonUnwrapped` members in + // records, and this is a workaround. Please see + // https://github.com/FasterXML/jackson-databind/issues/3726#issuecomment-1525396869 for additional context. + this(sessionId, recoveryPassword, accountAttributes, skipDeviceTransfer, requireAtomic, aciIdentityKey, pniIdentityKey, + new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnToken, gcmToken)); + } + + @AssertTrue + public boolean isEverySignedKeyValid() { + return validatePreKeySignature(aciIdentityKey(), deviceActivationRequest().aciSignedPreKey()) + && validatePreKeySignature(pniIdentityKey(), deviceActivationRequest().pniSignedPreKey()) + && validatePreKeySignature(aciIdentityKey(), deviceActivationRequest().aciPqLastResortPreKey()) + && validatePreKeySignature(pniIdentityKey(), deviceActivationRequest().pniPqLastResortPreKey()); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static boolean validatePreKeySignature(final Optional maybeIdentityKey, + final Optional> maybeSignedPreKey) { + + return maybeSignedPreKey.map(signedPreKey -> maybeIdentityKey + .map(identityKey -> PreKeySignatureValidator.validatePreKeySignatures(identityKey, List.of(signedPreKey))) + .orElse(false)) + .orElse(true); + } + + @AssertTrue + public boolean isCompleteRequest() { + final boolean hasNoAtomicAccountCreationParameters = + aciIdentityKey().isEmpty() + && pniIdentityKey().isEmpty() + && deviceActivationRequest().aciSignedPreKey().isEmpty() + && deviceActivationRequest().pniSignedPreKey().isEmpty() + && deviceActivationRequest().aciPqLastResortPreKey().isEmpty() + && deviceActivationRequest().pniPqLastResortPreKey().isEmpty(); + + return supportsAtomicAccountCreation() || (!requireAtomic() && hasNoAtomicAccountCreationParameters); + } + + public boolean supportsAtomicAccountCreation() { + return hasExactlyOneMessageDeliveryChannel() + && aciIdentityKey().isPresent() + && pniIdentityKey().isPresent() + && deviceActivationRequest().aciSignedPreKey().isPresent() + && deviceActivationRequest().pniSignedPreKey().isPresent() + && deviceActivationRequest().aciPqLastResortPreKey().isPresent() + && deviceActivationRequest().pniPqLastResortPreKey().isPresent(); + } + + @VisibleForTesting + boolean hasExactlyOneMessageDeliveryChannel() { + if (accountAttributes.getFetchesMessages()) { + return deviceActivationRequest().apnToken().isEmpty() && deviceActivationRequest().gcmToken().isEmpty(); + } else { + return deviceActivationRequest().apnToken().isPresent() ^ deviceActivationRequest().gcmToken().isPresent(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationServiceSession.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationServiceSession.java new file mode 100644 index 000000000..2f581877c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationServiceSession.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import java.util.Base64; +import javax.annotation.Nullable; +import org.signal.registration.rpc.RegistrationSessionMetadata; + +public record RegistrationServiceSession(byte[] id, String number, boolean verified, + @Nullable Long nextSms, @Nullable Long nextVoiceCall, + @Nullable Long nextVerificationAttempt, + long expiration) { + + + public String encodedSessionId() { + return encodeSessionId(id); + } + + public static String encodeSessionId(final byte[] sessionId) { + return Base64.getUrlEncoder().encodeToString(sessionId); + } + + public RegistrationServiceSession(byte[] id, String number, RegistrationSessionMetadata remoteSession) { + this(id, number, remoteSession.getVerified(), + remoteSession.getMayRequestSms() ? remoteSession.getNextSmsSeconds() : null, + remoteSession.getMayRequestVoiceCall() ? remoteSession.getNextVoiceCallSeconds() : null, + remoteSession.getMayCheckCode() ? remoteSession.getNextCodeCheckSeconds() : null, + remoteSession.getExpirationSeconds()); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ReserveUsernameHashRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ReserveUsernameHashRequest.java new file mode 100644 index 000000000..be37a9197 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ReserveUsernameHashRequest.java @@ -0,0 +1,25 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.whispersystems.textsecuregcm.controllers.AccountController; +import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.List; + +public record ReserveUsernameHashRequest( + @NotNull + @Valid + @Size(min=1, max=AccountController.MAXIMUM_USERNAME_HASHES_LIST_LENGTH) + @JsonSerialize(contentUsing = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(contentUsing = ByteArrayBase64UrlAdapter.Deserializing.class) + List usernameHashes +) {} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ReserveUsernameHashResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ReserveUsernameHashResponse.java new file mode 100644 index 000000000..b04b22efd --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/ReserveUsernameHashResponse.java @@ -0,0 +1,20 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.whispersystems.textsecuregcm.controllers.AccountController; +import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter; +import org.whispersystems.textsecuregcm.util.ExactlySize; +import java.util.UUID; + +public record ReserveUsernameHashResponse( + @JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class) + @ExactlySize(AccountController.USERNAME_HASH_LENGTH) + byte[] usernameHash +) {} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SelfBadge.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SelfBadge.java new file mode 100644 index 000000000..20593a645 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SelfBadge.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; +import java.util.List; +import java.util.Objects; + +/** + * Extension of the Badge object returned when asking for one's own badges. + */ +public class SelfBadge extends Badge { + private final Instant expiration; + private final boolean visible; + + public SelfBadge( + @JsonProperty("id") final String id, + @JsonProperty("category") final String category, + @JsonProperty("name") final String name, + @JsonProperty("description") final String description, + @JsonProperty("sprites6") final List sprites6, + @JsonProperty("svg") final String svg, + @JsonProperty("svgs") final List svgs, + @JsonProperty("expiration") final Instant expiration, + @JsonProperty("visible") final boolean visible) { + super(id, category, name, description, sprites6, svg, svgs); + this.expiration = expiration; + this.visible = visible; + } + + public Instant getExpiration() { + return expiration; + } + + public boolean isVisible() { + return visible; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + SelfBadge selfBadge = (SelfBadge) o; + return visible == selfBadge.visible && Objects.equals(expiration, selfBadge.expiration); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), expiration, visible); + } + + @Override + public String toString() { + return "SelfBadge{" + + "super=" + super.toString() + + ", expiration=" + expiration + + ", visible=" + visible + + '}'; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SendMessageResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SendMessageResponse.java new file mode 100644 index 000000000..9c8649036 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SendMessageResponse.java @@ -0,0 +1,20 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SendMessageResponse { + + @JsonProperty + private boolean needsSync; + + public SendMessageResponse() {} + + public SendMessageResponse(boolean needsSync) { + this.needsSync = needsSync; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SendMultiRecipientMessageResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SendMultiRecipientMessageResponse.java new file mode 100644 index 000000000..cc3967df9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SendMultiRecipientMessageResponse.java @@ -0,0 +1,20 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.VisibleForTesting; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter; +import java.util.List; +import java.util.UUID; + +public record SendMultiRecipientMessageResponse(@JsonSerialize(contentUsing = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(contentUsing = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class) + List uuids404) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SignedPreKey.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SignedPreKey.java new file mode 100644 index 000000000..a8715b5ce --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SignedPreKey.java @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import org.signal.libsignal.protocol.IdentityKey; + +public interface SignedPreKey extends PreKey { + + byte[] signature(); + + default boolean signatureValid(final IdentityKey identityKey) { + try { + return identityKey.getPublicKey().verifySignature(serializedPublicKey(), signature()); + } catch (final Exception e) { + return false; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SpamReport.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SpamReport.java new file mode 100644 index 000000000..185abbff6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SpamReport.java @@ -0,0 +1,13 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; +import javax.annotation.Nullable; +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; + +public record SpamReport(@JsonSerialize(using = ByteArrayAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) + @Nullable byte[] token) {} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/StaleDevices.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/StaleDevices.java new file mode 100644 index 000000000..bed26a51f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/StaleDevices.java @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +public record StaleDevices(@JsonProperty + @Schema(description = "Devices that are no longer active") + List staleDevices) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/StickerPackFormUploadAttributes.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/StickerPackFormUploadAttributes.java new file mode 100644 index 000000000..bd0cd92e6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/StickerPackFormUploadAttributes.java @@ -0,0 +1,114 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class StickerPackFormUploadAttributes { + + @JsonProperty + private StickerPackFormUploadItem manifest; + + @JsonProperty + private List stickers; + + @JsonProperty + private String packId; + + public StickerPackFormUploadAttributes() {} + + public StickerPackFormUploadAttributes(String packId, StickerPackFormUploadItem manifest, List stickers) { + this.packId = packId; + this.manifest = manifest; + this.stickers = stickers; + } + + public StickerPackFormUploadItem getManifest() { + return manifest; + } + + public List getStickers() { + return stickers; + } + + public String getPackId() { + return packId; + } + + public static class StickerPackFormUploadItem { + @JsonProperty + private int id; + + @JsonProperty + private String key; + + @JsonProperty + private String credential; + + @JsonProperty + private String acl; + + @JsonProperty + private String algorithm; + + @JsonProperty + private String date; + + @JsonProperty + private String policy; + + @JsonProperty + private String signature; + + public StickerPackFormUploadItem() {} + + public StickerPackFormUploadItem(int id, String key, String credential, String acl, String algorithm, String date, String policy, String signature) { + this.key = key; + this.credential = credential; + this.acl = acl; + this.algorithm = algorithm; + this.date = date; + this.policy = policy; + this.signature = signature; + this.id = id; + } + + public String getKey() { + return key; + } + + public String getCredential() { + return credential; + } + + public String getAcl() { + return acl; + } + + public String getAlgorithm() { + return algorithm; + } + + public String getDate() { + return date; + } + + public String getPolicy() { + return policy; + } + + public String getSignature() { + return signature; + } + + public int getId() { + return id; + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SubmitVerificationCodeRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SubmitVerificationCodeRequest.java new file mode 100644 index 000000000..07bf48487 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/SubmitVerificationCodeRequest.java @@ -0,0 +1,12 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import javax.validation.constraints.NotBlank; + +public record SubmitVerificationCodeRequest(@NotBlank String code) { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEvent.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEvent.java new file mode 100644 index 000000000..a7e58c3bd --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEvent.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotEmpty; + +public class UnregisteredEvent { + + @JsonProperty + @NotEmpty + private String registrationId; + + @JsonProperty + private String canonicalId; + + @JsonProperty + @NotEmpty + private String number; + + @JsonProperty + @Min(1) + private int deviceId; + + @JsonProperty + private long timestamp; + + public String getRegistrationId() { + return registrationId; + } + + public String getCanonicalId() { + return canonicalId; + } + + public String getNumber() { + return number; + } + + public int getDeviceId() { + return deviceId; + } + + public long getTimestamp() { + return timestamp; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEventList.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEventList.java new file mode 100644 index 000000000..91896ab0b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEventList.java @@ -0,0 +1,22 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.LinkedList; +import java.util.List; + +public class UnregisteredEventList { + + @JsonProperty + private List devices; + + public List getDevices() { + if (devices == null) return new LinkedList<>(); + else return devices; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UpdateVerificationSessionRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UpdateVerificationSessionRequest.java new file mode 100644 index 000000000..aca03d9ec --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UpdateVerificationSessionRequest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.Nullable; +import org.whispersystems.textsecuregcm.push.PushNotification; + +public record UpdateVerificationSessionRequest(@Nullable String pushToken, + @Nullable PushTokenType pushTokenType, + @Nullable String pushChallenge, + @Nullable String captcha, + @Nullable String mcc, + @Nullable String mnc) { + + public enum PushTokenType { + @JsonProperty("apn") + APN, + @JsonProperty("fcm") + FCM; + + public PushNotification.TokenType toTokenType() { + return switch (this) { + + case APN -> PushNotification.TokenType.APN; + case FCM -> PushNotification.TokenType.FCM; + }; + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserCapabilities.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserCapabilities.java new file mode 100644 index 000000000..cb0f1f133 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserCapabilities.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.whispersystems.textsecuregcm.storage.Account; + +public record UserCapabilities( + @Deprecated(forRemoval = true) + @JsonProperty("gv1-migration") + boolean gv1Migration, + + @Deprecated(forRemoval = true) + boolean senderKey, + + @Deprecated(forRemoval = true) + boolean announcementGroup, + + @Deprecated(forRemoval = true) + boolean changeNumber, + + @Deprecated(forRemoval = true) + boolean stories, + + @Deprecated(forRemoval = true) + boolean giftBadges, + boolean paymentActivation, + boolean pni) { + + public static UserCapabilities createForAccount(Account account) { + return new UserCapabilities( + true, + true, + true, + true, + true, + true, + + // Hardcode payment activation flag to false until all clients support the flow + false, + + // Although originally intended to indicate that clients support phone number identifiers, the scope of this + // flag has expanded to cover phone number privacy in general + account.isPniSupported()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserRemoteConfig.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserRemoteConfig.java new file mode 100644 index 000000000..276607cdd --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserRemoteConfig.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class UserRemoteConfig { + + @JsonProperty + private String name; + + @JsonProperty + private boolean enabled; + + @JsonProperty + private String value; + + public UserRemoteConfig() {} + + public UserRemoteConfig(String name, boolean enabled, String value) { + this.name = name; + this.enabled = enabled; + this.value = value; + } + + public String getName() { + return name; + } + + public boolean isEnabled() { + return enabled; + } + + public String getValue() { + return value; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserRemoteConfigList.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserRemoteConfigList.java new file mode 100644 index 000000000..de10314f1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserRemoteConfigList.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + +public class UserRemoteConfigList { + + @JsonProperty + private List config; + + @JsonProperty + @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) + private Instant serverEpochTime; + + public UserRemoteConfigList() {} + + public UserRemoteConfigList(List config, Instant serverEpochTime) { + this.config = config; + this.serverEpochTime = serverEpochTime != null ? serverEpochTime.truncatedTo(ChronoUnit.SECONDS) : null; + } + + public List getConfig() { + return config; + } + + public Instant getServerEpochTime() { + return serverEpochTime; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UsernameHashResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UsernameHashResponse.java new file mode 100644 index 000000000..f2738668e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UsernameHashResponse.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; +import org.whispersystems.textsecuregcm.controllers.AccountController; +import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter; +import org.whispersystems.textsecuregcm.util.ExactlySize; +import java.util.UUID; +import javax.annotation.Nullable; +import javax.validation.Valid; + +public record UsernameHashResponse( + @Valid + @JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class) + @ExactlySize(AccountController.USERNAME_HASH_LENGTH) + @Schema(type = "string", description = "The hash of the confirmed username, as supplied in the request") + byte[] usernameHash, + + @Nullable + @Valid + @Schema(type = "string", description = "A handle that can be included in username links to retrieve the stored encrypted username") + UUID usernameLinkHandle +) {} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UsernameLinkHandle.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UsernameLinkHandle.java new file mode 100644 index 000000000..0840b4a77 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/UsernameLinkHandle.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import java.util.UUID; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import io.swagger.v3.oas.annotations.media.Schema; + +public record UsernameLinkHandle( + @Schema(description = "A handle that can be included in username links to retrieve the stored encrypted username") + @NotNull + UUID usernameLinkHandle) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationCodeRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationCodeRequest.java new file mode 100644 index 000000000..b85a2c04e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationCodeRequest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.NotNull; +import org.whispersystems.textsecuregcm.registration.MessageTransport; + +public record VerificationCodeRequest(@NotNull Transport transport, @NotNull String client) { + + public enum Transport { + @JsonProperty("sms") + SMS, + @JsonProperty("voice") + VOICE; + + public MessageTransport toMessageTransport() { + return switch (this) { + case SMS -> MessageTransport.SMS; + case VOICE -> MessageTransport.VOICE; + }; + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationSessionResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationSessionResponse.java new file mode 100644 index 000000000..a3caaf972 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationSessionResponse.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import java.util.List; +import javax.annotation.Nullable; +import org.whispersystems.textsecuregcm.registration.VerificationSession; + +public record VerificationSessionResponse(String id, @Nullable Long nextSms, @Nullable Long nextCall, + @Nullable Long nextVerificationAttempt, boolean allowedToRequestCode, + List requestedInformation, + boolean verified) { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java new file mode 100644 index 000000000..3b8ed60dc --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java @@ -0,0 +1,83 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter; + +public class VersionedProfileResponse { + + @JsonUnwrapped + private BaseProfileResponse baseProfileResponse; + + @JsonProperty + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + private byte[] name; + + @JsonProperty + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + private byte[] about; + + @JsonProperty + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + private byte[] aboutEmoji; + + @JsonProperty + private String avatar; + + @JsonProperty + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + private byte[] paymentAddress; + + public VersionedProfileResponse() { + } + + public VersionedProfileResponse(final BaseProfileResponse baseProfileResponse, + final byte[] name, + final byte[] about, + final byte[] aboutEmoji, + final String avatar, + final byte[] paymentAddress) { + + this.baseProfileResponse = baseProfileResponse; + this.name = name; + this.about = about; + this.aboutEmoji = aboutEmoji; + this.avatar = avatar; + this.paymentAddress = paymentAddress; + } + + public BaseProfileResponse getBaseProfileResponse() { + return baseProfileResponse; + } + + public byte[] getName() { + return name; + } + + public byte[] getAbout() { + return about; + } + + public byte[] getAboutEmoji() { + return aboutEmoji; + } + + public String getAvatar() { + return avatar; + } + + public byte[] getPaymentAddress() { + return paymentAddress; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/experiment/Experiment.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/experiment/Experiment.java new file mode 100644 index 000000000..f049a84a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/experiment/Experiment.java @@ -0,0 +1,132 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.experiment; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.common.annotations.VisibleForTesting; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.function.Supplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An experiment compares the results of two operations and records metrics to assess how frequently they match. + */ +public class Experiment { + + private final String name; + + private final Timer matchTimer; + private final Timer errorTimer; + + private final Timer bothPresentMismatchTimer; + private final Timer controlNullMismatchTimer; + private final Timer experimentNullMismatchTimer; + + private static final String OUTCOME_TAG = "outcome"; + private static final String MATCH_OUTCOME = "match"; + private static final String MISMATCH_OUTCOME = "mismatch"; + private static final String ERROR_OUTCOME = "error"; + + private static final String MISMATCH_TYPE_TAG = "mismatchType"; + private static final String BOTH_PRESENT_MISMATCH = "bothPresent"; + private static final String CONTROL_NULL_MISMATCH = "controlResultNull"; + private static final String EXPERIMENT_NULL_MISMATCH = "experimentResultNull"; + + private static final Logger log = LoggerFactory.getLogger(Experiment.class); + + public Experiment(final String... names) { + this(name(Experiment.class, names), + Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, MATCH_OUTCOME), + Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, ERROR_OUTCOME), + Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, MISMATCH_OUTCOME, MISMATCH_TYPE_TAG, + BOTH_PRESENT_MISMATCH), + Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, MISMATCH_OUTCOME, MISMATCH_TYPE_TAG, + CONTROL_NULL_MISMATCH), + Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, MISMATCH_OUTCOME, MISMATCH_TYPE_TAG, + EXPERIMENT_NULL_MISMATCH)); + } + + @VisibleForTesting + Experiment(final String name, final Timer matchTimer, final Timer errorTimer, final Timer bothPresentMismatchTimer, + final Timer controlNullMismatchTimer, final Timer experimentNullMismatchTimer) { + this.name = name; + + this.matchTimer = matchTimer; + this.errorTimer = errorTimer; + + this.bothPresentMismatchTimer = bothPresentMismatchTimer; + this.controlNullMismatchTimer = controlNullMismatchTimer; + this.experimentNullMismatchTimer = experimentNullMismatchTimer; + } + + public void compareFutureResult(final T expected, final CompletionStage experimentStage) { + final Timer.Sample sample = Timer.start(); + + experimentStage.whenComplete((actual, cause) -> { + if (cause != null) { + recordError(cause, sample); + } else { + recordResult(expected, actual, sample); + } + }); + } + + public void compareSupplierResult(final T expected, final Supplier experimentSupplier) { + final Timer.Sample sample = Timer.start(); + + try { + final T result = experimentSupplier.get(); + + recordResult(expected, result, sample); + } catch (final Exception e) { + recordError(e, sample); + } + } + + public void compareSupplierResultAsync(final T expected, final Supplier experimentSupplier, final Executor executor) { + final Timer.Sample sample = Timer.start(); + + try { + compareFutureResult(expected, CompletableFuture.supplyAsync(experimentSupplier, executor)); + } catch (final Exception e) { + recordError(e, sample); + } + } + + private void recordError(final Throwable cause, final Timer.Sample sample) { + log.warn("Experiment {} threw an exception.", name, cause); + sample.stop(errorTimer); + } + + @VisibleForTesting + void recordResult(final T expected, final T actual, final Timer.Sample sample) { + if (expected instanceof Optional && actual instanceof Optional) { + recordResult(((Optional) expected).orElse(null), ((Optional) actual).orElse(null), sample); + } else { + final Timer timer; + + if (Objects.equals(expected, actual)) { + timer = matchTimer; + } else if (expected == null) { + timer = controlNullMismatchTimer; + } else if (actual == null) { + timer = experimentNullMismatchTimer; + } else { + timer = bothPresentMismatchTimer; + } + + sample.stop(timer); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/experiment/ExperimentEnrollmentManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/experiment/ExperimentEnrollmentManager.java new file mode 100644 index 000000000..e9f09c354 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/experiment/ExperimentEnrollmentManager.java @@ -0,0 +1,77 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.experiment; + +import java.util.Optional; +import java.util.UUID; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicExperimentEnrollmentConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicPreRegistrationExperimentEnrollmentConfiguration; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.util.Util; + +public class ExperimentEnrollmentManager { + + private final DynamicConfigurationManager dynamicConfigurationManager; + + public ExperimentEnrollmentManager(final DynamicConfigurationManager dynamicConfigurationManager) { + this.dynamicConfigurationManager = dynamicConfigurationManager; + } + + public boolean isEnrolled(final UUID accountUuid, final String experimentName) { + + final Optional maybeConfiguration = dynamicConfigurationManager + .getConfiguration().getExperimentEnrollmentConfiguration(experimentName); + + return maybeConfiguration.map(config -> { + + if (config.getEnrolledUuids().contains(accountUuid)) { + return true; + } + + return isEnrolled(accountUuid, config.getEnrollmentPercentage(), experimentName); + + }).orElse(false); + } + + public boolean isEnrolled(final String e164, final String experimentName) { + + final Optional maybeConfiguration = dynamicConfigurationManager + .getConfiguration().getPreRegistrationEnrollmentConfiguration(experimentName); + + return maybeConfiguration.map(config -> { + + if (config.getEnrolledE164s().contains(e164)) { + return true; + } + + if (config.getExcludedE164s().contains(e164)) { + return false; + } + + { + final String countryCode = Util.getCountryCode(e164); + + if (config.getIncludedCountryCodes().contains(countryCode)) { + return true; + } + + if (config.getExcludedCountryCodes().contains(countryCode)) { + return false; + } + } + + return isEnrolled(e164, config.getEnrollmentPercentage(), experimentName); + + }).orElse(false); + } + + private boolean isEnrolled(final Object entity, final int enrollmentPercentage, final String experimentName) { + final int enrollmentHash = ((entity.hashCode() ^ experimentName.hashCode()) & Integer.MAX_VALUE) % 100; + + return enrollmentHash < enrollmentPercentage; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/filters/RemoteDeprecationFilter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/filters/RemoteDeprecationFilter.java new file mode 100644 index 000000000..6e9c590af --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/filters/RemoteDeprecationFilter.java @@ -0,0 +1,153 @@ +/* + * Copyright 2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.filters; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.HttpHeaders; +import com.vdurmont.semver4j.Semver; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import io.micrometer.core.instrument.Metrics; +import java.io.IOException; +import java.util.Map; +import java.util.Set; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRemoteDeprecationConfiguration; +import org.whispersystems.textsecuregcm.grpc.StatusConstants; +import org.whispersystems.textsecuregcm.grpc.UserAgentInterceptor; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; +import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; +import org.whispersystems.textsecuregcm.util.ua.UserAgent; +import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; + +/** + * The remote deprecation filter rejects traffic from clients older than a configured minimum + * version. It may optionally also reject traffic from clients with unrecognized User-Agent strings. + * If a client platform does not have a configured minimum version, all traffic from that client + * platform is allowed. + */ +public class RemoteDeprecationFilter implements Filter, ServerInterceptor { + + private final DynamicConfigurationManager dynamicConfigurationManager; + + private static final String DEPRECATED_CLIENT_COUNTER_NAME = name(RemoteDeprecationFilter.class, "deprecated"); + private static final String PENDING_DEPRECATION_COUNTER_NAME = name(RemoteDeprecationFilter.class, "pendingDeprecation"); + private static final String PLATFORM_TAG = "platform"; + private static final String REASON_TAG_NAME = "reason"; + private static final String EXPIRED_CLIENT_REASON = "expired"; + private static final String BLOCKED_CLIENT_REASON = "blocked"; + private static final String UNRECOGNIZED_UA_REASON = "unrecognized_user_agent"; + + public RemoteDeprecationFilter(final DynamicConfigurationManager dynamicConfigurationManager) { + this.dynamicConfigurationManager = dynamicConfigurationManager; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + final String userAgentString = ((HttpServletRequest) request).getHeader(HttpHeaders.USER_AGENT); + + UserAgent userAgent; + try { + userAgent = UserAgentUtil.parseUserAgentString(userAgentString); + } catch (final UnrecognizedUserAgentException e) { + userAgent = null; + } + + if (shouldBlock(userAgent)) { + ((HttpServletResponse) response).sendError(499); + } else { + chain.doFilter(request, response); + } + } + + @Override + public ServerCall.Listener interceptCall( + final ServerCall call, + final Metadata headers, + final ServerCallHandler next) { + + if (shouldBlock(UserAgentUtil.userAgentFromGrpcContext())) { + call.close(StatusConstants.UPGRADE_NEEDED_STATUS, new Metadata()); + return new ServerCall.Listener<>() {}; + } else { + return next.startCall(call, headers); + } + } + + private boolean shouldBlock(final UserAgent userAgent) { + final DynamicRemoteDeprecationConfiguration configuration = dynamicConfigurationManager + .getConfiguration().getRemoteDeprecationConfiguration(); + final Map minimumVersionsByPlatform = configuration.getMinimumVersions(); + final Map versionsPendingDeprecationByPlatform = configuration + .getVersionsPendingDeprecation(); + final Map> blockedVersionsByPlatform = configuration.getBlockedVersions(); + final Map> versionsPendingBlockByPlatform = configuration.getVersionsPendingBlock(); + + boolean shouldBlock = false; + + if (userAgent == null) { + if (configuration.isUnrecognizedUserAgentAllowed()) { + return false; + } + recordDeprecation(null, UNRECOGNIZED_UA_REASON); + return true; + } + + if (blockedVersionsByPlatform.containsKey(userAgent.getPlatform())) { + if (blockedVersionsByPlatform.get(userAgent.getPlatform()).contains(userAgent.getVersion())) { + recordDeprecation(userAgent, BLOCKED_CLIENT_REASON); + shouldBlock = true; + } + } + + if (minimumVersionsByPlatform.containsKey(userAgent.getPlatform())) { + if (userAgent.getVersion().isLowerThan(minimumVersionsByPlatform.get(userAgent.getPlatform()))) { + recordDeprecation(userAgent, EXPIRED_CLIENT_REASON); + shouldBlock = true; + } + } + + if (versionsPendingBlockByPlatform.containsKey(userAgent.getPlatform())) { + if (versionsPendingBlockByPlatform.get(userAgent.getPlatform()).contains(userAgent.getVersion())) { + recordPendingDeprecation(userAgent, BLOCKED_CLIENT_REASON); + } + } + + if (versionsPendingDeprecationByPlatform.containsKey(userAgent.getPlatform())) { + if (userAgent.getVersion().isLowerThan(versionsPendingDeprecationByPlatform.get(userAgent.getPlatform()))) { + recordPendingDeprecation(userAgent, EXPIRED_CLIENT_REASON); + } + } + + return shouldBlock; + } + + private void recordDeprecation(final UserAgent userAgent, final String reason) { + Metrics.counter(DEPRECATED_CLIENT_COUNTER_NAME, + PLATFORM_TAG, userAgent != null ? userAgent.getPlatform().name().toLowerCase() : "unrecognized", + REASON_TAG_NAME, reason).increment(); + } + + private void recordPendingDeprecation(final UserAgent userAgent, final String reason) { + Metrics.counter(PENDING_DEPRECATION_COUNTER_NAME, + PLATFORM_TAG, userAgent.getPlatform().name().toLowerCase(), + REASON_TAG_NAME, reason).increment(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/filters/RequestStatisticsFilter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/filters/RequestStatisticsFilter.java new file mode 100644 index 000000000..078fb6a1b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/filters/RequestStatisticsFilter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.filters; + +import static com.codahale.metrics.MetricRegistry.name; +import static java.util.Objects.requireNonNull; + +import com.google.common.net.HttpHeaders; +import com.google.common.net.InetAddresses; +import io.micrometer.core.instrument.Metrics; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import javax.annotation.Nonnull; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.metrics.TrafficSource; +import org.whispersystems.textsecuregcm.util.HeaderUtils; + +public class RequestStatisticsFilter implements ContainerRequestFilter { + + private static final Logger logger = LoggerFactory.getLogger(RequestStatisticsFilter.class); + + private static final String CONTENT_LENGTH_DISTRIBUTION_NAME = name(RequestStatisticsFilter.class, "contentLength"); + + private static final String IP_VERSION_METRIC = name(RequestStatisticsFilter.class, "ipVersion"); + + private static final String TRAFFIC_SOURCE_TAG = "trafficSource"; + + private static final String IP_VERSION_TAG = "ipVersion"; + + @Nonnull + private final String trafficSourceTag; + + + public RequestStatisticsFilter(@Nonnull final TrafficSource trafficeSource) { + this.trafficSourceTag = requireNonNull(trafficeSource).name().toLowerCase(); + } + + @Override + public void filter(final ContainerRequestContext requestContext) throws IOException { + try { + Metrics.summary(CONTENT_LENGTH_DISTRIBUTION_NAME, TRAFFIC_SOURCE_TAG, trafficSourceTag) + .record(requestContext.getLength()); + Metrics.counter(IP_VERSION_METRIC, TRAFFIC_SOURCE_TAG, trafficSourceTag, IP_VERSION_TAG, resolveIpVersion(requestContext)) + .increment(); + } catch (final Exception e) { + logger.warn("Error recording request statistics", e); + } + } + + @Nonnull + private static String resolveIpVersion(@Nonnull final ContainerRequestContext ctx) { + return HeaderUtils.getMostRecentProxy(ctx.getHeaderString(HttpHeaders.X_FORWARDED_FOR)) + .map(ipString -> { + try { + //noinspection UnstableApiUsage + final InetAddress addr = InetAddresses.forString(ipString); + if (addr instanceof Inet4Address) { + return "IPv4"; + } + if (addr instanceof Inet6Address) { + return "IPv6"; + } + } catch (IllegalArgumentException e) { + // ignore illegal argument exception + } + return null; + }) + .orElse("unresolved"); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/filters/TimestampResponseFilter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/filters/TimestampResponseFilter.java new file mode 100644 index 000000000..5d9553ccb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/filters/TimestampResponseFilter.java @@ -0,0 +1,22 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.filters; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import org.whispersystems.textsecuregcm.util.HeaderUtils; + +/** + * Injects a timestamp header into all outbound responses. + */ +public class TimestampResponseFilter implements ContainerResponseFilter { + + @Override + public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext) { + responseContext.getHeaders().add(HeaderUtils.TIMESTAMP_HEADER, System.currentTimeMillis()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/gcp/CanonicalRequest.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/gcp/CanonicalRequest.java new file mode 100644 index 000000000..c4a94686b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/gcp/CanonicalRequest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.gcp; + +import javax.annotation.Nonnull; + +public class CanonicalRequest { + + @Nonnull + private final String canonicalRequest; + + @Nonnull + private final String resourcePath; + + @Nonnull + private final String canonicalQuery; + + @Nonnull + private final String activeDatetime; + + @Nonnull + private final String credentialScope; + + @Nonnull + private final String domain; + + private final int maxSizeInBytes; + + public CanonicalRequest(@Nonnull String canonicalRequest, @Nonnull String resourcePath, @Nonnull String canonicalQuery, @Nonnull String activeDatetime, @Nonnull String credentialScope, @Nonnull String domain, int maxSizeInBytes) { + this.canonicalRequest = canonicalRequest; + this.resourcePath = resourcePath; + this.canonicalQuery = canonicalQuery; + this.activeDatetime = activeDatetime; + this.credentialScope = credentialScope; + this.domain = domain; + this.maxSizeInBytes = maxSizeInBytes; + } + + @Nonnull + String getCanonicalRequest() { + return canonicalRequest; + } + + @Nonnull + public String getResourcePath() { + return resourcePath; + } + + @Nonnull + public String getCanonicalQuery() { + return canonicalQuery; + } + + @Nonnull + String getActiveDatetime() { + return activeDatetime; + } + + @Nonnull + String getCredentialScope() { + return credentialScope; + } + + @Nonnull + public String getDomain() { + return domain; + } + + public int getMaxSizeInBytes() { + return maxSizeInBytes; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/gcp/CanonicalRequestGenerator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/gcp/CanonicalRequestGenerator.java new file mode 100644 index 000000000..d530bf887 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/gcp/CanonicalRequestGenerator.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.gcp; + +import io.dropwizard.util.Strings; + +import javax.annotation.Nonnull; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Locale; + +public class CanonicalRequestGenerator { + private static final DateTimeFormatter SIMPLE_UTC_DATE = DateTimeFormatter.ofPattern("yyyyMMdd", Locale.US).withZone(ZoneOffset.UTC); + private static final DateTimeFormatter SIMPLE_UTC_DATE_TIME = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'", Locale.US).withZone(ZoneOffset.UTC); + + @Nonnull + private final String domain; + + @Nonnull + private final String email; + + private final int maxSizeBytes; + + @Nonnull + private final String pathPrefix; + + public CanonicalRequestGenerator(@Nonnull String domain, @Nonnull String email, int maxSizeBytes, @Nonnull String pathPrefix) { + this.domain = domain; + this.email = email; + this.maxSizeBytes = maxSizeBytes; + this.pathPrefix = pathPrefix; + } + + public CanonicalRequest createFor(@Nonnull final String key, @Nonnull final ZonedDateTime now) { + final StringBuilder result = new StringBuilder("POST\n"); + + final StringBuilder resourcePathBuilder = new StringBuilder(); + if (!Strings.isNullOrEmpty(pathPrefix)) { + resourcePathBuilder.append(pathPrefix); + } + resourcePathBuilder.append('/').append(URLEncoder.encode(key, StandardCharsets.UTF_8)); + final String resourcePath = resourcePathBuilder.toString(); + result.append(resourcePath).append('\n'); + + final String activeDatetime = SIMPLE_UTC_DATE_TIME.format(now); + final String canonicalQuery = "X-Goog-Algorithm=GOOG4-RSA-SHA256" + + "&X-Goog-Credential=" + URLEncoder.encode(makeCredential(email, now), StandardCharsets.UTF_8) + + "&X-Goog-Date=" + URLEncoder.encode(activeDatetime, StandardCharsets.UTF_8) + + "&X-Goog-Expires=" + Duration.of(25, ChronoUnit.HOURS).toSeconds() + + "&X-Goog-SignedHeaders=host%3Bx-goog-content-length-range%3Bx-goog-resumable"; + result.append(canonicalQuery).append('\n'); + + result.append("host:").append(domain).append('\n'); + result.append("x-goog-content-length-range:1,").append(maxSizeBytes).append('\n'); + result.append("x-goog-resumable:start\n"); + result.append('\n'); + + result.append("host;x-goog-content-length-range;x-goog-resumable\n"); + + result.append("UNSIGNED-PAYLOAD"); + + return new CanonicalRequest(result.toString(), resourcePath, canonicalQuery, activeDatetime, makeCredentialScope(now), domain, maxSizeBytes); + } + + private String makeCredentialScope(@Nonnull ZonedDateTime now) { + return SIMPLE_UTC_DATE.format(now) + "/auto/storage/goog4_request"; + } + + private String makeCredential(@Nonnull String email, @Nonnull ZonedDateTime now) { + return email + '/' + makeCredentialScope(now); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/gcp/CanonicalRequestSigner.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/gcp/CanonicalRequestSigner.java new file mode 100644 index 000000000..331336d41 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/gcp/CanonicalRequestSigner.java @@ -0,0 +1,101 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.gcp; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.HexFormat; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nonnull; + +public class CanonicalRequestSigner { + + @Nonnull + private final PrivateKey rsaSigningKey; + + private static final Pattern PRIVATE_KEY_PATTERN = + Pattern.compile("^-+BEGIN PRIVATE KEY-+\\s*(.+)\\n-+END PRIVATE KEY-+\\s*$", Pattern.DOTALL); + + public CanonicalRequestSigner(@Nonnull String rsaSigningKey) throws IOException, InvalidKeyException, InvalidKeySpecException { + this.rsaSigningKey = initializeRsaSigningKey(rsaSigningKey); + } + + public String sign(@Nonnull CanonicalRequest canonicalRequest) { + return sign(makeStringToSign(canonicalRequest)); + } + + private String makeStringToSign(@Nonnull final CanonicalRequest canonicalRequest) { + final StringBuilder result = new StringBuilder("GOOG4-RSA-SHA256\n"); + + result.append(canonicalRequest.getActiveDatetime()).append('\n'); + + result.append(canonicalRequest.getCredentialScope()).append('\n'); + + final MessageDigest sha256; + try { + sha256 = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + sha256.update(canonicalRequest.getCanonicalRequest().getBytes(StandardCharsets.UTF_8)); + result.append(HexFormat.of().formatHex(sha256.digest())); + + return result.toString(); + } + + private String sign(@Nonnull String stringToSign) { + final byte[] signature; + try { + final Signature sha256rsa = Signature.getInstance("SHA256WITHRSA"); + sha256rsa.initSign(rsaSigningKey); + sha256rsa.update(stringToSign.getBytes(StandardCharsets.UTF_8)); + signature = sha256rsa.sign(); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new AssertionError(e); + } + return HexFormat.of().formatHex(signature); + } + + private static PrivateKey initializeRsaSigningKey(String rsaSigningKey) throws IOException, InvalidKeyException, InvalidKeySpecException { + final Matcher matcher = PRIVATE_KEY_PATTERN.matcher(rsaSigningKey); + + if (matcher.matches()) { + try { + final KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getMimeDecoder().decode(matcher.group(1))); + final PrivateKey key = keyFactory.generatePrivate(keySpec); + + testKeyIsValidForSigning(key); + return key; + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + throw new IOException("Invalid RSA key"); + } + + private static void testKeyIsValidForSigning(PrivateKey key) throws InvalidKeyException { + final Signature sha256rsa; + try { + sha256rsa = Signature.getInstance("SHA256WITHRSA"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + sha256rsa.initSign(key); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/AcceptLanguageInterceptor.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/AcceptLanguageInterceptor.java new file mode 100644 index 000000000..a80c2eb32 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/AcceptLanguageInterceptor.java @@ -0,0 +1,68 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import com.google.common.annotations.VisibleForTesting; +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.micrometer.core.instrument.Metrics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.ua.UserAgent; +import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +public class AcceptLanguageInterceptor implements ServerInterceptor { + private static final Logger logger = LoggerFactory.getLogger(AcceptLanguageInterceptor.class); + private static final String INVALID_ACCEPT_LANGUAGE_COUNTER_NAME = name(AcceptLanguageInterceptor.class, "invalidAcceptLanguage"); + + @VisibleForTesting + public static final Metadata.Key ACCEPTABLE_LANGUAGES_GRPC_HEADER = + Metadata.Key.of("accept-language", Metadata.ASCII_STRING_MARSHALLER); + + @Override + public ServerCall.Listener interceptCall(final ServerCall call, + final Metadata headers, + final ServerCallHandler next) { + + final List locales = parseLocales(headers.get(ACCEPTABLE_LANGUAGES_GRPC_HEADER)); + + return Contexts.interceptCall( + Context.current().withValue(AcceptLanguageUtil.ACCEPTABLE_LANGUAGES_CONTEXT_KEY, locales), + call, + headers, + next); + } + + static List parseLocales(@Nullable final String acceptableLanguagesHeader) { + if (acceptableLanguagesHeader == null) { + return Collections.emptyList(); + } + try { + final List languageRanges = Locale.LanguageRange.parse(acceptableLanguagesHeader); + return Locale.filter(languageRanges, Arrays.asList(Locale.getAvailableLocales())); + } catch (final IllegalArgumentException e) { + final UserAgent userAgent = UserAgentUtil.userAgentFromGrpcContext(); + Metrics.counter(INVALID_ACCEPT_LANGUAGE_COUNTER_NAME, "platform", userAgent.getPlatform().name().toLowerCase()).increment(); + logger.debug("Could not get acceptable languages; Accept-Language: {}; User-Agent: {}", + acceptableLanguagesHeader, + userAgent, + e); + return Collections.emptyList(); + } + } +} + diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/AcceptLanguageUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/AcceptLanguageUtil.java new file mode 100644 index 000000000..a60e0c133 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/AcceptLanguageUtil.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.Context; +import java.util.List; +import java.util.Locale; + +public class AcceptLanguageUtil { + static final Context.Key> ACCEPTABLE_LANGUAGES_CONTEXT_KEY = Context.key("accept-language"); + public static List localeFromGrpcContext() { + return ACCEPTABLE_LANGUAGES_CONTEXT_KEY.get(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/AvatarChangeUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/AvatarChangeUtil.java new file mode 100644 index 000000000..fd0db7a7b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/AvatarChangeUtil.java @@ -0,0 +1,20 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.Status; +import org.whispersystems.textsecuregcm.entities.AvatarChange; + +public class AvatarChangeUtil { + public static AvatarChange fromGrpcAvatarChange(final org.signal.chat.profile.SetProfileRequest.AvatarChange avatarChangeType) { + return switch (avatarChangeType) { + case AVATAR_CHANGE_UNCHANGED -> AvatarChange.AVATAR_CHANGE_UNCHANGED; + case AVATAR_CHANGE_CLEAR -> AvatarChange.AVATAR_CHANGE_CLEAR; + case AVATAR_CHANGE_UPDATE -> AvatarChange.AVATAR_CHANGE_UPDATE; + case UNRECOGNIZED -> throw Status.INVALID_ARGUMENT.withDescription("Invalid avatar change value").asRuntimeException(); + }; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/CallingGrpcService.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/CallingGrpcService.java new file mode 100644 index 000000000..7acab7547 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/CallingGrpcService.java @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import org.signal.chat.calling.GetTurnCredentialsRequest; +import org.signal.chat.calling.GetTurnCredentialsResponse; +import org.signal.chat.calling.ReactorCallingGrpc; +import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; +import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice; +import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import reactor.core.publisher.Mono; + +public class CallingGrpcService extends ReactorCallingGrpc.CallingImplBase { + + private final TurnTokenGenerator turnTokenGenerator; + private final RateLimiters rateLimiters; + + public CallingGrpcService(final TurnTokenGenerator turnTokenGenerator, final RateLimiters rateLimiters) { + this.turnTokenGenerator = turnTokenGenerator; + this.rateLimiters = rateLimiters; + } + + @Override + public Mono getTurnCredentials(final GetTurnCredentialsRequest request) { + final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); + + return rateLimiters.getTurnLimiter().validateReactive(authenticatedDevice.accountIdentifier()) + .then(Mono.fromSupplier(() -> turnTokenGenerator.generate(authenticatedDevice.accountIdentifier()))) + .map(turnToken -> GetTurnCredentialsResponse.newBuilder() + .setUsername(turnToken.username()) + .setPassword(turnToken.password()) + .addAllUrls(turnToken.urls()) + .build()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ConvertibleToGrpcStatus.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ConvertibleToGrpcStatus.java new file mode 100644 index 000000000..ce0cc00fb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ConvertibleToGrpcStatus.java @@ -0,0 +1,20 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.Metadata; +import io.grpc.Status; +import java.util.Optional; + +/** + * Interface to be imlemented by our custom exceptions that are consistently mapped to a gRPC status. + */ +public interface ConvertibleToGrpcStatus { + + Status grpcStatus(); + + Optional grpcMetadata(); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/DevicesGrpcService.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/DevicesGrpcService.java new file mode 100644 index 000000000..aa3d0250c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/DevicesGrpcService.java @@ -0,0 +1,212 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import com.google.protobuf.ByteString; +import io.grpc.Status; +import java.util.Base64; +import java.util.Objects; +import javax.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; +import org.signal.chat.device.ClearPushTokenRequest; +import org.signal.chat.device.ClearPushTokenResponse; +import org.signal.chat.device.GetDevicesRequest; +import org.signal.chat.device.GetDevicesResponse; +import org.signal.chat.device.ReactorDevicesGrpc; +import org.signal.chat.device.RemoveDeviceRequest; +import org.signal.chat.device.RemoveDeviceResponse; +import org.signal.chat.device.SetCapabilitiesRequest; +import org.signal.chat.device.SetCapabilitiesResponse; +import org.signal.chat.device.SetDeviceNameRequest; +import org.signal.chat.device.SetDeviceNameResponse; +import org.signal.chat.device.SetPushTokenRequest; +import org.signal.chat.device.SetPushTokenResponse; +import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice; +import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import org.whispersystems.textsecuregcm.storage.MessagesManager; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase { + + private final AccountsManager accountsManager; + private final KeysManager keysManager; + private final MessagesManager messagesManager; + + private static final int MAX_NAME_LENGTH = 256; + + public DevicesGrpcService(final AccountsManager accountsManager, + final KeysManager keysManager, + final MessagesManager messagesManager) { + + this.accountsManager = accountsManager; + this.keysManager = keysManager; + this.messagesManager = messagesManager; + } + + @Override + public Mono getDevices(final GetDevicesRequest request) { + final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); + + return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier())) + .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)) + .flatMapMany(account -> Flux.fromIterable(account.getDevices())) + .reduce(GetDevicesResponse.newBuilder(), (builder, device) -> { + final GetDevicesResponse.LinkedDevice.Builder linkedDeviceBuilder = GetDevicesResponse.LinkedDevice.newBuilder(); + + if (StringUtils.isNotBlank(device.getName())) { + linkedDeviceBuilder.setName(ByteString.copyFrom(Base64.getDecoder().decode(device.getName()))); + } + + return builder.addDevices(linkedDeviceBuilder + .setId(device.getId()) + .setCreated(device.getCreated()) + .setLastSeen(device.getLastSeen()) + .build()); + }) + .map(GetDevicesResponse.Builder::build); + } + + @Override + public Mono removeDevice(final RemoveDeviceRequest request) { + if (request.getId() == Device.MASTER_ID) { + throw Status.INVALID_ARGUMENT.withDescription("Cannot remove primary device").asRuntimeException(); + } + + final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedPrimaryDevice(); + + return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier())) + .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)) + .flatMap(account -> Flux.merge( + Mono.fromFuture(() -> messagesManager.clear(account.getUuid(), request.getId())), + Mono.fromFuture(() -> keysManager.delete(account.getUuid(), request.getId()))) + .then(Mono.fromFuture(() -> accountsManager.updateAsync(account, a -> a.removeDevice(request.getId())))) + // Some messages may have arrived while we were performing the other updates; make a best effort to clear + // those out, too + .then(Mono.fromFuture(() -> messagesManager.clear(account.getUuid(), request.getId())))) + .thenReturn(RemoveDeviceResponse.newBuilder().build()); + } + + @Override + public Mono setDeviceName(final SetDeviceNameRequest request) { + final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); + + if (request.getName().isEmpty()) { + throw Status.INVALID_ARGUMENT.withDescription("Must specify a device name").asRuntimeException(); + } + + if (request.getName().size() > MAX_NAME_LENGTH) { + throw Status.INVALID_ARGUMENT.withDescription("Device name must be at most " + MAX_NAME_LENGTH + " bytes") + .asRuntimeException(); + } + + return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier())) + .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)) + .flatMap(account -> Mono.fromFuture(() -> accountsManager.updateDeviceAsync(account, authenticatedDevice.deviceId(), + device -> device.setName(Base64.getEncoder().encodeToString(request.getName().toByteArray()))))) + .thenReturn(SetDeviceNameResponse.newBuilder().build()); + } + + @Override + public Mono setPushToken(final SetPushTokenRequest request) { + final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); + + @Nullable final String apnsToken; + @Nullable final String apnsVoipToken; + @Nullable final String fcmToken; + + switch (request.getTokenRequestCase()) { + + case APNS_TOKEN_REQUEST -> { + final SetPushTokenRequest.ApnsTokenRequest apnsTokenRequest = request.getApnsTokenRequest(); + + if (StringUtils.isAllBlank(apnsTokenRequest.getApnsToken(), apnsTokenRequest.getApnsVoipToken())) { + throw Status.INVALID_ARGUMENT.withDescription("APNs tokens may not both be blank").asRuntimeException(); + } + + apnsToken = StringUtils.stripToNull(apnsTokenRequest.getApnsToken()); + apnsVoipToken = StringUtils.stripToNull(apnsTokenRequest.getApnsVoipToken()); + fcmToken = null; + } + + case FCM_TOKEN_REQUEST -> { + final SetPushTokenRequest.FcmTokenRequest fcmTokenRequest = request.getFcmTokenRequest(); + + if (StringUtils.isBlank(fcmTokenRequest.getFcmToken())) { + throw Status.INVALID_ARGUMENT.withDescription("FCM token must not be blank").asRuntimeException(); + } + + apnsToken = null; + apnsVoipToken = null; + fcmToken = StringUtils.stripToNull(fcmTokenRequest.getFcmToken()); + } + + default -> throw Status.INVALID_ARGUMENT.withDescription("No tokens specified").asRuntimeException(); + } + + return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier())) + .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)) + .flatMap(account -> { + final Device device = account.getDevice(authenticatedDevice.deviceId()) + .orElseThrow(Status.UNAUTHENTICATED::asRuntimeException); + + final boolean tokenUnchanged = + Objects.equals(device.getApnId(), apnsToken) && + Objects.equals(device.getVoipApnId(), apnsVoipToken) && + Objects.equals(device.getGcmId(), fcmToken); + + return tokenUnchanged + ? Mono.empty() + : Mono.fromFuture(() -> accountsManager.updateDeviceAsync(account, authenticatedDevice.deviceId(), d -> { + d.setApnId(apnsToken); + d.setVoipApnId(apnsVoipToken); + d.setGcmId(fcmToken); + d.setFetchesMessages(false); + })); + }) + .thenReturn(SetPushTokenResponse.newBuilder().build()); + } + + @Override + public Mono clearPushToken(final ClearPushTokenRequest request) { + final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); + + return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier())) + .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)) + .flatMap(account -> Mono.fromFuture(() -> accountsManager.updateDeviceAsync(account, authenticatedDevice.deviceId(), device -> { + if (StringUtils.isNotBlank(device.getApnId()) || StringUtils.isNotBlank(device.getVoipApnId())) { + device.setUserAgent(device.isMaster() ? "OWI" : "OWP"); + } else if (StringUtils.isNotBlank(device.getGcmId())) { + device.setUserAgent("OWA"); + } + + device.setApnId(null); + device.setVoipApnId(null); + device.setGcmId(null); + device.setFetchesMessages(true); + }))) + .thenReturn(ClearPushTokenResponse.newBuilder().build()); + } + + @Override + public Mono setCapabilities(final SetCapabilitiesRequest request) { + final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); + + return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier())) + .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)) + .flatMap(account -> + Mono.fromFuture(() -> accountsManager.updateDeviceAsync(account, authenticatedDevice.deviceId(), + d -> d.setCapabilities(new Device.DeviceCapabilities( + request.getStorage(), + request.getTransfer(), + request.getPni(), + request.getPaymentActivation()))))) + .thenReturn(SetCapabilitiesResponse.newBuilder().build()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ErrorMappingInterceptor.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ErrorMappingInterceptor.java new file mode 100644 index 000000000..75d25d498 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ErrorMappingInterceptor.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.ForwardingServerCall; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; + +/** + * This interceptor observes responses from the service and if the response status is {@link Status#UNKNOWN} + * and there is a non-null cause which is an instance of {@link ConvertibleToGrpcStatus}, + * then status and metadata to be returned to the client is resolved from that object. + *

    + * This eliminates the need of having each service to override {@code `onErrorMap()`} method for commonly used exceptions. + */ +public class ErrorMappingInterceptor implements ServerInterceptor { + + @Override + public ServerCall.Listener interceptCall( + final ServerCall call, + final Metadata headers, + final ServerCallHandler next) { + return next.startCall(new ForwardingServerCall.SimpleForwardingServerCall<>(call) { + @Override + public void close(final Status status, final Metadata trailers) { + // The idea is to only apply the automatic conversion logic in the cases + // when there was no explicit decision by the service to provide a status. + // I.e. if at this point we see anything but the `UNKNOWN`, + // that means that some logic in the service made this decision already + // and automatic conversion may conflict with it. + if (status.getCode().equals(Status.Code.UNKNOWN) + && status.getCause() instanceof ConvertibleToGrpcStatus convertibleToGrpcStatus) { + super.close( + convertibleToGrpcStatus.grpcStatus(), + convertibleToGrpcStatus.grpcMetadata().orElseGet(Metadata::new) + ); + } else { + super.close(status, trailers); + } + } + }, headers); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceCredentialsAnonymousGrpcService.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceCredentialsAnonymousGrpcService.java new file mode 100644 index 000000000..f76bf3d02 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceCredentialsAnonymousGrpcService.java @@ -0,0 +1,86 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static java.util.Objects.requireNonNull; + +import com.google.common.annotations.VisibleForTesting; +import java.time.Clock; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import org.signal.chat.credentials.AuthCheckResult; +import org.signal.chat.credentials.CheckSvrCredentialsRequest; +import org.signal.chat.credentials.CheckSvrCredentialsResponse; +import org.signal.chat.credentials.ReactorExternalServiceCredentialsAnonymousGrpc; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsSelector; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class ExternalServiceCredentialsAnonymousGrpcService extends + ReactorExternalServiceCredentialsAnonymousGrpc.ExternalServiceCredentialsAnonymousImplBase { + + private static final long MAX_SVR_PASSWORD_AGE_SECONDS = TimeUnit.DAYS.toSeconds(30); + + private final ExternalServiceCredentialsGenerator svrCredentialsGenerator; + + private final AccountsManager accountsManager; + + + public static ExternalServiceCredentialsAnonymousGrpcService create( + final AccountsManager accountsManager, + final WhisperServerConfiguration chatConfiguration) { + return new ExternalServiceCredentialsAnonymousGrpcService( + accountsManager, + ExternalServiceDefinitions.SVR.generatorFactory().apply(chatConfiguration, Clock.systemUTC()) + ); + } + + @VisibleForTesting + ExternalServiceCredentialsAnonymousGrpcService( + final AccountsManager accountsManager, + final ExternalServiceCredentialsGenerator svrCredentialsGenerator) { + this.accountsManager = requireNonNull(accountsManager); + this.svrCredentialsGenerator = requireNonNull(svrCredentialsGenerator); + } + + @Override + public Mono checkSvrCredentials(final CheckSvrCredentialsRequest request) { + final List tokens = request.getPasswordsList(); + final List credentials = ExternalServiceCredentialsSelector.check( + tokens, + svrCredentialsGenerator, + MAX_SVR_PASSWORD_AGE_SECONDS); + + // the username associated with the provided number + final Optional matchingUsername = accountsManager + .getByE164(request.getNumber()) + .map(Account::getUuid) + .map(svrCredentialsGenerator::generateForUuid) + .map(ExternalServiceCredentials::username); + + return Flux.fromIterable(credentials) + .reduce(CheckSvrCredentialsResponse.newBuilder(), ((builder, credentialInfo) -> { + final AuthCheckResult authCheckResult; + if (!credentialInfo.valid()) { + authCheckResult = AuthCheckResult.AUTH_CHECK_RESULT_INVALID; + } else { + final String username = credentialInfo.credentials().username(); + // does this credential match the account id for the e164 provided in the request? + authCheckResult = matchingUsername.map(username::equals).orElse(false) + ? AuthCheckResult.AUTH_CHECK_RESULT_MATCH + : AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH; + } + return builder.putMatches(credentialInfo.token(), authCheckResult); + })) + .map(CheckSvrCredentialsResponse.Builder::build); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceCredentialsGrpcService.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceCredentialsGrpcService.java new file mode 100644 index 000000000..df54391e9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceCredentialsGrpcService.java @@ -0,0 +1,68 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static java.util.Objects.requireNonNull; + +import com.google.common.annotations.VisibleForTesting; +import io.grpc.Status; +import java.time.Clock; +import java.util.Map; +import org.signal.chat.credentials.ExternalServiceType; +import org.signal.chat.credentials.GetExternalServiceCredentialsRequest; +import org.signal.chat.credentials.GetExternalServiceCredentialsResponse; +import org.signal.chat.credentials.ReactorExternalServiceCredentialsGrpc; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice; +import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import reactor.core.publisher.Mono; + +public class ExternalServiceCredentialsGrpcService extends ReactorExternalServiceCredentialsGrpc.ExternalServiceCredentialsImplBase { + + private final Map credentialsGeneratorByType; + + private final RateLimiters rateLimiters; + + + public static ExternalServiceCredentialsGrpcService createForAllExternalServices( + final WhisperServerConfiguration chatConfiguration, + final RateLimiters rateLimiters) { + return new ExternalServiceCredentialsGrpcService( + ExternalServiceDefinitions.createExternalServiceList(chatConfiguration, Clock.systemUTC()), + rateLimiters + ); + } + + @VisibleForTesting + ExternalServiceCredentialsGrpcService( + final Map credentialsGeneratorByType, + final RateLimiters rateLimiters) { + this.credentialsGeneratorByType = requireNonNull(credentialsGeneratorByType); + this.rateLimiters = requireNonNull(rateLimiters); + } + + @Override + public Mono getExternalServiceCredentials(final GetExternalServiceCredentialsRequest request) { + final ExternalServiceCredentialsGenerator credentialsGenerator = this.credentialsGeneratorByType + .get(request.getExternalService()); + if (credentialsGenerator == null) { + return Mono.error(Status.INVALID_ARGUMENT.asException()); + } + final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); + return rateLimiters.forDescriptor(RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS).validateReactive(authenticatedDevice.accountIdentifier()) + .then(Mono.fromSupplier(() -> { + final ExternalServiceCredentials externalServiceCredentials = credentialsGenerator + .generateForUuid(authenticatedDevice.accountIdentifier()); + return GetExternalServiceCredentialsResponse.newBuilder() + .setUsername(externalServiceCredentials.username()) + .setPassword(externalServiceCredentials.password()) + .build(); + })); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceDefinitions.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceDefinitions.java new file mode 100644 index 000000000..dcf285a3a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceDefinitions.java @@ -0,0 +1,95 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static java.util.Objects.requireNonNull; + +import java.time.Clock; +import java.util.Arrays; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import org.apache.commons.lang3.tuple.Pair; +import org.signal.chat.credentials.ExternalServiceType; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration; +import org.whispersystems.textsecuregcm.configuration.DirectoryV2ClientConfiguration; +import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration; +import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration; + +enum ExternalServiceDefinitions { + ART(ExternalServiceType.EXTERNAL_SERVICE_TYPE_ART, (chatConfig, clock) -> { + final ArtServiceConfiguration cfg = chatConfig.getArtServiceConfiguration(); + return ExternalServiceCredentialsGenerator + .builder(cfg.userAuthenticationTokenSharedSecret()) + .withUserDerivationKey(cfg.userAuthenticationTokenUserIdSecret()) + .prependUsername(false) + .truncateSignature(false) + .build(); + }), + DIRECTORY(ExternalServiceType.EXTERNAL_SERVICE_TYPE_DIRECTORY, (chatConfig, clock) -> { + final DirectoryV2ClientConfiguration cfg = chatConfig.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration(); + return ExternalServiceCredentialsGenerator + .builder(cfg.userAuthenticationTokenSharedSecret()) + .withUserDerivationKey(cfg.userIdTokenSharedSecret()) + .prependUsername(false) + .withClock(clock) + .build(); + }), + PAYMENTS(ExternalServiceType.EXTERNAL_SERVICE_TYPE_PAYMENTS, (chatConfig, clock) -> { + final PaymentsServiceConfiguration cfg = chatConfig.getPaymentsServiceConfiguration(); + return ExternalServiceCredentialsGenerator + .builder(cfg.userAuthenticationTokenSharedSecret()) + .prependUsername(true) + .build(); + }), + SVR(ExternalServiceType.EXTERNAL_SERVICE_TYPE_SVR, (chatConfig, clock) -> { + final SecureValueRecovery2Configuration cfg = chatConfig.getSvr2Configuration(); + return ExternalServiceCredentialsGenerator + .builder(cfg.userAuthenticationTokenSharedSecret()) + .withUserDerivationKey(cfg.userIdTokenSharedSecret().value()) + .prependUsername(false) + .withDerivedUsernameTruncateLength(16) + .withClock(clock) + .build(); + }), + STORAGE(ExternalServiceType.EXTERNAL_SERVICE_TYPE_STORAGE, (chatConfig, clock) -> { + final PaymentsServiceConfiguration cfg = chatConfig.getPaymentsServiceConfiguration(); + return ExternalServiceCredentialsGenerator + .builder(cfg.userAuthenticationTokenSharedSecret()) + .prependUsername(true) + .build(); + }), + ; + + private final ExternalServiceType externalService; + + private final BiFunction generatorFactory; + + ExternalServiceDefinitions( + final ExternalServiceType externalService, + final BiFunction factory) { + this.externalService = requireNonNull(externalService); + this.generatorFactory = requireNonNull(factory); + } + + public static Map createExternalServiceList( + final WhisperServerConfiguration chatConfiguration, + final Clock clock) { + return Arrays.stream(values()) + .map(esd -> Pair.of(esd.externalService, esd.generatorFactory().apply(chatConfiguration, clock))) + .collect(Collectors.toMap(Pair::getKey, Pair::getValue)); + } + + public BiFunction generatorFactory() { + return generatorFactory; + } + + ExternalServiceType externalService() { + return externalService; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/GrpcServerManagedWrapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/GrpcServerManagedWrapper.java new file mode 100644 index 000000000..1b88c290d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/GrpcServerManagedWrapper.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import io.dropwizard.lifecycle.Managed; +import io.grpc.Server; + +public class GrpcServerManagedWrapper implements Managed { + + private final Server server; + + public GrpcServerManagedWrapper(final Server server) { + this.server = server; + } + + @Override + public void start() throws IOException { + server.start(); + } + + @Override + public void stop() { + try { + server.shutdown().awaitTermination(5, TimeUnit.MINUTES); + } catch (InterruptedException e) { + server.shutdownNow(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/IdentityTypeUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/IdentityTypeUtil.java new file mode 100644 index 000000000..a94e7e25f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/IdentityTypeUtil.java @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.Status; +import org.whispersystems.textsecuregcm.identity.IdentityType; + +public class IdentityTypeUtil { + + private IdentityTypeUtil() { + } + + public static IdentityType fromGrpcIdentityType(final org.signal.chat.common.IdentityType grpcIdentityType) { + return switch (grpcIdentityType) { + case IDENTITY_TYPE_ACI -> IdentityType.ACI; + case IDENTITY_TYPE_PNI -> IdentityType.PNI; + case IDENTITY_TYPE_UNSPECIFIED, UNRECOGNIZED -> throw Status.INVALID_ARGUMENT.asRuntimeException(); + }; + } + + public static org.signal.chat.common.IdentityType toGrpcIdentityType(final IdentityType identityType) { + return switch (identityType) { + case ACI -> org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI; + case PNI -> org.signal.chat.common.IdentityType.IDENTITY_TYPE_PNI; + }; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcService.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcService.java new file mode 100644 index 000000000..48a4e3e21 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcService.java @@ -0,0 +1,81 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import com.google.protobuf.ByteString; +import io.grpc.Status; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import org.signal.chat.keys.CheckIdentityKeyRequest; +import org.signal.chat.keys.CheckIdentityKeyResponse; +import org.signal.chat.keys.GetPreKeysAnonymousRequest; +import org.signal.chat.keys.GetPreKeysResponse; +import org.signal.chat.keys.ReactorKeysAnonymousGrpc; +import org.signal.libsignal.protocol.IdentityKey; +import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuples; + +public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnonymousImplBase { + + private final AccountsManager accountsManager; + private final KeysManager keysManager; + + public KeysAnonymousGrpcService(final AccountsManager accountsManager, final KeysManager keysManager) { + this.accountsManager = accountsManager; + this.keysManager = keysManager; + } + + @Override + public Mono getPreKeys(final GetPreKeysAnonymousRequest request) { + final ServiceIdentifier serviceIdentifier = + ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getTargetIdentifier()); + + return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(serviceIdentifier)) + .flatMap(Mono::justOrEmpty) + .switchIfEmpty(Mono.error(Status.UNAUTHENTICATED.asException())) + .flatMap(targetAccount -> + UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, request.getUnidentifiedAccessKey().toByteArray()) + ? KeysGrpcHelper.getPreKeys(targetAccount, serviceIdentifier.identityType(), request.getRequest().getDeviceId(), keysManager) + : Mono.error(Status.UNAUTHENTICATED.asException())); + } + + @Override + public Flux checkIdentityKeys(final Flux requests) { + return requests + .map(request -> Tuples.of(ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getTargetIdentifier()), + request.getFingerprint().toByteArray())) + .flatMap(serviceIdentifierAndFingerprint -> Mono.fromFuture( + () -> accountsManager.getByServiceIdentifierAsync(serviceIdentifierAndFingerprint.getT1())) + .flatMap(Mono::justOrEmpty) + .filter(account -> !fingerprintMatches(account.getIdentityKey(serviceIdentifierAndFingerprint.getT1() + .identityType()), serviceIdentifierAndFingerprint.getT2())) + .map(account -> CheckIdentityKeyResponse.newBuilder() + .setTargetIdentifier( + ServiceIdentifierUtil.toGrpcServiceIdentifier(serviceIdentifierAndFingerprint.getT1())) + .setIdentityKey(ByteString.copyFrom(account.getIdentityKey(serviceIdentifierAndFingerprint.getT1() + .identityType()).serialize())) + .build()) + ); + } + + private static boolean fingerprintMatches(final IdentityKey identityKey, final byte[] fingerprint) { + final byte[] digest; + try { + digest = MessageDigest.getInstance("SHA-256").digest(identityKey.serialize()); + } catch (NoSuchAlgorithmException e) { + // SHA-256 should always be supported as an algorithm + throw new AssertionError("All Java implementations must support the SHA-256 message digest"); + } + + return Arrays.equals(digest, 0, 4, fingerprint, 0, 4); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcHelper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcHelper.java new file mode 100644 index 000000000..f0bc21a22 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcHelper.java @@ -0,0 +1,84 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.ByteString; +import io.grpc.Status; +import org.signal.chat.common.EcPreKey; +import org.signal.chat.common.EcSignedPreKey; +import org.signal.chat.common.KemSignedPreKey; +import org.signal.chat.keys.GetPreKeysResponse; +import org.whispersystems.textsecuregcm.entities.ECPreKey; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +class KeysGrpcHelper { + + @VisibleForTesting + static final long ALL_DEVICES = 0; + + static Mono getPreKeys(final Account targetAccount, + final IdentityType identityType, + final long targetDeviceId, + final KeysManager keysManager) { + + final Flux devices = targetDeviceId == ALL_DEVICES + ? Flux.fromIterable(targetAccount.getDevices()) + : Flux.from(Mono.justOrEmpty(targetAccount.getDevice(targetDeviceId))); + + return devices + .filter(Device::isEnabled) + .switchIfEmpty(Mono.error(Status.NOT_FOUND.asException())) + .flatMap(device -> { + final ECSignedPreKey ecSignedPreKey = device.getSignedPreKey(identityType); + + final GetPreKeysResponse.PreKeyBundle.Builder preKeyBundleBuilder = GetPreKeysResponse.PreKeyBundle.newBuilder() + .setEcSignedPreKey(EcSignedPreKey.newBuilder() + .setKeyId(ecSignedPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(ecSignedPreKey.serializedPublicKey())) + .setSignature(ByteString.copyFrom(ecSignedPreKey.signature())) + .build()); + + return Flux.merge( + Mono.fromFuture(() -> keysManager.takeEC(targetAccount.getIdentifier(identityType), device.getId())), + Mono.fromFuture(() -> keysManager.takePQ(targetAccount.getIdentifier(identityType), device.getId()))) + .flatMap(Mono::justOrEmpty) + .reduce(preKeyBundleBuilder, (builder, preKey) -> { + if (preKey instanceof ECPreKey ecPreKey) { + builder.setEcOneTimePreKey(EcPreKey.newBuilder() + .setKeyId(ecPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(ecPreKey.serializedPublicKey())) + .build()); + } else if (preKey instanceof KEMSignedPreKey kemSignedPreKey) { + preKeyBundleBuilder.setKemOneTimePreKey(KemSignedPreKey.newBuilder() + .setKeyId(kemSignedPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(kemSignedPreKey.serializedPublicKey())) + .setSignature(ByteString.copyFrom(kemSignedPreKey.signature())) + .build()); + } else { + throw new AssertionError("Unexpected pre-key type: " + preKey.getClass()); + } + + return builder; + }) + .map(builder -> Tuples.of(device.getId(), builder.build())); + }) + .collectMap(Tuple2::getT1, Tuple2::getT2) + .map(preKeyBundles -> GetPreKeysResponse.newBuilder() + .setIdentityKey(ByteString.copyFrom(targetAccount.getIdentityKey(identityType).serialize())) + .putAllPreKeys(preKeyBundles) + .build()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcService.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcService.java new file mode 100644 index 000000000..6d3c3ccf9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcService.java @@ -0,0 +1,279 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import org.signal.chat.common.EcPreKey; +import org.signal.chat.common.EcSignedPreKey; +import org.signal.chat.common.KemSignedPreKey; +import org.signal.chat.keys.GetPreKeyCountRequest; +import org.signal.chat.keys.GetPreKeyCountResponse; +import org.signal.chat.keys.GetPreKeysRequest; +import org.signal.chat.keys.GetPreKeysResponse; +import org.signal.chat.keys.ReactorKeysGrpc; +import org.signal.chat.keys.SetEcSignedPreKeyRequest; +import org.signal.chat.keys.SetKemLastResortPreKeyRequest; +import org.signal.chat.keys.SetOneTimeEcPreKeysRequest; +import org.signal.chat.keys.SetOneTimeKemSignedPreKeysRequest; +import org.signal.chat.keys.SetPreKeyResponse; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.signal.libsignal.protocol.kem.KEMPublicKey; +import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice; +import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil; +import org.whispersystems.textsecuregcm.entities.ECPreKey; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuples; + +public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { + + private final AccountsManager accountsManager; + private final KeysManager keysManager; + private final RateLimiters rateLimiters; + + private static final StatusRuntimeException INVALID_PUBLIC_KEY_EXCEPTION = Status.fromCode(Status.Code.INVALID_ARGUMENT) + .withDescription("Invalid public key") + .asRuntimeException(); + + private static final StatusRuntimeException INVALID_SIGNATURE_EXCEPTION = Status.fromCode(Status.Code.INVALID_ARGUMENT) + .withDescription("Invalid signature") + .asRuntimeException(); + + private enum PreKeyType { + EC, + KEM + } + + public KeysGrpcService(final AccountsManager accountsManager, + final KeysManager keysManager, + final RateLimiters rateLimiters) { + + this.accountsManager = accountsManager; + this.keysManager = keysManager; + this.rateLimiters = rateLimiters; + } + + @Override + public Mono getPreKeyCount(final GetPreKeyCountRequest request) { + return Mono.fromSupplier(AuthenticationUtil::requireAuthenticatedDevice) + .flatMap(authenticatedDevice -> Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier())) + .map(maybeAccount -> maybeAccount + .map(account -> Tuples.of(account, authenticatedDevice.deviceId())) + .orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))) + .flatMapMany(accountAndDeviceId -> Flux.just( + Tuples.of(IdentityType.ACI, accountAndDeviceId.getT1().getUuid(), accountAndDeviceId.getT2()), + Tuples.of(IdentityType.PNI, accountAndDeviceId.getT1().getPhoneNumberIdentifier(), accountAndDeviceId.getT2()) + )) + .flatMap(identityTypeUuidAndDeviceId -> Flux.merge( + Mono.fromFuture(() -> keysManager.getEcCount(identityTypeUuidAndDeviceId.getT2(), identityTypeUuidAndDeviceId.getT3())) + .map(ecKeyCount -> Tuples.of(identityTypeUuidAndDeviceId.getT1(), PreKeyType.EC, ecKeyCount)), + + Mono.fromFuture(() -> keysManager.getPqCount(identityTypeUuidAndDeviceId.getT2(), identityTypeUuidAndDeviceId.getT3())) + .map(ecKeyCount -> Tuples.of(identityTypeUuidAndDeviceId.getT1(), PreKeyType.KEM, ecKeyCount)) + )) + .reduce(GetPreKeyCountResponse.newBuilder(), (builder, tuple) -> { + final IdentityType identityType = tuple.getT1(); + final PreKeyType preKeyType = tuple.getT2(); + final int count = tuple.getT3(); + + switch (identityType) { + case ACI -> { + switch (preKeyType) { + case EC -> builder.setAciEcPreKeyCount(count); + case KEM -> builder.setAciKemPreKeyCount(count); + } + } + case PNI -> { + switch (preKeyType) { + case EC -> builder.setPniEcPreKeyCount(count); + case KEM -> builder.setPniKemPreKeyCount(count); + } + } + } + + return builder; + }) + .map(GetPreKeyCountResponse.Builder::build); + } + + @Override + public Mono getPreKeys(final GetPreKeysRequest request) { + final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); + + final ServiceIdentifier targetIdentifier = + ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getTargetIdentifier()); + + final String rateLimitKey = authenticatedDevice.accountIdentifier() + "." + + authenticatedDevice.deviceId() + "__" + + targetIdentifier.uuid() + "." + + request.getDeviceId(); + + return rateLimiters.getPreKeysLimiter().validateReactive(rateLimitKey) + .then(Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier)) + .flatMap(Mono::justOrEmpty)) + .switchIfEmpty(Mono.error(Status.NOT_FOUND.asException())) + .flatMap(targetAccount -> + KeysGrpcHelper.getPreKeys(targetAccount, targetIdentifier.identityType(), request.getDeviceId(), keysManager)); + } + + @Override + public Mono setOneTimeEcPreKeys(final SetOneTimeEcPreKeysRequest request) { + return Mono.fromSupplier(AuthenticationUtil::requireAuthenticatedDevice) + .flatMap(authenticatedDevice -> storeOneTimePreKeys(authenticatedDevice.accountIdentifier(), + request.getPreKeysList(), + IdentityTypeUtil.fromGrpcIdentityType(request.getIdentityType()), + (requestPreKey, ignored) -> checkEcPreKey(requestPreKey), + (identifier, preKeys) -> keysManager.storeEcOneTimePreKeys(identifier, authenticatedDevice.deviceId(), preKeys))); + } + + @Override + public Mono setOneTimeKemSignedPreKeys(final SetOneTimeKemSignedPreKeysRequest request) { + return Mono.fromSupplier(AuthenticationUtil::requireAuthenticatedDevice) + .flatMap(authenticatedDevice -> storeOneTimePreKeys(authenticatedDevice.accountIdentifier(), + request.getPreKeysList(), + IdentityTypeUtil.fromGrpcIdentityType(request.getIdentityType()), + KeysGrpcService::checkKemSignedPreKey, + (identifier, preKeys) -> keysManager.storeKemOneTimePreKeys(identifier, authenticatedDevice.deviceId(), preKeys))); + } + + private Mono storeOneTimePreKeys(final UUID authenticatedAccountUuid, + final List requestPreKeys, + final IdentityType identityType, + final BiFunction extractPreKeyFunction, + final BiFunction, CompletableFuture> storeKeysFunction) { + + return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedAccountUuid)) + .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)) + .map(account -> { + final List preKeys = requestPreKeys.stream() + .map(requestPreKey -> extractPreKeyFunction.apply(requestPreKey, account.getIdentityKey(identityType))) + .toList(); + + if (preKeys.isEmpty()) { + throw Status.INVALID_ARGUMENT.asRuntimeException(); + } + + return Tuples.of(account.getIdentifier(identityType), preKeys); + }) + .flatMap(identifierAndPreKeys -> Mono.fromFuture(() -> storeKeysFunction.apply(identifierAndPreKeys.getT1(), identifierAndPreKeys.getT2()))) + .thenReturn(SetPreKeyResponse.newBuilder().build()); + } + + @Override + public Mono setEcSignedPreKey(final SetEcSignedPreKeyRequest request) { + return Mono.fromSupplier(AuthenticationUtil::requireAuthenticatedDevice) + .flatMap(authenticatedDevice -> storeRepeatedUseKey(authenticatedDevice.accountIdentifier(), + request.getIdentityType(), + request.getSignedPreKey(), + KeysGrpcService::checkEcSignedPreKey, + (account, signedPreKey) -> { + final IdentityType identityType = IdentityTypeUtil.fromGrpcIdentityType(request.getIdentityType()); + + final Consumer deviceUpdater = switch (identityType) { + case ACI -> device -> device.setSignedPreKey(signedPreKey); + case PNI -> device -> device.setPhoneNumberIdentitySignedPreKey(signedPreKey); + }; + + final UUID identifier = account.getIdentifier(identityType); + + return Flux.merge( + Mono.fromFuture(() -> keysManager.storeEcSignedPreKeys(identifier, Map.of(authenticatedDevice.deviceId(), signedPreKey))), + Mono.fromFuture(() -> accountsManager.updateDeviceAsync(account, authenticatedDevice.deviceId(), deviceUpdater))) + .then(); + })); + } + + @Override + public Mono setKemLastResortPreKey(final SetKemLastResortPreKeyRequest request) { + return Mono.fromSupplier(AuthenticationUtil::requireAuthenticatedDevice) + .flatMap(authenticatedDevice -> storeRepeatedUseKey(authenticatedDevice.accountIdentifier(), + request.getIdentityType(), + request.getSignedPreKey(), + KeysGrpcService::checkKemSignedPreKey, + (account, lastResortKey) -> { + final UUID identifier = + account.getIdentifier(IdentityTypeUtil.fromGrpcIdentityType(request.getIdentityType())); + + return Mono.fromFuture(() -> keysManager.storePqLastResort(identifier, Map.of(authenticatedDevice.deviceId(), lastResortKey))); + })); + } + + private Mono storeRepeatedUseKey(final UUID authenticatedAccountUuid, + final org.signal.chat.common.IdentityType identityType, + final R storeKeyRequest, + final BiFunction extractKeyFunction, + final BiFunction> storeKeyFunction) { + + return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedAccountUuid)) + .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)) + .map(account -> { + final IdentityKey identityKey = account.getIdentityKey(IdentityTypeUtil.fromGrpcIdentityType(identityType)); + final K key = extractKeyFunction.apply(storeKeyRequest, identityKey); + + return Tuples.of(account, key); + }) + .flatMap(accountAndKey -> storeKeyFunction.apply(accountAndKey.getT1(), accountAndKey.getT2())) + .thenReturn(SetPreKeyResponse.newBuilder().build()); + } + + private static ECPreKey checkEcPreKey(final EcPreKey preKey) { + try { + return new ECPreKey(preKey.getKeyId(), new ECPublicKey(preKey.getPublicKey().toByteArray())); + } catch (final InvalidKeyException e) { + throw INVALID_PUBLIC_KEY_EXCEPTION; + } + } + + private static ECSignedPreKey checkEcSignedPreKey(final EcSignedPreKey preKey, final IdentityKey identityKey) { + try { + final ECSignedPreKey ecSignedPreKey = new ECSignedPreKey(preKey.getKeyId(), + new ECPublicKey(preKey.getPublicKey().toByteArray()), + preKey.getSignature().toByteArray()); + + if (ecSignedPreKey.signatureValid(identityKey)) { + return ecSignedPreKey; + } else { + throw INVALID_SIGNATURE_EXCEPTION; + } + } catch (final InvalidKeyException e) { + throw INVALID_PUBLIC_KEY_EXCEPTION; + } + } + + private static KEMSignedPreKey checkKemSignedPreKey(final KemSignedPreKey preKey, final IdentityKey identityKey) { + try { + final KEMSignedPreKey kemSignedPreKey = new KEMSignedPreKey(preKey.getKeyId(), + new KEMPublicKey(preKey.getPublicKey().toByteArray()), + preKey.getSignature().toByteArray()); + + if (kemSignedPreKey.signatureValid(identityKey)) { + return kemSignedPreKey; + } else { + throw INVALID_SIGNATURE_EXCEPTION; + } + } catch (final InvalidKeyException e) { + throw INVALID_PUBLIC_KEY_EXCEPTION; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/PaymentsGrpcService.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/PaymentsGrpcService.java new file mode 100644 index 000000000..845ca4fb4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/PaymentsGrpcService.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static java.util.Objects.requireNonNull; + +import io.grpc.Status; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.apache.commons.lang3.tuple.Pair; +import org.signal.chat.payments.GetCurrencyConversionsRequest; +import org.signal.chat.payments.GetCurrencyConversionsResponse; +import org.signal.chat.payments.ReactorPaymentsGrpc; +import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil; +import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager; +import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntityList; +import reactor.core.publisher.Mono; + +public class PaymentsGrpcService extends ReactorPaymentsGrpc.PaymentsImplBase { + + private final CurrencyConversionManager currencyManager; + + + public PaymentsGrpcService(final CurrencyConversionManager currencyManager) { + this.currencyManager = requireNonNull(currencyManager); + } + + @Override + public Mono getCurrencyConversions(final GetCurrencyConversionsRequest request) { + AuthenticationUtil.requireAuthenticatedDevice(); + + final CurrencyConversionEntityList currencyConversionEntityList = currencyManager + .getCurrencyConversions() + .orElseThrow(Status.UNAVAILABLE::asRuntimeException); + + final List currencyConversionEntities = currencyConversionEntityList + .getCurrencies() + .stream() + .map(cce -> GetCurrencyConversionsResponse.CurrencyConversionEntity.newBuilder() + .setBase(cce.getBase()) + .putAllConversions(transformBigDecimalsToStrings(cce.getConversions())) + .build()) + .toList(); + + return Mono.just(GetCurrencyConversionsResponse.newBuilder() + .addAllCurrencies(currencyConversionEntities).setTimestamp(currencyConversionEntityList.getTimestamp()) + .build()); + } + + @Nonnull + private static Map transformBigDecimalsToStrings(final Map conversions) { + AuthenticationUtil.requireAuthenticatedDevice(); + return conversions.entrySet().stream() + .map(e -> Pair.of(e.getKey(), e.getValue().toString())) + .collect(Collectors.toMap(Pair::getKey, Pair::getValue)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcService.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcService.java new file mode 100644 index 000000000..a8f89a257 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcService.java @@ -0,0 +1,97 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.Status; +import org.signal.chat.profile.CredentialType; +import org.signal.chat.profile.GetExpiringProfileKeyCredentialAnonymousRequest; +import org.signal.chat.profile.GetExpiringProfileKeyCredentialResponse; +import org.signal.chat.profile.GetUnversionedProfileAnonymousRequest; +import org.signal.chat.profile.GetUnversionedProfileResponse; +import org.signal.chat.profile.GetVersionedProfileAnonymousRequest; +import org.signal.chat.profile.GetVersionedProfileResponse; +import org.signal.chat.profile.ReactorProfileAnonymousGrpc; +import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; +import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; +import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.ProfilesManager; +import reactor.core.publisher.Mono; + +public class ProfileAnonymousGrpcService extends ReactorProfileAnonymousGrpc.ProfileAnonymousImplBase { + private final AccountsManager accountsManager; + private final ProfilesManager profilesManager; + private final ProfileBadgeConverter profileBadgeConverter; + private final ServerZkProfileOperations zkProfileOperations; + + public ProfileAnonymousGrpcService( + final AccountsManager accountsManager, + final ProfilesManager profilesManager, + final ProfileBadgeConverter profileBadgeConverter, + final ServerZkProfileOperations zkProfileOperations) { + this.accountsManager = accountsManager; + this.profilesManager = profilesManager; + this.profileBadgeConverter = profileBadgeConverter; + this.zkProfileOperations = zkProfileOperations; + } + + @Override + public Mono getUnversionedProfile(final GetUnversionedProfileAnonymousRequest request) { + final ServiceIdentifier targetIdentifier = + ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getServiceIdentifier()); + + // Callers must be authenticated to request unversioned profiles by PNI + if (targetIdentifier.identityType() == IdentityType.PNI) { + throw Status.UNAUTHENTICATED.asRuntimeException(); + } + + return getTargetAccountAndValidateUnidentifiedAccess(targetIdentifier, request.getUnidentifiedAccessKey().toByteArray()) + .map(targetAccount -> ProfileGrpcHelper.buildUnversionedProfileResponse(targetIdentifier, + null, + targetAccount, + profileBadgeConverter)); + } + + @Override + public Mono getVersionedProfile(final GetVersionedProfileAnonymousRequest request) { + final ServiceIdentifier targetIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getAccountIdentifier()); + + if (targetIdentifier.identityType() != IdentityType.ACI) { + throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asRuntimeException(); + } + + return getTargetAccountAndValidateUnidentifiedAccess(targetIdentifier, request.getUnidentifiedAccessKey().toByteArray()) + .flatMap(targetAccount -> ProfileGrpcHelper.getVersionedProfile(targetAccount, profilesManager, request.getRequest().getVersion())); + } + + @Override + public Mono getExpiringProfileKeyCredential( + final GetExpiringProfileKeyCredentialAnonymousRequest request) { + final ServiceIdentifier targetIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getAccountIdentifier()); + + if (targetIdentifier.identityType() != IdentityType.ACI) { + throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asRuntimeException(); + } + + if (request.getRequest().getCredentialType() != CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY) { + throw Status.INVALID_ARGUMENT.withDescription("Expected expiring profile key credential type").asRuntimeException(); + } + + return getTargetAccountAndValidateUnidentifiedAccess(targetIdentifier, request.getUnidentifiedAccessKey().toByteArray()) + .flatMap(account -> ProfileGrpcHelper.getExpiringProfileKeyCredentialResponse(account.getUuid(), + request.getRequest().getVersion(), request.getRequest().getCredentialRequest().toByteArray(), profilesManager, zkProfileOperations)); + } + + private Mono getTargetAccountAndValidateUnidentifiedAccess(final ServiceIdentifier targetIdentifier, final byte[] unidentifiedAccessKey) { + return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier)) + .flatMap(Mono::justOrEmpty) + .filter(targetAccount -> UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, unidentifiedAccessKey)) + .switchIfEmpty(Mono.error(Status.UNAUTHENTICATED.asException())); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcHelper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcHelper.java new file mode 100644 index 000000000..db1f6cd92 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcHelper.java @@ -0,0 +1,151 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.ByteString; +import io.grpc.Status; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.signal.chat.profile.Badge; +import org.signal.chat.profile.BadgeSvg; +import org.signal.chat.profile.GetExpiringProfileKeyCredentialResponse; +import org.signal.chat.profile.GetUnversionedProfileResponse; +import org.signal.chat.profile.GetVersionedProfileResponse; +import org.signal.chat.profile.UserCapabilities; +import org.signal.libsignal.protocol.ServiceId; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.VerificationFailedException; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; +import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; +import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum; +import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.ProfilesManager; +import org.whispersystems.textsecuregcm.storage.VersionedProfile; +import org.whispersystems.textsecuregcm.util.ProfileHelper; +import reactor.core.publisher.Mono; + +public class ProfileGrpcHelper { + static Mono getVersionedProfile(final Account account, + final ProfilesManager profilesManager, + final String requestVersion) { + return Mono.fromFuture(() -> profilesManager.getAsync(account.getUuid(), requestVersion)) + .map(maybeProfile -> { + if (maybeProfile.isEmpty()) { + throw Status.NOT_FOUND.withDescription("Profile version not found").asRuntimeException(); + } + + final GetVersionedProfileResponse.Builder responseBuilder = GetVersionedProfileResponse.newBuilder(); + + maybeProfile.map(VersionedProfile::name).map(ByteString::copyFrom).ifPresent(responseBuilder::setName); + maybeProfile.map(VersionedProfile::about).map(ByteString::copyFrom).ifPresent(responseBuilder::setAbout); + maybeProfile.map(VersionedProfile::aboutEmoji).map(ByteString::copyFrom).ifPresent(responseBuilder::setAboutEmoji); + maybeProfile.map(VersionedProfile::avatar).ifPresent(responseBuilder::setAvatar); + + // Allow requests where either the version matches the latest version on Account or the latest version on Account + // is empty to read the payment address. + maybeProfile + .filter(p -> account.getCurrentProfileVersion().map(v -> v.equals(requestVersion)).orElse(true)) + .map(VersionedProfile::paymentAddress) + .map(ByteString::copyFrom) + .ifPresent(responseBuilder::setPaymentAddress); + + return responseBuilder.build(); + }); + } + + @VisibleForTesting + static List buildBadges(final List badges) { + final ArrayList grpcBadges = new ArrayList<>(); + for (final org.whispersystems.textsecuregcm.entities.Badge badge : badges) { + grpcBadges.add(Badge.newBuilder() + .setId(badge.getId()) + .setCategory(badge.getCategory()) + .setName(badge.getName()) + .setDescription(badge.getDescription()) + .addAllSprites6(badge.getSprites6()) + .setSvg(badge.getSvg()) + .addAllSvgs(buildBadgeSvgs(badge.getSvgs())) + .build()); + } + return grpcBadges; + } + + @VisibleForTesting + static UserCapabilities buildUserCapabilities(final org.whispersystems.textsecuregcm.entities.UserCapabilities capabilities) { + return UserCapabilities.newBuilder() + .setPaymentActivation(capabilities.paymentActivation()) + .setPni(capabilities.pni()) + .build(); + } + + private static List buildBadgeSvgs(final List badgeSvgs) { + ArrayList grpcBadgeSvgs = new ArrayList<>(); + for (final org.whispersystems.textsecuregcm.entities.BadgeSvg badgeSvg : badgeSvgs) { + grpcBadgeSvgs.add(BadgeSvg.newBuilder() + .setDark(badgeSvg.getDark()) + .setLight(badgeSvg.getLight()) + .build()); + } + return grpcBadgeSvgs; + } + + static GetUnversionedProfileResponse buildUnversionedProfileResponse( + final ServiceIdentifier targetIdentifier, + final UUID requesterUuid, + final Account targetAccount, + final ProfileBadgeConverter profileBadgeConverter) { + final GetUnversionedProfileResponse.Builder responseBuilder = GetUnversionedProfileResponse.newBuilder() + .setIdentityKey(ByteString.copyFrom(targetAccount.getIdentityKey(targetIdentifier.identityType()).serialize())) + .setCapabilities(buildUserCapabilities(org.whispersystems.textsecuregcm.entities.UserCapabilities.createForAccount(targetAccount))); + + switch (targetIdentifier.identityType()) { + case ACI -> { + responseBuilder.setUnrestrictedUnidentifiedAccess(targetAccount.isUnrestrictedUnidentifiedAccess()) + .addAllBadges(buildBadges(profileBadgeConverter.convert( + AcceptLanguageUtil.localeFromGrpcContext(), + targetAccount.getBadges(), + ProfileHelper.isSelfProfileRequest(requesterUuid, (AciServiceIdentifier) targetIdentifier)))); + + targetAccount.getUnidentifiedAccessKey() + .map(UnidentifiedAccessChecksum::generateFor) + .map(ByteString::copyFrom) + .ifPresent(responseBuilder::setUnidentifiedAccess); + } + case PNI -> responseBuilder.setUnrestrictedUnidentifiedAccess(false); + } + + return responseBuilder.build(); + } + + static Mono getExpiringProfileKeyCredentialResponse( + final UUID targetUuid, + final String version, + final byte[] encodedCredentialRequest, + final ProfilesManager profilesManager, + final ServerZkProfileOperations zkProfileOperations) { + return Mono.fromFuture(profilesManager.getAsync(targetUuid, version)) + .flatMap(Mono::justOrEmpty) + .map(profile -> { + final ExpiringProfileKeyCredentialResponse profileKeyCredentialResponse; + try { + profileKeyCredentialResponse = ProfileHelper.getExpiringProfileKeyCredential(encodedCredentialRequest, + profile, new ServiceId.Aci(targetUuid), zkProfileOperations); + } catch (VerificationFailedException | InvalidInputException e) { + throw Status.INVALID_ARGUMENT.withCause(e).asRuntimeException(); + } + + return GetExpiringProfileKeyCredentialResponse.newBuilder() + .setProfileKeyCredential(ByteString.copyFrom(profileKeyCredentialResponse.serialize())) + .build(); + }) + .switchIfEmpty(Mono.error(Status.NOT_FOUND.withDescription("Profile version not found").asException())); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java new file mode 100644 index 000000000..7c26a4582 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java @@ -0,0 +1,266 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import com.google.protobuf.ByteString; +import io.grpc.Status; +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.signal.chat.profile.CredentialType; +import org.signal.chat.profile.GetExpiringProfileKeyCredentialRequest; +import org.signal.chat.profile.GetExpiringProfileKeyCredentialResponse; +import org.signal.chat.profile.GetUnversionedProfileRequest; +import org.signal.chat.profile.GetUnversionedProfileResponse; +import org.signal.chat.profile.GetVersionedProfileRequest; +import org.signal.chat.profile.GetVersionedProfileResponse; +import org.signal.chat.profile.ProfileAvatarUploadAttributes; +import org.signal.chat.profile.ReactorProfileGrpc; +import org.signal.chat.profile.SetProfileRequest; +import org.signal.chat.profile.SetProfileRequest.AvatarChange; +import org.signal.chat.profile.SetProfileResponse; +import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; +import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice; +import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil; +import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; +import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; +import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.s3.PolicySigner; +import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountBadge; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.storage.ProfilesManager; +import org.whispersystems.textsecuregcm.storage.VersionedProfile; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.ProfileHelper; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; + +public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase { + + private final Clock clock; + private final AccountsManager accountsManager; + private final ProfilesManager profilesManager; + private final DynamicConfigurationManager dynamicConfigurationManager; + private final Map badgeConfigurationMap; + private final S3AsyncClient asyncS3client; + private final PostPolicyGenerator policyGenerator; + private final PolicySigner policySigner; + private final ProfileBadgeConverter profileBadgeConverter; + private final RateLimiters rateLimiters; + private final ServerZkProfileOperations zkProfileOperations; + private final String bucket; + + private record AvatarData(Optional currentAvatar, + Optional finalAvatar, + Optional uploadAttributes) {} + + public ProfileGrpcService( + final Clock clock, + final AccountsManager accountsManager, + final ProfilesManager profilesManager, + final DynamicConfigurationManager dynamicConfigurationManager, + final BadgesConfiguration badgesConfiguration, + final S3AsyncClient asyncS3client, + final PostPolicyGenerator policyGenerator, + final PolicySigner policySigner, + final ProfileBadgeConverter profileBadgeConverter, + final RateLimiters rateLimiters, + final ServerZkProfileOperations zkProfileOperations, + final String bucket) { + this.clock = clock; + this.accountsManager = accountsManager; + this.profilesManager = profilesManager; + this.dynamicConfigurationManager = dynamicConfigurationManager; + this.badgeConfigurationMap = badgesConfiguration.getBadges().stream().collect(Collectors.toMap( + BadgeConfiguration::getId, Function.identity())); + this.asyncS3client = asyncS3client; + this.policyGenerator = policyGenerator; + this.policySigner = policySigner; + this.profileBadgeConverter = profileBadgeConverter; + this.rateLimiters = rateLimiters; + this.zkProfileOperations = zkProfileOperations; + this.bucket = bucket; + } + + @Override + public Mono setProfile(final SetProfileRequest request) { + validateRequest(request); + return Mono.fromSupplier(AuthenticationUtil::requireAuthenticatedDevice) + .flatMap(authenticatedDevice -> Mono.zip( + Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier())) + .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)), + Mono.fromFuture(() -> profilesManager.getAsync(authenticatedDevice.accountIdentifier(), request.getVersion())) + )) + .doOnNext(accountAndMaybeProfile -> { + if (!request.getPaymentAddress().isEmpty()) { + final boolean hasDisallowedPrefix = + dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getDisallowedPrefixes().stream() + .anyMatch(prefix -> accountAndMaybeProfile.getT1().getNumber().startsWith(prefix)); + if (hasDisallowedPrefix && accountAndMaybeProfile.getT2().map(VersionedProfile::paymentAddress).isEmpty()) { + throw Status.PERMISSION_DENIED.asRuntimeException(); + } + } + }) + .flatMap(accountAndMaybeProfile -> { + final Account account = accountAndMaybeProfile.getT1(); + final Optional currentAvatar = accountAndMaybeProfile.getT2().map(VersionedProfile::avatar) + .filter(avatar -> avatar.startsWith("profiles/")); + final AvatarData avatarData = switch (AvatarChangeUtil.fromGrpcAvatarChange(request.getAvatarChange())) { + case AVATAR_CHANGE_UNCHANGED -> new AvatarData(currentAvatar, currentAvatar, Optional.empty()); + case AVATAR_CHANGE_CLEAR -> new AvatarData(currentAvatar, Optional.empty(), Optional.empty()); + case AVATAR_CHANGE_UPDATE -> { + final String updateAvatarObjectName = ProfileHelper.generateAvatarObjectName(); + yield new AvatarData(currentAvatar, Optional.of(updateAvatarObjectName), + Optional.of(generateAvatarUploadForm(updateAvatarObjectName))); + } + }; + + final Mono profileSetMono = Mono.fromFuture(() -> profilesManager.setAsync(account.getUuid(), + new VersionedProfile( + request.getVersion(), + request.getName().toByteArray(), + avatarData.finalAvatar().orElse(null), + request.getAboutEmoji().toByteArray(), + request.getAbout().toByteArray(), + request.getPaymentAddress().toByteArray(), + request.getCommitment().toByteArray()))); + + final List> updates = new ArrayList<>(2); + final List updatedBadges = Optional.of(request.getBadgeIdsList()) + .map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges, account.getBadges())) + .orElseGet(account::getBadges); + + updates.add(Mono.fromFuture(() -> accountsManager.updateAsync(account, a -> { + a.setBadges(clock, updatedBadges); + a.setCurrentProfileVersion(request.getVersion()); + }))); + + if (request.getAvatarChange() != AvatarChange.AVATAR_CHANGE_UNCHANGED && avatarData.currentAvatar().isPresent()) { + updates.add(Mono.fromFuture(() -> asyncS3client.deleteObject(DeleteObjectRequest.builder() + .bucket(bucket) + .key(avatarData.currentAvatar().get()) + .build()))); + } + return profileSetMono.thenMany(Flux.merge(updates)).then(Mono.just(avatarData)); + }) + .map(avatarData -> avatarData.uploadAttributes() + .map(avatarUploadAttributes -> SetProfileResponse.newBuilder().setAttributes(avatarUploadAttributes).build()) + .orElse(SetProfileResponse.newBuilder().build()) + ); + } + + @Override + public Mono getUnversionedProfile(final GetUnversionedProfileRequest request) { + final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); + final ServiceIdentifier targetIdentifier = + ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getServiceIdentifier()); + return validateRateLimitAndGetAccount(authenticatedDevice.accountIdentifier(), targetIdentifier) + .map(targetAccount -> ProfileGrpcHelper.buildUnversionedProfileResponse(targetIdentifier, + authenticatedDevice.accountIdentifier(), + targetAccount, + profileBadgeConverter)); + } + + @Override + public Mono getVersionedProfile(final GetVersionedProfileRequest request) { + final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); + final ServiceIdentifier targetIdentifier = + ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getAccountIdentifier()); + + if (targetIdentifier.identityType() != IdentityType.ACI) { + throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asRuntimeException(); + } + + return validateRateLimitAndGetAccount(authenticatedDevice.accountIdentifier(), targetIdentifier) + .flatMap(account -> ProfileGrpcHelper.getVersionedProfile(account, profilesManager, request.getVersion())); + } + + @Override + public Mono getExpiringProfileKeyCredential( + final GetExpiringProfileKeyCredentialRequest request) { + final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); + final ServiceIdentifier targetIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getAccountIdentifier()); + + if (targetIdentifier.identityType() != IdentityType.ACI) { + throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asRuntimeException(); + } + + if (request.getCredentialType() != CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY) { + throw Status.INVALID_ARGUMENT.withDescription("Expected expiring profile key credential type").asRuntimeException(); + } + + return validateRateLimitAndGetAccount(authenticatedDevice.accountIdentifier(), targetIdentifier) + .flatMap(targetAccount -> ProfileGrpcHelper.getExpiringProfileKeyCredentialResponse(targetAccount.getUuid(), + request.getVersion(), request.getCredentialRequest().toByteArray(), profilesManager, zkProfileOperations)); + } + + + private Mono validateRateLimitAndGetAccount(final UUID requesterUuid, + final ServiceIdentifier targetIdentifier) { + return rateLimiters.getProfileLimiter().validateReactive(requesterUuid) + .then(Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier)) + .flatMap(Mono::justOrEmpty)) + .switchIfEmpty(Mono.error(Status.NOT_FOUND.asException())); + } + + private void validateRequest(final SetProfileRequest request) { + if (request.getVersion().isEmpty()) { + throw Status.INVALID_ARGUMENT.withDescription("Missing version").asRuntimeException(); + } + + if (request.getCommitment().isEmpty()) { + throw Status.INVALID_ARGUMENT.withDescription("Missing profile commitment").asRuntimeException(); + } + + checkByteStringLength(request.getName(), "Invalid name length", List.of(81, 285)); + checkByteStringLength(request.getAboutEmoji(), "Invalid about emoji length", List.of(0, 60)); + checkByteStringLength(request.getAbout(), "Invalid about length", List.of(0, 156, 282, 540)); + checkByteStringLength(request.getPaymentAddress(), "Invalid mobile coin address length", List.of(0, 582)); + } + + private static void checkByteStringLength(final ByteString byteString, final String errorMessage, final List allowedLengths) { + final int byteStringLength = byteString.toByteArray().length; + + for (int allowedLength : allowedLengths) { + if (byteStringLength == allowedLength) { + return; + } + } + + throw Status.INVALID_ARGUMENT.withDescription(errorMessage).asRuntimeException(); + } + + private ProfileAvatarUploadAttributes generateAvatarUploadForm(final String objectName) { + final ZonedDateTime now = ZonedDateTime.now(clock); + final Pair policy = policyGenerator.createFor(now, objectName, ProfileHelper.MAX_PROFILE_AVATAR_SIZE_BYTES); + final String signature = policySigner.getSignature(now, policy.second()); + + return ProfileAvatarUploadAttributes.newBuilder() + .setPath(objectName) + .setCredential(policy.first()) + .setAcl("private") + .setAlgorithm("AWS4-HMAC-SHA256") + .setDate(now.format(PostPolicyGenerator.AWS_DATE_TIME)) + .setPolicy(policy.second()) + .setSignature(ByteString.copyFrom(signature.getBytes())) + .build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/RemoteAddressInterceptor.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/RemoteAddressInterceptor.java new file mode 100644 index 000000000..0fe21bdb5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/RemoteAddressInterceptor.java @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Grpc; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import java.net.SocketAddress; + +public class RemoteAddressInterceptor implements ServerInterceptor { + + @Override + public ServerCall.Listener interceptCall(final ServerCall serverCall, + final Metadata headers, + final ServerCallHandler next) { + + // Note: the specific implementation for getting a remote client address may change depending on the client + // connection strategy. The important thing is that the remote address wind up in the context for the current + // call so it can be retrieved by `RemoteAddressUtil`. + final SocketAddress remoteAddress = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + + return Contexts.interceptCall( + Context.current().withValue(RemoteAddressUtil.REMOTE_ADDRESS_CONTEXT_KEY, remoteAddress), + serverCall, headers, next); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/RemoteAddressUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/RemoteAddressUtil.java new file mode 100644 index 000000000..946a6a3ba --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/RemoteAddressUtil.java @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.Context; +import java.net.SocketAddress; + +public class RemoteAddressUtil { + + static final Context.Key REMOTE_ADDRESS_CONTEXT_KEY = Context.key("remote-address"); + + /** + * Returns the socket address of the remote client in the current gRPC request context. + * + * @return the socket address of the remote client + */ + public static SocketAddress getRemoteAddress() { + return REMOTE_ADDRESS_CONTEXT_KEY.get(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ServiceIdentifierUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ServiceIdentifierUtil.java new file mode 100644 index 000000000..a3e5e9053 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ServiceIdentifierUtil.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import com.google.protobuf.ByteString; +import io.grpc.Status; +import java.util.UUID; +import org.signal.chat.common.IdentityType; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.util.UUIDUtil; + +public class ServiceIdentifierUtil { + + private ServiceIdentifierUtil() { + } + + public static ServiceIdentifier fromGrpcServiceIdentifier(final org.signal.chat.common.ServiceIdentifier serviceIdentifier) { + final UUID uuid; + + try { + uuid = UUIDUtil.fromByteString(serviceIdentifier.getUuid()); + } catch (final IllegalArgumentException e) { + throw Status.INVALID_ARGUMENT.asRuntimeException(); + } + + return switch (IdentityTypeUtil.fromGrpcIdentityType(serviceIdentifier.getIdentityType())) { + case ACI -> new AciServiceIdentifier(uuid); + case PNI -> new PniServiceIdentifier(uuid); + }; + } + + public static org.signal.chat.common.ServiceIdentifier toGrpcServiceIdentifier(final ServiceIdentifier serviceIdentifier) { + return org.signal.chat.common.ServiceIdentifier.newBuilder() + .setIdentityType(IdentityTypeUtil.toGrpcIdentityType(serviceIdentifier.identityType())) + .setUuid(UUIDUtil.toByteString(serviceIdentifier.uuid())) + .build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/StatusConstants.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/StatusConstants.java new file mode 100644 index 000000000..78836ff20 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/StatusConstants.java @@ -0,0 +1,12 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.Status; + +public abstract class StatusConstants { + public static final Status UPGRADE_NEEDED_STATUS = Status.INVALID_ARGUMENT.withDescription("signal-upgrade-required"); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/UserAgentInterceptor.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/UserAgentInterceptor.java new file mode 100644 index 000000000..4a0d71ef4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/grpc/UserAgentInterceptor.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import com.google.common.annotations.VisibleForTesting; + +import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; +import org.whispersystems.textsecuregcm.util.ua.UserAgent; +import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; + +public class UserAgentInterceptor implements ServerInterceptor { + @VisibleForTesting + public static final Metadata.Key USER_AGENT_GRPC_HEADER = + Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER); + + @Override + public ServerCall.Listener interceptCall(final ServerCall call, + final Metadata headers, + final ServerCallHandler next) { + + UserAgent userAgent; + try { + userAgent = UserAgentUtil.parseUserAgentString(headers.get(USER_AGENT_GRPC_HEADER)); + } catch (final UnrecognizedUserAgentException e) { + userAgent = null; + } + + final Context context = Context.current().withValue(UserAgentUtil.USER_AGENT_CONTEXT_KEY, userAgent); + return Contexts.interceptCall(context, call, headers, next); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/http/FaultTolerantHttpClient.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/http/FaultTolerantHttpClient.java new file mode 100644 index 000000000..ef729c136 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/http/FaultTolerantHttpClient.java @@ -0,0 +1,201 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.http; + +import com.google.common.annotations.VisibleForTesting; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Predicate; +import java.util.function.Supplier; +import org.glassfish.jersey.SslConfigurator; +import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; +import org.whispersystems.textsecuregcm.configuration.RetryConfiguration; +import org.whispersystems.textsecuregcm.util.CertificateUtil; +import org.whispersystems.textsecuregcm.util.CircuitBreakerUtil; +import org.whispersystems.textsecuregcm.util.ExceptionUtils; + +public class FaultTolerantHttpClient { + + private final HttpClient httpClient; + private final Duration defaultRequestTimeout; + private final ScheduledExecutorService retryExecutor; + private final Retry retry; + private final CircuitBreaker breaker; + + public static final String SECURITY_PROTOCOL_TLS_1_2 = "TLSv1.2"; + public static final String SECURITY_PROTOCOL_TLS_1_3 = "TLSv1.3"; + + public static Builder newBuilder() { + return new Builder(); + } + + @VisibleForTesting + FaultTolerantHttpClient(String name, HttpClient httpClient, ScheduledExecutorService retryExecutor, + Duration defaultRequestTimeout, RetryConfiguration retryConfiguration, + final Predicate retryOnException, CircuitBreakerConfiguration circuitBreakerConfiguration) { + + this.httpClient = httpClient; + this.retryExecutor = retryExecutor; + this.defaultRequestTimeout = defaultRequestTimeout; + this.breaker = CircuitBreaker.of(name + "-breaker", circuitBreakerConfiguration.toCircuitBreakerConfig()); + + CircuitBreakerUtil.registerMetrics(breaker, FaultTolerantHttpClient.class); + + if (retryConfiguration != null) { + if (this.retryExecutor == null) { + throw new IllegalArgumentException("retryExecutor must be specified with retryConfiguration"); + } + final RetryConfig.Builder retryConfig = retryConfiguration.toRetryConfigBuilder() + .retryOnResult(o -> o.statusCode() >= 500); + if (retryOnException != null) { + retryConfig.retryOnException(retryOnException); + } + this.retry = Retry.of(name + "-retry", retryConfig.build()); + CircuitBreakerUtil.registerMetrics(retry, FaultTolerantHttpClient.class); + } else { + this.retry = null; + } + } + + public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler bodyHandler) { + if (request.timeout().isEmpty()) { + request = HttpRequest.newBuilder(request, (n, v) -> true) + .timeout(defaultRequestTimeout) + .build(); + } + + Supplier>> asyncRequest = sendAsync(httpClient, request, bodyHandler); + + if (retry != null) { + return breaker.executeCompletionStage(retryableCompletionStage(asyncRequest)).toCompletableFuture(); + } else { + return breaker.executeCompletionStage(asyncRequest).toCompletableFuture(); + } + } + + private Supplier> retryableCompletionStage(Supplier> supplier) { + return () -> retry.executeCompletionStage(retryExecutor, supplier); + } + + private Supplier>> sendAsync(HttpClient client, HttpRequest request, HttpResponse.BodyHandler bodyHandler) { + return () -> client.sendAsync(request, bodyHandler); + } + + public static class Builder { + + private HttpClient.Version version = HttpClient.Version.HTTP_2; + private HttpClient.Redirect redirect = HttpClient.Redirect.NEVER; + private Duration connectTimeout = Duration.ofSeconds(10); + private Duration requestTimeout = Duration.ofSeconds(60); + + private String name; + private Executor executor; + private ScheduledExecutorService retryExecutor; + private KeyStore trustStore; + private String securityProtocol = SECURITY_PROTOCOL_TLS_1_2; + private RetryConfiguration retryConfiguration; + private Predicate retryOnException; + private CircuitBreakerConfiguration circuitBreakerConfiguration; + + private Builder() { + } + + public Builder withName(String name) { + this.name = name; + return this; + } + + public Builder withVersion(HttpClient.Version version) { + this.version = version; + return this; + } + + public Builder withRedirect(HttpClient.Redirect redirect) { + this.redirect = redirect; + return this; + } + + public Builder withExecutor(Executor executor) { + this.executor = executor; + return this; + } + + public Builder withConnectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + public Builder withRequestTimeout(Duration requestTimeout) { + this.requestTimeout = requestTimeout; + return this; + } + + public Builder withRetry(RetryConfiguration retryConfiguration) { + this.retryConfiguration = retryConfiguration; + return this; + } + + public Builder withRetryExecutor(ScheduledExecutorService retryExecutor) { + this.retryExecutor = retryExecutor; + return this; + } + + public Builder withCircuitBreaker(CircuitBreakerConfiguration circuitBreakerConfiguration) { + this.circuitBreakerConfiguration = circuitBreakerConfiguration; + return this; + } + + public Builder withSecurityProtocol(final String securityProtocol) { + this.securityProtocol = securityProtocol; + return this; + } + + public Builder withTrustedServerCertificates(final String... certificatePem) throws CertificateException { + this.trustStore = CertificateUtil.buildKeyStoreForPem(certificatePem); + return this; + } + + public Builder withRetryOnException(final Predicate predicate) { + this.retryOnException = throwable -> predicate.test(ExceptionUtils.unwrap(throwable)); + return this; + } + + public FaultTolerantHttpClient build() { + if (this.circuitBreakerConfiguration == null || this.name == null || this.executor == null) { + throw new IllegalArgumentException("Must specify circuit breaker config, name, and executor"); + } + + final HttpClient.Builder builder = HttpClient.newBuilder() + .connectTimeout(connectTimeout) + .followRedirects(redirect) + .version(version) + .executor(executor); + + final SslConfigurator sslConfigurator = SslConfigurator.newInstance().securityProtocol(securityProtocol); + + if (this.trustStore != null) { + sslConfigurator.trustStore(trustStore); + } + + builder.sslContext(sslConfigurator.createSSLContext()); + + return new FaultTolerantHttpClient(name, builder.build(), retryExecutor, requestTimeout, retryConfiguration, + retryOnException, circuitBreakerConfiguration); + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/http/FormDataBodyPublisher.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/http/FormDataBodyPublisher.java new file mode 100644 index 000000000..b2935e499 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/http/FormDataBodyPublisher.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.http; + +import java.net.URLEncoder; +import java.net.http.HttpRequest; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class FormDataBodyPublisher { + + public static HttpRequest.BodyPublisher of(Map data) { + StringBuilder builder = new StringBuilder(); + + for (Map.Entry entry : data.entrySet()) { + if (builder.length() > 0) { + builder.append("&"); + } + + builder.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)); + builder.append("="); + builder.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)); + } + + return HttpRequest.BodyPublishers.ofString(builder.toString()); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/identity/AciServiceIdentifier.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/identity/AciServiceIdentifier.java new file mode 100644 index 000000000..ba08c42ba --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/identity/AciServiceIdentifier.java @@ -0,0 +1,75 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.identity; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.UUID; +import io.swagger.v3.oas.annotations.media.Schema; +import org.whispersystems.textsecuregcm.util.UUIDUtil; + +/** + * An identifier for an account based on the account's ACI. + * + * @param uuid the account's ACI UUID + */ +@Schema( + type = "string", + description = "An identifier for an account based on the account's ACI" +) +public record AciServiceIdentifier(UUID uuid) implements ServiceIdentifier { + + private static final IdentityType IDENTITY_TYPE = IdentityType.ACI; + + @Override + public IdentityType identityType() { + return IDENTITY_TYPE; + } + + @Override + public String toServiceIdentifierString() { + return uuid.toString(); + } + + @Override + public byte[] toCompactByteArray() { + return UUIDUtil.toBytes(uuid); + } + + @Override + public byte[] toFixedWidthByteArray() { + final ByteBuffer byteBuffer = ByteBuffer.allocate(17); + byteBuffer.put(IDENTITY_TYPE.getBytePrefix()); + byteBuffer.putLong(uuid.getMostSignificantBits()); + byteBuffer.putLong(uuid.getLeastSignificantBits()); + byteBuffer.flip(); + + return byteBuffer.array(); + } + + public static AciServiceIdentifier valueOf(final String string) { + return new AciServiceIdentifier( + UUID.fromString(string.startsWith(IDENTITY_TYPE.getStringPrefix()) + ? string.substring(IDENTITY_TYPE.getStringPrefix().length()) : string)); + } + + public static AciServiceIdentifier fromBytes(final byte[] bytes) { + final UUID uuid; + + if (bytes.length == 17) { + if (bytes[0] != IDENTITY_TYPE.getBytePrefix()) { + throw new IllegalArgumentException("Unexpected byte array prefix: " + HexFormat.of().formatHex(new byte[] { bytes[0] })); + } + + uuid = UUIDUtil.fromBytes(Arrays.copyOfRange(bytes, 1, bytes.length)); + } else { + uuid = UUIDUtil.fromBytes(bytes); + } + + return new AciServiceIdentifier(uuid); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/identity/IdentityType.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/identity/IdentityType.java new file mode 100644 index 000000000..720267095 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/identity/IdentityType.java @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.identity; + +public enum IdentityType { + ACI((byte) 0x00, "ACI:"), + PNI((byte) 0x01, "PNI:"); + + private final byte bytePrefix; + private final String stringPrefix; + + IdentityType(final byte bytePrefix, final String stringPrefix) { + this.bytePrefix = bytePrefix; + this.stringPrefix = stringPrefix; + } + + byte getBytePrefix() { + return bytePrefix; + } + + String getStringPrefix() { + return stringPrefix; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/identity/PniServiceIdentifier.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/identity/PniServiceIdentifier.java new file mode 100644 index 000000000..2f184fd20 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/identity/PniServiceIdentifier.java @@ -0,0 +1,73 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.identity; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.whispersystems.textsecuregcm.util.UUIDUtil; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.UUID; + +/** + * An identifier for an account based on the account's phone number identifier (PNI). + * + * @param uuid the account's PNI UUID + */ +@Schema( + type = "string", + description = "An identifier for an account based on the account's phone number identifier (PNI)" +) +public record PniServiceIdentifier(UUID uuid) implements ServiceIdentifier { + + private static final IdentityType IDENTITY_TYPE = IdentityType.PNI; + + @Override + public IdentityType identityType() { + return IDENTITY_TYPE; + } + + @Override + public String toServiceIdentifierString() { + return IDENTITY_TYPE.getStringPrefix() + uuid.toString(); + } + + @Override + public byte[] toCompactByteArray() { + return toFixedWidthByteArray(); + } + + @Override + public byte[] toFixedWidthByteArray() { + final ByteBuffer byteBuffer = ByteBuffer.allocate(17); + byteBuffer.put(IDENTITY_TYPE.getBytePrefix()); + byteBuffer.putLong(uuid.getMostSignificantBits()); + byteBuffer.putLong(uuid.getLeastSignificantBits()); + byteBuffer.flip(); + + return byteBuffer.array(); + } + + public static PniServiceIdentifier valueOf(final String string) { + if (!string.startsWith(IDENTITY_TYPE.getStringPrefix())) { + throw new IllegalArgumentException("PNI account identifier did not start with \"PNI:\" prefix"); + } + + return new PniServiceIdentifier(UUID.fromString(string.substring(IDENTITY_TYPE.getStringPrefix().length()))); + } + + public static PniServiceIdentifier fromBytes(final byte[] bytes) { + if (bytes.length == 17) { + if (bytes[0] != IDENTITY_TYPE.getBytePrefix()) { + throw new IllegalArgumentException("Unexpected byte array prefix: " + HexFormat.of().formatHex(new byte[] { bytes[0] })); + } + + return new PniServiceIdentifier(UUIDUtil.fromBytes(Arrays.copyOfRange(bytes, 1, bytes.length))); + } + + throw new IllegalArgumentException("Unexpected byte array length: " + bytes.length); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/identity/ServiceIdentifier.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/identity/ServiceIdentifier.java new file mode 100644 index 000000000..c012f924c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/identity/ServiceIdentifier.java @@ -0,0 +1,73 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.identity; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.UUID; + +/** + * A "service identifier" is a tuple of a UUID and identity type that identifies an account and identity within the + * Signal service. + */ +@Schema( + type = "string", + description = "A service identifier is a tuple of a UUID and identity type that identifies an account and identity within the Signal service.", + subTypes = {AciServiceIdentifier.class, PniServiceIdentifier.class} +) +public interface ServiceIdentifier { + + /** + * Returns the identity type of this account identifier. + * + * @return the identity type of this account identifier + */ + IdentityType identityType(); + + /** + * Returns the UUID for this account identifier. + * + * @return the UUID for this account identifier + */ + UUID uuid(); + + /** + * Returns a string representation of this account identifier in a format that clients can unambiguously resolve into + * an identity type and UUID. + * + * @return a "strongly-typed" string representation of this account identifier + */ + String toServiceIdentifierString(); + + /** + * Returns a compact binary representation of this account identifier. + * + * @return a binary representation of this account identifier + */ + byte[] toCompactByteArray(); + + /** + * Returns a fixed-width binary representation of this account identifier. + * + * @return a binary representation of this account identifier + */ + byte[] toFixedWidthByteArray(); + + static ServiceIdentifier valueOf(final String string) { + try { + return AciServiceIdentifier.valueOf(string); + } catch (final IllegalArgumentException e) { + return PniServiceIdentifier.valueOf(string); + } + } + + static ServiceIdentifier fromBytes(final byte[] bytes) { + try { + return AciServiceIdentifier.fromBytes(bytes); + } catch (final IllegalArgumentException e) { + return PniServiceIdentifier.fromBytes(bytes); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/BaseRateLimiters.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/BaseRateLimiters.java new file mode 100644 index 000000000..4f87761ae --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/BaseRateLimiters.java @@ -0,0 +1,100 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import static java.util.Objects.requireNonNull; + +import io.lettuce.core.ScriptOutputType; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.invoke.MethodHandles; +import java.time.Clock; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.redis.ClusterLuaScript; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; + +public abstract class BaseRateLimiters { + + private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final Map rateLimiterByDescriptor; + + private final Map configs; + + + protected BaseRateLimiters( + final T[] values, + final Map configs, + final DynamicConfigurationManager dynamicConfigurationManager, + final ClusterLuaScript validateScript, + final FaultTolerantRedisCluster cacheCluster, + final Clock clock) { + this.configs = configs; + this.rateLimiterByDescriptor = Arrays.stream(values) + .map(descriptor -> Pair.of( + descriptor, + createForDescriptor(descriptor, configs, dynamicConfigurationManager, validateScript, cacheCluster, clock))) + .collect(Collectors.toUnmodifiableMap(Pair::getKey, Pair::getValue)); + } + + public RateLimiter forDescriptor(final T handle) { + return requireNonNull(rateLimiterByDescriptor.get(handle)); + } + + public void validateValuesAndConfigs() { + final Set ids = rateLimiterByDescriptor.keySet().stream() + .map(RateLimiterDescriptor::id) + .collect(Collectors.toSet()); + for (final String key: configs.keySet()) { + if (!ids.contains(key)) { + final String message = String.format( + "Static configuration has an unexpected field '%s' that doesn't match any RateLimiterDescriptor", + key + ); + logger.error(message); + throw new IllegalArgumentException(message); + } + } + } + + protected static ClusterLuaScript defaultScript(final FaultTolerantRedisCluster cacheCluster) { + try { + return ClusterLuaScript.fromResource( + cacheCluster, "lua/validate_rate_limit.lua", ScriptOutputType.INTEGER); + } catch (final IOException e) { + throw new UncheckedIOException("Failed to load rate limit validation script", e); + } + } + + private static RateLimiter createForDescriptor( + final RateLimiterDescriptor descriptor, + final Map configs, + final DynamicConfigurationManager dynamicConfigurationManager, + final ClusterLuaScript validateScript, + final FaultTolerantRedisCluster cacheCluster, + final Clock clock) { + if (descriptor.isDynamic()) { + final Supplier configResolver = () -> { + final RateLimiterConfig config = dynamicConfigurationManager.getConfiguration().getLimits().get(descriptor.id()); + return config != null + ? config + : configs.getOrDefault(descriptor.id(), descriptor.defaultConfig()); + }; + return new DynamicRateLimiter(descriptor.id(), dynamicConfigurationManager, configResolver, validateScript, cacheCluster, clock); + } + final RateLimiterConfig cfg = configs.getOrDefault(descriptor.id(), descriptor.defaultConfig()); + return new StaticRateLimiter(descriptor.id(), cfg, validateScript, cacheCluster, clock, dynamicConfigurationManager); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/CardinalityEstimator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/CardinalityEstimator.java new file mode 100644 index 000000000..40c62dc5d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/CardinalityEstimator.java @@ -0,0 +1,76 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import com.google.common.annotations.VisibleForTesting; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.util.Util; + +/** + * Estimate the number of unique items seen over a configurable period and update a metric + */ +public class CardinalityEstimator { + + private volatile double uniqueElementCount; + private final FaultTolerantRedisCluster redisCluster; + private final String hllName; + private final Duration period; + + public CardinalityEstimator(final FaultTolerantRedisCluster redisCluster, final String name, final Duration period) { + this.redisCluster = redisCluster; + this.hllName = "cardinality_estimator::" + name; + this.period = period; + Metrics.gauge( + MetricsUtil.name(getClass(), "unique"), + Tags.of("name", name), + this, + obj -> obj.uniqueElementCount); + } + + public void add(String element) { + addAsync(element).toCompletableFuture().join(); + } + + public CompletionStage addAsync(String element) { + return redisCluster.withCluster(connection -> connection.async() + .pfadd(hllName, element) + .thenCompose(modCount -> { + if (modCount == 0) { + return CompletableFuture.completedFuture(false); + } + + // The hll changed - update our local view of the cardinality, and + // initialize the TTL if required + return connection.async() + .pfcount(hllName) + .thenCompose(count -> { + uniqueElementCount = count; + // check if this is a new hll with no TTL set + return connection.async().ttl(hllName).thenApply(ttl -> ttl == -1); + }); + }) + .thenCompose(isNewHll -> { + if (!isNewHll) { + return CompletableFuture.completedFuture(null); + } + + // If this is a new hll, we need to set the TTL. This could be + // a single atomic op in redis 7.x with EXPIRE NX + return connection.async().expire(hllName, period).thenRun(Util.NOOP); + })); + } + + @VisibleForTesting + long estimate() { + return (long) this.uniqueElementCount; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/DynamicRateLimiter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/DynamicRateLimiter.java new file mode 100644 index 000000000..2b38483e2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/DynamicRateLimiter.java @@ -0,0 +1,93 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import static java.util.Objects.requireNonNull; + +import java.time.Clock; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import org.apache.commons.lang3.tuple.Pair; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.redis.ClusterLuaScript; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; + +public class DynamicRateLimiter implements RateLimiter { + + private final String name; + private final DynamicConfigurationManager dynamicConfigurationManager; + private final Supplier configResolver; + + private final ClusterLuaScript validateScript; + + private final FaultTolerantRedisCluster cluster; + + private final Clock clock; + + private final AtomicReference> currentHolder = new AtomicReference<>(); + + + public DynamicRateLimiter( + final String name, + final DynamicConfigurationManager dynamicConfigurationManager, + final Supplier configResolver, + final ClusterLuaScript validateScript, + final FaultTolerantRedisCluster cluster, + final Clock clock) { + this.name = requireNonNull(name); + this.dynamicConfigurationManager = dynamicConfigurationManager; + this.configResolver = requireNonNull(configResolver); + this.validateScript = requireNonNull(validateScript); + this.cluster = requireNonNull(cluster); + this.clock = requireNonNull(clock); + } + + @Override + public void validate(final String key, final int amount) throws RateLimitExceededException { + current().getRight().validate(key, amount); + } + + @Override + public CompletionStage validateAsync(final String key, final int amount) { + return current().getRight().validateAsync(key, amount); + } + + @Override + public boolean hasAvailablePermits(final String key, final int permits) { + return current().getRight().hasAvailablePermits(key, permits); + } + + @Override + public CompletionStage hasAvailablePermitsAsync(final String key, final int amount) { + return current().getRight().hasAvailablePermitsAsync(key, amount); + } + + @Override + public void clear(final String key) { + current().getRight().clear(key); + } + + @Override + public CompletionStage clearAsync(final String key) { + return current().getRight().clearAsync(key); + } + + @Override + public RateLimiterConfig config() { + return current().getLeft(); + } + + private Pair current() { + final RateLimiterConfig cfg = configResolver.get(); + return currentHolder.updateAndGet(p -> p != null && p.getLeft().equals(cfg) + ? p + : Pair.of(cfg, new StaticRateLimiter(name, cfg, validateScript, cluster, clock, dynamicConfigurationManager)) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/PushChallengeManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/PushChallengeManager.java new file mode 100644 index 000000000..4ec20570d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/PushChallengeManager.java @@ -0,0 +1,107 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import static com.codahale.metrics.MetricRegistry.name; + +import io.micrometer.core.instrument.Metrics; +import java.security.SecureRandom; +import java.time.Duration; +import java.util.HexFormat; +import org.apache.commons.lang3.StringUtils; +import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; +import org.whispersystems.textsecuregcm.push.PushNotificationManager; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.PushChallengeDynamoDb; +import org.whispersystems.textsecuregcm.util.Util; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; + +public class PushChallengeManager { + private final PushNotificationManager pushNotificationManager; + private final PushChallengeDynamoDb pushChallengeDynamoDb; + + private final SecureRandom random = new SecureRandom(); + + private static final int CHALLENGE_TOKEN_LENGTH = 16; + private static final Duration CHALLENGE_TTL = Duration.ofMinutes(5); + + private static final String CHALLENGE_REQUESTED_COUNTER_NAME = name(PushChallengeManager.class, "requested"); + private static final String CHALLENGE_ANSWERED_COUNTER_NAME = name(PushChallengeManager.class, "answered"); + + private static final String PLATFORM_TAG_NAME = "platform"; + private static final String SENT_TAG_NAME = "sent"; + private static final String SUCCESS_TAG_NAME = "success"; + private static final String SOURCE_COUNTRY_TAG_NAME = "sourceCountry"; + + public PushChallengeManager(final PushNotificationManager pushNotificationManager, + final PushChallengeDynamoDb pushChallengeDynamoDb) { + + this.pushNotificationManager = pushNotificationManager; + this.pushChallengeDynamoDb = pushChallengeDynamoDb; + } + + public void sendChallenge(final Account account) throws NotPushRegisteredException { + final Device masterDevice = account.getMasterDevice().orElseThrow(NotPushRegisteredException::new); + + final byte[] token = new byte[CHALLENGE_TOKEN_LENGTH]; + random.nextBytes(token); + + final boolean sent; + final String platform; + + if (pushChallengeDynamoDb.add(account.getUuid(), token, CHALLENGE_TTL)) { + pushNotificationManager.sendRateLimitChallengeNotification(account, HexFormat.of().formatHex(token)); + + sent = true; + + if (StringUtils.isNotBlank(masterDevice.getGcmId())) { + platform = ClientPlatform.ANDROID.name().toLowerCase(); + } else if (StringUtils.isNotBlank(masterDevice.getApnId())) { + platform = ClientPlatform.IOS.name().toLowerCase(); + } else { + // This should never happen; if the account has neither an APN nor FCM token, sending the challenge will result + // in a `NotPushRegisteredException` + platform = "unrecognized"; + } + } else { + sent = false; + platform = "unrecognized"; + } + + Metrics.counter(CHALLENGE_REQUESTED_COUNTER_NAME, + PLATFORM_TAG_NAME, platform, + SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.getNumber()), + SENT_TAG_NAME, String.valueOf(sent)).increment(); + } + + public boolean answerChallenge(final Account account, final String challengeTokenHex) { + boolean success = false; + + try { + success = pushChallengeDynamoDb.remove(account.getUuid(), HexFormat.of().parseHex(challengeTokenHex)); + } catch (final IllegalArgumentException ignored) { + } + + final String platform = account.getMasterDevice().map(masterDevice -> { + if (StringUtils.isNotBlank(masterDevice.getGcmId())) { + return ClientPlatform.IOS.name().toLowerCase(); + } else if (StringUtils.isNotBlank(masterDevice.getApnId())) { + return ClientPlatform.ANDROID.name().toLowerCase(); + } else { + return "unknown"; + } + }).orElse("unknown"); + + + Metrics.counter(CHALLENGE_ANSWERED_COUNTER_NAME, + PLATFORM_TAG_NAME, platform, + SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.getNumber()), + SUCCESS_TAG_NAME, String.valueOf(success)).increment(); + + return success; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitByIpFilter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitByIpFilter.java new file mode 100644 index 000000000..17c6e3957 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitByIpFilter.java @@ -0,0 +1,87 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import static java.util.Objects.requireNonNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.HttpHeaders; +import java.io.IOException; +import java.time.Duration; +import java.util.Optional; +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.util.HeaderUtils; + +public class RateLimitByIpFilter implements ContainerRequestFilter { + + private static final Logger logger = LoggerFactory.getLogger(RateLimitByIpFilter.class); + + @VisibleForTesting + static final RateLimitExceededException INVALID_HEADER_EXCEPTION = new RateLimitExceededException(Duration.ofHours(1), + true); + + private static final ExceptionMapper EXCEPTION_MAPPER = new RateLimitExceededExceptionMapper(); + + private final RateLimiters rateLimiters; + + + public RateLimitByIpFilter(final RateLimiters rateLimiters) { + this.rateLimiters = requireNonNull(rateLimiters); + } + + @Override + public void filter(final ContainerRequestContext requestContext) throws IOException { + // requestContext.getUriInfo() should always be an instance of `ExtendedUriInfo` + // in the Jersey client + if (!(requestContext.getUriInfo() instanceof final ExtendedUriInfo uriInfo)) { + return; + } + + final RateLimitedByIp annotation = uriInfo.getMatchedResourceMethod() + .getInvocable() + .getHandlingMethod() + .getAnnotation(RateLimitedByIp.class); + + if (annotation == null) { + return; + } + + final RateLimiters.For handle = annotation.value(); + + try { + final String xffHeader = requestContext.getHeaders().getFirst(HttpHeaders.X_FORWARDED_FOR); + final Optional maybeMostRecentProxy = Optional.ofNullable(xffHeader) + .flatMap(HeaderUtils::getMostRecentProxy); + + // checking if we failed to extract the most recent IP from the X-Forwarded-For header + // for any reason + if (maybeMostRecentProxy.isEmpty()) { + // checking if annotation is configured to fail when the most recent IP is not resolved + if (annotation.failOnUnresolvedIp()) { + logger.error("Missing/bad X-Forwarded-For: {}", xffHeader); + throw INVALID_HEADER_EXCEPTION; + } + // otherwise, allow request + return; + } + + final RateLimiter rateLimiter = rateLimiters.forDescriptor(handle); + rateLimiter.validate(maybeMostRecentProxy.get()); + } catch (RateLimitExceededException e) { + final Response response = EXCEPTION_MAPPER.toResponse(e); + throw new ClientErrorException(response); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManager.java new file mode 100644 index 000000000..c2d2b4bda --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManager.java @@ -0,0 +1,105 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import static com.codahale.metrics.MetricRegistry.name; + +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.whispersystems.textsecuregcm.captcha.Action; +import org.whispersystems.textsecuregcm.captcha.CaptchaChecker; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; +import org.whispersystems.textsecuregcm.spam.ChallengeType; +import org.whispersystems.textsecuregcm.spam.RateLimitChallengeListener; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.util.Util; + +public class RateLimitChallengeManager { + + private final PushChallengeManager pushChallengeManager; + private final CaptchaChecker captchaChecker; + private final RateLimiters rateLimiters; + + private final List rateLimitChallengeListeners = + Collections.synchronizedList(new ArrayList<>()); + + private static final String RECAPTCHA_ATTEMPT_COUNTER_NAME = name(RateLimitChallengeManager.class, "recaptcha", "attempt"); + private static final String RESET_RATE_LIMIT_EXCEEDED_COUNTER_NAME = name(RateLimitChallengeManager.class, "resetRateLimitExceeded"); + + private static final String SOURCE_COUNTRY_TAG_NAME = "sourceCountry"; + private static final String SUCCESS_TAG_NAME = "success"; + + public RateLimitChallengeManager( + final PushChallengeManager pushChallengeManager, + final CaptchaChecker captchaChecker, + final RateLimiters rateLimiters) { + + this.pushChallengeManager = pushChallengeManager; + this.captchaChecker = captchaChecker; + this.rateLimiters = rateLimiters; + } + + public void addListener(final RateLimitChallengeListener rateLimitChallengeListener) { + rateLimitChallengeListeners.add(rateLimitChallengeListener); + } + + public void answerPushChallenge(final Account account, final String challenge) throws RateLimitExceededException { + rateLimiters.getPushChallengeAttemptLimiter().validate(account.getUuid()); + + final boolean challengeSuccess = pushChallengeManager.answerChallenge(account, challenge); + + if (challengeSuccess) { + rateLimiters.getPushChallengeSuccessLimiter().validate(account.getUuid()); + resetRateLimits(account, ChallengeType.PUSH); + } + } + + public boolean answerRecaptchaChallenge(final Account account, final String captcha, final String mostRecentProxyIp, final String userAgent) + throws RateLimitExceededException, IOException { + + rateLimiters.getRecaptchaChallengeAttemptLimiter().validate(account.getUuid()); + + final boolean challengeSuccess = captchaChecker.verify(Action.CHALLENGE, captcha, mostRecentProxyIp).isValid(); + + final Tags tags = Tags.of( + Tag.of(SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.getNumber())), + Tag.of(SUCCESS_TAG_NAME, String.valueOf(challengeSuccess)), + UserAgentTagUtil.getPlatformTag(userAgent) + ); + + Metrics.counter(RECAPTCHA_ATTEMPT_COUNTER_NAME, tags).increment(); + + if (challengeSuccess) { + rateLimiters.getRecaptchaChallengeSuccessLimiter().validate(account.getUuid()); + resetRateLimits(account, ChallengeType.CAPTCHA); + } + return challengeSuccess; + } + + private void resetRateLimits(final Account account, final ChallengeType type) throws RateLimitExceededException { + try { + rateLimiters.getRateLimitResetLimiter().validate(account.getUuid()); + } catch (final RateLimitExceededException e) { + Metrics.counter(RESET_RATE_LIMIT_EXCEEDED_COUNTER_NAME, + SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.getNumber())).increment(); + + throw e; + } + + rateLimitChallengeListeners.forEach(listener -> listener.handleRateLimitChallengeAnswered(account, type)); + } + + public void sendPushChallenge(final Account account) throws NotPushRegisteredException { + pushChallengeManager.sendChallenge(account); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManager.java new file mode 100644 index 000000000..7918c728c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManager.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import java.util.ArrayList; +import java.util.List; +import org.whispersystems.textsecuregcm.storage.Account; + +public class RateLimitChallengeOptionManager { + + private final RateLimiters rateLimiters; + + public static final String OPTION_RECAPTCHA = "recaptcha"; + public static final String OPTION_PUSH_CHALLENGE = "pushChallenge"; + + public RateLimitChallengeOptionManager(final RateLimiters rateLimiters) { + this.rateLimiters = rateLimiters; + } + + public List getChallengeOptions(final Account account) { + final List options = new ArrayList<>(2); + + if (rateLimiters.getRecaptchaChallengeAttemptLimiter().hasAvailablePermits(account.getUuid(), 1) && + rateLimiters.getRecaptchaChallengeSuccessLimiter().hasAvailablePermits(account.getUuid(), 1)) { + + options.add(OPTION_RECAPTCHA); + } + + if (rateLimiters.getPushChallengeAttemptLimiter().hasAvailablePermits(account.getUuid(), 1) && + rateLimiters.getPushChallengeSuccessLimiter().hasAvailablePermits(account.getUuid(), 1)) { + + options.add(OPTION_PUSH_CHALLENGE); + } + + return options; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitedByIp.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitedByIp.java new file mode 100644 index 000000000..a06c29991 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitedByIp.java @@ -0,0 +1,20 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RateLimitedByIp { + + RateLimiters.For value(); + + boolean failOnUnresolvedIp() default true; +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiter.java new file mode 100644 index 000000000..98fa8a362 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiter.java @@ -0,0 +1,98 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import java.util.UUID; +import java.util.concurrent.CompletionStage; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import reactor.core.publisher.Mono; + +public interface RateLimiter { + + void validate(String key, int amount) throws RateLimitExceededException; + + CompletionStage validateAsync(String key, int amount); + + boolean hasAvailablePermits(String key, int permits); + + CompletionStage hasAvailablePermitsAsync(String key, int amount); + + void clear(String key); + + CompletionStage clearAsync(String key); + + RateLimiterConfig config(); + + default void validate(final String key) throws RateLimitExceededException { + validate(key, 1); + } + + default void validate(final UUID accountUuid) throws RateLimitExceededException { + validate(accountUuid.toString()); + } + + default void validate(final UUID accountUuid, final int permits) throws RateLimitExceededException { + validate(accountUuid.toString(), permits); + } + + default void validate(final UUID srcAccountUuid, final UUID dstAccountUuid) throws RateLimitExceededException { + validate(srcAccountUuid.toString() + "__" + dstAccountUuid.toString()); + } + + default CompletionStage validateAsync(final String key) { + return validateAsync(key, 1); + } + + default CompletionStage validateAsync(final UUID accountUuid) { + return validateAsync(accountUuid.toString()); + } + + default CompletionStage validateAsync(final UUID srcAccountUuid, final UUID dstAccountUuid) { + return validateAsync(srcAccountUuid.toString() + "__" + dstAccountUuid.toString()); + } + + default Mono validateReactive(final String key) { + return Mono.fromFuture(() -> validateAsync(key).toCompletableFuture()); + } + + default Mono validateReactive(final UUID accountUuid) { + return validateReactive(accountUuid.toString()); + } + + default boolean hasAvailablePermits(final UUID accountUuid, final int permits) { + return hasAvailablePermits(accountUuid.toString(), permits); + } + + default CompletionStage hasAvailablePermitsAsync(final UUID accountUuid, final int permits) { + return hasAvailablePermitsAsync(accountUuid.toString(), permits); + } + + default void clear(final UUID accountUuid) { + clear(accountUuid.toString()); + } + + default CompletionStage clearAsync(final UUID accountUuid) { + return clearAsync(accountUuid.toString()); + } + + /** + * If the wrapped {@code validate()} call throws a {@link RateLimitExceededException}, it will adapt it to ensure that + * {@link RateLimitExceededException#isLegacy()} returns {@code false} + */ + static void adaptLegacyException(final RateLimitValidator validator) throws RateLimitExceededException { + try { + validator.validate(); + } catch (final RateLimitExceededException e) { + throw new RateLimitExceededException(e.getRetryDuration().orElse(null), false); + } + } + + @FunctionalInterface + interface RateLimitValidator { + + void validate() throws RateLimitExceededException; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiterConfig.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiterConfig.java new file mode 100644 index 000000000..04c9afd3a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiterConfig.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import javax.validation.constraints.AssertTrue; +import java.time.Duration; + +public record RateLimiterConfig(int bucketSize, Duration permitRegenerationDuration) { + + public double leakRatePerMillis() { + return 1.0 / (permitRegenerationDuration.toNanos() / 1e6); + } + + @AssertTrue + public boolean hasPositiveRegenerationRate() { + try { + return permitRegenerationDuration.toNanos() > 0; + } catch (final ArithmeticException e) { + // The duration was too large to fit in a long, so it's definitely positive + return true; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiterDescriptor.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiterDescriptor.java new file mode 100644 index 000000000..38ae87b80 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiterDescriptor.java @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +/** + * Represents an information that defines a rate limiter. + */ +public interface RateLimiterDescriptor { + /** + * Implementing classes will likely be Enums, so name is chosen not to clash with {@link Enum#name()}. + * @return id of this rate limiter to be used in `yml` config files and as a part of the bucket key. + */ + String id(); + + /** + * @return {@code true} if this rate limiter needs to watch for dynamic configuration changes. + */ + boolean isDynamic(); + + /** + * @return an instance of {@link RateLimiterConfig} to be used by default, + * i.e. if there is no overrides in the application configuration files (static or dynamic). + */ + RateLimiterConfig defaultConfig(); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java new file mode 100644 index 000000000..bb6f8f23a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java @@ -0,0 +1,221 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.limits; + + +import com.google.common.annotations.VisibleForTesting; +import java.time.Clock; +import java.time.Duration; +import java.util.Map; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.redis.ClusterLuaScript; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; + +public class RateLimiters extends BaseRateLimiters { + + public enum For implements RateLimiterDescriptor { + BACKUP_AUTH_CHECK("backupAuthCheck", false, new RateLimiterConfig(100, Duration.ofMinutes(15))), + SMS_DESTINATION("smsDestination", false, new RateLimiterConfig(2, Duration.ofSeconds(30))), + VOICE_DESTINATION("voxDestination", false, new RateLimiterConfig(2, Duration.ofMinutes(2))), + VOICE_DESTINATION_DAILY("voxDestinationDaily", false, new RateLimiterConfig(10, Duration.ofMinutes(144))), + SMS_VOICE_IP("smsVoiceIp", false, new RateLimiterConfig(1000, Duration.ofMillis(60))), + SMS_VOICE_PREFIX("smsVoicePrefix", false, new RateLimiterConfig(1000, Duration.ofMillis(60))), + VERIFY("verify", false, new RateLimiterConfig(6, Duration.ofSeconds(30))), + PIN("pin", false, new RateLimiterConfig(10, Duration.ofDays(1))), + ATTACHMENT("attachmentCreate", false, new RateLimiterConfig(50, Duration.ofMillis(1200))), + PRE_KEYS("prekeys", false, new RateLimiterConfig(6, Duration.ofMinutes(10))), + MESSAGES("messages", false, new RateLimiterConfig(60, Duration.ofSeconds(1))), + STORIES("stories", false, new RateLimiterConfig(5_000, Duration.ofSeconds(8))), + ALLOCATE_DEVICE("allocateDevice", false, new RateLimiterConfig(2, Duration.ofMinutes(2))), + VERIFY_DEVICE("verifyDevice", false, new RateLimiterConfig(6, Duration.ofMinutes(10))), + TURN("turnAllocate", false, new RateLimiterConfig(60, Duration.ofSeconds(1))), + PROFILE("profile", false, new RateLimiterConfig(4320, Duration.ofSeconds(20))), + STICKER_PACK("stickerPack", false, new RateLimiterConfig(50, Duration.ofMinutes(72))), + USERNAME_LOOKUP("usernameLookup", false, new RateLimiterConfig(100, Duration.ofMinutes(15))), + USERNAME_SET("usernameSet", false, new RateLimiterConfig(100, Duration.ofMinutes(15))), + USERNAME_RESERVE("usernameReserve", false, new RateLimiterConfig(100, Duration.ofMinutes(15))), + USERNAME_LINK_OPERATION("usernameLinkOperation", false, new RateLimiterConfig(10, Duration.ofMinutes(1))), + USERNAME_LINK_LOOKUP_PER_IP("usernameLinkLookupPerIp", false, new RateLimiterConfig(100, Duration.ofSeconds(15))), + CHECK_ACCOUNT_EXISTENCE("checkAccountExistence", false, new RateLimiterConfig(1000, Duration.ofSeconds(4))), + REGISTRATION("registration", false, new RateLimiterConfig(6, Duration.ofSeconds(30))), + VERIFICATION_PUSH_CHALLENGE("verificationPushChallenge", false, new RateLimiterConfig(5, Duration.ofSeconds(30))), + VERIFICATION_CAPTCHA("verificationCaptcha", false, new RateLimiterConfig(10, Duration.ofSeconds(30))), + RATE_LIMIT_RESET("rateLimitReset", true, new RateLimiterConfig(2, Duration.ofHours(12))), + RECAPTCHA_CHALLENGE_ATTEMPT("recaptchaChallengeAttempt", true, new RateLimiterConfig(10, Duration.ofMinutes(144))), + RECAPTCHA_CHALLENGE_SUCCESS("recaptchaChallengeSuccess", true, new RateLimiterConfig(2, Duration.ofHours(12))), + PUSH_CHALLENGE_ATTEMPT("pushChallengeAttempt", true, new RateLimiterConfig(10, Duration.ofMinutes(144))), + PUSH_CHALLENGE_SUCCESS("pushChallengeSuccess", true, new RateLimiterConfig(2, Duration.ofHours(12))), + CREATE_CALL_LINK("createCallLink", false, new RateLimiterConfig(100, Duration.ofMinutes(15))), + INBOUND_MESSAGE_BYTES("inboundMessageBytes", true, new RateLimiterConfig(128 * 1024 * 1024, Duration.ofNanos(500_000))), + EXTERNAL_SERVICE_CREDENTIALS("externalServiceCredentials", true, new RateLimiterConfig(100, Duration.ofMinutes(15))), + ; + + private final String id; + + private final boolean dynamic; + + private final RateLimiterConfig defaultConfig; + + For(final String id, final boolean dynamic, final RateLimiterConfig defaultConfig) { + this.id = id; + this.dynamic = dynamic; + this.defaultConfig = defaultConfig; + } + + public String id() { + return id; + } + + @Override + public boolean isDynamic() { + return dynamic; + } + + public RateLimiterConfig defaultConfig() { + return defaultConfig; + } + } + + public static RateLimiters createAndValidate( + final Map configs, + final DynamicConfigurationManager dynamicConfigurationManager, + final FaultTolerantRedisCluster cacheCluster) { + final RateLimiters rateLimiters = new RateLimiters( + configs, dynamicConfigurationManager, defaultScript(cacheCluster), cacheCluster, Clock.systemUTC()); + rateLimiters.validateValuesAndConfigs(); + return rateLimiters; + } + + @VisibleForTesting + RateLimiters( + final Map configs, + final DynamicConfigurationManager dynamicConfigurationManager, + final ClusterLuaScript validateScript, + final FaultTolerantRedisCluster cacheCluster, + final Clock clock) { + super(For.values(), configs, dynamicConfigurationManager, validateScript, cacheCluster, clock); + } + + public RateLimiter getAllocateDeviceLimiter() { + return forDescriptor(For.ALLOCATE_DEVICE); + } + + public RateLimiter getVerifyDeviceLimiter() { + return forDescriptor(For.VERIFY_DEVICE); + } + + public RateLimiter getMessagesLimiter() { + return forDescriptor(For.MESSAGES); + } + + public RateLimiter getPreKeysLimiter() { + return forDescriptor(For.PRE_KEYS); + } + + public RateLimiter getAttachmentLimiter() { + return forDescriptor(For.ATTACHMENT); + } + + public RateLimiter getSmsDestinationLimiter() { + return forDescriptor(For.SMS_DESTINATION); + } + + public RateLimiter getSmsVoiceIpLimiter() { + return forDescriptor(For.SMS_VOICE_IP); + } + + public RateLimiter getSmsVoicePrefixLimiter() { + return forDescriptor(For.SMS_VOICE_PREFIX); + } + + public RateLimiter getVoiceDestinationLimiter() { + return forDescriptor(For.VOICE_DESTINATION); + } + + public RateLimiter getVoiceDestinationDailyLimiter() { + return forDescriptor(For.VOICE_DESTINATION_DAILY); + } + + public RateLimiter getVerifyLimiter() { + return forDescriptor(For.VERIFY); + } + + public RateLimiter getPinLimiter() { + return forDescriptor(For.PIN); + } + + public RateLimiter getTurnLimiter() { + return forDescriptor(For.TURN); + } + + public RateLimiter getProfileLimiter() { + return forDescriptor(For.PROFILE); + } + + public RateLimiter getStickerPackLimiter() { + return forDescriptor(For.STICKER_PACK); + } + + public RateLimiter getUsernameLookupLimiter() { + return forDescriptor(For.USERNAME_LOOKUP); + } + + public RateLimiter getUsernameSetLimiter() { + return forDescriptor(For.USERNAME_SET); + } + + public RateLimiter getUsernameReserveLimiter() { + return forDescriptor(For.USERNAME_RESERVE); + } + + public RateLimiter getCheckAccountExistenceLimiter() { + return forDescriptor(For.CHECK_ACCOUNT_EXISTENCE); + } + + public RateLimiter getRegistrationLimiter() { + return forDescriptor(For.REGISTRATION); + } + + public RateLimiter getRateLimitResetLimiter() { + return forDescriptor(For.RATE_LIMIT_RESET); + } + + public RateLimiter getRecaptchaChallengeAttemptLimiter() { + return forDescriptor(For.RECAPTCHA_CHALLENGE_ATTEMPT); + } + + public RateLimiter getRecaptchaChallengeSuccessLimiter() { + return forDescriptor(For.RECAPTCHA_CHALLENGE_SUCCESS); + } + + public RateLimiter getPushChallengeAttemptLimiter() { + return forDescriptor(For.PUSH_CHALLENGE_ATTEMPT); + } + + public RateLimiter getPushChallengeSuccessLimiter() { + return forDescriptor(For.PUSH_CHALLENGE_SUCCESS); + } + + public RateLimiter getVerificationPushChallengeLimiter() { + return forDescriptor(For.VERIFICATION_PUSH_CHALLENGE); + } + + public RateLimiter getVerificationCaptchaLimiter() { + return forDescriptor(For.VERIFICATION_CAPTCHA); + } + + public RateLimiter getCreateCallLinkLimiter() { + return forDescriptor(For.CREATE_CALL_LINK); + } + + public RateLimiter getInboundMessageBytes() { + return forDescriptor(For.INBOUND_MESSAGE_BYTES); + } + + public RateLimiter getStoriesLimiter() { + return forDescriptor(For.STORIES); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/StaticRateLimiter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/StaticRateLimiter.java new file mode 100644 index 000000000..5d3560ead --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/limits/StaticRateLimiter.java @@ -0,0 +1,171 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.limits; + +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.concurrent.CompletableFuture.failedFuture; + +import com.google.common.annotations.VisibleForTesting; +import io.lettuce.core.RedisException; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import java.time.Clock; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CompletionStage; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.redis.ClusterLuaScript; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.util.ExceptionUtils; +import org.whispersystems.textsecuregcm.util.Util; + +public class StaticRateLimiter implements RateLimiter { + + protected final String name; + + private final RateLimiterConfig config; + + private final Counter counter; + private final DynamicConfigurationManager dynamicConfigurationManager; + + private final ClusterLuaScript validateScript; + + private final FaultTolerantRedisCluster cacheCluster; + + private final Clock clock; + + + public StaticRateLimiter( + final String name, + final RateLimiterConfig config, + final ClusterLuaScript validateScript, + final FaultTolerantRedisCluster cacheCluster, + final Clock clock, + final DynamicConfigurationManager dynamicConfigurationManager) { + this.name = requireNonNull(name); + this.config = requireNonNull(config); + this.validateScript = requireNonNull(validateScript); + this.cacheCluster = requireNonNull(cacheCluster); + this.clock = requireNonNull(clock); + this.counter = Metrics.counter(MetricsUtil.name(getClass(), "exceeded"), "name", name); + this.dynamicConfigurationManager = dynamicConfigurationManager; + } + + @Override + public void validate(final String key, final int amount) throws RateLimitExceededException { + try { + final long deficitPermitsAmount = executeValidateScript(key, amount, true); + if (deficitPermitsAmount > 0) { + counter.increment(); + final Duration retryAfter = Duration.ofMillis( + (long) Math.ceil((double) deficitPermitsAmount / config.leakRatePerMillis())); + throw new RateLimitExceededException(retryAfter, true); + } + } catch (RedisException e) { + if (!failOpen()) { + throw e; + } + } + } + + @Override + public CompletionStage validateAsync(final String key, final int amount) { + return executeValidateScriptAsync(key, amount, true) + .thenCompose(deficitPermitsAmount -> { + if (deficitPermitsAmount == 0) { + return completedFuture((Void) null); + } + counter.increment(); + final Duration retryAfter = Duration.ofMillis( + (long) Math.ceil((double) deficitPermitsAmount / config.leakRatePerMillis())); + return failedFuture(new RateLimitExceededException(retryAfter, true)); + }) + .exceptionally(throwable -> { + if (ExceptionUtils.unwrap(throwable) instanceof RedisException && failOpen()) { + return null; + } + throw ExceptionUtils.wrap(throwable); + }); + } + + @Override + public boolean hasAvailablePermits(final String key, final int amount) { + try { + final long deficitPermitsAmount = executeValidateScript(key, amount, false); + return deficitPermitsAmount == 0; + } catch (RedisException e) { + if (failOpen()) { + return true; + } else { + throw e; + } + } + } + + @Override + public CompletionStage hasAvailablePermitsAsync(final String key, final int amount) { + return executeValidateScriptAsync(key, amount, false) + .thenApply(deficitPermitsAmount -> deficitPermitsAmount == 0) + .exceptionally(throwable -> { + if (ExceptionUtils.unwrap(throwable) instanceof RedisException && failOpen()) { + return true; + } + throw ExceptionUtils.wrap(throwable); + }); + } + + @Override + public void clear(final String key) { + cacheCluster.useCluster(connection -> connection.sync().del(bucketName(name, key))); + } + + @Override + public CompletionStage clearAsync(final String key) { + return cacheCluster.withCluster(connection -> connection.async().del(bucketName(name, key))) + .thenRun(Util.NOOP); + } + + @Override + public RateLimiterConfig config() { + return config; + } + + private boolean failOpen() { + return this.dynamicConfigurationManager.getConfiguration().getRateLimitPolicy().failOpen(); + } + + private long executeValidateScript(final String key, final int amount, final boolean applyChanges) { + final List keys = List.of(bucketName(name, key)); + final List arguments = List.of( + String.valueOf(config.bucketSize()), + String.valueOf(config.leakRatePerMillis()), + String.valueOf(clock.millis()), + String.valueOf(amount), + String.valueOf(applyChanges) + ); + return (Long) validateScript.execute(keys, arguments); + } + + private CompletionStage executeValidateScriptAsync(final String key, final int amount, final boolean applyChanges) { + final List keys = List.of(bucketName(name, key)); + final List arguments = List.of( + String.valueOf(config.bucketSize()), + String.valueOf(config.leakRatePerMillis()), + String.valueOf(clock.millis()), + String.valueOf(amount), + String.valueOf(applyChanges) + ); + return validateScript.executeAsync(keys, arguments).thenApply(o -> (Long) o); + } + + @VisibleForTesting + protected static String bucketName(final String name, final String key) { + return "leaky_bucket::" + name + "::" + key; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/CompletionExceptionMapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/CompletionExceptionMapper.java new file mode 100644 index 000000000..a6eb2396b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/CompletionExceptionMapper.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.mappers; + +import java.util.Optional; +import java.util.concurrent.CompletionException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import org.glassfish.jersey.spi.ExceptionMappers; + +@Provider +public class CompletionExceptionMapper implements ExceptionMapper { + + @Context + private ExceptionMappers exceptionMappers; + + @Override + public Response toResponse(final CompletionException exception) { + final Throwable cause = exception.getCause(); + + if (cause != null) { + + final ExceptionMapper exceptionMapper = exceptionMappers.findMapping(cause); + + // some exception mappers, like LoggingExceptionMapper, have side effects (e.g., logging) + // so we always build their response… + final Response exceptionMapperResponse = exceptionMapper.toResponse(cause); + + final Optional webApplicationExceptionResponse; + if (cause instanceof WebApplicationException webApplicationException) { + webApplicationExceptionResponse = Optional.of(webApplicationException.getResponse()); + } else { + webApplicationExceptionResponse = Optional.empty(); + } + + // …but if the exception was a WebApplicationException, and provides an entity, we want to keep it + return webApplicationExceptionResponse + .filter(Response::hasEntity) + .orElse(exceptionMapperResponse); + } + + return Response.serverError().build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/DeviceLimitExceededExceptionMapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/DeviceLimitExceededExceptionMapper.java new file mode 100644 index 000000000..3252c5665 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/DeviceLimitExceededExceptionMapper.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.mappers; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.whispersystems.textsecuregcm.controllers.DeviceLimitExceededException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class DeviceLimitExceededExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(DeviceLimitExceededException exception) { + return Response.status(411) + .entity(new DeviceLimitExceededDetails(exception.getCurrentDevices(), + exception.getMaxDevices())) + .build(); + } + + private static class DeviceLimitExceededDetails { + @JsonProperty + private int current; + @JsonProperty + private int max; + + public DeviceLimitExceededDetails(int current, int max) { + this.current = current; + this.max = max; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapper.java new file mode 100644 index 000000000..c57d16755 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.mappers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import java.io.IOException; + +@Provider +public class IOExceptionMapper implements ExceptionMapper { + + private final Logger logger = LoggerFactory.getLogger(IOExceptionMapper.class); + + @Override + public Response toResponse(IOException e) { + if (!(e.getCause() instanceof java.util.concurrent.TimeoutException)) { + logger.warn("IOExceptionMapper", e); + } + return Response.status(503).build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/ImpossiblePhoneNumberExceptionMapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/ImpossiblePhoneNumberExceptionMapper.java new file mode 100644 index 000000000..edbd670fa --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/ImpossiblePhoneNumberExceptionMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.mappers; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import org.whispersystems.textsecuregcm.util.ImpossiblePhoneNumberException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +public class ImpossiblePhoneNumberExceptionMapper implements ExceptionMapper { + + private static final Counter IMPOSSIBLE_NUMBER_COUNTER = + Metrics.counter(name(ImpossiblePhoneNumberExceptionMapper.class, "impossibleNumbers")); + + @Override + public Response toResponse(final ImpossiblePhoneNumberException exception) { + IMPOSSIBLE_NUMBER_COUNTER.increment(); + + return Response.status(Response.Status.BAD_REQUEST).build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/InvalidWebsocketAddressExceptionMapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/InvalidWebsocketAddressExceptionMapper.java new file mode 100644 index 000000000..5ad66661a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/InvalidWebsocketAddressExceptionMapper.java @@ -0,0 +1,20 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.mappers; + +import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class InvalidWebsocketAddressExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(InvalidWebsocketAddressException exception) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/JsonMappingExceptionMapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/JsonMappingExceptionMapper.java new file mode 100644 index 000000000..515a27296 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/JsonMappingExceptionMapper.java @@ -0,0 +1,12 @@ +package org.whispersystems.textsecuregcm.mappers; + +import com.fasterxml.jackson.databind.JsonMappingException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +public class JsonMappingExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(final JsonMappingException exception) { + return Response.status(422).build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/NonNormalizedPhoneNumberExceptionMapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/NonNormalizedPhoneNumberExceptionMapper.java new file mode 100644 index 000000000..9a4b26b9f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/NonNormalizedPhoneNumberExceptionMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.mappers; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import io.micrometer.core.instrument.Metrics; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import org.whispersystems.textsecuregcm.util.NonNormalizedPhoneNumberException; + +public class NonNormalizedPhoneNumberExceptionMapper implements ExceptionMapper { + + private static final String NON_NORMALIZED_NUMBER_COUNTER_NAME = + name(NonNormalizedPhoneNumberExceptionMapper.class, "nonNormalizedNumbers"); + + @Override + public Response toResponse(final NonNormalizedPhoneNumberException exception) { + String countryCode; + + try { + countryCode = + String.valueOf(PhoneNumberUtil.getInstance().parse(exception.getOriginalNumber(), null).getCountryCode()); + } catch (final NumberParseException ignored) { + countryCode = "unknown"; + } + + Metrics.counter(NON_NORMALIZED_NUMBER_COUNTER_NAME, "countryCode", countryCode).increment(); + + return Response.status(Status.BAD_REQUEST) + .entity(new NonNormalizedPhoneNumberResponse(exception.getOriginalNumber(), exception.getNormalizedNumber())) + .build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/NonNormalizedPhoneNumberResponse.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/NonNormalizedPhoneNumberResponse.java new file mode 100644 index 000000000..155ae73bf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/NonNormalizedPhoneNumberResponse.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.mappers; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class NonNormalizedPhoneNumberResponse { + + private final String originalNumber; + private final String normalizedNumber; + + @JsonCreator + NonNormalizedPhoneNumberResponse(@JsonProperty("originalNumber") final String originalNumber, + @JsonProperty("normalizedNumber") final String normalizedNumber) { + + this.originalNumber = originalNumber; + this.normalizedNumber = normalizedNumber; + } + + public String getOriginalNumber() { + return originalNumber; + } + + public String getNormalizedNumber() { + return normalizedNumber; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/RateLimitExceededExceptionMapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/RateLimitExceededExceptionMapper.java new file mode 100644 index 000000000..3202933b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/RateLimitExceededExceptionMapper.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.mappers; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; + +@Provider +public class RateLimitExceededExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(RateLimitExceededExceptionMapper.class); + + private static final int LEGACY_STATUS_CODE = 413; + private static final int STATUS_CODE = 429; + + /** + * Convert a RateLimitExceededException to a {@value STATUS_CODE} (or legacy {@value LEGACY_STATUS_CODE}) response + * with a Retry-After header. + * + * @param e A RateLimitExceededException potentially containing a recommended retry duration + * @return the response + */ + @Override + public Response toResponse(RateLimitExceededException e) { + final int statusCode = e.isLegacy() ? LEGACY_STATUS_CODE : STATUS_CODE; + return e.getRetryDuration() + .filter(d -> { + if (d.isNegative()) { + logger.warn("Encountered a negative retry duration: {}, will not include a Retry-After header in response", + d); + } + // only include non-negative durations in retry headers + return !d.isNegative(); + }) + .map(d -> Response.status(statusCode).header("Retry-After", d.toSeconds())) + .orElseGet(() -> Response.status(statusCode)).build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/RegistrationServiceSenderExceptionMapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/RegistrationServiceSenderExceptionMapper.java new file mode 100644 index 000000000..8c753b274 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/RegistrationServiceSenderExceptionMapper.java @@ -0,0 +1,29 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.mappers; + +import com.google.common.annotations.VisibleForTesting; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException; + +public class RegistrationServiceSenderExceptionMapper implements ExceptionMapper { + + public static int REMOTE_SERVICE_REJECTED_REQUEST_STATUS = 440; + + @Override + public Response toResponse(final RegistrationServiceSenderException exception) { + return Response.status(REMOTE_SERVICE_REJECTED_REQUEST_STATUS) + .entity(new SendVerificationCodeFailureResponse(exception.getReason(), exception.isPermanent())) + .build(); + } + + @VisibleForTesting + public record SendVerificationCodeFailureResponse(RegistrationServiceSenderException.Reason reason, + boolean permanentFailure) { + + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/ServerRejectedExceptionMapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/ServerRejectedExceptionMapper.java new file mode 100644 index 000000000..37f8787e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/ServerRejectedExceptionMapper.java @@ -0,0 +1,18 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.mappers; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import org.whispersystems.textsecuregcm.controllers.ServerRejectedException; + +public class ServerRejectedExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(final ServerRejectedException exception) { + return Response.status(508).build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/SubscriptionProcessorExceptionMapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/SubscriptionProcessorExceptionMapper.java new file mode 100644 index 000000000..73571ac50 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/mappers/SubscriptionProcessorExceptionMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.mappers; + +import java.util.Map; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorException; + +public class SubscriptionProcessorExceptionMapper implements ExceptionMapper { + + public static final int EXTERNAL_SERVICE_ERROR_STATUS_CODE = 440; + + @Override + public Response toResponse(final SubscriptionProcessorException exception) { + return Response.status(EXTERNAL_SERVICE_ERROR_STATUS_CODE) + .entity(Map.of( + "processor", exception.getProcessor().name(), + "chargeFailure", exception.getChargeFailure() + )) + .build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/ApplicationShutdownMonitor.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/ApplicationShutdownMonitor.java new file mode 100644 index 000000000..b9f13df8a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/ApplicationShutdownMonitor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + + +import static com.codahale.metrics.MetricRegistry.name; + +import io.dropwizard.lifecycle.Managed; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A managed monitor that reports whether the application is shutting down as a metric. That metric can then be used in + * conjunction with other indicators to conditionally fire or suppress alerts. + */ +public class ApplicationShutdownMonitor implements Managed { + + private final AtomicBoolean shuttingDown = new AtomicBoolean(false); + + public ApplicationShutdownMonitor(final MeterRegistry meterRegistry) { + // without a strong reference to the gauge’s value supplier, shutdown garbage collection + // might prevent the final value from being reported + Gauge.builder(name(getClass().getSimpleName(), "shuttingDown"), () -> shuttingDown.get() ? 1 : 0) + .strongReference(true) + .register(meterRegistry); + } + + @Override + public void start() throws Exception { + shuttingDown.set(false); + } + + @Override + public void stop() throws Exception { + shuttingDown.set(true); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/BufferPoolGauges.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/BufferPoolGauges.java new file mode 100644 index 000000000..4f6ea36ff --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/BufferPoolGauges.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; + +import java.lang.management.BufferPoolMXBean; +import java.lang.management.ManagementFactory; +import java.util.List; + +import static com.codahale.metrics.MetricRegistry.name; + +public class BufferPoolGauges { + + private BufferPoolGauges() {} + + public static void registerMetrics() { + for (final BufferPoolMXBean bufferPoolMXBean : ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class)) { + final List tags = List.of(Tag.of("name", bufferPoolMXBean.getName())); + + Metrics.gauge(name(BufferPoolGauges.class, "count"), tags, bufferPoolMXBean, BufferPoolMXBean::getCount); + Metrics.gauge(name(BufferPoolGauges.class, "memory_used"), tags, bufferPoolMXBean, BufferPoolMXBean::getMemoryUsed); + Metrics.gauge(name(BufferPoolGauges.class, "total_capacity"), tags, bufferPoolMXBean, BufferPoolMXBean::getTotalCapacity); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/CpuUsageGauge.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/CpuUsageGauge.java new file mode 100644 index 000000000..df40e80ce --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/CpuUsageGauge.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import com.codahale.metrics.CachedGauge; +import com.sun.management.OperatingSystemMXBean; + +import java.lang.management.ManagementFactory; +import java.util.concurrent.TimeUnit; + +public class CpuUsageGauge extends CachedGauge { + + private final OperatingSystemMXBean operatingSystemMXBean; + + public CpuUsageGauge(final long timeout, final TimeUnit timeoutUnit) { + super(timeout, timeoutUnit); + + this.operatingSystemMXBean = (com.sun.management.OperatingSystemMXBean) + ManagementFactory.getOperatingSystemMXBean(); + } + + @Override + protected Integer loadValue() { + return (int) Math.ceil(operatingSystemMXBean.getCpuLoad() * 100); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/FileDescriptorGauge.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/FileDescriptorGauge.java new file mode 100644 index 000000000..472e2f560 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/FileDescriptorGauge.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + + +import com.codahale.metrics.Gauge; + +import java.io.File; + +public class FileDescriptorGauge implements Gauge { + @Override + public Integer getValue() { + File file = new File("/proc/self/fd"); + + if (file.isDirectory() && file.exists()) { + return file.list().length; + } + + return 0; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/FreeMemoryGauge.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/FreeMemoryGauge.java new file mode 100644 index 000000000..a04330b36 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/FreeMemoryGauge.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import com.codahale.metrics.Gauge; +import com.sun.management.OperatingSystemMXBean; +import java.lang.management.ManagementFactory; + +public class FreeMemoryGauge implements Gauge { + + private final OperatingSystemMXBean operatingSystemMXBean; + + public FreeMemoryGauge() { + this.operatingSystemMXBean = (com.sun.management.OperatingSystemMXBean) + ManagementFactory.getOperatingSystemMXBean(); + } + + @Override + public Long getValue() { + return operatingSystemMXBean.getFreeMemorySize(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/GarbageCollectionGauges.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/GarbageCollectionGauges.java new file mode 100644 index 000000000..5eca2d5fd --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/GarbageCollectionGauges.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.util.List; + +import static com.codahale.metrics.MetricRegistry.name; + +public class GarbageCollectionGauges { + + private GarbageCollectionGauges() {} + + public static void registerMetrics() { + for (final GarbageCollectorMXBean garbageCollectorMXBean : ManagementFactory.getGarbageCollectorMXBeans()) { + final List tags = List.of(Tag.of("name", garbageCollectorMXBean.getName())); + + Metrics.gauge(name(GarbageCollectionGauges.class, "collection_count"), tags, garbageCollectorMXBean, GarbageCollectorMXBean::getCollectionCount); + Metrics.gauge(name(GarbageCollectionGauges.class, "collection_time"), tags, garbageCollectorMXBean, GarbageCollectorMXBean::getCollectionTime); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/LogstashTcpSocketAppenderFactory.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/LogstashTcpSocketAppenderFactory.java new file mode 100644 index 000000000..aa94b2e44 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/LogstashTcpSocketAppenderFactory.java @@ -0,0 +1,117 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.PatternLayout; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.encoder.LayoutWrappingEncoder; +import ch.qos.logback.core.helpers.NOPAppender; +import ch.qos.logback.core.net.ssl.SSLConfiguration; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import io.dropwizard.logging.AbstractAppenderFactory; +import io.dropwizard.logging.async.AsyncAppenderFactory; +import io.dropwizard.logging.filter.LevelFilterFactory; +import io.dropwizard.logging.layout.LayoutFactory; +import java.time.Duration; +import java.util.Optional; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import net.logstash.logback.appender.LogstashTcpSocketAppender; +import net.logstash.logback.encoder.LogstashEncoder; +import org.whispersystems.textsecuregcm.WhisperServerVersion; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; +import org.whispersystems.textsecuregcm.util.HostnameUtil; + +@JsonTypeName("logstashtcpsocket") +public class LogstashTcpSocketAppenderFactory extends AbstractAppenderFactory { + + @JsonProperty + private String destination; + + @JsonProperty + private Duration keepAlive = Duration.ofSeconds(20); + + @JsonProperty + @NotNull + private SecretString apiKey; + + @JsonProperty + private String environment; + + @JsonProperty + @NotEmpty + public String getDestination() { + return destination; + } + + @JsonProperty + public Duration getKeepAlive() { + return keepAlive; + } + + @JsonProperty + public SecretString getApiKey() { + return apiKey; + } + + @JsonProperty + @NotEmpty + public String getEnvironment() { + return environment; + } + + @Override + public Appender build( + final LoggerContext context, + final String applicationName, + final LayoutFactory layoutFactory, + final LevelFilterFactory levelFilterFactory, + final AsyncAppenderFactory asyncAppenderFactory) { + + final boolean disableLogstashTcpSocketAppender = Optional.ofNullable( + System.getenv("SIGNAL_DISABLE_LOGSTASH_TCP_SOCKET_APPENDER")) + .isPresent(); + + if (disableLogstashTcpSocketAppender) { + return new NOPAppender<>(); + } + + final SSLConfiguration sslConfiguration = new SSLConfiguration(); + final LogstashTcpSocketAppender appender = new LogstashTcpSocketAppender(); + appender.setName("logstashtcpsocket-appender"); + appender.setContext(context); + appender.setSsl(sslConfiguration); + appender.addDestination(destination); + appender.setKeepAliveDuration(new ch.qos.logback.core.util.Duration(keepAlive.toMillis())); + + final LogstashEncoder encoder = new LogstashEncoder(); + final ObjectNode customFieldsNode = new ObjectNode(JsonNodeFactory.instance); + customFieldsNode.set("host", TextNode.valueOf(HostnameUtil.getLocalHostname())); + customFieldsNode.set("service", TextNode.valueOf("chat")); + customFieldsNode.set("ddsource", TextNode.valueOf("logstash")); + customFieldsNode.set("ddtags", TextNode.valueOf("env:" + environment + ",version:" + WhisperServerVersion.getServerVersion())); + + encoder.setCustomFields(customFieldsNode.toString()); + final LayoutWrappingEncoder prefix = new LayoutWrappingEncoder<>(); + final PatternLayout layout = new PatternLayout(); + layout.setPattern(String.format("%s ", apiKey.value())); + prefix.setLayout(layout); + encoder.setPrefix(prefix); + appender.setEncoder(encoder); + + appender.addFilter(levelFilterFactory.build(threshold)); + getFilterFactories().forEach(f -> appender.addFilter(f.build())); + appender.start(); + + return wrapAsync(appender, asyncAppenderFactory); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MaxFileDescriptorGauge.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MaxFileDescriptorGauge.java new file mode 100644 index 000000000..397500859 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MaxFileDescriptorGauge.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import com.codahale.metrics.Gauge; +import com.sun.management.UnixOperatingSystemMXBean; + +import java.lang.management.ManagementFactory; + +/** + * A gauge that reports the maximum number of file descriptors allowed by the operating system. + */ +public class MaxFileDescriptorGauge implements Gauge { + + private final UnixOperatingSystemMXBean unixOperatingSystemMXBean; + + public MaxFileDescriptorGauge() { + this.unixOperatingSystemMXBean = (UnixOperatingSystemMXBean)ManagementFactory.getOperatingSystemMXBean(); + } + + @Override + public Long getValue() { + return unixOperatingSystemMXBean.getMaxFileDescriptorCount(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MessageMetrics.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MessageMetrics.java new file mode 100644 index 000000000..2e8502f92 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MessageMetrics.java @@ -0,0 +1,76 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; + +public final class MessageMetrics { + + private static final Logger logger = LoggerFactory.getLogger(MessageMetrics.class); + + private static final String MISMATCHED_ACCOUNT_ENVELOPE_UUID_COUNTER_NAME = name(MessageMetrics.class, + "mismatchedAccountEnvelopeUuid"); + + public static final String DELIVERY_LATENCY_TIMER_NAME = name(MessageMetrics.class, "deliveryLatency"); + + public static void measureAccountOutgoingMessageUuidMismatches(final Account account, + final OutgoingMessageEntity outgoingMessage) { + measureAccountDestinationUuidMismatches(account, outgoingMessage.destinationUuid()); + } + + public static void measureAccountEnvelopeUuidMismatches(final Account account, + final MessageProtos.Envelope envelope) { + if (envelope.hasDestinationUuid()) { + try { + measureAccountDestinationUuidMismatches(account, ServiceIdentifier.valueOf(envelope.getDestinationUuid())); + } catch (final IllegalArgumentException ignored) { + logger.warn("Envelope had invalid destination UUID: {}", envelope.getDestinationUuid()); + } + } + } + + private static void measureAccountDestinationUuidMismatches(final Account account, final ServiceIdentifier destinationIdentifier) { + if (!account.isIdentifiedBy(destinationIdentifier)) { + // In all cases, this represents a mismatch between the account’s current PNI and its PNI when the message was + // sent. This is an expected case, but if this metric changes significantly, it could indicate an issue to + // investigate. + Metrics.counter(MISMATCHED_ACCOUNT_ENVELOPE_UUID_COUNTER_NAME).increment(); + } + } + + public static void measureOutgoingMessageLatency(final long serverTimestamp, + final String channel, + final String userAgent, + final ClientReleaseManager clientReleaseManager) { + + final List tags = new ArrayList<>(3); + tags.add(UserAgentTagUtil.getPlatformTag(userAgent)); + tags.add(Tag.of("channel", channel)); + + UserAgentTagUtil.getClientVersionTag(userAgent, clientReleaseManager).ifPresent(tags::add); + + Timer.builder(DELIVERY_LATENCY_TIMER_NAME) + .publishPercentileHistogram(true) + .tags(tags) + .register(Metrics.globalRegistry) + .record(Duration.between(Instant.ofEpochMilli(serverTimestamp), Instant.now())); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsApplicationEventListener.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsApplicationEventListener.java new file mode 100644 index 000000000..62b048277 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsApplicationEventListener.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; + +/** + * Delegates request events to a listener that captures and reports request-level metrics. + */ +public class MetricsApplicationEventListener implements ApplicationEventListener { + + private final MetricsRequestEventListener metricsRequestEventListener; + + public MetricsApplicationEventListener(final TrafficSource trafficSource, final ClientReleaseManager clientReleaseManager) { + this.metricsRequestEventListener = new MetricsRequestEventListener(trafficSource, clientReleaseManager); + } + + @Override + public void onEvent(final ApplicationEvent event) { + } + + @Override + public RequestEventListener onRequest(final RequestEvent requestEvent) { + return metricsRequestEventListener; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListener.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListener.java new file mode 100644 index 000000000..988d16a96 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListener.java @@ -0,0 +1,90 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.HttpHeaders; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; +import org.whispersystems.textsecuregcm.util.logging.UriInfoUtil; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +/** + * Gathers and reports request-level metrics. + */ +public class MetricsRequestEventListener implements RequestEventListener { + + private final ClientReleaseManager clientReleaseManager; + + public static final String REQUEST_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "request"); + public static final String REQUESTS_BY_VERSION_COUNTER_NAME = MetricRegistry.name(MetricsRequestEventListener.class, "requestByVersion"); + + @VisibleForTesting + static final String PATH_TAG = "path"; + + @VisibleForTesting + static final String METHOD_TAG = "method"; + + @VisibleForTesting + static final String STATUS_CODE_TAG = "status"; + + @VisibleForTesting + static final String TRAFFIC_SOURCE_TAG = "trafficSource"; + + private final TrafficSource trafficSource; + private final MeterRegistry meterRegistry; + + public MetricsRequestEventListener(final TrafficSource trafficSource, final ClientReleaseManager clientReleaseManager) { + this(trafficSource, Metrics.globalRegistry, clientReleaseManager); + } + + @VisibleForTesting + MetricsRequestEventListener(final TrafficSource trafficSource, + final MeterRegistry meterRegistry, + final ClientReleaseManager clientReleaseManager) { + + this.trafficSource = trafficSource; + this.meterRegistry = meterRegistry; + this.clientReleaseManager = clientReleaseManager; + } + + @Override + public void onEvent(final RequestEvent event) { + if (event.getType() == RequestEvent.Type.FINISHED) { + if (!event.getUriInfo().getMatchedTemplates().isEmpty()) { + final List tags = new ArrayList<>(5); + tags.add(Tag.of(PATH_TAG, UriInfoUtil.getPathTemplate(event.getUriInfo()))); + tags.add(Tag.of(METHOD_TAG, event.getContainerRequest().getMethod())); + tags.add(Tag.of(STATUS_CODE_TAG, String.valueOf(event.getContainerResponse().getStatus()))); + tags.add(Tag.of(TRAFFIC_SOURCE_TAG, trafficSource.name().toLowerCase())); + + @Nullable final String userAgent; + { + final List userAgentValues = event.getContainerRequest().getRequestHeader(HttpHeaders.USER_AGENT); + userAgent = userAgentValues != null && !userAgentValues.isEmpty() ? userAgentValues.get(0) : null; + } + + tags.add(UserAgentTagUtil.getPlatformTag(userAgent)); + + meterRegistry.counter(REQUEST_COUNTER_NAME, tags).increment(); + + UserAgentTagUtil.getClientVersionTag(userAgent, clientReleaseManager) + .ifPresent(clientVersionTag -> meterRegistry.counter(REQUESTS_BY_VERSION_COUNTER_NAME, + Tags.of(clientVersionTag, UserAgentTagUtil.getPlatformTag(userAgent))) + .increment()); + } + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsUtil.java new file mode 100644 index 000000000..d1e2af7d5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MetricsUtil.java @@ -0,0 +1,115 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import com.codahale.metrics.SharedMetricRegistries; +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.setup.Environment; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.config.MeterFilter; +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; +import io.micrometer.statsd.StatsdMeterRegistry; +import java.util.concurrent.TimeUnit; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.WhisperServerVersion; +import org.whispersystems.textsecuregcm.push.PushLatencyManager; +import org.whispersystems.textsecuregcm.util.Constants; +import org.whispersystems.textsecuregcm.util.HostnameUtil; + +public class MetricsUtil { + + public static final String PREFIX = "chat"; + + /** + * Returns a dot-separated ('.') name for the given class and name parts + */ + public static String name(Class clazz, String... parts) { + return name(clazz.getSimpleName(), parts); + } + + private static String name(String name, String... parts) { + final StringBuilder sb = new StringBuilder(PREFIX); + sb.append(".").append(name); + for (String part : parts) { + sb.append(".").append(part); + } + return sb.toString(); + } + + public static void configureRegistries(final WhisperServerConfiguration config, final Environment environment) { + SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics()); + + { + final StatsdMeterRegistry dogstatsdMeterRegistry = new StatsdMeterRegistry( + config.getDatadogConfiguration(), io.micrometer.core.instrument.Clock.SYSTEM); + + dogstatsdMeterRegistry.config().commonTags( + Tags.of( + "service", "chat", + "host", HostnameUtil.getLocalHostname(), + "version", WhisperServerVersion.getServerVersion(), + "env", config.getDatadogConfiguration().getEnvironment())); + + configureMeterFilters(dogstatsdMeterRegistry.config()); + Metrics.addRegistry(dogstatsdMeterRegistry); + } + + environment.lifecycle().manage(new MicrometerRegistryManager(Metrics.globalRegistry)); + environment.lifecycle().manage(new ApplicationShutdownMonitor(Metrics.globalRegistry)); + } + + @VisibleForTesting + static MeterRegistry.Config configureMeterFilters(MeterRegistry.Config config) { + final DistributionStatisticConfig defaultDistributionStatisticConfig = DistributionStatisticConfig.builder() + .percentiles(.75, .95, .99, .999) + .build(); + + return config + .meterFilter(new MeterFilter() { + @Override + public DistributionStatisticConfig configure(final Meter.Id id, final DistributionStatisticConfig config) { + return defaultDistributionStatisticConfig.merge(config); + } + }) + // Remove high-cardinality `command` and `remote` tags from Lettuce metrics and prepend "chat." to meter names + .meterFilter(new MeterFilter() { + @Override + public Meter.Id map(final Meter.Id id) { + if (id.getName().startsWith("lettuce")) { + return id.withName(PREFIX + "." + id.getName()) + .replaceTags(id.getTags().stream() + .filter(tag -> !"command".equals(tag.getKey())) + .filter(tag -> !"remote".equals(tag.getKey())) + .toList()); + } + + return MeterFilter.super.map(id); + } + }) + .meterFilter(MeterFilter.denyNameStartsWith(PushLatencyManager.TIMER_NAME + ".percentile")) + .meterFilter(MeterFilter.denyNameStartsWith(MessageMetrics.DELIVERY_LATENCY_TIMER_NAME + ".percentile")); + } + + public static void registerSystemResourceMetrics(final Environment environment) { + environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge(3, TimeUnit.SECONDS)); + environment.metrics().register(name(FreeMemoryGauge.class, "free_memory"), new FreeMemoryGauge()); + environment.metrics().register(name(NetworkSentGauge.class, "bytes_sent"), new NetworkSentGauge()); + environment.metrics().register(name(NetworkReceivedGauge.class, "bytes_received"), new NetworkReceivedGauge()); + environment.metrics().register(name(FileDescriptorGauge.class, "fd_count"), new FileDescriptorGauge()); + environment.metrics().register(name(MaxFileDescriptorGauge.class, "max_fd_count"), new MaxFileDescriptorGauge()); + environment.metrics() + .register(name(OperatingSystemMemoryGauge.class, "buffers"), new OperatingSystemMemoryGauge("Buffers")); + environment.metrics() + .register(name(OperatingSystemMemoryGauge.class, "cached"), new OperatingSystemMemoryGauge("Cached")); + + BufferPoolGauges.registerMetrics(); + GarbageCollectionGauges.registerMetrics(); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MicrometerRegistryManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MicrometerRegistryManager.java new file mode 100644 index 000000000..4fc55043c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/MicrometerRegistryManager.java @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import io.dropwizard.lifecycle.Managed; +import io.micrometer.core.instrument.MeterRegistry; + +public class MicrometerRegistryManager implements Managed { + + private final MeterRegistry meterRegistry; + + public MicrometerRegistryManager(final MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } + + @Override + public void start() throws Exception { + + } + + @Override + public void stop() throws Exception { + // closing the registry publishes one final set of metrics + meterRegistry.close(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/NetworkGauge.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/NetworkGauge.java new file mode 100644 index 000000000..9b23f2a8f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/NetworkGauge.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + + +import com.codahale.metrics.Gauge; +import org.whispersystems.textsecuregcm.util.Pair; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +public abstract class NetworkGauge implements Gauge { + + protected Pair getSentReceived() throws IOException { + File proc = new File("/proc/net/dev"); + BufferedReader reader = new BufferedReader(new FileReader(proc)); + String header = reader.readLine(); + String header2 = reader.readLine(); + + long bytesSent = 0; + long bytesReceived = 0; + + String interfaceStats; + + while ((interfaceStats = reader.readLine()) != null) { + String[] stats = interfaceStats.split("\\s+"); + + if (!stats[1].equals("lo:")) { + bytesReceived += Long.parseLong(stats[2]); + bytesSent += Long.parseLong(stats[10]); + } + } + + return new Pair<>(bytesSent, bytesReceived); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/NetworkReceivedGauge.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/NetworkReceivedGauge.java new file mode 100644 index 000000000..6ecd85ac4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/NetworkReceivedGauge.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.Pair; + +import java.io.IOException; + +public class NetworkReceivedGauge extends NetworkGauge { + + private final Logger logger = LoggerFactory.getLogger(NetworkReceivedGauge.class); + + private long lastTimestamp; + private long lastReceived; + + public NetworkReceivedGauge() { + try { + this.lastTimestamp = System.currentTimeMillis(); + this.lastReceived = getSentReceived().second(); + } catch (IOException e) { + logger.warn(NetworkReceivedGauge.class.getSimpleName(), e); + } + } + + @Override + public Double getValue() { + try { + long timestamp = System.currentTimeMillis(); + Pair sentAndReceived = getSentReceived(); + double bytesReceived = sentAndReceived.second() - lastReceived; + double secondsElapsed = (timestamp - this.lastTimestamp) / 1000; + double result = bytesReceived / secondsElapsed; + + this.lastTimestamp = timestamp; + this.lastReceived = sentAndReceived.second(); + + return result; + } catch (IOException e) { + logger.warn("NetworkReceivedGauge", e); + return -1D; + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/NetworkSentGauge.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/NetworkSentGauge.java new file mode 100644 index 000000000..4952550e7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/NetworkSentGauge.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.Pair; + +import java.io.IOException; + +public class NetworkSentGauge extends NetworkGauge { + + private final Logger logger = LoggerFactory.getLogger(NetworkSentGauge.class); + + private long lastTimestamp; + private long lastSent; + + public NetworkSentGauge() { + try { + this.lastTimestamp = System.currentTimeMillis(); + this.lastSent = getSentReceived().first(); + } catch (IOException e) { + logger.warn(NetworkSentGauge.class.getSimpleName(), e); + } + } + + @Override + public Double getValue() { + try { + long timestamp = System.currentTimeMillis(); + Pair sentAndReceived = getSentReceived(); + double bytesTransmitted = sentAndReceived.first() - lastSent; + double secondsElapsed = (timestamp - this.lastTimestamp) / 1000; + double result = bytesTransmitted / secondsElapsed; + + this.lastSent = sentAndReceived.first(); + this.lastTimestamp = timestamp; + + return result; + } catch (IOException e) { + logger.warn("NetworkSentGauge", e); + return -1D; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/OperatingSystemMemoryGauge.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/OperatingSystemMemoryGauge.java new file mode 100644 index 000000000..b3dcb6cb4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/OperatingSystemMemoryGauge.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import com.codahale.metrics.Gauge; +import com.google.common.annotations.VisibleForTesting; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public class OperatingSystemMemoryGauge implements Gauge { + + private final String metricName; + + private static final File MEMINFO_FILE = new File("/proc/meminfo"); + private static final Pattern MEMORY_METRIC_PATTERN = Pattern.compile("^([^:]+):\\s+([0-9]+).*$"); + + public OperatingSystemMemoryGauge(final String metricName) { + this.metricName = metricName; + } + + @Override + public Long getValue() { + try (final BufferedReader bufferedReader = new BufferedReader(new FileReader(MEMINFO_FILE))) { + return getValue(bufferedReader.lines()); + } catch (final IOException e) { + return 0L; + } + } + + @VisibleForTesting + long getValue(final Stream lines) { + return lines.map(MEMORY_METRIC_PATTERN::matcher) + .filter(Matcher::matches) + .filter(matcher -> this.metricName.equalsIgnoreCase(matcher.group(1))) + .map(matcher -> Long.parseLong(matcher.group(2), 10)) + .findFirst() + .orElse(0L); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/ReportedMessageMetricsListener.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/ReportedMessageMetricsListener.java new file mode 100644 index 000000000..3c1c6eb85 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/ReportedMessageMetricsListener.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import static com.codahale.metrics.MetricRegistry.name; + +import io.micrometer.core.instrument.Metrics; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import net.logstash.logback.marker.Markers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.ReportMessageManager; +import org.whispersystems.textsecuregcm.storage.ReportedMessageListener; +import org.whispersystems.textsecuregcm.util.Util; + +public class ReportedMessageMetricsListener implements ReportedMessageListener { + + private final AccountsManager accountsManager; + + // ReportMessageManager name used deliberately to preserve continuity of metrics + private static final String REPORTED_COUNTER_NAME = name(ReportMessageManager.class, "reported"); + private static final String REPORTER_COUNTER_NAME = name(ReportMessageManager.class, "reporter"); + + private static final String COUNTRY_CODE_TAG_NAME = "countryCode"; + + private static final Logger logger = LoggerFactory.getLogger(ReportedMessageMetricsListener.class); + + public ReportedMessageMetricsListener(final AccountsManager accountsManager) { + this.accountsManager = accountsManager; + } + + @Override + public void handleMessageReported(final String sourceNumber, final UUID messageGuid, final UUID reporterUuid, + final Optional reportSpamToken) { + + final String sourceCountryCode = Util.getCountryCode(sourceNumber); + + Metrics.counter(REPORTED_COUNTER_NAME, COUNTRY_CODE_TAG_NAME, sourceCountryCode).increment(); + + accountsManager.getByAccountIdentifier(reporterUuid).ifPresent(reporter -> { + final String destinationCountryCode = Util.getCountryCode(reporter.getNumber()); + + logger.info(Markers.appendEntries(Map.of( + "sourceCountry", sourceCountryCode, + "destinationCountry", destinationCountryCode)), + "Message reported"); + + Metrics.counter(REPORTER_COUNTER_NAME, COUNTRY_CODE_TAG_NAME, destinationCountryCode).increment(); + }); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/SignalDatadogReporterFactory.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/SignalDatadogReporterFactory.java new file mode 100644 index 000000000..0a4920af0 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/SignalDatadogReporterFactory.java @@ -0,0 +1,105 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* + * This is derived from Coursera's dropwizard datadog reporter. + * https://github.com/coursera/metrics-datadog + */ + +package org.whispersystems.textsecuregcm.metrics; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.ScheduledReporter; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.dropwizard.metrics.BaseReporterFactory; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import org.coursera.metrics.datadog.DatadogReporter; +import org.coursera.metrics.datadog.DatadogReporter.Expansion; +import org.coursera.metrics.datadog.DefaultMetricNameFormatterFactory; +import org.coursera.metrics.datadog.DynamicTagsCallbackFactory; +import org.coursera.metrics.datadog.MetricNameFormatterFactory; +import org.coursera.metrics.datadog.transport.UdpTransport; +import org.whispersystems.textsecuregcm.WhisperServerVersion; +import org.whispersystems.textsecuregcm.util.HostnameUtil; + +@JsonTypeName("signal-datadog") +public class SignalDatadogReporterFactory extends BaseReporterFactory { + + @JsonProperty + private List tags = null; + + @Valid + @JsonProperty + private DynamicTagsCallbackFactory dynamicTagsCallback = null; + + @JsonProperty + private String prefix = null; + + @Valid + @NotNull + @JsonProperty + private MetricNameFormatterFactory metricNameFormatter = new DefaultMetricNameFormatterFactory(); + + @Valid + @NotNull + @JsonProperty("udpTransport") + private UdpTransportConfig udpTransportConfig; + + private static final EnumSet EXPANSIONS = EnumSet.of( + Expansion.COUNT, + Expansion.MIN, + Expansion.MAX, + Expansion.MEAN, + Expansion.MEDIAN, + Expansion.P75, + Expansion.P95, + Expansion.P99, + Expansion.P999 + ); + + public ScheduledReporter build(final MetricRegistry registry) { + final List tagsWithVersion; + + { + final String versionTag = "version:" + WhisperServerVersion.getServerVersion(); + + if (tags != null) { + tagsWithVersion = new ArrayList<>(tags); + tagsWithVersion.add(versionTag); + } else { + tagsWithVersion = List.of(versionTag); + } + } + + return DatadogReporter.forRegistry(registry) + .withTransport(udpTransportConfig.udpTransport()) + .withHost(HostnameUtil.getLocalHostname()) + .withTags(tagsWithVersion) + .withPrefix(prefix) + .withExpansions(EXPANSIONS) + .withMetricNameFormatter(metricNameFormatter.build()) + .withDynamicTagCallback(dynamicTagsCallback != null ? dynamicTagsCallback.build() : null) + .filter(getFilter()) + .convertDurationsTo(getDurationUnit()) + .convertRatesTo(getRateUnit()) + .build(); + } + + public record UdpTransportConfig(@NotNull String statsdHost, @Min(1) int port) { + + public UdpTransport udpTransport() { + return new UdpTransport.Builder() + .withStatsdHost(statsdHost) + .withPort(port) + .build(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/TrafficSource.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/TrafficSource.java new file mode 100644 index 000000000..303ffbdd1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/TrafficSource.java @@ -0,0 +1,11 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +public enum TrafficSource { + HTTP, + WEBSOCKET +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java new file mode 100644 index 000000000..7ca8ccfc1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtil.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import com.vdurmont.semver4j.Semver; +import io.micrometer.core.instrument.Tag; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; +import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; +import org.whispersystems.textsecuregcm.util.ua.UserAgent; +import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; + +/** + * Utility class for extracting platform/version metrics tags from User-Agent strings. + */ +public class UserAgentTagUtil { + + public static final String PLATFORM_TAG = "platform"; + public static final String VERSION_TAG = "clientVersion"; + + private UserAgentTagUtil() { + } + + public static Tag getPlatformTag(final String userAgentString) { + String platform; + + try { + platform = UserAgentUtil.parseUserAgentString(userAgentString).getPlatform().name().toLowerCase(); + } catch (final UnrecognizedUserAgentException e) { + platform = "unrecognized"; + } + + return Tag.of(PLATFORM_TAG, platform); + } + + public static Optional getClientVersionTag(final String userAgentString, final ClientReleaseManager clientReleaseManager) { + try { + final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString); + + if (clientReleaseManager.isVersionActive(userAgent.getPlatform(), userAgent.getVersion())) { + return Optional.of(Tag.of(VERSION_TAG, userAgent.getVersion().toString())); + } + } catch (final UnrecognizedUserAgentException ignored) { + } + + return Optional.empty(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/providers/MultiRecipientMessageProvider.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/providers/MultiRecipientMessageProvider.java new file mode 100644 index 000000000..74b1ec164 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/providers/MultiRecipientMessageProvider.java @@ -0,0 +1,161 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.providers; + +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.util.DataSizeUnit; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NoContentException; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.Provider; +import org.whispersystems.textsecuregcm.entities.MultiRecipientMessage; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; + +@Provider +@Consumes(MultiRecipientMessageProvider.MEDIA_TYPE) +public class MultiRecipientMessageProvider implements MessageBodyReader { + + public static final String MEDIA_TYPE = "application/vnd.signal-messenger.mrm"; + public static final int MAX_RECIPIENT_COUNT = 5000; + public static final int MAX_MESSAGE_SIZE = Math.toIntExact(32 + DataSizeUnit.KIBIBYTES.toBytes(256)); + + public static final byte AMBIGUOUS_ID_VERSION_IDENTIFIER = 0x22; + public static final byte EXPLICIT_ID_VERSION_IDENTIFIER = 0x23; + + private enum Version { + AMBIGUOUS_ID(AMBIGUOUS_ID_VERSION_IDENTIFIER), + EXPLICIT_ID(EXPLICIT_ID_VERSION_IDENTIFIER); + + private final byte identifier; + + Version(final byte identifier) { + this.identifier = identifier; + } + + static Version forVersionByte(final byte versionByte) { + for (final Version version : values()) { + if (version.identifier == versionByte) { + return version; + } + } + + throw new IllegalArgumentException("Unrecognized version byte: " + versionByte); + } + } + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return MEDIA_TYPE.equals(mediaType.toString()) && MultiRecipientMessage.class.isAssignableFrom(type); + } + + @Override + public MultiRecipientMessage readFrom(Class type, Type genericType, Annotation[] annotations, + MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) + throws IOException, WebApplicationException { + int versionByte = entityStream.read(); + if (versionByte == -1) { + throw new NoContentException("Empty body not allowed"); + } + + final Version version; + + try { + version = Version.forVersionByte((byte) versionByte); + } catch (final IllegalArgumentException e) { + throw new BadRequestException("Unsupported version"); + } + + long count = readVarint(entityStream); + if (count > MAX_RECIPIENT_COUNT) { + throw new BadRequestException("Maximum recipient count exceeded"); + } + MultiRecipientMessage.Recipient[] recipients = new MultiRecipientMessage.Recipient[Math.toIntExact(count)]; + for (int i = 0; i < Math.toIntExact(count); i++) { + ServiceIdentifier identifier = readIdentifier(entityStream, version); + long deviceId = readVarint(entityStream); + int registrationId = readU16(entityStream); + byte[] perRecipientKeyMaterial = entityStream.readNBytes(48); + if (perRecipientKeyMaterial.length != 48) { + throw new IOException("Failed to read expected number of key material bytes for a recipient"); + } + recipients[i] = new MultiRecipientMessage.Recipient(identifier, deviceId, registrationId, perRecipientKeyMaterial); + } + + // caller is responsible for checking that the entity stream is at EOF when we return; if there are more bytes than + // this it'll return an error back. We just need to limit how many we'll accept here. + byte[] commonPayload = entityStream.readNBytes(MAX_MESSAGE_SIZE); + if (commonPayload.length < 32) { + throw new IOException("Failed to read expected number of common key material bytes"); + } + return new MultiRecipientMessage(recipients, commonPayload); + } + + /** + * Reads a service identifier from the given stream. + */ + private ServiceIdentifier readIdentifier(final InputStream stream, final Version version) throws IOException { + final byte[] uuidBytes = switch (version) { + case AMBIGUOUS_ID -> stream.readNBytes(16); + case EXPLICIT_ID -> stream.readNBytes(17); + }; + + return ServiceIdentifier.fromBytes(uuidBytes); + } + + /** + * Reads a varint. A varint larger than 64 bits is rejected with a {@code WebApplicationException}. An + * {@code IOException} is thrown if the stream ends before we finish reading the varint. + * + * @return the varint value + */ + @VisibleForTesting + public static long readVarint(InputStream stream) throws IOException, WebApplicationException { + boolean hasMore = true; + int currentOffset = 0; + long result = 0; + while (hasMore) { + if (currentOffset >= 64) { + throw new BadRequestException("varint is too large"); + } + int b = stream.read(); + if (b == -1) { + throw new IOException("Missing byte " + (currentOffset / 7) + " of varint"); + } + if (currentOffset == 63 && (b & 0xFE) != 0) { + throw new BadRequestException("varint is too large"); + } + hasMore = (b & 0x80) != 0; + result |= (b & 0x7FL) << currentOffset; + currentOffset += 7; + } + return result; + } + + /** + * Reads two bytes with most significant byte first. Treats the value as unsigned so the range returned is + * {@code [0, 65535]}. + */ + @VisibleForTesting + static int readU16(InputStream stream) throws IOException { + int b1 = stream.read(); + if (b1 == -1) { + throw new IOException("Missing byte 1 of U16"); + } + int b2 = stream.read(); + if (b2 == -1) { + throw new IOException("Missing byte 2 of U16"); + } + return (b1 << 8) | b2; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/providers/RedisClusterHealthCheck.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/providers/RedisClusterHealthCheck.java new file mode 100644 index 000000000..bfef14dee --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/providers/RedisClusterHealthCheck.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.providers; + +import com.codahale.metrics.health.HealthCheck; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; + +public class RedisClusterHealthCheck extends HealthCheck { + + private final FaultTolerantRedisCluster redisCluster; + + public RedisClusterHealthCheck(final FaultTolerantRedisCluster redisCluster) { + this.redisCluster = redisCluster; + } + + @Override + protected Result check() { + redisCluster.withCluster(connection -> connection.sync().upstream().commands().ping()); + return Result.healthy(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java new file mode 100644 index 000000000..e4aaa5461 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java @@ -0,0 +1,180 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.push; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.eatthepath.pushy.apns.ApnsClient; +import com.eatthepath.pushy.apns.ApnsClientBuilder; +import com.eatthepath.pushy.apns.DeliveryPriority; +import com.eatthepath.pushy.apns.PushType; +import com.eatthepath.pushy.apns.auth.ApnsSigningKey; +import com.eatthepath.pushy.apns.util.SimpleApnsPayloadBuilder; +import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification; +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.lifecycle.Managed; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import org.whispersystems.textsecuregcm.configuration.ApnConfiguration; + +public class APNSender implements Managed, PushNotificationSender { + + private final ExecutorService executor; + private final String bundleId; + private final ApnsClient apnsClient; + + @VisibleForTesting + static final String APN_VOIP_NOTIFICATION_PAYLOAD = new SimpleApnsPayloadBuilder() + .setSound("default") + .setLocalizedAlertMessage("APN_Message") + .build(); + + @VisibleForTesting + static final String APN_NSE_NOTIFICATION_PAYLOAD = new SimpleApnsPayloadBuilder() + .setMutableContent(true) + .setLocalizedAlertMessage("APN_Message") + .build(); + + @VisibleForTesting + static final String APN_BACKGROUND_PAYLOAD = new SimpleApnsPayloadBuilder() + .setContentAvailable(true) + .build(); + + @VisibleForTesting + static final Instant MAX_EXPIRATION = Instant.ofEpochMilli(Integer.MAX_VALUE * 1000L); + + private static final String APNS_CA_FILENAME = "apns-certificates.pem"; + + private static final Timer SEND_NOTIFICATION_TIMER = Metrics.timer(name(APNSender.class, "sendNotification")); + + public APNSender(ExecutorService executor, ApnConfiguration configuration) + throws IOException, NoSuchAlgorithmException, InvalidKeyException + { + this.executor = executor; + this.bundleId = configuration.bundleId(); + this.apnsClient = new ApnsClientBuilder().setSigningKey( + ApnsSigningKey.loadFromInputStream(new ByteArrayInputStream(configuration.signingKey().value().getBytes()), + configuration.teamId().value(), configuration.keyId().value())) + .setTrustedServerCertificateChain(getClass().getResourceAsStream(APNS_CA_FILENAME)) + .setApnsServer(configuration.sandbox() ? ApnsClientBuilder.DEVELOPMENT_APNS_HOST : ApnsClientBuilder.PRODUCTION_APNS_HOST) + .build(); + } + + @VisibleForTesting + public APNSender(ExecutorService executor, ApnsClient apnsClient, String bundleId) { + this.executor = executor; + this.apnsClient = apnsClient; + this.bundleId = bundleId; + } + + @Override + public CompletableFuture sendNotification(final PushNotification notification) { + final String topic = switch (notification.tokenType()) { + case APN -> bundleId; + case APN_VOIP -> bundleId + ".voip"; + default -> throw new IllegalArgumentException("Unsupported token type: " + notification.tokenType()); + }; + + final boolean isVoip = notification.tokenType() == PushNotification.TokenType.APN_VOIP; + + final String payload = switch (notification.notificationType()) { + case NOTIFICATION -> { + if (isVoip) { + yield APN_VOIP_NOTIFICATION_PAYLOAD; + } else { + yield notification.urgent() ? APN_NSE_NOTIFICATION_PAYLOAD : APN_BACKGROUND_PAYLOAD; + } + } + + case ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY -> new SimpleApnsPayloadBuilder() + .setMutableContent(true) + .setLocalizedAlertMessage("APN_Message") + .addCustomProperty("attemptLoginContext", notification.data()) + .build(); + + case ATTEMPT_LOGIN_NOTIFICATION_LOW_PRIORITY -> new SimpleApnsPayloadBuilder() + .setContentAvailable(true) + .addCustomProperty("attemptLoginContext", notification.data()) + .build(); + + case CHALLENGE -> new SimpleApnsPayloadBuilder() + .setSound("default") + .setLocalizedAlertMessage("APN_Message") + .addCustomProperty("challenge", notification.data()) + .build(); + + case RATE_LIMIT_CHALLENGE -> new SimpleApnsPayloadBuilder() + .setSound("default") + .setLocalizedAlertMessage("APN_Message") + .addCustomProperty("rateLimitChallenge", notification.data()) + .build(); + }; + + final PushType pushType; + + if (isVoip) { + pushType = PushType.VOIP; + } else { + pushType = notification.urgent() ? PushType.ALERT : PushType.BACKGROUND; + } + + final DeliveryPriority deliveryPriority = + (notification.urgent() || isVoip) ? DeliveryPriority.IMMEDIATE : DeliveryPriority.CONSERVE_POWER; + + final String collapseId = + (notification.notificationType() == PushNotification.NotificationType.NOTIFICATION && notification.urgent() && !isVoip) + ? "incoming-message" : null; + + final Instant start = Instant.now(); + + return apnsClient.sendNotification(new SimpleApnsPushNotification(notification.deviceToken(), + topic, + payload, + MAX_EXPIRATION, + deliveryPriority, + pushType, + collapseId)) + .whenComplete((response, throwable) -> { + // Note that we deliberately run this small bit of non-blocking measurement on the "send notification" thread + // to avoid any measurement noise that could arise from dispatching to another executor and waiting in its + // queue + SEND_NOTIFICATION_TIMER.record(Duration.between(start, Instant.now())); + }) + .thenApplyAsync(response -> { + final boolean accepted; + final String rejectionReason; + final boolean unregistered; + + if (response.isAccepted()) { + accepted = true; + rejectionReason = null; + unregistered = false; + } else { + accepted = false; + rejectionReason = response.getRejectionReason().orElse("unknown"); + unregistered = ("Unregistered".equals(rejectionReason) || "BadDeviceToken".equals(rejectionReason)); + } + + return new SendPushNotificationResult(accepted, rejectionReason, unregistered); + }, executor); + } + + @Override + public void start() { + } + + @Override + public void stop() { + this.apnsClient.close().join(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationScheduler.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationScheduler.java new file mode 100644 index 000000000..19d5c0d27 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationScheduler.java @@ -0,0 +1,439 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.lifecycle.Managed; +import io.lettuce.core.Limit; +import io.lettuce.core.Range; +import io.lettuce.core.RedisException; +import io.lettuce.core.ScriptOutputType; +import io.lettuce.core.SetArgs; +import io.lettuce.core.cluster.SlotHash; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import java.io.IOException; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.redis.ClusterLuaScript; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.RedisClusterUtil; +import org.whispersystems.textsecuregcm.util.Util; + +public class ApnPushNotificationScheduler implements Managed { + + private static final Logger logger = LoggerFactory.getLogger(ApnPushNotificationScheduler.class); + + private static final String PENDING_RECURRING_VOIP_NOTIFICATIONS_KEY_PREFIX = "PENDING_APN"; + private static final String PENDING_BACKGROUND_NOTIFICATIONS_KEY_PREFIX = "PENDING_BACKGROUND_APN"; + private static final String LAST_BACKGROUND_NOTIFICATION_TIMESTAMP_KEY_PREFIX = "LAST_BACKGROUND_NOTIFICATION"; + + @VisibleForTesting + static final String NEXT_SLOT_TO_PROCESS_KEY = "pending_notification_next_slot"; + + private static final Counter delivered = Metrics.counter(name(ApnPushNotificationScheduler.class, "voip_delivered")); + private static final Counter sent = Metrics.counter(name(ApnPushNotificationScheduler.class, "voip_sent")); + private static final Counter retry = Metrics.counter(name(ApnPushNotificationScheduler.class, "voip_retry")); + private static final Counter evicted = Metrics.counter(name(ApnPushNotificationScheduler.class, "voip_evicted")); + + private static final Counter backgroundNotificationScheduledCounter = Metrics.counter(name(ApnPushNotificationScheduler.class, "backgroundNotification", "scheduled")); + private static final Counter backgroundNotificationSentCounter = Metrics.counter(name(ApnPushNotificationScheduler.class, "backgroundNotification", "sent")); + + private final APNSender apnSender; + private final AccountsManager accountsManager; + private final FaultTolerantRedisCluster pushSchedulingCluster; + private final Clock clock; + + private final ClusterLuaScript getPendingVoipDestinationsScript; + private final ClusterLuaScript insertPendingVoipDestinationScript; + private final ClusterLuaScript removePendingVoipDestinationScript; + + private final ClusterLuaScript scheduleBackgroundNotificationScript; + + private final Thread[] workerThreads; + + @VisibleForTesting + static final Duration BACKGROUND_NOTIFICATION_PERIOD = Duration.ofMinutes(20); + + private final AtomicBoolean running = new AtomicBoolean(false); + + class NotificationWorker implements Runnable { + + private static final int PAGE_SIZE = 128; + + @Override + public void run() { + do { + try { + final long entriesProcessed = processNextSlot(); + + if (entriesProcessed == 0) { + Util.sleep(1000); + } + } catch (Exception e) { + logger.warn("Exception while operating", e); + } + } while (running.get()); + } + + private long processNextSlot() { + final int slot = (int) (pushSchedulingCluster.withCluster(connection -> + connection.sync().incr(NEXT_SLOT_TO_PROCESS_KEY)) % SlotHash.SLOT_COUNT); + + return processRecurringVoipNotifications(slot) + processScheduledBackgroundNotifications(slot); + } + + @VisibleForTesting + long processRecurringVoipNotifications(final int slot) { + List pendingDestinations; + long entriesProcessed = 0; + + do { + pendingDestinations = getPendingDestinationsForRecurringVoipNotifications(slot, PAGE_SIZE); + entriesProcessed += pendingDestinations.size(); + + for (final String destination : pendingDestinations) { + try { + getAccountAndDeviceFromPairString(destination).ifPresentOrElse( + accountAndDevice -> sendRecurringVoipNotification(accountAndDevice.first(), accountAndDevice.second()), + () -> removeRecurringVoipNotificationEntrySync(destination)); + } catch (final IllegalArgumentException e) { + logger.warn("Failed to parse account/device pair: {}", destination, e); + } + } + } while (!pendingDestinations.isEmpty()); + + return entriesProcessed; + } + + @VisibleForTesting + long processScheduledBackgroundNotifications(final int slot) { + final long currentTimeMillis = clock.millis(); + final String queueKey = getPendingBackgroundNotificationQueueKey(slot); + + final long processedBackgroundNotifications = pushSchedulingCluster.withCluster(connection -> { + List destinations; + long offset = 0; + + do { + destinations = connection.sync().zrangebyscore(queueKey, Range.create(0, currentTimeMillis), Limit.create(offset, PAGE_SIZE)); + + for (final String destination : destinations) { + try { + getAccountAndDeviceFromPairString(destination).ifPresent(accountAndDevice -> + sendBackgroundNotification(accountAndDevice.first(), accountAndDevice.second())); + } catch (final IllegalArgumentException e) { + logger.warn("Failed to parse account/device pair: {}", destination, e); + } + } + + offset += destinations.size(); + } while (destinations.size() == PAGE_SIZE); + + return offset; + }); + + pushSchedulingCluster.useCluster(connection -> + connection.sync().zremrangebyscore(queueKey, Range.create(0, currentTimeMillis))); + + return processedBackgroundNotifications; + } + } + + public ApnPushNotificationScheduler(FaultTolerantRedisCluster pushSchedulingCluster, + APNSender apnSender, AccountsManager accountsManager, final int dedicatedProcessWorkerThreadCount) + throws IOException { + + this(pushSchedulingCluster, apnSender, accountsManager, Clock.systemUTC(), dedicatedProcessWorkerThreadCount); + } + + @VisibleForTesting + ApnPushNotificationScheduler(FaultTolerantRedisCluster pushSchedulingCluster, + APNSender apnSender, + AccountsManager accountsManager, + Clock clock, + int dedicatedProcessThreadCount) throws IOException { + + this.apnSender = apnSender; + this.accountsManager = accountsManager; + this.pushSchedulingCluster = pushSchedulingCluster; + this.clock = clock; + + this.getPendingVoipDestinationsScript = ClusterLuaScript.fromResource(pushSchedulingCluster, "lua/apn/get.lua", + ScriptOutputType.MULTI); + this.insertPendingVoipDestinationScript = ClusterLuaScript.fromResource(pushSchedulingCluster, "lua/apn/insert.lua", + ScriptOutputType.VALUE); + this.removePendingVoipDestinationScript = ClusterLuaScript.fromResource(pushSchedulingCluster, "lua/apn/remove.lua", + ScriptOutputType.INTEGER); + + this.scheduleBackgroundNotificationScript = ClusterLuaScript.fromResource(pushSchedulingCluster, + "lua/apn/schedule_background_notification.lua", ScriptOutputType.VALUE); + + this.workerThreads = new Thread[dedicatedProcessThreadCount]; + + for (int i = 0; i < this.workerThreads.length; i++) { + this.workerThreads[i] = new Thread(new NotificationWorker(), "ApnFallbackManagerWorker-" + i); + } + } + + /** + * Schedule a recurring VOIP notification until {@link this#cancelScheduledNotifications} is called or the device is + * removed + * + * @return A CompletionStage that completes when the recurring notification has successfully been scheduled + */ + public CompletionStage scheduleRecurringVoipNotification(Account account, Device device) { + sent.increment(); + return insertRecurringVoipNotificationEntry(account, device, clock.millis() + (15 * 1000), (15 * 1000)); + } + + /** + * Schedule a background notification to be sent some time in the future + * + * @return A CompletionStage that completes when the notification has successfully been scheduled + */ + public CompletionStage scheduleBackgroundNotification(final Account account, final Device device) { + backgroundNotificationScheduledCounter.increment(); + + return scheduleBackgroundNotificationScript.executeAsync( + List.of( + getLastBackgroundNotificationTimestampKey(account, device), + getPendingBackgroundNotificationQueueKey(account, device)), + List.of( + getPairString(account, device), + String.valueOf(clock.millis()), + String.valueOf(BACKGROUND_NOTIFICATION_PERIOD.toMillis()))) + .thenAccept(dropValue()); + } + + /** + * Cancel a scheduled recurring VOIP notification + * + * @return A CompletionStage that completes when the scheduled task has been cancelled. + */ + public CompletionStage cancelScheduledNotifications(Account account, Device device) { + return removeRecurringVoipNotificationEntry(account, device) + .thenCompose(removed -> { + if (removed) { + delivered.increment(); + } + return pushSchedulingCluster.withCluster(connection -> + connection.async().zrem( + getPendingBackgroundNotificationQueueKey(account, device), + getPairString(account, device))); + }) + .thenAccept(dropValue()); + } + + @Override + public synchronized void start() { + running.set(true); + + for (final Thread workerThread : workerThreads) { + workerThread.start(); + } + } + + @Override + public synchronized void stop() throws InterruptedException { + running.set(false); + + for (final Thread workerThread : workerThreads) { + workerThread.join(); + } + } + + private void sendRecurringVoipNotification(final Account account, final Device device) { + String apnId = device.getVoipApnId(); + + if (apnId == null) { + removeRecurringVoipNotificationEntrySync(getEndpointKey(account, device)); + return; + } + + long deviceLastSeen = device.getLastSeen(); + if (deviceLastSeen < clock.millis() - TimeUnit.DAYS.toMillis(7)) { + evicted.increment(); + removeRecurringVoipNotificationEntrySync(getEndpointKey(account, device)); + return; + } + + apnSender.sendNotification(new PushNotification(apnId, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device, true)); + retry.increment(); + } + + @VisibleForTesting + void sendBackgroundNotification(final Account account, final Device device) { + if (StringUtils.isNotBlank(device.getApnId())) { + // It's okay for the "last notification" timestamp to expire after the "cooldown" period has elapsed; a missing + // timestamp and a timestamp older than the period are functionally equivalent. + pushSchedulingCluster.useCluster(connection -> connection.sync().set( + getLastBackgroundNotificationTimestampKey(account, device), + String.valueOf(clock.millis()), new SetArgs().ex(BACKGROUND_NOTIFICATION_PERIOD))); + + apnSender.sendNotification(new PushNotification(device.getApnId(), PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, false)); + + backgroundNotificationSentCounter.increment(); + } + } + + @VisibleForTesting + static Optional> getSeparated(String encoded) { + try { + if (encoded == null) return Optional.empty(); + + String[] parts = encoded.split(":"); + + if (parts.length != 2) { + logger.warn("Got strange encoded number: " + encoded); + return Optional.empty(); + } + + return Optional.of(new Pair<>(parts[0], Long.parseLong(parts[1]))); + } catch (NumberFormatException e) { + logger.warn("Badly formatted: " + encoded, e); + return Optional.empty(); + } + } + + @VisibleForTesting + static String getPairString(final Account account, final Device device) { + return account.getUuid() + ":" + device.getId(); + } + + @VisibleForTesting + Optional> getAccountAndDeviceFromPairString(final String endpoint) { + try { + if (StringUtils.isBlank(endpoint)) { + throw new IllegalArgumentException("Endpoint must not be blank"); + } + + final String[] parts = endpoint.split(":"); + + if (parts.length != 2) { + throw new IllegalArgumentException("Could not parse endpoint string: " + endpoint); + } + + final Optional maybeAccount = accountsManager.getByAccountIdentifier(UUID.fromString(parts[0])); + + return maybeAccount.flatMap(account -> account.getDevice(Long.parseLong(parts[1]))) + .map(device -> new Pair<>(maybeAccount.get(), device)); + + } catch (final NumberFormatException e) { + throw new IllegalArgumentException(e); + } + } + + private boolean removeRecurringVoipNotificationEntrySync(final String endpoint) { + try { + return removeRecurringVoipNotificationEntry(endpoint).toCompletableFuture().get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof RedisException re) { + throw re; + } + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private CompletionStage removeRecurringVoipNotificationEntry(Account account, Device device) { + return removeRecurringVoipNotificationEntry(getEndpointKey(account, device)); + } + + private CompletionStage removeRecurringVoipNotificationEntry(final String endpoint) { + return removePendingVoipDestinationScript.executeAsync( + List.of(getPendingRecurringVoipNotificationQueueKey(endpoint), endpoint), + Collections.emptyList()) + .thenApply(result -> ((long) result) > 0); + } + + @SuppressWarnings("unchecked") + @VisibleForTesting + List getPendingDestinationsForRecurringVoipNotifications(final int slot, final int limit) { + return (List) getPendingVoipDestinationsScript.execute( + List.of(getPendingRecurringVoipNotificationQueueKey(slot)), + List.of(String.valueOf(clock.millis()), String.valueOf(limit))); + } + + private CompletionStage insertRecurringVoipNotificationEntry(final Account account, final Device device, final long timestamp, final long interval) { + final String endpoint = getEndpointKey(account, device); + + return insertPendingVoipDestinationScript.executeAsync( + List.of(getPendingRecurringVoipNotificationQueueKey(endpoint), endpoint), + List.of(String.valueOf(timestamp), + String.valueOf(interval), + account.getUuid().toString(), + String.valueOf(device.getId()))) + .thenAccept(dropValue()); + } + + @VisibleForTesting + static String getEndpointKey(final Account account, final Device device) { + return "apn_device::{" + account.getUuid() + "::" + device.getId() + "}"; + } + + private static String getPendingRecurringVoipNotificationQueueKey(final String endpoint) { + return getPendingRecurringVoipNotificationQueueKey(SlotHash.getSlot(endpoint)); + } + + private static String getPendingRecurringVoipNotificationQueueKey(final int slot) { + return PENDING_RECURRING_VOIP_NOTIFICATIONS_KEY_PREFIX + "::{" + RedisClusterUtil.getMinimalHashTag(slot) + "}"; + } + + @VisibleForTesting + static String getPendingBackgroundNotificationQueueKey(final Account account, final Device device) { + return getPendingBackgroundNotificationQueueKey(SlotHash.getSlot(getPairString(account, device))); + } + + private static String getPendingBackgroundNotificationQueueKey(final int slot) { + return PENDING_BACKGROUND_NOTIFICATIONS_KEY_PREFIX + "::{" + RedisClusterUtil.getMinimalHashTag(slot) + "}"; + } + + private static String getLastBackgroundNotificationTimestampKey(final Account account, final Device device) { + return LAST_BACKGROUND_NOTIFICATION_TIMESTAMP_KEY_PREFIX + "::{" + getPairString(account, device) + "}"; + } + + @VisibleForTesting + Optional getLastBackgroundNotificationTimestamp(final Account account, final Device device) { + return Optional.ofNullable( + pushSchedulingCluster.withCluster(connection -> + connection.sync().get(getLastBackgroundNotificationTimestampKey(account, device)))) + .map(timestampString -> Instant.ofEpochMilli(Long.parseLong(timestampString))); + } + + @VisibleForTesting + Optional getNextScheduledBackgroundNotificationTimestamp(final Account account, final Device device) { + return Optional.ofNullable( + pushSchedulingCluster.withCluster(connection -> + connection.sync().zscore(getPendingBackgroundNotificationQueueKey(account, device), + getPairString(account, device)))) + .map(timestamp -> Instant.ofEpochMilli(timestamp.longValue())); + } + + private static Consumer dropValue() { + return ignored -> {}; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/ClientPresenceManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/ClientPresenceManager.java new file mode 100644 index 000000000..fc7d455e1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/ClientPresenceManager.java @@ -0,0 +1,357 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.SharedMetricRegistries; +import com.codahale.metrics.Timer; +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.lifecycle.Managed; +import io.lettuce.core.LettuceFutures; +import io.lettuce.core.RedisFuture; +import io.lettuce.core.ScriptOutputType; +import io.lettuce.core.cluster.SlotHash; +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import io.lettuce.core.cluster.models.partitions.RedisClusterNode; +import io.lettuce.core.cluster.pubsub.RedisClusterPubSubAdapter; +import io.micrometer.core.instrument.Counter; +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import io.micrometer.core.instrument.Metrics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.redis.ClusterLuaScript; +import org.whispersystems.textsecuregcm.redis.FaultTolerantPubSubConnection; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.Constants; + +/** + * The client presence manager keeps track of which clients are actively connected and "present" to receive messages. + * Only one client per account/device may be present at a time; if a second client for the same account/device declares + * its presence, the previous client is displaced. + *

    + * The client presence manager depends on Redis keyspace notifications and requires that the Redis instance support at + * least the following notification types: {@code K$z}. + */ +public class ClientPresenceManager extends RedisClusterPubSubAdapter implements Managed { + + private final String managerId = UUID.randomUUID().toString(); + private final String connectedClientSetKey = getConnectedClientSetKey(managerId); + + private final FaultTolerantRedisCluster presenceCluster; + private final FaultTolerantPubSubConnection pubSubConnection; + + private final ClusterLuaScript clearPresenceScript; + private final ClusterLuaScript renewPresenceScript; + + private final ExecutorService keyspaceNotificationExecutorService; + private final ScheduledExecutorService scheduledExecutorService; + private ScheduledFuture pruneMissingPeersFuture; + + private final Map displacementListenersByPresenceKey = new ConcurrentHashMap<>(); + + private final Timer checkPresenceTimer; + private final Timer setPresenceTimer; + private final Timer clearPresenceTimer; + private final Timer prunePeersTimer; + private final Meter pruneClientMeter; + private final Meter remoteDisplacementMeter; + private final Meter pubSubMessageMeter; + private final Counter displacementListenerAlreadyRemovedCounter; + + private static final int PRUNE_PEERS_INTERVAL_SECONDS = (int) Duration.ofSeconds(30).toSeconds(); + private static final int PRESENCE_EXPIRATION_SECONDS = (int) Duration.ofMinutes(11).toSeconds(); + + static final String MANAGER_SET_KEY = "presence::managers"; + + private static final Logger log = LoggerFactory.getLogger(ClientPresenceManager.class); + + public ClientPresenceManager(final FaultTolerantRedisCluster presenceCluster, + final ScheduledExecutorService scheduledExecutorService, + final ExecutorService keyspaceNotificationExecutorService) throws IOException { + this.presenceCluster = presenceCluster; + this.pubSubConnection = this.presenceCluster.createPubSubConnection(); + this.clearPresenceScript = ClusterLuaScript.fromResource(presenceCluster, "lua/clear_presence.lua", + ScriptOutputType.INTEGER); + this.renewPresenceScript = ClusterLuaScript.fromResource(presenceCluster, "lua/renew_presence.lua", + ScriptOutputType.VALUE); + this.scheduledExecutorService = scheduledExecutorService; + this.keyspaceNotificationExecutorService = keyspaceNotificationExecutorService; + + final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); + metricRegistry.gauge(name(getClass(), "localClientCount"), () -> displacementListenersByPresenceKey::size); + + this.checkPresenceTimer = metricRegistry.timer(name(getClass(), "checkPresence")); + this.setPresenceTimer = metricRegistry.timer(name(getClass(), "setPresence")); + this.clearPresenceTimer = metricRegistry.timer(name(getClass(), "clearPresence")); + this.prunePeersTimer = metricRegistry.timer(name(getClass(), "prunePeers")); + this.pruneClientMeter = metricRegistry.meter(name(getClass(), "pruneClient")); + this.remoteDisplacementMeter = metricRegistry.meter(name(getClass(), "remoteDisplacement")); + this.pubSubMessageMeter = metricRegistry.meter(name(getClass(), "pubSubMessage")); + this.displacementListenerAlreadyRemovedCounter = Metrics.counter( + name(getClass(), "displacementListenerAlreadyRemoved")); + } + + @VisibleForTesting + FaultTolerantPubSubConnection getPubSubConnection() { + return pubSubConnection; + } + + @Override + public void start() { + pubSubConnection.usePubSubConnection(connection -> { + connection.addListener(this); + + final String presenceChannel = getManagerPresenceChannel(managerId); + final int slot = SlotHash.getSlot(presenceChannel); + + connection.sync().nodes(node -> node.is(RedisClusterNode.NodeFlag.UPSTREAM) && node.hasSlot(slot)) + .commands() + .subscribe(presenceChannel); + }); + + pubSubConnection.subscribeToClusterTopologyChangedEvents(this::resubscribeAll); + + presenceCluster.useCluster(connection -> connection.sync().sadd(MANAGER_SET_KEY, managerId)); + + pruneMissingPeersFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + pruneMissingPeers(); + } catch (final Throwable t) { + log.warn("Failed to prune missing peers", t); + } + }, new Random().nextInt(PRUNE_PEERS_INTERVAL_SECONDS), PRUNE_PEERS_INTERVAL_SECONDS, TimeUnit.SECONDS); + } + + @Override + public void stop() { + pubSubConnection.usePubSubConnection(connection -> connection.removeListener(this)); + + if (pruneMissingPeersFuture != null) { + pruneMissingPeersFuture.cancel(false); + } + + for (final String presenceKey : displacementListenersByPresenceKey.keySet()) { + clearPresence(presenceKey); + } + + presenceCluster.useCluster(connection -> { + connection.sync().srem(MANAGER_SET_KEY, managerId); + connection.sync().del(getConnectedClientSetKey(managerId)); + }); + + pubSubConnection.usePubSubConnection( + connection -> connection.sync().upstream().commands().unsubscribe(getManagerPresenceChannel(managerId))); + } + + public void setPresent(final UUID accountUuid, final long deviceId, final DisplacedPresenceListener displacementListener) { + + try (final Timer.Context ignored = setPresenceTimer.time()) { + final String presenceKey = getPresenceKey(accountUuid, deviceId); + + displacePresence(presenceKey, true); + + displacementListenersByPresenceKey.put(presenceKey, displacementListener); + + presenceCluster.useCluster(connection -> { + final RedisAdvancedClusterCommands commands = connection.sync(); + + commands.sadd(connectedClientSetKey, presenceKey); + commands.setex(presenceKey, PRESENCE_EXPIRATION_SECONDS, managerId); + }); + + subscribeForRemotePresenceChanges(presenceKey); + } + } + + public void renewPresence(final UUID accountUuid, final long deviceId) { + renewPresenceScript.execute(List.of(getPresenceKey(accountUuid, deviceId)), + List.of(managerId, String.valueOf(PRESENCE_EXPIRATION_SECONDS))); + } + + public void disconnectAllPresences(final UUID accountUuid, final List deviceIds) { + + List presenceKeys = new ArrayList<>(); + deviceIds.forEach(deviceId -> { + String presenceKey = getPresenceKey(accountUuid, deviceId); + if (isLocallyPresent(accountUuid, deviceId)) { + displacePresence(presenceKey, false); + } + presenceKeys.add(presenceKey); + }); + + presenceCluster.useCluster(connection -> { + List> futures = presenceKeys.stream().map(key -> connection.async().del(key)).toList(); + LettuceFutures.awaitAll(connection.getTimeout(), futures.toArray(new RedisFuture[0])); + }); + } + + public void disconnectAllPresencesForUuid(final UUID accountUuid) { + disconnectAllPresences(accountUuid, Device.ALL_POSSIBLE_DEVICE_IDS); + } + + public void disconnectPresence(final UUID accountUuid, final long deviceId) { + disconnectAllPresences(accountUuid, List.of(deviceId)); + } + + private void displacePresence(final String presenceKey, final boolean connectedElsewhere) { + final DisplacedPresenceListener displacementListener = displacementListenersByPresenceKey.get(presenceKey); + + if (displacementListener != null) { + displacementListener.handleDisplacement(connectedElsewhere); + } + + clearPresence(presenceKey); + } + + public boolean isPresent(final UUID accountUuid, final long deviceId) { + try (final Timer.Context ignored = checkPresenceTimer.time()) { + return presenceCluster.withCluster(connection -> + connection.sync().exists(getPresenceKey(accountUuid, deviceId))) == 1; + } + } + + public boolean isLocallyPresent(final UUID accountUuid, final long deviceId) { + return displacementListenersByPresenceKey.containsKey(getPresenceKey(accountUuid, deviceId)); + } + + public boolean clearPresence(final UUID accountUuid, final long deviceId, final DisplacedPresenceListener listener) { + final String presenceKey = getPresenceKey(accountUuid, deviceId); + if (displacementListenersByPresenceKey.remove(presenceKey, listener)) { + return clearPresence(presenceKey); + } else { + displacementListenerAlreadyRemovedCounter.increment(); + return false; + } + } + + private boolean clearPresence(final String presenceKey) { + try (final Timer.Context ignored = clearPresenceTimer.time()) { + displacementListenersByPresenceKey.remove(presenceKey); + unsubscribeFromRemotePresenceChanges(presenceKey); + + final boolean removed = clearPresenceScript.execute(List.of(presenceKey), List.of(managerId)) != null; + presenceCluster.useCluster(connection -> connection.sync().srem(connectedClientSetKey, presenceKey)); + + return removed; + } + } + + private void subscribeForRemotePresenceChanges(final String presenceKey) { + final int slot = SlotHash.getSlot(presenceKey); + + pubSubConnection.usePubSubConnection( + connection -> connection.sync().nodes(node -> node.is(RedisClusterNode.NodeFlag.UPSTREAM) && node.hasSlot(slot)) + .commands() + .subscribe(getKeyspaceNotificationChannel(presenceKey))); + } + + private void resubscribeAll() { + for (final String presenceKey : displacementListenersByPresenceKey.keySet()) { + subscribeForRemotePresenceChanges(presenceKey); + } + } + + private void unsubscribeFromRemotePresenceChanges(final String presenceKey) { + pubSubConnection.usePubSubConnection( + connection -> connection.sync().upstream().commands().unsubscribe(getKeyspaceNotificationChannel(presenceKey))); + } + + void pruneMissingPeers() { + try (final Timer.Context ignored = prunePeersTimer.time()) { + final Set peerIds = presenceCluster.withCluster( + connection -> connection.sync().smembers(MANAGER_SET_KEY)); + peerIds.remove(managerId); + + for (final String peerId : peerIds) { + final boolean peerMissing = presenceCluster.withCluster( + connection -> connection.sync().publish(getManagerPresenceChannel(peerId), "ping") == 0); + + if (peerMissing) { + log.debug("Presence manager {} did not respond to ping", peerId); + + final String connectedClientsKey = getConnectedClientSetKey(peerId); + + String presenceKey; + + while ((presenceKey = presenceCluster.withCluster(connection -> connection.sync().spop(connectedClientsKey))) + != null) { + clearPresenceScript.execute(List.of(presenceKey), List.of(peerId)); + pruneClientMeter.mark(); + } + + presenceCluster.useCluster(connection -> { + connection.sync().del(connectedClientsKey); + connection.sync().srem(MANAGER_SET_KEY, peerId); + }); + } + } + } + } + + @Override + public void message(final RedisClusterNode node, final String channel, final String message) { + pubSubMessageMeter.mark(); + + if (channel.startsWith("__keyspace@0__:presence::{")) { + if ("set".equals(message) || "del".equals(message)) { + // for "set", another process has overwritten this presence key, which means the client has connected to another host. + // for "del", another process has indicated the client should be disconnected + final boolean connectedElsewhere = "set".equals(message); + + // At this point, we're on a Lettuce IO thread and need to dispatch to a separate thread before making + // synchronous Lettuce calls to avoid deadlocking. + keyspaceNotificationExecutorService.execute(() -> { + try { + displacePresence(channel.substring("__keyspace@0__:".length()), connectedElsewhere); + remoteDisplacementMeter.mark(); + } catch (final Exception e) { + log.warn("Error displacing presence", e); + } + }); + } + } + } + + @VisibleForTesting + String getManagerId() { + return managerId; + } + + @VisibleForTesting + static String getPresenceKey(final UUID accountUuid, final long deviceId) { + return "presence::{" + accountUuid.toString() + "::" + deviceId + "}"; + } + + private static String getKeyspaceNotificationChannel(final String presenceKey) { + return "__keyspace@0__:" + presenceKey; + } + + @VisibleForTesting + static String getConnectedClientSetKey(final String managerId) { + return "presence::clients::" + managerId; + } + + @VisibleForTesting + static String getManagerPresenceChannel(final String managerId) { + return "presence::manager::" + managerId; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/DisplacedPresenceListener.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/DisplacedPresenceListener.java new file mode 100644 index 000000000..88258be70 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/DisplacedPresenceListener.java @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +/** + * A displaced presence listener is notified when a specific client's presence has been displaced because the same + * client opened a newer connection to the Signal service. + */ +@FunctionalInterface +public interface DisplacedPresenceListener { + + void handleDisplacement(boolean connectedElsewhere); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/FcmSender.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/FcmSender.java new file mode 100644 index 000000000..15c39dae7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/FcmSender.java @@ -0,0 +1,139 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.ThreadManager; +import com.google.firebase.messaging.AndroidConfig; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.MessagingErrorCode; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FcmSender implements PushNotificationSender { + + private final ExecutorService executor; + private final FirebaseMessaging firebaseMessagingClient; + + private static final Timer SEND_NOTIFICATION_TIMER = Metrics.timer(name(FcmSender.class, "sendNotification")); + + private static final Logger logger = LoggerFactory.getLogger(FcmSender.class); + + public FcmSender(ExecutorService executor, String credentials) throws IOException { + try (final ByteArrayInputStream credentialInputStream = new ByteArrayInputStream(credentials.getBytes(StandardCharsets.UTF_8))) { + FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(credentialInputStream)) + .setThreadManager(new ThreadManager() { + @Override + protected ExecutorService getExecutor(final FirebaseApp app) { + return executor; + } + + @Override + protected void releaseExecutor(final FirebaseApp app, final ExecutorService executor) { + // Do nothing; the executor service is managed by Dropwizard + } + + @Override + protected ThreadFactory getThreadFactory() { + return new ThreadFactoryBuilder() + .setNameFormat("firebase-%d") + .build(); + } + }) + .build()); + } + + this.executor = executor; + this.firebaseMessagingClient = FirebaseMessaging.getInstance(); + } + + @VisibleForTesting + public FcmSender(ExecutorService executor, FirebaseMessaging firebaseMessagingClient) { + this.executor = executor; + this.firebaseMessagingClient = firebaseMessagingClient; + } + + @Override + public CompletableFuture sendNotification(PushNotification pushNotification) { + Message.Builder builder = Message.builder() + .setToken(pushNotification.deviceToken()) + .setAndroidConfig(AndroidConfig.builder() + .setPriority(pushNotification.urgent() ? AndroidConfig.Priority.HIGH : AndroidConfig.Priority.NORMAL) + .build()); + + final String key = switch (pushNotification.notificationType()) { + case NOTIFICATION -> "newMessageAlert"; + case ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY, ATTEMPT_LOGIN_NOTIFICATION_LOW_PRIORITY -> "attemptLoginContext"; + case CHALLENGE -> "challenge"; + case RATE_LIMIT_CHALLENGE -> "rateLimitChallenge"; + }; + + builder.putData(key, pushNotification.data() != null ? pushNotification.data() : ""); + + final Instant start = Instant.now(); + final CompletableFuture completableSendFuture = new CompletableFuture<>(); + + final ApiFuture sendFuture = firebaseMessagingClient.sendAsync(builder.build()); + + // We want to record the time taken to send the push notification as directly as possible; executing this very small + // bit of non-blocking measurement on the sender thread lets us do that without picking up any confounding factors + // like having a callback waiting in an executor's queue. + sendFuture.addListener(() -> SEND_NOTIFICATION_TIMER.record(Duration.between(start, Instant.now())), + MoreExecutors.directExecutor()); + + ApiFutures.addCallback(sendFuture, new ApiFutureCallback<>() { + @Override + public void onSuccess(final String result) { + completableSendFuture.complete(new SendPushNotificationResult(true, null, false)); + } + + @Override + public void onFailure(final Throwable cause) { + if (cause instanceof final FirebaseMessagingException firebaseMessagingException) { + final String errorCode; + + if (firebaseMessagingException.getMessagingErrorCode() != null) { + errorCode = firebaseMessagingException.getMessagingErrorCode().name(); + } else { + logger.warn("Received an FCM exception with no error code", firebaseMessagingException); + errorCode = "unknown"; + } + + completableSendFuture.complete(new SendPushNotificationResult(false, + errorCode, + firebaseMessagingException.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED)); + } else { + completableSendFuture.completeExceptionally(cause); + } + } + }, executor); + + return completableSendFuture; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/MessageSender.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/MessageSender.java new file mode 100644 index 000000000..f693c8458 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/MessageSender.java @@ -0,0 +1,111 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.push; + +import static com.codahale.metrics.MetricRegistry.name; +import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope; + +import io.micrometer.core.instrument.Metrics; +import org.apache.commons.lang3.StringUtils; +import org.whispersystems.textsecuregcm.redis.RedisOperation; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.MessagesManager; + +/** + * A MessageSender sends Signal messages to destination devices. Messages may be "normal" user-to-user messages, + * ephemeral ("online") messages like typing indicators, or delivery receipts. + *

    + * If a client is not actively connected to a Signal server to receive a message as soon as it is sent, the + * MessageSender will send a push notification to the destination device if possible. Some messages may be designated + * for "online" delivery only and will not be delivered (and clients will not be notified) if the destination device + * isn't actively connected to a Signal server. + * + * @see ClientPresenceManager + * @see org.whispersystems.textsecuregcm.storage.MessageAvailabilityListener + * @see ReceiptSender + */ +public class MessageSender { + + private final ClientPresenceManager clientPresenceManager; + private final MessagesManager messagesManager; + private final PushNotificationManager pushNotificationManager; + private final PushLatencyManager pushLatencyManager; + + private static final String SEND_COUNTER_NAME = name(MessageSender.class, "sendMessage"); + private static final String CHANNEL_TAG_NAME = "channel"; + private static final String EPHEMERAL_TAG_NAME = "ephemeral"; + private static final String CLIENT_ONLINE_TAG_NAME = "clientOnline"; + private static final String URGENT_TAG_NAME = "urgent"; + private static final String STORY_TAG_NAME = "story"; + private static final String SEALED_SENDER_TAG_NAME = "sealedSender"; + private static final String HAS_SPAM_REPORTING_TOKEN_TAG_NAME = "hasSpamReportingToken"; + + public MessageSender(ClientPresenceManager clientPresenceManager, + MessagesManager messagesManager, + PushNotificationManager pushNotificationManager, + PushLatencyManager pushLatencyManager) { + this.clientPresenceManager = clientPresenceManager; + this.messagesManager = messagesManager; + this.pushNotificationManager = pushNotificationManager; + this.pushLatencyManager = pushLatencyManager; + } + + public void sendMessage(final Account account, final Device device, final Envelope message, final boolean online) + throws NotPushRegisteredException { + + final String channel; + + if (device.getGcmId() != null) { + channel = "gcm"; + } else if (device.getApnId() != null) { + channel = "apn"; + } else if (device.getFetchesMessages()) { + channel = "websocket"; + } else { + throw new AssertionError(); + } + + final boolean clientPresent; + + if (online) { + clientPresent = clientPresenceManager.isPresent(account.getUuid(), device.getId()); + + if (clientPresent) { + messagesManager.insert(account.getUuid(), device.getId(), message.toBuilder().setEphemeral(true).build()); + } + } else { + messagesManager.insert(account.getUuid(), device.getId(), message); + + // We check for client presence after inserting the message to take a conservative view of notifications. If the + // client wasn't present at the time of insertion but is now, they'll retrieve the message. If they were present + // but disconnected before the message was delivered, we should send a notification. + clientPresent = clientPresenceManager.isPresent(account.getUuid(), device.getId()); + + if (!clientPresent) { + try { + pushNotificationManager.sendNewMessageNotification(account, device.getId(), message.getUrgent()); + + final boolean useVoip = StringUtils.isNotBlank(device.getVoipApnId()); + RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), useVoip, message.getUrgent())); + } catch (final NotPushRegisteredException e) { + if (!device.getFetchesMessages()) { + throw e; + } + } + } + } + + Metrics.counter(SEND_COUNTER_NAME, + CHANNEL_TAG_NAME, channel, + EPHEMERAL_TAG_NAME, String.valueOf(online), + CLIENT_ONLINE_TAG_NAME, String.valueOf(clientPresent), + URGENT_TAG_NAME, String.valueOf(message.getUrgent()), + STORY_TAG_NAME, String.valueOf(message.getStory()), + SEALED_SENDER_TAG_NAME, String.valueOf(!message.hasSourceUuid()), + HAS_SPAM_REPORTING_TOKEN_TAG_NAME, String.valueOf(message.getReportSpamToken() != null && !message.getReportSpamToken().isEmpty())) + .increment(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/NotPushRegisteredException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/NotPushRegisteredException.java new file mode 100644 index 000000000..c5dc14281 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/NotPushRegisteredException.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +public class NotPushRegisteredException extends Exception { + public NotPushRegisteredException() { + super(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/ProvisioningManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/ProvisioningManager.java new file mode 100644 index 000000000..5fe177aaa --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/ProvisioningManager.java @@ -0,0 +1,163 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import io.dropwizard.lifecycle.Managed; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.lettuce.core.RedisClient; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.codec.ByteArrayCodec; +import io.lettuce.core.pubsub.RedisPubSubAdapter; +import io.lettuce.core.pubsub.StatefulRedisPubSubConnection; +import io.lettuce.core.resource.ClientResources; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; +import org.whispersystems.textsecuregcm.redis.RedisOperation; +import org.whispersystems.textsecuregcm.redis.RedisUriUtil; +import org.whispersystems.textsecuregcm.storage.PubSubProtos; +import org.whispersystems.textsecuregcm.util.CircuitBreakerUtil; +import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException; +import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress; + +public class ProvisioningManager extends RedisPubSubAdapter implements Managed { + + private final RedisClient redisClient; + private final StatefulRedisPubSubConnection subscriptionConnection; + private final StatefulRedisConnection publicationConnection; + + private final CircuitBreaker circuitBreaker; + + private final Map> listenersByProvisioningAddress = + new ConcurrentHashMap<>(); + + private static final String ACTIVE_LISTENERS_GAUGE_NAME = name(ProvisioningManager.class, "activeListeners"); + + private static final String SEND_PROVISIONING_MESSAGE_COUNTER_NAME = + name(ProvisioningManager.class, "sendProvisioningMessage"); + + private static final String RECEIVE_PROVISIONING_MESSAGE_COUNTER_NAME = + name(ProvisioningManager.class, "receiveProvisioningMessage"); + + private static final Logger logger = LoggerFactory.getLogger(ProvisioningManager.class); + + public ProvisioningManager(final String redisUri, + final ClientResources clientResources, + final Duration timeout, + final CircuitBreakerConfiguration circuitBreakerConfiguration) { + + this(RedisClient.create(clientResources, RedisUriUtil.createRedisUriWithTimeout(redisUri, timeout)), timeout, + circuitBreakerConfiguration); + } + + @VisibleForTesting + ProvisioningManager(final RedisClient redisClient, + final Duration timeout, + final CircuitBreakerConfiguration circuitBreakerConfiguration) { + + this.redisClient = redisClient; + this.redisClient.setDefaultTimeout(timeout); + + this.subscriptionConnection = redisClient.connectPubSub(new ByteArrayCodec()); + this.publicationConnection = redisClient.connect(new ByteArrayCodec()); + + this.circuitBreaker = CircuitBreaker.of("pubsub-breaker", circuitBreakerConfiguration.toCircuitBreakerConfig()); + + CircuitBreakerUtil.registerMetrics(circuitBreaker, ProvisioningManager.class); + + Metrics.gaugeMapSize(ACTIVE_LISTENERS_GAUGE_NAME, Tags.empty(), listenersByProvisioningAddress); + } + + @Override + public void start() throws Exception { + subscriptionConnection.addListener(this); + } + + @Override + public void stop() throws Exception { + subscriptionConnection.removeListener(this); + + subscriptionConnection.close(); + publicationConnection.close(); + + redisClient.shutdown(); + } + + public void addListener(final ProvisioningAddress address, final Consumer listener) { + listenersByProvisioningAddress.put(address, listener); + + circuitBreaker.executeRunnable( + () -> subscriptionConnection.sync().subscribe(address.serialize().getBytes(StandardCharsets.UTF_8))); + } + + public void removeListener(final ProvisioningAddress address) { + RedisOperation.unchecked(() -> circuitBreaker.executeRunnable( + () -> subscriptionConnection.sync().unsubscribe(address.serialize().getBytes(StandardCharsets.UTF_8)))); + + listenersByProvisioningAddress.remove(address); + } + + public boolean sendProvisioningMessage(final ProvisioningAddress address, final byte[] body) { + final PubSubProtos.PubSubMessage pubSubMessage = PubSubProtos.PubSubMessage.newBuilder() + .setType(PubSubProtos.PubSubMessage.Type.DELIVER) + .setContent(ByteString.copyFrom(body)) + .build(); + + final boolean receiverPresent = circuitBreaker.executeSupplier( + () -> publicationConnection.sync() + .publish(address.serialize().getBytes(StandardCharsets.UTF_8), pubSubMessage.toByteArray()) > 0); + + Metrics.counter(SEND_PROVISIONING_MESSAGE_COUNTER_NAME, "online", String.valueOf(receiverPresent)).increment(); + + return receiverPresent; + } + + @Override + public void message(final byte[] channel, final byte[] message) { + try { + final ProvisioningAddress address = new ProvisioningAddress(new String(channel, StandardCharsets.UTF_8)); + final PubSubProtos.PubSubMessage pubSubMessage = PubSubProtos.PubSubMessage.parseFrom(message); + + if (pubSubMessage.getType() == PubSubProtos.PubSubMessage.Type.DELIVER) { + final Consumer listener = listenersByProvisioningAddress.get(address); + + boolean listenerPresent = false; + + if (listener != null) { + listenerPresent = true; + listener.accept(pubSubMessage); + } + + Metrics.counter(RECEIVE_PROVISIONING_MESSAGE_COUNTER_NAME, "listenerPresent", String.valueOf(listenerPresent)).increment(); + } + } catch (final InvalidWebsocketAddressException e) { + logger.warn("Failed to parse provisioning address", e); + } catch (final InvalidProtocolBufferException e) { + logger.warn("Failed to parse pub/sub message", e); + } + } + + @Override + public void unsubscribed(final byte[] channel, final long count) { + try { + listenersByProvisioningAddress.remove(new ProvisioningAddress(new String(channel))); + } catch (final InvalidWebsocketAddressException e) { + logger.warn("Failed to parse provisioning address for `unsubscribe` event", e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/PushLatencyManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/PushLatencyManager.java new file mode 100644 index 000000000..6908b0117 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/PushLatencyManager.java @@ -0,0 +1,147 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.annotations.VisibleForTesting; +import io.lettuce.core.SetArgs; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +/** + * Measures and records the latency between sending a push notification to a device and that device draining its queue + * of messages. + *

    + * When the server sends a push notification to a device, the push latency manager creates a Redis key/value pair + * mapping the current timestamp to the given device if such a mapping doesn't already exist. When a client connects and + * clears its message queue, the push latency manager gets and clears the time of the initial push notification to that + * device and records the time elapsed since the push notification timestamp as a latency observation. + */ +public class PushLatencyManager { + + private final FaultTolerantRedisCluster redisCluster; + private final ClientReleaseManager clientReleaseManager; + + private final Clock clock; + + public static final String TIMER_NAME = MetricRegistry.name(PushLatencyManager.class, "latency"); + private static final int TTL = (int) Duration.ofDays(1).toSeconds(); + + private static final Logger log = LoggerFactory.getLogger(PushLatencyManager.class); + + @VisibleForTesting + enum PushType { + STANDARD, + VOIP + } + + record PushRecord(Instant timestamp, PushType pushType, Optional urgent) { + } + + public PushLatencyManager(final FaultTolerantRedisCluster redisCluster, + final ClientReleaseManager clientReleaseManager) { + + this(redisCluster, clientReleaseManager, Clock.systemUTC()); + } + + @VisibleForTesting + PushLatencyManager(final FaultTolerantRedisCluster redisCluster, + final ClientReleaseManager clientReleaseManager, + final Clock clock) { + + this.redisCluster = redisCluster; + this.clientReleaseManager = clientReleaseManager; + this.clock = clock; + } + + void recordPushSent(final UUID accountUuid, final long deviceId, final boolean isVoip, final boolean isUrgent) { + try { + final String recordJson = SystemMapper.jsonMapper().writeValueAsString( + new PushRecord(Instant.now(clock), isVoip ? PushType.VOIP : PushType.STANDARD, Optional.of(isUrgent))); + + redisCluster.useCluster(connection -> + connection.async().set(getFirstUnacknowledgedPushKey(accountUuid, deviceId), + recordJson, + SetArgs.Builder.nx().ex(TTL))); + } catch (final JsonProcessingException e) { + // This should never happen + log.error("Failed to write push latency record JSON", e); + } + } + + void recordQueueRead(final UUID accountUuid, final long deviceId, final String userAgentString) { + takePushRecord(accountUuid, deviceId).thenAccept(pushRecord -> { + if (pushRecord != null) { + final Duration latency = Duration.between(pushRecord.timestamp(), Instant.now()); + + final List tags = new ArrayList<>(3); + + tags.add(UserAgentTagUtil.getPlatformTag(userAgentString)); + tags.add(Tag.of("pushType", pushRecord.pushType().name().toLowerCase())); + + UserAgentTagUtil.getClientVersionTag(userAgentString, clientReleaseManager) + .ifPresent(tags::add); + + pushRecord.urgent().ifPresent(urgent -> tags.add(Tag.of("urgent", String.valueOf(urgent)))); + + Timer.builder(TIMER_NAME) + .publishPercentileHistogram(true) + .tags(tags) + .register(Metrics.globalRegistry) + .record(latency); + } + }); + } + + @VisibleForTesting + CompletableFuture takePushRecord(final UUID accountUuid, final long deviceId) { + final String key = getFirstUnacknowledgedPushKey(accountUuid, deviceId); + + return redisCluster.withCluster(connection -> { + final CompletableFuture getFuture = connection.async().get(key).toCompletableFuture() + .thenApply(recordJson -> { + if (StringUtils.isNotEmpty(recordJson)) { + try { + return SystemMapper.jsonMapper().readValue(recordJson, PushRecord.class); + } catch (JsonProcessingException e) { + return null; + } + } else { + return null; + } + }); + + getFuture.whenComplete((record, cause) -> { + if (cause == null) { + connection.async().del(key); + } + }); + + return getFuture; + }); + } + + private static String getFirstUnacknowledgedPushKey(final UUID accountUuid, final long deviceId) { + return "push_latency::v2::" + accountUuid.toString() + "::" + deviceId; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotification.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotification.java new file mode 100644 index 000000000..37cbda79e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotification.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import javax.annotation.Nullable; + +public record PushNotification(String deviceToken, + TokenType tokenType, + NotificationType notificationType, + @Nullable String data, + @Nullable Account destination, + @Nullable Device destinationDevice, + boolean urgent) { + + public enum NotificationType { + NOTIFICATION, + ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY, + @Deprecated ATTEMPT_LOGIN_NOTIFICATION_LOW_PRIORITY, // Temporary support for iOS clients; can be removed after 2023-06-12 + CHALLENGE, + RATE_LIMIT_CHALLENGE + } + + public enum TokenType { + FCM, + APN, + APN_VOIP, + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationManager.java new file mode 100644 index 000000000..8fdb6171c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationManager.java @@ -0,0 +1,184 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.common.annotations.VisibleForTesting; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import java.util.function.BiConsumer; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.redis.RedisOperation; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.Util; + +public class PushNotificationManager { + + private final AccountsManager accountsManager; + private final APNSender apnSender; + private final FcmSender fcmSender; + private final ApnPushNotificationScheduler apnPushNotificationScheduler; + private final PushLatencyManager pushLatencyManager; + + private static final String SENT_NOTIFICATION_COUNTER_NAME = name(PushNotificationManager.class, "sentPushNotification"); + private static final String FAILED_NOTIFICATION_COUNTER_NAME = name(PushNotificationManager.class, "failedPushNotification"); + + private static final Logger logger = LoggerFactory.getLogger(PushNotificationManager.class); + + public PushNotificationManager(final AccountsManager accountsManager, + final APNSender apnSender, + final FcmSender fcmSender, + final ApnPushNotificationScheduler apnPushNotificationScheduler, + final PushLatencyManager pushLatencyManager) { + + this.accountsManager = accountsManager; + this.apnSender = apnSender; + this.fcmSender = fcmSender; + this.apnPushNotificationScheduler = apnPushNotificationScheduler; + this.pushLatencyManager = pushLatencyManager; + } + + public void sendNewMessageNotification(final Account destination, final long destinationDeviceId, final boolean urgent) throws NotPushRegisteredException { + final Device device = destination.getDevice(destinationDeviceId).orElseThrow(NotPushRegisteredException::new); + final Pair tokenAndType = getToken(device); + + sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(), + PushNotification.NotificationType.NOTIFICATION, null, destination, device, urgent)); + } + + public void sendRegistrationChallengeNotification(final String deviceToken, final PushNotification.TokenType tokenType, final String challengeToken) { + sendNotification(new PushNotification(deviceToken, tokenType, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null, true)); + } + + public void sendRateLimitChallengeNotification(final Account destination, final String challengeToken) + throws NotPushRegisteredException { + + final Device device = destination.getDevice(Device.MASTER_ID).orElseThrow(NotPushRegisteredException::new); + final Pair tokenAndType = getToken(device); + + sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(), + PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, destination, device, true)); + } + + public void sendAttemptLoginNotification(final Account destination, final String context) throws NotPushRegisteredException { + final Device device = destination.getDevice(Device.MASTER_ID).orElseThrow(NotPushRegisteredException::new); + final Pair tokenAndType = getToken(device); + + sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(), + PushNotification.NotificationType.ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY, + context, destination, device, true)); + + // This is a workaround for older iOS clients who need a low priority push to trigger the logout notification + if (tokenAndType.second() == PushNotification.TokenType.APN) { + sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(), + PushNotification.NotificationType.ATTEMPT_LOGIN_NOTIFICATION_LOW_PRIORITY, + context, destination, device, false)); + } + } + + public void handleMessagesRetrieved(final Account account, final Device device, final String userAgent) { + RedisOperation.unchecked(() -> pushLatencyManager.recordQueueRead(account.getUuid(), device.getId(), userAgent)); + apnPushNotificationScheduler.cancelScheduledNotifications(account, device).whenComplete(logErrors()); + } + + @VisibleForTesting + Pair getToken(final Device device) throws NotPushRegisteredException { + final Pair tokenAndType; + + if (StringUtils.isNotBlank(device.getGcmId())) { + tokenAndType = new Pair<>(device.getGcmId(), PushNotification.TokenType.FCM); + } else if (StringUtils.isNotBlank(device.getVoipApnId())) { + tokenAndType = new Pair<>(device.getVoipApnId(), PushNotification.TokenType.APN_VOIP); + } else if (StringUtils.isNotBlank(device.getApnId())) { + tokenAndType = new Pair<>(device.getApnId(), PushNotification.TokenType.APN); + } else { + throw new NotPushRegisteredException(); + } + + return tokenAndType; + } + + @VisibleForTesting + void sendNotification(final PushNotification pushNotification) { + if (pushNotification.tokenType() == PushNotification.TokenType.APN && !pushNotification.urgent()) { + // APNs imposes a per-device limit on background push notifications; schedule a notification for some time in the + // future (possibly even now!) rather than sending a notification directly + apnPushNotificationScheduler + .scheduleBackgroundNotification(pushNotification.destination(), pushNotification.destinationDevice()) + .whenComplete(logErrors()); + + } else { + final PushNotificationSender sender = switch (pushNotification.tokenType()) { + case FCM -> fcmSender; + case APN, APN_VOIP -> apnSender; + }; + + sender.sendNotification(pushNotification).whenComplete((result, throwable) -> { + if (throwable == null) { + Tags tags = Tags.of("tokenType", pushNotification.tokenType().name(), + "notificationType", pushNotification.notificationType().name(), + "urgent", String.valueOf(pushNotification.urgent()), + "accepted", String.valueOf(result.accepted()), + "unregistered", String.valueOf(result.unregistered())); + + if (StringUtils.isNotBlank(result.errorCode())) { + tags = tags.and("errorCode", result.errorCode()); + } + + Metrics.counter(SENT_NOTIFICATION_COUNTER_NAME, tags).increment(); + + if (result.unregistered() && pushNotification.destination() != null + && pushNotification.destinationDevice() != null) { + handleDeviceUnregistered(pushNotification.destination(), pushNotification.destinationDevice()); + } + + if (result.accepted() && + pushNotification.tokenType() == PushNotification.TokenType.APN_VOIP && + pushNotification.notificationType() == PushNotification.NotificationType.NOTIFICATION && + pushNotification.destination() != null && + pushNotification.destinationDevice() != null) { + + apnPushNotificationScheduler.scheduleRecurringVoipNotification( + pushNotification.destination(), + pushNotification.destinationDevice()) + .whenComplete(logErrors()); + } + } else { + logger.debug("Failed to deliver {} push notification to {} ({})", + pushNotification.notificationType(), pushNotification.deviceToken(), pushNotification.tokenType(), + throwable); + + Metrics.counter(FAILED_NOTIFICATION_COUNTER_NAME, "cause", throwable.getClass().getSimpleName()).increment(); + } + }); + } + } + + private static BiConsumer logErrors() { + return (ignored, throwable) -> { + if (throwable != null) { + logger.warn("Failed push scheduling operation", throwable); + } + }; + } + + private void handleDeviceUnregistered(final Account account, final Device device) { + if (StringUtils.isNotBlank(device.getGcmId())) { + if (device.getUninstalledFeedbackTimestamp() == 0) { + accountsManager.updateDevice(account, device.getId(), d -> + d.setUninstalledFeedbackTimestamp(Util.todayInMillis())); + } + } else { + apnPushNotificationScheduler.cancelScheduledNotifications(account, device).whenComplete(logErrors()); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationSender.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationSender.java new file mode 100644 index 000000000..2631efda2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationSender.java @@ -0,0 +1,13 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import java.util.concurrent.CompletableFuture; + +public interface PushNotificationSender { + + CompletableFuture sendNotification(PushNotification notification); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/ReceiptSender.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/ReceiptSender.java new file mode 100644 index 000000000..ae235e528 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/ReceiptSender.java @@ -0,0 +1,81 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import com.codahale.metrics.InstrumentedExecutorService; +import com.codahale.metrics.SharedMetricRegistries; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics; +import java.util.concurrent.ExecutorService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.Constants; + +public class ReceiptSender { + + private final MessageSender messageSender; + private final AccountsManager accountManager; + private final ExecutorService executor; + + private static final Logger logger = LoggerFactory.getLogger(ReceiptSender.class); + + public ReceiptSender(final AccountsManager accountManager, final MessageSender messageSender, + final ExecutorService executor) { + this.accountManager = accountManager; + this.messageSender = messageSender; + this.executor = ExecutorServiceMetrics.monitor( + Metrics.globalRegistry, + new InstrumentedExecutorService(executor, + SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME), + MetricsUtil.name(ReceiptSender.class, "executor")), + MetricsUtil.name(ReceiptSender.class, "executor"), MetricsUtil.PREFIX) + ; + } + + public void sendReceipt(ServiceIdentifier sourceIdentifier, long sourceDeviceId, AciServiceIdentifier destinationIdentifier, long messageId) { + if (sourceIdentifier.equals(destinationIdentifier)) { + return; + } + + executor.submit(() -> { + try { + accountManager.getByAccountIdentifier(destinationIdentifier.uuid()).ifPresentOrElse( + destinationAccount -> { + final Envelope.Builder message = Envelope.newBuilder() + .setServerTimestamp(System.currentTimeMillis()) + .setSourceUuid(sourceIdentifier.toServiceIdentifierString()) + .setSourceDevice((int) sourceDeviceId) + .setDestinationUuid(destinationIdentifier.toServiceIdentifierString()) + .setTimestamp(messageId) + .setType(Envelope.Type.SERVER_DELIVERY_RECEIPT) + .setUrgent(false); + + for (final Device destinationDevice : destinationAccount.getDevices()) { + try { + messageSender.sendMessage(destinationAccount, destinationDevice, message.build(), false); + } catch (final NotPushRegisteredException e) { + logger.debug("User no longer push registered for delivery receipt: {}", e.getMessage()); + } catch (final Exception e) { + logger.warn("Could not send delivery receipt", e); + } + } + }, + () -> logger.info("No longer registered: {}", destinationIdentifier) + ); + + } catch (final Exception e) { + // this exception is most likely a Dynamo timeout or a Redis timeout/circuit breaker + logger.warn("Could not send delivery receipt", e); + } + }); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/SendPushNotificationResult.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/SendPushNotificationResult.java new file mode 100644 index 000000000..fc7abdd85 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/SendPushNotificationResult.java @@ -0,0 +1,11 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import javax.annotation.Nullable; + +public record SendPushNotificationResult(boolean accepted, @Nullable String errorCode, boolean unregistered) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/TransientPushFailureException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/TransientPushFailureException.java new file mode 100644 index 000000000..cedf9c837 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/push/TransientPushFailureException.java @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +public class TransientPushFailureException extends Exception { + public TransientPushFailureException(String s) { + super(s); + } + + public TransientPushFailureException(Exception e) { + super(e); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/ClusterLuaScript.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/ClusterLuaScript.java new file mode 100644 index 000000000..3915f53bd --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/ClusterLuaScript.java @@ -0,0 +1,145 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.redis; + +import com.google.common.annotations.VisibleForTesting; +import io.lettuce.core.RedisException; +import io.lettuce.core.RedisNoScriptException; +import io.lettuce.core.ScriptOutputType; +import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class ClusterLuaScript { + + private final FaultTolerantRedisCluster redisCluster; + private final ScriptOutputType scriptOutputType; + private final String script; + private final String sha; + + private static final String[] STRING_ARRAY = new String[0]; + private static final byte[][] BYTE_ARRAY_ARRAY = new byte[0][]; + + private static final Logger log = LoggerFactory.getLogger(ClusterLuaScript.class); + + public static ClusterLuaScript fromResource(final FaultTolerantRedisCluster redisCluster, + final String resource, + final ScriptOutputType scriptOutputType) throws IOException { + + try (final InputStream inputStream = ClusterLuaScript.class.getClassLoader().getResourceAsStream(resource)) { + if (inputStream == null) { + throw new IllegalArgumentException("Script not found: " + resource); + } + + return new ClusterLuaScript(redisCluster, + new String(inputStream.readAllBytes(), StandardCharsets.UTF_8), + scriptOutputType); + } + } + + @VisibleForTesting + ClusterLuaScript(final FaultTolerantRedisCluster redisCluster, + final String script, + final ScriptOutputType scriptOutputType) { + + this.redisCluster = redisCluster; + this.scriptOutputType = scriptOutputType; + this.script = script; + + try { + this.sha = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-1").digest(script.getBytes(StandardCharsets.UTF_8))); + } catch (final NoSuchAlgorithmException e) { + // All Java implementations are required to support SHA-1, so this should never happen + throw new AssertionError(e); + } + } + + @VisibleForTesting + String getSha() { + return sha; + } + + public Object execute(final List keys, final List args) { + return redisCluster.withCluster(connection -> + execute(connection, keys.toArray(STRING_ARRAY), args.toArray(STRING_ARRAY))); + } + + public CompletableFuture executeAsync(final List keys, final List args) { + return redisCluster.withCluster(connection -> + executeAsync(connection, keys.toArray(STRING_ARRAY), args.toArray(STRING_ARRAY))); + } + + public Flux executeReactive(final List keys, final List args) { + return redisCluster.withCluster(connection -> + executeReactive(connection, keys.toArray(STRING_ARRAY), args.toArray(STRING_ARRAY))); + } + + public Object executeBinary(final List keys, final List args) { + return redisCluster.withBinaryCluster(connection -> + execute(connection, keys.toArray(BYTE_ARRAY_ARRAY), args.toArray(BYTE_ARRAY_ARRAY))); + } + + public CompletableFuture executeBinaryAsync(final List keys, final List args) { + return redisCluster.withBinaryCluster(connection -> + executeAsync(connection, keys.toArray(BYTE_ARRAY_ARRAY), args.toArray(BYTE_ARRAY_ARRAY))); + } + + public Flux executeBinaryReactive(final List keys, final List args) { + return redisCluster.withBinaryCluster(connection -> + executeReactive(connection, keys.toArray(BYTE_ARRAY_ARRAY), args.toArray(BYTE_ARRAY_ARRAY))); + } + + private Object execute(final StatefulRedisClusterConnection connection, final T[] keys, final T[] args) { + try { + try { + return connection.sync().evalsha(sha, scriptOutputType, keys, args); + } catch (final RedisNoScriptException e) { + return connection.sync().eval(script, scriptOutputType, keys, args); + } + } catch (final Exception e) { + log.warn("Failed to execute script", e); + throw e; + } + } + + private CompletableFuture executeAsync(final StatefulRedisClusterConnection connection, + final T[] keys, final T[] args) { + + return connection.async().evalsha(sha, scriptOutputType, keys, args) + .exceptionallyCompose(throwable -> { + if (throwable instanceof RedisNoScriptException) { + return connection.async().eval(script, scriptOutputType, keys, args); + } + + log.warn("Failed to execute script", throwable); + throw new RedisException(throwable); + }).toCompletableFuture(); + } + + private Flux executeReactive(final StatefulRedisClusterConnection connection, + final T[] keys, final T[] args) { + + return connection.reactive().evalsha(sha, scriptOutputType, keys, args) + .onErrorResume(e -> { + if (e instanceof RedisNoScriptException) { + return connection.reactive().eval(script, scriptOutputType, keys, args); + } + + log.warn("Failed to execute script", e); + return Mono.error(e); + }); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/ConnectionEventLogger.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/ConnectionEventLogger.java new file mode 100644 index 000000000..aa059b25b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/ConnectionEventLogger.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.redis; + +import io.lettuce.core.cluster.event.ClusterTopologyChangedEvent; +import io.lettuce.core.event.connection.ConnectionEvent; +import io.lettuce.core.resource.ClientResources; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ConnectionEventLogger { + + private static final Logger logger = LoggerFactory.getLogger(ConnectionEventLogger.class); + + public static void logConnectionEvents(final ClientResources clientResources) { + clientResources.eventBus().get().subscribe(event -> { + if (event instanceof ConnectionEvent) { + logger.debug("Connection event: {}", event); + } else if (event instanceof ClusterTopologyChangedEvent) { + logger.info("Cluster topology changed: {}", event); + } + }); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/FaultTolerantPubSubConnection.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/FaultTolerantPubSubConnection.java new file mode 100644 index 000000000..c620758fb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/FaultTolerantPubSubConnection.java @@ -0,0 +1,102 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.redis; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.retry.Retry; +import io.lettuce.core.RedisException; +import io.lettuce.core.cluster.event.ClusterTopologyChangedEvent; +import io.lettuce.core.cluster.pubsub.StatefulRedisClusterPubSubConnection; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import java.util.function.Consumer; +import java.util.function.Function; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.CircuitBreakerUtil; +import reactor.core.scheduler.Scheduler; + +public class FaultTolerantPubSubConnection { + + private static final Logger logger = LoggerFactory.getLogger(FaultTolerantPubSubConnection.class); + + + private final String name; + private final StatefulRedisClusterPubSubConnection pubSubConnection; + + private final CircuitBreaker circuitBreaker; + private final Retry retry; + private final Retry resubscribeRetry; + private final Scheduler topologyChangedEventScheduler; + + private final Timer executeTimer; + + public FaultTolerantPubSubConnection(final String name, + final StatefulRedisClusterPubSubConnection pubSubConnection, final CircuitBreaker circuitBreaker, + final Retry retry, final Retry resubscribeRetry, final Scheduler topologyChangedEventScheduler) { + this.name = name; + this.pubSubConnection = pubSubConnection; + this.circuitBreaker = circuitBreaker; + this.retry = retry; + this.resubscribeRetry = resubscribeRetry; + this.topologyChangedEventScheduler = topologyChangedEventScheduler; + + this.pubSubConnection.setNodeMessagePropagation(true); + + this.executeTimer = Metrics.timer(name(getClass(), "execute"), "name", name + "-pubsub"); + + CircuitBreakerUtil.registerMetrics(circuitBreaker, FaultTolerantPubSubConnection.class); + } + + public void usePubSubConnection(final Consumer> consumer) { + try { + circuitBreaker.executeCheckedRunnable( + () -> retry.executeRunnable(() -> executeTimer.record(() -> consumer.accept(pubSubConnection)))); + } catch (final Throwable t) { + if (t instanceof RedisException) { + throw (RedisException) t; + } else { + throw new RedisException(t); + } + } + } + + public T withPubSubConnection(final Function, T> function) { + try { + return circuitBreaker.executeCheckedSupplier( + () -> retry.executeCallable(() -> executeTimer.record(() -> function.apply(pubSubConnection)))); + } catch (final Throwable t) { + if (t instanceof RedisException) { + throw (RedisException) t; + } else { + throw new RedisException(t); + } + } + } + + + public void subscribeToClusterTopologyChangedEvents(final Runnable eventHandler) { + + usePubSubConnection(connection -> connection.getResources().eventBus().get() + .filter(event -> event instanceof ClusterTopologyChangedEvent) + .subscribeOn(topologyChangedEventScheduler) + .subscribe(event -> { + logger.info("Got topology change event for {}, resubscribing all keyspace notifications", name); + + resubscribeRetry.executeRunnable(() -> { + try { + eventHandler.run(); + } catch (final RuntimeException e) { + logger.warn("Resubscribe for {} failed", name, e); + throw e; + } + }); + })); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/FaultTolerantRedisCluster.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/FaultTolerantRedisCluster.java new file mode 100644 index 000000000..f9dd2659d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/FaultTolerantRedisCluster.java @@ -0,0 +1,183 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.redis; + +import com.google.common.annotations.VisibleForTesting; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.core.IntervalFunction; +import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator; +import io.github.resilience4j.reactor.retry.RetryOperator; +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; +import io.lettuce.core.ClientOptions.DisconnectedBehavior; +import io.lettuce.core.RedisCommandTimeoutException; +import io.lettuce.core.RedisException; +import io.lettuce.core.TimeoutOptions; +import io.lettuce.core.cluster.ClusterClientOptions; +import io.lettuce.core.cluster.ClusterTopologyRefreshOptions; +import io.lettuce.core.cluster.RedisClusterClient; +import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; +import io.lettuce.core.cluster.pubsub.StatefulRedisClusterPubSubConnection; +import io.lettuce.core.codec.ByteArrayCodec; +import io.lettuce.core.resource.ClientResources; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import org.reactivestreams.Publisher; +import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; +import org.whispersystems.textsecuregcm.configuration.RedisClusterConfiguration; +import org.whispersystems.textsecuregcm.configuration.RetryConfiguration; +import org.whispersystems.textsecuregcm.util.CircuitBreakerUtil; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Schedulers; + +/** + * A fault-tolerant access manager for a Redis cluster. A fault-tolerant Redis cluster provides managed, + * circuit-breaker-protected access to connections. + */ +public class FaultTolerantRedisCluster { + + private final String name; + + private final RedisClusterClient clusterClient; + + private final StatefulRedisClusterConnection stringConnection; + private final StatefulRedisClusterConnection binaryConnection; + + private final List> pubSubConnections = new ArrayList<>(); + + private final CircuitBreaker circuitBreaker; + private final Retry retry; + private final Retry topologyChangedEventRetry; + + public FaultTolerantRedisCluster(final String name, final RedisClusterConfiguration clusterConfiguration, + final ClientResources clientResources) { + this(name, + RedisClusterClient.create(clientResources, + RedisUriUtil.createRedisUriWithTimeout(clusterConfiguration.getConfigurationUri(), + clusterConfiguration.getTimeout())), + clusterConfiguration.getTimeout(), + clusterConfiguration.getCircuitBreakerConfiguration(), + clusterConfiguration.getRetryConfiguration()); + } + + @VisibleForTesting + FaultTolerantRedisCluster(final String name, final RedisClusterClient clusterClient, final Duration commandTimeout, + final CircuitBreakerConfiguration circuitBreakerConfiguration, final RetryConfiguration retryConfiguration) { + this.name = name; + + this.clusterClient = clusterClient; + this.clusterClient.setOptions(ClusterClientOptions.builder() + .disconnectedBehavior(DisconnectedBehavior.REJECT_COMMANDS) + .validateClusterNodeMembership(false) + .topologyRefreshOptions(ClusterTopologyRefreshOptions.builder() + .enableAllAdaptiveRefreshTriggers() + .build()) + // for asynchronous commands + .timeoutOptions(TimeoutOptions.builder() + .fixedTimeout(commandTimeout) + .build()) + .publishOnScheduler(true) + .build()); + + this.stringConnection = clusterClient.connect(); + this.binaryConnection = clusterClient.connect(ByteArrayCodec.INSTANCE); + + this.circuitBreaker = CircuitBreaker.of(name + "-breaker", circuitBreakerConfiguration.toCircuitBreakerConfig()); + this.retry = Retry.of(name + "-retry", retryConfiguration.toRetryConfigBuilder() + .retryOnException(exception -> exception instanceof RedisCommandTimeoutException).build()); + final RetryConfig topologyChangedEventRetryConfig = RetryConfig.custom() + .maxAttempts(Integer.MAX_VALUE) + .intervalFunction( + IntervalFunction.ofExponentialRandomBackoff(Duration.ofSeconds(1), 1.5, Duration.ofSeconds(30))) + .build(); + + this.topologyChangedEventRetry = Retry.of(name + "-topologyChangedRetry", topologyChangedEventRetryConfig); + + CircuitBreakerUtil.registerMetrics(circuitBreaker, FaultTolerantRedisCluster.class); + CircuitBreakerUtil.registerMetrics(retry, FaultTolerantRedisCluster.class); + } + + void shutdown() { + stringConnection.close(); + binaryConnection.close(); + + for (final StatefulRedisClusterPubSubConnection pubSubConnection : pubSubConnections) { + pubSubConnection.close(); + } + + clusterClient.shutdown(); + } + + public String getName() { + return name; + } + + public void useCluster(final Consumer> consumer) { + useConnection(stringConnection, consumer); + } + + public T withCluster(final Function, T> function) { + return withConnection(stringConnection, function); + } + + public void useBinaryCluster(final Consumer> consumer) { + useConnection(binaryConnection, consumer); + } + + public T withBinaryCluster(final Function, T> function) { + return withConnection(binaryConnection, function); + } + + public Publisher withBinaryClusterReactive( + final Function, Publisher> function) { + return withConnectionReactive(binaryConnection, function); + } + + private void useConnection(final StatefulRedisClusterConnection connection, + final Consumer> consumer) { + try { + circuitBreaker.executeCheckedRunnable(() -> retry.executeRunnable(() -> consumer.accept(connection))); + } catch (final Throwable t) { + if (t instanceof RedisException) { + throw (RedisException) t; + } else { + throw new RedisException(t); + } + } + } + + private T withConnection(final StatefulRedisClusterConnection connection, + final Function, T> function) { + try { + return circuitBreaker.executeCheckedSupplier(() -> retry.executeCallable(() -> function.apply(connection))); + } catch (final Throwable t) { + if (t instanceof RedisException) { + throw (RedisException) t; + } else { + throw new RedisException(t); + } + } + } + + private Publisher withConnectionReactive(final StatefulRedisClusterConnection connection, + final Function, Publisher> function) { + + return Flux.from(function.apply(connection)) + .transformDeferred(RetryOperator.of(retry)) + .transformDeferred(CircuitBreakerOperator.of(circuitBreaker)); + } + + public FaultTolerantPubSubConnection createPubSubConnection() { + final StatefulRedisClusterPubSubConnection pubSubConnection = clusterClient.connectPubSub(); + pubSubConnections.add(pubSubConnection); + + return new FaultTolerantPubSubConnection<>(name, pubSubConnection, circuitBreaker, retry, topologyChangedEventRetry, + Schedulers.newSingle(name + "-redisPubSubEvents", true)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/RedisOperation.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/RedisOperation.java new file mode 100644 index 000000000..f4f548db9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/RedisOperation.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.redis; + +import io.lettuce.core.RedisException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RedisOperation { + + private static final Logger logger = LoggerFactory.getLogger(RedisOperation.class); + + /** + * Executes the given task and logs and discards any {@link RedisException} that may be thrown. This method should be + * used for best-effort tasks like gathering metrics. + * + * @param runnable the Redis-related task to be executed + */ + public static void unchecked(final Runnable runnable) { + try { + runnable.run(); + } catch (RedisException e) { + logger.warn("Redis failure", e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/RedisUriUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/RedisUriUtil.java new file mode 100644 index 000000000..79d39e370 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/redis/RedisUriUtil.java @@ -0,0 +1,20 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.redis; + +import io.lettuce.core.RedisURI; +import java.time.Duration; + +public class RedisUriUtil { + + public static RedisURI createRedisUriWithTimeout(final String uri, final Duration timeout) { + final RedisURI redisUri = RedisURI.create(uri); + // for synchronous commands and the initial connection + redisUri.setTimeout(timeout); + return redisUri; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/ClientType.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/ClientType.java new file mode 100644 index 000000000..7a5a9c546 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/ClientType.java @@ -0,0 +1,13 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.registration; + +public enum ClientType { + IOS, + ANDROID_WITH_FCM, + ANDROID_WITHOUT_FCM, + UNKNOWN +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/IdentityTokenCallCredentials.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/IdentityTokenCallCredentials.java new file mode 100644 index 000000000..7b261bcda --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/IdentityTokenCallCredentials.java @@ -0,0 +1,78 @@ +package org.whispersystems.textsecuregcm.registration; + +import com.google.auth.oauth2.ExternalAccountCredentials; +import com.google.auth.oauth2.ImpersonatedCredentials; +import com.google.common.base.Suppliers; +import io.grpc.CallCredentials; +import io.grpc.Metadata; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class IdentityTokenCallCredentials extends CallCredentials { + + private final Supplier identityTokenSupplier; + + private static final Duration IDENTITY_TOKEN_LIFETIME = Duration.ofHours(1); + private static final Duration IDENTITY_TOKEN_REFRESH_BUFFER = Duration.ofMinutes(10); + + private static final Metadata.Key AUTHORIZATION_METADATA_KEY = + Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); + + private static final Logger logger = LoggerFactory.getLogger(IdentityTokenCallCredentials.class); + + IdentityTokenCallCredentials(final Supplier identityTokenSupplier) { + this.identityTokenSupplier = identityTokenSupplier; + } + + static IdentityTokenCallCredentials fromCredentialConfig(final String credentialConfigJson, final String audience) throws IOException { + try (final InputStream configInputStream = new ByteArrayInputStream(credentialConfigJson.getBytes(StandardCharsets.UTF_8))) { + final ExternalAccountCredentials credentials = ExternalAccountCredentials.fromStream(configInputStream); + final ImpersonatedCredentials impersonatedCredentials = ImpersonatedCredentials.create(credentials, + credentials.getServiceAccountEmail(), null, List.of(), (int) IDENTITY_TOKEN_LIFETIME.toSeconds()); + + final Supplier idTokenSupplier = Suppliers.memoizeWithExpiration(() -> { + try { + impersonatedCredentials.getSourceCredentials().refresh(); + return impersonatedCredentials.idTokenWithAudience(audience, null).getTokenValue(); + } catch (final IOException e) { + logger.warn("Failed to retrieve identity token", e); + throw new UncheckedIOException(e); + } + }, + IDENTITY_TOKEN_LIFETIME.minus(IDENTITY_TOKEN_REFRESH_BUFFER).toMillis(), + TimeUnit.MILLISECONDS); + + return new IdentityTokenCallCredentials(idTokenSupplier); + } + } + + @Override + public void applyRequestMetadata(final RequestInfo requestInfo, + final Executor appExecutor, + final MetadataApplier applier) { + + @Nullable final String identityTokenValue = identityTokenSupplier.get(); + + if (identityTokenValue != null) { + final Metadata metadata = new Metadata(); + metadata.put(AUTHORIZATION_METADATA_KEY, "Bearer " + identityTokenValue); + + applier.apply(metadata); + } + } + + @Override + public void thisUsesUnstableApi() { + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/MessageTransport.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/MessageTransport.java new file mode 100644 index 000000000..f45f0f0e3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/MessageTransport.java @@ -0,0 +1,14 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.registration; + +/** + * A message transport is a medium via which verification codes can be delivered to a destination phone. + */ +public enum MessageTransport { + SMS, + VOICE +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java new file mode 100644 index 000000000..bf848b4a6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java @@ -0,0 +1,275 @@ +package org.whispersystems.textsecuregcm.registration; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.Phonenumber; +import com.google.protobuf.ByteString; +import io.dropwizard.lifecycle.Managed; +import io.grpc.ChannelCredentials; +import io.grpc.Deadline; +import io.grpc.Grpc; +import io.grpc.ManagedChannel; +import io.grpc.TlsChannelCredentials; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.signal.registration.rpc.CheckVerificationCodeRequest; +import org.signal.registration.rpc.CreateRegistrationSessionRequest; +import org.signal.registration.rpc.GetRegistrationSessionMetadataRequest; +import org.signal.registration.rpc.RegistrationServiceGrpc; +import org.signal.registration.rpc.RegistrationSessionMetadata; +import org.signal.registration.rpc.SendVerificationCodeRequest; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.controllers.VerificationSessionRateLimitExceededException; +import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; + +public class RegistrationServiceClient implements Managed { + + private final ManagedChannel channel; + private final RegistrationServiceGrpc.RegistrationServiceFutureStub stub; + private final Executor callbackExecutor; + + /** + * @param from an e164 in a {@code long} representation e.g. {@code 18005550123} + * @return the e164 in a {@code String} representation (e.g. {@code "+18005550123"}) + * @throws IllegalArgumentException if the number cannot be parsed to a string + */ + static String convertNumeralE164ToString(long from) { + + try { + final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance() + .parse("+" + from, null); + return PhoneNumberUtil.getInstance() + .format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164); + } catch (final NumberParseException e) { + throw new IllegalArgumentException("could not parse to phone number", e); + } + } + + public RegistrationServiceClient(final String host, + final int port, + final String credentialConfigJson, + final String identityTokenAudience, + final String caCertificatePem, + final Executor callbackExecutor) throws IOException { + + try (final ByteArrayInputStream certificateInputStream = new ByteArrayInputStream(caCertificatePem.getBytes(StandardCharsets.UTF_8))) { + final ChannelCredentials tlsChannelCredentials = TlsChannelCredentials.newBuilder() + .trustManager(certificateInputStream) + .build(); + + this.channel = Grpc.newChannelBuilderForAddress(host, port, tlsChannelCredentials) + .idleTimeout(1, TimeUnit.MINUTES) + .build(); + } + + this.stub = RegistrationServiceGrpc.newFutureStub(channel) + .withCallCredentials(IdentityTokenCallCredentials.fromCredentialConfig(credentialConfigJson, identityTokenAudience)); + + this.callbackExecutor = callbackExecutor; + } + + public CompletableFuture createRegistrationSession( + final Phonenumber.PhoneNumber phoneNumber, final boolean accountExistsWithPhoneNumber, final Duration timeout) { + final long e164 = Long.parseLong( + PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164).substring(1)); + + return toCompletableFuture(stub.withDeadline(toDeadline(timeout)) + .createSession(CreateRegistrationSessionRequest.newBuilder() + .setE164(e164) + .setAccountExistsWithE164(accountExistsWithPhoneNumber) + .build())) + .thenApply(response -> switch (response.getResponseCase()) { + case SESSION_METADATA -> buildSessionResponseFromMetadata(response.getSessionMetadata()); + + case ERROR -> { + switch (response.getError().getErrorType()) { + case CREATE_REGISTRATION_SESSION_ERROR_TYPE_RATE_LIMITED -> throw new CompletionException( + new RateLimitExceededException(response.getError().getMayRetry() + ? Duration.ofSeconds(response.getError().getRetryAfterSeconds()) + : null, + true)); + case CREATE_REGISTRATION_SESSION_ERROR_TYPE_ILLEGAL_PHONE_NUMBER -> throw new IllegalArgumentException(); + default -> throw new RuntimeException( + "Unrecognized error type from registration service: " + response.getError().getErrorType()); + } + } + + case RESPONSE_NOT_SET -> throw new RuntimeException("No response from registration service"); + }); + } + + public CompletableFuture sendVerificationCode(final byte[] sessionId, + final MessageTransport messageTransport, + final ClientType clientType, + @Nullable final String acceptLanguage, + final Duration timeout) { + + final SendVerificationCodeRequest.Builder requestBuilder = SendVerificationCodeRequest.newBuilder() + .setSessionId(ByteString.copyFrom(sessionId)) + .setTransport(getRpcMessageTransport(messageTransport)) + .setClientType(getRpcClientType(clientType)); + + if (StringUtils.isNotBlank(acceptLanguage)) { + requestBuilder.setAcceptLanguage(acceptLanguage); + } + + return toCompletableFuture(stub.withDeadline(toDeadline(timeout)) + .sendVerificationCode(requestBuilder.build())) + .thenApply(response -> { + if (response.hasError()) { + switch (response.getError().getErrorType()) { + case SEND_VERIFICATION_CODE_ERROR_TYPE_RATE_LIMITED -> throw new CompletionException( + new VerificationSessionRateLimitExceededException( + buildSessionResponseFromMetadata(response.getSessionMetadata()), + response.getError().getMayRetry() + ? Duration.ofSeconds(response.getError().getRetryAfterSeconds()) + : null, + true)); + + case SEND_VERIFICATION_CODE_ERROR_TYPE_SESSION_NOT_FOUND -> throw new CompletionException( + new RegistrationServiceException(null)); + + case SEND_VERIFICATION_CODE_ERROR_TYPE_SESSION_ALREADY_VERIFIED -> throw new CompletionException( + new RegistrationServiceException(buildSessionResponseFromMetadata(response.getSessionMetadata()))); + + case SEND_VERIFICATION_CODE_ERROR_TYPE_SENDER_REJECTED -> throw new CompletionException( + RegistrationServiceSenderException.rejected(response.getError().getMayRetry())); + case SEND_VERIFICATION_CODE_ERROR_TYPE_SENDER_ILLEGAL_ARGUMENT -> throw new CompletionException( + RegistrationServiceSenderException.illegalArgument(response.getError().getMayRetry())); + case SEND_VERIFICATION_CODE_ERROR_TYPE_UNSPECIFIED -> throw new CompletionException( + RegistrationServiceSenderException.unknown(response.getError().getMayRetry())); + case SEND_VERIFICATION_CODE_ERROR_TYPE_TRANSPORT_NOT_ALLOWED -> throw new CompletionException( + new TransportNotAllowedException(buildSessionResponseFromMetadata(response.getSessionMetadata()))); + + default -> throw new CompletionException( + new RuntimeException("Failed to send verification code: " + response.getError().getErrorType())); + } + } else { + return buildSessionResponseFromMetadata(response.getSessionMetadata()); + } + }); + } + + public CompletableFuture checkVerificationCode(final byte[] sessionId, + final String verificationCode, + final Duration timeout) { + return toCompletableFuture(stub.withDeadline(toDeadline(timeout)) + .checkVerificationCode(CheckVerificationCodeRequest.newBuilder() + .setSessionId(ByteString.copyFrom(sessionId)) + .setVerificationCode(verificationCode) + .build())) + .thenApply(response -> { + if (response.hasError()) { + switch (response.getError().getErrorType()) { + case CHECK_VERIFICATION_CODE_ERROR_TYPE_RATE_LIMITED -> throw new CompletionException( + new VerificationSessionRateLimitExceededException( + buildSessionResponseFromMetadata(response.getSessionMetadata()), + response.getError().getMayRetry() + ? Duration.ofSeconds(response.getError().getRetryAfterSeconds()) + : null, + true)); + + case CHECK_VERIFICATION_CODE_ERROR_TYPE_NO_CODE_SENT, CHECK_VERIFICATION_CODE_ERROR_TYPE_ATTEMPT_EXPIRED -> + throw new CompletionException( + new RegistrationServiceException(buildSessionResponseFromMetadata(response.getSessionMetadata())) + ); + + case CHECK_VERIFICATION_CODE_ERROR_TYPE_SESSION_NOT_FOUND -> throw new CompletionException( + new RegistrationServiceException(null) + ); + + default -> throw new CompletionException( + new RuntimeException("Failed to check verification code: " + response.getError().getErrorType())); + } + } else { + return buildSessionResponseFromMetadata(response.getSessionMetadata()); + } + }); + } + + public CompletableFuture> getSession(final byte[] sessionId, + final Duration timeout) { + return toCompletableFuture(stub.withDeadline(toDeadline(timeout)).getSessionMetadata( + GetRegistrationSessionMetadataRequest.newBuilder() + .setSessionId(ByteString.copyFrom(sessionId)).build())) + .thenApply(response -> { + if (response.hasError()) { + switch (response.getError().getErrorType()) { + case GET_REGISTRATION_SESSION_METADATA_ERROR_TYPE_NOT_FOUND -> { + return Optional.empty(); + } + default -> throw new RuntimeException("Failed to get session: " + response.getError().getErrorType()); + } + } + + return Optional.of(buildSessionResponseFromMetadata(response.getSessionMetadata())); + }); + } + + private static RegistrationServiceSession buildSessionResponseFromMetadata( + final RegistrationSessionMetadata sessionMetadata) { + return new RegistrationServiceSession(sessionMetadata.getSessionId().toByteArray(), + convertNumeralE164ToString(sessionMetadata.getE164()), sessionMetadata); + } + + private static Deadline toDeadline(final Duration timeout) { + return Deadline.after(timeout.toMillis(), TimeUnit.MILLISECONDS); + } + + private static org.signal.registration.rpc.ClientType getRpcClientType(final ClientType clientType) { + return switch (clientType) { + case IOS -> org.signal.registration.rpc.ClientType.CLIENT_TYPE_IOS; + case ANDROID_WITH_FCM -> org.signal.registration.rpc.ClientType.CLIENT_TYPE_ANDROID_WITH_FCM; + case ANDROID_WITHOUT_FCM -> org.signal.registration.rpc.ClientType.CLIENT_TYPE_ANDROID_WITHOUT_FCM; + case UNKNOWN -> org.signal.registration.rpc.ClientType.CLIENT_TYPE_UNSPECIFIED; + }; + } + + private static org.signal.registration.rpc.MessageTransport getRpcMessageTransport(final MessageTransport transport) { + return switch (transport) { + case SMS -> org.signal.registration.rpc.MessageTransport.MESSAGE_TRANSPORT_SMS; + case VOICE -> org.signal.registration.rpc.MessageTransport.MESSAGE_TRANSPORT_VOICE; + }; + } + + private CompletableFuture toCompletableFuture(final ListenableFuture listenableFuture) { + final CompletableFuture completableFuture = new CompletableFuture<>(); + + Futures.addCallback(listenableFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable final T result) { + completableFuture.complete(result); + } + + @Override + public void onFailure(final Throwable throwable) { + completableFuture.completeExceptionally(throwable); + } + }, callbackExecutor); + + return completableFuture; + } + + @Override + public void start() throws Exception { + } + + @Override + public void stop() throws Exception { + if (channel != null) { + channel.shutdown(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceException.java new file mode 100644 index 000000000..69f7ad229 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceException.java @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.registration; + +import java.util.Optional; +import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; + +/** + * When the Registration Service returns an error, it will also return the latest {@link RegistrationServiceSession} + * data, so that clients may have the latest details on requesting and submitting codes. + */ +public class RegistrationServiceException extends Exception { + + private final RegistrationServiceSession registrationServiceSession; + + public RegistrationServiceException(final RegistrationServiceSession registrationServiceSession) { + super(null, null, true, false); + this.registrationServiceSession = registrationServiceSession; + } + + /** + * @return if empty, the session that encountered should be considered non-existent and may be discarded + */ + public Optional getRegistrationSession() { + return Optional.ofNullable(registrationServiceSession); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceSenderException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceSenderException.java new file mode 100644 index 000000000..a2f9372ab --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceSenderException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.registration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * An error from an SMS/voice provider (“sender”) downstream of Registration Service is mapped to a {@link Reason}, and + * may be permanent. + */ +public class RegistrationServiceSenderException extends Exception { + + private final Reason reason; + private final boolean permanent; + + public static RegistrationServiceSenderException illegalArgument(final boolean permanent) { + return new RegistrationServiceSenderException(Reason.ILLEGAL_ARGUMENT, permanent); + } + + public static RegistrationServiceSenderException rejected(final boolean permanent) { + return new RegistrationServiceSenderException(Reason.PROVIDER_REJECTED, permanent); + } + + public static RegistrationServiceSenderException unknown(final boolean permanent) { + return new RegistrationServiceSenderException(Reason.PROVIDER_UNAVAILABLE, permanent); + } + + private RegistrationServiceSenderException(final Reason reason, final boolean permanent) { + super(null, null, true, false); + this.reason = reason; + this.permanent = permanent; + } + + public Reason getReason() { + return reason; + } + + public boolean isPermanent() { + return permanent; + } + + public enum Reason { + + @JsonProperty("providerUnavailable") + PROVIDER_UNAVAILABLE, + @JsonProperty("providerRejected") + PROVIDER_REJECTED, + @JsonProperty("illegalArgument") + ILLEGAL_ARGUMENT + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/TransportNotAllowedException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/TransportNotAllowedException.java new file mode 100644 index 000000000..095858ac6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/TransportNotAllowedException.java @@ -0,0 +1,14 @@ +package org.whispersystems.textsecuregcm.registration; + +import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; + +/** + * Indicates that a request to send a verification code failed because the destination number does not support the + * requested transport (e.g. the caller asked to send an SMS to a landline number). + */ +public class TransportNotAllowedException extends RegistrationServiceException { + + public TransportNotAllowedException(RegistrationServiceSession registrationServiceSession) { + super(registrationServiceSession); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/VerificationSession.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/VerificationSession.java new file mode 100644 index 000000000..d9f7c7810 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/registration/VerificationSession.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.registration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; +import java.util.List; +import javax.annotation.Nullable; +import org.whispersystems.textsecuregcm.storage.SerializedExpireableJsonDynamoStore; + +/** + * Server-internal stored session object. Primarily used by + * {@link org.whispersystems.textsecuregcm.controllers.VerificationController} to manage the steps required to begin + * requesting codes from Registration Service, in order to get a verified session to be provided to + * {@link org.whispersystems.textsecuregcm.controllers.RegistrationController}. + * + * @param pushChallenge the value of a push challenge sent to a client, after it submitted a push token + * @param requestedInformation information requested that a client send to the server + * @param submittedInformation information that a client has submitted and that the server has verified + * @param allowedToRequestCode whether the client is allowed to request a code. This request will be forwarded to + * Registration Service + * @param createdTimestamp when this session was created + * @param updatedTimestamp when this session was updated + * @param remoteExpirationSeconds when the remote + * {@link org.whispersystems.textsecuregcm.entities.RegistrationServiceSession} expires + * @see org.whispersystems.textsecuregcm.entities.RegistrationServiceSession + * @see org.whispersystems.textsecuregcm.entities.VerificationSessionResponse + */ +public record VerificationSession(@Nullable String pushChallenge, + List requestedInformation, List submittedInformation, + boolean allowedToRequestCode, long createdTimestamp, long updatedTimestamp, + long remoteExpirationSeconds) implements + SerializedExpireableJsonDynamoStore.Expireable { + + @Override + public long getExpirationEpochSeconds() { + return Instant.ofEpochMilli(updatedTimestamp).plusSeconds(remoteExpirationSeconds).getEpochSecond(); + } + + public enum Information { + @JsonProperty("pushChallenge") + PUSH_CHALLENGE, + @JsonProperty("captcha") + CAPTCHA + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/s3/PolicySigner.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/s3/PolicySigner.java new file mode 100644 index 000000000..44381ecbc --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/s3/PolicySigner.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.s3; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HexFormat; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +public class PolicySigner { + + private final String awsAccessSecret; + private final String region; + + public PolicySigner(String awsAccessSecret, String region) { + this.awsAccessSecret = awsAccessSecret; + this.region = region; + } + + public String getSignature(ZonedDateTime now, String policy) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + + mac.init(new SecretKeySpec(("AWS4" + awsAccessSecret).getBytes("UTF-8"), "HmacSHA256")); + byte[] dateKey = mac.doFinal(now.format(DateTimeFormatter.ofPattern("yyyyMMdd")).getBytes("UTF-8")); + + mac.init(new SecretKeySpec(dateKey, "HmacSHA256")); + byte[] dateRegionKey = mac.doFinal(region.getBytes("UTF-8")); + + mac.init(new SecretKeySpec(dateRegionKey, "HmacSHA256")); + byte[] dateRegionServiceKey = mac.doFinal("s3".getBytes("UTF-8")); + + mac.init(new SecretKeySpec(dateRegionServiceKey, "HmacSHA256")); + byte[] signingKey = mac.doFinal("aws4_request".getBytes("UTF-8")); + + mac.init(new SecretKeySpec(signingKey, "HmacSHA256")); + + return HexFormat.of().formatHex(mac.doFinal(policy.getBytes("UTF-8"))); + } catch (NoSuchAlgorithmException | InvalidKeyException | UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/s3/PostPolicyGenerator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/s3/PostPolicyGenerator.java new file mode 100644 index 000000000..744891e8e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/s3/PostPolicyGenerator.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.s3; + +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import org.whispersystems.textsecuregcm.util.Pair; + +public class PostPolicyGenerator { + + public static final DateTimeFormatter AWS_DATE_TIME = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssX"); + private static final DateTimeFormatter CREDENTIAL_DATE = DateTimeFormatter.ofPattern("yyyyMMdd" ); + + private final String region; + private final String bucket; + private final String awsAccessId; + + public PostPolicyGenerator(String region, String bucket, String awsAccessId) { + this.region = region; + this.bucket = bucket; + this.awsAccessId = awsAccessId; + } + + public Pair createFor(ZonedDateTime now, String object, int maxSizeInBytes) { + String expiration = now.plusMinutes(30).format(DateTimeFormatter.ISO_INSTANT); + String credentialDate = now.format(CREDENTIAL_DATE); + String requestDate = now.format(AWS_DATE_TIME); + String credential = String.format("%s/%s/%s/s3/aws4_request", awsAccessId, credentialDate, region); + + String policy = String.format("{ \"expiration\": \"%s\",\n" + + " \"conditions\": [\n" + + " {\"bucket\": \"%s\"},\n" + + " {\"key\": \"%s\"},\n" + + " {\"acl\": \"private\"},\n" + + " [\"starts-with\", \"$Content-Type\", \"\"],\n" + + " [\"content-length-range\", 1, " + maxSizeInBytes + "],\n" + + "\n" + + " {\"x-amz-credential\": \"%s\"},\n" + + " {\"x-amz-algorithm\": \"AWS4-HMAC-SHA256\"},\n" + + " {\"x-amz-date\": \"%s\" }\n" + + " ]\n" + + "}", expiration, bucket, object, credential, requestDate); + + return new Pair<>(credential, Base64.getEncoder().encodeToString(policy.getBytes(StandardCharsets.UTF_8))); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClient.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClient.java new file mode 100644 index 000000000..6fe2f7334 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClient.java @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.securestorage; + +import static org.whispersystems.textsecuregcm.util.HeaderUtils.basicAuthHeader; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.HttpHeaders; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.security.cert.CertificateException; +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration; +import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; +import org.whispersystems.textsecuregcm.util.HttpUtils; + +/** + * A client for sending requests to Signal's secure storage service on behalf of authenticated users. + */ +public class SecureStorageClient { + + private final ExternalServiceCredentialsGenerator storageServiceCredentialsGenerator; + private final URI deleteUri; + private final FaultTolerantHttpClient httpClient; + + @VisibleForTesting + static final String DELETE_PATH = "/v1/storage"; + + public SecureStorageClient(final ExternalServiceCredentialsGenerator storageServiceCredentialsGenerator, + final Executor executor, final + ScheduledExecutorService retryExecutor, final SecureStorageServiceConfiguration configuration) + throws CertificateException { + this.storageServiceCredentialsGenerator = storageServiceCredentialsGenerator; + this.deleteUri = URI.create(configuration.uri()).resolve(DELETE_PATH); + this.httpClient = FaultTolerantHttpClient.newBuilder() + .withCircuitBreaker(configuration.circuitBreaker()) + .withRetry(configuration.retry()) + .withRetryExecutor(retryExecutor) + .withVersion(HttpClient.Version.HTTP_1_1) + .withConnectTimeout(Duration.ofSeconds(10)) + .withRedirect(HttpClient.Redirect.NEVER) + .withExecutor(executor) + .withName("secure-storage") + .withSecurityProtocol(FaultTolerantHttpClient.SECURITY_PROTOCOL_TLS_1_3) + .withTrustedServerCertificates(configuration.storageCaCertificates().toArray(new String[0])) + .build(); + } + + public CompletableFuture deleteStoredData(final UUID accountUuid) { + final ExternalServiceCredentials credentials = storageServiceCredentialsGenerator.generateForUuid(accountUuid); + + final HttpRequest request = HttpRequest.newBuilder() + .uri(deleteUri) + .DELETE() + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader(credentials)) + .build(); + + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenApply(response -> { + if (HttpUtils.isSuccessfulResponse(response.statusCode())) { + return null; + } + + throw new SecureStorageException("Failed to delete storage service data: " + response.statusCode()); + }); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageException.java new file mode 100644 index 000000000..31a06de0a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageException.java @@ -0,0 +1,13 @@ +/* + * Copyright 2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.securestorage; + +public class SecureStorageException extends RuntimeException { + + public SecureStorageException(final String message) { + super(message); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2Client.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2Client.java new file mode 100644 index 000000000..465958be5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2Client.java @@ -0,0 +1,79 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.securevaluerecovery; + +import static org.whispersystems.textsecuregcm.util.HeaderUtils.basicAuthHeader; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.HttpHeaders; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.security.cert.CertificateException; +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration; +import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; +import org.whispersystems.textsecuregcm.util.HttpUtils; + +/** + * A client for sending requests to Signal's secure value recovery v2 service on behalf of authenticated users. + */ +public class SecureValueRecovery2Client { + + private final ExternalServiceCredentialsGenerator secureValueRecoveryCredentialsGenerator; + private final URI deleteUri; + private final FaultTolerantHttpClient httpClient; + + @VisibleForTesting + static final String DELETE_PATH = "/v1/delete"; + + public SecureValueRecovery2Client(final ExternalServiceCredentialsGenerator secureValueRecoveryCredentialsGenerator, + final Executor executor, final ScheduledExecutorService retryExecutor, + final SecureValueRecovery2Configuration configuration) + throws CertificateException { + this.secureValueRecoveryCredentialsGenerator = secureValueRecoveryCredentialsGenerator; + this.deleteUri = URI.create(configuration.uri()).resolve(DELETE_PATH); + this.httpClient = FaultTolerantHttpClient.newBuilder() + .withCircuitBreaker(configuration.circuitBreaker()) + .withRetry(configuration.retry()) + .withRetryExecutor(retryExecutor) + .withVersion(HttpClient.Version.HTTP_1_1) + .withConnectTimeout(Duration.ofSeconds(10)) + .withRedirect(HttpClient.Redirect.NEVER) + .withExecutor(executor) + .withName("secure-value-recovery") + .withSecurityProtocol(FaultTolerantHttpClient.SECURITY_PROTOCOL_TLS_1_2) + .withTrustedServerCertificates(configuration.svrCaCertificates().toArray(new String[0])) + .build(); + } + + public CompletableFuture deleteBackups(final UUID accountUuid) { + + final ExternalServiceCredentials credentials = secureValueRecoveryCredentialsGenerator.generateForUuid(accountUuid); + + final HttpRequest request = HttpRequest.newBuilder() + .uri(deleteUri) + .DELETE() + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader(credentials)) + .build(); + + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenApply(response -> { + if (HttpUtils.isSuccessfulResponse(response.statusCode())) { + return null; + } + + throw new SecureValueRecoveryException("Failed to delete backup: " + response.statusCode()); + }); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecoveryException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecoveryException.java new file mode 100644 index 000000000..e6a3a9b70 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecoveryException.java @@ -0,0 +1,13 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.securevaluerecovery; + +public class SecureValueRecoveryException extends RuntimeException { + + public SecureValueRecoveryException(final String message) { + super(message); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/ChallengeType.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/ChallengeType.java new file mode 100644 index 000000000..500d65e47 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/ChallengeType.java @@ -0,0 +1,11 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.spam; + +public enum ChallengeType { + PUSH, + CAPTCHA +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/Extract.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/Extract.java new file mode 100644 index 000000000..aa0a407c4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/Extract.java @@ -0,0 +1,20 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.spam; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that a parameter should be parsed from a {@link org.glassfish.jersey.server.ContainerRequest} + */ +@Target({ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Extract { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/FilterSpam.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/FilterSpam.java new file mode 100644 index 000000000..9a771acfd --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/FilterSpam.java @@ -0,0 +1,21 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.spam; + +import javax.ws.rs.NameBinding; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A name-binding annotation that associates {@link SpamFilter}s with resource methods. + */ +@NameBinding +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface FilterSpam { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/RateLimitChallengeListener.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/RateLimitChallengeListener.java new file mode 100644 index 000000000..d4e58e752 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/RateLimitChallengeListener.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.spam; + + +import org.whispersystems.textsecuregcm.storage.Account; +import java.io.IOException; + +public interface RateLimitChallengeListener { + + void handleRateLimitChallengeAnswered(Account account, ChallengeType type); + + /** + * Configures this rate limit challenge listener. This method will be called before the service begins processing any + * challenges. + * + * @param environmentName the name of the environment in which this listener is running (e.g. "staging" or "production") + * @throws IOException if the listener could not read its configuration source for any reason + */ + void configure(String environmentName) throws IOException; +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/RateLimitChallengeType.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/RateLimitChallengeType.java new file mode 100644 index 000000000..2b7f298e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/RateLimitChallengeType.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.spam; + +public enum RateLimitChallengeType { + + PUSH_CHALLENGE, + RECAPTCHA +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/ReportSpamTokenProvider.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/ReportSpamTokenProvider.java new file mode 100644 index 000000000..00e616434 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/ReportSpamTokenProvider.java @@ -0,0 +1,28 @@ +package org.whispersystems.textsecuregcm.spam; + +import javax.ws.rs.container.ContainerRequestContext; +import java.util.Optional; +import java.util.function.Function; + +/** + * Generates ReportSpamTokens to be used for spam reports. + */ +public interface ReportSpamTokenProvider { + + /** + * Generate a new ReportSpamToken + * + * @param context the message request context + * @return either a generated token or nothing + */ + Optional makeReportSpamToken(ContainerRequestContext context); + + /** + * Provider which generates nothing + * + * @return the provider + */ + static ReportSpamTokenProvider noop() { + return context -> Optional.empty(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/ScoreThreshold.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/ScoreThreshold.java new file mode 100644 index 000000000..e2d463435 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/ScoreThreshold.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.spam; + +import org.glassfish.jersey.server.ContainerRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.Optional; + +/** + * A ScoreThreshold may be provided by an upstream request filter. If request contains a property for + * SCORE_THRESHOLD_PROPERTY_NAME it can be forwarded to a downstream filter to indicate it can use + * a more or less strict score threshold when evaluating whether a request should be allowed to continue. + */ +public class ScoreThreshold { + private static final Logger logger = LoggerFactory.getLogger(ScoreThreshold.class); + + public static final String PROPERTY_NAME = "scoreThreshold"; + + /** + * A score threshold in the range [0, 1.0] + */ + private final Optional scoreThreshold; + + /** + * Extract an optional score threshold parameter provided by an upstream request filter + */ + public ScoreThreshold(final ContainerRequest containerRequest) { + this.scoreThreshold = Optional + .ofNullable(containerRequest.getProperty(PROPERTY_NAME)) + .flatMap(obj -> { + if (obj instanceof Float f) { + return Optional.of(f); + } + logger.warn("invalid format for filter provided score threshold {}", obj); + return Optional.empty(); + }); + } + + public Optional getScoreThreshold() { + return this.scoreThreshold; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/ScoreThresholdProvider.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/ScoreThresholdProvider.java new file mode 100644 index 000000000..844294fa7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/ScoreThresholdProvider.java @@ -0,0 +1,55 @@ +package org.whispersystems.textsecuregcm.spam; + +import java.util.function.Function; +import javax.inject.Singleton; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.model.Parameter; +import org.glassfish.jersey.server.spi.internal.ValueParamProvider; + +/** + * Parses a {@link ScoreThreshold} out of a {@link ContainerRequest} to provide to jersey resources. + * + * A request filter may enrich a ContainerRequest with a scoreThreshold by providing a float property with the name + * {@link ScoreThreshold#PROPERTY_NAME}. This indicates the desired scoreThreshold to use when evaluating whether a + * request should proceed. + * + * A resource can consume a ScoreThreshold with by annotating a ScoreThreshold parameter with {@link Extract} + */ +public class ScoreThresholdProvider implements ValueParamProvider { + + /** + * Configures the ScoreThresholdProvider + */ + public static class ScoreThresholdFeature implements Feature { + @Override + public boolean configure(FeatureContext context) { + context.register(new AbstractBinder() { + @Override + protected void configure() { + bind(ScoreThresholdProvider.class) + .to(ValueParamProvider.class) + .in(Singleton.class); + } + }); + return true; + } + } + + @Override + public Function getValueProvider(final Parameter parameter) { + if (parameter.getRawType().equals(ScoreThreshold.class) + && parameter.isAnnotationPresent(Extract.class)) { + return ScoreThreshold::new; + } + return null; + + } + + @Override + public PriorityType getPriority() { + return Priority.HIGH; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/SpamFilter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/SpamFilter.java new file mode 100644 index 000000000..2591f0ae3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/spam/SpamFilter.java @@ -0,0 +1,50 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.spam; + +import io.dropwizard.lifecycle.Managed; +import org.whispersystems.textsecuregcm.storage.ReportedMessageListener; +import javax.ws.rs.container.ContainerRequestFilter; +import java.io.IOException; +import java.util.List; + +/** + * A spam filter is a {@link ContainerRequestFilter} that filters requests to message-sending endpoints to + * detect and respond to patterns of spam. + *

    + * Spam filters are managed components that are generally loaded dynamically via a + * {@link java.util.ServiceLoader}. Their {@link #configure(String)} method will be called prior to be adding to the + * server's pool of {@link Managed} objects. + *

    + * Spam filters must be annotated with {@link FilterSpam}, a name binding annotation that + * restricts the endpoints to which the filter may apply. + */ +public interface SpamFilter extends ContainerRequestFilter, Managed { + + /** + * Configures this spam filter. This method will be called before the filter is added to the server's pool + * of managed objects and before the server processes any requests. + * + * @param environmentName the name of the environment in which this filter is running (e.g. "staging" or "production") + * @throws IOException if the filter could not read its configuration source for any reason + */ + void configure(String environmentName) throws IOException; + + /** + * Builds a spam report token provider. This will generate tokens used by the spam reporting system. + * + * @return the configured spam report token provider. + */ + ReportSpamTokenProvider getReportSpamTokenProvider(); + + /** + * Return any and all reported message listeners controlled by the spam filter. Listeners will be registered with the + * {@link org.whispersystems.textsecuregcm.storage.ReportMessageManager}. + * + * @return a list of reported message listeners controlled by the spam filter + */ + List getReportedMessageListeners(); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AbstractDynamoDbStore.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AbstractDynamoDbStore.java new file mode 100644 index 000000000..a5abc8d56 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AbstractDynamoDbStore.java @@ -0,0 +1,107 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static com.codahale.metrics.MetricRegistry.name; +import static io.micrometer.core.instrument.Metrics.counter; +import static io.micrometer.core.instrument.Metrics.timer; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Timer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemResponse; +import software.amazon.awssdk.services.dynamodb.model.ScanRequest; +import software.amazon.awssdk.services.dynamodb.model.WriteRequest; +import javax.annotation.Nonnull; + +public abstract class AbstractDynamoDbStore { + + private static final int MAX_ATTEMPTS_TO_SAVE_BATCH_WRITE = 25; // This was arbitrarily chosen and may be entirely too high. + + public static final int DYNAMO_DB_MAX_BATCH_SIZE = 25; // This limit comes from Amazon Dynamo DB itself. It will reject batch writes larger than this. + + public static final int RESULT_SET_CHUNK_SIZE = 100; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final Timer batchWriteItemsFirstPass = timer(name(getClass(), "batchWriteItems"), "firstAttempt", "true"); + + private final Timer batchWriteItemsRetryPass = timer(name(getClass(), "batchWriteItems"), "firstAttempt", "false"); + + private final Counter batchWriteItemsUnprocessed = counter(name(getClass(), "batchWriteItemsUnprocessed")); + + private final DynamoDbClient dynamoDbClient; + + + public AbstractDynamoDbStore(final DynamoDbClient dynamoDbClient) { + this.dynamoDbClient = dynamoDbClient; + } + + protected DynamoDbClient db() { + return dynamoDbClient; + } + + protected void executeTableWriteItemsUntilComplete(final Map> items) { + final AtomicReference outcome = new AtomicReference<>(); + writeAndStoreOutcome(items, batchWriteItemsFirstPass, outcome); + int attemptCount = 0; + while (!outcome.get().unprocessedItems().isEmpty() && attemptCount < MAX_ATTEMPTS_TO_SAVE_BATCH_WRITE) { + writeAndStoreOutcome(outcome.get().unprocessedItems(), batchWriteItemsRetryPass, outcome); + ++attemptCount; + } + if (!outcome.get().unprocessedItems().isEmpty()) { + final int totalItems = outcome.get().unprocessedItems().values().stream().mapToInt(List::size).sum(); + logger.error( + "Attempt count ({}) reached max ({}}) before applying all batch writes to dynamo. {} unprocessed items remain.", + attemptCount, MAX_ATTEMPTS_TO_SAVE_BATCH_WRITE, totalItems); + batchWriteItemsUnprocessed.increment(totalItems); + } + } + + @Nonnull + protected List> scan(final ScanRequest scanRequest, final int max) { + return db().scanPaginator(scanRequest) + .items() + .stream() + .limit(max) + .toList(); + } + + private void writeAndStoreOutcome( + final Map> items, + final Timer timer, + final AtomicReference outcome) { + timer.record( + () -> outcome.set(dynamoDbClient.batchWriteItem(BatchWriteItemRequest.builder().requestItems(items).build())) + ); + } + + static void writeInBatches(final Iterable items, final Consumer> action) { + final List batch = new ArrayList<>(DYNAMO_DB_MAX_BATCH_SIZE); + + for (final T item : items) { + batch.add(item); + + if (batch.size() == DYNAMO_DB_MAX_BATCH_SIZE) { + action.accept(batch); + batch.clear(); + } + } + if (!batch.isEmpty()) { + action.accept(batch); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java new file mode 100644 index 000000000..38d8f9ddf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -0,0 +1,534 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.time.Clock; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Predicate; +import javax.annotation.Nullable; +import org.signal.libsignal.protocol.IdentityKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; +import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; +import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter; +import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter; +import org.whispersystems.textsecuregcm.util.Util; + +@JsonFilter("Account") +public class Account { + + private static final Logger logger = LoggerFactory.getLogger(Account.class); + + @JsonProperty + private UUID uuid; + + @JsonProperty("pni") + private UUID phoneNumberIdentifier; + + @JsonProperty + private String number; + + @JsonProperty + @JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class) + @Nullable + private byte[] usernameHash; + + @JsonProperty + @JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class) + @Nullable + private byte[] reservedUsernameHash; + + @JsonProperty + @Nullable + private UUID usernameLinkHandle; + + @JsonProperty("eu") + @Nullable + private byte[] encryptedUsername; + + @JsonProperty + private List devices = new ArrayList<>(); + + @JsonProperty + @JsonSerialize(using = IdentityKeyAdapter.Serializer.class) + @JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class) + private IdentityKey identityKey; + + @JsonProperty("pniIdentityKey") + @JsonSerialize(using = IdentityKeyAdapter.Serializer.class) + @JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class) + private IdentityKey phoneNumberIdentityKey; + + @JsonProperty("cpv") + private String currentProfileVersion; + + @JsonProperty + private List badges = new ArrayList<>(); + + @JsonProperty + private String registrationLock; + + @JsonProperty + private String registrationLockSalt; + + @JsonProperty("uak") + private byte[] unidentifiedAccessKey; + + @JsonProperty("uua") + private boolean unrestrictedUnidentifiedAccess; + + @JsonProperty("inCds") + private boolean discoverableByPhoneNumber = true; + + @JsonProperty + private int version; + + @JsonIgnore + private boolean stale; + + public UUID getIdentifier(final IdentityType identityType) { + return switch (identityType) { + case ACI -> getUuid(); + case PNI -> getPhoneNumberIdentifier(); + }; + } + + public UUID getUuid() { + // this is the one method that may be called on a stale account + return uuid; + } + + public void setUuid(final UUID uuid) { + requireNotStale(); + + this.uuid = uuid; + } + + public UUID getPhoneNumberIdentifier() { + requireNotStale(); + + return phoneNumberIdentifier; + } + + /** + * Tests whether this account's account identifier or phone number identifier (depending on the given service + * identifier's identity type) matches the given service identifier. + * + * @param serviceIdentifier the identifier to test + * @return {@code true} if this account's identifier or phone number identifier matches + */ + public boolean isIdentifiedBy(final ServiceIdentifier serviceIdentifier) { + return switch (serviceIdentifier.identityType()) { + case ACI -> serviceIdentifier.uuid().equals(uuid); + case PNI -> serviceIdentifier.uuid().equals(phoneNumberIdentifier); + }; + } + + public String getNumber() { + requireNotStale(); + + return number; + } + + public void setNumber(final String number, final UUID phoneNumberIdentifier) { + requireNotStale(); + + this.number = number; + this.phoneNumberIdentifier = phoneNumberIdentifier; + } + + public Optional getUsernameHash() { + requireNotStale(); + + return Optional.ofNullable(usernameHash); + } + + public void setUsernameHash(final byte[] usernameHash) { + requireNotStale(); + + this.usernameHash = usernameHash; + } + + public Optional getReservedUsernameHash() { + requireNotStale(); + + return Optional.ofNullable(reservedUsernameHash); + } + + public void setReservedUsernameHash(final byte[] reservedUsernameHash) { + requireNotStale(); + + this.reservedUsernameHash = reservedUsernameHash; + } + + @Nullable + public UUID getUsernameLinkHandle() { + requireNotStale(); + return usernameLinkHandle; + } + + public Optional getEncryptedUsername() { + requireNotStale(); + return Optional.ofNullable(encryptedUsername); + } + + public void setUsernameLinkDetails(@Nullable final UUID usernameLinkHandle, @Nullable final byte[] encryptedUsername) { + requireNotStale(); + if ((usernameLinkHandle == null) ^ (encryptedUsername == null)) { + throw new IllegalArgumentException("Both or neither arguments must be null"); + } + if (usernameHash == null && encryptedUsername != null) { + throw new IllegalArgumentException("usernameHash field must be set to store username link"); + } + this.encryptedUsername = encryptedUsername; + this.usernameLinkHandle = usernameLinkHandle; + } + + /* + * This method is intentionally left package-private so that it's only used + * when Account is read from DB + */ + void setUsernameLinkHandle(@Nullable final UUID usernameLinkHandle) { + requireNotStale(); + this.usernameLinkHandle = usernameLinkHandle; + } + + public void addDevice(final Device device) { + requireNotStale(); + + removeDevice(device.getId()); + this.devices.add(device); + } + + public void removeDevice(final long deviceId) { + requireNotStale(); + + this.devices.removeIf(device -> device.getId() == deviceId); + } + + public List getDevices() { + requireNotStale(); + + return devices; + } + + public Optional getMasterDevice() { + requireNotStale(); + + return getDevice(Device.MASTER_ID); + } + + public Optional getDevice(final long deviceId) { + requireNotStale(); + + return devices.stream().filter(device -> device.getId() == deviceId).findFirst(); + } + + public boolean isStorageSupported() { + requireNotStale(); + + return devices.stream().anyMatch(device -> device.getCapabilities() != null && device.getCapabilities().storage()); + } + + public boolean isTransferSupported() { + requireNotStale(); + + return getMasterDevice().map(Device::getCapabilities).map(Device.DeviceCapabilities::transfer).orElse(false); + } + + public boolean isPniSupported() { + return allEnabledDevicesHaveCapability(DeviceCapabilities::pni); + } + + public boolean isPaymentActivationSupported() { + return allEnabledDevicesHaveCapability(DeviceCapabilities::paymentActivation); + } + + private boolean allEnabledDevicesHaveCapability(final Predicate predicate) { + requireNotStale(); + + return devices.stream() + .filter(Device::isEnabled) + .allMatch(device -> device.getCapabilities() != null && predicate.test(device.getCapabilities())); + } + + public boolean isEnabled() { + requireNotStale(); + + return getMasterDevice().map(Device::isEnabled).orElse(false); + } + + public long getNextDeviceId() { + requireNotStale(); + + long candidateId = Device.MASTER_ID + 1; + + while (getDevice(candidateId).isPresent()) { + candidateId++; + } + + return candidateId; + } + + public int getEnabledDeviceCount() { + requireNotStale(); + + int count = 0; + + for (final Device device : devices) { + if (device.isEnabled()) count++; + } + + return count; + } + + public void setIdentityKey(final IdentityKey identityKey) { + requireNotStale(); + + this.identityKey = identityKey; + } + + public IdentityKey getIdentityKey(final IdentityType identityType) { + requireNotStale(); + + return switch (identityType) { + case ACI -> identityKey; + case PNI -> phoneNumberIdentityKey; + }; + } + + public void setPhoneNumberIdentityKey(final IdentityKey phoneNumberIdentityKey) { + this.phoneNumberIdentityKey = phoneNumberIdentityKey; + } + + public long getLastSeen() { + requireNotStale(); + return devices.stream() + .map(Device::getLastSeen) + .max(Long::compare) + .orElse(0L); + } + + public Optional getCurrentProfileVersion() { + requireNotStale(); + + return Optional.ofNullable(currentProfileVersion); + } + + public void setCurrentProfileVersion(final String currentProfileVersion) { + requireNotStale(); + + this.currentProfileVersion = currentProfileVersion; + } + + public List getBadges() { + requireNotStale(); + + return badges; + } + + public void setBadges(final Clock clock, final List badges) { + requireNotStale(); + + this.badges = badges; + + purgeStaleBadges(clock); + } + + public void addBadge(final Clock clock, final AccountBadge badge) { + requireNotStale(); + boolean added = false; + for (int i = 0; i < badges.size(); i++) { + final AccountBadge badgeInList = badges.get(i); + if (Objects.equals(badgeInList.getId(), badge.getId())) { + if (added) { + badges.remove(i); + i--; + } else { + badges.set(i, badgeInList.mergeWith(badge)); + added = true; + } + } + } + + if (!added) { + badges.add(badge); + } + + purgeStaleBadges(clock); + } + + public void makeBadgePrimaryIfExists(final Clock clock, final String badgeId) { + requireNotStale(); + + // early exit if it's already the first item in the list + if (!badges.isEmpty() && Objects.equals(badges.get(0).getId(), badgeId)) { + purgeStaleBadges(clock); + return; + } + + int indexOfBadge = -1; + for (int i = 1; i < badges.size(); i++) { + if (Objects.equals(badgeId, badges.get(i).getId())) { + indexOfBadge = i; + break; + } + } + + if (indexOfBadge != -1) { + badges.add(0, badges.remove(indexOfBadge)); + } + + purgeStaleBadges(clock); + } + + public void removeBadge(final Clock clock, final String id) { + requireNotStale(); + + badges.removeIf(accountBadge -> Objects.equals(accountBadge.getId(), id)); + purgeStaleBadges(clock); + } + + private void purgeStaleBadges(final Clock clock) { + final Instant now = clock.instant(); + badges.removeIf(accountBadge -> now.isAfter(accountBadge.getExpiration())); + } + + public void setRegistrationLockFromAttributes(final AccountAttributes attributes) { + if (!Util.isEmpty(attributes.getRegistrationLock())) { + final SaltedTokenHash credentials = SaltedTokenHash.generateFor(attributes.getRegistrationLock()); + setRegistrationLock(credentials.hash(), credentials.salt()); + } else { + setRegistrationLock(null, null); + } + } + + public void setRegistrationLock(final String registrationLock, final String registrationLockSalt) { + requireNotStale(); + + this.registrationLock = registrationLock; + this.registrationLockSalt = registrationLockSalt; + } + + public StoredRegistrationLock getRegistrationLock() { + requireNotStale(); + + return new StoredRegistrationLock(Optional.ofNullable(registrationLock), Optional.ofNullable(registrationLockSalt), Instant.ofEpochMilli(getLastSeen())); + } + + public Optional getUnidentifiedAccessKey() { + requireNotStale(); + + return Optional.ofNullable(unidentifiedAccessKey); + } + + public void setUnidentifiedAccessKey(final byte[] unidentifiedAccessKey) { + requireNotStale(); + + this.unidentifiedAccessKey = unidentifiedAccessKey; + } + + public boolean isUnrestrictedUnidentifiedAccess() { + requireNotStale(); + + return unrestrictedUnidentifiedAccess; + } + + public void setUnrestrictedUnidentifiedAccess(final boolean unrestrictedUnidentifiedAccess) { + requireNotStale(); + + this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; + } + + public boolean isDiscoverableByPhoneNumber() { + requireNotStale(); + + return this.discoverableByPhoneNumber; + } + + public void setDiscoverableByPhoneNumber(final boolean discoverableByPhoneNumber) { + requireNotStale(); + + this.discoverableByPhoneNumber = discoverableByPhoneNumber; + } + + public boolean shouldBeVisibleInDirectory() { + requireNotStale(); + + return isEnabled() && isDiscoverableByPhoneNumber(); + } + + public int getVersion() { + requireNotStale(); + + return version; + } + + public void setVersion(final int version) { + requireNotStale(); + + this.version = version; + } + + + /** + * Have all this account's devices been manually locked? + * + * @see Device#hasLockedCredentials + * + * @return true if all the account's devices were locked, false otherwise. + */ + public boolean hasLockedCredentials() { + return devices.stream().allMatch(Device::hasLockedCredentials); + } + + /** + * Lock account by invalidating authentication tokens. + * + * We only want to do this in cases where there is a potential conflict between the + * phone number holder and the registration lock holder. In that case, locking the + * account will ensure that either the registration lock holder proves ownership + * of the phone number, or after 7 days the phone number holder can register a new + * account. + */ + public void lockAuthTokenHash() { + devices.forEach(Device::lockAuthTokenHash); + } + + boolean isStale() { + return stale; + } + + public void markStale() { + stale = true; + } + + private void requireNotStale() { + assert !stale; + + //noinspection ConstantConditions + if (stale) { + logger.error("Accessor called on stale account", new RuntimeException()); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountBadge.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountBadge.java new file mode 100644 index 000000000..c058ed742 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountBadge.java @@ -0,0 +1,104 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; +import java.util.Objects; + +public class AccountBadge { + + private final String id; + private final Instant expiration; + private final boolean visible; + + @JsonCreator + public AccountBadge( + @JsonProperty("id") String id, + @JsonProperty("expiration") Instant expiration, + @JsonProperty("visible") boolean visible) { + this.id = id; + this.expiration = expiration; + this.visible = visible; + } + + /** + * Returns a new AccountBadge that is a merging of the two originals. IDs must match for this operation to make sense. + * The expiration will be the later of the two. + * Visibility will be set if either of the passed in objects is visible. + */ + public AccountBadge mergeWith(AccountBadge other) { + if (!Objects.equals(other.id, id)) { + throw new IllegalArgumentException("merging badges should only take place for same id"); + } + + final Instant latestExpiration; + if (expiration == null || other.expiration == null) { + latestExpiration = null; + } else if (expiration.isAfter(other.expiration)) { + latestExpiration = expiration; + } else { + latestExpiration = other.expiration; + } + + return new AccountBadge( + id, + latestExpiration, + visible || other.visible + ); + } + + public AccountBadge withVisibility(boolean visible) { + if (this.visible == visible) { + return this; + } else { + return new AccountBadge( + this.id, + this.expiration, + visible); + } + } + + public String getId() { + return id; + } + + public Instant getExpiration() { + return expiration; + } + + public boolean isVisible() { + return visible; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AccountBadge that = (AccountBadge) o; + return visible == that.visible && Objects.equals(id, that.id) + && Objects.equals(expiration, that.expiration); + } + + @Override + public int hashCode() { + return Objects.hash(id, expiration, visible); + } + + @Override + public String toString() { + return "AccountBadge{" + + "id='" + id + '\'' + + ", expiration=" + expiration + + ", visible=" + visible + + '}'; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountChangeValidator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountChangeValidator.java new file mode 100644 index 000000000..507b6c0a8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountChangeValidator.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.Optionals; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Arrays; + + +class AccountChangeValidator { + + private final boolean allowNumberChange; + private final boolean allowUsernameHashChange; + + static final AccountChangeValidator GENERAL_CHANGE_VALIDATOR = new AccountChangeValidator(false, false); + static final AccountChangeValidator NUMBER_CHANGE_VALIDATOR = new AccountChangeValidator(true, false); + static final AccountChangeValidator USERNAME_CHANGE_VALIDATOR = new AccountChangeValidator(false, true); + + private static final Logger logger = LoggerFactory.getLogger(AccountChangeValidator.class); + + AccountChangeValidator(final boolean allowNumberChange, + final boolean allowUsernameHashChange) { + + this.allowNumberChange = allowNumberChange; + this.allowUsernameHashChange = allowUsernameHashChange; + } + + public void validateChange(final Account originalAccount, final Account updatedAccount) { + if (!allowNumberChange) { + assert updatedAccount.getNumber().equals(originalAccount.getNumber()); + + if (!updatedAccount.getNumber().equals(originalAccount.getNumber())) { + logger.error("Account number changed via \"normal\" update; numbers must be changed via changeNumber method", + new RuntimeException()); + } + + assert updatedAccount.getPhoneNumberIdentifier().equals(originalAccount.getPhoneNumberIdentifier()); + + if (!updatedAccount.getPhoneNumberIdentifier().equals(originalAccount.getPhoneNumberIdentifier())) { + logger.error( + "Phone number identifier changed via \"normal\" update; PNIs must be changed via changeNumber method", + new RuntimeException()); + } + } + + if (!allowUsernameHashChange) { + // We can potentially replace this with the actual hash of some invalid username (e.g. 1nickname.123) + final byte[] dummyHash = new byte[32]; + new SecureRandom().nextBytes(dummyHash); + + final byte[] updatedAccountUsernameHash = updatedAccount.getUsernameHash().orElse(dummyHash); + final byte[] originalAccountUsernameHash = originalAccount.getUsernameHash().orElse(dummyHash); + + boolean usernameUnchanged = MessageDigest.isEqual(updatedAccountUsernameHash, originalAccountUsernameHash); + + if (!usernameUnchanged) { + logger.error("Username hash changed via \"normal\" update; username hashes must be changed via reserveUsernameHash and confirmUsernameHash methods", + new RuntimeException()); + } + assert usernameUnchanged; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountCleaner.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountCleaner.java new file mode 100644 index 000000000..96c3040cf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountCleaner.java @@ -0,0 +1,66 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AccountCleaner extends AccountDatabaseCrawlerListener { + + private static final Logger log = LoggerFactory.getLogger(AccountCleaner.class); + + private static final Counter DELETED_ACCOUNT_COUNTER = Metrics.counter(name(AccountCleaner.class, "deletedAccounts")); + + private final AccountsManager accountsManager; + + public AccountCleaner(final AccountsManager accountsManager) { + this.accountsManager = accountsManager; + } + + @Override + public void onCrawlStart() { + } + + @Override + public void onCrawlEnd() { + } + + @Override + protected void onCrawlChunk(Optional fromUuid, List chunkAccounts) { + final List> deletionFutures = chunkAccounts.stream() + .filter(AccountCleaner::isExpired) + .map(account -> accountsManager.delete(account, AccountsManager.DeletionReason.EXPIRED) + .whenComplete((ignored, throwable) -> { + if (throwable != null) { + log.warn("Failed to delete account {}", account.getUuid(), throwable); + } else { + DELETED_ACCOUNT_COUNTER.increment(); + } + })) + .toList(); + + try { + CompletableFuture.allOf(deletionFutures.toArray(new CompletableFuture[0])) + .orTimeout(10, TimeUnit.MINUTES) + .join(); + } catch (final Exception e) { + log.debug("Failed to delete one or more accounts in chunk", e); + } + } + + private static boolean isExpired(Account account) { + return account.getLastSeen() + TimeUnit.DAYS.toMillis(180) < System.currentTimeMillis(); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountCrawlChunk.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountCrawlChunk.java new file mode 100644 index 000000000..de6bc68c2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountCrawlChunk.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public class AccountCrawlChunk { + + private final List accounts; + @Nullable + private final UUID lastUuid; + + public AccountCrawlChunk(final List accounts, @Nullable final UUID lastUuid) { + this.accounts = accounts; + this.lastUuid = lastUuid; + } + + public List getAccounts() { + return accounts; + } + + public Optional getLastUuid() { + return Optional.ofNullable(lastUuid); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawler.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawler.java new file mode 100644 index 000000000..e934ea07a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawler.java @@ -0,0 +1,111 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.SharedMetricRegistries; +import com.codahale.metrics.Timer; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.Constants; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class AccountDatabaseCrawler { + + private static final Logger logger = LoggerFactory.getLogger(AccountDatabaseCrawler.class); + private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); + private static final Timer readChunkTimer = metricRegistry.timer(name(AccountDatabaseCrawler.class, "readChunk")); + private static final Timer processChunkTimer = metricRegistry.timer( + name(AccountDatabaseCrawler.class, "processChunk")); + + private static final long WORKER_TTL_MS = 120_000L; + + private final String name; + private final AccountsManager accounts; + private final int chunkSize; + private final String workerId; + private final AccountDatabaseCrawlerCache cache; + private final List listeners; + + public AccountDatabaseCrawler(final String name, + AccountsManager accounts, + AccountDatabaseCrawlerCache cache, + List listeners, + int chunkSize) { + this.name = name; + this.accounts = accounts; + this.chunkSize = chunkSize; + this.workerId = UUID.randomUUID().toString(); + this.cache = cache; + this.listeners = listeners; + } + + public void crawlAllAccounts() { + if (!cache.claimActiveWork(workerId, WORKER_TTL_MS)) { + logger.info("Did not claim active work"); + return; + } + try { + Optional fromUuid = getLastUuid(); + + if (fromUuid.isEmpty()) { + logger.info("{}: Started crawl", name); + listeners.forEach(AccountDatabaseCrawlerListener::onCrawlStart); + } else { + logger.info("{}: Resuming crawl", name); + } + + AccountCrawlChunk chunkAccounts; + do { + try (Timer.Context timer = processChunkTimer.time()) { + logger.debug("{}: Processing chunk", name); + chunkAccounts = readChunk(fromUuid, chunkSize); + + for (AccountDatabaseCrawlerListener listener : listeners) { + listener.timeAndProcessCrawlChunk(fromUuid, chunkAccounts.getAccounts()); + } + fromUuid = chunkAccounts.getLastUuid(); + cacheLastUuid(fromUuid); + } + + } while (!chunkAccounts.getAccounts().isEmpty()); + + logger.info("{}: Finished crawl", name); + listeners.forEach(AccountDatabaseCrawlerListener::onCrawlEnd); + + } finally { + cache.releaseActiveWork(workerId); + } + } + + private AccountCrawlChunk readChunk(Optional fromUuid, int chunkSize) { + return readChunk(fromUuid, chunkSize, readChunkTimer); + } + + private AccountCrawlChunk readChunk(Optional fromUuid, int chunkSize, Timer readTimer) { + try (Timer.Context timer = readTimer.time()) { + + if (fromUuid.isPresent()) { + return accounts.getAllFromDynamo(fromUuid.get(), chunkSize); + } + + return accounts.getAllFromDynamo(chunkSize); + } + } + + private Optional getLastUuid() { + return cache.getLastUuid(); + } + + private void cacheLastUuid(final Optional lastUuid) { + cache.setLastUuid(lastUuid); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawlerCache.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawlerCache.java new file mode 100644 index 000000000..fb76a86d1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawlerCache.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + +import io.lettuce.core.ScriptOutputType; +import io.lettuce.core.SetArgs; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.whispersystems.textsecuregcm.redis.ClusterLuaScript; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class AccountDatabaseCrawlerCache { + + public static final String GENERAL_PURPOSE_PREFIX = ""; + public static final String ACCOUNT_CLEANER_PREFIX = "account-cleaner"; + + private static final String ACTIVE_WORKER_KEY = "account_database_crawler_cache_active_worker"; + private static final String LAST_UUID_DYNAMO_KEY = "account_database_crawler_cache_last_uuid_dynamo"; + + private static final long LAST_NUMBER_TTL_MS = 86400_000L; + + private final FaultTolerantRedisCluster cacheCluster; + private final ClusterLuaScript unlockClusterScript; + + private final String prefix; + + public AccountDatabaseCrawlerCache(FaultTolerantRedisCluster cacheCluster, String prefix) throws IOException { + this.cacheCluster = cacheCluster; + this.unlockClusterScript = ClusterLuaScript.fromResource(cacheCluster, "lua/account_database_crawler/unlock.lua", + ScriptOutputType.INTEGER); + + this.prefix = prefix + "::"; + } + + public boolean claimActiveWork(String workerId, long ttlMs) { + return "OK".equals(cacheCluster.withCluster(connection -> connection.sync() + .set(getPrefixedKey(ACTIVE_WORKER_KEY), workerId, SetArgs.Builder.nx().px(ttlMs)))); + } + + public void releaseActiveWork(String workerId) { + unlockClusterScript.execute(List.of(getPrefixedKey(ACTIVE_WORKER_KEY)), List.of(workerId)); + } + + public Optional getLastUuid() { + final String lastUuidString = cacheCluster.withCluster( + connection -> connection.sync().get(getPrefixedKey(LAST_UUID_DYNAMO_KEY))); + + if (lastUuidString == null) { + return Optional.empty(); + } else { + return Optional.of(UUID.fromString(lastUuidString)); + } + } + + public void setLastUuid(Optional lastUuid) { + if (lastUuid.isPresent()) { + cacheCluster.useCluster( + connection -> connection.sync() + .psetex(getPrefixedKey(LAST_UUID_DYNAMO_KEY), LAST_NUMBER_TTL_MS, lastUuid.get().toString())); + } else { + cacheCluster.useCluster(connection -> connection.sync().del(getPrefixedKey(LAST_UUID_DYNAMO_KEY))); + } + } + + private String getPrefixedKey(final String key) { + return prefix + key; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawlerListener.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawlerListener.java new file mode 100644 index 000000000..ff497dae5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawlerListener.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.codahale.metrics.SharedMetricRegistries; +import com.codahale.metrics.Timer; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.whispersystems.textsecuregcm.util.Constants; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public abstract class AccountDatabaseCrawlerListener { + + private final Timer processChunkTimer; + + abstract public void onCrawlStart(); + + abstract public void onCrawlEnd(); + + abstract protected void onCrawlChunk(Optional fromUuid, List chunkAccounts); + + public AccountDatabaseCrawlerListener() { + processChunkTimer = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME).timer(name(AccountDatabaseCrawlerListener.class, "processChunk", getClass().getSimpleName())); + } + + public void timeAndProcessCrawlChunk(Optional fromUuid, List chunkAccounts) { + try (Timer.Context timer = processChunkTimer.time()) { + onCrawlChunk(fromUuid, chunkAccounts); + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountLockManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountLockManager.java new file mode 100644 index 000000000..fd2c6c16f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountLockManager.java @@ -0,0 +1,113 @@ +package org.whispersystems.textsecuregcm.storage; + +import com.amazonaws.services.dynamodbv2.AcquireLockOptions; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClient; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClientOptions; +import com.amazonaws.services.dynamodbv2.LockItem; +import com.amazonaws.services.dynamodbv2.ReleaseLockOptions; +import com.google.common.annotations.VisibleForTesting; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import org.whispersystems.textsecuregcm.util.Util; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +public class AccountLockManager { + + private final AmazonDynamoDBLockClient lockClient; + + static final String KEY_ACCOUNT_E164 = "P"; + + public AccountLockManager(final DynamoDbClient lockDynamoDb, final String lockTableName) { + this(new AmazonDynamoDBLockClient( + AmazonDynamoDBLockClientOptions.builder(lockDynamoDb, lockTableName) + .withPartitionKeyName(KEY_ACCOUNT_E164) + .withLeaseDuration(15L) + .withHeartbeatPeriod(2L) + .withTimeUnit(TimeUnit.SECONDS) + .withCreateHeartbeatBackgroundThread(true) + .build())); + } + + @VisibleForTesting + AccountLockManager(final AmazonDynamoDBLockClient lockClient) { + this.lockClient = lockClient; + } + + /** + * Acquires a distributed, pessimistic lock for the accounts identified by the given phone numbers. By design, the + * accounts need not actually exist in order to acquire a lock; this allows lock acquisition for operations that span + * account lifecycle changes (like deleting an account or changing a phone number). The given task runs once locks for + * all given phone numbers have been acquired, and the locks are released as soon as the task completes by any means. + * + * @param e164s the phone numbers for which to acquire a distributed, pessimistic lock + * @param task the task to execute once locks have been acquired + * + * @throws InterruptedException if interrupted while acquiring a lock + */ + public void withLock(final List e164s, final Runnable task) throws InterruptedException { + if (e164s.isEmpty()) { + throw new IllegalArgumentException("List of e164s to lock must not be empty"); + } + + final List lockItems = new ArrayList<>(e164s.size()); + + try { + for (final String e164 : e164s) { + lockItems.add(lockClient.acquireLock(AcquireLockOptions.builder(e164) + .withAcquireReleasedLocksConsistently(true) + .build())); + } + + task.run(); + } finally { + lockItems.forEach(lockItem -> lockClient.releaseLock(ReleaseLockOptions.builder(lockItem) + .withBestEffort(true) + .build())); + } + } + + /** + * Acquires a distributed, pessimistic lock for the accounts identified by the given phone numbers. By design, the + * accounts need not actually exist in order to acquire a lock; this allows lock acquisition for operations that span + * account lifecycle changes (like deleting an account or changing a phone number). The given task runs once locks for + * all given phone numbers have been acquired, and the locks are released as soon as the task completes by any means. + * + * @param e164s the phone numbers for which to acquire a distributed, pessimistic lock + * @param taskSupplier a supplier for the task to execute once locks have been acquired + * @param executor the executor on which to acquire and release locks + * + * @return a future that completes normally when the given task has executed successfully and all locks have been + * released; the returned future may fail with an {@link InterruptedException} if interrupted while acquiring a lock + */ public CompletableFuture withLockAsync(final List e164s, + final Supplier> taskSupplier, + final Executor executor) { + + if (e164s.isEmpty()) { + throw new IllegalArgumentException("List of e164s to lock must not be empty"); + } + + final List lockItems = new ArrayList<>(e164s.size()); + + return CompletableFuture.runAsync(() -> { + for (final String e164 : e164s) { + try { + lockItems.add(lockClient.acquireLock(AcquireLockOptions.builder(e164) + .withAcquireReleasedLocksConsistently(true) + .build())); + } catch (final InterruptedException e) { + throw new CompletionException(e); + } + } + }, executor) + .thenCompose(ignored -> taskSupplier.get()) + .whenCompleteAsync((ignored, throwable) -> lockItems.forEach(lockItem -> lockClient.releaseLock(ReleaseLockOptions.builder(lockItem) + .withBestEffort(true) + .build())), executor) + .thenRun(Util.NOOP); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java new file mode 100644 index 000000000..9ea0d9e61 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java @@ -0,0 +1,1152 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + +import static com.codahale.metrics.MetricRegistry.name; +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Throwables; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.AsyncTimerUtil; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import org.whispersystems.textsecuregcm.util.ExceptionUtils; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.UUIDUtil; +import org.whispersystems.textsecuregcm.util.Util; +import reactor.core.publisher.Flux; +import reactor.core.publisher.ParallelFlux; +import reactor.core.scheduler.Scheduler; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.CancellationReason; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.Delete; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.Put; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; +import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure; +import software.amazon.awssdk.services.dynamodb.model.ScanRequest; +import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem; +import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; +import software.amazon.awssdk.services.dynamodb.model.TransactionConflictException; +import software.amazon.awssdk.services.dynamodb.model.Update; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; +import software.amazon.awssdk.utils.CompletableFutureUtils; + +/** + * "Accounts" DDB table's structure doesn't match 1:1 the {@link Account} class: most of the class fields are serialized + * and stored in the {@link Accounts#ATTR_ACCOUNT_DATA} attribute, however there are certain fields that are stored only as DDB attributes + * (e.g. if indexing or lookup by field is required), and there are also fields that stored in both places. + * This class contains all the logic that decides whether or not a field of the {@link Account} class should be + * added as an attribute, serialized as a part of {@link Accounts#ATTR_ACCOUNT_DATA}, or both. To skip serialization, + * make sure attribute name is listed in {@link Accounts#ACCOUNT_FIELDS_TO_EXCLUDE_FROM_SERIALIZATION}. If serialization is skipped, + * make sure the field is stored in a DDB attribute and then put back into the account object in {@link Accounts#fromItem(Map)}. + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class Accounts extends AbstractDynamoDbStore { + + private static final Logger log = LoggerFactory.getLogger(Accounts.class); + + static final List ACCOUNT_FIELDS_TO_EXCLUDE_FROM_SERIALIZATION = List.of("uuid", "usernameLinkHandle"); + + private static final ObjectWriter ACCOUNT_DDB_JSON_WRITER = SystemMapper.jsonMapper() + .writer(SystemMapper.excludingField(Account.class, ACCOUNT_FIELDS_TO_EXCLUDE_FROM_SERIALIZATION)); + + private static final Timer CREATE_TIMER = Metrics.timer(name(Accounts.class, "create")); + private static final Timer CHANGE_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "changeNumber")); + private static final Timer SET_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "setUsername")); + private static final Timer RESERVE_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "reserveUsername")); + private static final Timer CLEAR_USERNAME_HASH_TIMER = Metrics.timer(name(Accounts.class, "clearUsernameHash")); + private static final Timer UPDATE_TIMER = Metrics.timer(name(Accounts.class, "update")); + private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "getByNumber")); + private static final Timer GET_BY_USERNAME_HASH_TIMER = Metrics.timer(name(Accounts.class, "getByUsernameHash")); + private static final Timer GET_BY_USERNAME_LINK_HANDLE_TIMER = Metrics.timer(name(Accounts.class, "getByUsernameLinkHandle")); + private static final Timer GET_BY_PNI_TIMER = Metrics.timer(name(Accounts.class, "getByPni")); + private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(Accounts.class, "getByUuid")); + private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(Accounts.class, "getAllFrom")); + private static final Timer GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(Accounts.class, "getAllFromOffset")); + private static final Timer DELETE_TIMER = Metrics.timer(name(Accounts.class, "delete")); + + private static final String CONDITIONAL_CHECK_FAILED = "ConditionalCheckFailed"; + + private static final String TRANSACTION_CONFLICT = "TransactionConflict"; + + // uuid, primary key + static final String KEY_ACCOUNT_UUID = "U"; + // uuid, attribute on account table, primary key for PNI table + static final String ATTR_PNI_UUID = "PNI"; + // uuid of the current username link or null + static final String ATTR_USERNAME_LINK_UUID = "UL"; + // phone number + static final String ATTR_ACCOUNT_E164 = "P"; + // account, serialized to JSON + static final String ATTR_ACCOUNT_DATA = "D"; + // internal version for optimistic locking + static final String ATTR_VERSION = "V"; + // canonically discoverable + static final String ATTR_CANONICALLY_DISCOVERABLE = "C"; + // username hash; byte[] or null + static final String ATTR_USERNAME_HASH = "N"; + // confirmed; bool + static final String ATTR_CONFIRMED = "F"; + // unidentified access key; byte[] or null + static final String ATTR_UAK = "UAK"; + // time to live; number + static final String ATTR_TTL = "TTL"; + + static final String DELETED_ACCOUNTS_KEY_ACCOUNT_E164 = "P"; + static final String DELETED_ACCOUNTS_ATTR_ACCOUNT_UUID = "U"; + static final String DELETED_ACCOUNTS_ATTR_EXPIRES = "E"; + static final String DELETED_ACCOUNTS_UUID_TO_E164_INDEX_NAME = "u_to_p"; + + static final String USERNAME_LINK_TO_UUID_INDEX = "ul_to_u"; + + static final Duration DELETED_ACCOUNTS_TIME_TO_LIVE = Duration.ofDays(30); + + private final Clock clock; + + private final DynamoDbAsyncClient asyncClient; + + private final String phoneNumberConstraintTableName; + private final String phoneNumberIdentifierConstraintTableName; + private final String usernamesConstraintTableName; + private final String deletedAccountsTableName; + private final String accountsTableName; + + private final int scanPageSize; + + + @VisibleForTesting + public Accounts( + final Clock clock, + final DynamoDbClient client, + final DynamoDbAsyncClient asyncClient, + final String accountsTableName, + final String phoneNumberConstraintTableName, + final String phoneNumberIdentifierConstraintTableName, + final String usernamesConstraintTableName, + final String deletedAccountsTableName, + final int scanPageSize) { + super(client); + this.clock = clock; + this.asyncClient = asyncClient; + this.phoneNumberConstraintTableName = phoneNumberConstraintTableName; + this.phoneNumberIdentifierConstraintTableName = phoneNumberIdentifierConstraintTableName; + this.accountsTableName = accountsTableName; + this.usernamesConstraintTableName = usernamesConstraintTableName; + this.deletedAccountsTableName = deletedAccountsTableName; + this.scanPageSize = scanPageSize; + } + + public Accounts( + final DynamoDbClient client, + final DynamoDbAsyncClient asyncClient, + final String accountsTableName, + final String phoneNumberConstraintTableName, + final String phoneNumberIdentifierConstraintTableName, + final String usernamesConstraintTableName, + final String deletedAccountsTableName, + final int scanPageSize) { + this(Clock.systemUTC(), client, asyncClient, accountsTableName, + phoneNumberConstraintTableName, phoneNumberIdentifierConstraintTableName, usernamesConstraintTableName, + deletedAccountsTableName, scanPageSize); + } + + public boolean create(final Account account) { + return CREATE_TIMER.record(() -> { + try { + final AttributeValue uuidAttr = AttributeValues.fromUUID(account.getUuid()); + final AttributeValue numberAttr = AttributeValues.fromString(account.getNumber()); + final AttributeValue pniUuidAttr = AttributeValues.fromUUID(account.getPhoneNumberIdentifier()); + + final TransactWriteItem phoneNumberConstraintPut = buildConstraintTablePutIfAbsent( + phoneNumberConstraintTableName, uuidAttr, ATTR_ACCOUNT_E164, numberAttr); + + final TransactWriteItem phoneNumberIdentifierConstraintPut = buildConstraintTablePutIfAbsent( + phoneNumberIdentifierConstraintTableName, uuidAttr, ATTR_PNI_UUID, pniUuidAttr); + + final TransactWriteItem accountPut = buildAccountPut(account, uuidAttr, numberAttr, pniUuidAttr); + + // Clear any "recently deleted account" record for this number since, if it existed, we've used its old ACI for + // the newly-created account. + final TransactWriteItem deletedAccountDelete = buildRemoveDeletedAccount(account.getNumber()); + + final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder() + .transactItems(phoneNumberConstraintPut, phoneNumberIdentifierConstraintPut, accountPut, deletedAccountDelete) + .build(); + + try { + db().transactWriteItems(request); + } catch (final TransactionCanceledException e) { + + final CancellationReason accountCancellationReason = e.cancellationReasons().get(2); + + if (conditionalCheckFailed(accountCancellationReason)) { + throw new IllegalArgumentException("account identifier present with different phone number"); + } + + final CancellationReason phoneNumberConstraintCancellationReason = e.cancellationReasons().get(0); + final CancellationReason phoneNumberIdentifierConstraintCancellationReason = e.cancellationReasons().get(1); + + if (conditionalCheckFailed(phoneNumberConstraintCancellationReason) + || conditionalCheckFailed(phoneNumberIdentifierConstraintCancellationReason)) { + + // In theory, both reasons should trip in tandem and either should give us the information we need. Even so, + // we'll be cautious here and make sure we're choosing a condition check that really failed. + final CancellationReason reason = conditionalCheckFailed(phoneNumberConstraintCancellationReason) + ? phoneNumberConstraintCancellationReason + : phoneNumberIdentifierConstraintCancellationReason; + + final ByteBuffer actualAccountUuid = reason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer(); + account.setUuid(UUIDUtil.fromByteBuffer(actualAccountUuid)); + + final Account existingAccount = getByAccountIdentifier(account.getUuid()).orElseThrow(); + + // It's up to the client to delete this username hash if they can't retrieve and decrypt the plaintext username from storage service + existingAccount.getUsernameHash().ifPresent(account::setUsernameHash); + account.setNumber(existingAccount.getNumber(), existingAccount.getPhoneNumberIdentifier()); + account.setVersion(existingAccount.getVersion()); + + update(account); + + return false; + } + + if (TRANSACTION_CONFLICT.equals(accountCancellationReason.code())) { + // this should only happen if two clients manage to make concurrent create() calls + throw new ContestedOptimisticLockException(); + } + + // this shouldn't happen + throw new RuntimeException("could not create account: " + extractCancellationReasonCodes(e)); + } + } catch (final JsonProcessingException e) { + throw new IllegalArgumentException(e); + } + + return true; + }); + } + + /** + * Changes the phone number for the given account. The given account's number should be its current, pre-change + * number. If this method succeeds, the account's number will be changed to the new number and its phone number + * identifier will be changed to the given phone number identifier. If the update fails for any reason, the account's + * number and PNI will be unchanged. + *

    + * This method expects that any accounts with conflicting numbers will have been removed by the time this method is + * called. This method may fail with an unspecified {@link RuntimeException} if another account with the same number + * exists in the data store. + * + * @param account the account for which to change the phone number + * @param number the new phone number + */ + public void changeNumber(final Account account, + final String number, + final UUID phoneNumberIdentifier, + final Optional maybeDisplacedAccountIdentifier) { + + CHANGE_NUMBER_TIMER.record(() -> { + final String originalNumber = account.getNumber(); + final UUID originalPni = account.getPhoneNumberIdentifier(); + + boolean succeeded = false; + + account.setNumber(number, phoneNumberIdentifier); + + try { + final List writeItems = new ArrayList<>(); + final AttributeValue uuidAttr = AttributeValues.fromUUID(account.getUuid()); + final AttributeValue numberAttr = AttributeValues.fromString(number); + final AttributeValue pniAttr = AttributeValues.fromUUID(phoneNumberIdentifier); + + writeItems.add(buildDelete(phoneNumberConstraintTableName, ATTR_ACCOUNT_E164, originalNumber)); + writeItems.add(buildConstraintTablePut(phoneNumberConstraintTableName, uuidAttr, ATTR_ACCOUNT_E164, numberAttr)); + writeItems.add(buildDelete(phoneNumberIdentifierConstraintTableName, ATTR_PNI_UUID, originalPni)); + writeItems.add(buildConstraintTablePut(phoneNumberIdentifierConstraintTableName, uuidAttr, ATTR_PNI_UUID, pniAttr)); + writeItems.add(buildRemoveDeletedAccount(number)); + maybeDisplacedAccountIdentifier.ifPresent(displacedAccountIdentifier -> + writeItems.add(buildPutDeletedAccount(displacedAccountIdentifier, originalNumber))); + + writeItems.add( + TransactWriteItem.builder() + .update(Update.builder() + .tableName(accountsTableName) + .key(Map.of(KEY_ACCOUNT_UUID, uuidAttr)) + .updateExpression( + "SET #data = :data, #number = :number, #pni = :pni, #cds = :cds ADD #version :version_increment") + .conditionExpression( + "attribute_exists(#number) AND #version = :version") + .expressionAttributeNames(Map.of( + "#number", ATTR_ACCOUNT_E164, + "#data", ATTR_ACCOUNT_DATA, + "#cds", ATTR_CANONICALLY_DISCOVERABLE, + "#pni", ATTR_PNI_UUID, + "#version", ATTR_VERSION)) + .expressionAttributeValues(Map.of( + ":number", numberAttr, + ":data", accountDataAttributeValue(account), + ":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()), + ":pni", pniAttr, + ":version", AttributeValues.fromInt(account.getVersion()), + ":version_increment", AttributeValues.fromInt(1))) + .build()) + .build()); + + final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder() + .transactItems(writeItems) + .build(); + + db().transactWriteItems(request); + + account.setVersion(account.getVersion() + 1); + succeeded = true; + } catch (final JsonProcessingException e) { + throw new IllegalArgumentException(e); + } finally { + if (!succeeded) { + account.setNumber(originalNumber, originalPni); + } + } + }); + } + + /** + * Reserve a username hash under the account UUID + */ + public CompletableFuture reserveUsernameHash( + final Account account, + final byte[] reservedUsernameHash, + final Duration ttl) { + + final Timer.Sample sample = Timer.start(); + + // if there is an existing old reservation it will be cleaned up via ttl + final Optional maybeOriginalReservation = account.getReservedUsernameHash(); + account.setReservedUsernameHash(reservedUsernameHash); + + final long expirationTime = clock.instant().plus(ttl).getEpochSecond(); + + // Use account UUID as a "reservation token" - by providing this, the client proves ownership of the hash + final UUID uuid = account.getUuid(); + final byte[] accountJsonBytes; + + try { + accountJsonBytes = SystemMapper.jsonMapper().writeValueAsBytes(account); + } catch (final JsonProcessingException e) { + throw new IllegalArgumentException(e); + } + + final List writeItems = new ArrayList<>(); + + writeItems.add(TransactWriteItem.builder() + .put(Put.builder() + .tableName(usernamesConstraintTableName) + .item(Map.of( + KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid), + ATTR_USERNAME_HASH, AttributeValues.fromByteArray(reservedUsernameHash), + ATTR_TTL, AttributeValues.fromLong(expirationTime), + ATTR_CONFIRMED, AttributeValues.fromBool(false))) + .conditionExpression("attribute_not_exists(#username_hash) OR (#ttl < :now)") + .expressionAttributeNames(Map.of("#username_hash", ATTR_USERNAME_HASH, "#ttl", ATTR_TTL)) + .expressionAttributeValues(Map.of(":now", AttributeValues.fromLong(clock.instant().getEpochSecond()))) + .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD) + .build()) + .build()); + + writeItems.add( + TransactWriteItem.builder() + .update(Update.builder() + .tableName(accountsTableName) + .key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid))) + .updateExpression("SET #data = :data ADD #version :version_increment") + .conditionExpression("#version = :version") + .expressionAttributeNames(Map.of("#data", ATTR_ACCOUNT_DATA, "#version", ATTR_VERSION)) + .expressionAttributeValues(Map.of( + ":data", AttributeValues.fromByteArray(accountJsonBytes), + ":version", AttributeValues.fromInt(account.getVersion()), + ":version_increment", AttributeValues.fromInt(1))) + .build()) + .build()); + + return asyncClient.transactWriteItems(TransactWriteItemsRequest.builder() + .transactItems(writeItems) + .build()) + .exceptionally(throwable -> { + if (ExceptionUtils.unwrap(throwable) instanceof TransactionCanceledException e) { + if (e.cancellationReasons().stream().map(CancellationReason::code).anyMatch(CONDITIONAL_CHECK_FAILED::equals)) { + throw new ContestedOptimisticLockException(); + } + } + + throw ExceptionUtils.wrap(throwable); + }) + .whenComplete((response, throwable) -> { + sample.stop(RESERVE_USERNAME_TIMER); + + if (throwable == null) { + account.setVersion(account.getVersion() + 1); + } else { + account.setReservedUsernameHash(maybeOriginalReservation.orElse(null)); + } + }) + .thenRun(() -> {}); + } + + /** + * Confirm (set) a previously reserved username hash + * + * @param account to update + * @param usernameHash believed to be available + * @return a future that completes once the username hash has been confirmed; may fail with an + * {@link ContestedOptimisticLockException} if the account has been updated or the username has taken by someone else + */ + public CompletableFuture confirmUsernameHash(final Account account, final byte[] usernameHash, @Nullable final byte[] encryptedUsername) { + final Timer.Sample sample = Timer.start(); + + final Optional maybeOriginalUsernameHash = account.getUsernameHash(); + final Optional maybeOriginalReservationHash = account.getReservedUsernameHash(); + final Optional maybeOriginalUsernameLinkHandle = Optional.ofNullable(account.getUsernameLinkHandle()); + final Optional maybeOriginalEncryptedUsername = account.getEncryptedUsername(); + + final UUID newLinkHandle = UUID.randomUUID(); + + account.setUsernameHash(usernameHash); + account.setReservedUsernameHash(null); + account.setUsernameLinkDetails(encryptedUsername == null ? null : newLinkHandle, encryptedUsername); + + final TransactWriteItemsRequest request; + + try { + final List writeItems = new ArrayList<>(); + + // add the username hash to the constraint table, wiping out the ttl if we had already reserved the hash + writeItems.add(TransactWriteItem.builder() + .put(Put.builder() + .tableName(usernamesConstraintTableName) + .item(Map.of( + KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()), + ATTR_USERNAME_HASH, AttributeValues.fromByteArray(usernameHash), + ATTR_CONFIRMED, AttributeValues.fromBool(true))) + // it's not in the constraint table OR it's expired OR it was reserved by us + .conditionExpression("attribute_not_exists(#username_hash) OR #ttl < :now OR (#aci = :aci AND #confirmed = :confirmed)") + .expressionAttributeNames(Map.of("#username_hash", ATTR_USERNAME_HASH, "#ttl", ATTR_TTL, "#aci", KEY_ACCOUNT_UUID, "#confirmed", ATTR_CONFIRMED)) + .expressionAttributeValues(Map.of( + ":now", AttributeValues.fromLong(clock.instant().getEpochSecond()), + ":aci", AttributeValues.fromUUID(account.getUuid()), + ":confirmed", AttributeValues.fromBool(false))) + .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD) + .build()) + .build()); + + final StringBuilder updateExpr = new StringBuilder("SET #data = :data, #username_hash = :username_hash"); + final Map expressionAttributeValues = new HashMap<>(Map.of( + ":data", accountDataAttributeValue(account), + ":username_hash", AttributeValues.fromByteArray(usernameHash), + ":version", AttributeValues.fromInt(account.getVersion()), + ":version_increment", AttributeValues.fromInt(1))); + if (account.getUsernameLinkHandle() != null) { + updateExpr.append(", #ul = :ul"); + expressionAttributeValues.put(":ul", AttributeValues.fromUUID(account.getUsernameLinkHandle())); + } else { + updateExpr.append(" REMOVE #ul"); + } + updateExpr.append(" ADD #version :version_increment"); + + writeItems.add( + TransactWriteItem.builder() + .update(Update.builder() + .tableName(accountsTableName) + .key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()))) + .updateExpression(updateExpr.toString()) + .conditionExpression("#version = :version") + .expressionAttributeNames(Map.of("#data", ATTR_ACCOUNT_DATA, + "#username_hash", ATTR_USERNAME_HASH, + "#ul", ATTR_USERNAME_LINK_UUID, + "#version", ATTR_VERSION)) + .expressionAttributeValues(expressionAttributeValues) + .build()) + .build()); + + maybeOriginalUsernameHash.ifPresent(originalUsernameHash -> writeItems.add( + buildDelete(usernamesConstraintTableName, ATTR_USERNAME_HASH, originalUsernameHash))); + + request = TransactWriteItemsRequest.builder() + .transactItems(writeItems) + .build(); + } catch (final JsonProcessingException e) { + throw new IllegalArgumentException(e); + } finally { + account.setUsernameLinkDetails(maybeOriginalUsernameLinkHandle.orElse(null), maybeOriginalEncryptedUsername.orElse(null)); + account.setReservedUsernameHash(maybeOriginalReservationHash.orElse(null)); + account.setUsernameHash(maybeOriginalUsernameHash.orElse(null)); + } + + return asyncClient.transactWriteItems(request) + .thenRun(() -> { + account.setUsernameHash(usernameHash); + account.setReservedUsernameHash(null); + account.setUsernameLinkDetails(encryptedUsername == null ? null : newLinkHandle, encryptedUsername); + + account.setVersion(account.getVersion() + 1); + }) + .exceptionally(throwable -> { + if (ExceptionUtils.unwrap(throwable) instanceof TransactionCanceledException transactionCanceledException) { + if (transactionCanceledException.cancellationReasons().stream().map(CancellationReason::code).anyMatch(CONDITIONAL_CHECK_FAILED::equals)) { + throw new ContestedOptimisticLockException(); + } + } + + throw ExceptionUtils.wrap(throwable); + }) + .whenComplete((ignored, throwable) -> sample.stop(SET_USERNAME_TIMER)); + } + + public CompletableFuture clearUsernameHash(final Account account) { + return account.getUsernameHash().map(usernameHash -> { + final Timer.Sample sample = Timer.start(); + + final TransactWriteItemsRequest request; + + try { + final List writeItems = new ArrayList<>(); + + account.setUsernameHash(null); + + writeItems.add( + TransactWriteItem.builder() + .update(Update.builder() + .tableName(accountsTableName) + .key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()))) + .updateExpression("SET #data = :data REMOVE #username_hash ADD #version :version_increment") + .conditionExpression("#version = :version") + .expressionAttributeNames(Map.of("#data", ATTR_ACCOUNT_DATA, + "#username_hash", ATTR_USERNAME_HASH, + "#version", ATTR_VERSION)) + .expressionAttributeValues(Map.of( + ":data", accountDataAttributeValue(account), + ":version", AttributeValues.fromInt(account.getVersion()), + ":version_increment", AttributeValues.fromInt(1))) + .build()) + .build()); + + writeItems.add(buildDelete(usernamesConstraintTableName, ATTR_USERNAME_HASH, usernameHash)); + + request = TransactWriteItemsRequest.builder() + .transactItems(writeItems) + .build(); + } catch (final JsonProcessingException e) { + throw new IllegalArgumentException(e); + } finally { + account.setUsernameHash(usernameHash); + } + + return asyncClient.transactWriteItems(request) + .thenAccept(ignored -> { + account.setUsernameHash(null); + account.setVersion(account.getVersion() + 1); + }) + .exceptionally(throwable -> { + if (ExceptionUtils.unwrap(throwable) instanceof TransactionCanceledException transactionCanceledException) { + if (conditionalCheckFailed(transactionCanceledException.cancellationReasons().get(0))) { + throw new ContestedOptimisticLockException(); + } + } + + throw ExceptionUtils.wrap(throwable); + }) + .whenComplete((ignored, throwable) -> sample.stop(CLEAR_USERNAME_HASH_TIMER)); + }).orElseGet(() -> CompletableFuture.completedFuture(null)); + } + + @Nonnull + public CompletionStage updateAsync(final Account account) { + return AsyncTimerUtil.record(UPDATE_TIMER, () -> { + final UpdateItemRequest updateItemRequest; + try { + // username, e164, and pni cannot be modified through this method + final Map attrNames = new HashMap<>(Map.of( + "#number", ATTR_ACCOUNT_E164, + "#data", ATTR_ACCOUNT_DATA, + "#cds", ATTR_CANONICALLY_DISCOVERABLE, + "#version", ATTR_VERSION)); + + final Map attrValues = new HashMap<>(Map.of( + ":data", accountDataAttributeValue(account), + ":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()), + ":version", AttributeValues.fromInt(account.getVersion()), + ":version_increment", AttributeValues.fromInt(1))); + + final StringBuilder updateExpressionBuilder = new StringBuilder("SET #data = :data, #cds = :cds"); + if (account.getUnidentifiedAccessKey().isPresent()) { + // if it's present in the account, also set the uak + attrNames.put("#uak", ATTR_UAK); + attrValues.put(":uak", AttributeValues.fromByteArray(account.getUnidentifiedAccessKey().get())); + updateExpressionBuilder.append(", #uak = :uak"); + } + if (account.getEncryptedUsername().isPresent() && account.getUsernameLinkHandle() != null) { + attrNames.put("#ul", ATTR_USERNAME_LINK_UUID); + attrValues.put(":ul", AttributeValues.fromUUID(account.getUsernameLinkHandle())); + updateExpressionBuilder.append(", #ul = :ul"); + } + updateExpressionBuilder.append(" ADD #version :version_increment"); + if (account.getEncryptedUsername().isEmpty() || account.getUsernameLinkHandle() == null) { + attrNames.put("#ul", ATTR_USERNAME_LINK_UUID); + updateExpressionBuilder.append(" REMOVE #ul"); + } + + updateItemRequest = UpdateItemRequest.builder() + .tableName(accountsTableName) + .key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()))) + .updateExpression(updateExpressionBuilder.toString()) + .conditionExpression("attribute_exists(#number) AND #version = :version") + .expressionAttributeNames(attrNames) + .expressionAttributeValues(attrValues) + .build(); + } catch (final JsonProcessingException e) { + throw new IllegalArgumentException(e); + } + + return asyncClient.updateItem(updateItemRequest) + .thenApply(response -> { + account.setVersion(AttributeValues.getInt(response.attributes(), "V", account.getVersion() + 1)); + return (Void) null; + }) + .exceptionally(throwable -> { + final Throwable unwrapped = ExceptionUtils.unwrap(throwable); + if (unwrapped instanceof TransactionConflictException) { + throw new ContestedOptimisticLockException(); + } else if (unwrapped instanceof ConditionalCheckFailedException e) { + // the exception doesn't give details about which condition failed, + // but we can infer it was an optimistic locking failure if the UUID is known + throw getByAccountIdentifier(account.getUuid()).isPresent() ? new ContestedOptimisticLockException() : e; + } else { + // rethrow + throw CompletableFutureUtils.errorAsCompletionException(throwable); + } + }); + }); + } + + public void update(final Account account) throws ContestedOptimisticLockException { + try { + updateAsync(account).toCompletableFuture().join(); + } catch (final CompletionException e) { + // unwrap CompletionExceptions, throw as long is it's unchecked + Throwables.throwIfUnchecked(ExceptionUtils.unwrap(e)); + + // if we otherwise somehow got a wrapped checked exception, + // rethrow the checked exception wrapped by the original CompletionException + log.error("Unexpected checked exception thrown from dynamo update", e); + throw e; + } + } + + public CompletableFuture usernameHashAvailable(final byte[] username) { + return usernameHashAvailable(Optional.empty(), username); + } + + public CompletableFuture usernameHashAvailable(final Optional accountUuid, final byte[] usernameHash) { + return itemByKeyAsync(usernamesConstraintTableName, ATTR_USERNAME_HASH, AttributeValues.fromByteArray(usernameHash)) + .thenApply(maybeUsernameHashItem -> maybeUsernameHashItem + .map(item -> { + if (AttributeValues.getLong(item, ATTR_TTL, Long.MAX_VALUE) < clock.instant().getEpochSecond()) { + // username hash was reserved, but has expired + return true; + } + + // username hash is reserved by us + return !AttributeValues.getBool(item, ATTR_CONFIRMED, true) && accountUuid + .map(AttributeValues.getUUID(item, KEY_ACCOUNT_UUID, new UUID(0, 0))::equals) + .orElse(false); + }) + // If no item was found, then the username hash is free + .orElse(true)); + } + + @Nonnull + public Optional getByE164(final String number) { + return getByIndirectLookup( + GET_BY_NUMBER_TIMER, phoneNumberConstraintTableName, ATTR_ACCOUNT_E164, AttributeValues.fromString(number)); + } + + @Nonnull + public CompletableFuture> getByE164Async(final String number) { + return getByIndirectLookupAsync( + GET_BY_NUMBER_TIMER, phoneNumberConstraintTableName, ATTR_ACCOUNT_E164, AttributeValues.fromString(number)); + } + + @Nonnull + public Optional getByPhoneNumberIdentifier(final UUID phoneNumberIdentifier) { + return getByIndirectLookup( + GET_BY_PNI_TIMER, phoneNumberIdentifierConstraintTableName, ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)); + } + + @Nonnull + public CompletableFuture> getByPhoneNumberIdentifierAsync(final UUID phoneNumberIdentifier) { + return getByIndirectLookupAsync(GET_BY_PNI_TIMER, phoneNumberIdentifierConstraintTableName, ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)); + } + + @Nonnull + public CompletableFuture> getByUsernameHash(final byte[] usernameHash) { + return getByIndirectLookupAsync(GET_BY_USERNAME_HASH_TIMER, + usernamesConstraintTableName, + ATTR_USERNAME_HASH, + AttributeValues.fromByteArray(usernameHash), + item -> AttributeValues.getBool(item, ATTR_CONFIRMED, false) // ignore items that are reservations (not confirmed) + ); + } + + @Nonnull + public CompletableFuture> getByUsernameLinkHandle(final UUID usernameLinkHandle) { + final Timer.Sample sample = Timer.start(); + + return itemByGsiKeyAsync(accountsTableName, USERNAME_LINK_TO_UUID_INDEX, ATTR_USERNAME_LINK_UUID, AttributeValues.fromUUID(usernameLinkHandle)) + .thenApply(maybeItem -> maybeItem.map(Accounts::fromItem)) + .whenComplete((account, throwable) -> sample.stop(GET_BY_USERNAME_LINK_HANDLE_TIMER)); + } + + @Nonnull + public Optional getByAccountIdentifier(final UUID uuid) { + return requireNonNull(GET_BY_UUID_TIMER.record(() -> + itemByKey(accountsTableName, KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)) + .map(Accounts::fromItem))); + } + + private TransactWriteItem buildPutDeletedAccount(final UUID uuid, final String e164) { + return TransactWriteItem.builder() + .put(Put.builder() + .tableName(deletedAccountsTableName) + .item(Map.of( + DELETED_ACCOUNTS_KEY_ACCOUNT_E164, AttributeValues.fromString(e164), + DELETED_ACCOUNTS_ATTR_ACCOUNT_UUID, AttributeValues.fromUUID(uuid), + DELETED_ACCOUNTS_ATTR_EXPIRES, AttributeValues.fromLong(Instant.now().plus(DELETED_ACCOUNTS_TIME_TO_LIVE).getEpochSecond()))) + .build()) + .build(); + } + + private TransactWriteItem buildRemoveDeletedAccount(final String e164) { + return TransactWriteItem.builder() + .delete(Delete.builder() + .tableName(deletedAccountsTableName) + .key(Map.of(DELETED_ACCOUNTS_KEY_ACCOUNT_E164, AttributeValues.fromString(e164))) + .build()) + .build(); + } + + @Nonnull + public CompletableFuture> getByAccountIdentifierAsync(final UUID uuid) { + return AsyncTimerUtil.record(GET_BY_UUID_TIMER, () -> itemByKeyAsync(accountsTableName, KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)) + .thenApply(maybeItem -> maybeItem.map(Accounts::fromItem))) + .toCompletableFuture(); + } + + public Optional findRecentlyDeletedAccountIdentifier(final String e164) { + final GetItemResponse response = db().getItem(GetItemRequest.builder() + .tableName(deletedAccountsTableName) + .consistentRead(true) + .key(Map.of(DELETED_ACCOUNTS_KEY_ACCOUNT_E164, AttributeValues.fromString(e164))) + .build()); + + return Optional.ofNullable(AttributeValues.getUUID(response.item(), DELETED_ACCOUNTS_ATTR_ACCOUNT_UUID, null)); + } + + public Optional findRecentlyDeletedE164(final UUID uuid) { + final QueryResponse response = db().query(QueryRequest.builder() + .tableName(deletedAccountsTableName) + .indexName(DELETED_ACCOUNTS_UUID_TO_E164_INDEX_NAME) + .keyConditionExpression("#uuid = :uuid") + .projectionExpression("#e164") + .expressionAttributeNames(Map.of("#uuid", DELETED_ACCOUNTS_ATTR_ACCOUNT_UUID, + "#e164", DELETED_ACCOUNTS_KEY_ACCOUNT_E164)) + .expressionAttributeValues(Map.of(":uuid", AttributeValues.fromUUID(uuid))).build()); + + if (response.count() == 0) { + return Optional.empty(); + } + + if (response.count() > 1) { + throw new RuntimeException("Impossible result: more than one phone number returned for UUID: " + uuid); + } + + return Optional.ofNullable(response.items().get(0).get(DELETED_ACCOUNTS_KEY_ACCOUNT_E164).s()); + } + + public CompletableFuture delete(final UUID uuid) { + final Timer.Sample sample = Timer.start(); + + return getByAccountIdentifierAsync(uuid) + .thenCompose(maybeAccount -> maybeAccount.map(account -> { + final List transactWriteItems = new ArrayList<>(List.of( + buildDelete(phoneNumberConstraintTableName, ATTR_ACCOUNT_E164, account.getNumber()), + buildDelete(accountsTableName, KEY_ACCOUNT_UUID, uuid), + buildDelete(phoneNumberIdentifierConstraintTableName, ATTR_PNI_UUID, account.getPhoneNumberIdentifier()), + buildPutDeletedAccount(uuid, account.getNumber()) + )); + + account.getUsernameHash().ifPresent(usernameHash -> transactWriteItems.add( + buildDelete(usernamesConstraintTableName, ATTR_USERNAME_HASH, usernameHash))); + + return asyncClient.transactWriteItems(TransactWriteItemsRequest.builder() + .transactItems(transactWriteItems) + .build()) + .thenRun(Util.NOOP); + }) + .orElseGet(() -> CompletableFuture.completedFuture(null))) + .thenRun(() -> sample.stop(DELETE_TIMER)); + } + + ParallelFlux getAll(final int segments, final Scheduler scheduler) { + if (segments < 1) { + throw new IllegalArgumentException("Total number of segments must be positive"); + } + + return Flux.range(0, segments) + .parallel() + .runOn(scheduler) + .flatMap(segment -> asyncClient.scanPaginator(ScanRequest.builder() + .tableName(accountsTableName) + .consistentRead(true) + .segment(segment) + .totalSegments(segments) + .build()) + .items() + .map(Accounts::fromItem)); + } + + @Nonnull + public AccountCrawlChunk getAllFrom(final UUID from, final int maxCount) { + final ScanRequest.Builder scanRequestBuilder = ScanRequest.builder() + .limit(scanPageSize) + .exclusiveStartKey(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(from))); + + return scanForChunk(scanRequestBuilder, maxCount, GET_ALL_FROM_OFFSET_TIMER); + } + + @Nonnull + public AccountCrawlChunk getAllFromStart(final int maxCount) { + final ScanRequest.Builder scanRequestBuilder = ScanRequest.builder() + .limit(scanPageSize); + + return scanForChunk(scanRequestBuilder, maxCount, GET_ALL_FROM_START_TIMER); + } + + @Nonnull + private Optional getByIndirectLookup( + final Timer timer, + final String tableName, + final String keyName, + final AttributeValue keyValue) { + return getByIndirectLookup(timer, tableName, keyName, keyValue, i -> true); + } + + @Nonnull + private CompletableFuture> getByIndirectLookupAsync( + final Timer timer, + final String tableName, + final String keyName, + final AttributeValue keyValue) { + + return getByIndirectLookupAsync(timer, tableName, keyName, keyValue, i -> true); + } + + @Nonnull + private Optional getByIndirectLookup( + final Timer timer, + final String tableName, + final String keyName, + final AttributeValue keyValue, + final Predicate> predicate) { + + return requireNonNull(timer.record(() -> itemByKey(tableName, keyName, keyValue) + .filter(predicate) + .map(item -> item.get(KEY_ACCOUNT_UUID)) + .flatMap(uuid -> itemByKey(accountsTableName, KEY_ACCOUNT_UUID, uuid)) + .map(Accounts::fromItem))); + } + + @Nonnull + private CompletableFuture> getByIndirectLookupAsync( + final Timer timer, + final String tableName, + final String keyName, + final AttributeValue keyValue, + final Predicate> predicate) { + + return AsyncTimerUtil.record(timer, () -> itemByKeyAsync(tableName, keyName, keyValue) + .thenCompose(maybeItem -> maybeItem + .filter(predicate) + .map(item -> item.get(KEY_ACCOUNT_UUID)) + .map(uuid -> itemByKeyAsync(accountsTableName, KEY_ACCOUNT_UUID, uuid) + .thenApply(maybeAccountItem -> maybeAccountItem.map(Accounts::fromItem))) + .orElse(CompletableFuture.completedFuture(Optional.empty())))) + .toCompletableFuture(); + } + + @Nonnull + private Optional> itemByKey(final String table, final String keyName, final AttributeValue keyValue) { + final GetItemResponse response = db().getItem(GetItemRequest.builder() + .tableName(table) + .key(Map.of(keyName, keyValue)) + .consistentRead(true) + .build()); + return Optional.ofNullable(response.item()).filter(m -> !m.isEmpty()); + } + + @Nonnull + private CompletableFuture>> itemByKeyAsync(final String table, final String keyName, final AttributeValue keyValue) { + return asyncClient.getItem(GetItemRequest.builder() + .tableName(table) + .key(Map.of(keyName, keyValue)) + .consistentRead(true) + .build()) + .thenApply(response -> Optional.ofNullable(response.item()).filter(item -> !item.isEmpty())); + } + + @Nonnull + private Optional> itemByGsiKey(final String table, final String indexName, final String keyName, final AttributeValue keyValue) { + final QueryResponse response = db().query(QueryRequest.builder() + .tableName(table) + .indexName(indexName) + .keyConditionExpression("#gsiKey = :gsiValue") + .projectionExpression("#uuid") + .expressionAttributeNames(Map.of( + "#gsiKey", keyName, + "#uuid", KEY_ACCOUNT_UUID)) + .expressionAttributeValues(Map.of( + ":gsiValue", keyValue)) + .build()); + + if (response.count() == 0) { + return Optional.empty(); + } + + if (response.count() > 1) { + throw new IllegalStateException("More than one row located for GSI [%s], key-value pair [%s, %s]" + .formatted(indexName, keyName, keyValue)); + } + + final AttributeValue primaryKeyValue = response.items().get(0).get(KEY_ACCOUNT_UUID); + return itemByKey(table, KEY_ACCOUNT_UUID, primaryKeyValue); + } + + @Nonnull + private CompletableFuture>> itemByGsiKeyAsync(final String table, final String indexName, final String keyName, final AttributeValue keyValue) { + return asyncClient.query(QueryRequest.builder() + .tableName(table) + .indexName(indexName) + .keyConditionExpression("#gsiKey = :gsiValue") + .projectionExpression("#uuid") + .expressionAttributeNames(Map.of( + "#gsiKey", keyName, + "#uuid", KEY_ACCOUNT_UUID)) + .expressionAttributeValues(Map.of( + ":gsiValue", keyValue)) + .build()) + .thenCompose(response -> { + if (response.count() == 0) { + return CompletableFuture.completedFuture(Optional.empty()); + } + + if (response.count() > 1) { + return CompletableFuture.failedFuture(new IllegalStateException( + "More than one row located for GSI [%s], key-value pair [%s, %s]" + .formatted(indexName, keyName, keyValue))); + } + + final AttributeValue primaryKeyValue = response.items().get(0).get(KEY_ACCOUNT_UUID); + return itemByKeyAsync(table, KEY_ACCOUNT_UUID, primaryKeyValue); + }); + } + + @Nonnull + private TransactWriteItem buildAccountPut( + final Account account, + final AttributeValue uuidAttr, + final AttributeValue numberAttr, + final AttributeValue pniUuidAttr) throws JsonProcessingException { + + final Map item = new HashMap<>(Map.of( + KEY_ACCOUNT_UUID, uuidAttr, + ATTR_ACCOUNT_E164, numberAttr, + ATTR_PNI_UUID, pniUuidAttr, + ATTR_ACCOUNT_DATA, accountDataAttributeValue(account), + ATTR_VERSION, AttributeValues.fromInt(account.getVersion()), + ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory()))); + + // Add the UAK if it's in the account + account.getUnidentifiedAccessKey() + .map(AttributeValues::fromByteArray) + .ifPresent(uak -> item.put(ATTR_UAK, uak)); + + return TransactWriteItem.builder() + .put(Put.builder() + .conditionExpression("attribute_not_exists(#number) OR #number = :number") + .expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164)) + .expressionAttributeValues(Map.of(":number", numberAttr)) + .tableName(accountsTableName) + .item(item) + .build()) + .build(); + } + + @Nonnull + private static TransactWriteItem buildConstraintTablePutIfAbsent( + final String tableName, + final AttributeValue uuidAttr, + final String keyName, + final AttributeValue keyValue + ) { + return TransactWriteItem.builder() + .put(Put.builder() + .tableName(tableName) + .item(Map.of( + keyName, keyValue, + KEY_ACCOUNT_UUID, uuidAttr)) + .conditionExpression( + "attribute_not_exists(#key) OR #uuid = :uuid") + .expressionAttributeNames(Map.of( + "#key", keyName, + "#uuid", KEY_ACCOUNT_UUID)) + .expressionAttributeValues(Map.of( + ":uuid", uuidAttr)) + .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD) + .build()) + .build(); + } + + @Nonnull + private static TransactWriteItem buildConstraintTablePut( + final String tableName, + final AttributeValue uuidAttr, + final String keyName, + final AttributeValue keyValue) { + return TransactWriteItem.builder() + .put(Put.builder() + .tableName(tableName) + .item(Map.of( + keyName, keyValue, + KEY_ACCOUNT_UUID, uuidAttr)) + .conditionExpression( + "attribute_not_exists(#key)") + .expressionAttributeNames(Map.of( + "#key", keyName)) + .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD) + .build()) + .build(); + } + + @Nonnull + private static TransactWriteItem buildDelete(final String tableName, final String keyName, final String keyValue) { + return buildDelete(tableName, keyName, AttributeValues.fromString(keyValue)); + } + + @Nonnull + private static TransactWriteItem buildDelete(final String tableName, final String keyName, final byte[] keyValue) { + return buildDelete(tableName, keyName, AttributeValues.fromByteArray(keyValue)); + } + + @Nonnull + private static TransactWriteItem buildDelete(final String tableName, final String keyName, final UUID keyValue) { + return buildDelete(tableName, keyName, AttributeValues.fromUUID(keyValue)); + } + + @Nonnull + private static TransactWriteItem buildDelete(final String tableName, final String keyName, final AttributeValue keyValue) { + return TransactWriteItem.builder() + .delete(Delete.builder() + .tableName(tableName) + .key(Map.of(keyName, keyValue)) + .build()) + .build(); + } + + @Nonnull + private AccountCrawlChunk scanForChunk(final ScanRequest.Builder scanRequestBuilder, final int maxCount, final Timer timer) { + scanRequestBuilder.tableName(accountsTableName); + final List> items = requireNonNull(timer.record(() -> scan(scanRequestBuilder.build(), maxCount))); + final List accounts = items.stream().map(Accounts::fromItem).toList(); + return new AccountCrawlChunk(accounts, accounts.size() > 0 ? accounts.get(accounts.size() - 1).getUuid() : null); + } + + @Nonnull + private static String extractCancellationReasonCodes(final TransactionCanceledException exception) { + return exception.cancellationReasons().stream() + .map(CancellationReason::code) + .collect(Collectors.joining(", ")); + } + + @VisibleForTesting + @Nonnull + static Account fromItem(final Map item) { + if (!item.containsKey(ATTR_ACCOUNT_DATA) + || !item.containsKey(ATTR_ACCOUNT_E164) + || !item.containsKey(KEY_ACCOUNT_UUID) + || !item.containsKey(ATTR_CANONICALLY_DISCOVERABLE)) { + throw new RuntimeException("item missing values"); + } + try { + final Account account = SystemMapper.jsonMapper().readValue(item.get(ATTR_ACCOUNT_DATA).b().asByteArray(), Account.class); + + final UUID accountIdentifier = UUIDUtil.fromByteBuffer(item.get(KEY_ACCOUNT_UUID).b().asByteBuffer()); + final UUID phoneNumberIdentifierFromAttribute = AttributeValues.getUUID(item, ATTR_PNI_UUID, null); + + if (account.getPhoneNumberIdentifier() == null || phoneNumberIdentifierFromAttribute == null || + !Objects.equals(account.getPhoneNumberIdentifier(), phoneNumberIdentifierFromAttribute)) { + + log.warn("Missing or mismatched PNIs for account {}. From JSON: {}; from attribute: {}", + accountIdentifier, account.getPhoneNumberIdentifier(), phoneNumberIdentifierFromAttribute); + } + + account.setNumber(item.get(ATTR_ACCOUNT_E164).s(), phoneNumberIdentifierFromAttribute); + account.setUuid(accountIdentifier); + account.setUsernameHash(AttributeValues.getByteArray(item, ATTR_USERNAME_HASH, null)); + account.setUsernameLinkHandle(AttributeValues.getUUID(item, ATTR_USERNAME_LINK_UUID, null)); + account.setVersion(Integer.parseInt(item.get(ATTR_VERSION).n())); + + return account; + + } catch (final IOException e) { + throw new RuntimeException("Could not read stored account data", e); + } + } + + private static AttributeValue accountDataAttributeValue(final Account account) throws JsonProcessingException { + return AttributeValues.fromByteArray(ACCOUNT_DDB_JSON_WRITER.writeValueAsBytes(account)); + } + + private static boolean conditionalCheckFailed(final CancellationReason reason) { + return CONDITIONAL_CHECK_FAILED.equals(reason.code()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java new file mode 100644 index 000000000..1bdb183a2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java @@ -0,0 +1,1097 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + + +import static com.codahale.metrics.MetricRegistry.name; +import static java.util.Objects.requireNonNull; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.SharedMetricRegistries; +import com.codahale.metrics.Timer; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import io.lettuce.core.RedisException; +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.time.Clock; +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Queue; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.signal.libsignal.protocol.IdentityKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; +import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.redis.RedisOperation; +import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; +import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client; +import org.whispersystems.textsecuregcm.util.Constants; +import org.whispersystems.textsecuregcm.util.DestinationDeviceValidator; +import org.whispersystems.textsecuregcm.util.ExceptionUtils; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.Util; +import reactor.core.publisher.ParallelFlux; +import reactor.core.scheduler.Scheduler; + +public class AccountsManager { + + private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); + private static final Timer createTimer = metricRegistry.timer(name(AccountsManager.class, "create")); + private static final Timer updateTimer = metricRegistry.timer(name(AccountsManager.class, "update")); + private static final Timer getByNumberTimer = metricRegistry.timer(name(AccountsManager.class, "getByNumber")); + private static final Timer getByUsernameHashTimer = metricRegistry.timer(name(AccountsManager.class, "getByUsernameHash")); + private static final Timer getByUsernameLinkHandleTimer = metricRegistry.timer(name(AccountsManager.class, "getByUsernameLinkHandle")); + private static final Timer getByUuidTimer = metricRegistry.timer(name(AccountsManager.class, "getByUuid")); + private static final Timer deleteTimer = metricRegistry.timer(name(AccountsManager.class, "delete")); + + private static final Timer redisSetTimer = metricRegistry.timer(name(AccountsManager.class, "redisSet")); + private static final Timer redisNumberGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisNumberGet")); + private static final Timer redisUsernameHashGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUsernameHashGet")); + private static final Timer redisUsernameLinkHandleGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUsernameLinkHandleGet")); + private static final Timer redisPniGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisPniGet")); + private static final Timer redisUuidGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUuidGet")); + private static final Timer redisDeleteTimer = metricRegistry.timer(name(AccountsManager.class, "redisDelete")); + + private static final String CREATE_COUNTER_NAME = name(AccountsManager.class, "createCounter"); + private static final String DELETE_COUNTER_NAME = name(AccountsManager.class, "deleteCounter"); + private static final String COUNTRY_CODE_TAG_NAME = "country"; + private static final String DELETION_REASON_TAG_NAME = "reason"; + + @VisibleForTesting + public static final String USERNAME_EXPERIMENT_NAME = "usernames"; + + private static final Logger logger = LoggerFactory.getLogger(AccountsManager.class); + + private final Accounts accounts; + private final PhoneNumberIdentifiers phoneNumberIdentifiers; + private final FaultTolerantRedisCluster cacheCluster; + private final AccountLockManager accountLockManager; + private final KeysManager keysManager; + private final MessagesManager messagesManager; + private final ProfilesManager profilesManager; + private final SecureStorageClient secureStorageClient; + private final SecureValueRecovery2Client secureValueRecovery2Client; + private final ClientPresenceManager clientPresenceManager; + private final ExperimentEnrollmentManager experimentEnrollmentManager; + private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager; + private final Executor accountLockExecutor; + private final Clock clock; + + private static final ObjectWriter ACCOUNT_REDIS_JSON_WRITER = SystemMapper.jsonMapper() + .writer(SystemMapper.excludingField(Account.class, List.of("uuid"))); + + // An account that's used at least daily will get reset in the cache at least once per day when its "last seen" + // timestamp updates; expiring entries after two days will help clear out "zombie" cache entries that are read + // frequently (e.g. the account is in an active group and receives messages frequently), but aren't actively used by + // the owner. + private static final long CACHE_TTL_SECONDS = Duration.ofDays(2).toSeconds(); + + private static final Duration USERNAME_HASH_RESERVATION_TTL_MINUTES = Duration.ofMinutes(5); + + private static final int MAX_UPDATE_ATTEMPTS = 10; + + @FunctionalInterface + private interface AccountPersister { + void persistAccount(Account account) throws UsernameHashNotAvailableException; + } + + public enum DeletionReason { + ADMIN_DELETED("admin"), + EXPIRED ("expired"), + USER_REQUEST ("userRequest"); + + private final String tagValue; + + DeletionReason(final String tagValue) { + this.tagValue = tagValue; + } + } + + public AccountsManager(final Accounts accounts, + final PhoneNumberIdentifiers phoneNumberIdentifiers, + final FaultTolerantRedisCluster cacheCluster, + final AccountLockManager accountLockManager, + final KeysManager keysManager, + final MessagesManager messagesManager, + final ProfilesManager profilesManager, + final SecureStorageClient secureStorageClient, + final SecureValueRecovery2Client secureValueRecovery2Client, + final ClientPresenceManager clientPresenceManager, + final ExperimentEnrollmentManager experimentEnrollmentManager, + final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager, + final Executor accountLockExecutor, + final Clock clock) { + this.accounts = accounts; + this.phoneNumberIdentifiers = phoneNumberIdentifiers; + this.cacheCluster = cacheCluster; + this.accountLockManager = accountLockManager; + this.keysManager = keysManager; + this.messagesManager = messagesManager; + this.profilesManager = profilesManager; + this.secureStorageClient = secureStorageClient; + this.secureValueRecovery2Client = secureValueRecovery2Client; + this.clientPresenceManager = clientPresenceManager; + this.experimentEnrollmentManager = experimentEnrollmentManager; + this.registrationRecoveryPasswordsManager = requireNonNull(registrationRecoveryPasswordsManager); + this.accountLockExecutor = accountLockExecutor; + this.clock = requireNonNull(clock); + } + + public Account create(final String number, + final String password, + final String signalAgent, + final AccountAttributes accountAttributes, + final List accountBadges) throws InterruptedException { + + try (Timer.Context ignored = createTimer.time()) { + final Account account = new Account(); + + accountLockManager.withLock(List.of(number), () -> { + Device device = new Device(); + device.setId(Device.MASTER_ID); + device.setAuthTokenHash(SaltedTokenHash.generateFor(password)); + device.setFetchesMessages(accountAttributes.getFetchesMessages()); + device.setRegistrationId(accountAttributes.getRegistrationId()); + accountAttributes.getPhoneNumberIdentityRegistrationId().ifPresent(device::setPhoneNumberIdentityRegistrationId); + device.setName(accountAttributes.getName()); + device.setCapabilities(accountAttributes.getCapabilities()); + device.setCreated(System.currentTimeMillis()); + device.setLastSeen(Util.todayInMillis()); + device.setUserAgent(signalAgent); + + account.setNumber(number, phoneNumberIdentifiers.getPhoneNumberIdentifier(number)); + + final Optional maybeRecentlyDeletedAccountIdentifier = + accounts.findRecentlyDeletedAccountIdentifier(number); + + // Reuse the ACI from any recently-deleted account with this number to cover cases where somebody is + // re-registering. + account.setUuid(maybeRecentlyDeletedAccountIdentifier.orElseGet(UUID::randomUUID)); + account.addDevice(device); + account.setRegistrationLockFromAttributes(accountAttributes); + account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey()); + account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess()); + account.setDiscoverableByPhoneNumber(accountAttributes.isDiscoverableByPhoneNumber()); + account.setBadges(clock, accountBadges); + + final UUID originalUuid = account.getUuid(); + + boolean freshUser = accounts.create(account); + + // create() sometimes updates the UUID, if there was a number conflict. + // for metrics, we want secondary to run with the same original UUID + final UUID actualUuid = account.getUuid(); + + redisSet(account); + + // In terms of previously-existing accounts, there are three possible cases: + // + // 1. This is a completely new account; there was no pre-existing account and no recently-deleted account + // 2. This is a re-registration of an existing account. The storage layer will update the existing account in + // place to match the account record created above, and will update the UUID of the newly-created account + // instance to match the stored account record (i.e. originalUuid != actualUuid). + // 3. This is a re-registration of a recently-deleted account, in which case maybeRecentlyDeletedUuid is + // present. + // + // All cases are mutually-exclusive. In the first case, we don't need to do anything. In the third, we can be + // confident that everything has already been deleted. In the second case, though, we're taking over an existing + // account and need to clear out messages and keys that may have been stored for the old account. + if (!originalUuid.equals(actualUuid)) { + final CompletableFuture deleteKeysFuture = CompletableFuture.allOf( + keysManager.delete(actualUuid), + keysManager.delete(account.getPhoneNumberIdentifier())); + + messagesManager.clear(actualUuid).join(); + profilesManager.deleteAll(actualUuid).join(); + + deleteKeysFuture.join(); + + clientPresenceManager.disconnectAllPresencesForUuid(actualUuid); + } + + final Tags tags; + + if (freshUser) { + tags = Tags.of("type", "new"); + } else if (maybeRecentlyDeletedAccountIdentifier.isPresent()) { + tags = Tags.of("type", "recently-deleted"); + } else { + tags = Tags.of("type", "re-registration"); + } + + Metrics.counter(CREATE_COUNTER_NAME, tags).increment(); + + accountAttributes.recoveryPassword().ifPresent(registrationRecoveryPassword -> + registrationRecoveryPasswordsManager.storeForCurrentNumber(account.getNumber(), registrationRecoveryPassword)); + }); + + return account; + } + } + + public Account changeNumber(final Account account, + final String targetNumber, + @Nullable final IdentityKey pniIdentityKey, + @Nullable final Map pniSignedPreKeys, + @Nullable final Map pniPqLastResortPreKeys, + @Nullable final Map pniRegistrationIds) throws InterruptedException, MismatchedDevicesException { + + final String originalNumber = account.getNumber(); + final UUID originalPhoneNumberIdentifier = account.getPhoneNumberIdentifier(); + + if (originalNumber.equals(targetNumber)) { + if (pniIdentityKey != null) { + throw new IllegalArgumentException("change number must supply a changed phone number; otherwise use updatePniKeys"); + } + return account; + } + + validateDevices(account, pniSignedPreKeys, pniPqLastResortPreKeys, pniRegistrationIds); + + final AtomicReference updatedAccount = new AtomicReference<>(); + + accountLockManager.withLock(List.of(account.getNumber(), targetNumber), () -> { + redisDelete(account); + + // There are three possible states for accounts associated with the target phone number: + // + // 1. An account exists with the target number; the caller has proved ownership of the number, so delete the + // account with the target number. This will leave a "deleted account" record for the deleted account mapping + // the UUID of the deleted account to the target phone number. We'll then overwrite that so it points to the + // original number to facilitate switching back and forth between numbers. + // 2. No account with the target number exists, but one has recently been deleted. In that case, add a "deleted + // account" record that maps the ACI of the recently-deleted account to the now-abandoned original phone number + // of the account changing its number (which facilitates ACI consistency in cases that a party is switching + // back and forth between numbers). + // 3. No account with the target number exists at all, in which case no additional action is needed. + final Optional recentlyDeletedAci = accounts.findRecentlyDeletedAccountIdentifier(targetNumber); + final Optional maybeExistingAccount = getByE164(targetNumber); + final Optional maybeDisplacedUuid; + + if (maybeExistingAccount.isPresent()) { + delete(maybeExistingAccount.get()).join(); + maybeDisplacedUuid = maybeExistingAccount.map(Account::getUuid); + } else { + maybeDisplacedUuid = recentlyDeletedAci; + } + + final UUID uuid = account.getUuid(); + final UUID phoneNumberIdentifier = phoneNumberIdentifiers.getPhoneNumberIdentifier(targetNumber); + + final Account numberChangedAccount; + + numberChangedAccount = updateWithRetries( + account, + a -> { + setPniKeys(account, pniIdentityKey, pniSignedPreKeys, pniRegistrationIds); + return true; + }, + a -> accounts.changeNumber(a, targetNumber, phoneNumberIdentifier, maybeDisplacedUuid), + () -> accounts.getByAccountIdentifier(uuid).orElseThrow(), + AccountChangeValidator.NUMBER_CHANGE_VALIDATOR); + + updatedAccount.set(numberChangedAccount); + + CompletableFuture.allOf( + keysManager.delete(phoneNumberIdentifier), + keysManager.delete(originalPhoneNumberIdentifier)) + .join(); + + keysManager.storeEcSignedPreKeys(phoneNumberIdentifier, pniSignedPreKeys); + + if (pniPqLastResortPreKeys != null) { + keysManager.storePqLastResort( + phoneNumberIdentifier, + keysManager.getPqEnabledDevices(uuid).join().stream().collect( + Collectors.toMap( + Function.identity(), + pniPqLastResortPreKeys::get))); + } + }); + + return updatedAccount.get(); + } + + public static boolean validNewAccountAttributes(final AccountAttributes accountAttributes) { + if (!validRegistrationId(accountAttributes.getRegistrationId())) { + return false; + } + final OptionalInt pniRegistrationId = accountAttributes.getPhoneNumberIdentityRegistrationId(); + if (pniRegistrationId.isPresent() && !validRegistrationId(pniRegistrationId.getAsInt())) { + return false; + } + return true; + } + + private static boolean validRegistrationId(int registrationId) { + return registrationId > 0 && registrationId <= Device.MAX_REGISTRATION_ID; + } + + public Account updatePniKeys(final Account account, + final IdentityKey pniIdentityKey, + final Map pniSignedPreKeys, + @Nullable final Map pniPqLastResortPreKeys, + final Map pniRegistrationIds) throws MismatchedDevicesException { + validateDevices(account, pniSignedPreKeys, pniPqLastResortPreKeys, pniRegistrationIds); + + final UUID pni = account.getPhoneNumberIdentifier(); + final Account updatedAccount = update(account, a -> { return setPniKeys(a, pniIdentityKey, pniSignedPreKeys, pniRegistrationIds); }); + + final List pqEnabledDeviceIDs = keysManager.getPqEnabledDevices(pni).join(); + keysManager.delete(pni); + keysManager.storeEcSignedPreKeys(pni, pniSignedPreKeys).join(); + if (pniPqLastResortPreKeys != null && !pqEnabledDeviceIDs.isEmpty()) { + keysManager.storePqLastResort(pni, pqEnabledDeviceIDs.stream().collect(Collectors.toMap(Function.identity(), pniPqLastResortPreKeys::get))).join(); + } + + return updatedAccount; + } + + private boolean setPniKeys(final Account account, + @Nullable final IdentityKey pniIdentityKey, + @Nullable final Map pniSignedPreKeys, + @Nullable final Map pniRegistrationIds) { + if (ObjectUtils.allNull(pniIdentityKey, pniSignedPreKeys, pniRegistrationIds)) { + return false; + } else if (!ObjectUtils.allNotNull(pniIdentityKey, pniSignedPreKeys, pniRegistrationIds)) { + throw new IllegalArgumentException("PNI identity key, signed pre-keys, and registration IDs must be all null or all non-null"); + } + + boolean changed = !Objects.equals(pniIdentityKey, account.getIdentityKey(IdentityType.PNI)); + + for (Device device : account.getDevices()) { + if (!device.isEnabled()) { + continue; + } + ECSignedPreKey signedPreKey = pniSignedPreKeys.get(device.getId()); + int registrationId = pniRegistrationIds.get(device.getId()); + changed = changed || + !signedPreKey.equals(device.getSignedPreKey(IdentityType.PNI)) || + device.getRegistrationId() != registrationId; + device.setPhoneNumberIdentitySignedPreKey(signedPreKey); + device.setPhoneNumberIdentityRegistrationId(registrationId); + } + + account.setPhoneNumberIdentityKey(pniIdentityKey); + + return changed; + } + + private void validateDevices(final Account account, + @Nullable final Map pniSignedPreKeys, + @Nullable final Map pniPqLastResortPreKeys, + @Nullable final Map pniRegistrationIds) throws MismatchedDevicesException { + if (pniSignedPreKeys == null && pniRegistrationIds == null) { + return; + } else if (pniSignedPreKeys == null || pniRegistrationIds == null) { + throw new IllegalArgumentException("Signed pre-keys and registration IDs must both be null or both be non-null"); + } + + // Check that all including master ID are in signed pre-keys + DestinationDeviceValidator.validateCompleteDeviceList( + account, + pniSignedPreKeys.keySet(), + Collections.emptySet()); + + // Check that all including master ID are in Pq pre-keys + if (pniPqLastResortPreKeys != null) { + DestinationDeviceValidator.validateCompleteDeviceList( + account, + pniPqLastResortPreKeys.keySet(), + Collections.emptySet()); + } + + // Check that all devices are accounted for in the map of new PNI registration IDs + DestinationDeviceValidator.validateCompleteDeviceList( + account, + pniRegistrationIds.keySet(), + Collections.emptySet()); + } + + public record UsernameReservation(Account account, byte[] reservedUsernameHash){} + + /** + * Reserve a username hash so that no other accounts may take it. + *

    + * The reserved hash can later be set with {@link #confirmReservedUsernameHash(Account, byte[], byte[])}. The reservation + * will eventually expire, after which point confirmReservedUsernameHash may fail if another account has taken the + * username hash. + * + * @param account the account to update + * @param requestedUsernameHashes the list of username hashes to attempt to reserve + * @return a future that yields the reserved username hash and an updated Account object on success; may fail with a + * {@link UsernameHashNotAvailableException} if none of the given username hashes are available + */ + public CompletableFuture reserveUsernameHash(final Account account, final List requestedUsernameHashes) { + if (!experimentEnrollmentManager.isEnrolled(account.getUuid(), USERNAME_EXPERIMENT_NAME)) { + return CompletableFuture.failedFuture(new UsernameHashNotAvailableException()); + } + + final AtomicReference reservedUsernameHash = new AtomicReference<>(); + + return redisDeleteAsync(account) + .thenCompose(ignored -> updateWithRetriesAsync( + account, + a -> true, + a -> checkAndReserveNextUsernameHash(a, new ArrayDeque<>(requestedUsernameHashes)) + .thenAccept(reservedUsernameHash::set), + () -> accounts.getByAccountIdentifierAsync(account.getUuid()).thenApply(Optional::orElseThrow), + AccountChangeValidator.USERNAME_CHANGE_VALIDATOR, + MAX_UPDATE_ATTEMPTS)) + .thenApply(updatedAccount -> new UsernameReservation(updatedAccount, reservedUsernameHash.get())); + } + + private CompletableFuture checkAndReserveNextUsernameHash(final Account account, final Queue requestedUsernameHashes) { + final byte[] usernameHash; + + try { + usernameHash = requestedUsernameHashes.remove(); + } catch (final NoSuchElementException e) { + return CompletableFuture.failedFuture(new UsernameHashNotAvailableException()); + } + + return accounts.usernameHashAvailable(usernameHash) + .thenCompose(usernameHashAvailable -> { + if (usernameHashAvailable) { + return accounts.reserveUsernameHash(account, usernameHash, USERNAME_HASH_RESERVATION_TTL_MINUTES) + .thenApply(ignored -> usernameHash); + } else { + return checkAndReserveNextUsernameHash(account, requestedUsernameHashes); + } + }); + } + + /** + * Set a username hash previously reserved with {@link #reserveUsernameHash(Account, List)} + * + * @param account the account to update + * @param reservedUsernameHash the previously reserved username hash + * @param encryptedUsername the encrypted form of the previously reserved username for the username link + * @return a future that yields the updated account with the username hash field set; may fail with a + * {@link UsernameHashNotAvailableException} if the reserved username hash has been taken (because the reservation + * expired) or a {@link UsernameReservationNotFoundException} if {@code reservedUsernameHash} was not reserved in the + * account + */ + public CompletableFuture confirmReservedUsernameHash(final Account account, final byte[] reservedUsernameHash, @Nullable final byte[] encryptedUsername) { + if (!experimentEnrollmentManager.isEnrolled(account.getUuid(), USERNAME_EXPERIMENT_NAME)) { + return CompletableFuture.failedFuture(new UsernameHashNotAvailableException()); + } + + if (account.getUsernameHash().map(currentUsernameHash -> Arrays.equals(currentUsernameHash, reservedUsernameHash)).orElse(false)) { + // the client likely already succeeded and is retrying + return CompletableFuture.completedFuture(account); + } + + if (!account.getReservedUsernameHash().map(oldHash -> Arrays.equals(oldHash, reservedUsernameHash)).orElse(false)) { + // no such reservation existed, either there was no previous call to reserveUsername + // or the reservation changed + return CompletableFuture.failedFuture(new UsernameReservationNotFoundException()); + } + + return redisDeleteAsync(account) + .thenCompose(ignored -> updateWithRetriesAsync( + account, + a -> true, + a -> accounts.usernameHashAvailable(Optional.of(account.getUuid()), reservedUsernameHash) + .thenCompose(usernameHashAvailable -> { + if (!usernameHashAvailable) { + return CompletableFuture.failedFuture(new UsernameHashNotAvailableException()); + } + + return accounts.confirmUsernameHash(a, reservedUsernameHash, encryptedUsername); + }), + () -> accounts.getByAccountIdentifierAsync(account.getUuid()).thenApply(Optional::orElseThrow), + AccountChangeValidator.USERNAME_CHANGE_VALIDATOR, + MAX_UPDATE_ATTEMPTS + )); + } + + public CompletableFuture clearUsernameHash(final Account account) { + return redisDeleteAsync(account) + .thenCompose(ignored -> updateWithRetriesAsync( + account, + a -> true, + accounts::clearUsernameHash, + () -> accounts.getByAccountIdentifierAsync(account.getUuid()).thenApply(Optional::orElseThrow), + AccountChangeValidator.USERNAME_CHANGE_VALIDATOR, + MAX_UPDATE_ATTEMPTS)); + } + + public Account update(Account account, Consumer updater) { + return update(account, a -> { + updater.accept(a); + // assume that all updaters passed to the public method actually modify the account + return true; + }); + } + + public CompletableFuture updateAsync(Account account, Consumer updater) { + return updateAsync(account, a -> { + updater.accept(a); + // assume that all updaters passed to the public method actually modify the account + return true; + }); + } + + /** + * Specialized version of {@link #updateDevice(Account, long, Consumer)} that minimizes potentially contentious and + * redundant updates of {@code device.lastSeen} + */ + public Account updateDeviceLastSeen(Account account, Device device, final long lastSeen) { + return update(account, a -> { + + final Optional maybeDevice = a.getDevice(device.getId()); + + return maybeDevice.map(d -> { + if (d.getLastSeen() >= lastSeen) { + return false; + } + + d.setLastSeen(lastSeen); + + return true; + + }).orElse(false); + }); + } + + public Account updateDeviceAuthentication(final Account account, final Device device, final SaltedTokenHash credentials) { + Preconditions.checkArgument(credentials.getVersion() == SaltedTokenHash.CURRENT_VERSION); + return updateDevice(account, device.getId(), device1 -> device1.setAuthTokenHash(credentials)); + } + + /** + * @param account account to update + * @param updater must return {@code true} if the account was actually updated + */ + private Account update(Account account, Function updater) { + + final Account updatedAccount; + + try (Timer.Context ignored = updateTimer.time()) { + + redisDelete(account); + + final UUID uuid = account.getUuid(); + + updatedAccount = updateWithRetries(account, + updater, + accounts::update, + () -> accounts.getByAccountIdentifier(uuid).orElseThrow(), + AccountChangeValidator.GENERAL_CHANGE_VALIDATOR); + + redisSet(updatedAccount); + } + + return updatedAccount; + } + + private CompletableFuture updateAsync(final Account account, final Function updater) { + + final Timer.Context timerContext = updateTimer.time(); + + return redisDeleteAsync(account) + .thenCompose(ignored -> { + final UUID uuid = account.getUuid(); + + return updateWithRetriesAsync(account, + updater, + a -> accounts.updateAsync(a).toCompletableFuture(), + () -> accounts.getByAccountIdentifierAsync(uuid).thenApply(Optional::orElseThrow), + AccountChangeValidator.GENERAL_CHANGE_VALIDATOR, + MAX_UPDATE_ATTEMPTS); + }) + .thenCompose(updatedAccount -> redisSetAsync(updatedAccount).thenApply(ignored -> updatedAccount)) + .whenComplete((ignored, throwable) -> timerContext.close()); + } + + private Account updateWithRetries(Account account, + final Function updater, + final Consumer persister, + final Supplier retriever, + final AccountChangeValidator changeValidator) { + try { + return failableUpdateWithRetries(account, updater, persister::accept, retriever, changeValidator); + } catch (UsernameHashNotAvailableException e) { + // not possible + throw new IllegalStateException(e); + } + } + + private Account failableUpdateWithRetries(Account account, + final Function updater, + final AccountPersister persister, + final Supplier retriever, + final AccountChangeValidator changeValidator) throws UsernameHashNotAvailableException { + + Account originalAccount = cloneAccountAsNotStale(account); + + if (!updater.apply(account)) { + return account; + } + + final int maxTries = 10; + int tries = 0; + + while (tries < maxTries) { + + try { + persister.persistAccount(account); + + final Account updatedAccount = cloneAccountAsNotStale(account); + account.markStale(); + + changeValidator.validateChange(originalAccount, updatedAccount); + + return updatedAccount; + } catch (final ContestedOptimisticLockException e) { + tries++; + + account = retriever.get(); + originalAccount = cloneAccountAsNotStale(account); + + if (!updater.apply(account)) { + return account; + } + } + } + + throw new OptimisticLockRetryLimitExceededException(); + } + + private CompletionStage updateWithRetriesAsync(Account account, + final Function updater, + final Function> persister, + final Supplier> retriever, + final AccountChangeValidator changeValidator, + final int remainingTries) { + + final Account originalAccount = cloneAccountAsNotStale(account); + + if (!updater.apply(account)) { + return CompletableFuture.completedFuture(account); + } + + if (remainingTries > 0) { + return persister.apply(account) + .thenApply(ignored -> { + final Account updatedAccount = cloneAccountAsNotStale(account); + account.markStale(); + + changeValidator.validateChange(originalAccount, updatedAccount); + + return updatedAccount; + }) + .exceptionallyCompose(throwable -> { + if (ExceptionUtils.unwrap(throwable) instanceof ContestedOptimisticLockException) { + return retriever.get().thenCompose(refreshedAccount -> + updateWithRetriesAsync(refreshedAccount, updater, persister, retriever, changeValidator, remainingTries - 1)); + } else { + throw ExceptionUtils.wrap(throwable); + } + }); + } + + return CompletableFuture.failedFuture(new OptimisticLockRetryLimitExceededException()); + } + + private static Account cloneAccountAsNotStale(final Account account) { + try { + return SystemMapper.jsonMapper().readValue( + SystemMapper.jsonMapper().writeValueAsBytes(account), Account.class); + } catch (final IOException e) { + // this should really, truly, never happen + throw new IllegalArgumentException(e); + } + } + + public Account updateDevice(Account account, long deviceId, Consumer deviceUpdater) { + return update(account, a -> { + a.getDevice(deviceId).ifPresent(deviceUpdater); + // assume that all updaters passed to the public method actually modify the device + return true; + }); + } + + public CompletableFuture updateDeviceAsync(final Account account, final long deviceId, final Consumer deviceUpdater) { + return updateAsync(account, a -> { + a.getDevice(deviceId).ifPresent(deviceUpdater); + // assume that all updaters passed to the public method actually modify the device + return true; + }); + } + + public Optional getByE164(final String number) { + return checkRedisThenAccounts( + getByNumberTimer, + () -> redisGetBySecondaryKey(getAccountMapKey(number), redisNumberGetTimer), + () -> accounts.getByE164(number) + ); + } + + public CompletableFuture> getByE164Async(final String number) { + return checkRedisThenAccountsAsync( + getByNumberTimer, + () -> redisGetBySecondaryKeyAsync(getAccountMapKey(number), redisNumberGetTimer), + () -> accounts.getByE164Async(number) + ); + } + + public Optional getByPhoneNumberIdentifier(final UUID pni) { + return checkRedisThenAccounts( + getByNumberTimer, + () -> redisGetBySecondaryKey(getAccountMapKey(pni.toString()), redisPniGetTimer), + () -> accounts.getByPhoneNumberIdentifier(pni) + ); + } + + public CompletableFuture> getByPhoneNumberIdentifierAsync(final UUID pni) { + return checkRedisThenAccountsAsync( + getByNumberTimer, + () -> redisGetBySecondaryKeyAsync(getAccountMapKey(pni.toString()), redisPniGetTimer), + () -> accounts.getByPhoneNumberIdentifierAsync(pni) + ); + } + + public CompletableFuture> getByUsernameLinkHandle(final UUID usernameLinkHandle) { + return checkRedisThenAccountsAsync( + getByUsernameLinkHandleTimer, + () -> redisGetBySecondaryKeyAsync(getAccountMapKey(usernameLinkHandle.toString()), redisUsernameLinkHandleGetTimer), + () -> accounts.getByUsernameLinkHandle(usernameLinkHandle) + ); + } + + public CompletableFuture> getByUsernameHash(final byte[] usernameHash) { + return checkRedisThenAccountsAsync( + getByUsernameHashTimer, + () -> redisGetBySecondaryKeyAsync(getUsernameHashAccountMapKey(usernameHash), redisUsernameHashGetTimer), + () -> accounts.getByUsernameHash(usernameHash) + ); + } + + public Optional getByServiceIdentifier(final ServiceIdentifier serviceIdentifier) { + return switch (serviceIdentifier.identityType()) { + case ACI -> getByAccountIdentifier(serviceIdentifier.uuid()); + case PNI -> getByPhoneNumberIdentifier(serviceIdentifier.uuid()); + }; + } + + public CompletableFuture> getByServiceIdentifierAsync(final ServiceIdentifier serviceIdentifier) { + return switch (serviceIdentifier.identityType()) { + case ACI -> getByAccountIdentifierAsync(serviceIdentifier.uuid()); + case PNI -> getByPhoneNumberIdentifierAsync(serviceIdentifier.uuid()); + }; + } + + public Optional getByAccountIdentifier(final UUID uuid) { + return checkRedisThenAccounts( + getByUuidTimer, + () -> redisGetByAccountIdentifier(uuid), + () -> accounts.getByAccountIdentifier(uuid) + ); + } + + public CompletableFuture> getByAccountIdentifierAsync(final UUID uuid) { + return checkRedisThenAccountsAsync( + getByUuidTimer, + () -> redisGetByAccountIdentifierAsync(uuid), + () -> accounts.getByAccountIdentifierAsync(uuid) + ); + } + + public UUID getPhoneNumberIdentifier(String e164) { + return phoneNumberIdentifiers.getPhoneNumberIdentifier(e164); + } + + public Optional findRecentlyDeletedAccountIdentifier(final String e164) { + return accounts.findRecentlyDeletedAccountIdentifier(e164); + } + + public Optional findRecentlyDeletedE164(final UUID uuid) { + return accounts.findRecentlyDeletedE164(uuid); + } + + public AccountCrawlChunk getAllFromDynamo(int length) { + return accounts.getAllFromStart(length); + } + + public AccountCrawlChunk getAllFromDynamo(UUID uuid, int length) { + return accounts.getAllFrom(uuid, length); + } + + public ParallelFlux streamAllFromDynamo(final int segments, final Scheduler scheduler) { + return accounts.getAll(segments, scheduler); + } + + public CompletableFuture delete(final Account account, final DeletionReason deletionReason) { + @SuppressWarnings("resource") final Timer.Context timerContext = deleteTimer.time(); + + return accountLockManager.withLockAsync(List.of(account.getNumber()), () -> delete(account), accountLockExecutor) + .whenComplete((ignored, throwable) -> { + timerContext.close(); + + if (throwable == null) { + Metrics.counter(DELETE_COUNTER_NAME, + COUNTRY_CODE_TAG_NAME, Util.getCountryCode(account.getNumber()), + DELETION_REASON_TAG_NAME, deletionReason.tagValue) + .increment(); + } else { + logger.warn("Failed to delete account", throwable); + } + }); + } + + private CompletableFuture delete(final Account account) { + return CompletableFuture.allOf( + secureStorageClient.deleteStoredData(account.getUuid()), + secureValueRecovery2Client.deleteBackups(account.getUuid()), + keysManager.delete(account.getUuid()), + keysManager.delete(account.getPhoneNumberIdentifier()), + messagesManager.clear(account.getUuid()), + messagesManager.clear(account.getPhoneNumberIdentifier()), + profilesManager.deleteAll(account.getUuid()), + registrationRecoveryPasswordsManager.removeForNumber(account.getNumber())) + .thenCompose(ignored -> CompletableFuture.allOf(accounts.delete(account.getUuid()), redisDeleteAsync(account))) + .thenRun(() -> RedisOperation.unchecked(() -> + account.getDevices().forEach(device -> + clientPresenceManager.disconnectPresence(account.getUuid(), device.getId())))); + } + + private String getUsernameHashAccountMapKey(byte[] usernameHash) { + return "UAccountMap::" + Base64.getUrlEncoder().withoutPadding().encodeToString(usernameHash); + } + + private String getAccountMapKey(String key) { + return "AccountMap::" + key; + } + + private String getAccountEntityKey(UUID uuid) { + return "Account3::" + uuid.toString(); + } + + private void redisSet(Account account) { + try (Timer.Context ignored = redisSetTimer.time()) { + final String accountJson = writeRedisAccountJson(account); + + cacheCluster.useCluster(connection -> { + final RedisAdvancedClusterCommands commands = connection.sync(); + + commands.setex(getAccountMapKey(account.getPhoneNumberIdentifier().toString()), CACHE_TTL_SECONDS, account.getUuid().toString()); + commands.setex(getAccountMapKey(account.getNumber()), CACHE_TTL_SECONDS, account.getUuid().toString()); + commands.setex(getAccountEntityKey(account.getUuid()), CACHE_TTL_SECONDS, accountJson); + + account.getUsernameHash().ifPresent(usernameHash -> + commands.setex(getUsernameHashAccountMapKey(usernameHash), CACHE_TTL_SECONDS, account.getUuid().toString())); + }); + } catch (JsonProcessingException e) { + throw new IllegalStateException(e); + } + } + + private CompletableFuture redisSetAsync(final Account account) { + final String accountJson; + + try { + accountJson = writeRedisAccountJson(account); + } catch (final JsonProcessingException e) { + throw new UncheckedIOException(e); + } + + return cacheCluster.withCluster(connection -> CompletableFuture.allOf( + connection.async().setex( + getAccountMapKey(account.getPhoneNumberIdentifier().toString()), CACHE_TTL_SECONDS, + account.getUuid().toString()) + .toCompletableFuture(), + + connection.async() + .setex(getAccountMapKey(account.getNumber()), CACHE_TTL_SECONDS, account.getUuid().toString()) + .toCompletableFuture(), + + connection.async().setex(getAccountEntityKey(account.getUuid()), CACHE_TTL_SECONDS, accountJson) + .toCompletableFuture(), + + account.getUsernameHash() + .map(usernameHash -> connection.async() + .setex(getUsernameHashAccountMapKey(usernameHash), CACHE_TTL_SECONDS, account.getUuid().toString()) + .toCompletableFuture()) + .orElseGet(() -> CompletableFuture.completedFuture(null)) + )); + } + + private Optional checkRedisThenAccounts( + final Timer overallTimer, + final Supplier> resolveFromRedis, + final Supplier> resolveFromAccounts) { + try (final Timer.Context ignored = overallTimer.time()) { + Optional account = resolveFromRedis.get(); + if (account.isEmpty()) { + account = resolveFromAccounts.get(); + account.ifPresent(this::redisSet); + } + return account; + } + } + + private CompletableFuture> checkRedisThenAccountsAsync( + final Timer overallTimer, + final Supplier>> resolveFromRedis, + final Supplier>> resolveFromAccounts) { + + @SuppressWarnings("resource") final Timer.Context timerContext = overallTimer.time(); + + return resolveFromRedis.get() + .thenCompose(maybeAccountFromRedis -> maybeAccountFromRedis + .map(accountFromRedis -> CompletableFuture.completedFuture(maybeAccountFromRedis)) + .orElseGet(() -> resolveFromAccounts.get() + .thenCompose(maybeAccountFromAccounts -> maybeAccountFromAccounts + .map(account -> redisSetAsync(account).thenApply(ignored -> maybeAccountFromAccounts)) + .orElseGet(() -> CompletableFuture.completedFuture(maybeAccountFromAccounts))))) + .whenComplete((ignored, throwable) -> timerContext.close()); + } + + private Optional redisGetBySecondaryKey(final String secondaryKey, final Timer timer) { + try (final Timer.Context ignored = timer.time()) { + return Optional.ofNullable(cacheCluster.withCluster(connection -> connection.sync().get(secondaryKey))) + .map(UUID::fromString) + .flatMap(this::getByAccountIdentifier); + } catch (IllegalArgumentException e) { + logger.warn("Deserialization error", e); + return Optional.empty(); + } catch (RedisException e) { + logger.warn("Redis failure", e); + return Optional.empty(); + } + } + + private CompletableFuture> redisGetBySecondaryKeyAsync(final String secondaryKey, final Timer timer) { + @SuppressWarnings("resource") final Timer.Context timerContext = timer.time(); + + return cacheCluster.withCluster(connection -> connection.async().get(secondaryKey)) + .thenCompose(nullableUuid -> { + if (nullableUuid != null) { + return getByAccountIdentifierAsync(UUID.fromString(nullableUuid)); + } else { + return CompletableFuture.completedFuture(Optional.empty()); + } + }) + .exceptionally(throwable -> { + logger.warn("Failed to retrieve account from Redis", throwable); + return Optional.empty(); + }) + .whenComplete((ignored, throwable) -> timerContext.close()) + .toCompletableFuture(); + } + + private Optional redisGetByAccountIdentifier(UUID uuid) { + try (Timer.Context ignored = redisUuidGetTimer.time()) { + final String json = cacheCluster.withCluster(connection -> connection.sync().get(getAccountEntityKey(uuid))); + + return parseAccountJson(json, uuid); + } catch (final RedisException e) { + logger.warn("Redis failure", e); + return Optional.empty(); + } + } + + private CompletableFuture> redisGetByAccountIdentifierAsync(final UUID uuid) { + return cacheCluster.withCluster(connection -> connection.async().get(getAccountEntityKey(uuid))) + .thenApply(accountJson -> parseAccountJson(accountJson, uuid)) + .exceptionally(throwable -> { + logger.warn("Failed to retrieve account from Redis", throwable); + return Optional.empty(); + }) + .toCompletableFuture(); + } + + @VisibleForTesting + static Optional parseAccountJson(@Nullable final String accountJson, final UUID uuid) { + try { + if (StringUtils.isNotBlank(accountJson)) { + Account account = SystemMapper.jsonMapper().readValue(accountJson, Account.class); + account.setUuid(uuid); + + if (account.getPhoneNumberIdentifier() == null) { + logger.warn("Account {} loaded from Redis is missing a PNI", uuid); + } + + return Optional.of(account); + } + + return Optional.empty(); + } catch (final IOException e) { + logger.warn("Deserialization error", e); + return Optional.empty(); + } + } + + @VisibleForTesting + static String writeRedisAccountJson(final Account account) throws JsonProcessingException { + return ACCOUNT_REDIS_JSON_WRITER.writeValueAsString(account); + } + + private void redisDelete(final Account account) { + try (final Timer.Context ignored = redisDeleteTimer.time()) { + cacheCluster.useCluster(connection -> { + connection.sync().del( + getAccountMapKey(account.getNumber()), + getAccountMapKey(account.getPhoneNumberIdentifier().toString()), + getAccountEntityKey(account.getUuid())); + + account.getUsernameHash().ifPresent(usernameHash -> connection.sync().del(getUsernameHashAccountMapKey(usernameHash))); + }); + } + } + + private CompletableFuture redisDeleteAsync(final Account account) { + @SuppressWarnings("resource") final Timer.Context timerContext = redisDeleteTimer.time(); + + final List keysToDelete = new ArrayList<>(4); + keysToDelete.add(getAccountMapKey(account.getNumber())); + keysToDelete.add(getAccountMapKey(account.getPhoneNumberIdentifier().toString())); + keysToDelete.add(getAccountEntityKey(account.getUuid())); + + account.getUsernameHash().ifPresent(usernameHash -> keysToDelete.add(getUsernameHashAccountMapKey(usernameHash))); + + return cacheCluster.withCluster(connection -> connection.async().del(keysToDelete.toArray(new String[0]))) + .toCompletableFuture() + .thenRun(timerContext::close); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManager.java new file mode 100644 index 000000000..d16ab858a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManager.java @@ -0,0 +1,153 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + +import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.ByteString; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.apache.commons.lang3.ObjectUtils; +import org.signal.libsignal.protocol.IdentityKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.controllers.AccountController; +import org.whispersystems.textsecuregcm.controllers.MessageController; +import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; +import org.whispersystems.textsecuregcm.controllers.StaleDevicesException; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.IncomingMessage; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.push.MessageSender; +import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; +import org.whispersystems.textsecuregcm.util.DestinationDeviceValidator; + +public class ChangeNumberManager { + private static final Logger logger = LoggerFactory.getLogger(AccountController.class); + private final MessageSender messageSender; + private final AccountsManager accountsManager; + + public ChangeNumberManager( + final MessageSender messageSender, + final AccountsManager accountsManager) { + this.messageSender = messageSender; + this.accountsManager = accountsManager; + } + + public Account changeNumber(final Account account, final String number, + @Nullable final IdentityKey pniIdentityKey, + @Nullable final Map deviceSignedPreKeys, + @Nullable final Map devicePqLastResortPreKeys, + @Nullable final List deviceMessages, + @Nullable final Map pniRegistrationIds) + throws InterruptedException, MismatchedDevicesException, StaleDevicesException { + + if (ObjectUtils.allNotNull(pniIdentityKey, deviceSignedPreKeys, deviceMessages, pniRegistrationIds)) { + // AccountsManager validates the device set on deviceSignedPreKeys and pniRegistrationIds + validateDeviceMessages(account, deviceMessages); + } else if (!ObjectUtils.allNull(pniIdentityKey, deviceSignedPreKeys, deviceMessages, pniRegistrationIds)) { + throw new IllegalArgumentException("PNI identity key, signed pre-keys, device messages, and registration IDs must be all null or all non-null"); + } + + + if (number.equals(account.getNumber())) { + // The client has gotten confused/desynchronized with us about their own phone number, most likely due to losing + // our OK response to an immediately preceding change-number request, and are sending a change they don't realize + // is a no-op change. + // + // We don't need to actually do a number-change operation in our DB, but we *do* need to accept their new key + // material and distribute the sync messages, to be sure all clients agree with us and each other about what their + // keys are. Pretend this change-number request was actually a PNI key distribution request. + if (pniIdentityKey == null) { + return account; + } + return updatePniKeys(account, pniIdentityKey, deviceSignedPreKeys, devicePqLastResortPreKeys, deviceMessages, pniRegistrationIds); + } + + final Account updatedAccount = accountsManager.changeNumber( + account, number, pniIdentityKey, deviceSignedPreKeys, devicePqLastResortPreKeys, pniRegistrationIds); + + if (deviceMessages != null) { + sendDeviceMessages(updatedAccount, deviceMessages); + } + + return updatedAccount; + } + + public Account updatePniKeys(final Account account, + final IdentityKey pniIdentityKey, + final Map deviceSignedPreKeys, + @Nullable final Map devicePqLastResortPreKeys, + final List deviceMessages, + final Map pniRegistrationIds) throws MismatchedDevicesException, StaleDevicesException { + validateDeviceMessages(account, deviceMessages); + + // Don't try to be smart about ignoring unnecessary retries. If we make literally no change we will skip the ddb + // write anyway. Linked devices can handle some wasted extra key rotations. + final Account updatedAccount = accountsManager.updatePniKeys( + account, pniIdentityKey, deviceSignedPreKeys, devicePqLastResortPreKeys, pniRegistrationIds); + + sendDeviceMessages(updatedAccount, deviceMessages); + return updatedAccount; + } + + private void validateDeviceMessages(final Account account, + final List deviceMessages) throws MismatchedDevicesException, StaleDevicesException { + // Check that all except master ID are in device messages + DestinationDeviceValidator.validateCompleteDeviceList( + account, + deviceMessages.stream().map(IncomingMessage::destinationDeviceId).collect(Collectors.toSet()), + Set.of(Device.MASTER_ID)); + + // check that all sync messages are to the current registration ID for the matching device + DestinationDeviceValidator.validateRegistrationIds( + account, + deviceMessages, + IncomingMessage::destinationDeviceId, + IncomingMessage::destinationRegistrationId, + false); + } + + private void sendDeviceMessages(final Account account, final List deviceMessages) { + deviceMessages.forEach(message -> + sendMessageToSelf(account, account.getDevice(message.destinationDeviceId()), message)); + } + + @VisibleForTesting + void sendMessageToSelf( + Account sourceAndDestinationAccount, Optional destinationDevice, IncomingMessage message) { + Optional contents = MessageController.getMessageContent(message); + if (contents.isEmpty()) { + logger.debug("empty message contents sending to self, ignoring"); + return; + } else if (destinationDevice.isEmpty()) { + logger.debug("destination device not present"); + return; + } + try { + long serverTimestamp = System.currentTimeMillis(); + Envelope envelope = Envelope.newBuilder() + .setType(Envelope.Type.forNumber(message.type())) + .setTimestamp(serverTimestamp) + .setServerTimestamp(serverTimestamp) + .setDestinationUuid(new AciServiceIdentifier(sourceAndDestinationAccount.getUuid()).toServiceIdentifierString()) + .setContent(ByteString.copyFrom(contents.get())) + .setSourceUuid(new AciServiceIdentifier(sourceAndDestinationAccount.getUuid()).toServiceIdentifierString()) + .setSourceDevice((int) Device.MASTER_ID) + .setUpdatedPni(sourceAndDestinationAccount.getPhoneNumberIdentifier().toString()) + .setUrgent(true) + .build(); + + messageSender.sendMessage(sourceAndDestinationAccount, destinationDevice.get(), envelope, false); + } catch (NotPushRegisteredException e) { + logger.debug("Not registered", e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChunkProcessingFailedException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChunkProcessingFailedException.java new file mode 100644 index 000000000..759a963e1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChunkProcessingFailedException.java @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + +public class ChunkProcessingFailedException extends Exception { + + public ChunkProcessingFailedException(String message) { + super(message); + } + + public ChunkProcessingFailedException(Exception cause) { + super(cause); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ClientRelease.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ClientRelease.java new file mode 100644 index 000000000..7d90958d5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ClientRelease.java @@ -0,0 +1,13 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.vdurmont.semver4j.Semver; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; +import java.time.Instant; + +public record ClientRelease(ClientPlatform platform, Semver version, Instant release, Instant expiration) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManager.java new file mode 100644 index 000000000..0774a8425 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManager.java @@ -0,0 +1,85 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.vdurmont.semver4j.Semver; +import java.time.Clock; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import io.dropwizard.lifecycle.Managed; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; +import javax.annotation.Nullable; + +public class ClientReleaseManager implements Managed { + + private final ClientReleases clientReleases; + private final ScheduledExecutorService scheduledExecutorService; + private final Duration refreshInterval; + private final Clock clock; + + @Nullable + private ScheduledFuture refreshClientReleasesFuture; + + private volatile Map> clientReleasesByPlatform = Collections.emptyMap(); + + private static final Logger logger = LoggerFactory.getLogger(ClientReleaseManager.class); + + public ClientReleaseManager(final ClientReleases clientReleases, + final ScheduledExecutorService scheduledExecutorService, + final Duration refreshInterval, + final Clock clock) { + + this.clientReleases = clientReleases; + this.scheduledExecutorService = scheduledExecutorService; + this.refreshInterval = refreshInterval; + this.clock = clock; + } + + public boolean isVersionActive(final ClientPlatform platform, final Semver version) { + final Map releasesByVersion = clientReleasesByPlatform.get(platform); + + return releasesByVersion != null && + releasesByVersion.containsKey(version) && + releasesByVersion.get(version).expiration().isAfter(clock.instant()); + } + + @Override + public void start() throws Exception { + refreshClientVersions(); + + refreshClientReleasesFuture = + scheduledExecutorService.scheduleWithFixedDelay(this::refreshClientVersions, + refreshInterval.toMillis(), + refreshInterval.toMillis(), + TimeUnit.MILLISECONDS); + } + + @Override + public void stop() throws Exception { + if (refreshClientReleasesFuture != null) { + refreshClientReleasesFuture.cancel(true); + } + } + + void refreshClientVersions() { + try { + clientReleasesByPlatform = clientReleases.getClientReleases(); + + logger.debug("Loaded client releases; android: {}, desktop: {}, ios: {}", + clientReleasesByPlatform.getOrDefault(ClientPlatform.ANDROID, Collections.emptyMap()).size(), + clientReleasesByPlatform.getOrDefault(ClientPlatform.DESKTOP, Collections.emptyMap()).size(), + clientReleasesByPlatform.getOrDefault(ClientPlatform.IOS, Collections.emptyMap()).size()); + } catch (final Exception e) { + logger.warn("Failed to refresh client releases", e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ClientReleases.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ClientReleases.java new file mode 100644 index 000000000..b1844af51 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ClientReleases.java @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.vdurmont.semver4j.Semver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; +import reactor.core.publisher.Flux; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ScanRequest; +import javax.annotation.Nullable; +import java.time.Instant; +import java.util.Collections; +import java.util.Map; + +public class ClientReleases { + + private final DynamoDbAsyncClient dynamoDbAsyncClient; + private final String tableName; + + public static final String ATTR_PLATFORM = "P"; + public static final String ATTR_VERSION = "V"; + public static final String ATTR_RELEASE_TIMESTAMP = "T"; + public static final String ATTR_EXPIRATION = "E"; + + private static final Logger logger = LoggerFactory.getLogger(ClientReleases.class); + + public ClientReleases(final DynamoDbAsyncClient dynamoDbAsyncClient, final String tableName) { + this.dynamoDbAsyncClient = dynamoDbAsyncClient; + this.tableName = tableName; + } + + public Map> getClientReleases() { + return Collections.unmodifiableMap( + Flux.from(dynamoDbAsyncClient.scanPaginator(ScanRequest.builder() + .tableName(tableName) + .build()) + .items()) + .mapNotNull(ClientReleases::releaseFromItem) + .groupBy(ClientRelease::platform) + .flatMap(groupedFlux -> groupedFlux.collectMap(ClientRelease::version) + .map(releasesByVersion -> Tuples.of(groupedFlux.key(), releasesByVersion))) + .collectMap(Tuple2::getT1, Tuple2::getT2) + .blockOptional() + .orElseGet(Collections::emptyMap)); + } + + @Nullable + static ClientRelease releaseFromItem(final Map item) { + try { + final ClientPlatform platform = ClientPlatform.valueOf(item.get(ATTR_PLATFORM).s()); + final Semver version = new Semver(item.get(ATTR_VERSION).s()); + final Instant release = Instant.ofEpochSecond(Long.parseLong(item.get(ATTR_RELEASE_TIMESTAMP).n())); + final Instant expiration = Instant.ofEpochSecond(Long.parseLong(item.get(ATTR_EXPIRATION).n())); + + return new ClientRelease(platform, version, release, expiration); + } catch (final Exception e) { + logger.warn("Failed to parse client release item", e); + return null; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ContestedOptimisticLockException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ContestedOptimisticLockException.java new file mode 100644 index 000000000..b68d24f41 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ContestedOptimisticLockException.java @@ -0,0 +1,11 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import org.whispersystems.textsecuregcm.util.NoStackTraceRuntimeException; + +public class ContestedOptimisticLockException extends NoStackTraceRuntimeException { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java new file mode 100644 index 000000000..dbad027c9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java @@ -0,0 +1,263 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.OptionalInt; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.LongStream; +import javax.annotation.Nullable; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.util.Util; + +public class Device { + + public static final long MASTER_ID = 1; + public static final int MAXIMUM_DEVICE_ID = 256; + public static final int MAX_REGISTRATION_ID = 0x3FFF; + public static final List ALL_POSSIBLE_DEVICE_IDS = LongStream.range(1, MAXIMUM_DEVICE_ID).boxed().collect(Collectors.toList()); + + @JsonProperty + private long id; + + @JsonProperty + private String name; + + @JsonProperty + private String authToken; + + @JsonProperty + private String salt; + + @JsonProperty + private String gcmId; + + @JsonProperty + private String apnId; + + @JsonProperty + private String voipApnId; + + @JsonProperty + private long pushTimestamp; + + @JsonProperty + private long uninstalledFeedback; + + @JsonProperty + private boolean fetchesMessages; + + @JsonProperty + private int registrationId; + + @Nullable + @JsonProperty("pniRegistrationId") + private Integer phoneNumberIdentityRegistrationId; + + @JsonProperty + private ECSignedPreKey signedPreKey; + + @JsonProperty("pniSignedPreKey") + private ECSignedPreKey phoneNumberIdentitySignedPreKey; + + @JsonProperty + private long lastSeen; + + @JsonProperty + private long created; + + @JsonProperty + private String userAgent; + + @JsonProperty + private DeviceCapabilities capabilities; + + public String getApnId() { + return apnId; + } + + public void setApnId(String apnId) { + this.apnId = apnId; + + if (apnId != null) { + this.pushTimestamp = System.currentTimeMillis(); + } + } + + public String getVoipApnId() { + return voipApnId; + } + + public void setVoipApnId(String voipApnId) { + this.voipApnId = voipApnId; + } + + public void setUninstalledFeedbackTimestamp(long uninstalledFeedback) { + this.uninstalledFeedback = uninstalledFeedback; + } + + public long getUninstalledFeedbackTimestamp() { + return uninstalledFeedback; + } + + public void setLastSeen(long lastSeen) { + this.lastSeen = lastSeen; + } + + public long getLastSeen() { + return lastSeen; + } + + public void setCreated(long created) { + this.created = created; + } + + public long getCreated() { + return this.created; + } + + public String getGcmId() { + return gcmId; + } + + public void setGcmId(String gcmId) { + this.gcmId = gcmId; + + if (gcmId != null) { + this.pushTimestamp = System.currentTimeMillis(); + } + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public void setAuthTokenHash(SaltedTokenHash credentials) { + this.authToken = credentials.hash(); + this.salt = credentials.salt(); + } + + /** + * Has this device been manually locked? + * + * We lock a device by prepending "!" to its token. + * This character cannot normally appear in valid tokens. + * + * @return true if the credential was locked, false otherwise. + */ + public boolean hasLockedCredentials() { + SaltedTokenHash auth = getAuthTokenHash(); + return auth.hash().startsWith("!"); + } + + /** + * Lock device by invalidating authentication tokens. + * + * This should only be used from Account::lockAuthenticationCredentials. + * + * See that method for more information. + */ + public void lockAuthTokenHash() { + SaltedTokenHash oldAuth = getAuthTokenHash(); + String token = "!" + oldAuth.hash(); + String salt = oldAuth.salt(); + setAuthTokenHash(new SaltedTokenHash(token, salt)); + } + + public SaltedTokenHash getAuthTokenHash() { + return new SaltedTokenHash(authToken, salt); + } + + @Nullable + public DeviceCapabilities getCapabilities() { + return capabilities; + } + + public void setCapabilities(DeviceCapabilities capabilities) { + this.capabilities = capabilities; + } + + public boolean isEnabled() { + boolean hasChannel = fetchesMessages || !Util.isEmpty(getApnId()) || !Util.isEmpty(getGcmId()); + + return (id == MASTER_ID && hasChannel && signedPreKey != null) || + (id != MASTER_ID && hasChannel && signedPreKey != null && lastSeen > (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30))); + } + + public boolean getFetchesMessages() { + return fetchesMessages; + } + + public void setFetchesMessages(boolean fetchesMessages) { + this.fetchesMessages = fetchesMessages; + } + + public boolean isMaster() { + return getId() == MASTER_ID; + } + + public int getRegistrationId() { + return registrationId; + } + + public void setRegistrationId(int registrationId) { + this.registrationId = registrationId; + } + + public OptionalInt getPhoneNumberIdentityRegistrationId() { + return phoneNumberIdentityRegistrationId != null ? OptionalInt.of(phoneNumberIdentityRegistrationId) : OptionalInt.empty(); + } + + public void setPhoneNumberIdentityRegistrationId(final int phoneNumberIdentityRegistrationId) { + this.phoneNumberIdentityRegistrationId = phoneNumberIdentityRegistrationId; + } + + public ECSignedPreKey getSignedPreKey(final IdentityType identityType) { + return switch (identityType) { + case ACI -> signedPreKey; + case PNI -> phoneNumberIdentitySignedPreKey; + }; + } + + public void setSignedPreKey(ECSignedPreKey signedPreKey) { + this.signedPreKey = signedPreKey; + } + + public void setPhoneNumberIdentitySignedPreKey(final ECSignedPreKey phoneNumberIdentitySignedPreKey) { + this.phoneNumberIdentitySignedPreKey = phoneNumberIdentitySignedPreKey; + } + + public long getPushTimestamp() { + return pushTimestamp; + } + + public void setUserAgent(String userAgent) { + this.userAgent = userAgent; + } + + public String getUserAgent() { + return this.userAgent; + } + + public record DeviceCapabilities(boolean storage, boolean transfer, boolean pni, boolean paymentActivation) { + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/DynamicConfigurationManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/DynamicConfigurationManager.java new file mode 100644 index 000000000..e9f1b1f0b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/DynamicConfigurationManager.java @@ -0,0 +1,177 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.annotations.VisibleForTesting; +import io.micrometer.core.instrument.Metrics; +import java.time.Duration; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.Util; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient; +import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationRequest; +import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse; +import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest; +import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionResponse; + +public class DynamicConfigurationManager { + + private final String application; + private final String environment; + private final String configurationName; + private final AppConfigDataClient appConfigClient; + private final Class configurationClass; + + // Set on initial config fetch + private final AtomicReference configuration = new AtomicReference<>(); + private String configurationToken = null; + private boolean initialized = false; + + private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator(); + + private static final String ERROR_COUNTER_NAME = name(DynamicConfigurationManager.class, "error"); + private static final String ERROR_TYPE_TAG_NAME = "type"; + private static final String CONFIG_CLASS_TAG_NAME = "configClass"; + + private static final Logger logger = LoggerFactory.getLogger(DynamicConfigurationManager.class); + + public DynamicConfigurationManager(String application, String environment, String configurationName, + Class configurationClass) { + this(AppConfigDataClient + .builder() + .overrideConfiguration(ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofSeconds(10)) + .apiCallAttemptTimeout(Duration.ofSeconds(10)).build()) + .build(), + application, environment, configurationName, configurationClass); + } + + @VisibleForTesting + DynamicConfigurationManager(AppConfigDataClient appConfigClient, String application, String environment, + String configurationName, Class configurationClass) { + this.appConfigClient = appConfigClient; + this.application = application; + this.environment = environment; + this.configurationName = configurationName; + this.configurationClass = configurationClass; + } + + public T getConfiguration() { + synchronized (this) { + while (!initialized) { + Util.wait(this); + } + } + return configuration.get(); + } + + public void start() { + configuration.set(retrieveInitialDynamicConfiguration()); + synchronized (this) { + this.initialized = true; + this.notifyAll(); + } + + final Thread workerThread = new Thread(() -> { + while (true) { + try { + retrieveDynamicConfiguration().ifPresent(configuration::set); + } catch (Exception e) { + logger.warn("Error retrieving dynamic configuration", e); + } + + Util.sleep(5000); + } + }, "DynamicConfigurationManagerWorker"); + + workerThread.setDaemon(true); + workerThread.start(); + } + + private Optional retrieveDynamicConfiguration() throws JsonProcessingException { + if (configurationToken == null) { + logger.error("Invalid configuration token, will not be able to fetch configuration updates"); + } + GetLatestConfigurationResponse latestConfiguration; + try { + latestConfiguration = appConfigClient.getLatestConfiguration(GetLatestConfigurationRequest.builder() + .configurationToken(configurationToken) + .build()); + // token to use in the next fetch + configurationToken = latestConfiguration.nextPollConfigurationToken(); + logger.debug("next token: {}", configurationToken); + } catch (final RuntimeException e) { + Metrics.counter(ERROR_COUNTER_NAME, ERROR_TYPE_TAG_NAME, "fetch").increment(); + throw e; + } + + if (!latestConfiguration.configuration().asByteBuffer().hasRemaining()) { + // empty configuration means nothing has changed + return Optional.empty(); + } + logger.info("Received new config of length {}, next configuration token: {}", + latestConfiguration.configuration().asByteBuffer().remaining(), + configurationToken); + + try { + return parseConfiguration(latestConfiguration.configuration().asUtf8String(), configurationClass); + } catch (final JsonProcessingException e) { + Metrics.counter(ERROR_COUNTER_NAME, + ERROR_TYPE_TAG_NAME, "parse", + CONFIG_CLASS_TAG_NAME, configurationClass.getName()).increment(); + throw e; + } + } + + @VisibleForTesting + public static Optional parseConfiguration(final String configurationYaml, final Class configurationClass) + throws JsonProcessingException { + final T configuration = SystemMapper.yamlMapper().readValue(configurationYaml, configurationClass); + final Set> violations = VALIDATOR.validate(configuration); + + final Optional maybeDynamicConfiguration; + + if (violations.isEmpty()) { + maybeDynamicConfiguration = Optional.of(configuration); + } else { + logger.warn("Failed to validate configuration: {}", violations); + maybeDynamicConfiguration = Optional.empty(); + } + + return maybeDynamicConfiguration; + } + + private T retrieveInitialDynamicConfiguration() { + for (;;) { + try { + if (configurationToken == null) { + // first time around, start the configuration session + final StartConfigurationSessionResponse startResponse = appConfigClient + .startConfigurationSession(StartConfigurationSessionRequest.builder() + .applicationIdentifier(application) + .environmentIdentifier(environment) + .configurationProfileIdentifier(configurationName).build()); + configurationToken = startResponse.initialConfigurationToken(); + } + return retrieveDynamicConfiguration().orElseThrow(() -> new IllegalStateException("No initial configuration available")); + } catch (Exception e) { + logger.warn("Error retrieving initial dynamic configuration", e); + Util.sleep(1000); + } + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/IssuedReceiptsManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/IssuedReceiptsManager.java new file mode 100644 index 000000000..483b4bfd2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/IssuedReceiptsManager.java @@ -0,0 +1,126 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.whispersystems.textsecuregcm.util.AttributeValues.b; +import static org.whispersystems.textsecuregcm.util.AttributeValues.n; +import static org.whispersystems.textsecuregcm.util.AttributeValues.s; + +import com.google.common.base.Throwables; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.core.Response.Status; +import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequest; +import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.ReturnValue; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; + +public class IssuedReceiptsManager { + + public static final String KEY_PROCESSOR_ITEM_ID = "A"; // S (HashKey) + public static final String KEY_ISSUED_RECEIPT_TAG = "B"; // B + public static final String KEY_EXPIRATION = "E"; // N + + private final String table; + private final Duration expiration; + private final DynamoDbAsyncClient dynamoDbAsyncClient; + private final byte[] receiptTagGenerator; + + public IssuedReceiptsManager( + @Nonnull String table, + @Nonnull Duration expiration, + @Nonnull DynamoDbAsyncClient dynamoDbAsyncClient, + @Nonnull byte[] receiptTagGenerator) { + this.table = Objects.requireNonNull(table); + this.expiration = Objects.requireNonNull(expiration); + this.dynamoDbAsyncClient = Objects.requireNonNull(dynamoDbAsyncClient); + this.receiptTagGenerator = Objects.requireNonNull(receiptTagGenerator); + } + + /** + * Returns a future that completes normally if either this processor item was never issued a receipt credential + * previously OR if it was issued a receipt credential previously for the exact same receipt credential request + * enabling clients to retry in case they missed the original response. + *

    + * If this item has already been used to issue another receipt, throws a 409 conflict web application exception. + *

    + * For {@link SubscriptionProcessor#STRIPE}, item is expected to refer to an invoice line item (subscriptions) or a + * payment intent (one-time). + */ + public CompletableFuture recordIssuance( + String processorItemId, + SubscriptionProcessor processor, + ReceiptCredentialRequest request, + Instant now) { + + final AttributeValue key; + if (processor == SubscriptionProcessor.STRIPE) { + // As the first processor, Stripe’s IDs were not prefixed. Its item IDs have documented prefixes (`il_`, `pi_`) + // that will not collide with `SubscriptionProcessor` names + key = s(processorItemId); + } else { + key = s(processor.name() + "_" + processorItemId); + } + UpdateItemRequest updateItemRequest = UpdateItemRequest.builder() + .tableName(table) + .key(Map.of(KEY_PROCESSOR_ITEM_ID, key)) + .conditionExpression("attribute_not_exists(#key) OR #tag = :tag") + .returnValues(ReturnValue.NONE) + .updateExpression("SET " + + "#tag = if_not_exists(#tag, :tag), " + + "#exp = if_not_exists(#exp, :exp)") + .expressionAttributeNames(Map.of( + "#key", KEY_PROCESSOR_ITEM_ID, + "#tag", KEY_ISSUED_RECEIPT_TAG, + "#exp", KEY_EXPIRATION)) + .expressionAttributeValues(Map.of( + ":tag", b(generateIssuedReceiptTag(request)), + ":exp", n(now.plus(expiration).getEpochSecond()))) + .build(); + return dynamoDbAsyncClient.updateItem(updateItemRequest).handle((updateItemResponse, throwable) -> { + if (throwable != null) { + Throwable rootCause = Throwables.getRootCause(throwable); + if (rootCause instanceof ConditionalCheckFailedException) { + throw new ClientErrorException(Status.CONFLICT, rootCause); + } + Throwables.throwIfUnchecked(throwable); + throw new CompletionException(throwable); + } + return null; + }); + } + + private byte[] generateIssuedReceiptTag(ReceiptCredentialRequest request) { + return generateHmac("issuedReceiptTag", mac -> mac.update(request.serialize())); + } + + private byte[] generateHmac(String type, Consumer byteConsumer) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(receiptTagGenerator, "HmacSHA256")); + mac.update(type.getBytes(StandardCharsets.UTF_8)); + byteConsumer.accept(mac); + return mac.doFinal(); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new AssertionError(e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/KeysManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/KeysManager.java new file mode 100644 index 000000000..65167ceb3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/KeysManager.java @@ -0,0 +1,152 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.google.common.annotations.VisibleForTesting; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.entities.ECPreKey; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; + +public class KeysManager { + + private final DynamicConfigurationManager dynamicConfigurationManager; + + private final SingleUseECPreKeyStore ecPreKeys; + private final SingleUseKEMPreKeyStore pqPreKeys; + private final RepeatedUseECSignedPreKeyStore ecSignedPreKeys; + private final RepeatedUseKEMSignedPreKeyStore pqLastResortKeys; + + public KeysManager( + final DynamoDbAsyncClient dynamoDbAsyncClient, + final String ecTableName, + final String pqTableName, + final String ecSignedPreKeysTableName, + final String pqLastResortTableName, + final DynamicConfigurationManager dynamicConfigurationManager) { + this.ecPreKeys = new SingleUseECPreKeyStore(dynamoDbAsyncClient, ecTableName); + this.pqPreKeys = new SingleUseKEMPreKeyStore(dynamoDbAsyncClient, pqTableName); + this.ecSignedPreKeys = new RepeatedUseECSignedPreKeyStore(dynamoDbAsyncClient, ecSignedPreKeysTableName); + this.pqLastResortKeys = new RepeatedUseKEMSignedPreKeyStore(dynamoDbAsyncClient, pqLastResortTableName); + this.dynamicConfigurationManager = dynamicConfigurationManager; + } + + public CompletableFuture store(final UUID identifier, final long deviceId, final List keys) { + return store(identifier, deviceId, keys, null, null, null); + } + + public CompletableFuture store( + final UUID identifier, final long deviceId, + @Nullable final List ecKeys, + @Nullable final List pqKeys, + @Nullable final ECSignedPreKey ecSignedPreKey, + @Nullable final KEMSignedPreKey pqLastResortKey) { + + final List> storeFutures = new ArrayList<>(); + + if (ecKeys != null && !ecKeys.isEmpty()) { + storeFutures.add(ecPreKeys.store(identifier, deviceId, ecKeys)); + } + + if (pqKeys != null && !pqKeys.isEmpty()) { + storeFutures.add(pqPreKeys.store(identifier, deviceId, pqKeys)); + } + + if (ecSignedPreKey != null && dynamicConfigurationManager.getConfiguration().getEcPreKeyMigrationConfiguration().storeEcSignedPreKeys()) { + storeFutures.add(ecSignedPreKeys.store(identifier, deviceId, ecSignedPreKey)); + } + + if (pqLastResortKey != null) { + storeFutures.add(pqLastResortKeys.store(identifier, deviceId, pqLastResortKey)); + } + + return CompletableFuture.allOf(storeFutures.toArray(new CompletableFuture[0])); + } + + public CompletableFuture storeEcSignedPreKeys(final UUID identifier, final Map keys) { + if (dynamicConfigurationManager.getConfiguration().getEcPreKeyMigrationConfiguration().storeEcSignedPreKeys()) { + return ecSignedPreKeys.store(identifier, keys); + } else { + return CompletableFuture.completedFuture(null); + } + } + + public CompletableFuture storeEcSignedPreKeyIfAbsent(final UUID identifier, final long deviceId, final ECSignedPreKey signedPreKey) { + return ecSignedPreKeys.storeIfAbsent(identifier, deviceId, signedPreKey); + } + + public CompletableFuture storePqLastResort(final UUID identifier, final Map keys) { + return pqLastResortKeys.store(identifier, keys); + } + + public CompletableFuture storeEcOneTimePreKeys(final UUID identifier, final long deviceId, final List preKeys) { + return ecPreKeys.store(identifier, deviceId, preKeys); + } + + public CompletableFuture storeKemOneTimePreKeys(final UUID identifier, final long deviceId, final List preKeys) { + return pqPreKeys.store(identifier, deviceId, preKeys); + } + + public CompletableFuture> takeEC(final UUID identifier, final long deviceId) { + return ecPreKeys.take(identifier, deviceId); + } + + public CompletableFuture> takePQ(final UUID identifier, final long deviceId) { + return pqPreKeys.take(identifier, deviceId) + .thenCompose(maybeSingleUsePreKey -> maybeSingleUsePreKey + .map(singleUsePreKey -> CompletableFuture.completedFuture(maybeSingleUsePreKey)) + .orElseGet(() -> pqLastResortKeys.find(identifier, deviceId))); + } + + @VisibleForTesting + CompletableFuture> getLastResort(final UUID identifier, final long deviceId) { + return pqLastResortKeys.find(identifier, deviceId); + } + + public CompletableFuture> getEcSignedPreKey(final UUID identifier, final long deviceId) { + return ecSignedPreKeys.find(identifier, deviceId); + } + + public CompletableFuture> getPqEnabledDevices(final UUID identifier) { + return pqLastResortKeys.getDeviceIdsWithKeys(identifier).collectList().toFuture(); + } + + public CompletableFuture getEcCount(final UUID identifier, final long deviceId) { + return ecPreKeys.getCount(identifier, deviceId); + } + + public CompletableFuture getPqCount(final UUID identifier, final long deviceId) { + return pqPreKeys.getCount(identifier, deviceId); + } + + public CompletableFuture delete(final UUID accountUuid) { + return CompletableFuture.allOf( + ecPreKeys.delete(accountUuid), + pqPreKeys.delete(accountUuid), + dynamicConfigurationManager.getConfiguration().getEcPreKeyMigrationConfiguration().deleteEcSignedPreKeys() + ? ecSignedPreKeys.delete(accountUuid) + : CompletableFuture.completedFuture(null), + pqLastResortKeys.delete(accountUuid)); + } + + public CompletableFuture delete(final UUID accountUuid, final long deviceId) { + return CompletableFuture.allOf( + ecPreKeys.delete(accountUuid, deviceId), + pqPreKeys.delete(accountUuid, deviceId), + dynamicConfigurationManager.getConfiguration().getEcPreKeyMigrationConfiguration().deleteEcSignedPreKeys() + ? ecSignedPreKeys.delete(accountUuid, deviceId) + : CompletableFuture.completedFuture(null), + pqLastResortKeys.delete(accountUuid, deviceId)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessageAvailabilityListener.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessageAvailabilityListener.java new file mode 100644 index 000000000..e7fed470a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessageAvailabilityListener.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +/** + * A message availability listener is notified when new messages are available for a specific device for a specific + * account. Availability listeners are also notified when messages are moved from the message cache to long-term storage + * as an optimization hint to implementing classes. + */ +public interface MessageAvailabilityListener { + + /** + * @return whether the listener is still active. {@code false} indicates the listener can no longer handle messages + * and may be discarded + */ + boolean handleNewMessagesAvailable(); + + /** + * @return whether the listener is still active. {@code false} indicates the listener can no longer handle messages + * and may be discarded + */ + boolean handleMessagesPersisted(); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagePersistenceException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagePersistenceException.java new file mode 100644 index 000000000..3e96a8fc4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagePersistenceException.java @@ -0,0 +1,13 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +public class MessagePersistenceException extends Exception { + + public MessagePersistenceException(String message) { + super(message); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagePersister.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagePersister.java new file mode 100644 index 000000000..09860d290 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagePersister.java @@ -0,0 +1,205 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static com.codahale.metrics.MetricRegistry.name; +import static io.micrometer.core.instrument.Metrics.counter; + +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.SharedMetricRegistries; +import com.codahale.metrics.Timer; +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.lifecycle.Managed; +import io.micrometer.core.instrument.Counter; +import software.amazon.awssdk.services.dynamodb.model.ItemCollectionSizeLimitExceededException; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.util.Constants; +import org.whispersystems.textsecuregcm.util.Util; + +public class MessagePersister implements Managed { + + private final MessagesCache messagesCache; + private final MessagesManager messagesManager; + private final AccountsManager accountsManager; + + private final Duration persistDelay; + + private final boolean dedicatedProcess; + private final Thread[] workerThreads; + private volatile boolean running; + + private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); + private final Timer getQueuesTimer = metricRegistry.timer(name(MessagePersister.class, "getQueues")); + private final Timer persistQueueTimer = metricRegistry.timer(name(MessagePersister.class, "persistQueue")); + private final Meter persistQueueExceptionMeter = metricRegistry.meter( + name(MessagePersister.class, "persistQueueException")); + private final Counter oversizedQueueCounter = counter(name(MessagePersister.class, "persistQueueOversized")); + private final Histogram queueCountHistogram = metricRegistry.histogram(name(MessagePersister.class, "queueCount")); + private final Histogram queueSizeHistogram = metricRegistry.histogram(name(MessagePersister.class, "queueSize")); + + static final int QUEUE_BATCH_LIMIT = 100; + static final int MESSAGE_BATCH_LIMIT = 100; + + private static final long EXCEPTION_PAUSE_MILLIS = Duration.ofSeconds(3).toMillis(); + + private static final int CONSECUTIVE_EMPTY_CACHE_REMOVAL_LIMIT = 3; + + private static final Logger logger = LoggerFactory.getLogger(MessagePersister.class); + + public MessagePersister(final MessagesCache messagesCache, final MessagesManager messagesManager, + final AccountsManager accountsManager, + final DynamicConfigurationManager dynamicConfigurationManager, + final Duration persistDelay, + final int dedicatedProcessWorkerThreadCount) { + this.messagesCache = messagesCache; + this.messagesManager = messagesManager; + this.accountsManager = accountsManager; + this.persistDelay = persistDelay; + this.workerThreads = new Thread[dedicatedProcessWorkerThreadCount]; + this.dedicatedProcess = true; + + for (int i = 0; i < workerThreads.length; i++) { + workerThreads[i] = new Thread(() -> { + while (running) { + if (dynamicConfigurationManager.getConfiguration().getMessagePersisterConfiguration() + .isPersistenceEnabled()) { + try { + final int queuesPersisted = persistNextQueues(Instant.now()); + queueCountHistogram.update(queuesPersisted); + + if (queuesPersisted == 0) { + Util.sleep(100); + } + } catch (final Throwable t) { + logger.warn("Failed to persist queues", t); + Util.sleep(EXCEPTION_PAUSE_MILLIS); + } + } else { + Util.sleep(1000); + } + } + }, "MessagePersisterWorker-" + i); + } + } + + @VisibleForTesting + Duration getPersistDelay() { + return persistDelay; + } + + @Override + public void start() { + running = true; + + for (final Thread workerThread : workerThreads) { + workerThread.start(); + } + } + + @Override + public void stop() { + running = false; + + for (final Thread workerThread : workerThreads) { + try { + workerThread.join(); + } catch (final InterruptedException e) { + logger.warn("Interrupted while waiting for worker thread to complete current operation"); + } + } + } + + @VisibleForTesting + int persistNextQueues(final Instant currentTime) { + final int slot = messagesCache.getNextSlotToPersist(); + + List queuesToPersist; + int queuesPersisted = 0; + + do { + try (final Timer.Context ignored = getQueuesTimer.time()) { + queuesToPersist = messagesCache.getQueuesToPersist(slot, currentTime.minus(persistDelay), QUEUE_BATCH_LIMIT); + } + + for (final String queue : queuesToPersist) { + final UUID accountUuid = MessagesCache.getAccountUuidFromQueueName(queue); + final long deviceId = MessagesCache.getDeviceIdFromQueueName(queue); + + try { + persistQueue(accountUuid, deviceId); + } catch (final Exception e) { + if (e instanceof ItemCollectionSizeLimitExceededException) { + oversizedQueueCounter.increment(); + } + persistQueueExceptionMeter.mark(); + logger.warn("Failed to persist queue {}::{}; will schedule for retry", accountUuid, deviceId, e); + + messagesCache.addQueueToPersist(accountUuid, deviceId); + + Util.sleep(EXCEPTION_PAUSE_MILLIS); + } + } + + queuesPersisted += queuesToPersist.size(); + } while (queuesToPersist.size() >= QUEUE_BATCH_LIMIT); + + return queuesPersisted; + } + + @VisibleForTesting + void persistQueue(final UUID accountUuid, final long deviceId) throws MessagePersistenceException { + final Optional maybeAccount = accountsManager.getByAccountIdentifier(accountUuid); + + if (maybeAccount.isEmpty()) { + logger.error("No account record found for account {}", accountUuid); + return; + } + + try (final Timer.Context ignored = persistQueueTimer.time()) { + messagesCache.lockQueueForPersistence(accountUuid, deviceId); + + try { + int messageCount = 0; + List messages; + + int consecutiveEmptyCacheRemovals = 0; + + do { + messages = messagesCache.getMessagesToPersist(accountUuid, deviceId, MESSAGE_BATCH_LIMIT); + + int messagesRemovedFromCache = messagesManager.persistMessages(accountUuid, deviceId, messages); + messageCount += messages.size(); + + if (messagesRemovedFromCache == 0) { + consecutiveEmptyCacheRemovals += 1; + } else { + consecutiveEmptyCacheRemovals = 0; + } + + if (consecutiveEmptyCacheRemovals > CONSECUTIVE_EMPTY_CACHE_REMOVAL_LIMIT) { + throw new MessagePersistenceException("persistence failure loop detected"); + } + + } while (!messages.isEmpty()); + + queueSizeHistogram.update(messageCount); + } finally { + messagesCache.unlockQueueForPersistence(accountUuid, deviceId); + } + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagesCache.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagesCache.java new file mode 100644 index 000000000..c0be72667 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagesCache.java @@ -0,0 +1,545 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.InvalidProtocolBufferException; +import io.dropwizard.lifecycle.Managed; +import io.lettuce.core.ScoredValue; +import io.lettuce.core.ScriptOutputType; +import io.lettuce.core.ZAddArgs; +import io.lettuce.core.cluster.SlotHash; +import io.lettuce.core.cluster.models.partitions.RedisClusterNode; +import io.lettuce.core.cluster.pubsub.RedisClusterPubSubAdapter; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.redis.ClusterLuaScript; +import org.whispersystems.textsecuregcm.redis.FaultTolerantPubSubConnection; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.RedisClusterUtil; +import org.whispersystems.textsecuregcm.util.Util; +import reactor.core.observability.micrometer.Micrometer; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +public class MessagesCache extends RedisClusterPubSubAdapter implements Managed { + + private final FaultTolerantRedisCluster readDeleteCluster; + private final FaultTolerantPubSubConnection pubSubConnection; + private final Clock clock; + + private final ExecutorService notificationExecutorService; + private final Scheduler messageDeliveryScheduler; + private final ExecutorService messageDeletionExecutorService; + // messageDeletionExecutorService wrapped into a reactor Scheduler + private final Scheduler messageDeletionScheduler; + + private final ClusterLuaScript insertScript; + private final ClusterLuaScript removeByGuidScript; + private final ClusterLuaScript getItemsScript; + private final ClusterLuaScript removeQueueScript; + private final ClusterLuaScript getQueuesToPersistScript; + + private final Map messageListenersByQueueName = new HashMap<>(); + private final Map queueNamesByMessageListener = new IdentityHashMap<>(); + + private final Timer insertTimer = Metrics.timer(name(MessagesCache.class, "insert")); + private final Timer getMessagesTimer = Metrics.timer(name(MessagesCache.class, "get")); + private final Timer getQueuesToPersistTimer = Metrics.timer(name(MessagesCache.class, "getQueuesToPersist")); + private final Timer clearQueueTimer = Metrics.timer(name(MessagesCache.class, "clear")); + private final Counter pubSubMessageCounter = Metrics.counter(name(MessagesCache.class, "pubSubMessage")); + private final Counter newMessageNotificationCounter = Metrics.counter( + name(MessagesCache.class, "newMessageNotification")); + private final Counter queuePersistedNotificationCounter = Metrics.counter( + name(MessagesCache.class, "queuePersisted")); + private final Counter staleEphemeralMessagesCounter = Metrics.counter( + name(MessagesCache.class, "staleEphemeralMessages")); + private final Counter messageAvailabilityListenerRemovedAfterAddCounter = Metrics.counter( + name(MessagesCache.class, "messageAvailabilityListenerRemovedAfterAdd")); + private final Counter prunedStaleSubscriptionCounter = Metrics.counter( + name(MessagesCache.class, "prunedStaleSubscription")); + + static final String NEXT_SLOT_TO_PERSIST_KEY = "user_queue_persist_slot"; + private static final byte[] LOCK_VALUE = "1".getBytes(StandardCharsets.UTF_8); + + private static final String QUEUE_KEYSPACE_PREFIX = "__keyspace@0__:user_queue::"; + private static final String PERSISTING_KEYSPACE_PREFIX = "__keyspace@0__:user_queue_persisting::"; + + @VisibleForTesting + static final Duration MAX_EPHEMERAL_MESSAGE_DELAY = Duration.ofSeconds(10); + + private static final String GET_FLUX_NAME = MetricsUtil.name(MessagesCache.class, "get"); + private static final int PAGE_SIZE = 100; + + private static final Logger logger = LoggerFactory.getLogger(MessagesCache.class); + + public MessagesCache(final FaultTolerantRedisCluster insertCluster, final FaultTolerantRedisCluster readDeleteCluster, + final ExecutorService notificationExecutorService, final Scheduler messageDeliveryScheduler, + final ExecutorService messageDeletionExecutorService, final Clock clock) throws IOException { + + this.readDeleteCluster = readDeleteCluster; + this.pubSubConnection = readDeleteCluster.createPubSubConnection(); + this.clock = clock; + + this.notificationExecutorService = notificationExecutorService; + this.messageDeliveryScheduler = messageDeliveryScheduler; + this.messageDeletionExecutorService = messageDeletionExecutorService; + this.messageDeletionScheduler = Schedulers.fromExecutorService(messageDeletionExecutorService, "messageDeletion"); + + this.insertScript = ClusterLuaScript.fromResource(insertCluster, "lua/insert_item.lua", ScriptOutputType.INTEGER); + this.removeByGuidScript = ClusterLuaScript.fromResource(readDeleteCluster, "lua/remove_item_by_guid.lua", + ScriptOutputType.MULTI); + this.getItemsScript = ClusterLuaScript.fromResource(readDeleteCluster, "lua/get_items.lua", ScriptOutputType.MULTI); + this.removeQueueScript = ClusterLuaScript.fromResource(readDeleteCluster, "lua/remove_queue.lua", + ScriptOutputType.STATUS); + this.getQueuesToPersistScript = ClusterLuaScript.fromResource(readDeleteCluster, "lua/get_queues_to_persist.lua", + ScriptOutputType.MULTI); + } + + @Override + public void start() { + pubSubConnection.usePubSubConnection(connection -> connection.addListener(this)); + pubSubConnection.subscribeToClusterTopologyChangedEvents(this::resubscribeAll); + } + + @Override + public void stop() { + pubSubConnection.usePubSubConnection(connection -> connection.sync().upstream().commands().unsubscribe()); + } + + private void resubscribeAll() { + + final Set queueNames; + + synchronized (messageListenersByQueueName) { + queueNames = new HashSet<>(messageListenersByQueueName.keySet()); + } + + for (final String queueName : queueNames) { + // avoid overwhelming a newly recovered node by processing synchronously, rather than using CompletableFuture.allOf() + subscribeForKeyspaceNotifications(queueName).join(); + } + } + + public long insert(final UUID guid, final UUID destinationUuid, final long destinationDevice, + final MessageProtos.Envelope message) { + final MessageProtos.Envelope messageWithGuid = message.toBuilder().setServerGuid(guid.toString()).build(); + return (long) insertTimer.record(() -> + insertScript.executeBinary(List.of(getMessageQueueKey(destinationUuid, destinationDevice), + getMessageQueueMetadataKey(destinationUuid, destinationDevice), + getQueueIndexKey(destinationUuid, destinationDevice)), + List.of(messageWithGuid.toByteArray(), + String.valueOf(message.getServerTimestamp()).getBytes(StandardCharsets.UTF_8), + guid.toString().getBytes(StandardCharsets.UTF_8)))); + } + + public CompletableFuture> remove(final UUID destinationUuid, + final long destinationDevice, + final UUID messageGuid) { + + return remove(destinationUuid, destinationDevice, List.of(messageGuid)) + .thenApply(removed -> removed.isEmpty() ? Optional.empty() : Optional.of(removed.get(0))); + } + + @SuppressWarnings("unchecked") + public CompletableFuture> remove(final UUID destinationUuid, + final long destinationDevice, + final List messageGuids) { + + return removeByGuidScript.executeBinaryAsync(List.of(getMessageQueueKey(destinationUuid, destinationDevice), + getMessageQueueMetadataKey(destinationUuid, destinationDevice), + getQueueIndexKey(destinationUuid, destinationDevice)), + messageGuids.stream().map(guid -> guid.toString().getBytes(StandardCharsets.UTF_8)) + .collect(Collectors.toList())) + .thenApplyAsync(result -> { + List serialized = (List) result; + + final List removedMessages = new ArrayList<>(serialized.size()); + + for (final byte[] bytes : serialized) { + try { + removedMessages.add(MessageProtos.Envelope.parseFrom(bytes)); + } catch (final InvalidProtocolBufferException e) { + logger.warn("Failed to parse envelope", e); + } + } + + return removedMessages; + }, messageDeletionExecutorService); + } + + public boolean hasMessages(final UUID destinationUuid, final long destinationDevice) { + return readDeleteCluster.withBinaryCluster( + connection -> connection.sync().zcard(getMessageQueueKey(destinationUuid, destinationDevice)) > 0); + } + + public Publisher get(final UUID destinationUuid, final long destinationDevice) { + + final long earliestAllowableEphemeralTimestamp = + clock.millis() - MAX_EPHEMERAL_MESSAGE_DELAY.toMillis(); + + final Flux allMessages = getAllMessages(destinationUuid, destinationDevice) + .publish() + // We expect exactly two subscribers to this base flux: + // 1. the websocket that delivers messages to clients + // 2. an internal process to discard stale ephemeral messages + // The discard subscriber will subscribe immediately, but we don’t want to do any work if the + // websocket never subscribes. + .autoConnect(2); + + final Flux messagesToPublish = allMessages + .filter(Predicate.not(envelope -> isStaleEphemeralMessage(envelope, earliestAllowableEphemeralTimestamp))); + + final Flux staleEphemeralMessages = allMessages + .filter(envelope -> isStaleEphemeralMessage(envelope, earliestAllowableEphemeralTimestamp)); + + discardStaleEphemeralMessages(destinationUuid, destinationDevice, staleEphemeralMessages); + + return messagesToPublish.name(GET_FLUX_NAME) + .tap(Micrometer.metrics(Metrics.globalRegistry)); + } + + private static boolean isStaleEphemeralMessage(final MessageProtos.Envelope message, + long earliestAllowableTimestamp) { + return message.hasEphemeral() && message.getEphemeral() && message.getTimestamp() < earliestAllowableTimestamp; + } + + private void discardStaleEphemeralMessages(final UUID destinationUuid, final long destinationDevice, + Flux staleEphemeralMessages) { + staleEphemeralMessages + .map(e -> UUID.fromString(e.getServerGuid())) + .buffer(PAGE_SIZE) + .subscribeOn(messageDeletionScheduler) + .subscribe(staleEphemeralMessageGuids -> + remove(destinationUuid, destinationDevice, staleEphemeralMessageGuids) + .thenAccept(removedMessages -> staleEphemeralMessagesCounter.increment(removedMessages.size())), + e -> logger.warn("Could not remove stale ephemeral messages from cache", e)); + } + + @VisibleForTesting + Flux getAllMessages(final UUID destinationUuid, final long destinationDevice) { + + // fetch messages by page + return getNextMessagePage(destinationUuid, destinationDevice, -1) + .expand(queueItemsAndLastMessageId -> { + // expand() is breadth-first, so each page will be published in order + if (queueItemsAndLastMessageId.first().isEmpty()) { + return Mono.empty(); + } + + return getNextMessagePage(destinationUuid, destinationDevice, queueItemsAndLastMessageId.second()); + }) + .limitRate(1) + // we want to ensure we don’t accidentally block the Lettuce/netty i/o executors + .publishOn(messageDeliveryScheduler) + .map(Pair::first) + .flatMapIterable(queueItems -> { + final List envelopes = new ArrayList<>(queueItems.size() / 2); + + for (int i = 0; i < queueItems.size() - 1; i += 2) { + try { + final MessageProtos.Envelope message = MessageProtos.Envelope.parseFrom(queueItems.get(i)); + + envelopes.add(message); + } catch (InvalidProtocolBufferException e) { + logger.warn("Failed to parse envelope", e); + } + } + + return envelopes; + }); + } + + private Flux, Long>> getNextMessagePage(final UUID destinationUuid, final long destinationDevice, + long messageId) { + + return getItemsScript.executeBinaryReactive( + List.of(getMessageQueueKey(destinationUuid, destinationDevice), + getPersistInProgressKey(destinationUuid, destinationDevice)), + List.of(String.valueOf(PAGE_SIZE).getBytes(StandardCharsets.UTF_8), + String.valueOf(messageId).getBytes(StandardCharsets.UTF_8))) + .map(result -> { + logger.trace("Processing page: {}", messageId); + + @SuppressWarnings("unchecked") + List queueItems = (List) result; + + if (queueItems.isEmpty()) { + return new Pair<>(Collections.emptyList(), null); + } + + if (queueItems.size() % 2 != 0) { + logger.error("\"Get messages\" operation returned a list with a non-even number of elements."); + return new Pair<>(Collections.emptyList(), null); + } + + final long lastMessageId = Long.parseLong( + new String(queueItems.get(queueItems.size() - 1), StandardCharsets.UTF_8)); + + return new Pair<>(queueItems, lastMessageId); + }); + } + + @VisibleForTesting + List getMessagesToPersist(final UUID accountUuid, final long destinationDevice, + final int limit) { + return getMessagesTimer.record(() -> { + final List> scoredMessages = readDeleteCluster.withBinaryCluster( + connection -> connection.sync() + .zrangeWithScores(getMessageQueueKey(accountUuid, destinationDevice), 0, limit)); + final List envelopes = new ArrayList<>(scoredMessages.size()); + + for (final ScoredValue scoredMessage : scoredMessages) { + try { + envelopes.add(MessageProtos.Envelope.parseFrom(scoredMessage.getValue())); + } catch (InvalidProtocolBufferException e) { + logger.warn("Failed to parse envelope", e); + } + } + + return envelopes; + }); + } + + public CompletableFuture clear(final UUID destinationUuid) { + final CompletableFuture[] clearFutures = new CompletableFuture[Device.MAXIMUM_DEVICE_ID]; + + for (int deviceId = 0; deviceId < Device.MAXIMUM_DEVICE_ID; deviceId++) { + clearFutures[deviceId] = clear(destinationUuid, deviceId); + } + + return CompletableFuture.allOf(clearFutures); + } + + public CompletableFuture clear(final UUID destinationUuid, final long deviceId) { + final Timer.Sample sample = Timer.start(); + + return removeQueueScript.executeBinaryAsync(List.of(getMessageQueueKey(destinationUuid, deviceId), + getMessageQueueMetadataKey(destinationUuid, deviceId), + getQueueIndexKey(destinationUuid, deviceId)), + Collections.emptyList()) + .thenRun(() -> sample.stop(clearQueueTimer)); + } + + int getNextSlotToPersist() { + return (int) (readDeleteCluster.withCluster(connection -> connection.sync().incr(NEXT_SLOT_TO_PERSIST_KEY)) + % SlotHash.SLOT_COUNT); + } + + List getQueuesToPersist(final int slot, final Instant maxTime, final int limit) { + //noinspection unchecked + return getQueuesToPersistTimer.record(() -> (List) getQueuesToPersistScript.execute( + List.of(new String(getQueueIndexKey(slot), StandardCharsets.UTF_8)), + List.of(String.valueOf(maxTime.toEpochMilli()), + String.valueOf(limit)))); + } + + void addQueueToPersist(final UUID accountUuid, final long deviceId) { + readDeleteCluster.useBinaryCluster(connection -> connection.sync() + .zadd(getQueueIndexKey(accountUuid, deviceId), ZAddArgs.Builder.nx(), System.currentTimeMillis(), + getMessageQueueKey(accountUuid, deviceId))); + } + + void lockQueueForPersistence(final UUID accountUuid, final long deviceId) { + readDeleteCluster.useBinaryCluster( + connection -> connection.sync().setex(getPersistInProgressKey(accountUuid, deviceId), 30, LOCK_VALUE)); + } + + void unlockQueueForPersistence(final UUID accountUuid, final long deviceId) { + readDeleteCluster.useBinaryCluster( + connection -> connection.sync().del(getPersistInProgressKey(accountUuid, deviceId))); + } + + public void addMessageAvailabilityListener(final UUID destinationUuid, final long deviceId, + final MessageAvailabilityListener listener) { + final String queueName = getQueueName(destinationUuid, deviceId); + + final CompletableFuture subscribeFuture; + synchronized (messageListenersByQueueName) { + messageListenersByQueueName.put(queueName, listener); + queueNamesByMessageListener.put(listener, queueName); + // Submit to the Redis queue within the synchronized block, but don’t wait until exiting + subscribeFuture = subscribeForKeyspaceNotifications(queueName); + } + + subscribeFuture.join(); + } + + public void removeMessageAvailabilityListener(final MessageAvailabilityListener listener) { + @Nullable final String queueName; + synchronized (messageListenersByQueueName) { + queueName = queueNamesByMessageListener.get(listener); + } + + if (queueName != null) { + + final CompletableFuture unsubscribeFuture; + synchronized (messageListenersByQueueName) { + queueNamesByMessageListener.remove(listener); + if (messageListenersByQueueName.remove(queueName, listener)) { + // Submit to the Redis queue within the synchronized block, but don’t wait until exiting + unsubscribeFuture = unsubscribeFromKeyspaceNotifications(queueName); + } else { + messageAvailabilityListenerRemovedAfterAddCounter.increment(); + unsubscribeFuture = CompletableFuture.completedFuture(null); + } + } + + unsubscribeFuture.join(); + } + } + + private void pruneStaleSubscription(final String channel) { + unsubscribeFromKeyspaceNotifications(getQueueNameFromKeyspaceChannel(channel)) + .thenRun(prunedStaleSubscriptionCounter::increment); + } + + private CompletableFuture subscribeForKeyspaceNotifications(final String queueName) { + final int slot = SlotHash.getSlot(queueName); + + return pubSubConnection.withPubSubConnection( + connection -> connection.async() + .nodes(node -> node.is(RedisClusterNode.NodeFlag.UPSTREAM) && node.hasSlot(slot)) + .commands() + .subscribe(getKeyspaceChannels(queueName))).toCompletableFuture() + .thenRun(Util.NOOP); + } + + private CompletableFuture unsubscribeFromKeyspaceNotifications(final String queueName) { + final int slot = SlotHash.getSlot(queueName); + + return pubSubConnection.withPubSubConnection( + connection -> connection.async() + .nodes(node -> node.is(RedisClusterNode.NodeFlag.UPSTREAM) && node.hasSlot(slot)) + .commands() + .unsubscribe(getKeyspaceChannels(queueName))) + .toCompletableFuture() + .thenRun(Util.NOOP); + } + + private static String[] getKeyspaceChannels(final String queueName) { + return new String[]{ + QUEUE_KEYSPACE_PREFIX + "{" + queueName + "}", + PERSISTING_KEYSPACE_PREFIX + "{" + queueName + "}" + }; + } + + @Override + public void message(final RedisClusterNode node, final String channel, final String message) { + pubSubMessageCounter.increment(); + + if (channel.startsWith(QUEUE_KEYSPACE_PREFIX) && "zadd".equals(message)) { + newMessageNotificationCounter.increment(); + notificationExecutorService.execute(() -> { + try { + findListener(channel).ifPresentOrElse(listener -> { + if (!listener.handleNewMessagesAvailable()) { + removeMessageAvailabilityListener(listener); + } + }, () -> pruneStaleSubscription(channel)); + } catch (final Exception e) { + logger.warn("Unexpected error handling new message", e); + } + }); + } else if (channel.startsWith(PERSISTING_KEYSPACE_PREFIX) && "del".equals(message)) { + queuePersistedNotificationCounter.increment(); + notificationExecutorService.execute(() -> { + try { + findListener(channel).ifPresentOrElse(listener -> { + if (!listener.handleMessagesPersisted()) { + removeMessageAvailabilityListener(listener); + } + }, () -> pruneStaleSubscription(channel)); + } catch (final Exception e) { + logger.warn("Unexpected error handling messages persisted", e); + } + }); + } + } + + private Optional findListener(final String keyspaceChannel) { + final String queueName = getQueueNameFromKeyspaceChannel(keyspaceChannel); + + synchronized (messageListenersByQueueName) { + return Optional.ofNullable(messageListenersByQueueName.get(queueName)); + } + } + + @VisibleForTesting + static String getQueueName(final UUID accountUuid, final long deviceId) { + return accountUuid + "::" + deviceId; + } + + @VisibleForTesting + static String getQueueNameFromKeyspaceChannel(final String channel) { + final int startOfHashTag = channel.indexOf('{'); + final int endOfHashTag = channel.lastIndexOf('}'); + + return channel.substring(startOfHashTag + 1, endOfHashTag); + } + + @VisibleForTesting + static byte[] getMessageQueueKey(final UUID accountUuid, final long deviceId) { + return ("user_queue::{" + accountUuid.toString() + "::" + deviceId + "}").getBytes(StandardCharsets.UTF_8); + } + + private static byte[] getMessageQueueMetadataKey(final UUID accountUuid, final long deviceId) { + return ("user_queue_metadata::{" + accountUuid.toString() + "::" + deviceId + "}").getBytes(StandardCharsets.UTF_8); + } + + private static byte[] getQueueIndexKey(final UUID accountUuid, final long deviceId) { + return getQueueIndexKey(SlotHash.getSlot(accountUuid.toString() + "::" + deviceId)); + } + + private static byte[] getQueueIndexKey(final int slot) { + return ("user_queue_index::{" + RedisClusterUtil.getMinimalHashTag(slot) + "}").getBytes(StandardCharsets.UTF_8); + } + + private static byte[] getPersistInProgressKey(final UUID accountUuid, final long deviceId) { + return ("user_queue_persisting::{" + accountUuid + "::" + deviceId + "}").getBytes(StandardCharsets.UTF_8); + } + + static UUID getAccountUuidFromQueueName(final String queueName) { + final int startOfHashTag = queueName.indexOf('{'); + + return UUID.fromString(queueName.substring(startOfHashTag + 1, queueName.indexOf("::", startOfHashTag))); + } + + static long getDeviceIdFromQueueName(final String queueName) { + return Long.parseLong(queueName.substring(queueName.lastIndexOf("::") + 2, queueName.lastIndexOf('}'))); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagesDynamoDb.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagesDynamoDb.java new file mode 100644 index 000000000..a4a85608f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagesDynamoDb.java @@ -0,0 +1,305 @@ +/* + * Copyright 2021-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static com.codahale.metrics.MetricRegistry.name; +import static io.micrometer.core.instrument.Metrics.timer; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.InvalidProtocolBufferException; +import io.micrometer.core.instrument.Timer; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.function.Predicate; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.ReturnValue; +import software.amazon.awssdk.services.dynamodb.model.WriteRequest; + +public class MessagesDynamoDb extends AbstractDynamoDbStore { + + @VisibleForTesting + static final String KEY_PARTITION = "H"; + + @VisibleForTesting + static final String KEY_SORT = "S"; + + @VisibleForTesting + static final String LOCAL_INDEX_MESSAGE_UUID_NAME = "Message_UUID_Index"; + + @VisibleForTesting + static final String LOCAL_INDEX_MESSAGE_UUID_KEY_SORT = "U"; + + private static final String KEY_TTL = "E"; + private static final String KEY_ENVELOPE_BYTES = "EB"; + + private final Timer storeTimer = timer(name(getClass(), "store")); + private final Timer deleteByAccount = timer(name(getClass(), "delete", "account")); + private final Timer deleteByDevice = timer(name(getClass(), "delete", "device")); + + private final DynamoDbAsyncClient dbAsyncClient; + private final String tableName; + private final Duration timeToLive; + private final ExecutorService messageDeletionExecutor; + private final Scheduler messageDeletionScheduler; + + private static final Logger logger = LoggerFactory.getLogger(MessagesDynamoDb.class); + + public MessagesDynamoDb(DynamoDbClient dynamoDb, DynamoDbAsyncClient dynamoDbAsyncClient, String tableName, + Duration timeToLive, ExecutorService messageDeletionExecutor) { + super(dynamoDb); + + this.dbAsyncClient = dynamoDbAsyncClient; + this.tableName = tableName; + this.timeToLive = timeToLive; + + this.messageDeletionExecutor = messageDeletionExecutor; + this.messageDeletionScheduler = Schedulers.fromExecutor(messageDeletionExecutor); + } + + public void store(final List messages, final UUID destinationAccountUuid, final long destinationDeviceId) { + storeTimer.record(() -> writeInBatches(messages, (messageBatch) -> storeBatch(messageBatch, destinationAccountUuid, destinationDeviceId))); + } + + private void storeBatch(final List messages, final UUID destinationAccountUuid, final long destinationDeviceId) { + if (messages.size() > DYNAMO_DB_MAX_BATCH_SIZE) { + throw new IllegalArgumentException("Maximum batch size of " + DYNAMO_DB_MAX_BATCH_SIZE + " exceeded with " + messages.size() + " messages"); + } + + final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid); + List writeItems = new ArrayList<>(); + for (MessageProtos.Envelope message : messages) { + final UUID messageUuid = UUID.fromString(message.getServerGuid()); + + final ImmutableMap.Builder item = ImmutableMap.builder() + .put(KEY_PARTITION, partitionKey) + .put(KEY_SORT, convertSortKey(destinationDeviceId, message.getServerTimestamp(), messageUuid)) + .put(LOCAL_INDEX_MESSAGE_UUID_KEY_SORT, convertLocalIndexMessageUuidSortKey(messageUuid)) + .put(KEY_TTL, AttributeValues.fromLong(getTtlForMessage(message))) + .put(KEY_ENVELOPE_BYTES, AttributeValue.builder().b(SdkBytes.fromByteArray(message.toByteArray())).build()); + + writeItems.add(WriteRequest.builder().putRequest(PutRequest.builder() + .item(item.build()) + .build()).build()); + } + + executeTableWriteItemsUntilComplete(Map.of(tableName, writeItems)); + } + + public Publisher load(final UUID destinationAccountUuid, final long destinationDeviceId, + final Integer limit) { + + final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid); + final QueryRequest.Builder queryRequestBuilder = QueryRequest.builder() + .tableName(tableName) + .consistentRead(true) + .keyConditionExpression("#part = :part AND begins_with ( #sort , :sortprefix )") + .expressionAttributeNames(Map.of( + "#part", KEY_PARTITION, + "#sort", KEY_SORT)) + .expressionAttributeValues(Map.of( + ":part", partitionKey, + ":sortprefix", convertDestinationDeviceIdToSortKeyPrefix(destinationDeviceId))); + + if (limit != null) { + // some callers don’t take advantage of reactive streams, so we want to support limiting the fetch size. Otherwise, + // we could fetch up to 1 MB (likely >1,000 messages) and discard 90% of them + queryRequestBuilder.limit(Math.min(RESULT_SET_CHUNK_SIZE, limit)); + } + + final QueryRequest queryRequest = queryRequestBuilder.build(); + + return dbAsyncClient.queryPaginator(queryRequest).items() + .map(message -> { + try { + return convertItemToEnvelope(message); + } catch (final InvalidProtocolBufferException e) { + logger.error("Failed to parse envelope", e); + return null; + } + }) + .filter(Predicate.not(Objects::isNull)); + } + + public CompletableFuture> deleteMessageByDestinationAndGuid( + final UUID destinationAccountUuid, final UUID messageUuid) { + + final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid); + final QueryRequest queryRequest = QueryRequest.builder() + .tableName(tableName) + .indexName(LOCAL_INDEX_MESSAGE_UUID_NAME) + .projectionExpression(KEY_SORT) + .consistentRead(true) + .keyConditionExpression("#part = :part AND #uuid = :uuid") + .expressionAttributeNames(Map.of( + "#part", KEY_PARTITION, + "#uuid", LOCAL_INDEX_MESSAGE_UUID_KEY_SORT)) + .expressionAttributeValues(Map.of( + ":part", partitionKey, + ":uuid", convertLocalIndexMessageUuidSortKey(messageUuid))) + .build(); + + // because we are filtering on message UUID, this query should return at most one item, + // but it’s simpler to handle the full stream and return the “last” item + return Flux.from(dbAsyncClient.queryPaginator(queryRequest).items()) + .flatMap(item -> Mono.fromCompletionStage(dbAsyncClient.deleteItem(DeleteItemRequest.builder() + .tableName(tableName) + .key(Map.of(KEY_PARTITION, partitionKey, KEY_SORT, + AttributeValues.fromByteArray(item.get(KEY_SORT).b().asByteArray()))) + .returnValues(ReturnValue.ALL_OLD) + .build()))) + .mapNotNull(deleteItemResponse -> { + try { + if (deleteItemResponse.attributes() != null && deleteItemResponse.attributes().containsKey(KEY_PARTITION)) { + return convertItemToEnvelope(deleteItemResponse.attributes()); + } + } catch (final InvalidProtocolBufferException e) { + logger.error("Failed to parse envelope", e); + } + return null; + }) + .map(Optional::ofNullable) + .subscribeOn(messageDeletionScheduler) + .last(Optional.empty()) // if the flux is empty, last() will throw without a default + .toFuture(); + } + + public CompletableFuture> deleteMessage(final UUID destinationAccountUuid, + final long destinationDeviceId, final UUID messageUuid, final long serverTimestamp) { + + final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid); + final AttributeValue sortKey = convertSortKey(destinationDeviceId, serverTimestamp, messageUuid); + DeleteItemRequest.Builder deleteItemRequest = DeleteItemRequest.builder() + .tableName(tableName) + .key(Map.of(KEY_PARTITION, partitionKey, KEY_SORT, sortKey)) + .returnValues(ReturnValue.ALL_OLD); + + return dbAsyncClient.deleteItem(deleteItemRequest.build()) + .thenApplyAsync(deleteItemResponse -> { + if (deleteItemResponse.attributes() != null && deleteItemResponse.attributes().containsKey(KEY_PARTITION)) { + try { + return Optional.of(convertItemToEnvelope(deleteItemResponse.attributes())); + } catch (final InvalidProtocolBufferException e) { + logger.error("Failed to parse envelope", e); + } + } + + return Optional.empty(); + }, messageDeletionExecutor); + } + + public CompletableFuture deleteAllMessagesForAccount(final UUID destinationAccountUuid) { + final Timer.Sample sample = Timer.start(); + + final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid); + + return Flux.from(dbAsyncClient.queryPaginator(QueryRequest.builder() + .tableName(tableName) + .projectionExpression(KEY_SORT) + .consistentRead(true) + .keyConditionExpression("#part = :part") + .expressionAttributeNames(Map.of("#part", KEY_PARTITION)) + .expressionAttributeValues(Map.of(":part", partitionKey)) + .build()) + .items()) + .flatMap(item -> Mono.fromFuture(() -> dbAsyncClient.deleteItem(DeleteItemRequest.builder() + .tableName(tableName) + .key(Map.of( + KEY_PARTITION, partitionKey, + KEY_SORT, item.get(KEY_SORT))) + .build())), + DYNAMO_DB_MAX_BATCH_SIZE) + .doOnComplete(() -> sample.stop(deleteByAccount)) + .then() + .toFuture(); + } + + public CompletableFuture deleteAllMessagesForDevice(final UUID destinationAccountUuid, final long destinationDeviceId) { + final Timer.Sample sample = Timer.start(); + final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid); + + return Flux.from(dbAsyncClient.queryPaginator(QueryRequest.builder() + .tableName(tableName) + .keyConditionExpression("#part = :part AND begins_with ( #sort , :sortprefix )") + .expressionAttributeNames(Map.of( + "#part", KEY_PARTITION, + "#sort", KEY_SORT)) + .expressionAttributeValues(Map.of( + ":part", partitionKey, + ":sortprefix", convertDestinationDeviceIdToSortKeyPrefix(destinationDeviceId))) + .projectionExpression(KEY_SORT) + .consistentRead(true) + .build()) + .items()) + .flatMap(item -> Mono.fromFuture(() -> dbAsyncClient.deleteItem(DeleteItemRequest.builder() + .tableName(tableName) + .key(Map.of( + KEY_PARTITION, partitionKey, + KEY_SORT, item.get(KEY_SORT))) + .build())), + DYNAMO_DB_MAX_BATCH_SIZE) + .doOnComplete(() -> sample.stop(deleteByDevice)) + .then() + .toFuture(); + } + + @VisibleForTesting + static MessageProtos.Envelope convertItemToEnvelope(final Map item) + throws InvalidProtocolBufferException { + + return MessageProtos.Envelope.parseFrom(item.get(KEY_ENVELOPE_BYTES).b().asByteArray()); + } + + private long getTtlForMessage(MessageProtos.Envelope message) { + return message.getServerTimestamp() / 1000 + timeToLive.getSeconds(); + } + + private static AttributeValue convertPartitionKey(final UUID destinationAccountUuid) { + return AttributeValues.fromUUID(destinationAccountUuid); + } + + private static AttributeValue convertSortKey(final long destinationDeviceId, final long serverTimestamp, final UUID messageUuid) { + ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[32]); + byteBuffer.putLong(destinationDeviceId); + byteBuffer.putLong(serverTimestamp); + byteBuffer.putLong(messageUuid.getMostSignificantBits()); + byteBuffer.putLong(messageUuid.getLeastSignificantBits()); + return AttributeValues.fromByteBuffer(byteBuffer.flip()); + } + + private static AttributeValue convertDestinationDeviceIdToSortKeyPrefix(final long destinationDeviceId) { + ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[8]); + byteBuffer.putLong(destinationDeviceId); + return AttributeValues.fromByteBuffer(byteBuffer.flip()); + } + + private static AttributeValue convertLocalIndexMessageUuidSortKey(final UUID messageUuid) { + return AttributeValues.fromUUID(messageUuid); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagesManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagesManager.java new file mode 100644 index 000000000..27b13b868 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/MessagesManager.java @@ -0,0 +1,177 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.SharedMetricRegistries; +import io.micrometer.core.instrument.Metrics; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.util.Constants; +import org.whispersystems.textsecuregcm.util.Pair; +import reactor.core.observability.micrometer.Micrometer; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class MessagesManager { + + private static final int RESULT_SET_CHUNK_SIZE = 100; + final String GET_MESSAGES_FOR_DEVICE_FLUX_NAME = MetricsUtil.name(MessagesManager.class, "getMessagesForDevice"); + + private static final Logger logger = LoggerFactory.getLogger(MessagesManager.class); + + private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); + private static final Meter cacheHitByGuidMeter = metricRegistry.meter(name(MessagesManager.class, "cacheHitByGuid")); + private static final Meter cacheMissByGuidMeter = metricRegistry.meter( + name(MessagesManager.class, "cacheMissByGuid")); + private static final Meter persistMessageMeter = metricRegistry.meter(name(MessagesManager.class, "persistMessage")); + + private final MessagesDynamoDb messagesDynamoDb; + private final MessagesCache messagesCache; + private final ReportMessageManager reportMessageManager; + private final ExecutorService messageDeletionExecutor; + + public MessagesManager( + final MessagesDynamoDb messagesDynamoDb, + final MessagesCache messagesCache, + final ReportMessageManager reportMessageManager, + final ExecutorService messageDeletionExecutor) { + this.messagesDynamoDb = messagesDynamoDb; + this.messagesCache = messagesCache; + this.reportMessageManager = reportMessageManager; + this.messageDeletionExecutor = messageDeletionExecutor; + } + + public void insert(UUID destinationUuid, long destinationDevice, Envelope message) { + final UUID messageGuid = UUID.randomUUID(); + + messagesCache.insert(messageGuid, destinationUuid, destinationDevice, message); + + if (message.hasSourceUuid() && !destinationUuid.toString().equals(message.getSourceUuid())) { + reportMessageManager.store(message.getSourceUuid(), messageGuid); + } + } + + public boolean hasCachedMessages(final UUID destinationUuid, final long destinationDevice) { + return messagesCache.hasMessages(destinationUuid, destinationDevice); + } + + public Mono, Boolean>> getMessagesForDevice(UUID destinationUuid, long destinationDevice, + boolean cachedMessagesOnly) { + + return Flux.from( + getMessagesForDevice(destinationUuid, destinationDevice, RESULT_SET_CHUNK_SIZE, cachedMessagesOnly)) + .take(RESULT_SET_CHUNK_SIZE) + .collectList() + .map(envelopes -> new Pair<>(envelopes, envelopes.size() >= RESULT_SET_CHUNK_SIZE)); + } + + public Publisher getMessagesForDeviceReactive(UUID destinationUuid, long destinationDevice, + final boolean cachedMessagesOnly) { + + return getMessagesForDevice(destinationUuid, destinationDevice, null, cachedMessagesOnly); + } + + private Publisher getMessagesForDevice(UUID destinationUuid, long destinationDevice, + @Nullable Integer limit, final boolean cachedMessagesOnly) { + + final Publisher dynamoPublisher = + cachedMessagesOnly ? Flux.empty() : messagesDynamoDb.load(destinationUuid, destinationDevice, limit); + final Publisher cachePublisher = messagesCache.get(destinationUuid, destinationDevice); + + return Flux.concat(dynamoPublisher, cachePublisher) + .name(GET_MESSAGES_FOR_DEVICE_FLUX_NAME) + .tap(Micrometer.metrics(Metrics.globalRegistry)); + } + + public CompletableFuture clear(UUID destinationUuid) { + return CompletableFuture.allOf( + messagesCache.clear(destinationUuid), + messagesDynamoDb.deleteAllMessagesForAccount(destinationUuid)); + } + + public CompletableFuture clear(UUID destinationUuid, long deviceId) { + return CompletableFuture.allOf( + messagesCache.clear(destinationUuid, deviceId), + messagesDynamoDb.deleteAllMessagesForDevice(destinationUuid, deviceId)); + } + + public CompletableFuture> delete(UUID destinationUuid, long destinationDeviceId, UUID guid, + @Nullable Long serverTimestamp) { + return messagesCache.remove(destinationUuid, destinationDeviceId, guid) + .thenComposeAsync(removed -> { + + if (removed.isPresent()) { + cacheHitByGuidMeter.mark(); + return CompletableFuture.completedFuture(removed); + } + + cacheMissByGuidMeter.mark(); + + if (serverTimestamp == null) { + return messagesDynamoDb.deleteMessageByDestinationAndGuid(destinationUuid, guid); + } else { + return messagesDynamoDb.deleteMessage(destinationUuid, destinationDeviceId, guid, serverTimestamp); + } + + }, messageDeletionExecutor); + } + + /** + * @return the number of messages successfully removed from the cache. + */ + public int persistMessages( + final UUID destinationUuid, + final long destinationDeviceId, + final List messages) { + + final List nonEphemeralMessages = messages.stream() + .filter(envelope -> !envelope.getEphemeral()) + .collect(Collectors.toList()); + + messagesDynamoDb.store(nonEphemeralMessages, destinationUuid, destinationDeviceId); + + final List messageGuids = messages.stream().map(message -> UUID.fromString(message.getServerGuid())) + .collect(Collectors.toList()); + int messagesRemovedFromCache = 0; + try { + messagesRemovedFromCache = messagesCache.remove(destinationUuid, destinationDeviceId, messageGuids) + .get(30, TimeUnit.SECONDS).size(); + persistMessageMeter.mark(nonEphemeralMessages.size()); + + } catch (InterruptedException | ExecutionException | TimeoutException e) { + logger.warn("Failed to remove messages from cache", e); + } + return messagesRemovedFromCache; + } + + public void addMessageAvailabilityListener( + final UUID destinationUuid, + final long destinationDeviceId, + final MessageAvailabilityListener listener) { + messagesCache.addMessageAvailabilityListener(destinationUuid, destinationDeviceId, listener); + } + + public void removeMessageAvailabilityListener(final MessageAvailabilityListener listener) { + messagesCache.removeMessageAvailabilityListener(listener); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/OptimisticLockRetryLimitExceededException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/OptimisticLockRetryLimitExceededException.java new file mode 100644 index 000000000..1e608ed09 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/OptimisticLockRetryLimitExceededException.java @@ -0,0 +1,10 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +public class OptimisticLockRetryLimitExceededException extends RuntimeException { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/PhoneNumberIdentifiers.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/PhoneNumberIdentifiers.java new file mode 100644 index 000000000..384b7c71f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/PhoneNumberIdentifiers.java @@ -0,0 +1,119 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.common.annotations.VisibleForTesting; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; +import software.amazon.awssdk.services.dynamodb.model.ReturnValue; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse; + +/** + * Manages a global, persistent mapping of phone numbers to phone number identifiers regardless of whether those + * numbers/identifiers are actually associated with an account. + */ +public class PhoneNumberIdentifiers { + + private final DynamoDbClient dynamoDbClient; + private final String tableName; + + @VisibleForTesting + static final String KEY_E164 = "P"; + @VisibleForTesting + static final String INDEX_NAME = "pni_to_p"; + @VisibleForTesting + static final String ATTR_PHONE_NUMBER_IDENTIFIER = "PNI"; + + private static final Timer GET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "get")); + private static final Timer SET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "set")); + + public PhoneNumberIdentifiers(final DynamoDbClient dynamoDbClient, final String tableName) { + this.dynamoDbClient = dynamoDbClient; + this.tableName = tableName; + } + + /** + * Returns the phone number identifier (PNI) associated with the given phone number. + * + * @param phoneNumber the phone number for which to retrieve a phone number identifier + * @return the phone number identifier associated with the given phone number + */ + public UUID getPhoneNumberIdentifier(final String phoneNumber) { + final GetItemResponse response = GET_PNI_TIMER.record(() -> dynamoDbClient.getItem(GetItemRequest.builder() + .tableName(tableName) + .key(Map.of(KEY_E164, AttributeValues.fromString(phoneNumber))) + .projectionExpression(ATTR_PHONE_NUMBER_IDENTIFIER) + .build())); + + final UUID phoneNumberIdentifier; + + if (response.hasItem()) { + phoneNumberIdentifier = AttributeValues.getUUID(response.item(), ATTR_PHONE_NUMBER_IDENTIFIER, null); + } else { + phoneNumberIdentifier = generatePhoneNumberIdentifierIfNotExists(phoneNumber); + } + + if (phoneNumberIdentifier == null) { + throw new RuntimeException("Could not retrieve phone number identifier from stored item"); + } + + return phoneNumberIdentifier; + } + + public Optional getPhoneNumber(final UUID phoneNumberIdentifier) { + final QueryResponse response = dynamoDbClient.query(QueryRequest.builder() + .tableName(tableName) + .indexName(INDEX_NAME) + .keyConditionExpression("#pni = :pni") + .projectionExpression("#phone_number") + .expressionAttributeNames(Map.of( + "#phone_number", KEY_E164, + "#pni", ATTR_PHONE_NUMBER_IDENTIFIER + )) + .expressionAttributeValues(Map.of( + ":pni", AttributeValues.fromUUID(phoneNumberIdentifier) + )) + .build()); + + if (response.count() == 0) { + return Optional.empty(); + } + + if (response.count() > 1) { + throw new RuntimeException( + "Impossible result: more than one phone number returned for PNI: " + phoneNumberIdentifier); + } + + return Optional.ofNullable(response.items().get(0).get(KEY_E164).s()); + } + + + @VisibleForTesting + UUID generatePhoneNumberIdentifierIfNotExists(final String phoneNumber) { + final UpdateItemResponse response = SET_PNI_TIMER.record(() -> dynamoDbClient.updateItem(UpdateItemRequest.builder() + .tableName(tableName) + .key(Map.of(KEY_E164, AttributeValues.fromString(phoneNumber))) + .updateExpression("SET #pni = if_not_exists(#pni, :pni)") + .expressionAttributeNames(Map.of("#pni", ATTR_PHONE_NUMBER_IDENTIFIER)) + .expressionAttributeValues(Map.of(":pni", AttributeValues.fromUUID(UUID.randomUUID()))) + .returnValues(ReturnValue.ALL_NEW) + .build())); + + return AttributeValues.getUUID(response.attributes(), ATTR_PHONE_NUMBER_IDENTIFIER, null); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/Profiles.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/Profiles.java new file mode 100644 index 000000000..c82d62adf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/Profiles.java @@ -0,0 +1,274 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import com.google.common.annotations.VisibleForTesting; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.whispersystems.textsecuregcm.util.AsyncTimerUtil; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import org.whispersystems.textsecuregcm.util.Util; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; + +public class Profiles { + + private final DynamoDbClient dynamoDbClient; + private final DynamoDbAsyncClient dynamoDbAsyncClient; + private final String tableName; + + // UUID of the account that owns this profile; byte array + @VisibleForTesting + static final String KEY_ACCOUNT_UUID = "U"; + + // Version of this profile; string + @VisibleForTesting + static final String ATTR_VERSION = "V"; + + // User's name; byte array + private static final String ATTR_NAME = "N"; + + // Avatar path/filename; string + private static final String ATTR_AVATAR = "A"; + + // Bio/about text; byte array + private static final String ATTR_ABOUT = "B"; + + // Bio/about emoji; byte array + private static final String ATTR_EMOJI = "E"; + + // Payment address; byte array + private static final String ATTR_PAYMENT_ADDRESS = "P"; + + // Commitment; byte array + private static final String ATTR_COMMITMENT = "C"; + + private static final Map UPDATE_EXPRESSION_ATTRIBUTE_NAMES = Map.of( + "#commitment", ATTR_COMMITMENT, + "#name", ATTR_NAME, + "#avatar", ATTR_AVATAR, + "#about", ATTR_ABOUT, + "#aboutEmoji", ATTR_EMOJI, + "#paymentAddress", ATTR_PAYMENT_ADDRESS); + + private static final Timer SET_PROFILES_TIMER = Metrics.timer(name(Profiles.class, "set")); + private static final Timer GET_PROFILE_TIMER = Metrics.timer(name(Profiles.class, "get")); + private static final Timer DELETE_PROFILES_TIMER = Metrics.timer(name(Profiles.class, "delete")); + private static final String PARSE_BYTE_ARRAY_COUNTER_NAME = name(Profiles.class, "parseByteArray"); + + private static final int MAX_CONCURRENCY = 32; + + public Profiles(final DynamoDbClient dynamoDbClient, + final DynamoDbAsyncClient dynamoDbAsyncClient, + final String tableName) { + + this.dynamoDbClient = dynamoDbClient; + this.dynamoDbAsyncClient = dynamoDbAsyncClient; + this.tableName = tableName; + } + + public void set(final UUID uuid, final VersionedProfile profile) { + SET_PROFILES_TIMER.record(() -> { + dynamoDbClient.updateItem(UpdateItemRequest.builder() + .tableName(tableName) + .key(buildPrimaryKey(uuid, profile.version())) + .updateExpression(buildUpdateExpression(profile)) + .expressionAttributeNames(UPDATE_EXPRESSION_ATTRIBUTE_NAMES) + .expressionAttributeValues(buildUpdateExpressionAttributeValues(profile)) + .build()); + }); + } + + public CompletableFuture setAsync(final UUID uuid, final VersionedProfile profile) { + return AsyncTimerUtil.record(SET_PROFILES_TIMER, () -> dynamoDbAsyncClient.updateItem(UpdateItemRequest.builder() + .tableName(tableName) + .key(buildPrimaryKey(uuid, profile.version())) + .updateExpression(buildUpdateExpression(profile)) + .expressionAttributeNames(UPDATE_EXPRESSION_ATTRIBUTE_NAMES) + .expressionAttributeValues(buildUpdateExpressionAttributeValues(profile)) + .build() + ).thenRun(Util.NOOP) + ).toCompletableFuture(); + } + + private static Map buildPrimaryKey(final UUID uuid, final String version) { + return Map.of( + KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid), + ATTR_VERSION, AttributeValues.fromString(version)); + } + + @VisibleForTesting + static String buildUpdateExpression(final VersionedProfile profile) { + final List updatedAttributes = new ArrayList<>(5); + final List deletedAttributes = new ArrayList<>(5); + + if (profile.name() != null) { + updatedAttributes.add("name"); + } else { + deletedAttributes.add("name"); + } + + if (StringUtils.isNotBlank(profile.avatar())) { + updatedAttributes.add("avatar"); + } else { + deletedAttributes.add("avatar"); + } + + if (profile.about() != null) { + updatedAttributes.add("about"); + } else { + deletedAttributes.add("about"); + } + + if (profile.aboutEmoji() != null) { + updatedAttributes.add("aboutEmoji"); + } else { + deletedAttributes.add("aboutEmoji"); + } + + if (profile.paymentAddress() != null) { + updatedAttributes.add("paymentAddress"); + } else { + deletedAttributes.add("paymentAddress"); + } + + final StringBuilder updateExpressionBuilder = new StringBuilder( + "SET #commitment = if_not_exists(#commitment, :commitment)"); + + if (!updatedAttributes.isEmpty()) { + updatedAttributes.forEach(token -> updateExpressionBuilder + .append(", #") + .append(token) + .append(" = :") + .append(token)); + } + + if (!deletedAttributes.isEmpty()) { + updateExpressionBuilder.append(" REMOVE "); + updateExpressionBuilder.append(deletedAttributes.stream() + .map(token -> "#" + token) + .collect(Collectors.joining(", "))); + } + + return updateExpressionBuilder.toString(); + } + + @VisibleForTesting + static Map buildUpdateExpressionAttributeValues(final VersionedProfile profile) { + final Map expressionValues = new HashMap<>(); + + expressionValues.put(":commitment", AttributeValues.fromByteArray(profile.commitment())); + + if (profile.name() != null) { + expressionValues.put(":name", AttributeValues.fromByteArray(profile.name())); + } + + if (StringUtils.isNotBlank(profile.avatar())) { + expressionValues.put(":avatar", AttributeValues.fromString(profile.avatar())); + } + + if (profile.about() != null) { + expressionValues.put(":about", AttributeValues.fromByteArray(profile.about())); + } + + if (profile.aboutEmoji() != null) { + expressionValues.put(":aboutEmoji", AttributeValues.fromByteArray(profile.aboutEmoji())); + } + + if (profile.paymentAddress() != null) { + expressionValues.put(":paymentAddress", AttributeValues.fromByteArray(profile.paymentAddress())); + } + + return expressionValues; + } + + public Optional get(final UUID uuid, final String version) { + return GET_PROFILE_TIMER.record(() -> { + final GetItemResponse response = dynamoDbClient.getItem(GetItemRequest.builder() + .tableName(tableName) + .key(buildPrimaryKey(uuid, version)) + .consistentRead(true) + .build()); + + return response.hasItem() ? Optional.of(fromItem(response.item())) : Optional.empty(); + }); + } + + public CompletableFuture> getAsync(final UUID uuid, final String version) { + return AsyncTimerUtil.record(GET_PROFILE_TIMER, () -> dynamoDbAsyncClient.getItem(GetItemRequest.builder() + .tableName(tableName) + .key(buildPrimaryKey(uuid, version)) + .consistentRead(true) + .build()) + .thenApply(response -> + response.hasItem() ? Optional.of(fromItem(response.item())) : Optional.empty()) + ).toCompletableFuture(); + } + + private static VersionedProfile fromItem(final Map item) { + return new VersionedProfile( + AttributeValues.getString(item, ATTR_VERSION, null), + getBytes(item, ATTR_NAME), + AttributeValues.getString(item, ATTR_AVATAR, null), + getBytes(item, ATTR_EMOJI), + getBytes(item, ATTR_ABOUT), + getBytes(item, ATTR_PAYMENT_ADDRESS), + AttributeValues.getByteArray(item, ATTR_COMMITMENT, null)); + } + + private static byte[] getBytes(final Map item, final String attributeName) { + final AttributeValue attributeValue = item.get(attributeName); + + if (attributeValue == null) { + return null; + } + return AttributeValues.extractByteArray(attributeValue, PARSE_BYTE_ARRAY_COUNTER_NAME); + } + + public CompletableFuture deleteAll(final UUID uuid) { + final Timer.Sample sample = Timer.start(); + + final AttributeValue uuidAttributeValue = AttributeValues.fromUUID(uuid); + + return Flux.from(dynamoDbAsyncClient.queryPaginator(QueryRequest.builder() + .tableName(tableName) + .keyConditionExpression("#uuid = :uuid") + .expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID)) + .expressionAttributeValues(Map.of(":uuid", uuidAttributeValue)) + .projectionExpression(ATTR_VERSION) + .consistentRead(true) + .build()) + .items()) + .flatMap(item -> Mono.fromFuture(() -> dynamoDbAsyncClient.deleteItem(DeleteItemRequest.builder() + .tableName(tableName) + .key(Map.of( + KEY_ACCOUNT_UUID, uuidAttributeValue, + ATTR_VERSION, item.get(ATTR_VERSION))) + .build())), MAX_CONCURRENCY) + .doOnComplete(() -> sample.stop(DELETE_PROFILES_TIMER)) + .then() + .toFuture(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ProfilesManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ProfilesManager.java new file mode 100644 index 000000000..f3464d237 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ProfilesManager.java @@ -0,0 +1,143 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.lettuce.core.RedisException; +import java.io.IOException; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.Util; +import javax.annotation.Nullable; + +public class ProfilesManager { + + private final Logger logger = LoggerFactory.getLogger(ProfilesManager.class); + + private static final String CACHE_PREFIX = "profiles::"; + + private final Profiles profiles; + private final FaultTolerantRedisCluster cacheCluster; + private final ObjectMapper mapper; + + + public ProfilesManager(final Profiles profiles, + final FaultTolerantRedisCluster cacheCluster) { + this.profiles = profiles; + this.cacheCluster = cacheCluster; + this.mapper = SystemMapper.jsonMapper(); + } + + public void set(UUID uuid, VersionedProfile versionedProfile) { + redisSet(uuid, versionedProfile); + profiles.set(uuid, versionedProfile); + } + + public CompletableFuture setAsync(UUID uuid, VersionedProfile versionedProfile) { + return profiles.setAsync(uuid, versionedProfile) + .thenCompose(ignored -> redisSetAsync(uuid, versionedProfile)); + } + + public CompletableFuture deleteAll(UUID uuid) { + return CompletableFuture.allOf(redisDelete(uuid), profiles.deleteAll(uuid)); + } + + public Optional get(UUID uuid, String version) { + Optional profile = redisGet(uuid, version); + + if (profile.isEmpty()) { + profile = profiles.get(uuid, version); + profile.ifPresent(versionedProfile -> redisSet(uuid, versionedProfile)); + } + + return profile; + } + + public CompletableFuture> getAsync(UUID uuid, String version) { + return redisGetAsync(uuid, version) + .thenCompose(maybeVersionedProfile -> maybeVersionedProfile + .map(versionedProfile -> CompletableFuture.completedFuture(maybeVersionedProfile)) + .orElseGet(() -> profiles.getAsync(uuid, version) + .thenCompose(maybeVersionedProfileFromDynamo -> maybeVersionedProfileFromDynamo + .map(profile -> redisSetAsync(uuid, profile).thenApply(ignored -> maybeVersionedProfileFromDynamo)) + .orElseGet(() -> CompletableFuture.completedFuture(maybeVersionedProfileFromDynamo))))); + } + + private void redisSet(UUID uuid, VersionedProfile profile) { + try { + final String profileJson = mapper.writeValueAsString(profile); + + cacheCluster.useCluster(connection -> connection.sync().hset(getCacheKey(uuid), profile.version(), profileJson)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException(e); + } + } + + private CompletableFuture redisSetAsync(UUID uuid, VersionedProfile profile) { + final String profileJson; + + try { + profileJson = mapper.writeValueAsString(profile); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException(e); + } + + return cacheCluster.withCluster(connection -> + connection.async().hset(getCacheKey(uuid), profile.version(), profileJson)) + .thenRun(Util.NOOP) + .toCompletableFuture(); + } + + private Optional redisGet(UUID uuid, String version) { + try { + @Nullable final String json = cacheCluster.withCluster(connection -> connection.sync().hget(getCacheKey(uuid), version)); + + return parseProfileJson(json); + } catch (RedisException e) { + logger.warn("Redis exception", e); + return Optional.empty(); + } + } + + private CompletableFuture> redisGetAsync(UUID uuid, String version) { + return cacheCluster.withCluster(connection -> + connection.async().hget(getCacheKey(uuid), version)) + .thenApply(this::parseProfileJson) + .exceptionally(throwable -> { + logger.warn("Failed to read versioned profile from Redis", throwable); + return Optional.empty(); + }) + .toCompletableFuture(); + } + + private Optional parseProfileJson(@Nullable final String maybeJson) { + try { + if (maybeJson != null) { + return Optional.of(mapper.readValue(maybeJson, VersionedProfile.class)); + } + return Optional.empty(); + } catch (final IOException e) { + logger.warn("Error deserializing value...", e); + return Optional.empty(); + } + } + + private CompletableFuture redisDelete(UUID uuid) { + return cacheCluster.withCluster(connection -> connection.async().del(getCacheKey(uuid))) + .toCompletableFuture() + .thenRun(Util.NOOP); + } + + private String getCacheKey(UUID uuid) { + return CACHE_PREFIX + uuid.toString(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/PubSubAddress.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/PubSubAddress.java new file mode 100644 index 000000000..f63ea86ff --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/PubSubAddress.java @@ -0,0 +1,11 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +public interface PubSubAddress { + + String serialize(); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/PushChallengeDynamoDb.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/PushChallengeDynamoDb.java new file mode 100644 index 000000000..2ad0600d6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/PushChallengeDynamoDb.java @@ -0,0 +1,101 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.google.common.annotations.VisibleForTesting; +import java.time.Clock; +import java.time.Duration; +import java.util.Map; +import java.util.UUID; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; + +/** + * Stores push challenge tokens. Users may have at most one outstanding push challenge token at a time. + */ +public class PushChallengeDynamoDb extends AbstractDynamoDbStore { + + private final String tableName; + private final Clock clock; + + static final String KEY_ACCOUNT_UUID = "U"; + static final String ATTR_CHALLENGE_TOKEN = "C"; + static final String ATTR_TTL = "T"; + + private static final Map UUID_NAME_MAP = Map.of("#uuid", KEY_ACCOUNT_UUID); + private static final Map CHALLENGE_TOKEN_NAME_MAP = Map.of("#challenge", ATTR_CHALLENGE_TOKEN, "#ttl", + ATTR_TTL); + + public PushChallengeDynamoDb(final DynamoDbClient dynamoDB, final String tableName) { + this(dynamoDB, tableName, Clock.systemUTC()); + } + + @VisibleForTesting + PushChallengeDynamoDb(final DynamoDbClient dynamoDB, final String tableName, final Clock clock) { + super(dynamoDB); + + this.tableName = tableName; + this.clock = clock; + } + + /** + * Stores a push challenge token for the given user if and only if the user doesn't already have a token stored. The + * existence check is strongly-consistent. + * + * @param accountUuid the UUID of the account for which to store a push challenge token + * @param challengeToken the challenge token itself + * @param ttl the time after which the token is no longer valid + * @return {@code true} if a new token was stored of {@code false} if another token already exists for the given + * account + */ + public boolean add(final UUID accountUuid, final byte[] challengeToken, final Duration ttl) { + try { + db().putItem(PutItemRequest.builder() + .tableName(tableName) + .item(Map.of( + KEY_ACCOUNT_UUID, AttributeValues.fromUUID(accountUuid), + ATTR_CHALLENGE_TOKEN, AttributeValues.fromByteArray(challengeToken), + ATTR_TTL, AttributeValues.fromLong(getExpirationTimestamp(ttl)))) + .conditionExpression("attribute_not_exists(#uuid)") + .expressionAttributeNames(UUID_NAME_MAP) + .build()); + return true; + } catch (final ConditionalCheckFailedException e) { + return false; + } + } + + long getExpirationTimestamp(final Duration ttl) { + return clock.instant().plus(ttl).getEpochSecond(); + } + + /** + * Clears a push challenge token for the given user if and only if the given challenge token matches the stored token. + * The token comparison is a strongly-consistent operation. + * + * @param accountUuid the account for which to remove a stored token + * @param challengeToken the token to remove + * @return {@code true} if the given token matched the stored token for the given user or {@code false} otherwise + */ + public boolean remove(final UUID accountUuid, final byte[] challengeToken) { + try { + db().deleteItem(DeleteItemRequest.builder() + .tableName(tableName) + .key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(accountUuid))) + .conditionExpression("#challenge = :challenge AND #ttl >= :currentTime") + .expressionAttributeNames(CHALLENGE_TOKEN_NAME_MAP) + .expressionAttributeValues(Map.of(":challenge", AttributeValues.fromByteArray(challengeToken), + ":currentTime", AttributeValues.fromLong(clock.instant().getEpochSecond()))) + .build()); + return true; + } catch (final ConditionalCheckFailedException e) { + return false; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/PushFeedbackProcessor.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/PushFeedbackProcessor.java new file mode 100644 index 000000000..4e4173f08 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/PushFeedbackProcessor.java @@ -0,0 +1,131 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.SharedMetricRegistries; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.util.Constants; +import org.whispersystems.textsecuregcm.util.Util; + +public class PushFeedbackProcessor extends AccountDatabaseCrawlerListener { + + private static final Logger log = LoggerFactory.getLogger(PushFeedbackProcessor.class); + + private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); + private final Meter expired = metricRegistry.meter(name(getClass(), "unregistered", "expired")); + private final Meter recovered = metricRegistry.meter(name(getClass(), "unregistered", "recovered")); + + private static final Counter UPDATED_ACCOUNT_COUNTER = Metrics.counter( + MetricsUtil.name(PushFeedbackProcessor.class, "updatedAccounts")); + + + private final AccountsManager accountsManager; + private final ExecutorService updateExecutor; + + public PushFeedbackProcessor(AccountsManager accountsManager, ExecutorService updateExecutor) { + this.accountsManager = accountsManager; + this.updateExecutor = updateExecutor; + } + + @Override + public void onCrawlStart() {} + + @Override + public void onCrawlEnd() { + } + + @Override + protected void onCrawlChunk(Optional fromUuid, List chunkAccounts) { + + final List> updateFutures = chunkAccounts.stream() + .filter(account -> { + boolean update = false; + + for (Device device : account.getDevices()) { + if (deviceNeedsUpdate(device)) { + if (deviceExpired(device)) { + if (device.isEnabled()) { + expired.mark(); + update = true; + } + } else { + recovered.mark(); + update = true; + } + } + } + + return update; + }) + .map(account -> CompletableFuture.runAsync(() -> { + // fetch a new version, since the chunk is shared and implicitly read-only + accountsManager.getByAccountIdentifier(account.getUuid()).ifPresent(accountToUpdate -> { + accountsManager.update(accountToUpdate, a -> { + for (Device device : a.getDevices()) { + if (deviceNeedsUpdate(device)) { + if (deviceExpired(device)) { + if (!Util.isEmpty(device.getApnId())) { + if (device.getId() == 1) { + device.setUserAgent("OWI"); + } else { + device.setUserAgent("OWP"); + } + } else if (!Util.isEmpty(device.getGcmId())) { + device.setUserAgent("OWA"); + } + device.setGcmId(null); + device.setApnId(null); + device.setVoipApnId(null); + device.setFetchesMessages(false); + } else { + device.setUninstalledFeedbackTimestamp(0); + } + } + } + }); + }); + }, updateExecutor) + .whenComplete((ignored, throwable) -> { + if (throwable != null) { + log.warn("Failed to update account {}", account.getUuid(), throwable); + } else { + UPDATED_ACCOUNT_COUNTER.increment(); + } + })) + .toList(); + + try { + CompletableFuture.allOf(updateFutures.toArray(new CompletableFuture[0])) + .orTimeout(10, TimeUnit.MINUTES) + .join(); + } catch (final Exception e) { + log.debug("Failed to update one or more accounts in chunk", e); + } + } + + private boolean deviceNeedsUpdate(final Device device) { + return device.getUninstalledFeedbackTimestamp() != 0 && + device.getUninstalledFeedbackTimestamp() + TimeUnit.DAYS.toMillis(2) <= Util.todayInMillis(); + } + + private boolean deviceExpired(final Device device) { + return device.getLastSeen() + TimeUnit.DAYS.toMillis(2) <= Util.todayInMillis(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RedeemedReceiptsManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RedeemedReceiptsManager.java new file mode 100644 index 000000000..b59ee48d6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RedeemedReceiptsManager.java @@ -0,0 +1,99 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; +import org.signal.libsignal.zkgroup.receipts.ReceiptSerial; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import org.whispersystems.textsecuregcm.util.UUIDUtil; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ReturnValue; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; + +public class RedeemedReceiptsManager { + + public static final String KEY_SERIAL = "S"; + public static final String KEY_TTL = "E"; + public static final String KEY_RECEIPT_EXPIRATION = "G"; + public static final String KEY_RECEIPT_LEVEL = "L"; + public static final String KEY_ACCOUNT_UUID = "U"; + public static final String KEY_REDEMPTION_TIME = "R"; + + private final Clock clock; + private final String table; + private final DynamoDbAsyncClient client; + private final Duration expirationTime; + + public RedeemedReceiptsManager( + @Nonnull final Clock clock, + @Nonnull final String table, + @Nonnull final DynamoDbAsyncClient client, + @Nonnull final Duration expirationTime) { + this.clock = Objects.requireNonNull(clock); + this.table = Objects.requireNonNull(table); + this.client = Objects.requireNonNull(client); + this.expirationTime = Objects.requireNonNull(expirationTime); + } + + /** + * Returns true either if it's able to insert a new redeemed receipt entry with the {@code receiptExpiration}, {@code + * receiptLevel}, and {@code accountUuid} provided or if an existing entry already exists with the same values thereby + * allowing idempotent request processing. + */ + public CompletableFuture put( + @Nonnull final ReceiptSerial receiptSerial, + final long receiptExpiration, + final long receiptLevel, + @Nonnull final UUID accountUuid) { + + // fail early if given bad inputs + Objects.requireNonNull(receiptSerial); + Objects.requireNonNull(accountUuid); + + final Instant now = clock.instant(); + final Instant rowExpiration = now.plus(expirationTime); + final AttributeValue serialAttributeValue = AttributeValues.b(receiptSerial.serialize()); + + final UpdateItemRequest updateItemRequest = UpdateItemRequest.builder() + .tableName(table) + .key(Map.of(KEY_SERIAL, serialAttributeValue)) + .returnValues(ReturnValue.ALL_NEW) + .updateExpression("SET #ttl = if_not_exists(#ttl, :ttl), " + + "#receipt_expiration = if_not_exists(#receipt_expiration, :receipt_expiration), " + + "#receipt_level = if_not_exists(#receipt_level, :receipt_level), " + + "#account_uuid = if_not_exists(#account_uuid, :account_uuid), " + + "#redemption_time = if_not_exists(#redemption_time, :redemption_time)") + .expressionAttributeNames(Map.of( + "#ttl", KEY_TTL, + "#receipt_expiration", KEY_RECEIPT_EXPIRATION, + "#receipt_level", KEY_RECEIPT_LEVEL, + "#account_uuid", KEY_ACCOUNT_UUID, + "#redemption_time", KEY_REDEMPTION_TIME)) + .expressionAttributeValues(Map.of( + ":ttl", AttributeValues.n(rowExpiration.getEpochSecond()), + ":receipt_expiration", AttributeValues.n(receiptExpiration), + ":receipt_level", AttributeValues.n(receiptLevel), + ":account_uuid", AttributeValues.b(accountUuid), + ":redemption_time", AttributeValues.n(now.getEpochSecond()))) + .build(); + return client.updateItem(updateItemRequest).thenApply(updateItemResponse -> { + final Map attributes = updateItemResponse.attributes(); + final long ddbReceiptExpiration = Long.parseLong(attributes.get(KEY_RECEIPT_EXPIRATION).n()); + final long ddbReceiptLevel = Long.parseLong(attributes.get(KEY_RECEIPT_LEVEL).n()); + final UUID ddbAccountUuid = UUIDUtil.fromByteBuffer(attributes.get(KEY_ACCOUNT_UUID).b().asByteBuffer()); + return ddbReceiptExpiration == receiptExpiration && ddbReceiptLevel == receiptLevel && + Objects.equals(ddbAccountUuid, accountUuid); + }); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RefreshingAccountAndDeviceNotFoundException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RefreshingAccountAndDeviceNotFoundException.java new file mode 100644 index 000000000..0ea57a0ad --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RefreshingAccountAndDeviceNotFoundException.java @@ -0,0 +1,14 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +public class RefreshingAccountAndDeviceNotFoundException extends RuntimeException { + + public RefreshingAccountAndDeviceNotFoundException(final String message) { + super(message); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RefreshingAccountAndDeviceSupplier.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RefreshingAccountAndDeviceSupplier.java new file mode 100644 index 000000000..1c12e1177 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RefreshingAccountAndDeviceSupplier.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import java.util.function.Supplier; +import org.whispersystems.textsecuregcm.util.Pair; + +public class RefreshingAccountAndDeviceSupplier implements Supplier> { + + private Account account; + private Device device; + private final AccountsManager accountsManager; + + public RefreshingAccountAndDeviceSupplier(Account account, long deviceId, AccountsManager accountsManager) { + this.account = account; + this.device = account.getDevice(deviceId) + .orElseThrow(() -> new RefreshingAccountAndDeviceNotFoundException("Could not find device")); + this.accountsManager = accountsManager; + } + + @Override + public Pair get() { + if (account.isStale()) { + account = accountsManager.getByAccountIdentifier(account.getUuid()) + .orElseThrow(() -> new RuntimeException("Could not find account")); + device = account.getDevice(device.getId()) + .orElseThrow(() -> new RefreshingAccountAndDeviceNotFoundException("Could not find device")); + } + + return new Pair<>(account, device); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RegistrationRecoveryPasswords.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RegistrationRecoveryPasswords.java new file mode 100644 index 000000000..3d51a4e17 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RegistrationRecoveryPasswords.java @@ -0,0 +1,101 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static java.util.Objects.requireNonNull; + +import java.time.Clock; +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import org.whispersystems.textsecuregcm.util.Util; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; + +public class RegistrationRecoveryPasswords extends AbstractDynamoDbStore { + + static final String KEY_E164 = "P"; + static final String ATTR_EXP = "E"; + static final String ATTR_SALT = "S"; + static final String ATTR_HASH = "H"; + + private final String tableName; + + private final Duration expiration; + + private final DynamoDbAsyncClient asyncClient; + + private final Clock clock; + + public RegistrationRecoveryPasswords( + final String tableName, + final Duration expiration, + final DynamoDbClient dynamoDbClient, + final DynamoDbAsyncClient asyncClient) { + this(tableName, expiration, dynamoDbClient, asyncClient, Clock.systemUTC()); + } + + RegistrationRecoveryPasswords( + final String tableName, + final Duration expiration, + final DynamoDbClient dynamoDbClient, + final DynamoDbAsyncClient asyncClient, + final Clock clock) { + super(dynamoDbClient); + this.tableName = requireNonNull(tableName); + this.expiration = requireNonNull(expiration); + this.asyncClient = requireNonNull(asyncClient); + this.clock = requireNonNull(clock); + } + + public CompletableFuture> lookup(final String number) { + return asyncClient.getItem(GetItemRequest.builder() + .tableName(tableName) + .key(Map.of(KEY_E164, AttributeValues.fromString(number))) + .consistentRead(true) + .build()) + .thenApply(getItemResponse -> { + final Map item = getItemResponse.item(); + if (item == null || !item.containsKey(ATTR_SALT) || !item.containsKey(ATTR_HASH)) { + return Optional.empty(); + } + final String salt = item.get(ATTR_SALT).s(); + final String hash = item.get(ATTR_HASH).s(); + return Optional.of(new SaltedTokenHash(hash, salt)); + }); + } + + public CompletableFuture addOrReplace(final String number, final SaltedTokenHash data) { + return asyncClient.putItem(PutItemRequest.builder() + .tableName(tableName) + .item(Map.of( + KEY_E164, AttributeValues.fromString(number), + ATTR_EXP, AttributeValues.fromLong(expirationSeconds()), + ATTR_SALT, AttributeValues.fromString(data.salt()), + ATTR_HASH, AttributeValues.fromString(data.hash()))) + .build()) + .thenRun(Util.NOOP); + } + + public CompletableFuture removeEntry(final String number) { + return asyncClient.deleteItem(DeleteItemRequest.builder() + .tableName(tableName) + .key(Map.of(KEY_E164, AttributeValues.fromString(number))) + .build()) + .thenRun(Util.NOOP); + } + + private long expirationSeconds() { + return clock.instant().plus(expiration).getEpochSecond(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RegistrationRecoveryPasswordsManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RegistrationRecoveryPasswordsManager.java new file mode 100644 index 000000000..40c988219 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RegistrationRecoveryPasswordsManager.java @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static java.util.Objects.requireNonNull; + +import java.lang.invoke.MethodHandles; +import java.util.HexFormat; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; + +public class RegistrationRecoveryPasswordsManager { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final RegistrationRecoveryPasswords registrationRecoveryPasswords; + + + public RegistrationRecoveryPasswordsManager(final RegistrationRecoveryPasswords registrationRecoveryPasswords) { + this.registrationRecoveryPasswords = requireNonNull(registrationRecoveryPasswords); + } + + public CompletableFuture verify(final String number, final byte[] password) { + return registrationRecoveryPasswords.lookup(number) + .thenApply(maybeHash -> maybeHash.filter(hash -> hash.verify(bytesToString(password)))) + .whenComplete((result, error) -> { + if (error != null) { + logger.warn("Failed to lookup Registration Recovery Password", error); + } + }) + .thenApply(Optional::isPresent); + } + + public CompletableFuture storeForCurrentNumber(final String number, final byte[] password) { + final String token = bytesToString(password); + final SaltedTokenHash tokenHash = SaltedTokenHash.generateFor(token); + return registrationRecoveryPasswords.addOrReplace(number, tokenHash) + .whenComplete((result, error) -> { + if (error != null) { + logger.warn("Failed to store Registration Recovery Password", error); + } + }); + } + + public CompletableFuture removeForNumber(final String number) { + // remove is a "fire-and-forget" operation, + // there is no action to be taken on its completion + return registrationRecoveryPasswords.removeEntry(number) + .whenComplete((ignored, error) -> { + if (error instanceof ResourceNotFoundException) { + // These will naturally happen if a recovery password is already deleted. Since we can remove + // the recovery password through many flows, we avoid creating log messages for these exceptions + } else if (error != null) { + logger.warn("Failed to remove Registration Recovery Password", error); + } + }); + } + + private static String bytesToString(final byte[] bytes) { + return HexFormat.of().formatHex(bytes); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RemoteConfig.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RemoteConfig.java new file mode 100644 index 000000000..c1215df12 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RemoteConfig.java @@ -0,0 +1,77 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class RemoteConfig { + + @JsonProperty + @Pattern(regexp = "[A-Za-z0-9\\.]+") + private String name; + + @JsonProperty + @NotNull + @Min(0) + @Max(100) + private int percentage; + + @JsonProperty + @NotNull + private Set uuids = new HashSet<>(); + + @JsonProperty + private String defaultValue; + + @JsonProperty + private String value; + + @JsonProperty + private String hashKey; + + public RemoteConfig() {} + + public RemoteConfig(String name, int percentage, Set uuids, String defaultValue, String value, String hashKey) { + this.name = name; + this.percentage = percentage; + this.uuids = uuids; + this.defaultValue = defaultValue; + this.value = value; + this.hashKey = hashKey; + } + + public int getPercentage() { + return percentage; + } + + public String getName() { + return name; + } + + public Set getUuids() { + return uuids; + } + + public String getDefaultValue() { + return defaultValue; + } + + public String getValue() { + return value; + } + + public String getHashKey() { + return hashKey; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RemoteConfigs.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RemoteConfigs.java new file mode 100644 index 000000000..8d2141be1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RemoteConfigs.java @@ -0,0 +1,114 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import org.whispersystems.textsecuregcm.util.AttributeValues; +import org.whispersystems.textsecuregcm.util.UUIDUtil; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.ScanRequest; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +public class RemoteConfigs { + + private final DynamoDbClient dynamoDbClient; + private final String tableName; + + // Config name; string + static final String KEY_NAME = "N"; + // Rollout percentage; integer + private static final String ATTR_PERCENTAGE = "P"; + // Enrolled UUIDs (ACIs); list of byte arrays + private static final String ATTR_UUIDS = "U"; + // Default value; string + private static final String ATTR_DEFAULT_VALUE = "D"; + // Value when enrolled; string + private static final String ATTR_VALUE = "V"; + // Hash key; string + private static final String ATTR_HASH_KEY = "H"; + + public RemoteConfigs(final DynamoDbClient dynamoDbClient, final String tableName) { + this.dynamoDbClient = dynamoDbClient; + this.tableName = tableName; + } + + public void set(final RemoteConfig remoteConfig) { + final Map item = new HashMap<>(Map.of( + KEY_NAME, AttributeValues.fromString(remoteConfig.getName()), + ATTR_PERCENTAGE, AttributeValues.fromInt(remoteConfig.getPercentage()))); + + if (remoteConfig.getUuids() != null && !remoteConfig.getUuids().isEmpty()) { + final List uuidByteSets = remoteConfig.getUuids().stream() + .map(UUIDUtil::toByteBuffer) + .map(SdkBytes::fromByteBuffer) + .collect(Collectors.toList()); + + item.put(ATTR_UUIDS, AttributeValue.builder().bs(uuidByteSets).build()); + } + + if (remoteConfig.getDefaultValue() != null) { + item.put(ATTR_DEFAULT_VALUE, AttributeValues.fromString(remoteConfig.getDefaultValue())); + } + + if (remoteConfig.getValue() != null) { + item.put(ATTR_VALUE, AttributeValues.fromString(remoteConfig.getValue())); + } + + if (remoteConfig.getHashKey() != null) { + item.put(ATTR_HASH_KEY, AttributeValues.fromString(remoteConfig.getHashKey())); + } + + dynamoDbClient.putItem(PutItemRequest.builder() + .tableName(tableName) + .item(item) + .build()); + } + + public List getAll() { + return dynamoDbClient.scanPaginator(ScanRequest.builder() + .tableName(tableName) + .consistentRead(true) + .build()) + .items() + .stream() + .map(item -> { + final String name = AttributeValues.getString(item, KEY_NAME, null); + final int percentage = AttributeValues.getInt(item, ATTR_PERCENTAGE, 0); + final String defaultValue = AttributeValues.getString(item, ATTR_DEFAULT_VALUE, null); + final String value = AttributeValues.getString(item, ATTR_VALUE, null); + final String hashKey = AttributeValues.getString(item, ATTR_HASH_KEY, null); + + final Set uuids; + + if (item.containsKey(ATTR_UUIDS)) { + uuids = item.get(ATTR_UUIDS).bs().stream() + .map(sdkBytes -> UUIDUtil.fromByteBuffer(sdkBytes.asByteBuffer())) + .collect(Collectors.toSet()); + } else { + uuids = Collections.emptySet(); + } + + return new RemoteConfig(name, percentage, uuids, defaultValue, value, hashKey); + }) + .collect(Collectors.toList()); + } + + public void delete(final String name) { + dynamoDbClient.deleteItem(DeleteItemRequest.builder() + .tableName(tableName) + .key(Map.of(KEY_NAME, AttributeValues.fromString(name))) + .build()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RemoteConfigsManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RemoteConfigsManager.java new file mode 100644 index 000000000..267cfc275 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RemoteConfigsManager.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.google.common.base.Suppliers; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public class RemoteConfigsManager { + + private final RemoteConfigs remoteConfigs; + + private final Supplier> remoteConfigSupplier; + + public RemoteConfigsManager(RemoteConfigs remoteConfigs) { + this.remoteConfigs = remoteConfigs; + + remoteConfigSupplier = + Suppliers.memoizeWithExpiration(remoteConfigs::getAll, 10, TimeUnit.SECONDS); + } + + public List getAll() { + return remoteConfigSupplier.get(); + } + + public void set(RemoteConfig config) { + remoteConfigs.set(config); + } + + public void delete(String name) { + remoteConfigs.delete(name); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RepeatedUseECSignedPreKeyStore.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RepeatedUseECSignedPreKeyStore.java new file mode 100644 index 000000000..daa870b8c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RepeatedUseECSignedPreKeyStore.java @@ -0,0 +1,73 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import org.whispersystems.textsecuregcm.util.ExceptionUtils; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; + +public class RepeatedUseECSignedPreKeyStore extends RepeatedUseSignedPreKeyStore { + + private final DynamoDbAsyncClient dynamoDbAsyncClient; + private final String tableName; + + public RepeatedUseECSignedPreKeyStore(final DynamoDbAsyncClient dynamoDbAsyncClient, final String tableName) { + super(dynamoDbAsyncClient, tableName); + + this.dynamoDbAsyncClient = dynamoDbAsyncClient; + this.tableName = tableName; + } + + @Override + protected Map getItemFromPreKey(final UUID accountUuid, final long deviceId, final ECSignedPreKey signedPreKey) { + + return Map.of( + KEY_ACCOUNT_UUID, getPartitionKey(accountUuid), + KEY_DEVICE_ID, getSortKey(deviceId), + ATTR_KEY_ID, AttributeValues.fromLong(signedPreKey.keyId()), + ATTR_PUBLIC_KEY, AttributeValues.fromByteArray(signedPreKey.serializedPublicKey()), + ATTR_SIGNATURE, AttributeValues.fromByteArray(signedPreKey.signature())); + } + + @Override + protected ECSignedPreKey getPreKeyFromItem(final Map item) { + try { + return new ECSignedPreKey( + Long.parseLong(item.get(ATTR_KEY_ID).n()), + new ECPublicKey(item.get(ATTR_PUBLIC_KEY).b().asByteArray()), + item.get(ATTR_SIGNATURE).b().asByteArray()); + } catch (final InvalidKeyException e) { + // This should never happen since we're serializing keys directly from `ECPublicKey` instances on the way in + throw new IllegalArgumentException(e); + } + } + + public CompletableFuture storeIfAbsent(final UUID identifier, final long deviceId, final ECSignedPreKey signedPreKey) { + return dynamoDbAsyncClient.putItem(PutItemRequest.builder() + .tableName(tableName) + .item(getItemFromPreKey(identifier, deviceId, signedPreKey)) + .conditionExpression("attribute_not_exists(#public_key)") + .expressionAttributeNames(Map.of("#public_key", ATTR_PUBLIC_KEY)) + .build()) + .thenApply(ignored -> true) + .exceptionally(throwable -> { + if (ExceptionUtils.unwrap(throwable) instanceof ConditionalCheckFailedException) { + return false; + } + + throw ExceptionUtils.wrap(throwable); + }); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RepeatedUseKEMSignedPreKeyStore.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RepeatedUseKEMSignedPreKeyStore.java new file mode 100644 index 000000000..e6720a213 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RepeatedUseKEMSignedPreKeyStore.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.kem.KEMPublicKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import java.util.Map; +import java.util.UUID; + +public class RepeatedUseKEMSignedPreKeyStore extends RepeatedUseSignedPreKeyStore { + + public RepeatedUseKEMSignedPreKeyStore(final DynamoDbAsyncClient dynamoDbAsyncClient, final String tableName) { + super(dynamoDbAsyncClient, tableName); + } + + @Override + protected Map getItemFromPreKey(final UUID accountUuid, final long deviceId, final KEMSignedPreKey signedPreKey) { + + return Map.of( + KEY_ACCOUNT_UUID, getPartitionKey(accountUuid), + KEY_DEVICE_ID, getSortKey(deviceId), + ATTR_KEY_ID, AttributeValues.fromLong(signedPreKey.keyId()), + ATTR_PUBLIC_KEY, AttributeValues.fromByteArray(signedPreKey.serializedPublicKey()), + ATTR_SIGNATURE, AttributeValues.fromByteArray(signedPreKey.signature())); + } + + @Override + protected KEMSignedPreKey getPreKeyFromItem(final Map item) { + try { + return new KEMSignedPreKey( + Long.parseLong(item.get(ATTR_KEY_ID).n()), + new KEMPublicKey(item.get(ATTR_PUBLIC_KEY).b().asByteArray()), + item.get(ATTR_SIGNATURE).b().asByteArray()); + } catch (final InvalidKeyException e) { + // This should never happen since we're serializing keys directly from `KEMPublicKey` instances on the way in + throw new IllegalArgumentException(e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RepeatedUseSignedPreKeyStore.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RepeatedUseSignedPreKeyStore.java new file mode 100644 index 000000000..471aaf9e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/RepeatedUseSignedPreKeyStore.java @@ -0,0 +1,209 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.whispersystems.textsecuregcm.entities.SignedPreKey; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.Put; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem; +import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest; + +/** + * A repeated-use signed pre-key store manages storage for pre-keys that may be used more than once. Generally, these + * are considered "last resort" keys and should only be used when a device's supply of single-use pre-keys has been + * exhausted. + *

    + * Each {@link Account} may have one or more {@link Device devices}. Each "active" (i.e. those that have completed + * provisioning and are capable of sending and receiving messages) must have exactly one "last resort" pre-key. + */ +public abstract class RepeatedUseSignedPreKeyStore> { + + private final DynamoDbAsyncClient dynamoDbAsyncClient; + private final String tableName; + + static final String KEY_ACCOUNT_UUID = "U"; + static final String KEY_DEVICE_ID = "D"; + static final String ATTR_KEY_ID = "I"; + static final String ATTR_PUBLIC_KEY = "P"; + static final String ATTR_SIGNATURE = "S"; + + private final Timer storeSingleKeyTimer = Metrics.timer(MetricsUtil.name(getClass(), "storeSingleKey")); + private final Timer storeKeyBatchTimer = Metrics.timer(MetricsUtil.name(getClass(), "storeKeyBatch")); + private final Timer deleteForDeviceTimer = Metrics.timer(MetricsUtil.name(getClass(), "deleteForDevice")); + private final Timer deleteForAccountTimer = Metrics.timer(MetricsUtil.name(getClass(), "deleteForAccount")); + + private final String findKeyTimerName = MetricsUtil.name(getClass(), "findKey"); + + public RepeatedUseSignedPreKeyStore(final DynamoDbAsyncClient dynamoDbAsyncClient, final String tableName) { + this.dynamoDbAsyncClient = dynamoDbAsyncClient; + this.tableName = tableName; + } + + /** + * Stores a repeated-use pre-key for a specific device, displacing any previously-stored repeated-use pre-key for that + * device. + * + * @param identifier the identifier for the account/identity with which the target device is associated + * @param deviceId the identifier for the device within the given account/identity + * @param signedPreKey the key to store for the target device + * + * @return a future that completes once the key has been stored + */ + public CompletableFuture store(final UUID identifier, final long deviceId, final K signedPreKey) { + final Timer.Sample sample = Timer.start(); + + return dynamoDbAsyncClient.putItem(PutItemRequest.builder() + .tableName(tableName) + .item(getItemFromPreKey(identifier, deviceId, signedPreKey)) + .build()) + .thenRun(() -> sample.stop(storeSingleKeyTimer)); + } + + /** + * Stores repeated-use pre-keys for a collection of devices associated with a single account/identity, displacing any + * previously-stored repeated-use pre-keys for the targeted devices. Note that this method is transactional; either + * all keys will be stored or none will. + * + * @param identifier the identifier for the account/identity with which the target devices are associated + * @param signedPreKeysByDeviceId a map of device identifiers to pre-keys + * + * @return a future that completes once all keys have been stored + */ + public CompletableFuture store(final UUID identifier, final Map signedPreKeysByDeviceId) { + final Timer.Sample sample = Timer.start(); + + return dynamoDbAsyncClient.transactWriteItems(TransactWriteItemsRequest.builder() + .transactItems(signedPreKeysByDeviceId.entrySet().stream() + .map(entry -> { + final long deviceId = entry.getKey(); + final K signedPreKey = entry.getValue(); + + return TransactWriteItem.builder() + .put(Put.builder() + .tableName(tableName) + .item(getItemFromPreKey(identifier, deviceId, signedPreKey)) + .build()) + .build(); + }) + .toList()) + .build()) + .thenRun(() -> sample.stop(storeKeyBatchTimer)); + } + + /** + * Finds a repeated-use pre-key for a specific device. + * + * @param identifier the identifier for the account/identity with which the target device is associated + * @param deviceId the identifier for the device within the given account/identity + * + * @return a future that yields an optional signed pre-key if one is available for the target device or empty if no + * key could be found for the target device + */ + public CompletableFuture> find(final UUID identifier, final long deviceId) { + final Timer.Sample sample = Timer.start(); + + final CompletableFuture> findFuture = dynamoDbAsyncClient.getItem(GetItemRequest.builder() + .tableName(tableName) + .key(getPrimaryKey(identifier, deviceId)) + .consistentRead(true) + .build()) + .thenApply(response -> response.hasItem() ? Optional.of(getPreKeyFromItem(response.item())) : Optional.empty()); + + findFuture.whenComplete((maybeSignedPreKey, throwable) -> + sample.stop(Metrics.timer(findKeyTimerName, + "keyPresent", String.valueOf(maybeSignedPreKey != null && maybeSignedPreKey.isPresent())))); + + return findFuture; + } + + /** + * Clears all repeated-use pre-keys associated with the given account/identity. + * + * @param identifier the identifier for the account/identity for which to clear repeated-use pre-keys + * + * @return a future that completes once repeated-use pre-keys have been cleared from all devices associated with the + * target account/identity + */ + public CompletableFuture delete(final UUID identifier) { + final Timer.Sample sample = Timer.start(); + + return getDeviceIdsWithKeys(identifier) + .map(deviceId -> DeleteItemRequest.builder() + .tableName(tableName) + .key(getPrimaryKey(identifier, deviceId)) + .build()) + .flatMap(deleteItemRequest -> Mono.fromFuture(() -> dynamoDbAsyncClient.deleteItem(deleteItemRequest))) + // Idiom: wait for everything to finish, but discard the results + .reduce(0, (a, b) -> 0) + .toFuture() + .thenRun(() -> sample.stop(deleteForAccountTimer)); + } + + /** + * Removes the repeated-use pre-key associated with a specific device. + * + * @param identifier the identifier for the account/identity with which the target device is associated + * @param deviceId the identifier for the device within the given account/identity + * + * @return a future that completes once the repeated-use pre-key has been removed from the target device + */ + public CompletableFuture delete(final UUID identifier, final long deviceId) { + final Timer.Sample sample = Timer.start(); + + return dynamoDbAsyncClient.deleteItem(DeleteItemRequest.builder() + .tableName(tableName) + .key(getPrimaryKey(identifier, deviceId)) + .build()) + .thenRun(() -> sample.stop(deleteForDeviceTimer)); + } + + public Flux getDeviceIdsWithKeys(final UUID identifier) { + return Flux.from(dynamoDbAsyncClient.queryPaginator(QueryRequest.builder() + .tableName(tableName) + .keyConditionExpression("#uuid = :uuid") + .expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID)) + .expressionAttributeValues(Map.of( + ":uuid", getPartitionKey(identifier))) + .projectionExpression(KEY_DEVICE_ID) + .consistentRead(true) + .build()) + .items()) + .map(item -> Long.parseLong(item.get(KEY_DEVICE_ID).n())); + } + + protected static Map getPrimaryKey(final UUID identifier, final long deviceId) { + return Map.of( + KEY_ACCOUNT_UUID, getPartitionKey(identifier), + KEY_DEVICE_ID, getSortKey(deviceId)); + } + + protected static AttributeValue getPartitionKey(final UUID accountUuid) { + return AttributeValues.fromUUID(accountUuid); + } + + protected static AttributeValue getSortKey(final long deviceId) { + return AttributeValues.fromLong(deviceId); + } + + protected abstract Map getItemFromPreKey(final UUID accountUuid, final long deviceId, final K signedPreKey); + + protected abstract K getPreKeyFromItem(final Map item); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ReportMessageDynamoDb.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ReportMessageDynamoDb.java new file mode 100644 index 000000000..49111f991 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ReportMessageDynamoDb.java @@ -0,0 +1,73 @@ +package org.whispersystems.textsecuregcm.storage; + +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.ReturnValue; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +public class ReportMessageDynamoDb { + + static final String KEY_HASH = "H"; + static final String ATTR_TTL = "E"; + + private final DynamoDbClient db; + private final String tableName; + private final Duration ttl; + + private static final String REMOVED_MESSAGE_COUNTER_NAME = name(ReportMessageDynamoDb.class, "removed"); + private static final Timer REMOVED_MESSAGE_AGE_TIMER = Timer + .builder(name(ReportMessageDynamoDb.class, "removedMessageAge")) + .publishPercentiles(0.5, 0.75, 0.95, 0.99) + .distributionStatisticExpiry(Duration.ofDays(1)) + .register(Metrics.globalRegistry); + + public ReportMessageDynamoDb(final DynamoDbClient dynamoDB, final String tableName, final Duration ttl) { + this.db = dynamoDB; + this.tableName = tableName; + this.ttl = ttl; + } + + public void store(byte[] hash) { + db.putItem(PutItemRequest.builder() + .tableName(tableName) + .item(Map.of( + KEY_HASH, AttributeValues.fromByteArray(hash), + ATTR_TTL, AttributeValues.fromLong(Instant.now().plus(ttl).getEpochSecond()) + )) + .build()); + } + + public boolean remove(byte[] hash) { + final DeleteItemResponse deleteItemResponse = db.deleteItem(DeleteItemRequest.builder() + .tableName(tableName) + .key(Map.of(KEY_HASH, AttributeValues.fromByteArray(hash))) + .returnValues(ReturnValue.ALL_OLD) + .build()); + + final boolean found = !deleteItemResponse.attributes().isEmpty(); + + if (found) { + if (deleteItemResponse.attributes().containsKey(ATTR_TTL)) { + final Instant expiration = + Instant.ofEpochSecond(Long.parseLong(deleteItemResponse.attributes().get(ATTR_TTL).n())); + + final Duration approximateAge = ttl.minus(Duration.between(Instant.now(), expiration)); + + REMOVED_MESSAGE_AGE_TIMER.record(approximateAge); + } + } + + Metrics.counter(REMOVED_MESSAGE_COUNTER_NAME, "found", String.valueOf(found)).increment(); + + return found; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ReportMessageManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ReportMessageManager.java new file mode 100644 index 000000000..26ebe939b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ReportMessageManager.java @@ -0,0 +1,151 @@ +/* + * Copyright 2021-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import io.lettuce.core.RedisException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.util.UUIDUtil; + +public class ReportMessageManager { + + private final ReportMessageDynamoDb reportMessageDynamoDb; + private final FaultTolerantRedisCluster rateLimitCluster; + + private final Duration counterTtl; + + private final List reportedMessageListeners = new ArrayList<>(); + + private static final String REPORT_MESSAGE_COUNTER_NAME = MetricsUtil.name(ReportMessageManager.class, "reportMessage"); + private static final String FOUND_MESSAGE_TAG = "foundMessage"; + private static final String TOKEN_PRESENT_TAG = "hasReportSpamToken"; + + private static final Logger logger = LoggerFactory.getLogger(ReportMessageManager.class); + + public ReportMessageManager(final ReportMessageDynamoDb reportMessageDynamoDb, + final FaultTolerantRedisCluster rateLimitCluster, + final Duration counterTtl) { + + this.reportMessageDynamoDb = reportMessageDynamoDb; + this.rateLimitCluster = rateLimitCluster; + + this.counterTtl = counterTtl; + } + + public void addListener(final ReportedMessageListener listener) { + this.reportedMessageListeners.add(listener); + } + + public void store(String sourceAci, UUID messageGuid) { + + try { + Objects.requireNonNull(sourceAci); + + reportMessageDynamoDb.store(hash(messageGuid, sourceAci)); + } catch (final Exception e) { + logger.warn("Failed to store hash", e); + } + } + + public void report(final Optional sourceNumber, + final Optional sourceAci, + final Optional sourcePni, + final UUID messageGuid, + final UUID reporterUuid, + final Optional reportSpamToken, + final String reporterUserAgent) { + + final boolean found = sourceAci.map(uuid -> reportMessageDynamoDb.remove(hash(messageGuid, uuid.toString()))) + .orElse(false); + + Metrics.counter(REPORT_MESSAGE_COUNTER_NAME, + Tags.of(FOUND_MESSAGE_TAG, String.valueOf(found), + TOKEN_PRESENT_TAG, String.valueOf(reportSpamToken.isPresent())) + .and(UserAgentTagUtil.getPlatformTag(reporterUserAgent))) + .increment(); + + if (found) { + rateLimitCluster.useCluster(connection -> { + sourcePni.ifPresent(pni -> { + final String reportedSenderKey = getReportedSenderPniKey(pni); + connection.sync().pfadd(reportedSenderKey, reporterUuid.toString()); + connection.sync().expire(reportedSenderKey, counterTtl.toSeconds()); + }); + + sourceAci.ifPresent(aci -> { + final String reportedSenderKey = getReportedSenderAciKey(aci); + connection.sync().pfadd(reportedSenderKey, reporterUuid.toString()); + connection.sync().expire(reportedSenderKey, counterTtl.toSeconds()); + }); + }); + + sourceNumber.ifPresent(number -> + reportedMessageListeners.forEach(listener -> { + try { + listener.handleMessageReported(number, messageGuid, reporterUuid, reportSpamToken); + } catch (final Exception e) { + logger.error("Failed to notify listener of reported message", e); + } + })); + } + } + + /** + * Returns the number of times messages from the given account have been reported by recipients as spam. Note that + * this method makes a call to an external service, and callers should take care to memoize calls where possible and + * avoid unnecessary calls. + * + * @param account the account to check for recent reports + * @return the number of times the given number has been reported recently + */ + public int getRecentReportCount(final Account account) { + try { + return rateLimitCluster.withCluster( + connection -> + Math.max( + connection.sync().pfcount(getReportedSenderPniKey(account.getPhoneNumberIdentifier())).intValue(), + connection.sync().pfcount(getReportedSenderAciKey(account.getUuid())).intValue())); + } catch (final RedisException e) { + return 0; + } + } + + private byte[] hash(UUID messageGuid, String otherId) { + final MessageDigest sha256; + try { + sha256 = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + + sha256.update(UUIDUtil.toBytes(messageGuid)); + sha256.update(otherId.getBytes(StandardCharsets.UTF_8)); + + return sha256.digest(); + } + + private static String getReportedSenderAciKey(final UUID aci) { + return "reported_account::" + aci.toString(); + } + + private static String getReportedSenderPniKey(final UUID pni) { + return "reported_pni::" + pni.toString(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ReportedMessageListener.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ReportedMessageListener.java new file mode 100644 index 000000000..96e580b11 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/ReportedMessageListener.java @@ -0,0 +1,14 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import java.util.Optional; +import java.util.UUID; + +public interface ReportedMessageListener { + + void handleMessageReported(String sourceNumber, UUID messageGuid, UUID reporterUuid, Optional reportSpamToken); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SerializedExpireableJsonDynamoStore.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SerializedExpireableJsonDynamoStore.java new file mode 100644 index 000000000..d2e80ad50 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SerializedExpireableJsonDynamoStore.java @@ -0,0 +1,151 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.annotations.VisibleForTesting; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.time.Clock; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; + +public abstract class SerializedExpireableJsonDynamoStore { + + public interface Expireable { + + @JsonIgnore + long getExpirationEpochSeconds(); + } + + private final DynamoDbAsyncClient dynamoDbClient; + private final String tableName; + private final Clock clock; + private final Class deserializationTargetClass; + + @VisibleForTesting + static final String KEY_KEY = "K"; + + private static final String ATTR_SERIALIZED_VALUE = "V"; + private static final String ATTR_TTL = "E"; + + private final Logger log = LoggerFactory.getLogger(getClass()); + + public SerializedExpireableJsonDynamoStore(final DynamoDbAsyncClient dynamoDbClient, final String tableName, + final Clock clock) { + this.dynamoDbClient = dynamoDbClient; + this.tableName = tableName; + this.clock = clock; + + if (getClass().getGenericSuperclass() instanceof ParameterizedType pt) { + // Extract the parameterized class declared by concrete implementations, so that it can + // be passed to future deserialization calls + final Type[] actualTypeArguments = pt.getActualTypeArguments(); + if (actualTypeArguments.length != 1) { + throw new RuntimeException("Unexpected number of type arguments: " + actualTypeArguments.length); + } + deserializationTargetClass = (Class) actualTypeArguments[0]; + } else { + throw new RuntimeException( + "Unable to determine target class for deserialization - generic superclass is not a ParameterizedType"); + } + } + + public CompletableFuture insert(final String key, final T v) { + return put(key, v, builder -> builder.expressionAttributeNames(Map.of( + "#key", KEY_KEY + )).conditionExpression("attribute_not_exists(#key)")); + } + + public CompletableFuture update(final String key, final T v) { + return put(key, v, ignored -> { + }); + } + + private CompletableFuture put(final String key, final T v, + final Consumer putRequestCustomizer) { + try { + final Map attributeValueMap = new HashMap<>(Map.of( + KEY_KEY, AttributeValues.fromString(key), + ATTR_SERIALIZED_VALUE, + AttributeValues.fromString(SystemMapper.jsonMapper().writeValueAsString(v)))); + if (v instanceof Expireable ev) { + attributeValueMap.put(ATTR_TTL, AttributeValues.fromLong(getExpirationTimestamp(ev))); + } + final PutItemRequest.Builder builder = PutItemRequest.builder() + .tableName(tableName) + .item(attributeValueMap); + putRequestCustomizer.accept(builder); + + return dynamoDbClient.putItem(builder.build()) + .thenRun(() -> { + }); + } catch (final JsonProcessingException e) { + // This should never happen when writing directly to a string except in cases of serious misconfiguration, which + // would be caught by tests. + throw new AssertionError(e); + } + } + + private long getExpirationTimestamp(final Expireable v) { + return v.getExpirationEpochSeconds(); + } + + public CompletableFuture> findForKey(final String key) { + return dynamoDbClient.getItem(GetItemRequest.builder() + .tableName(tableName) + .consistentRead(true) + .key(Map.of(KEY_KEY, AttributeValues.fromString(key))) + .build()) + .thenApply(response -> { + try { + return response.hasItem() + ? filterMaybeExpiredValue( + SystemMapper.jsonMapper() + .readValue(response.item().get(ATTR_SERIALIZED_VALUE).s(), deserializationTargetClass)) + : Optional.empty(); + } catch (final JsonProcessingException e) { + log.error("Failed to parse stored value", e); + return Optional.empty(); + } + }); + } + + private Optional filterMaybeExpiredValue(T v) { + // It's possible for DynamoDB to return items after their expiration time (although it is very unlikely for small + // tables) + if (v instanceof Expireable ev) { + if (getExpirationTimestamp(ev) < clock.instant().getEpochSecond()) { + return Optional.empty(); + } + } + + return Optional.of(v); + } + + public CompletableFuture remove(final String key) { + return dynamoDbClient.deleteItem(DeleteItemRequest.builder() + .tableName(tableName) + .key(Map.of(KEY_KEY, AttributeValues.fromString(key))) + .build()) + .thenRun(() -> { + }); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SingleUseECPreKeyStore.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SingleUseECPreKeyStore.java new file mode 100644 index 000000000..025f10b81 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SingleUseECPreKeyStore.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.whispersystems.textsecuregcm.entities.ECPreKey; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import java.util.Map; +import java.util.UUID; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +public class SingleUseECPreKeyStore extends SingleUsePreKeyStore { + private static final String PARSE_BYTE_ARRAY_COUNTER_NAME = name(SingleUseECPreKeyStore.class, "parseByteArray"); + + protected SingleUseECPreKeyStore(final DynamoDbAsyncClient dynamoDbAsyncClient, final String tableName) { + super(dynamoDbAsyncClient, tableName); + } + + @Override + protected Map getItemFromPreKey(final UUID identifier, final long deviceId, final ECPreKey preKey) { + return Map.of( + KEY_ACCOUNT_UUID, getPartitionKey(identifier), + KEY_DEVICE_ID_KEY_ID, getSortKey(deviceId, preKey.keyId()), + ATTR_PUBLIC_KEY, AttributeValues.fromByteArray(preKey.serializedPublicKey())); + } + + @Override + protected ECPreKey getPreKeyFromItem(final Map item) { + final long keyId = item.get(KEY_DEVICE_ID_KEY_ID).b().asByteBuffer().getLong(8); + final byte[] publicKey = AttributeValues.extractByteArray(item.get(ATTR_PUBLIC_KEY), PARSE_BYTE_ARRAY_COUNTER_NAME); + + try { + return new ECPreKey(keyId, new ECPublicKey(publicKey)); + } catch (final InvalidKeyException e) { + // This should never happen since we're serializing keys directly from `ECPublicKey` instances on the way in + throw new IllegalArgumentException(e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SingleUseKEMPreKeyStore.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SingleUseKEMPreKeyStore.java new file mode 100644 index 000000000..2e54fad37 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SingleUseKEMPreKeyStore.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.kem.KEMPublicKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import java.util.Map; +import java.util.UUID; + +public class SingleUseKEMPreKeyStore extends SingleUsePreKeyStore { + + protected SingleUseKEMPreKeyStore(final DynamoDbAsyncClient dynamoDbAsyncClient, final String tableName) { + super(dynamoDbAsyncClient, tableName); + } + + @Override + protected Map getItemFromPreKey(final UUID identifier, final long deviceId, final KEMSignedPreKey signedPreKey) { + return Map.of( + KEY_ACCOUNT_UUID, getPartitionKey(identifier), + KEY_DEVICE_ID_KEY_ID, getSortKey(deviceId, signedPreKey.keyId()), + ATTR_PUBLIC_KEY, AttributeValues.fromByteArray(signedPreKey.serializedPublicKey()), + ATTR_SIGNATURE, AttributeValues.fromByteArray(signedPreKey.signature())); + } + + @Override + protected KEMSignedPreKey getPreKeyFromItem(final Map item) { + final long keyId = item.get(KEY_DEVICE_ID_KEY_ID).b().asByteBuffer().getLong(8); + final byte[] publicKey = item.get(ATTR_PUBLIC_KEY).b().asByteArray(); + final byte[] signature = item.get(ATTR_SIGNATURE).b().asByteArray(); + + try { + return new KEMSignedPreKey(keyId, new KEMPublicKey(publicKey), signature); + } catch (final InvalidKeyException e) { + // This should never happen since we're serializing keys directly from `KEMPublicKey` instances on the way in + throw new IllegalArgumentException(e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SingleUsePreKeyStore.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SingleUsePreKeyStore.java new file mode 100644 index 000000000..95e086544 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SingleUsePreKeyStore.java @@ -0,0 +1,287 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; +import static org.whispersystems.textsecuregcm.storage.AbstractDynamoDbStore.DYNAMO_DB_MAX_BATCH_SIZE; + +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Timer; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import org.whispersystems.textsecuregcm.entities.PreKey; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import org.whispersystems.textsecuregcm.util.Util; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; +import software.amazon.awssdk.services.dynamodb.model.ReturnValue; +import software.amazon.awssdk.services.dynamodb.model.Select; + +/** + * A single-use pre-key store stores single-use pre-keys of a specific type. Keys returned by a single-use pre-key + * store's {@link #take(UUID, long)} method are guaranteed to be returned exactly once, and repeated calls will never + * yield the same key. + *

    + * Each {@link Account} may have one or more {@link Device devices}. Clients should regularly check their + * supply of single-use pre-keys (see {@link #getCount(UUID, long)}) and upload new keys when their supply runs low. In + * the event that a party wants to begin a session with a device that has no single-use pre-keys remaining, that party + * may fall back to using the device's repeated-use ("last-resort") signed pre-key instead. + */ +public abstract class SingleUsePreKeyStore> { + + private final DynamoDbAsyncClient dynamoDbAsyncClient; + private final String tableName; + + private final Timer storeKeyTimer = Metrics.timer(name(getClass(), "storeKey")); + private final Timer storeKeyBatchTimer = Metrics.timer(name(getClass(), "storeKeyBatch")); + private final Timer getKeyCountTimer = Metrics.timer(name(getClass(), "getCount")); + private final Timer deleteForDeviceTimer = Metrics.timer(name(getClass(), "deleteForDevice")); + private final Timer deleteForAccountTimer = Metrics.timer(name(getClass(), "deleteForAccount")); + + final DistributionSummary keysConsideredForTakeDistributionSummary = DistributionSummary + .builder(name(getClass(), "keysConsideredForTake")) + .publishPercentiles(0.5, 0.75, 0.95, 0.99, 0.999) + .distributionStatisticExpiry(Duration.ofMinutes(10)) + .register(Metrics.globalRegistry); + + final DistributionSummary availableKeyCountDistributionSummary = DistributionSummary + .builder(name(getClass(), "availableKeyCount")) + .publishPercentiles(0.5, 0.75, 0.95, 0.99, 0.999) + .distributionStatisticExpiry(Duration.ofMinutes(10)) + .register(Metrics.globalRegistry); + + private final String takeKeyTimerName = name(getClass(), "takeKey"); + private static final String KEY_PRESENT_TAG_NAME = "keyPresent"; + + static final String KEY_ACCOUNT_UUID = "U"; + static final String KEY_DEVICE_ID_KEY_ID = "DK"; + static final String ATTR_PUBLIC_KEY = "P"; + static final String ATTR_SIGNATURE = "S"; + + protected SingleUsePreKeyStore(final DynamoDbAsyncClient dynamoDbAsyncClient, final String tableName) { + this.dynamoDbAsyncClient = dynamoDbAsyncClient; + this.tableName = tableName; + } + + /** + * Stores a batch of single-use pre-keys for a specific device. All previously-stored keys for the device are cleared + * before storing new keys. + * + * @param identifier the identifier for the account/identity with which the target device is associated + * @param deviceId the identifier for the device within the given account/identity + * @param preKeys a collection of single-use pre-keys to store for the target device + * + * @return a future that completes when all previously-stored keys have been removed and the given collection of + * pre-keys has been stored in its place + */ + public CompletableFuture store(final UUID identifier, final long deviceId, final List preKeys) { + final Timer.Sample sample = Timer.start(); + + return Mono.fromFuture(() -> delete(identifier, deviceId)) + .thenMany( + Flux.fromIterable(preKeys) + .flatMap(preKey -> Mono.fromFuture(() -> store(identifier, deviceId, preKey)), DYNAMO_DB_MAX_BATCH_SIZE)) + .then() + .toFuture() + .thenRun(() -> sample.stop(storeKeyBatchTimer)); + } + + private CompletableFuture store(final UUID identifier, final long deviceId, final K preKey) { + final Timer.Sample sample = Timer.start(); + + return dynamoDbAsyncClient.putItem(PutItemRequest.builder() + .tableName(tableName) + .item(getItemFromPreKey(identifier, deviceId, preKey)) + .build()) + .thenRun(() -> sample.stop(storeKeyTimer)); + } + + /** + * Attempts to retrieve a single-use pre-key for a specific device. Keys may only be returned by this method at most + * once; once the key is returned, it is removed from the key store and subsequent calls to this method will never + * return the same key. + * + * @param identifier the identifier for the account/identity with which the target device is associated + * @param deviceId the identifier for the device within the given account/identity + * + * @return a future that yields a single-use pre-key if one is available or empty if no single-use pre-keys are + * available for the target device + */ + public CompletableFuture> take(final UUID identifier, final long deviceId) { + final Timer.Sample sample = Timer.start(); + final AttributeValue partitionKey = getPartitionKey(identifier); + final AtomicInteger keysConsidered = new AtomicInteger(0); + + return Flux.from(dynamoDbAsyncClient.queryPaginator(QueryRequest.builder() + .tableName(tableName) + .keyConditionExpression("#uuid = :uuid AND begins_with (#sort, :sortprefix)") + .expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID, "#sort", KEY_DEVICE_ID_KEY_ID)) + .expressionAttributeValues(Map.of( + ":uuid", partitionKey, + ":sortprefix", getSortKeyPrefix(deviceId))) + .projectionExpression(KEY_DEVICE_ID_KEY_ID) + .consistentRead(false) + .build()) + .items()) + .map(item -> DeleteItemRequest.builder() + .tableName(tableName) + .key(Map.of( + KEY_ACCOUNT_UUID, partitionKey, + KEY_DEVICE_ID_KEY_ID, item.get(KEY_DEVICE_ID_KEY_ID))) + .returnValues(ReturnValue.ALL_OLD) + .build()) + .flatMap(deleteItemRequest -> Mono.fromFuture(() -> dynamoDbAsyncClient.deleteItem(deleteItemRequest)), 1) + .doOnNext(deleteItemResponse -> keysConsidered.incrementAndGet()) + .filter(DeleteItemResponse::hasAttributes) + .next() + .map(deleteItemResponse -> getPreKeyFromItem(deleteItemResponse.attributes())) + .toFuture() + .thenApply(Optional::ofNullable) + .whenComplete((maybeKey, throwable) -> { + sample.stop(Metrics.timer(takeKeyTimerName, KEY_PRESENT_TAG_NAME, String.valueOf(maybeKey != null && maybeKey.isPresent()))); + keysConsideredForTakeDistributionSummary.record(keysConsidered.get()); + }); + } + + /** + * Estimates the number of single-use pre-keys available for a given device. + + * @param identifier the identifier for the account/identity with which the target device is associated + * @param deviceId the identifier for the device within the given account/identity + + * @return a future that yields the approximate number of single-use pre-keys currently available for the target + * device + */ + public CompletableFuture getCount(final UUID identifier, final long deviceId) { + final Timer.Sample sample = Timer.start(); + + // Getting an accurate count from DynamoDB can be very confusing. See: + // + // - https://github.com/aws/aws-sdk-java/issues/693 + // - https://github.com/aws/aws-sdk-java/issues/915 + // - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.Count + return Flux.from(dynamoDbAsyncClient.queryPaginator(QueryRequest.builder() + .tableName(tableName) + .keyConditionExpression("#uuid = :uuid AND begins_with (#sort, :sortprefix)") + .expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID, "#sort", KEY_DEVICE_ID_KEY_ID)) + .expressionAttributeValues(Map.of( + ":uuid", getPartitionKey(identifier), + ":sortprefix", getSortKeyPrefix(deviceId))) + .select(Select.COUNT) + .consistentRead(false) + .build())) + .map(QueryResponse::count) + .reduce(0, Integer::sum) + .toFuture() + .whenComplete((keyCount, throwable) -> { + sample.stop(getKeyCountTimer); + + if (throwable == null && keyCount != null) { + availableKeyCountDistributionSummary.record(keyCount); + } + }); + } + + /** + * Removes all single-use pre-keys for all devices associated with the given account/identity. + * + * @param identifier the identifier for the account/identity for which to remove single-use pre-keys + * + * @return a future that completes when all single-use pre-keys have been removed for all devices associated with the + * given account/identity + */ + public CompletableFuture delete(final UUID identifier) { + final Timer.Sample sample = Timer.start(); + + return deleteItems(getPartitionKey(identifier), Flux.from(dynamoDbAsyncClient.queryPaginator(QueryRequest.builder() + .tableName(tableName) + .keyConditionExpression("#uuid = :uuid") + .expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID)) + .expressionAttributeValues(Map.of(":uuid", getPartitionKey(identifier))) + .projectionExpression(KEY_DEVICE_ID_KEY_ID) + .consistentRead(true) + .build()) + .items())) + .thenRun(() -> sample.stop(deleteForAccountTimer)); + } + + /** + * Removes all single-use pre-keys for a specific device. + * + * @param identifier the identifier for the account/identity with which the target device is associated + * @param deviceId the identifier for the device within the given account/identity + + * @return a future that completes when all single-use pre-keys have been removed for the target device + */ + public CompletableFuture delete(final UUID identifier, final long deviceId) { + final Timer.Sample sample = Timer.start(); + + return deleteItems(getPartitionKey(identifier), Flux.from(dynamoDbAsyncClient.queryPaginator(QueryRequest.builder() + .tableName(tableName) + .keyConditionExpression("#uuid = :uuid AND begins_with (#sort, :sortprefix)") + .expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID, "#sort", KEY_DEVICE_ID_KEY_ID)) + .expressionAttributeValues(Map.of( + ":uuid", getPartitionKey(identifier), + ":sortprefix", getSortKeyPrefix(deviceId))) + .projectionExpression(KEY_DEVICE_ID_KEY_ID) + .consistentRead(true) + .build()) + .items())) + .thenRun(() -> sample.stop(deleteForDeviceTimer)); + } + + private CompletableFuture deleteItems(final AttributeValue partitionKey, final Flux> items) { + return items + .map(item -> DeleteItemRequest.builder() + .tableName(tableName) + .key(Map.of( + KEY_ACCOUNT_UUID, partitionKey, + KEY_DEVICE_ID_KEY_ID, item.get(KEY_DEVICE_ID_KEY_ID) + )) + .build()) + .flatMap(deleteItemRequest -> Mono.fromFuture(() -> dynamoDbAsyncClient.deleteItem(deleteItemRequest)), DYNAMO_DB_MAX_BATCH_SIZE) + // Idiom: wait for everything to finish, but discard the results + .reduce(0, (a, b) -> 0) + .toFuture() + .thenRun(Util.NOOP); + } + + protected static AttributeValue getPartitionKey(final UUID accountUuid) { + return AttributeValues.fromUUID(accountUuid); + } + + protected static AttributeValue getSortKey(final long deviceId, final long keyId) { + final ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); + byteBuffer.putLong(deviceId); + byteBuffer.putLong(keyId); + return AttributeValues.fromByteBuffer(byteBuffer.flip()); + } + + private static AttributeValue getSortKeyPrefix(final long deviceId) { + final ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[8]); + byteBuffer.putLong(deviceId); + return AttributeValues.fromByteBuffer(byteBuffer.flip()); + } + + protected abstract Map getItemFromPreKey(final UUID identifier, final long deviceId, + final K preKey); + + protected abstract K getPreKeyFromItem(final Map item); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java new file mode 100644 index 000000000..8837e1eb8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java @@ -0,0 +1,429 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.whispersystems.textsecuregcm.util.AttributeValues.b; +import static org.whispersystems.textsecuregcm.util.AttributeValues.n; +import static org.whispersystems.textsecuregcm.util.AttributeValues.s; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Throwables; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Instant; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.core.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer; +import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor; +import org.whispersystems.textsecuregcm.util.Pair; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.ReturnValue; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; + +public class SubscriptionManager { + + private static final Logger logger = LoggerFactory.getLogger(SubscriptionManager.class); + + private static final int USER_LENGTH = 16; + + public static final String KEY_USER = "U"; // B (Hash Key) + public static final String KEY_PASSWORD = "P"; // B + public static final String KEY_PROCESSOR_ID_CUSTOMER_ID = "PC"; // B (GSI Hash Key of `pc_to_u` index) + public static final String KEY_CREATED_AT = "R"; // N + public static final String KEY_SUBSCRIPTION_ID = "S"; // S + public static final String KEY_SUBSCRIPTION_CREATED_AT = "T"; // N + public static final String KEY_SUBSCRIPTION_LEVEL = "L"; + public static final String KEY_SUBSCRIPTION_LEVEL_CHANGED_AT = "V"; // N + public static final String KEY_ACCESSED_AT = "A"; // N + public static final String KEY_CANCELED_AT = "B"; // N + public static final String KEY_CURRENT_PERIOD_ENDS_AT = "D"; // N + + public static final String INDEX_NAME = "pc_to_u"; // Hash Key "PC" + + public static class Record { + + public final byte[] user; + public final byte[] password; + public final Instant createdAt; + @VisibleForTesting + @Nullable + ProcessorCustomer processorCustomer; + @Nullable + public String subscriptionId; + public Instant subscriptionCreatedAt; + public Long subscriptionLevel; + public Instant subscriptionLevelChangedAt; + public Instant accessedAt; + public Instant canceledAt; + public Instant currentPeriodEndsAt; + + private Record(byte[] user, byte[] password, Instant createdAt) { + this.user = checkUserLength(user); + this.password = Objects.requireNonNull(password); + this.createdAt = Objects.requireNonNull(createdAt); + } + + public static Record from(byte[] user, Map item) { + Record record = new Record( + user, + item.get(KEY_PASSWORD).b().asByteArray(), + getInstant(item, KEY_CREATED_AT)); + + final Pair processorCustomerId = getProcessorAndCustomer(item); + if (processorCustomerId != null) { + record.processorCustomer = new ProcessorCustomer(processorCustomerId.second(), processorCustomerId.first()); + } + record.subscriptionId = getString(item, KEY_SUBSCRIPTION_ID); + record.subscriptionCreatedAt = getInstant(item, KEY_SUBSCRIPTION_CREATED_AT); + record.subscriptionLevel = getLong(item, KEY_SUBSCRIPTION_LEVEL); + record.subscriptionLevelChangedAt = getInstant(item, KEY_SUBSCRIPTION_LEVEL_CHANGED_AT); + record.accessedAt = getInstant(item, KEY_ACCESSED_AT); + record.canceledAt = getInstant(item, KEY_CANCELED_AT); + record.currentPeriodEndsAt = getInstant(item, KEY_CURRENT_PERIOD_ENDS_AT); + return record; + } + + public Optional getProcessorCustomer() { + return Optional.ofNullable(processorCustomer); + } + + /** + * Extracts the active processor and customer from a single attribute value in the given item. + *

    + * Until existing data is migrated, this may return {@code null}. + */ + @Nullable + private static Pair getProcessorAndCustomer(Map item) { + + final AttributeValue attributeValue = item.get(KEY_PROCESSOR_ID_CUSTOMER_ID); + + if (attributeValue == null) { + // temporarily allow null values + return null; + } + + final byte[] processorAndCustomerId = attributeValue.b().asByteArray(); + final byte processorId = processorAndCustomerId[0]; + + final SubscriptionProcessor processor = SubscriptionProcessor.forId(processorId); + if (processor == null) { + throw new IllegalStateException("unknown processor id: " + processorId); + } + + final String customerId = new String(processorAndCustomerId, 1, processorAndCustomerId.length - 1, + StandardCharsets.UTF_8); + + return new Pair<>(processor, customerId); + } + + private static String getString(Map item, String key) { + AttributeValue attributeValue = item.get(key); + if (attributeValue == null) { + return null; + } + return attributeValue.s(); + } + + private static Long getLong(Map item, String key) { + AttributeValue attributeValue = item.get(key); + if (attributeValue == null || attributeValue.n() == null) { + return null; + } + return Long.valueOf(attributeValue.n()); + } + + private static Instant getInstant(Map item, String key) { + AttributeValue attributeValue = item.get(key); + if (attributeValue == null || attributeValue.n() == null) { + return null; + } + return Instant.ofEpochSecond(Long.parseLong(attributeValue.n())); + } + } + + private final String table; + private final DynamoDbAsyncClient client; + + public SubscriptionManager( + @Nonnull String table, + @Nonnull DynamoDbAsyncClient client) { + this.table = Objects.requireNonNull(table); + this.client = Objects.requireNonNull(client); + } + + /** + * Looks in the GSI for a record with the given customer id and returns the user id. + */ + public CompletableFuture getSubscriberUserByProcessorCustomer(ProcessorCustomer processorCustomer) { + QueryRequest query = QueryRequest.builder() + .tableName(table) + .indexName(INDEX_NAME) + .keyConditionExpression("#processor_customer_id = :processor_customer_id") + .projectionExpression("#user") + .expressionAttributeNames(Map.of( + "#processor_customer_id", KEY_PROCESSOR_ID_CUSTOMER_ID, + "#user", KEY_USER)) + .expressionAttributeValues(Map.of( + ":processor_customer_id", b(processorCustomer.toDynamoBytes()))) + .build(); + return client.query(query).thenApply(queryResponse -> { + int count = queryResponse.count(); + if (count == 0) { + return null; + } else if (count > 1) { + logger.error("expected invariant of 1-1 subscriber-customer violated for customer {} ({})", + processorCustomer.customerId(), processorCustomer.processor()); + throw new IllegalStateException( + "expected invariant of 1-1 subscriber-customer violated for customer " + processorCustomer); + } else { + Map result = queryResponse.items().get(0); + return result.get(KEY_USER).b().asByteArray(); + } + }); + } + + public static class GetResult { + + public static final GetResult NOT_STORED = new GetResult(Type.NOT_STORED, null); + public static final GetResult PASSWORD_MISMATCH = new GetResult(Type.PASSWORD_MISMATCH, null); + + public enum Type { + NOT_STORED, + PASSWORD_MISMATCH, + FOUND + } + + public final Type type; + public final Record record; + + private GetResult(Type type, Record record) { + this.type = type; + this.record = record; + } + + public static GetResult found(Record record) { + return new GetResult(Type.FOUND, record); + } + } + + /** + * Looks up a record with the given {@code user} and validates the {@code hmac} before returning it. + */ + public CompletableFuture get(byte[] user, byte[] hmac) { + return getUser(user).thenApply(getItemResponse -> { + if (!getItemResponse.hasItem()) { + return GetResult.NOT_STORED; + } + + Record record = Record.from(user, getItemResponse.item()); + if (!MessageDigest.isEqual(hmac, record.password)) { + return GetResult.PASSWORD_MISMATCH; + } + return GetResult.found(record); + }); + } + + private CompletableFuture getUser(byte[] user) { + checkUserLength(user); + + GetItemRequest request = GetItemRequest.builder() + .consistentRead(Boolean.TRUE) + .tableName(table) + .key(Map.of(KEY_USER, b(user))) + .build(); + + return client.getItem(request); + } + + public CompletableFuture create(byte[] user, byte[] password, Instant createdAt) { + checkUserLength(user); + + UpdateItemRequest request = UpdateItemRequest.builder() + .tableName(table) + .key(Map.of(KEY_USER, b(user))) + .returnValues(ReturnValue.ALL_NEW) + .conditionExpression("attribute_not_exists(#user) OR #password = :password") + .updateExpression("SET " + + "#password = if_not_exists(#password, :password), " + + "#created_at = if_not_exists(#created_at, :created_at), " + + "#accessed_at = if_not_exists(#accessed_at, :accessed_at)" + ) + .expressionAttributeNames(Map.of( + "#user", KEY_USER, + "#password", KEY_PASSWORD, + "#created_at", KEY_CREATED_AT, + "#accessed_at", KEY_ACCESSED_AT) + ) + .expressionAttributeValues(Map.of( + ":password", b(password), + ":created_at", n(createdAt.getEpochSecond()), + ":accessed_at", n(createdAt.getEpochSecond())) + ) + .build(); + return client.updateItem(request).handle((updateItemResponse, throwable) -> { + if (throwable != null) { + if (Throwables.getRootCause(throwable) instanceof ConditionalCheckFailedException) { + return null; + } + Throwables.throwIfUnchecked(throwable); + throw new CompletionException(throwable); + } + + return Record.from(user, updateItemResponse.attributes()); + }); + } + + /** + * Sets the processor and customer ID for the given user record. + * + * @return the user record. + */ + public CompletableFuture setProcessorAndCustomerId(Record userRecord, + ProcessorCustomer activeProcessorCustomer, Instant updatedAt) { + + UpdateItemRequest request = UpdateItemRequest.builder() + .tableName(table) + .key(Map.of(KEY_USER, b(userRecord.user))) + .returnValues(ReturnValue.ALL_NEW) + .conditionExpression("attribute_not_exists(#processor_customer_id)") + .updateExpression("SET " + + "#processor_customer_id = :processor_customer_id, " + + "#accessed_at = :accessed_at" + ) + .expressionAttributeNames(Map.of( + "#accessed_at", KEY_ACCESSED_AT, + "#processor_customer_id", KEY_PROCESSOR_ID_CUSTOMER_ID + )) + .expressionAttributeValues(Map.of( + ":accessed_at", n(updatedAt.getEpochSecond()), + ":processor_customer_id", b(activeProcessorCustomer.toDynamoBytes()) + )).build(); + + return client.updateItem(request) + .thenApply(updateItemResponse -> Record.from(userRecord.user, updateItemResponse.attributes())) + .exceptionallyCompose(throwable -> { + if (Throwables.getRootCause(throwable) instanceof ConditionalCheckFailedException) { + throw new ClientErrorException(Response.Status.CONFLICT); + } + Throwables.throwIfUnchecked(throwable); + throw new CompletionException(throwable); + }); + } + + public CompletableFuture accessedAt(byte[] user, Instant accessedAt) { + checkUserLength(user); + + UpdateItemRequest request = UpdateItemRequest.builder() + .tableName(table) + .key(Map.of(KEY_USER, b(user))) + .returnValues(ReturnValue.NONE) + .updateExpression("SET #accessed_at = :accessed_at") + .expressionAttributeNames(Map.of("#accessed_at", KEY_ACCESSED_AT)) + .expressionAttributeValues(Map.of(":accessed_at", n(accessedAt.getEpochSecond()))) + .build(); + return client.updateItem(request).thenApply(updateItemResponse -> null); + } + + public CompletableFuture canceledAt(byte[] user, Instant canceledAt) { + checkUserLength(user); + + UpdateItemRequest request = UpdateItemRequest.builder() + .tableName(table) + .key(Map.of(KEY_USER, b(user))) + .returnValues(ReturnValue.NONE) + .updateExpression("SET " + + "#accessed_at = :accessed_at, " + + "#canceled_at = :canceled_at " + + "REMOVE #subscription_id") + .expressionAttributeNames(Map.of( + "#accessed_at", KEY_ACCESSED_AT, + "#canceled_at", KEY_CANCELED_AT, + "#subscription_id", KEY_SUBSCRIPTION_ID)) + .expressionAttributeValues(Map.of( + ":accessed_at", n(canceledAt.getEpochSecond()), + ":canceled_at", n(canceledAt.getEpochSecond()))) + .build(); + return client.updateItem(request).thenApply(updateItemResponse -> null); + } + + public CompletableFuture subscriptionCreated( + byte[] user, String subscriptionId, Instant subscriptionCreatedAt, long level) { + checkUserLength(user); + + UpdateItemRequest request = UpdateItemRequest.builder() + .tableName(table) + .key(Map.of(KEY_USER, b(user))) + .returnValues(ReturnValue.NONE) + .updateExpression("SET " + + "#accessed_at = :accessed_at, " + + "#subscription_id = :subscription_id, " + + "#subscription_created_at = :subscription_created_at, " + + "#subscription_level = :subscription_level, " + + "#subscription_level_changed_at = :subscription_level_changed_at") + .expressionAttributeNames(Map.of( + "#accessed_at", KEY_ACCESSED_AT, + "#subscription_id", KEY_SUBSCRIPTION_ID, + "#subscription_created_at", KEY_SUBSCRIPTION_CREATED_AT, + "#subscription_level", KEY_SUBSCRIPTION_LEVEL, + "#subscription_level_changed_at", KEY_SUBSCRIPTION_LEVEL_CHANGED_AT)) + .expressionAttributeValues(Map.of( + ":accessed_at", n(subscriptionCreatedAt.getEpochSecond()), + ":subscription_id", s(subscriptionId), + ":subscription_created_at", n(subscriptionCreatedAt.getEpochSecond()), + ":subscription_level", n(level), + ":subscription_level_changed_at", n(subscriptionCreatedAt.getEpochSecond()))) + .build(); + return client.updateItem(request).thenApply(updateItemResponse -> null); + } + + public CompletableFuture subscriptionLevelChanged( + byte[] user, Instant subscriptionLevelChangedAt, long level, String subscriptionId) { + checkUserLength(user); + + UpdateItemRequest request = UpdateItemRequest.builder() + .tableName(table) + .key(Map.of(KEY_USER, b(user))) + .returnValues(ReturnValue.NONE) + .updateExpression("SET " + + "#accessed_at = :accessed_at, " + + "#subscription_id = :subscription_id, " + + "#subscription_level = :subscription_level, " + + "#subscription_level_changed_at = :subscription_level_changed_at") + .expressionAttributeNames(Map.of( + "#accessed_at", KEY_ACCESSED_AT, + "#subscription_id", KEY_SUBSCRIPTION_ID, + "#subscription_level", KEY_SUBSCRIPTION_LEVEL, + "#subscription_level_changed_at", KEY_SUBSCRIPTION_LEVEL_CHANGED_AT)) + .expressionAttributeValues(Map.of( + ":accessed_at", n(subscriptionLevelChangedAt.getEpochSecond()), + ":subscription_id", s(subscriptionId), + ":subscription_level", n(level), + ":subscription_level_changed_at", n(subscriptionLevelChangedAt.getEpochSecond()))) + .build(); + return client.updateItem(request).thenApply(updateItemResponse -> null); + } + + private static byte[] checkUserLength(final byte[] user) { + if (user.length != USER_LENGTH) { + throw new IllegalArgumentException("user length is wrong; expected " + USER_LENGTH + "; was " + user.length); + } + return user; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/UsernameHashNotAvailableException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/UsernameHashNotAvailableException.java new file mode 100644 index 000000000..04f81f625 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/UsernameHashNotAvailableException.java @@ -0,0 +1,9 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +public class UsernameHashNotAvailableException extends Exception { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/UsernameReservationNotFoundException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/UsernameReservationNotFoundException.java new file mode 100644 index 000000000..066e89994 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/UsernameReservationNotFoundException.java @@ -0,0 +1,10 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +public class UsernameReservationNotFoundException extends Exception { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/VerificationSessionManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/VerificationSessionManager.java new file mode 100644 index 000000000..ed9d29cf4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/VerificationSessionManager.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import org.whispersystems.textsecuregcm.registration.VerificationSession; + +public class VerificationSessionManager { + + private final VerificationSessions verificationSessions; + + public VerificationSessionManager(final VerificationSessions verificationSessions) { + this.verificationSessions = verificationSessions; + } + + public CompletableFuture insert(final String encodedSessionId, final VerificationSession verificationSession) { + return verificationSessions.insert(encodedSessionId, verificationSession); + } + + public CompletableFuture update(final String encodedSessionId, final VerificationSession verificationSession) { + return verificationSessions.update(encodedSessionId, verificationSession); + } + + public CompletableFuture> findForId(final String encodedSessionId) { + return verificationSessions.findForKey(encodedSessionId); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/VerificationSessions.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/VerificationSessions.java new file mode 100644 index 000000000..5c543076b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/VerificationSessions.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import java.time.Clock; +import org.whispersystems.textsecuregcm.registration.VerificationSession; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; + +public class VerificationSessions extends SerializedExpireableJsonDynamoStore { + + public VerificationSessions(final DynamoDbAsyncClient dynamoDbClient, final String tableName, final Clock clock) { + super(dynamoDbClient, tableName, clock); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/VersionedProfile.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/VersionedProfile.java new file mode 100644 index 000000000..2535c9b9f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/storage/VersionedProfile.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; +import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter; +import java.util.Arrays; +import java.util.Objects; + +public record VersionedProfile (String version, + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + byte[] name, + + String avatar, + + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + byte[] aboutEmoji, + + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + byte[] about, + + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + byte[] paymentAddress, + + @JsonSerialize(using = ByteArrayAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) + byte[] commitment) {} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BankMandateTranslator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BankMandateTranslator.java new file mode 100644 index 000000000..438617b1a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BankMandateTranslator.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.ResourceBundle; +import javax.annotation.Nonnull; +import org.signal.i18n.HeaderControlledResourceBundleLookup; + +public class BankMandateTranslator { + private static final String BASE_NAME = "org.signal.bankmandate.BankMandate"; + private final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup; + + public BankMandateTranslator( + @Nonnull final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup) { + this.headerControlledResourceBundleLookup = Objects.requireNonNull(headerControlledResourceBundleLookup); + } + + public String translate(final List acceptableLanguages, final BankTransferType bankTransferType) { + final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME, + acceptableLanguages); + return resourceBundle.getString(getKey(bankTransferType)); + } + + private static String getKey(final BankTransferType bankTransferType) { + return switch (bankTransferType) { + case SEPA_DEBIT -> "SEPA_MANDATE"; + }; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BankTransferType.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BankTransferType.java new file mode 100644 index 000000000..589202692 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BankTransferType.java @@ -0,0 +1,10 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +public enum BankTransferType { + SEPA_DEBIT +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeGraphqlClient.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeGraphqlClient.java new file mode 100644 index 000000000..2c59fea0c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeGraphqlClient.java @@ -0,0 +1,331 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +import com.apollographql.apollo3.api.ApolloResponse; +import com.apollographql.apollo3.api.Operation; +import com.apollographql.apollo3.api.Operations; +import com.apollographql.apollo3.api.Optional; +import com.apollographql.apollo3.api.json.BufferedSinkJsonWriter; +import com.braintree.graphql.client.type.ChargePaymentMethodInput; +import com.braintree.graphql.client.type.CreatePayPalBillingAgreementInput; +import com.braintree.graphql.client.type.CreatePayPalOneTimePaymentInput; +import com.braintree.graphql.client.type.CustomFieldInput; +import com.braintree.graphql.client.type.MonetaryAmountInput; +import com.braintree.graphql.client.type.PayPalBillingAgreementChargePattern; +import com.braintree.graphql.client.type.PayPalBillingAgreementExperienceProfileInput; +import com.braintree.graphql.client.type.PayPalBillingAgreementInput; +import com.braintree.graphql.client.type.PayPalExperienceProfileInput; +import com.braintree.graphql.client.type.PayPalIntent; +import com.braintree.graphql.client.type.PayPalLandingPageType; +import com.braintree.graphql.client.type.PayPalOneTimePaymentInput; +import com.braintree.graphql.client.type.PayPalProductAttributesInput; +import com.braintree.graphql.client.type.PayPalUserAction; +import com.braintree.graphql.client.type.TokenizePayPalBillingAgreementInput; +import com.braintree.graphql.client.type.TokenizePayPalOneTimePaymentInput; +import com.braintree.graphql.client.type.TransactionInput; +import com.braintree.graphql.client.type.VaultPaymentMethodInput; +import com.braintree.graphql.clientoperation.ChargePayPalOneTimePaymentMutation; +import com.braintree.graphql.clientoperation.CreatePayPalBillingAgreementMutation; +import com.braintree.graphql.clientoperation.CreatePayPalOneTimePaymentMutation; +import com.braintree.graphql.clientoperation.TokenizePayPalBillingAgreementMutation; +import com.braintree.graphql.clientoperation.TokenizePayPalOneTimePaymentMutation; +import com.braintree.graphql.clientoperation.VaultPaymentMethodMutation; +import java.math.BigDecimal; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Base64; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import javax.ws.rs.ServiceUnavailableException; +import okio.Buffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; + +class BraintreeGraphqlClient { + + // required header value, recommended to be the date the integration began + // https://graphql.braintreepayments.com/guides/making_api_calls/#the-braintree-version-header + private static final String BRAINTREE_VERSION = "2022-10-01"; + + private static final Logger logger = LoggerFactory.getLogger(BraintreeGraphqlClient.class); + + private final FaultTolerantHttpClient httpClient; + private final URI graphqlUri; + private final String authorizationHeader; + + BraintreeGraphqlClient(final FaultTolerantHttpClient httpClient, + final String graphqlUri, + final String publicKey, + final String privateKey) { + this.httpClient = httpClient; + try { + this.graphqlUri = new URI(graphqlUri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid URI", e); + } + // “public”/“private” key is a bit of a misnomer, but we follow the upstream nomenclature + // they are used for Basic auth similar to “client key”/“client secret” credentials + this.authorizationHeader = "Basic " + Base64.getEncoder().encodeToString((publicKey + ":" + privateKey).getBytes()); + } + + CompletableFuture createPayPalOneTimePayment( + final BigDecimal amount, final String currency, final String returnUrl, + final String cancelUrl, final String locale) { + + final CreatePayPalOneTimePaymentInput input = buildCreatePayPalOneTimePaymentInput(amount, currency, returnUrl, + cancelUrl, locale); + final CreatePayPalOneTimePaymentMutation mutation = new CreatePayPalOneTimePaymentMutation(input); + final HttpRequest request = buildRequest(mutation); + + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(httpResponse -> + { + // IntelliJ users: type parameters error “no instance of type variable exists so that Data conforms to Data” + // is not accurate; this might be fixed in Kotlin 1.8: https://youtrack.jetbrains.com/issue/KTIJ-21905/ + final CreatePayPalOneTimePaymentMutation.Data data = assertSuccessAndExtractData(httpResponse, mutation); + return data.createPayPalOneTimePayment; + }); + } + + private static CreatePayPalOneTimePaymentInput buildCreatePayPalOneTimePaymentInput(BigDecimal amount, + String currency, String returnUrl, String cancelUrl, String locale) { + + return new CreatePayPalOneTimePaymentInput( + Optional.absent(), + Optional.absent(), // merchant account ID will be specified when charging + new MonetaryAmountInput(amount.toString(), currency), // this could potentially use a CustomScalarAdapter + cancelUrl, + Optional.absent(), + PayPalIntent.SALE, + Optional.absent(), + Optional.present(false), // offerPayLater, + Optional.absent(), + Optional.present( + new PayPalExperienceProfileInput(Optional.present("Signal"), + Optional.present(false), + Optional.present(PayPalLandingPageType.LOGIN), + Optional.present(locale), + Optional.absent(), + Optional.present(PayPalUserAction.COMMIT))), + Optional.absent(), + Optional.absent(), + returnUrl, + Optional.absent(), + Optional.absent() + ); + } + + CompletableFuture tokenizePayPalOneTimePayment( + final String payerId, final String paymentId, final String paymentToken) { + + final TokenizePayPalOneTimePaymentInput input = new TokenizePayPalOneTimePaymentInput( + Optional.absent(), + Optional.absent(), // merchant account ID will be specified when charging + new PayPalOneTimePaymentInput(payerId, paymentId, paymentToken) + ); + + final TokenizePayPalOneTimePaymentMutation mutation = new TokenizePayPalOneTimePaymentMutation(input); + final HttpRequest request = buildRequest(mutation); + + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(httpResponse -> { + // IntelliJ users: type parameters error “no instance of type variable exists so that Data conforms to Data” + // is not accurate; this might be fixed in Kotlin 1.8: https://youtrack.jetbrains.com/issue/KTIJ-21905/ + final TokenizePayPalOneTimePaymentMutation.Data data = assertSuccessAndExtractData(httpResponse, mutation); + return data.tokenizePayPalOneTimePayment; + }); + } + + CompletableFuture chargeOneTimePayment( + final String paymentMethodId, final BigDecimal amount, final String merchantAccount, final long level) { + + final List customFields = List.of( + new CustomFieldInput("level", Optional.present(Long.toString(level)))); + + final ChargePaymentMethodInput input = buildChargePaymentMethodInput(paymentMethodId, amount, merchantAccount, + customFields); + final ChargePayPalOneTimePaymentMutation mutation = new ChargePayPalOneTimePaymentMutation(input); + final HttpRequest request = buildRequest(mutation); + + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(httpResponse -> { + // IntelliJ users: type parameters error “no instance of type variable exists so that Data conforms to Data” + // is not accurate; this might be fixed in Kotlin 1.8: https://youtrack.jetbrains.com/issue/KTIJ-21905/ + final ChargePayPalOneTimePaymentMutation.Data data = assertSuccessAndExtractData(httpResponse, + mutation); + return data.chargePaymentMethod; + }); + } + + private static ChargePaymentMethodInput buildChargePaymentMethodInput(String paymentMethodId, BigDecimal amount, + String merchantAccount, List customFields) { + + return new ChargePaymentMethodInput( + Optional.absent(), + paymentMethodId, + new TransactionInput( + // documented as “amount: whole number, or exactly two or three decimal places” + amount.toString(), // this could potentially use a CustomScalarAdapter + Optional.present(merchantAccount), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.present(customFields), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent() + ) + ); + } + + public CompletableFuture createPayPalBillingAgreement( + final String returnUrl, final String cancelUrl, final String locale) { + + final CreatePayPalBillingAgreementInput input = buildCreatePayPalBillingAgreementInput(returnUrl, cancelUrl, + locale); + final CreatePayPalBillingAgreementMutation mutation = new CreatePayPalBillingAgreementMutation(input); + final HttpRequest request = buildRequest(mutation); + + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(httpResponse -> { + // IntelliJ users: type parameters error “no instance of type variable exists so that Data conforms to Data” + // is not accurate; this might be fixed in Kotlin 1.8: https://youtrack.jetbrains.com/issue/KTIJ-21905/ + final CreatePayPalBillingAgreementMutation.Data data = assertSuccessAndExtractData(httpResponse, mutation); + return data.createPayPalBillingAgreement; + }); + } + + private static CreatePayPalBillingAgreementInput buildCreatePayPalBillingAgreementInput(String returnUrl, + String cancelUrl, String locale) { + + return new CreatePayPalBillingAgreementInput( + Optional.absent(), + Optional.absent(), + returnUrl, + cancelUrl, + Optional.absent(), + Optional.absent(), + Optional.present(false), // offerPayPalCredit + Optional.absent(), + Optional.present( + new PayPalBillingAgreementExperienceProfileInput(Optional.present("Signal"), + Optional.present(false), // collectShippingAddress + Optional.present(PayPalLandingPageType.LOGIN), + Optional.present(locale), + Optional.absent())), + Optional.absent(), + Optional.present(new PayPalProductAttributesInput( + Optional.present(PayPalBillingAgreementChargePattern.RECURRING_PREPAID) + )) + ); + } + + public CompletableFuture tokenizePayPalBillingAgreement( + final String billingAgreementToken) { + + final TokenizePayPalBillingAgreementInput input = new TokenizePayPalBillingAgreementInput( + Optional.absent(), + new PayPalBillingAgreementInput(billingAgreementToken)); + final TokenizePayPalBillingAgreementMutation mutation = new TokenizePayPalBillingAgreementMutation(input); + final HttpRequest request = buildRequest(mutation); + + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(httpResponse -> { + // IntelliJ users: type parameters error “no instance of type variable exists so that Data conforms to Data” + // is not accurate; this might be fixed in Kotlin 1.8: https://youtrack.jetbrains.com/issue/KTIJ-21905/ + final TokenizePayPalBillingAgreementMutation.Data data = assertSuccessAndExtractData(httpResponse, mutation); + return data.tokenizePayPalBillingAgreement; + }); + } + + public CompletableFuture vaultPaymentMethod(final String customerId, + final String paymentMethodId) { + + final VaultPaymentMethodInput input = buildVaultPaymentMethodInput(customerId, paymentMethodId); + final VaultPaymentMethodMutation mutation = new VaultPaymentMethodMutation(input); + final HttpRequest request = buildRequest(mutation); + + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(httpResponse -> { + // IntelliJ users: type parameters error “no instance of type variable exists so that Data conforms to Data” + // is not accurate; this might be fixed in Kotlin 1.8: https://youtrack.jetbrains.com/issue/KTIJ-21905/ + final VaultPaymentMethodMutation.Data data = assertSuccessAndExtractData(httpResponse, mutation); + return data.vaultPaymentMethod; + }); + } + + private static VaultPaymentMethodInput buildVaultPaymentMethodInput(String customerId, String paymentMethodId) { + return new VaultPaymentMethodInput( + Optional.absent(), + paymentMethodId, + Optional.absent(), + Optional.absent(), + Optional.present(customerId), + Optional.absent(), + Optional.absent() + ); + } + + /** + * Verifies that the HTTP response has a {@code 200} status code and the GraphQL response has no errors, otherwise + * throws a {@link ServiceUnavailableException}. + */ + private , U extends Operation.Data> U assertSuccessAndExtractData( + HttpResponse httpResponse, T operation) { + + if (httpResponse.statusCode() != 200) { + logger.warn("Received HTTP response status {} ({})", httpResponse.statusCode(), + httpResponse.headers().firstValue("paypal-debug-id").orElse("")); + throw new ServiceUnavailableException(); + } + + ApolloResponse response = Operations.parseJsonResponse(operation, httpResponse.body()); + + if (response.hasErrors() || response.data == null) { + //noinspection ConstantConditions + response.errors.forEach( + error -> { + final Object legacyCode = java.util.Optional.ofNullable(error.getExtensions()) + .map(extensions -> extensions.get("legacyCode")) + .orElse(""); + logger.warn("Received GraphQL error for {}: \"{}\" (legacyCode: {})", + response.operation.name(), error.getMessage(), legacyCode); + }); + + throw new ServiceUnavailableException(); + } + + return response.data; + } + + private HttpRequest buildRequest(final Operation operation) { + + final Buffer buffer = new Buffer(); + Operations.composeJsonRequest(operation, new BufferedSinkJsonWriter(buffer)); + + return HttpRequest.newBuilder() + .uri(graphqlUri) + .method("POST", HttpRequest.BodyPublishers.ofString(buffer.readUtf8())) + .header("Content-Type", "application/json") + .header("Authorization", authorizationHeader) + .header("Braintree-Version", BRAINTREE_VERSION) + .build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java new file mode 100644 index 000000000..3d039029f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java @@ -0,0 +1,571 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +import com.braintreegateway.BraintreeGateway; +import com.braintreegateway.ClientTokenRequest; +import com.braintreegateway.Customer; +import com.braintreegateway.CustomerRequest; +import com.braintreegateway.Plan; +import com.braintreegateway.ResourceCollection; +import com.braintreegateway.Result; +import com.braintreegateway.Subscription; +import com.braintreegateway.SubscriptionRequest; +import com.braintreegateway.Transaction; +import com.braintreegateway.TransactionSearchRequest; +import com.braintreegateway.exceptions.BraintreeException; +import com.braintreegateway.exceptions.NotFoundException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.annotations.VisibleForTesting; +import java.math.BigDecimal; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.Comparator; +import java.util.HexFormat; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; +import javax.annotation.Nullable; +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; +import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +public class BraintreeManager implements SubscriptionProcessorManager { + + private static final Logger logger = LoggerFactory.getLogger(BraintreeManager.class); + + private static final String GENERIC_DECLINED_PROCESSOR_CODE = "2046"; + private static final String PAYPAL_FUNDING_INSTRUMENT_DECLINED_PROCESSOR_CODE = "2074"; + private static final String PAYPAL_PAYMENT_ALREADY_COMPLETED_PROCESSOR_CODE = "2094"; + private final BraintreeGateway braintreeGateway; + private final BraintreeGraphqlClient braintreeGraphqlClient; + private final Executor executor; + private final Map> supportedCurrenciesByPaymentMethod; + private final Map currenciesToMerchantAccounts; + + public BraintreeManager(final String braintreeMerchantId, final String braintreePublicKey, + final String braintreePrivateKey, + final String braintreeEnvironment, + final Map> supportedCurrenciesByPaymentMethod, + final Map currenciesToMerchantAccounts, + final String graphqlUri, + final CircuitBreakerConfiguration circuitBreakerConfiguration, + final Executor executor, + final ScheduledExecutorService retryExecutor) { + + this(new BraintreeGateway(braintreeEnvironment, braintreeMerchantId, braintreePublicKey, + braintreePrivateKey), + supportedCurrenciesByPaymentMethod, + currenciesToMerchantAccounts, + new BraintreeGraphqlClient(FaultTolerantHttpClient.newBuilder() + .withName("braintree-graphql") + .withCircuitBreaker(circuitBreakerConfiguration) + .withExecutor(executor) + .withRetryExecutor(retryExecutor) + // Braintree documents its internal timeout at 60 seconds, and we want to make sure we don’t miss + // a response + // https://developer.paypal.com/braintree/docs/reference/general/best-practices/java#timeouts + .withRequestTimeout(Duration.ofSeconds(70)) + .build(), graphqlUri, braintreePublicKey, braintreePrivateKey), + executor); + } + + @VisibleForTesting + BraintreeManager(final BraintreeGateway braintreeGateway, + final Map> supportedCurrenciesByPaymentMethod, + final Map currenciesToMerchantAccounts, final BraintreeGraphqlClient braintreeGraphqlClient, + final Executor executor) { + this.braintreeGateway = braintreeGateway; + this.supportedCurrenciesByPaymentMethod = supportedCurrenciesByPaymentMethod; + this.currenciesToMerchantAccounts = currenciesToMerchantAccounts; + this.braintreeGraphqlClient = braintreeGraphqlClient; + this.executor = executor; + } + + @Override + public Set getSupportedCurrenciesForPaymentMethod(final PaymentMethod paymentMethod) { + return supportedCurrenciesByPaymentMethod.getOrDefault(paymentMethod, Collections.emptySet()); + } + + @Override + public SubscriptionProcessor getProcessor() { + return SubscriptionProcessor.BRAINTREE; + } + + @Override + public boolean supportsPaymentMethod(final PaymentMethod paymentMethod) { + return paymentMethod == PaymentMethod.PAYPAL; + } + + @Override + public CompletableFuture getPaymentDetails(final String paymentId) { + return CompletableFuture.supplyAsync(() -> { + try { + final Transaction transaction = braintreeGateway.transaction().find(paymentId); + + return new PaymentDetails(transaction.getGraphQLId(), + transaction.getCustomFields(), + getPaymentStatus(transaction.getStatus()), + transaction.getCreatedAt().toInstant()); + + } catch (final NotFoundException e) { + return null; + } + }, executor); + } + + public CompletableFuture createOneTimePayment(String currency, long amount, + String locale, String returnUrl, String cancelUrl) { + return braintreeGraphqlClient.createPayPalOneTimePayment(convertApiAmountToBraintreeAmount(currency, amount), + currency.toUpperCase(Locale.ROOT), returnUrl, + cancelUrl, locale) + .thenApply(result -> new PayPalOneTimePaymentApprovalDetails((String) result.approvalUrl, result.paymentId)); + } + + public CompletableFuture captureOneTimePayment(String payerId, String paymentId, + String paymentToken, String currency, long amount, long level) { + return braintreeGraphqlClient.tokenizePayPalOneTimePayment(payerId, paymentId, paymentToken) + .thenCompose(response -> braintreeGraphqlClient.chargeOneTimePayment( + response.paymentMethod.id, + convertApiAmountToBraintreeAmount(currency, amount), + currenciesToMerchantAccounts.get(currency.toLowerCase(Locale.ROOT)), + level) + .thenComposeAsync(chargeResponse -> { + + final PaymentStatus paymentStatus = getPaymentStatus(chargeResponse.transaction.status); + if (paymentStatus == PaymentStatus.SUCCEEDED || paymentStatus == PaymentStatus.PROCESSING) { + return CompletableFuture.completedFuture(new PayPalChargeSuccessDetails(chargeResponse.transaction.id)); + } + + // the GraphQL/Apollo interfaces are a tad unwieldy for this type of status checking + final Transaction unsuccessfulTx = braintreeGateway.transaction().find(chargeResponse.transaction.id); + + if (PAYPAL_PAYMENT_ALREADY_COMPLETED_PROCESSOR_CODE.equals(unsuccessfulTx.getProcessorResponseCode()) + || Transaction.GatewayRejectionReason.DUPLICATE.equals( + unsuccessfulTx.getGatewayRejectionReason())) { + // the payment has already been charged - maybe a previous call timed out or was interrupted - + // in any case, check for a successful transaction with the paymentId + final ResourceCollection search = braintreeGateway.transaction() + .search(new TransactionSearchRequest() + .paypalPaymentId().is(paymentId) + .status().in( + Transaction.Status.SETTLED, + Transaction.Status.SETTLING, + Transaction.Status.SUBMITTED_FOR_SETTLEMENT, + Transaction.Status.SETTLEMENT_PENDING + ) + ); + + if (search.getMaximumSize() == 0) { + return CompletableFuture.failedFuture( + new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR)); + } + + final Transaction successfulTx = search.getFirst(); + + return CompletableFuture.completedFuture( + new PayPalChargeSuccessDetails(successfulTx.getGraphQLId())); + } + + return switch (unsuccessfulTx.getProcessorResponseCode()) { + case GENERIC_DECLINED_PROCESSOR_CODE, PAYPAL_FUNDING_INSTRUMENT_DECLINED_PROCESSOR_CODE -> + CompletableFuture.failedFuture( + new SubscriptionProcessorException(getProcessor(), createChargeFailure(unsuccessfulTx))); + + default -> { + logger.info("PayPal charge unexpectedly failed: {}", unsuccessfulTx.getProcessorResponseCode()); + + yield CompletableFuture.failedFuture( + new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR)); + } + }; + }, executor)); + } + + private static PaymentStatus getPaymentStatus(Transaction.Status status) { + return switch (status) { + case SETTLEMENT_CONFIRMED, SETTLING, SUBMITTED_FOR_SETTLEMENT, SETTLED -> PaymentStatus.SUCCEEDED; + case AUTHORIZATION_EXPIRED, GATEWAY_REJECTED, PROCESSOR_DECLINED, SETTLEMENT_DECLINED, VOIDED, FAILED -> + PaymentStatus.FAILED; + default -> PaymentStatus.UNKNOWN; + }; + } + + private static PaymentStatus getPaymentStatus(com.braintree.graphql.client.type.PaymentStatus status) { + try { + Transaction.Status transactionStatus = Transaction.Status.valueOf(status.rawValue); + + return getPaymentStatus(transactionStatus); + } catch (final Exception e) { + return PaymentStatus.UNKNOWN; + } + } + + private static SubscriptionStatus getSubscriptionStatus(final Subscription.Status status) { + return switch (status) { + case ACTIVE -> SubscriptionStatus.ACTIVE; + case CANCELED, EXPIRED -> SubscriptionStatus.CANCELED; + case PAST_DUE -> SubscriptionStatus.PAST_DUE; + case PENDING -> SubscriptionStatus.INCOMPLETE; + case UNRECOGNIZED -> { + logger.error("Subscription has unrecognized status; library may need to be updated: {}", status); + yield SubscriptionStatus.UNKNOWN; + } + }; + } + + private BigDecimal convertApiAmountToBraintreeAmount(final String currency, final long amount) { + return switch (currency.toLowerCase(Locale.ROOT)) { + // JPY is the only supported zero-decimal currency + case "jpy" -> BigDecimal.valueOf(amount); + default -> BigDecimal.valueOf(amount).scaleByPowerOfTen(-2); + }; + } + + public record PayPalOneTimePaymentApprovalDetails(String approvalUrl, String paymentId) { + + } + + public record PayPalChargeSuccessDetails(String paymentId) { + + } + + @Override + public CompletableFuture createCustomer(final byte[] subscriberUser) { + return CompletableFuture.supplyAsync(() -> { + final CustomerRequest request = new CustomerRequest() + .customField("subscriber_user", HexFormat.of().formatHex(subscriberUser)); + try { + return braintreeGateway.customer().create(request); + } catch (BraintreeException e) { + throw new CompletionException(e); + } + }, executor) + .thenApply(result -> { + if (!result.isSuccess()) { + throw new CompletionException(new BraintreeException(result.getMessage())); + } + + return new ProcessorCustomer(result.getTarget().getId(), SubscriptionProcessor.BRAINTREE); + }); + + } + + @Override + public CompletableFuture createPaymentMethodSetupToken(final String customerId) { + return CompletableFuture.supplyAsync(() -> { + ClientTokenRequest request = new ClientTokenRequest() + .customerId(customerId); + + return braintreeGateway.clientToken().generate(request); + }, executor); + } + + @Override + public CompletableFuture setDefaultPaymentMethodForCustomer(String customerId, String billingAgreementToken, + @Nullable String currentSubscriptionId) { + final Optional maybeSubscriptionId = Optional.ofNullable(currentSubscriptionId); + return braintreeGraphqlClient.tokenizePayPalBillingAgreement(billingAgreementToken) + .thenCompose(tokenizePayPalBillingAgreement -> + braintreeGraphqlClient.vaultPaymentMethod(customerId, tokenizePayPalBillingAgreement.paymentMethod.id)) + .thenApplyAsync(vaultPaymentMethod -> braintreeGateway.customer() + .update(customerId, new CustomerRequest() + .defaultPaymentMethodToken(vaultPaymentMethod.paymentMethod.id)), + executor) + .thenAcceptAsync(result -> { + maybeSubscriptionId.ifPresent( + subscriptionId -> braintreeGateway.subscription() + .update(subscriptionId, new SubscriptionRequest() + .paymentMethodToken(result.getTarget().getDefaultPaymentMethod().getToken()))); + }, executor); + } + + @Override + public CompletableFuture getSubscription(String subscriptionId) { + return CompletableFuture.supplyAsync(() -> braintreeGateway.subscription().find(subscriptionId), executor); + } + + @Override + public CompletableFuture createSubscription(String customerId, String planId, long level, + long lastSubscriptionCreatedAt) { + + return getDefaultPaymentMethod(customerId) + .thenCompose(paymentMethod -> { + if (paymentMethod == null) { + throw new ClientErrorException(Response.Status.CONFLICT); + } + + final Optional maybeExistingSubscription = paymentMethod.getSubscriptions().stream() + .filter(sub -> sub.getStatus().equals(Subscription.Status.ACTIVE)) + .filter(Subscription::neverExpires) + .findAny(); + + return maybeExistingSubscription.map(subscription -> findPlan(subscription.getPlanId()) + .thenApply(plan -> { + if (getLevelForPlan(plan) != level) { + // if this happens, the likely cause is retrying an apparently failed request (likely some sort of timeout or network interruption) + // with a different level. + // In this case, it’s safer and easier to recover by returning this subscription, rather than + // returning an error + logger.warn("existing subscription had unexpected level"); + } + return subscription; + })) + .orElseGet(() -> findPlan(planId).thenApplyAsync(plan -> { + final Result result = braintreeGateway.subscription().create(new SubscriptionRequest() + .planId(planId) + .paymentMethodToken(paymentMethod.getToken()) + .merchantAccountId( + currenciesToMerchantAccounts.get(plan.getCurrencyIsoCode().toLowerCase(Locale.ROOT))) + .options() + .startImmediately(true) + .done() + ); + + if (!result.isSuccess()) { + final CompletionException completionException; + if (result.getTarget() != null) { + completionException = result.getTarget().getTransactions().stream().findFirst() + .map(transaction -> new CompletionException( + new SubscriptionProcessorException(getProcessor(), createChargeFailure(transaction)))) + .orElseGet(() -> new CompletionException(new BraintreeException(result.getMessage()))); + } else { + completionException = new CompletionException(new BraintreeException(result.getMessage())); + } + + throw completionException; + } + + return result.getTarget(); + })); + }).thenApply(subscription -> new SubscriptionId(subscription.getId())); + } + + private CompletableFuture getDefaultPaymentMethod(String customerId) { + return CompletableFuture.supplyAsync(() -> braintreeGateway.customer().find(customerId).getDefaultPaymentMethod(), + executor); + } + + + @Override + public CompletableFuture updateSubscription(Object subscriptionObj, String planId, long level, + String idempotencyKey) { + + if (!(subscriptionObj instanceof final Subscription subscription)) { + throw new IllegalArgumentException("invalid subscription object: " + subscriptionObj.getClass().getName()); + } + + // since badge redemption is untrackable by design and unrevokable, subscription changes must be immediate and + // not prorated. Braintree subscriptions cannot change their next billing date, + // so we must end the existing one and create a new one + return cancelSubscriptionAtEndOfCurrentPeriod(subscription) + .thenCompose(ignored -> { + + final Transaction transaction = getLatestTransactionForSubscription(subscription).orElseThrow( + () -> new ClientErrorException( + Response.Status.CONFLICT)); + + final Customer customer = transaction.getCustomer(); + + return createSubscription(customer.getId(), planId, level, + subscription.getCreatedAt().toInstant().getEpochSecond()); + }); + } + + @Override + public CompletableFuture getLevelAndCurrencyForSubscription(Object subscriptionObj) { + final Subscription subscription = getSubscription(subscriptionObj); + + return findPlan(subscription.getPlanId()) + .thenApply( + plan -> new LevelAndCurrency(getLevelForPlan(plan), plan.getCurrencyIsoCode().toLowerCase(Locale.ROOT))); + + } + + private CompletableFuture findPlan(String planId) { + return CompletableFuture.supplyAsync(() -> braintreeGateway.plan().find(planId), executor); + } + + private long getLevelForPlan(final Plan plan) { + final BraintreePlanMetadata metadata; + try { + metadata = SystemMapper.jsonMapper().readValue(plan.getDescription(), BraintreePlanMetadata.class); + + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + return metadata.level(); + } + + @Override + public CompletableFuture getSubscriptionInformation(Object subscriptionObj) { + final Subscription subscription = getSubscription(subscriptionObj); + + return CompletableFuture.supplyAsync(() -> { + + final Plan plan = braintreeGateway.plan().find(subscription.getPlanId()); + + final long level = getLevelForPlan(plan); + + final Instant anchor = subscription.getFirstBillingDate().toInstant(); + final Instant endOfCurrentPeriod = subscription.getBillingPeriodEndDate().toInstant(); + + boolean paymentProcessing = false; + ChargeFailure chargeFailure = null; + + final Optional latestTransaction = getLatestTransactionForSubscription(subscription); + + if (latestTransaction.isPresent()){ + paymentProcessing = isPaymentProcessing(latestTransaction.get().getStatus()); + if (!getPaymentStatus(latestTransaction.get().getStatus()).equals(PaymentStatus.SUCCEEDED)) { + chargeFailure = createChargeFailure(latestTransaction.get()); + } + } + + return new SubscriptionInformation( + new SubscriptionPrice(plan.getCurrencyIsoCode().toUpperCase(Locale.ROOT), + SubscriptionCurrencyUtil.convertBraintreeAmountToApiAmount(plan.getCurrencyIsoCode(), plan.getPrice())), + level, + anchor, + endOfCurrentPeriod, + Subscription.Status.ACTIVE == subscription.getStatus(), + !subscription.neverExpires(), + getSubscriptionStatus(subscription.getStatus()), + latestTransaction.map(this::getPaymentMethodFromTransaction).orElse(PaymentMethod.PAYPAL), + paymentProcessing, + chargeFailure + ); + }, executor); + } + + private PaymentMethod getPaymentMethodFromTransaction(Transaction transaction) { + if (transaction.getPayPalDetails() != null) { + return PaymentMethod.PAYPAL; + } + logger.error("Unexpected payment method from Braintree: {}, transaction id {}", transaction.getPaymentInstrumentType(), transaction.getId()); + return PaymentMethod.UNKNOWN; + } + + private static boolean isPaymentProcessing(final Transaction.Status status) { + return status == Transaction.Status.SETTLEMENT_PENDING; + } + + private ChargeFailure createChargeFailure(Transaction transaction) { + + final String code; + final String message; + if (transaction.getProcessorResponseCode() != null) { + code = transaction.getProcessorResponseCode(); + message = transaction.getProcessorResponseText(); + } else if (transaction.getGatewayRejectionReason() != null) { + code = "gateway"; + message = transaction.getGatewayRejectionReason().toString(); + } else { + code = "unknown"; + message = "unknown"; + } + + return new ChargeFailure( + code, + message, + null, + null, + null); + } + + @Override + public CompletableFuture cancelAllActiveSubscriptions(String customerId) { + + return CompletableFuture.supplyAsync(() -> braintreeGateway.customer().find(customerId), executor).thenCompose(customer -> { + + final List> subscriptionCancelFutures = Optional.ofNullable(customer.getDefaultPaymentMethod()) + .map(com.braintreegateway.PaymentMethod::getSubscriptions) + .orElse(Collections.emptyList()) + .stream() + .map(this::cancelSubscriptionAtEndOfCurrentPeriod) + .toList(); + + return CompletableFuture.allOf(subscriptionCancelFutures.toArray(new CompletableFuture[0])); + }); + } + + private CompletableFuture cancelSubscriptionAtEndOfCurrentPeriod(Subscription subscription) { + return CompletableFuture.supplyAsync(() -> { + braintreeGateway.subscription().update(subscription.getId(), + new SubscriptionRequest().numberOfBillingCycles(subscription.getCurrentBillingCycle())); + return null; + }, executor); + } + + + @Override + public CompletableFuture getReceiptItem(String subscriptionId) { + + return getLatestTransactionForSubscription(subscriptionId).thenApply(maybeTransaction -> maybeTransaction.map(transaction -> { + + if (!getPaymentStatus(transaction.getStatus()).equals(PaymentStatus.SUCCEEDED)) { + throw new WebApplicationException(Response.Status.PAYMENT_REQUIRED); + } + + final Instant expiration = transaction.getSubscriptionDetails().getBillingPeriodEndDate().toInstant(); + final Plan plan = braintreeGateway.plan().find(transaction.getPlanId()); + + final BraintreePlanMetadata metadata; + try { + metadata = SystemMapper.jsonMapper().readValue(plan.getDescription(), BraintreePlanMetadata.class); + + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + return new ReceiptItem(transaction.getId(), expiration, metadata.level()); + + }).orElseThrow(() -> new WebApplicationException(Response.Status.NO_CONTENT))); + } + + private static Subscription getSubscription(Object subscriptionObj) { + if (!(subscriptionObj instanceof final Subscription subscription)) { + throw new IllegalArgumentException("Invalid subscription object: " + subscriptionObj.getClass().getName()); + } + return subscription; + } + + public CompletableFuture> getLatestTransactionForSubscription(String subscriptionId) { + return getSubscription(subscriptionId) + .thenApply(BraintreeManager::getSubscription) + .thenApply(this::getLatestTransactionForSubscription); + } + + private Optional getLatestTransactionForSubscription(Subscription subscription) { + return subscription.getTransactions().stream() + .max(Comparator.comparing(Transaction::getCreatedAt)); + } + + public CompletableFuture createPayPalBillingAgreement(final String returnUrl, + final String cancelUrl, final String locale) { + return braintreeGraphqlClient.createPayPalBillingAgreement(returnUrl, cancelUrl, locale) + .thenApply(response -> + new PayPalBillingAgreementApprovalDetails((String) response.approvalUrl, response.billingAgreementToken) + ); + } + + public record PayPalBillingAgreementApprovalDetails(String approvalUrl, String billingAgreementToken) { + + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreePlanMetadata.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreePlanMetadata.java new file mode 100644 index 000000000..ab60cf699 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreePlanMetadata.java @@ -0,0 +1,10 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +public record BraintreePlanMetadata(long level) { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/ChargeFailure.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/ChargeFailure.java new file mode 100644 index 000000000..55b56bd2a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/ChargeFailure.java @@ -0,0 +1,13 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +import javax.annotation.Nullable; + +public record ChargeFailure(String code, String message, @Nullable String outcomeNetworkStatus, + @Nullable String outcomeReason, @Nullable String outcomeType) { + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/PaymentMethod.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/PaymentMethod.java new file mode 100644 index 000000000..2dfa09d92 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/PaymentMethod.java @@ -0,0 +1,26 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +public enum PaymentMethod { + UNKNOWN, + /** + * A credit card or debit card, including those from Apple Pay and Google Pay + */ + CARD, + /** + * A PayPal account + */ + PAYPAL, + /** + * A SEPA debit account + */ + SEPA_DEBIT, + /** + * An iDEAL account + */ + IDEAL, +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/ProcessorCustomer.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/ProcessorCustomer.java new file mode 100644 index 000000000..c994439c3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/ProcessorCustomer.java @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +import java.nio.charset.StandardCharsets; + +public record ProcessorCustomer(String customerId, SubscriptionProcessor processor) { + + public byte[] toDynamoBytes() { + final byte[] customerIdBytes = customerId.getBytes(StandardCharsets.UTF_8); + final byte[] combinedBytes = new byte[customerIdBytes.length + 1]; + + combinedBytes[0] = processor.getId(); + System.arraycopy(customerIdBytes, 0, combinedBytes, 1, customerIdBytes.length); + + return combinedBytes; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java new file mode 100644 index 000000000..35b6fa921 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java @@ -0,0 +1,655 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.stripe.StripeClient; +import com.stripe.exception.CardException; +import com.stripe.exception.StripeException; +import com.stripe.model.Charge; +import com.stripe.model.Customer; +import com.stripe.model.Invoice; +import com.stripe.model.InvoiceLineItem; +import com.stripe.model.PaymentIntent; +import com.stripe.model.Price; +import com.stripe.model.Product; +import com.stripe.model.SetupIntent; +import com.stripe.model.StripeCollection; +import com.stripe.model.Subscription; +import com.stripe.model.SubscriptionItem; +import com.stripe.net.RequestOptions; +import com.stripe.param.CustomerCreateParams; +import com.stripe.param.CustomerRetrieveParams; +import com.stripe.param.CustomerUpdateParams; +import com.stripe.param.CustomerUpdateParams.InvoiceSettings; +import com.stripe.param.InvoiceListParams; +import com.stripe.param.PaymentIntentCreateParams; +import com.stripe.param.PriceRetrieveParams; +import com.stripe.param.SetupIntentCreateParams; +import com.stripe.param.SubscriptionCancelParams; +import com.stripe.param.SubscriptionCreateParams; +import com.stripe.param.SubscriptionItemListParams; +import com.stripe.param.SubscriptionListParams; +import com.stripe.param.SubscriptionRetrieveParams; +import com.stripe.param.SubscriptionUpdateParams; +import com.stripe.param.SubscriptionUpdateParams.BillingCycleAnchor; +import com.stripe.param.SubscriptionUpdateParams.ProrationBehavior; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HexFormat; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.Conversions; + +public class StripeManager implements SubscriptionProcessorManager { + private static final Logger logger = LoggerFactory.getLogger(StripeManager.class); + private static final String METADATA_KEY_LEVEL = "level"; + + private final StripeClient stripeClient; + private final Executor executor; + private final byte[] idempotencyKeyGenerator; + private final String boostDescription; + private final Map> supportedCurrenciesByPaymentMethod; + + public StripeManager( + @Nonnull String apiKey, + @Nonnull Executor executor, + @Nonnull byte[] idempotencyKeyGenerator, + @Nonnull String boostDescription, + @Nonnull Map> supportedCurrenciesByPaymentMethod) { + if (Strings.isNullOrEmpty(apiKey)) { + throw new IllegalArgumentException("apiKey cannot be empty"); + } + this.stripeClient = new StripeClient(apiKey); + this.executor = Objects.requireNonNull(executor); + this.idempotencyKeyGenerator = Objects.requireNonNull(idempotencyKeyGenerator); + if (idempotencyKeyGenerator.length == 0) { + throw new IllegalArgumentException("idempotencyKeyGenerator cannot be empty"); + } + this.boostDescription = Objects.requireNonNull(boostDescription); + this.supportedCurrenciesByPaymentMethod = supportedCurrenciesByPaymentMethod; + } + + @Override + public SubscriptionProcessor getProcessor() { + return SubscriptionProcessor.STRIPE; + } + + @Override + public boolean supportsPaymentMethod(PaymentMethod paymentMethod) { + return paymentMethod == PaymentMethod.CARD + || paymentMethod == PaymentMethod.SEPA_DEBIT + || paymentMethod == PaymentMethod.IDEAL; + } + + private RequestOptions commonOptions() { + return commonOptions(null); + } + + private RequestOptions commonOptions(@Nullable String idempotencyKey) { + return RequestOptions.builder() + .setIdempotencyKey(idempotencyKey) + .build(); + } + + @Override + public CompletableFuture createCustomer(byte[] subscriberUser) { + return CompletableFuture.supplyAsync(() -> { + CustomerCreateParams params = CustomerCreateParams.builder() + .putMetadata("subscriberUser", HexFormat.of().formatHex(subscriberUser)) + .build(); + try { + return stripeClient.customers() + .create(params, commonOptions(generateIdempotencyKeyForSubscriberUser(subscriberUser))); + } catch (StripeException e) { + throw new CompletionException(e); + } + }, executor) + .thenApply(customer -> new ProcessorCustomer(customer.getId(), getProcessor())); + } + + public CompletableFuture getCustomer(String customerId) { + return CompletableFuture.supplyAsync(() -> { + CustomerRetrieveParams params = CustomerRetrieveParams.builder().build(); + try { + return stripeClient.customers().retrieve(customerId, params, commonOptions()); + } catch (StripeException e) { + throw new CompletionException(e); + } + }, executor); + } + + @Override + public CompletableFuture setDefaultPaymentMethodForCustomer(String customerId, String paymentMethodId, + @Nullable String currentSubscriptionId) { + return CompletableFuture.supplyAsync(() -> { + CustomerUpdateParams params = CustomerUpdateParams.builder() + .setInvoiceSettings(InvoiceSettings.builder() + .setDefaultPaymentMethod(paymentMethodId) + .build()) + .build(); + try { + stripeClient.customers().update(customerId, params, commonOptions()); + return null; + } catch (StripeException e) { + throw new CompletionException(e); + } + }, executor); + } + + @Override + public CompletableFuture createPaymentMethodSetupToken(String customerId) { + return CompletableFuture.supplyAsync(() -> { + SetupIntentCreateParams params = SetupIntentCreateParams.builder() + .setCustomer(customerId) + .build(); + try { + return stripeClient.setupIntents().create(params, commonOptions()); + } catch (StripeException e) { + throw new CompletionException(e); + } + }, executor) + .thenApply(SetupIntent::getClientSecret); + } + + @Override + public Set getSupportedCurrenciesForPaymentMethod(final PaymentMethod paymentMethod) { + return supportedCurrenciesByPaymentMethod.getOrDefault(paymentMethod, Collections.emptySet()); + } + + /** + * Creates a payment intent. May throw a 400 WebApplicationException if the amount is too small. + */ + public CompletableFuture createPaymentIntent(String currency, long amount, long level) { + return CompletableFuture.supplyAsync(() -> { + PaymentIntentCreateParams params = PaymentIntentCreateParams.builder() + .setAmount(amount) + .setCurrency(currency.toLowerCase(Locale.ROOT)) + .setDescription(boostDescription) + .putMetadata("level", Long.toString(level)) + .build(); + try { + return stripeClient.paymentIntents().create(params, commonOptions()); + } catch (StripeException e) { + if ("amount_too_small".equalsIgnoreCase(e.getCode())) { + throw new WebApplicationException(Response + .status(Status.BAD_REQUEST) + .entity(Map.of("error", "amount_too_small")) + .build()); + } else { + throw new CompletionException(e); + } + } + }, executor); + } + + public CompletableFuture getPaymentDetails(String paymentIntentId) { + return CompletableFuture.supplyAsync(() -> { + try { + final PaymentIntent paymentIntent = stripeClient.paymentIntents().retrieve(paymentIntentId, commonOptions()); + + return new PaymentDetails(paymentIntent.getId(), + paymentIntent.getMetadata() == null ? Collections.emptyMap() : paymentIntent.getMetadata(), + getPaymentStatusForStatus(paymentIntent.getStatus()), + Instant.ofEpochSecond(paymentIntent.getCreated())); + } catch (StripeException e) { + if (e.getStatusCode() == 404) { + return null; + } else { + throw new CompletionException(e); + } + } + }, executor); + } + + private static PaymentStatus getPaymentStatusForStatus(String status) { + return switch (status.toLowerCase(Locale.ROOT)) { + case "processing" -> PaymentStatus.PROCESSING; + case "succeeded" -> PaymentStatus.SUCCEEDED; + default -> PaymentStatus.UNKNOWN; + }; + } + + private static SubscriptionStatus getSubscriptionStatus(final String status) { + return SubscriptionStatus.forApiValue(status); + } + + @Override + public CompletableFuture createSubscription(String customerId, String priceId, long level, + long lastSubscriptionCreatedAt) { + // this relies on Stripe's idempotency key to avoid creating more than one subscription if the client + // retries this request + return CompletableFuture.supplyAsync(() -> { + SubscriptionCreateParams params = SubscriptionCreateParams.builder() + .setCustomer(customerId) + .setOffSession(true) + .setPaymentBehavior(SubscriptionCreateParams.PaymentBehavior.ERROR_IF_INCOMPLETE) + .addItem(SubscriptionCreateParams.Item.builder() + .setPrice(priceId) + .build()) + .putMetadata(METADATA_KEY_LEVEL, Long.toString(level)) + .build(); + try { + // the idempotency key intentionally excludes priceId + // + // If the client tells the server several times in a row before the initial creation of a subscription to + // create a subscription, we want to ensure only one gets created. + return stripeClient.subscriptions() + .create(params, commonOptions(generateIdempotencyKeyForCreateSubscription( + customerId, lastSubscriptionCreatedAt))); + } catch (StripeException e) { + + if (e instanceof CardException ce) { + throw new CompletionException(new SubscriptionProcessorException(getProcessor(), + new ChargeFailure( + StringUtils.defaultIfBlank(ce.getDeclineCode(), ce.getCode()), + e.getStripeError().getMessage(), + null, + null, + null + ))); + } + + throw new CompletionException(e); + } + }, executor) + .thenApply(subscription -> new SubscriptionId(subscription.getId())); + } + + @Override + public CompletableFuture updateSubscription( + Object subscriptionObj, String priceId, long level, String idempotencyKey) { + + final Subscription subscription = getSubscription(subscriptionObj); + + return CompletableFuture.supplyAsync(() -> { + List items = new ArrayList<>(); + try { + final StripeCollection subscriptionItems = stripeClient.subscriptionItems() + .list(SubscriptionItemListParams.builder().setSubscription(subscription.getId()).build(), + commonOptions()); + for (final SubscriptionItem item : subscriptionItems.autoPagingIterable()) { + items.add(SubscriptionUpdateParams.Item.builder() + .setId(item.getId()) + .setDeleted(true) + .build()); + } + items.add(SubscriptionUpdateParams.Item.builder() + .setPrice(priceId) + .build()); + SubscriptionUpdateParams params = SubscriptionUpdateParams.builder() + .putMetadata(METADATA_KEY_LEVEL, Long.toString(level)) + + // since badge redemption is untrackable by design and unrevokable, subscription changes must be immediate and + // not prorated + .setProrationBehavior(ProrationBehavior.NONE) + .setBillingCycleAnchor(BillingCycleAnchor.NOW) + .setOffSession(true) + .setPaymentBehavior(SubscriptionUpdateParams.PaymentBehavior.ERROR_IF_INCOMPLETE) + .addAllItem(items) + .build(); + return stripeClient.subscriptions().update(subscription.getId(), params, + commonOptions( + generateIdempotencyKeyForSubscriptionUpdate(subscription.getCustomer(), idempotencyKey))); + } catch (StripeException e) { + throw new CompletionException(e); + } + }, executor) + .thenApply(subscription1 -> new SubscriptionId(subscription1.getId())); + } + + public CompletableFuture getSubscription(String subscriptionId) { + return CompletableFuture.supplyAsync(() -> { + SubscriptionRetrieveParams params = SubscriptionRetrieveParams.builder() + .addExpand("latest_invoice") + .addExpand("latest_invoice.charge") + .build(); + try { + return stripeClient.subscriptions().retrieve(subscriptionId, params, commonOptions()); + } catch (StripeException e) { + throw new CompletionException(e); + } + }, executor); + } + + public CompletableFuture cancelAllActiveSubscriptions(String customerId) { + return getCustomer(customerId).thenCompose(customer -> { + if (customer == null) { + throw new InternalServerErrorException( + "no customer record found for id " + customerId); + } + return listNonCanceledSubscriptions(customer); + }).thenCompose(subscriptions -> { + @SuppressWarnings("unchecked") + CompletableFuture[] futures = (CompletableFuture[]) subscriptions.stream() + .map(this::cancelSubscriptionAtEndOfCurrentPeriod).toArray(CompletableFuture[]::new); + return CompletableFuture.allOf(futures); + }); + } + + public CompletableFuture> listNonCanceledSubscriptions(Customer customer) { + return CompletableFuture.supplyAsync(() -> { + SubscriptionListParams params = SubscriptionListParams.builder() + .setCustomer(customer.getId()) + .build(); + try { + return Lists.newArrayList( + stripeClient.subscriptions().list(params, commonOptions()).autoPagingIterable(null, commonOptions())); + } catch (StripeException e) { + throw new CompletionException(e); + } + }, executor); + } + + public CompletableFuture cancelSubscriptionImmediately(Subscription subscription) { + return CompletableFuture.supplyAsync(() -> { + SubscriptionCancelParams params = SubscriptionCancelParams.builder().build(); + try { + return stripeClient.subscriptions().cancel(subscription.getId(), params, commonOptions()); + } catch (StripeException e) { + throw new CompletionException(e); + } + }, executor); + } + + public CompletableFuture cancelSubscriptionAtEndOfCurrentPeriod(Subscription subscription) { + return CompletableFuture.supplyAsync(() -> { + SubscriptionUpdateParams params = SubscriptionUpdateParams.builder() + .setCancelAtPeriodEnd(true) + .build(); + try { + return stripeClient.subscriptions().update(subscription.getId(), params, commonOptions()); + } catch (StripeException e) { + throw new CompletionException(e); + } + }, executor); + } + + public CompletableFuture> getItemsForSubscription(Subscription subscription) { + return CompletableFuture.supplyAsync( + () -> { + try { + final StripeCollection subscriptionItems = stripeClient.subscriptionItems().list( + SubscriptionItemListParams.builder().setSubscription(subscription.getId()).build(), commonOptions()); + return Lists.newArrayList(subscriptionItems.autoPagingIterable(null, commonOptions())); + + } catch (final StripeException e) { + throw new CompletionException(e); + } + }, + executor); + } + + public CompletableFuture getPriceForSubscription(Subscription subscription) { + return getItemsForSubscription(subscription).thenApply(subscriptionItems -> { + if (subscriptionItems.isEmpty()) { + throw new IllegalStateException("no items found in subscription " + subscription.getId()); + } else if (subscriptionItems.size() > 1) { + throw new IllegalStateException( + "too many items found in subscription " + subscription.getId() + "; items=" + subscriptionItems.size()); + } else { + return subscriptionItems.stream().findAny().get().getPrice(); + } + }); + } + + private CompletableFuture getProductForSubscription(Subscription subscription) { + return getPriceForSubscription(subscription).thenCompose(price -> getProductForPrice(price.getId())); + } + + @Override + public CompletableFuture getLevelAndCurrencyForSubscription(Object subscriptionObj) { + final Subscription subscription = getSubscription(subscriptionObj); + + return getProductForSubscription(subscription).thenApply( + product -> new LevelAndCurrency(getLevelForProduct(product), subscription.getCurrency().toLowerCase( + Locale.ROOT))); + } + + public CompletableFuture getLevelForPrice(Price price) { + return getProductForPrice(price.getId()).thenApply(this::getLevelForProduct); + } + + public CompletableFuture getProductForPrice(String priceId) { + return CompletableFuture.supplyAsync(() -> { + PriceRetrieveParams params = PriceRetrieveParams.builder().addExpand("product").build(); + try { + return stripeClient.prices().retrieve(priceId, params, commonOptions()).getProductObject(); + } catch (StripeException e) { + throw new CompletionException(e); + } + }, executor); + } + + public long getLevelForProduct(Product product) { + return Long.parseLong(product.getMetadata().get(METADATA_KEY_LEVEL)); + } + + /** + * Returns the paid invoices within the past 90 days for a subscription ordered by the creation date in descending + * order (latest first). + */ + public CompletableFuture> getPaidInvoicesForSubscription(String subscriptionId, Instant now) { + return CompletableFuture.supplyAsync(() -> { + InvoiceListParams params = InvoiceListParams.builder() + .setSubscription(subscriptionId) + .setStatus(InvoiceListParams.Status.PAID) + .setCreated(InvoiceListParams.Created.builder() + .setGte(now.minus(Duration.ofDays(90)).getEpochSecond()) + .build()) + .build(); + try { + ArrayList invoices = Lists.newArrayList(stripeClient.invoices().list(params, commonOptions()) + .autoPagingIterable(null, commonOptions())); + invoices.sort(Comparator.comparingLong(Invoice::getCreated).reversed()); + return invoices; + } catch (StripeException e) { + throw new CompletionException(e); + } + }, executor); + } + + @Override + public CompletableFuture getSubscriptionInformation(Object subscriptionObj) { + + final Subscription subscription = getSubscription(subscriptionObj); + + return getPriceForSubscription(subscription).thenCompose(price -> + getLevelForPrice(price).thenApply(level -> { + ChargeFailure chargeFailure = null; + boolean paymentProcessing = false; + PaymentMethod paymentMethod = null; + + if (subscription.getLatestInvoiceObject() != null) { + final Invoice invoice = subscription.getLatestInvoiceObject(); + paymentProcessing = "open".equals(invoice.getStatus()); + + if (invoice.getChargeObject() != null) { + final Charge charge = invoice.getChargeObject(); + if (charge.getFailureCode() != null || charge.getFailureMessage() != null) { + Charge.Outcome outcome = charge.getOutcome(); + chargeFailure = new ChargeFailure( + charge.getFailureCode(), + charge.getFailureMessage(), + outcome != null ? outcome.getNetworkStatus() : null, + outcome != null ? outcome.getReason() : null, + outcome != null ? outcome.getType() : null); + } + + if (charge.getPaymentMethodDetails() != null + && charge.getPaymentMethodDetails().getType() != null) { + paymentMethod = getPaymentMethodFromStripeString(charge.getPaymentMethodDetails().getType(), invoice.getId()); + } + } + } + + return new SubscriptionInformation( + new SubscriptionPrice(price.getCurrency().toUpperCase(Locale.ROOT), price.getUnitAmountDecimal()), + level, + Instant.ofEpochSecond(subscription.getBillingCycleAnchor()), + Instant.ofEpochSecond(subscription.getCurrentPeriodEnd()), + Objects.equals(subscription.getStatus(), "active"), + subscription.getCancelAtPeriodEnd(), + getSubscriptionStatus(subscription.getStatus()), + paymentMethod, + paymentProcessing, + chargeFailure + ); + })); + } + + private static PaymentMethod getPaymentMethodFromStripeString(final String paymentMethodString, final String invoiceId) { + return switch (paymentMethodString) { + case "sepa_debit" -> PaymentMethod.SEPA_DEBIT; + case "card" -> PaymentMethod.CARD; + default -> { + logger.error("Unexpected payment method from Stripe: {}, invoice id: {}", paymentMethodString, invoiceId); + yield PaymentMethod.UNKNOWN; + } + }; + } + + private Subscription getSubscription(Object subscriptionObj) { + if (!(subscriptionObj instanceof final Subscription subscription)) { + throw new IllegalArgumentException("invalid subscription object: " + subscriptionObj.getClass().getName()); + } + + return subscription; + } + + @Override + public CompletableFuture getReceiptItem(String subscriptionId) { + return getLatestInvoiceForSubscription(subscriptionId) + .thenCompose(invoice -> convertInvoiceToReceipt(invoice, subscriptionId)); + } + + public CompletableFuture getLatestInvoiceForSubscription(String subscriptionId) { + return CompletableFuture.supplyAsync(() -> { + SubscriptionRetrieveParams params = SubscriptionRetrieveParams.builder() + .addExpand("latest_invoice") + .build(); + try { + return stripeClient.subscriptions().retrieve(subscriptionId, params, commonOptions()).getLatestInvoiceObject(); + } catch (StripeException e) { + throw new CompletionException(e); + } + }, executor); + } + + private CompletableFuture convertInvoiceToReceipt(Invoice latestSubscriptionInvoice, String subscriptionId) { + if (latestSubscriptionInvoice == null) { + throw new WebApplicationException(Status.NO_CONTENT); + } + if (StringUtils.equalsIgnoreCase("open", latestSubscriptionInvoice.getStatus())) { + throw new WebApplicationException(Status.NO_CONTENT); + } + if (!StringUtils.equalsIgnoreCase("paid", latestSubscriptionInvoice.getStatus())) { + throw new WebApplicationException(Status.PAYMENT_REQUIRED); + } + + return getInvoiceLineItemsForInvoice(latestSubscriptionInvoice).thenCompose(invoiceLineItems -> { + Collection subscriptionLineItems = invoiceLineItems.stream() + .filter(invoiceLineItem -> Objects.equals("subscription", invoiceLineItem.getType())) + .toList(); + if (subscriptionLineItems.isEmpty()) { + throw new IllegalStateException("latest subscription invoice has no subscription line items; subscriptionId=" + + subscriptionId + "; invoiceId=" + latestSubscriptionInvoice.getId()); + } + if (subscriptionLineItems.size() > 1) { + throw new IllegalStateException( + "latest subscription invoice has too many subscription line items; subscriptionId=" + subscriptionId + + "; invoiceId=" + latestSubscriptionInvoice.getId() + "; count=" + subscriptionLineItems.size()); + } + + InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get(); + return getReceiptForSubscriptionInvoiceLineItem(subscriptionLineItem); + }); + } + + private CompletableFuture getReceiptForSubscriptionInvoiceLineItem(InvoiceLineItem subscriptionLineItem) { + return getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new ReceiptItem( + subscriptionLineItem.getId(), + Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd()), + getLevelForProduct(product))); + } + + public CompletableFuture> getInvoiceLineItemsForInvoice(Invoice invoice) { + return CompletableFuture.supplyAsync( + () -> { + try { + final StripeCollection lineItems = stripeClient.invoices().lineItems() + .list(invoice.getId(), commonOptions()); + return Lists.newArrayList(lineItems.autoPagingIterable(null, commonOptions())); + } catch (final StripeException e) { + throw new CompletionException(e); + } + }, executor); + } + + /** + * We use a client generated idempotency key for subscription updates due to not being able to distinguish between a + * call to update to level 2, then back to level 1, then back to level 2. If this all happens within Stripe's + * idempotency window the subsequent update call would not happen unless we get some indication from the client that + * it is intentionally sending a repeat of the update to level 2 request because user is changing again, so in this + * case we derive idempotency from the client. + */ + private String generateIdempotencyKeyForSubscriptionUpdate(String customerId, String idempotencyKey) { + return generateIdempotencyKey("subscriptionUpdate", mac -> { + mac.update(customerId.getBytes(StandardCharsets.UTF_8)); + mac.update(idempotencyKey.getBytes(StandardCharsets.UTF_8)); + }); + } + + private String generateIdempotencyKeyForSubscriberUser(byte[] subscriberUser) { + return generateIdempotencyKey("subscriberUser", mac -> mac.update(subscriberUser)); + } + + private String generateIdempotencyKeyForCreateSubscription(String customerId, long lastSubscriptionCreatedAt) { + return generateIdempotencyKey("customerId", mac -> { + mac.update(customerId.getBytes(StandardCharsets.UTF_8)); + mac.update(Conversions.longToByteArray(lastSubscriptionCreatedAt)); + }); + } + + private String generateIdempotencyKey(String type, Consumer byteConsumer) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(idempotencyKeyGenerator, "HmacSHA256")); + mac.update(type.getBytes(StandardCharsets.UTF_8)); + byteConsumer.accept(mac); + return Base64.getUrlEncoder().encodeToString(mac.doFinal()); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new AssertionError(e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionCurrencyUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionCurrencyUtil.java new file mode 100644 index 000000000..d3c943cac --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionCurrencyUtil.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +import java.math.BigDecimal; +import java.util.Locale; +import java.util.Set; + +/** + * Utility for scaling amounts among Stripe, Braintree, configuration, and API responses. + *

    + * In general, the API input and output follow’s Stripe’s specification to use amounts in a currency’s + * smallest unit. The exception is configuration APIs, which return values in the currency’s primary unit. Braintree + * uses the currency’s primary unit for its input and output. + *

    Examples

    + * + * + * API + * + * + * + * + * + * + * + * + * + *
    Currency, AmountStripeBraintree
    USD 4.994994994.99
    JPY 501501501501
    + */ +public class SubscriptionCurrencyUtil { + + // This list was taken from https://stripe.com/docs/currencies?presentment-currency=US + // Braintree + private static final Set stripeZeroDecimalCurrencies = Set.of("bif", "clp", "djf", "gnf", "jpy", "kmf", "krw", + "mga", "pyg", "rwf", "vnd", "vuv", "xaf", "xof", "xpf"); + + + /** + * Takes an amount as configured and turns it into an amount as API clients (and Stripe) expect to see it. For + * instance, {@code USD 4.99} return {@code 499}, while {@code JPY 500} returns {@code 500}. + * + *

    + * Stripe appears to only support zero- and two-decimal currencies, but also has some backwards compatibility issues + * with 0 decimal currencies, so this is not to any ISO standard but rather directly from Stripe's API doc page. + */ + public static BigDecimal convertConfiguredAmountToApiAmount(String currency, BigDecimal configuredAmount) { + if (stripeZeroDecimalCurrencies.contains(currency.toLowerCase(Locale.ROOT))) { + return configuredAmount; + } + + return configuredAmount.scaleByPowerOfTen(2); + } + + /** + * @see org.whispersystems.textsecuregcm.subscriptions.SubscriptionCurrencyUtil#convertConfiguredAmountToApiAmount(String, + * BigDecimal) + */ + public static BigDecimal convertConfiguredAmountToStripeAmount(String currency, BigDecimal configuredAmount) { + return convertConfiguredAmountToApiAmount(currency, configuredAmount); + } + + /** + * Braintree’s API expects amounts in a currency’s primary unit (e.g. USD 4.99) + * + * @see org.whispersystems.textsecuregcm.subscriptions.SubscriptionCurrencyUtil#convertConfiguredAmountToApiAmount(String, + * BigDecimal) + */ + static BigDecimal convertBraintreeAmountToApiAmount(final String currency, final BigDecimal amount) { + return convertConfiguredAmountToApiAmount(currency, amount); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessor.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessor.java new file mode 100644 index 000000000..a76dc8593 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessor.java @@ -0,0 +1,49 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * A set of payment providers used for donations + */ +public enum SubscriptionProcessor { + // because provider IDs are stored, they should not be reused, and great care + // must be used if a provider is removed from the list + STRIPE(1), + BRAINTREE(2), + ; + + private static final Map IDS_TO_PROCESSORS = new HashMap<>(); + + static { + Arrays.stream(SubscriptionProcessor.values()) + .forEach(provider -> IDS_TO_PROCESSORS.put((int) provider.id, provider)); + } + + /** + * @return the provider associated with the given ID, or {@code null} if none exists + */ + public static SubscriptionProcessor forId(byte id) { + return IDS_TO_PROCESSORS.get((int) id); + } + + private final byte id; + + SubscriptionProcessor(int id) { + if (id > 255) { + throw new IllegalArgumentException("ID must fit in one byte: " + id); + } + + this.id = (byte) id; + } + + public byte getId() { + return id; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorException.java new file mode 100644 index 000000000..aa2aa2e7e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorException.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +public class SubscriptionProcessorException extends Exception { + + private final SubscriptionProcessor processor; + private final ChargeFailure chargeFailure; + + public SubscriptionProcessorException(final SubscriptionProcessor processor, + final ChargeFailure chargeFailure) { + this.processor = processor; + this.chargeFailure = chargeFailure; + } + + public SubscriptionProcessor getProcessor() { + return processor; + } + + public ChargeFailure getChargeFailure() { + return chargeFailure; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorManager.java new file mode 100644 index 000000000..e82d6e5bc --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorManager.java @@ -0,0 +1,164 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public interface SubscriptionProcessorManager { + SubscriptionProcessor getProcessor(); + + boolean supportsPaymentMethod(PaymentMethod paymentMethod); + + Set getSupportedCurrenciesForPaymentMethod(PaymentMethod paymentMethod); + + CompletableFuture getPaymentDetails(String paymentId); + + CompletableFuture createCustomer(byte[] subscriberUser); + + CompletableFuture createPaymentMethodSetupToken(String customerId); + + + /** + * @param customerId + * @param paymentMethodToken a processor-specific token necessary + * @param currentSubscriptionId (nullable) an active subscription ID, in case it needs an explicit update + * @return + */ + CompletableFuture setDefaultPaymentMethodForCustomer(String customerId, String paymentMethodToken, + @Nullable String currentSubscriptionId); + + CompletableFuture getSubscription(String subscriptionId); + + CompletableFuture createSubscription(String customerId, String templateId, long level, + long lastSubscriptionCreatedAt); + + CompletableFuture updateSubscription( + Object subscription, String templateId, long level, String idempotencyKey); + + /** + * @param subscription + * @return the subscription’s current level and lower-case currency code + */ + CompletableFuture getLevelAndCurrencyForSubscription(Object subscription); + + CompletableFuture cancelAllActiveSubscriptions(String customerId); + + CompletableFuture getReceiptItem(String subscriptionId); + + CompletableFuture getSubscriptionInformation(Object subscription); + + record PaymentDetails(String id, + Map customMetadata, + PaymentStatus status, + Instant created) { + + } + + enum PaymentStatus { + SUCCEEDED, + PROCESSING, + FAILED, + UNKNOWN, + } + + enum SubscriptionStatus { + /** + * The subscription is in good standing and the most recent payment was successful. + */ + ACTIVE("active"), + + /** + * Payment failed when creating the subscription, or the subscription’s start date is in the future. + */ + INCOMPLETE("incomplete"), + + /** + * Payment on the latest renewal either failed or wasn't attempted. + */ + PAST_DUE("past_due"), + + /** + * The subscription has been canceled. + */ + CANCELED("canceled"), + + /** + * The latest renewal hasn't been paid but the subscription remains in place. + */ + UNPAID("unpaid"), + + /** + * The status from the downstream processor is unknown. + */ + UNKNOWN("unknown"); + + + private final String apiValue; + + SubscriptionStatus(String apiValue) { + this.apiValue = apiValue; + } + + public static SubscriptionStatus forApiValue(String status) { + return switch (status) { + case "active" -> ACTIVE; + case "canceled", "incomplete_expired" -> CANCELED; + case "unpaid" -> UNPAID; + case "past_due" -> PAST_DUE; + case "incomplete" -> INCOMPLETE; + + case "trialing" -> { + final Logger logger = LoggerFactory.getLogger(SubscriptionProcessorManager.class); + logger.error("Subscription has status that should never happen: {}", status); + + yield UNKNOWN; + } + default -> { + final Logger logger = LoggerFactory.getLogger(SubscriptionProcessorManager.class); + logger.error("Subscription has unknown status: {}", status); + + yield UNKNOWN; + } + }; + } + + public String getApiValue() { + return apiValue; + } + } + + + record SubscriptionId(String id) { + + } + + record SubscriptionInformation(SubscriptionPrice price, long level, Instant billingCycleAnchor, + Instant endOfCurrentPeriod, boolean active, boolean cancelAtPeriodEnd, + SubscriptionStatus status, PaymentMethod paymentMethod, boolean paymentProcessing, + @Nullable ChargeFailure chargeFailure) { + + } + + record SubscriptionPrice(String currency, BigDecimal amount) { + + } + + record ReceiptItem(String itemId, Instant expiration, long level) { + + } + + record LevelAndCurrency(long level, String currency) { + + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/AsyncTimerUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/AsyncTimerUtil.java new file mode 100644 index 000000000..b7c87cda6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/AsyncTimerUtil.java @@ -0,0 +1,15 @@ +package org.whispersystems.textsecuregcm.util; + +import io.micrometer.core.instrument.Timer; +import javax.annotation.Nonnull; +import java.util.concurrent.CompletionStage; +import java.util.function.Supplier; + +public class AsyncTimerUtil { + @Nonnull + public static CompletionStage record(final Timer timer, final Supplier> toRecord) { + final Timer.Sample sample = Timer.start(); + return toRecord.get().whenComplete((ignoreT, ignoreE) -> sample.stop(timer)); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/AttributeValues.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/AttributeValues.java new file mode 100644 index 000000000..b0d37b7c1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/AttributeValues.java @@ -0,0 +1,151 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import java.nio.ByteBuffer; +import java.util.Base64; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import com.google.common.annotations.VisibleForTesting; +import io.micrometer.core.instrument.Metrics; +import org.apache.commons.lang3.StringUtils; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** AwsAV provides static helper methods for working with AWS AttributeValues. */ +public class AttributeValues { + + // Clear-type methods + + public static AttributeValue b(byte[] value) { + return AttributeValue.builder().b(SdkBytes.fromByteArray(value)).build(); + } + + public static AttributeValue b(ByteBuffer value) { + return AttributeValue.builder().b(SdkBytes.fromByteBuffer(value)).build(); + } + + public static AttributeValue b(UUID value) { + return b(UUIDUtil.toByteBuffer(value)); + } + + public static AttributeValue n(long value) { + return AttributeValue.builder().n(String.valueOf(value)).build(); + } + + public static AttributeValue s(String value) { + return AttributeValue.builder().s(value).build(); + } + + public static AttributeValue m(Map value) { + return AttributeValue.builder().m(value).build(); + } + + // More opinionated methods + + public static AttributeValue fromString(String value) { + return AttributeValue.builder().s(value).build(); + } + + public static AttributeValue fromLong(long value) { + return AttributeValue.builder().n(Long.toString(value)).build(); + } + + public static AttributeValue fromBool(boolean value) { return AttributeValue.builder().bool(value).build(); } + + public static AttributeValue fromInt(int value) { + return AttributeValue.builder().n(Integer.toString(value)).build(); + } + + public static AttributeValue fromByteArray(byte[] value) { + return AttributeValues.fromSdkBytes(SdkBytes.fromByteArray(value)); + } + + public static AttributeValue fromByteBuffer(ByteBuffer value) { + return AttributeValues.fromSdkBytes(SdkBytes.fromByteBuffer(value)); + } + + public static AttributeValue fromUUID(UUID uuid) { + return AttributeValues.fromSdkBytes(SdkBytes.fromByteArrayUnsafe(UUIDUtil.toBytes(uuid))); + } + + public static AttributeValue fromSdkBytes(SdkBytes value) { + return AttributeValue.builder().b(value).build(); + } + + private static boolean toBool(AttributeValue av) { + return av.bool(); + } + + private static int toInt(AttributeValue av) { + return Integer.parseInt(av.n()); + } + + private static long toLong(AttributeValue av) { + return Long.parseLong(av.n()); + } + + private static UUID toUUID(AttributeValue av) { + return UUIDUtil.fromBytes(av.b().asByteArrayUnsafe()); // We're guaranteed not to modify the byte array + } + + private static byte[] toByteArray(AttributeValue av) { + return av.b().asByteArray(); + } + + private static String toString(AttributeValue av) { + return av.s(); + } + + public static Optional get(Map item, String key) { + return Optional.ofNullable(item.get(key)); + } + + public static boolean getBool(Map item, String key, boolean defaultValue) { + return AttributeValues.get(item, key).map(AttributeValues::toBool).orElse(defaultValue); + } + + public static int getInt(Map item, String key, int defaultValue) { + return AttributeValues.get(item, key).map(AttributeValues::toInt).orElse(defaultValue); + } + + public static String getString(Map item, String key, String defaultValue) { + return AttributeValues.get(item, key).map(AttributeValues::toString).orElse(defaultValue); + } + + public static long getLong(Map item, String key, long defaultValue) { + return AttributeValues.get(item, key).map(AttributeValues::toLong).orElse(defaultValue); + } + + public static byte[] getByteArray(Map item, String key, byte[] defaultValue) { + return AttributeValues.get(item, key).map(AttributeValues::toByteArray).orElse(defaultValue); + } + + public static UUID getUUID(Map item, String key, UUID defaultValue) { + return AttributeValues.get(item, key).filter(av -> av.b() != null).map(AttributeValues::toUUID).orElse(defaultValue); + } + + /** + * Extracts a byte array from an {@link AttributeValue} that may be either a byte array or a base64-encoded string. + * + * @param attributeValue the {@code AttributeValue} from which to extract a byte array + * + * @return the byte array represented by the given {@code AttributeValue} + */ + @VisibleForTesting + public static byte[] extractByteArray(final AttributeValue attributeValue, final String counterName) { + if (attributeValue.b() != null) { + Metrics.counter(counterName, "format", "bytes").increment(); + return attributeValue.b().asByteArray(); + } else if (StringUtils.isNotBlank(attributeValue.s())) { + Metrics.counter(counterName, "format", "string").increment(); + return Base64.getDecoder().decode(attributeValue.s()); + } + + throw new IllegalArgumentException("Attribute value has neither a byte array nor a string value"); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ByteArrayAdapter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ByteArrayAdapter.java new file mode 100644 index 000000000..0942486c0 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ByteArrayAdapter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.util; + + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.util.Base64; + +public class ByteArrayAdapter { + + public static class Serializing extends JsonSerializer { + @Override + public void serialize(byte[] bytes, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + jsonGenerator.writeString(Base64.getEncoder().withoutPadding().encodeToString(bytes)); + } + } + + public static class Deserializing extends JsonDeserializer { + @Override + public byte[] deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + return Base64.getDecoder().decode(jsonParser.getValueAsString()); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ByteArrayBase64UrlAdapter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ByteArrayBase64UrlAdapter.java new file mode 100644 index 000000000..1e3c2933a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ByteArrayBase64UrlAdapter.java @@ -0,0 +1,27 @@ +package org.whispersystems.textsecuregcm.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.util.Base64; + +public class ByteArrayBase64UrlAdapter { + public static class Serializing extends JsonSerializer { + @Override + public void serialize(byte[] bytes, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + jsonGenerator.writeString(Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)); + } + } + + public static class Deserializing extends JsonDeserializer { + @Override + public byte[] deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + return Base64.getUrlDecoder().decode(jsonParser.getValueAsString()); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ByteArrayBase64WithPaddingAdapter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ByteArrayBase64WithPaddingAdapter.java new file mode 100644 index 000000000..78188a478 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ByteArrayBase64WithPaddingAdapter.java @@ -0,0 +1,27 @@ +package org.whispersystems.textsecuregcm.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.util.Base64; + +public class ByteArrayBase64WithPaddingAdapter { + public static class Serializing extends JsonSerializer { + @Override + public void serialize(byte[] bytes, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + jsonGenerator.writeString(Base64.getEncoder().encodeToString(bytes)); + } + } + + public static class Deserializing extends JsonDeserializer { + @Override + public byte[] deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + return Base64.getDecoder().decode(jsonParser.getValueAsString()); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/CertificateUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/CertificateUtil.java new file mode 100644 index 000000000..688351c34 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/CertificateUtil.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +public class CertificateUtil { + + public static KeyStore buildKeyStoreForPem(final String... caCertificatePems) throws CertificateException { + try { + final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + + for (int i = 0; i < caCertificatePems.length; i++) { + final X509Certificate certificate = getCertificate(caCertificatePems[i]); + + if (certificate == null) { + throw new CertificateException("No certificate found in parsing!"); + } + + keyStore.setCertificateEntry("ca-" + i, certificate); + } + + return keyStore; + } catch (IOException | KeyStoreException ex) { + throw new CertificateException(ex); + } catch (NoSuchAlgorithmException ex) { + throw new AssertionError(ex); + } + } + + public static X509Certificate getCertificate(final String certificatePem) throws CertificateException { + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + + try (final ByteArrayInputStream pemInputStream = new ByteArrayInputStream(certificatePem.getBytes())) { + return (X509Certificate) certificateFactory.generateCertificate(pemInputStream); + } catch (IOException e) { + throw new CertificateException(e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/CircuitBreakerUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/CircuitBreakerUtil.java new file mode 100644 index 000000000..06cbc731a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/CircuitBreakerUtil.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static com.codahale.metrics.MetricRegistry.name; + +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.retry.Retry; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; + +public class CircuitBreakerUtil { + + private static final String CIRCUIT_BREAKER_CALL_COUNTER_NAME = name(CircuitBreakerUtil.class, "breaker", "call"); + private static final String CIRCUIT_BREAKER_STATE_GAUGE_NAME = name(CircuitBreakerUtil.class, "breaker", "state"); + private static final String RETRY_CALL_COUNTER_NAME = name(CircuitBreakerUtil.class, "retry", "call"); + + private static final String NAME_TAG_NAME = "name"; + private static final String OUTCOME_TAG_NAME = "outcome"; + + public static void registerMetrics(CircuitBreaker circuitBreaker, Class clazz) { + final String breakerName = clazz.getSimpleName() + "/" + circuitBreaker.getName(); + + final Counter successCounter = Metrics.counter(CIRCUIT_BREAKER_CALL_COUNTER_NAME, + NAME_TAG_NAME, breakerName, + OUTCOME_TAG_NAME, "success"); + + final Counter failureCounter = Metrics.counter(CIRCUIT_BREAKER_CALL_COUNTER_NAME, + NAME_TAG_NAME, breakerName, + OUTCOME_TAG_NAME, "failure"); + + final Counter unpermittedCounter = Metrics.counter(CIRCUIT_BREAKER_CALL_COUNTER_NAME, + NAME_TAG_NAME, breakerName, + OUTCOME_TAG_NAME, "unpermitted"); + + circuitBreaker.getEventPublisher().onSuccess(event -> { + successCounter.increment(); + }); + + circuitBreaker.getEventPublisher().onError(event -> { + failureCounter.increment(); + }); + + circuitBreaker.getEventPublisher().onCallNotPermitted(event -> { + unpermittedCounter.increment(); + }); + + Metrics.gauge(CIRCUIT_BREAKER_STATE_GAUGE_NAME, + Tags.of(Tag.of(NAME_TAG_NAME, circuitBreaker.getName())), + circuitBreaker, breaker -> breaker.getState().getOrder()); + } + + public static void registerMetrics(Retry retry, Class clazz) { + final String retryName = clazz.getSimpleName() + "/" + retry.getName(); + + final Counter successCounter = Metrics.counter(RETRY_CALL_COUNTER_NAME, + NAME_TAG_NAME, retryName, + OUTCOME_TAG_NAME, "success"); + + final Counter retryCounter = Metrics.counter(RETRY_CALL_COUNTER_NAME, + NAME_TAG_NAME, retryName, + OUTCOME_TAG_NAME, "retry"); + + final Counter errorCounter = Metrics.counter(RETRY_CALL_COUNTER_NAME, + NAME_TAG_NAME, retryName, + OUTCOME_TAG_NAME, "error"); + + final Counter ignoredErrorCounter = Metrics.counter(RETRY_CALL_COUNTER_NAME, + NAME_TAG_NAME, retryName, + OUTCOME_TAG_NAME, "ignored_error"); + + retry.getEventPublisher().onSuccess(event -> { + successCounter.increment(); + }); + + retry.getEventPublisher().onRetry(event -> { + retryCounter.increment(); + }); + + retry.getEventPublisher().onError(event -> { + errorCounter.increment(); + }); + + retry.getEventPublisher().onIgnoredError(event -> { + ignoredErrorCounter.increment(); + }); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Constants.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Constants.java new file mode 100644 index 000000000..7035be3a8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Constants.java @@ -0,0 +1,14 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import io.dropwizard.util.DataSize; + +public class Constants { + public static final String METRICS_NAME = "textsecure"; + public static final int MAXIMUM_STICKER_SIZE_BYTES = (int) DataSize.kibibytes(300 + 1).toBytes(); // add 1 kiB for encryption overhead + public static final int MAXIMUM_STICKER_MANIFEST_SIZE_BYTES = (int) DataSize.kibibytes(10).toBytes(); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Conversions.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Conversions.java new file mode 100644 index 000000000..14f0008e4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Conversions.java @@ -0,0 +1,168 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.util; + +public class Conversions { + + public static byte intsToByteHighAndLow(int highValue, int lowValue) { + return (byte)((highValue << 4 | lowValue) & 0xFF); + } + + public static int highBitsToInt(byte value) { + return (value & 0xFF) >> 4; + } + + public static int lowBitsToInt(byte value) { + return (value & 0xF); + } + + public static int highBitsToMedium(int value) { + return (value >> 12); + } + + public static int lowBitsToMedium(int value) { + return (value & 0xFFF); + } + + public static byte[] shortToByteArray(int value) { + byte[] bytes = new byte[2]; + shortToByteArray(bytes, 0, value); + return bytes; + } + + public static int shortToByteArray(byte[] bytes, int offset, int value) { + bytes[offset+1] = (byte)value; + bytes[offset] = (byte)(value >> 8); + return 2; + } + + public static int shortToLittleEndianByteArray(byte[] bytes, int offset, int value) { + bytes[offset] = (byte)value; + bytes[offset+1] = (byte)(value >> 8); + return 2; + } + + public static byte[] mediumToByteArray(int value) { + byte[] bytes = new byte[3]; + mediumToByteArray(bytes, 0, value); + return bytes; + } + + public static int mediumToByteArray(byte[] bytes, int offset, int value) { + bytes[offset + 2] = (byte)value; + bytes[offset + 1] = (byte)(value >> 8); + bytes[offset] = (byte)(value >> 16); + return 3; + } + + public static byte[] intToByteArray(int value) { + byte[] bytes = new byte[4]; + intToByteArray(bytes, 0, value); + return bytes; + } + + public static int intToByteArray(byte[] bytes, int offset, int value) { + bytes[offset + 3] = (byte)value; + bytes[offset + 2] = (byte)(value >> 8); + bytes[offset + 1] = (byte)(value >> 16); + bytes[offset] = (byte)(value >> 24); + return 4; + } + + public static int intToLittleEndianByteArray(byte[] bytes, int offset, int value) { + bytes[offset] = (byte)value; + bytes[offset+1] = (byte)(value >> 8); + bytes[offset+2] = (byte)(value >> 16); + bytes[offset+3] = (byte)(value >> 24); + return 4; + } + + public static byte[] longToByteArray(long l) { + byte[] bytes = new byte[8]; + longToByteArray(bytes, 0, l); + return bytes; + } + + public static int longToByteArray(byte[] bytes, int offset, long value) { + bytes[offset + 7] = (byte)value; + bytes[offset + 6] = (byte)(value >> 8); + bytes[offset + 5] = (byte)(value >> 16); + bytes[offset + 4] = (byte)(value >> 24); + bytes[offset + 3] = (byte)(value >> 32); + bytes[offset + 2] = (byte)(value >> 40); + bytes[offset + 1] = (byte)(value >> 48); + bytes[offset] = (byte)(value >> 56); + return 8; + } + + public static int longTo4ByteArray(byte[] bytes, int offset, long value) { + bytes[offset + 3] = (byte)value; + bytes[offset + 2] = (byte)(value >> 8); + bytes[offset + 1] = (byte)(value >> 16); + bytes[offset + 0] = (byte)(value >> 24); + return 4; + } + + public static int byteArrayToShort(byte[] bytes) { + return byteArrayToShort(bytes, 0); + } + + public static int byteArrayToShort(byte[] bytes, int offset) { + return + (bytes[offset] & 0xff) << 8 | (bytes[offset + 1] & 0xff); + } + + // The SSL patented 3-byte Value. + public static int byteArrayToMedium(byte[] bytes, int offset) { + return + (bytes[offset] & 0xff) << 16 | + (bytes[offset + 1] & 0xff) << 8 | + (bytes[offset + 2] & 0xff); + } + + public static int byteArrayToInt(byte[] bytes) { + return byteArrayToInt(bytes, 0); + } + + public static int byteArrayToInt(byte[] bytes, int offset) { + return + (bytes[offset] & 0xff) << 24 | + (bytes[offset + 1] & 0xff) << 16 | + (bytes[offset + 2] & 0xff) << 8 | + (bytes[offset + 3] & 0xff); + } + + public static int byteArrayToIntLittleEndian(byte[] bytes, int offset) { + return + (bytes[offset + 3] & 0xff) << 24 | + (bytes[offset + 2] & 0xff) << 16 | + (bytes[offset + 1] & 0xff) << 8 | + (bytes[offset] & 0xff); + } + + public static long byteArrayToLong(byte[] bytes) { + return byteArrayToLong(bytes, 0); + } + + public static long byteArray4ToLong(byte[] bytes, int offset) { + return + ((bytes[offset + 0] & 0xffL) << 24) | + ((bytes[offset + 1] & 0xffL) << 16) | + ((bytes[offset + 2] & 0xffL) << 8) | + ((bytes[offset + 3] & 0xffL)); + } + + public static long byteArrayToLong(byte[] bytes, int offset) { + return + ((bytes[offset] & 0xffL) << 56) | + ((bytes[offset + 1] & 0xffL) << 48) | + ((bytes[offset + 2] & 0xffL) << 40) | + ((bytes[offset + 3] & 0xffL) << 32) | + ((bytes[offset + 4] & 0xffL) << 24) | + ((bytes[offset + 5] & 0xffL) << 16) | + ((bytes[offset + 6] & 0xffL) << 8) | + ((bytes[offset + 7] & 0xffL)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/DestinationDeviceValidator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/DestinationDeviceValidator.java new file mode 100644 index 000000000..1c26c731e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/DestinationDeviceValidator.java @@ -0,0 +1,108 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; +import org.whispersystems.textsecuregcm.controllers.StaleDevicesException; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; + +public class DestinationDeviceValidator { + + /** + * @see #validateRegistrationIds(Account, Stream, boolean) + */ + public static void validateRegistrationIds(final Account account, final Collection messages, + Function getDeviceId, Function getRegistrationId, boolean usePhoneNumberIdentity) + throws StaleDevicesException { + validateRegistrationIds(account, + messages.stream().map(m -> new Pair<>(getDeviceId.apply(m), getRegistrationId.apply(m))), + usePhoneNumberIdentity); + + } + + /** + * Validates that the given device ID/registration ID pairs exactly match the corresponding device ID/registration ID + * pairs in the given destination account. This method does not validate that all devices associated with the + * destination account are present in the given device ID/registration ID pairs. + * + * @param account the destination account against which to check the given device + * ID/registration ID pairs + * @param deviceIdAndRegistrationIdStream a stream of device ID and registration ID pairs + * @param usePhoneNumberIdentity if {@code true}, compare provided registration IDs against device + * registration IDs associated with the account's PNI (if available); compare + * against the ACI-associated registration ID otherwise + * @throws StaleDevicesException if the device ID/registration ID pairs contained an entry for which the destination + * account does not have a corresponding device or if the registration IDs do not match + */ + public static void validateRegistrationIds(final Account account, + final Stream> deviceIdAndRegistrationIdStream, + final boolean usePhoneNumberIdentity) throws StaleDevicesException { + + final List staleDevices = deviceIdAndRegistrationIdStream + .filter(deviceIdAndRegistrationId -> deviceIdAndRegistrationId.second() > 0) + .filter(deviceIdAndRegistrationId -> { + final long deviceId = deviceIdAndRegistrationId.first(); + final int registrationId = deviceIdAndRegistrationId.second(); + boolean registrationIdMatches = account.getDevice(deviceId) + .map(device -> registrationId == (usePhoneNumberIdentity + ? device.getPhoneNumberIdentityRegistrationId().orElse(device.getRegistrationId()) + : device.getRegistrationId())) + .orElse(false); + return !registrationIdMatches; + }) + .map(Pair::first) + .collect(Collectors.toList()); + + if (!staleDevices.isEmpty()) { + throw new StaleDevicesException(staleDevices); + } + } + + /** + * Validates that the given set of device IDs from a set of messages matches the set of device IDs associated with the + * given destination account in preparation for sending those messages to the destination account. In general, the set + * of device IDs must exactly match the set of active devices associated with the destination account. When sending a + * "sync," message, though, the authenticated account is sending messages from one of their devices to all other + * devices; in that case, callers must pass the ID of the sending device in the set of {@code excludedDeviceIds}. + * + * @param account the destination account against which to check the given set of device IDs + * @param messageDeviceIds the set of device IDs to check against the destination account + * @param excludedDeviceIds a set of device IDs that may be associated with the destination account, but must not be + * present in the given set of device IDs (i.e. the device that is sending a sync message) + * @throws MismatchedDevicesException if the given set of device IDs contains entries not currently associated with + * the destination account or is missing entries associated with the destination + * account + */ + public static void validateCompleteDeviceList(final Account account, + final Set messageDeviceIds, + final Set excludedDeviceIds) throws MismatchedDevicesException { + + final Set accountDeviceIds = account.getDevices().stream() + .filter(Device::isEnabled) + .map(Device::getId) + .filter(deviceId -> !excludedDeviceIds.contains(deviceId)) + .collect(Collectors.toSet()); + + final Set missingDeviceIds = new HashSet<>(accountDeviceIds); + missingDeviceIds.removeAll(messageDeviceIds); + + final Set extraDeviceIds = new HashSet<>(messageDeviceIds); + extraDeviceIds.removeAll(accountDeviceIds); + + if (!missingDeviceIds.isEmpty() || !extraDeviceIds.isEmpty()) { + throw new MismatchedDevicesException(new ArrayList<>(missingDeviceIds), new ArrayList<>(extraDeviceIds)); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/DynamoDbFromConfig.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/DynamoDbFromConfig.java new file mode 100644 index 000000000..559baf3d2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/DynamoDbFromConfig.java @@ -0,0 +1,35 @@ +package org.whispersystems.textsecuregcm.util; + +import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +public class DynamoDbFromConfig { + + public static DynamoDbClient client(DynamoDbClientConfiguration config, AwsCredentialsProvider credentialsProvider) { + return DynamoDbClient.builder() + .region(Region.of(config.getRegion())) + .credentialsProvider(credentialsProvider) + .overrideConfiguration(ClientOverrideConfiguration.builder() + .apiCallTimeout(config.getClientExecutionTimeout()) + .apiCallAttemptTimeout(config.getClientRequestTimeout()) + .build()) + .build(); + } + + public static DynamoDbAsyncClient asyncClient( + DynamoDbClientConfiguration config, + AwsCredentialsProvider credentialsProvider) { + return DynamoDbAsyncClient.builder() + .region(Region.of(config.getRegion())) + .credentialsProvider(credentialsProvider) + .overrideConfiguration(ClientOverrideConfiguration.builder() + .apiCallTimeout(config.getClientExecutionTimeout()) + .apiCallAttemptTimeout(config.getClientRequestTimeout()) + .build()) + .build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/E164.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/E164.java new file mode 100644 index 000000000..6dab03ea7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/E164.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Objects; +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.Payload; + +/** + * Constraint annotation that requires annotated entity + * to hold (or return) a string value that is a valid E164-normalized phone number. + */ +@Target({ FIELD, PARAMETER, METHOD }) +@Retention(RUNTIME) +@Constraint(validatedBy = E164.Validator.class) +@Documented +public @interface E164 { + + String message() default "value is not a valid E164 number"; + + Class[] groups() default { }; + + Class[] payload() default { }; + + class Validator implements ConstraintValidator { + + @Override + public boolean isValid(final String value, final ConstraintValidatorContext context) { + if (Objects.isNull(value)) { + return true; + } + if (!value.startsWith("+")) { + return false; + } + try { + Util.requireNormalizedNumber(value); + } catch (final ImpossiblePhoneNumberException | NonNormalizedPhoneNumberException e) { + return false; + } + return true; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ECPublicKeyAdapter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ECPublicKeyAdapter.java new file mode 100644 index 000000000..c89e0b77d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ECPublicKeyAdapter.java @@ -0,0 +1,67 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.util.Base64; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; + +public class ECPublicKeyAdapter { + + private static final Counter EC_PUBLIC_KEY_WITHOUT_VERSION_BYTE_COUNTER = + Metrics.counter(MetricsUtil.name(ECPublicKeyAdapter.class, "keyWithoutVersionByte")); + + public static class Serializer extends JsonSerializer { + + @Override + public void serialize(final ECPublicKey ecPublicKey, + final JsonGenerator jsonGenerator, + final SerializerProvider serializers) throws IOException { + + jsonGenerator.writeString(Base64.getEncoder().encodeToString(ecPublicKey.serialize())); + } + } + + public static class Deserializer extends JsonDeserializer { + + @Override + public ECPublicKey deserialize(final JsonParser parser, final DeserializationContext context) throws IOException { + final byte[] ecPublicKeyBytes; + + try { + ecPublicKeyBytes = Base64.getDecoder().decode(parser.getValueAsString()); + } catch (final IllegalArgumentException e) { + throw new JsonParseException(parser, "Could not parse EC public key as a base64-encoded value", e); + } + + if (ecPublicKeyBytes.length == 0) { + return null; + } + + try { + return new ECPublicKey(ecPublicKeyBytes); + } catch (final InvalidKeyException e) { + if (ecPublicKeyBytes.length == ECPublicKey.KEY_SIZE - 1) { + EC_PUBLIC_KEY_WITHOUT_VERSION_BYTE_COUNTER.increment(); + return ECPublicKey.fromPublicKeyBytes(ecPublicKeyBytes); + } + + throw new JsonParseException(parser, "Could not interpret identity key bytes as an EC public key", e); + } + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySize.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySize.java new file mode 100644 index 000000000..0bb9416f8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySize.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import javax.validation.Constraint; +import javax.validation.Payload; + +@Target({ FIELD, METHOD, CONSTRUCTOR, PARAMETER, ANNOTATION_TYPE, TYPE_USE }) +@Retention(RUNTIME) +@Constraint(validatedBy = { + ExactlySizeValidatorForString.class, + ExactlySizeValidatorForArraysOfByte.class, + ExactlySizeValidatorForCollection.class, + ExactlySizeValidatorForSecretBytes.class, +}) +@Documented +public @interface ExactlySize { + + String message() default "{org.whispersystems.textsecuregcm.util.ExactlySize.message}"; + + Class[] groups() default { }; + + Class[] payload() default { }; + + int[] value(); + + @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE }) + @Retention(RUNTIME) + @Documented + @interface List { + ExactlySize[] value(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidator.java new file mode 100644 index 000000000..a66cf939f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidator.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public abstract class ExactlySizeValidator implements ConstraintValidator { + + private Set permittedSizes; + + @Override + public void initialize(ExactlySize annotation) { + permittedSizes = Arrays.stream(annotation.value()).boxed().collect(Collectors.toSet()); + } + + @Override + public boolean isValid(T value, ConstraintValidatorContext context) { + return permittedSizes.contains(size(value)); + } + + protected abstract int size(T value); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidatorForArraysOfByte.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidatorForArraysOfByte.java new file mode 100644 index 000000000..358eaf65a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidatorForArraysOfByte.java @@ -0,0 +1,14 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +public class ExactlySizeValidatorForArraysOfByte extends ExactlySizeValidator { + + @Override + protected int size(final byte[] value) { + return value == null ? 0 : value.length; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidatorForCollection.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidatorForCollection.java new file mode 100644 index 000000000..df05bee24 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidatorForCollection.java @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import java.util.Collection; + +public class ExactlySizeValidatorForCollection extends ExactlySizeValidator> { + + @Override + protected int size(final Collection value) { + return value == null ? 0 : value.size(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidatorForSecretBytes.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidatorForSecretBytes.java new file mode 100644 index 000000000..723237e06 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidatorForSecretBytes.java @@ -0,0 +1,15 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; + +public class ExactlySizeValidatorForSecretBytes extends ExactlySizeValidator { + @Override + protected int size(final SecretBytes value) { + return value == null ? 0 : value.value().length; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidatorForString.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidatorForString.java new file mode 100644 index 000000000..9d77641cd --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySizeValidatorForString.java @@ -0,0 +1,15 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + + +public class ExactlySizeValidatorForString extends ExactlySizeValidator { + + @Override + protected int size(final String value) { + return value == null ? 0 : value.length(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExceptionUtils.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExceptionUtils.java new file mode 100644 index 000000000..28da692c2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExceptionUtils.java @@ -0,0 +1,40 @@ +package org.whispersystems.textsecuregcm.util; + +import java.util.concurrent.CompletionException; + +public final class ExceptionUtils { + + private ExceptionUtils() { + // utility class + } + + /** + * Extracts the cause of a {@link CompletionException}. If the given {@code throwable} is a + * {@code CompletionException}, this method will recursively iterate through its causal chain until it finds the first + * cause that is not a {@code CompletionException}. If the last {@code CompletionException} in the causal chain has a + * {@code null} cause, then this method returns the last {@code CompletionException} in the chain. If the given + * {@code throwable} is not a {@code CompletionException}, then this method returns the original {@code throwable}. + * + * @param throwable the throwable to "unwrap" + * + * @return the first entity in the given {@code throwable}'s causal chain that is not a {@code CompletionException} + */ + public static Throwable unwrap(Throwable throwable) { + while (throwable instanceof CompletionException e && throwable.getCause() != null) { + throwable = e.getCause(); + } + return throwable; + } + + /** + * Wraps the given {@code throwable} in a {@link CompletionException} unless the given {@code throwable} is already + * a {@code CompletionException}, in which case this method returns the original throwable. + * + * @param throwable the throwable to wrap in a {@code CompletionException} + */ + public static CompletionException wrap(final Throwable throwable) { + return throwable instanceof CompletionException completionException + ? completionException + : new CompletionException(throwable); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExecutorUtils.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExecutorUtils.java new file mode 100644 index 000000000..c380be717 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ExecutorUtils.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ExecutorUtils { + + public static Executor newFixedThreadBoundedQueueExecutor(int threadCount, int queueSize) { + ThreadPoolExecutor executor = new ThreadPoolExecutor(threadCount, threadCount, + Long.MAX_VALUE, TimeUnit.NANOSECONDS, + new ArrayBlockingQueue<>(queueSize), + new ThreadPoolExecutor.AbortPolicy()); + + executor.prestartAllCoreThreads(); + + return executor; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/HeaderUtils.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/HeaderUtils.java new file mode 100644 index 000000000..89b9e7b3b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/HeaderUtils.java @@ -0,0 +1,101 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static java.util.Objects.requireNonNull; + +import io.dropwizard.auth.basic.BasicCredentials; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; + +public final class HeaderUtils { + + public static final String X_SIGNAL_AGENT = "X-Signal-Agent"; + + public static final String X_SIGNAL_KEY = "X-Signal-Key"; + + public static final String TIMESTAMP_HEADER = "X-Signal-Timestamp"; + + private HeaderUtils() { + // utility class + } + + public static String basicAuthHeader(final ExternalServiceCredentials credentials) { + return basicAuthHeader(credentials.username(), credentials.password()); + } + + public static String basicAuthHeader(final String username, final String password) { + requireNonNull(username); + requireNonNull(password); + return "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)); + } + + @Nonnull + public static String getTimestampHeader() { + return TIMESTAMP_HEADER + ":" + System.currentTimeMillis(); + } + + /** + * Returns the most recent proxy in a chain described by an {@code X-Forwarded-For} header. + * + * @param forwardedFor the value of an X-Forwarded-For header + * + * @return the IP address of the most recent proxy in the forwarding chain, or empty if none was found or + * {@code forwardedFor} was null + * + * @see X-Forwarded-For - HTTP | MDN + */ + @Nonnull + public static Optional getMostRecentProxy(@Nullable final String forwardedFor) { + return Optional.ofNullable(forwardedFor) + .map(ff -> { + final int idx = forwardedFor.lastIndexOf(',') + 1; + return idx < forwardedFor.length() + ? forwardedFor.substring(idx).trim() + : null; + }) + .filter(StringUtils::isNotBlank); + } + + /** + * Parses a Base64-encoded value of the `Authorization` header + * in the form of `Basic dXNlcm5hbWU6cGFzc3dvcmQ=`. + * Note: parsing logic is copied from {@link io.dropwizard.auth.basic.BasicCredentialAuthFilter#getCredentials(String)}. + */ + public static Optional basicCredentialsFromAuthHeader(final String authHeader) { + final int space = authHeader.indexOf(' '); + if (space <= 0) { + return Optional.empty(); + } + + final String method = authHeader.substring(0, space); + if (!"Basic".equalsIgnoreCase(method)) { + return Optional.empty(); + } + + final String decoded; + try { + decoded = new String(Base64.getDecoder().decode(authHeader.substring(space + 1)), StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + return Optional.empty(); + } + + // Decoded credentials is 'username:password' + final int i = decoded.indexOf(':'); + if (i <= 0) { + return Optional.empty(); + } + + final String username = decoded.substring(0, i); + final String password = decoded.substring(i + 1); + return Optional.of(new BasicCredentials(username, password)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/HmacUtils.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/HmacUtils.java new file mode 100644 index 000000000..6469fe01b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/HmacUtils.java @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +public final class HmacUtils { + + private static final HexFormat HEX = HexFormat.of(); + + private static final String HMAC_SHA_256 = "HmacSHA256"; + + private static final ThreadLocal THREAD_LOCAL_HMAC_SHA_256 = ThreadLocal.withInitial(() -> { + try { + return Mac.getInstance(HMAC_SHA_256); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + }); + + public static byte[] hmac256(final byte[] key, final byte[] input) { + try { + final Mac mac = THREAD_LOCAL_HMAC_SHA_256.get(); + mac.init(new SecretKeySpec(key, HMAC_SHA_256)); + return mac.doFinal(input); + } catch (final InvalidKeyException e) { + throw new RuntimeException(e); + } + } + + public static byte[] hmac256(final byte[] key, final String input) { + return hmac256(key, input.getBytes(StandardCharsets.UTF_8)); + } + + public static String hmac256ToHexString(final byte[] key, final byte[] input) { + return HEX.formatHex(hmac256(key, input)); + } + + public static String hmac256ToHexString(final byte[] key, final String input) { + return hmac256ToHexString(key, input.getBytes(StandardCharsets.UTF_8)); + } + + public static byte[] hmac256Truncated(final byte[] key, final byte[] input, final int length) { + return Util.truncate(hmac256(key, input), length); + } + + public static byte[] hmac256Truncated(final byte[] key, final String input, final int length) { + return hmac256Truncated(key, input.getBytes(StandardCharsets.UTF_8), length); + } + + public static String hmac256TruncatedToHexString(final byte[] key, final byte[] input, final int length) { + return HEX.formatHex(Util.truncate(hmac256(key, input), length)); + } + + public static String hmac256TruncatedToHexString(final byte[] key, final String input, final int length) { + return hmac256TruncatedToHexString(key, input.getBytes(StandardCharsets.UTF_8), length); + } + + public static boolean hmacHexStringsEqual(final String expectedAsHexString, final String actualAsHexString) { + try { + final byte[] aBytes = HEX.parseHex(expectedAsHexString); + final byte[] bBytes = HEX.parseHex(actualAsHexString); + return MessageDigest.isEqual(aBytes, bBytes); + } catch (final IllegalArgumentException e) { + return false; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/HostnameUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/HostnameUtil.java new file mode 100644 index 000000000..ad8c1a8f9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/HostnameUtil.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Locale; + +public class HostnameUtil { + + private static final Logger log = LoggerFactory.getLogger(HostnameUtil.class); + + public static String getLocalHostname() { + try { + return InetAddress.getLocalHost().getHostName().toLowerCase(Locale.US); + } catch (final UnknownHostException e) { + log.warn("Failed to get hostname", e); + return "unknown"; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/HttpUtils.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/HttpUtils.java new file mode 100644 index 000000000..4f5169fd5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/HttpUtils.java @@ -0,0 +1,17 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +public final class HttpUtils { + + private HttpUtils() { + // utility class + } + + public static boolean isSuccessfulResponse(final int statusCode) { + return statusCode >= 200 && statusCode < 300; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/IdentityKeyAdapter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/IdentityKeyAdapter.java new file mode 100644 index 000000000..04bda90f1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/IdentityKeyAdapter.java @@ -0,0 +1,68 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.util.Base64; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; + +public class IdentityKeyAdapter { + + private static final Counter IDENTITY_KEY_WITHOUT_VERSION_BYTE_COUNTER = + Metrics.counter(MetricsUtil.name(IdentityKeyAdapter.class, "identityKeyWithoutVersionByte")); + + public static class Serializer extends JsonSerializer { + + @Override + public void serialize(final IdentityKey identityKey, + final JsonGenerator jsonGenerator, + final SerializerProvider serializers) throws IOException { + + jsonGenerator.writeString(Base64.getEncoder().encodeToString(identityKey.serialize())); + } + } + + public static class Deserializer extends JsonDeserializer { + + @Override + public IdentityKey deserialize(final JsonParser parser, final DeserializationContext context) throws IOException { + final byte[] identityKeyBytes; + + try { + identityKeyBytes = Base64.getDecoder().decode(parser.getValueAsString()); + } catch (final IllegalArgumentException e) { + throw new JsonParseException(parser, "Could not parse identity key as a base64-encoded value", e); + } + + if (identityKeyBytes.length == 0) { + return null; + } + + try { + return new IdentityKey(identityKeyBytes); + } catch (final InvalidKeyException e) { + if (identityKeyBytes.length == ECPublicKey.KEY_SIZE - 1) { + IDENTITY_KEY_WITHOUT_VERSION_BYTE_COUNTER.increment(); + return new IdentityKey(ECPublicKey.fromPublicKeyBytes(identityKeyBytes)); + } + + throw new JsonParseException(parser, "Could not interpret identity key bytes as an EC public key", e); + } + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ImpossiblePhoneNumberException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ImpossiblePhoneNumberException.java new file mode 100644 index 000000000..1e6e0d73e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ImpossiblePhoneNumberException.java @@ -0,0 +1,17 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +public class ImpossiblePhoneNumberException extends Exception { + + public ImpossiblePhoneNumberException() { + super(); + } + + public ImpossiblePhoneNumberException(final Throwable cause) { + super(cause); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/IterablePair.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/IterablePair.java new file mode 100644 index 000000000..7f94099d5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/IterablePair.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.util; + +import java.util.Iterator; +import java.util.List; + +public class IterablePair implements Iterable> { + private final List first; + private final List second; + + public IterablePair(List first, List second) { + this.first = first; + this.second = second; + } + + @Override + public Iterator> iterator(){ + return new ParallelIterator<>( first.iterator(), second.iterator() ); + } + + public static class ParallelIterator implements Iterator> { + + private final Iterator it1; + private final Iterator it2; + + public ParallelIterator(Iterator it1, Iterator it2) { + this.it1 = it1; this.it2 = it2; + } + + @Override + public boolean hasNext() { return it1.hasNext() && it2.hasNext(); } + + @Override + public Pair next() { + return new Pair<>(it1.next(), it2.next()); + } + + @Override + public void remove(){ + it1.remove(); + it2.remove(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/KEMPublicKeyAdapter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/KEMPublicKeyAdapter.java new file mode 100644 index 000000000..6af30454a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/KEMPublicKeyAdapter.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.kem.KEMPublicKey; +import java.io.IOException; +import java.util.Base64; + +public class KEMPublicKeyAdapter { + + public static class Serializer extends JsonSerializer { + + @Override + public void serialize(final KEMPublicKey kemPublicKey, + final JsonGenerator jsonGenerator, + final SerializerProvider serializers) throws IOException { + + jsonGenerator.writeString(Base64.getEncoder().encodeToString(kemPublicKey.serialize())); + } + } + + public static class Deserializer extends JsonDeserializer { + + @Override + public KEMPublicKey deserialize(final JsonParser parser, final DeserializationContext context) throws IOException { + final byte[] kemPublicKeyBytes; + + try { + kemPublicKeyBytes = Base64.getDecoder().decode(parser.getValueAsString()); + } catch (final IllegalArgumentException e) { + throw new JsonParseException(parser, "Could not parse KEM public key as a base64-encoded value", e); + } + + if (kemPublicKeyBytes.length == 0) { + return null; + } + + try { + return new KEMPublicKey(kemPublicKeyBytes); + } catch (final InvalidKeyException e) { + throw new JsonParseException(parser, "Could not interpret key bytes as a KEM public key", e); + } + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/NoStackTraceRuntimeException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/NoStackTraceRuntimeException.java new file mode 100644 index 000000000..1fa8c6824 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/NoStackTraceRuntimeException.java @@ -0,0 +1,29 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +/** + * An abstract base class for runtime exceptions that do not include a stack trace. Stackless exceptions are generally + * intended for internal error-handling cases where the error will never be logged or otherwise reported. + */ +public abstract class NoStackTraceRuntimeException extends RuntimeException { + + public NoStackTraceRuntimeException() { + super(null, null, true, false); + } + + public NoStackTraceRuntimeException(final String message) { + super(message, null, true, false); + } + + public NoStackTraceRuntimeException(final String message, final Throwable cause) { + super(message, cause, true, false); + } + + public NoStackTraceRuntimeException(final Throwable cause) { + super(null, cause, true, false); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/NonNormalizedPhoneNumberException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/NonNormalizedPhoneNumberException.java new file mode 100644 index 000000000..4ce4d11b3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/NonNormalizedPhoneNumberException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +public class NonNormalizedPhoneNumberException extends Exception { + + private final String originalNumber; + private final String normalizedNumber; + + public NonNormalizedPhoneNumberException(final String originalNumber, final String normalizedNumber) { + this.originalNumber = originalNumber; + this.normalizedNumber = normalizedNumber; + } + + public String getOriginalNumber() { + return originalNumber; + } + + public String getNormalizedNumber() { + return normalizedNumber; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/OptionalIdentityKeyAdapter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/OptionalIdentityKeyAdapter.java new file mode 100644 index 000000000..61a4b97cb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/OptionalIdentityKeyAdapter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.util.Base64; +import java.util.Optional; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.InvalidKeyException; + +public class OptionalIdentityKeyAdapter { + + public static class Serializer extends JsonSerializer> { + + @Override + public void serialize(final Optional maybePublicKey, + final JsonGenerator jsonGenerator, + final SerializerProvider serializers) throws IOException { + + if (maybePublicKey.isPresent()) { + jsonGenerator.writeString(Base64.getEncoder().encodeToString(maybePublicKey.get().serialize())); + } else { + jsonGenerator.writeNull(); + } + } + } + + public static class Deserializer extends JsonDeserializer> { + + @Override + public Optional deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException { + try { + return Optional.of(new IdentityKey(Base64.getDecoder().decode(jsonParser.getValueAsString()))); + } catch (final InvalidKeyException e) { + throw new IOException(e); + } + } + + @Override + public Optional getNullValue(DeserializationContext ctxt) { + return Optional.empty(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Optionals.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Optionals.java new file mode 100644 index 000000000..4e3e49038 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Optionals.java @@ -0,0 +1,21 @@ +package org.whispersystems.textsecuregcm.util; + +import java.util.Optional; +import java.util.function.BiFunction; + +public class Optionals { + + private Optionals() {} + + /** + * Apply a function to two optional arguments, returning empty if either argument is empty + * + * @param optionalT Optional of type T + * @param optionalU Optional of type U + * @param fun Function of T and U that returns R + * @return The function applied to the values of optionalT and optionalU, or empty + */ + public static Optional zipWith(Optional optionalT, Optional optionalU, BiFunction fun) { + return optionalT.flatMap(t -> optionalU.map(u -> fun.apply(t, u))); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Pair.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Pair.java new file mode 100644 index 000000000..014a5e906 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Pair.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.util; + +import static com.google.common.base.Objects.equal; + +public class Pair { + private final T1 v1; + private final T2 v2; + + public Pair(T1 v1, T2 v2) { + this.v1 = v1; + this.v2 = v2; + } + + public T1 first() { + return v1; + } + + public T2 second() { + return v2; + } + + public boolean equals(Object o) { + return o instanceof Pair && + equal(((Pair) o).first(), first()) && + equal(((Pair) o).second(), second()); + } + + public int hashCode() { + return first().hashCode() ^ second().hashCode(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ProfileHelper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ProfileHelper.java new file mode 100644 index 000000000..ad9bce015 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ProfileHelper.java @@ -0,0 +1,100 @@ +package org.whispersystems.textsecuregcm.util; + +import com.google.common.annotations.VisibleForTesting; +import org.signal.libsignal.protocol.ServiceId; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.VerificationFailedException; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; +import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; +import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; +import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; +import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.storage.AccountBadge; +import org.whispersystems.textsecuregcm.storage.VersionedProfile; +import javax.annotation.Nullable; +import java.security.SecureRandom; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class ProfileHelper { + public static int MAX_PROFILE_AVATAR_SIZE_BYTES = 10 * 1024 * 1024; + @VisibleForTesting + public static final Duration EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION = Duration.ofDays(7); + + public static List mergeBadgeIdsWithExistingAccountBadges( + final Clock clock, + final Map badgeConfigurationMap, + final List badgeIds, + final List accountBadges) { + LinkedHashMap existingBadges = new LinkedHashMap<>(accountBadges.size()); + for (final AccountBadge accountBadge : accountBadges) { + existingBadges.putIfAbsent(accountBadge.getId(), accountBadge); + } + + LinkedHashMap result = new LinkedHashMap<>(accountBadges.size()); + for (final String badgeId : badgeIds) { + + // duplicate in the list, ignore it + if (result.containsKey(badgeId)) { + continue; + } + + // This is for testing badges and allows them to be added to an account at any time with an expiration of 1 day + // in the future. + BadgeConfiguration badgeConfiguration = badgeConfigurationMap.get(badgeId); + if (badgeConfiguration != null && badgeConfiguration.isTestBadge()) { + result.put(badgeId, new AccountBadge(badgeId, clock.instant().plus(Duration.ofDays(1)), true)); + continue; + } + + // reordering or making visible existing badges + if (existingBadges.containsKey(badgeId)) { + AccountBadge accountBadge = existingBadges.get(badgeId).withVisibility(true); + result.put(badgeId, accountBadge); + } + } + + // take any remaining account badges and make them invisible + for (final Map.Entry entry : existingBadges.entrySet()) { + if (!result.containsKey(entry.getKey())) { + AccountBadge accountBadge = entry.getValue().withVisibility(false); + result.put(accountBadge.getId(), accountBadge); + } + } + + return new ArrayList<>(result.values()); + } + + public static String generateAvatarObjectName() { + final byte[] object = new byte[16]; + new SecureRandom().nextBytes(object); + + return "profiles/" + Base64.getUrlEncoder().encodeToString(object); + } + + public static boolean isSelfProfileRequest(@Nullable final UUID requesterUuid, final AciServiceIdentifier targetIdentifier) { + return targetIdentifier.uuid().equals(requesterUuid); + } + + public static ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredential( + final byte[] encodedCredentialRequest, + final VersionedProfile profile, + final ServiceId.Aci accountIdentifier, + final ServerZkProfileOperations zkProfileOperations) throws InvalidInputException, VerificationFailedException { + final Instant expiration = Instant.now().plus(EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION).truncatedTo(ChronoUnit.DAYS); + final ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.commitment()); + final ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest( + encodedCredentialRequest); + + return zkProfileOperations.issueExpiringProfileKeyCredential(request, accountIdentifier, commitment, expiration); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/RedisClusterUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/RedisClusterUtil.java new file mode 100644 index 000000000..188e1e4f4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/RedisClusterUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import io.lettuce.core.cluster.SlotHash; + +public class RedisClusterUtil { + + private static final String[] HASHES_BY_SLOT = new String[SlotHash.SLOT_COUNT]; + + static { + int slotsCovered = 0; + int i = 0; + + while (slotsCovered < HASHES_BY_SLOT.length) { + final String hash = Integer.toString(i++, 36); + final int slot = SlotHash.getSlot(hash); + + if (HASHES_BY_SLOT[slot] == null) { + HASHES_BY_SLOT[slot] = hash; + slotsCovered += 1; + } + } + } + + /** + * Returns a Redis hash tag that maps to the given cluster slot. + * + * @param slot the Redis cluster slot for which to retrieve a hash tag + * + * @return a Redis hash tag that maps to the given cluster slot + * + * @see Redis Cluster Specification - Keys hash tags + */ + public static String getMinimalHashTag(final int slot) { + return HASHES_BY_SLOT[slot]; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ServiceIdentifierAdapter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ServiceIdentifierAdapter.java new file mode 100644 index 000000000..239fa002d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ServiceIdentifierAdapter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import java.io.IOException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; + +public class ServiceIdentifierAdapter { + + public static class ServiceIdentifierSerializer extends JsonSerializer { + + @Override + public void serialize(final ServiceIdentifier identifier, final JsonGenerator jsonGenerator, final SerializerProvider serializers) + throws IOException { + + jsonGenerator.writeString(identifier.toServiceIdentifierString()); + } + } + + public static class AciServiceIdentifierDeserializer extends JsonDeserializer { + + @Override + public AciServiceIdentifier deserialize(final JsonParser parser, final DeserializationContext context) + throws IOException { + + return AciServiceIdentifier.valueOf(parser.getValueAsString()); + } + } + + public static class PniServiceIdentifierDeserializer extends JsonDeserializer { + + @Override + public PniServiceIdentifier deserialize(final JsonParser parser, final DeserializationContext context) + throws IOException { + + return PniServiceIdentifier.valueOf(parser.getValueAsString()); + } + } + + public static class ServiceIdentifierDeserializer extends JsonDeserializer { + + @Override + public ServiceIdentifier deserialize(final JsonParser parser, final DeserializationContext context) + throws IOException { + + return ServiceIdentifier.valueOf(parser.getValueAsString()); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/SystemMapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/SystemMapper.java new file mode 100644 index 000000000..e055c716f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/SystemMapper.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ser.FilterProvider; +import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; +import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretsModule; + +public class SystemMapper { + + private static final ObjectMapper JSON_MAPPER = configureMapper(new ObjectMapper()); + + private static final ObjectMapper YAML_MAPPER = configureMapper(new YAMLMapper()); + + + @Nonnull + public static ObjectMapper jsonMapper() { + return JSON_MAPPER; + } + + @Nonnull + public static ObjectMapper yamlMapper() { + return YAML_MAPPER; + } + + public static ObjectMapper configureMapper(final ObjectMapper mapper) { + return mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .setFilterProvider(new SimpleFilterProvider().setDefaultFilter(SimpleBeanPropertyFilter.serializeAll())) + .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .setVisibility(PropertyAccessor.CREATOR, JsonAutoDetect.Visibility.PUBLIC_ONLY) + .registerModules( + SecretsModule.INSTANCE, + new JavaTimeModule(), + new Jdk8Module()); + } + + public static FilterProvider excludingField(final Class clazz, final List fieldsToExclude) { + final String filterId = clazz.getSimpleName(); + + // validate that the target class is annotated with @JsonFilter, + final List jsonFilterAnnotations = Arrays.stream(clazz.getAnnotations()) + .map(a -> a instanceof JsonFilter jsonFilter ? jsonFilter : null) + .filter(Objects::nonNull) + .toList(); + if (jsonFilterAnnotations.size() != 1 || !jsonFilterAnnotations.get(0).value().equals(filterId)) { + throw new IllegalStateException(""" + Class `%1$s` must have a single annotation of type `JsonFilter` + with the value equal to the name of the class itself: `@JsonFilter("%1$s")` + """.formatted(filterId)); + } + + return new SimpleFilterProvider() + .addFilter(filterId, SimpleBeanPropertyFilter.serializeAllExcept(fieldsToExclude.toArray(new String[0]))); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/UUIDUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/UUIDUtil.java new file mode 100644 index 000000000..38f2cb34b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/UUIDUtil.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import com.google.protobuf.ByteString; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.UUID; + +public final class UUIDUtil { + + private UUIDUtil() { + // utility class + } + + public static byte[] toBytes(final UUID uuid) { + return toByteBuffer(uuid).array(); + } + + public static ByteBuffer toByteBuffer(final UUID uuid) { + final ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); + byteBuffer.putLong(uuid.getMostSignificantBits()); + byteBuffer.putLong(uuid.getLeastSignificantBits()); + return byteBuffer.flip(); + } + + public static ByteString toByteString(final UUID uuid) { + return ByteString.copyFrom(toByteBuffer(uuid)); + } + + public static UUID fromByteString(final ByteString byteString) { + return fromBytes(byteString.toByteArray()); + } + + public static UUID fromBytes(final byte[] bytes) { + return fromByteBuffer(ByteBuffer.wrap(bytes)); + } + + public static UUID fromByteBuffer(final ByteBuffer byteBuffer) { + try { + final long mostSigBits = byteBuffer.getLong(); + final long leastSigBits = byteBuffer.getLong(); + if (byteBuffer.hasRemaining()) { + throw new IllegalArgumentException("unexpected byte array length; was greater than 16"); + } + return new UUID(mostSigBits, leastSigBits); + } catch (BufferUnderflowException e) { + throw new IllegalArgumentException("unexpected byte array length; was less than 16"); + } + } + + public static Optional fromStringSafe(final String uuidString) { + try { + return Optional.of(UUID.fromString(uuidString)); + } catch (final IllegalArgumentException e) { + return Optional.empty(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/UsernameHashZkProofVerifier.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/UsernameHashZkProofVerifier.java new file mode 100644 index 000000000..3851510cd --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/UsernameHashZkProofVerifier.java @@ -0,0 +1,15 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import org.signal.libsignal.usernames.BaseUsernameException; +import org.signal.libsignal.usernames.Username; + +public class UsernameHashZkProofVerifier { + public void verifyProof(final byte[] proof, final byte[] hash) throws BaseUsernameException { + Username.verifyProof(proof, hash); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java new file mode 100644 index 000000000..abe09b481 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java @@ -0,0 +1,220 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.util; + +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; +import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; +import java.time.Clock; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Locale.LanguageRange; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nonnull; +import org.apache.commons.lang3.StringUtils; + +public class Util { + + private static final Pattern COUNTRY_CODE_PATTERN = Pattern.compile("^\\+([17]|2[07]|3[0123469]|4[013456789]|5[12345678]|6[0123456]|8[1246]|9[0123458]|\\d{3})"); + + private static final PhoneNumberUtil PHONE_NUMBER_UTIL = PhoneNumberUtil.getInstance(); + + public static final Runnable NOOP = () -> {}; + + /** + * Checks that the given number is a valid, E164-normalized phone number. + * + * @param number the number to check + * + * @throws ImpossiblePhoneNumberException if the given number is not a valid phone number at all + * @throws NonNormalizedPhoneNumberException if the given number is a valid phone number, but isn't E164-normalized + */ + public static void requireNormalizedNumber(final String number) throws ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException { + if (!PHONE_NUMBER_UTIL.isPossibleNumber(number, null)) { + throw new ImpossiblePhoneNumberException(); + } + + try { + final PhoneNumber inputNumber = PHONE_NUMBER_UTIL.parse(number, null); + + // For normalization, we want to format from a version parsed with the country code removed. + // This handles some cases of "possible", but non-normalized input numbers with a doubled country code, that is + // with the format "+{country code} {country code} {national number}" + final int countryCode = inputNumber.getCountryCode(); + final String region = PHONE_NUMBER_UTIL.getRegionCodeForCountryCode(countryCode); + + final PhoneNumber normalizedNumber = switch (region) { + // the country code has no associated region. Be lenient (and simple) and accept the input number + case "ZZ", "001" -> inputNumber; + default -> { + final String maybeLeadingZero = + inputNumber.hasItalianLeadingZero() && inputNumber.isItalianLeadingZero() ? "0" : ""; + yield PHONE_NUMBER_UTIL.parse( + maybeLeadingZero + inputNumber.getNationalNumber(), region); + } + }; + + final String normalizedE164 = PHONE_NUMBER_UTIL.format(normalizedNumber, + PhoneNumberFormat.E164); + + if (!number.equals(normalizedE164)) { + throw new NonNormalizedPhoneNumberException(number, normalizedE164); + } + } catch (final NumberParseException e) { + throw new ImpossiblePhoneNumberException(e); + } + } + + public static String getCountryCode(String number) { + Matcher matcher = COUNTRY_CODE_PATTERN.matcher(number); + + if (matcher.find()) return matcher.group(1); + else return "0"; + } + + public static String getRegion(final String number) { + try { + final PhoneNumber phoneNumber = PHONE_NUMBER_UTIL.parse(number, null); + return StringUtils.defaultIfBlank(PHONE_NUMBER_UTIL.getRegionCodeForNumber(phoneNumber), "ZZ"); + } catch (final NumberParseException e) { + return "ZZ"; + } + } + + public static String getNumberPrefix(String number) { + String countryCode = getCountryCode(number); + int remaining = number.length() - (1 + countryCode.length()); + int prefixLength = Math.min(4, remaining); + + return number.substring(0, 1 + countryCode.length() + prefixLength); + } + + public static boolean isEmpty(String param) { + return param == null || param.length() == 0; + } + + public static boolean nonEmpty(String param) { + return !isEmpty(param); + } + + public static byte[] truncate(byte[] element, int length) { + byte[] result = new byte[length]; + System.arraycopy(element, 0, result, 0, result.length); + + return result; + } + + public static byte[][] split(byte[] input, int firstLength, int secondLength) { + byte[][] parts = new byte[2][]; + + parts[0] = new byte[firstLength]; + System.arraycopy(input, 0, parts[0], 0, firstLength); + + parts[1] = new byte[secondLength]; + System.arraycopy(input, firstLength, parts[1], 0, secondLength); + + return parts; + } + + public static byte[][] split(byte[] input, int firstLength, int secondLength, int thirdLength, int fourthLength) { + byte[][] parts = new byte[4][]; + + parts[0] = new byte[firstLength]; + System.arraycopy(input, 0, parts[0], 0, firstLength); + + parts[1] = new byte[secondLength]; + System.arraycopy(input, firstLength, parts[1], 0, secondLength); + + parts[2] = new byte[thirdLength]; + System.arraycopy(input, firstLength + secondLength, parts[2], 0, thirdLength); + + parts[3] = new byte[fourthLength]; + System.arraycopy(input, firstLength + secondLength + thirdLength, parts[3], 0, fourthLength); + + return parts; + } + + public static final long DAY_IN_MILLIS = 86400000L; + public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7; + + public static int currentDaysSinceEpoch(@Nonnull Clock clock) { + return Math.toIntExact(clock.millis() / DAY_IN_MILLIS); + } + + public static void sleep(long i) { + try { + Thread.sleep(i); + } catch (InterruptedException ie) {} + } + + public static void wait(Object object) { + try { + object.wait(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + } + + public static void wait(Object object, long timeoutMs) { + try { + object.wait(timeoutMs); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + } + + public static int hashCode(Object... objects) { + return Arrays.hashCode(objects); + } + + public static long todayInMillis() { + return todayInMillis(Clock.systemUTC()); + } + + public static long todayInMillis(Clock clock) { + return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(clock.millis())); + } + + public static long todayInMillisGivenOffsetFromNow(Clock clock, Duration offset) { + final long ms = offset.toMillis() + clock.millis(); + return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(ms)); + } + + public static Optional findBestLocale(List priorityList, Collection supportedLocales) { + return Optional.ofNullable(Locale.lookupTag(priorityList, supportedLocales)); + } + + /** + * Map ints to non-negative ints. + *
    + * Unlike Math.abs this method handles Integer.MIN_VALUE correctly. + * + * @param n any int value + * @return an int value guaranteed to be non-negative + */ + public static int ensureNonNegativeInt(int n) { + return n == Integer.MIN_VALUE ? 0 : Math.abs(n); + } + + /** + * Map longs to non-negative longs. + *
    + * Unlike Math.abs this method handles Long.MIN_VALUE correctly. + * + * @param n any long value + * @return a long value guaranteed to be non-negative + */ + public static long ensureNonNegativeLong(long n) { + return n == Long.MIN_VALUE ? 0 : Math.abs(n); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java new file mode 100644 index 000000000..5d502e1aa --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java @@ -0,0 +1,8 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.util; + +public record VerificationCode(String verificationCode) { +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/WeightedRandomSelect.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/WeightedRandomSelect.java new file mode 100644 index 000000000..bb23386f4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/WeightedRandomSelect.java @@ -0,0 +1,54 @@ +package org.whispersystems.textsecuregcm.util; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Select a random item according to its weight + * + * @param the type of the objects to select from + */ +public class WeightedRandomSelect { + + List> weightedItems; + long totalWeight; + + public WeightedRandomSelect(List> weightedItems) throws IllegalArgumentException { + this.weightedItems = weightedItems; + this.totalWeight = weightedItems.stream().mapToLong(Pair::second).sum(); + + weightedItems.stream().map(Pair::second).filter(w -> w < 0).findFirst().ifPresent(invalid -> { + throw new IllegalArgumentException("Illegal selection weight " + invalid); + }); + + if (weightedItems.isEmpty() || totalWeight == 0) { + throw new IllegalArgumentException("Cannot create an empty weighted random selector"); + } + } + + public T select() { + if (weightedItems.size() == 1) { + return weightedItems.get(0).first(); + } + long select = ThreadLocalRandom.current().nextLong(0, totalWeight); + long current = 0; + for (Pair item : weightedItems) { + /* + Accumulate weights for each item and select the first item whose + cumulative weight exceeds the selected value. nextLong() is exclusive, + so by the last item we're guaranteed to find a value as the + last item's weight is one more than the maximum value of select. + */ + current += item.second(); + if (current > select) { + return item.first(); + } + } + throw new IllegalStateException("totalWeight " + totalWeight + " exceeds item weights"); + } + + public static T select(List> weightedItems) { + return new WeightedRandomSelect(weightedItems).select(); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/LoggingUnhandledExceptionMapper.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/LoggingUnhandledExceptionMapper.java new file mode 100644 index 000000000..3886598f5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/LoggingUnhandledExceptionMapper.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.logging; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.HttpHeaders; +import io.dropwizard.jersey.errors.LoggingExceptionMapper; +import javax.inject.Provider; +import javax.ws.rs.core.Context; +import org.glassfish.jersey.server.ContainerRequest; +import org.slf4j.Logger; +import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; +import org.whispersystems.textsecuregcm.util.ua.UserAgent; +import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; + +/** + * Extends {@link LoggingExceptionMapper} to include the method and path in the log message, if they are available. + */ +public class LoggingUnhandledExceptionMapper extends LoggingExceptionMapper { + + @Context + private Provider request; + + public LoggingUnhandledExceptionMapper() { + super(); + } + + @VisibleForTesting + LoggingUnhandledExceptionMapper(final Logger logger) { + super(logger); + } + + @Override + protected String formatLogMessage(final long id, final Throwable exception) { + String requestMethod = "unknown method"; + String userAgent = "missing"; + String requestPath = "/{unknown path}"; + try { + // request shouldn’t be `null`, but it is technically possible + requestMethod = request.get().getMethod(); + requestPath = UriInfoUtil.getPathTemplate(request.get().getUriInfo()); + userAgent = request.get().getHeaderString(HttpHeaders.USER_AGENT); + + // streamline the user-agent if it is recognized + final UserAgent ua = UserAgentUtil.parseUserAgentString(userAgent); + userAgent = String.format("%s %s", ua.getPlatform(), ua.getVersion()); + } catch (final UnrecognizedUserAgentException ignored) { + + } catch (final Exception e) { + logger.warn("Unexpected exception getting request details", e); + } + + return String.format("%s at %s %s (%s)", + super.formatLogMessage(id, exception), + requestMethod, + requestPath, + userAgent) ; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/RequestLogEnabledFilter.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/RequestLogEnabledFilter.java new file mode 100644 index 000000000..640f4d47a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/RequestLogEnabledFilter.java @@ -0,0 +1,23 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.logging; + +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.FilterReply; + +class RequestLogEnabledFilter extends Filter { + + private volatile boolean requestLoggingEnabled = false; + + @Override + public FilterReply decide(final E event) { + return requestLoggingEnabled ? FilterReply.NEUTRAL : FilterReply.DENY; + } + + public void setRequestLoggingEnabled(final boolean requestLoggingEnabled) { + this.requestLoggingEnabled = requestLoggingEnabled; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/RequestLogEnabledFilterFactory.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/RequestLogEnabledFilterFactory.java new file mode 100644 index 000000000..7de341827 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/RequestLogEnabledFilterFactory.java @@ -0,0 +1,20 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.logging; + +import ch.qos.logback.access.spi.IAccessEvent; +import ch.qos.logback.core.filter.Filter; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.dropwizard.logging.filter.FilterFactory; + +@JsonTypeName("requestLogEnabled") +class RequestLogEnabledFilterFactory implements FilterFactory { + + @Override + public Filter build() { + return RequestLogManager.getHttpRequestLogFilter(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/RequestLogManager.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/RequestLogManager.java new file mode 100644 index 000000000..1fc38fa49 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/RequestLogManager.java @@ -0,0 +1,21 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.logging; + +import ch.qos.logback.access.spi.IAccessEvent; +import ch.qos.logback.core.filter.Filter; + +public class RequestLogManager { + private static final RequestLogEnabledFilter HTTP_REQUEST_LOG_FILTER = new RequestLogEnabledFilter<>(); + + static Filter getHttpRequestLogFilter() { + return HTTP_REQUEST_LOG_FILTER; + } + + public static void setRequestLoggingEnabled(final boolean enabled) { + HTTP_REQUEST_LOG_FILTER.setRequestLoggingEnabled(enabled); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/UncaughtExceptionHandler.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/UncaughtExceptionHandler.java new file mode 100644 index 000000000..35409fb05 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/UncaughtExceptionHandler.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.logging; + +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class UncaughtExceptionHandler { + + private static final Logger logger = LoggerFactory.getLogger(UncaughtExceptionHandler.class); + + public static void register() { + @Nullable final Thread.UncaughtExceptionHandler current = Thread.getDefaultUncaughtExceptionHandler(); + + if (current != null) { + logger.warn("Uncaught exception handler already exists: {}", current); + return; + } + + Thread.setDefaultUncaughtExceptionHandler((t, e) -> logger.error("Uncaught exception on thread {}", t, e)); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/UriInfoUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/UriInfoUtil.java new file mode 100644 index 000000000..bd94d2c18 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/logging/UriInfoUtil.java @@ -0,0 +1,21 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.logging; + +import org.glassfish.jersey.server.ExtendedUriInfo; + +public class UriInfoUtil { + + public static String getPathTemplate(final ExtendedUriInfo uriInfo) { + final StringBuilder pathBuilder = new StringBuilder(); + + for (int i = uriInfo.getMatchedTemplates().size() - 1; i >= 0; i--) { + pathBuilder.append(uriInfo.getMatchedTemplates().get(i).getTemplate()); + } + + return pathBuilder.toString(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ua/ClientPlatform.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ua/ClientPlatform.java new file mode 100644 index 000000000..e9c05e970 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ua/ClientPlatform.java @@ -0,0 +1,12 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.ua; + +public enum ClientPlatform { + ANDROID, + DESKTOP, + IOS; +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ua/UnrecognizedUserAgentException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ua/UnrecognizedUserAgentException.java new file mode 100644 index 000000000..149ed5406 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ua/UnrecognizedUserAgentException.java @@ -0,0 +1,20 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.ua; + +public class UnrecognizedUserAgentException extends Exception { + + public UnrecognizedUserAgentException() { + } + + public UnrecognizedUserAgentException(final String message) { + super(message); + } + + public UnrecognizedUserAgentException(final Throwable cause) { + super(cause); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ua/UserAgent.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ua/UserAgent.java new file mode 100644 index 000000000..d4253f14e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ua/UserAgent.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.ua; + +import com.vdurmont.semver4j.Semver; +import java.util.Objects; +import java.util.Optional; + +public class UserAgent { + + private final ClientPlatform platform; + private final Semver version; + private final String additionalSpecifiers; + + public UserAgent(final ClientPlatform platform, final Semver version) { + this(platform, version, null); + } + + public UserAgent(final ClientPlatform platform, final Semver version, final String additionalSpecifiers) { + this.platform = platform; + this.version = version; + this.additionalSpecifiers = additionalSpecifiers; + } + + public ClientPlatform getPlatform() { + return platform; + } + + public Semver getVersion() { + return version; + } + + public Optional getAdditionalSpecifiers() { + return Optional.ofNullable(additionalSpecifiers); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final UserAgent userAgent = (UserAgent)o; + return platform == userAgent.platform && + version.equals(userAgent.version) && + Objects.equals(additionalSpecifiers, userAgent.additionalSpecifiers); + } + + @Override + public int hashCode() { + return Objects.hash(platform, version, additionalSpecifiers); + } + + @Override + public String toString() { + return "UserAgent{" + + "platform=" + platform + + ", version=" + version + + ", additionalSpecifiers='" + additionalSpecifiers + '\'' + + '}'; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ua/UserAgentUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ua/UserAgentUtil.java new file mode 100644 index 000000000..fe04b9f91 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/util/ua/UserAgentUtil.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.ua; + +import com.google.common.annotations.VisibleForTesting; +import com.vdurmont.semver4j.Semver; +import io.grpc.Context; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; + +public class UserAgentUtil { + + public static final Context.Key USER_AGENT_CONTEXT_KEY = Context.key("x-signal-user-agent"); + + private static final Pattern STANDARD_UA_PATTERN = Pattern.compile("^Signal-(Android|Desktop|iOS)/([^ ]+)( (.+))?$", Pattern.CASE_INSENSITIVE); + + public static UserAgent parseUserAgentString(final String userAgentString) throws UnrecognizedUserAgentException { + if (StringUtils.isBlank(userAgentString)) { + throw new UnrecognizedUserAgentException("User-Agent string is blank"); + } + + try { + final UserAgent standardUserAgent = parseStandardUserAgentString(userAgentString); + + if (standardUserAgent != null) { + return standardUserAgent; + } + } catch (final Exception e) { + throw new UnrecognizedUserAgentException(e); + } + + throw new UnrecognizedUserAgentException(); + } + + public static UserAgent userAgentFromGrpcContext() { + return USER_AGENT_CONTEXT_KEY.get(); + } + + @VisibleForTesting + static UserAgent parseStandardUserAgentString(final String userAgentString) { + final Matcher matcher = STANDARD_UA_PATTERN.matcher(userAgentString); + + if (matcher.matches()) { + return new UserAgent(ClientPlatform.valueOf(matcher.group(1).toUpperCase()), new Semver(matcher.group(2)), StringUtils.stripToNull(matcher.group(4))); + } + + return null; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/AuthenticatedConnectListener.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/AuthenticatedConnectListener.java new file mode 100644 index 000000000..bfaae44a0 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/AuthenticatedConnectListener.java @@ -0,0 +1,229 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.websocket; + +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import java.util.EnumMap; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; +import org.whispersystems.textsecuregcm.push.PushNotificationManager; +import org.whispersystems.textsecuregcm.push.ReceiptSender; +import org.whispersystems.textsecuregcm.redis.RedisOperation; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; +import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; +import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; +import org.whispersystems.websocket.session.WebSocketSessionContext; +import org.whispersystems.websocket.setup.WebSocketConnectListener; +import reactor.core.scheduler.Scheduler; + +public class AuthenticatedConnectListener implements WebSocketConnectListener { + + private static final String OPEN_WEBSOCKET_COUNTER_NAME = + name(WebSocketConnection.class, "openWebsockets"); + private static final String CONNECTED_DURATION_TIMER_NAME = name(AuthenticatedConnectListener.class, + "connectedDuration"); + + private static final String AUTHENTICATED_TAG_NAME = "authenticated"; + + private static final long RENEW_PRESENCE_INTERVAL_MINUTES = 5; + + private static final Logger log = LoggerFactory.getLogger(AuthenticatedConnectListener.class); + + private final ReceiptSender receiptSender; + private final MessagesManager messagesManager; + private final PushNotificationManager pushNotificationManager; + private final ClientPresenceManager clientPresenceManager; + private final ScheduledExecutorService scheduledExecutorService; + private final Scheduler messageDeliveryScheduler; + private final ClientReleaseManager clientReleaseManager; + + private final Map openAuthenticatedWebsocketsByClientPlatform; + private final Map openUnauthenticatedWebsocketsByClientPlatform; + private final Map durationTimersByClientPlatform; + private final Map unauthenticatedDurationTimersByClientPlatform; + + private final AtomicInteger openAuthenticatedWebsocketsFromUnknownPlatforms; + private final AtomicInteger openUnauthenticatedWebsocketsFromUnknownPlatforms; + private final Timer durationTimerForUnknownPlatforms; + private final Timer unauthenticatedDurationTimerForUnknownPlatforms; + + public AuthenticatedConnectListener(ReceiptSender receiptSender, + MessagesManager messagesManager, + PushNotificationManager pushNotificationManager, + ClientPresenceManager clientPresenceManager, + ScheduledExecutorService scheduledExecutorService, + Scheduler messageDeliveryScheduler, + ClientReleaseManager clientReleaseManager) { + this.receiptSender = receiptSender; + this.messagesManager = messagesManager; + this.pushNotificationManager = pushNotificationManager; + this.clientPresenceManager = clientPresenceManager; + this.scheduledExecutorService = scheduledExecutorService; + this.messageDeliveryScheduler = messageDeliveryScheduler; + this.clientReleaseManager = clientReleaseManager; + + openAuthenticatedWebsocketsByClientPlatform = new EnumMap<>(ClientPlatform.class); + openUnauthenticatedWebsocketsByClientPlatform = new EnumMap<>(ClientPlatform.class); + durationTimersByClientPlatform = new EnumMap<>(ClientPlatform.class); + unauthenticatedDurationTimersByClientPlatform = new EnumMap<>(ClientPlatform.class); + + final Tags authenticatedTag = Tags.of(AUTHENTICATED_TAG_NAME, "true"); + final Tags unauthenticatedTag = Tags.of(AUTHENTICATED_TAG_NAME, "false"); + + for (final ClientPlatform clientPlatform : ClientPlatform.values()) { + openAuthenticatedWebsocketsByClientPlatform.put(clientPlatform, new AtomicInteger(0)); + openUnauthenticatedWebsocketsByClientPlatform.put(clientPlatform, new AtomicInteger(0)); + + final Tags clientPlatformTag = Tags.of(UserAgentTagUtil.PLATFORM_TAG, clientPlatform.name().toLowerCase()); + Metrics.gauge(OPEN_WEBSOCKET_COUNTER_NAME, clientPlatformTag.and(authenticatedTag), + openAuthenticatedWebsocketsByClientPlatform.get(clientPlatform)); + + Metrics.gauge(OPEN_WEBSOCKET_COUNTER_NAME, clientPlatformTag.and(unauthenticatedTag), + openUnauthenticatedWebsocketsByClientPlatform.get(clientPlatform)); + + durationTimersByClientPlatform.put(clientPlatform, + Metrics.timer(CONNECTED_DURATION_TIMER_NAME, clientPlatformTag.and(authenticatedTag))); + + unauthenticatedDurationTimersByClientPlatform.put(clientPlatform, + Metrics.timer(CONNECTED_DURATION_TIMER_NAME, clientPlatformTag.and(unauthenticatedTag))); + } + + openAuthenticatedWebsocketsFromUnknownPlatforms = new AtomicInteger(0); + openUnauthenticatedWebsocketsFromUnknownPlatforms = new AtomicInteger(0); + + final Tags unrecognizedPlatform = Tags.of(UserAgentTagUtil.PLATFORM_TAG, "unrecognized"); + Metrics.gauge(OPEN_WEBSOCKET_COUNTER_NAME, unrecognizedPlatform.and(authenticatedTag), + openAuthenticatedWebsocketsFromUnknownPlatforms); + + Metrics.gauge(OPEN_WEBSOCKET_COUNTER_NAME, unrecognizedPlatform.and(unauthenticatedTag), + openUnauthenticatedWebsocketsFromUnknownPlatforms); + + durationTimerForUnknownPlatforms = Metrics.timer(CONNECTED_DURATION_TIMER_NAME, + unrecognizedPlatform.and(authenticatedTag)); + + unauthenticatedDurationTimerForUnknownPlatforms = Metrics.timer(CONNECTED_DURATION_TIMER_NAME, + unrecognizedPlatform.and(unauthenticatedTag)); + } + + @Override + public void onWebSocketConnect(WebSocketSessionContext context) { + + final boolean authenticated = (context.getAuthenticated() != null); + final String userAgent = context.getClient().getUserAgent(); + final AtomicInteger openWebsocketAtomicInteger = getOpenWebsocketCounter(userAgent, authenticated); + final Timer connectionTimer = getConnectionTimer(userAgent, authenticated); + + if (authenticated) { + final AuthenticatedAccount auth = context.getAuthenticated(AuthenticatedAccount.class); + final Device device = auth.getAuthenticatedDevice(); + final Timer.Sample sample = Timer.start(); + final WebSocketConnection connection = new WebSocketConnection(receiptSender, + messagesManager, auth, device, + context.getClient(), + scheduledExecutorService, + messageDeliveryScheduler, + clientReleaseManager); + + openWebsocketAtomicInteger.incrementAndGet(); + + pushNotificationManager.handleMessagesRetrieved(auth.getAccount(), device, userAgent); + + final AtomicReference> renewPresenceFutureReference = new AtomicReference<>(); + + context.addWebsocketClosedListener((closingContext, statusCode, reason) -> { + openWebsocketAtomicInteger.decrementAndGet(); + sample.stop(connectionTimer); + + final ScheduledFuture renewPresenceFuture = renewPresenceFutureReference.get(); + + if (renewPresenceFuture != null) { + renewPresenceFuture.cancel(false); + } + + connection.stop(); + + RedisOperation.unchecked( + () -> clientPresenceManager.clearPresence(auth.getAccount().getUuid(), device.getId(), connection)); + RedisOperation.unchecked(() -> { + messagesManager.removeMessageAvailabilityListener(connection); + + if (messagesManager.hasCachedMessages(auth.getAccount().getUuid(), device.getId())) { + try { + pushNotificationManager.sendNewMessageNotification(auth.getAccount(), device.getId(), true); + } catch (NotPushRegisteredException ignored) { + } + } + }); + }); + + try { + connection.start(); + clientPresenceManager.setPresent(auth.getAccount().getUuid(), device.getId(), connection); + messagesManager.addMessageAvailabilityListener(auth.getAccount().getUuid(), device.getId(), connection); + + renewPresenceFutureReference.set(scheduledExecutorService.scheduleAtFixedRate(() -> RedisOperation.unchecked(() -> + clientPresenceManager.renewPresence(auth.getAccount().getUuid(), device.getId())), + RENEW_PRESENCE_INTERVAL_MINUTES, + RENEW_PRESENCE_INTERVAL_MINUTES, + TimeUnit.MINUTES)); + } catch (final Exception e) { + log.warn("Failed to initialize websocket", e); + context.getClient().close(1011, "Unexpected error initializing connection"); + } + } else { + openWebsocketAtomicInteger.incrementAndGet(); + final Timer.Sample sample = Timer.start(); + context.addWebsocketClosedListener((context1, statusCode, reason) -> { + openWebsocketAtomicInteger.decrementAndGet(); + sample.stop(connectionTimer); + }); + } + } + + private AtomicInteger getOpenWebsocketCounter(final String userAgentString, final boolean authenticated) { + try { + final ClientPlatform platform = UserAgentUtil.parseUserAgentString(userAgentString).getPlatform(); + return authenticated + ? openAuthenticatedWebsocketsByClientPlatform.get(platform) + : openUnauthenticatedWebsocketsByClientPlatform.get(platform); + } catch (final UnrecognizedUserAgentException e) { + return authenticated + ? openAuthenticatedWebsocketsFromUnknownPlatforms + : openUnauthenticatedWebsocketsFromUnknownPlatforms; + } + } + + private Timer getConnectionTimer(final String userAgentString, + final boolean authenticated) { + try { + final ClientPlatform platform = UserAgentUtil.parseUserAgentString(userAgentString).getPlatform(); + return authenticated + ? durationTimersByClientPlatform.get(platform) + : unauthenticatedDurationTimersByClientPlatform.get(platform); + } catch (final UnrecognizedUserAgentException e) { + return authenticated + ? durationTimerForUnknownPlatforms + : unauthenticatedDurationTimerForUnknownPlatforms; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/InvalidWebsocketAddressException.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/InvalidWebsocketAddressException.java new file mode 100644 index 000000000..7be8e3f6d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/InvalidWebsocketAddressException.java @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.websocket; + +public class InvalidWebsocketAddressException extends Exception { + public InvalidWebsocketAddressException(String serialized) { + super(serialized); + } + + public InvalidWebsocketAddressException(Exception e) { + super(e); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/ProvisioningAddress.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/ProvisioningAddress.java new file mode 100644 index 000000000..38008bf76 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/ProvisioningAddress.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.websocket; + +import java.security.SecureRandom; +import java.util.Base64; + +public class ProvisioningAddress extends WebsocketAddress { + + public ProvisioningAddress(String address, int id) { + super(address, id); + } + + public ProvisioningAddress(String serialized) throws InvalidWebsocketAddressException { + super(serialized); + } + + public String getAddress() { + return getNumber(); + } + + public static ProvisioningAddress generate() { + byte[] random = new byte[16]; + new SecureRandom().nextBytes(random); + + return new ProvisioningAddress(Base64.getUrlEncoder().withoutPadding().encodeToString(random), 0); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/ProvisioningConnectListener.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/ProvisioningConnectListener.java new file mode 100644 index 000000000..3817155be --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/ProvisioningConnectListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.websocket; + +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.push.ProvisioningManager; +import org.whispersystems.textsecuregcm.storage.PubSubProtos; +import org.whispersystems.textsecuregcm.util.HeaderUtils; +import org.whispersystems.websocket.session.WebSocketSessionContext; +import org.whispersystems.websocket.setup.WebSocketConnectListener; +import java.util.List; +import java.util.Optional; + +public class ProvisioningConnectListener implements WebSocketConnectListener { + + private final ProvisioningManager provisioningManager; + + public ProvisioningConnectListener(final ProvisioningManager provisioningManager) { + this.provisioningManager = provisioningManager; + } + + @Override + public void onWebSocketConnect(WebSocketSessionContext context) { + final ProvisioningAddress provisioningAddress = ProvisioningAddress.generate(); + context.addWebsocketClosedListener((context1, statusCode, reason) -> provisioningManager.removeListener(provisioningAddress)); + + provisioningManager.addListener(provisioningAddress, message -> { + assert message.getType() == PubSubProtos.PubSubMessage.Type.DELIVER; + + final Optional body = Optional.of(message.getContent().toByteArray()); + + context.getClient().sendRequest("PUT", "/v1/message", List.of(HeaderUtils.getTimestampHeader()), body) + .whenComplete((ignored, throwable) -> context.getClient().close(1000, "Closed")); + }); + + context.getClient().sendRequest("PUT", "/v1/address", List.of(HeaderUtils.getTimestampHeader()), + Optional.of(MessageProtos.ProvisioningUuid.newBuilder() + .setUuid(provisioningAddress.getAddress()) + .build() + .toByteArray())); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/WebSocketAccountAuthenticator.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/WebSocketAccountAuthenticator.java new file mode 100644 index 000000000..28f3a00cb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/WebSocketAccountAuthenticator.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.websocket; + +import static org.whispersystems.textsecuregcm.util.HeaderUtils.basicCredentialsFromAuthHeader; + +import com.google.common.net.HttpHeaders; +import io.dropwizard.auth.basic.BasicCredentials; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nullable; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.websocket.auth.AuthenticationException; +import org.whispersystems.websocket.auth.WebSocketAuthenticator; + + +public class WebSocketAccountAuthenticator implements WebSocketAuthenticator { + + private static final AuthenticationResult CREDENTIALS_NOT_PRESENTED = + new AuthenticationResult<>(Optional.empty(), false); + + private static final AuthenticationResult INVALID_CREDENTIALS_PRESENTED = + new AuthenticationResult<>(Optional.empty(), true); + + private final AccountAuthenticator accountAuthenticator; + + + public WebSocketAccountAuthenticator(final AccountAuthenticator accountAuthenticator) { + this.accountAuthenticator = accountAuthenticator; + } + + @Override + public AuthenticationResult authenticate(final UpgradeRequest request) + throws AuthenticationException { + try { + final AuthenticationResult authResultFromHeader = + authenticatedAccountFromHeaderAuth(request.getHeader(HttpHeaders.AUTHORIZATION)); + // the logic here is that if the `Authorization` header was set for the request, + // it takes the priority and we use the result of the header-based auth + // ignoring the result of the query-based auth. + if (authResultFromHeader.credentialsPresented()) { + return authResultFromHeader; + } + return authenticatedAccountFromQueryParams(request); + } catch (final Exception e) { + // this will be handled and logged upstream + // the most likely exception is a transient error connecting to account storage + throw new AuthenticationException(e); + } + } + + private AuthenticationResult authenticatedAccountFromQueryParams(final UpgradeRequest request) { + final Map> parameters = request.getParameterMap(); + final List usernames = parameters.get("login"); + final List passwords = parameters.get("password"); + if (usernames == null || usernames.size() == 0 || + passwords == null || passwords.size() == 0) { + return CREDENTIALS_NOT_PRESENTED; + } + final BasicCredentials credentials = new BasicCredentials(usernames.get(0).replace(" ", "+"), + passwords.get(0).replace(" ", "+")); + return new AuthenticationResult<>(accountAuthenticator.authenticate(credentials), true); + } + + private AuthenticationResult authenticatedAccountFromHeaderAuth(@Nullable final String authHeader) + throws AuthenticationException { + if (authHeader == null) { + return CREDENTIALS_NOT_PRESENTED; + } + return basicCredentialsFromAuthHeader(authHeader) + .map(credentials -> new AuthenticationResult<>(accountAuthenticator.authenticate(credentials), true)) + .orElse(INVALID_CREDENTIALS_PRESENTED); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/WebSocketConnection.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/WebSocketConnection.java new file mode 100644 index 000000000..e81a1446a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/WebSocketConnection.java @@ -0,0 +1,485 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.websocket; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.SharedMetricRegistries; +import com.google.common.annotations.VisibleForTesting; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.LongAdder; +import org.apache.commons.lang3.StringUtils; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.controllers.MessageController; +import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.metrics.MessageMetrics; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.push.DisplacedPresenceListener; +import org.whispersystems.textsecuregcm.push.ReceiptSender; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.MessageAvailabilityListener; +import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.util.Constants; +import org.whispersystems.textsecuregcm.util.HeaderUtils; +import org.whispersystems.websocket.WebSocketClient; +import org.whispersystems.websocket.WebSocketResourceProvider; +import org.whispersystems.websocket.messages.WebSocketResponseMessage; +import reactor.core.Disposable; +import reactor.core.observability.micrometer.Micrometer; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +public class WebSocketConnection implements MessageAvailabilityListener, DisplacedPresenceListener { + + private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); + private static final Histogram messageTime = metricRegistry.histogram( + name(MessageController.class, "message_delivery_duration")); + private static final Histogram primaryDeviceMessageTime = metricRegistry.histogram( + name(MessageController.class, "primary_device_message_delivery_duration")); + private static final Meter sendMessageMeter = metricRegistry.meter(name(WebSocketConnection.class, "send_message")); + private static final Meter messageAvailableMeter = metricRegistry.meter( + name(WebSocketConnection.class, "messagesAvailable")); + private static final Meter messagesPersistedMeter = metricRegistry.meter( + name(WebSocketConnection.class, "messagesPersisted")); + private static final Meter bytesSentMeter = metricRegistry.meter(name(WebSocketConnection.class, "bytes_sent")); + private static final Meter sendFailuresMeter = metricRegistry.meter(name(WebSocketConnection.class, "send_failures")); + + private static final String INITIAL_QUEUE_LENGTH_DISTRIBUTION_NAME = name(WebSocketConnection.class, + "initialQueueLength"); + private static final String INITIAL_QUEUE_DRAIN_TIMER_NAME = name(WebSocketConnection.class, "drainInitialQueue"); + private static final String SLOW_QUEUE_DRAIN_COUNTER_NAME = name(WebSocketConnection.class, "slowQueueDrain"); + private static final String QUEUE_DRAIN_RETRY_COUNTER_NAME = name(WebSocketConnection.class, "queueDrainRetry"); + private static final String DISPLACEMENT_COUNTER_NAME = name(WebSocketConnection.class, "displacement"); + private static final String NON_SUCCESS_RESPONSE_COUNTER_NAME = name(WebSocketConnection.class, + "clientNonSuccessResponse"); + private static final String CLIENT_CLOSED_MESSAGE_AVAILABLE_COUNTER_NAME = name(WebSocketConnection.class, + "messageAvailableAfterClientClosed"); + private static final String SEND_MESSAGES_FLUX_NAME = MetricsUtil.name(WebSocketConnection.class, + "sendMessages"); + private static final String SEND_MESSAGE_ERROR_COUNTER = MetricsUtil.name(WebSocketConnection.class, + "sendMessageError"); + private static final String STATUS_CODE_TAG = "status"; + private static final String STATUS_MESSAGE_TAG = "message"; + private static final String ERROR_TYPE_TAG = "errorType"; + + private static final long SLOW_DRAIN_THRESHOLD = 10_000; + + @VisibleForTesting + static final int MESSAGE_PUBLISHER_LIMIT_RATE = 100; + + @VisibleForTesting + static final int MAX_CONSECUTIVE_RETRIES = 5; + private static final long RETRY_DELAY_MILLIS = 1_000; + private static final int RETRY_DELAY_JITTER_MILLIS = 500; + + private static final int DEFAULT_SEND_FUTURES_TIMEOUT_MILLIS = 5 * 60 * 1000; + + private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class); + + private final ReceiptSender receiptSender; + private final MessagesManager messagesManager; + + private final AuthenticatedAccount auth; + private final Device device; + private final WebSocketClient client; + + private final int sendFuturesTimeoutMillis; + + private final ScheduledExecutorService scheduledExecutorService; + + private final Semaphore processStoredMessagesSemaphore = new Semaphore(1); + private final AtomicReference storedMessageState = new AtomicReference<>( + StoredMessageState.PERSISTED_NEW_MESSAGES_AVAILABLE); + private final AtomicBoolean sentInitialQueueEmptyMessage = new AtomicBoolean(false); + private final LongAdder sentMessageCounter = new LongAdder(); + private final AtomicLong queueDrainStartTime = new AtomicLong(); + private final AtomicInteger consecutiveRetries = new AtomicInteger(); + private final AtomicReference> retryFuture = new AtomicReference<>(); + private final AtomicReference messageSubscription = new AtomicReference<>(); + + private final Random random = new Random(); + private final Scheduler messageDeliveryScheduler; + + private final ClientReleaseManager clientReleaseManager; + + private enum StoredMessageState { + EMPTY, + CACHED_NEW_MESSAGES_AVAILABLE, + PERSISTED_NEW_MESSAGES_AVAILABLE + } + + public WebSocketConnection(ReceiptSender receiptSender, + MessagesManager messagesManager, + AuthenticatedAccount auth, + Device device, + WebSocketClient client, + ScheduledExecutorService scheduledExecutorService, + Scheduler messageDeliveryScheduler, + ClientReleaseManager clientReleaseManager) { + + this(receiptSender, + messagesManager, + auth, + device, + client, + DEFAULT_SEND_FUTURES_TIMEOUT_MILLIS, + scheduledExecutorService, + messageDeliveryScheduler, + clientReleaseManager); + } + + @VisibleForTesting + WebSocketConnection(ReceiptSender receiptSender, + MessagesManager messagesManager, + AuthenticatedAccount auth, + Device device, + WebSocketClient client, + int sendFuturesTimeoutMillis, + ScheduledExecutorService scheduledExecutorService, + Scheduler messageDeliveryScheduler, + ClientReleaseManager clientReleaseManager) { + + this.receiptSender = receiptSender; + this.messagesManager = messagesManager; + this.auth = auth; + this.device = device; + this.client = client; + this.sendFuturesTimeoutMillis = sendFuturesTimeoutMillis; + this.scheduledExecutorService = scheduledExecutorService; + this.messageDeliveryScheduler = messageDeliveryScheduler; + this.clientReleaseManager = clientReleaseManager; + } + + public void start() { + queueDrainStartTime.set(System.currentTimeMillis()); + processStoredMessages(); + } + + public void stop() { + final ScheduledFuture future = retryFuture.get(); + + if (future != null) { + future.cancel(false); + } + + final Disposable subscription = messageSubscription.get(); + if (subscription != null) { + subscription.dispose(); + } + + client.close(1000, "OK"); + } + + private CompletableFuture sendMessage(final Envelope message, StoredMessageInfo storedMessageInfo) { + // clear ephemeral field from the envelope + final Optional body = Optional.ofNullable(message.toBuilder().clearEphemeral().build().toByteArray()); + + sendMessageMeter.mark(); + sentMessageCounter.increment(); + bytesSentMeter.mark(body.map(bytes -> bytes.length).orElse(0)); + MessageMetrics.measureAccountEnvelopeUuidMismatches(auth.getAccount(), message); + + // X-Signal-Key: false must be sent until Android stops assuming it missing means true + return client.sendRequest("PUT", "/api/v1/message", + List.of(HeaderUtils.X_SIGNAL_KEY + ": false", HeaderUtils.getTimestampHeader()), body) + .whenComplete((ignored, throwable) -> { + if (throwable != null) { + sendFailuresMeter.mark(); + } else { + MessageMetrics.measureOutgoingMessageLatency(message.getServerTimestamp(), "websocket", client.getUserAgent(), clientReleaseManager); + } + }).thenCompose(response -> { + final CompletableFuture result; + if (isSuccessResponse(response)) { + + result = messagesManager.delete(auth.getAccount().getUuid(), device.getId(), + storedMessageInfo.guid(), storedMessageInfo.serverTimestamp()) + .thenApply(ignored -> null); + + if (message.getType() != Envelope.Type.SERVER_DELIVERY_RECEIPT) { + recordMessageDeliveryDuration(message.getTimestamp(), device); + sendDeliveryReceiptFor(message); + } + } else { + final List tags = new ArrayList<>( + List.of( + Tag.of(STATUS_CODE_TAG, String.valueOf(response.getStatus())), + UserAgentTagUtil.getPlatformTag(client.getUserAgent()) + )); + + // TODO Remove this once we've identified the cause of message rejections from desktop clients + if (StringUtils.isNotBlank(response.getMessage())) { + tags.add(Tag.of(STATUS_MESSAGE_TAG, response.getMessage())); + } + + Metrics.counter(NON_SUCCESS_RESPONSE_COUNTER_NAME, tags).increment(); + + result = CompletableFuture.completedFuture(null); + } + + return result; + }); + } + + public static void recordMessageDeliveryDuration(long timestamp, Device messageDestinationDevice) { + final long messageDeliveryDuration = System.currentTimeMillis() - timestamp; + messageTime.update(messageDeliveryDuration); + if (messageDestinationDevice.isMaster()) { + primaryDeviceMessageTime.update(messageDeliveryDuration); + } + } + + private void sendDeliveryReceiptFor(Envelope message) { + if (!message.hasSourceUuid()) { + return; + } + + try { + receiptSender.sendReceipt(ServiceIdentifier.valueOf(message.getDestinationUuid()), + auth.getAuthenticatedDevice().getId(), AciServiceIdentifier.valueOf(message.getSourceUuid()), + message.getTimestamp()); + } catch (IllegalArgumentException e) { + logger.error("Could not parse UUID: {}", message.getSourceUuid()); + } catch (Exception e) { + logger.warn("Failed to send receipt", e); + } + } + + private boolean isSuccessResponse(WebSocketResponseMessage response) { + return response != null && response.getStatus() >= 200 && response.getStatus() < 300; + } + + @VisibleForTesting + void processStoredMessages() { + if (processStoredMessagesSemaphore.tryAcquire()) { + final StoredMessageState state = storedMessageState.getAndSet(StoredMessageState.EMPTY); + final CompletableFuture queueCleared = new CompletableFuture<>(); + + sendMessages(state != StoredMessageState.PERSISTED_NEW_MESSAGES_AVAILABLE, queueCleared); + + setQueueClearedHandler(state, queueCleared); + } + } + + private void setQueueClearedHandler(final StoredMessageState state, final CompletableFuture queueCleared) { + + queueCleared.whenComplete((v, cause) -> { + if (cause == null) { + consecutiveRetries.set(0); + + if (sentInitialQueueEmptyMessage.compareAndSet(false, true)) { + final List tags = List.of( + UserAgentTagUtil.getPlatformTag(client.getUserAgent()) + ); + final long drainDuration = System.currentTimeMillis() - queueDrainStartTime.get(); + + Metrics.summary(INITIAL_QUEUE_LENGTH_DISTRIBUTION_NAME, tags).record(sentMessageCounter.sum()); + Metrics.timer(INITIAL_QUEUE_DRAIN_TIMER_NAME, tags).record(drainDuration, TimeUnit.MILLISECONDS); + + if (drainDuration > SLOW_DRAIN_THRESHOLD) { + Metrics.counter(SLOW_QUEUE_DRAIN_COUNTER_NAME, tags).increment(); + } + + client.sendRequest("PUT", "/api/v1/queue/empty", + Collections.singletonList(HeaderUtils.getTimestampHeader()), Optional.empty()); + } + } else { + storedMessageState.compareAndSet(StoredMessageState.EMPTY, state); + } + + processStoredMessagesSemaphore.release(); + + if (cause == null) { + if (storedMessageState.get() != StoredMessageState.EMPTY) { + processStoredMessages(); + } + } else { + if (client.isOpen()) { + + if (consecutiveRetries.incrementAndGet() > MAX_CONSECUTIVE_RETRIES) { + logger.warn("Max consecutive retries exceeded", cause); + client.close(1011, "Failed to retrieve messages"); + } else { + logger.debug("Failed to clear queue", cause); + final List tags = List.of(UserAgentTagUtil.getPlatformTag(client.getUserAgent())); + + Metrics.counter(QUEUE_DRAIN_RETRY_COUNTER_NAME, tags).increment(); + + final long delay = RETRY_DELAY_MILLIS + random.nextInt(RETRY_DELAY_JITTER_MILLIS); + retryFuture + .set(scheduledExecutorService.schedule(this::processStoredMessages, delay, TimeUnit.MILLISECONDS)); + } + } else { + logger.debug("Client disconnected before queue cleared"); + } + } + }); + } + + private void sendMessages(final boolean cachedMessagesOnly, final CompletableFuture queueCleared) { + + final Publisher messages = + messagesManager.getMessagesForDeviceReactive(auth.getAccount().getUuid(), device.getId(), cachedMessagesOnly); + + final AtomicBoolean hasErrored = new AtomicBoolean(); + + final Disposable subscription = Flux.from(messages) + .name(SEND_MESSAGES_FLUX_NAME) + .tap(Micrometer.metrics(Metrics.globalRegistry)) + .limitRate(MESSAGE_PUBLISHER_LIMIT_RATE) + .flatMapSequential(envelope -> + Mono.fromFuture(() -> sendMessage(envelope) + .orTimeout(sendFuturesTimeoutMillis, TimeUnit.MILLISECONDS)) + .onErrorResume( + // let the first error pass through to terminate the subscription + e -> { + final boolean firstError = !hasErrored.getAndSet(true); + measureSendMessageErrors(e, firstError); + + return !firstError; + }, + // otherwise just emit nothing + e -> Mono.empty() + ) + ) + .subscribeOn(messageDeliveryScheduler) + .subscribe( + // no additional consumer of values - it is Flux by now + null, + // this first error will terminate the stream, but we may get multiple errors from in-flight messages + queueCleared::completeExceptionally, + // completion + () -> queueCleared.complete(null) + ); + + messageSubscription.set(subscription); + } + + private void measureSendMessageErrors(Throwable e, final boolean terminal) { + final String errorType; + if (e instanceof TimeoutException) { + errorType = "timeout"; + } else if (e instanceof java.nio.channels.ClosedChannelException) { + errorType = "closedChannel"; + } else if (e == WebSocketResourceProvider.CONNECTION_CLOSED_EXCEPTION) { + errorType = "connectionClosed"; + } else { + logger.warn(terminal ? "Send message failure terminated stream" : "Send message failed", e); + errorType = "other"; + } + final Tags tags = Tags.of( + UserAgentTagUtil.getPlatformTag(client.getUserAgent()), + Tag.of(ERROR_TYPE_TAG, errorType)); + Metrics.counter(SEND_MESSAGE_ERROR_COUNTER, tags).increment(); + } + + private CompletableFuture sendMessage(Envelope envelope) { + final UUID messageGuid = UUID.fromString(envelope.getServerGuid()); + + if (envelope.getStory() && !client.shouldDeliverStories()) { + messagesManager.delete(auth.getAccount().getUuid(), device.getId(), messageGuid, envelope.getServerTimestamp()); + + return CompletableFuture.completedFuture(null); + } else { + return sendMessage(envelope, new StoredMessageInfo(messageGuid, envelope.getServerTimestamp())); + } + } + + @Override + public boolean handleNewMessagesAvailable() { + if (!client.isOpen()) { + // The client may become closed without successful removal of references to the `MessageAvailabilityListener` + Metrics.counter(CLIENT_CLOSED_MESSAGE_AVAILABLE_COUNTER_NAME).increment(); + return false; + } + + messageAvailableMeter.mark(); + + storedMessageState.compareAndSet(StoredMessageState.EMPTY, StoredMessageState.CACHED_NEW_MESSAGES_AVAILABLE); + + processStoredMessages(); + + return true; + } + + @Override + public boolean handleMessagesPersisted() { + if (!client.isOpen()) { + // The client may become without successful removal of references to the `MessageAvailabilityListener` + Metrics.counter(CLIENT_CLOSED_MESSAGE_AVAILABLE_COUNTER_NAME).increment(); + return false; + } + messagesPersistedMeter.mark(); + + storedMessageState.set(StoredMessageState.PERSISTED_NEW_MESSAGES_AVAILABLE); + + processStoredMessages(); + + return true; + } + + @Override + public void handleDisplacement(final boolean connectedElsewhere) { + final Tags tags = Tags.of( + UserAgentTagUtil.getPlatformTag(client.getUserAgent()), + Tag.of("connectedElsewhere", String.valueOf(connectedElsewhere)) + ); + + Metrics.counter(DISPLACEMENT_COUNTER_NAME, tags).increment(); + + final int code; + final String message; + + if (connectedElsewhere) { + code = 4409; + message = "Connected elsewhere"; + } else { + code = 1000; + message = "OK"; + } + + try { + client.close(code, message); + } catch (final Exception e) { + logger.warn("Orderly close failed", e); + + client.hardDisconnectQuietly(); + } + } + + private record StoredMessageInfo(UUID guid, long serverTimestamp) { + + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/WebsocketAddress.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/WebsocketAddress.java new file mode 100644 index 000000000..df670064f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/websocket/WebsocketAddress.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.websocket; + +import org.whispersystems.textsecuregcm.storage.PubSubAddress; + +public class WebsocketAddress implements PubSubAddress { + + private final String number; + private final long deviceId; + + public WebsocketAddress(String number, long deviceId) { + this.number = number; + this.deviceId = deviceId; + } + + public WebsocketAddress(String serialized) throws InvalidWebsocketAddressException { + try { + String[] parts = serialized.split(":", 2); + + if (parts.length != 2) { + throw new InvalidWebsocketAddressException("Bad address: " + serialized); + } + + this.number = parts[0]; + this.deviceId = Long.parseLong(parts[1]); + } catch (NumberFormatException e) { + throw new InvalidWebsocketAddressException(e); + } + } + + public String getNumber() { + return number; + } + + public long getDeviceId() { + return deviceId; + } + + public String serialize() { + return number + ":" + deviceId; + } + + public String toString() { + return serialize(); + } + + @Override + public boolean equals(Object other) { + if (other == null) return false; + if (!(other instanceof WebsocketAddress)) return false; + + WebsocketAddress that = (WebsocketAddress)other; + + return + this.number.equals(that.number) && + this.deviceId == that.deviceId; + } + + @Override + public int hashCode() { + return number.hashCode() ^ (int)deviceId; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractSinglePassCrawlAccountsCommand.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractSinglePassCrawlAccountsCommand.java new file mode 100644 index 000000000..539efe54d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/AbstractSinglePassCrawlAccountsCommand.java @@ -0,0 +1,87 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import io.dropwizard.Application; +import io.dropwizard.cli.Cli; +import io.dropwizard.cli.EnvironmentCommand; +import io.dropwizard.setup.Environment; +import java.util.Objects; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.util.logging.UncaughtExceptionHandler; +import reactor.core.publisher.ParallelFlux; +import reactor.core.scheduler.Schedulers; + +public abstract class AbstractSinglePassCrawlAccountsCommand extends EnvironmentCommand { + + private CommandDependencies commandDependencies; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private static final String SEGMENT_COUNT = "segments"; + + public AbstractSinglePassCrawlAccountsCommand(final String name, final String description) { + super(new Application<>() { + @Override + public void run(final WhisperServerConfiguration configuration, final Environment environment) { + } + }, name, description); + } + + @Override + public void configure(final Subparser subparser) { + super.configure(subparser); + + subparser.addArgument("--segments") + .type(Integer.class) + .dest(SEGMENT_COUNT) + .required(false) + .setDefault(1) + .help("The total number of segments for a DynamoDB scan"); + } + + protected CommandDependencies getCommandDependencies() { + return commandDependencies; + } + + @Override + protected void run(final Environment environment, final Namespace namespace, + final WhisperServerConfiguration configuration) throws Exception { + + UncaughtExceptionHandler.register(); + + MetricsUtil.configureRegistries(configuration, environment); + commandDependencies = CommandDependencies.build(getName(), environment, configuration); + + final int segments = Objects.requireNonNull(namespace.getInt(SEGMENT_COUNT)); + + logger.info("Crawling accounts with {} segments and {} processors", + segments, + Runtime.getRuntime().availableProcessors()); + + final CommandStopListener commandStopListener = new CommandStopListener(configuration.getCommandStopListener()); + try { + commandStopListener.start(); + crawlAccounts(commandDependencies.accountsManager().streamAllFromDynamo(segments, Schedulers.parallel())); + } finally { + commandStopListener.stop(); + } + + } + + @Override + public void onError(final Cli cli, final Namespace namespace, final Throwable throwable) { + logger.error("Unhandled error", throwable); + } + + protected abstract void crawlAccounts(final ParallelFlux accounts); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/AssignUsernameCommand.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/AssignUsernameCommand.java new file mode 100644 index 000000000..0b899071f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/AssignUsernameCommand.java @@ -0,0 +1,238 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import io.dropwizard.Application; +import io.dropwizard.cli.EnvironmentCommand; +import io.dropwizard.setup.Environment; +import io.lettuce.core.resource.ClientResources; +import java.time.Clock; +import java.util.Base64; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.WhisperServerService; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.controllers.SecureStorageController; +import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; +import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountLockManager; +import org.whispersystems.textsecuregcm.storage.Accounts; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import org.whispersystems.textsecuregcm.storage.MessagesCache; +import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb; +import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers; +import org.whispersystems.textsecuregcm.storage.Profiles; +import org.whispersystems.textsecuregcm.storage.ProfilesManager; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; +import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb; +import org.whispersystems.textsecuregcm.storage.ReportMessageManager; +import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableException; +import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException; +import org.whispersystems.textsecuregcm.util.DynamoDbFromConfig; +import org.whispersystems.textsecuregcm.util.ExceptionUtils; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +public class AssignUsernameCommand extends EnvironmentCommand { + + public AssignUsernameCommand() { + super(new Application<>() { + @Override + public void run(WhisperServerConfiguration configuration, Environment environment) { + + } + }, "assign-username-hash", "assign a username hash to an account"); + } + + @Override + public void configure(Subparser subparser) { + super.configure(subparser); + + subparser.addArgument("-u", "--usernameHash") + .dest("usernameHash") + .type(String.class) + .required(true) + .help("The username hash to assign"); + + subparser.addArgument("-e", "--encryptedUsername") + .dest("encryptedUsername") + .type(String.class) + .required(false) + .help("The encrypted username for the username link"); + + subparser.addArgument("-a", "--aci") + .dest("aci") + .type(String.class) + .required(true) + .help("The ACI of the account to which to assign the username"); + } + + @Override + protected void run(Environment environment, Namespace namespace, + WhisperServerConfiguration configuration) + throws Exception { + environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + ClientResources redisClusterClientResources = ClientResources.builder().build(); + + FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", + configuration.getCacheClusterConfiguration(), redisClusterClientResources); + + Scheduler messageDeliveryScheduler = Schedulers.fromExecutorService( + environment.lifecycle().executorService("messageDelivery-%d").maxThreads(4) + .build()); + ExecutorService keyspaceNotificationDispatchExecutor = environment.lifecycle() + .executorService(name(getClass(), "keyspaceNotification-%d")).maxThreads(4).build(); + ExecutorService messageDeletionExecutor = environment.lifecycle() + .executorService(name(getClass(), "messageDeletion-%d")).maxThreads(4).build(); + ExecutorService secureValueRecoveryExecutor = environment.lifecycle() + .executorService(name(getClass(), "secureValueRecoveryService-%d")).maxThreads(1).minThreads(1).build(); + ExecutorService storageServiceExecutor = environment.lifecycle() + .executorService(name(getClass(), "storageService-%d")).maxThreads(1).minThreads(1).build(); + ExecutorService accountLockExecutor = environment.lifecycle() + .executorService(name(getClass(), "accountLock-%d")).minThreads(1).maxThreads(1).build(); + ScheduledExecutorService secureValueRecoveryServiceRetryExecutor = environment.lifecycle() + .scheduledExecutorService(name(getClass(), "secureValueRecoveryServiceRetry-%d")).threads(1).build(); + ScheduledExecutorService storageServiceRetryExecutor = environment.lifecycle() + .scheduledExecutorService(name(getClass(), "storageServiceRetry-%d")).threads(1).build(); + + ExternalServiceCredentialsGenerator storageCredentialsGenerator = SecureStorageController.credentialsGenerator( + configuration.getSecureStorageServiceConfiguration()); + ExternalServiceCredentialsGenerator secureValueRecoveryCredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator( + configuration.getSvr2Configuration()); + + DynamicConfigurationManager dynamicConfigurationManager = new DynamicConfigurationManager<>( + configuration.getAppConfig().getApplication(), configuration.getAppConfig().getEnvironment(), + configuration.getAppConfig().getConfigurationName(), DynamicConfiguration.class); + dynamicConfigurationManager.start(); + + ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager( + dynamicConfigurationManager); + + DynamoDbAsyncClient dynamoDbAsyncClient = DynamoDbFromConfig.asyncClient( + configuration.getDynamoDbClientConfiguration(), WhisperServerService.AWSSDK_CREDENTIALS_PROVIDER); + + DynamoDbClient dynamoDbClient = DynamoDbFromConfig.client(configuration.getDynamoDbClientConfiguration(), + WhisperServerService.AWSSDK_CREDENTIALS_PROVIDER); + + RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords( + configuration.getDynamoDbTables().getRegistrationRecovery().getTableName(), + configuration.getDynamoDbTables().getRegistrationRecovery().getExpiration(), + dynamoDbClient, + dynamoDbAsyncClient + ); + + RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords); + + Accounts accounts = new Accounts( + dynamoDbClient, + dynamoDbAsyncClient, + configuration.getDynamoDbTables().getAccounts().getTableName(), + configuration.getDynamoDbTables().getAccounts().getPhoneNumberTableName(), + configuration.getDynamoDbTables().getAccounts().getPhoneNumberIdentifierTableName(), + configuration.getDynamoDbTables().getAccounts().getUsernamesTableName(), + configuration.getDynamoDbTables().getDeletedAccounts().getTableName(), + configuration.getDynamoDbTables().getAccounts().getScanPageSize()); + PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(dynamoDbClient, + configuration.getDynamoDbTables().getPhoneNumberIdentifiers().getTableName()); + Profiles profiles = new Profiles(dynamoDbClient, dynamoDbAsyncClient, + configuration.getDynamoDbTables().getProfiles().getTableName()); + KeysManager keys = new KeysManager( + dynamoDbAsyncClient, + configuration.getDynamoDbTables().getEcKeys().getTableName(), + configuration.getDynamoDbTables().getKemKeys().getTableName(), + configuration.getDynamoDbTables().getEcSignedPreKeys().getTableName(), + configuration.getDynamoDbTables().getKemLastResortKeys().getTableName(), + dynamicConfigurationManager); + MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(dynamoDbClient, dynamoDbAsyncClient, + configuration.getDynamoDbTables().getMessages().getTableName(), + configuration.getDynamoDbTables().getMessages().getExpiration(), + messageDeletionExecutor); + FaultTolerantRedisCluster messageInsertCacheCluster = new FaultTolerantRedisCluster("message_insert_cluster", + configuration.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClusterClientResources); + FaultTolerantRedisCluster messageReadDeleteCluster = new FaultTolerantRedisCluster("message_read_delete_cluster", + configuration.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClusterClientResources); + FaultTolerantRedisCluster clientPresenceCluster = new FaultTolerantRedisCluster("client_presence_cluster", + configuration.getClientPresenceClusterConfiguration(), redisClusterClientResources); + FaultTolerantRedisCluster rateLimitersCluster = new FaultTolerantRedisCluster("rate_limiters", + configuration.getRateLimitersCluster(), redisClusterClientResources); + SecureValueRecovery2Client secureValueRecovery2Client = new SecureValueRecovery2Client( + secureValueRecoveryCredentialsGenerator, secureValueRecoveryExecutor, secureValueRecoveryServiceRetryExecutor, + configuration.getSvr2Configuration()); + SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator, + storageServiceExecutor, storageServiceRetryExecutor, configuration.getSecureStorageServiceConfiguration()); + ClientPresenceManager clientPresenceManager = new ClientPresenceManager(clientPresenceCluster, + Executors.newSingleThreadScheduledExecutor(), keyspaceNotificationDispatchExecutor); + MessagesCache messagesCache = new MessagesCache(messageInsertCacheCluster, messageReadDeleteCluster, + keyspaceNotificationDispatchExecutor, messageDeliveryScheduler, messageDeletionExecutor, Clock.systemUTC()); + ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster); + ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(dynamoDbClient, + configuration.getDynamoDbTables().getReportMessage().getTableName(), + configuration.getReportMessageConfiguration().getReportTtl()); + ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, rateLimitersCluster, + configuration.getReportMessageConfiguration().getCounterTtl()); + MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, + reportMessageManager, messageDeletionExecutor); + AccountLockManager accountLockManager = new AccountLockManager(dynamoDbClient, + configuration.getDynamoDbTables().getDeletedAccountsLock().getTableName()); + AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster, + accountLockManager, keys, messagesManager, profilesManager, + secureStorageClient, secureValueRecovery2Client, clientPresenceManager, + experimentEnrollmentManager, registrationRecoveryPasswordsManager, accountLockExecutor, Clock.systemUTC()); + + final String usernameHash = namespace.getString("usernameHash"); + final String encryptedUsername = namespace.getString("encryptedUsername"); + final UUID accountIdentifier = UUID.fromString(namespace.getString("aci")); + + accountsManager.getByAccountIdentifier(accountIdentifier).ifPresentOrElse(account -> { + try { + final AccountsManager.UsernameReservation reservation = accountsManager.reserveUsernameHash(account, + List.of(Base64.getUrlDecoder().decode(usernameHash))).join(); + final Account result = accountsManager.confirmReservedUsernameHash( + account, + reservation.reservedUsernameHash(), + encryptedUsername == null ? null : Base64.getUrlDecoder().decode(encryptedUsername)).join(); + System.out.println("New username hash: " + Base64.getUrlEncoder().encodeToString(result.getUsernameHash().orElseThrow())); + System.out.println("New username link handle: " + result.getUsernameLinkHandle().toString()); + } catch (final CompletionException e) { + if (ExceptionUtils.unwrap(e) instanceof UsernameHashNotAvailableException) { + throw new IllegalArgumentException("Username hash already taken"); + } + + if (ExceptionUtils.unwrap(e) instanceof UsernameReservationNotFoundException) { + throw new IllegalArgumentException("Username hash reservation not found"); + } + + throw e; + } + }, + () -> { + throw new IllegalArgumentException("Account not found"); + }); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CertificateCommand.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CertificateCommand.java new file mode 100644 index 000000000..b54aaae0a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CertificateCommand.java @@ -0,0 +1,109 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import com.google.common.base.MoreObjects; +import com.google.protobuf.ByteString; +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.signal.libsignal.protocol.ecc.ECPrivateKey; +import org.whispersystems.textsecuregcm.entities.MessageProtos; + +import java.security.InvalidKeyException; +import java.util.Base64; +import java.util.Set; + +import io.dropwizard.cli.Command; +import io.dropwizard.setup.Bootstrap; + +public class CertificateCommand extends Command { + + private static final Set RESERVED_CERTIFICATE_IDS = Set.of( + 0xdeadc357 // Reserved for testing; see https://github.com/signalapp/libsignal-client/pull/118 + ); + + public CertificateCommand() { + super("certificate", "Generates server certificates for unidentified delivery"); + } + + @Override + public void configure(Subparser subparser) { + subparser.addArgument("-ca", "--ca") + .dest("ca") + .action(Arguments.storeTrue()) + .setDefault(Boolean.FALSE) + .help("Generate CA parameters"); + + subparser.addArgument("-k", "--key") + .dest("key") + .type(String.class) + .help("The CA private signing key"); + + subparser.addArgument("-i", "--id") + .dest("keyId") + .type(Integer.class) + .help("The key ID to create"); + } + + @Override + public void run(Bootstrap bootstrap, Namespace namespace) throws Exception { + if (MoreObjects.firstNonNull(namespace.getBoolean("ca"), false)) runCaCommand(); + else runCertificateCommand(namespace); + } + + private void runCaCommand() { + ECKeyPair keyPair = Curve.generateKeyPair(); + System.out.println("Public key : " + Base64.getEncoder().encodeToString(keyPair.getPublicKey().serialize())); + System.out.println("Private key: " + Base64.getEncoder().encodeToString(keyPair.getPrivateKey().serialize())); + } + + private void runCertificateCommand(Namespace namespace) throws InvalidKeyException, org.signal.libsignal.protocol.InvalidKeyException { + if (namespace.getString("key") == null) { + System.out.println("No key specified!"); + return; + } + + if (namespace.getInt("keyId") == null) { + System.out.print("No key id specified!"); + return; + } + + ECPrivateKey key = Curve.decodePrivatePoint(Base64.getDecoder().decode(namespace.getString("key"))); + int keyId = namespace.getInt("keyId"); + + if (RESERVED_CERTIFICATE_IDS.contains(keyId)) { + throw new IllegalArgumentException( + String.format("Key ID %08x has been reserved or revoked and may not be used in new certificates.", keyId)); + } + + ECKeyPair keyPair = Curve.generateKeyPair(); + + byte[] certificate = MessageProtos.ServerCertificate.Certificate.newBuilder() + .setId(keyId) + .setKey(ByteString.copyFrom(keyPair.getPublicKey().serialize())) + .build() + .toByteArray(); + + byte[] signature; + try { + signature = Curve.calculateSignature(key, certificate); + } catch (org.signal.libsignal.protocol.InvalidKeyException e) { + throw new InvalidKeyException(e); + } + + byte[] signedCertificate = MessageProtos.ServerCertificate.newBuilder() + .setCertificate(ByteString.copyFrom(certificate)) + .setSignature(ByteString.copyFrom(signature)) + .build() + .toByteArray(); + + System.out.println("Certificate: " + Base64.getEncoder().encodeToString(signedCertificate)); + System.out.println("Private key: " + Base64.getEncoder().encodeToString(keyPair.getPrivateKey().serialize())); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CheckDynamicConfigurationCommand.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CheckDynamicConfigurationCommand.java new file mode 100644 index 000000000..aeaaef826 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CheckDynamicConfigurationCommand.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import io.dropwizard.cli.Command; +import io.dropwizard.setup.Bootstrap; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; + +public class CheckDynamicConfigurationCommand extends Command { + + public CheckDynamicConfigurationCommand() { + super("check-dynamic-config", "Check validity of a dynamic configuration file"); + } + + @Override + public void configure(final Subparser subparser) { + subparser.addArgument("file") + .type(String.class) + .required(true) + .help("Dynamic configuration file to check"); + + subparser.addArgument("-c", "--class") + .type(String.class) + .nargs("*") + .setDefault(DynamicConfiguration.class.getCanonicalName()); + } + + @Override + public void run(final Bootstrap bootstrap, final Namespace namespace) throws Exception { + final Path path = Path.of(namespace.getString("file")); + + final List> configurationClasses; + + if (namespace.get("class") instanceof List) { + final List> classesFromArguments = new ArrayList<>(); + + for (final Object object : namespace.getList("class")) { + classesFromArguments.add(Class.forName(object.toString())); + } + + configurationClasses = classesFromArguments; + } else { + configurationClasses = List.of(Class.forName(namespace.getString("class"))); + } + + for (final Class configurationClass : configurationClasses) { + if (DynamicConfigurationManager.parseConfiguration(Files.readString(path), configurationClass).isPresent()) { + System.out.println(configurationClass.getSimpleName() + ": dynamic configuration file at " + path + " is valid"); + } else { + System.err.println(configurationClass.getSimpleName() + ": dynamic configuration file at " + path + " is not valid"); + } + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CommandDependencies.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CommandDependencies.java new file mode 100644 index 000000000..94c51ee10 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CommandDependencies.java @@ -0,0 +1,200 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import io.dropwizard.setup.Environment; +import io.lettuce.core.resource.ClientResources; +import java.io.IOException; +import java.security.cert.CertificateException; +import java.time.Clock; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.WhisperServerService; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.controllers.SecureStorageController; +import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; +import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client; +import org.whispersystems.textsecuregcm.storage.AccountLockManager; +import org.whispersystems.textsecuregcm.storage.Accounts; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import org.whispersystems.textsecuregcm.storage.MessagesCache; +import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb; +import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers; +import org.whispersystems.textsecuregcm.storage.Profiles; +import org.whispersystems.textsecuregcm.storage.ProfilesManager; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; +import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb; +import org.whispersystems.textsecuregcm.storage.ReportMessageManager; +import org.whispersystems.textsecuregcm.util.DynamoDbFromConfig; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +/** + * Construct utilities commonly used by worker commands + */ +record CommandDependencies( + AccountsManager accountsManager, + ProfilesManager profilesManager, + ReportMessageManager reportMessageManager, + MessagesCache messagesCache, + MessagesManager messagesManager, + ClientPresenceManager clientPresenceManager, + KeysManager keysManager, + FaultTolerantRedisCluster cacheCluster, + ClientResources redisClusterClientResources) { + + static CommandDependencies build( + final String name, + final Environment environment, + final WhisperServerConfiguration configuration) throws IOException, CertificateException { + Clock clock = Clock.systemUTC(); + + environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + ClientResources redisClusterClientResources = ClientResources.builder().build(); + + FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", + configuration.getCacheClusterConfiguration(), redisClusterClientResources); + + ScheduledExecutorService recurringJobExecutor = environment.lifecycle() + .scheduledExecutorService(name(name, "recurringJob-%d")).threads(2).build(); + Scheduler messageDeliveryScheduler = Schedulers.fromExecutorService( + environment.lifecycle().executorService("messageDelivery").maxThreads(4) + .build()); + ExecutorService keyspaceNotificationDispatchExecutor = environment.lifecycle() + .executorService(name(name, "keyspaceNotification-%d")).maxThreads(4).build(); + ExecutorService messageDeletionExecutor = environment.lifecycle() + .executorService(name(name, "messageDeletion-%d")).maxThreads(4).build(); + ExecutorService secureValueRecoveryServiceExecutor = environment.lifecycle() + .executorService(name(name, "secureValueRecoveryService-%d")).maxThreads(8).minThreads(8).build(); + ExecutorService storageServiceExecutor = environment.lifecycle() + .executorService(name(name, "storageService-%d")).maxThreads(8).minThreads(8).build(); + ExecutorService accountLockExecutor = environment.lifecycle() + .executorService(name(name, "accountLock-%d")).minThreads(8).maxThreads(8).build(); + + ScheduledExecutorService secureValueRecoveryServiceRetryExecutor = environment.lifecycle() + .scheduledExecutorService(name(name, "secureValueRecoveryServiceRetry-%d")).threads(1).build(); + ScheduledExecutorService storageServiceRetryExecutor = environment.lifecycle() + .scheduledExecutorService(name(name, "storageServiceRetry-%d")).threads(1).build(); + + ExternalServiceCredentialsGenerator storageCredentialsGenerator = SecureStorageController.credentialsGenerator( + configuration.getSecureStorageServiceConfiguration()); + ExternalServiceCredentialsGenerator secureValueRecoveryCredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator( + configuration.getSvr2Configuration()); + + DynamicConfigurationManager dynamicConfigurationManager = new DynamicConfigurationManager<>( + configuration.getAppConfig().getApplication(), configuration.getAppConfig().getEnvironment(), + configuration.getAppConfig().getConfigurationName(), DynamicConfiguration.class); + dynamicConfigurationManager.start(); + + ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager( + dynamicConfigurationManager); + + DynamoDbAsyncClient dynamoDbAsyncClient = DynamoDbFromConfig.asyncClient( + configuration.getDynamoDbClientConfiguration(), WhisperServerService.AWSSDK_CREDENTIALS_PROVIDER); + + DynamoDbClient dynamoDbClient = DynamoDbFromConfig.client( + configuration.getDynamoDbClientConfiguration(), WhisperServerService.AWSSDK_CREDENTIALS_PROVIDER); + + RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords( + configuration.getDynamoDbTables().getRegistrationRecovery().getTableName(), + configuration.getDynamoDbTables().getRegistrationRecovery().getExpiration(), + dynamoDbClient, + dynamoDbAsyncClient + ); + + RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = new RegistrationRecoveryPasswordsManager( + registrationRecoveryPasswords); + + Accounts accounts = new Accounts( + dynamoDbClient, + dynamoDbAsyncClient, + configuration.getDynamoDbTables().getAccounts().getTableName(), + configuration.getDynamoDbTables().getAccounts().getPhoneNumberTableName(), + configuration.getDynamoDbTables().getAccounts().getPhoneNumberIdentifierTableName(), + configuration.getDynamoDbTables().getAccounts().getUsernamesTableName(), + configuration.getDynamoDbTables().getDeletedAccounts().getTableName(), + configuration.getDynamoDbTables().getAccounts().getScanPageSize()); + PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(dynamoDbClient, + configuration.getDynamoDbTables().getPhoneNumberIdentifiers().getTableName()); + Profiles profiles = new Profiles(dynamoDbClient, dynamoDbAsyncClient, + configuration.getDynamoDbTables().getProfiles().getTableName()); + KeysManager keys = new KeysManager( + dynamoDbAsyncClient, + configuration.getDynamoDbTables().getEcKeys().getTableName(), + configuration.getDynamoDbTables().getKemKeys().getTableName(), + configuration.getDynamoDbTables().getEcSignedPreKeys().getTableName(), + configuration.getDynamoDbTables().getKemLastResortKeys().getTableName(), + dynamicConfigurationManager); + MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(dynamoDbClient, dynamoDbAsyncClient, + configuration.getDynamoDbTables().getMessages().getTableName(), + configuration.getDynamoDbTables().getMessages().getExpiration(), + messageDeletionExecutor); + FaultTolerantRedisCluster messageInsertCacheCluster = new FaultTolerantRedisCluster("message_insert_cluster", + configuration.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClusterClientResources); + FaultTolerantRedisCluster messageReadDeleteCluster = new FaultTolerantRedisCluster("message_read_delete_cluster", + configuration.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClusterClientResources); + FaultTolerantRedisCluster clientPresenceCluster = new FaultTolerantRedisCluster("client_presence_cluster", + configuration.getClientPresenceClusterConfiguration(), redisClusterClientResources); + FaultTolerantRedisCluster rateLimitersCluster = new FaultTolerantRedisCluster("rate_limiters", + configuration.getRateLimitersCluster(), redisClusterClientResources); + SecureValueRecovery2Client secureValueRecovery2Client = new SecureValueRecovery2Client( + secureValueRecoveryCredentialsGenerator, secureValueRecoveryServiceExecutor, + secureValueRecoveryServiceRetryExecutor, + configuration.getSvr2Configuration()); + SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator, + storageServiceExecutor, storageServiceRetryExecutor, configuration.getSecureStorageServiceConfiguration()); + ClientPresenceManager clientPresenceManager = new ClientPresenceManager(clientPresenceCluster, + recurringJobExecutor, keyspaceNotificationDispatchExecutor); + MessagesCache messagesCache = new MessagesCache(messageInsertCacheCluster, messageReadDeleteCluster, + keyspaceNotificationDispatchExecutor, messageDeliveryScheduler, messageDeletionExecutor, Clock.systemUTC()); + ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster); + ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(dynamoDbClient, + configuration.getDynamoDbTables().getReportMessage().getTableName(), + configuration.getReportMessageConfiguration().getReportTtl()); + ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, rateLimitersCluster, + configuration.getReportMessageConfiguration().getCounterTtl()); + MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, + reportMessageManager, messageDeletionExecutor); + AccountLockManager accountLockManager = new AccountLockManager(dynamoDbClient, + configuration.getDynamoDbTables().getDeletedAccountsLock().getTableName()); + AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster, + accountLockManager, keys, messagesManager, profilesManager, + secureStorageClient, secureValueRecovery2Client, clientPresenceManager, + experimentEnrollmentManager, registrationRecoveryPasswordsManager, accountLockExecutor, clock); + + environment.lifecycle().manage(messagesCache); + environment.lifecycle().manage(clientPresenceManager); + + return new CommandDependencies( + accountsManager, + profilesManager, + reportMessageManager, + messagesCache, + messagesManager, + clientPresenceManager, + keys, + cacheCluster, + redisClusterClientResources + ); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CommandStopListener.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CommandStopListener.java new file mode 100644 index 000000000..2c43b6e1a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CommandStopListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import io.dropwizard.lifecycle.Managed; +import java.io.FileWriter; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.configuration.CommandStopListenerConfiguration; + +/** + * When {@link Managed#stop()} is called, writes to a configured file path + *

    + * Useful for coordinating process termination via a shared file-system, such as a Kubernetes volume mount. + */ +public class CommandStopListener implements Managed { + + private static final Logger logger = LoggerFactory.getLogger(CommandStopListener.class); + + private final String path; + + CommandStopListener(CommandStopListenerConfiguration config) { + this.path = config.path(); + } + + @Override + public void start() { + + } + + @Override + public void stop() { + try { + try (FileWriter writer = new FileWriter(path)) { + writer.write("stopped"); + } + } catch (final IOException e) { + logger.error("Failed to open file {}", path, e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CrawlAccountsCommand.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CrawlAccountsCommand.java new file mode 100644 index 000000000..fb0c9ede0 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/CrawlAccountsCommand.java @@ -0,0 +1,165 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import static com.codahale.metrics.MetricRegistry.name; + +import io.dropwizard.Application; +import io.dropwizard.cli.EnvironmentCommand; +import io.dropwizard.setup.Environment; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import net.sourceforge.argparse4j.inf.Argument; +import net.sourceforge.argparse4j.inf.ArgumentParser; +import net.sourceforge.argparse4j.inf.ArgumentParserException; +import net.sourceforge.argparse4j.inf.ArgumentType; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.storage.AccountCleaner; +import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawler; +import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerCache; +import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerListener; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.storage.PushFeedbackProcessor; +import org.whispersystems.textsecuregcm.util.logging.UncaughtExceptionHandler; + +public class CrawlAccountsCommand extends EnvironmentCommand { + + private static final String CRAWL_TYPE = "crawlType"; + private static final String WORKER_COUNT = "workers"; + + private static final Logger logger = LoggerFactory.getLogger(CrawlAccountsCommand.class); + + public enum CrawlType implements ArgumentType { + GENERAL_PURPOSE, + ACCOUNT_CLEANER, + ; + + @Override + public CrawlType convert(final ArgumentParser parser, final Argument arg, final String value) + throws ArgumentParserException { + return CrawlType.valueOf(value); + } + } + + public CrawlAccountsCommand() { + super(new Application<>() { + @Override + public void run(final WhisperServerConfiguration configuration, final Environment environment) throws Exception { + + } + }, "crawl-accounts", "Runs account crawler tasks"); + } + + @Override + public void configure(final Subparser subparser) { + super.configure(subparser); + subparser.addArgument("--crawl-type") + .type(CrawlType.class) + .dest(CRAWL_TYPE) + .required(true) + .help("The type of crawl to perform"); + + subparser.addArgument("--workers") + .type(Integer.class) + .dest(WORKER_COUNT) + .required(true) + .help("The number of worker threads"); + } + + @Override + protected void run(final Environment environment, final Namespace namespace, + final WhisperServerConfiguration configuration) throws Exception { + + UncaughtExceptionHandler.register(); + + MetricsUtil.configureRegistries(configuration, environment); + + final CommandDependencies deps = CommandDependencies.build("account-crawler", environment, configuration); + final AccountsManager accountsManager = deps.accountsManager(); + + final FaultTolerantRedisCluster cacheCluster = deps.cacheCluster(); + final FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", + configuration.getMetricsClusterConfiguration(), deps.redisClusterClientResources()); + + final DynamicConfigurationManager dynamicConfigurationManager = + new DynamicConfigurationManager<>(configuration.getAppConfig().getApplication(), + configuration.getAppConfig().getEnvironment(), + configuration.getAppConfig().getConfigurationName(), + DynamicConfiguration.class); + + dynamicConfigurationManager.start(); + MetricsUtil.registerSystemResourceMetrics(environment); + + final int workers = Objects.requireNonNull(namespace.getInt(WORKER_COUNT)); + + final AccountDatabaseCrawler crawler = switch ((CrawlType) namespace.get(CRAWL_TYPE)) { + case GENERAL_PURPOSE -> { + final ExecutorService pushFeedbackUpdateExecutor = environment.lifecycle() + .executorService(name(getClass(), "pushFeedback-%d")).maxThreads(workers).minThreads(workers).build(); + + // TODO listeners must be ordered so that ones that directly update accounts come last, so that read-only ones are not working with stale data + final List accountDatabaseCrawlerListeners = List.of( + // PushFeedbackProcessor may update device properties + new PushFeedbackProcessor(accountsManager, pushFeedbackUpdateExecutor)); + + final AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache( + cacheCluster, + AccountDatabaseCrawlerCache.GENERAL_PURPOSE_PREFIX); + + yield new AccountDatabaseCrawler("General-purpose account crawler", + accountsManager, + accountDatabaseCrawlerCache, accountDatabaseCrawlerListeners, + configuration.getAccountDatabaseCrawlerConfiguration().getChunkSize() + ); + } + case ACCOUNT_CLEANER -> { + final AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache( + cacheCluster, AccountDatabaseCrawlerCache.ACCOUNT_CLEANER_PREFIX); + + yield new AccountDatabaseCrawler("Account cleaner crawler", + accountsManager, + accountDatabaseCrawlerCache, + List.of(new AccountCleaner(accountsManager)), + configuration.getAccountDatabaseCrawlerConfiguration().getChunkSize() + ); + } + }; + + environment.lifecycle().manage(new CommandStopListener(configuration.getCommandStopListener())); + + environment.lifecycle().getManagedObjects().forEach(managedObject -> { + try { + managedObject.start(); + } catch (final Exception e) { + logger.error("Failed to start managed object", e); + throw new RuntimeException(e); + } + }); + + try { + crawler.crawlAllAccounts(); + } catch (final Exception e) { + LoggerFactory.getLogger(CrawlAccountsCommand.class).error("Error crawling accounts", e); + } + + environment.lifecycle().getManagedObjects().forEach(managedObject -> { + try { + managedObject.stop(); + } catch (final Exception e) { + logger.error("Failed to stop managed object", e); + } + }); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/DeleteUserCommand.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/DeleteUserCommand.java new file mode 100644 index 000000000..6b58963df --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/DeleteUserCommand.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import io.dropwizard.Application; +import io.dropwizard.cli.EnvironmentCommand; +import io.dropwizard.setup.Environment; +import java.util.Optional; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.AccountsManager.DeletionReason; + +public class DeleteUserCommand extends EnvironmentCommand { + + private final Logger logger = LoggerFactory.getLogger(DeleteUserCommand.class); + + public DeleteUserCommand() { + super(new Application<>() { + @Override + public void run(WhisperServerConfiguration configuration, Environment environment) { + + } + }, "rmuser", "remove user"); + } + + @Override + public void configure(Subparser subparser) { + super.configure(subparser); + subparser.addArgument("-u", "--user") + .dest("user") + .type(String.class) + .required(true) + .help("The user to remove"); + } + + @Override + protected void run(Environment environment, Namespace namespace, WhisperServerConfiguration configuration) + throws Exception + { + try { + String[] users = namespace.getString("user").split(","); + environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + final CommandDependencies deps = CommandDependencies.build("rmuser", environment, configuration); + + AccountsManager accountsManager = deps.accountsManager(); + + for (String user : users) { + Optional account = accountsManager.getByE164(user); + + if (account.isPresent()) { + accountsManager.delete(account.get(), DeletionReason.ADMIN_DELETED).join(); + logger.warn("Removed " + account.get().getNumber()); + } else { + logger.warn("Account not found"); + } + } + } catch (Exception ex) { + logger.warn("Removal Exception", ex); + throw new RuntimeException(ex); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/MessagePersisterServiceCommand.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/MessagePersisterServiceCommand.java new file mode 100644 index 000000000..8b0ca1cab --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/MessagePersisterServiceCommand.java @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import io.dropwizard.Application; +import io.dropwizard.cli.ServerCommand; +import io.dropwizard.setup.Environment; +import java.time.Duration; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.storage.MessagePersister; +import org.whispersystems.textsecuregcm.util.logging.UncaughtExceptionHandler; + +public class MessagePersisterServiceCommand extends ServerCommand { + + private static final String WORKER_COUNT = "workers"; + + public MessagePersisterServiceCommand() { + super(new Application<>() { + @Override + public void run(WhisperServerConfiguration configuration, Environment environment) { + + } + }, "message-persister-service", + "Starts a persistent service to persist undelivered messages from Redis to Dynamo DB"); + } + + @Override + public void configure(final Subparser subparser) { + super.configure(subparser); + subparser.addArgument("--workers") + .type(Integer.class) + .dest(WORKER_COUNT) + .required(true) + .help("The number of worker threads"); + } + + @Override + protected void run(Environment environment, Namespace namespace, WhisperServerConfiguration configuration) + throws Exception { + + UncaughtExceptionHandler.register(); + + MetricsUtil.configureRegistries(configuration, environment); + + final CommandDependencies deps = CommandDependencies.build("message-persister-service", environment, configuration); + + final DynamicConfigurationManager dynamicConfigurationManager = new DynamicConfigurationManager<>( + configuration.getAppConfig().getApplication(), + configuration.getAppConfig().getEnvironment(), + configuration.getAppConfig().getConfigurationName(), + DynamicConfiguration.class); + + dynamicConfigurationManager.start(); + + final MessagePersister messagePersister = new MessagePersister(deps.messagesCache(), deps.messagesManager(), + deps.accountsManager(), + dynamicConfigurationManager, + Duration.ofMinutes(configuration.getMessageCacheConfiguration().getPersistDelayMinutes()), + namespace.getInt(WORKER_COUNT)); + + environment.lifecycle().manage(deps.messagesCache()); + environment.lifecycle().manage(messagePersister); + + MetricsUtil.registerSystemResourceMetrics(environment); + + super.run(environment, namespace, configuration); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/MigrateSignedECPreKeysCommand.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/MigrateSignedECPreKeysCommand.java new file mode 100644 index 000000000..770e23b05 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/MigrateSignedECPreKeysCommand.java @@ -0,0 +1,64 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import io.micrometer.core.instrument.Metrics; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.ParallelFlux; +import reactor.util.function.Tuple3; +import reactor.util.function.Tuples; +import reactor.util.retry.Retry; + +public class MigrateSignedECPreKeysCommand extends AbstractSinglePassCrawlAccountsCommand { + + private static final String STORE_KEY_ATTEMPT_COUNTER_NAME = + MetricsUtil.name(MigrateSignedECPreKeysCommand.class, "storeKeyAttempt"); + + // It's tricky to find, but the default connection count for the AWS SDK's async DynamoDB client is 50. We expect + // four workers, so this should keep us below the concurrency limit. + private static final int MAX_CONCURRENCY = 12; + + public MigrateSignedECPreKeysCommand() { + super("migrate-signed-ec-pre-keys", "Migrate signed EC pre-keys from Account records to a dedicated table"); + } + + @Override + protected void crawlAccounts(final ParallelFlux accounts) { + final KeysManager keysManager = getCommandDependencies().keysManager(); + + accounts.flatMap(account -> Flux.fromIterable(account.getDevices()) + .flatMap(device -> { + final List> keys = new ArrayList<>(2); + + if (device.getSignedPreKey(IdentityType.ACI) != null) { + keys.add(Tuples.of(account.getUuid(), device.getId(), device.getSignedPreKey(IdentityType.ACI))); + } + + if (device.getSignedPreKey(IdentityType.PNI) != null) { + keys.add(Tuples.of(account.getPhoneNumberIdentifier(), device.getId(), + device.getSignedPreKey(IdentityType.PNI))); + } + + return Flux.fromIterable(keys); + })) + .flatMap(keyTuple -> Mono.fromFuture(() -> keysManager.storeEcSignedPreKeyIfAbsent(keyTuple.getT1(), keyTuple.getT2(), keyTuple.getT3())) + .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)).onRetryExhaustedThrow((spec, rs) -> rs.failure())), + false, MAX_CONCURRENCY) + .doOnNext(keyStored -> Metrics.counter(STORE_KEY_ATTEMPT_COUNTER_NAME, "stored", String.valueOf(keyStored)).increment()) + .then() + .block(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/ScheduledApnPushNotificationSenderServiceCommand.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/ScheduledApnPushNotificationSenderServiceCommand.java new file mode 100644 index 000000000..6a5e98b90 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/ScheduledApnPushNotificationSenderServiceCommand.java @@ -0,0 +1,75 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import static com.codahale.metrics.MetricRegistry.name; + +import io.dropwizard.Application; +import io.dropwizard.cli.ServerCommand; +import io.dropwizard.setup.Environment; +import java.util.concurrent.ExecutorService; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.metrics.MetricsUtil; +import org.whispersystems.textsecuregcm.push.APNSender; +import org.whispersystems.textsecuregcm.push.ApnPushNotificationScheduler; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.util.logging.UncaughtExceptionHandler; + +public class ScheduledApnPushNotificationSenderServiceCommand extends ServerCommand { + + private static final String WORKER_COUNT = "workers"; + + public ScheduledApnPushNotificationSenderServiceCommand() { + super(new Application<>() { + @Override + public void run(WhisperServerConfiguration configuration, Environment environment) { + + } + }, "scheduled-apn-push-notification-sender-service", + "Starts a persistent service to send scheduled APNs push notifications"); + } + + @Override + public void configure(final Subparser subparser) { + super.configure(subparser); + subparser.addArgument("--workers") + .type(Integer.class) + .dest(WORKER_COUNT) + .required(true) + .help("The number of worker threads"); + } + + @Override + protected void run(Environment environment, Namespace namespace, WhisperServerConfiguration configuration) + throws Exception { + + UncaughtExceptionHandler.register(); + + MetricsUtil.configureRegistries(configuration, environment); + + final CommandDependencies deps = CommandDependencies.build("scheduled-apn-sender", environment, configuration); + + final FaultTolerantRedisCluster pushSchedulerCluster = new FaultTolerantRedisCluster("push_scheduler", + configuration.getPushSchedulerCluster(), deps.redisClusterClientResources()); + + final ExecutorService apnSenderExecutor = environment.lifecycle().executorService(name(getClass(), "apnSender-%d")) + .maxThreads(1).minThreads(1).build(); + + final APNSender apnSender = new APNSender(apnSenderExecutor, configuration.getApnConfiguration()); + final ApnPushNotificationScheduler apnPushNotificationScheduler = new ApnPushNotificationScheduler( + pushSchedulerCluster, apnSender, deps.accountsManager(), namespace.getInt(WORKER_COUNT)); + + environment.lifecycle().manage(apnSender); + environment.lifecycle().manage(apnPushNotificationScheduler); + + MetricsUtil.registerSystemResourceMetrics(environment); + + super.run(environment, namespace, configuration); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/ServerVersionCommand.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/ServerVersionCommand.java new file mode 100644 index 000000000..efe12a668 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/ServerVersionCommand.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import io.dropwizard.cli.Command; +import io.dropwizard.setup.Bootstrap; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.whispersystems.textsecuregcm.WhisperServerVersion; + +public class ServerVersionCommand extends Command { + + public ServerVersionCommand() { + super("version", "Print the version of the service"); + } + + @Override + public void configure(final Subparser subparser) { + } + + @Override + public void run(final Bootstrap bootstrap, final Namespace namespace) throws Exception { + System.out.println(WhisperServerVersion.getServerVersion()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/SetRequestLoggingEnabledTask.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/SetRequestLoggingEnabledTask.java new file mode 100644 index 000000000..4c0d277dd --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/SetRequestLoggingEnabledTask.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import io.dropwizard.servlets.tasks.Task; +import org.whispersystems.textsecuregcm.util.logging.RequestLogManager; + +import java.io.PrintWriter; +import java.util.List; +import java.util.Map; + +public class SetRequestLoggingEnabledTask extends Task { + + public SetRequestLoggingEnabledTask() { + super("set-request-logging-enabled"); + } + + @Override + public void execute(final Map> parameters, final PrintWriter out) { + if (parameters.containsKey("enabled") && parameters.get("enabled").size() == 1) { + final boolean enabled = Boolean.parseBoolean(parameters.get("enabled").get(0)); + + RequestLogManager.setRequestLoggingEnabled(enabled); + + if (enabled) { + out.println("Request logging now enabled"); + } else { + out.println("Request logging now disabled"); + } + } else { + out.println("Usage: set-request-logging-enabled?enabled=[true|false]"); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/SetUserDiscoverabilityCommand.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/SetUserDiscoverabilityCommand.java new file mode 100644 index 000000000..0c46a5d0a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/SetUserDiscoverabilityCommand.java @@ -0,0 +1,81 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import io.dropwizard.Application; +import io.dropwizard.cli.EnvironmentCommand; +import io.dropwizard.setup.Environment; +import java.util.Optional; +import java.util.UUID; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; + +public class SetUserDiscoverabilityCommand extends EnvironmentCommand { + + public SetUserDiscoverabilityCommand() { + + super(new Application<>() { + @Override + public void run(final WhisperServerConfiguration whisperServerConfiguration, final Environment environment) { + } + }, "set-discoverability", "sets whether a user should be discoverable in CDS"); + } + + @Override + public void configure(final Subparser subparser) { + super.configure(subparser); + + subparser.addArgument("-u", "--user") + .dest("user") + .type(String.class) + .required(true) + .help("the user (UUID or E164) for whom to change discoverability"); + + subparser.addArgument("-d", "--discoverable") + .dest("discoverable") + .type(Boolean.class) + .required(true) + .help("whether the user should be discoverable in CDS"); + } + + @Override + protected void run(final Environment environment, + final Namespace namespace, + final WhisperServerConfiguration configuration) throws Exception { + + try { + environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + final CommandDependencies deps = CommandDependencies.build("set-discoverability", environment, configuration); + final AccountsManager accountsManager = deps.accountsManager(); + Optional maybeAccount; + + try { + maybeAccount = accountsManager.getByAccountIdentifier(UUID.fromString(namespace.getString("user"))); + } catch (final IllegalArgumentException e) { + maybeAccount = accountsManager.getByE164(namespace.getString("user")); + } + + maybeAccount.ifPresentOrElse(account -> { + final boolean initiallyDiscoverable = account.isDiscoverableByPhoneNumber(); + accountsManager.update(account, a -> a.setDiscoverableByPhoneNumber(namespace.getBoolean("discoverable"))); + + System.out.format("Set discoverability flag for %s to %s (was previously %s)\n", + namespace.getString("user"), + namespace.getBoolean("discoverable"), + initiallyDiscoverable); + }, + () -> System.err.println("User not found: " + namespace.getString("user"))); + } catch (final Exception e) { + System.err.println("Failed to update discoverability setting for " + namespace.getString("user")); + e.printStackTrace(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/UnlinkDeviceCommand.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/UnlinkDeviceCommand.java new file mode 100644 index 000000000..045465cb1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/UnlinkDeviceCommand.java @@ -0,0 +1,91 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import io.dropwizard.Application; +import io.dropwizard.cli.EnvironmentCommand; +import io.dropwizard.setup.Environment; + +import java.util.List; +import java.util.UUID; +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; + +public class UnlinkDeviceCommand extends EnvironmentCommand { + + public UnlinkDeviceCommand() { + super(new Application<>() { + @Override + public void run(WhisperServerConfiguration configuration, Environment environment) { + + } + }, "unlink-device", "Unlink a device and clear messages"); + } + + @Override + public void configure(final Subparser subparser) { + super.configure(subparser); + + subparser.addArgument("-d", "--deviceId") + .dest("deviceIds") + .type(Long.class) + .action(Arguments.append()) + .required(true); + + subparser.addArgument("-u", "--uuid") + .help("the UUID of the account to modify") + .dest("uuid") + .type(String.class) + .required(true); + } + + @Override + protected void run(final Environment environment, final Namespace namespace, + final WhisperServerConfiguration configuration) throws Exception { + environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + final CommandStopListener commandStopListener = new CommandStopListener(configuration.getCommandStopListener()); + try { + commandStopListener.start(); + + final UUID aci = UUID.fromString(namespace.getString("uuid").trim()); + final List deviceIds = namespace.getList("deviceIds"); + + final CommandDependencies deps = CommandDependencies.build("unlink-device", environment, configuration); + + Account account = deps.accountsManager().getByAccountIdentifier(aci) + .orElseThrow(() -> new IllegalArgumentException("account id " + aci + " does not exist")); + + if (deviceIds.contains(Device.MASTER_ID)) { + throw new IllegalArgumentException("cannot delete primary device"); + } + + for (long deviceId : deviceIds) { + /** see {@link org.whispersystems.textsecuregcm.controllers.DeviceController#removeDevice} */ + System.out.format("Removing device %s::%d\n", aci, deviceId); + account = deps.accountsManager().update(account, a -> a.removeDevice(deviceId)); + + System.out.format("Removing keys for device %s::%d\n", aci, deviceId); + deps.keysManager().delete(account.getUuid(), deviceId).join(); + + System.out.format("Clearing additional messages for %s::%d\n", aci, deviceId); + deps.messagesManager().clear(account.getUuid(), deviceId).join(); + + System.out.format("Clearing presence state for %s::%d\n", aci, deviceId); + deps.clientPresenceManager().disconnectPresence(aci, deviceId); + + System.out.format("Device %s::%d successfully removed\n", aci, deviceId); + } + } finally { + commandStopListener.stop(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/ZkParamsCommand.java b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/ZkParamsCommand.java new file mode 100644 index 000000000..3cb85d4de --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/java/org/whispersystems/textsecuregcm/workers/ZkParamsCommand.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.signal.libsignal.zkgroup.ServerPublicParams; +import org.signal.libsignal.zkgroup.ServerSecretParams; + +import io.dropwizard.cli.Command; +import io.dropwizard.setup.Bootstrap; +import java.util.Base64; + +public class ZkParamsCommand extends Command { + + public ZkParamsCommand() { + super("zkparams", "Generates server zkparams"); + } + + @Override + public void configure(Subparser subparser) { + + } + + @Override + public void run(Bootstrap bootstrap, Namespace namespace) throws Exception { + ServerSecretParams serverSecretParams = ServerSecretParams.generate(); + ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams(); + + System.out.println("Public: " + Base64.getEncoder().withoutPadding().encodeToString(serverPublicParams.serialize())); + System.out.println("Private: " + Base64.getEncoder().withoutPadding().encodeToString(serverSecretParams.serialize())); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/PubSubMessage.proto b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/PubSubMessage.proto new file mode 100644 index 000000000..de8dd31db --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/PubSubMessage.proto @@ -0,0 +1,24 @@ +/** + * Copyright 2014 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +syntax = "proto2"; + +package textsecure; + +option java_package = "org.whispersystems.textsecuregcm.storage"; +option java_outer_classname = "PubSubProtos"; + +message PubSubMessage { + enum Type { + UNKNOWN = 0; + QUERY_DB = 1; + DELIVER = 2; + KEEPALIVE = 3; + CLOSE = 4; + CONNECTED = 5; + } + + optional Type type = 1; + optional bytes content = 2; +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/RegistrationService.proto b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/RegistrationService.proto new file mode 100644 index 000000000..5208059ac --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/RegistrationService.proto @@ -0,0 +1,409 @@ +syntax = "proto3"; + +option java_multiple_files = true; + +package org.signal.registration.rpc; + +service RegistrationService { + /** + * Create a new registration session for a given destination phone number. + */ + rpc CreateSession (CreateRegistrationSessionRequest) returns (CreateRegistrationSessionResponse) {} + + /** + * Retrieves session metadata for a given session. + */ + rpc GetSessionMetadata (GetRegistrationSessionMetadataRequest) returns (GetRegistrationSessionMetadataResponse) {} + + /** + * Sends a verification code to a destination phone number within the context + * of a previously-created registration session. + */ + rpc SendVerificationCode (SendVerificationCodeRequest) returns (SendVerificationCodeResponse) {} + + /** + * Checks a client-provided verification code for a given registration + * session. + */ + rpc CheckVerificationCode (CheckVerificationCodeRequest) returns (CheckVerificationCodeResponse) {} +} + +message CreateRegistrationSessionRequest { + /** + * The phone number for which to create a new registration session. + */ + uint64 e164 = 1; + + /** + * Indicates whether an account already exists with the given e164 (i.e. this + * session represents a "re-registration" attempt). + */ + bool account_exists_with_e164 = 2; +} + +message CreateRegistrationSessionResponse { + oneof response { + /** + * Metadata for the newly-created session. + */ + RegistrationSessionMetadata session_metadata = 1; + + /** + * A response explaining why a session could not be created as requested. + */ + CreateRegistrationSessionError error = 2; + } +} + +message RegistrationSessionMetadata { + /** + * An opaque sequence of bytes that uniquely identifies the registration + * session associated with this registration attempt. + */ + bytes session_id = 1; + + /** + * Indicates whether a valid verification code has been submitted in the scope + * of this session. + */ + bool verified = 2; + + /** + * The phone number associated with this registration session. + */ + uint64 e164 = 3; + + /** + * Indicates whether the caller may request delivery of a verification code + * via SMS now or at some time in the future. If true, the time a caller must + * wait before requesting a verification code via SMS is given in the + * `next_sms_seconds` field. + */ + bool may_request_sms = 4; + + /** + * The duration, in seconds, after which a caller will next be allowed to + * request delivery of a verification code via SMS if `may_request_sms` is + * true. If zero, a caller may request a verification code via SMS + * immediately. If `may_request_sms` is false, this field has no meaning. + */ + uint64 next_sms_seconds = 5; + + /** + * Indicates whether the caller may request delivery of a verification code + * via a phone call now or at some time in the future. If true, the time a + * caller must wait before requesting a verification code via SMS is given in + * the `next_voice_call_seconds` field. If false, simply waiting will not + * allow the caller to request a phone call and the caller may need to + * perform some other action (like attempting verification code delivery via + * SMS) before requesting a voice call. + */ + bool may_request_voice_call = 6; + + /** + * The duration, in seconds, after which a caller will next be allowed to + * request delivery of a verification code via a phone call if + * `may_request_voice_call` is true. If zero, a caller may request a + * verification code via a phone call immediately. If `may_request_voice_call` + * is false, this field has no meaning. + */ + uint64 next_voice_call_seconds = 7; + + /** + * Indicates whether the caller may submit new verification codes now or at + * some time in the future. If true, the time a caller must wait before + * submitting a verification code is given in the `next_code_check_seconds` + * field. If false, simply waiting will not allow the caller to submit a + * verification code and the caller may need to perform some other action + * (like requesting delivery of a verification code) before checking a + * verification code. + */ + bool may_check_code = 8; + + /** + * The duration, in seconds, after which a caller will next be allowed to + * submit a verification code if `may_check_code` is true. If zero, a caller + * may submit a verification code immediately. If `may_check_code` is false, + * this field has no meaning. + */ + uint64 next_code_check_seconds = 9; + + /** + * The duration, in seconds, after which this session will expire. + */ + uint64 expiration_seconds = 10; +} + +message CreateRegistrationSessionError { + /** + * The type of error that prevented a session from being created. + */ + CreateRegistrationSessionErrorType error_type = 1; + + /** + * Indicates that this error may succeed if retried without modification after + * a delay indicated by `retry_after_seconds`. If false, callers should not + * retry the request without modification. + */ + bool may_retry = 2; + + /** + * If this error may be retried,, indicates the duration in seconds from the + * present after which the request may be retried without modification. This + * value has no meaning otherwise. + */ + uint64 retry_after_seconds = 3; +} + +enum CreateRegistrationSessionErrorType { + CREATE_REGISTRATION_SESSION_ERROR_TYPE_UNSPECIFIED = 0; + + /** + * Indicates that a session could not be created because too many requests to + * create a session for the given phone number have been received in some + * window of time. Callers should wait and try again later. + */ + CREATE_REGISTRATION_SESSION_ERROR_TYPE_RATE_LIMITED = 1; + + /** + * Indicates that the provided phone number could not be parsed. + */ + CREATE_REGISTRATION_SESSION_ERROR_TYPE_ILLEGAL_PHONE_NUMBER = 2; +} + +message GetRegistrationSessionMetadataRequest { + /** + * The ID of the session for which to retrieve metadata. + */ + bytes session_id = 1; +} + +message GetRegistrationSessionMetadataResponse { + oneof response { + RegistrationSessionMetadata session_metadata = 1; + GetRegistrationSessionMetadataError error = 2; + } +} + +message GetRegistrationSessionMetadataError { + GetRegistrationSessionMetadataErrorType error_type = 1; +} + +enum GetRegistrationSessionMetadataErrorType { + GET_REGISTRATION_SESSION_METADATA_ERROR_TYPE_UNSPECIFIED = 0; + + /** + * No session was found with the given identifier. + */ + GET_REGISTRATION_SESSION_METADATA_ERROR_TYPE_NOT_FOUND = 1; +} + +message SendVerificationCodeRequest { + + reserved 1; + + /** + * The message transport to use to send a verification code to the destination + * phone number. + */ + MessageTransport transport = 2; + + /** + * A prioritized list of languages accepted by the destination; should be + * provided in the same format as the value of an HTTP Accept-Language header. + */ + string accept_language = 3; + + /** + * The type of client requesting a verification code. + */ + ClientType client_type = 4; + + /** + * The ID of a session within which to send (or re-send) a verification code. + */ + bytes session_id = 5; + + /** + * If provided, always attempt to use the specified sender to send + * this message. + */ + string sender_name = 6; +} + +enum MessageTransport { + MESSAGE_TRANSPORT_UNSPECIFIED = 0; + MESSAGE_TRANSPORT_SMS = 1; + MESSAGE_TRANSPORT_VOICE = 2; +} + +enum ClientType { + CLIENT_TYPE_UNSPECIFIED = 0; + CLIENT_TYPE_IOS = 1; + CLIENT_TYPE_ANDROID_WITH_FCM = 2; + CLIENT_TYPE_ANDROID_WITHOUT_FCM = 3; +} + +message SendVerificationCodeResponse { + reserved 1; + + /** + * Metadata for the named session. May be absent if the session could not be + * found or has expired. + */ + RegistrationSessionMetadata session_metadata = 2; + + /** + * If a code could not be sent, explains the underlying error. Will be absent + * if a code was sent successfully. Note that both an error and session + * metadata may be present in the same response because the session metadata + * may include information helpful for resolving the underlying error (i.e. + * "next attempt" times). + */ + SendVerificationCodeError error = 3; +} + +message SendVerificationCodeError { + /** + * The type of error that prevented a verification code from being sent. + */ + SendVerificationCodeErrorType error_type = 1; + + /** + * Indicates that this error may succeed if retried without modification after + * a delay indicated by `retry_after_seconds`. If false, callers should not + * retry the request without modification. + */ + bool may_retry = 2; + + /** + * If this error may be retried,, indicates the duration in seconds from the + * present after which the request may be retried without modification. This + * value has no meaning otherwise. + */ + uint64 retry_after_seconds = 3; +} + +enum SendVerificationCodeErrorType { + SEND_VERIFICATION_CODE_ERROR_TYPE_UNSPECIFIED = 0; + + /** + * The sender received and understood the request to send a verification code, + * but declined to do so (i.e. due to rate limits or suspected fraud). + */ + SEND_VERIFICATION_CODE_ERROR_TYPE_SENDER_REJECTED = 1; + + /** + * The sender could not process or would not accept some part of a request + * (e.g. a valid phone number that cannot receive SMS messages). + */ + SEND_VERIFICATION_CODE_ERROR_TYPE_SENDER_ILLEGAL_ARGUMENT = 2; + + /** + * A verification could could not be sent via the requested channel due to + * timing/rate restrictions. The response object containing this error should + * include session metadata that indicates when the next attempt is allowed. + */ + SEND_VERIFICATION_CODE_ERROR_TYPE_RATE_LIMITED = 3; + + /** + * No session was found with the given ID. + */ + SEND_VERIFICATION_CODE_ERROR_TYPE_SESSION_NOT_FOUND = 4; + + /** + * A new verification could could not be sent because the session has already + * been verified. + */ + SEND_VERIFICATION_CODE_ERROR_TYPE_SESSION_ALREADY_VERIFIED = 5; + + /** + * A verification code could not be sent via the requested transport because + * the destination phone number (or the sender) does not support the requested + * transport. + */ + SEND_VERIFICATION_CODE_ERROR_TYPE_TRANSPORT_NOT_ALLOWED = 6; +} + +message CheckVerificationCodeRequest { + /** + * The session ID returned when sending a verification code. + */ + bytes session_id = 1; + + /** + * The client-provided verification code. + */ + string verification_code = 2; +} + +message CheckVerificationCodeResponse { + reserved 1; + + /** + * Metadata for the named session. May be absent if the session could not be + * found or has expired. + */ + RegistrationSessionMetadata session_metadata = 2; + + /** + * If a code could not be checked, explains the underlying error. Will be + * absent if no error occurred. Note that both an error and session + * metadata may be present in the same response because the session metadata + * may include information helpful for resolving the underlying error (i.e. + * "next attempt" times). + */ + CheckVerificationCodeError error = 3; +} + +message CheckVerificationCodeError { + /** + * The type of error that prevented a verification code from being checked. + */ + CheckVerificationCodeErrorType error_type = 1; + + /** + * Indicates that this error may succeed if retried without modification after + * a delay indicated by `retry_after_seconds`. If false, callers should not + * retry the request without modification. + */ + bool may_retry = 2; + + /** + * If this error may be retried,, indicates the duration in seconds from the + * present after which the request may be retried without modification. This + * value has no meaning otherwise. + */ + uint64 retry_after_seconds = 3; +} + +enum CheckVerificationCodeErrorType { + CHECK_VERIFICATION_CODE_ERROR_TYPE_UNSPECIFIED = 0; + + /** + * The caller has attempted to submit a verification code even though no + * verification codes have been sent within the scope of this session. The + * caller must issue a "send code" request before trying again. + */ + CHECK_VERIFICATION_CODE_ERROR_TYPE_NO_CODE_SENT = 1; + + /** + * The caller has made too many guesses within some period of time. Callers + * should wait for the duration prescribed in the session metadata object + * elsewhere in the response before trying again. + */ + CHECK_VERIFICATION_CODE_ERROR_TYPE_RATE_LIMITED = 2; + + /** + * The session identified in this request could not be found (possibly due to + * session expiration). + */ + CHECK_VERIFICATION_CODE_ERROR_TYPE_SESSION_NOT_FOUND = 3; + + /** + * The session identified in this request is still active, but the most + * recently-sent code has expired. Callers should request a new code, then + * try again. + */ + CHECK_VERIFICATION_CODE_ERROR_TYPE_ATTEMPT_EXPIRED = 4; +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/TextSecure.proto b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/TextSecure.proto new file mode 100644 index 000000000..ef340fb1b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/TextSecure.proto @@ -0,0 +1,66 @@ +/** + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +syntax = "proto2"; + +package textsecure; + +option java_package = "org.whispersystems.textsecuregcm.entities"; +option java_outer_classname = "MessageProtos"; + +message Envelope { + enum Type { + UNKNOWN = 0; + CIPHERTEXT = 1; + KEY_EXCHANGE = 2; + PREKEY_BUNDLE = 3; + SERVER_DELIVERY_RECEIPT = 5; + UNIDENTIFIED_SENDER = 6; + reserved 7; + PLAINTEXT_CONTENT = 8; // for decryption error receipts + } + + optional Type type = 1; + optional string source_uuid = 11; + optional uint32 source_device = 7; + optional uint64 timestamp = 5; + optional bytes content = 8; // Contains an encrypted Content + optional string server_guid = 9; + optional uint64 server_timestamp = 10; + optional bool ephemeral = 12; // indicates that the message should not be persisted if the recipient is offline + optional string destination_uuid = 13; + optional bool urgent = 14 [default=true]; + optional string updated_pni = 15; + optional bool story = 16; // indicates that the content is a story. + optional bytes report_spam_token = 17; // token sent when reporting spam + // next: 18 +} + +message ProvisioningUuid { + optional string uuid = 1; +} + +message ServerCertificate { + message Certificate { + optional uint32 id = 1; + optional bytes key = 2; + } + + optional bytes certificate = 1; + optional bytes signature = 2; +} + +message SenderCertificate { + message Certificate { + optional string sender = 1; + optional string sender_uuid = 6; + optional uint32 sender_device = 2; + optional fixed64 expires = 3; + optional bytes identity_key = 4; + optional ServerCertificate signer = 5; + } + + optional bytes certificate = 1; + optional bytes signature = 2; +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/calling.proto b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/calling.proto new file mode 100644 index 000000000..e52858bc6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/calling.proto @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +syntax = "proto3"; + +option java_multiple_files = true; + +package org.signal.chat.calling; + +/** + * Provides methods for getting credentials for one-on-one and group calls. + */ +service Calling { + + /** + * Generates and returns TURN credentials for the caller. + * + * This RPC may fail with a `RESOURCE_EXHAUSTED` status if a rate limit for + * generating TURN credentials has been exceeded, in which case a + * `retry-after` header containing an ISO 8601 duration string will be present + * in the response trailers. + */ + rpc GetTurnCredentials(GetTurnCredentialsRequest) returns (GetTurnCredentialsResponse) {} +} + +message GetTurnCredentialsRequest {} + +message GetTurnCredentialsResponse { + /** + * A username that can be presented to authenticate with a TURN server. + */ + string username = 1; + + /** + * A password that can be presented to authenticate with a TURN server. + */ + string password = 2; + + /** + * A list of TURN (or TURNS or STUN) servers where the provided credentials + * may be used. + */ + repeated string urls = 3; +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/common.proto b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/common.proto new file mode 100644 index 000000000..7d4af557d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/common.proto @@ -0,0 +1,76 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +syntax = "proto3"; + +option java_multiple_files = true; + +package org.signal.chat.common; + +enum IdentityType { + IDENTITY_TYPE_UNSPECIFIED = 0; + IDENTITY_TYPE_ACI = 1; + IDENTITY_TYPE_PNI = 2; +} + +message ServiceIdentifier { + /** + * The type of identity represented by this service identifier. + */ + IdentityType identity_type = 1; + + /** + * The UUID of the identity represented by this service identifier. + */ + bytes uuid = 2; +} + +message EcPreKey { + /** + * A locally-unique identifier for this key. + */ + uint64 key_id = 1; + + /** + * The serialized form of the public key. + */ + bytes public_key = 2; +} + +message EcSignedPreKey { + /** + * A locally-unique identifier for this key. + */ + uint64 key_id = 1; + + /** + * The serialized form of the public key. + */ + bytes public_key = 2; + + /** + * A signature of the public key, verifiable with the identity key for the + * account/identity associated with this pre-key. + */ + bytes signature = 3; +} + +message KemSignedPreKey { + /** + * A locally-unique identifier for this key. + */ + uint64 key_id = 1; + + /** + * The serialized form of the public key. + */ + bytes public_key = 2; + + /** + * A signature of the public key, verifiable with the identity key for the + * account/identity associated with this pre-key. + */ + bytes signature = 3; +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/credentials.proto b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/credentials.proto new file mode 100644 index 000000000..5ce2bace8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/credentials.proto @@ -0,0 +1,117 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +syntax = "proto3"; + +option java_multiple_files = true; + +package org.signal.chat.credentials; + +/** + * Provides methods for obtaining and verifying credentials for "external" services + * (i.e. services that are not a part of the chat server deployment). + * All methods of this service require authentication. + */ +service ExternalServiceCredentials { + + /** + * Generates and returns an external service credentials for the caller. + * + * `UNAUTHENTICATED` status is returned if the call is made on unauthenticated channel. + * + * `INVALID_ARGUMENT` status is returned if service is not configured for the service type + * found in the request OR if `externalService` is not specified in the request. + * + * `RESOURCE_EXHAUSTED` status is returned if a rate limit for + * generating credentials has been exceeded, in which case a + * `retry-after` header containing an ISO 8601 duration string will be present + * in the response trailers. + */ + rpc GetExternalServiceCredentials(GetExternalServiceCredentialsRequest) + returns (GetExternalServiceCredentialsResponse) {} +} + +service ExternalServiceCredentialsAnonymous { + /** + * Given a list of secure value recovery (SVR) service credentials and a phone number, + * checks, which of the provided credetials were generated by the user with the given phone number + * and have not yet expired. + * + * `UNAUTHENTICATED` status is returned if the call is made on unauthenticated channel. + * + * `INVALID_ARGUMENT` status is returned if request contains more than 10 passwords to be checked. + */ + rpc CheckSvrCredentials(CheckSvrCredentialsRequest) + returns (CheckSvrCredentialsResponse) {} +} + +enum ExternalServiceType { + EXTERNAL_SERVICE_TYPE_UNSPECIFIED = 0; + EXTERNAL_SERVICE_TYPE_ART = 1; + EXTERNAL_SERVICE_TYPE_DIRECTORY = 2; + EXTERNAL_SERVICE_TYPE_PAYMENTS = 3; + EXTERNAL_SERVICE_TYPE_STORAGE = 4; + EXTERNAL_SERVICE_TYPE_SVR = 5; +} + +message GetExternalServiceCredentialsRequest { + /** + * A service to request credentials for. + */ + ExternalServiceType externalService = 1; +} + +message GetExternalServiceCredentialsResponse { + /** + * A username that can be presented to authenticate with the external service. + */ + string username = 1; + + /** + * A password that can be presented to authenticate with the external service. + */ + string password = 2; +} + +enum AuthCheckResult { + AUTH_CHECK_RESULT_UNSPECIFIED = 0; + /** + * The credentials could be used to make a call to SVR service by the user + * associated with the `CheckSvrCredentialsRequest.number` phone number. + */ + AUTH_CHECK_RESULT_MATCH = 1; + /** + * The credentials were generated by a different user. + */ + AUTH_CHECK_RESULT_NO_MATCH = 2; + /** + * This status indicates that the corresponding credentials token should no longer be used. + * This may be because it has expired or invalid, but it can also mean that there is a more + * recent token in the request which should be used instead. + */ + AUTH_CHECK_RESULT_INVALID = 3; +} + +message CheckSvrCredentialsRequest { + /** + * A phone number in the E164 format to check the passwords against. + * Only passwords generated for the user associated with the given number will be marked as `AUTH_CHECK_RESULT_MATCH`. + */ + string number = 1; + + /** + * A list of credentials from previously made calls to `ExternalServiceCredentials.GetExternalServiceCredentials()` + * for `EXTERNAL_SERVICE_TYPE_SVR`. This list may contain credentials generated by different users. + */ + repeated string passwords = 2; +} + +/** + * For each of the credentials tokens in the `CheckSvrCredentialsRequest` contains the result of the check. + */ +message CheckSvrCredentialsResponse { + + map matches = 1; +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/device.proto b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/device.proto new file mode 100644 index 000000000..75de93101 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/device.proto @@ -0,0 +1,154 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +syntax = "proto3"; + +option java_multiple_files = true; + +package org.signal.chat.device; + +/** + * Provides methods for working with devices attached to a Signal account. + */ +service Devices { + /** + * Returns a list of devices associated with the caller's account. + */ + rpc GetDevices(GetDevicesRequest) returns (GetDevicesResponse) {} + + /** + * Removes a linked device from the caller's account. This call will fail with + * a status of `PERMISSION_DENIED` if not called from the primary device + * associated with an account. It will also fail with a status of + * `INVALID_ARGUMENT` if the targeted device is the primary device associated + * with the account. + */ + rpc RemoveDevice(RemoveDeviceRequest) returns (RemoveDeviceResponse) {} + + rpc SetDeviceName(SetDeviceNameRequest) returns (SetDeviceNameResponse) {} + + /** + * Sets the token(s) the server should use to send new message notifications + * to the authenticated device. + */ + rpc SetPushToken(SetPushTokenRequest) returns (SetPushTokenResponse) {} + + /** + * Removes any push tokens associated with the authenticated device. After + * calling this method, the server will assume that the authenticated device + * will periodically poll for new messages. + */ + rpc ClearPushToken(ClearPushTokenRequest) returns (ClearPushTokenResponse) {} + + /** + * Declares that the authenticated device supports certain features. + */ + rpc SetCapabilities(SetCapabilitiesRequest) returns (SetCapabilitiesResponse) {} +} + +message GetDevicesRequest {} + +message GetDevicesResponse { + message LinkedDevice { + /** + * The identifier for the device within an account. + */ + uint64 id = 1; + + /** + * A sequence of bytes that encodes an encrypted human-readable name for + * this device. + */ + bytes name = 2; + + /** + * The time, in milliseconds since the epoch, at which this device was + * attached to its parent account. + */ + uint64 created = 3; + + /** + * The approximate time, in milliseconds since the epoch, at which this + * device last connected to the server. + */ + uint64 last_seen = 4; + } + + /** + * A list of devices linked to the authenticated account. + */ + repeated LinkedDevice devices = 1; +} + +message RemoveDeviceRequest { + /** + * The identifier for the device to remove from the authenticated account. + */ + uint64 id = 1; +} + +message SetDeviceNameRequest { + /** + * A sequence of bytes that encodes an encrypted human-readable name for this + * device. + */ + bytes name = 1; +} + +message SetDeviceNameResponse {} + +message RemoveDeviceResponse {} + +message SetPushTokenRequest { + message ApnsTokenRequest { + /** + * A "standard" APNs device token. + */ + string apns_token = 1; + + /** + * A VoIP APNs device token. If present, the server will prefer to send + * message notifications to the device using this token on a VOIP APNs + * topic. + */ + string apns_voip_token = 2; + } + + message FcmTokenRequest { + /** + * An FCM push token. + */ + string fcm_token = 1; + } + + oneof token_request { + /** + * If present, specifies the APNs device token(s) the server will use to + * send new message notifications to the authenticated device. + */ + ApnsTokenRequest apns_token_request = 1; + + /** + * If present, specifies the FCM push token the server will use to send new + * message notifications to the authenticated device. + */ + FcmTokenRequest fcm_token_request = 2; + } +} + +message SetPushTokenResponse {} + +message ClearPushTokenRequest {} + +message ClearPushTokenResponse {} + +message SetCapabilitiesRequest { + bool storage = 1; + bool transfer = 2; + bool pni = 3; + bool paymentActivation = 4; +} + +message SetCapabilitiesResponse {} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/keys.proto b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/keys.proto new file mode 100644 index 000000000..317f64765 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/keys.proto @@ -0,0 +1,279 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +syntax = "proto3"; + +option java_multiple_files = true; + +package org.signal.chat.keys; + +import "org/signal/chat/common.proto"; + +/** + * Provides methods for working with pre-keys. + */ +service Keys { + + /** + * Retrieves an approximate count of the number of the various kinds of + * pre-keys stored for the authenticated device. + */ + rpc GetPreKeyCount (GetPreKeyCountRequest) returns (GetPreKeyCountResponse) {} + + /** + * Retrieves a set of pre-keys for establishing a session with the targeted + * device or devices. Note that callers with an unidentified access key for + * the targeted account should use the version of this method in + * `KeysAnonymous` instead. + * + * This RPC may fail with a `NOT_FOUND` status if the target account was not + * found, if no active device with the given ID (if specified) was found on + * the target account, or if the account has no active devices. It may also + * fail with a `RESOURCE_EXHAUSTED` if a rate limit for fetching keys has been + * exceeded, in which case a `retry-after` header containing an ISO 8601 + * duration string will be present in the response trailers. + */ + rpc GetPreKeys(GetPreKeysRequest) returns (GetPreKeysResponse) {} + + /** + * Uploads a new set of one-time EC pre-keys for the authenticated device, + * clearing any previously-stored pre-keys. Note that all keys submitted via + * a single call to this method _must_ have the same identity type (i.e. if + * the first key has an ACI identity type, then all other keys in the same + * stream must also have an ACI identity type). + * + * This RPC may fail with an `INVALID_ARGUMENT` status if one or more of the + * given pre-keys was structurally invalid or if the list of pre-keys was + * empty. + */ + rpc SetOneTimeEcPreKeys (SetOneTimeEcPreKeysRequest) returns (SetPreKeyResponse) {} + + /** + * Uploads a new set of one-time KEM pre-keys for the authenticated device, + * clearing any previously-stored pre-keys. Note that all keys submitted via + * a single call to this method _must_ have the same identity type (i.e. if + * the first key has an ACI identity type, then all other keys in the same + * stream must also have an ACI identity type). + * + * This RPC may fail with an `INVALID_ARGUMENT` status if one or more of the + * given pre-keys was structurally invalid, had an invalid signature, or if + * the list of pre-keys was empty. + */ + rpc SetOneTimeKemSignedPreKeys (SetOneTimeKemSignedPreKeysRequest) returns (SetPreKeyResponse) {} + + /** + * Sets the signed EC pre-key for one identity (i.e. ACI or PNI) associated + * with the authenticated device. + * + * This RPC may fail with an `INVALID_ARGUMENT` status if the given pre-key + * was structurally invalid, had a bad signature, or was missing entirely. + */ + rpc SetEcSignedPreKey (SetEcSignedPreKeyRequest) returns (SetPreKeyResponse) {} + + /** + * Sets the last-resort KEM pre-key for one identity (i.e. ACI or PNI) + * associated with the authenticated device. + * + * This RPC may fail with an `INVALID_ARGUMENT` status if the given pre-key + * was structurally invalid, had a bad signature, or was missing entirely. + */ + rpc SetKemLastResortPreKey (SetKemLastResortPreKeyRequest) returns (SetPreKeyResponse) {} +} + +/** + * Provides methods for working with pre-keys using "unidentified access" + * credentials. + */ +service KeysAnonymous { + + /** + * Retrieves a set of pre-keys for establishing a session with the targeted + * device or devices. Callers must not submit any self-identifying credentials + * when calling this method and must instead present the targeted account's + * unidentified access key as an anonymous authentication mechanism. Callers + * without an unidentified access key should use the equivalent, authenticated + * method in `Keys` instead. + * + * This RPC may fail with an `UNAUTHENTICATED` status if the given + * unidentified access key did not match the target account's unidentified + * access key or if the target account was not found. It may also fail with a + * `NOT_FOUND` status if no active device with the given ID (if specified) was + * found on the target account, or if the target account has no active + * devices. + */ + rpc GetPreKeys(GetPreKeysAnonymousRequest) returns (GetPreKeysResponse) {} + + /** + * Checks identity key fingerprints of the target accounts. + * + * Returns a stream of elements, each one representing an account that had a mismatched + * identity key fingerprint with the server and the corresponding identity key stored by the server. + */ + rpc CheckIdentityKeys(stream CheckIdentityKeyRequest) returns (stream CheckIdentityKeyResponse) {} +} + +message GetPreKeyCountRequest { +} + +message GetPreKeyCountResponse { + /** + * The approximate number of one-time EC pre-keys stored for the + * authenticated device and associated with the caller's ACI. + */ + uint32 aci_ec_pre_key_count = 1; + + /** + * The approximate number of one-time Kyber pre-keys stored for the + * authenticated device and associated with the caller's ACI. + */ + uint32 aci_kem_pre_key_count = 2; + + /** + * The approximate number of one-time EC pre-keys stored for the + * authenticated device and associated with the caller's PNI. + */ + uint32 pni_ec_pre_key_count = 3; + + /** + * The approximate number of one-time KEM pre-keys stored for the + * authenticated device and associated with the caller's PNI. + */ + uint32 pni_kem_pre_key_count = 4; +} + +message GetPreKeysRequest { + /** + * The service identifier of the account for which to retrieve pre-keys. + */ + common.ServiceIdentifier target_identifier = 1; + + /** + * The ID of the device associated with the targeted account for which to + * retrieve pre-keys. If not set, pre-keys are returned for all devices + * associated with the targeted account. + */ + uint64 device_id = 2; +} + +message GetPreKeysAnonymousRequest { + /** + * The request to retrieve pre-keys for a specific account/device(s). + */ + GetPreKeysRequest request = 1; + + /** + * The unidentified access key (UAK) for the targeted account. + */ + bytes unidentified_access_key = 2; +} + +message GetPreKeysResponse { + message PreKeyBundle { + /** + * The EC signed pre-key associated with the targeted + * account/device/identity. + */ + common.EcSignedPreKey ec_signed_pre_key = 1; + + /** + * A one-time EC pre-key for the targeted account/device/identity. May not + * be set if no one-time EC pre-keys are available. + */ + common.EcPreKey ec_one_time_pre_key = 2; + + /** + * A one-time KEM pre-key (or a last-resort KEM pre-key) for the targeted + * account/device/identity. May not be set if the targeted device has not + * yet uploaded any KEM pre-keys. + */ + common.KemSignedPreKey kem_one_time_pre_key = 3; + } + + /** + * The identity key associated with the targeted account/identity. + */ + bytes identity_key = 1; + + /** + * A map of device IDs to pre-key "bundles" for the targeted account. + */ + map pre_keys = 2; +} + +message SetOneTimeEcPreKeysRequest { + /** + * The identity type (i.e. ACI/PNI) with which the keys in this request are + * associated. + */ + common.IdentityType identity_type = 1; + + /** + * The unsigned EC pre-keys to be stored. + */ + repeated common.EcPreKey pre_keys = 2; +} + +message SetOneTimeKemSignedPreKeysRequest { + /** + * The identity type (i.e. ACI/PNI) with which the keys in this request are + * associated. + */ + common.IdentityType identity_type = 1; + + /** + * The KEM pre-keys to be stored. + */ + repeated common.KemSignedPreKey pre_keys = 2; +} + +message SetEcSignedPreKeyRequest { + /** + * The identity type (i.e. ACI/PNI) with which this key is associated. + */ + common.IdentityType identity_type = 1; + + /** + * The signed EC pre-key itself. + */ + common.EcSignedPreKey signed_pre_key = 2; +} + +message SetKemLastResortPreKeyRequest { + /** + * The identity type (i.e. ACI/PNI) with which this key is associated. + */ + common.IdentityType identity_type = 1; + + /** + * The signed KEM pre-key itself. + */ + common.KemSignedPreKey signed_pre_key = 2; +} + +message SetPreKeyResponse { +} + +message CheckIdentityKeyRequest { + /** + * The service identifier of the account for which we want to check the associated identity key fingerprint. + */ + common.ServiceIdentifier target_identifier = 1; + /** + * The most significant 4 bytes of the SHA-256 hash of the identity key associated with the target account/identity type. + */ + bytes fingerprint = 2; +} + +message CheckIdentityKeyResponse { + /** + * The service identifier of the account for which there is a mismatch between the client and server identity key fingerprints. + */ + common.ServiceIdentifier target_identifier = 1; + /** + * The identity key that is stored by the server for the target account/identity type. + */ + bytes identity_key = 2; +} + diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/payments.proto b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/payments.proto new file mode 100644 index 000000000..ec4a5d6e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/payments.proto @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +syntax = "proto3"; + +option java_multiple_files = true; + +package org.signal.chat.payments; + +/** + * Provides methods for working with payments. + */ +service Payments { + /** + */ + rpc GetCurrencyConversions(GetCurrencyConversionsRequest) returns (GetCurrencyConversionsResponse) {} +} + +message GetCurrencyConversionsRequest { +} + +message GetCurrencyConversionsResponse { + + message CurrencyConversionEntity { + + string base = 1; + + map conversions = 2; + } + + uint64 timestamp = 1; + + repeated CurrencyConversionEntity currencies = 2; +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/profile.proto b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/profile.proto new file mode 100644 index 000000000..504e8807c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/proto/org/signal/chat/profile.proto @@ -0,0 +1,360 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +syntax = "proto3"; + +option java_multiple_files = true; + +package org.signal.chat.profile; + +import "org/signal/chat/common.proto"; + +/** + * Provides methods for working with profiles and profile-related data. + */ +service Profile { + /** + * Sets profile data and if needed, returns S3 credentials used by clients to upload an avatar. + * + * This RPC may fail with `PERMISSION_DENIED` if it attempts to set the MobileCoin wallet ID + * on an account whose profile does not currently have a MobileCoin wallet ID and + * whose phone number contains a disallowed country prefix. + */ + rpc SetProfile(SetProfileRequest) returns (SetProfileResponse) {} + + /** + * Retrieves versioned profile data. Callers with an unidentified access key for the account + * should use the version of this method in `ProfileAnonymous` instead. + * + * This RPC may fail with a `NOT_FOUND` status if the target account was not + * found. It may fail with a `RESOURCE_EXHAUSTED` if a rate limit for fetching profiles has been + * exceeded, in which case a `retry-after` header containing an ISO 8601 + * duration string will be present in the response trailers. + */ + rpc GetVersionedProfile(GetVersionedProfileRequest) returns (GetVersionedProfileResponse) {} + + /** + * Retrieves unversioned profile data. Callers with an unidentified access key for the account + * should use the version of this method in `ProfileAnonymous` instead. + * + * This RPC may fail with a `NOT_FOUND` status if the target account was not + * found. It may fail with a `RESOURCE_EXHAUSTED` if a rate limit for fetching profiles has been + * exceeded, in which case a `retry-after` header containing an ISO 8601 + * duration string will be present in the response trailers. + */ + rpc GetUnversionedProfile(GetUnversionedProfileRequest) returns (GetUnversionedProfileResponse) {} + + /** + * Retrieves a profile key credential. + * Callers with an unidentified access key for the account + * should use the version of this method in `ProfileAnonymous` instead. + * + * This RPC may fail with a `NOT_FOUND` status if the target account was not + * found. It may fail with a `RESOURCE_EXHAUSTED` if a rate limit for fetching profiles has been + * exceeded, in which case a `retry-after` header containing an ISO 8601 + * duration string will be present in the response trailers. It may also fail with an + * `INVALID_ARGUMENT` status if the given credential type is invalid. + */ + rpc GetExpiringProfileKeyCredential(GetExpiringProfileKeyCredentialRequest) returns (GetExpiringProfileKeyCredentialResponse) {} +} + +/** + * Provides methods for working with profiles and profile-related data using "unidentified access" + * credentials. Callers must not submit any self-identifying credentials + * when calling methods in this service and must instead present the targeted account's + * unidentified access key as an anonymous authentication mechanism. Callers + * without an unidentified access key should use the equivalent, authenticated + * methods in `Profile` instead. + */ +service ProfileAnonymous { + /** + * Retrieves versioned profile data. + * + * This RPC may fail with a `NOT_FOUND` status if the target account was not + * found. It may also fail with an `UNAUTHENTICATED` status if the given + * unidentified access key did not match the target account's unidentified + * access key. + */ + rpc GetVersionedProfile(GetVersionedProfileAnonymousRequest) returns (GetVersionedProfileResponse) {} + /** + * Retrieves unversioned profile data. + * + * This RPC may fail with a `NOT_FOUND` status if the target account was not + * found. It may also fail with an `UNAUTHENTICATED` status if the given + * unidentified access key did not match the target account's unidentified + * access key. + */ + rpc GetUnversionedProfile(GetUnversionedProfileAnonymousRequest) returns (GetUnversionedProfileResponse) {} + /** + * Retrieves a profile key credential. + * + * This RPC may fail with a `NOT_FOUND` status if the target account was not + * found. It may also fail with an `UNAUTHENTICATED` status if the given + * unidentified access key did not match the target account's unidentified + * access key, or an `INVALID_ARGUMENT` status if the given credential type is invalid. + */ + rpc GetExpiringProfileKeyCredential(GetExpiringProfileKeyCredentialAnonymousRequest) returns (GetExpiringProfileKeyCredentialResponse) {} +} + +message SetProfileRequest { + enum AvatarChange { + AVATAR_CHANGE_UNCHANGED = 0; + AVATAR_CHANGE_CLEAR = 1; + AVATAR_CHANGE_UPDATE = 2; + } + /** + * The profile version. Must be set. + */ + string version = 1; + /** + * The ciphertext of a name that users must set on the profile. + */ + bytes name = 2; + /** + * An enum to indicate what change, if any, is made to the avatar with this request. + */ + AvatarChange avatarChange = 3; + /** + * The ciphertext of an emoji that users can set on their profile. + */ + bytes about_emoji = 4; + /** + * The ciphertext of a description that users can set on their profile. + */ + bytes about = 5; + /** + * The ciphertext of the MobileCoin wallet ID on the profile. + */ + bytes payment_address = 6; + /** + * A list of badge IDs associated with the profile. + */ + repeated string badge_ids = 7; + /** + * The profile key commitment. Used to issue a profile key credential response. + * Must be set on the request. + */ + bytes commitment = 9; +} + +message SetProfileResponse { + /** + * The policy and credential used by clients to upload an avatar to S3. + */ + ProfileAvatarUploadAttributes attributes = 1; +} + +message GetVersionedProfileRequest { + /** + * The ACI of the account for which to get profile data. + */ + common.ServiceIdentifier accountIdentifier = 1; + /** + * The profile version to retrieve. + */ + string version = 2; +} + +message GetVersionedProfileAnonymousRequest { + /** + * Contains the data necessary to request a versioned profile. + */ + GetVersionedProfileRequest request = 1; + /** + * The unidentified access key for the targeted account. + */ + bytes unidentified_access_key = 2; +} + +message GetVersionedProfileResponse { + /** + * The ciphertext of the name on the profile. + */ + bytes name = 1; + /** + * The ciphertext of the description on the profile. + */ + bytes about = 2; + /** + * The ciphertext of the emoji on the profile. + */ + bytes about_emoji = 3; + /** + * The S3 path of the avatar on the profile. + */ + string avatar = 4; + /** + * The ciphertext of the MobileCoin wallet ID on the profile. + */ + bytes payment_address = 5; +} + +message GetUnversionedProfileRequest { + /** + * The service identifier of the account for which to get profile data. + */ + common.ServiceIdentifier serviceIdentifier = 1; +} + +message GetUnversionedProfileAnonymousRequest { + /** + * Contains the data necessary to request an unversioned profile. + */ + GetUnversionedProfileRequest request = 1; + /** + * The unidentified access key for the targeted account. + */ + bytes unidentified_access_key = 2; +} + +message GetUnversionedProfileResponse { + /** + * The identity key of the targeted account/identity type. + */ + bytes identity_key = 1; + /** + * A checksum of the unidentified access key for the targeted account. + */ + bytes unidentified_access = 2; + /** + * Whether the account has enabled sealed sender from anyone. + */ + bool unrestricted_unidentified_access = 3; + /** + * A list of capabilities enabled on the account. + */ + UserCapabilities capabilities = 4; + /** + * A list of badges associated with the account. + */ + repeated Badge badges = 5; +} + +message GetExpiringProfileKeyCredentialRequest { + /** + * The ACI of the account for which to get a profile key credential. + */ + common.ServiceIdentifier accountIdentifier = 1; + /** + * A zkgroup request for a profile key credential. + */ + bytes credential_request = 2; + /** + * The type of credential being requested. + */ + CredentialType credential_type = 3; + /** + * The profile version for which to generate a profile key credential. + */ + string version = 4; +} + +message GetExpiringProfileKeyCredentialAnonymousRequest { + /** + * Contains the data necessary to request an expiring profile key credential. + */ + GetExpiringProfileKeyCredentialRequest request = 1; + /** + * The unidentified access key for the targeted account. + */ + bytes unidentified_access_key = 2; +} + +message GetExpiringProfileKeyCredentialResponse { + /** + * A zkgroup credential used by a client to prove that it has the profile key + * of a targeted account. + */ + bytes profileKeyCredential = 1; +} + +message ProfileAvatarUploadAttributes { + /** + * The S3 upload path for the profile's avatar. + */ + string path = 1; + /** + * A scoped credential. Includes the AWS access key, date, region targeted, and AWS service. + */ + string credential = 2; + /** + * The type of access control for the avatar object. + */ + string acl = 3; + /** + * The algorithm used to calculate a signature on the S3 policy. + */ + string algorithm = 4; + /** + * The timestamp at which the S3 policy and signature were generated. + */ + string date = 5; + /** + * The S3 policy used to upload the avatar object. + */ + string policy = 6; + /** + * A digital signature on the S3 policy. + */ + bytes signature = 7; +} + +message UserCapabilities { + /** + * Whether all devices linked to the account support MobileCoin payments. + */ + bool payment_activation = 1; + /** + * Whether all devices linked to the account support phone number privacy. + */ + bool pni = 2; +} + +message Badge { + /** + * An ID that uniquely identifies the badge. + */ + string id = 1; + /** + * The category the badge falls in ("donor" or "other"). + */ + string category = 2; + /** + * The badge name. + */ + string name = 3; + /** + * The badge description. + */ + string description = 4; + /** + * Different size badge SVG files. + */ + repeated string sprites6 = 5; + /** + * File name of the scalable vector graphic representing this badge. + */ + string svg = 6; + /** + * Pairs of light/dark SVG files designed for display at different sizes. + */ + repeated BadgeSvg svgs = 7; +} + +message BadgeSvg { + /** + * File name of the scalable vector graphic for light mode. + */ + string light = 1; + /** + * File name of the scalable vector graphic for dark mode. + */ + string dark = 2; +} + +enum CredentialType { + CREDENTIAL_TYPE_UNSPECIFIED = 0; + CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY = 1; +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/services/io.dropwizard.logging.AppenderFactory b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/services/io.dropwizard.logging.AppenderFactory new file mode 100644 index 000000000..ebe62c359 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/services/io.dropwizard.logging.AppenderFactory @@ -0,0 +1 @@ +org.whispersystems.textsecuregcm.metrics.LogstashTcpSocketAppenderFactory diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/services/io.dropwizard.logging.filter.FilterFactory b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/services/io.dropwizard.logging.filter.FilterFactory new file mode 100644 index 000000000..5e33b192a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/services/io.dropwizard.logging.filter.FilterFactory @@ -0,0 +1 @@ +org.whispersystems.textsecuregcm.util.logging.RequestLogEnabledFilterFactory diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/services/io.dropwizard.metrics.ReporterFactory b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/services/io.dropwizard.metrics.ReporterFactory new file mode 100644 index 000000000..c6b835054 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/services/io.dropwizard.metrics.ReporterFactory @@ -0,0 +1 @@ +org.whispersystems.textsecuregcm.metrics.SignalDatadogReporterFactory diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/validation.xml b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/validation.xml new file mode 100644 index 000000000..2a285fd33 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/validation.xml @@ -0,0 +1,8 @@ + + + META-INF/validation/constraints-custom.xml + diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/validation/constraints-custom.xml b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/validation/constraints-custom.xml new file mode 100644 index 000000000..e1d56701c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/META-INF/validation/constraints-custom.xml @@ -0,0 +1,15 @@ + + + + + org.whispersystems.textsecuregcm.configuration.secrets.SecretStringList$ValidatorNotEmpty + org.whispersystems.textsecuregcm.configuration.secrets.SecretBytesList$ValidatorNotEmpty + + + + diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/banner.txt b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/banner.txt new file mode 100644 index 000000000..6c26fda70 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/banner.txt @@ -0,0 +1,9 @@ + _____ _ _ _____ +/ ___|(_) | |/ ___| +\ `--. _ __ _ _ __ __ _ | |\ `--. ___ _ __ __ __ ___ _ __ + `--. \| | / _` || '_ \ / _` || | `--. \ / _ \| '__|\ \ / // _ \| '__| +/\__/ /| || (_| || | | || (_| || |/\__/ /| __/| | \ V /| __/| | +\____/ |_| \__, ||_| |_| \__,_||_|\____/ \___||_| \_/ \___||_| + __/ | + |___/ + diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/account_database_crawler/unlock.lua b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/account_database_crawler/unlock.lua new file mode 100644 index 000000000..b95d15d66 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/account_database_crawler/unlock.lua @@ -0,0 +1,8 @@ +-- keys: lock_key +-- argv: lock_value + +if redis.call("GET", KEYS[1]) == ARGV[1] then + return redis.call("DEL", KEYS[1]) +else + return 0 +end diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/apn/get.lua b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/apn/get.lua new file mode 100644 index 000000000..404a21b19 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/apn/get.lua @@ -0,0 +1,70 @@ +local pendingNotificationQueue = KEYS[1] + +local maxTime = ARGV[1] +local limit = ARGV[2] + +local hgetall = function (key) + local bulk = redis.call('HGETALL', key) + local result = {} + local nextkey + for i, v in ipairs(bulk) do + if i % 2 == 1 then + nextkey = v + else + result[nextkey] = v + end + end + return result +end + +local getNextInterval = function(interval) + if interval < 20000 then + return 20000 + end + + if interval < 40000 then + return 40000 + end + + if interval < 80000 then + return 80000 + end + + if interval < 160000 then + return 160000 + end + + if interval < 600000 then + return 600000 + end + + if interval < 1800000 then + return 1800000 + end + + return 3600000 +end + + +local results = redis.call("ZRANGEBYSCORE", pendingNotificationQueue, 0, maxTime, "LIMIT", 0, limit) +local collated = {} + +if results and next(results) then + for i, name in ipairs(results) do + local pending = hgetall(name) + local lastInterval = pending["interval"] + + if lastInterval == nil then + lastInterval = 0 + end + + local nextInterval = getNextInterval(tonumber(lastInterval)) + + redis.call("HSET", name, "interval", nextInterval) + redis.call("ZADD", pendingNotificationQueue, tonumber(maxTime) + nextInterval, name) + + collated[i] = pending["account"] .. ":" .. pending["device"] + end +end + +return collated diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/apn/insert.lua b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/apn/insert.lua new file mode 100644 index 000000000..3512f22a3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/apn/insert.lua @@ -0,0 +1,14 @@ +local pendingNotificationQueue = KEYS[1] +local endpoint = KEYS[2] + +local timestamp = ARGV[1] +local interval = ARGV[2] +local account = ARGV[3] +local deviceId = ARGV[4] + +redis.call("HSET", endpoint, "created", timestamp) +redis.call("HSET", endpoint, "interval", interval) +redis.call("HSET", endpoint, "account", account) +redis.call("HSET", endpoint, "device", deviceId) + +redis.call("ZADD", pendingNotificationQueue, timestamp, endpoint) diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/apn/remove.lua b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/apn/remove.lua new file mode 100644 index 000000000..c2fb84e42 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/apn/remove.lua @@ -0,0 +1,5 @@ +local pendingNotificationQueue = KEYS[1] +local endpoint = KEYS[2] + +redis.call("DEL", endpoint) +return redis.call("ZREM", pendingNotificationQueue, endpoint) diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/apn/schedule_background_notification.lua b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/apn/schedule_background_notification.lua new file mode 100644 index 000000000..cab867819 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/apn/schedule_background_notification.lua @@ -0,0 +1,17 @@ +local lastBackgroundNotificationTimestampKey = KEYS[1] +local queueKey = KEYS[2] + +local accountDevicePair = ARGV[1] +local currentTimeMillis = tonumber(ARGV[2]) +local backgroundNotificationPeriod = tonumber(ARGV[3]) + +local lastBackgroundNotificationTimestamp = redis.call("GET", lastBackgroundNotificationTimestampKey) +local nextNotificationTimestamp + +if (lastBackgroundNotificationTimestamp) then + nextNotificationTimestamp = tonumber(lastBackgroundNotificationTimestamp) + backgroundNotificationPeriod +else + nextNotificationTimestamp = currentTimeMillis +end + +redis.call("ZADD", queueKey, "NX", nextNotificationTimestamp, accountDevicePair) diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/clear_presence.lua b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/clear_presence.lua new file mode 100644 index 000000000..9e716c118 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/clear_presence.lua @@ -0,0 +1,9 @@ +local presenceKey = KEYS[1] +local presenceUuid = ARGV[1] + +if redis.call("GET", presenceKey) == presenceUuid then + redis.call("DEL", presenceKey) + return true +end + +return false diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/get_items.lua b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/get_items.lua new file mode 100644 index 000000000..045a804ac --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/get_items.lua @@ -0,0 +1,25 @@ +local queueKey = KEYS[1] +local queueLockKey = KEYS[2] +local limit = ARGV[1] +local afterMessageId = ARGV[2] + +local locked = redis.call("GET", queueLockKey) + +if locked then + return {} +end + +if afterMessageId == "null" then + -- An index range is inclusive + local min = 0 + local max = limit - 1 + + if max < 0 then + return {} + end + + return redis.call("ZRANGE", queueKey, min, max, "WITHSCORES") +else + -- note: this is deprecated in Redis 6.2, and should be migrated to zrange after the cluster is updated + return redis.call("ZRANGEBYSCORE", queueKey, "("..afterMessageId, "+inf", "WITHSCORES", "LIMIT", 0, limit) +end diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/get_queues_to_persist.lua b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/get_queues_to_persist.lua new file mode 100644 index 000000000..97f9793ab --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/get_queues_to_persist.lua @@ -0,0 +1,11 @@ +local queueTotalIndexKey = KEYS[1] +local maxTime = ARGV[1] +local limit = ARGV[2] + +local results = redis.call("ZRANGEBYSCORE", queueTotalIndexKey, 0, maxTime, "LIMIT", 0, limit) + +if results and next(results) then + redis.call("ZREM", queueTotalIndexKey, unpack(results)) +end + +return results diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/insert_item.lua b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/insert_item.lua new file mode 100644 index 000000000..76eea91bf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/insert_item.lua @@ -0,0 +1,22 @@ +local queueKey = KEYS[1] +local queueMetadataKey = KEYS[2] +local queueTotalIndexKey = KEYS[3] +local message = ARGV[1] +local currentTime = ARGV[2] +local guid = ARGV[3] + +if redis.call("HEXISTS", queueMetadataKey, guid) == 1 then + return tonumber(redis.call("HGET", queueMetadataKey, guid)) +end + +local messageId = redis.call("HINCRBY", queueMetadataKey, "counter", 1) + +redis.call("ZADD", queueKey, "NX", messageId, message) + +redis.call("HSET", queueMetadataKey, guid, messageId) + +redis.call("EXPIRE", queueKey, 7776000) -- 90 days +redis.call("EXPIRE", queueMetadataKey, 7776000) -- 90 days + +redis.call("ZADD", queueTotalIndexKey, "NX", currentTime, queueKey) +return messageId diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/remove_item_by_guid.lua b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/remove_item_by_guid.lua new file mode 100644 index 000000000..b80d32f96 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/remove_item_by_guid.lua @@ -0,0 +1,28 @@ +local queueKey = KEYS[1] +local queueMetadataKey = KEYS[2] +local queueTotalIndexKey = KEYS[3] + +local removedMessages = {} + +for _, guid in ipairs(ARGV) do + local messageId = redis.call("HGET", queueMetadataKey, guid) + + if messageId then + local envelope = redis.call("ZRANGEBYSCORE", queueKey, messageId, messageId, "LIMIT", 0, 1) + + redis.call("ZREMRANGEBYSCORE", queueKey, messageId, messageId) + redis.call("HDEL", queueMetadataKey, guid) + + if envelope and next(envelope) then + removedMessages[#removedMessages + 1] = envelope[1] + end + end +end + +if (redis.call("ZCARD", queueKey) == 0) then + redis.call("DEL", queueKey) + redis.call("DEL", queueMetadataKey) + redis.call("ZREM", queueTotalIndexKey, queueKey) +end + +return removedMessages diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/remove_queue.lua b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/remove_queue.lua new file mode 100644 index 000000000..ace767eb5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/remove_queue.lua @@ -0,0 +1,7 @@ +local queueKey = KEYS[1] +local queueMetadataKey = KEYS[2] +local queueTotalIndexKey = KEYS[3] + +redis.call("DEL", queueKey) +redis.call("DEL", queueMetadataKey) +redis.call("ZREM", queueTotalIndexKey, queueKey) diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/renew_presence.lua b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/renew_presence.lua new file mode 100644 index 000000000..71b47c869 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/renew_presence.lua @@ -0,0 +1,7 @@ +local presenceKey = KEYS[1] +local presenceUuid = ARGV[1] +local expireSeconds = ARGV[2] + +if redis.call("GET", presenceKey) == presenceUuid then + redis.call("EXPIRE", presenceKey, expireSeconds) +end diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/validate_rate_limit.lua b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/validate_rate_limit.lua new file mode 100644 index 000000000..3851089eb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/lua/validate_rate_limit.lua @@ -0,0 +1,62 @@ +-- The script encapsulates the logic of a token bucket rate limiter. +-- Two types of operations are supported: 'check-only' and 'use-if-available' (controlled by the 'useTokens' arg). +-- Both operations take in rate limiter configuration parameters and the requested amount of tokens. +-- Both operations return 0, if the rate limiter has enough tokens to cover the requested amount, +-- and the deficit amount otherwise. +-- However, 'check-only' operation doesn't modify the bucket, while 'use-if-available' (if successful) +-- reduces the amount of available tokens by the requested amount. + +local bucketId = KEYS[1] + +local bucketSize = tonumber(ARGV[1]) +local refillRatePerMillis = tonumber(ARGV[2]) +local currentTimeMillis = tonumber(ARGV[3]) +local requestedAmount = tonumber(ARGV[4]) +local useTokens = ARGV[5] and string.lower(ARGV[5]) == "true" + +local SIZE_FIELD = "s" +local TIME_FIELD = "t" + +local changesMade = false +local tokensRemaining +local lastUpdateTimeMillis + +local tokensRemainingStr, lastUpdateTimeMillisStr = unpack(redis.call("HMGET", bucketId, SIZE_FIELD, TIME_FIELD)) +if tokensRemainingStr and lastUpdateTimeMillisStr then + tokensRemaining = tonumber(tokensRemainingStr) + lastUpdateTimeMillis = tonumber(lastUpdateTimeMillisStr) +else + tokensRemaining = bucketSize + lastUpdateTimeMillis = currentTimeMillis +end + +local elapsedTime = currentTimeMillis - lastUpdateTimeMillis +local availableAmount = math.min( + bucketSize, + math.floor(tokensRemaining + (elapsedTime * refillRatePerMillis)) +) + +if availableAmount >= requestedAmount then + if useTokens then + tokensRemaining = availableAmount - requestedAmount + lastUpdateTimeMillis = currentTimeMillis + changesMade = true + end + if changesMade then + local tokensUsed = bucketSize - tokensRemaining + -- Storing a 'full' bucket (i.e. tokensUsed == 0) is equivalent of not storing any state at all + -- (in which case a bucket will be just initialized from the input configs as a 'full' one). + -- For this reason, we either set an expiration time on the record (calculated to let the bucket fully replenish) + -- or we just delete the key if the bucket is full. + if tokensUsed > 0 then + local ttlMillis = math.ceil(tokensUsed / refillRatePerMillis) + redis.call("HSET", bucketId, SIZE_FIELD, tokensRemaining, TIME_FIELD, lastUpdateTimeMillis) + redis.call("PEXPIRE", bucketId, ttlMillis) + else + redis.call("DEL", bucketId) + end + end + return 0 +else + return requestedAmount - availableAmount +end diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/signal/badges/Badges.properties b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/signal/badges/Badges.properties new file mode 100644 index 000000000..fa1c048ca --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/signal/badges/Badges.properties @@ -0,0 +1,31 @@ +# +# Copyright 2021 Signal Messenger, LLC +# SPDX-License-Identifier: AGPL-3.0-only +# + +TEST_name = Test Badge +TEST_description = {short_name} has this badge for testing purposes. + +TEST1_name = Test Badge Alpha +TEST1_description = {short_name} is testing the alpha test badge. + +TEST2_name = Test Badge Beta +TEST2_description = {short_name} is testing the beta test badge. + +TEST3_name = Test Badge Gamma +TEST3_description = {short_name} is testing the gamma test badge. + +R_LOW_name = Signal Star +R_LOW_description = {short_name} supports Signal with a monthly donation. Signal is a nonprofit with no advertisers or investors, supported only by people like you. + +R_MED_name = Signal Planet +R_MED_description = {short_name} supports Signal with a monthly donation. Signal is a nonprofit with no advertisers or investors, supported only by people like you. + +R_HIGH_name = Signal Sun +R_HIGH_description = {short_name} supports Signal with a monthly donation. Signal is a nonprofit with no advertisers or investors, supported only by people like you. + +BOOST_name = Signal Boost +BOOST_description = {short_name} supported Signal with a donation. Signal is a nonprofit with no advertisers or investors, supported only by people like you. + +GIFT_name = Signal UFO +GIFT_description = A friend made a donation to Signal on behalf of {short_name}. Signal is a nonprofit with no advertisers or investors, supported only by people like you. diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/signal/badges/Badges_en.properties b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/signal/badges/Badges_en.properties new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/signal/bankmandate/BankMandate.properties b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/signal/bankmandate/BankMandate.properties new file mode 100644 index 000000000..e9492beda --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/signal/bankmandate/BankMandate.properties @@ -0,0 +1,7 @@ +# +# Copyright 2023 Signal Messenger, LLC +# SPDX-License-Identifier: AGPL-3.0-only +# + +SEPA_MANDATE = By providing your payment information and confirming this payment, you authorise (A) Signal Technology Foundation and Stripe, our payment service provider, to send instructions to your bank to debit your account and (B) your bank to debit your account in accordance with those instructions. As part of your rights, you are entitled to a refund from your bank under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks starting from the date on which your account was debited. Your rights are explained in a statement that you can obtain from your bank. You agree to receive notifications for future debits up to 2 days before they occur. + diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/signal/subscriptions/Subscriptions.properties b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/signal/subscriptions/Subscriptions.properties new file mode 100644 index 000000000..ed041b066 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/signal/subscriptions/Subscriptions.properties @@ -0,0 +1,13 @@ +# +# Copyright 2021 Signal Messenger, LLC +# SPDX-License-Identifier: AGPL-3.0-only +# +# These are deprecated, will be unused by clients in a future update, and can be removed in ~April 2023. +# First subscription level +R_LOW = Sustainer 1 + +# Second subscription level +R_MED = Sustainer 2 + +# Third subscription level +R_HIGH = Sustainer 3 diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/signal/subscriptions/Subscriptions_en.properties b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/signal/subscriptions/Subscriptions_en.properties new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/whispersystems/textsecuregcm/push/apns-certificates.pem b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/whispersystems/textsecuregcm/push/apns-certificates.pem new file mode 100644 index 000000000..66a721f5a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/main/resources/org/whispersystems/textsecuregcm/push/apns-certificates.pem @@ -0,0 +1,46 @@ +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/CheckServiceConfigurations.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/CheckServiceConfigurations.java new file mode 100644 index 000000000..e4fa8ac7e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/CheckServiceConfigurations.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm; + +import java.io.File; +import java.util.Arrays; + +/** + * Checks whether all YAML configuration files in a given directory are valid. + *

    + * Note: the current implementation fails fast, rather than reporting multiple invalid files + */ +public class CheckServiceConfigurations { + + private static final String SECRETS_BUNDLE_FILENAME = "sample-secrets-bundle.yml"; + + private void checkConfiguration(final File configDirectory) { + + final File[] configFiles = configDirectory.listFiles(f -> + !f.isDirectory() + && f.getPath().endsWith(".yml") + && !f.getPath().endsWith(SECRETS_BUNDLE_FILENAME)); + + if (configFiles == null || configFiles.length == 0) { + throw new IllegalArgumentException("No .yml configuration files found at " + configDirectory.getPath()); + } + + final File[] secretsBundle = configDirectory.listFiles(f -> !f.isDirectory() && f.getName().equals(SECRETS_BUNDLE_FILENAME)); + if (secretsBundle == null || secretsBundle.length != 1) { + throw new IllegalArgumentException("No [%s] file found at %s".formatted(SECRETS_BUNDLE_FILENAME, configDirectory.getPath())); + } + System.setProperty(WhisperServerService.SECRETS_BUNDLE_FILE_NAME_PROPERTY, secretsBundle[0].getAbsolutePath()); + + for (final File configFile : configFiles) { + final String[] args = new String[]{"check", configFile.getAbsolutePath()}; + try { + new WhisperServerService().run(args); + } catch (final Exception e) { + // Invalid configuration will cause the "check" command to call `System.exit()`, rather than throwing, + // so this is unexpected + throw new RuntimeException(e); + } + } + } + + public static void main(final String[] args) { + if (args.length != 1) { + throw new IllegalArgumentException("Expected single argument with config directory: " + Arrays.toString(args)); + } + + final File configDirectory = new File(args[0]); + + if (!(configDirectory.exists() && configDirectory.isDirectory())) { + throw new IllegalArgumentException("No directory found at " + configDirectory.getPath()); + } + + new CheckServiceConfigurations().checkConfiguration(configDirectory); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRefreshRequirementProviderTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRefreshRequirementProviderTest.java new file mode 100644 index 000000000..617eea77e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/AuthEnablementRefreshRequirementProviderTest.java @@ -0,0 +1,483 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.InvalidProtocolBufferException; +import io.dropwizard.auth.Auth; +import io.dropwizard.auth.PolymorphicAuthDynamicFeature; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.auth.basic.BasicCredentialAuthFilter; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.LongStream; +import java.util.stream.Stream; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.tests.util.DevicesHelper; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.websocket.WebSocketResourceProvider; +import org.whispersystems.websocket.auth.WebsocketAuthValueFactoryProvider; +import org.whispersystems.websocket.logging.WebsocketRequestLog; +import org.whispersystems.websocket.messages.protobuf.ProtobufWebSocketMessageFactory; +import org.whispersystems.websocket.messages.protobuf.SubProtocol; +import org.whispersystems.websocket.session.WebSocketSessionContextValueFactoryProvider; + +@ExtendWith(DropwizardExtensionsSupport.class) +class AuthEnablementRefreshRequirementProviderTest { + + private final ApplicationEventListener applicationEventListener = mock(ApplicationEventListener.class); + + private final Account account = new Account(); + private final Device authenticatedDevice = DevicesHelper.createDevice(1L); + + private final Supplier> principalSupplier = () -> Optional.of( + new TestPrincipal("test", account, authenticatedDevice)); + + private final ResourceExtension resources = ResourceExtension.builder() + .addProvider( + new PolymorphicAuthDynamicFeature<>(ImmutableMap.of( + TestPrincipal.class, + new BasicCredentialAuthFilter.Builder() + .setAuthenticator(c -> principalSupplier.get()).buildAuthFilter()))) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(TestPrincipal.class))) + .addProvider(applicationEventListener) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new TestResource()) + .build(); + + private AccountsManager accountsManager; + private ClientPresenceManager clientPresenceManager; + + private AuthEnablementRefreshRequirementProvider provider; + + @BeforeEach + void setup() { + accountsManager = mock(AccountsManager.class); + clientPresenceManager = mock(ClientPresenceManager.class); + + provider = new AuthEnablementRefreshRequirementProvider(accountsManager); + + final WebsocketRefreshRequestEventListener listener = + new WebsocketRefreshRequestEventListener(clientPresenceManager, provider); + + when(applicationEventListener.onRequest(any())).thenReturn(listener); + + final UUID uuid = UUID.randomUUID(); + account.setUuid(uuid); + account.addDevice(authenticatedDevice); + LongStream.range(2, 4).forEach(deviceId -> account.addDevice(DevicesHelper.createDevice(deviceId))); + + when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); + + account.getDevices() + .forEach(device -> when(clientPresenceManager.isPresent(uuid, device.getId())).thenReturn(true)); + } + + @Test + void testBuildDevicesEnabled() { + + final long disabledDeviceId = 3L; + + final Account account = mock(Account.class); + + final List devices = new ArrayList<>(); + when(account.getDevices()).thenReturn(devices); + + LongStream.range(1, 5) + .forEach(id -> { + final Device device = mock(Device.class); + when(device.getId()).thenReturn(id); + when(device.isEnabled()).thenReturn(id != disabledDeviceId); + devices.add(device); + }); + + final Map devicesEnabled = AuthEnablementRefreshRequirementProvider.buildDevicesEnabledMap(account); + + assertEquals(4, devicesEnabled.size()); + + assertAll(devicesEnabled.entrySet().stream() + .map(deviceAndEnabled -> () -> { + if (deviceAndEnabled.getKey().equals(disabledDeviceId)) { + assertFalse(deviceAndEnabled.getValue()); + } else { + assertTrue(deviceAndEnabled.getValue()); + } + })); + } + + @ParameterizedTest + @MethodSource + void testDeviceEnabledChanged(final Map initialEnabled, final Map finalEnabled) { + assert initialEnabled.size() == finalEnabled.size(); + + assert account.getMasterDevice().orElseThrow().isEnabled(); + + initialEnabled.forEach((deviceId, enabled) -> + DevicesHelper.setEnabled(account.getDevice(deviceId).orElseThrow(), enabled)); + + final Response response = resources.getJerseyTest() + .target("/v1/test/account/devices/enabled") + .request() + .header("Authorization", + "Basic " + Base64.getEncoder().encodeToString("user:pass".getBytes(StandardCharsets.UTF_8))) + .post(Entity.entity(finalEnabled, MediaType.APPLICATION_JSON)); + + assertEquals(200, response.getStatus()); + + final boolean expectDisplacedPresence = !initialEnabled.equals(finalEnabled); + + assertAll( + initialEnabled.keySet().stream() + .map(deviceId -> () -> verify(clientPresenceManager, times(expectDisplacedPresence ? 1 : 0)) + .disconnectPresence(account.getUuid(), deviceId))); + + assertAll( + finalEnabled.keySet().stream() + .map(deviceId -> () -> verify(clientPresenceManager, times(expectDisplacedPresence ? 1 : 0)) + .disconnectPresence(account.getUuid(), deviceId))); + } + + static Stream testDeviceEnabledChanged() { + return Stream.of( + Arguments.of(Map.of(1L, false, 2L, false), Map.of(1L, true, 2L, false)), + Arguments.of(Map.of(2L, false, 3L, false), Map.of(2L, true, 3L, true)), + Arguments.of(Map.of(2L, true, 3L, true), Map.of(2L, false, 3L, false)), + Arguments.of(Map.of(2L, true, 3L, true), Map.of(2L, true, 3L, true)), + Arguments.of(Map.of(2L, false, 3L, true), Map.of(2L, true, 3L, true)), + Arguments.of(Map.of(2L, true, 3L, false), Map.of(2L, true, 3L, true)) + ); + } + + @Test + void testDeviceAdded() { + assert account.getMasterDevice().orElseThrow().isEnabled(); + + final int initialDeviceCount = account.getDevices().size(); + + final List addedDeviceNames = List.of("newDevice1", "newDevice2"); + final Response response = resources.getJerseyTest() + .target("/v1/test/account/devices") + .request() + .header("Authorization", + "Basic " + Base64.getEncoder().encodeToString("user:pass".getBytes(StandardCharsets.UTF_8))) + .put(Entity.entity(addedDeviceNames, MediaType.APPLICATION_JSON_PATCH_JSON)); + + assertEquals(200, response.getStatus()); + + assertEquals(initialDeviceCount + addedDeviceNames.size(), account.getDevices().size()); + + verify(clientPresenceManager).disconnectPresence(account.getUuid(), 1); + verify(clientPresenceManager).disconnectPresence(account.getUuid(), 2); + verify(clientPresenceManager).disconnectPresence(account.getUuid(), 3); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2}) + void testDeviceRemoved(final int removedDeviceCount) { + assert account.getMasterDevice().orElseThrow().isEnabled(); + + final List initialDeviceIds = account.getDevices().stream().map(Device::getId).collect(Collectors.toList()); + + final List deletedDeviceIds = account.getDevices().stream() + .map(Device::getId) + .filter(deviceId -> deviceId != 1L) + .limit(removedDeviceCount) + .collect(Collectors.toList()); + + assert deletedDeviceIds.size() == removedDeviceCount; + + final String deletedDeviceIdsParam = deletedDeviceIds.stream().map(String::valueOf) + .collect(Collectors.joining(",")); + + final Response response = resources.getJerseyTest() + .target("/v1/test/account/devices/" + deletedDeviceIdsParam) + .request() + .header("Authorization", + "Basic " + Base64.getEncoder().encodeToString("user:pass".getBytes(StandardCharsets.UTF_8))) + .delete(); + + assertEquals(200, response.getStatus()); + + initialDeviceIds.forEach(deviceId -> + verify(clientPresenceManager).disconnectPresence(account.getUuid(), deviceId)); + + verifyNoMoreInteractions(clientPresenceManager); + } + + @Test + void testMasterDeviceDisabledAndDeviceRemoved() { + assert account.getMasterDevice().orElseThrow().isEnabled(); + + final Set initialDeviceIds = account.getDevices().stream().map(Device::getId).collect(Collectors.toSet()); + + final long deletedDeviceId = 2L; + assertTrue(initialDeviceIds.remove(deletedDeviceId)); + + final Response response = resources.getJerseyTest() + .target("/v1/test/account/disableMasterDeviceAndDeleteDevice/" + deletedDeviceId) + .request() + .header("Authorization", + "Basic " + Base64.getEncoder().encodeToString("user:pass".getBytes(StandardCharsets.UTF_8))) + .post(Entity.entity("", MediaType.TEXT_PLAIN)); + + assertEquals(200, response.getStatus()); + + assertTrue(account.getDevice(deletedDeviceId).isEmpty()); + + initialDeviceIds.forEach(deviceId -> verify(clientPresenceManager).disconnectPresence(account.getUuid(), deviceId)); + verify(clientPresenceManager).disconnectPresence(account.getUuid(), deletedDeviceId); + + verifyNoMoreInteractions(clientPresenceManager); + } + + @Test + void testOnEvent() { + Response response = resources.getJerseyTest() + .target("/v1/test/hello") + .request() + // no authorization required + .get(); + + assertEquals(200, response.getStatus()); + + response = resources.getJerseyTest() + .target("/v1/test/authorized") + .request() + .header("Authorization", + "Basic " + Base64.getEncoder().encodeToString("user:pass".getBytes(StandardCharsets.UTF_8))) + .get(); + + assertEquals(200, response.getStatus()); + + verify(accountsManager, never()).getByAccountIdentifier(any(UUID.class)); + } + + @Nested + class WebSocket { + + private WebSocketResourceProvider provider; + private RemoteEndpoint remoteEndpoint; + + @BeforeEach + void setup() { + ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(applicationEventListener); + resourceConfig.register(new TestResource()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(SystemMapper.jsonMapper())); + + ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + + provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, new TestPrincipal("test", account, authenticatedDevice), new ProtobufWebSocketMessageFactory(), + Optional.empty(), 30000); + + remoteEndpoint = mock(RemoteEndpoint.class); + Session session = mock(Session.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getRemote()).thenReturn(remoteEndpoint); + when(session.getUpgradeRequest()).thenReturn(request); + + provider.onWebSocketConnect(session); + } + + @Test + void testOnEvent() throws Exception { + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/hello", + new LinkedList<>(), Optional.empty()).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + final SubProtocol.WebSocketResponseMessage response = verifyAndGetResponse(remoteEndpoint); + + assertEquals(200, response.getStatus()); + } + + private SubProtocol.WebSocketResponseMessage verifyAndGetResponse(final RemoteEndpoint remoteEndpoint) + throws InvalidProtocolBufferException { + ArgumentCaptor responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(remoteEndpoint).sendBytesByFuture(responseBytesCaptor.capture()); + + return SubProtocol.WebSocketMessage.parseFrom(responseBytesCaptor.getValue().array()).getResponse(); + } + } + + public static class TestPrincipal implements Principal, AccountAndAuthenticatedDeviceHolder { + + private final String name; + private final Account account; + private final Device device; + + private TestPrincipal(String name, final Account account, final Device device) { + this.name = name; + this.account = account; + this.device = device; + } + + @Override + public String getName() { + return name; + } + + @Override + public Account getAccount() { + return account; + } + + @Override + public Device getAuthenticatedDevice() { + return device; + } + } + + @Path("/v1/test") + public static class TestResource { + + @GET + @Path("/hello") + public String testGetHello() { + return "Hello!"; + } + + @GET + @Path("/authorized") + public String testAuth(@Auth TestPrincipal principal) { + return "You’re in!"; + } + + @PUT + @Path("/account/enabled/{enabled}") + @ChangesDeviceEnabledState + public String setAccountEnabled(@Auth TestPrincipal principal, @PathParam("enabled") final boolean enabled) { + + final Device device = principal.getAccount().getMasterDevice().orElseThrow(); + + DevicesHelper.setEnabled(device, enabled); + + assert device.isEnabled() == enabled; + + return String.format("Set account to %s", enabled); + } + + @POST + @Path("/account/devices/enabled") + @ChangesDeviceEnabledState + public String setEnabled(@Auth TestPrincipal principal, Map deviceIdsEnabled) { + + final StringBuilder response = new StringBuilder(); + + for (Entry deviceIdEnabled : deviceIdsEnabled.entrySet()) { + final Device device = principal.getAccount().getDevice(deviceIdEnabled.getKey()).orElseThrow(); + DevicesHelper.setEnabled(device, deviceIdEnabled.getValue()); + + response.append(String.format("Set device enabled %s", deviceIdEnabled)); + } + + return response.toString(); + } + + @PUT + @Path("/account/devices") + @ChangesDeviceEnabledState + public String addDevices(@Auth TestPrincipal auth, List deviceNames) { + + deviceNames.forEach(name -> { + final Device device = DevicesHelper.createDevice(auth.getAccount().getNextDeviceId()); + auth.getAccount().addDevice(device); + + device.setName(name); + }); + + return "Added devices " + deviceNames; + } + + @DELETE + @Path("/account/devices/{deviceIds}") + @ChangesDeviceEnabledState + public String removeDevices(@Auth TestPrincipal auth, @PathParam("deviceIds") String deviceIds) { + + Arrays.stream(deviceIds.split(",")) + .map(Long::valueOf) + .forEach(auth.getAccount()::removeDevice); + + return "Removed device(s) " + deviceIds; + } + + @POST + @Path("/account/disableMasterDeviceAndDeleteDevice/{deviceId}") + @ChangesDeviceEnabledState + public String disableMasterDeviceAndRemoveDevice(@Auth TestPrincipal auth, @PathParam("deviceId") long deviceId) { + + DevicesHelper.setEnabled(auth.getAccount().getMasterDevice().orElseThrow(), false); + + auth.getAccount().removeDevice(deviceId); + + return "Removed device " + deviceId; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticatorTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticatorTest.java new file mode 100644 index 000000000..fb9a2e8c2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticatorTest.java @@ -0,0 +1,393 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.dropwizard.auth.basic.BasicCredentials; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.junitpioneer.jupiter.cartesian.CartesianTest; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.TestClock; + +class BaseAccountAuthenticatorTest { + + private final long today = 1590451200000L; + private final long yesterday = today - 86_400_000L; + private final long oldTime = yesterday - 86_400_000L; + private final long currentTime = today + 68_000_000L; + + private AccountsManager accountsManager; + private BaseAccountAuthenticator baseAccountAuthenticator; + private TestClock clock; + private Account acct1; + private Account acct2; + private Account oldAccount; + + @BeforeEach + void setup() { + accountsManager = mock(AccountsManager.class); + clock = TestClock.now(); + baseAccountAuthenticator = new BaseAccountAuthenticator(accountsManager, clock); + + // We use static UUIDs here because the UUID affects the "date last seen" offset + acct1 = AccountsHelper.generateTestAccount("+14088675309", UUID.fromString("c139cb3e-f70c-4460-b221-815e8bdf778f"), UUID.randomUUID(), List.of(generateTestDevice(yesterday)), null); + acct2 = AccountsHelper.generateTestAccount("+14088675310", UUID.fromString("30018a41-2764-4bc7-a935-775dfef84ad1"), UUID.randomUUID(), List.of(generateTestDevice(yesterday)), null); + oldAccount = AccountsHelper.generateTestAccount("+14088675311", UUID.fromString("adfce52b-9299-4c25-9c51-412fb420c6a6"), UUID.randomUUID(), List.of(generateTestDevice(oldTime)), null); + + AccountsHelper.setupMockUpdate(accountsManager); + } + + private static Device generateTestDevice(final long lastSeen) { + final Device device = new Device(); + device.setId(Device.MASTER_ID); + device.setLastSeen(lastSeen); + + return device; + } + + @Test + void testUpdateLastSeenMiddleOfDay() { + clock.pin(Instant.ofEpochMilli(currentTime)); + + final Device device1 = acct1.getDevices().stream().findFirst().get(); + final Device device2 = acct2.getDevices().stream().findFirst().get(); + + final Account updatedAcct1 = baseAccountAuthenticator.updateLastSeen(acct1, device1); + final Account updatedAcct2 = baseAccountAuthenticator.updateLastSeen(acct2, device2); + + verify(accountsManager, never()).updateDeviceLastSeen(eq(acct1), any(), anyLong()); + verify(accountsManager).updateDeviceLastSeen(eq(acct2), eq(device2), anyLong()); + + assertThat(device1.getLastSeen()).isEqualTo(yesterday); + assertThat(device2.getLastSeen()).isEqualTo(today); + + assertThat(acct1).isSameAs(updatedAcct1); + assertThat(acct2).isNotSameAs(updatedAcct2); + } + + @Test + void testUpdateLastSeenStartOfDay() { + clock.pin(Instant.ofEpochMilli(today)); + + final Device device1 = acct1.getDevices().stream().findFirst().get(); + final Device device2 = acct2.getDevices().stream().findFirst().get(); + + final Account updatedAcct1 = baseAccountAuthenticator.updateLastSeen(acct1, device1); + final Account updatedAcct2 = baseAccountAuthenticator.updateLastSeen(acct2, device2); + + verify(accountsManager, never()).updateDeviceLastSeen(eq(acct1), any(), anyLong()); + verify(accountsManager, never()).updateDeviceLastSeen(eq(acct2), any(), anyLong()); + + assertThat(device1.getLastSeen()).isEqualTo(yesterday); + assertThat(device2.getLastSeen()).isEqualTo(yesterday); + + assertThat(acct1).isSameAs(updatedAcct1); + assertThat(acct2).isSameAs(updatedAcct2); + } + + @Test + void testUpdateLastSeenEndOfDay() { + clock.pin(Instant.ofEpochMilli(today + 86_400_000L - 1)); + + final Device device1 = acct1.getDevices().stream().findFirst().get(); + final Device device2 = acct2.getDevices().stream().findFirst().get(); + + final Account updatedAcct1 = baseAccountAuthenticator.updateLastSeen(acct1, device1); + final Account updatedAcct2 = baseAccountAuthenticator.updateLastSeen(acct2, device2); + + verify(accountsManager).updateDeviceLastSeen(eq(acct1), eq(device1), anyLong()); + verify(accountsManager).updateDeviceLastSeen(eq(acct2), eq(device2), anyLong()); + + assertThat(device1.getLastSeen()).isEqualTo(today); + assertThat(device2.getLastSeen()).isEqualTo(today); + + assertThat(updatedAcct1).isNotSameAs(acct1); + assertThat(updatedAcct2).isNotSameAs(acct2); + } + + @Test + void testNeverWriteYesterday() { + clock.pin(Instant.ofEpochMilli(today)); + + final Device device = oldAccount.getDevices().stream().findFirst().get(); + + baseAccountAuthenticator.updateLastSeen(oldAccount, device); + + verify(accountsManager).updateDeviceLastSeen(eq(oldAccount), eq(device), anyLong()); + + assertThat(device.getLastSeen()).isEqualTo(today); + } + + @Test + void testAuthenticate() { + final UUID uuid = UUID.randomUUID(); + final long deviceId = 1; + final String password = "12345"; + + final Account account = mock(Account.class); + final Device device = mock(Device.class); + final SaltedTokenHash credentials = mock(SaltedTokenHash.class); + + clock.unpin(); + when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); + when(account.getUuid()).thenReturn(uuid); + when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); + when(account.isEnabled()).thenReturn(true); + when(device.getId()).thenReturn(deviceId); + when(device.isEnabled()).thenReturn(true); + when(device.getAuthTokenHash()).thenReturn(credentials); + when(credentials.verify(password)).thenReturn(true); + when(credentials.getVersion()).thenReturn(SaltedTokenHash.CURRENT_VERSION); + + final Optional maybeAuthenticatedAccount = + baseAccountAuthenticator.authenticate(new BasicCredentials(uuid.toString(), password), true); + + assertThat(maybeAuthenticatedAccount).isPresent(); + assertThat(maybeAuthenticatedAccount.get().getAccount().getUuid()).isEqualTo(uuid); + assertThat(maybeAuthenticatedAccount.get().getAuthenticatedDevice()).isEqualTo(device); + verify(accountsManager, never()).updateDeviceAuthentication(any(), any(), any());; + } + + @Test + void testAuthenticateNonDefaultDevice() { + final UUID uuid = UUID.randomUUID(); + final long deviceId = 2; + final String password = "12345"; + + final Account account = mock(Account.class); + final Device device = mock(Device.class); + final SaltedTokenHash credentials = mock(SaltedTokenHash.class); + + clock.unpin(); + when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); + when(account.getUuid()).thenReturn(uuid); + when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); + when(account.isEnabled()).thenReturn(true); + when(device.getId()).thenReturn(deviceId); + when(device.isEnabled()).thenReturn(true); + when(device.getAuthTokenHash()).thenReturn(credentials); + when(credentials.verify(password)).thenReturn(true); + when(credentials.getVersion()).thenReturn(SaltedTokenHash.CURRENT_VERSION); + + final Optional maybeAuthenticatedAccount = + baseAccountAuthenticator.authenticate(new BasicCredentials(uuid + "." + deviceId, password), true); + + assertThat(maybeAuthenticatedAccount).isPresent(); + assertThat(maybeAuthenticatedAccount.get().getAccount().getUuid()).isEqualTo(uuid); + assertThat(maybeAuthenticatedAccount.get().getAuthenticatedDevice()).isEqualTo(device); + verify(accountsManager, never()).updateDeviceAuthentication(any(), any(), any()); + } + + @CartesianTest + void testAuthenticateEnabledRequired( + @CartesianTest.Values(booleans = {true, false}) final boolean enabledRequired, + @CartesianTest.Values(booleans = {true, false}) final boolean accountEnabled, + @CartesianTest.Values(booleans = {true, false}) final boolean deviceEnabled, + @CartesianTest.Values(booleans = {true, false}) final boolean authenticatedDeviceIsPrimary) { + final UUID uuid = UUID.randomUUID(); + final long deviceId = authenticatedDeviceIsPrimary ? 1 : 2; + final String password = "12345"; + + final Account account = mock(Account.class); + final Device authenticatedDevice = mock(Device.class); + final SaltedTokenHash credentials = mock(SaltedTokenHash.class); + + clock.unpin(); + when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); + when(account.getUuid()).thenReturn(uuid); + when(account.getDevice(deviceId)).thenReturn(Optional.of(authenticatedDevice)); + when(account.isEnabled()).thenReturn(accountEnabled); + when(authenticatedDevice.getId()).thenReturn(deviceId); + when(authenticatedDevice.isEnabled()).thenReturn(deviceEnabled); + when(authenticatedDevice.getAuthTokenHash()).thenReturn(credentials); + when(credentials.verify(password)).thenReturn(true); + when(credentials.getVersion()).thenReturn(SaltedTokenHash.CURRENT_VERSION); + + final String identifier; + if (authenticatedDeviceIsPrimary) { + identifier = uuid.toString(); + } else { + identifier = uuid.toString() + BaseAccountAuthenticator.DEVICE_ID_SEPARATOR + deviceId; + } + final Optional maybeAuthenticatedAccount = + baseAccountAuthenticator.authenticate(new BasicCredentials(identifier, password), enabledRequired); + + if (enabledRequired && !(accountEnabled && deviceEnabled)) { + assertThat(maybeAuthenticatedAccount).isEmpty(); + } else { + assertThat(maybeAuthenticatedAccount).isPresent(); + assertThat(maybeAuthenticatedAccount.get().getAccount().getUuid()).isEqualTo(uuid); + assertThat(maybeAuthenticatedAccount.get().getAuthenticatedDevice()).isEqualTo(authenticatedDevice); + } + } + + @Test + void testAuthenticateV1() { + final UUID uuid = UUID.randomUUID(); + final long deviceId = 1; + final String password = "12345"; + + final Account account = mock(Account.class); + final Device device = mock(Device.class); + final SaltedTokenHash credentials = mock(SaltedTokenHash.class); + + clock.unpin(); + when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); + when(account.getUuid()).thenReturn(uuid); + when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); + when(account.isEnabled()).thenReturn(true); + when(device.getId()).thenReturn(deviceId); + when(device.isEnabled()).thenReturn(true); + when(device.getAuthTokenHash()).thenReturn(credentials); + when(credentials.verify(password)).thenReturn(true); + when(credentials.getVersion()).thenReturn(SaltedTokenHash.Version.V1); + + final Optional maybeAuthenticatedAccount = + baseAccountAuthenticator.authenticate(new BasicCredentials(uuid.toString(), password), true); + + assertThat(maybeAuthenticatedAccount).isPresent(); + assertThat(maybeAuthenticatedAccount.get().getAccount().getUuid()).isEqualTo(uuid); + assertThat(maybeAuthenticatedAccount.get().getAuthenticatedDevice()).isEqualTo(device); + verify(accountsManager, times(1)).updateDeviceAuthentication( + any(), // this won't be 'account', because it'll already be updated by updateDeviceLastSeen + eq(device), any()); + } + @Test + void testAuthenticateAccountNotFound() { + assertThat(baseAccountAuthenticator.authenticate(new BasicCredentials(UUID.randomUUID().toString(), "password"), true)) + .isEmpty(); + } + + @Test + void testAuthenticateDeviceNotFound() { + final UUID uuid = UUID.randomUUID(); + final long deviceId = 1; + final String password = "12345"; + + final Account account = mock(Account.class); + final Device device = mock(Device.class); + final SaltedTokenHash credentials = mock(SaltedTokenHash.class); + + clock.unpin(); + when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); + when(account.getUuid()).thenReturn(uuid); + when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); + when(account.isEnabled()).thenReturn(true); + when(device.getId()).thenReturn(deviceId); + when(device.isEnabled()).thenReturn(true); + when(device.getAuthTokenHash()).thenReturn(credentials); + when(credentials.verify(password)).thenReturn(true); + when(credentials.getVersion()).thenReturn(SaltedTokenHash.CURRENT_VERSION); + + final Optional maybeAuthenticatedAccount = + baseAccountAuthenticator.authenticate(new BasicCredentials(uuid + "." + (deviceId + 1), password), true); + + assertThat(maybeAuthenticatedAccount).isEmpty(); + verify(account).getDevice(deviceId + 1); + } + + @Test + void testAuthenticateIncorrectPassword() { + final UUID uuid = UUID.randomUUID(); + final long deviceId = 1; + final String password = "12345"; + + final Account account = mock(Account.class); + final Device device = mock(Device.class); + final SaltedTokenHash credentials = mock(SaltedTokenHash.class); + + clock.unpin(); + when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); + when(account.getUuid()).thenReturn(uuid); + when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); + when(account.isEnabled()).thenReturn(true); + when(device.getId()).thenReturn(deviceId); + when(device.isEnabled()).thenReturn(true); + when(device.getAuthTokenHash()).thenReturn(credentials); + when(credentials.verify(password)).thenReturn(true); + when(credentials.getVersion()).thenReturn(SaltedTokenHash.CURRENT_VERSION); + + final String incorrectPassword = password + "incorrect"; + + final Optional maybeAuthenticatedAccount = + baseAccountAuthenticator.authenticate(new BasicCredentials(uuid.toString(), incorrectPassword), true); + + assertThat(maybeAuthenticatedAccount).isEmpty(); + verify(credentials).verify(incorrectPassword); + } + + @ParameterizedTest + @MethodSource + void testAuthenticateMalformedCredentials(final String username) { + final Optional maybeAuthenticatedAccount = assertDoesNotThrow( + () -> baseAccountAuthenticator.authenticate(new BasicCredentials(username, "password"), true)); + + assertThat(maybeAuthenticatedAccount).isEmpty(); + verify(accountsManager, never()).getByAccountIdentifier(any(UUID.class)); + } + + private static Stream testAuthenticateMalformedCredentials() { + return Stream.of( + "", + ".4", + "This is definitely not a valid UUID", + UUID.randomUUID() + "."); + } + + @ParameterizedTest + @MethodSource + void testGetIdentifierAndDeviceId(final String username, final String expectedIdentifier, final long expectedDeviceId) { + final Pair identifierAndDeviceId = BaseAccountAuthenticator.getIdentifierAndDeviceId(username); + + assertEquals(expectedIdentifier, identifierAndDeviceId.first()); + assertEquals(expectedDeviceId, identifierAndDeviceId.second()); + } + + private static Stream testGetIdentifierAndDeviceId() { + return Stream.of( + Arguments.of("", "", Device.MASTER_ID), + Arguments.of("test", "test", Device.MASTER_ID), + Arguments.of("test.7", "test", 7)); + } + + @ParameterizedTest + @ValueSource(strings = { + ".", + ".....", + "test.7.8", + "test." + }) + void testGetIdentifierAndDeviceIdMalformed(final String malformedUsername) { + assertThrows(IllegalArgumentException.class, + () -> BaseAccountAuthenticator.getIdentifierAndDeviceId(malformedUsername)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/BasicAuthorizationHeaderTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/BasicAuthorizationHeaderTest.java new file mode 100644 index 000000000..ddee28320 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/BasicAuthorizationHeaderTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.storage.Device; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class BasicAuthorizationHeaderTest { + + @Test + void fromString() throws InvalidAuthorizationHeaderException { + { + final BasicAuthorizationHeader header = + BasicAuthorizationHeader.fromString("Basic YWxhZGRpbjpvcGVuc2VzYW1l"); + + assertEquals("aladdin", header.getUsername()); + assertEquals("opensesame", header.getPassword()); + assertEquals(Device.MASTER_ID, header.getDeviceId()); + } + + { + final BasicAuthorizationHeader header = BasicAuthorizationHeader.fromString("Basic " + + Base64.getEncoder().encodeToString("username.7:password".getBytes(StandardCharsets.UTF_8))); + + assertEquals("username", header.getUsername()); + assertEquals("password", header.getPassword()); + assertEquals(7, header.getDeviceId()); + } + } + + @ParameterizedTest + @MethodSource + void fromStringMalformed(final String header) { + assertThrows(InvalidAuthorizationHeaderException.class, + () -> BasicAuthorizationHeader.fromString(header)); + } + + private static Stream fromStringMalformed() { + return Stream.of( + null, + "", + " ", + "Obviously not a valid authorization header", + "Digest YWxhZGRpbjpvcGVuc2VzYW1l", + "Basic", + "Basic ", + "Basic &&&&&&", + "Basic " + Base64.getEncoder().encodeToString("".getBytes(StandardCharsets.UTF_8)), + "Basic " + Base64.getEncoder().encodeToString(":".getBytes(StandardCharsets.UTF_8)), + "Basic " + Base64.getEncoder().encodeToString("test".getBytes(StandardCharsets.UTF_8)), + "Basic " + Base64.getEncoder().encodeToString("test.".getBytes(StandardCharsets.UTF_8)), + "Basic " + Base64.getEncoder().encodeToString("test.:".getBytes(StandardCharsets.UTF_8)), + "Basic " + Base64.getEncoder().encodeToString("test.:password".getBytes(StandardCharsets.UTF_8)), + "Basic " + Base64.getEncoder().encodeToString(":password".getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/CertificateGeneratorTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/CertificateGeneratorTest.java new file mode 100644 index 000000000..3e40c715a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/CertificateGeneratorTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.util.Base64; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; + +class CertificateGeneratorTest { + + private static final String SIGNING_CERTIFICATE = "CiUIDBIhBbTz4h1My+tt+vw+TVscgUe/DeHS0W02tPWAWbTO2xc3EkD+go4bJnU0AcnFfbOLKoiBfCzouZtDYMOVi69rE7r4U9cXREEqOkUmU2WJBjykAxWPCcSTmVTYHDw7hkSp/puG"; + private static final String SIGNING_KEY = "ABOxG29xrfq4E7IrW11Eg7+HBbtba9iiS0500YoBjn4="; + private static final IdentityKey IDENTITY_KEY = new IdentityKey(ECPublicKey.fromPublicKeyBytes(Base64.getDecoder().decode("BcxxDU9FGMda70E7+Uvm7pnQcEdXQ64aJCpPUeRSfcFo"))); + + @Test + void testCreateFor() throws IOException, InvalidKeyException, org.signal.libsignal.protocol.InvalidKeyException { + final Account account = mock(Account.class); + final Device device = mock(Device.class); + final CertificateGenerator certificateGenerator = new CertificateGenerator(Base64.getDecoder().decode(SIGNING_CERTIFICATE), Curve.decodePrivatePoint(Base64.getDecoder().decode(SIGNING_KEY)), 1); + + when(account.getIdentityKey(IdentityType.ACI)).thenReturn(IDENTITY_KEY); + when(account.getUuid()).thenReturn(UUID.randomUUID()); + when(account.getNumber()).thenReturn("+18005551234"); + when(device.getId()).thenReturn(4L); + + assertTrue(certificateGenerator.createFor(account, device, true).length > 0); + assertTrue(certificateGenerator.createFor(account, device, false).length > 0); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialsGeneratorTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialsGeneratorTest.java new file mode 100644 index 000000000..3e951f641 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialsGeneratorTest.java @@ -0,0 +1,178 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.whispersystems.textsecuregcm.util.HmacUtils.hmac256TruncatedToHexString; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.util.MockUtils; +import org.whispersystems.textsecuregcm.util.MutableClock; + +class ExternalServiceCredentialsGeneratorTest { + private static final String PREFIX = "prefix"; + + private static final String E164 = "+14152222222"; + + private static final long TIME_SECONDS = 12345; + + private static final long TIME_MILLIS = TimeUnit.SECONDS.toMillis(TIME_SECONDS); + + private static final String TIME_SECONDS_STRING = Long.toString(TIME_SECONDS); + + private static final String USERNAME_TIMESTAMP = PREFIX + ":" + Instant.ofEpochSecond(TIME_SECONDS).truncatedTo(ChronoUnit.DAYS).getEpochSecond(); + + private static final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS); + + private static final ExternalServiceCredentialsGenerator standardGenerator = ExternalServiceCredentialsGenerator + .builder(new byte[32]) + .withClock(clock) + .build(); + + private static final ExternalServiceCredentials standardCredentials = standardGenerator.generateFor(E164); + + private static final ExternalServiceCredentialsGenerator usernameIsTimestampGenerator = ExternalServiceCredentialsGenerator + .builder(new byte[32]) + .withUsernameTimestampTruncatorAndPrefix(timestamp -> timestamp.truncatedTo(ChronoUnit.DAYS), PREFIX) + .withClock(clock) + .build(); + + private static final ExternalServiceCredentials usernameIsTimestampCredentials = usernameIsTimestampGenerator.generateWithTimestampAsUsername(); + + @BeforeEach + public void before() throws Exception { + clock.setTimeMillis(TIME_MILLIS); + } + + @Test + void testInvalidConstructor() { + assertThrows(RuntimeException.class, () -> ExternalServiceCredentialsGenerator + .builder(new byte[32]) + .withUsernameTimestampTruncatorAndPrefix(null, PREFIX) + .build()); + + assertThrows(RuntimeException.class, () -> ExternalServiceCredentialsGenerator + .builder(new byte[32]) + .withUsernameTimestampTruncatorAndPrefix(timestamp -> timestamp.truncatedTo(ChronoUnit.DAYS), null) + .build()); + } + + @Test + void testGenerateDerivedUsername() { + final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator + .builder(new byte[32]) + .withUserDerivationKey(new byte[32]) + .build(); + final ExternalServiceCredentials credentials = generator.generateFor(E164); + assertNotEquals(credentials.username(), E164); + assertFalse(credentials.password().startsWith(E164)); + assertEquals(credentials.password().split(":").length, 3); + } + + @Test + void testGenerateNoDerivedUsername() { + assertEquals(standardCredentials.username(), E164); + assertTrue(standardCredentials.password().startsWith(E164)); + assertEquals(standardCredentials.password().split(":").length, 3); + } + + @Test + public void testNotPrependUsername() throws Exception { + final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator + .builder(new byte[32]) + .prependUsername(false) + .withClock(clock) + .build(); + final ExternalServiceCredentials credentials = generator.generateFor(E164); + assertEquals(credentials.username(), E164); + assertTrue(credentials.password().startsWith(TIME_SECONDS_STRING)); + assertEquals(credentials.password().split(":").length, 2); + } + + @Test + public void testWithUsernameIsTimestamp() { + assertEquals(USERNAME_TIMESTAMP, usernameIsTimestampCredentials.username()); + + final String[] passwordComponents = usernameIsTimestampCredentials.password().split(":"); + assertEquals(USERNAME_TIMESTAMP, passwordComponents[0] + ":" + passwordComponents[1]); + assertEquals(hmac256TruncatedToHexString(new byte[32], USERNAME_TIMESTAMP, 10), passwordComponents[2]); + } + + @Test + public void testValidateValid() throws Exception { + assertEquals(standardGenerator.validateAndGetTimestamp(standardCredentials).orElseThrow(), TIME_SECONDS); + } + + @Test + public void testValidateValidWithUsernameIsTimestamp() { + final long expectedTimestamp = Instant.ofEpochSecond(TIME_SECONDS).truncatedTo(ChronoUnit.DAYS).getEpochSecond(); + assertEquals(expectedTimestamp, usernameIsTimestampGenerator.validateAndGetTimestamp(usernameIsTimestampCredentials).orElseThrow()); + } + + @Test + public void testValidateInvalid() throws Exception { + final ExternalServiceCredentials corruptedStandardUsername = new ExternalServiceCredentials( + standardCredentials.username(), standardCredentials.password().replace(E164, E164 + "0")); + final ExternalServiceCredentials corruptedStandardTimestamp = new ExternalServiceCredentials( + standardCredentials.username(), standardCredentials.password().replace(TIME_SECONDS_STRING, TIME_SECONDS_STRING + "0")); + final ExternalServiceCredentials corruptedStandardPassword = new ExternalServiceCredentials( + standardCredentials.username(), standardCredentials.password() + "0"); + + final ExternalServiceCredentials corruptedUsernameTimestamp = new ExternalServiceCredentials( + usernameIsTimestampCredentials.username(), usernameIsTimestampCredentials.password().replace(USERNAME_TIMESTAMP, USERNAME_TIMESTAMP + + "0")); + final ExternalServiceCredentials corruptedUsernameTimestampPassword = new ExternalServiceCredentials( + usernameIsTimestampCredentials.username(), usernameIsTimestampCredentials.password() + "0"); + + assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardUsername).isEmpty()); + assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardTimestamp).isEmpty()); + assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardPassword).isEmpty()); + + assertTrue(usernameIsTimestampGenerator.validateAndGetTimestamp(corruptedUsernameTimestamp).isEmpty()); + assertTrue(usernameIsTimestampGenerator.validateAndGetTimestamp(corruptedUsernameTimestampPassword).isEmpty()); + } + + @Test + public void testValidateWithExpiration() throws Exception { + final long elapsedSeconds = 10000; + clock.incrementSeconds(elapsedSeconds); + + assertEquals(standardGenerator.validateAndGetTimestamp(standardCredentials, elapsedSeconds + 1).orElseThrow(), TIME_SECONDS); + assertTrue(standardGenerator.validateAndGetTimestamp(standardCredentials, elapsedSeconds - 1).isEmpty()); + } + + @Test + public void testGetIdentityFromSignature() { + final String identity = standardGenerator.identityFromSignature(standardCredentials.password()).orElseThrow(); + assertEquals(E164, identity); + } + + @Test + public void testGetIdentityFromSignatureIsTimestamp() { + final String identity = usernameIsTimestampGenerator.identityFromSignature(usernameIsTimestampCredentials.password()).orElseThrow(); + assertEquals(USERNAME_TIMESTAMP, identity); + } + + @Test + public void testTruncateLength() throws Exception { + final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator.builder(new byte[32]) + .withUserDerivationKey(new byte[32]) + .withDerivedUsernameTruncateLength(14) + .build(); + final ExternalServiceCredentials creds = generator.generateFor(E164); + assertEquals(14*2 /* 2 chars per byte, because hex */, creds.username().length()); + assertEquals("805b84df7eff1e8fe1baf0c6e838", creds.username()); + generator.validateAndGetTimestamp(creds); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialsSelectorTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialsSelectorTest.java new file mode 100644 index 000000000..89848eb9b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialsSelectorTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import org.apache.commons.lang3.RandomUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsSelector.CredentialInfo; +import org.whispersystems.textsecuregcm.util.MockUtils; +import org.whispersystems.textsecuregcm.util.MutableClock; + +public class ExternalServiceCredentialsSelectorTest { + + private static final UUID UUID1 = UUID.randomUUID(); + private static final UUID UUID2 = UUID.randomUUID(); + private static final MutableClock CLOCK = MockUtils.mutableClock(TimeUnit.DAYS.toSeconds(1)); + + private static final ExternalServiceCredentialsGenerator GEN1 = + ExternalServiceCredentialsGenerator + .builder(RandomUtils.nextBytes(32)) + .prependUsername(true) + .withClock(CLOCK) + .build(); + + private static final ExternalServiceCredentialsGenerator GEN2 = + ExternalServiceCredentialsGenerator + .builder(RandomUtils.nextBytes(32)) + .withUserDerivationKey(RandomUtils.nextBytes(32)) + .prependUsername(false) + .withDerivedUsernameTruncateLength(16) + .withClock(CLOCK) + .build(); + + private static ExternalServiceCredentials atTime( + final ExternalServiceCredentialsGenerator gen, + final long deltaMillis, + final UUID identity) { + final Instant old = CLOCK.instant(); + try { + CLOCK.incrementMillis(deltaMillis); + return gen.generateForUuid(identity); + } finally { + CLOCK.setTimeInstant(old); + } + } + + private static String token(final ExternalServiceCredentials cred) { + return cred.username() + ":" + cred.password(); + } + + @Test + void single() { + final ExternalServiceCredentials cred = GEN1.generateForUuid(UUID1); + var result = ExternalServiceCredentialsSelector.check( + List.of(token(cred)), GEN1, TimeUnit.MINUTES.toSeconds(1)); + assertThat(result).singleElement() + .matches(CredentialInfo::valid) + .matches(info -> info.credentials().equals(cred)); + } + + @Test + void multipleUsernames() { + final ExternalServiceCredentials cred1New = GEN1.generateForUuid(UUID1); + final ExternalServiceCredentials cred1Old = atTime(GEN1, -1, UUID1); + + final ExternalServiceCredentials cred2New = GEN1.generateForUuid(UUID2); + final ExternalServiceCredentials cred2Old = atTime(GEN1, -1, UUID2); + + final List tokens = Stream.of(cred1New, cred1Old, cred2New, cred2Old) + .map(ExternalServiceCredentialsSelectorTest::token) + .toList(); + + final List result = ExternalServiceCredentialsSelector.check(tokens, GEN1, + TimeUnit.MINUTES.toSeconds(1)); + assertThat(result).hasSize(4); + assertThat(result).filteredOn(CredentialInfo::valid) + .hasSize(2) + .map(CredentialInfo::credentials) + .containsExactlyInAnyOrder(cred1New, cred2New); + assertThat(result).filteredOn(info -> !info.valid()) + .map(CredentialInfo::token) + .containsExactlyInAnyOrder(token(cred1Old), token(cred2Old)); + } + + @Test + void multipleGenerators() { + final ExternalServiceCredentials gen1Cred = GEN1.generateForUuid(UUID1); + final ExternalServiceCredentials gen2Cred = GEN2.generateForUuid(UUID1); + + final List result = ExternalServiceCredentialsSelector.check( + List.of(token(gen1Cred), token(gen2Cred)), + GEN2, + TimeUnit.MINUTES.toSeconds(1)); + + assertThat(result) + .hasSize(2) + .filteredOn(CredentialInfo::valid) + .singleElement() + .matches(info -> info.credentials().equals(gen2Cred)); + + assertThat(result) + .filteredOn(info -> !info.valid()) + .singleElement() + .matches(info -> info.token().equals(token(gen1Cred))); + } + + @ParameterizedTest + @MethodSource + void invalidCredentials(final String invalidCredential) { + final ExternalServiceCredentials validCredential = GEN1.generateForUuid(UUID1); + var result = ExternalServiceCredentialsSelector.check( + List.of(invalidCredential, token(validCredential)), GEN1, TimeUnit.MINUTES.toSeconds(1)); + assertThat(result).hasSize(2); + assertThat(result).filteredOn(CredentialInfo::valid).singleElement() + .matches(info -> info.credentials().equals(validCredential)); + assertThat(result).filteredOn(info -> !info.valid()).singleElement() + .matches(info -> info.token().equals(invalidCredential)); + } + + static Stream invalidCredentials() { + return Stream.of( + "blah:blah", + token(atTime(GEN1, -TimeUnit.MINUTES.toSeconds(2), UUID1)), // too old + "nocolon", + "nothingaftercolon:", + ":nothingbeforecolon", + token(GEN2.generateForUuid(UUID1)) + ); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/OptionalAccessTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/OptionalAccessTest.java new file mode 100644 index 000000000..cc0afbcca --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/OptionalAccessTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Base64; +import java.util.Optional; +import javax.ws.rs.WebApplicationException; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.storage.Account; + +class OptionalAccessTest { + + @Test + void testUnidentifiedMissingTarget() { + try { + OptionalAccess.verify(Optional.empty(), Optional.empty(), Optional.empty()); + throw new AssertionError("should fail"); + } catch (WebApplicationException e) { + assertEquals(e.getResponse().getStatus(), 401); + } + } + + @Test + void testUnidentifiedMissingTargetDevice() { + Account account = mock(Account.class); + when(account.isEnabled()).thenReturn(true); + when(account.getDevice(eq(10))).thenReturn(Optional.empty()); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes())); + + try { + OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("1234".getBytes()))), Optional.of(account), "10"); + } catch (WebApplicationException e) { + assertEquals(e.getResponse().getStatus(), 401); + } + } + + @Test + void testUnidentifiedBadTargetDevice() { + Account account = mock(Account.class); + when(account.isEnabled()).thenReturn(true); + when(account.getDevice(eq(10))).thenReturn(Optional.empty()); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes())); + + try { + OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("1234".getBytes()))), Optional.of(account), "$$"); + } catch (WebApplicationException e) { + assertEquals(e.getResponse().getStatus(), 422); + } + } + + + @Test + void testUnidentifiedBadCode() { + Account account = mock(Account.class); + when(account.isEnabled()).thenReturn(true); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes())); + + try { + OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("5678".getBytes()))), Optional.of(account)); + throw new AssertionError("should fail"); + } catch (WebApplicationException e) { + assertEquals(e.getResponse().getStatus(), 401); + } + } + + @Test + void testIdentifiedMissingTarget() { + Account account = mock(Account.class); + when(account.isEnabled()).thenReturn(true); + + try { + OptionalAccess.verify(Optional.of(account), Optional.empty(), Optional.empty()); + throw new AssertionError("should fail"); + } catch (WebApplicationException e) { + assertEquals(e.getResponse().getStatus(), 404); + } + } + + @Test + void testUnsolicitedBadTarget() { + Account account = mock(Account.class); + when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false); + when(account.isEnabled()).thenReturn(true); + + try { + OptionalAccess.verify(Optional.empty(), Optional.empty(), Optional.of(account)); + throw new AssertionError("should fail"); + } catch (WebApplicationException e) { + assertEquals(e.getResponse().getStatus(), 401); + } + } + + @Test + void testUnsolicitedGoodTarget() { + Account account = mock(Account.class); + Anonymous random = mock(Anonymous.class); + when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(true); + when(account.isEnabled()).thenReturn(true); + OptionalAccess.verify(Optional.empty(), Optional.of(random), Optional.of(account)); + } + + @Test + void testUnidentifiedGoodTarget() { + Account account = mock(Account.class); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes())); + when(account.isEnabled()).thenReturn(true); + OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("1234".getBytes()))), Optional.of(account)); + } + + @Test + void testUnidentifiedInactive() { + Account account = mock(Account.class); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes())); + when(account.isEnabled()).thenReturn(false); + + try { + OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("1234".getBytes()))), Optional.of(account)); + throw new AssertionError(); + } catch (WebApplicationException e) { + assertEquals(e.getResponse().getStatus(), 401); + } + } + + @Test + void testIdentifiedGoodTarget() { + Account source = mock(Account.class); + Account target = mock(Account.class); + when(target.isEnabled()).thenReturn(true); + OptionalAccess.verify(Optional.of(source), Optional.empty(), Optional.of(target)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/PhoneNumberChangeRefreshRequirementProviderTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/PhoneNumberChangeRefreshRequirementProviderTest.java new file mode 100644 index 000000000..5e04da700 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/PhoneNumberChangeRefreshRequirementProviderTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import javax.annotation.Nullable; +import javax.ws.rs.core.SecurityContext; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.Pair; + +class PhoneNumberChangeRefreshRequirementProviderTest { + + private PhoneNumberChangeRefreshRequirementProvider provider; + + private Account account; + private RequestEvent requestEvent; + private ContainerRequest request; + + private static final UUID ACCOUNT_UUID = UUID.randomUUID(); + private static final String NUMBER = "+18005551234"; + private static final String CHANGED_NUMBER = "+18005554321"; + + @BeforeEach + void setUp() { + provider = new PhoneNumberChangeRefreshRequirementProvider(); + + account = mock(Account.class); + final Device device = mock(Device.class); + + when(account.getUuid()).thenReturn(ACCOUNT_UUID); + when(account.getNumber()).thenReturn(NUMBER); + when(account.getDevices()).thenReturn(List.of(device)); + when(device.getId()).thenReturn(Device.MASTER_ID); + + request = mock(ContainerRequest.class); + + final Map requestProperties = new HashMap<>(); + + doAnswer(invocation -> { + requestProperties.put(invocation.getArgument(0, String.class), invocation.getArgument(1)); + return null; + }).when(request).setProperty(anyString(), any()); + + when(request.getProperty(anyString())).thenAnswer( + invocation -> requestProperties.get(invocation.getArgument(0, String.class))); + + requestEvent = mock(RequestEvent.class); + when(requestEvent.getContainerRequest()).thenReturn(request); + } + + @Test + void handleRequestNoChange() { + setAuthenticatedAccount(request, account); + + provider.handleRequestFiltered(requestEvent); + assertEquals(Collections.emptyList(), provider.handleRequestFinished(requestEvent)); + } + + @Test + void handleRequestNumberChange() { + setAuthenticatedAccount(request, account); + + provider.handleRequestFiltered(requestEvent); + when(account.getNumber()).thenReturn(CHANGED_NUMBER); + assertEquals(List.of(new Pair<>(ACCOUNT_UUID, Device.MASTER_ID)), provider.handleRequestFinished(requestEvent)); + } + + @Test + void handleRequestNoAuthenticatedAccount() { + final ContainerRequest request = mock(ContainerRequest.class); + setAuthenticatedAccount(request, null); + + when(requestEvent.getContainerRequest()).thenReturn(request); + + provider.handleRequestFiltered(requestEvent); + assertEquals(Collections.emptyList(), provider.handleRequestFinished(requestEvent)); + } + + private static void setAuthenticatedAccount(final ContainerRequest mockRequest, @Nullable final Account account) { + final SecurityContext securityContext = mock(SecurityContext.class); + + when(mockRequest.getSecurityContext()).thenReturn(securityContext); + + if (account != null) { + final AuthenticatedAccount authenticatedAccount = mock(AuthenticatedAccount.class); + + when(securityContext.getUserPrincipal()).thenReturn(authenticatedAccount); + when(authenticatedAccount.getAccount()).thenReturn(account); + } else { + when(securityContext.getUserPrincipal()).thenReturn(null); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/RegistrationLockError.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/RegistrationLockError.java new file mode 100644 index 000000000..5c3a00ab3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/RegistrationLockError.java @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +public enum RegistrationLockError { + MISMATCH(RegistrationLockVerificationManager.FAILURE_HTTP_STATUS), + RATE_LIMITED(413) // This will be changed to 429 in a future revision + ; + + private final int expectedStatus; + + RegistrationLockError(final int expectedStatus) { + this.expectedStatus = expectedStatus; + } + + public int getExpectedStatus() { + return expectedStatus; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/RegistrationLockVerificationManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/RegistrationLockVerificationManagerTest.java new file mode 100644 index 000000000..44e3b66e2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/RegistrationLockVerificationManagerTest.java @@ -0,0 +1,182 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import javax.ws.rs.WebApplicationException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; +import org.whispersystems.textsecuregcm.push.PushNotificationManager; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.util.Pair; + +class RegistrationLockVerificationManagerTest { + + private final AccountsManager accountsManager = mock(AccountsManager.class); + private final ClientPresenceManager clientPresenceManager = mock(ClientPresenceManager.class); + private final ExternalServiceCredentialsGenerator svr2CredentialsGenerator = mock( + ExternalServiceCredentialsGenerator.class); + private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock( + RegistrationRecoveryPasswordsManager.class); + private static PushNotificationManager pushNotificationManager = mock(PushNotificationManager.class); + private final RateLimiters rateLimiters = mock(RateLimiters.class); + private final RegistrationLockVerificationManager registrationLockVerificationManager = new RegistrationLockVerificationManager( + accountsManager, clientPresenceManager, svr2CredentialsGenerator, registrationRecoveryPasswordsManager, pushNotificationManager, rateLimiters); + + private final RateLimiter pinLimiter = mock(RateLimiter.class); + + private Account account; + private StoredRegistrationLock existingRegistrationLock; + + @BeforeEach + void setUp() { + clearInvocations(pushNotificationManager); + when(rateLimiters.getPinLimiter()).thenReturn(pinLimiter); + when(svr2CredentialsGenerator.generateForUuid(any())) + .thenReturn(mock(ExternalServiceCredentials.class)); + + final Device device = mock(Device.class); + when(device.getId()).thenReturn(Device.MASTER_ID); + + AccountsHelper.setupMockUpdate(accountsManager); + + account = mock(Account.class); + when(account.getUuid()).thenReturn(UUID.randomUUID()); + when(account.getNumber()).thenReturn("+18005551212"); + when(account.getDevices()).thenReturn(List.of(device)); + + existingRegistrationLock = mock(StoredRegistrationLock.class); + when(account.getRegistrationLock()).thenReturn(existingRegistrationLock); + } + + @ParameterizedTest + @MethodSource + void testErrors(RegistrationLockError error, + PhoneVerificationRequest.VerificationType verificationType, + @Nullable String clientRegistrationLock, + boolean alreadyLocked) throws Exception { + + when(existingRegistrationLock.getStatus()).thenReturn(StoredRegistrationLock.Status.REQUIRED); + when(account.hasLockedCredentials()).thenReturn(alreadyLocked); + doThrow(new NotPushRegisteredException()).when(pushNotificationManager).sendAttemptLoginNotification(any(), any()); + + final Pair, Consumer> exceptionType = switch (error) { + case MISMATCH -> { + when(existingRegistrationLock.verify(clientRegistrationLock)).thenReturn(false); + yield new Pair<>(WebApplicationException.class, e -> { + if (e instanceof WebApplicationException wae) { + assertEquals(RegistrationLockVerificationManager.FAILURE_HTTP_STATUS, wae.getResponse().getStatus()); + if (!verificationType.equals(PhoneVerificationRequest.VerificationType.RECOVERY_PASSWORD) || clientRegistrationLock != null) { + verify(registrationRecoveryPasswordsManager).removeForNumber(account.getNumber()); + } else { + verify(registrationRecoveryPasswordsManager, never()).removeForNumber(account.getNumber()); + } + verify(clientPresenceManager).disconnectAllPresences(account.getUuid(), List.of(Device.MASTER_ID)); + try { + verify(pushNotificationManager).sendAttemptLoginNotification(any(), eq("failedRegistrationLock")); + } catch (NotPushRegisteredException npre) {} + if (alreadyLocked) { + verify(account, never()).lockAuthTokenHash(); + } else { + verify(account).lockAuthTokenHash(); + } + } else { + fail("Exception was not of expected type"); + } + }); + } + case RATE_LIMITED -> { + when(existingRegistrationLock.verify(any())).thenReturn(true); + doThrow(RateLimitExceededException.class).when(pinLimiter).validate(anyString()); + yield new Pair<>(RateLimitExceededException.class, ignored -> { + verify(account, never()).lockAuthTokenHash(); + try { + verify(pushNotificationManager, never()).sendAttemptLoginNotification(any(), eq("failedRegistrationLock")); + } catch (NotPushRegisteredException npre) {} + verify(registrationRecoveryPasswordsManager, never()).removeForNumber(account.getNumber()); + verify(clientPresenceManager, never()).disconnectAllPresences(account.getUuid(), List.of(Device.MASTER_ID)); + }); + } + }; + + final Exception e = assertThrows(exceptionType.first(), () -> + registrationLockVerificationManager.verifyRegistrationLock(account, clientRegistrationLock, + "Signal-Android/4.68.3", RegistrationLockVerificationManager.Flow.REGISTRATION, + verificationType)); + + exceptionType.second().accept(e); + } + + static Stream testErrors() { + return Stream.of( + Arguments.of(RegistrationLockError.MISMATCH, PhoneVerificationRequest.VerificationType.SESSION, "reglock", true), + Arguments.of(RegistrationLockError.MISMATCH, PhoneVerificationRequest.VerificationType.SESSION, "reglock", false), + Arguments.of(RegistrationLockError.MISMATCH, PhoneVerificationRequest.VerificationType.RECOVERY_PASSWORD, "reglock", false), + Arguments.of(RegistrationLockError.MISMATCH, PhoneVerificationRequest.VerificationType.RECOVERY_PASSWORD, null, false), + Arguments.of(RegistrationLockError.RATE_LIMITED, PhoneVerificationRequest.VerificationType.SESSION, "reglock", false) + ); + } + + @ParameterizedTest + @MethodSource + void testSuccess(final StoredRegistrationLock.Status status, @Nullable final String submittedRegistrationLock) { + + when(existingRegistrationLock.getStatus()) + .thenReturn(status); + when(existingRegistrationLock.verify(submittedRegistrationLock)).thenReturn(true); + + assertDoesNotThrow( + () -> registrationLockVerificationManager.verifyRegistrationLock(account, submittedRegistrationLock, + "Signal-Android/4.68.3", RegistrationLockVerificationManager.Flow.REGISTRATION, + PhoneVerificationRequest.VerificationType.SESSION)); + + verify(account, never()).lockAuthTokenHash(); + verify(registrationRecoveryPasswordsManager, never()).removeForNumber(account.getNumber()); + verify(clientPresenceManager, never()).disconnectAllPresences(account.getUuid(), List.of(Device.MASTER_ID)); + } + + static Stream testSuccess() { + return Stream.of( + Arguments.of(StoredRegistrationLock.Status.ABSENT, null), + Arguments.of(StoredRegistrationLock.Status.EXPIRED, null), + Arguments.of(StoredRegistrationLock.Status.REQUIRED, null), + Arguments.of(StoredRegistrationLock.Status.ABSENT, "reglock"), + Arguments.of(StoredRegistrationLock.Status.EXPIRED, "reglock"), + Arguments.of(StoredRegistrationLock.Status.REQUIRED, "reglock") + ); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/SaltedTokenHashTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/SaltedTokenHashTest.java new file mode 100644 index 000000000..be2a57631 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/SaltedTokenHashTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.junit.jupiter.api.Test; + +class SaltedTokenHashTest { + + @Test + void testCreating() { + SaltedTokenHash credentials = SaltedTokenHash.generateFor("mypassword"); + assertThat(credentials.salt()).isNotEmpty(); + assertThat(credentials.hash()).isNotEmpty(); + assertThat(credentials.hash().length()).isEqualTo(66); + } + + @Test + void testMatching() { + SaltedTokenHash credentials = SaltedTokenHash.generateFor("mypassword"); + + SaltedTokenHash provided = new SaltedTokenHash(credentials.hash(), credentials.salt()); + assertThat(provided.verify("mypassword")).isTrue(); + } + + @Test + void testMisMatching() { + SaltedTokenHash credentials = SaltedTokenHash.generateFor("mypassword"); + + SaltedTokenHash provided = new SaltedTokenHash(credentials.hash(), credentials.salt()); + assertThat(provided.verify("wrong")).isFalse(); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/StoredRegistrationLockTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/StoredRegistrationLockTest.java new file mode 100644 index 000000000..da71585ec --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/StoredRegistrationLockTest.java @@ -0,0 +1,36 @@ +package org.whispersystems.textsecuregcm.auth; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import javax.swing.text.html.Option; +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.auth.StoredRegistrationLock.REGISTRATION_LOCK_EXPIRATION_DAYS; + +public class StoredRegistrationLockTest { + @ParameterizedTest + @MethodSource + void getStatus(final Optional registrationLock, final Optional salt, final long lastSeen, + final StoredRegistrationLock.Status expectedStatus) { + final StoredRegistrationLock storedLock = new StoredRegistrationLock(registrationLock, salt, Instant.ofEpochMilli(lastSeen)); + + assertEquals(expectedStatus, storedLock.getStatus()); + } + + private static Stream getStatus() { + return Stream.of( + Arguments.of(Optional.of("registrationLock"), Optional.of("salt"), System.currentTimeMillis() - Duration.ofDays(1).toMillis(), StoredRegistrationLock.Status.REQUIRED), + Arguments.of(Optional.empty(), Optional.empty(), 0L, StoredRegistrationLock.Status.ABSENT), + Arguments.of(Optional.of("registrationLock"), Optional.of("salt"), System.currentTimeMillis() - REGISTRATION_LOCK_EXPIRATION_DAYS.toMillis(), StoredRegistrationLock.Status.EXPIRED) + ); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/TurnTokenGeneratorTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/TurnTokenGeneratorTest.java new file mode 100644 index 000000000..4c0599bf3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/TurnTokenGeneratorTest.java @@ -0,0 +1,138 @@ +package org.whispersystems.textsecuregcm.auth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TurnTokenGeneratorTest { + + @Test + public void testAlwaysSelectFirst() throws JsonProcessingException { + final String configString = """ + captcha: + scoreFloor: 1.0 + turn: + uriConfigs: + - uris: + - always1.org + - always2.org + - uris: + - never.org + weight: 0 + """; + DynamicConfiguration config = DynamicConfigurationManager + .parseConfiguration(configString, DynamicConfiguration.class) + .orElseThrow(); + + @SuppressWarnings("unchecked") + DynamicConfigurationManager mockDynamicConfigManager = mock( + DynamicConfigurationManager.class); + + when(mockDynamicConfigManager.getConfiguration()).thenReturn(config); + + final TurnTokenGenerator turnTokenGenerator = + new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8)); + + final long COUNT = 1000; + + final Map urlCounts = Stream + .generate(() -> turnTokenGenerator.generate(UUID.randomUUID())) + .limit(COUNT) + .flatMap(token -> token.urls().stream()) + .collect(Collectors.groupingBy(i -> i, Collectors.counting())); + + assertThat(urlCounts.get("always1.org")).isEqualTo(COUNT); + assertThat(urlCounts.get("always2.org")).isEqualTo(COUNT); + assertThat(urlCounts).doesNotContainKey("never.org"); + } + + @Test + public void testProbabilisticUrls() throws JsonProcessingException { + final String configString = """ + captcha: + scoreFloor: 1.0 + turn: + uriConfigs: + - uris: + - always.org + - sometimes1.org + weight: 5 + - uris: + - always.org + - sometimes2.org + weight: 5 + """; + DynamicConfiguration config = DynamicConfigurationManager + .parseConfiguration(configString, DynamicConfiguration.class) + .orElseThrow(); + + @SuppressWarnings("unchecked") + DynamicConfigurationManager mockDynamicConfigManager = mock( + DynamicConfigurationManager.class); + + when(mockDynamicConfigManager.getConfiguration()).thenReturn(config); + final TurnTokenGenerator turnTokenGenerator = + new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8)); + + final long COUNT = 1000; + + final Map urlCounts = Stream + .generate(() -> turnTokenGenerator.generate(UUID.randomUUID())) + .limit(COUNT) + .flatMap(token -> token.urls().stream()) + .collect(Collectors.groupingBy(i -> i, Collectors.counting())); + + assertThat(urlCounts.get("always.org")).isEqualTo(COUNT); + assertThat(urlCounts.get("sometimes1.org")).isGreaterThan(0); + assertThat(urlCounts.get("sometimes2.org")).isGreaterThan(0); + } + + @Test + public void testExplicitEnrollment() throws JsonProcessingException { + final String configString = """ + captcha: + scoreFloor: 1.0 + turn: + secret: bloop + uriConfigs: + - uris: + - enrolled.org + weight: 0 + enrolledAcis: + - 732506d7-d04f-43a4-b1d7-8a3a91ebe8a6 + - uris: + - unenrolled.org + weight: 1 + """; + DynamicConfiguration config = DynamicConfigurationManager + .parseConfiguration(configString, DynamicConfiguration.class) + .orElseThrow(); + + @SuppressWarnings("unchecked") + DynamicConfigurationManager mockDynamicConfigManager = mock( + DynamicConfigurationManager.class); + + when(mockDynamicConfigManager.getConfiguration()).thenReturn(config); + + final TurnTokenGenerator turnTokenGenerator = + new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8)); + + TurnToken token = turnTokenGenerator.generate(UUID.fromString("732506d7-d04f-43a4-b1d7-8a3a91ebe8a6")); + assertThat(token.urls().get(0)).isEqualTo("enrolled.org"); + token = turnTokenGenerator.generate(UUID.randomUUID()); + assertThat(token.urls().get(0)).isEqualTo("unenrolled.org"); + + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/UnidentifiedAccessChecksumTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/UnidentifiedAccessChecksumTest.java new file mode 100644 index 000000000..4098310cb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/UnidentifiedAccessChecksumTest.java @@ -0,0 +1,39 @@ +package org.whispersystems.textsecuregcm.auth; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class UnidentifiedAccessChecksumTest { + @ParameterizedTest + @MethodSource + public void generateFor(final byte[] unidentifiedAccessKey, final byte[] expectedChecksum) { + final byte[] checksum = UnidentifiedAccessChecksum.generateFor(unidentifiedAccessKey); + + assertArrayEquals(expectedChecksum, checksum); + } + + private static Stream generateFor() { + return Stream.of( + Arguments.of(Base64.getDecoder().decode("hqqo9upWeC0HSHOSJcXl/Q=="), + Base64.getDecoder().decode("2DNxpQCjTefuEhdvJayIbAVUcZSXotu8nqXwWr+q6hI=")), + Arguments.of(Base64.getDecoder().decode("0bNEmhGzmxBsDYhEhk+bAw=="), + Base64.getDecoder().decode("gJTodQfP8TUITZhvrWr0t1siDZXYxRQ/qdpNB8jC+yc=")) + ); + } + + @Test + public void generateForIllegalArgument() { + final byte[] invalidLengthUnidentifiedAccessKey = new byte[15]; + new SecureRandom().nextBytes(invalidLengthUnidentifiedAccessKey); + + assertThrows(IllegalArgumentException.class, () -> UnidentifiedAccessChecksum.generateFor(invalidLengthUnidentifiedAccessKey)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/UnidentifiedAccessUtilTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/UnidentifiedAccessUtilTest.java new file mode 100644 index 000000000..993974b07 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/UnidentifiedAccessUtilTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.security.SecureRandom; +import java.util.Optional; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.storage.Account; + +class UnidentifiedAccessUtilTest { + + @ParameterizedTest + @MethodSource + void checkUnidentifiedAccess(@Nullable final byte[] targetUak, + final boolean unrestrictedUnidentifiedAccess, + final byte[] presentedUak, + final boolean expectAccessAllowed) { + + final Account account = mock(Account.class); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.ofNullable(targetUak)); + when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(unrestrictedUnidentifiedAccess); + + assertEquals(expectAccessAllowed, UnidentifiedAccessUtil.checkUnidentifiedAccess(account, presentedUak)); + } + + private static Stream checkUnidentifiedAccess() { + final byte[] uak = new byte[16]; + new SecureRandom().nextBytes(uak); + + final byte[] incorrectUak = new byte[uak.length + 1]; + + return Stream.of( + Arguments.of(null, false, uak, false), + Arguments.of(null, true, uak, true), + Arguments.of(uak, false, incorrectUak, false), + Arguments.of(uak, false, uak, true), + Arguments.of(uak, true, incorrectUak, true), + Arguments.of(uak, true, uak, true) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/grpc/BasicCredentialAuthenticationInterceptorTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/grpc/BasicCredentialAuthenticationInterceptorTest.java new file mode 100644 index 000000000..16eadcd35 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/grpc/BasicCredentialAuthenticationInterceptorTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth.grpc; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.grpc.CallCredentials; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import java.io.IOException; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.Executor; +import java.util.stream.Stream; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.signal.chat.rpc.EchoRequest; +import org.signal.chat.rpc.EchoServiceGrpc; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator; +import org.whispersystems.textsecuregcm.grpc.EchoServiceImpl; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.HeaderUtils; +import org.whispersystems.textsecuregcm.util.Pair; + +class BasicCredentialAuthenticationInterceptorTest { + + private Server server; + private ManagedChannel managedChannel; + + private BaseAccountAuthenticator baseAccountAuthenticator; + + + @BeforeEach + void setUp() throws IOException { + baseAccountAuthenticator = mock(BaseAccountAuthenticator.class); + + final BasicCredentialAuthenticationInterceptor authenticationInterceptor = + new BasicCredentialAuthenticationInterceptor(baseAccountAuthenticator); + + final String serverName = InProcessServerBuilder.generateName(); + + server = InProcessServerBuilder.forName(serverName) + .directExecutor() + .intercept(authenticationInterceptor) + .addService(new EchoServiceImpl()) + .build() + .start(); + + managedChannel = InProcessChannelBuilder.forName(serverName) + .directExecutor() + .build(); + } + + @AfterEach + void tearDown() { + managedChannel.shutdown(); + server.shutdown(); + } + + @ParameterizedTest + @MethodSource + void interceptCall(final Metadata headers, final boolean acceptCredentials, final boolean expectAuthentication) { + if (acceptCredentials) { + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(UUID.randomUUID()); + + final Device device = mock(Device.class); + when(device.getId()).thenReturn(Device.MASTER_ID); + + when(baseAccountAuthenticator.authenticate(any(), anyBoolean())) + .thenReturn(Optional.of(new AuthenticatedAccount(() -> new Pair<>(account, device)))); + } else { + when(baseAccountAuthenticator.authenticate(any(), anyBoolean())) + .thenReturn(Optional.empty()); + } + + final EchoServiceGrpc.EchoServiceBlockingStub stub = EchoServiceGrpc.newBlockingStub(managedChannel) + .withCallCredentials(new CallCredentials() { + @Override + public void applyRequestMetadata(final RequestInfo requestInfo, final Executor appExecutor, final MetadataApplier applier) { + applier.apply(headers); + } + + @Override + public void thisUsesUnstableApi() { + } + }); + + if (expectAuthentication) { + assertDoesNotThrow(() -> stub.echo(EchoRequest.newBuilder().build())); + } else { + final StatusRuntimeException exception = + assertThrows(StatusRuntimeException.class, () -> stub.echo(EchoRequest.newBuilder().build())); + + assertEquals(Status.UNAUTHENTICATED.getCode(), exception.getStatus().getCode()); + } + } + + private static Stream interceptCall() { + final Metadata malformedCredentialHeaders = new Metadata(); + malformedCredentialHeaders.put(BasicCredentialAuthenticationInterceptor.BASIC_CREDENTIALS, "Incorrect"); + + final Metadata structurallyValidCredentialHeaders = new Metadata(); + structurallyValidCredentialHeaders.put( + BasicCredentialAuthenticationInterceptor.BASIC_CREDENTIALS, + HeaderUtils.basicAuthHeader(UUID.randomUUID().toString(), RandomStringUtils.randomAlphanumeric(16)) + ); + + return Stream.of( + Arguments.of(new Metadata(), true, false), + Arguments.of(malformedCredentialHeaders, true, false), + Arguments.of(structurallyValidCredentialHeaders, false, false), + Arguments.of(structurallyValidCredentialHeaders, true, true) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/grpc/MockAuthenticationInterceptor.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/grpc/MockAuthenticationInterceptor.java new file mode 100644 index 000000000..a4fd52df8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/auth/grpc/MockAuthenticationInterceptor.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.auth.grpc; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import java.util.UUID; +import javax.annotation.Nullable; +import org.whispersystems.textsecuregcm.util.Pair; + +public class MockAuthenticationInterceptor implements ServerInterceptor { + + @Nullable + private Pair authenticatedDevice; + + public void setAuthenticatedDevice(final UUID accountIdentifier, final long deviceId) { + authenticatedDevice = new Pair<>(accountIdentifier, deviceId); + } + + public void clearAuthenticatedDevice() { + authenticatedDevice = null; + } + + @Override + public ServerCall.Listener interceptCall(final ServerCall call, + final Metadata headers, + final ServerCallHandler next) { + + if (authenticatedDevice != null) { + final Context context = Context.current() + .withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY, authenticatedDevice.first()) + .withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY, authenticatedDevice.second()); + + return Contexts.interceptCall(context, call, headers, next); + } + + return next.startCall(call, headers); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java new file mode 100644 index 000000000..529d8c17a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java @@ -0,0 +1,214 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.badges; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import java.time.Clock; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.ListResourceBundle; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.ResourceBundle.Control; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.signal.i18n.HeaderControlledResourceBundleLookup; +import org.signal.i18n.ResourceBundleFactory; +import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; +import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; +import org.whispersystems.textsecuregcm.entities.Badge; +import org.whispersystems.textsecuregcm.entities.BadgeSvg; +import org.whispersystems.textsecuregcm.entities.SelfBadge; +import org.whispersystems.textsecuregcm.storage.AccountBadge; +import org.whispersystems.textsecuregcm.util.TestClock; + +public class ConfiguredProfileBadgeConverterTest { + + private final Clock clock = TestClock.pinned(Instant.ofEpochSecond(42)); + private ResourceBundleFactory resourceBundleFactory; + private ResourceBundle resourceBundle; + + @BeforeEach + void beforeEach() { + resourceBundleFactory = mock(ResourceBundleFactory.class, (invocation) -> { + throw new UnsupportedOperationException(); + }); + } + + private static String idFor(int i) { + return "Badge-" + i; + } + + private static String nameFor(int i) { + return "TRANSLATED NAME " + i; + } + + private static String desriptionFor(int i) { + return "TRANSLATED DESCRIPTION " + i; + } + + private static BadgeConfiguration newBadge(int i) { + return new BadgeConfiguration( + idFor(i), "other", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", + List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))); + } + + private BadgesConfiguration createBadges(int count) { + List badges = new ArrayList<>(count); + Object[][] objects = new Object[count * 2][2]; + for (int i = 0; i < count; i++) { + badges.add(newBadge(i)); + objects[(i * 2)] = new Object[]{idFor(i) + "_name", nameFor(i)}; + objects[(i * 2) + 1] = new Object[]{idFor(i) + "_description", desriptionFor(i)}; + } + resourceBundle = new ListResourceBundle() { + @Override + protected Object[][] getContents() { + return objects; + } + }; + return new BadgesConfiguration(badges, List.of(), Map.of()); + } + + private BadgeConfiguration getBadge(BadgesConfiguration badgesConfiguration, int i) { + return badgesConfiguration.getBadges().stream() + .filter(badgeConfiguration -> idFor(i).equals(badgeConfiguration.getId())) + .findFirst().orElse(null); + } + + private ArgumentCaptor setupResourceBundle(Locale expectedLocale) { + ArgumentCaptor controlArgumentCaptor = + ArgumentCaptor.forClass(ResourceBundle.Control.class); + doReturn(resourceBundle).when(resourceBundleFactory).createBundle( + eq(ConfiguredProfileBadgeConverter.BASE_NAME), eq(expectedLocale), controlArgumentCaptor.capture()); + return controlArgumentCaptor; + } + + @Test + void testConvertEmptyList() { + BadgesConfiguration badgesConfiguration = createBadges(1); + ConfiguredProfileBadgeConverter badgeConverter = new ConfiguredProfileBadgeConverter(clock, badgesConfiguration, + new HeaderControlledResourceBundleLookup(resourceBundleFactory)); + assertThat(badgeConverter.convert(List.of(Locale.getDefault()), List.of(), false)).isNotNull().isEmpty(); + } + + @ParameterizedTest + @MethodSource + void testNoLocales(String name, Instant expiration, boolean visible, boolean isSelf, Badge expectedBadge) { + BadgesConfiguration badgesConfiguration = createBadges(1); + ConfiguredProfileBadgeConverter badgeConverter = + new ConfiguredProfileBadgeConverter(clock, badgesConfiguration, + new HeaderControlledResourceBundleLookup(resourceBundleFactory)); + setupResourceBundle(Locale.getDefault()); + + if (expectedBadge != null) { + assertThat(badgeConverter.convert(List.of(), List.of(new AccountBadge(name, expiration, visible)), isSelf)) + .isNotNull() + .hasSize(1) + .containsOnly(expectedBadge); + } else { + assertThat(badgeConverter.convert(List.of(), List.of(new AccountBadge(name, expiration, visible)), isSelf)) + .isNotNull() + .isEmpty(); + } + } + + @SuppressWarnings("unused") + static Stream testNoLocales() { + Instant expired = Instant.ofEpochSecond(41); + Instant notExpired = Instant.ofEpochSecond(43); + return Stream.of( + arguments(idFor(0), expired, false, false, null), + arguments(idFor(0), notExpired, false, false, null), + arguments(idFor(0), expired, true, false, null), + arguments(idFor(0), notExpired, true, false, + new Badge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", + List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")))), + arguments(idFor(1), expired, false, false, null), + arguments(idFor(1), notExpired, false, false, null), + arguments(idFor(1), expired, true, false, null), + arguments(idFor(1), notExpired, true, false, null), + arguments(idFor(0), expired, false, true, null), + arguments(idFor(0), notExpired, false, true, + new SelfBadge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"), + "SVG", + List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")), + notExpired, false)), + arguments(idFor(0), expired, true, true, null), + arguments(idFor(0), notExpired, true, true, + new SelfBadge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"), + "SVG", + List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")), + notExpired, true)), + arguments(idFor(1), expired, false, true, null), + arguments(idFor(1), notExpired, false, true, null), + arguments(idFor(1), expired, true, true, null), + arguments(idFor(1), notExpired, true, true, null)); + } + + @Test + void testCustomControl() { + BadgesConfiguration badgesConfiguration = createBadges(1); + ConfiguredProfileBadgeConverter badgeConverter = + new ConfiguredProfileBadgeConverter(clock, badgesConfiguration, + new HeaderControlledResourceBundleLookup(resourceBundleFactory)); + + Locale defaultLocale = Locale.getDefault(); + Locale enGb = new Locale("en", "GB"); + Locale en = new Locale("en"); + Locale esUs = new Locale("es", "US"); + + ArgumentCaptor controlArgumentCaptor = setupResourceBundle(enGb); + badgeConverter.convert(List.of(enGb, en, esUs), + List.of(new AccountBadge(idFor(0), Instant.ofEpochSecond(43), true)), false); + Control control = controlArgumentCaptor.getValue(); + + assertThatNullPointerException().isThrownBy(() -> control.getFormats(null)); + assertThatNullPointerException().isThrownBy(() -> control.getFallbackLocale(null, enGb)); + assertThatNullPointerException().isThrownBy( + () -> control.getFallbackLocale(ConfiguredProfileBadgeConverter.BASE_NAME, null)); + + assertThat(control.getFormats(ConfiguredProfileBadgeConverter.BASE_NAME)).isNotNull().hasSize(1).containsOnly( + Control.FORMAT_PROPERTIES.toArray(new String[0])); + + try { + // temporarily override for purpose of ensuring this test doesn't change based on system default locale + Locale.setDefault(new Locale("xx", "XX")); + + assertThat(control.getFallbackLocale(ConfiguredProfileBadgeConverter.BASE_NAME, enGb)).isEqualTo(en); + assertThat(control.getFallbackLocale(ConfiguredProfileBadgeConverter.BASE_NAME, en)).isEqualTo(esUs); + assertThat(control.getFallbackLocale(ConfiguredProfileBadgeConverter.BASE_NAME, esUs)).isEqualTo( + Locale.getDefault()); + assertThat(control.getFallbackLocale(ConfiguredProfileBadgeConverter.BASE_NAME, Locale.getDefault())).isNull(); + + // now test what happens if the system default locale is in the list + // this should always terminate at the system default locale since the development defined bundle should get + // returned at that point anyhow + badgeConverter.convert(List.of(enGb, Locale.getDefault(), en, esUs), + List.of(new AccountBadge(idFor(0), Instant.ofEpochSecond(43), true)), false); + Control control2 = controlArgumentCaptor.getValue(); + + assertThat(control2.getFallbackLocale(ConfiguredProfileBadgeConverter.BASE_NAME, enGb)).isEqualTo( + Locale.getDefault()); + assertThat(control2.getFallbackLocale(ConfiguredProfileBadgeConverter.BASE_NAME, Locale.getDefault())).isNull(); + } finally { + Locale.setDefault(defaultLocale); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/captcha/CaptchaCheckerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/captcha/CaptchaCheckerTest.java new file mode 100644 index 000000000..c61d9d866 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/captcha/CaptchaCheckerTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.captcha; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.captcha.CaptchaChecker.SEPARATOR; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import javax.ws.rs.BadRequestException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class CaptchaCheckerTest { + + private static final String CHALLENGE_SITE_KEY = "challenge-site-key"; + private static final String REG_SITE_KEY = "registration-site-key"; + private static final String TOKEN = "some-token"; + private static final String PREFIX = "prefix"; + private static final String PREFIX_A = "prefix-a"; + private static final String PREFIX_B = "prefix-b"; + + static Stream parseInputToken() { + return Stream.of( + Arguments.of( + String.join(SEPARATOR, PREFIX, CHALLENGE_SITE_KEY, "challenge", TOKEN), + TOKEN, + CHALLENGE_SITE_KEY, + Action.CHALLENGE), + Arguments.of( + String.join(SEPARATOR, PREFIX, REG_SITE_KEY, "registration", TOKEN), + TOKEN, + REG_SITE_KEY, + Action.REGISTRATION), + Arguments.of( + String.join(SEPARATOR, PREFIX, CHALLENGE_SITE_KEY, "challenge", TOKEN, "something-else"), + TOKEN + SEPARATOR + "something-else", + CHALLENGE_SITE_KEY, + Action.CHALLENGE), + Arguments.of( + String.join(SEPARATOR, PREFIX, CHALLENGE_SITE_KEY, "ChAlLeNgE", TOKEN), + TOKEN, + CHALLENGE_SITE_KEY, + Action.CHALLENGE) + ); + } + + private static CaptchaClient mockClient(final String prefix) throws IOException { + final CaptchaClient captchaClient = mock(CaptchaClient.class); + when(captchaClient.scheme()).thenReturn(prefix); + when(captchaClient.validSiteKeys(eq(Action.CHALLENGE))).thenReturn(Collections.singleton(CHALLENGE_SITE_KEY)); + when(captchaClient.validSiteKeys(eq(Action.REGISTRATION))).thenReturn(Collections.singleton(REG_SITE_KEY)); + when(captchaClient.verify(any(), any(), any(), any())).thenReturn(AssessmentResult.invalid()); + return captchaClient; + } + + + @ParameterizedTest + @MethodSource + void parseInputToken( + final String input, + final String expectedToken, + final String siteKey, + final Action expectedAction) throws IOException { + final CaptchaClient captchaClient = mockClient(PREFIX); + new CaptchaChecker(null, List.of(captchaClient)).verify(expectedAction, input, null); + verify(captchaClient, times(1)).verify(eq(siteKey), eq(expectedAction), eq(expectedToken), any()); + } + + @ParameterizedTest + @MethodSource + void scoreString(float score, String expected) { + assertThat(AssessmentResult.fromScore(score, 0.0f).getScoreString()).isEqualTo(expected); + } + + + static Stream scoreString() { + return Stream.of( + Arguments.of(0.3f, "30"), + Arguments.of(0.0f, "0"), + Arguments.of(0.333f, "30"), + Arguments.of(0.29f, "30"), + Arguments.of(Float.NaN, "0") + ); + } + + @Test + public void choose() throws IOException { + String ainput = String.join(SEPARATOR, PREFIX_A, CHALLENGE_SITE_KEY, "challenge", TOKEN); + String binput = String.join(SEPARATOR, PREFIX_B, CHALLENGE_SITE_KEY, "challenge", TOKEN); + final CaptchaClient a = mockClient(PREFIX_A); + final CaptchaClient b = mockClient(PREFIX_B); + + new CaptchaChecker(null, List.of(a, b)).verify(Action.CHALLENGE, ainput, null); + verify(a, times(1)).verify(any(), any(), any(), any()); + + new CaptchaChecker(null, List.of(a, b)).verify(Action.CHALLENGE, binput, null); + verify(b, times(1)).verify(any(), any(), any(), any()); + } + + static Stream badArgs() { + return Stream.of( + Arguments.of(String.join(SEPARATOR, "invalid", CHALLENGE_SITE_KEY, "challenge", TOKEN)), // bad prefix + Arguments.of(String.join(SEPARATOR, PREFIX, "challenge", TOKEN)), // no site key + Arguments.of(String.join(SEPARATOR, CHALLENGE_SITE_KEY, PREFIX, "challenge", TOKEN)), // incorrect order + Arguments.of(String.join(SEPARATOR, PREFIX, CHALLENGE_SITE_KEY, "unknown_action", TOKEN)), // bad action + Arguments.of(String.join(SEPARATOR, PREFIX, CHALLENGE_SITE_KEY, "registration", TOKEN)), // action mismatch + Arguments.of(String.join(SEPARATOR, PREFIX, "bad-site-key", "challenge", TOKEN)), // invalid site key + Arguments.of(String.join(SEPARATOR, PREFIX, CHALLENGE_SITE_KEY, "registration", TOKEN)), // site key for wrong type + Arguments.of(String.join(SEPARATOR, PREFIX, REG_SITE_KEY, "challenge", TOKEN)) // site key for wrong type + ); + } + + @ParameterizedTest + @MethodSource + public void badArgs(final String input) throws IOException { + final CaptchaClient cc = mockClient(PREFIX); + assertThrows(BadRequestException.class, + () -> new CaptchaChecker(null, List.of(cc)).verify(Action.CHALLENGE, input, null)); + + } + + @Test + public void testShortened() throws IOException { + final CaptchaClient captchaClient = mockClient(PREFIX); + final ShortCodeExpander retriever = mock(ShortCodeExpander.class); + when(retriever.retrieve("abc")).thenReturn(Optional.of(TOKEN)); + final String input = String.join(SEPARATOR, PREFIX + "-short", REG_SITE_KEY, "registration", "abc"); + new CaptchaChecker(retriever, List.of(captchaClient)).verify(Action.REGISTRATION, input, null); + verify(captchaClient, times(1)).verify(eq(REG_SITE_KEY), eq(Action.REGISTRATION), eq(TOKEN), any()); + + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClientTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClientTest.java new file mode 100644 index 000000000..6f6d98597 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/captcha/HCaptchaClientTest.java @@ -0,0 +1,135 @@ +package org.whispersystems.textsecuregcm.captcha; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.math.BigDecimal; +import java.net.http.HttpClient; +import java.net.http.HttpResponse; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; + +public class HCaptchaClientTest { + + private static final String SITE_KEY = "site-key"; + private static final String TOKEN = "token"; + + + static Stream captchaProcessed() { + return Stream.of( + Arguments.of(true, 0.6f, true), + Arguments.of(false, 0.6f, false), + Arguments.of(true, 0.4f, false), + Arguments.of(false, 0.4f, false) + ); + } + + @ParameterizedTest + @MethodSource + public void captchaProcessed(final boolean success, final float score, final boolean expectedResult) + throws IOException, InterruptedException { + + final FaultTolerantHttpClient client = mockResponder(200, String.format(""" + { + "success": %b, + "score": %f, + "score-reasons": ["great job doing this captcha"] + } + """, + success, 1 - score)); // hCaptcha scores are inverted compared to recaptcha scores. (low score is good) + + final AssessmentResult result = new HCaptchaClient("fake", client, mockConfig(true, 0.5)) + .verify(SITE_KEY, Action.CHALLENGE, TOKEN, null); + if (!success) { + assertThat(result).isEqualTo(AssessmentResult.invalid()); + } else { + assertThat(result.isValid()).isEqualTo(expectedResult); + } + } + + @Test + public void errorResponse() throws IOException, InterruptedException { + final FaultTolerantHttpClient httpClient = mockResponder(503, ""); + final HCaptchaClient client = new HCaptchaClient("fake", httpClient, mockConfig(true, 0.5)); + assertThrows(IOException.class, () -> client.verify(SITE_KEY, Action.CHALLENGE, TOKEN, null)); + } + + @Test + public void invalidScore() throws IOException, InterruptedException { + final FaultTolerantHttpClient httpClient = mockResponder(200, """ + {"success" : true, "score": 1.1} + """); + final HCaptchaClient client = new HCaptchaClient("fake", httpClient, mockConfig(true, 0.5)); + assertThat(client.verify(SITE_KEY, Action.CHALLENGE, TOKEN, null)).isEqualTo(AssessmentResult.invalid()); + } + + @Test + public void badBody() throws IOException, InterruptedException { + final FaultTolerantHttpClient httpClient = mockResponder(200, """ + {"success" : true, + """); + final HCaptchaClient client = new HCaptchaClient("fake", httpClient, mockConfig(true, 0.5)); + assertThrows(IOException.class, () -> client.verify(SITE_KEY, Action.CHALLENGE, TOKEN, null)); + } + + @Test + public void disabled() throws IOException { + final HCaptchaClient hc = new HCaptchaClient("fake", null, mockConfig(false, 0.5)); + assertTrue(Arrays.stream(Action.values()).map(hc::validSiteKeys).allMatch(Set::isEmpty)); + } + + @Test + public void badSiteKey() throws IOException { + final HCaptchaClient hc = new HCaptchaClient("fake", null, mockConfig(true, 0.5)); + for (Action action : Action.values()) { + assertThat(hc.validSiteKeys(action)).contains(SITE_KEY); + assertThat(hc.validSiteKeys(action)).doesNotContain("invalid"); + } + } + + private static FaultTolerantHttpClient mockResponder(final int statusCode, final String jsonBody) { + FaultTolerantHttpClient httpClient = mock(FaultTolerantHttpClient.class); + @SuppressWarnings("unchecked") final HttpResponse httpResponse = mock(HttpResponse.class); + + when(httpResponse.body()).thenReturn(jsonBody); + when(httpResponse.statusCode()).thenReturn(statusCode); + + when(httpClient.sendAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(httpResponse)); + return httpClient; + } + + private static DynamicConfigurationManager mockConfig(boolean enabled, double scoreFloor) { + final DynamicCaptchaConfiguration config = new DynamicCaptchaConfiguration(); + config.setAllowHCaptcha(enabled); + config.setScoreFloor(BigDecimal.valueOf(scoreFloor)); + config.setHCaptchaSiteKeys(Map.of( + Action.REGISTRATION, Collections.singleton(SITE_KEY), + Action.CHALLENGE, Collections.singleton(SITE_KEY) + )); + + @SuppressWarnings("unchecked") final DynamicConfigurationManager m = mock( + DynamicConfigurationManager.class); + final DynamicConfiguration d = mock(DynamicConfiguration.class); + when(m.getConfiguration()).thenReturn(d); + when(d.getCaptchaConfiguration()).thenReturn(config); + return m; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/captcha/ShortCodeExpanderTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/captcha/ShortCodeExpanderTest.java new file mode 100644 index 000000000..4541174a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/captcha/ShortCodeExpanderTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.captcha; + +import com.google.api.Http; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ShortCodeExpanderTest { + + @Test + public void testUriResolution() throws IOException, InterruptedException { + final HttpClient httpClient = mock(HttpClient.class); + final ShortCodeExpander expander = new ShortCodeExpander(httpClient, "https://www.example.org/shortener/"); + when(httpClient + .send(argThat(req -> req.uri().toString().equals("https://www.example.org/shortener/shorturl")), any())) + .thenReturn(new FakeResponse(200, "longurl")); + assertThat(expander.retrieve("shorturl").get()).isEqualTo("longurl"); + } + + private record FakeResponse(int statusCode, String body) implements HttpResponse { + + @Override + public HttpRequest request() { + return null; + } + + @Override + public Optional> previousResponse() { + return Optional.empty(); + } + + @Override + public HttpHeaders headers() { + return null; + } + + @Override + public Optional sslSession() { + return Optional.empty(); + } + + @Override + public URI uri() { + return null; + } + + @Override + public HttpClient.Version version() { + return null; + } + + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java new file mode 100644 index 000000000..fe698b1bf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java @@ -0,0 +1,389 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.dynamic; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.vdurmont.semver4j.Semver; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.captcha.Action; +import org.whispersystems.textsecuregcm.limits.RateLimiterConfig; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; + +class DynamicConfigurationTest { + + private static final String REQUIRED_CONFIG = """ + captcha: + scoreFloor: 1.0 + """; + + @Test + void testParseExperimentConfig() throws JsonProcessingException { + { + final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true"); + final DynamicConfiguration emptyConfig = + DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); + + assertFalse(emptyConfig.getExperimentEnrollmentConfiguration("test").isPresent()); + } + + { + final String experimentConfigYaml = REQUIRED_CONFIG.concat(""" + experiments: + percentageOnly: + enrollmentPercentage: 12 + uuidsAndPercentage: + enrolledUuids: + - 717b1c09-ed0b-4120-bb0e-f4697534b8e1 + - 279f264c-56d7-4bbf-b9da-de718ff90903 + enrollmentPercentage: 77 + uuidsOnly: + enrolledUuids: + - 71618739-114c-4b1f-bb0d-6478a44eb600 + uuids-with-dash: + enrolledUuids: + - 71618739-114c-4b1f-bb0d-6478ffffffff + """); + + final DynamicConfiguration config = + DynamicConfigurationManager.parseConfiguration(experimentConfigYaml, DynamicConfiguration.class).orElseThrow(); + + assertFalse(config.getExperimentEnrollmentConfiguration("unconfigured").isPresent()); + + assertTrue(config.getExperimentEnrollmentConfiguration("percentageOnly").isPresent()); + assertEquals(12, config.getExperimentEnrollmentConfiguration("percentageOnly").get().getEnrollmentPercentage()); + assertEquals(Collections.emptySet(), + config.getExperimentEnrollmentConfiguration("percentageOnly").get().getEnrolledUuids()); + + assertTrue(config.getExperimentEnrollmentConfiguration("uuidsAndPercentage").isPresent()); + assertEquals(77, + config.getExperimentEnrollmentConfiguration("uuidsAndPercentage").get().getEnrollmentPercentage()); + assertEquals(Set.of(UUID.fromString("717b1c09-ed0b-4120-bb0e-f4697534b8e1"), + UUID.fromString("279f264c-56d7-4bbf-b9da-de718ff90903")), + config.getExperimentEnrollmentConfiguration("uuidsAndPercentage").get().getEnrolledUuids()); + + assertTrue(config.getExperimentEnrollmentConfiguration("uuidsOnly").isPresent()); + assertEquals(0, config.getExperimentEnrollmentConfiguration("uuidsOnly").get().getEnrollmentPercentage()); + assertEquals(Set.of(UUID.fromString("71618739-114c-4b1f-bb0d-6478a44eb600")), + config.getExperimentEnrollmentConfiguration("uuidsOnly").get().getEnrolledUuids()); + + assertTrue(config.getExperimentEnrollmentConfiguration("uuids-with-dash").isPresent()); + assertEquals(0, config.getExperimentEnrollmentConfiguration("uuids-with-dash").get().getEnrollmentPercentage()); + assertEquals(Set.of(UUID.fromString("71618739-114c-4b1f-bb0d-6478ffffffff")), + config.getExperimentEnrollmentConfiguration("uuids-with-dash").get().getEnrolledUuids()); + } + } + + @Test + void testParsePreRegistrationExperiments() throws JsonProcessingException { + { + final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true"); + final DynamicConfiguration emptyConfig = + DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); + + assertFalse(emptyConfig.getPreRegistrationEnrollmentConfiguration("test").isPresent()); + } + + { + final String experimentConfigYaml = REQUIRED_CONFIG.concat(""" + preRegistrationExperiments: + percentageOnly: + enrollmentPercentage: 17 + e164sCountryCodesAndPercentage: + enrolledE164s: + - +120255551212 + - +3655323174 + excludedE164s: + - +120255551213 + - +3655323175 + enrollmentPercentage: 46 + excludedCountryCodes: + - 47 + includedCountryCodes: + - 56 + e164sAndExcludedCodes: + enrolledE164s: + - +120255551212 + excludedCountryCodes: + - 47 + """); + + final DynamicConfiguration config = + DynamicConfigurationManager.parseConfiguration(experimentConfigYaml, DynamicConfiguration.class).orElseThrow(); + + assertFalse(config.getPreRegistrationEnrollmentConfiguration("unconfigured").isPresent()); + + { + final Optional percentageOnly = config + .getPreRegistrationEnrollmentConfiguration("percentageOnly"); + assertTrue(percentageOnly.isPresent()); + assertEquals(17, + percentageOnly.get().getEnrollmentPercentage()); + assertEquals(Collections.emptySet(), + percentageOnly.get().getEnrolledE164s()); + assertEquals(Collections.emptySet(), + percentageOnly.get().getExcludedE164s()); + } + + { + final Optional e164sCountryCodesAndPercentage = config + .getPreRegistrationEnrollmentConfiguration("e164sCountryCodesAndPercentage"); + + assertTrue(e164sCountryCodesAndPercentage.isPresent()); + assertEquals(46, + e164sCountryCodesAndPercentage.get().getEnrollmentPercentage()); + assertEquals(Set.of("+120255551212", "+3655323174"), + e164sCountryCodesAndPercentage.get().getEnrolledE164s()); + assertEquals(Set.of("+120255551213", "+3655323175"), + e164sCountryCodesAndPercentage.get().getExcludedE164s()); + assertEquals(Set.of("47"), + e164sCountryCodesAndPercentage.get().getExcludedCountryCodes()); + assertEquals(Set.of("56"), + e164sCountryCodesAndPercentage.get().getIncludedCountryCodes()); + } + + { + final Optional e164sAndExcludedCodes = config + .getPreRegistrationEnrollmentConfiguration("e164sAndExcludedCodes"); + assertTrue(e164sAndExcludedCodes.isPresent()); + assertEquals(0, e164sAndExcludedCodes.get().getEnrollmentPercentage()); + assertEquals(Set.of("+120255551212"), + e164sAndExcludedCodes.get().getEnrolledE164s()); + assertTrue(e164sAndExcludedCodes.get().getExcludedE164s().isEmpty()); + assertEquals(Set.of("47"), + e164sAndExcludedCodes.get().getExcludedCountryCodes()); + } + } + } + + @Test + void testParseRemoteDeprecationConfig() throws JsonProcessingException { + { + final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true"); + final DynamicConfiguration emptyConfig = + DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); + + assertNotNull(emptyConfig.getRemoteDeprecationConfiguration()); + } + + { + final String remoteDeprecationConfig = REQUIRED_CONFIG.concat(""" + remoteDeprecation: + minimumVersions: + IOS: 1.2.3 + ANDROID: 4.5.6 + versionsPendingDeprecation: + DESKTOP: 7.8.9 + blockedVersions: + DESKTOP: + - 1.4.0-beta.2 + """); + + final DynamicConfiguration config = + DynamicConfigurationManager.parseConfiguration(remoteDeprecationConfig, DynamicConfiguration.class).orElseThrow(); + + final DynamicRemoteDeprecationConfiguration remoteDeprecationConfiguration = config + .getRemoteDeprecationConfiguration(); + + assertEquals(Map.of(ClientPlatform.IOS, new Semver("1.2.3"), ClientPlatform.ANDROID, new Semver("4.5.6")), + remoteDeprecationConfiguration.getMinimumVersions()); + assertEquals(Map.of(ClientPlatform.DESKTOP, new Semver("7.8.9")), + remoteDeprecationConfiguration.getVersionsPendingDeprecation()); + assertEquals(Map.of(ClientPlatform.DESKTOP, Set.of(new Semver("1.4.0-beta.2"))), + remoteDeprecationConfiguration.getBlockedVersions()); + assertTrue(remoteDeprecationConfiguration.getVersionsPendingBlock().isEmpty()); + } + } + + @Test + void testParsePaymentsConfiguration() throws JsonProcessingException { + { + final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true"); + final DynamicConfiguration emptyConfig = + DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); + + assertTrue(emptyConfig.getPaymentsConfiguration().getDisallowedPrefixes().isEmpty()); + } + + { + final String paymentsConfigYaml = REQUIRED_CONFIG.concat(""" + payments: + disallowedPrefixes: + - +44 + """); + + final DynamicPaymentsConfiguration config = + DynamicConfigurationManager.parseConfiguration(paymentsConfigYaml, DynamicConfiguration.class).orElseThrow() + .getPaymentsConfiguration(); + + assertEquals(List.of("+44"), config.getDisallowedPrefixes()); + } + } + + @Test + void testParseCaptchaConfiguration() throws JsonProcessingException { + { + final String emptyConfigYaml = "test: true"; + + assertTrue(DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).isEmpty(), + "empty config should not validate"); + } + + { + final String captchaConfig = """ + captcha: + scoreFloor: null + """; + + assertTrue(DynamicConfigurationManager.parseConfiguration(captchaConfig, DynamicConfiguration.class).isEmpty(), + "score floor must not be null"); + } + + { + final String captchaConfig = """ + captcha: + scoreFloor: 0.9 + scoreFloorByAction: + challenge: 0.1 + registration: 0.2 + hCaptchaSiteKeys: + challenge: + - ab317f2a-2b76-4098-84c9-ecdf8ea44f53 + registration: + - e4ddb6ff-05e7-497b-9a29-b76e7331789c + - 52fdbc88-f246-4705-a7dd-05ad85b93420 + recaptchaSiteKeys: + challenge: + - 299068b6-ac78-4288-a90b-2e2ce5a6ddfe + """; + + final DynamicCaptchaConfiguration config = + DynamicConfigurationManager.parseConfiguration(captchaConfig, DynamicConfiguration.class).orElseThrow() + .getCaptchaConfiguration(); + + assertEquals(0.9f, config.getScoreFloor().floatValue()); + assertEquals(0.1f, config.getScoreFloorByAction().get(Action.CHALLENGE).floatValue()); + assertEquals(0.2f, config.getScoreFloorByAction().get(Action.REGISTRATION).floatValue()); + + assertThat(config.getHCaptchaSiteKeys().get(Action.CHALLENGE)).contains("ab317f2a-2b76-4098-84c9-ecdf8ea44f53"); + assertThat(config.getHCaptchaSiteKeys().get(Action.REGISTRATION)).contains("e4ddb6ff-05e7-497b-9a29-b76e7331789c"); + assertThat(config.getHCaptchaSiteKeys().get(Action.REGISTRATION)).contains("52fdbc88-f246-4705-a7dd-05ad85b93420"); + + assertThat(config.getRecaptchaSiteKeys().get(Action.CHALLENGE)).contains("299068b6-ac78-4288-a90b-2e2ce5a6ddfe"); + assertThat(config.getRecaptchaSiteKeys().get(Action.REGISTRATION)).isNull(); + } + } + + @Test + void testParseLimits() throws JsonProcessingException { + final String limitsConfig = REQUIRED_CONFIG.concat(""" + limits: + rateLimitReset: + bucketSize: 17 + permitRegenerationDuration: PT0.000004S + """); + + final RateLimiterConfig resetRateLimiterConfig = + DynamicConfigurationManager.parseConfiguration(limitsConfig, DynamicConfiguration.class).orElseThrow() + .getLimits().get(RateLimiters.For.RATE_LIMIT_RESET.id()); + + assertThat(resetRateLimiterConfig.bucketSize()).isEqualTo(17); + assertThat(resetRateLimiterConfig.permitRegenerationDuration()).isEqualTo(Duration.ofNanos(4_000)); + } + + @Test + void testParseTurnConfig() throws JsonProcessingException { + { + final String config = REQUIRED_CONFIG.concat(""" + turn: + secret: bloop + uriConfigs: + - uris: + - turn:test.org + weight: -1 + """); + assertThat(DynamicConfigurationManager.parseConfiguration(config, DynamicConfiguration.class)).isEmpty(); + } + { + final String config = REQUIRED_CONFIG.concat(""" + turn: + uriConfigs: + - uris: + - turn:test0.org + - turn:test1.org + - uris: + - turn:test2.org + weight: 2 + enrolledAcis: + - 732506d7-d04f-43a4-b1d7-8a3a91ebe8a6 + """); + DynamicTurnConfiguration turnConfiguration = DynamicConfigurationManager + .parseConfiguration(config, DynamicConfiguration.class) + .orElseThrow() + .getTurnConfiguration(); + assertThat(turnConfiguration.getUriConfigs().get(0).getUris()).hasSize(2); + assertThat(turnConfiguration.getUriConfigs().get(1).getUris()).hasSize(1); + assertThat(turnConfiguration.getUriConfigs().get(0).getWeight()).isEqualTo(1); + assertThat(turnConfiguration.getUriConfigs().get(1).getWeight()).isEqualTo(2); + assertThat(turnConfiguration.getUriConfigs().get(1).getEnrolledAcis()) + .containsExactly(UUID.fromString("732506d7-d04f-43a4-b1d7-8a3a91ebe8a6")); + + } + } + + @Test + void testMessagePersister() throws JsonProcessingException { + { + final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true"); + final DynamicConfiguration emptyConfig = + DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); + + assertTrue(emptyConfig.getMessagePersisterConfiguration().isPersistenceEnabled()); + } + + { + final String messagePersisterEnabledYaml = REQUIRED_CONFIG.concat(""" + messagePersister: + persistenceEnabled: true + dedicatedProcessEnabled: true + """); + + final DynamicConfiguration config = + DynamicConfigurationManager.parseConfiguration(messagePersisterEnabledYaml, DynamicConfiguration.class) + .orElseThrow(); + + assertTrue(config.getMessagePersisterConfiguration().isPersistenceEnabled()); + } + + { + final String messagePersisterDisabledYaml = REQUIRED_CONFIG.concat(""" + messagePersister: + persistenceEnabled: false + """); + + final DynamicConfiguration config = + DynamicConfigurationManager.parseConfiguration(messagePersisterDisabledYaml, DynamicConfiguration.class) + .orElseThrow(); + + assertFalse(config.getMessagePersisterConfiguration().isPersistenceEnabled()); + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretsTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretsTest.java new file mode 100644 index 000000000..e5b645ff1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/configuration/secrets/SecretsTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.secrets; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonMappingException; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.constraints.NotEmpty; +import org.apache.commons.lang3.RandomUtils; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.util.ExactlySize; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +public class SecretsTest { + + private static final String SECRET_REF = "secret_string"; + + private static final String SECRET_LIST_REF = "secret_string_list"; + + private static final String SECRET_BYTES_REF = "secret_bytes"; + + private static final String SECRET_BYTES_LIST_REF = "secret_bytes_list"; + + public record TestData(SecretString secret, + SecretBytes secretBytes, + SecretStringList secretList, + SecretBytesList secretBytesList) { + } + + private static final String VALID_CONFIG_YAML = """ + secret: secret://%s + secretBytes: secret://%s + secretList: secret://%s + secretBytesList: secret://%s + """.formatted(SECRET_REF, SECRET_BYTES_REF, SECRET_LIST_REF, SECRET_BYTES_LIST_REF); + + + @Test + public void testDeserialization() throws Exception { + final String secretString = "secret_string"; + final byte[] secretBytes = RandomUtils.nextBytes(16); + final String secretBytesBase64 = Base64.getEncoder().encodeToString(secretBytes); + final List secretStringList = List.of("secret1", "secret2", "secret3"); + final List secretBytesList = List.of(RandomUtils.nextBytes(16), RandomUtils.nextBytes(16), RandomUtils.nextBytes(16)); + final List secretBytesListBase64 = secretBytesList.stream().map(Base64.getEncoder()::encodeToString).toList(); + final Map> storeMap = Map.of( + SECRET_REF, new SecretString(secretString), + SECRET_BYTES_REF, new SecretString(secretBytesBase64), + SECRET_LIST_REF, new SecretStringList(secretStringList), + SECRET_BYTES_LIST_REF, new SecretStringList(secretBytesListBase64) + ); + SecretsModule.INSTANCE.setSecretStore(new SecretStore(storeMap)); + + final TestData result = SystemMapper.yamlMapper().readValue(VALID_CONFIG_YAML, TestData.class); + assertEquals(secretString, result.secret().value()); + assertEquals(secretStringList, result.secretList().value()); + assertArrayEquals(secretBytes, result.secretBytes().value()); + for (int i = 0; i < secretBytesList.size(); i++) { + assertArrayEquals(secretBytesList.get(i), result.secretBytesList().value().get(i)); + } + } + + @Test + public void testValueWithoutPrefix() throws Exception { + final String config = """ + secret: ref + """; + SecretsModule.INSTANCE.setSecretStore(new SecretStore(Collections.emptyMap())); + assertThrows(JsonMappingException.class, () -> SystemMapper.yamlMapper().readValue(config, TestData.class)); + } + + @Test + public void testNoSecretInTheStore() throws Exception { + final String config = """ + secret: secret://missing + secretBytes: secret://missing + secretList: secret://missing + secretBytesList: secret://missing + """; + SecretsModule.INSTANCE.setSecretStore(new SecretStore(Collections.emptyMap())); + assertThrows(JsonMappingException.class, () -> SystemMapper.yamlMapper().readValue(config, TestData.class)); + } + + @Test + public void testSecretStoreNotSet() throws Exception { + assertThrows(JsonMappingException.class, () -> SystemMapper.yamlMapper().readValue(VALID_CONFIG_YAML, TestData.class)); + } + + @Test + public void testReadFromJson() throws Exception { + // checking that valid json secrets bundle is read correctly + final SecretStore secretStore = SecretStore.fromYamlStringSecretsBundle(""" + secret_string: value + secret_string_list: + - value1 + - value2 + - value3 + """); + assertEquals("value", secretStore.secretString("secret_string").value()); + assertEquals(List.of("value1", "value2", "value3"), secretStore.secretStringList("secret_string_list").value()); + + // checking that secrets bundle can't have objects as values + assertThrows(IllegalArgumentException.class, () -> SecretStore.fromYamlStringSecretsBundle(""" + secret_string: value + not_a_string_or_list: + k: v + """)); + + // checking that secrets bundle can't have numbers as values + assertThrows(IllegalArgumentException.class, () -> SecretStore.fromYamlStringSecretsBundle(""" + secret_string: value + not_a_string_or_list: 42 + """)); + } + + record NotEmptySecretStringList(@NotEmpty SecretStringList secret) { + } + + record NotEmptySecretBytesList(@NotEmpty SecretBytesList secret) { + } + + record ExactlySizeBytesSecret(@ExactlySize(32) SecretBytes secret) { + } + + @Test + public void testValidators() throws Exception { + final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + // @NotEmpty SecretStringList + assertFalse(validator.validate(new NotEmptySecretStringList(new SecretStringList(List.of()))).isEmpty()); + assertTrue(validator.validate(new NotEmptySecretStringList(new SecretStringList(List.of("smth")))).isEmpty()); + + // @NotEmpty SecretBytesList + assertFalse(validator.validate(new NotEmptySecretBytesList(new SecretBytesList(List.of()))).isEmpty()); + assertTrue(validator.validate(new NotEmptySecretBytesList(new SecretBytesList(List.of(new byte[4])))).isEmpty()); + + // @ExactlySize SecretBytes + assertFalse(validator.validate(new ExactlySizeBytesSecret(new SecretBytes(new byte[16]))).isEmpty()); + assertTrue(validator.validate(new ExactlySizeBytesSecret(new SecretBytes(new byte[32]))).isEmpty()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java new file mode 100644 index 000000000..eb4b5f728 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java @@ -0,0 +1,1001 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableSet; +import com.google.common.net.HttpHeaders; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.security.SecureRandom; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.Collections; +import java.util.HexFormat; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.Response; +import org.apache.commons.lang3.RandomUtils; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; +import org.signal.libsignal.usernames.BaseUsernameException; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; +import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock; +import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse; +import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse; +import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; +import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest; +import org.whispersystems.textsecuregcm.entities.EncryptedUsername; +import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; +import org.whispersystems.textsecuregcm.entities.RegistrationLock; +import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest; +import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse; +import org.whispersystems.textsecuregcm.entities.UsernameHashResponse; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; +import org.whispersystems.textsecuregcm.limits.RateLimitByIpFilter; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.JsonMappingExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; +import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableException; +import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.MockUtils; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.UsernameHashZkProofVerifier; + +@ExtendWith(DropwizardExtensionsSupport.class) +class AccountControllerTest { + private static final String SENDER = "+14152222222"; + private static final String SENDER_OLD = "+14151111111"; + private static final String SENDER_PIN = "+14153333333"; + private static final String SENDER_OVER_PIN = "+14154444444"; + private static final String SENDER_PREAUTH = "+14157777777"; + private static final String SENDER_REG_LOCK = "+14158888888"; + private static final String SENDER_HAS_STORAGE = "+14159999999"; + private static final String SENDER_TRANSFER = "+14151111112"; + private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE"; + private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc"; + private static final String BASE_64_URL_ENCRYPTED_USERNAME_1 = "md1votbj9r794DsqTNrBqA"; + + private static final String INVALID_BASE_64_URL_USERNAME_HASH = "fA+VkNbvB6dVfx/6NpaRSK6mvhhAUBgDNWFaD7+7gvs="; + private static final String TOO_SHORT_BASE_64_URL_USERNAME_HASH = "P2oMuxx0xgGxSpTO0ACq3IztEOBDaV9t9YFu4bAGpQ"; + private static final byte[] USERNAME_HASH_1 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_1); + private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2); + private static final byte[] ENCRYPTED_USERNAME_1 = Base64.getUrlDecoder().decode(BASE_64_URL_ENCRYPTED_USERNAME_1); + private static final byte[] INVALID_USERNAME_HASH = Base64.getDecoder().decode(INVALID_BASE_64_URL_USERNAME_HASH); + private static final byte[] TOO_SHORT_USERNAME_HASH = Base64.getUrlDecoder().decode(TOO_SHORT_BASE_64_URL_USERNAME_HASH); + private static final String BASE_64_URL_ZK_PROOF = "2kambOgmdeeIO0faCMgR6HR4G2BQ5bnhXdIe9ZuZY0NmQXSra5BzDBQ7jzy1cvoEqUHYLpBYMrXudkYPJaWoQg"; + private static final byte[] ZK_PROOF = Base64.getUrlDecoder().decode(BASE_64_URL_ZK_PROOF); + private static final UUID SENDER_REG_LOCK_UUID = UUID.randomUUID(); + private static final UUID SENDER_TRANSFER_UUID = UUID.randomUUID(); + + private static final String NICE_HOST = "127.0.0.1"; + private static final String RATE_LIMITED_IP_HOST = "10.0.0.1"; + + private static AccountsManager accountsManager = mock(AccountsManager.class); + private static RateLimiters rateLimiters = mock(RateLimiters.class); + private static RateLimiter rateLimiter = mock(RateLimiter.class); + private static RateLimiter usernameSetLimiter = mock(RateLimiter.class); + private static RateLimiter usernameReserveLimiter = mock(RateLimiter.class); + private static RateLimiter usernameLookupLimiter = mock(RateLimiter.class); + private static RateLimiter checkAccountExistence = mock(RateLimiter.class); + private static TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class); + private static Account senderPinAccount = mock(Account.class); + private static Account senderRegLockAccount = mock(Account.class); + private static Account senderHasStorage = mock(Account.class); + private static Account senderTransfer = mock(Account.class); + private static RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock( + RegistrationRecoveryPasswordsManager.class); + private static final UsernameHashZkProofVerifier usernameZkProofVerifier = mock(UsernameHashZkProofVerifier.class); + + private byte[] registration_lock_key = new byte[32]; + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) + .addProvider(AuthHelper.getAuthFilter()) + .addProvider( + new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, + DisabledPermittedAuthenticatedAccount.class))) + .addProvider(new JsonMappingExceptionMapper()) + .addProvider(new RateLimitExceededExceptionMapper()) + .addProvider(new ImpossiblePhoneNumberExceptionMapper()) + .addProvider(new NonNormalizedPhoneNumberExceptionMapper()) + .addProvider(new RateLimitByIpFilter(rateLimiters)) + .addProvider(ScoreThresholdProvider.ScoreThresholdFeature.class) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new AccountController( + accountsManager, + rateLimiters, + turnTokenGenerator, + registrationRecoveryPasswordsManager, + usernameZkProofVerifier + )) + .build(); + + + @BeforeEach + void setup() throws Exception { + clearInvocations(AuthHelper.VALID_ACCOUNT, AuthHelper.UNDISCOVERABLE_ACCOUNT); + + new SecureRandom().nextBytes(registration_lock_key); + SaltedTokenHash registrationLockCredentials = SaltedTokenHash.generateFor( + HexFormat.of().formatHex(registration_lock_key)); + + AccountsHelper.setupMockUpdate(accountsManager); + + when(rateLimiters.getUsernameSetLimiter()).thenReturn(usernameSetLimiter); + when(rateLimiters.getUsernameReserveLimiter()).thenReturn(usernameReserveLimiter); + when(rateLimiters.getUsernameLookupLimiter()).thenReturn(usernameLookupLimiter); + when(rateLimiters.forDescriptor(eq(RateLimiters.For.USERNAME_LOOKUP))).thenReturn(usernameLookupLimiter); + when(rateLimiters.forDescriptor(eq(RateLimiters.For.CHECK_ACCOUNT_EXISTENCE))).thenReturn(checkAccountExistence); + + when(usernameSetLimiter.validateAsync(any(UUID.class))).thenReturn(CompletableFuture.completedFuture(null)); + + when(senderPinAccount.getLastSeen()).thenReturn(System.currentTimeMillis()); + when(senderPinAccount.getRegistrationLock()).thenReturn( + new StoredRegistrationLock(Optional.empty(), Optional.empty(), Instant.ofEpochMilli(System.currentTimeMillis()))); + + when(senderHasStorage.getUuid()).thenReturn(UUID.randomUUID()); + when(senderHasStorage.isStorageSupported()).thenReturn(true); + when(senderHasStorage.getRegistrationLock()).thenReturn( + new StoredRegistrationLock(Optional.empty(), Optional.empty(), Instant.ofEpochMilli(System.currentTimeMillis()))); + + when(senderRegLockAccount.getRegistrationLock()).thenReturn( + new StoredRegistrationLock(Optional.of(registrationLockCredentials.hash()), + Optional.of(registrationLockCredentials.salt()), Instant.ofEpochMilli(System.currentTimeMillis()))); + when(senderRegLockAccount.getLastSeen()).thenReturn(System.currentTimeMillis()); + when(senderRegLockAccount.getUuid()).thenReturn(SENDER_REG_LOCK_UUID); + when(senderRegLockAccount.getNumber()).thenReturn(SENDER_REG_LOCK); + + when(senderTransfer.getRegistrationLock()).thenReturn( + new StoredRegistrationLock(Optional.empty(), Optional.empty(), Instant.ofEpochMilli(System.currentTimeMillis()))); + when(senderTransfer.getUuid()).thenReturn(SENDER_TRANSFER_UUID); + when(senderTransfer.getNumber()).thenReturn(SENDER_TRANSFER); + + when(accountsManager.getByE164(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount)); + when(accountsManager.getByE164(eq(SENDER_REG_LOCK))).thenReturn(Optional.of(senderRegLockAccount)); + when(accountsManager.getByE164(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount)); + when(accountsManager.getByE164(eq(SENDER))).thenReturn(Optional.empty()); + when(accountsManager.getByE164(eq(SENDER_OLD))).thenReturn(Optional.empty()); + when(accountsManager.getByE164(eq(SENDER_PREAUTH))).thenReturn(Optional.empty()); + when(accountsManager.getByE164(eq(SENDER_HAS_STORAGE))).thenReturn(Optional.of(senderHasStorage)); + when(accountsManager.getByE164(eq(SENDER_TRANSFER))).thenReturn(Optional.of(senderTransfer)); + + when(accountsManager.create(any(), any(), any(), any(), any())).thenAnswer((Answer) invocation -> { + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(UUID.randomUUID()); + when(account.getNumber()).thenReturn(invocation.getArgument(0, String.class)); + when(account.getBadges()).thenReturn(invocation.getArgument(4, List.class)); + + return account; + }); + } + + @AfterEach + void teardown() { + reset( + accountsManager, + rateLimiters, + rateLimiter, + usernameSetLimiter, + usernameReserveLimiter, + usernameLookupLimiter, + turnTokenGenerator, + senderPinAccount, + senderRegLockAccount, + senderHasStorage, + senderTransfer, + usernameZkProofVerifier); + + clearInvocations(AuthHelper.DISABLED_DEVICE); + } + + @Test + void testSetRegistrationLock() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/registration_lock/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new RegistrationLock("1234567890123456789012345678901234567890123456789012345678901234"))); + + assertThat(response.getStatus()).isEqualTo(204); + + ArgumentCaptor pinCapture = ArgumentCaptor.forClass(String.class); + ArgumentCaptor pinSaltCapture = ArgumentCaptor.forClass(String.class); + + verify(AuthHelper.VALID_ACCOUNT, times(1)).setRegistrationLock(pinCapture.capture(), pinSaltCapture.capture()); + + assertThat(pinCapture.getValue()).isNotEmpty(); + assertThat(pinSaltCapture.getValue()).isNotEmpty(); + + assertThat(pinCapture.getValue().length()).isEqualTo(66); + } + + @Test + void testSetShortRegistrationLock() throws Exception { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/registration_lock/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new RegistrationLock("313"))); + + assertThat(response.getStatus()).isEqualTo(422); + } + + @Test + void testSetRegistrationLockDisabled() throws Exception { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/registration_lock/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .put(Entity.json(new RegistrationLock("1234567890123456789012345678901234567890123456789012345678901234"))); + + assertThat(response.getStatus()).isEqualTo(401); + } + + @Test + void testSetGcmId() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/gcm/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .put(Entity.json(new GcmRegistrationId("z000"))); + + assertThat(response.getStatus()).isEqualTo(204); + + verify(AuthHelper.DISABLED_DEVICE, times(1)).setGcmId(eq("z000")); + verify(accountsManager, times(1)).updateDevice(eq(AuthHelper.DISABLED_ACCOUNT), anyLong(), any()); + } + + @Test + void testSetGcmIdInvalidrequest() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/gcm/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .put(Entity.json("{}")); + + assertThat(response.getStatus()).isEqualTo(422); + + } + + @Test + void testSetApnId() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/apn/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .put(Entity.json(new ApnRegistrationId("first", "second"))); + + assertThat(response.getStatus()).isEqualTo(204); + + verify(AuthHelper.DISABLED_DEVICE, times(1)).setApnId(eq("first")); + verify(AuthHelper.DISABLED_DEVICE, times(1)).setVoipApnId(eq("second")); + verify(accountsManager, times(1)).updateDevice(eq(AuthHelper.DISABLED_ACCOUNT), anyLong(), any()); + } + + @Test + void testSetApnIdNoVoip() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/apn/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .put(Entity.json(new ApnRegistrationId("first", null))); + + assertThat(response.getStatus()).isEqualTo(204); + + verify(AuthHelper.DISABLED_DEVICE, times(1)).setApnId(eq("first")); + verify(AuthHelper.DISABLED_DEVICE, times(1)).setVoipApnId(null); + verify(accountsManager, times(1)).updateDevice(eq(AuthHelper.DISABLED_ACCOUNT), anyLong(), any()); + } + + @ParameterizedTest + @MethodSource + void testWhoAmI(final String path, final boolean enabledAccount, final int expectedHttpStatusCode) { + final UUID aci; + final String password; + if (enabledAccount) { + aci = AuthHelper.VALID_UUID; + password = AuthHelper.VALID_PASSWORD; + } else { + aci = AuthHelper.DISABLED_UUID; + password = AuthHelper.DISABLED_PASSWORD; + } + + final Response response = resources.getJerseyTest() + .target(path) + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(aci, password)) + .get(); + + assertThat(response.getStatus()).isEqualTo(expectedHttpStatusCode); + + if (expectedHttpStatusCode == 200) { + assertThat(response.readEntity(AccountIdentityResponse.class).uuid()).isEqualTo(aci); + } + } + + static Stream testWhoAmI() { + return Stream.of( + Arguments.of("/v1/accounts/whoami", true, 200), + Arguments.of("/v1/accounts/whoami", false, 401), + Arguments.of("/v1/accounts/me", true, 200), + Arguments.of("/v1/accounts/me", false, 200) + ); + } + + static Stream testSetUsernameLink() { + return Stream.of( + Arguments.of(false, true, true, 32, 401), + Arguments.of(true, true, false, 32, 409), + Arguments.of(true, true, true, 129, 422), + Arguments.of(true, true, true, 0, 422), + Arguments.of(true, false, true, 32, 429), + Arguments.of(true, true, true, 128, 200) + ); + } + + @ParameterizedTest + @MethodSource + public void testSetUsernameLink( + final boolean auth, + final boolean passRateLimiting, + final boolean setUsernameHash, + final int payloadSize, + final int expectedStatus) throws Exception { + + // checking if rate limiting needs to pass or fail for this test + if (passRateLimiting) { + MockUtils.updateRateLimiterResponseToAllow( + rateLimiters, RateLimiters.For.USERNAME_LINK_OPERATION, AuthHelper.VALID_UUID); + } else { + MockUtils.updateRateLimiterResponseToFail( + rateLimiters, RateLimiters.For.USERNAME_LINK_OPERATION, AuthHelper.VALID_UUID, Duration.ofMinutes(10), false); + } + + // checking if username is to be set for this test + if (setUsernameHash) { + when(AuthHelper.VALID_ACCOUNT.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH_1)); + } else { + when(AuthHelper.VALID_ACCOUNT.getUsernameHash()).thenReturn(Optional.empty()); + } + + final Invocation.Builder builder = resources.getJerseyTest() + .target("/v1/accounts/username_link") + .request(); + + // checking if auth is needed for this test + if (auth) { + builder.header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); + } + + // make sure `update()` works + doReturn(AuthHelper.VALID_ACCOUNT).when(accountsManager).update(any(), any()); + + final Response put = builder.put(Entity.json(new EncryptedUsername(RandomUtils.nextBytes(payloadSize)))); + + assertEquals(expectedStatus, put.getStatus()); + } + + static Stream testDeleteUsernameLink() { + return Stream.of( + Arguments.of(false, true, 401), + Arguments.of(true, false, 429), + Arguments.of(true, true, 204) + ); + } + + @ParameterizedTest + @MethodSource + public void testDeleteUsernameLink( + final boolean auth, + final boolean passRateLimiting, + final int expectedStatus) throws Exception { + + // checking if rate limiting needs to pass or fail for this test + if (passRateLimiting) { + MockUtils.updateRateLimiterResponseToAllow( + rateLimiters, RateLimiters.For.USERNAME_LINK_OPERATION, AuthHelper.VALID_UUID); + } else { + MockUtils.updateRateLimiterResponseToFail( + rateLimiters, RateLimiters.For.USERNAME_LINK_OPERATION, AuthHelper.VALID_UUID, Duration.ofMinutes(10), false); + } + + final Invocation.Builder builder = resources.getJerseyTest() + .target("/v1/accounts/username_link") + .request(); + + // checking if auth is needed for this test + if (auth) { + builder.header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); + } + + // make sure `update()` works + doReturn(AuthHelper.VALID_ACCOUNT).when(accountsManager).update(any(), any()); + + final Response delete = builder.delete(); + + assertEquals(expectedStatus, delete.getStatus()); + } + + static Stream testLookupUsernameLink() { + return Stream.of( + Arguments.of(false, true, true, true, 400), + Arguments.of(true, false, true, true, 429), + Arguments.of(true, true, false, true, 404), + Arguments.of(true, true, true, false, 404), + Arguments.of(true, true, true, true, 200) + ); + } + + @ParameterizedTest + @MethodSource + public void testLookupUsernameLink( + final boolean stayUnauthenticated, + final boolean passRateLimiting, + final boolean validUuidInput, + final boolean locateLinkByUuid, + final int expectedStatus) { + + MockUtils.updateRateLimiterResponseToAllow( + rateLimiters, RateLimiters.For.USERNAME_LINK_LOOKUP_PER_IP, NICE_HOST); + MockUtils.updateRateLimiterResponseToFail( + rateLimiters, RateLimiters.For.USERNAME_LINK_LOOKUP_PER_IP, RATE_LIMITED_IP_HOST, Duration.ofMinutes(10), false); + + when(accountsManager.getByUsernameLinkHandle(any())).thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + final String uuid = validUuidInput ? UUID.randomUUID().toString() : "invalid-uuid"; + + if (validUuidInput && locateLinkByUuid) { + final Account account = mock(Account.class); + when(account.getEncryptedUsername()).thenReturn(Optional.of(RandomUtils.nextBytes(16))); + when(accountsManager.getByUsernameLinkHandle(UUID.fromString(uuid))).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + } + + final Invocation.Builder builder = resources.getJerseyTest() + .target("/v1/accounts/username_link/" + uuid) + .request(); + if (!stayUnauthenticated) { + builder.header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); + } + final Response get = builder + .header(HttpHeaders.X_FORWARDED_FOR, passRateLimiting ? NICE_HOST : RATE_LIMITED_IP_HOST) + .get(); + + assertEquals(expectedStatus, get.getStatus()); + } + + @Test + void testReserveUsernameHash() { + when(accountsManager.reserveUsernameHash(any(), any())) + .thenReturn(CompletableFuture.completedFuture(new AccountsManager.UsernameReservation(null, USERNAME_HASH_1))); + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/reserve") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new ReserveUsernameHashRequest(List.of(USERNAME_HASH_1, USERNAME_HASH_2)))); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.readEntity(ReserveUsernameHashResponse.class)) + .satisfies(r -> assertThat(r.usernameHash()).hasSize(32)); + } + + @Test + void testReserveUsernameHashUnavailable() { + when(accountsManager.reserveUsernameHash(any(), anyList())) + .thenReturn(CompletableFuture.failedFuture(new UsernameHashNotAvailableException())); + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/reserve") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new ReserveUsernameHashRequest(List.of(USERNAME_HASH_1, USERNAME_HASH_2)))); + assertThat(response.getStatus()).isEqualTo(409); + } + + @ParameterizedTest + @MethodSource + void testReserveUsernameHashListSizeInvalid(List usernameHashes) { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/reserve") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new ReserveUsernameHashRequest(usernameHashes))); + assertThat(response.getStatus()).isEqualTo(422); + } + + static Stream testReserveUsernameHashListSizeInvalid() { + return Stream.of( + Arguments.of(Collections.nCopies(21, USERNAME_HASH_1)), + Arguments.of(Collections.emptyList()) + ); + } + + @Test + void testReserveUsernameHashInvalidHashSize() { + List usernameHashes = List.of(new byte[31]); + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/reserve") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new ReserveUsernameHashRequest(usernameHashes))); + assertThat(response.getStatus()).isEqualTo(422); + } + + @Test + void testReserveUsernameHashNullList() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/reserve") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new ReserveUsernameHashRequest(null))); + assertThat(response.getStatus()).isEqualTo(422); + } + + @Test + void testReserveUsernameHashInvalidBase64UrlEncoding() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/reserve") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json( + // Has '+' and '='characters which are invalid in base64url + """ + { + "usernameHashes": ["jh1jJ50oGn9wUXAFNtDus6AJgWOQ6XbZzF+wCv7OOQs="] + } + """)); + assertThat(response.getStatus()).isEqualTo(422); + } + + @Test + void testConfirmUsernameHash() throws BaseUsernameException { + Account account = mock(Account.class); + final UUID uuid = UUID.randomUUID(); + when(account.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH_1)); + when(account.getUsernameLinkHandle()).thenReturn(uuid); + when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1), eq(ENCRYPTED_USERNAME_1))) + .thenReturn(CompletableFuture.completedFuture(account)); + + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/confirm") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new ConfirmUsernameHashRequest(USERNAME_HASH_1, ZK_PROOF, ENCRYPTED_USERNAME_1))); + assertThat(response.getStatus()).isEqualTo(200); + + final UsernameHashResponse respEntity = response.readEntity(UsernameHashResponse.class); + assertArrayEquals(respEntity.usernameHash(), USERNAME_HASH_1); + assertEquals(respEntity.usernameLinkHandle(), uuid); + verify(usernameZkProofVerifier).verifyProof(ZK_PROOF, USERNAME_HASH_1); + } + + @Test + void testConfirmUsernameHashOld() throws BaseUsernameException { + Account account = mock(Account.class); + when(account.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH_1)); + when(account.getUsernameLinkHandle()).thenReturn(null); + when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1), eq(null))) + .thenReturn(CompletableFuture.completedFuture(account)); + + + + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/confirm") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new ConfirmUsernameHashRequest(USERNAME_HASH_1, ZK_PROOF, null))); + assertThat(response.getStatus()).isEqualTo(200); + + final UsernameHashResponse respEntity = response.readEntity(UsernameHashResponse.class); + assertArrayEquals(respEntity.usernameHash(), USERNAME_HASH_1); + assertNull(respEntity.usernameLinkHandle()); + verify(usernameZkProofVerifier).verifyProof(ZK_PROOF, USERNAME_HASH_1); + } + + @Test + void testConfirmUnreservedUsernameHash() throws BaseUsernameException { + when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1), any())) + .thenReturn(CompletableFuture.failedFuture(new UsernameReservationNotFoundException())); + + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/confirm") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new ConfirmUsernameHashRequest(USERNAME_HASH_1, ZK_PROOF, ENCRYPTED_USERNAME_1))); + assertThat(response.getStatus()).isEqualTo(409); + verify(usernameZkProofVerifier).verifyProof(ZK_PROOF, USERNAME_HASH_1); + } + + @Test + void testConfirmLapsedUsernameHash() throws BaseUsernameException { + when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1), any())) + .thenReturn(CompletableFuture.failedFuture(new UsernameHashNotAvailableException())); + + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/confirm") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new ConfirmUsernameHashRequest(USERNAME_HASH_1, ZK_PROOF, ENCRYPTED_USERNAME_1))); + assertThat(response.getStatus()).isEqualTo(410); + verify(usernameZkProofVerifier).verifyProof(ZK_PROOF, USERNAME_HASH_1); + } + + @Test + void testConfirmUsernameHashInvalidBase64UrlEncoding() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/confirm") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json( + // Has '+' and '='characters which are invalid in base64url + """ + { + "usernameHash": "jh1jJ50oGn9wUXAFNtDus6AJgWOQ6XbZzF+wCv7OOQs=", + "zkProof": "iYXE0QPK60PS3lGa-xdNv0GlXA3B03xQLzltSf-2xmscyS_8fjy5H9ymfaEr62PcVY7tsWhWjOOvcCnhmP_HS=" + } + """)); + assertThat(response.getStatus()).isEqualTo(422); + verifyNoInteractions(usernameZkProofVerifier); + } + + @Test + void testConfirmUsernameHashInvalidHashSize() { + byte[] usernameHash = new byte[31]; + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/confirm") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new ConfirmUsernameHashRequest(usernameHash, ZK_PROOF, ENCRYPTED_USERNAME_1))); + assertThat(response.getStatus()).isEqualTo(422); + verifyNoInteractions(usernameZkProofVerifier); + } + + @Test + void testCommitUsernameHashWithInvalidProof() throws BaseUsernameException { + doThrow(new BaseUsernameException("invalid username")).when(usernameZkProofVerifier).verifyProof(eq(ZK_PROOF), eq(USERNAME_HASH_1)); + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/confirm") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new ConfirmUsernameHashRequest(USERNAME_HASH_1, ZK_PROOF, ENCRYPTED_USERNAME_1))); + assertThat(response.getStatus()).isEqualTo(422); + verify(usernameZkProofVerifier).verifyProof(ZK_PROOF, USERNAME_HASH_1); + } + + @Test + void testDeleteUsername() { + when(accountsManager.clearUsernameHash(any())) + .thenAnswer(invocation -> CompletableFuture.completedFuture(invocation.getArgument(0))); + + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .delete(); + + assertThat(response.getStatus()).isEqualTo(204); + verify(accountsManager).clearUsernameHash(AuthHelper.VALID_ACCOUNT); + } + + @Test + void testDeleteUsernameBadAuth() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/username_hash/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD)) + .delete(); + + assertThat(response.getStatus()).isEqualTo(401); + } + + @Test + void testSetAccountAttributesNoDiscoverabilityChange() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/attributes/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new AccountAttributes(false, 2222, null, null, true, null))); + + assertThat(response.getStatus()).isEqualTo(204); + } + + @Test + void testSetAccountAttributesEnableDiscovery() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/attributes/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.UNDISCOVERABLE_UUID, AuthHelper.UNDISCOVERABLE_PASSWORD)) + .put(Entity.json(new AccountAttributes(false, 2222, null, null, true, null))); + + assertThat(response.getStatus()).isEqualTo(204); + } + + @Test + void testAccountsAttributesUpdateRecoveryPassword() { + final byte[] recoveryPassword = RandomUtils.nextBytes(32); + final Response response = + resources.getJerseyTest() + .target("/v1/accounts/attributes/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.UNDISCOVERABLE_UUID, AuthHelper.UNDISCOVERABLE_PASSWORD)) + .put(Entity.json(new AccountAttributes(false, 2222, null, null, true, null) + .withRecoveryPassword(recoveryPassword))); + + assertThat(response.getStatus()).isEqualTo(204); + verify(registrationRecoveryPasswordsManager).storeForCurrentNumber(eq(AuthHelper.UNDISCOVERABLE_NUMBER), eq(recoveryPassword)); + } + + @Test + void testSetAccountAttributesDisableDiscovery() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/attributes/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new AccountAttributes(false, 2222, null, null, false, null))); + + assertThat(response.getStatus()).isEqualTo(204); + } + + @Test + void testSetAccountAttributesBadUnidentifiedKeyLength() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/attributes/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new AccountAttributes(false, 2222, null, null, false, null) + .withUnidentifiedAccessKey(new byte[7]))); + + assertThat(response.getStatus()).isEqualTo(422); + } + + @Test + void testDeleteAccount() { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/me") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .delete(); + + assertThat(response.getStatus()).isEqualTo(204); + verify(accountsManager).delete(AuthHelper.VALID_ACCOUNT, AccountsManager.DeletionReason.USER_REQUEST); + } + + @Test + void testDeleteAccountException() { + when(accountsManager.delete(any(), any())).thenReturn(CompletableFuture.failedFuture(new RuntimeException("OH NO"))); + + try (final Response response = resources.getJerseyTest() + .target("/v1/accounts/me") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .delete()) { + + assertThat(response.getStatus()).isEqualTo(500); + verify(accountsManager).delete(AuthHelper.VALID_ACCOUNT, AccountsManager.DeletionReason.USER_REQUEST); + } + } + + @Test + void testAccountExists() { + final Account account = mock(Account.class); + + final UUID accountIdentifier = UUID.randomUUID(); + final UUID phoneNumberIdentifier = UUID.randomUUID(); + + when(accountsManager.getByServiceIdentifier(any())).thenReturn(Optional.empty()); + when(accountsManager.getByServiceIdentifier(new AciServiceIdentifier(accountIdentifier))).thenReturn(Optional.of(account)); + when(accountsManager.getByServiceIdentifier(new PniServiceIdentifier(phoneNumberIdentifier))).thenReturn(Optional.of(account)); + + when(rateLimiters.getCheckAccountExistenceLimiter()).thenReturn(mock(RateLimiter.class)); + + assertThat(resources.getJerseyTest() + .target(String.format("/v1/accounts/account/%s", accountIdentifier)) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1") + .head() + .getStatus()).isEqualTo(200); + + assertThat(resources.getJerseyTest() + .target(String.format("/v1/accounts/account/PNI:%s", phoneNumberIdentifier)) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1") + .head() + .getStatus()).isEqualTo(200); + + assertThat(resources.getJerseyTest() + .target(String.format("/v1/accounts/account/%s", UUID.randomUUID())) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1") + .head() + .getStatus()).isEqualTo(404); + } + + @Test + void testAccountExistsRateLimited() throws RateLimitExceededException { + final Duration expectedRetryAfter = Duration.ofSeconds(13); + final Account account = mock(Account.class); + final UUID accountIdentifier = UUID.randomUUID(); + when(accountsManager.getByAccountIdentifier(accountIdentifier)).thenReturn(Optional.of(account)); + + MockUtils.updateRateLimiterResponseToFail( + rateLimiters, RateLimiters.For.CHECK_ACCOUNT_EXISTENCE, "127.0.0.1", expectedRetryAfter, true); + + final Response response = resources.getJerseyTest() + .target(String.format("/v1/accounts/account/%s", accountIdentifier)) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1") + .head(); + + assertThat(response.getStatus()).isEqualTo(413); + assertThat(response.getHeaderString("Retry-After")).isEqualTo(String.valueOf(expectedRetryAfter.toSeconds())); + } + + @Test + void testAccountExistsNoForwardedFor() throws RateLimitExceededException { + final Response response = resources.getJerseyTest() + .target(String.format("/v1/accounts/account/%s", UUID.randomUUID())) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "") + .head(); + + assertThat(response.getStatus()).isEqualTo(413); + assertThat(Long.parseLong(response.getHeaderString("Retry-After"))).isNotNegative(); + } + + @Test + void testAccountExistsAuthenticated() { + assertThat(resources.getJerseyTest() + .target(String.format("/v1/accounts/account/%s", UUID.randomUUID())) + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1") + .head() + .getStatus()).isEqualTo(400); + } + + @Test + void testLookupUsername() { + final Account account = mock(Account.class); + final UUID uuid = UUID.randomUUID(); + when(account.getUuid()).thenReturn(uuid); + + when(accountsManager.getByUsernameHash(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + Response response = resources.getJerseyTest() + .target(String.format("v1/accounts/username_hash/%s", BASE_64_URL_USERNAME_HASH_1)) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1") + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.readEntity(AccountIdentifierResponse.class).uuid().uuid()).isEqualTo(uuid); + } + + @Test + void testLookupUsernameDoesNotExist() { + when(accountsManager.getByUsernameHash(any())).thenReturn(CompletableFuture.completedFuture(Optional.empty())); + assertThat(resources.getJerseyTest() + .target(String.format("v1/accounts/username_hash/%s", BASE_64_URL_USERNAME_HASH_1)) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1") + .get().getStatus()).isEqualTo(404); + } + + @Test + void testLookupUsernameRateLimited() throws RateLimitExceededException { + final Duration expectedRetryAfter = Duration.ofSeconds(13); + MockUtils.updateRateLimiterResponseToFail( + rateLimiters, RateLimiters.For.USERNAME_LOOKUP, "127.0.0.1", expectedRetryAfter, true); + final Response response = resources.getJerseyTest() + .target(String.format("v1/accounts/username_hash/%s", BASE_64_URL_USERNAME_HASH_1)) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1") + .get(); + + assertThat(response.getStatus()).isEqualTo(413); + assertThat(response.getHeaderString("Retry-After")).isEqualTo(String.valueOf(expectedRetryAfter.toSeconds())); + } + + @Test + void testLookupUsernameAuthenticated() { + assertThat(resources.getJerseyTest() + .target(String.format("/v1/accounts/username_hash/%s", USERNAME_HASH_1)) + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1") + .get() + .getStatus()).isEqualTo(400); + } + + @Test + void testLookupUsernameInvalidFormat() { + assertThat(resources.getJerseyTest() + .target(String.format("/v1/accounts/username_hash/%s", INVALID_USERNAME_HASH)) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1") + .get() + .getStatus()).isEqualTo(422); + + assertThat(resources.getJerseyTest() + .target(String.format("/v1/accounts/username_hash/%s", TOO_SHORT_USERNAME_HASH)) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1") + .get() + .getStatus()).isEqualTo(422); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2Test.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2Test.java new file mode 100644 index 000000000..e6ff0d951 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2Test.java @@ -0,0 +1,836 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableSet; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.Phonenumber; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.apache.http.HttpStatus; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager; +import org.whispersystems.textsecuregcm.auth.RegistrationLockError; +import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; +import org.whispersystems.textsecuregcm.entities.AccountDataReportResponse; +import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse; +import org.whispersystems.textsecuregcm.entities.ChangeNumberRequest; +import org.whispersystems.textsecuregcm.entities.PhoneNumberDiscoverabilityRequest; +import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountBadge; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.ChangeNumberManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.Util; + +@ExtendWith(DropwizardExtensionsSupport.class) +class AccountControllerV2Test { + + private static final long SESSION_EXPIRATION_SECONDS = Duration.ofMinutes(10).toSeconds(); + + private static final IdentityKey IDENTITY_KEY = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + + private static final String NEW_NUMBER = PhoneNumberUtil.getInstance().format( + PhoneNumberUtil.getInstance().getExampleNumber("US"), + PhoneNumberUtil.PhoneNumberFormat.E164); + + private final AccountsManager accountsManager = mock(AccountsManager.class); + private final ChangeNumberManager changeNumberManager = mock(ChangeNumberManager.class); + private final RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class); + private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock( + RegistrationRecoveryPasswordsManager.class); + private final RegistrationLockVerificationManager registrationLockVerificationManager = mock( + RegistrationLockVerificationManager.class); + private final RateLimiters rateLimiters = mock(RateLimiters.class); + private final RateLimiter registrationLimiter = mock(RateLimiter.class); + + private final ResourceExtension resources = ResourceExtension.builder() + .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) + .addProvider(AuthHelper.getAuthFilter()) + .addProvider( + new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, + DisabledPermittedAuthenticatedAccount.class))) + .addProvider(new RateLimitExceededExceptionMapper()) + .addProvider(new ImpossiblePhoneNumberExceptionMapper()) + .addProvider(new NonNormalizedPhoneNumberExceptionMapper()) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource( + new AccountControllerV2(accountsManager, changeNumberManager, + new PhoneVerificationTokenManager(registrationServiceClient, registrationRecoveryPasswordsManager), + registrationLockVerificationManager, rateLimiters)) + .build(); + + @Nested + class ChangeNumber { + + @BeforeEach + void setUp() throws Exception { + when(rateLimiters.getRegistrationLimiter()).thenReturn(registrationLimiter); + + when(changeNumberManager.changeNumber(any(), any(), any(), any(), any(), any(), any())).thenAnswer( + (Answer) invocation -> { + final Account account = invocation.getArgument(0); + final String number = invocation.getArgument(1); + final IdentityKey pniIdentityKey = invocation.getArgument(2); + + final UUID uuid = account.getUuid(); + final List devices = account.getDevices(); + + final Account updatedAccount = mock(Account.class); + when(updatedAccount.getUuid()).thenReturn(uuid); + when(updatedAccount.getNumber()).thenReturn(number); + when(updatedAccount.getIdentityKey(IdentityType.PNI)).thenReturn(pniIdentityKey); + if (number.equals(account.getNumber())) { + when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(AuthHelper.VALID_PNI); + } else { + when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(UUID.randomUUID()); + } + when(updatedAccount.getDevices()).thenReturn(devices); + + for (long i = 1; i <= 3; i++) { + final Optional d = account.getDevice(i); + when(updatedAccount.getDevice(i)).thenReturn(d); + } + + return updatedAccount; + }); + } + + @Test + void changeNumberSuccess() throws Exception { + + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new RegistrationServiceSession(new byte[16], NEW_NUMBER, true, null, null, null, + SESSION_EXPIRATION_SECONDS)))); + + final AccountIdentityResponse accountIdentityResponse = + resources.getJerseyTest() + .target("/v2/accounts/number") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity( + new ChangeNumberRequest(encodeSessionId("session"), null, NEW_NUMBER, "123", new IdentityKey(Curve.generateKeyPair().getPublicKey()), + Collections.emptyList(), + Collections.emptyMap(), null, Collections.emptyMap()), + MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); + + verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(NEW_NUMBER), any(), any(), any(), + any(), any()); + + assertEquals(AuthHelper.VALID_UUID, accountIdentityResponse.uuid()); + assertEquals(NEW_NUMBER, accountIdentityResponse.number()); + assertNotEquals(AuthHelper.VALID_PNI, accountIdentityResponse.pni()); + } + + @Test + void changeNumberSameNumber() throws Exception { + final AccountIdentityResponse accountIdentityResponse = + resources.getJerseyTest() + .target("/v2/accounts/number") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity( + new ChangeNumberRequest(encodeSessionId("session"), null, AuthHelper.VALID_NUMBER, null, + new IdentityKey(Curve.generateKeyPair().getPublicKey()), + Collections.emptyList(), + Collections.emptyMap(), null, Collections.emptyMap()), + MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); + + verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(AuthHelper.VALID_NUMBER), any(), any(), any(), + any(), any()); + + assertEquals(AuthHelper.VALID_UUID, accountIdentityResponse.uuid()); + assertEquals(AuthHelper.VALID_NUMBER, accountIdentityResponse.number()); + assertEquals(AuthHelper.VALID_PNI, accountIdentityResponse.pni()); + } + + @Test + void unprocessableRequestJson() { + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/number") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); + try (Response response = request.put(Entity.json(unprocessableJson()))) { + assertEquals(400, response.getStatus()); + } + } + + @Test + void missingBasicAuthorization() { + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/number") + .request(); + try (Response response = request.put(Entity.json(requestJson("sessionId", NEW_NUMBER)))) { + assertEquals(401, response.getStatus()); + } + } + + @Test + void invalidBasicAuthorization() { + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/number") + .request() + .header(HttpHeaders.AUTHORIZATION, "Basic but-invalid"); + try (Response response = request.put(Entity.json(invalidRequestJson()))) { + assertEquals(401, response.getStatus()); + } + } + + @Test + void invalidRequestBody() { + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/number") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); + try (Response response = request.put(Entity.json(invalidRequestJson()))) { + assertEquals(422, response.getStatus()); + } + } + + @Test + void rateLimitedNumber() throws Exception { + doThrow(new RateLimitExceededException(null, true)) + .when(registrationLimiter).validate(anyString()); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/number") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); + try (Response response = request.put(Entity.json(requestJson("sessionId", NEW_NUMBER)))) { + assertEquals(429, response.getStatus()); + } + } + + @Test + void registrationServiceTimeout() { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.failedFuture(new RuntimeException())); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/number") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); + try (Response response = request.put(Entity.json(requestJson("sessionId", NEW_NUMBER)))) { + assertEquals(HttpStatus.SC_SERVICE_UNAVAILABLE, response.getStatus()); + } + } + + @ParameterizedTest + @MethodSource + void registrationServiceSessionCheck(@Nullable final RegistrationServiceSession session, final int expectedStatus, + final String message) { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture(Optional.ofNullable(session))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/number") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); + try (Response response = request.put(Entity.json(requestJson("sessionId", NEW_NUMBER)))) { + assertEquals(expectedStatus, response.getStatus(), message); + } + } + + static Stream registrationServiceSessionCheck() { + return Stream.of( + Arguments.of(null, 401, "session not found"), + Arguments.of(new RegistrationServiceSession(new byte[16], "+18005551234", false, null, null, null, + SESSION_EXPIRATION_SECONDS), 400, + "session number mismatch"), + Arguments.of( + new RegistrationServiceSession(new byte[16], NEW_NUMBER, false, null, null, null, + SESSION_EXPIRATION_SECONDS), + 401, + "session not verified") + ); + } + + @ParameterizedTest + @EnumSource(RegistrationLockError.class) + void registrationLock(final RegistrationLockError error) throws Exception { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn( + CompletableFuture.completedFuture( + Optional.of(new RegistrationServiceSession(new byte[16], NEW_NUMBER, true, null, null, null, + SESSION_EXPIRATION_SECONDS)))); + + when(accountsManager.getByE164(any())).thenReturn(Optional.of(mock(Account.class))); + + final Exception e = switch (error) { + case MISMATCH -> new WebApplicationException(error.getExpectedStatus()); + case RATE_LIMITED -> new RateLimitExceededException(null, true); + }; + doThrow(e) + .when(registrationLockVerificationManager).verifyRegistrationLock(any(), any(), any(), any(), any()); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/number") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); + try (Response response = request.put(Entity.json(requestJson("sessionId", NEW_NUMBER)))) { + assertEquals(error.getExpectedStatus(), response.getStatus()); + } + } + + @Test + void recoveryPasswordManagerVerificationTrue() throws Exception { + when(registrationRecoveryPasswordsManager.verify(any(), any())) + .thenReturn(CompletableFuture.completedFuture(true)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/number") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); + final byte[] recoveryPassword = new byte[32]; + try (Response response = request.put(Entity.json(requestJsonRecoveryPassword(recoveryPassword, NEW_NUMBER)))) { + assertEquals(200, response.getStatus()); + + final AccountIdentityResponse accountIdentityResponse = response.readEntity(AccountIdentityResponse.class); + + verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(NEW_NUMBER), any(), any(), any(), + any(), any()); + + assertEquals(AuthHelper.VALID_UUID, accountIdentityResponse.uuid()); + assertEquals(NEW_NUMBER, accountIdentityResponse.number()); + assertNotEquals(AuthHelper.VALID_PNI, accountIdentityResponse.pni()); + } + } + + @Test + void recoveryPasswordManagerVerificationFalse() { + when(registrationRecoveryPasswordsManager.verify(any(), any())) + .thenReturn(CompletableFuture.completedFuture(false)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/number") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); + try (Response response = request.put(Entity.json(requestJsonRecoveryPassword(new byte[32], NEW_NUMBER)))) { + assertEquals(403, response.getStatus()); + } + } + + /** + * Valid request JSON with the given Recovery Password + */ + private static String requestJsonRecoveryPassword(final byte[] recoveryPassword, final String newNumber) { + return requestJson("", recoveryPassword, newNumber); + } + + /** + * Valid request JSON with the give session ID and recovery password + */ + private static String requestJson(final String sessionId, final byte[] recoveryPassword, final String newNumber) { + return String.format(""" + { + "sessionId": "%s", + "recoveryPassword": "%s", + "number": "%s", + "reglock": "1234", + "pniIdentityKey": "%s", + "deviceMessages": [], + "devicePniSignedPrekeys": {}, + "pniRegistrationIds": {} + } + """, encodeSessionId(sessionId), encodeRecoveryPassword(recoveryPassword), newNumber, Base64.getEncoder().encodeToString(IDENTITY_KEY.serialize())); + } + + /** + * Valid request JSON with the give session ID + */ + private static String requestJson(final String sessionId, final String newNumber) { + return requestJson(sessionId, new byte[0], newNumber); + } + + /** + * Request JSON in the shape of {@link org.whispersystems.textsecuregcm.entities.ChangeNumberRequest}, but that + * fails validation + */ + private static String invalidRequestJson() { + return """ + { + "sessionId": null + } + """; + } + + /** + * Request JSON that cannot be marshalled into + * {@link org.whispersystems.textsecuregcm.entities.ChangeNumberRequest} + */ + private static String unprocessableJson() { + return """ + { + "sessionId": [] + } + """; + } + + private static String encodeSessionId(final String sessionId) { + return Base64.getUrlEncoder().encodeToString(sessionId.getBytes(StandardCharsets.UTF_8)); + } + + private static String encodeRecoveryPassword(final byte[] recoveryPassword) { + return Base64.getEncoder().encodeToString(recoveryPassword); + } + } + + @Nested + class PhoneNumberIdentityKeyDistribution { + + @BeforeEach + void setUp() throws Exception { + when(changeNumberManager.updatePniKeys(any(), any(), any(), any(), any(), any())).thenAnswer( + (Answer) invocation -> { + final Account account = invocation.getArgument(0); + final IdentityKey pniIdentityKey = invocation.getArgument(1); + + final UUID uuid = account.getUuid(); + final UUID pni = account.getPhoneNumberIdentifier(); + final String number = account.getNumber(); + final List devices = account.getDevices(); + + final Account updatedAccount = mock(Account.class); + when(updatedAccount.getUuid()).thenReturn(uuid); + when(updatedAccount.getNumber()).thenReturn(number); + when(updatedAccount.getIdentityKey(IdentityType.PNI)).thenReturn(pniIdentityKey); + when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(pni); + when(updatedAccount.getDevices()).thenReturn(devices); + + for (long i = 1; i <= 3; i++) { + final Optional d = account.getDevice(i); + when(updatedAccount.getDevice(i)).thenReturn(d); + } + + return updatedAccount; + }); + } + + @Test + void pniKeyDistributionSuccess() throws Exception { + when(AuthHelper.VALID_ACCOUNT.isPniSupported()).thenReturn(true); + + final AccountIdentityResponse accountIdentityResponse = + resources.getJerseyTest() + .target("/v2/accounts/phone_number_identity_key_distribution") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(requestJson()), AccountIdentityResponse.class); + + verify(changeNumberManager).updatePniKeys(eq(AuthHelper.VALID_ACCOUNT), eq(IDENTITY_KEY), any(), any(), any(), any()); + + assertEquals(AuthHelper.VALID_UUID, accountIdentityResponse.uuid()); + assertEquals(AuthHelper.VALID_NUMBER, accountIdentityResponse.number()); + assertEquals(AuthHelper.VALID_PNI, accountIdentityResponse.pni()); + } + + @Test + void unprocessableRequestJson() { + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/phone_number_identity_key_distribution") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); + try (Response response = request.put(Entity.json(unprocessableJson()))) { + assertEquals(400, response.getStatus()); + } + } + + @Test + void missingBasicAuthorization() { + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/phone_number_identity_key_distribution") + .request(); + try (Response response = request.put(Entity.json(requestJson()))) { + assertEquals(401, response.getStatus()); + } + } + + @Test + void invalidBasicAuthorization() { + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/phone_number_identity_key_distribution") + .request() + .header(HttpHeaders.AUTHORIZATION, "Basic but-invalid"); + try (Response response = request.put(Entity.json(requestJson()))) { + assertEquals(401, response.getStatus()); + } + } + + @Test + void invalidRequestBody() { + final Invocation.Builder request = resources.getJerseyTest() + .target("/v2/accounts/phone_number_identity_key_distribution") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)); + try (Response response = request.put(Entity.json(invalidRequestJson()))) { + assertEquals(422, response.getStatus()); + } + } + + /** + * Valid request JSON for a {@link org.whispersystems.textsecuregcm.entities.PhoneNumberIdentityKeyDistributionRequest} + */ + private static String requestJson() { + return String.format(""" + { + "pniIdentityKey": "%s", + "deviceMessages": [], + "devicePniSignedPrekeys": {}, + "devicePniSignedPqPrekeys": {}, + "pniRegistrationIds": {} + } + """, Base64.getEncoder().encodeToString(IDENTITY_KEY.serialize())); + } + + /** + * Request JSON in the shape of {@link org.whispersystems.textsecuregcm.entities.PhoneNumberIdentityKeyDistributionRequest}, but that + * fails validation + */ + private static String invalidRequestJson() { + return """ + { + "pniIdentityKey": null, + "deviceMessages": [], + "devicePniSignedPrekeys": {}, + "pniRegistrationIds": {} + } + """; + } + + /** + * Request JSON that cannot be marshalled into + * {@link org.whispersystems.textsecuregcm.entities.PhoneNumberIdentityKeyDistributionRequest} + */ + private static String unprocessableJson() { + return """ + { + "pniIdentityKey": [] + } + """; + } + + } + + @Nested + class PhoneNumberDiscoverability { + + @BeforeEach + void setup() { + AccountsHelper.setupMockUpdate(accountsManager); + } + @Test + void testSetPhoneNumberDiscoverability() { + Response response = resources.getJerseyTest() + .target("/v2/accounts/phone_number_discoverability") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new PhoneNumberDiscoverabilityRequest(true))); + + assertThat(response.getStatus()).isEqualTo(204); + + ArgumentCaptor discoverabilityCapture = ArgumentCaptor.forClass(Boolean.class); + verify(AuthHelper.VALID_ACCOUNT).setDiscoverableByPhoneNumber(discoverabilityCapture.capture()); + assertThat(discoverabilityCapture.getValue()).isTrue(); + } + + @Test + void testSetNullPhoneNumberDiscoverability() { + Response response = resources.getJerseyTest() + .target("/v2/accounts/phone_number_discoverability") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json( + """ + { + "discoverableByPhoneNumber": null + } + """)); + + assertThat(response.getStatus()).isEqualTo(422); + verify(AuthHelper.VALID_ACCOUNT, never()).setDiscoverableByPhoneNumber(anyBoolean()); + } + + @ParameterizedTest + @MethodSource + void testGetAccountDataReport(final Account account, final String expectedTextAfterHeader) throws Exception { + when(AuthHelper.ACCOUNTS_MANAGER.getByAccountIdentifier(account.getUuid())).thenReturn(Optional.of(account)); + + final Response response = resources.getJerseyTest() + .target("/v2/accounts/data_report") + .request() + .header("Authorization", AuthHelper.getAuthHeader(account.getUuid(), "password")) + .get(); + + assertEquals(200, response.getStatus()); + + final String stringResponse = response.readEntity(String.class); + + final AccountDataReportResponse structuredResponse = SystemMapper.jsonMapper() + .readValue(stringResponse, AccountDataReportResponse.class); + + assertEquals(account.getNumber(), structuredResponse.data().account().phoneNumber()); + assertEquals(account.isDiscoverableByPhoneNumber(), + structuredResponse.data().account().findAccountByPhoneNumber()); + assertEquals(account.isUnrestrictedUnidentifiedAccess(), + structuredResponse.data().account().allowSealedSenderFromAnyone()); + + final Set deviceIds = account.getDevices().stream().map(Device::getId).collect(Collectors.toSet()); + + // all devices should be present + structuredResponse.data().devices().forEach(deviceDataReport -> { + assertTrue(deviceIds.remove(deviceDataReport.id())); + assertEquals(account.getDevice(deviceDataReport.id()).orElseThrow().getUserAgent(), + deviceDataReport.userAgent()); + }); + assertTrue(deviceIds.isEmpty()); + + final String actualText = (String) SystemMapper.jsonMapper().readValue(stringResponse, Map.class).get("text"); + final int headerEnd = actualText.indexOf("# Account"); + assertEquals(expectedTextAfterHeader, actualText.substring(headerEnd)); + + final String actualHeader = actualText.substring(0, headerEnd); + assertTrue(actualHeader.matches( + "Report ID: [a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}\nReport timestamp: \\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z\n\n")); + } + + static Stream testGetAccountDataReport() { + final String exampleNumber1 = toE164(PhoneNumberUtil.getInstance().getExampleNumber("ES")); + final String account2PhoneNumber = toE164(PhoneNumberUtil.getInstance().getExampleNumber("AU")); + final String account3PhoneNumber = toE164(PhoneNumberUtil.getInstance().getExampleNumber("IN")); + + final Instant account1Device1Created = Instant.ofEpochSecond(1669323142); // 2022-11-24T20:52:22Z + final Instant account1Device2Created = Instant.ofEpochSecond(1679155122); // 2023-03-18T15:58:42Z + final Instant account1Device1LastSeen = Instant.ofEpochMilli(Util.todayInMillis()); + final Instant account1Device2LastSeen = Instant.ofEpochSecond(1678838400); // 2023-03-15T00:00:00Z + + final Instant account2Device1Created = Instant.ofEpochSecond(1659123001); // 2022-07-29T19:30:01Z + final Instant account2Device1LastSeen = Instant.ofEpochMilli(Util.todayInMillis()); + final Instant badgeAExpiration = Instant.now().plus(Duration.ofDays(21)).truncatedTo(ChronoUnit.SECONDS); + + final Instant account3Device1Created = Instant.ofEpochSecond(1639923487); // 2021-12-19T14:18:07Z + final Instant account3Device1LastSeen = Instant.ofEpochMilli(Util.todayInMillis()); + final Instant badgeBExpiration = Instant.now().plus(Duration.ofDays(21)).truncatedTo(ChronoUnit.SECONDS); + final Instant badgeCExpiration = Instant.now().plus(Duration.ofDays(24)).truncatedTo(ChronoUnit.SECONDS); + + return Stream.of( + Arguments.of( + buildTestAccountForDataReport(UUID.randomUUID(), exampleNumber1, + true, true, + Collections.emptyList(), + List.of(new DeviceData(1, account1Device1LastSeen, account1Device1Created, null), + new DeviceData(2, account1Device2LastSeen, account1Device2Created, "OWP"))), + String.format(""" + # Account + Phone number: %s + Allow sealed sender from anyone: true + Find account by phone number: true + Badges: None + + # Devices + - ID: 1 + Created: 2022-11-24T20:52:22Z + Last seen: %s + User-agent: null + - ID: 2 + Created: 2023-03-18T15:58:42Z + Last seen: 2023-03-15T00:00:00Z + User-agent: OWP + """, + exampleNumber1, + account1Device1LastSeen) + ), + Arguments.of( + buildTestAccountForDataReport(UUID.randomUUID(), account2PhoneNumber, + false, true, + List.of(new AccountBadge("badge_a", badgeAExpiration, true)), + List.of(new DeviceData(1, account2Device1LastSeen, account2Device1Created, "OWI"))), + String.format(""" + # Account + Phone number: %s + Allow sealed sender from anyone: false + Find account by phone number: true + Badges: + - ID: badge_a + Expiration: %s + Visible: true + + # Devices + - ID: 1 + Created: 2022-07-29T19:30:01Z + Last seen: %s + User-agent: OWI + """, account2PhoneNumber, + badgeAExpiration, + account2Device1LastSeen) + ), + Arguments.of( + buildTestAccountForDataReport(UUID.randomUUID(), account3PhoneNumber, + true, false, + List.of( + new AccountBadge("badge_b", badgeBExpiration, true), + new AccountBadge("badge_c", badgeCExpiration, false)), + List.of(new DeviceData(1, account3Device1LastSeen, account3Device1Created, "OWA"))), + String.format(""" + # Account + Phone number: %s + Allow sealed sender from anyone: true + Find account by phone number: false + Badges: + - ID: badge_b + Expiration: %s + Visible: true + - ID: badge_c + Expiration: %s + Visible: false + + # Devices + - ID: 1 + Created: 2021-12-19T14:18:07Z + Last seen: %s + User-agent: OWA + """, account3PhoneNumber, + badgeBExpiration, + badgeCExpiration, + account3Device1LastSeen) + ) + ); + } + + /** + * Creates an {@link Account} with data sufficient for + * {@link AccountControllerV2#getAccountDataReport(AuthenticatedAccount)}. + *

    + * Note: All devices will have a {@link SaltedTokenHash} for "password" + */ + static Account buildTestAccountForDataReport(final UUID aci, final String number, + final boolean unrestrictedUnidentifiedAccess, final boolean discoverableByPhoneNumber, + List badges, List devices) { + + final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair(); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + + final Account account = new Account(); + account.setUuid(aci); + account.setNumber(number, UUID.randomUUID()); + account.setUnrestrictedUnidentifiedAccess(unrestrictedUnidentifiedAccess); + account.setDiscoverableByPhoneNumber(discoverableByPhoneNumber); + account.setBadges(Clock.systemUTC(), new ArrayList<>(badges)); + account.setIdentityKey(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + account.setPhoneNumberIdentityKey(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + + assert !devices.isEmpty(); + + final SaltedTokenHash passwordTokenHash = SaltedTokenHash.generateFor("password"); + + devices.forEach(deviceData -> { + final Device device = new Device(); + device.setId(deviceData.id); + device.setAuthTokenHash(passwordTokenHash); + device.setFetchesMessages(true); + device.setSignedPreKey(KeysHelper.signedECPreKey(1, aciIdentityKeyPair)); + device.setPhoneNumberIdentitySignedPreKey(KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); + device.setLastSeen(deviceData.lastSeen().toEpochMilli()); + device.setCreated(deviceData.created().toEpochMilli()); + device.setUserAgent(deviceData.userAgent()); + account.addDevice(device); + }); + + return account; + } + + private record DeviceData(long id, Instant lastSeen, Instant created, @Nullable String userAgent) { + + } + + private static String toE164(Phonenumber.PhoneNumber phoneNumber) { + return PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArtControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArtControllerTest.java new file mode 100644 index 000000000..f3cd583c2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArtControllerTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes; + +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.time.Duration; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.MockUtils; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +@ExtendWith(DropwizardExtensionsSupport.class) +class ArtControllerTest { + private static final ArtServiceConfiguration ART_SERVICE_CONFIGURATION = new ArtServiceConfiguration( + randomSecretBytes(32), randomSecretBytes(32), Duration.ofDays(1)); + private static final ExternalServiceCredentialsGenerator artCredentialsGenerator = ArtController.credentialsGenerator(ART_SERVICE_CONFIGURATION); + private static final RateLimiters rateLimiters = mock(RateLimiters.class); + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new ArtController(rateLimiters, artCredentialsGenerator)) + .build(); + + @Test + void testGetAuthToken() { + MockUtils.updateRateLimiterResponseToAllow(rateLimiters, RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS, AuthHelper.VALID_UUID); + final ExternalServiceCredentials token = + resources.getJerseyTest() + .target("/v1/art/auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(ExternalServiceCredentials.class); + + assertThat(token.password()).isNotEmpty(); + assertThat(token.username()).isNotEmpty(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerTest.java new file mode 100644 index 000000000..3092d86af --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerTest.java @@ -0,0 +1,252 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; +import javax.ws.rs.core.Response; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.Condition; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.whispersystems.textsecuregcm.attachments.GcsAttachmentGenerator; +import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator; +import org.whispersystems.textsecuregcm.attachments.TusConfiguration; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; +import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV2; +import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV3; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.MockUtils; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +@ExtendWith(DropwizardExtensionsSupport.class) +class AttachmentControllerTest { + + private static final RateLimiter RATE_LIMITER = mock(RateLimiter.class); + + private static final RateLimiters RATE_LIMITERS = MockUtils.buildMock(RateLimiters.class, rateLimiters -> + when(rateLimiters.getAttachmentLimiter()).thenReturn(RATE_LIMITER)); + + + private static String CDN3_ENABLED_CREDS = AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD); + private static String CDN3_DISABLED_CREDS = AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO); + private static final ExperimentEnrollmentManager EXPERIMENT_MANAGER = MockUtils.buildMock(ExperimentEnrollmentManager.class, mgr -> { + when(mgr.isEnrolled(AuthHelper.VALID_UUID, AttachmentControllerV4.CDN3_EXPERIMENT_NAME)).thenReturn(true); + when(mgr.isEnrolled(AuthHelper.VALID_UUID_TWO, AttachmentControllerV4.CDN3_EXPERIMENT_NAME)).thenReturn(false); + }); + + private static final byte[] TUS_SECRET = getRandomBytes(32); + private static final String TUS_URL = "https://example.com/uploads"; + + public static final String RSA_PRIVATE_KEY_PEM; + + static { + try { + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(1024); + final KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + RSA_PRIVATE_KEY_PEM = "-----BEGIN PRIVATE KEY-----\n" + + Base64.getMimeEncoder().encodeToString(keyPair.getPrivate().getEncoded()) + "\n" + + "-----END PRIVATE KEY-----"; + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + + private static final ResourceExtension resources; + + static { + try { + final GcsAttachmentGenerator gcsAttachmentGenerator = new GcsAttachmentGenerator("some-cdn.signal.org", + "signal@example.com", 1000, "/attach-here", RSA_PRIVATE_KEY_PEM); + resources = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new AttachmentControllerV2(RATE_LIMITERS, "accessKey", "accessSecret", "us-east-1", "attachmentv2-bucket")) + .addResource(new AttachmentControllerV3(RATE_LIMITERS, gcsAttachmentGenerator)) + .addProvider(new AttachmentControllerV4(RATE_LIMITERS, + gcsAttachmentGenerator, + new TusAttachmentGenerator(new TusConfiguration( new SecretBytes(TUS_SECRET), TUS_URL)), + EXPERIMENT_MANAGER)) + .build(); + } catch (IOException | InvalidKeyException | InvalidKeySpecException e) { + throw new AssertionError(e); + } + } + + @Test + void testV4TusForm() { + AttachmentDescriptorV3 descriptor = resources.getJerseyTest() + .target("/v4/attachments/form/upload") + .request() + .header("Authorization", CDN3_ENABLED_CREDS) + .get(AttachmentDescriptorV3.class); + assertThat(descriptor.cdn()).isEqualTo(3); + assertThat(descriptor.key()).isNotBlank(); + assertThat(descriptor.signedUploadLocation()).isEqualTo(TUS_URL + "/" + "attachments"); + final String filenameb64 = descriptor.headers().get("Upload-Metadata").split(" ")[1]; + final String filename = new String(Base64.getDecoder().decode(filenameb64)); + assertThat(descriptor.key()).isEqualTo(filename); + } + + @Test + void testV4GcsForm() { + AttachmentDescriptorV3 descriptor = resources.getJerseyTest() + .target("/v4/attachments/form/upload") + .request() + .header("Authorization", CDN3_DISABLED_CREDS) + .get(AttachmentDescriptorV3.class); + assertThat(descriptor.cdn()).isEqualTo(2); + assertValidCdn2Response(descriptor); + } + + @Test + void testV3Form() { + AttachmentDescriptorV3 descriptor = resources.getJerseyTest() + .target("/v3/attachments/form/upload") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(AttachmentDescriptorV3.class); + assertValidCdn2Response(descriptor); + } + + private static void assertValidCdn2Response(final AttachmentDescriptorV3 descriptor) { + assertThat(descriptor.key()).isNotBlank(); + assertThat(descriptor.cdn()).isEqualTo(2); + assertThat(descriptor.headers()).hasSize(3); + assertThat(descriptor.headers()).extractingByKey("host").isEqualTo("some-cdn.signal.org"); + assertThat(descriptor.headers()).extractingByKey("x-goog-resumable").isEqualTo("start"); + assertThat(descriptor.headers()).extractingByKey("x-goog-content-length-range").isEqualTo("1,1000"); + assertThat(descriptor.signedUploadLocation()).isNotEmpty(); + assertThat(descriptor.signedUploadLocation()).contains("X-Goog-Signature"); + assertThat(descriptor.signedUploadLocation()).is(new Condition<>(x -> { + try { + new URL(x); + } catch (MalformedURLException e) { + return false; + } + return true; + }, "convertible to a URL", (Object[]) null)); + + final URL signedUploadLocation; + try { + signedUploadLocation = new URL(descriptor.signedUploadLocation()); + } catch (MalformedURLException e) { + throw new AssertionError(e); + } + assertThat(signedUploadLocation.getHost()).isEqualTo("some-cdn.signal.org"); + assertThat(signedUploadLocation.getPath()).startsWith("/attach-here/"); + final Map queryParamMap = new HashMap<>(); + final String[] queryTerms = signedUploadLocation.getQuery().split("&"); + for (final String queryTerm : queryTerms) { + final String[] keyValueArray = queryTerm.split("=", 2); + queryParamMap.put( + URLDecoder.decode(keyValueArray[0], StandardCharsets.UTF_8), + URLDecoder.decode(keyValueArray[1], StandardCharsets.UTF_8)); + } + + assertThat(queryParamMap).extractingByKey("X-Goog-Algorithm").isEqualTo("GOOG4-RSA-SHA256"); + assertThat(queryParamMap).extractingByKey("X-Goog-Expires").isEqualTo("90000"); + assertThat(queryParamMap).extractingByKey("X-Goog-SignedHeaders").isEqualTo("host;x-goog-content-length-range;x-goog-resumable"); + assertThat(queryParamMap).extractingByKey("X-Goog-Date", Assertions.as(InstanceOfAssertFactories.STRING)).isNotEmpty(); + + final String credential = queryParamMap.get("X-Goog-Credential"); + String[] credentialParts = credential.split("/"); + assertThat(credentialParts).hasSize(5); + assertThat(credentialParts[0]).isEqualTo("signal@example.com"); + assertThat(credentialParts[2]).isEqualTo("auto"); + assertThat(credentialParts[3]).isEqualTo("storage"); + assertThat(credentialParts[4]).isEqualTo("goog4_request"); + } + + @Test + void testV3FormDisabled() { + Response response = resources.getJerseyTest() + .target("/v3/attachments/form/upload") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + } + + @Test + void testV2Form() throws IOException { + AttachmentDescriptorV2 descriptor = resources.getJerseyTest() + .target("/v2/attachments/form/upload") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(AttachmentDescriptorV2.class); + + assertThat(descriptor.key()).isEqualTo(descriptor.attachmentIdString()); + assertThat(descriptor.acl()).isEqualTo("private"); + assertThat(descriptor.algorithm()).isEqualTo("AWS4-HMAC-SHA256"); + assertThat(descriptor.attachmentId()).isGreaterThan(0); + assertThat(String.valueOf(descriptor.attachmentId())).isEqualTo(descriptor.attachmentIdString()); + + String[] credentialParts = descriptor.credential().split("/"); + + assertThat(credentialParts[0]).isEqualTo("accessKey"); + assertThat(credentialParts[2]).isEqualTo("us-east-1"); + assertThat(credentialParts[3]).isEqualTo("s3"); + assertThat(credentialParts[4]).isEqualTo("aws4_request"); + + assertThat(descriptor.date()).isNotBlank(); + assertThat(descriptor.policy()).isNotBlank(); + assertThat(descriptor.signature()).isNotBlank(); + + assertThat(new String(Base64.getDecoder().decode(descriptor.policy()))).contains("[\"content-length-range\", 1, 104857600]"); + } + + @Test + void testV2FormDisabled() { + Response response = resources.getJerseyTest() + .target("/v2/attachments/form/upload") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + } + + private static byte[] getRandomBytes(int length) { + byte[] result = new byte[length]; + ThreadLocalRandom.current().nextBytes(result); + return result; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallLinkControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallLinkControllerTest.java new file mode 100644 index 000000000..acf6cee60 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallLinkControllerTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.signal.libsignal.protocol.util.Hex; +import org.signal.libsignal.zkgroup.GenericServerSecretParams; +import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialRequestContext; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.entities.GetCreateCallLinkCredentialsRequest; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +@ExtendWith(DropwizardExtensionsSupport.class) +public class CallLinkControllerTest { + private static final GenericServerSecretParams genericServerSecretParams = GenericServerSecretParams.generate(); + private static final RateLimiters rateLimiters = mock(RateLimiters.class); + private static final RateLimiter createCallLinkLimiter = mock(RateLimiter.class); + private static final byte[] roomId = Hex.fromStringCondensedAssert("c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7"); + private static final CreateCallLinkCredentialRequestContext createCallLinkRequestContext = CreateCallLinkCredentialRequestContext.forRoom(roomId); + private static final byte[] createCallLinkRequestSerialized = createCallLinkRequestContext.getRequest().serialize(); + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .addProvider(new RateLimitExceededExceptionMapper()) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new CallLinkController(rateLimiters, genericServerSecretParams)) + .build(); + + @BeforeEach + void setup() { + when(rateLimiters.getCreateCallLinkLimiter()).thenReturn(createCallLinkLimiter); + } + + @Test + void testGetCreateAuth() { + try (Response response = resources.getJerseyTest() + .target("/v1/call-link/create-auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(Entity.json(new GetCreateCallLinkCredentialsRequest(createCallLinkRequestSerialized)))) { + assertThat(response.getStatus()).isEqualTo(200); + } + } + + @Test + void testGetCreateAuthInvalidInput() { + try (Response response = resources.getJerseyTest() + .target("/v1/call-link/create-auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(Entity.json(new GetCreateCallLinkCredentialsRequest(new byte[10])))) { + assertThat(response.getStatus()).isEqualTo(400); + } + } + + @Test + void testGetCreateAuthInvalidAuth() { + try (Response response = resources.getJerseyTest() + .target("/v1/call-link/create-auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.INVALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(Entity.json(new GetCreateCallLinkCredentialsRequest(createCallLinkRequestSerialized)))) { + assertThat(response.getStatus()).isEqualTo(401); + } + } + + @Test + void testGetCreateAuthInvalidRequest() { + try (Response response = resources.getJerseyTest() + .target("/v1/call-link/create-auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(Entity.json(""))) { + + assertThat(response.getStatus()).isEqualTo(422); + } + } + + @Test + void testGetCreateAuthInvalidInputEmptyRequestBody() { + try (Response response = resources.getJerseyTest() + .target("/v1/call-link/create-auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(Entity.json("{}"))) { + assertThat(response.getStatus()).isEqualTo(422); + } + } + + @Test + void testGetCreateAuthInvalidInputEmptyField() { + try (Response response = resources.getJerseyTest() + .target("/v1/call-link/create-auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(Entity.json("{\"createCallLinkCredentialRequest\": \"\"}"))) { + assertThat(response.getStatus()).isEqualTo(422); + } + } + + @Test + void testGetCreateAuthRatelimited() throws RateLimitExceededException{ + doThrow(new RateLimitExceededException(null, false)) + .when(createCallLinkLimiter).validate(AuthHelper.VALID_UUID); + + try (Response response = resources.getJerseyTest() + .target("/v1/call-link/create-auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(Entity.json(new GetCreateCallLinkCredentialsRequest(createCallLinkRequestSerialized)))) { + + assertThat(response.getStatus()).isEqualTo(429); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CertificateControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CertificateControllerTest.java new file mode 100644 index 000000000..fc1b8b0aa --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CertificateControllerTest.java @@ -0,0 +1,369 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.io.IOException; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.stream.Stream; +import javax.ws.rs.core.Response; +import org.apache.commons.lang3.StringUtils; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.ServiceId; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.zkgroup.GenericServerSecretParams; +import org.signal.libsignal.zkgroup.ServerSecretParams; +import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse; +import org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations; +import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations; +import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialResponse; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.CertificateGenerator; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.OptionalAccess; +import org.whispersystems.textsecuregcm.entities.DeliveryCertificate; +import org.whispersystems.textsecuregcm.entities.GroupCredentials; +import org.whispersystems.textsecuregcm.entities.MessageProtos.SenderCertificate; +import org.whispersystems.textsecuregcm.entities.MessageProtos.ServerCertificate; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +@ExtendWith(DropwizardExtensionsSupport.class) +class CertificateControllerTest { + + private static final String caPublicKey = "BWh+UOhT1hD8bkb+MFRvb6tVqhoG8YYGCzOd7mgjo8cV"; + + @SuppressWarnings("unused") + private static final String caPrivateKey = "EO3Mnf0kfVlVnwSaqPoQnAxhnnGL1JTdXqktCKEe9Eo="; + + private static final String signingCertificate = "CiUIDBIhBbTz4h1My+tt+vw+TVscgUe/DeHS0W02tPWAWbTO2xc3EkD+go4bJnU0AcnFfbOLKoiBfCzouZtDYMOVi69rE7r4U9cXREEqOkUmU2WJBjykAxWPCcSTmVTYHDw7hkSp/puG"; + private static final String signingKey = "ABOxG29xrfq4E7IrW11Eg7+HBbtba9iiS0500YoBjn4="; + + private static final ServerSecretParams serverSecretParams = ServerSecretParams.generate(); + + private static final GenericServerSecretParams genericServerSecretParams = GenericServerSecretParams.generate(); + private static final CertificateGenerator certificateGenerator; + private static final ServerZkAuthOperations serverZkAuthOperations; + private static final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + + static { + try { + certificateGenerator = new CertificateGenerator(Base64.getDecoder().decode(signingCertificate), + Curve.decodePrivatePoint(Base64.getDecoder().decode(signingKey)), 1); + serverZkAuthOperations = new ServerZkAuthOperations(serverSecretParams); + } catch (IOException | InvalidKeyException e) { + throw new AssertionError(e); + } + } + + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new CertificateController(certificateGenerator, serverZkAuthOperations, genericServerSecretParams, clock)) + .build(); + + @Test + void testValidCertificate() throws Exception { + DeliveryCertificate certificateObject = resources.getJerseyTest() + .target("/v1/certificate/delivery") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(DeliveryCertificate.class); + + SenderCertificate certificateHolder = SenderCertificate.parseFrom(certificateObject.getCertificate()); + SenderCertificate.Certificate certificate = SenderCertificate.Certificate.parseFrom( + certificateHolder.getCertificate()); + + ServerCertificate serverCertificateHolder = certificate.getSigner(); + ServerCertificate.Certificate serverCertificate = ServerCertificate.Certificate.parseFrom( + serverCertificateHolder.getCertificate()); + + assertTrue(Curve.verifySignature(Curve.decodePoint(serverCertificate.getKey().toByteArray(), 0), + certificateHolder.getCertificate().toByteArray(), certificateHolder.getSignature().toByteArray())); + assertTrue(Curve.verifySignature(Curve.decodePoint(Base64.getDecoder().decode(caPublicKey), 0), + serverCertificateHolder.getCertificate().toByteArray(), serverCertificateHolder.getSignature().toByteArray())); + + assertEquals(certificate.getSender(), AuthHelper.VALID_NUMBER); + assertEquals(certificate.getSenderDevice(), 1L); + assertTrue(certificate.hasSenderUuid()); + assertEquals(AuthHelper.VALID_UUID.toString(), certificate.getSenderUuid()); + assertArrayEquals(certificate.getIdentityKey().toByteArray(), AuthHelper.VALID_IDENTITY.serialize()); + } + + @Test + void testValidCertificateWithUuid() throws Exception { + DeliveryCertificate certificateObject = resources.getJerseyTest() + .target("/v1/certificate/delivery") + .queryParam("includeUuid", "true") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(DeliveryCertificate.class); + + SenderCertificate certificateHolder = SenderCertificate.parseFrom(certificateObject.getCertificate()); + SenderCertificate.Certificate certificate = SenderCertificate.Certificate.parseFrom( + certificateHolder.getCertificate()); + + ServerCertificate serverCertificateHolder = certificate.getSigner(); + ServerCertificate.Certificate serverCertificate = ServerCertificate.Certificate.parseFrom( + serverCertificateHolder.getCertificate()); + + assertTrue(Curve.verifySignature(Curve.decodePoint(serverCertificate.getKey().toByteArray(), 0), + certificateHolder.getCertificate().toByteArray(), certificateHolder.getSignature().toByteArray())); + assertTrue(Curve.verifySignature(Curve.decodePoint(Base64.getDecoder().decode(caPublicKey), 0), + serverCertificateHolder.getCertificate().toByteArray(), serverCertificateHolder.getSignature().toByteArray())); + + assertEquals(certificate.getSender(), AuthHelper.VALID_NUMBER); + assertEquals(certificate.getSenderDevice(), 1L); + assertEquals(certificate.getSenderUuid(), AuthHelper.VALID_UUID.toString()); + assertArrayEquals(certificate.getIdentityKey().toByteArray(), AuthHelper.VALID_IDENTITY.serialize()); + } + + @Test + void testValidCertificateWithUuidNoE164() throws Exception { + DeliveryCertificate certificateObject = resources.getJerseyTest() + .target("/v1/certificate/delivery") + .queryParam("includeUuid", "true") + .queryParam("includeE164", "false") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(DeliveryCertificate.class); + + SenderCertificate certificateHolder = SenderCertificate.parseFrom(certificateObject.getCertificate()); + SenderCertificate.Certificate certificate = SenderCertificate.Certificate.parseFrom( + certificateHolder.getCertificate()); + + ServerCertificate serverCertificateHolder = certificate.getSigner(); + ServerCertificate.Certificate serverCertificate = ServerCertificate.Certificate.parseFrom( + serverCertificateHolder.getCertificate()); + + assertTrue(Curve.verifySignature(Curve.decodePoint(serverCertificate.getKey().toByteArray(), 0), + certificateHolder.getCertificate().toByteArray(), certificateHolder.getSignature().toByteArray())); + assertTrue(Curve.verifySignature(Curve.decodePoint(Base64.getDecoder().decode(caPublicKey), 0), + serverCertificateHolder.getCertificate().toByteArray(), serverCertificateHolder.getSignature().toByteArray())); + + assertTrue(StringUtils.isBlank(certificate.getSender())); + assertEquals(certificate.getSenderDevice(), 1L); + assertEquals(certificate.getSenderUuid(), AuthHelper.VALID_UUID.toString()); + assertArrayEquals(certificate.getIdentityKey().toByteArray(), AuthHelper.VALID_IDENTITY.serialize()); + } + + @Test + void testBadAuthentication() { + Response response = resources.getJerseyTest() + .target("/v1/certificate/delivery") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD)) + .get(); + + assertEquals(response.getStatus(), 401); + } + + + @Test + void testNoAuthentication() { + Response response = resources.getJerseyTest() + .target("/v1/certificate/delivery") + .request() + .get(); + + assertEquals(response.getStatus(), 401); + } + + + @Test + void testUnidentifiedAuthentication() { + Response response = resources.getJerseyTest() + .target("/v1/certificate/delivery") + .request() + .header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("1234".getBytes())) + .get(); + + assertEquals(response.getStatus(), 401); + } + + @Test + void testDisabledAuthentication() { + Response response = resources.getJerseyTest() + .target("/v1/certificate/delivery") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .get(); + + assertEquals(response.getStatus(), 401); + } + + @Test + void testGetSingleGroupCredentialWithPniAsAci() { + final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS); + + final GroupCredentials credentials = resources.getJerseyTest() + .target("/v1/certificate/auth/group") + .queryParam("redemptionStartSeconds", startOfDay.getEpochSecond()) + .queryParam("redemptionEndSeconds", startOfDay.getEpochSecond()) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(GroupCredentials.class); + + assertEquals(1, credentials.credentials().size()); + assertEquals(1, credentials.callLinkAuthCredentials().size()); + + assertEquals(AuthHelper.VALID_PNI, credentials.pni()); + assertEquals(startOfDay.getEpochSecond(), credentials.credentials().get(0).redemptionTime()); + assertEquals(startOfDay.getEpochSecond(), credentials.callLinkAuthCredentials().get(0).redemptionTime()); + + final ClientZkAuthOperations clientZkAuthOperations = + new ClientZkAuthOperations(serverSecretParams.getPublicParams()); + + assertDoesNotThrow(() -> { + clientZkAuthOperations.receiveAuthCredentialWithPniAsAci( + new ServiceId.Aci(AuthHelper.VALID_UUID), + new ServiceId.Pni(AuthHelper.VALID_PNI), + (int) startOfDay.getEpochSecond(), + new AuthCredentialWithPniResponse(credentials.credentials().get(0).credential())); + }); + + assertDoesNotThrow(() -> { + new CallLinkAuthCredentialResponse(credentials.callLinkAuthCredentials().get(0).credential()) + .receive(new ServiceId.Aci(AuthHelper.VALID_UUID), startOfDay, genericServerSecretParams.getPublicParams()); + }); + } + + @Test + void testGetSingleGroupCredentialWithPniAsServiceId() { + final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS); + + final GroupCredentials credentials = resources.getJerseyTest() + .target("/v1/certificate/auth/group") + .queryParam("redemptionStartSeconds", startOfDay.getEpochSecond()) + .queryParam("redemptionEndSeconds", startOfDay.getEpochSecond()) + .queryParam("pniAsServiceId", true) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(GroupCredentials.class); + + assertEquals(1, credentials.credentials().size()); + assertEquals(1, credentials.callLinkAuthCredentials().size()); + + assertEquals(AuthHelper.VALID_PNI, credentials.pni()); + assertEquals(startOfDay.getEpochSecond(), credentials.credentials().get(0).redemptionTime()); + assertEquals(startOfDay.getEpochSecond(), credentials.callLinkAuthCredentials().get(0).redemptionTime()); + + final ClientZkAuthOperations clientZkAuthOperations = + new ClientZkAuthOperations(serverSecretParams.getPublicParams()); + + assertDoesNotThrow(() -> { + clientZkAuthOperations.receiveAuthCredentialWithPniAsServiceId( + new ServiceId.Aci(AuthHelper.VALID_UUID), + new ServiceId.Pni(AuthHelper.VALID_PNI), + (int) startOfDay.getEpochSecond(), + new AuthCredentialWithPniResponse(credentials.credentials().get(0).credential())); + }); + + assertDoesNotThrow(() -> { + new CallLinkAuthCredentialResponse(credentials.callLinkAuthCredentials().get(0).credential()) + .receive(new ServiceId.Aci(AuthHelper.VALID_UUID), startOfDay, genericServerSecretParams.getPublicParams()); + }); + } + + @Test + void testGetWeekLongGroupCredentials() { + final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS); + + final GroupCredentials credentials = resources.getJerseyTest() + .target("/v1/certificate/auth/group") + .queryParam("redemptionStartSeconds", startOfDay.getEpochSecond()) + .queryParam("redemptionEndSeconds", startOfDay.plus(Duration.ofDays(7)).getEpochSecond()) + .queryParam("pniAsServiceId", true) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(GroupCredentials.class); + + assertEquals(AuthHelper.VALID_PNI, credentials.pni()); + assertEquals(8, credentials.credentials().size()); + assertEquals(8, credentials.callLinkAuthCredentials().size()); + + final ClientZkAuthOperations clientZkAuthOperations = + new ClientZkAuthOperations(serverSecretParams.getPublicParams()); + + for (int i = 0; i < 8; i++) { + final Instant redemptionTime = startOfDay.plus(Duration.ofDays(i)); + assertEquals(redemptionTime.getEpochSecond(), credentials.credentials().get(i).redemptionTime()); + assertEquals(redemptionTime.getEpochSecond(), credentials.callLinkAuthCredentials().get(i).redemptionTime()); + + final int index = i; + + assertDoesNotThrow(() -> { + clientZkAuthOperations.receiveAuthCredentialWithPniAsServiceId( + new ServiceId.Aci(AuthHelper.VALID_UUID), + new ServiceId.Pni(AuthHelper.VALID_PNI), + redemptionTime.getEpochSecond(), + new AuthCredentialWithPniResponse(credentials.credentials().get(index).credential())); + }); + + assertDoesNotThrow(() -> { + new CallLinkAuthCredentialResponse(credentials.callLinkAuthCredentials().get(index).credential()) + .receive(new ServiceId.Aci(AuthHelper.VALID_UUID), redemptionTime, genericServerSecretParams.getPublicParams()); + }); + } + } + + @ParameterizedTest + @MethodSource + void testBadRedemptionTimes(final Instant redemptionStart, final Instant redemptionEnd) { + final Response response = resources.getJerseyTest() + .target("/v1/certificate/auth/group") + .queryParam("redemptionStartSeconds", redemptionStart.getEpochSecond()) + .queryParam("redemptionEndSeconds", redemptionEnd.getEpochSecond()) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(); + + assertEquals(400, response.getStatus()); + } + + private static Stream testBadRedemptionTimes() { + return Stream.of( + // Start is after end + Arguments.of(clock.instant().plus(Duration.ofDays(1)), clock.instant()), + + // Start is in the past + Arguments.of(clock.instant().minus(Duration.ofDays(1)), clock.instant()), + + // End is too far in the future + Arguments.of(clock.instant(), + clock.instant().plus(CertificateController.MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1))), + + // Start is not at a day boundary + Arguments.of(clock.instant().plusSeconds(17), clock.instant().plus(Duration.ofDays(1))), + + // End is not at a day boundary + Arguments.of(clock.instant(), clock.instant().plusSeconds(17)) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ChallengeControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ChallengeControllerTest.java new file mode 100644 index 000000000..c91b9dfc3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ChallengeControllerTest.java @@ -0,0 +1,248 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.net.HttpHeaders; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.io.IOException; +import java.time.Duration; +import java.util.Set; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager; +import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +@ExtendWith(DropwizardExtensionsSupport.class) +class ChallengeControllerTest { + + private static final RateLimitChallengeManager rateLimitChallengeManager = mock(RateLimitChallengeManager.class); + + private static final ChallengeController challengeController = new ChallengeController(rateLimitChallengeManager); + + private static final ResourceExtension EXTENSION = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + Set.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new RateLimitExceededExceptionMapper()) + .addResource(challengeController) + .build(); + + @AfterEach + void teardown() { + reset(rateLimitChallengeManager); + } + + @Test + void testHandlePushChallenge() throws RateLimitExceededException { + final String pushChallengeJson = """ + { + "type": "rateLimitPushChallenge", + "challenge": "Hello I am a push challenge token" + } + """; + + final Response response = EXTENSION.target("/v1/challenge") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(pushChallengeJson)); + + assertEquals(200, response.getStatus()); + verify(rateLimitChallengeManager).answerPushChallenge(AuthHelper.VALID_ACCOUNT, "Hello I am a push challenge token"); + } + + @Test + void testHandlePushChallengeRateLimited() throws RateLimitExceededException { + final String pushChallengeJson = """ + { + "type": "rateLimitPushChallenge", + "challenge": "Hello I am a push challenge token" + } + """; + + final Duration retryAfter = Duration.ofMinutes(17); + doThrow(new RateLimitExceededException(retryAfter, true)).when(rateLimitChallengeManager) + .answerPushChallenge(any(), any()); + + final Response response = EXTENSION.target("/v1/challenge") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(pushChallengeJson)); + + assertEquals(413, response.getStatus()); + assertEquals(String.valueOf(retryAfter.toSeconds()), response.getHeaderString("Retry-After")); + } + + @Test + void testHandleRecaptcha() throws RateLimitExceededException, IOException { + final String recaptchaChallengeJson = """ + { + "type": "recaptcha", + "token": "A server-generated token", + "captcha": "The value of the solved captcha token" + } + """; + + when(rateLimitChallengeManager.answerRecaptchaChallenge(any(), any(), any(), any())) + .thenReturn(true); + + final Response response = EXTENSION.target("/v1/challenge") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "10.0.0.1") + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(recaptchaChallengeJson)); + + assertEquals(200, response.getStatus()); + + verify(rateLimitChallengeManager).answerRecaptchaChallenge(eq(AuthHelper.VALID_ACCOUNT), eq("The value of the solved captcha token"), eq("10.0.0.1"), anyString()); + } + + @Test + void testHandleInvalidCaptcha() throws RateLimitExceededException, IOException { + final String recaptchaChallengeJson = """ + { + "type": "recaptcha", + "token": "A server-generated token", + "captcha": "The value of the solved captcha token" + } + """; + + when(rateLimitChallengeManager.answerRecaptchaChallenge(eq(AuthHelper.VALID_ACCOUNT), eq("The value of the solved captcha token"), eq("10.0.0.1"), anyString())) + .thenReturn(false); + + final Response response = EXTENSION.target("/v1/challenge") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "10.0.0.1") + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(recaptchaChallengeJson)); + + assertEquals(428, response.getStatus()); + } + + @Test + void testHandleRecaptchaRateLimited() throws RateLimitExceededException, IOException { + final String recaptchaChallengeJson = """ + { + "type": "recaptcha", + "token": "A server-generated token", + "captcha": "The value of the solved captcha token" + } + """; + + final Duration retryAfter = Duration.ofMinutes(17); + doThrow(new RateLimitExceededException(retryAfter, true)).when(rateLimitChallengeManager) + .answerRecaptchaChallenge(any(), any(), any(), any()); + + final Response response = EXTENSION.target("/v1/challenge") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "10.0.0.1") + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(recaptchaChallengeJson)); + + assertEquals(413, response.getStatus()); + assertEquals(String.valueOf(retryAfter.toSeconds()), response.getHeaderString("Retry-After")); + } + + @Test + void testHandleRecaptchaNoForwardedFor() { + final String recaptchaChallengeJson = """ + { + "type": "recaptcha", + "token": "A server-generated token", + "captcha": "The value of the solved captcha token" + } + """; + + final Response response = EXTENSION.target("/v1/challenge") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(recaptchaChallengeJson)); + + assertEquals(400, response.getStatus()); + verifyNoInteractions(rateLimitChallengeManager); + } + + @Test + void testHandleUnrecognizedAnswer() { + final String unrecognizedJson = """ + { + "type": "unrecognized" + } + """; + + final Response response = EXTENSION.target("/v1/challenge") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "10.0.0.1") + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(unrecognizedJson)); + + assertEquals(400, response.getStatus()); + + verifyNoInteractions(rateLimitChallengeManager); + } + + @Test + void testRequestPushChallenge() throws NotPushRegisteredException { + { + final Response response = EXTENSION.target("/v1/challenge/push") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(Entity.text("")); + + assertEquals(200, response.getStatus()); + } + + { + doThrow(NotPushRegisteredException.class).when(rateLimitChallengeManager).sendPushChallenge(AuthHelper.VALID_ACCOUNT_TWO); + + final Response response = EXTENSION.target("/v1/challenge/push") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .post(Entity.text("")); + + assertEquals(404, response.getStatus()); + } + } + + @Test + void testValidationError() { + final String unrecognizedJson = """ + { + "type": "rateLimitPushChallenge" + } + """; + + final Response response = EXTENSION.target("/v1/challenge") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(unrecognizedJson)); + + assertEquals(422, response.getStatus()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java new file mode 100644 index 000000000..0d02c6268 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java @@ -0,0 +1,836 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableSet; +import com.google.common.net.HttpHeaders; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; +import org.whispersystems.textsecuregcm.entities.DeviceActivationRequest; +import org.whispersystems.textsecuregcm.entities.DeviceResponse; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.entities.LinkDeviceRequest; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; +import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper; +import org.whispersystems.textsecuregcm.util.TestClock; +import org.whispersystems.textsecuregcm.util.VerificationCode; + +@ExtendWith(DropwizardExtensionsSupport.class) +class DeviceControllerTest { + + private static AccountsManager accountsManager = mock(AccountsManager.class); + private static MessagesManager messagesManager = mock(MessagesManager.class); + private static KeysManager keysManager = mock(KeysManager.class); + private static RateLimiters rateLimiters = mock(RateLimiters.class); + private static RateLimiter rateLimiter = mock(RateLimiter.class); + private static RedisAdvancedClusterCommands commands = mock(RedisAdvancedClusterCommands.class); + private static Account account = mock(Account.class); + private static Account maxedAccount = mock(Account.class); + private static Device masterDevice = mock(Device.class); + private static ClientPresenceManager clientPresenceManager = mock(ClientPresenceManager.class); + private static Map deviceConfiguration = new HashMap<>(); + private static TestClock testClock = TestClock.now(); + + private static DeviceController deviceController = new DeviceController( + generateLinkDeviceSecret(), + accountsManager, + messagesManager, + keysManager, + rateLimiters, + RedisClusterHelper.builder().stringCommands(commands).build(), + deviceConfiguration, + testClock); + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addProvider(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager)) + .addProvider(new DeviceLimitExceededExceptionMapper()) + .addResource(deviceController) + .build(); + + private static byte[] generateLinkDeviceSecret() { + final byte[] linkDeviceSecret = new byte[32]; + new SecureRandom().nextBytes(linkDeviceSecret); + + return linkDeviceSecret; + } + + @BeforeEach + void setup() { + when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter); + when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter); + when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter); + when(rateLimiters.getAllocateDeviceLimiter()).thenReturn(rateLimiter); + when(rateLimiters.getVerifyDeviceLimiter()).thenReturn(rateLimiter); + + when(masterDevice.getId()).thenReturn(1L); + + when(account.getNextDeviceId()).thenReturn(42L); + when(account.getNumber()).thenReturn(AuthHelper.VALID_NUMBER); + when(account.getUuid()).thenReturn(AuthHelper.VALID_UUID); + when(account.getPhoneNumberIdentifier()).thenReturn(AuthHelper.VALID_PNI); + when(account.isEnabled()).thenReturn(false); + when(account.isPniSupported()).thenReturn(true); + when(account.isPaymentActivationSupported()).thenReturn(false); + + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); + when(accountsManager.getByE164(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(account)); + when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(maxedAccount)); + + AccountsHelper.setupMockUpdate(accountsManager); + + when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + when(keysManager.delete(any(), anyLong())).thenReturn(CompletableFuture.completedFuture(null)); + + when(messagesManager.clear(any(), anyLong())).thenReturn(CompletableFuture.completedFuture(null)); + } + + @AfterEach + void teardown() { + reset( + accountsManager, + messagesManager, + keysManager, + rateLimiters, + rateLimiter, + commands, + account, + maxedAccount, + masterDevice, + clientPresenceManager + ); + + testClock.unpin(); + } + + @Test + void validDeviceRegisterTest() { + final Device existingDevice = mock(Device.class); + when(existingDevice.getId()).thenReturn(Device.MASTER_ID); + when(existingDevice.isEnabled()).thenReturn(true); + when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice)); + + VerificationCode deviceCode = resources.getJerseyTest() + .target("/v1/devices/provisioning/code") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(VerificationCode.class); + + DeviceResponse response = resources.getJerseyTest() + .target("/v1/devices/" + deviceCode.verificationCode()) + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.entity(new AccountAttributes(false, 1234, null, + null, true, null), + MediaType.APPLICATION_JSON_TYPE), + DeviceResponse.class); + + assertThat(response.getDeviceId()).isEqualTo(42L); + + verify(messagesManager).clear(eq(AuthHelper.VALID_UUID), eq(42L)); + verify(commands).set(anyString(), anyString(), any()); + } + + @Test + void validDeviceRegisterTestSignedTokenUsed() { + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); + + final Device existingDevice = mock(Device.class); + when(existingDevice.getId()).thenReturn(Device.MASTER_ID); + when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice)); + + final String verificationToken = deviceController.generateVerificationToken(AuthHelper.VALID_UUID); + + when(commands.get(anyString())).thenReturn(""); + + final Response response = resources.getJerseyTest() + .target("/v1/devices/" + verificationToken) + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.entity(new AccountAttributes(false, 1234, null, + null, true, null), + MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); + } + + @Test + void verifyDeviceWithNullAccountAttributes() { + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT)); + + final Device existingDevice = mock(Device.class); + when(existingDevice.getId()).thenReturn(Device.MASTER_ID); + when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice)); + + VerificationCode deviceCode = resources.getJerseyTest() + .target("/v1/devices/provisioning/code") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(VerificationCode.class); + + final Response response = resources.getJerseyTest() + .target("/v1/devices/" + deviceCode.verificationCode()) + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.json("")); + + assertThat(response.getStatus()).isNotEqualTo(500); + } + + @Test + void verifyDeviceTokenBadCredentials() { + final String verificationToken = deviceController.generateVerificationToken(AuthHelper.VALID_UUID); + + final Response response = resources.getJerseyTest() + .target("/v1/devices/" + verificationToken) + .request() + .header("Authorization", "This is not a valid authorization header") + .put(Entity.entity(new AccountAttributes(false, 1234, null, + null, true, null), + MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(401, response.getStatus()); + } + + @ParameterizedTest + @MethodSource + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + void linkDeviceAtomic(final boolean fetchesMessages, + final Optional apnRegistrationId, + final Optional gcmRegistrationId, + final Optional expectedApnsToken, + final Optional expectedApnsVoipToken, + final Optional expectedGcmToken) { + + final Device existingDevice = mock(Device.class); + when(existingDevice.getId()).thenReturn(Device.MASTER_ID); + when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice)); + + VerificationCode deviceCode = resources.getJerseyTest() + .target("/v1/devices/provisioning/code") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(VerificationCode.class); + + final Optional aciSignedPreKey; + final Optional pniSignedPreKey; + final Optional aciPqLastResortPreKey; + final Optional pniPqLastResortPreKey; + + final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair(); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + + aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair)); + pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); + aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair)); + pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair)); + + when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + + when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(), + new AccountAttributes(fetchesMessages, 1234, null, null, true, null), + new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnRegistrationId, gcmRegistrationId)); + + final DeviceResponse response = resources.getJerseyTest() + .target("/v1/devices/link") + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE), DeviceResponse.class); + + assertThat(response.getDeviceId()).isEqualTo(42L); + + final ArgumentCaptor deviceCaptor = ArgumentCaptor.forClass(Device.class); + verify(account).addDevice(deviceCaptor.capture()); + + final Device device = deviceCaptor.getValue(); + + assertEquals(aciSignedPreKey.get(), device.getSignedPreKey(IdentityType.ACI)); + assertEquals(pniSignedPreKey.get(), device.getSignedPreKey(IdentityType.PNI)); + assertEquals(fetchesMessages, device.getFetchesMessages()); + + expectedApnsToken.ifPresentOrElse(expectedToken -> assertEquals(expectedToken, device.getApnId()), + () -> assertNull(device.getApnId())); + + expectedApnsVoipToken.ifPresentOrElse(expectedToken -> assertEquals(expectedToken, device.getVoipApnId()), + () -> assertNull(device.getVoipApnId())); + + expectedGcmToken.ifPresentOrElse(expectedToken -> assertEquals(expectedToken, device.getGcmId()), + () -> assertNull(device.getGcmId())); + + verify(messagesManager).clear(eq(AuthHelper.VALID_UUID), eq(42L)); + verify(keysManager).storeEcSignedPreKeys(AuthHelper.VALID_UUID, Map.of(response.getDeviceId(), aciSignedPreKey.get())); + verify(keysManager).storeEcSignedPreKeys(AuthHelper.VALID_PNI, Map.of(response.getDeviceId(), pniSignedPreKey.get())); + verify(keysManager).storePqLastResort(AuthHelper.VALID_UUID, Map.of(response.getDeviceId(), aciPqLastResortPreKey.get())); + verify(keysManager).storePqLastResort(AuthHelper.VALID_PNI, Map.of(response.getDeviceId(), pniPqLastResortPreKey.get())); + verify(commands).set(anyString(), anyString(), any()); + } + + + private static Stream linkDeviceAtomic() { + final String apnsToken = "apns-token"; + final String apnsVoipToken = "apns-voip-token"; + final String gcmToken = "gcm-token"; + + return Stream.of( + Arguments.of(true, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()), + Arguments.of(false, Optional.of(new ApnRegistrationId(apnsToken, null)), Optional.empty(), Optional.of(apnsToken), Optional.empty(), Optional.empty()), + Arguments.of(false, Optional.of(new ApnRegistrationId(apnsToken, apnsVoipToken)), Optional.empty(), Optional.of(apnsToken), Optional.of(apnsVoipToken), Optional.empty()), + Arguments.of(false, Optional.empty(), Optional.of(new GcmRegistrationId(gcmToken)), Optional.empty(), Optional.empty(), Optional.of(gcmToken)) + ); + } + + @Test + void linkDeviceAtomicWithVerificationTokenUsed() { + + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); + + final Device existingDevice = mock(Device.class); + when(existingDevice.getId()).thenReturn(Device.MASTER_ID); + when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice)); + + final Optional aciSignedPreKey; + final Optional pniSignedPreKey; + final Optional aciPqLastResortPreKey; + final Optional pniPqLastResortPreKey; + + final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair(); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + + aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair)); + pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); + aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair)); + pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair)); + + when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + + when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + when(commands.get(anyString())).thenReturn(""); + + final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID), + new AccountAttributes(false, 1234, null, null, true, null), + new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.of(new GcmRegistrationId("gcm-id")))); + + try (final Response response = resources.getJerseyTest() + .target("/v1/devices/link") + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) { + + assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); + } + } + + @ParameterizedTest + @MethodSource + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + void linkDeviceAtomicConflictingChannel(final boolean fetchesMessages, + final Optional apnRegistrationId, + final Optional gcmRegistrationId) { + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT)); + + final Device existingDevice = mock(Device.class); + when(existingDevice.getId()).thenReturn(Device.MASTER_ID); + when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice)); + + VerificationCode deviceCode = resources.getJerseyTest() + .target("/v1/devices/provisioning/code") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(VerificationCode.class); + + final Optional aciSignedPreKey; + final Optional pniSignedPreKey; + final Optional aciPqLastResortPreKey; + final Optional pniPqLastResortPreKey; + + final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair(); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + + aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair)); + pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); + aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair)); + pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair)); + + when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + + final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(), + new AccountAttributes(fetchesMessages, 1234, null, null, true, null), + new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnRegistrationId, gcmRegistrationId)); + + try (final Response response = resources.getJerseyTest() + .target("/v1/devices/link") + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) { + + assertEquals(422, response.getStatus()); + } + } + + private static Stream linkDeviceAtomicConflictingChannel() { + return Stream.of( + Arguments.of(true, Optional.of(new ApnRegistrationId("apns-token", null)), Optional.of(new GcmRegistrationId("gcm-token"))), + Arguments.of(true, Optional.empty(), Optional.of(new GcmRegistrationId("gcm-token"))), + Arguments.of(true, Optional.of(new ApnRegistrationId("apns-token", null)), Optional.empty()), + Arguments.of(false, Optional.of(new ApnRegistrationId("apns-token", null)), Optional.of(new GcmRegistrationId("gcm-token"))) + ); + } + + @ParameterizedTest + @MethodSource + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + void linkDeviceAtomicMissingProperty(final IdentityKey aciIdentityKey, + final IdentityKey pniIdentityKey, + final Optional aciSignedPreKey, + final Optional pniSignedPreKey, + final Optional aciPqLastResortPreKey, + final Optional pniPqLastResortPreKey) { + + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT)); + + final Device existingDevice = mock(Device.class); + when(existingDevice.getId()).thenReturn(Device.MASTER_ID); + when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice)); + + VerificationCode deviceCode = resources.getJerseyTest() + .target("/v1/devices/provisioning/code") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(VerificationCode.class); + + when(account.getIdentityKey(IdentityType.ACI)).thenReturn(aciIdentityKey); + when(account.getIdentityKey(IdentityType.PNI)).thenReturn(pniIdentityKey); + + final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(), + new AccountAttributes(true, 1234, null, null, true, null), + new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.empty())); + + try (final Response response = resources.getJerseyTest() + .target("/v1/devices/link") + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) { + + assertEquals(422, response.getStatus()); + } + } + + private static Stream linkDeviceAtomicMissingProperty() { + final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair(); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + + final Optional aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair)); + final Optional pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); + final Optional aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair)); + final Optional pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair)); + + final IdentityKey aciIdentityKey = new IdentityKey(aciIdentityKeyPair.getPublicKey()); + final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey()); + + return Stream.of( + Arguments.of(aciIdentityKey, pniIdentityKey, Optional.empty(), pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey), + Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, Optional.empty(), aciPqLastResortPreKey, pniPqLastResortPreKey), + Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, pniSignedPreKey, Optional.empty(), pniPqLastResortPreKey), + Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, Optional.empty()) + ); + } + + @ParameterizedTest + @MethodSource + void linkDeviceAtomicInvalidSignature(final IdentityKey aciIdentityKey, + final IdentityKey pniIdentityKey, + final ECSignedPreKey aciSignedPreKey, + final ECSignedPreKey pniSignedPreKey, + final KEMSignedPreKey aciPqLastResortPreKey, + final KEMSignedPreKey pniPqLastResortPreKey) { + + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT)); + + final Device existingDevice = mock(Device.class); + when(existingDevice.getId()).thenReturn(Device.MASTER_ID); + when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice)); + + VerificationCode deviceCode = resources.getJerseyTest() + .target("/v1/devices/provisioning/code") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(VerificationCode.class); + + when(account.getIdentityKey(IdentityType.ACI)).thenReturn(aciIdentityKey); + when(account.getIdentityKey(IdentityType.PNI)).thenReturn(pniIdentityKey); + + final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(), + new AccountAttributes(true, 1234, null, null, true, null), + new DeviceActivationRequest(Optional.of(aciSignedPreKey), Optional.of(pniSignedPreKey), Optional.of(aciPqLastResortPreKey), Optional.of(pniPqLastResortPreKey), Optional.empty(), Optional.empty())); + + try (final Response response = resources.getJerseyTest() + .target("/v1/devices/link") + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) { + + assertEquals(422, response.getStatus()); + } + } + + private static Stream linkDeviceAtomicInvalidSignature() { + final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair(); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + + final ECSignedPreKey aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair); + final ECSignedPreKey pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair); + final KEMSignedPreKey aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair); + final KEMSignedPreKey pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); + + final IdentityKey aciIdentityKey = new IdentityKey(aciIdentityKeyPair.getPublicKey()); + final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey()); + + return Stream.of( + Arguments.of(aciIdentityKey, pniIdentityKey, ecSignedPreKeyWithBadSignature(aciSignedPreKey), pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey), + Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, ecSignedPreKeyWithBadSignature(pniSignedPreKey), aciPqLastResortPreKey, pniPqLastResortPreKey), + Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, pniSignedPreKey, kemSignedPreKeyWithBadSignature(aciPqLastResortPreKey), pniPqLastResortPreKey), + Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, kemSignedPreKeyWithBadSignature(pniPqLastResortPreKey)) + ); + } + + private static ECSignedPreKey ecSignedPreKeyWithBadSignature(final ECSignedPreKey signedPreKey) { + return new ECSignedPreKey(signedPreKey.keyId(), + signedPreKey.publicKey(), + "incorrect-signature".getBytes(StandardCharsets.UTF_8)); + } + + private static KEMSignedPreKey kemSignedPreKeyWithBadSignature(final KEMSignedPreKey signedPreKey) { + return new KEMSignedPreKey(signedPreKey.keyId(), + signedPreKey.publicKey(), + "incorrect-signature".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void disabledDeviceRegisterTest() { + Response response = resources.getJerseyTest() + .target("/v1/devices/provisioning/code") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + } + + @Test + void invalidDeviceRegisterTest() { + VerificationCode deviceCode = resources.getJerseyTest() + .target("/v1/devices/provisioning/code") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(VerificationCode.class); + + Response response = resources.getJerseyTest() + .target("/v1/devices/" + deviceCode.verificationCode() + "-incorrect") + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.entity(new AccountAttributes(false, 1234, null, null, true, null), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(403); + + verifyNoMoreInteractions(messagesManager); + } + + @Test + void oldDeviceRegisterTest() { + Response response = resources.getJerseyTest() + .target("/v1/devices/1112223") + .request() + .header("Authorization", + AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity(new AccountAttributes(false, 1234, null, null, true, null), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(403); + + verifyNoMoreInteractions(messagesManager); + } + + @Test + void maxDevicesTest() { + Response response = resources.getJerseyTest() + .target("/v1/devices/provisioning/code") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .get(); + + assertEquals(411, response.getStatus()); + verifyNoMoreInteractions(messagesManager); + } + + @Test + void longNameTest() { + final String verificationToken = deviceController.generateVerificationToken(AuthHelper.VALID_UUID); + + Response response = resources.getJerseyTest() + .target("/v1/devices/" + verificationToken) + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.entity(new AccountAttributes(false, 1234, + "this is a really long name that is longer than 80 characters it's so long that it's even longer than 204 characters. that's a lot of characters. we're talking lots and lots and lots of characters. 12345678", + null, true, null), + MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(response.getStatus(), 422); + verifyNoMoreInteractions(messagesManager); + } + + @Test + void deviceDowngradePniTest() { + DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, + false, true); + AccountAttributes accountAttributes = + new AccountAttributes(false, 1234, null, null, true, deviceCapabilities); + + final String verificationToken = deviceController.generateVerificationToken(AuthHelper.VALID_UUID); + + Response response = resources + .getJerseyTest() + .target("/v1/devices/" + verificationToken) + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30") + .put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE)); + assertThat(response.getStatus()).isEqualTo(409); + + deviceCapabilities = new DeviceCapabilities(true, true, true, true); + accountAttributes = new AccountAttributes(false, 1234, null, null, true, deviceCapabilities); + response = resources + .getJerseyTest() + .target("/v1/devices/" + verificationToken) + .request() + .header("Authorization", + AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30") + .put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE)); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + void putCapabilitiesSuccessTest() { + final DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, true); + final Response response = resources + .getJerseyTest() + .target("/v1/devices/capabilities") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30") + .put(Entity.entity(deviceCapabilities, MediaType.APPLICATION_JSON_TYPE)); + assertThat(response.getStatus()).isEqualTo(204); + assertThat(response.hasEntity()).isFalse(); + } + + @Test + void putCapabilitiesFailureTest() { + final Response response = resources + .getJerseyTest() + .target("/v1/devices/capabilities") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30") + .put(Entity.json("")); + assertThat(response.getStatus()).isEqualTo(422); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void deviceDowngradePaymentActivationTest(boolean paymentActivation) { + // Update when we start returning true value of capability & restricting downgrades + DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, paymentActivation); + AccountAttributes accountAttributes = new AccountAttributes(false, 1234, null, null, true, deviceCapabilities); + + final String verificationToken = deviceController.generateVerificationToken(AuthHelper.VALID_UUID); + + Response response = resources + .getJerseyTest() + .target("/v1/devices/" + verificationToken) + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30") + .put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE)); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + void deviceRemovalClearsMessagesAndKeys() { + + // this is a static mock, so it might have previous invocations + clearInvocations(AuthHelper.VALID_ACCOUNT); + + final long deviceId = 2; + + final Response response = resources + .getJerseyTest() + .target("/v1/devices/" + deviceId) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30") + .delete(); + + assertThat(response.getStatus()).isEqualTo(204); + assertThat(response.hasEntity()).isFalse(); + + verify(messagesManager, times(2)).clear(AuthHelper.VALID_UUID, deviceId); + verify(accountsManager, times(1)).update(eq(AuthHelper.VALID_ACCOUNT), any()); + verify(AuthHelper.VALID_ACCOUNT).removeDevice(deviceId); + verify(keysManager).delete(AuthHelper.VALID_UUID, deviceId); + } + + @Test + void unlinkPrimaryDevice() { + // this is a static mock, so it might have previous invocations + clearInvocations(AuthHelper.VALID_ACCOUNT); + + try (final Response response = resources + .getJerseyTest() + .target("/v1/devices/" + Device.MASTER_ID) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30") + .delete()) { + + assertThat(response.getStatus()).isEqualTo(403); + + verify(messagesManager, never()).clear(any(), anyLong()); + verify(accountsManager, never()).update(eq(AuthHelper.VALID_ACCOUNT), any()); + verify(AuthHelper.VALID_ACCOUNT, never()).removeDevice(anyLong()); + verify(keysManager, never()).delete(any(), anyLong()); + } + } + + @Test + void checkVerificationToken() { + final UUID uuid = UUID.randomUUID(); + + assertEquals(Optional.of(uuid), + deviceController.checkVerificationToken(deviceController.generateVerificationToken(uuid))); + } + + @ParameterizedTest + @MethodSource + void checkVerificationTokenBadToken(final String token, final Instant currentTime) { + testClock.pin(currentTime); + + assertEquals(Optional.empty(), + deviceController.checkVerificationToken(token)); + } + + private static Stream checkVerificationTokenBadToken() { + final Instant tokenTimestamp = testClock.instant(); + + return Stream.of( + // Expired token + Arguments.of(deviceController.generateVerificationToken(UUID.randomUUID()), + tokenTimestamp.plus(DeviceController.TOKEN_EXPIRATION_DURATION).plusSeconds(1)), + + // Bad UUID + Arguments.of("not-a-valid-uuid.1691096565171:0CKWF7q3E9fi4sB2or4q1A0Up2z_73EQlMAy7Dpel9c=", tokenTimestamp), + + // No UUID + Arguments.of(".1691096565171:0CKWF7q3E9fi4sB2or4q1A0Up2z_73EQlMAy7Dpel9c=", tokenTimestamp), + + // Bad timestamp + Arguments.of("e552603a-1492-4de6-872d-bac19a2825b4.not-a-valid-timestamp:0CKWF7q3E9fi4sB2or4q1A0Up2z_73EQlMAy7Dpel9c=", tokenTimestamp), + + // No timestamp + Arguments.of("e552603a-1492-4de6-872d-bac19a2825b4:0CKWF7q3E9fi4sB2or4q1A0Up2z_73EQlMAy7Dpel9c=", tokenTimestamp), + + // Blank timestamp + Arguments.of("e552603a-1492-4de6-872d-bac19a2825b4.:0CKWF7q3E9fi4sB2or4q1A0Up2z_73EQlMAy7Dpel9c=", tokenTimestamp), + + // No signature + Arguments.of("e552603a-1492-4de6-872d-bac19a2825b4.1691096565171", tokenTimestamp), + + // Blank signature + Arguments.of("e552603a-1492-4de6-872d-bac19a2825b4.1691096565171:", tokenTimestamp), + + // Incorrect signature + Arguments.of("e552603a-1492-4de6-872d-bac19a2825b4.1691096565171:0CKWF7q3E9fi4sB2or4q1A0Up2z_73EQlMAy7Dpel9c=", tokenTimestamp), + + // Invalid signature + Arguments.of("e552603a-1492-4de6-872d-bac19a2825b4.1691096565171:This is not valid base64", tokenTimestamp) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DirectoryControllerV2Test.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DirectoryControllerV2Test.java new file mode 100644 index 000000000..296bd0920 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DirectoryControllerV2Test.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.util.MockUtils.secretBytesOf; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.DirectoryV2ClientConfiguration; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.Pair; + +class DirectoryControllerV2Test { + + @Test + void testAuthToken() { + final ExternalServiceCredentialsGenerator credentialsGenerator = DirectoryV2Controller.credentialsGenerator( + new DirectoryV2ClientConfiguration(secretBytesOf(0x01), secretBytesOf(0x02)), + Clock.fixed(Instant.ofEpochSecond(1633738643L), ZoneId.of("Etc/UTC")) + ); + + final DirectoryV2Controller controller = new DirectoryV2Controller(credentialsGenerator); + + final Account account = mock(Account.class); + final UUID uuid = UUID.fromString("11111111-1111-1111-1111-111111111111"); + when(account.getUuid()).thenReturn(uuid); + + final ExternalServiceCredentials credentials = (ExternalServiceCredentials) controller.getAuthToken( + new AuthenticatedAccount(() -> new Pair<>(account, mock(Device.class)))).getEntity(); + + assertEquals(credentials.username(), "d369bc712e2e0dd36258"); + assertEquals(credentials.password(), "1633738643:4433b0fab41f25f79dd4"); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DonationControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DonationControllerTest.java new file mode 100644 index 000000000..45abc1223 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DonationControllerTest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.security.SecureRandom; +import java.time.Clock; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation; +import org.signal.libsignal.zkgroup.receipts.ReceiptSerial; +import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; +import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; +import org.whispersystems.textsecuregcm.entities.BadgeSvg; +import org.whispersystems.textsecuregcm.entities.RedeemReceiptRequest; +import org.whispersystems.textsecuregcm.storage.AccountBadge; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.TestClock; + +class DonationControllerTest { + + private static final long nowEpochSeconds = 1_500_000_000L; + + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + static BadgesConfiguration getBadgesConfiguration() { + return new BadgesConfiguration( + List.of( + new BadgeConfiguration("TEST", "other", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", + List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))), + new BadgeConfiguration("TEST1", "testing", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", + List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))), + new BadgeConfiguration("TEST2", "testing", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", + List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))), + new BadgeConfiguration("TEST3", "testing", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", + List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")))), + List.of("TEST"), + Map.of(1L, "TEST1", 2L, "TEST2", 3L, "TEST3")); + } + + final Clock clock = TestClock.pinned(Instant.ofEpochSecond(nowEpochSeconds)); + ServerZkReceiptOperations zkReceiptOperations; + RedeemedReceiptsManager redeemedReceiptsManager; + AccountsManager accountsManager; + byte[] receiptSerialBytes; + ReceiptSerial receiptSerial; + byte[] presentation; + DonationController.ReceiptCredentialPresentationFactory receiptCredentialPresentationFactory; + ReceiptCredentialPresentation receiptCredentialPresentation; + ResourceExtension resources; + + @BeforeEach + void beforeEach() throws Throwable { + zkReceiptOperations = mock(ServerZkReceiptOperations.class); + redeemedReceiptsManager = mock(RedeemedReceiptsManager.class); + accountsManager = mock(AccountsManager.class); + AccountsHelper.setupMockUpdate(accountsManager); + receiptSerialBytes = new byte[ReceiptSerial.SIZE]; + SECURE_RANDOM.nextBytes(receiptSerialBytes); + receiptSerial = new ReceiptSerial(receiptSerialBytes); + presentation = new byte[25]; + SECURE_RANDOM.nextBytes(presentation); + receiptCredentialPresentationFactory = mock(DonationController.ReceiptCredentialPresentationFactory.class); + receiptCredentialPresentation = mock(ReceiptCredentialPresentation.class); + + try { + when(receiptCredentialPresentationFactory.build(presentation)).thenReturn(receiptCredentialPresentation); + } catch (InvalidInputException e) { + throw new AssertionError(e); + } + + resources = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, + getBadgesConfiguration(), receiptCredentialPresentationFactory)) + .build(); + resources.before(); + } + + @AfterEach + void afterEach() throws Throwable { + resources.after(); + } + + @Test + void testRedeemReceipt() { + when(receiptCredentialPresentation.getReceiptSerial()).thenReturn(receiptSerial); + final long receiptLevel = 1L; + when(receiptCredentialPresentation.getReceiptLevel()).thenReturn(receiptLevel); + final long receiptExpiration = nowEpochSeconds + 86400 * 30; + when(receiptCredentialPresentation.getReceiptExpirationTime()).thenReturn(receiptExpiration); + when(redeemedReceiptsManager.put(same(receiptSerial), eq(receiptExpiration), eq(receiptLevel), eq(AuthHelper.VALID_UUID))).thenReturn( + CompletableFuture.completedFuture(Boolean.TRUE)); + when(accountsManager.getByAccountIdentifier(eq(AuthHelper.VALID_UUID))).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT)); + + RedeemReceiptRequest request = new RedeemReceiptRequest(presentation, true, true); + Response response = resources.getJerseyTest() + .target("/v1/donation/redeem-receipt") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(200); + verify(AuthHelper.VALID_ACCOUNT).addBadge(same(clock), eq(new AccountBadge("TEST1", Instant.ofEpochSecond(receiptExpiration), true))); + verify(AuthHelper.VALID_ACCOUNT).makeBadgePrimaryIfExists(same(clock), eq("TEST1")); + } + + @Test + void testRedeemReceiptAlreadyRedeemedWithDifferentParameters() { + when(receiptCredentialPresentation.getReceiptSerial()).thenReturn(receiptSerial); + final long receiptLevel = 1L; + when(receiptCredentialPresentation.getReceiptLevel()).thenReturn(receiptLevel); + final long receiptExpiration = nowEpochSeconds + 86400 * 30; + when(receiptCredentialPresentation.getReceiptExpirationTime()).thenReturn(receiptExpiration); + when(redeemedReceiptsManager.put(same(receiptSerial), eq(receiptExpiration), eq(receiptLevel), eq(AuthHelper.VALID_UUID))).thenReturn( + CompletableFuture.completedFuture(Boolean.FALSE)); + + RedeemReceiptRequest request = new RedeemReceiptRequest(presentation, true, true); + Response response = resources.getJerseyTest() + .target("/v1/donation/redeem-receipt") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.readEntity(String.class)).isEqualTo("receipt serial is already redeemed"); + } + + @Test + void testRedeemReceiptBadCredentialPresentation() throws InvalidInputException { + when(receiptCredentialPresentationFactory.build(any())).thenThrow(new InvalidInputException()); + + final Response response = resources.getJerseyTest() + .target("/v1/donation/redeem-receipt") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(Entity.entity(new RedeemReceiptRequest(presentation, true, true), MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(400); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeysControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeysControllerTest.java new file mode 100644 index 000000000..d05df2ce5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeysControllerTest.java @@ -0,0 +1,961 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.OptionalAccess; +import org.whispersystems.textsecuregcm.entities.ECPreKey; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.entities.PreKeyCount; +import org.whispersystems.textsecuregcm.entities.PreKeyResponse; +import org.whispersystems.textsecuregcm.entities.PreKeyState; +import org.whispersystems.textsecuregcm.entities.SignedPreKey; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; +import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; + +@ExtendWith(DropwizardExtensionsSupport.class) +class KeysControllerTest { + + private static final String EXISTS_NUMBER = "+14152222222"; + private static final UUID EXISTS_UUID = UUID.randomUUID(); + private static final UUID EXISTS_PNI = UUID.randomUUID(); + + private static final UUID NOT_EXISTS_UUID = UUID.randomUUID(); + + private static final int SAMPLE_REGISTRATION_ID = 999; + private static final int SAMPLE_REGISTRATION_ID2 = 1002; + private static final int SAMPLE_REGISTRATION_ID4 = 1555; + + private static final int SAMPLE_PNI_REGISTRATION_ID = 1717; + + private final ECKeyPair IDENTITY_KEY_PAIR = Curve.generateKeyPair(); + private final IdentityKey IDENTITY_KEY = new IdentityKey(IDENTITY_KEY_PAIR.getPublicKey()); + + private final ECKeyPair PNI_IDENTITY_KEY_PAIR = Curve.generateKeyPair(); + private final IdentityKey PNI_IDENTITY_KEY = new IdentityKey(PNI_IDENTITY_KEY_PAIR.getPublicKey()); + + private final ECPreKey SAMPLE_KEY = KeysHelper.ecPreKey(1234); + private final ECPreKey SAMPLE_KEY2 = KeysHelper.ecPreKey(5667); + private final ECPreKey SAMPLE_KEY3 = KeysHelper.ecPreKey(334); + private final ECPreKey SAMPLE_KEY4 = KeysHelper.ecPreKey(336); + + private final ECPreKey SAMPLE_KEY_PNI = KeysHelper.ecPreKey(7777); + + private final KEMSignedPreKey SAMPLE_PQ_KEY = KeysHelper.signedKEMPreKey(2424, Curve.generateKeyPair()); + private final KEMSignedPreKey SAMPLE_PQ_KEY2 = KeysHelper.signedKEMPreKey(6868, Curve.generateKeyPair()); + private final KEMSignedPreKey SAMPLE_PQ_KEY3 = KeysHelper.signedKEMPreKey(1313, Curve.generateKeyPair()); + + private final KEMSignedPreKey SAMPLE_PQ_KEY_PNI = KeysHelper.signedKEMPreKey(8888, Curve.generateKeyPair()); + + private final ECSignedPreKey SAMPLE_SIGNED_KEY = KeysHelper.signedECPreKey(1111, IDENTITY_KEY_PAIR); + private final ECSignedPreKey SAMPLE_SIGNED_KEY2 = KeysHelper.signedECPreKey(2222, IDENTITY_KEY_PAIR); + private final ECSignedPreKey SAMPLE_SIGNED_KEY3 = KeysHelper.signedECPreKey(3333, IDENTITY_KEY_PAIR); + private final ECSignedPreKey SAMPLE_SIGNED_PNI_KEY = KeysHelper.signedECPreKey(4444, PNI_IDENTITY_KEY_PAIR); + private final ECSignedPreKey SAMPLE_SIGNED_PNI_KEY2 = KeysHelper.signedECPreKey(5555, PNI_IDENTITY_KEY_PAIR); + private final ECSignedPreKey SAMPLE_SIGNED_PNI_KEY3 = KeysHelper.signedECPreKey(6666, PNI_IDENTITY_KEY_PAIR); + private final ECSignedPreKey VALID_DEVICE_SIGNED_KEY = KeysHelper.signedECPreKey(89898, IDENTITY_KEY_PAIR); + private final ECSignedPreKey VALID_DEVICE_PNI_SIGNED_KEY = KeysHelper.signedECPreKey(7777, PNI_IDENTITY_KEY_PAIR); + + private final static KeysManager KEYS = mock(KeysManager.class ); + private final static AccountsManager accounts = mock(AccountsManager.class ); + private final static Account existsAccount = mock(Account.class ); + + private static final RateLimiters rateLimiters = mock(RateLimiters.class); + private static final RateLimiter rateLimiter = mock(RateLimiter.class ); + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(CompletionExceptionMapper.class) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of( + AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new ServerRejectedExceptionMapper()) + .addResource(new KeysController(rateLimiters, KEYS, accounts)) + .addResource(new RateLimitExceededExceptionMapper()) + .build(); + + private Device sampleDevice; + + private record WeaklyTypedPreKey(long keyId, + + @JsonSerialize(using = ByteArrayAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) + byte[] publicKey) { + } + + private record WeaklyTypedSignedPreKey(long keyId, + + @JsonSerialize(using = ByteArrayAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) + byte[] publicKey, + + @JsonSerialize(using = ByteArrayAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) + byte[] signature) { + + static WeaklyTypedSignedPreKey fromSignedPreKey(final SignedPreKey signedPreKey) { + return new WeaklyTypedSignedPreKey(signedPreKey.keyId(), signedPreKey.serializedPublicKey(), signedPreKey.signature()); + } + } + + private record WeaklyTypedPreKeyState(List preKeys, + WeaklyTypedSignedPreKey signedPreKey, + List pqPreKeys, + WeaklyTypedSignedPreKey pqLastResortPreKey, + + @JsonSerialize(using = ByteArrayAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) + byte[] identityKey) { + } + + @BeforeEach + void setup() { + sampleDevice = mock(Device.class); + final Device sampleDevice2 = mock(Device.class); + final Device sampleDevice3 = mock(Device.class); + final Device sampleDevice4 = mock(Device.class); + + final List allDevices = List.of(sampleDevice, sampleDevice2, sampleDevice3, sampleDevice4); + + AccountsHelper.setupMockUpdate(accounts); + + when(sampleDevice.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID); + when(sampleDevice2.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2); + when(sampleDevice3.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2); + when(sampleDevice4.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID4); + when(sampleDevice.getPhoneNumberIdentityRegistrationId()).thenReturn(OptionalInt.of(SAMPLE_PNI_REGISTRATION_ID)); + when(sampleDevice.isEnabled()).thenReturn(true); + when(sampleDevice2.isEnabled()).thenReturn(true); + when(sampleDevice3.isEnabled()).thenReturn(false); + when(sampleDevice4.isEnabled()).thenReturn(true); + when(sampleDevice.getSignedPreKey(IdentityType.ACI)).thenReturn(SAMPLE_SIGNED_KEY); + when(sampleDevice2.getSignedPreKey(IdentityType.ACI)).thenReturn(SAMPLE_SIGNED_KEY2); + when(sampleDevice3.getSignedPreKey(IdentityType.ACI)).thenReturn(SAMPLE_SIGNED_KEY3); + when(sampleDevice4.getSignedPreKey(IdentityType.ACI)).thenReturn(null); + when(sampleDevice.getSignedPreKey(IdentityType.PNI)).thenReturn(SAMPLE_SIGNED_PNI_KEY); + when(sampleDevice2.getSignedPreKey(IdentityType.PNI)).thenReturn(SAMPLE_SIGNED_PNI_KEY2); + when(sampleDevice3.getSignedPreKey(IdentityType.PNI)).thenReturn(SAMPLE_SIGNED_PNI_KEY3); + when(sampleDevice4.getSignedPreKey(IdentityType.PNI)).thenReturn(null); + when(sampleDevice.getId()).thenReturn(1L); + when(sampleDevice2.getId()).thenReturn(2L); + when(sampleDevice3.getId()).thenReturn(3L); + when(sampleDevice4.getId()).thenReturn(4L); + + when(existsAccount.getUuid()).thenReturn(EXISTS_UUID); + when(existsAccount.getPhoneNumberIdentifier()).thenReturn(EXISTS_PNI); + when(existsAccount.getDevice(1L)).thenReturn(Optional.of(sampleDevice)); + when(existsAccount.getDevice(2L)).thenReturn(Optional.of(sampleDevice2)); + when(existsAccount.getDevice(3L)).thenReturn(Optional.of(sampleDevice3)); + when(existsAccount.getDevice(4L)).thenReturn(Optional.of(sampleDevice4)); + when(existsAccount.getDevice(22L)).thenReturn(Optional.empty()); + when(existsAccount.getDevices()).thenReturn(allDevices); + when(existsAccount.isEnabled()).thenReturn(true); + when(existsAccount.getIdentityKey(IdentityType.ACI)).thenReturn(IDENTITY_KEY); + when(existsAccount.getIdentityKey(IdentityType.PNI)).thenReturn(PNI_IDENTITY_KEY); + when(existsAccount.getNumber()).thenReturn(EXISTS_NUMBER); + when(existsAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of("1337".getBytes())); + + when(accounts.getByServiceIdentifier(any())).thenReturn(Optional.empty()); + + when(accounts.getByServiceIdentifier(new AciServiceIdentifier(EXISTS_UUID))).thenReturn(Optional.of(existsAccount)); + when(accounts.getByServiceIdentifier(new PniServiceIdentifier(EXISTS_PNI))).thenReturn(Optional.of(existsAccount)); + + when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter); + + when(KEYS.store(any(), anyLong(), any(), any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + when(KEYS.getEcSignedPreKey(any(), anyLong())).thenReturn(CompletableFuture.completedFuture(Optional.empty())); + when(KEYS.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + when(KEYS.takeEC(EXISTS_UUID, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY))); + when(KEYS.takePQ(EXISTS_UUID, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_PQ_KEY))); + when(KEYS.takeEC(EXISTS_PNI, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY_PNI))); + when(KEYS.takePQ(EXISTS_PNI, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_PQ_KEY_PNI))); + + when(KEYS.getEcCount(AuthHelper.VALID_UUID, 1)).thenReturn(CompletableFuture.completedFuture(5)); + when(KEYS.getPqCount(AuthHelper.VALID_UUID, 1)).thenReturn(CompletableFuture.completedFuture(5)); + + when(AuthHelper.VALID_DEVICE.getSignedPreKey(IdentityType.ACI)).thenReturn(VALID_DEVICE_SIGNED_KEY); + when(AuthHelper.VALID_DEVICE.getSignedPreKey(IdentityType.PNI)).thenReturn(VALID_DEVICE_PNI_SIGNED_KEY); + when(AuthHelper.VALID_ACCOUNT.getIdentityKey(IdentityType.ACI)).thenReturn(null); + } + + @AfterEach + void teardown() { + reset( + KEYS, + accounts, + existsAccount, + rateLimiters, + rateLimiter + ); + + clearInvocations(AuthHelper.VALID_DEVICE); + } + + @Test + void validKeyStatusTest() { + PreKeyCount result = resources.getJerseyTest() + .target("/v2/keys") + .request() + .header("Authorization", + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(PreKeyCount.class); + + assertThat(result.getCount()).isEqualTo(5); + assertThat(result.getPqCount()).isEqualTo(5); + + verify(KEYS).getEcCount(AuthHelper.VALID_UUID, 1); + verify(KEYS).getPqCount(AuthHelper.VALID_UUID, 1); + } + + @Test + void putSignedPreKeyV2() { + ECSignedPreKey test = KeysHelper.signedECPreKey(9998, IDENTITY_KEY_PAIR); + Response response = resources.getJerseyTest() + .target("/v2/keys/signed") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(test, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(204); + + verify(AuthHelper.VALID_DEVICE).setSignedPreKey(eq(test)); + verify(AuthHelper.VALID_DEVICE, never()).setPhoneNumberIdentitySignedPreKey(any()); + verify(accounts).updateDevice(eq(AuthHelper.VALID_ACCOUNT), anyLong(), any()); + verify(KEYS).storeEcSignedPreKeys(AuthHelper.VALID_UUID, Map.of(Device.MASTER_ID, test)); + } + + @Test + void putPhoneNumberIdentitySignedPreKeyV2() { + final ECSignedPreKey replacementKey = KeysHelper.signedECPreKey(9998, PNI_IDENTITY_KEY_PAIR); + + Response response = resources.getJerseyTest() + .target("/v2/keys/signed") + .queryParam("identity", "pni") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(replacementKey, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(204); + + verify(AuthHelper.VALID_DEVICE).setPhoneNumberIdentitySignedPreKey(eq(replacementKey)); + verify(AuthHelper.VALID_DEVICE, never()).setSignedPreKey(any()); + verify(accounts).updateDevice(eq(AuthHelper.VALID_ACCOUNT), anyLong(), any()); + verify(KEYS).storeEcSignedPreKeys(AuthHelper.VALID_PNI, Map.of(Device.MASTER_ID, replacementKey)); + } + + @Test + void disabledPutSignedPreKeyV2() { + ECSignedPreKey test = KeysHelper.signedECPreKey(9999, IDENTITY_KEY_PAIR); + Response response = resources.getJerseyTest() + .target("/v2/keys/signed") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .put(Entity.entity(test, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(401); + } + + @Test + void validSingleRequestTestV2() { + PreKeyResponse result = resources.getJerseyTest() + .target(String.format("/v2/keys/%s/1", EXISTS_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(PreKeyResponse.class); + + assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey(IdentityType.ACI)); + assertThat(result.getDevicesCount()).isEqualTo(1); + assertEquals(SAMPLE_KEY, result.getDevice(1).getPreKey()); + assertThat(result.getDevice(1).getPqPreKey()).isNull(); + assertThat(result.getDevice(1).getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID); + assertEquals(existsAccount.getDevice(1).get().getSignedPreKey(IdentityType.ACI), + result.getDevice(1).getSignedPreKey()); + + verify(KEYS).takeEC(EXISTS_UUID, 1); + verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 1); + verifyNoMoreInteractions(KEYS); + } + + @Test + void validSingleRequestPqTestNoPqKeysV2() { + when(KEYS.takePQ(EXISTS_UUID, 1)).thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + PreKeyResponse result = resources.getJerseyTest() + .target(String.format("/v2/keys/%s/1", EXISTS_UUID)) + .queryParam("pq", "true") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(PreKeyResponse.class); + + assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey(IdentityType.ACI)); + assertThat(result.getDevicesCount()).isEqualTo(1); + assertEquals(SAMPLE_KEY, result.getDevice(1).getPreKey()); + assertThat(result.getDevice(1).getPqPreKey()).isNull(); + assertThat(result.getDevice(1).getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID); + assertEquals(existsAccount.getDevice(1).get().getSignedPreKey(IdentityType.ACI), + result.getDevice(1).getSignedPreKey()); + + verify(KEYS).takeEC(EXISTS_UUID, 1); + verify(KEYS).takePQ(EXISTS_UUID, 1); + verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 1); + verifyNoMoreInteractions(KEYS); + } + + @Test + void validSingleRequestPqTestV2() { + PreKeyResponse result = resources.getJerseyTest() + .target(String.format("/v2/keys/%s/1", EXISTS_UUID)) + .queryParam("pq", "true") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(PreKeyResponse.class); + + assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey(IdentityType.ACI)); + assertThat(result.getDevicesCount()).isEqualTo(1); + assertEquals(SAMPLE_KEY, result.getDevice(1).getPreKey()); + assertEquals(SAMPLE_PQ_KEY, result.getDevice(1).getPqPreKey()); + assertThat(result.getDevice(1).getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID); + assertEquals(existsAccount.getDevice(1).get().getSignedPreKey(IdentityType.ACI), + result.getDevice(1).getSignedPreKey()); + + verify(KEYS).takeEC(EXISTS_UUID, 1); + verify(KEYS).takePQ(EXISTS_UUID, 1); + verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 1); + verifyNoMoreInteractions(KEYS); + } + + @Test + void validSingleRequestByPhoneNumberIdentifierTestV2() { + PreKeyResponse result = resources.getJerseyTest() + .target(String.format("/v2/keys/PNI:%s/1", EXISTS_PNI)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(PreKeyResponse.class); + + assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey(IdentityType.PNI)); + assertThat(result.getDevicesCount()).isEqualTo(1); + assertEquals(SAMPLE_KEY_PNI, result.getDevice(1).getPreKey()); + assertThat(result.getDevice(1).getPqPreKey()).isNull(); + assertThat(result.getDevice(1).getRegistrationId()).isEqualTo(SAMPLE_PNI_REGISTRATION_ID); + assertEquals(existsAccount.getDevice(1).get().getSignedPreKey(IdentityType.PNI), + result.getDevice(1).getSignedPreKey()); + + verify(KEYS).takeEC(EXISTS_PNI, 1); + verify(KEYS).getEcSignedPreKey(EXISTS_PNI, 1); + verifyNoMoreInteractions(KEYS); + } + + @Test + void validSingleRequestPqByPhoneNumberIdentifierTestV2() { + PreKeyResponse result = resources.getJerseyTest() + .target(String.format("/v2/keys/PNI:%s/1", EXISTS_PNI)) + .queryParam("pq", "true") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(PreKeyResponse.class); + + assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey(IdentityType.PNI)); + assertThat(result.getDevicesCount()).isEqualTo(1); + assertEquals(SAMPLE_KEY_PNI, result.getDevice(1).getPreKey()); + assertThat(result.getDevice(1).getPqPreKey()).isEqualTo(SAMPLE_PQ_KEY_PNI); + assertThat(result.getDevice(1).getRegistrationId()).isEqualTo(SAMPLE_PNI_REGISTRATION_ID); + assertEquals(existsAccount.getDevice(1).get().getSignedPreKey(IdentityType.PNI), + result.getDevice(1).getSignedPreKey()); + + verify(KEYS).takeEC(EXISTS_PNI, 1); + verify(KEYS).takePQ(EXISTS_PNI, 1); + verify(KEYS).getEcSignedPreKey(EXISTS_PNI, 1); + verifyNoMoreInteractions(KEYS); + } + + @Test + void validSingleRequestByPhoneNumberIdentifierNoPniRegistrationIdTestV2() { + when(sampleDevice.getPhoneNumberIdentityRegistrationId()).thenReturn(OptionalInt.empty()); + + PreKeyResponse result = resources.getJerseyTest() + .target(String.format("/v2/keys/PNI:%s/1", EXISTS_PNI)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(PreKeyResponse.class); + + assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey(IdentityType.PNI)); + assertThat(result.getDevicesCount()).isEqualTo(1); + assertEquals(SAMPLE_KEY_PNI, result.getDevice(1).getPreKey()); + assertThat(result.getDevice(1).getPqPreKey()).isNull(); + assertThat(result.getDevice(1).getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID); + assertEquals(existsAccount.getDevice(1).get().getSignedPreKey(IdentityType.PNI), + result.getDevice(1).getSignedPreKey()); + + verify(KEYS).takeEC(EXISTS_PNI, 1); + verify(KEYS).getEcSignedPreKey(EXISTS_PNI, 1); + verifyNoMoreInteractions(KEYS); + } + + @Test + void testGetKeysRateLimited() throws RateLimitExceededException { + Duration retryAfter = Duration.ofSeconds(31); + doThrow(new RateLimitExceededException(retryAfter, true)).when(rateLimiter).validate(anyString()); + + Response result = resources.getJerseyTest() + .target(String.format("/v2/keys/PNI:%s/*", EXISTS_PNI)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(); + + assertThat(result.getStatus()).isEqualTo(413); + assertThat(result.getHeaderString("Retry-After")).isEqualTo(String.valueOf(retryAfter.toSeconds())); + } + + @Test + void testUnidentifiedRequest() { + PreKeyResponse result = resources.getJerseyTest() + .target(String.format("/v2/keys/%s/1", EXISTS_UUID)) + .queryParam("pq", "true") + .request() + .header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("1337".getBytes())) + .get(PreKeyResponse.class); + + assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey(IdentityType.ACI)); + assertThat(result.getDevicesCount()).isEqualTo(1); + assertEquals(SAMPLE_KEY, result.getDevice(1).getPreKey()); + assertEquals(SAMPLE_PQ_KEY, result.getDevice(1).getPqPreKey()); + assertEquals(existsAccount.getDevice(1).get().getSignedPreKey(IdentityType.ACI), + result.getDevice(1).getSignedPreKey()); + + verify(KEYS).takeEC(EXISTS_UUID, 1); + verify(KEYS).takePQ(EXISTS_UUID, 1); + verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 1); + verifyNoMoreInteractions(KEYS); + } + + @Test + void testNoDevices() { + + when(existsAccount.getDevices()).thenReturn(Collections.emptyList()); + + Response result = resources.getJerseyTest() + .target(String.format("/v2/keys/%s/*", EXISTS_UUID)) + .request() + .header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("1337".getBytes())) + .get(); + + assertThat(result).isNotNull(); + assertThat(result.getStatus()).isEqualTo(404); + } + + @Test + void testUnauthorizedUnidentifiedRequest() { + Response response = resources.getJerseyTest() + .target(String.format("/v2/keys/%s/1", EXISTS_UUID)) + .request() + .header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("9999".getBytes())) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + verifyNoMoreInteractions(KEYS); + } + + @Test + void testMalformedUnidentifiedRequest() { + Response response = resources.getJerseyTest() + .target(String.format("/v2/keys/%s/1", EXISTS_UUID)) + .request() + .header(OptionalAccess.UNIDENTIFIED, "$$$$$$$$$") + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + verifyNoMoreInteractions(KEYS); + } + + + @Test + void validMultiRequestTestV2() { + when(KEYS.takeEC(EXISTS_UUID, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY))); + when(KEYS.takeEC(EXISTS_UUID, 2)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY2))); + when(KEYS.takeEC(EXISTS_UUID, 3)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY3))); + when(KEYS.takeEC(EXISTS_UUID, 4)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY4))); + + PreKeyResponse results = resources.getJerseyTest() + .target(String.format("/v2/keys/%s/*", EXISTS_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(PreKeyResponse.class); + + assertThat(results.getDevicesCount()).isEqualTo(3); + assertThat(results.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey(IdentityType.ACI)); + + ECSignedPreKey signedPreKey = results.getDevice(1).getSignedPreKey(); + ECPreKey preKey = results.getDevice(1).getPreKey(); + long registrationId = results.getDevice(1).getRegistrationId(); + long deviceId = results.getDevice(1).getDeviceId(); + + assertEquals(SAMPLE_KEY, preKey); + assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID); + assertEquals(SAMPLE_SIGNED_KEY, signedPreKey); + assertThat(deviceId).isEqualTo(1); + + signedPreKey = results.getDevice(2).getSignedPreKey(); + preKey = results.getDevice(2).getPreKey(); + registrationId = results.getDevice(2).getRegistrationId(); + deviceId = results.getDevice(2).getDeviceId(); + + assertEquals(SAMPLE_KEY2, preKey); + assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID2); + assertEquals(SAMPLE_SIGNED_KEY2, signedPreKey); + assertThat(deviceId).isEqualTo(2); + + signedPreKey = results.getDevice(4).getSignedPreKey(); + preKey = results.getDevice(4).getPreKey(); + registrationId = results.getDevice(4).getRegistrationId(); + deviceId = results.getDevice(4).getDeviceId(); + + assertEquals(SAMPLE_KEY4, preKey); + assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID4); + assertThat(signedPreKey).isNull(); + assertThat(deviceId).isEqualTo(4); + + verify(KEYS).takeEC(EXISTS_UUID, 1); + verify(KEYS).takeEC(EXISTS_UUID, 2); + verify(KEYS).takeEC(EXISTS_UUID, 4); + verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 1); + verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 2); + verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 4); + verifyNoMoreInteractions(KEYS); + } + + @Test + void validMultiRequestPqTestV2() { + when(KEYS.takeEC(any(), anyLong())).thenReturn(CompletableFuture.completedFuture(Optional.empty())); + when(KEYS.takePQ(any(), anyLong())).thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + when(KEYS.takeEC(EXISTS_UUID, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY))); + when(KEYS.takeEC(EXISTS_UUID, 3)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY3))); + when(KEYS.takeEC(EXISTS_UUID, 4)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_KEY4))); + when(KEYS.takePQ(EXISTS_UUID, 1)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_PQ_KEY))); + when(KEYS.takePQ(EXISTS_UUID, 2)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_PQ_KEY2))); + when(KEYS.takePQ(EXISTS_UUID, 3)).thenReturn(CompletableFuture.completedFuture(Optional.of(SAMPLE_PQ_KEY3))); + + PreKeyResponse results = resources.getJerseyTest() + .target(String.format("/v2/keys/%s/*", EXISTS_UUID)) + .queryParam("pq", "true") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(PreKeyResponse.class); + + assertThat(results.getDevicesCount()).isEqualTo(3); + assertThat(results.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey(IdentityType.ACI)); + + ECSignedPreKey signedPreKey = results.getDevice(1).getSignedPreKey(); + ECPreKey preKey = results.getDevice(1).getPreKey(); + KEMSignedPreKey pqPreKey = results.getDevice(1).getPqPreKey(); + long registrationId = results.getDevice(1).getRegistrationId(); + long deviceId = results.getDevice(1).getDeviceId(); + + assertEquals(SAMPLE_KEY, preKey); + assertEquals(SAMPLE_PQ_KEY, pqPreKey); + assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID); + assertEquals(SAMPLE_SIGNED_KEY, signedPreKey); + assertThat(deviceId).isEqualTo(1); + + signedPreKey = results.getDevice(2).getSignedPreKey(); + preKey = results.getDevice(2).getPreKey(); + pqPreKey = results.getDevice(2).getPqPreKey(); + registrationId = results.getDevice(2).getRegistrationId(); + deviceId = results.getDevice(2).getDeviceId(); + + assertThat(preKey).isNull(); + assertEquals(SAMPLE_PQ_KEY2, pqPreKey); + assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID2); + assertEquals(SAMPLE_SIGNED_KEY2, signedPreKey); + assertThat(deviceId).isEqualTo(2); + + signedPreKey = results.getDevice(4).getSignedPreKey(); + preKey = results.getDevice(4).getPreKey(); + pqPreKey = results.getDevice(4).getPqPreKey(); + registrationId = results.getDevice(4).getRegistrationId(); + deviceId = results.getDevice(4).getDeviceId(); + + assertEquals(SAMPLE_KEY4, preKey); + assertThat(pqPreKey).isNull(); + assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID4); + assertThat(signedPreKey).isNull(); + assertThat(deviceId).isEqualTo(4); + + verify(KEYS).takeEC(EXISTS_UUID, 1); + verify(KEYS).takePQ(EXISTS_UUID, 1); + verify(KEYS).takeEC(EXISTS_UUID, 2); + verify(KEYS).takePQ(EXISTS_UUID, 2); + verify(KEYS).takeEC(EXISTS_UUID, 4); + verify(KEYS).takePQ(EXISTS_UUID, 4); + verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 1); + verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 2); + verify(KEYS).getEcSignedPreKey(EXISTS_UUID, 4); + verifyNoMoreInteractions(KEYS); + } + + + @Test + void invalidRequestTestV2() { + Response response = resources.getJerseyTest() + .target(String.format("/v2/keys/%s", NOT_EXISTS_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(); + + assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404); + } + + @Test + void anotherInvalidRequestTestV2() { + Response response = resources.getJerseyTest() + .target(String.format("/v2/keys/%s/22", EXISTS_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(); + + assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404); + } + + @Test + void unauthorizedRequestTestV2() { + Response response = + resources.getJerseyTest() + .target(String.format("/v2/keys/%s/1", EXISTS_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD)) + .get(); + + assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401); + + response = + resources.getJerseyTest() + .target(String.format("/v2/keys/%s/1", EXISTS_UUID)) + .request() + .get(); + + assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401); + } + + @Test + void putKeysTestV2() { + final ECPreKey preKey = KeysHelper.ecPreKey(31337); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + + PreKeyState preKeyState = new PreKeyState(identityKey, signedPreKey, List.of(preKey)); + + Response response = + resources.getJerseyTest() + .target("/v2/keys") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(204); + + ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class); + verify(KEYS).store(eq(AuthHelper.VALID_UUID), eq(1L), listCaptor.capture(), isNull(), eq(signedPreKey), isNull()); + + assertThat(listCaptor.getValue()).containsExactly(preKey); + + verify(AuthHelper.VALID_ACCOUNT).setIdentityKey(eq(identityKey)); + verify(AuthHelper.VALID_DEVICE).setSignedPreKey(eq(signedPreKey)); + verify(accounts).update(eq(AuthHelper.VALID_ACCOUNT), any()); + } + + @Test + void putKeysPqTestV2() { + final ECPreKey preKey = KeysHelper.ecPreKey(31337); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair); + final KEMSignedPreKey pqPreKey = KeysHelper.signedKEMPreKey(31339, identityKeyPair); + final KEMSignedPreKey pqLastResortPreKey = KeysHelper.signedKEMPreKey(31340, identityKeyPair); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + + PreKeyState preKeyState = new PreKeyState(identityKey, signedPreKey, List.of(preKey), List.of(pqPreKey), pqLastResortPreKey); + + Response response = + resources.getJerseyTest() + .target("/v2/keys") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(204); + + ArgumentCaptor> ecCaptor = ArgumentCaptor.forClass(List.class); + ArgumentCaptor> pqCaptor = ArgumentCaptor.forClass(List.class); + verify(KEYS).store(eq(AuthHelper.VALID_UUID), eq(1L), ecCaptor.capture(), pqCaptor.capture(), eq(signedPreKey), eq(pqLastResortPreKey)); + + assertThat(ecCaptor.getValue()).containsExactly(preKey); + assertThat(pqCaptor.getValue()).containsExactly(pqPreKey); + + verify(AuthHelper.VALID_ACCOUNT).setIdentityKey(eq(identityKey)); + verify(AuthHelper.VALID_DEVICE).setSignedPreKey(eq(signedPreKey)); + verify(accounts).update(eq(AuthHelper.VALID_ACCOUNT), any()); + } + + @Test + void putKeysStructurallyInvalidSignedECKey() { + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + final KEMSignedPreKey wrongPreKey = KeysHelper.signedKEMPreKey(1, identityKeyPair); + final WeaklyTypedPreKeyState preKeyState = + new WeaklyTypedPreKeyState(null, WeaklyTypedSignedPreKey.fromSignedPreKey(wrongPreKey), null, null, identityKey.serialize()); + + Response response = + resources.getJerseyTest() + .target("/v2/keys") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(400); + } + + @Test + void putKeysStructurallyInvalidUnsignedECKey() { + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + final WeaklyTypedPreKey wrongPreKey = new WeaklyTypedPreKey(1, "cluck cluck i'm a parrot".getBytes()); + final WeaklyTypedPreKeyState preKeyState = + new WeaklyTypedPreKeyState(List.of(wrongPreKey), null, null, null, identityKey.serialize()); + + Response response = + resources.getJerseyTest() + .target("/v2/keys") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(400); + } + + @Test + void putKeysStructurallyInvalidPQOneTimeKey() { + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + final WeaklyTypedSignedPreKey wrongPreKey = WeaklyTypedSignedPreKey.fromSignedPreKey(KeysHelper.signedECPreKey(1, identityKeyPair)); + final WeaklyTypedPreKeyState preKeyState = + new WeaklyTypedPreKeyState(null, null, List.of(wrongPreKey), null, identityKey.serialize()); + + Response response = + resources.getJerseyTest() + .target("/v2/keys") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(400); + } + + @Test + void putKeysStructurallyInvalidPQLastResortKey() { + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + final WeaklyTypedSignedPreKey wrongPreKey = WeaklyTypedSignedPreKey.fromSignedPreKey(KeysHelper.signedECPreKey(1, identityKeyPair)); + final WeaklyTypedPreKeyState preKeyState = + new WeaklyTypedPreKeyState(null, null, null, wrongPreKey, identityKey.serialize()); + + Response response = + resources.getJerseyTest() + .target("/v2/keys") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(400); + } + + @Test + void putKeysByPhoneNumberIdentifierTestV2() { + final ECPreKey preKey = KeysHelper.ecPreKey(31337); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + + PreKeyState preKeyState = new PreKeyState(identityKey, signedPreKey, List.of(preKey)); + + Response response = + resources.getJerseyTest() + .target("/v2/keys") + .queryParam("identity", "pni") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(204); + + ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class); + verify(KEYS).store(eq(AuthHelper.VALID_PNI), eq(1L), listCaptor.capture(), isNull(), eq(signedPreKey), isNull()); + + assertThat(listCaptor.getValue()).containsExactly(preKey); + + verify(AuthHelper.VALID_ACCOUNT).setPhoneNumberIdentityKey(eq(identityKey)); + verify(AuthHelper.VALID_DEVICE).setPhoneNumberIdentitySignedPreKey(eq(signedPreKey)); + verify(accounts).update(eq(AuthHelper.VALID_ACCOUNT), any()); + } + + @Test + void putKeysByPhoneNumberIdentifierPqTestV2() { + final ECPreKey preKey = KeysHelper.ecPreKey(31337); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair); + final KEMSignedPreKey pqPreKey = KeysHelper.signedKEMPreKey(31339, identityKeyPair); + final KEMSignedPreKey pqLastResortPreKey = KeysHelper.signedKEMPreKey(31340, identityKeyPair); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + + PreKeyState preKeyState = new PreKeyState(identityKey, signedPreKey, List.of(preKey), List.of(pqPreKey), pqLastResortPreKey); + + Response response = + resources.getJerseyTest() + .target("/v2/keys") + .queryParam("identity", "pni") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(204); + + ArgumentCaptor> ecCaptor = ArgumentCaptor.forClass(List.class); + ArgumentCaptor> pqCaptor = ArgumentCaptor.forClass(List.class); + verify(KEYS).store(eq(AuthHelper.VALID_PNI), eq(1L), ecCaptor.capture(), pqCaptor.capture(), eq(signedPreKey), eq(pqLastResortPreKey)); + + assertThat(ecCaptor.getValue()).containsExactly(preKey); + assertThat(pqCaptor.getValue()).containsExactly(pqPreKey); + + verify(AuthHelper.VALID_ACCOUNT).setPhoneNumberIdentityKey(eq(identityKey)); + verify(AuthHelper.VALID_DEVICE).setPhoneNumberIdentitySignedPreKey(eq(signedPreKey)); + verify(accounts).update(eq(AuthHelper.VALID_ACCOUNT), any()); + } + + @Test + void putPrekeyWithInvalidSignature() { + final ECSignedPreKey badSignedPreKey = KeysHelper.signedECPreKey(1, Curve.generateKeyPair()); + PreKeyState preKeyState = new PreKeyState(IDENTITY_KEY, badSignedPreKey, List.of()); + Response response = + resources.getJerseyTest() + .target("/v2/keys") + .queryParam("identity", "aci") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(422); + } + + @Test + void disabledPutKeysTestV2() { + final ECPreKey preKey = KeysHelper.ecPreKey(31337); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, identityKeyPair); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + + PreKeyState preKeyState = new PreKeyState(identityKey, signedPreKey, List.of(preKey)); + + Response response = + resources.getJerseyTest() + .target("/v2/keys") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(204); + + ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class); + verify(KEYS).store(eq(AuthHelper.DISABLED_UUID), eq(1L), listCaptor.capture(), isNull(), eq(signedPreKey), isNull()); + + List capturedList = listCaptor.getValue(); + assertThat(capturedList.size()).isEqualTo(1); + assertThat(capturedList.get(0).keyId()).isEqualTo(31337); + assertThat(capturedList.get(0).publicKey()).isEqualTo(preKey.publicKey()); + + verify(AuthHelper.DISABLED_ACCOUNT).setIdentityKey(eq(identityKey)); + verify(AuthHelper.DISABLED_DEVICE).setSignedPreKey(eq(signedPreKey)); + verify(accounts).update(eq(AuthHelper.DISABLED_ACCOUNT), any()); + } + + @Test + void putIdentityKeyNonPrimary() { + final ECPreKey preKey = KeysHelper.ecPreKey(31337); + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(31338, IDENTITY_KEY_PAIR); + + List preKeys = List.of(preKey); + + PreKeyState preKeyState = new PreKeyState(IDENTITY_KEY, signedPreKey, preKeys); + + Response response = + resources.getJerseyTest() + .target("/v2/keys") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_3, 2L, AuthHelper.VALID_PASSWORD_3_LINKED)) + .put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(403); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java new file mode 100644 index 000000000..a4d84d3cd --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java @@ -0,0 +1,1471 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.asJson; +import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.jsonFixture; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.ByteString; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.hamcrest.Matcher; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.OptionalAccess; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicInboundMessageByteLimitConfiguration; +import org.whispersystems.textsecuregcm.entities.AccountMismatchedDevices; +import org.whispersystems.textsecuregcm.entities.AccountStaleDevices; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.IncomingMessage; +import org.whispersystems.textsecuregcm.entities.IncomingMessageList; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope; +import org.whispersystems.textsecuregcm.entities.MismatchedDevices; +import org.whispersystems.textsecuregcm.entities.MultiRecipientMessage; +import org.whispersystems.textsecuregcm.entities.MultiRecipientMessage.Recipient; +import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; +import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList; +import org.whispersystems.textsecuregcm.entities.SendMultiRecipientMessageResponse; +import org.whispersystems.textsecuregcm.entities.SpamReport; +import org.whispersystems.textsecuregcm.entities.StaleDevices; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.limits.CardinalityEstimator; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.providers.MultiRecipientMessageProvider; +import org.whispersystems.textsecuregcm.push.MessageSender; +import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; +import org.whispersystems.textsecuregcm.push.PushNotificationManager; +import org.whispersystems.textsecuregcm.push.ReceiptSender; +import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.storage.ReportMessageManager; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.UUIDUtil; +import org.whispersystems.websocket.Stories; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +@ExtendWith(DropwizardExtensionsSupport.class) +class MessageControllerTest { + + private static final String SINGLE_DEVICE_RECIPIENT = "+14151111111"; + private static final UUID SINGLE_DEVICE_UUID = UUID.fromString("11111111-1111-1111-1111-111111111111"); + private static final UUID SINGLE_DEVICE_PNI = UUID.fromString("11111111-0000-0000-0000-111111111111"); + private static final int SINGLE_DEVICE_ID1 = 1; + private static final int SINGLE_DEVICE_REG_ID1 = 111; + + private static final String MULTI_DEVICE_RECIPIENT = "+14152222222"; + private static final UUID MULTI_DEVICE_UUID = UUID.fromString("22222222-2222-2222-2222-222222222222"); + private static final UUID MULTI_DEVICE_PNI = UUID.fromString("22222222-0000-0000-0000-222222222222"); + private static final int MULTI_DEVICE_ID1 = 1; + private static final int MULTI_DEVICE_ID2 = 2; + private static final int MULTI_DEVICE_ID3 = 3; + private static final int MULTI_DEVICE_REG_ID1 = 222; + private static final int MULTI_DEVICE_REG_ID2 = 333; + private static final int MULTI_DEVICE_REG_ID3 = 444; + + private static final byte[] UNIDENTIFIED_ACCESS_BYTES = "0123456789abcdef".getBytes(); + + private static final String INTERNATIONAL_RECIPIENT = "+61123456789"; + private static final UUID INTERNATIONAL_UUID = UUID.fromString("33333333-3333-3333-3333-333333333333"); + + @SuppressWarnings("unchecked") + private static final RedisAdvancedClusterCommands redisCommands = mock(RedisAdvancedClusterCommands.class); + + private static final MessageSender messageSender = mock(MessageSender.class); + private static final ReceiptSender receiptSender = mock(ReceiptSender.class); + private static final AccountsManager accountsManager = mock(AccountsManager.class); + private static final MessagesManager messagesManager = mock(MessagesManager.class); + private static final RateLimiters rateLimiters = mock(RateLimiters.class); + private static final CardinalityEstimator cardinalityEstimator = mock(CardinalityEstimator.class); + private static final RateLimiter rateLimiter = mock(RateLimiter.class); + private static final PushNotificationManager pushNotificationManager = mock(PushNotificationManager.class); + private static final ReportMessageManager reportMessageManager = mock(ReportMessageManager.class); + private static final ExecutorService multiRecipientMessageExecutor = mock(ExecutorService.class); + private static final Scheduler messageDeliveryScheduler = Schedulers.newBoundedElastic(10, 10_000, "messageDelivery"); + private static final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .addProvider(RateLimitExceededExceptionMapper.class) + .addProvider(MultiRecipientMessageProvider.class) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource( + new MessageController(rateLimiters, cardinalityEstimator, messageSender, receiptSender, accountsManager, + messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor, + messageDeliveryScheduler, ReportSpamTokenProvider.noop(), mock(ClientReleaseManager.class), dynamicConfigurationManager)) + .build(); + + @BeforeEach + void setup() { + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + + + + final List singleDeviceList = List.of( + generateTestDevice(SINGLE_DEVICE_ID1, SINGLE_DEVICE_REG_ID1, 1111, KeysHelper.signedECPreKey(333, identityKeyPair), System.currentTimeMillis(), System.currentTimeMillis()) + ); + + final List multiDeviceList = List.of( + generateTestDevice(MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1, 2222, KeysHelper.signedECPreKey(111, identityKeyPair), System.currentTimeMillis(), System.currentTimeMillis()), + generateTestDevice(MULTI_DEVICE_ID2, MULTI_DEVICE_REG_ID2, 3333, KeysHelper.signedECPreKey(222, identityKeyPair), System.currentTimeMillis(), System.currentTimeMillis()), + generateTestDevice(MULTI_DEVICE_ID3, MULTI_DEVICE_REG_ID3, 4444, null, System.currentTimeMillis(), System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31)) + ); + + Account singleDeviceAccount = AccountsHelper.generateTestAccount(SINGLE_DEVICE_RECIPIENT, SINGLE_DEVICE_UUID, SINGLE_DEVICE_PNI, singleDeviceList, UNIDENTIFIED_ACCESS_BYTES); + Account multiDeviceAccount = AccountsHelper.generateTestAccount(MULTI_DEVICE_RECIPIENT, MULTI_DEVICE_UUID, MULTI_DEVICE_PNI, multiDeviceList, UNIDENTIFIED_ACCESS_BYTES); + Account internationalAccount = AccountsHelper.generateTestAccount(INTERNATIONAL_RECIPIENT, INTERNATIONAL_UUID, + UUID.randomUUID(), singleDeviceList, UNIDENTIFIED_ACCESS_BYTES); + + when(accountsManager.getByServiceIdentifier(new AciServiceIdentifier(SINGLE_DEVICE_UUID))).thenReturn(Optional.of(singleDeviceAccount)); + when(accountsManager.getByServiceIdentifier(new PniServiceIdentifier(SINGLE_DEVICE_PNI))).thenReturn(Optional.of(singleDeviceAccount)); + when(accountsManager.getByServiceIdentifier(new AciServiceIdentifier(MULTI_DEVICE_UUID))).thenReturn(Optional.of(multiDeviceAccount)); + when(accountsManager.getByServiceIdentifier(new PniServiceIdentifier(MULTI_DEVICE_PNI))).thenReturn(Optional.of(multiDeviceAccount)); + when(accountsManager.getByServiceIdentifier(new AciServiceIdentifier(INTERNATIONAL_UUID))).thenReturn(Optional.of(internationalAccount)); + + final DynamicInboundMessageByteLimitConfiguration inboundMessageByteLimitConfiguration = + mock(DynamicInboundMessageByteLimitConfiguration.class); + + when(inboundMessageByteLimitConfiguration.enforceInboundLimit()).thenReturn(false); + + final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); + when(dynamicConfiguration.getInboundMessageByteLimitConfiguration()).thenReturn(inboundMessageByteLimitConfiguration); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + + when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter); + when(rateLimiters.getStoriesLimiter()).thenReturn(rateLimiter); + when(rateLimiters.getInboundMessageBytes()).thenReturn(rateLimiter); + } + + private static Device generateTestDevice(final long id, final int registrationId, final int pniRegistrationId, final ECSignedPreKey signedPreKey, final long createdAt, final long lastSeen) { + final Device device = new Device(); + device.setId(id); + device.setRegistrationId(registrationId); + device.setPhoneNumberIdentityRegistrationId(pniRegistrationId); + device.setSignedPreKey(signedPreKey); + device.setCreated(createdAt); + device.setLastSeen(lastSeen); + device.setGcmId("isgcm"); + + return device; + } + + @AfterEach + void teardown() { + reset( + redisCommands, + messageSender, + receiptSender, + accountsManager, + messagesManager, + rateLimiters, + rateLimiter, + cardinalityEstimator, + pushNotificationManager, + reportMessageManager, + multiRecipientMessageExecutor + ); + } + + @AfterAll + static void teardownAll() { + messageDeliveryScheduler.dispose(); + } + + @Test + void testSendFromDisabledAccount() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", SINGLE_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_single_device.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Unauthorized response", response.getStatus(), is(equalTo(401))); + } + + @Test + void testSingleDeviceCurrent() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", SINGLE_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_single_device.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response", response.getStatus(), is(equalTo(200))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Envelope.class); + verify(messageSender, times(1)).sendMessage(any(Account.class), any(Device.class), captor.capture(), eq(false)); + + assertTrue(captor.getValue().hasSourceUuid()); + assertTrue(captor.getValue().hasSourceDevice()); + assertTrue(captor.getValue().getUrgent()); + } + + @Test + void testSingleDeviceCurrentNotUrgent() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", SINGLE_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_single_device_not_urgent.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response", response.getStatus(), is(equalTo(200))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Envelope.class); + verify(messageSender, times(1)).sendMessage(any(Account.class), any(Device.class), captor.capture(), eq(false)); + + assertTrue(captor.getValue().hasSourceUuid()); + assertTrue(captor.getValue().hasSourceDevice()); + assertFalse(captor.getValue().getUrgent()); + } + + @Test + void testSingleDeviceCurrentByPni() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/PNI:%s", SINGLE_DEVICE_PNI)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_single_device.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response", response.getStatus(), is(equalTo(200))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Envelope.class); + verify(messageSender, times(1)).sendMessage(any(Account.class), any(Device.class), captor.capture(), eq(false)); + + assertTrue(captor.getValue().hasSourceUuid()); + assertTrue(captor.getValue().hasSourceDevice()); + } + + @Test + void testNullMessageInList() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", SINGLE_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_null_message_in_list.json"), IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Bad request", response.getStatus(), is(equalTo(422))); + } + + @Test + void testSingleDeviceCurrentUnidentified() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", SINGLE_DEVICE_UUID)) + .request() + .header(OptionalAccess.UNIDENTIFIED, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_single_device.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response", response.getStatus(), is(equalTo(200))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Envelope.class); + verify(messageSender, times(1)).sendMessage(any(Account.class), any(Device.class), captor.capture(), eq(false)); + + assertFalse(captor.getValue().hasSourceUuid()); + assertFalse(captor.getValue().hasSourceDevice()); + } + + @Test + void testSendBadAuth() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", SINGLE_DEVICE_UUID)) + .request() + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_single_device.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response", response.getStatus(), is(equalTo(401))); + } + + @Test + void testMultiDeviceMissing() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", MULTI_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_single_device.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(409))); + + assertThat("Good Response Body", + asJson(response.readEntity(MismatchedDevices.class)), + is(equalTo(jsonFixture("fixtures/missing_device_response.json")))); + + verifyNoMoreInteractions(messageSender); + } + + @Test + void testMultiDeviceExtra() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", MULTI_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_extra_device.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(409))); + + assertThat("Good Response Body", + asJson(response.readEntity(MismatchedDevices.class)), + is(equalTo(jsonFixture("fixtures/missing_device_response2.json")))); + + verifyNoMoreInteractions(messageSender); + } + + @Test + void testMultiDeviceDuplicate() throws Exception { + Response response = resources.getJerseyTest() + .target(String.format("/v1/messages/%s", MULTI_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_duplicate_device.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(422))); + + verifyNoMoreInteractions(messageSender); + } + + @Test + void testMultiDevice() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", MULTI_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_multi_device.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(200))); + + final ArgumentCaptor envelopeCaptor = ArgumentCaptor.forClass(Envelope.class); + + verify(messageSender, times(2)).sendMessage(any(Account.class), any(Device.class), envelopeCaptor.capture(), eq(false)); + + envelopeCaptor.getAllValues().forEach(envelope -> assertTrue(envelope.getUrgent())); + } + + @Test + void testMultiDeviceNotUrgent() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", MULTI_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_multi_device_not_urgent.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(200))); + + final ArgumentCaptor envelopeCaptor = ArgumentCaptor.forClass(Envelope.class); + + verify(messageSender, times(2)).sendMessage(any(Account.class), any(Device.class), envelopeCaptor.capture(), eq(false)); + + envelopeCaptor.getAllValues().forEach(envelope -> assertFalse(envelope.getUrgent())); + } + + @Test + void testMultiDeviceByPni() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/PNI:%s", MULTI_DEVICE_PNI)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_multi_device_pni.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(200))); + + verify(messageSender, times(2)).sendMessage(any(Account.class), any(Device.class), any(Envelope.class), eq(false)); + } + + @Test + void testRegistrationIdMismatch() throws Exception { + Response response = + resources.getJerseyTest().target(String.format("/v1/messages/%s", MULTI_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_registration_id.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(410))); + + assertThat("Good Response Body", + asJson(response.readEntity(StaleDevices.class)), + is(equalTo(jsonFixture("fixtures/mismatched_registration_id.json")))); + + verifyNoMoreInteractions(messageSender); + } + + @ParameterizedTest + @MethodSource + void testGetMessages(boolean receiveStories) { + + final long timestampOne = 313377; + final long timestampTwo = 313388; + + final UUID messageGuidOne = UUID.randomUUID(); + final UUID messageGuidTwo = UUID.randomUUID(); + final UUID sourceUuid = UUID.randomUUID(); + + final UUID updatedPniOne = UUID.randomUUID(); + + List envelopes = List.of( + generateEnvelope(messageGuidOne, Envelope.Type.CIPHERTEXT_VALUE, timestampOne, sourceUuid, 2, + AuthHelper.VALID_UUID, updatedPniOne, "hi there".getBytes(), 0, false), + generateEnvelope(messageGuidTwo, Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE, timestampTwo, sourceUuid, 2, + AuthHelper.VALID_UUID, null, null, 0, true) + ); + + when(messagesManager.getMessagesForDevice(eq(AuthHelper.VALID_UUID), eq(1L), anyBoolean())) + .thenReturn(Mono.just(new Pair<>(envelopes, false))); + + final String userAgent = "Test-UA"; + + OutgoingMessageEntityList response = + resources.getJerseyTest().target("/v1/messages/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .header(Stories.X_SIGNAL_RECEIVE_STORIES, receiveStories ? "true" : "false") + .header(HttpHeaders.USER_AGENT, userAgent) + .accept(MediaType.APPLICATION_JSON_TYPE) + .get(OutgoingMessageEntityList.class); + + List messages = response.messages(); + int expectedSize = receiveStories ? 2 : 1; + assertEquals(expectedSize, messages.size()); + + OutgoingMessageEntity first = messages.get(0); + assertEquals(first.timestamp(), timestampOne); + assertEquals(first.guid(), messageGuidOne); + assertEquals(first.sourceUuid().uuid(), sourceUuid); + assertEquals(updatedPniOne, first.updatedPni()); + + if (receiveStories) { + OutgoingMessageEntity second = messages.get(1); + assertEquals(second.timestamp(), timestampTwo); + assertEquals(second.guid(), messageGuidTwo); + assertEquals(second.sourceUuid().uuid(), sourceUuid); + assertNull(second.updatedPni()); + } + + verify(pushNotificationManager).handleMessagesRetrieved(AuthHelper.VALID_ACCOUNT, AuthHelper.VALID_DEVICE, userAgent); + } + + private static Stream testGetMessages() { + return Stream.of( + Arguments.of(true), + Arguments.of(false) + ); + } + + @Test + void testGetMessagesBadAuth() { + final long timestampOne = 313377; + final long timestampTwo = 313388; + + final List messages = List.of( + generateEnvelope(UUID.randomUUID(), Envelope.Type.CIPHERTEXT_VALUE, timestampOne, UUID.randomUUID(), 2, + AuthHelper.VALID_UUID, null, "hi there".getBytes(), 0), + generateEnvelope(UUID.randomUUID(), Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE, timestampTwo, + UUID.randomUUID(), 2, AuthHelper.VALID_UUID, null, null, 0) + ); + + when(messagesManager.getMessagesForDevice(eq(AuthHelper.VALID_UUID), eq(1L), anyBoolean())) + .thenReturn(Mono.just(new Pair<>(messages, false))); + + Response response = + resources.getJerseyTest().target("/v1/messages/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD)) + .accept(MediaType.APPLICATION_JSON_TYPE) + .get(); + + assertThat("Unauthorized response", response.getStatus(), is(equalTo(401))); + } + + @Test + void testDeleteMessages() { + long timestamp = System.currentTimeMillis(); + + UUID sourceUuid = UUID.randomUUID(); + + UUID uuid1 = UUID.randomUUID(); + when(messagesManager.delete(AuthHelper.VALID_UUID, 1, uuid1, null)) + .thenReturn( + CompletableFuture.completedFuture(Optional.of(generateEnvelope(uuid1, Envelope.Type.CIPHERTEXT_VALUE, + timestamp, sourceUuid, 1, AuthHelper.VALID_UUID, null, "hi".getBytes(), 0)))); + + UUID uuid2 = UUID.randomUUID(); + when(messagesManager.delete(AuthHelper.VALID_UUID, 1, uuid2, null)) + .thenReturn( + CompletableFuture.completedFuture(Optional.of(generateEnvelope( + uuid2, Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE, + System.currentTimeMillis(), sourceUuid, 1, AuthHelper.VALID_UUID, null, null, 0)))); + + UUID uuid3 = UUID.randomUUID(); + when(messagesManager.delete(AuthHelper.VALID_UUID, 1, uuid3, null)) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + UUID uuid4 = UUID.randomUUID(); + when(messagesManager.delete(AuthHelper.VALID_UUID, 1, uuid4, null)) + .thenReturn(CompletableFuture.failedFuture(new RuntimeException("Oh No"))); + + Response response = resources.getJerseyTest() + .target(String.format("/v1/messages/uuid/%s", uuid1)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .delete(); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(204))); + verify(receiptSender).sendReceipt(eq(new AciServiceIdentifier(AuthHelper.VALID_UUID)), eq(1L), + eq(new AciServiceIdentifier(sourceUuid)), eq(timestamp)); + + response = resources.getJerseyTest() + .target(String.format("/v1/messages/uuid/%s", uuid2)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .delete(); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(204))); + verifyNoMoreInteractions(receiptSender); + + response = resources.getJerseyTest() + .target(String.format("/v1/messages/uuid/%s", uuid3)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .delete(); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(204))); + verifyNoMoreInteractions(receiptSender); + + response = resources.getJerseyTest() + .target(String.format("/v1/messages/uuid/%s", uuid4)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .delete(); + + assertThat("Bad Response Code", response.getStatus(), is(equalTo(500))); + verifyNoMoreInteractions(receiptSender); + + } + + @Test + void testReportMessageByE164() { + + final String senderNumber = "+12125550001"; + final UUID senderAci = UUID.randomUUID(); + final UUID senderPni = UUID.randomUUID(); + final String userAgent = "user-agent"; + UUID messageGuid = UUID.randomUUID(); + + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(senderAci); + when(account.getNumber()).thenReturn(senderNumber); + when(account.getPhoneNumberIdentifier()).thenReturn(senderPni); + + when(accountsManager.getByE164(senderNumber)).thenReturn(Optional.of(account)); + when(accountsManager.findRecentlyDeletedAccountIdentifier(senderNumber)).thenReturn(Optional.of(senderAci)); + when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni); + + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/report/%s/%s", senderNumber, messageGuid)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.USER_AGENT, userAgent) + .post(null); + + assertThat(response.getStatus(), is(equalTo(202))); + + verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni), + messageGuid, AuthHelper.VALID_UUID, Optional.empty(), userAgent); + verify(accountsManager, never()).findRecentlyDeletedE164(any(UUID.class)); + verify(accountsManager, never()).getPhoneNumberIdentifier(anyString()); + + when(accountsManager.getByE164(senderNumber)).thenReturn(Optional.empty()); + messageGuid = UUID.randomUUID(); + + response = + resources.getJerseyTest() + .target(String.format("/v1/messages/report/%s/%s", senderNumber, messageGuid)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.USER_AGENT, userAgent) + .post(null); + + assertThat(response.getStatus(), is(equalTo(202))); + + verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni), + messageGuid, AuthHelper.VALID_UUID, Optional.empty(), userAgent); + } + + @Test + void testReportMessageByAci() { + + final String senderNumber = "+12125550001"; + final UUID senderAci = UUID.randomUUID(); + final UUID senderPni = UUID.randomUUID(); + final String userAgent = "user-agent"; + UUID messageGuid = UUID.randomUUID(); + + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(senderAci); + when(account.getNumber()).thenReturn(senderNumber); + when(account.getPhoneNumberIdentifier()).thenReturn(senderPni); + + when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account)); + when(accountsManager.findRecentlyDeletedE164(senderAci)).thenReturn(Optional.of(senderNumber)); + when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni); + + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/report/%s/%s", senderAci, messageGuid)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.USER_AGENT, userAgent) + .post(null); + + assertThat(response.getStatus(), is(equalTo(202))); + + verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni), + messageGuid, AuthHelper.VALID_UUID, Optional.empty(), userAgent); + verify(accountsManager, never()).findRecentlyDeletedE164(any(UUID.class)); + verify(accountsManager, never()).getPhoneNumberIdentifier(anyString()); + + when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.empty()); + + messageGuid = UUID.randomUUID(); + + response = + resources.getJerseyTest() + .target(String.format("/v1/messages/report/%s/%s", senderAci, messageGuid)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.USER_AGENT, userAgent) + .post(null); + + assertThat(response.getStatus(), is(equalTo(202))); + + verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni), + messageGuid, AuthHelper.VALID_UUID, Optional.empty(), userAgent); + } + + @Test + void testReportMessageByAciWithSpamReportToken() { + + final String senderNumber = "+12125550001"; + final UUID senderAci = UUID.randomUUID(); + final UUID senderPni = UUID.randomUUID(); + UUID messageGuid = UUID.randomUUID(); + + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(senderAci); + when(account.getNumber()).thenReturn(senderNumber); + when(account.getPhoneNumberIdentifier()).thenReturn(senderPni); + + when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account)); + when(accountsManager.findRecentlyDeletedE164(senderAci)).thenReturn(Optional.of(senderNumber)); + when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni); + + Entity entity = Entity.entity(new SpamReport(new byte[3]), "application/json"); + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/report/%s/%s", senderAci, messageGuid)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(entity); + + assertThat(response.getStatus(), is(equalTo(202))); + verify(reportMessageManager).report(eq(Optional.of(senderNumber)), + eq(Optional.of(senderAci)), + eq(Optional.of(senderPni)), + eq(messageGuid), + eq(AuthHelper.VALID_UUID), + argThat(maybeBytes -> maybeBytes.map(bytes -> Arrays.equals(bytes, new byte[3])).orElse(false)), + any()); + verify(accountsManager, never()).findRecentlyDeletedE164(any(UUID.class)); + verify(accountsManager, never()).getPhoneNumberIdentifier(anyString()); + when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.empty()); + + messageGuid = UUID.randomUUID(); + + entity = Entity.entity(new SpamReport(new byte[5]), "application/json"); + response = + resources.getJerseyTest() + .target(String.format("/v1/messages/report/%s/%s", senderAci, messageGuid)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(entity); + + assertThat(response.getStatus(), is(equalTo(202))); + verify(reportMessageManager).report(eq(Optional.of(senderNumber)), + eq(Optional.of(senderAci)), + eq(Optional.of(senderPni)), + eq(messageGuid), + eq(AuthHelper.VALID_UUID), + argThat(maybeBytes -> maybeBytes.map(bytes -> Arrays.equals(bytes, new byte[5])).orElse(false)), + any()); + } + + @ParameterizedTest + @MethodSource + void testReportMessageByAciWithNullSpamReportToken(Entity entity, boolean expectOk) { + + final String senderNumber = "+12125550001"; + final UUID senderAci = UUID.randomUUID(); + final UUID senderPni = UUID.randomUUID(); + UUID messageGuid = UUID.randomUUID(); + + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(senderAci); + when(account.getNumber()).thenReturn(senderNumber); + when(account.getPhoneNumberIdentifier()).thenReturn(senderPni); + + when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account)); + when(accountsManager.findRecentlyDeletedE164(senderAci)).thenReturn(Optional.of(senderNumber)); + when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni); + + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/report/%s/%s", senderAci, messageGuid)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .post(entity); + + Matcher matcher = expectOk ? is(equalTo(202)) : not(equalTo(202)); + assertThat(response.getStatus(), matcher); + } + + private static Stream testReportMessageByAciWithNullSpamReportToken() { + return Stream.of( + Arguments.of(Entity.json(new SpamReport(new byte[5])), true), + Arguments.of(Entity.json("{\"token\":\"AAAAAAA\"}"), true), + Arguments.of(Entity.json(new SpamReport(new byte[0])), true), + Arguments.of(Entity.json(new SpamReport(null)), true), + Arguments.of(Entity.json("{\"token\": \"\"}"), true), + Arguments.of(Entity.json("{\"token\": null}"), true), + Arguments.of(Entity.json("null"), true), + Arguments.of(Entity.json("{\"weird\": 123}"), true), + Arguments.of(Entity.json("\"weirder\""), false), + Arguments.of(Entity.json("weirdest"), false) + ); + } + + @Test + void testValidateContentLength() throws Exception { + final int contentLength = Math.toIntExact(MessageController.MAX_MESSAGE_SIZE + 1); + final byte[] contentBytes = new byte[contentLength]; + Arrays.fill(contentBytes, (byte) 1); + + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", SINGLE_DEVICE_UUID)) + .request() + .header(OptionalAccess.UNIDENTIFIED, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES)) + .put(Entity.entity(new IncomingMessageList( + List.of(new IncomingMessage(1, 1L, 1, new String(contentBytes))), false, true, + System.currentTimeMillis()), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat("Bad response", response.getStatus(), is(equalTo(413))); + + verify(messageSender, never()).sendMessage(any(Account.class), any(Device.class), any(Envelope.class), + anyBoolean()); + } + + @ParameterizedTest + @MethodSource + void testValidateEnvelopeType(String payloadFilename, boolean expectOk) throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", SINGLE_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .header(HttpHeaders.USER_AGENT, "Test-UA") + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture(payloadFilename), IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + if (expectOk) { + assertEquals(200, response.getStatus()); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(Envelope.class); + verify(messageSender).sendMessage(any(Account.class), any(Device.class), captor.capture(), eq(false)); + } else { + assertEquals(400, response.getStatus()); + verify(messageSender, never()).sendMessage(any(), any(), any(), anyBoolean()); + } + } + + private static Stream testValidateEnvelopeType() { + return Stream.of( + Arguments.of("fixtures/current_message_single_device.json", true), + Arguments.of("fixtures/current_message_single_device_server_receipt_type.json", false) + ); + } + + private static void writePayloadDeviceId(ByteBuffer bb, long deviceId) { + long x = deviceId; + // write the device-id in the 7-bit varint format we use, least significant bytes first. + do { + long b = x & 0x7f; + x = x >>> 7; + if (x != 0) b |= 0x80; + bb.put((byte)b); + } while (x != 0); + } + + private static void writeMultiPayloadRecipient(final ByteBuffer bb, final Recipient r, final boolean useExplicitIdentifier) { + if (useExplicitIdentifier) { + bb.put(r.uuid().toFixedWidthByteArray()); + } else { + bb.put(UUIDUtil.toBytes(r.uuid().uuid())); + } + + writePayloadDeviceId(bb, r.deviceId()); // device id (1-9 bytes) + bb.putShort((short) r.registrationId()); // registration id (2 bytes) + bb.put(r.perRecipientKeyMaterial()); // key material (48 bytes) + } + + private static InputStream initializeMultiPayload(List recipients, byte[] buffer, final boolean explicitIdentifiers) { + // initialize a binary payload according to our wire format + ByteBuffer bb = ByteBuffer.wrap(buffer); + bb.order(ByteOrder.BIG_ENDIAN); + + // first write the header + bb.put(explicitIdentifiers + ? MultiRecipientMessageProvider.EXPLICIT_ID_VERSION_IDENTIFIER + : MultiRecipientMessageProvider.AMBIGUOUS_ID_VERSION_IDENTIFIER); // version byte + bb.put((byte)recipients.size()); // count varint + + Iterator it = recipients.iterator(); + while (it.hasNext()) { + writeMultiPayloadRecipient(bb, it.next(), explicitIdentifiers); + } + + // now write the actual message body (empty for now) + bb.put(new byte[39]); // payload (variable but >= 32, 39 bytes here) + + // return the input stream + return new ByteArrayInputStream(buffer, 0, bb.position()); + } + + @ParameterizedTest + @MethodSource + void testMultiRecipientMessage(UUID recipientUUID, boolean authorize, boolean isStory, boolean urgent, boolean explicitIdentifier) throws Exception { + + final List recipients; + if (recipientUUID == MULTI_DEVICE_UUID) { + recipients = List.of( + new Recipient(new AciServiceIdentifier(MULTI_DEVICE_UUID), MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1, new byte[48]), + new Recipient(new AciServiceIdentifier(MULTI_DEVICE_UUID), MULTI_DEVICE_ID2, MULTI_DEVICE_REG_ID2, new byte[48]) + ); + } else { + recipients = List.of(new Recipient(new AciServiceIdentifier(SINGLE_DEVICE_UUID), SINGLE_DEVICE_ID1, SINGLE_DEVICE_REG_ID1, new byte[48])); + } + + // initialize our binary payload and create an input stream + byte[] buffer = new byte[2048]; + //InputStream stream = initializeMultiPayload(recipientUUID, buffer); + InputStream stream = initializeMultiPayload(recipients, buffer, explicitIdentifier); + + // set up the entity to use in our PUT request + Entity entity = Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE); + + when(multiRecipientMessageExecutor.invokeAll(any())) + .thenAnswer(answer -> { + final List tasks = answer.getArgument(0, List.class); + tasks.forEach(c -> { + try { + c.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + return null; + }); + + // start building the request + Invocation.Builder bldr = resources + .getJerseyTest() + .target("/v1/messages/multi_recipient") + .queryParam("online", true) + .queryParam("ts", 1663798405641L) + .queryParam("story", isStory) + .queryParam("urgent", urgent) + .request() + .header(HttpHeaders.USER_AGENT, "FIXME"); + + // add access header if needed + if (authorize) { + String encodedBytes = Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES); + bldr = bldr.header(OptionalAccess.UNIDENTIFIED, encodedBytes); + } + + // make the PUT request + Response response = bldr.put(entity); + + if (authorize) { + ArgumentCaptor envelopeArgumentCaptor = ArgumentCaptor.forClass(Envelope.class); + verify(messageSender, atLeastOnce()).sendMessage(any(), any(), envelopeArgumentCaptor.capture(), anyBoolean()); + assertEquals(urgent, envelopeArgumentCaptor.getValue().getUrgent()); + } + + // We have a 2x2x2 grid of possible situations based on: + // - recipient enabled stories? + // - sender is authorized? + // - message is a story? + // + // (urgent is not included in the grid because it has no effect + // on any of the other settings.) + + if (recipientUUID == MULTI_DEVICE_UUID) { + // This is the case where the recipient has enabled stories. + if(isStory) { + // We are sending a story, so we ignore access checks and expect this + // to go out to both the recipient's devices. + checkGoodMultiRecipientResponse(response, 2); + } else { + // We are not sending a story, so we need to do access checks. + if (authorize) { + // When authorized we send a message to the recipient's devices. + checkGoodMultiRecipientResponse(response, 2); + } else { + // When forbidden, we return a 401 error. + checkBadMultiRecipientResponse(response, 401); + } + } + } else { + // This is the case where the recipient has not enabled stories. + if (isStory) { + // We are sending a story, so we ignore access checks. + // this recipient has one device. + checkGoodMultiRecipientResponse(response, 1); + } else { + // We are not sending a story so check access. + if (authorize) { + // If allowed, send a message to the recipient's one device. + checkGoodMultiRecipientResponse(response, 1); + } else { + // If forbidden, return a 401 error. + checkBadMultiRecipientResponse(response, 401); + } + } + } + } + + // Arguments here are: recipient-UUID, is-authorized?, is-story? + private static Stream testMultiRecipientMessage() { + return Stream.of( + Arguments.of(MULTI_DEVICE_UUID, false, true, true, false), + Arguments.of(MULTI_DEVICE_UUID, false, false, true, false), + Arguments.of(SINGLE_DEVICE_UUID, false, true, true, false), + Arguments.of(SINGLE_DEVICE_UUID, false, false, true, false), + Arguments.of(MULTI_DEVICE_UUID, true, true, true, false), + Arguments.of(MULTI_DEVICE_UUID, true, false, true, false), + Arguments.of(SINGLE_DEVICE_UUID, true, true, true, false), + Arguments.of(SINGLE_DEVICE_UUID, true, false, true, false), + Arguments.of(MULTI_DEVICE_UUID, false, true, false, false), + Arguments.of(MULTI_DEVICE_UUID, false, false, false, false), + Arguments.of(SINGLE_DEVICE_UUID, false, true, false, false), + Arguments.of(SINGLE_DEVICE_UUID, false, false, false, false), + Arguments.of(MULTI_DEVICE_UUID, true, true, false, false), + Arguments.of(MULTI_DEVICE_UUID, true, false, false, false), + Arguments.of(SINGLE_DEVICE_UUID, true, true, false, false), + Arguments.of(SINGLE_DEVICE_UUID, true, false, false, false), + Arguments.of(MULTI_DEVICE_UUID, false, true, true, true), + Arguments.of(MULTI_DEVICE_UUID, false, false, true, true), + Arguments.of(SINGLE_DEVICE_UUID, false, true, true, true), + Arguments.of(SINGLE_DEVICE_UUID, false, false, true, true), + Arguments.of(MULTI_DEVICE_UUID, true, true, true, true), + Arguments.of(MULTI_DEVICE_UUID, true, false, true, true), + Arguments.of(SINGLE_DEVICE_UUID, true, true, true, true), + Arguments.of(SINGLE_DEVICE_UUID, true, false, true, true), + Arguments.of(MULTI_DEVICE_UUID, false, true, false, true), + Arguments.of(MULTI_DEVICE_UUID, false, false, false, true), + Arguments.of(SINGLE_DEVICE_UUID, false, true, false, true), + Arguments.of(SINGLE_DEVICE_UUID, false, false, false, true), + Arguments.of(MULTI_DEVICE_UUID, true, true, false, true), + Arguments.of(MULTI_DEVICE_UUID, true, false, false, true), + Arguments.of(SINGLE_DEVICE_UUID, true, true, false, true), + Arguments.of(SINGLE_DEVICE_UUID, true, false, false, true) + ); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMultiRecipientRedisBombProtection(final boolean useExplicitIdentifier) throws Exception { + final List recipients = List.of( + new Recipient(new AciServiceIdentifier(MULTI_DEVICE_UUID), MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1, new byte[48]), + new Recipient(new AciServiceIdentifier(MULTI_DEVICE_UUID), MULTI_DEVICE_ID2, MULTI_DEVICE_REG_ID1, new byte[48]), + new Recipient(new AciServiceIdentifier(MULTI_DEVICE_UUID), MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1, new byte[48])); + + Response response = resources + .getJerseyTest() + .target("/v1/messages/multi_recipient") + .queryParam("online", true) + .queryParam("ts", 1663798405641L) + .queryParam("story", false) + .queryParam("urgent", false) + .request() + .header(HttpHeaders.USER_AGENT, "cluck cluck, i'm a parrot") + .header(OptionalAccess.UNIDENTIFIED, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES)) + .put(Entity.entity(initializeMultiPayload(recipients, new byte[2048], useExplicitIdentifier), MultiRecipientMessageProvider.MEDIA_TYPE)); + + checkBadMultiRecipientResponse(response, 422); + } + + @Test + void testSendStoryToUnknownAccount() throws Exception { + String accessBytes = Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES); + String json = jsonFixture("fixtures/current_message_single_device.json"); + UUID unknownUUID = UUID.randomUUID(); + IncomingMessageList list = SystemMapper.jsonMapper().readValue(json, IncomingMessageList.class); + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", unknownUUID)) + .queryParam("story", "true") + .request() + .header(OptionalAccess.UNIDENTIFIED, accessBytes) + .put(Entity.entity(list, MediaType.APPLICATION_JSON_TYPE)); + + assertThat("200 masks unknown recipient", response.getStatus(), is(equalTo(200))); + } + + @ParameterizedTest + @MethodSource + void testSendMultiRecipientMessageToUnknownAccounts(boolean story, boolean known, boolean useExplicitIdentifier) { + + final Recipient r1; + if (known) { + r1 = new Recipient(new AciServiceIdentifier(SINGLE_DEVICE_UUID), SINGLE_DEVICE_ID1, SINGLE_DEVICE_REG_ID1, new byte[48]); + } else { + r1 = new Recipient(new AciServiceIdentifier(UUID.randomUUID()), 999, 999, new byte[48]); + } + + Recipient r2 = new Recipient(new AciServiceIdentifier(MULTI_DEVICE_UUID), MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1, new byte[48]); + Recipient r3 = new Recipient(new AciServiceIdentifier(MULTI_DEVICE_UUID), MULTI_DEVICE_ID2, MULTI_DEVICE_REG_ID2, new byte[48]); + + List recipients = List.of(r1, r2, r3); + + byte[] buffer = new byte[2048]; + InputStream stream = initializeMultiPayload(recipients, buffer, useExplicitIdentifier); + // set up the entity to use in our PUT request + Entity entity = Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE); + + // This looks weird, but there is a method to the madness. + // new bytes[16] is equivalent to UNIDENTIFIED_ACCESS_BYTES ^ UNIDENTIFIED_ACCESS_BYTES + // (i.e. we need to XOR all the access keys together) + String accessBytes = Base64.getEncoder().encodeToString(new byte[16]); + + // start building the request + Invocation.Builder bldr = resources + .getJerseyTest() + .target("/v1/messages/multi_recipient") + .queryParam("online", true) + .queryParam("ts", 1663798405641L) + .queryParam("story", story) + .request() + .header(HttpHeaders.USER_AGENT, "Test User Agent") + .header(OptionalAccess.UNIDENTIFIED, accessBytes); + + // make the PUT request + Response response = bldr.put(entity); + + if (story || known) { + // it's a story so we unconditionally get 200 ok + assertEquals(200, response.getStatus()); + } else { + // unknown recipient means 404 not found + assertEquals(404, response.getStatus()); + } + } + + private static Stream testSendMultiRecipientMessageToUnknownAccounts() { + return Stream.of( + Arguments.of(true, true, false), + Arguments.of(true, false, false), + Arguments.of(false, true, false), + Arguments.of(false, false, false), + + Arguments.of(true, true, true), + Arguments.of(true, false, true), + Arguments.of(false, true, true), + Arguments.of(false, false, true) + ); + } + + @ParameterizedTest + @MethodSource + void sendMultiRecipientMessageMismatchedDevices(final ServiceIdentifier serviceIdentifier) + throws JsonProcessingException { + + final List recipients = List.of( + new Recipient(serviceIdentifier, MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1, new byte[48]), + new Recipient(serviceIdentifier, MULTI_DEVICE_ID2, MULTI_DEVICE_REG_ID2, new byte[48]), + new Recipient(serviceIdentifier, MULTI_DEVICE_ID3, MULTI_DEVICE_REG_ID3, new byte[48])); + + // initialize our binary payload and create an input stream + byte[] buffer = new byte[2048]; + // InputStream stream = initializeMultiPayload(recipientUUID, buffer); + InputStream stream = initializeMultiPayload(recipients, buffer, true); + + // set up the entity to use in our PUT request + Entity entity = Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE); + + // start building the request + final Invocation.Builder invocationBuilder = resources + .getJerseyTest() + .target("/v1/messages/multi_recipient") + .queryParam("online", false) + .queryParam("ts", System.currentTimeMillis()) + .queryParam("story", false) + .queryParam("urgent", true) + .request() + .header(HttpHeaders.USER_AGENT, "FIXME") + .header(OptionalAccess.UNIDENTIFIED, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES)); + + // make the PUT request + final Response response = invocationBuilder.put(entity); + + assertEquals(409, response.getStatus()); + + final List mismatchedDevices = + SystemMapper.jsonMapper().readValue(response.readEntity(String.class), + SystemMapper.jsonMapper().getTypeFactory().constructCollectionType(List.class, AccountMismatchedDevices.class)); + + assertEquals(List.of(new AccountMismatchedDevices(serviceIdentifier, + new MismatchedDevices(Collections.emptyList(), List.of((long) MULTI_DEVICE_ID3)))), + mismatchedDevices); + } + + private static Stream sendMultiRecipientMessageMismatchedDevices() { + return Stream.of( + Arguments.of(new AciServiceIdentifier(MULTI_DEVICE_UUID)), + Arguments.of(new PniServiceIdentifier(MULTI_DEVICE_PNI))); + } + + @ParameterizedTest + @MethodSource + void sendMultiRecipientMessageStaleDevices(final ServiceIdentifier serviceIdentifier) throws JsonProcessingException { + final List recipients = List.of( + new Recipient(serviceIdentifier, MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1 + 1, new byte[48]), + new Recipient(serviceIdentifier, MULTI_DEVICE_ID2, MULTI_DEVICE_REG_ID2 + 1, new byte[48])); + + // initialize our binary payload and create an input stream + byte[] buffer = new byte[2048]; + // InputStream stream = initializeMultiPayload(recipientUUID, buffer); + InputStream stream = initializeMultiPayload(recipients, buffer, true); + + // set up the entity to use in our PUT request + Entity entity = Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE); + + // start building the request + final Invocation.Builder invocationBuilder = resources + .getJerseyTest() + .target("/v1/messages/multi_recipient") + .queryParam("online", false) + .queryParam("ts", System.currentTimeMillis()) + .queryParam("story", false) + .queryParam("urgent", true) + .request() + .header(HttpHeaders.USER_AGENT, "FIXME") + .header(OptionalAccess.UNIDENTIFIED, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES)); + + // make the PUT request + final Response response = invocationBuilder.put(entity); + + assertEquals(410, response.getStatus()); + + final List staleDevices = + SystemMapper.jsonMapper().readValue(response.readEntity(String.class), + SystemMapper.jsonMapper().getTypeFactory().constructCollectionType(List.class, AccountStaleDevices.class)); + + assertEquals(1, staleDevices.size()); + assertEquals(serviceIdentifier, staleDevices.get(0).uuid()); + assertEquals(Set.of((long) MULTI_DEVICE_ID1, (long) MULTI_DEVICE_ID2), new HashSet<>(staleDevices.get(0).devices().staleDevices())); + } + + private static Stream sendMultiRecipientMessageStaleDevices() { + return Stream.of( + Arguments.of(new AciServiceIdentifier(MULTI_DEVICE_UUID)), + Arguments.of(new PniServiceIdentifier(MULTI_DEVICE_PNI))); + } + + @ParameterizedTest + @MethodSource + void sendMultiRecipientMessage404(final ServiceIdentifier serviceIdentifier) + throws NotPushRegisteredException, InterruptedException { + + when(multiRecipientMessageExecutor.invokeAll(any())) + .thenAnswer(answer -> { + final List tasks = answer.getArgument(0, List.class); + tasks.forEach(c -> { + try { + c.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + return null; + }); + + final List recipients = List.of( + new Recipient(serviceIdentifier, MULTI_DEVICE_ID1, MULTI_DEVICE_REG_ID1, new byte[48]), + new Recipient(serviceIdentifier, MULTI_DEVICE_ID2, MULTI_DEVICE_REG_ID2, new byte[48])); + + // initialize our binary payload and create an input stream + byte[] buffer = new byte[2048]; + // InputStream stream = initializeMultiPayload(recipientUUID, buffer); + InputStream stream = initializeMultiPayload(recipients, buffer, true); + + // set up the entity to use in our PUT request + Entity entity = Entity.entity(stream, MultiRecipientMessageProvider.MEDIA_TYPE); + + // start building the request + final Invocation.Builder invocationBuilder = resources + .getJerseyTest() + .target("/v1/messages/multi_recipient") + .queryParam("online", false) + .queryParam("ts", System.currentTimeMillis()) + .queryParam("story", true) + .queryParam("urgent", true) + .request() + .header(HttpHeaders.USER_AGENT, "FIXME") + .header(OptionalAccess.UNIDENTIFIED, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_BYTES)); + + doThrow(NotPushRegisteredException.class) + .when(messageSender).sendMessage(any(), any(), any(), anyBoolean()); + + // make the PUT request + final SendMultiRecipientMessageResponse response = invocationBuilder.put(entity, SendMultiRecipientMessageResponse.class); + + assertEquals(List.of(serviceIdentifier), response.uuids404()); + } + + private static Stream sendMultiRecipientMessage404() { + return Stream.of( + Arguments.of(new AciServiceIdentifier(MULTI_DEVICE_UUID)), + Arguments.of(new PniServiceIdentifier(MULTI_DEVICE_PNI))); + } + + private void checkBadMultiRecipientResponse(Response response, int expectedCode) throws Exception { + assertThat("Unexpected response", response.getStatus(), is(equalTo(expectedCode))); + verify(messageSender, never()).sendMessage(any(), any(), any(), anyBoolean()); + verify(multiRecipientMessageExecutor, never()).invokeAll(any()); + } + + private void checkGoodMultiRecipientResponse(Response response, int expectedCount) throws Exception { + assertThat("Unexpected response", response.getStatus(), is(equalTo(200))); + ArgumentCaptor>> captor = ArgumentCaptor.forClass(List.class); + verify(multiRecipientMessageExecutor, times(1)).invokeAll(captor.capture()); + assert (captor.getValue().size() == expectedCount); + SendMultiRecipientMessageResponse smrmr = response.readEntity(SendMultiRecipientMessageResponse.class); + assert (smrmr.uuids404().isEmpty()); + } + + private static Envelope generateEnvelope(UUID guid, int type, long timestamp, UUID sourceUuid, + int sourceDevice, UUID destinationUuid, UUID updatedPni, byte[] content, long serverTimestamp) { + return generateEnvelope(guid, type, timestamp, sourceUuid, sourceDevice, destinationUuid, updatedPni, content, serverTimestamp, false); + } + + private static Envelope generateEnvelope(UUID guid, int type, long timestamp, UUID sourceUuid, + int sourceDevice, UUID destinationUuid, UUID updatedPni, byte[] content, long serverTimestamp, boolean story) { + + final MessageProtos.Envelope.Builder builder = MessageProtos.Envelope.newBuilder() + .setType(MessageProtos.Envelope.Type.forNumber(type)) + .setTimestamp(timestamp) + .setServerTimestamp(serverTimestamp) + .setDestinationUuid(destinationUuid.toString()) + .setStory(story) + .setServerGuid(guid.toString()); + + if (sourceUuid != null) { + builder.setSourceUuid(sourceUuid.toString()); + builder.setSourceDevice(sourceDevice); + } + + if (content != null) { + builder.setContent(ByteString.copyFrom(content)); + } + + if (updatedPni != null) { + builder.setUpdatedPni(updatedPni.toString()); + } + + return builder.build(); + } + + private static Recipient genRecipient(Random rng) { + UUID u1 = UUID.randomUUID(); // non-null + long d1 = rng.nextLong() & 0x3fffffffffffffffL + 1; // 1 to 4611686018427387903 + int dr1 = rng.nextInt() & 0xffff; // 0 to 65535 + byte[] perKeyBytes = new byte[48]; // size=48, non-null + rng.nextBytes(perKeyBytes); + return new Recipient(new AciServiceIdentifier(u1), d1, dr1, perKeyBytes); + } + + private static void roundTripVarint(long expected, byte [] bytes) throws Exception { + ByteBuffer bb = ByteBuffer.wrap(bytes); + writePayloadDeviceId(bb, expected); + InputStream stream = new ByteArrayInputStream(bytes, 0, bb.position()); + long got = MultiRecipientMessageProvider.readVarint(stream); + assertEquals(expected, got, String.format("encoded as: %s", Arrays.toString(bytes))); + } + + @Test + void testVarintPayload() throws Exception { + Random rng = new Random(); + byte[] bytes = new byte[12]; + + // some static test cases + for (long i = 1L; i <= 10L; i++) { + roundTripVarint(i, bytes); + } + roundTripVarint(Long.MAX_VALUE, bytes); + + for (int i = 0; i < 1000; i++) { + // we need to ensure positive device IDs + long start = rng.nextLong() & Long.MAX_VALUE; + if (start == 0L) start = 1L; + + // run the test for this case + roundTripVarint(start, bytes); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMultiPayloadRoundtrip(final boolean useExplicitIdentifiers) throws Exception { + Random rng = new java.util.Random(); + List expected = new LinkedList<>(); + for(int i = 0; i < 100; i++) { + expected.add(genRecipient(rng)); + } + + byte[] buffer = new byte[100 + expected.size() * 100]; + InputStream entityStream = initializeMultiPayload(expected, buffer, useExplicitIdentifiers); + MultiRecipientMessageProvider provider = new MultiRecipientMessageProvider(); + // the provider ignores the headers, java reflection, etc. so we don't use those here. + MultiRecipientMessage res = provider.readFrom(null, null, null, null, null, entityStream); + List got = Arrays.asList(res.recipients()); + + assertEquals(expected, got); + } + + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/PaymentsControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/PaymentsControllerTest.java new file mode 100644 index 000000000..e7a4d1ca1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/PaymentsControllerTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager; +import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntity; +import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntityList; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; + +@ExtendWith(DropwizardExtensionsSupport.class) +class PaymentsControllerTest { + + private static final ExternalServiceCredentialsGenerator paymentsCredentialsGenerator = mock(ExternalServiceCredentialsGenerator.class); + private static final CurrencyConversionManager currencyManager = mock(CurrencyConversionManager.class); + + private final ExternalServiceCredentials validCredentials = new ExternalServiceCredentials("username", "password"); + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new PaymentsController(currencyManager, paymentsCredentialsGenerator)) + .build(); + + + @BeforeEach + void setup() { + when(paymentsCredentialsGenerator.generateForUuid(eq(AuthHelper.VALID_UUID))).thenReturn(validCredentials); + when(currencyManager.getCurrencyConversions()).thenReturn(Optional.of( + new CurrencyConversionEntityList(List.of( + new CurrencyConversionEntity("FOO", Map.of( + "USD", new BigDecimal("2.35"), + "EUR", new BigDecimal("1.89") + )), + new CurrencyConversionEntity("BAR", Map.of( + "USD", new BigDecimal("1.50"), + "EUR", new BigDecimal("0.98") + )) + ), System.currentTimeMillis()))); + } + + @Test + void testGetAuthToken() { + ExternalServiceCredentials token = + resources.getJerseyTest() + .target("/v1/payments/auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(ExternalServiceCredentials.class); + + assertThat(token.username()).isEqualTo(validCredentials.username()); + assertThat(token.password()).isEqualTo(validCredentials.password()); + } + + @Test + void testInvalidAuthGetAuthToken() { + Response response = + resources.getJerseyTest() + .target("/v1/payments/auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.INVALID_UUID, AuthHelper.INVALID_PASSWORD)) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + } + + @Test + void testDisabledGetAuthToken() { + Response response = + resources.getJerseyTest() + .target("/v1/payments/auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .get(); + assertThat(response.getStatus()).isEqualTo(401); + } + + @Test + void testGetCurrencyConversions() { + CurrencyConversionEntityList conversions = + resources.getJerseyTest() + .target("/v1/payments/conversions") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(CurrencyConversionEntityList.class); + + + assertThat(conversions.getCurrencies().size()).isEqualTo(2); + assertThat(conversions.getCurrencies().get(0).getBase()).isEqualTo("FOO"); + assertThat(conversions.getCurrencies().get(0).getConversions().get("USD")).isEqualTo(new BigDecimal("2.35")); + } + + @Test + void testGetCurrencyConversions_Json() { + String json = + resources.getJerseyTest() + .target("/v1/payments/conversions") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(String.class); + + // the currency serialization might occur in either order + assertThat(json).containsPattern("\\{(\"EUR\":1.89,\"USD\":2.35|\"USD\":2.35,\"EUR\":1.89)}"); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java new file mode 100644 index 000000000..e0256f420 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java @@ -0,0 +1,1374 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.stream.Stream; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import org.assertj.core.api.Condition; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ServiceId; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.ServerPublicParams; +import org.signal.libsignal.zkgroup.ServerSecretParams; +import org.signal.libsignal.zkgroup.VerificationFailedException; +import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; +import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; +import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; +import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequestContext; +import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.OptionalAccess; +import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; +import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicPaymentsConfiguration; +import org.whispersystems.textsecuregcm.entities.Badge; +import org.whispersystems.textsecuregcm.entities.BadgeSvg; +import org.whispersystems.textsecuregcm.entities.BaseProfileResponse; +import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckRequest; +import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckResponse; +import org.whispersystems.textsecuregcm.entities.CreateProfileRequest; +import org.whispersystems.textsecuregcm.entities.ExpiringProfileKeyCredentialProfileResponse; +import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes; +import org.whispersystems.textsecuregcm.entities.VersionedProfileResponse; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.s3.PolicySigner; +import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountBadge; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.storage.ProfilesManager; +import org.whispersystems.textsecuregcm.storage.VersionedProfile; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.tests.util.ProfileTestHelper; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.TestClock; +import org.whispersystems.textsecuregcm.util.Util; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; + +@ExtendWith(DropwizardExtensionsSupport.class) +class ProfileControllerTest { + + private static final Clock clock = TestClock.pinned(Instant.ofEpochSecond(42)); + private static final AccountsManager accountsManager = mock(AccountsManager.class); + private static final ProfilesManager profilesManager = mock(ProfilesManager.class); + private static final RateLimiters rateLimiters = mock(RateLimiters.class); + private static final RateLimiter rateLimiter = mock(RateLimiter.class); + private static final RateLimiter usernameRateLimiter = mock(RateLimiter.class); + + private static final S3Client s3client = mock(S3Client.class); + private static final PostPolicyGenerator postPolicyGenerator = new PostPolicyGenerator("us-west-1", "profile-bucket", + "accessKey"); + private static final PolicySigner policySigner = new PolicySigner("accessSecret", "us-west-1"); + private static final ServerZkProfileOperations zkProfileOperations = mock(ServerZkProfileOperations.class); + + private static final byte[] UNIDENTIFIED_ACCESS_KEY = "sixteenbytes1234".getBytes(StandardCharsets.UTF_8); + private static final IdentityKey ACCOUNT_IDENTITY_KEY = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + private static final IdentityKey ACCOUNT_PHONE_NUMBER_IDENTITY_KEY = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + private static final IdentityKey ACCOUNT_TWO_IDENTITY_KEY = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + private static final IdentityKey ACCOUNT_TWO_PHONE_NUMBER_IDENTITY_KEY = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + private static final String BASE_64_URL_USERNAME_HASH = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE"; + private static final byte[] USERNAME_HASH = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH); + @SuppressWarnings("unchecked") + private static final DynamicConfigurationManager dynamicConfigurationManager = mock( + DynamicConfigurationManager.class); + + private DynamicPaymentsConfiguration dynamicPaymentsConfiguration; + private Account profileAccount; + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .addProvider(new RateLimitExceededExceptionMapper()) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new ProfileController( + clock, + rateLimiters, + accountsManager, + profilesManager, + dynamicConfigurationManager, + (acceptableLanguages, accountBadges, isSelf) -> List.of(new Badge("TEST", "other", "Test Badge", + "This badge is in unit tests.", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))) + ), + new BadgesConfiguration(List.of( + new BadgeConfiguration("TEST", "other", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))), + new BadgeConfiguration("TEST1", "testing", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))), + new BadgeConfiguration("TEST2", "testing", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))), + new BadgeConfiguration("TEST3", "testing", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))) + ), List.of("TEST1"), Map.of(1L, "TEST1", 2L, "TEST2", 3L, "TEST3")), + s3client, + postPolicyGenerator, + policySigner, + "profilesBucket", + zkProfileOperations, + Executors.newSingleThreadExecutor())) + .build(); + + @BeforeEach + void setup() { + reset(s3client); + + AccountsHelper.setupMockUpdate(accountsManager); + + dynamicPaymentsConfiguration = mock(DynamicPaymentsConfiguration.class); + final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + when(dynamicConfiguration.getPaymentsConfiguration()).thenReturn(dynamicPaymentsConfiguration); + when(dynamicPaymentsConfiguration.getDisallowedPrefixes()).thenReturn(Collections.emptyList()); + + when(rateLimiters.getProfileLimiter()).thenReturn(rateLimiter); + when(rateLimiters.getUsernameLookupLimiter()).thenReturn(usernameRateLimiter); + + profileAccount = mock(Account.class); + + when(profileAccount.getIdentityKey(IdentityType.ACI)).thenReturn(ACCOUNT_TWO_IDENTITY_KEY); + when(profileAccount.getIdentityKey(IdentityType.PNI)).thenReturn(ACCOUNT_TWO_PHONE_NUMBER_IDENTITY_KEY); + when(profileAccount.getUuid()).thenReturn(AuthHelper.VALID_UUID_TWO); + when(profileAccount.getPhoneNumberIdentifier()).thenReturn(AuthHelper.VALID_PNI_TWO); + when(profileAccount.isEnabled()).thenReturn(true); + when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.empty()); + when(profileAccount.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH)); + when(profileAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY)); + + Account capabilitiesAccount = mock(Account.class); + + when(capabilitiesAccount.getUuid()).thenReturn(AuthHelper.VALID_UUID); + when(capabilitiesAccount.getIdentityKey(IdentityType.ACI)).thenReturn(ACCOUNT_IDENTITY_KEY); + when(capabilitiesAccount.getIdentityKey(IdentityType.PNI)).thenReturn(ACCOUNT_PHONE_NUMBER_IDENTITY_KEY); + when(capabilitiesAccount.isEnabled()).thenReturn(true); + + when(accountsManager.getByServiceIdentifier(any())).thenReturn(Optional.empty()); + + when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(profileAccount)); + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID_TWO)).thenReturn(Optional.of(profileAccount)); + when(accountsManager.getByPhoneNumberIdentifier(AuthHelper.VALID_PNI_TWO)).thenReturn(Optional.of(profileAccount)); + when(accountsManager.getByServiceIdentifier(new AciServiceIdentifier(AuthHelper.VALID_UUID_TWO))).thenReturn(Optional.of(profileAccount)); + when(accountsManager.getByServiceIdentifier(new PniServiceIdentifier(AuthHelper.VALID_PNI_TWO))).thenReturn(Optional.of(profileAccount)); + when(accountsManager.getByUsernameHash(USERNAME_HASH)).thenReturn(CompletableFuture.completedFuture(Optional.of(profileAccount))); + + when(accountsManager.getByE164(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(capabilitiesAccount)); + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(capabilitiesAccount)); + when(accountsManager.getByServiceIdentifier(new AciServiceIdentifier(AuthHelper.VALID_UUID))).thenReturn(Optional.of(capabilitiesAccount)); + + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] emoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] about = ProfileTestHelper.generateRandomByteArray(156); + + when(profilesManager.get(eq(AuthHelper.VALID_UUID), eq("someversion"))).thenReturn(Optional.empty()); + when(profilesManager.get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"))).thenReturn(Optional.of(new VersionedProfile( + "validversion", name, "profiles/validavatar", emoji, about, null, "validcommitmnet".getBytes()))); + + clearInvocations(rateLimiter); + clearInvocations(accountsManager); + clearInvocations(usernameRateLimiter); + clearInvocations(profilesManager); + clearInvocations(zkProfileOperations); + } + + @AfterEach + void teardown() { + reset(accountsManager); + reset(rateLimiter); + } + + @Test + void testProfileGetByAci() throws RateLimitExceededException { + final BaseProfileResponse profile = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(BaseProfileResponse.class); + + assertThat(profile.getIdentityKey()).isEqualTo(ACCOUNT_TWO_IDENTITY_KEY); + assertThat(profile.getBadges()).hasSize(1).element(0).has(new Condition<>( + badge -> "Test Badge".equals(badge.getName()), "has badge with expected name")); + + verify(accountsManager).getByAccountIdentifier(AuthHelper.VALID_UUID_TWO); + verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID); + } + + @Test + void testProfileGetByAciRateLimited() throws RateLimitExceededException { + doThrow(new RateLimitExceededException(Duration.ofSeconds(13), true)).when(rateLimiter) + .validate(AuthHelper.VALID_UUID); + + final Response response = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(); + + assertThat(response.getStatus()).isEqualTo(413); + assertThat(response.getHeaderString("Retry-After")).isEqualTo(String.valueOf(Duration.ofSeconds(13).toSeconds())); + } + + @Test + void testProfileGetByAciUnidentified() throws RateLimitExceededException { + final BaseProfileResponse profile = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO) + .request() + .header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader(UNIDENTIFIED_ACCESS_KEY)) + .get(BaseProfileResponse.class); + + assertThat(profile.getIdentityKey()).isEqualTo(ACCOUNT_TWO_IDENTITY_KEY); + assertThat(profile.getBadges()).hasSize(1).element(0).has(new Condition<>( + badge -> "Test Badge".equals(badge.getName()), "has badge with expected name")); + + verify(accountsManager).getByAccountIdentifier(AuthHelper.VALID_UUID_TWO); + verify(rateLimiter, never()).validate(AuthHelper.VALID_UUID); + } + + @Test + void testProfileGetByAciUnidentifiedBadKey() { + final Response response = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO) + .request() + .header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("incorrect".getBytes())) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + } + + @Test + void testProfileGetByAciUnidentifiedAccountNotFound() { + final Response response = resources.getJerseyTest() + .target("/v1/profile/" + UUID.randomUUID()) + .request() + .header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader(UNIDENTIFIED_ACCESS_KEY)) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + } + + @Test + void testProfileGetByPni() throws RateLimitExceededException { + final BaseProfileResponse profile = resources.getJerseyTest() + .target("/v1/profile/PNI:" + AuthHelper.VALID_PNI_TWO) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(BaseProfileResponse.class); + + assertThat(profile.getIdentityKey()).isEqualTo(ACCOUNT_TWO_PHONE_NUMBER_IDENTITY_KEY); + assertThat(profile.getBadges()).isEmpty(); + assertThat(profile.getUuid()).isEqualTo(new PniServiceIdentifier(AuthHelper.VALID_PNI_TWO)); + assertThat(profile.getCapabilities()).isNotNull(); + assertThat(profile.isUnrestrictedUnidentifiedAccess()).isFalse(); + assertThat(profile.getUnidentifiedAccess()).isNull(); + + verify(accountsManager).getByPhoneNumberIdentifier(AuthHelper.VALID_PNI_TWO); + verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID); + } + + @Test + void testProfileGetByPniRateLimited() throws RateLimitExceededException { + doThrow(new RateLimitExceededException(Duration.ofSeconds(13), true)).when(rateLimiter) + .validate(AuthHelper.VALID_UUID); + + final Response response = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_PNI_TWO) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(); + + assertThat(response.getStatus()).isEqualTo(413); + assertThat(response.getHeaderString("Retry-After")).isEqualTo(String.valueOf(Duration.ofSeconds(13).toSeconds())); + } + + @Test + void testProfileGetByPniUnidentified() throws RateLimitExceededException { + final Response response = resources.getJerseyTest() + .target("/v1/profile/PNI:" + AuthHelper.VALID_PNI_TWO) + .request() + .header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader(UNIDENTIFIED_ACCESS_KEY)) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + + verify(accountsManager).getByPhoneNumberIdentifier(AuthHelper.VALID_PNI_TWO); + verify(rateLimiter, never()).validate(AuthHelper.VALID_UUID); + } + + @Test + void testProfileGetByPniUnidentifiedBadKey() { + final Response response = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_PNI_TWO) + .request() + .header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("incorrect".getBytes())) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + } + + @Test + void testProfileGetUnauthorized() { + final Response response = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO) + .request() + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + } + + + @Test + void testProfileGetDisabled() { + final Response response = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_UUID, AuthHelper.DISABLED_PASSWORD)) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + } + + @Test + void testProfileCapabilities() { + final BaseProfileResponse profile = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_UUID) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(BaseProfileResponse.class); + + assertThat(profile.getCapabilities().gv1Migration()).isTrue(); + assertThat(profile.getCapabilities().senderKey()).isTrue(); + assertThat(profile.getCapabilities().announcementGroup()).isTrue(); + } + + @Test + void testSetProfileWantAvatarUpload() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + + final ProfileAvatarUploadAttributes uploadAttributes = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(new CreateProfileRequest(commitment, "someversion", + name, null, null, + null, true, false, List.of()), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class); + + final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID), eq("someversion")); + verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID), profileArgumentCaptor.capture()); + + verifyNoMoreInteractions(s3client); + + assertThat(profileArgumentCaptor.getValue().commitment()).isEqualTo(commitment.serialize()); + assertThat(profileArgumentCaptor.getValue().avatar()).isEqualTo(uploadAttributes.getKey()); + assertThat(profileArgumentCaptor.getValue().version()).isEqualTo("someversion"); + assertThat(profileArgumentCaptor.getValue().name()).isEqualTo(name); + assertThat(profileArgumentCaptor.getValue().aboutEmoji()).isNull(); + assertThat(profileArgumentCaptor.getValue().about()).isNull(); + } + + @Test + void testSetProfileWantAvatarUploadWithBadProfileSize() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + final byte[] name = ProfileTestHelper.generateRandomByteArray(82); + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(new CreateProfileRequest(commitment, "someversion", name, + null, null, null, true, false, List.of()), MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(422); + } + } + + @Test + void testSetProfileWithoutAvatarUpload() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + + clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", name, null, null, + null, false, false, List.of()), MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("anotherversion")); + verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture()); + + verifyNoMoreInteractions(s3client); + + assertThat(profileArgumentCaptor.getValue().commitment()).isEqualTo(commitment.serialize()); + assertThat(profileArgumentCaptor.getValue().avatar()).isNull(); + assertThat(profileArgumentCaptor.getValue().version()).isEqualTo("anotherversion"); + assertThat(profileArgumentCaptor.getValue().name()).isEqualTo(name); + assertThat(profileArgumentCaptor.getValue().aboutEmoji()).isNull(); + assertThat(profileArgumentCaptor.getValue().about()).isNull(); + } + } + + @Test + void testSetProfileWithAvatarUploadAndPreviousAvatar() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID_TWO)); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + + resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", + name, null, null, + null, true, false, List.of()), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class); + + final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion")); + verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture()); + verify(s3client, times(1)).deleteObject(eq(DeleteObjectRequest.builder().bucket("profilesBucket").key("profiles/validavatar").build())); + + assertThat(profileArgumentCaptor.getValue().commitment()).isEqualTo(commitment.serialize()); + assertThat(profileArgumentCaptor.getValue().avatar()).startsWith("profiles/"); + assertThat(profileArgumentCaptor.getValue().version()).isEqualTo("validversion"); + assertThat(profileArgumentCaptor.getValue().name()).isEqualTo(name); + assertThat(profileArgumentCaptor.getValue().aboutEmoji()).isNull(); + assertThat(profileArgumentCaptor.getValue().about()).isNull(); + } + + @Test + void testSetProfileClearPreviousAvatar() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID_TWO)); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", name, + null, null, null, false, false, List.of()), MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion")); + verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture()); + verify(s3client, times(1)).deleteObject(eq(DeleteObjectRequest.builder().bucket("profilesBucket").key("profiles/validavatar").build())); + + assertThat(profileArgumentCaptor.getValue().commitment()).isEqualTo(commitment.serialize()); + assertThat(profileArgumentCaptor.getValue().avatar()).isNull(); + assertThat(profileArgumentCaptor.getValue().version()).isEqualTo("validversion"); + assertThat(profileArgumentCaptor.getValue().name()).isEqualTo(name); + assertThat(profileArgumentCaptor.getValue().aboutEmoji()).isNull(); + assertThat(profileArgumentCaptor.getValue().about()).isNull(); + } + } + + @Test + void testSetProfileWithSameAvatar() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID_TWO)); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", name, + null, null, null, true, true, List.of()), MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion")); + verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture()); + verify(s3client, never()).deleteObject(any(DeleteObjectRequest.class)); + + assertThat(profileArgumentCaptor.getValue().commitment()).isEqualTo(commitment.serialize()); + assertThat(profileArgumentCaptor.getValue().avatar()).isEqualTo("profiles/validavatar"); + assertThat(profileArgumentCaptor.getValue().version()).isEqualTo("validversion"); + assertThat(profileArgumentCaptor.getValue().name()).isEqualTo(name); + assertThat(profileArgumentCaptor.getValue().aboutEmoji()).isNull(); + assertThat(profileArgumentCaptor.getValue().about()).isNull(); + } + } + + @Test + void testSetProfileClearPreviousAvatarDespiteSameAvatarFlagSet() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID_TWO)); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + + try (final Response ignored = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", name, + null, null, + null, false, true, List.of()), MediaType.APPLICATION_JSON_TYPE))) { + + final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion")); + verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture()); + verify(s3client, times(1)).deleteObject(eq(DeleteObjectRequest.builder().bucket("profilesBucket").key("profiles/validavatar").build())); + + assertThat(profileArgumentCaptor.getValue().commitment()).isEqualTo(commitment.serialize()); + assertThat(profileArgumentCaptor.getValue().avatar()).isNull(); + assertThat(profileArgumentCaptor.getValue().version()).isEqualTo("validversion"); + assertThat(profileArgumentCaptor.getValue().name()).isEqualTo(name); + assertThat(profileArgumentCaptor.getValue().aboutEmoji()).isNull(); + assertThat(profileArgumentCaptor.getValue().about()).isNull(); + } + } + + @Test + void testSetProfileWithSameAvatarDespiteNoPreviousAvatar() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", name, + null, null, null, true, true, List.of()), MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID), eq("validversion")); + verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID), profileArgumentCaptor.capture()); + verify(s3client, never()).deleteObject(any(DeleteObjectRequest.class)); + + assertThat(profileArgumentCaptor.getValue().commitment()).isEqualTo(commitment.serialize()); + assertThat(profileArgumentCaptor.getValue().avatar()).isNull(); + assertThat(profileArgumentCaptor.getValue().version()).isEqualTo("validversion"); + assertThat(profileArgumentCaptor.getValue().name()).isEqualTo(name); + assertThat(profileArgumentCaptor.getValue().aboutEmoji()).isNull(); + assertThat(profileArgumentCaptor.getValue().about()).isNull(); + } + } + + @Test + void testSetProfileExtendedName() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID_TWO)); + + final byte[] name = ProfileTestHelper.generateRandomByteArray(285); + + resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity( + new CreateProfileRequest(commitment, "validversion", name, + null, null, null, true, false, List.of()), + MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class); + + final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion")); + verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture()); + verify(s3client, times(1)).deleteObject(eq(DeleteObjectRequest.builder().bucket("profilesBucket").key("profiles/validavatar").build())); + + assertThat(profileArgumentCaptor.getValue().commitment()).isEqualTo(commitment.serialize()); + assertThat(profileArgumentCaptor.getValue().avatar()).startsWith("profiles/"); + assertThat(profileArgumentCaptor.getValue().version()).isEqualTo("validversion"); + assertThat(profileArgumentCaptor.getValue().name()).isEqualTo(name); + assertThat(profileArgumentCaptor.getValue().aboutEmoji()).isNull(); + assertThat(profileArgumentCaptor.getValue().about()).isNull(); + } + + @Test + void testSetProfileEmojiAndBioText() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + + clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); + + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] emoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] about = ProfileTestHelper.generateRandomByteArray(156); + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity( + new CreateProfileRequest(commitment, "anotherversion", name, emoji, about, null, + false, false, List.of()), + MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("anotherversion")); + verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture()); + + verifyNoMoreInteractions(s3client); + + final VersionedProfile profile = profileArgumentCaptor.getValue(); + assertThat(profile.commitment()).isEqualTo(commitment.serialize()); + assertThat(profile.avatar()).isNull(); + assertThat(profile.version()).isEqualTo("anotherversion"); + assertThat(profile.name()).isEqualTo(name); + assertThat(profile.aboutEmoji()).isEqualTo(emoji); + assertThat(profile.about()).isEqualTo(about); + assertThat(profile.paymentAddress()).isNull(); + } + } + + @Test + void testSetProfilePaymentAddress() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + + clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); + + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] paymentAddress = ProfileTestHelper.generateRandomByteArray(582); + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity( + new CreateProfileRequest(commitment, "yetanotherversion", name, + null, null, paymentAddress, false, false, + List.of()), MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager).get(eq(AuthHelper.VALID_UUID_TWO), eq("yetanotherversion")); + verify(profilesManager).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture()); + + verifyNoMoreInteractions(s3client); + + final VersionedProfile profile = profileArgumentCaptor.getValue(); + assertThat(profile.commitment()).isEqualTo(commitment.serialize()); + assertThat(profile.avatar()).isNull(); + assertThat(profile.version()).isEqualTo("yetanotherversion"); + assertThat(profile.name()).isEqualTo(name); + assertThat(profile.aboutEmoji()).isNull(); + assertThat(profile.about()).isNull(); + assertThat(profile.paymentAddress()).isEqualTo(paymentAddress); + } + } + + @Test + void testSetProfilePaymentAddressCountryNotAllowed() throws InvalidInputException { + when(dynamicPaymentsConfiguration.getDisallowedPrefixes()) + .thenReturn(List.of(AuthHelper.VALID_NUMBER_TWO.substring(0, 3))); + + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + + clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); + + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] paymentAddress = ProfileTestHelper.generateRandomByteArray(582); + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity( + new CreateProfileRequest(commitment, "yetanotherversion", name, + null, null, paymentAddress, false, false, + List.of()), MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.hasEntity()).isFalse(); + + verify(profilesManager, never()).set(any(), any()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSetProfilePaymentAddressCountryNotAllowedExistingPaymentAddress( + final boolean existingPaymentAddressOnProfile) throws InvalidInputException { + when(dynamicPaymentsConfiguration.getDisallowedPrefixes()) + .thenReturn(List.of(AuthHelper.VALID_NUMBER_TWO.substring(0, 3))); + + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] paymentAddress = ProfileTestHelper.generateRandomByteArray(582); + + clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); + + when(profilesManager.get(eq(AuthHelper.VALID_UUID_TWO), any())) + .thenReturn(Optional.of( + new VersionedProfile("1", name, null, null, null, + existingPaymentAddressOnProfile ? ProfileTestHelper.generateRandomByteArray(582) : null, + commitment.serialize()))); + + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity( + new CreateProfileRequest(commitment, "yetanotherversion", name, + null, null, paymentAddress, false, false, + List.of()), MediaType.APPLICATION_JSON_TYPE))) { + + if (existingPaymentAddressOnProfile) { + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager).get(eq(AuthHelper.VALID_UUID_TWO), eq("yetanotherversion")); + verify(profilesManager).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture()); + + verifyNoMoreInteractions(s3client); + + final VersionedProfile profile = profileArgumentCaptor.getValue(); + assertThat(profile.commitment()).isEqualTo(commitment.serialize()); + assertThat(profile.avatar()).isNull(); + assertThat(profile.version()).isEqualTo("yetanotherversion"); + assertThat(profile.name()).isEqualTo(name); + assertThat(profile.aboutEmoji()).isNull(); + assertThat(profile.about()).isNull(); + assertThat(profile.paymentAddress()).isEqualTo(paymentAddress); + } else { + assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.hasEntity()).isFalse(); + + verify(profilesManager, never()).set(any(), any()); + } + } + } + + @Test + void testGetProfileByVersion() throws RateLimitExceededException { + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] emoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] about = ProfileTestHelper.generateRandomByteArray(156); + + when(profilesManager.get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"))).thenReturn(Optional.of(new VersionedProfile( + "validversion", name, "profiles/validavatar", emoji, about, null, "validcommitmnet".getBytes()))); + + final VersionedProfileResponse profile = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO + "/validversion") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(VersionedProfileResponse.class); + + assertThat(profile.getBaseProfileResponse().getIdentityKey()).isEqualTo(ACCOUNT_TWO_IDENTITY_KEY); + assertThat(profile.getName()).containsExactly(name); + assertThat(profile.getAbout()).containsExactly(about); + assertThat(profile.getAboutEmoji()).containsExactly(emoji); + assertThat(profile.getAvatar()).isEqualTo("profiles/validavatar"); + assertThat(profile.getBaseProfileResponse().getCapabilities().gv1Migration()).isTrue(); + assertThat(profile.getBaseProfileResponse().getUuid()).isEqualTo(new AciServiceIdentifier(AuthHelper.VALID_UUID_TWO)); + assertThat(profile.getBaseProfileResponse().getBadges()).hasSize(1).element(0).has(new Condition<>( + badge -> "Test Badge".equals(badge.getName()), "has badge with expected name")); + + verify(accountsManager, times(1)).getByAccountIdentifier(eq(AuthHelper.VALID_UUID_TWO)); + verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion")); + + verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID); + } + + @Test + void testSetProfileUpdatesAccountCurrentVersion() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID_TWO)); + + clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); + + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] paymentAddress = ProfileTestHelper.generateRandomByteArray(582); + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity( + new CreateProfileRequest(commitment, "someversion", name, null, null, paymentAddress, false, false, + List.of()), MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + verify(AuthHelper.VALID_ACCOUNT_TWO).setCurrentProfileVersion("someversion"); + } + } + + @Test + void testGetProfileReturnsNoPaymentAddressIfCurrentVersionMismatch() { + final byte[] paymentAddress = ProfileTestHelper.generateRandomByteArray(582); + when(profilesManager.get(AuthHelper.VALID_UUID_TWO, "validversion")).thenReturn( + Optional.of(new VersionedProfile(null, null, null, null, null, paymentAddress, null))); + + { + final VersionedProfileResponse profile = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO + "/validversion") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(VersionedProfileResponse.class); + + assertThat(profile.getPaymentAddress()).containsExactly(paymentAddress); + } + + when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.of("validversion")); + + { + final VersionedProfileResponse profile = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO + "/validversion") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(VersionedProfileResponse.class); + + assertThat(profile.getPaymentAddress()).containsExactly(paymentAddress); + } + + when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.of("someotherversion")); + + { + final VersionedProfileResponse profile = resources.getJerseyTest() + .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO + "/validversion") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(VersionedProfileResponse.class); + + assertThat(profile.getPaymentAddress()).isNull(); + } + } + + @Test + void testGetProfileWithExpiringProfileKeyCredentialVersionNotFound() throws VerificationFailedException { + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(AuthHelper.VALID_UUID); + when(account.getCurrentProfileVersion()).thenReturn(Optional.of("version")); + when(account.isEnabled()).thenReturn(true); + + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); + when(profilesManager.get(any(), any())).thenReturn(Optional.empty()); + + final ExpiringProfileKeyCredentialProfileResponse profile = resources.getJerseyTest() + .target(String.format("/v1/profile/%s/%s/%s", AuthHelper.VALID_UUID, "version-that-does-not-exist", "credential-request")) + .queryParam("credentialType", "expiringProfileKey") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(ExpiringProfileKeyCredentialProfileResponse.class); + + assertThat(profile.getVersionedProfileResponse().getBaseProfileResponse().getUuid()) + .isEqualTo(new AciServiceIdentifier(AuthHelper.VALID_UUID)); + + assertThat(profile.getCredential()).isNull(); + + verify(zkProfileOperations, never()).issueExpiringProfileKeyCredential(any(), any(), any(), any()); + } + + @Test + void testSetProfileBadges() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + + clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); + + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] emoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] about = ProfileTestHelper.generateRandomByteArray(156); + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", name, emoji, about, null, false, false, + List.of("TEST2")), MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + @SuppressWarnings("unchecked") + final ArgumentCaptor> badgeCaptor = ArgumentCaptor.forClass(List.class); + verify(AuthHelper.VALID_ACCOUNT_TWO).setBadges(refEq(clock), badgeCaptor.capture()); + + final List badges = badgeCaptor.getValue(); + assertThat(badges).isNotNull().hasSize(1).containsOnly(new AccountBadge("TEST2", Instant.ofEpochSecond(42 + 86400), true)); + + clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); + when(AuthHelper.VALID_ACCOUNT_TWO.getBadges()).thenReturn(List.of( + new AccountBadge("TEST2", Instant.ofEpochSecond(42 + 86400), true) + )); + } + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", name, emoji, about, null, false, false, + List.of("TEST3", "TEST2")), MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + //noinspection unchecked + final ArgumentCaptor> badgeCaptor = ArgumentCaptor.forClass(List.class); + verify(AuthHelper.VALID_ACCOUNT_TWO).setBadges(refEq(clock), badgeCaptor.capture()); + + final List badges = badgeCaptor.getValue(); + assertThat(badges).isNotNull().hasSize(2).containsOnly( + new AccountBadge("TEST3", Instant.ofEpochSecond(42 + 86400), true), + new AccountBadge("TEST2", Instant.ofEpochSecond(42 + 86400), true)); + + clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); + when(AuthHelper.VALID_ACCOUNT_TWO.getBadges()).thenReturn(List.of( + new AccountBadge("TEST3", Instant.ofEpochSecond(42 + 86400), true), + new AccountBadge("TEST2", Instant.ofEpochSecond(42 + 86400), true) + )); + } + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", name, emoji, about, null, false, false, + List.of("TEST2", "TEST3")), MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + //noinspection unchecked + final ArgumentCaptor> badgeCaptor = ArgumentCaptor.forClass(List.class); + verify(AuthHelper.VALID_ACCOUNT_TWO).setBadges(refEq(clock), badgeCaptor.capture()); + + final List badges = badgeCaptor.getValue(); + assertThat(badges).isNotNull().hasSize(2).containsOnly( + new AccountBadge("TEST2", Instant.ofEpochSecond(42 + 86400), true), + new AccountBadge("TEST3", Instant.ofEpochSecond(42 + 86400), true)); + + clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); + when(AuthHelper.VALID_ACCOUNT_TWO.getBadges()).thenReturn(List.of( + new AccountBadge("TEST2", Instant.ofEpochSecond(42 + 86400), true), + new AccountBadge("TEST3", Instant.ofEpochSecond(42 + 86400), true) + )); + } + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", name, emoji, about, null, false, false, + List.of("TEST1")), MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + //noinspection unchecked + final ArgumentCaptor> badgeCaptor = ArgumentCaptor.forClass(List.class); + verify(AuthHelper.VALID_ACCOUNT_TWO).setBadges(refEq(clock), badgeCaptor.capture()); + + final List badges = badgeCaptor.getValue(); + assertThat(badges).isNotNull().hasSize(3).containsOnly( + new AccountBadge("TEST1", Instant.ofEpochSecond(42 + 86400), true), + new AccountBadge("TEST2", Instant.ofEpochSecond(42 + 86400), false), + new AccountBadge("TEST3", Instant.ofEpochSecond(42 + 86400), false)); + } + } + + @ParameterizedTest + @MethodSource + void testGetProfileWithExpiringProfileKeyCredential(final MultivaluedMap authHeaders) + throws VerificationFailedException, InvalidInputException { + final String version = "version"; + + final ServerSecretParams serverSecretParams = ServerSecretParams.generate(); + final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams(); + + final ServerZkProfileOperations serverZkProfile = new ServerZkProfileOperations(serverSecretParams); + final ClientZkProfileOperations clientZkProfile = new ClientZkProfileOperations(serverPublicParams); + + final byte[] profileKeyBytes = new byte[32]; + new SecureRandom().nextBytes(profileKeyBytes); + + final ProfileKey profileKey = new ProfileKey(profileKeyBytes); + final ProfileKeyCommitment profileKeyCommitment = profileKey.getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + + final VersionedProfile versionedProfile = mock(VersionedProfile.class); + when(versionedProfile.commitment()).thenReturn(profileKeyCommitment.serialize()); + + final ProfileKeyCredentialRequestContext profileKeyCredentialRequestContext = + clientZkProfile.createProfileKeyCredentialRequestContext(new ServiceId.Aci(AuthHelper.VALID_UUID), profileKey); + + final ProfileKeyCredentialRequest credentialRequest = profileKeyCredentialRequestContext.getRequest(); + + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(AuthHelper.VALID_UUID); + when(account.getCurrentProfileVersion()).thenReturn(Optional.of(version)); + when(account.isEnabled()).thenReturn(true); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY)); + + final Instant expiration = Instant.now().plus(org.whispersystems.textsecuregcm.util.ProfileHelper.EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION) + .truncatedTo(ChronoUnit.DAYS); + + final ExpiringProfileKeyCredentialResponse credentialResponse = + serverZkProfile.issueExpiringProfileKeyCredential(credentialRequest, new ServiceId.Aci(AuthHelper.VALID_UUID), profileKeyCommitment, expiration); + + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); + when(profilesManager.get(AuthHelper.VALID_UUID, version)).thenReturn(Optional.of(versionedProfile)); + when(zkProfileOperations.issueExpiringProfileKeyCredential(eq(credentialRequest), eq(new ServiceId.Aci(AuthHelper.VALID_UUID)), eq(profileKeyCommitment), any())) + .thenReturn(credentialResponse); + + final ExpiringProfileKeyCredentialProfileResponse profile = resources.getJerseyTest() + .target(String.format("/v1/profile/%s/%s/%s", AuthHelper.VALID_UUID, version, + HexFormat.of().formatHex(credentialRequest.serialize()))) + .queryParam("credentialType", "expiringProfileKey") + .request() + .headers(authHeaders) + .get(ExpiringProfileKeyCredentialProfileResponse.class); + + assertThat(profile.getVersionedProfileResponse().getBaseProfileResponse().getUuid()) + .isEqualTo(new AciServiceIdentifier(AuthHelper.VALID_UUID)); + assertThat(profile.getCredential()).isEqualTo(credentialResponse); + + verify(zkProfileOperations).issueExpiringProfileKeyCredential(credentialRequest, new ServiceId.Aci(AuthHelper.VALID_UUID), profileKeyCommitment, expiration); + + final ClientZkProfileOperations clientZkProfileCipher = new ClientZkProfileOperations(serverPublicParams); + assertThatNoException().isThrownBy(() -> + clientZkProfileCipher.receiveExpiringProfileKeyCredential(profileKeyCredentialRequestContext, profile.getCredential())); + } + + private static Stream testGetProfileWithExpiringProfileKeyCredential() { + return Stream.of( + Arguments.of(new MultivaluedHashMap<>(Map.of(OptionalAccess.UNIDENTIFIED, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_KEY)))), + Arguments.of(new MultivaluedHashMap<>(Map.of("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)))), + Arguments.of(new MultivaluedHashMap<>(Map.of("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)))) + ); + } + + @Test + void testGetProfileWithExpiringProfileKeyCredentialBadRequest() + throws VerificationFailedException, InvalidInputException { + final String version = "version"; + + final ServerSecretParams serverSecretParams = ServerSecretParams.generate(); + final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams(); + + final ClientZkProfileOperations clientZkProfile = new ClientZkProfileOperations(serverPublicParams); + + final byte[] profileKeyBytes = new byte[32]; + new SecureRandom().nextBytes(profileKeyBytes); + + final ProfileKey profileKey = new ProfileKey(profileKeyBytes); + final ProfileKeyCommitment profileKeyCommitment = profileKey.getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + + final VersionedProfile versionedProfile = mock(VersionedProfile.class); + when(versionedProfile.commitment()).thenReturn(profileKeyCommitment.serialize()); + + final ProfileKeyCredentialRequestContext profileKeyCredentialRequestContext = + clientZkProfile.createProfileKeyCredentialRequestContext(new ServiceId.Aci(AuthHelper.VALID_UUID), profileKey); + + final ProfileKeyCredentialRequest credentialRequest = profileKeyCredentialRequestContext.getRequest(); + + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(AuthHelper.VALID_UUID); + when(account.isEnabled()).thenReturn(true); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY)); + + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); + when(profilesManager.get(AuthHelper.VALID_UUID, version)).thenReturn(Optional.of(versionedProfile)); + when(zkProfileOperations.issueExpiringProfileKeyCredential(any(), any(), any(), any())) + .thenThrow(new VerificationFailedException()); + + final Response response = resources.getJerseyTest() + .target(String.format("/v1/profile/%s/%s/%s", AuthHelper.VALID_UUID, version, + HexFormat.of().formatHex(credentialRequest.serialize()))) + .queryParam("credentialType", "expiringProfileKey") + .request() + .headers(new MultivaluedHashMap<>(Map.of(OptionalAccess.UNIDENTIFIED, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_KEY)))) + .get(); + + assertEquals(400, response.getStatus()); + } + + @Test + void testSetProfileBadgesMissingFromRequest() throws InvalidInputException { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + + clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); + + final String name = ProfileTestHelper.generateRandomBase64FromByteArray(81); + final String emoji = ProfileTestHelper.generateRandomBase64FromByteArray(60); + final String text = ProfileTestHelper.generateRandomBase64FromByteArray(156); + + when(AuthHelper.VALID_ACCOUNT_TWO.getBadges()).thenReturn(List.of( + new AccountBadge("TEST", Instant.ofEpochSecond(42 + 86400), true) + )); + + // Older clients may not include badges in their requests + final String requestJson = String.format(""" + { + "commitment": "%s", + "version": "version", + "name": "%s", + "avatar": false, + "aboutEmoji": "%s", + "about": "%s" + } + """, + Base64.getEncoder().encodeToString(commitment.serialize()), name, emoji, text); + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.json(requestJson))) { + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + verify(AuthHelper.VALID_ACCOUNT_TWO).setBadges(refEq(clock), eq(List.of(new AccountBadge("TEST", Instant.ofEpochSecond(42 + 86400), true)))); + } + } + + @Test + void testBatchIdentityCheck() { + try (final Response response = resources.getJerseyTest().target("/v1/profile/identity_check/batch").request() + .post(Entity.json(new BatchIdentityCheckRequest(List.of( + new BatchIdentityCheckRequest.Element(new AciServiceIdentifier(AuthHelper.VALID_UUID), null, + convertKeyToFingerprint(ACCOUNT_IDENTITY_KEY)), + new BatchIdentityCheckRequest.Element(new PniServiceIdentifier(AuthHelper.VALID_PNI_TWO), null, + convertKeyToFingerprint(ACCOUNT_TWO_PHONE_NUMBER_IDENTITY_KEY)), + new BatchIdentityCheckRequest.Element(null, new AciServiceIdentifier(AuthHelper.VALID_UUID_TWO), + convertKeyToFingerprint(ACCOUNT_TWO_IDENTITY_KEY)), + new BatchIdentityCheckRequest.Element(new AciServiceIdentifier(AuthHelper.INVALID_UUID), null, + convertKeyToFingerprint(ACCOUNT_TWO_PHONE_NUMBER_IDENTITY_KEY)) + ))))) { + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(200); + BatchIdentityCheckResponse identityCheckResponse = response.readEntity(BatchIdentityCheckResponse.class); + assertThat(identityCheckResponse).isNotNull(); + assertThat(identityCheckResponse.elements()).isNotNull().isEmpty(); + } + + final Map expectedIdentityKeys = Map.of( + new AciServiceIdentifier(AuthHelper.VALID_UUID), ACCOUNT_IDENTITY_KEY, + new PniServiceIdentifier(AuthHelper.VALID_PNI_TWO), ACCOUNT_TWO_PHONE_NUMBER_IDENTITY_KEY, + new AciServiceIdentifier(AuthHelper.VALID_UUID_TWO), ACCOUNT_TWO_IDENTITY_KEY); + + final Condition isAnExpectedUuid = + new Condition<>(element -> element.identityKey() + .equals(expectedIdentityKeys.get(Objects.requireNonNullElse(element.uuid(), element.aci()))), + "is an expected UUID with the correct identity key"); + + final IdentityKey validAciIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + final IdentityKey secondValidPniIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + final IdentityKey secondValidAciIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + final IdentityKey invalidAciIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + + try (final Response response = resources.getJerseyTest().target("/v1/profile/identity_check/batch").request() + .post(Entity.json(new BatchIdentityCheckRequest(List.of( + new BatchIdentityCheckRequest.Element(new AciServiceIdentifier(AuthHelper.VALID_UUID), null, + convertKeyToFingerprint(validAciIdentityKey)), + new BatchIdentityCheckRequest.Element(new PniServiceIdentifier(AuthHelper.VALID_PNI_TWO), null, + convertKeyToFingerprint(secondValidPniIdentityKey)), + new BatchIdentityCheckRequest.Element(null, new AciServiceIdentifier(AuthHelper.VALID_UUID_TWO), + convertKeyToFingerprint(secondValidAciIdentityKey)), + new BatchIdentityCheckRequest.Element(new AciServiceIdentifier(AuthHelper.INVALID_UUID), null, + convertKeyToFingerprint(invalidAciIdentityKey)) + ))))) { + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(200); + BatchIdentityCheckResponse identityCheckResponse = response.readEntity(BatchIdentityCheckResponse.class); + assertThat(identityCheckResponse).isNotNull(); + assertThat(identityCheckResponse.elements()).isNotNull().hasSize(3); + assertThat(identityCheckResponse.elements()).element(0).isNotNull().is(isAnExpectedUuid); + assertThat(identityCheckResponse.elements()).element(1).isNotNull().is(isAnExpectedUuid); + assertThat(identityCheckResponse.elements()).element(2).isNotNull().is(isAnExpectedUuid); + } + + final List largeElementList = new ArrayList<>(List.of( + new BatchIdentityCheckRequest.Element(new AciServiceIdentifier(AuthHelper.VALID_UUID), null, + convertKeyToFingerprint(validAciIdentityKey)), + new BatchIdentityCheckRequest.Element(new PniServiceIdentifier(AuthHelper.VALID_PNI_TWO), null, + convertKeyToFingerprint(secondValidPniIdentityKey)), + new BatchIdentityCheckRequest.Element(new AciServiceIdentifier(AuthHelper.INVALID_UUID), null, + convertKeyToFingerprint(invalidAciIdentityKey)))); + + for (int i = 0; i < 900; i++) { + largeElementList.add( + new BatchIdentityCheckRequest.Element(new AciServiceIdentifier(UUID.randomUUID()), null, + convertKeyToFingerprint(new IdentityKey(Curve.generateKeyPair().getPublicKey())))); + } + + try (final Response response = resources.getJerseyTest().target("/v1/profile/identity_check/batch").request() + .post(Entity.json(new BatchIdentityCheckRequest(largeElementList)))) { + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(200); + BatchIdentityCheckResponse identityCheckResponse = response.readEntity(BatchIdentityCheckResponse.class); + assertThat(identityCheckResponse).isNotNull(); + assertThat(identityCheckResponse.elements()).isNotNull().hasSize(2); + assertThat(identityCheckResponse.elements()).element(0).isNotNull().is(isAnExpectedUuid); + assertThat(identityCheckResponse.elements()).element(1).isNotNull().is(isAnExpectedUuid); + } + } + + @Test + void testBatchIdentityCheckDeserialization() throws Exception { + + final Map expectedIdentityKeys = Map.of( + new AciServiceIdentifier(AuthHelper.VALID_UUID), ACCOUNT_IDENTITY_KEY, + new PniServiceIdentifier(AuthHelper.VALID_PNI_TWO), ACCOUNT_TWO_PHONE_NUMBER_IDENTITY_KEY); + + final Condition isAnExpectedUuid = + new Condition<>(element -> element.identityKey().equals(expectedIdentityKeys.get(element.uuid())), + "is an expected UUID with the correct identity key"); + + // null properties are ok to omit + final String json = String.format(""" + { + "elements": [ + { "uuid": "%s", "fingerprint": "%s" }, + { "uuid": "%s", "fingerprint": "%s" }, + { "uuid": "%s", "fingerprint": "%s" } + ] + } + """, AuthHelper.VALID_UUID, Base64.getEncoder().encodeToString(convertKeyToFingerprint(new IdentityKey(Curve.generateKeyPair().getPublicKey()))), + "PNI:" + AuthHelper.VALID_PNI_TWO, Base64.getEncoder().encodeToString(convertKeyToFingerprint(new IdentityKey(Curve.generateKeyPair().getPublicKey()))), + AuthHelper.INVALID_UUID, Base64.getEncoder().encodeToString(convertKeyToFingerprint(new IdentityKey(Curve.generateKeyPair().getPublicKey())))); + + try (final Response response = resources.getJerseyTest().target("/v1/profile/identity_check/batch").request() + .post(Entity.entity(json, "application/json"))) { + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(200); + String responseJson = response.readEntity(String.class); + + // `null` properties should be omitted from the response + assertThat(responseJson).doesNotContain("null"); + + final BatchIdentityCheckResponse identityCheckResponse = + SystemMapper.jsonMapper().readValue(responseJson, BatchIdentityCheckResponse.class); + + assertThat(identityCheckResponse).isNotNull(); + assertThat(identityCheckResponse.elements()).isNotNull().hasSize(2); + assertThat(identityCheckResponse.elements()).element(0).isNotNull().is(isAnExpectedUuid); + assertThat(identityCheckResponse.elements()).element(1).isNotNull().is(isAnExpectedUuid); + } + } + + @ParameterizedTest + @MethodSource + void testBatchIdentityCheckDeserializationBadRequest(final String json, final int expectedStatus) { + try (final Response response = resources.getJerseyTest().target("/v1/profile/identity_check/batch").request() + .post(Entity.entity(json, "application/json"))) { + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(expectedStatus); + } + } + + static Stream testBatchIdentityCheckDeserializationBadRequest() { + return Stream.of( + Arguments.of( // aci and uuid cannot both be null + String.format(""" + { + "elements": [ + { "uuid": null, "fingerprint": "%s" } + ] + } + """, Base64.getEncoder().encodeToString(convertKeyToFingerprint(new IdentityKey(Curve.generateKeyPair().getPublicKey())))), + 400), + Arguments.of( // a blank string is invalid + String.format(""" + { + "elements": [ + { "uuid": " ", "fingerprint": "%s" } + ] + } + """, Base64.getEncoder().encodeToString(convertKeyToFingerprint(new IdentityKey(Curve.generateKeyPair().getPublicKey())))), + 400) + ); + } + + private static byte[] convertKeyToFingerprint(final IdentityKey publicKey) { + try { + return Util.truncate(MessageDigest.getInstance("SHA-256").digest(publicKey.serialize()), 4); + } catch (final NoSuchAlgorithmException e) { + throw new AssertionError("All Java implementations must support SHA-256 MessageDigest algorithm", e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProvisioningControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProvisioningControllerTest.java new file mode 100644 index 000000000..bfd0d67e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProvisioningControllerTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.UUID; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.entities.ProvisioningMessage; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.push.ProvisioningManager; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress; + +@ExtendWith(DropwizardExtensionsSupport.class) +class ProvisioningControllerTest { + + private RateLimiter messagesRateLimiter; + + private static final RateLimiters rateLimiters = mock(RateLimiters.class); + private static final ProvisioningManager provisioningManager = mock(ProvisioningManager.class); + + private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .addProvider(new RateLimitExceededExceptionMapper()) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new ProvisioningController(rateLimiters, provisioningManager)) + .build(); + + @BeforeEach + void setUp() { + reset(rateLimiters, provisioningManager); + + messagesRateLimiter = mock(RateLimiter.class); + when(rateLimiters.getMessagesLimiter()).thenReturn(messagesRateLimiter); + } + + @Test + void sendProvisioningMessage() { + final String destination = UUID.randomUUID().toString(); + final byte[] messageBody = "test".getBytes(StandardCharsets.UTF_8); + + when(provisioningManager.sendProvisioningMessage(any(), any())).thenReturn(true); + + try (final Response response = RESOURCE_EXTENSION.getJerseyTest() + .target("/v1/provisioning/" + destination) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(new ProvisioningMessage(Base64.getMimeEncoder().encodeToString(messageBody)), + MediaType.APPLICATION_JSON))) { + + assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); + + final ArgumentCaptor provisioningAddressCaptor = + ArgumentCaptor.forClass(ProvisioningAddress.class); + + final ArgumentCaptor provisioningMessageCaptor = ArgumentCaptor.forClass(byte[].class); + + verify(provisioningManager).sendProvisioningMessage(provisioningAddressCaptor.capture(), + provisioningMessageCaptor.capture()); + + assertEquals(destination, provisioningAddressCaptor.getValue().getAddress()); + assertEquals(0, provisioningAddressCaptor.getValue().getDeviceId()); + + assertArrayEquals(messageBody, provisioningMessageCaptor.getValue()); + } + } + + @Test + void sendProvisioningMessageRateLimited() throws RateLimitExceededException { + final String destination = UUID.randomUUID().toString(); + final byte[] messageBody = "test".getBytes(StandardCharsets.UTF_8); + + doThrow(new RateLimitExceededException(Duration.ZERO, true)) + .when(messagesRateLimiter).validate(AuthHelper.VALID_UUID); + + try (final Response response = RESOURCE_EXTENSION.getJerseyTest() + .target("/v1/provisioning/" + destination) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(new ProvisioningMessage(Base64.getMimeEncoder().encodeToString(messageBody)), + MediaType.APPLICATION_JSON))) { + + assertEquals(413, response.getStatus()); + + verify(provisioningManager, never()).sendProvisioningMessage(any(), any()); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java new file mode 100644 index 000000000..4fee853d5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java @@ -0,0 +1,964 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import org.apache.http.HttpStatus; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.junitpioneer.jupiter.cartesian.ArgumentSets; +import org.junitpioneer.jupiter.cartesian.CartesianTest; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager; +import org.whispersystems.textsecuregcm.auth.RegistrationLockError; +import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.entities.RegistrationRequest; +import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; +import org.whispersystems.textsecuregcm.util.MockUtils; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +@ExtendWith(DropwizardExtensionsSupport.class) +class RegistrationControllerTest { + + private static final long SESSION_EXPIRATION_SECONDS = Duration.ofMinutes(10).toSeconds(); + + private static final String NUMBER = PhoneNumberUtil.getInstance().format( + PhoneNumberUtil.getInstance().getExampleNumber("US"), + PhoneNumberUtil.PhoneNumberFormat.E164); + private static final String PASSWORD = "password"; + + private final AccountsManager accountsManager = mock(AccountsManager.class); + private final RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class); + private final RegistrationLockVerificationManager registrationLockVerificationManager = mock( + RegistrationLockVerificationManager.class); + private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock( + RegistrationRecoveryPasswordsManager.class); + private final KeysManager keysManager = mock(KeysManager.class); + private final RateLimiters rateLimiters = mock(RateLimiters.class); + + private final RateLimiter registrationLimiter = mock(RateLimiter.class); + + private final ResourceExtension resources = ResourceExtension.builder() + .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) + .addProvider(new RateLimitExceededExceptionMapper()) + .addProvider(new ImpossiblePhoneNumberExceptionMapper()) + .addProvider(new NonNormalizedPhoneNumberExceptionMapper()) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource( + new RegistrationController(accountsManager, + new PhoneVerificationTokenManager(registrationServiceClient, registrationRecoveryPasswordsManager), + registrationLockVerificationManager, keysManager, rateLimiters)) + .build(); + + @BeforeEach + void setUp() { + when(rateLimiters.getRegistrationLimiter()).thenReturn(registrationLimiter); + } + + @Test + public void testRegistrationRequest() throws Exception { + assertFalse(new RegistrationRequest("", new byte[0], new AccountAttributes(), true, false, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()).isValid()); + assertFalse(new RegistrationRequest("some", new byte[32], new AccountAttributes(), true, false, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()).isValid()); + assertTrue(new RegistrationRequest("", new byte[32], new AccountAttributes(), true, false, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()).isValid()); + assertTrue(new RegistrationRequest("some", new byte[0], new AccountAttributes(), true, false, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()).isValid()); + } + + @Test + void unprocessableRequestJson() { + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request(); + try (Response response = request.post(Entity.json(unprocessableJson()))) { + assertEquals(400, response.getStatus()); + } + } + + static Stream invalidRegistrationId() { + return Stream.of( + Arguments.of(Optional.of(1), Optional.of(1), 200), + Arguments.of(Optional.of(1), Optional.empty(), 200), + Arguments.of(Optional.of(0x3FFF), Optional.empty(), 200), + Arguments.of(Optional.empty(), Optional.of(1), 422), + Arguments.of(Optional.of(Integer.MAX_VALUE), Optional.empty(), 422), + Arguments.of(Optional.of(0x3FFF + 1), Optional.empty(), 422), + Arguments.of(Optional.of(1), Optional.of(0x3FFF + 1), 422) + ); + } + + @ParameterizedTest + @MethodSource() + void invalidRegistrationId(Optional registrationId, Optional pniRegistrationId, int statusCode) throws InterruptedException, JsonProcessingException { + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn( + CompletableFuture.completedFuture( + Optional.of(new RegistrationServiceSession(new byte[16], NUMBER, true, null, null, null, + SESSION_EXPIRATION_SECONDS)))); + when(accountsManager.create(any(), any(), any(), any(), any())) + .thenReturn(mock(Account.class)); + + final String recoveryPassword = encodeRecoveryPassword(new byte[0]); + + final Map accountAttrs = new HashMap<>(); + accountAttrs.put("recoveryPassword", recoveryPassword); + registrationId.ifPresent(id -> accountAttrs.put("registrationId", id)); + pniRegistrationId.ifPresent(id -> accountAttrs.put("pniRegistrationId", id)); + final String json = SystemMapper.jsonMapper().writeValueAsString(Map.of( + "sessionId", encodeSessionId("sessionId"), + "recoveryPassword", recoveryPassword, + "accountAttributes", accountAttrs, + "skipDeviceTransfer", true + )); + + try (Response response = request.post(Entity.json(json))) { + assertEquals(statusCode, response.getStatus()); + } + } + + @Test + void missingBasicAuthorization() { + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request(); + try (Response response = request.post(Entity.json(requestJson("sessionId")))) { + assertEquals(400, response.getStatus()); + } + } + + @Test + void invalidBasicAuthorization() { + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, "Basic but-invalid"); + try (Response response = request.post(Entity.json(invalidRequestJson()))) { + assertEquals(401, response.getStatus()); + } + } + + @Test + void invalidRequestBody() { + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + try (Response response = request.post(Entity.json(invalidRequestJson()))) { + assertEquals(422, response.getStatus()); + } + } + + @Test + void rateLimitedNumber() throws Exception { + doThrow(RateLimitExceededException.class) + .when(registrationLimiter).validate(NUMBER); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + try (Response response = request.post(Entity.json(requestJson("sessionId")))) { + assertEquals(429, response.getStatus()); + } + } + + @Test + void registrationServiceTimeout() { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.failedFuture(new RuntimeException())); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + try (Response response = request.post(Entity.json(requestJson("sessionId")))) { + assertEquals(HttpStatus.SC_SERVICE_UNAVAILABLE, response.getStatus()); + } + } + + @Test + void recoveryPasswordManagerVerificationFailureOrTimeout() { + when(registrationRecoveryPasswordsManager.verify(any(), any())) + .thenReturn(CompletableFuture.failedFuture(new RuntimeException())); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + try (Response response = request.post(Entity.json(requestJsonRecoveryPassword(new byte[32])))) { + assertEquals(HttpStatus.SC_SERVICE_UNAVAILABLE, response.getStatus()); + } + } + + @ParameterizedTest + @MethodSource + void registrationServiceSessionCheck(@Nullable final RegistrationServiceSession session, final int expectedStatus, + final String message) { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture(Optional.ofNullable(session))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + try (Response response = request.post(Entity.json(requestJson("sessionId")))) { + assertEquals(expectedStatus, response.getStatus(), message); + } + } + + static Stream registrationServiceSessionCheck() { + return Stream.of( + Arguments.of(null, 401, "session not found"), + Arguments.of( + new RegistrationServiceSession(new byte[16], "+18005551234", false, null, null, null, + SESSION_EXPIRATION_SECONDS), + 400, + "session number mismatch"), + Arguments.of( + new RegistrationServiceSession(new byte[16], NUMBER, false, null, null, null, SESSION_EXPIRATION_SECONDS), + 401, + "session not verified") + ); + } + + @Test + void recoveryPasswordManagerVerificationTrue() throws InterruptedException { + when(registrationRecoveryPasswordsManager.verify(any(), any())) + .thenReturn(CompletableFuture.completedFuture(true)); + when(accountsManager.create(any(), any(), any(), any(), any())) + .thenReturn(mock(Account.class)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + final byte[] recoveryPassword = new byte[32]; + try (Response response = request.post(Entity.json(requestJsonRecoveryPassword(recoveryPassword)))) { + assertEquals(200, response.getStatus()); + } + } + + @Test + void recoveryPasswordManagerVerificationFalse() throws InterruptedException { + when(registrationRecoveryPasswordsManager.verify(any(), any())) + .thenReturn(CompletableFuture.completedFuture(false)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + try (Response response = request.post(Entity.json(requestJsonRecoveryPassword(new byte[32])))) { + assertEquals(403, response.getStatus()); + } + } + + @CartesianTest + @CartesianTest.MethodFactory("registrationLockAndDeviceTransfer") + void registrationLockAndDeviceTransfer( + final boolean deviceTransferSupported, + @Nullable final RegistrationLockError error) + throws Exception { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn( + CompletableFuture.completedFuture( + Optional.of(new RegistrationServiceSession(new byte[16], NUMBER, true, null, null, null, + SESSION_EXPIRATION_SECONDS)))); + + final Account account = mock(Account.class); + when(accountsManager.getByE164(any())).thenReturn(Optional.of(account)); + when(account.isTransferSupported()).thenReturn(deviceTransferSupported); + + final int expectedStatus; + if (deviceTransferSupported) { + expectedStatus = 409; + } else if (error != null) { + final Exception e = switch (error) { + case MISMATCH -> new WebApplicationException(error.getExpectedStatus()); + case RATE_LIMITED -> new RateLimitExceededException(null, true); + }; + doThrow(e) + .when(registrationLockVerificationManager).verifyRegistrationLock(any(), any(), any(), any(), any()); + expectedStatus = error.getExpectedStatus(); + } else { + when(accountsManager.create(any(), any(), any(), any(), any())) + .thenReturn(mock(Account.class)); + expectedStatus = 200; + } + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + try (Response response = request.post(Entity.json(requestJson("sessionId")))) { + assertEquals(expectedStatus, response.getStatus()); + } + } + + @SuppressWarnings("unused") + static ArgumentSets registrationLockAndDeviceTransfer() { + final Set registrationLockErrors = new HashSet<>(EnumSet.allOf(RegistrationLockError.class)); + registrationLockErrors.add(null); + + return ArgumentSets.argumentsForFirstParameter(true, false) + .argumentsForNextParameter(registrationLockErrors); + } + + + @ParameterizedTest + @CsvSource({ + "false, false, false, 200", + "true, false, false, 200", + "true, false, true, 200", + "true, true, false, 409", + "true, true, true, 200" + }) + void deviceTransferAvailable(final boolean existingAccount, final boolean transferSupported, + final boolean skipDeviceTransfer, final int expectedStatus) throws Exception { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn( + CompletableFuture.completedFuture( + Optional.of(new RegistrationServiceSession(new byte[16], NUMBER, true, null, null, null, + SESSION_EXPIRATION_SECONDS)))); + + final Optional maybeAccount; + if (existingAccount) { + final Account account = mock(Account.class); + when(account.isTransferSupported()).thenReturn(transferSupported); + maybeAccount = Optional.of(account); + } else { + maybeAccount = Optional.empty(); + } + when(accountsManager.getByE164(any())).thenReturn(maybeAccount); + when(accountsManager.create(any(), any(), any(), any(), any())).thenReturn(mock(Account.class)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + try (Response response = request.post(Entity.json(requestJson("sessionId", new byte[0], skipDeviceTransfer)))) { + assertEquals(expectedStatus, response.getStatus()); + } + } + + // this is functionally the same as deviceTransferAvailable(existingAccount=false) + @Test + void registrationSuccess() throws Exception { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn( + CompletableFuture.completedFuture( + Optional.of(new RegistrationServiceSession(new byte[16], NUMBER, true, null, null, null, + SESSION_EXPIRATION_SECONDS)))); + when(accountsManager.create(any(), any(), any(), any(), any())) + .thenReturn(mock(Account.class)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + try (Response response = request.post(Entity.json(requestJson("sessionId")))) { + assertEquals(200, response.getStatus()); + } + } + + @ParameterizedTest + @MethodSource + void atomicAccountCreationConflictingChannel(final RegistrationRequest conflictingChannelRequest) { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn( + CompletableFuture.completedFuture( + Optional.of(new RegistrationServiceSession(new byte[16], NUMBER, true, null, null, null, + SESSION_EXPIRATION_SECONDS)))); + + try (final Response response = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)) + .post(Entity.json(conflictingChannelRequest))) { + + assertEquals(422, response.getStatus()); + } + } + + static Stream atomicAccountCreationConflictingChannel() { + final Optional aciIdentityKey; + final Optional pniIdentityKey; + final Optional aciSignedPreKey; + final Optional pniSignedPreKey; + final Optional aciPqLastResortPreKey; + final Optional pniPqLastResortPreKey; + { + final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair(); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + + aciIdentityKey = Optional.of(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + pniIdentityKey = Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair)); + pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); + aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair)); + pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair)); + } + + final AccountAttributes fetchesMessagesAccountAttributes = + new AccountAttributes(true, 1, "test", null, true, new Device.DeviceCapabilities(false, false, false, false)); + + final AccountAttributes pushAccountAttributes = + new AccountAttributes(false, 1, "test", null, true, new Device.DeviceCapabilities(false, false, false, false)); + + return Stream.of( + // "Fetches messages" is true, but an APNs token is provided + Arguments.of(new RegistrationRequest("session-id", + new byte[0], + fetchesMessagesAccountAttributes, + true, + false, + aciIdentityKey, + pniIdentityKey, + aciSignedPreKey, + pniSignedPreKey, + aciPqLastResortPreKey, + pniPqLastResortPreKey, + Optional.of(new ApnRegistrationId("apns-token", null)), + Optional.empty())), + + // "Fetches messages" is true, but an FCM (GCM) token is provided + Arguments.of(new RegistrationRequest("session-id", + new byte[0], + fetchesMessagesAccountAttributes, + true, + false, + aciIdentityKey, + pniIdentityKey, + aciSignedPreKey, + pniSignedPreKey, + aciPqLastResortPreKey, + pniPqLastResortPreKey, + Optional.empty(), + Optional.of(new GcmRegistrationId("gcm-token")))), + + // "Fetches messages" is false, but multiple types of push tokens are provided + Arguments.of(new RegistrationRequest("session-id", + new byte[0], + pushAccountAttributes, + true, + false, + aciIdentityKey, + pniIdentityKey, + aciSignedPreKey, + pniSignedPreKey, + aciPqLastResortPreKey, + pniPqLastResortPreKey, + Optional.of(new ApnRegistrationId("apns-token", null)), + Optional.of(new GcmRegistrationId("gcm-token")))) + ); + } + + @ParameterizedTest + @MethodSource + void atomicAccountCreationPartialSignedPreKeys(final RegistrationRequest partialSignedPreKeyRequest) { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn( + CompletableFuture.completedFuture( + Optional.of(new RegistrationServiceSession(new byte[16], NUMBER, true, null, null, null, + SESSION_EXPIRATION_SECONDS)))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + + try (final Response response = request.post(Entity.json(partialSignedPreKeyRequest))) { + assertEquals(422, response.getStatus()); + } + } + + static Stream atomicAccountCreationPartialSignedPreKeys() { + final Optional aciIdentityKey; + final Optional pniIdentityKey; + final Optional aciSignedPreKey; + final Optional pniSignedPreKey; + final Optional aciPqLastResortPreKey; + final Optional pniPqLastResortPreKey; + { + final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair(); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + + aciIdentityKey = Optional.of(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + pniIdentityKey = Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair)); + pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); + aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair)); + pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair)); + } + + final AccountAttributes accountAttributes = + new AccountAttributes(true, 1, "test", null, true, new Device.DeviceCapabilities(false, false, false, false)); + + return Stream.of( + // Signed PNI EC pre-key is missing + Arguments.of(new RegistrationRequest("session-id", + new byte[0], + accountAttributes, + true, + false, + aciIdentityKey, + pniIdentityKey, + aciSignedPreKey, + Optional.empty(), + aciPqLastResortPreKey, + pniPqLastResortPreKey, + Optional.empty(), + Optional.empty())), + + // Signed ACI EC pre-key is missing + Arguments.of(new RegistrationRequest("session-id", + new byte[0], + accountAttributes, + true, + false, + aciIdentityKey, + pniIdentityKey, + Optional.empty(), + pniSignedPreKey, + aciPqLastResortPreKey, + pniPqLastResortPreKey, + Optional.empty(), + Optional.empty())), + + // Signed PNI KEM pre-key is missing + Arguments.of(new RegistrationRequest("session-id", + new byte[0], + accountAttributes, + true, + false, + aciIdentityKey, + pniIdentityKey, + aciSignedPreKey, + pniSignedPreKey, + aciPqLastResortPreKey, + Optional.empty(), + Optional.empty(), + Optional.empty())), + + // Signed ACI KEM pre-key is missing + Arguments.of(new RegistrationRequest("session-id", + new byte[0], + accountAttributes, + true, + false, + aciIdentityKey, + pniIdentityKey, + aciSignedPreKey, + pniSignedPreKey, + Optional.empty(), + pniPqLastResortPreKey, + Optional.empty(), + Optional.empty())), + + // All signed pre-keys are present, but ACI identity key is missing + Arguments.of(new RegistrationRequest("session-id", + new byte[0], + accountAttributes, + true, + false, + Optional.empty(), + pniIdentityKey, + aciSignedPreKey, + pniSignedPreKey, + aciPqLastResortPreKey, + pniPqLastResortPreKey, + Optional.empty(), + Optional.empty())), + + // All signed pre-keys are present, but PNI identity key is missing + Arguments.of(new RegistrationRequest("session-id", + new byte[0], + accountAttributes, + true, + false, + aciIdentityKey, + Optional.empty(), + aciSignedPreKey, + pniSignedPreKey, + aciPqLastResortPreKey, + pniPqLastResortPreKey, + Optional.empty(), + Optional.empty())) + ); + } + + + @ParameterizedTest + @MethodSource + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + void atomicAccountCreationSuccess(final RegistrationRequest registrationRequest, + final IdentityKey expectedAciIdentityKey, + final IdentityKey expectedPniIdentityKey, + final ECSignedPreKey expectedAciSignedPreKey, + final ECSignedPreKey expectedPniSignedPreKey, + final KEMSignedPreKey expectedAciPqLastResortPreKey, + final KEMSignedPreKey expectedPniPqLastResortPreKey, + final Optional expectedApnsToken, + final Optional expectedApnsVoipToken, + final Optional expectedGcmToken) throws InterruptedException { + + when(registrationServiceClient.getSession(any(), any())) + .thenReturn( + CompletableFuture.completedFuture( + Optional.of(new RegistrationServiceSession(new byte[16], NUMBER, true, null, null, null, + SESSION_EXPIRATION_SECONDS)))); + + when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + final UUID accountIdentifier = UUID.randomUUID(); + final UUID phoneNumberIdentifier = UUID.randomUUID(); + final Device device = mock(Device.class); + + final Account account = MockUtils.buildMock(Account.class, a -> { + when(a.getUuid()).thenReturn(accountIdentifier); + when(a.getPhoneNumberIdentifier()).thenReturn(phoneNumberIdentifier); + when(a.getMasterDevice()).thenReturn(Optional.of(device)); + }); + + when(accountsManager.create(any(), any(), any(), any(), any())).thenReturn(account); + + when(accountsManager.update(eq(account), any())).thenAnswer(invocation -> { + final Consumer accountUpdater = invocation.getArgument(1); + accountUpdater.accept(account); + + return invocation.getArgument(0); + }); + + when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + + try (Response response = request.post(Entity.json(registrationRequest))) { + assertEquals(200, response.getStatus()); + } + + verify(accountsManager).create(any(), any(), any(), any(), any()); + + verify(account).setIdentityKey(expectedAciIdentityKey); + verify(account).setPhoneNumberIdentityKey(expectedPniIdentityKey); + + verify(device).setSignedPreKey(expectedAciSignedPreKey); + verify(device).setPhoneNumberIdentitySignedPreKey(expectedPniSignedPreKey); + + verify(keysManager).storeEcSignedPreKeys(accountIdentifier, Map.of(Device.MASTER_ID, expectedAciSignedPreKey)); + verify(keysManager).storeEcSignedPreKeys(phoneNumberIdentifier, Map.of(Device.MASTER_ID, expectedPniSignedPreKey)); + verify(keysManager).storePqLastResort(accountIdentifier, Map.of(Device.MASTER_ID, expectedAciPqLastResortPreKey)); + verify(keysManager).storePqLastResort(phoneNumberIdentifier, Map.of(Device.MASTER_ID, expectedPniPqLastResortPreKey)); + + expectedApnsToken.ifPresentOrElse(expectedToken -> verify(device).setApnId(expectedToken), + () -> verify(device, never()).setApnId(any())); + + expectedApnsVoipToken.ifPresentOrElse(expectedToken -> verify(device).setVoipApnId(expectedToken), + () -> verify(device, never()).setVoipApnId(any())); + + expectedGcmToken.ifPresentOrElse(expectedToken -> verify(device).setGcmId(expectedToken), + () -> verify(device, never()).setGcmId(any())); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void nonAtomicAccountCreationWithNoAtomicFields(boolean requireAtomic) throws InterruptedException { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn( + CompletableFuture.completedFuture( + Optional.of(new RegistrationServiceSession(new byte[16], NUMBER, true, null, null, null, + SESSION_EXPIRATION_SECONDS)))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + + when(accountsManager.create(any(), any(), any(), any(), any())) + .thenReturn(mock(Account.class)); + + RegistrationRequest reg = new RegistrationRequest("session-id", + new byte[0], + new AccountAttributes(true, 1, "test", null, true, new Device.DeviceCapabilities(false, false, false, false)), + true, + requireAtomic, + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + + try (final Response response = request.post(Entity.json(reg))) { + int expected = requireAtomic ? 422 : 200; + assertEquals(expected, response.getStatus()); + } + } + + private static Stream atomicAccountCreationSuccess() { + final Optional aciIdentityKey; + final Optional pniIdentityKey; + final Optional aciSignedPreKey; + final Optional pniSignedPreKey; + final Optional aciPqLastResortPreKey; + final Optional pniPqLastResortPreKey; + { + final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair(); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + + aciIdentityKey = Optional.of(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + pniIdentityKey = Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair)); + pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); + aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair)); + pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair)); + } + + final AccountAttributes fetchesMessagesAccountAttributes = + new AccountAttributes(true, 1, "test", null, true, new Device.DeviceCapabilities(false, false, false, false)); + + final AccountAttributes pushAccountAttributes = + new AccountAttributes(false, 1, "test", null, true, new Device.DeviceCapabilities(false, false, false, false)); + + final String apnsToken = "apns-token"; + final String apnsVoipToken = "apns-voip-token"; + final String gcmToken = "gcm-token"; + + return Stream.of(false, true) + // try with and without strict atomic checking + .flatMap(requireAtomic -> + Stream.of( + // Fetches messages; no push tokens + Arguments.of(new RegistrationRequest("session-id", + new byte[0], + fetchesMessagesAccountAttributes, + true, + requireAtomic, + aciIdentityKey, + pniIdentityKey, + aciSignedPreKey, + pniSignedPreKey, + aciPqLastResortPreKey, + pniPqLastResortPreKey, + Optional.empty(), + Optional.empty()), + aciIdentityKey.get(), + pniIdentityKey.get(), + aciSignedPreKey.get(), + pniSignedPreKey.get(), + aciPqLastResortPreKey.get(), + pniPqLastResortPreKey.get(), + Optional.empty(), + Optional.empty(), + Optional.empty()), + + // Has APNs tokens + Arguments.of(new RegistrationRequest("session-id", + new byte[0], + pushAccountAttributes, + true, + requireAtomic, + aciIdentityKey, + pniIdentityKey, + aciSignedPreKey, + pniSignedPreKey, + aciPqLastResortPreKey, + pniPqLastResortPreKey, + Optional.of(new ApnRegistrationId(apnsToken, apnsVoipToken)), + Optional.empty()), + aciIdentityKey.get(), + pniIdentityKey.get(), + aciSignedPreKey.get(), + pniSignedPreKey.get(), + aciPqLastResortPreKey.get(), + pniPqLastResortPreKey.get(), + Optional.of(apnsToken), + Optional.of(apnsVoipToken), + Optional.empty()), + + // requires the request to be atomic + Arguments.of(new RegistrationRequest("session-id", + new byte[0], + pushAccountAttributes, + true, + requireAtomic, + aciIdentityKey, + pniIdentityKey, + aciSignedPreKey, + pniSignedPreKey, + aciPqLastResortPreKey, + pniPqLastResortPreKey, + Optional.of(new ApnRegistrationId(apnsToken, apnsVoipToken)), + Optional.empty()), + aciIdentityKey.get(), + pniIdentityKey.get(), + aciSignedPreKey.get(), + pniSignedPreKey.get(), + aciPqLastResortPreKey.get(), + pniPqLastResortPreKey.get(), + Optional.of(apnsToken), + Optional.of(apnsVoipToken), + Optional.empty()), + + // Fetches messages; no push tokens + Arguments.of(new RegistrationRequest("session-id", + new byte[0], + pushAccountAttributes, + true, + requireAtomic, + aciIdentityKey, + pniIdentityKey, + aciSignedPreKey, + pniSignedPreKey, + aciPqLastResortPreKey, + pniPqLastResortPreKey, + Optional.empty(), + Optional.of(new GcmRegistrationId(gcmToken))), + aciIdentityKey.get(), + pniIdentityKey.get(), + aciSignedPreKey.get(), + pniSignedPreKey.get(), + aciPqLastResortPreKey.get(), + pniPqLastResortPreKey.get(), + Optional.empty(), + Optional.empty(), + Optional.of(gcmToken)))); + } + + /** + * Valid request JSON with the give session ID and skipDeviceTransfer + */ + private static String requestJson(final String sessionId, final byte[] recoveryPassword, final boolean skipDeviceTransfer) { + final String rp = encodeRecoveryPassword(recoveryPassword); + return String.format(""" + { + "sessionId": "%s", + "recoveryPassword": "%s", + "accountAttributes": { + "recoveryPassword": "%s", + "registrationId": 1 + }, + "skipDeviceTransfer": %s + } + """, encodeSessionId(sessionId), rp, rp, skipDeviceTransfer); + } + + /** + * Valid request JSON with the given session ID + */ + private static String requestJson(final String sessionId) { + return requestJson(sessionId, new byte[0], false); + } + + /** + * Valid request JSON with the given Recovery Password + */ + private static String requestJsonRecoveryPassword(final byte[] recoveryPassword) { + return requestJson("", recoveryPassword, false); + } + + /** + * Request JSON in the shape of {@link org.whispersystems.textsecuregcm.entities.RegistrationRequest}, but that fails + * validation + */ + private static String invalidRequestJson() { + return """ + { + "sessionId": null, + "accountAttributes": {}, + "skipDeviceTransfer": false + } + """; + } + + /** + * Request JSON that cannot be marshalled into {@link org.whispersystems.textsecuregcm.entities.RegistrationRequest} + */ + private static String unprocessableJson() { + return """ + { + "sessionId": [] + } + """; + } + + private static String encodeSessionId(final String sessionId) { + return Base64.getUrlEncoder().encodeToString(sessionId.getBytes(StandardCharsets.UTF_8)); + } + + private static String encodeRecoveryPassword(final byte[] recoveryPassword) { + return Base64.getEncoder().encodeToString(recoveryPassword); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigControllerTest.java new file mode 100644 index 000000000..48ea3db70 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigControllerTest.java @@ -0,0 +1,436 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.signal.event.NoOpAdminEventLogger; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.entities.UserRemoteConfig; +import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList; +import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.storage.RemoteConfig; +import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; + +@ExtendWith(DropwizardExtensionsSupport.class) +class RemoteConfigControllerTest { + + private static final RemoteConfigsManager remoteConfigsManager = mock(RemoteConfigsManager.class); + + private static final Set remoteConfigsUsers = Set.of("user1@example.com", "user2@example.com"); + + private static final String requiredHostedDomain = "example.com"; + private static final GoogleIdTokenVerifier.Builder googleIdVerificationTokenBuilder = mock( + GoogleIdTokenVerifier.Builder.class); + private static final GoogleIdTokenVerifier googleIdTokenVerifier = mock(GoogleIdTokenVerifier.class); + + static { + when(googleIdVerificationTokenBuilder.setAudience(any())).thenReturn(googleIdVerificationTokenBuilder); + when(googleIdVerificationTokenBuilder.build()).thenReturn(googleIdTokenVerifier); + } + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addProvider(new DeviceLimitExceededExceptionMapper()) + .addResource(new RemoteConfigController(remoteConfigsManager, new NoOpAdminEventLogger(), remoteConfigsUsers, + requiredHostedDomain, Collections.singletonList("aud.example.com"), + googleIdVerificationTokenBuilder, Map.of("maxGroupSize", "42"))) + .build(); + + + @BeforeEach + void setup() throws Exception { + when(remoteConfigsManager.getAll()).thenReturn(new LinkedList<>() {{ + add(new RemoteConfig("android.stickers", 25, Set.of(AuthHelper.DISABLED_UUID, AuthHelper.INVALID_UUID), null, + null, null)); + add(new RemoteConfig("ios.stickers", 50, Set.of(), null, null, null)); + add(new RemoteConfig("always.true", 100, Set.of(), null, null, null)); + add(new RemoteConfig("only.special", 0, Set.of(AuthHelper.VALID_UUID), null, null, null)); + add(new RemoteConfig("value.always.true", 100, Set.of(), "foo", "bar", null)); + add(new RemoteConfig("value.only.special", 0, Set.of(AuthHelper.VALID_UUID), "abc", "xyz", null)); + add(new RemoteConfig("value.always.false", 0, Set.of(), "red", "green", null)); + add(new RemoteConfig("linked.config.0", 50, Set.of(), null, null, null)); + add(new RemoteConfig("linked.config.1", 50, Set.of(), null, null, "linked.config.0")); + add(new RemoteConfig("unlinked.config", 50, Set.of(), null, null, null)); + }}); + + final Map googleIdTokens = new HashMap<>(); + + for (int i = 1; i <= 3; i++) { + final String user = "user" + i; + final GoogleIdToken googleIdToken = mock(GoogleIdToken.class); + final GoogleIdToken.Payload payload = mock(GoogleIdToken.Payload.class); + when(googleIdToken.getPayload()).thenReturn(payload); + + when(payload.getEmail()).thenReturn(user + "@" + requiredHostedDomain); + when(payload.getEmailVerified()).thenReturn(true); + when(payload.getHostedDomain()).thenReturn(requiredHostedDomain); + + googleIdTokens.put(user + ".valid", googleIdToken); + } + + when(googleIdTokenVerifier.verify(anyString())) + .thenAnswer(answer -> googleIdTokens.get(answer.getArgument(0, String.class))); + } + + @AfterEach + void teardown() { + reset(remoteConfigsManager); + } + + @Test + void testRetrieveConfig() { + UserRemoteConfigList configuration = resources.getJerseyTest() + .target("/v1/config/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(UserRemoteConfigList.class); + + verify(remoteConfigsManager, times(1)).getAll(); + + assertThat(configuration.getConfig()).hasSize(11); + assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers"); + assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers"); + assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true"); + assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true); + assertThat(configuration.getConfig().get(2).getValue()).isNull(); + assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special"); + assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(true); + assertThat(configuration.getConfig().get(2).getValue()).isNull(); + assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true"); + assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true); + assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar"); + assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special"); + assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(true); + assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("xyz"); + assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false"); + assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false); + assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red"); + assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0"); + assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1"); + assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config"); + assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize"); + } + + @Test + void testRetrieveConfigNotSpecial() { + UserRemoteConfigList configuration = resources.getJerseyTest() + .target("/v1/config/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .get(UserRemoteConfigList.class); + + verify(remoteConfigsManager, times(1)).getAll(); + + assertThat(configuration.getConfig()).hasSize(11); + assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers"); + assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers"); + assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true"); + assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true); + assertThat(configuration.getConfig().get(2).getValue()).isNull(); + assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special"); + assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(false); + assertThat(configuration.getConfig().get(2).getValue()).isNull(); + assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true"); + assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true); + assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar"); + assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special"); + assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(false); + assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("abc"); + assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false"); + assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false); + assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red"); + assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0"); + assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1"); + assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config"); + assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize"); + } + + @Test + void testHashKeyLinkedConfigs() { + boolean allUnlinkedConfigsMatched = true; + for (AuthHelper.TestAccount testAccount : AuthHelper.TEST_ACCOUNTS) { + UserRemoteConfigList configuration = resources.getJerseyTest().target("/v1/config/").request().header("Authorization", testAccount.getAuthHeader()).get(UserRemoteConfigList.class); + assertThat(configuration.getConfig()).hasSize(11); + + final UserRemoteConfig linkedConfig0 = configuration.getConfig().get(7); + assertThat(linkedConfig0.getName()).isEqualTo("linked.config.0"); + + final UserRemoteConfig linkedConfig1 = configuration.getConfig().get(8); + assertThat(linkedConfig1.getName()).isEqualTo("linked.config.1"); + + final UserRemoteConfig unlinkedConfig = configuration.getConfig().get(9); + assertThat(unlinkedConfig.getName()).isEqualTo("unlinked.config"); + + assertThat(linkedConfig0.isEnabled() == linkedConfig1.isEnabled()).isTrue(); + allUnlinkedConfigsMatched &= (linkedConfig0.isEnabled() == unlinkedConfig.isEnabled()); + } + assertThat(allUnlinkedConfigsMatched).isFalse(); + } + + @Test + void testRetrieveConfigUnauthorized() { + Response response = resources.getJerseyTest() + .target("/v1/config/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD)) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + + verifyNoMoreInteractions(remoteConfigsManager); + } + + @Test + void testSetConfig() { + Response response = resources.getJerseyTest() + .target("/v1/config") + .request() + .header("Config-Token", "user1.valid") + .put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(204); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RemoteConfig.class); + + verify(remoteConfigsManager, times(1)).set(captor.capture()); + + assertThat(captor.getValue().getName()).isEqualTo("android.stickers"); + assertThat(captor.getValue().getPercentage()).isEqualTo(88); + assertThat(captor.getValue().getUuids()).isEmpty(); + } + + @Test + void testSetConfigValued() { + Response response = resources.getJerseyTest() + .target("/v1/config") + .request() + .header("Config-Token", "user1.valid") + .put(Entity.entity(new RemoteConfig("value.sometimes", 50, Set.of(), "a", "b", null), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(204); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RemoteConfig.class); + + verify(remoteConfigsManager, times(1)).set(captor.capture()); + + assertThat(captor.getValue().getName()).isEqualTo("value.sometimes"); + assertThat(captor.getValue().getPercentage()).isEqualTo(50); + assertThat(captor.getValue().getUuids()).isEmpty(); + } + + @Test + void testSetConfigWithHashKey() { + final String configToken = "user1.valid"; + Response response1 = resources.getJerseyTest() + .target("/v1/config") + .request() + .header("Config-Token", configToken) + .put(Entity.entity(new RemoteConfig("linked.config.0", 50, Set.of(), "FALSE", "TRUE", null), + MediaType.APPLICATION_JSON_TYPE)); + assertThat(response1.getStatus()).isEqualTo(204); + + Response response2 = resources.getJerseyTest() + .target("/v1/config") + .request() + .header("Config-Token", configToken) + .put(Entity.entity(new RemoteConfig("linked.config.1", 50, Set.of(), "FALSE", "TRUE", "linked.config.0"), MediaType.APPLICATION_JSON_TYPE)); + assertThat(response2.getStatus()).isEqualTo(204); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RemoteConfig.class); + + verify(remoteConfigsManager, times(2)).set(captor.capture()); + assertThat(captor.getAllValues()).hasSize(2); + + final RemoteConfig capture1 = captor.getAllValues().get(0); + assertThat(capture1).isNotNull(); + assertThat(capture1.getName()).isEqualTo("linked.config.0"); + assertThat(capture1.getPercentage()).isEqualTo(50); + assertThat(capture1.getUuids()).isEmpty(); + assertThat(capture1.getHashKey()).isNull(); + + final RemoteConfig capture2 = captor.getAllValues().get(1); + assertThat(capture2).isNotNull(); + assertThat(capture2.getName()).isEqualTo("linked.config.1"); + assertThat(capture2.getPercentage()).isEqualTo(50); + assertThat(capture2.getUuids()).isEmpty(); + assertThat(capture2.getHashKey()).isEqualTo("linked.config.0"); + } + + @Test + void testSetConfigUnauthorized() { + Response response = resources.getJerseyTest() + .target("/v1/config") + .request() + .header("Config-Token", "user3.valid") + .put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(401); + + verifyNoMoreInteractions(remoteConfigsManager); + } + + @Test + void testSetConfigMissingUnauthorized() { + Response response = resources.getJerseyTest() + .target("/v1/config") + .request() + .put(Entity.entity(new RemoteConfig("android.stickers", 88, Set.of(), "FALSE", "TRUE", null), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(401); + + verifyNoMoreInteractions(remoteConfigsManager); + } + + @Test + void testSetConfigBadName() { + Response response = resources.getJerseyTest() + .target("/v1/config") + .request() + .header("Config-Token", "user1.valid") + .put(Entity.entity(new RemoteConfig("android-stickers", 88, Set.of(), "FALSE", "TRUE", null), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(422); + + verifyNoMoreInteractions(remoteConfigsManager); + } + + @Test + void testSetConfigEmptyName() { + Response response = resources.getJerseyTest() + .target("/v1/config") + .request() + .header("Config-Token", "user1.valid") + .put(Entity.entity(new RemoteConfig("", 88, Set.of(), "FALSE", "TRUE", null), MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(422); + + verifyNoMoreInteractions(remoteConfigsManager); + } + + @Test + void testSetGlobalConfig() { + Response response = resources.getJerseyTest() + .target("/v1/config") + .request() + .header("Config-Token", "user1.valid") + .put(Entity.entity(new RemoteConfig("global.maxGroupSize", 88, Set.of(), "FALSE", "TRUE", null), + MediaType.APPLICATION_JSON_TYPE)); + assertThat(response.getStatus()).isEqualTo(403); + verifyNoMoreInteractions(remoteConfigsManager); + } + + @Test + void testDelete() { + Response response = resources.getJerseyTest() + .target("/v1/config/android.stickers") + .request() + .header("Config-Token", "user1.valid") + .delete(); + + assertThat(response.getStatus()).isEqualTo(204); + + verify(remoteConfigsManager, times(1)).delete("android.stickers"); + verifyNoMoreInteractions(remoteConfigsManager); + } + + @Test + void testDeleteUnauthorized() { + Response response = resources.getJerseyTest() + .target("/v1/config/android.stickers") + .request() + .header("Config-Token", "baz") + .delete(); + + assertThat(response.getStatus()).isEqualTo(401); + + verifyNoMoreInteractions(remoteConfigsManager); + } + + @Test + void testDeleteGlobalConfig() { + Response response = resources.getJerseyTest() + .target("/v1/config/global.maxGroupSize") + .request() + .header("Config-Token", "user1.valid") + .delete(); + assertThat(response.getStatus()).isEqualTo(403); + verifyNoMoreInteractions(remoteConfigsManager); + } + + @Test + void testMath() throws NoSuchAlgorithmException { + List remoteConfigList = remoteConfigsManager.getAll(); + Map enabledMap = new HashMap<>(); + MessageDigest digest = MessageDigest.getInstance("SHA1"); + int iterations = 100000; + Random random = new Random(9424242L); // the seed value doesn't matter so much as it's constant to make the test not flaky + + for (int i=0;i())) { + count++; + } + + enabledMap.put(config.getName(), count); + } + } + + for (RemoteConfig config : remoteConfigList) { + double targetNumber = iterations * (config.getPercentage() / 100.0); + double variance = targetNumber * 0.01; + + assertThat(enabledMap.get(config.getName())).isBetween((int) (targetNumber - variance), + (int) (targetNumber + variance)); + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureStorageControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureStorageControllerTest.java new file mode 100644 index 000000000..1a7c276ac --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureStorageControllerTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes; + +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.MockUtils; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +@ExtendWith(DropwizardExtensionsSupport.class) +class SecureStorageControllerTest { + + private static final SecureStorageServiceConfiguration STORAGE_CFG = MockUtils.buildMock( + SecureStorageServiceConfiguration.class, + cfg -> when(cfg.userAuthenticationTokenSharedSecret()).thenReturn(randomSecretBytes(32))); + + private static final ExternalServiceCredentialsGenerator STORAGE_CREDENTIAL_GENERATOR = SecureStorageController + .credentialsGenerator(STORAGE_CFG); + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new SecureStorageController(STORAGE_CREDENTIAL_GENERATOR)) + .build(); + + + @Test + void testGetCredentials() throws Exception { + ExternalServiceCredentials credentials = resources.getJerseyTest() + .target("/v1/storage/auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(ExternalServiceCredentials.class); + + assertThat(credentials.password()).isNotEmpty(); + assertThat(credentials.username()).isNotEmpty(); + } + + @Test + void testGetCredentialsBadAuth() throws Exception { + Response response = resources.getJerseyTest() + .target("/v1/storage/auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.INVALID_UUID, AuthHelper.INVALID_PASSWORD)) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2ControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2ControllerTest.java new file mode 100644 index 000000000..a7357d29c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecovery2ControllerTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + + +import static org.mockito.Mockito.mock; +import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes; + +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.extension.ExtendWith; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.MutableClock; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +@ExtendWith(DropwizardExtensionsSupport.class) +public class SecureValueRecovery2ControllerTest extends SecureValueRecoveryControllerBaseTest { + + private static final SecureValueRecovery2Configuration CFG = new SecureValueRecovery2Configuration( + "", + randomSecretBytes(32), + randomSecretBytes(32), + null, + null, + null + ); + + private static final MutableClock CLOCK = new MutableClock(); + + private static final ExternalServiceCredentialsGenerator CREDENTIAL_GENERATOR = + SecureValueRecovery2Controller.credentialsGenerator(CFG, CLOCK); + + private static final AccountsManager ACCOUNTS_MANAGER = mock(AccountsManager.class); + private static final SecureValueRecovery2Controller CONTROLLER = + new SecureValueRecovery2Controller(CREDENTIAL_GENERATOR, ACCOUNTS_MANAGER); + + private static final ResourceExtension RESOURCES = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(CONTROLLER) + .build(); + + protected SecureValueRecovery2ControllerTest() { + super("/v2", ACCOUNTS_MANAGER, CLOCK, RESOURCES, CREDENTIAL_GENERATOR); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryControllerBaseTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryControllerBaseTest.java new file mode 100644 index 000000000..36ee25170 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryControllerBaseTest.java @@ -0,0 +1,289 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.dropwizard.testing.junit5.ResourceExtension; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.entities.AuthCheckRequest; +import org.whispersystems.textsecuregcm.entities.AuthCheckResponse; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.util.MutableClock; + +abstract class SecureValueRecoveryControllerBaseTest { + + private static final UUID USER_1 = UUID.randomUUID(); + + private static final UUID USER_2 = UUID.randomUUID(); + + private static final UUID USER_3 = UUID.randomUUID(); + + private static final String E164_VALID = "+18005550123"; + + private static final String E164_INVALID = "1(800)555-0123"; + + private final String pathPrefix; + private final ResourceExtension resourceExtension; + private final AccountsManager mockAccountsManager; + private final ExternalServiceCredentialsGenerator credentialsGenerator; + private final MutableClock clock; + + @BeforeEach + public void before() throws Exception { + Mockito.when(mockAccountsManager.getByE164(E164_VALID)).thenReturn(Optional.of(account(USER_1))); + } + + protected SecureValueRecoveryControllerBaseTest( + final String pathPrefix, + final AccountsManager mockAccountsManager, + final MutableClock mutableClock, + final ResourceExtension resourceExtension, + final ExternalServiceCredentialsGenerator credentialsGenerator) { + this.pathPrefix = pathPrefix; + this.resourceExtension = resourceExtension; + this.mockAccountsManager = mockAccountsManager; + this.credentialsGenerator = credentialsGenerator; + this.clock = mutableClock; + } + + @Test + public void testOneMatch() throws Exception { + validate(Map.of( + token(USER_1, day(1)), AuthCheckResponse.Result.MATCH, + token(USER_2, day(1)), AuthCheckResponse.Result.NO_MATCH, + token(USER_3, day(1)), AuthCheckResponse.Result.NO_MATCH + ), day(2)); + } + + @Test + public void testNoMatch() throws Exception { + validate(Map.of( + token(USER_2, day(1)), AuthCheckResponse.Result.NO_MATCH, + token(USER_3, day(1)), AuthCheckResponse.Result.NO_MATCH + ), day(2)); + } + + @Test + public void testSomeInvalid() throws Exception { + final ExternalServiceCredentials user1Cred = credentials(USER_1, day(1)); + final ExternalServiceCredentials user2Cred = credentials(USER_2, day(1)); + final ExternalServiceCredentials user3Cred = credentials(USER_3, day(1)); + + final String fakeToken = token(new ExternalServiceCredentials(user2Cred.username(), user3Cred.password())); + validate(Map.of( + token(user1Cred), AuthCheckResponse.Result.MATCH, + token(user2Cred), AuthCheckResponse.Result.NO_MATCH, + fakeToken, AuthCheckResponse.Result.INVALID + ), day(2)); + } + + @Test + public void testSomeExpired() throws Exception { + validate(Map.of( + token(USER_1, day(100)), AuthCheckResponse.Result.MATCH, + token(USER_2, day(100)), AuthCheckResponse.Result.NO_MATCH, + token(USER_3, day(10)), AuthCheckResponse.Result.INVALID, + token(USER_3, day(20)), AuthCheckResponse.Result.INVALID + ), day(110)); + } + + @Test + public void testSomeHaveNewerVersions() throws Exception { + validate(Map.of( + token(USER_1, day(10)), AuthCheckResponse.Result.INVALID, + token(USER_1, day(20)), AuthCheckResponse.Result.MATCH, + token(USER_2, day(10)), AuthCheckResponse.Result.NO_MATCH, + token(USER_3, day(20)), AuthCheckResponse.Result.NO_MATCH, + token(USER_3, day(10)), AuthCheckResponse.Result.INVALID + ), day(25)); + } + + private void validate( + final Map expected, + final long nowMillis) throws Exception { + clock.setTimeMillis(nowMillis); + final AuthCheckRequest request = new AuthCheckRequest(E164_VALID, List.copyOf(expected.keySet())); + final Response response = resourceExtension.getJerseyTest().target(pathPrefix + "/backup/auth/check") + .request() + .post(Entity.entity(request, MediaType.APPLICATION_JSON)); + try (response) { + final AuthCheckResponse res = response.readEntity(AuthCheckResponse.class); + assertEquals(200, response.getStatus()); + assertEquals(expected, res.matches()); + } + } + + @Test + public void testHttpResponseCodeSuccess() throws Exception { + final Map expected = Map.of( + token(USER_1, day(10)), AuthCheckResponse.Result.INVALID, + token(USER_1, day(20)), AuthCheckResponse.Result.MATCH, + token(USER_2, day(10)), AuthCheckResponse.Result.NO_MATCH, + token(USER_3, day(20)), AuthCheckResponse.Result.NO_MATCH, + token(USER_3, day(10)), AuthCheckResponse.Result.INVALID + ); + + clock.setTimeMillis(day(25)); + + final AuthCheckRequest in = new AuthCheckRequest(E164_VALID, List.copyOf(expected.keySet())); + + final Response response = resourceExtension.getJerseyTest() + .target(pathPrefix + "/backup/auth/check") + .request() + .post(Entity.entity(in, MediaType.APPLICATION_JSON)); + + try (response) { + final AuthCheckResponse res = response.readEntity(AuthCheckResponse.class); + assertEquals(200, response.getStatus()); + assertEquals(expected, res.matches()); + } + } + + @Test + public void testHttpResponseCodeWhenInvalidNumber() throws Exception { + final AuthCheckRequest in = new AuthCheckRequest(E164_INVALID, Collections.singletonList("1")); + final Response response = resourceExtension.getJerseyTest() + .target(pathPrefix + "/backup/auth/check") + .request() + .post(Entity.entity(in, MediaType.APPLICATION_JSON)); + + try (response) { + assertEquals(422, response.getStatus()); + } + } + + @Test + public void testHttpResponseCodeWhenTooManyTokens() throws Exception { + final AuthCheckRequest inOkay = new AuthCheckRequest(E164_VALID, List.of( + "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" + )); + final AuthCheckRequest inTooMany = new AuthCheckRequest(E164_VALID, List.of( + "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11" + )); + final AuthCheckRequest inNoTokens = new AuthCheckRequest(E164_VALID, Collections.emptyList()); + + final Response responseOkay = resourceExtension.getJerseyTest() + .target(pathPrefix + "/backup/auth/check") + .request() + .post(Entity.entity(inOkay, MediaType.APPLICATION_JSON)); + + final Response responseError1 = resourceExtension.getJerseyTest() + .target(pathPrefix + "/backup/auth/check") + .request() + .post(Entity.entity(inTooMany, MediaType.APPLICATION_JSON)); + + final Response responseError2 = resourceExtension.getJerseyTest() + .target(pathPrefix + "/backup/auth/check") + .request() + .post(Entity.entity(inNoTokens, MediaType.APPLICATION_JSON)); + + try (responseOkay; responseError1; responseError2) { + assertEquals(200, responseOkay.getStatus()); + assertEquals(422, responseError1.getStatus()); + assertEquals(422, responseError2.getStatus()); + } + } + + @Test + public void testHttpResponseCodeWhenPasswordsMissing() throws Exception { + final Response response = resourceExtension.getJerseyTest() + .target(pathPrefix + "/backup/auth/check") + .request() + .post(Entity.entity(""" + { + "number": "123" + } + """, MediaType.APPLICATION_JSON)); + + try (response) { + assertEquals(422, response.getStatus()); + } + } + + @Test + public void testHttpResponseCodeWhenNumberMissing() throws Exception { + final Response response = resourceExtension.getJerseyTest() + .target(pathPrefix + "/backup/auth/check") + .request() + .post(Entity.entity(""" + { + "passwords": ["aaa:bbb"] + } + """, MediaType.APPLICATION_JSON)); + + try (response) { + assertEquals(422, response.getStatus()); + } + } + + @Test + public void testHttpResponseCodeWhenExtraFields() throws Exception { + final Response response = resourceExtension.getJerseyTest() + .target(pathPrefix + "/backup/auth/check") + .request() + .post(Entity.entity(""" + { + "number": "+18005550123", + "passwords": ["aaa:bbb"], + "unexpected": "value" + } + """, MediaType.APPLICATION_JSON)); + + try (response) { + assertEquals(200, response.getStatus()); + } + } + + @Test + public void testHttpResponseCodeWhenNotAJson() throws Exception { + final Response response = resourceExtension.getJerseyTest() + .target(pathPrefix + "/backup/auth/check") + .request() + .post(Entity.entity("random text", MediaType.APPLICATION_JSON)); + + try (response) { + assertEquals(400, response.getStatus()); + } + } + + private String token(final UUID uuid, final long timeMillis) { + return token(credentials(uuid, timeMillis)); + } + + private static String token(final ExternalServiceCredentials credentials) { + return credentials.username() + ":" + credentials.password(); + } + + private ExternalServiceCredentials credentials(final UUID uuid, final long timeMillis) { + clock.setTimeMillis(timeMillis); + return credentialsGenerator.generateForUuid(uuid); + } + + private static long day(final int n) { + return TimeUnit.DAYS.toMillis(n); + } + + private static Account account(final UUID uuid) { + final Account a = new Account(); + a.setUuid(uuid); + return a; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/StickerControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/StickerControllerTest.java new file mode 100644 index 000000000..962703473 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/StickerControllerTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.util.Base64; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.entities.StickerPackFormUploadAttributes; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +@ExtendWith(DropwizardExtensionsSupport.class) +class StickerControllerTest { + + private static final RateLimiter rateLimiter = mock(RateLimiter.class ); + private static final RateLimiters rateLimiters = mock(RateLimiters.class); + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new StickerController(rateLimiters, "foo", "bar", "us-east-1", "mybucket")) + .build(); + + @BeforeEach + void setup() { + when(rateLimiters.getStickerPackLimiter()).thenReturn(rateLimiter); + } + + @Test + void testCreatePack() throws RateLimitExceededException { + StickerPackFormUploadAttributes attributes = resources.getJerseyTest() + .target("/v1/sticker/pack/form/10") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(StickerPackFormUploadAttributes.class); + + assertThat(attributes.getPackId()).isNotNull(); + assertThat(attributes.getPackId().length()).isEqualTo(32); + + assertThat(attributes.getManifest()).isNotNull(); + assertThat(attributes.getManifest().getKey()).isEqualTo("stickers/" + attributes.getPackId() + "/manifest.proto"); + assertThat(attributes.getManifest().getAcl()).isEqualTo("private"); + assertThat(attributes.getManifest().getPolicy()).isNotEmpty(); + assertThat(new String(Base64.getDecoder().decode(attributes.getManifest().getPolicy()))).contains("[\"content-length-range\", 1, 10240]"); + assertThat(attributes.getManifest().getSignature()).isNotEmpty(); + assertThat(attributes.getManifest().getAlgorithm()).isEqualTo("AWS4-HMAC-SHA256"); + assertThat(attributes.getManifest().getCredential()).isNotEmpty(); + assertThat(attributes.getManifest().getId()).isEqualTo(-1); + + assertThat(attributes.getStickers().size()).isEqualTo(10); + + for (int i=0;i<10;i++) { + assertThat(attributes.getStickers().get(i).getId()).isEqualTo(i); + assertThat(attributes.getStickers().get(i).getKey()).isEqualTo("stickers/" + attributes.getPackId() + "/full/" + i); + assertThat(attributes.getStickers().get(i).getAcl()).isEqualTo("private"); + assertThat(attributes.getStickers().get(i).getPolicy()).isNotEmpty(); + assertThat(new String(Base64.getDecoder().decode(attributes.getStickers().get(i).getPolicy()))).contains("[\"content-length-range\", 1, 308224]"); + assertThat(attributes.getStickers().get(i).getSignature()).isNotEmpty(); + assertThat(attributes.getStickers().get(i).getAlgorithm()).isEqualTo("AWS4-HMAC-SHA256"); + assertThat(attributes.getStickers().get(i).getCredential()).isNotEmpty(); + } + + verify(rateLimiters, times(1)).getStickerPackLimiter(); + verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID); + } + + @Test + void testCreateTooLargePack() { + Response response = resources.getJerseyTest() + .target("/v1/sticker/pack/form/202") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(); + + assertThat(response.getStatus()).isEqualTo(400); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java new file mode 100644 index 000000000..6ca617adc --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java @@ -0,0 +1,1054 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.util.AttributeValues.b; +import static org.whispersystems.textsecuregcm.util.AttributeValues.n; +import static org.whispersystems.textsecuregcm.util.AttributeValues.s; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stripe.exception.ApiException; +import com.stripe.model.PaymentIntent; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.Clock; +import java.time.Instant; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.Predicate; +import java.util.stream.Stream; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Response; +import org.apache.http.HttpHeaders; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.badges.BadgeTranslator; +import org.whispersystems.textsecuregcm.badges.LevelTranslator; +import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration; +import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; +import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetBankMandateResponse; +import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetSubscriptionConfigurationResponse; +import org.whispersystems.textsecuregcm.entities.Badge; +import org.whispersystems.textsecuregcm.entities.BadgeSvg; +import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.SubscriptionProcessorExceptionMapper; +import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager; +import org.whispersystems.textsecuregcm.storage.SubscriptionManager; +import org.whispersystems.textsecuregcm.subscriptions.BankMandateTranslator; +import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager; +import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager.PayPalOneTimePaymentApprovalDetails; +import org.whispersystems.textsecuregcm.subscriptions.ChargeFailure; +import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod; +import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer; +import org.whispersystems.textsecuregcm.subscriptions.StripeManager; +import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor; +import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorException; +import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessorManager; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@ExtendWith(DropwizardExtensionsSupport.class) +class SubscriptionControllerTest { + + private static final Clock CLOCK = mock(Clock.class); + + private static final ObjectMapper YAML_MAPPER = SystemMapper.yamlMapper(); + + private static final SubscriptionConfiguration SUBSCRIPTION_CONFIG = ConfigHelper.getSubscriptionConfig(); + private static final OneTimeDonationConfiguration ONETIME_CONFIG = ConfigHelper.getOneTimeConfig(); + private static final SubscriptionManager SUBSCRIPTION_MANAGER = mock(SubscriptionManager.class); + private static final StripeManager STRIPE_MANAGER = mock(StripeManager.class); + private static final BraintreeManager BRAINTREE_MANAGER = mock(BraintreeManager.class); + private static final PaymentIntent PAYMENT_INTENT = mock(PaymentIntent.class); + private static final ServerZkReceiptOperations ZK_OPS = mock(ServerZkReceiptOperations.class); + private static final IssuedReceiptsManager ISSUED_RECEIPTS_MANAGER = mock(IssuedReceiptsManager.class); + private static final BadgeTranslator BADGE_TRANSLATOR = mock(BadgeTranslator.class); + private static final LevelTranslator LEVEL_TRANSLATOR = mock(LevelTranslator.class); + private static final BankMandateTranslator BANK_MANDATE_TRANSLATOR = mock(BankMandateTranslator.class); + private static final SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController( + CLOCK, SUBSCRIPTION_CONFIG, ONETIME_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, BRAINTREE_MANAGER, ZK_OPS, + ISSUED_RECEIPTS_MANAGER, BADGE_TRANSLATOR, LEVEL_TRANSLATOR, BANK_MANDATE_TRANSLATOR); + private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder() + .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(CompletionExceptionMapper.class) + .addProvider(SubscriptionProcessorExceptionMapper.class) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(Set.of( + AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(SUBSCRIPTION_CONTROLLER) + .build(); + + @BeforeEach + void setUp() { + reset(CLOCK, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, BRAINTREE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER, + BADGE_TRANSLATOR, LEVEL_TRANSLATOR); + + when(STRIPE_MANAGER.getProcessor()).thenReturn(SubscriptionProcessor.STRIPE); + when(BRAINTREE_MANAGER.getProcessor()).thenReturn(SubscriptionProcessor.BRAINTREE); + + List.of(STRIPE_MANAGER, BRAINTREE_MANAGER) + .forEach(manager -> { + when(manager.supportsPaymentMethod(any())) + .thenCallRealMethod(); + }); + when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.CARD)) + .thenReturn(Set.of("usd", "jpy", "bif", "eur")); + when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.SEPA_DEBIT)) + .thenReturn(Set.of("eur")); + when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.IDEAL)) + .thenReturn(Set.of("eur")); + when(BRAINTREE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.PAYPAL)) + .thenReturn(Set.of("usd", "jpy")); + } + + @Test + void testCreateBoostPaymentIntentAmountBelowCurrencyMinimum() { + when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.CARD)) + .thenReturn(Set.of("usd", "jpy", "bif", "eur")); + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create") + .request() + .post(Entity.json(""" + { + "currency": "USD", + "amount": 249, + "level": null + } + """)); + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.hasEntity()).isTrue(); + final Map responseMap = response.readEntity(Map.class); + assertThat(responseMap.get("error")).isEqualTo("amount_below_currency_minimum"); + assertThat(responseMap.get("minimum")).isEqualTo("2.50"); + } + + @Test + void testCreateBoostPaymentIntentAmountAboveSepaLimit() { + when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.SEPA_DEBIT)) + .thenReturn(Set.of("eur")); + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create") + .request() + .post(Entity.json(""" + { + "currency": "EUR", + "amount": 1000001, + "level": null, + "paymentMethod": "SEPA_DEBIT" + } + """)); + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.hasEntity()).isTrue(); + + final Map responseMap = response.readEntity(Map.class); + assertThat(responseMap.get("error")).isEqualTo("amount_above_sepa_limit"); + assertThat(responseMap.get("maximum")).isEqualTo("10000"); + } + + @Test + void testCreateBoostPaymentIntentUnsupportedCurrency() { + when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.SEPA_DEBIT)) + .thenReturn(Set.of("eur")); + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create") + .request() + .post(Entity.json(""" + { + "currency": "USD", + "amount": 3000, + "level": null, + "paymentMethod": "SEPA_DEBIT" + } + """)); + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.hasEntity()).isTrue(); + + final Map responseMap = response.readEntity(Map.class); + assertThat(responseMap.get("error")).isEqualTo("unsupported_currency"); + } + + @Test + void testCreateBoostPaymentIntentLevelAmountMismatch() { + when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.CARD)) + .thenReturn(Set.of("usd", "jpy", "bif", "eur")); + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create") + .request() + .post(Entity.json(""" + { + "currency": "USD", + "amount": 25, + "level": 100 + } + """ + )); + assertThat(response.getStatus()).isEqualTo(409); + } + + @Test + void testCreateBoostPaymentIntent() { + when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.CARD)) + .thenReturn(Set.of("usd", "jpy", "bif", "eur")); + when(STRIPE_MANAGER.createPaymentIntent(anyString(), anyLong(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(PAYMENT_INTENT)); + + String clientSecret = "some_client_secret"; + when(PAYMENT_INTENT.getClientSecret()).thenReturn(clientSecret); + + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create") + .request() + .post(Entity.json("{\"currency\": \"USD\", \"amount\": 300, \"level\": null}")); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + void testCreateBoostPayPal() { + final PayPalOneTimePaymentApprovalDetails payPalOneTimePaymentApprovalDetails = mock(PayPalOneTimePaymentApprovalDetails.class); + when(BRAINTREE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.PAYPAL)) + .thenReturn(Set.of("usd", "jpy", "bif", "eur")); + when(BRAINTREE_MANAGER.createOneTimePayment(anyString(), anyLong(), anyString(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(payPalOneTimePaymentApprovalDetails)); + when(payPalOneTimePaymentApprovalDetails.approvalUrl()).thenReturn("approvalUrl"); + when(payPalOneTimePaymentApprovalDetails.paymentId()).thenReturn("someId"); + + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/paypal/create") + .request() + .post(Entity.json(""" + { + "currency": "USD", + "amount": 300, + "cancelUrl": "cancelUrl", + "returnUrl": "returnUrl" + } + """ + )); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + void createBoostReceiptInvalid() { + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/receipt_credentials") + .request() + // invalid, request body should have receiptCredentialRequest + .post(Entity.json("{\"paymentIntentId\": \"foo\"}")); + assertThat(response.getStatus()).isEqualTo(422); + } + + @Test + void confirmPaypalBoostProcessorError() { + + when(BRAINTREE_MANAGER.captureOneTimePayment(anyString(), anyString(), anyString(), anyString(), anyLong(), + anyLong())) + .thenReturn(CompletableFuture.failedFuture(new SubscriptionProcessorException(SubscriptionProcessor.BRAINTREE, + new ChargeFailure("2046", "Declined", null, null, null)))); + + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/paypal/confirm") + .request() + .post(Entity.json(Map.of("payerId", "payer123", + "paymentId", "PAYID-456", + "paymentToken", "EC-789", + "currency", "usd", + "amount", 123))); + + assertThat(response.getStatus()).isEqualTo(SubscriptionProcessorExceptionMapper.EXTERNAL_SERVICE_ERROR_STATUS_CODE); + + final Map responseMap = response.readEntity(Map.class); + assertThat(responseMap.get("processor")).isEqualTo("BRAINTREE"); + assertThat(responseMap.get("chargeFailure")).asInstanceOf( + InstanceOfAssertFactories.map(String.class, Object.class)) + .extracting("code") + .isEqualTo("2046"); + } + + @Test + void createBoostReceiptNoRequest() { + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/receipt_credentials") + .request() + .post(Entity.json("")); + assertThat(response.getStatus()).isEqualTo(422); + } + + @Nested + class SetSubscriptionLevel { + + private final long levelId = 5L; + private final String currency = "jpy"; + + private String subscriberId; + + @BeforeEach + void setUp() { + when(CLOCK.instant()).thenReturn(Instant.now()); + + final byte[] subscriberUserAndKey = new byte[32]; + Arrays.fill(subscriberUserAndKey, (byte) 1); + subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey); + + final ProcessorCustomer processorCustomer = new ProcessorCustomer("testCustomerId", SubscriptionProcessor.STRIPE); + + final Map dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]), + SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_PROCESSOR_ID_CUSTOMER_ID, b(processorCustomer.toDynamoBytes()) + ); + final SubscriptionManager.Record record = SubscriptionManager.Record.from( + Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem); + when(SUBSCRIPTION_MANAGER.get(eq(Arrays.copyOfRange(subscriberUserAndKey, 0, 16)), any())) + .thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record))); + + when(SUBSCRIPTION_MANAGER.subscriptionCreated(any(), any(), any(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(null)); + } + + @Test + void createSubscriptionSuccess() { + when(STRIPE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(mock(SubscriptionProcessorManager.SubscriptionId.class))); + + final String level = String.valueOf(levelId); + final String idempotencyKey = UUID.randomUUID().toString(); + final Response response = RESOURCE_EXTENSION.target( + String.format("/v1/subscription/%s/level/%s/%s/%s", subscriberId, level, currency, idempotencyKey)) + .request() + .put(Entity.json("")); + + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + void createSubscriptionProcessorDeclined() { + when(STRIPE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong())) + .thenReturn(CompletableFuture.failedFuture(new SubscriptionProcessorException(SubscriptionProcessor.STRIPE, + new ChargeFailure("card_declined", "Insufficient funds", null, null, null)))); + + final String level = String.valueOf(levelId); + final String idempotencyKey = UUID.randomUUID().toString(); + final Response response = RESOURCE_EXTENSION.target( + String.format("/v1/subscription/%s/level/%s/%s/%s", subscriberId, level, currency, idempotencyKey)) + .request() + .put(Entity.json("")); + + assertThat(response.getStatus()).isEqualTo( + SubscriptionProcessorExceptionMapper.EXTERNAL_SERVICE_ERROR_STATUS_CODE); + + final Map responseMap = response.readEntity(Map.class); + assertThat(responseMap.get("processor")).isEqualTo("STRIPE"); + assertThat(responseMap.get("chargeFailure")).asInstanceOf( + InstanceOfAssertFactories.map(String.class, Object.class)) + .extracting("code") + .isEqualTo("card_declined"); + } + + @Test + void missingCustomerId() { + final byte[] subscriberUserAndKey = new byte[32]; + Arrays.fill(subscriberUserAndKey, (byte) 1); + subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey); + + final Map dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]), + SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond()) + // missing processor:customer field + ); + final SubscriptionManager.Record record = SubscriptionManager.Record.from( + Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem); + when(SUBSCRIPTION_MANAGER.get(eq(Arrays.copyOfRange(subscriberUserAndKey, 0, 16)), any())) + .thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record))); + + final String level = String.valueOf(levelId); + final String idempotencyKey = UUID.randomUUID().toString(); + final Response response = RESOURCE_EXTENSION.target( + String.format("/v1/subscription/%s/level/%s/%s/%s", subscriberId, level, currency, idempotencyKey)) + .request() + .put(Entity.json("")); + + assertThat(response.getStatus()).isEqualTo(409); + } + + @Test + void stripePaymentIntentRequiresAction() { + final ApiException stripeException = new ApiException("Payment intent requires action", + UUID.randomUUID().toString(), "subscription_payment_intent_requires_action", 400, new Exception()); + when(STRIPE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong())) + .thenReturn(CompletableFuture.failedFuture(new CompletionException(stripeException))); + + final String level = String.valueOf(levelId); + final String idempotencyKey = UUID.randomUUID().toString(); + final Response response = RESOURCE_EXTENSION.target( + String.format("/v1/subscription/%s/level/%s/%s/%s", subscriberId, level, currency, idempotencyKey)) + .request() + .put(Entity.json("")); + + assertThat(response.getStatus()).isEqualTo(400); + + assertThat(response.readEntity(SubscriptionController.SetSubscriptionLevelErrorResponse.class)) + .satisfies(errorResponse -> { + assertThat(errorResponse.errors()) + .anySatisfy(error -> { + assertThat(error.type()).isEqualTo( + SubscriptionController.SetSubscriptionLevelErrorResponse.Error.Type.PAYMENT_REQUIRES_ACTION); + }); + }); + } + } + + @Test + void createSubscriber() { + when(CLOCK.instant()).thenReturn(Instant.now()); + + // basic create + final byte[] subscriberUserAndKey = new byte[32]; + Arrays.fill(subscriberUserAndKey, (byte) 1); + final String subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey); + + when(SUBSCRIPTION_MANAGER.get(any(), any())).thenReturn(CompletableFuture.completedFuture( + SubscriptionManager.GetResult.NOT_STORED)); + + final Map dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]), + SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond()) + ); + final SubscriptionManager.Record record = SubscriptionManager.Record.from( + Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem); + when(SUBSCRIPTION_MANAGER.create(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(record)); + + final Response createResponse = RESOURCE_EXTENSION.target(String.format("/v1/subscription/%s", subscriberId)) + .request() + .put(Entity.json("")); + assertThat(createResponse.getStatus()).isEqualTo(200); + + // creating should be idempotent + when(SUBSCRIPTION_MANAGER.get(any(), any())).thenReturn(CompletableFuture.completedFuture( + SubscriptionManager.GetResult.found(record))); + when(SUBSCRIPTION_MANAGER.accessedAt(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + final Response idempotentCreateResponse = RESOURCE_EXTENSION.target( + String.format("/v1/subscription/%s", subscriberId)) + .request() + .put(Entity.json("")); + assertThat(idempotentCreateResponse.getStatus()).isEqualTo(200); + + // when the manager returns `null`, it means there was a password mismatch from the storage layer `create`. + // this could happen if there is a race between two concurrent `create` requests for the same user ID + when(SUBSCRIPTION_MANAGER.get(any(), any())).thenReturn(CompletableFuture.completedFuture( + SubscriptionManager.GetResult.NOT_STORED)); + when(SUBSCRIPTION_MANAGER.create(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + final Response managerCreateNullResponse = RESOURCE_EXTENSION.target( + String.format("/v1/subscription/%s", subscriberId)) + .request() + .put(Entity.json("")); + assertThat(managerCreateNullResponse.getStatus()).isEqualTo(403); + + final byte[] subscriberUserAndMismatchedKey = new byte[32]; + Arrays.fill(subscriberUserAndMismatchedKey, 0, 16, (byte) 1); + Arrays.fill(subscriberUserAndMismatchedKey, 16, 32, (byte) 2); + final String mismatchedSubscriberId = Base64.getEncoder().encodeToString(subscriberUserAndMismatchedKey); + + // a password mismatch for an existing record + when(SUBSCRIPTION_MANAGER.get(any(), any())).thenReturn(CompletableFuture.completedFuture( + SubscriptionManager.GetResult.PASSWORD_MISMATCH)); + + final Response passwordMismatchResponse = RESOURCE_EXTENSION.target( + String.format("/v1/subscription/%s", mismatchedSubscriberId)) + .request() + .put(Entity.json("")); + + assertThat(passwordMismatchResponse.getStatus()).isEqualTo(403); + + // invalid request data is a 404 + final byte[] malformedUserAndKey = new byte[16]; + Arrays.fill(malformedUserAndKey, (byte) 1); + final String malformedUserId = Base64.getEncoder().encodeToString(malformedUserAndKey); + + final Response malformedUserAndKeyResponse = RESOURCE_EXTENSION.target( + String.format("/v1/subscription/%s", malformedUserId)) + .request() + .put(Entity.json("")); + + assertThat(malformedUserAndKeyResponse.getStatus()).isEqualTo(404); + } + + @Test + void createPaymentMethod() { + final byte[] subscriberUserAndKey = new byte[32]; + Arrays.fill(subscriberUserAndKey, (byte) 1); + final String subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey); + + when(CLOCK.instant()).thenReturn(Instant.now()); + when(SUBSCRIPTION_MANAGER.get(any(), any())).thenReturn(CompletableFuture.completedFuture( + SubscriptionManager.GetResult.NOT_STORED)); + + final Map dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]), + SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond()) + ); + final SubscriptionManager.Record record = SubscriptionManager.Record.from( + Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem); + when(SUBSCRIPTION_MANAGER.create(any(), any(), any(Instant.class))) + .thenReturn(CompletableFuture.completedFuture(record)); + + final Response createSubscriberResponse = RESOURCE_EXTENSION + .target(String.format("/v1/subscription/%s", subscriberId)) + .request() + .put(Entity.json("")); + + assertThat(createSubscriberResponse.getStatus()).isEqualTo(200); + + when(SUBSCRIPTION_MANAGER.get(any(), any())) + .thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record))); + + final String customerId = "some-customer-id"; + final ProcessorCustomer customer = new ProcessorCustomer( + customerId, SubscriptionProcessor.STRIPE); + when(STRIPE_MANAGER.createCustomer(any())) + .thenReturn(CompletableFuture.completedFuture(customer)); + + final Map dynamoItemWithProcessorCustomer = new HashMap<>(dynamoItem); + dynamoItemWithProcessorCustomer.put(SubscriptionManager.KEY_PROCESSOR_ID_CUSTOMER_ID, + b(new ProcessorCustomer(customerId, SubscriptionProcessor.STRIPE).toDynamoBytes())); + final SubscriptionManager.Record recordWithCustomerId = SubscriptionManager.Record.from(record.user, + dynamoItemWithProcessorCustomer); + + when(SUBSCRIPTION_MANAGER.setProcessorAndCustomerId(any(SubscriptionManager.Record.class), any(), + any(Instant.class))) + .thenReturn(CompletableFuture.completedFuture(recordWithCustomerId)); + + final String clientSecret = "some-client-secret"; + when(STRIPE_MANAGER.createPaymentMethodSetupToken(customerId)) + .thenReturn(CompletableFuture.completedFuture(clientSecret)); + + final SubscriptionController.CreatePaymentMethodResponse createPaymentMethodResponse = RESOURCE_EXTENSION + .target(String.format("/v1/subscription/%s/create_payment_method", subscriberId)) + .request() + .post(Entity.json("")) + .readEntity(SubscriptionController.CreatePaymentMethodResponse.class); + + assertThat(createPaymentMethodResponse.processor()).isEqualTo(SubscriptionProcessor.STRIPE); + assertThat(createPaymentMethodResponse.clientSecret()).isEqualTo(clientSecret); + + } + + @Test + void setSubscriptionLevelMissingProcessorCustomer() { + // set up record + final byte[] subscriberUserAndKey = new byte[32]; + Arrays.fill(subscriberUserAndKey, (byte) 1); + final String subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey); + + final Map dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]), + SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond()) + ); + final SubscriptionManager.Record record = SubscriptionManager.Record.from( + Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem); + when(SUBSCRIPTION_MANAGER.create(any(), any(), any(Instant.class))) + .thenReturn(CompletableFuture.completedFuture(record)); + + // set up mocks + when(CLOCK.instant()).thenReturn(Instant.now()); + when(SUBSCRIPTION_MANAGER.get(any(), any())) + .thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record))); + + final Response response = RESOURCE_EXTENSION + .target(String.format("/v1/subscription/%s/level/%d/%s/%s", subscriberId, 5, "usd", "abcd")) + .request() + .put(Entity.json("")); + + assertThat(response.getStatus()).isEqualTo(409); + } + + @Test + void setSubscriptionLevel() { + // set up record + final byte[] subscriberUserAndKey = new byte[32]; + Arrays.fill(subscriberUserAndKey, (byte) 1); + final String subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey); + + final String customerId = "customer"; + final Map dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]), + SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_PROCESSOR_ID_CUSTOMER_ID, + b(new ProcessorCustomer(customerId, SubscriptionProcessor.BRAINTREE).toDynamoBytes()) + ); + final SubscriptionManager.Record record = SubscriptionManager.Record.from( + Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem); + when(SUBSCRIPTION_MANAGER.create(any(), any(), any(Instant.class))) + .thenReturn(CompletableFuture.completedFuture(record)); + + // set up mocks + when(CLOCK.instant()).thenReturn(Instant.now()); + when(SUBSCRIPTION_MANAGER.get(any(), any())) + .thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record))); + + when(BRAINTREE_MANAGER.createSubscription(any(), any(), anyLong(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(new SubscriptionProcessorManager.SubscriptionId( + "subscription"))); + when(SUBSCRIPTION_MANAGER.subscriptionCreated(any(), any(), any(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final long level = 5; + final Response response = RESOURCE_EXTENSION + .target(String.format("/v1/subscription/%s/level/%d/%s/%s", subscriberId, level, "usd", "abcd")) + .request() + .put(Entity.json("")); + + verify(BRAINTREE_MANAGER).createSubscription(eq(customerId), eq("M1"), eq(level), eq(0L)); + verifyNoMoreInteractions(BRAINTREE_MANAGER); + + assertThat(response.getStatus()).isEqualTo(200); + + assertThat(response.readEntity(SubscriptionController.SetSubscriptionLevelSuccessResponse.class)) + .extracting(SubscriptionController.SetSubscriptionLevelSuccessResponse::level) + .isEqualTo(level); + } + + @ParameterizedTest + @MethodSource + void setSubscriptionLevelExistingSubscription(final String existingCurrency, final long existingLevel, + final String requestCurrency, final long requestLevel, final boolean expectUpdate) { + + // set up record + final byte[] subscriberUserAndKey = new byte[32]; + Arrays.fill(subscriberUserAndKey, (byte) 1); + final String subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey); + + final String customerId = "customer"; + final String existingSubscriptionId = "existingSubscription"; + final Map dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]), + SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond()), + SubscriptionManager.KEY_PROCESSOR_ID_CUSTOMER_ID, + b(new ProcessorCustomer(customerId, SubscriptionProcessor.BRAINTREE).toDynamoBytes()), + SubscriptionManager.KEY_SUBSCRIPTION_ID, s(existingSubscriptionId) + ); + final SubscriptionManager.Record record = SubscriptionManager.Record.from( + Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem); + when(SUBSCRIPTION_MANAGER.create(any(), any(), any(Instant.class))) + .thenReturn(CompletableFuture.completedFuture(record)); + + // set up mocks + when(CLOCK.instant()).thenReturn(Instant.now()); + when(SUBSCRIPTION_MANAGER.get(any(), any())) + .thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record))); + + final Object subscriptionObj = new Object(); + when(BRAINTREE_MANAGER.getSubscription(any())) + .thenReturn(CompletableFuture.completedFuture(subscriptionObj)); + when(BRAINTREE_MANAGER.getLevelAndCurrencyForSubscription(subscriptionObj)) + .thenReturn(CompletableFuture.completedFuture( + new SubscriptionProcessorManager.LevelAndCurrency(existingLevel, existingCurrency))); + final String updatedSubscriptionId = "updatedSubscriptionId"; + + if (expectUpdate) { + when(BRAINTREE_MANAGER.updateSubscription(any(), any(), anyLong(), anyString())) + .thenReturn(CompletableFuture.completedFuture(new SubscriptionProcessorManager.SubscriptionId( + updatedSubscriptionId))); + when(SUBSCRIPTION_MANAGER.subscriptionLevelChanged(any(), any(), anyLong(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + } + + final String idempotencyKey = "abcd"; + final Response response = RESOURCE_EXTENSION + .target(String.format("/v1/subscription/%s/level/%d/%s/%s", subscriberId, requestLevel, requestCurrency, + idempotencyKey)) + .request() + .put(Entity.json("")); + + verify(BRAINTREE_MANAGER).getSubscription(any()); + verify(BRAINTREE_MANAGER).getLevelAndCurrencyForSubscription(any()); + + if (expectUpdate) { + verify(BRAINTREE_MANAGER).updateSubscription(any(), any(), eq(requestLevel), eq(idempotencyKey)); + verify(SUBSCRIPTION_MANAGER).subscriptionLevelChanged(any(), any(), eq(requestLevel), eq(updatedSubscriptionId)); + } + + verifyNoMoreInteractions(BRAINTREE_MANAGER); + + assertThat(response.getStatus()).isEqualTo(200); + + assertThat(response.readEntity(SubscriptionController.SetSubscriptionLevelSuccessResponse.class)) + .extracting(SubscriptionController.SetSubscriptionLevelSuccessResponse::level) + .isEqualTo(requestLevel); + } + + static Stream setSubscriptionLevelExistingSubscription() { + return Stream.of( + Arguments.of("usd", 5, "usd", 5, false), + Arguments.of("usd", 5, "jpy", 5, true), + Arguments.of("usd", 5, "usd", 15, true), + Arguments.of("usd", 5, "jpy", 15, true) + ); + } + + @Test + void testGetBankMandate() { + when(BANK_MANDATE_TRANSLATOR.translate(any(), any())).thenReturn("bankMandate"); + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/bank_mandate/sepa_debit") + .request() + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.readEntity(GetBankMandateResponse.class).mandate()).isEqualTo("bankMandate"); + } + + @Test + void testGetBankMandateInvalidBankTransferType() { + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/ach") + .request() + .get(); + assertThat(response.getStatus()).isEqualTo(404); + } + + @ParameterizedTest + @MethodSource + void getSubscriptionConfiguration(final String userAgent, final boolean expectNonCardPaymentMethods) { + when(BADGE_TRANSLATOR.translate(any(), eq("B1"))).thenReturn(new Badge("B1", "cat1", "name1", "desc1", + List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", + List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")))); + when(BADGE_TRANSLATOR.translate(any(), eq("B2"))).thenReturn(new Badge("B2", "cat2", "name2", "desc2", + List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", + List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")))); + when(BADGE_TRANSLATOR.translate(any(), eq("B3"))).thenReturn(new Badge("B3", "cat3", "name3", "desc3", + List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", + List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")))); + when(BADGE_TRANSLATOR.translate(any(), eq("BOOST"))).thenReturn(new Badge("BOOST", "boost1", "boost1", "boost1", + List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", + List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")))); + when(BADGE_TRANSLATOR.translate(any(), eq("GIFT"))).thenReturn(new Badge("GIFT", "gift1", "gift1", "gift1", + List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", + List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")))); + when(LEVEL_TRANSLATOR.translate(any(), eq("B1"))).thenReturn("Z1"); + when(LEVEL_TRANSLATOR.translate(any(), eq("B2"))).thenReturn("Z2"); + when(LEVEL_TRANSLATOR.translate(any(), eq("B3"))).thenReturn("Z3"); + + GetSubscriptionConfigurationResponse response = RESOURCE_EXTENSION.target("/v1/subscription/configuration") + .request() + .header(HttpHeaders.USER_AGENT, userAgent) + .get(GetSubscriptionConfigurationResponse.class); + + assertThat(response.currencies()).containsKeys("usd", "jpy", "bif", "eur").satisfies(currencyMap -> { + assertThat(currencyMap).extractingByKey("usd").satisfies(currency -> { + assertThat(currency.minimum()).isEqualByComparingTo( + BigDecimal.valueOf(2.5).setScale(2, RoundingMode.HALF_EVEN)); + assertThat(currency.oneTime()).isEqualTo( + Map.of("1", + List.of(BigDecimal.valueOf(5.5).setScale(2, RoundingMode.HALF_EVEN), BigDecimal.valueOf(6), + BigDecimal.valueOf(7), BigDecimal.valueOf(8), + BigDecimal.valueOf(9), BigDecimal.valueOf(10)), "100", + List.of(BigDecimal.valueOf(20)))); + assertThat(currency.subscription()).isEqualTo( + Map.of("5", BigDecimal.valueOf(5), "15", BigDecimal.valueOf(15), "35", BigDecimal.valueOf(35))); + assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD", "PAYPAL")); + }); + + assertThat(currencyMap).extractingByKey("jpy").satisfies(currency -> { + assertThat(currency.minimum()).isEqualByComparingTo( + BigDecimal.valueOf(250)); + assertThat(currency.oneTime()).isEqualTo( + Map.of("1", + List.of(BigDecimal.valueOf(550), BigDecimal.valueOf(600), + BigDecimal.valueOf(700), BigDecimal.valueOf(800), + BigDecimal.valueOf(900), BigDecimal.valueOf(1000)), "100", + List.of(BigDecimal.valueOf(2000)))); + assertThat(currency.subscription()).isEqualTo( + Map.of("5", BigDecimal.valueOf(500), "15", BigDecimal.valueOf(1500), "35", BigDecimal.valueOf(3500))); + assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD", "PAYPAL")); + }); + + assertThat(currencyMap).extractingByKey("bif").satisfies(currency -> { + assertThat(currency.minimum()).isEqualByComparingTo( + BigDecimal.valueOf(2500)); + assertThat(currency.oneTime()).isEqualTo( + Map.of("1", + List.of(BigDecimal.valueOf(5500), BigDecimal.valueOf(6000), + BigDecimal.valueOf(7000), BigDecimal.valueOf(8000), + BigDecimal.valueOf(9000), BigDecimal.valueOf(10000)), "100", + List.of(BigDecimal.valueOf(20000)))); + assertThat(currency.subscription()).isEqualTo( + Map.of("5", BigDecimal.valueOf(5000), "15", BigDecimal.valueOf(15000), "35", BigDecimal.valueOf(35000))); + assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD")); + }); + + assertThat(currencyMap).extractingByKey("eur").satisfies(currency -> { + assertThat(currency.minimum()).isEqualByComparingTo( + BigDecimal.valueOf(3)); + assertThat(currency.oneTime()).isEqualTo( + Map.of("1", + List.of(BigDecimal.valueOf(5), BigDecimal.valueOf(10), + BigDecimal.valueOf(20), BigDecimal.valueOf(30), BigDecimal.valueOf(50), BigDecimal.valueOf(100)), "100", + List.of(BigDecimal.valueOf(5)))); + assertThat(currency.subscription()).isEqualTo( + Map.of("5", BigDecimal.valueOf(5), "15", BigDecimal.valueOf(15),"35", BigDecimal.valueOf(35))); + final List expectedPaymentMethods = expectNonCardPaymentMethods ? List.of("CARD", "SEPA_DEBIT", "IDEAL") : List.of("CARD"); + assertThat(currency.supportedPaymentMethods()).isEqualTo(expectedPaymentMethods); + }); + }); + + assertThat(response.levels()).containsKeys("1", "5", "15", "35", "100").satisfies(levelsMap -> { + assertThat(levelsMap).extractingByKey("1").satisfies(level -> { + assertThat(level.name()).isEqualTo("boost1"); // level name is the same as badge name + assertThat(level).extracting(SubscriptionController.LevelConfiguration::badge).satisfies(badge -> { + assertThat(badge.getId()).isEqualTo("BOOST"); + assertThat(badge.getName()).isEqualTo("boost1"); + }); + }); + + assertThat(levelsMap).extractingByKey("100").satisfies(level -> { + assertThat(level.name()).isEqualTo("gift1"); // level name is the same as badge name + assertThat(level).extracting(SubscriptionController.LevelConfiguration::badge).satisfies(badge -> { + assertThat(badge.getId()).isEqualTo("GIFT"); + assertThat(badge.getName()).isEqualTo("gift1"); + }); + }); + + assertThat(levelsMap).extractingByKey("5").satisfies(level -> { + assertThat(level.name()).isEqualTo("Z1"); + assertThat(level).extracting(SubscriptionController.LevelConfiguration::badge).satisfies(badge -> { + assertThat(badge.getId()).isEqualTo("B1"); + assertThat(badge.getName()).isEqualTo("name1"); + }); + }); + + assertThat(levelsMap).extractingByKey("15").satisfies(level -> { + assertThat(level.name()).isEqualTo("Z2"); + assertThat(level).extracting(SubscriptionController.LevelConfiguration::badge).satisfies(badge -> { + assertThat(badge.getId()).isEqualTo("B2"); + assertThat(badge.getName()).isEqualTo("name2"); + }); + }); + + assertThat(levelsMap).extractingByKey("35").satisfies(level -> { + assertThat(level.name()).isEqualTo("Z3"); + assertThat(level).extracting(SubscriptionController.LevelConfiguration::badge).satisfies(badge -> { + assertThat(badge.getId()).isEqualTo("B3"); + assertThat(badge.getName()).isEqualTo("name3"); + }); + }); + }); + + // check the badge vs purchasable badge fields + // subscription levels are Badge, while one-time levels are PurchasableBadge, which adds `duration` + Map genericResponse = RESOURCE_EXTENSION.target("/v1/subscription/configuration") + .request() + .header(HttpHeaders.USER_AGENT, userAgent) + .get(Map.class); + + assertThat(genericResponse.get("levels")).satisfies(levels -> { + final Set oneTimeLevels = Set.of("1", "100"); + oneTimeLevels.forEach(oneTimeLevel -> { + assertThat((Map>>) levels).extractingByKey(oneTimeLevel) + .satisfies(level -> { + assertThat(level.get("badge")).containsKeys("duration"); + }); + }); + + ((Map) levels).keySet().stream() + .filter(Predicate.not(oneTimeLevels::contains)) + .forEach(subscriptionLevel -> { + assertThat((Map>>) levels).extractingByKey(subscriptionLevel) + .satisfies(level -> { + assertThat(level.get("badge")).doesNotContainKeys("duration"); + }); + }); + }); + } + + private static Stream getSubscriptionConfiguration() { + return Stream.of( + Arguments.of("Signal-iOS/6.44.0.8", false), + Arguments.of("Signal-iOS/6.45.0.0", true), + Arguments.of("Signal-iOS/6.45.0.2", true), + Arguments.of("Signal-iOS/6.46.0.0", true), + Arguments.of("Signal-Android/1.2.3", true), + Arguments.of(null, true), + Arguments.of("", true), + Arguments.of("definitely not a parseable user agent", true) + ); + } + + /** + * Encapsulates {@code static} configuration, to keep the class header simpler and avoid illegal forward references + */ + private record ConfigHelper() { + + private static SubscriptionConfiguration getSubscriptionConfig() { + return readValue(SUBSCRIPTION_CONFIG_YAML, SubscriptionConfiguration.class); + } + + private static OneTimeDonationConfiguration getOneTimeConfig() { + return readValue(ONETIME_CONFIG_YAML, OneTimeDonationConfiguration.class); + } + + private static T readValue(String yaml, Class type) { + try { + return YAML_MAPPER.readValue(yaml, type); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static final String SUBSCRIPTION_CONFIG_YAML = """ + badgeGracePeriod: P15D + levels: + 5: + badge: B1 + prices: + usd: + amount: '5' + processorIds: + STRIPE: R1 + BRAINTREE: M1 + jpy: + amount: '500' + processorIds: + STRIPE: Q1 + BRAINTREE: N1 + bif: + amount: '5000' + processorIds: + STRIPE: S1 + BRAINTREE: O1 + eur: + amount: '5' + processorIds: + STRIPE: A1 + BRAINTREE: B1 + 15: + badge: B2 + prices: + usd: + amount: '15' + processorIds: + STRIPE: R2 + BRAINTREE: M2 + jpy: + amount: '1500' + processorIds: + STRIPE: Q2 + BRAINTREE: N2 + bif: + amount: '15000' + processorIds: + STRIPE: S2 + BRAINTREE: O2 + eur: + amount: '15' + processorIds: + STRIPE: A2 + BRAINTREE: B2 + 35: + badge: B3 + prices: + usd: + amount: '35' + processorIds: + STRIPE: R3 + BRAINTREE: M3 + jpy: + amount: '3500' + processorIds: + STRIPE: Q3 + BRAINTREE: N3 + bif: + amount: '35000' + processorIds: + STRIPE: S3 + BRAINTREE: O3 + eur: + amount: '35' + processorIds: + STRIPE: A3 + BRAINTREE: B3 + """; + + private static final String ONETIME_CONFIG_YAML = """ + boost: + level: 1 + expiration: P45D + badge: BOOST + gift: + level: 100 + expiration: P60D + badge: GIFT + currencies: + usd: + minimum: '2.50' # fractional to test BigDecimal conversion + gift: '20' + boosts: + - '5.50' + - '6' + - '7' + - '8' + - '9' + - '10' + eur: + minimum: '3' + gift: '5' + boosts: + - '5' + - '10' + - '20' + - '30' + - '50' + - '100' + jpy: + minimum: '250' + gift: '2000' + boosts: + - '550' + - '600' + - '700' + - '800' + - '900' + - '1000' + bif: + minimum: '2500' + gift: '20000' + boosts: + - '5500' + - '6000' + - '7000' + - '8000' + - '9000' + - '10000' + sepaMaxTransactionSizeEuros: '10000' + """; + + } + + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/VerificationControllerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/VerificationControllerTest.java new file mode 100644 index 000000000..c5384d8e3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/controllers/VerificationControllerTest.java @@ -0,0 +1,1360 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.controllers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.net.HttpHeaders; +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.stream.Stream; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.Response; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpStatus; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.whispersystems.textsecuregcm.captcha.AssessmentResult; +import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager; +import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; +import org.whispersystems.textsecuregcm.entities.VerificationSessionResponse; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper; +import org.whispersystems.textsecuregcm.push.PushNotificationManager; +import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; +import org.whispersystems.textsecuregcm.registration.RegistrationServiceException; +import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException; +import org.whispersystems.textsecuregcm.registration.TransportNotAllowedException; +import org.whispersystems.textsecuregcm.registration.VerificationSession; +import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; +import org.whispersystems.textsecuregcm.storage.VerificationSessionManager; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +@ExtendWith(DropwizardExtensionsSupport.class) +class VerificationControllerTest { + + private static final long SESSION_EXPIRATION_SECONDS = Duration.ofMinutes(10).toSeconds(); + + private static final byte[] SESSION_ID = "session".getBytes(StandardCharsets.UTF_8); + private static final String NUMBER = "+18005551212"; + + private final RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class); + private final VerificationSessionManager verificationSessionManager = mock(VerificationSessionManager.class); + private final PushNotificationManager pushNotificationManager = mock(PushNotificationManager.class); + private final RegistrationCaptchaManager registrationCaptchaManager = mock(RegistrationCaptchaManager.class); + private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock( + RegistrationRecoveryPasswordsManager.class); + private final RateLimiters rateLimiters = mock(RateLimiters.class); + private final AccountsManager accountsManager = mock(AccountsManager.class); + private final Clock clock = Clock.systemUTC(); + + private final RateLimiter captchaLimiter = mock(RateLimiter.class); + private final RateLimiter pushChallengeLimiter = mock(RateLimiter.class); + + private final ResourceExtension resources = ResourceExtension.builder() + .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) + .addProvider(new RateLimitExceededExceptionMapper()) + .addProvider(new ImpossiblePhoneNumberExceptionMapper()) + .addProvider(new NonNormalizedPhoneNumberExceptionMapper()) + .addProvider(new RegistrationServiceSenderExceptionMapper()) + .addProvider(ScoreThresholdProvider.ScoreThresholdFeature.class) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource( + new VerificationController(registrationServiceClient, verificationSessionManager, pushNotificationManager, + registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters, accountsManager, clock)) + .build(); + + @BeforeEach + void setUp() { + when(rateLimiters.getVerificationCaptchaLimiter()) + .thenReturn(captchaLimiter); + when(rateLimiters.getVerificationPushChallengeLimiter()) + .thenReturn(pushChallengeLimiter); + + when(accountsManager.getByE164(any())).thenReturn(Optional.empty()); + } + + @ParameterizedTest + @MethodSource + void createSessionUnprocessableRequestJson(final String number, final String pushToken, final String pushTokenType) { + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session") + .request(); + try (Response response = request.post( + Entity.json(unprocessableCreateSessionJson(number, pushToken, pushTokenType)))) { + assertEquals(400, response.getStatus()); + } + + } + + static Stream createSessionUnprocessableRequestJson() { + return Stream.of( + Arguments.of("[]", null, null), + Arguments.of(String.format("\"%s\"", NUMBER), "some-push-token", "invalid-token-type") + ); + } + + @ParameterizedTest + @MethodSource + void createSessionInvalidRequestJson(final String number, final String pushToken, final String pushTokenType) { + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.post(Entity.json(createSessionJson(number, pushToken, pushTokenType)))) { + assertEquals(422, response.getStatus()); + } + } + + static Stream createSessionInvalidRequestJson() { + return Stream.of( + Arguments.of(null, null, null), + Arguments.of("+1800", null, null), + Arguments.of(" ", null, null), + Arguments.of(NUMBER, null, "fcm"), + Arguments.of(NUMBER, "some-push-token", null) + ); + } + + @Test + void createSessionRateLimited() { + when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) + .thenReturn(CompletableFuture.failedFuture(new RateLimitExceededException(null, true))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.post(Entity.json(createSessionJson(NUMBER, null, null)))) { + assertEquals(429, response.getStatus()); + } + } + + @Test + void createSessionRegistrationServiceError() { + when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) + .thenReturn(CompletableFuture.failedFuture(new RuntimeException("expected service error"))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.post(Entity.json(createSessionJson(NUMBER, null, null)))) { + assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, response.getStatus()); + } + } + + @ParameterizedTest + @MethodSource + void createSessionSuccess(final String pushToken, final String pushTokenType, + final List expectedRequestedInformation) { + when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) + .thenReturn( + CompletableFuture.completedFuture( + new RegistrationServiceSession(SESSION_ID, NUMBER, false, null, null, null, + SESSION_EXPIRATION_SECONDS))); + when(verificationSessionManager.insert(any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.post(Entity.json(createSessionJson(NUMBER, pushToken, pushTokenType)))) { + assertEquals(HttpStatus.SC_OK, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + assertEquals(expectedRequestedInformation, verificationSessionResponse.requestedInformation()); + assertFalse(verificationSessionResponse.allowedToRequestCode()); + assertFalse(verificationSessionResponse.verified()); + } + } + + static Stream createSessionSuccess() { + return Stream.of( + Arguments.of(null, null, List.of(VerificationSession.Information.CAPTCHA)), + Arguments.of("token", "fcm", + List.of(VerificationSession.Information.PUSH_CHALLENGE, VerificationSession.Information.CAPTCHA)) + ); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void createSessionReregistration(final boolean isReregistration) throws NumberParseException { + when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) + .thenReturn( + CompletableFuture.completedFuture( + new RegistrationServiceSession(SESSION_ID, NUMBER, false, null, null, null, + SESSION_EXPIRATION_SECONDS))); + + when(verificationSessionManager.insert(any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + when(accountsManager.getByE164(NUMBER)) + .thenReturn(isReregistration ? Optional.of(mock(Account.class)) : Optional.empty()); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + + try (final Response response = request.post(Entity.json(createSessionJson(NUMBER, null, null)))) { + assertEquals(HttpStatus.SC_OK, response.getStatus()); + + verify(registrationServiceClient).createRegistrationSession( + eq(PhoneNumberUtil.getInstance().parse(NUMBER, null)), + eq(isReregistration), + any() + ); + } + } + + @Test + void patchSessionMalformedId() { + final String invalidSessionId = "()()()"; + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + invalidSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.method("PATCH", Entity.json("{}"))) { + assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, response.getStatus()); + } + } + + @Test + void patchSessionNotFound() { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodeSessionId(SESSION_ID)) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.method("PATCH", Entity.json("{}"))) { + assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatus()); + } + } + + @Test + void patchSessionPushToken() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + new VerificationSession(null, List.of(VerificationSession.Information.CAPTCHA), Collections.emptyList(), + false, clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + + when(verificationSessionManager.update(any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.method("PATCH", Entity.json(updateSessionJson(null, null, "abcde", "fcm")))) { + assertEquals(HttpStatus.SC_OK, response.getStatus()); + + final ArgumentCaptor verificationSessionArgumentCaptor = ArgumentCaptor.forClass( + VerificationSession.class); + verify(verificationSessionManager).update(any(), verificationSessionArgumentCaptor.capture()); + + final VerificationSession updatedSession = verificationSessionArgumentCaptor.getValue(); + assertEquals(List.of(VerificationSession.Information.PUSH_CHALLENGE, VerificationSession.Information.CAPTCHA), + updatedSession.requestedInformation()); + assertTrue(updatedSession.submittedInformation().isEmpty()); + assertNotNull(updatedSession.pushChallenge()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertFalse(verificationSessionResponse.allowedToRequestCode()); + assertEquals(List.of(VerificationSession.Information.PUSH_CHALLENGE, VerificationSession.Information.CAPTCHA), + verificationSessionResponse.requestedInformation()); + } + } + + @Test + void patchSessionCaptchaRateLimited() throws Exception { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), false, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + + when(verificationSessionManager.update(any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + doThrow(RateLimitExceededException.class) + .when(captchaLimiter).validate(anyString()); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.method("PATCH", Entity.json(updateSessionJson("captcha", null, null, null)))) { + assertEquals(HttpStatus.SC_TOO_MANY_REQUESTS, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertFalse(verificationSessionResponse.allowedToRequestCode()); + assertTrue(verificationSessionResponse.requestedInformation().isEmpty()); + } + } + + @Test + void patchSessionPushChallengeRateLimited() throws Exception { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), false, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + + when(verificationSessionManager.update(any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + doThrow(RateLimitExceededException.class) + .when(pushChallengeLimiter).validate(anyString()); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.method("PATCH", Entity.json(updateSessionJson(null, "challenge", null, null)))) { + assertEquals(HttpStatus.SC_TOO_MANY_REQUESTS, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertFalse(verificationSessionResponse.allowedToRequestCode()); + assertTrue(verificationSessionResponse.requestedInformation().isEmpty()); + } + } + + @Test + void patchSessionPushChallengeMismatch() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession("challenge", List.of(VerificationSession.Information.PUSH_CHALLENGE), + Collections.emptyList(), false, clock.millis(), clock.millis(), + registrationServiceSession.expiration())))); + when(verificationSessionManager.update(any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.method("PATCH", Entity.json(updateSessionJson(null, "mismatched", null, null)))) { + assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertFalse(verificationSessionResponse.allowedToRequestCode()); + assertEquals(List.of( + VerificationSession.Information.PUSH_CHALLENGE), verificationSessionResponse.requestedInformation()); + } + } + + @Test + void patchSessionCaptchaInvalid() throws Exception { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, List.of(VerificationSession.Information.CAPTCHA), + Collections.emptyList(), false, clock.millis(), clock.millis(), + registrationServiceSession.expiration())))); + + when(registrationCaptchaManager.assessCaptcha(any(), any())) + .thenReturn(Optional.of(AssessmentResult.invalid())); + + when(verificationSessionManager.update(any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.method("PATCH", Entity.json(updateSessionJson("captcha", null, null, null)))) { + assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatus()); + + final ArgumentCaptor verificationSessionArgumentCaptor = ArgumentCaptor.forClass( + VerificationSession.class); + + verify(verificationSessionManager).update(any(), verificationSessionArgumentCaptor.capture()); + + final VerificationSession updatedSession = verificationSessionArgumentCaptor.getValue(); + assertEquals(List.of(VerificationSession.Information.CAPTCHA), + updatedSession.requestedInformation()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertFalse(verificationSessionResponse.allowedToRequestCode()); + assertEquals(List.of( + VerificationSession.Information.CAPTCHA), verificationSessionResponse.requestedInformation()); + } + } + + @Test + void patchSessionPushChallengeAlreadySubmitted() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession("challenge", + List.of(VerificationSession.Information.CAPTCHA), + List.of(VerificationSession.Information.PUSH_CHALLENGE), false, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + when(verificationSessionManager.update(any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.method("PATCH", Entity.json(updateSessionJson(null, "challenge", null, null)))) { + assertEquals(HttpStatus.SC_OK, response.getStatus()); + + final ArgumentCaptor verificationSessionArgumentCaptor = ArgumentCaptor.forClass( + VerificationSession.class); + + verify(verificationSessionManager).update(any(), verificationSessionArgumentCaptor.capture()); + + final VerificationSession updatedSession = verificationSessionArgumentCaptor.getValue(); + assertEquals(List.of(VerificationSession.Information.PUSH_CHALLENGE), + updatedSession.submittedInformation()); + assertEquals(List.of(VerificationSession.Information.CAPTCHA), updatedSession.requestedInformation()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertFalse(verificationSessionResponse.allowedToRequestCode()); + assertEquals(List.of( + VerificationSession.Information.CAPTCHA), verificationSessionResponse.requestedInformation()); + } + } + + @Test + void patchSessionAlreadyVerified() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + true, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession("challenge", List.of(), List.of(), true, clock.millis(), clock.millis(), + registrationServiceSession.expiration())))); + + when(verificationSessionManager.update(any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.method("PATCH", Entity.json(updateSessionJson(null, "challenge", null, null)))) { + assertEquals(HttpStatus.SC_OK, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertTrue(verificationSessionResponse.allowedToRequestCode()); + assertTrue(verificationSessionResponse.verified()); + assertTrue(verificationSessionResponse.requestedInformation().isEmpty()); + + verify(registrationRecoveryPasswordsManager).removeForNumber(registrationServiceSession.number()); + } + } + + @Test + void patchSessionPushChallengeSuccess() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession("challenge", + List.of(VerificationSession.Information.PUSH_CHALLENGE, VerificationSession.Information.CAPTCHA), + Collections.emptyList(), false, clock.millis(), clock.millis(), + registrationServiceSession.expiration())))); + when(verificationSessionManager.update(any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.method("PATCH", Entity.json(updateSessionJson(null, "challenge", null, null)))) { + assertEquals(HttpStatus.SC_OK, response.getStatus()); + + final ArgumentCaptor verificationSessionArgumentCaptor = ArgumentCaptor.forClass( + VerificationSession.class); + + verify(verificationSessionManager).update(any(), verificationSessionArgumentCaptor.capture()); + + final VerificationSession updatedSession = verificationSessionArgumentCaptor.getValue(); + assertEquals(List.of(VerificationSession.Information.PUSH_CHALLENGE), + updatedSession.submittedInformation()); + assertTrue(updatedSession.requestedInformation().isEmpty()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertTrue(verificationSessionResponse.allowedToRequestCode()); + assertTrue(verificationSessionResponse.requestedInformation().isEmpty()); + } + } + + @Test + void patchSessionCaptchaSuccess() throws Exception { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, List.of(VerificationSession.Information.CAPTCHA), + Collections.emptyList(), false, clock.millis(), clock.millis(), + registrationServiceSession.expiration())))); + + when(registrationCaptchaManager.assessCaptcha(any(), any())) + .thenReturn(Optional.of(AssessmentResult.alwaysValid())); + + when(verificationSessionManager.update(any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.method("PATCH", Entity.json(updateSessionJson("captcha", null, null, null)))) { + assertEquals(HttpStatus.SC_OK, response.getStatus()); + + final ArgumentCaptor verificationSessionArgumentCaptor = ArgumentCaptor.forClass( + VerificationSession.class); + + verify(verificationSessionManager).update(any(), verificationSessionArgumentCaptor.capture()); + + final VerificationSession updatedSession = verificationSessionArgumentCaptor.getValue(); + assertEquals(List.of(VerificationSession.Information.CAPTCHA), + updatedSession.submittedInformation()); + assertTrue(updatedSession.requestedInformation().isEmpty()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertTrue(verificationSessionResponse.allowedToRequestCode()); + assertTrue(verificationSessionResponse.requestedInformation().isEmpty()); + } + } + + @Test + void patchSessionPushAndCaptchaSuccess() throws Exception { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession("challenge", + List.of(VerificationSession.Information.CAPTCHA, VerificationSession.Information.CAPTCHA), + Collections.emptyList(), false, clock.millis(), clock.millis(), + registrationServiceSession.expiration())))); + + when(registrationCaptchaManager.assessCaptcha(any(), any())) + .thenReturn(Optional.of(AssessmentResult.alwaysValid())); + + when(verificationSessionManager.update(any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.method("PATCH", + Entity.json(updateSessionJson("captcha", "challenge", null, null)))) { + assertEquals(HttpStatus.SC_OK, response.getStatus()); + + final ArgumentCaptor verificationSessionArgumentCaptor = ArgumentCaptor.forClass( + VerificationSession.class); + + verify(verificationSessionManager).update(any(), verificationSessionArgumentCaptor.capture()); + + final VerificationSession updatedSession = verificationSessionArgumentCaptor.getValue(); + assertEquals(List.of(VerificationSession.Information.PUSH_CHALLENGE, VerificationSession.Information.CAPTCHA), + updatedSession.submittedInformation()); + assertTrue(updatedSession.requestedInformation().isEmpty()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertTrue(verificationSessionResponse.allowedToRequestCode()); + assertTrue(verificationSessionResponse.requestedInformation().isEmpty()); + } + } + + @Test + void patchSessionTokenUpdatedCaptchaError() throws Exception { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture(Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, + List.of(VerificationSession.Information.CAPTCHA), + Collections.emptyList(), false, clock.millis(), clock.millis(), + registrationServiceSession.expiration())))); + + when(registrationCaptchaManager.assessCaptcha(any(), any())) + .thenThrow(new IOException("expected service error")); + + when(verificationSessionManager.update(any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.method("PATCH", + Entity.json(updateSessionJson("captcha", null, "token", "fcm")))) { + assertEquals(HttpStatus.SC_SERVICE_UNAVAILABLE, response.getStatus()); + + final ArgumentCaptor verificationSessionArgumentCaptor = ArgumentCaptor.forClass( + VerificationSession.class); + + verify(verificationSessionManager).update(any(), verificationSessionArgumentCaptor.capture()); + + final VerificationSession updatedSession = verificationSessionArgumentCaptor.getValue(); + assertTrue(updatedSession.submittedInformation().isEmpty()); + assertEquals(List.of(VerificationSession.Information.PUSH_CHALLENGE, VerificationSession.Information.CAPTCHA), + updatedSession.requestedInformation()); + assertNotNull(updatedSession.pushChallenge()); + } + } + + @Test + void getSessionMalformedId() { + final String invalidSessionId = "()()()"; + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + invalidSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.get()) { + assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, response.getStatus()); + } + } + + @Test + void getSessionInvalidArgs() { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.failedFuture(new StatusRuntimeException(Status.INVALID_ARGUMENT))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodeSessionId(SESSION_ID)) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.get()) { + assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatus()); + } + } + + @Test + void getSessionNotFound() { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + when(verificationSessionManager.findForId(encodeSessionId(SESSION_ID))) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodeSessionId(SESSION_ID)) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.get()) { + assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatus()); + } + + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + new RegistrationServiceSession(SESSION_ID, NUMBER, false, null, null, null, + SESSION_EXPIRATION_SECONDS)))); + + request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodeSessionId(SESSION_ID)) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.get()) { + assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatus()); + } + } + + @Test + void getSessionError() { + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.failedFuture(new RuntimeException())); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodeSessionId(SESSION_ID)) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.get()) { + assertEquals(HttpStatus.SC_SERVICE_UNAVAILABLE, response.getStatus()); + } + } + + @Test + void getSessionSuccess() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + new RegistrationServiceSession(SESSION_ID, NUMBER, false, null, null, null, + SESSION_EXPIRATION_SECONDS)))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture(Optional.of(mock(VerificationSession.class)))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.get()) { + assertEquals(HttpStatus.SC_OK, response.getStatus()); + } + } + + @Test + void getSessionSuccessAlreadyVerified() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + true, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture(Optional.of(mock(VerificationSession.class)))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.get()) { + assertEquals(HttpStatus.SC_OK, response.getStatus()); + + verify(registrationRecoveryPasswordsManager).removeForNumber(registrationServiceSession.number()); + } + } + + @Test + void requestVerificationCodeAlreadyVerified() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + true, null, null, + null, SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture(Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), true, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + when(registrationServiceClient.sendVerificationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(registrationServiceSession)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.post(Entity.json(requestVerificationCodeJson("sms", "android")))) { + assertEquals(HttpStatus.SC_CONFLICT, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertTrue(verificationSessionResponse.verified()); + } + } + + @Test + void requestVerificationCodeNotAllowedInformationRequested() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture(Optional.of(new VerificationSession(null, List.of( + VerificationSession.Information.CAPTCHA), Collections.emptyList(), false, clock.millis(), clock.millis(), + registrationServiceSession.expiration())))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.post(Entity.json(requestVerificationCodeJson("sms", "ios")))) { + assertEquals(HttpStatus.SC_CONFLICT, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertFalse(verificationSessionResponse.allowedToRequestCode()); + assertEquals(List.of(VerificationSession.Information.CAPTCHA), + verificationSessionResponse.requestedInformation()); + } + } + + @Test + void requestVerificationCodeNotAllowed() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, null, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of( + registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), false, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.post(Entity.json(requestVerificationCodeJson("voice", "android")))) { + assertEquals(HttpStatus.SC_TOO_MANY_REQUESTS, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertFalse(verificationSessionResponse.allowedToRequestCode()); + assertTrue(verificationSessionResponse.requestedInformation().isEmpty()); + } + } + + @Test + void requestVerificationCodeRateLimitExceeded() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, + null, SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture(Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), true, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + when(registrationServiceClient.sendVerificationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.failedFuture( + new CompletionException(new VerificationSessionRateLimitExceededException(registrationServiceSession, + Duration.ofMinutes(1), true)))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.post(Entity.json(requestVerificationCodeJson("sms", "android")))) { + assertEquals(HttpStatus.SC_TOO_MANY_REQUESTS, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertTrue(verificationSessionResponse.allowedToRequestCode()); + assertTrue(verificationSessionResponse.requestedInformation().isEmpty()); + } + } + + @Test + void requestVerificationCodeTransportNotAllowed() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, + null, SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture(Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), true, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + when(registrationServiceClient.sendVerificationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.failedFuture( + new CompletionException(new TransportNotAllowedException(registrationServiceSession)))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + + try (final Response response = request.post(Entity.json(requestVerificationCodeJson("sms", "android")))) { + assertEquals(418, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = + response.readEntity(VerificationSessionResponse.class); + + assertTrue(verificationSessionResponse.allowedToRequestCode()); + assertTrue(verificationSessionResponse.requestedInformation().isEmpty()); + } + } + + @Test + void requestVerificationCodeSuccess() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, + null, SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture(Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), true, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + when(registrationServiceClient.sendVerificationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(registrationServiceSession)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.post(Entity.json(requestVerificationCodeJson("sms", "android")))) { + assertEquals(HttpStatus.SC_OK, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertTrue(verificationSessionResponse.allowedToRequestCode()); + assertTrue(verificationSessionResponse.requestedInformation().isEmpty()); + } + } + + @ParameterizedTest + @MethodSource + void requestVerificationCodeExternalServiceRefused(final boolean expectedPermanent, final String expectedReason, + final RegistrationServiceSenderException exception) { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, 0L, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), true, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + + when(registrationServiceClient.sendVerificationCode(any(), any(), any(), any(), any())) + .thenReturn( + CompletableFuture.failedFuture(new CompletionException(exception))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.post(Entity.json(requestVerificationCodeJson("voice", "ios")))) { + assertEquals(RegistrationServiceSenderExceptionMapper.REMOTE_SERVICE_REJECTED_REQUEST_STATUS, response.getStatus()); + + final Map responseMap = response.readEntity(Map.class); + + assertEquals(expectedReason, responseMap.get("reason")); + assertEquals(expectedPermanent, responseMap.get("permanentFailure")); + } + } + + static Stream requestVerificationCodeExternalServiceRefused() { + return Stream.of( + Arguments.of(true, "illegalArgument", RegistrationServiceSenderException.illegalArgument(true)), + Arguments.of(true, "providerRejected", RegistrationServiceSenderException.rejected(true)), + Arguments.of(false, "providerUnavailable", RegistrationServiceSenderException.unknown(false)) + ); + } + + @Test + void verifyCodeServerError() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, 0L, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), true, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + + when(registrationServiceClient.checkVerificationCode(any(), any(), any())) + .thenReturn(CompletableFuture.failedFuture(new CompletionException(new RuntimeException()))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.put(Entity.json(submitVerificationCodeJson("123456")))) { + assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, response.getStatus()); + } + } + + @Test + void verifyCodeAlreadyVerified() { + + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + true, null, null, 0L, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), true, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.put( + Entity.json(submitVerificationCodeJson("123456")))) { + + verify(registrationServiceClient).getSession(any(), any()); + verifyNoMoreInteractions(registrationServiceClient); + + assertEquals(HttpStatus.SC_CONFLICT, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + assertTrue(verificationSessionResponse.verified()); + + verify(registrationRecoveryPasswordsManager).removeForNumber(registrationServiceSession.number()); + } + } + + @Test + void verifyCodeNoCodeRequested() { + + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, 0L, null, 0L, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), true, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + + // There is no explicit indication in the exception that no code has been sent, but we treat all RegistrationServiceExceptions + // in which the response has a session object as conflicted state + when(registrationServiceClient.checkVerificationCode(any(), any(), any())) + .thenReturn(CompletableFuture.failedFuture(new CompletionException( + new RegistrationServiceException(new RegistrationServiceSession(SESSION_ID, NUMBER, false, 0L, null, null, + SESSION_EXPIRATION_SECONDS))))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.put(Entity.json(submitVerificationCodeJson("123456")))) { + assertEquals(HttpStatus.SC_CONFLICT, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertNotNull(verificationSessionResponse.nextSms()); + assertNull(verificationSessionResponse.nextVerificationAttempt()); + } + } + + @Test + void verifyCodeNoSession() { + + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, 0L, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), true, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + + when(registrationServiceClient.checkVerificationCode(any(), any(), any())) + .thenReturn(CompletableFuture.failedFuture(new CompletionException(new RegistrationServiceException(null)))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.put(Entity.json(submitVerificationCodeJson("123456")))) { + assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatus()); + } + } + + @Test + void verifyCodeRateLimitExceeded() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, 0L, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), true, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + when(registrationServiceClient.checkVerificationCode(any(), any(), any())) + .thenReturn(CompletableFuture.failedFuture( + new CompletionException(new VerificationSessionRateLimitExceededException(registrationServiceSession, + Duration.ofMinutes(1), true)))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.put(Entity.json(submitVerificationCodeJson("567890")))) { + assertEquals(HttpStatus.SC_TOO_MANY_REQUESTS, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertTrue(verificationSessionResponse.allowedToRequestCode()); + assertTrue(verificationSessionResponse.requestedInformation().isEmpty()); + } + } + + @Test + void verifyCodeSuccess() { + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, 0L, SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), true, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + + final RegistrationServiceSession verifiedSession = new RegistrationServiceSession(SESSION_ID, NUMBER, true, null, + null, 0L, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.checkVerificationCode(any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(verifiedSession)); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.put(Entity.json(submitVerificationCodeJson("123456")))) { + assertEquals(HttpStatus.SC_OK, response.getStatus()); + + final VerificationSessionResponse verificationSessionResponse = response.readEntity( + VerificationSessionResponse.class); + + assertTrue(verificationSessionResponse.verified()); + + verify(registrationRecoveryPasswordsManager).removeForNumber(verifiedSession.number()); + } + } + + /** + * Request JSON in the shape of {@link org.whispersystems.textsecuregcm.entities.CreateVerificationSessionRequest} + */ + private static String createSessionJson(final String number, final String pushToken, + final String pushTokenType) { + return String.format(""" + { + "number": %s, + "pushToken": %s, + "pushTokenType": %s + } + """, quoteIfNotNull(number), quoteIfNotNull(pushToken), quoteIfNotNull(pushTokenType)); + } + + /** + * Request JSON in the shape of {@link org.whispersystems.textsecuregcm.entities.UpdateVerificationSessionRequest} + */ + private static String updateSessionJson(final String captcha, final String pushChallenge, final String pushToken, + final String pushTokenType) { + return String.format(""" + { + "captcha": %s, + "pushChallenge": %s, + "pushToken": %s, + "pushTokenType": %s + } + """, quoteIfNotNull(captcha), quoteIfNotNull(pushChallenge), quoteIfNotNull(pushToken), + quoteIfNotNull(pushTokenType)); + } + + /** + * Request JSON in the shape of {@link org.whispersystems.textsecuregcm.entities.VerificationCodeRequest} + */ + private static String requestVerificationCodeJson(final String transport, final String client) { + return String.format(""" + { + "transport": "%s", + "client": "%s" + } + """, transport, client); + } + + /** + * Request JSON in the shape of {@link org.whispersystems.textsecuregcm.entities.SubmitVerificationCodeRequest} + */ + private static String submitVerificationCodeJson(final String code) { + return String.format(""" + { + "code": "%s" + } + """, code); + } + + private static String quoteIfNotNull(final String s) { + return s == null ? null : StringUtils.join(new String[]{"\"", "\""}, s); + } + + /** + * Request JSON that cannot be marshalled into + * {@link org.whispersystems.textsecuregcm.entities.CreateVerificationSessionRequest} + */ + private static String unprocessableCreateSessionJson(final String number, final String pushToken, + final String pushTokenType) { + return String.format(""" + { + "number": %s, + "pushToken": %s, + "pushTokenType": %s + } + """, number, quoteIfNotNull(pushToken), quoteIfNotNull(pushTokenType)); + } + + private static String encodeSessionId(final byte[] sessionId) { + return Base64.getUrlEncoder().encodeToString(sessionId); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/currency/CoinMarketCapClientTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/currency/CoinMarketCapClientTest.java new file mode 100644 index 000000000..85f3c5c60 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/currency/CoinMarketCapClientTest.java @@ -0,0 +1,61 @@ +package org.whispersystems.textsecuregcm.currency; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class CoinMarketCapClientTest { + + private static final String RESPONSE_JSON = """ + { + "status": { + "timestamp": "2022-11-09T17:15:06.356Z", + "error_code": 0, + "error_message": null, + "elapsed": 41, + "credit_count": 1, + "notice": null + }, + "data": { + "id": 7878, + "symbol": "MOB", + "name": "MobileCoin", + "amount": 1, + "last_updated": "2022-11-09T17:14:00.000Z", + "quote": { + "USD": { + "price": 0.6625319895827952, + "last_updated": "2022-11-09T17:14:00.000Z" + } + } + } + } + """; + + @Test + void parseResponse() throws JsonProcessingException { + final CoinMarketCapClient.CoinMarketCapResponse parsedResponse = CoinMarketCapClient.parseResponse(RESPONSE_JSON); + + assertEquals(7878, parsedResponse.priceConversionResponse().id()); + assertEquals("MOB", parsedResponse.priceConversionResponse().symbol()); + + final Map quote = + parsedResponse.priceConversionResponse().quote(); + + assertEquals(1, quote.size()); + assertEquals(new BigDecimal("0.6625319895827952"), quote.get("USD").price()); + } + + @Test + void extractConversionRate() throws IOException { + final CoinMarketCapClient.CoinMarketCapResponse parsedResponse = CoinMarketCapClient.parseResponse(RESPONSE_JSON); + + assertEquals(new BigDecimal("0.6625319895827952"), CoinMarketCapClient.extractConversionRate(parsedResponse, "USD")); + assertThrows(IOException.class, () -> CoinMarketCapClient.extractConversionRate(parsedResponse, "CAD")); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManagerTest.java new file mode 100644 index 000000000..4a3797c33 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/currency/CurrencyConversionManagerTest.java @@ -0,0 +1,235 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.currency; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntityList; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; + +class CurrencyConversionManagerTest { + + @RegisterExtension + static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); + + @Test + void testCurrencyCalculations() throws IOException { + FixerClient fixerClient = mock(FixerClient.class); + CoinMarketCapClient coinMarketCapClient = mock(CoinMarketCapClient.class); + + when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("2.35")); + when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( + "EUR", new BigDecimal("0.822876"), + "FJD", new BigDecimal("2.0577"), + "FKP", new BigDecimal("0.743446") + )); + + CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), + List.of("FOO"), EXECUTOR, Clock.systemUTC()); + + manager.updateCacheIfNecessary(); + + CurrencyConversionEntityList conversions = manager.getCurrencyConversions().orElseThrow(); + + assertThat(conversions.getCurrencies().size()).isEqualTo(1); + assertThat(conversions.getCurrencies().get(0).getBase()).isEqualTo("FOO"); + assertThat(conversions.getCurrencies().get(0).getConversions().size()).isEqualTo(4); + assertThat(conversions.getCurrencies().get(0).getConversions().get("USD")).isEqualTo(new BigDecimal("2.35")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("EUR")).isEqualTo(new BigDecimal("1.9337586")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("FJD")).isEqualTo(new BigDecimal("4.835595")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("FKP")).isEqualTo(new BigDecimal("1.7470981")); + } + + @Test + void testCurrencyCalculations_noTrailingZeros() throws IOException { + FixerClient fixerClient = mock(FixerClient.class); + CoinMarketCapClient coinMarketCapClient = mock(CoinMarketCapClient.class); + + when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("1.00000")); + when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( + "EUR", new BigDecimal("0.200000"), + "FJD", new BigDecimal("3.00000"), + "FKP", new BigDecimal("50.0000"), + "CAD", new BigDecimal("700.000") + )); + + CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), + List.of("FOO"), EXECUTOR, Clock.systemUTC()); + + manager.updateCacheIfNecessary(); + + CurrencyConversionEntityList conversions = manager.getCurrencyConversions().orElseThrow(); + + assertThat(conversions.getCurrencies().size()).isEqualTo(1); + assertThat(conversions.getCurrencies().get(0).getBase()).isEqualTo("FOO"); + assertThat(conversions.getCurrencies().get(0).getConversions().size()).isEqualTo(5); + assertThat(conversions.getCurrencies().get(0).getConversions().get("USD")).isEqualTo(new BigDecimal("1")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("EUR")).isEqualTo(new BigDecimal("0.2")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("FJD")).isEqualTo(new BigDecimal("3")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("FKP")).isEqualTo(new BigDecimal("50")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("CAD")).isEqualTo(new BigDecimal("700")); + } + + @Test + void testCurrencyCalculations_accuracy() throws IOException { + FixerClient fixerClient = mock(FixerClient.class); + CoinMarketCapClient coinMarketCapClient = mock(CoinMarketCapClient.class); + + when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("0.999999")); + when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( + "EUR", new BigDecimal("1.000001"), + "FJD", new BigDecimal("0.000001"), + "FKP", new BigDecimal("1") + )); + + CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), + List.of("FOO"), EXECUTOR, Clock.systemUTC()); + + manager.updateCacheIfNecessary(); + + CurrencyConversionEntityList conversions = manager.getCurrencyConversions().orElseThrow(); + + assertThat(conversions.getCurrencies().size()).isEqualTo(1); + assertThat(conversions.getCurrencies().get(0).getBase()).isEqualTo("FOO"); + assertThat(conversions.getCurrencies().get(0).getConversions().size()).isEqualTo(4); + assertThat(conversions.getCurrencies().get(0).getConversions().get("USD")).isEqualTo(new BigDecimal("0.999999")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("EUR")).isEqualTo(new BigDecimal("0.999999999999")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("FJD")).isEqualTo(new BigDecimal("0.000000999999")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("FKP")).isEqualTo(new BigDecimal("0.999999")); + + } + + @Test + void testCurrencyCalculationsTimeoutNoRun() throws IOException { + FixerClient fixerClient = mock(FixerClient.class); + CoinMarketCapClient coinMarketCapClient = mock(CoinMarketCapClient.class); + + when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("2.35")); + when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( + "EUR", new BigDecimal("0.822876"), + "FJD", new BigDecimal("2.0577"), + "FKP", new BigDecimal("0.743446") + )); + + CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), + List.of("FOO"), EXECUTOR, Clock.systemUTC()); + + manager.updateCacheIfNecessary(); + + when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("3.50")); + + manager.updateCacheIfNecessary(); + + CurrencyConversionEntityList conversions = manager.getCurrencyConversions().orElseThrow(); + + assertThat(conversions.getCurrencies().size()).isEqualTo(1); + assertThat(conversions.getCurrencies().get(0).getBase()).isEqualTo("FOO"); + assertThat(conversions.getCurrencies().get(0).getConversions().size()).isEqualTo(4); + assertThat(conversions.getCurrencies().get(0).getConversions().get("USD")).isEqualTo(new BigDecimal("2.35")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("EUR")).isEqualTo(new BigDecimal("1.9337586")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("FJD")).isEqualTo(new BigDecimal("4.835595")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("FKP")).isEqualTo(new BigDecimal("1.7470981")); + } + + @Test + void testCurrencyCalculationsCoinMarketCapTimeoutWithRun() throws IOException { + FixerClient fixerClient = mock(FixerClient.class); + CoinMarketCapClient coinMarketCapClient = mock(CoinMarketCapClient.class); + + when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("2.35")); + when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( + "EUR", new BigDecimal("0.822876"), + "FJD", new BigDecimal("2.0577"), + "FKP", new BigDecimal("0.743446") + )); + + CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), + List.of("FOO"), EXECUTOR, Clock.systemUTC()); + + manager.updateCacheIfNecessary(); + + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster(connection -> + connection.sync().del(CurrencyConversionManager.COIN_MARKET_CAP_SHARED_CACHE_CURRENT_KEY)); + + when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("3.50")); + manager.updateCacheIfNecessary(); + + CurrencyConversionEntityList conversions = manager.getCurrencyConversions().orElseThrow(); + + assertThat(conversions.getCurrencies().size()).isEqualTo(1); + assertThat(conversions.getCurrencies().get(0).getBase()).isEqualTo("FOO"); + assertThat(conversions.getCurrencies().get(0).getConversions().size()).isEqualTo(4); + assertThat(conversions.getCurrencies().get(0).getConversions().get("USD")).isEqualTo(new BigDecimal("3.5")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("EUR")).isEqualTo(new BigDecimal("2.880066")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("FJD")).isEqualTo(new BigDecimal("7.20195")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("FKP")).isEqualTo(new BigDecimal("2.602061")); + } + + + @Test + void testCurrencyCalculationsFixerTimeoutWithRun() throws IOException { + FixerClient fixerClient = mock(FixerClient.class); + CoinMarketCapClient coinMarketCapClient = mock(CoinMarketCapClient.class); + + when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("2.35")); + when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( + "EUR", new BigDecimal("0.822876"), + "FJD", new BigDecimal("2.0577"), + "FKP", new BigDecimal("0.743446") + )); + + final Instant currentTime = Instant.now().truncatedTo(ChronoUnit.MILLIS); + + final Clock clock = mock(Clock.class); + when(clock.instant()).thenReturn(currentTime); + when(clock.millis()).thenReturn(currentTime.toEpochMilli()); + + CurrencyConversionManager manager = new CurrencyConversionManager(fixerClient, coinMarketCapClient, REDIS_CLUSTER_EXTENSION.getRedisCluster(), + List.of("FOO"), EXECUTOR, clock); + + manager.updateCacheIfNecessary(); + + when(coinMarketCapClient.getSpotPrice(eq("FOO"), eq("USD"))).thenReturn(new BigDecimal("3.50")); + when(fixerClient.getConversionsForBase(eq("USD"))).thenReturn(Map.of( + "EUR", new BigDecimal("0.922876"), + "FJD", new BigDecimal("2.0577"), + "FKP", new BigDecimal("0.743446") + )); + + final Instant afterFixerExpiration = currentTime.plus(CurrencyConversionManager.FIXER_REFRESH_INTERVAL).plusMillis(1); + when(clock.instant()).thenReturn(afterFixerExpiration); + when(clock.millis()).thenReturn(afterFixerExpiration.toEpochMilli()); + + manager.updateCacheIfNecessary(); + + CurrencyConversionEntityList conversions = manager.getCurrencyConversions().orElseThrow(); + + assertThat(conversions.getCurrencies().size()).isEqualTo(1); + assertThat(conversions.getCurrencies().get(0).getBase()).isEqualTo("FOO"); + assertThat(conversions.getCurrencies().get(0).getConversions().size()).isEqualTo(4); + assertThat(conversions.getCurrencies().get(0).getConversions().get("USD")).isEqualTo(new BigDecimal("2.35")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("EUR")).isEqualTo(new BigDecimal("2.1687586")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("FJD")).isEqualTo(new BigDecimal("4.835595")); + assertThat(conversions.getCurrencies().get(0).getConversions().get("FKP")).isEqualTo(new BigDecimal("1.7470981")); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/currency/FixerClientTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/currency/FixerClientTest.java new file mode 100644 index 000000000..94d1fd469 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/currency/FixerClientTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.currency; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.jsonFixture; + +import java.io.IOException; +import java.math.BigDecimal; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class FixerClientTest { + + @Test + public void testGetConversionsForBase() throws IOException, InterruptedException { + HttpResponse httpResponse = mock(HttpResponse.class); + when(httpResponse.statusCode()).thenReturn(200); + when(httpResponse.body()).thenReturn(jsonFixture("fixtures/fixer.res.json")); + + HttpClient httpClient = mock(HttpClient.class); + when(httpClient.send(any(HttpRequest.class), any(BodyHandler.class))).thenReturn(httpResponse); + + FixerClient fixerClient = new FixerClient(httpClient, "foo"); + Map conversions = fixerClient.getConversionsForBase("EUR"); + assertThat(conversions.get("CAD")).isEqualTo(new BigDecimal("1.560132")); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/entities/AnswerChallengeRequestTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/entities/AnswerChallengeRequestTest.java new file mode 100644 index 000000000..8572833d9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/entities/AnswerChallengeRequestTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +class AnswerChallengeRequestTest { + + @Test + void parse() throws JsonProcessingException { + { + final String pushChallengeJson = """ + { + "type": "rateLimitPushChallenge", + "challenge": "Hello I am a push challenge token" + } + """; + + final AnswerChallengeRequest answerChallengeRequest = + SystemMapper.jsonMapper().readValue(pushChallengeJson, AnswerChallengeRequest.class); + + assertTrue(answerChallengeRequest instanceof AnswerPushChallengeRequest); + assertEquals("Hello I am a push challenge token", + ((AnswerPushChallengeRequest) answerChallengeRequest).getChallenge()); + } + + { + final String recaptchaChallengeJson = """ + { + "type": "recaptcha", + "token": "A server-generated token", + "captcha": "The value of the solved captcha token" + } + """; + + final AnswerChallengeRequest answerChallengeRequest = + SystemMapper.jsonMapper().readValue(recaptchaChallengeJson, AnswerChallengeRequest.class); + + assertTrue(answerChallengeRequest instanceof AnswerRecaptchaChallengeRequest); + + assertEquals("A server-generated token", + ((AnswerRecaptchaChallengeRequest) answerChallengeRequest).getToken()); + + assertEquals("The value of the solved captcha token", + ((AnswerRecaptchaChallengeRequest) answerChallengeRequest).getCaptcha()); + } + + { + final String unrecognizedTypeJson = """ + { + "type": "unrecognized", + "token": "A server-generated token", + "captcha": "The value of the solved captcha token" + } + """; + + assertThrows(InvalidTypeIdException.class, + () -> SystemMapper.jsonMapper().readValue(unrecognizedTypeJson, AnswerChallengeRequest.class)); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/entities/IncomingMessageListTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/entities/IncomingMessageListTest.java new file mode 100644 index 000000000..d455f1e66 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/entities/IncomingMessageListTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +class IncomingMessageListTest { + + @Test + void fromJson() throws JsonProcessingException { + { + final String incomingMessageListJson = """ + { + "messages": [], + "timestamp": 123456789, + "online": true, + "urgent": false + } + """; + + final IncomingMessageList incomingMessageList = + SystemMapper.jsonMapper().readValue(incomingMessageListJson, IncomingMessageList.class); + + assertTrue(incomingMessageList.online()); + assertFalse(incomingMessageList.urgent()); + } + + { + final String incomingMessageListJson = """ + { + "messages": [], + "timestamp": 123456789, + "online": true + } + """; + + final IncomingMessageList incomingMessageList = + SystemMapper.jsonMapper().readValue(incomingMessageListJson, IncomingMessageList.class); + + assertTrue(incomingMessageList.online()); + assertTrue(incomingMessageList.urgent()); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntityTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntityTest.java new file mode 100644 index 000000000..5c62c7e19 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/entities/OutgoingMessageEntityTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Random; +import java.util.UUID; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.cartesian.ArgumentSets; +import org.junitpioneer.jupiter.cartesian.CartesianTest; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; + +class OutgoingMessageEntityTest { + + @CartesianTest + @CartesianTest.MethodFactory("roundTripThroughEnvelope") + void roundTripThroughEnvelope(@Nullable final ServiceIdentifier sourceIdentifier, + final ServiceIdentifier destinationIdentifier, + @Nullable final UUID updatedPni) { + + final byte[] messageContent = new byte[16]; + new Random().nextBytes(messageContent); + + final long messageTimestamp = System.currentTimeMillis(); + final long serverTimestamp = messageTimestamp + 17; + + byte[] reportSpamToken = {1, 2, 3, 4, 5}; + + final OutgoingMessageEntity outgoingMessageEntity = new OutgoingMessageEntity( + UUID.randomUUID(), + MessageProtos.Envelope.Type.CIPHERTEXT_VALUE, + messageTimestamp, + sourceIdentifier, + sourceIdentifier != null ? (int) Device.MASTER_ID : 0, + destinationIdentifier, + updatedPni, + messageContent, + serverTimestamp, + true, + false, + reportSpamToken); + + assertEquals(outgoingMessageEntity, OutgoingMessageEntity.fromEnvelope(outgoingMessageEntity.toEnvelope())); + } + + @SuppressWarnings("unused") + static ArgumentSets roundTripThroughEnvelope() { + return ArgumentSets.argumentsForFirstParameter(new AciServiceIdentifier(UUID.randomUUID()), + new PniServiceIdentifier(UUID.randomUUID()), + null) + .argumentsForNextParameter(new AciServiceIdentifier(UUID.randomUUID()), + new PniServiceIdentifier(UUID.randomUUID())) + .argumentsForNextParameter(UUID.randomUUID(), null); + } + + @Test + void entityPreservesEnvelope() { + final Random random = new Random(); + + final byte[] messageContent = new byte[16]; + random.nextBytes(messageContent); + + final byte[] reportSpamToken = new byte[8]; + random.nextBytes(reportSpamToken); + + final Account account = new Account(); + account.setUuid(UUID.randomUUID()); + + IncomingMessage message = new IncomingMessage(1, 4444L, 55, "AAAAAA"); + + MessageProtos.Envelope baseEnvelope = message.toEnvelope( + new AciServiceIdentifier(UUID.randomUUID()), + account, + 123L, + System.currentTimeMillis(), + false, + true, + reportSpamToken); + + MessageProtos.Envelope envelope = baseEnvelope.toBuilder().setServerGuid(UUID.randomUUID().toString()).build(); + + assertEquals(envelope, OutgoingMessageEntity.fromEnvelope(envelope).toEnvelope()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/entities/PreKeyTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/entities/PreKeyTest.java new file mode 100644 index 000000000..37e033e84 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/entities/PreKeyTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.asJson; +import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.jsonFixture; + +import java.util.Base64; +import org.junit.jupiter.api.Test; +import org.signal.libsignal.protocol.ecc.ECPublicKey; + +class PreKeyTest { + + private static final byte[] PUBLIC_KEY = Base64.getDecoder().decode("BQ+NbroQtVKyFaCSfqzSw8Wy72Ff22RSa5ERKTv5DIk2"); + + @Test + void serializeToJSONV2() throws Exception { + ECPreKey preKey = new ECPreKey(1234, new ECPublicKey(PUBLIC_KEY)); + + assertThat("PreKeyV2 Serialization works", + asJson(preKey), + is(equalTo(jsonFixture("fixtures/prekey_v2.json")))); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/experiment/ExperimentEnrollmentManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/experiment/ExperimentEnrollmentManagerTest.java new file mode 100644 index 000000000..9f079afb6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/experiment/ExperimentEnrollmentManagerTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.experiment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicExperimentEnrollmentConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicPreRegistrationExperimentEnrollmentConfiguration; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; + +class ExperimentEnrollmentManagerTest { + + private DynamicExperimentEnrollmentConfiguration experimentEnrollmentConfiguration; + private DynamicPreRegistrationExperimentEnrollmentConfiguration preRegistrationExperimentEnrollmentConfiguration; + + private ExperimentEnrollmentManager experimentEnrollmentManager; + + private Account account; + + private static final UUID ACCOUNT_UUID = UUID.randomUUID(); + private static final String UUID_EXPERIMENT_NAME = "uuid_test"; + + private static final String ENROLLED_164 = "+12025551212"; + private static final String EXCLUDED_164 = "+18005551212"; + private static final String E164_EXPERIMENT_NAME = "e164_test"; + + @BeforeEach + void setUp() { + final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); + final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); + + experimentEnrollmentManager = new ExperimentEnrollmentManager(dynamicConfigurationManager); + + experimentEnrollmentConfiguration = mock(DynamicExperimentEnrollmentConfiguration.class); + preRegistrationExperimentEnrollmentConfiguration = mock( + DynamicPreRegistrationExperimentEnrollmentConfiguration.class); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + when(dynamicConfiguration.getExperimentEnrollmentConfiguration(UUID_EXPERIMENT_NAME)) + .thenReturn(Optional.of(experimentEnrollmentConfiguration)); + when(dynamicConfiguration.getPreRegistrationEnrollmentConfiguration(E164_EXPERIMENT_NAME)) + .thenReturn(Optional.of(preRegistrationExperimentEnrollmentConfiguration)); + + account = mock(Account.class); + when(account.getUuid()).thenReturn(ACCOUNT_UUID); + } + + @Test + void testIsEnrolled_UuidExperiment() { + assertFalse(experimentEnrollmentManager.isEnrolled(account.getUuid(), UUID_EXPERIMENT_NAME)); + assertFalse( + experimentEnrollmentManager.isEnrolled(account.getUuid(), UUID_EXPERIMENT_NAME + "-unrelated-experiment")); + + when(experimentEnrollmentConfiguration.getEnrolledUuids()).thenReturn(Set.of(ACCOUNT_UUID)); + assertTrue(experimentEnrollmentManager.isEnrolled(account.getUuid(), UUID_EXPERIMENT_NAME)); + + when(experimentEnrollmentConfiguration.getEnrolledUuids()).thenReturn(Collections.emptySet()); + when(experimentEnrollmentConfiguration.getEnrollmentPercentage()).thenReturn(0); + + assertFalse(experimentEnrollmentManager.isEnrolled(account.getUuid(), UUID_EXPERIMENT_NAME)); + + when(experimentEnrollmentConfiguration.getEnrollmentPercentage()).thenReturn(100); + assertTrue(experimentEnrollmentManager.isEnrolled(account.getUuid(), UUID_EXPERIMENT_NAME)); + } + + @ParameterizedTest + @MethodSource + void testIsEnrolled_PreRegistrationExperiment(final String e164, final String experimentName, + final Set enrolledE164s, final Set excludedE164s, final Set includedCountryCodes, + final Set excludedCountryCodes, + final int enrollmentPercentage, + final boolean expectEnrolled, final String message) { + + when(preRegistrationExperimentEnrollmentConfiguration.getEnrolledE164s()).thenReturn(enrolledE164s); + when(preRegistrationExperimentEnrollmentConfiguration.getExcludedE164s()).thenReturn(excludedE164s); + when(preRegistrationExperimentEnrollmentConfiguration.getEnrollmentPercentage()).thenReturn(enrollmentPercentage); + when(preRegistrationExperimentEnrollmentConfiguration.getIncludedCountryCodes()).thenReturn(includedCountryCodes); + when(preRegistrationExperimentEnrollmentConfiguration.getExcludedCountryCodes()).thenReturn(excludedCountryCodes); + + assertEquals(expectEnrolled, experimentEnrollmentManager.isEnrolled(e164, experimentName), message); + } + + @SuppressWarnings("unused") + static Stream testIsEnrolled_PreRegistrationExperiment() { + return Stream.of( + Arguments.of(ENROLLED_164, E164_EXPERIMENT_NAME, Collections.emptySet(), Collections.emptySet(), + Collections.emptySet(), Collections.emptySet(), 0, false, "default configuration expects no enrollment"), + Arguments.of(ENROLLED_164, E164_EXPERIMENT_NAME + "-unrelated-experiment", Collections.emptySet(), + Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), 0, false, + "unknown experiment expects no enrollment"), + Arguments.of(ENROLLED_164, E164_EXPERIMENT_NAME, Set.of(ENROLLED_164), Set.of(EXCLUDED_164), + Collections.emptySet(), Collections.emptySet(), 0, true, "explicitly enrolled E164 overrides 0% rollout"), + Arguments.of(ENROLLED_164, E164_EXPERIMENT_NAME, Set.of(ENROLLED_164), Set.of(EXCLUDED_164), + Collections.emptySet(), Set.of("1"), 0, true, "explicitly enrolled E164 overrides excluded country code"), + Arguments.of(ENROLLED_164, E164_EXPERIMENT_NAME, Collections.emptySet(), Collections.emptySet(), Set.of("1"), + Collections.emptySet(), 0, true, "included country code overrides 0% rollout"), + Arguments.of(EXCLUDED_164, E164_EXPERIMENT_NAME, Collections.emptySet(), Set.of(EXCLUDED_164), Set.of("1"), + Collections.emptySet(), 100, false, "excluded E164 overrides 100% rollout"), + Arguments.of(ENROLLED_164, E164_EXPERIMENT_NAME, Collections.emptySet(), Collections.emptySet(), + Collections.emptySet(), Set.of("1"), 100, false, "excluded country code overrides 100% rollout"), + Arguments.of(ENROLLED_164, E164_EXPERIMENT_NAME, Collections.emptySet(), Collections.emptySet(), + Collections.emptySet(), Collections.emptySet(), 100, true, "enrollment expected for 100% rollout") + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/experiment/ExperimentTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/experiment/ExperimentTest.java new file mode 100644 index 000000000..0dc6f446b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/experiment/ExperimentTest.java @@ -0,0 +1,144 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.experiment; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Timer; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class ExperimentTest { + + private Timer matchTimer; + private Timer errorTimer; + + private Experiment experiment; + + @BeforeEach + void setUp() { + matchTimer = mock(Timer.class); + errorTimer = mock(Timer.class); + + experiment = new Experiment("test", matchTimer, errorTimer, mock(Timer.class), mock(Timer.class), + mock(Timer.class)); + } + + @Test + void compareFutureResult() { + experiment.compareFutureResult(12, CompletableFuture.completedFuture(12)); + verify(matchTimer).record(anyLong(), eq(TimeUnit.NANOSECONDS)); + } + + @Test + void compareFutureResultError() { + experiment.compareFutureResult(12, CompletableFuture.failedFuture(new RuntimeException("OH NO"))); + verify(errorTimer).record(anyLong(), eq(TimeUnit.NANOSECONDS)); + } + + @Test + void compareSupplierResultMatch() { + experiment.compareSupplierResult(12, () -> 12); + verify(matchTimer).record(anyLong(), eq(TimeUnit.NANOSECONDS)); + } + + @Test + void compareSupplierResultError() { + experiment.compareSupplierResult(12, () -> { + throw new RuntimeException("OH NO"); + }); + verify(errorTimer).record(anyLong(), eq(TimeUnit.NANOSECONDS)); + } + + @Test + void compareSupplierResultAsyncMatch() throws InterruptedException { + final ExecutorService experimentExecutor = Executors.newSingleThreadExecutor(); + + experiment.compareSupplierResultAsync(12, () -> 12, experimentExecutor); + experimentExecutor.shutdown(); + experimentExecutor.awaitTermination(1, TimeUnit.SECONDS); + + verify(matchTimer).record(anyLong(), eq(TimeUnit.NANOSECONDS)); + } + + @Test + void compareSupplierResultAsyncError() throws InterruptedException { + final ExecutorService experimentExecutor = Executors.newSingleThreadExecutor(); + + experiment.compareSupplierResultAsync(12, () -> { + throw new RuntimeException("OH NO"); + }, experimentExecutor); + experimentExecutor.shutdown(); + experimentExecutor.awaitTermination(1, TimeUnit.SECONDS); + + verify(errorTimer).record(anyLong(), eq(TimeUnit.NANOSECONDS)); + } + + @Test + void compareSupplierResultAsyncRejection() { + final ExecutorService executorService = mock(ExecutorService.class); + doThrow(new RejectedExecutionException()).when(executorService).execute(any(Runnable.class)); + + experiment.compareSupplierResultAsync(12, () -> 12, executorService); + verify(errorTimer).record(anyLong(), eq(TimeUnit.NANOSECONDS)); + } + + @ParameterizedTest + @MethodSource + public void testRecordResult(final Object expected, final Object actual, final Experiment experiment, final Timer expectedTimer) { + reset(expectedTimer); + + final MockClock clock = new MockClock(); + final Timer.Sample sample = Timer.start(clock); + + final long durationNanos = 123; + clock.add(durationNanos, TimeUnit.NANOSECONDS); + + experiment.recordResult(expected, actual, sample); + verify(expectedTimer).record(durationNanos, TimeUnit.NANOSECONDS); + } + + @SuppressWarnings("unused") + private static Stream testRecordResult() { + // Hack: parameters are set before the @Before method gets called + final Timer matchTimer = mock(Timer.class); + final Timer errorTimer = mock(Timer.class); + final Timer bothPresentMismatchTimer = mock(Timer.class); + final Timer controlNullMismatchTimer = mock(Timer.class); + final Timer experimentNullMismatchTimer = mock(Timer.class); + + final Experiment experiment = new Experiment("test", matchTimer, errorTimer, bothPresentMismatchTimer, + controlNullMismatchTimer, experimentNullMismatchTimer); + + return Stream.of( + Arguments.of(12, 12, experiment, matchTimer), + Arguments.of(null, 12, experiment, controlNullMismatchTimer), + Arguments.of(12, null, experiment, experimentNullMismatchTimer), + Arguments.of(12, 17, experiment, bothPresentMismatchTimer), + Arguments.of(Optional.of(12), Optional.of(12), experiment, matchTimer), + Arguments.of(Optional.empty(), Optional.of(12), experiment, controlNullMismatchTimer), + Arguments.of(Optional.of(12), Optional.empty(), experiment, experimentNullMismatchTimer), + Arguments.of(Optional.of(12), Optional.of(17), experiment, bothPresentMismatchTimer) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/filters/RemoteDeprecationFilterTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/filters/RemoteDeprecationFilterTest.java new file mode 100644 index 000000000..48572f294 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/filters/RemoteDeprecationFilterTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.filters; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.net.HttpHeaders; +import com.google.protobuf.ByteString; +import com.vdurmont.semver4j.Semver; +import java.io.IOException; +import java.util.EnumMap; +import java.util.Set; +import java.util.stream.Stream; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.signal.chat.rpc.EchoServiceGrpc; +import org.signal.chat.rpc.EchoRequest; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRemoteDeprecationConfiguration; +import org.whispersystems.textsecuregcm.grpc.EchoServiceImpl; +import org.whispersystems.textsecuregcm.grpc.StatusConstants; +import org.whispersystems.textsecuregcm.grpc.UserAgentInterceptor; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; + +import io.grpc.Metadata; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.StatusRuntimeException; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.stub.MetadataUtils; + +class RemoteDeprecationFilterTest { + + @Test + void testEmptyMap() throws IOException, ServletException { + // We're happy as long as there's no exception + final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); + final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); + final DynamicRemoteDeprecationConfiguration emptyConfiguration = new DynamicRemoteDeprecationConfiguration(); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + when(dynamicConfiguration.getRemoteDeprecationConfiguration()).thenReturn(emptyConfiguration); + + final RemoteDeprecationFilter filter = new RemoteDeprecationFilter(dynamicConfigurationManager); + + final HttpServletRequest servletRequest = mock(HttpServletRequest.class); + final HttpServletResponse servletResponse = mock(HttpServletResponse.class); + final FilterChain filterChain = mock(FilterChain.class); + + when(servletRequest.getHeader("UserAgent")).thenReturn("Signal-Android/4.68.3"); + + filter.doFilter(servletRequest, servletResponse, filterChain); + + verify(filterChain).doFilter(servletRequest, servletResponse); + verify(servletResponse, never()).sendError(anyInt()); + } + + private RemoteDeprecationFilter filterConfiguredForTest() { + final EnumMap minimumVersionsByPlatform = new EnumMap<>(ClientPlatform.class); + minimumVersionsByPlatform.put(ClientPlatform.ANDROID, new Semver("1.0.0")); + minimumVersionsByPlatform.put(ClientPlatform.IOS, new Semver("1.0.0")); + minimumVersionsByPlatform.put(ClientPlatform.DESKTOP, new Semver("1.0.0")); + + final EnumMap versionsPendingDeprecationByPlatform = new EnumMap<>(ClientPlatform.class); + minimumVersionsByPlatform.put(ClientPlatform.ANDROID, new Semver("1.1.0")); + minimumVersionsByPlatform.put(ClientPlatform.IOS, new Semver("1.1.0")); + minimumVersionsByPlatform.put(ClientPlatform.DESKTOP, new Semver("1.1.0")); + + final EnumMap> blockedVersionsByPlatform = new EnumMap<>(ClientPlatform.class); + blockedVersionsByPlatform.put(ClientPlatform.DESKTOP, Set.of(new Semver("8.0.0-beta.2"))); + + final EnumMap> versionsPendingBlockByPlatform = new EnumMap<>(ClientPlatform.class); + versionsPendingBlockByPlatform.put(ClientPlatform.DESKTOP, Set.of(new Semver("8.0.0-beta.3"))); + + final DynamicRemoteDeprecationConfiguration remoteDeprecationConfiguration = new DynamicRemoteDeprecationConfiguration(); + remoteDeprecationConfiguration.setMinimumVersions(minimumVersionsByPlatform); + remoteDeprecationConfiguration.setVersionsPendingDeprecation(versionsPendingDeprecationByPlatform); + remoteDeprecationConfiguration.setBlockedVersions(blockedVersionsByPlatform); + remoteDeprecationConfiguration.setVersionsPendingBlock(versionsPendingBlockByPlatform); + remoteDeprecationConfiguration.setUnrecognizedUserAgentAllowed(true); + + final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); + final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + when(dynamicConfiguration.getRemoteDeprecationConfiguration()).thenReturn(remoteDeprecationConfiguration); + + return new RemoteDeprecationFilter(dynamicConfigurationManager); + } + + @ParameterizedTest + @MethodSource + void testFilter(final String userAgent, final boolean expectDeprecation) throws IOException, ServletException { + final HttpServletRequest servletRequest = mock(HttpServletRequest.class); + final HttpServletResponse servletResponse = mock(HttpServletResponse.class); + final FilterChain filterChain = mock(FilterChain.class); + + when(servletRequest.getHeader(HttpHeaders.USER_AGENT)).thenReturn(userAgent); + + final RemoteDeprecationFilter filter = filterConfiguredForTest(); + filter.doFilter(servletRequest, servletResponse, filterChain); + + if (expectDeprecation) { + verify(filterChain, never()).doFilter(any(), any()); + verify(servletResponse).sendError(499); + } else { + verify(filterChain).doFilter(servletRequest, servletResponse); + verify(servletResponse, never()).sendError(anyInt()); + } + } + + @ParameterizedTest + @MethodSource(value="testFilter") + void testGrpcFilter(final String userAgent, final boolean expectDeprecation) throws Exception { + final Server testServer = InProcessServerBuilder.forName("RemoteDeprecationFilterTest") + .directExecutor() + .addService(new EchoServiceImpl()) + .intercept(filterConfiguredForTest()) + .intercept(new UserAgentInterceptor()) + .build() + .start(); + final ManagedChannel channel = InProcessChannelBuilder.forName("RemoteDeprecationFilterTest") + .directExecutor() + .userAgent(userAgent) + .build(); + + try { + final EchoServiceGrpc.EchoServiceBlockingStub client = EchoServiceGrpc.newBlockingStub(channel); + + final EchoRequest req = EchoRequest.newBuilder().setPayload(ByteString.copyFromUtf8("cluck cluck, i'm a parrot")).build(); + if (expectDeprecation) { + final StatusRuntimeException e = assertThrows( + StatusRuntimeException.class, + () -> client.echo(req)); + assertEquals(StatusConstants.UPGRADE_NEEDED_STATUS.toString(), e.getStatus().toString()); + } else { + assertEquals("cluck cluck, i'm a parrot", client.echo(req).getPayload().toStringUtf8()); + } + } finally { + testServer.shutdownNow(); + testServer.awaitTermination(); + } + } + + private static Stream testFilter() { + return Stream.of( + Arguments.of("Unrecognized UA", false), + Arguments.of("Signal-Android/4.68.3", false), + Arguments.of("Signal-iOS/3.9.0", false), + Arguments.of("Signal-Desktop/1.2.3", false), + Arguments.of("Signal-Android/0.68.3", true), + Arguments.of("Signal-iOS/0.9.0", true), + Arguments.of("Signal-Desktop/0.2.3", true), + Arguments.of("Signal-Desktop/8.0.0-beta.2", true), + Arguments.of("Signal-Desktop/8.0.0-beta.1", false), + Arguments.of("Signal-iOS/8.0.0-beta.2", false)); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/filters/RequestStatisticsFilterTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/filters/RequestStatisticsFilterTest.java new file mode 100644 index 000000000..32038cc8d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/filters/RequestStatisticsFilterTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.filters; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import javax.ws.rs.container.ContainerRequestContext; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.metrics.TrafficSource; + +class RequestStatisticsFilterTest { + + @Test + void testFilter() throws Exception { + + final RequestStatisticsFilter requestStatisticsFilter = new RequestStatisticsFilter(TrafficSource.WEBSOCKET); + + final ContainerRequestContext requestContext = mock(ContainerRequestContext.class); + + when(requestContext.getLength()).thenReturn(-1); + when(requestContext.getLength()).thenReturn(Integer.MAX_VALUE); + when(requestContext.getLength()).thenThrow(RuntimeException.class); + + requestStatisticsFilter.filter(requestContext); + requestStatisticsFilter.filter(requestContext); + requestStatisticsFilter.filter(requestContext); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/filters/TimestampResponseFilterTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/filters/TimestampResponseFilterTest.java new file mode 100644 index 000000000..100931eac --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/filters/TimestampResponseFilterTest.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.filters; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.core.MultivaluedMap; +import org.glassfish.jersey.message.internal.HeaderUtils; +import org.junit.jupiter.api.Test; + +class TimestampResponseFilterTest { + + @Test + void testFilter() { + final ContainerRequestContext requestContext = mock(ContainerRequestContext.class); + final ContainerResponseContext responseContext = mock(ContainerResponseContext.class); + + final MultivaluedMap headers = HeaderUtils.createOutbound(); + + when(responseContext.getHeaders()).thenReturn(headers); + + new TimestampResponseFilter().filter(requestContext, responseContext); + + assertTrue(headers.containsKey(org.whispersystems.textsecuregcm.util.HeaderUtils.TIMESTAMP_HEADER)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/AcceptLanguageInterceptorTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/AcceptLanguageInterceptorTest.java new file mode 100644 index 000000000..c56ee22f7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/AcceptLanguageInterceptorTest.java @@ -0,0 +1,79 @@ +package org.whispersystems.textsecuregcm.grpc; + +import com.google.protobuf.ByteString; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.MetadataUtils; +import io.grpc.stub.StreamObserver; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.signal.chat.rpc.EchoRequest; +import org.signal.chat.rpc.EchoResponse; +import org.signal.chat.rpc.EchoServiceGrpc; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class AcceptLanguageInterceptorTest { + @ParameterizedTest + @MethodSource + void parseLocale(final String header, final List expectedLocales) throws IOException, InterruptedException { + final AtomicReference> observedLocales = new AtomicReference<>(null); + final EchoServiceImpl serviceImpl = new EchoServiceImpl() { + @Override + public void echo(EchoRequest req, StreamObserver responseObserver) { + observedLocales.set(AcceptLanguageUtil.localeFromGrpcContext()); + super.echo(req, responseObserver); + } + }; + + final Server testServer = InProcessServerBuilder.forName("AcceptLanguageTest") + .directExecutor() + .addService(serviceImpl) + .intercept(new AcceptLanguageInterceptor()) + .intercept(new UserAgentInterceptor()) + .build() + .start(); + + try { + final ManagedChannel channel = InProcessChannelBuilder.forName("AcceptLanguageTest") + .directExecutor() + .userAgent("Signal-Android/1.2.3") + .build(); + + final Metadata metadata = new Metadata(); + metadata.put(AcceptLanguageInterceptor.ACCEPTABLE_LANGUAGES_GRPC_HEADER, header); + + final EchoServiceGrpc.EchoServiceBlockingStub client = EchoServiceGrpc.newBlockingStub(channel) + .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata)); + + final EchoRequest request = EchoRequest.newBuilder().setPayload(ByteString.copyFromUtf8("test request")).build(); + client.echo(request); + assertEquals(expectedLocales, observedLocales.get()); + } finally { + testServer.shutdownNow(); + testServer.awaitTermination(); + } + } + + private static Stream parseLocale() { + return Stream.of( + // en-US-POSIX is a special locale that exists alongside en-US. It matches because of the definition of + // basic filtering in RFC 4647 (https://datatracker.ietf.org/doc/html/rfc4647#section-3.3.1) + Arguments.of("en-US,fr-CA", List.of(Locale.forLanguageTag("en-US-POSIX"), Locale.forLanguageTag("en-US"), Locale.forLanguageTag("fr-CA"))), + Arguments.of("en-US; q=0.9, fr-CA", List.of(Locale.forLanguageTag("fr-CA"), Locale.forLanguageTag("en-US-POSIX"), Locale.forLanguageTag("en-US"))), + Arguments.of("invalid-locale,fr-CA", List.of(Locale.forLanguageTag("fr-CA"))), + Arguments.of("", Collections.emptyList()), + Arguments.of("acompletely,unexpectedfor , mat", Collections.emptyList()) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/CallingGrpcServiceTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/CallingGrpcServiceTest.java new file mode 100644 index 000000000..3f75c7eb9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/CallingGrpcServiceTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertRateLimitExceeded; + +import java.time.Duration; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.signal.chat.calling.CallingGrpc; +import org.signal.chat.calling.GetTurnCredentialsRequest; +import org.signal.chat.calling.GetTurnCredentialsResponse; +import org.whispersystems.textsecuregcm.auth.TurnToken; +import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.util.MockUtils; + +class CallingGrpcServiceTest extends SimpleBaseGrpcTest { + + @Mock + private TurnTokenGenerator turnTokenGenerator; + + @Mock + private RateLimiter turnCredentialRateLimiter; + + + @Override + protected CallingGrpcService createServiceBeforeEachTest() { + final RateLimiters rateLimiters = mock(RateLimiters.class); + when(rateLimiters.getTurnLimiter()).thenReturn(turnCredentialRateLimiter); + return new CallingGrpcService(turnTokenGenerator, rateLimiters); + } + + @Test + void getTurnCredentials() { + final String username = "test-username"; + final String password = "test-password"; + final List urls = List.of("first", "second"); + + MockUtils.updateRateLimiterResponseToAllow(turnCredentialRateLimiter, AUTHENTICATED_ACI); + when(turnTokenGenerator.generate(any())).thenReturn(new TurnToken(username, password, urls)); + + final GetTurnCredentialsResponse response = authenticatedServiceStub().getTurnCredentials(GetTurnCredentialsRequest.newBuilder().build()); + + final GetTurnCredentialsResponse expectedResponse = GetTurnCredentialsResponse.newBuilder() + .setUsername(username) + .setPassword(password) + .addAllUrls(urls) + .build(); + + assertEquals(expectedResponse, response); + } + + @Test + void getTurnCredentialsRateLimited() { + final Duration retryAfter = MockUtils.updateRateLimiterResponseToFail( + turnCredentialRateLimiter, AUTHENTICATED_ACI, Duration.ofMinutes(19), false); + assertRateLimitExceeded(retryAfter, () -> authenticatedServiceStub().getTurnCredentials(GetTurnCredentialsRequest.newBuilder().build())); + verify(turnTokenGenerator, never()).generate(any()); + verifyNoInteractions(turnTokenGenerator); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/DevicesGrpcServiceTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/DevicesGrpcServiceTest.java new file mode 100644 index 000000000..b5a5e673c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/DevicesGrpcServiceTest.java @@ -0,0 +1,418 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException; + +import com.google.protobuf.ByteString; +import io.grpc.Status; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.junitpioneer.jupiter.cartesian.CartesianTest; +import org.mockito.Mock; +import org.signal.chat.device.ClearPushTokenRequest; +import org.signal.chat.device.ClearPushTokenResponse; +import org.signal.chat.device.DevicesGrpc; +import org.signal.chat.device.GetDevicesRequest; +import org.signal.chat.device.GetDevicesResponse; +import org.signal.chat.device.RemoveDeviceRequest; +import org.signal.chat.device.RemoveDeviceResponse; +import org.signal.chat.device.SetCapabilitiesRequest; +import org.signal.chat.device.SetCapabilitiesResponse; +import org.signal.chat.device.SetDeviceNameRequest; +import org.signal.chat.device.SetDeviceNameResponse; +import org.signal.chat.device.SetPushTokenRequest; +import org.signal.chat.device.SetPushTokenResponse; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import org.whispersystems.textsecuregcm.storage.MessagesManager; + +class DevicesGrpcServiceTest extends SimpleBaseGrpcTest { + + @Mock + private AccountsManager accountsManager; + + @Mock + private KeysManager keysManager; + + @Mock + private MessagesManager messagesManager; + + @Mock + private Account authenticatedAccount; + + + @Override + protected DevicesGrpcService createServiceBeforeEachTest() { + when(authenticatedAccount.getUuid()).thenReturn(AUTHENTICATED_ACI); + + when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(authenticatedAccount))); + + when(accountsManager.updateAsync(any(), any())) + .thenAnswer(invocation -> { + final Account account = invocation.getArgument(0); + final Consumer updater = invocation.getArgument(1); + + updater.accept(account); + + return CompletableFuture.completedFuture(account); + }); + + when(accountsManager.updateDeviceAsync(any(), anyLong(), any())) + .thenAnswer(invocation -> { + final Account account = invocation.getArgument(0); + final Device device = account.getDevice(invocation.getArgument(1)).orElseThrow(); + final Consumer updater = invocation.getArgument(2); + + updater.accept(device); + + return CompletableFuture.completedFuture(account); + }); + + when(keysManager.delete(any(), anyLong())).thenReturn(CompletableFuture.completedFuture(null)); + when(messagesManager.clear(any(), anyLong())).thenReturn(CompletableFuture.completedFuture(null)); + + return new DevicesGrpcService(accountsManager, keysManager, messagesManager); + } + + @Test + void getDevices() { + final Instant primaryDeviceCreated = Instant.now().minus(Duration.ofDays(7)).truncatedTo(ChronoUnit.MILLIS); + final Instant primaryDeviceLastSeen = primaryDeviceCreated.plus(Duration.ofHours(6)); + final Instant linkedDeviceCreated = Instant.now().minus(Duration.ofDays(1)).truncatedTo(ChronoUnit.MILLIS); + final Instant linkedDeviceLastSeen = linkedDeviceCreated.plus(Duration.ofHours(7)); + + final Device primaryDevice = mock(Device.class); + when(primaryDevice.getId()).thenReturn(Device.MASTER_ID); + when(primaryDevice.getCreated()).thenReturn(primaryDeviceCreated.toEpochMilli()); + when(primaryDevice.getLastSeen()).thenReturn(primaryDeviceLastSeen.toEpochMilli()); + + final String linkedDeviceName = "A linked device"; + + final Device linkedDevice = mock(Device.class); + when(linkedDevice.getId()).thenReturn(Device.MASTER_ID + 1); + when(linkedDevice.getCreated()).thenReturn(linkedDeviceCreated.toEpochMilli()); + when(linkedDevice.getLastSeen()).thenReturn(linkedDeviceLastSeen.toEpochMilli()); + when(linkedDevice.getName()) + .thenReturn(Base64.getEncoder().encodeToString(linkedDeviceName.getBytes(StandardCharsets.UTF_8))); + + when(authenticatedAccount.getDevices()).thenReturn(List.of(primaryDevice, linkedDevice)); + + final GetDevicesResponse expectedResponse = GetDevicesResponse.newBuilder() + .addDevices(GetDevicesResponse.LinkedDevice.newBuilder() + .setId(Device.MASTER_ID) + .setCreated(primaryDeviceCreated.toEpochMilli()) + .setLastSeen(primaryDeviceLastSeen.toEpochMilli()) + .build()) + .addDevices(GetDevicesResponse.LinkedDevice.newBuilder() + .setId(Device.MASTER_ID + 1) + .setCreated(linkedDeviceCreated.toEpochMilli()) + .setLastSeen(linkedDeviceLastSeen.toEpochMilli()) + .setName(ByteString.copyFrom(linkedDeviceName.getBytes(StandardCharsets.UTF_8))) + .build()) + .build(); + + assertEquals(expectedResponse, authenticatedServiceStub().getDevices(GetDevicesRequest.newBuilder().build())); + } + + @Test + void removeDevice() { + final long deviceId = 17; + + final RemoveDeviceResponse ignored = authenticatedServiceStub().removeDevice(RemoveDeviceRequest.newBuilder() + .setId(deviceId) + .build()); + + verify(messagesManager, times(2)).clear(AUTHENTICATED_ACI, deviceId); + verify(keysManager).delete(AUTHENTICATED_ACI, deviceId); + verify(authenticatedAccount).removeDevice(deviceId); + } + + @Test + void removeDevicePrimary() { + assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().removeDevice(RemoveDeviceRequest.newBuilder() + .setId(1) + .build())); + } + + @Test + void removeDeviceNonPrimaryAuthenticated() { + mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, Device.MASTER_ID + 1); + assertStatusException(Status.PERMISSION_DENIED, () -> authenticatedServiceStub().removeDevice(RemoveDeviceRequest.newBuilder() + .setId(17) + .build())); + } + + @ParameterizedTest + @ValueSource(longs = {Device.MASTER_ID, Device.MASTER_ID + 1}) + void setDeviceName(final long deviceId) { + mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, deviceId); + + final Device device = mock(Device.class); + when(authenticatedAccount.getDevice(deviceId)).thenReturn(Optional.of(device)); + + final byte[] deviceName = new byte[128]; + ThreadLocalRandom.current().nextBytes(deviceName); + + final SetDeviceNameResponse ignored = authenticatedServiceStub().setDeviceName(SetDeviceNameRequest.newBuilder() + .setName(ByteString.copyFrom(deviceName)) + .build()); + + verify(device).setName(Base64.getEncoder().encodeToString(deviceName)); + } + + @ParameterizedTest + @MethodSource + void setDeviceNameIllegalArgument(final SetDeviceNameRequest request) { + when(authenticatedAccount.getDevice(AUTHENTICATED_DEVICE_ID)).thenReturn(Optional.of(mock(Device.class))); + assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().setDeviceName(request)); + } + + private static Stream setDeviceNameIllegalArgument() { + return Stream.of( + // No device name + Arguments.of(SetDeviceNameRequest.newBuilder().build()), + + // Excessively-long device name + Arguments.of(SetDeviceNameRequest.newBuilder() + .setName(ByteString.copyFrom(RandomStringUtils.randomAlphanumeric(1024).getBytes(StandardCharsets.UTF_8))) + .build()) + ); + } + + @ParameterizedTest + @MethodSource + void setPushToken(final long deviceId, + final SetPushTokenRequest request, + @Nullable final String expectedApnsToken, + @Nullable final String expectedApnsVoipToken, + @Nullable final String expectedFcmToken) { + + mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, deviceId); + + final Device device = mock(Device.class); + when(authenticatedAccount.getDevice(deviceId)).thenReturn(Optional.of(device)); + + final SetPushTokenResponse ignored = authenticatedServiceStub().setPushToken(request); + + verify(device).setApnId(expectedApnsToken); + verify(device).setVoipApnId(expectedApnsVoipToken); + verify(device).setGcmId(expectedFcmToken); + verify(device).setFetchesMessages(false); + } + + private static Stream setPushToken() { + final String apnsToken = "apns-token"; + final String apnsVoipToken = "apns-voip-token"; + final String fcmToken = "fcm-token"; + + final Stream.Builder streamBuilder = Stream.builder(); + + for (final long deviceId : new long[] { Device.MASTER_ID, Device.MASTER_ID + 1 }) { + streamBuilder.add(Arguments.of(deviceId, + SetPushTokenRequest.newBuilder() + .setApnsTokenRequest(SetPushTokenRequest.ApnsTokenRequest.newBuilder() + .setApnsToken(apnsToken) + .setApnsVoipToken(apnsVoipToken) + .build()) + .build(), + apnsToken, apnsVoipToken, null)); + + streamBuilder.add(Arguments.of(deviceId, + SetPushTokenRequest.newBuilder() + .setApnsTokenRequest(SetPushTokenRequest.ApnsTokenRequest.newBuilder() + .setApnsToken(apnsToken) + .build()) + .build(), + apnsToken, null, null)); + + streamBuilder.add(Arguments.of(deviceId, + SetPushTokenRequest.newBuilder() + .setFcmTokenRequest(SetPushTokenRequest.FcmTokenRequest.newBuilder() + .setFcmToken(fcmToken) + .build()) + .build(), + null, null, fcmToken)); + } + + return streamBuilder.build(); + } + + @ParameterizedTest + @MethodSource + void setPushTokenUnchanged(final SetPushTokenRequest request, + @Nullable final String apnsToken, + @Nullable final String apnsVoipToken, + @Nullable final String fcmToken) { + + final Device device = mock(Device.class); + when(device.getApnId()).thenReturn(apnsToken); + when(device.getVoipApnId()).thenReturn(apnsVoipToken); + when(device.getGcmId()).thenReturn(fcmToken); + + when(authenticatedAccount.getDevice(AUTHENTICATED_DEVICE_ID)).thenReturn(Optional.of(device)); + + final SetPushTokenResponse ignored = authenticatedServiceStub().setPushToken(request); + + verify(accountsManager, never()).updateDevice(any(), anyLong(), any()); + } + + private static Stream setPushTokenUnchanged() { + final String apnsToken = "apns-token"; + final String apnsVoipToken = "apns-voip-token"; + final String fcmToken = "fcm-token"; + + return Stream.of( + Arguments.of(SetPushTokenRequest.newBuilder() + .setApnsTokenRequest(SetPushTokenRequest.ApnsTokenRequest.newBuilder() + .setApnsToken(apnsToken) + .setApnsVoipToken(apnsVoipToken) + .build()) + .build(), + apnsToken, apnsVoipToken, null, false), + + Arguments.of(SetPushTokenRequest.newBuilder() + .setApnsTokenRequest(SetPushTokenRequest.ApnsTokenRequest.newBuilder() + .setApnsToken(apnsToken) + .build()) + .build(), + apnsToken, null, null, false), + + Arguments.of(SetPushTokenRequest.newBuilder() + .setFcmTokenRequest(SetPushTokenRequest.FcmTokenRequest.newBuilder() + .setFcmToken(fcmToken) + .build()) + .build(), + null, null, fcmToken, false) + ); + } + + @ParameterizedTest + @MethodSource + void setPushTokenIllegalArgument(final SetPushTokenRequest request) { + final Device device = mock(Device.class); + when(authenticatedAccount.getDevice(AUTHENTICATED_DEVICE_ID)).thenReturn(Optional.of(device)); + assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().setPushToken(request)); + verify(accountsManager, never()).updateDevice(any(), anyLong(), any()); + } + + private static Stream setPushTokenIllegalArgument() { + return Stream.of( + Arguments.of(SetPushTokenRequest.newBuilder().build()), + + Arguments.of(SetPushTokenRequest.newBuilder() + .setApnsTokenRequest(SetPushTokenRequest.ApnsTokenRequest.newBuilder().build()) + .build()), + + Arguments.of(SetPushTokenRequest.newBuilder() + .setFcmTokenRequest(SetPushTokenRequest.FcmTokenRequest.newBuilder().build()) + .build()) + ); + } + + @ParameterizedTest + @MethodSource + void clearPushToken(final long deviceId, + @Nullable final String apnsToken, + @Nullable final String apnsVoipToken, + @Nullable final String fcmToken, + @Nullable final String expectedUserAgent) { + + mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, deviceId); + + final Device device = mock(Device.class); + when(device.getId()).thenReturn(deviceId); + when(device.isMaster()).thenReturn(deviceId == Device.MASTER_ID); + when(device.getApnId()).thenReturn(apnsToken); + when(device.getVoipApnId()).thenReturn(apnsVoipToken); + when(device.getGcmId()).thenReturn(fcmToken); + when(authenticatedAccount.getDevice(deviceId)).thenReturn(Optional.of(device)); + + final ClearPushTokenResponse ignored = authenticatedServiceStub().clearPushToken(ClearPushTokenRequest.newBuilder().build()); + + verify(device).setApnId(null); + verify(device).setVoipApnId(null); + verify(device).setGcmId(null); + verify(device).setFetchesMessages(true); + + if (expectedUserAgent != null) { + verify(device).setUserAgent(expectedUserAgent); + } else { + verify(device, never()).setUserAgent(any()); + } + } + + private static Stream clearPushToken() { + return Stream.of( + Arguments.of(Device.MASTER_ID, "apns-token", null, null, "OWI"), + Arguments.of(Device.MASTER_ID, "apns-token", "apns-voip-token", null, "OWI"), + Arguments.of(Device.MASTER_ID, null, "apns-voip-token", null, "OWI"), + Arguments.of(Device.MASTER_ID, null, null, "fcm-token", "OWA"), + Arguments.of(Device.MASTER_ID, null, null, null, null), + Arguments.of(Device.MASTER_ID + 1, "apns-token", null, null, "OWP"), + Arguments.of(Device.MASTER_ID + 1, "apns-token", "apns-voip-token", null, "OWP"), + Arguments.of(Device.MASTER_ID + 1, null, "apns-voip-token", null, "OWP"), + Arguments.of(Device.MASTER_ID + 1, null, null, "fcm-token", "OWA"), + Arguments.of(Device.MASTER_ID + 1, null, null, null, null) + ); + } + + @CartesianTest + void setCapabilities( + @CartesianTest.Values(longs = {Device.MASTER_ID, Device.MASTER_ID + 1}) final long deviceId, + @CartesianTest.Values(booleans = {true, false}) final boolean storage, + @CartesianTest.Values(booleans = {true, false}) final boolean transfer, + @CartesianTest.Values(booleans = {true, false}) final boolean pni, + @CartesianTest.Values(booleans = {true, false}) final boolean paymentActivation) { + + mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, deviceId); + + final Device device = mock(Device.class); + when(authenticatedAccount.getDevice(deviceId)).thenReturn(Optional.of(device)); + + final SetCapabilitiesResponse ignored = authenticatedServiceStub().setCapabilities(SetCapabilitiesRequest.newBuilder() + .setStorage(storage) + .setTransfer(transfer) + .setPni(pni) + .setPaymentActivation(paymentActivation) + .build()); + + final Device.DeviceCapabilities expectedCapabilities = new Device.DeviceCapabilities( + storage, + transfer, + pni, + paymentActivation); + + verify(device).setCapabilities(expectedCapabilities); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/EchoServiceImpl.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/EchoServiceImpl.java new file mode 100644 index 000000000..d1dd1974d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/EchoServiceImpl.java @@ -0,0 +1,19 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.stub.StreamObserver; +import org.signal.chat.rpc.EchoServiceGrpc; +import org.signal.chat.rpc.EchoRequest; +import org.signal.chat.rpc.EchoResponse; + +public class EchoServiceImpl extends EchoServiceGrpc.EchoServiceImplBase { + @Override + public void echo(EchoRequest req, StreamObserver responseObserver) { + responseObserver.onNext(EchoResponse.newBuilder().setPayload(req.getPayload()).build()); + responseObserver.onCompleted(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceCredentialsAnonymousGrpcServiceTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceCredentialsAnonymousGrpcServiceTest.java new file mode 100644 index 000000000..c4b253aca --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceCredentialsAnonymousGrpcServiceTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.apache.commons.lang3.RandomUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.signal.chat.credentials.AuthCheckResult; +import org.signal.chat.credentials.CheckSvrCredentialsRequest; +import org.signal.chat.credentials.CheckSvrCredentialsResponse; +import org.signal.chat.credentials.ExternalServiceCredentialsAnonymousGrpc; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.util.MockUtils; +import org.whispersystems.textsecuregcm.util.MutableClock; + +class ExternalServiceCredentialsAnonymousGrpcServiceTest extends + SimpleBaseGrpcTest { + + private static final UUID USER_UUID = UUID.randomUUID(); + + private static final String USER_E164 = PhoneNumberUtil.getInstance().format( + PhoneNumberUtil.getInstance().getExampleNumber("US"), + PhoneNumberUtil.PhoneNumberFormat.E164 + ); + + private static final MutableClock CLOCK = MockUtils.mutableClock(0); + + private static final ExternalServiceCredentialsGenerator SVR_CREDENTIALS_GENERATOR = Mockito.spy(ExternalServiceCredentialsGenerator + .builder(RandomUtils.nextBytes(32)) + .withUserDerivationKey(RandomUtils.nextBytes(32)) + .prependUsername(false) + .withDerivedUsernameTruncateLength(16) + .withClock(CLOCK) + .build()); + + @Mock + private AccountsManager accountsManager; + + @Override + protected ExternalServiceCredentialsAnonymousGrpcService createServiceBeforeEachTest() { + return new ExternalServiceCredentialsAnonymousGrpcService(accountsManager, SVR_CREDENTIALS_GENERATOR); + } + + @BeforeEach + public void setup() { + Mockito.when(accountsManager.getByE164(USER_E164)).thenReturn(Optional.of(account(USER_UUID))); + } + + @Test + public void testOneMatch() throws Exception { + final UUID user2 = UUID.randomUUID(); + final UUID user3 = UUID.randomUUID(); + assertExpectedCredentialCheckResponse(Map.of( + token(USER_UUID, day(1)), AuthCheckResult.AUTH_CHECK_RESULT_MATCH, + token(user2, day(1)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH, + token(user3, day(1)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH + ), day(2)); + } + + @Test + public void testNoMatch() throws Exception { + final UUID user2 = UUID.randomUUID(); + final UUID user3 = UUID.randomUUID(); + assertExpectedCredentialCheckResponse(Map.of( + token(user2, day(1)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH, + token(user3, day(1)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH + ), day(2)); + } + + @Test + public void testSomeInvalid() throws Exception { + final UUID user2 = UUID.randomUUID(); + final UUID user3 = UUID.randomUUID(); + final ExternalServiceCredentials user1Cred = credentials(USER_UUID, day(1)); + final ExternalServiceCredentials user2Cred = credentials(user2, day(1)); + final ExternalServiceCredentials user3Cred = credentials(user3, day(1)); + + final String fakeToken = token(new ExternalServiceCredentials(user2Cred.username(), user3Cred.password())); + assertExpectedCredentialCheckResponse(Map.of( + token(user1Cred), AuthCheckResult.AUTH_CHECK_RESULT_MATCH, + token(user2Cred), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH, + fakeToken, AuthCheckResult.AUTH_CHECK_RESULT_INVALID + ), day(2)); + } + + @Test + public void testSomeExpired() throws Exception { + final UUID user2 = UUID.randomUUID(); + final UUID user3 = UUID.randomUUID(); + assertExpectedCredentialCheckResponse(Map.of( + token(USER_UUID, day(100)), AuthCheckResult.AUTH_CHECK_RESULT_MATCH, + token(user2, day(100)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH, + token(user3, day(10)), AuthCheckResult.AUTH_CHECK_RESULT_INVALID, + token(user3, day(20)), AuthCheckResult.AUTH_CHECK_RESULT_INVALID + ), day(110)); + } + + @Test + public void testSomeHaveNewerVersions() throws Exception { + final UUID user2 = UUID.randomUUID(); + final UUID user3 = UUID.randomUUID(); + assertExpectedCredentialCheckResponse(Map.of( + token(USER_UUID, day(10)), AuthCheckResult.AUTH_CHECK_RESULT_INVALID, + token(USER_UUID, day(20)), AuthCheckResult.AUTH_CHECK_RESULT_MATCH, + token(user2, day(10)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH, + token(user3, day(20)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH, + token(user3, day(10)), AuthCheckResult.AUTH_CHECK_RESULT_INVALID + ), day(25)); + } + + private void assertExpectedCredentialCheckResponse( + final Map expected, + final long nowMillis) throws Exception { + CLOCK.setTimeMillis(nowMillis); + final CheckSvrCredentialsRequest request = CheckSvrCredentialsRequest.newBuilder() + .setNumber(USER_E164) + .addAllPasswords(expected.keySet()) + .build(); + final CheckSvrCredentialsResponse response = unauthenticatedServiceStub().checkSvrCredentials(request); + final Map matchesMap = response.getMatchesMap(); + assertEquals(expected, matchesMap); + } + + private static String token(final UUID uuid, final long timeMillis) { + return token(credentials(uuid, timeMillis)); + } + + private static String token(final ExternalServiceCredentials credentials) { + return credentials.username() + ":" + credentials.password(); + } + + private static ExternalServiceCredentials credentials(final UUID uuid, final long timeMillis) { + CLOCK.setTimeMillis(timeMillis); + return SVR_CREDENTIALS_GENERATOR.generateForUuid(uuid); + } + + private static long day(final int n) { + return Duration.ofDays(n).toMillis(); + } + + private static Account account(final UUID uuid) { + final Account a = new Account(); + a.setUuid(uuid); + return a; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceCredentialsGrpcServiceTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceCredentialsGrpcServiceTest.java new file mode 100644 index 000000000..1beaabb53 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceCredentialsGrpcServiceTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertRateLimitExceeded; +import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException; +import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusUnauthenticated; + +import io.grpc.Status; +import java.time.Duration; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import org.apache.commons.lang3.RandomUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.signal.chat.credentials.ExternalServiceCredentialsGrpc; +import org.signal.chat.credentials.ExternalServiceType; +import org.signal.chat.credentials.GetExternalServiceCredentialsRequest; +import org.signal.chat.credentials.GetExternalServiceCredentialsResponse; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.util.MockUtils; +import reactor.core.publisher.Mono; + +public class ExternalServiceCredentialsGrpcServiceTest + extends SimpleBaseGrpcTest { + + private static final ExternalServiceCredentialsGenerator ART_CREDENTIALS_GENERATOR = Mockito.spy(ExternalServiceCredentialsGenerator + .builder(RandomUtils.nextBytes(32)) + .withUserDerivationKey(RandomUtils.nextBytes(32)) + .prependUsername(false) + .truncateSignature(false) + .build()); + + private static final ExternalServiceCredentialsGenerator PAYMENTS_CREDENTIALS_GENERATOR = Mockito.spy(ExternalServiceCredentialsGenerator + .builder(RandomUtils.nextBytes(32)) + .prependUsername(true) + .build()); + + @Mock + private RateLimiters rateLimiters; + + + @Override + protected ExternalServiceCredentialsGrpcService createServiceBeforeEachTest() { + return new ExternalServiceCredentialsGrpcService(Map.of( + ExternalServiceType.EXTERNAL_SERVICE_TYPE_ART, ART_CREDENTIALS_GENERATOR, + ExternalServiceType.EXTERNAL_SERVICE_TYPE_PAYMENTS, PAYMENTS_CREDENTIALS_GENERATOR + ), rateLimiters); + } + + static Stream testSuccess() { + return Stream.of( + Arguments.of(ExternalServiceType.EXTERNAL_SERVICE_TYPE_ART, ART_CREDENTIALS_GENERATOR), + Arguments.of(ExternalServiceType.EXTERNAL_SERVICE_TYPE_PAYMENTS, PAYMENTS_CREDENTIALS_GENERATOR) + ); + } + + @ParameterizedTest + @MethodSource + public void testSuccess( + final ExternalServiceType externalServiceType, + final ExternalServiceCredentialsGenerator credentialsGenerator) throws Exception { + final RateLimiter limiter = mock(RateLimiter.class); + doReturn(limiter).when(rateLimiters).forDescriptor(eq(RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS)); + doReturn(Mono.fromFuture(CompletableFuture.completedFuture(null))).when(limiter).validateReactive(eq(AUTHENTICATED_ACI)); + final GetExternalServiceCredentialsResponse artResponse = authenticatedServiceStub().getExternalServiceCredentials( + GetExternalServiceCredentialsRequest.newBuilder() + .setExternalService(externalServiceType) + .build()); + final Optional artValidation = credentialsGenerator.validateAndGetTimestamp( + new ExternalServiceCredentials(artResponse.getUsername(), artResponse.getPassword())); + assertTrue(artValidation.isPresent()); + } + + @ParameterizedTest + @ValueSource(ints = { -1, 0, 1000 }) + public void testUnrecognizedService(final int externalServiceTypeValue) throws Exception { + assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().getExternalServiceCredentials( + GetExternalServiceCredentialsRequest.newBuilder() + .setExternalServiceValue(externalServiceTypeValue) + .build())); + } + + @Test + public void testInvalidRequest() throws Exception { + assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().getExternalServiceCredentials( + GetExternalServiceCredentialsRequest.newBuilder() + .build())); + } + + @Test + public void testRateLimitExceeded() throws Exception { + final Duration retryAfter = MockUtils.updateRateLimiterResponseToFail( + rateLimiters, RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS, AUTHENTICATED_ACI, Duration.ofSeconds(100), false); + Mockito.reset(ART_CREDENTIALS_GENERATOR); + assertRateLimitExceeded( + retryAfter, + () -> authenticatedServiceStub().getExternalServiceCredentials( + GetExternalServiceCredentialsRequest.newBuilder() + .setExternalService(ExternalServiceType.EXTERNAL_SERVICE_TYPE_ART) + .build()), + ART_CREDENTIALS_GENERATOR + ); + } + + @Test + public void testUnauthenticatedCall() throws Exception { + assertStatusUnauthenticated(() -> unauthenticatedServiceStub().getExternalServiceCredentials( + GetExternalServiceCredentialsRequest.newBuilder() + .setExternalService(ExternalServiceType.EXTERNAL_SERVICE_TYPE_ART) + .build())); + } + + /** + * `ExternalServiceDefinitions` enum is supposed to have entries for all values in `ExternalServiceType`, + * except for the `EXTERNAL_SERVICE_TYPE_UNSPECIFIED` and `UNRECOGNIZED`. + * This test makes sure that is the case. + */ + @ParameterizedTest + @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = { "UNRECOGNIZED", "EXTERNAL_SERVICE_TYPE_UNSPECIFIED" }) + public void testHaveExternalServiceDefinitionForServiceTypes(final ExternalServiceType externalServiceType) throws Exception { + assertTrue( + Arrays.stream(ExternalServiceDefinitions.values()).anyMatch(v -> v.externalService() == externalServiceType), + "`ExternalServiceDefinitions` enum entry is missing for the `%s` value of `ExternalServiceType`".formatted(externalServiceType) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcServerExtension.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcServerExtension.java new file mode 100644 index 000000000..725ecaa9d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcServerExtension.java @@ -0,0 +1,119 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.BindableService; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.ServerServiceDefinition; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.util.MutableHandlerRegistry; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +// This is mostly a direct port of +// https://github.com/grpc/grpc-java/blob/master/testing/src/main/java/io/grpc/testing/GrpcServerRule.java, but for +// JUnit 5. +public class GrpcServerExtension implements BeforeEachCallback, AfterEachCallback { + + private ManagedChannel channel; + private Server server; + private String serverName; + private MutableHandlerRegistry serviceRegistry; + private boolean useDirectExecutor; + + /** + * Returns {@code this} configured to use a direct executor for the {@link ManagedChannel} and + * {@link Server}. This can only be called at the rule instantiation. + */ + public final GrpcServerExtension directExecutor() { + if (serverName != null) { + throw new IllegalStateException("directExecutor() can only be called at the rule instantiation"); + } + + useDirectExecutor = true; + return this; + } + + /** + * Returns a {@link ManagedChannel} connected to this service. + */ + public final ManagedChannel getChannel() { + return channel; + } + + /** + * Returns the underlying gRPC {@link Server} for this service. + */ + public final Server getServer() { + return server; + } + + /** + * Returns the randomly generated server name for this service. + */ + public final String getServerName() { + return serverName; + } + + /** + * Returns the service registry for this service. The registry is used to add service instances + * (e.g. {@link BindableService} or {@link ServerServiceDefinition} to the server. + */ + public final MutableHandlerRegistry getServiceRegistry() { + return serviceRegistry; + } + + @Override + public void beforeEach(final ExtensionContext extensionContext) throws Exception { + serverName = UUID.randomUUID().toString(); + serviceRegistry = new MutableHandlerRegistry(); + + final InProcessServerBuilder serverBuilder = InProcessServerBuilder.forName(serverName) + .fallbackHandlerRegistry(serviceRegistry); + + if (useDirectExecutor) { + serverBuilder.directExecutor(); + } + + server = serverBuilder.build().start(); + + final InProcessChannelBuilder channelBuilder = InProcessChannelBuilder.forName(serverName); + + if (useDirectExecutor) { + channelBuilder.directExecutor(); + } + + channel = channelBuilder.build(); + } + + @Override + public void afterEach(final ExtensionContext extensionContext) throws Exception { + serverName = null; + serviceRegistry = null; + + channel.shutdown(); + server.shutdown(); + + try { + channel.awaitTermination(1, TimeUnit.MINUTES); + server.awaitTermination(1, TimeUnit.MINUTES); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } finally { + channel.shutdownNow(); + channel = null; + + server.shutdownNow(); + server = null; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcTestUtils.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcTestUtils.java new file mode 100644 index 000000000..2432b88bb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/GrpcTestUtils.java @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.verifyNoInteractions; + +import io.grpc.BindableService; +import io.grpc.ServerInterceptors; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import java.time.Duration; +import java.util.UUID; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.function.Executable; +import org.whispersystems.textsecuregcm.auth.grpc.MockAuthenticationInterceptor; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; + +public final class GrpcTestUtils { + + private GrpcTestUtils() { + // noop + } + + public static MockAuthenticationInterceptor setupAuthenticatedExtension( + final GrpcServerExtension extension, + final UUID authenticatedAci, + final long authenticatedDeviceId, + final BindableService service) { + final MockAuthenticationInterceptor mockAuthenticationInterceptor = new MockAuthenticationInterceptor(); + mockAuthenticationInterceptor.setAuthenticatedDevice(authenticatedAci, authenticatedDeviceId); + extension.getServiceRegistry() + .addService(ServerInterceptors.intercept(service, mockAuthenticationInterceptor, new ErrorMappingInterceptor())); + return mockAuthenticationInterceptor; + } + + public static void setupUnauthenticatedExtension( + final GrpcServerExtension extension, + final BindableService service) { + extension.getServiceRegistry() + .addService(ServerInterceptors.intercept(service, new ErrorMappingInterceptor())); + } + + public static void assertStatusException(final Status expected, final Executable serviceCall) { + final StatusRuntimeException exception = Assertions.assertThrows(StatusRuntimeException.class, serviceCall); + assertEquals(expected.getCode(), exception.getStatus().getCode()); + } + + public static void assertStatusInvalidArgument(final Executable serviceCall) { + assertStatusException(Status.INVALID_ARGUMENT, serviceCall); + } + + public static void assertStatusUnauthenticated(final Executable serviceCall) { + assertStatusException(Status.UNAUTHENTICATED, serviceCall); + } + + public static void assertStatusPermissionDenied(final Executable serviceCall) { + assertStatusException(Status.PERMISSION_DENIED, serviceCall); + } + + public static void assertRateLimitExceeded( + final Duration expectedRetryAfter, + final Executable serviceCall, + final Object... mocksToCheckForNoInteraction) { + final StatusRuntimeException exception = Assertions.assertThrows(StatusRuntimeException.class, serviceCall); + assertEquals(Status.RESOURCE_EXHAUSTED, exception.getStatus()); + assertNotNull(exception.getTrailers()); + assertEquals(expectedRetryAfter, exception.getTrailers().get(RateLimitExceededException.RETRY_AFTER_DURATION_KEY)); + for (final Object mock: mocksToCheckForNoInteraction) { + verifyNoInteractions(mock); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcServiceTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcServiceTest.java new file mode 100644 index 000000000..f66dc08ff --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcServiceTest.java @@ -0,0 +1,291 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException; + +import com.google.protobuf.ByteString; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.signal.chat.common.EcPreKey; +import org.signal.chat.common.EcSignedPreKey; +import org.signal.chat.common.KemSignedPreKey; +import org.signal.chat.common.ServiceIdentifier; +import org.signal.chat.keys.CheckIdentityKeyRequest; +import org.signal.chat.keys.GetPreKeysAnonymousRequest; +import org.signal.chat.keys.GetPreKeysRequest; +import org.signal.chat.keys.GetPreKeysResponse; +import org.signal.chat.keys.KeysAnonymousGrpc; +import org.signal.chat.keys.ReactorKeysAnonymousGrpc; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.whispersystems.textsecuregcm.entities.ECPreKey; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; +import org.whispersystems.textsecuregcm.util.UUIDUtil; +import org.whispersystems.textsecuregcm.util.Util; +import reactor.core.publisher.Flux; + +class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest { + + @Mock + private AccountsManager accountsManager; + + @Mock + private KeysManager keysManager; + + @Override + protected KeysAnonymousGrpcService createServiceBeforeEachTest() { + return new KeysAnonymousGrpcService(accountsManager, keysManager); + } + + @Test + void getPreKeys() { + final Account targetAccount = mock(Account.class); + final Device targetDevice = mock(Device.class); + + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + final UUID identifier = UUID.randomUUID(); + + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + + when(targetDevice.getId()).thenReturn(Device.MASTER_ID); + when(targetDevice.isEnabled()).thenReturn(true); + when(targetAccount.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(targetDevice)); + + when(targetAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); + when(targetAccount.getIdentifier(IdentityType.ACI)).thenReturn(identifier); + when(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(identityKey); + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(identifier))) + .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); + + final ECPreKey ecPreKey = new ECPreKey(1, Curve.generateKeyPair().getPublicKey()); + final ECSignedPreKey ecSignedPreKey = KeysHelper.signedECPreKey(2, identityKeyPair); + final KEMSignedPreKey kemSignedPreKey = KeysHelper.signedKEMPreKey(3, identityKeyPair); + + when(keysManager.takeEC(identifier, Device.MASTER_ID)).thenReturn(CompletableFuture.completedFuture(Optional.of(ecPreKey))); + when(keysManager.takePQ(identifier, Device.MASTER_ID)).thenReturn(CompletableFuture.completedFuture(Optional.of(kemSignedPreKey))); + when(targetDevice.getSignedPreKey(IdentityType.ACI)).thenReturn(ecSignedPreKey); + + final GetPreKeysResponse response = unauthenticatedServiceStub().getPreKeys(GetPreKeysAnonymousRequest.newBuilder() + .setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey)) + .setRequest(GetPreKeysRequest.newBuilder() + .setTargetIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) + .setUuid(UUIDUtil.toByteString(identifier)) + .build()) + .setDeviceId(Device.MASTER_ID) + .build()) + .build()); + + final GetPreKeysResponse expectedResponse = GetPreKeysResponse.newBuilder() + .setIdentityKey(ByteString.copyFrom(identityKey.serialize())) + .putPreKeys(Device.MASTER_ID, GetPreKeysResponse.PreKeyBundle.newBuilder() + .setEcOneTimePreKey(EcPreKey.newBuilder() + .setKeyId(ecPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(ecPreKey.serializedPublicKey())) + .build()) + .setEcSignedPreKey(EcSignedPreKey.newBuilder() + .setKeyId(ecSignedPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(ecSignedPreKey.serializedPublicKey())) + .setSignature(ByteString.copyFrom(ecSignedPreKey.signature())) + .build()) + .setKemOneTimePreKey(KemSignedPreKey.newBuilder() + .setKeyId(kemSignedPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(kemSignedPreKey.serializedPublicKey())) + .setSignature(ByteString.copyFrom(kemSignedPreKey.signature())) + .build()) + .build()) + .build(); + + assertEquals(expectedResponse, response); + } + + @Test + void getPreKeysIncorrectUnidentifiedAccessKey() { + final Account targetAccount = mock(Account.class); + + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + final UUID identifier = UUID.randomUUID(); + + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + + when(targetAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); + when(targetAccount.getUuid()).thenReturn(identifier); + when(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(identityKey); + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(identifier))) + .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); + + assertStatusException(Status.UNAUTHENTICATED, () -> unauthenticatedServiceStub().getPreKeys(GetPreKeysAnonymousRequest.newBuilder() + .setRequest(GetPreKeysRequest.newBuilder() + .setTargetIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) + .setUuid(UUIDUtil.toByteString(identifier)) + .build()) + .setDeviceId(Device.MASTER_ID) + .build()) + .build())); + } + + @Test + void getPreKeysAccountNotFound() { + when(accountsManager.getByServiceIdentifierAsync(any())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + final StatusRuntimeException exception = + assertThrows(StatusRuntimeException.class, + () -> unauthenticatedServiceStub().getPreKeys(GetPreKeysAnonymousRequest.newBuilder() + .setUnidentifiedAccessKey(UUIDUtil.toByteString(UUID.randomUUID())) + .setRequest(GetPreKeysRequest.newBuilder() + .setTargetIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) + .setUuid(UUIDUtil.toByteString(UUID.randomUUID())) + .build()) + .build()) + .build())); + + assertEquals(Status.Code.UNAUTHENTICATED, exception.getStatus().getCode()); + } + + @ParameterizedTest + @ValueSource(longs = {KeysGrpcHelper.ALL_DEVICES, 1}) + void getPreKeysDeviceNotFound(final long deviceId) { + final UUID accountIdentifier = UUID.randomUUID(); + + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + + final Account targetAccount = mock(Account.class); + when(targetAccount.getUuid()).thenReturn(accountIdentifier); + when(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(Curve.generateKeyPair().getPublicKey())); + when(targetAccount.getDevices()).thenReturn(Collections.emptyList()); + when(targetAccount.getDevice(anyLong())).thenReturn(Optional.empty()); + when(targetAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); + + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(accountIdentifier))) + .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); + + assertStatusException(Status.NOT_FOUND, () -> unauthenticatedServiceStub().getPreKeys(GetPreKeysAnonymousRequest.newBuilder() + .setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey)) + .setRequest(GetPreKeysRequest.newBuilder() + .setTargetIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) + .setUuid(UUIDUtil.toByteString(accountIdentifier)) + .build()) + .setDeviceId(deviceId) + .build()) + .build())); + } + + @Test + void checkIdentityKeys() { + final ReactorKeysAnonymousGrpc.ReactorKeysAnonymousStub reactiveKeysAnonymousStub = ReactorKeysAnonymousGrpc.newReactorStub(SimpleBaseGrpcTest.GRPC_SERVER_EXTENSION_UNAUTHENTICATED.getChannel()); + when(accountsManager.getByServiceIdentifierAsync(any())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + final Account mismatchedAciFingerprintAccount = mock(Account.class); + final UUID mismatchedAciFingerprintAccountIdentifier = UUID.randomUUID(); + final IdentityKey mismatchedAciFingerprintAccountIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + + final Account matchingAciFingerprintAccount = mock(Account.class); + final UUID matchingAciFingerprintAccountIdentifier = UUID.randomUUID(); + final IdentityKey matchingAciFingerprintAccountIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + + final Account mismatchedPniFingerprintAccount = mock(Account.class); + final UUID mismatchedPniFingerprintAccountIdentifier = UUID.randomUUID(); + final IdentityKey mismatchedPniFingerpringAccountIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + + when(mismatchedAciFingerprintAccount.getIdentityKey(IdentityType.ACI)).thenReturn(mismatchedAciFingerprintAccountIdentityKey); + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(mismatchedAciFingerprintAccountIdentifier))) + .thenReturn(CompletableFuture.completedFuture(Optional.of(mismatchedAciFingerprintAccount))); + + when(matchingAciFingerprintAccount.getIdentityKey(IdentityType.ACI)).thenReturn(matchingAciFingerprintAccountIdentityKey); + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(matchingAciFingerprintAccountIdentifier))) + .thenReturn(CompletableFuture.completedFuture(Optional.of(matchingAciFingerprintAccount))); + + when(mismatchedPniFingerprintAccount.getIdentityKey(IdentityType.PNI)).thenReturn(mismatchedPniFingerpringAccountIdentityKey); + when(accountsManager.getByServiceIdentifierAsync(new PniServiceIdentifier(mismatchedPniFingerprintAccountIdentifier))) + .thenReturn(CompletableFuture.completedFuture(Optional.of(mismatchedPniFingerprintAccount))); + + final Flux requests = Flux.just( + buildCheckIdentityKeyRequest(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI, mismatchedAciFingerprintAccountIdentifier, + new IdentityKey(Curve.generateKeyPair().getPublicKey())), + buildCheckIdentityKeyRequest(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI, matchingAciFingerprintAccountIdentifier, + matchingAciFingerprintAccountIdentityKey), + buildCheckIdentityKeyRequest(org.signal.chat.common.IdentityType.IDENTITY_TYPE_PNI, UUID.randomUUID(), + new IdentityKey(Curve.generateKeyPair().getPublicKey())), + buildCheckIdentityKeyRequest(org.signal.chat.common.IdentityType.IDENTITY_TYPE_PNI, mismatchedPniFingerprintAccountIdentifier, + new IdentityKey(Curve.generateKeyPair().getPublicKey())) + ); + + final Map expectedResponses = Map.of( + mismatchedAciFingerprintAccountIdentifier, mismatchedAciFingerprintAccountIdentityKey, + mismatchedPniFingerprintAccountIdentifier, mismatchedPniFingerpringAccountIdentityKey); + + final Map responses = reactiveKeysAnonymousStub.checkIdentityKeys(requests) + .collectMap(response -> ServiceIdentifierUtil.fromGrpcServiceIdentifier(response.getTargetIdentifier()).uuid(), + response -> { + try { + return new IdentityKey(response.getIdentityKey().toByteArray()); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } + }) + .block(); + + assertEquals(expectedResponses, responses); + } + + private static CheckIdentityKeyRequest buildCheckIdentityKeyRequest(final org.signal.chat.common.IdentityType identityType, + final UUID uuid, final IdentityKey identityKey) { + return CheckIdentityKeyRequest.newBuilder() + .setTargetIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(identityType) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(uuid)))) + .setFingerprint(ByteString.copyFrom(getFingerprint(identityKey))) + .build(); + } + + private static byte[] getFingerprint(final IdentityKey publicKey) { + try { + return Util.truncate(MessageDigest.getInstance("SHA-256").digest(publicKey.serialize()), 4); + } catch (final NoSuchAlgorithmException e) { + throw new AssertionError("All Java implementations must support SHA-256 MessageDigest algorithm", e); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcServiceTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcServiceTest.java new file mode 100644 index 000000000..b0659107f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcServiceTest.java @@ -0,0 +1,641 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertRateLimitExceeded; +import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException; + +import com.google.protobuf.ByteString; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.signal.chat.common.EcPreKey; +import org.signal.chat.common.EcSignedPreKey; +import org.signal.chat.common.KemSignedPreKey; +import org.signal.chat.common.ServiceIdentifier; +import org.signal.chat.keys.GetPreKeyCountRequest; +import org.signal.chat.keys.GetPreKeyCountResponse; +import org.signal.chat.keys.GetPreKeysRequest; +import org.signal.chat.keys.GetPreKeysResponse; +import org.signal.chat.keys.KeysGrpc; +import org.signal.chat.keys.SetEcSignedPreKeyRequest; +import org.signal.chat.keys.SetKemLastResortPreKeyRequest; +import org.signal.chat.keys.SetOneTimeEcPreKeysRequest; +import org.signal.chat.keys.SetOneTimeKemSignedPreKeysRequest; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.entities.ECPreKey; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.KeysManager; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; +import org.whispersystems.textsecuregcm.util.UUIDUtil; +import reactor.core.publisher.Mono; + +class KeysGrpcServiceTest extends SimpleBaseGrpcTest { + + private static final ECKeyPair ACI_IDENTITY_KEY_PAIR = Curve.generateKeyPair(); + + private static final ECKeyPair PNI_IDENTITY_KEY_PAIR = Curve.generateKeyPair(); + + protected static final UUID AUTHENTICATED_PNI = UUID.randomUUID(); + + @Mock + private AccountsManager accountsManager; + + @Mock + private KeysManager keysManager; + + @Mock + private RateLimiter preKeysRateLimiter; + + @Mock + private Device authenticatedDevice; + + + @Override + protected KeysGrpcService createServiceBeforeEachTest() { + final RateLimiters rateLimiters = mock(RateLimiters.class); + when(rateLimiters.getPreKeysLimiter()).thenReturn(preKeysRateLimiter); + + when(preKeysRateLimiter.validateReactive(anyString())).thenReturn(Mono.empty()); + + when(authenticatedDevice.getId()).thenReturn(AUTHENTICATED_DEVICE_ID); + + final Account authenticatedAccount = mock(Account.class); + when(authenticatedAccount.getUuid()).thenReturn(AUTHENTICATED_ACI); + when(authenticatedAccount.getPhoneNumberIdentifier()).thenReturn(AUTHENTICATED_PNI); + when(authenticatedAccount.getIdentifier(IdentityType.ACI)).thenReturn(AUTHENTICATED_ACI); + when(authenticatedAccount.getIdentifier(IdentityType.PNI)).thenReturn(AUTHENTICATED_PNI); + when(authenticatedAccount.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(ACI_IDENTITY_KEY_PAIR.getPublicKey())); + when(authenticatedAccount.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(PNI_IDENTITY_KEY_PAIR.getPublicKey())); + when(authenticatedAccount.getDevice(AUTHENTICATED_DEVICE_ID)).thenReturn(Optional.of(authenticatedDevice)); + + when(accountsManager.getByAccountIdentifier(AUTHENTICATED_ACI)).thenReturn(Optional.of(authenticatedAccount)); + when(accountsManager.getByPhoneNumberIdentifier(AUTHENTICATED_PNI)).thenReturn(Optional.of(authenticatedAccount)); + + when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI)).thenReturn(CompletableFuture.completedFuture(Optional.of(authenticatedAccount))); + when(accountsManager.getByPhoneNumberIdentifierAsync(AUTHENTICATED_PNI)).thenReturn(CompletableFuture.completedFuture(Optional.of(authenticatedAccount))); + + return new KeysGrpcService(accountsManager, keysManager, rateLimiters); + } + + @Test + void getPreKeyCount() { + when(keysManager.getEcCount(AUTHENTICATED_ACI, AUTHENTICATED_DEVICE_ID)) + .thenReturn(CompletableFuture.completedFuture(1)); + + when(keysManager.getPqCount(AUTHENTICATED_ACI, AUTHENTICATED_DEVICE_ID)) + .thenReturn(CompletableFuture.completedFuture(2)); + + when(keysManager.getEcCount(AUTHENTICATED_PNI, AUTHENTICATED_DEVICE_ID)) + .thenReturn(CompletableFuture.completedFuture(3)); + + when(keysManager.getPqCount(AUTHENTICATED_PNI, AUTHENTICATED_DEVICE_ID)) + .thenReturn(CompletableFuture.completedFuture(4)); + + assertEquals(GetPreKeyCountResponse.newBuilder() + .setAciEcPreKeyCount(1) + .setAciKemPreKeyCount(2) + .setPniEcPreKeyCount(3) + .setPniKemPreKeyCount(4) + .build(), + authenticatedServiceStub().getPreKeyCount(GetPreKeyCountRequest.newBuilder().build())); + } + + @ParameterizedTest + @EnumSource(value = org.signal.chat.common.IdentityType.class, names = {"IDENTITY_TYPE_ACI", "IDENTITY_TYPE_PNI"}) + void setOneTimeEcPreKeys(final org.signal.chat.common.IdentityType identityType) { + final List preKeys = new ArrayList<>(); + + for (int keyId = 0; keyId < 100; keyId++) { + preKeys.add(new ECPreKey(keyId, Curve.generateKeyPair().getPublicKey())); + } + + when(keysManager.storeEcOneTimePreKeys(any(), anyLong(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + //noinspection ResultOfMethodCallIgnored + authenticatedServiceStub().setOneTimeEcPreKeys(SetOneTimeEcPreKeysRequest.newBuilder() + .setIdentityType(identityType) + .addAllPreKeys(preKeys.stream() + .map(preKey -> EcPreKey.newBuilder() + .setKeyId(preKey.keyId()) + .setPublicKey(ByteString.copyFrom(preKey.serializedPublicKey())) + .build()) + .toList()) + .build()); + + final UUID expectedIdentifier = switch (IdentityTypeUtil.fromGrpcIdentityType(identityType)) { + case ACI -> AUTHENTICATED_ACI; + case PNI -> AUTHENTICATED_PNI; + }; + + verify(keysManager).storeEcOneTimePreKeys(expectedIdentifier, AUTHENTICATED_DEVICE_ID, preKeys); + } + + @ParameterizedTest + @MethodSource + void setOneTimeEcPreKeysWithError(final SetOneTimeEcPreKeysRequest request) { + assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().setOneTimeEcPreKeys(request)); + } + + private static Stream setOneTimeEcPreKeysWithError() { + final SetOneTimeEcPreKeysRequest prototypeRequest = SetOneTimeEcPreKeysRequest.newBuilder() + .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) + .addPreKeys(EcPreKey.newBuilder() + .setKeyId(1) + .setPublicKey(ByteString.copyFrom(Curve.generateKeyPair().getPublicKey().serialize())) + .build()) + .build(); + + return Stream.of( + // Missing identity type + Arguments.of(SetOneTimeEcPreKeysRequest.newBuilder(prototypeRequest) + .clearIdentityType() + .build()), + + // Invalid public key + Arguments.of(SetOneTimeEcPreKeysRequest.newBuilder(prototypeRequest) + .setPreKeys(0, EcPreKey.newBuilder(prototypeRequest.getPreKeys(0)) + .clearPublicKey() + .build()) + .build()), + + // No keys + Arguments.of(SetOneTimeEcPreKeysRequest.newBuilder(prototypeRequest) + .clearPreKeys() + .build()) + ); + } + + @ParameterizedTest + @EnumSource(value = org.signal.chat.common.IdentityType.class, names = {"IDENTITY_TYPE_ACI", "IDENTITY_TYPE_PNI"}) + void setOneTimeKemSignedPreKeys(final org.signal.chat.common.IdentityType identityType) { + final ECKeyPair identityKeyPair = switch (IdentityTypeUtil.fromGrpcIdentityType(identityType)) { + case ACI -> ACI_IDENTITY_KEY_PAIR; + case PNI -> PNI_IDENTITY_KEY_PAIR; + }; + + final List preKeys = new ArrayList<>(); + + for (int keyId = 0; keyId < 100; keyId++) { + preKeys.add(KeysHelper.signedKEMPreKey(keyId, identityKeyPair)); + } + + when(keysManager.storeKemOneTimePreKeys(any(), anyLong(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + //noinspection ResultOfMethodCallIgnored + authenticatedServiceStub().setOneTimeKemSignedPreKeys( + SetOneTimeKemSignedPreKeysRequest.newBuilder() + .setIdentityType(identityType) + .addAllPreKeys(preKeys.stream() + .map(preKey -> KemSignedPreKey.newBuilder() + .setKeyId(preKey.keyId()) + .setPublicKey(ByteString.copyFrom(preKey.serializedPublicKey())) + .setSignature(ByteString.copyFrom(preKey.signature())) + .build()) + .toList()) + .build()); + + final UUID expectedIdentifier = switch (IdentityTypeUtil.fromGrpcIdentityType(identityType)) { + case ACI -> AUTHENTICATED_ACI; + case PNI -> AUTHENTICATED_PNI; + }; + + verify(keysManager).storeKemOneTimePreKeys(expectedIdentifier, AUTHENTICATED_DEVICE_ID, preKeys); + } + + @ParameterizedTest + @MethodSource + void setOneTimeKemSignedPreKeysWithError(final SetOneTimeKemSignedPreKeysRequest request) { + assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().setOneTimeKemSignedPreKeys(request)); + } + + private static Stream setOneTimeKemSignedPreKeysWithError() { + final KEMSignedPreKey signedPreKey = KeysHelper.signedKEMPreKey(1, ACI_IDENTITY_KEY_PAIR); + + final SetOneTimeKemSignedPreKeysRequest prototypeRequest = SetOneTimeKemSignedPreKeysRequest.newBuilder() + .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) + .addPreKeys(KemSignedPreKey.newBuilder() + .setKeyId(1) + .setPublicKey(ByteString.copyFrom(signedPreKey.serializedPublicKey())) + .setSignature(ByteString.copyFrom(signedPreKey.signature())) + .build()) + .build(); + + return Stream.of( + // Missing identity type + Arguments.of(SetOneTimeKemSignedPreKeysRequest.newBuilder(prototypeRequest) + .clearIdentityType() + .build()), + + // Invalid public key + Arguments.of(SetOneTimeKemSignedPreKeysRequest.newBuilder(prototypeRequest) + .setPreKeys(0, KemSignedPreKey.newBuilder(prototypeRequest.getPreKeys(0)) + .clearPublicKey() + .build()) + .build()), + + // Invalid signature + Arguments.of(SetOneTimeKemSignedPreKeysRequest.newBuilder(prototypeRequest) + .setPreKeys(0, KemSignedPreKey.newBuilder(prototypeRequest.getPreKeys(0)) + .clearSignature() + .build()) + .build()), + + // No keys + Arguments.of(SetOneTimeKemSignedPreKeysRequest.newBuilder(prototypeRequest) + .clearPreKeys() + .build()) + ); + } + + @ParameterizedTest + @EnumSource(value = org.signal.chat.common.IdentityType.class, names = {"IDENTITY_TYPE_ACI", "IDENTITY_TYPE_PNI"}) + void setSignedPreKey(final org.signal.chat.common.IdentityType identityType) { + when(accountsManager.updateDeviceAsync(any(), anyLong(), any())).thenAnswer(invocation -> { + final Account account = invocation.getArgument(0); + final long deviceId = invocation.getArgument(1); + final Consumer deviceUpdater = invocation.getArgument(2); + + account.getDevice(deviceId).ifPresent(deviceUpdater); + + return CompletableFuture.completedFuture(account); + }); + + when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + final ECKeyPair identityKeyPair = switch (IdentityTypeUtil.fromGrpcIdentityType(identityType)) { + case ACI -> ACI_IDENTITY_KEY_PAIR; + case PNI -> PNI_IDENTITY_KEY_PAIR; + }; + + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(17, identityKeyPair); + + //noinspection ResultOfMethodCallIgnored + authenticatedServiceStub().setEcSignedPreKey(SetEcSignedPreKeyRequest.newBuilder() + .setIdentityType(identityType) + .setSignedPreKey(EcSignedPreKey.newBuilder() + .setKeyId(signedPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(signedPreKey.serializedPublicKey())) + .setSignature(ByteString.copyFrom(signedPreKey.signature())) + .build()) + .build()); + + switch (identityType) { + case IDENTITY_TYPE_ACI -> { + verify(authenticatedDevice).setSignedPreKey(signedPreKey); + verify(keysManager).storeEcSignedPreKeys(AUTHENTICATED_ACI, Map.of(AUTHENTICATED_DEVICE_ID, signedPreKey)); + } + + case IDENTITY_TYPE_PNI -> { + verify(authenticatedDevice).setPhoneNumberIdentitySignedPreKey(signedPreKey); + verify(keysManager).storeEcSignedPreKeys(AUTHENTICATED_PNI, Map.of(AUTHENTICATED_DEVICE_ID, signedPreKey)); + } + } + } + + @ParameterizedTest + @MethodSource + void setSignedPreKeyWithError(final SetEcSignedPreKeyRequest request) { + final StatusRuntimeException exception = + assertThrows(StatusRuntimeException.class, () -> authenticatedServiceStub().setEcSignedPreKey(request)); + + assertEquals(Status.INVALID_ARGUMENT.getCode(), exception.getStatus().getCode()); + } + + private static Stream setSignedPreKeyWithError() { + final ECSignedPreKey signedPreKey = KeysHelper.signedECPreKey(17, ACI_IDENTITY_KEY_PAIR); + + final SetEcSignedPreKeyRequest prototypeRequest = SetEcSignedPreKeyRequest.newBuilder() + .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) + .setSignedPreKey(EcSignedPreKey.newBuilder() + .setKeyId(signedPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(signedPreKey.serializedPublicKey())) + .setSignature(ByteString.copyFrom(signedPreKey.signature())) + .build()) + .build(); + + return Stream.of( + // Missing identity type + Arguments.of(SetEcSignedPreKeyRequest.newBuilder(prototypeRequest) + .clearIdentityType() + .build()), + + // Invalid public key + Arguments.of(SetEcSignedPreKeyRequest.newBuilder(prototypeRequest) + .setSignedPreKey(EcSignedPreKey.newBuilder(prototypeRequest.getSignedPreKey()) + .clearPublicKey() + .build()) + .build()), + + // Invalid signature + Arguments.of(SetEcSignedPreKeyRequest.newBuilder(prototypeRequest) + .setSignedPreKey(EcSignedPreKey.newBuilder(prototypeRequest.getSignedPreKey()) + .clearSignature() + .build()) + .build()), + + // Missing key + Arguments.of(SetEcSignedPreKeyRequest.newBuilder(prototypeRequest) + .clearSignedPreKey() + .build()) + ); + } + + @ParameterizedTest + @EnumSource(value = org.signal.chat.common.IdentityType.class, names = {"IDENTITY_TYPE_ACI", "IDENTITY_TYPE_PNI"}) + void setLastResortPreKey(final org.signal.chat.common.IdentityType identityType) { + when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + final ECKeyPair identityKeyPair = switch (IdentityTypeUtil.fromGrpcIdentityType(identityType)) { + case ACI -> ACI_IDENTITY_KEY_PAIR; + case PNI -> PNI_IDENTITY_KEY_PAIR; + }; + + final KEMSignedPreKey lastResortPreKey = KeysHelper.signedKEMPreKey(17, identityKeyPair); + + //noinspection ResultOfMethodCallIgnored + authenticatedServiceStub().setKemLastResortPreKey(SetKemLastResortPreKeyRequest.newBuilder() + .setIdentityType(identityType) + .setSignedPreKey(KemSignedPreKey.newBuilder() + .setKeyId(lastResortPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(lastResortPreKey.serializedPublicKey())) + .setSignature(ByteString.copyFrom(lastResortPreKey.signature())) + .build()) + .build()); + + final UUID expectedIdentifier = switch (identityType) { + case IDENTITY_TYPE_ACI -> AUTHENTICATED_ACI; + case IDENTITY_TYPE_PNI -> AUTHENTICATED_PNI; + case IDENTITY_TYPE_UNSPECIFIED, UNRECOGNIZED -> throw new AssertionError("Bad identity type"); + }; + + verify(keysManager).storePqLastResort(expectedIdentifier, Map.of(AUTHENTICATED_DEVICE_ID, lastResortPreKey)); + } + + @ParameterizedTest + @MethodSource + void setLastResortPreKeyWithError(final SetKemLastResortPreKeyRequest request) { + assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().setKemLastResortPreKey(request)); + } + + private static Stream setLastResortPreKeyWithError() { + final KEMSignedPreKey lastResortPreKey = KeysHelper.signedKEMPreKey(17, ACI_IDENTITY_KEY_PAIR); + + final SetKemLastResortPreKeyRequest prototypeRequest = SetKemLastResortPreKeyRequest.newBuilder() + .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) + .setSignedPreKey(KemSignedPreKey.newBuilder() + .setKeyId(lastResortPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(lastResortPreKey.serializedPublicKey())) + .setSignature(ByteString.copyFrom(lastResortPreKey.signature())) + .build()) + .build(); + + return Stream.of( + // No identity type + Arguments.of(SetKemLastResortPreKeyRequest.newBuilder(prototypeRequest) + .clearIdentityType() + .build()), + + // Bad public key + Arguments.of(SetKemLastResortPreKeyRequest.newBuilder(prototypeRequest) + .setSignedPreKey(KemSignedPreKey.newBuilder(prototypeRequest.getSignedPreKey()) + .clearPublicKey() + .build()) + .build()), + + // Bad signature + Arguments.of(SetKemLastResortPreKeyRequest.newBuilder(prototypeRequest) + .setSignedPreKey(KemSignedPreKey.newBuilder(prototypeRequest.getSignedPreKey()) + .clearSignature() + .build()) + .build()), + + // Missing key + Arguments.of(SetKemLastResortPreKeyRequest.newBuilder(prototypeRequest) + .clearSignedPreKey() + .build()) + ); + } + + @ParameterizedTest + @EnumSource(value = org.signal.chat.common.IdentityType.class, names = {"IDENTITY_TYPE_ACI", "IDENTITY_TYPE_PNI"}) + void getPreKeys(final org.signal.chat.common.IdentityType grpcIdentityType) { + final Account targetAccount = mock(Account.class); + + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + final UUID identifier = UUID.randomUUID(); + + final IdentityType identityType = IdentityTypeUtil.fromGrpcIdentityType(grpcIdentityType); + + when(targetAccount.getUuid()).thenReturn(UUID.randomUUID()); + when(targetAccount.getIdentifier(identityType)).thenReturn(identifier); + when(targetAccount.getIdentityKey(identityType)).thenReturn(identityKey); + when(accountsManager.getByServiceIdentifierAsync(argThat(serviceIdentifier -> serviceIdentifier.uuid().equals(identifier)))) + .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); + + final Map ecOneTimePreKeys = new HashMap<>(); + final Map kemPreKeys = new HashMap<>(); + final Map ecSignedPreKeys = new HashMap<>(); + + final Map devices = new HashMap<>(); + + for (final long deviceId : List.of(1, 2)) { + ecOneTimePreKeys.put(deviceId, new ECPreKey(1, Curve.generateKeyPair().getPublicKey())); + kemPreKeys.put(deviceId, KeysHelper.signedKEMPreKey(2, identityKeyPair)); + ecSignedPreKeys.put(deviceId, KeysHelper.signedECPreKey(3, identityKeyPair)); + + final Device device = mock(Device.class); + when(device.getId()).thenReturn(deviceId); + when(device.isEnabled()).thenReturn(true); + when(device.getSignedPreKey(identityType)).thenReturn(ecSignedPreKeys.get(deviceId)); + + devices.put(deviceId, device); + when(targetAccount.getDevice(deviceId)).thenReturn(Optional.of(device)); + } + + when(targetAccount.getDevices()).thenReturn(new ArrayList<>(devices.values())); + + ecOneTimePreKeys.forEach((deviceId, preKey) -> when(keysManager.takeEC(identifier, deviceId)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(preKey)))); + + kemPreKeys.forEach((deviceId, preKey) -> when(keysManager.takePQ(identifier, deviceId)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(preKey)))); + + { + final GetPreKeysResponse response = authenticatedServiceStub().getPreKeys(GetPreKeysRequest.newBuilder() + .setTargetIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(grpcIdentityType) + .setUuid(UUIDUtil.toByteString(identifier)) + .build()) + .setDeviceId(1) + .build()); + + final GetPreKeysResponse expectedResponse = GetPreKeysResponse.newBuilder() + .setIdentityKey(ByteString.copyFrom(identityKey.serialize())) + .putPreKeys(1, GetPreKeysResponse.PreKeyBundle.newBuilder() + .setEcSignedPreKey(EcSignedPreKey.newBuilder() + .setKeyId(ecSignedPreKeys.get(1L).keyId()) + .setPublicKey(ByteString.copyFrom(ecSignedPreKeys.get(1L).serializedPublicKey())) + .setSignature(ByteString.copyFrom(ecSignedPreKeys.get(1L).signature())) + .build()) + .setEcOneTimePreKey(EcPreKey.newBuilder() + .setKeyId(ecOneTimePreKeys.get(1L).keyId()) + .setPublicKey(ByteString.copyFrom(ecOneTimePreKeys.get(1L).serializedPublicKey())) + .build()) + .setKemOneTimePreKey(KemSignedPreKey.newBuilder() + .setKeyId(kemPreKeys.get(1L).keyId()) + .setPublicKey(ByteString.copyFrom(kemPreKeys.get(1L).serializedPublicKey())) + .setSignature(ByteString.copyFrom(kemPreKeys.get(1L).signature())) + .build()) + .build()) + .build(); + + assertEquals(expectedResponse, response); + } + + when(keysManager.takeEC(identifier, 2)).thenReturn(CompletableFuture.completedFuture(Optional.empty())); + when(keysManager.takePQ(identifier, 2)).thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + { + final GetPreKeysResponse response = authenticatedServiceStub().getPreKeys(GetPreKeysRequest.newBuilder() + .setTargetIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(grpcIdentityType) + .setUuid(UUIDUtil.toByteString(identifier)) + .build()) + .build()); + + final GetPreKeysResponse expectedResponse = GetPreKeysResponse.newBuilder() + .setIdentityKey(ByteString.copyFrom(identityKey.serialize())) + .putPreKeys(1, GetPreKeysResponse.PreKeyBundle.newBuilder() + .setEcSignedPreKey(EcSignedPreKey.newBuilder() + .setKeyId(ecSignedPreKeys.get(1L).keyId()) + .setPublicKey(ByteString.copyFrom(ecSignedPreKeys.get(1L).serializedPublicKey())) + .setSignature(ByteString.copyFrom(ecSignedPreKeys.get(1L).signature())) + .build()) + .setEcOneTimePreKey(EcPreKey.newBuilder() + .setKeyId(ecOneTimePreKeys.get(1L).keyId()) + .setPublicKey(ByteString.copyFrom(ecOneTimePreKeys.get(1L).serializedPublicKey())) + .build()) + .setKemOneTimePreKey(KemSignedPreKey.newBuilder() + .setKeyId(kemPreKeys.get(1L).keyId()) + .setPublicKey(ByteString.copyFrom(kemPreKeys.get(1L).serializedPublicKey())) + .setSignature(ByteString.copyFrom(kemPreKeys.get(1L).signature())) + .build()) + .build()) + .putPreKeys(2, GetPreKeysResponse.PreKeyBundle.newBuilder() + .setEcSignedPreKey(EcSignedPreKey.newBuilder() + .setKeyId(ecSignedPreKeys.get(2L).keyId()) + .setPublicKey(ByteString.copyFrom(ecSignedPreKeys.get(2L).serializedPublicKey())) + .setSignature(ByteString.copyFrom(ecSignedPreKeys.get(2L).signature())) + .build()) + .build()) + .build(); + + assertEquals(expectedResponse, response); + } + } + + @Test + void getPreKeysAccountNotFound() { + when(accountsManager.getByServiceIdentifierAsync(any())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + assertStatusException(Status.NOT_FOUND, () -> authenticatedServiceStub().getPreKeys(GetPreKeysRequest.newBuilder() + .setTargetIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) + .setUuid(UUIDUtil.toByteString(UUID.randomUUID())) + .build()) + .build())); + } + + @ParameterizedTest + @ValueSource(longs = {KeysGrpcHelper.ALL_DEVICES, 1}) + void getPreKeysDeviceNotFound(final long deviceId) { + final UUID accountIdentifier = UUID.randomUUID(); + + final Account targetAccount = mock(Account.class); + when(targetAccount.getUuid()).thenReturn(accountIdentifier); + when(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(Curve.generateKeyPair().getPublicKey())); + when(targetAccount.getDevices()).thenReturn(Collections.emptyList()); + when(targetAccount.getDevice(anyLong())).thenReturn(Optional.empty()); + + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(accountIdentifier))) + .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); + + assertStatusException(Status.NOT_FOUND, () -> authenticatedServiceStub().getPreKeys(GetPreKeysRequest.newBuilder() + .setTargetIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) + .setUuid(UUIDUtil.toByteString(accountIdentifier)) + .build()) + .setDeviceId(deviceId) + .build())); + } + + @Test + void getPreKeysRateLimited() { + final Account targetAccount = mock(Account.class); + when(targetAccount.getUuid()).thenReturn(UUID.randomUUID()); + when(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(Curve.generateKeyPair().getPublicKey())); + when(targetAccount.getDevices()).thenReturn(Collections.emptyList()); + when(targetAccount.getDevice(anyLong())).thenReturn(Optional.empty()); + + when(accountsManager.getByServiceIdentifierAsync(any())) + .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); + + final Duration retryAfterDuration = Duration.ofMinutes(7); + when(preKeysRateLimiter.validateReactive(anyString())) + .thenReturn(Mono.error(new RateLimitExceededException(retryAfterDuration, false))); + + assertRateLimitExceeded(retryAfterDuration, () -> authenticatedServiceStub().getPreKeys(GetPreKeysRequest.newBuilder() + .setTargetIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) + .setUuid(UUIDUtil.toByteString(UUID.randomUUID())) + .build()) + .build())); + verifyNoInteractions(accountsManager); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MockRemoteAddressInterceptor.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MockRemoteAddressInterceptor.java new file mode 100644 index 000000000..11e316b42 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MockRemoteAddressInterceptor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import java.net.SocketAddress; +import javax.annotation.Nullable; + +public class MockRemoteAddressInterceptor implements ServerInterceptor { + + @Nullable + private SocketAddress remoteAddress; + + public void setRemoteAddress(@Nullable final SocketAddress remoteAddress) { + this.remoteAddress = remoteAddress; + } + + @Override + public ServerCall.Listener interceptCall(final ServerCall serverCall, + final Metadata headers, + final ServerCallHandler next) { + + return remoteAddress != null + ? next.startCall(serverCall, headers) + : Contexts.interceptCall( + Context.current().withValue(RemoteAddressUtil.REMOTE_ADDRESS_CONTEXT_KEY, remoteAddress), + serverCall, headers, next); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/PaymentsGrpcServiceTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/PaymentsGrpcServiceTest.java new file mode 100644 index 000000000..1c70d1ebb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/PaymentsGrpcServiceTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException; + +import io.grpc.Status; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.signal.chat.payments.GetCurrencyConversionsRequest; +import org.signal.chat.payments.GetCurrencyConversionsResponse; +import org.signal.chat.payments.PaymentsGrpc; +import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager; +import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntity; +import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntityList; + +class PaymentsGrpcServiceTest extends SimpleBaseGrpcTest { + + @Mock + private CurrencyConversionManager currencyManager; + + @Override + protected PaymentsGrpcService createServiceBeforeEachTest() { + return new PaymentsGrpcService(currencyManager); + } + + @Test + void testGetCurrencyConversions() { + final long timestamp = System.currentTimeMillis(); + when(currencyManager.getCurrencyConversions()).thenReturn(Optional.of( + new CurrencyConversionEntityList(List.of( + new CurrencyConversionEntity("FOO", Map.of( + "USD", new BigDecimal("2.35"), + "EUR", new BigDecimal("1.89") + )), + new CurrencyConversionEntity("BAR", Map.of( + "USD", new BigDecimal("1.50"), + "EUR", new BigDecimal("0.98") + )) + ), timestamp))); + + final GetCurrencyConversionsResponse currencyConversions = authenticatedServiceStub().getCurrencyConversions( + GetCurrencyConversionsRequest.newBuilder().build()); + + assertEquals(timestamp, currencyConversions.getTimestamp()); + assertEquals(2, currencyConversions.getCurrenciesCount()); + assertEquals("FOO", currencyConversions.getCurrencies(0).getBase()); + assertEquals("2.35", currencyConversions.getCurrencies(0).getConversionsMap().get("USD")); + } + + @Test + void testUnavailable() { + when(currencyManager.getCurrencyConversions()).thenReturn(Optional.empty()); + assertStatusException(Status.UNAVAILABLE, () -> authenticatedServiceStub().getCurrencyConversions( + GetCurrencyConversionsRequest.newBuilder().build())); + } + + @Test + public void testUnauthenticated() throws Exception { + assertStatusException(Status.UNAUTHENTICATED, () -> unauthenticatedServiceStub().getCurrencyConversions( + GetCurrencyConversionsRequest.newBuilder().build())); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcServiceTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcServiceTest.java new file mode 100644 index 000000000..65ef88fe9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcServiceTest.java @@ -0,0 +1,523 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException; + +import com.google.protobuf.ByteString; +import io.grpc.Channel; +import io.grpc.Metadata; +import io.grpc.Status; +import io.grpc.stub.MetadataUtils; +import java.lang.reflect.InvocationTargetException; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.signal.chat.common.IdentityType; +import org.signal.chat.common.ServiceIdentifier; +import org.signal.chat.profile.CredentialType; +import org.signal.chat.profile.GetExpiringProfileKeyCredentialAnonymousRequest; +import org.signal.chat.profile.GetExpiringProfileKeyCredentialRequest; +import org.signal.chat.profile.GetExpiringProfileKeyCredentialResponse; +import org.signal.chat.profile.GetUnversionedProfileAnonymousRequest; +import org.signal.chat.profile.GetUnversionedProfileRequest; +import org.signal.chat.profile.GetUnversionedProfileResponse; +import org.signal.chat.profile.GetVersionedProfileAnonymousRequest; +import org.signal.chat.profile.GetVersionedProfileRequest; +import org.signal.chat.profile.GetVersionedProfileResponse; +import org.signal.chat.profile.ProfileAnonymousGrpc; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ServiceId; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.ServerPublicParams; +import org.signal.libsignal.zkgroup.ServerSecretParams; +import org.signal.libsignal.zkgroup.VerificationFailedException; +import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; +import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; +import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; +import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequestContext; +import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; +import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum; +import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; +import org.whispersystems.textsecuregcm.entities.Badge; +import org.whispersystems.textsecuregcm.entities.BadgeSvg; +import org.whispersystems.textsecuregcm.entities.UserCapabilities; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.ProfilesManager; +import org.whispersystems.textsecuregcm.storage.VersionedProfile; +import org.whispersystems.textsecuregcm.tests.util.ProfileTestHelper; +import org.whispersystems.textsecuregcm.util.UUIDUtil; + +public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest { + + @Mock + private Account account; + + @Mock + private AccountsManager accountsManager; + + @Mock + private ProfilesManager profilesManager; + + @Mock + private ProfileBadgeConverter profileBadgeConverter; + + @Mock + private ServerZkProfileOperations serverZkProfileOperations; + + + @Override + protected ProfileAnonymousGrpcService createServiceBeforeEachTest() { + return new ProfileAnonymousGrpcService( + accountsManager, + profilesManager, + profileBadgeConverter, + serverZkProfileOperations + ); + } + + @Override + protected ProfileAnonymousGrpc.ProfileAnonymousBlockingStub createStub(final Channel channel) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException { + final Metadata metadata = new Metadata(); + metadata.put(AcceptLanguageInterceptor.ACCEPTABLE_LANGUAGES_GRPC_HEADER, "en-us"); + metadata.put(UserAgentInterceptor.USER_AGENT_GRPC_HEADER, "Signal-Android/1.2.3"); + return super.createStub(channel).withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata)); + } + + @Test + void getUnversionedProfile() { + final UUID targetUuid = UUID.randomUUID(); + final org.whispersystems.textsecuregcm.identity.ServiceIdentifier serviceIdentifier = new AciServiceIdentifier(targetUuid); + + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + + final List badges = List.of(new Badge( + "TEST", + "other", + "Test Badge", + "This badge is in unit tests.", + List.of("l", "m", "h", "x", "xx", "xxx"), + "SVG", + List.of( + new BadgeSvg("sl", "sd"), + new BadgeSvg("ml", "md"), + new BadgeSvg("ll", "ld"))) + ); + + when(account.getBadges()).thenReturn(Collections.emptyList()); + when(profileBadgeConverter.convert(any(), any(), anyBoolean())).thenReturn(badges); + when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); + when(account.getIdentityKey(org.whispersystems.textsecuregcm.identity.IdentityType.ACI)).thenReturn(identityKey); + when(accountsManager.getByServiceIdentifierAsync(serviceIdentifier)).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + final GetUnversionedProfileAnonymousRequest request = GetUnversionedProfileAnonymousRequest.newBuilder() + .setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey)) + .setRequest(GetUnversionedProfileRequest.newBuilder() + .setServiceIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(targetUuid))) + .build()) + .build()) + .build(); + + final GetUnversionedProfileResponse response = unauthenticatedServiceStub().getUnversionedProfile(request); + + final byte[] unidentifiedAccessChecksum = UnidentifiedAccessChecksum.generateFor(unidentifiedAccessKey); + final GetUnversionedProfileResponse expectedResponse = GetUnversionedProfileResponse.newBuilder() + .setIdentityKey(ByteString.copyFrom(identityKey.serialize())) + .setUnidentifiedAccess(ByteString.copyFrom(unidentifiedAccessChecksum)) + .setUnrestrictedUnidentifiedAccess(false) + .setCapabilities(ProfileGrpcHelper.buildUserCapabilities(UserCapabilities.createForAccount(account))) + .addAllBadges(ProfileGrpcHelper.buildBadges(badges)) + .build(); + + verify(accountsManager).getByServiceIdentifierAsync(serviceIdentifier); + assertEquals(expectedResponse, response); + } + + @ParameterizedTest + @MethodSource + void getUnversionedProfileUnauthenticated(final IdentityType identityType, final boolean missingUnidentifiedAccessKey, final boolean accountNotFound) { + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); + when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false); + when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn( + CompletableFuture.completedFuture(accountNotFound ? Optional.empty() : Optional.of(account))); + + final GetUnversionedProfileAnonymousRequest.Builder requestBuilder = GetUnversionedProfileAnonymousRequest.newBuilder() + .setRequest(GetUnversionedProfileRequest.newBuilder() + .setServiceIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(identityType) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID()))) + .build()) + .build()); + + if (!missingUnidentifiedAccessKey) { + requestBuilder.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey)); + } + + assertStatusException(Status.UNAUTHENTICATED, () -> unauthenticatedServiceStub().getUnversionedProfile(requestBuilder.build())); + } + + private static Stream getUnversionedProfileUnauthenticated() { + return Stream.of( + Arguments.of(IdentityType.IDENTITY_TYPE_PNI, false, false), + Arguments.of(IdentityType.IDENTITY_TYPE_ACI, true, false), + Arguments.of(IdentityType.IDENTITY_TYPE_ACI, false, true) + ); + } + + @ParameterizedTest + @MethodSource + void getVersionedProfile(final String requestVersion, + @Nullable final String accountVersion, + final boolean expectResponseHasPaymentAddress) { + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + + final VersionedProfile profile = mock(VersionedProfile.class); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] emoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] about = ProfileTestHelper.generateRandomByteArray(156); + final byte[] paymentAddress = ProfileTestHelper.generateRandomByteArray(582); + final String avatar = "profiles/" + ProfileTestHelper.generateRandomBase64FromByteArray(16); + + when(profile.name()).thenReturn(name); + when(profile.aboutEmoji()).thenReturn(emoji); + when(profile.about()).thenReturn(about); + when(profile.paymentAddress()).thenReturn(paymentAddress); + when(profile.avatar()).thenReturn(avatar); + + when(account.getCurrentProfileVersion()).thenReturn(Optional.ofNullable(accountVersion)); + when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); + + when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + when(profilesManager.getAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(Optional.of(profile))); + + final GetVersionedProfileAnonymousRequest request = GetVersionedProfileAnonymousRequest.newBuilder() + .setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey)) + .setRequest(GetVersionedProfileRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID()))) + .build()) + .setVersion(requestVersion) + .build()) + .build(); + + final GetVersionedProfileResponse response = unauthenticatedServiceStub().getVersionedProfile(request); + + final GetVersionedProfileResponse.Builder expectedResponseBuilder = GetVersionedProfileResponse.newBuilder() + .setName(ByteString.copyFrom(name)) + .setAbout(ByteString.copyFrom(about)) + .setAboutEmoji(ByteString.copyFrom(emoji)) + .setAvatar(avatar); + + if (expectResponseHasPaymentAddress) { + expectedResponseBuilder.setPaymentAddress(ByteString.copyFrom(paymentAddress)); + } + + assertEquals(expectedResponseBuilder.build(), response); + } + + private static Stream getVersionedProfile() { + return Stream.of( + Arguments.of("version1", "version1", true), + Arguments.of("version1", null, true), + Arguments.of("version1", "version2", false) + ); + } + + @Test + void getVersionedProfileVersionNotFound() { + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); + when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false); + + when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + when(profilesManager.getAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + final GetVersionedProfileAnonymousRequest request = GetVersionedProfileAnonymousRequest.newBuilder() + .setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey)) + .setRequest(GetVersionedProfileRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID()))) + .build()) + .setVersion("someVersion") + .build()) + .build(); + + assertStatusException(Status.NOT_FOUND, () -> unauthenticatedServiceStub().getVersionedProfile(request)); + } + + @ParameterizedTest + @MethodSource + void getVersionedProfileUnauthenticated(final boolean missingUnidentifiedAccessKey, + final boolean accountNotFound) { + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + + when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); + when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn( + CompletableFuture.completedFuture(accountNotFound ? Optional.empty() : Optional.of(account))); + + final GetVersionedProfileAnonymousRequest.Builder requestBuilder = GetVersionedProfileAnonymousRequest.newBuilder() + .setRequest(GetVersionedProfileRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID()))) + .build()) + .setVersion("someVersion") + .build()); + + if (!missingUnidentifiedAccessKey) { + requestBuilder.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey)); + } + + assertStatusException(Status.UNAUTHENTICATED, () -> unauthenticatedServiceStub().getVersionedProfile(requestBuilder.build())); + } + private static Stream getVersionedProfileUnauthenticated() { + return Stream.of( + Arguments.of(true, false), + Arguments.of(false, true) + ); + } + + @Test + void getVersionedProfilePniInvalidArgument() { + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + + final GetVersionedProfileAnonymousRequest request = GetVersionedProfileAnonymousRequest.newBuilder() + .setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey)) + .setRequest(GetVersionedProfileRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_PNI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID()))) + .build()) + .setVersion("someVersion") + .build()) + .build(); + + assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().getVersionedProfile(request)); + } + + @Test + void getExpiringProfileKeyCredential() throws InvalidInputException, VerificationFailedException { + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + final UUID targetUuid = UUID.randomUUID(); + + final ServerSecretParams serverSecretParams = ServerSecretParams.generate(); + final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams(); + + final ServerZkProfileOperations serverZkProfile = new ServerZkProfileOperations(serverSecretParams); + final ClientZkProfileOperations clientZkProfile = new ClientZkProfileOperations(serverPublicParams); + + final byte[] profileKeyBytes = new byte[32]; + new SecureRandom().nextBytes(profileKeyBytes); + + final ProfileKey profileKey = new ProfileKey(profileKeyBytes); + final ProfileKeyCommitment profileKeyCommitment = profileKey.getCommitment(new ServiceId.Aci(targetUuid)); + final ProfileKeyCredentialRequestContext profileKeyCredentialRequestContext = + clientZkProfile.createProfileKeyCredentialRequestContext(new ServiceId.Aci(targetUuid), profileKey); + + final VersionedProfile profile = mock(VersionedProfile.class); + when(profile.commitment()).thenReturn(profileKeyCommitment.serialize()); + + when(account.getUuid()).thenReturn(targetUuid); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(targetUuid))).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + when(profilesManager.getAsync(targetUuid, "someVersion")).thenReturn(CompletableFuture.completedFuture(Optional.of(profile))); + + final ProfileKeyCredentialRequest credentialRequest = profileKeyCredentialRequestContext.getRequest(); + + final Instant expiration = Instant.now().plus(org.whispersystems.textsecuregcm.util.ProfileHelper.EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION) + .truncatedTo(ChronoUnit.DAYS); + + final ExpiringProfileKeyCredentialResponse credentialResponse = + serverZkProfile.issueExpiringProfileKeyCredential(credentialRequest, new ServiceId.Aci(targetUuid), profileKeyCommitment, expiration); + + when(serverZkProfileOperations.issueExpiringProfileKeyCredential(credentialRequest, new ServiceId.Aci(targetUuid), profileKeyCommitment, expiration)) + .thenReturn(credentialResponse); + + final GetExpiringProfileKeyCredentialAnonymousRequest request = GetExpiringProfileKeyCredentialAnonymousRequest.newBuilder() + .setRequest(GetExpiringProfileKeyCredentialRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(targetUuid))) + .build()) + .setCredentialRequest(ByteString.copyFrom(credentialRequest.serialize())) + .setCredentialType(CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY) + .setVersion("someVersion") + .build()) + .setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey)) + .build(); + + final GetExpiringProfileKeyCredentialResponse response = unauthenticatedServiceStub().getExpiringProfileKeyCredential(request); + + assertArrayEquals(credentialResponse.serialize(), response.getProfileKeyCredential().toByteArray()); + + verify(serverZkProfileOperations).issueExpiringProfileKeyCredential(credentialRequest, new ServiceId.Aci(targetUuid), profileKeyCommitment, expiration); + + final ClientZkProfileOperations clientZkProfileCipher = new ClientZkProfileOperations(serverPublicParams); + assertThatNoException().isThrownBy(() -> + clientZkProfileCipher.receiveExpiringProfileKeyCredential(profileKeyCredentialRequestContext, new ExpiringProfileKeyCredentialResponse(response.getProfileKeyCredential().toByteArray()))); + } + + @ParameterizedTest + @MethodSource + void getExpiringProfileKeyCredentialUnauthenticated(final boolean missingAccount, final boolean missingUnidentifiedAccessKey) { + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + final UUID targetUuid = UUID.randomUUID(); + + when(account.getUuid()).thenReturn(targetUuid); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(targetUuid))).thenReturn( + CompletableFuture.completedFuture(missingAccount ? Optional.empty() : Optional.of(account))); + + final GetExpiringProfileKeyCredentialAnonymousRequest.Builder requestBuilder = GetExpiringProfileKeyCredentialAnonymousRequest.newBuilder() + .setRequest(GetExpiringProfileKeyCredentialRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(targetUuid))) + .build()) + .setCredentialRequest(ByteString.copyFrom("credentialRequest".getBytes(StandardCharsets.UTF_8))) + .setCredentialType(CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY) + .setVersion("someVersion") + .build()); + + if (!missingUnidentifiedAccessKey) { + requestBuilder.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey)); + } + + assertStatusException(Status.UNAUTHENTICATED, () -> unauthenticatedServiceStub().getExpiringProfileKeyCredential(requestBuilder.build())); + + verifyNoInteractions(profilesManager); + } + + private static Stream getExpiringProfileKeyCredentialUnauthenticated() { + return Stream.of( + Arguments.of(true, false), + Arguments.of(false, true) + ); + } + + + @Test + void getExpiringProfileKeyCredentialProfileNotFound() { + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + final UUID targetUuid = UUID.randomUUID(); + + when(account.getUuid()).thenReturn(targetUuid); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(targetUuid))).thenReturn( + CompletableFuture.completedFuture(Optional.of(account))); + when(profilesManager.getAsync(targetUuid, "someVersion")).thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + final GetExpiringProfileKeyCredentialAnonymousRequest request = GetExpiringProfileKeyCredentialAnonymousRequest.newBuilder() + .setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey)) + .setRequest(GetExpiringProfileKeyCredentialRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(targetUuid))) + .build()) + .setCredentialRequest(ByteString.copyFrom("credentialRequest".getBytes(StandardCharsets.UTF_8))) + .setCredentialType(CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY) + .setVersion("someVersion") + .build()) + .build(); + + assertStatusException(Status.NOT_FOUND, () -> unauthenticatedServiceStub().getExpiringProfileKeyCredential(request)); + } + + @ParameterizedTest + @MethodSource + void getExpiringProfileKeyCredentialInvalidArgument(final IdentityType identityType, final CredentialType credentialType, + final boolean throwZkVerificationException) throws VerificationFailedException { + final UUID targetUuid = UUID.randomUUID(); + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + + if (throwZkVerificationException) { + when(serverZkProfileOperations.issueExpiringProfileKeyCredential(any(), any(), any(), any())).thenThrow(new VerificationFailedException()); + } + + final VersionedProfile profile = mock(VersionedProfile.class); + when(profile.commitment()).thenReturn("commitment".getBytes(StandardCharsets.UTF_8)); + when(account.getUuid()).thenReturn(targetUuid); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(targetUuid))).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + when(profilesManager.getAsync(targetUuid, "someVersion")).thenReturn(CompletableFuture.completedFuture(Optional.of(profile))); + + final GetExpiringProfileKeyCredentialAnonymousRequest request = GetExpiringProfileKeyCredentialAnonymousRequest.newBuilder() + .setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey)) + .setRequest(GetExpiringProfileKeyCredentialRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(identityType) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(targetUuid))) + .build()) + .setCredentialRequest(ByteString.copyFrom("credentialRequest".getBytes(StandardCharsets.UTF_8))) + .setCredentialType(credentialType) + .setVersion("someVersion") + .build()) + .build(); + + assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().getExpiringProfileKeyCredential(request)); + } + + private static Stream getExpiringProfileKeyCredentialInvalidArgument() { + return Stream.of( + // Credential type unspecified + Arguments.of(IdentityType.IDENTITY_TYPE_ACI, CredentialType.CREDENTIAL_TYPE_UNSPECIFIED, false), + // Illegal identity type + Arguments.of(IdentityType.IDENTITY_TYPE_PNI, CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY, false), + // Artificially fails zero knowledge verification + Arguments.of(IdentityType.IDENTITY_TYPE_ACI, CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY, true) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java new file mode 100644 index 000000000..bf570284b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java @@ -0,0 +1,726 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertRateLimitExceeded; +import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException; + +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.Phonenumber; +import com.google.protobuf.ByteString; +import io.grpc.Metadata; +import io.grpc.Status; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.signal.chat.common.IdentityType; +import org.signal.chat.common.ServiceIdentifier; +import org.signal.chat.profile.CredentialType; +import org.signal.chat.profile.GetExpiringProfileKeyCredentialRequest; +import org.signal.chat.profile.GetExpiringProfileKeyCredentialResponse; +import org.signal.chat.profile.GetUnversionedProfileRequest; +import org.signal.chat.profile.GetUnversionedProfileResponse; +import org.signal.chat.profile.GetVersionedProfileRequest; +import org.signal.chat.profile.GetVersionedProfileResponse; +import org.signal.chat.profile.ProfileGrpc; +import org.signal.chat.profile.SetProfileRequest; +import org.signal.chat.profile.SetProfileRequest.AvatarChange; +import org.signal.chat.profile.SetProfileResponse; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ServiceId; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.ServerPublicParams; +import org.signal.libsignal.zkgroup.ServerSecretParams; +import org.signal.libsignal.zkgroup.VerificationFailedException; +import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse; +import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; +import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; +import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequestContext; +import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; +import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum; +import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; +import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; +import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicPaymentsConfiguration; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.entities.Badge; +import org.whispersystems.textsecuregcm.entities.BadgeSvg; +import org.whispersystems.textsecuregcm.entities.UserCapabilities; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.s3.PolicySigner; +import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.storage.ProfilesManager; +import org.whispersystems.textsecuregcm.storage.VersionedProfile; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.tests.util.ProfileTestHelper; +import org.whispersystems.textsecuregcm.util.MockUtils; +import org.whispersystems.textsecuregcm.util.UUIDUtil; +import reactor.core.publisher.Mono; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; + +public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest { + + private static final String S3_BUCKET = "profileBucket"; + + private static final String VERSION = "someVersion"; + + private static final byte[] VALID_NAME = new byte[81]; + + @Mock + private AccountsManager accountsManager; + + @Mock + private ProfilesManager profilesManager; + + @Mock + private DynamicPaymentsConfiguration dynamicPaymentsConfiguration; + + @Mock + private S3AsyncClient asyncS3client; + + @Mock + private VersionedProfile profile; + + @Mock + private Account account; + + @Mock + private RateLimiter rateLimiter; + + @Mock + private ProfileBadgeConverter profileBadgeConverter; + + @Mock + private ServerZkProfileOperations serverZkProfileOperations; + + @Override + protected ProfileGrpcService createServiceBeforeEachTest() { + @SuppressWarnings("unchecked") final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); + final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); + final PolicySigner policySigner = new PolicySigner("accessSecret", "us-west-1"); + final PostPolicyGenerator policyGenerator = new PostPolicyGenerator("us-west-1", "profile-bucket", "accessKey"); + final BadgesConfiguration badgesConfiguration = new BadgesConfiguration( + List.of(new BadgeConfiguration( + "TEST", + "other", + List.of("l", "m", "h", "x", "xx", "xxx"), + "SVG", + List.of( + new BadgeSvg("sl", "sd"), + new BadgeSvg("ml", "md"), + new BadgeSvg("ll", "ld") + ) + )), + List.of("TEST1"), + Map.of(1L, "TEST1", 2L, "TEST2", 3L, "TEST3") + ); + final RateLimiters rateLimiters = mock(RateLimiters.class); + final String phoneNumber = PhoneNumberUtil.getInstance().format( + PhoneNumberUtil.getInstance().getExampleNumber("US"), + PhoneNumberUtil.PhoneNumberFormat.E164); + final Metadata metadata = new Metadata(); + metadata.put(AcceptLanguageInterceptor.ACCEPTABLE_LANGUAGES_GRPC_HEADER, "en-us"); + metadata.put(UserAgentInterceptor.USER_AGENT_GRPC_HEADER, "Signal-Android/1.2.3"); + + when(rateLimiters.getProfileLimiter()).thenReturn(rateLimiter); + when(rateLimiter.validateReactive(any(UUID.class))).thenReturn(Mono.empty()); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + when(dynamicConfiguration.getPaymentsConfiguration()).thenReturn(dynamicPaymentsConfiguration); + + when(account.getUuid()).thenReturn(AUTHENTICATED_ACI); + when(account.getNumber()).thenReturn(phoneNumber); + when(account.getBadges()).thenReturn(Collections.emptyList()); + + when(profile.paymentAddress()).thenReturn(null); + when(profile.avatar()).thenReturn(""); + + when(accountsManager.getByAccountIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + when(accountsManager.updateAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + when(profilesManager.getAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(Optional.of(profile))); + when(profilesManager.setAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + when(dynamicConfiguration.getPaymentsConfiguration()).thenReturn(dynamicPaymentsConfiguration); + when(dynamicPaymentsConfiguration.getDisallowedPrefixes()).thenReturn(Collections.emptyList()); + + when(asyncS3client.deleteObject(any(DeleteObjectRequest.class))).thenReturn(CompletableFuture.completedFuture(null)); + + return new ProfileGrpcService( + Clock.systemUTC(), + accountsManager, + profilesManager, + dynamicConfigurationManager, + badgesConfiguration, + asyncS3client, + policyGenerator, + policySigner, + profileBadgeConverter, + rateLimiters, + serverZkProfileOperations, + S3_BUCKET + ); + } + + @Test + void setProfile() throws InvalidInputException { + final byte[] commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AUTHENTICATED_ACI)).serialize(); + final byte[] validAboutEmoji = new byte[60]; + final byte[] validAbout = new byte[540]; + final byte[] validPaymentAddress = new byte[582]; + + final SetProfileRequest request = SetProfileRequest.newBuilder() + .setVersion(VERSION) + .setName(ByteString.copyFrom(VALID_NAME)) + .setAvatarChange(AvatarChange.AVATAR_CHANGE_UNCHANGED) + .setAboutEmoji(ByteString.copyFrom(validAboutEmoji)) + .setAbout(ByteString.copyFrom(validAbout)) + .setPaymentAddress(ByteString.copyFrom(validPaymentAddress)) + .setCommitment(ByteString.copyFrom(commitment)) + .build(); + + authenticatedServiceStub().setProfile(request); + + final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager).setAsync(eq(account.getUuid()), profileArgumentCaptor.capture()); + + final VersionedProfile profile = profileArgumentCaptor.getValue(); + + assertThat(profile.commitment()).isEqualTo(commitment); + assertThat(profile.avatar()).isNull(); + assertThat(profile.version()).isEqualTo(VERSION); + assertThat(profile.name()).isEqualTo(VALID_NAME); + assertThat(profile.aboutEmoji()).isEqualTo(validAboutEmoji); + assertThat(profile.about()).isEqualTo(validAbout); + assertThat(profile.paymentAddress()).isEqualTo(validPaymentAddress); + } + + @ParameterizedTest + @MethodSource + void setProfileUpload(final AvatarChange avatarChange, final boolean hasPreviousProfile, + final boolean expectHasS3UploadPath, final boolean expectDeleteS3Object) throws InvalidInputException { + final String currentAvatar = "profiles/currentAvatar"; + final byte[] commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AUTHENTICATED_ACI)).serialize(); + + final SetProfileRequest request = SetProfileRequest.newBuilder() + .setVersion(VERSION) + .setName(ByteString.copyFrom(VALID_NAME)) + .setAvatarChange(avatarChange) + .setCommitment(ByteString.copyFrom(commitment)) + .build(); + + when(profile.avatar()).thenReturn(currentAvatar); + + when(profilesManager.getAsync(any(), anyString())).thenReturn(CompletableFuture.completedFuture( + hasPreviousProfile ? Optional.of(profile) : Optional.empty())); + when(profilesManager.setAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + final SetProfileResponse response = authenticatedServiceStub().setProfile(request); + + if (expectHasS3UploadPath) { + assertTrue(response.getAttributes().getPath().startsWith("profiles/")); + } else { + assertEquals(response.getAttributes().getPath(), ""); + } + + if (expectDeleteS3Object) { + verify(asyncS3client).deleteObject(DeleteObjectRequest.builder() + .bucket(S3_BUCKET) + .key(currentAvatar) + .build()); + } else { + verifyNoInteractions(asyncS3client); + } + } + + private static Stream setProfileUpload() { + return Stream.of( + // Upload new avatar, no previous avatar + Arguments.of(AvatarChange.AVATAR_CHANGE_UPDATE, false, true, false), + // Upload new avatar, has previous avatar + Arguments.of(AvatarChange.AVATAR_CHANGE_UPDATE, true, true, true), + // Clear avatar on profile, no previous avatar + Arguments.of(AvatarChange.AVATAR_CHANGE_CLEAR, false, false, false), + // Clear avatar on profile, has previous avatar + Arguments.of(AvatarChange.AVATAR_CHANGE_CLEAR, true, false, true), + // Set same avatar, no previous avatar + Arguments.of(AvatarChange.AVATAR_CHANGE_UNCHANGED, false, false, false), + // Set same avatar, has previous avatar + Arguments.of(AvatarChange.AVATAR_CHANGE_UNCHANGED, true, false, false) + ); + } + + @ParameterizedTest + @MethodSource + void setProfileInvalidRequestData(final SetProfileRequest request) { + assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().setProfile(request)); + } + + private static Stream setProfileInvalidRequestData() throws InvalidInputException{ + final byte[] commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID_TWO)).serialize(); + final byte[] invalidValue = new byte[42]; + + final SetProfileRequest prototypeRequest = SetProfileRequest.newBuilder() + .setVersion(VERSION) + .setName(ByteString.copyFrom(VALID_NAME)) + .setCommitment(ByteString.copyFrom(commitment)) + .build(); + + return Stream.of( + // Missing version + Arguments.of(SetProfileRequest.newBuilder(prototypeRequest) + .clearVersion() + .build()), + // Missing name + Arguments.of(SetProfileRequest.newBuilder(prototypeRequest) + .clearName() + .build()), + // Invalid name length + Arguments.of(SetProfileRequest.newBuilder(prototypeRequest) + .setName(ByteString.copyFrom(invalidValue)) + .build()), + // Invalid about emoji length + Arguments.of(SetProfileRequest.newBuilder(prototypeRequest) + .setAboutEmoji(ByteString.copyFrom(invalidValue)) + .build()), + // Invalid about length + Arguments.of(SetProfileRequest.newBuilder(prototypeRequest) + .setAbout(ByteString.copyFrom(invalidValue)) + .build()), + // Invalid payment address + Arguments.of(SetProfileRequest.newBuilder(prototypeRequest) + .setPaymentAddress(ByteString.copyFrom(invalidValue)) + .build()), + // Missing profile commitment + Arguments.of(SetProfileRequest.newBuilder() + .clearCommitment() + .build()) + ); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void setPaymentAddressDisallowedCountry(final boolean hasExistingPaymentAddress) throws InvalidInputException { + final Phonenumber.PhoneNumber disallowedPhoneNumber = PhoneNumberUtil.getInstance().getExampleNumber("CU"); + final byte[] commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AUTHENTICATED_ACI)).serialize(); + + final byte[] validPaymentAddress = new byte[582]; + if (hasExistingPaymentAddress) { + when(profile.paymentAddress()).thenReturn(validPaymentAddress); + } + + final SetProfileRequest request = SetProfileRequest.newBuilder() + .setVersion(VERSION) + .setName(ByteString.copyFrom(VALID_NAME)) + .setAvatarChange(AvatarChange.AVATAR_CHANGE_UNCHANGED) + .setPaymentAddress(ByteString.copyFrom(validPaymentAddress)) + .setCommitment(ByteString.copyFrom(commitment)) + .build(); + final String disallowedCountryCode = String.format("+%d", disallowedPhoneNumber.getCountryCode()); + when(dynamicPaymentsConfiguration.getDisallowedPrefixes()).thenReturn(List.of(disallowedCountryCode)); + when(account.getNumber()).thenReturn(PhoneNumberUtil.getInstance().format( + disallowedPhoneNumber, + PhoneNumberUtil.PhoneNumberFormat.E164)); + when(profilesManager.getAsync(any(), anyString())).thenReturn(CompletableFuture.completedFuture(Optional.of(profile))); + + if (hasExistingPaymentAddress) { + assertDoesNotThrow(() -> authenticatedServiceStub().setProfile(request), + "Payment address changes in disallowed countries should still be allowed if the account already has a valid payment address"); + } else { + assertStatusException(Status.PERMISSION_DENIED, () -> authenticatedServiceStub().setProfile(request)); + } + } + + @ParameterizedTest + @EnumSource(value = org.signal.chat.common.IdentityType.class, names = {"IDENTITY_TYPE_ACI", "IDENTITY_TYPE_PNI"}) + void getUnversionedProfile(final IdentityType identityType) { + final UUID targetUuid = UUID.randomUUID(); + final org.whispersystems.textsecuregcm.identity.ServiceIdentifier targetIdentifier = + identityType == IdentityType.IDENTITY_TYPE_ACI ? new AciServiceIdentifier(targetUuid) : new PniServiceIdentifier(targetUuid); + + final GetUnversionedProfileRequest request = GetUnversionedProfileRequest.newBuilder() + .setServiceIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(identityType) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(targetUuid))) + .build()) + .build(); + final byte[] unidentifiedAccessKey = new byte[16]; + new SecureRandom().nextBytes(unidentifiedAccessKey); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); + + final List badges = List.of(new Badge( + "TEST", + "other", + "Test Badge", + "This badge is in unit tests.", + List.of("l", "m", "h", "x", "xx", "xxx"), + "SVG", + List.of( + new BadgeSvg("sl", "sd"), + new BadgeSvg("ml", "md"), + new BadgeSvg("ll", "ld"))) + ); + + when(account.getIdentityKey(IdentityTypeUtil.fromGrpcIdentityType(identityType))).thenReturn(identityKey); + when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(true); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); + when(account.getBadges()).thenReturn(Collections.emptyList()); + when(profileBadgeConverter.convert(any(), any(), anyBoolean())).thenReturn(badges); + when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + final GetUnversionedProfileResponse response = authenticatedServiceStub().getUnversionedProfile(request); + + final byte[] unidentifiedAccessChecksum = UnidentifiedAccessChecksum.generateFor(unidentifiedAccessKey); + final GetUnversionedProfileResponse prototypeExpectedResponse = GetUnversionedProfileResponse.newBuilder() + .setIdentityKey(ByteString.copyFrom(identityKey.serialize())) + .setUnidentifiedAccess(ByteString.copyFrom(unidentifiedAccessChecksum)) + .setUnrestrictedUnidentifiedAccess(true) + .setCapabilities(ProfileGrpcHelper.buildUserCapabilities(UserCapabilities.createForAccount(account))) + .addAllBadges(ProfileGrpcHelper.buildBadges(badges)) + .build(); + + final GetUnversionedProfileResponse expectedResponse; + if (identityType == IdentityType.IDENTITY_TYPE_PNI) { + expectedResponse = GetUnversionedProfileResponse.newBuilder(prototypeExpectedResponse) + .clearUnidentifiedAccess() + .clearBadges() + .setUnrestrictedUnidentifiedAccess(false) + .build(); + } else { + expectedResponse = prototypeExpectedResponse; + } + + verify(rateLimiter).validateReactive(AUTHENTICATED_ACI); + verify(accountsManager).getByServiceIdentifierAsync(targetIdentifier); + + assertEquals(expectedResponse, response); + } + + @Test + void getUnversionedProfileTargetAccountNotFound() { + when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + final GetUnversionedProfileRequest request = GetUnversionedProfileRequest.newBuilder() + .setServiceIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID()))) + .build()) + .build(); + + assertStatusException(Status.NOT_FOUND, () -> authenticatedServiceStub().getUnversionedProfile(request)); + } + + @ParameterizedTest + @EnumSource(value = org.signal.chat.common.IdentityType.class, names = {"IDENTITY_TYPE_ACI", "IDENTITY_TYPE_PNI"}) + void getUnversionedProfileRatelimited(final IdentityType identityType) { + final Duration retryAfterDuration = Duration.ofMinutes(7); + when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + when(rateLimiter.validateReactive(any(UUID.class))) + .thenReturn(Mono.error(new RateLimitExceededException(retryAfterDuration, false))); + + final GetUnversionedProfileRequest request = GetUnversionedProfileRequest.newBuilder() + .setServiceIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(identityType) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID()))) + .build()) + .build(); + + assertRateLimitExceeded(retryAfterDuration, () -> authenticatedServiceStub().getUnversionedProfile(request), accountsManager); + } + + @ParameterizedTest + @MethodSource + void getVersionedProfile(final String requestVersion, @Nullable final String accountVersion, final boolean expectResponseHasPaymentAddress) { + final VersionedProfile profile = mock(VersionedProfile.class); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] emoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] about = ProfileTestHelper.generateRandomByteArray(156); + final byte[] paymentAddress = ProfileTestHelper.generateRandomByteArray(582); + final String avatar = "profiles/" + ProfileTestHelper.generateRandomBase64FromByteArray(16); + + final GetVersionedProfileRequest request = GetVersionedProfileRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID()))) + .build()) + .setVersion(requestVersion) + .build(); + + when(profile.name()).thenReturn(name); + when(profile.about()).thenReturn(about); + when(profile.aboutEmoji()).thenReturn(emoji); + when(profile.avatar()).thenReturn(avatar); + when(profile.paymentAddress()).thenReturn(paymentAddress); + + when(account.getCurrentProfileVersion()).thenReturn(Optional.ofNullable(accountVersion)); + when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + when(profilesManager.getAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(Optional.of(profile))); + + final GetVersionedProfileResponse response = authenticatedServiceStub().getVersionedProfile(request); + + final GetVersionedProfileResponse.Builder expectedResponseBuilder = GetVersionedProfileResponse.newBuilder() + .setName(ByteString.copyFrom(name)) + .setAbout(ByteString.copyFrom(about)) + .setAboutEmoji(ByteString.copyFrom(emoji)) + .setAvatar(avatar); + + if (expectResponseHasPaymentAddress) { + expectedResponseBuilder.setPaymentAddress(ByteString.copyFrom(paymentAddress)); + } + + assertEquals(expectedResponseBuilder.build(), response); + } + private static Stream getVersionedProfile() { + return Stream.of( + Arguments.of("version1", "version1", true), + Arguments.of("version1", null, true), + Arguments.of("version1", "version2", false) + ); + } + + @ParameterizedTest + @MethodSource + void getVersionedProfileAccountOrProfileNotFound(final boolean missingAccount, final boolean missingProfile) { + final GetVersionedProfileRequest request = GetVersionedProfileRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID()))) + .build()) + .setVersion("versionWithNoProfile") + .build(); + when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(missingAccount ? Optional.empty() : Optional.of(account))); + when(profilesManager.getAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(missingProfile ? Optional.empty() : Optional.of(profile))); + + assertStatusException(Status.NOT_FOUND, () -> authenticatedServiceStub().getVersionedProfile(request)); + } + + private static Stream getVersionedProfileAccountOrProfileNotFound() { + return Stream.of( + Arguments.of(true, false), + Arguments.of(false, true) + ); + } + + @Test + void getVersionedProfileRatelimited() { + final Duration retryAfterDuration = MockUtils.updateRateLimiterResponseToFail(rateLimiter, AUTHENTICATED_ACI, Duration.ofMinutes(7), false); + + final GetVersionedProfileRequest request = GetVersionedProfileRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID()))) + .build()) + .setVersion("someVersion") + .build(); + + assertRateLimitExceeded(retryAfterDuration, () -> authenticatedServiceStub().getVersionedProfile(request), accountsManager, profilesManager); + } + + @Test + void getVersionedProfilePniInvalidArgument() { + final GetVersionedProfileRequest request = GetVersionedProfileRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_PNI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID()))) + .build()) + .setVersion("someVersion") + .build(); + + assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().getVersionedProfile(request)); + } + + @Test + void getExpiringProfileKeyCredential() throws InvalidInputException, VerificationFailedException { + final UUID targetUuid = UUID.randomUUID(); + + final ServerSecretParams serverSecretParams = ServerSecretParams.generate(); + final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams(); + + final ServerZkProfileOperations serverZkProfile = new ServerZkProfileOperations(serverSecretParams); + final ClientZkProfileOperations clientZkProfile = new ClientZkProfileOperations(serverPublicParams); + + final byte[] profileKeyBytes = new byte[32]; + new SecureRandom().nextBytes(profileKeyBytes); + + final ProfileKey profileKey = new ProfileKey(profileKeyBytes); + final ProfileKeyCommitment profileKeyCommitment = profileKey.getCommitment(new ServiceId.Aci(targetUuid)); + final ProfileKeyCredentialRequestContext profileKeyCredentialRequestContext = + clientZkProfile.createProfileKeyCredentialRequestContext(new ServiceId.Aci(targetUuid), profileKey); + + when(account.getUuid()).thenReturn(targetUuid); + when(profile.commitment()).thenReturn(profileKeyCommitment.serialize()); + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(targetUuid))).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + when(profilesManager.getAsync(targetUuid, "someVersion")).thenReturn(CompletableFuture.completedFuture(Optional.of(profile))); + + final ProfileKeyCredentialRequest credentialRequest = profileKeyCredentialRequestContext.getRequest(); + + final Instant expiration = Instant.now().plus(org.whispersystems.textsecuregcm.util.ProfileHelper.EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION) + .truncatedTo(ChronoUnit.DAYS); + + final ExpiringProfileKeyCredentialResponse credentialResponse = + serverZkProfile.issueExpiringProfileKeyCredential(credentialRequest, new ServiceId.Aci(targetUuid), profileKeyCommitment, expiration); + + when(serverZkProfileOperations.issueExpiringProfileKeyCredential(credentialRequest, new ServiceId.Aci(targetUuid), profileKeyCommitment, expiration)) + .thenReturn(credentialResponse); + + final GetExpiringProfileKeyCredentialRequest request = GetExpiringProfileKeyCredentialRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(targetUuid))) + .build()) + .setCredentialRequest(ByteString.copyFrom(credentialRequest.serialize())) + .setCredentialType(CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY) + .setVersion("someVersion") + .build(); + + final GetExpiringProfileKeyCredentialResponse response = authenticatedServiceStub().getExpiringProfileKeyCredential(request); + + assertArrayEquals(credentialResponse.serialize(), response.getProfileKeyCredential().toByteArray()); + + verify(serverZkProfileOperations).issueExpiringProfileKeyCredential(credentialRequest, new ServiceId.Aci(targetUuid), profileKeyCommitment, expiration); + + final ClientZkProfileOperations clientZkProfileCipher = new ClientZkProfileOperations(serverPublicParams); + assertThatNoException().isThrownBy(() -> + clientZkProfileCipher.receiveExpiringProfileKeyCredential(profileKeyCredentialRequestContext, new ExpiringProfileKeyCredentialResponse(response.getProfileKeyCredential().toByteArray()))); + } + + @Test + void getExpiringProfileKeyCredentialRateLimited() { + final Duration retryAfterDuration = MockUtils.updateRateLimiterResponseToFail( + rateLimiter, AUTHENTICATED_ACI, Duration.ofMinutes(5), false); + when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + final GetExpiringProfileKeyCredentialRequest request = GetExpiringProfileKeyCredentialRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID()))) + .build()) + .setCredentialRequest(ByteString.copyFrom("credentialRequest".getBytes(StandardCharsets.UTF_8))) + .setCredentialType(CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY) + .setVersion("someVersion") + .build(); + + assertRateLimitExceeded(retryAfterDuration, () -> authenticatedServiceStub().getExpiringProfileKeyCredential(request), profilesManager); + } + + @ParameterizedTest + @MethodSource + void getExpiringProfileKeyCredentialAccountOrProfileNotFound(final boolean missingAccount, + final boolean missingProfile) { + final UUID targetUuid = UUID.randomUUID(); + + when(account.getUuid()).thenReturn(targetUuid); + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(targetUuid))).thenReturn(CompletableFuture.completedFuture( + missingAccount ? Optional.empty() : Optional.of(account))); + when(profilesManager.getAsync(targetUuid, "someVersion")).thenReturn(CompletableFuture.completedFuture(missingProfile ? Optional.empty() : Optional.of(profile))); + + final GetExpiringProfileKeyCredentialRequest request = GetExpiringProfileKeyCredentialRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(targetUuid))) + .build()) + .setCredentialRequest(ByteString.copyFrom("credentialRequest".getBytes(StandardCharsets.UTF_8))) + .setCredentialType(CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY) + .setVersion("someVersion") + .build(); + + assertStatusException(Status.NOT_FOUND, () -> authenticatedServiceStub().getExpiringProfileKeyCredential(request)); + } + + private static Stream getExpiringProfileKeyCredentialAccountOrProfileNotFound() { + return Stream.of( + Arguments.of(true, false), + Arguments.of(false, true) + ); + } + + @ParameterizedTest + @MethodSource + void getExpiringProfileKeyCredentialInvalidArgument(final IdentityType identityType, final CredentialType credentialType, + final boolean throwZkVerificationException) throws VerificationFailedException { + final UUID targetUuid = UUID.randomUUID(); + + if (throwZkVerificationException) { + when(serverZkProfileOperations.issueExpiringProfileKeyCredential(any(), any(), any(), any())).thenThrow(new VerificationFailedException()); + } + + when(account.getUuid()).thenReturn(targetUuid); + when(profile.commitment()).thenReturn("commitment".getBytes(StandardCharsets.UTF_8)); + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(targetUuid))).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + when(profilesManager.getAsync(targetUuid, "someVersion")).thenReturn(CompletableFuture.completedFuture(Optional.of(profile))); + + final GetExpiringProfileKeyCredentialRequest request = GetExpiringProfileKeyCredentialRequest.newBuilder() + .setAccountIdentifier(ServiceIdentifier.newBuilder() + .setIdentityType(identityType) + .setUuid(ByteString.copyFrom(UUIDUtil.toBytes(targetUuid))) + .build()) + .setCredentialRequest(ByteString.copyFrom("credentialRequest".getBytes(StandardCharsets.UTF_8))) + .setCredentialType(credentialType) + .setVersion("someVersion") + .build(); + + assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().getExpiringProfileKeyCredential(request)); + } + + private static Stream getExpiringProfileKeyCredentialInvalidArgument() { + return Stream.of( + // Credential type unspecified + Arguments.of(IdentityType.IDENTITY_TYPE_ACI, CredentialType.CREDENTIAL_TYPE_UNSPECIFIED, false), + // Illegal identity type + Arguments.of(IdentityType.IDENTITY_TYPE_PNI, CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY, false), + // Artificially fails zero knowledge verification + Arguments.of(IdentityType.IDENTITY_TYPE_ACI, CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY, true) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/SimpleBaseGrpcTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/SimpleBaseGrpcTest.java new file mode 100644 index 000000000..8d3907022 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/SimpleBaseGrpcTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static java.util.Objects.requireNonNull; + +import io.grpc.BindableService; +import io.grpc.Channel; +import io.grpc.stub.AbstractBlockingStub; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.UUID; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.MockitoAnnotations; +import org.whispersystems.textsecuregcm.auth.grpc.MockAuthenticationInterceptor; +import org.whispersystems.textsecuregcm.storage.Device; + +/** + * Base class for the common case of gRPC services tests. This base class makes some assumptions + * and introduces some constraints on the implementing classes with a goal of simplifying the process + * of creating a test for the most of the gRPC services. + *

      + *
    • + * Test classes extending this class will have to override the {@link #createServiceBeforeEachTest()} method + * with the logic that creates an instance of the service to test. This method is called before each test and should + * contain other setup code that would normally go into {@code @BeforeEach} method. + *
    • + *
    • + * This base class takes care of creating two service stubs: {@code authenticatedServiceStub} and {@code unauthenticatedServiceStub}. + * Normally, those stubs are created by the call to the {@code newBlockingStub()} method on the {@code *Stub} class, e.g.: + *
      CallingGrpc.newBlockingStub(GRPC_SERVER_EXTENSION_AUTHENTICATED.getChannel());
      + * In this class, those stubs are created by the {@link #createStub(Channel)} method that has a default implementation that is based on + * figuring out the name of the {@code `*Grpc`} class and invoking {@code `*Grpc.newBlockingStub()`} method with reflection. + *
    • + *
    • + * This class takes care of initializing {@code Mockito} annotations processing, so implementing classes + * can annotate their fields with {@code @Mock} and have those mocks ready by the time {@link #createServiceBeforeEachTest()} is called. + *
    • + *
    + * @param Class of the gRPC service that is being tested. + * @param Class of the gRPC service stub. + */ +public abstract class SimpleBaseGrpcTest> { + + @RegisterExtension + protected static final GrpcServerExtension GRPC_SERVER_EXTENSION_AUTHENTICATED = new GrpcServerExtension(); + + @RegisterExtension + protected static final GrpcServerExtension GRPC_SERVER_EXTENSION_UNAUTHENTICATED = new GrpcServerExtension(); + + protected static final UUID AUTHENTICATED_ACI = UUID.randomUUID(); + + protected static final long AUTHENTICATED_DEVICE_ID = Device.MASTER_ID; + + private AutoCloseable mocksCloseable; + + private MockAuthenticationInterceptor mockAuthenticationInterceptor; + + private SERVICE service; + + private STUB authenticatedServiceStub; + + private STUB unauthenticatedServiceStub; + + + /** + * This method is invoked before each test and is expected to create an instance of the gRPC service + * that is being tested and also to perform all necessary before-each setup. + *

    + * Extending classes may have their own {@code @BeforeEach} method, but it will be called after this method. + * @return an instance of the gRPC service. + */ + protected abstract SERVICE createServiceBeforeEachTest(); + + /** + * The default implementation of this method is based on figuring out the name of the {@code `*Grpc`} class + * and invoking {@code `*Grpc.newBlockingStub()`} method with reflection. + *

    + * Overriding this method can be helpful if addutional configuration of the stub is required, e.g. adding interceptors: + *

    +   * protected ProfileAnonymousGrpc.ProfileAnonymousBlockingStub createStub(final Channel channel) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException {
    +   *   final Metadata metadata = new Metadata();
    +   *   metadata.put(AcceptLanguageInterceptor.ACCEPTABLE_LANGUAGES_GRPC_HEADER, "en-us");
    +   *   metadata.put(UserAgentInterceptor.USER_AGENT_GRPC_HEADER, "Signal-Android/1.2.3");
    +   *   return super.createStub(channel)
    +   *       .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
    +   * }
    +   * 
    + * @param channel grpc channel to create create the stub for. + * @return and instance of the service stub. + */ + protected STUB createStub(final Channel channel) throws + ClassNotFoundException, + NoSuchMethodException, + InvocationTargetException, + IllegalAccessException { + final String serviceClassName = service.bindService().getServiceDescriptor().getName(); + final String grpcClassName = serviceClassName + "Grpc"; + final Class grpcClass = ClassLoader.getSystemClassLoader().loadClass(grpcClassName); + final Method newBlockingStubMethod = grpcClass.getMethod("newBlockingStub", Channel.class); + final Object stub = newBlockingStubMethod.invoke(null, channel); + //noinspection unchecked + return (STUB) stub; + } + + @BeforeEach + protected void baseSetup() { + mocksCloseable = MockitoAnnotations.openMocks(this); + service = requireNonNull(createServiceBeforeEachTest(), "created service must not be `null`"); + mockAuthenticationInterceptor = GrpcTestUtils.setupAuthenticatedExtension( + GRPC_SERVER_EXTENSION_AUTHENTICATED, AUTHENTICATED_ACI, AUTHENTICATED_DEVICE_ID, service); + GrpcTestUtils.setupUnauthenticatedExtension(GRPC_SERVER_EXTENSION_UNAUTHENTICATED, service); + try { + authenticatedServiceStub = createStub(GRPC_SERVER_EXTENSION_AUTHENTICATED.getChannel()); + unauthenticatedServiceStub = createStub(GRPC_SERVER_EXTENSION_UNAUTHENTICATED.getChannel()); + } catch (Exception e) { + throw new RuntimeException("Could not create a stub based on the service name. Try overriding `createStub()` method."); + } + } + + @AfterEach + public void releaseMocks() throws Exception { + mocksCloseable.close(); + } + + public MockAuthenticationInterceptor mockAuthenticationInterceptor() { + return mockAuthenticationInterceptor; + } + + protected SERVICE service() { + return service; + } + + protected STUB authenticatedServiceStub() { + return authenticatedServiceStub; + } + + protected STUB unauthenticatedServiceStub() { + return unauthenticatedServiceStub; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/UserAgentInterceptorTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/UserAgentInterceptorTest.java new file mode 100644 index 000000000..b338e03f1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/grpc/UserAgentInterceptorTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.google.protobuf.ByteString; +import com.vdurmont.semver4j.Semver; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.MetadataUtils; +import io.grpc.stub.StreamObserver; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.signal.chat.rpc.EchoRequest; +import org.signal.chat.rpc.EchoResponse; +import org.signal.chat.rpc.EchoServiceGrpc; +import org.whispersystems.textsecuregcm.grpc.EchoServiceImpl; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; +import org.whispersystems.textsecuregcm.util.ua.UserAgent; +import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +public class UserAgentInterceptorTest { + + @ParameterizedTest + @MethodSource + void testInterceptor(final String header, final ClientPlatform platform, final String version) throws Exception { + + final AtomicReference observedUserAgent = new AtomicReference<>(null); + final EchoServiceImpl serviceImpl = new EchoServiceImpl() { + @Override + public void echo(EchoRequest req, StreamObserver responseObserver) { + observedUserAgent.set(UserAgentUtil.userAgentFromGrpcContext()); + super.echo(req, responseObserver); + } + }; + + final Server testServer = InProcessServerBuilder.forName("RemoteDeprecationFilterTest") + .directExecutor() + .addService(serviceImpl) + .intercept(new UserAgentInterceptor()) + .build() + .start(); + + try { + final ManagedChannel channel = InProcessChannelBuilder.forName("RemoteDeprecationFilterTest") + .directExecutor() + .userAgent(header) + .build(); + + final EchoServiceGrpc.EchoServiceBlockingStub client = EchoServiceGrpc.newBlockingStub(channel); + + final EchoRequest req = EchoRequest.newBuilder().setPayload(ByteString.copyFromUtf8("cluck cluck, i'm a parrot")).build(); + assertEquals("cluck cluck, i'm a parrot", client.echo(req).getPayload().toStringUtf8()); + if (platform == null) { + assertNull(observedUserAgent.get()); + } else { + assertEquals(platform, observedUserAgent.get().getPlatform()); + assertEquals(new Semver(version), observedUserAgent.get().getVersion()); + // can't assert on the additional specifiers because they include internal details of the grpc in-process channel itself + } + } finally { + testServer.shutdownNow(); + testServer.awaitTermination(); + } + } + + private static Stream testInterceptor() { + return Stream.of( + Arguments.of(null, null, null), + Arguments.of("", null, null), + Arguments.of("Unrecognized UA", null, null), + Arguments.of("Signal-Android/4.68.3", ClientPlatform.ANDROID, "4.68.3"), + Arguments.of("Signal-iOS/3.9.0", ClientPlatform.IOS, "3.9.0"), + Arguments.of("Signal-Desktop/1.2.3", ClientPlatform.DESKTOP, "1.2.3"), + Arguments.of("Signal-Desktop/8.0.0-beta.2", ClientPlatform.DESKTOP, "8.0.0-beta.2"), + Arguments.of("Signal-iOS/8.0.0-beta.2", ClientPlatform.IOS, "8.0.0-beta.2")); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/http/FaultTolerantHttpClientTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/http/FaultTolerantHttpClientTest.java new file mode 100644 index 000000000..f1380dfff --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/http/FaultTolerantHttpClientTest.java @@ -0,0 +1,219 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.http; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import io.github.resilience4j.circuitbreaker.CallNotPermittedException; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; +import org.whispersystems.textsecuregcm.configuration.RetryConfiguration; + +class FaultTolerantHttpClientTest { + + @RegisterExtension + private final WireMockExtension wireMock = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort().dynamicHttpsPort()) + .build(); + + private ExecutorService httpExecutor; + private ScheduledExecutorService retryExecutor; + + @BeforeEach + void setUp() { + httpExecutor = Executors.newSingleThreadExecutor(); + retryExecutor = Executors.newSingleThreadScheduledExecutor(); + } + + @AfterEach + void tearDown() throws InterruptedException { + httpExecutor.shutdown(); + httpExecutor.awaitTermination(1, TimeUnit.SECONDS); + retryExecutor.shutdown(); + retryExecutor.awaitTermination(1, TimeUnit.SECONDS); + } + + @Test + void testSimpleGet() { + wireMock.stubFor(get(urlEqualTo("/ping")) + .willReturn(aResponse() + .withHeader("Content-Type", "text/plain") + .withBody("Pong!"))); + + FaultTolerantHttpClient client = FaultTolerantHttpClient.newBuilder() + .withCircuitBreaker(new CircuitBreakerConfiguration()) + .withRetry(new RetryConfiguration()) + .withExecutor(httpExecutor) + .withRetryExecutor(retryExecutor) + .withName("test") + .withVersion(HttpClient.Version.HTTP_2) + .build(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + wireMock.getPort() + "/ping")) + .GET() + .build(); + + HttpResponse response = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join(); + + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body()).isEqualTo("Pong!"); + + wireMock.verify(1, getRequestedFor(urlEqualTo("/ping"))); + } + + @Test + void testRetryGet() { + wireMock.stubFor(get(urlEqualTo("/failure")) + .willReturn(aResponse() + .withStatus(500) + .withHeader("Content-Type", "text/plain") + .withBody("Pong!"))); + + FaultTolerantHttpClient client = FaultTolerantHttpClient.newBuilder() + .withCircuitBreaker(new CircuitBreakerConfiguration()) + .withRetry(new RetryConfiguration()) + .withExecutor(httpExecutor) + .withRetryExecutor(retryExecutor) + .withName("test") + .withVersion(HttpClient.Version.HTTP_2) + .build(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + wireMock.getPort() + "/failure")) + .GET() + .build(); + + HttpResponse response = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join(); + + assertThat(response.statusCode()).isEqualTo(500); + assertThat(response.body()).isEqualTo("Pong!"); + + wireMock.verify(3, getRequestedFor(urlEqualTo("/failure"))); + } + + @Test + void testRetryGetOnException() { + final HttpClient mockHttpClient = mock(HttpClient.class); + final FaultTolerantHttpClient client = new FaultTolerantHttpClient( + "test", + mockHttpClient, + retryExecutor, + Duration.ofSeconds(1), + new RetryConfiguration(), + throwable -> throwable instanceof IOException, + new CircuitBreakerConfiguration()); + + when(mockHttpClient.sendAsync(any(), any())) + .thenReturn(CompletableFuture.failedFuture(new IOException("test exception"))); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:1234/failure")) + .GET() + .build(); + + try { + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join(); + throw new AssertionError("Should have failed!"); + } catch (CompletionException e) { + assertThat(e.getCause()).isInstanceOf(IOException.class); + } + verify(mockHttpClient, times(3)).sendAsync(any(), any()); + } + + @Test + void testNetworkFailureCircuitBreaker() throws InterruptedException { + CircuitBreakerConfiguration circuitBreakerConfiguration = new CircuitBreakerConfiguration(); + circuitBreakerConfiguration.setSlidingWindowSize(2); + circuitBreakerConfiguration.setSlidingWindowMinimumNumberOfCalls(2); + circuitBreakerConfiguration.setPermittedNumberOfCallsInHalfOpenState(1); + circuitBreakerConfiguration.setFailureRateThreshold(50); + circuitBreakerConfiguration.setWaitDurationInOpenStateInSeconds(1); + + FaultTolerantHttpClient client = FaultTolerantHttpClient.newBuilder() + .withCircuitBreaker(circuitBreakerConfiguration) + .withRetry(new RetryConfiguration()) + .withRetryExecutor(retryExecutor) + .withExecutor(httpExecutor) + .withName("test") + .withVersion(HttpClient.Version.HTTP_2) + .build(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + 39873 + "/failure")) + .GET() + .build(); + + try { + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join(); + throw new AssertionError("Should have failed!"); + } catch (CompletionException e) { + assertThat(e.getCause()).isInstanceOf(IOException.class); + // good + } + + try { + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join(); + throw new AssertionError("Should have failed!"); + } catch (CompletionException e) { + assertThat(e.getCause()).isInstanceOf(IOException.class); + // good + } + + try { + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join(); + throw new AssertionError("Should have failed!"); + } catch (CompletionException e) { + assertThat(e.getCause()).isInstanceOf(CallNotPermittedException.class); + // good + } + + Thread.sleep(1001); + + try { + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join(); + throw new AssertionError("Should have failed!"); + } catch (CompletionException e) { + assertThat(e.getCause()).isInstanceOf(IOException.class); + // good + } + + try { + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join(); + throw new AssertionError("Should have failed!"); + } catch (CompletionException e) { + assertThat(e.getCause()).isInstanceOf(CallNotPermittedException.class); + // good + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/identity/AciServiceIdentifierTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/identity/AciServiceIdentifierTest.java new file mode 100644 index 000000000..6cefcfe11 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/identity/AciServiceIdentifierTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.identity; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.ByteBuffer; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.util.UUIDUtil; + +class AciServiceIdentifierTest { + + @Test + void identityType() { + assertEquals(IdentityType.ACI, new AciServiceIdentifier(UUID.randomUUID()).identityType()); + } + + @Test + void toServiceIdentifierString() { + final UUID uuid = UUID.randomUUID(); + + assertEquals(uuid.toString(), new AciServiceIdentifier(uuid).toServiceIdentifierString()); + } + + @Test + void toCompactByteArray() { + final UUID uuid = UUID.randomUUID(); + + assertArrayEquals(UUIDUtil.toBytes(uuid), new AciServiceIdentifier(uuid).toCompactByteArray()); + } + + @Test + void toFixedWidthByteArray() { + final UUID uuid = UUID.randomUUID(); + + final ByteBuffer expectedBytesBuffer = ByteBuffer.allocate(17); + expectedBytesBuffer.put((byte) 0x00); + expectedBytesBuffer.putLong(uuid.getMostSignificantBits()); + expectedBytesBuffer.putLong(uuid.getLeastSignificantBits()); + expectedBytesBuffer.flip(); + + assertArrayEquals(expectedBytesBuffer.array(), new AciServiceIdentifier(uuid).toFixedWidthByteArray()); + } + + @Test + void valueOf() { + final UUID uuid = UUID.randomUUID(); + + assertEquals(uuid, AciServiceIdentifier.valueOf(uuid.toString()).uuid()); + assertEquals(uuid, AciServiceIdentifier.valueOf("ACI:" + uuid).uuid()); + assertThrows(IllegalArgumentException.class, () -> AciServiceIdentifier.valueOf("Not a valid UUID")); + assertThrows(IllegalArgumentException.class, () -> AciServiceIdentifier.valueOf("PNI:" + uuid)); + } + + @Test + void fromBytes() { + final UUID uuid = UUID.randomUUID(); + + assertEquals(uuid, AciServiceIdentifier.fromBytes(UUIDUtil.toBytes(uuid)).uuid()); + + final byte[] prefixedBytes = new byte[17]; + prefixedBytes[0] = 0x00; + System.arraycopy(UUIDUtil.toBytes(uuid), 0, prefixedBytes, 1, 16); + + assertEquals(uuid, AciServiceIdentifier.fromBytes(prefixedBytes).uuid()); + + prefixedBytes[0] = 0x01; + + assertThrows(IllegalArgumentException.class, () -> AciServiceIdentifier.fromBytes(prefixedBytes)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/identity/PniServiceIdentifierTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/identity/PniServiceIdentifierTest.java new file mode 100644 index 000000000..c53de62fc --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/identity/PniServiceIdentifierTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.identity; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.ByteBuffer; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.util.UUIDUtil; + +class PniServiceIdentifierTest { + + @Test + void identityType() { + assertEquals(IdentityType.PNI, new PniServiceIdentifier(UUID.randomUUID()).identityType()); + } + + @Test + void toServiceIdentifierString() { + final UUID uuid = UUID.randomUUID(); + + assertEquals("PNI:" + uuid, new PniServiceIdentifier(uuid).toServiceIdentifierString()); + } + + @Test + void toByteArray() { + final UUID uuid = UUID.randomUUID(); + + final ByteBuffer expectedBytesBuffer = ByteBuffer.allocate(17); + expectedBytesBuffer.put((byte) 0x01); + expectedBytesBuffer.putLong(uuid.getMostSignificantBits()); + expectedBytesBuffer.putLong(uuid.getLeastSignificantBits()); + expectedBytesBuffer.flip(); + + assertArrayEquals(expectedBytesBuffer.array(), new PniServiceIdentifier(uuid).toCompactByteArray()); + assertArrayEquals(expectedBytesBuffer.array(), new PniServiceIdentifier(uuid).toFixedWidthByteArray()); + } + + @Test + void valueOf() { + final UUID uuid = UUID.randomUUID(); + + assertEquals(uuid, PniServiceIdentifier.valueOf("PNI:" + uuid).uuid()); + assertThrows(IllegalArgumentException.class, () -> PniServiceIdentifier.valueOf(uuid.toString())); + assertThrows(IllegalArgumentException.class, () -> PniServiceIdentifier.valueOf("Not a valid UUID")); + assertThrows(IllegalArgumentException.class, () -> PniServiceIdentifier.valueOf("ACI:" + uuid)); + } + + @Test + void fromBytes() { + final UUID uuid = UUID.randomUUID(); + + assertThrows(IllegalArgumentException.class, () -> PniServiceIdentifier.fromBytes(UUIDUtil.toBytes(uuid))); + + final byte[] prefixedBytes = new byte[17]; + prefixedBytes[0] = 0x00; + System.arraycopy(UUIDUtil.toBytes(uuid), 0, prefixedBytes, 1, 16); + + assertThrows(IllegalArgumentException.class, () -> PniServiceIdentifier.fromBytes(prefixedBytes)); + + prefixedBytes[0] = 0x01; + + assertEquals(uuid, PniServiceIdentifier.fromBytes(prefixedBytes).uuid()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/identity/ServiceIdentifierTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/identity/ServiceIdentifierTest.java new file mode 100644 index 000000000..b06bca9e1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/identity/ServiceIdentifierTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.identity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.UUID; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.whispersystems.textsecuregcm.util.UUIDUtil; + +class ServiceIdentifierTest { + + @ParameterizedTest + @MethodSource + void valueOf(final String identifierString, final IdentityType expectedIdentityType, final UUID expectedUuid) { + final ServiceIdentifier serviceIdentifier = ServiceIdentifier.valueOf(identifierString); + + assertEquals(expectedIdentityType, serviceIdentifier.identityType()); + assertEquals(expectedUuid, serviceIdentifier.uuid()); + } + + private static Stream valueOf() { + final UUID uuid = UUID.randomUUID(); + + return Stream.of( + Arguments.of(uuid.toString(), IdentityType.ACI, uuid), + Arguments.of("ACI:" + uuid, IdentityType.ACI, uuid), + Arguments.of("PNI:" + uuid, IdentityType.PNI, uuid)); + } + + @ParameterizedTest + @ValueSource(strings = {"Not a valid UUID", "BAD:a9edc243-3e93-45d4-95c6-e3a84cd4a254"}) + void valueOfIllegalArgument(final String identifierString) { + assertThrows(IllegalArgumentException.class, () -> ServiceIdentifier.valueOf(identifierString)); + } + + @ParameterizedTest + @MethodSource + void fromBytes(final byte[] bytes, final IdentityType expectedIdentityType, final UUID expectedUuid) { + final ServiceIdentifier serviceIdentifier = ServiceIdentifier.fromBytes(bytes); + + assertEquals(expectedIdentityType, serviceIdentifier.identityType()); + assertEquals(expectedUuid, serviceIdentifier.uuid()); + } + + private static Stream fromBytes() { + final UUID uuid = UUID.randomUUID(); + + final byte[] aciPrefixedBytes = new byte[17]; + aciPrefixedBytes[0] = 0x00; + System.arraycopy(UUIDUtil.toBytes(uuid), 0, aciPrefixedBytes, 1, 16); + + final byte[] pniPrefixedBytes = new byte[17]; + pniPrefixedBytes[0] = 0x01; + System.arraycopy(UUIDUtil.toBytes(uuid), 0, pniPrefixedBytes, 1, 16); + + return Stream.of( + Arguments.of(UUIDUtil.toBytes(uuid), IdentityType.ACI, uuid), + Arguments.of(aciPrefixedBytes, IdentityType.ACI, uuid), + Arguments.of(pniPrefixedBytes, IdentityType.PNI, uuid)); + } + + @ParameterizedTest + @MethodSource + void fromBytesIllegalArgument(final byte[] bytes) { + assertThrows(IllegalArgumentException.class, () -> ServiceIdentifier.fromBytes(bytes)); + } + + private static Stream fromBytesIllegalArgument() { + final byte[] invalidPrefixBytes = new byte[17]; + invalidPrefixBytes[0] = (byte) 0xff; + + return Stream.of( + Arguments.of(new byte[0]), + Arguments.of(new byte[15]), + Arguments.of(new byte[18]), + Arguments.of(invalidPrefixBytes)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/CardinalityEstimatorTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/CardinalityEstimatorTest.java new file mode 100644 index 000000000..643d2a9fa --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/CardinalityEstimatorTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; +import java.time.Duration; + +public class CardinalityEstimatorTest { + + @RegisterExtension + private static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + @Test + public void testAdd() throws Exception { + final FaultTolerantRedisCluster redisCluster = REDIS_CLUSTER_EXTENSION.getRedisCluster(); + final CardinalityEstimator estimator = new CardinalityEstimator(redisCluster, "test", Duration.ofSeconds(1)); + + estimator.add("1"); + + long count = redisCluster.withCluster(conn -> conn.sync().pfcount("cardinality_estimator::test")); + assertThat(count).isEqualTo(1).isEqualTo(estimator.estimate()); + + estimator.add("2"); + count = redisCluster.withCluster(conn -> conn.sync().pfcount("cardinality_estimator::test")); + assertThat(count).isEqualTo(2).isEqualTo(estimator.estimate()); + + estimator.add("1"); + count = redisCluster.withCluster(conn -> conn.sync().pfcount("cardinality_estimator::test")); + assertThat(count).isEqualTo(2).isEqualTo(estimator.estimate()); + } + + @Test + @Timeout(5) + public void testEventuallyExpires() throws InterruptedException { + final FaultTolerantRedisCluster redisCluster = REDIS_CLUSTER_EXTENSION.getRedisCluster(); + final CardinalityEstimator estimator = new CardinalityEstimator(redisCluster, "test", Duration.ofMillis(100)); + estimator.add("1"); + long count; + do { + count = redisCluster.withCluster(conn -> conn.sync().pfcount("cardinality_estimator::test")); + Thread.sleep(1); + } while (count != 0); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManagerTest.java new file mode 100644 index 000000000..7665455e6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManagerTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.whispersystems.textsecuregcm.captcha.Action; +import org.whispersystems.textsecuregcm.captcha.AssessmentResult; +import org.whispersystems.textsecuregcm.captcha.CaptchaChecker; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.spam.ChallengeType; +import org.whispersystems.textsecuregcm.spam.RateLimitChallengeListener; +import org.whispersystems.textsecuregcm.storage.Account; + +class RateLimitChallengeManagerTest { + + private PushChallengeManager pushChallengeManager; + private CaptchaChecker captchaChecker; + private RateLimiters rateLimiters; + private RateLimitChallengeListener rateLimitChallengeListener; + + private RateLimitChallengeManager rateLimitChallengeManager; + + @BeforeEach + void setUp() { + pushChallengeManager = mock(PushChallengeManager.class); + captchaChecker = mock(CaptchaChecker.class); + rateLimiters = mock(RateLimiters.class); + rateLimitChallengeListener = mock(RateLimitChallengeListener.class); + + rateLimitChallengeManager = new RateLimitChallengeManager( + pushChallengeManager, + captchaChecker, + rateLimiters); + + rateLimitChallengeManager.addListener(rateLimitChallengeListener); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void answerPushChallenge(final boolean successfulChallenge) throws RateLimitExceededException { + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(UUID.randomUUID()); + + when(pushChallengeManager.answerChallenge(eq(account), any())).thenReturn(successfulChallenge); + + when(rateLimiters.getPushChallengeAttemptLimiter()).thenReturn(mock(RateLimiter.class)); + when(rateLimiters.getPushChallengeSuccessLimiter()).thenReturn(mock(RateLimiter.class)); + when(rateLimiters.getRateLimitResetLimiter()).thenReturn(mock(RateLimiter.class)); + + rateLimitChallengeManager.answerPushChallenge(account, "challenge"); + + if (successfulChallenge) { + verify(rateLimitChallengeListener).handleRateLimitChallengeAnswered(account, ChallengeType.PUSH); + } else { + verifyNoInteractions(rateLimitChallengeListener); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void answerRecaptchaChallenge(final boolean successfulChallenge) throws RateLimitExceededException, IOException { + final Account account = mock(Account.class); + when(account.getNumber()).thenReturn("+18005551234"); + when(account.getUuid()).thenReturn(UUID.randomUUID()); + + when(captchaChecker.verify(eq(Action.CHALLENGE), any(), any())) + .thenReturn(successfulChallenge + ? AssessmentResult.alwaysValid() + : AssessmentResult.invalid()); + + when(rateLimiters.getRecaptchaChallengeAttemptLimiter()).thenReturn(mock(RateLimiter.class)); + when(rateLimiters.getRecaptchaChallengeSuccessLimiter()).thenReturn(mock(RateLimiter.class)); + when(rateLimiters.getRateLimitResetLimiter()).thenReturn(mock(RateLimiter.class)); + + rateLimitChallengeManager.answerRecaptchaChallenge(account, "captcha", "10.0.0.1", "Test User-Agent"); + + if (successfulChallenge) { + verify(rateLimitChallengeListener).handleRateLimitChallengeAnswered(account, ChallengeType.CAPTCHA); + } else { + verifyNoInteractions(rateLimitChallengeListener); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManagerTest.java new file mode 100644 index 000000000..7a9429245 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManagerTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.storage.Account; + +class RateLimitChallengeOptionManagerTest { + + private RateLimiters rateLimiters; + + private RateLimitChallengeOptionManager rateLimitChallengeOptionManager; + + @BeforeEach + void setUp() { + rateLimiters = mock(RateLimiters.class); + rateLimitChallengeOptionManager = new RateLimitChallengeOptionManager(rateLimiters); + } + + @ParameterizedTest + @MethodSource + void getChallengeOptions(final boolean captchaAttemptPermitted, + final boolean captchaSuccessPermitted, + final boolean pushAttemptPermitted, + final boolean pushSuccessPermitted, + final boolean expectCaptcha, + final boolean expectPushChallenge) { + + final RateLimiter recaptchaChallengeAttemptLimiter = mock(RateLimiter.class); + final RateLimiter recaptchaChallengeSuccessLimiter = mock(RateLimiter.class); + final RateLimiter pushChallengeAttemptLimiter = mock(RateLimiter.class); + final RateLimiter pushChallengeSuccessLimiter = mock(RateLimiter.class); + + when(rateLimiters.getRecaptchaChallengeAttemptLimiter()).thenReturn(recaptchaChallengeAttemptLimiter); + when(rateLimiters.getRecaptchaChallengeSuccessLimiter()).thenReturn(recaptchaChallengeSuccessLimiter); + when(rateLimiters.getPushChallengeAttemptLimiter()).thenReturn(pushChallengeAttemptLimiter); + when(rateLimiters.getPushChallengeSuccessLimiter()).thenReturn(pushChallengeSuccessLimiter); + + when(recaptchaChallengeAttemptLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(captchaAttemptPermitted); + when(recaptchaChallengeSuccessLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(captchaSuccessPermitted); + when(pushChallengeAttemptLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(pushAttemptPermitted); + when(pushChallengeSuccessLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(pushSuccessPermitted); + + final int expectedLength = (expectCaptcha ? 1 : 0) + (expectPushChallenge ? 1 : 0); + + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(UUID.randomUUID()); + + final List options = rateLimitChallengeOptionManager.getChallengeOptions(account); + assertEquals(expectedLength, options.size()); + + if (expectCaptcha) { + assertTrue(options.contains(RateLimitChallengeOptionManager.OPTION_RECAPTCHA)); + } + + if (expectPushChallenge) { + assertTrue(options.contains(RateLimitChallengeOptionManager.OPTION_PUSH_CHALLENGE)); + } + } + + private static Stream getChallengeOptions() { + return Stream.of( + Arguments.of(false, false, false, false, false, false), + Arguments.of(false, false, false, true, false, false), + Arguments.of(false, false, true, false, false, false), + Arguments.of(false, false, true, true, false, true), + Arguments.of(false, true, false, false, false, false), + Arguments.of(false, true, false, true, false, false), + Arguments.of(false, true, true, false, false, false), + Arguments.of(false, true, true, true, false, true), + Arguments.of(true, false, false, false, false, false), + Arguments.of(true, false, false, true, false, false), + Arguments.of(true, false, true, false, false, false), + Arguments.of(true, false, true, true, false, true), + Arguments.of(true, true, false, false, true, false), + Arguments.of(true, true, false, true, true, false), + Arguments.of(true, true, true, false, true, false), + Arguments.of(true, true, true, true, true, true) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitedByIpTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitedByIpTest.java new file mode 100644 index 000000000..94c3dc767 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitedByIpTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.common.net.HttpHeaders; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.time.Duration; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.util.MockUtils; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +@ExtendWith(DropwizardExtensionsSupport.class) +public class RateLimitedByIpTest { + + private static final String IP = "70.130.130.200"; + + private static final String VALID_X_FORWARDED_FOR = "1.1.1.1," + IP; + + private static final String INVALID_X_FORWARDED_FOR = "1.1.1.1,"; + + private static final Duration RETRY_AFTER = Duration.ofSeconds(100); + + private static final Duration RETRY_AFTER_INVALID_HEADER = RateLimitByIpFilter.INVALID_HEADER_EXCEPTION + .getRetryDuration() + .orElseThrow(); + + + @Path("/test") + public static class Controller { + @GET + @Path("/strict") + @RateLimitedByIp(RateLimiters.For.BACKUP_AUTH_CHECK) + public Response strict() { + return Response.ok().build(); + } + + @GET + @Path("/loose") + @RateLimitedByIp(value = RateLimiters.For.BACKUP_AUTH_CHECK, failOnUnresolvedIp = false) + public Response loose() { + return Response.ok().build(); + } + } + + private static final RateLimiter RATE_LIMITER = Mockito.mock(RateLimiter.class); + + private static final RateLimiters RATE_LIMITERS = MockUtils.buildMock(RateLimiters.class, rl -> + Mockito.when(rl.forDescriptor(Mockito.eq(RateLimiters.For.BACKUP_AUTH_CHECK))).thenReturn(RATE_LIMITER)); + + private static final ResourceExtension RESOURCES = ResourceExtension.builder() + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new Controller()) + .addProvider(new RateLimitByIpFilter(RATE_LIMITERS)) + .build(); + + @Test + public void testRateLimits() throws Exception { + Mockito.doNothing().when(RATE_LIMITER).validate(Mockito.eq(IP)); + validateSuccess("/test/strict", VALID_X_FORWARDED_FOR); + Mockito.doThrow(new RateLimitExceededException(RETRY_AFTER, true)).when(RATE_LIMITER).validate(Mockito.eq(IP)); + validateFailure("/test/strict", VALID_X_FORWARDED_FOR, RETRY_AFTER); + Mockito.doNothing().when(RATE_LIMITER).validate(Mockito.eq(IP)); + validateSuccess("/test/strict", VALID_X_FORWARDED_FOR); + Mockito.doThrow(new RateLimitExceededException(RETRY_AFTER, true)).when(RATE_LIMITER).validate(Mockito.eq(IP)); + validateFailure("/test/strict", VALID_X_FORWARDED_FOR, RETRY_AFTER); + } + + @Test + public void testInvalidHeader() throws Exception { + Mockito.doNothing().when(RATE_LIMITER).validate(Mockito.eq(IP)); + validateSuccess("/test/strict", VALID_X_FORWARDED_FOR); + validateFailure("/test/strict", INVALID_X_FORWARDED_FOR, RETRY_AFTER_INVALID_HEADER); + validateFailure("/test/strict", "", RETRY_AFTER_INVALID_HEADER); + + validateSuccess("/test/loose", VALID_X_FORWARDED_FOR); + validateSuccess("/test/loose", INVALID_X_FORWARDED_FOR); + validateSuccess("/test/loose", ""); + + // also checking that even if rate limiter is failing -- it doesn't matter in the case of invalid IP + Mockito.doThrow(new RateLimitExceededException(RETRY_AFTER, true)).when(RATE_LIMITER).validate(Mockito.anyString()); + validateFailure("/test/loose", VALID_X_FORWARDED_FOR, RETRY_AFTER); + validateSuccess("/test/loose", INVALID_X_FORWARDED_FOR); + validateSuccess("/test/loose", ""); + } + + private static void validateSuccess(final String path, final String xff) { + final Response response = RESOURCES.getJerseyTest() + .target(path) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, xff) + .get(); + + assertEquals(200, response.getStatus()); + } + + private static void validateFailure(final String path, final String xff, final Duration expectedRetryAfter) { + final Response response = RESOURCES.getJerseyTest() + .target(path) + .request() + .header(HttpHeaders.X_FORWARDED_FOR, xff) + .get(); + + assertEquals(413, response.getStatus()); + assertEquals("" + expectedRetryAfter.getSeconds(), response.getHeaderString(HttpHeaders.RETRY_AFTER)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimiterConfigTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimiterConfigTest.java new file mode 100644 index 000000000..6e5c82465 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimiterConfigTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class RateLimiterConfigTest { + + @Test + void leakRatePerMillis() { + assertEquals(0.001, new RateLimiterConfig(1, Duration.ofSeconds(1)).leakRatePerMillis()); + assertEquals(1e6, new RateLimiterConfig(1, Duration.ofNanos(1)).leakRatePerMillis()); + } + + @Test + void isRegenerationRatePositive() { + assertTrue(new RateLimiterConfig(1, Duration.ofSeconds(1)).hasPositiveRegenerationRate()); + assertTrue(new RateLimiterConfig(1, Duration.ofNanos(1)).hasPositiveRegenerationRate()); + assertFalse(new RateLimiterConfig(1, Duration.ZERO).hasPositiveRegenerationRate()); + assertFalse(new RateLimiterConfig(1, Duration.ofSeconds(-1)).hasPositiveRegenerationRate()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitersLuaScriptTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitersLuaScriptTest.java new file mode 100644 index 000000000..6d3908470 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitersLuaScriptTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.lettuce.core.RedisException; +import io.lettuce.core.ScriptOutputType; +import java.time.Clock; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRateLimitPolicy; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.util.MockUtils; +import org.whispersystems.textsecuregcm.util.MutableClock; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.redis.RedisLuaScriptSandbox; +import org.whispersystems.textsecuregcm.util.redis.SimpleCacheCommandsHandler; + +public class RateLimitersLuaScriptTest { + + @RegisterExtension + private static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + private final DynamicConfiguration configuration = mock(DynamicConfiguration.class); + + private final MutableClock clock = MockUtils.mutableClock(0); + + private final RedisLuaScriptSandbox sandbox = RedisLuaScriptSandbox.fromResource( + "lua/validate_rate_limit.lua", + ScriptOutputType.INTEGER); + + private final SimpleCacheCommandsHandler redisCommandsHandler = new SimpleCacheCommandsHandler(clock); + + private final DynamicConfigurationManager dynamicConfig = + MockUtils.buildMock(DynamicConfigurationManager.class, cfg -> when(cfg.getConfiguration()).thenReturn(configuration)); + + @Test + public void testWithEmbeddedRedis() throws Exception { + final RateLimiters.For descriptor = RateLimiters.For.REGISTRATION; + final FaultTolerantRedisCluster redisCluster = REDIS_CLUSTER_EXTENSION.getRedisCluster(); + final RateLimiters limiters = new RateLimiters( + Map.of(descriptor.id(), new RateLimiterConfig(60, Duration.ofSeconds(1))), + dynamicConfig, + RateLimiters.defaultScript(redisCluster), + redisCluster, + Clock.systemUTC()); + + final RateLimiter rateLimiter = limiters.forDescriptor(descriptor); + rateLimiter.validate("test", 25); + rateLimiter.validate("test", 25); + assertThrows(RateLimitExceededException.class, () -> rateLimiter.validate("test", 25)); + } + + @Test + public void testTtl() throws Exception { + final RateLimiters.For descriptor = RateLimiters.For.REGISTRATION; + final FaultTolerantRedisCluster redisCluster = REDIS_CLUSTER_EXTENSION.getRedisCluster(); + final RateLimiters limiters = new RateLimiters( + Map.of(descriptor.id(), new RateLimiterConfig(1000, Duration.ofSeconds(1))), + dynamicConfig, + RateLimiters.defaultScript(redisCluster), + redisCluster, + Clock.systemUTC()); + + final RateLimiter rateLimiter = limiters.forDescriptor(descriptor); + rateLimiter.validate("test", 200); + // after using 200 tokens, we expect 200 seconds to refill, so the TTL should be under 200000 + final long ttl = redisCluster.withCluster(c -> c.sync().ttl("test")); + assertTrue(ttl <= 200000); + } + + @Test + public void testLuaUpdatesTokenBucket() throws Exception { + final String key = "key1"; + clock.setTimeMillis(0); + long result = (long) sandbox.execute( + List.of(key), + scriptArgs(1000, 1, 200, true), + redisCommandsHandler + ); + assertEquals(0L, result); + assertEquals(800L, decodeBucket(key).orElseThrow().tokensRemaining); + + // 50 tokens replenished, acquiring 100 more, should end up with 750 available + clock.setTimeMillis(50); + result = (long) sandbox.execute( + List.of(key), + scriptArgs(1000, 1, 100, true), + redisCommandsHandler + ); + assertEquals(0L, result); + assertEquals(750L, decodeBucket(key).orElseThrow().tokensRemaining); + + // now checking without an update, should not affect the count + result = (long) sandbox.execute( + List.of(key), + scriptArgs(1000, 1, 100, false), + redisCommandsHandler + ); + assertEquals(0L, result); + assertEquals(750L, decodeBucket(key).orElseThrow().tokensRemaining); + } + + @Test + public void testFailOpen() throws Exception { + when(configuration.getRateLimitPolicy()).thenReturn(new DynamicRateLimitPolicy(true)); + final RateLimiters.For descriptor = RateLimiters.For.REGISTRATION; + final FaultTolerantRedisCluster redisCluster = mock(FaultTolerantRedisCluster.class); + final RateLimiters limiters = new RateLimiters( + Map.of(descriptor.id(), new RateLimiterConfig(1000, Duration.ofSeconds(1))), + dynamicConfig, + RateLimiters.defaultScript(redisCluster), + redisCluster, + Clock.systemUTC()); + when(redisCluster.withCluster(any())).thenThrow(new RedisException("fail")); + final RateLimiter rateLimiter = limiters.forDescriptor(descriptor); + rateLimiter.validate("test", 200); + } + + private String serializeToOldBucketValueFormat( + final long bucketSize, + final long leakRatePerMillis, + final long spaceRemaining, + final long lastUpdateTimeMillis) { + try { + return SystemMapper.jsonMapper().writeValueAsString(Map.of( + "bucketSize", bucketSize, + "leakRatePerMillis", leakRatePerMillis, + "spaceRemaining", spaceRemaining, + "lastUpdateTimeMillis", lastUpdateTimeMillis + )); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private Optional decodeBucket(final String key) { + final Object[] fields = redisCommandsHandler.hmget(key, List.of("s", "t")); + return fields[0] == null + ? Optional.empty() + : Optional.of(new TokenBucket( + Double.valueOf(fields[0].toString()).longValue(), Double.valueOf(fields[1].toString()).longValue())); + } + + private List scriptArgs( + final long bucketSize, + final long ratePerMillis, + final long requestedAmount, + final boolean useTokens) { + return List.of( + String.valueOf(bucketSize), + String.valueOf(ratePerMillis), + String.valueOf(clock.millis()), + String.valueOf(requestedAmount), + String.valueOf(useTokens) + ); + } + + private record TokenBucket(long tokensRemaining, long lastUpdateTimeMillis) { + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitersTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitersTest.java new file mode 100644 index 000000000..9d90e2405 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitersTest.java @@ -0,0 +1,204 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRateLimitPolicy; +import org.whispersystems.textsecuregcm.redis.ClusterLuaScript; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.util.MockUtils; +import org.whispersystems.textsecuregcm.util.MutableClock; + +@SuppressWarnings("unchecked") +public class RateLimitersTest { + + private final DynamicConfiguration configuration = mock(DynamicConfiguration.class); + + private final DynamicConfigurationManager dynamicConfig = + MockUtils.buildMock(DynamicConfigurationManager.class, cfg -> when(cfg.getConfiguration()).thenReturn(configuration)); + + private final ClusterLuaScript validateScript = mock(ClusterLuaScript.class); + + private final FaultTolerantRedisCluster redisCluster = mock(FaultTolerantRedisCluster.class); + + private final MutableClock clock = MockUtils.mutableClock(0); + + private static final String BAD_YAML = """ + limits: + smsVoicePrefix: + bucketSize: 150 + permitRegenerationDuration: PT6S + unexpected: + bucketSize: 4 + permitRegenerationDuration: PT30S + """; + + private static final String GOOD_YAML = """ + limits: + smsVoicePrefix: + bucketSize: 150 + permitRegenerationDuration: PT6S + attachmentCreate: + bucketSize: 4 + permitRegenerationDuration: PT30S + rateLimitPolicy: + failOpen: true + """; + + public record GenericHolder( + @Valid @NotNull @JsonProperty Map limits, + @Valid @JsonProperty DynamicRateLimitPolicy rateLimitPolicy) { + } + + @Test + public void testValidateConfigs() throws Exception { + assertThrows(IllegalArgumentException.class, () -> { + final GenericHolder cfg = DynamicConfigurationManager.parseConfiguration(BAD_YAML, GenericHolder.class).orElseThrow(); + final RateLimiters rateLimiters = new RateLimiters(cfg.limits(), dynamicConfig, validateScript, redisCluster, clock); + rateLimiters.validateValuesAndConfigs(); + }); + + final GenericHolder cfg = DynamicConfigurationManager.parseConfiguration(GOOD_YAML, GenericHolder.class).orElseThrow(); + assertTrue(cfg.rateLimitPolicy.failOpen()); + final RateLimiters rateLimiters = new RateLimiters(cfg.limits(), dynamicConfig, validateScript, redisCluster, clock); + rateLimiters.validateValuesAndConfigs(); + } + + @Test + public void testValidateDuplicates() throws Exception { + final TestDescriptor td1 = new TestDescriptor("id1"); + final TestDescriptor td2 = new TestDescriptor("id2"); + final TestDescriptor td3 = new TestDescriptor("id3"); + final TestDescriptor tdDup = new TestDescriptor("id1"); + + assertThrows(IllegalStateException.class, () -> new BaseRateLimiters<>( + new TestDescriptor[] { td1, td2, td3, tdDup }, + Collections.emptyMap(), + dynamicConfig, + validateScript, + redisCluster, + clock) {}); + + new BaseRateLimiters<>( + new TestDescriptor[] { td1, td2, td3 }, + Collections.emptyMap(), + dynamicConfig, + validateScript, + redisCluster, + clock) {}; + } + + @Test + void testUnchangingConfiguration() { + final RateLimiters rateLimiters = new RateLimiters(Collections.emptyMap(), dynamicConfig, validateScript, redisCluster, clock); + final RateLimiter limiter = rateLimiters.getRateLimitResetLimiter(); + final RateLimiterConfig expected = RateLimiters.For.RATE_LIMIT_RESET.defaultConfig(); + assertEquals(expected, config(limiter)); + } + + @Test + void testChangingConfiguration() { + final RateLimiterConfig initialRateLimiterConfig = new RateLimiterConfig(4, Duration.ofMinutes(1)); + final RateLimiterConfig updatedRateLimiterCongig = new RateLimiterConfig(17, Duration.ofSeconds(3)); + final RateLimiterConfig baseConfig = new RateLimiterConfig(1, Duration.ofMinutes(1)); + + final Map limitsConfigMap = new HashMap<>(); + + limitsConfigMap.put(RateLimiters.For.RECAPTCHA_CHALLENGE_ATTEMPT.id(), baseConfig); + limitsConfigMap.put(RateLimiters.For.RECAPTCHA_CHALLENGE_SUCCESS.id(), baseConfig); + + when(configuration.getLimits()).thenReturn(limitsConfigMap); + + final RateLimiters rateLimiters = new RateLimiters(Collections.emptyMap(), dynamicConfig, validateScript, redisCluster, clock); + final RateLimiter limiter = rateLimiters.getRateLimitResetLimiter(); + + limitsConfigMap.put(RateLimiters.For.RATE_LIMIT_RESET.id(), initialRateLimiterConfig); + assertEquals(initialRateLimiterConfig, config(limiter)); + + assertEquals(baseConfig, config(rateLimiters.getRecaptchaChallengeAttemptLimiter())); + assertEquals(baseConfig, config(rateLimiters.getRecaptchaChallengeSuccessLimiter())); + + limitsConfigMap.put(RateLimiters.For.RATE_LIMIT_RESET.id(), updatedRateLimiterCongig); + assertEquals(updatedRateLimiterCongig, config(limiter)); + + assertEquals(baseConfig, config(rateLimiters.getRecaptchaChallengeAttemptLimiter())); + assertEquals(baseConfig, config(rateLimiters.getRecaptchaChallengeSuccessLimiter())); + } + + @Test + public void testRateLimiterHasItsPrioritiesStraight() throws Exception { + final RateLimiters.For descriptor = RateLimiters.For.RECAPTCHA_CHALLENGE_ATTEMPT; + final RateLimiterConfig configForDynamic = new RateLimiterConfig(1, Duration.ofMinutes(1)); + final RateLimiterConfig configForStatic = new RateLimiterConfig(2, Duration.ofSeconds(30)); + final RateLimiterConfig defaultConfig = descriptor.defaultConfig(); + + final Map mapForDynamic = new HashMap<>(); + final Map mapForStatic = new HashMap<>(); + + when(configuration.getLimits()).thenReturn(mapForDynamic); + + final RateLimiters rateLimiters = new RateLimiters(mapForStatic, dynamicConfig, validateScript, redisCluster, clock); + final RateLimiter limiter = rateLimiters.forDescriptor(descriptor); + + // test only default is present + mapForDynamic.remove(descriptor.id()); + mapForStatic.remove(descriptor.id()); + assertEquals(defaultConfig, config(limiter)); + + // test dynamic and no static + mapForDynamic.put(descriptor.id(), configForDynamic); + mapForStatic.remove(descriptor.id()); + assertEquals(configForDynamic, config(limiter)); + + // test dynamic and static + mapForDynamic.put(descriptor.id(), configForDynamic); + mapForStatic.put(descriptor.id(), configForStatic); + assertEquals(configForDynamic, config(limiter)); + + // test static, but no dynamic + mapForDynamic.remove(descriptor.id()); + mapForStatic.put(descriptor.id(), configForStatic); + assertEquals(configForStatic, config(limiter)); + } + + private record TestDescriptor(String id) implements RateLimiterDescriptor { + + @Override + public boolean isDynamic() { + return false; + } + + @Override + public RateLimiterConfig defaultConfig() { + return new RateLimiterConfig(1, Duration.ofMinutes(1)); + } + } + + private static RateLimiterConfig config(final RateLimiter rateLimiter) { + if (rateLimiter instanceof StaticRateLimiter rm) { + return rm.config(); + } + if (rateLimiter instanceof DynamicRateLimiter rm) { + return rm.config(); + } + throw new IllegalArgumentException("Rate limiter is of an unexpected type: " + rateLimiter.getClass().getName()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MessageMetricsTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MessageMetricsTest.java new file mode 100644 index 000000000..c413d2cb9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MessageMetricsTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.storage.Account; + +class MessageMetricsTest { + + private final Account account = mock(Account.class); + private final UUID aci = UUID.fromString("11111111-1111-1111-1111-111111111111"); + private final UUID pni = UUID.fromString("22222222-2222-2222-2222-222222222222"); + private final UUID otherUuid = UUID.fromString("99999999-9999-9999-9999-999999999999"); + private SimpleMeterRegistry simpleMeterRegistry; + + @BeforeEach + void setup() { + when(account.getUuid()).thenReturn(aci); + when(account.getPhoneNumberIdentifier()).thenReturn(pni); + when(account.isIdentifiedBy(any())).thenReturn(false); + when(account.isIdentifiedBy(new AciServiceIdentifier(aci))).thenReturn(true); + when(account.isIdentifiedBy(new PniServiceIdentifier(pni))).thenReturn(true); + Metrics.globalRegistry.clear(); + simpleMeterRegistry = new SimpleMeterRegistry(); + Metrics.globalRegistry.add(simpleMeterRegistry); + } + + @AfterEach + void teardown() { + Metrics.globalRegistry.remove(simpleMeterRegistry); + Metrics.globalRegistry.clear(); + } + + @Test + void measureAccountOutgoingMessageUuidMismatches() { + + final OutgoingMessageEntity outgoingMessageToAci = createOutgoingMessageEntity(new AciServiceIdentifier(aci)); + MessageMetrics.measureAccountOutgoingMessageUuidMismatches(account, outgoingMessageToAci); + + Optional counter = findCounter(simpleMeterRegistry); + + assertTrue(counter.isEmpty()); + + final OutgoingMessageEntity outgoingMessageToPni = createOutgoingMessageEntity(new PniServiceIdentifier(pni)); + MessageMetrics.measureAccountOutgoingMessageUuidMismatches(account, outgoingMessageToPni); + counter = findCounter(simpleMeterRegistry); + + assertTrue(counter.isEmpty()); + + final OutgoingMessageEntity outgoingMessageToOtherUuid = createOutgoingMessageEntity(new AciServiceIdentifier(otherUuid)); + MessageMetrics.measureAccountOutgoingMessageUuidMismatches(account, outgoingMessageToOtherUuid); + counter = findCounter(simpleMeterRegistry); + + assertEquals(1.0, counter.map(Counter::count).orElse(0.0)); + } + + private OutgoingMessageEntity createOutgoingMessageEntity(final ServiceIdentifier destinationIdentifier) { + return new OutgoingMessageEntity(UUID.randomUUID(), 1, 1L, null, 1, destinationIdentifier, null, new byte[]{}, 1, true, false, null); + } + + @Test + void measureAccountEnvelopeUuidMismatches() { + final MessageProtos.Envelope envelopeToAci = createEnvelope(new AciServiceIdentifier(aci)); + MessageMetrics.measureAccountEnvelopeUuidMismatches(account, envelopeToAci); + + Optional counter = findCounter(simpleMeterRegistry); + + assertTrue(counter.isEmpty()); + + final MessageProtos.Envelope envelopeToPni = createEnvelope(new PniServiceIdentifier(pni)); + MessageMetrics.measureAccountEnvelopeUuidMismatches(account, envelopeToPni); + counter = findCounter(simpleMeterRegistry); + + assertTrue(counter.isEmpty()); + + final MessageProtos.Envelope envelopeToOtherUuid = createEnvelope(new AciServiceIdentifier(otherUuid)); + MessageMetrics.measureAccountEnvelopeUuidMismatches(account, envelopeToOtherUuid); + counter = findCounter(simpleMeterRegistry); + + assertEquals(1.0, counter.map(Counter::count).orElse(0.0)); + + final MessageProtos.Envelope envelopeToNull = createEnvelope(null); + MessageMetrics.measureAccountEnvelopeUuidMismatches(account, envelopeToNull); + counter = findCounter(simpleMeterRegistry); + + assertEquals(1.0, counter.map(Counter::count).orElse(0.0)); + } + + private MessageProtos.Envelope createEnvelope(ServiceIdentifier destinationIdentifier) { + final MessageProtos.Envelope.Builder builder = MessageProtos.Envelope.newBuilder(); + + if (destinationIdentifier != null) { + builder.setDestinationUuid(destinationIdentifier.toServiceIdentifierString()); + } + + return builder.build(); + } + + private Optional findCounter(SimpleMeterRegistry meterRegistry) { + final Optional maybeMeter = meterRegistry.getMeters().stream().findFirst(); + return maybeMeter.map(meter -> meter instanceof Counter ? (Counter) meter : null); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListenerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListenerTest.java new file mode 100644 index 000000000..c9aeca5e9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsRequestEventListenerTest.java @@ -0,0 +1,277 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.net.HttpHeaders; +import com.google.protobuf.InvalidProtocolBufferException; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import java.nio.ByteBuffer; +import java.security.Principal; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.uri.UriTemplate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; +import org.whispersystems.websocket.WebSocketResourceProvider; +import org.whispersystems.websocket.auth.WebsocketAuthValueFactoryProvider; +import org.whispersystems.websocket.logging.WebsocketRequestLog; +import org.whispersystems.websocket.messages.protobuf.ProtobufWebSocketMessageFactory; +import org.whispersystems.websocket.messages.protobuf.SubProtocol; +import org.whispersystems.websocket.session.WebSocketSessionContextValueFactoryProvider; + +class MetricsRequestEventListenerTest { + + private MeterRegistry meterRegistry; + private Counter counter; + private MetricsRequestEventListener listener; + + private static final TrafficSource TRAFFIC_SOURCE = TrafficSource.HTTP; + + @BeforeEach + void setup() { + meterRegistry = mock(MeterRegistry.class); + counter = mock(Counter.class); + + final ClientReleaseManager clientReleaseManager = mock(ClientReleaseManager.class); + when(clientReleaseManager.isVersionActive(any(), any())).thenReturn(false); + + listener = new MetricsRequestEventListener(TRAFFIC_SOURCE, meterRegistry, clientReleaseManager); + } + + @Test + @SuppressWarnings("unchecked") + void testOnEvent() { + final String path = "/test"; + final String method = "GET"; + final int statusCode = 200; + + final ExtendedUriInfo uriInfo = mock(ExtendedUriInfo.class); + when(uriInfo.getMatchedTemplates()).thenReturn(Collections.singletonList(new UriTemplate(path))); + + final ContainerRequest request = mock(ContainerRequest.class); + when(request.getMethod()).thenReturn(method); + when(request.getRequestHeader(HttpHeaders.USER_AGENT)).thenReturn( + Collections.singletonList("Signal-Android/4.53.7 (Android 8.1)")); + + final ContainerResponse response = mock(ContainerResponse.class); + when(response.getStatus()).thenReturn(statusCode); + + final RequestEvent event = mock(RequestEvent.class); + when(event.getType()).thenReturn(RequestEvent.Type.FINISHED); + when(event.getUriInfo()).thenReturn(uriInfo); + when(event.getContainerRequest()).thenReturn(request); + when(event.getContainerResponse()).thenReturn(response); + + final ArgumentCaptor> tagCaptor = ArgumentCaptor.forClass(Iterable.class); + when(meterRegistry.counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), any(Iterable.class))) + .thenReturn(counter); + + listener.onEvent(event); + + verify(meterRegistry).counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), tagCaptor.capture()); + + final Iterable tagIterable = tagCaptor.getValue(); + final Set tags = new HashSet<>(); + + for (final Tag tag : tagIterable) { + tags.add(tag); + } + + assertEquals(5, tags.size()); + assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.PATH_TAG, path))); + assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.METHOD_TAG, method))); + assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.STATUS_CODE_TAG, String.valueOf(statusCode)))); + assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.TRAFFIC_SOURCE_TAG, TRAFFIC_SOURCE.name().toLowerCase()))); + assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android"))); + } + + @Test + void testActualRouteMessageSuccess() throws InvalidProtocolBufferException { + final MetricsApplicationEventListener applicationEventListener = mock(MetricsApplicationEventListener.class); + when(applicationEventListener.onRequest(any())).thenReturn(listener); + + final ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(applicationEventListener); + resourceConfig.register(new TestResource()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(new ObjectMapper())); + + final ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + final WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + final WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", + applicationHandler, + requestLog, + new TestPrincipal("foo"), + new ProtobufWebSocketMessageFactory(), + Optional.empty(), + 30000); + + final Session session = mock(Session.class); + final RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + final UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + when(request.getHeader(HttpHeaders.USER_AGENT)).thenReturn("Signal-Android/4.53.7 (Android 8.1)"); + when(request.getHeaders()).thenReturn(Map.of(HttpHeaders.USER_AGENT, List.of("Signal-Android/4.53.7 (Android 8.1)"))); + + final ArgumentCaptor> tagCaptor = ArgumentCaptor.forClass(Iterable.class); + when(meterRegistry.counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), any(Iterable.class))) + .thenReturn(counter); + + provider.onWebSocketConnect(session); + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/hello", + new LinkedList<>(), Optional.empty()).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + final ArgumentCaptor responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(remoteEndpoint).sendBytesByFuture(responseBytesCaptor.capture()); + + SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor); + + assertThat(response.getStatus()).isEqualTo(200); + + verify(meterRegistry).counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), tagCaptor.capture()); + + final Iterable tagIterable = tagCaptor.getValue(); + final Set tags = new HashSet<>(); + + for (final Tag tag : tagIterable) { + tags.add(tag); + } + + assertEquals(5, tags.size()); + assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.PATH_TAG, "/v1/test/hello"))); + assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.METHOD_TAG, "GET"))); + assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.STATUS_CODE_TAG, String.valueOf(200)))); + assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.TRAFFIC_SOURCE_TAG, TRAFFIC_SOURCE.name().toLowerCase()))); + assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android"))); + } + + @Test + void testActualRouteMessageSuccessNoUserAgent() throws InvalidProtocolBufferException { + final MetricsApplicationEventListener applicationEventListener = mock(MetricsApplicationEventListener.class); + when(applicationEventListener.onRequest(any())).thenReturn(listener); + + final ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(applicationEventListener); + resourceConfig.register(new TestResource()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(new ObjectMapper())); + + final ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + final WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + final WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, new TestPrincipal("foo"), new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); + + final Session session = mock(Session.class); + final RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + final UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + + final ArgumentCaptor> tagCaptor = ArgumentCaptor.forClass(Iterable.class); + when(meterRegistry.counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), any(Iterable.class))).thenReturn( + counter); + + provider.onWebSocketConnect(session); + + final byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/hello", + new LinkedList<>(), Optional.empty()).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + final ArgumentCaptor responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(remoteEndpoint).sendBytesByFuture(responseBytesCaptor.capture()); + + SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor); + + assertThat(response.getStatus()).isEqualTo(200); + + verify(meterRegistry).counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), tagCaptor.capture()); + + final Iterable tagIterable = tagCaptor.getValue(); + final Set tags = new HashSet<>(); + + for (final Tag tag : tagIterable) { + tags.add(tag); + } + + assertEquals(5, tags.size()); + assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.PATH_TAG, "/v1/test/hello"))); + assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.METHOD_TAG, "GET"))); + assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.STATUS_CODE_TAG, String.valueOf(200)))); + assertTrue(tags.contains(Tag.of(MetricsRequestEventListener.TRAFFIC_SOURCE_TAG, TRAFFIC_SOURCE.name().toLowerCase()))); + assertTrue(tags.contains(Tag.of(UserAgentTagUtil.PLATFORM_TAG, "unrecognized"))); + } + + private static SubProtocol.WebSocketResponseMessage getResponse(ArgumentCaptor responseCaptor) + throws InvalidProtocolBufferException { + + return SubProtocol.WebSocketMessage.parseFrom(responseCaptor.getValue().array()).getResponse(); + } + + public static class TestPrincipal implements Principal { + + private final String name; + + private TestPrincipal(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + } + + @Path("/v1/test") + public static class TestResource { + + @GET + @Path("/hello") + public String testGetHello() { + return "Hello!"; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsUtilTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsUtilTest.java new file mode 100644 index 000000000..d253b216d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/MetricsUtilTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import java.util.List; +import org.junit.jupiter.api.Test; + + +class MetricsUtilTest { + + @Test + void name() { + + assertEquals("chat.MetricsUtilTest.metric", MetricsUtil.name(MetricsUtilTest.class, "metric")); + assertEquals("chat.MetricsUtilTest.namespace.metric", + MetricsUtil.name(MetricsUtilTest.class, "namespace", "metric")); + } + + @Test + void lettuceTagRejection() { + MeterRegistry registry = new SimpleMeterRegistry(); + MetricsUtil.configureMeterFilters(registry.config()); + + registry.counter("lettuce.command.completion.max", "command", "hello", "remote", "world", "allowed", "!").increment(); + final List meters = registry.getMeters(); + assertThat(meters).hasSize(1); + + Meter meter = meters.get(0); + assertThat(meter.getId().getName()).isEqualTo("chat.lettuce.command.completion.max"); + assertThat(meter.getId().getTag("command")).isNull(); + assertThat(meter.getId().getTag("remote")).isNull(); + assertThat(meter.getId().getTag("allowed")).isNotNull(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/OperatingSystemMemoryGaugeTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/OperatingSystemMemoryGaugeTest.java new file mode 100644 index 000000000..78e62e749 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/OperatingSystemMemoryGaugeTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class OperatingSystemMemoryGaugeTest { + + private static final String MEMINFO = + """ + MemTotal: 16052208 kB + MemFree: 4568468 kB + MemAvailable: 7702848 kB + Buffers: 636372 kB + Cached: 5019116 kB + SwapCached: 6692 kB + Active: 7746436 kB + Inactive: 2729876 kB + Active(anon): 5580980 kB + Inactive(anon): 1648108 kB + Active(file): 2165456 kB + Inactive(file): 1081768 kB + Unevictable: 443948 kB + Mlocked: 4924 kB + SwapTotal: 1003516 kB + SwapFree: 935932 kB + Dirty: 28308 kB + Writeback: 0 kB + AnonPages: 5258396 kB + Mapped: 1530740 kB + Shmem: 2419340 kB + KReclaimable: 229392 kB + Slab: 408156 kB + SReclaimable: 229392 kB + SUnreclaim: 178764 kB + KernelStack: 17360 kB + PageTables: 50436 kB + NFS_Unstable: 0 kB + Bounce: 0 kB + WritebackTmp: 0 kB + CommitLimit: 9029620 kB + Committed_AS: 16681884 kB + VmallocTotal: 34359738367 kB + VmallocUsed: 41944 kB + VmallocChunk: 0 kB + Percpu: 4240 kB + HardwareCorrupted: 0 kB + AnonHugePages: 0 kB + ShmemHugePages: 0 kB + ShmemPmdMapped: 0 kB + FileHugePages: 0 kB + FilePmdMapped: 0 kB + CmaTotal: 0 kB + CmaFree: 0 kB + HugePages_Total: 0 + HugePages_Free: 7 + HugePages_Rsvd: 0 + HugePages_Surp: 0 + Hugepagesize: 2048 kB + Hugetlb: 0 kB + DirectMap4k: 481804 kB + DirectMap2M: 14901248 kB + DirectMap1G: 2097152 kB + """; + + @ParameterizedTest + @MethodSource + void testGetValue(final String metricName, final long expectedValue) { + assertEquals(expectedValue, new OperatingSystemMemoryGauge(metricName).getValue(MEMINFO.lines())); + } + + @SuppressWarnings("unused") + private static Stream testGetValue() { + return Stream.of( + Arguments.of("MemTotal", 16052208L), + Arguments.of("Active(anon)", 5580980L), + Arguments.of("Committed_AS", 16681884L), + Arguments.of("HugePages_Free", 7L), + Arguments.of("NonsenseMetric", 0L) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java new file mode 100644 index 000000000..e1ff4f53b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/metrics/UserAgentTagUtilTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.metrics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.vdurmont.semver4j.Semver; +import io.micrometer.core.instrument.Tag; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; + +class UserAgentTagUtilTest { + + @ParameterizedTest + @MethodSource + void getPlatformTag(final String userAgent, final Tag expectedTag) { + assertEquals(expectedTag, UserAgentTagUtil.getPlatformTag(userAgent)); + } + + private static Stream getPlatformTag() { + return Stream.of( + Arguments.of("This is obviously not a reasonable User-Agent string.", Tag.of(UserAgentTagUtil.PLATFORM_TAG, "unrecognized")), + Arguments.of(null, Tag.of(UserAgentTagUtil.PLATFORM_TAG, "unrecognized")), + Arguments.of("Signal-Android/4.53.7 (Android 8.1)", Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android")), + Arguments.of("Signal-Desktop/1.2.3", Tag.of(UserAgentTagUtil.PLATFORM_TAG, "desktop")), + Arguments.of("Signal-iOS/3.9.0 (iPhone; iOS 12.2; Scale/3.00)", Tag.of(UserAgentTagUtil.PLATFORM_TAG, "ios")), + Arguments.of("Signal-Android/1.2.3 (Android 8.1)", Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android")), + Arguments.of("Signal-Desktop/3.9.0", Tag.of(UserAgentTagUtil.PLATFORM_TAG, "desktop")), + Arguments.of("Signal-iOS/4.53.7 (iPhone; iOS 12.2; Scale/3.00)", Tag.of(UserAgentTagUtil.PLATFORM_TAG, "ios")), + Arguments.of("Signal-Android/4.68.3 (Android 9)", Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android")), + Arguments.of("Signal-Android/1.2.3 (Android 4.3)", Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android")), + Arguments.of("Signal-Android/4.68.3.0-bobsbootlegclient", Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android")), + Arguments.of("Signal-Desktop/1.22.45-foo-0", Tag.of(UserAgentTagUtil.PLATFORM_TAG, "desktop")), + Arguments.of("Signal-Desktop/1.34.5-beta.1-fakeclientemporium", Tag.of(UserAgentTagUtil.PLATFORM_TAG, "desktop")), + Arguments.of("Signal-Desktop/1.32.0-beta.3", Tag.of(UserAgentTagUtil.PLATFORM_TAG, "desktop")) + ); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + @ParameterizedTest + @MethodSource + void getClientVersionTag(final String userAgent, final boolean isVersionLive, final Optional expectedTag) { + final ClientReleaseManager clientReleaseManager = mock(ClientReleaseManager.class); + when(clientReleaseManager.isVersionActive(any(), any())).thenReturn(isVersionLive); + + assertEquals(expectedTag, UserAgentTagUtil.getClientVersionTag(userAgent, clientReleaseManager)); + } + + private static Stream getClientVersionTag() { + return Stream.of( + Arguments.of("Signal-Android/1.2.3 (Android 9)", + true, + Optional.of(Tag.of(UserAgentTagUtil.VERSION_TAG, "1.2.3"))), + + Arguments.of("Signal-Android/1.2.3 (Android 9)", + false, + Optional.empty()) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/providers/MultiRecipientMessageProviderTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/providers/MultiRecipientMessageProviderTest.java new file mode 100644 index 000000000..ff3240759 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/providers/MultiRecipientMessageProviderTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.providers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.io.ByteArrayInputStream; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class MultiRecipientMessageProviderTest { + + static byte[] createTwoByteArray(int b1, int b2) { + return new byte[]{(byte) b1, (byte) b2}; + } + + static Stream readU16TestCases() { + return Stream.of( + arguments(0xFFFE, createTwoByteArray(0xFF, 0xFE)), + arguments(0x0001, createTwoByteArray(0x00, 0x01)), + arguments(0xBEEF, createTwoByteArray(0xBE, 0xEF)), + arguments(0xFFFF, createTwoByteArray(0xFF, 0xFF)), + arguments(0x0000, createTwoByteArray(0x00, 0x00)), + arguments(0xF080, createTwoByteArray(0xF0, 0x80)) + ); + } + + @ParameterizedTest + @MethodSource("readU16TestCases") + void testReadU16(int expectedValue, byte[] input) throws Exception { + try (final ByteArrayInputStream stream = new ByteArrayInputStream(input)) { + assertThat(MultiRecipientMessageProvider.readU16(stream)).isEqualTo(expectedValue); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/APNSenderTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/APNSenderTest.java new file mode 100644 index 000000000..d302cf5ef --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/APNSenderTest.java @@ -0,0 +1,230 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.eatthepath.pushy.apns.ApnsClient; +import com.eatthepath.pushy.apns.ApnsPushNotification; +import com.eatthepath.pushy.apns.DeliveryPriority; +import com.eatthepath.pushy.apns.PushNotificationResponse; +import com.eatthepath.pushy.apns.PushType; +import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification; +import com.eatthepath.pushy.apns.util.concurrent.PushNotificationFuture; +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.CompletionException; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService; + +class APNSenderTest { + + private static final String DESTINATION_DEVICE_TOKEN = RandomStringUtils.randomAlphanumeric(32); + private static final String BUNDLE_ID = "org.signal.test"; + + private Account destinationAccount; + private Device destinationDevice; + + private ApnsClient apnsClient; + private APNSender apnSender; + + @BeforeEach + void setup() { + destinationAccount = mock(Account.class); + destinationDevice = mock(Device.class); + + apnsClient = mock(ApnsClient.class); + apnSender = new APNSender(new SynchronousExecutorService(), apnsClient, BUNDLE_ID); + + when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice)); + when(destinationDevice.getApnId()).thenReturn(DESTINATION_DEVICE_TOKEN); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSendVoip(final boolean urgent) { + PushNotificationResponse response = mock(PushNotificationResponse.class); + when(response.isAccepted()).thenReturn(true); + + when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class))) + .thenAnswer( + (Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); + + PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN_VOIP, + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, urgent); + + final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join(); + + ArgumentCaptor notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class); + verify(apnsClient).sendNotification(notification.capture()); + + assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_DEVICE_TOKEN); + assertThat(notification.getValue().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION); + assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD); + // Delivery priority should always be `IMMEDIATE` for VOIP notifications + assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); + assertThat(notification.getValue().getTopic()).isEqualTo(BUNDLE_ID + ".voip"); + + assertThat(result.accepted()).isTrue(); + assertThat(result.errorCode()).isNull(); + assertThat(result.unregistered()).isFalse(); + + verifyNoMoreInteractions(apnsClient); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSendApns(final boolean urgent) { + PushNotificationResponse response = mock(PushNotificationResponse.class); + when(response.isAccepted()).thenReturn(true); + + when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class))) + .thenAnswer( + (Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); + + PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN, + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, urgent); + + final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join(); + + ArgumentCaptor notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class); + verify(apnsClient).sendNotification(notification.capture()); + + assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_DEVICE_TOKEN); + assertThat(notification.getValue().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION); + assertThat(notification.getValue().getPayload()) + .isEqualTo(urgent ? APNSender.APN_NSE_NOTIFICATION_PAYLOAD : APNSender.APN_BACKGROUND_PAYLOAD); + + assertThat(notification.getValue().getPriority()) + .isEqualTo(urgent ? DeliveryPriority.IMMEDIATE : DeliveryPriority.CONSERVE_POWER); + + assertThat(notification.getValue().getTopic()).isEqualTo(BUNDLE_ID); + assertThat(notification.getValue().getPushType()) + .isEqualTo(urgent ? PushType.ALERT : PushType.BACKGROUND); + + if (urgent) { + assertThat(notification.getValue().getCollapseId()).isNotNull(); + } else { + assertThat(notification.getValue().getCollapseId()).isNull(); + } + + assertThat(result.accepted()).isTrue(); + assertThat(result.errorCode()).isNull(); + assertThat(result.unregistered()).isFalse(); + + verifyNoMoreInteractions(apnsClient); + } + + @Test + void testUnregisteredUser() { + PushNotificationResponse response = mock(PushNotificationResponse.class); + when(response.isAccepted()).thenReturn(false); + when(response.getRejectionReason()).thenReturn(Optional.of("Unregistered")); + + when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class))) + .thenAnswer( + (Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); + + PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN_VOIP, + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true); + + when(destinationDevice.getApnId()).thenReturn(DESTINATION_DEVICE_TOKEN); + when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(11)); + + final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join(); + + ArgumentCaptor notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class); + verify(apnsClient).sendNotification(notification.capture()); + + assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_DEVICE_TOKEN); + assertThat(notification.getValue().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION); + assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD); + assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); + + assertThat(result.accepted()).isFalse(); + assertThat(result.errorCode()).isEqualTo("Unregistered"); + assertThat(result.unregistered()).isTrue(); + } + + @Test + void testGenericFailure() { + PushNotificationResponse response = mock(PushNotificationResponse.class); + when(response.isAccepted()).thenReturn(false); + when(response.getRejectionReason()).thenReturn(Optional.of("BadTopic")); + + when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class))) + .thenAnswer( + (Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); + + PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN_VOIP, + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true); + + final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join(); + + ArgumentCaptor notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class); + verify(apnsClient).sendNotification(notification.capture()); + + assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_DEVICE_TOKEN); + assertThat(notification.getValue().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION); + assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD); + assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); + + assertThat(result.accepted()).isFalse(); + assertThat(result.errorCode()).isEqualTo("BadTopic"); + assertThat(result.unregistered()).isFalse(); + } + + @Test + void testFailure() { + PushNotificationResponse response = mock(PushNotificationResponse.class); + when(response.isAccepted()).thenReturn(true); + + when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class))) + .thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), + new IOException("lost connection"))); + + PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN_VOIP, + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true); + + assertThatThrownBy(() -> apnSender.sendNotification(pushNotification).join()) + .isInstanceOf(CompletionException.class) + .hasCauseInstanceOf(IOException.class); + + verify(apnsClient).sendNotification(any()); + + verifyNoMoreInteractions(apnsClient); + } + + private static class MockPushNotificationFuture

    extends + PushNotificationFuture { + + MockPushNotificationFuture(final P pushNotification, final V response) { + super(pushNotification); + complete(response); + } + + MockPushNotificationFuture(final P pushNotification, final Exception exception) { + super(pushNotification); + completeExceptionally(exception); + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationSchedulerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationSchedulerTest.java new file mode 100644 index 000000000..714e43625 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationSchedulerTest.java @@ -0,0 +1,255 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import io.lettuce.core.cluster.SlotHash; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.ArgumentCaptor; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.TestClock; + +class ApnPushNotificationSchedulerTest { + + @RegisterExtension + static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + private Account account; + private Device device; + + private APNSender apnSender; + private TestClock clock; + + private ApnPushNotificationScheduler apnPushNotificationScheduler; + + private static final UUID ACCOUNT_UUID = UUID.randomUUID(); + private static final String ACCOUNT_NUMBER = "+18005551234"; + private static final long DEVICE_ID = 1L; + private static final String APN_ID = RandomStringUtils.randomAlphanumeric(32); + private static final String VOIP_APN_ID = RandomStringUtils.randomAlphanumeric(32); + + @BeforeEach + void setUp() throws Exception { + + device = mock(Device.class); + when(device.getId()).thenReturn(DEVICE_ID); + when(device.getApnId()).thenReturn(APN_ID); + when(device.getVoipApnId()).thenReturn(VOIP_APN_ID); + when(device.getLastSeen()).thenReturn(System.currentTimeMillis()); + + account = mock(Account.class); + when(account.getUuid()).thenReturn(ACCOUNT_UUID); + when(account.getNumber()).thenReturn(ACCOUNT_NUMBER); + when(account.getDevice(DEVICE_ID)).thenReturn(Optional.of(device)); + + final AccountsManager accountsManager = mock(AccountsManager.class); + when(accountsManager.getByE164(ACCOUNT_NUMBER)).thenReturn(Optional.of(account)); + when(accountsManager.getByAccountIdentifier(ACCOUNT_UUID)).thenReturn(Optional.of(account)); + + apnSender = mock(APNSender.class); + clock = TestClock.now(); + + apnPushNotificationScheduler = new ApnPushNotificationScheduler(REDIS_CLUSTER_EXTENSION.getRedisCluster(), + apnSender, accountsManager, clock, 1); + } + + @Test + void testClusterInsert() throws ExecutionException, InterruptedException { + final String endpoint = ApnPushNotificationScheduler.getEndpointKey(account, device); + final long currentTimeMillis = System.currentTimeMillis(); + + assertTrue( + apnPushNotificationScheduler.getPendingDestinationsForRecurringVoipNotifications(SlotHash.getSlot(endpoint), 1).isEmpty()); + + clock.pin(Instant.ofEpochMilli(currentTimeMillis - 30_000)); + apnPushNotificationScheduler.scheduleRecurringVoipNotification(account, device).toCompletableFuture().get(); + + clock.pin(Instant.ofEpochMilli(currentTimeMillis)); + final List pendingDestinations = apnPushNotificationScheduler.getPendingDestinationsForRecurringVoipNotifications(SlotHash.getSlot(endpoint), 2); + assertEquals(1, pendingDestinations.size()); + + final Optional> maybeUuidAndDeviceId = ApnPushNotificationScheduler.getSeparated( + pendingDestinations.get(0)); + + assertTrue(maybeUuidAndDeviceId.isPresent()); + assertEquals(ACCOUNT_UUID.toString(), maybeUuidAndDeviceId.get().first()); + assertEquals(DEVICE_ID, (long) maybeUuidAndDeviceId.get().second()); + + assertTrue( + apnPushNotificationScheduler.getPendingDestinationsForRecurringVoipNotifications(SlotHash.getSlot(endpoint), 1).isEmpty()); + } + + @Test + void testProcessRecurringVoipNotifications() throws ExecutionException, InterruptedException { + final ApnPushNotificationScheduler.NotificationWorker worker = apnPushNotificationScheduler.new NotificationWorker(); + final long currentTimeMillis = System.currentTimeMillis(); + + clock.pin(Instant.ofEpochMilli(currentTimeMillis - 30_000)); + apnPushNotificationScheduler.scheduleRecurringVoipNotification(account, device).toCompletableFuture().get(); + + clock.pin(Instant.ofEpochMilli(currentTimeMillis)); + + final int slot = SlotHash.getSlot(ApnPushNotificationScheduler.getEndpointKey(account, device)); + + assertEquals(1, worker.processRecurringVoipNotifications(slot)); + + final ArgumentCaptor notificationCaptor = ArgumentCaptor.forClass(PushNotification.class); + verify(apnSender).sendNotification(notificationCaptor.capture()); + + final PushNotification pushNotification = notificationCaptor.getValue(); + + assertEquals(VOIP_APN_ID, pushNotification.deviceToken()); + assertEquals(account, pushNotification.destination()); + assertEquals(device, pushNotification.destinationDevice()); + + assertEquals(0, worker.processRecurringVoipNotifications(slot)); + } + + @Test + void testScheduleBackgroundNotificationWithNoRecentNotification() throws ExecutionException, InterruptedException { + final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); + clock.pin(now); + + assertEquals(Optional.empty(), + apnPushNotificationScheduler.getLastBackgroundNotificationTimestamp(account, device)); + + assertEquals(Optional.empty(), + apnPushNotificationScheduler.getNextScheduledBackgroundNotificationTimestamp(account, device)); + + apnPushNotificationScheduler.scheduleBackgroundNotification(account, device).toCompletableFuture().get(); + + assertEquals(Optional.of(now), + apnPushNotificationScheduler.getNextScheduledBackgroundNotificationTimestamp(account, device)); + } + + @Test + void testScheduleBackgroundNotificationWithRecentNotification() throws ExecutionException, InterruptedException { + final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); + final Instant recentNotificationTimestamp = + now.minus(ApnPushNotificationScheduler.BACKGROUND_NOTIFICATION_PERIOD.dividedBy(2)); + + // Insert a timestamp for a recently-sent background push notification + clock.pin(Instant.ofEpochMilli(recentNotificationTimestamp.toEpochMilli())); + apnPushNotificationScheduler.sendBackgroundNotification(account, device); + + clock.pin(now); + apnPushNotificationScheduler.scheduleBackgroundNotification(account, device).toCompletableFuture().get(); + + final Instant expectedScheduledTimestamp = + recentNotificationTimestamp.plus(ApnPushNotificationScheduler.BACKGROUND_NOTIFICATION_PERIOD); + + assertEquals(Optional.of(expectedScheduledTimestamp), + apnPushNotificationScheduler.getNextScheduledBackgroundNotificationTimestamp(account, device)); + } + + @Test + void testProcessScheduledBackgroundNotifications() throws ExecutionException, InterruptedException { + final ApnPushNotificationScheduler.NotificationWorker worker = apnPushNotificationScheduler.new NotificationWorker(); + + final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); + + clock.pin(Instant.ofEpochMilli(now.toEpochMilli())); + apnPushNotificationScheduler.scheduleBackgroundNotification(account, device).toCompletableFuture().get(); + + final int slot = + SlotHash.getSlot(ApnPushNotificationScheduler.getPendingBackgroundNotificationQueueKey(account, device)); + + clock.pin(Instant.ofEpochMilli(now.minusMillis(1).toEpochMilli())); + assertEquals(0, worker.processScheduledBackgroundNotifications(slot)); + + clock.pin(now); + assertEquals(1, worker.processScheduledBackgroundNotifications(slot)); + + final ArgumentCaptor notificationCaptor = ArgumentCaptor.forClass(PushNotification.class); + verify(apnSender).sendNotification(notificationCaptor.capture()); + + final PushNotification pushNotification = notificationCaptor.getValue(); + + assertEquals(PushNotification.TokenType.APN, pushNotification.tokenType()); + assertEquals(APN_ID, pushNotification.deviceToken()); + assertEquals(account, pushNotification.destination()); + assertEquals(device, pushNotification.destinationDevice()); + assertEquals(PushNotification.NotificationType.NOTIFICATION, pushNotification.notificationType()); + assertFalse(pushNotification.urgent()); + + assertEquals(0, worker.processRecurringVoipNotifications(slot)); + } + + @Test + void testProcessScheduledBackgroundNotificationsCancelled() throws ExecutionException, InterruptedException { + final ApnPushNotificationScheduler.NotificationWorker worker = apnPushNotificationScheduler.new NotificationWorker(); + + final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); + + clock.pin(now); + apnPushNotificationScheduler.scheduleBackgroundNotification(account, device).toCompletableFuture().get(); + apnPushNotificationScheduler.cancelScheduledNotifications(account, device).toCompletableFuture().get(); + + final int slot = + SlotHash.getSlot(ApnPushNotificationScheduler.getPendingBackgroundNotificationQueueKey(account, device)); + + assertEquals(0, worker.processScheduledBackgroundNotifications(slot)); + + verify(apnSender, never()).sendNotification(any()); + } + + @ParameterizedTest + @CsvSource({ + "1, true", + "0, false", + }) + void testDedicatedProcessDynamicConfiguration(final int dedicatedThreadCount, final boolean expectActivity) + throws Exception { + + final FaultTolerantRedisCluster redisCluster = mock(FaultTolerantRedisCluster.class); + when(redisCluster.withCluster(any())).thenReturn(0L); + + final AccountsManager accountsManager = mock(AccountsManager.class); + + final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); + + apnPushNotificationScheduler = new ApnPushNotificationScheduler(redisCluster, apnSender, + accountsManager, dedicatedThreadCount); + + apnPushNotificationScheduler.start(); + apnPushNotificationScheduler.stop(); + + if (expectActivity) { + verify(redisCluster, atLeastOnce()).withCluster(any()); + } else { + verifyNoInteractions(redisCluster); + verifyNoInteractions(accountsManager); + verifyNoInteractions(apnSender); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/ClientPresenceManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/ClientPresenceManagerTest.java new file mode 100644 index 000000000..82e70b5cf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/ClientPresenceManagerTest.java @@ -0,0 +1,388 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; + +import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; +import io.lettuce.core.cluster.event.ClusterTopologyChangedEvent; +import java.time.Duration; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; + +class ClientPresenceManagerTest { + + @RegisterExtension + static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + private ScheduledExecutorService presenceRenewalExecutorService; + private ClientPresenceManager clientPresenceManager; + + private static final DisplacedPresenceListener NO_OP = connectedElsewhere -> { + }; + + private boolean expectExceptionOnClientPresenceManagerStop = false; + + @BeforeEach + void setUp() throws Exception { + + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster(connection -> { + connection.sync().flushall(); + connection.sync().upstream().commands().configSet("notify-keyspace-events", "K$glz"); + }); + + presenceRenewalExecutorService = Executors.newSingleThreadScheduledExecutor(); + clientPresenceManager = new ClientPresenceManager(REDIS_CLUSTER_EXTENSION.getRedisCluster(), + presenceRenewalExecutorService, + presenceRenewalExecutorService); + } + + @AfterEach + public void tearDown() throws Exception { + presenceRenewalExecutorService.shutdown(); + presenceRenewalExecutorService.awaitTermination(1, TimeUnit.MINUTES); + + try { + clientPresenceManager.stop(); + } catch (final Exception e) { + if (!expectExceptionOnClientPresenceManagerStop) { + throw e; + } + } + } + + @Test + void testIsPresent() { + final UUID accountUuid = UUID.randomUUID(); + final long deviceId = 1; + + assertFalse(clientPresenceManager.isPresent(accountUuid, deviceId)); + + clientPresenceManager.setPresent(accountUuid, deviceId, NO_OP); + assertTrue(clientPresenceManager.isPresent(accountUuid, deviceId)); + } + + @Test + void testIsLocallyPresent() { + final UUID accountUuid = UUID.randomUUID(); + final long deviceId = 1; + + assertFalse(clientPresenceManager.isLocallyPresent(accountUuid, deviceId)); + + clientPresenceManager.setPresent(accountUuid, deviceId, NO_OP); + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster(connection -> connection.sync().flushall()); + + assertTrue(clientPresenceManager.isLocallyPresent(accountUuid, deviceId)); + } + + @Test + void testLocalDisplacement() { + final UUID accountUuid = UUID.randomUUID(); + final long deviceId = 1; + + final AtomicInteger displacementCounter = new AtomicInteger(0); + final DisplacedPresenceListener displacementListener = connectedElsewhere -> displacementCounter.incrementAndGet(); + + clientPresenceManager.setPresent(accountUuid, deviceId, displacementListener); + + assertEquals(0, displacementCounter.get()); + + clientPresenceManager.setPresent(accountUuid, deviceId, displacementListener); + + assertEquals(1, displacementCounter.get()); + } + + @Test + void testRemoteDisplacement() { + final UUID accountUuid = UUID.randomUUID(); + final long deviceId = 1; + + final CompletableFuture displaced = new CompletableFuture<>(); + + clientPresenceManager.start(); + + clientPresenceManager.setPresent(accountUuid, deviceId, connectedElsewhere -> displaced.complete(null)); + + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster( + connection -> connection.sync().set(ClientPresenceManager.getPresenceKey(accountUuid, deviceId), + UUID.randomUUID().toString())); + + assertTimeoutPreemptively(Duration.ofSeconds(10), displaced::join); + } + + @Test + void testRemoteDisplacementAfterTopologyChange() { + final UUID accountUuid = UUID.randomUUID(); + final long deviceId = 1; + + final CompletableFuture displaced = new CompletableFuture<>(); + + clientPresenceManager.start(); + + clientPresenceManager.setPresent(accountUuid, deviceId, connectedElsewhere -> displaced.complete(null)); + + clientPresenceManager.getPubSubConnection() + .usePubSubConnection(connection -> connection.getResources().eventBus() + .publish(new ClusterTopologyChangedEvent(List.of(), List.of()))); + + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster( + connection -> connection.sync().set(ClientPresenceManager.getPresenceKey(accountUuid, deviceId), + UUID.randomUUID().toString())); + + assertTimeoutPreemptively(Duration.ofSeconds(10), displaced::join); + } + + @Test + void testClearPresence() { + final UUID accountUuid = UUID.randomUUID(); + final long deviceId = 1; + + assertFalse(clientPresenceManager.isPresent(accountUuid, deviceId)); + + clientPresenceManager.setPresent(accountUuid, deviceId, NO_OP); + assertFalse(clientPresenceManager.clearPresence(accountUuid, deviceId, + ignored -> fail("this listener should never be called"))); + assertTrue(clientPresenceManager.clearPresence(accountUuid, deviceId, NO_OP)); + + clientPresenceManager.setPresent(accountUuid, deviceId, NO_OP); + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster( + connection -> connection.sync().set(ClientPresenceManager.getPresenceKey(accountUuid, deviceId), + UUID.randomUUID().toString())); + + assertFalse(clientPresenceManager.clearPresence(accountUuid, deviceId, NO_OP)); + } + + @Test + void testPruneMissingPeers() { + final String presentPeerId = UUID.randomUUID().toString(); + final String missingPeerId = UUID.randomUUID().toString(); + + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster(connection -> { + connection.sync().sadd(ClientPresenceManager.MANAGER_SET_KEY, presentPeerId); + connection.sync().sadd(ClientPresenceManager.MANAGER_SET_KEY, missingPeerId); + }); + + for (int i = 0; i < 10; i++) { + addClientPresence(presentPeerId); + addClientPresence(missingPeerId); + } + + clientPresenceManager.getPubSubConnection().usePubSubConnection( + connection -> connection.sync().upstream().commands() + .subscribe(ClientPresenceManager.getManagerPresenceChannel(presentPeerId))); + clientPresenceManager.pruneMissingPeers(); + + assertEquals(1, (long) REDIS_CLUSTER_EXTENSION.getRedisCluster().withCluster( + connection -> connection.sync().exists(ClientPresenceManager.getConnectedClientSetKey(presentPeerId)))); + assertTrue(REDIS_CLUSTER_EXTENSION.getRedisCluster().withCluster( + (Function, Boolean>) connection -> connection.sync() + .sismember(ClientPresenceManager.MANAGER_SET_KEY, presentPeerId))); + + assertEquals(0, (long) REDIS_CLUSTER_EXTENSION.getRedisCluster().withCluster( + connection -> connection.sync().exists(ClientPresenceManager.getConnectedClientSetKey(missingPeerId)))); + assertFalse(REDIS_CLUSTER_EXTENSION.getRedisCluster().withCluster( + (Function, Boolean>) connection -> connection.sync() + .sismember(ClientPresenceManager.MANAGER_SET_KEY, missingPeerId))); + } + + @Test + void testInitialPresenceExpiration() { + final UUID accountUuid = UUID.randomUUID(); + final long deviceId = 1; + + clientPresenceManager.setPresent(accountUuid, deviceId, NO_OP); + + { + final int ttl = REDIS_CLUSTER_EXTENSION.getRedisCluster().withCluster(connection -> + connection.sync().ttl(ClientPresenceManager.getPresenceKey(accountUuid, deviceId)).intValue()); + + assertTrue(ttl > 0); + } + } + + @Test + void testRenewPresence() { + final UUID accountUuid = UUID.randomUUID(); + final long deviceId = 1; + + final String presenceKey = ClientPresenceManager.getPresenceKey(accountUuid, deviceId); + + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster(connection -> + connection.sync().set(presenceKey, clientPresenceManager.getManagerId())); + + { + final int ttl = REDIS_CLUSTER_EXTENSION.getRedisCluster().withCluster(connection -> + connection.sync().ttl(presenceKey).intValue()); + + assertEquals(-1, ttl); + } + + clientPresenceManager.renewPresence(accountUuid, deviceId); + + { + final int ttl = REDIS_CLUSTER_EXTENSION.getRedisCluster().withCluster(connection -> + connection.sync().ttl(presenceKey).intValue()); + + assertTrue(ttl > 0); + } + } + + @Test + void testExpiredPresence() { + final UUID accountUuid = UUID.randomUUID(); + final long deviceId = 1; + + clientPresenceManager.setPresent(accountUuid, deviceId, NO_OP); + + assertTrue(clientPresenceManager.isPresent(accountUuid, deviceId)); + + // Hackily set this key to expire immediately + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster(connection -> + connection.sync().expire(ClientPresenceManager.getPresenceKey(accountUuid, deviceId), 0)); + + assertFalse(clientPresenceManager.isPresent(accountUuid, deviceId)); + } + + private void addClientPresence(final String managerId) { + final String clientPresenceKey = ClientPresenceManager.getPresenceKey(UUID.randomUUID(), 7); + + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster(connection -> { + connection.sync().set(clientPresenceKey, managerId); + connection.sync().sadd(ClientPresenceManager.getConnectedClientSetKey(managerId), clientPresenceKey); + }); + } + + @Test + void testClearAllOnStop() { + final int localAccounts = 10; + final UUID[] localUuids = new UUID[localAccounts]; + final long[] localDeviceIds = new long[localAccounts]; + + for (int i = 0; i < localAccounts; i++) { + localUuids[i] = UUID.randomUUID(); + localDeviceIds[i] = i; + + clientPresenceManager.setPresent(localUuids[i], localDeviceIds[i], NO_OP); + } + + final UUID displacedAccountUuid = UUID.randomUUID(); + final long displacedAccountDeviceId = 7; + + clientPresenceManager.setPresent(displacedAccountUuid, displacedAccountDeviceId, NO_OP); + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster(connection -> connection.sync() + .set(ClientPresenceManager.getPresenceKey(displacedAccountUuid, displacedAccountDeviceId), + UUID.randomUUID().toString())); + + clientPresenceManager.stop(); + + for (int i = 0; i < localAccounts; i++) { + localUuids[i] = UUID.randomUUID(); + localDeviceIds[i] = i; + + assertFalse(clientPresenceManager.isPresent(localUuids[i], localDeviceIds[i])); + } + + assertTrue(clientPresenceManager.isPresent(displacedAccountUuid, displacedAccountDeviceId)); + + expectExceptionOnClientPresenceManagerStop = true; + } + + @Nested + class MultiServerTest { + + private ClientPresenceManager server1; + private ClientPresenceManager server2; + + @BeforeEach + void setup() throws Exception { + + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster(connection -> { + connection.sync().flushall(); + connection.sync().upstream().commands().configSet("notify-keyspace-events", "K$glz"); + }); + + final ScheduledExecutorService scheduledExecutorService1 = mock(ScheduledExecutorService.class); + final ExecutorService keyspaceNotificationExecutorService1 = Executors.newSingleThreadExecutor(); + server1 = new ClientPresenceManager(REDIS_CLUSTER_EXTENSION.getRedisCluster(), + scheduledExecutorService1, keyspaceNotificationExecutorService1); + + final ScheduledExecutorService scheduledExecutorService2 = mock(ScheduledExecutorService.class); + final ExecutorService keyspaceNotificationExecutorService2 = Executors.newSingleThreadExecutor(); + server2 = new ClientPresenceManager(REDIS_CLUSTER_EXTENSION.getRedisCluster(), + scheduledExecutorService2, keyspaceNotificationExecutorService2); + + server1.start(); + server2.start(); + } + + @AfterEach + void teardown() { + server2.stop(); + server1.stop(); + } + + @Test + void testSetPresentRemotely() { + final UUID uuid1 = UUID.randomUUID(); + final long deviceId = 1L; + + final CompletableFuture displaced = new CompletableFuture<>(); + final DisplacedPresenceListener listener1 = connectedElsewhere -> displaced.complete(null); + server1.setPresent(uuid1, deviceId, listener1); + + server2.setPresent(uuid1, deviceId, connectedElsewhere -> {}); + + assertTimeoutPreemptively(Duration.ofSeconds(10), displaced::join); + } + + @Test + void testDisconnectPresenceLocally() { + final UUID uuid1 = UUID.randomUUID(); + final long deviceId = 1L; + + final CompletableFuture displaced = new CompletableFuture<>(); + final DisplacedPresenceListener listener1 = connectedElsewhere -> displaced.complete(null); + server1.setPresent(uuid1, deviceId, listener1); + + server1.disconnectPresence(uuid1, deviceId); + + assertTimeoutPreemptively(Duration.ofSeconds(10), displaced::join); + } + + @Test + void testDisconnectPresenceRemotely() { + final UUID uuid1 = UUID.randomUUID(); + final long deviceId = 1L; + + final CompletableFuture displaced = new CompletableFuture<>(); + final DisplacedPresenceListener listener1 = connectedElsewhere -> displaced.complete(null); + server1.setPresent(uuid1, deviceId, listener1); + + server2.disconnectPresence(uuid1, deviceId); + + assertTimeoutPreemptively(Duration.ofSeconds(10), displaced::join); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/FcmSenderTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/FcmSenderTest.java new file mode 100644 index 000000000..7b337cfcf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/FcmSenderTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.core.SettableApiFuture; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.MessagingErrorCode; +import java.io.IOException; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService; + +class FcmSenderTest { + + private ExecutorService executorService; + private FirebaseMessaging firebaseMessaging; + + private FcmSender fcmSender; + + @BeforeEach + void setUp() { + executorService = new SynchronousExecutorService(); + firebaseMessaging = mock(FirebaseMessaging.class); + + fcmSender = new FcmSender(executorService, firebaseMessaging); + } + + @AfterEach + void tearDown() throws InterruptedException { + executorService.shutdown(); + + //noinspection ResultOfMethodCallIgnored + executorService.awaitTermination(1, TimeUnit.SECONDS); + } + + @Test + void testSendMessage() { + final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null, true); + + final SettableApiFuture sendFuture = SettableApiFuture.create(); + sendFuture.set("message-id"); + + when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture); + + final SendPushNotificationResult result = fcmSender.sendNotification(pushNotification).join(); + + verify(firebaseMessaging).sendAsync(any(Message.class)); + assertTrue(result.accepted()); + assertNull(result.errorCode()); + assertFalse(result.unregistered()); + } + + @Test + void testSendMessageRejected() { + final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null, true); + + final FirebaseMessagingException invalidArgumentException = mock(FirebaseMessagingException.class); + when(invalidArgumentException.getMessagingErrorCode()).thenReturn(MessagingErrorCode.INVALID_ARGUMENT); + + final SettableApiFuture sendFuture = SettableApiFuture.create(); + sendFuture.setException(invalidArgumentException); + + when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture); + + final SendPushNotificationResult result = fcmSender.sendNotification(pushNotification).join(); + + verify(firebaseMessaging).sendAsync(any(Message.class)); + assertFalse(result.accepted()); + assertEquals("INVALID_ARGUMENT", result.errorCode()); + assertFalse(result.unregistered()); + } + + @Test + void testSendMessageUnregistered() { + final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null, true); + + final FirebaseMessagingException unregisteredException = mock(FirebaseMessagingException.class); + when(unregisteredException.getMessagingErrorCode()).thenReturn(MessagingErrorCode.UNREGISTERED); + + final SettableApiFuture sendFuture = SettableApiFuture.create(); + sendFuture.setException(unregisteredException); + + when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture); + + final SendPushNotificationResult result = fcmSender.sendNotification(pushNotification).join(); + + verify(firebaseMessaging).sendAsync(any(Message.class)); + assertFalse(result.accepted()); + assertEquals("UNREGISTERED", result.errorCode()); + assertTrue(result.unregistered()); + } + + @Test + void testSendMessageException() { + final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null, true); + + final SettableApiFuture sendFuture = SettableApiFuture.create(); + sendFuture.setException(new IOException()); + + when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture); + + final CompletionException completionException = + assertThrows(CompletionException.class, () -> fcmSender.sendNotification(pushNotification).join()); + + verify(firebaseMessaging).sendAsync(any(Message.class)); + assertTrue(completionException.getCause() instanceof IOException); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/MessageSenderTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/MessageSenderTest.java new file mode 100644 index 000000000..87cb32832 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/MessageSenderTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import java.util.UUID; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.MessagesManager; + +class MessageSenderTest { + + private Account account; + private Device device; + private MessageProtos.Envelope message; + + private ClientPresenceManager clientPresenceManager; + private MessagesManager messagesManager; + private PushNotificationManager pushNotificationManager; + private MessageSender messageSender; + + private static final UUID ACCOUNT_UUID = UUID.randomUUID(); + private static final long DEVICE_ID = 1L; + + @BeforeEach + void setUp() { + + account = mock(Account.class); + device = mock(Device.class); + message = generateRandomMessage(); + + clientPresenceManager = mock(ClientPresenceManager.class); + messagesManager = mock(MessagesManager.class); + pushNotificationManager = mock(PushNotificationManager.class); + messageSender = new MessageSender(clientPresenceManager, + messagesManager, + pushNotificationManager, + mock(PushLatencyManager.class)); + + when(account.getUuid()).thenReturn(ACCOUNT_UUID); + when(device.getId()).thenReturn(DEVICE_ID); + } + + @Test + void testSendOnlineMessageClientPresent() throws Exception { + when(clientPresenceManager.isPresent(ACCOUNT_UUID, DEVICE_ID)).thenReturn(true); + when(device.getGcmId()).thenReturn("gcm-id"); + + messageSender.sendMessage(account, device, message, true); + + ArgumentCaptor envelopeArgumentCaptor = ArgumentCaptor.forClass( + MessageProtos.Envelope.class); + + verify(messagesManager).insert(any(), anyLong(), envelopeArgumentCaptor.capture()); + + assertTrue(envelopeArgumentCaptor.getValue().getEphemeral()); + + verifyNoInteractions(pushNotificationManager); + } + + @Test + void testSendOnlineMessageClientNotPresent() throws Exception { + when(clientPresenceManager.isPresent(ACCOUNT_UUID, DEVICE_ID)).thenReturn(false); + when(device.getGcmId()).thenReturn("gcm-id"); + + messageSender.sendMessage(account, device, message, true); + + verify(messagesManager, never()).insert(any(), anyLong(), any()); + verifyNoInteractions(pushNotificationManager); + } + + @Test + void testSendMessageClientPresent() throws Exception { + when(clientPresenceManager.isPresent(ACCOUNT_UUID, DEVICE_ID)).thenReturn(true); + when(device.getGcmId()).thenReturn("gcm-id"); + + messageSender.sendMessage(account, device, message, false); + + final ArgumentCaptor envelopeArgumentCaptor = ArgumentCaptor.forClass( + MessageProtos.Envelope.class); + + verify(messagesManager).insert(eq(ACCOUNT_UUID), eq(DEVICE_ID), envelopeArgumentCaptor.capture()); + + assertFalse(envelopeArgumentCaptor.getValue().getEphemeral()); + assertEquals(message, envelopeArgumentCaptor.getValue()); + verifyNoInteractions(pushNotificationManager); + } + + @Test + void testSendMessageGcmClientNotPresent() throws Exception { + when(clientPresenceManager.isPresent(ACCOUNT_UUID, DEVICE_ID)).thenReturn(false); + when(device.getGcmId()).thenReturn("gcm-id"); + + messageSender.sendMessage(account, device, message, false); + + verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message); + verify(pushNotificationManager).sendNewMessageNotification(account, device.getId(), message.getUrgent()); + } + + @Test + void testSendMessageApnClientNotPresent() throws Exception { + when(clientPresenceManager.isPresent(ACCOUNT_UUID, DEVICE_ID)).thenReturn(false); + when(device.getApnId()).thenReturn("apn-id"); + + messageSender.sendMessage(account, device, message, false); + + verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message); + verify(pushNotificationManager).sendNewMessageNotification(account, device.getId(), message.getUrgent()); + } + + @Test + void testSendMessageFetchClientNotPresent() throws Exception { + when(clientPresenceManager.isPresent(ACCOUNT_UUID, DEVICE_ID)).thenReturn(false); + when(device.getFetchesMessages()).thenReturn(true); + + doThrow(NotPushRegisteredException.class) + .when(pushNotificationManager).sendNewMessageNotification(account, DEVICE_ID, message.getUrgent()); + + assertDoesNotThrow(() -> messageSender.sendMessage(account, device, message, false)); + verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message); + } + + private MessageProtos.Envelope generateRandomMessage() { + return MessageProtos.Envelope.newBuilder() + .setTimestamp(System.currentTimeMillis()) + .setServerTimestamp(System.currentTimeMillis()) + .setContent(ByteString.copyFromUtf8(RandomStringUtils.randomAlphanumeric(256))) + .setType(MessageProtos.Envelope.Type.CIPHERTEXT) + .setServerGuid(UUID.randomUUID().toString()) + .build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/ProvisioningManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/ProvisioningManagerTest.java new file mode 100644 index 000000000..ceb4c9daf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/ProvisioningManagerTest.java @@ -0,0 +1,82 @@ +package org.whispersystems.textsecuregcm.push; + +import com.google.protobuf.ByteString; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; +import org.whispersystems.textsecuregcm.redis.RedisSingletonExtension; +import org.whispersystems.textsecuregcm.storage.PubSubProtos; +import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress; + +import java.time.Duration; +import java.util.Random; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.after; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +class ProvisioningManagerTest { + + private ProvisioningManager provisioningManager; + + @RegisterExtension + static final RedisSingletonExtension REDIS_EXTENSION = RedisSingletonExtension.builder().build(); + + private static final long PUBSUB_TIMEOUT_MILLIS = 1_000; + + @BeforeEach + void setUp() throws Exception { + provisioningManager = new ProvisioningManager(REDIS_EXTENSION.getRedisClient(), Duration.ofSeconds(1), new CircuitBreakerConfiguration()); + provisioningManager.start(); + } + + @AfterEach + void tearDown() throws Exception { + provisioningManager.stop(); + } + + @Test + void sendProvisioningMessage() { + final ProvisioningAddress address = new ProvisioningAddress("address", 0); + + final byte[] content = new byte[16]; + new Random().nextBytes(content); + + @SuppressWarnings("unchecked") final Consumer subscribedConsumer = mock(Consumer.class); + + provisioningManager.addListener(address, subscribedConsumer); + provisioningManager.sendProvisioningMessage(address, content); + + final ArgumentCaptor messageCaptor = + ArgumentCaptor.forClass(PubSubProtos.PubSubMessage.class); + + verify(subscribedConsumer, timeout(PUBSUB_TIMEOUT_MILLIS)).accept(messageCaptor.capture()); + + assertEquals(PubSubProtos.PubSubMessage.Type.DELIVER, messageCaptor.getValue().getType()); + assertEquals(ByteString.copyFrom(content), messageCaptor.getValue().getContent()); + } + + @Test + void removeListener() { + final ProvisioningAddress address = new ProvisioningAddress("address", 0); + + final byte[] content = new byte[16]; + new Random().nextBytes(content); + + @SuppressWarnings("unchecked") final Consumer subscribedConsumer = mock(Consumer.class); + + provisioningManager.addListener(address, subscribedConsumer); + provisioningManager.removeListener(address); + provisioningManager.sendProvisioningMessage(address, content); + + // Make sure that we give the message enough time to show up (if it was going to) before declaring victory + verify(subscribedConsumer, after(PUBSUB_TIMEOUT_MILLIS).never()).accept(any()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/PushLatencyManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/PushLatencyManagerTest.java new file mode 100644 index 000000000..3fc81e29f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/PushLatencyManagerTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.push.PushLatencyManager.PushRecord; +import org.whispersystems.textsecuregcm.push.PushLatencyManager.PushType; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; + +class PushLatencyManagerTest { + + @RegisterExtension + static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + @ParameterizedTest + @MethodSource + void testTakeRecord(final boolean isVoip, final boolean isUrgent) throws ExecutionException, InterruptedException { + final UUID accountUuid = UUID.randomUUID(); + final long deviceId = 1; + + final Instant pushTimestamp = Instant.now(); + + final PushLatencyManager pushLatencyManager = new PushLatencyManager(REDIS_CLUSTER_EXTENSION.getRedisCluster(), + mock(ClientReleaseManager.class), Clock.fixed(pushTimestamp, ZoneId.systemDefault())); + + assertNull(pushLatencyManager.takePushRecord(accountUuid, deviceId).get()); + + pushLatencyManager.recordPushSent(accountUuid, deviceId, isVoip, isUrgent); + + final PushRecord pushRecord = pushLatencyManager.takePushRecord(accountUuid, deviceId).get(); + + assertNotNull(pushRecord); + assertEquals(pushTimestamp, pushRecord.timestamp()); + assertEquals(isVoip ? PushType.VOIP : PushType.STANDARD, pushRecord.pushType()); + assertEquals(Optional.of(isUrgent), pushRecord.urgent()); + + assertNull(pushLatencyManager.takePushRecord(accountUuid, deviceId).get()); + } + + private static Stream testTakeRecord() { + return Stream.of( + Arguments.of(true, true), + Arguments.of(true, false), + Arguments.of(false, true), + Arguments.of(false, false) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/PushNotificationManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/PushNotificationManagerTest.java new file mode 100644 index 000000000..e3cdcf9a9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/push/PushNotificationManagerTest.java @@ -0,0 +1,288 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.push; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.net.HttpHeaders; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.util.Util; + +class PushNotificationManagerTest { + + private AccountsManager accountsManager; + private APNSender apnSender; + private FcmSender fcmSender; + private ApnPushNotificationScheduler apnPushNotificationScheduler; + private PushLatencyManager pushLatencyManager; + + private PushNotificationManager pushNotificationManager; + + @BeforeEach + void setUp() { + accountsManager = mock(AccountsManager.class); + apnSender = mock(APNSender.class); + fcmSender = mock(FcmSender.class); + apnPushNotificationScheduler = mock(ApnPushNotificationScheduler.class); + pushLatencyManager = mock(PushLatencyManager.class); + + AccountsHelper.setupMockUpdate(accountsManager); + + pushNotificationManager = new PushNotificationManager(accountsManager, apnSender, fcmSender, + apnPushNotificationScheduler, pushLatencyManager); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void sendNewMessageNotification(final boolean urgent) throws NotPushRegisteredException { + final Account account = mock(Account.class); + final Device device = mock(Device.class); + + final String deviceToken = "token"; + + when(device.getId()).thenReturn(Device.MASTER_ID); + when(device.getGcmId()).thenReturn(deviceToken); + when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); + + when(fcmSender.sendNotification(any())) + .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); + + pushNotificationManager.sendNewMessageNotification(account, Device.MASTER_ID, urgent); + verify(fcmSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent)); + } + + @Test + void sendRegistrationChallengeNotification() { + final String deviceToken = "token"; + final String challengeToken = "challenge"; + + when(apnSender.sendNotification(any())) + .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); + + pushNotificationManager.sendRegistrationChallengeNotification(deviceToken, PushNotification.TokenType.APN_VOIP, challengeToken); + verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null, true)); + } + + @Test + void sendRateLimitChallengeNotification() throws NotPushRegisteredException { + final Account account = mock(Account.class); + final Device device = mock(Device.class); + + final String deviceToken = "token"; + final String challengeToken = "challenge"; + + when(device.getId()).thenReturn(Device.MASTER_ID); + when(device.getApnId()).thenReturn(deviceToken); + when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); + + when(apnSender.sendNotification(any())) + .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); + + pushNotificationManager.sendRateLimitChallengeNotification(account, challengeToken); + verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN, PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, account, device, true)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void sendAttemptLoginNotification(final boolean isApn) throws NotPushRegisteredException { + final Account account = mock(Account.class); + final Device device = mock(Device.class); + + final String deviceToken = "token"; + + when(device.getId()).thenReturn(Device.MASTER_ID); + if (isApn) { + when(device.getApnId()).thenReturn(deviceToken); + when(apnSender.sendNotification(any())) + .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); + when(apnPushNotificationScheduler.scheduleBackgroundNotification(account, device)) + .thenReturn(CompletableFuture.completedFuture(null)); + } else { + when(device.getGcmId()).thenReturn(deviceToken); + when(fcmSender.sendNotification(any())) + .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); + } + when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); + + pushNotificationManager.sendAttemptLoginNotification(account, "someContext"); + + if (isApn){ + verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN, + PushNotification.NotificationType.ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY, "someContext", account, device, true)); + verify(apnPushNotificationScheduler).scheduleBackgroundNotification(account, device); + } else { + verify(fcmSender, times(1)).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.FCM, + PushNotification.NotificationType.ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY, "someContext", account, device, true)); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSendNotificationFcm(final boolean urgent) { + final Account account = mock(Account.class); + final Device device = mock(Device.class); + + when(device.getId()).thenReturn(Device.MASTER_ID); + when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); + + final PushNotification pushNotification = new PushNotification( + "token", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent); + + when(fcmSender.sendNotification(pushNotification)) + .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); + + pushNotificationManager.sendNotification(pushNotification); + + verify(fcmSender).sendNotification(pushNotification); + verifyNoInteractions(apnSender); + verify(accountsManager, never()).updateDevice(eq(account), eq(Device.MASTER_ID), any()); + verify(device, never()).setUninstalledFeedbackTimestamp(Util.todayInMillis()); + verifyNoInteractions(apnPushNotificationScheduler); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSendNotificationApn(final boolean urgent) { + final Account account = mock(Account.class); + final Device device = mock(Device.class); + + when(device.getId()).thenReturn(Device.MASTER_ID); + when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); + + final PushNotification pushNotification = new PushNotification( + "token", PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent); + + when(apnSender.sendNotification(pushNotification)) + .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); + + if (!urgent) { + when(apnPushNotificationScheduler.scheduleBackgroundNotification(account, device)) + .thenReturn(CompletableFuture.completedFuture(null)); + } + + pushNotificationManager.sendNotification(pushNotification); + + verifyNoInteractions(fcmSender); + + if (urgent) { + verify(apnSender).sendNotification(pushNotification); + verifyNoInteractions(apnPushNotificationScheduler); + } else { + verifyNoInteractions(apnSender); + verify(apnPushNotificationScheduler).scheduleBackgroundNotification(account, device); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSendNotificationApnVoip(final boolean urgent) { + final Account account = mock(Account.class); + final Device device = mock(Device.class); + + when(device.getId()).thenReturn(Device.MASTER_ID); + when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); + + final PushNotification pushNotification = new PushNotification( + "token", PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent); + + when(apnSender.sendNotification(pushNotification)) + .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); + + pushNotificationManager.sendNotification(pushNotification); + + verify(apnSender).sendNotification(pushNotification); + + verifyNoInteractions(fcmSender); + verify(accountsManager, never()).updateDevice(eq(account), eq(Device.MASTER_ID), any()); + verify(device, never()).setUninstalledFeedbackTimestamp(Util.todayInMillis()); + verify(apnPushNotificationScheduler).scheduleRecurringVoipNotification(account, device); + verify(apnPushNotificationScheduler, never()).scheduleBackgroundNotification(any(), any()); + } + + @Test + void testSendNotificationUnregisteredFcm() { + final Account account = mock(Account.class); + final Device device = mock(Device.class); + + when(device.getId()).thenReturn(Device.MASTER_ID); + when(device.getGcmId()).thenReturn("token"); + when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); + + final PushNotification pushNotification = new PushNotification( + "token", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device, true); + + when(fcmSender.sendNotification(pushNotification)) + .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, null, true))); + + pushNotificationManager.sendNotification(pushNotification); + + verify(accountsManager).updateDevice(eq(account), eq(Device.MASTER_ID), any()); + verify(device).setUninstalledFeedbackTimestamp(Util.todayInMillis()); + verifyNoInteractions(apnSender); + verifyNoInteractions(apnPushNotificationScheduler); + } + + @Test + void testSendNotificationUnregisteredApn() { + final Account account = mock(Account.class); + final Device device = mock(Device.class); + + when(device.getId()).thenReturn(Device.MASTER_ID); + when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); + + final PushNotification pushNotification = new PushNotification( + "token", PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device, true); + + when(apnSender.sendNotification(pushNotification)) + .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, null, true))); + + when(apnPushNotificationScheduler.cancelScheduledNotifications(account, device)) + .thenReturn(CompletableFuture.completedFuture(null)); + + pushNotificationManager.sendNotification(pushNotification); + + verifyNoInteractions(fcmSender); + verify(accountsManager, never()).updateDevice(eq(account), eq(Device.MASTER_ID), any()); + verify(device, never()).setUninstalledFeedbackTimestamp(Util.todayInMillis()); + verify(apnPushNotificationScheduler).cancelScheduledNotifications(account, device); + } + + @Test + void testHandleMessagesRetrieved() { + final UUID accountIdentifier = UUID.randomUUID(); + final Account account = mock(Account.class); + final Device device = mock(Device.class); + final String userAgent = HttpHeaders.USER_AGENT; + + when(account.getUuid()).thenReturn(accountIdentifier); + when(device.getId()).thenReturn(Device.MASTER_ID); + + when(apnPushNotificationScheduler.cancelScheduledNotifications(account, device)) + .thenReturn(CompletableFuture.completedFuture(null)); + + pushNotificationManager.handleMessagesRetrieved(account, device, userAgent); + + verify(pushLatencyManager).recordQueueRead(accountIdentifier, Device.MASTER_ID, userAgent); + verify(apnPushNotificationScheduler).cancelScheduledNotifications(account, device); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/ClusterLuaScriptTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/ClusterLuaScriptTest.java new file mode 100644 index 000000000..992df8921 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/ClusterLuaScriptTest.java @@ -0,0 +1,211 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.redis; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.lettuce.core.FlushMode; +import io.lettuce.core.RedisFuture; +import io.lettuce.core.RedisNoScriptException; +import io.lettuce.core.ScriptOutputType; +import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; +import io.lettuce.core.cluster.api.reactive.RedisAdvancedClusterReactiveCommands; +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import io.lettuce.core.protocol.AsyncCommand; +import io.lettuce.core.protocol.RedisCommand; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper; +import reactor.core.publisher.Flux; + +class ClusterLuaScriptTest { + + @RegisterExtension + static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + @Test + void testExecute() { + final RedisAdvancedClusterCommands commands = mock(RedisAdvancedClusterCommands.class); + final FaultTolerantRedisCluster mockCluster = RedisClusterHelper.builder().stringCommands(commands).build(); + + final String script = "return redis.call(\"SET\", KEYS[1], ARGV[1])"; + final ScriptOutputType scriptOutputType = ScriptOutputType.VALUE; + final List keys = List.of("key"); + final List values = List.of("value"); + + when(commands.evalsha(any(), any(), any(), any())).thenReturn("OK"); + + final ClusterLuaScript luaScript = new ClusterLuaScript(mockCluster, script, scriptOutputType); + luaScript.execute(keys, values); + + verify(commands).evalsha(luaScript.getSha(), scriptOutputType, keys.toArray(new String[0]), values.toArray(new String[0])); + verify(commands, never()).eval(anyString(), any(), any(), any()); + } + + @Test + void testExecuteScriptNotLoaded() { + final RedisAdvancedClusterCommands commands = mock(RedisAdvancedClusterCommands.class); + final FaultTolerantRedisCluster mockCluster = RedisClusterHelper.builder().stringCommands(commands).build(); + + final String script = "return redis.call(\"SET\", KEYS[1], ARGV[1])"; + final ScriptOutputType scriptOutputType = ScriptOutputType.VALUE; + final List keys = List.of("key"); + final List values = List.of("value"); + + when(commands.evalsha(any(), any(), any(), any())).thenThrow(new RedisNoScriptException("OH NO")); + + final ClusterLuaScript luaScript = new ClusterLuaScript(mockCluster, script, scriptOutputType); + luaScript.execute(keys, values); + + verify(commands).eval(script, scriptOutputType, keys.toArray(new String[0]), values.toArray(new String[0])); + verify(commands).evalsha(luaScript.getSha(), scriptOutputType, keys.toArray(new String[0]), values.toArray(new String[0])); + } + + @Test + void testExecuteBinaryScriptNotLoaded() { + final RedisAdvancedClusterCommands stringCommands = mock(RedisAdvancedClusterCommands.class); + final RedisAdvancedClusterCommands binaryCommands = mock(RedisAdvancedClusterCommands.class); + final FaultTolerantRedisCluster mockCluster = RedisClusterHelper.builder() + .stringCommands(stringCommands) + .binaryCommands(binaryCommands) + .build(); + + final String script = "return redis.call(\"SET\", KEYS[1], ARGV[1])"; + final ScriptOutputType scriptOutputType = ScriptOutputType.VALUE; + final List keys = List.of("key".getBytes(StandardCharsets.UTF_8)); + final List values = List.of("value".getBytes(StandardCharsets.UTF_8)); + + when(binaryCommands.evalsha(any(), any(), any(), any())).thenThrow(new RedisNoScriptException("OH NO")); + + final ClusterLuaScript luaScript = new ClusterLuaScript(mockCluster, script, scriptOutputType); + luaScript.executeBinary(keys, values); + + verify(binaryCommands).eval(script, scriptOutputType, keys.toArray(new byte[0][]), values.toArray(new byte[0][])); + verify(binaryCommands).evalsha(luaScript.getSha(), scriptOutputType, keys.toArray(new byte[0][]), + values.toArray(new byte[0][])); + } + + @Test + void testExecuteBinaryAsyncScriptNotLoaded() throws Exception { + final RedisAdvancedClusterAsyncCommands binaryAsyncCommands = + mock(RedisAdvancedClusterAsyncCommands.class); + final FaultTolerantRedisCluster mockCluster = + RedisClusterHelper.builder().binaryAsyncCommands(binaryAsyncCommands).build(); + + final String script = "return redis.call(\"SET\", KEYS[1], ARGV[1])"; + final ScriptOutputType scriptOutputType = ScriptOutputType.VALUE; + final List keys = List.of("key".getBytes(StandardCharsets.UTF_8)); + final List values = List.of("value".getBytes(StandardCharsets.UTF_8)); + + final AsyncCommand evalShaFailure = new AsyncCommand<>(mock(RedisCommand.class)); + evalShaFailure.completeExceptionally(new RedisNoScriptException("OH NO")); + + final AsyncCommand evalSuccess = new AsyncCommand<>(mock(RedisCommand.class)); + evalSuccess.complete(); + + when(binaryAsyncCommands.evalsha(any(), any(), any(), any())).thenReturn((RedisFuture) evalShaFailure); + when(binaryAsyncCommands.eval(anyString(), any(), any(), any())).thenReturn((RedisFuture) evalSuccess); + + final ClusterLuaScript luaScript = new ClusterLuaScript(mockCluster, script, scriptOutputType); + luaScript.executeBinaryAsync(keys, values).get(5, TimeUnit.SECONDS); + + verify(binaryAsyncCommands).eval(script, scriptOutputType, keys.toArray(new byte[0][]), + values.toArray(new byte[0][])); + verify(binaryAsyncCommands).evalsha(luaScript.getSha(), scriptOutputType, keys.toArray(new byte[0][]), + values.toArray(new byte[0][])); + } + + @Test + void testExecuteBinaryReactiveScriptNotLoaded() { + final RedisAdvancedClusterReactiveCommands binaryReactiveCommands = + mock(RedisAdvancedClusterReactiveCommands.class); + final FaultTolerantRedisCluster mockCluster = RedisClusterHelper.builder() + .binaryReactiveCommands(binaryReactiveCommands).build(); + + final String script = "return redis.call(\"SET\", KEYS[1], ARGV[1])"; + final ScriptOutputType scriptOutputType = ScriptOutputType.VALUE; + final List keys = List.of("key".getBytes(StandardCharsets.UTF_8)); + final List values = List.of("value".getBytes(StandardCharsets.UTF_8)); + + when(binaryReactiveCommands.evalsha(any(), any(), any(), any())) + .thenReturn(Flux.error(new RedisNoScriptException("OH NO"))); + when(binaryReactiveCommands.eval(anyString(), any(), any(), any())).thenReturn(Flux.just("ok")); + + final ClusterLuaScript luaScript = new ClusterLuaScript(mockCluster, script, scriptOutputType); + luaScript.executeBinaryReactive(keys, values).blockLast(Duration.ofSeconds(5)); + + verify(binaryReactiveCommands).eval(script, scriptOutputType, keys.toArray(new byte[0][]), + values.toArray(new byte[0][])); + verify(binaryReactiveCommands).evalsha(luaScript.getSha(), scriptOutputType, keys.toArray(new byte[0][]), + values.toArray(new byte[0][])); + } + + @ParameterizedTest + @EnumSource(ExecuteMode.class) + void testExecuteRealCluster(final ExecuteMode mode) throws Exception { + REDIS_CLUSTER_EXTENSION.getRedisCluster().withCluster(c -> c.sync().scriptFlush(FlushMode.SYNC)); + REDIS_CLUSTER_EXTENSION.getRedisCluster().withCluster(c -> c.sync().configResetstat()); + + final ClusterLuaScript script = new ClusterLuaScript(REDIS_CLUSTER_EXTENSION.getRedisCluster(), + "return 2;", + ScriptOutputType.INTEGER); + + for (int i = 0; i < 7; i++) { + final long actual = switch (mode) { + case SYNC -> (long) script.execute(Collections.emptyList(), Collections.emptyList()); + case ASYNC -> + (long) script.executeAsync(Collections.emptyList(), Collections.emptyList()).get(5, TimeUnit.SECONDS); + case REACTIVE -> (long) script.executeReactive(Collections.emptyList(), Collections.emptyList()) + .blockLast(Duration.ofSeconds(5)); + }; + + assertEquals(2L, actual); + } + + final int evalCount = REDIS_CLUSTER_EXTENSION.getRedisCluster().withCluster(connection -> { + final String commandStats = connection.sync().info("commandstats"); + + // We're looking for (and parsing) a line in the command stats that looks like: + // + // ``` + // cmdstat_eval:calls=1,usec=44,usec_per_call=44.00 + // ``` + return Arrays.stream(commandStats.split("\\n")) + .filter(line -> line.startsWith("cmdstat_eval:")) + .map(String::trim) + .map(evalLine -> Arrays.stream(evalLine.substring(evalLine.indexOf(':') + 1).split(",")) + .filter(pair -> pair.startsWith("calls=")) + .map(callsPair -> Integer.parseInt(callsPair.substring(callsPair.indexOf('=') + 1))) + .findFirst() + .orElse(0)) + .findFirst() + .orElse(0); + }); + + assertEquals(1, evalCount); + } + + private enum ExecuteMode { + SYNC, + ASYNC, + REACTIVE + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/FaultTolerantPubSubConnectionTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/FaultTolerantPubSubConnectionTest.java new file mode 100644 index 000000000..71efef66b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/FaultTolerantPubSubConnectionTest.java @@ -0,0 +1,225 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.redis; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.github.resilience4j.circuitbreaker.CallNotPermittedException; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.core.IntervalFunction; +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; +import io.lettuce.core.RedisCommandTimeoutException; +import io.lettuce.core.RedisException; +import io.lettuce.core.cluster.event.ClusterTopologyChangedEvent; +import io.lettuce.core.cluster.pubsub.StatefulRedisClusterPubSubConnection; +import io.lettuce.core.cluster.pubsub.api.sync.RedisClusterPubSubCommands; +import io.lettuce.core.event.Event; +import io.lettuce.core.event.EventBus; +import io.lettuce.core.resource.ClientResources; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; +import org.whispersystems.textsecuregcm.configuration.RetryConfiguration; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Schedulers; +import reactor.test.publisher.TestPublisher; + +class FaultTolerantPubSubConnectionTest { + + private StatefulRedisClusterPubSubConnection pubSubConnection; + private RedisClusterPubSubCommands pubSubCommands; + private FaultTolerantPubSubConnection faultTolerantPubSubConnection; + + + @SuppressWarnings("unchecked") + @BeforeEach + public void setUp() { + pubSubConnection = mock(StatefulRedisClusterPubSubConnection.class); + + pubSubCommands = mock(RedisClusterPubSubCommands.class); + + when(pubSubConnection.sync()).thenReturn(pubSubCommands); + + final CircuitBreakerConfiguration breakerConfiguration = new CircuitBreakerConfiguration(); + breakerConfiguration.setFailureRateThreshold(100); + breakerConfiguration.setSlidingWindowSize(1); + breakerConfiguration.setSlidingWindowMinimumNumberOfCalls(1); + breakerConfiguration.setWaitDurationInOpenStateInSeconds(Integer.MAX_VALUE); + + final RetryConfiguration retryConfiguration = new RetryConfiguration(); + retryConfiguration.setMaxAttempts(3); + retryConfiguration.setWaitDuration(10); + + final CircuitBreaker circuitBreaker = CircuitBreaker.of("test", breakerConfiguration.toCircuitBreakerConfig()); + final Retry retry = Retry.of("test", retryConfiguration.toRetryConfig()); + + final RetryConfig resubscribeRetryConfiguration = RetryConfig.custom() + .maxAttempts(Integer.MAX_VALUE) + .intervalFunction(IntervalFunction.ofExponentialBackoff(5)) + .build(); + final Retry resubscribeRetry = Retry.of("test-resubscribe", resubscribeRetryConfiguration); + + faultTolerantPubSubConnection = new FaultTolerantPubSubConnection<>("test", pubSubConnection, circuitBreaker, + retry, resubscribeRetry, Schedulers.newSingle("test")); + } + + @Test + void testBreaker() { + when(pubSubCommands.get(anyString())) + .thenReturn("value") + .thenThrow(new RuntimeException("Badness has ensued.")); + + assertEquals("value", + faultTolerantPubSubConnection.withPubSubConnection(connection -> connection.sync().get("key"))); + + assertThrows(RedisException.class, + () -> faultTolerantPubSubConnection.withPubSubConnection(connection -> connection.sync().get("OH NO"))); + + final RedisException redisException = assertThrows(RedisException.class, + () -> faultTolerantPubSubConnection.withPubSubConnection(connection -> connection.sync().get("OH NO"))); + + assertTrue(redisException.getCause() instanceof CallNotPermittedException); + } + + @Test + void testRetry() { + when(pubSubCommands.get(anyString())) + .thenThrow(new RedisCommandTimeoutException()) + .thenThrow(new RedisCommandTimeoutException()) + .thenReturn("value"); + + assertEquals("value", + faultTolerantPubSubConnection.withPubSubConnection(connection -> connection.sync().get("key"))); + + when(pubSubCommands.get(anyString())) + .thenThrow(new RedisCommandTimeoutException()) + .thenThrow(new RedisCommandTimeoutException()) + .thenThrow(new RedisCommandTimeoutException()) + .thenReturn("value"); + + assertThrows(RedisCommandTimeoutException.class, + () -> faultTolerantPubSubConnection.withPubSubConnection(connection -> connection.sync().get("key"))); + } + + @Nested + class ClusterTopologyChangedEventTest { + + private TestPublisher eventPublisher; + + private Runnable resubscribe; + + private AtomicInteger resubscribeCounter; + private CountDownLatch resubscribeFailure; + private CountDownLatch resubscribeSuccess; + + @BeforeEach + @SuppressWarnings("unchecked") + void setup() { + // ignore inherited stubbing + reset(pubSubConnection); + + eventPublisher = TestPublisher.createCold(); + + final ClientResources clientResources = mock(ClientResources.class); + when(pubSubConnection.getResources()) + .thenReturn(clientResources); + final EventBus eventBus = mock(EventBus.class); + when(clientResources.eventBus()) + .thenReturn(eventBus); + + final Flux eventFlux = Flux.from(eventPublisher); + when(eventBus.get()).thenReturn(eventFlux); + + resubscribeCounter = new AtomicInteger(); + + resubscribe = () -> { + try { + resubscribeCounter.incrementAndGet(); + pubSubConnection.sync().nodes((ignored) -> true); + resubscribeSuccess.countDown(); + } catch (final RuntimeException e) { + resubscribeFailure.countDown(); + throw e; + } + }; + + resubscribeSuccess = new CountDownLatch(1); + resubscribeFailure = new CountDownLatch(1); + } + + @SuppressWarnings("unchecked") + @Test + void testSubscribeToClusterTopologyChangedEvents() throws Exception { + + when(pubSubConnection.sync()) + .thenThrow(new RedisException("Cluster unavailable")); + + eventPublisher.next(new ClusterTopologyChangedEvent(Collections.emptyList(), Collections.emptyList())); + + faultTolerantPubSubConnection.subscribeToClusterTopologyChangedEvents(resubscribe); + + assertTrue(resubscribeFailure.await(1, TimeUnit.SECONDS)); + + // simulate cluster recovery - no more exceptions, run the retry + reset(pubSubConnection); + clearInvocations(pubSubCommands); + when(pubSubConnection.sync()) + .thenReturn(pubSubCommands); + + assertTrue(resubscribeSuccess.await(1, TimeUnit.SECONDS)); + + assertTrue(resubscribeCounter.get() >= 2, String.format("resubscribe called %d times", resubscribeCounter.get())); + verify(pubSubCommands).nodes(any()); + } + + @Test + @SuppressWarnings("unchecked") + void testMultipleEventsWithPendingRetries() throws Exception { + // more complicated scenario: multiple events while retries are pending + + // cluster is down + when(pubSubConnection.sync()) + .thenThrow(new RedisException("Cluster unavailable")); + + // publish multiple topology changed events + eventPublisher.next(new ClusterTopologyChangedEvent(Collections.emptyList(), Collections.emptyList())); + eventPublisher.next(new ClusterTopologyChangedEvent(Collections.emptyList(), Collections.emptyList())); + eventPublisher.next(new ClusterTopologyChangedEvent(Collections.emptyList(), Collections.emptyList())); + eventPublisher.next(new ClusterTopologyChangedEvent(Collections.emptyList(), Collections.emptyList())); + + faultTolerantPubSubConnection.subscribeToClusterTopologyChangedEvents(resubscribe); + + assertTrue(resubscribeFailure.await(1, TimeUnit.SECONDS)); + + // simulate cluster recovery - no more exceptions, run the retry + reset(pubSubConnection); + clearInvocations(pubSubCommands); + when(pubSubConnection.sync()) + .thenReturn(pubSubCommands); + + assertTrue(resubscribeSuccess.await(1, TimeUnit.SECONDS)); + + verify(pubSubCommands, atLeastOnce()).nodes(any()); + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/FaultTolerantRedisClusterTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/FaultTolerantRedisClusterTest.java new file mode 100644 index 000000000..415a4b775 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/FaultTolerantRedisClusterTest.java @@ -0,0 +1,144 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.redis; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.github.resilience4j.circuitbreaker.CallNotPermittedException; +import io.lettuce.core.RedisCommandTimeoutException; +import io.lettuce.core.RedisException; +import io.lettuce.core.cluster.RedisClusterClient; +import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import io.lettuce.core.cluster.pubsub.StatefulRedisClusterPubSubConnection; +import io.lettuce.core.event.EventBus; +import io.lettuce.core.resource.ClientResources; +import java.time.Duration; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; +import org.whispersystems.textsecuregcm.configuration.RetryConfiguration; +import reactor.core.publisher.Flux; + +class FaultTolerantRedisClusterTest { + + private RedisAdvancedClusterCommands clusterCommands; + private FaultTolerantRedisCluster faultTolerantCluster; + + @SuppressWarnings("unchecked") + @BeforeEach + public void setUp() { + final RedisClusterClient clusterClient = mock(RedisClusterClient.class); + final StatefulRedisClusterConnection clusterConnection = mock(StatefulRedisClusterConnection.class); + final StatefulRedisClusterPubSubConnection pubSubConnection = mock( + StatefulRedisClusterPubSubConnection.class); + final ClientResources clientResources = mock(ClientResources.class); + final EventBus eventBus = mock(EventBus.class); + + clusterCommands = mock(RedisAdvancedClusterCommands.class); + + when(clusterClient.connect()).thenReturn(clusterConnection); + when(clusterClient.connectPubSub()).thenReturn(pubSubConnection); + when(clusterClient.getResources()).thenReturn(clientResources); + when(clusterConnection.sync()).thenReturn(clusterCommands); + when(clientResources.eventBus()).thenReturn(eventBus); + when(eventBus.get()).thenReturn(mock(Flux.class)); + + final CircuitBreakerConfiguration breakerConfiguration = new CircuitBreakerConfiguration(); + breakerConfiguration.setFailureRateThreshold(100); + breakerConfiguration.setSlidingWindowSize(1); + breakerConfiguration.setSlidingWindowMinimumNumberOfCalls(1); + breakerConfiguration.setWaitDurationInOpenStateInSeconds(Integer.MAX_VALUE); + + final RetryConfiguration retryConfiguration = new RetryConfiguration(); + retryConfiguration.setMaxAttempts(3); + retryConfiguration.setWaitDuration(0); + + faultTolerantCluster = new FaultTolerantRedisCluster("test", clusterClient, Duration.ofSeconds(2), + breakerConfiguration, retryConfiguration); + } + + @Test + void testBreaker() { + when(clusterCommands.get(anyString())) + .thenReturn("value") + .thenThrow(new RuntimeException("Badness has ensued.")); + + assertEquals("value", faultTolerantCluster.withCluster(connection -> connection.sync().get("key"))); + + assertThrows(RedisException.class, + () -> faultTolerantCluster.withCluster(connection -> connection.sync().get("OH NO"))); + + final RedisException redisException = assertThrows(RedisException.class, + () -> faultTolerantCluster.withCluster(connection -> connection.sync().get("OH NO"))); + + assertTrue(redisException.getCause() instanceof CallNotPermittedException); + } + + @Test + void testRetry() { + when(clusterCommands.get(anyString())) + .thenThrow(new RedisCommandTimeoutException()) + .thenThrow(new RedisCommandTimeoutException()) + .thenReturn("value"); + + assertEquals("value", faultTolerantCluster.withCluster(connection -> connection.sync().get("key"))); + + when(clusterCommands.get(anyString())) + .thenThrow(new RedisCommandTimeoutException()) + .thenThrow(new RedisCommandTimeoutException()) + .thenThrow(new RedisCommandTimeoutException()) + .thenReturn("value"); + + assertThrows(RedisCommandTimeoutException.class, + () -> faultTolerantCluster.withCluster(connection -> connection.sync().get("key"))); + + } + + @Nested + class WithRealCluster { + + private static final Duration TIMEOUT = Duration.ofMillis(50); + + private static final RetryConfiguration retryConfiguration = new RetryConfiguration(); + + static { + retryConfiguration.setMaxAttempts(1); + retryConfiguration.setWaitDuration(50); + } + + @RegisterExtension + static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder() + .retryConfiguration(retryConfiguration) + .timeout(TIMEOUT) + .build(); + + @Test + void testTimeout() { + final FaultTolerantRedisCluster cluster = REDIS_CLUSTER_EXTENSION.getRedisCluster(); + + assertTimeoutPreemptively(Duration.ofSeconds(1), () -> { + final ExecutionException asyncException = assertThrows(ExecutionException.class, + () -> cluster.withCluster(connection -> connection.async().blpop(TIMEOUT.toMillis() * 2, "key")).get()); + assertTrue(asyncException.getCause() instanceof RedisCommandTimeoutException); + + assertThrows(RedisCommandTimeoutException.class, + () -> cluster.withCluster(connection -> connection.sync().blpop(TIMEOUT.toMillis() * 2, "key"))); + }); + + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/RedisClusterExtension.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/RedisClusterExtension.java new file mode 100644 index 000000000..4d1ca048d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/RedisClusterExtension.java @@ -0,0 +1,240 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.redis; + +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisException; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.cluster.RedisClusterClient; +import io.lettuce.core.cluster.SlotHash; +import java.io.File; +import java.io.IOException; +import java.net.ServerSocket; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; +import org.whispersystems.textsecuregcm.configuration.RetryConfiguration; +import org.whispersystems.textsecuregcm.util.RedisClusterUtil; +import redis.embedded.RedisServer; +import redis.embedded.exceptions.EmbeddedRedisException; + +public class RedisClusterExtension implements BeforeAllCallback, BeforeEachCallback, AfterAllCallback, + AfterEachCallback { + + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(2); + private static final int NODE_COUNT = 2; + + private static final RedisServer[] CLUSTER_NODES = new RedisServer[NODE_COUNT]; + + private final Duration timeout; + private final RetryConfiguration retryConfiguration; + private FaultTolerantRedisCluster redisCluster; + + public RedisClusterExtension(final Duration timeout, final RetryConfiguration retryConfiguration) { + this.timeout = timeout; + this.retryConfiguration = retryConfiguration; + } + + + public static RedisClusterExtensionBuilder builder() { + return new RedisClusterExtensionBuilder(); + } + + @Override + public void afterAll(final ExtensionContext context) throws Exception { + for (final RedisServer node : CLUSTER_NODES) { + node.stop(); + } + } + + @Override + public void afterEach(final ExtensionContext context) throws Exception { + redisCluster.shutdown(); + } + + @Override + public void beforeAll(final ExtensionContext context) throws Exception { + assumeFalse(System.getProperty("os.name").equalsIgnoreCase("windows")); + + for (int i = 0; i < NODE_COUNT; i++) { + // We're occasionally seeing redis server startup failing due to the bind address being already in use. + // To mitigate that, we're going to just retry a couple of times before failing the test. + CLUSTER_NODES[i] = startWithRetries(3); + } + + assembleCluster(CLUSTER_NODES); + } + + @Override + public void beforeEach(final ExtensionContext context) throws Exception { + final List urls = Arrays.stream(CLUSTER_NODES) + .map(node -> String.format("redis://127.0.0.1:%d", node.ports().get(0))) + .toList(); + + redisCluster = new FaultTolerantRedisCluster("test-cluster", + RedisClusterClient.create(urls.stream().map(RedisURI::create).collect(Collectors.toList())), + timeout, + new CircuitBreakerConfiguration(), + retryConfiguration); + + redisCluster.useCluster(connection -> { + boolean setAll = false; + + final String[] keys = new String[NODE_COUNT]; + + for (int i = 0; i < keys.length; i++) { + keys[i] = RedisClusterUtil.getMinimalHashTag(i * SlotHash.SLOT_COUNT / keys.length); + } + + while (!setAll) { + try { + for (final String key : keys) { + connection.sync().set(key, "warmup"); + } + + setAll = true; + } catch (final RedisException ignored) { + // Cluster isn't ready; wait and retry. + try { + Thread.sleep(500); + } catch (final InterruptedException ignored2) { + } + } + } + }); + + redisCluster.useCluster(connection -> connection.sync().flushall()); + } + + public FaultTolerantRedisCluster getRedisCluster() { + return redisCluster; + } + + private static RedisServer buildClusterNode(final int port) throws IOException { + final File clusterConfigFile = File.createTempFile("redis", ".conf"); + clusterConfigFile.deleteOnExit(); + + return RedisServer.builder() + .setting("cluster-enabled yes") + .setting("cluster-config-file " + clusterConfigFile.getAbsolutePath()) + .setting("cluster-node-timeout 5000") + .setting("appendonly no") + .setting("save \"\"") + .setting("dir " + System.getProperty("java.io.tmpdir")) + .port(port) + .build(); + } + + private static void assembleCluster(final RedisServer... nodes) throws InterruptedException { + try (final RedisClient meetClient = RedisClient.create(RedisURI.create("127.0.0.1", nodes[0].ports().get(0)))) { + final StatefulRedisConnection connection = meetClient.connect(); + final RedisCommands commands = connection.sync(); + + for (int i = 1; i < nodes.length; i++) { + commands.clusterMeet("127.0.0.1", nodes[i].ports().get(0)); + } + } + + final int slotsPerNode = SlotHash.SLOT_COUNT / nodes.length; + + for (int i = 0; i < nodes.length; i++) { + final int startInclusive = i * slotsPerNode; + final int endExclusive = i == nodes.length - 1 ? SlotHash.SLOT_COUNT : (i + 1) * slotsPerNode; + + try (final RedisClient assignSlotClient = RedisClient.create(RedisURI.create("127.0.0.1", nodes[i].ports().get(0))); + final StatefulRedisConnection assignSlotConnection = assignSlotClient.connect()) { + final int[] slots = new int[endExclusive - startInclusive]; + + for (int s = startInclusive; s < endExclusive; s++) { + slots[s - startInclusive] = s; + } + + assignSlotConnection.sync().clusterAddSlots(slots); + } + } + + try (final RedisClient waitClient = RedisClient.create(RedisURI.create("127.0.0.1", nodes[0].ports().get(0))); + final StatefulRedisConnection connection = waitClient.connect()) { + // CLUSTER INFO gives us a big blob of key-value pairs, but the one we're interested in is `cluster_state`. + // According to https://redis.io/commands/cluster-info, `cluster_state:ok` means that the node is ready to + // receive queries, all slots are assigned, and a majority of master nodes are reachable. + + final int sleepMillis = 500; + int tries = 0; + while (!connection.sync().clusterInfo().contains("cluster_state:ok")) { + Thread.sleep(sleepMillis); + tries++; + + if (tries == 20) { + throw new RuntimeException( + String.format("Timeout: Redis not ready after waiting %d milliseconds", tries * sleepMillis)); + } + } + } + } + + public static int getNextRedisClusterPort() throws IOException { + final int maxIterations = 11_000; + for (int i = 0; i < maxIterations; i++) { + try (final ServerSocket socket = new ServerSocket(0)) { + socket.setReuseAddress(false); + final int port = socket.getLocalPort(); + if (port < 55535) { + return port; + } + } + } + throw new IOException("Couldn't find an unused open port below 55,535 in " + maxIterations + " tries"); + } + + private static RedisServer startWithRetries(final int attemptsLeft) throws Exception { + try { + final RedisServer redisServer = buildClusterNode(getNextRedisClusterPort()); + redisServer.start(); + return redisServer; + } catch (final IOException | EmbeddedRedisException e) { + if (attemptsLeft == 0) { + throw e; + } + Thread.sleep(500); + return startWithRetries(attemptsLeft - 1); + } + } + + public static class RedisClusterExtensionBuilder { + + private Duration timeout = DEFAULT_TIMEOUT; + private RetryConfiguration retryConfiguration = new RetryConfiguration(); + + private RedisClusterExtensionBuilder() { + } + + RedisClusterExtensionBuilder timeout(Duration timeout) { + this.timeout = timeout; + return this; + } + + RedisClusterExtensionBuilder retryConfiguration(RetryConfiguration retryConfiguration) { + this.retryConfiguration = retryConfiguration; + return this; + } + + public RedisClusterExtension build() { + return new RedisClusterExtension(timeout, retryConfiguration); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/RedisSingletonExtension.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/RedisSingletonExtension.java new file mode 100644 index 000000000..cf1b28eb2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/redis/RedisSingletonExtension.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.redis; + +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +import io.lettuce.core.RedisClient; +import io.lettuce.core.api.StatefulRedisConnection; +import java.io.IOException; +import java.net.ServerSocket; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import redis.embedded.RedisServer; + +public class RedisSingletonExtension implements BeforeAllCallback, BeforeEachCallback, AfterAllCallback, AfterEachCallback { + + private static RedisServer redisServer; + private RedisClient redisClient; + + public static class RedisSingletonExtensionBuilder { + + private RedisSingletonExtensionBuilder() { + } + + public RedisSingletonExtension build() { + return new RedisSingletonExtension(); + } + } + + public static RedisSingletonExtensionBuilder builder() { + return new RedisSingletonExtensionBuilder(); + } + + @Override + public void beforeAll(final ExtensionContext context) throws Exception { + assumeFalse(System.getProperty("os.name").equalsIgnoreCase("windows")); + + redisServer = RedisServer.builder() + .setting("appendonly no") + .setting("save \"\"") + .setting("dir " + System.getProperty("java.io.tmpdir")) + .port(getAvailablePort()) + .build(); + + redisServer.start(); + } + + @Override + public void beforeEach(final ExtensionContext context) { + redisClient = RedisClient.create(String.format("redis://127.0.0.1:%d", redisServer.ports().get(0))); + + try (final StatefulRedisConnection connection = redisClient.connect()) { + connection.sync().flushall(); + } + } + + @Override + public void afterEach(final ExtensionContext context) { + redisClient.shutdown(); + } + + @Override + public void afterAll(final ExtensionContext context) { + if (redisServer != null) { + redisServer.stop(); + } + } + + public RedisClient getRedisClient() { + return redisClient; + } + + private static int getAvailablePort() throws IOException { + try (ServerSocket socket = new ServerSocket(0)) { + socket.setReuseAddress(false); + return socket.getLocalPort(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/s3/PolicySignerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/s3/PolicySignerTest.java new file mode 100644 index 000000000..52df80c4c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/s3/PolicySignerTest.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.s3; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import org.junit.jupiter.api.Test; + +class PolicySignerTest { + + @Test + void testSignature() { + Instant time = Instant.parse("2015-12-29T00:00:00Z"); + ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(time, ZoneOffset.UTC); + String encodedPolicy = "eyAiZXhwaXJhdGlvbiI6ICIyMDE1LTEyLTMwVDEyOjAwOjAwLjAwMFoiLA0KICAiY29uZGl0aW9ucyI6IFsNCiAgICB7ImJ1Y2tldCI6ICJzaWd2NGV4YW1wbGVidWNrZXQifSwNCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci91c2VyMS8iXSwNCiAgICB7ImFjbCI6ICJwdWJsaWMtcmVhZCJ9LA0KICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL3NpZ3Y0ZXhhbXBsZWJ1Y2tldC5zMy5hbWF6b25hd3MuY29tL3N1Y2Nlc3NmdWxfdXBsb2FkLmh0bWwifSwNCiAgICBbInN0YXJ0cy13aXRoIiwgIiRDb250ZW50LVR5cGUiLCAiaW1hZ2UvIl0sDQogICAgeyJ4LWFtei1tZXRhLXV1aWQiOiAiMTQzNjUxMjM2NTEyNzQifSwNCiAgICB7IngtYW16LXNlcnZlci1zaWRlLWVuY3J5cHRpb24iOiAiQUVTMjU2In0sDQogICAgWyJzdGFydHMtd2l0aCIsICIkeC1hbXotbWV0YS10YWciLCAiIl0sDQoNCiAgICB7IngtYW16LWNyZWRlbnRpYWwiOiAiQUtJQUlPU0ZPRE5ON0VYQU1QTEUvMjAxNTEyMjkvdXMtZWFzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LA0KICAgIHsieC1hbXotYWxnb3JpdGhtIjogIkFXUzQtSE1BQy1TSEEyNTYifSwNCiAgICB7IngtYW16LWRhdGUiOiAiMjAxNTEyMjlUMDAwMDAwWiIgfQ0KICBdDQp9"; + PolicySigner policySigner = new PolicySigner("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "us-east-1"); + + assertEquals(policySigner.getSignature(zonedDateTime, encodedPolicy), "8afdbf4008c03f22c2cd3cdb72e4afbb1f6a588f3255ac628749a66d7f09699e"); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClientTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClientTest.java new file mode 100644 index 000000000..44e9ddf58 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClientTest.java @@ -0,0 +1,156 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.securestorage; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.delete; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; +import org.whispersystems.textsecuregcm.configuration.RetryConfiguration; +import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration; + +class SecureStorageClientTest { + + private UUID accountUuid; + private ExternalServiceCredentialsGenerator credentialsGenerator; + private ExecutorService httpExecutor; + private ScheduledExecutorService retryExecutor; + + private SecureStorageClient secureStorageClient; + + @RegisterExtension + private final WireMockExtension wireMock = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort().dynamicHttpsPort()) + .build(); + + @BeforeEach + void setUp() throws CertificateException { + accountUuid = UUID.randomUUID(); + credentialsGenerator = mock(ExternalServiceCredentialsGenerator.class); + httpExecutor = Executors.newSingleThreadExecutor(); + retryExecutor = Executors.newSingleThreadScheduledExecutor(); + + final SecureStorageServiceConfiguration config = new SecureStorageServiceConfiguration( + randomSecretBytes(32), + "http://localhost:" + wireMock.getPort(), + List.of(""" + -----BEGIN CERTIFICATE----- + MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEL + MAkGA1UECAwCVVMxHjAcBgNVBAoMFVNpZ25hbCBNZXNzZW5nZXIsIExMQzETMBEG + A1UEAwwKc2lnbmFsLm9yZzAeFw0yMDEyMjMyMjQ3NTlaFw0zMDEyMjEyMjQ3NTla + ME8xCzAJBgNVBAYTAnVzMQswCQYDVQQIDAJVUzEeMBwGA1UECgwVU2lnbmFsIE1l + c3NlbmdlciwgTExDMRMwEQYDVQQDDApzaWduYWwub3JnMIGfMA0GCSqGSIb3DQEB + AQUAA4GNADCBiQKBgQCfSLcZNHYqbxSsgWp4JvbPRHjQTrlsrKrgD2q7f/OY6O3Y + /X0QNcNSOJpliN8rmzwslfsrXHO3q1diGRw4xHogUJZ/7NQrHiP/zhN0VTDh49pD + ZpjXVyUbayLS/6qM5arKxBspzEFBb5v8cF6bPr76SO/rpGXiI0j6yJKX6fRiKwID + AQABo1AwTjAdBgNVHQ4EFgQU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwHwYDVR0jBBgw + FoAU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B + AQ0FAAOBgQB+5d5+NtzLILfrc9QmJdIO1YeDP64JmFwTER0kEUouRsb9UwknVWZa + y7MTM4NoBV1k0zb5LAk89SIDPr/maW5AsLtEomzjnEiomjoMBUdNe3YCgQReoLnr + R/QaUNbrCjTGYfBsjGbIzmkWPUyTec2ZdRyJ8JiVl386+6CZkxnndQ== + -----END CERTIFICATE----- + """, """ + -----BEGIN CERTIFICATE----- + MIIEpDCCAowCCQC43PUTWSADVjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls + b2NhbGhvc3QwHhcNMjIxMDE3MjA0NTM0WhcNMjMxMDE3MjA0NTM0WjAUMRIwEAYD + VQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDV + x1cdEd2ffQTlTXWRiCHGcrlYf4RJnctt9sw/BuHWTLXBu5LhyJSGn5LRszO/NCXK + Z/cmGR7pLj366RtiwL+Qo3nhvDCK7T9xZeNIusM6XMcMK9D/DGCYPqtjQz8NXd9V + ajBBe6nwTDTa+oqX8Mt89foWNkg5Il/lY62u9Dr18LRZ2W9zzYi3Q9/K0CbIX6pM + yVlPIO5rITOR2IsbeyqsO9jufgX5lP4ZKLLBAP1b7usjC4YdvWacjQg/rK5aay1x + jC2HCDgo/4N30QVXzSA9nFfSe6AE/xkStK4819JqOkY5JsJCbef1P3hOOdSLEjbp + xq3MjOs6G6dOgteaAGs10vx7dHxDWETTIiD7BIZ9zRYgOF5bkCaIUO+JfySE1MHD + KBAFLoRuvmRev5Ln5R0MCHpUMSmMNgJqz+RWZV3g/gpYbuWiHgJOwL1393eK50Bg + W7SXQ8EjJj2yXZSH+1gPzN0DRoJZiaBoTPnCL2qUgvwFpW1PJsM5FDyUJFUoK5kK + HLBBSKAPt6ZlSrUe2nBgJv7EF1GK+fTU08LXgW33OpLceGPa0zTShkukQUMtUtZ8 + GqhO12ohMzEupIu5Xurthq4VVUrzHUdj1ZZRMhAbfLU36sd03MMyL/xBqTN6dzCa + GDGIPGpYjAllZ5xMRt2kZdv+Kr6oo3u2nLUIsqI7KQIDAQABMA0GCSqGSIb3DQEB + CwUAA4ICAQCB5s43YF35ssf5YONW5iAaifGpi1o0866xfeOybtohFGvQ7V2W34i9 + TYBCt8+0hgatMcvZ08f0vqig1i7nrvYcE1hnhL7JNkU8qm0s9ytHZt6j62nB0kd/ + uqE2hOEQalTf/2TGPV0CCgiqLyd8lEUQvQeA38wktwUeZpVnErlzHeMR2CvV3K8R + u4vV6SnBcf+TAt56RKYZkPyvZj5llQPo14Glyoo8qZES7Ky1SHmM0GL+baPRBjRW + 3KgSt98Wyu4yr9qu21JpnbAnLhBfzfSKjSeCRgFElUE1GIaFGRZ7ypA74dUKeLnb + /VUWrszmUhGaEjV9dpI6x6B/kSpQMtIQqBaKRY2ALUeEujS/rURi4iMDwSU+GkSH + cyEvZKS97OA/dWeXfLXdo4beDBRG93bI4rQnDg5+VdlBOkQSLueb8x6/VThMoC5d + vZiotFQHseljQAdTkNa6tBu6c4XDYPCKB3CfkMYOlCfTS7Acn5G6dxTPKBtLGBnL + nQfYyzuwYkN09+2PVzt6auBHr3To7uoclkxX+hxyvPIwIZ0N6b4tQR1FCAkvg29Q + WIOjZOKGW690ESKCKOnFjUHVO0HpuWnT81URTuY62FXsYdVc2wE4v0E04mEbqQ0P + lY6ZKNA81Lm3YADYtObmK1IUrOPo9BeIaPy0UM08SmN880Vunqa91Q== + -----END CERTIFICATE----- + """), + new CircuitBreakerConfiguration(), + new RetryConfiguration()); + + secureStorageClient = new SecureStorageClient(credentialsGenerator, httpExecutor, retryExecutor, config); + } + + @AfterEach + void tearDown() throws InterruptedException { + + httpExecutor.shutdown(); + httpExecutor.awaitTermination(1, TimeUnit.SECONDS); + retryExecutor.shutdown(); + retryExecutor.awaitTermination(1, TimeUnit.SECONDS); + } + + @Test + void deleteStoredData() { + + final String username = RandomStringUtils.randomAlphabetic(16); + final String password = RandomStringUtils.randomAlphanumeric(32); + + when(credentialsGenerator.generateForUuid(accountUuid)).thenReturn( + new ExternalServiceCredentials(username, password)); + + wireMock.stubFor(delete(urlEqualTo(SecureStorageClient.DELETE_PATH)) + .withBasicAuth(username, password) + .willReturn(aResponse().withStatus(202))); + + // We're happy as long as this doesn't throw an exception + secureStorageClient.deleteStoredData(accountUuid).join(); + } + + @Test + void deleteStoredDataFailure() { + + final String username = RandomStringUtils.randomAlphabetic(16); + final String password = RandomStringUtils.randomAlphanumeric(32); + + when(credentialsGenerator.generateForUuid(accountUuid)).thenReturn( + new ExternalServiceCredentials(username, password)); + + wireMock.stubFor(delete(urlEqualTo(SecureStorageClient.DELETE_PATH)) + .withBasicAuth(username, password) + .willReturn(aResponse().withStatus(400))); + + final CompletionException completionException = assertThrows(CompletionException.class, + () -> secureStorageClient.deleteStoredData(accountUuid).join()); + assertTrue(completionException.getCause() instanceof SecureStorageException); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2ClientTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2ClientTest.java new file mode 100644 index 000000000..9e21628d8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2ClientTest.java @@ -0,0 +1,153 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.securevaluerecovery; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.delete; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration; + +class SecureValueRecovery2ClientTest { + + private UUID accountUuid; + private ExternalServiceCredentialsGenerator credentialsGenerator; + private ExecutorService httpExecutor; + private ScheduledExecutorService retryExecutor; + + private SecureValueRecovery2Client secureValueRecovery2Client; + + @RegisterExtension + private final WireMockExtension wireMock = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort().dynamicHttpsPort()) + .build(); + + @BeforeEach + void setUp() throws CertificateException { + accountUuid = UUID.randomUUID(); + credentialsGenerator = mock(ExternalServiceCredentialsGenerator.class); + httpExecutor = Executors.newSingleThreadExecutor(); + retryExecutor = Executors.newSingleThreadScheduledExecutor(); + + final SecureValueRecovery2Configuration config = new SecureValueRecovery2Configuration( + "http://localhost:" + wireMock.getPort(), + randomSecretBytes(32), + randomSecretBytes(32), + // This is a randomly-generated, throwaway certificate that's not actually connected to anything + List.of(""" + -----BEGIN CERTIFICATE----- + MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEL + MAkGA1UECAwCVVMxHjAcBgNVBAoMFVNpZ25hbCBNZXNzZW5nZXIsIExMQzETMBEG + A1UEAwwKc2lnbmFsLm9yZzAeFw0yMDEyMjMyMjQ3NTlaFw0zMDEyMjEyMjQ3NTla + ME8xCzAJBgNVBAYTAnVzMQswCQYDVQQIDAJVUzEeMBwGA1UECgwVU2lnbmFsIE1l + c3NlbmdlciwgTExDMRMwEQYDVQQDDApzaWduYWwub3JnMIGfMA0GCSqGSIb3DQEB + AQUAA4GNADCBiQKBgQCfSLcZNHYqbxSsgWp4JvbPRHjQTrlsrKrgD2q7f/OY6O3Y + /X0QNcNSOJpliN8rmzwslfsrXHO3q1diGRw4xHogUJZ/7NQrHiP/zhN0VTDh49pD + ZpjXVyUbayLS/6qM5arKxBspzEFBb5v8cF6bPr76SO/rpGXiI0j6yJKX6fRiKwID + AQABo1AwTjAdBgNVHQ4EFgQU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwHwYDVR0jBBgw + FoAU6Jrs/Fmj0z4dA3wvdq/WqA4P49IwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B + AQ0FAAOBgQB+5d5+NtzLILfrc9QmJdIO1YeDP64JmFwTER0kEUouRsb9UwknVWZa + y7MTM4NoBV1k0zb5LAk89SIDPr/maW5AsLtEomzjnEiomjoMBUdNe3YCgQReoLnr + R/QaUNbrCjTGYfBsjGbIzmkWPUyTec2ZdRyJ8JiVl386+6CZkxnndQ== + -----END CERTIFICATE----- + """, """ + -----BEGIN CERTIFICATE----- + MIIEpDCCAowCCQC43PUTWSADVjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls + b2NhbGhvc3QwHhcNMjIxMDE3MjA0NTM0WhcNMjMxMDE3MjA0NTM0WjAUMRIwEAYD + VQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDV + x1cdEd2ffQTlTXWRiCHGcrlYf4RJnctt9sw/BuHWTLXBu5LhyJSGn5LRszO/NCXK + Z/cmGR7pLj366RtiwL+Qo3nhvDCK7T9xZeNIusM6XMcMK9D/DGCYPqtjQz8NXd9V + ajBBe6nwTDTa+oqX8Mt89foWNkg5Il/lY62u9Dr18LRZ2W9zzYi3Q9/K0CbIX6pM + yVlPIO5rITOR2IsbeyqsO9jufgX5lP4ZKLLBAP1b7usjC4YdvWacjQg/rK5aay1x + jC2HCDgo/4N30QVXzSA9nFfSe6AE/xkStK4819JqOkY5JsJCbef1P3hOOdSLEjbp + xq3MjOs6G6dOgteaAGs10vx7dHxDWETTIiD7BIZ9zRYgOF5bkCaIUO+JfySE1MHD + KBAFLoRuvmRev5Ln5R0MCHpUMSmMNgJqz+RWZV3g/gpYbuWiHgJOwL1393eK50Bg + W7SXQ8EjJj2yXZSH+1gPzN0DRoJZiaBoTPnCL2qUgvwFpW1PJsM5FDyUJFUoK5kK + HLBBSKAPt6ZlSrUe2nBgJv7EF1GK+fTU08LXgW33OpLceGPa0zTShkukQUMtUtZ8 + GqhO12ohMzEupIu5Xurthq4VVUrzHUdj1ZZRMhAbfLU36sd03MMyL/xBqTN6dzCa + GDGIPGpYjAllZ5xMRt2kZdv+Kr6oo3u2nLUIsqI7KQIDAQABMA0GCSqGSIb3DQEB + CwUAA4ICAQCB5s43YF35ssf5YONW5iAaifGpi1o0866xfeOybtohFGvQ7V2W34i9 + TYBCt8+0hgatMcvZ08f0vqig1i7nrvYcE1hnhL7JNkU8qm0s9ytHZt6j62nB0kd/ + uqE2hOEQalTf/2TGPV0CCgiqLyd8lEUQvQeA38wktwUeZpVnErlzHeMR2CvV3K8R + u4vV6SnBcf+TAt56RKYZkPyvZj5llQPo14Glyoo8qZES7Ky1SHmM0GL+baPRBjRW + 3KgSt98Wyu4yr9qu21JpnbAnLhBfzfSKjSeCRgFElUE1GIaFGRZ7ypA74dUKeLnb + /VUWrszmUhGaEjV9dpI6x6B/kSpQMtIQqBaKRY2ALUeEujS/rURi4iMDwSU+GkSH + cyEvZKS97OA/dWeXfLXdo4beDBRG93bI4rQnDg5+VdlBOkQSLueb8x6/VThMoC5d + vZiotFQHseljQAdTkNa6tBu6c4XDYPCKB3CfkMYOlCfTS7Acn5G6dxTPKBtLGBnL + nQfYyzuwYkN09+2PVzt6auBHr3To7uoclkxX+hxyvPIwIZ0N6b4tQR1FCAkvg29Q + WIOjZOKGW690ESKCKOnFjUHVO0HpuWnT81URTuY62FXsYdVc2wE4v0E04mEbqQ0P + lY6ZKNA81Lm3YADYtObmK1IUrOPo9BeIaPy0UM08SmN880Vunqa91Q== + -----END CERTIFICATE----- + """), + null, null); + + secureValueRecovery2Client = new SecureValueRecovery2Client(credentialsGenerator, httpExecutor, retryExecutor, + config); + } + + @AfterEach + void tearDown() throws InterruptedException { + httpExecutor.shutdown(); + httpExecutor.awaitTermination(1, TimeUnit.SECONDS); + retryExecutor.shutdown(); + retryExecutor.awaitTermination(1, TimeUnit.SECONDS); + } + + @Test + void deleteStoredData() { + final String username = RandomStringUtils.randomAlphabetic(16); + final String password = RandomStringUtils.randomAlphanumeric(32); + + when(credentialsGenerator.generateForUuid(accountUuid)).thenReturn( + new ExternalServiceCredentials(username, password)); + + wireMock.stubFor(delete(urlEqualTo(SecureValueRecovery2Client.DELETE_PATH)) + .withBasicAuth(username, password) + .willReturn(aResponse().withStatus(202))); + + assertDoesNotThrow(() -> secureValueRecovery2Client.deleteBackups(accountUuid).join()); + } + + @Test + void deleteStoredDataFailure() { + final String username = RandomStringUtils.randomAlphabetic(16); + final String password = RandomStringUtils.randomAlphanumeric(32); + + when(credentialsGenerator.generateForUuid(accountUuid)).thenReturn( + new ExternalServiceCredentials(username, password)); + + wireMock.stubFor(delete(urlEqualTo(SecureValueRecovery2Client.DELETE_PATH)) + .withBasicAuth(username, password) + .willReturn(aResponse().withStatus(400))); + + final CompletionException completionException = assertThrows(CompletionException.class, + () -> secureValueRecovery2Client.deleteBackups(accountUuid).join()); + assertTrue(completionException.getCause() instanceof SecureValueRecoveryException); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountChangeValidatorTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountChangeValidatorTest.java new file mode 100644 index 000000000..484b05ff6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountChangeValidatorTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Base64; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; +import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class AccountChangeValidatorTest { + + private static final String ORIGINAL_NUMBER = "+18005551234"; + private static final String CHANGED_NUMBER = "+18005559876"; + + private static final UUID ORIGINAL_PNI = UUID.randomUUID(); + private static final UUID CHANGED_PNI = UUID.randomUUID(); + + private static final String BASE_64_URL_ORIGINAL_USERNAME = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE"; + private static final String BASE_64_URL_CHANGED_USERNAME = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc"; + private static final byte[] ORIGINAL_USERNAME_HASH = Base64.getUrlDecoder().decode(BASE_64_URL_ORIGINAL_USERNAME); + private static final byte[] CHANGED_USERNAME_HASH = Base64.getUrlDecoder().decode(BASE_64_URL_CHANGED_USERNAME); + + @ParameterizedTest + @MethodSource + void validateChange(final Account originalAccount, + final Account updatedAccount, + final AccountChangeValidator changeValidator, + final boolean expectChangeAllowed) { + + final Executable applyChange = () -> changeValidator.validateChange(originalAccount, updatedAccount); + + if (expectChangeAllowed) { + assertDoesNotThrow(applyChange); + } else { + assertThrows(AssertionError.class, applyChange); + } + } + + private static Stream validateChange() { + final Account originalAccount = mock(Account.class); + when(originalAccount.getNumber()).thenReturn(ORIGINAL_NUMBER); + when(originalAccount.getPhoneNumberIdentifier()).thenReturn(ORIGINAL_PNI); + when(originalAccount.getUsernameHash()).thenReturn(Optional.of(ORIGINAL_USERNAME_HASH)); + + final Account unchangedAccount = mock(Account.class); + when(unchangedAccount.getNumber()).thenReturn(ORIGINAL_NUMBER); + when(unchangedAccount.getPhoneNumberIdentifier()).thenReturn(ORIGINAL_PNI); + when(unchangedAccount.getUsernameHash()).thenReturn(Optional.of(ORIGINAL_USERNAME_HASH)); + + final Account changedNumberAccount = mock(Account.class); + when(changedNumberAccount.getNumber()).thenReturn(CHANGED_NUMBER); + when(changedNumberAccount.getPhoneNumberIdentifier()).thenReturn(CHANGED_PNI); + when(changedNumberAccount.getUsernameHash()).thenReturn(Optional.of(ORIGINAL_USERNAME_HASH)); + + final Account changedUsernameAccount = mock(Account.class); + when(changedUsernameAccount.getNumber()).thenReturn(ORIGINAL_NUMBER); + when(changedUsernameAccount.getPhoneNumberIdentifier()).thenReturn(ORIGINAL_PNI); + when(changedUsernameAccount.getUsernameHash()).thenReturn(Optional.of(CHANGED_USERNAME_HASH)); + + return Stream.of( + Arguments.of(originalAccount, unchangedAccount, AccountChangeValidator.GENERAL_CHANGE_VALIDATOR, true), + Arguments.of(originalAccount, unchangedAccount, AccountChangeValidator.NUMBER_CHANGE_VALIDATOR, true), + Arguments.of(originalAccount, unchangedAccount, AccountChangeValidator.USERNAME_CHANGE_VALIDATOR, true), + + Arguments.of(originalAccount, changedNumberAccount, AccountChangeValidator.GENERAL_CHANGE_VALIDATOR, false), + Arguments.of(originalAccount, changedNumberAccount, AccountChangeValidator.NUMBER_CHANGE_VALIDATOR, true), + Arguments.of(originalAccount, changedNumberAccount, AccountChangeValidator.USERNAME_CHANGE_VALIDATOR, false), + + Arguments.of(originalAccount, changedUsernameAccount, AccountChangeValidator.GENERAL_CHANGE_VALIDATOR, false), + Arguments.of(originalAccount, changedUsernameAccount, AccountChangeValidator.NUMBER_CHANGE_VALIDATOR, false), + Arguments.of(originalAccount, changedUsernameAccount, AccountChangeValidator.USERNAME_CHANGE_VALIDATOR, true) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountCleanerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountCleanerTest.java new file mode 100644 index 000000000..b79bf4f0a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountCleanerTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.storage.AccountsManager.DeletionReason; + +class AccountCleanerTest { + + private final AccountsManager accountsManager = mock(AccountsManager.class); + + private final Account deletedDisabledAccount = mock(Account.class); + private final Account undeletedDisabledAccount = mock(Account.class); + private final Account undeletedEnabledAccount = mock(Account.class); + + private final Device deletedDisabledDevice = mock(Device.class ); + private final Device undeletedDisabledDevice = mock(Device.class ); + private final Device undeletedEnabledDevice = mock(Device.class ); + + @BeforeEach + void setup() { + when(accountsManager.delete(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + when(deletedDisabledDevice.isEnabled()).thenReturn(false); + when(deletedDisabledDevice.getGcmId()).thenReturn(null); + when(deletedDisabledDevice.getApnId()).thenReturn(null); + when(deletedDisabledDevice.getVoipApnId()).thenReturn(null); + when(deletedDisabledDevice.getFetchesMessages()).thenReturn(false); + when(deletedDisabledAccount.isEnabled()).thenReturn(false); + when(deletedDisabledAccount.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1000)); + when(deletedDisabledAccount.getMasterDevice()).thenReturn(Optional.of(deletedDisabledDevice)); + when(deletedDisabledAccount.getNumber()).thenReturn("+14151231234"); + when(deletedDisabledAccount.getUuid()).thenReturn(UUID.randomUUID()); + + when(undeletedDisabledDevice.isEnabled()).thenReturn(false); + when(undeletedDisabledDevice.getGcmId()).thenReturn("foo"); + when(undeletedDisabledAccount.isEnabled()).thenReturn(false); + when(undeletedDisabledAccount.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(181)); + when(undeletedDisabledAccount.getMasterDevice()).thenReturn(Optional.of(undeletedDisabledDevice)); + when(undeletedDisabledAccount.getNumber()).thenReturn("+14152222222"); + when(undeletedDisabledAccount.getUuid()).thenReturn(UUID.randomUUID()); + + when(undeletedEnabledDevice.isEnabled()).thenReturn(true); + when(undeletedEnabledDevice.getApnId()).thenReturn("bar"); + when(undeletedEnabledAccount.isEnabled()).thenReturn(true); + when(undeletedEnabledAccount.getMasterDevice()).thenReturn(Optional.of(undeletedEnabledDevice)); + when(undeletedEnabledAccount.getNumber()).thenReturn("+14153333333"); + when(undeletedEnabledAccount.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(179)); + when(undeletedEnabledAccount.getUuid()).thenReturn(UUID.randomUUID()); + } + + @Test + void testAccounts() { + AccountCleaner accountCleaner = new AccountCleaner(accountsManager); + accountCleaner.onCrawlStart(); + accountCleaner.timeAndProcessCrawlChunk(Optional.empty(), + Arrays.asList(deletedDisabledAccount, undeletedDisabledAccount, undeletedEnabledAccount)); + accountCleaner.onCrawlEnd(); + + verify(accountsManager).delete(deletedDisabledAccount, DeletionReason.EXPIRED); + verify(accountsManager).delete(undeletedDisabledAccount, DeletionReason.EXPIRED); + verify(accountsManager, never()).delete(eq(undeletedEnabledAccount), any()); + + verifyNoMoreInteractions(accountsManager); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawlerIntegrationTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawlerIntegrationTest.java new file mode 100644 index 000000000..be7073460 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawlerIntegrationTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; + +class AccountDatabaseCrawlerIntegrationTest { + + @RegisterExtension + static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + private static final UUID FIRST_UUID = UUID.fromString("82339e80-81cd-48e2-9ed2-ccd5dd262ad9"); + private static final UUID SECOND_UUID = UUID.fromString("cc705c84-33cf-456b-8239-a6a34e2f561a"); + + private Account firstAccount; + private Account secondAccount; + + private AccountsManager accountsManager; + private AccountDatabaseCrawlerListener listener; + + private AccountDatabaseCrawler accountDatabaseCrawler; + + private static final int CHUNK_SIZE = 1; + + @BeforeEach + void setUp() throws Exception { + + firstAccount = mock(Account.class); + secondAccount = mock(Account.class); + + accountsManager = mock(AccountsManager.class); + listener = mock(AccountDatabaseCrawlerListener.class); + + when(firstAccount.getUuid()).thenReturn(FIRST_UUID); + when(secondAccount.getUuid()).thenReturn(SECOND_UUID); + + when(accountsManager.getAllFromDynamo(CHUNK_SIZE)).thenReturn( + new AccountCrawlChunk(List.of(firstAccount), FIRST_UUID)); + when(accountsManager.getAllFromDynamo(any(UUID.class), eq(CHUNK_SIZE))) + .thenReturn(new AccountCrawlChunk(List.of(secondAccount), SECOND_UUID)) + .thenReturn(new AccountCrawlChunk(Collections.emptyList(), null)); + + final AccountDatabaseCrawlerCache crawlerCache = new AccountDatabaseCrawlerCache( + REDIS_CLUSTER_EXTENSION.getRedisCluster(), "test"); + accountDatabaseCrawler = new AccountDatabaseCrawler("test", accountsManager, crawlerCache, List.of(listener), + CHUNK_SIZE); + } + + @Test + void testCrawlAllAccounts() throws Exception { + accountDatabaseCrawler.crawlAllAccounts(); + + verify(accountsManager).getAllFromDynamo(CHUNK_SIZE); + verify(accountsManager).getAllFromDynamo(FIRST_UUID, CHUNK_SIZE); + verify(accountsManager).getAllFromDynamo(SECOND_UUID, CHUNK_SIZE); + + verify(listener).onCrawlStart(); + verify(listener).timeAndProcessCrawlChunk(Optional.empty(), List.of(firstAccount)); + verify(listener).timeAndProcessCrawlChunk(Optional.of(FIRST_UUID), List.of(secondAccount)); + verify(listener).onCrawlEnd(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawlerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawlerTest.java new file mode 100644 index 000000000..f17a13b61 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountDatabaseCrawlerTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AccountDatabaseCrawlerTest { + + private static final UUID ACCOUNT1 = UUID.randomUUID(); + private static final UUID ACCOUNT2 = UUID.randomUUID(); + + private static final int CHUNK_SIZE = 1000; + + private final Account account1 = mock(Account.class); + private final Account account2 = mock(Account.class); + + private final AccountsManager accounts = mock(AccountsManager.class); + private final AccountDatabaseCrawlerListener listener = mock(AccountDatabaseCrawlerListener.class); + private final AccountDatabaseCrawlerCache cache = mock(AccountDatabaseCrawlerCache.class); + + private final AccountDatabaseCrawler crawler = + new AccountDatabaseCrawler("test", accounts, cache, List.of(listener), CHUNK_SIZE); + + @BeforeEach + void setup() { + when(account1.getUuid()).thenReturn(ACCOUNT1); + when(account2.getUuid()).thenReturn(ACCOUNT2); + + when(accounts.getAllFromDynamo(anyInt())).thenReturn( + new AccountCrawlChunk(List.of(account1, account2), ACCOUNT2)); + when(accounts.getAllFromDynamo(eq(ACCOUNT1), anyInt())).thenReturn( + new AccountCrawlChunk(List.of(account2), ACCOUNT2)); + when(accounts.getAllFromDynamo(eq(ACCOUNT2), anyInt())).thenReturn( + new AccountCrawlChunk(Collections.emptyList(), null)); + + when(cache.claimActiveWork(any(), anyLong())).thenReturn(true); + } + + @Test + void testCrawlAllAccounts() { + when(cache.getLastUuid()) + .thenReturn(Optional.empty()); + + crawler.crawlAllAccounts(); + + verify(cache, times(1)).claimActiveWork(any(String.class), anyLong()); + verify(cache, times(1)).getLastUuid(); + verify(listener, times(1)).onCrawlStart(); + verify(accounts, times(1)).getAllFromDynamo(eq(CHUNK_SIZE)); + verify(accounts, times(1)).getAllFromDynamo(eq(ACCOUNT2), eq(CHUNK_SIZE)); + verify(listener, times(1)).timeAndProcessCrawlChunk(Optional.empty(), List.of(account1, account2)); + verify(listener, times(1)).timeAndProcessCrawlChunk(Optional.of(ACCOUNT2), Collections.emptyList()); + verify(listener, times(1)).onCrawlEnd(); + verify(cache, times(1)).setLastUuid(eq(Optional.of(ACCOUNT2))); + // times(2) because empty() will get cached on the last run of loop and then again at the end + verify(cache, times(1)).setLastUuid(eq(Optional.empty())); + verify(cache, times(1)).releaseActiveWork(any(String.class)); + + verifyNoMoreInteractions(accounts); + verifyNoMoreInteractions(listener); + verifyNoMoreInteractions(cache); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountLockManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountLockManagerTest.java new file mode 100644 index 000000000..154a67590 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountLockManagerTest.java @@ -0,0 +1,107 @@ +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBLockClient; +import com.amazonaws.services.dynamodbv2.ReleaseLockOptions; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AccountLockManagerTest { + + private AmazonDynamoDBLockClient lockClient; + private ExecutorService executor; + + private AccountLockManager accountLockManager; + + private static final String FIRST_NUMBER = PhoneNumberUtil.getInstance().format( + PhoneNumberUtil.getInstance().getExampleNumber("US"), PhoneNumberUtil.PhoneNumberFormat.E164); + + private static final String SECOND_NUMBER = PhoneNumberUtil.getInstance().format( + PhoneNumberUtil.getInstance().getExampleNumber("JP"), PhoneNumberUtil.PhoneNumberFormat.E164); + + @BeforeEach + void setUp() { + lockClient = mock(AmazonDynamoDBLockClient.class); + executor = Executors.newSingleThreadExecutor(); + + accountLockManager = new AccountLockManager(lockClient); + } + + @AfterEach + void tearDown() throws InterruptedException { + executor.shutdown(); + + //noinspection ResultOfMethodCallIgnored + executor.awaitTermination(1, TimeUnit.SECONDS); + } + + @Test + void withLock() throws InterruptedException { + accountLockManager.withLock(List.of(FIRST_NUMBER, SECOND_NUMBER), () -> {}); + + verify(lockClient, times(2)).acquireLock(any()); + verify(lockClient, times(2)).releaseLock(any(ReleaseLockOptions.class)); + } + + @Test + void withLockTaskThrowsException() throws InterruptedException { + assertThrows(RuntimeException.class, () -> accountLockManager.withLock(List.of(FIRST_NUMBER, SECOND_NUMBER), () -> { + throw new RuntimeException(); + })); + + verify(lockClient, times(2)).acquireLock(any()); + verify(lockClient, times(2)).releaseLock(any(ReleaseLockOptions.class)); + } + + @Test + void withLockEmptyList() { + final Runnable task = mock(Runnable.class); + + assertThrows(IllegalArgumentException.class, () -> accountLockManager.withLock(Collections.emptyList(), () -> {})); + verify(task, never()).run(); + } + + @Test + void withLockAsync() throws InterruptedException { + accountLockManager.withLockAsync(List.of(FIRST_NUMBER, SECOND_NUMBER), + () -> CompletableFuture.completedFuture(null), executor).join(); + + verify(lockClient, times(2)).acquireLock(any()); + verify(lockClient, times(2)).releaseLock(any(ReleaseLockOptions.class)); + } + + @Test + void withLockAsyncTaskThrowsException() throws InterruptedException { + assertThrows(RuntimeException.class, + () -> accountLockManager.withLockAsync(List.of(FIRST_NUMBER, SECOND_NUMBER), + () -> CompletableFuture.failedFuture(new RuntimeException()), executor).join()); + + verify(lockClient, times(2)).acquireLock(any()); + verify(lockClient, times(2)).releaseLock(any(ReleaseLockOptions.class)); + } + + @Test + void withLockAsyncEmptyList() { + final Runnable task = mock(Runnable.class); + + assertThrows(IllegalArgumentException.class, + () -> accountLockManager.withLockAsync(Collections.emptyList(), + () -> CompletableFuture.completedFuture(null), executor)); + + verify(task, never()).run(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountTest.java new file mode 100644 index 000000000..0ff085f43 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountTest.java @@ -0,0 +1,383 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.tests.util.DevicesHelper.createDevice; +import static org.whispersystems.textsecuregcm.tests.util.DevicesHelper.setEnabled; + +import com.fasterxml.jackson.annotation.JsonFilter; +import java.lang.annotation.Annotation; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.util.TestClock; + +class AccountTest { + + private final Device oldMasterDevice = mock(Device.class); + private final Device recentMasterDevice = mock(Device.class); + private final Device agingSecondaryDevice = mock(Device.class); + private final Device recentSecondaryDevice = mock(Device.class); + private final Device oldSecondaryDevice = mock(Device.class); + + private final Device senderKeyCapableDevice = mock(Device.class); + private final Device senderKeyIncapableDevice = mock(Device.class); + private final Device senderKeyIncapableExpiredDevice = mock(Device.class); + + private final Device announcementGroupCapableDevice = mock(Device.class); + private final Device announcementGroupIncapableDevice = mock(Device.class); + private final Device announcementGroupIncapableExpiredDevice = mock(Device.class); + + private final Device changeNumberCapableDevice = mock(Device.class); + private final Device changeNumberIncapableDevice = mock(Device.class); + private final Device changeNumberIncapableExpiredDevice = mock(Device.class); + + private final Device pniCapableDevice = mock(Device.class); + private final Device pniIncapableDevice = mock(Device.class); + private final Device pniIncapableExpiredDevice = mock(Device.class); + + private final Device storiesCapableDevice = mock(Device.class); + private final Device storiesIncapableDevice = mock(Device.class); + private final Device storiesIncapableExpiredDevice = mock(Device.class); + + private final Device giftBadgesCapableDevice = mock(Device.class); + private final Device giftBadgesIncapableDevice = mock(Device.class); + private final Device giftBadgesIncapableExpiredDevice = mock(Device.class); + + private final Device paymentActivationCapableDevice = mock(Device.class); + private final Device paymentActivationIncapableDevice = mock(Device.class); + private final Device paymentActivationIncapableExpiredDevice = mock(Device.class); + + @BeforeEach + void setup() { + when(oldMasterDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(366)); + when(oldMasterDevice.isEnabled()).thenReturn(true); + when(oldMasterDevice.getId()).thenReturn(Device.MASTER_ID); + + when(recentMasterDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)); + when(recentMasterDevice.isEnabled()).thenReturn(true); + when(recentMasterDevice.getId()).thenReturn(Device.MASTER_ID); + + when(agingSecondaryDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31)); + when(agingSecondaryDevice.isEnabled()).thenReturn(false); + when(agingSecondaryDevice.getId()).thenReturn(2L); + + when(recentSecondaryDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)); + when(recentSecondaryDevice.isEnabled()).thenReturn(true); + when(recentSecondaryDevice.getId()).thenReturn(2L); + + when(oldSecondaryDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(366)); + when(oldSecondaryDevice.isEnabled()).thenReturn(false); + when(oldSecondaryDevice.getId()).thenReturn(2L); + + when(senderKeyCapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(senderKeyCapableDevice.isEnabled()).thenReturn(true); + + when(senderKeyIncapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(senderKeyIncapableDevice.isEnabled()).thenReturn(true); + + when(senderKeyIncapableExpiredDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(senderKeyIncapableExpiredDevice.isEnabled()).thenReturn(false); + + when(announcementGroupCapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(announcementGroupCapableDevice.isEnabled()).thenReturn(true); + + when(announcementGroupIncapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(announcementGroupIncapableDevice.isEnabled()).thenReturn(true); + + when(announcementGroupIncapableExpiredDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(announcementGroupIncapableExpiredDevice.isEnabled()).thenReturn(false); + + when(changeNumberCapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(changeNumberCapableDevice.isEnabled()).thenReturn(true); + + when(changeNumberIncapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(changeNumberIncapableDevice.isEnabled()).thenReturn(true); + + when(changeNumberIncapableExpiredDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(changeNumberIncapableExpiredDevice.isEnabled()).thenReturn(false); + + when(pniCapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, true, false)); + when(pniCapableDevice.isEnabled()).thenReturn(true); + + when(pniIncapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(pniIncapableDevice.isEnabled()).thenReturn(true); + + when(pniIncapableExpiredDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(pniIncapableExpiredDevice.isEnabled()).thenReturn(false); + + when(storiesCapableDevice.getId()).thenReturn(1L); + when(storiesCapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(storiesCapableDevice.isEnabled()).thenReturn(true); + + when(storiesCapableDevice.getId()).thenReturn(2L); + when(storiesIncapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(storiesIncapableDevice.isEnabled()).thenReturn(true); + + when(storiesCapableDevice.getId()).thenReturn(3L); + when(storiesIncapableExpiredDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, false, false)); + when(storiesIncapableExpiredDevice.isEnabled()).thenReturn(false); + + when(giftBadgesCapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, true, false)); + when(giftBadgesCapableDevice.isEnabled()).thenReturn(true); + when(giftBadgesIncapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, true, false)); + when(giftBadgesIncapableDevice.isEnabled()).thenReturn(true); + when(giftBadgesIncapableExpiredDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, true, false)); + when(giftBadgesIncapableExpiredDevice.isEnabled()).thenReturn(false); + + when(paymentActivationCapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, true, true)); + when(paymentActivationCapableDevice.isEnabled()).thenReturn(true); + when(paymentActivationIncapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, true, false)); + when(paymentActivationIncapableDevice.isEnabled()).thenReturn(true); + when(paymentActivationIncapableExpiredDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, true, false)); + when(paymentActivationIncapableExpiredDevice.isEnabled()).thenReturn(false); + + } + + @Test + void testIsEnabled() { + final Device enabledMasterDevice = mock(Device.class); + final Device enabledLinkedDevice = mock(Device.class); + final Device disabledMasterDevice = mock(Device.class); + final Device disabledLinkedDevice = mock(Device.class); + + when(enabledMasterDevice.isEnabled()).thenReturn(true); + when(enabledLinkedDevice.isEnabled()).thenReturn(true); + when(disabledMasterDevice.isEnabled()).thenReturn(false); + when(disabledLinkedDevice.isEnabled()).thenReturn(false); + + when(enabledMasterDevice.getId()).thenReturn(1L); + when(enabledLinkedDevice.getId()).thenReturn(2L); + when(disabledMasterDevice.getId()).thenReturn(1L); + when(disabledLinkedDevice.getId()).thenReturn(2L); + + assertTrue(AccountsHelper.generateTestAccount("+14151234567", List.of(enabledMasterDevice)).isEnabled()); + assertTrue(AccountsHelper.generateTestAccount("+14151234567", List.of(enabledMasterDevice, enabledLinkedDevice)).isEnabled()); + assertTrue(AccountsHelper.generateTestAccount("+14151234567", List.of(enabledMasterDevice, disabledLinkedDevice)).isEnabled()); + assertFalse(AccountsHelper.generateTestAccount("+14151234567", List.of(disabledMasterDevice)).isEnabled()); + assertFalse(AccountsHelper.generateTestAccount("+14151234567", List.of(disabledMasterDevice, enabledLinkedDevice)).isEnabled()); + assertFalse(AccountsHelper.generateTestAccount("+14151234567", List.of(disabledMasterDevice, disabledLinkedDevice)).isEnabled()); + } + + @Test + void testIsTransferSupported() { + final Device transferCapableMasterDevice = mock(Device.class); + final Device nonTransferCapableMasterDevice = mock(Device.class); + final Device transferCapableLinkedDevice = mock(Device.class); + + final DeviceCapabilities transferCapabilities = mock(DeviceCapabilities.class); + final DeviceCapabilities nonTransferCapabilities = mock(DeviceCapabilities.class); + + when(transferCapableMasterDevice.getId()).thenReturn(1L); + when(transferCapableMasterDevice.isMaster()).thenReturn(true); + when(transferCapableMasterDevice.getCapabilities()).thenReturn(transferCapabilities); + + when(nonTransferCapableMasterDevice.getId()).thenReturn(1L); + when(nonTransferCapableMasterDevice.isMaster()).thenReturn(true); + when(nonTransferCapableMasterDevice.getCapabilities()).thenReturn(nonTransferCapabilities); + + when(transferCapableLinkedDevice.getId()).thenReturn(2L); + when(transferCapableLinkedDevice.isMaster()).thenReturn(false); + when(transferCapableLinkedDevice.getCapabilities()).thenReturn(transferCapabilities); + + when(transferCapabilities.transfer()).thenReturn(true); + when(nonTransferCapabilities.transfer()).thenReturn(false); + + { + final Account transferableMasterAccount = + AccountsHelper.generateTestAccount("+14152222222", UUID.randomUUID(), UUID.randomUUID(), List.of(transferCapableMasterDevice), "1234".getBytes()); + + assertTrue(transferableMasterAccount.isTransferSupported()); + } + + { + final Account nonTransferableMasterAccount = + AccountsHelper.generateTestAccount("+14152222222", UUID.randomUUID(), UUID.randomUUID(), List.of(nonTransferCapableMasterDevice), "1234".getBytes()); + + assertFalse(nonTransferableMasterAccount.isTransferSupported()); + } + + { + final Account transferableLinkedAccount = AccountsHelper.generateTestAccount("+14152222222", UUID.randomUUID(), UUID.randomUUID(), List.of(nonTransferCapableMasterDevice, transferCapableLinkedDevice), "1234".getBytes()); + + assertFalse(transferableLinkedAccount.isTransferSupported()); + } + } + + @Test + void testDiscoverableByPhoneNumber() { + final Account account = AccountsHelper.generateTestAccount("+14152222222", UUID.randomUUID(), UUID.randomUUID(), List.of(recentMasterDevice), + "1234".getBytes()); + + assertTrue(account.isDiscoverableByPhoneNumber(), + "Freshly-loaded legacy accounts should be discoverable by phone number."); + + account.setDiscoverableByPhoneNumber(false); + assertFalse(account.isDiscoverableByPhoneNumber()); + + account.setDiscoverableByPhoneNumber(true); + assertTrue(account.isDiscoverableByPhoneNumber()); + } + + @Test + void isPniSupported() { + assertThat(AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), + UUID.randomUUID(), List.of(pniCapableDevice), + "1234".getBytes(StandardCharsets.UTF_8)).isPniSupported()).isTrue(); + assertThat(AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), + UUID.randomUUID(), List.of(pniCapableDevice, pniIncapableDevice), + "1234".getBytes(StandardCharsets.UTF_8)).isPniSupported()).isFalse(); + assertThat(AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), + UUID.randomUUID(), List.of(pniCapableDevice, pniIncapableExpiredDevice), + "1234".getBytes(StandardCharsets.UTF_8)).isPniSupported()).isTrue(); + } + + @Test + void isPaymentActivationSupported() { + assertThat(AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), + List.of(paymentActivationCapableDevice), + "1234".getBytes(StandardCharsets.UTF_8)).isPaymentActivationSupported()).isTrue(); + assertThat(AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), + List.of(paymentActivationCapableDevice, paymentActivationIncapableDevice), + "1234".getBytes(StandardCharsets.UTF_8)).isPaymentActivationSupported()).isFalse(); + assertThat(AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), + List.of(paymentActivationCapableDevice, paymentActivationIncapableExpiredDevice), + "1234".getBytes(StandardCharsets.UTF_8)).isPaymentActivationSupported()).isTrue(); + } + + @Test + void stale() { + final Account account = AccountsHelper.generateTestAccount("+14151234567", UUID.randomUUID(), UUID.randomUUID(), Collections.emptyList(), + new byte[0]); + + assertDoesNotThrow(account::getNumber); + + account.markStale(); + + assertThrows(AssertionError.class, account::getNumber); + assertDoesNotThrow(account::getUuid); + } + + @Test + void getNextDeviceId() { + + final List devices = List.of(createDevice(Device.MASTER_ID)); + + final Account account = AccountsHelper.generateTestAccount("+14151234567", UUID.randomUUID(), UUID.randomUUID(), devices, new byte[0]); + + assertThat(account.getNextDeviceId()).isEqualTo(2L); + + account.addDevice(createDevice(2L)); + + assertThat(account.getNextDeviceId()).isEqualTo(3L); + + account.addDevice(createDevice(3L)); + + setEnabled(account.getDevice(2L).orElseThrow(), false); + + assertThat(account.getNextDeviceId()).isEqualTo(4L); + + account.removeDevice(2L); + + assertThat(account.getNextDeviceId()).isEqualTo(2L); + } + + @Test + void replaceDevice() { + final Device firstDevice = createDevice(Device.MASTER_ID); + final Device secondDevice = createDevice(Device.MASTER_ID); + final Account account = AccountsHelper.generateTestAccount("+14151234567", UUID.randomUUID(), UUID.randomUUID(), List.of(firstDevice), new byte[0]); + + assertEquals(List.of(firstDevice), account.getDevices()); + + account.addDevice(secondDevice); + + assertEquals(List.of(secondDevice), account.getDevices()); + } + + @Test + void addAndRemoveBadges() { + final Account account = AccountsHelper.generateTestAccount("+14151234567", UUID.randomUUID(), UUID.randomUUID(), List.of(createDevice(Device.MASTER_ID)), new byte[0]); + final Clock clock = TestClock.pinned(Instant.ofEpochSecond(40)); + + account.addBadge(clock, new AccountBadge("foo", Instant.ofEpochSecond(42), false)); + account.addBadge(clock, new AccountBadge("bar", Instant.ofEpochSecond(44), true)); + account.addBadge(clock, new AccountBadge("baz", Instant.ofEpochSecond(46), true)); + + assertThat(account.getBadges()).hasSize(3); + + account.removeBadge(clock, "baz"); + + assertThat(account.getBadges()).hasSize(2); + + account.addBadge(clock, new AccountBadge("foo", Instant.ofEpochSecond(50), false)); + + assertThat(account.getBadges()).hasSize(2).element(0).satisfies(badge -> { + assertThat(badge.getId()).isEqualTo("foo"); + assertThat(badge.getExpiration().getEpochSecond()).isEqualTo(50); + assertThat(badge.isVisible()).isFalse(); + }); + + account.addBadge(clock, new AccountBadge("foo", Instant.ofEpochSecond(51), true)); + + assertThat(account.getBadges()).hasSize(2).element(0).satisfies(badge -> { + assertThat(badge.getId()).isEqualTo("foo"); + assertThat(badge.getExpiration().getEpochSecond()).isEqualTo(51); + assertThat(badge.isVisible()).isTrue(); + }); + } + + @Test + public void testAccountClassJsonFilterIdMatchesClassName() throws Exception { + // Some logic relies on the @JsonFilter name being equal to the class name. + // This test is just making sure that annotation is there and that the ID matches class name. + final Optional maybeJsonFilterAnnotation = Arrays.stream(Account.class.getAnnotations()) + .filter(a -> a.annotationType().equals(JsonFilter.class)) + .findFirst(); + assertTrue(maybeJsonFilterAnnotation.isPresent()); + final JsonFilter jsonFilterAnnotation = (JsonFilter) maybeJsonFilterAnnotation.get(); + assertEquals(Account.class.getSimpleName(), jsonFilterAnnotation.value()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerChangeNumberIntegrationTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerChangeNumberIntegrationTest.java new file mode 100644 index 000000000..40b5b1c25 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerChangeNumberIntegrationTest.java @@ -0,0 +1,294 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Clock; +import java.util.ArrayList; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; +import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; +import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; + +class AccountsManagerChangeNumberIntegrationTest { + + private static final int SCAN_PAGE_SIZE = 1; + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension( + Tables.ACCOUNTS, + Tables.DELETED_ACCOUNTS, + Tables.DELETED_ACCOUNTS_LOCK, + Tables.NUMBERS, + Tables.PNI, + Tables.PNI_ASSIGNMENTS, + Tables.USERNAMES); + + @RegisterExtension + static final RedisClusterExtension CACHE_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + private ClientPresenceManager clientPresenceManager; + private ExecutorService accountLockExecutor; + + private AccountsManager accountsManager; + + @BeforeEach + void setup() throws InterruptedException { + + { + @SuppressWarnings("unchecked") final DynamicConfigurationManager dynamicConfigurationManager = + mock(DynamicConfigurationManager.class); + + DynamicConfiguration dynamicConfiguration = new DynamicConfiguration(); + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + + final Accounts accounts = new Accounts( + DYNAMO_DB_EXTENSION.getDynamoDbClient(), + DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + Tables.ACCOUNTS.tableName(), + Tables.NUMBERS.tableName(), + Tables.PNI_ASSIGNMENTS.tableName(), + Tables.USERNAMES.tableName(), + Tables.DELETED_ACCOUNTS.tableName(), + SCAN_PAGE_SIZE); + + accountLockExecutor = Executors.newSingleThreadExecutor(); + + final AccountLockManager accountLockManager = new AccountLockManager(DYNAMO_DB_EXTENSION.getDynamoDbClient(), + Tables.DELETED_ACCOUNTS_LOCK.tableName()); + + final SecureStorageClient secureStorageClient = mock(SecureStorageClient.class); + when(secureStorageClient.deleteStoredData(any())).thenReturn(CompletableFuture.completedFuture(null)); + + final SecureValueRecovery2Client svr2Client = mock(SecureValueRecovery2Client.class); + when(svr2Client.deleteBackups(any())).thenReturn(CompletableFuture.completedFuture(null)); + + clientPresenceManager = mock(ClientPresenceManager.class); + + final PhoneNumberIdentifiers phoneNumberIdentifiers = + new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbClient(), Tables.PNI.tableName()); + + final KeysManager keysManager = mock(KeysManager.class); + when(keysManager.delete(any())).thenReturn(CompletableFuture.completedFuture(null)); + + final MessagesManager messagesManager = mock(MessagesManager.class); + when(messagesManager.clear(any())).thenReturn(CompletableFuture.completedFuture(null)); + + final ProfilesManager profilesManager = mock(ProfilesManager.class); + when(profilesManager.deleteAll(any())).thenReturn(CompletableFuture.completedFuture(null)); + + final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = + mock(RegistrationRecoveryPasswordsManager.class); + + when(registrationRecoveryPasswordsManager.removeForNumber(any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + accountsManager = new AccountsManager( + accounts, + phoneNumberIdentifiers, + CACHE_CLUSTER_EXTENSION.getRedisCluster(), + accountLockManager, + keysManager, + messagesManager, + profilesManager, + secureStorageClient, + svr2Client, + clientPresenceManager, + mock(ExperimentEnrollmentManager.class), + registrationRecoveryPasswordsManager, + accountLockExecutor, + mock(Clock.class)); + } + } + + @AfterEach + void tearDown() throws InterruptedException { + accountLockExecutor.shutdown(); + + //noinspection ResultOfMethodCallIgnored + accountLockExecutor.awaitTermination(1, TimeUnit.SECONDS); + } + + @Test + void testChangeNumber() throws InterruptedException, MismatchedDevicesException { + final String originalNumber = "+18005551111"; + final String secondNumber = "+18005552222"; + + final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>()); + final UUID originalUuid = account.getUuid(); + final UUID originalPni = account.getPhoneNumberIdentifier(); + + accountsManager.changeNumber(account, secondNumber, null, null, null, null); + + assertTrue(accountsManager.getByE164(originalNumber).isEmpty()); + + assertTrue(accountsManager.getByE164(secondNumber).isPresent()); + assertEquals(originalUuid, accountsManager.getByE164(secondNumber).map(Account::getUuid).orElseThrow()); + assertNotEquals(originalPni, accountsManager.getByE164(secondNumber).map(Account::getPhoneNumberIdentifier).orElseThrow()); + + assertEquals(secondNumber, accountsManager.getByAccountIdentifier(originalUuid).map(Account::getNumber).orElseThrow()); + + assertEquals(Optional.empty(), accountsManager.findRecentlyDeletedAccountIdentifier(originalNumber)); + assertEquals(Optional.empty(), accountsManager.findRecentlyDeletedAccountIdentifier(secondNumber)); + } + + @Test + void testChangeNumberWithPniExtensions() throws InterruptedException, MismatchedDevicesException { + final String originalNumber = "+18005551111"; + final String secondNumber = "+18005552222"; + final int rotatedPniRegistrationId = 17; + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + final ECSignedPreKey rotatedSignedPreKey = KeysHelper.signedECPreKey(1L, pniIdentityKeyPair); + + final AccountAttributes accountAttributes = new AccountAttributes(true, rotatedPniRegistrationId + 1, "test", null, true, new Device.DeviceCapabilities(false, false, false, false)); + final Account account = accountsManager.create(originalNumber, "password", null, accountAttributes, new ArrayList<>()); + account.getMasterDevice().orElseThrow().setSignedPreKey(KeysHelper.signedECPreKey(1, pniIdentityKeyPair)); + + final UUID originalUuid = account.getUuid(); + final UUID originalPni = account.getPhoneNumberIdentifier(); + + final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey()); + final Map preKeys = Map.of(Device.MASTER_ID, rotatedSignedPreKey); + final Map registrationIds = Map.of(Device.MASTER_ID, rotatedPniRegistrationId); + + final Account updatedAccount = accountsManager.changeNumber(account, secondNumber, pniIdentityKey, preKeys, null, registrationIds); + + assertTrue(accountsManager.getByE164(originalNumber).isEmpty()); + + assertTrue(accountsManager.getByE164(secondNumber).isPresent()); + assertEquals(originalUuid, accountsManager.getByE164(secondNumber).map(Account::getUuid).orElseThrow()); + assertNotEquals(originalPni, accountsManager.getByE164(secondNumber).map(Account::getPhoneNumberIdentifier).orElseThrow()); + + assertEquals(secondNumber, accountsManager.getByAccountIdentifier(originalUuid).map(Account::getNumber).orElseThrow()); + + assertEquals(Optional.empty(), accountsManager.findRecentlyDeletedAccountIdentifier(originalNumber)); + assertEquals(Optional.empty(), accountsManager.findRecentlyDeletedAccountIdentifier(secondNumber)); + + assertEquals(pniIdentityKey, updatedAccount.getIdentityKey(IdentityType.PNI)); + + assertEquals(OptionalInt.of(rotatedPniRegistrationId), + updatedAccount.getMasterDevice().orElseThrow().getPhoneNumberIdentityRegistrationId()); + + assertEquals(rotatedSignedPreKey, updatedAccount.getMasterDevice().orElseThrow().getSignedPreKey(IdentityType.PNI)); + } + + @Test + void testChangeNumberReturnToOriginal() throws InterruptedException, MismatchedDevicesException { + final String originalNumber = "+18005551111"; + final String secondNumber = "+18005552222"; + + Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>()); + final UUID originalUuid = account.getUuid(); + final UUID originalPni = account.getPhoneNumberIdentifier(); + + account = accountsManager.changeNumber(account, secondNumber, null, null, null, null); + accountsManager.changeNumber(account, originalNumber, null, null, null, null); + + assertTrue(accountsManager.getByE164(originalNumber).isPresent()); + assertEquals(originalUuid, accountsManager.getByE164(originalNumber).map(Account::getUuid).orElseThrow()); + assertEquals(originalPni, accountsManager.getByE164(originalNumber).map(Account::getPhoneNumberIdentifier).orElseThrow()); + + assertTrue(accountsManager.getByE164(secondNumber).isEmpty()); + + assertEquals(originalNumber, accountsManager.getByAccountIdentifier(originalUuid).map(Account::getNumber).orElseThrow()); + + assertEquals(Optional.empty(), accountsManager.findRecentlyDeletedAccountIdentifier(originalNumber)); + assertEquals(Optional.empty(), accountsManager.findRecentlyDeletedAccountIdentifier(secondNumber)); + } + + @Test + void testChangeNumberContested() throws InterruptedException, MismatchedDevicesException { + final String originalNumber = "+18005551111"; + final String secondNumber = "+18005552222"; + + final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>()); + final UUID originalUuid = account.getUuid(); + + final Account existingAccount = accountsManager.create(secondNumber, "password", null, new AccountAttributes(), new ArrayList<>()); + final UUID existingAccountUuid = existingAccount.getUuid(); + + accountsManager.changeNumber(account, secondNumber, null, null, null, null); + + assertTrue(accountsManager.getByE164(originalNumber).isEmpty()); + + assertTrue(accountsManager.getByE164(secondNumber).isPresent()); + assertEquals(Optional.of(originalUuid), accountsManager.getByE164(secondNumber).map(Account::getUuid)); + + assertEquals(secondNumber, accountsManager.getByAccountIdentifier(originalUuid).map(Account::getNumber).orElseThrow()); + + verify(clientPresenceManager).disconnectPresence(existingAccountUuid, Device.MASTER_ID); + + assertEquals(Optional.of(existingAccountUuid), accountsManager.findRecentlyDeletedAccountIdentifier(originalNumber)); + assertEquals(Optional.empty(), accountsManager.findRecentlyDeletedAccountIdentifier(secondNumber)); + + accountsManager.changeNumber(accountsManager.getByAccountIdentifier(originalUuid).orElseThrow(), originalNumber, null, null, null, null); + + final Account existingAccount2 = accountsManager.create(secondNumber, "password", null, new AccountAttributes(), + new ArrayList<>()); + + assertEquals(existingAccountUuid, existingAccount2.getUuid()); + } + + @Test + void testChangeNumberChaining() throws InterruptedException, MismatchedDevicesException { + final String originalNumber = "+18005551111"; + final String secondNumber = "+18005552222"; + + final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>()); + final UUID originalUuid = account.getUuid(); + final UUID originalPni = account.getPhoneNumberIdentifier(); + + final Account existingAccount = accountsManager.create(secondNumber, "password", null, new AccountAttributes(), new ArrayList<>()); + final UUID existingAccountUuid = existingAccount.getUuid(); + + final Account changedNumberAccount = accountsManager.changeNumber(account, secondNumber, null, null, null, null); + final UUID secondPni = changedNumberAccount.getPhoneNumberIdentifier(); + + final Account reRegisteredAccount = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>()); + + assertEquals(existingAccountUuid, reRegisteredAccount.getUuid()); + assertEquals(originalPni, reRegisteredAccount.getPhoneNumberIdentifier()); + + assertEquals(Optional.empty(), accountsManager.findRecentlyDeletedAccountIdentifier(originalNumber)); + assertEquals(Optional.empty(), accountsManager.findRecentlyDeletedAccountIdentifier(secondNumber)); + + final Account changedNumberReRegisteredAccount = accountsManager.changeNumber(reRegisteredAccount, secondNumber, null, null, null, null); + + assertEquals(Optional.of(originalUuid), accountsManager.findRecentlyDeletedAccountIdentifier(originalNumber)); + assertEquals(Optional.empty(), accountsManager.findRecentlyDeletedAccountIdentifier(secondNumber)); + assertEquals(secondPni, changedNumberReRegisteredAccount.getPhoneNumberIdentifier()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerConcurrentModificationIntegrationTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerConcurrentModificationIntegrationTest.java new file mode 100644 index 000000000..ca1cfd0f2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerConcurrentModificationIntegrationTest.java @@ -0,0 +1,224 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import java.io.IOException; +import java.time.Clock; +import java.time.Instant; +import java.util.ArrayList; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.Curve; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; +import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import org.whispersystems.textsecuregcm.tests.util.DevicesHelper; +import org.whispersystems.textsecuregcm.tests.util.JsonHelpers; +import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper; +import org.whispersystems.textsecuregcm.util.Pair; + + +class AccountsManagerConcurrentModificationIntegrationTest { + + private static final int SCAN_PAGE_SIZE = 1; + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension( + Tables.ACCOUNTS, + Tables.NUMBERS, + Tables.PNI_ASSIGNMENTS, + Tables.DELETED_ACCOUNTS + ); + + private Accounts accounts; + + private AccountsManager accountsManager; + + private RedisAdvancedClusterCommands commands; + + private Executor mutationExecutor = new ThreadPoolExecutor(20, 20, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(20)); + + @BeforeEach + void setup() throws InterruptedException { + + @SuppressWarnings("unchecked") final DynamicConfigurationManager dynamicConfigurationManager = + mock(DynamicConfigurationManager.class); + when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration()); + + accounts = new Accounts( + DYNAMO_DB_EXTENSION.getDynamoDbClient(), + DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + Tables.ACCOUNTS.tableName(), + Tables.NUMBERS.tableName(), + Tables.PNI_ASSIGNMENTS.tableName(), + Tables.USERNAMES.tableName(), + Tables.DELETED_ACCOUNTS.tableName(), + SCAN_PAGE_SIZE); + + { + //noinspection unchecked + commands = mock(RedisAdvancedClusterCommands.class); + + final AccountLockManager accountLockManager = mock(AccountLockManager.class); + + doAnswer(invocation -> { + final Runnable task = invocation.getArgument(1); + task.run(); + + return null; + }).when(accountLockManager).withLock(any(), any()); + + when(accountLockManager.withLockAsync(any(), any(), any())).thenAnswer(invocation -> { + final Supplier> taskSupplier = invocation.getArgument(1); + taskSupplier.get().join(); + + return CompletableFuture.completedFuture(null); + }); + + final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class); + when(phoneNumberIdentifiers.getPhoneNumberIdentifier(anyString())) + .thenAnswer((Answer) invocation -> UUID.randomUUID()); + + accountsManager = new AccountsManager( + accounts, + phoneNumberIdentifiers, + RedisClusterHelper.builder().stringCommands(commands).build(), + accountLockManager, + mock(KeysManager.class), + mock(MessagesManager.class), + mock(ProfilesManager.class), + mock(SecureStorageClient.class), + mock(SecureValueRecovery2Client.class), + mock(ClientPresenceManager.class), + mock(ExperimentEnrollmentManager.class), + mock(RegistrationRecoveryPasswordsManager.class), + mock(Executor.class), + mock(Clock.class) + ); + } + } + + @Test + void testConcurrentUpdate() throws IOException, InterruptedException { + + final UUID uuid; + { + final Account account = accountsManager.update( + accountsManager.create("+14155551212", "password", null, new AccountAttributes(), new ArrayList<>()), + a -> { + a.setUnidentifiedAccessKey(new byte[16]); + a.removeDevice(1); + a.addDevice(DevicesHelper.createDevice(1)); + }); + + uuid = account.getUuid(); + } + + final boolean discoverableByPhoneNumber = false; + final String currentProfileVersion = "cpv"; + final IdentityKey identityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + final byte[] unidentifiedAccessKey = new byte[]{1}; + final String pin = "1234"; + final String registrationLock = "reglock"; + final SaltedTokenHash credentials = SaltedTokenHash.generateFor(registrationLock); + final boolean unrestrictedUnidentifiedAccess = true; + final long lastSeen = Instant.now().getEpochSecond(); + + CompletableFuture.allOf( + modifyAccount(uuid, account -> account.setDiscoverableByPhoneNumber(discoverableByPhoneNumber)), + modifyAccount(uuid, account -> account.setCurrentProfileVersion(currentProfileVersion)), + modifyAccount(uuid, account -> account.setIdentityKey(identityKey)), + modifyAccount(uuid, account -> account.setUnidentifiedAccessKey(unidentifiedAccessKey)), + modifyAccount(uuid, account -> account.setRegistrationLock(credentials.hash(), credentials.salt())), + modifyAccount(uuid, account -> account.setUnrestrictedUnidentifiedAccess(unrestrictedUnidentifiedAccess)), + modifyDevice(uuid, Device.MASTER_ID, device -> device.setLastSeen(lastSeen)), + modifyDevice(uuid, Device.MASTER_ID, device -> device.setName("deviceName")) + ).join(); + + final Account managerAccount = accountsManager.getByAccountIdentifier(uuid).orElseThrow(); + final Account dynamoAccount = accounts.getByAccountIdentifier(uuid).orElseThrow(); + + final Account redisAccount = getLastAccountFromRedisMock(commands); + + Stream.of( + new Pair<>("manager", managerAccount), + new Pair<>("dynamo", dynamoAccount), + new Pair<>("redis", redisAccount) + ).forEach(pair -> + verifyAccount(pair.first(), pair.second(), discoverableByPhoneNumber, + currentProfileVersion, identityKey, unidentifiedAccessKey, pin, registrationLock, + unrestrictedUnidentifiedAccess, lastSeen)); + } + + private Account getLastAccountFromRedisMock(RedisAdvancedClusterCommands commands) throws IOException { + ArgumentCaptor redisSetArgumentCapture = ArgumentCaptor.forClass(String.class); + + verify(commands, atLeast(20)).setex(anyString(), anyLong(), redisSetArgumentCapture.capture()); + + return JsonHelpers.fromJson(redisSetArgumentCapture.getValue(), Account.class); + } + + private void verifyAccount(final String name, final Account account, final boolean discoverableByPhoneNumber, final String currentProfileVersion, final IdentityKey identityKey, final byte[] unidentifiedAccessKey, final String pin, final String clientRegistrationLock, final boolean unrestrictedUnidentifiedAccess, final long lastSeen) { + + assertAll(name, + () -> assertEquals(discoverableByPhoneNumber, account.isDiscoverableByPhoneNumber()), + () -> assertEquals(currentProfileVersion, account.getCurrentProfileVersion().orElseThrow()), + () -> assertEquals(identityKey, account.getIdentityKey(IdentityType.ACI)), + () -> assertArrayEquals(unidentifiedAccessKey, account.getUnidentifiedAccessKey().orElseThrow()), + () -> assertTrue(account.getRegistrationLock().verify(clientRegistrationLock)), + () -> assertEquals(unrestrictedUnidentifiedAccess, account.isUnrestrictedUnidentifiedAccess()) + ); + } + + private CompletableFuture modifyAccount(final UUID uuid, final Consumer accountMutation) { + + return CompletableFuture.runAsync(() -> { + final Account account = accountsManager.getByAccountIdentifier(uuid).orElseThrow(); + accountsManager.update(account, accountMutation); + }, mutationExecutor); + } + + private CompletableFuture modifyDevice(final UUID uuid, final long deviceId, final Consumer deviceMutation) { + + return CompletableFuture.runAsync(() -> { + final Account account = accountsManager.getByAccountIdentifier(uuid).orElseThrow(); + accountsManager.updateDevice(account, deviceId, deviceMutation); + }, mutationExecutor); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java new file mode 100644 index 000000000..7da82dd22 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java @@ -0,0 +1,1558 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import io.lettuce.core.RedisException; +import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.stubbing.Answer; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; +import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client; +import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.tests.util.DevicesHelper; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; +import org.whispersystems.textsecuregcm.tests.util.MockRedisFuture; +import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper; +import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil; + +@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) +class AccountsManagerTest { + private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE"; + private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc"; + private static final String BASE_64_URL_ENCRYPTED_USERNAME_1 = "md1votbj9r794DsqTNrBqA"; + private static final String BASE_64_URL_ENCRYPTED_USERNAME_2 = "9hrqVLy59bzgPse-S9NUsA"; + + private static final byte[] USERNAME_HASH_1 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_1); + private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2); + private static final byte[] ENCRYPTED_USERNAME_1 = Base64.getUrlDecoder().decode(BASE_64_URL_ENCRYPTED_USERNAME_1); + private static final byte[] ENCRYPTED_USERNAME_2 = Base64.getUrlDecoder().decode(BASE_64_URL_ENCRYPTED_USERNAME_2); + + private Accounts accounts; + private KeysManager keysManager; + private MessagesManager messagesManager; + private ProfilesManager profilesManager; + private ClientPresenceManager clientPresenceManager; + private ExperimentEnrollmentManager enrollmentManager; + + private Map phoneNumberIdentifiersByE164; + + private RedisAdvancedClusterCommands commands; + private RedisAdvancedClusterAsyncCommands asyncCommands; + private AccountsManager accountsManager; + + private static final Answer ACCOUNT_UPDATE_ANSWER = (answer) -> { + // it is implicit in the update() contract is that a successful call will + // result in an incremented version + final Account updatedAccount = answer.getArgument(0, Account.class); + updatedAccount.setVersion(updatedAccount.getVersion() + 1); + return null; + }; + + private static final Answer> ACCOUNT_UPDATE_ASYNC_ANSWER = invocation -> { + // it is implicit in the update() contract is that a successful call will + // result in an incremented version + final Account updatedAccount = invocation.getArgument(0, Account.class); + updatedAccount.setVersion(updatedAccount.getVersion() + 1); + + return CompletableFuture.completedFuture(null); + }; + + @BeforeEach + void setup() throws InterruptedException { + accounts = mock(Accounts.class); + keysManager = mock(KeysManager.class); + messagesManager = mock(MessagesManager.class); + profilesManager = mock(ProfilesManager.class); + clientPresenceManager = mock(ClientPresenceManager.class); + + //noinspection unchecked + commands = mock(RedisAdvancedClusterCommands.class); + + //noinspection unchecked + asyncCommands = mock(RedisAdvancedClusterAsyncCommands.class); + when(asyncCommands.del(any())).thenReturn(MockRedisFuture.completedFuture(0L)); + when(asyncCommands.get(any())).thenReturn(MockRedisFuture.completedFuture(null)); + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + when(accounts.updateAsync(any())).thenReturn(CompletableFuture.completedFuture(null)); + when(accounts.delete(any())).thenReturn(CompletableFuture.completedFuture(null)); + + doAnswer((Answer) invocation -> { + final Account account = invocation.getArgument(0, Account.class); + final String number = invocation.getArgument(1, String.class); + final UUID phoneNumberIdentifier = invocation.getArgument(2, UUID.class); + + account.setNumber(number, phoneNumberIdentifier); + + return null; + }).when(accounts).changeNumber(any(), anyString(), any(), any()); + + final SecureStorageClient storageClient = mock(SecureStorageClient.class); + when(storageClient.deleteStoredData(any())).thenReturn(CompletableFuture.completedFuture(null)); + + final SecureValueRecovery2Client svr2Client = mock(SecureValueRecovery2Client.class); + when(svr2Client.deleteBackups(any())).thenReturn(CompletableFuture.completedFuture(null)); + + final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class); + phoneNumberIdentifiersByE164 = new HashMap<>(); + + when(phoneNumberIdentifiers.getPhoneNumberIdentifier(anyString())).thenAnswer((Answer) invocation -> { + final String number = invocation.getArgument(0, String.class); + return phoneNumberIdentifiersByE164.computeIfAbsent(number, n -> UUID.randomUUID()); + }); + + @SuppressWarnings("unchecked") final DynamicConfigurationManager dynamicConfigurationManager = + mock(DynamicConfigurationManager.class); + + final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + + enrollmentManager = mock(ExperimentEnrollmentManager.class); + when(enrollmentManager.isEnrolled(any(UUID.class), eq(AccountsManager.USERNAME_EXPERIMENT_NAME))).thenReturn(true); + when(accounts.usernameHashAvailable(any())).thenReturn(CompletableFuture.completedFuture(true)); + + final AccountLockManager accountLockManager = mock(AccountLockManager.class); + + doAnswer(invocation -> { + final Runnable task = invocation.getArgument(1); + task.run(); + + return null; + }).when(accountLockManager).withLock(any(), any()); + + when(accountLockManager.withLockAsync(any(), any(), any())).thenAnswer(invocation -> { + final Supplier> taskSupplier = invocation.getArgument(1); + taskSupplier.get().join(); + + return CompletableFuture.completedFuture(null); + }); + + final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = + mock(RegistrationRecoveryPasswordsManager.class); + + when(registrationRecoveryPasswordsManager.removeForNumber(anyString())).thenReturn(CompletableFuture.completedFuture(null)); + + when(keysManager.delete(any())).thenReturn(CompletableFuture.completedFuture(null)); + when(messagesManager.clear(any())).thenReturn(CompletableFuture.completedFuture(null)); + when(profilesManager.deleteAll(any())).thenReturn(CompletableFuture.completedFuture(null)); + + accountsManager = new AccountsManager( + accounts, + phoneNumberIdentifiers, + RedisClusterHelper.builder() + .stringCommands(commands) + .stringAsyncCommands(asyncCommands) + .build(), + accountLockManager, + keysManager, + messagesManager, + profilesManager, + storageClient, + svr2Client, + clientPresenceManager, + enrollmentManager, + registrationRecoveryPasswordsManager, + mock(Executor.class), + mock(Clock.class)); + } + + @Test + void testGetByServiceIdentifier() { + final UUID aci = UUID.randomUUID(); + final UUID pni = UUID.randomUUID(); + + when(commands.get(eq("AccountMap::" + pni))).thenReturn(aci.toString()); + when(commands.get(eq("Account3::" + aci))).thenReturn( + "{\"number\": \"+14152222222\", \"pni\": \"" + pni + "\"}"); + + assertTrue(accountsManager.getByServiceIdentifier(new AciServiceIdentifier(aci)).isPresent()); + assertTrue(accountsManager.getByServiceIdentifier(new PniServiceIdentifier(pni)).isPresent()); + assertFalse(accountsManager.getByServiceIdentifier(new AciServiceIdentifier(pni)).isPresent()); + assertFalse(accountsManager.getByServiceIdentifier(new PniServiceIdentifier(aci)).isPresent()); + } + + @Test + void testGetByServiceIdentifierAsync() { + final UUID aci = UUID.randomUUID(); + final UUID pni = UUID.randomUUID(); + + when(asyncCommands.get(eq("AccountMap::" + pni))).thenReturn(MockRedisFuture.completedFuture(aci.toString())); + when(asyncCommands.get(eq("Account3::" + aci))).thenReturn(MockRedisFuture.completedFuture( + "{\"number\": \"+14152222222\", \"pni\": \"" + pni + "\"}")); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + when(accounts.getByAccountIdentifierAsync(any())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + when(accounts.getByPhoneNumberIdentifierAsync(any())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + assertTrue(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(aci)).join().isPresent()); + assertTrue(accountsManager.getByServiceIdentifierAsync(new PniServiceIdentifier(pni)).join().isPresent()); + assertFalse(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(pni)).join().isPresent()); + assertFalse(accountsManager.getByServiceIdentifierAsync(new PniServiceIdentifier(aci)).join().isPresent()); + } + + @Test + void testGetAccountByNumberInCache() { + UUID uuid = UUID.randomUUID(); + + when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(uuid.toString()); + when(commands.get(eq("Account3::" + uuid))).thenReturn( + "{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}"); + + Optional account = accountsManager.getByE164("+14152222222"); + + assertTrue(account.isPresent()); + assertEquals(account.get().getNumber(), "+14152222222"); + assertEquals(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e"), account.get().getPhoneNumberIdentifier()); + + verify(commands, times(1)).get(eq("AccountMap::+14152222222")); + verify(commands, times(1)).get(eq("Account3::" + uuid)); + verifyNoMoreInteractions(commands); + + verifyNoInteractions(accounts); + } + + @Test + void testGetAccountByNumberAsyncInCache() { + UUID uuid = UUID.randomUUID(); + + when(asyncCommands.get(eq("AccountMap::+14152222222"))) + .thenReturn(MockRedisFuture.completedFuture(uuid.toString())); + + when(asyncCommands.get(eq("Account3::" + uuid))).thenReturn(MockRedisFuture.completedFuture( + "{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}")); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + Optional account = accountsManager.getByE164Async("+14152222222").join(); + + assertTrue(account.isPresent()); + assertEquals(account.get().getNumber(), "+14152222222"); + assertEquals(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e"), account.get().getPhoneNumberIdentifier()); + + verify(asyncCommands).get(eq("AccountMap::+14152222222")); + verify(asyncCommands).get(eq("Account3::" + uuid)); + verifyNoMoreInteractions(asyncCommands); + + verifyNoInteractions(accounts); + } + + @Test + void testGetAccountByUuidInCache() { + UUID uuid = UUID.randomUUID(); + + when(commands.get(eq("Account3::" + uuid))).thenReturn( + "{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}"); + + Optional account = accountsManager.getByAccountIdentifier(uuid); + + assertTrue(account.isPresent()); + assertEquals(account.get().getNumber(), "+14152222222"); + assertEquals(account.get().getUuid(), uuid); + assertEquals(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e"), account.get().getPhoneNumberIdentifier()); + + verify(commands, times(1)).get(eq("Account3::" + uuid)); + verifyNoMoreInteractions(commands); + + verifyNoInteractions(accounts); + } + + @Test + void testGetAccountByUuidInCacheAsync() { + UUID uuid = UUID.randomUUID(); + + when(asyncCommands.get(eq("Account3::" + uuid))).thenReturn(MockRedisFuture.completedFuture( + "{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}")); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + Optional account = accountsManager.getByAccountIdentifierAsync(uuid).join(); + + assertTrue(account.isPresent()); + assertEquals(account.get().getNumber(), "+14152222222"); + assertEquals(account.get().getUuid(), uuid); + assertEquals(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e"), account.get().getPhoneNumberIdentifier()); + + verify(asyncCommands, times(1)).get(eq("Account3::" + uuid)); + verifyNoMoreInteractions(asyncCommands); + + verifyNoInteractions(accounts); + } + + @Test + void testGetAccountByPniInCache() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + + when(commands.get(eq("AccountMap::" + pni))).thenReturn(uuid.toString()); + when(commands.get(eq("Account3::" + uuid))).thenReturn( + "{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}"); + + Optional account = accountsManager.getByPhoneNumberIdentifier(pni); + + assertTrue(account.isPresent()); + assertEquals(account.get().getNumber(), "+14152222222"); + assertEquals(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e"), account.get().getPhoneNumberIdentifier()); + + verify(commands).get(eq("AccountMap::" + pni)); + verify(commands).get(eq("Account3::" + uuid)); + verifyNoMoreInteractions(commands); + + verifyNoInteractions(accounts); + } + + @Test + void testGetAccountByPniInCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + + when(asyncCommands.get(eq("AccountMap::" + pni))) + .thenReturn(MockRedisFuture.completedFuture(uuid.toString())); + + when(asyncCommands.get(eq("Account3::" + uuid))).thenReturn(MockRedisFuture.completedFuture( + "{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}")); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + Optional account = accountsManager.getByPhoneNumberIdentifierAsync(pni).join(); + + assertTrue(account.isPresent()); + assertEquals(account.get().getNumber(), "+14152222222"); + assertEquals(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e"), account.get().getPhoneNumberIdentifier()); + + verify(asyncCommands).get(eq("AccountMap::" + pni)); + verify(asyncCommands).get(eq("Account3::" + uuid)); + verifyNoMoreInteractions(asyncCommands); + + verifyNoInteractions(accounts); + } + + @Test + void testGetAccountByUsernameHashInCache() { + UUID uuid = UUID.randomUUID(); + when(asyncCommands.get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1))) + .thenReturn(MockRedisFuture.completedFuture(uuid.toString())); + + when(asyncCommands.get(eq("Account3::" + uuid))).thenReturn(MockRedisFuture.completedFuture( + String.format("{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\", \"usernameHash\": \"%s\"}", + BASE_64_URL_USERNAME_HASH_1))); + + Optional account = accountsManager.getByUsernameHash(USERNAME_HASH_1).join(); + + assertTrue(account.isPresent()); + assertEquals(account.get().getNumber(), "+14152222222"); + assertEquals(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e"), account.get().getPhoneNumberIdentifier()); + assertArrayEquals(USERNAME_HASH_1, account.get().getUsernameHash().get()); + + verify(asyncCommands).get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1)); + verify(asyncCommands).get(eq("Account3::" + uuid)); + verifyNoMoreInteractions(asyncCommands); + + verifyNoInteractions(accounts); + } + + @Test + void testGetAccountByNumberNotInCache() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(null); + when(accounts.getByE164(eq("+14152222222"))).thenReturn(Optional.of(account)); + + Optional retrieved = accountsManager.getByE164("+14152222222"); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(commands, times(1)).get(eq("AccountMap::+14152222222")); + verify(commands, times(1)).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(commands, times(1)).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(commands, times(1)).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(commands); + + verify(accounts, times(1)).getByE164(eq("+14152222222")); + verifyNoMoreInteractions(accounts); + } + + @Test + void testGetAccountByNumberNotInCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(asyncCommands.get(eq("AccountMap::+14152222222"))).thenReturn(MockRedisFuture.completedFuture(null)); + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + when(accounts.getByE164Async(eq("+14152222222"))) + .thenReturn(MockRedisFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByE164Async("+14152222222").join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("AccountMap::+14152222222")); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByE164Async(eq("+14152222222")); + verifyNoMoreInteractions(accounts); + } + + @Test + void testGetAccountByUuidNotInCache() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(commands.get(eq("Account3::" + uuid))).thenReturn(null); + when(accounts.getByAccountIdentifier(eq(uuid))).thenReturn(Optional.of(account)); + + Optional retrieved = accountsManager.getByAccountIdentifier(uuid); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(commands, times(1)).get(eq("Account3::" + uuid)); + verify(commands, times(1)).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(commands, times(1)).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(commands, times(1)).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(commands); + + verify(accounts, times(1)).getByAccountIdentifier(eq(uuid)); + verifyNoMoreInteractions(accounts); + } + + @Test + void testGetAccountByUuidNotInCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(asyncCommands.get(eq("Account3::" + uuid))).thenReturn(MockRedisFuture.completedFuture(null)); + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + when(accounts.getByAccountIdentifierAsync(eq(uuid))) + .thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByAccountIdentifierAsync(uuid).join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("Account3::" + uuid)); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByAccountIdentifierAsync(eq(uuid)); + verifyNoMoreInteractions(accounts); + } + + @Test + void testGetAccountByPniNotInCache() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(commands.get(eq("AccountMap::" + pni))).thenReturn(null); + when(accounts.getByPhoneNumberIdentifier(pni)).thenReturn(Optional.of(account)); + + Optional retrieved = accountsManager.getByPhoneNumberIdentifier(pni); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(commands).get(eq("AccountMap::" + pni)); + verify(commands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(commands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(commands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(commands); + + verify(accounts).getByPhoneNumberIdentifier(pni); + verifyNoMoreInteractions(accounts); + } + + @Test + void testGetAccountByPniNotInCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(asyncCommands.get(eq("AccountMap::" + pni))).thenReturn(MockRedisFuture.completedFuture(null)); + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + when(accounts.getByPhoneNumberIdentifierAsync(pni)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByPhoneNumberIdentifierAsync(pni).join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("AccountMap::" + pni)); + verify(asyncCommands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByPhoneNumberIdentifierAsync(pni); + verifyNoMoreInteractions(accounts); + } + + @Test + void testGetAccountByUsernameHashNotInCache() { + UUID uuid = UUID.randomUUID(); + + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, UUID.randomUUID(), new ArrayList<>(), new byte[16]); + account.setUsernameHash(USERNAME_HASH_1); + + when(asyncCommands.get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1))) + .thenReturn(MockRedisFuture.completedFuture(null)); + + when(accounts.getByUsernameHash(USERNAME_HASH_1)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByUsernameHash(USERNAME_HASH_1).join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1)); + verify(asyncCommands).setex(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::" + account.getPhoneNumberIdentifier()), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByUsernameHash(USERNAME_HASH_1); + verifyNoMoreInteractions(accounts); + } + + @Test + void testGetAccountByNumberBrokenCache() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(commands.get(eq("AccountMap::+14152222222"))).thenThrow(new RedisException("Connection lost!")); + when(accounts.getByE164(eq("+14152222222"))).thenReturn(Optional.of(account)); + + Optional retrieved = accountsManager.getByE164("+14152222222"); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(commands, times(1)).get(eq("AccountMap::+14152222222")); + verify(commands, times(1)).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(commands, times(1)).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(commands, times(1)).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(commands); + + verify(accounts, times(1)).getByE164(eq("+14152222222")); + verifyNoMoreInteractions(accounts); + } + + @Test + void testGetAccountByNumberBrokenCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(asyncCommands.get(eq("AccountMap::+14152222222"))) + .thenReturn(MockRedisFuture.failedFuture(new RedisException("Connection lost!"))); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + when(accounts.getByE164Async(eq("+14152222222"))).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByE164Async("+14152222222").join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("AccountMap::+14152222222")); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByE164Async(eq("+14152222222")); + verifyNoMoreInteractions(accounts); + } + + @Test + void testGetAccountByUuidBrokenCache() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(commands.get(eq("Account3::" + uuid))).thenThrow(new RedisException("Connection lost!")); + when(accounts.getByAccountIdentifier(eq(uuid))).thenReturn(Optional.of(account)); + + Optional retrieved = accountsManager.getByAccountIdentifier(uuid); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(commands, times(1)).get(eq("Account3::" + uuid)); + verify(commands, times(1)).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(commands, times(1)).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(commands, times(1)).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(commands); + + verify(accounts, times(1)).getByAccountIdentifier(eq(uuid)); + verifyNoMoreInteractions(accounts); + } + + @Test + void testGetAccountByUuidBrokenCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(asyncCommands.get(eq("Account3::" + uuid))) + .thenReturn(MockRedisFuture.failedFuture(new RedisException("Connection lost!"))); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + when(accounts.getByAccountIdentifierAsync(eq(uuid))) + .thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByAccountIdentifierAsync(uuid).join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("Account3::" + uuid)); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByAccountIdentifierAsync(eq(uuid)); + verifyNoMoreInteractions(accounts); + } + + @Test + void testGetAccountByPniBrokenCache() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(commands.get(eq("AccountMap::" + pni))).thenThrow(new RedisException("OH NO")); + when(accounts.getByPhoneNumberIdentifier(pni)).thenReturn(Optional.of(account)); + + Optional retrieved = accountsManager.getByPhoneNumberIdentifier(pni); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(commands).get(eq("AccountMap::" + pni)); + verify(commands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(commands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(commands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(commands); + + verify(accounts).getByPhoneNumberIdentifier(pni); + verifyNoMoreInteractions(accounts); + } + + @Test + void testGetAccountByPniBrokenCacheAsync() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(asyncCommands.get(eq("AccountMap::" + pni))) + .thenReturn(MockRedisFuture.failedFuture(new RedisException("OH NO"))); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + when(accounts.getByPhoneNumberIdentifierAsync(pni)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByPhoneNumberIdentifierAsync(pni).join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("AccountMap::" + pni)); + verify(asyncCommands).setex(eq("AccountMap::" + pni), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByPhoneNumberIdentifierAsync(pni); + verifyNoMoreInteractions(accounts); + } + + @Test + void testGetAccountByUsernameBrokenCache() { + UUID uuid = UUID.randomUUID(); + + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, UUID.randomUUID(), new ArrayList<>(), new byte[16]); + account.setUsernameHash(USERNAME_HASH_1); + + when(asyncCommands.get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1))) + .thenReturn(MockRedisFuture.failedFuture(new RedisException("OH NO"))); + + when(accounts.getByUsernameHash(USERNAME_HASH_1)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + Optional retrieved = accountsManager.getByUsernameHash(USERNAME_HASH_1).join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), account); + + verify(asyncCommands).get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1)); + verify(asyncCommands).setex(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::" + account.getPhoneNumberIdentifier()), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString())); + verify(asyncCommands).setex(eq("Account3::" + uuid), anyLong(), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(accounts).getByUsernameHash(USERNAME_HASH_1); + verifyNoMoreInteractions(accounts); + } + + @Test + void testUpdate_optimisticLockingFailure() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(commands.get(eq("Account3::" + uuid))).thenReturn(null); + + when(accounts.getByAccountIdentifier(uuid)).thenReturn( + Optional.of(AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]))); + doThrow(ContestedOptimisticLockException.class) + .doAnswer(ACCOUNT_UPDATE_ANSWER) + .when(accounts).update(any()); + + final IdentityKey identityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + + account = accountsManager.update(account, a -> a.setIdentityKey(identityKey)); + + assertEquals(1, account.getVersion()); + assertEquals(identityKey, account.getIdentityKey(IdentityType.ACI)); + + verify(accounts, times(1)).getByAccountIdentifier(uuid); + verify(accounts, times(2)).update(any()); + verifyNoMoreInteractions(accounts); + } + + @Test + void testUpdateAsync_optimisticLockingFailure() { + UUID uuid = UUID.randomUUID(); + UUID pni = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16]); + + when(asyncCommands.get(eq("Account3::" + uuid))).thenReturn(null); + + when(accounts.getByAccountIdentifierAsync(uuid)).thenReturn(CompletableFuture.completedFuture( + Optional.of(AccountsHelper.generateTestAccount("+14152222222", uuid, pni, new ArrayList<>(), new byte[16])))); + + when(accounts.updateAsync(any())) + .thenReturn(CompletableFuture.failedFuture(new ContestedOptimisticLockException())) + .thenAnswer(ACCOUNT_UPDATE_ASYNC_ANSWER); + + final IdentityKey identityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + + account = accountsManager.updateAsync(account, a -> a.setIdentityKey(identityKey)).join(); + + assertEquals(1, account.getVersion()); + assertEquals(identityKey, account.getIdentityKey(IdentityType.ACI)); + + verify(accounts, times(1)).getByAccountIdentifierAsync(uuid); + verify(accounts, times(2)).updateAsync(any()); + verifyNoMoreInteractions(accounts); + } + + @Test + void testUpdate_dynamoOptimisticLockingFailureDuringCreate() { + UUID uuid = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, UUID.randomUUID(), new ArrayList<>(), new byte[16]); + + when(commands.get(eq("Account3::" + uuid))).thenReturn(null); + when(accounts.getByAccountIdentifier(uuid)).thenReturn(Optional.empty()) + .thenReturn(Optional.of(account)); + when(accounts.create(any())).thenThrow(ContestedOptimisticLockException.class); + + accountsManager.update(account, a -> { + }); + + verify(accounts, times(1)).update(account); + verifyNoMoreInteractions(accounts); + } + + @Test + void testUpdateDevice() { + final UUID uuid = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, UUID.randomUUID(), new ArrayList<>(), new byte[16]); + + when(accounts.getByAccountIdentifier(uuid)).thenReturn( + Optional.of(AccountsHelper.generateTestAccount("+14152222222", uuid, UUID.randomUUID(), new ArrayList<>(), new byte[16]))); + + assertTrue(account.getDevices().isEmpty()); + + Device enabledDevice = new Device(); + enabledDevice.setFetchesMessages(true); + enabledDevice.setSignedPreKey(KeysHelper.signedECPreKey(1, Curve.generateKeyPair())); + enabledDevice.setLastSeen(System.currentTimeMillis()); + final long deviceId = account.getNextDeviceId(); + enabledDevice.setId(deviceId); + account.addDevice(enabledDevice); + + @SuppressWarnings("unchecked") Consumer deviceUpdater = mock(Consumer.class); + @SuppressWarnings("unchecked") Consumer unknownDeviceUpdater = mock(Consumer.class); + + account = accountsManager.updateDevice(account, deviceId, deviceUpdater); + account = accountsManager.updateDevice(account, deviceId, d -> d.setName("deviceName")); + + assertEquals("deviceName", account.getDevice(deviceId).orElseThrow().getName()); + + verify(deviceUpdater, times(1)).accept(any(Device.class)); + + accountsManager.updateDevice(account, account.getNextDeviceId(), unknownDeviceUpdater); + + verify(unknownDeviceUpdater, never()).accept(any(Device.class)); + } + + @Test + void testUpdateDeviceAsync() { + final UUID uuid = UUID.randomUUID(); + Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, UUID.randomUUID(), new ArrayList<>(), new byte[16]); + + when(accounts.getByAccountIdentifierAsync(uuid)).thenReturn(CompletableFuture.completedFuture( + Optional.of(AccountsHelper.generateTestAccount("+14152222222", uuid, UUID.randomUUID(), new ArrayList<>(), new byte[16])))); + + assertTrue(account.getDevices().isEmpty()); + + Device enabledDevice = new Device(); + enabledDevice.setFetchesMessages(true); + enabledDevice.setSignedPreKey(KeysHelper.signedECPreKey(1, Curve.generateKeyPair())); + enabledDevice.setLastSeen(System.currentTimeMillis()); + final long deviceId = account.getNextDeviceId(); + enabledDevice.setId(deviceId); + account.addDevice(enabledDevice); + + @SuppressWarnings("unchecked") Consumer deviceUpdater = mock(Consumer.class); + @SuppressWarnings("unchecked") Consumer unknownDeviceUpdater = mock(Consumer.class); + + account = accountsManager.updateDeviceAsync(account, deviceId, deviceUpdater).join(); + account = accountsManager.updateDeviceAsync(account, deviceId, d -> d.setName("deviceName")).join(); + + assertEquals("deviceName", account.getDevice(deviceId).orElseThrow().getName()); + + verify(deviceUpdater, times(1)).accept(any(Device.class)); + + accountsManager.updateDeviceAsync(account, account.getNextDeviceId(), unknownDeviceUpdater).join(); + + verify(unknownDeviceUpdater, never()).accept(any(Device.class)); + } + + @Test + void testCreateFreshAccount() throws InterruptedException { + when(accounts.create(any())).thenReturn(true); + + final String e164 = "+18005550123"; + final AccountAttributes attributes = new AccountAttributes(false, 0, null, null, true, null); + accountsManager.create(e164, "password", null, attributes, new ArrayList<>()); + + verify(accounts).create(argThat(account -> e164.equals(account.getNumber()))); + verifyNoInteractions(keysManager); + verifyNoInteractions(messagesManager); + verifyNoInteractions(profilesManager); + } + + @Test + void testReregisterAccount() throws InterruptedException { + final UUID existingUuid = UUID.randomUUID(); + + when(accounts.create(any())).thenAnswer(invocation -> { + invocation.getArgument(0, Account.class).setUuid(existingUuid); + return false; + }); + + final String e164 = "+18005550123"; + final AccountAttributes attributes = new AccountAttributes(false, 0, null, null, true, null); + accountsManager.create(e164, "password", null, attributes, new ArrayList<>()); + + assertTrue(phoneNumberIdentifiersByE164.containsKey(e164)); + + verify(accounts) + .create(argThat(account -> e164.equals(account.getNumber()) && existingUuid.equals(account.getUuid()))); + + verify(keysManager).delete(existingUuid); + verify(keysManager).delete(phoneNumberIdentifiersByE164.get(e164)); + verify(messagesManager).clear(existingUuid); + verify(profilesManager).deleteAll(existingUuid); + verify(clientPresenceManager).disconnectAllPresencesForUuid(existingUuid); + } + + @Test + void testCreateAccountRecentlyDeleted() throws InterruptedException { + final UUID recentlyDeletedUuid = UUID.randomUUID(); + + when(accounts.findRecentlyDeletedAccountIdentifier(anyString())).thenReturn(Optional.of(recentlyDeletedUuid)); + when(accounts.create(any())).thenReturn(true); + + final String e164 = "+18005550123"; + final AccountAttributes attributes = new AccountAttributes(false, 0, null, null, true, null); + accountsManager.create(e164, "password", null, attributes, new ArrayList<>()); + + verify(accounts).create( + argThat(account -> e164.equals(account.getNumber()) && recentlyDeletedUuid.equals(account.getUuid()))); + verifyNoInteractions(keysManager); + verifyNoInteractions(messagesManager); + verifyNoInteractions(profilesManager); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testCreateWithDiscoverability(final boolean discoverable) throws InterruptedException { + final AccountAttributes attributes = new AccountAttributes(false, 0, null, null, discoverable, null); + final Account account = accountsManager.create("+18005550123", "password", null, attributes, new ArrayList<>()); + + assertEquals(discoverable, account.isDiscoverableByPhoneNumber()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testCreateWithStorageCapability(final boolean hasStorage) throws InterruptedException { + final AccountAttributes attributes = new AccountAttributes(false, 0, null, null, true, + new DeviceCapabilities(hasStorage, false, false, false)); + + final Account account = accountsManager.create("+18005550123", "password", null, attributes, new ArrayList<>()); + + assertEquals(hasStorage, account.isStorageSupported()); + } + + @ParameterizedTest + @MethodSource + void testUpdateDeviceLastSeen(final boolean expectUpdate, final long initialLastSeen, final long updatedLastSeen) { + final Account account = AccountsHelper.generateTestAccount("+14152222222", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]); + final Device device = generateTestDevice(initialLastSeen); + account.addDevice(device); + + accountsManager.updateDeviceLastSeen(account, device, updatedLastSeen); + + assertEquals(expectUpdate ? updatedLastSeen : initialLastSeen, device.getLastSeen()); + verify(accounts, expectUpdate ? times(1) : never()).update(account); + } + + @SuppressWarnings("unused") + private static Stream testUpdateDeviceLastSeen() { + return Stream.of( + Arguments.of(true, 1, 2), + Arguments.of(false, 1, 1), + Arguments.of(false, 2, 1) + ); + } + + @Test + void testChangePhoneNumber() throws InterruptedException, MismatchedDevicesException { + final String originalNumber = "+14152222222"; + final String targetNumber = "+14153333333"; + final UUID uuid = UUID.randomUUID(); + final UUID originalPni = UUID.randomUUID(); + + Account account = AccountsHelper.generateTestAccount(originalNumber, uuid, originalPni, new ArrayList<>(), new byte[16]); + account = accountsManager.changeNumber(account, targetNumber, null, null, null, null); + + assertEquals(targetNumber, account.getNumber()); + + assertTrue(phoneNumberIdentifiersByE164.containsKey(targetNumber)); + + verify(keysManager).delete(originalPni); + verify(keysManager).delete(phoneNumberIdentifiersByE164.get(targetNumber)); + } + + @Test + void testChangePhoneNumberSameNumber() throws InterruptedException, MismatchedDevicesException { + final String number = "+14152222222"; + + Account account = AccountsHelper.generateTestAccount(number, UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]); + account = accountsManager.changeNumber(account, number, null, null, null, null); + + assertEquals(number, account.getNumber()); + verify(keysManager, never()).delete(any()); + } + + @Test + void testChangePhoneNumberSameNumberWithPniData() { + final String number = "+14152222222"; + + Account account = AccountsHelper.generateTestAccount(number, UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + assertThrows(IllegalArgumentException.class, + () -> accountsManager.changeNumber( + account, number, new IdentityKey(Curve.generateKeyPair().getPublicKey()), Map.of(1L, KeysHelper.signedECPreKey(1, pniIdentityKeyPair)), null, Map.of(1L, 101)), + "AccountsManager should not allow use of changeNumber with new PNI keys but without changing number"); + + verify(accounts, never()).update(any()); + verifyNoInteractions(keysManager); + } + + @Test + void testChangePhoneNumberExistingAccount() throws InterruptedException, MismatchedDevicesException { + final String originalNumber = "+14152222222"; + final String targetNumber = "+14153333333"; + final UUID existingAccountUuid = UUID.randomUUID(); + final UUID uuid = UUID.randomUUID(); + final UUID originalPni = UUID.randomUUID(); + final UUID targetPni = UUID.randomUUID(); + + final Account existingAccount = AccountsHelper.generateTestAccount(targetNumber, existingAccountUuid, targetPni, new ArrayList<>(), new byte[16]); + when(accounts.getByE164(targetNumber)).thenReturn(Optional.of(existingAccount)); + + Account account = AccountsHelper.generateTestAccount(originalNumber, uuid, originalPni, new ArrayList<>(), new byte[16]); + account = accountsManager.changeNumber(account, targetNumber, null, null, null, null); + + assertEquals(targetNumber, account.getNumber()); + + assertTrue(phoneNumberIdentifiersByE164.containsKey(targetNumber)); + final UUID newPni = phoneNumberIdentifiersByE164.get(targetNumber); + + verify(keysManager).delete(existingAccountUuid); + verify(keysManager).delete(originalPni); + verify(keysManager, atLeastOnce()).delete(targetPni); + verify(keysManager).delete(newPni); + verify(keysManager).storeEcSignedPreKeys(eq(newPni), any()); + verifyNoMoreInteractions(keysManager); + } + + @Test + void testChangePhoneNumberWithPqKeysExistingAccount() throws InterruptedException, MismatchedDevicesException { + final String originalNumber = "+14152222222"; + final String targetNumber = "+14153333333"; + final UUID existingAccountUuid = UUID.randomUUID(); + final UUID uuid = UUID.randomUUID(); + final UUID originalPni = UUID.randomUUID(); + final UUID targetPni = UUID.randomUUID(); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final Map newSignedKeys = Map.of( + 1L, KeysHelper.signedECPreKey(1, identityKeyPair), + 2L, KeysHelper.signedECPreKey(2, identityKeyPair)); + final Map newSignedPqKeys = Map.of( + 1L, KeysHelper.signedKEMPreKey(3, identityKeyPair), + 2L, KeysHelper.signedKEMPreKey(4, identityKeyPair)); + final Map newRegistrationIds = Map.of(1L, 201, 2L, 202); + + final Account existingAccount = AccountsHelper.generateTestAccount(targetNumber, existingAccountUuid, targetPni, new ArrayList<>(), new byte[16]); + when(accounts.getByE164(targetNumber)).thenReturn(Optional.of(existingAccount)); + when(keysManager.getPqEnabledDevices(uuid)).thenReturn(CompletableFuture.completedFuture(List.of(1L))); + + final List devices = List.of(DevicesHelper.createDevice(1L, 0L, 101), DevicesHelper.createDevice(2L, 0L, 102)); + final Account account = AccountsHelper.generateTestAccount(originalNumber, uuid, originalPni, devices, new byte[16]); + final Account updatedAccount = accountsManager.changeNumber( + account, targetNumber, new IdentityKey(Curve.generateKeyPair().getPublicKey()), newSignedKeys, newSignedPqKeys, newRegistrationIds); + + assertEquals(targetNumber, updatedAccount.getNumber()); + + assertTrue(phoneNumberIdentifiersByE164.containsKey(targetNumber)); + + final UUID newPni = phoneNumberIdentifiersByE164.get(targetNumber); + verify(keysManager).delete(existingAccountUuid); + verify(keysManager, atLeastOnce()).delete(targetPni); + verify(keysManager).delete(newPni); + verify(keysManager).delete(originalPni); + verify(keysManager).getPqEnabledDevices(uuid); + verify(keysManager).storeEcSignedPreKeys(newPni, newSignedKeys); + verify(keysManager).storePqLastResort(eq(newPni), eq(Map.of(1L, newSignedPqKeys.get(1L)))); + verifyNoMoreInteractions(keysManager); + } + + + @Test + void testChangePhoneNumberWithMismatchedPqKeys() throws InterruptedException, MismatchedDevicesException { + final String originalNumber = "+14152222222"; + final String targetNumber = "+14153333333"; + final UUID existingAccountUuid = UUID.randomUUID(); + final UUID uuid = UUID.randomUUID(); + final UUID originalPni = UUID.randomUUID(); + final UUID targetPni = UUID.randomUUID(); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final Map newSignedKeys = Map.of( + 1L, KeysHelper.signedECPreKey(1, identityKeyPair), + 2L, KeysHelper.signedECPreKey(2, identityKeyPair)); + final Map newSignedPqKeys = Map.of( + 1L, KeysHelper.signedKEMPreKey(3, identityKeyPair)); + final Map newRegistrationIds = Map.of(1L, 201, 2L, 202); + + final Account existingAccount = AccountsHelper.generateTestAccount(targetNumber, existingAccountUuid, targetPni, new ArrayList<>(), new byte[16]); + when(accounts.getByE164(targetNumber)).thenReturn(Optional.of(existingAccount)); + when(keysManager.getPqEnabledDevices(uuid)).thenReturn(CompletableFuture.completedFuture(List.of(1L))); + + final List devices = List.of(DevicesHelper.createDevice(1L, 0L, 101), DevicesHelper.createDevice(2L, 0L, 102)); + final Account account = AccountsHelper.generateTestAccount(originalNumber, uuid, originalPni, devices, new byte[16]); + assertThrows(MismatchedDevicesException.class, + () -> accountsManager.changeNumber( + account, targetNumber, new IdentityKey(Curve.generateKeyPair().getPublicKey()), newSignedKeys, newSignedPqKeys, newRegistrationIds)); + + verifyNoInteractions(accounts); + verifyNoInteractions(keysManager); + } + + @Test + void testChangePhoneNumberViaUpdate() { + final String originalNumber = "+14152222222"; + final String targetNumber = "+14153333333"; + final UUID uuid = UUID.randomUUID(); + + final Account account = AccountsHelper.generateTestAccount(originalNumber, uuid, UUID.randomUUID(), new ArrayList<>(), new byte[16]); + + assertThrows(AssertionError.class, () -> accountsManager.update(account, a -> a.setNumber(targetNumber, UUID.randomUUID()))); + } + + @Test + void testPniUpdate() throws MismatchedDevicesException { + final String number = "+14152222222"; + + List devices = List.of(DevicesHelper.createDevice(1L, 0L, 101), DevicesHelper.createDevice(2L, 0L, 102)); + Account account = AccountsHelper.generateTestAccount(number, UUID.randomUUID(), UUID.randomUUID(), devices, new byte[16]); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + Map newSignedKeys = Map.of( + 1L, KeysHelper.signedECPreKey(1, identityKeyPair), + 2L, KeysHelper.signedECPreKey(2, identityKeyPair)); + Map newRegistrationIds = Map.of(1L, 201, 2L, 202); + + UUID oldUuid = account.getUuid(); + UUID oldPni = account.getPhoneNumberIdentifier(); + Map oldSignedPreKeys = account.getDevices().stream() + .collect(Collectors.toMap(Device::getId, d -> d.getSignedPreKey(IdentityType.ACI))); + + final IdentityKey pniIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + + when(keysManager.getPqEnabledDevices(any())).thenReturn(CompletableFuture.completedFuture(Collections.emptyList())); + when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + final Account updatedAccount = accountsManager.updatePniKeys(account, pniIdentityKey, newSignedKeys, null, newRegistrationIds); + + // non-PNI stuff should not change + assertEquals(oldUuid, updatedAccount.getUuid()); + assertEquals(number, updatedAccount.getNumber()); + assertEquals(oldPni, updatedAccount.getPhoneNumberIdentifier()); + assertNull(updatedAccount.getIdentityKey(IdentityType.ACI)); + assertEquals(oldSignedPreKeys, updatedAccount.getDevices().stream() + .collect(Collectors.toMap(Device::getId, d -> d.getSignedPreKey(IdentityType.ACI)))); + assertEquals(Map.of(1L, 101, 2L, 102), + updatedAccount.getDevices().stream().collect(Collectors.toMap(Device::getId, Device::getRegistrationId))); + + // PNI stuff should + assertEquals(pniIdentityKey, updatedAccount.getIdentityKey(IdentityType.PNI)); + assertEquals(newSignedKeys, + updatedAccount.getDevices().stream() + .collect(Collectors.toMap(Device::getId, d -> d.getSignedPreKey(IdentityType.PNI)))); + assertEquals(newRegistrationIds, + updatedAccount.getDevices().stream().collect(Collectors.toMap(Device::getId, d -> d.getPhoneNumberIdentityRegistrationId().getAsInt()))); + + verify(accounts).update(any()); + + verify(keysManager).delete(oldPni); + } + + @Test + void testPniPqUpdate() throws MismatchedDevicesException { + final String number = "+14152222222"; + + List devices = List.of(DevicesHelper.createDevice(1L, 0L, 101), DevicesHelper.createDevice(2L, 0L, 102)); + Account account = AccountsHelper.generateTestAccount(number, UUID.randomUUID(), UUID.randomUUID(), devices, new byte[16]); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final Map newSignedKeys = Map.of( + 1L, KeysHelper.signedECPreKey(1, identityKeyPair), + 2L, KeysHelper.signedECPreKey(2, identityKeyPair)); + final Map newSignedPqKeys = Map.of( + 1L, KeysHelper.signedKEMPreKey(3, identityKeyPair), + 2L, KeysHelper.signedKEMPreKey(4, identityKeyPair)); + Map newRegistrationIds = Map.of(1L, 201, 2L, 202); + + UUID oldUuid = account.getUuid(); + UUID oldPni = account.getPhoneNumberIdentifier(); + + when(keysManager.getPqEnabledDevices(oldPni)).thenReturn(CompletableFuture.completedFuture(List.of(1L))); + when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + + Map oldSignedPreKeys = account.getDevices().stream() + .collect(Collectors.toMap(Device::getId, d -> d.getSignedPreKey(IdentityType.ACI))); + + final IdentityKey pniIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + + final Account updatedAccount = + accountsManager.updatePniKeys(account, pniIdentityKey, newSignedKeys, newSignedPqKeys, newRegistrationIds); + + // non-PNI-keys stuff should not change + assertEquals(oldUuid, updatedAccount.getUuid()); + assertEquals(number, updatedAccount.getNumber()); + assertEquals(oldPni, updatedAccount.getPhoneNumberIdentifier()); + assertNull(updatedAccount.getIdentityKey(IdentityType.ACI)); + assertEquals(oldSignedPreKeys, updatedAccount.getDevices().stream() + .collect(Collectors.toMap(Device::getId, d -> d.getSignedPreKey(IdentityType.ACI)))); + assertEquals(Map.of(1L, 101, 2L, 102), + updatedAccount.getDevices().stream().collect(Collectors.toMap(Device::getId, Device::getRegistrationId))); + + // PNI keys should + assertEquals(pniIdentityKey, updatedAccount.getIdentityKey(IdentityType.PNI)); + assertEquals(newSignedKeys, + updatedAccount.getDevices().stream() + .collect(Collectors.toMap(Device::getId, d -> d.getSignedPreKey(IdentityType.PNI)))); + assertEquals(newRegistrationIds, + updatedAccount.getDevices().stream().collect(Collectors.toMap(Device::getId, d -> d.getPhoneNumberIdentityRegistrationId().getAsInt()))); + + verify(accounts).update(any()); + + verify(keysManager).delete(oldPni); + verify(keysManager).storeEcSignedPreKeys(oldPni, newSignedKeys); + + // only the pq key for the already-pq-enabled device should be saved + verify(keysManager).storePqLastResort(eq(oldPni), eq(Map.of(1L, newSignedPqKeys.get(1L)))); + } + + @Test + void testPniNonPqToPqUpdate() throws MismatchedDevicesException { + final String number = "+14152222222"; + + List devices = List.of(DevicesHelper.createDevice(1L, 0L, 101), DevicesHelper.createDevice(2L, 0L, 102)); + Account account = AccountsHelper.generateTestAccount(number, UUID.randomUUID(), UUID.randomUUID(), devices, new byte[16]); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final Map newSignedKeys = Map.of( + 1L, KeysHelper.signedECPreKey(1, identityKeyPair), + 2L, KeysHelper.signedECPreKey(2, identityKeyPair)); + final Map newSignedPqKeys = Map.of( + 1L, KeysHelper.signedKEMPreKey(3, identityKeyPair), + 2L, KeysHelper.signedKEMPreKey(4, identityKeyPair)); + Map newRegistrationIds = Map.of(1L, 201, 2L, 202); + + UUID oldUuid = account.getUuid(); + UUID oldPni = account.getPhoneNumberIdentifier(); + + when(keysManager.getPqEnabledDevices(oldPni)).thenReturn(CompletableFuture.completedFuture(List.of())); + when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + when(keysManager.storePqLastResort(any(), any())).thenAnswer( + invocation -> { + assertFalse(invocation.getArgument(1, Map.class).isEmpty()); + return CompletableFuture.completedFuture(null); + }); + + Map oldSignedPreKeys = account.getDevices().stream() + .collect(Collectors.toMap(Device::getId, d -> d.getSignedPreKey(IdentityType.ACI))); + + final IdentityKey pniIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + + final Account updatedAccount = + accountsManager.updatePniKeys(account, pniIdentityKey, newSignedKeys, newSignedPqKeys, newRegistrationIds); + + // non-PNI-keys stuff should not change + assertEquals(oldUuid, updatedAccount.getUuid()); + assertEquals(number, updatedAccount.getNumber()); + assertEquals(oldPni, updatedAccount.getPhoneNumberIdentifier()); + assertNull(updatedAccount.getIdentityKey(IdentityType.ACI)); + assertEquals(oldSignedPreKeys, updatedAccount.getDevices().stream() + .collect(Collectors.toMap(Device::getId, d -> d.getSignedPreKey(IdentityType.ACI)))); + assertEquals(Map.of(1L, 101, 2L, 102), + updatedAccount.getDevices().stream().collect(Collectors.toMap(Device::getId, Device::getRegistrationId))); + + // PNI keys should + assertEquals(pniIdentityKey, updatedAccount.getIdentityKey(IdentityType.PNI)); + assertEquals(newSignedKeys, + updatedAccount.getDevices().stream() + .collect(Collectors.toMap(Device::getId, d -> d.getSignedPreKey(IdentityType.PNI)))); + assertEquals(newRegistrationIds, + updatedAccount.getDevices().stream().collect(Collectors.toMap(Device::getId, d -> d.getPhoneNumberIdentityRegistrationId().getAsInt()))); + + verify(accounts).update(any()); + + verify(keysManager).delete(oldPni); + verify(keysManager).storeEcSignedPreKeys(oldPni, newSignedKeys); + + // no pq-enabled devices -> no pq last resort keys should be stored + verify(keysManager, never()).storePqLastResort(any(), any()); + } + + @Test + void testPniUpdate_incompleteKeys() { + final String number = "+14152222222"; + + List devices = List.of(DevicesHelper.createDevice(1L, 0L, 101), DevicesHelper.createDevice(2L, 0L, 102)); + Account account = AccountsHelper.generateTestAccount(number, UUID.randomUUID(), UUID.randomUUID(), devices, new byte[16]); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final Map newSignedKeys = Map.of( + 2L, KeysHelper.signedECPreKey(1, identityKeyPair), + 3L, KeysHelper.signedECPreKey(2, identityKeyPair)); + Map newRegistrationIds = Map.of(1L, 201, 2L, 202); + + UUID oldUuid = account.getUuid(); + UUID oldPni = account.getPhoneNumberIdentifier(); + + Map oldSignedPreKeys = account.getDevices().stream() + .collect(Collectors.toMap(Device::getId, d -> d.getSignedPreKey(IdentityType.ACI))); + + final IdentityKey pniIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + + assertThrows(MismatchedDevicesException.class, + () -> accountsManager.updatePniKeys(account, pniIdentityKey, newSignedKeys, null, newRegistrationIds)); + + verifyNoInteractions(accounts); + verifyNoInteractions(keysManager); + } + + @Test + void testPniPqUpdate_incompleteKeys() { + final String number = "+14152222222"; + + List devices = List.of(DevicesHelper.createDevice(1L, 0L, 101), DevicesHelper.createDevice(2L, 0L, 102)); + Account account = AccountsHelper.generateTestAccount(number, UUID.randomUUID(), UUID.randomUUID(), devices, new byte[16]); + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + final Map newSignedKeys = Map.of( + 1L, KeysHelper.signedECPreKey(1, identityKeyPair), + 2L, KeysHelper.signedECPreKey(2, identityKeyPair)); + final Map newSignedPqKeys = Map.of( + 1L, KeysHelper.signedKEMPreKey(3, identityKeyPair)); + Map newRegistrationIds = Map.of(1L, 201, 2L, 202); + + UUID oldUuid = account.getUuid(); + UUID oldPni = account.getPhoneNumberIdentifier(); + + Map oldSignedPreKeys = account.getDevices().stream() + .collect(Collectors.toMap(Device::getId, d -> d.getSignedPreKey(IdentityType.ACI))); + + final IdentityKey pniIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + + assertThrows(MismatchedDevicesException.class, + () -> accountsManager.updatePniKeys(account, pniIdentityKey, newSignedKeys, newSignedPqKeys, newRegistrationIds)); + + verifyNoInteractions(accounts); + verifyNoInteractions(keysManager); + } + + @Test + void testReserveUsernameHash() throws UsernameHashNotAvailableException { + final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]); + final List usernameHashes = List.of(new byte[32], new byte[32]); + when(accounts.usernameHashAvailable(any())).thenReturn(CompletableFuture.completedFuture(true)); + when(accounts.reserveUsernameHash(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null)); + accountsManager.reserveUsernameHash(account, usernameHashes); + verify(accounts).reserveUsernameHash(eq(account), eq(new byte[32]), eq(Duration.ofMinutes(5))); + } + + @Test + void testReserveUsernameHashNotAvailable() { + final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]); + when(accounts.usernameHashAvailable(any())).thenReturn(CompletableFuture.completedFuture(false)); + + CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class, + accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1, USERNAME_HASH_2))); + } + + @Test + void testReserveUsernameDisabled() { + final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]); + when(enrollmentManager.isEnrolled(account.getUuid(), AccountsManager.USERNAME_EXPERIMENT_NAME)).thenReturn(false); + CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class, + accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1))); + } + + @Test + void testConfirmReservedUsernameHash() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException { + final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]); + setReservationHash(account, USERNAME_HASH_1); + when(accounts.usernameHashAvailable(Optional.of(account.getUuid()), USERNAME_HASH_1)) + .thenReturn(CompletableFuture.completedFuture(true)); + + when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)) + .thenReturn(CompletableFuture.completedFuture(null)); + + accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join(); + verify(accounts).confirmUsernameHash(eq(account), eq(USERNAME_HASH_1), eq(ENCRYPTED_USERNAME_1)); + } + + @Test + void testConfirmReservedHashNameMismatch() { + final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]); + setReservationHash(account, USERNAME_HASH_1); + when(accounts.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1))) + .thenReturn(CompletableFuture.completedFuture(true)); + CompletableFutureTestUtil.assertFailsWithCause(UsernameReservationNotFoundException.class, + accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_2)); + } + + @Test + void testConfirmReservedLapsed() { + final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]); + // hash was reserved, but the reservation lapsed and another account took it + setReservationHash(account, USERNAME_HASH_1); + when(accounts.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1))) + .thenReturn(CompletableFuture.completedFuture(false)); + CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class, + accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)); + verify(accounts, never()).confirmUsernameHash(any(), any(), any()); + } + + @Test + void testConfirmReservedRetry() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException { + final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]); + account.setUsernameHash(USERNAME_HASH_1); + + // reserved username already set, should be treated as a replay + accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join(); + verifyNoInteractions(accounts); + } + + @Test + void testConfirmReservedUsernameHashWithNoReservation() { + final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), + new ArrayList<>(), new byte[16]); + CompletableFutureTestUtil.assertFailsWithCause(UsernameReservationNotFoundException.class, + accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)); + verify(accounts, never()).confirmUsernameHash(any(), any(), any()); + } + + @Test + void testClearUsernameHash() { + when(accounts.clearUsernameHash(any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]); + account.setUsernameHash(USERNAME_HASH_1); + accountsManager.clearUsernameHash(account).join(); + verify(accounts).clearUsernameHash(eq(account)); + } + + @Test + void testSetUsernameViaUpdate() { + final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]); + + assertThrows(AssertionError.class, () -> accountsManager.update(account, a -> a.setUsernameHash(USERNAME_HASH_1))); + } + + @Test + void testJsonRoundTripSerialization() throws Exception { + String originalJson; + try (InputStream inputStream = getClass().getResourceAsStream( + "AccountsManagerTest-testJsonRoundTripSerialization.json")) { + Objects.requireNonNull(inputStream); + originalJson = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } + + final Account originalAccount = AccountsManager.parseAccountJson(originalJson, + UUID.fromString("111111-1111-1111-1111-111111111111")).orElseThrow(); + + final String serialized = AccountsManager.writeRedisAccountJson(originalAccount); + final Account parsedAccount = AccountsManager.parseAccountJson(serialized, originalAccount.getUuid()).orElseThrow(); + + assertEquals(originalAccount.getUuid(), parsedAccount.getUuid()); + assertEquals(originalAccount.getPhoneNumberIdentifier(), parsedAccount.getPhoneNumberIdentifier()); + assertEquals(originalAccount.getIdentityKey(IdentityType.ACI), parsedAccount.getIdentityKey(IdentityType.ACI)); + assertEquals(originalAccount.getIdentityKey(IdentityType.PNI), parsedAccount.getIdentityKey(IdentityType.PNI)); + assertEquals(originalAccount.getNumber(), parsedAccount.getNumber()); + assertArrayEquals(originalAccount.getUnidentifiedAccessKey().orElseThrow(), + parsedAccount.getUnidentifiedAccessKey().orElseThrow()); + assertEquals(originalAccount.isDiscoverableByPhoneNumber(), parsedAccount.isDiscoverableByPhoneNumber()); + assertEquals(originalAccount.isUnrestrictedUnidentifiedAccess(), parsedAccount.isUnrestrictedUnidentifiedAccess()); + + assertEquals(originalAccount.getDevices().size(), parsedAccount.getDevices().size()); + + final Device originalDevice = originalAccount.getMasterDevice().orElseThrow(); + final Device parsedDevice = parsedAccount.getMasterDevice().orElseThrow(); + + assertEquals(originalDevice.getId(), parsedDevice.getId()); + assertEquals(originalDevice.getSignedPreKey(IdentityType.ACI), parsedDevice.getSignedPreKey(IdentityType.ACI)); + assertEquals(originalDevice.getSignedPreKey(IdentityType.PNI), parsedDevice.getSignedPreKey(IdentityType.PNI)); + assertEquals(originalDevice.getRegistrationId(), parsedDevice.getRegistrationId()); + assertEquals(originalDevice.getPhoneNumberIdentityRegistrationId(), + parsedDevice.getPhoneNumberIdentityRegistrationId()); + assertEquals(originalDevice.getCapabilities(), parsedDevice.getCapabilities()); + assertEquals(originalDevice.getFetchesMessages(), parsedDevice.getFetchesMessages()); + } + + private void setReservationHash(final Account account, final byte[] reservedUsernameHash) { + account.setReservedUsernameHash(reservedUsernameHash); + } + + private static Device generateTestDevice(final long lastSeen) { + final Device device = new Device(); + device.setId(Device.MASTER_ID); + device.setFetchesMessages(true); + device.setSignedPreKey(KeysHelper.signedECPreKey(1, Curve.generateKeyPair())); + device.setLastSeen(lastSeen); + + return device; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerUsernameIntegrationTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerUsernameIntegrationTest.java new file mode 100644 index 000000000..87efd662b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerUsernameIntegrationTest.java @@ -0,0 +1,302 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.security.SecureRandom; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; +import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; +import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; + +class AccountsManagerUsernameIntegrationTest { + + private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE"; + private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc"; + private static final String BASE_64_URL_ENCRYPTED_USERNAME_1 = "md1votbj9r794DsqTNrBqA"; + private static final String BASE_64_URL_ENCRYPTED_USERNAME_2 = "9hrqVLy59bzgPse-S9NUsA"; + private static final int SCAN_PAGE_SIZE = 1; + private static final byte[] USERNAME_HASH_1 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_1); + private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2); + private static final byte[] ENCRYPTED_USERNAME_1 = Base64.getUrlDecoder().decode(BASE_64_URL_ENCRYPTED_USERNAME_1); + private static final byte[] ENCRYPTED_USERNAME_2 = Base64.getUrlDecoder().decode(BASE_64_URL_ENCRYPTED_USERNAME_2); + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension( + Tables.ACCOUNTS, + Tables.NUMBERS, + Tables.USERNAMES, + Tables.DELETED_ACCOUNTS, + Tables.PNI, + Tables.PNI_ASSIGNMENTS); + + @RegisterExtension + static RedisClusterExtension CACHE_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + private AccountsManager accountsManager; + private Accounts accounts; + + @BeforeEach + void setup() throws InterruptedException { + buildAccountsManager(1, 2, 10); + } + + private void buildAccountsManager(final int initialWidth, int discriminatorMaxWidth, int attemptsPerWidth) + throws InterruptedException { + @SuppressWarnings("unchecked") final DynamicConfigurationManager dynamicConfigurationManager = + mock(DynamicConfigurationManager.class); + + DynamicConfiguration dynamicConfiguration = new DynamicConfiguration(); + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + + accounts = Mockito.spy(new Accounts( + DYNAMO_DB_EXTENSION.getDynamoDbClient(), + DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + Tables.ACCOUNTS.tableName(), + Tables.NUMBERS.tableName(), + Tables.PNI_ASSIGNMENTS.tableName(), + Tables.USERNAMES.tableName(), + Tables.DELETED_ACCOUNTS.tableName(), + SCAN_PAGE_SIZE)); + + final AccountLockManager accountLockManager = mock(AccountLockManager.class); + + doAnswer(invocation -> { + final Runnable task = invocation.getArgument(1); + task.run(); + + return null; + }).when(accountLockManager).withLock(any(), any()); + + when(accountLockManager.withLockAsync(any(), any(), any())).thenAnswer(invocation -> { + final Supplier> taskSupplier = invocation.getArgument(1); + taskSupplier.get().join(); + + return CompletableFuture.completedFuture(null); + }); + + final PhoneNumberIdentifiers phoneNumberIdentifiers = + new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbClient(), Tables.PNI.tableName()); + + final ExperimentEnrollmentManager experimentEnrollmentManager = mock(ExperimentEnrollmentManager.class); + when(experimentEnrollmentManager.isEnrolled(any(UUID.class), eq(AccountsManager.USERNAME_EXPERIMENT_NAME))) + .thenReturn(true); + accountsManager = new AccountsManager( + accounts, + phoneNumberIdentifiers, + CACHE_CLUSTER_EXTENSION.getRedisCluster(), + accountLockManager, + mock(KeysManager.class), + mock(MessagesManager.class), + mock(ProfilesManager.class), + mock(SecureStorageClient.class), + mock(SecureValueRecovery2Client.class), + mock(ClientPresenceManager.class), + experimentEnrollmentManager, + mock(RegistrationRecoveryPasswordsManager.class), + mock(Executor.class), + mock(Clock.class)); + } + + @Test + void testNoUsernames() throws InterruptedException { + Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(), + new ArrayList<>()); + List usernameHashes = List.of(USERNAME_HASH_1, USERNAME_HASH_2); + int i = 0; + for (byte[] hash : usernameHashes) { + final Map item = new HashMap<>(Map.of( + Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(UUID.randomUUID()), + Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(hash))); + // half of these are taken usernames, half are only reservations (have a TTL) + if (i % 2 == 0) { + item.put(Accounts.ATTR_TTL, + AttributeValues.fromLong(Instant.now().plus(Duration.ofMinutes(1)).getEpochSecond())); + } + i++; + DYNAMO_DB_EXTENSION.getDynamoDbClient().putItem(PutItemRequest.builder() + .tableName(Tables.USERNAMES.tableName()) + .item(item) + .build()); + } + + CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class, + accountsManager.reserveUsernameHash(account, usernameHashes)); + + assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsernameHash()).isEmpty(); + } + + @Test + void testReserveUsernameSnatched() throws InterruptedException, UsernameHashNotAvailableException { + final Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(), + new ArrayList<>()); + ArrayList usernameHashes = new ArrayList<>(Arrays.asList(USERNAME_HASH_1, USERNAME_HASH_2)); + for (byte[] hash : usernameHashes) { + DYNAMO_DB_EXTENSION.getDynamoDbClient().putItem(PutItemRequest.builder() + .tableName(Tables.USERNAMES.tableName()) + .item(Map.of( + Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(UUID.randomUUID()), + Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(hash))) + .build()); + } + + + byte[] availableHash = new byte[32]; + new SecureRandom().nextBytes(availableHash); + usernameHashes.add(availableHash); + + // first time this is called lie and say the username is available + // this simulates seeing an available username and then it being taken + // by someone before the write + doReturn(CompletableFuture.completedFuture(true)) + .doCallRealMethod() + .when(accounts).usernameHashAvailable(any()); + final byte[] username = accountsManager + .reserveUsernameHash(account, usernameHashes) + .join() + .reservedUsernameHash(); + + assertArrayEquals(username, availableHash); + + // 1 attempt on first try (returns true), + // 5 more attempts until "availableHash" returns true + verify(accounts, times(4)).usernameHashAvailable(any()); + } + + @Test + public void testReserveConfirmClear() + throws InterruptedException, UsernameHashNotAvailableException, UsernameReservationNotFoundException { + Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(), + new ArrayList<>()); + + // reserve + AccountsManager.UsernameReservation reservation = + accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1)).join(); + + assertArrayEquals(reservation.account().getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1); + assertThat(accountsManager.getByUsernameHash(reservation.reservedUsernameHash()).join()).isEmpty(); + + // confirm + account = accountsManager.confirmReservedUsernameHash( + reservation.account(), + reservation.reservedUsernameHash(), + ENCRYPTED_USERNAME_1).join(); + assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1); + assertThat(accountsManager.getByUsernameHash(USERNAME_HASH_1).join().orElseThrow().getUuid()).isEqualTo( + account.getUuid()); + assertThat(account.getUsernameLinkHandle()).isNotNull(); + assertThat(accountsManager.getByUsernameLinkHandle(account.getUsernameLinkHandle()).join().orElseThrow().getUuid()) + .isEqualTo(account.getUuid()); + + // clear + account = accountsManager.clearUsernameHash(account).join(); + assertThat(accountsManager.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty(); + assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsernameHash()).isEmpty(); + } + + @Test + public void testReservationLapsed() + throws InterruptedException, UsernameHashNotAvailableException, UsernameReservationNotFoundException { + + final Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(), + new ArrayList<>()); + + AccountsManager.UsernameReservation reservation1 = + accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1)).join(); + + long past = Instant.now().minus(Duration.ofMinutes(1)).getEpochSecond(); + // force expiration + DYNAMO_DB_EXTENSION.getDynamoDbClient().updateItem(UpdateItemRequest.builder() + .tableName(Tables.USERNAMES.tableName()) + .key(Map.of(Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(USERNAME_HASH_1))) + .updateExpression("SET #ttl = :ttl") + .expressionAttributeNames(Map.of("#ttl", Accounts.ATTR_TTL)) + .expressionAttributeValues(Map.of(":ttl", AttributeValues.fromLong(past))) + .build()); + + // a different account should be able to reserve it + Account account2 = accountsManager.create("+18005552222", "password", null, new AccountAttributes(), + new ArrayList<>()); + final AccountsManager.UsernameReservation reservation2 = + accountsManager.reserveUsernameHash(account2, List.of(USERNAME_HASH_1)).join(); + assertArrayEquals(reservation2.reservedUsernameHash(), USERNAME_HASH_1); + + CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class, + accountsManager.confirmReservedUsernameHash(reservation1.account(), USERNAME_HASH_1, ENCRYPTED_USERNAME_1)); + account2 = accountsManager.confirmReservedUsernameHash(reservation2.account(), USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join(); + assertEquals(accountsManager.getByUsernameHash(USERNAME_HASH_1).join().orElseThrow().getUuid(), account2.getUuid()); + assertArrayEquals(account2.getUsernameHash().orElseThrow(), USERNAME_HASH_1); + } + + @Test + void testUsernameSetReserveAnotherClearSetReserved() throws InterruptedException { + Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(), + new ArrayList<>()); + + // Set username hash + final AccountsManager.UsernameReservation reservation1 = + accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1)).join(); + + account = accountsManager.confirmReservedUsernameHash(reservation1.account(), USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join(); + + // Reserve another hash on the same account + final AccountsManager.UsernameReservation reservation2 = + accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_2)).join(); + + account = reservation2.account(); + + assertArrayEquals(account.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_2); + assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1); + assertArrayEquals(account.getEncryptedUsername().orElseThrow(), ENCRYPTED_USERNAME_1); + + // Clear the set username hash but not the reserved one + account = accountsManager.clearUsernameHash(account).join(); + assertThat(account.getReservedUsernameHash()).isPresent(); + assertThat(account.getUsernameHash()).isEmpty(); + + // Confirm second reservation + account = accountsManager.confirmReservedUsernameHash(account, reservation2.reservedUsernameHash(), ENCRYPTED_USERNAME_2).join(); + assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_2); + assertArrayEquals(account.getEncryptedUsername().orElseThrow(), ENCRYPTED_USERNAME_2); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java new file mode 100644 index 000000000..7d8c0833f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java @@ -0,0 +1,1193 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.uuid.UUIDComparator; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.lang3.RandomUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; +import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.tests.util.DevicesHelper; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.TestClock; +import reactor.core.scheduler.Schedulers; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.Put; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure; +import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem; +import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; +import software.amazon.awssdk.services.dynamodb.model.TransactionConflictException; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; + +@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) +class AccountsTest { + + private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE"; + private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc"; + private static final String BASE_64_URL_ENCRYPTED_USERNAME_1 = "md1votbj9r794DsqTNrBqA"; + private static final String BASE_64_URL_ENCRYPTED_USERNAME_2 = "9hrqVLy59bzgPse-S9NUsA"; + private static final byte[] USERNAME_HASH_1 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_1); + private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2); + private static final byte[] ENCRYPTED_USERNAME_1 = Base64.getUrlDecoder().decode(BASE_64_URL_ENCRYPTED_USERNAME_1); + private static final byte[] ENCRYPTED_USERNAME_2 = Base64.getUrlDecoder().decode(BASE_64_URL_ENCRYPTED_USERNAME_2); + + private static final int SCAN_PAGE_SIZE = 1; + + private static final AtomicInteger ACCOUNT_COUNTER = new AtomicInteger(1); + + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension( + Tables.ACCOUNTS, + Tables.NUMBERS, + Tables.PNI_ASSIGNMENTS, + Tables.USERNAMES, + Tables.DELETED_ACCOUNTS); + + private final TestClock clock = TestClock.pinned(Instant.EPOCH); + private DynamicConfigurationManager mockDynamicConfigManager; + private Accounts accounts; + + @BeforeEach + void setupAccountsDao() { + + @SuppressWarnings("unchecked") DynamicConfigurationManager m = mock(DynamicConfigurationManager.class); + mockDynamicConfigManager = m; + + when(mockDynamicConfigManager.getConfiguration()) + .thenReturn(new DynamicConfiguration()); + + this.accounts = new Accounts( + clock, + DYNAMO_DB_EXTENSION.getDynamoDbClient(), + DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + Tables.ACCOUNTS.tableName(), + Tables.NUMBERS.tableName(), + Tables.PNI_ASSIGNMENTS.tableName(), + Tables.USERNAMES.tableName(), + Tables.DELETED_ACCOUNTS.tableName(), + SCAN_PAGE_SIZE); + } + + @Test + public void testStoreAndLookupUsernameLink() throws Exception { + final Account account = nextRandomAccount(); + account.setUsernameHash(RandomUtils.nextBytes(16)); + accounts.create(account); + + final BiConsumer, byte[]> validator = (maybeAccount, expectedEncryptedUsername) -> { + assertTrue(maybeAccount.isPresent()); + assertTrue(maybeAccount.get().getEncryptedUsername().isPresent()); + assertEquals(account.getUuid(), maybeAccount.get().getUuid()); + assertArrayEquals(expectedEncryptedUsername, maybeAccount.get().getEncryptedUsername().get()); + }; + + // creating a username link, storing it, checking that it can be looked up + final UUID linkHandle1 = UUID.randomUUID(); + final byte[] encruptedUsername1 = RandomUtils.nextBytes(32); + account.setUsernameLinkDetails(linkHandle1, encruptedUsername1); + accounts.update(account); + validator.accept(accounts.getByUsernameLinkHandle(linkHandle1).join(), encruptedUsername1); + + // updating username link, storing new one, checking that it can be looked up, checking that old one can't be looked up + final UUID linkHandle2 = UUID.randomUUID(); + final byte[] encruptedUsername2 = RandomUtils.nextBytes(32); + account.setUsernameLinkDetails(linkHandle2, encruptedUsername2); + accounts.update(account); + validator.accept(accounts.getByUsernameLinkHandle(linkHandle2).join(), encruptedUsername2); + assertTrue(accounts.getByUsernameLinkHandle(linkHandle1).join().isEmpty()); + + // deleting username link, checking it can't be looked up by either handle + account.setUsernameLinkDetails(null, null); + accounts.update(account); + assertTrue(accounts.getByUsernameLinkHandle(linkHandle1).join().isEmpty()); + assertTrue(accounts.getByUsernameLinkHandle(linkHandle2).join().isEmpty()); + } + + @Test + @Disabled + // TODO: @Sergey: what's the story with this test? + public void testUsernameLinksViaAccountsManager() { + final AccountsManager accountsManager = new AccountsManager( + accounts, + mock(PhoneNumberIdentifiers.class), + mock(FaultTolerantRedisCluster.class), + mock(AccountLockManager.class), + mock(KeysManager.class), + mock(MessagesManager.class), + mock(ProfilesManager.class), + mock(SecureStorageClient.class), + mock(SecureValueRecovery2Client.class), + mock(ClientPresenceManager.class), + mock(ExperimentEnrollmentManager.class), + mock(RegistrationRecoveryPasswordsManager.class), + mock(Executor.class), + mock(Clock.class)); + + final Account account = nextRandomAccount(); + account.setUsernameHash(RandomUtils.nextBytes(16)); + accounts.create(account); + + final UUID linkHandle = UUID.randomUUID(); + final byte[] encryptedUsername = RandomUtils.nextBytes(32); + accountsManager.update(account, a -> a.setUsernameLinkDetails(linkHandle, encryptedUsername)); + + final Optional maybeAccount = accountsManager.getByUsernameLinkHandle(linkHandle).join(); + assertTrue(maybeAccount.isPresent()); + assertTrue(maybeAccount.get().getEncryptedUsername().isPresent()); + assertArrayEquals(encryptedUsername, maybeAccount.get().getEncryptedUsername().get()); + + // making some unrelated change and updating account to check that username link data is still there + final Optional accountToChange = accountsManager.getByAccountIdentifier(account.getUuid()); + assertTrue(accountToChange.isPresent()); + accountsManager.update(accountToChange.get(), a -> a.setDiscoverableByPhoneNumber(!a.isDiscoverableByPhoneNumber())); + final Optional accountAfterChange = accountsManager.getByUsernameLinkHandle(linkHandle).join(); + assertTrue(accountAfterChange.isPresent()); + assertTrue(accountAfterChange.get().getEncryptedUsername().isPresent()); + assertArrayEquals(encryptedUsername, accountAfterChange.get().getEncryptedUsername().get()); + + // now deleting the link + final Optional accountToDeleteLink = accountsManager.getByAccountIdentifier(account.getUuid()); + accountsManager.update(accountToDeleteLink.get(), a -> a.setUsernameLinkDetails(null, null)); + assertTrue(accounts.getByUsernameLinkHandle(linkHandle).join().isEmpty()); + } + + @Test + void testStore() { + Device device = generateDevice(1); + Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), List.of(device)); + + boolean freshUser = accounts.create(account); + + assertThat(freshUser).isTrue(); + verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), null, account, true); + + assertPhoneNumberConstraintExists("+14151112222", account.getUuid()); + assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid()); + + freshUser = accounts.create(account); + assertThat(freshUser).isTrue(); + verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), null, account, true); + + assertPhoneNumberConstraintExists("+14151112222", account.getUuid()); + assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid()); + } + + @Test + void testStoreRecentlyDeleted() { + final UUID originalUuid = UUID.randomUUID(); + + Device device = generateDevice(1); + Account account = generateAccount("+14151112222", originalUuid, UUID.randomUUID(), List.of(device)); + + boolean freshUser = accounts.create(account); + + assertThat(freshUser).isTrue(); + verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), null, account, true); + + assertPhoneNumberConstraintExists("+14151112222", account.getUuid()); + assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid()); + + accounts.delete(originalUuid).join(); + assertThat(accounts.findRecentlyDeletedAccountIdentifier(account.getNumber())).hasValue(originalUuid); + + freshUser = accounts.create(account); + assertThat(freshUser).isTrue(); + verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), null, account, true); + + assertPhoneNumberConstraintExists("+14151112222", account.getUuid()); + assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid()); + + assertThat(accounts.findRecentlyDeletedAccountIdentifier(account.getNumber())).isEmpty(); + } + + @Test + void testStoreMulti() { + final List devices = List.of(generateDevice(1), generateDevice(2)); + final Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), devices); + + accounts.create(account); + + verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), null, account, true); + + assertPhoneNumberConstraintExists("+14151112222", account.getUuid()); + assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid()); + } + + @Test + void testRetrieve() { + final List devicesFirst = List.of(generateDevice(1), generateDevice(2)); + + UUID uuidFirst = UUID.randomUUID(); + UUID pniFirst = UUID.randomUUID(); + Account accountFirst = generateAccount("+14151112222", uuidFirst, pniFirst, devicesFirst); + + final List devicesSecond = List.of(generateDevice(1), generateDevice(2)); + + UUID uuidSecond = UUID.randomUUID(); + UUID pniSecond = UUID.randomUUID(); + Account accountSecond = generateAccount("+14152221111", uuidSecond, pniSecond, devicesSecond); + + accounts.create(accountFirst); + accounts.create(accountSecond); + + Optional retrievedFirst = accounts.getByE164("+14151112222"); + Optional retrievedSecond = accounts.getByE164("+14152221111"); + + assertThat(retrievedFirst.isPresent()).isTrue(); + assertThat(retrievedSecond.isPresent()).isTrue(); + + verifyStoredState("+14151112222", uuidFirst, pniFirst, null, retrievedFirst.get(), accountFirst); + verifyStoredState("+14152221111", uuidSecond, pniSecond, null, retrievedSecond.get(), accountSecond); + + retrievedFirst = accounts.getByAccountIdentifier(uuidFirst); + retrievedSecond = accounts.getByAccountIdentifier(uuidSecond); + + assertThat(retrievedFirst.isPresent()).isTrue(); + assertThat(retrievedSecond.isPresent()).isTrue(); + + verifyStoredState("+14151112222", uuidFirst, pniFirst, null, retrievedFirst.get(), accountFirst); + verifyStoredState("+14152221111", uuidSecond, pniSecond, null, retrievedSecond.get(), accountSecond); + + retrievedFirst = accounts.getByPhoneNumberIdentifier(pniFirst); + retrievedSecond = accounts.getByPhoneNumberIdentifier(pniSecond); + + assertThat(retrievedFirst.isPresent()).isTrue(); + assertThat(retrievedSecond.isPresent()).isTrue(); + + verifyStoredState("+14151112222", uuidFirst, pniFirst, null, retrievedFirst.get(), accountFirst); + verifyStoredState("+14152221111", uuidSecond, pniSecond, null, retrievedSecond.get(), accountSecond); + } + + @Test + void testRetrieveNoPni() throws JsonProcessingException { + final List devices = List.of(generateDevice(1), generateDevice(2)); + final UUID uuid = UUID.randomUUID(); + final Account account = generateAccount("+14151112222", uuid, null, devices); + + // Accounts#create enforces that newly-created accounts have a PNI, so we need to make a bit of an end-run around it + // to simulate an existing account with no PNI. + { + final TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder() + .put( + Put.builder() + .tableName(Tables.NUMBERS.tableName()) + .item(Map.of( + Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()), + Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()))) + .conditionExpression( + "attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)") + .expressionAttributeNames( + Map.of("#uuid", Accounts.KEY_ACCOUNT_UUID, + "#number", Accounts.ATTR_ACCOUNT_E164)) + .expressionAttributeValues( + Map.of(":uuid", AttributeValues.fromUUID(account.getUuid()))) + .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD) + .build()) + .build(); + + final TransactWriteItem accountPut = TransactWriteItem.builder() + .put(Put.builder() + .tableName(Tables.ACCOUNTS.tableName()) + .conditionExpression("attribute_not_exists(#number) OR #number = :number") + .expressionAttributeNames(Map.of("#number", Accounts.ATTR_ACCOUNT_E164)) + .expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber()))) + .item(Map.of( + Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid), + Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()), + Accounts.ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.jsonMapper().writeValueAsBytes(account)), + Accounts.ATTR_VERSION, AttributeValues.fromInt(account.getVersion()), + Accounts.ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory()))) + .build()) + .build(); + + DYNAMO_DB_EXTENSION.getDynamoDbClient().transactWriteItems(TransactWriteItemsRequest.builder() + .transactItems(phoneNumberConstraintPut, accountPut) + .build()); + } + + Optional retrieved = accounts.getByE164("+14151112222"); + + assertThat(retrieved.isPresent()).isTrue(); + verifyStoredState("+14151112222", uuid, null, null, retrieved.get(), account); + + retrieved = accounts.getByAccountIdentifier(uuid); + + assertThat(retrieved.isPresent()).isTrue(); + verifyStoredState("+14151112222", uuid, null, null, retrieved.get(), account); + } + + @Test + void testOverwrite() { + Device device = generateDevice(1); + UUID firstUuid = UUID.randomUUID(); + UUID firstPni = UUID.randomUUID(); + Account account = generateAccount("+14151112222", firstUuid, firstPni, List.of(device)); + + accounts.create(account); + + final SecureRandom byteGenerator = new SecureRandom(); + final byte[] usernameHash = new byte[32]; + byteGenerator.nextBytes(usernameHash); + final byte[] encryptedUsername = new byte[16]; + byteGenerator.nextBytes(encryptedUsername); + + // Set up the existing account to have a username hash + accounts.confirmUsernameHash(account, usernameHash, encryptedUsername).join(); + + verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), usernameHash, account, true); + + assertPhoneNumberConstraintExists("+14151112222", firstUuid); + assertPhoneNumberIdentifierConstraintExists(firstPni, firstUuid); + + accounts.update(account); + + UUID secondUuid = UUID.randomUUID(); + + device = generateDevice(1); + account = generateAccount("+14151112222", secondUuid, UUID.randomUUID(), List.of(device)); + + final boolean freshUser = accounts.create(account); + assertThat(freshUser).isFalse(); + verifyStoredState("+14151112222", firstUuid, firstPni, usernameHash, account, true); + + assertPhoneNumberConstraintExists("+14151112222", firstUuid); + assertPhoneNumberIdentifierConstraintExists(firstPni, firstUuid); + + device = generateDevice(1); + Account invalidAccount = generateAccount("+14151113333", firstUuid, UUID.randomUUID(), List.of(device)); + + assertThatThrownBy(() -> accounts.create(invalidAccount)); + } + + @Test + void testUpdate() { + Device device = generateDevice (1 ); + Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), List.of(device)); + + accounts.create(account); + + assertPhoneNumberConstraintExists("+14151112222", account.getUuid()); + assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid()); + + device.setName("foobar"); + + accounts.update(account); + + assertPhoneNumberConstraintExists("+14151112222", account.getUuid()); + assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier(), account.getUuid()); + + Optional retrieved = accounts.getByE164("+14151112222"); + + assertThat(retrieved.isPresent()).isTrue(); + verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), null, retrieved.get(), account); + + retrieved = accounts.getByAccountIdentifier(account.getUuid()); + + assertThat(retrieved.isPresent()).isTrue(); + verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), null, account, true); + + device = generateDevice(1); + Account unknownAccount = generateAccount("+14151113333", UUID.randomUUID(), UUID.randomUUID(), List.of(device)); + + assertThatThrownBy(() -> accounts.update(unknownAccount)).isInstanceOfAny(ConditionalCheckFailedException.class); + + accounts.update(account); + + assertThat(account.getVersion()).isEqualTo(2); + + verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), null, account, true); + + account.setVersion(1); + + assertThatThrownBy(() -> accounts.update(account)).isInstanceOfAny(ContestedOptimisticLockException.class); + + account.setVersion(2); + + accounts.update(account); + + verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), null, account, true); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testUpdateWithMockTransactionConflictException(boolean wrapException) { + + final DynamoDbAsyncClient dynamoDbAsyncClient = mock(DynamoDbAsyncClient.class); + accounts = new Accounts(mock(DynamoDbClient.class), + dynamoDbAsyncClient, Tables.ACCOUNTS.tableName(), + Tables.NUMBERS.tableName(), Tables.PNI_ASSIGNMENTS.tableName(), Tables.USERNAMES.tableName(), + Tables.DELETED_ACCOUNTS.tableName(), SCAN_PAGE_SIZE); + + Exception e = TransactionConflictException.builder().build(); + e = wrapException ? new CompletionException(e) : e; + + when(dynamoDbAsyncClient.updateItem(any(UpdateItemRequest.class))) + .thenReturn(CompletableFuture.failedFuture(e)); + + Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID()); + + assertThatThrownBy(() -> accounts.update(account)).isInstanceOfAny(ContestedOptimisticLockException.class); + } + + @Test + void testRetrieveFrom() { + List users = new ArrayList<>(); + + for (int i = 1; i <= 100; i++) { + Account account = generateAccount("+1" + String.format("%03d", i), UUID.randomUUID(), UUID.randomUUID()); + users.add(account); + accounts.create(account); + } + + users.sort((account, t1) -> UUIDComparator.staticCompare(account.getUuid(), t1.getUuid())); + + AccountCrawlChunk retrieved = accounts.getAllFromStart(10); + assertThat(retrieved.getAccounts().size()).isEqualTo(10); + + for (int i = 0; i < retrieved.getAccounts().size(); i++) { + final Account retrievedAccount = retrieved.getAccounts().get(i); + + final Account expectedAccount = users.stream() + .filter(account -> account.getUuid().equals(retrievedAccount.getUuid())) + .findAny() + .orElseThrow(); + + verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), expectedAccount.getPhoneNumberIdentifier(), null, retrievedAccount, expectedAccount); + + users.remove(expectedAccount); + } + + for (int j = 0; j < 9; j++) { + retrieved = accounts.getAllFrom(retrieved.getLastUuid().orElseThrow(), 10); + assertThat(retrieved.getAccounts().size()).isEqualTo(10); + + for (int i = 0; i < retrieved.getAccounts().size(); i++) { + final Account retrievedAccount = retrieved.getAccounts().get(i); + + final Account expectedAccount = users.stream() + .filter(account -> account.getUuid().equals(retrievedAccount.getUuid())) + .findAny() + .orElseThrow(); + + verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), expectedAccount.getPhoneNumberIdentifier(), null, retrievedAccount, expectedAccount); + + users.remove(expectedAccount); + } + } + + assertThat(users).isEmpty(); + } + + @Test + void testGetAll() { + final List expectedAccounts = new ArrayList<>(); + + for (int i = 1; i <= 100; i++) { + final Account account = generateAccount("+1" + String.format("%03d", i), UUID.randomUUID(), UUID.randomUUID()); + expectedAccounts.add(account); + accounts.create(account); + } + + final List retrievedAccounts = + accounts.getAll(2, Schedulers.parallel()).sequential().collectList().block(); + + assertNotNull(retrievedAccounts); + assertEquals(expectedAccounts.stream().map(Account::getUuid).collect(Collectors.toSet()), + retrievedAccounts.stream().map(Account::getUuid).collect(Collectors.toSet())); + } + + @Test + void testDelete() { + final Device deletedDevice = generateDevice(1); + final Account deletedAccount = generateAccount("+14151112222", UUID.randomUUID(), + UUID.randomUUID(), List.of(deletedDevice)); + final Device retainedDevice = generateDevice(1); + final Account retainedAccount = generateAccount("+14151112345", UUID.randomUUID(), + UUID.randomUUID(), List.of(retainedDevice)); + + accounts.create(deletedAccount); + accounts.create(retainedAccount); + + assertThat(accounts.findRecentlyDeletedAccountIdentifier(deletedAccount.getNumber())).isEmpty(); + + assertPhoneNumberConstraintExists("+14151112222", deletedAccount.getUuid()); + assertPhoneNumberIdentifierConstraintExists(deletedAccount.getPhoneNumberIdentifier(), deletedAccount.getUuid()); + assertPhoneNumberConstraintExists("+14151112345", retainedAccount.getUuid()); + assertPhoneNumberIdentifierConstraintExists(retainedAccount.getPhoneNumberIdentifier(), retainedAccount.getUuid()); + + assertThat(accounts.getByAccountIdentifier(deletedAccount.getUuid())).isPresent(); + assertThat(accounts.getByAccountIdentifier(retainedAccount.getUuid())).isPresent(); + + accounts.delete(deletedAccount.getUuid()).join(); + + assertThat(accounts.getByAccountIdentifier(deletedAccount.getUuid())).isNotPresent(); + assertThat(accounts.findRecentlyDeletedAccountIdentifier(deletedAccount.getNumber())).hasValue(deletedAccount.getUuid()); + + assertPhoneNumberConstraintDoesNotExist(deletedAccount.getNumber()); + assertPhoneNumberIdentifierConstraintDoesNotExist(deletedAccount.getPhoneNumberIdentifier()); + + verifyStoredState(retainedAccount.getNumber(), retainedAccount.getUuid(), retainedAccount.getPhoneNumberIdentifier(), + null, accounts.getByAccountIdentifier(retainedAccount.getUuid()).get(), retainedAccount); + + { + final Account recreatedAccount = generateAccount(deletedAccount.getNumber(), UUID.randomUUID(), + UUID.randomUUID(), List.of(generateDevice(1))); + + final boolean freshUser = accounts.create(recreatedAccount); + + assertThat(freshUser).isTrue(); + assertThat(accounts.getByAccountIdentifier(recreatedAccount.getUuid())).isPresent(); + verifyStoredState(recreatedAccount.getNumber(), recreatedAccount.getUuid(), recreatedAccount.getPhoneNumberIdentifier(), + null, accounts.getByAccountIdentifier(recreatedAccount.getUuid()).get(), recreatedAccount); + + assertPhoneNumberConstraintExists(recreatedAccount.getNumber(), recreatedAccount.getUuid()); + assertPhoneNumberIdentifierConstraintExists(recreatedAccount.getPhoneNumberIdentifier(), recreatedAccount.getUuid()); + } + } + + @Test + void testMissing() { + Device device = generateDevice (1 ); + Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), List.of(device)); + + accounts.create(account); + + Optional retrieved = accounts.getByE164("+11111111"); + assertThat(retrieved.isPresent()).isFalse(); + + retrieved = accounts.getByAccountIdentifier(UUID.randomUUID()); + assertThat(retrieved.isPresent()).isFalse(); + } + + @Test + void getByAccountIdentifierAsync() { + assertThat(accounts.getByAccountIdentifierAsync(UUID.randomUUID()).join()).isEmpty(); + + final Account account = + generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), List.of(generateDevice(1))); + + accounts.create(account); + + assertThat(accounts.getByAccountIdentifierAsync(account.getUuid()).join()).isPresent(); + } + + @Test + void getByPhoneNumberIdentifierAsync() { + assertThat(accounts.getByPhoneNumberIdentifierAsync(UUID.randomUUID()).join()).isEmpty(); + + final Account account = + generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), List.of(generateDevice(1))); + + accounts.create(account); + + assertThat(accounts.getByPhoneNumberIdentifierAsync(account.getPhoneNumberIdentifier()).join()).isPresent(); + } + + @Test + void getByE164Async() { + final String e164 = "+14151112222"; + + assertThat(accounts.getByE164Async(e164).join()).isEmpty(); + + final Account account = + generateAccount(e164, UUID.randomUUID(), UUID.randomUUID(), List.of(generateDevice(1))); + + accounts.create(account); + + assertThat(accounts.getByE164Async(e164).join()).isPresent(); + } + + @Test + void testCanonicallyDiscoverableSet() { + Device device = generateDevice(1); + Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), List.of(device)); + account.setDiscoverableByPhoneNumber(false); + accounts.create(account); + verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), null, account, false); + account.setDiscoverableByPhoneNumber(true); + accounts.update(account); + verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), null, account, true); + account.setDiscoverableByPhoneNumber(false); + accounts.update(account); + verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier(), null, account, false); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + @ParameterizedTest + @MethodSource + public void testChangeNumber(final Optional maybeDisplacedAccountIdentifier) { + final String originalNumber = "+14151112222"; + final String targetNumber = "+14151113333"; + + final UUID originalPni = UUID.randomUUID(); + final UUID targetPni = UUID.randomUUID(); + + final Device device = generateDevice(1); + final Account account = generateAccount(originalNumber, UUID.randomUUID(), originalPni, List.of(device)); + + accounts.create(account); + + assertThat(accounts.getByPhoneNumberIdentifier(originalPni)).isPresent(); + + assertPhoneNumberConstraintExists(originalNumber, account.getUuid()); + assertPhoneNumberIdentifierConstraintExists(originalPni, account.getUuid()); + + { + final Optional retrieved = accounts.getByE164(originalNumber); + assertThat(retrieved).isPresent(); + + verifyStoredState(originalNumber, account.getUuid(), account.getPhoneNumberIdentifier(), null, retrieved.get(), account); + } + + accounts.changeNumber(account, targetNumber, targetPni, maybeDisplacedAccountIdentifier); + + assertThat(accounts.getByE164(originalNumber)).isEmpty(); + assertThat(accounts.getByAccountIdentifier(originalPni)).isEmpty(); + + assertPhoneNumberConstraintDoesNotExist(originalNumber); + assertPhoneNumberIdentifierConstraintDoesNotExist(originalPni); + assertPhoneNumberConstraintExists(targetNumber, account.getUuid()); + assertPhoneNumberIdentifierConstraintExists(targetPni, account.getUuid()); + + { + final Optional retrieved = accounts.getByE164(targetNumber); + assertThat(retrieved).isPresent(); + + verifyStoredState(targetNumber, account.getUuid(), account.getPhoneNumberIdentifier(), null, retrieved.get(), account); + + assertThat(retrieved.get().getPhoneNumberIdentifier()).isEqualTo(targetPni); + assertThat(accounts.getByPhoneNumberIdentifier(targetPni)).isPresent(); + } + + assertThat(accounts.findRecentlyDeletedAccountIdentifier(originalNumber)).isEqualTo(maybeDisplacedAccountIdentifier); + } + + private static Stream testChangeNumber() { + return Stream.of( + Arguments.of(Optional.empty()), + Arguments.of(Optional.of(UUID.randomUUID())) + ); + } + + @Test + public void testChangeNumberConflict() { + final String originalNumber = "+14151112222"; + final String targetNumber = "+14151113333"; + + final UUID originalPni = UUID.randomUUID(); + final UUID targetPni = UUID.randomUUID(); + + final Device existingDevice = generateDevice(1); + final Account existingAccount = generateAccount(targetNumber, UUID.randomUUID(), targetPni, List.of(existingDevice)); + + final Device device = generateDevice(1); + final Account account = generateAccount(originalNumber, UUID.randomUUID(), originalPni, List.of(device)); + + accounts.create(account); + accounts.create(existingAccount); + + assertThrows(TransactionCanceledException.class, () -> accounts.changeNumber(account, targetNumber, targetPni, Optional.of(existingAccount.getUuid()))); + + assertPhoneNumberConstraintExists(originalNumber, account.getUuid()); + assertPhoneNumberIdentifierConstraintExists(originalPni, account.getUuid()); + assertPhoneNumberConstraintExists(targetNumber, existingAccount.getUuid()); + assertPhoneNumberIdentifierConstraintExists(targetPni, existingAccount.getUuid()); + } + + @Test + public void testChangeNumberPhoneNumberIdentifierConflict() { + final String originalNumber = "+14151112222"; + final String targetNumber = "+14151113333"; + + final Device device = generateDevice(1); + final Account account = generateAccount(originalNumber, UUID.randomUUID(), UUID.randomUUID(), List.of(device)); + + accounts.create(account); + + final UUID existingAccountIdentifier = UUID.randomUUID(); + final UUID existingPhoneNumberIdentifier = UUID.randomUUID(); + + // Artificially inject a conflicting PNI entry + DYNAMO_DB_EXTENSION.getDynamoDbClient().putItem(PutItemRequest.builder() + .tableName(Tables.PNI_ASSIGNMENTS.tableName()) + .item(Map.of( + Accounts.ATTR_PNI_UUID, AttributeValues.fromUUID(existingPhoneNumberIdentifier), + Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(existingAccountIdentifier))) + .conditionExpression( + "attribute_not_exists(#pni) OR (attribute_exists(#pni) AND #uuid = :uuid)") + .expressionAttributeNames( + Map.of("#uuid", Accounts.KEY_ACCOUNT_UUID, + "#pni", Accounts.ATTR_PNI_UUID)) + .expressionAttributeValues( + Map.of(":uuid", AttributeValues.fromUUID(existingAccountIdentifier))) + .build()); + + assertThrows(TransactionCanceledException.class, () -> accounts.changeNumber(account, targetNumber, existingPhoneNumberIdentifier, Optional.empty())); + } + + @Test + void testSwitchUsernameHashes() { + final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account); + + assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty(); + + accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join(); + accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join(); + final UUID oldHandle = account.getUsernameLinkHandle(); + + { + final Optional maybeAccount = accounts.getByUsernameHash(USERNAME_HASH_1).join(); + verifyStoredState(account.getNumber(), account.getUuid(), account.getPhoneNumberIdentifier(), USERNAME_HASH_1, maybeAccount.orElseThrow(), account); + + final Optional maybeAccount2 = accounts.getByUsernameLinkHandle(oldHandle).join(); + verifyStoredState(account.getNumber(), account.getUuid(), account.getPhoneNumberIdentifier(), USERNAME_HASH_1, maybeAccount2.orElseThrow(), account); + } + + accounts.reserveUsernameHash(account, USERNAME_HASH_2, Duration.ofDays(1)).join(); + accounts.confirmUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_2).join(); + final UUID newHandle = account.getUsernameLinkHandle(); + + assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty(); + assertThat(DYNAMO_DB_EXTENSION.getDynamoDbClient() + .getItem(GetItemRequest.builder() + .tableName(Tables.USERNAMES.tableName()) + .key(Map.of(Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(USERNAME_HASH_1))) + .build()) + .item()).isEmpty(); + assertThat(accounts.getByUsernameLinkHandle(oldHandle).join()).isEmpty(); + + { + final Optional maybeAccount = accounts.getByUsernameHash(USERNAME_HASH_2).join(); + + assertThat(maybeAccount).isPresent(); + verifyStoredState(account.getNumber(), account.getUuid(), account.getPhoneNumberIdentifier(), + USERNAME_HASH_2, maybeAccount.get(), account); + final Optional maybeAccount2 = accounts.getByUsernameLinkHandle(newHandle).join(); + verifyStoredState(account.getNumber(), account.getUuid(), account.getPhoneNumberIdentifier(), + USERNAME_HASH_2, maybeAccount2.get(), account); + } + } + + @Test + void testUsernameHashConflict() { + final Account firstAccount = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID()); + final Account secondAccount = generateAccount("+18005559876", UUID.randomUUID(), UUID.randomUUID()); + + accounts.create(firstAccount); + accounts.create(secondAccount); + + // first account reserves and confirms username hash + assertThatNoException().isThrownBy(() -> { + accounts.reserveUsernameHash(firstAccount, USERNAME_HASH_1, Duration.ofDays(1)).join(); + accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join(); + }); + + final Optional maybeAccount = accounts.getByUsernameHash(USERNAME_HASH_1).join(); + + assertThat(maybeAccount).isPresent(); + verifyStoredState(firstAccount.getNumber(), firstAccount.getUuid(), firstAccount.getPhoneNumberIdentifier(), USERNAME_HASH_1, maybeAccount.get(), firstAccount); + + // throw an error if second account tries to reserve or confirm the same username hash + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.reserveUsernameHash(secondAccount, USERNAME_HASH_1, Duration.ofDays(1))); + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.confirmUsernameHash(secondAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)); + + // throw an error if first account tries to reserve or confirm the username hash that it has already confirmed + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.reserveUsernameHash(firstAccount, USERNAME_HASH_1, Duration.ofDays(1))); + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)); + + assertThat(secondAccount.getReservedUsernameHash()).isEmpty(); + assertThat(secondAccount.getUsernameHash()).isEmpty(); + } + + @Test + void testConfirmUsernameHashVersionMismatch() { + final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account); + accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join(); + account.setVersion(account.getVersion() + 77); + + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)); + + assertThat(account.getUsernameHash()).isEmpty(); + } + + @Test + void testClearUsername() { + final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account); + + accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join(); + accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join(); + assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isPresent(); + + accounts.clearUsernameHash(account).join(); + + assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty(); + assertThat(accounts.getByAccountIdentifier(account.getUuid())) + .hasValueSatisfying(clearedAccount -> assertThat(clearedAccount.getUsernameHash()).isEmpty()); + } + + @Test + void testClearUsernameNoUsername() { + final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account); + + assertThatNoException().isThrownBy(() -> accounts.clearUsernameHash(account).join()); + } + + @Test + void testClearUsernameVersionMismatch() { + final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account); + + accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join(); + accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join(); + + account.setVersion(account.getVersion() + 12); + + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.clearUsernameHash(account)); + + assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1); + } + + @Test + void testReservedUsernameHash() { + final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account1); + final Account account2 = generateAccount("+18005552222", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account2); + + accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1)).join(); + assertArrayEquals(account1.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1); + assertThat(account1.getUsernameHash()).isEmpty(); + + // account 2 shouldn't be able to reserve or confirm the same username hash + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1))); + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)); + assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty(); + + accounts.confirmUsernameHash(account1, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join(); + assertThat(account1.getReservedUsernameHash()).isEmpty(); + assertArrayEquals(account1.getUsernameHash().orElseThrow(), USERNAME_HASH_1); + assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join().get().getUuid()).isEqualTo(account1.getUuid()); + + final Map usernameConstraintRecord = DYNAMO_DB_EXTENSION.getDynamoDbClient() + .getItem(GetItemRequest.builder() + .tableName(Tables.USERNAMES.tableName()) + .key(Map.of(Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(USERNAME_HASH_1))) + .build()) + .item(); + + assertThat(usernameConstraintRecord).containsKey(Accounts.ATTR_USERNAME_HASH); + assertThat(usernameConstraintRecord).doesNotContainKey(Accounts.ATTR_TTL); + } + + @Test + void testUsernameHashAvailable() { + final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account1); + + accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1)).join(); + assertThat(accounts.usernameHashAvailable(USERNAME_HASH_1).join()).isFalse(); + assertThat(accounts.usernameHashAvailable(Optional.empty(), USERNAME_HASH_1).join()).isFalse(); + assertThat(accounts.usernameHashAvailable(Optional.of(UUID.randomUUID()), USERNAME_HASH_1).join()).isFalse(); + assertThat(accounts.usernameHashAvailable(Optional.of(account1.getUuid()), USERNAME_HASH_1).join()).isTrue(); + + accounts.confirmUsernameHash(account1, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join(); + assertThat(accounts.usernameHashAvailable(USERNAME_HASH_1).join()).isFalse(); + assertThat(accounts.usernameHashAvailable(Optional.empty(), USERNAME_HASH_1).join()).isFalse(); + assertThat(accounts.usernameHashAvailable(Optional.of(UUID.randomUUID()), USERNAME_HASH_1).join()).isFalse(); + assertThat(accounts.usernameHashAvailable(Optional.of(account1.getUuid()), USERNAME_HASH_1).join()).isFalse(); + } + + @Test + void testConfirmReservedUsernameHashWrongAccountUuid() { + final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account1); + final Account account2 = generateAccount("+18005552222", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account2); + + accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1)).join(); + assertArrayEquals(account1.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1); + assertThat(account1.getUsernameHash()).isEmpty(); + + // only account1 should be able to confirm the reserved hash + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)); + } + + @Test + void testConfirmExpiredReservedUsernameHash() { + final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account1); + final Account account2 = generateAccount("+18005552222", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account2); + + accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(2)).join(); + + for (int i = 0; i <= 2; i++) { + clock.pin(Instant.EPOCH.plus(Duration.ofDays(i))); + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1))); + } + + // after 2 days, can reserve and confirm the hash + clock.pin(Instant.EPOCH.plus(Duration.ofDays(2)).plus(Duration.ofSeconds(1))); + accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)).join(); + assertEquals(account2.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1); + + accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join(); + + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(2))); + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.confirmUsernameHash(account1, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)); + assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join().get().getUuid()).isEqualTo(account2.getUuid()); + } + + @Test + void testRetryReserveUsernameHash() { + final Account account = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account); + accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(2)).join(); + + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(2)), + "Shouldn't be able to re-reserve same username hash (would extend ttl)"); + } + + @Test + void testReserveConfirmUsernameHashVersionConflict() { + final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account); + account.setVersion(account.getVersion() + 12); + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1))); + CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)); + assertThat(account.getReservedUsernameHash()).isEmpty(); + assertThat(account.getUsernameHash()).isEmpty(); + } + + @Test + public void testIgnoredFieldsNotAddedToDataAttribute() throws Exception { + final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID()); + account.setUsernameHash(RandomUtils.nextBytes(32)); + account.setUsernameLinkDetails(UUID.randomUUID(), RandomUtils.nextBytes(32)); + accounts.create(account); + final Map accountRecord = DYNAMO_DB_EXTENSION.getDynamoDbClient() + .getItem(GetItemRequest.builder() + .tableName(Tables.ACCOUNTS.tableName()) + .key(Map.of(Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()))) + .build()) + .item(); + final Map dataMap = SystemMapper.jsonMapper() + .readValue(accountRecord.get(Accounts.ATTR_ACCOUNT_DATA).b().asByteArray(), Map.class); + Accounts.ACCOUNT_FIELDS_TO_EXCLUDE_FROM_SERIALIZATION + .forEach(field -> assertFalse(dataMap.containsKey(field))); + } + + @Test + void testGetByUsernameHashAsync() { + assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty(); + + final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID()); + accounts.create(account); + + assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty(); + + accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join(); + accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join(); + + assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isPresent(); + } + + private static Device generateDevice(long id) { + return DevicesHelper.createDevice(id); + } + + private static Account nextRandomAccount() { + final String nextNumber = "+1800%07d".formatted(ACCOUNT_COUNTER.getAndIncrement()); + return generateAccount(nextNumber, UUID.randomUUID(), UUID.randomUUID()); + } + + private static Account generateAccount(String number, UUID uuid, final UUID pni) { + Device device = generateDevice(1); + return generateAccount(number, uuid, pni, List.of(device)); + } + + private static Account generateAccount(String number, UUID uuid, final UUID pni, List devices) { + final byte[] unidentifiedAccessKey = new byte[16]; + final Random random = new Random(System.currentTimeMillis()); + Arrays.fill(unidentifiedAccessKey, (byte) random.nextInt(255)); + + return AccountsHelper.generateTestAccount(number, uuid, pni, devices, unidentifiedAccessKey); + } + + private void assertPhoneNumberConstraintExists(final String number, final UUID uuid) { + final GetItemResponse numberConstraintResponse = DYNAMO_DB_EXTENSION.getDynamoDbClient().getItem( + GetItemRequest.builder() + .tableName(Tables.NUMBERS.tableName()) + .key(Map.of(Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(number))) + .build()); + + assertThat(numberConstraintResponse.hasItem()).isTrue(); + assertThat(AttributeValues.getUUID(numberConstraintResponse.item(), Accounts.KEY_ACCOUNT_UUID, null)).isEqualTo(uuid); + } + + private void assertPhoneNumberConstraintDoesNotExist(final String number) { + final GetItemResponse numberConstraintResponse = DYNAMO_DB_EXTENSION.getDynamoDbClient().getItem( + GetItemRequest.builder() + .tableName(Tables.NUMBERS.tableName()) + .key(Map.of(Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(number))) + .build()); + + assertThat(numberConstraintResponse.hasItem()).isFalse(); + } + + private void assertPhoneNumberIdentifierConstraintExists(final UUID phoneNumberIdentifier, final UUID uuid) { + final GetItemResponse pniConstraintResponse = DYNAMO_DB_EXTENSION.getDynamoDbClient().getItem( + GetItemRequest.builder() + .tableName(Tables.PNI_ASSIGNMENTS.tableName()) + .key(Map.of(Accounts.ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier))) + .build()); + + assertThat(pniConstraintResponse.hasItem()).isTrue(); + assertThat(AttributeValues.getUUID(pniConstraintResponse.item(), Accounts.KEY_ACCOUNT_UUID, null)).isEqualTo(uuid); + } + + private void assertPhoneNumberIdentifierConstraintDoesNotExist(final UUID phoneNumberIdentifier) { + final GetItemResponse pniConstraintResponse = DYNAMO_DB_EXTENSION.getDynamoDbClient().getItem( + GetItemRequest.builder() + .tableName(Tables.PNI_ASSIGNMENTS.tableName()) + .key(Map.of(Accounts.ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier))) + .build()); + + assertThat(pniConstraintResponse.hasItem()).isFalse(); + } + + private void verifyStoredState(String number, UUID uuid, UUID pni, byte[] usernameHash, Account expecting, boolean canonicallyDiscoverable) { + final DynamoDbClient db = DYNAMO_DB_EXTENSION.getDynamoDbClient(); + + final GetItemResponse get = db.getItem(GetItemRequest.builder() + .tableName(Tables.ACCOUNTS.tableName()) + .key(Map.of(Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid))) + .consistentRead(true) + .build()); + + if (get.hasItem()) { + String data = new String(get.item().get(Accounts.ATTR_ACCOUNT_DATA).b().asByteArray(), StandardCharsets.UTF_8); + assertThat(data).isNotEmpty(); + + assertThat(AttributeValues.getInt(get.item(), Accounts.ATTR_VERSION, -1)) + .isEqualTo(expecting.getVersion()); + + assertThat(AttributeValues.getBool(get.item(), Accounts.ATTR_CANONICALLY_DISCOVERABLE, + !canonicallyDiscoverable)).isEqualTo(canonicallyDiscoverable); + + assertThat(AttributeValues.getByteArray(get.item(), Accounts.ATTR_UAK, null)) + .isEqualTo(expecting.getUnidentifiedAccessKey().orElse(null)); + + assertArrayEquals(AttributeValues.getByteArray(get.item(), Accounts.ATTR_USERNAME_HASH, null), usernameHash); + + Account result = Accounts.fromItem(get.item()); + verifyStoredState(number, uuid, pni, usernameHash, result, expecting); + } else { + throw new AssertionError("No data"); + } + } + + private void verifyStoredState(String number, UUID uuid, UUID pni, byte[] usernameHash, Account result, Account expecting) { + assertThat(result.getNumber()).isEqualTo(number); + assertThat(result.getPhoneNumberIdentifier()).isEqualTo(pni); + assertThat(result.getLastSeen()).isEqualTo(expecting.getLastSeen()); + assertThat(result.getUuid()).isEqualTo(uuid); + assertThat(result.getVersion()).isEqualTo(expecting.getVersion()); + assertArrayEquals(result.getUsernameHash().orElse(null), usernameHash); + assertThat(Arrays.equals(result.getUnidentifiedAccessKey().get(), expecting.getUnidentifiedAccessKey().get())).isTrue(); + + for (Device expectingDevice : expecting.getDevices()) { + Device resultDevice = result.getDevice(expectingDevice.getId()).get(); + assertThat(resultDevice.getApnId()).isEqualTo(expectingDevice.getApnId()); + assertThat(resultDevice.getGcmId()).isEqualTo(expectingDevice.getGcmId()); + assertThat(resultDevice.getLastSeen()).isEqualTo(expectingDevice.getLastSeen()); + assertThat(resultDevice.getSignedPreKey(IdentityType.ACI)).isEqualTo( + expectingDevice.getSignedPreKey(IdentityType.ACI)); + assertThat(resultDevice.getFetchesMessages()).isEqualTo(expectingDevice.getFetchesMessages()); + assertThat(resultDevice.getUserAgent()).isEqualTo(expectingDevice.getUserAgent()); + assertThat(resultDevice.getName()).isEqualTo(expectingDevice.getName()); + assertThat(resultDevice.getCreated()).isEqualTo(expectingDevice.getCreated()); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManagerTest.java new file mode 100644 index 000000000..7ce758211 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManagerTest.java @@ -0,0 +1,428 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.whispersystems.textsecuregcm.controllers.StaleDevicesException; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.IncomingMessage; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.push.MessageSender; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; + +public class ChangeNumberManagerTest { + private AccountsManager accountsManager; + private MessageSender messageSender; + private ChangeNumberManager changeNumberManager; + + private Map updatedPhoneNumberIdentifiersByAccount; + + @BeforeEach + void setUp() throws Exception { + accountsManager = mock(AccountsManager.class); + messageSender = mock(MessageSender.class); + changeNumberManager = new ChangeNumberManager(messageSender, accountsManager); + + updatedPhoneNumberIdentifiersByAccount = new HashMap<>(); + + when(accountsManager.changeNumber(any(), any(), any(), any(), any(), any())).thenAnswer((Answer)invocation -> { + final Account account = invocation.getArgument(0, Account.class); + final String number = invocation.getArgument(1, String.class); + + final UUID uuid = account.getUuid(); + final List devices = account.getDevices(); + + final UUID updatedPni = UUID.randomUUID(); + updatedPhoneNumberIdentifiersByAccount.put(account, updatedPni); + + final Account updatedAccount = mock(Account.class); + when(updatedAccount.getUuid()).thenReturn(uuid); + when(updatedAccount.getNumber()).thenReturn(number); + when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(updatedPni); + when(updatedAccount.getDevices()).thenReturn(devices); + for (long i = 1; i <= 3; i++) { + final Optional d = account.getDevice(i); + when(updatedAccount.getDevice(i)).thenReturn(d); + } + + return updatedAccount; + }); + + when(accountsManager.updatePniKeys(any(), any(), any(), any(), any())).thenAnswer((Answer)invocation -> { + final Account account = invocation.getArgument(0, Account.class); + + final UUID uuid = account.getUuid(); + final UUID pni = account.getPhoneNumberIdentifier(); + final List devices = account.getDevices(); + + final Account updatedAccount = mock(Account.class); + when(updatedAccount.getUuid()).thenReturn(uuid); + when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(pni); + when(updatedAccount.getDevices()).thenReturn(devices); + for (long i = 1; i <= 3; i++) { + final Optional d = account.getDevice(i); + when(updatedAccount.getDevice(i)).thenReturn(d); + } + + return updatedAccount; + }); + } + + @Test + void changeNumberNoMessages() throws Exception { + Account account = mock(Account.class); + when(account.getNumber()).thenReturn("+18005551234"); + changeNumberManager.changeNumber(account, "+18025551234", null, null, null, null, null); + verify(accountsManager).changeNumber(account, "+18025551234", null, null, null, null); + verify(accountsManager, never()).updateDevice(any(), eq(1L), any()); + verify(messageSender, never()).sendMessage(eq(account), any(), any(), eq(false)); + } + + @Test + void changeNumberSetPrimaryDevicePrekey() throws Exception { + Account account = mock(Account.class); + when(account.getNumber()).thenReturn("+18005551234"); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + final IdentityKey pniIdentityKey = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + final Map prekeys = Map.of(1L, KeysHelper.signedECPreKey(1, pniIdentityKeyPair)); + + changeNumberManager.changeNumber(account, "+18025551234", pniIdentityKey, prekeys, null, Collections.emptyList(), Collections.emptyMap()); + verify(accountsManager).changeNumber(account, "+18025551234", pniIdentityKey, prekeys, null, Collections.emptyMap()); + verify(messageSender, never()).sendMessage(eq(account), any(), any(), eq(false)); + } + + @Test + void changeNumberSetPrimaryDevicePrekeyAndSendMessages() throws Exception { + final String originalE164 = "+18005551234"; + final String changedE164 = "+18025551234"; + final UUID aci = UUID.randomUUID(); + final UUID pni = UUID.randomUUID(); + + final Account account = mock(Account.class); + when(account.getNumber()).thenReturn(originalE164); + when(account.getUuid()).thenReturn(aci); + when(account.getPhoneNumberIdentifier()).thenReturn(pni); + + final Device d2 = mock(Device.class); + when(d2.isEnabled()).thenReturn(true); + when(d2.getId()).thenReturn(2L); + + when(account.getDevice(2L)).thenReturn(Optional.of(d2)); + when(account.getDevices()).thenReturn(List.of(d2)); + + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey()); + final Map prekeys = Map.of(1L, KeysHelper.signedECPreKey(1, pniIdentityKeyPair), 2L, KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); + final Map registrationIds = Map.of(1L, 17, 2L, 19); + + final IncomingMessage msg = mock(IncomingMessage.class); + when(msg.destinationDeviceId()).thenReturn(2L); + when(msg.content()).thenReturn(Base64.getEncoder().encodeToString(new byte[]{1})); + + changeNumberManager.changeNumber(account, changedE164, pniIdentityKey, prekeys, null, List.of(msg), registrationIds); + + verify(accountsManager).changeNumber(account, changedE164, pniIdentityKey, prekeys, null, registrationIds); + + final ArgumentCaptor envelopeCaptor = ArgumentCaptor.forClass(MessageProtos.Envelope.class); + verify(messageSender).sendMessage(any(), eq(d2), envelopeCaptor.capture(), eq(false)); + + final MessageProtos.Envelope envelope = envelopeCaptor.getValue(); + + assertEquals(aci, UUID.fromString(envelope.getDestinationUuid())); + assertEquals(aci, UUID.fromString(envelope.getSourceUuid())); + assertEquals(Device.MASTER_ID, envelope.getSourceDevice()); + assertEquals(updatedPhoneNumberIdentifiersByAccount.get(account), UUID.fromString(envelope.getUpdatedPni())); + } + + + @Test + void changeNumberSetPrimaryDevicePrekeyPqAndSendMessages() throws Exception { + final String originalE164 = "+18005551234"; + final String changedE164 = "+18025551234"; + final UUID aci = UUID.randomUUID(); + final UUID pni = UUID.randomUUID(); + + final Account account = mock(Account.class); + when(account.getNumber()).thenReturn(originalE164); + when(account.getUuid()).thenReturn(aci); + when(account.getPhoneNumberIdentifier()).thenReturn(pni); + + final Device d2 = mock(Device.class); + when(d2.isEnabled()).thenReturn(true); + when(d2.getId()).thenReturn(2L); + + when(account.getDevice(2L)).thenReturn(Optional.of(d2)); + when(account.getDevices()).thenReturn(List.of(d2)); + + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey()); + final Map prekeys = Map.of(1L, KeysHelper.signedECPreKey(1, pniIdentityKeyPair), 2L, KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); + final Map pqPrekeys = Map.of(3L, KeysHelper.signedKEMPreKey(3, pniIdentityKeyPair), 4L, KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair)); + final Map registrationIds = Map.of(1L, 17, 2L, 19); + + final IncomingMessage msg = mock(IncomingMessage.class); + when(msg.destinationDeviceId()).thenReturn(2L); + when(msg.content()).thenReturn(Base64.getEncoder().encodeToString(new byte[]{1})); + + changeNumberManager.changeNumber(account, changedE164, pniIdentityKey, prekeys, pqPrekeys, List.of(msg), registrationIds); + + verify(accountsManager).changeNumber(account, changedE164, pniIdentityKey, prekeys, pqPrekeys, registrationIds); + + final ArgumentCaptor envelopeCaptor = ArgumentCaptor.forClass(MessageProtos.Envelope.class); + verify(messageSender).sendMessage(any(), eq(d2), envelopeCaptor.capture(), eq(false)); + + final MessageProtos.Envelope envelope = envelopeCaptor.getValue(); + + assertEquals(aci, UUID.fromString(envelope.getDestinationUuid())); + assertEquals(aci, UUID.fromString(envelope.getSourceUuid())); + assertEquals(Device.MASTER_ID, envelope.getSourceDevice()); + assertEquals(updatedPhoneNumberIdentifiersByAccount.get(account), UUID.fromString(envelope.getUpdatedPni())); + } + + @Test + void changeNumberSameNumberSetPrimaryDevicePrekeyAndSendMessages() throws Exception { + final String originalE164 = "+18005551234"; + final UUID aci = UUID.randomUUID(); + final UUID pni = UUID.randomUUID(); + + final Account account = mock(Account.class); + when(account.getNumber()).thenReturn(originalE164); + when(account.getUuid()).thenReturn(aci); + when(account.getPhoneNumberIdentifier()).thenReturn(pni); + + final Device d2 = mock(Device.class); + when(d2.isEnabled()).thenReturn(true); + when(d2.getId()).thenReturn(2L); + + when(account.getDevice(2L)).thenReturn(Optional.of(d2)); + when(account.getDevices()).thenReturn(List.of(d2)); + + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey()); + final Map prekeys = Map.of(1L, KeysHelper.signedECPreKey(1, pniIdentityKeyPair), 2L, KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); + final Map pqPrekeys = Map.of(3L, KeysHelper.signedKEMPreKey(3, pniIdentityKeyPair), 4L, KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair)); + final Map registrationIds = Map.of(1L, 17, 2L, 19); + + final IncomingMessage msg = mock(IncomingMessage.class); + when(msg.destinationDeviceId()).thenReturn(2L); + when(msg.content()).thenReturn(Base64.getEncoder().encodeToString(new byte[]{1})); + + changeNumberManager.changeNumber(account, originalE164, pniIdentityKey, prekeys, pqPrekeys, List.of(msg), registrationIds); + + verify(accountsManager).updatePniKeys(account, pniIdentityKey, prekeys, pqPrekeys, registrationIds); + + final ArgumentCaptor envelopeCaptor = ArgumentCaptor.forClass(MessageProtos.Envelope.class); + verify(messageSender).sendMessage(any(), eq(d2), envelopeCaptor.capture(), eq(false)); + + final MessageProtos.Envelope envelope = envelopeCaptor.getValue(); + + assertEquals(aci, UUID.fromString(envelope.getDestinationUuid())); + assertEquals(aci, UUID.fromString(envelope.getSourceUuid())); + assertEquals(Device.MASTER_ID, envelope.getSourceDevice()); + assertFalse(updatedPhoneNumberIdentifiersByAccount.containsKey(account)); + } + + @Test + void updatePniKeysSetPrimaryDevicePrekeyAndSendMessages() throws Exception { + final UUID aci = UUID.randomUUID(); + final UUID pni = UUID.randomUUID(); + + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(aci); + when(account.getPhoneNumberIdentifier()).thenReturn(pni); + + final Device d2 = mock(Device.class); + when(d2.isEnabled()).thenReturn(true); + when(d2.getId()).thenReturn(2L); + + when(account.getDevice(2L)).thenReturn(Optional.of(d2)); + when(account.getDevices()).thenReturn(List.of(d2)); + + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey()); + final Map prekeys = Map.of(1L, KeysHelper.signedECPreKey(1, pniIdentityKeyPair), 2L, KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); + final Map registrationIds = Map.of(1L, 17, 2L, 19); + + final IncomingMessage msg = mock(IncomingMessage.class); + when(msg.destinationDeviceId()).thenReturn(2L); + when(msg.content()).thenReturn(Base64.getEncoder().encodeToString(new byte[]{1})); + + changeNumberManager.updatePniKeys(account, pniIdentityKey, prekeys, null, List.of(msg), registrationIds); + + verify(accountsManager).updatePniKeys(account, pniIdentityKey, prekeys, null, registrationIds); + + final ArgumentCaptor envelopeCaptor = ArgumentCaptor.forClass(MessageProtos.Envelope.class); + verify(messageSender).sendMessage(any(), eq(d2), envelopeCaptor.capture(), eq(false)); + + final MessageProtos.Envelope envelope = envelopeCaptor.getValue(); + + assertEquals(aci, UUID.fromString(envelope.getDestinationUuid())); + assertEquals(aci, UUID.fromString(envelope.getSourceUuid())); + assertEquals(Device.MASTER_ID, envelope.getSourceDevice()); + assertFalse(updatedPhoneNumberIdentifiersByAccount.containsKey(account)); + } + + @Test + void updatePniKeysSetPrimaryDevicePrekeyPqAndSendMessages() throws Exception { + final UUID aci = UUID.randomUUID(); + final UUID pni = UUID.randomUUID(); + + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(aci); + when(account.getPhoneNumberIdentifier()).thenReturn(pni); + + final Device d2 = mock(Device.class); + when(d2.isEnabled()).thenReturn(true); + when(d2.getId()).thenReturn(2L); + + when(account.getDevice(2L)).thenReturn(Optional.of(d2)); + when(account.getDevices()).thenReturn(List.of(d2)); + + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey()); + final Map prekeys = Map.of(1L, KeysHelper.signedECPreKey(1, pniIdentityKeyPair), 2L, KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); + final Map pqPrekeys = Map.of(3L, KeysHelper.signedKEMPreKey(3, pniIdentityKeyPair), 4L, KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair)); + final Map registrationIds = Map.of(1L, 17, 2L, 19); + + final IncomingMessage msg = mock(IncomingMessage.class); + when(msg.destinationDeviceId()).thenReturn(2L); + when(msg.content()).thenReturn(Base64.getEncoder().encodeToString(new byte[]{1})); + + changeNumberManager.updatePniKeys(account, pniIdentityKey, prekeys, pqPrekeys, List.of(msg), registrationIds); + + verify(accountsManager).updatePniKeys(account, pniIdentityKey, prekeys, pqPrekeys, registrationIds); + + final ArgumentCaptor envelopeCaptor = ArgumentCaptor.forClass(MessageProtos.Envelope.class); + verify(messageSender).sendMessage(any(), eq(d2), envelopeCaptor.capture(), eq(false)); + + final MessageProtos.Envelope envelope = envelopeCaptor.getValue(); + + assertEquals(aci, UUID.fromString(envelope.getDestinationUuid())); + assertEquals(aci, UUID.fromString(envelope.getSourceUuid())); + assertEquals(Device.MASTER_ID, envelope.getSourceDevice()); + assertFalse(updatedPhoneNumberIdentifiersByAccount.containsKey(account)); + } + + @Test + void changeNumberMismatchedRegistrationId() { + final Account account = mock(Account.class); + when(account.getNumber()).thenReturn("+18005551234"); + + final List devices = new ArrayList<>(); + + for (int i = 1; i <= 3; i++) { + final Device device = mock(Device.class); + when(device.getId()).thenReturn((long) i); + when(device.isEnabled()).thenReturn(true); + when(device.getRegistrationId()).thenReturn(i); + + devices.add(device); + when(account.getDevice(i)).thenReturn(Optional.of(device)); + } + + when(account.getDevices()).thenReturn(devices); + + final List messages = List.of( + new IncomingMessage(1, 2, 1, "foo"), + new IncomingMessage(1, 3, 1, "foo")); + + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + final ECPublicKey pniIdentityKey = pniIdentityKeyPair.getPublicKey(); + + final Map preKeys = Map.of(1L, KeysHelper.signedECPreKey(1, pniIdentityKeyPair), 2L, KeysHelper.signedECPreKey(2, pniIdentityKeyPair), 3L, KeysHelper.signedECPreKey(3, pniIdentityKeyPair)); + final Map registrationIds = Map.of(1L, 17, 2L, 47, 3L, 89); + + assertThrows(StaleDevicesException.class, + () -> changeNumberManager.changeNumber(account, "+18005559876", new IdentityKey(Curve.generateKeyPair().getPublicKey()), preKeys, null, messages, registrationIds)); + } + + @Test + void updatePniKeysMismatchedRegistrationId() { + final Account account = mock(Account.class); + when(account.getNumber()).thenReturn("+18005551234"); + + final List devices = new ArrayList<>(); + + for (int i = 1; i <= 3; i++) { + final Device device = mock(Device.class); + when(device.getId()).thenReturn((long) i); + when(device.isEnabled()).thenReturn(true); + when(device.getRegistrationId()).thenReturn(i); + + devices.add(device); + when(account.getDevice(i)).thenReturn(Optional.of(device)); + } + + when(account.getDevices()).thenReturn(devices); + + final List messages = List.of( + new IncomingMessage(1, 2, 1, "foo"), + new IncomingMessage(1, 3, 1, "foo")); + + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + final ECPublicKey pniIdentityKey = pniIdentityKeyPair.getPublicKey(); + + final Map preKeys = Map.of(1L, KeysHelper.signedECPreKey(1, pniIdentityKeyPair), 2L, KeysHelper.signedECPreKey(2, pniIdentityKeyPair), 3L, KeysHelper.signedECPreKey(3, pniIdentityKeyPair)); + final Map registrationIds = Map.of(1L, 17, 2L, 47, 3L, 89); + + assertThrows(StaleDevicesException.class, + () -> changeNumberManager.updatePniKeys(account, new IdentityKey(Curve.generateKeyPair().getPublicKey()), preKeys, null, messages, registrationIds)); + } + + @Test + void changeNumberMissingData() { + final Account account = mock(Account.class); + when(account.getNumber()).thenReturn("+18005551234"); + + final List devices = new ArrayList<>(); + + for (int i = 1; i <= 3; i++) { + final Device device = mock(Device.class); + when(device.getId()).thenReturn((long) i); + when(device.isEnabled()).thenReturn(true); + when(device.getRegistrationId()).thenReturn(i); + + devices.add(device); + when(account.getDevice(i)).thenReturn(Optional.of(device)); + } + + when(account.getDevices()).thenReturn(devices); + + final List messages = List.of( + new IncomingMessage(1, 2, 2, "foo"), + new IncomingMessage(1, 3, 3, "foo")); + + final Map registrationIds = Map.of(1L, 17, 2L, 47, 3L, 89); + + assertThrows(IllegalArgumentException.class, + () -> changeNumberManager.changeNumber(account, "+18005559876", new IdentityKey(Curve.generateKeyPair().getPublicKey()), null, null, messages, registrationIds)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManagerTest.java new file mode 100644 index 000000000..8cb17ebff --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManagerTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.vdurmont.semver4j.Semver; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; + +class ClientReleaseManagerTest { + + private ClientReleases clientReleases; + private Clock clock; + + private ClientReleaseManager clientReleaseManager; + + @BeforeEach + void setUp() { + clientReleases = mock(ClientReleases.class); + clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + + clientReleaseManager = + new ClientReleaseManager(clientReleases, mock(ScheduledExecutorService.class), Duration.ofHours(4), clock); + } + + @Test + void isVersionActive() { + final Semver iosVersion = new Semver("1.2.3"); + final Semver desktopVersion = new Semver("4.5.6"); + + when(clientReleases.getClientReleases()).thenReturn(Map.of( + ClientPlatform.DESKTOP, Map.of(desktopVersion, new ClientRelease(ClientPlatform.DESKTOP, desktopVersion, clock.instant(), clock.instant().plus(Duration.ofDays(90)))), + ClientPlatform.IOS, Map.of(iosVersion, new ClientRelease(ClientPlatform.IOS, iosVersion, clock.instant().minus(Duration.ofDays(91)), clock.instant().minus(Duration.ofDays(1)))) + )); + + clientReleaseManager.refreshClientVersions(); + + assertTrue(clientReleaseManager.isVersionActive(ClientPlatform.DESKTOP, desktopVersion)); + assertFalse(clientReleaseManager.isVersionActive(ClientPlatform.DESKTOP, iosVersion)); + assertFalse(clientReleaseManager.isVersionActive(ClientPlatform.IOS, iosVersion)); + assertFalse(clientReleaseManager.isVersionActive(ClientPlatform.ANDROID, new Semver("7.8.9"))); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ClientReleasesTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ClientReleasesTest.java new file mode 100644 index 000000000..2097bec30 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ClientReleasesTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.vdurmont.semver4j.Semver; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; + +class ClientReleasesTest { + + private ClientReleases clientReleases; + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = + new DynamoDbExtension(DynamoDbExtensionSchema.Tables.CLIENT_RELEASES); + + @BeforeEach + void setUp() { + clientReleases = new ClientReleases(DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + DynamoDbExtensionSchema.Tables.CLIENT_RELEASES.tableName()); + } + + @Test + void getClientReleases() { + final Instant releaseTimestamp = Instant.now().truncatedTo(ChronoUnit.SECONDS); + final Instant expiration = releaseTimestamp.plusSeconds(60); + + storeClientRelease("IOS", "1.2.3", releaseTimestamp, expiration); + storeClientRelease("IOS", "not-a-valid-version", releaseTimestamp, expiration); + storeClientRelease("ANDROID", "4.5.6", releaseTimestamp, expiration); + storeClientRelease("UNRECOGNIZED_PLATFORM", "7.8.9", releaseTimestamp, expiration); + + final Map> expectedVersions = Map.of( + ClientPlatform.IOS, Map.of(new Semver("1.2.3"), new ClientRelease(ClientPlatform.IOS, new Semver("1.2.3"), releaseTimestamp, expiration)), + ClientPlatform.ANDROID, Map.of(new Semver("4.5.6"), new ClientRelease(ClientPlatform.ANDROID, new Semver("4.5.6"), releaseTimestamp, expiration))); + + assertEquals(expectedVersions, clientReleases.getClientReleases()); + } + + private void storeClientRelease(final String platform, final String version, final Instant release, final Instant expiration) { + DYNAMO_DB_EXTENSION.getDynamoDbClient().putItem(PutItemRequest.builder() + .tableName(DynamoDbExtensionSchema.Tables.CLIENT_RELEASES.tableName()) + .item(Map.of( + ClientReleases.ATTR_PLATFORM, AttributeValue.builder().s(platform).build(), + ClientReleases.ATTR_VERSION, AttributeValue.builder().s(version).build(), + ClientReleases.ATTR_RELEASE_TIMESTAMP, + AttributeValue.builder().n(String.valueOf(release.getEpochSecond())).build(), + ClientReleases.ATTR_EXPIRATION, + AttributeValue.builder().n(String.valueOf(expiration.getEpochSecond())).build())) + .build()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/DeviceTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/DeviceTest.java new file mode 100644 index 000000000..44f25921a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/DeviceTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +import java.time.Duration; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; + +class DeviceTest { + + @ParameterizedTest + @MethodSource + void testIsEnabled(final boolean master, final boolean fetchesMessages, final String apnId, final String gcmId, + final ECSignedPreKey signedPreKey, final Duration timeSinceLastSeen, final boolean expectEnabled) { + + final long lastSeen = System.currentTimeMillis() - timeSinceLastSeen.toMillis(); + + final Device device = new Device(); + device.setId(master ? Device.MASTER_ID : Device.MASTER_ID + 1); + device.setFetchesMessages(fetchesMessages); + device.setApnId(apnId); + device.setGcmId(gcmId); + device.setSignedPreKey(signedPreKey); + device.setCreated(lastSeen); + device.setLastSeen(lastSeen); + + assertEquals(expectEnabled, device.isEnabled()); + } + + private static Stream testIsEnabled() { + return Stream.of( + // master fetchesMessages apnId gcmId signedPreKey lastSeen expectEnabled + Arguments.of(true, false, null, null, null, Duration.ofDays(60), false), + Arguments.of(true, false, null, null, null, Duration.ofDays(1), false), + Arguments.of(true, false, null, null, mock(ECSignedPreKey.class), Duration.ofDays(60), false), + Arguments.of(true, false, null, null, mock(ECSignedPreKey.class), Duration.ofDays(1), false), + Arguments.of(true, false, null, "gcm-id", null, Duration.ofDays(60), false), + Arguments.of(true, false, null, "gcm-id", null, Duration.ofDays(1), false), + Arguments.of(true, false, null, "gcm-id", mock(ECSignedPreKey.class), Duration.ofDays(60), true), + Arguments.of(true, false, null, "gcm-id", mock(ECSignedPreKey.class), Duration.ofDays(1), true), + Arguments.of(true, false, "apn-id", null, null, Duration.ofDays(60), false), + Arguments.of(true, false, "apn-id", null, null, Duration.ofDays(1), false), + Arguments.of(true, false, "apn-id", null, mock(ECSignedPreKey.class), Duration.ofDays(60), true), + Arguments.of(true, false, "apn-id", null, mock(ECSignedPreKey.class), Duration.ofDays(1), true), + Arguments.of(true, true, null, null, null, Duration.ofDays(60), false), + Arguments.of(true, true, null, null, null, Duration.ofDays(1), false), + Arguments.of(true, true, null, null, mock(ECSignedPreKey.class), Duration.ofDays(60), true), + Arguments.of(true, true, null, null, mock(ECSignedPreKey.class), Duration.ofDays(1), true), + Arguments.of(false, false, null, null, null, Duration.ofDays(60), false), + Arguments.of(false, false, null, null, null, Duration.ofDays(1), false), + Arguments.of(false, false, null, null, mock(ECSignedPreKey.class), Duration.ofDays(60), false), + Arguments.of(false, false, null, null, mock(ECSignedPreKey.class), Duration.ofDays(1), false), + Arguments.of(false, false, null, "gcm-id", null, Duration.ofDays(60), false), + Arguments.of(false, false, null, "gcm-id", null, Duration.ofDays(1), false), + Arguments.of(false, false, null, "gcm-id", mock(ECSignedPreKey.class), Duration.ofDays(60), false), + Arguments.of(false, false, null, "gcm-id", mock(ECSignedPreKey.class), Duration.ofDays(1), true), + Arguments.of(false, false, "apn-id", null, null, Duration.ofDays(60), false), + Arguments.of(false, false, "apn-id", null, null, Duration.ofDays(1), false), + Arguments.of(false, false, "apn-id", null, mock(ECSignedPreKey.class), Duration.ofDays(60), false), + Arguments.of(false, false, "apn-id", null, mock(ECSignedPreKey.class), Duration.ofDays(1), true), + Arguments.of(false, true, null, null, null, Duration.ofDays(60), false), + Arguments.of(false, true, null, null, null, Duration.ofDays(1), false), + Arguments.of(false, true, null, null, mock(ECSignedPreKey.class), Duration.ofDays(60), false), + Arguments.of(false, true, null, null, mock(ECSignedPreKey.class), Duration.ofDays(1), true) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/DynamicConfigurationManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/DynamicConfigurationManagerTest.java new file mode 100644 index 000000000..d0b35743d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/DynamicConfigurationManagerTest.java @@ -0,0 +1,152 @@ +package org.whispersystems.textsecuregcm.storage; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.appconfigdata.AppConfigDataClient; +import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationRequest; +import software.amazon.awssdk.services.appconfigdata.model.GetLatestConfigurationResponse; +import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionRequest; +import software.amazon.awssdk.services.appconfigdata.model.StartConfigurationSessionResponse; + +class DynamicConfigurationManagerTest { + + private static final SdkBytes VALID_CONFIG = SdkBytes.fromUtf8String(""" + test: true + captcha: + scoreFloor: 1.0 + """); + + private DynamicConfigurationManager dynamicConfigurationManager; + private AppConfigDataClient appConfig; + private StartConfigurationSessionRequest startConfigurationSession; + + @BeforeEach + void setup() { + this.appConfig = mock(AppConfigDataClient.class); + this.dynamicConfigurationManager = new DynamicConfigurationManager<>( + appConfig, "foo", "bar", "baz", DynamicConfiguration.class); + this.startConfigurationSession = StartConfigurationSessionRequest.builder() + .applicationIdentifier("foo") + .environmentIdentifier("bar") + .configurationProfileIdentifier("baz") + .build(); + } + + @Test + void testGetInitialConfig() { + when(appConfig.startConfigurationSession(startConfigurationSession)) + .thenReturn(StartConfigurationSessionResponse.builder() + .initialConfigurationToken("initial") + .build()); + + // call with initial token will return a real config + when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder() + .configurationToken("initial").build())) + .thenReturn(GetLatestConfigurationResponse.builder() + .configuration(VALID_CONFIG) + .nextPollConfigurationToken("next").build()); + + // subsequent config calls will return empty (no update) + when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder(). + configurationToken("next").build())) + .thenReturn(GetLatestConfigurationResponse.builder() + .configuration(SdkBytes.fromUtf8String("")) + .nextPollConfigurationToken("next").build()); + + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + dynamicConfigurationManager.start(); + assertThat(dynamicConfigurationManager.getConfiguration()).isNotNull(); + }); + } + + @Test + void testBadConfig() { + when(appConfig.startConfigurationSession(startConfigurationSession)) + .thenReturn(StartConfigurationSessionResponse.builder() + .initialConfigurationToken("initial") + .build()); + + // call with initial token will return a bad config + when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder() + .configurationToken("initial").build())) + .thenReturn(GetLatestConfigurationResponse.builder() + .configuration(SdkBytes.fromUtf8String("zzz")) + .nextPollConfigurationToken("goodconfig").build()); + + // next config call will return a good config + when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder(). + configurationToken("goodconfig").build())) + .thenReturn(GetLatestConfigurationResponse.builder() + .configuration(VALID_CONFIG) + .nextPollConfigurationToken("next").build()); + + // all subsequent config calls will return an empty config (no update) + when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder(). + configurationToken("next").build())) + .thenReturn(GetLatestConfigurationResponse.builder() + .configuration(SdkBytes.fromUtf8String("")) + .nextPollConfigurationToken("next").build()); + + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + dynamicConfigurationManager.start(); + assertThat(dynamicConfigurationManager.getConfiguration()).isNotNull(); + }); + } + + @Test + void testGetConfigMultiple() { + when(appConfig.startConfigurationSession(startConfigurationSession)) + .thenReturn(StartConfigurationSessionResponse.builder() + .initialConfigurationToken("0") + .build()); + + // initial config + when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder(). + configurationToken("0").build())) + .thenReturn(GetLatestConfigurationResponse.builder() + .configuration(VALID_CONFIG) + .nextPollConfigurationToken("1").build()); + + // config update with a real config + when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder(). + configurationToken("1").build())) + .thenReturn(GetLatestConfigurationResponse.builder() + .configuration(SdkBytes.fromUtf8String(""" + experiments: + test: + enrollmentPercentage: 50 + captcha: + scoreFloor: 1.0 + """)) + .nextPollConfigurationToken("2").build()); + + // all subsequent are no update + when(appConfig.getLatestConfiguration(GetLatestConfigurationRequest.builder(). + configurationToken("2").build())) + .thenReturn(GetLatestConfigurationResponse.builder() + .configuration(SdkBytes.fromUtf8String("")) + .nextPollConfigurationToken("2").build()); + + // the internal waiting done by dynamic configuration manager catches the InterruptedException used + // by JUnit’s @Timeout, so we use assertTimeoutPreemptively + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + // we should eventually get the updated config (or the test will timeout) + dynamicConfigurationManager.start(); + while (dynamicConfigurationManager.getConfiguration().getExperimentEnrollmentConfiguration("test").isEmpty()) { + Thread.sleep(100); + } + assertThat( + dynamicConfigurationManager.getConfiguration().getExperimentEnrollmentConfiguration("test").get() + .getEnrollmentPercentage()).isEqualTo(50); + }); + + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/DynamoDbExtension.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/DynamoDbExtension.java new file mode 100644 index 000000000..bb35b5ecf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/DynamoDbExtension.java @@ -0,0 +1,171 @@ +/* + * Copyright 2021-2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.almworks.sqlite4java.SQLite; +import com.amazonaws.services.dynamodbv2.local.main.ServerRunner; +import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer; +import java.net.ServerSocket; +import java.net.URI; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndex; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; + +public class DynamoDbExtension implements BeforeEachCallback, AfterEachCallback { + + public interface TableSchema { + String tableName(); + String hashKeyName(); + String rangeKeyName(); + List attributeDefinitions(); + List globalSecondaryIndexes(); + List localSecondaryIndexes(); + } + + record RawSchema( + String tableName, + String hashKeyName, + String rangeKeyName, + List attributeDefinitions, + List globalSecondaryIndexes, + List localSecondaryIndexes + ) implements TableSchema { } + + static final ProvisionedThroughput DEFAULT_PROVISIONED_THROUGHPUT = ProvisionedThroughput.builder() + .readCapacityUnits(20L) + .writeCapacityUnits(20L) + .build(); + + private static final AtomicBoolean libraryLoaded = new AtomicBoolean(); + + private DynamoDBProxyServer server; + private int port; + + private final List schemas; + private DynamoDbClient dynamoDB2; + private DynamoDbAsyncClient dynamoAsyncDB2; + + public DynamoDbExtension(TableSchema... schemas) { + this.schemas = List.of(schemas); + } + + private static void loadLibrary() { + // to avoid noise in the logs from “library already loaded” warnings, we make sure we only set it once + if (libraryLoaded.get()) { + return; + } + if (libraryLoaded.compareAndSet(false, true)) { + // if you see a library failed to load error, you need to run mvn test-compile at least once first + SQLite.setLibraryPath("target/lib"); + } + } + + @Override + public void afterEach(ExtensionContext context) { + stopServer(); + } + + /** + * For use in integration tests that want to test resiliency/error handling + */ + public void stopServer() { + try { + server.stop(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + + startServer(); + + initializeClient(); + + createTables(); + } + + private void createTables() { + schemas.stream().forEach(this::createTable); + } + + private void createTable(TableSchema schema) { + KeySchemaElement[] keySchemaElements; + if (schema.rangeKeyName() == null) { + keySchemaElements = new KeySchemaElement[] { + KeySchemaElement.builder().attributeName(schema.hashKeyName()).keyType(KeyType.HASH).build(), + }; + } else { + keySchemaElements = new KeySchemaElement[] { + KeySchemaElement.builder().attributeName(schema.hashKeyName()).keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName(schema.rangeKeyName()).keyType(KeyType.RANGE).build(), + }; + } + + final CreateTableRequest createTableRequest = CreateTableRequest.builder() + .tableName(schema.tableName()) + .keySchema(keySchemaElements) + .attributeDefinitions(schema.attributeDefinitions().isEmpty() ? null : schema.attributeDefinitions()) + .globalSecondaryIndexes(schema.globalSecondaryIndexes().isEmpty() ? null : schema.globalSecondaryIndexes()) + .localSecondaryIndexes(schema.localSecondaryIndexes().isEmpty() ? null : schema.localSecondaryIndexes()) + .provisionedThroughput(DEFAULT_PROVISIONED_THROUGHPUT) + .build(); + + getDynamoDbClient().createTable(createTableRequest); + } + + private void startServer() throws Exception { + // Even though we're using AWS SDK v2, Dynamo's local implementation's canonical location + // is within v1 (https://github.com/aws/aws-sdk-java-v2/issues/982). This does support + // v2 clients, though. + loadLibrary(); + ServerSocket serverSocket = new ServerSocket(0); + serverSocket.setReuseAddress(false); + port = serverSocket.getLocalPort(); + serverSocket.close(); + server = ServerRunner.createServerFromCommandLineArgs(new String[]{"-inMemory", "-port", String.valueOf(port)}); + server.start(); + } + + private void initializeClient() { + dynamoDB2 = DynamoDbClient.builder() + .endpointOverride(URI.create("http://localhost:" + port)) + .region(Region.of("local-test-region")) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("accessKey", "secretKey"))) + .build(); + dynamoAsyncDB2 = DynamoDbAsyncClient.builder() + .endpointOverride(URI.create("http://localhost:" + port)) + .region(Region.of("local-test-region")) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("accessKey", "secretKey"))) + .build(); + } + + public DynamoDbClient getDynamoDbClient() { + return dynamoDB2; + } + + public DynamoDbAsyncClient getDynamoDbAsyncClient() { + return dynamoAsyncDB2; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/DynamoDbExtensionSchema.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/DynamoDbExtensionSchema.java new file mode 100644 index 000000000..d167368b6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/DynamoDbExtensionSchema.java @@ -0,0 +1,377 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import java.util.List; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndex; +import software.amazon.awssdk.services.dynamodb.model.Projection; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; + +public final class DynamoDbExtensionSchema { + + public enum Tables implements DynamoDbExtension.TableSchema { + + ACCOUNTS("accounts_test", + Accounts.KEY_ACCOUNT_UUID, + null, + List.of( + AttributeDefinition.builder() + .attributeName(Accounts.KEY_ACCOUNT_UUID) + .attributeType(ScalarAttributeType.B) + .build(), + AttributeDefinition.builder() + .attributeName(Accounts.ATTR_USERNAME_LINK_UUID) + .attributeType(ScalarAttributeType.B) + .build()), + List.of( + GlobalSecondaryIndex.builder() + .indexName(Accounts.USERNAME_LINK_TO_UUID_INDEX) + .keySchema( + KeySchemaElement.builder() + .attributeName(Accounts.ATTR_USERNAME_LINK_UUID) + .keyType(KeyType.HASH) + .build() + ) + .projection(Projection.builder().projectionType(ProjectionType.KEYS_ONLY).build()) + .provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build()) + .build() + ), + List.of()), + + CLIENT_RELEASES("client_releases_test", + ClientReleases.ATTR_PLATFORM, + ClientReleases.ATTR_VERSION, + List.of( + AttributeDefinition.builder() + .attributeName(ClientReleases.ATTR_PLATFORM) + .attributeType(ScalarAttributeType.S) + .build(), + AttributeDefinition.builder() + .attributeName(ClientReleases.ATTR_VERSION) + .attributeType(ScalarAttributeType.S) + .build()), + List.of(), + List.of()), + + DELETED_ACCOUNTS("deleted_accounts_test", + Accounts.DELETED_ACCOUNTS_KEY_ACCOUNT_E164, + null, + List.of( + AttributeDefinition.builder() + .attributeName(Accounts.DELETED_ACCOUNTS_KEY_ACCOUNT_E164) + .attributeType(ScalarAttributeType.S).build(), + AttributeDefinition.builder() + .attributeName(Accounts.DELETED_ACCOUNTS_ATTR_ACCOUNT_UUID) + .attributeType(ScalarAttributeType.B) + .build()), + List.of( + GlobalSecondaryIndex.builder() + .indexName(Accounts.DELETED_ACCOUNTS_UUID_TO_E164_INDEX_NAME) + .keySchema( + KeySchemaElement.builder().attributeName(Accounts.DELETED_ACCOUNTS_ATTR_ACCOUNT_UUID).keyType(KeyType.HASH).build() + ) + .projection(Projection.builder().projectionType(ProjectionType.KEYS_ONLY).build()) + .provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build()) + .build()), + List.of() + ), + + DELETED_ACCOUNTS_LOCK("deleted_accounts_lock_test", + AccountLockManager.KEY_ACCOUNT_E164, + null, + List.of(AttributeDefinition.builder() + .attributeName(AccountLockManager.KEY_ACCOUNT_E164) + .attributeType(ScalarAttributeType.S).build()), + List.of(), List.of()), + + NUMBERS("numbers_test", + Accounts.ATTR_ACCOUNT_E164, + null, + List.of(AttributeDefinition.builder() + .attributeName(Accounts.ATTR_ACCOUNT_E164) + .attributeType(ScalarAttributeType.S) + .build()), + List.of(), List.of()), + + EC_KEYS("keys_test", + SingleUsePreKeyStore.KEY_ACCOUNT_UUID, + SingleUsePreKeyStore.KEY_DEVICE_ID_KEY_ID, + List.of( + AttributeDefinition.builder() + .attributeName(SingleUsePreKeyStore.KEY_ACCOUNT_UUID) + .attributeType(ScalarAttributeType.B) + .build(), + AttributeDefinition.builder() + .attributeName(SingleUsePreKeyStore.KEY_DEVICE_ID_KEY_ID) + .attributeType(ScalarAttributeType.B) + .build()), + List.of(), List.of()), + + PQ_KEYS("pq_keys_test", + SingleUsePreKeyStore.KEY_ACCOUNT_UUID, + SingleUsePreKeyStore.KEY_DEVICE_ID_KEY_ID, + List.of( + AttributeDefinition.builder() + .attributeName(SingleUsePreKeyStore.KEY_ACCOUNT_UUID) + .attributeType(ScalarAttributeType.B) + .build(), + AttributeDefinition.builder() + .attributeName(SingleUsePreKeyStore.KEY_DEVICE_ID_KEY_ID) + .attributeType(ScalarAttributeType.B) + .build()), + List.of(), List.of()), + + REPEATED_USE_EC_SIGNED_PRE_KEYS("repeated_use_signed_ec_pre_keys_test", + RepeatedUseSignedPreKeyStore.KEY_ACCOUNT_UUID, + RepeatedUseSignedPreKeyStore.KEY_DEVICE_ID, + List.of( + AttributeDefinition.builder() + .attributeName(RepeatedUseSignedPreKeyStore.KEY_ACCOUNT_UUID) + .attributeType(ScalarAttributeType.B) + .build(), + AttributeDefinition.builder() + .attributeName(RepeatedUseSignedPreKeyStore.KEY_DEVICE_ID) + .attributeType(ScalarAttributeType.N) + .build()), + List.of(), List.of()), + + REPEATED_USE_KEM_SIGNED_PRE_KEYS("repeated_use_signed_kem_pre_keys_test", + RepeatedUseSignedPreKeyStore.KEY_ACCOUNT_UUID, + RepeatedUseSignedPreKeyStore.KEY_DEVICE_ID, + List.of( + AttributeDefinition.builder() + .attributeName(RepeatedUseSignedPreKeyStore.KEY_ACCOUNT_UUID) + .attributeType(ScalarAttributeType.B) + .build(), + AttributeDefinition.builder() + .attributeName(RepeatedUseSignedPreKeyStore.KEY_DEVICE_ID) + .attributeType(ScalarAttributeType.N) + .build()), + List.of(), List.of()), + + PNI("pni_test", + PhoneNumberIdentifiers.KEY_E164, + null, + List.of( + AttributeDefinition.builder() + .attributeName(PhoneNumberIdentifiers.KEY_E164) + .attributeType(ScalarAttributeType.S) + .build(), + AttributeDefinition.builder() + .attributeName(PhoneNumberIdentifiers.ATTR_PHONE_NUMBER_IDENTIFIER) + .attributeType(ScalarAttributeType.B) + .build()), + List.of(GlobalSecondaryIndex.builder() + .indexName(PhoneNumberIdentifiers.INDEX_NAME) + .projection(Projection.builder() + .projectionType(ProjectionType.KEYS_ONLY) + .build()) + .keySchema(KeySchemaElement.builder().keyType(KeyType.HASH) + .attributeName(PhoneNumberIdentifiers.ATTR_PHONE_NUMBER_IDENTIFIER) + .build()) + .provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build()) + .build()), + List.of()), + + PNI_ASSIGNMENTS("pni_assignment_test", + Accounts.ATTR_PNI_UUID, + null, + List.of(AttributeDefinition.builder() + .attributeName(Accounts.ATTR_PNI_UUID) + .attributeType(ScalarAttributeType.B) + .build()), + List.of(), List.of()), + + ISSUED_RECEIPTS("issued_receipts_test", + IssuedReceiptsManager.KEY_PROCESSOR_ITEM_ID, + null, + List.of(AttributeDefinition.builder() + .attributeName(IssuedReceiptsManager.KEY_PROCESSOR_ITEM_ID) + .attributeType(ScalarAttributeType.S) + .build()), + List.of(), List.of()), + + MESSAGES("messages_test", + MessagesDynamoDb.KEY_PARTITION, + MessagesDynamoDb.KEY_SORT, + List.of( + AttributeDefinition.builder().attributeName(MessagesDynamoDb.KEY_PARTITION).attributeType(ScalarAttributeType.B).build(), + AttributeDefinition.builder().attributeName(MessagesDynamoDb.KEY_SORT).attributeType(ScalarAttributeType.B).build(), + AttributeDefinition.builder().attributeName(MessagesDynamoDb.LOCAL_INDEX_MESSAGE_UUID_KEY_SORT) + .attributeType(ScalarAttributeType.B).build()), + List.of(), + List.of(LocalSecondaryIndex.builder() + .indexName(MessagesDynamoDb.LOCAL_INDEX_MESSAGE_UUID_NAME) + .keySchema( + KeySchemaElement.builder().attributeName(MessagesDynamoDb.KEY_PARTITION).keyType(KeyType.HASH).build(), + KeySchemaElement.builder() + .attributeName(MessagesDynamoDb.LOCAL_INDEX_MESSAGE_UUID_KEY_SORT) + .keyType(KeyType.RANGE) + .build()) + .projection(Projection.builder().projectionType(ProjectionType.KEYS_ONLY).build()) + .build())), + + PROFILES("profiles_test", + Profiles.KEY_ACCOUNT_UUID, + Profiles.ATTR_VERSION, + List.of( + AttributeDefinition.builder() + .attributeName(Profiles.KEY_ACCOUNT_UUID) + .attributeType(ScalarAttributeType.B) + .build(), + AttributeDefinition.builder() + .attributeName(Profiles.ATTR_VERSION) + .attributeType(ScalarAttributeType.S) + .build()), + List.of(), List.of()), + + PUSH_CHALLENGES("push_challenge_test", + PushChallengeDynamoDb.KEY_ACCOUNT_UUID, + null, + List.of(AttributeDefinition.builder() + .attributeName(PushChallengeDynamoDb.KEY_ACCOUNT_UUID) + .attributeType(ScalarAttributeType.B) + .build()), + List.of(), List.of()), + + REDEEMED_RECEIPTS("redeemed_receipts_test", + RedeemedReceiptsManager.KEY_SERIAL, + null, + List.of(AttributeDefinition.builder() + .attributeName(RedeemedReceiptsManager.KEY_SERIAL) + .attributeType(ScalarAttributeType.B) + .build()), + List.of(), List.of()), + + REGISTRATION_RECOVERY_PASSWORDS("registration_recovery_passwords_test", + RegistrationRecoveryPasswords.KEY_E164, + null, + List.of(AttributeDefinition.builder() + .attributeName(RegistrationRecoveryPasswords.KEY_E164) + .attributeType(ScalarAttributeType.S) + .build()), + List.of(), List.of()), + + REMOTE_CONFIGS("remote_configs_test", + RemoteConfigs.KEY_NAME, + null, + List.of(AttributeDefinition.builder() + .attributeName(RemoteConfigs.KEY_NAME) + .attributeType(ScalarAttributeType.S) + .build()), + List.of(), List.of()), + + REPORT_MESSAGES("report_messages_test", + ReportMessageDynamoDb.KEY_HASH, + null, + List.of(AttributeDefinition.builder() + .attributeName(ReportMessageDynamoDb.KEY_HASH) + .attributeType(ScalarAttributeType.B) + .build()), + List.of(), List.of()), + + SUBSCRIPTIONS("subscriptions_test", + SubscriptionManager.KEY_USER, + null, + List.of( + AttributeDefinition.builder() + .attributeName(SubscriptionManager.KEY_USER) + .attributeType(ScalarAttributeType.B) + .build(), + AttributeDefinition.builder() + .attributeName(SubscriptionManager.KEY_PROCESSOR_ID_CUSTOMER_ID) + .attributeType(ScalarAttributeType.B) + .build()), + List.of(GlobalSecondaryIndex.builder() + .indexName(SubscriptionManager.INDEX_NAME) + .keySchema(KeySchemaElement.builder() + .attributeName(SubscriptionManager.KEY_PROCESSOR_ID_CUSTOMER_ID) + .keyType(KeyType.HASH) + .build()) + .projection(Projection.builder() + .projectionType(ProjectionType.KEYS_ONLY) + .build()) + .provisionedThroughput(ProvisionedThroughput.builder() + .readCapacityUnits(20L) + .writeCapacityUnits(20L) + .build()) + .build()), + List.of()), + + USERNAMES("usernames_test", + Accounts.ATTR_USERNAME_HASH, + null, + List.of(AttributeDefinition.builder() + .attributeName(Accounts.ATTR_USERNAME_HASH) + .attributeType(ScalarAttributeType.B) + .build()), + List.of(), List.of()), + + VERIFICATION_SESSIONS("verification_sessions_test", + VerificationSessions.KEY_KEY, + null, + List.of(AttributeDefinition.builder() + .attributeName(VerificationSessions.KEY_KEY) + .attributeType(ScalarAttributeType.S) + .build()), + List.of(), List.of()); + + private final String tableName; + private final String hashKeyName; + private final String rangeKeyName; + private final List attributeDefinitions; + private final List globalSecondaryIndexes; + private final List localSecondaryIndexes; + + Tables( + final String tableName, + final String hashKeyName, + final String rangeKeyName, + final List attributeDefinitions, + final List globalSecondaryIndexes, + final List localSecondaryIndexes + ) { + this.tableName = tableName; + this.hashKeyName = hashKeyName; + this.rangeKeyName = rangeKeyName; + this.attributeDefinitions = attributeDefinitions; + this.globalSecondaryIndexes = globalSecondaryIndexes; + this.localSecondaryIndexes = localSecondaryIndexes; + } + + public String tableName() { + return tableName; + } + + public String hashKeyName() { + return hashKeyName; + } + + public String rangeKeyName() { + return rangeKeyName; + } + + public List attributeDefinitions() { + return attributeDefinitions; + } + + public List globalSecondaryIndexes() { + return globalSecondaryIndexes; + } + + public List localSecondaryIndexes() { + return localSecondaryIndexes; + } + + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/IssuedReceiptsManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/IssuedReceiptsManagerTest.java new file mode 100644 index 000000000..c3cf5fb80 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/IssuedReceiptsManagerTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.security.SecureRandom; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.CompletableFuture; +import javax.ws.rs.ClientErrorException; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequest; +import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; + +class IssuedReceiptsManagerTest { + + private static final long NOW_EPOCH_SECONDS = 1_500_000_000L; + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(Tables.ISSUED_RECEIPTS); + + ReceiptCredentialRequest receiptCredentialRequest; + IssuedReceiptsManager issuedReceiptsManager; + + @BeforeEach + void beforeEach() { + receiptCredentialRequest = mock(ReceiptCredentialRequest.class); + byte[] generator = new byte[16]; + SECURE_RANDOM.nextBytes(generator); + issuedReceiptsManager = new IssuedReceiptsManager( + Tables.ISSUED_RECEIPTS.tableName(), + Duration.ofDays(90), + DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + generator); + } + + @Test + void testRecordIssuance() { + Instant now = Instant.ofEpochSecond(NOW_EPOCH_SECONDS); + byte[] request1 = new byte[20]; + SECURE_RANDOM.nextBytes(request1); + when(receiptCredentialRequest.serialize()).thenReturn(request1); + CompletableFuture future = issuedReceiptsManager.recordIssuance("item-1", SubscriptionProcessor.STRIPE, + receiptCredentialRequest, now); + assertThat(future).succeedsWithin(Duration.ofSeconds(3)); + + // same request should succeed + future = issuedReceiptsManager.recordIssuance("item-1", SubscriptionProcessor.STRIPE, receiptCredentialRequest, + now); + assertThat(future).succeedsWithin(Duration.ofSeconds(3)); + + // same item with new request should fail + byte[] request2 = new byte[20]; + SECURE_RANDOM.nextBytes(request2); + when(receiptCredentialRequest.serialize()).thenReturn(request2); + future = issuedReceiptsManager.recordIssuance("item-1", SubscriptionProcessor.STRIPE, receiptCredentialRequest, + now); + assertThat(future).failsWithin(Duration.ofSeconds(3)). + withThrowableOfType(Throwable.class). + havingCause(). + isExactlyInstanceOf(ClientErrorException.class). + has(new Condition<>( + e -> e instanceof ClientErrorException && ((ClientErrorException) e).getResponse().getStatus() == 409, + "status 409")); + + // different item with new request should be okay though + future = issuedReceiptsManager.recordIssuance("item-2", SubscriptionProcessor.STRIPE, receiptCredentialRequest, + now); + assertThat(future).succeedsWithin(Duration.ofSeconds(3)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/KeysManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/KeysManagerTest.java new file mode 100644 index 000000000..dbd0abcd9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/KeysManagerTest.java @@ -0,0 +1,303 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicECPreKeyMigrationConfiguration; +import org.whispersystems.textsecuregcm.entities.ECPreKey; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; + +class KeysManagerTest { + + private DynamicECPreKeyMigrationConfiguration ecPreKeyMigrationConfiguration; + private KeysManager keysManager; + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension( + Tables.EC_KEYS, Tables.PQ_KEYS, Tables.REPEATED_USE_EC_SIGNED_PRE_KEYS, Tables.REPEATED_USE_KEM_SIGNED_PRE_KEYS); + + private static final UUID ACCOUNT_UUID = UUID.randomUUID(); + private static final long DEVICE_ID = 1L; + + private static final ECKeyPair IDENTITY_KEY_PAIR = Curve.generateKeyPair(); + + @BeforeEach + void setup() { + final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); + final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); + ecPreKeyMigrationConfiguration = mock(DynamicECPreKeyMigrationConfiguration.class); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + when(dynamicConfiguration.getEcPreKeyMigrationConfiguration()).thenReturn(ecPreKeyMigrationConfiguration); + when(ecPreKeyMigrationConfiguration.storeEcSignedPreKeys()).thenReturn(true); + when(ecPreKeyMigrationConfiguration.deleteEcSignedPreKeys()).thenReturn(true); + + keysManager = new KeysManager( + DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + Tables.EC_KEYS.tableName(), + Tables.PQ_KEYS.tableName(), + Tables.REPEATED_USE_EC_SIGNED_PRE_KEYS.tableName(), + Tables.REPEATED_USE_KEM_SIGNED_PRE_KEYS.tableName(), + dynamicConfigurationManager); + } + + @Test + void testStore() { + assertEquals(0, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join(), + "Initial pre-key count for an account should be zero"); + assertEquals(0, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join(), + "Initial pre-key count for an account should be zero"); + assertFalse(keysManager.getLastResort(ACCOUNT_UUID, DEVICE_ID).join().isPresent(), + "Initial last-resort pre-key for an account should be missing"); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID, List.of(generateTestPreKey(1)), null, null, null).join(); + assertEquals(1, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join()); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID, List.of(generateTestPreKey(1)), null, null, null).join(); + assertEquals(1, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join(), + "Repeatedly storing same key should have no effect"); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID, null, List.of(generateTestKEMSignedPreKey(1)), null, null).join(); + assertEquals(1, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join(), + "Uploading new PQ prekeys should have no effect on EC prekeys"); + assertEquals(1, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join()); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID, null, null, null, generateTestKEMSignedPreKey(1001)).join(); + assertEquals(1, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join(), + "Uploading new PQ last-resort prekey should have no effect on EC prekeys"); + assertEquals(1, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join(), + "Uploading new PQ last-resort prekey should have no effect on one-time PQ prekeys"); + assertEquals(1001, keysManager.getLastResort(ACCOUNT_UUID, DEVICE_ID).join().get().keyId()); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID, List.of(generateTestPreKey(2)), null, null, null).join(); + assertEquals(1, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join(), + "Inserting a new key should overwrite all prior keys of the same type for the given account/device"); + assertEquals(1, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join(), + "Uploading new EC prekeys should have no effect on PQ prekeys"); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID, List.of(generateTestPreKey(3)), List.of(generateTestKEMSignedPreKey(2)), null, null).join(); + assertEquals(1, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join(), + "Inserting a new key should overwrite all prior keys of the same type for the given account/device"); + assertEquals(1, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join(), + "Inserting a new key should overwrite all prior keys of the same type for the given account/device"); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID, + List.of(generateTestPreKey(4), generateTestPreKey(5)), + List.of(generateTestKEMSignedPreKey(6), generateTestKEMSignedPreKey(7)), null, generateTestKEMSignedPreKey(1002)).join(); + assertEquals(2, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join(), + "Inserting multiple new keys should overwrite all prior keys for the given account/device"); + assertEquals(2, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join(), + "Inserting multiple new keys should overwrite all prior keys for the given account/device"); + assertEquals(1002, keysManager.getLastResort(ACCOUNT_UUID, DEVICE_ID).join().get().keyId(), + "Uploading new last-resort key should overwrite prior last-resort key for the account/device"); + } + + @Test + void testTakeAccountAndDeviceId() { + assertEquals(Optional.empty(), keysManager.takeEC(ACCOUNT_UUID, DEVICE_ID).join()); + + final ECPreKey preKey = generateTestPreKey(1); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID, List.of(preKey, generateTestPreKey(2)), null, null, null).join(); + final Optional takenKey = keysManager.takeEC(ACCOUNT_UUID, DEVICE_ID).join(); + assertEquals(Optional.of(preKey), takenKey); + assertEquals(1, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join()); + } + + @Test + void testTakePQ() { + assertEquals(Optional.empty(), keysManager.takeEC(ACCOUNT_UUID, DEVICE_ID).join()); + + final KEMSignedPreKey preKey1 = generateTestKEMSignedPreKey(1); + final KEMSignedPreKey preKey2 = generateTestKEMSignedPreKey(2); + final KEMSignedPreKey preKeyLast = generateTestKEMSignedPreKey(1001); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID, null, List.of(preKey1, preKey2), null, preKeyLast).join(); + + assertEquals(Optional.of(preKey1), keysManager.takePQ(ACCOUNT_UUID, DEVICE_ID).join()); + assertEquals(1, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join()); + + assertEquals(Optional.of(preKey2), keysManager.takePQ(ACCOUNT_UUID, DEVICE_ID).join()); + assertEquals(0, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join()); + + assertEquals(Optional.of(preKeyLast), keysManager.takePQ(ACCOUNT_UUID, DEVICE_ID).join()); + assertEquals(0, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join()); + + assertEquals(Optional.of(preKeyLast), keysManager.takePQ(ACCOUNT_UUID, DEVICE_ID).join()); + assertEquals(0, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join()); + } + + @Test + void testGetCount() { + assertEquals(0, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join()); + assertEquals(0, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join()); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID, List.of(generateTestPreKey(1)), List.of(generateTestKEMSignedPreKey(1)), null, null).join(); + assertEquals(1, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join()); + assertEquals(1, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join()); + } + + @Test + void testDeleteByAccount() { + keysManager.store(ACCOUNT_UUID, DEVICE_ID, + List.of(generateTestPreKey(1), generateTestPreKey(2)), + List.of(generateTestKEMSignedPreKey(3), generateTestKEMSignedPreKey(4)), + generateTestECSignedPreKey(5), + generateTestKEMSignedPreKey(6)) + .join(); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID + 1, + List.of(generateTestPreKey(7)), + List.of(generateTestKEMSignedPreKey(8)), + generateTestECSignedPreKey(9), + generateTestKEMSignedPreKey(10)) + .join(); + + assertEquals(2, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join()); + assertEquals(2, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join()); + assertTrue(keysManager.getEcSignedPreKey(ACCOUNT_UUID, DEVICE_ID).join().isPresent()); + assertTrue(keysManager.getLastResort(ACCOUNT_UUID, DEVICE_ID).join().isPresent()); + assertEquals(1, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID + 1).join()); + assertEquals(1, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID + 1).join()); + assertTrue(keysManager.getEcSignedPreKey(ACCOUNT_UUID, DEVICE_ID + 1).join().isPresent()); + assertTrue(keysManager.getLastResort(ACCOUNT_UUID, DEVICE_ID + 1).join().isPresent()); + + keysManager.delete(ACCOUNT_UUID).join(); + + assertEquals(0, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join()); + assertEquals(0, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join()); + assertFalse(keysManager.getEcSignedPreKey(ACCOUNT_UUID, DEVICE_ID).join().isPresent()); + assertFalse(keysManager.getLastResort(ACCOUNT_UUID, DEVICE_ID).join().isPresent()); + assertEquals(0, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID + 1).join()); + assertEquals(0, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID + 1).join()); + assertFalse(keysManager.getEcSignedPreKey(ACCOUNT_UUID, DEVICE_ID + 1).join().isPresent()); + assertFalse(keysManager.getLastResort(ACCOUNT_UUID, DEVICE_ID + 1).join().isPresent()); + } + + @Test + void testDeleteByAccountAndDevice() { + keysManager.store(ACCOUNT_UUID, DEVICE_ID, + List.of(generateTestPreKey(1), generateTestPreKey(2)), + List.of(generateTestKEMSignedPreKey(3), generateTestKEMSignedPreKey(4)), + generateTestECSignedPreKey(5), + generateTestKEMSignedPreKey(6)) + .join(); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID + 1, + List.of(generateTestPreKey(7)), + List.of(generateTestKEMSignedPreKey(8)), + generateTestECSignedPreKey(9), + generateTestKEMSignedPreKey(10)) + .join(); + + assertEquals(2, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join()); + assertEquals(2, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join()); + assertTrue(keysManager.getEcSignedPreKey(ACCOUNT_UUID, DEVICE_ID).join().isPresent()); + assertTrue(keysManager.getLastResort(ACCOUNT_UUID, DEVICE_ID).join().isPresent()); + assertEquals(1, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID + 1).join()); + assertEquals(1, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID + 1).join()); + assertTrue(keysManager.getEcSignedPreKey(ACCOUNT_UUID, DEVICE_ID + 1).join().isPresent()); + assertTrue(keysManager.getLastResort(ACCOUNT_UUID, DEVICE_ID + 1).join().isPresent()); + + keysManager.delete(ACCOUNT_UUID, DEVICE_ID).join(); + + assertEquals(0, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join()); + assertEquals(0, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join()); + assertFalse(keysManager.getEcSignedPreKey(ACCOUNT_UUID, DEVICE_ID).join().isPresent()); + assertFalse(keysManager.getLastResort(ACCOUNT_UUID, DEVICE_ID).join().isPresent()); + assertEquals(1, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID + 1).join()); + assertEquals(1, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID + 1).join()); + assertTrue(keysManager.getEcSignedPreKey(ACCOUNT_UUID, DEVICE_ID + 1).join().isPresent()); + assertTrue(keysManager.getLastResort(ACCOUNT_UUID, DEVICE_ID + 1).join().isPresent()); + } + + @Test + void testStorePqLastResort() { + assertEquals(0, keysManager.getPqEnabledDevices(ACCOUNT_UUID).join().size()); + + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + + keysManager.storePqLastResort( + ACCOUNT_UUID, + Map.of(1L, KeysHelper.signedKEMPreKey(1, identityKeyPair), 2L, KeysHelper.signedKEMPreKey(2, identityKeyPair))).join(); + assertEquals(2, keysManager.getPqEnabledDevices(ACCOUNT_UUID).join().size()); + assertEquals(1L, keysManager.getLastResort(ACCOUNT_UUID, 1L).join().get().keyId()); + assertEquals(2L, keysManager.getLastResort(ACCOUNT_UUID, 2L).join().get().keyId()); + assertFalse(keysManager.getLastResort(ACCOUNT_UUID, 3L).join().isPresent()); + + keysManager.storePqLastResort( + ACCOUNT_UUID, + Map.of(1L, KeysHelper.signedKEMPreKey(3, identityKeyPair), 3L, KeysHelper.signedKEMPreKey(4, identityKeyPair))).join(); + assertEquals(3, keysManager.getPqEnabledDevices(ACCOUNT_UUID).join().size(), "storing new last-resort keys should not create duplicates"); + assertEquals(3L, keysManager.getLastResort(ACCOUNT_UUID, 1L).join().get().keyId(), "storing new last-resort keys should overwrite old ones"); + assertEquals(2L, keysManager.getLastResort(ACCOUNT_UUID, 2L).join().get().keyId(), "storing new last-resort keys should leave untouched ones alone"); + assertEquals(4L, keysManager.getLastResort(ACCOUNT_UUID, 3L).join().get().keyId(), "storing new last-resort keys should overwrite old ones"); + } + + @Test + void testGetPqEnabledDevices() { + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID, null, List.of(KeysHelper.signedKEMPreKey(1, identityKeyPair)), null, null).join(); + keysManager.store(ACCOUNT_UUID, DEVICE_ID + 1, null, null, null, KeysHelper.signedKEMPreKey(2, identityKeyPair)).join(); + keysManager.store(ACCOUNT_UUID, DEVICE_ID + 2, null, List.of(KeysHelper.signedKEMPreKey(3, identityKeyPair)), null, KeysHelper.signedKEMPreKey(4, identityKeyPair)).join(); + keysManager.store(ACCOUNT_UUID, DEVICE_ID + 3, null, null, null, null).join(); + assertIterableEquals( + Set.of(DEVICE_ID + 1, DEVICE_ID + 2), + Set.copyOf(keysManager.getPqEnabledDevices(ACCOUNT_UUID).join())); + } + + @Test + void testStoreEcSignedPreKeyDisabled() { + when(ecPreKeyMigrationConfiguration.storeEcSignedPreKeys()).thenReturn(false); + + final ECKeyPair identityKeyPair = Curve.generateKeyPair(); + + keysManager.store(ACCOUNT_UUID, DEVICE_ID, + List.of(generateTestPreKey(1)), + List.of(KeysHelper.signedKEMPreKey(2, identityKeyPair)), + KeysHelper.signedECPreKey(3, identityKeyPair), + KeysHelper.signedKEMPreKey(4, identityKeyPair)) + .join(); + + assertEquals(1, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID).join()); + assertEquals(1, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID).join()); + assertTrue(keysManager.getLastResort(ACCOUNT_UUID, DEVICE_ID).join().isPresent()); + assertFalse(keysManager.getEcSignedPreKey(ACCOUNT_UUID, DEVICE_ID).join().isPresent()); + } + + private static ECPreKey generateTestPreKey(final long keyId) { + return new ECPreKey(keyId, Curve.generateKeyPair().getPublicKey()); + } + + private static ECSignedPreKey generateTestECSignedPreKey(final long keyId) { + return KeysHelper.signedECPreKey(keyId, IDENTITY_KEY_PAIR); + } + + private static KEMSignedPreKey generateTestKEMSignedPreKey(final long keyId) { + return KeysHelper.signedKEMPreKey(keyId, IDENTITY_KEY_PAIR); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterIntegrationTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterIntegrationTest.java new file mode 100644 index 000000000..af832d581 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterIntegrationTest.java @@ -0,0 +1,191 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import io.lettuce.core.cluster.SlotHash; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.ScanRequest; + +class MessagePersisterIntegrationTest { + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(Tables.MESSAGES); + + @RegisterExtension + static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + private ExecutorService notificationExecutorService; + private Scheduler messageDeliveryScheduler; + private ExecutorService messageDeletionExecutorService; + private MessagesCache messagesCache; + private MessagesManager messagesManager; + private MessagePersister messagePersister; + private Account account; + + private static final Duration PERSIST_DELAY = Duration.ofMinutes(10); + + @BeforeEach + void setUp() throws Exception { + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster(connection -> { + connection.sync().flushall(); + connection.sync().upstream().commands().configSet("notify-keyspace-events", "K$glz"); + }); + + @SuppressWarnings("unchecked") final DynamicConfigurationManager dynamicConfigurationManager = + mock(DynamicConfigurationManager.class); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration()); + + messageDeliveryScheduler = Schedulers.newBoundedElastic(10, 10_000, "messageDelivery"); + messageDeletionExecutorService = Executors.newSingleThreadExecutor(); + final MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(DYNAMO_DB_EXTENSION.getDynamoDbClient(), + DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), Tables.MESSAGES.tableName(), Duration.ofDays(14), + messageDeletionExecutorService); + final AccountsManager accountsManager = mock(AccountsManager.class); + + notificationExecutorService = Executors.newSingleThreadExecutor(); + messagesCache = new MessagesCache(REDIS_CLUSTER_EXTENSION.getRedisCluster(), + REDIS_CLUSTER_EXTENSION.getRedisCluster(), notificationExecutorService, + messageDeliveryScheduler, messageDeletionExecutorService, Clock.systemUTC()); + messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, mock(ReportMessageManager.class), + messageDeletionExecutorService); + messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, + dynamicConfigurationManager, PERSIST_DELAY, 1); + + account = mock(Account.class); + + final UUID accountUuid = UUID.randomUUID(); + + when(account.getNumber()).thenReturn("+18005551234"); + when(account.getUuid()).thenReturn(accountUuid); + when(accountsManager.getByAccountIdentifier(accountUuid)).thenReturn(Optional.of(account)); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration()); + + messagesCache.start(); + } + + @AfterEach + void tearDown() throws Exception { + notificationExecutorService.shutdown(); + notificationExecutorService.awaitTermination(15, TimeUnit.SECONDS); + + messageDeletionExecutorService.shutdown(); + messageDeletionExecutorService.awaitTermination(15, TimeUnit.SECONDS); + + messageDeliveryScheduler.dispose(); + } + + @Test + void testScheduledPersistMessages() { + + final int messageCount = 377; + final List expectedMessages = new ArrayList<>(messageCount); + final Instant now = Instant.now(); + + assertTimeoutPreemptively(Duration.ofSeconds(15), () -> { + + for (int i = 0; i < messageCount; i++) { + final UUID messageGuid = UUID.randomUUID(); + final long timestamp = now.minus(PERSIST_DELAY.multipliedBy(2)).toEpochMilli() + i; + + final MessageProtos.Envelope message = generateRandomMessage(messageGuid, timestamp); + + messagesCache.insert(messageGuid, account.getUuid(), 1, message); + expectedMessages.add(message); + } + + REDIS_CLUSTER_EXTENSION.getRedisCluster() + .useCluster(connection -> connection.sync().set(MessagesCache.NEXT_SLOT_TO_PERSIST_KEY, + String.valueOf(SlotHash.getSlot(MessagesCache.getMessageQueueKey(account.getUuid(), 1)) - 1))); + + final AtomicBoolean messagesPersisted = new AtomicBoolean(false); + + messagesManager.addMessageAvailabilityListener(account.getUuid(), 1, new MessageAvailabilityListener() { + @Override + public boolean handleNewMessagesAvailable() { + return true; + } + + @Override + public boolean handleMessagesPersisted() { + synchronized (messagesPersisted) { + messagesPersisted.set(true); + messagesPersisted.notifyAll(); + return true; + } + } + }); + + messagePersister.start(); + + synchronized (messagesPersisted) { + while (!messagesPersisted.get()) { + messagesPersisted.wait(); + } + } + + messagePersister.stop(); + + DynamoDbClient dynamoDB = DYNAMO_DB_EXTENSION.getDynamoDbClient(); + + final List persistedMessages = + dynamoDB.scan(ScanRequest.builder().tableName(Tables.MESSAGES.tableName()).build()).items().stream() + .map(item -> { + try { + return MessagesDynamoDb.convertItemToEnvelope(item); + } catch (InvalidProtocolBufferException e) { + fail("Could not parse stored message", e); + return null; + } + }) + .toList(); + + assertEquals(expectedMessages, persistedMessages); + }); + } + + private MessageProtos.Envelope generateRandomMessage(final UUID messageGuid, final long serverTimestamp) { + return MessageProtos.Envelope.newBuilder() + .setTimestamp(serverTimestamp * 2) // client timestamp may not be accurate + .setServerTimestamp(serverTimestamp) + .setContent(ByteString.copyFromUtf8(RandomStringUtils.randomAlphanumeric(256))) + .setType(MessageProtos.Envelope.Type.CIPHERTEXT) + .setServerGuid(messageGuid.toString()) + .setDestinationUuid(UUID.randomUUID().toString()) + .build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterTest.java new file mode 100644 index 000000000..70259bfb8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagePersisterTest.java @@ -0,0 +1,268 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import io.lettuce.core.cluster.SlotHash; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +class MessagePersisterTest { + + @RegisterExtension + static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + private ExecutorService sharedExecutorService; + private ScheduledExecutorService resubscribeRetryExecutorService; + private Scheduler messageDeliveryScheduler; + private MessagesCache messagesCache; + private MessagesDynamoDb messagesDynamoDb; + private MessagePersister messagePersister; + private AccountsManager accountsManager; + private MessagesManager messagesManager; + + private static final UUID DESTINATION_ACCOUNT_UUID = UUID.randomUUID(); + private static final String DESTINATION_ACCOUNT_NUMBER = "+18005551234"; + private static final long DESTINATION_DEVICE_ID = 7; + + private static final Duration PERSIST_DELAY = Duration.ofMinutes(5); + + @BeforeEach + void setUp() throws Exception { + + messagesManager = mock(MessagesManager.class); + final DynamicConfigurationManager dynamicConfigurationManager = mock( + DynamicConfigurationManager.class); + + messagesDynamoDb = mock(MessagesDynamoDb.class); + accountsManager = mock(AccountsManager.class); + + final Account account = mock(Account.class); + + when(accountsManager.getByAccountIdentifier(DESTINATION_ACCOUNT_UUID)).thenReturn(Optional.of(account)); + when(account.getNumber()).thenReturn(DESTINATION_ACCOUNT_NUMBER); + when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration()); + + sharedExecutorService = Executors.newSingleThreadExecutor(); + resubscribeRetryExecutorService = Executors.newSingleThreadScheduledExecutor(); + messageDeliveryScheduler = Schedulers.newBoundedElastic(10, 10_000, "messageDelivery"); + messagesCache = new MessagesCache(REDIS_CLUSTER_EXTENSION.getRedisCluster(), + REDIS_CLUSTER_EXTENSION.getRedisCluster(), sharedExecutorService, messageDeliveryScheduler, + sharedExecutorService, Clock.systemUTC()); + messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, + dynamicConfigurationManager, PERSIST_DELAY, 1); + + doAnswer(invocation -> { + final UUID destinationUuid = invocation.getArgument(0); + final long destinationDeviceId = invocation.getArgument(1); + final List messages = invocation.getArgument(2); + + messagesDynamoDb.store(messages, destinationUuid, destinationDeviceId); + + for (final MessageProtos.Envelope message : messages) { + messagesCache.remove(destinationUuid, destinationDeviceId, UUID.fromString(message.getServerGuid())).get(); + } + + return null; + }).when(messagesManager).persistMessages(any(UUID.class), anyLong(), any()); + } + + @AfterEach + void tearDown() throws Exception { + sharedExecutorService.shutdown(); + sharedExecutorService.awaitTermination(1, TimeUnit.SECONDS); + + messageDeliveryScheduler.dispose(); + resubscribeRetryExecutorService.shutdown(); + resubscribeRetryExecutorService.awaitTermination(1, TimeUnit.SECONDS); + } + + @Test + void testPersistNextQueuesNoQueues() { + messagePersister.persistNextQueues(Instant.now()); + + verify(accountsManager, never()).getByAccountIdentifier(any(UUID.class)); + } + + @Test + void testPersistNextQueuesSingleQueue() { + final String queueName = new String( + MessagesCache.getMessageQueueKey(DESTINATION_ACCOUNT_UUID, DESTINATION_DEVICE_ID), StandardCharsets.UTF_8); + final int messageCount = (MessagePersister.MESSAGE_BATCH_LIMIT * 3) + 7; + final Instant now = Instant.now(); + + insertMessages(DESTINATION_ACCOUNT_UUID, DESTINATION_DEVICE_ID, messageCount, now); + setNextSlotToPersist(SlotHash.getSlot(queueName)); + + messagePersister.persistNextQueues(now.plus(messagePersister.getPersistDelay())); + + final ArgumentCaptor> messagesCaptor = ArgumentCaptor.forClass(List.class); + + verify(messagesDynamoDb, atLeastOnce()).store(messagesCaptor.capture(), eq(DESTINATION_ACCOUNT_UUID), + eq(DESTINATION_DEVICE_ID)); + assertEquals(messageCount, messagesCaptor.getAllValues().stream().mapToInt(List::size).sum()); + } + + @Test + void testPersistNextQueuesSingleQueueTooSoon() { + final String queueName = new String( + MessagesCache.getMessageQueueKey(DESTINATION_ACCOUNT_UUID, DESTINATION_DEVICE_ID), StandardCharsets.UTF_8); + final int messageCount = (MessagePersister.MESSAGE_BATCH_LIMIT * 3) + 7; + final Instant now = Instant.now(); + + insertMessages(DESTINATION_ACCOUNT_UUID, DESTINATION_DEVICE_ID, messageCount, now); + setNextSlotToPersist(SlotHash.getSlot(queueName)); + + messagePersister.persistNextQueues(now); + + verify(messagesDynamoDb, never()).store(any(), any(), anyLong()); + } + + @Test + void testPersistNextQueuesMultiplePages() { + final int slot = 7; + final int queueCount = (MessagePersister.QUEUE_BATCH_LIMIT * 3) + 7; + final int messagesPerQueue = 10; + final Instant now = Instant.now(); + + for (int i = 0; i < queueCount; i++) { + final String queueName = generateRandomQueueNameForSlot(slot); + final UUID accountUuid = MessagesCache.getAccountUuidFromQueueName(queueName); + final long deviceId = MessagesCache.getDeviceIdFromQueueName(queueName); + final String accountNumber = "+1" + RandomStringUtils.randomNumeric(10); + + final Account account = mock(Account.class); + + when(accountsManager.getByAccountIdentifier(accountUuid)).thenReturn(Optional.of(account)); + when(account.getNumber()).thenReturn(accountNumber); + + insertMessages(accountUuid, deviceId, messagesPerQueue, now); + } + + setNextSlotToPersist(slot); + + messagePersister.persistNextQueues(now.plus(messagePersister.getPersistDelay())); + + final ArgumentCaptor> messagesCaptor = ArgumentCaptor.forClass(List.class); + + verify(messagesDynamoDb, atLeastOnce()).store(messagesCaptor.capture(), any(UUID.class), anyLong()); + assertEquals(queueCount * messagesPerQueue, messagesCaptor.getAllValues().stream().mapToInt(List::size).sum()); + } + + @Test + void testPersistQueueRetry() { + final String queueName = new String( + MessagesCache.getMessageQueueKey(DESTINATION_ACCOUNT_UUID, DESTINATION_DEVICE_ID), StandardCharsets.UTF_8); + final int messageCount = (MessagePersister.MESSAGE_BATCH_LIMIT * 3) + 7; + final Instant now = Instant.now(); + + insertMessages(DESTINATION_ACCOUNT_UUID, DESTINATION_DEVICE_ID, messageCount, now); + setNextSlotToPersist(SlotHash.getSlot(queueName)); + + doAnswer((Answer) invocation -> { + throw new RuntimeException("OH NO."); + }).when(messagesDynamoDb).store(any(), eq(DESTINATION_ACCOUNT_UUID), eq(DESTINATION_DEVICE_ID)); + + messagePersister.persistNextQueues(now.plus(messagePersister.getPersistDelay())); + + assertEquals(List.of(queueName), + messagesCache.getQueuesToPersist(SlotHash.getSlot(queueName), + Instant.now().plus(messagePersister.getPersistDelay()), 1)); + } + + @Test + void testPersistQueueRetryLoop() { + final String queueName = new String( + MessagesCache.getMessageQueueKey(DESTINATION_ACCOUNT_UUID, DESTINATION_DEVICE_ID), StandardCharsets.UTF_8); + final int messageCount = (MessagePersister.MESSAGE_BATCH_LIMIT * 3) + 7; + final Instant now = Instant.now(); + + insertMessages(DESTINATION_ACCOUNT_UUID, DESTINATION_DEVICE_ID, messageCount, now); + setNextSlotToPersist(SlotHash.getSlot(queueName)); + + // returning `0` indicates something not working correctly + when(messagesManager.persistMessages(any(UUID.class), anyLong(), anyList())).thenReturn(0); + + assertTimeoutPreemptively(Duration.ofSeconds(1), () -> + assertThrows(MessagePersistenceException.class, + () -> messagePersister.persistQueue(DESTINATION_ACCOUNT_UUID, DESTINATION_DEVICE_ID))); + } + + @SuppressWarnings("SameParameterValue") + private static String generateRandomQueueNameForSlot(final int slot) { + final UUID uuid = UUID.randomUUID(); + + final String queueNameBase = "user_queue::{" + uuid + "::"; + + for (int deviceId = 0; deviceId < Integer.MAX_VALUE; deviceId++) { + final String queueName = queueNameBase + deviceId + "}"; + + if (SlotHash.getSlot(queueName) == slot) { + return queueName; + } + } + + throw new IllegalStateException("Could not find a queue name for slot " + slot); + } + + private void insertMessages(final UUID accountUuid, final long deviceId, final int messageCount, + final Instant firstMessageTimestamp) { + for (int i = 0; i < messageCount; i++) { + final UUID messageGuid = UUID.randomUUID(); + + final MessageProtos.Envelope envelope = MessageProtos.Envelope.newBuilder() + .setTimestamp(firstMessageTimestamp.toEpochMilli() + i) + .setServerTimestamp(firstMessageTimestamp.toEpochMilli() + i) + .setContent(ByteString.copyFromUtf8(RandomStringUtils.randomAlphanumeric(256))) + .setType(MessageProtos.Envelope.Type.CIPHERTEXT) + .setServerGuid(messageGuid.toString()) + .build(); + + messagesCache.insert(messageGuid, accountUuid, deviceId, envelope); + } + } + + private void setNextSlotToPersist(final int nextSlot) { + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster( + connection -> connection.sync().set(MessagesCache.NEXT_SLOT_TO_PERSIST_KEY, String.valueOf(nextSlot - 1))); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagesCacheTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagesCacheTest.java new file mode 100644 index 000000000..c9aff667d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagesCacheTest.java @@ -0,0 +1,761 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import io.lettuce.core.RedisFuture; +import io.lettuce.core.cluster.SlotHash; +import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; +import io.lettuce.core.cluster.api.reactive.RedisAdvancedClusterReactiveCommands; +import io.lettuce.core.protocol.AsyncCommand; +import io.lettuce.core.protocol.RedisCommand; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.reactivestreams.Publisher; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; +import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; + +class MessagesCacheTest { + + private final Random random = new Random(); + private long serialTimestamp = 0; + + @Nested + class WithRealCluster { + + @RegisterExtension + static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + private ExecutorService sharedExecutorService; + private ScheduledExecutorService resubscribeRetryExecutorService; + private Scheduler messageDeliveryScheduler; + private MessagesCache messagesCache; + + private static final UUID DESTINATION_UUID = UUID.randomUUID(); + + private static final int DESTINATION_DEVICE_ID = 7; + + @BeforeEach + void setUp() throws Exception { + + REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster(connection -> { + connection.sync().flushall(); + connection.sync().upstream().commands().configSet("notify-keyspace-events", "K$glz"); + }); + + sharedExecutorService = Executors.newSingleThreadExecutor(); + resubscribeRetryExecutorService = Executors.newSingleThreadScheduledExecutor(); + messageDeliveryScheduler = Schedulers.newBoundedElastic(10, 10_000, "messageDelivery"); + messagesCache = new MessagesCache(REDIS_CLUSTER_EXTENSION.getRedisCluster(), + REDIS_CLUSTER_EXTENSION.getRedisCluster(), sharedExecutorService, messageDeliveryScheduler, sharedExecutorService, Clock.systemUTC()); + + messagesCache.start(); + } + + @AfterEach + void tearDown() throws Exception { + messagesCache.stop(); + + sharedExecutorService.shutdown(); + sharedExecutorService.awaitTermination(1, TimeUnit.SECONDS); + + messageDeliveryScheduler.dispose(); + resubscribeRetryExecutorService.shutdown(); + resubscribeRetryExecutorService.awaitTermination(1, TimeUnit.SECONDS); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testInsert(final boolean sealedSender) { + final UUID messageGuid = UUID.randomUUID(); + assertTrue(messagesCache.insert(messageGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID, + generateRandomMessage(messageGuid, sealedSender)) > 0); + } + + @Test + void testDoubleInsertGuid() { + final UUID duplicateGuid = UUID.randomUUID(); + final MessageProtos.Envelope duplicateMessage = generateRandomMessage(duplicateGuid, false); + + final long firstId = messagesCache.insert(duplicateGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID, + duplicateMessage); + final long secondId = messagesCache.insert(duplicateGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID, + duplicateMessage); + + assertEquals(firstId, secondId); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testRemoveByUUID(final boolean sealedSender) throws Exception { + final UUID messageGuid = UUID.randomUUID(); + + assertEquals(Optional.empty(), + messagesCache.remove(DESTINATION_UUID, DESTINATION_DEVICE_ID, messageGuid).get(5, TimeUnit.SECONDS)); + + final MessageProtos.Envelope message = generateRandomMessage(messageGuid, sealedSender); + + messagesCache.insert(messageGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID, message); + final Optional maybeRemovedMessage = messagesCache.remove(DESTINATION_UUID, + DESTINATION_DEVICE_ID, messageGuid).get(5, TimeUnit.SECONDS); + + assertEquals(Optional.of(message), maybeRemovedMessage); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testRemoveBatchByUUID(final boolean sealedSender) throws Exception { + final int messageCount = 10; + + final List messagesToRemove = new ArrayList<>(messageCount); + final List messagesToPreserve = new ArrayList<>(messageCount); + + for (int i = 0; i < 10; i++) { + messagesToRemove.add(generateRandomMessage(UUID.randomUUID(), sealedSender)); + messagesToPreserve.add(generateRandomMessage(UUID.randomUUID(), sealedSender)); + } + + assertEquals(Collections.emptyList(), messagesCache.remove(DESTINATION_UUID, DESTINATION_DEVICE_ID, + messagesToRemove.stream().map(message -> UUID.fromString(message.getServerGuid())) + .collect(Collectors.toList())).get(5, TimeUnit.SECONDS)); + + for (final MessageProtos.Envelope message : messagesToRemove) { + messagesCache.insert(UUID.fromString(message.getServerGuid()), DESTINATION_UUID, DESTINATION_DEVICE_ID, + message); + } + + for (final MessageProtos.Envelope message : messagesToPreserve) { + messagesCache.insert(UUID.fromString(message.getServerGuid()), DESTINATION_UUID, DESTINATION_DEVICE_ID, + message); + } + + final List removedMessages = messagesCache.remove(DESTINATION_UUID, DESTINATION_DEVICE_ID, + messagesToRemove.stream().map(message -> UUID.fromString(message.getServerGuid())) + .collect(Collectors.toList())).get(5, TimeUnit.SECONDS); + + assertEquals(messagesToRemove, removedMessages); + assertEquals(messagesToPreserve, + messagesCache.getMessagesToPersist(DESTINATION_UUID, DESTINATION_DEVICE_ID, messageCount)); + } + + @Test + void testHasMessages() { + assertFalse(messagesCache.hasMessages(DESTINATION_UUID, DESTINATION_DEVICE_ID)); + + final UUID messageGuid = UUID.randomUUID(); + final MessageProtos.Envelope message = generateRandomMessage(messageGuid, true); + messagesCache.insert(messageGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID, message); + + assertTrue(messagesCache.hasMessages(DESTINATION_UUID, DESTINATION_DEVICE_ID)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testGetMessages(final boolean sealedSender) throws Exception { + final int messageCount = 100; + + final List expectedMessages = new ArrayList<>(messageCount); + + for (int i = 0; i < messageCount; i++) { + final UUID messageGuid = UUID.randomUUID(); + final MessageProtos.Envelope message = generateRandomMessage(messageGuid, sealedSender); + messagesCache.insert(messageGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID, message); + + expectedMessages.add(message); + } + + assertEquals(expectedMessages, get(DESTINATION_UUID, DESTINATION_DEVICE_ID, messageCount)); + + messagesCache.remove(DESTINATION_UUID, DESTINATION_DEVICE_ID, + expectedMessages.stream() + .map(MessageProtos.Envelope::getServerGuid) + .map(UUID::fromString) + .collect(Collectors.toList())); + + final UUID message1Guid = UUID.randomUUID(); + final MessageProtos.Envelope message1 = generateRandomMessage(message1Guid, sealedSender); + messagesCache.insert(message1Guid, DESTINATION_UUID, DESTINATION_DEVICE_ID, message1); + final List get1 = get(DESTINATION_UUID, DESTINATION_DEVICE_ID, + 1); + assertEquals(List.of(message1), get1); + + messagesCache.remove(DESTINATION_UUID, DESTINATION_DEVICE_ID, message1Guid).get(5, TimeUnit.SECONDS); + + final UUID message2Guid = UUID.randomUUID(); + final MessageProtos.Envelope message2 = generateRandomMessage(message2Guid, sealedSender); + + messagesCache.insert(message2Guid, DESTINATION_UUID, DESTINATION_DEVICE_ID, message2); + + assertEquals(List.of(message2), get(DESTINATION_UUID, DESTINATION_DEVICE_ID, 1)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testGetMessagesPublisher(final boolean expectStale) throws Exception { + final int messageCount = 214; + + final List expectedMessages = new ArrayList<>(messageCount); + + for (int i = 0; i < messageCount; i++) { + final UUID messageGuid = UUID.randomUUID(); + final MessageProtos.Envelope message = generateRandomMessage(messageGuid, true); + messagesCache.insert(messageGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID, message); + + expectedMessages.add(message); + } + + final UUID ephemeralMessageGuid = UUID.randomUUID(); + final MessageProtos.Envelope ephemeralMessage = generateRandomMessage(ephemeralMessageGuid, true) + .toBuilder().setEphemeral(true).build(); + messagesCache.insert(ephemeralMessageGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID, ephemeralMessage); + + final Clock cacheClock; + if (expectStale) { + cacheClock = Clock.fixed(Instant.ofEpochMilli(serialTimestamp + 1), + ZoneId.of("Etc/UTC")); + } else { + cacheClock = Clock.fixed( + Instant.ofEpochMilli(serialTimestamp + 1).plus(MessagesCache.MAX_EPHEMERAL_MESSAGE_DELAY), + ZoneId.of("Etc/UTC")); + } + + final MessagesCache messagesCache = new MessagesCache(REDIS_CLUSTER_EXTENSION.getRedisCluster(), + REDIS_CLUSTER_EXTENSION.getRedisCluster(), sharedExecutorService, messageDeliveryScheduler, sharedExecutorService, cacheClock); + + final List actualMessages = Flux.from( + messagesCache.get(DESTINATION_UUID, DESTINATION_DEVICE_ID)) + .collectList() + .block(Duration.ofSeconds(5)); + + if (expectStale) { + final List expectedAllMessages = new ArrayList<>() {{ + addAll(expectedMessages); + add(ephemeralMessage); + }}; + + assertEquals(expectedAllMessages, actualMessages); + + } else { + assertEquals(expectedMessages, actualMessages); + + // delete all of these messages and call `getAll()`, to confirm that ephemeral messages have been discarded + CompletableFuture.allOf(actualMessages.stream() + .map(message -> messagesCache.remove(DESTINATION_UUID, DESTINATION_DEVICE_ID, + UUID.fromString(message.getServerGuid()))) + .toArray(CompletableFuture[]::new)) + .get(5, TimeUnit.SECONDS); + + final List messages = messagesCache.getAllMessages(DESTINATION_UUID, + DESTINATION_DEVICE_ID) + .collectList() + .toFuture().get(5, TimeUnit.SECONDS); + + assertTrue(messages.isEmpty()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testClearQueueForDevice(final boolean sealedSender) { + final int messageCount = 100; + + for (final int deviceId : new int[]{DESTINATION_DEVICE_ID, DESTINATION_DEVICE_ID + 1}) { + for (int i = 0; i < messageCount; i++) { + final UUID messageGuid = UUID.randomUUID(); + final MessageProtos.Envelope message = generateRandomMessage(messageGuid, sealedSender); + + messagesCache.insert(messageGuid, DESTINATION_UUID, deviceId, message); + } + } + + messagesCache.clear(DESTINATION_UUID, DESTINATION_DEVICE_ID).join(); + + assertEquals(Collections.emptyList(), get(DESTINATION_UUID, DESTINATION_DEVICE_ID, messageCount)); + assertEquals(messageCount, get(DESTINATION_UUID, DESTINATION_DEVICE_ID + 1, messageCount).size()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testClearQueueForAccount(final boolean sealedSender) { + final int messageCount = 100; + + for (final int deviceId : new int[]{DESTINATION_DEVICE_ID, DESTINATION_DEVICE_ID + 1}) { + for (int i = 0; i < messageCount; i++) { + final UUID messageGuid = UUID.randomUUID(); + final MessageProtos.Envelope message = generateRandomMessage(messageGuid, sealedSender); + + messagesCache.insert(messageGuid, DESTINATION_UUID, deviceId, message); + } + } + + messagesCache.clear(DESTINATION_UUID).join(); + + assertEquals(Collections.emptyList(), get(DESTINATION_UUID, DESTINATION_DEVICE_ID, messageCount)); + assertEquals(Collections.emptyList(), get(DESTINATION_UUID, DESTINATION_DEVICE_ID + 1, messageCount)); + } + + @Test + void testGetAccountFromQueueName() { + assertEquals(DESTINATION_UUID, + MessagesCache.getAccountUuidFromQueueName( + new String(MessagesCache.getMessageQueueKey(DESTINATION_UUID, DESTINATION_DEVICE_ID), + StandardCharsets.UTF_8))); + } + + @Test + void testGetDeviceIdFromQueueName() { + assertEquals(DESTINATION_DEVICE_ID, + MessagesCache.getDeviceIdFromQueueName( + new String(MessagesCache.getMessageQueueKey(DESTINATION_UUID, DESTINATION_DEVICE_ID), + StandardCharsets.UTF_8))); + } + + @Test + void testGetQueueNameFromKeyspaceChannel() { + assertEquals("1b363a31-a429-4fb6-8959-984a025e72ff::7", + MessagesCache.getQueueNameFromKeyspaceChannel( + "__keyspace@0__:user_queue::{1b363a31-a429-4fb6-8959-984a025e72ff::7}")); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testGetQueuesToPersist(final boolean sealedSender) { + final UUID messageGuid = UUID.randomUUID(); + + messagesCache.insert(messageGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID, + generateRandomMessage(messageGuid, sealedSender)); + final int slot = SlotHash.getSlot(DESTINATION_UUID + "::" + DESTINATION_DEVICE_ID); + + assertTrue(messagesCache.getQueuesToPersist(slot + 1, Instant.now().plusSeconds(60), 100).isEmpty()); + + final List queues = messagesCache.getQueuesToPersist(slot, Instant.now().plusSeconds(60), 100); + + assertEquals(1, queues.size()); + assertEquals(DESTINATION_UUID, MessagesCache.getAccountUuidFromQueueName(queues.get(0))); + assertEquals(DESTINATION_DEVICE_ID, MessagesCache.getDeviceIdFromQueueName(queues.get(0))); + } + + @Test + void testNotifyListenerNewMessage() { + final AtomicBoolean notified = new AtomicBoolean(false); + final UUID messageGuid = UUID.randomUUID(); + + final MessageAvailabilityListener listener = new MessageAvailabilityListener() { + @Override + public boolean handleNewMessagesAvailable() { + synchronized (notified) { + notified.set(true); + notified.notifyAll(); + + return true; + } + } + + @Override + public boolean handleMessagesPersisted() { + return true; + } + }; + + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + messagesCache.addMessageAvailabilityListener(DESTINATION_UUID, DESTINATION_DEVICE_ID, listener); + messagesCache.insert(messageGuid, DESTINATION_UUID, DESTINATION_DEVICE_ID, + generateRandomMessage(messageGuid, true)); + + synchronized (notified) { + while (!notified.get()) { + notified.wait(); + } + } + + assertTrue(notified.get()); + }); + } + + @Test + void testNotifyListenerPersisted() { + final AtomicBoolean notified = new AtomicBoolean(false); + + final MessageAvailabilityListener listener = new MessageAvailabilityListener() { + @Override + public boolean handleNewMessagesAvailable() { + return true; + } + + @Override + public boolean handleMessagesPersisted() { + synchronized (notified) { + notified.set(true); + notified.notifyAll(); + + return true; + } + } + }; + + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + messagesCache.addMessageAvailabilityListener(DESTINATION_UUID, DESTINATION_DEVICE_ID, listener); + + messagesCache.lockQueueForPersistence(DESTINATION_UUID, DESTINATION_DEVICE_ID); + messagesCache.unlockQueueForPersistence(DESTINATION_UUID, DESTINATION_DEVICE_ID); + + synchronized (notified) { + while (!notified.get()) { + notified.wait(); + } + } + + assertTrue(notified.get()); + }); + } + + + /** + * Helper class that implements {@link MessageAvailabilityListener#handleNewMessagesAvailable()} by always returning + * {@code false}. Its {@code counter} field tracks how many times {@code handleNewMessagesAvailable} has been + * called. + *

    + * It uses a {@link CompletableFuture} to signal that it has received a “messages available” callback for the first + * time. + */ + private static class NewMessagesAvailabilityClosedListener implements MessageAvailabilityListener { + + private int counter; + + private final Consumer messageHandledCallback; + private final CompletableFuture firstMessageHandled = new CompletableFuture<>(); + + private NewMessagesAvailabilityClosedListener(final Consumer messageHandledCallback) { + this.messageHandledCallback = messageHandledCallback; + } + + @Override + public boolean handleNewMessagesAvailable() { + counter++; + messageHandledCallback.accept(counter); + firstMessageHandled.complete(null); + + return false; + + } + + @Override + public boolean handleMessagesPersisted() { + return true; + } + } + + @Test + void testAvailabilityListenerResponses() { + final NewMessagesAvailabilityClosedListener listener1 = new NewMessagesAvailabilityClosedListener( + count -> assertEquals(1, count)); + final NewMessagesAvailabilityClosedListener listener2 = new NewMessagesAvailabilityClosedListener( + count -> assertEquals(1, count)); + + assertTimeoutPreemptively(Duration.ofSeconds(30), () -> { + messagesCache.addMessageAvailabilityListener(DESTINATION_UUID, DESTINATION_DEVICE_ID, listener1); + final UUID messageGuid1 = UUID.randomUUID(); + messagesCache.insert(messageGuid1, DESTINATION_UUID, DESTINATION_DEVICE_ID, + generateRandomMessage(messageGuid1, true)); + + listener1.firstMessageHandled.get(); + + // Avoid a race condition by blocking on the message handled future *and* the current notification executor task— + // the notification executor task includes unsubscribing `listener1`, and, if we don’t wait, sometimes + // `listener2` will get subscribed before `listener1` is cleaned up + sharedExecutorService.submit(() -> listener1.firstMessageHandled.get()).get(); + + final UUID messageGuid2 = UUID.randomUUID(); + messagesCache.insert(messageGuid2, DESTINATION_UUID, DESTINATION_DEVICE_ID, + generateRandomMessage(messageGuid2, true)); + + messagesCache.addMessageAvailabilityListener(DESTINATION_UUID, DESTINATION_DEVICE_ID, listener2); + + final UUID messageGuid3 = UUID.randomUUID(); + messagesCache.insert(messageGuid3, DESTINATION_UUID, DESTINATION_DEVICE_ID, + generateRandomMessage(messageGuid3, true)); + + listener2.firstMessageHandled.get(); + }); + } + + private List get(final UUID destinationUuid, final long destinationDeviceId, + final int messageCount) { + return Flux.from(messagesCache.get(destinationUuid, destinationDeviceId)) + .take(messageCount, true) + .collectList() + .block(); + } + + } + + @Nested + class WithMockCluster { + + private MessagesCache messagesCache; + private RedisAdvancedClusterReactiveCommands reactiveCommands; + private RedisAdvancedClusterAsyncCommands asyncCommands; + private Scheduler messageDeliveryScheduler; + + @SuppressWarnings("unchecked") + @BeforeEach + void setup() throws Exception { + reactiveCommands = mock(RedisAdvancedClusterReactiveCommands.class); + asyncCommands = mock(RedisAdvancedClusterAsyncCommands.class); + final FaultTolerantRedisCluster mockCluster = RedisClusterHelper.builder() + .binaryReactiveCommands(reactiveCommands) + .binaryAsyncCommands(asyncCommands) + .build(); + + messageDeliveryScheduler = Schedulers.newBoundedElastic(10, 10_000, "messageDelivery"); + + messagesCache = new MessagesCache(mockCluster, mockCluster, mock(ExecutorService.class), + messageDeliveryScheduler, Executors.newSingleThreadExecutor(), Clock.systemUTC()); + } + + @AfterEach + void teardown() { + StepVerifier.resetDefaultTimeout(); + messageDeliveryScheduler.dispose(); + } + + @Test + @Disabled("flaky test") + void testGetAllMessagesLimitsAndBackpressure() { + // this test makes sure that we don’t fetch and buffer all messages from the cache when the publisher + // is subscribed. Rather, we should be fetching in pages to satisfy downstream requests, so that memory usage + // is limited to few pages of messages + + // we use a combination of Flux.just() and TestPublishers to control when data is “fetched” and emitted from the + // cache. The initial Flux.just()s are pages that are readily available, on demand. By design, there are more of + // these pages than the initial prefetch. The publishers allow us to create extra demand but defer producing + // values to satisfy the demand until later on. + + final TestPublisher page4Publisher = TestPublisher.create(); + final TestPublisher page56Publisher = TestPublisher.create(); + final TestPublisher emptyFinalPagePublisher = TestPublisher.create(); + + final Deque> pages = new ArrayDeque<>(); + pages.add(generatePage()); + pages.add(generatePage()); + pages.add(generatePage()); + pages.add(generatePage()); + // make sure that stale ephemeral messages are also produced by calls to getAllMessages() + pages.add(generateStaleEphemeralPage()); + pages.add(generatePage()); + + when(reactiveCommands.evalsha(any(), any(), any(), any())) + .thenReturn(Flux.just(pages.pop())) + .thenReturn(Flux.just(pages.pop())) + .thenReturn(Flux.just(pages.pop())) + .thenReturn(Flux.from(page4Publisher)) + .thenReturn(Flux.from(page56Publisher)) + .thenReturn(Flux.from(emptyFinalPagePublisher)) + .thenReturn(Flux.empty()); + + final Flux allMessages = messagesCache.getAllMessages(UUID.randomUUID(), 1L); + + // Why initialValue = 3? + // 1. messagesCache.getAllMessages() above produces the first call + // 2. when we subscribe, the prefetch of 1 results in `expand()`, which produces a second call + // 3. there is an implicit “low tide mark” of 1, meaning there will be an extra call to replenish when there is + // 1 value remaining + final AtomicInteger expectedReactiveCommandInvocations = new AtomicInteger(3); + + StepVerifier.setDefaultTimeout(Duration.ofSeconds(5)); + + final int page = 100; + final int halfPage = page / 2; + + // in order to fully control demand and separate the prefetch mechanics, initially subscribe with a request of 0 + StepVerifier.create(allMessages, 0) + .expectSubscription() + .then(() -> verify(reactiveCommands, times(expectedReactiveCommandInvocations.get())).evalsha(any(), any(), + any(), any())) + .thenRequest(halfPage) // page 0.5 requested + .expectNextCount(halfPage) // page 0.5 produced + // page 0.5 produced, 1.5 remain, so no additional interactions with the cache cluster + .then(() -> verify(reactiveCommands, atMost(expectedReactiveCommandInvocations.get())).evalsha(any(), + any(), any(), any())) + .then(page4Publisher::assertWasNotRequested) + .thenRequest(page) // page 1.5 requested + .expectNextCount(page) // page 1.5 produced + + // we now have produced 1.5 pages, have 0.5 buffered, and two more have been prefetched. + // after producing more than a full page, we’ll need to replenish from the cache. + // future requests will depend on sink emitters. + // also NB: times() checks cumulative calls, hence addAndGet + .then(() -> verify(reactiveCommands, times(expectedReactiveCommandInvocations.addAndGet(1))).evalsha(any(), + any(), any(), any())) + .then(page4Publisher::assertWasSubscribed) + .thenRequest(page + halfPage) // page 3 requested + .expectNextCount(page + halfPage) // page 1.5–3 produced + + .thenRequest(halfPage) // page 3.5 requested + .then(page56Publisher::assertWasNotRequested) + .then(() -> page4Publisher.emit(pages.pop())) + .expectNextCount(halfPage) // page 3.5 produced + .then(() -> verify(reactiveCommands, times(expectedReactiveCommandInvocations.addAndGet(1))).evalsha(any(), + any(), any(), any())) + .then(page56Publisher::assertWasSubscribed) + + .thenRequest(page) // page 4.5 requested + .expectNextCount(halfPage) // page 4 produced + + .thenRequest(page * 4) // request more demand than we will ultimately satisfy + + .then(() -> page56Publisher.next(pages.pop()).next(pages.pop()).complete()) + .expectNextCount(page + page) // page 5 and 6 produced + .then(emptyFinalPagePublisher::complete) + // confirm that cache calls increased by 2: one for page 5-and-6 (we got a two-fer in next(pop()).next(pop()), + // and one for the final, empty page + .then(() -> verify(reactiveCommands, times(expectedReactiveCommandInvocations.addAndGet(2))).evalsha(any(), + any(), any(), + any())) + .expectComplete() + .log() + .verify(); + + // make sure that we consumed all the pages, especially in case of refactoring + assertTrue(pages.isEmpty()); + } + + @Test + void testGetDiscardsEphemeralMessages() { + final Deque> pages = new ArrayDeque<>(); + pages.add(generatePage()); + pages.add(generatePage()); + pages.add(generateStaleEphemeralPage()); + + when(reactiveCommands.evalsha(any(), any(), any(), any())) + .thenReturn(Flux.just(pages.pop())) + .thenReturn(Flux.just(pages.pop())) + .thenReturn(Flux.just(pages.pop())) + .thenReturn(Flux.empty()); + + final AsyncCommand removeSuccess = new AsyncCommand<>(mock(RedisCommand.class)); + removeSuccess.complete(); + + when(asyncCommands.evalsha(any(), any(), any(), any())) + .thenReturn((RedisFuture) removeSuccess); + + final Publisher allMessages = messagesCache.get(UUID.randomUUID(), 1L); + + StepVerifier.setDefaultTimeout(Duration.ofSeconds(5)); + + // async commands are used for remove(), and nothing should happen until we are subscribed + verify(asyncCommands, never()).evalsha(any(), any(), any(byte[][].class), any(byte[].class)); + // the reactive commands will be called once, to prep the first page fetch (but no remote request would actually be sent) + verify(reactiveCommands, times(1)).evalsha(any(), any(), any(byte[][].class), any(byte[].class)); + + StepVerifier.create(allMessages) + .expectSubscription() + .expectNextCount(200) + .expectComplete() + .log() + .verify(); + + assertTrue(pages.isEmpty()); + verify(asyncCommands, atLeast(1)).evalsha(any(), any(), any(), any()); + } + + private List generatePage() { + final List messagesAndIds = new ArrayList<>(); + + for (int i = 0; i < 100; i++) { + final MessageProtos.Envelope envelope = generateRandomMessage(UUID.randomUUID(), true); + messagesAndIds.add(envelope.toByteArray()); + messagesAndIds.add(String.valueOf(serialTimestamp).getBytes()); + } + + return messagesAndIds; + } + + private List generateStaleEphemeralPage() { + final List messagesAndIds = new ArrayList<>(); + + for (int i = 0; i < 100; i++) { + final MessageProtos.Envelope envelope = generateRandomMessage(UUID.randomUUID(), true) + .toBuilder().setEphemeral(true).build(); + messagesAndIds.add(envelope.toByteArray()); + messagesAndIds.add(String.valueOf(serialTimestamp).getBytes()); + } + + return messagesAndIds; + } + } + + private MessageProtos.Envelope generateRandomMessage(final UUID messageGuid, final boolean sealedSender) { + return generateRandomMessage(messageGuid, sealedSender, serialTimestamp++); + } + + private MessageProtos.Envelope generateRandomMessage(final UUID messageGuid, final boolean sealedSender, + final long timestamp) { + final MessageProtos.Envelope.Builder envelopeBuilder = MessageProtos.Envelope.newBuilder() + .setTimestamp(timestamp) + .setServerTimestamp(timestamp) + .setContent(ByteString.copyFromUtf8(RandomStringUtils.randomAlphanumeric(256))) + .setType(MessageProtos.Envelope.Type.CIPHERTEXT) + .setServerGuid(messageGuid.toString()) + .setDestinationUuid(UUID.randomUUID().toString()); + + if (!sealedSender) { + envelopeBuilder.setSourceDevice(random.nextInt(256)) + .setSourceUuid(UUID.randomUUID().toString()); + } + + return envelopeBuilder.build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagesDynamoDbTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagesDynamoDbTest.java new file mode 100644 index 000000000..0f3c7dfca --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagesDynamoDbTest.java @@ -0,0 +1,298 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.protobuf.ByteString; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.reactivestreams.Publisher; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import org.whispersystems.textsecuregcm.tests.util.MessageHelper; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +class MessagesDynamoDbTest { + + + private static final Random random = new Random(); + private static final MessageProtos.Envelope MESSAGE1; + private static final MessageProtos.Envelope MESSAGE2; + private static final MessageProtos.Envelope MESSAGE3; + + static { + final long serverTimestamp = System.currentTimeMillis(); + MessageProtos.Envelope.Builder builder = MessageProtos.Envelope.newBuilder(); + builder.setType(MessageProtos.Envelope.Type.UNIDENTIFIED_SENDER); + builder.setTimestamp(123456789L); + builder.setContent(ByteString.copyFrom(new byte[]{(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF})); + builder.setServerGuid(UUID.randomUUID().toString()); + builder.setServerTimestamp(serverTimestamp); + builder.setDestinationUuid(UUID.randomUUID().toString()); + + MESSAGE1 = builder.build(); + + builder.setType(MessageProtos.Envelope.Type.CIPHERTEXT); + builder.setSourceUuid(UUID.randomUUID().toString()); + builder.setSourceDevice(1); + builder.setContent(ByteString.copyFromUtf8("MOO")); + builder.setServerGuid(UUID.randomUUID().toString()); + builder.setServerTimestamp(serverTimestamp + 1); + builder.setDestinationUuid(UUID.randomUUID().toString()); + + MESSAGE2 = builder.build(); + + builder.setType(MessageProtos.Envelope.Type.UNIDENTIFIED_SENDER); + builder.clearSourceUuid(); + builder.clearSourceDevice(); + builder.setContent(ByteString.copyFromUtf8("COW")); + builder.setServerGuid(UUID.randomUUID().toString()); + builder.setServerTimestamp(serverTimestamp); // Test same millisecond arrival for two different messages + builder.setDestinationUuid(UUID.randomUUID().toString()); + + MESSAGE3 = builder.build(); + } + + private ExecutorService messageDeletionExecutorService; + private MessagesDynamoDb messagesDynamoDb; + + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(Tables.MESSAGES); + + @BeforeEach + void setup() { + messageDeletionExecutorService = Executors.newSingleThreadExecutor(); + messagesDynamoDb = new MessagesDynamoDb(DYNAMO_DB_EXTENSION.getDynamoDbClient(), + DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), Tables.MESSAGES.tableName(), Duration.ofDays(14), + messageDeletionExecutorService); + } + + @AfterEach + void teardown() throws Exception { + messageDeletionExecutorService.shutdown(); + messageDeletionExecutorService.awaitTermination(5, TimeUnit.SECONDS); + + StepVerifier.resetDefaultTimeout(); + } + + @Test + void testSimpleFetchAfterInsert() { + final UUID destinationUuid = UUID.randomUUID(); + final int destinationDeviceId = random.nextInt(255) + 1; + messagesDynamoDb.store(List.of(MESSAGE1, MESSAGE2, MESSAGE3), destinationUuid, destinationDeviceId); + + final List messagesStored = load(destinationUuid, destinationDeviceId, + MessagesDynamoDb.RESULT_SET_CHUNK_SIZE); + assertThat(messagesStored).isNotNull().hasSize(3); + final MessageProtos.Envelope firstMessage = + MESSAGE1.getServerGuid().compareTo(MESSAGE3.getServerGuid()) < 0 ? MESSAGE1 : MESSAGE3; + final MessageProtos.Envelope secondMessage = firstMessage == MESSAGE1 ? MESSAGE3 : MESSAGE1; + assertThat(messagesStored).element(0).isEqualTo(firstMessage); + assertThat(messagesStored).element(1).isEqualTo(secondMessage); + assertThat(messagesStored).element(2).isEqualTo(MESSAGE2); + } + + @ParameterizedTest + @ValueSource(ints = {10, 100, 100, 1_000, 3_000}) + void testLoadManyAfterInsert(final int messageCount) { + final UUID destinationUuid = UUID.randomUUID(); + final int destinationDeviceId = random.nextInt(255) + 1; + + final List messages = new ArrayList<>(messageCount); + for (int i = 0; i < messageCount; i++) { + messages.add(MessageHelper.createMessage(UUID.randomUUID(), 1, destinationUuid, (i + 1L) * 1000, "message " + i)); + } + + messagesDynamoDb.store(messages, destinationUuid, destinationDeviceId); + + final Publisher fetchedMessages = messagesDynamoDb.load(destinationUuid, destinationDeviceId, null); + + final long firstRequest = Math.min(10, messageCount); + StepVerifier.setDefaultTimeout(Duration.ofSeconds(15)); + + StepVerifier.Step step = StepVerifier.create(fetchedMessages, 0) + .expectSubscription() + .thenRequest(firstRequest) + .expectNextCount(firstRequest); + + if (messageCount > firstRequest) { + step = step.thenRequest(messageCount) + .expectNextCount(messageCount - firstRequest); + } + + step.thenCancel() + .verify(); + } + + @Test + void testLimitedLoad() { + final int messageCount = 200; + final UUID destinationUuid = UUID.randomUUID(); + final int destinationDeviceId = random.nextInt(255) + 1; + + final List messages = new ArrayList<>(messageCount); + for (int i = 0; i < messageCount; i++) { + messages.add(MessageHelper.createMessage(UUID.randomUUID(), 1, destinationUuid, (i + 1L) * 1000, "message " + i)); + } + + messagesDynamoDb.store(messages, destinationUuid, destinationDeviceId); + + final int messageLoadLimit = 100; + final int halfOfMessageLoadLimit = messageLoadLimit / 2; + final Publisher fetchedMessages = messagesDynamoDb.load(destinationUuid, destinationDeviceId, messageLoadLimit); + + StepVerifier.setDefaultTimeout(Duration.ofSeconds(10)); + + final AtomicInteger messagesRemaining = new AtomicInteger(messageLoadLimit); + + StepVerifier.create(fetchedMessages, 0) + .expectSubscription() + .thenRequest(halfOfMessageLoadLimit) + .expectNextCount(halfOfMessageLoadLimit) + // the first 100 should be fetched and buffered, but further requests should fail + .then(() -> DYNAMO_DB_EXTENSION.stopServer()) + .thenRequest(halfOfMessageLoadLimit) + .expectNextCount(halfOfMessageLoadLimit) + // we’ve consumed all the buffered messages, so a single request will fail + .thenRequest(1) + .expectError() + .verify(); + } + + @Test + void testDeleteForDestination() { + final UUID destinationUuid = UUID.randomUUID(); + final UUID secondDestinationUuid = UUID.randomUUID(); + messagesDynamoDb.store(List.of(MESSAGE1), destinationUuid, 1); + messagesDynamoDb.store(List.of(MESSAGE2), secondDestinationUuid, 1); + messagesDynamoDb.store(List.of(MESSAGE3), destinationUuid, 2); + + assertThat(load(destinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1) + .element(0).isEqualTo(MESSAGE1); + assertThat(load(destinationUuid, 2, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1) + .element(0).isEqualTo(MESSAGE3); + assertThat(load(secondDestinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull() + .hasSize(1).element(0).isEqualTo(MESSAGE2); + + messagesDynamoDb.deleteAllMessagesForAccount(destinationUuid).join(); + + assertThat(load(destinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().isEmpty(); + assertThat(load(destinationUuid, 2, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().isEmpty(); + assertThat(load(secondDestinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull() + .hasSize(1).element(0).isEqualTo(MESSAGE2); + } + + @Test + void testDeleteForDestinationDevice() { + final UUID destinationUuid = UUID.randomUUID(); + final UUID secondDestinationUuid = UUID.randomUUID(); + messagesDynamoDb.store(List.of(MESSAGE1), destinationUuid, 1); + messagesDynamoDb.store(List.of(MESSAGE2), secondDestinationUuid, 1); + messagesDynamoDb.store(List.of(MESSAGE3), destinationUuid, 2); + + assertThat(load(destinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1) + .element(0).isEqualTo(MESSAGE1); + assertThat(load(destinationUuid, 2, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1) + .element(0).isEqualTo(MESSAGE3); + assertThat(load(secondDestinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull() + .hasSize(1).element(0).isEqualTo(MESSAGE2); + + messagesDynamoDb.deleteAllMessagesForDevice(destinationUuid, 2).join(); + + assertThat(load(destinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1) + .element(0).isEqualTo(MESSAGE1); + assertThat(load(destinationUuid, 2, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().isEmpty(); + assertThat(load(secondDestinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull() + .hasSize(1).element(0).isEqualTo(MESSAGE2); + } + + @Test + void testDeleteMessageByDestinationAndGuid() throws Exception { + final UUID destinationUuid = UUID.randomUUID(); + final UUID secondDestinationUuid = UUID.randomUUID(); + messagesDynamoDb.store(List.of(MESSAGE1), destinationUuid, 1); + messagesDynamoDb.store(List.of(MESSAGE2), secondDestinationUuid, 1); + messagesDynamoDb.store(List.of(MESSAGE3), destinationUuid, 2); + + assertThat(load(destinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1) + .element(0).isEqualTo(MESSAGE1); + assertThat(load(destinationUuid, 2, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1) + .element(0).isEqualTo(MESSAGE3); + assertThat(load(secondDestinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull() + .hasSize(1).element(0).isEqualTo(MESSAGE2); + + final Optional deletedMessage = messagesDynamoDb.deleteMessageByDestinationAndGuid( + secondDestinationUuid, + UUID.fromString(MESSAGE2.getServerGuid())).get(5, TimeUnit.SECONDS); + + assertThat(deletedMessage).isPresent(); + + assertThat(load(destinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1) + .element(0).isEqualTo(MESSAGE1); + assertThat(load(destinationUuid, 2, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1) + .element(0).isEqualTo(MESSAGE3); + assertThat(load(secondDestinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull() + .isEmpty(); + + final Optional alreadyDeletedMessage = messagesDynamoDb.deleteMessageByDestinationAndGuid( + secondDestinationUuid, + UUID.fromString(MESSAGE2.getServerGuid())).get(5, TimeUnit.SECONDS); + + assertThat(alreadyDeletedMessage).isNotPresent(); + + } + + @Test + void testDeleteSingleMessage() throws Exception { + final UUID destinationUuid = UUID.randomUUID(); + final UUID secondDestinationUuid = UUID.randomUUID(); + messagesDynamoDb.store(List.of(MESSAGE1), destinationUuid, 1); + messagesDynamoDb.store(List.of(MESSAGE2), secondDestinationUuid, 1); + messagesDynamoDb.store(List.of(MESSAGE3), destinationUuid, 2); + + assertThat(load(destinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1) + .element(0).isEqualTo(MESSAGE1); + assertThat(load(destinationUuid, 2, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1) + .element(0).isEqualTo(MESSAGE3); + assertThat(load(secondDestinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull() + .hasSize(1).element(0).isEqualTo(MESSAGE2); + + messagesDynamoDb.deleteMessage(secondDestinationUuid, 1, + UUID.fromString(MESSAGE2.getServerGuid()), MESSAGE2.getServerTimestamp()).get(1, TimeUnit.SECONDS); + + assertThat(load(destinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1) + .element(0).isEqualTo(MESSAGE1); + assertThat(load(destinationUuid, 2, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1) + .element(0).isEqualTo(MESSAGE3); + assertThat(load(secondDestinationUuid, 1, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull() + .isEmpty(); + } + + private List load(final UUID destinationUuid, final long destinationDeviceId, + final int count) { + return Flux.from(messagesDynamoDb.load(destinationUuid, destinationDeviceId, count)) + .take(count, true) + .collectList() + .block(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagesManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagesManagerTest.java new file mode 100644 index 000000000..5c7f31af9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/MessagesManagerTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.util.UUID; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope; + +class MessagesManagerTest { + + private final MessagesDynamoDb messagesDynamoDb = mock(MessagesDynamoDb.class); + private final MessagesCache messagesCache = mock(MessagesCache.class); + private final ReportMessageManager reportMessageManager = mock(ReportMessageManager.class); + + private final MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, + reportMessageManager, Executors.newSingleThreadExecutor()); + + @Test + void insert() { + final UUID sourceAci = UUID.randomUUID(); + final Envelope message = Envelope.newBuilder() + .setSourceUuid(sourceAci.toString()) + .build(); + + final UUID destinationUuid = UUID.randomUUID(); + + messagesManager.insert(destinationUuid, 1L, message); + + verify(reportMessageManager).store(eq(sourceAci.toString()), any(UUID.class)); + + final Envelope syncMessage = Envelope.newBuilder(message) + .setSourceUuid(destinationUuid.toString()) + .build(); + + messagesManager.insert(destinationUuid, 1L, syncMessage); + + verifyNoMoreInteractions(reportMessageManager); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/PhoneNumberIdentifiersTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/PhoneNumberIdentifiersTest.java new file mode 100644 index 000000000..174465a3b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/PhoneNumberIdentifiersTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; + +class PhoneNumberIdentifiersTest { + + @RegisterExtension + static DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(Tables.PNI); + + private PhoneNumberIdentifiers phoneNumberIdentifiers; + + @BeforeEach + void setUp() { + phoneNumberIdentifiers = new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbClient(), + Tables.PNI.tableName()); + } + + @Test + void getPhoneNumberIdentifier() { + final String number = "+18005551234"; + final String differentNumber = "+18005556789"; + + final UUID firstPni = phoneNumberIdentifiers.getPhoneNumberIdentifier(number); + final UUID secondPni = phoneNumberIdentifiers.getPhoneNumberIdentifier(number); + + assertEquals(firstPni, secondPni); + assertNotEquals(firstPni, phoneNumberIdentifiers.getPhoneNumberIdentifier(differentNumber)); + } + + @Test + void generatePhoneNumberIdentifierIfNotExists() { + final String number = "+18005551234"; + + assertEquals(phoneNumberIdentifiers.generatePhoneNumberIdentifierIfNotExists(number), + phoneNumberIdentifiers.generatePhoneNumberIdentifierIfNotExists(number)); + } + + @Test + void getPhoneNumber() { + final String number = "+18005551234"; + + assertFalse(phoneNumberIdentifiers.getPhoneNumber(UUID.randomUUID()).isPresent()); + + final UUID pni = phoneNumberIdentifiers.getPhoneNumberIdentifier(number); + assertEquals(Optional.of(number), phoneNumberIdentifiers.getPhoneNumber(pni)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ProfilesManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ProfilesManagerTest.java new file mode 100644 index 000000000..3069c1964 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ProfilesManagerTest.java @@ -0,0 +1,235 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import io.lettuce.core.RedisException; +import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.signal.libsignal.protocol.ServiceId; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.tests.util.MockRedisFuture; +import org.whispersystems.textsecuregcm.tests.util.ProfileTestHelper; +import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper; + +@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) +public class ProfilesManagerTest { + + private Profiles profiles; + private RedisAdvancedClusterCommands commands; + private RedisAdvancedClusterAsyncCommands asyncCommands; + + private ProfilesManager profilesManager; + + @BeforeEach + void setUp() { + //noinspection unchecked + commands = mock(RedisAdvancedClusterCommands.class); + asyncCommands = mock(RedisAdvancedClusterAsyncCommands.class); + final FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.builder() + .stringCommands(commands) + .stringAsyncCommands(asyncCommands) + .build(); + + profiles = mock(Profiles.class); + + profilesManager = new ProfilesManager(profiles, cacheCluster); + } + + @Test + public void testGetProfileInCache() throws InvalidInputException { + final UUID uuid = UUID.randomUUID(); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(uuid)).serialize(); + when(commands.hget(eq("profiles::" + uuid), eq("someversion"))).thenReturn(String.format( + "{\"version\": \"someversion\", \"name\": \"%s\", \"avatar\": \"someavatar\", \"commitment\":\"%s\"}", + ProfileTestHelper.encodeToBase64(name), + ProfileTestHelper.encodeToBase64(commitment))); + + Optional profile = profilesManager.get(uuid, "someversion"); + + assertTrue(profile.isPresent()); + assertArrayEquals(profile.get().name(), name); + assertEquals(profile.get().avatar(), "someavatar"); + assertArrayEquals(profile.get().commitment(), commitment); + + verify(commands, times(1)).hget(eq("profiles::" + uuid), eq("someversion")); + verifyNoMoreInteractions(commands); + verifyNoMoreInteractions(profiles); + } + + @Test + public void testGetProfileAsyncInCache() throws InvalidInputException { + final UUID uuid = UUID.randomUUID(); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(uuid)).serialize(); + + when(asyncCommands.hget(eq("profiles::" + uuid), eq("someversion"))).thenReturn( + MockRedisFuture.completedFuture(String.format("{\"version\": \"someversion\", \"name\": \"%s\", \"avatar\": \"someavatar\", \"commitment\":\"%s\"}", + ProfileTestHelper.encodeToBase64(name), + ProfileTestHelper.encodeToBase64(commitment)))); + + Optional profile = profilesManager.getAsync(uuid, "someversion").join(); + + assertTrue(profile.isPresent()); + assertArrayEquals(profile.get().name(), name); + assertEquals(profile.get().avatar(), "someavatar"); + assertArrayEquals(profile.get().commitment(), commitment); + + verify(asyncCommands, times(1)).hget(eq("profiles::" + uuid), eq("someversion")); + verifyNoMoreInteractions(asyncCommands); + verifyNoMoreInteractions(profiles); + } + + @Test + public void testGetProfileNotInCache() { + final UUID uuid = UUID.randomUUID(); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final VersionedProfile profile = new VersionedProfile("someversion", name, "someavatar", null, null, + null, "somecommitment".getBytes()); + + when(commands.hget(eq("profiles::" + uuid), eq("someversion"))).thenReturn(null); + when(profiles.get(eq(uuid), eq("someversion"))).thenReturn(Optional.of(profile)); + + Optional retrieved = profilesManager.get(uuid, "someversion"); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), profile); + + verify(commands, times(1)).hget(eq("profiles::" + uuid), eq("someversion")); + verify(commands, times(1)).hset(eq("profiles::" + uuid), eq("someversion"), anyString()); + verifyNoMoreInteractions(commands); + + verify(profiles, times(1)).get(eq(uuid), eq("someversion")); + verifyNoMoreInteractions(profiles); + } + + @Test + public void testGetProfileAsyncNotInCache() { + final UUID uuid = UUID.randomUUID(); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final VersionedProfile profile = new VersionedProfile("someversion", name, "someavatar", null, null, + null, "somecommitment".getBytes()); + + when(asyncCommands.hget(eq("profiles::" + uuid), eq("someversion"))).thenReturn(MockRedisFuture.completedFuture(null)); + when(asyncCommands.hset(eq("profiles::" + uuid), eq("someversion"), anyString())).thenReturn(MockRedisFuture.completedFuture(null)); + when(profiles.getAsync(eq(uuid), eq("someversion"))).thenReturn(CompletableFuture.completedFuture(Optional.of(profile))); + + Optional retrieved = profilesManager.getAsync(uuid, "someversion").join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), profile); + + verify(asyncCommands, times(1)).hget(eq("profiles::" + uuid), eq("someversion")); + verify(asyncCommands, times(1)).hset(eq("profiles::" + uuid), eq("someversion"), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(profiles, times(1)).getAsync(eq(uuid), eq("someversion")); + verifyNoMoreInteractions(profiles); + } + + @Test + public void testGetProfileBrokenCache() { + final UUID uuid = UUID.randomUUID(); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final VersionedProfile profile = new VersionedProfile("someversion", name, "someavatar", null, null, + null, "somecommitment".getBytes()); + + when(commands.hget(eq("profiles::" + uuid), eq("someversion"))).thenThrow(new RedisException("Connection lost")); + when(profiles.get(eq(uuid), eq("someversion"))).thenReturn(Optional.of(profile)); + + Optional retrieved = profilesManager.get(uuid, "someversion"); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), profile); + + verify(commands, times(1)).hget(eq("profiles::" + uuid), eq("someversion")); + verify(commands, times(1)).hset(eq("profiles::" + uuid), eq("someversion"), anyString()); + verifyNoMoreInteractions(commands); + + verify(profiles, times(1)).get(eq(uuid), eq("someversion")); + verifyNoMoreInteractions(profiles); + } + + @Test + public void testGetProfileAsyncBrokenCache() { + final UUID uuid = UUID.randomUUID(); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final VersionedProfile profile = new VersionedProfile("someversion", name, "someavatar", null, null, + null, "somecommitment".getBytes()); + + when(asyncCommands.hget(eq("profiles::" + uuid), eq("someversion"))).thenReturn(MockRedisFuture.failedFuture(new RedisException("Connection lost"))); + when(asyncCommands.hset(eq("profiles::" + uuid), eq("someversion"), anyString())).thenReturn(MockRedisFuture.completedFuture(null)); + when(profiles.getAsync(eq(uuid), eq("someversion"))).thenReturn(CompletableFuture.completedFuture(Optional.of(profile))); + + Optional retrieved = profilesManager.getAsync(uuid, "someversion").join(); + + assertTrue(retrieved.isPresent()); + assertSame(retrieved.get(), profile); + + verify(asyncCommands, times(1)).hget(eq("profiles::" + uuid), eq("someversion")); + verify(asyncCommands, times(1)).hset(eq("profiles::" + uuid), eq("someversion"), anyString()); + verifyNoMoreInteractions(asyncCommands); + + verify(profiles, times(1)).getAsync(eq(uuid), eq("someversion")); + verifyNoMoreInteractions(profiles); + } + + @Test + public void testSet() { + final UUID uuid = UUID.randomUUID(); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final VersionedProfile profile = new VersionedProfile("someversion", name, "someavatar", null, null, + null, "somecommitment".getBytes()); + + profilesManager.set(uuid, profile); + + verify(commands, times(1)).hset(eq("profiles::" + uuid), eq("someversion"), any()); + verifyNoMoreInteractions(commands); + + verify(profiles, times(1)).set(eq(uuid), eq(profile)); + verifyNoMoreInteractions(profiles); + } + + @Test + public void testSetAsync() { + final UUID uuid = UUID.randomUUID(); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final VersionedProfile profile = new VersionedProfile("someversion", name, "someavatar", null, null, + null, "somecommitment".getBytes()); + + when(asyncCommands.hset(eq("profiles::" + uuid), eq("someversion"), anyString())).thenReturn(MockRedisFuture.completedFuture(null)); + when(profiles.setAsync(eq(uuid), eq(profile))).thenReturn(CompletableFuture.completedFuture(null)); + + profilesManager.setAsync(uuid, profile).join(); + + verify(asyncCommands, times(1)).hset(eq("profiles::" + uuid), eq("someversion"), any()); + verifyNoMoreInteractions(asyncCommands); + + verify(profiles, times(1)).setAsync(eq(uuid), eq(profile)); + verifyNoMoreInteractions(profiles); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ProfilesTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ProfilesTest.java new file mode 100644 index 000000000..e18e3df5c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ProfilesTest.java @@ -0,0 +1,359 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.signal.libsignal.protocol.ServiceId; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.whispersystems.textsecuregcm.tests.util.ProfileTestHelper; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) +public class ProfilesTest { + private static final UUID ACI = UUID.randomUUID(); + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(Tables.PROFILES); + + private Profiles profiles; + private VersionedProfile validProfile; + + @BeforeEach + void setUp() throws InvalidInputException { + profiles = new Profiles(DYNAMO_DB_EXTENSION.getDynamoDbClient(), + DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + Tables.PROFILES.tableName()); + final byte[] commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(ACI)).serialize(); + final String version = "someVersion"; + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] validAboutEmoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] validAbout = ProfileTestHelper.generateRandomByteArray(156); + final String avatar = "profiles/" + ProfileTestHelper.generateRandomBase64FromByteArray(16); + + validProfile = new VersionedProfile(version, name, avatar, validAboutEmoji, validAbout, null, commitment); + } + + @Test + void testSetGet() { + profiles.set(ACI, validProfile); + + Optional retrieved = profiles.get(ACI, validProfile.version()); + + assertThat(retrieved.isPresent()).isTrue(); + assertThat(retrieved.get().name()).isEqualTo(validProfile.name()); + assertThat(retrieved.get().avatar()).isEqualTo(validProfile.avatar()); + assertThat(retrieved.get().commitment()).isEqualTo(validProfile.commitment()); + assertThat(retrieved.get().about()).isEqualTo(validProfile.about()); + assertThat(retrieved.get().aboutEmoji()).isEqualTo(validProfile.aboutEmoji()); + } + + @Test + void testSetGetAsync() { + profiles.setAsync(ACI, validProfile).join(); + + Optional retrieved = profiles.getAsync(ACI, validProfile.version()).join(); + + assertThat(retrieved.isPresent()).isTrue(); + assertThat(retrieved.get().name()).isEqualTo(validProfile.name()); + assertThat(retrieved.get().avatar()).isEqualTo(validProfile.avatar()); + assertThat(retrieved.get().commitment()).isEqualTo(validProfile.commitment()); + assertThat(retrieved.get().about()).isEqualTo(validProfile.about()); + assertThat(retrieved.get().aboutEmoji()).isEqualTo(validProfile.aboutEmoji()); + } + + @Test + void testDeleteReset() throws InvalidInputException { + profiles.set(ACI, validProfile); + + profiles.deleteAll(ACI).join(); + + final String version = "someVersion"; + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final String differentAvatar = "profiles/" + ProfileTestHelper.generateRandomBase64FromByteArray(16); + final byte[] differentEmoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] differentAbout = ProfileTestHelper.generateRandomByteArray(156); + final byte[] paymentAddress = ProfileTestHelper.generateRandomByteArray(582); + final byte[] commitment = new ProfileKey(ProfileTestHelper.generateRandomByteArray(32)).getCommitment(new ServiceId.Aci(ACI)).serialize(); + + VersionedProfile updatedProfile = new VersionedProfile(version, name, differentAvatar, + differentEmoji, differentAbout, paymentAddress, commitment); + + profiles.set(ACI, updatedProfile); + + Optional retrieved = profiles.get(ACI, version); + + assertThat(retrieved.isPresent()).isTrue(); + assertThat(retrieved.get().name()).isEqualTo(updatedProfile.name()); + assertThat(retrieved.get().avatar()).isEqualTo(updatedProfile.avatar()); + assertThat(retrieved.get().commitment()).isEqualTo(updatedProfile.commitment()); + assertThat(retrieved.get().about()).isEqualTo(updatedProfile.about()); + assertThat(retrieved.get().aboutEmoji()).isEqualTo(updatedProfile.aboutEmoji()); + } + + @Test + void testSetGetNullOptionalFields() throws InvalidInputException { + final String version = "someVersion"; + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] commitment = new ProfileKey(ProfileTestHelper.generateRandomByteArray(32)).getCommitment(new ServiceId.Aci(ACI)).serialize(); + + VersionedProfile profile = new VersionedProfile(version, name, null, null, null, null, + commitment); + profiles.set(ACI, profile); + + Optional retrieved = profiles.get(ACI, version); + + assertThat(retrieved.isPresent()).isTrue(); + assertThat(retrieved.get().name()).isEqualTo(profile.name()); + assertThat(retrieved.get().avatar()).isEqualTo(profile.avatar()); + assertThat(retrieved.get().commitment()).isEqualTo(profile.commitment()); + assertThat(retrieved.get().about()).isEqualTo(profile.about()); + assertThat(retrieved.get().aboutEmoji()).isEqualTo(profile.aboutEmoji()); + } + + @Test + void testSetReplace() throws InvalidInputException { + profiles.set(ACI, validProfile); + + Optional retrieved = profiles.get(ACI, validProfile.version()); + + assertThat(retrieved.isPresent()).isTrue(); + assertThat(retrieved.get().name()).isEqualTo(validProfile.name()); + assertThat(retrieved.get().avatar()).isEqualTo(validProfile.avatar()); + assertThat(retrieved.get().commitment()).isEqualTo(validProfile.commitment()); + assertThat(retrieved.get().about()).isEqualTo(validProfile.about()); + assertThat(retrieved.get().aboutEmoji()).isEqualTo(validProfile.aboutEmoji()); + assertThat(retrieved.get().paymentAddress()).isNull(); + + final byte[] differentName = ProfileTestHelper.generateRandomByteArray(81); + final byte[] differentEmoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] differentAbout = ProfileTestHelper.generateRandomByteArray(156); + final String differentAvatar = "profiles/" + ProfileTestHelper.generateRandomBase64FromByteArray(16); + final byte[] differentCommitment = new ProfileKey(ProfileTestHelper.generateRandomByteArray(32)).getCommitment(new ServiceId.Aci(ACI)).serialize(); + + VersionedProfile updated = new VersionedProfile(validProfile.version(), differentName, differentAvatar, differentEmoji, differentAbout, null, + differentCommitment); + profiles.set(ACI, updated); + + retrieved = profiles.get(ACI, updated.version()); + + assertThat(retrieved.isPresent()).isTrue(); + assertThat(retrieved.get().name()).isEqualTo(updated.name()); + assertThat(retrieved.get().about()).isEqualTo(updated.about()); + assertThat(retrieved.get().aboutEmoji()).isEqualTo(updated.aboutEmoji()); + assertThat(retrieved.get().avatar()).isEqualTo(updated.avatar()); + + // Commitment should be unchanged after an overwrite + assertThat(retrieved.get().commitment()).isEqualTo(validProfile.commitment()); + } + + @Test + void testMultipleVersions() throws InvalidInputException { + final String versionOne = "versionOne"; + final String versionTwo = "versionTwo"; + + final byte[] nameOne = ProfileTestHelper.generateRandomByteArray(81); + final byte[] nameTwo = ProfileTestHelper.generateRandomByteArray(81); + + final String avatarOne = "profiles/" + ProfileTestHelper.generateRandomBase64FromByteArray(16); + final String avatarTwo = "profiles/" + ProfileTestHelper.generateRandomBase64FromByteArray(16); + + final byte[] aboutEmoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] about = ProfileTestHelper.generateRandomByteArray(156); + + final byte[] commitmentOne = new ProfileKey(ProfileTestHelper.generateRandomByteArray(32)).getCommitment(new ServiceId.Aci(ACI)).serialize(); + final byte[] commitmentTwo = new ProfileKey(ProfileTestHelper.generateRandomByteArray(32)).getCommitment(new ServiceId.Aci(ACI)).serialize(); + + VersionedProfile profileOne = new VersionedProfile(versionOne, nameOne, avatarOne, null, null, + null, commitmentOne); + VersionedProfile profileTwo = new VersionedProfile(versionTwo, nameTwo, avatarTwo, aboutEmoji, about, null, commitmentTwo); + + profiles.set(ACI, profileOne); + profiles.set(ACI, profileTwo); + + Optional retrieved = profiles.get(ACI, versionOne); + + assertThat(retrieved.isPresent()).isTrue(); + assertThat(retrieved.get().name()).isEqualTo(profileOne.name()); + assertThat(retrieved.get().avatar()).isEqualTo(profileOne.avatar()); + assertThat(retrieved.get().commitment()).isEqualTo(profileOne.commitment()); + assertThat(retrieved.get().about()).isEqualTo(profileOne.about()); + assertThat(retrieved.get().aboutEmoji()).isEqualTo(profileOne.aboutEmoji()); + + retrieved = profiles.get(ACI, versionTwo); + + assertThat(retrieved.isPresent()).isTrue(); + assertThat(retrieved.get().name()).isEqualTo(profileTwo.name()); + assertThat(retrieved.get().avatar()).isEqualTo(profileTwo.avatar()); + assertThat(retrieved.get().commitment()).isEqualTo(profileTwo.commitment()); + assertThat(retrieved.get().about()).isEqualTo(profileTwo.about()); + assertThat(retrieved.get().aboutEmoji()).isEqualTo(profileTwo.aboutEmoji()); + } + + @Test + void testMissing() { + profiles.set(ACI, validProfile); + final String missingVersion = "missingVersion"; + + Optional retrieved = profiles.get(ACI, missingVersion); + assertThat(retrieved.isPresent()).isFalse(); + } + + + @Test + void testDelete() throws InvalidInputException { + final String versionOne = "versionOne"; + final String versionTwo = "versionTwo"; + + final byte[] nameOne = ProfileTestHelper.generateRandomByteArray(81); + final byte[] nameTwo = ProfileTestHelper.generateRandomByteArray(81); + + final byte[] aboutEmoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] about = ProfileTestHelper.generateRandomByteArray(156); + + final String avatarOne = "profiles/" + ProfileTestHelper.generateRandomBase64FromByteArray(16); + final String avatarTwo = "profiles/" + ProfileTestHelper.generateRandomBase64FromByteArray(16); + + final byte[] commitmentOne = new ProfileKey(ProfileTestHelper.generateRandomByteArray(32)).getCommitment(new ServiceId.Aci(ACI)).serialize(); + final byte[] commitmentTwo = new ProfileKey(ProfileTestHelper.generateRandomByteArray(32)).getCommitment(new ServiceId.Aci(ACI)).serialize(); + + VersionedProfile profileOne = new VersionedProfile(versionOne, nameOne, avatarOne, null, null, + null, commitmentOne); + VersionedProfile profileTwo = new VersionedProfile(versionTwo, nameTwo, avatarTwo, aboutEmoji, about, null, commitmentTwo); + + profiles.set(ACI, profileOne); + profiles.set(ACI, profileTwo); + + profiles.deleteAll(ACI).join(); + + Optional retrieved = profiles.get(ACI, versionOne); + + assertThat(retrieved.isPresent()).isFalse(); + + retrieved = profiles.get(ACI, versionTwo); + + assertThat(retrieved.isPresent()).isFalse(); + } + + @ParameterizedTest + @MethodSource + void buildUpdateExpression(final VersionedProfile profile, final String expectedUpdateExpression) { + assertEquals(expectedUpdateExpression, Profiles.buildUpdateExpression(profile)); + } + + private static Stream buildUpdateExpression() throws InvalidInputException { + final String version = "someVersion"; + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final String avatar = "profiles/" + ProfileTestHelper.generateRandomBase64FromByteArray(16);; + final byte[] emoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] about = ProfileTestHelper.generateRandomByteArray(156); + final byte[] paymentAddress = ProfileTestHelper.generateRandomByteArray(582); + final byte[] commitment = new ProfileKey(ProfileTestHelper.generateRandomByteArray(32)).getCommitment(new ServiceId.Aci(ACI)).serialize(); + + return Stream.of( + Arguments.of( + new VersionedProfile(version, name, avatar, emoji, about, paymentAddress, commitment), + "SET #commitment = if_not_exists(#commitment, :commitment), #name = :name, #avatar = :avatar, #about = :about, #aboutEmoji = :aboutEmoji, #paymentAddress = :paymentAddress"), + + Arguments.of( + new VersionedProfile(version, name, avatar, emoji, about, null, commitment), + "SET #commitment = if_not_exists(#commitment, :commitment), #name = :name, #avatar = :avatar, #about = :about, #aboutEmoji = :aboutEmoji REMOVE #paymentAddress"), + + Arguments.of( + new VersionedProfile(version, name, avatar, emoji, null, null, commitment), + "SET #commitment = if_not_exists(#commitment, :commitment), #name = :name, #avatar = :avatar, #aboutEmoji = :aboutEmoji REMOVE #about, #paymentAddress"), + + Arguments.of( + new VersionedProfile(version, name, avatar, null, null, null, commitment), + "SET #commitment = if_not_exists(#commitment, :commitment), #name = :name, #avatar = :avatar REMOVE #about, #aboutEmoji, #paymentAddress"), + + Arguments.of( + new VersionedProfile(version, name, null, null, null, null, commitment), + "SET #commitment = if_not_exists(#commitment, :commitment), #name = :name REMOVE #avatar, #about, #aboutEmoji, #paymentAddress"), + + Arguments.of( + new VersionedProfile(version, null, null, null, null, null, commitment), + "SET #commitment = if_not_exists(#commitment, :commitment) REMOVE #name, #avatar, #about, #aboutEmoji, #paymentAddress") + ); + } + + @ParameterizedTest + @MethodSource + void buildUpdateExpressionAttributeValues(final VersionedProfile profile, final Map expectedAttributeValues) { + assertEquals(expectedAttributeValues, Profiles.buildUpdateExpressionAttributeValues(profile)); + } + + private static Stream buildUpdateExpressionAttributeValues() throws InvalidInputException { + final String version = "someVersion"; + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final String avatar = "profiles/" + ProfileTestHelper.generateRandomBase64FromByteArray(16);; + final byte[] emoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] about = ProfileTestHelper.generateRandomByteArray(156); + final byte[] paymentAddress = ProfileTestHelper.generateRandomByteArray(582); + final byte[] commitment = new ProfileKey(ProfileTestHelper.generateRandomByteArray(32)).getCommitment(new ServiceId.Aci(ACI)).serialize(); + + return Stream.of( + Arguments.of( + new VersionedProfile(version, name, avatar, emoji, about, paymentAddress, commitment), + Map.of( + ":commitment", AttributeValues.fromByteArray(commitment), + ":name", AttributeValues.fromByteArray(name), + ":avatar", AttributeValues.fromString(avatar), + ":aboutEmoji", AttributeValues.fromByteArray(emoji), + ":about", AttributeValues.fromByteArray(about), + ":paymentAddress", AttributeValues.fromByteArray(paymentAddress))), + + Arguments.of( + new VersionedProfile(version, name, avatar, emoji, about, null, commitment), + Map.of( + ":commitment", AttributeValues.fromByteArray(commitment), + ":name", AttributeValues.fromByteArray(name), + ":avatar", AttributeValues.fromString(avatar), + ":aboutEmoji", AttributeValues.fromByteArray(emoji), + ":about", AttributeValues.fromByteArray(about))), + + Arguments.of( + new VersionedProfile(version, name, avatar, emoji, null, null, commitment), + Map.of( + ":commitment", AttributeValues.fromByteArray(commitment), + ":name",AttributeValues.fromByteArray(name), + ":avatar", AttributeValues.fromString(avatar), + ":aboutEmoji", AttributeValues.fromByteArray(emoji))), + + Arguments.of( + new VersionedProfile(version, name, avatar, null, null, null, commitment), + Map.of( + ":commitment", AttributeValues.fromByteArray(commitment), + ":name", AttributeValues.fromByteArray(name), + ":avatar", AttributeValues.fromString(avatar))), + + Arguments.of( + new VersionedProfile(version, name, null, null, null, null, commitment), + Map.of( + ":commitment", AttributeValues.fromByteArray(commitment), + ":name", AttributeValues.fromByteArray(name))), + + Arguments.of( + new VersionedProfile(version, null, null, null, null, null, commitment), + Map.of(":commitment", AttributeValues.fromByteArray(commitment))) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/PushChallengeDynamoDbTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/PushChallengeDynamoDbTest.java new file mode 100644 index 000000000..0dd06c4f3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/PushChallengeDynamoDbTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Random; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; + +class PushChallengeDynamoDbTest { + + private PushChallengeDynamoDb pushChallengeDynamoDb; + + private static final long CURRENT_TIME_MILLIS = 1_000_000_000; + + private static final Random RANDOM = new Random(); + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(Tables.PUSH_CHALLENGES); + + @BeforeEach + void setUp() { + this.pushChallengeDynamoDb = new PushChallengeDynamoDb( + DYNAMO_DB_EXTENSION.getDynamoDbClient(), + Tables.PUSH_CHALLENGES.tableName(), + Clock.fixed(Instant.ofEpochMilli(CURRENT_TIME_MILLIS), ZoneId.systemDefault())); + } + + @Test + void add() { + final UUID uuid = UUID.randomUUID(); + + assertTrue(pushChallengeDynamoDb.add(uuid, generateRandomToken(), Duration.ofMinutes(1))); + assertFalse(pushChallengeDynamoDb.add(uuid, generateRandomToken(), Duration.ofMinutes(1))); + } + + @Test + void remove() { + final UUID uuid = UUID.randomUUID(); + final byte[] token = generateRandomToken(); + + assertFalse(pushChallengeDynamoDb.remove(uuid, token)); + assertTrue(pushChallengeDynamoDb.add(uuid, token, Duration.ofMinutes(1))); + assertTrue(pushChallengeDynamoDb.remove(uuid, token)); + assertTrue(pushChallengeDynamoDb.add(uuid, token, Duration.ofMinutes(-1))); + assertFalse(pushChallengeDynamoDb.remove(uuid, token)); + } + + @Test + void getExpirationTimestamp() { + assertEquals((CURRENT_TIME_MILLIS / 1000) + 3600, + pushChallengeDynamoDb.getExpirationTimestamp(Duration.ofHours(1))); + } + + private static byte[] generateRandomToken() { + final byte[] token = new byte[16]; + RANDOM.nextBytes(token); + + return token; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/PushFeedbackProcessorTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/PushFeedbackProcessorTest.java new file mode 100644 index 000000000..62bd69c64 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/PushFeedbackProcessorTest.java @@ -0,0 +1,159 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.tests.util.AccountsHelper.eqUuid; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; +import org.whispersystems.textsecuregcm.util.Util; + +class PushFeedbackProcessorTest { + + private final AccountsManager accountsManager = mock(AccountsManager.class); + + private Account uninstalledAccount = mock(Account.class); + private Account mixedAccount = mock(Account.class); + private Account freshAccount = mock(Account.class); + private Account cleanAccount = mock(Account.class); + private Account stillActiveAccount = mock(Account.class); + + private Device uninstalledDevice = mock(Device.class); + private Device uninstalledDeviceTwo = mock(Device.class); + private Device installedDevice = mock(Device.class); + private Device installedDeviceTwo = mock(Device.class); + private Device recentUninstalledDevice = mock(Device.class); + private Device stillActiveDevice = mock(Device.class); + + @BeforeEach + void setup() { + AccountsHelper.setupMockUpdate(accountsManager); + + when(uninstalledDevice.getUninstalledFeedbackTimestamp()).thenReturn( + Util.todayInMillis() - TimeUnit.DAYS.toMillis(2)); + when(uninstalledDevice.getLastSeen()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(2)); + when(uninstalledDevice.isEnabled()).thenReturn(true); + when(uninstalledDeviceTwo.getUninstalledFeedbackTimestamp()).thenReturn( + Util.todayInMillis() - TimeUnit.DAYS.toMillis(3)); + when(uninstalledDeviceTwo.getLastSeen()).thenReturn(Util.todayInMillis() - TimeUnit.DAYS.toMillis(3)); + when(uninstalledDeviceTwo.isEnabled()).thenReturn(true); + + when(installedDevice.getUninstalledFeedbackTimestamp()).thenReturn(0L); + when(installedDevice.isEnabled()).thenReturn(true); + when(installedDeviceTwo.getUninstalledFeedbackTimestamp()).thenReturn(0L); + when(installedDeviceTwo.isEnabled()).thenReturn(true); + + when(recentUninstalledDevice.getUninstalledFeedbackTimestamp()).thenReturn( + Util.todayInMillis() - TimeUnit.DAYS.toMillis(1)); + when(recentUninstalledDevice.getLastSeen()).thenReturn(Util.todayInMillis()); + when(recentUninstalledDevice.isEnabled()).thenReturn(true); + + when(stillActiveDevice.getUninstalledFeedbackTimestamp()).thenReturn( + Util.todayInMillis() - TimeUnit.DAYS.toMillis(2)); + when(stillActiveDevice.getLastSeen()).thenReturn(Util.todayInMillis()); + when(stillActiveDevice.isEnabled()).thenReturn(true); + + when(uninstalledAccount.getDevices()).thenReturn(List.of(uninstalledDevice)); + when(mixedAccount.getDevices()).thenReturn(List.of(installedDevice, uninstalledDeviceTwo)); + when(freshAccount.getDevices()).thenReturn(List.of(recentUninstalledDevice)); + when(cleanAccount.getDevices()).thenReturn(List.of(installedDeviceTwo)); + when(stillActiveAccount.getDevices()).thenReturn(List.of(stillActiveDevice)); + + when(mixedAccount.getUuid()).thenReturn(UUID.randomUUID()); + when(freshAccount.getUuid()).thenReturn(UUID.randomUUID()); + when(cleanAccount.getUuid()).thenReturn(UUID.randomUUID()); + when(stillActiveAccount.getUuid()).thenReturn(UUID.randomUUID()); + + when(uninstalledAccount.isEnabled()).thenReturn(true); + when(uninstalledAccount.isDiscoverableByPhoneNumber()).thenReturn(true); + when(uninstalledAccount.getUuid()).thenReturn(UUID.randomUUID()); + when(uninstalledAccount.getNumber()).thenReturn("+18005551234"); + + AccountsHelper.setupMockGet(accountsManager, + Set.of(uninstalledAccount, mixedAccount, freshAccount, cleanAccount, stillActiveAccount)); + } + + @Test + void testEmpty() { + PushFeedbackProcessor processor = new PushFeedbackProcessor(accountsManager, Executors.newSingleThreadExecutor()); + processor.timeAndProcessCrawlChunk(Optional.of(UUID.randomUUID()), Collections.emptyList()); + + verifyNoInteractions(accountsManager); + } + + @Test + void testUpdate() { + PushFeedbackProcessor processor = new PushFeedbackProcessor(accountsManager, Executors.newSingleThreadExecutor()); + processor.timeAndProcessCrawlChunk(Optional.of(UUID.randomUUID()), + List.of(uninstalledAccount, mixedAccount, stillActiveAccount, freshAccount, cleanAccount)); + + verify(uninstalledDevice).setApnId(isNull()); + verify(uninstalledDevice).setGcmId(isNull()); + verify(uninstalledDevice).setFetchesMessages(eq(false)); + when(uninstalledDevice.isEnabled()).thenReturn(false); + + verify(accountsManager).update(eqUuid(uninstalledAccount), any()); + + verify(uninstalledDeviceTwo).setApnId(isNull()); + verify(uninstalledDeviceTwo).setGcmId(isNull()); + verify(uninstalledDeviceTwo).setFetchesMessages(eq(false)); + when(uninstalledDeviceTwo.isEnabled()).thenReturn(false); + + verify(installedDevice, never()).setApnId(any()); + verify(installedDevice, never()).setGcmId(any()); + verify(installedDevice, never()).setFetchesMessages(anyBoolean()); + + verify(accountsManager).update(eqUuid(mixedAccount), any()); + + verify(recentUninstalledDevice, never()).setApnId(any()); + verify(recentUninstalledDevice, never()).setGcmId(any()); + verify(recentUninstalledDevice, never()).setFetchesMessages(anyBoolean()); + + verify(accountsManager, never()).update(eqUuid(freshAccount), any()); + + verify(installedDeviceTwo, never()).setApnId(any()); + verify(installedDeviceTwo, never()).setGcmId(any()); + verify(installedDeviceTwo, never()).setFetchesMessages(anyBoolean()); + + verify(accountsManager, never()).update(eqUuid(cleanAccount), any()); + + verify(stillActiveDevice).setUninstalledFeedbackTimestamp(eq(0L)); + verify(stillActiveDevice, never()).setApnId(any()); + verify(stillActiveDevice, never()).setGcmId(any()); + verify(stillActiveDevice, never()).setFetchesMessages(anyBoolean()); + when(stillActiveDevice.getUninstalledFeedbackTimestamp()).thenReturn(0L); + + verify(accountsManager).update(eqUuid(stillActiveAccount), any()); + + // there are un-verified calls to updateDevice + clearInvocations(accountsManager); + + // a second crawl should not make any further updates + processor.timeAndProcessCrawlChunk(Optional.of(UUID.randomUUID()), + List.of(uninstalledAccount, mixedAccount, stillActiveAccount, freshAccount, cleanAccount)); + + verify(accountsManager, never()).update(any(Account.class), any()); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RedeemedReceiptsManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RedeemedReceiptsManagerTest.java new file mode 100644 index 000000000..f0f35d120 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RedeemedReceiptsManagerTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.security.SecureRandom; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.receipts.ReceiptSerial; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.TestClock; + +class RedeemedReceiptsManagerTest { + + private static final long NOW_EPOCH_SECONDS = 1_500_000_000L; + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(Tables.REDEEMED_RECEIPTS); + + Clock clock = TestClock.pinned(Instant.ofEpochSecond(NOW_EPOCH_SECONDS)); + ReceiptSerial receiptSerial; + RedeemedReceiptsManager redeemedReceiptsManager; + + @BeforeEach + void beforeEach() throws InvalidInputException { + byte[] receiptSerialBytes = new byte[ReceiptSerial.SIZE]; + SECURE_RANDOM.nextBytes(receiptSerialBytes); + receiptSerial = new ReceiptSerial(receiptSerialBytes); + redeemedReceiptsManager = new RedeemedReceiptsManager( + clock, + Tables.REDEEMED_RECEIPTS.tableName(), + DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + Duration.ofDays(90)); + } + + @Test + void testPut() throws ExecutionException, InterruptedException { + final long receiptExpiration = 42; + final long receiptLevel = 3; + CompletableFuture put; + + // initial insert should return true + put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, AuthHelper.VALID_UUID); + assertThat(put.get()).isTrue(); + + // subsequent attempted inserts with modified parameters should return false + put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration + 1, receiptLevel, AuthHelper.VALID_UUID); + assertThat(put.get()).isFalse(); + put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel + 1, AuthHelper.VALID_UUID); + assertThat(put.get()).isFalse(); + put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, AuthHelper.VALID_UUID_TWO); + assertThat(put.get()).isFalse(); + + // repeated insert attempt of the original parameters should return true + put = redeemedReceiptsManager.put(receiptSerial, receiptExpiration, receiptLevel, AuthHelper.VALID_UUID); + assertThat(put.get()).isTrue(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RefreshingAccountAndDeviceSupplierTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RefreshingAccountAndDeviceSupplierTest.java new file mode 100644 index 000000000..dacbf13c3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RefreshingAccountAndDeviceSupplierTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.util.Pair; + +class RefreshingAccountAndDeviceSupplierTest { + + @Test + void test() { + + final AccountsManager accountsManager = mock(AccountsManager.class); + + final UUID uuid = UUID.randomUUID(); + final long deviceId = 2L; + + final Account initialAccount = mock(Account.class); + final Device initialDevice = mock(Device.class); + + when(initialAccount.getUuid()).thenReturn(uuid); + when(initialDevice.getId()).thenReturn(deviceId); + when(initialAccount.getDevice(deviceId)).thenReturn(Optional.of(initialDevice)); + + when(accountsManager.getByAccountIdentifier(any(UUID.class))).thenAnswer(answer -> { + final Account account = mock(Account.class); + final Device device = mock(Device.class); + + when(account.getUuid()).thenReturn(answer.getArgument(0, UUID.class)); + when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); + when(device.getId()).thenReturn(deviceId); + + return Optional.of(account); + }); + + final RefreshingAccountAndDeviceSupplier refreshingAccountAndDeviceSupplier = new RefreshingAccountAndDeviceSupplier( + initialAccount, deviceId, accountsManager); + + Pair accountAndDevice = refreshingAccountAndDeviceSupplier.get(); + + assertSame(initialAccount, accountAndDevice.first()); + assertSame(initialDevice, accountAndDevice.second()); + + accountAndDevice = refreshingAccountAndDeviceSupplier.get(); + + assertSame(initialAccount, accountAndDevice.first()); + assertSame(initialDevice, accountAndDevice.second()); + + when(initialAccount.isStale()).thenReturn(true); + + accountAndDevice = refreshingAccountAndDeviceSupplier.get(); + + assertNotSame(initialAccount, accountAndDevice.first()); + assertNotSame(initialDevice, accountAndDevice.second()); + + assertEquals(uuid, accountAndDevice.first().getUuid()); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RegistrationRecoveryTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RegistrationRecoveryTest.java new file mode 100644 index 000000000..1763fb977 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RegistrationRecoveryTest.java @@ -0,0 +1,155 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; +import org.whispersystems.textsecuregcm.util.AttributeValues; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import org.whispersystems.textsecuregcm.util.MockUtils; +import org.whispersystems.textsecuregcm.util.MutableClock; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; + +public class RegistrationRecoveryTest { + + private static final MutableClock CLOCK = MockUtils.mutableClock(0); + + private static final Duration EXPIRATION = Duration.ofSeconds(1000); + + private static final String NUMBER = "+18005555555"; + + private static final SaltedTokenHash ORIGINAL_HASH = SaltedTokenHash.generateFor("pass1"); + + private static final SaltedTokenHash ANOTHER_HASH = SaltedTokenHash.generateFor("pass2"); + + @RegisterExtension + private static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension( + Tables.REGISTRATION_RECOVERY_PASSWORDS); + + private RegistrationRecoveryPasswords store; + + private RegistrationRecoveryPasswordsManager manager; + + @BeforeEach + public void before() throws Exception { + CLOCK.setTimeMillis(Clock.systemUTC().millis()); + store = new RegistrationRecoveryPasswords( + Tables.REGISTRATION_RECOVERY_PASSWORDS.tableName(), + EXPIRATION, + DYNAMO_DB_EXTENSION.getDynamoDbClient(), + DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + CLOCK + ); + manager = new RegistrationRecoveryPasswordsManager(store); + } + + @Test + public void testLookupAfterWrite() throws Exception { + store.addOrReplace(NUMBER, ORIGINAL_HASH).get(); + final long initialExp = fetchTimestamp(NUMBER); + final long expectedExpiration = CLOCK.instant().getEpochSecond() + EXPIRATION.getSeconds(); + assertEquals(expectedExpiration, initialExp); + + final Optional saltedTokenHash = store.lookup(NUMBER).get(); + assertTrue(saltedTokenHash.isPresent()); + assertEquals(ORIGINAL_HASH.salt(), saltedTokenHash.get().salt()); + assertEquals(ORIGINAL_HASH.hash(), saltedTokenHash.get().hash()); + } + + @Test + public void testLookupAfterRefresh() throws Exception { + store.addOrReplace(NUMBER, ORIGINAL_HASH).get(); + + CLOCK.increment(50, TimeUnit.SECONDS); + store.addOrReplace(NUMBER, ORIGINAL_HASH).get(); + final long updatedExp = fetchTimestamp(NUMBER); + final long expectedExp = CLOCK.instant().getEpochSecond() + EXPIRATION.getSeconds(); + assertEquals(expectedExp, updatedExp); + + final Optional saltedTokenHash = store.lookup(NUMBER).get(); + assertTrue(saltedTokenHash.isPresent()); + assertEquals(ORIGINAL_HASH.salt(), saltedTokenHash.get().salt()); + assertEquals(ORIGINAL_HASH.hash(), saltedTokenHash.get().hash()); + } + + @Test + public void testReplace() throws Exception { + store.addOrReplace(NUMBER, ORIGINAL_HASH).get(); + store.addOrReplace(NUMBER, ANOTHER_HASH).get(); + + final Optional saltedTokenHash = store.lookup(NUMBER).get(); + assertTrue(saltedTokenHash.isPresent()); + assertEquals(ANOTHER_HASH.salt(), saltedTokenHash.get().salt()); + assertEquals(ANOTHER_HASH.hash(), saltedTokenHash.get().hash()); + } + + @Test + public void testRemove() throws Exception { + store.addOrReplace(NUMBER, ORIGINAL_HASH).get(); + assertTrue(store.lookup(NUMBER).get().isPresent()); + + store.removeEntry(NUMBER).get(); + assertTrue(store.lookup(NUMBER).get().isEmpty()); + } + + @Test + public void testManagerFlow() throws Exception { + final byte[] password = "password".getBytes(StandardCharsets.UTF_8); + final byte[] updatedPassword = "udpate".getBytes(StandardCharsets.UTF_8); + final byte[] wrongPassword = "qwerty123".getBytes(StandardCharsets.UTF_8); + + // initial store + manager.storeForCurrentNumber(NUMBER, password).get(); + assertTrue(manager.verify(NUMBER, password).get()); + assertFalse(manager.verify(NUMBER, wrongPassword).get()); + + // update + manager.storeForCurrentNumber(NUMBER, password).get(); + assertTrue(manager.verify(NUMBER, password).get()); + assertFalse(manager.verify(NUMBER, wrongPassword).get()); + + // replace + manager.storeForCurrentNumber(NUMBER, updatedPassword).get(); + assertTrue(manager.verify(NUMBER, updatedPassword).get()); + assertFalse(manager.verify(NUMBER, password).get()); + assertFalse(manager.verify(NUMBER, wrongPassword).get()); + + manager.removeForNumber(NUMBER).get(); + assertFalse(manager.verify(NUMBER, updatedPassword).get()); + assertFalse(manager.verify(NUMBER, password).get()); + assertFalse(manager.verify(NUMBER, wrongPassword).get()); + } + + private static long fetchTimestamp(final String number) throws ExecutionException, InterruptedException { + return DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient().getItem(GetItemRequest.builder() + .tableName(Tables.REGISTRATION_RECOVERY_PASSWORDS.tableName()) + .key(Map.of(RegistrationRecoveryPasswords.KEY_E164, AttributeValues.fromString(number))) + .build()) + .thenApply(getItemResponse -> { + final Map item = getItemResponse.item(); + if (item == null || !item.containsKey(RegistrationRecoveryPasswords.ATTR_EXP)) { + throw new RuntimeException("Data not found"); + } + final String exp = item.get(RegistrationRecoveryPasswords.ATTR_EXP).n(); + return Long.parseLong(exp); + }) + .get(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RemoteConfigsManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RemoteConfigsManagerTest.java new file mode 100644 index 000000000..1cb849d8f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RemoteConfigsManagerTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; + +class RemoteConfigsManagerTest { + + private RemoteConfigs remoteConfigs; + private RemoteConfigsManager remoteConfigsManager; + + @BeforeEach + void setup() { + this.remoteConfigs = mock(RemoteConfigs.class); + this.remoteConfigsManager = new RemoteConfigsManager(remoteConfigs); + } + + @Test + void testGetAll() { + remoteConfigsManager.getAll(); + remoteConfigsManager.getAll(); + + // A memoized supplier should prevent multiple calls to the underlying data source + verify(remoteConfigs, times(1)).getAll(); + } + + @Test + void testSet() { + final RemoteConfig remoteConfig = mock(RemoteConfig.class); + + remoteConfigsManager.set(remoteConfig); + remoteConfigsManager.set(remoteConfig); + + verify(remoteConfigs, times(2)).set(remoteConfig); + } + + @Test + void testDelete() { + final String name = "name"; + + remoteConfigsManager.delete(name); + remoteConfigsManager.delete(name); + + verify(remoteConfigs, times(2)).delete(name); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RemoteConfigsTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RemoteConfigsTest.java new file mode 100644 index 000000000..92f874d16 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RemoteConfigsTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; + +class RemoteConfigsTest { + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(Tables.REMOTE_CONFIGS); + + private RemoteConfigs remoteConfigs; + + @BeforeEach + void setUp() { + remoteConfigs = new RemoteConfigs(DYNAMO_DB_EXTENSION.getDynamoDbClient(), Tables.REMOTE_CONFIGS.tableName()); + } + + @Test + void testStore() { + remoteConfigs.set(new RemoteConfig("android.stickers", 50, Set.of(AuthHelper.VALID_UUID, AuthHelper.VALID_UUID_TWO), "FALSE", "TRUE", null)); + remoteConfigs.set(new RemoteConfig("value.sometimes", 25, Set.of(AuthHelper.VALID_UUID_TWO), "default", "custom", null)); + + List configs = remoteConfigs.getAll(); + + assertThat(configs).hasSize(2); + + assertThat(configs.get(0).getName()).isEqualTo("android.stickers"); + assertThat(configs.get(0).getValue()).isEqualTo("TRUE"); + assertThat(configs.get(0).getDefaultValue()).isEqualTo("FALSE"); + assertThat(configs.get(0).getPercentage()).isEqualTo(50); + assertThat(configs.get(0).getUuids()).hasSize(2); + assertThat(configs.get(0).getUuids()).contains(AuthHelper.VALID_UUID); + assertThat(configs.get(0).getUuids()).contains(AuthHelper.VALID_UUID_TWO); + assertThat(configs.get(0).getUuids()).doesNotContain(AuthHelper.INVALID_UUID); + + assertThat(configs.get(1).getName()).isEqualTo("value.sometimes"); + assertThat(configs.get(1).getValue()).isEqualTo("custom"); + assertThat(configs.get(1).getDefaultValue()).isEqualTo("default"); + assertThat(configs.get(1).getPercentage()).isEqualTo(25); + assertThat(configs.get(1).getUuids()).hasSize(1); + assertThat(configs.get(1).getUuids()).contains(AuthHelper.VALID_UUID_TWO); + assertThat(configs.get(1).getUuids()).doesNotContain(AuthHelper.VALID_UUID); + assertThat(configs.get(1).getUuids()).doesNotContain(AuthHelper.INVALID_UUID); + } + + @Test + void testUpdate() { + remoteConfigs.set(new RemoteConfig("android.stickers", 50, Set.of(), "FALSE", "TRUE", null)); + remoteConfigs.set(new RemoteConfig("value.sometimes", 22, Set.of(), "def", "!", null)); + remoteConfigs.set(new RemoteConfig("ios.stickers", 50, Set.of(AuthHelper.DISABLED_UUID), "FALSE", "TRUE", null)); + remoteConfigs.set(new RemoteConfig("ios.stickers", 75, Set.of(), "FALSE", "TRUE", null)); + remoteConfigs.set(new RemoteConfig("value.sometimes", 77, Set.of(), "hey", "wut", null)); + + List configs = remoteConfigs.getAll(); + + assertThat(configs).hasSize(3); + + assertThat(configs.get(0).getName()).isEqualTo("android.stickers"); + assertThat(configs.get(0).getPercentage()).isEqualTo(50); + assertThat(configs.get(0).getUuids()).isEmpty(); + assertThat(configs.get(0).getDefaultValue()).isEqualTo("FALSE"); + assertThat(configs.get(0).getValue()).isEqualTo("TRUE"); + + assertThat(configs.get(1).getName()).isEqualTo("ios.stickers"); + assertThat(configs.get(1).getPercentage()).isEqualTo(75); + assertThat(configs.get(1).getUuids()).isEmpty(); + assertThat(configs.get(1).getDefaultValue()).isEqualTo("FALSE"); + assertThat(configs.get(1).getValue()).isEqualTo("TRUE"); + + assertThat(configs.get(2).getName()).isEqualTo("value.sometimes"); + assertThat(configs.get(2).getPercentage()).isEqualTo(77); + assertThat(configs.get(2).getUuids()).isEmpty(); + assertThat(configs.get(2).getDefaultValue()).isEqualTo("hey"); + assertThat(configs.get(2).getValue()).isEqualTo("wut"); + } + + @Test + void testDelete() { + remoteConfigs.set(new RemoteConfig("android.stickers", 50, Set.of(AuthHelper.VALID_UUID), "FALSE", "TRUE", null)); + remoteConfigs.set(new RemoteConfig("ios.stickers", 50, Set.of(), "FALSE", "TRUE", null)); + remoteConfigs.set(new RemoteConfig("ios.stickers", 75, Set.of(), "FALSE", "TRUE", null)); + remoteConfigs.set(new RemoteConfig("value.always", 100, Set.of(), "never", "always", null)); + remoteConfigs.delete("android.stickers"); + + List configs = remoteConfigs.getAll(); + + assertThat(configs).hasSize(2); + + assertThat(configs.get(0).getName()).isEqualTo("ios.stickers"); + assertThat(configs.get(0).getPercentage()).isEqualTo(75); + assertThat(configs.get(0).getDefaultValue()).isEqualTo("FALSE"); + assertThat(configs.get(0).getValue()).isEqualTo("TRUE"); + + assertThat(configs.get(1).getName()).isEqualTo("value.always"); + assertThat(configs.get(1).getPercentage()).isEqualTo(100); + assertThat(configs.get(1).getValue()).isEqualTo("always"); + assertThat(configs.get(1).getDefaultValue()).isEqualTo("never"); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RepeatedUseECSignedPreKeyStoreTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RepeatedUseECSignedPreKeyStoreTest.java new file mode 100644 index 000000000..57dcdfe95 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RepeatedUseECSignedPreKeyStoreTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; + +class RepeatedUseECSignedPreKeyStoreTest extends RepeatedUseSignedPreKeyStoreTest { + + private RepeatedUseECSignedPreKeyStore keyStore; + + private int currentKeyId = 1; + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = + new DynamoDbExtension(DynamoDbExtensionSchema.Tables.REPEATED_USE_EC_SIGNED_PRE_KEYS); + + private static final ECKeyPair IDENTITY_KEY_PAIR = Curve.generateKeyPair(); + + @BeforeEach + void setUp() { + keyStore = new RepeatedUseECSignedPreKeyStore(DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + DynamoDbExtensionSchema.Tables.REPEATED_USE_EC_SIGNED_PRE_KEYS.tableName()); + } + + @Override + protected RepeatedUseSignedPreKeyStore getKeyStore() { + return keyStore; + } + + @Override + protected ECSignedPreKey generateSignedPreKey() { + return KeysHelper.signedECPreKey(currentKeyId++, IDENTITY_KEY_PAIR); + } + + @Test + void storeIfAbsent() { + final UUID identifier = UUID.randomUUID(); + final long deviceIdWithExistingKey = 1; + final long deviceIdWithoutExistingKey = deviceIdWithExistingKey + 1; + + final ECSignedPreKey originalSignedPreKey = generateSignedPreKey(); + + keyStore.store(identifier, deviceIdWithExistingKey, originalSignedPreKey).join(); + + assertFalse(keyStore.storeIfAbsent(identifier, deviceIdWithExistingKey, generateSignedPreKey()).join()); + assertTrue(keyStore.storeIfAbsent(identifier, deviceIdWithoutExistingKey, generateSignedPreKey()).join()); + + assertEquals(Optional.of(originalSignedPreKey), keyStore.find(identifier, deviceIdWithExistingKey).join()); + assertTrue(keyStore.find(identifier, deviceIdWithoutExistingKey).join().isPresent()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RepeatedUseKEMSignedPreKeyStoreTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RepeatedUseKEMSignedPreKeyStoreTest.java new file mode 100644 index 000000000..57a0c4751 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RepeatedUseKEMSignedPreKeyStoreTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; + +class RepeatedUseKEMSignedPreKeyStoreTest extends RepeatedUseSignedPreKeyStoreTest { + + private RepeatedUseKEMSignedPreKeyStore keyStore; + + private int currentKeyId = 1; + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = + new DynamoDbExtension(DynamoDbExtensionSchema.Tables.REPEATED_USE_KEM_SIGNED_PRE_KEYS); + + private static final ECKeyPair IDENTITY_KEY_PAIR = Curve.generateKeyPair(); + + @BeforeEach + void setUp() { + keyStore = new RepeatedUseKEMSignedPreKeyStore(DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + DynamoDbExtensionSchema.Tables.REPEATED_USE_KEM_SIGNED_PRE_KEYS.tableName()); + } + + @Override + protected RepeatedUseSignedPreKeyStore getKeyStore() { + return keyStore; + } + + @Override + protected KEMSignedPreKey generateSignedPreKey() { + return KeysHelper.signedKEMPreKey(currentKeyId++, IDENTITY_KEY_PAIR); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RepeatedUseSignedPreKeyStoreTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RepeatedUseSignedPreKeyStoreTest.java new file mode 100644 index 000000000..456b24450 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/RepeatedUseSignedPreKeyStoreTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.entities.SignedPreKey; + +abstract class RepeatedUseSignedPreKeyStoreTest> { + + protected abstract RepeatedUseSignedPreKeyStore getKeyStore(); + + protected abstract K generateSignedPreKey(); + + @Test + void storeFind() { + final RepeatedUseSignedPreKeyStore keys = getKeyStore(); + + assertEquals(Optional.empty(), keys.find(UUID.randomUUID(), 1).join()); + + { + final UUID identifier = UUID.randomUUID(); + final long deviceId = 1; + final K signedPreKey = generateSignedPreKey(); + + assertDoesNotThrow(() -> keys.store(identifier, deviceId, signedPreKey).join()); + assertEquals(Optional.of(signedPreKey), keys.find(identifier, deviceId).join()); + } + + { + final UUID identifier = UUID.randomUUID(); + final Map signedPreKeys = Map.of( + 1L, generateSignedPreKey(), + 2L, generateSignedPreKey() + ); + + assertDoesNotThrow(() -> keys.store(identifier, signedPreKeys).join()); + assertEquals(Optional.of(signedPreKeys.get(1L)), keys.find(identifier, 1).join()); + assertEquals(Optional.of(signedPreKeys.get(2L)), keys.find(identifier, 2).join()); + } + } + + @Test + void delete() { + final RepeatedUseSignedPreKeyStore keys = getKeyStore(); + + assertDoesNotThrow(() -> keys.delete(UUID.randomUUID()).join()); + + { + final UUID identifier = UUID.randomUUID(); + final Map signedPreKeys = Map.of( + 1L, generateSignedPreKey(), + 2L, generateSignedPreKey() + ); + + keys.store(identifier, signedPreKeys).join(); + keys.delete(identifier, 1).join(); + + assertEquals(Optional.empty(), keys.find(identifier, 1).join()); + assertEquals(Optional.of(signedPreKeys.get(2L)), keys.find(identifier, 2).join()); + } + + { + final UUID identifier = UUID.randomUUID(); + final Map signedPreKeys = Map.of( + 1L, generateSignedPreKey(), + 2L, generateSignedPreKey() + ); + + keys.store(identifier, signedPreKeys).join(); + keys.delete(identifier).join(); + + assertEquals(Optional.empty(), keys.find(identifier, 1).join()); + assertEquals(Optional.empty(), keys.find(identifier, 2).join()); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ReportMessageDynamoDbTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ReportMessageDynamoDbTest.java new file mode 100644 index 000000000..89cd11b02 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ReportMessageDynamoDbTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import org.whispersystems.textsecuregcm.util.UUIDUtil; + +class ReportMessageDynamoDbTest { + + private ReportMessageDynamoDb reportMessageDynamoDb; + + @RegisterExtension + static DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(Tables.REPORT_MESSAGES); + + + @BeforeEach + void setUp() { + this.reportMessageDynamoDb = new ReportMessageDynamoDb( + DYNAMO_DB_EXTENSION.getDynamoDbClient(), + Tables.REPORT_MESSAGES.tableName(), + Duration.ofDays(1)); + } + + @Test + void testStore() { + + final byte[] hash1 = UUIDUtil.toBytes(UUID.randomUUID()); + final byte[] hash2 = UUIDUtil.toBytes(UUID.randomUUID()); + + assertAll("database should be empty", + () -> assertFalse(reportMessageDynamoDb.remove(hash1)), + () -> assertFalse(reportMessageDynamoDb.remove(hash2)) + ); + + reportMessageDynamoDb.store(hash1); + reportMessageDynamoDb.store(hash2); + + assertAll("both hashes should be found", + () -> assertTrue(reportMessageDynamoDb.remove(hash1)), + () -> assertTrue(reportMessageDynamoDb.remove(hash2)) + ); + + assertAll( "database should be empty", + () -> assertFalse(reportMessageDynamoDb.remove(hash1)), + () -> assertFalse(reportMessageDynamoDb.remove(hash2)) + ); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ReportMessageManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ReportMessageManagerTest.java new file mode 100644 index 000000000..2cc6d9adb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/ReportMessageManagerTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2021-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.time.Duration; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; + +class ReportMessageManagerTest { + + private ReportMessageDynamoDb reportMessageDynamoDb; + + private ReportMessageManager reportMessageManager; + + private String sourceNumber; + private UUID sourceAci; + private UUID sourcePni; + private Account sourceAccount; + private UUID messageGuid; + private UUID reporterUuid; + + @RegisterExtension + static RedisClusterExtension RATE_LIMIT_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + @BeforeEach + void setUp() { + reportMessageDynamoDb = mock(ReportMessageDynamoDb.class); + + reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, + RATE_LIMIT_CLUSTER_EXTENSION.getRedisCluster(), Duration.ofDays(1)); + + sourceNumber = "+15105551111"; + sourceAci = UUID.randomUUID(); + sourcePni = UUID.randomUUID(); + messageGuid = UUID.randomUUID(); + reporterUuid = UUID.randomUUID(); + + sourceAccount = mock(Account.class); + when(sourceAccount.getUuid()).thenReturn(sourceAci); + when(sourceAccount.getNumber()).thenReturn(sourceNumber); + when(sourceAccount.getPhoneNumberIdentifier()).thenReturn(sourcePni); + } + + @Test + void testStore() { + assertDoesNotThrow(() -> reportMessageManager.store(null, messageGuid)); + + verifyNoInteractions(reportMessageDynamoDb); + + reportMessageManager.store(sourceAci.toString(), messageGuid); + + verify(reportMessageDynamoDb).store(any()); + + doThrow(RuntimeException.class) + .when(reportMessageDynamoDb).store(any()); + + assertDoesNotThrow(() -> reportMessageManager.store(sourceAci.toString(), messageGuid)); + } + + @Test + void testReport() { + final ReportedMessageListener listener = mock(ReportedMessageListener.class); + reportMessageManager.addListener(listener); + + when(reportMessageDynamoDb.remove(any())).thenReturn(false); + reportMessageManager.report(Optional.of(sourceNumber), Optional.of(sourceAci), Optional.of(sourcePni), messageGuid, + reporterUuid, Optional.empty(), "user-agent"); + + assertEquals(0, reportMessageManager.getRecentReportCount(sourceAccount)); + + when(reportMessageDynamoDb.remove(any())).thenReturn(true); + reportMessageManager.report(Optional.of(sourceNumber), Optional.of(sourceAci), Optional.of(sourcePni), messageGuid, + reporterUuid, Optional.empty(), "user-agent"); + + assertEquals(1, reportMessageManager.getRecentReportCount(sourceAccount)); + verify(listener).handleMessageReported(sourceNumber, messageGuid, reporterUuid, Optional.empty()); + } + + @Test + void testReportMultipleReporters() { + when(reportMessageDynamoDb.remove(any())).thenReturn(true); + assertEquals(0, reportMessageManager.getRecentReportCount(sourceAccount)); + + for (int i = 0; i < 100; i++) { + reportMessageManager.report(Optional.of(sourceNumber), Optional.of(sourceAci), Optional.of(sourcePni), + messageGuid, UUID.randomUUID(), Optional.empty(), "user-agent"); + } + + assertTrue(reportMessageManager.getRecentReportCount(sourceAccount) > 10); + } + + @Test + void testReportSingleReporter() { + when(reportMessageDynamoDb.remove(any())).thenReturn(true); + assertEquals(0, reportMessageManager.getRecentReportCount(sourceAccount)); + + for (int i = 0; i < 100; i++) { + reportMessageManager.report(Optional.of(sourceNumber), Optional.of(sourceAci), Optional.of(sourcePni), + messageGuid, + reporterUuid, Optional.empty(), "user-agent"); + } + + assertEquals(1, reportMessageManager.getRecentReportCount(sourceAccount)); + } + + @Test + void testReportMultipleReportersByPni() { + when(reportMessageDynamoDb.remove(any())).thenReturn(true); + assertEquals(0, reportMessageManager.getRecentReportCount(sourceAccount)); + + for (int i = 0; i < 100; i++) { + reportMessageManager.report(Optional.empty(), Optional.of(sourceAci), Optional.of(sourcePni), + messageGuid, UUID.randomUUID(), Optional.empty(), "user-agent"); + } + + reportMessageManager.report(Optional.empty(), Optional.of(sourceAci), Optional.empty(), + messageGuid, UUID.randomUUID(), Optional.empty(), "user-agent"); + + final int recentReportCount = reportMessageManager.getRecentReportCount(sourceAccount); + assertTrue(recentReportCount > 10); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SerializedExpireableJsonDynamoStoreTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SerializedExpireableJsonDynamoStoreTest.java new file mode 100644 index 000000000..4205e771f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SerializedExpireableJsonDynamoStoreTest.java @@ -0,0 +1,187 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; + +class SerializedExpireableJsonDynamoStoreTest { + + static abstract class Tests { + + private static final String TABLE_NAME = "test"; + private static final String KEY = "foo"; + + static final Clock clock = Clock.systemUTC(); + + interface Value { + + String v(); + } + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension( + new DynamoDbExtension.RawSchema( + TABLE_NAME, + SerializedExpireableJsonDynamoStore.KEY_KEY, + null, + List.of(AttributeDefinition.builder() + .attributeName(SerializedExpireableJsonDynamoStore.KEY_KEY) + .attributeType(ScalarAttributeType.S) + .build()), + List.of(), + List.of())); + + private SerializedExpireableJsonDynamoStore store; + + abstract SerializedExpireableJsonDynamoStore getStore(final DynamoDbAsyncClient dynamoDbClient, + final String tableName); + + abstract T testValue(final String v); + + abstract T maybeExpiredTestValue(final String v); + + @BeforeEach + void setUp() { + store = getStore(DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), TABLE_NAME); + } + + @Test + void testStoreAndFind() throws Exception { + assertEquals(Optional.empty(), store.findForKey(KEY).get(1, TimeUnit.SECONDS)); + + final T original = testValue("1234"); + final T second = testValue("5678"); + + store.insert(KEY, original).get(1, TimeUnit.SECONDS); + { + final Optional maybeValue = store.findForKey(KEY).get(1, TimeUnit.SECONDS); + + assertTrue(maybeValue.isPresent()); + assertEquals(original, maybeValue.get()); + } + + assertThrows(Exception.class, () -> store.insert(KEY, second).get(1, TimeUnit.SECONDS)); + + assertDoesNotThrow(() -> store.update(KEY, second).get(1, TimeUnit.SECONDS)); + { + final Optional maybeValue = store.findForKey(KEY).get(1, TimeUnit.SECONDS); + + assertTrue(maybeValue.isPresent()); + assertEquals(second, maybeValue.get()); + } + } + + @Test + void testRemove() throws Exception { + assertEquals(Optional.empty(), store.findForKey(KEY).get(1, TimeUnit.SECONDS)); + + store.insert(KEY, testValue("1234")).get(1, TimeUnit.SECONDS); + assertTrue(store.findForKey(KEY).get(1, TimeUnit.SECONDS).isPresent()); + + store.remove(KEY).get(1, TimeUnit.SECONDS); + assertFalse(store.findForKey(KEY).get(1, TimeUnit.SECONDS).isPresent()); + + final T v = maybeExpiredTestValue("1234"); + store.insert(KEY, v).get(1, TimeUnit.SECONDS); + + assertEquals(v instanceof SerializedExpireableJsonDynamoStore.Expireable, + store.findForKey(KEY).get(1, TimeUnit.SECONDS).isEmpty()); + } + + } + + record Expires(String v, long timestamp) implements SerializedExpireableJsonDynamoStore.Expireable, Tests.Value { + + static final Duration EXPIRATION = Duration.ofSeconds(30); + + @Override + public long getExpirationEpochSeconds() { + return Instant.ofEpochMilli(timestamp()).plus(EXPIRATION).getEpochSecond(); + } + } + + @Nested + class Expireable extends Tests { + + class ExpiresStore extends SerializedExpireableJsonDynamoStore { + + public ExpiresStore(final DynamoDbAsyncClient dynamoDbClient, final String tableName) { + super(dynamoDbClient, tableName, clock); + } + } + + private static final long VALID_TIMESTAMP = Instant.now().toEpochMilli(); + private static final long EXPIRED_TIMESTAMP = Instant.now().minus(Expires.EXPIRATION).minus( + Duration.ofHours(1)).toEpochMilli(); + + @Override + SerializedExpireableJsonDynamoStore getStore(final DynamoDbAsyncClient dynamoDbClient, + final String tableName) { + return new ExpiresStore(dynamoDbClient, tableName); + } + + @Override + Expires testValue(final String v) { + return new Expires(v, VALID_TIMESTAMP); + } + + @Override + Expires maybeExpiredTestValue(final String v) { + return new Expires(v, EXPIRED_TIMESTAMP); + } + } + + record DoesNotExpire(String v) implements Tests.Value { + + } + + + @Nested + class NotExpireable extends Tests { + + class DoesNotExpireStore extends SerializedExpireableJsonDynamoStore { + + public DoesNotExpireStore(final DynamoDbAsyncClient dynamoDbClient, final String tableName) { + super(dynamoDbClient, tableName, clock); + } + } + + @Override + SerializedExpireableJsonDynamoStore getStore(final DynamoDbAsyncClient dynamoDbClient, + final String tableName) { + return new DoesNotExpireStore(dynamoDbClient, tableName); + } + + @Override + DoesNotExpire testValue(final String v) { + return new DoesNotExpire(v); + } + + @Override + DoesNotExpire maybeExpiredTestValue(final String v) { + return new DoesNotExpire(v); + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SingleUseECPreKeyStoreTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SingleUseECPreKeyStoreTest.java new file mode 100644 index 000000000..52044e4f9 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SingleUseECPreKeyStoreTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.signal.libsignal.protocol.ecc.Curve; +import org.whispersystems.textsecuregcm.entities.ECPreKey; + +class SingleUseECPreKeyStoreTest extends SingleUsePreKeyStoreTest { + + private SingleUseECPreKeyStore preKeyStore; + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(DynamoDbExtensionSchema.Tables.EC_KEYS); + + @BeforeEach + void setUp() { + preKeyStore = new SingleUseECPreKeyStore(DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + DynamoDbExtensionSchema.Tables.EC_KEYS.tableName()); + } + + @Override + protected SingleUsePreKeyStore getPreKeyStore() { + return preKeyStore; + } + + @Override + protected ECPreKey generatePreKey(final long keyId) { + return new ECPreKey(keyId, Curve.generateKeyPair().getPublicKey()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SingleUseKEMPreKeyStoreTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SingleUseKEMPreKeyStoreTest.java new file mode 100644 index 000000000..e21685f3c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SingleUseKEMPreKeyStoreTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.tests.util.KeysHelper; + +class SingleUseKEMPreKeyStoreTest extends SingleUsePreKeyStoreTest { + + private SingleUseKEMPreKeyStore preKeyStore; + + private static final ECKeyPair IDENTITY_KEY_PAIR = Curve.generateKeyPair(); + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(DynamoDbExtensionSchema.Tables.PQ_KEYS); + + @BeforeEach + void setUp() { + preKeyStore = new SingleUseKEMPreKeyStore(DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), + DynamoDbExtensionSchema.Tables.PQ_KEYS.tableName()); + } + + @Override + protected SingleUsePreKeyStore getPreKeyStore() { + return preKeyStore; + } + + @Override + protected KEMSignedPreKey generatePreKey(final long keyId) { + return KeysHelper.signedKEMPreKey(keyId, IDENTITY_KEY_PAIR); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SingleUsePreKeyStoreTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SingleUsePreKeyStoreTest.java new file mode 100644 index 000000000..e284a6eb1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SingleUsePreKeyStoreTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.entities.PreKey; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +abstract class SingleUsePreKeyStoreTest> { + + private static final int KEY_COUNT = 100; + + protected abstract SingleUsePreKeyStore getPreKeyStore(); + + protected abstract K generatePreKey(final long keyId); + + @Test + void storeTake() { + final SingleUsePreKeyStore preKeyStore = getPreKeyStore(); + + final UUID accountIdentifier = UUID.randomUUID(); + final long deviceId = 1; + + assertEquals(Optional.empty(), preKeyStore.take(accountIdentifier, deviceId).join()); + + final List preKeys = new ArrayList<>(KEY_COUNT); + + for (int i = 0; i < KEY_COUNT; i++) { + preKeys.add(generatePreKey(i)); + } + + assertDoesNotThrow(() -> preKeyStore.store(accountIdentifier, deviceId, preKeys).join()); + + assertEquals(Optional.of(preKeys.get(0)), preKeyStore.take(accountIdentifier, deviceId).join()); + assertEquals(Optional.of(preKeys.get(1)), preKeyStore.take(accountIdentifier, deviceId).join()); + } + + @Test + void getCount() { + final SingleUsePreKeyStore preKeyStore = getPreKeyStore(); + + final UUID accountIdentifier = UUID.randomUUID(); + final long deviceId = 1; + + assertEquals(0, preKeyStore.getCount(accountIdentifier, deviceId).join()); + + final List preKeys = new ArrayList<>(KEY_COUNT); + + for (int i = 0; i < KEY_COUNT; i++) { + preKeys.add(generatePreKey(i)); + } + + preKeyStore.store(accountIdentifier, deviceId, preKeys).join(); + + assertEquals(KEY_COUNT, preKeyStore.getCount(accountIdentifier, deviceId).join()); + } + + @Test + void deleteSingleDevice() { + final SingleUsePreKeyStore preKeyStore = getPreKeyStore(); + + final UUID accountIdentifier = UUID.randomUUID(); + final long deviceId = 1; + + assertEquals(0, preKeyStore.getCount(accountIdentifier, deviceId).join()); + assertDoesNotThrow(() -> preKeyStore.delete(accountIdentifier, deviceId).join()); + + final List preKeys = new ArrayList<>(KEY_COUNT); + + for (int i = 0; i < KEY_COUNT; i++) { + preKeys.add(generatePreKey(i)); + } + + preKeyStore.store(accountIdentifier, deviceId, preKeys).join(); + preKeyStore.store(accountIdentifier, deviceId + 1, preKeys).join(); + + assertDoesNotThrow(() -> preKeyStore.delete(accountIdentifier, deviceId).join()); + + assertEquals(0, preKeyStore.getCount(accountIdentifier, deviceId).join()); + assertEquals(KEY_COUNT, preKeyStore.getCount(accountIdentifier, deviceId + 1).join()); + } + + @Test + void deleteAllDevices() { + final SingleUsePreKeyStore preKeyStore = getPreKeyStore(); + + final UUID accountIdentifier = UUID.randomUUID(); + final long deviceId = 1; + + assertEquals(0, preKeyStore.getCount(accountIdentifier, deviceId).join()); + assertDoesNotThrow(() -> preKeyStore.delete(accountIdentifier).join()); + + final List preKeys = new ArrayList<>(KEY_COUNT); + + for (int i = 0; i < KEY_COUNT; i++) { + preKeys.add(generatePreKey(i)); + } + + preKeyStore.store(accountIdentifier, deviceId, preKeys).join(); + preKeyStore.store(accountIdentifier, deviceId + 1, preKeys).join(); + + assertDoesNotThrow(() -> preKeyStore.delete(accountIdentifier).join()); + + assertEquals(0, preKeyStore.getCount(accountIdentifier, deviceId).join()); + assertEquals(0, preKeyStore.getCount(accountIdentifier, deviceId + 1).join()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SubscriptionManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SubscriptionManagerTest.java new file mode 100644 index 000000000..81bf6348d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/SubscriptionManagerTest.java @@ -0,0 +1,268 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.whispersystems.textsecuregcm.storage.SubscriptionManager.GetResult.Type.FOUND; +import static org.whispersystems.textsecuregcm.storage.SubscriptionManager.GetResult.Type.NOT_STORED; +import static org.whispersystems.textsecuregcm.storage.SubscriptionManager.GetResult.Type.PASSWORD_MISMATCH; + +import java.security.SecureRandom; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import javax.ws.rs.ClientErrorException; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.storage.SubscriptionManager.GetResult; +import org.whispersystems.textsecuregcm.storage.SubscriptionManager.Record; +import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer; +import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; + +class SubscriptionManagerTest { + + private static final long NOW_EPOCH_SECONDS = 1_500_000_000L; + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(3); + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(Tables.SUBSCRIPTIONS); + + byte[] user; + byte[] password; + String customer; + Instant created; + SubscriptionManager subscriptionManager; + + @BeforeEach + void beforeEach() { + user = getRandomBytes(16); + password = getRandomBytes(16); + customer = Base64.getEncoder().encodeToString(getRandomBytes(16)); + created = Instant.ofEpochSecond(NOW_EPOCH_SECONDS); + subscriptionManager = new SubscriptionManager( + Tables.SUBSCRIPTIONS.tableName(), DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient()); + } + + @Test + void testCreateOnlyOnce() { + byte[] password1 = getRandomBytes(16); + byte[] password2 = getRandomBytes(16); + Instant created1 = Instant.ofEpochSecond(NOW_EPOCH_SECONDS); + Instant created2 = Instant.ofEpochSecond(NOW_EPOCH_SECONDS + 1); + + CompletableFuture getFuture = subscriptionManager.get(user, password1); + assertThat(getFuture).succeedsWithin(DEFAULT_TIMEOUT).satisfies(getResult -> { + assertThat(getResult.type).isEqualTo(NOT_STORED); + assertThat(getResult.record).isNull(); + }); + + getFuture = subscriptionManager.get(user, password2); + assertThat(getFuture).succeedsWithin(DEFAULT_TIMEOUT).satisfies(getResult -> { + assertThat(getResult.type).isEqualTo(NOT_STORED); + assertThat(getResult.record).isNull(); + }); + + CompletableFuture createFuture = + subscriptionManager.create(user, password1, created1); + Consumer recordRequirements = checkFreshlyCreatedRecord(user, password1, created1); + assertThat(createFuture).succeedsWithin(DEFAULT_TIMEOUT).satisfies(recordRequirements); + + // password check fails so this should return null + createFuture = subscriptionManager.create(user, password2, created2); + assertThat(createFuture).succeedsWithin(DEFAULT_TIMEOUT).isNull(); + + // password check matches, but the record already exists so nothing should get updated + createFuture = subscriptionManager.create(user, password1, created2); + assertThat(createFuture).succeedsWithin(DEFAULT_TIMEOUT).satisfies(recordRequirements); + } + + @Test + void testGet() { + byte[] wrongUser = getRandomBytes(16); + byte[] wrongPassword = getRandomBytes(16); + assertThat(subscriptionManager.create(user, password, created)).succeedsWithin(DEFAULT_TIMEOUT); + + assertThat(subscriptionManager.get(user, password)).succeedsWithin(DEFAULT_TIMEOUT).satisfies(getResult -> { + assertThat(getResult.type).isEqualTo(FOUND); + assertThat(getResult.record).isNotNull().satisfies(checkFreshlyCreatedRecord(user, password, created)); + }); + + assertThat(subscriptionManager.get(user, wrongPassword)).succeedsWithin(DEFAULT_TIMEOUT) + .satisfies(getResult -> { + assertThat(getResult.type).isEqualTo(PASSWORD_MISMATCH); + assertThat(getResult.record).isNull(); + }); + + assertThat(subscriptionManager.get(wrongUser, password)).succeedsWithin(DEFAULT_TIMEOUT) + .satisfies(getResult -> { + assertThat(getResult.type).isEqualTo(NOT_STORED); + assertThat(getResult.record).isNull(); + }); + } + + @Test + void testSetCustomerIdAndProcessor() throws Exception { + Instant subscriptionUpdated = Instant.ofEpochSecond(NOW_EPOCH_SECONDS + 1); + assertThat(subscriptionManager.create(user, password, created)).succeedsWithin(DEFAULT_TIMEOUT); + + final CompletableFuture getUser = subscriptionManager.get(user, password); + assertThat(getUser).succeedsWithin(DEFAULT_TIMEOUT); + final Record userRecord = getUser.get().record; + + assertThat(subscriptionManager.setProcessorAndCustomerId(userRecord, + new ProcessorCustomer(customer, SubscriptionProcessor.STRIPE), + subscriptionUpdated)).succeedsWithin(DEFAULT_TIMEOUT) + .hasFieldOrPropertyWithValue("processorCustomer", + Optional.of(new ProcessorCustomer(customer, SubscriptionProcessor.STRIPE))); + + final Condition clientError409Condition = new Condition<>(e -> + e instanceof ClientErrorException cee && cee.getResponse().getStatus() == 409, "Client error: 409"); + + // changing the customer ID is not permitted + assertThat( + subscriptionManager.setProcessorAndCustomerId(userRecord, + new ProcessorCustomer(customer + "1", SubscriptionProcessor.STRIPE), + subscriptionUpdated)).failsWithin(DEFAULT_TIMEOUT) + .withThrowableOfType(ExecutionException.class) + .withCauseInstanceOf(ClientErrorException.class) + .extracting(Throwable::getCause) + .satisfies(clientError409Condition); + + // calling setProcessorAndCustomerId() with the same customer ID is also an error + assertThat( + subscriptionManager.setProcessorAndCustomerId(userRecord, + new ProcessorCustomer(customer, SubscriptionProcessor.STRIPE), + subscriptionUpdated)).failsWithin(DEFAULT_TIMEOUT) + .withThrowableOfType(ExecutionException.class) + .withCauseInstanceOf(ClientErrorException.class) + .extracting(Throwable::getCause) + .satisfies(clientError409Condition); + + assertThat(subscriptionManager.getSubscriberUserByProcessorCustomer( + new ProcessorCustomer(customer, SubscriptionProcessor.STRIPE))) + .succeedsWithin(DEFAULT_TIMEOUT). + isEqualTo(user); + } + + @Test + void testLookupByCustomerId() throws Exception { + Instant subscriptionUpdated = Instant.ofEpochSecond(NOW_EPOCH_SECONDS + 1); + assertThat(subscriptionManager.create(user, password, created)).succeedsWithin(DEFAULT_TIMEOUT); + + final CompletableFuture getUser = subscriptionManager.get(user, password); + assertThat(getUser).succeedsWithin(DEFAULT_TIMEOUT); + final Record userRecord = getUser.get().record; + + assertThat(subscriptionManager.setProcessorAndCustomerId(userRecord, + new ProcessorCustomer(customer, SubscriptionProcessor.STRIPE), + subscriptionUpdated)).succeedsWithin(DEFAULT_TIMEOUT); + assertThat(subscriptionManager.getSubscriberUserByProcessorCustomer( + new ProcessorCustomer(customer, SubscriptionProcessor.STRIPE))). + succeedsWithin(DEFAULT_TIMEOUT). + isEqualTo(user); + } + + @Test + void testCanceledAt() { + Instant canceled = Instant.ofEpochSecond(NOW_EPOCH_SECONDS + 42); + assertThat(subscriptionManager.create(user, password, created)).succeedsWithin(DEFAULT_TIMEOUT); + assertThat(subscriptionManager.canceledAt(user, canceled)).succeedsWithin(DEFAULT_TIMEOUT); + assertThat(subscriptionManager.get(user, password)).succeedsWithin(DEFAULT_TIMEOUT).satisfies(getResult -> { + assertThat(getResult).isNotNull(); + assertThat(getResult.type).isEqualTo(FOUND); + assertThat(getResult.record).isNotNull().satisfies(record -> { + assertThat(record.accessedAt).isEqualTo(canceled); + assertThat(record.canceledAt).isEqualTo(canceled); + assertThat(record.subscriptionId).isNull(); + }); + }); + } + + @Test + void testSubscriptionCreated() { + String subscriptionId = Base64.getEncoder().encodeToString(getRandomBytes(16)); + Instant subscriptionCreated = Instant.ofEpochSecond(NOW_EPOCH_SECONDS + 1); + long level = 42; + assertThat(subscriptionManager.create(user, password, created)).succeedsWithin(DEFAULT_TIMEOUT); + assertThat(subscriptionManager.subscriptionCreated(user, subscriptionId, subscriptionCreated, level)). + succeedsWithin(DEFAULT_TIMEOUT); + assertThat(subscriptionManager.get(user, password)).succeedsWithin(DEFAULT_TIMEOUT).satisfies(getResult -> { + assertThat(getResult).isNotNull(); + assertThat(getResult.type).isEqualTo(FOUND); + assertThat(getResult.record).isNotNull().satisfies(record -> { + assertThat(record.accessedAt).isEqualTo(subscriptionCreated); + assertThat(record.subscriptionId).isEqualTo(subscriptionId); + assertThat(record.subscriptionCreatedAt).isEqualTo(subscriptionCreated); + assertThat(record.subscriptionLevel).isEqualTo(level); + assertThat(record.subscriptionLevelChangedAt).isEqualTo(subscriptionCreated); + }); + }); + } + + @Test + void testSubscriptionLevelChanged() { + Instant at = Instant.ofEpochSecond(NOW_EPOCH_SECONDS + 500); + long level = 1776; + String updatedSubscriptionId = "new"; + assertThat(subscriptionManager.create(user, password, created)).succeedsWithin(DEFAULT_TIMEOUT); + assertThat(subscriptionManager.subscriptionCreated(user, "original", created, level - 1)).succeedsWithin( + DEFAULT_TIMEOUT); + assertThat(subscriptionManager.subscriptionLevelChanged(user, at, level, updatedSubscriptionId)).succeedsWithin( + DEFAULT_TIMEOUT); + assertThat(subscriptionManager.get(user, password)).succeedsWithin(DEFAULT_TIMEOUT).satisfies(getResult -> { + assertThat(getResult).isNotNull(); + assertThat(getResult.type).isEqualTo(FOUND); + assertThat(getResult.record).isNotNull().satisfies(record -> { + assertThat(record.accessedAt).isEqualTo(at); + assertThat(record.subscriptionLevelChangedAt).isEqualTo(at); + assertThat(record.subscriptionLevel).isEqualTo(level); + assertThat(record.subscriptionId).isEqualTo(updatedSubscriptionId); + }); + }); + } + + @Test + void testProcessorAndCustomerId() { + final ProcessorCustomer processorCustomer = + new ProcessorCustomer("abc", SubscriptionProcessor.STRIPE); + + assertThat(processorCustomer.toDynamoBytes()).isEqualTo(new byte[]{1, 97, 98, 99}); + } + + private static byte[] getRandomBytes(int length) { + byte[] result = new byte[length]; + SECURE_RANDOM.nextBytes(result); + return result; + } + + @Nonnull + private static Consumer checkFreshlyCreatedRecord( + byte[] user, byte[] password, Instant created) { + return record -> { + assertThat(record).isNotNull(); + assertThat(record.user).isEqualTo(user); + assertThat(record.password).isEqualTo(password); + assertThat(record.processorCustomer).isNull(); + assertThat(record.createdAt).isEqualTo(created); + assertThat(record.subscriptionId).isNull(); + assertThat(record.subscriptionCreatedAt).isNull(); + assertThat(record.subscriptionLevel).isNull(); + assertThat(record.subscriptionLevelChangedAt).isNull(); + assertThat(record.accessedAt).isEqualTo(created); + assertThat(record.canceledAt).isNull(); + assertThat(record.currentPeriodEndsAt).isNull(); + }; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/VerificationSessionsTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/VerificationSessionsTest.java new file mode 100644 index 000000000..4d6042b85 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/storage/VerificationSessionsTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletionException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.registration.VerificationSession; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import org.whispersystems.textsecuregcm.util.ExceptionUtils; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; + +class VerificationSessionsTest { + + private static final Clock clock = Clock.systemUTC(); + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(Tables.VERIFICATION_SESSIONS); + + private VerificationSessions verificationSessions; + + @BeforeEach + void setUp() { + verificationSessions = new VerificationSessions( + DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), Tables.VERIFICATION_SESSIONS.tableName(), clock); + } + + @Test + void testExpiration() { + final Instant created = Instant.now().minusSeconds(60); + final Instant updates = Instant.now(); + final Duration remoteExpiration = Duration.ofMinutes(2); + + final VerificationSession verificationSession = new VerificationSession(null, + List.of(VerificationSession.Information.PUSH_CHALLENGE), Collections.emptyList(), true, + created.toEpochMilli(), updates.toEpochMilli(), remoteExpiration.toSeconds()); + + assertEquals(updates.plus(remoteExpiration).getEpochSecond(), verificationSession.getExpirationEpochSeconds()); + } + + @Test + void testStore() { + + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + + final String sessionId = "sessionId"; + + final Optional absentSession = verificationSessions.findForKey(sessionId).join(); + assertTrue(absentSession.isEmpty()); + + final VerificationSession session = new VerificationSession(null, + List.of(VerificationSession.Information.PUSH_CHALLENGE), Collections.emptyList(), true, + clock.millis(), clock.millis(), Duration.ofMinutes(1).toSeconds()); + + verificationSessions.insert(sessionId, session).join(); + + assertEquals(session, verificationSessions.findForKey(sessionId).join().orElseThrow()); + + final CompletionException ce = assertThrows(CompletionException.class, + () -> verificationSessions.insert(sessionId, session).join()); + + final Throwable t = ExceptionUtils.unwrap(ce); + assertTrue(t instanceof ConditionalCheckFailedException, + "inserting with the same key should fail conditional checks"); + + final VerificationSession updatedSession = new VerificationSession(null, Collections.emptyList(), + List.of(VerificationSession.Information.PUSH_CHALLENGE), true, clock.millis(), clock.millis(), + Duration.ofMinutes(2).toSeconds()); + verificationSessions.update(sessionId, updatedSession).join(); + + assertEquals(updatedSession, verificationSessions.findForKey(sessionId).join().orElseThrow()); + }); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeGraphqlClientTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeGraphqlClientTest.java new file mode 100644 index 000000000..131eee029 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeGraphqlClientTest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.braintree.graphql.clientoperation.CreatePayPalOneTimePaymentMutation; +import java.math.BigDecimal; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import javax.ws.rs.ServiceUnavailableException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; + +class BraintreeGraphqlClientTest { + + private static final String CURRENCY = "xts"; + private static final String RETURN_URL = "https://example.com/return"; + private static final String CANCEL_URL = "https://example.com/cancel"; + private static final String LOCALE = "xx"; + + private FaultTolerantHttpClient httpClient; + private BraintreeGraphqlClient braintreeGraphqlClient; + + + @BeforeEach + void setUp() { + httpClient = mock(FaultTolerantHttpClient.class); + + braintreeGraphqlClient = new BraintreeGraphqlClient(httpClient, "https://example.com", "public", "super-secret"); + } + + @Test + void createPayPalOneTimePayment() { + + final HttpResponse response = mock(HttpResponse.class); + when(httpClient.sendAsync(any(), any())) + .thenReturn(CompletableFuture.completedFuture(response)); + + final String paymentId = "PAYID-AAA1AAAA1A11111AA111111A"; + when(response.body()) + .thenReturn(createPayPalOneTimePaymentResponse(paymentId)); + when(response.statusCode()) + .thenReturn(200); + + final CompletableFuture future = braintreeGraphqlClient.createPayPalOneTimePayment( + BigDecimal.ONE, CURRENCY, + RETURN_URL, CANCEL_URL, LOCALE); + + assertTimeoutPreemptively(Duration.ofSeconds(3), () -> { + final CreatePayPalOneTimePaymentMutation.CreatePayPalOneTimePayment result = future.get(); + + assertEquals(paymentId, result.paymentId); + assertNotNull(result.approvalUrl); + }); + } + + @Test + void createPayPalOneTimePaymentHttpError() { + + final HttpResponse response = mock(HttpResponse.class); + when(httpClient.sendAsync(any(), any())) + .thenReturn(CompletableFuture.completedFuture(response)); + + when(response.statusCode()) + .thenReturn(500); + final HttpHeaders httpheaders = mock(HttpHeaders.class); + when(httpheaders.firstValue(any())).thenReturn(Optional.empty()); + when(response.headers()) + .thenReturn(httpheaders); + + final CompletableFuture future = braintreeGraphqlClient.createPayPalOneTimePayment( + BigDecimal.ONE, CURRENCY, + RETURN_URL, CANCEL_URL, LOCALE); + + assertTimeoutPreemptively(Duration.ofSeconds(3), () -> { + + final ExecutionException e = assertThrows(ExecutionException.class, future::get); + + assertTrue(e.getCause() instanceof ServiceUnavailableException); + }); + } + + @Test + void createPayPalOneTimePaymentGraphQlError() { + + final HttpResponse response = mock(HttpResponse.class); + when(httpClient.sendAsync(any(), any())) + .thenReturn(CompletableFuture.completedFuture(response)); + + when(response.body()) + .thenReturn(createErrorResponse("createPayPalOneTimePayment", "12345")); + when(response.statusCode()) + .thenReturn(200); + + final CompletableFuture future = braintreeGraphqlClient.createPayPalOneTimePayment( + BigDecimal.ONE, CURRENCY, + RETURN_URL, CANCEL_URL, LOCALE); + + assertTimeoutPreemptively(Duration.ofSeconds(3), () -> { + + final ExecutionException e = assertThrows(ExecutionException.class, future::get); + assertTrue(e.getCause() instanceof ServiceUnavailableException); + }); + } + + private String createPayPalOneTimePaymentResponse(final String paymentId) { + final String cannedToken = "EC-1AA11111AA111111A"; + return String.format(""" + { + "data": { + "createPayPalOneTimePayment": { + "approvalUrl": "https://www.sandbox.paypal.com/checkoutnow?nolegacy=1&token=%2$s", + "paymentId": "%1$s" + } + }, + "extensions": { + "requestId": "%3$s" + } + } + """, paymentId, cannedToken, UUID.randomUUID()); + } + + private String createErrorResponse(final String operationName, final String legacyCode) { + return String.format(""" + { + "data": { + "%1$s": null + }, + "errors": [ { + "message": "This is a test error message.", + "locations": [ { + "line": 2, + "column": 7 + } ], + "path": [ "%1$s" ], + "extensions": { + "errorType": "user_error", + "errorClass": "VALIDATION", + "legacyCode": "%2$s", + "inputPath": [ "input", "testField" ] + } + }], + "extensions": { + "requestId": "%3$s" + } + } + """, operationName, legacyCode, UUID.randomUUID()); + } + + @Test + void tokenizePayPalOneTimePayment() { + } + + @Test + void chargeOneTimePayment() { + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManagerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManagerTest.java new file mode 100644 index 000000000..fff9204ff --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManagerTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.subscriptions; + +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.braintreegateway.BraintreeGateway; +import com.braintreegateway.Customer; +import com.braintreegateway.CustomerGateway; +import java.time.Duration; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BraintreeManagerTest { + + private BraintreeGateway braintreeGateway; + private BraintreeManager braintreeManager; + + @BeforeEach + void setup() { + braintreeGateway = mock(BraintreeGateway.class); + braintreeManager = new BraintreeManager(braintreeGateway, + Map.of(PaymentMethod.CARD, Set.of("usd")), + Map.of("usd", "usdMerchant"), + mock(BraintreeGraphqlClient.class), + Executors.newSingleThreadExecutor()); + } + + @Test + void cancelAllActiveSubscriptions_nullDefaultPaymentMethod() { + + final Customer customer = mock(Customer.class); + when(customer.getDefaultPaymentMethod()).thenReturn(null); + + final CustomerGateway customerGateway = mock(CustomerGateway.class); + when(customerGateway.find(anyString())).thenReturn(customer); + + when(braintreeGateway.customer()).thenReturn(customerGateway); + + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> + braintreeManager.cancelAllActiveSubscriptions("customerId")).join(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/ProcessorCustomerTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/ProcessorCustomerTest.java new file mode 100644 index 000000000..1fe65c941 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/ProcessorCustomerTest.java @@ -0,0 +1,16 @@ +package org.whispersystems.textsecuregcm.subscriptions; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ProcessorCustomerTest { + + @Test + void toDynamoBytes() { + final ProcessorCustomer processorCustomer = new ProcessorCustomer("Test", SubscriptionProcessor.BRAINTREE); + + assertArrayEquals(new byte[] { SubscriptionProcessor.BRAINTREE.getId(), 'T', 'e', 's', 't' }, + processorCustomer.toDynamoBytes()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java new file mode 100644 index 000000000..b1ff2f2db --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java @@ -0,0 +1,151 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.tests.util; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockingDetails; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import org.mockito.MockingDetails; +import org.mockito.stubbing.Stubbing; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +public class AccountsHelper { + + public static Account generateTestAccount(String number, List devices) { + return generateTestAccount(number, UUID.randomUUID(), UUID.randomUUID(), devices, null); + } + + public static Account generateTestAccount(String number, UUID uuid, final UUID phoneNumberIdentifier, List devices, byte[] unidentifiedAccessKey) { + final Account account = new Account(); + account.setNumber(number, phoneNumberIdentifier); + account.setUuid(uuid); + devices.forEach(account::addDevice); + account.setUnidentifiedAccessKey(unidentifiedAccessKey); + + return account; + } + + public static void setupMockUpdate(final AccountsManager mockAccountsManager) { + setupMockUpdate(mockAccountsManager, true); + } + + /** + * Only for use by {@link AuthHelper} + */ + public static void setupMockUpdateForAuthHelper(final AccountsManager mockAccountsManager) { + setupMockUpdate(mockAccountsManager, false); + } + + private static void setupMockUpdate(final AccountsManager mockAccountsManager, final boolean markStale) { + when(mockAccountsManager.update(any(), any())).thenAnswer(answer -> { + final Account account = answer.getArgument(0, Account.class); + answer.getArgument(1, Consumer.class).accept(account); + + return markStale ? copyAndMarkStale(account) : account; + }); + + when(mockAccountsManager.updateDevice(any(), anyLong(), any())).thenAnswer(answer -> { + final Account account = answer.getArgument(0, Account.class); + final Long deviceId = answer.getArgument(1, Long.class); + account.getDevice(deviceId).ifPresent(answer.getArgument(2, Consumer.class)); + + return markStale ? copyAndMarkStale(account) : account; + }); + + when(mockAccountsManager.updateDeviceLastSeen(any(), any(), anyLong())).thenAnswer(answer -> { + answer.getArgument(1, Device.class).setLastSeen(answer.getArgument(2, Long.class)); + return mockAccountsManager.update(answer.getArgument(0, Account.class), account -> {}); + }); + + when(mockAccountsManager.updateDeviceAuthentication(any(), any(), any())).thenAnswer(answer -> { + answer.getArgument(1, Device.class).setAuthTokenHash(answer.getArgument(2, SaltedTokenHash.class)); + return mockAccountsManager.update(answer.getArgument(0, Account.class), account -> {}); + }); + } + + public static void setupMockGet(final AccountsManager mockAccountsManager, final Set mockAccounts) { + when(mockAccountsManager.getByAccountIdentifier(any(UUID.class))).thenAnswer(answer -> { + + final UUID uuid = answer.getArgument(0, UUID.class); + + return mockAccounts.stream() + .filter(account -> uuid.equals(account.getUuid())) + .findFirst() + .map(account -> { + try { + return copyAndMarkStale(account); + } catch (final Exception e) { + throw new RuntimeException(e); + } + }); + }); + } + + private static Account copyAndMarkStale(Account account) throws IOException { + MockingDetails mockingDetails = mockingDetails(account); + + final Account updatedAccount; + if (mockingDetails.isMock()) { + + updatedAccount = mock(Account.class); + + // it’s not possible to make `account` behave as if it were stale, because we use static mocks in AuthHelper + + for (Stubbing stubbing : mockingDetails.getStubbings()) { + switch (stubbing.getInvocation().getMethod().getName()) { + case "getUuid" -> when(updatedAccount.getUuid()).thenAnswer(stubbing); + case "getPhoneNumberIdentifier" -> when(updatedAccount.getPhoneNumberIdentifier()).thenAnswer(stubbing); + case "getNumber" -> when(updatedAccount.getNumber()).thenAnswer(stubbing); + case "getUsername" -> when(updatedAccount.getUsernameHash()).thenAnswer(stubbing); + case "getUsernameHash" -> when(updatedAccount.getUsernameHash()).thenAnswer(stubbing); + case "getDevices" -> when(updatedAccount.getDevices()).thenAnswer(stubbing); + case "getDevice" -> when(updatedAccount.getDevice(stubbing.getInvocation().getArgument(0))).thenAnswer(stubbing); + case "getMasterDevice" -> when(updatedAccount.getMasterDevice()).thenAnswer(stubbing); + case "isEnabled" -> when(updatedAccount.isEnabled()).thenAnswer(stubbing); + case "isDiscoverableByPhoneNumber" -> when(updatedAccount.isDiscoverableByPhoneNumber()).thenAnswer(stubbing); + case "getNextDeviceId" -> when(updatedAccount.getNextDeviceId()).thenAnswer(stubbing); + case "isPniSupported" -> when(updatedAccount.isPniSupported()).thenAnswer(stubbing); + case "isPaymentActivationSupported" -> when(updatedAccount.isPaymentActivationSupported()).thenAnswer(stubbing); + case "getEnabledDeviceCount" -> when(updatedAccount.getEnabledDeviceCount()).thenAnswer(stubbing); + case "getRegistrationLock" -> when(updatedAccount.getRegistrationLock()).thenAnswer(stubbing); + case "getIdentityKey" -> + when(updatedAccount.getIdentityKey(stubbing.getInvocation().getArgument(0))).thenAnswer(stubbing); + case "getBadges" -> when(updatedAccount.getBadges()).thenAnswer(stubbing); + case "getLastSeen" -> when(updatedAccount.getLastSeen()).thenAnswer(stubbing); + case "hasLockedCredentials" -> when(updatedAccount.hasLockedCredentials()).thenAnswer(stubbing); + default -> throw new IllegalArgumentException("unsupported method: Account#" + stubbing.getInvocation().getMethod().getName()); + } + } + + } else { + final ObjectMapper mapper = SystemMapper.jsonMapper(); + updatedAccount = mapper.readValue(mapper.writeValueAsBytes(account), Account.class); + updatedAccount.setNumber(account.getNumber(), account.getPhoneNumberIdentifier()); + account.markStale(); + } + + return updatedAccount; + } + + public static Account eqUuid(Account value) { + return argThat(other -> other.getUuid().equals(value.getUuid())); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java new file mode 100644 index 000000000..59abd528d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java @@ -0,0 +1,275 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.tests.util; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import io.dropwizard.auth.AuthFilter; +import io.dropwizard.auth.PolymorphicAuthDynamicFeature; +import io.dropwizard.auth.basic.BasicCredentialAuthFilter; +import io.dropwizard.auth.basic.BasicCredentials; +import java.security.Principal; +import java.util.Base64; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccountAuthenticator; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; +import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.HeaderUtils; + +public class AuthHelper { + // Static seed to ensure reproducible tests. + private static final Random random = new Random(0xf744df3b43a3339cL); + + public static final TestAccount[] TEST_ACCOUNTS = generateTestAccounts(); + + public static final String VALID_NUMBER = "+14150000000"; + public static final UUID VALID_UUID = UUID.randomUUID(); + public static final UUID VALID_PNI = UUID.randomUUID(); + public static final String VALID_PASSWORD = "foo"; + + public static final String VALID_NUMBER_TWO = "+201511111110"; + public static final UUID VALID_UUID_TWO = UUID.randomUUID(); + public static final UUID VALID_PNI_TWO = UUID.randomUUID(); + public static final String VALID_PASSWORD_TWO = "baz"; + + public static final String VALID_NUMBER_3 = "+14445556666"; + public static final UUID VALID_UUID_3 = UUID.randomUUID(); + public static final UUID VALID_PNI_3 = UUID.randomUUID(); + public static final String VALID_PASSWORD_3_PRIMARY = "3primary"; + public static final String VALID_PASSWORD_3_LINKED = "3linked"; + + public static final UUID INVALID_UUID = UUID.randomUUID(); + public static final String INVALID_PASSWORD = "bar"; + + public static final String DISABLED_NUMBER = "+78888888"; + public static final UUID DISABLED_UUID = UUID.randomUUID(); + public static final String DISABLED_PASSWORD = "poof"; + + public static final String UNDISCOVERABLE_NUMBER = "+18005551234"; + public static final UUID UNDISCOVERABLE_UUID = UUID.randomUUID(); + public static final String UNDISCOVERABLE_PASSWORD = "IT'S A SECRET TO EVERYBODY."; + + public static final IdentityKey VALID_IDENTITY = new IdentityKey(ECPublicKey.fromPublicKeyBytes( + Base64.getDecoder().decode("BcxxDU9FGMda70E7+Uvm7pnQcEdXQ64aJCpPUeRSfcFo"))); + + public static AccountsManager ACCOUNTS_MANAGER = mock(AccountsManager.class); + public static Account VALID_ACCOUNT = mock(Account.class ); + public static Account VALID_ACCOUNT_TWO = mock(Account.class ); + public static Account DISABLED_ACCOUNT = mock(Account.class ); + public static Account UNDISCOVERABLE_ACCOUNT = mock(Account.class ); + public static Account VALID_ACCOUNT_3 = mock(Account.class ); + + public static Device VALID_DEVICE = mock(Device.class); + public static Device VALID_DEVICE_TWO = mock(Device.class); + public static Device DISABLED_DEVICE = mock(Device.class); + public static Device UNDISCOVERABLE_DEVICE = mock(Device.class); + public static Device VALID_DEVICE_3_PRIMARY = mock(Device.class); + public static Device VALID_DEVICE_3_LINKED = mock(Device.class); + + private static SaltedTokenHash VALID_CREDENTIALS = mock(SaltedTokenHash.class); + private static SaltedTokenHash VALID_CREDENTIALS_TWO = mock(SaltedTokenHash.class); + private static SaltedTokenHash VALID_CREDENTIALS_3_PRIMARY = mock(SaltedTokenHash.class); + private static SaltedTokenHash VALID_CREDENTIALS_3_LINKED = mock(SaltedTokenHash.class); + private static SaltedTokenHash DISABLED_CREDENTIALS = mock(SaltedTokenHash.class); + private static SaltedTokenHash UNDISCOVERABLE_CREDENTIALS = mock(SaltedTokenHash.class); + + public static PolymorphicAuthDynamicFeature getAuthFilter() { + when(VALID_CREDENTIALS.verify("foo")).thenReturn(true); + when(VALID_CREDENTIALS_TWO.verify("baz")).thenReturn(true); + when(VALID_CREDENTIALS_3_PRIMARY.verify(VALID_PASSWORD_3_PRIMARY)).thenReturn(true); + when(VALID_CREDENTIALS_3_LINKED.verify(VALID_PASSWORD_3_LINKED)).thenReturn(true); + when(DISABLED_CREDENTIALS.verify(DISABLED_PASSWORD)).thenReturn(true); + when(UNDISCOVERABLE_CREDENTIALS.verify(UNDISCOVERABLE_PASSWORD)).thenReturn(true); + + when(VALID_DEVICE.getAuthTokenHash()).thenReturn(VALID_CREDENTIALS); + when(VALID_DEVICE_TWO.getAuthTokenHash()).thenReturn(VALID_CREDENTIALS_TWO); + when(VALID_DEVICE_3_PRIMARY.getAuthTokenHash()).thenReturn(VALID_CREDENTIALS_3_PRIMARY); + when(VALID_DEVICE_3_LINKED.getAuthTokenHash()).thenReturn(VALID_CREDENTIALS_3_LINKED); + when(DISABLED_DEVICE.getAuthTokenHash()).thenReturn(DISABLED_CREDENTIALS); + when(UNDISCOVERABLE_DEVICE.getAuthTokenHash()).thenReturn(UNDISCOVERABLE_CREDENTIALS); + + when(VALID_DEVICE.isMaster()).thenReturn(true); + when(VALID_DEVICE_TWO.isMaster()).thenReturn(true); + when(DISABLED_DEVICE.isMaster()).thenReturn(true); + when(UNDISCOVERABLE_DEVICE.isMaster()).thenReturn(true); + when(VALID_DEVICE_3_PRIMARY.isMaster()).thenReturn(true); + when(VALID_DEVICE_3_LINKED.isMaster()).thenReturn(false); + + when(VALID_DEVICE.getId()).thenReturn(1L); + when(VALID_DEVICE_TWO.getId()).thenReturn(1L); + when(DISABLED_DEVICE.getId()).thenReturn(1L); + when(UNDISCOVERABLE_DEVICE.getId()).thenReturn(1L); + when(VALID_DEVICE_3_PRIMARY.getId()).thenReturn(1L); + when(VALID_DEVICE_3_LINKED.getId()).thenReturn(2L); + + when(VALID_DEVICE.isEnabled()).thenReturn(true); + when(VALID_DEVICE_TWO.isEnabled()).thenReturn(true); + when(DISABLED_DEVICE.isEnabled()).thenReturn(false); + when(UNDISCOVERABLE_DEVICE.isMaster()).thenReturn(true); + when(VALID_DEVICE_3_PRIMARY.isEnabled()).thenReturn(true); + when(VALID_DEVICE_3_LINKED.isEnabled()).thenReturn(true); + + when(VALID_ACCOUNT.getDevice(1L)).thenReturn(Optional.of(VALID_DEVICE)); + when(VALID_ACCOUNT.getMasterDevice()).thenReturn(Optional.of(VALID_DEVICE)); + when(VALID_ACCOUNT_TWO.getDevice(eq(1L))).thenReturn(Optional.of(VALID_DEVICE_TWO)); + when(VALID_ACCOUNT_TWO.getMasterDevice()).thenReturn(Optional.of(VALID_DEVICE_TWO)); + when(DISABLED_ACCOUNT.getDevice(eq(1L))).thenReturn(Optional.of(DISABLED_DEVICE)); + when(DISABLED_ACCOUNT.getMasterDevice()).thenReturn(Optional.of(DISABLED_DEVICE)); + when(UNDISCOVERABLE_ACCOUNT.getDevice(eq(1L))).thenReturn(Optional.of(UNDISCOVERABLE_DEVICE)); + when(UNDISCOVERABLE_ACCOUNT.getMasterDevice()).thenReturn(Optional.of(UNDISCOVERABLE_DEVICE)); + when(VALID_ACCOUNT_3.getDevice(1L)).thenReturn(Optional.of(VALID_DEVICE_3_PRIMARY)); + when(VALID_ACCOUNT_3.getMasterDevice()).thenReturn(Optional.of(VALID_DEVICE_3_PRIMARY)); + when(VALID_ACCOUNT_3.getDevice(2L)).thenReturn(Optional.of(VALID_DEVICE_3_LINKED)); + + when(VALID_ACCOUNT_TWO.getEnabledDeviceCount()).thenReturn(6); + + when(VALID_ACCOUNT.getNumber()).thenReturn(VALID_NUMBER); + when(VALID_ACCOUNT.getUuid()).thenReturn(VALID_UUID); + when(VALID_ACCOUNT.getPhoneNumberIdentifier()).thenReturn(VALID_PNI); + when(VALID_ACCOUNT_TWO.getNumber()).thenReturn(VALID_NUMBER_TWO); + when(VALID_ACCOUNT_TWO.getUuid()).thenReturn(VALID_UUID_TWO); + when(VALID_ACCOUNT_TWO.getPhoneNumberIdentifier()).thenReturn(VALID_PNI_TWO); + when(DISABLED_ACCOUNT.getNumber()).thenReturn(DISABLED_NUMBER); + when(DISABLED_ACCOUNT.getUuid()).thenReturn(DISABLED_UUID); + when(UNDISCOVERABLE_ACCOUNT.getNumber()).thenReturn(UNDISCOVERABLE_NUMBER); + when(UNDISCOVERABLE_ACCOUNT.getUuid()).thenReturn(UNDISCOVERABLE_UUID); + when(VALID_ACCOUNT_3.getNumber()).thenReturn(VALID_NUMBER_3); + when(VALID_ACCOUNT_3.getUuid()).thenReturn(VALID_UUID_3); + when(VALID_ACCOUNT_3.getPhoneNumberIdentifier()).thenReturn(VALID_PNI_3); + + when(VALID_ACCOUNT.isEnabled()).thenReturn(true); + when(VALID_ACCOUNT_TWO.isEnabled()).thenReturn(true); + when(DISABLED_ACCOUNT.isEnabled()).thenReturn(false); + when(UNDISCOVERABLE_ACCOUNT.isEnabled()).thenReturn(true); + when(VALID_ACCOUNT_3.isEnabled()).thenReturn(true); + + when(VALID_ACCOUNT.isDiscoverableByPhoneNumber()).thenReturn(true); + when(VALID_ACCOUNT_TWO.isDiscoverableByPhoneNumber()).thenReturn(true); + when(DISABLED_ACCOUNT.isDiscoverableByPhoneNumber()).thenReturn(true); + when(UNDISCOVERABLE_ACCOUNT.isDiscoverableByPhoneNumber()).thenReturn(false); + when(VALID_ACCOUNT_3.isDiscoverableByPhoneNumber()).thenReturn(true); + + when(VALID_ACCOUNT.getIdentityKey(IdentityType.ACI)).thenReturn(VALID_IDENTITY); + + reset(ACCOUNTS_MANAGER); + + when(ACCOUNTS_MANAGER.getByE164(VALID_NUMBER)).thenReturn(Optional.of(VALID_ACCOUNT)); + when(ACCOUNTS_MANAGER.getByAccountIdentifier(VALID_UUID)).thenReturn(Optional.of(VALID_ACCOUNT)); + when(ACCOUNTS_MANAGER.getByPhoneNumberIdentifier(VALID_PNI)).thenReturn(Optional.of(VALID_ACCOUNT)); + + when(ACCOUNTS_MANAGER.getByE164(VALID_NUMBER_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO)); + when(ACCOUNTS_MANAGER.getByAccountIdentifier(VALID_UUID_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO)); + when(ACCOUNTS_MANAGER.getByPhoneNumberIdentifier(VALID_PNI_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO)); + + when(ACCOUNTS_MANAGER.getByE164(DISABLED_NUMBER)).thenReturn(Optional.of(DISABLED_ACCOUNT)); + when(ACCOUNTS_MANAGER.getByAccountIdentifier(DISABLED_UUID)).thenReturn(Optional.of(DISABLED_ACCOUNT)); + + when(ACCOUNTS_MANAGER.getByE164(UNDISCOVERABLE_NUMBER)).thenReturn(Optional.of(UNDISCOVERABLE_ACCOUNT)); + when(ACCOUNTS_MANAGER.getByAccountIdentifier(UNDISCOVERABLE_UUID)).thenReturn(Optional.of(UNDISCOVERABLE_ACCOUNT)); + + when(ACCOUNTS_MANAGER.getByE164(VALID_NUMBER_3)).thenReturn(Optional.of(VALID_ACCOUNT_3)); + when(ACCOUNTS_MANAGER.getByAccountIdentifier(VALID_UUID_3)).thenReturn(Optional.of(VALID_ACCOUNT_3)); + when(ACCOUNTS_MANAGER.getByPhoneNumberIdentifier(VALID_PNI_3)).thenReturn(Optional.of(VALID_ACCOUNT_3)); + + AccountsHelper.setupMockUpdateForAuthHelper(ACCOUNTS_MANAGER); + + for (TestAccount testAccount : TEST_ACCOUNTS) { + testAccount.setup(ACCOUNTS_MANAGER); + } + + AuthFilter accountAuthFilter = new BasicCredentialAuthFilter.Builder().setAuthenticator( + new AccountAuthenticator(ACCOUNTS_MANAGER)).buildAuthFilter(); + AuthFilter disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder().setAuthenticator( + new DisabledPermittedAccountAuthenticator(ACCOUNTS_MANAGER)).buildAuthFilter(); + + return new PolymorphicAuthDynamicFeature<>(ImmutableMap.of(AuthenticatedAccount.class, accountAuthFilter, + DisabledPermittedAuthenticatedAccount.class, disabledPermittedAccountAuthFilter)); + } + + public static String getAuthHeader(UUID uuid, long deviceId, String password) { + return HeaderUtils.basicAuthHeader(uuid.toString() + "." + deviceId, password); + } + + public static String getAuthHeader(UUID uuid, String password) { + return HeaderUtils.basicAuthHeader(uuid.toString(), password); + } + + public static String getProvisioningAuthHeader(String number, String password) { + return HeaderUtils.basicAuthHeader(number, password); + } + + public static String getUnidentifiedAccessHeader(byte[] key) { + return Base64.getEncoder().encodeToString(key); + } + + public static UUID getRandomUUID(Random random) { + long mostSignificantBits = random.nextLong(); + long leastSignificantBits = random.nextLong(); + mostSignificantBits &= 0xffffffffffff0fffL; + mostSignificantBits |= 0x0000000000004000L; + leastSignificantBits &= 0x3fffffffffffffffL; + leastSignificantBits |= 0x8000000000000000L; + return new UUID(mostSignificantBits, leastSignificantBits); + } + + public static final class TestAccount { + public final String number; + public final UUID uuid; + public final String password; + public final Account account = mock(Account.class); + public final Device device = mock(Device.class); + public final SaltedTokenHash saltedTokenHash = mock(SaltedTokenHash.class); + + public TestAccount(String number, UUID uuid, String password) { + this.number = number; + this.uuid = uuid; + this.password = password; + } + + public String getAuthHeader() { + return AuthHelper.getAuthHeader(uuid, password); + } + + private void setup(final AccountsManager accountsManager) { + when(saltedTokenHash.verify(password)).thenReturn(true); + when(device.getAuthTokenHash()).thenReturn(saltedTokenHash); + when(device.isMaster()).thenReturn(true); + when(device.getId()).thenReturn(1L); + when(device.isEnabled()).thenReturn(true); + when(account.getDevice(1L)).thenReturn(Optional.of(device)); + when(account.getMasterDevice()).thenReturn(Optional.of(device)); + when(account.getNumber()).thenReturn(number); + when(account.getUuid()).thenReturn(uuid); + when(account.isEnabled()).thenReturn(true); + when(accountsManager.getByE164(number)).thenReturn(Optional.of(account)); + when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account)); + } + } + + private static TestAccount[] generateTestAccounts() { + final TestAccount[] testAccounts = new TestAccount[20]; + final long numberBase = 1_409_000_0000L; + for (int i = 0; i < testAccounts.length; i++) { + long currentNumber = numberBase + i; + testAccounts[i] = new TestAccount("+" + currentNumber, getRandomUUID(random), "TestAccountPassword-" + currentNumber); + } + return testAccounts; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/DevicesHelper.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/DevicesHelper.java new file mode 100644 index 000000000..54ed18e7d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/DevicesHelper.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.tests.util; + +import java.util.Random; +import org.signal.libsignal.protocol.ecc.Curve; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.Util; + +public class DevicesHelper { + + private static final Random RANDOM = new Random(); + + public static Device createDevice(final long deviceId) { + return createDevice(deviceId, 0); + } + + public static Device createDevice(final long deviceId, final long lastSeen) { + return createDevice(deviceId, lastSeen, 0); + } + + public static Device createDevice(final long deviceId, final long lastSeen, final int registrationId) { + final Device device = new Device(); + device.setId(deviceId); + device.setLastSeen(lastSeen); + device.setUserAgent("OWT"); + device.setRegistrationId(registrationId); + + setEnabled(device, true); + + return device; + } + + public static void setEnabled(Device device, boolean enabled) { + if (enabled) { + device.setSignedPreKey(KeysHelper.signedECPreKey(RANDOM.nextLong(), Curve.generateKeyPair())); + device.setPhoneNumberIdentitySignedPreKey(KeysHelper.signedECPreKey(RANDOM.nextLong(), Curve.generateKeyPair())); + device.setGcmId("testGcmId" + RANDOM.nextLong()); + device.setLastSeen(Util.todayInMillis()); + } else { + device.setSignedPreKey(null); + } + + // fail fast, to guard against a change to the isEnabled() implementation causing unexpected test behavior + assert enabled == device.isEnabled(); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/JsonHelpers.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/JsonHelpers.java new file mode 100644 index 000000000..fc25c010d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/JsonHelpers.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.tests.util; + +import static io.dropwizard.testing.FixtureHelpers.fixture; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +public class JsonHelpers { + + private static final ObjectMapper objectMapper = SystemMapper.jsonMapper(); + + public static String asJson(Object object) throws JsonProcessingException { + return objectMapper.writeValueAsString(object); + } + + public static T fromJson(String value, Class clazz) throws IOException { + return objectMapper.readValue(value, clazz); + } + + public static String jsonFixture(String filename) throws IOException { + return objectMapper.writeValueAsString(objectMapper.readValue(fixture(filename), JsonNode.class)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/KeysHelper.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/KeysHelper.java new file mode 100644 index 000000000..1217fbb02 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/KeysHelper.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.tests.util; + +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECKeyPair; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.signal.libsignal.protocol.kem.KEMKeyPair; +import org.signal.libsignal.protocol.kem.KEMKeyType; +import org.signal.libsignal.protocol.kem.KEMPublicKey; +import org.whispersystems.textsecuregcm.entities.ECPreKey; +import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; + +public final class KeysHelper { + + public static ECPreKey ecPreKey(final long id) { + return new ECPreKey(id, Curve.generateKeyPair().getPublicKey()); + } + + public static ECSignedPreKey signedECPreKey(long id, final ECKeyPair identityKeyPair) { + final ECPublicKey pubKey = Curve.generateKeyPair().getPublicKey(); + final byte[] sig = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize()); + return new ECSignedPreKey(id, pubKey, sig); + } + + public static KEMSignedPreKey signedKEMPreKey(long id, final ECKeyPair identityKeyPair) { + final KEMPublicKey pubKey = KEMKeyPair.generate(KEMKeyType.KYBER_1024).getPublicKey(); + final byte[] sig = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize()); + return new KEMSignedPreKey(id, pubKey, sig); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/MessageHelper.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/MessageHelper.java new file mode 100644 index 000000000..0ff6e7856 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/MessageHelper.java @@ -0,0 +1,28 @@ +/* + * Copyright 2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.tests.util; + +import com.google.protobuf.ByteString; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import org.whispersystems.textsecuregcm.entities.MessageProtos; + +public class MessageHelper { + + public static MessageProtos.Envelope createMessage(UUID senderUuid, final int senderDeviceId, UUID destinationUuid, + long timestamp, String content) { + return MessageProtos.Envelope.newBuilder() + .setServerGuid(UUID.randomUUID().toString()) + .setType(MessageProtos.Envelope.Type.CIPHERTEXT) + .setTimestamp(timestamp) + .setServerTimestamp(0) + .setSourceUuid(senderUuid.toString()) + .setSourceDevice(senderDeviceId) + .setDestinationUuid(destinationUuid.toString()) + .setContent(ByteString.copyFrom(content.getBytes(StandardCharsets.UTF_8))) + .build(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/MockRedisFuture.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/MockRedisFuture.java new file mode 100644 index 000000000..363631c72 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/MockRedisFuture.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.tests.util; + +import io.lettuce.core.RedisFuture; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public class MockRedisFuture extends CompletableFuture implements RedisFuture { + + public static MockRedisFuture completedFuture(final T value) { + final MockRedisFuture future = new MockRedisFuture(); + future.complete(value); + return future; + } + + public static MockRedisFuture failedFuture(final Throwable cause) { + final MockRedisFuture future = new MockRedisFuture(); + future.completeExceptionally(cause); + return future; + } + + @Override + public String getError() { + return null; + } + + @Override + public boolean await(final long l, final TimeUnit timeUnit) throws InterruptedException { + return false; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/ProfileTestHelper.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/ProfileTestHelper.java new file mode 100644 index 000000000..5024c52ec --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/ProfileTestHelper.java @@ -0,0 +1,20 @@ +package org.whispersystems.textsecuregcm.tests.util; + +import java.util.Base64; +import java.util.Random; + +public class ProfileTestHelper { + public static String generateRandomBase64FromByteArray(final int byteArrayLength) { + return encodeToBase64(generateRandomByteArray(byteArrayLength)); + } + + public static byte[] generateRandomByteArray(final int length) { + byte[] byteArray = new byte[length]; + new Random().nextBytes(byteArray); + return byteArray; + } + + public static String encodeToBase64(final byte[] input) { + return Base64.getEncoder().encodeToString(input); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/RedisClusterHelper.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/RedisClusterHelper.java new file mode 100644 index 000000000..e0f8201b6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/RedisClusterHelper.java @@ -0,0 +1,134 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.tests.util; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; +import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; +import io.lettuce.core.cluster.api.reactive.RedisAdvancedClusterReactiveCommands; +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import java.util.function.Consumer; +import java.util.function.Function; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; + +public class RedisClusterHelper { + + public static RedisClusterHelper.Builder builder() { + return new Builder(); + } + + @SuppressWarnings("unchecked") + private static FaultTolerantRedisCluster buildMockRedisCluster( + final RedisAdvancedClusterCommands stringCommands, + final RedisAdvancedClusterAsyncCommands stringAsyncCommands, + final RedisAdvancedClusterCommands binaryCommands, + final RedisAdvancedClusterAsyncCommands binaryAsyncCommands, + final RedisAdvancedClusterReactiveCommands binaryReactiveCommands) { + final FaultTolerantRedisCluster cluster = mock(FaultTolerantRedisCluster.class); + final StatefulRedisClusterConnection stringConnection = mock(StatefulRedisClusterConnection.class); + final StatefulRedisClusterConnection binaryConnection = mock(StatefulRedisClusterConnection.class); + + when(stringConnection.sync()).thenReturn(stringCommands); + when(stringConnection.async()).thenReturn(stringAsyncCommands); + when(binaryConnection.sync()).thenReturn(binaryCommands); + when(binaryConnection.async()).thenReturn(binaryAsyncCommands); + when(binaryConnection.reactive()).thenReturn(binaryReactiveCommands); + + when(cluster.withCluster(any(Function.class))).thenAnswer(invocation -> { + return invocation.getArgument(0, Function.class).apply(stringConnection); + }); + + doAnswer(invocation -> { + invocation.getArgument(0, Consumer.class).accept(stringConnection); + return null; + }).when(cluster).useCluster(any(Consumer.class)); + + when(cluster.withCluster(any(Function.class))).thenAnswer(invocation -> { + return invocation.getArgument(0, Function.class).apply(stringConnection); + }); + + doAnswer(invocation -> { + invocation.getArgument(0, Consumer.class).accept(stringConnection); + return null; + }).when(cluster).useCluster(any(Consumer.class)); + + when(cluster.withBinaryCluster(any(Function.class))).thenAnswer(invocation -> { + return invocation.getArgument(0, Function.class).apply(binaryConnection); + }); + + doAnswer(invocation -> { + invocation.getArgument(0, Consumer.class).accept(binaryConnection); + return null; + }).when(cluster).useBinaryCluster(any(Consumer.class)); + + when(cluster.withBinaryCluster(any(Function.class))).thenAnswer(invocation -> { + return invocation.getArgument(0, Function.class).apply(binaryConnection); + }); + + doAnswer(invocation -> { + invocation.getArgument(0, Consumer.class).accept(binaryConnection); + return null; + }).when(cluster).useBinaryCluster(any(Consumer.class)); + + return cluster; + } + + @SuppressWarnings("unchecked") + public static class Builder { + + private RedisAdvancedClusterCommands stringCommands = mock(RedisAdvancedClusterCommands.class); + private RedisAdvancedClusterAsyncCommands stringAsyncCommands = + mock(RedisAdvancedClusterAsyncCommands.class); + + private RedisAdvancedClusterCommands binaryCommands = mock(RedisAdvancedClusterCommands.class); + + private RedisAdvancedClusterAsyncCommands binaryAsyncCommands = + mock(RedisAdvancedClusterAsyncCommands.class); + + private RedisAdvancedClusterReactiveCommands binaryReactiveCommands = + mock(RedisAdvancedClusterReactiveCommands.class); + + private Builder() { + + } + + public Builder stringCommands(final RedisAdvancedClusterCommands stringCommands) { + this.stringCommands = stringCommands; + return this; + } + + public Builder stringAsyncCommands(final RedisAdvancedClusterAsyncCommands stringAsyncCommands) { + this.stringAsyncCommands = stringAsyncCommands; + return this; + } + + public Builder binaryCommands(final RedisAdvancedClusterCommands binaryCommands) { + this.binaryCommands = binaryCommands; + return this; + } + + public Builder binaryAsyncCommands(final RedisAdvancedClusterAsyncCommands binaryAsyncCommands) { + this.binaryAsyncCommands = binaryAsyncCommands; + return this; + } + + public Builder binaryReactiveCommands( + final RedisAdvancedClusterReactiveCommands binaryReactiveCommands) { + this.binaryReactiveCommands = binaryReactiveCommands; + return this; + } + + public FaultTolerantRedisCluster build() { + return RedisClusterHelper.buildMockRedisCluster(stringCommands, stringAsyncCommands, binaryCommands, binaryAsyncCommands, + binaryReactiveCommands); + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/SynchronousExecutorService.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/SynchronousExecutorService.java new file mode 100644 index 000000000..b8552b14f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/SynchronousExecutorService.java @@ -0,0 +1,115 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.tests.util; + +import com.google.common.util.concurrent.SettableFuture; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class SynchronousExecutorService implements ExecutorService { + + private boolean shutdown = false; + + @Override + public void shutdown() { + shutdown = true; + } + + @Override + public List shutdownNow() { + shutdown = true; + return Collections.emptyList(); + } + + @Override + public boolean isShutdown() { + return shutdown; + } + + @Override + public boolean isTerminated() { + return shutdown; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return true; + } + + @Override + public Future submit(Callable task) { + SettableFuture future = null; + try { + future = SettableFuture.create(); + future.set(task.call()); + } catch (Throwable e) { + future.setException(e); + } + + return future; + } + + @Override + public Future submit(Runnable task, T result) { + SettableFuture future = SettableFuture.create(); + task.run(); + + future.set(result); + + return future; + } + + @Override + public Future submit(Runnable task) { + SettableFuture future = SettableFuture.create(); + task.run(); + future.set(null); + return future; + } + + @Override + public List> invokeAll(Collection> tasks) throws InterruptedException { + List> results = new LinkedList<>(); + for (Callable callable : tasks) { + SettableFuture future = SettableFuture.create(); + try { + future.set(callable.call()); + } catch (Throwable e) { + future.setException(e); + } + results.add(future); + } + return results; + } + + @Override + public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException { + return invokeAll(tasks); + } + + @Override + public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { + return null; + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return null; + } + + @Override + public void execute(Runnable command) { + command.run(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/AttributeValuesTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/AttributeValuesTest.java new file mode 100644 index 000000000..94be5c743 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/AttributeValuesTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import java.nio.ByteBuffer; +import java.util.Base64; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +public class AttributeValuesTest { + @Test + void testUUIDRoundTrip() { + UUID orig = UUID.randomUUID(); + AttributeValue av = AttributeValues.fromUUID(orig); + UUID returned = AttributeValues.getUUID(Map.of("foo", av), "foo", null); + assertEquals(orig, returned); + } + + @Test + void testLongRoundTrip() { + long orig = 12345; + AttributeValue av = AttributeValues.fromLong(orig); + long returned = AttributeValues.getLong(Map.of("foo", av), "foo", -1); + assertEquals(orig, returned); + } + + @Test + void testIntRoundTrip() { + int orig = 12345; + AttributeValue av = AttributeValues.fromInt(orig); + int returned = AttributeValues.getInt(Map.of("foo", av), "foo", -1); + assertEquals(orig, returned); + } + + @Test + void testByteBuffer() { + byte[] bytes = {1, 2, 3}; + ByteBuffer bb = ByteBuffer.wrap(bytes); + AttributeValue av = AttributeValues.fromByteBuffer(bb); + byte[] returned = av.b().asByteArray(); + assertArrayEquals(bytes, returned); + returned = AttributeValues.getByteArray(Map.of("foo", av), "foo", null); + assertArrayEquals(bytes, returned); + } + + @Test + void testByteBuffer2() { + final ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[8]); + byteBuffer.putLong(123); + assertEquals(byteBuffer.remaining(), 0); + AttributeValue av = AttributeValues.fromByteBuffer(byteBuffer.flip()); + assertArrayEquals(new byte[]{0, 0, 0, 0, 0, 0, 0, 123}, AttributeValues.getByteArray(Map.of("foo", av), "foo", null)); + } + + @Test + void testNullUuid() { + final Map item = Map.of("key", AttributeValue.builder().nul(true).build()); + assertNull(AttributeValues.getUUID(item, "key", null)); + } + + @ParameterizedTest + @MethodSource + void extractByteArray(final AttributeValue attributeValue, final byte[] expectedByteArray) { + assertArrayEquals(expectedByteArray, AttributeValues.extractByteArray(attributeValue, "counter")); + } + + private static Stream extractByteArray() { + final byte[] key = Base64.getDecoder().decode("c+k+8zv8WaFdDjR9IOvCk6BcY5OI7rge/YUDkaDGyRc="); + + return Stream.of( + Arguments.of(AttributeValue.fromB(SdkBytes.fromByteArray(key)), key), + Arguments.of(AttributeValue.fromS(Base64.getEncoder().encodeToString(key)), key), + Arguments.of(AttributeValue.fromS(Base64.getEncoder().withoutPadding().encodeToString(key)), key) + ); + } + + @ParameterizedTest + @MethodSource + void extractByteArrayIllegalArgument(final AttributeValue attributeValue) { + assertThrows(IllegalArgumentException.class, () -> AttributeValues.extractByteArray(attributeValue, "counter")); + } + + private static Stream extractByteArrayIllegalArgument() { + return Stream.of( + Arguments.of(AttributeValue.fromN("12")), + Arguments.of(AttributeValue.fromS("")), + Arguments.of(AttributeValue.fromS("Definitely not legitimate base64 👎")) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/CompletableFutureTestUtil.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/CompletableFutureTestUtil.java new file mode 100644 index 000000000..6024cbda8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/CompletableFutureTestUtil.java @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public class CompletableFutureTestUtil { + + private CompletableFutureTestUtil() { + } + + public static void assertFailsWithCause(final Class expectedCause, final CompletableFuture completableFuture) { + assertFailsWithCause(expectedCause, completableFuture, null); + } + + public static void assertFailsWithCause(final Class expectedCause, final CompletableFuture completableFuture, final String message) { + final CompletionException completionException = assertThrows(CompletionException.class, completableFuture::join, message); + assertTrue(ExceptionUtils.unwrap(completionException).getClass().isAssignableFrom(expectedCause), message); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/DestinationDeviceValidatorTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/DestinationDeviceValidatorTest.java new file mode 100644 index 000000000..2ad4e7837 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/DestinationDeviceValidatorTest.java @@ -0,0 +1,252 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; +import org.whispersystems.textsecuregcm.controllers.StaleDevicesException; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; + +@ExtendWith(DropwizardExtensionsSupport.class) +class DestinationDeviceValidatorTest { + + static Account mockAccountWithDeviceAndRegId(final Map registrationIdsByDeviceId) { + final Account account = mock(Account.class); + + registrationIdsByDeviceId.forEach((deviceId, registrationId) -> { + final Device device = mock(Device.class); + when(device.getRegistrationId()).thenReturn(registrationId); + when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); + }); + + return account; + } + + static Stream validateRegistrationIdsSource() { + return Stream.of( + arguments( + mockAccountWithDeviceAndRegId(Map.of(1L, 0xFFFF, 2L, 0xDEAD, 3L, 0xBEEF)), + Map.of(1L, 0xFFFF, 2L, 0xDEAD, 3L, 0xBEEF), + null), + arguments( + mockAccountWithDeviceAndRegId(Map.of(1L, 42)), + Map.of(1L, 1492), + Set.of(1L)), + arguments( + mockAccountWithDeviceAndRegId(Map.of(1L, 42)), + Map.of(1L, 42), + null), + arguments( + mockAccountWithDeviceAndRegId(Map.of(1L, 42)), + Map.of(1L, 0), + null), + arguments( + mockAccountWithDeviceAndRegId(Map.of(1L, 42, 2L, 255)), + Map.of(1L, 0, 2L, 42), + Set.of(2L)), + arguments( + mockAccountWithDeviceAndRegId(Map.of(1L, 42, 2L, 256)), + Map.of(1L, 41, 2L, 257), + Set.of(1L, 2L)) + ); + } + + @ParameterizedTest + @MethodSource("validateRegistrationIdsSource") + void testValidateRegistrationIds( + Account account, + Map registrationIdsByDeviceId, + Set expectedStaleDeviceIds) throws Exception { + if (expectedStaleDeviceIds != null) { + Assertions.assertThat(assertThrows(StaleDevicesException.class, + () -> DestinationDeviceValidator.validateRegistrationIds( + account, + registrationIdsByDeviceId.entrySet(), + Map.Entry::getKey, + Map.Entry::getValue, + false)) + .getStaleDevices()) + .hasSameElementsAs(expectedStaleDeviceIds); + } else { + DestinationDeviceValidator.validateRegistrationIds(account, registrationIdsByDeviceId.entrySet(), + Map.Entry::getKey, Map.Entry::getValue, false); + } + } + + static Account mockAccountWithDeviceAndEnabled(final Map enabledStateByDeviceId) { + final Account account = mock(Account.class); + final List devices = new ArrayList<>(); + + enabledStateByDeviceId.forEach((deviceId, enabled) -> { + final Device device = mock(Device.class); + when(device.isEnabled()).thenReturn(enabled); + when(device.getId()).thenReturn(deviceId); + when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); + + devices.add(device); + }); + + when(account.getDevices()).thenReturn(devices); + + return account; + } + + static Stream validateCompleteDeviceListSource() { + return Stream.of( + arguments( + mockAccountWithDeviceAndEnabled(Map.of(1L, true, 2L, false, 3L, true)), + Set.of(1L, 3L), + null, + null, + Collections.emptySet()), + arguments( + mockAccountWithDeviceAndEnabled(Map.of(1L, true, 2L, false, 3L, true)), + Set.of(1L, 2L, 3L), + null, + Set.of(2L), + Collections.emptySet()), + arguments( + mockAccountWithDeviceAndEnabled(Map.of(1L, true, 2L, false, 3L, true)), + Set.of(1L), + Set.of(3L), + null, + Collections.emptySet()), + arguments( + mockAccountWithDeviceAndEnabled(Map.of(1L, true, 2L, false, 3L, true)), + Set.of(1L, 2L), + Set.of(3L), + Set.of(2L), + Collections.emptySet()), + arguments( + mockAccountWithDeviceAndEnabled(Map.of(1L, true, 2L, false, 3L, true)), + Set.of(1L), + Set.of(3L), + Set.of(1L), + Set.of(1L) + ), + arguments( + mockAccountWithDeviceAndEnabled(Map.of(1L, true, 2L, false, 3L, true)), + Set.of(2L), + Set.of(3L), + Set.of(2L), + Set.of(1L) + ), + arguments( + mockAccountWithDeviceAndEnabled(Map.of(1L, true, 2L, false, 3L, true)), + Set.of(3L), + null, + null, + Set.of(1L) + ) + ); + } + + @ParameterizedTest + @MethodSource("validateCompleteDeviceListSource") + void testValidateCompleteDeviceList( + Account account, + Set deviceIds, + Collection expectedMissingDeviceIds, + Collection expectedExtraDeviceIds, + Set excludedDeviceIds) throws Exception { + + if (expectedMissingDeviceIds != null || expectedExtraDeviceIds != null) { + final MismatchedDevicesException mismatchedDevicesException = assertThrows(MismatchedDevicesException.class, + () -> DestinationDeviceValidator.validateCompleteDeviceList(account, deviceIds, excludedDeviceIds)); + if (expectedMissingDeviceIds != null) { + Assertions.assertThat(mismatchedDevicesException.getMissingDevices()) + .hasSameElementsAs(expectedMissingDeviceIds); + } + if (expectedExtraDeviceIds != null) { + Assertions.assertThat(mismatchedDevicesException.getExtraDevices()).hasSameElementsAs(expectedExtraDeviceIds); + } + } else { + DestinationDeviceValidator.validateCompleteDeviceList(account, deviceIds, excludedDeviceIds); + } + } + + @Test + void testDuplicateDeviceIds() { + final Account account = mockAccountWithDeviceAndRegId(Map.of(Device.MASTER_ID, 17)); + try { + DestinationDeviceValidator.validateRegistrationIds(account, + Stream.of(new Pair<>(Device.MASTER_ID, 16), new Pair<>(Device.MASTER_ID, 17)), false); + Assertions.fail("duplicate devices should throw StaleDevicesException"); + } catch (StaleDevicesException e) { + Assertions.assertThat(e.getStaleDevices()).hasSameElementsAs(Collections.singletonList(Device.MASTER_ID)); + } + } + + @Test + void testValidatePniRegistrationIds() { + final Device device = mock(Device.class); + when(device.getId()).thenReturn(Device.MASTER_ID); + + final Account account = mock(Account.class); + when(account.getDevices()).thenReturn(List.of(device)); + when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); + + final int aciRegistrationId = 17; + final int pniRegistrationId = 89; + final int incorrectRegistrationId = aciRegistrationId + pniRegistrationId; + + when(device.getRegistrationId()).thenReturn(aciRegistrationId); + when(device.getPhoneNumberIdentityRegistrationId()).thenReturn(OptionalInt.of(pniRegistrationId)); + + assertDoesNotThrow( + () -> DestinationDeviceValidator.validateRegistrationIds(account, + Stream.of(new Pair<>(Device.MASTER_ID, aciRegistrationId)), false)); + assertDoesNotThrow( + () -> DestinationDeviceValidator.validateRegistrationIds(account, + Stream.of(new Pair<>(Device.MASTER_ID, pniRegistrationId)), + true)); + assertThrows(StaleDevicesException.class, + () -> DestinationDeviceValidator.validateRegistrationIds(account, + Stream.of(new Pair<>(Device.MASTER_ID, aciRegistrationId)), + true)); + assertThrows(StaleDevicesException.class, + () -> DestinationDeviceValidator.validateRegistrationIds(account, + Stream.of(new Pair<>(Device.MASTER_ID, pniRegistrationId)), + false)); + + when(device.getPhoneNumberIdentityRegistrationId()).thenReturn(OptionalInt.empty()); + + assertDoesNotThrow( + () -> DestinationDeviceValidator.validateRegistrationIds(account, + Stream.of(new Pair<>(Device.MASTER_ID, aciRegistrationId)), + false)); + assertDoesNotThrow( + () -> DestinationDeviceValidator.validateRegistrationIds(account, + Stream.of(new Pair<>(Device.MASTER_ID, aciRegistrationId)), + true)); + assertThrows(StaleDevicesException.class, () -> DestinationDeviceValidator.validateRegistrationIds(account, + Stream.of(new Pair<>(Device.MASTER_ID, incorrectRegistrationId)), true)); + assertThrows(StaleDevicesException.class, () -> DestinationDeviceValidator.validateRegistrationIds(account, + Stream.of(new Pair<>(Device.MASTER_ID, incorrectRegistrationId)), false)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/E164Test.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/E164Test.java new file mode 100644 index 000000000..8901e00df --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/E164Test.java @@ -0,0 +1,111 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; +import java.util.Set; +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import org.junit.jupiter.api.Test; + +public class E164Test { + + private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator(); + + private static final String E164_VALID = "+18005550123"; + + private static final String E164_INVALID = "1(800)555-0123"; + + private static final String EMPTY = ""; + + @SuppressWarnings("FieldCanBeLocal") + private static class Data { + + @E164 + private final String number; + + private Data(final String number) { + this.number = number; + } + } + + private static class Methods { + + public void foo(@E164 final String number) { + // noop + } + + @E164 + public String bar() { + return "nevermind"; + } + } + + private record Rec(@E164 String number) { + } + + @Test + public void testRecord() throws Exception { + checkNoViolations(new Rec(E164_VALID)); + checkHasViolations(new Rec(E164_INVALID)); + checkHasViolations(new Rec(EMPTY)); + } + + @Test + public void testClassField() throws Exception { + checkNoViolations(new Data(E164_VALID)); + checkHasViolations(new Data(E164_INVALID)); + checkHasViolations(new Data(EMPTY)); + } + + @Test + public void testParameters() throws Exception { + final Methods m = new Methods(); + final Method foo = Methods.class.getMethod("foo", String.class); + + final Set> violations1 = + VALIDATOR.forExecutables().validateParameters(m, foo, new Object[] {E164_VALID}); + final Set> violations2 = + VALIDATOR.forExecutables().validateParameters(m, foo, new Object[] {E164_INVALID}); + final Set> violations3 = + VALIDATOR.forExecutables().validateParameters(m, foo, new Object[] {EMPTY}); + + assertTrue(violations1.isEmpty()); + assertFalse(violations2.isEmpty()); + assertFalse(violations3.isEmpty()); + } + + @Test + public void testReturnValue() throws Exception { + final Methods m = new Methods(); + final Method bar = Methods.class.getMethod("bar"); + + final Set> violations1 = + VALIDATOR.forExecutables().validateReturnValue(m, bar, E164_VALID); + final Set> violations2 = + VALIDATOR.forExecutables().validateReturnValue(m, bar, E164_INVALID); + final Set> violations3 = + VALIDATOR.forExecutables().validateReturnValue(m, bar, EMPTY); + + assertTrue(violations1.isEmpty()); + assertFalse(violations2.isEmpty()); + assertFalse(violations3.isEmpty()); + } + + private static void checkNoViolations(final T object) { + final Set> violations = VALIDATOR.validate(object); + assertTrue(violations.isEmpty()); + } + + private static void checkHasViolations(final T object) { + final Set> violations = VALIDATOR.validate(object); + assertFalse(violations.isEmpty()); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/ECPublicKeyAdapterTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/ECPublicKeyAdapterTest.java new file mode 100644 index 000000000..ce17dd633 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/ECPublicKeyAdapterTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.protocol.ecc.ECPublicKey; + +import javax.annotation.Nullable; +import java.util.Base64; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ECPublicKeyAdapterTest { + + private static final ECPublicKey EC_PUBLIC_KEY = Curve.generateKeyPair().getPublicKey(); + + private record ECPublicKeyCarrier(@JsonSerialize(using = ECPublicKeyAdapter.Serializer.class) + @JsonDeserialize(using = ECPublicKeyAdapter.Deserializer.class) + ECPublicKey publicKey) { + } + + @ParameterizedTest + @MethodSource + void deserialize(final String json, @Nullable final ECPublicKey expectedPublicKey) throws JsonProcessingException { + final ECPublicKeyCarrier publicKeyCarrier = SystemMapper.jsonMapper().readValue(json, ECPublicKeyCarrier.class); + + assertEquals(expectedPublicKey, publicKeyCarrier.publicKey()); + } + + private static Stream deserialize() { + final String template = """ + { + "publicKey": %s + } + """; + + return Stream.of( + Arguments.of(String.format(template, "null"), null), + Arguments.of(String.format(template, "\"\""), null), + Arguments.of(String.format(template, "\"" + Base64.getEncoder().encodeToString(EC_PUBLIC_KEY.serialize()) + "\""), EC_PUBLIC_KEY), + Arguments.of(String.format(template, "\"" + Base64.getEncoder().encodeToString(EC_PUBLIC_KEY.getPublicKeyBytes()) + "\""), EC_PUBLIC_KEY) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/HeaderUtilsTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/HeaderUtilsTest.java new file mode 100644 index 000000000..92f2a0b9f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/HeaderUtilsTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class HeaderUtilsTest { + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + @ParameterizedTest + @MethodSource("argumentsForGetMostRecentProxy") + void getMostRecentProxy(final String forwardedFor, final Optional expectedMostRecentProxy) { + assertEquals(expectedMostRecentProxy, HeaderUtils.getMostRecentProxy(forwardedFor)); + } + + private static Stream argumentsForGetMostRecentProxy() { + return Stream.of( + arguments(null, Optional.empty()), + arguments("", Optional.empty()), + arguments(" ", Optional.empty()), + arguments("203.0.113.195,", Optional.empty()), + arguments("203.0.113.195, ", Optional.empty()), + arguments("203.0.113.195", Optional.of("203.0.113.195")), + arguments("203.0.113.195, 70.41.3.18, 150.172.238.178", Optional.of("150.172.238.178")) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/IdentityKeyAdapterTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/IdentityKeyAdapterTest.java new file mode 100644 index 000000000..9aabddd85 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/IdentityKeyAdapterTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.ecc.Curve; + +import javax.annotation.Nullable; + +import java.util.Base64; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class IdentityKeyAdapterTest { + + private static final IdentityKey IDENTITY_KEY = new IdentityKey(Curve.generateKeyPair().getPublicKey()); + + private record IdentityKeyCarrier(@JsonSerialize(using = IdentityKeyAdapter.Serializer.class) + @JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class) + IdentityKey identityKey) { + + }; + + @ParameterizedTest + @MethodSource + void deserialize(final String json, @Nullable final IdentityKey expectedIdentityKey) throws JsonProcessingException { + final IdentityKeyCarrier identityKeyCarrier = SystemMapper.jsonMapper().readValue(json, IdentityKeyCarrier.class); + + assertEquals(expectedIdentityKey, identityKeyCarrier.identityKey()); + } + + private static Stream deserialize() { + final String template = """ + { + "identityKey": %s + } + """; + + return Stream.of( + Arguments.of(String.format(template, "null"), null), + Arguments.of(String.format(template, "\"\""), null), + Arguments.of(String.format(template, "\"" + Base64.getEncoder().encodeToString(IDENTITY_KEY.serialize()) + "\""), IDENTITY_KEY), + Arguments.of(String.format(template, "\"" + Base64.getEncoder().encodeToString(IDENTITY_KEY.getPublicKey().getPublicKeyBytes()) + "\""), IDENTITY_KEY) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/KEMPublicKeyAdapterTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/KEMPublicKeyAdapterTest.java new file mode 100644 index 000000000..5a729ed2e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/KEMPublicKeyAdapterTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.signal.libsignal.protocol.kem.KEMKeyPair; +import org.signal.libsignal.protocol.kem.KEMKeyType; +import org.signal.libsignal.protocol.kem.KEMPublicKey; +import javax.annotation.Nullable; +import java.util.Base64; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class KEMPublicKeyAdapterTest { + + private static final KEMPublicKey KEM_PUBLIC_KEY = KEMKeyPair.generate(KEMKeyType.KYBER_1024).getPublicKey(); + + private record KEMPublicKeyCarrier(@JsonSerialize(using = KEMPublicKeyAdapter.Serializer.class) + @JsonDeserialize(using = KEMPublicKeyAdapter.Deserializer.class) + KEMPublicKey publicKey) { + } + + @ParameterizedTest + @MethodSource + void deserialize(final String json, @Nullable final KEMPublicKey expectedPublicKey) throws JsonProcessingException { + final KEMPublicKeyCarrier publicKeyCarrier = SystemMapper.jsonMapper().readValue(json, KEMPublicKeyAdapterTest.KEMPublicKeyCarrier.class); + + assertEquals(expectedPublicKey, publicKeyCarrier.publicKey()); + } + + private static Stream deserialize() { + final String template = """ + { + "publicKey": %s + } + """; + + return Stream.of( + Arguments.of(String.format(template, "null"), null), + Arguments.of(String.format(template, "\"\""), null), + Arguments.of(String.format(template, "\"" + Base64.getEncoder().encodeToString(KEM_PUBLIC_KEY.serialize()) + "\""), + KEM_PUBLIC_KEY) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/LocaleTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/LocaleTest.java new file mode 100644 index 000000000..bc7685e91 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/LocaleTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; +import java.util.List; +import java.util.Locale.LanguageRange; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class LocaleTest { + + private static final Set SUPPORTED_LOCALES = Set.of("es", "en", "zh", "zh-HK"); + + @ParameterizedTest + @MethodSource + void testFindBestLocale(@Nullable final String languageRange, @Nullable final String expectedLocale) { + + final List languageRanges = Optional.ofNullable(languageRange) + .map(LanguageRange::parse) + .orElse(Collections.emptyList()); + + assertEquals(Optional.ofNullable(expectedLocale), Util.findBestLocale(languageRanges, SUPPORTED_LOCALES)); + } + + static Stream testFindBestLocale() { + return Stream.of( + // languageRange, expectedLocale + Arguments.of("en-US, fr", "en"), + Arguments.of("es-ES", "es"), + Arguments.of("zh-Hant-HK, zh-HK", "zh"), + // zh-HK is supported, but Locale#lookup truncates from the end, per RFC-4647 + Arguments.of("zh-Hant-HK", "zh"), + Arguments.of("zh-HK", "zh-HK"), + Arguments.of("de", null), + Arguments.of(null, null) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/MockUtils.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/MockUtils.java new file mode 100644 index 000000000..af5a62b3b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/MockUtils.java @@ -0,0 +1,157 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; + +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.RandomUtils; +import org.mockito.Mockito; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import reactor.core.publisher.Mono; + +public final class MockUtils { + + private MockUtils() { + // utility class + } + + @FunctionalInterface + public interface MockInitializer { + + void init(T mock) throws Exception; + } + + public static T buildMock(final Class clazz, final MockInitializer initializer) throws RuntimeException { + final T mock = Mockito.mock(clazz); + try { + initializer.init(mock); + } catch (Exception e) { + throw new RuntimeException(e); + } + return mock; + } + + public static MutableClock mutableClock(final long timeMillis) { + return new MutableClock(timeMillis); + } + + public static void updateRateLimiterResponseToAllow( + final RateLimiter mockRateLimiter, + final String input) { + try { + doNothing().when(mockRateLimiter).validate(eq(input)); + doReturn(CompletableFuture.completedFuture(null)).when(mockRateLimiter).validateAsync(eq(input)); + doReturn(Mono.fromFuture(CompletableFuture.completedFuture(null))).when(mockRateLimiter).validateReactive(eq(input)); + } catch (final RateLimitExceededException e) { + throw new RuntimeException(e); + } + } + + public static void updateRateLimiterResponseToAllow( + final RateLimiter mockRateLimiter, + final UUID input) { + try { + doNothing().when(mockRateLimiter).validate(eq(input)); + doReturn(CompletableFuture.completedFuture(null)).when(mockRateLimiter).validateAsync(eq(input)); + doReturn(Mono.fromFuture(CompletableFuture.completedFuture(null))).when(mockRateLimiter).validateReactive(eq(input)); + } catch (final RateLimitExceededException e) { + throw new RuntimeException(e); + } + } + + public static void updateRateLimiterResponseToAllow( + final RateLimiters rateLimitersMock, + final RateLimiters.For handle, + final String input) { + final RateLimiter mockRateLimiter = Mockito.mock(RateLimiter.class); + doReturn(mockRateLimiter).when(rateLimitersMock).forDescriptor(eq(handle)); + updateRateLimiterResponseToAllow(mockRateLimiter, input); + } + + public static void updateRateLimiterResponseToAllow( + final RateLimiters rateLimitersMock, + final RateLimiters.For handle, + final UUID input) { + final RateLimiter mockRateLimiter = Mockito.mock(RateLimiter.class); + doReturn(mockRateLimiter).when(rateLimitersMock).forDescriptor(eq(handle)); + updateRateLimiterResponseToAllow(mockRateLimiter, input); + } + + public static Duration updateRateLimiterResponseToFail( + final RateLimiter mockRateLimiter, + final String input, + final Duration retryAfter, + final boolean legacyStatusCode) { + try { + final RateLimitExceededException exception = new RateLimitExceededException(retryAfter, legacyStatusCode); + doThrow(exception).when(mockRateLimiter).validate(eq(input)); + doReturn(CompletableFuture.failedFuture(exception)).when(mockRateLimiter).validateAsync(eq(input)); + doReturn(Mono.fromFuture(CompletableFuture.failedFuture(exception))).when(mockRateLimiter).validateReactive(eq(input)); + return retryAfter; + } catch (final RateLimitExceededException e) { + throw new RuntimeException(e); + } + } + + public static Duration updateRateLimiterResponseToFail( + final RateLimiter mockRateLimiter, + final UUID input, + final Duration retryAfter, + final boolean legacyStatusCode) { + try { + final RateLimitExceededException exception = new RateLimitExceededException(retryAfter, legacyStatusCode); + doThrow(exception).when(mockRateLimiter).validate(eq(input)); + doReturn(CompletableFuture.failedFuture(exception)).when(mockRateLimiter).validateAsync(eq(input)); + doReturn(Mono.fromFuture(CompletableFuture.failedFuture(exception))).when(mockRateLimiter).validateReactive(eq(input)); + return retryAfter; + } catch (final RateLimitExceededException e) { + throw new RuntimeException(e); + } + } + + public static Duration updateRateLimiterResponseToFail( + final RateLimiters rateLimitersMock, + final RateLimiters.For handle, + final String input, + final Duration retryAfter, + final boolean legacyStatusCode) { + final RateLimiter mockRateLimiter = Mockito.mock(RateLimiter.class); + doReturn(mockRateLimiter).when(rateLimitersMock).forDescriptor(eq(handle)); + return updateRateLimiterResponseToFail(mockRateLimiter, input, retryAfter, legacyStatusCode); + } + + public static Duration updateRateLimiterResponseToFail( + final RateLimiters rateLimitersMock, + final RateLimiters.For handle, + final UUID input, + final Duration retryAfter, + final boolean legacyStatusCode) { + final RateLimiter mockRateLimiter = Mockito.mock(RateLimiter.class); + doReturn(mockRateLimiter).when(rateLimitersMock).forDescriptor(eq(handle)); + return updateRateLimiterResponseToFail(mockRateLimiter, input, retryAfter, legacyStatusCode); + } + + public static SecretBytes randomSecretBytes(final int size) { + return new SecretBytes(RandomUtils.nextBytes(size)); + } + + public static SecretBytes secretBytesOf(final int... byteVals) { + final byte[] bytes = new byte[byteVals.length]; + for (int i = 0; i < byteVals.length; i++) { + bytes[i] = (byte) byteVals[i]; + } + return new SecretBytes(bytes); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/MutableClock.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/MutableClock.java new file mode 100644 index 000000000..839e2b94a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/MutableClock.java @@ -0,0 +1,73 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +public class MutableClock extends Clock { + + private final AtomicReference delegate; + + + public MutableClock(final long timeMillis) { + this(fixedTimeMillis(timeMillis)); + } + + public MutableClock(final Clock clock) { + this.delegate = new AtomicReference<>(clock); + } + + public MutableClock() { + this(Clock.systemUTC()); + } + + public MutableClock setTimeInstant(final Instant instant) { + delegate.set(Clock.fixed(instant, ZoneId.of("Etc/UTC"))); + return this; + } + + public MutableClock setTimeMillis(final long timeMillis) { + delegate.set(fixedTimeMillis(timeMillis)); + return this; + } + + public MutableClock incrementMillis(final long incrementMillis) { + return increment(incrementMillis, TimeUnit.MILLISECONDS); + } + + public MutableClock incrementSeconds(final long incrementSeconds) { + return increment(incrementSeconds, TimeUnit.SECONDS); + } + + public MutableClock increment(final long increment, final TimeUnit timeUnit) { + final long current = delegate.get().instant().toEpochMilli(); + delegate.set(fixedTimeMillis(current + timeUnit.toMillis(increment))); + return this; + } + + @Override + public ZoneId getZone() { + return delegate.get().getZone(); + } + + @Override + public Clock withZone(final ZoneId zone) { + return delegate.get().withZone(zone); + } + + @Override + public Instant instant() { + return delegate.get().instant(); + } + + private static Clock fixedTimeMillis(final long timeMillis) { + return Clock.fixed(Instant.ofEpochMilli(timeMillis), ZoneId.of("Etc/UTC")); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/NumberPrefixTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/NumberPrefixTest.java new file mode 100644 index 000000000..7ca106d5a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/NumberPrefixTest.java @@ -0,0 +1,22 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.junit.jupiter.api.Test; + +class NumberPrefixTest { + + @Test + void testPrefixes() { + assertThat(Util.getNumberPrefix("+14151234567")).isEqualTo("+14151"); + assertThat(Util.getNumberPrefix("+22587654321")).isEqualTo("+2258765"); + assertThat(Util.getNumberPrefix("+298654321")).isEqualTo("+2986543"); + assertThat(Util.getNumberPrefix("+12")).isEqualTo("+12"); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/RedisClusterUtilTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/RedisClusterUtilTest.java new file mode 100644 index 000000000..465d6aac1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/RedisClusterUtilTest.java @@ -0,0 +1,21 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.lettuce.core.cluster.SlotHash; +import org.junit.jupiter.api.Test; + +class RedisClusterUtilTest { + + @Test + void testGetMinimalHashTag() { + for (int slot = 0; slot < SlotHash.SLOT_COUNT; slot++) { + assertEquals(slot, SlotHash.getSlot(RedisClusterUtil.getMinimalHashTag(slot))); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/SystemMapperTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/SystemMapperTest.java new file mode 100644 index 000000000..dfb73551e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/SystemMapperTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class SystemMapperTest { + + private static final ObjectMapper MAPPER = SystemMapper.configureMapper(new ObjectMapper()); + + private static final String JSON_NO_FIELD = """ + {} + """.trim(); + + private static final String JSON_NULL_FIELD = """ + {"name":null} + """.trim(); + + private static final String JSON_WITH_FIELD = """ + {"name":"value"} + """.trim(); + + interface Data { + Optional name(); + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public record DataRecord(Optional name) implements Data { + } + + public static class DataClass implements Data { + + @JsonProperty + private Optional name = Optional.empty(); + + public DataClass() { + } + + public DataClass(final Optional name) { + this.name = name; + } + + @Override + public Optional name() { + return name; + } + } + + @JsonInclude(JsonInclude.Include.NON_ABSENT) + public static class DataClass2 extends DataClass { + + public DataClass2(final Optional name) { + super(name); + } + } + + @ParameterizedTest + @ValueSource(classes = {DataClass.class, DataRecord.class}) + public void testOptionalField(final Class clazz) throws Exception { + assertTrue(MAPPER.readValue(JSON_NO_FIELD, clazz).name().isEmpty()); + assertTrue(MAPPER.readValue(JSON_NULL_FIELD, clazz).name().isEmpty()); + assertEquals("value", MAPPER.readValue(JSON_WITH_FIELD, clazz).name().orElseThrow()); + } + + @ParameterizedTest + @MethodSource("provideStringsForIsBlank") + public void testSerialization(final Data data, final String expectedJson) throws Exception { + assertEquals(expectedJson, MAPPER.writeValueAsString(data)); + } + + private static Stream provideStringsForIsBlank() { + return Stream.of( + Arguments.of(new DataClass(Optional.of("value")), JSON_WITH_FIELD), + Arguments.of(new DataClass(Optional.empty()), JSON_NULL_FIELD), + Arguments.of(new DataClass(null), JSON_NULL_FIELD), + Arguments.of(new DataClass2(Optional.of("value")), JSON_WITH_FIELD), + Arguments.of(new DataClass2(Optional.of("value")), JSON_WITH_FIELD), + Arguments.of(new DataClass2(Optional.empty()), JSON_NO_FIELD), + Arguments.of(new DataRecord(Optional.of("value")), JSON_WITH_FIELD), + Arguments.of(new DataRecord(Optional.empty()), JSON_NO_FIELD), + Arguments.of(new DataRecord(null), JSON_NO_FIELD) + ); + } + + public record NotAnnotatedWithJsonFilter(String data) { + } + + @JsonFilter("AnnotatedWithJsonFilter") + public record AnnotatedWithJsonFilter(String data, String excluded) { + } + + @Test + public void testFiltering() throws Exception { + assertThrows(IllegalStateException.class, () -> SystemMapper.excludingField(NotAnnotatedWithJsonFilter.class, List.of("data"))); + final ObjectWriter writer = SystemMapper.jsonMapper() + .writer(SystemMapper.excludingField(AnnotatedWithJsonFilter.class, List.of("excluded"))); + final AnnotatedWithJsonFilter obj = new AnnotatedWithJsonFilter("valData", "valExcluded"); + final String json = writer.writeValueAsString(obj); + final Map serializedFields = SystemMapper.jsonMapper().readValue(json, Map.class); + assertEquals(Map.of("data", "valData"), serializedFields); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/TestClock.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/TestClock.java new file mode 100644 index 000000000..3d33ab75b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/TestClock.java @@ -0,0 +1,87 @@ +package org.whispersystems.textsecuregcm.util; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Optional; + +/** + * Clock class specialized for testing. + *

    + * This clock can be pinned to a particular instant or can provide the "normal" time. + *

    + * Unlike normal clocks it can be dynamically pinned and unpinned to help with testing. + * It should not be used in production. + */ +public class TestClock extends Clock { + + private volatile Optional pinnedInstant; + private final ZoneId zoneId; + + private TestClock(Optional maybePinned, ZoneId id) { + this.pinnedInstant = maybePinned; + this.zoneId = id; + } + + /** + * Instantiate a test clock that returns the "real" time. + *

    + * The clock can later be pinned to an instant if desired. + * + * @return unpinned test clock. + */ + public static TestClock now() { + return new TestClock(Optional.empty(), ZoneId.of("UTC")); + } + + /** + * Instantiate a test clock pinned to a particular instant. + *

    + * The clock can later be pinned to a different instant or unpinned if desired. + *

    + * Unlike the fixed constructor no time zone is required (it defaults to UTC). + * + * @param instant the instant to pin the clock to. + * @return test clock pinned to the given instant. + */ + public static TestClock pinned(Instant instant) { + return new TestClock(Optional.of(instant), ZoneId.of("UTC")); + } + + /** + * Pin this test clock to the given instance. + *

    + * This modifies the existing clock in-place. + * + * @param instant the instant to pin the clock to. + */ + public void pin(Instant instant) { + this.pinnedInstant = Optional.of(instant); + } + + /** + * Unpin this test clock so it will being returning the "real" time. + *

    + * This modifies the existing clock in-place. + */ + public void unpin() { + this.pinnedInstant = Optional.empty(); + } + + + @Override + public TestClock withZone(ZoneId id) { + return new TestClock(pinnedInstant, id); + } + + @Override + public ZoneId getZone() { + return zoneId; + } + + @Override + public Instant instant() { + return pinnedInstant.orElseGet(Instant::now); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/ValidNumberTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/ValidNumberTest.java new file mode 100644 index 000000000..de8bcc258 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/ValidNumberTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ValidNumberTest { + + @ParameterizedTest + @ValueSource(strings = { + "+447700900111", + "+14151231234", + "+71234567890", + "+447535742222", + "+4915174108888", + "+2250707312345", + "+298123456", + "+299123456", + "+376123456", + "+68512345", + "+689123456", + "+80011111111"}) + void requireNormalizedNumber(final String number) { + assertDoesNotThrow(() -> Util.requireNormalizedNumber(number)); + } + + @Test + void requireNormalizedNumberNull() { + assertThrows(ImpossiblePhoneNumberException.class, () -> Util.requireNormalizedNumber(null)); + } + + @ParameterizedTest + @ValueSource(strings = { + "Definitely not a phone number at all", + "+141512312341", + "+712345678901", + "+4475357422221", + "+491517410888811111", + "71234567890", + "001447535742222", + "+1415123123a" + }) + void requireNormalizedNumberImpossibleNumber(final String number) { + assertThrows(ImpossiblePhoneNumberException.class, () -> Util.requireNormalizedNumber(number)); + } + + @ParameterizedTest + @ValueSource(strings = { + "+4407700900111", + "+49493023125000", // double country code - this e164 is "possible" + "+1 415 123 1234", + "+1 (415) 123-1234", + "+1 415)123-1234", + " +14151231234"}) + void requireNormalizedNumberNonNormalized(final String number) { + assertThrows(NonNormalizedPhoneNumberException.class, () -> Util.requireNormalizedNumber(number)); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/WeightedRandomSelectTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/WeightedRandomSelectTest.java new file mode 100644 index 000000000..0e992b899 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/WeightedRandomSelectTest.java @@ -0,0 +1,46 @@ +package org.whispersystems.textsecuregcm.util; + +import org.junit.jupiter.api.Test; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +public class WeightedRandomSelectTest { + + @Test + public void test5050() { + final WeightedRandomSelect selector = new WeightedRandomSelect<>( + List.of(new Pair<>("a", 1L), new Pair<>("b", 1L))); + final Map counts = Stream.generate(selector::select) + .limit(1000) + .collect(Collectors.groupingBy(s -> s, Collectors.counting())); + assertThat(counts.get("a")).isGreaterThan(1); + assertThat(counts.get("b")).isGreaterThan(1); + } + + @Test + public void testAlways() { + final WeightedRandomSelect selector = new WeightedRandomSelect<>( + List.of(new Pair<>("a", 1L), new Pair<>("b", 0L))); + final Map counts = Stream.generate(selector::select) + .limit(1000) + .collect(Collectors.groupingBy(s -> s, Collectors.counting())); + assertThat(counts.get("a")).isEqualTo(1000); + assertThat(counts).doesNotContainKey("b"); + } + + @Test + public void testThree() { + final WeightedRandomSelect selector = new WeightedRandomSelect<>( + List.of(new Pair<>("a", 33L), new Pair<>("b", 33L), new Pair<>("c", 33L))); + final Map counts = Stream.generate(selector::select) + .limit(1000) + .collect(Collectors.groupingBy(s -> s, Collectors.counting())); + assertThat(counts.get("a")).isGreaterThan(1); + assertThat(counts.get("b")).isGreaterThan(1); + assertThat(counts.get("c")).isGreaterThan(1); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/logging/LoggingUnhandledExceptionMapperTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/logging/LoggingUnhandledExceptionMapperTest.java new file mode 100644 index 000000000..9ac97b4a6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/logging/LoggingUnhandledExceptionMapperTest.java @@ -0,0 +1,250 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.logging; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.matches; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.net.HttpHeaders; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import io.dropwizard.testing.junit5.ResourceExtension; +import java.nio.ByteBuffer; +import java.security.Principal; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Stream; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Response; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.websocket.WebSocketResourceProvider; +import org.whispersystems.websocket.auth.WebsocketAuthValueFactoryProvider; +import org.whispersystems.websocket.logging.WebsocketRequestLog; +import org.whispersystems.websocket.messages.protobuf.ProtobufWebSocketMessageFactory; +import org.whispersystems.websocket.session.WebSocketSessionContextValueFactoryProvider; + +@ExtendWith(DropwizardExtensionsSupport.class) +class LoggingUnhandledExceptionMapperTest { + + private static final Logger logger = mock(Logger.class); + + private static final LoggingUnhandledExceptionMapper exceptionMapper = spy( + new LoggingUnhandledExceptionMapper(logger)); + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) + .addProvider(new CompletionExceptionMapper()) + .addProvider(exceptionMapper) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new TestController()) + .build(); + + static ScheduledExecutorService scheduledExecutorService; + + static Stream testExceptionMapper() { + return Stream.of( + Arguments.of(false, "/v1/test/no-exception", "/v1/test/no-exception", "Signal-Android/5.1.2 Android/30", null), + Arguments.of(true, "/v1/test/unhandled-runtime-exception", "/v1/test/unhandled-runtime-exception", + "Signal-Android/5.1.2 Android/30", "ANDROID 5.1.2"), + Arguments.of(true, "/v1/test/unhandled-runtime-exception/1/and/two", + "/v1/test/unhandled-runtime-exception/\\{parameter1\\}/and/\\{parameter2\\}", "Signal-iOS/5.10.2 iOS/14.1", + "IOS 5.10.2"), + Arguments.of(true, "/v1/test/unhandled-runtime-exception", "/v1/test/unhandled-runtime-exception", + "Some literal user-agent", "Some literal user-agent"), + Arguments.of(true, "/v1/test/unhandled-runtime-exception-async", "/v1/test/unhandled-runtime-exception-async", + "Some literal user-agent", "Some literal user-agent"), + Arguments.of(true, "/v1/test/unhandled-runtime-exception-async-completion", + "/v1/test/unhandled-runtime-exception-async-completion", + "Some literal user-agent", "Some literal user-agent") + ); + } + + @BeforeEach + void setup() { + scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + } + + @AfterEach + void teardown() { + scheduledExecutorService.shutdown(); + reset(exceptionMapper, logger); + } + + @ParameterizedTest + @MethodSource + void testExceptionMapper(final boolean expectException, final String targetPath, final String loggedPath, + final String userAgentHeader, final String userAgentLog) { + + resources.getJerseyTest() + .target(targetPath) + .request() + .header(HttpHeaders.USER_AGENT, userAgentHeader) + .get(); + + if (expectException) { + verify(exceptionMapper, times(1)).toResponse(any(Exception.class)); + verify(logger, times(1)) + .error(matches(String.format(".* at GET %s \\(%s\\)", loggedPath, userAgentLog)), any(Exception.class)); + + } else { + verifyNoInteractions(exceptionMapper); + } + } + + @ParameterizedTest + @MethodSource("testExceptionMapper") + void testWebsocketExceptionMapper(final boolean expectException, final String targetPath, final String loggedPath, + final String userAgentHeader, final String userAgentLog) throws Exception { + + final CompletableFuture responseFuture = new CompletableFuture<>(); + + Session session = mock(Session.class); + WebSocketResourceProvider provider = createWebsocketProvider(userAgentHeader, session, + responseFuture::complete); + + provider.onWebSocketConnect(session); + + byte[] message = new ProtobufWebSocketMessageFactory() + .createRequest(Optional.of(111L), "GET", targetPath, new LinkedList<>(), Optional.empty()).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + responseFuture.get(1, TimeUnit.SECONDS); + + if (expectException) { + verify(exceptionMapper, times(1)).toResponse(any(Exception.class)); + verify(logger, times(1)) + .error(matches(String.format(".* at GET %s \\(%s\\)", loggedPath, userAgentLog)), any(Exception.class)); + + } else { + verifyNoInteractions(exceptionMapper); + } + + } + + private WebSocketResourceProvider createWebsocketProvider(final String userAgentHeader, + final Session session, final Consumer responseHandler) { + ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(exceptionMapper); + resourceConfig.register(new TestController()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(SystemMapper.jsonMapper())); + + ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, new TestPrincipal("foo"), new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); + + RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + when(remoteEndpoint.sendBytesByFuture(any())) + .thenAnswer(answer -> { + responseHandler.accept(answer.getArgument(0, ByteBuffer.class)); + return CompletableFuture.completedFuture(null); + }); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + when(request.getHeader(HttpHeaders.USER_AGENT)).thenReturn(userAgentHeader); + when(request.getHeaders()).thenReturn(Map.of(HttpHeaders.USER_AGENT, List.of(userAgentHeader))); + + return provider; + } + + @Path("/v1/test") + public static class TestController { + + @GET + @Path("/no-exception") + public Response testNoException() { + return Response.ok().build(); + } + + @GET + @Path("/unhandled-runtime-exception") + public Response testUnhandledException() { + throw new RuntimeException(); + } + + @GET + @Path("/unhandled-runtime-exception-async") + public CompletableFuture testUnhandledExceptionAsync() { + final CompletableFuture responseFuture = new CompletableFuture<>(); + + scheduledExecutorService.schedule(() -> responseFuture.completeExceptionally(new RuntimeException("async")), + 50, TimeUnit.MILLISECONDS); + + return responseFuture; + } + + @GET + @Path("/unhandled-runtime-exception-async-completion") + public CompletableFuture testUnhandledCompletionExceptionAsync() { + final CompletableFuture responseFuture = new CompletableFuture<>(); + + scheduledExecutorService.schedule( + () -> responseFuture.completeExceptionally(new CompletionException(new RuntimeException("async"))), + 50, TimeUnit.MILLISECONDS); + + return responseFuture; + } + + @GET + @Path("/unhandled-runtime-exception/{parameter1}/and/{parameter2}") + public Response testUnhandledExceptionWithPathParameter(@PathParam("parameter1") String parameter1, + @PathParam("parameter2") String parameter2) { + throw new RuntimeException(); + } + } + + public static class TestPrincipal implements Principal { + + private final String name; + + private TestPrincipal(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/logging/UriInfoUtilTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/logging/UriInfoUtilTest.java new file mode 100644 index 000000000..a5800dfa7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/logging/UriInfoUtilTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.logging; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.uri.UriTemplate; +import org.junit.jupiter.api.Test; + +class UriInfoUtilTest { + + @Test + void testGetPathTemplate() { + final UriTemplate firstComponent = new UriTemplate("/first"); + final UriTemplate secondComponent = new UriTemplate("/second"); + final UriTemplate thirdComponent = new UriTemplate("/{param}/{moreDifferentParam}"); + + final ExtendedUriInfo uriInfo = mock(ExtendedUriInfo.class); + when(uriInfo.getMatchedTemplates()).thenReturn(Arrays.asList(thirdComponent, secondComponent, firstComponent)); + + assertEquals("/first/second/{param}/{moreDifferentParam}", UriInfoUtil.getPathTemplate(uriInfo)); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/redis/BaseRedisCommandsHandler.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/redis/BaseRedisCommandsHandler.java new file mode 100644 index 000000000..8343722b1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/redis/BaseRedisCommandsHandler.java @@ -0,0 +1,121 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.redis; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.whispersystems.textsecuregcm.util.redis.RedisLuaScriptSandbox.tail; + +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * This class is to be extended with implementations of Redis commands as needed. + */ +public class BaseRedisCommandsHandler implements RedisCommandsHandler { + + @Override + public Object redisCommand(final String command, final List args) { + return switch (command.toUpperCase(Locale.ROOT)) { + case "SET" -> { + assertTrue(args.size() > 2); + yield set(args.get(0).toString(), args.get(1).toString(), tail(args, 2)); + } + case "GET" -> { + assertEquals(1, args.size()); + yield get(args.get(0).toString()); + } + case "DEL" -> { + assertTrue(args.size() >= 1); + yield del(args.stream().map(Object::toString).toList()); + } + case "HSET" -> { + assertTrue(args.size() > 1); + assertTrue(args.size() % 2 == 1); + yield hset(args.get(0).toString(), tail(args, 1)); + } + case "HGET" -> { + assertEquals(2, args.size()); + yield hget(args.get(0).toString(), args.get(1).toString()); + } + case "HMGET" -> { + assertTrue(args.size() > 1); + yield hmget(args.get(0).toString(), tail(args, 1)); + } + case "PEXPIRE" -> { + assertEquals(2, args.size()); + yield pexpire(args.get(0).toString(), Double.valueOf(args.get(1).toString()).longValue(), tail(args, 2)); + } + case "TYPE" -> { + assertEquals(1, args.size()); + yield type(args.get(0).toString()); + } + case "RPUSH" -> { + assertTrue(args.size() > 1); + yield push(false, args.get(0).toString(), tail(args, 1)); + } + case "LPUSH" -> { + assertTrue(args.size() > 1); + yield push(true, args.get(0).toString(), tail(args, 1)); + } + case "RPOP" -> { + assertEquals(2, args.size()); + yield pop(false, args.get(0).toString(), Double.valueOf(args.get(1).toString()).intValue()); + } + case "LPOP" -> { + assertEquals(2, args.size()); + yield pop(true, args.get(0).toString(), Double.valueOf(args.get(1).toString()).intValue()); + } + + default -> other(command, args); + }; + } + + public Object[] pop(final boolean left, final String key, final int count) { + return new Object[count]; + } + + public Object push(final boolean left, final String key, final List values) { + return 0; + } + + public Object type(final String key) { + return Map.of("ok", "none"); + } + + public Object pexpire(final String key, final long ttlMillis, final List args) { + return 0; + } + + public Object hset(final String key, final List fieldsAndValues) { + return "OK"; + } + + public Object hget(final String key, final String field) { + return null; + } + + public Object[] hmget(final String key, final List fields) { + return new Object[fields.size()]; + } + + public Object set(final String key, final String value, final List tail) { + return "OK"; + } + + public String get(final String key) { + return null; + } + + public int del(final List keys) { + return 0; + } + + public Object other(final String command, final List args) { + return "OK"; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/redis/RedisCommandsHandler.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/redis/RedisCommandsHandler.java new file mode 100644 index 000000000..e287577df --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/redis/RedisCommandsHandler.java @@ -0,0 +1,14 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.redis; + +import java.util.List; + +@FunctionalInterface +public interface RedisCommandsHandler { + + Object redisCommand(String command, List args); +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/redis/RedisLuaScriptSandbox.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/redis/RedisLuaScriptSandbox.java new file mode 100644 index 000000000..734617ed5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/redis/RedisLuaScriptSandbox.java @@ -0,0 +1,172 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.redis; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.io.Resources; +import io.lettuce.core.ScriptOutputType; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import party.iroiro.luajava.Lua; +import party.iroiro.luajava.lua51.Lua51; +import party.iroiro.luajava.value.ImmutableLuaValue; + +public class RedisLuaScriptSandbox { + + private static final String PREFIX = """ + function redis_call(...) + -- variable name needs to match the one used in the `L.setGlobal()` call + -- method name needs to match method name of the Java class + local result = proxy:redisCall(arg) + if type(result) == "userdata" then + return java.luaify(result) + else + return result + end + end + + function json_encode(obj) + return mapper:encode(obj) + end + + function json_decode(json) + return java.luaify(mapper:decode(json)) + end + + local redis = { call = redis_call } + local cjson = { encode = json_encode, decode = json_decode } + + """; + + private final String luaScript; + + private final ScriptOutputType scriptOutputType; + + + public static RedisLuaScriptSandbox fromResource( + final String resource, + final ScriptOutputType scriptOutputType) { + try { + final String src = Resources.toString(Resources.getResource(resource), StandardCharsets.UTF_8); + return new RedisLuaScriptSandbox(src, scriptOutputType); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public RedisLuaScriptSandbox(final String luaScript, final ScriptOutputType scriptOutputType) { + this.luaScript = luaScript; + this.scriptOutputType = scriptOutputType; + } + + public Object execute( + final List keys, + final List args, + final RedisCommandsHandler redisCallsHandler) { + + try (final Lua lua = new Lua51()) { + lua.openLibraries(); + final RedisLuaProxy proxy = new RedisLuaProxy(redisCallsHandler); + lua.push(MapperLuaProxy.INSTANCE, Lua.Conversion.FULL); + lua.setGlobal("mapper"); + lua.push(proxy, Lua.Conversion.FULL); + lua.setGlobal("proxy"); + lua.push(keys, Lua.Conversion.FULL); + lua.setGlobal("KEYS"); + lua.push(args, Lua.Conversion.FULL); + lua.setGlobal("ARGV"); + final Lua.LuaError executionResult = lua.run(PREFIX + luaScript); + assertEquals("OK", executionResult.name(), "Runtime error during Lua script execution"); + return adaptOutputResult(lua.get()); + } + } + + protected Object adaptOutputResult(final Object luaObject) { + if (luaObject instanceof ImmutableLuaValue luaValue) { + final Object javaValue = luaValue.toJavaObject(); + // validate expected script output type + switch (scriptOutputType) { + case INTEGER -> assertTrue(javaValue instanceof Double); // lua number is always Double + case STATUS -> assertTrue(javaValue instanceof String); + case BOOLEAN -> assertTrue(javaValue instanceof Boolean); + }; + if (javaValue instanceof Double d) { + return d.longValue(); + } + if (javaValue instanceof String s) { + return s; + } + if (javaValue instanceof Boolean b) { + return b; + } + if (javaValue == null) { + return null; + } + throw new IllegalStateException("unexpected script result java type: " + javaValue.getClass().getName()); + } + throw new IllegalStateException("unexpected script result lua type: " + luaObject.getClass().getName()); + } + + public static List tail(final List list, final int fromIdx) { + return fromIdx < list.size() ? list.subList(fromIdx, list.size()) : Collections.emptyList(); + } + + public static final class MapperLuaProxy { + + public static final MapperLuaProxy INSTANCE = new MapperLuaProxy(); + + public String encode(final Map obj) { + try { + return SystemMapper.jsonMapper().writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public Map decode(final Object json) { + try { + //noinspection unchecked + return SystemMapper.jsonMapper().readValue(json.toString(), Map.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + + /** + * Instances of this class are passed to the Lua scripting engine + * and serve as a stubs for the calls to `redis.call()`. + * + * @see #PREFIX + */ + public static final class RedisLuaProxy { + + private final RedisCommandsHandler handler; + + public RedisLuaProxy(final RedisCommandsHandler handler) { + this.handler = handler; + } + + /** + * Method name needs to match the one from the {@link #PREFIX} code. + * The method is getting called from the Lua scripting engine. + */ + @SuppressWarnings("unused") + public Object redisCall(final List args) { + assertFalse(args.isEmpty(), "`redis.call()` in Lua script invoked without arguments"); + assertTrue(args.get(0) instanceof String, "first argument to `redis.call()` must be of type `String`"); + return handler.redisCommand((String) args.get(0), tail(args, 1)); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/redis/SimpleCacheCommandsHandler.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/redis/SimpleCacheCommandsHandler.java new file mode 100644 index 000000000..e99615711 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/redis/SimpleCacheCommandsHandler.java @@ -0,0 +1,176 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.redis; + +import java.time.Clock; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; + +public class SimpleCacheCommandsHandler extends BaseRedisCommandsHandler { + + public record Entry(Object value, long expirationEpochMillis) { + } + + private final Map cache = new ConcurrentHashMap<>(); + + private final Clock clock; + + + public SimpleCacheCommandsHandler(final Clock clock) { + this.clock = clock; + } + + @Override + public Object set(final String key, final String value, final List tail) { + cache.put(key, new Entry(value, resolveExpirationEpochMillis(tail))); + return "OK"; + } + + @Override + public String get(final String key) { + return getIfNotExpired(key, String.class); + } + + @Override + public int del(final List key) { + return key.stream() + .mapToInt(k -> cache.remove(k) != null ? 1 : 0) + .sum(); + } + + @SuppressWarnings("unchecked") + @Override + public Object hset(final String key, final List fieldsAndValues) { + Map map = getIfNotExpired(key, Map.class); + if (map == null) { + map = new ConcurrentHashMap<>(); + cache.put(key, new Entry(map, Long.MAX_VALUE)); + } + final Iterator iter = fieldsAndValues.iterator(); + while (iter.hasNext()) { + final Object k = iter.next(); + final Object v = iter.next(); + map.put(k, v); + } + return "OK"; + } + + @Override + public Object hget(final String key, final String field) { + final Map map = getIfNotExpired(key, Map.class); + return map == null ? null : map.get(field); + } + + @Override + public Object[] hmget(final String key, final List fields) { + final Object[] res = new Object[fields.size()]; + for (int i = 0; i < fields.size(); i++) { + res[i] = hget(key, fields.get(i).toString()); + } + return res; + } + + @SuppressWarnings("unchecked") + @Override + public Object push(final boolean left, final String key, final List values) { + LinkedList list = getIfNotExpired(key, LinkedList.class); + if (list == null) { + list = new LinkedList<>(); + cache.put(key, new Entry(list, Long.MAX_VALUE)); + } + for (Object v: values) { + if (left) { + list.addFirst(v.toString()); + } else { + list.addLast(v.toString()); + } + } + return list.size(); + } + + @SuppressWarnings("unchecked") + @Override + public Object[] pop(final boolean left, final String key, final int count) { + final Object[] result = new String[count]; + final LinkedList list = getIfNotExpired(key, LinkedList.class); + if (list == null) { + return result; + } + for (int i = 0; i < Math.min(count, list.size()); i++) { + result[i] = left ? list.removeFirst() : list.removeLast(); + } + return result; + } + + @Override + public Object pexpire(final String key, final long ttlMillis, final List args) { + final Entry e = cache.get(key); + if (e == null) { + return 0; + } + final Entry updated = new Entry(e.value(), clock.millis() + ttlMillis); + cache.put(key, updated); + return 1; + } + + @Override + public Object type(final String key) { + final Object o = getIfNotExpired(key, Object.class); + final String type; + if (o == null) { + type = "none"; + } else if (o.getClass() == String.class) { + type = "string"; + } else if (Map.class.isAssignableFrom(o.getClass())) { + type = "hash"; + } else if (List.class.isAssignableFrom(o.getClass())) { + type = "list"; + } else { + throw new IllegalArgumentException("Unsupported value type: " + o.getClass()); + } + return Map.of("ok", type); + } + + @Nullable + protected T getIfNotExpired(final String key, final Class expectedType) { + final Entry entry = cache.get(key); + if (entry == null) { + return null; + } + if (entry.expirationEpochMillis() < clock.millis()) { + del(List.of(key)); + return null; + } + return expectedType.cast(entry.value()); + } + + protected long resolveExpirationEpochMillis(final List args) { + for (int i = 0; i < args.size() - 1; i++) { + final long currentTimeMillis = clock.millis(); + final String param = args.get(i).toString(); + final String value = args.get(i + 1).toString(); + switch (param) { + case "EX" -> { + return currentTimeMillis + Double.valueOf(value).longValue() * 1000; + } + case "PX" -> { + return currentTimeMillis + Double.valueOf(value).longValue(); + } + case "EXAT" -> { + return Double.valueOf(value).longValue() * 1000; + } + case "PXAT" -> { + return Double.valueOf(value).longValue(); + } + } + } + return Long.MAX_VALUE; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/ua/UserAgentUtilTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/ua/UserAgentUtilTest.java new file mode 100644 index 000000000..d26eae53e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/util/ua/UserAgentUtilTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.vdurmont.semver4j.Semver; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class UserAgentUtilTest { + + @ParameterizedTest + @MethodSource + void testParseBogusUserAgentString(final String userAgentString) { + assertThrows(UnrecognizedUserAgentException.class, () -> UserAgentUtil.parseUserAgentString(userAgentString)); + } + + @SuppressWarnings("unused") + private static Stream testParseBogusUserAgentString() { + return Stream.of( + null, + "This is obviously not a reasonable User-Agent string.", + "Signal-Android/4.6-8.3.unreasonableversionstring-17" + ); + } + + @ParameterizedTest + @MethodSource("argumentsForTestParseStandardUserAgentString") + void testParseStandardUserAgentString(final String userAgentString, final UserAgent expectedUserAgent) { + assertEquals(expectedUserAgent, UserAgentUtil.parseStandardUserAgentString(userAgentString)); + } + + private static Stream argumentsForTestParseStandardUserAgentString() { + return Stream.of( + Arguments.of("This is obviously not a reasonable User-Agent string.", null), + Arguments.of("Signal-Android/4.68.3 Android/25", + new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"), "Android/25")), + Arguments.of("Signal-Android/4.68.3", new UserAgent(ClientPlatform.ANDROID, new Semver("4.68.3"))), + Arguments.of("Signal-Desktop/1.2.3 Linux", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Linux")), + Arguments.of("Signal-Desktop/1.2.3 macOS", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "macOS")), + Arguments.of("Signal-Desktop/1.2.3 Windows", + new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"), "Windows")), + Arguments.of("Signal-Desktop/1.2.3", new UserAgent(ClientPlatform.DESKTOP, new Semver("1.2.3"))), + Arguments.of("Signal-Desktop/1.32.0-beta.3", + new UserAgent(ClientPlatform.DESKTOP, new Semver("1.32.0-beta.3"))), + Arguments.of("Signal-iOS/3.9.0 (iPhone; iOS 12.2; Scale/3.00)", + new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "(iPhone; iOS 12.2; Scale/3.00)")), + Arguments.of("Signal-iOS/3.9.0 iOS/14.2", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"), "iOS/14.2")), + Arguments.of("Signal-iOS/3.9.0", new UserAgent(ClientPlatform.IOS, new Semver("3.9.0"))), + Arguments.of("Signal-Android/7.11.23-nightly-1982-06-28-07-07-07 tonic/0.31", + new UserAgent(ClientPlatform.ANDROID, new Semver("7.11.23-nightly-1982-06-28-07-07-07"), "tonic/0.31")), + Arguments.of("Signal-Android/7.11.23-nightly-1982-06-28-07-07-07 Android/42 tonic/0.31", + new UserAgent(ClientPlatform.ANDROID, new Semver("7.11.23-nightly-1982-06-28-07-07-07"), "Android/42 tonic/0.31"))); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/websocket/WebSocketAccountAuthenticatorTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/websocket/WebSocketAccountAuthenticatorTest.java new file mode 100644 index 000000000..8601c8ad6 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/websocket/WebSocketAccountAuthenticatorTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.websocket; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.net.HttpHeaders; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import io.dropwizard.auth.basic.BasicCredentials; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.HeaderUtils; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.websocket.auth.WebSocketAuthenticator; + +class WebSocketAccountAuthenticatorTest { + + private static final String VALID_USER = PhoneNumberUtil.getInstance().format( + PhoneNumberUtil.getInstance().getExampleNumber("NZ"), PhoneNumberUtil.PhoneNumberFormat.E164); + + private static final String VALID_PASSWORD = "valid"; + + private static final String INVALID_USER = PhoneNumberUtil.getInstance().format( + PhoneNumberUtil.getInstance().getExampleNumber("AU"), PhoneNumberUtil.PhoneNumberFormat.E164); + + private static final String INVALID_PASSWORD = "invalid"; + + private AccountAuthenticator accountAuthenticator; + + private UpgradeRequest upgradeRequest; + + @BeforeEach + void setUp() { + accountAuthenticator = mock(AccountAuthenticator.class); + + when(accountAuthenticator.authenticate(eq(new BasicCredentials(VALID_USER, VALID_PASSWORD)))) + .thenReturn(Optional.of(new AuthenticatedAccount(() -> new Pair<>(mock(Account.class), mock(Device.class))))); + + when(accountAuthenticator.authenticate(eq(new BasicCredentials(INVALID_USER, INVALID_PASSWORD)))) + .thenReturn(Optional.empty()); + + upgradeRequest = mock(UpgradeRequest.class); + } + + @ParameterizedTest + @MethodSource + void testAuthenticate( + @Nullable final String authorizationHeaderValue, + final Map> upgradeRequestParameters, + final boolean expectAccount, + final boolean expectCredentialsPresented) throws Exception { + + when(upgradeRequest.getParameterMap()).thenReturn(upgradeRequestParameters); + if (authorizationHeaderValue != null) { + when(upgradeRequest.getHeader(eq(HttpHeaders.AUTHORIZATION))).thenReturn(authorizationHeaderValue); + } + + final WebSocketAccountAuthenticator webSocketAuthenticator = new WebSocketAccountAuthenticator( + accountAuthenticator); + + final WebSocketAuthenticator.AuthenticationResult result = webSocketAuthenticator.authenticate( + upgradeRequest); + + assertEquals(expectAccount, result.getUser().isPresent()); + assertEquals(expectCredentialsPresented, result.credentialsPresented()); + } + + private static Stream testAuthenticate() { + final Map> paramsMapWithValidAuth = + Map.of("login", List.of(VALID_USER), "password", List.of(VALID_PASSWORD)); + final Map> paramsMapWithInvalidAuth = + Map.of("login", List.of(INVALID_USER), "password", List.of(INVALID_PASSWORD)); + final String headerWithValidAuth = + HeaderUtils.basicAuthHeader(VALID_USER, VALID_PASSWORD); + final String headerWithInvalidAuth = + HeaderUtils.basicAuthHeader(INVALID_USER, INVALID_PASSWORD); + return Stream.of( + // if `Authorization` header is present, outcome should not depend on the value of query parameters + Arguments.of(headerWithValidAuth, Map.of(), true, true), + Arguments.of(headerWithInvalidAuth, Map.of(), false, true), + Arguments.of("invalid header value", Map.of(), false, true), + Arguments.of(headerWithValidAuth, paramsMapWithValidAuth, true, true), + Arguments.of(headerWithInvalidAuth, paramsMapWithValidAuth, false, true), + Arguments.of("invalid header value", paramsMapWithValidAuth, false, true), + Arguments.of(headerWithValidAuth, paramsMapWithInvalidAuth, true, true), + Arguments.of(headerWithInvalidAuth, paramsMapWithInvalidAuth, false, true), + Arguments.of("invalid header value", paramsMapWithInvalidAuth, false, true), + // if `Authorization` header is not set, outcome should match the query params based auth + Arguments.of(null, paramsMapWithValidAuth, true, true), + Arguments.of(null, paramsMapWithInvalidAuth, false, true), + Arguments.of(null, Map.of(), false, false) + ); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/websocket/WebSocketConnectionIntegrationTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/websocket/WebSocketConnectionIntegrationTest.java new file mode 100644 index 000000000..5d24ffae1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/websocket/WebSocketConnectionIntegrationTest.java @@ -0,0 +1,382 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.websocket; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import java.io.IOException; +import java.time.Clock; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope; +import org.whispersystems.textsecuregcm.push.ReceiptSender; +import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtension; +import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables; +import org.whispersystems.textsecuregcm.storage.MessagesCache; +import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb; +import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.storage.ReportMessageManager; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.websocket.WebSocketClient; +import org.whispersystems.websocket.messages.WebSocketResponseMessage; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +class WebSocketConnectionIntegrationTest { + + @RegisterExtension + static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(Tables.MESSAGES); + + @RegisterExtension + static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); + + private ExecutorService sharedExecutorService; + private ScheduledExecutorService scheduledExecutorService; + private MessagesDynamoDb messagesDynamoDb; + private MessagesCache messagesCache; + private ReportMessageManager reportMessageManager; + private Account account; + private Device device; + private WebSocketClient webSocketClient; + private Scheduler messageDeliveryScheduler; + private ClientReleaseManager clientReleaseManager; + + private long serialTimestamp = System.currentTimeMillis(); + + @BeforeEach + void setUp() throws Exception { + + sharedExecutorService = Executors.newSingleThreadExecutor(); + scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + messageDeliveryScheduler = Schedulers.newBoundedElastic(10, 10_000, "messageDelivery"); + messagesCache = new MessagesCache(REDIS_CLUSTER_EXTENSION.getRedisCluster(), + REDIS_CLUSTER_EXTENSION.getRedisCluster(), sharedExecutorService, messageDeliveryScheduler, sharedExecutorService, Clock.systemUTC()); + messagesDynamoDb = new MessagesDynamoDb(DYNAMO_DB_EXTENSION.getDynamoDbClient(), + DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), Tables.MESSAGES.tableName(), Duration.ofDays(7), + sharedExecutorService); + reportMessageManager = mock(ReportMessageManager.class); + account = mock(Account.class); + device = mock(Device.class); + webSocketClient = mock(WebSocketClient.class); + clientReleaseManager = mock(ClientReleaseManager.class); + + when(account.getNumber()).thenReturn("+18005551234"); + when(account.getUuid()).thenReturn(UUID.randomUUID()); + when(device.getId()).thenReturn(1L); + } + + @AfterEach + void tearDown() throws Exception { + sharedExecutorService.shutdown(); + sharedExecutorService.awaitTermination(2, TimeUnit.SECONDS); + + scheduledExecutorService.shutdown(); + scheduledExecutorService.awaitTermination(2, TimeUnit.SECONDS); + } + + @ParameterizedTest + @CsvSource({ + "207, 173", + "323, 0", + "0, 221", + }) + void testProcessStoredMessages(final int persistedMessageCount, final int cachedMessageCount) { + final WebSocketConnection webSocketConnection = new WebSocketConnection( + mock(ReceiptSender.class), + new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager, sharedExecutorService), + new AuthenticatedAccount(() -> new Pair<>(account, device)), + device, + webSocketClient, + scheduledExecutorService, + messageDeliveryScheduler, + clientReleaseManager); + + final List expectedMessages = new ArrayList<>(persistedMessageCount + cachedMessageCount); + + assertTimeoutPreemptively(Duration.ofSeconds(15), () -> { + + { + final List persistedMessages = new ArrayList<>(persistedMessageCount); + + for (int i = 0; i < persistedMessageCount; i++) { + final MessageProtos.Envelope envelope = generateRandomMessage(UUID.randomUUID()); + + persistedMessages.add(envelope); + expectedMessages.add(envelope); + } + + messagesDynamoDb.store(persistedMessages, account.getUuid(), device.getId()); + } + + for (int i = 0; i < cachedMessageCount; i++) { + final UUID messageGuid = UUID.randomUUID(); + final MessageProtos.Envelope envelope = generateRandomMessage(messageGuid); + + messagesCache.insert(messageGuid, account.getUuid(), device.getId(), envelope); + expectedMessages.add(envelope); + } + + final WebSocketResponseMessage successResponse = mock(WebSocketResponseMessage.class); + final AtomicBoolean queueCleared = new AtomicBoolean(false); + + when(successResponse.getStatus()).thenReturn(200); + when(webSocketClient.sendRequest(eq("PUT"), eq("/api/v1/message"), anyList(), any())) + .thenReturn(CompletableFuture.completedFuture(successResponse)); + + when(webSocketClient.sendRequest(eq("PUT"), eq("/api/v1/queue/empty"), anyList(), any())).thenAnswer( + (Answer>) invocation -> { + synchronized (queueCleared) { + queueCleared.set(true); + queueCleared.notifyAll(); + } + + return CompletableFuture.completedFuture(successResponse); + }); + + webSocketConnection.processStoredMessages(); + + synchronized (queueCleared) { + while (!queueCleared.get()) { + queueCleared.wait(); + } + } + + @SuppressWarnings("unchecked") final ArgumentCaptor> messageBodyCaptor = ArgumentCaptor.forClass( + Optional.class); + + verify(webSocketClient, times(persistedMessageCount + cachedMessageCount)).sendRequest(eq("PUT"), + eq("/api/v1/message"), anyList(), messageBodyCaptor.capture()); + verify(webSocketClient).sendRequest(eq("PUT"), eq("/api/v1/queue/empty"), anyList(), eq(Optional.empty())); + + final List sentMessages = new ArrayList<>(); + + for (final Optional maybeMessageBody : messageBodyCaptor.getAllValues()) { + maybeMessageBody.ifPresent(messageBytes -> { + try { + sentMessages.add(MessageProtos.Envelope.parseFrom(messageBytes)); + } catch (final InvalidProtocolBufferException e) { + fail("Could not parse sent message"); + } + }); + } + + assertEquals(expectedMessages, sentMessages); + }); + } + + @Test + void testProcessStoredMessagesClientClosed() { + final WebSocketConnection webSocketConnection = new WebSocketConnection( + mock(ReceiptSender.class), + new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager, sharedExecutorService), + new AuthenticatedAccount(() -> new Pair<>(account, device)), + device, + webSocketClient, + scheduledExecutorService, + messageDeliveryScheduler, + clientReleaseManager); + + final int persistedMessageCount = 207; + final int cachedMessageCount = 173; + + final List expectedMessages = new ArrayList<>(persistedMessageCount + cachedMessageCount); + + assertTimeoutPreemptively(Duration.ofSeconds(15), () -> { + + { + final List persistedMessages = new ArrayList<>(persistedMessageCount); + + for (int i = 0; i < persistedMessageCount; i++) { + final MessageProtos.Envelope envelope = generateRandomMessage(UUID.randomUUID()); + persistedMessages.add(envelope); + expectedMessages.add(envelope); + } + + messagesDynamoDb.store(persistedMessages, account.getUuid(), device.getId()); + } + + for (int i = 0; i < cachedMessageCount; i++) { + final UUID messageGuid = UUID.randomUUID(); + final MessageProtos.Envelope envelope = generateRandomMessage(messageGuid); + messagesCache.insert(messageGuid, account.getUuid(), device.getId(), envelope); + + expectedMessages.add(envelope); + } + + when(webSocketClient.sendRequest(eq("PUT"), eq("/api/v1/message"), anyList(), any())).thenReturn( + CompletableFuture.failedFuture(new IOException("Connection closed"))); + + webSocketConnection.processStoredMessages(); + + //noinspection unchecked + ArgumentCaptor> messageBodyCaptor = ArgumentCaptor.forClass(Optional.class); + + verify(webSocketClient, atMost(persistedMessageCount + cachedMessageCount)).sendRequest(eq("PUT"), + eq("/api/v1/message"), anyList(), messageBodyCaptor.capture()); + verify(webSocketClient, never()).sendRequest(eq("PUT"), eq("/api/v1/queue/empty"), anyList(), + eq(Optional.empty())); + + final List sentMessages = messageBodyCaptor.getAllValues().stream() + .map(Optional::get) + .map(messageBytes -> { + try { + return Envelope.parseFrom(messageBytes); + } catch (InvalidProtocolBufferException e) { + throw new RuntimeException(e); + } + }).toList(); + + assertTrue(expectedMessages.containsAll(sentMessages)); + }); + } + + @Test + void testProcessStoredMessagesSendFutureTimeout() { + final WebSocketConnection webSocketConnection = new WebSocketConnection( + mock(ReceiptSender.class), + new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager, sharedExecutorService), + new AuthenticatedAccount(() -> new Pair<>(account, device)), + device, + webSocketClient, + 100, // use a very short timeout, so that this test completes quickly + scheduledExecutorService, + messageDeliveryScheduler, + clientReleaseManager); + + final int persistedMessageCount = 207; + final int cachedMessageCount = 173; + + final List expectedMessages = new ArrayList<>(persistedMessageCount + cachedMessageCount); + + assertTimeoutPreemptively(Duration.ofSeconds(15), () -> { + + { + final List persistedMessages = new ArrayList<>(persistedMessageCount); + + for (int i = 0; i < persistedMessageCount; i++) { + final MessageProtos.Envelope envelope = generateRandomMessage(UUID.randomUUID()); + persistedMessages.add(envelope); + expectedMessages.add(envelope); + } + + messagesDynamoDb.store(persistedMessages, account.getUuid(), device.getId()); + } + + for (int i = 0; i < cachedMessageCount; i++) { + final UUID messageGuid = UUID.randomUUID(); + final MessageProtos.Envelope envelope = generateRandomMessage(messageGuid); + messagesCache.insert(messageGuid, account.getUuid(), device.getId(), envelope); + + expectedMessages.add(envelope); + } + + final WebSocketResponseMessage successResponse = mock(WebSocketResponseMessage.class); + when(successResponse.getStatus()).thenReturn(200); + + final CompletableFuture neverCompleting = new CompletableFuture<>(); + + // for the first message, return a future that never completes + when(webSocketClient.sendRequest(eq("PUT"), eq("/api/v1/message"), anyList(), any())) + .thenReturn(neverCompleting) + .thenReturn(CompletableFuture.completedFuture(successResponse)); + + when(webSocketClient.isOpen()).thenReturn(true); + + final AtomicBoolean queueCleared = new AtomicBoolean(false); + + when(webSocketClient.sendRequest(eq("PUT"), eq("/api/v1/queue/empty"), anyList(), any())).thenAnswer( + (Answer>) invocation -> { + synchronized (queueCleared) { + queueCleared.set(true); + queueCleared.notifyAll(); + } + + return CompletableFuture.completedFuture(successResponse); + }); + + webSocketConnection.processStoredMessages(); + + synchronized (queueCleared) { + while (!queueCleared.get()) { + queueCleared.wait(); + } + } + + //noinspection unchecked + ArgumentCaptor> messageBodyCaptor = ArgumentCaptor.forClass(Optional.class); + + // We expect all of the messages from both pools to be sent, plus one for the future that times out + verify(webSocketClient, atMost(persistedMessageCount + cachedMessageCount + 1)).sendRequest(eq("PUT"), + eq("/api/v1/message"), anyList(), messageBodyCaptor.capture()); + + verify(webSocketClient).sendRequest(eq("PUT"), eq("/api/v1/queue/empty"), anyList(), eq(Optional.empty())); + + final List sentMessages = messageBodyCaptor.getAllValues().stream() + .map(Optional::get) + .map(messageBytes -> { + try { + return Envelope.parseFrom(messageBytes); + } catch (InvalidProtocolBufferException e) { + throw new RuntimeException(e); + } + }).toList(); + + assertTrue(expectedMessages.containsAll(sentMessages)); + }); + } + + private MessageProtos.Envelope generateRandomMessage(final UUID messageGuid) { + final long timestamp = serialTimestamp++; + + return MessageProtos.Envelope.newBuilder() + .setTimestamp(timestamp) + .setServerTimestamp(timestamp) + .setContent(ByteString.copyFromUtf8(RandomStringUtils.randomAlphanumeric(256))) + .setType(MessageProtos.Envelope.Type.CIPHERTEXT) + .setServerGuid(messageGuid.toString()) + .setDestinationUuid(UUID.randomUUID().toString()) + .build(); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/websocket/WebSocketConnectionTest.java b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/websocket/WebSocketConnectionTest.java new file mode 100644 index 000000000..5076b27af --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/java/org/whispersystems/textsecuregcm/websocket/WebSocketConnectionTest.java @@ -0,0 +1,865 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.websocket; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope; + +import com.google.common.net.HttpHeaders; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import io.dropwizard.auth.basic.BasicCredentials; +import io.lettuce.core.RedisException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.Answer; +import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; +import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; +import org.whispersystems.textsecuregcm.push.PushNotificationManager; +import org.whispersystems.textsecuregcm.push.ReceiptSender; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.ClientReleaseManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.websocket.WebSocketClient; +import org.whispersystems.websocket.auth.WebSocketAuthenticator.AuthenticationResult; +import org.whispersystems.websocket.messages.WebSocketResponseMessage; +import org.whispersystems.websocket.session.WebSocketSessionContext; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; + +class WebSocketConnectionTest { + + private static final String VALID_USER = "+14152222222"; + + private static final int SOURCE_DEVICE_ID = 1; + + private static final String VALID_PASSWORD = "secure"; + + private AccountAuthenticator accountAuthenticator; + private AccountsManager accountsManager; + private Account account; + private Device device; + private AuthenticatedAccount auth; + private UpgradeRequest upgradeRequest; + private MessagesManager messagesManager; + private ReceiptSender receiptSender; + private ScheduledExecutorService retrySchedulingExecutor; + private Scheduler messageDeliveryScheduler; + private ClientReleaseManager clientReleaseManager; + + @BeforeEach + void setup() { + accountAuthenticator = mock(AccountAuthenticator.class); + accountsManager = mock(AccountsManager.class); + account = mock(Account.class); + device = mock(Device.class); + auth = new AuthenticatedAccount(() -> new Pair<>(account, device)); + upgradeRequest = mock(UpgradeRequest.class); + messagesManager = mock(MessagesManager.class); + receiptSender = mock(ReceiptSender.class); + retrySchedulingExecutor = mock(ScheduledExecutorService.class); + messageDeliveryScheduler = Schedulers.newBoundedElastic(10, 10_000, "messageDelivery"); + clientReleaseManager = mock(ClientReleaseManager.class); + } + + @AfterEach + void teardown() { + StepVerifier.resetDefaultTimeout(); + messageDeliveryScheduler.dispose(); + } + + @Test + void testCredentials() throws Exception { + WebSocketAccountAuthenticator webSocketAuthenticator = new WebSocketAccountAuthenticator(accountAuthenticator); + AuthenticatedConnectListener connectListener = new AuthenticatedConnectListener(receiptSender, messagesManager, + mock(PushNotificationManager.class), mock(ClientPresenceManager.class), + retrySchedulingExecutor, messageDeliveryScheduler, clientReleaseManager); + WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class); + + when(accountAuthenticator.authenticate(eq(new BasicCredentials(VALID_USER, VALID_PASSWORD)))) + .thenReturn(Optional.of(new AuthenticatedAccount(() -> new Pair<>(account, device)))); + + AuthenticationResult account = webSocketAuthenticator.authenticate(upgradeRequest); + when(sessionContext.getAuthenticated()).thenReturn(account.getUser().orElse(null)); + when(sessionContext.getAuthenticated(AuthenticatedAccount.class)).thenReturn(account.getUser().orElse(null)); + + final WebSocketClient webSocketClient = mock(WebSocketClient.class); + when(webSocketClient.getUserAgent()).thenReturn("Signal-Android/6.22.8"); + when(sessionContext.getClient()).thenReturn(webSocketClient); + + // authenticated - valid user + connectListener.onWebSocketConnect(sessionContext); + + verify(sessionContext, times(1)).addWebsocketClosedListener( + any(WebSocketSessionContext.WebSocketEventListener.class)); + + // unauthenticated + when(upgradeRequest.getParameterMap()).thenReturn(Map.of()); + account = webSocketAuthenticator.authenticate(upgradeRequest); + assertFalse(account.getUser().isPresent()); + assertFalse(account.credentialsPresented()); + + connectListener.onWebSocketConnect(sessionContext); + verify(sessionContext, times(2)).addWebsocketClosedListener( + any(WebSocketSessionContext.WebSocketEventListener.class)); + + verifyNoMoreInteractions(messagesManager); + } + + @Test + void testOpen() { + + UUID accountUuid = UUID.randomUUID(); + UUID senderOneUuid = UUID.randomUUID(); + UUID senderTwoUuid = UUID.randomUUID(); + + List outgoingMessages = List.of(createMessage(senderOneUuid, accountUuid, 1111, "first"), + createMessage(senderOneUuid, accountUuid, 2222, "second"), + createMessage(senderTwoUuid, accountUuid, 3333, "third")); + + final long deviceId = 2L; + when(device.getId()).thenReturn(deviceId); + + when(account.getNumber()).thenReturn("+14152222222"); + when(account.getUuid()).thenReturn(accountUuid); + + final Device sender1device = mock(Device.class); + + List sender1devices = List.of(sender1device); + + Account sender1 = mock(Account.class); + when(sender1.getDevices()).thenReturn(sender1devices); + + when(accountsManager.getByE164("sender1")).thenReturn(Optional.of(sender1)); + when(accountsManager.getByE164("sender2")).thenReturn(Optional.empty()); + + when(messagesManager.delete(any(), anyLong(), any(), any())).thenReturn( + CompletableFuture.completedFuture(Optional.empty())); + + String userAgent = HttpHeaders.USER_AGENT; + + when(messagesManager.getMessagesForDeviceReactive(account.getUuid(), device.getId(), false)) + .thenReturn(Flux.fromIterable(outgoingMessages)); + + final List> futures = new LinkedList<>(); + final WebSocketClient client = mock(WebSocketClient.class); + + when(client.getUserAgent()).thenReturn(userAgent); + when(client.sendRequest(eq("PUT"), eq("/api/v1/message"), nullable(List.class), any())) + .thenAnswer(invocation -> { + CompletableFuture future = new CompletableFuture<>(); + futures.add(future); + return future; + }); + + WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, + auth, device, client, retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager); + + connection.start(); + verify(client, times(3)).sendRequest(eq("PUT"), eq("/api/v1/message"), any(List.class), + any()); + + assertEquals(3, futures.size()); + + WebSocketResponseMessage response = mock(WebSocketResponseMessage.class); + when(response.getStatus()).thenReturn(200); + futures.get(1).complete(response); + + futures.get(0).completeExceptionally(new IOException()); + futures.get(2).completeExceptionally(new IOException()); + + verify(messagesManager, times(1)).delete(eq(accountUuid), eq(deviceId), + eq(UUID.fromString(outgoingMessages.get(1).getServerGuid())), eq(outgoingMessages.get(1).getServerTimestamp())); + verify(receiptSender, times(1)).sendReceipt(eq(new AciServiceIdentifier(accountUuid)), eq(deviceId), eq(new AciServiceIdentifier(senderOneUuid)), + eq(2222L)); + + connection.stop(); + verify(client).close(anyInt(), anyString()); + } + + @Test + public void testOnlineSend() { + final WebSocketClient client = mock(WebSocketClient.class); + final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client, + retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager); + + final UUID accountUuid = UUID.randomUUID(); + + when(account.getNumber()).thenReturn("+18005551234"); + when(account.getUuid()).thenReturn(accountUuid); + when(device.getId()).thenReturn(1L); + when(client.isOpen()).thenReturn(true); + + when(messagesManager.getMessagesForDeviceReactive(eq(accountUuid), eq(1L), anyBoolean())) + .thenReturn(Flux.empty()) + .thenReturn(Flux.just(createMessage(UUID.randomUUID(), UUID.randomUUID(), 1111, "first"))) + .thenReturn(Flux.just(createMessage(UUID.randomUUID(), UUID.randomUUID(), 2222, "second"))) + .thenReturn(Flux.empty()); + + final WebSocketResponseMessage successResponse = mock(WebSocketResponseMessage.class); + when(successResponse.getStatus()).thenReturn(200); + + final AtomicInteger sendCounter = new AtomicInteger(0); + + when(client.sendRequest(eq("PUT"), eq("/api/v1/message"), any(List.class), any(Optional.class))) + .thenAnswer(invocation -> { + synchronized (sendCounter) { + sendCounter.incrementAndGet(); + sendCounter.notifyAll(); + } + + return CompletableFuture.completedFuture(successResponse); + }); + + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + // This is a little hacky and non-obvious, but because the first call to getMessagesForDevice returns empty list of + // messages, the call to CompletableFuture.allOf(...) in processStoredMessages will produce an instantly-succeeded + // future, and the whenComplete method will get called immediately on THIS thread, so we don't need to synchronize + // or wait for anything. + connection.start(); + + connection.handleNewMessagesAvailable(); + + synchronized (sendCounter) { + while (sendCounter.get() < 1) { + sendCounter.wait(); + } + } + + connection.handleNewMessagesAvailable(); + + synchronized (sendCounter) { + while (sendCounter.get() < 2) { + sendCounter.wait(); + } + } + }); + + verify(client, times(1)).sendRequest(eq("PUT"), eq("/api/v1/queue/empty"), any(List.class), eq(Optional.empty())); + verify(client, times(2)).sendRequest(eq("PUT"), eq("/api/v1/message"), any(List.class), any(Optional.class)); + } + + @Test + void testPendingSend() { + final UUID accountUuid = UUID.randomUUID(); + final UUID senderTwoUuid = UUID.randomUUID(); + + final Envelope firstMessage = Envelope.newBuilder() + .setServerGuid(UUID.randomUUID().toString()) + .setSourceUuid(UUID.randomUUID().toString()) + .setDestinationUuid(accountUuid.toString()) + .setUpdatedPni(UUID.randomUUID().toString()) + .setTimestamp(System.currentTimeMillis()) + .setSourceDevice(1) + .setType(Envelope.Type.CIPHERTEXT) + .build(); + + final Envelope secondMessage = Envelope.newBuilder() + .setServerGuid(UUID.randomUUID().toString()) + .setSourceUuid(senderTwoUuid.toString()) + .setDestinationUuid(accountUuid.toString()) + .setTimestamp(System.currentTimeMillis()) + .setSourceDevice(2) + .setType(Envelope.Type.CIPHERTEXT) + .build(); + + final List pendingMessages = List.of(firstMessage, secondMessage); + + final long deviceId = 2L; + when(device.getId()).thenReturn(deviceId); + + when(account.getNumber()).thenReturn("+14152222222"); + when(account.getUuid()).thenReturn(accountUuid); + + final Device sender1device = mock(Device.class); + + List sender1devices = List.of(sender1device); + + Account sender1 = mock(Account.class); + when(sender1.getDevices()).thenReturn(sender1devices); + + when(accountsManager.getByE164("sender1")).thenReturn(Optional.of(sender1)); + when(accountsManager.getByE164("sender2")).thenReturn(Optional.empty()); + + when(messagesManager.delete(any(), anyLong(), any(), any())).thenReturn( + CompletableFuture.completedFuture(Optional.empty())); + + String userAgent = HttpHeaders.USER_AGENT; + + when(messagesManager.getMessagesForDeviceReactive(account.getUuid(), device.getId(), false)) + .thenReturn(Flux.fromIterable(pendingMessages)); + + final List> futures = new LinkedList<>(); + final WebSocketClient client = mock(WebSocketClient.class); + + when(client.getUserAgent()).thenReturn(userAgent); + when(client.sendRequest(eq("PUT"), eq("/api/v1/message"), any(), any())) + .thenAnswer((Answer>) invocationOnMock -> { + CompletableFuture future = new CompletableFuture<>(); + futures.add(future); + return future; + }); + + WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, + auth, device, client, retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager); + + connection.start(); + + verify(client, times(2)).sendRequest(eq("PUT"), eq("/api/v1/message"), any(), any()); + + assertEquals(futures.size(), 2); + + WebSocketResponseMessage response = mock(WebSocketResponseMessage.class); + when(response.getStatus()).thenReturn(200); + futures.get(1).complete(response); + futures.get(0).completeExceptionally(new IOException()); + + verify(receiptSender, times(1)).sendReceipt(eq(new AciServiceIdentifier(account.getUuid())), eq(deviceId), eq(new AciServiceIdentifier(senderTwoUuid)), + eq(secondMessage.getTimestamp())); + + connection.stop(); + verify(client).close(anyInt(), anyString()); + } + + @Test + void testProcessStoredMessageConcurrency() { + final WebSocketClient client = mock(WebSocketClient.class); + final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client, + retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager); + + when(account.getNumber()).thenReturn("+18005551234"); + when(account.getUuid()).thenReturn(UUID.randomUUID()); + when(device.getId()).thenReturn(1L); + when(client.isOpen()).thenReturn(true); + + final AtomicBoolean threadWaiting = new AtomicBoolean(false); + final AtomicBoolean returnMessageList = new AtomicBoolean(false); + + when( + messagesManager.getMessagesForDeviceReactive(account.getUuid(), 1L, false)) + .thenAnswer(invocation -> { + synchronized (threadWaiting) { + threadWaiting.set(true); + threadWaiting.notifyAll(); + } + + synchronized (returnMessageList) { + while (!returnMessageList.get()) { + returnMessageList.wait(); + } + } + + return Flux.empty(); + }); + + final Thread[] threads = new Thread[10]; + final CountDownLatch unblockedThreadsLatch = new CountDownLatch(threads.length - 1); + + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(() -> { + connection.processStoredMessages(); + unblockedThreadsLatch.countDown(); + }); + + threads[i].start(); + } + + unblockedThreadsLatch.await(); + + synchronized (threadWaiting) { + while (!threadWaiting.get()) { + threadWaiting.wait(); + } + } + + synchronized (returnMessageList) { + returnMessageList.set(true); + returnMessageList.notifyAll(); + } + + for (final Thread thread : threads) { + thread.join(); + } + }); + + verify(messagesManager).getMessagesForDeviceReactive(any(UUID.class), anyLong(), eq(false)); + } + + @Test + void testProcessStoredMessagesMultiplePages() { + final WebSocketClient client = mock(WebSocketClient.class); + final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client, + retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager); + + when(account.getNumber()).thenReturn("+18005551234"); + final UUID accountUuid = UUID.randomUUID(); + when(account.getUuid()).thenReturn(accountUuid); + when(device.getId()).thenReturn(1L); + when(client.isOpen()).thenReturn(true); + + final List firstPageMessages = + List.of(createMessage(UUID.randomUUID(), UUID.randomUUID(), 1111, "first"), + createMessage(UUID.randomUUID(), UUID.randomUUID(), 2222, "second")); + + final List secondPageMessages = + List.of(createMessage(UUID.randomUUID(), UUID.randomUUID(), 3333, "third")); + + when(messagesManager.getMessagesForDeviceReactive(eq(accountUuid), eq(1L), eq(false))) + .thenReturn(Flux.fromStream(Stream.concat(firstPageMessages.stream(), secondPageMessages.stream()))); + + when(messagesManager.delete(eq(accountUuid), eq(1L), any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final WebSocketResponseMessage successResponse = mock(WebSocketResponseMessage.class); + when(successResponse.getStatus()).thenReturn(200); + + final CountDownLatch queueEmptyLatch = new CountDownLatch(1); + + when(client.sendRequest(eq("PUT"), eq("/api/v1/message"), any(List.class), any(Optional.class))) + .thenAnswer(invocation -> { + return CompletableFuture.completedFuture(successResponse); + }); + + when(client.sendRequest(eq("PUT"), eq("/api/v1/queue/empty"), any(List.class), eq(Optional.empty()))) + .thenAnswer(invocation -> { + queueEmptyLatch.countDown(); + return CompletableFuture.completedFuture(successResponse); + }); + + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + connection.processStoredMessages(); + + queueEmptyLatch.await(); + }); + + verify(client, times(firstPageMessages.size() + secondPageMessages.size())).sendRequest(eq("PUT"), + eq("/api/v1/message"), any(List.class), any(Optional.class)); + verify(client).sendRequest(eq("PUT"), eq("/api/v1/queue/empty"), any(List.class), eq(Optional.empty())); + } + + @Test + void testProcessStoredMessagesContainsSenderUuid() { + final WebSocketClient client = mock(WebSocketClient.class); + final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client, + retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager); + + when(account.getNumber()).thenReturn("+18005551234"); + final UUID accountUuid = UUID.randomUUID(); + when(account.getUuid()).thenReturn(accountUuid); + when(device.getId()).thenReturn(1L); + when(client.isOpen()).thenReturn(true); + + final UUID senderUuid = UUID.randomUUID(); + final List messages = List.of( + createMessage(senderUuid, UUID.randomUUID(), 1111L, "message the first")); + + when(messagesManager.getMessagesForDeviceReactive(account.getUuid(), 1L, false)) + .thenReturn(Flux.fromIterable(messages)) + .thenReturn(Flux.empty()); + + when(messagesManager.delete(eq(accountUuid), eq(1L), any(UUID.class), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final WebSocketResponseMessage successResponse = mock(WebSocketResponseMessage.class); + when(successResponse.getStatus()).thenReturn(200); + + final CountDownLatch queueEmptyLatch = new CountDownLatch(1); + + when(client.sendRequest(eq("PUT"), eq("/api/v1/message"), any(List.class), any(Optional.class))).thenAnswer( + invocation -> CompletableFuture.completedFuture(successResponse)); + + when(client.sendRequest(eq("PUT"), eq("/api/v1/queue/empty"), any(List.class), eq(Optional.empty()))) + .thenAnswer(invocation -> { + queueEmptyLatch.countDown(); + return CompletableFuture.completedFuture(successResponse); + }); + + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + connection.processStoredMessages(); + queueEmptyLatch.await(); + }); + + verify(client, times(messages.size())).sendRequest(eq("PUT"), eq("/api/v1/message"), any(List.class), + argThat(argument -> { + if (argument.isEmpty()) { + return false; + } + + final byte[] body = argument.get(); + try { + final Envelope envelope = Envelope.parseFrom(body); + if (!envelope.hasSourceUuid() || envelope.getSourceUuid().length() == 0) { + return false; + } + return envelope.getSourceUuid().equals(senderUuid.toString()); + } catch (InvalidProtocolBufferException e) { + return false; + } + })); + verify(client).sendRequest(eq("PUT"), eq("/api/v1/queue/empty"), any(List.class), eq(Optional.empty())); + } + + @Test + void testProcessStoredMessagesSingleEmptyCall() { + final WebSocketClient client = mock(WebSocketClient.class); + final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client, + retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager); + + final UUID accountUuid = UUID.randomUUID(); + + when(account.getNumber()).thenReturn("+18005551234"); + when(account.getUuid()).thenReturn(accountUuid); + when(device.getId()).thenReturn(1L); + when(client.isOpen()).thenReturn(true); + + when(messagesManager.getMessagesForDeviceReactive(eq(accountUuid), eq(1L), anyBoolean())) + .thenReturn(Flux.empty()); + + final WebSocketResponseMessage successResponse = mock(WebSocketResponseMessage.class); + when(successResponse.getStatus()).thenReturn(200); + + // This is a little hacky and non-obvious, but because we're always returning an empty list of messages, the call to + // CompletableFuture.allOf(...) in processStoredMessages will produce an instantly-succeeded future, and the + // whenComplete method will get called immediately on THIS thread, so we don't need to synchronize or wait for + // anything. + connection.processStoredMessages(); + connection.processStoredMessages(); + + verify(client, times(1)).sendRequest(eq("PUT"), eq("/api/v1/queue/empty"), any(List.class), eq(Optional.empty())); + } + + @Test + public void testRequeryOnStateMismatch() { + final WebSocketClient client = mock(WebSocketClient.class); + final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client, + retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager); + final UUID accountUuid = UUID.randomUUID(); + + when(account.getNumber()).thenReturn("+18005551234"); + when(account.getUuid()).thenReturn(accountUuid); + when(device.getId()).thenReturn(1L); + when(client.isOpen()).thenReturn(true); + + final List firstPageMessages = + List.of(createMessage(UUID.randomUUID(), UUID.randomUUID(), 1111, "first"), + createMessage(UUID.randomUUID(), UUID.randomUUID(), 2222, "second")); + + final List secondPageMessages = + List.of(createMessage(UUID.randomUUID(), UUID.randomUUID(), 3333, "third")); + + when(messagesManager.getMessagesForDeviceReactive(eq(accountUuid), eq(1L), anyBoolean())) + .thenReturn(Flux.fromIterable(firstPageMessages)) + .thenReturn(Flux.fromIterable(secondPageMessages)) + .thenReturn(Flux.empty()); + + when(messagesManager.delete(eq(accountUuid), eq(1L), any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + final WebSocketResponseMessage successResponse = mock(WebSocketResponseMessage.class); + when(successResponse.getStatus()).thenReturn(200); + + final CountDownLatch queueEmptyLatch = new CountDownLatch(1); + + when(client.sendRequest(eq("PUT"), eq("/api/v1/message"), any(List.class), any(Optional.class))) + .thenAnswer(invocation -> { + connection.handleNewMessagesAvailable(); + + return CompletableFuture.completedFuture(successResponse); + }); + + when(client.sendRequest(eq("PUT"), eq("/api/v1/queue/empty"), any(List.class), eq(Optional.empty()))) + .thenAnswer(invocation -> { + queueEmptyLatch.countDown(); + return CompletableFuture.completedFuture(successResponse); + }); + + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + connection.processStoredMessages(); + + queueEmptyLatch.await(); + }); + + verify(client, times(firstPageMessages.size() + secondPageMessages.size())).sendRequest(eq("PUT"), + eq("/api/v1/message"), any(List.class), any(Optional.class)); + verify(client).sendRequest(eq("PUT"), eq("/api/v1/queue/empty"), any(List.class), eq(Optional.empty())); + } + + @Test + void testProcessCachedMessagesOnly() { + final WebSocketClient client = mock(WebSocketClient.class); + final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client, + retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager); + + final UUID accountUuid = UUID.randomUUID(); + + when(account.getNumber()).thenReturn("+18005551234"); + when(account.getUuid()).thenReturn(accountUuid); + when(device.getId()).thenReturn(1L); + when(client.isOpen()).thenReturn(true); + + when(messagesManager.getMessagesForDeviceReactive(eq(accountUuid), eq(1L), anyBoolean())) + .thenReturn(Flux.empty()); + + final WebSocketResponseMessage successResponse = mock(WebSocketResponseMessage.class); + when(successResponse.getStatus()).thenReturn(200); + + // This is a little hacky and non-obvious, but because we're always returning an empty list of messages, the call to + // CompletableFuture.allOf(...) in processStoredMessages will produce an instantly-succeeded future, and the + // whenComplete method will get called immediately on THIS thread, so we don't need to synchronize or wait for + // anything. + connection.processStoredMessages(); + + verify(messagesManager).getMessagesForDeviceReactive(account.getUuid(), device.getId(), false); + + connection.handleNewMessagesAvailable(); + + verify(messagesManager).getMessagesForDeviceReactive(account.getUuid(), device.getId(), true); + } + + @Test + void testProcessDatabaseMessagesAfterPersist() { + final WebSocketClient client = mock(WebSocketClient.class); + final WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client, + retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager); + + final UUID accountUuid = UUID.randomUUID(); + + when(account.getNumber()).thenReturn("+18005551234"); + when(account.getUuid()).thenReturn(accountUuid); + when(device.getId()).thenReturn(1L); + when(client.isOpen()).thenReturn(true); + + when(messagesManager.getMessagesForDeviceReactive(eq(accountUuid), eq(1L), anyBoolean())) + .thenReturn(Flux.empty()); + + final WebSocketResponseMessage successResponse = mock(WebSocketResponseMessage.class); + when(successResponse.getStatus()).thenReturn(200); + + // This is a little hacky and non-obvious, but because we're always returning an empty list of messages, the call to + // CompletableFuture.allOf(...) in processStoredMessages will produce an instantly-succeeded future, and the + // whenComplete method will get called immediately on THIS thread, so we don't need to synchronize or wait for + // anything. + connection.processStoredMessages(); + connection.handleMessagesPersisted(); + + verify(messagesManager, times(2)).getMessagesForDeviceReactive(account.getUuid(), device.getId(), false); + } + + @Test + void testRetrieveMessageException() { + UUID accountUuid = UUID.randomUUID(); + + when(device.getId()).thenReturn(2L); + + when(account.getNumber()).thenReturn("+14152222222"); + when(account.getUuid()).thenReturn(accountUuid); + + when(messagesManager.getMessagesForDeviceReactive(account.getUuid(), device.getId(), false)) + .thenReturn(Flux.error(new RedisException("OH NO"))); + + when(retrySchedulingExecutor.schedule(any(Runnable.class), anyLong(), any())).thenAnswer( + (Answer>) invocation -> { + invocation.getArgument(0, Runnable.class).run(); + return mock(ScheduledFuture.class); + }); + + final WebSocketClient client = mock(WebSocketClient.class); + when(client.isOpen()).thenReturn(true); + + WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client, + retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager); + connection.start(); + + verify(retrySchedulingExecutor, times(WebSocketConnection.MAX_CONSECUTIVE_RETRIES)).schedule(any(Runnable.class), + anyLong(), any()); + verify(client).close(eq(1011), anyString()); + } + + @Test + void testRetrieveMessageExceptionClientDisconnected() { + UUID accountUuid = UUID.randomUUID(); + + when(device.getId()).thenReturn(2L); + + when(account.getNumber()).thenReturn("+14152222222"); + when(account.getUuid()).thenReturn(accountUuid); + + when(messagesManager.getMessagesForDeviceReactive(account.getUuid(), device.getId(), false)) + .thenReturn(Flux.error(new RedisException("OH NO"))); + + final WebSocketClient client = mock(WebSocketClient.class); + when(client.isOpen()).thenReturn(false); + + WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client, + retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager); + connection.start(); + + verify(retrySchedulingExecutor, never()).schedule(any(Runnable.class), anyLong(), any()); + verify(client, never()).close(anyInt(), anyString()); + } + + @Test + void testReactivePublisherLimitRate() { + final UUID accountUuid = UUID.randomUUID(); + + final long deviceId = 2L; + when(device.getId()).thenReturn(deviceId); + + when(account.getNumber()).thenReturn("+14152222222"); + when(account.getUuid()).thenReturn(accountUuid); + + final int totalMessages = 1000; + + final TestPublisher testPublisher = TestPublisher.createCold(); + final Flux flux = Flux.from(testPublisher); + + when(messagesManager.getMessagesForDeviceReactive(eq(accountUuid), eq(deviceId), anyBoolean())) + .thenReturn(flux); + + final WebSocketClient client = mock(WebSocketClient.class); + when(client.isOpen()).thenReturn(true); + final WebSocketResponseMessage successResponse = mock(WebSocketResponseMessage.class); + when(successResponse.getStatus()).thenReturn(200); + when(client.sendRequest(any(), any(), any(), any())).thenReturn(CompletableFuture.completedFuture(successResponse)); + when(messagesManager.delete(any(), anyLong(), any(), any())).thenReturn( + CompletableFuture.completedFuture(Optional.empty())); + + WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client, + retrySchedulingExecutor, messageDeliveryScheduler, clientReleaseManager); + + connection.start(); + + StepVerifier.setDefaultTimeout(Duration.ofSeconds(5)); + + StepVerifier.create(flux, 0) + .expectSubscription() + .thenRequest(totalMessages * 2) + .then(() -> { + for (long i = 0; i < totalMessages; i++) { + testPublisher.next(createMessage(UUID.randomUUID(), accountUuid, 1111 * i + 1, "message " + i)); + } + testPublisher.complete(); + }) + .expectNextCount(totalMessages) + .expectComplete() + .log() + .verify(); + + testPublisher.assertMaxRequested(WebSocketConnection.MESSAGE_PUBLISHER_LIMIT_RATE); + } + + @Test + void testReactivePublisherDisposedWhenConnectionStopped() { + final UUID accountUuid = UUID.randomUUID(); + + final long deviceId = 2L; + when(device.getId()).thenReturn(deviceId); + + when(account.getNumber()).thenReturn("+14152222222"); + when(account.getUuid()).thenReturn(accountUuid); + + final AtomicBoolean canceled = new AtomicBoolean(); + + final Flux flux = Flux.create(s -> { + s.onRequest(n -> { + // the subscriber should request more than 1 message, but we will only send one, so that + // we are sure the subscriber is waiting for more when we stop the connection + assert n > 1; + s.next(createMessage(UUID.randomUUID(), UUID.randomUUID(), 1111, "first")); + }); + + s.onCancel(() -> canceled.set(true)); + }); + when(messagesManager.getMessagesForDeviceReactive(eq(accountUuid), eq(deviceId), anyBoolean())) + .thenReturn(flux); + + final WebSocketClient client = mock(WebSocketClient.class); + when(client.isOpen()).thenReturn(true); + final WebSocketResponseMessage successResponse = mock(WebSocketResponseMessage.class); + when(successResponse.getStatus()).thenReturn(200); + when(client.sendRequest(any(), any(), any(), any())).thenReturn(CompletableFuture.completedFuture(successResponse)); + when(messagesManager.delete(any(), anyLong(), any(), any())).thenReturn( + CompletableFuture.completedFuture(Optional.empty())); + + WebSocketConnection connection = new WebSocketConnection(receiptSender, messagesManager, auth, device, client, + retrySchedulingExecutor, Schedulers.immediate(), clientReleaseManager); + + connection.start(); + + verify(client).sendRequest(any(), any(), any(), any()); + + // close the connection before the publisher completes + connection.stop(); + + StepVerifier.setDefaultTimeout(Duration.ofSeconds(2)); + + StepVerifier.create(flux) + .expectSubscription() + .expectNextCount(1) + .then(() -> assertTrue(canceled.get())) + // this is not entirely intuitive, but expecting a timeout is the recommendation for verifying cancellation + .expectTimeout(Duration.ofMillis(100)) + .log() + .verify(); + } + + private Envelope createMessage(UUID senderUuid, UUID destinationUuid, long timestamp, String content) { + return Envelope.newBuilder() + .setServerGuid(UUID.randomUUID().toString()) + .setType(Envelope.Type.CIPHERTEXT) + .setTimestamp(timestamp) + .setServerTimestamp(0) + .setSourceUuid(senderUuid.toString()) + .setSourceDevice(SOURCE_DEVICE_ID) + .setDestinationUuid(destinationUuid.toString()) + .setContent(ByteString.copyFrom(content.getBytes(StandardCharsets.UTF_8))) + .build(); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/proto/echo_service.proto b/jdk_17_maven/cs/rest/signal-server/service/src/test/proto/echo_service.proto new file mode 100644 index 000000000..9cc4a6ff5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/proto/echo_service.proto @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +syntax = "proto3"; + +option java_multiple_files = true; + +package org.signal.chat.rpc; + +// A simple service for testing gRPC interceptors +service EchoService { + rpc echo (EchoRequest) returns (EchoResponse) {} +} + +message EchoRequest { + bytes payload = 1; +} + +message EchoResponse { + bytes payload = 1; +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_duplicate_device.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_duplicate_device.json new file mode 100644 index 000000000..ec0496486 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_duplicate_device.json @@ -0,0 +1,23 @@ +{ + "messages" : [{ + "type" : 1, + "destinationDeviceId" : 1, + "destinationRegistrationId" : 222, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }, + { + "type" : 1, + "destinationDeviceId" : 2, + "destinationRegistrationId" : 333, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }, + { + "type" : 1, + "destinationDeviceId" : 1, + "destinationRegistrationId" : 222, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }] +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_extra_device.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_extra_device.json new file mode 100644 index 000000000..cd9b8fcb8 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_extra_device.json @@ -0,0 +1,14 @@ +{ + "messages" : [{ + "type" : 1, + "destinationDeviceId" : 1, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }, + { + "type" : 1, + "destinationDeviceId" : 3, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }] +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_multi_device.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_multi_device.json new file mode 100644 index 000000000..4236372fe --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_multi_device.json @@ -0,0 +1,16 @@ +{ + "messages" : [{ + "type" : 1, + "destinationDeviceId" : 1, + "destinationRegistrationId" : 222, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }, + { + "type" : 1, + "destinationDeviceId" : 2, + "destinationRegistrationId" : 333, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }] +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_multi_device_not_urgent.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_multi_device_not_urgent.json new file mode 100644 index 000000000..c07ca93a2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_multi_device_not_urgent.json @@ -0,0 +1,19 @@ +{ + "urgent": false, + "messages": [ + { + "type": 1, + "destinationDeviceId": 1, + "destinationRegistrationId": 222, + "content": "Zm9vYmFyego", + "timestamp": 1234 + }, + { + "type": 1, + "destinationDeviceId": 2, + "destinationRegistrationId": 333, + "content": "Zm9vYmFyego", + "timestamp": 1234 + } + ] +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_multi_device_pni.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_multi_device_pni.json new file mode 100644 index 000000000..0be6b7832 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_multi_device_pni.json @@ -0,0 +1,16 @@ +{ + "messages" : [{ + "type" : 1, + "destinationDeviceId" : 1, + "destinationRegistrationId" : 2222, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }, + { + "type" : 1, + "destinationDeviceId" : 2, + "destinationRegistrationId" : 3333, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }] +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_null_message_in_list.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_null_message_in_list.json new file mode 100644 index 000000000..11dcf0b5c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_null_message_in_list.json @@ -0,0 +1,8 @@ +{ + "messages" : [ { + "type" : 1, + "destinationDeviceId" : 1, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }, null ] +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_registration_id.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_registration_id.json new file mode 100644 index 000000000..1b7b03ad5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_registration_id.json @@ -0,0 +1,16 @@ +{ + "messages" : [{ + "type" : 1, + "destinationDeviceId" : 1, + "destinationRegistrationId" : 222, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }, + { + "type" : 1, + "destinationDeviceId" : 2, + "destinationRegistrationId" : 999, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }] +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_single_device.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_single_device.json new file mode 100644 index 000000000..d35253c3a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_single_device.json @@ -0,0 +1,8 @@ +{ + "messages" : [{ + "type" : 1, + "destinationDeviceId" : 1, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }] +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_single_device_bad_type.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_single_device_bad_type.json new file mode 100644 index 000000000..4822afcfb --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_single_device_bad_type.json @@ -0,0 +1,8 @@ +{ + "messages" : [{ + "type" : 7, + "destinationDeviceId" : 1, + "content" : "Zm9vYmFyego", + "timestamp" : 1234 + }] +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_single_device_not_urgent.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_single_device_not_urgent.json new file mode 100644 index 000000000..78aa66af3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_single_device_not_urgent.json @@ -0,0 +1,11 @@ +{ + "urgent": false, + "messages": [ + { + "type": 1, + "destinationDeviceId": 1, + "content": "Zm9vYmFyego", + "timestamp": 1234 + } + ] +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_single_device_server_receipt_type.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_single_device_server_receipt_type.json new file mode 100644 index 000000000..34646ef4d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/current_message_single_device_server_receipt_type.json @@ -0,0 +1,10 @@ +{ + "messages": [ + { + "type": 5, + "destinationDeviceId": 1, + "content": "Zm9vYmFyego", + "timestamp": 1234 + } + ] +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/fixer.res.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/fixer.res.json new file mode 100644 index 000000000..b388c2e7c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/fixer.res.json @@ -0,0 +1,15 @@ +{ + "success": true, + "timestamp": 1519296206, + "base": "EUR", + "date": "2021-08-01", + "rates": { + "AUD": 1.566015, + "CAD": 1.560132, + "CHF": 1.154727, + "CNY": 7.827874, + "GBP": 0.882047, + "JPY": 132.360679, + "USD": 1.23396 + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/mismatched_registration_id.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/mismatched_registration_id.json new file mode 100644 index 000000000..273eea57d --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/mismatched_registration_id.json @@ -0,0 +1,3 @@ +{ + "staleDevices" : [2] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/missing_device_response.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/missing_device_response.json new file mode 100644 index 000000000..a0ad77d79 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/missing_device_response.json @@ -0,0 +1,4 @@ +{ + "missingDevices" : [2], + "extraDevices" : [] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/missing_device_response2.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/missing_device_response2.json new file mode 100644 index 000000000..960900cc5 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/missing_device_response2.json @@ -0,0 +1,4 @@ +{ + "missingDevices" : [2], + "extraDevices" : [3] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/prekey_v2.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/prekey_v2.json new file mode 100644 index 000000000..fb1b2829e --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/fixtures/prekey_v2.json @@ -0,0 +1,4 @@ +{ + "keyId" : 1234, + "publicKey" : "BQ+NbroQtVKyFaCSfqzSw8Wy72Ff22RSa5ERKTv5DIk2" +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/logback-test.xml b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/logback-test.xml new file mode 100644 index 000000000..b01f95f92 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..1f0955d45 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/org/whispersystems/textsecuregcm/storage/AccountsManagerTest-testJsonRoundTripSerialization.json b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/org/whispersystems/textsecuregcm/storage/AccountsManagerTest-testJsonRoundTripSerialization.json new file mode 100644 index 000000000..3163b9686 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/org/whispersystems/textsecuregcm/storage/AccountsManagerTest-testJsonRoundTripSerialization.json @@ -0,0 +1,48 @@ +{ + "number": "+14152222222", + "usernameHash": null, + "reservedUsernameHash": null, + "usernameLinkHandle": null, + "devices": [ + { + "id": 1, + "name": null, + "authToken": null, + "salt": null, + "gcmId": null, + "apnId": null, + "voipApnId": null, + "pushTimestamp": 0, + "uninstalledFeedback": 0, + "fetchesMessages": true, + "registrationId": 1, + "signedPreKey": { + "keyId": 1, + "publicKey": "BerKjYSh1PdniL5bhI9kwbH/Et3mx/8CypR1TYo/+d5o", + "signature": "iK2yJkl0l6qe58Fy1dVo31X5sp6EiXSS5FZfa3W//E+Abylfa6ZRmM97CzTdXNu2DjgxZYF43G6HfJ49+99hgg" + }, + "lastSeen" : 1692748800000, + "created" : 1692718240137, + "userAgent": null, + "capabilities": null, + "pniRegistrationId": 2, + "pniSignedPreKey": { + "keyId": 2, + "publicKey": "BXcLL1VLft3tUnr/5UIW5Q0Hsr8/Az0CGJ+EuFqiXCYc", + "signature": "YoKqyeOCHC0E9mqMoc1UPeyuLqGc8nvY+3D3YX5HC1bhxS48ZLYo40xql51A2CpIBqVmA+2gV3PXCV1Yhq4UAQ" + } + } + ], + "identityKey": "BaMV4k/+jSn7jmHnRAPvfc7XBZOcayrhOmHFbGJwMyFS", + "badges": [], + "registrationLock": null, + "registrationLockSalt": null, + "version": 0, + "pni": "22222222-2222-2222-2222-222222222222", + "eu": null, + "pniIdentityKey": "Bc0Myhpf2D+iCgUfIs+UStgffR/VGQRfP9mwFHI4U2x4", + "cpv": null, + "uak": "p5uWNi83Muqsd16PLi0/tQ==", + "uua": true, + "inCds": true +} diff --git a/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/org/whispersystems/textsecuregcm/util/ip2asn-test.tsv b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/org/whispersystems/textsecuregcm/util/ip2asn-test.tsv new file mode 100644 index 000000000..08c4ce2ba --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/service/src/test/resources/org/whispersystems/textsecuregcm/util/ip2asn-test.tsv @@ -0,0 +1,3 @@ +95865344 95865855 0 None Not routed +458051584 458227711 7552 VN VIETEL-AS-AP Viettel Group +843841536 844103679 7922 US COMCAST-7922 - Comcast Cable Communications, LLC diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/pom.xml b/jdk_17_maven/cs/rest/signal-server/websocket-resources/pom.xml new file mode 100644 index 000000000..1ae53ca9c --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/pom.xml @@ -0,0 +1,113 @@ + + + + TextSecureServer + org.whispersystems.textsecure + + 10.3.0 + + 4.0.0 + websocket-resources + + + + org.eclipse.jetty.websocket + websocket-api + + + org.eclipse.jetty.websocket + websocket-server + runtime + + + org.eclipse.jetty.websocket + websocket-servlet + + + io.dropwizard + dropwizard-core + + + io.dropwizard + dropwizard-auth + + + io.dropwizard + dropwizard-jersey + + + io.dropwizard + dropwizard-logging + + + jakarta.inject + jakarta.inject-api + + + jakarta.validation + jakarta.validation-api + + + jakarta.ws.rs + jakarta.ws.rs-api + + + org.glassfish.jersey.core + jersey-common + + + org.glassfish.jersey.core + jersey-server + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + com.google.protobuf + protobuf-java + + + com.google.guava + guava + + + org.slf4j + slf4j-api + + + com.google.code.findbugs + jsr305 + + + + org.mockito + mockito-inline + test + + + org.junit.jupiter + junit-jupiter + test + + + + diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/Stories.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/Stories.java new file mode 100644 index 000000000..9cf859126 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/Stories.java @@ -0,0 +1,15 @@ +package org.whispersystems.websocket; + +/** + * Class containing constants and shared logic for handling stories. + *

    + * In particular, it defines the way we interpret the X-Signal-Receive-Stories header + * which is used by both WebSockets and by the REST API. + */ +public class Stories { + public final static String X_SIGNAL_RECEIVE_STORIES = "X-Signal-Receive-Stories"; + + public static boolean parseReceiveStoriesHeader(String s) { + return "true".equals(s); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/WebSocketClient.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/WebSocketClient.java new file mode 100644 index 000000000..a3dbd33d3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/WebSocketClient.java @@ -0,0 +1,111 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket; + +import com.google.common.net.HttpHeaders; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WebSocketException; +import org.eclipse.jetty.websocket.api.WriteCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.websocket.messages.WebSocketMessage; +import org.whispersystems.websocket.messages.WebSocketMessageFactory; +import org.whispersystems.websocket.messages.WebSocketResponseMessage; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class WebSocketClient { + + private static final Logger logger = LoggerFactory.getLogger(WebSocketClient.class); + + private final Session session; + private final RemoteEndpoint remoteEndpoint; + private final WebSocketMessageFactory messageFactory; + private final Map> pendingRequestMapper; + private final long created; + + public WebSocketClient(Session session, RemoteEndpoint remoteEndpoint, + WebSocketMessageFactory messageFactory, + Map> pendingRequestMapper) { + this.session = session; + this.remoteEndpoint = remoteEndpoint; + this.messageFactory = messageFactory; + this.pendingRequestMapper = pendingRequestMapper; + this.created = System.currentTimeMillis(); + } + + public CompletableFuture sendRequest(String verb, String path, + List headers, + Optional body) + { + final long requestId = generateRequestId(); + final CompletableFuture future = new CompletableFuture<>(); + + pendingRequestMapper.put(requestId, future); + + WebSocketMessage requestMessage = messageFactory.createRequest(Optional.of(requestId), verb, path, headers, body); + + try { + remoteEndpoint.sendBytes(ByteBuffer.wrap(requestMessage.toByteArray()), new WriteCallback() { + @Override + public void writeFailed(Throwable x) { + logger.debug("Write failed", x); + pendingRequestMapper.remove(requestId); + future.completeExceptionally(x); + } + + @Override + public void writeSuccess() {} + }); + } catch (WebSocketException e) { + logger.debug("Write", e); + pendingRequestMapper.remove(requestId); + future.completeExceptionally(e); + } + + return future; + } + + public String getUserAgent() { + return session.getUpgradeRequest().getHeader(HttpHeaders.USER_AGENT); + } + + public long getCreatedTimestamp() { + return this.created; + } + + public boolean isOpen() { + return session.isOpen(); + } + + public void close(int code, String message) { + session.close(code, message); + } + + public boolean shouldDeliverStories() { + String value = session.getUpgradeRequest().getHeader(Stories.X_SIGNAL_RECEIVE_STORIES); + return Stories.parseReceiveStoriesHeader(value); + } + + public void hardDisconnectQuietly() { + try { + session.disconnect(); + } catch (IOException e) { + // quietly we said + } + } + + private long generateRequestId() { + return Math.abs(new SecureRandom().nextLong()); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/WebSocketResourceProvider.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/WebSocketResourceProvider.java new file mode 100644 index 000000000..31faeaefe --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/WebSocketResourceProvider.java @@ -0,0 +1,268 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket; + +import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.UninitializedMessageException; +import org.eclipse.jetty.websocket.api.MessageTooLargeException; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WebSocketListener; +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.websocket.logging.WebsocketRequestLog; +import org.whispersystems.websocket.messages.InvalidMessageException; +import org.whispersystems.websocket.messages.WebSocketMessage; +import org.whispersystems.websocket.messages.WebSocketMessageFactory; +import org.whispersystems.websocket.messages.WebSocketRequestMessage; +import org.whispersystems.websocket.messages.WebSocketResponseMessage; +import org.whispersystems.websocket.session.ContextPrincipal; +import org.whispersystems.websocket.session.WebSocketSessionContext; +import org.whispersystems.websocket.setup.WebSocketConnectListener; + +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.security.Principal; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public class WebSocketResourceProvider implements WebSocketListener { + + /** + * A static exception instance passed to outstanding requests (via {@code completeExceptionally} in + * {@link #onWebSocketClose(int, String)} + */ + public static final IOException CONNECTION_CLOSED_EXCEPTION = new IOException("Connection closed!"); + private static final Logger logger = LoggerFactory.getLogger(WebSocketResourceProvider.class); + + private final Map> requestMap = new ConcurrentHashMap<>(); + + private final T authenticated; + private final WebSocketMessageFactory messageFactory; + private final Optional connectListener; + private final ApplicationHandler jerseyHandler; + private final WebsocketRequestLog requestLog; + private final long idleTimeoutMillis; + private final String remoteAddress; + + private Session session; + private RemoteEndpoint remoteEndpoint; + private WebSocketSessionContext context; + + private static final Set EXCLUDED_UPGRADE_REQUEST_HEADERS = Set.of("connection", "upgrade"); + + public WebSocketResourceProvider(String remoteAddress, + ApplicationHandler jerseyHandler, + WebsocketRequestLog requestLog, + T authenticated, + WebSocketMessageFactory messageFactory, + Optional connectListener, + long idleTimeoutMillis) + { + this.remoteAddress = remoteAddress; + this.jerseyHandler = jerseyHandler; + this.requestLog = requestLog; + this.authenticated = authenticated; + this.messageFactory = messageFactory; + this.connectListener = connectListener; + this.idleTimeoutMillis = idleTimeoutMillis; + } + + @Override + public void onWebSocketConnect(Session session) { + this.session = session; + this.remoteEndpoint = session.getRemote(); + this.context = new WebSocketSessionContext(new WebSocketClient(session, remoteEndpoint, messageFactory, requestMap)); + this.context.setAuthenticated(authenticated); + this.session.setIdleTimeout(idleTimeoutMillis); + + connectListener.ifPresent(listener -> listener.onWebSocketConnect(this.context)); + } + + @Override + public void onWebSocketError(Throwable cause) { + logger.debug("onWebSocketError", cause); + + final int closeCode; + final String message; + if (cause instanceof MessageTooLargeException) { + closeCode = 1009; + message = "Frame too large"; + } else { + closeCode = 1011; + message = "Server error"; + } + + close(session, closeCode, message); + } + + @Override + public void onWebSocketBinary(byte[] payload, int offset, int length) { + try { + WebSocketMessage webSocketMessage = messageFactory.parseMessage(payload, offset, length); + + switch (webSocketMessage.getType()) { + case REQUEST_MESSAGE: + handleRequest(webSocketMessage.getRequestMessage()); + break; + case RESPONSE_MESSAGE: + handleResponse(webSocketMessage.getResponseMessage()); + break; + default: + close(session, 1018, "Badly formatted"); + break; + } + } catch (UninitializedMessageException | InvalidMessageException e) { + logger.debug("Parsing", e); + close(session, 1018, "Badly formatted"); + } + } + + @Override + public void onWebSocketClose(int statusCode, String reason) { + if (context != null) { + context.notifyClosed(statusCode, reason); + + for (long requestId : requestMap.keySet()) { + CompletableFuture outstandingRequest = requestMap.remove(requestId); + + if (outstandingRequest != null) { + outstandingRequest.completeExceptionally(CONNECTION_CLOSED_EXCEPTION); + } + } + } + } + + @Override + public void onWebSocketText(String message) { + logger.debug("onWebSocketText!"); + } + + private void handleRequest(WebSocketRequestMessage requestMessage) { + ContainerRequest containerRequest = new ContainerRequest(null, URI.create(requestMessage.getPath()), requestMessage.getVerb(), new WebSocketSecurityContext(new ContextPrincipal(context)), new MapPropertiesDelegate(new HashMap<>()), jerseyHandler.getConfiguration()); + containerRequest.headers(getCombinedHeaders(session.getUpgradeRequest().getHeaders(), requestMessage.getHeaders())); + + if (requestMessage.getBody().isPresent()) { + containerRequest.setEntityStream(new ByteArrayInputStream(requestMessage.getBody().get())); + } + + ByteArrayOutputStream responseBody = new ByteArrayOutputStream(); + CompletableFuture responseFuture = (CompletableFuture) jerseyHandler.apply(containerRequest, responseBody); + + responseFuture.thenAccept(response -> { + sendResponse(requestMessage, response, responseBody); + requestLog.log(remoteAddress, containerRequest, response); + }).exceptionally(exception -> { + logger.warn("Websocket Error: " + requestMessage.getVerb() + " " + requestMessage.getPath() + "\n" + requestMessage.getBody(), exception); + sendErrorResponse(requestMessage, Response.status(500).build()); + requestLog.log(remoteAddress, containerRequest, new ContainerResponse(containerRequest, Response.status(500).build())); + return null; + }); + } + + @VisibleForTesting + static Map> getCombinedHeaders(final Map> upgradeRequestHeaders, final Map requestMessageHeaders) { + final Map> combinedHeaders = new HashMap<>(); + + upgradeRequestHeaders.entrySet().stream() + .filter(entry -> shouldIncludeUpgradeRequestHeader(entry.getKey())) + .forEach(entry -> combinedHeaders.put(entry.getKey(), entry.getValue())); + + requestMessageHeaders.entrySet().stream() + .filter(entry -> shouldIncludeRequestMessageHeader(entry.getKey())) + .forEach(entry -> combinedHeaders.put(entry.getKey(), List.of(entry.getValue()))); + + return combinedHeaders; + } + + @VisibleForTesting + static boolean shouldIncludeUpgradeRequestHeader(final String header) { + return !EXCLUDED_UPGRADE_REQUEST_HEADERS.contains(header.toLowerCase()) && !header.toLowerCase().contains("websocket-"); + } + + @VisibleForTesting + static boolean shouldIncludeRequestMessageHeader(final String header) { + return !"X-Forwarded-For".equalsIgnoreCase(header.trim()); + } + + private void handleResponse(WebSocketResponseMessage responseMessage) { + CompletableFuture future = requestMap.remove(responseMessage.getRequestId()); + + if (future != null) { + future.complete(responseMessage); + } + } + + private void close(Session session, int status, String message) { + session.close(status, message); + } + + private void sendResponse(WebSocketRequestMessage requestMessage, ContainerResponse response, ByteArrayOutputStream responseBody) { + if (requestMessage.hasRequestId()) { + byte[] body = responseBody.toByteArray(); + + if (body.length <= 0) { + body = null; + } + + byte[] responseBytes = messageFactory.createResponse(requestMessage.getRequestId(), + response.getStatus(), + response.getStatusInfo().getReasonPhrase(), + getHeaderList(response.getStringHeaders()), + Optional.ofNullable(body)) + .toByteArray(); + + remoteEndpoint.sendBytesByFuture(ByteBuffer.wrap(responseBytes)); + } + } + + private void sendErrorResponse(WebSocketRequestMessage requestMessage, Response error) { + if (requestMessage.hasRequestId()) { + WebSocketMessage response = messageFactory.createResponse(requestMessage.getRequestId(), + error.getStatus(), + "Error response", + getHeaderList(error.getStringHeaders()), + Optional.empty()); + + remoteEndpoint.sendBytesByFuture(ByteBuffer.wrap(response.toByteArray())); + } + } + + + @VisibleForTesting + WebSocketSessionContext getContext() { + return context; + } + + @VisibleForTesting + static List getHeaderList(final MultivaluedMap headerMap) { + final List headers = new LinkedList<>(); + + if (headerMap != null) { + for (String key : headerMap.keySet()) { + headers.add(key + ":" + headerMap.getFirst(key)); + } + } + + return headers; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/WebSocketResourceProviderFactory.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/WebSocketResourceProviderFactory.java new file mode 100644 index 000000000..e9b6e88a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/WebSocketResourceProviderFactory.java @@ -0,0 +1,104 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket; + +import static java.util.Optional.ofNullable; + +import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; +import java.io.IOException; +import java.security.Principal; +import java.util.Arrays; +import java.util.Optional; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.servlet.WebSocketCreator; +import org.eclipse.jetty.websocket.servlet.WebSocketServlet; +import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.glassfish.jersey.server.ApplicationHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.websocket.auth.AuthenticationException; +import org.whispersystems.websocket.auth.WebSocketAuthenticator; +import org.whispersystems.websocket.auth.WebSocketAuthenticator.AuthenticationResult; +import org.whispersystems.websocket.auth.WebsocketAuthValueFactoryProvider; +import org.whispersystems.websocket.configuration.WebSocketConfiguration; +import org.whispersystems.websocket.session.WebSocketSessionContextValueFactoryProvider; +import org.whispersystems.websocket.setup.WebSocketEnvironment; + +public class WebSocketResourceProviderFactory extends WebSocketServlet implements WebSocketCreator { + + private static final Logger logger = LoggerFactory.getLogger(WebSocketResourceProviderFactory.class); + + private final WebSocketEnvironment environment; + private final ApplicationHandler jerseyApplicationHandler; + private final WebSocketConfiguration configuration; + + public WebSocketResourceProviderFactory(WebSocketEnvironment environment, Class principalClass, + WebSocketConfiguration configuration) { + this.environment = environment; + + environment.jersey().register(new WebSocketSessionContextValueFactoryProvider.Binder()); + environment.jersey().register(new WebsocketAuthValueFactoryProvider.Binder(principalClass)); + environment.jersey().register(new JacksonMessageBodyProvider(environment.getObjectMapper())); + + this.jerseyApplicationHandler = new ApplicationHandler(environment.jersey()); + + this.configuration = configuration; + } + + @Override + public Object createWebSocket(ServletUpgradeRequest request, ServletUpgradeResponse response) { + try { + Optional> authenticator = Optional.ofNullable(environment.getAuthenticator()); + T authenticated = null; + + if (authenticator.isPresent()) { + AuthenticationResult authenticationResult = authenticator.get().authenticate(request); + + if (authenticationResult.getUser().isEmpty() && authenticationResult.credentialsPresented()) { + response.sendForbidden("Unauthorized"); + return null; + } else { + authenticated = authenticationResult.getUser().orElse(null); + } + } + + return new WebSocketResourceProvider<>(getRemoteAddress(request), + this.jerseyApplicationHandler, + this.environment.getRequestLog(), + authenticated, + this.environment.getMessageFactory(), + ofNullable(this.environment.getConnectListener()), + this.environment.getIdleTimeoutMillis()); + } catch (AuthenticationException | IOException e) { + logger.warn("Authentication failure", e); + try { + response.sendError(500, "Failure"); + } catch (IOException ignored) { + } + return null; + } + } + + @Override + public void configure(WebSocketServletFactory factory) { + factory.setCreator(this); + factory.getPolicy().setMaxBinaryMessageSize(configuration.getMaxBinaryMessageSize()); + factory.getPolicy().setMaxTextMessageSize(configuration.getMaxTextMessageSize()); + } + + private String getRemoteAddress(ServletUpgradeRequest request) { + String forwardedFor = request.getHeader("X-Forwarded-For"); + + if (forwardedFor == null || forwardedFor.isBlank()) { + return request.getRemoteAddress(); + } else { + return Arrays.stream(forwardedFor.split(",")) + .map(String::trim) + .reduce((a, b) -> b) + .orElseThrow(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/WebSocketSecurityContext.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/WebSocketSecurityContext.java new file mode 100644 index 000000000..f079cf2fa --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/WebSocketSecurityContext.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket; + +import org.whispersystems.websocket.session.ContextPrincipal; +import org.whispersystems.websocket.session.WebSocketSessionContext; + +import javax.ws.rs.core.SecurityContext; +import java.security.Principal; + +public class WebSocketSecurityContext implements SecurityContext { + + private final ContextPrincipal principal; + + public WebSocketSecurityContext(ContextPrincipal principal) { + this.principal = principal; + } + + @Override + public Principal getUserPrincipal() { + return (Principal)principal.getContext().getAuthenticated(); + } + + @Override + public boolean isUserInRole(String role) { + return false; + } + + @Override + public boolean isSecure() { + return principal != null; + } + + @Override + public String getAuthenticationScheme() { + return null; + } + + public WebSocketSessionContext getSessionContext() { + return principal.getContext(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/auth/AuthenticationException.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/auth/AuthenticationException.java new file mode 100644 index 000000000..447928ad1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/auth/AuthenticationException.java @@ -0,0 +1,17 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.auth; + +public class AuthenticationException extends Exception { + + public AuthenticationException(String s) { + super(s); + } + + public AuthenticationException(Exception e) { + super(e); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/auth/WebSocketAuthenticator.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/auth/WebSocketAuthenticator.java new file mode 100644 index 000000000..4c913fe86 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/auth/WebSocketAuthenticator.java @@ -0,0 +1,32 @@ +/* + * Copyright 2013 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.auth; + +import java.security.Principal; +import java.util.Optional; +import org.eclipse.jetty.websocket.api.UpgradeRequest; + +public interface WebSocketAuthenticator { + AuthenticationResult authenticate(UpgradeRequest request) throws AuthenticationException; + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + class AuthenticationResult { + private final Optional user; + private final boolean credentialsPresented; + + public AuthenticationResult(final Optional user, final boolean credentialsPresented) { + this.user = user; + this.credentialsPresented = credentialsPresented; + } + + public Optional getUser() { + return user; + } + + public boolean credentialsPresented() { + return credentialsPresented; + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/auth/WebsocketAuthValueFactoryProvider.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/auth/WebsocketAuthValueFactoryProvider.java new file mode 100644 index 000000000..28c488ea3 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/auth/WebsocketAuthValueFactoryProvider.java @@ -0,0 +1,117 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.auth; + +import io.dropwizard.auth.Auth; +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.internal.inject.AbstractValueParamProvider; +import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; +import org.glassfish.jersey.server.model.Parameter; +import org.glassfish.jersey.server.spi.internal.ValueParamProvider; + +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.WebApplicationException; +import java.lang.reflect.ParameterizedType; +import java.security.Principal; +import java.util.Optional; +import java.util.function.Function; + +@Singleton +public class WebsocketAuthValueFactoryProvider extends AbstractValueParamProvider { + + private final Class principalClass; + + @Inject + public WebsocketAuthValueFactoryProvider(MultivaluedParameterExtractorProvider mpep, WebsocketPrincipalClassProvider principalClassProvider) { + super(() -> mpep, Parameter.Source.UNKNOWN); + this.principalClass = principalClassProvider.clazz; + } + + @Nullable + @Override + protected Function createValueProvider(Parameter parameter) { + if (!parameter.isAnnotationPresent(Auth.class)) { + return null; + } + + if (parameter.getRawType() == Optional.class && + ParameterizedType.class.isAssignableFrom(parameter.getType().getClass()) && + principalClass == ((ParameterizedType)parameter.getType()).getActualTypeArguments()[0]) + { + return request -> new OptionalContainerRequestValueFactory(request).provide(); + } else if (principalClass.equals(parameter.getRawType())) { + return request -> new StandardContainerRequestValueFactory(request).provide(); + } else { + throw new IllegalStateException("Can't inject unassignable principal: " + principalClass + " for parameter: " + parameter); + } + } + + @Singleton + static class WebsocketPrincipalClassProvider { + + private final Class clazz; + + WebsocketPrincipalClassProvider(Class clazz) { + this.clazz = clazz; + } + } + + /** + * Injection binder for {@link io.dropwizard.auth.AuthValueFactoryProvider}. + * + * @param the type of the principal + */ + public static class Binder extends AbstractBinder { + + private final Class principalClass; + + public Binder(Class principalClass) { + this.principalClass = principalClass; + } + + @Override + protected void configure() { + bind(new WebsocketPrincipalClassProvider<>(principalClass)).to(WebsocketPrincipalClassProvider.class); + bind(WebsocketAuthValueFactoryProvider.class).to(ValueParamProvider.class).in(Singleton.class); + } + } + + private static class StandardContainerRequestValueFactory { + + private final ContainerRequest request; + + public StandardContainerRequestValueFactory(ContainerRequest request) { + this.request = request; + } + + public Principal provide() { + final Principal principal = request.getSecurityContext().getUserPrincipal(); + + if (principal == null) { + throw new WebApplicationException("Authenticated resource", 401); + } + + return principal; + } + + } + + private static class OptionalContainerRequestValueFactory { + + private final ContainerRequest request; + + public OptionalContainerRequestValueFactory(ContainerRequest request) { + this.request = request; + } + + public Optional provide() { + return Optional.ofNullable(request.getSecurityContext().getUserPrincipal()); + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/configuration/WebSocketConfiguration.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/configuration/WebSocketConfiguration.java new file mode 100644 index 000000000..fb8c8a957 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/configuration/WebSocketConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import org.whispersystems.websocket.logging.WebsocketRequestLoggerFactory; + +public class WebSocketConfiguration { + + @Valid + @NotNull + @JsonProperty + private WebsocketRequestLoggerFactory requestLog = new WebsocketRequestLoggerFactory(); + + @Min(512 * 1024) // 512 KB + @Max(10 * 1024 * 1024) // 10 MB + @JsonProperty + private int maxBinaryMessageSize = 512 * 1024; + + @Min(512 * 1024) // 512 KB + @Max(10 * 1024 * 1024) // 10 MB + @JsonProperty + private int maxTextMessageSize = 512 * 1024; + + public WebsocketRequestLoggerFactory getRequestLog() { + return requestLog; + } + + public int getMaxBinaryMessageSize() { + return maxBinaryMessageSize; + } + + public int getMaxTextMessageSize() { + return maxTextMessageSize; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/AsyncWebsocketEventAppenderFactory.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/AsyncWebsocketEventAppenderFactory.java new file mode 100644 index 000000000..2d0399de7 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/AsyncWebsocketEventAppenderFactory.java @@ -0,0 +1,20 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging; + +import ch.qos.logback.core.AsyncAppenderBase; +import io.dropwizard.logging.async.AsyncAppenderFactory; + +public class AsyncWebsocketEventAppenderFactory implements AsyncAppenderFactory { + @Override + public AsyncAppenderBase build() { + return new AsyncAppenderBase() { + @Override + protected void preprocess(WebsocketEvent event) { + event.prepareForDeferredProcessing(); + } + }; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/WebsocketEvent.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/WebsocketEvent.java new file mode 100644 index 000000000..29cd5b098 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/WebsocketEvent.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging; + +import ch.qos.logback.core.spi.DeferredProcessingAware; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; + +import javax.ws.rs.core.MultivaluedMap; +import java.util.List; + +public class WebsocketEvent implements DeferredProcessingAware { + + public static final int SENTINEL = -1; + public static final String NA = "-"; + + private final String remoteAddress; + private final ContainerRequest request; + private final ContainerResponse response; + private final long timestamp; + + public WebsocketEvent(String remoteAddress, ContainerRequest jerseyRequest, ContainerResponse jettyResponse) { + this.timestamp = System.currentTimeMillis(); + this.remoteAddress = remoteAddress; + this.request = jerseyRequest; + this.response = jettyResponse; + } + + public String getRemoteHost() { + return remoteAddress; + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public void prepareForDeferredProcessing() { + + } + + public String getMethod() { + return request.getMethod(); + } + + public String getPath() { + return request.getBaseUri().getPath() + request.getPath(false); + } + + public String getProtocol() { + return "WS"; + } + + public int getStatusCode() { + return response.getStatus(); + } + + public long getContentLength() { + return response.getLength(); + } + + public String getRequestHeader(String key) { + List values = request.getRequestHeader(key); + + if (values == null) return NA; + else return values.stream().findFirst().orElse(NA); + } + + public MultivaluedMap getRequestHeaderMap() { + return request.getRequestHeaders(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/WebsocketRequestLog.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/WebsocketRequestLog.java new file mode 100644 index 000000000..8fbac2f27 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/WebsocketRequestLog.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging; + +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; + +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.AppenderAttachableImpl; +import ch.qos.logback.core.spi.FilterAttachableImpl; +import ch.qos.logback.core.spi.FilterReply; + +public class WebsocketRequestLog { + + private final AppenderAttachableImpl aai = new AppenderAttachableImpl<>(); + private final FilterAttachableImpl fai = new FilterAttachableImpl<>(); + + public WebsocketRequestLog() { + } + + public void log(String remoteAddress, ContainerRequest jerseyRequest, ContainerResponse jettyResponse) { + WebsocketEvent event = new WebsocketEvent(remoteAddress, jerseyRequest, jettyResponse); + + if (getFilterChainDecision(event) == FilterReply.DENY) { + return; + } + + aai.appendLoopOnAppenders(event); + } + + + public void addAppender(Appender newAppender) { + aai.addAppender(newAppender); + } + + public void addFilter(Filter newFilter) { + fai.addFilter(newFilter); + } + + public FilterReply getFilterChainDecision(WebsocketEvent event) { + return fai.getFilterChainDecision(event); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/WebsocketRequestLoggerFactory.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/WebsocketRequestLoggerFactory.java new file mode 100644 index 000000000..697739394 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/WebsocketRequestLoggerFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.LoggerFactory; +import org.whispersystems.websocket.logging.layout.WebsocketEventLayoutFactory; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.List; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import io.dropwizard.logging.AppenderFactory; +import io.dropwizard.logging.ConsoleAppenderFactory; +import io.dropwizard.logging.async.AsyncAppenderFactory; +import io.dropwizard.logging.filter.LevelFilterFactory; +import io.dropwizard.logging.filter.NullLevelFilterFactory; +import io.dropwizard.logging.layout.LayoutFactory; + +public class WebsocketRequestLoggerFactory { + + @VisibleForTesting + @Valid + @NotNull + public List> appenders = Collections.singletonList(new ConsoleAppenderFactory<>()); + + public WebsocketRequestLog build(String name) { + final Logger logger = (Logger) LoggerFactory.getLogger("websocket.request"); + logger.setAdditive(false); + + final LoggerContext context = logger.getLoggerContext(); + final WebsocketRequestLog requestLog = new WebsocketRequestLog(); + final LevelFilterFactory levelFilterFactory = new NullLevelFilterFactory<>(); + final AsyncAppenderFactory asyncAppenderFactory = new AsyncWebsocketEventAppenderFactory(); + final LayoutFactory layoutFactory = new WebsocketEventLayoutFactory(); + + for (AppenderFactory output : appenders) { + requestLog.addAppender(output.build(context, name, layoutFactory, levelFilterFactory, asyncAppenderFactory)); + } + + return requestLog; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/WebsocketEventLayout.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/WebsocketEventLayout.java new file mode 100644 index 000000000..d42916b98 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/WebsocketEventLayout.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging.layout; + +import ch.qos.logback.core.Context; +import ch.qos.logback.core.pattern.PatternLayoutBase; +import org.whispersystems.websocket.logging.WebsocketEvent; +import org.whispersystems.websocket.logging.layout.converters.ContentLengthConverter; +import org.whispersystems.websocket.logging.layout.converters.DateConverter; +import org.whispersystems.websocket.logging.layout.converters.EnsureLineSeparation; +import org.whispersystems.websocket.logging.layout.converters.NAConverter; +import org.whispersystems.websocket.logging.layout.converters.RemoteHostConverter; +import org.whispersystems.websocket.logging.layout.converters.RequestHeaderConverter; +import org.whispersystems.websocket.logging.layout.converters.RequestUrlConverter; +import org.whispersystems.websocket.logging.layout.converters.StatusCodeConverter; + +import java.util.HashMap; +import java.util.Map; + +public class WebsocketEventLayout extends PatternLayoutBase { + + private static final Map DEFAULT_CONVERTERS = new HashMap<>() {{ + put("h", RemoteHostConverter.class.getName()); + put("l", NAConverter.class.getName()); + put("u", NAConverter.class.getName()); + put("t", DateConverter.class.getName()); + put("r", RequestUrlConverter.class.getName()); + put("s", StatusCodeConverter.class.getName()); + put("b", ContentLengthConverter.class.getName()); + put("i", RequestHeaderConverter.class.getName()); + }}; + + public static final String CLF_PATTERN = "%h %l %u [%t] \"%r\" %s %b"; + public static final String CLF_PATTERN_NAME = "common"; + public static final String CLF_PATTERN_NAME_2 = "clf"; + public static final String COMBINED_PATTERN = "%h %l %u [%t] \"%r\" %s %b \"%i{Referer}\" \"%i{User-Agent}\""; + public static final String COMBINED_PATTERN_NAME = "combined"; + public static final String HEADER_PREFIX = "#logback.access pattern: "; + + public WebsocketEventLayout(Context context) { + setOutputPatternAsHeader(false); + setPattern(COMBINED_PATTERN); + setContext(context); + + this.postCompileProcessor = new EnsureLineSeparation(); + } + + @Override + public Map getDefaultConverterMap() { + return DEFAULT_CONVERTERS; + } + + @Override + public String doLayout(WebsocketEvent event) { + if (!isStarted()) { + return null; + } + + return writeLoopOnConverters(event); + } + + @Override + public void start() { + if (getPattern().equalsIgnoreCase(CLF_PATTERN_NAME) || getPattern().equalsIgnoreCase(CLF_PATTERN_NAME_2)) { + setPattern(CLF_PATTERN); + } else if (getPattern().equalsIgnoreCase(COMBINED_PATTERN_NAME)) { + setPattern(COMBINED_PATTERN); + } + + super.start(); + } + + @Override + protected String getPresentationHeaderPrefix() { + return HEADER_PREFIX; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/WebsocketEventLayoutFactory.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/WebsocketEventLayoutFactory.java new file mode 100644 index 000000000..8013d38dc --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/WebsocketEventLayoutFactory.java @@ -0,0 +1,19 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging.layout; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.pattern.PatternLayoutBase; +import io.dropwizard.logging.layout.LayoutFactory; +import org.whispersystems.websocket.logging.WebsocketEvent; + +import java.util.TimeZone; + +public class WebsocketEventLayoutFactory implements LayoutFactory { + @Override + public PatternLayoutBase build(LoggerContext context, TimeZone timeZone) { + return new WebsocketEventLayout(context); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/ContentLengthConverter.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/ContentLengthConverter.java new file mode 100644 index 000000000..46187a2d0 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/ContentLengthConverter.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging.layout.converters; + +import org.whispersystems.websocket.logging.WebsocketEvent; + +public class ContentLengthConverter extends WebSocketEventConverter { + @Override + public String convert(WebsocketEvent event) { + if (event.getContentLength() == WebsocketEvent.SENTINEL) { + return WebsocketEvent.NA; + } else { + return Long.toString(event.getContentLength()); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/DateConverter.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/DateConverter.java new file mode 100644 index 000000000..abbf27cea --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/DateConverter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging.layout.converters; + +import ch.qos.logback.core.CoreConstants; +import ch.qos.logback.core.util.CachingDateFormatter; +import org.whispersystems.websocket.logging.WebsocketEvent; + +import java.util.List; +import java.util.TimeZone; + +public class DateConverter extends WebSocketEventConverter { + + private CachingDateFormatter cachingDateFormatter = null; + + @Override + public void start() { + + String datePattern = getFirstOption(); + if (datePattern == null) { + datePattern = CoreConstants.CLF_DATE_PATTERN; + } + + if (datePattern.equals(CoreConstants.ISO8601_STR)) { + datePattern = CoreConstants.ISO8601_PATTERN; + } + + try { + cachingDateFormatter = new CachingDateFormatter(datePattern); + // maximumCacheValidity = CachedDateFormat.getMaximumCacheValidity(pattern); + } catch (IllegalArgumentException e) { + addWarn("Could not instantiate SimpleDateFormat with pattern " + datePattern, e); + addWarn("Defaulting to " + CoreConstants.CLF_DATE_PATTERN); + cachingDateFormatter = new CachingDateFormatter(CoreConstants.CLF_DATE_PATTERN); + } + + List optionList = getOptionList(); + + // if the option list contains a TZ option, then set it. + if (optionList != null && optionList.size() > 1) { + TimeZone tz = TimeZone.getTimeZone((String) optionList.get(1)); + cachingDateFormatter.setTimeZone(tz); + } + } + + @Override + public String convert(WebsocketEvent websocketEvent) { + long timestamp = websocketEvent.getTimestamp(); + return cachingDateFormatter.format(timestamp); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/EnsureLineSeparation.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/EnsureLineSeparation.java new file mode 100644 index 000000000..59322e154 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/EnsureLineSeparation.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging.layout.converters; + +import org.whispersystems.websocket.logging.WebsocketEvent; + +import ch.qos.logback.core.Context; +import ch.qos.logback.core.pattern.Converter; +import ch.qos.logback.core.pattern.ConverterUtil; +import ch.qos.logback.core.pattern.PostCompileProcessor; + +public class EnsureLineSeparation implements PostCompileProcessor { + + /** + * Add a line separator converter so that access event appears on a separate + * line. + */ + @Override + public void process(Context context, Converter head) { + if (head == null) + throw new IllegalArgumentException("Empty converter chain"); + + // if head != null, then tail != null as well + Converter tail = ConverterUtil.findTail(head); + Converter newLineConverter = new LineSeparatorConverter(); + + if (!(tail instanceof LineSeparatorConverter)) { + tail.setNext(newLineConverter); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/LineSeparatorConverter.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/LineSeparatorConverter.java new file mode 100644 index 000000000..d1860b392 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/LineSeparatorConverter.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging.layout.converters; + +import org.whispersystems.websocket.logging.WebsocketEvent; + +import ch.qos.logback.core.CoreConstants; + +public class LineSeparatorConverter extends WebSocketEventConverter { + public LineSeparatorConverter() { + } + + public String convert(WebsocketEvent event) { + return CoreConstants.LINE_SEPARATOR; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/NAConverter.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/NAConverter.java new file mode 100644 index 000000000..e83eb2636 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/NAConverter.java @@ -0,0 +1,14 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging.layout.converters; + +import org.whispersystems.websocket.logging.WebsocketEvent; + +public class NAConverter extends WebSocketEventConverter { + @Override + public String convert(WebsocketEvent event) { + return WebsocketEvent.NA; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/RemoteHostConverter.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/RemoteHostConverter.java new file mode 100644 index 000000000..5862faacf --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/RemoteHostConverter.java @@ -0,0 +1,14 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging.layout.converters; + +import org.whispersystems.websocket.logging.WebsocketEvent; + +public class RemoteHostConverter extends WebSocketEventConverter { + @Override + public String convert(WebsocketEvent event) { + return event.getRemoteHost(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/RequestHeaderConverter.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/RequestHeaderConverter.java new file mode 100644 index 000000000..7a10466d2 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/RequestHeaderConverter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging.layout.converters; + +import org.whispersystems.websocket.logging.WebsocketEvent; + +import ch.qos.logback.core.util.OptionHelper; + +public class RequestHeaderConverter extends WebSocketEventConverter { + + private String key; + + @Override + public void start() { + key = getFirstOption(); + if (OptionHelper.isEmpty(key)) { + addWarn("Missing key for the requested header. Defaulting to all keys."); + key = null; + } + super.start(); + } + + @Override + public String convert(WebsocketEvent websocketEvent) { + if (!isStarted()) { + return "INACTIVE_HEADER_CONV"; + } + + if (key != null) { + return websocketEvent.getRequestHeader(key); + } else { + return websocketEvent.getRequestHeaderMap().toString(); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/RequestUrlConverter.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/RequestUrlConverter.java new file mode 100644 index 000000000..f74f2d9be --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/RequestUrlConverter.java @@ -0,0 +1,19 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging.layout.converters; + +import org.whispersystems.websocket.logging.WebsocketEvent; + +public class RequestUrlConverter extends WebSocketEventConverter { + @Override + public String convert(WebsocketEvent event) { + return + event.getMethod() + + WebSocketEventConverter.SPACE_CHAR + + event.getPath() + + WebSocketEventConverter.SPACE_CHAR + + event.getProtocol(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/StatusCodeConverter.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/StatusCodeConverter.java new file mode 100644 index 000000000..903f923ac --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/StatusCodeConverter.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging.layout.converters; + +import org.whispersystems.websocket.logging.WebsocketEvent; + +public class StatusCodeConverter extends WebSocketEventConverter { + @Override + public String convert(WebsocketEvent event) { + if (event.getStatusCode() == WebsocketEvent.SENTINEL) { + return WebsocketEvent.NA; + } else { + return Integer.toString(event.getStatusCode()); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/WebSocketEventConverter.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/WebSocketEventConverter.java new file mode 100644 index 000000000..979600185 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/logging/layout/converters/WebSocketEventConverter.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging.layout.converters; + +import org.whispersystems.websocket.logging.WebsocketEvent; + +import ch.qos.logback.core.Context; +import ch.qos.logback.core.pattern.DynamicConverter; +import ch.qos.logback.core.spi.ContextAware; +import ch.qos.logback.core.spi.ContextAwareBase; +import ch.qos.logback.core.status.Status; + +public abstract class WebSocketEventConverter extends DynamicConverter implements ContextAware { + + public final static char SPACE_CHAR = ' '; + public final static char QUESTION_CHAR = '?'; + + ContextAwareBase cab = new ContextAwareBase(); + + @Override + public void setContext(Context context) { + cab.setContext(context); + } + + @Override + public Context getContext() { + return cab.getContext(); + } + + @Override + public void addStatus(Status status) { + cab.addStatus(status); + } + + @Override + public void addInfo(String msg) { + cab.addInfo(msg); + } + + @Override + public void addInfo(String msg, Throwable ex) { + cab.addInfo(msg, ex); + } + + @Override + public void addWarn(String msg) { + cab.addWarn(msg); + } + + @Override + public void addWarn(String msg, Throwable ex) { + cab.addWarn(msg, ex); + } + + @Override + public void addError(String msg) { + cab.addError(msg); + } + + @Override + public void addError(String msg, Throwable ex) { + cab.addError(msg, ex); + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/InvalidMessageException.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/InvalidMessageException.java new file mode 100644 index 000000000..027634ff4 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/InvalidMessageException.java @@ -0,0 +1,15 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.messages; + +public class InvalidMessageException extends Exception { + public InvalidMessageException(String s) { + super(s); + } + + public InvalidMessageException(Exception e) { + super(e); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/WebSocketMessage.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/WebSocketMessage.java new file mode 100644 index 000000000..4cf41f89f --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/WebSocketMessage.java @@ -0,0 +1,20 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.messages; + +public interface WebSocketMessage { + + public enum Type { + UNKNOWN_MESSAGE, + REQUEST_MESSAGE, + RESPONSE_MESSAGE + } + + public Type getType(); + public WebSocketRequestMessage getRequestMessage(); + public WebSocketResponseMessage getResponseMessage(); + public byte[] toByteArray(); + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/WebSocketMessageFactory.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/WebSocketMessageFactory.java new file mode 100644 index 000000000..d24a7e09b --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/WebSocketMessageFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.messages; + + +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public interface WebSocketMessageFactory { + + public WebSocketMessage parseMessage(byte[] serialized, int offset, int len) + throws InvalidMessageException; + + public WebSocketMessage createRequest(Optional requestId, + String verb, String path, + List headers, + Optional body); + + public WebSocketMessage createResponse(long requestId, int status, String message, + List headers, + Optional body); + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/WebSocketRequestMessage.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/WebSocketRequestMessage.java new file mode 100644 index 000000000..c5cd94483 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/WebSocketRequestMessage.java @@ -0,0 +1,19 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.messages; + +import java.util.Map; +import java.util.Optional; + +public interface WebSocketRequestMessage { + + public String getVerb(); + public String getPath(); + public Map getHeaders(); + public Optional getBody(); + public long getRequestId(); + public boolean hasRequestId(); + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/WebSocketResponseMessage.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/WebSocketResponseMessage.java new file mode 100644 index 000000000..38e6bacfa --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/WebSocketResponseMessage.java @@ -0,0 +1,17 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.messages; + + +import java.util.Map; +import java.util.Optional; + +public interface WebSocketResponseMessage { + public long getRequestId(); + public int getStatus(); + public String getMessage(); + public Map getHeaders(); + public Optional getBody(); +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketMessage.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketMessage.java new file mode 100644 index 000000000..909673363 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketMessage.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.messages.protobuf; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import org.whispersystems.websocket.messages.InvalidMessageException; +import org.whispersystems.websocket.messages.WebSocketMessage; +import org.whispersystems.websocket.messages.WebSocketRequestMessage; +import org.whispersystems.websocket.messages.WebSocketResponseMessage; + +public class ProtobufWebSocketMessage implements WebSocketMessage { + + private final SubProtocol.WebSocketMessage message; + + ProtobufWebSocketMessage(byte[] buffer, int offset, int length) throws InvalidMessageException { + try { + this.message = SubProtocol.WebSocketMessage.parseFrom(ByteString.copyFrom(buffer, offset, length)); + + if (getType() == Type.REQUEST_MESSAGE) { + if (!message.getRequest().hasVerb() || !message.getRequest().hasPath()) { + throw new InvalidMessageException("Missing required request attributes!"); + } + } else if (getType() == Type.RESPONSE_MESSAGE) { + if (!message.getResponse().hasId() || !message.getResponse().hasStatus() || !message.getResponse().hasMessage()) { + throw new InvalidMessageException("Missing required response attributes!"); + } + } + } catch (InvalidProtocolBufferException e) { + throw new InvalidMessageException(e); + } + } + + ProtobufWebSocketMessage(SubProtocol.WebSocketMessage message) { + this.message = message; + } + + @Override + public Type getType() { + if (message.getType().getNumber() == SubProtocol.WebSocketMessage.Type.REQUEST_VALUE && + message.hasRequest()) + { + return Type.REQUEST_MESSAGE; + } else if (message.getType().getNumber() == SubProtocol.WebSocketMessage.Type.RESPONSE_VALUE && + message.hasResponse()) + { + return Type.RESPONSE_MESSAGE; + } else { + return Type.UNKNOWN_MESSAGE; + } + } + + @Override + public WebSocketRequestMessage getRequestMessage() { + return new ProtobufWebSocketRequestMessage(message.getRequest()); + } + + @Override + public WebSocketResponseMessage getResponseMessage() { + return new ProtobufWebSocketResponseMessage(message.getResponse()); + } + + @Override + public byte[] toByteArray() { + return message.toByteArray(); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketMessageFactory.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketMessageFactory.java new file mode 100644 index 000000000..d4f25bd89 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketMessageFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.messages.protobuf; + +import com.google.protobuf.ByteString; +import org.whispersystems.websocket.messages.InvalidMessageException; +import org.whispersystems.websocket.messages.WebSocketMessage; +import org.whispersystems.websocket.messages.WebSocketMessageFactory; + +import java.util.List; +import java.util.Optional; + +public class ProtobufWebSocketMessageFactory implements WebSocketMessageFactory { + + @Override + public WebSocketMessage parseMessage(byte[] serialized, int offset, int len) + throws InvalidMessageException + { + return new ProtobufWebSocketMessage(serialized, offset, len); + } + + @Override + public WebSocketMessage createRequest(Optional requestId, + String verb, String path, + List headers, + Optional body) + { + SubProtocol.WebSocketRequestMessage.Builder requestMessage = + SubProtocol.WebSocketRequestMessage.newBuilder() + .setVerb(verb) + .setPath(path); + + if (requestId.isPresent()) { + requestMessage.setId(requestId.get()); + } + + if (body.isPresent()) { + requestMessage.setBody(ByteString.copyFrom(body.get())); + } + + if (headers != null) { + requestMessage.addAllHeaders(headers); + } + + SubProtocol.WebSocketMessage message + = SubProtocol.WebSocketMessage.newBuilder() + .setType(SubProtocol.WebSocketMessage.Type.REQUEST) + .setRequest(requestMessage) + .build(); + + return new ProtobufWebSocketMessage(message); + } + + @Override + public WebSocketMessage createResponse(long requestId, int status, String messageString, List headers, Optional body) { + SubProtocol.WebSocketResponseMessage.Builder responseMessage = + SubProtocol.WebSocketResponseMessage.newBuilder() + .setId(requestId) + .setStatus(status) + .setMessage(messageString); + + if (body.isPresent()) { + responseMessage.setBody(ByteString.copyFrom(body.get())); + } + + if (headers != null) { + responseMessage.addAllHeaders(headers); + } + + SubProtocol.WebSocketMessage message = + SubProtocol.WebSocketMessage.newBuilder() + .setType(SubProtocol.WebSocketMessage.Type.RESPONSE) + .setResponse(responseMessage) + .build(); + + return new ProtobufWebSocketMessage(message); + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketRequestMessage.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketRequestMessage.java new file mode 100644 index 000000000..d1233f465 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketRequestMessage.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.messages.protobuf; + +import org.whispersystems.websocket.messages.WebSocketRequestMessage; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class ProtobufWebSocketRequestMessage implements WebSocketRequestMessage { + + private final SubProtocol.WebSocketRequestMessage message; + + ProtobufWebSocketRequestMessage(SubProtocol.WebSocketRequestMessage message) { + this.message = message; + } + + @Override + public String getVerb() { + return message.getVerb(); + } + + @Override + public String getPath() { + return message.getPath(); + } + + @Override + public Optional getBody() { + if (message.hasBody()) { + return Optional.of(message.getBody().toByteArray()); + } else { + return Optional.empty(); + } + } + + @Override + public long getRequestId() { + return message.getId(); + } + + @Override + public boolean hasRequestId() { + return message.hasId(); + } + + @Override + public Map getHeaders() { + Map results = new HashMap<>(); + + for (String header : message.getHeadersList()) { + String[] tokenized = header.split(":"); + + if (tokenized.length == 2 && tokenized[0] != null && tokenized[1] != null) { + results.put(tokenized[0].trim().toLowerCase(), tokenized[1].trim()); + } + } + + return results; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketResponseMessage.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketResponseMessage.java new file mode 100644 index 000000000..62981772a --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/messages/protobuf/ProtobufWebSocketResponseMessage.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.messages.protobuf; + +import org.whispersystems.websocket.messages.WebSocketResponseMessage; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class ProtobufWebSocketResponseMessage implements WebSocketResponseMessage { + + private final SubProtocol.WebSocketResponseMessage message; + + public ProtobufWebSocketResponseMessage(SubProtocol.WebSocketResponseMessage message) { + this.message = message; + } + + @Override + public long getRequestId() { + return message.getId(); + } + + @Override + public int getStatus() { + return message.getStatus(); + } + + @Override + public String getMessage() { + return message.getMessage(); + } + + @Override + public Optional getBody() { + if (message.hasBody()) { + return Optional.of(message.getBody().toByteArray()); + } else { + return Optional.empty(); + } + } + + @Override + public Map getHeaders() { + Map results = new HashMap<>(); + + for (String header : message.getHeadersList()) { + String[] tokenized = header.split(":"); + + if (tokenized.length == 2 && tokenized[0] != null && tokenized[1] != null) { + results.put(tokenized[0].trim().toLowerCase(), tokenized[1].trim()); + } + } + + return results; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/ContextPrincipal.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/ContextPrincipal.java new file mode 100644 index 000000000..6ab381c30 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/ContextPrincipal.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.session; + +import java.security.Principal; + +public class ContextPrincipal implements Principal { + + private final WebSocketSessionContext context; + + public ContextPrincipal(WebSocketSessionContext context) { + this.context = context; + } + + @Override + public boolean equals(Object another) { + return another instanceof ContextPrincipal && + context.equals(((ContextPrincipal) another).context); + } + + @Override + public String toString() { + return super.toString(); + } + + @Override + public int hashCode() { + return context.hashCode(); + } + + @Override + public String getName() { + return "WebSocketSessionContext"; + } + + public WebSocketSessionContext getContext() { + return context; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/WebSocketSession.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/WebSocketSession.java new file mode 100644 index 000000000..5548d78e1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/WebSocketSession.java @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.session; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) +public @interface WebSocketSession { +} + diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/WebSocketSessionContainerRequestValueFactory.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/WebSocketSessionContainerRequestValueFactory.java new file mode 100644 index 000000000..b57ad4ed1 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/WebSocketSessionContainerRequestValueFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.session; + +import org.glassfish.jersey.server.ContainerRequest; +import org.whispersystems.websocket.WebSocketSecurityContext; + +import javax.ws.rs.core.SecurityContext; + +public class WebSocketSessionContainerRequestValueFactory { + private final ContainerRequest request; + + public WebSocketSessionContainerRequestValueFactory(ContainerRequest request) { + this.request = request; + } + + public WebSocketSessionContext provide() { + SecurityContext securityContext = request.getSecurityContext(); + + if (!(securityContext instanceof WebSocketSecurityContext)) { + throw new IllegalStateException("Security context isn't for websocket!"); + } + + WebSocketSessionContext sessionContext = ((WebSocketSecurityContext)securityContext).getSessionContext(); + + if (sessionContext == null) { + throw new IllegalStateException("No session context found for websocket!"); + } + + return sessionContext; + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/WebSocketSessionContext.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/WebSocketSessionContext.java new file mode 100644 index 000000000..a83b41075 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/WebSocketSessionContext.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.session; + +import java.util.LinkedList; +import java.util.List; +import javax.annotation.Nullable; +import org.whispersystems.websocket.WebSocketClient; + +public class WebSocketSessionContext { + + private final List closeListeners = new LinkedList<>(); + + private final WebSocketClient webSocketClient; + + private Object authenticated; + private boolean closed; + + public WebSocketSessionContext(WebSocketClient webSocketClient) { + this.webSocketClient = webSocketClient; + } + + public void setAuthenticated(Object authenticated) { + this.authenticated = authenticated; + } + + public T getAuthenticated(Class clazz) { + if (authenticated != null && clazz.equals(authenticated.getClass())) { + return clazz.cast(authenticated); + } + + throw new IllegalArgumentException("No authenticated type for: " + clazz + ", we have: " + authenticated); + } + + @Nullable + public Object getAuthenticated() { + return authenticated; + } + + public synchronized void addWebsocketClosedListener(WebSocketEventListener listener) { + if (!closed) this.closeListeners.add(listener); + else listener.onWebSocketClose(this, 1000, "Closed"); + } + + public WebSocketClient getClient() { + return webSocketClient; + } + + public synchronized void notifyClosed(int statusCode, String reason) { + for (WebSocketEventListener listener : closeListeners) { + listener.onWebSocketClose(this, statusCode, reason); + } + + closed = true; + } + + public interface WebSocketEventListener { + public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason); + } + + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/WebSocketSessionContextValueFactoryProvider.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/WebSocketSessionContextValueFactoryProvider.java new file mode 100644 index 000000000..511434524 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/session/WebSocketSessionContextValueFactoryProvider.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.session; + +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.internal.inject.AbstractValueParamProvider; +import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; +import org.glassfish.jersey.server.model.Parameter; +import org.glassfish.jersey.server.spi.internal.ValueParamProvider; + +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.function.Function; + + +@Singleton +public class WebSocketSessionContextValueFactoryProvider extends AbstractValueParamProvider { + + @Inject + public WebSocketSessionContextValueFactoryProvider(MultivaluedParameterExtractorProvider mpep) { + super(() -> mpep, Parameter.Source.UNKNOWN); + } + + @Nullable + @Override + protected Function createValueProvider(Parameter parameter) { + if (!parameter.isAnnotationPresent(WebSocketSession.class)) { + return null; + } else if (WebSocketSessionContext.class.equals(parameter.getRawType())) { + return request -> new WebSocketSessionContainerRequestValueFactory(request).provide(); + } else { + throw new IllegalArgumentException("Can't inject custom type"); + } + } + + public static class Binder extends AbstractBinder { + + public Binder() { } + + @Override + protected void configure() { + bind(WebSocketSessionContextValueFactoryProvider.class).to(ValueParamProvider.class).in(Singleton.class); + } + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/setup/WebSocketConnectListener.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/setup/WebSocketConnectListener.java new file mode 100644 index 000000000..be6f043de --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/setup/WebSocketConnectListener.java @@ -0,0 +1,11 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.setup; + +import org.whispersystems.websocket.session.WebSocketSessionContext; + +public interface WebSocketConnectListener { + public void onWebSocketConnect(WebSocketSessionContext context); +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/setup/WebSocketEnvironment.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/setup/WebSocketEnvironment.java new file mode 100644 index 000000000..64413b383 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/java/org/whispersystems/websocket/setup/WebSocketEnvironment.java @@ -0,0 +1,92 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.setup; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.setup.Environment; +import org.glassfish.jersey.server.ResourceConfig; +import org.whispersystems.websocket.auth.WebSocketAuthenticator; +import org.whispersystems.websocket.configuration.WebSocketConfiguration; +import org.whispersystems.websocket.logging.WebsocketRequestLog; +import org.whispersystems.websocket.messages.WebSocketMessageFactory; +import org.whispersystems.websocket.messages.protobuf.ProtobufWebSocketMessageFactory; + +import javax.validation.Validator; +import java.security.Principal; + +public class WebSocketEnvironment { + + private final ResourceConfig jerseyConfig; + private final ObjectMapper objectMapper; + private final Validator validator; + private final WebsocketRequestLog requestLog; + private final long idleTimeoutMillis; + + private WebSocketAuthenticator authenticator; + private WebSocketMessageFactory messageFactory; + private WebSocketConnectListener connectListener; + + public WebSocketEnvironment(Environment environment, WebSocketConfiguration configuration) { + this(environment, configuration, 60000); + } + + public WebSocketEnvironment(Environment environment, WebSocketConfiguration configuration, long idleTimeoutMillis) { + this(environment, configuration.getRequestLog().build("websocket"), idleTimeoutMillis); + } + + public WebSocketEnvironment(Environment environment, WebsocketRequestLog requestLog, long idleTimeoutMillis) { + this.jerseyConfig = new DropwizardResourceConfig(environment.metrics()); + this.objectMapper = environment.getObjectMapper(); + this.validator = environment.getValidator(); + this.requestLog = requestLog; + this.messageFactory = new ProtobufWebSocketMessageFactory(); + this.idleTimeoutMillis = idleTimeoutMillis; + } + + public ResourceConfig jersey() { + return jerseyConfig; + } + + public WebSocketAuthenticator getAuthenticator() { + return authenticator; + } + + public void setAuthenticator(WebSocketAuthenticator authenticator) { + this.authenticator = authenticator; + } + + public long getIdleTimeoutMillis() { + return idleTimeoutMillis; + } + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + public WebsocketRequestLog getRequestLog() { + return requestLog; + } + + public Validator getValidator() { + return validator; + } + + public WebSocketMessageFactory getMessageFactory() { + return messageFactory; + } + + public void setMessageFactory(WebSocketMessageFactory messageFactory) { + this.messageFactory = messageFactory; + } + + public WebSocketConnectListener getConnectListener() { + return connectListener; + } + + public void setConnectListener(WebSocketConnectListener connectListener) { + this.connectListener = connectListener; + } +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/proto/WebSocketProtocol.proto b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/proto/WebSocketProtocol.proto new file mode 100644 index 000000000..cc28f8925 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/main/proto/WebSocketProtocol.proto @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +syntax = "proto2"; + +package signalservice; + +option java_package = "org.whispersystems.websocket.messages.protobuf"; +option java_outer_classname = "SubProtocol"; + +message WebSocketRequestMessage { + optional string verb = 1; + optional string path = 2; + optional bytes body = 3; + repeated string headers = 5; + optional uint64 id = 4; +} + +message WebSocketResponseMessage { + optional uint64 id = 1; + optional uint32 status = 2; + optional string message = 3; + repeated string headers = 5; + optional bytes body = 4; +} + +message WebSocketMessage { + enum Type { + UNKNOWN = 0; + REQUEST = 1; + RESPONSE = 2; + } + + optional Type type = 1; + optional WebSocketRequestMessage request = 2; + optional WebSocketResponseMessage response = 3; +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/test/java/org/whispersystems/websocket/WebSocketResourceProviderFactoryTest.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/test/java/org/whispersystems/websocket/WebSocketResourceProviderFactoryTest.java new file mode 100644 index 000000000..33baf6e29 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/test/java/org/whispersystems/websocket/WebSocketResourceProviderFactoryTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import io.dropwizard.jersey.DropwizardResourceConfig; +import java.io.IOException; +import java.security.Principal; +import java.util.Optional; +import javax.security.auth.Subject; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.eclipse.jetty.websocket.api.WebSocketPolicy; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.whispersystems.websocket.auth.AuthenticationException; +import org.whispersystems.websocket.auth.WebSocketAuthenticator; +import org.whispersystems.websocket.configuration.WebSocketConfiguration; +import org.whispersystems.websocket.setup.WebSocketEnvironment; + +public class WebSocketResourceProviderFactoryTest { + + private ResourceConfig jerseyEnvironment; + private WebSocketEnvironment environment; + private WebSocketAuthenticator authenticator; + private ServletUpgradeRequest request; + private ServletUpgradeResponse response; + + @BeforeEach + void setup() { + jerseyEnvironment = new DropwizardResourceConfig(); + //noinspection unchecked + environment = mock(WebSocketEnvironment.class); + //noinspection unchecked + authenticator = mock(WebSocketAuthenticator.class); + request = mock(ServletUpgradeRequest.class); + response = mock(ServletUpgradeResponse.class); + + } + + @Test + void testUnauthorized() throws AuthenticationException, IOException { + when(environment.getAuthenticator()).thenReturn(authenticator); + when(authenticator.authenticate(eq(request))).thenReturn( + new WebSocketAuthenticator.AuthenticationResult<>(Optional.empty(), true)); + when(environment.jersey()).thenReturn(jerseyEnvironment); + + WebSocketResourceProviderFactory factory = new WebSocketResourceProviderFactory<>(environment, Account.class, + mock(WebSocketConfiguration.class)); + Object connection = factory.createWebSocket(request, response); + + assertNull(connection); + verify(response).sendForbidden(eq("Unauthorized")); + verify(authenticator).authenticate(eq(request)); + } + + @Test + void testValidAuthorization() throws AuthenticationException { + Session session = mock(Session.class); + Account account = new Account(); + + when(environment.getAuthenticator()).thenReturn(authenticator); + when(authenticator.authenticate(eq(request))).thenReturn( + new WebSocketAuthenticator.AuthenticationResult<>(Optional.of(account), true)); + when(environment.jersey()).thenReturn(jerseyEnvironment); + when(session.getUpgradeRequest()).thenReturn(mock(UpgradeRequest.class)); + + WebSocketResourceProviderFactory factory = new WebSocketResourceProviderFactory<>(environment, Account.class, + mock(WebSocketConfiguration.class)); + Object connection = factory.createWebSocket(request, response); + + assertNotNull(connection); + verifyNoMoreInteractions(response); + verify(authenticator).authenticate(eq(request)); + + ((WebSocketResourceProvider) connection).onWebSocketConnect(session); + + assertNotNull(((WebSocketResourceProvider) connection).getContext().getAuthenticated()); + assertEquals(((WebSocketResourceProvider) connection).getContext().getAuthenticated(), account); + } + + @Test + void testErrorAuthorization() throws AuthenticationException, IOException { + when(environment.getAuthenticator()).thenReturn(authenticator); + when(authenticator.authenticate(eq(request))).thenThrow(new AuthenticationException("database failure")); + when(environment.jersey()).thenReturn(jerseyEnvironment); + + WebSocketResourceProviderFactory factory = new WebSocketResourceProviderFactory<>(environment, + Account.class, + mock(WebSocketConfiguration.class)); + Object connection = factory.createWebSocket(request, response); + + assertNull(connection); + verify(response).sendError(eq(500), eq("Failure")); + verify(authenticator).authenticate(eq(request)); + } + + @Test + void testConfigure() { + WebSocketServletFactory servletFactory = mock(WebSocketServletFactory.class); + when(environment.jersey()).thenReturn(jerseyEnvironment); + when(servletFactory.getPolicy()).thenReturn(mock(WebSocketPolicy.class)); + + WebSocketResourceProviderFactory factory = new WebSocketResourceProviderFactory<>(environment, + Account.class, + mock(WebSocketConfiguration.class)); + factory.configure(servletFactory); + + verify(servletFactory).setCreator(eq(factory)); + } + + + private static class Account implements Principal { + @Override + public String getName() { + return null; + } + + @Override + public boolean implies(Subject subject) { + return false; + } + } + + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/test/java/org/whispersystems/websocket/WebSocketResourceProviderTest.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/test/java/org/whispersystems/websocket/WebSocketResourceProviderTest.java new file mode 100644 index 000000000..80557ae17 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/test/java/org/whispersystems/websocket/WebSocketResourceProviderTest.java @@ -0,0 +1,809 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.net.HttpHeaders; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import io.dropwizard.auth.Auth; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.security.Principal; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotEmpty; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import org.eclipse.jetty.websocket.api.CloseStatus; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.eclipse.jetty.websocket.api.WriteCallback; +import org.glassfish.jersey.server.ApplicationHandler; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; +import org.whispersystems.websocket.auth.WebsocketAuthValueFactoryProvider; +import org.whispersystems.websocket.logging.WebsocketRequestLog; +import org.whispersystems.websocket.messages.protobuf.ProtobufWebSocketMessageFactory; +import org.whispersystems.websocket.messages.protobuf.SubProtocol; +import org.whispersystems.websocket.session.WebSocketSession; +import org.whispersystems.websocket.session.WebSocketSessionContext; +import org.whispersystems.websocket.session.WebSocketSessionContextValueFactoryProvider; +import org.whispersystems.websocket.setup.WebSocketConnectListener; + +class WebSocketResourceProviderTest { + + @Test + void testOnConnect() { + ApplicationHandler applicationHandler = mock(ApplicationHandler.class); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketConnectListener connectListener = mock(WebSocketConnectListener.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", + applicationHandler, requestLog, + new TestPrincipal("fooz"), + new ProtobufWebSocketMessageFactory(), + Optional.of(connectListener), + 30000); + + Session session = mock(Session.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + + provider.onWebSocketConnect(session); + + verify(session, never()).close(anyInt(), anyString()); + verify(session, never()).close(); + verify(session, never()).close(any(CloseStatus.class)); + + ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass( + WebSocketSessionContext.class); + verify(connectListener).onWebSocketConnect(contextArgumentCaptor.capture()); + + assertThat(contextArgumentCaptor.getValue().getAuthenticated(TestPrincipal.class).getName()).isEqualTo("fooz"); + } + + @Test + void testMockedRouteMessageSuccess() throws Exception { + ApplicationHandler applicationHandler = mock(ApplicationHandler.class); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, new TestPrincipal("foo"), new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); + + Session session = mock(Session.class); + RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + + ContainerResponse response = mock(ContainerResponse.class); + when(response.getStatus()).thenReturn(200); + when(response.getStatusInfo()).thenReturn(new Response.StatusType() { + @Override + public int getStatusCode() { + return 200; + } + + @Override + public Response.Status.Family getFamily() { + return Response.Status.Family.SUCCESSFUL; + } + + @Override + public String getReasonPhrase() { + return "OK"; + } + }); + + ArgumentCaptor responseOutputStream = ArgumentCaptor.forClass(OutputStream.class); + + when(applicationHandler.apply(any(ContainerRequest.class), responseOutputStream.capture())) + .thenAnswer((Answer>) invocation -> { + responseOutputStream.getValue().write("hello world!".getBytes()); + return CompletableFuture.completedFuture(response); + }); + + provider.onWebSocketConnect(session); + + verify(session, never()).close(anyInt(), anyString()); + verify(session, never()).close(); + verify(session, never()).close(any(CloseStatus.class)); + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/bar", + new LinkedList<>(), Optional.of("hello world!".getBytes())).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ContainerRequest.class); + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(applicationHandler).apply(requestCaptor.capture(), any(OutputStream.class)); + + ContainerRequest bundledRequest = requestCaptor.getValue(); + + assertThat(bundledRequest.getRequest().getMethod()).isEqualTo("GET"); + assertThat(bundledRequest.getBaseUri().toString()).isEqualTo("/"); + assertThat(bundledRequest.getPath(false)).isEqualTo("bar"); + + verify(requestLog).log(eq("127.0.0.1"), eq(bundledRequest), eq(response)); + verify(remoteEndpoint).sendBytesByFuture(responseCaptor.capture()); + + SubProtocol.WebSocketMessage responseMessageContainer = SubProtocol.WebSocketMessage.parseFrom( + responseCaptor.getValue().array()); + assertThat(responseMessageContainer.getResponse().getId()).isEqualTo(111L); + assertThat(responseMessageContainer.getResponse().getStatus()).isEqualTo(200); + assertThat(responseMessageContainer.getResponse().getMessage()).isEqualTo("OK"); + assertThat(responseMessageContainer.getResponse().getBody()).isEqualTo( + ByteString.copyFrom("hello world!".getBytes())); + } + + @Test + void testMockedRouteMessageFailure() throws Exception { + ApplicationHandler applicationHandler = mock(ApplicationHandler.class); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, new TestPrincipal("foo"), new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); + + Session session = mock(Session.class); + RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + + when(applicationHandler.apply(any(ContainerRequest.class), any(OutputStream.class))).thenReturn( + CompletableFuture.failedFuture(new IllegalStateException("foo"))); + + provider.onWebSocketConnect(session); + + verify(session, never()).close(anyInt(), anyString()); + verify(session, never()).close(); + verify(session, never()).close(any(CloseStatus.class)); + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/bar", + new LinkedList<>(), Optional.of("hello world!".getBytes())).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ContainerRequest.class); + + verify(applicationHandler).apply(requestCaptor.capture(), any(OutputStream.class)); + + ContainerRequest bundledRequest = requestCaptor.getValue(); + + assertThat(bundledRequest.getRequest().getMethod()).isEqualTo("GET"); + assertThat(bundledRequest.getBaseUri().toString()).isEqualTo("/"); + assertThat(bundledRequest.getPath(false)).isEqualTo("bar"); + + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(remoteEndpoint).sendBytesByFuture(responseCaptor.capture()); + + SubProtocol.WebSocketMessage responseMessageContainer = SubProtocol.WebSocketMessage.parseFrom( + responseCaptor.getValue().array()); + assertThat(responseMessageContainer.getResponse().getStatus()).isEqualTo(500); + assertThat(responseMessageContainer.getResponse().getMessage()).isEqualTo("Error response"); + assertThat(responseMessageContainer.getResponse().hasBody()).isFalse(); + } + + @Test + void testActualRouteMessageSuccess() throws InvalidProtocolBufferException { + ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(new TestResource()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(new ObjectMapper())); + + ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, new TestPrincipal("foo"), new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); + + Session session = mock(Session.class); + RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + + provider.onWebSocketConnect(session); + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/hello", + new LinkedList<>(), Optional.empty()).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + ArgumentCaptor responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(remoteEndpoint).sendBytesByFuture(responseBytesCaptor.capture()); + + SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor); + + assertThat(response.getId()).isEqualTo(111L); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getMessage()).isEqualTo("OK"); + assertThat(response.getBody()).isEqualTo(ByteString.copyFrom("Hello!".getBytes())); + } + + @Test + void testActualRouteMessageNotFound() throws InvalidProtocolBufferException { + ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(new TestResource()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(new ObjectMapper())); + + ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, new TestPrincipal("foo"), new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); + + Session session = mock(Session.class); + RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + + provider.onWebSocketConnect(session); + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", + "/v1/test/doesntexist", new LinkedList<>(), Optional.empty()).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + ArgumentCaptor responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(remoteEndpoint).sendBytesByFuture(responseBytesCaptor.capture()); + + SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor); + + assertThat(response.getId()).isEqualTo(111L); + assertThat(response.getStatus()).isEqualTo(404); + assertThat(response.getMessage()).isEqualTo("Not Found"); + assertThat(response.hasBody()).isFalse(); + } + + @Test + void testActualRouteMessageAuthorized() throws InvalidProtocolBufferException { + ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(new TestResource()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(new ObjectMapper())); + + ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, new TestPrincipal("authorizedUserName"), new ProtobufWebSocketMessageFactory(), Optional.empty(), + 30000); + + Session session = mock(Session.class); + RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + + provider.onWebSocketConnect(session); + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/world", + new LinkedList<>(), Optional.empty()).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + ArgumentCaptor responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(remoteEndpoint).sendBytesByFuture(responseBytesCaptor.capture()); + + SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor); + + assertThat(response.getId()).isEqualTo(111L); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getMessage()).isEqualTo("OK"); + assertThat(response.getBody().toStringUtf8()).isEqualTo("World: authorizedUserName"); + } + + @Test + void testActualRouteMessageUnauthorized() throws InvalidProtocolBufferException { + ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(new TestResource()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(new ObjectMapper())); + + ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, null, new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); + + Session session = mock(Session.class); + RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + + provider.onWebSocketConnect(session); + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/world", + new LinkedList<>(), Optional.empty()).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + ArgumentCaptor responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(remoteEndpoint).sendBytesByFuture(responseBytesCaptor.capture()); + + SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor); + + assertThat(response.getId()).isEqualTo(111L); + assertThat(response.getStatus()).isEqualTo(401); + assertThat(response.hasBody()).isFalse(); + } + + @Test + void testActualRouteMessageOptionalAuthorizedPresent() throws InvalidProtocolBufferException { + ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(new TestResource()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(new ObjectMapper())); + + ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, new TestPrincipal("something"), new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); + + Session session = mock(Session.class); + RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + + provider.onWebSocketConnect(session); + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/optional", + new LinkedList<>(), Optional.empty()).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + ArgumentCaptor responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(remoteEndpoint).sendBytesByFuture(responseBytesCaptor.capture()); + + SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor); + + assertThat(response.getId()).isEqualTo(111L); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getMessage()).isEqualTo("OK"); + assertThat(response.getBody().toStringUtf8()).isEqualTo("World: something"); + } + + @Test + void testActualRouteMessageOptionalAuthorizedEmpty() throws InvalidProtocolBufferException { + ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(new TestResource()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(new ObjectMapper())); + + ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, null, new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); + + Session session = mock(Session.class); + RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + + provider.onWebSocketConnect(session); + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/optional", + new LinkedList<>(), Optional.empty()).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + ArgumentCaptor responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(remoteEndpoint).sendBytesByFuture(responseBytesCaptor.capture()); + + SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor); + + assertThat(response.getId()).isEqualTo(111L); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getMessage()).isEqualTo("OK"); + assertThat(response.getBody().toStringUtf8()).isEqualTo("Empty world"); + } + + @Test + void testActualRouteMessagePutAuthenticatedEntity() throws InvalidProtocolBufferException, JsonProcessingException { + ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(new TestResource()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(new ObjectMapper())); + + ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, new TestPrincipal("gooduser"), new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); + + Session session = mock(Session.class); + RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + + provider.onWebSocketConnect(session); + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "PUT", + "/v1/test/some/testparam", List.of("Content-Type: application/json"), + Optional.of(new ObjectMapper().writeValueAsBytes(new TestResource.TestEntity("mykey", 1001)))).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + ArgumentCaptor responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(remoteEndpoint).sendBytesByFuture(responseBytesCaptor.capture()); + + SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor); + + assertThat(response.getId()).isEqualTo(111L); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getMessage()).isEqualTo("OK"); + assertThat(response.getBody().toStringUtf8()).isEqualTo("gooduser:testparam:mykey:1001"); + } + + @Test + void testActualRouteMessagePutAuthenticatedBadEntity() + throws InvalidProtocolBufferException, JsonProcessingException { + ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(new TestResource()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(new ObjectMapper())); + + ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, new TestPrincipal("gooduser"), new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); + + Session session = mock(Session.class); + RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + + provider.onWebSocketConnect(session); + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "PUT", + "/v1/test/some/testparam", List.of("Content-Type: application/json"), + Optional.of(new ObjectMapper().writeValueAsBytes(new TestResource.TestEntity("mykey", 5)))).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + ArgumentCaptor responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(remoteEndpoint).sendBytesByFuture(responseBytesCaptor.capture()); + + SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor); + + assertThat(response.getId()).isEqualTo(111L); + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.getMessage()).isEqualTo("Bad Request"); + assertThat(response.hasBody()).isFalse(); + } + + @Test + void testActualRouteMessageExceptionMapping() throws InvalidProtocolBufferException { + ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(new TestResource()); + resourceConfig.register(new TestExceptionMapper()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(new ObjectMapper())); + + ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, new TestPrincipal("gooduser"), new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); + + Session session = mock(Session.class); + RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + + provider.onWebSocketConnect(session); + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", + "/v1/test/exception/map", List.of("Content-Type: application/json"), Optional.empty()).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + ArgumentCaptor responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(remoteEndpoint).sendBytesByFuture(responseBytesCaptor.capture()); + + SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor); + + assertThat(response.getId()).isEqualTo(111L); + assertThat(response.getStatus()).isEqualTo(1337); + assertThat(response.hasBody()).isFalse(); + } + + @Test + void testActualRouteSessionContextInjection() throws InvalidProtocolBufferException { + ResourceConfig resourceConfig = new DropwizardResourceConfig(); + resourceConfig.register(new TestResource()); + resourceConfig.register(new TestExceptionMapper()); + resourceConfig.register(new WebSocketSessionContextValueFactoryProvider.Binder()); + resourceConfig.register(new WebsocketAuthValueFactoryProvider.Binder<>(TestPrincipal.class)); + resourceConfig.register(new JacksonMessageBodyProvider(new ObjectMapper())); + + ApplicationHandler applicationHandler = new ApplicationHandler(resourceConfig); + WebsocketRequestLog requestLog = mock(WebsocketRequestLog.class); + WebSocketResourceProvider provider = new WebSocketResourceProvider<>("127.0.0.1", applicationHandler, + requestLog, new TestPrincipal("gooduser"), new ProtobufWebSocketMessageFactory(), Optional.empty(), 30000); + + Session session = mock(Session.class); + RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class); + UpgradeRequest request = mock(UpgradeRequest.class); + + when(session.getUpgradeRequest()).thenReturn(request); + when(session.getRemote()).thenReturn(remoteEndpoint); + + provider.onWebSocketConnect(session); + + byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/keepalive", + new LinkedList<>(), Optional.empty()).toByteArray(); + + provider.onWebSocketBinary(message, 0, message.length); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(remoteEndpoint).sendBytes(requestCaptor.capture(), any(WriteCallback.class)); + + SubProtocol.WebSocketRequestMessage requestMessage = getRequest(requestCaptor); + assertThat(requestMessage.getVerb()).isEqualTo("GET"); + assertThat(requestMessage.getPath()).isEqualTo("/v1/miccheck"); + assertThat(requestMessage.getBody().toStringUtf8()).isEqualTo("smert ze smert"); + + byte[] clientResponse = new ProtobufWebSocketMessageFactory().createResponse(requestMessage.getId(), 200, "OK", + new LinkedList<>(), Optional.of("my response".getBytes())).toByteArray(); + + provider.onWebSocketBinary(clientResponse, 0, clientResponse.length); + + ArgumentCaptor responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class); + + verify(remoteEndpoint).sendBytesByFuture(responseBytesCaptor.capture()); + + SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor); + + assertThat(response.getId()).isEqualTo(111L); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getMessage()).isEqualTo("OK"); + assertThat(response.getBody().toStringUtf8()).isEqualTo("my response"); + } + + @Test + void testGetHeaderList() { + assertThat(WebSocketResourceProvider.getHeaderList(new MultivaluedHashMap<>())).isEmpty(); + + { + final MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.put("test", Arrays.asList("a", "b", "c")); + + final List headerStrings = WebSocketResourceProvider.getHeaderList(headers); + + assertThat(headerStrings).hasSize(1); + assertThat(headerStrings).contains("test:a"); + } + } + + @Test + void testShouldIncludeUpgradeRequestHeader() { + assertThat(WebSocketResourceProvider.shouldIncludeUpgradeRequestHeader("Upgrade")).isFalse(); + assertThat(WebSocketResourceProvider.shouldIncludeUpgradeRequestHeader("Connection")).isFalse(); + assertThat(WebSocketResourceProvider.shouldIncludeUpgradeRequestHeader("Sec-WebSocket-Key")).isFalse(); + assertThat(WebSocketResourceProvider.shouldIncludeUpgradeRequestHeader(HttpHeaders.USER_AGENT)).isTrue(); + assertThat(WebSocketResourceProvider.shouldIncludeUpgradeRequestHeader("X-Forwarded-For")).isTrue(); + assertThat(WebSocketResourceProvider.shouldIncludeUpgradeRequestHeader("X-Signal-Receive-Stories")).isTrue(); + } + + @Test + void testShouldIncludeRequestMessageHeader() { + assertThat(WebSocketResourceProvider.shouldIncludeRequestMessageHeader("X-Forwarded-For")).isFalse(); + assertThat(WebSocketResourceProvider.shouldIncludeRequestMessageHeader(HttpHeaders.USER_AGENT)).isTrue(); + assertThat(WebSocketResourceProvider.shouldIncludeRequestMessageHeader("X-Signal-Receive-Stories")).isTrue(); + } + + @Test + void testGetCombinedHeaders() { + final Map> upgradeRequestHeaders = Map.of( + "Host", List.of("server.example.com"), + "Upgrade", List.of("websocket"), + "Connection", List.of("Upgrade"), + "Sec-WebSocket-Key", List.of("dGhlIHNhbXBsZSBub25jZQ=="), + "Sec-WebSocket-Protocol", List.of("chat, superchat"), + "Sec-WebSocket-Version", List.of("13"), + "X-Forwarded-For", List.of("127.0.0.1"), + HttpHeaders.USER_AGENT, List.of("Upgrade request user agent")); + + final Map requestMessageHeaders = Map.of( + "X-Forwarded-For", "192.168.0.1", + HttpHeaders.USER_AGENT, "Request message user agent"); + + final Map> expectedHeaders = Map.of( + "Host", List.of("server.example.com"), + "X-Forwarded-For", List.of("127.0.0.1"), + HttpHeaders.USER_AGENT, List.of("Request message user agent")); + + assertThat(WebSocketResourceProvider.getCombinedHeaders(upgradeRequestHeaders, requestMessageHeaders)).isEqualTo( + expectedHeaders); + } + + private SubProtocol.WebSocketResponseMessage getResponse(ArgumentCaptor responseCaptor) + throws InvalidProtocolBufferException { + return SubProtocol.WebSocketMessage.parseFrom(responseCaptor.getValue().array()).getResponse(); + } + + private SubProtocol.WebSocketRequestMessage getRequest(ArgumentCaptor requestCaptor) + throws InvalidProtocolBufferException { + return SubProtocol.WebSocketMessage.parseFrom(requestCaptor.getValue().array()).getRequest(); + } + + + public static class TestPrincipal implements Principal { + + private final String name; + + private TestPrincipal(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + } + + public static class TestException extends Exception { + + public TestException(String message) { + super(message); + } + } + + @Provider + public static class TestExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(TestException exception) { + return Response.status(1337).build(); + } + } + + @Path("/v1/test") + public static class TestResource { + + @GET + @Path("/hello") + public String testGetHello() { + return "Hello!"; + } + + @GET + @Path("/world") + public String testAuthorizedHello(@Auth TestPrincipal user) { + if (user == null) { + throw new AssertionError(); + } + + return "World: " + user.getName(); + } + + @GET + @Path("/optional") + public String testAuthorizedHello(@Auth Optional user) { + if (user.isPresent()) { + return "World: " + user.get().getName(); + } else { + return "Empty world"; + } + } + + @PUT + @Path("/some/{param}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response testSet(@Auth TestPrincipal user, @PathParam("param") String param, @Valid TestEntity entity) { + return Response.ok(user.name + ":" + param + ":" + entity.key + ":" + entity.value).build(); + } + + @GET + @Path("/exception/map") + public Response testExceptionMapping() throws TestException { + throw new TestException("I'd like to map this"); + } + + @GET + @Path("/keepalive") + public CompletableFuture testContextInjection(@WebSocketSession WebSocketSessionContext context) { + if (context == null) { + throw new AssertionError(); + } + + return context.getClient() + .sendRequest("GET", "/v1/miccheck", new LinkedList<>(), Optional.of("smert ze smert".getBytes())) + .thenApply(response -> Response.ok().entity(new String(response.getBody().get())).build()); + } + + public static class TestEntity { + + public TestEntity(String key, long value) { + this.key = key; + this.value = value; + } + + public TestEntity() { + } + + @JsonProperty + @NotEmpty + private String key; + + @JsonProperty + @Min(100) + private long value; + + } + } + +} diff --git a/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/test/java/org/whispersystems/websocket/logging/WebSocketRequestLogTest.java b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/test/java/org/whispersystems/websocket/logging/WebSocketRequestLogTest.java new file mode 100644 index 000000000..dd4b8fea0 --- /dev/null +++ b/jdk_17_maven/cs/rest/signal-server/websocket-resources/src/test/java/org/whispersystems/websocket/logging/WebSocketRequestLogTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2013-2020 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.websocket.logging; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.OutputStreamAppender; +import ch.qos.logback.core.spi.DeferredProcessingAware; +import com.google.common.net.HttpHeaders; +import io.dropwizard.logging.AbstractOutputStreamAppenderFactory; +import java.io.ByteArrayOutputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import javax.ws.rs.core.Response; +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.whispersystems.websocket.WebSocketSecurityContext; +import org.whispersystems.websocket.session.ContextPrincipal; +import org.whispersystems.websocket.session.WebSocketSessionContext; + +public class WebSocketRequestLogTest { + + private final static Locale ORIGINAL_DEFAULT_LOCALE = Locale.getDefault(); + + @BeforeEach + void beforeEachTest() { + Locale.setDefault(Locale.ENGLISH); + } + + @AfterEach + void afterEachTest() { + Locale.setDefault(ORIGINAL_DEFAULT_LOCALE); + } + + @Test + void testLogLineWithoutHeaders() throws InterruptedException { + WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class); + + ListAppender listAppender = new ListAppender<>(); + WebsocketRequestLoggerFactory requestLoggerFactory = new WebsocketRequestLoggerFactory(); + requestLoggerFactory.appenders = List.of(new ListAppenderFactory<>(listAppender)); + + WebsocketRequestLog requestLog = requestLoggerFactory.build("test-logger"); + ContainerRequest request = new ContainerRequest(null, URI.create("/v1/test"), "GET", + new WebSocketSecurityContext(new ContextPrincipal(sessionContext)), new MapPropertiesDelegate(new HashMap<>()), + null); + ContainerResponse response = new ContainerResponse(request, Response.ok("My response body").build()); + + requestLog.log("123.456.789.123", request, response); + + listAppender.waitForListSize(1); + assertThat(listAppender.list.size()).isEqualTo(1); + + String loggedLine = new String(listAppender.outputStream.toByteArray()); + assertThat(loggedLine).matches( + "123\\.456\\.789\\.123 \\- \\- \\[[0-9]{2}\\/[a-zA-Z]{3}\\/[0-9]{4}:[0-9]{2}:[0-9]{2}:[0-9]{2} (\\-|\\+)[0-9]{4}\\] \"GET \\/v1\\/test WS\" 200 \\- \"\\-\" \"\\-\"\n"); + } + + @Test + void testLogLineWithHeaders() throws InterruptedException { + WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class); + + ListAppender listAppender = new ListAppender<>(); + WebsocketRequestLoggerFactory requestLoggerFactory = new WebsocketRequestLoggerFactory(); + requestLoggerFactory.appenders = List.of(new ListAppenderFactory<>(listAppender)); + + WebsocketRequestLog requestLog = requestLoggerFactory.build("test-logger"); + ContainerRequest request = new ContainerRequest(null, URI.create("/v1/test"), "GET", + new WebSocketSecurityContext(new ContextPrincipal(sessionContext)), new MapPropertiesDelegate(new HashMap<>()), + null); + request.header(HttpHeaders.USER_AGENT, "SmertZeSmert"); + request.header("Referer", "https://moxie.org"); + ContainerResponse response = new ContainerResponse(request, Response.ok("My response body").build()); + + requestLog.log("123.456.789.123", request, response); + + listAppender.waitForListSize(1); + assertThat(listAppender.list.size()).isEqualTo(1); + + String loggedLine = new String(listAppender.outputStream.toByteArray()); + assertThat(loggedLine).matches( + "123\\.456\\.789\\.123 \\- \\- \\[[0-9]{2}\\/[a-zA-Z]{3}\\/[0-9]{4}:[0-9]{2}:[0-9]{2}:[0-9]{2} (\\-|\\+)[0-9]{4}\\] \"GET \\/v1\\/test WS\" 200 \\- \"https://moxie.org\" \"SmertZeSmert\"\n"); + + System.out.println(listAppender.list.get(0)); + System.out.println(new String(listAppender.outputStream.toByteArray())); + } + + private static class ListAppenderFactory extends + AbstractOutputStreamAppenderFactory { + + private final ListAppender listAppender; + + public ListAppenderFactory(ListAppender listAppender) { + this.listAppender = listAppender; + } + + @Override + protected OutputStreamAppender appender(LoggerContext context) { + listAppender.setContext(context); + return listAppender; + } + } + + private static class ListAppender extends OutputStreamAppender { + + public final List list = new ArrayList(); + public final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + protected void append(E e) { + super.append(e); + + synchronized (list) { + list.add(e); + list.notifyAll(); + } + } + + @Override + public void start() { + setOutputStream(outputStream); + super.start(); + } + + public void waitForListSize(int size) throws InterruptedException { + synchronized (list) { + while (list.size() < size) { + list.wait(5000); + } + } + } + + } + + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/CODEOWNERS b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/CODEOWNERS new file mode 100644 index 000000000..8710707bd --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/CODEOWNERS @@ -0,0 +1,3 @@ +# DISABLED: Turned this off since it's currently generating very noisy notifications to the team and reviewers can't +# tell the difference between being explicitly added as a reviewer versus by way of this group. +# * @navikt/arbeidsgiver diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/Dockerfile b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/Dockerfile new file mode 100644 index 000000000..321fc69a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/Dockerfile @@ -0,0 +1,3 @@ +FROM ghcr.io/navikt/baseimages/temurin:17 +COPY import-vault-token.sh /init-scripts +COPY /target/tiltaksgjennomforing-api-1.0.0-SNAPSHOT.jar app.jar diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/LICENSE.md b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/LICENSE.md new file mode 100644 index 000000000..df45544b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License + +Copyright 2018 NAV (Arbeids- og velferdsdirektoratet) - The Norwegian Labour and Welfare Administration + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/README.md b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/README.md new file mode 100644 index 000000000..291e7cff9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/README.md @@ -0,0 +1,7 @@ +Tiltaksgjennomføring API +=================================== + +For NAV-interne: Ta kontakt på Slack-kanal #arbeidsgiver-tiltak + +For utviklere: +Bygges med Maven: `mvn install` diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/docker-compose.yml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/docker-compose.yml new file mode 100644 index 000000000..b19e705b1 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/docker-compose.yml @@ -0,0 +1,92 @@ +version: "3" +services: + zookeeper: + image: confluentinc/cp-zookeeper:5.4.0 + hostname: zookeeper + container_name: zookeeper + ports: + - "2181:2181" + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + broker: + image: confluentinc/cp-server:5.4.0 + hostname: broker + container_name: broker + depends_on: + - zookeeper + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_METRIC_REPORTERS: io.confluent.metrics.reporter.ConfluentMetricsReporter + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_CONFLUENT_LICENSE_TOPIC_REPLICATION_FACTOR: 1 + CONFLUENT_METRICS_REPORTER_BOOTSTRAP_SERVERS: broker:29092 + CONFLUENT_METRICS_REPORTER_ZOOKEEPER_CONNECT: zookeeper:2181 + CONFLUENT_METRICS_REPORTER_TOPIC_REPLICAS: 1 + CONFLUENT_METRICS_ENABLE: "true" + CONFLUENT_SUPPORT_CUSTOMER_ID: "anonymous" + + kafka-tools: + image: confluentinc/cp-kafka:5.4.0 + hostname: kafka-tools + container_name: kafka-tools + command: [ "tail", "-f", "/dev/null" ] + network_mode: "host" + + schema-registry: + image: confluentinc/cp-schema-registry:5.4.0 + hostname: schema-registry + container_name: schema-registry + depends_on: + - zookeeper + - broker + ports: + - "8081:8081" + environment: + SCHEMA_REGISTRY_HOST_NAME: schema-registry + SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: "zookeeper:2181" + + control-center: + image: confluentinc/cp-enterprise-control-center:5.4.0 + hostname: control-center + container_name: control-center + depends_on: + - zookeeper + - broker + - schema-registry + ports: + - "9021:9021" + environment: + CONTROL_CENTER_BOOTSTRAP_SERVERS: 'broker:29092' + CONTROL_CENTER_ZOOKEEPER_CONNECT: 'zookeeper:2181' + CONTROL_CENTER_SCHEMA_REGISTRY_URL: "http://schema-registry:8081" + CONTROL_CENTER_REPLICATION_FACTOR: 1 + CONTROL_CENTER_INTERNAL_TOPICS_PARTITIONS: 1 + CONTROL_CENTER_MONITORING_INTERCEPTOR_TOPIC_PARTITIONS: 1 + CONFLUENT_METRICS_TOPIC_REPLICATION: 1 + PORT: 9021 + postgres: + image: postgres:latest + environment: + POSTGRES_USER: sample + POSTGRES_PASSWORD: sample + POSTGRES_DB: sample +# volumes: +# - ./dbdata:/var/lib/postgresql/data + ports: + - 6432:5432 + tiltak-dokgen: + build: + context: ../tiltak-dokgen + ports: + - 5913:8080 + volumes: + - ../tiltak-dokgen/content:/app/content + environment: + SPRING_PROFILES_ACTIVE: dev \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/import-vault-token.sh b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/import-vault-token.sh new file mode 100644 index 000000000..6ebab1578 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/import-vault-token.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +if test /var/run/secrets/nais.io/vault/vault_token; +then + export VAULT_TOKEN=$(cat /var/run/secrets/nais.io/vault/vault_token) +fi \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/lombok.config b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/lombok.config new file mode 100644 index 000000000..f902278dd --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties=true \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/alerterator.yaml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/alerterator.yaml new file mode 100644 index 000000000..229e2f21f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/alerterator.yaml @@ -0,0 +1,41 @@ +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: tiltaksgjennomforing-api + namespace: arbeidsgiver + labels: + team: team-tiltak +spec: + groups: + - name: tiltaksgjennomforing-api-alert + rules: + - alert: applikasjon nede + expr: sum(up{app="tiltaksgjennomforing-api", job="nais-system/monitoring-apps-tenant"}) == 0 + for: 1s + annotations: + summary: Appen er nede + action: "`kubectl describe pod {{ $labels.kubernetes_pod_name }} -n {{ $labels.kubernetes_namespace }}` for events, og `kubectl logs {{ $labels.kubernetes_pod_name }} -n {{ $labels.kubernetes_namespace }}` for logger" + labels: + namespace: team-tiltak + severity: critical + + - alert: last ned pdf feiler + expr: sum(increase(tiltaksgjennomforing_pdf_feil_total[15m])) > 0 + for: 1s + annotations: + summary: tiltaksgjennomforing-api feiler med pdf-generering + action: "`kubectl describe pod {{ $labels.kubernetes_pod_name }} -n {{ $labels.kubernetes_namespace }}` for events, og `kubectl logs {{ $labels.kubernetes_pod_name }} -n {{ $labels.kubernetes_namespace }}` for logger" + labels: + namespace: team-tiltak + severity: critical + + - alert: TILTAKSGJENNOMFORING-API ERROR! + expr: sum(increase(logback_events_total{app="tiltaksgjennomforing-api",level="error"}[10m])) > 0 + for: 10s + annotations: + summary: |- + tiltaksgjennomforing-api har logget en feil :this-is-fine-fire: Sjekk loggene om noe bør gjøres! + action: "Sjekk logs.adeo.no for logger: https://logs.adeo.no/app/r/s/HT3Nd" + labels: + namespace: team-tiltak + severity: critical \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/dev-fss.yaml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/dev-fss.yaml new file mode 100644 index 000000000..24cbbbf46 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/dev-fss.yaml @@ -0,0 +1,77 @@ +apiVersion: "nais.io/v1alpha1" +kind: "Application" +metadata: + name: tiltaksgjennomforing-api + namespace: arbeidsgiver + labels: + team: arbeidsgiver +spec: + env: + - name: MILJO + value: dev-fss + azure: + application: + enabled: true + allowAllUsers: true + tenant: trygdeetaten.no + claims: + groups: + - id: fbfea82d-13da-43ad-a2f2-d7f21cb95f12 + extra: + - "NAVident" + kafka: + pool: nav-dev + image: {{image}} + team: arbeidsgiver + port: 8080 + liveness: + path: /tiltaksgjennomforing-api/internal/healthcheck + initialDelay: 30 + timeout: 1 + periodSeconds: 30 + failureThreshold: 10 + readiness: + path: /tiltaksgjennomforing-api/internal/healthcheck + initialDelay: 30 + timeout: 1 + periodSeconds: 30 + failureThreshold: 10 + resources: + limits: + cpu: 2000m + memory: 3000Mi + requests: + cpu: 500m + memory: 600Mi + ingresses: + - https://arbeidsgiver.nais.preprod.local/tiltaksgjennomforing-api/ + leaderElection: true + vault: + enabled: true + webproxy: true + strategy: + type: RollingUpdate + tokenx: + enabled: true + prometheus: + enabled: true + path: /tiltaksgjennomforing-api/internal/actuator/prometheus + accessPolicy: + inbound: + rules: + - application: tiltak-proxy + - application: min-side-arbeidsgiver + namespace: fager + cluster: dev-gcp + - application: mulighetsrommet-api + namespace: team-mulighetsrommet + cluster: dev-gcp + outbound: + rules: + - application: poao-tilgang + namespace: poao + external: + - host: team-tiltak-unleash-api.nav.cloud.nais.io + envFrom: + - configmap: loginservice-idporten + - secret: tiltaksgjennomforing-api-unleash-api-token diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/dev-gcp-labs.yaml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/dev-gcp-labs.yaml new file mode 100644 index 000000000..6164eba88 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/dev-gcp-labs.yaml @@ -0,0 +1,51 @@ +apiVersion: "nais.io/v1alpha1" +kind: "Application" +metadata: + name: tiltaksgjennomforing-api-labs + namespace: arbeidsgiver + labels: + team: arbeidsgiver +spec: + env: + - name: MILJO + value: dev-gcp-labs + image: {{image}} + team: arbeidsgiver + port: 8080 + replicas: + min: 1 + max: 1 + liveness: + path: /tiltaksgjennomforing-api/internal/healthcheck + initialDelay: 30 + timeout: 1 + periodSeconds: 30 + failureThreshold: 10 + readiness: + path: /tiltaksgjennomforing-api/internal/healthcheck + initialDelay: 30 + timeout: 1 + periodSeconds: 30 + failureThreshold: 10 + resources: + limits: + cpu: 2000m + memory: 3000Mi + requests: + cpu: 500m + memory: 600Mi + prometheus: + enabled: true + path: /tiltaksgjennomforing-api/internal/actuator/prometheus + accessPolicy: + inbound: + rules: + - application: tiltaksgjennomforing-labs + outbound: + rules: + - application: tiltak-fakelogin + - application: tiltaksgjennomforing-wiremock + - application: tiltak-refusjon-api-labs + - application: tiltak-dokgen + envFrom: + - configmap: loginservice-idporten \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/prod-fss.yaml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/prod-fss.yaml new file mode 100644 index 000000000..40b43ca9d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/prod-fss.yaml @@ -0,0 +1,79 @@ +apiVersion: "nais.io/v1alpha1" +kind: "Application" +metadata: + name: tiltaksgjennomforing-api + namespace: arbeidsgiver + labels: + team: arbeidsgiver +spec: + env: + - name: MILJO + value: prod-fss + azure: + application: + enabled: true + allowAllUsers: true + tenant: nav.no + claims: + groups: + # Beslutter-gruppe + - id: 156f4f79-6909-4be1-8045-323f55590898 + # Team Tiltak + - id: fb516b74-0f2e-4b62-bad8-d70b82c3ae0b + extra: + - "NAVident" + kafka: + pool: nav-prod + image: {{image}} + team: arbeidsgiver + port: 8080 + liveness: + path: /tiltaksgjennomforing-api/internal/healthcheck + initialDelay: 30 + timeout: 1 + periodSeconds: 30 + failureThreshold: 10 + readiness: + path: /tiltaksgjennomforing-api/internal/healthcheck + initialDelay: 30 + timeout: 1 + periodSeconds: 30 + failureThreshold: 10 + replicas: + min: 3 + resources: + limits: + cpu: 2000m + memory: 12000Mi + requests: + cpu: 500m + memory: 6000Mi + ingresses: + - https://arbeidsgiver.nais.adeo.no/tiltaksgjennomforing-api/ + leaderElection: true + vault: + enabled: true + webproxy: true + strategy: + type: RollingUpdate + tokenx: + enabled: true + prometheus: + enabled: true + path: /tiltaksgjennomforing-api/internal/actuator/prometheus + accessPolicy: + inbound: + rules: + - application: tiltak-proxy + - application: min-side-arbeidsgiver + namespace: fager + cluster: prod-gcp + outbound: + rules: + - application: poao-tilgang + namespace: poao + external: + - host: team-tiltak-unleash-api.nav.cloud.nais.io + envFrom: + - configmap: loginservice-idporten + - secret: tiltaksgjennomforing-api-unleash-api-token \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/unleash-apitoken.yml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/unleash-apitoken.yml new file mode 100644 index 000000000..67b3e730f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/unleash-apitoken.yml @@ -0,0 +1,14 @@ +apiVersion: unleash.nais.io/v1 +kind: ApiToken +metadata: + name: tiltaksgjennomforing-api + namespace: arbeidsgiver + labels: + team: arbeidsgiver +spec: + unleashInstance: + apiVersion: unleash.nais.io/v1 + kind: RemoteUnleash + name: team-tiltak + secretName: tiltaksgjennomforing-api-unleash-api-token + environment: {{ unleash-environment }} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/wiremock.yaml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/wiremock.yaml new file mode 100644 index 000000000..03f2cf12b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/nais/wiremock.yaml @@ -0,0 +1,42 @@ +apiVersion: nais.io/v1alpha1 +kind: Application +metadata: + name: tiltaksgjennomforing-wiremock + namespace: arbeidsgiver + labels: + team: arbeidsgiver +spec: + image: ghcr.io/navikt/tiltaksgjennomforing-api/wiremock:2.27.2 + replicas: + min: 1 + max: 1 + port: 8080 + liveness: + path: /ereg/912345678 + initialDelay: 1 + timeout: 1 + periodSeconds: 10 + failureThreshold: 3 + readiness: + path: /ereg/912345678 + initialDelay: 1 + timeout: 1 + periodSeconds: 10 + failureThreshold: 3 + resources: + limits: + cpu: 1000m + memory: 1000Mi + requests: + cpu: 500m + memory: 500Mi + env: + - name: deploytrigger + value: "{{deploytrigger}}" + filesFrom: + - configmap: tiltaksgjennomforing-wiremock + mountPath: /home/wiremock/mappings + accessPolicy: + inbound: + rules: + - application: tiltaksgjennomforing-api-labs diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/pom.xml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/pom.xml new file mode 100644 index 000000000..53cdd6727 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/pom.xml @@ -0,0 +1,301 @@ + + + 4.0.0 + + no.nav.tag + tiltaksgjennomforing-api + 1.0.0-SNAPSHOT + jar + + org.springframework.boot + spring-boot-starter-parent + 2.7.14 + + + + + UTF-8 + UTF-8 + 17 + 2.9.2 + 1.11.3 + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.jayway.jsonpath + json-path + 2.8.0 + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-cache + + + javax.cache + cache-api + + + org.ehcache + ehcache + + + net.sf.ehcache + ehcache + + + org.springframework.boot + spring-boot-configuration-processor + true + + + com.h2database + h2 + runtime + + + org.flywaydb + flyway-core + + + org.postgresql + postgresql + + + org.projectlombok + lombok + provided + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-validation + + + io.micrometer + micrometer-registry-prometheus + + + net.logstash.logback + logstash-logback-encoder + 7.2 + + + no.nav + vault-jdbc + 1.3.1 + + + org.apache.commons + commons-text + 1.10.0 + + + org.springframework.kafka + spring-kafka + + + + + no.nav.poao-tilgang + client + 2023.09.25_09.26-72043f243cad + + + + + no.nav.security + token-validation-spring + 2.1.9 + + + no.nav.security + token-client-spring + 2.1.9 + + + + + org.springdoc + springdoc-openapi-ui + 1.6.15 + + + + + io.getunleash + unleash-client-java + 8.3.0 + + + + no.nav.arbeidsgiver + altinn-rettigheter-proxy-klient + 2.0.1 + + + + org.apache.avro + avro + ${avro.version} + + + + io.confluent + kafka-avro-serializer + 7.1.1 + + + + com.github.victools + jsonschema-generator + 4.28.0 + + + + + + org.awaitility + awaitility + 4.1.0 + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.kafka + spring-kafka-test + test + + + com.github.tomakehurst + wiremock + 2.27.2 + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.avro + avro-maven-plugin + ${avro.version} + + + generate-sources + + schema + + + src/main/resources/avro + String + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 17 + 17 + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + confluent + https://packages.confluent.io/maven/ + + + + central + https://repo.maven.apache.org/maven2 + + + github + https://github-package-registry-mirror.gc.nav.no/cached/maven-release + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/ApiBeskrivelse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/ApiBeskrivelse.java new file mode 100644 index 000000000..1547763ef --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/ApiBeskrivelse.java @@ -0,0 +1,12 @@ +package no.nav.tag.tiltaksgjennomforing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiBeskrivelse { + String value(); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/ApiBeskriver.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/ApiBeskriver.java new file mode 100644 index 000000000..bf3ffa7a6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/ApiBeskriver.java @@ -0,0 +1,26 @@ +package no.nav.tag.tiltaksgjennomforing; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Aspect +@Component +public class ApiBeskriver { + + public static final String API_BESKRIVELSE_ATTRIBUTT = "API_BESKRIVELSE"; + + @Around("@annotation(no.nav.tag.tiltaksgjennomforing.ApiBeskrivelse)") + public Object validateAspect(ProceedingJoinPoint pjp) throws Throwable { + var request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + + var methodSig = (MethodSignature) pjp.getSignature(); + var annotasjon = methodSig.getMethod().getAnnotation(ApiBeskrivelse.class); + request.setAttribute(API_BESKRIVELSE_ATTRIBUTT, annotasjon.value()); + return pjp.proceed(); + } +} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/Milj\303\270.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/Milj\303\270.java" new file mode 100644 index 000000000..43ab2ab8a --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/Milj\303\270.java" @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing; + +public class Miljø { + public static final String LOCAL = "local"; + public static final String DEV_FSS = "dev-fss"; + public static final String DEV_GCP_LABS = "dev-gcp-labs"; + public static final String PROD_FSS = "prod-fss"; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/TiltaksgjennomforingApplication.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/TiltaksgjennomforingApplication.java new file mode 100644 index 000000000..c704b73b1 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/TiltaksgjennomforingApplication.java @@ -0,0 +1,34 @@ +package no.nav.tag.tiltaksgjennomforing; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import no.nav.security.token.support.spring.api.EnableJwtTokenValidation; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@SpringBootApplication +@EnableJwtTokenValidation(ignore = { + "org.springdoc", + "springfox.documentation.swagger.web.ApiResourceController", + "no.nav.tag.tiltaksgjennomforing.featuretoggles.FeatureToggleController", + "org.springframework" +}) +@EnableConfigurationProperties +@EnableJpaRepositories +@EnableCaching +@OpenAPIDefinition +public class TiltaksgjennomforingApplication { + public static void main(String[] args) { + String clusterName = System.getenv("MILJO"); + if (clusterName == null) { + System.out.println("Kan ikke startes uten miljøvariabel MILJO. Lokalt kan LokalTiltaksgjennomforingApplication kjøres."); + System.exit(1); + } + new SpringApplicationBuilder(TiltaksgjennomforingApplication.class) + .profiles(clusterName) + .build() + .run(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/BeslutterAdGruppeProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/BeslutterAdGruppeProperties.java new file mode 100644 index 000000000..9c9ab03fa --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/BeslutterAdGruppeProperties.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.beslutter-ad-gruppe") +public class BeslutterAdGruppeProperties { + private UUID id; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetArbeidsgiver.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetArbeidsgiver.java new file mode 100644 index 000000000..eef976f17 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetArbeidsgiver.java @@ -0,0 +1,21 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import lombok.Value; +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.model.AltinnReportee; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +@Value +public class InnloggetArbeidsgiver implements InnloggetBruker { + Fnr identifikator; + Set altinnOrganisasjoner; + Map> tilganger; + Avtalerolle rolle = Avtalerolle.ARBEIDSGIVER; + boolean erNavAnsatt = false; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetBeslutter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetBeslutter.java new file mode 100644 index 000000000..64d8d925d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetBeslutter.java @@ -0,0 +1,17 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.NavEnhet; + +import java.util.Set; + +@Value +public class InnloggetBeslutter implements InnloggetBruker { + + NavIdent identifikator; + Avtalerolle rolle = Avtalerolle.BESLUTTER; + boolean erNavAnsatt = true; + Set navEnheter; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetBruker.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetBruker.java new file mode 100644 index 000000000..7e5754645 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetBruker.java @@ -0,0 +1,6 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +public interface InnloggetBruker { + + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetBrukerController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetBrukerController.java new file mode 100644 index 000000000..bbf10ce5f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetBrukerController.java @@ -0,0 +1,30 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import no.nav.security.token.support.core.api.Protected; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.exceptions.IkkeValgtPartException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Optional; + +@Protected +@RestController +@RequestMapping("/innlogget-bruker") +public class InnloggetBrukerController { + private final InnloggingService innloggingService; + + @Autowired + public InnloggetBrukerController(InnloggingService innloggingService) { + this.innloggingService = innloggingService; + } + + @GetMapping + public ResponseEntity hentInnloggetBruker(@CookieValue("innlogget-part") Optional innloggetPart) { + return ResponseEntity.ok(innloggingService.hentInnloggetBruker(innloggetPart.orElseThrow(IkkeValgtPartException::new))); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetDeltaker.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetDeltaker.java new file mode 100644 index 000000000..7c3e4dc47 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetDeltaker.java @@ -0,0 +1,12 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; + +@Value +public class InnloggetDeltaker implements InnloggetBruker { + Fnr identifikator; + Avtalerolle rolle = Avtalerolle.DELTAKER; + boolean erNavAnsatt = false; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetMentor.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetMentor.java new file mode 100644 index 000000000..bebfc79ec --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetMentor.java @@ -0,0 +1,12 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; + +@Value +public class InnloggetMentor implements InnloggetBruker { + Fnr identifikator; + Avtalerolle rolle = Avtalerolle.MENTOR; + boolean erNavAnsatt = false; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetVeileder.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetVeileder.java new file mode 100644 index 000000000..30540cf94 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetVeileder.java @@ -0,0 +1,16 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import java.util.Set; +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.NavEnhet; + +@Value +public class InnloggetVeileder implements InnloggetBruker { + NavIdent identifikator; + Set navEnheter; + Avtalerolle rolle = Avtalerolle.VEILEDER; + boolean erNavAnsatt = true; + boolean kanVæreBeslutter; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggingService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggingService.java new file mode 100644 index 000000000..a5e74bf39 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggingService.java @@ -0,0 +1,120 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.model.AltinnReportee; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils.BrukerOgIssuer; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils.Issuer; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.TilgangskontrollService; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring.AltinnTilgangsstyringService; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring.ArbeidsgiverTokenStrategyFactory; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring.HentArbeidsgiverToken; +import no.nav.tag.tiltaksgjennomforing.avtale.Arbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalepart; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; +import no.nav.tag.tiltaksgjennomforing.avtale.Beslutter; +import no.nav.tag.tiltaksgjennomforing.avtale.Deltaker; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.avtale.Mentor; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import no.nav.tag.tiltaksgjennomforing.avtale.Veileder; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2Client; +import no.nav.tag.tiltaksgjennomforing.enhet.VeilarbArenaClient; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; +import no.nav.tag.tiltaksgjennomforing.exceptions.TilgangskontrollException; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.AxsysService; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.NavEnhet; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class InnloggingService { + private final SystembrukerProperties systembrukerProperties; + private final BeslutterAdGruppeProperties beslutterAdGruppeProperties; + private final TokenUtils tokenUtils; + private final AltinnTilgangsstyringService altinnTilgangsstyringService; + private final TilgangskontrollService tilgangskontrollService; + private final PersondataService persondataService; + private final Norg2Client norg2Client; + private final AxsysService axsysService; + private final SlettemerkeProperties slettemerkeProperties; + private final VeilarbArenaClient veilarbArenaClient; + private final ArbeidsgiverTokenStrategyFactory arbeidsgiverTokenStrategyFactory; + + public Avtalepart hentAvtalepart(Avtalerolle avtalerolle) { + BrukerOgIssuer brukerOgIssuer = tokenUtils.hentBrukerOgIssuer().orElseThrow(() -> new TilgangskontrollException("Bruker er ikke innlogget.")); + Issuer issuer = brukerOgIssuer.getIssuer(); + + if (issuer == Issuer.ISSUER_TOKENX && (avtalerolle == Avtalerolle.DELTAKER || avtalerolle == Avtalerolle.MENTOR)) { + if(avtalerolle == Avtalerolle.DELTAKER) return new Deltaker(new Fnr(brukerOgIssuer.getBrukerIdent())); + else return new Mentor(new Fnr(brukerOgIssuer.getBrukerIdent())); + }else if (issuer == Issuer.ISSUER_TOKENX && avtalerolle == Avtalerolle.ARBEIDSGIVER) { + HentArbeidsgiverToken hentArbeidsgiverToken = arbeidsgiverTokenStrategyFactory.create(issuer); + + Set altinnOrganisasjoner = altinnTilgangsstyringService + .hentAltinnOrganisasjoner(new Fnr(brukerOgIssuer.getBrukerIdent()), hentArbeidsgiverToken); + Map> tilganger = altinnTilgangsstyringService.hentTilganger(new Fnr(brukerOgIssuer.getBrukerIdent()), hentArbeidsgiverToken); + return new Arbeidsgiver(new Fnr(brukerOgIssuer.getBrukerIdent()), altinnOrganisasjoner, tilganger, persondataService, norg2Client); + } else if (issuer == Issuer.ISSUER_AAD && avtalerolle == Avtalerolle.VEILEDER) { + NavIdent navIdent = new NavIdent(brukerOgIssuer.getBrukerIdent()); + Set navEnheter = hentNavEnheter(navIdent); + boolean harAdGruppeForBeslutter = tokenUtils.harAdGruppe(beslutterAdGruppeProperties.getId()); + return new Veileder(navIdent, tokenUtils.hentAzureOid(), tilgangskontrollService, persondataService, norg2Client, navEnheter, slettemerkeProperties, harAdGruppeForBeslutter, veilarbArenaClient); + } else if (issuer == Issuer.ISSUER_AAD && avtalerolle == Avtalerolle.BESLUTTER) { + boolean harAdGruppeForBeslutter = tokenUtils.harAdGruppe(beslutterAdGruppeProperties.getId()); + if (harAdGruppeForBeslutter) { + var navIdent = new NavIdent(brukerOgIssuer.getBrukerIdent()); + var navEnheter = hentNavEnheter(navIdent); + return new Beslutter(navIdent, tokenUtils.hentAzureOid(), navEnheter, tilgangskontrollService, norg2Client); + } else { + throw new FeilkodeException(Feilkode.MANGLER_AD_GRUPPE_BESLUTTER); + } + } else { + log.warn("Ugyldig kombinasjon av issuer={} og rolle={}", issuer, avtalerolle); + throw new FeilkodeException(Feilkode.UGYLDIG_KOMBINASJON_AV_ISSUER_OG_ROLLE); + } + } + + private Set hentNavEnheter(NavIdent navIdent) { + return new HashSet<>(axsysService.hentEnheterNavAnsattHarTilgangTil(navIdent)); + } + + public Veileder hentVeileder() { + return (Veileder) hentAvtalepart(Avtalerolle.VEILEDER); + } + + public Arbeidsgiver hentArbeidsgiver() { + return (Arbeidsgiver) hentAvtalepart(Avtalerolle.ARBEIDSGIVER); + } + + public InnloggetBruker hentInnloggetBruker(Avtalerolle avtalerolle) { + return hentAvtalepart(avtalerolle).innloggetBruker(); + } + + public InnloggetVeileder hentInnloggetVeileder() { + try { + return (InnloggetVeileder) hentInnloggetBruker(Avtalerolle.VEILEDER); + } catch (ClassCastException e) { + throw new TilgangskontrollException("Innlogget bruker er ikke veileder."); + } + } + + public void validerSystembruker() { + tokenUtils.hentBrukerOgIssuer() + .filter(t -> (Issuer.ISSUER_SYSTEM == t.getIssuer() && systembrukerProperties.getId().equals(t.getBrukerIdent()))) + .orElseThrow(() -> new TilgangskontrollException("Systemet har ikke tilgang til tjenesten")); + } + + public Beslutter hentBeslutter() { + return (Beslutter) hentAvtalepart(Avtalerolle.BESLUTTER); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/LabsSecurityAzureClientConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/LabsSecurityAzureClientConfiguration.java new file mode 100644 index 000000000..edcf8ffbc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/LabsSecurityAzureClientConfiguration.java @@ -0,0 +1,27 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.Miljø; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.web.client.RestTemplate; + + +@Configuration +@Profile(value = {Miljø.DEV_GCP_LABS }) +@Slf4j +public class LabsSecurityAzureClientConfiguration { + @Bean("notifikasjonerRestTemplate") + public RestTemplate anonymProxyRestTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder.build(); + } + + @Bean("veilarbarenaRestTemplate") + public RestTemplate anonymProxyRestTemplateVeilabArena(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder.build(); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/PoaoTilgangService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/PoaoTilgangService.java new file mode 100644 index 000000000..1581a23c5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/PoaoTilgangService.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import java.util.UUID; + +public interface PoaoTilgangService { + boolean harSkriveTilgang(UUID beslutterAzureUUID, String deltakerFnr); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/PoaoTilgangServiceImpl.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/PoaoTilgangServiceImpl.java new file mode 100644 index 000000000..1897b91ba --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/PoaoTilgangServiceImpl.java @@ -0,0 +1,56 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import java.time.Duration; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import com.github.benmanes.caffeine.cache.Caffeine; + +import lombok.extern.slf4j.Slf4j; +import no.nav.common.rest.client.RestClient; +import no.nav.poao_tilgang.client.NavAnsattTilgangTilEksternBrukerPolicyInput; +import no.nav.poao_tilgang.client.PoaoTilgangCachedClient; +import no.nav.poao_tilgang.client.PoaoTilgangClient; +import no.nav.poao_tilgang.client.PoaoTilgangHttpClient; +import no.nav.poao_tilgang.client.TilgangType; +import no.nav.security.token.support.client.core.ClientProperties; +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenService; +import no.nav.security.token.support.client.spring.ClientConfigurationProperties; +import no.nav.tag.tiltaksgjennomforing.Miljø; + +@Service +@Profile(value = { Miljø.DEV_FSS, Miljø.PROD_FSS }) +@Slf4j +public class PoaoTilgangServiceImpl implements PoaoTilgangService { + + private final PoaoTilgangClient klient; + + public PoaoTilgangServiceImpl( + @Value("${tiltaksgjennomforing.poao-tilgang.url}") String poaoTilgangUrl, + ClientConfigurationProperties clientConfigurationProperties, OAuth2AccessTokenService oAuth2AccessTokenService + ) { + ClientProperties clientProperties = clientConfigurationProperties.getRegistration().get("poao-tilgang"); + klient = new PoaoTilgangCachedClient( + new PoaoTilgangHttpClient(poaoTilgangUrl, + () -> oAuth2AccessTokenService.getAccessToken(clientProperties).getAccessToken(), + RestClient.baseClient()), + Caffeine.newBuilder() + .expireAfterWrite(Duration.ofMinutes(30)) + .build(), + Caffeine.newBuilder() + .expireAfterWrite(Duration.ofMinutes(30)) + .build(), + Caffeine.newBuilder() + .expireAfterWrite(Duration.ofMinutes(30)) + .build()); + } + + public boolean harSkriveTilgang(UUID beslutterAzureUUID, String deltakerFnr) { + return klient.evaluatePolicy(new NavAnsattTilgangTilEksternBrukerPolicyInput( + beslutterAzureUUID, + TilgangType.SKRIVE, + deltakerFnr) + ).get().isPermit(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/PoaoTilgangServiceLabs.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/PoaoTilgangServiceLabs.java new file mode 100644 index 000000000..eb2318f30 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/PoaoTilgangServiceLabs.java @@ -0,0 +1,16 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import java.util.UUID; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import no.nav.tag.tiltaksgjennomforing.Miljø; + +@Service +@Profile(value = { Miljø.DEV_GCP_LABS, Miljø.LOCAL }) +public class PoaoTilgangServiceLabs implements PoaoTilgangService { + + public boolean harSkriveTilgang(UUID beslutterAzureUUID, String deltakerFnr) { + return true; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/SecurityAzureClientConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/SecurityAzureClientConfiguration.java new file mode 100644 index 000000000..0f8cdaf32 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/SecurityAzureClientConfiguration.java @@ -0,0 +1,57 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + + +import lombok.extern.slf4j.Slf4j; +import no.nav.security.token.support.client.core.ClientProperties; +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse; +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenService; +import no.nav.security.token.support.client.spring.ClientConfigurationProperties; +import no.nav.security.token.support.client.spring.oauth2.EnableOAuth2Client; +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.exceptions.TilgangskontrollException; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpHeaders; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestTemplate; + + +@EnableOAuth2Client(cacheEnabled = true) +@Configuration +@Profile(value = {Miljø.PROD_FSS, Miljø.DEV_FSS}) +@Slf4j +public class SecurityAzureClientConfiguration { + + + @Bean("notifikasjonerRestTemplate") + public RestTemplate anonymProxyRestTemplate(RestTemplateBuilder restTemplateBuilder, + ClientConfigurationProperties clientConfigurationProperties, + OAuth2AccessTokenService oAuth2AccessTokenService) { + + final ClientProperties clientProperties = clientConfigurationProperties.getRegistration().get("notifikasjoner"); + return restTemplateBuilder.additionalInterceptors(bearerTokenInterceptor(clientProperties, oAuth2AccessTokenService)).build(); + } + + @Bean("veilarbarenaRestTemplate") + public RestTemplate anonymProxyRestTemplateVeilabArena(RestTemplateBuilder restTemplateBuilder, + ClientConfigurationProperties clientConfigurationProperties, + OAuth2AccessTokenService oAuth2AccessTokenService) { + + final ClientProperties clientProperties = clientConfigurationProperties.getRegistration().get("veilarbarena"); + return restTemplateBuilder.additionalInterceptors(bearerTokenInterceptor(clientProperties, oAuth2AccessTokenService)).build(); + } + + private ClientHttpRequestInterceptor bearerTokenInterceptor(final ClientProperties clientProperties, final OAuth2AccessTokenService oAuth2AccessTokenService) { + return (request, body, execution) -> { + OAuth2AccessTokenResponse response = oAuth2AccessTokenService.getAccessToken(clientProperties); + HttpHeaders headers = request.getHeaders(); + if (response == null || body == null) { + throw new TilgangskontrollException("Azure klient feilet med lesing av response data"); + } + headers.setBearerAuth(response.getAccessToken()); + return execution.execute(request, body); + }; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/SlettemerkeProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/SlettemerkeProperties.java new file mode 100644 index 000000000..ca653fbe0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/SlettemerkeProperties.java @@ -0,0 +1,17 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import lombok.Data; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.slettemerking.tilgang") +public class SlettemerkeProperties { + private List ident = new ArrayList<>(); + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/SystembrukerProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/SystembrukerProperties.java new file mode 100644 index 000000000..00163ae64 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/SystembrukerProperties.java @@ -0,0 +1,13 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.consumer.system") +public class SystembrukerProperties { + + private String id; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/TokenUtils.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/TokenUtils.java new file mode 100644 index 000000000..0a781cfd1 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/TokenUtils.java @@ -0,0 +1,102 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import lombok.RequiredArgsConstructor; +import lombok.Value; +import no.nav.security.token.support.core.context.TokenValidationContext; +import no.nav.security.token.support.core.context.TokenValidationContextHolder; +import no.nav.security.token.support.core.jwt.JwtTokenClaims; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils.Issuer.*; + +@Component +@RequiredArgsConstructor +public class TokenUtils { + private static final String ACR = "acr"; + private static final String LEVEL4 = "Level4"; + + public UUID hentAzureOid() { + return hentClaim(ISSUER_AAD, "oid").map(UUID::fromString).orElse(null); + } + + public enum Issuer { + ISSUER_AAD("aad"), + ISSUER_SYSTEM("system"), + ISSUER_TOKENX("tokenx"); + + final String issuerName; + + Issuer(String issuerName) { + this.issuerName = issuerName; + } + } + + @Value + public static class BrukerOgIssuer { + Issuer issuer; + String brukerIdent; + } + + private final TokenValidationContextHolder contextHolder; + + public Optional hentBrukerOgIssuer() { + return hentClaim(ISSUER_SYSTEM, "sub").map(sub -> new BrukerOgIssuer(ISSUER_SYSTEM, sub)) + .or(() -> hentClaim(ISSUER_AAD, "NAVident").map(sub -> new BrukerOgIssuer(ISSUER_AAD, sub))) + .or(() -> hentClaim(ISSUER_TOKENX, "pid").map(it -> new BrukerOgIssuer(ISSUER_TOKENX, it))); + } + + public boolean harAdGruppe(UUID gruppeAD) { + Optional> groupsClaim = hentClaims(ISSUER_AAD, "groups"); + if (!groupsClaim.isPresent()) { + return false; + } + return groupsClaim.get().contains(gruppeAD.toString()); + } + + public boolean harAdRolle(String rolle) { + Optional> roller = hentClaims(ISSUER_AAD, "roles"); + if (!roller.isPresent()) { + return false; + } + return roller.get().contains(rolle.toString()); + } + + private Optional> hentClaims(Issuer issuer, String claim) { + return hentClaimSet(issuer).filter(jwtClaimsSet -> innloggingsNivaOK(issuer, jwtClaimsSet)) + .map(jwtClaimsSet -> (List) jwtClaimsSet.get(claim)); + } + + private Optional hentClaim(Issuer issuer, String claim) { + return hentClaimSet(issuer) + .filter(jwtClaimsSet -> innloggingsNivaOK(issuer, jwtClaimsSet)) + .map(jwtClaimsSet -> jwtClaimsSet.get(claim)) + .map(String::valueOf); + } + + private boolean innloggingsNivaOK(Issuer issuer, JwtTokenClaims jwtClaimsSet) { + + return issuer != ISSUER_TOKENX || LEVEL4.equals(jwtClaimsSet.get(ACR)); + } + + private Optional hentClaimSet(Issuer issuer) { + TokenValidationContext tokenValidationContext; + try { + tokenValidationContext = contextHolder.getTokenValidationContext(); + } catch (IllegalStateException e) { + // Er ikke i kontekst av en request + return Optional.empty(); + } + JwtTokenClaims claims = tokenValidationContext.getClaims(issuer.issuerName); + return Optional.ofNullable(claims); + + } + + public String hentTokenx() { + return contextHolder.getTokenValidationContext().getJwtToken(ISSUER_TOKENX.issuerName).getTokenAsString(); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/UtviklerTilgangProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/UtviklerTilgangProperties.java new file mode 100644 index 000000000..2a25cbb8a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/UtviklerTilgangProperties.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.utvikler-tilgang") +public class UtviklerTilgangProperties { + private UUID gruppeTilgang; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/TilgangskontrollService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/TilgangskontrollService.java new file mode 100644 index 000000000..c4abebe47 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/TilgangskontrollService.java @@ -0,0 +1,13 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.abac; + +import java.util.Map; +import java.util.Set; + +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.avtale.InternBruker; + +public interface TilgangskontrollService { + boolean harSkrivetilgangTilKandidat(InternBruker internBruker, Fnr fnr); + + Map skriveTilganger(InternBruker internBruker, Set fnr); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/TilgangskontrollServiceImpl.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/TilgangskontrollServiceImpl.java new file mode 100644 index 000000000..a2d6c8b3e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/TilgangskontrollServiceImpl.java @@ -0,0 +1,54 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.abac; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +import org.slf4j.MDC; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.PoaoTilgangService; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.adapter.AbacAdapter; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.avtale.InternBruker; + +@Service +@Slf4j +@RequiredArgsConstructor +public class TilgangskontrollServiceImpl implements TilgangskontrollService { + + private final AbacAdapter abacAdapter; + private final PoaoTilgangService poaoTilgangService; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + public boolean harSkrivetilgangTilKandidat(InternBruker internBruker, Fnr fnr) { + var harAbacTilgang = abacAdapter.harSkriveTilgang(internBruker.getNavIdent().asString(), fnr.asString()); + var contextMap = MDC.getCopyOfContextMap(); + executorService.submit(() -> { + MDC.setContextMap(contextMap); + try { + if (internBruker.getAzureOid() != null && fnr.asString() != null) { + var harPoaoTilgang = poaoTilgangService.harSkriveTilgang(internBruker.getAzureOid(), fnr.asString()); + if (harPoaoTilgang != harAbacTilgang) { + log.warn("Tilgangskontroll: ulikt utfall i abac ({}) og poao ({})", harAbacTilgang, harPoaoTilgang); + } + } + } catch (Exception e) { + log.error("Feil ved tilgangskontroll-sammenligning", e); + } finally { + MDC.clear(); + } + }); + return harAbacTilgang; + } + + public Map skriveTilganger(InternBruker internBruker, Set fnrListe) { + return fnrListe.stream() + .map(fnr -> Map.entry(fnr, harSkrivetilgangTilKandidat(internBruker, fnr))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacAdapter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacAdapter.java new file mode 100644 index 000000000..f751bb2fa --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacAdapter.java @@ -0,0 +1,90 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.adapter; + +import no.nav.tag.tiltaksgjennomforing.infrastruktur.cache.EhCacheConfig; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.sts.STSClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.Cache; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.ehcache.EhCacheCacheManager; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import static no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.adapter.AbacTransformer.tilAbacRequestBody; + +@Service +public class AbacAdapter { + final Logger log = LoggerFactory.getLogger(getClass()); + private final RestTemplate restTemplate; + + private final STSClient stsClient; + + private final AbacProperties abacProperties; + + private final Cache cache; + + public AbacAdapter(RestTemplate restTemplate, STSClient stsClient, AbacProperties abacProperties, EhCacheCacheManager cacheManager) { + this.restTemplate = restTemplate; + this.stsClient = stsClient; + this.abacProperties = abacProperties; + this.cache = cacheManager.getCache(EhCacheConfig.ABAC_CACHE); + } + + Map cacheKey(String navIdent, String deltakerFnr) { + return Map.of("navIdent", navIdent, "deltakerFnr", deltakerFnr); + } + + private AbacResponse hentRespons(String navIdent, String deltakerFnr) { + return restTemplate.postForObject( + abacProperties.getUri(), + getHttpEntity(tilAbacRequestBody(navIdent, deltakerFnr)), + AbacResponse.class + ); + } + + public boolean harSkriveTilgang(String navIdent, String deltakerFnr) { + if (navIdent == null) { + log.error("Navident manglet i tilgangskontroll"); + return false; + } + if (deltakerFnr == null) { + log.error("DeltakerFnr manglet i tilgangskontroll"); + return false; + } + var key = cacheKey(navIdent, deltakerFnr); + var cachedValue = cache.get(key, Boolean.class); + if (cachedValue != null) { + return cachedValue; + } + try { + var result = Objects.equals(hentRespons(navIdent, deltakerFnr).response.decision, "Permit"); + cache.putIfAbsent(key, result); + return result; + } catch (RuntimeException ex) { + log.error("Abac feil", ex); + return false; + } + } + + private HttpEntity getHttpEntity(String body) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Nav-Consumer-Id", abacProperties.getNavConsumerId()); + headers.set("Nav-Call-Id", UUID.randomUUID().toString()); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(stsClient.hentSTSToken().getAccessToken()); + return new HttpEntity<>(body, headers); + } + + @CacheEvict(cacheNames = EhCacheConfig.ABAC_CACHE, allEntries = true) + public void cacheEvict() { + log.info("Tømmer abac cache for data"); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacProperties.java new file mode 100644 index 000000000..e982d8696 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacProperties.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.adapter; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.abac") +public class AbacProperties { + private String uri; + private String navConsumerId; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacResponse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacResponse.java new file mode 100644 index 000000000..d0524a80d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacResponse.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.adapter; + +import com.fasterxml.jackson.databind.PropertyNamingStrategy.UpperCamelCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(UpperCamelCaseStrategy.class) +public class AbacResponse { + public AbacResponseResponse response; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacResponseResponse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacResponseResponse.java new file mode 100644 index 000000000..8fc21beb6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacResponseResponse.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.adapter; + +import com.fasterxml.jackson.databind.PropertyNamingStrategy.UpperCamelCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(UpperCamelCaseStrategy.class) +public class AbacResponseResponse { + public String decision; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacTransformer.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacTransformer.java new file mode 100644 index 000000000..ea9833688 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacTransformer.java @@ -0,0 +1,59 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.adapter; + +public class AbacTransformer { + + public static String tilAbacRequestBody(String navIdent, String deltakerFnr){ + return "{\n" + + " \"Request\": {\n" + + " \"AccessSubject\": {\n" + + " \"Attribute\": [\n" + + " {\n" + + " \"AttributeId\": \"urn:oasis:names:tc:xacml:1.0:subject:subject-id\",\n" + + " \"Value\": \"" + navIdent + "\"\n" + + " },\n" + + " {\n" + + " \"AttributeId\": \"no.nav.abac.attributter.subject.felles.subjectType\",\n" + + " \"Value\": \"InternBruker\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"Environment\": {\n" + + " \"Attribute\": [\n" + + " {\n" + + " \"AttributeId\": \"no.nav.abac.attributter.environment.felles.pep_id\",\n" + + " \"Value\": \"\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"Action\": {\n" + + " \"Attribute\": [\n" + + " {\n" + + " \"AttributeId\": \"urn:oasis:names:tc:xacml:1.0:action:action-id\",\n" + + " \"Value\": \"update\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"Resource\": [\n" + + " {\n" + + " \"Attribute\": [\n" + + " {\n" + + " \"AttributeId\": \"no.nav.abac.attributter.resource.felles.resource_type\",\n" + + " \"Value\": \"no.nav.abac.attributter.resource.felles.person\"\n" + + " },\n" + + " {\n" + + " \"AttributeId\": \"no.nav.abac.attributter.resource.felles.domene\",\n" + + " \"Value\": \"veilarb\"\n" + + " },\n" + + " {\n" + + " \"AttributeId\": \"no.nav.abac.attributter.resource.felles.person.fnr\",\n" + + " \"Value\": \"" + deltakerFnr+ "\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " }"; + } + + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/ClearCacheInterceptor.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/ClearCacheInterceptor.java new file mode 100644 index 000000000..faaca9c25 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/ClearCacheInterceptor.java @@ -0,0 +1,41 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.adapter; + +import static java.util.Optional.ofNullable; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.AxsysService; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +; + +@Component +@RequiredArgsConstructor +@Profile(value = { Miljø.LOCAL, Miljø.DEV_FSS }) +public class ClearCacheInterceptor implements HandlerInterceptor, WebMvcConfigurer { + + public static final String CLEAR_CACHE_HEADER = "x-clear-cache"; + private final AbacAdapter abacAdapter; + private final AxsysService axsysService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (ofNullable(request.getHeader(CLEAR_CACHE_HEADER)).map(Boolean::valueOf).orElse(false)) { + abacAdapter.cacheEvict(); + axsysService.cacheEvict(); + } + return HandlerInterceptor.super.preHandle(request, response, handler); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(this); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/RestTemplateConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/RestTemplateConfiguration.java new file mode 100644 index 000000000..b06a0b5e0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/RestTemplateConfiguration.java @@ -0,0 +1,16 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.adapter; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfiguration { + @Bean + @Primary + public RestTemplate stsBasicAuthRestTemplate() { + return new RestTemplate(); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/AltinnTilgangsstyringProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/AltinnTilgangsstyringProperties.java new file mode 100644 index 000000000..88ee3a659 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/AltinnTilgangsstyringProperties.java @@ -0,0 +1,30 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.net.URI; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.altinn-tilgangsstyring") +public class AltinnTilgangsstyringProperties { + private URI uri; + private URI proxyUri; + private String altinnApiKey; + private String apiGwApiKey; + private String beOmRettighetBaseUrl; + private Integer ltsMidlertidigServiceCode; + private Integer ltsMidlertidigServiceEdition; + private Integer ltsVarigServiceCode; + private Integer ltsVarigServiceEdition; + private Integer arbtreningServiceCode; + private Integer arbtreningServiceEdition; + private Integer sommerjobbServiceCode; + private Integer sommerjobbServiceEdition; + private Integer inkluderingstilskuddServiceCode; + private Integer inkluderingstilskuddServiceEdition; + private Integer mentorServiceCode; + private Integer mentorServiceEdition; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/AltinnTilgangsstyringService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/AltinnTilgangsstyringService.java new file mode 100644 index 000000000..91e9c165f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/AltinnTilgangsstyringService.java @@ -0,0 +1,118 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring; + +import lombok.extern.slf4j.Slf4j; +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.AltinnConfig; +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.AltinnrettigheterProxyKlient; +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.AltinnrettigheterProxyKlientConfig; +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.ProxyConfig; +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.error.exceptions.AltinnrettigheterProxyKlientFallbackException; +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.model.*; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import no.nav.tag.tiltaksgjennomforing.exceptions.AltinnFeilException; +import no.nav.tag.tiltaksgjennomforing.exceptions.TiltaksgjennomforingException; +import no.nav.tag.tiltaksgjennomforing.utils.MultiValueMap; +import no.nav.tag.tiltaksgjennomforing.utils.Utils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Component +@Slf4j +public class AltinnTilgangsstyringService { + private final AltinnTilgangsstyringProperties altinnTilgangsstyringProperties; + private final AltinnrettigheterProxyKlient klient; + + public AltinnTilgangsstyringService( + AltinnTilgangsstyringProperties altinnTilgangsstyringProperties, + TokenUtils tokenUtils, + @Value("${spring.application.name}") String applicationName) { + + if (Utils.erNoenTomme(altinnTilgangsstyringProperties.getArbtreningServiceCode(), + altinnTilgangsstyringProperties.getArbtreningServiceEdition(), + altinnTilgangsstyringProperties.getLtsMidlertidigServiceCode(), + altinnTilgangsstyringProperties.getLtsMidlertidigServiceEdition(), + altinnTilgangsstyringProperties.getLtsVarigServiceCode(), + altinnTilgangsstyringProperties.getLtsVarigServiceEdition(), + altinnTilgangsstyringProperties.getSommerjobbServiceCode(), + altinnTilgangsstyringProperties.getSommerjobbServiceEdition())) { + throw new TiltaksgjennomforingException("Altinn konfigurasjon ikke komplett"); + } + this.altinnTilgangsstyringProperties = altinnTilgangsstyringProperties; + + String altinnProxyUrl = altinnTilgangsstyringProperties.getProxyUri().toString(); + String altinnProxyFallbackUrl = altinnTilgangsstyringProperties.getUri().toString(); + + AltinnrettigheterProxyKlientConfig proxyKlientConfig = new AltinnrettigheterProxyKlientConfig( + new ProxyConfig(applicationName, altinnProxyUrl), + new AltinnConfig( + altinnProxyFallbackUrl, + altinnTilgangsstyringProperties.getAltinnApiKey(), + altinnTilgangsstyringProperties.getApiGwApiKey() + ) + ); + this.klient = new AltinnrettigheterProxyKlient(proxyKlientConfig); + + } + + public Map> hentTilganger(Fnr fnr, HentArbeidsgiverToken hentArbeidsgiverToken) { + MultiValueMap tilganger = MultiValueMap.empty(); + String arbeidsgiverToken = hentArbeidsgiverToken.hentArbeidsgiverToken(); + + AltinnReportee[] arbeidstreningOrger = kallAltinn(altinnTilgangsstyringProperties.getArbtreningServiceCode(), altinnTilgangsstyringProperties.getArbtreningServiceEdition(), fnr, arbeidsgiverToken); + leggTil(tilganger, arbeidstreningOrger, Tiltakstype.ARBEIDSTRENING); + + AltinnReportee[] varigLtsOrger = kallAltinn(altinnTilgangsstyringProperties.getLtsVarigServiceCode(), altinnTilgangsstyringProperties.getLtsVarigServiceEdition(), fnr, arbeidsgiverToken); + leggTil(tilganger, varigLtsOrger, Tiltakstype.VARIG_LONNSTILSKUDD); + + AltinnReportee[] midlLtsOrger = kallAltinn(altinnTilgangsstyringProperties.getLtsMidlertidigServiceCode(), altinnTilgangsstyringProperties.getLtsMidlertidigServiceEdition(), fnr, arbeidsgiverToken); + leggTil(tilganger, midlLtsOrger, Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD); + + AltinnReportee[] sommerjobbOrger = kallAltinn(altinnTilgangsstyringProperties.getSommerjobbServiceCode(), altinnTilgangsstyringProperties.getSommerjobbServiceEdition(), fnr, arbeidsgiverToken); + leggTil(tilganger, sommerjobbOrger, Tiltakstype.SOMMERJOBB); + + AltinnReportee[] mentorOrger = kallAltinn(altinnTilgangsstyringProperties.getMentorServiceCode(), altinnTilgangsstyringProperties.getMentorServiceEdition(), fnr, arbeidsgiverToken); + leggTil(tilganger, mentorOrger, Tiltakstype.MENTOR); + + AltinnReportee[] inkluderingstilskuddOrger = kallAltinn(altinnTilgangsstyringProperties.getInkluderingstilskuddServiceCode(), altinnTilgangsstyringProperties.getInkluderingstilskuddServiceEdition(), fnr, + arbeidsgiverToken); + leggTil(tilganger, inkluderingstilskuddOrger, Tiltakstype.INKLUDERINGSTILSKUDD); + + return tilganger.toMap(); + } + + private void leggTil(MultiValueMap tilganger, AltinnReportee[] arbeidstreningOrger, Tiltakstype tiltakstype) { + for (AltinnReportee altinnReportee : arbeidstreningOrger) { + if (!altinnReportee.getType().equals("Enterprise")) { + tilganger.put(new BedriftNr(altinnReportee.getOrganizationNumber()), tiltakstype); + } + } + } + + public Set hentAltinnOrganisasjoner(Fnr fnr, HentArbeidsgiverToken hentArbeidsgiverToken) { + return new HashSet<>(List.of(kallAltinn(null, null, fnr, hentArbeidsgiverToken.hentArbeidsgiverToken()))); + } + + private AltinnReportee[] kallAltinn(Integer serviceCode, Integer serviceEdition, Fnr fnr, String arbeidsgiverToken) { + try { + List reportees; + if (serviceCode != null && serviceEdition != null) { + reportees = klient.hentOrganisasjoner( + new SelvbetjeningToken(arbeidsgiverToken), + new Subject(fnr.asString()), new ServiceCode(serviceCode.toString()), new ServiceEdition(serviceEdition.toString()), + true + ); + } else { + reportees = klient.hentOrganisasjoner(new SelvbetjeningToken(arbeidsgiverToken), new Subject(fnr.asString()), true); + } + return reportees.toArray(new AltinnReportee[0]); + + } catch (AltinnrettigheterProxyKlientFallbackException exception) { + log.warn("Feil ved kall mot Altinn.", exception); + throw new AltinnFeilException(); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/ArbeidsgiverTokenStrategyFactory.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/ArbeidsgiverTokenStrategyFactory.java new file mode 100644 index 000000000..c381f4c7e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/ArbeidsgiverTokenStrategyFactory.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring; + +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils; + +public interface ArbeidsgiverTokenStrategyFactory { + HentArbeidsgiverToken create(TokenUtils.Issuer issuer); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/ArbeidsgiverTokenStrategyFactoryImpl.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/ArbeidsgiverTokenStrategyFactoryImpl.java new file mode 100644 index 000000000..fddd1a401 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/ArbeidsgiverTokenStrategyFactoryImpl.java @@ -0,0 +1,29 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring; + +import lombok.RequiredArgsConstructor; +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenService; +import no.nav.security.token.support.client.spring.ClientConfigurationProperties; +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils.Issuer; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +@Profile(value = { Miljø.PROD_FSS, Miljø.DEV_FSS }) +public class ArbeidsgiverTokenStrategyFactoryImpl implements ArbeidsgiverTokenStrategyFactory { + private final TokenUtils tokenUtils; + private final OAuth2AccessTokenService oAuth2AccessTokenService; + private final ClientConfigurationProperties clientConfigurationProperties; + + public HentArbeidsgiverToken create(Issuer issuer) { + switch (issuer) { + case ISSUER_TOKENX: + return new HentArbeidsgiverTokenxImpl(oAuth2AccessTokenService, clientConfigurationProperties); + default: + throw new RuntimeException(); + } + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/ArbeidsgiverTokenStrategyFactoryLabsMock.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/ArbeidsgiverTokenStrategyFactoryLabsMock.java new file mode 100644 index 000000000..31eb3b90e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/ArbeidsgiverTokenStrategyFactoryLabsMock.java @@ -0,0 +1,16 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile(value = { Miljø.DEV_GCP_LABS }) +public class ArbeidsgiverTokenStrategyFactoryLabsMock implements ArbeidsgiverTokenStrategyFactory { + + @Override + public HentArbeidsgiverToken create(TokenUtils.Issuer issuer) { + return () -> ""; + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/BeOmAltinnRettighetUrlerController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/BeOmAltinnRettighetUrlerController.java new file mode 100644 index 000000000..c23ee4bf4 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/BeOmAltinnRettighetUrlerController.java @@ -0,0 +1,35 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring; + +import lombok.RequiredArgsConstructor; +import no.nav.security.token.support.core.api.Unprotected; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequestMapping("/be-om-altinn-rettighet-urler") +@Unprotected +@RequiredArgsConstructor +public class BeOmAltinnRettighetUrlerController { + private final AltinnTilgangsstyringProperties props; + + @GetMapping + public Map beOmRettighetUrler(@RequestParam("orgNr") String orgNr) { + return Map.of( + Tiltakstype.ARBEIDSTRENING, beOmRettighetUrl(orgNr), + Tiltakstype.INKLUDERINGSTILSKUDD, beOmRettighetUrl(orgNr), + Tiltakstype.MENTOR, beOmRettighetUrl(orgNr), + Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, beOmRettighetUrl(orgNr), + Tiltakstype.VARIG_LONNSTILSKUDD, beOmRettighetUrl(orgNr), + Tiltakstype.SOMMERJOBB, beOmRettighetUrl(orgNr) + ); + } + + private String beOmRettighetUrl(String orgNr) { + return props.getBeOmRettighetBaseUrl() + "&bedrift=" + orgNr; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/HentArbeidsgiverToken.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/HentArbeidsgiverToken.java new file mode 100644 index 000000000..96d4aae94 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/HentArbeidsgiverToken.java @@ -0,0 +1,5 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring; + +public interface HentArbeidsgiverToken { + public String hentArbeidsgiverToken (); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/HentArbeidsgiverTokenxImpl.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/HentArbeidsgiverTokenxImpl.java new file mode 100644 index 000000000..0bf211650 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/altinntilgangsstyring/HentArbeidsgiverTokenxImpl.java @@ -0,0 +1,19 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring; + +import lombok.RequiredArgsConstructor; +import no.nav.security.token.support.client.core.ClientProperties; +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenService; +import no.nav.security.token.support.client.spring.ClientConfigurationProperties; + +@RequiredArgsConstructor +public class HentArbeidsgiverTokenxImpl implements HentArbeidsgiverToken { + private final OAuth2AccessTokenService oAuth2AccessTokenService; + private final ClientConfigurationProperties clientConfigurationProperties; + + @Override + public String hentArbeidsgiverToken() { + ClientProperties clientProperties = clientConfigurationProperties.getRegistration().get("tokenx-altinn"); + String accessToken = oAuth2AccessTokenService.getAccessToken(clientProperties).getAccessToken(); + return accessToken; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AdminController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AdminController.java new file mode 100644 index 000000000..5618b08ac --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AdminController.java @@ -0,0 +1,193 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.security.token.support.core.api.ProtectedWithClaims; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.UtviklerTilgangProperties; +import no.nav.tag.tiltaksgjennomforing.exceptions.RessursFinnesIkkeException; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.HttpClientErrorException; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +@ProtectedWithClaims(issuer = "aad") +@RestController +@RequestMapping("/utvikler-admin/") +@Slf4j +@RequiredArgsConstructor +public class AdminController { + private final AvtaleRepository avtaleRepository; + private final TilskuddPeriodeRepository tilskuddPeriodeRepository; + private final UtviklerTilgangProperties utviklerTilgangProperties; + private final TokenUtils tokenUtils; + + private void sjekkTilgang() { + if (!tokenUtils.harAdGruppe(utviklerTilgangProperties.getGruppeTilgang())) { + throw new HttpClientErrorException(HttpStatus.FORBIDDEN); + } + } + + @PostMapping("reberegn") + public void reberegnLønnstilskudd(@RequestBody List avtaleIder) { + sjekkTilgang(); + for (UUID avtaleId : avtaleIder) { + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(); + avtale.reberegnLønnstilskudd(); + avtaleRepository.save(avtale); + } + } + + @PostMapping("/reberegn-mangler-dato-for-redusert-prosent/{migreringsDato}") + @Transactional + public void reberegnVarigLønnstilskuddSomIkkeHarRedusertDato(@PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate migreringsDato) { + sjekkTilgang(); + log.info("Starter jobb for å fikse manglende redusert prosent og redusert sum"); + // 1. Generer dato for redusert prosent og sumRedusert + List varigeLønnstilskudd = avtaleRepository.findAllByTiltakstypeAndGjeldendeInnhold_DatoForRedusertProsentNullAndGjeldendeInnhold_AvtaleInngåttNotNull(Tiltakstype.VARIG_LONNSTILSKUDD); + log.info("Fant {} varige lønnstilskudd avtaler som mangler redusert prosent til fiksing.", varigeLønnstilskudd.size()); + AtomicInteger antallUnder67 = new AtomicInteger(); + varigeLønnstilskudd.forEach(avtale -> { + LocalDate startDato = avtale.getGjeldendeInnhold().getStartDato(); + LocalDate sluttDato = avtale.getGjeldendeInnhold().getSluttDato(); + if (avtale.getGjeldendeInnhold().getLonnstilskuddProsent() > 67 + && startDato.isBefore(sluttDato.minusMonths(12)) + && avtale.getAnnullertTidspunkt() == null + && avtale.getAvbruttGrunn() == null + && avtale.getGjeldendeInnhold().getSumLonnstilskudd() != null) { + + avtale.reUtregnRedusert(); + avtale.nyeTilskuddsperioderEtterMigreringFraArena(migreringsDato, false); + avtaleRepository.save(avtale); + antallUnder67.getAndIncrement(); + } + }); + log.info("Ferdig kjørt reberegning av fiks for manglende redusert prosent og redusert sum på {} avtaler", antallUnder67); + } + + @PostMapping("/reberegn-mangler-dato-for-redusert-prosent-dry-run/{migreringsDato}") + public void reberegnVarigLønnstilskuddSomIkkeHarRedusertDatoDryRun(@PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate migreringsDato) { + sjekkTilgang(); + log.info("DRY-RUN: Starter DRY-RUN jobb for å fikse manglende redusert prosent og redusert sum"); + // 1. Generer dato for redusert prosent og sumRedusert + List varigeLønnstilskudd = avtaleRepository.findAllByTiltakstypeAndGjeldendeInnhold_DatoForRedusertProsentNullAndGjeldendeInnhold_AvtaleInngåttNotNull(Tiltakstype.VARIG_LONNSTILSKUDD); + log.info("DRY-RUN: Fant {} varige lønnstilskudd avtaler som mangler redusert prosent til fiksing.", varigeLønnstilskudd.size()); + AtomicInteger antallUnder67 = new AtomicInteger(); + varigeLønnstilskudd.forEach(avtale -> { + LocalDate startDato = avtale.getGjeldendeInnhold().getStartDato(); + LocalDate sluttDato = avtale.getGjeldendeInnhold().getSluttDato(); + + if (avtale.getGjeldendeInnhold().getLonnstilskuddProsent() > 67 + && startDato.isBefore(sluttDato.minusMonths(12)) + && avtale.getAnnullertTidspunkt() == null + && avtale.getAvbruttGrunn() == null + && avtale.getGjeldendeInnhold().getSumLonnstilskudd() != null) { + antallUnder67.getAndIncrement(); + } + }); + log.info("DRY-RUN: Fant {} avtaler som vil bli kjørt fiksing av redusert sum og sats på", antallUnder67.get()); + } + + @PostMapping("/annuller-tilskuddsperiode/{tilskuddsperiodeId}") + @Transactional + public void annullerTilskuddsperiode(@PathVariable("tilskuddsperiodeId") UUID id) { + sjekkTilgang(); + log.info("Annullerer tilskuddsperiode {}", id); + TilskuddPeriode tilskuddPeriode = tilskuddPeriodeRepository.findById(id).orElseThrow(RessursFinnesIkkeException::new); + Avtale avtale = tilskuddPeriode.getAvtale(); + avtale.annullerTilskuddsperiode(tilskuddPeriode); + tilskuddPeriodeRepository.save(tilskuddPeriode); + avtaleRepository.save(avtale); + } + + @PostMapping("/annuller-og-resend-tilskuddsperiode/{tilskuddsperiodeId}") + @Transactional + public void annullerOgResendTilskuddsperiode(@PathVariable("tilskuddsperiodeId") UUID id) { + sjekkTilgang(); + log.info("Annullerer tilskuddsperiode {} og resender som godkjent", id); + TilskuddPeriode tilskuddPeriode = tilskuddPeriodeRepository.findById(id).orElseThrow(RessursFinnesIkkeException::new); + Avtale avtale = tilskuddPeriode.getAvtale(); + avtale.annullerTilskuddsperiode(tilskuddPeriode); + avtale.lagNyGodkjentTilskuddsperiodeFraAnnullertPeriode(tilskuddPeriode); + avtaleRepository.save(avtale); + } + + @PostMapping("/annuller-og-generer-tilskuddsperiode/{tilskuddsperiodeId}") + @Transactional + public void annullerOgGenererTilskuddsperiode(@PathVariable("tilskuddsperiodeId") UUID id) { + sjekkTilgang(); + log.info("Annullerer tilskuddsperiode {} og genererer ny ubehandlet", id); + TilskuddPeriode tilskuddPeriode = tilskuddPeriodeRepository.findById(id).orElseThrow(RessursFinnesIkkeException::new); + Avtale avtale = tilskuddPeriode.getAvtale(); + avtale.annullerTilskuddsperiode(tilskuddPeriode); + avtale.lagNyTilskuddsperiodeFraAnnullertPeriode(tilskuddPeriode); + avtaleRepository.save(avtale); + } + + @PostMapping("/annuller-og-generer-behandlet-i-arena-perioder/{avtaleId}/{dato}") + @Transactional + public void annullerOgGenererBehandletIArenaPerioder(@PathVariable("avtaleId") UUID avtaleId, @PathVariable("dato") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate dato) { + sjekkTilgang(); + log.info("Annullerer tilskuddsperioder med sluttdato før {} på avtale {} og lager nye med status behandlet i arena", dato, avtaleId); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + List tilskuddsperioder = tilskuddPeriodeRepository.findAllByAvtaleAndSluttDatoBefore(avtale, dato); + log.info("Fant {} tilskuddsperioder som skal annulleres og genereres på nytt med behandlet i arena status", tilskuddsperioder.size()); + + tilskuddsperioder.stream().toList().forEach(tp -> { + avtale.annullerTilskuddsperiode(tp); + avtale.lagNyBehandletIArenaTilskuddsperiodeFraAnnullertPeriode(tp); + }); + + log.info("Avtale {} har nå {} perioder med status behandlet i arena", avtaleId, avtale.getTilskuddPeriode().stream().filter(tp -> tp.getStatus() == TilskuddPeriodeStatus.BEHANDLET_I_ARENA).count()); + avtaleRepository.save(avtale); + } + + @PostMapping("/lag-tilskuddsperioder-for-en-avtale/{avtaleId}/{migreringsDato}") + @Transactional + public void lagTilskuddsperioderPåEnAvtale(@PathVariable("avtaleId") UUID id, @PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate migreringsDato) { + sjekkTilgang(); + log.info("Lager tilskuddsperioder på en enkelt avtale {} fra dato {}", id, migreringsDato); + Avtale avtale = avtaleRepository.findById(id) + .orElseThrow(RessursFinnesIkkeException::new); + avtale.nyeTilskuddsperioderEtterMigreringFraArena(migreringsDato, false); + avtaleRepository.save(avtale); + } + + @PostMapping("/reberegn-ubehandlede-tilskuddsperioder/{avtaleId}") + @Transactional + public void reberegnUbehandledeTilskuddsperioder(@PathVariable("avtaleId") UUID avtaleId) { + sjekkTilgang(); + log.info("Reberegner ubehandlede tilskuddsperioder for avtale: {}", avtaleId); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + avtale.reberegnUbehandledeTilskuddsperioder(); + avtaleRepository.save(avtale); + } + + @PostMapping("/finn-avtaler-med-tilskuddsperioder-feil-datoer") + public void finnTilskuddsperioderMedFeilDatoer() { + sjekkTilgang(); + log.info("Finner avtaler som har tilskuddsperioder med mindre startdato enn en periode med lavere løpenummer"); + List midlertidige = avtaleRepository.findAllByTiltakstype(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD); + midlertidige.removeIf(a -> a.getGjeldendeInnhold().getAvtaleInngått() == null); + midlertidige.removeIf(a -> a.getTilskuddPeriode().size() == 0); + + midlertidige.forEach(avtale -> { + avtale.getTilskuddPeriode().forEach(tp -> { + if (tp.getLøpenummer() > 1) { + TilskuddPeriode forrigePeriode = avtale.getTilskuddPeriode().stream().filter(t -> t.getLøpenummer() == tp.getLøpenummer() - 1).collect(Collectors.toList()).stream().findFirst().orElseThrow(); + if (tp.getStartDato().isBefore(forrigePeriode.getStartDato())) { + log.warn("Tilskuddsperiode med id {} har startDato før startDatoen til forrige løpenummer!", tp.getId()); + } + } + }); + }); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AlleredeRegistrertAvtale.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AlleredeRegistrertAvtale.java new file mode 100644 index 000000000..6c1c48b1d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AlleredeRegistrertAvtale.java @@ -0,0 +1,70 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AlleredeRegistrertAvtale { + + private UUID id; + private Integer avtaleNr; + private Tiltakstype tiltakstype; + private Fnr deltakerFnr; + private BedriftNr bedriftNr; + private NavIdent veilederNavIdent; + private Status status; + private boolean opprettetAvArbeidsgiver; + + private LocalDate startDato; + private LocalDate sluttDato; + private LocalDateTime godkjentAvVeileder; + private LocalDateTime godkjentAvBeslutter; + private LocalDateTime avtaleInngått; + + private static List filtrerAvtaler(Stream avtaler) { + return avtaler.map(AlleredeRegistrertAvtale::setAvtaleFelter).toList(); + } + + public static AlleredeRegistrertAvtale setAvtaleFelter(Avtale avtale) { + AlleredeRegistrertAvtale alleredeRegistrertAvtale = new AlleredeRegistrertAvtale(); + alleredeRegistrertAvtale.setId(avtale.getId()); + alleredeRegistrertAvtale.setAvtaleNr(avtale.getAvtaleNr()); + alleredeRegistrertAvtale.setTiltakstype(avtale.getTiltakstype()); + alleredeRegistrertAvtale.setDeltakerFnr(avtale.getDeltakerFnr()); + alleredeRegistrertAvtale.setBedriftNr(avtale.getBedriftNr()); + alleredeRegistrertAvtale.setStatus(avtale.statusSomEnum()); + alleredeRegistrertAvtale.setVeilederNavIdent(avtale.getVeilederNavIdent()); + alleredeRegistrertAvtale.setOpprettetAvArbeidsgiver(avtale.isOpprettetAvArbeidsgiver()); + alleredeRegistrertAvtale.setStartDato(avtale.getGjeldendeInnhold().getStartDato()); + alleredeRegistrertAvtale.setSluttDato(avtale.getGjeldendeInnhold().getSluttDato()); + alleredeRegistrertAvtale.setGodkjentAvVeileder(avtale.getGjeldendeInnhold().getGodkjentAvVeileder()); + alleredeRegistrertAvtale.setGodkjentAvBeslutter(avtale.getGjeldendeInnhold().getGodkjentAvBeslutter()); + alleredeRegistrertAvtale.setAvtaleInngått(avtale.getGjeldendeInnhold().getAvtaleInngått()); + return alleredeRegistrertAvtale; + } + + public static List filtrerAvtaleDeltakerAlleredeErRegistrertPaa( + List alleAvtalerPaaDeltaker, + Tiltakstype tiltakstype + ) { + if(List.of(Tiltakstype.INKLUDERINGSTILSKUDD, Tiltakstype.MENTOR).contains(tiltakstype)) { + return filtrerAvtaler(alleAvtalerPaaDeltaker.stream().filter(avtale -> avtale.getTiltakstype().equals(tiltakstype))); + } + return filtrerAvtaler(alleAvtalerPaaDeltaker.stream().filter(avtale -> List.of( + Tiltakstype.SOMMERJOBB, + Tiltakstype.ARBEIDSTRENING, + Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, + Tiltakstype.VARIG_LONNSTILSKUDD + ).contains(avtale.getTiltakstype()))); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AnnullertInfo.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AnnullertInfo.java new file mode 100644 index 000000000..d97eb00bd --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AnnullertInfo.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Value; + +@Value +public class AnnullertInfo { + String annullertGrunn; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Arbeidsgiver.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Arbeidsgiver.java new file mode 100644 index 000000000..2cf39827a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Arbeidsgiver.java @@ -0,0 +1,239 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.model.AltinnReportee; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetArbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetBruker; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2Client; + +import no.nav.tag.tiltaksgjennomforing.exceptions.TilgangskontrollException; +import no.nav.tag.tiltaksgjennomforing.exceptions.VarighetDatoErTilbakeITidException; +import no.nav.tag.tiltaksgjennomforing.persondata.PdlRespons; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import static no.nav.tag.tiltaksgjennomforing.persondata.PersondataService.hentNavnFraPdlRespons; + +public class Arbeidsgiver extends Avtalepart { + private final Map> tilganger; + private final Set altinnOrganisasjoner; + private final PersondataService persondataService; + private final Norg2Client norg2Client; + + public Arbeidsgiver( + Fnr identifikator, + Set altinnOrganisasjoner, + Map> tilganger, + PersondataService persondataService, + Norg2Client norg2Client + ) { + super(identifikator); + this.altinnOrganisasjoner = altinnOrganisasjoner; + this.tilganger = tilganger; + this.persondataService = persondataService; + this.norg2Client = norg2Client; + } + + private static boolean avbruttForMerEnn12UkerSiden(Avtale avtale) { + return avtale.isAvbrutt() && avtale.getSistEndret() + .plus(84, ChronoUnit.DAYS) + .isBefore(Now.instant()); + } + + private static boolean annullertForMerEnn12UkerSiden(Avtale avtale) { + return avtale.getAnnullertTidspunkt() != null && avtale.getAnnullertTidspunkt() + .plus(84, ChronoUnit.DAYS) + .isBefore(Now.instant()); + } + + private static boolean sluttdatoPassertMedMerEnn12Uker(Avtale avtale) { + return avtale.erGodkjentAvVeileder() && avtale.getGjeldendeInnhold() + .getSluttDato().plusWeeks(12) + .isBefore(Now.localDate()); + } + + private static Avtale fjernAvbruttGrunn(Avtale avtale) { + avtale.setAvbruttGrunn(null); + return avtale; + } + + private static Avtale fjernAnnullertGrunn(Avtale avtale) { + avtale.setAnnullertGrunn(null); + return avtale; + } + + private static Avtale fjernKvalifiseringsgruppe(Avtale avtale) { + avtale.setKvalifiseringsgruppe(null); + avtale.setFormidlingsgruppe(null); + return avtale; + } + + @Override + protected void avvisDatoerTilbakeITid(Avtale avtale, LocalDate startDato, LocalDate sluttDato) { + if (!avtale.erUfordelt()) { + return; + } + if (startDato != null && startDato.isBefore(Now.localDate())) { + throw new VarighetDatoErTilbakeITidException(); + } + if (sluttDato != null && sluttDato.isBefore(Now.localDate())) { + throw new VarighetDatoErTilbakeITidException(); + } + } + + @Override + void godkjennForAvtalepart(Avtale avtale) { + avtale.godkjennForArbeidsgiver(getIdentifikator()); + } + + @Override + public boolean kanEndreAvtale() { + return true; + } + + @Override + public boolean erGodkjentAvInnloggetBruker(Avtale avtale) { + return avtale.erGodkjentAvArbeidsgiver(); + } + + @Override + boolean kanOppheveGodkjenninger(Avtale avtale) { + return !avtale.erGodkjentAvVeileder(); + } + + @Override + void opphevGodkjenningerSomAvtalepart(Avtale avtale) { + avtale.opphevGodkjenningerSomArbeidsgiver(); + } + + @Override + protected Avtalerolle rolle() { + return Avtalerolle.ARBEIDSGIVER; + } + + @Override + public InnloggetBruker innloggetBruker() { + return new InnloggetArbeidsgiver(getIdentifikator(), altinnOrganisasjoner, tilganger); + } + + @Override + public Collection identifikatorer() { + return tilganger.keySet(); + } + + @Override + public boolean harTilgangTilAvtale(Avtale avtale) { + if (sluttdatoPassertMedMerEnn12Uker(avtale)) { + return false; + } + if (avbruttForMerEnn12UkerSiden(avtale)) { + return false; + } + if (annullertForMerEnn12UkerSiden(avtale)) { + return false; + } + return harTilgangPåTiltakIBedrift(avtale.getBedriftNr(), avtale.getTiltakstype()); + } + + private boolean harTilgangPåTiltakIBedrift(BedriftNr bedriftNr, Tiltakstype tiltakstype) { + if (!tilganger.containsKey(bedriftNr)) { + return false; + } + Collection gyldigeTilgangerPåBedriftNr = tilganger.get(bedriftNr); + return gyldigeTilgangerPåBedriftNr.contains(tiltakstype); + } + + @Override + Page hentAlleAvtalerMedMuligTilgang(AvtaleRepository avtaleRepository, AvtalePredicate queryParametre, Pageable pageable) { + if (tilganger.isEmpty()) { + return Page.empty(); + } + Page avtaler; + if (queryParametre.getTiltakstype() != null) { + if (harTilgangPåTiltakIBedrift(queryParametre.getBedriftNr(), queryParametre.getTiltakstype())) + avtaler = avtaleRepository.findAllByBedriftNrInAndTiltakstype(Set.of(queryParametre.getBedriftNr()), queryParametre.getTiltakstype(), pageable); + else if (queryParametre.getBedriftNr() == null) { + avtaler = avtaleRepository.findAllByBedriftNrInAndTiltakstype(tilganger.keySet(), queryParametre.getTiltakstype(), pageable); + } else { // Bruker ba om informasjon på en bedrift hen ikke har tilgang til, og får dermed tom liste + avtaler = Page.empty(); + } + } else { + if (queryParametre.getBedriftNr() != null && tilganger.containsKey(queryParametre.getBedriftNr())) + avtaler = avtaleRepository.findAllByBedriftNrIn(Set.of(queryParametre.getBedriftNr()), pageable); + else if (queryParametre.getBedriftNr() == null) { + avtaler = avtaleRepository.findAllByBedriftNrIn(tilganger.keySet(), pageable); + } else { // Bruker ba om informasjon på en bedrift hen ikke har tilgang til, og får dermed tom liste + avtaler = Page.empty(); + } + } + return avtaler + .map(Arbeidsgiver::fjernAvbruttGrunn) + .map(Arbeidsgiver::fjernAnnullertGrunn); + } + + public List hentAvtalerForMinsideArbeidsgiver(AvtaleRepository avtaleRepository, BedriftNr bedriftNr) { + return avtaleRepository.findAllByBedriftNr(bedriftNr).stream() + .filter(this::harTilgang) + .map(Arbeidsgiver::fjernAvbruttGrunn) + .map(Arbeidsgiver::fjernAnnullertGrunn) + .collect(Collectors.toList()); + } + + private void tilgangTilBedriftVedOpprettelseAvAvtale(BedriftNr bedriftNr, Tiltakstype tiltakstype) { + if (!harTilgangPåTiltakIBedrift(bedriftNr, tiltakstype)) { + throw new TilgangskontrollException("Har ikke tilgang på tiltak i valgt bedrift"); + } + } + + public Avtale opprettAvtale(OpprettAvtale opprettAvtale) { + this.tilgangTilBedriftVedOpprettelseAvAvtale(opprettAvtale.getBedriftNr(), opprettAvtale.getTiltakstype()); + Avtale avtale = Avtale.arbeidsgiverOppretterAvtale(opprettAvtale); + leggEnheterVedOpprettelseAvAvtale(avtale); + + return avtale; + } + + public Avtale opprettMentorAvtale(OpprettMentorAvtale opprettMentorAvtale) { + this.tilgangTilBedriftVedOpprettelseAvAvtale( + opprettMentorAvtale.getBedriftNr(), + opprettMentorAvtale.getTiltakstype() + ); + Avtale avtale = Avtale.arbeidsgiverOppretterAvtale(opprettMentorAvtale); + leggEnheterVedOpprettelseAvAvtale(avtale); + + return avtale; + } + + protected void leggEnheterVedOpprettelseAvAvtale(Avtale avtale) { + final PdlRespons persondata = this.hentPersonDataForOpprettelseAvAvtale(avtale); + super.hentGeoEnhetFraNorg2(avtale, persondata, norg2Client); + super.hentOppfølingenhetNavnFraNorg2(avtale, norg2Client); + } + + private PdlRespons hentPersonDataForOpprettelseAvAvtale(Avtale avtale) { + final PdlRespons persondata = persondataService.hentPersondata(avtale.getDeltakerFnr()); + avtale.leggTilDeltakerNavn(hentNavnFraPdlRespons(persondata)); + return persondata; + } + + @Override + public Avtale hentAvtale(AvtaleRepository avtaleRepository, UUID avtaleId) { + Avtale avtale = super.hentAvtale(avtaleRepository, avtaleId); + fjernAvbruttGrunn(avtale); + fjernAnnullertGrunn(avtale); + fjernKvalifiseringsgruppe(avtale); + return avtale; + } + + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ArbeidstreningStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ArbeidstreningStrategy.java new file mode 100644 index 000000000..5e303b9b3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ArbeidstreningStrategy.java @@ -0,0 +1,36 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import java.util.HashMap; +import java.util.Map; + +public class ArbeidstreningStrategy extends BaseAvtaleInnholdStrategy { + + public ArbeidstreningStrategy(AvtaleInnhold avtaleInnhold) { + super(avtaleInnhold); + } + + @Override + public void endre(EndreAvtale nyAvtale) { + nyAvtale.getMaal().forEach(Maal::sjekkMaalLengde); + avtaleInnhold.getMaal().clear(); + avtaleInnhold.getMaal().addAll(nyAvtale.getMaal()); + avtaleInnhold.getMaal().forEach(m -> m.setAvtaleInnhold(avtaleInnhold)); + avtaleInnhold.setStillingstittel(nyAvtale.getStillingstittel()); + avtaleInnhold.setStillingStyrk08(nyAvtale.getStillingStyrk08()); + avtaleInnhold.setStillingKonseptId(nyAvtale.getStillingKonseptId()); + avtaleInnhold.setRefusjonKontaktperson(nyAvtale.getRefusjonKontaktperson()); + super.endre(nyAvtale); + } + + @Override + public Map alleFelterSomMåFyllesUt() { + HashMap alleFelterSomMåFyllesUt = new HashMap<>(); + alleFelterSomMåFyllesUt.putAll(super.alleFelterSomMåFyllesUt()); + alleFelterSomMåFyllesUt.put(AvtaleInnhold.Fields.stillingprosent, avtaleInnhold.getStillingprosent()); + alleFelterSomMåFyllesUt.put(AvtaleInnhold.Fields.stillingstittel, avtaleInnhold.getStillingstittel()); + alleFelterSomMåFyllesUt.put(AvtaleInnhold.Fields.arbeidsoppgaver, avtaleInnhold.getArbeidsoppgaver()); + alleFelterSomMåFyllesUt.put(AvtaleInnhold.Fields.maal, avtaleInnhold.getMaal()); + alleFelterSomMåFyllesUt.put(AvtaleInnhold.Fields.antallDagerPerUke, avtaleInnhold.getAntallDagerPerUke()); + return alleFelterSomMåFyllesUt; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ArenaRyddeAvtale.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ArenaRyddeAvtale.java new file mode 100644 index 000000000..cf1bb4213 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ArenaRyddeAvtale.java @@ -0,0 +1,22 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.*; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; +import java.time.LocalDate; +import java.util.UUID; + +@Entity +@Data +@NoArgsConstructor +public class ArenaRyddeAvtale { + @Id + private UUID id = UUID.randomUUID(); + @OneToOne + @JoinColumn(name = "avtale") + private Avtale avtale; + private LocalDate migreringsdato; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ArenaRyddeAvtaleRepository.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ArenaRyddeAvtaleRepository.java new file mode 100644 index 000000000..37029ce9c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ArenaRyddeAvtaleRepository.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.Optional; +import java.util.UUID; + +public interface ArenaRyddeAvtaleRepository extends JpaRepository, JpaSpecificationExecutor { + @Override + Optional findById(UUID id); + + Optional findByAvtale(Avtale avtale); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvbruttInfo.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvbruttInfo.java new file mode 100644 index 000000000..5463dbfa1 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvbruttInfo.java @@ -0,0 +1,23 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; + +import java.time.LocalDate; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AvbruttInfo { + private LocalDate avbruttDato; + private String avbruttGrunn; + + public void grunnErOppgitt() { + if (avbruttGrunn == null || avbruttGrunn.isEmpty()) { + throw new FeilkodeException(Feilkode.GRUNN_TIL_AVBRYTELSE); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvslagRequest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvslagRequest.java new file mode 100644 index 000000000..eb3c4ea72 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvslagRequest.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Value; + +import java.util.EnumSet; + +@Value +public class AvslagRequest { + EnumSet avslagsårsaker; + String avslagsforklaring; +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Avslags\303\245rsak.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Avslags\303\245rsak.java" new file mode 100644 index 000000000..4bfeb1a3e --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Avslags\303\245rsak.java" @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum Avslagsårsak { + FEIL_I_FAKTA("Feil i fakta"), + FEIL_I_REGELFORSTÅELSE("Feil i regelforståelse"), + ANNET("Annet"), + FEIL_I_PROSENTSATS("Feil i prosentsats"); + + private final String tekst; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Avtale.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Avtale.java new file mode 100644 index 000000000..f610dc199 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Avtale.java @@ -0,0 +1,1379 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; +import lombok.experimental.FieldNameConstants; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.events.*; +import no.nav.tag.tiltaksgjennomforing.avtale.startOgSluttDatoStrategy.StartOgSluttDatoStrategyFactory; +import no.nav.tag.tiltaksgjennomforing.enhet.Formidlingsgruppe; +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import no.nav.tag.tiltaksgjennomforing.exceptions.*; +import no.nav.tag.tiltaksgjennomforing.persondata.Navn; +import no.nav.tag.tiltaksgjennomforing.persondata.NavnFormaterer; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import no.nav.tag.tiltaksgjennomforing.utils.TelefonnummerValidator; +import no.nav.tag.tiltaksgjennomforing.utils.Utils; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.annotations.*; +import org.hibernate.annotations.Generated; +import org.hibernate.type.PostgresUUIDType; +import org.springframework.data.domain.AbstractAggregateRoot; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.*; +import javax.persistence.OrderBy; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static no.nav.tag.tiltaksgjennomforing.utils.DatoUtils.sisteDatoIMnd; +import static no.nav.tag.tiltaksgjennomforing.utils.Utils.sjekkAtIkkeNull; + +@Slf4j +@Data +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@Builder(toBuilder = true) +@Entity +@AllArgsConstructor +@NoArgsConstructor +@FieldNameConstants +@TypeDef(name = "postgres-uuid", + defaultForType = UUID.class, + typeClass = PostgresUUIDType.class) +public class Avtale extends AbstractAggregateRoot { + + @Convert(converter = FnrConverter.class) + private Fnr deltakerFnr; + @Convert(converter = FnrConverter.class) + private Fnr mentorFnr; + @Convert(converter = BedriftNrConverter.class) + private BedriftNr bedriftNr; + @Convert(converter = NavIdentConverter.class) + private NavIdent veilederNavIdent; + + @Enumerated(EnumType.STRING) + @Column(updatable = false) + private Tiltakstype tiltakstype; + + private LocalDateTime opprettetTidspunkt; + @Id + @EqualsAndHashCode.Include + private UUID id; + + @Generated(GenerationTime.INSERT) + private Integer avtaleNr; + + @OneToOne(cascade = CascadeType.ALL) + private AvtaleInnhold gjeldendeInnhold; + + private Instant sistEndret; + private Instant annullertTidspunkt; + private String annullertGrunn; + private boolean avbrutt; + private boolean slettemerket; + private LocalDate avbruttDato; + private String avbruttGrunn; + private boolean opprettetAvArbeidsgiver; + private String enhetGeografisk; + private String enhetsnavnGeografisk; + private String enhetOppfolging; + private String enhetsnavnOppfolging; + + + private boolean godkjentForEtterregistrering; + + @Enumerated(EnumType.STRING) + private Kvalifiseringsgruppe kvalifiseringsgruppe; + @Enumerated(EnumType.STRING) + private Formidlingsgruppe formidlingsgruppe; + + + @OneToMany(mappedBy = "avtale", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + @Fetch(FetchMode.SUBSELECT) + @SortNatural + @OrderBy("startDato ASC") + private SortedSet tilskuddPeriode = new TreeSet<>(); + private boolean feilregistrert; + + @JsonIgnore + @OneToOne(mappedBy = "avtale", fetch = FetchType.EAGER) + private ArenaRyddeAvtale arenaRyddeAvtale; + + private Avtale(OpprettAvtale opprettAvtale) { + sjekkAtIkkeNull(opprettAvtale.getDeltakerFnr(), "Deltakers fnr må være satt."); + sjekkAtIkkeNull(opprettAvtale.getBedriftNr(), "Arbeidsgivers bedriftnr må være satt."); + if (opprettAvtale.getDeltakerFnr().erUnder16år()) { + throw new FeilkodeException(Feilkode.SOMMERJOBB_IKKE_GAMMEL_NOK); + } + if (opprettAvtale.getTiltakstype() == Tiltakstype.SOMMERJOBB && opprettAvtale.getDeltakerFnr().erOver30årFørsteJanuar()) { + throw new FeilkodeException(Feilkode.SOMMERJOBB_FOR_GAMMEL); + } + + this.id = UUID.randomUUID(); + this.opprettetTidspunkt = Now.localDateTime(); + this.deltakerFnr = opprettAvtale.getDeltakerFnr(); + this.bedriftNr = opprettAvtale.getBedriftNr(); + this.tiltakstype = opprettAvtale.getTiltakstype(); + this.sistEndret = Now.instant(); + this.gjeldendeInnhold = AvtaleInnhold.nyttTomtInnhold(tiltakstype); + this.gjeldendeInnhold.setAvtale(this); + } + + private Avtale(OpprettMentorAvtale opprettMentorAvtale) { + sjekkAtIkkeNull(opprettMentorAvtale.getDeltakerFnr(), "Deltakers fnr må være satt."); + sjekkAtIkkeNull(opprettMentorAvtale.getBedriftNr(), "Arbeidsgivers bedriftnr må være satt."); + if (opprettMentorAvtale.getDeltakerFnr().erUnder16år()) { + throw new FeilkodeException(Feilkode.SOMMERJOBB_IKKE_GAMMEL_NOK); + } + if (opprettMentorAvtale.getTiltakstype() == Tiltakstype.SOMMERJOBB && opprettMentorAvtale.getDeltakerFnr().erOver30årFørsteJanuar()) { + throw new FeilkodeException(Feilkode.SOMMERJOBB_FOR_GAMMEL); + } + + this.id = UUID.randomUUID(); + this.opprettetTidspunkt = Now.localDateTime(); + this.deltakerFnr = opprettMentorAvtale.getDeltakerFnr(); + this.mentorFnr = opprettMentorAvtale.getMentorFnr(); + this.bedriftNr = opprettMentorAvtale.getBedriftNr(); + this.tiltakstype = opprettMentorAvtale.getTiltakstype(); + this.sistEndret = Now.instant(); + this.gjeldendeInnhold = AvtaleInnhold.nyttTomtInnhold(tiltakstype); + this.gjeldendeInnhold.setAvtale(this); + } + + public static Avtale veilederOppretterAvtale(OpprettAvtale opprettAvtale, NavIdent navIdent) { + Avtale avtale = new Avtale(opprettAvtale); + avtale.veilederNavIdent = sjekkAtIkkeNull(navIdent, "Veileders NAV-ident må være satt."); + avtale.registerEvent(new AvtaleOpprettetAvVeileder(avtale, navIdent)); + return avtale; + } + + protected boolean harOppfølgingsStatus() { + return (this.getEnhetOppfolging() != null || + this.getKvalifiseringsgruppe() != null || + this.getFormidlingsgruppe() != null); + } + + public static Avtale veilederOppretterAvtale(OpprettMentorAvtale opprettMentorAvtale, NavIdent navIdent) { + Avtale avtale = new Avtale(opprettMentorAvtale); + avtale.veilederNavIdent = sjekkAtIkkeNull(navIdent, "Veileders NAV-ident må være satt."); + avtale.registerEvent(new AvtaleOpprettetAvVeileder(avtale, navIdent)); + return avtale; + } + + public static Avtale arbeidsgiverOppretterAvtale(OpprettAvtale opprettAvtale) { + Avtale avtale = new Avtale(opprettAvtale); + avtale.opprettetAvArbeidsgiver = true; + avtale.registerEvent(new AvtaleOpprettetAvArbeidsgiver(avtale)); + return avtale; + } + + public static Avtale arbeidsgiverOppretterAvtale(OpprettMentorAvtale opprettMentorAvtale) { + Avtale avtale = new Avtale(opprettMentorAvtale); + avtale.opprettetAvArbeidsgiver = true; + avtale.registerEvent(new AvtaleOpprettetAvArbeidsgiver(avtale)); + return avtale; + } + + public void endreAvtale( + Instant sistEndret, + EndreAvtale nyAvtale, + Avtalerolle utfortAvRolle, + EnumSet tiltakstyperMedTilskuddsperioder, + Identifikator identifikator + ) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + sjekkOmAvtalenKanEndres(); + sjekkSistEndret(sistEndret); + sjekkStartOgSluttDato(nyAvtale.getStartDato(), nyAvtale.getSluttDato()); + getGjeldendeInnhold().endreAvtale(nyAvtale); + if (tiltakstyperMedTilskuddsperioder.contains(tiltakstype)) { + nyeTilskuddsperioder(); + } + sistEndretNå(); + registerEvent(new AvtaleEndret(this, utfortAvRolle, identifikator)); + } + + public void endreAvtale( + Instant sistEndret, + EndreAvtale nyAvtale, + Avtalerolle utfortAv, + EnumSet tiltakstyperMedTilskuddsperioder + ) { + endreAvtale(sistEndret, nyAvtale, utfortAv, tiltakstyperMedTilskuddsperioder, null); + } + + public void delMedAvtalepart(Avtalerolle avtalerolle) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + + String tlf = telefonnummerTilAvtalepart(avtalerolle); + if (!TelefonnummerValidator.erGyldigMobilnummer(tlf)) { + throw new FeilkodeException(Feilkode.UGYLDIG_TLF); + } + registerEvent(new AvtaleDeltMedAvtalepart(this, avtalerolle)); + } + + public void refusjonKlar(LocalDate fristForGodkjenning) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + registerEvent(new RefusjonKlar(this, fristForGodkjenning)); + } + + public void refusjonRevarsel(LocalDate fristForGodkjenning) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + registerEvent(new RefusjonKlarRevarsel(this, fristForGodkjenning)); + } + + public void refusjonFristForlenget() { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + registerEvent(new RefusjonFristForlenget(this)); + } + + public void refusjonKorrigert() { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + registerEvent(new RefusjonKorrigert(this)); + } + + private String telefonnummerTilAvtalepart(Avtalerolle avtalerolle) { + return switch (avtalerolle) { + case DELTAKER -> gjeldendeInnhold.getDeltakerTlf(); + case ARBEIDSGIVER -> gjeldendeInnhold.getArbeidsgiverTlf(); + case VEILEDER -> gjeldendeInnhold.getVeilederTlf(); + case MENTOR -> gjeldendeInnhold.getMentorTlf(); + default -> throw new IllegalArgumentException(); + }; + } + + @JsonProperty + public boolean erRyddeAvtale() { + if (arenaRyddeAvtale != null) { + return true; + } + return false; + } + + @JsonProperty + public boolean erLaast() { + return erGodkjentAvVeileder() && erGodkjentAvArbeidsgiver() && erGodkjentAvDeltaker(); + } + + @JsonProperty + public boolean erGodkjentAvDeltaker() { + return gjeldendeInnhold.getGodkjentAvDeltaker() != null; + } + + @JsonProperty + public boolean erGodkjentTaushetserklæringAvMentor() { + if (gjeldendeInnhold == null) return false; + return gjeldendeInnhold.getGodkjentTaushetserklæringAvMentor() != null; + } + + @JsonProperty + public boolean erGodkjentAvArbeidsgiver() { + return gjeldendeInnhold.getGodkjentAvArbeidsgiver() != null; + } + + @JsonProperty + public boolean erGodkjentAvVeileder() { + return gjeldendeInnhold.getGodkjentAvVeileder() != null; + } + + @JsonProperty + public boolean erAvtaleInngått() { + return gjeldendeInnhold.getAvtaleInngått() != null; + } + + @JsonProperty + public LocalDateTime godkjentAvDeltaker() { + return gjeldendeInnhold.getGodkjentAvDeltaker(); + } + + @JsonProperty + public LocalDateTime godkjentAvMentor() { + return gjeldendeInnhold.getGodkjentTaushetserklæringAvMentor(); + } + + @JsonProperty + public LocalDateTime godkjentAvArbeidsgiver() { + return gjeldendeInnhold.getGodkjentAvArbeidsgiver(); + } + + @JsonProperty + public LocalDateTime godkjentAvVeileder() { + return gjeldendeInnhold.getGodkjentAvVeileder(); + } + + @JsonProperty + public LocalDateTime godkjentAvBeslutter() { + return gjeldendeInnhold.getGodkjentAvBeslutter(); + } + + @JsonProperty + private LocalDateTime avtaleInngått() { + return gjeldendeInnhold.getAvtaleInngått(); + } + + @JsonProperty + private NavIdent godkjentAvNavIdent() { + return gjeldendeInnhold.getGodkjentAvNavIdent(); + } + + @JsonProperty + private NavIdent godkjentAvBeslutterNavIdent() { + return gjeldendeInnhold.getGodkjentAvBeslutterNavIdent(); + } + + @JsonProperty + private GodkjentPaVegneGrunn godkjentPaVegneGrunn() { + return gjeldendeInnhold.getGodkjentPaVegneGrunn(); + } + + @JsonProperty + private boolean godkjentPaVegneAv() { + return gjeldendeInnhold.isGodkjentPaVegneAv(); + } + + @JsonProperty + private GodkjentPaVegneAvArbeidsgiverGrunn godkjentPaVegneAvArbeidsgiverGrunn() { + return gjeldendeInnhold.getGodkjentPaVegneAvArbeidsgiverGrunn(); + } + + @JsonProperty + private boolean godkjentPaVegneAvArbeidsgiver() { + return gjeldendeInnhold.isGodkjentPaVegneAvArbeidsgiver(); + } + + private boolean skalBesluttes() { + return tiltakstype == Tiltakstype.SOMMERJOBB || tiltakstype == Tiltakstype.VARIG_LONNSTILSKUDD || tiltakstype == Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD; + } + + private void sjekkOmAvtalenKanEndres() { + if (erGodkjentAvDeltaker() || erGodkjentAvArbeidsgiver() || erGodkjentAvVeileder()) { + throw new TilgangskontrollException("Godkjenninger må oppheves før avtalen kan endres."); + } + } + + void opphevGodkjenningerSomArbeidsgiver() { + boolean varGodkjentAvDeltaker = erGodkjentAvDeltaker(); + opphevGodkjenninger(); + registerEvent(new GodkjenningerOpphevetAvArbeidsgiver(this, new GamleVerdier(varGodkjentAvDeltaker, false))); + } + + void opphevGodkjenningerSomVeileder() { + boolean varGodkjentAvDeltaker = erGodkjentAvDeltaker(); + boolean varGodkjentAvArbeidsgiver = erGodkjentAvArbeidsgiver(); + opphevGodkjenninger(); + registerEvent(new GodkjenningerOpphevetAvVeileder(this, new GamleVerdier(varGodkjentAvDeltaker, varGodkjentAvArbeidsgiver))); + } + + private void opphevGodkjenninger() { + gjeldendeInnhold.setGodkjentAvDeltaker(null); + gjeldendeInnhold.setGodkjentAvArbeidsgiver(null); + gjeldendeInnhold.setGodkjentAvVeileder(null); + gjeldendeInnhold.setGodkjentPaVegneAv(false); + gjeldendeInnhold.setGodkjentPaVegneGrunn(null); + gjeldendeInnhold.setGodkjentAvNavIdent(null); + sistEndretNå(); + } + + private void sistEndretNå() { + this.sistEndret = Now.instant(); + } + + void sjekkSistEndret(Instant sistEndret) { + if (sistEndret == null || sistEndret.isBefore(this.sistEndret)) { + throw new SamtidigeEndringerException(); + } + } + + //TODO TEST MEG + void godkjennForArbeidsgiver(Identifikator utfortAv) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + sjekkOmAltErKlarTilGodkjenning(); + if (erGodkjentAvArbeidsgiver()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_GODKJENNE_ARBEIDSGIVER_HAR_ALLEREDE_GODKJENT); + } + gjeldendeInnhold.setGodkjentAvArbeidsgiver(Now.localDateTime()); + sistEndretNå(); + registerEvent(new GodkjentAvArbeidsgiver(this, utfortAv)); + } + + void godkjennForVeileder(NavIdent utfortAv) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + sjekkOmAltErKlarTilGodkjenning(); + if (erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_GODKJENNE_VEILEDER_HAR_ALLEREDE_GODKJENT); + } + if (erUfordelt()) { + throw new AvtaleErIkkeFordeltException(); + } + if (this.getTiltakstype() == Tiltakstype.MENTOR && !erGodkjentTaushetserklæringAvMentor()) { + throw new FeilkodeException(Feilkode.MENTOR_MÅ_SIGNERE_TAUSHETSERKLÆRING); + } + if (!erGodkjentAvArbeidsgiver() || !erGodkjentAvDeltaker()) { + throw new VeilederSkalGodkjenneSistException(); + } + if (this.getTiltakstype() == Tiltakstype.SOMMERJOBB && + this.getDeltakerFnr().erOver30årFraOppstartDato(getGjeldendeInnhold().getStartDato())) { + throw new FeilkodeException(Feilkode.SOMMERJOBB_FOR_GAMMEL_FRA_OPPSTARTDATO); + } + else if (this.getTiltakstype() != Tiltakstype.SOMMERJOBB && this.getDeltakerFnr().erOver72ÅrFraSluttDato(getGjeldendeInnhold().getSluttDato())) { + throw new FeilkodeException(Feilkode.DELTAKER_72_AAR); + } + + LocalDateTime tidspunkt = Now.localDateTime(); + gjeldendeInnhold.setGodkjentAvVeileder(tidspunkt); + gjeldendeInnhold.setGodkjentAvNavIdent(new NavIdent(utfortAv.asString())); + if (!skalBesluttes()) { + avtaleInngått(tidspunkt, Avtalerolle.VEILEDER, utfortAv); + } + gjeldendeInnhold.setIkrafttredelsestidspunkt(tidspunkt); + sistEndretNå(); + registerEvent(new GodkjentAvVeileder(this, utfortAv)); + } + + private void avtaleInngått(LocalDateTime tidspunkt, Avtalerolle utførtAvRolle, NavIdent utførtAv) { + gjeldendeInnhold.setAvtaleInngått(tidspunkt); + registerEvent(new AvtaleInngått(this, utførtAvRolle, utførtAv)); + } + + void godkjennForVeilederOgDeltaker(NavIdent utfortAv, GodkjentPaVegneGrunn paVegneAvGrunn) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + sjekkOmAltErKlarTilGodkjenning(); + if (erGodkjentAvDeltaker()) { + throw new DeltakerHarGodkjentException(); + } + if (!erGodkjentAvArbeidsgiver()) { + throw new ArbeidsgiverSkalGodkjenneFørVeilederException(); + } + if (erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_GODKJENNE_VEILEDER_HAR_ALLEREDE_GODKJENT); + } + if (this.getTiltakstype() == Tiltakstype.SOMMERJOBB && + this.getDeltakerFnr().erOver30årFraOppstartDato(getGjeldendeInnhold().getStartDato())) { + throw new FeilkodeException(Feilkode.SOMMERJOBB_FOR_GAMMEL_FRA_OPPSTARTDATO); + } + if (this.getTiltakstype() == Tiltakstype.MENTOR && !erGodkjentTaushetserklæringAvMentor()) { + throw new FeilkodeException(Feilkode.MENTOR_MÅ_SIGNERE_TAUSHETSERKLÆRING); + } + if (this.getTiltakstype() == Tiltakstype.VARIG_LONNSTILSKUDD && this.getDeltakerFnr().erOver72ÅrFraSluttDato(getGjeldendeInnhold().getSluttDato())) { + throw new FeilkodeException(Feilkode.DELTAKER_72_AAR); + } else if (this.getTiltakstype() != Tiltakstype.VARIG_LONNSTILSKUDD && this.getDeltakerFnr().erOver67ÅrFraSluttDato(getGjeldendeInnhold().getSluttDato())) { + throw new FeilkodeException(Feilkode.DELTAKER_67_AAR); + } + + paVegneAvGrunn.valgtMinstEnGrunn(); + LocalDateTime tidspunkt = Now.localDateTime(); + gjeldendeInnhold.setGodkjentAvVeileder(tidspunkt); + gjeldendeInnhold.setGodkjentAvDeltaker(tidspunkt); + gjeldendeInnhold.setGodkjentPaVegneAv(true); + gjeldendeInnhold.setGodkjentPaVegneGrunn(paVegneAvGrunn); + gjeldendeInnhold.setGodkjentAvNavIdent(new NavIdent(utfortAv.asString())); + gjeldendeInnhold.setIkrafttredelsestidspunkt(tidspunkt); + if (!skalBesluttes()) { + avtaleInngått(tidspunkt, Avtalerolle.VEILEDER, utfortAv); + } + sistEndretNå(); + registerEvent(new GodkjentPaVegneAvDeltaker(this, utfortAv)); + } + + void godkjennForVeilederOgArbeidsgiver(NavIdent utfortAv, GodkjentPaVegneAvArbeidsgiverGrunn godkjentPaVegneAvArbeidsgiverGrunn) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + sjekkOmAltErKlarTilGodkjenning(); + if (tiltakstype != Tiltakstype.SOMMERJOBB && tiltakstype != Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD && tiltakstype != Tiltakstype.VARIG_LONNSTILSKUDD) { + throw new FeilkodeException(Feilkode.GODKJENN_PAA_VEGNE_AV_FEIL_TILTAKSTYPE); + } + if (erGodkjentAvArbeidsgiver()) { + throw new FeilkodeException(Feilkode.ARBEIDSGIVER_HAR_GODKJENT); + } + if (!erGodkjentAvDeltaker()) { + throw new FeilkodeException(Feilkode.DELTAKER_SKAL_GODKJENNE_FOER_VEILEDER); + } + if (erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_GODKJENNE_VEILEDER_HAR_ALLEREDE_GODKJENT); + } + if (tiltakstype == Tiltakstype.SOMMERJOBB && this.getDeltakerFnr().erOver30årFraOppstartDato(getGjeldendeInnhold().getStartDato())) { + throw new FeilkodeException(Feilkode.SOMMERJOBB_FOR_GAMMEL_FRA_OPPSTARTDATO); + } + godkjentPaVegneAvArbeidsgiverGrunn.valgtMinstEnGrunn(); + LocalDateTime tidspunkt = Now.localDateTime(); + gjeldendeInnhold.setGodkjentAvVeileder(tidspunkt); + gjeldendeInnhold.setGodkjentAvArbeidsgiver(tidspunkt); + gjeldendeInnhold.setGodkjentPaVegneAvArbeidsgiver(true); + gjeldendeInnhold.setGodkjentPaVegneAvArbeidsgiverGrunn(godkjentPaVegneAvArbeidsgiverGrunn); + gjeldendeInnhold.setGodkjentAvNavIdent(new NavIdent(utfortAv.asString())); + gjeldendeInnhold.setIkrafttredelsestidspunkt(tidspunkt); + if (!skalBesluttes()) { + avtaleInngått(tidspunkt, Avtalerolle.VEILEDER, utfortAv); + } + sistEndretNå(); + registerEvent(new GodkjentPaVegneAvArbeidsgiver(this, utfortAv)); + } + + public void godkjennForVeilederOgDeltakerOgArbeidsgiver(NavIdent utfortAv, GodkjentPaVegneAvDeltakerOgArbeidsgiverGrunn paVegneAvDeltakerOgArbeidsgiverGrunn) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + sjekkOmAltErKlarTilGodkjenning(); + if (tiltakstype != Tiltakstype.SOMMERJOBB && tiltakstype != Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD && tiltakstype != Tiltakstype.VARIG_LONNSTILSKUDD) { + throw new FeilkodeException(Feilkode.GODKJENN_PAA_VEGNE_AV_FEIL_TILTAKSTYPE); + } + if (erGodkjentAvDeltaker()) { + throw new DeltakerHarGodkjentException(); + } + if (erGodkjentAvArbeidsgiver()) { + throw new FeilkodeException(Feilkode.ARBEIDSGIVER_HAR_GODKJENT); + } + if (erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_GODKJENNE_VEILEDER_HAR_ALLEREDE_GODKJENT); + } + if (tiltakstype == Tiltakstype.SOMMERJOBB && this.getDeltakerFnr().erOver30årFraOppstartDato(getGjeldendeInnhold().getStartDato())) { + throw new FeilkodeException(Feilkode.SOMMERJOBB_FOR_GAMMEL_FRA_OPPSTARTDATO); + } + + + paVegneAvDeltakerOgArbeidsgiverGrunn.valgtMinstEnGrunn(); + LocalDateTime tidspunkt = Now.localDateTime(); + gjeldendeInnhold.setGodkjentAvVeileder(tidspunkt); + gjeldendeInnhold.setGodkjentAvDeltaker(tidspunkt); + gjeldendeInnhold.setGodkjentAvArbeidsgiver(tidspunkt); + gjeldendeInnhold.setGodkjentPaVegneAv(true); + gjeldendeInnhold.setGodkjentPaVegneAvArbeidsgiver(true); + gjeldendeInnhold.setGodkjentPaVegneGrunn(paVegneAvDeltakerOgArbeidsgiverGrunn.getGodkjentPaVegneAvDeltakerGrunn()); + gjeldendeInnhold.setGodkjentPaVegneAvArbeidsgiverGrunn(paVegneAvDeltakerOgArbeidsgiverGrunn.getGodkjentPaVegneAvArbeidsgiverGrunn()); + gjeldendeInnhold.setGodkjentAvNavIdent(new NavIdent(utfortAv.asString())); + gjeldendeInnhold.setIkrafttredelsestidspunkt(tidspunkt); + if (!skalBesluttes()) { + avtaleInngått(tidspunkt, Avtalerolle.VEILEDER, utfortAv); + } + sistEndretNå(); + registerEvent(new GodkjentPaVegneAvDeltakerOgArbeidsgiver(this, utfortAv)); + } + + void godkjennForDeltaker(Identifikator utfortAv) { + sjekkOmAltErKlarTilGodkjenning(); + if (erGodkjentAvDeltaker()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_GODKJENNE_DELTAKER_HAR_ALLEREDE_GODKJENT); + } + gjeldendeInnhold.setGodkjentAvDeltaker(Now.localDateTime()); + sistEndretNå(); + registerEvent(new GodkjentAvDeltaker(this, utfortAv)); + } + + void godkjennForMentor(Identifikator utfortAv) { + if (erGodkjentTaushetserklæringAvMentor()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_GODKJENNE_MENTOR_HAR_ALLEREDE_GODKJENT); + } + gjeldendeInnhold.setGodkjentTaushetserklæringAvMentor(Now.localDateTime()); + sistEndretNå(); + registerEvent(new SignertAvMentor(this, utfortAv)); + } + + void sjekkOmAltErKlarTilGodkjenning() { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + + if (!erAltUtfylt()) { + throw new AltMåVæreFyltUtException(); + } + if (List.of(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, Tiltakstype.VARIG_LONNSTILSKUDD, Tiltakstype.SOMMERJOBB).contains(tiltakstype) && + Utils.erNoenTomme(gjeldendeInnhold.getSumLonnstilskudd(), gjeldendeInnhold.getLonnstilskuddProsent(), tilskuddPeriode)) { + throw new FeilkodeException(Feilkode.MANGLER_BEREGNING); + } + if (veilederNavIdent == null) { + throw new FeilkodeException(Feilkode.MANGLER_VEILEDER_PÅ_AVTALE); + } + } + + @JsonProperty + public String status() { + return statusSomEnum().getBeskrivelse(); + } + + @JsonProperty + public Status statusSomEnum() { + if (getAnnullertTidspunkt() != null) { + return Status.ANNULLERT; + } else if (isAvbrutt()) { + return Status.AVBRUTT; + } else if (erAvtaleInngått() && (gjeldendeInnhold.getSluttDato().isBefore(Now.localDate()))) { + return Status.AVSLUTTET; + } else if (erAvtaleInngått() && (gjeldendeInnhold.getStartDato().isBefore(Now.localDate().plusDays(1)))) { + return Status.GJENNOMFØRES; + } else if (erAvtaleInngått()) { + return Status.KLAR_FOR_OPPSTART; + } else if (erAltUtfylt()) { + return Status.MANGLER_GODKJENNING; + } else { + return Status.PÅBEGYNT; + } + } + + @JsonProperty + public boolean kanAvbrytes() { + return !isAvbrutt(); + } + + @JsonProperty + public boolean kanGjenopprettes() { + return isAvbrutt(); + } + + public void annuller(Veileder veileder, String annullerGrunn) { + sjekkAtIkkeAvtalenInneholderUtbetaltTilskuddsperiode(); + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + + annullerTilskuddsperioder(); + setAnnullertTidspunkt(Now.instant()); + setAnnullertGrunn(annullerGrunn); + if (erUfordelt()) { + setVeilederNavIdent(veileder.getIdentifikator()); + } + if ("Feilregistrering".equals(annullerGrunn)) { + setFeilregistrert(true); + } + sistEndretNå(); + registerEvent(new AnnullertAvVeileder(this, veileder.getIdentifikator())); + } + + private void sjekkAtIkkeAvtalenInneholderUtbetaltTilskuddsperiode() { + if (this.getTilskuddPeriode().stream().anyMatch(TilskuddPeriode::erUtbetalt)) + throw new FeilkodeException(Feilkode.AVTALE_INNEHOLDER_UTBETALT_TILSKUDDSPERIODE); + if (this.getTilskuddPeriode().stream().anyMatch(TilskuddPeriode::erRefusjonGodkjent)) + throw new FeilkodeException(Feilkode.AVTALE_INNEHOLDER_TILSKUDDSPERIODE_MED_GODKJENT_REFUSJON); + } + + public void overtaAvtale(NavIdent nyNavIdent) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + NavIdent gammelNavIdent = this.getVeilederNavIdent(); + this.setVeilederNavIdent(nyNavIdent); + getGjeldendeInnhold().reberegnLønnstilskudd(); + sistEndretNå(); + if (gammelNavIdent == null) { + nyeTilskuddsperioder(); + this.registerEvent(new AvtaleOpprettetAvArbeidsgiverErFordelt(this)); + } else { + registerEvent(new AvtaleNyVeileder(this, gammelNavIdent)); + } + } + + private boolean erAltUtfylt() { + return felterSomIkkeErFyltUt().isEmpty(); + } + + public void leggTilBedriftNavn(String bedriftNavn) { + gjeldendeInnhold.setBedriftNavn(bedriftNavn); + } + + public void leggTilDeltakerNavn(Navn navn) { + NavnFormaterer formaterer = new NavnFormaterer(navn); + gjeldendeInnhold.setDeltakerFornavn(formaterer.getFornavn()); + gjeldendeInnhold.setDeltakerEtternavn(formaterer.getEtternavn()); + } + + @JsonProperty + public Set felterSomIkkeErFyltUt() { + return getGjeldendeInnhold().felterSomIkkeErFyltUt(); + } + + public void annullerTilskuddsperiode(TilskuddPeriode tilskuddsperiode) { + // Sjekk på refusjonens status + if (tilskuddsperiode.getRefusjonStatus() == RefusjonStatus.UTGÅTT) { + log.warn("Sender ikke annuleringsmelding for tilskuddsperiode {} med utgått refusjon.", tilskuddsperiode.getId()); + } else { + tilskuddsperiode.setStatus(TilskuddPeriodeStatus.ANNULLERT); + registerEvent(new TilskuddsperiodeAnnullert(this, tilskuddsperiode)); + } + } + + @JsonProperty + public boolean erUfordelt() { + return this.getVeilederNavIdent() == null; + } + + public void godkjennTilskuddsperiode(NavIdent beslutter, String enhet) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + + if (!erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.TILSKUDDSPERIODE_KAN_KUN_BEHANDLES_VED_INNGAATT_AVTALE); + } + if (enhet == null || !enhet.matches("^\\d{4}$")) { + throw new FeilkodeException(Feilkode.TILSKUDDSPERIODE_ENHET_FIRE_SIFFER); + } + if (beslutter.equals(gjeldendeInnhold.getGodkjentAvNavIdent())) { + throw new FeilkodeException(Feilkode.TILSKUDDSPERIODE_IKKE_GODKJENNE_EGNE); + } + TilskuddPeriode gjeldendePeriode = gjeldendeTilskuddsperiode(); + + // Sjekk om samme løpenummer allerede er godkjent og annullert. Trenger da en "ekstra" resendingsnummer + Integer resendingsnummer = finnResendingsNummer(gjeldendePeriode); + gjeldendePeriode.godkjenn(beslutter, enhet); + if (!erAvtaleInngått()) { + LocalDateTime tidspunkt = Now.localDateTime(); + godkjennForBeslutter(tidspunkt, beslutter); + avtaleInngått(tidspunkt, Avtalerolle.BESLUTTER, beslutter); + } + sistEndretNå(); + registerEvent(new TilskuddsperiodeGodkjent(this, gjeldendePeriode, beslutter, resendingsnummer)); + } + + private void godkjennForBeslutter(LocalDateTime tidspunkt, NavIdent beslutter) { + gjeldendeInnhold.setGodkjentAvBeslutter(tidspunkt); + gjeldendeInnhold.setGodkjentAvBeslutterNavIdent(beslutter); + } + + public void avslåTilskuddsperiode(NavIdent beslutter, EnumSet avslagsårsaker, String avslagsforklaring) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + + if (!erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.TILSKUDDSPERIODE_KAN_KUN_BEHANDLES_VED_INNGAATT_AVTALE); + } + TilskuddPeriode gjeldendePeriode = gjeldendeTilskuddsperiode(); + gjeldendePeriode.avslå(beslutter, avslagsårsaker, avslagsforklaring); + sistEndretNå(); + registerEvent(new TilskuddsperiodeAvslått(this, beslutter, gjeldendePeriode)); + } + + public void togglegodkjennEtterregistrering(NavIdent beslutter) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + if (erAvtaleInngått()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_MERKES_FOR_ETTERREGISTRERING_AVTALE_GODKJENT); + } + setGodkjentForEtterregistrering(!this.godkjentForEtterregistrering); + sistEndretNå(); + if (this.godkjentForEtterregistrering) { + registerEvent(new GodkjentForEtterregistrering(this, beslutter)); + } else { + registerEvent(new FjernetEtterregistrering(this, beslutter)); + } + } + + protected TilskuddPeriodeStatus getGjeldendeTilskuddsperiodestatus() { + TilskuddPeriode tilskuddPeriode = gjeldendeTilskuddsperiode(); + if (tilskuddPeriode == null) { + return null; + } + return tilskuddPeriode.getStatus(); + } + + public TilskuddPeriode tilskuddsperiode(int index) { + return tilskuddPeriode.toArray(new TilskuddPeriode[0])[index]; + } + + @JsonProperty + public TilskuddPeriode gjeldendeTilskuddsperiode() { + TreeSet aktiveTilskuddsperioder = new TreeSet(tilskuddPeriode.stream().filter(t -> t.isAktiv()).collect(Collectors.toSet())); + + if (aktiveTilskuddsperioder.isEmpty()) { + return null; + } + + // Finner første avslått + Optional førsteAvslått = aktiveTilskuddsperioder.stream().filter(tilskuddPeriode -> tilskuddPeriode.getStatus() == TilskuddPeriodeStatus.AVSLÅTT).findFirst(); + if (førsteAvslått.isPresent()) { + return førsteAvslått.get(); + } + + // Finn første som kan behandles + Optional førsteSomKanBehandles = aktiveTilskuddsperioder.stream().filter(TilskuddPeriode::kanBehandles).findFirst(); + if (førsteSomKanBehandles.isPresent()) { + return førsteSomKanBehandles.get(); + } + + // Finn siste godkjent + Optional sisteGodkjent = aktiveTilskuddsperioder.descendingSet().stream().filter(tilskuddPeriode -> tilskuddPeriode.getStatus() == TilskuddPeriodeStatus.GODKJENT) + .findFirst(); + if (sisteGodkjent.isPresent()) { + return sisteGodkjent.get(); + } + + return aktiveTilskuddsperioder.first(); + } + + public TreeSet finnTilskuddsperioderIkkeLukketForEndring() { + TreeSet tilskuddsperioder = tilskuddPeriode.stream() + .filter(t -> t.isAktiv() && (t.getStatus().equals(TilskuddPeriodeStatus.UBEHANDLET) || + t.getStatus().equals(TilskuddPeriodeStatus.AVSLÅTT))) + .collect(Collectors.toCollection(TreeSet::new)); + if (tilskuddsperioder.isEmpty()) { + return null; + } + return tilskuddsperioder; + } + + public void oppdatereKostnadsstedForTilskuddsperioder(NyttKostnadssted nyttKostnadssted) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + if (erAvtaleInngått()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_OPPDATERE_KOSTNADSSTED_INGAATT_AVTALE); + } + gjeldendeInnhold.setEnhetKostnadssted(nyttKostnadssted.getEnhet()); + gjeldendeInnhold.setEnhetsnavnKostnadssted(nyttKostnadssted.getEnhetsnavn()); + nyeTilskuddsperioder(); + } + + public void slettemerk(NavIdent utførtAv) { + this.setSlettemerket(true); + registerEvent(new AvtaleSlettemerket(this, utførtAv)); + } + + void forlengTilskuddsperioder(LocalDate gammelSluttDato, LocalDate nySluttDato) { + if (tilskuddPeriode.isEmpty()) { + return; + } + Optional periodeMedHøyestLøpenummer = tilskuddPeriode.stream().max(Comparator.comparing(tilskuddPeriode1 -> tilskuddPeriode1.getLøpenummer())); + if(periodeMedHøyestLøpenummer.isPresent()) { + TilskuddPeriode sisteTilskuddsperiode = periodeMedHøyestLøpenummer.get(); + if (sisteTilskuddsperiode.getStatus() == TilskuddPeriodeStatus.UBEHANDLET) { + // Kan utvide siste tilskuddsperiode hvis den er ubehandlet + tilskuddPeriode.remove(sisteTilskuddsperiode); + List nyeTilskuddperioder = beregnTilskuddsperioder(sisteTilskuddsperiode.getStartDato(), nySluttDato); + fikseLøpenumre(nyeTilskuddperioder, sisteTilskuddsperiode.getLøpenummer()); + tilskuddPeriode.addAll(nyeTilskuddperioder); + } else if (sisteTilskuddsperiode.getSluttDato().isBefore(sisteDatoIMnd(sisteTilskuddsperiode.getSluttDato())) && sisteTilskuddsperiode.getStatus() == TilskuddPeriodeStatus.GODKJENT && (!sisteTilskuddsperiode.erRefusjonGodkjent() && !sisteTilskuddsperiode.erUtbetalt())) { + annullerTilskuddsperiode(sisteTilskuddsperiode); + List nyeTilskuddperioder = beregnTilskuddsperioder(sisteTilskuddsperiode.getStartDato(), nySluttDato); + fikseLøpenumre(nyeTilskuddperioder, sisteTilskuddsperiode.getLøpenummer() + 1); + tilskuddPeriode.addAll(nyeTilskuddperioder); + } else { + // Regner ut nye perioder fra gammel avtaleslutt til ny avtaleslutt + List nyeTilskuddperioder = beregnTilskuddsperioder(gammelSluttDato.plusDays(1), nySluttDato); + fikseLøpenumre(nyeTilskuddperioder, sisteTilskuddsperiode.getLøpenummer() + 1); + tilskuddPeriode.addAll(nyeTilskuddperioder); + } + } + } + + private void fikseLøpenumre(List tilskuddperioder, int startPåLøpenummer) { + for (int i = 0; i < tilskuddperioder.size(); i++) { + tilskuddperioder.get(i).setLøpenummer(startPåLøpenummer + i); + } + } + + private void annullerTilskuddsperioder() { + for (TilskuddPeriode tilskuddsperiode : Set.copyOf(tilskuddPeriode)) { + TilskuddPeriodeStatus status = tilskuddsperiode.getStatus(); + if (status == TilskuddPeriodeStatus.UBEHANDLET) { + tilskuddPeriode.remove(tilskuddsperiode); + } else if (status == TilskuddPeriodeStatus.GODKJENT) { + annullerTilskuddsperiode(tilskuddsperiode); + } + } + } + + private void forkortTilskuddsperioder(LocalDate nySluttDato) { + for (TilskuddPeriode tilskuddsperiode : Set.copyOf(tilskuddPeriode)) { + TilskuddPeriodeStatus status = tilskuddsperiode.getStatus(); + if (tilskuddsperiode.getStartDato().isAfter(nySluttDato)) { + if (status == TilskuddPeriodeStatus.UBEHANDLET) { + tilskuddPeriode.remove(tilskuddsperiode); + } else if (status == TilskuddPeriodeStatus.GODKJENT) { + annullerTilskuddsperiode(tilskuddsperiode); + } + } else if (tilskuddsperiode.getSluttDato().isAfter(nySluttDato)) { + if (status == TilskuddPeriodeStatus.UBEHANDLET || status == TilskuddPeriodeStatus.GODKJENT) { + tilskuddsperiode.setSluttDato(nySluttDato); + tilskuddsperiode.setBeløp(beregnTilskuddsbeløp(tilskuddsperiode.getStartDato(), tilskuddsperiode.getSluttDato())); + if (status == TilskuddPeriodeStatus.GODKJENT) { + registerEvent(new TilskuddsperiodeForkortet(this, tilskuddsperiode)); + } + } + } + } + } + + void endreBeløpITilskuddsperioder() { + sendTilbakeTilBeslutter(); + tilskuddPeriode.stream().filter(t -> t.getStatus() == TilskuddPeriodeStatus.UBEHANDLET) + .forEach(t -> t.setBeløp(beregnTilskuddsbeløp(t.getStartDato(), t.getSluttDato()))); + } + + public void sendTilbakeTilBeslutter() { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + var rettede = tilskuddPeriode.stream() + .filter(TilskuddPeriode::isAktiv) + .filter(t -> t.getStatus() == TilskuddPeriodeStatus.AVSLÅTT) + .map(TilskuddPeriode::deaktiverOgLagNyUbehandlet).toList(); + tilskuddPeriode.addAll(rettede); + } + + private void sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt() { + if (erAnnullertEllerAvbrutt()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_ANNULLERT_AVTALE); + } + } + + @JsonProperty + public boolean erAnnullertEllerAvbrutt() { + return isAvbrutt() || annullertTidspunkt != null; + } + + private Integer beregnTilskuddsbeløp(LocalDate startDato, LocalDate sluttDato) { + return RegnUtTilskuddsperioderForAvtale.beløpForPeriode(startDato, + sluttDato, + gjeldendeInnhold.getDatoForRedusertProsent(), + gjeldendeInnhold.getSumLonnstilskudd(), + gjeldendeInnhold.getSumLønnstilskuddRedusert()); + } + + private List beregnTilskuddsperioder(LocalDate startDato, LocalDate sluttDato) { + List tilskuddsperioder = RegnUtTilskuddsperioderForAvtale.beregnTilskuddsperioderForAvtale( + id, + tiltakstype, + gjeldendeInnhold.getSumLonnstilskudd(), + startDato, + sluttDato, + gjeldendeInnhold.getLonnstilskuddProsent(), + gjeldendeInnhold.getDatoForRedusertProsent(), + gjeldendeInnhold.getSumLønnstilskuddRedusert()); + tilskuddsperioder.forEach(t -> t.setAvtale(this)); + tilskuddsperioder.forEach(t -> t.setEnhet(gjeldendeInnhold.getEnhetKostnadssted())); + tilskuddsperioder.forEach(t -> t.setEnhetsnavn(gjeldendeInnhold.getEnhetsnavnKostnadssted())); + return tilskuddsperioder; + } + + private void nyeTilskuddsperioder() { + if (erAvtaleInngått()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_LAGE_NYE_TILSKUDDSPRIODER_INNGAATT_AVTALE); + } + tilskuddPeriode.removeIf(t -> (t.getStatus() == TilskuddPeriodeStatus.UBEHANDLET) || (t.getStatus() == TilskuddPeriodeStatus.BEHANDLET_I_ARENA)); + if (Utils.erIkkeTomme(gjeldendeInnhold.getStartDato(), gjeldendeInnhold.getSluttDato(), gjeldendeInnhold.getSumLonnstilskudd())) { + List tilskuddsperioder = beregnTilskuddsperioder(gjeldendeInnhold.getStartDato(), gjeldendeInnhold.getSluttDato()); + if (arenaRyddeAvtale != null) { + LocalDate standardMigreringsdato = LocalDate.of(2023, 02, 01); + LocalDate migreringsdato = arenaRyddeAvtale.getMigreringsdato() != null ? arenaRyddeAvtale.getMigreringsdato() : standardMigreringsdato; + + tilskuddsperioder.forEach(periode -> { + // Set status BEHANDLET_I_ARENA på tilskuddsperioder før migreringsdato + // Eller skal det være startdato? Er jo den samme datoen som migreringsdato. hmm... + if (periode.getSluttDato().minusDays(1).isBefore(migreringsdato)) { + periode.setStatus(TilskuddPeriodeStatus.BEHANDLET_I_ARENA); + } + }); + } + fikseLøpenumre(tilskuddsperioder, 1); + tilskuddPeriode.addAll(tilskuddsperioder); + } + } + + private boolean sjekkRyddingAvTilskuddsperioder() { + if (Utils.erNoenTomme( + gjeldendeInnhold.getStartDato(), + gjeldendeInnhold.getSluttDato(), + gjeldendeInnhold.getSumLonnstilskudd(), + gjeldendeInnhold.getLonnstilskuddProsent(), + gjeldendeInnhold.getArbeidsgiveravgift(), + gjeldendeInnhold.getManedslonn(), + gjeldendeInnhold.getOtpSats())) { + return false; + } + // Statuser som skal få tilskuddsperioder + Status status = statusSomEnum(); + if (status == Status.ANNULLERT || status == Status.AVBRUTT) { + return false; + } + + return true; + } + + /** + * Avtaler (lønnstilskudd) som avsluttes i Arena må få tilskuddsperioder her. + *

    + * - Sjekk at avtalen ikke allerede har perioder (altså en pilotavtale) + * - Tilskuddsperioder lages fra startdato til sluttdato, de som er før dato for migrering settes til en ny status, f eks. BEHANDLET_I_ARENA + * - Sjekk logikk som skjer ved godkjenning av første perioden + * - Tar ikke høyde for perioder med lengde tre måneder som i arena + * - + */ + public boolean nyeTilskuddsperioderEtterMigreringFraArena(LocalDate migreringsDato, boolean dryRun) { + if (sjekkRyddingAvTilskuddsperioder()) { + + for (TilskuddPeriode tilskuddsperiode : Set.copyOf(tilskuddPeriode)) { + TilskuddPeriodeStatus status = tilskuddsperiode.getStatus(); + if (status == TilskuddPeriodeStatus.UBEHANDLET || status == TilskuddPeriodeStatus.BEHANDLET_I_ARENA) { + tilskuddPeriode.remove(tilskuddsperiode); + } else if (status == TilskuddPeriodeStatus.GODKJENT) { + + if (tilskuddsperiode.getRefusjonStatus() == RefusjonStatus.SENDT_KRAV || tilskuddsperiode.getRefusjonStatus() == RefusjonStatus.UTBETALT) { + log.error("Prøver å rydde tilskuddsperiode {} som har status: {}", tilskuddsperiode.getId(), tilskuddsperiode.getRefusjonStatus()); + } else { + annullerTilskuddsperiode(tilskuddsperiode); + } + + } else { + log.error("Prøver rydde tilskuddsperioder for en avtale, men statusen er ikke UBEHANDLET, eller GODKJENT (som blir annullert) på periode {}", tilskuddsperiode.getId()); + } + } + + List tilskuddsperioder = beregnTilskuddsperioder(gjeldendeInnhold.getStartDato(), gjeldendeInnhold.getSluttDato()); + tilskuddsperioder.forEach(periode -> { + // Set status BEHANDLET_I_ARENA på tilskuddsperioder før migreringsdato + // Eller skal det være startdato? Er jo den samme datoen som migreringsdato. hmm... + if (periode.getSluttDato().minusDays(1).isBefore(migreringsDato)) { + periode.setStatus(TilskuddPeriodeStatus.BEHANDLET_I_ARENA); + } + }); + fikseLøpenumre(tilskuddsperioder, 1); + if (!dryRun) { + tilskuddPeriode.addAll(tilskuddsperioder); + } + return true; + } else { + log.info("Avtale {} har allerede tilskuddsperioder eller en status som ikke skal ha perioder, eller er ikke tilstrekkelig fylt ut, genererer ikke nye", id); + return false; + } + } + + public void reberegnUbehandledeTilskuddsperioder() { + krevEnAvTiltakstyper(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, Tiltakstype.VARIG_LONNSTILSKUDD, Tiltakstype.SOMMERJOBB); + + // Finn første ubehandlede periode + //Optional førsteUbehandledeTilskuddsperiode = tilskuddPeriode.stream().filter(t -> t.getStatus() == TilskuddPeriodeStatus.UBEHANDLET).findFirst(); + // Fjern ubehandlede + for (TilskuddPeriode tilskuddsperiode : Set.copyOf(tilskuddPeriode)) { + TilskuddPeriodeStatus status = tilskuddsperiode.getStatus(); + if (status == TilskuddPeriodeStatus.UBEHANDLET) { + tilskuddPeriode.remove(tilskuddsperiode); + } + } + + // Lag nye fra og med siste ikke ubehandlet + en dag + LocalDate startDato; + List godkjentePerioder = tilskuddPeriode.stream().filter(t -> t.getStatus() == TilskuddPeriodeStatus.GODKJENT).sorted(Comparator.comparing(t -> t.getLøpenummer())).toList(); + + if (godkjentePerioder.size() != 0) { + startDato = godkjentePerioder.get(godkjentePerioder.size() - 1).getSluttDato().plusDays(1); + } else { + startDato = tilskuddPeriode.first().getStartDato(); + } + + List nyetilskuddsperioder = beregnTilskuddsperioder(startDato, gjeldendeInnhold.getSluttDato()); + tilskuddPeriode.addAll(nyetilskuddsperioder); + fikseLøpenumre(tilskuddPeriode.stream().toList(), 1); + } + + public void lagNyGodkjentTilskuddsperiodeFraAnnullertPeriode(TilskuddPeriode annullertTilskuddPeriode) { + krevEnAvTiltakstyper(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, Tiltakstype.VARIG_LONNSTILSKUDD, Tiltakstype.SOMMERJOBB); + if(annullertTilskuddPeriode.getStatus() != TilskuddPeriodeStatus.ANNULLERT) { + throw new FeilkodeException(Feilkode.TILSKUDDSPERIODE_ER_ALLEREDE_BEHANDLET); + } + TilskuddPeriode nyTilskuddsperiode = annullertTilskuddPeriode.deaktiverOgLagNyUbehandlet(); + annullertTilskuddPeriode.setAktiv(true); + nyTilskuddsperiode.setStatus(TilskuddPeriodeStatus.GODKJENT); + nyTilskuddsperiode.setGodkjentAvNavIdent(annullertTilskuddPeriode.getGodkjentAvNavIdent()); + nyTilskuddsperiode.setGodkjentTidspunkt(annullertTilskuddPeriode.getGodkjentTidspunkt()); + nyTilskuddsperiode.setEnhet(annullertTilskuddPeriode.getEnhet()); + Integer resendingsnummer = finnResendingsNummer(annullertTilskuddPeriode); + registerEvent(new TilskuddsperiodeGodkjent(this, nyTilskuddsperiode, nyTilskuddsperiode.getGodkjentAvNavIdent(), resendingsnummer)); + tilskuddPeriode.add(nyTilskuddsperiode); + } + + private Integer finnResendingsNummer(TilskuddPeriode gjeldendePeriode) { + Integer resendingsnummer = null; + for (TilskuddPeriode periode : tilskuddPeriode) { + if(periode.getStatus() == TilskuddPeriodeStatus.ANNULLERT && periode.getLøpenummer().equals(gjeldendePeriode.getLøpenummer())) { + if(resendingsnummer == null) { + resendingsnummer = 0; + } + resendingsnummer++; + } + } + return resendingsnummer; + } + + public void lagNyTilskuddsperiodeFraAnnullertPeriode(TilskuddPeriode annullertTilskuddPeriode) { + krevEnAvTiltakstyper(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, Tiltakstype.VARIG_LONNSTILSKUDD, Tiltakstype.SOMMERJOBB); + if(annullertTilskuddPeriode.getStatus() != TilskuddPeriodeStatus.ANNULLERT) { + throw new FeilkodeException(Feilkode.TILSKUDDSPERIODE_ER_ALLEREDE_BEHANDLET); + } + TilskuddPeriode nyUbehandletPeriode = annullertTilskuddPeriode.deaktiverOgLagNyUbehandlet(); + annullertTilskuddPeriode.setAktiv(true); + tilskuddPeriode.add(nyUbehandletPeriode); + } + + public void lagNyBehandletIArenaTilskuddsperiodeFraAnnullertPeriode(TilskuddPeriode annullertTilskuddPeriode) { + krevEnAvTiltakstyper(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, Tiltakstype.VARIG_LONNSTILSKUDD, Tiltakstype.SOMMERJOBB); + if(annullertTilskuddPeriode.getStatus() != TilskuddPeriodeStatus.ANNULLERT) { + throw new FeilkodeException(Feilkode.TILSKUDDSPERIODE_ER_ALLEREDE_BEHANDLET); + } + TilskuddPeriode nyUbehandletPeriode = annullertTilskuddPeriode.deaktiverOgLagNyUbehandlet(); + nyUbehandletPeriode.setStatus(TilskuddPeriodeStatus.BEHANDLET_I_ARENA); + annullertTilskuddPeriode.setAktiv(true); + tilskuddPeriode.add(nyUbehandletPeriode); + } + + public void forkortAvtale(LocalDate nySluttDato, String grunn, String annetGrunn, NavIdent utførtAv) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + + if (!erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_FORKORTE_IKKE_GODKJENT_AVTALE); + } + if (!nySluttDato.isBefore(gjeldendeInnhold.getSluttDato())) { + throw new FeilkodeException(Feilkode.KAN_IKKE_FORKORTE_ETTER_SLUTTDATO); + } + // Kan ikke forkorte før en utbetalt/sendtkrav tilskuddsperiode + TreeSet aktiveTilskuddsperioder = new TreeSet(tilskuddPeriode.stream().filter(t -> t.isAktiv()).collect(Collectors.toSet())); + Optional sisteUtbetalt = aktiveTilskuddsperioder.descendingSet().stream().filter(tilskuddPeriode -> ( + tilskuddPeriode.getRefusjonStatus() == RefusjonStatus.SENDT_KRAV || + tilskuddPeriode.getRefusjonStatus() == RefusjonStatus.UTBETALT || + tilskuddPeriode.getRefusjonStatus() == RefusjonStatus.UTBETALING_FEILET || + tilskuddPeriode.getRefusjonStatus() == RefusjonStatus.GODKJENT_MINUSBELØP || + tilskuddPeriode.getRefusjonStatus() == RefusjonStatus.GODKJENT_NULLBELØP) + ).max(Comparator.comparing(tilskuddPeriode1 -> tilskuddPeriode1.getStartDato())); + if (sisteUtbetalt.isPresent() && nySluttDato.isBefore(sisteUtbetalt.get().getSluttDato())) { + throw new FeilkodeException(Feilkode.KAN_IKKE_FORKORTE_FOR_UTBETALT_TILSKUDDSPERIODE); + } + sjekkStartOgSluttDato(gjeldendeInnhold.getStartDato(), nySluttDato); + if (StringUtils.isBlank(grunn) || (grunn.equals("Annet") && StringUtils.isBlank(annetGrunn))) { + throw new FeilkodeException(Feilkode.KAN_IKKE_FORKORTE_GRUNN_MANGLER); + } + AvtaleInnhold nyAvtaleInnholdVersjon = getGjeldendeInnhold().nyGodkjentVersjon(AvtaleInnholdType.FORKORTE); + gjeldendeInnhold = nyAvtaleInnholdVersjon; + getGjeldendeInnhold().endreSluttDato(nySluttDato); + sendTilbakeTilBeslutter(); + forkortTilskuddsperioder(nySluttDato); + sistEndretNå(); + registerEvent(new AvtaleForkortet(this, nyAvtaleInnholdVersjon, nySluttDato, grunn, annetGrunn, utførtAv)); + } + + public void forlengAvtale(LocalDate nySluttDato, NavIdent utførtAv) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + + if (!erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_FORLENGE_IKKE_GODKJENT_AVTALE); + } + if (!nySluttDato.isAfter(gjeldendeInnhold.getSluttDato())) { + throw new FeilkodeException(Feilkode.KAN_IKKE_FORLENGE_FEIL_SLUTTDATO); + } + sjekkStartOgSluttDato(gjeldendeInnhold.getStartDato(), nySluttDato); + var gammelSluttDato = gjeldendeInnhold.getSluttDato(); + AvtaleInnhold nyVersjon = getGjeldendeInnhold().nyGodkjentVersjon(AvtaleInnholdType.FORLENGE); + gjeldendeInnhold = nyVersjon; + getGjeldendeInnhold().endreSluttDato(nySluttDato); + sendTilbakeTilBeslutter(); + forlengTilskuddsperioder(gammelSluttDato, nySluttDato); + sistEndretNå(); + registerEvent(new AvtaleForlenget(this, utførtAv)); + } + + + private void sjekkStartOgSluttDato(LocalDate startDato, LocalDate sluttDato) { + StartOgSluttDatoStrategyFactory.create(getTiltakstype(), getKvalifiseringsgruppe()).sjekkStartOgSluttDato(startDato, sluttDato, isGodkjentForEtterregistrering(), erAvtaleInngått()); + } + + public void endreTilskuddsberegning(EndreTilskuddsberegning tilskuddsberegning, NavIdent utførtAv) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + + krevEnAvTiltakstyper(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, Tiltakstype.VARIG_LONNSTILSKUDD, Tiltakstype.SOMMERJOBB); + if (!erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_OKONOMI_IKKE_GODKJENT_AVTALE); + } + if (Utils.erNoenTomme(tilskuddsberegning.getArbeidsgiveravgift(), + tilskuddsberegning.getFeriepengesats(), + tilskuddsberegning.getManedslonn(), + tilskuddsberegning.getOtpSats())) { + + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_OKONOMI_UGYLDIG_INPUT); + } + gjeldendeInnhold = getGjeldendeInnhold().nyGodkjentVersjon(AvtaleInnholdType.ENDRE_TILSKUDDSBEREGNING); + getGjeldendeInnhold().endreTilskuddsberegning(tilskuddsberegning); + endreBeløpITilskuddsperioder(); + sistEndretNå(); + getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + registerEvent(new TilskuddsberegningEndret(this, utførtAv)); + } + + // Metode for å rydde opp i beregnede felter som ikke har blitt satt etter at lønnstilskuddsprosent manuelt i databasen har blitt satt inn + public void reberegnLønnstilskudd() { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + krevEnAvTiltakstyper(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, Tiltakstype.VARIG_LONNSTILSKUDD, Tiltakstype.SOMMERJOBB); + if (gjeldendeInnhold.getSumLonnstilskudd() == null && Utils.erIkkeTomme( + gjeldendeInnhold.getLonnstilskuddProsent(), + gjeldendeInnhold.getArbeidsgiveravgift(), + gjeldendeInnhold.getFeriepengesats(), + gjeldendeInnhold.getManedslonn(), + gjeldendeInnhold.getOtpSats())) { + getGjeldendeInnhold().reberegnLønnstilskudd(); + return; + } + throw new FeilkodeException(Feilkode.KAN_IKKE_REBEREGNE); + } + + public void reUtregnRedusert() { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + krevEnAvTiltakstyper(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, Tiltakstype.VARIG_LONNSTILSKUDD, Tiltakstype.SOMMERJOBB); + if (Utils.erIkkeTomme( + gjeldendeInnhold.getStartDato(), + gjeldendeInnhold.getSluttDato(), + gjeldendeInnhold.getSumLonnstilskudd(), + gjeldendeInnhold.getLonnstilskuddProsent(), + gjeldendeInnhold.getArbeidsgiveravgift(), + gjeldendeInnhold.getFeriepengesats(), + gjeldendeInnhold.getManedslonn(), + gjeldendeInnhold.getOtpSats())) { + + getGjeldendeInnhold().reberegnRedusertProsentOgRedusertLonnstilskudd(); + return; + } + throw new FeilkodeException(Feilkode.KAN_IKKE_REBEREGNE); + } + + private void krevEnAvTiltakstyper(Tiltakstype... tiltakstyper) { + if (Stream.of(tiltakstyper).noneMatch(t -> t == tiltakstype)) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_FEIL_TILTAKSTYPE); + } + } + + public void endreKontaktInformasjon(EndreKontaktInformasjon endreKontaktInformasjon, NavIdent utførtAv) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + + if (!erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_KONTAKTINFO_GRUNN_IKKE_GODKJENT_AVTALE); + } + if (Utils.erNoenTomme(endreKontaktInformasjon.getDeltakerFornavn(), + endreKontaktInformasjon.getDeltakerEtternavn(), + endreKontaktInformasjon.getDeltakerTlf(), endreKontaktInformasjon.getVeilederFornavn(), + endreKontaktInformasjon.getVeilederEtternavn(), + endreKontaktInformasjon.getVeilederTlf(), + endreKontaktInformasjon.getArbeidsgiverFornavn(), + endreKontaktInformasjon.getArbeidsgiverEtternavn(), + endreKontaktInformasjon.getArbeidsgiverTlf()) + ) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_KONTAKTINFO_GRUNN_MANGLER); + } + + gjeldendeInnhold = getGjeldendeInnhold().nyGodkjentVersjon(AvtaleInnholdType.ENDRE_KONTAKTINFO); + getGjeldendeInnhold().endreKontaktInfo(endreKontaktInformasjon); + getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + sistEndretNå(); + sendTilbakeTilBeslutter(); + registerEvent(new KontaktinformasjonEndret(this, utførtAv)); + } + + public void endreStillingsbeskrivelse(EndreStillingsbeskrivelse endreStillingsbeskrivelse, NavIdent utførtAv) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + + if (!erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_STILLINGSBESKRIVELSE_GRUNN_IKKE_GODKJENT_AVTALE); + } + if (Utils.erNoenTomme(endreStillingsbeskrivelse.getStillingstittel(), + endreStillingsbeskrivelse.getArbeidsoppgaver(), + endreStillingsbeskrivelse.getStillingStyrk08(), + endreStillingsbeskrivelse.getStillingKonseptId(), + endreStillingsbeskrivelse.getStillingprosent(), + endreStillingsbeskrivelse.getAntallDagerPerUke()) + ) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_STILLINGSBESKRIVELSE_GRUNN_MANGLER); + } + gjeldendeInnhold = getGjeldendeInnhold().nyGodkjentVersjon(AvtaleInnholdType.ENDRE_STILLING); + getGjeldendeInnhold().endreStillingsInfo(endreStillingsbeskrivelse); + getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + sistEndretNå(); + sendTilbakeTilBeslutter(); + registerEvent(new StillingsbeskrivelseEndret(this, utførtAv)); + } + + public void endreOppfølgingOgTilrettelegging(EndreOppfølgingOgTilrettelegging endreOppfølgingOgTilrettelegging, NavIdent utførtAv) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + + if (!erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_OPPFØLGING_OG_TILRETTELEGGING_GRUNN_IKKE_GODKJENT_AVTALE); + } + if (Utils.erNoenTomme(endreOppfølgingOgTilrettelegging.getOppfolging(), + endreOppfølgingOgTilrettelegging.getTilrettelegging()) + ) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_OPPFØLGING_OG_TILRETTELEGGING_GRUNN_MANGLER); + } + gjeldendeInnhold = gjeldendeInnhold.nyGodkjentVersjon(AvtaleInnholdType.ENDRE_OPPFØLGING_OG_TILRETTELEGGING); + gjeldendeInnhold.endreOppfølgingOgTilretteleggingInfo(endreOppfølgingOgTilrettelegging); + gjeldendeInnhold.setIkrafttredelsestidspunkt(Now.localDateTime()); + sistEndretNå(); + sendTilbakeTilBeslutter(); + registerEvent(new OppfølgingOgTilretteleggingEndret(this, utførtAv)); + } + + public void endreMål(EndreMål endreMål, NavIdent utførtAv) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + + krevEnAvTiltakstyper(Tiltakstype.ARBEIDSTRENING); + if (!erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_MAAL_IKKE_INNGAATT_AVTALE); + } + if (endreMål.getMaal().isEmpty()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_MAAL_TOM_LISTE); + } + for (Maal m : endreMål.getMaal()) { + if (Utils.erNoenTomme(m.getBeskrivelse(), m.getKategori())) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_MAAL_IKKE_BESKRIVELSE_ELLER_KATEGORI); + } + } + gjeldendeInnhold = getGjeldendeInnhold().nyGodkjentVersjon(AvtaleInnholdType.ENDRE_MÅL); + getGjeldendeInnhold().getMaal().clear(); + List nyeMål = endreMål.getMaal().stream().map(m -> new Maal().setId(UUID.randomUUID()).setBeskrivelse(m.getBeskrivelse()).setKategori(m.getKategori())).collect(Collectors.toList()); + getGjeldendeInnhold().getMaal().addAll(nyeMål); + getGjeldendeInnhold().getMaal().forEach(m -> m.setAvtaleInnhold(getGjeldendeInnhold())); + getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + sistEndretNå(); + sendTilbakeTilBeslutter(); + registerEvent(new MålEndret(this, utførtAv)); + } + + public void endreInkluderingstilskudd(EndreInkluderingstilskudd endreInkluderingstilskudd, NavIdent utførtAv) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + + krevEnAvTiltakstyper(Tiltakstype.INKLUDERINGSTILSKUDD); + if (!erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_INKLUDERINGSTILSKUDD_IKKE_INNGAATT_AVTALE); + } + if (endreInkluderingstilskudd.getInkluderingstilskuddsutgift().isEmpty()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_INKLUDERINGSTILSKUDD_TOM_LISTE); + } + if (endreInkluderingstilskudd.inkluderingstilskuddTotalBeløp() > 136000) { + throw new FeilkodeException(Feilkode.INKLUDERINGSTILSKUDD_SUM_FOR_HØY); + } + for (Inkluderingstilskuddsutgift i : endreInkluderingstilskudd.getInkluderingstilskuddsutgift()) { + if (Utils.erNoenTomme(i.getBeløp(), i.getType())) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_INKLUDERINGSTILSKUDD_IKKE_BELOP_ELLER_TYPE); + } + } + List inkluderingstilskuddsutgifterPåForrigeVersjon = getGjeldendeInnhold().getInkluderingstilskuddsutgift(); + List forrigeVersjonFraKlient = endreInkluderingstilskudd.getInkluderingstilskuddsutgift().stream().filter(e -> e.getId() != null).collect(Collectors.toList()); + + // Sjekk at det er like mange utgifter på forrige versjon som det er id'er i request. Hvis ikke er ikke frontend i sync + if (inkluderingstilskuddsutgifterPåForrigeVersjon.size() != forrigeVersjonFraKlient.size()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_INKLUDERINGSTILSKUDD_TOM_LISTE); + } + gjeldendeInnhold = getGjeldendeInnhold().nyGodkjentVersjon(AvtaleInnholdType.ENDRE_INKLUDERINGSTILSKUDD); + + List nye = endreInkluderingstilskudd.getInkluderingstilskuddsutgift().stream().filter(e -> e.getId() == null).collect(Collectors.toList()); + List nyeInkluderingstilskuddsutgifter = nye.stream().map(m -> new Inkluderingstilskuddsutgift().setId(UUID.randomUUID()).setBeløp(m.getBeløp()).setType(m.getType())).collect(Collectors.toList()); + + getGjeldendeInnhold().getInkluderingstilskuddsutgift().addAll(nyeInkluderingstilskuddsutgifter); + getGjeldendeInnhold().getInkluderingstilskuddsutgift().forEach(i -> i.setAvtaleInnhold(getGjeldendeInnhold())); + getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + sistEndretNå(); + sendTilbakeTilBeslutter(); + registerEvent(new InkluderingstilskuddEndret(this, utførtAv)); + } + + public void endreOmMentor(EndreOmMentor endreOmMentor, NavIdent utførtAv) { + sjekkAtIkkeAvtaleErAnnullertEllerAvbrutt(); + krevEnAvTiltakstyper(Tiltakstype.MENTOR); + + if (!erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_OM_MENTOR_IKKE_INNGAATT_AVTALE); + } + if (Utils.erNoenTomme(endreOmMentor.getMentorFornavn(), endreOmMentor.getMentorEtternavn(), + endreOmMentor.getMentorTlf(), endreOmMentor.getMentorTimelonn(), + endreOmMentor.getMentorAntallTimer(), endreOmMentor.getMentorOppgaver())) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_OM_MENTOR_UGYLDIG_INPUT); + } + gjeldendeInnhold = getGjeldendeInnhold().nyGodkjentVersjon(AvtaleInnholdType.ENDRE_OM_MENTOR); + getGjeldendeInnhold().endreOmMentor(endreOmMentor); + getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + sistEndretNå(); + sendTilbakeTilBeslutter(); + registerEvent(new OmMentorEndret(this, utførtAv)); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleController.java new file mode 100644 index 000000000..8a8b8bb29 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleController.java @@ -0,0 +1,759 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import io.micrometer.core.annotation.Timed; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.security.token.support.core.api.Protected; +import no.nav.tag.tiltaksgjennomforing.ApiBeskrivelse; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggingService; +import no.nav.tag.tiltaksgjennomforing.dokgen.DokgenService; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2Client; +import no.nav.tag.tiltaksgjennomforing.enhet.VeilarbArenaClient; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; +import no.nav.tag.tiltaksgjennomforing.exceptions.RessursFinnesIkkeException; +import no.nav.tag.tiltaksgjennomforing.exceptions.TiltaksgjennomforingException; +import no.nav.tag.tiltaksgjennomforing.okonomi.KontoregisterService; +import no.nav.tag.tiltaksgjennomforing.orgenhet.EregService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.*; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +import static java.util.Map.entry; +import static no.nav.tag.tiltaksgjennomforing.avtale.AvtaleSorterer.getSortingOrderForPageable; +import static no.nav.tag.tiltaksgjennomforing.utils.Utils.lagUri; + +@Protected +@RestController +@RequestMapping("/avtaler") +@Timed +@Slf4j +@RequiredArgsConstructor +public class AvtaleController { + + private final AvtaleRepository avtaleRepository; + private final AvtaleInnholdRepository avtaleInnholdRepository; + private final ArenaRyddeAvtaleRepository arenaRyddeAvtaleRepository; + private final InnloggingService innloggingService; + private final EregService eregService; + private final Norg2Client norg2Client; + private final KontoregisterService kontoregisterService; + private final DokgenService dokgenService; + private final TilskuddsperiodeConfig tilskuddsperiodeConfig; + private final SalesforceKontorerConfig salesforceKontorerConfig; + private final VeilarbArenaClient veilarbArenaClient; + private final FilterSokRepository filterSokRepository; + + @ApiBeskrivelse("Hent detaljer for avtale om arbeidsmarkedstiltak") + @GetMapping("/{avtaleId}") + public Avtale hent(@PathVariable("avtaleId") UUID id, @CookieValue("innlogget-part") Avtalerolle innloggetPart) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + return avtalepart.hentAvtale(avtaleRepository, id); + } + + @GetMapping("/{avtaleId}/vis-salesforce-dialog") + public Boolean visSalesforceDialog(@PathVariable("avtaleId") UUID id, @CookieValue("innlogget-part") Avtalerolle innloggetPart) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + Avtale avtale = avtalepart.hentAvtale(avtaleRepository, id); + if(salesforceKontorerConfig.getEnheter().contains(avtale.getEnhetOppfolging()) && + avtale.getTiltakstype() == Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD && + (avtale.statusSomEnum() == Status.GJENNOMFØRES || avtale.statusSomEnum() == Status.AVSLUTTET)) { + return true; + } + return false; + } + + @ApiBeskrivelse("Hent detaljer for avtale om arbeidsmarkedstiltak") + @GetMapping("/avtaleNr/{avtaleNr}") + public Avtale hentFraAvtaleNr(@PathVariable("avtaleNr") int avtaleNr, @CookieValue("innlogget-part") Avtalerolle innloggetPart) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + return avtalepart.hentAvtaleFraAvtaleNr(avtaleRepository, avtaleNr); + } + + @GetMapping("/{avtaleId}/versjoner") + public List hentVersjoner( + @PathVariable("avtaleId") UUID id, + @CookieValue("innlogget-part") Avtalerolle innloggetPart + ) { + return innloggingService + .hentAvtalepart(innloggetPart) + .hentAvtaleVersjoner( + avtaleRepository, + avtaleInnholdRepository, + id + ); + } + + @ApiBeskrivelse("Hent liste over avtaler om arbeidsmarkedstiltak") + @GetMapping + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + public Map hentAlleAvtalerInnloggetBrukerHarTilgangTil( + + AvtalePredicate queryParametre, + @CookieValue("innlogget-part") Avtalerolle innloggetPart, + @RequestParam(value = "sorteringskolonne", required = false, defaultValue = Avtale.Fields.sistEndret) String sorteringskolonne, + @RequestParam(value = "page", required = false, defaultValue = "0") Integer page, + @RequestParam(value = "size", required = false, defaultValue = "10") Integer size + ) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + Pageable pageable = PageRequest.of(Math.abs(page), Math.abs(size), Sort.by(getSortingOrderForPageable(sorteringskolonne))); + Map avtaler = avtalepart.hentAlleAvtalerMedLesetilgang( + avtaleRepository, + queryParametre, + sorteringskolonne, + pageable + + ); + return avtaler; + } + + @ApiBeskrivelse("Hent liste over avtaler om arbeidsmarkedstiltak") + @GetMapping("/sok") + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + public Map hentAlleAvtalerInnloggetBrukerHarTilgangTilMedGet( + @RequestParam(value = "sokId") String filterSokId, + @CookieValue("innlogget-part") Avtalerolle innloggetPart, + @RequestParam(value = "sorteringskolonne", required = false, defaultValue = Avtale.Fields.sistEndret) String sorteringskolonne, + @RequestParam(value = "page", required = false, defaultValue = "0") Integer page, + @RequestParam(value = "size", required = false, defaultValue = "10") Integer size + ) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + + FilterSok filterSok = filterSokRepository.findFilterSokBySokId(filterSokId).orElse(null); + if (filterSok != null) { + filterSok.setAntallGangerSokt(filterSok.getAntallGangerSokt() + 1); + filterSok.setSistSoktTidspunkt(LocalDateTime.now()); + filterSokRepository.save(filterSok); + AvtalePredicate avtalePredicate = filterSok.getAvtalePredicate(); + + Pageable pageable = PageRequest.of(Math.abs(page), Math.abs(size), Sort.by(getSortingOrderForPageable(sorteringskolonne))); + Map avtaler = avtalepart.hentAlleAvtalerMedLesetilgang( + avtaleRepository, + avtalePredicate, + sorteringskolonne, + pageable + + ); + HashMap stringObjectHashMap = new HashMap<>(avtaler); + stringObjectHashMap.put("sokeParametere", avtalePredicate); + stringObjectHashMap.put("sokId", filterSok.getSokId()); + stringObjectHashMap.put("sorteringskolonne", sorteringskolonne); + return stringObjectHashMap; + + } else { + return Map.ofEntries( + entry("avtaler", List.of()), + entry("size", 0), + entry("currentPage", 0), + entry("totalItems", 0), + entry("totalPages", 0), + entry("sokeParametere", new AvtalePredicate()), + entry("sorteringskolonne", "sistEndret"), + entry("sokId", "") + ); + } + } + + @ApiBeskrivelse("Hent liste over avtaler om arbeidsmarkedstiltak") + @PostMapping("/sok") + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + public Map hentAlleAvtalerInnloggetBrukerHarTilgangTilMedPost( + @RequestBody AvtalePredicate queryParametre, + @CookieValue("innlogget-part") Avtalerolle innloggetPart, + @RequestParam(value = "sorteringskolonne", required = false, defaultValue = Avtale.Fields.sistEndret) String sorteringskolonne, + @RequestParam(value = "page", required = false, defaultValue = "0") Integer page, + @RequestParam(value = "size", required = false, defaultValue = "10") Integer size + ) { + + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + Pageable pageable = PageRequest.of(Math.abs(page), Math.abs(size), Sort.by(getSortingOrderForPageable(sorteringskolonne))); + Map avtaler = avtalepart.hentAlleAvtalerMedLesetilgang( + avtaleRepository, + queryParametre, + sorteringskolonne, + pageable + + ); + HashMap stringObjectHashMap = new HashMap<>(avtaler); + stringObjectHashMap.put("sokeParametere", queryParametre); + stringObjectHashMap.put("sorteringskolonne", sorteringskolonne); + + + FilterSok filterSokiDb = filterSokRepository.findFilterSokBySokId(queryParametre.generateHash()).orElse(null); + if (filterSokiDb != null) { + stringObjectHashMap.put("sokId", filterSokiDb.getSokId()); + filterSokiDb.setAntallGangerSokt(filterSokiDb.getAntallGangerSokt() + 1); + filterSokiDb.setSistSoktTidspunkt(LocalDateTime.now()); + filterSokRepository.save(filterSokiDb); + if (!filterSokiDb.erLik(queryParametre)) { + log.error("Kollisjon i søkId: " + filterSokiDb.getSokId()); + } + } else { + FilterSok filterSok = new FilterSok(queryParametre); + filterSokRepository.save(filterSok); + stringObjectHashMap.put("sokId", filterSok.getSokId()); + } + return stringObjectHashMap; + } + + @ApiBeskrivelse("Hent liste over avtaler om arbeidsmarkedstiltak") + @GetMapping("/beslutter-liste") + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + public Map finnGodkjenteAvtalerMedTilskuddsperiodestatusOgNavEnheterListe( + AvtalePredicate queryParametre, + @RequestParam(value = "sorteringskolonne", required = false, defaultValue = "startDato") String sorteringskolonne, + @RequestParam(value = "page", required = false, defaultValue = "0") Integer page, + @RequestParam(value = "size", required = false, defaultValue = "20") Integer size, + @RequestParam(value = "sorteringOrder", required = false, defaultValue = "ASC") String sorteringOrder + ) { + Beslutter beslutter = innloggingService.hentBeslutter(); + Page avtaler = beslutter.finnGodkjenteAvtalerMedTilskuddsperiodestatusOgNavEnheterListe( + avtaleRepository, + queryParametre, + sorteringskolonne, + page, + size, + sorteringOrder + ); + List avtalerMedTilgang = avtaler.getContent().stream() + .filter(oversiktDTO -> beslutter.harTilgangTilFnr( + oversiktDTO.getDeltakerFnr())).toList(); + + return Map.ofEntries( + entry("avtaler", avtalerMedTilgang), + entry("size", avtaler.getSize()), + entry("currentPage", avtaler.getNumber()), + entry("totalItems", avtaler.getTotalElements()), + entry("totalPages", avtaler.getTotalPages()) + ); + } + + @GetMapping("/{avtaleId}/pdf") + public HttpEntity hentAvtalePdf( + @PathVariable("avtaleId") UUID avtaleId, + @CookieValue("innlogget-part") Avtalerolle innloggetPart + ) { + Avtale avtale = innloggingService.hentAvtalepart(innloggetPart).hentAvtale(avtaleRepository, avtaleId); + if (!avtale.erGodkjentAvVeileder()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_LASTE_NED_PDF); + } + byte[] avtalePdf = dokgenService.avtalePdf(avtale, innloggetPart); + HttpHeaders header = new HttpHeaders(); + header.setContentType(MediaType.APPLICATION_PDF); + header.set(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=Avtale om " + avtale.getTiltakstype().getBeskrivelse() + ".pdf"); + header.setContentLength(avtalePdf.length); + return new HttpEntity<>(avtalePdf, header); + } + + @PutMapping("/{avtaleId}") + @Transactional + public ResponseEntity endreAvtale( + @PathVariable("avtaleId") UUID avtaleId, + @RequestHeader(HttpHeaders.IF_UNMODIFIED_SINCE) Instant sistEndret, + @RequestBody EndreAvtale endreAvtale, + @CookieValue("innlogget-part") Avtalerolle innloggetPart) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + Avtale avtale = avtaleRepository.findById(avtaleId) + .orElseThrow(RessursFinnesIkkeException::new); + + avtalepart.endreAvtale(sistEndret, endreAvtale, avtale, tilskuddsperiodeConfig.getTiltakstyper()); + Avtale lagretAvtale = avtaleRepository.save(avtale); + return ResponseEntity.ok().lastModified(lagretAvtale.getSistEndret()).build(); + } + + @ApiBeskrivelse("Test endring av avtale om arbeidsmarkedstiltak") + @PutMapping("/{avtaleId}/dry-run") + public Avtale endreAvtaleDryRun( + @PathVariable("avtaleId") UUID avtaleId, + @RequestHeader(HttpHeaders.IF_UNMODIFIED_SINCE) Instant sistEndret, + @RequestBody EndreAvtale endreAvtale, @CookieValue("innlogget-part") Avtalerolle innloggetPart + ) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + Avtale avtale = avtaleRepository.findById(avtaleId) + .orElseThrow(RessursFinnesIkkeException::new); + avtalepart.endreAvtale(sistEndret, endreAvtale, avtale, tilskuddsperiodeConfig.getTiltakstyper()); + return avtale; + } + + @PostMapping("/{avtaleId}/opphev-godkjenninger") + @Transactional + public void opphevGodkjenninger( + @PathVariable("avtaleId") UUID avtaleId, + @CookieValue("innlogget-part") Avtalerolle innloggetPart + ) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + avtalepart.opphevGodkjenninger(avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/godkjenn") + @Transactional + public void godkjenn( + @PathVariable("avtaleId") UUID avtaleId, + @RequestHeader(HttpHeaders.IF_UNMODIFIED_SINCE) Instant sistEndret, + @CookieValue("innlogget-part") Avtalerolle innloggetPart + ) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + avtalepart.godkjennAvtale(sistEndret, avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/mentorGodkjennTaushetserklæring") + @Transactional + public void mentorGodkjennTaushetserklæring( + @PathVariable("avtaleId") UUID avtaleId, + @RequestHeader(HttpHeaders.IF_UNMODIFIED_SINCE) Instant sistEndret, + @CookieValue("innlogget-part") Avtalerolle innloggetPart + ) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + if (!avtalepart.rolle().equals(Avtalerolle.MENTOR)) + throw new TiltaksgjennomforingException("Du må være mentor for å signere her"); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + avtalepart.godkjennAvtale(sistEndret, avtale); + avtaleRepository.save(avtale); + } + + // Arbeidsgiver-operasjoner + + @ApiBeskrivelse("Hent liste over avtaler om arbeidsmarkedstiltak") + @GetMapping("/min-side-arbeidsgiver") + public List hentAlleAvtalerForMinSideArbeidsgiver(@RequestParam("bedriftNr") BedriftNr bedriftNr) { + Arbeidsgiver arbeidsgiver = innloggingService.hentArbeidsgiver(); + return arbeidsgiver.hentAvtalerForMinsideArbeidsgiver(avtaleRepository, bedriftNr); + } + + @GetMapping(path = "/{avtaleId}/kontonummer-arbeidsgiver") + public String hentBedriftKontonummer( + @PathVariable("avtaleId") UUID avtaleId, + @CookieValue("innlogget-part") Avtalerolle innloggetPart + ) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + Avtale avtale = avtalepart.hentAvtale(avtaleRepository, avtaleId); + return kontoregisterService.hentKontonummer(avtale.getBedriftNr().asString()); + } + + @PostMapping("/opprett-som-arbeidsgiver") + @Transactional + public ResponseEntity opprettAvtaleSomArbeidsgiver(@RequestBody OpprettAvtale opprettAvtale) { + Arbeidsgiver arbeidsgiver = innloggingService.hentArbeidsgiver(); + Avtale avtale = arbeidsgiver.opprettAvtale(opprettAvtale); + Avtale opprettetAvtale = avtaleRepository.save(avtale); + URI uri = lagUri("/avtaler/" + opprettetAvtale.getId()); + return ResponseEntity.created(uri).build(); + } + + /** + * VEILEDER-OPERASJONER + **/ + @ApiBeskrivelse("Hent liste over registrerte avtaler for bruker") + @GetMapping("/deltaker-allerede-paa-tiltak") + @Transactional + public ResponseEntity> sjekkOmDeltakerAlleredeErRegistrertPaaTiltak( + @RequestParam(value = "deltakerFnr") Fnr deltakerFnr, + @RequestParam(value = "tiltakstype") Tiltakstype tiltakstype, + @RequestParam(value = "startDato", required = false) String startDato, + @RequestParam(value = "sluttDato", required = false) String sluttDato, + @RequestParam(value = "avtaleId", required = false) String avtaleId + + ) { + Veileder veileder = innloggingService.hentVeileder(); + List avtaler = veileder.hentAvtaleDeltakerAlleredeErRegistrertPaa( + deltakerFnr, + tiltakstype, + avtaleId != null ? UUID.fromString(avtaleId) : null, + startDato != null ? LocalDate.parse(startDato) : null, + sluttDato != null ? LocalDate.parse(sluttDato) : null, + avtaleRepository + ); + return new ResponseEntity>(avtaler, HttpStatus.OK); + } + + @PostMapping + @Transactional + public ResponseEntity opprettAvtaleSomVeileder( + @RequestBody OpprettAvtale opprettAvtale, + @RequestParam(value = "ryddeavtale", required = false) + Boolean ryddeavtale + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = veileder.opprettAvtale(opprettAvtale); + avtale.leggTilBedriftNavn(eregService.hentVirksomhet(avtale.getBedriftNr()).getBedriftNavn()); + + Avtale opprettetAvtale = avtaleRepository.save(avtale); + if (ryddeavtale != null && ryddeavtale) { + ArenaRyddeAvtale arenaRyddeAvtale = new ArenaRyddeAvtale(); + arenaRyddeAvtale.setAvtale(avtale); + arenaRyddeAvtaleRepository.save(arenaRyddeAvtale); + } + URI uri = lagUri("/avtaler/" + opprettetAvtale.getId()); + return ResponseEntity.created(uri).build(); + } + + @PostMapping("/opprett-mentor-avtale") + @Transactional + public ResponseEntity opprettMentorAvtale(@RequestBody OpprettMentorAvtale opprettMentorAvtale) { + Avtale avtale = null; + if (opprettMentorAvtale.getDeltakerFnr().equals(opprettMentorAvtale.getMentorFnr())) { + throw new FeilkodeException(Feilkode.DELTAGER_OG_MENTOR_KAN_IKKE_HA_SAMME_FØDSELSNUMMER); + } + + if (opprettMentorAvtale.getAvtalerolle().equals(Avtalerolle.VEILEDER)) { + avtale = innloggingService.hentVeileder().opprettMentorAvtale(opprettMentorAvtale); + + } else if (opprettMentorAvtale.getAvtalerolle().equals(Avtalerolle.ARBEIDSGIVER)) { + avtale = innloggingService.hentArbeidsgiver().opprettMentorAvtale(opprettMentorAvtale); + } + if (avtale == null) { + throw new RuntimeException("Opprett Mentor fant ingen avtale å behandle."); + } + avtale.leggTilBedriftNavn(eregService.hentVirksomhet(opprettMentorAvtale.getBedriftNr()).getBedriftNavn()); + Avtale opprettetAvtale = avtaleRepository.save(avtale); + URI uri = lagUri("/avtaler/" + opprettetAvtale.getId()); + return ResponseEntity.created(uri).build(); + } + + @PostMapping("/{avtaleId}/forkort") + @Transactional + public void forkortAvtale( + @PathVariable("avtaleId") UUID avtaleId, + @RequestHeader(HttpHeaders.IF_UNMODIFIED_SINCE) Instant sistEndret, + @RequestBody ForkortAvtale forkortAvtale + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId) + .orElseThrow(RessursFinnesIkkeException::new); + veileder.forkortAvtale(avtale, forkortAvtale.getSluttDato(), forkortAvtale.getGrunn(), forkortAvtale.getAnnetGrunn()); + avtaleRepository.save(avtale); + } + + @ApiBeskrivelse("Test forkortelse av avtale om arbeidsmarkedstiltak") + @PostMapping("/{avtaleId}/forkort-dry-run") + public Avtale forkortAvtaleDryRun( + @PathVariable("avtaleId") UUID avtaleId, + @RequestHeader(HttpHeaders.IF_UNMODIFIED_SINCE) Instant sistEndret, + @RequestBody ForkortAvtale forkortAvtale + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId) + .orElseThrow(RessursFinnesIkkeException::new); + // Er ikke nødvending med en reell grunn siden det ikke påvirker tilskuddsperioder + veileder.forkortAvtale(avtale, forkortAvtale.getSluttDato(), "dry run", ""); + return avtale; + } + + @PostMapping("/{avtaleId}/forleng") + @Transactional + public void forlengAvtale( + @PathVariable("avtaleId") UUID avtaleId, + @RequestHeader(HttpHeaders.IF_UNMODIFIED_SINCE) Instant sistEndret, + @RequestBody ForlengAvtale forlengAvtale + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId) + .orElseThrow(RessursFinnesIkkeException::new); + veileder.forlengAvtale(forlengAvtale.getSluttDato(), avtale); + avtaleRepository.save(avtale); + } + + @ApiBeskrivelse("Test forlengelse av avtale om arbeidsmarkedstiltak") + @PostMapping("/{avtaleId}/forleng-dry-run") + public Avtale forlengAvtaleDryRun( + @PathVariable("avtaleId") UUID avtaleId, + @RequestHeader(HttpHeaders.IF_UNMODIFIED_SINCE) Instant sistEndret, + @RequestBody ForlengAvtale forlengAvtale + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId) + .orElseThrow(RessursFinnesIkkeException::new); + veileder.forlengAvtale(forlengAvtale.getSluttDato(), avtale); + return avtale; + } + + @PostMapping("/{avtaleId}/endre-maal") + @Transactional + public void endreMål( + @PathVariable("avtaleId") UUID avtaleId, + @RequestBody EndreMål endreMål + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.endreMål(endreMål, avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/endre-inkluderingstilskudd") + @Transactional + public void endreInkluderingstilskudd(@PathVariable("avtaleId") UUID avtaleId, + @RequestBody EndreInkluderingstilskudd endreInkluderingstilskudd) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.endreInkluderingstilskudd(endreInkluderingstilskudd, avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/endre-stillingbeskrivelse") + @Transactional + public void endreStillingbeskrivelse( + @PathVariable("avtaleId") UUID avtaleId, + @RequestBody EndreStillingsbeskrivelse endreStillingsbeskrivelse + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.endreStillingbeskrivelse(endreStillingsbeskrivelse, avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/endre-oppfolging-og-tilrettelegging") + @Transactional + public void endreOppfølgingOgTilrettelegging( + @PathVariable("avtaleId") UUID avtaleId, + @RequestBody EndreOppfølgingOgTilrettelegging endreOppfølgingOgTilrettelegging + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.endreOppfølgingOgTilrettelegging(endreOppfølgingOgTilrettelegging, avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/endre-om-mentor") + @Transactional + public void endreOmMentor( + @PathVariable("avtaleId") UUID avtaleId, + @RequestBody EndreOmMentor endreOmMentor + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.endreOmMentor(endreOmMentor, avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/endre-kontaktinfo") + @Transactional + public void endreKontaktinfo( + @PathVariable("avtaleId") UUID avtaleId, + @RequestBody EndreKontaktInformasjon endreKontaktInformasjon + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.endreKontaktinfo(endreKontaktInformasjon, avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/endre-tilskuddsberegning") + @Transactional + public void endreTilskuddsberegning(@PathVariable("avtaleId") UUID avtaleId, + @RequestBody EndreTilskuddsberegning endreTilskuddsberegning) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId) + .orElseThrow(RessursFinnesIkkeException::new); + veileder.endreTilskuddsberegning(endreTilskuddsberegning, avtale); + avtaleRepository.save(avtale); + } + + @ApiBeskrivelse("Test endring av tilskuddsberegning på avtale om arbeidsmarkedstiltak") + @PostMapping("/{avtaleId}/endre-tilskuddsberegning-dry-run") + public Avtale endreTilskuddsberegningDryRun( + @PathVariable("avtaleId") UUID avtaleId, + @RequestBody EndreTilskuddsberegning endreTilskuddsberegning + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId) + .orElseThrow(RessursFinnesIkkeException::new); + veileder.endreTilskuddsberegning(endreTilskuddsberegning, avtale); + return avtale; + } + + @PostMapping("/{avtaleId}/send-tilbake-til-beslutter") + @Transactional + public void sendTilbakeTilBeslutter(@PathVariable("avtaleId") UUID avtaleId) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId) + .orElseThrow(RessursFinnesIkkeException::new); + veileder.sendTilbakeTilBeslutter(avtale); + avtaleRepository.save(avtale); + } + + @PostMapping({ "/{avtaleId}/godkjenn-paa-vegne-av", "/{avtaleId}/godkjenn-paa-vegne-av-deltaker" }) + @Transactional + public void godkjennPaVegneAv( + @PathVariable("avtaleId") UUID avtaleId, + @RequestBody GodkjentPaVegneGrunn paVegneAvGrunn + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.godkjennForVeilederOgDeltaker(paVegneAvGrunn, avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/godkjenn-paa-vegne-av-arbeidsgiver") + @Transactional + public void godkjennPaVegneAvArbeidsgiver( + @PathVariable("avtaleId") UUID avtaleId, + @RequestBody GodkjentPaVegneAvArbeidsgiverGrunn paVegneAvGrunn + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.godkjennForVeilederOgArbeidsgiver(paVegneAvGrunn, avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/godkjenn-paa-vegne-av-deltaker-og-arbeidsgiver") + @Transactional + public void godkjennPaVegneAvDeltakerOgArbeidsgiver( + @PathVariable("avtaleId") UUID avtaleId, + @RequestBody GodkjentPaVegneAvDeltakerOgArbeidsgiverGrunn paVegneAvGrunn + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.godkjennForVeilederOgDeltakerOgArbeidsgiver(paVegneAvGrunn, avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/annuller") + @Transactional + public void annuller( + @PathVariable("avtaleId") UUID avtaleId, + @RequestHeader(HttpHeaders.IF_UNMODIFIED_SINCE) Instant sistEndret, + @RequestBody AnnullertInfo annullertInfo + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.annullerAvtale(sistEndret, annullertInfo.getAnnullertGrunn(), avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/slettemerk") + @Transactional + public void slettemerk(@PathVariable("avtaleId") UUID avtaleId) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.slettemerk(avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/del-med-avtalepart") + @Transactional + public void delAvtaleMedAvtalepart(@PathVariable("avtaleId") UUID avtaleId, @RequestBody Avtalerolle avtalerolle) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.delAvtaleMedAvtalepart(avtalerolle, avtale); + avtaleRepository.save(avtale); + } + + @PutMapping("/{avtaleId}/overta") + @Transactional + public void settNyVeilederPåAvtale(@PathVariable("avtaleId") UUID avtaleId) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.hentOppfølgingFraArena(avtale, veilarbArenaClient); + veileder.overtaAvtale(avtale); + avtaleRepository.save(avtale); + } + + @PostMapping("/{avtaleId}/juster-arena-migreringsdato") + @Transactional + public void justerArenaMigreringsdato(@PathVariable("avtaleId") UUID avtaleId, @RequestBody JusterArenaMigreringsdato justerArenaMigreringsdato) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + if (avtale.erAvtaleInngått()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_ENDRE_ARENA_MIGRERINGSDATO_INNGAATT_AVTALE); + } + veileder.sjekkTilgang(avtale); + avtale.nyeTilskuddsperioderEtterMigreringFraArena(justerArenaMigreringsdato.getMigreringsdato(), false); + Optional lagretAvtaleSomRyddeAvtale = arenaRyddeAvtaleRepository.findByAvtale(avtale); + + if (!lagretAvtaleSomRyddeAvtale.isPresent()) { + ArenaRyddeAvtale arenaRyddeAvtale = new ArenaRyddeAvtale(); + arenaRyddeAvtale.setAvtale(avtale); + arenaRyddeAvtale.setMigreringsdato(justerArenaMigreringsdato.getMigreringsdato()); + arenaRyddeAvtaleRepository.save(arenaRyddeAvtale); + } else { + ArenaRyddeAvtale oppdatertRyddeAvtale = lagretAvtaleSomRyddeAvtale.get(); + oppdatertRyddeAvtale.setMigreringsdato(justerArenaMigreringsdato.getMigreringsdato()); + arenaRyddeAvtaleRepository.save(oppdatertRyddeAvtale); + } + avtaleRepository.save(avtale); + } + + @ApiBeskrivelse("Justering av migreringsdato i avtale om arbeidsmarkedstiltak") + @PostMapping("/{avtaleId}/juster-arena-migreringsdato/dry-run") + public Avtale justerArenaMigreringsdatoDryRun(@PathVariable("avtaleId") UUID avtaleId, @RequestBody JusterArenaMigreringsdato justerArenaMigreringsdato) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.sjekkTilgang(avtale); + avtale.nyeTilskuddsperioderEtterMigreringFraArena(justerArenaMigreringsdato.getMigreringsdato(), false); + return avtale; + } + + @PostMapping("/{avtaleId}/godkjenn-tilskuddsperiode") + @Transactional + public void godkjennTilskuddsperiode( + @PathVariable("avtaleId") UUID avtaleId, + @RequestBody GodkjennTilskuddsperiodeRequest godkjennTilskuddsperiodeRequest + ) { + Beslutter beslutter = innloggingService.hentBeslutter(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + beslutter.godkjennTilskuddsperiode(avtale, godkjennTilskuddsperiodeRequest.getEnhet()); + avtaleRepository.save(avtale); + } + + @ApiBeskrivelse("Oppdater avtale om arbeidsmarkedstiltak") + @PostMapping("/{avtaleId}/set-om-avtalen-kan-etterregistreres") + @Transactional + public Avtale setOmAvtalenKanEtterregistreres(@PathVariable("avtaleId") UUID avtaleId) { + Beslutter beslutter = innloggingService.hentBeslutter(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + beslutter.setOmAvtalenKanEtterregistreres(avtale); + var oppdatertAvtale = avtaleRepository.save(avtale); + return oppdatertAvtale; + } + + @ApiBeskrivelse("Oppdater avtale om arbeidsmarkedstiltak") + @PostMapping("/{avtaleId}/endre-kostnadssted") + @Transactional + public Avtale endreKostnadssted( + @PathVariable("avtaleId") UUID avtaleId, + @RequestBody EndreKostnadsstedRequest endreKostnadsstedRequest + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + veileder.oppdatereKostnadssted(avtale, norg2Client, endreKostnadsstedRequest.getEnhet()); + var oppdatertAvtale = avtaleRepository.save(avtale); + return oppdatertAvtale; + } + + @PostMapping("/{avtaleId}/avslag-tilskuddsperiode") + @Transactional + public void avslåTilskuddsperiode(@PathVariable("avtaleId") UUID avtaleId, @RequestBody AvslagRequest avslagRequest) { + Beslutter beslutter = innloggingService.hentBeslutter(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + beslutter.avslåTilskuddsperiode(avtale, avslagRequest.getAvslagsårsaker(), avslagRequest.getAvslagsforklaring()); + avtaleRepository.save(avtale); + } + + @ApiBeskrivelse("Oppdater avtale om arbeidsmarkedstiltak") + @PostMapping("/{avtaleId}/oppdaterOppfølgingsEnhet") + public Avtale oppdaterOppfølgingsEnhet( + @PathVariable("avtaleId") UUID avtaleId + ) { + Veileder veileder = innloggingService.hentVeileder(); + Avtale avtale = veileder.hentAvtale(avtaleRepository, avtaleId); + veileder.oppdatereEnheterEtterForespørsel(avtale); + var oppdatertAvtale = avtaleRepository.save(avtale); + + return oppdatertAvtale; + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleForkortetEntitet.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleForkortetEntitet.java new file mode 100644 index 000000000..20835039c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleForkortetEntitet.java @@ -0,0 +1,44 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Data; +import no.nav.tag.tiltaksgjennomforing.utils.Now; + +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import java.time.Instant; +import java.time.LocalDate; +import java.util.UUID; + +@Data +@Entity +@Table(name = "avtale_forkortet") +public class AvtaleForkortetEntitet { + @Id + private UUID id; + private UUID avtaleId; + private UUID avtaleInnholdId; + private Instant tidspunkt; + @Convert(converter = NavIdentConverter.class) + private NavIdent utførtAv; + private LocalDate nySluttDato; + private String grunn; + private String annetGrunn; + + public AvtaleForkortetEntitet() { + } + + public AvtaleForkortetEntitet(Avtale avtale, AvtaleInnhold avtaleInnhold, NavIdent utførtAv, LocalDate nySluttDato, String grunn, String annetGrunn) { + this.id = UUID.randomUUID(); + this.avtaleId = avtale.getId(); + this.avtaleInnholdId = avtaleInnhold.getId(); + this.tidspunkt = Now.instant(); + this.utførtAv = utførtAv; + this.nySluttDato = nySluttDato; + this.grunn = grunn; + this.annetGrunn = annetGrunn; + } + + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleForkortetLytter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleForkortetLytter.java new file mode 100644 index 000000000..1d3128bd8 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleForkortetLytter.java @@ -0,0 +1,17 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.RequiredArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleForkortet; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AvtaleForkortetLytter { + private final AvtaleForkortetRepository avtaleForkortetRepository; + + @EventListener + public void avtaleForkortet(AvtaleForkortet event) { + avtaleForkortetRepository.save(new AvtaleForkortetEntitet(event.getAvtale(), event.getAvtaleInnhold(), event.getUtførtAv(), event.getNySluttDato(), event.getGrunn(), event.getAnnetGrunn())); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleForkortetRepository.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleForkortetRepository.java new file mode 100644 index 000000000..8b0c95ad4 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleForkortetRepository.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + + +public interface AvtaleForkortetRepository extends JpaRepository { + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnhold.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnhold.java new file mode 100644 index 000000000..abb975c43 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnhold.java @@ -0,0 +1,273 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import static no.nav.tag.tiltaksgjennomforing.utils.Utils.erTom; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; +import javax.persistence.CascadeType; +import javax.persistence.Convert; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.FieldNameConstants; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; + +// Lombok +@Data +@Builder(toBuilder = true) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@AllArgsConstructor +@NoArgsConstructor +@FieldNameConstants +// Hibernate +@Entity +public class AvtaleInnhold { + + @Id + @JsonIgnore + private UUID id; + + @ManyToOne + @JoinColumn(name = "avtale") + @JsonIgnore + @ToString.Exclude + private Avtale avtale; + + private Integer versjon; + + private String deltakerFornavn; + private String deltakerEtternavn; + private String deltakerTlf; + private String bedriftNavn; + private String arbeidsgiverFornavn; + private String arbeidsgiverEtternavn; + private String arbeidsgiverTlf; + private String veilederFornavn; + private String veilederEtternavn; + private String veilederTlf; + private String oppfolging; + private String tilrettelegging; + private LocalDate startDato; + private LocalDate sluttDato; + private Integer stillingprosent; + private String journalpostId; + private String arbeidsoppgaver; + private String stillingstittel; + private Integer stillingStyrk08; + private Integer stillingKonseptId; + private Integer antallDagerPerUke; + + @Embedded + private RefusjonKontaktperson refusjonKontaktperson; + + + // Mentor + private String mentorFornavn; + private String mentorEtternavn; + private String mentorOppgaver; + private Double mentorAntallTimer; + private Integer mentorTimelonn; + private String mentorTlf; + + // Lønnstilskudd + private String arbeidsgiverKontonummer; + private Integer lonnstilskuddProsent; + private Integer manedslonn; + private BigDecimal feriepengesats; + private BigDecimal arbeidsgiveravgift; + private Boolean harFamilietilknytning; + private String familietilknytningForklaring; + private Integer feriepengerBelop; + private Double otpSats; + private Integer otpBelop; + private Integer arbeidsgiveravgiftBelop; + private Integer sumLonnsutgifter; + private Integer sumLonnstilskudd; + private Integer manedslonn100pst; + private Integer sumLønnstilskuddRedusert; + private LocalDate datoForRedusertProsent; + @Enumerated(EnumType.STRING) + private Stillingstype stillingstype; + + // Arbeidstrening + @OneToMany(mappedBy = "avtaleInnhold", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + @Fetch(FetchMode.SUBSELECT) + private List maal = new ArrayList<>(); + + // Inkluderingstilskudd + @OneToMany(mappedBy = "avtaleInnhold", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + @Fetch(FetchMode.SUBSELECT) + private List inkluderingstilskuddsutgift = new ArrayList<>(); + private String inkluderingstilskuddBegrunnelse; + + @JsonProperty + public Integer inkluderingstilskuddTotalBeløp() { + return inkluderingstilskuddsutgift.stream().map(inkluderingstilskuddsutgift -> inkluderingstilskuddsutgift.getBeløp()) + .collect(Collectors.toList()).stream() + .reduce(0, Integer::sum); + } + + // Godkjenning + private LocalDateTime godkjentAvDeltaker; + private LocalDateTime godkjentTaushetserklæringAvMentor; + private LocalDateTime godkjentAvArbeidsgiver; + private LocalDateTime godkjentAvVeileder; + private LocalDateTime godkjentAvBeslutter; + private LocalDateTime avtaleInngått; + private LocalDateTime ikrafttredelsestidspunkt; + @Convert(converter = NavIdentConverter.class) + private NavIdent godkjentAvNavIdent; + @Convert(converter = NavIdentConverter.class) + private NavIdent godkjentAvBeslutterNavIdent; + + // Kostnadssted + private String enhetKostnadssted; + private String enhetsnavnKostnadssted; + + @Embedded + private GodkjentPaVegneGrunn godkjentPaVegneGrunn; + private boolean godkjentPaVegneAv; + + @Embedded + private GodkjentPaVegneAvArbeidsgiverGrunn godkjentPaVegneAvArbeidsgiverGrunn; + private boolean godkjentPaVegneAvArbeidsgiver; + + @Enumerated(EnumType.STRING) + private AvtaleInnholdType innholdType; + + + public static AvtaleInnhold nyttTomtInnhold(Tiltakstype tiltakstype) { + var innhold = new AvtaleInnhold(); + innhold.setId(UUID.randomUUID()); + innhold.setVersjon(1); + innhold.setInnholdType(AvtaleInnholdType.INNGÅ); + if (tiltakstype == Tiltakstype.SOMMERJOBB) { + innhold.setStillingstype(Stillingstype.MIDLERTIDIG); + } + return innhold; + } + + public AvtaleInnhold nyGodkjentVersjon(AvtaleInnholdType innholdType) { + AvtaleInnhold nyVersjon = toBuilder() + .id(UUID.randomUUID()) + .maal(kopiAvMål()) + .inkluderingstilskuddsutgift(kopiAvInkluderingstilskuddsutgifer()) + .journalpostId(null) + .versjon(versjon + 1) + .innholdType(innholdType) + .build(); + nyVersjon.getMaal().forEach(m -> m.setAvtaleInnhold(nyVersjon)); + nyVersjon.getInkluderingstilskuddsutgift().forEach(i -> i.setAvtaleInnhold(nyVersjon)); + return nyVersjon; + } + + private List kopiAvMål() { + return maal.stream().map(m -> new Maal(m)).collect(Collectors.toList()); + } + + private List kopiAvInkluderingstilskuddsutgifer() { + return inkluderingstilskuddsutgift.stream().map(i -> new Inkluderingstilskuddsutgift(i)).collect(Collectors.toList()); + } + + void endreAvtale(EndreAvtale nyAvtale) { + if (tiltakstypeHarFastsattLonnstilskuddsprosentsatsUtIfraKvalifiseringsgruppe()) { + innholdStrategi().endreAvtaleInnholdMedKvalifiseringsgruppe(nyAvtale, avtale.getKvalifiseringsgruppe()); + } else { + innholdStrategi().endre(nyAvtale); + } + } + + public Set felterSomIkkeErFyltUt() { + return innholdStrategi().alleFelterSomMåFyllesUt() + .entrySet().stream() + .filter(entry -> erTom(entry.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + + private AvtaleInnholdStrategy innholdStrategi() { + return AvtaleInnholdStrategyFactory.create(this, avtale.getTiltakstype()); + } + + private boolean tiltakstypeHarFastsattLonnstilskuddsprosentsatsUtIfraKvalifiseringsgruppe() { + // Midlertidig skrudd av utleding av lønnstilskuddprosent for Sommerjobb fra kvalifiseringsgruppe for å åpne for etterregistrering. + return avtale.getTiltakstype() == Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD; + } + + public boolean skalJournalfores() { + return this.godkjentAvVeileder != null && this.getJournalpostId() == null; + } + + public void endreTilskuddsberegning(EndreTilskuddsberegning tilskuddsberegning) { + innholdStrategi().endreTilskuddsberegning(tilskuddsberegning); + } + + public void reberegnLønnstilskudd() { + innholdStrategi().regnUtTotalLonnstilskudd(); + } + + public void reberegnRedusertProsentOgRedusertLonnstilskudd() { + innholdStrategi().reUtregnRedusertProsentOgSum(); + } + + public void endreKontaktInfo(EndreKontaktInformasjon endreKontaktInformasjon) { + setDeltakerFornavn(endreKontaktInformasjon.getDeltakerFornavn()); + setDeltakerEtternavn(endreKontaktInformasjon.getDeltakerEtternavn()); + setDeltakerTlf(endreKontaktInformasjon.getDeltakerTlf()); + setVeilederFornavn(endreKontaktInformasjon.getVeilederFornavn()); + setVeilederEtternavn(endreKontaktInformasjon.getVeilederEtternavn()); + setVeilederTlf(endreKontaktInformasjon.getVeilederTlf()); + setArbeidsgiverFornavn(endreKontaktInformasjon.getArbeidsgiverFornavn()); + setArbeidsgiverEtternavn(endreKontaktInformasjon.getArbeidsgiverEtternavn()); + setArbeidsgiverTlf(endreKontaktInformasjon.getArbeidsgiverTlf()); + setRefusjonKontaktperson(endreKontaktInformasjon.getRefusjonKontaktperson()); + } + + public void endreStillingsInfo(EndreStillingsbeskrivelse endreStillingsbeskrivelse) { + setStillingstittel(endreStillingsbeskrivelse.getStillingstittel()); + setArbeidsoppgaver(endreStillingsbeskrivelse.getArbeidsoppgaver()); + setStillingStyrk08(endreStillingsbeskrivelse.getStillingStyrk08()); + setStillingKonseptId(endreStillingsbeskrivelse.getStillingKonseptId()); + setStillingprosent(endreStillingsbeskrivelse.getStillingprosent()); + setAntallDagerPerUke(endreStillingsbeskrivelse.getAntallDagerPerUke()); + } + + public void endreOppfølgingOgTilretteleggingInfo(EndreOppfølgingOgTilrettelegging endreOppfølgingOgTilrettelegging) { + setOppfolging(endreOppfølgingOgTilrettelegging.getOppfolging()); + setTilrettelegging(endreOppfølgingOgTilrettelegging.getTilrettelegging()); + } + + public void endreSluttDato(LocalDate nySluttDato) { + innholdStrategi().endreSluttDato(nySluttDato); + } + + public void endreOmMentor(EndreOmMentor endreOmMentor) { + setMentorFornavn(endreOmMentor.getMentorFornavn()); + setMentorEtternavn(endreOmMentor.getMentorEtternavn()); + setMentorTlf(endreOmMentor.getMentorTlf()); + setMentorAntallTimer(endreOmMentor.getMentorAntallTimer()); + setMentorTimelonn(endreOmMentor.getMentorTimelonn()); + setMentorOppgaver(endreOmMentor.getMentorOppgaver()); + } +} + + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnholdRepository.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnholdRepository.java new file mode 100644 index 000000000..e240822d0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnholdRepository.java @@ -0,0 +1,23 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import io.micrometer.core.annotation.Timed; +import java.util.List; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface AvtaleInnholdRepository extends JpaRepository { + + @Timed(percentiles = { 0.5d, 0.75d, 0.9d, 0.99d, 0.999d }) + @Override + List findAllById(Iterable ids); + + @Timed(percentiles = { 0.5d, 0.75d, 0.9d, 0.99d, 0.999d }) + @Query(value = "select ai from AvtaleInnhold ai where ai.journalpostId is null and ai.avtaleInngått is not null") + List finnAvtaleVersjonerTilJournalfoering(); + + @Timed(percentiles = { 0.5d, 0.75d, 0.9d, 0.99d, 0.999d }) + List findAllByAvtale(Avtale avtale); +} + + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnholdStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnholdStrategy.java new file mode 100644 index 000000000..ff1bab54b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnholdStrategy.java @@ -0,0 +1,20 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; + +import java.time.LocalDate; +import java.util.Map; + +public interface AvtaleInnholdStrategy { + void endre(EndreAvtale endreAvtale); + default void endreTilskuddsberegning(EndreTilskuddsberegning endreTilskuddsberegning) { + throw new RuntimeException("Ikke implementert"); + } + default void endreAvtaleInnholdMedKvalifiseringsgruppe(EndreAvtale endreAvtale, Kvalifiseringsgruppe kvalifiseringsgruppe) {} + default void regnUtTotalLonnstilskudd() {} + + default void reUtregnRedusertProsentOgSum() {} + Map alleFelterSomMåFyllesUt(); + + void endreSluttDato(LocalDate nySluttDato); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnholdStrategyFactory.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnholdStrategyFactory.java new file mode 100644 index 000000000..24f59f136 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnholdStrategyFactory.java @@ -0,0 +1,24 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class AvtaleInnholdStrategyFactory { + public AvtaleInnholdStrategy create(AvtaleInnhold avtaleInnhold, Tiltakstype tiltakstype) { + switch (tiltakstype) { + case ARBEIDSTRENING: + return new ArbeidstreningStrategy(avtaleInnhold); + case MIDLERTIDIG_LONNSTILSKUDD: + return new MidlertidigLonnstilskuddStrategy(avtaleInnhold); + case VARIG_LONNSTILSKUDD: + return new VarigLonnstilskuddStrategy(avtaleInnhold); + case MENTOR: + return new MentorStrategy(avtaleInnhold); + case INKLUDERINGSTILSKUDD: + return new InkluderingstilskuddStrategy(avtaleInnhold); + case SOMMERJOBB: + return new SommerjobbStrategy(avtaleInnhold); + } + throw new IllegalStateException(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnholdType.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnholdType.java new file mode 100644 index 000000000..b5067e9a3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleInnholdType.java @@ -0,0 +1,5 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +public enum AvtaleInnholdType { + INNGÅ, LÅSE_OPP, FORLENGE, FORKORTE, ENDRE_MÅL, ENDRE_INKLUDERINGSTILSKUDD, ENDRE_OM_MENTOR, ENDRE_TILSKUDDSBEREGNING, ENDRE_STILLING, ENDRE_KONTAKTINFO, ENDRE_OPPFØLGING_OG_TILRETTELEGGING, ANNULLERE +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleMinimal.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleMinimal.java new file mode 100644 index 000000000..1c68e8da5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleMinimal.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +public interface AvtaleMinimal { + String getId(); + String getVeilederNavIdent(); + String getBedriftNavn(); + String getDeltakerFornavn(); + String getDeltakerEtternavn(); + String getStartDatoPeriode(); + String getAntallUbehandlet(); + String getDeltakerFnr(); + String getBedriftNr(); + String getTilskuddsperiodestatus(); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleMinimalListevisning.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleMinimalListevisning.java new file mode 100644 index 000000000..dad4ccaa3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleMinimalListevisning.java @@ -0,0 +1,48 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.time.LocalDate; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class AvtaleMinimalListevisning { + private String id; + private String deltakerFnr; + private String deltakerFornavn; + private String deltakerEtternavn; + private String bedriftNavn; + private String veilederNavIdent; + private LocalDate startDato; + private LocalDate sluttDato; + private Status status; + private Tiltakstype tiltakstype; + private boolean erGodkjentTaushetserklæringAvMentor; + private TilskuddPeriodeStatus gjeldendeTilskuddsperiodeStatus; + private Instant sistEndret; + + public static AvtaleMinimalListevisning fromAvtale(Avtale avtale) { + AvtaleMinimalListevisning avtaleMininal = AvtaleMinimalListevisning.builder() + .id(avtale.getId().toString()) + .deltakerFnr(avtale.getDeltakerFnr() != null ? avtale.getDeltakerFnr().asString() : null) + .deltakerEtternavn(avtale.getGjeldendeInnhold().getDeltakerEtternavn()) + .deltakerFornavn(avtale.getGjeldendeInnhold().getDeltakerFornavn()) + .bedriftNavn(avtale.getGjeldendeInnhold().getBedriftNavn()) + .veilederNavIdent(avtale.getVeilederNavIdent() != null ? avtale.getVeilederNavIdent().asString() : null) + .startDato(avtale.getGjeldendeInnhold().getStartDato()) + .sluttDato(avtale.getGjeldendeInnhold().getSluttDato()) + .status(avtale.statusSomEnum()) + .tiltakstype(avtale.getTiltakstype()) + .erGodkjentTaushetserklæringAvMentor(avtale.erGodkjentTaushetserklæringAvMentor()) + .gjeldendeTilskuddsperiodeStatus(avtale.getGjeldendeTilskuddsperiodestatus()) + .sistEndret(avtale.getSistEndret()) + .build(); + return avtaleMininal; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtalePredicate.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtalePredicate.java new file mode 100644 index 000000000..3dbfc4289 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtalePredicate.java @@ -0,0 +1,70 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Data; +import lombok.experimental.Accessors; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; + +@Data +@Accessors(chain = true) +public class AvtalePredicate implements Predicate { + + private NavIdent veilederNavIdent; + private BedriftNr bedriftNr; + private Fnr deltakerFnr; + private Tiltakstype tiltakstype; + private Status status; + private Boolean erUfordelt; + private TilskuddPeriodeStatus tilskuddPeriodeStatus; + private String navEnhet; + private Integer avtaleNr; + + + private static boolean erLiktHvisOppgitt(Object kriterie, Object avtaleVerdi) { + return kriterie == null || kriterie.equals(avtaleVerdi); + } + + @Override + public boolean test(Avtale avtale) { + return erLiktHvisOppgitt(veilederNavIdent, avtale.getVeilederNavIdent()) + && erLiktHvisOppgitt(bedriftNr, avtale.getBedriftNr()) + && erLiktHvisOppgitt(deltakerFnr, avtale.getDeltakerFnr()) + && erLiktHvisOppgitt(tiltakstype, avtale.getTiltakstype()) + && erLiktHvisOppgitt(status, avtale.statusSomEnum()) + && erLiktHvisOppgitt(tilskuddPeriodeStatus, avtale.getGjeldendeTilskuddsperiodestatus()) + && (erLiktHvisOppgitt(navEnhet, avtale.getEnhetGeografisk()) || erLiktHvisOppgitt(navEnhet, avtale.getEnhetOppfolging())) + && erLiktHvisOppgitt(avtaleNr, avtale.getAvtaleNr()); + } + + public String generateHash() { + List liste = List.of(Objects.toString(veilederNavIdent, "") + , Objects.toString(bedriftNr, "") + , Objects.toString(deltakerFnr, "") + , Objects.toString(tiltakstype, "") + , Objects.toString(status, "") + , Objects.toString(tilskuddPeriodeStatus, "") + , Objects.toString(navEnhet, "") + , Objects.toString(avtaleNr, "")); + + String predicateString = String.join(";", liste); + + MessageDigest digest = null; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + byte[] hash = digest.digest(predicateString.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : hash) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleRepository.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleRepository.java new file mode 100644 index 000000000..117ec71f2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleRepository.java @@ -0,0 +1,152 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import io.micrometer.core.annotation.Timed; + +import java.sql.Date; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + + +public interface AvtaleRepository extends JpaRepository, JpaSpecificationExecutor { + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + @Override + Optional findById(UUID id); + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Optional findByAvtaleNr(Integer avtaleNr); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + @Override + List findAllById(Iterable ids); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + List findAllByBedriftNr(BedriftNr bedriftNr); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByBedriftNrIn(Set bedriftNrList, Pageable pageable); + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByBedriftNr(BedriftNr bedriftNr, Pageable pageable); + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByBedriftNrAndTiltakstype(BedriftNr bedriftNr, Tiltakstype tiltakstype, Pageable pageable); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByBedriftNrInAndTiltakstype(Set bedriftNrList, Tiltakstype tiltakstype, Pageable pageable); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByDeltakerFnr(Fnr deltakerFnr, Pageable pageable); + Page findAllByDeltakerFnrAndTiltakstype(Fnr deltakerFnr, Tiltakstype tiltakstype, Pageable pageable); + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByVeilederNavIdent(NavIdent veilederNavIdent, Pageable pageable); + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByVeilederNavIdentAndTiltakstype(NavIdent veilederNavIdent, Tiltakstype tiltakstype, Pageable pageable); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + List findAllByDeltakerFnr(Fnr deltakerFnr); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByMentorFnr(Fnr mentorFnr, Pageable pageable); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByVeilederNavIdentIsNullAndEnhetGeografiskOrVeilederNavIdentIsNullAndEnhetOppfolging(String enhetGeografisk, String enhetOppfolging, Pageable pageable); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByVeilederNavIdentIsNullAndEnhetGeografiskAndTiltakstypeOrVeilederNavIdentIsNullAndEnhetOppfolgingAndTiltakstype(String enhetGeografisk, Tiltakstype tiltakstype, String enhetOppfolging, Tiltakstype tiltakstype2, Pageable pageable); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByEnhetGeografiskOrEnhetOppfolging(String enhetGeografisk, String enhetOppfolging, Pageable pageable); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByEnhetGeografiskAndTiltakstypeOrEnhetOppfolgingAndTiltakstype(String enhetGeografisk, Tiltakstype tiltakstype, String enhetOppfolging, Tiltakstype tiltakstype2, Pageable pageable); + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByAvtaleNr(Integer avtaleNr, Pageable pageable); + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + Page findAllByAvtaleNrAndTiltakstype(Integer avtaleNr, Tiltakstype tiltakstype, Pageable pageable); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + List findAllByTiltakstype(Tiltakstype tiltakstype); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + List findAllByTiltakstypeAndGjeldendeInnhold_DatoForRedusertProsentNullAndGjeldendeInnhold_AvtaleInngåttNotNull(Tiltakstype tiltakstype); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + @Override + List findAll(); + + List findAllByGjeldendeInnhold_AvtaleInngåttNotNull(); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + @Override + Avtale save(Avtale entity); + + @Query(value = "SELECT AVTALE.* FROM AVTALE LEFT JOIN AVTALE_INNHOLD " + + "ON AVTALE.ID = AVTALE_INNHOLD.AVTALE " + + "WHERE :deltakerFnr = AVTALE.deltaker_fnr and " + + "AVTALE.annullert_tidspunkt is null and " + + "AVTALE.avbrutt is false and " + + "AVTALE.slettemerket is false and " + + "((CAST(:startDato as date) is not null and AVTALE_INNHOLD.start_dato is not null and AVTALE_INNHOLD.slutt_dato is not null and" + + " (CAST(:startDato as date) >= AVTALE_INNHOLD.start_dato and CAST(:startDato as date) <= AVTALE_INNHOLD.slutt_dato)) " + + "or " + + "AVTALE_INNHOLD.godkjent_av_veileder is null)" + , nativeQuery = true) + List finnAvtalerSomOverlapperForDeltakerVedOpprettelseAvAvtale( + @Param("deltakerFnr") String deltakerFnr, + @Param("startDato") Date startDato + ); + + @Query(value = "SELECT AVTALE.* FROM AVTALE LEFT JOIN AVTALE_INNHOLD " + + "ON AVTALE.ID = AVTALE_INNHOLD.AVTALE " + + "WHERE :deltakerFnr = AVTALE.deltaker_fnr and " + + "(:avtaleId is not null and :avtaleId NOT LIKE CAST(AVTALE.id as text)) and " + + "AVTALE.annullert_tidspunkt is null and " + + "AVTALE.avbrutt is false and " + + "AVTALE.slettemerket is false and " + + "((CAST(:startDato as date) is not null and AVTALE_INNHOLD.start_dato is not null and AVTALE_INNHOLD.slutt_dato is not null and" + + " (CAST(:startDato as date) >= AVTALE_INNHOLD.start_dato and CAST(:startDato as date) <= AVTALE_INNHOLD.slutt_dato)) " + + "or " + + "(CAST(:sluttDato as date) is not null and AVTALE_INNHOLD.start_dato is not null and AVTALE_INNHOLD.slutt_dato is not null and " + + "(CAST(:sluttDato as date) >= AVTALE_INNHOLD.start_dato and CAST(:sluttDato as date) <= AVTALE_INNHOLD.slutt_dato)) " + + "or " + + "AVTALE_INNHOLD.godkjent_av_veileder is null)" + , nativeQuery = true) + List finnAvtalerSomOverlapperForDeltakerVedGodkjenningAvAvtale( + @Param("deltakerFnr") String deltakerFnr, + @Param("avtaleId") String avtaleId, + @Param("startDato") Date startDato, + @Param("sluttDato") Date sluttDato + ); + + @Query(value = "SELECT a.id as id, a.avtaleNr as avtaleNr, a.tiltakstype as tiltakstype, a.veilederNavIdent as veilederNavIdent, a.gjeldendeInnhold.deltakerFornavn as deltakerFornavn, " + + "a.opprettetTidspunkt as opprettetTidspunkt, a.sistEndret as sistEndret, a.gjeldendeInnhold.deltakerEtternavn as deltakerEtternavn, " + + "a.deltakerFnr as deltakerFnr, a.gjeldendeInnhold.bedriftNavn as bedriftNavn, a.bedriftNr as bedriftNr, min(t.startDato) as startDato, " + + " t.status as status, count(t.id) as antallUbehandlet " + + "from Avtale a " + + "left join AvtaleInnhold i on i.id = a.gjeldendeInnhold.id " + + "left join TilskuddPeriode t on (t.avtale.id = a.id and t.status = :tilskuddsperiodestatus and t.startDato <= :decisiondate) " + + "where a.gjeldendeInnhold.godkjentAvVeileder is not null " + + "and a.tiltakstype in (:tiltakstype) " + + "and exists (select distinct p.avtale.id, p.status, p.løpenummer, p.startDato from TilskuddPeriode p where p.avtale.id = a.id " + + "and ((:tilskuddsperiodestatus = p.status and p.startDato <= :decisiondate) or (:tilskuddsperiodestatus = p.status AND p.løpenummer = 1))) " + + "and a.enhetOppfolging IN (:navEnheter) AND (:avtaleNr is null or a.avtaleNr = :avtaleNr) AND (:bedriftNr is null or cast(a.bedriftNr as text) = :bedriftNr) " + + "GROUP BY a.id, a.gjeldendeInnhold.deltakerFornavn, a.gjeldendeInnhold.deltakerEtternavn, a.veilederNavIdent, a.gjeldendeInnhold.bedriftNavn, status ", + nativeQuery = false) + Page finnGodkjenteAvtalerMedTilskuddsperiodestatusOgNavEnheter( + @Param("tilskuddsperiodestatus") TilskuddPeriodeStatus tilskuddsperiodestatus, + @Param("decisiondate") LocalDate decisiondate, + @Param("tiltakstype") Set tiltakstype, + @Param("navEnheter") Set navEnheter, + @Param("bedriftNr") String bedriftNr, + @Param("avtaleNr") Integer avtaleNr, + Pageable pageable); + +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleSorterer.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleSorterer.java new file mode 100644 index 000000000..d76cefcdf --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleSorterer.java @@ -0,0 +1,67 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import java.util.Comparator; +import lombok.experimental.UtilityClass; +import org.springframework.data.domain.Sort; + +@UtilityClass +public class AvtaleSorterer { + public Comparator comparatorForAvtale(String sorteringskolonne) { + return switch (sorteringskolonne) { + case Avtale.Fields.opprettetTidspunkt -> Comparator.comparing(Avtale::getOpprettetTidspunkt, Comparator.reverseOrder()); + case AvtaleInnhold.Fields.bedriftNavn -> Comparator.comparing(avtale -> lowercaseEllerNull(avtale.getGjeldendeInnhold().getBedriftNavn()), Comparator.nullsLast(Comparator.naturalOrder())); + case AvtaleInnhold.Fields.deltakerEtternavn -> Comparator.comparing(avtale -> lowercaseEllerNull(avtale.getGjeldendeInnhold().getDeltakerEtternavn()), Comparator.nullsLast(Comparator.naturalOrder())); + case AvtaleInnhold.Fields.deltakerFornavn -> Comparator.comparing(avtale -> lowercaseEllerNull(avtale.getGjeldendeInnhold().getDeltakerFornavn()), Comparator.nullsLast(Comparator.naturalOrder())); + case "status" -> Comparator.comparing(Avtale::status); + case "startDato" -> Comparator.comparing(avtale -> (avtale.gjeldendeTilskuddsperiode() != null ? avtale.gjeldendeTilskuddsperiode().getStartDato() : avtale.getGjeldendeInnhold().getStartDato()), Comparator.nullsLast(Comparator.naturalOrder())); + default -> Comparator.comparing(Avtale::getSistEndret, Comparator.reverseOrder()); + }; + } + + private static String lowercaseEllerNull(String x) { + return x != null ? x.toLowerCase() : null; + } + + static Sort.Order getSortingOrderForPageable(String sortingOrder) { + return switch (sortingOrder) { + case "deltakerFornavn" -> Sort.Order.asc("gjeldendeInnhold.deltakerFornavn"); + case "opprettetTidspunkt" -> Sort.Order.desc("opprettetTidspunkt"); + case "bedriftNavn" -> Sort.Order.asc("gjeldendeInnhold.bedriftNavn"); + case "startDato" -> Sort.Order.asc("gjeldendeInnhold.startDato"); + case "tiltakstype" -> Sort.Order.asc("tiltakstype"); + default -> Sort.Order.desc("sistEndret"); + }; + } + + static protected Sort.Order getSortingOrderForPageable(String order, String direction) { + SortingDirection sortingDirection = SortingDirection.valueOf(direction.toUpperCase()); + return switch (sortingDirection) { + case ASC -> getSortingOrderForPageableASC(SortingOrder.valueOf(order.toUpperCase())); + case DESC -> getSortingOrderForPageableDESC(SortingOrder.valueOf(order.toUpperCase())); + }; + } + + static private Sort.Order getSortingOrderForPageableASC(SortingOrder sortingOrder) { + return switch (sortingOrder) { + case OPPRETTETTIDSPUNKT -> Sort.Order.asc("opprettetTidspunkt"); + case BEDRIFTNAVN -> Sort.Order.asc("bedriftNavn"); + case DELTAKERFORNAVN -> Sort.Order.asc("deltakerFornavn"); + case STATUS -> Sort.Order.asc("antallUbehandlet"); + case STARTDATO -> Sort.Order.asc("startDato"); + case SISTENDRET -> Sort.Order.asc("sistEndret"); + case TILTAKSTYPE -> Sort.Order.asc("tiltakstype"); + }; + } + + static private Sort.Order getSortingOrderForPageableDESC(SortingOrder sortingOrder) { + return switch (sortingOrder) { + case OPPRETTETTIDSPUNKT -> Sort.Order.desc("opprettetTidspunkt"); + case BEDRIFTNAVN -> Sort.Order.desc("bedriftNavn"); + case DELTAKERFORNAVN -> Sort.Order.desc("deltakerFornavn"); + case STATUS -> Sort.Order.desc("antallUbehandlet"); + case STARTDATO -> Sort.Order.desc("startDato"); + case SISTENDRET -> Sort.Order.desc("sistEndret"); + case TILTAKSTYPE -> Sort.Order.desc("tiltakstype"); + }; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleStatusDetaljer.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleStatusDetaljer.java new file mode 100644 index 000000000..50878d2c2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleStatusDetaljer.java @@ -0,0 +1,32 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AvtaleStatusDetaljer { + boolean godkjentAvInnloggetBruker; + String header; + String infoDel1; + String infoDel2; + String part1; + Boolean part1Status; + String part2; + Boolean part2Status; + void setInnloggetBrukerStatus( String header,String infoDel1,String infoDel2){ + this.header=header; + this.infoDel1=infoDel1; + this.infoDel2=infoDel2; + } + void setPart1Detaljer(String part1,boolean part1Status){ + this.part1=part1; + this.part1Status=part1Status; + } + void setPart2Detaljer(String part2, boolean part2Status){ + this.part2=part2; + this.part2Status=part2Status; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Avtalepart.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Avtalepart.java new file mode 100644 index 000000000..efd1dae4a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Avtalepart.java @@ -0,0 +1,195 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetBruker; +import no.nav.tag.tiltaksgjennomforing.enhet.*; +import no.nav.tag.tiltaksgjennomforing.exceptions.*; +import no.nav.tag.tiltaksgjennomforing.persondata.PdlRespons; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; + +import static java.util.Map.entry; + +@AllArgsConstructor +@Slf4j +@Data +public abstract class Avtalepart { + private final T identifikator; + static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd. MMMM yyyy"); + + public boolean harTilgang(Avtale avtale) { + if (avtale.isSlettemerket()) { + return false; + } + return harTilgangTilAvtale(avtale); + } + + abstract boolean harTilgangTilAvtale(Avtale avtale); + + abstract Page hentAlleAvtalerMedMuligTilgang(AvtaleRepository avtaleRepository, AvtalePredicate queryParametre, Pageable pageable); + + public Map hentAlleAvtalerMedLesetilgang(AvtaleRepository avtaleRepository, AvtalePredicate queryParametre, String sorteringskolonne, Pageable pageable) { + Page avtaler = hentAlleAvtalerMedMuligTilgang(avtaleRepository, queryParametre, pageable); + + List avtalerMedTilgang = avtaler.getContent().stream() + .filter(avtale -> !avtale.isFeilregistrert()) + .filter(this::harTilgang) + .toList(); + + List listMinimal = avtalerMedTilgang.stream().map(AvtaleMinimalListevisning::fromAvtale).toList(); + + return Map.ofEntries( + entry("avtaler", listMinimal), + entry("size", avtaler.getSize()), + entry("currentPage", avtaler.getNumber()), + entry("totalItems", avtaler.getTotalElements()), + entry("totalPages", avtaler.getTotalPages()) + ); + } + + public Avtale hentAvtale(AvtaleRepository avtaleRepository, UUID avtaleId) { + Avtale avtale = avtaleRepository.findById(avtaleId) + .orElseThrow(RessursFinnesIkkeException::new); + sjekkTilgang(avtale); + return avtale; + } + + public Avtale hentAvtaleFraAvtaleNr(AvtaleRepository avtaleRepository, int avtaleNr) { + Avtale avtale = avtaleRepository.findByAvtaleNr(avtaleNr).orElseThrow(RessursFinnesIkkeException::new); + sjekkTilgang(avtale); + return avtale; + } + + public List hentAvtaleVersjoner(AvtaleRepository avtaleRepository, AvtaleInnholdRepository avtaleInnholdRepository, UUID avtaleId) { + Avtale avtale = avtaleRepository.findById(avtaleId) + .orElseThrow(RessursFinnesIkkeException::new); + sjekkTilgang(avtale); + return avtaleInnholdRepository.findAllByAvtale(avtale); + } + + abstract void godkjennForAvtalepart(Avtale avtale); + + abstract boolean kanEndreAvtale(); + + public abstract boolean erGodkjentAvInnloggetBruker(Avtale avtale); + + abstract boolean kanOppheveGodkjenninger(Avtale avtale); + + abstract void opphevGodkjenningerSomAvtalepart(Avtale avtale); + + public void godkjennAvtale(Instant sistEndret, Avtale avtale) { + sjekkTilgang(avtale); + avtale.sjekkSistEndret(sistEndret); + godkjennForAvtalepart(avtale); + } + + public void sjekkTilgang(Avtale avtale) { + if (!harTilgang(avtale)) { + throw new TilgangskontrollException("Ikke tilgang til avtale"); + } + } + + protected void avtalePartKanEndreAvtale() { + if (!kanEndreAvtale()) { + throw new KanIkkeEndreException(); + } + } + + public void endreAvtale( + Instant sistEndret, + EndreAvtale endreAvtale, + Avtale avtale, + EnumSet tiltakstyperMedTilskuddsperioder + ) { + sjekkTilgang(avtale); + if (!kanEndreAvtale()) { + throw new KanIkkeEndreException(); + } + avvisDatoerTilbakeITid(avtale, endreAvtale.getStartDato(), endreAvtale.getSluttDato()); + avtale.endreAvtale(sistEndret, endreAvtale, rolle(), tiltakstyperMedTilskuddsperioder, identifikator); + } + + protected void sjekkTilgangOgEndreAvtale( + Instant sistEndret, + EndreAvtale endreAvtale, + Avtale avtale, + EnumSet tiltakstyperMedTilskuddsperioder + ) { + sjekkTilgang(avtale); + avtalePartKanEndreAvtale(); + avvisDatoerTilbakeITid(avtale, endreAvtale.getStartDato(), endreAvtale.getSluttDato()); + avtale.endreAvtale( + sistEndret, + endreAvtale, + rolle(), + tiltakstyperMedTilskuddsperioder, + identifikator + ); + } + + protected void avvisDatoerTilbakeITid(Avtale avtale, LocalDate startDato, LocalDate sluttDato) { + } + + protected abstract Avtalerolle rolle(); + + public void opphevGodkjenninger(Avtale avtale) { + if (!kanOppheveGodkjenninger(avtale)) { + throw new KanIkkeOppheveException(); + } + boolean AlleParterHarIkkeGodkjentAvtale = !avtale.erGodkjentAvVeileder() && + !avtale.erGodkjentAvArbeidsgiver() && + !avtale.erGodkjentAvDeltaker(); + + if (AlleParterHarIkkeGodkjentAvtale) { + throw new KanIkkeOppheveException(); + } + if (avtale.erAvtaleInngått()) { + throw new FeilkodeException(Feilkode.KAN_IKKE_OPPHEVE_GODKJENNINGER_VED_INNGAATT_AVTALE); + } + opphevGodkjenningerSomAvtalepart(avtale); + } + + public abstract InnloggetBruker innloggetBruker(); + + public Collection identifikatorer() { + return List.of(getIdentifikator()); + } + + protected void hentGeoEnhetFraNorg2(Avtale avtale, PdlRespons pdlRespons, Norg2Client norg2Client) { + Norg2GeoResponse enhet = PersondataService.hentGeoLokasjonFraPdlRespons(pdlRespons) + .map(norg2Client::hentGeografiskEnhet).orElse(null); + if (enhet == null) return; + avtale.setEnhetGeografisk(enhet.getEnhetNr()); + avtale.setEnhetsnavnGeografisk(enhet.getNavn()); + } + + protected void hentOppfølingenhetNavnFraNorg2(Avtale avtale, Norg2Client norg2Client) { + if (avtale.getEnhetOppfolging() == null) return; + if (avtale.getEnhetOppfolging().equals(avtale.getEnhetGeografisk())) { + avtale.setEnhetsnavnOppfolging(avtale.getEnhetsnavnGeografisk()); + } else { + final Norg2OppfølgingResponse response = norg2Client.hentOppfølgingsEnhetsnavn(avtale.getEnhetOppfolging()); + if (response == null) return; + avtale.setEnhetsnavnOppfolging(response.getNavn()); + } + } + + public void settLonntilskuddProsentsats(Avtale avtale) { + if (avtale.getTiltakstype() == Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD) { + avtale.getGjeldendeInnhold().setLonnstilskuddProsent( + avtale.getKvalifiseringsgruppe().finnLonntilskuddProsentsatsUtifraKvalifiseringsgruppe( + 40, + 60 + ) + ); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Avtalerolle.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Avtalerolle.java new file mode 100644 index 000000000..0c5f814f2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Avtalerolle.java @@ -0,0 +1,5 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +public enum Avtalerolle { + DELTAKER, MENTOR, ARBEIDSGIVER, VEILEDER, BESLUTTER +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BaseAvtaleInnholdStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BaseAvtaleInnholdStrategy.java new file mode 100644 index 000000000..2c28f1f0e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BaseAvtaleInnholdStrategy.java @@ -0,0 +1,70 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; +import no.nav.tag.tiltaksgjennomforing.avtale.RefusjonKontaktperson.Fields; +import no.nav.tag.tiltaksgjennomforing.utils.Now; + +public abstract class BaseAvtaleInnholdStrategy implements AvtaleInnholdStrategy { + final AvtaleInnhold avtaleInnhold; + + public BaseAvtaleInnholdStrategy(AvtaleInnhold avtaleInnhold) { + this.avtaleInnhold = avtaleInnhold; + } + + @Override + public void endre(EndreAvtale nyAvtale) { + avtaleInnhold.setDeltakerFornavn(nyAvtale.getDeltakerFornavn()); + avtaleInnhold.setDeltakerEtternavn(nyAvtale.getDeltakerEtternavn()); + avtaleInnhold.setDeltakerTlf(nyAvtale.getDeltakerTlf()); + avtaleInnhold.setBedriftNavn(nyAvtale.getBedriftNavn()); + avtaleInnhold.setArbeidsgiverFornavn(nyAvtale.getArbeidsgiverFornavn()); + avtaleInnhold.setArbeidsgiverEtternavn(nyAvtale.getArbeidsgiverEtternavn()); + avtaleInnhold.setArbeidsgiverTlf(nyAvtale.getArbeidsgiverTlf()); + avtaleInnhold.setVeilederFornavn(nyAvtale.getVeilederFornavn()); + avtaleInnhold.setVeilederEtternavn(nyAvtale.getVeilederEtternavn()); + avtaleInnhold.setVeilederTlf(nyAvtale.getVeilederTlf()); + avtaleInnhold.setArbeidsoppgaver(nyAvtale.getArbeidsoppgaver()); + avtaleInnhold.setOppfolging(nyAvtale.getOppfolging()); + avtaleInnhold.setTilrettelegging(nyAvtale.getTilrettelegging()); + avtaleInnhold.setStartDato(nyAvtale.getStartDato()); + avtaleInnhold.setSluttDato(nyAvtale.getSluttDato()); + avtaleInnhold.setStillingprosent(nyAvtale.getStillingprosent()); + avtaleInnhold.setAntallDagerPerUke(nyAvtale.getAntallDagerPerUke()); + avtaleInnhold.setRefusjonKontaktperson(nyAvtale.getRefusjonKontaktperson()); + } + + @Override + public Map alleFelterSomMåFyllesUt() { + // Felter som er felles for alle tiltakstyper + + Map alleFelter = new HashMap<>(); + alleFelter.put(AvtaleInnhold.Fields.deltakerFornavn, avtaleInnhold.getDeltakerFornavn()); + alleFelter.put(AvtaleInnhold.Fields.deltakerEtternavn, avtaleInnhold.getDeltakerEtternavn()); + alleFelter.put(AvtaleInnhold.Fields.deltakerTlf, avtaleInnhold.getDeltakerTlf()); + alleFelter.put(AvtaleInnhold.Fields.bedriftNavn, avtaleInnhold.getBedriftNavn()); + alleFelter.put(AvtaleInnhold.Fields.arbeidsgiverFornavn, avtaleInnhold.getArbeidsgiverFornavn()); + alleFelter.put(AvtaleInnhold.Fields.arbeidsgiverEtternavn, avtaleInnhold.getArbeidsgiverEtternavn()); + alleFelter.put(AvtaleInnhold.Fields.arbeidsgiverTlf, avtaleInnhold.getArbeidsgiverTlf()); + alleFelter.put(AvtaleInnhold.Fields.veilederFornavn, avtaleInnhold.getVeilederFornavn()); + alleFelter.put(AvtaleInnhold.Fields.veilederEtternavn, avtaleInnhold.getVeilederEtternavn()); + alleFelter.put(AvtaleInnhold.Fields.veilederTlf, avtaleInnhold.getVeilederTlf()); + alleFelter.put(AvtaleInnhold.Fields.startDato, avtaleInnhold.getStartDato()); + alleFelter.put(AvtaleInnhold.Fields.sluttDato, avtaleInnhold.getSluttDato()); + alleFelter.put(AvtaleInnhold.Fields.oppfolging, avtaleInnhold.getOppfolging()); + alleFelter.put(AvtaleInnhold.Fields.tilrettelegging, avtaleInnhold.getTilrettelegging()); + if(avtaleInnhold.getRefusjonKontaktperson() != null){ + alleFelter.put(Fields.refusjonKontaktpersonFornavn, avtaleInnhold.getRefusjonKontaktperson().getRefusjonKontaktpersonFornavn()); + alleFelter.put(Fields.refusjonKontaktpersonEtternavn, avtaleInnhold.getRefusjonKontaktperson().getRefusjonKontaktpersonEtternavn()); + alleFelter.put(Fields.refusjonKontaktpersonTlf, avtaleInnhold.getRefusjonKontaktperson().getRefusjonKontaktpersonTlf()); + } + return alleFelter; + } + + @Override + public void endreSluttDato(LocalDate nySluttDato) { + avtaleInnhold.setSluttDato(nySluttDato); + avtaleInnhold.setIkrafttredelsestidspunkt(Now.localDateTime()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BedriftNr.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BedriftNr.java new file mode 100644 index 000000000..6c5b615de --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BedriftNr.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +public class BedriftNr extends Identifikator { + public BedriftNr(String verdi) { + super(verdi); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BedriftNrConverter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BedriftNrConverter.java new file mode 100644 index 000000000..d1d641e0f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BedriftNrConverter.java @@ -0,0 +1,18 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +@Converter +public class BedriftNrConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(BedriftNr attribute) { + return attribute.asString(); + } + + @Override + public BedriftNr convertToEntityAttribute(String dbData) { + return new BedriftNr(dbData); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Beslutter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Beslutter.java new file mode 100644 index 000000000..32134021e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Beslutter.java @@ -0,0 +1,179 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetBeslutter; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetBruker; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.TilgangskontrollService; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2Client; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2OppfølgingResponse; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; +import no.nav.tag.tiltaksgjennomforing.exceptions.NavEnhetIkkeFunnetException; +import no.nav.tag.tiltaksgjennomforing.exceptions.TilgangskontrollException; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.NavEnhet; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Slf4j +public class Beslutter extends Avtalepart implements InternBruker { + private Norg2Client norg2Client; + private TilgangskontrollService tilgangskontrollService; + + private UUID azureOid; + + private Set navEnheter; + + public Beslutter(NavIdent identifikator, UUID azureOid, Set navEnheter, TilgangskontrollService tilgangskontrollService, Norg2Client norg2Client) { + super(identifikator); + this.azureOid = azureOid; + this.navEnheter = navEnheter; + this.tilgangskontrollService = tilgangskontrollService; + this.norg2Client = norg2Client; + } + + public void godkjennTilskuddsperiode(Avtale avtale, String enhet) { + sjekkTilgang(avtale); + final Norg2OppfølgingResponse response = norg2Client.hentOppfølgingsEnhetsnavn(enhet); + + if (response == null) { + throw new FeilkodeException(Feilkode.ENHET_FINNES_IKKE); + } + avtale.godkjennTilskuddsperiode(getIdentifikator(), enhet); + } + + public void avslåTilskuddsperiode(Avtale avtale, EnumSet avslagsårsaker, String avslagsforklaring) { + sjekkTilgang(avtale); + avtale.avslåTilskuddsperiode(getIdentifikator(), avslagsårsaker, avslagsforklaring); + } + + public void setOmAvtalenKanEtterregistreres(Avtale avtale) { + sjekkTilgang(avtale); + avtale.togglegodkjennEtterregistrering(getIdentifikator()); + } + + @Override + public boolean harTilgangTilAvtale(Avtale avtale) { + return tilgangskontrollService.harSkrivetilgangTilKandidat(this, avtale.getDeltakerFnr()); + } + + public boolean harTilgangTilFnr(Fnr fnr) { + return tilgangskontrollService.harSkrivetilgangTilKandidat(this, fnr); + } + + @Override + Page hentAlleAvtalerMedMuligTilgang(AvtaleRepository avtaleRepository, AvtalePredicate queryParametre, Pageable pageable) { + return avtaleRepository.findAllByAvtaleNr(queryParametre.getAvtaleNr(), pageable); + } + + private Integer getPlussdato() { + return ((int) ChronoUnit.DAYS.between(LocalDate.now(), LocalDate.now().plusMonths(3))); + } + + Page finnGodkjenteAvtalerMedTilskuddsperiodestatusOgNavEnheterListe( + AvtaleRepository avtaleRepository, + AvtalePredicate queryParametre, + String sorteringskolonne, + Integer page, + Integer size, + String sorteringOrder + ) { + Sort by = Sort.by(AvtaleSorterer.getSortingOrderForPageable(sorteringskolonne, sorteringOrder)); + Pageable paging = PageRequest.of(page, size, by); + + Set navEnheter = hentNavEnheter(); + + if (navEnheter.isEmpty()) { + throw new NavEnhetIkkeFunnetException(); + } + + TilskuddPeriodeStatus status = queryParametre.getTilskuddPeriodeStatus(); + Tiltakstype tiltakstype = queryParametre.getTiltakstype(); + BedriftNr bedriftNr = queryParametre.getBedriftNr(); + Integer avtaleNr = queryParametre.getAvtaleNr(); + String filtrertNavEnhet = queryParametre.getNavEnhet(); + Integer plussDato = getPlussdato(); + LocalDate decisiondate = LocalDate.now().plusDays(plussDato); + + if (status == null) { + status = TilskuddPeriodeStatus.UBEHANDLET; + } + + Set tiltakstyper = new HashSet<>(); + if (tiltakstype != null) { + tiltakstyper.add(tiltakstype); + } else { + tiltakstyper.add(Tiltakstype.SOMMERJOBB); + tiltakstyper.add(Tiltakstype.VARIG_LONNSTILSKUDD); + tiltakstyper.add(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD); + } + + return avtaleRepository.finnGodkjenteAvtalerMedTilskuddsperiodestatusOgNavEnheter( + status, + decisiondate, + tiltakstyper, + filtrertNavEnhet != null ? Set.of(filtrertNavEnhet) : navEnheter, + bedriftNr != null ? bedriftNr.asString() : null, + avtaleNr, + paging + ); + } + + private Set hentNavEnheter() { + return this.navEnheter.stream().map(NavEnhet::getVerdi).collect(Collectors.toSet()); + } + + @Override + void godkjennForAvtalepart(Avtale avtale) { + throw new TilgangskontrollException("Beslutter kan ikke godkjenne avtaler"); + } + + @Override + public boolean kanEndreAvtale() { + return false; + } + + @Override + public boolean erGodkjentAvInnloggetBruker(Avtale avtale) { + return false; + } + + @Override + boolean kanOppheveGodkjenninger(Avtale avtale) { + return false; + } + + @Override + void opphevGodkjenningerSomAvtalepart(Avtale avtale) { + throw new TilgangskontrollException("Beslutter kan ikke oppheve godkjenninger av avtaler"); + } + + @Override + protected Avtalerolle rolle() { + return Avtalerolle.BESLUTTER; + } + + @Override + public InnloggetBruker innloggetBruker() { + return new InnloggetBeslutter(getIdentifikator(), navEnheter); + } + + @Override + public UUID getAzureOid() { + return azureOid; + } + + @Override + public NavIdent getNavIdent() { + return getIdentifikator(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BeslutterOversikt.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BeslutterOversikt.java new file mode 100644 index 000000000..f3b1a7dbd --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BeslutterOversikt.java @@ -0,0 +1,47 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.springframework.data.domain.Page; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Data +@AllArgsConstructor +public class BeslutterOversikt { + + private String id; + private String veilederNavIdent; + private String deltakerFornavn; + private String deltakerEtternavn; + private String deltakerFnr; + private String bedriftNavn; + private String bedriftNr; + private LocalDate StartDato; + private LocalDate sluttDato; + private String Status; + private String antallUbehandlet; + private LocalDateTime OpprettetTidspunkt; + + + protected static List getBeslutterOversikt(Page beslutterOversikt) { + return beslutterOversikt.getContent().stream().map(listElement -> new BeslutterOversikt( + listElement.getId(), + listElement.getVeilederNavIdent().asString(), + listElement.getDeltakerFornavn(), + listElement.getDeltakerEtternavn(), + listElement.getDeltakerFnr().asString(), + listElement.getBedriftNavn(), + listElement.getBedriftNr() != null ? listElement.getBedriftNr().asString() : null, + listElement.getStartDato(), + listElement.getSluttDato(), + listElement.getStatus(), + listElement.getAntallUbehandlet(), + listElement.getOpprettetTidspunkt() + )).collect(Collectors.toList()); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BeslutterOversiktDTO.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BeslutterOversiktDTO.java new file mode 100644 index 000000000..976d08217 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/BeslutterOversiktDTO.java @@ -0,0 +1,22 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +public interface BeslutterOversiktDTO { + String getId(); + Integer getAvtaleNr(); + Tiltakstype getTiltakstype(); + NavIdent getVeilederNavIdent(); + String getDeltakerFornavn(); + String getDeltakerEtternavn(); + Fnr getDeltakerFnr(); + String getBedriftNavn(); + BedriftNr getBedriftNr(); + LocalDate getStartDato(); + LocalDate getSluttDato(); + String getStatus(); + String getAntallUbehandlet(); + LocalDateTime getOpprettetTidspunkt(); + LocalDateTime getSistEndret(); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Deltaker.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Deltaker.java new file mode 100644 index 000000000..fcf1b026c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Deltaker.java @@ -0,0 +1,79 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + + +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetBruker; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetDeltaker; +import no.nav.tag.tiltaksgjennomforing.exceptions.TilgangskontrollException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.UUID; + +public class Deltaker extends Avtalepart { + + public Deltaker(Fnr identifikator) { + super(identifikator); + } + + @Override + public Avtale hentAvtale(AvtaleRepository avtaleRepository, UUID avtaleId) { + Avtale avtale = super.hentAvtale(avtaleRepository,avtaleId); + return skjulMentorFødselsnummer(avtale); + } + @Override + public boolean harTilgangTilAvtale(Avtale avtale) { + return avtale.getDeltakerFnr().equals(getIdentifikator()); + } + + @Override + Page hentAlleAvtalerMedMuligTilgang(AvtaleRepository avtaleRepository, AvtalePredicate queryParametre, Pageable pageable) { + + Page avtaler = avtaleRepository.findAllByDeltakerFnr(getIdentifikator(), pageable); + Page filtrereAvtalerKanske = avtaler.map(this::skjulMentorFødselsnummer); + return filtrereAvtalerKanske; + } + + private Avtale skjulMentorFødselsnummer(Avtale avtale){ + if(avtale.getTiltakstype() == Tiltakstype.MENTOR) { + avtale.setMentorFnr(null); + avtale.getGjeldendeInnhold().setMentorTimelonn(null); + } + return avtale; + } + + @Override + public void godkjennForAvtalepart(Avtale avtale) { + avtale.godkjennForDeltaker(getIdentifikator()); + } + + @Override + public boolean kanEndreAvtale() { + return false; + } + + @Override + public boolean erGodkjentAvInnloggetBruker(Avtale avtale) { + return avtale.erGodkjentAvDeltaker(); + } + + + @Override + boolean kanOppheveGodkjenninger(Avtale avtale) { + return false; + } + + @Override + void opphevGodkjenningerSomAvtalepart(Avtale avtale) { + throw new TilgangskontrollException("Deltaker kan ikke oppheve godkjenninger"); + } + + @Override + protected Avtalerolle rolle() { + return Avtalerolle.DELTAKER; + } + + @Override + public InnloggetBruker innloggetBruker() { + return new InnloggetDeltaker(getIdentifikator()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreAvtale.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreAvtale.java new file mode 100644 index 000000000..cf157f71b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreAvtale.java @@ -0,0 +1,87 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EndreAvtale { + + private String deltakerFornavn; + private String deltakerEtternavn; + private String deltakerTlf; + private String bedriftNavn; + private String arbeidsgiverFornavn; + private String arbeidsgiverEtternavn; + private String arbeidsgiverTlf; + private String veilederFornavn; + private String veilederEtternavn; + private String veilederTlf; + + private String oppfolging; + private String tilrettelegging; + + private LocalDate startDato; + private LocalDate sluttDato; + private Integer stillingprosent; + private String arbeidsoppgaver; + private String stillingstittel; + private Integer stillingStyrk08; + private Integer stillingKonseptId; + private Integer antallDagerPerUke; + + private String refusjonKontaktpersonFornavn; + private String refusjonKontaktpersonEtternavn; + private String refusjonKontaktpersonTlf; + private Boolean ønskerVarslingOmRefusjon; + + // Arbeidstreningsfelter + private List maal = new ArrayList<>(); + + // Inkluderingstilskuddsfelter + private List inkluderingstilskuddsutgift = new ArrayList<>(); + private String inkluderingstilskuddBegrunnelse; + + // Lønnstilskuddsfelter + private String arbeidsgiverKontonummer; + private Integer lonnstilskuddProsent; + private Integer manedslonn; + private BigDecimal feriepengesats; + private BigDecimal arbeidsgiveravgift; + private Double otpSats; + private Boolean harFamilietilknytning; + private String familietilknytningForklaring; + private Stillingstype stillingstype; + + // Mentorfelter + private String mentorFornavn; + private String mentorEtternavn; + private String mentorOppgaver; + private Double mentorAntallTimer; + private String mentorTlf; + private Integer mentorTimelonn; + + + public RefusjonKontaktperson getRefusjonKontaktperson(){ + if(refusjonKontaktpersonTlf == null && refusjonKontaktpersonFornavn == null && refusjonKontaktpersonEtternavn == null) { + return null; + } + + return new RefusjonKontaktperson(refusjonKontaktpersonFornavn, refusjonKontaktpersonEtternavn, refusjonKontaktpersonTlf, + ønskerVarslingOmRefusjon); + } + + public void setRefusjonKontaktperson(RefusjonKontaktperson refusjonKontaktperson) { + if(refusjonKontaktperson == null) { return; } + this.refusjonKontaktpersonFornavn = refusjonKontaktperson.getRefusjonKontaktpersonFornavn(); + this.refusjonKontaktpersonEtternavn = refusjonKontaktperson.getRefusjonKontaktpersonEtternavn(); + this.refusjonKontaktpersonTlf = refusjonKontaktperson.getRefusjonKontaktpersonTlf(); + this.ønskerVarslingOmRefusjon = refusjonKontaktperson.getØnskerVarslingOmRefusjon(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreInkluderingstilskudd.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreInkluderingstilskudd.java new file mode 100644 index 000000000..11e20dd5a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreInkluderingstilskudd.java @@ -0,0 +1,20 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.NoArgsConstructor; +import lombok.Value; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Value +@NoArgsConstructor +public class EndreInkluderingstilskudd { + List inkluderingstilskuddsutgift = new ArrayList<>(); + + public Integer inkluderingstilskuddTotalBeløp() { + return inkluderingstilskuddsutgift.stream().map(inkluderingstilskuddsutgift -> inkluderingstilskuddsutgift.getBeløp()) + .collect(Collectors.toList()).stream() + .reduce(0, Integer::sum); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreKontaktInformasjon.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreKontaktInformasjon.java new file mode 100644 index 000000000..8f3df7e1b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreKontaktInformasjon.java @@ -0,0 +1,19 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Builder; +import lombok.Value; + +@Value +@Builder(toBuilder = true) +public class EndreKontaktInformasjon { + String deltakerFornavn; + String deltakerEtternavn; + String deltakerTlf; + String veilederFornavn; + String veilederEtternavn; + String veilederTlf; + String arbeidsgiverFornavn; + String arbeidsgiverEtternavn; + String arbeidsgiverTlf; + RefusjonKontaktperson refusjonKontaktperson; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreKostnadsstedRequest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreKostnadsstedRequest.java new file mode 100644 index 000000000..69c7e771b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreKostnadsstedRequest.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Value; + +@Value +public class EndreKostnadsstedRequest { + String enhet; +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreM\303\245l.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreM\303\245l.java" new file mode 100644 index 000000000..545ba1a5f --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreM\303\245l.java" @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Value; + +import java.util.List; + +@Value +public class EndreMål { + List maal; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreOmMentor.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreOmMentor.java new file mode 100644 index 000000000..c7f83b7d2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreOmMentor.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Builder; +import lombok.Value; + +@Value +@Builder(toBuilder = true) +public class EndreOmMentor { + String mentorFornavn; + String mentorEtternavn; + String mentorTlf; + String mentorOppgaver; + Double mentorAntallTimer; + Integer mentorTimelonn; +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreOppf\303\270lgingOgTilrettelegging.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreOppf\303\270lgingOgTilrettelegging.java" new file mode 100644 index 000000000..3d3ee1993 --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreOppf\303\270lgingOgTilrettelegging.java" @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Builder; +import lombok.Value; + +@Value +@Builder(toBuilder = true) +public class EndreOppfølgingOgTilrettelegging { + String oppfolging; + String tilrettelegging; +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreStillingsbeskrivelse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreStillingsbeskrivelse.java new file mode 100644 index 000000000..ef7f9f911 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreStillingsbeskrivelse.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Builder; +import lombok.Value; + +@Value +@Builder(toBuilder = true) +public class EndreStillingsbeskrivelse { + String stillingstittel; + String arbeidsoppgaver; + Integer stillingStyrk08; + Integer stillingKonseptId; + Integer stillingprosent; + Integer antallDagerPerUke; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreTilskuddsberegning.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreTilskuddsberegning.java new file mode 100644 index 000000000..3cbbfb154 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/EndreTilskuddsberegning.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import java.math.BigDecimal; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder(toBuilder = true) +public class EndreTilskuddsberegning { + Integer manedslonn; + BigDecimal feriepengesats; + BigDecimal arbeidsgiveravgift; + Double otpSats; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/FilterSok.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/FilterSok.java new file mode 100644 index 000000000..a8dd98c7a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/FilterSok.java @@ -0,0 +1,53 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; + +import javax.persistence.Entity; +import javax.persistence.Id; +import java.time.LocalDateTime; + +@Data +@Entity +@NoArgsConstructor +public class FilterSok { + @Id + private String sokId; + private LocalDateTime sistSoktTidspunkt; + private String queryParametre; + private Integer antallGangerSokt; + + + @SneakyThrows + public FilterSok(AvtalePredicate queryParametre) { + this.sistSoktTidspunkt = LocalDateTime.now(); + this.antallGangerSokt = 1; + this.sokId = queryParametre.generateHash(); + ObjectMapper mapper = new ObjectMapper(); + this.queryParametre = mapper.writeValueAsString(queryParametre); + } + + public boolean erLik(AvtalePredicate avtalePredicate) { + ObjectMapper mapper = new ObjectMapper(); + try { + return mapper.readValue(this.queryParametre, AvtalePredicate.class).equals(avtalePredicate); + } catch (JsonProcessingException e) { + return false; + } + } + + public AvtalePredicate getAvtalePredicate() { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + try { + return mapper.readValue(this.queryParametre, AvtalePredicate.class); + } catch (JsonProcessingException e) { + return new AvtalePredicate(); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/FilterSokRepository.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/FilterSokRepository.java new file mode 100644 index 000000000..5637c6e17 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/FilterSokRepository.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface FilterSokRepository extends JpaRepository { + + Optional findFilterSokBySokId(String sokId); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Fnr.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Fnr.java new file mode 100644 index 000000000..2bbc6312b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Fnr.java @@ -0,0 +1,161 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import java.time.LocalDate; +import no.nav.tag.tiltaksgjennomforing.exceptions.TiltaksgjennomforingException; +import no.nav.tag.tiltaksgjennomforing.utils.Now; + +public class Fnr extends Identifikator { + + public Fnr(String fnr) { + super(fnr); + if (fnr != null && !erGyldigFnr(fnr)) { + throw new TiltaksgjennomforingException("Ugyldig fødselsnummer. Må bestå av 11 tegn."); + } + } + + public static boolean erGyldigFnr(String fnr) { + return fnr.matches("^[0-9]{11}$"); + } + + private LocalDate fødselsdato() { + int dag = Integer.parseInt(this.getDayInMonth()); + int måned = Integer.parseInt(this.getMonth()); + int år = Integer.parseInt(this.getBirthYear()); + return LocalDate.of(år, måned, dag); + } + + public boolean erUnder16år() { + if (this.asString().equals("00000000000")) { + return false; + } + return this.fødselsdato().isAfter(Now.localDate().minusYears(16)); + } + + public boolean erOver30år() { + if (this.asString().equals("00000000000")) { + return false; + } + return this.fødselsdato().isBefore(Now.localDate().minusYears(30)); + } + + private static LocalDate førsteJanuarIÅr() { + return Now.localDate() + .minusMonths(Now.localDate().getMonthValue() - 1).minusDays(Now.localDate().getDayOfMonth() - 1); + } + + public boolean erOver30årFørsteJanuar() { + if (this.asString().equals("00000000000")) { + return false; + } + return this.fødselsdato().isBefore(førsteJanuarIÅr().minusYears(30)); + } + + public boolean erOver30årFraOppstartDato(LocalDate opprettetTidspunkt) { + if (this.asString().equals("00000000000")) { + return false; + } + return this.fødselsdato().isBefore(opprettetTidspunkt.minusYears(30)); + } + + public boolean erOver67ÅrFraSluttDato(LocalDate sluttDato) { + if (this.asString().equals("00000000000")) { + return false; + } + return this.fødselsdato().isBefore(sluttDato.minusYears(67).plusDays(1)); + } + + public boolean erOver72ÅrFraSluttDato(LocalDate sluttDato) { + if (this.asString().equals("00000000000")) { + return false; + } + return this.fødselsdato().isBefore(sluttDato.minusYears(72).plusDays(1)); + } + + private String getDayInMonth() { + return parseSynthenticNumber(parseDNumber(this.asString())).substring(0, 2); + } + + private String getMonth() { + return parseSynthenticNumber(parseDNumber(this.asString())).substring(2, 4); + } + + private String getBirthYear() { + return getCentury() + get2DigitBirthYear(); + } + + private static String parseSynthenticNumber(String fodselsnummer) { + if (!isSynthetic(fodselsnummer)) { + return fodselsnummer; + } else { + if (getThirdDigit(fodselsnummer) > 7) { + return fodselsnummer.substring(0, 2) + (getThirdDigit(fodselsnummer) - 8) + fodselsnummer.substring(3); + } else { + return fodselsnummer.substring(0, 2) + (getThirdDigit(fodselsnummer) - 4) + fodselsnummer.substring(3); + } + } + } + + private static boolean isSynthetic(String fodselsnummer) { + try { + int thirdDigit = getThirdDigit(fodselsnummer); + if (thirdDigit > 3) { + return true; + } + } catch (IllegalArgumentException e) { + // ignore + } + return false; + } + + private static int getThirdDigit(String fodselsnummer) { + return Integer.parseInt(fodselsnummer.substring(2, 3)); + } + + private static int getFirstDigit(String fodselsnummer) { + return Integer.parseInt(fodselsnummer.substring(0, 1)); + } + + private static String parseDNumber(String fodselsnummer) { + if (!isDNumber(fodselsnummer)) { + return fodselsnummer; + } else { + return (getFirstDigit(fodselsnummer) - 4) + fodselsnummer.substring(1); + } + } + + private static boolean isDNumber(String fodselsnummer) { + try { + int firstDigit = getFirstDigit(fodselsnummer); + if (firstDigit > 3 && firstDigit < 8) { + return true; + } + } catch (IllegalArgumentException e) { + // ignore + } + return false; + } + + private String getCentury() { + String result = null; + int individnummerInt = Integer.parseInt(getIndividnummer()); + int birthYear = Integer.parseInt(get2DigitBirthYear()); + if (individnummerInt <= 499) { + result = "19"; + } else if (individnummerInt >= 500 && birthYear < 40) { + result = "20"; + } else if (individnummerInt >= 500 && individnummerInt <= 749 && birthYear >= 54) { + result = "18"; + } else if (individnummerInt >= 900 && birthYear > 39) { + result = "19"; + } + return result; + } + + private String getIndividnummer() { + return this.asString().substring(6, 9); + } + + private String get2DigitBirthYear() { + return this.asString().substring(4, 6); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/FnrConverter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/FnrConverter.java new file mode 100644 index 000000000..67c16feb4 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/FnrConverter.java @@ -0,0 +1,19 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +@Converter +public class FnrConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(Fnr attribute) { + if(attribute == null) return null; + return attribute.asString(); + } + + @Override + public Fnr convertToEntityAttribute(String dbData) { + return new Fnr(dbData); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ForkortAvtale.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ForkortAvtale.java new file mode 100644 index 000000000..5bafe6831 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ForkortAvtale.java @@ -0,0 +1,12 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Value; + +import java.time.LocalDate; + +@Value +public class ForkortAvtale { + LocalDate sluttDato; + String grunn; + String annetGrunn; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ForlengAvtale.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ForlengAvtale.java new file mode 100644 index 000000000..94637fe4e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/ForlengAvtale.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Value; + +import java.time.LocalDate; + +@Value +public class ForlengAvtale { + LocalDate sluttDato; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/GodkjennTilskuddsperiodeRequest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/GodkjennTilskuddsperiodeRequest.java new file mode 100644 index 000000000..307018766 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/GodkjennTilskuddsperiodeRequest.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Value; + +@Value +public class GodkjennTilskuddsperiodeRequest { + String enhet; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/GodkjentPaVegneAvArbeidsgiverGrunn.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/GodkjentPaVegneAvArbeidsgiverGrunn.java new file mode 100644 index 000000000..6672ee5b3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/GodkjentPaVegneAvArbeidsgiverGrunn.java @@ -0,0 +1,22 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Data; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; + +import javax.persistence.Embeddable; + +@Data +@Embeddable +public class GodkjentPaVegneAvArbeidsgiverGrunn { + boolean klarerIkkeGiFaTilgang; + boolean vetIkkeHvemSomKanGiTilgang; + boolean farIkkeTilgangPersonvern; + boolean arenaMigreringArbeidsgiver; + + public void valgtMinstEnGrunn() { + if (!klarerIkkeGiFaTilgang && !vetIkkeHvemSomKanGiTilgang && !farIkkeTilgangPersonvern && !arenaMigreringArbeidsgiver) { + throw new FeilkodeException(Feilkode.GODKJENT_PAA_VEGNE_GRUNN_MAA_VELGES); + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/GodkjentPaVegneAvDeltakerOgArbeidsgiverGrunn.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/GodkjentPaVegneAvDeltakerOgArbeidsgiverGrunn.java new file mode 100644 index 000000000..b524a33da --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/GodkjentPaVegneAvDeltakerOgArbeidsgiverGrunn.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Value; + +@Value +public class GodkjentPaVegneAvDeltakerOgArbeidsgiverGrunn { + GodkjentPaVegneAvArbeidsgiverGrunn godkjentPaVegneAvArbeidsgiverGrunn; + GodkjentPaVegneGrunn godkjentPaVegneAvDeltakerGrunn; + + public void valgtMinstEnGrunn() { + godkjentPaVegneAvArbeidsgiverGrunn.valgtMinstEnGrunn(); + godkjentPaVegneAvDeltakerGrunn.valgtMinstEnGrunn(); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/GodkjentPaVegneGrunn.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/GodkjentPaVegneGrunn.java new file mode 100644 index 000000000..48e150b0f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/GodkjentPaVegneGrunn.java @@ -0,0 +1,23 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Data; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; + +import javax.persistence.Embeddable; + +@Data +@Embeddable +public class GodkjentPaVegneGrunn { + private boolean ikkeBankId; + private boolean reservert; + private boolean digitalKompetanse; + private boolean arenaMigreringDeltaker; + + public void valgtMinstEnGrunn() { + if (!ikkeBankId && !reservert && !digitalKompetanse && !arenaMigreringDeltaker) { + throw new FeilkodeException(Feilkode.GODKJENT_PAA_VEGNE_GRUNN_MAA_VELGES); + } + } +} + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/HendelseType.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/HendelseType.java new file mode 100644 index 000000000..6c45f5d68 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/HendelseType.java @@ -0,0 +1,53 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum HendelseType { + OPPRETTET("Avtale er opprettet av veileder"), + GODKJENT_AV_ARBEIDSGIVER("Avtale er godkjent av arbeidsgiver"), + GODKJENT_AV_VEILEDER("Avtale er godkjent av veileder"), + GODKJENT_AV_DELTAKER("Avtale er godkjent av deltaker"), + SIGNERT_AV_MENTOR("Mentor har signert taushetserklæring"), + GODKJENT_PAA_VEGNE_AV("Veileder godkjente avtalen på vegne av seg selv og deltaker"), + GODKJENT_PAA_VEGNE_AV_DELTAKER_OG_ARBEIDSGIVER("Veileder godkjente avtalen på vegne av seg selv, deltaker og arbeidsgiver"), + GODKJENT_PAA_VEGNE_AV_ARBEIDSGIVER("Veileder godkjente avtalen på vegne av seg selv og arbeidsgiver"), + GODKJENNINGER_OPPHEVET_AV_ARBEIDSGIVER("Avtalens godkjenninger er opphevet av arbeidsgiver"), + GODKJENNINGER_OPPHEVET_AV_VEILEDER("Avtalens godkjenninger er opphevet av veileder"), + DELT_MED_DELTAKER("Avtale delt med deltaker"), + DELT_MED_ARBEIDSGIVER("Avtale delt med arbeidsgiver"), + DELT_MED_MENTOR("Avtale delt med mentor"), + ENDRET("Avtale endret"), + AVBRUTT("Avtale avbrutt av veileder"), + ANNULLERT("Avtale annullert av veileder"), + LÅST_OPP("Avtale låst opp av veileder"), + GJENOPPRETTET("Avtale gjenopprettet"), + OPPRETTET_AV_ARBEIDSGIVER("Avtale er opprettet av arbeidsgiver"), + NY_VEILEDER("Avtale tildelt ny veileder"), + AVTALE_FORDELT("Avtale tildelt veileder"), + TILSKUDDSPERIODE_AVSLATT("Tilskuddsperiode har blitt sendt i retur av "), + TILSKUDDSPERIODE_GODKJENT("Tilskuddsperiode har blitt godkjent av beslutter"), + AVTALE_FORKORTET("Avtale forkortet"), + AVTALE_FORLENGET("Avtale forlenget av veileder"), + MÅL_ENDRET("Mål endret av veileder"), + INKLUDERINGSTILSKUDD_ENDRET("Inkluderingstilskudd endret av veileder"), + OM_MENTOR_ENDRET("Om mentor endret av veileder"), + TILSKUDDSBEREGNING_ENDRET("Tilskuddsberegning endret av veileder"), + KONTAKTINFORMASJON_ENDRET("Kontaktinformasjon endret av veileder"), + STILLINGSBESKRIVELSE_ENDRET("Stillingsbeskrivelse endret av veileder" ), + OPPFØLGING_OG_TILRETTELEGGING_ENDRET("Oppfølging og tilrettelegging endret av veileder"), + AVTALE_INNGÅTT("Avtale godkjent av NAV"), + REFUSJON_KLAR("Refusjon klar"), + REFUSJON_KLAR_REVARSEL("Refusjon klar, revarsel"), + REFUSJON_FRIST_FORLENGET("Frist for refusjon forlenget"), + REFUSJON_KORRIGERT("Refusjon korrigert"), + VARSLER_SETT("Varsler lest"), + AVTALE_SLETTET("Avtale slettet av veileder"), + GODKJENT_FOR_ETTERREGISTRERING("Avtale er godkjent for etterregistrering"), + FJERNET_ETTERREGISTRERING("Fjernet etterregistrering på avtale"), + STATUSENDRING("Statusendring"); + + private final String tekst; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Identifikator.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Identifikator.java new file mode 100644 index 000000000..ca42891e1 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Identifikator.java @@ -0,0 +1,20 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@EqualsAndHashCode +@ToString +public class Identifikator { + private final String verdi; + + public Identifikator(String verdi) { + this.verdi = verdi; + } + + @JsonValue + public String asString() { + return verdi; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/IdentifikatorConverter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/IdentifikatorConverter.java new file mode 100644 index 000000000..a6fc88003 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/IdentifikatorConverter.java @@ -0,0 +1,21 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +@Converter +public class IdentifikatorConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(Identifikator attribute) { + if(attribute == null){ + return null; + } + return attribute.asString(); + } + + @Override + public Identifikator convertToEntityAttribute(String dbData) { + return new Identifikator(dbData); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/InkluderingstilskuddStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/InkluderingstilskuddStrategy.java new file mode 100644 index 000000000..8a5cc4a96 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/InkluderingstilskuddStrategy.java @@ -0,0 +1,51 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; + +import java.util.HashMap; +import java.util.Map; + +public class InkluderingstilskuddStrategy extends BaseAvtaleInnholdStrategy { + + public InkluderingstilskuddStrategy(AvtaleInnhold avtaleInnhold){ + super(avtaleInnhold); + } + + @Override + public void endre(EndreAvtale nyAvtale) { + sjekkTotalBeløp(); + + avtaleInnhold.getInkluderingstilskuddsutgift().clear(); + avtaleInnhold.getInkluderingstilskuddsutgift().addAll(nyAvtale.getInkluderingstilskuddsutgift()); + avtaleInnhold.getInkluderingstilskuddsutgift().forEach(i -> i.setAvtaleInnhold(avtaleInnhold)); + avtaleInnhold.setInkluderingstilskuddBegrunnelse(nyAvtale.getInkluderingstilskuddBegrunnelse()); + avtaleInnhold.setHarFamilietilknytning(nyAvtale.getHarFamilietilknytning()); + avtaleInnhold.setFamilietilknytningForklaring(nyAvtale.getFamilietilknytningForklaring()); + + super.endre(nyAvtale); + } + + @Override + public Map alleFelterSomMåFyllesUt() { + var alleFelter = new HashMap(); + alleFelter.putAll(super.alleFelterSomMåFyllesUt()); + + alleFelter.put(AvtaleInnhold.Fields.inkluderingstilskuddsutgift, avtaleInnhold.getInkluderingstilskuddsutgift()); + alleFelter.put(AvtaleInnhold.Fields.inkluderingstilskuddBegrunnelse, avtaleInnhold.getInkluderingstilskuddBegrunnelse()); + alleFelter.put(AvtaleInnhold.Fields.harFamilietilknytning, avtaleInnhold.getHarFamilietilknytning()); + if (avtaleInnhold.getHarFamilietilknytning() != null && avtaleInnhold.getHarFamilietilknytning()) { + alleFelter.put(AvtaleInnhold.Fields.familietilknytningForklaring, avtaleInnhold.getFamilietilknytningForklaring()); + } + return alleFelter; + } + + private void sjekkTotalBeløp() { + Integer MAX_SUM = 136700; + Integer sum = avtaleInnhold.inkluderingstilskuddTotalBeløp(); + if (sum > MAX_SUM) { + throw new FeilkodeException(Feilkode.INKLUDERINGSTILSKUDD_SUM_FOR_HØY); + } + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Inkluderingstilskuddsutgift.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Inkluderingstilskuddsutgift.java new file mode 100644 index 000000000..57fa48c49 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Inkluderingstilskuddsutgift.java @@ -0,0 +1,36 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.ToString; +import lombok.experimental.Accessors; +import lombok.experimental.FieldNameConstants; + +import javax.persistence.*; +import java.util.UUID; + +@Data +@Entity +@Accessors(chain = true) +@FieldNameConstants +public class Inkluderingstilskuddsutgift { + @Id + @GeneratedValue + private UUID id; + private Integer beløp; + @Enumerated(EnumType.STRING) + private InkluderingstilskuddsutgiftType type; + @ManyToOne + @JoinColumn(name = "avtale_innhold_id") + @JsonIgnore + @ToString.Exclude + private AvtaleInnhold avtaleInnhold; + + public Inkluderingstilskuddsutgift() {} + + public Inkluderingstilskuddsutgift(Inkluderingstilskuddsutgift utgift) { + id = UUID.randomUUID(); + beløp = utgift.beløp; + type = utgift.type; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/InkluderingstilskuddsutgiftType.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/InkluderingstilskuddsutgiftType.java new file mode 100644 index 000000000..34425d877 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/InkluderingstilskuddsutgiftType.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +public enum InkluderingstilskuddsutgiftType { + TILRETTELEGGINGSBEHOV, + TILTAKSPLASS, + UTSTYR, + PROGRAMVARE, + ARBEIDSHJELPEMIDLER, + OPPLÆRING, + FORSIKRING_LISENS_SERTIFISERING +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/InternBruker.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/InternBruker.java new file mode 100644 index 000000000..c37506649 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/InternBruker.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import java.util.UUID; + +public interface InternBruker { + UUID getAzureOid(); + NavIdent getNavIdent(); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/JusterArenaMigreringsdato.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/JusterArenaMigreringsdato.java new file mode 100644 index 000000000..f29765f01 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/JusterArenaMigreringsdato.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Value; + +import java.time.LocalDate; + +@Value +public class JusterArenaMigreringsdato { + LocalDate migreringsdato; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/LonnstilskuddStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/LonnstilskuddStrategy.java new file mode 100644 index 000000000..2e5352c03 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/LonnstilskuddStrategy.java @@ -0,0 +1,133 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import static no.nav.tag.tiltaksgjennomforing.utils.Utils.erIkkeTomme; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; + +public class LonnstilskuddStrategy extends BaseAvtaleInnholdStrategy { + public LonnstilskuddStrategy(AvtaleInnhold avtaleInnhold) { + super(avtaleInnhold); + } + + @Override + public void endre(EndreAvtale nyAvtale) { + if (nyAvtale.getOtpSats() != null && (nyAvtale.getOtpSats() > 0.3 || nyAvtale.getOtpSats() < 0.0)) { + throw new FeilkodeException(Feilkode.FEIL_OTP_SATS); + } + avtaleInnhold.setArbeidsgiverKontonummer(nyAvtale.getArbeidsgiverKontonummer()); + avtaleInnhold.setManedslonn(nyAvtale.getManedslonn()); + avtaleInnhold.setFeriepengesats(nyAvtale.getFeriepengesats()); + avtaleInnhold.setArbeidsgiveravgift(nyAvtale.getArbeidsgiveravgift()); + avtaleInnhold.setHarFamilietilknytning(nyAvtale.getHarFamilietilknytning()); + avtaleInnhold.setFamilietilknytningForklaring(nyAvtale.getFamilietilknytningForklaring()); + avtaleInnhold.setStillingstype(nyAvtale.getStillingstype()); + avtaleInnhold.setStillingstittel(nyAvtale.getStillingstittel()); + avtaleInnhold.setStillingStyrk08(nyAvtale.getStillingStyrk08()); + avtaleInnhold.setStillingKonseptId(nyAvtale.getStillingKonseptId()); + avtaleInnhold.setOtpSats(nyAvtale.getOtpSats()); + avtaleInnhold.setRefusjonKontaktperson(nyAvtale.getRefusjonKontaktperson()); + super.endre(nyAvtale); + regnUtTotalLonnstilskudd(); + } + + @Override + public void endreTilskuddsberegning(EndreTilskuddsberegning endreTilskuddsberegning) { + avtaleInnhold.setArbeidsgiveravgift(endreTilskuddsberegning.getArbeidsgiveravgift()); + avtaleInnhold.setOtpSats(endreTilskuddsberegning.getOtpSats()); + avtaleInnhold.setManedslonn(endreTilskuddsberegning.getManedslonn()); + avtaleInnhold.setFeriepengesats(endreTilskuddsberegning.getFeriepengesats()); + regnUtTotalLonnstilskudd(); + } + + @Override + public void regnUtTotalLonnstilskudd() { + Integer feriepengerBelop = getFeriepengerBelop(avtaleInnhold.getFeriepengesats(), avtaleInnhold.getManedslonn()); + Integer obligTjenestepensjon = getBeregnetOtpBelop(avtaleInnhold.getOtpSats(), avtaleInnhold.getManedslonn(), feriepengerBelop); + Integer arbeidsgiveravgiftBelop = getArbeidsgiverAvgift(avtaleInnhold.getManedslonn(), feriepengerBelop, obligTjenestepensjon, + avtaleInnhold.getArbeidsgiveravgift()); + Integer sumLonnsutgifter = getSumLonnsutgifter(avtaleInnhold.getManedslonn(), feriepengerBelop, obligTjenestepensjon, arbeidsgiveravgiftBelop); + Integer sumlønnTilskudd = getSumLonnsTilskudd(sumLonnsutgifter, avtaleInnhold.getLonnstilskuddProsent()); + Integer månedslønnFullStilling = getLønnVedFullStilling(sumLonnsutgifter, avtaleInnhold.getStillingprosent()); + avtaleInnhold.setFeriepengerBelop(feriepengerBelop); + avtaleInnhold.setOtpBelop(obligTjenestepensjon); + avtaleInnhold.setArbeidsgiveravgiftBelop(arbeidsgiveravgiftBelop); + avtaleInnhold.setSumLonnsutgifter(sumLonnsutgifter); + avtaleInnhold.setSumLonnstilskudd(sumlønnTilskudd); + avtaleInnhold.setManedslonn100pst(månedslønnFullStilling); + } + + private Integer getLønnVedFullStilling(Integer sumUtgifter, Integer stillingsProsent) { + if (sumUtgifter == null || stillingsProsent == null || stillingsProsent == 0) { + return null; + } + return (sumUtgifter * 100) / stillingsProsent; + } + + Integer getSumLonnsTilskudd(Integer sumLonnsutgifter, Integer lonnstilskuddProsent) { + if (sumLonnsutgifter == null || lonnstilskuddProsent == null) { + return null; + } + double lonnstilskuddProsentSomDecimal = lonnstilskuddProsent != null ? (lonnstilskuddProsent.doubleValue() / 100) : 0; + return (int) Math.round(sumLonnsutgifter * lonnstilskuddProsentSomDecimal); + } + + private Integer getSumLonnsutgifter(Integer manedslonn, Integer feriepengerBelop, Integer obligTjenestepensjon, Integer arbeidsgiveravgiftBelop) { + if (erIkkeTomme(feriepengerBelop, obligTjenestepensjon, arbeidsgiveravgiftBelop)) { + return manedslonn + feriepengerBelop + obligTjenestepensjon + arbeidsgiveravgiftBelop; + } + return null; + } + + private Integer getArbeidsgiverAvgift(Integer manedslonn, Integer feriepengerBelop, Integer obligTjenestepensjon, BigDecimal arbeidsgiveravgift) { + if (erIkkeTomme(manedslonn, feriepengerBelop, obligTjenestepensjon, arbeidsgiveravgift)) { + return (int) Math.round((manedslonn + feriepengerBelop + obligTjenestepensjon) * (arbeidsgiveravgift.doubleValue())); + } + return null; + } + + private Integer getBeregnetOtpBelop(Double optSats, Integer manedslonn, Integer feriepenger) { + if (erIkkeTomme(optSats, manedslonn, feriepenger)) { + return (int) Math.round((manedslonn + feriepenger) * optSats); + } + return null; + } + + private Integer getFeriepengerBelop(BigDecimal feriepengersats, Integer manedslonn) { + if (erIkkeTomme(feriepengersats, manedslonn)) { + return (int) Math.round((feriepengersats.doubleValue()) * manedslonn); + } + return null; + } + + @Override + public Map alleFelterSomMåFyllesUt() { + HashMap alleFelter = new HashMap<>(); + alleFelter.putAll(super.alleFelterSomMåFyllesUt()); + alleFelter.put(AvtaleInnhold.Fields.stillingstittel, avtaleInnhold.getStillingstittel()); + alleFelter.put(AvtaleInnhold.Fields.stillingprosent, avtaleInnhold.getStillingprosent()); + alleFelter.put(AvtaleInnhold.Fields.arbeidsoppgaver, avtaleInnhold.getArbeidsoppgaver()); + alleFelter.put(AvtaleInnhold.Fields.arbeidsgiverKontonummer, avtaleInnhold.getArbeidsgiverKontonummer()); + alleFelter.put(AvtaleInnhold.Fields.manedslonn, avtaleInnhold.getManedslonn()); + alleFelter.put(AvtaleInnhold.Fields.feriepengesats, avtaleInnhold.getFeriepengesats()); + alleFelter.put(AvtaleInnhold.Fields.otpSats, avtaleInnhold.getOtpSats()); + alleFelter.put(AvtaleInnhold.Fields.arbeidsgiveravgift, avtaleInnhold.getArbeidsgiveravgift()); + alleFelter.put(AvtaleInnhold.Fields.harFamilietilknytning, avtaleInnhold.getHarFamilietilknytning()); + alleFelter.put(AvtaleInnhold.Fields.stillingstype, avtaleInnhold.getStillingstype()); + alleFelter.put(AvtaleInnhold.Fields.antallDagerPerUke, avtaleInnhold.getAntallDagerPerUke()); + if (avtaleInnhold.getHarFamilietilknytning() != null && avtaleInnhold.getHarFamilietilknytning()) { + alleFelter.put(AvtaleInnhold.Fields.familietilknytningForklaring, avtaleInnhold.getFamilietilknytningForklaring()); + } + return alleFelter; + } + + @Override + public void endreSluttDato(LocalDate nySluttDato) { + super.endreSluttDato(nySluttDato); + regnUtTotalLonnstilskudd(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Maal.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Maal.java new file mode 100644 index 000000000..0257ca969 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Maal.java @@ -0,0 +1,42 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.ToString; +import lombok.experimental.Accessors; +import lombok.experimental.FieldNameConstants; +import no.nav.tag.tiltaksgjennomforing.utils.Utils; + +import javax.persistence.*; +import java.util.UUID; + +@Data +@Entity +@Accessors(chain = true) +@FieldNameConstants +public class Maal { + @Id + @GeneratedValue + private UUID id; + @Enumerated(EnumType.STRING) + private MaalKategori kategori; + private String beskrivelse; + @ManyToOne + @JoinColumn(name = "avtale_innhold") + @JsonIgnore + @ToString.Exclude + private AvtaleInnhold avtaleInnhold; + + public Maal() {} + + public Maal(Maal fra) { + id = UUID.randomUUID(); + kategori = fra.kategori; + beskrivelse = fra.beskrivelse; + } + + + public void sjekkMaalLengde() { + Utils.sjekkAtTekstIkkeOverskrider1000Tegn(this.getBeskrivelse(), "Maks lengde for mål er 1000 tegn"); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/MaalKategori.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/MaalKategori.java new file mode 100644 index 000000000..6aa207171 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/MaalKategori.java @@ -0,0 +1,20 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +public enum MaalKategori { + FÅ_JOBB_I_BEDRIFTEN("Få jobb i bedriften"), + ARBEIDSERFARING("Arbeidserfaring"), + UTPRØVING("Utprøving"), + SPRÅKOPPLÆRING("Språkopplæring"), + OPPNÅ_FAGBREV_KOMPETANSEBEVIS("Oppnå fagbrev/kompetansebevis"), + ANNET("Annet"); + + private final String verdi; + + MaalKategori(String verdi) { + this.verdi = verdi; + } + + public String getVerdi() { + return verdi; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Mentor.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Mentor.java new file mode 100644 index 000000000..c65d3766b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Mentor.java @@ -0,0 +1,90 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import java.util.UUID; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetBruker; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetMentor; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; +import no.nav.tag.tiltaksgjennomforing.exceptions.TilgangskontrollException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public class Mentor extends Avtalepart { + + public Mentor(Fnr identifikator) { + super(identifikator); + } + + @Override + public boolean harTilgangTilAvtale(Avtale avtale) { + return avtale.getMentorFnr().equals(getIdentifikator()); + } + + @Override + public Avtale hentAvtale(AvtaleRepository avtaleRepository, UUID avtaleId) { + Avtale avtale = super.hentAvtale(avtaleRepository,avtaleId); + if(!avtale.erGodkjentTaushetserklæringAvMentor()) throw new FeilkodeException(Feilkode.IKKE_TILGANG_TIL_AVTALE); + return skjulDeltakerFødselsnummer(avtale); + } + + @Override + Page hentAlleAvtalerMedMuligTilgang(AvtaleRepository avtaleRepository, AvtalePredicate queryParametre, Pageable pageable) { + Page avtaler = avtaleRepository.findAllByMentorFnr(getIdentifikator(), pageable); + Page avtalerMedMuligTilgang = avtaler + .map(this::gjemInnholdOmMentorIkkeHarSignertErklæring) + .map(this::skjulDeltakerFødselsnummer); + return avtalerMedMuligTilgang; + } + + private Avtale gjemInnholdOmMentorIkkeHarSignertErklæring(Avtale avtale){ + if(!avtale.erGodkjentTaushetserklæringAvMentor()) { + AvtaleInnhold innhold = AvtaleInnhold.nyttTomtInnhold(avtale.getTiltakstype()); + innhold.setBedriftNavn(avtale.getGjeldendeInnhold().getBedriftNavn()); + innhold.setAvtale(avtale); + avtale.setGjeldendeInnhold(innhold); + avtale.setDeltakerFnr(null); + avtale.setVeilederNavIdent(null); + } + return avtale; + } + + @Override + public void godkjennForAvtalepart(Avtale avtale) { + avtale.godkjennForMentor(getIdentifikator()); + } + + @Override + public boolean kanEndreAvtale() { + return false; + } + + @Override + public boolean erGodkjentAvInnloggetBruker(Avtale avtale) { + return avtale.getMentorFnr().equals(getIdentifikator().asString()) && avtale.erGodkjentTaushetserklæringAvMentor(); + } + + @Override + boolean kanOppheveGodkjenninger(Avtale avtale) { + return false; + } + + @Override + void opphevGodkjenningerSomAvtalepart(Avtale avtale) { + throw new TilgangskontrollException("Deltaker kan ikke oppheve godkjenninger"); + } + + private Avtale skjulDeltakerFødselsnummer(Avtale avtale){ + avtale.setDeltakerFnr(null); + return avtale; + } + + @Override + protected Avtalerolle rolle() { + return Avtalerolle.MENTOR; + } + + @Override + public InnloggetBruker innloggetBruker() { + return new InnloggetMentor(getIdentifikator()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/MentorStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/MentorStrategy.java new file mode 100644 index 000000000..3f8acc853 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/MentorStrategy.java @@ -0,0 +1,43 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import java.util.HashMap; +import java.util.Map; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleInnhold.Fields; + +public class MentorStrategy extends BaseAvtaleInnholdStrategy { + + public MentorStrategy(AvtaleInnhold avtaleInnhold) { + super(avtaleInnhold); + } + + @Override + public void endre(EndreAvtale nyAvtale) { + avtaleInnhold.setMentorFornavn(nyAvtale.getMentorFornavn()); + avtaleInnhold.setMentorEtternavn(nyAvtale.getMentorEtternavn()); + avtaleInnhold.setMentorOppgaver(nyAvtale.getMentorOppgaver()); + avtaleInnhold.setMentorAntallTimer(nyAvtale.getMentorAntallTimer()); + avtaleInnhold.setMentorTlf(nyAvtale.getMentorTlf()); + avtaleInnhold.setMentorTimelonn(nyAvtale.getMentorTimelonn()); + avtaleInnhold.setHarFamilietilknytning(nyAvtale.getHarFamilietilknytning()); + avtaleInnhold.setFamilietilknytningForklaring(nyAvtale.getFamilietilknytningForklaring()); + super.endre(nyAvtale); + } + + @Override + public Map alleFelterSomMåFyllesUt() { + var alleFelter = new HashMap(); + alleFelter.putAll(super.alleFelterSomMåFyllesUt()); + alleFelter.put(AvtaleInnhold.Fields.mentorFornavn, avtaleInnhold.getMentorFornavn()); + alleFelter.put(AvtaleInnhold.Fields.mentorEtternavn, avtaleInnhold.getMentorEtternavn()); + alleFelter.put(AvtaleInnhold.Fields.mentorOppgaver, avtaleInnhold.getMentorOppgaver()); + alleFelter.put(AvtaleInnhold.Fields.mentorAntallTimer, avtaleInnhold.getMentorAntallTimer()); + alleFelter.put(AvtaleInnhold.Fields.mentorTimelonn, avtaleInnhold.getMentorTimelonn()); + alleFelter.put(Fields.mentorTlf, avtaleInnhold.getMentorTlf()); + alleFelter.put(AvtaleInnhold.Fields.harFamilietilknytning, avtaleInnhold.getHarFamilietilknytning()); + if (avtaleInnhold.getHarFamilietilknytning() != null && avtaleInnhold.getHarFamilietilknytning()) { + alleFelter.put(AvtaleInnhold.Fields.familietilknytningForklaring, avtaleInnhold.getFamilietilknytningForklaring()); + } + + return alleFelter; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/MidlertidigLonnstilskuddStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/MidlertidigLonnstilskuddStrategy.java new file mode 100644 index 000000000..a27c52504 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/MidlertidigLonnstilskuddStrategy.java @@ -0,0 +1,87 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilLonnstilskuddsprosentException; + +import java.time.LocalDate; + +public class MidlertidigLonnstilskuddStrategy extends LonnstilskuddStrategy { + + public MidlertidigLonnstilskuddStrategy(AvtaleInnhold avtaleInnhold) { + super(avtaleInnhold); + } + + @Override + public void endreAvtaleInnholdMedKvalifiseringsgruppe(EndreAvtale endreAvtale, Kvalifiseringsgruppe kvalifiseringsgruppe) { + if (kvalifiseringsgruppe != null) { + settTilskuddsprosentSats(kvalifiseringsgruppe); + this.endre(endreAvtale); + } else { + sjekktilskuddsprosentSats(endreAvtale); + super.endre(endreAvtale); + } + } + + @Override + public void endre(EndreAvtale endreAvtale) { + sjekktilskuddsprosentSats(endreAvtale); + super.endre(endreAvtale); + } + + @Override + public void regnUtTotalLonnstilskudd() { + super.regnUtTotalLonnstilskudd(); + regnUtDatoOgSumRedusert(); + } + + private void sjekktilskuddsprosentSats(EndreAvtale endreAvtale) { + if (endreAvtale.getLonnstilskuddProsent() != null && ( + endreAvtale.getLonnstilskuddProsent() != 40 && endreAvtale.getLonnstilskuddProsent() != 60)) { + throw new FeilLonnstilskuddsprosentException(); + } + } + + private void settTilskuddsprosentSats(Kvalifiseringsgruppe kvalifiseringsgruppe) { + final Integer sats = kvalifiseringsgruppe.finnLonntilskuddProsentsatsUtifraKvalifiseringsgruppe(40, 60); + avtaleInnhold.setLonnstilskuddProsent(sats); + } + + private void regnUtDatoOgSumRedusert() { + LocalDate datoForRedusertProsent = getDatoForRedusertProsent(avtaleInnhold.getStartDato(), avtaleInnhold.getSluttDato(), avtaleInnhold.getLonnstilskuddProsent()); + avtaleInnhold.setDatoForRedusertProsent(datoForRedusertProsent); + Integer sumLønnstilskuddRedusert = regnUtRedusertLønnstilskudd(); + avtaleInnhold.setSumLønnstilskuddRedusert(sumLønnstilskuddRedusert); + } + + private Integer regnUtRedusertLønnstilskudd() { + if (avtaleInnhold.getDatoForRedusertProsent() != null && avtaleInnhold.getLonnstilskuddProsent() != null) { + return getSumLonnsTilskudd(avtaleInnhold.getSumLonnsutgifter(), avtaleInnhold.getLonnstilskuddProsent() - 10); + } else { + return null; + } + } + + private LocalDate getDatoForRedusertProsent(LocalDate startDato, LocalDate sluttDato, Integer lonnstilskuddprosent) { + if (startDato == null || sluttDato == null || lonnstilskuddprosent == null) { + return null; + } + if (lonnstilskuddprosent == 40) { + if (startDato.plusMonths(6).minusDays(1).isBefore(sluttDato)) { + return startDato.plusMonths(6); + } + + } else if (lonnstilskuddprosent == 60) { + if (startDato.plusYears(1).minusDays(1).isBefore(sluttDato)) { + return startDato.plusYears(1); + } + } + + return null; + } + + @Override + public void endreSluttDato(LocalDate nySluttDato) { + super.endreSluttDato(nySluttDato); + regnUtDatoOgSumRedusert(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/NavIdent.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/NavIdent.java new file mode 100644 index 000000000..cf30d67c6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/NavIdent.java @@ -0,0 +1,17 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class NavIdent extends Identifikator { + public NavIdent(String verdi) { + super(verdi); + if (!erNavIdent(verdi)) { + throw new IllegalArgumentException("Er ikke en nav-ident"); + } + } + + public static boolean erNavIdent(String verdi) { + return verdi != null && verdi.matches("\\w\\d{6}"); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/NavIdentConverter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/NavIdentConverter.java new file mode 100644 index 000000000..d96363e32 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/NavIdentConverter.java @@ -0,0 +1,20 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +@Converter +public class NavIdentConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(NavIdent attribute) { + if (attribute == null) return null; + return attribute.asString(); + } + + @Override + public NavIdent convertToEntityAttribute(String dbData) { + if (dbData == null) return null; + return new NavIdent(dbData); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/NyttKostnadssted.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/NyttKostnadssted.java new file mode 100644 index 000000000..be3922ea2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/NyttKostnadssted.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Value; + +@Value +public class NyttKostnadssted { + String enhet; + String enhetsnavn; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/OpprettAvtale.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/OpprettAvtale.java new file mode 100644 index 000000000..7bb2a0dbe --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/OpprettAvtale.java @@ -0,0 +1,18 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OpprettAvtale { + private Fnr deltakerFnr; + private BedriftNr bedriftNr; + private Tiltakstype tiltakstype; + + boolean erLønnstilskudd() { + return tiltakstype.equals(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD) || tiltakstype.equals(Tiltakstype.VARIG_LONNSTILSKUDD); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/OpprettMentorAvtale.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/OpprettMentorAvtale.java new file mode 100644 index 000000000..b2fa976b3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/OpprettMentorAvtale.java @@ -0,0 +1,16 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OpprettMentorAvtale { + private Fnr deltakerFnr; + private Fnr mentorFnr; + private BedriftNr bedriftNr; + private Tiltakstype tiltakstype; + private Avtalerolle avtalerolle; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/RefusjonKontaktperson.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/RefusjonKontaktperson.java new file mode 100644 index 000000000..47f3ce620 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/RefusjonKontaktperson.java @@ -0,0 +1,29 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import javax.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Value; +import lombok.experimental.FieldNameConstants; + +@Value +@Embeddable +@AllArgsConstructor +@FieldNameConstants +public class RefusjonKontaktperson { + + String refusjonKontaktpersonFornavn; + String refusjonKontaktpersonEtternavn; + String refusjonKontaktpersonTlf; + Boolean ønskerVarslingOmRefusjon; + + public RefusjonKontaktperson() { + this.refusjonKontaktpersonFornavn = ""; + this.refusjonKontaktpersonEtternavn = ""; + this.refusjonKontaktpersonTlf = ""; + this.ønskerVarslingOmRefusjon = true; + } + + public boolean erIkkeTom() { + return refusjonKontaktpersonEtternavn != null && refusjonKontaktpersonFornavn != null && refusjonKontaktpersonTlf != null && !this.refusjonKontaktpersonEtternavn.isEmpty() && !refusjonKontaktpersonFornavn.isEmpty() && !refusjonKontaktpersonTlf.isEmpty(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/RefusjonStatus.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/RefusjonStatus.java new file mode 100644 index 000000000..7699697fc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/RefusjonStatus.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +public enum RefusjonStatus { + KLAR_FOR_INNSENDING, + FOR_TIDLIG, + SENDT_KRAV, + UTBETALT, + KORRIGERT, + UTGÅTT, + ANNULLERT, + UTBETALING_FEILET, + GODKJENT_MINUSBELØP, + GODKJENT_NULLBELØP +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/RegnUtTilskuddsperioderForAvtale.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/RegnUtTilskuddsperioderForAvtale.java new file mode 100644 index 000000000..2ebe4d111 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/RegnUtTilskuddsperioderForAvtale.java @@ -0,0 +1,130 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; +import no.nav.tag.tiltaksgjennomforing.utils.Periode; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.Period; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static no.nav.tag.tiltaksgjennomforing.utils.DatoUtils.sisteDatoIMnd; + +@Slf4j +@UtilityClass +public class RegnUtTilskuddsperioderForAvtale { + + private final static BigDecimal DAGER_I_MÅNED = new BigDecimal("30.4375"); + private final static int ANTALL_MÅNEDER_I_EN_PERIODE = 1; + + /** + TODO: TODO: Kalkulering av redusert prosent og redusert dato bør kun skje i {@link no.nav.tag.tiltaksgjennomforing.avtale.VarigLonnstilskuddStrategy} og heller ikke i frontend + */ + public static List beregnTilskuddsperioderForAvtale(UUID id, Tiltakstype tiltakstype, Integer sumLønnstilskuddPerMåned, LocalDate datoFraOgMed, LocalDate datoTilOgMed, Integer lonnstilskuddprosent, LocalDate datoForRedusertProsent, Integer sumLønnstilskuddPerMånedRedusert) { + if (datoForRedusertProsent == null) { + // Ingen reduserte peridoder -----60-----60------60------ | + return lagPeriode(datoFraOgMed, datoTilOgMed).stream().map(datoPar -> { + Integer beløp = beløpForPeriode(datoPar.getStart(), datoPar.getSlutt(), sumLønnstilskuddPerMåned); + return new TilskuddPeriode(beløp, datoPar.getStart(), datoPar.getSlutt(), lonnstilskuddprosent); + }).collect(Collectors.toList()); + } else { + if (datoFraOgMed.isBefore(datoForRedusertProsent.plusDays(1)) && datoTilOgMed.isAfter(datoForRedusertProsent.minusDays(1))) { + // Både ikke reduserte og reduserte ---60---60-----50----|--50----50----- + List tilskuddperioderFørRedusering = lagPeriode(datoFraOgMed, datoForRedusertProsent.minusDays(1)).stream().map(datoPar -> { + Integer beløp = beløpForPeriode(datoPar.getStart(), datoPar.getSlutt(), sumLønnstilskuddPerMåned); + return new TilskuddPeriode(beløp, datoPar.getStart(), datoPar.getSlutt(), lonnstilskuddprosent); + }).collect(Collectors.toList()); + + List tilskuddperioderEtterRedusering = lagPeriode(datoForRedusertProsent, datoTilOgMed).stream().map(datoPar -> { + Integer beløp = beløpForPeriode(datoPar.getStart(), datoPar.getSlutt(), sumLønnstilskuddPerMånedRedusert); + return new TilskuddPeriode(beløp, datoPar.getStart(), datoPar.getSlutt(), getLonnstilskuddProsent(tiltakstype, lonnstilskuddprosent)); + }).collect(Collectors.toList()); + + ArrayList tilskuddsperioder = new ArrayList<>(); + tilskuddsperioder.addAll(tilskuddperioderFørRedusering); + tilskuddsperioder.addAll(tilskuddperioderEtterRedusering); + return tilskuddsperioder; + } else if (datoFraOgMed.isAfter(datoForRedusertProsent)) { + // Kun redusete peridoer ---60----60----60---50---|--50----50---50--50-- + List tilskuddperioderEtterRedusering = lagPeriode(datoFraOgMed, datoTilOgMed).stream().map(datoPar -> { + Integer beløp = beløpForPeriode(datoPar.getStart(), datoPar.getSlutt(), sumLønnstilskuddPerMånedRedusert); + return new TilskuddPeriode(beløp, datoPar.getStart(), datoPar.getSlutt(), getLonnstilskuddProsent(tiltakstype, lonnstilskuddprosent)); + }).collect(Collectors.toList()); + ArrayList tilskuddsperioder = new ArrayList<>(); + tilskuddsperioder.addAll(tilskuddperioderEtterRedusering); + return tilskuddsperioder; + } else { + log.error("Uventet feil i utregning av tilskuddsperioder med startdato: {}, sluttdato: {}, datoForRedusertProsent: {}, avtaleId: {}", datoFraOgMed, datoTilOgMed, datoForRedusertProsent, id); + throw new FeilkodeException(Feilkode.FORLENG_MIDLERTIDIG_IKKE_TILGJENGELIG); + } + } + + } + + private static int getLonnstilskuddProsent(Tiltakstype tiltakstype, Integer lonnstilskuddprosent) { + if(tiltakstype == Tiltakstype.VARIG_LONNSTILSKUDD){ + if(lonnstilskuddprosent >= 68) return 67; + return lonnstilskuddprosent; + } + return lonnstilskuddprosent - 10; + } + + public static Integer beløpForPeriode(LocalDate datoFraOgMed, LocalDate datoTilOgMed, LocalDate datoForRedusertProsent, Integer sumLønnstilskuddPerMåned, Integer sumLønnstilskuddPerMånedRedusert) { + if (datoForRedusertProsent == null || datoTilOgMed.isBefore(datoForRedusertProsent)) { + return beløpForPeriode(datoFraOgMed, datoTilOgMed, sumLønnstilskuddPerMåned); + } else { + return beløpForPeriode(datoFraOgMed, datoTilOgMed, sumLønnstilskuddPerMånedRedusert); + } + } + + public static Integer beløpForPeriode(LocalDate fra, LocalDate til, Integer sumLønnstilskuddPerMåned) { + Period period = Period.between(fra, til.plusDays(1)); + Integer sumHeleMåneder = period.getMonths() * sumLønnstilskuddPerMåned; + BigDecimal dagsats = new BigDecimal(sumLønnstilskuddPerMåned).divide(DAGER_I_MÅNED, 10, RoundingMode.HALF_UP); + Integer sumEnkeltdager = dagsats.multiply(BigDecimal.valueOf(period.getDays()), MathContext.UNLIMITED).setScale(0, RoundingMode.HALF_UP).intValue(); + return sumHeleMåneder + sumEnkeltdager; + } + + private static List lagPeriode(LocalDate datoFraOgMed, LocalDate datoTilOgMed) { + if (datoFraOgMed.isAfter(datoTilOgMed)) { + return List.of(); + } + List startDatoer = datoFraOgMed.datesUntil(datoTilOgMed.plusDays(1), Period.ofMonths(ANTALL_MÅNEDER_I_EN_PERIODE)).collect(Collectors.toList()); + ArrayList datoPar = new ArrayList<>(); + for (int i = 0; i < startDatoer.size(); i++) { + // fra: Hvis startdato er lik datoFraOgMed, bruk denne, hvis ikke, bruk første datoen i mnd. + LocalDate fra = startDatoer.get(i).equals(datoFraOgMed) ? startDatoer.get(i) : førsteDatoIMnd(startDatoer.get(i)); + // til: Hvis siste dag i mnd. er mindre enn datoTilOgMed, bruk siste dag i mnd, ellers bruk datoTilOgMed + LocalDate til = sisteDatoIMnd(startDatoer.get(i)).isBefore(datoTilOgMed) ? sisteDatoIMnd(startDatoer.get(i)) : datoTilOgMed; + + datoPar.addAll(splittHvisNyttÅr(fra, til)); + } + // Legg til siste periode hvis den ikke kom med i loopen + if (datoPar.get(datoPar.size() - 1).getSlutt() != datoTilOgMed) { + datoPar.addAll(splittHvisNyttÅr(førsteDatoIMnd(datoTilOgMed), datoTilOgMed)); + } + return datoPar; + } + + private LocalDate førsteDatoIMnd(LocalDate dato) { + return LocalDate.of(dato.getYear(), dato.getMonth(), 01); + } + + private static List splittHvisNyttÅr (LocalDate fraDato, LocalDate tilDato) { + if (fraDato.getYear() != tilDato.getYear()) { + Periode datoPar1 = new Periode(fraDato, fraDato.withMonth(12).withDayOfMonth(31)); + Periode datoPar2 = new Periode(tilDato.withMonth(1).withDayOfMonth(1), tilDato); + return List.of(datoPar1, datoPar2); + } else { + return List.of(new Periode(fraDato, tilDato)); + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/SalesforceKontorerConfig.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/SalesforceKontorerConfig.java new file mode 100644 index 000000000..af5cd1fbe --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/SalesforceKontorerConfig.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.salesforcekontorer") +public class SalesforceKontorerConfig { + private List enheter = Collections.emptyList(); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/SommerjobbStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/SommerjobbStrategy.java new file mode 100644 index 000000000..db565d4f5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/SommerjobbStrategy.java @@ -0,0 +1,44 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilLonnstilskuddsprosentException; + +public class SommerjobbStrategy extends LonnstilskuddStrategy { + + public SommerjobbStrategy(AvtaleInnhold avtaleInnhold) { + super(avtaleInnhold); + } + + @Override + public void endreAvtaleInnholdMedKvalifiseringsgruppe(EndreAvtale endreAvtale, Kvalifiseringsgruppe kvalifiseringsgruppe) { + if (kvalifiseringsgruppe != null) { + final EndreAvtale endreAvtaleMedOppdatertProsentsats = settTilskuddsprosentSats(endreAvtale, kvalifiseringsgruppe); + super.endre(endreAvtaleMedOppdatertProsentsats); + } else { + sjekkSommerjobbProsentsats(endreAvtale); + super.endre(endreAvtale); + } + } + + @Override + public void endre(EndreAvtale endreAvtale) { + endreAvtale.setStillingstype(Stillingstype.MIDLERTIDIG); + sjekkSommerjobbProsentsats(endreAvtale); + avtaleInnhold.setLonnstilskuddProsent(endreAvtale.getLonnstilskuddProsent()); + super.endre(endreAvtale); + } + + private void sjekkSommerjobbProsentsats(EndreAvtale endreAvtale) { + Integer lonnstilskuddProsent = endreAvtale.getLonnstilskuddProsent(); + if (lonnstilskuddProsent != null && !(lonnstilskuddProsent == 75 || lonnstilskuddProsent == 50)) { + throw new FeilLonnstilskuddsprosentException(); + } + } + + private EndreAvtale settTilskuddsprosentSats(EndreAvtale endreAvtale, Kvalifiseringsgruppe kvalifiseringsgruppe) { + final Integer sats = kvalifiseringsgruppe.finnLonntilskuddProsentsatsUtifraKvalifiseringsgruppe(50, 75); + endreAvtale.setLonnstilskuddProsent(sats); + return endreAvtale; + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/SortingDirection.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/SortingDirection.java new file mode 100644 index 000000000..316170017 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/SortingDirection.java @@ -0,0 +1,5 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +public enum SortingDirection { + ASC, DESC +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/SortingOrder.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/SortingOrder.java new file mode 100644 index 000000000..1026e9784 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/SortingOrder.java @@ -0,0 +1,5 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +enum SortingOrder { + OPPRETTETTIDSPUNKT, BEDRIFTNAVN, DELTAKERFORNAVN, STATUS, STARTDATO, SISTENDRET, TILTAKSTYPE +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Status.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Status.java new file mode 100644 index 000000000..78e4d9ae2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Status.java @@ -0,0 +1,21 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +public enum Status { + ANNULLERT("Annullert"), + AVBRUTT("Avbrutt"), + PÅBEGYNT("Påbegynt"), + MANGLER_GODKJENNING("Mangler godkjenning"), + KLAR_FOR_OPPSTART("Klar for oppstart"), + GJENNOMFØRES("Gjennomføres"), + AVSLUTTET("Avsluttet"); + + private final String beskrivelse; + + Status(String beskrivelse) { + this.beskrivelse = beskrivelse; + } + + public String getBeskrivelse() { + return beskrivelse; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Stillingstype.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Stillingstype.java new file mode 100644 index 000000000..04b3e0adc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Stillingstype.java @@ -0,0 +1,5 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +public enum Stillingstype { + FAST, MIDLERTIDIG +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddPeriode.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddPeriode.java new file mode 100644 index 000000000..c5e928554 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddPeriode.java @@ -0,0 +1,166 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.apache.commons.lang3.builder.CompareToBuilder; +import org.jetbrains.annotations.NotNull; + +import javax.persistence.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.EnumSet; +import java.util.Set; +import java.util.UUID; + +@Entity +@Data +@NoArgsConstructor +@RequiredArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@Builder(toBuilder = true) +public class TilskuddPeriode implements Comparable { + + @Id + @EqualsAndHashCode.Include + private UUID id = UUID.randomUUID(); + + @ManyToOne + @JoinColumn(name = "avtale_id") + @JsonIgnore + @ToString.Exclude + private Avtale avtale; + + @NonNull + private Integer beløp; + @NonNull + private LocalDate startDato; + @NonNull + private LocalDate sluttDato; + + @Convert(converter = NavIdentConverter.class) + private NavIdent godkjentAvNavIdent; + + private LocalDateTime godkjentTidspunkt; + + private String enhet; + private String enhetsnavn; + + @NonNull + private Integer lonnstilskuddProsent; + + @Enumerated(EnumType.STRING) + @ElementCollection(fetch = FetchType.LAZY) + private Set avslagsårsaker = EnumSet.noneOf(Avslagsårsak.class); + + private String avslagsforklaring; + @Convert(converter = NavIdentConverter.class) + private NavIdent avslåttAvNavIdent; + private LocalDateTime avslåttTidspunkt; + private Integer løpenummer = 1; + + @Enumerated(EnumType.STRING) + private TilskuddPeriodeStatus status = TilskuddPeriodeStatus.UBEHANDLET; + + @Enumerated(EnumType.STRING) + private RefusjonStatus refusjonStatus = null; + + private boolean aktiv = true; + + public TilskuddPeriode deaktiverOgLagNyUbehandlet() { + this.aktiv = false; + TilskuddPeriode kopi = new TilskuddPeriode(); + kopi.id = UUID.randomUUID(); + kopi.løpenummer = this.løpenummer; + kopi.beløp = this.beløp; + kopi.lonnstilskuddProsent = this.lonnstilskuddProsent; + kopi.startDato = this.startDato; + kopi.sluttDato = this.sluttDato; + kopi.avtale = this.avtale; + kopi.aktiv = true; + kopi.status = TilskuddPeriodeStatus.UBEHANDLET; + return kopi; + } + + private void sjekkOmKanBehandles() { + if (status != TilskuddPeriodeStatus.UBEHANDLET) { + throw new FeilkodeException(Feilkode.TILSKUDDSPERIODE_ER_ALLEREDE_BEHANDLET); + } + if (Now.localDate().isBefore(kanBesluttesFom())) { + throw new FeilkodeException(Feilkode.TILSKUDDSPERIODE_BEHANDLE_FOR_TIDLIG); + } + } + + @JsonProperty + private LocalDate kanBesluttesFom() { + // TODO: DENNE KODEN MÅ FJERNES NÅR VI FÅR BESKJED OM AT DET ER OK Å HOLDE AV PENGER FOR NESTE ÅR + if (LocalDate.now().getYear() == 2023 && startDato.getYear() == 2024) { + if (startDato.minusMonths(3).getYear() == 2023) { + // Setter kun 01-01-2024 hvis den opprinnelig hadde blitt satt til 2023. + return LocalDate.of(2024, 01, 1); + } + } + + if (løpenummer == 1) { + return LocalDate.MIN; + } + return startDato.minusMonths(3); + } + + void godkjenn(NavIdent beslutter, String enhet) { + sjekkOmKanBehandles(); + + setGodkjentTidspunkt(Now.localDateTime()); + setGodkjentAvNavIdent(beslutter); + setEnhet(enhet); + setStatus(TilskuddPeriodeStatus.GODKJENT); + } + + void avslå(NavIdent beslutter, EnumSet avslagsårsaker, String avslagsforklaring) { + sjekkOmKanBehandles(); + if (avslagsforklaring.isBlank()) { + throw new FeilkodeException(Feilkode.TILSKUDDSPERIODE_AVSLAGSFORKLARING_PAAKREVD); + } + if (avslagsårsaker.isEmpty()) { + throw new FeilkodeException(Feilkode.TILSKUDDSPERIODE_INGEN_AVSLAGSAARSAKER); + } + + setAvslåttTidspunkt(Now.localDateTime()); + setAvslåttAvNavIdent(beslutter); + this.avslagsårsaker.addAll(avslagsårsaker); + setAvslagsforklaring(avslagsforklaring); + setStatus(TilskuddPeriodeStatus.AVSLÅTT); + } + + public boolean kanBehandles() { + try { + sjekkOmKanBehandles(); + return true; + } catch (FeilkodeException e) { + return false; + } + } + + @Override + public int compareTo(@NotNull TilskuddPeriode o) { + return new CompareToBuilder() + .append(this.getStartDato(), o.getStartDato()) + .append(this.isAktiv(), o.isAktiv()) + .append(this.getStatus(), o.getStatus()) + .append(this.getId(), o.getId()) + .toComparison(); + } + + public boolean erUtbetalt() { + return refusjonStatus == RefusjonStatus.UTBETALT || refusjonStatus == RefusjonStatus.KORRIGERT; + } + + public boolean erRefusjonGodkjent() { + return refusjonStatus == RefusjonStatus.SENDT_KRAV || refusjonStatus == RefusjonStatus.GODKJENT_MINUSBELØP || refusjonStatus == RefusjonStatus.GODKJENT_NULLBELØP; + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddPeriodeRepository.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddPeriodeRepository.java new file mode 100644 index 000000000..330485951 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddPeriodeRepository.java @@ -0,0 +1,18 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface TilskuddPeriodeRepository extends JpaRepository, JpaSpecificationExecutor { + + @Override + Optional findById(UUID id); + + List findAllByAvtaleAndSluttDatoBefore(Avtale avtale, LocalDate sluttDato); + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddPeriodeStatus.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddPeriodeStatus.java new file mode 100644 index 000000000..e8e5dd817 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddPeriodeStatus.java @@ -0,0 +1,5 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +public enum TilskuddPeriodeStatus { + ANNULLERT, GODKJENT, AVSLÅTT, UBEHANDLET, BEHANDLET_I_ARENA +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddsperiodeConfig.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddsperiodeConfig.java new file mode 100644 index 000000000..9bbe224fc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddsperiodeConfig.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.EnumSet; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.tilskuddsperioder") +public class TilskuddsperiodeConfig { + private EnumSet tiltakstyper = EnumSet.allOf(Tiltakstype.class); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Tiltakstype.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Tiltakstype.java new file mode 100644 index 000000000..6b68ab6fc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Tiltakstype.java @@ -0,0 +1,32 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +public enum Tiltakstype { + ARBEIDSTRENING("Arbeidstrening", "ab0422", "ARBTREN"), + MIDLERTIDIG_LONNSTILSKUDD("Midlertidig lønnstilskudd", "ab0336", "MIDLONTIL"), + VARIG_LONNSTILSKUDD("Varig lønnstilskudd", "ab0337", "VARLONTIL"), + MENTOR("Mentor", "ab0416", "MENTOR"), + INKLUDERINGSTILSKUDD("Inkluderingstilskudd", "ab0417", "INKLUTILS"), + SOMMERJOBB("Sommerjobb", "ab0450", null); + + final String beskrivelse; + final String behandlingstema; + final String tiltakskodeArena; + + Tiltakstype(String beskrivelse, String behandlingstema, String tiltakskodeArena) { + this.beskrivelse = beskrivelse; + this.behandlingstema = behandlingstema; + this.tiltakskodeArena = tiltakskodeArena; + } + + public String getBeskrivelse() { + return this.beskrivelse; + } + + public String getBehandlingstema() { + return this.behandlingstema; + } + + public String getTiltakskodeArena() { + return tiltakskodeArena; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/VarigLonnstilskuddStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/VarigLonnstilskuddStrategy.java new file mode 100644 index 000000000..5e934562f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/VarigLonnstilskuddStrategy.java @@ -0,0 +1,81 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import java.time.LocalDate; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilLonnstilskuddsprosentException; + +import java.util.Map; + +/** + * TODO: Kalkulering av redusert prosent og redusert dato bør kun skje her og ikke i @see avtale.java og heller ikke i frontend + * + */ +public class VarigLonnstilskuddStrategy extends LonnstilskuddStrategy { + + public static final int GRENSE_68_PROSENT_ETTER_12_MND = 68; + public static final int MAX_67_PROSENT_ETTER_12_MND = 67; + + @Override + public Map alleFelterSomMåFyllesUt() { + Map felterSomMåFyllesUt = super.alleFelterSomMåFyllesUt(); + felterSomMåFyllesUt.put(AvtaleInnhold.Fields.lonnstilskuddProsent, avtaleInnhold.getLonnstilskuddProsent()); + return felterSomMåFyllesUt; + } + + public VarigLonnstilskuddStrategy(AvtaleInnhold avtaleInnhold) { + super(avtaleInnhold); + } + + @Override + public void endre(EndreAvtale endreAvtale) { + if (endreAvtale.getLonnstilskuddProsent() != null && ( + endreAvtale.getLonnstilskuddProsent() < 0 || endreAvtale.getLonnstilskuddProsent() > 75)) { + throw new FeilLonnstilskuddsprosentException(); + } + avtaleInnhold.setLonnstilskuddProsent(endreAvtale.getLonnstilskuddProsent()); + super.endre(endreAvtale); + } + + @Override + public void regnUtTotalLonnstilskudd() { + super.regnUtTotalLonnstilskudd(); + regnUtDatoOgSumRedusert(); + } + + @Override + public void reUtregnRedusertProsentOgSum() { + regnUtDatoOgSumRedusert(); + } + + private LocalDate getDatoForRedusertProsent(LocalDate startDato, LocalDate sluttDato, Integer lonnstilskuddprosent) { + if (startDato == null || sluttDato == null || lonnstilskuddprosent == null) { + return null; + } + if (startDato.plusYears(1).minusDays(1).isBefore(sluttDato)) { + return startDato.plusYears(1); + } + return null; + } + private void regnUtDatoOgSumRedusert() { + if(avtaleInnhold.getLonnstilskuddProsent() == null || avtaleInnhold.getLonnstilskuddProsent() < GRENSE_68_PROSENT_ETTER_12_MND) { + avtaleInnhold.setDatoForRedusertProsent(null); + avtaleInnhold.setSumLønnstilskuddRedusert(null); + return; + } + LocalDate datoForRedusertProsent = getDatoForRedusertProsent(avtaleInnhold.getStartDato(), avtaleInnhold.getSluttDato(), avtaleInnhold.getLonnstilskuddProsent()); + avtaleInnhold.setDatoForRedusertProsent(datoForRedusertProsent); + Integer sumLønnstilskuddRedusert = regnUtRedusertLønnstilskudd(); + avtaleInnhold.setSumLønnstilskuddRedusert(sumLønnstilskuddRedusert); + + } + + private Integer regnUtRedusertLønnstilskudd() { + if (avtaleInnhold.getDatoForRedusertProsent() != null && avtaleInnhold.getLonnstilskuddProsent() != null) { + int lonnstilskuddProsent = avtaleInnhold.getLonnstilskuddProsent(); + if(lonnstilskuddProsent >= GRENSE_68_PROSENT_ETTER_12_MND) lonnstilskuddProsent = MAX_67_PROSENT_ETTER_12_MND; + return getSumLonnsTilskudd(avtaleInnhold.getSumLonnsutgifter(), lonnstilskuddProsent); + } else { + return null; + } + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Veileder.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Veileder.java new file mode 100644 index 000000000..347e7e030 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/Veileder.java @@ -0,0 +1,525 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetBruker; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetVeileder; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.SlettemerkeProperties; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.TilgangskontrollService; +import no.nav.tag.tiltaksgjennomforing.enhet.*; +import no.nav.tag.tiltaksgjennomforing.exceptions.*; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.NavEnhet; +import no.nav.tag.tiltaksgjennomforing.persondata.PdlRespons; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; +import org.springframework.data.domain.*; + +import java.sql.Date; +import java.time.Instant; +import java.time.LocalDate; +import java.util.*; + +import static no.nav.tag.tiltaksgjennomforing.persondata.PersondataService.hentNavnFraPdlRespons; + +@Slf4j +public class Veileder extends Avtalepart implements InternBruker { + private final TilgangskontrollService tilgangskontrollService; + + private final PersondataService persondataService; + private final SlettemerkeProperties slettemerkeProperties; + private final boolean harAdGruppeForBeslutter; + private final Norg2Client norg2Client; + private final Set navEnheter; + private final VeilarbArenaClient veilarbArenaClient; + private final UUID azureOid; + + public Veileder( + NavIdent identifikator, + UUID azureOid, + TilgangskontrollService tilgangskontrollService, + PersondataService persondataService, + Norg2Client norg2Client, + Set navEnheter, + SlettemerkeProperties slettemerkeProperties, + boolean harAdGruppeForBeslutter, + VeilarbArenaClient veilarbArenaClient + ) { + + super(identifikator); + this.azureOid = azureOid; + this.tilgangskontrollService = tilgangskontrollService; + this.persondataService = persondataService; + this.norg2Client = norg2Client; + this.navEnheter = navEnheter; + this.slettemerkeProperties = slettemerkeProperties; + this.harAdGruppeForBeslutter = harAdGruppeForBeslutter; + this.veilarbArenaClient = veilarbArenaClient; + } + + @Deprecated + public Veileder( + NavIdent identifikator, + TilgangskontrollService tilgangskontrollService, + PersondataService persondataService, + Norg2Client norg2Client, + Set navEnheter, + SlettemerkeProperties slettemerkeProperties, + boolean harAdGruppeForBeslutter, + VeilarbArenaClient veilarbArenaClient + ) { + this(identifikator, null, tilgangskontrollService, persondataService, norg2Client, navEnheter, slettemerkeProperties, harAdGruppeForBeslutter, veilarbArenaClient); + } + + @Override + public boolean harTilgangTilAvtale(Avtale avtale) { + boolean harTilgang = tilgangskontrollService.harSkrivetilgangTilKandidat(this, avtale.getDeltakerFnr()); + if(!harTilgang) { + log.info("Har ikke tilgang til avtale {}", avtale.getAvtaleNr()); + } + return harTilgang; + } + + @Override + Page hentAlleAvtalerMedMuligTilgang(AvtaleRepository avtaleRepository, AvtalePredicate queryParametre, Pageable pageable) { + NavIdent veilederNavIdent = queryParametre.getVeilederNavIdent() != null ? queryParametre.getVeilederNavIdent() : getIdentifikator(); + + if(queryParametre.getStatus() != null) { + // Har filtrert på status. Da går siste filtrering på status i java-kode + Pageable allPages = PageRequest.of(0, Integer.MAX_VALUE, pageable.getSort()); + Page avtalerUtenStatusFiltrering = null; + if(queryParametre.getTiltakstype() != null) { + // Filtrer på tiltakstype + if (queryParametre.getVeilederNavIdent() != null) { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByVeilederNavIdentAndTiltakstype(queryParametre.getVeilederNavIdent(), queryParametre.getTiltakstype(), allPages); + } else if (queryParametre.getDeltakerFnr() != null) { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByDeltakerFnrAndTiltakstype(queryParametre.getDeltakerFnr(), queryParametre.getTiltakstype(), allPages); + } else if (queryParametre.getBedriftNr() != null) { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByBedriftNrAndTiltakstype(queryParametre.getBedriftNr(), queryParametre.getTiltakstype(), allPages); + } else if (queryParametre.getNavEnhet() != null && queryParametre.getErUfordelt() != null && queryParametre.getErUfordelt()) { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByVeilederNavIdentIsNullAndEnhetGeografiskAndTiltakstypeOrVeilederNavIdentIsNullAndEnhetOppfolgingAndTiltakstype(queryParametre.getNavEnhet(), queryParametre.getTiltakstype(), queryParametre.getNavEnhet(), queryParametre.getTiltakstype(), allPages); + } else if (queryParametre.getNavEnhet() != null) { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByEnhetGeografiskAndTiltakstypeOrEnhetOppfolgingAndTiltakstype(queryParametre.getNavEnhet(), queryParametre.getTiltakstype(), queryParametre.getNavEnhet(), queryParametre.getTiltakstype(), allPages); + } else if (queryParametre.getAvtaleNr() != null) { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByAvtaleNrAndTiltakstype(queryParametre.getAvtaleNr(), queryParametre.getTiltakstype(), allPages); + } else { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByVeilederNavIdentAndTiltakstype(veilederNavIdent, queryParametre.getTiltakstype(), allPages); + } + } else { + if (queryParametre.getVeilederNavIdent() != null) { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByVeilederNavIdent(queryParametre.getVeilederNavIdent(), allPages); + } else if (queryParametre.getDeltakerFnr() != null) { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByDeltakerFnr(queryParametre.getDeltakerFnr(), allPages); + } else if (queryParametre.getBedriftNr() != null) { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByBedriftNr(queryParametre.getBedriftNr(), allPages); + } else if (queryParametre.getNavEnhet() != null && queryParametre.getErUfordelt() != null && queryParametre.getErUfordelt()) { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByVeilederNavIdentIsNullAndEnhetGeografiskOrVeilederNavIdentIsNullAndEnhetOppfolging(queryParametre.getNavEnhet(), queryParametre.getNavEnhet(), allPages); + } else if (queryParametre.getNavEnhet() != null) { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByEnhetGeografiskOrEnhetOppfolging(queryParametre.getNavEnhet(), queryParametre.getNavEnhet(), allPages); + } else if (queryParametre.getAvtaleNr() != null) { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByAvtaleNr(queryParametre.getAvtaleNr(), allPages); + } else { + avtalerUtenStatusFiltrering = avtaleRepository.findAllByVeilederNavIdent(veilederNavIdent, allPages); + } + } + int skip = pageable.getPageNumber() > 0 ? (pageable.getPageNumber())*pageable.getPageSize() : 0; + List totaltFørPaging = avtalerUtenStatusFiltrering.getContent().stream() + .filter(avtale -> avtale.statusSomEnum() == queryParametre.getStatus()).toList(); + List avtaler = avtalerUtenStatusFiltrering.getContent().stream() + .filter(avtale -> avtale.statusSomEnum() == queryParametre.getStatus()) + .skip(skip) + .limit(pageable.getPageSize()).toList(); + return new PageImpl<>(avtaler, pageable, totaltFørPaging.size()); + } + else { + // Har ikke filtrert på status. Da går all filtrering i JPA/database + // Om det er filter med tiltaksType + if(queryParametre.getTiltakstype() != null) { + if (queryParametre.getVeilederNavIdent() != null) { + return avtaleRepository.findAllByVeilederNavIdentAndTiltakstype(queryParametre.getVeilederNavIdent(), queryParametre.getTiltakstype(), pageable); + } else if (queryParametre.getDeltakerFnr() != null) { + return avtaleRepository.findAllByDeltakerFnrAndTiltakstype(queryParametre.getDeltakerFnr(), queryParametre.getTiltakstype(), pageable); + } else if (queryParametre.getBedriftNr() != null) { + return avtaleRepository.findAllByBedriftNrAndTiltakstype(queryParametre.getBedriftNr(), queryParametre.getTiltakstype(), pageable); + } else if (queryParametre.getNavEnhet() != null && queryParametre.getErUfordelt() != null && queryParametre.getErUfordelt()) { + return avtaleRepository.findAllByVeilederNavIdentIsNullAndEnhetGeografiskAndTiltakstypeOrVeilederNavIdentIsNullAndEnhetOppfolgingAndTiltakstype(queryParametre.getNavEnhet(), queryParametre.getTiltakstype(), queryParametre.getNavEnhet(), queryParametre.getTiltakstype(), pageable); + } else if (queryParametre.getNavEnhet() != null) { + return avtaleRepository.findAllByEnhetGeografiskAndTiltakstypeOrEnhetOppfolgingAndTiltakstype(queryParametre.getNavEnhet(), queryParametre.getTiltakstype(), queryParametre.getNavEnhet(), queryParametre.getTiltakstype(), pageable); + } else if (queryParametre.getAvtaleNr() != null) { + return avtaleRepository.findAllByAvtaleNrAndTiltakstype(queryParametre.getAvtaleNr(), queryParametre.getTiltakstype(), pageable); + } else { + return avtaleRepository.findAllByVeilederNavIdentAndTiltakstype(veilederNavIdent, queryParametre.getTiltakstype(), pageable); + } + } else { + // Ingen tiltakstyper i query + if (queryParametre.getVeilederNavIdent() != null) { + return avtaleRepository.findAllByVeilederNavIdent(queryParametre.getVeilederNavIdent(), pageable); + } else if (queryParametre.getDeltakerFnr() != null) { + return avtaleRepository.findAllByDeltakerFnr(queryParametre.getDeltakerFnr(), pageable); + } else if (queryParametre.getBedriftNr() != null) { + return avtaleRepository.findAllByBedriftNr(queryParametre.getBedriftNr(), pageable); + } else if (queryParametre.getNavEnhet() != null && queryParametre.getErUfordelt() != null && queryParametre.getErUfordelt()) { + return avtaleRepository.findAllByVeilederNavIdentIsNullAndEnhetGeografiskOrVeilederNavIdentIsNullAndEnhetOppfolging(queryParametre.getNavEnhet(), queryParametre.getNavEnhet(), pageable); + } else if (queryParametre.getNavEnhet() != null) { + return avtaleRepository.findAllByEnhetGeografiskOrEnhetOppfolging(queryParametre.getNavEnhet(), queryParametre.getNavEnhet(), pageable); + } else if (queryParametre.getAvtaleNr() != null) { + return avtaleRepository.findAllByAvtaleNr(queryParametre.getAvtaleNr(), pageable); + } else { + return avtaleRepository.findAllByVeilederNavIdent(veilederNavIdent, pageable); + } + } + } + } + + public void annullerAvtale(Instant sistEndret, String annullerGrunn, Avtale avtale) { + avtale.sjekkSistEndret(sistEndret); + avtale.annuller(this, annullerGrunn); + } + + @Override + public boolean kanEndreAvtale() { + return true; + } + + @Override + void godkjennForAvtalepart(Avtale avtale) { + if (persondataService.erKode6(avtale.getDeltakerFnr())) { + throw new KanIkkeGodkjenneAvtalePåKode6Exception(); + } + if (avtale.getTiltakstype() != Tiltakstype.SOMMERJOBB) { + veilarbArenaClient.sjekkOppfølingStatus(avtale); + } + avtale.godkjennForVeileder(getIdentifikator()); + } + + @Override + public boolean erGodkjentAvInnloggetBruker(Avtale avtale) { + return avtale.erGodkjentAvVeileder(); + } + + @Override + boolean kanOppheveGodkjenninger(Avtale avtale) { + return true; + } + + public void godkjennForVeilederOgDeltaker(GodkjentPaVegneGrunn paVegneAvGrunn, Avtale avtale) { + sjekkTilgang(avtale); + if (persondataService.erKode6(avtale.getDeltakerFnr())) { + throw new KanIkkeGodkjenneAvtalePåKode6Exception(); + } + if (avtale.getTiltakstype() != Tiltakstype.SOMMERJOBB) { + veilarbArenaClient.sjekkOppfølingStatus(avtale); + } + avtale.godkjennForVeilederOgDeltaker(getIdentifikator(), paVegneAvGrunn); + } + + private void blokkereKode6Prosessering(Fnr deltakerFnr) { + if (persondataService.erKode6(deltakerFnr)) { + throw new KanIkkeGodkjenneAvtalePåKode6Exception(); + } + } + + private void sjekkOppfølgingStatusForTiltak(Avtale avtale) { + if (avtale.getTiltakstype() != Tiltakstype.SOMMERJOBB) { + veilarbArenaClient.sjekkOppfølingStatus(avtale); + } + } + + public void godkjennForVeilederOgArbeidsgiver( + GodkjentPaVegneAvArbeidsgiverGrunn paVegneAvArbeidsgiverGrunn, + Avtale avtale + ) { + super.sjekkTilgang(avtale); + this.blokkereKode6Prosessering(avtale.getDeltakerFnr()); + this.sjekkOppfølgingStatusForTiltak(avtale); + avtale.godkjennForVeilederOgArbeidsgiver(getIdentifikator(), paVegneAvArbeidsgiverGrunn); + } + + public void godkjennForVeilederOgDeltakerOgArbeidsgiver( + GodkjentPaVegneAvDeltakerOgArbeidsgiverGrunn paVegneAvDeltakerOgArbeidsgiverGrunn, + Avtale avtale + ) { + super.sjekkTilgang(avtale); + this.blokkereKode6Prosessering(avtale.getDeltakerFnr()); + this.sjekkOppfølgingStatusForTiltak(avtale); + avtale.godkjennForVeilederOgDeltakerOgArbeidsgiver(getIdentifikator(), paVegneAvDeltakerOgArbeidsgiverGrunn); + } + + @Override + void opphevGodkjenningerSomAvtalepart(Avtale avtale) { + avtale.opphevGodkjenningerSomVeileder(); + } + + @Override + protected Avtalerolle rolle() { + return Avtalerolle.VEILEDER; + } + + @Override + public InnloggetBruker innloggetBruker() { + return new InnloggetVeileder(getIdentifikator(), navEnheter, harAdGruppeForBeslutter); + } + + public void delAvtaleMedAvtalepart(Avtalerolle avtalerolle, Avtale avtale) { + avtale.delMedAvtalepart(avtalerolle); + } + + public void overtaAvtale(Avtale avtale) { + super.sjekkTilgang(avtale); + if (this.getIdentifikator().equals(avtale.getVeilederNavIdent())) { + throw new ErAlleredeVeilederException(); + } + avtale.overtaAvtale(this.getIdentifikator()); + } + + @Override + public void endreAvtale( + Instant sistEndret, + EndreAvtale endreAvtale, + Avtale avtale, + EnumSet tiltakstyperMedTilskuddsperioder + ) { + super.sjekkTilgangOgEndreAvtale( + sistEndret, + endreAvtale, + avtale, + tiltakstyperMedTilskuddsperioder + ); + this.oppdatereEnheterVedEndreAvtale(avtale); + } + + protected void oppdatereEnheterVedEndreAvtale(Avtale avtale) { + PdlRespons pdlRespons = this.oppdaterePersondataFraPdlVedEndreAvtale(avtale.getDeltakerFnr()); + this.oppdatereOppfølgingStatusVedEndreAvtale(avtale); + this.oppdatereGeoEnhetVedEndreAvtale(avtale, pdlRespons); + this.oppdatereOppfølgingEnhetsnavnVedEndreAvtale(avtale); + + } + + public PdlRespons oppdaterePersondataFraPdlVedEndreAvtale(Fnr deltakerFnr) { + final PdlRespons persondata = persondataService.hentPersondataFraPdl(deltakerFnr); + this.sjekkKode6(persondata); + return persondata; + } + + private void oppdatereGeoEnhetVedEndreAvtale(Avtale avtale, PdlRespons pdlRespons) { + Norg2GeoResponse norg2GeoResponse = PersondataService.hentGeoLokasjonFraPdlRespons(pdlRespons) + .map(norg2Client::hentGeoEnhetFraCacheEllerNorg2) + .orElse(null); + if (norg2GeoResponse == null) return; + avtale.setEnhetGeografisk(norg2GeoResponse.getEnhetNr()); + avtale.setEnhetsnavnGeografisk(norg2GeoResponse.getNavn()); + } + + private void oppdatereOppfølgingEnhetsnavnVedEndreAvtale(Avtale avtale) { + final Norg2OppfølgingResponse response = norg2Client.hentOppfølgingsEnhetsnavnFraCacheNorg2( + avtale.getEnhetOppfolging() + ); + if (response == null) return; + avtale.setEnhetsnavnOppfolging(response.getNavn()); + } + + public void oppdatereOppfølgingStatusVedEndreAvtale(Avtale avtale) { + Oppfølgingsstatus oppfølgingsstatus = veilarbArenaClient.HentOppfølgingsenhetFraCacheEllerArena( + avtale.getDeltakerFnr().asString() + ); + if (oppfølgingsstatus == null) return; + this.settOppfølgingsStatus(avtale, oppfølgingsstatus); + } + + private void sjekkKode6(PdlRespons persondata) { + if (persondataService.erKode6(persondata)) { + throw new KanIkkeOppretteAvtalePåKode6Exception(); + } + } + + public Avtale opprettAvtale(OpprettAvtale opprettAvtale) { + this.sjekkTilgangskontroll(opprettAvtale.getDeltakerFnr()); + Avtale avtale = Avtale.veilederOppretterAvtale(opprettAvtale, getIdentifikator()); + leggTilEnheter(avtale); + return avtale; + } + + public Avtale opprettMentorAvtale(OpprettMentorAvtale opprettMentorAvtale) { + this.sjekkTilgangskontroll(opprettMentorAvtale.getDeltakerFnr()); + Avtale avtale = Avtale.veilederOppretterAvtale(opprettMentorAvtale, getIdentifikator()); + leggTilEnheter(avtale); + return avtale; + } + + private void sjekkTilgangskontroll(Fnr deltakerFnr) { + if(!tilgangskontrollService.harSkrivetilgangTilKandidat(this, deltakerFnr)) { + throw new IkkeTilgangTilDeltakerException(); + } + } + + protected void leggTilEnheter(Avtale avtale){ + final PdlRespons persondata = this.hentPersonDataForOpprettelseAvAvtale(avtale); + this.hentOppfølgingFraArena(avtale, veilarbArenaClient); + super.hentGeoEnhetFraNorg2(avtale, persondata, norg2Client); + this.hentOppfolgingEnhetsnavnFraNorg2(avtale, norg2Client); + } + + private PdlRespons hentPersonDataForOpprettelseAvAvtale(Avtale avtale) { + final PdlRespons persondata = hentPersondata(avtale.getDeltakerFnr()); + avtale.leggTilDeltakerNavn(hentNavnFraPdlRespons(persondata)); + return persondata; + } + + public void hentOppfolgingEnhetsnavnFraNorg2(Avtale avtale, Norg2Client norg2Client) { + final Norg2OppfølgingResponse response = norg2Client.hentOppfølgingsEnhetsnavn(avtale.getEnhetOppfolging()); + if (response == null) return; + avtale.setEnhetsnavnOppfolging(response.getNavn()); + } + + public void hentOppfølgingFraArena( + Avtale avtale, + VeilarbArenaClient veilarbArenaClient + ) { + if(avtale.harOppfølgingsStatus()) return; + Oppfølgingsstatus oppfølgingsstatus = veilarbArenaClient.sjekkOgHentOppfølgingStatus(avtale); + if (oppfølgingsstatus == null) return; + this.settOppfølgingsStatus(avtale, oppfølgingsstatus); + this.settLonntilskuddProsentsats(avtale); + } + + private PdlRespons hentPersondata(Fnr deltakerFnr) { + final PdlRespons persondata = persondataService.hentPersondata(deltakerFnr); + this.sjekkKode6(persondata); + return persondata; + } + + public void sjekkOgHentOppfølgingStatus(Avtale avtale, VeilarbArenaClient veilarbArenaClient) { + Oppfølgingsstatus oppfølgingsstatus = veilarbArenaClient.sjekkOgHentOppfølgingStatus(avtale); + this.settOppfølgingsStatus(avtale, oppfølgingsstatus); + } + + protected void settOppfølgingsStatus(Avtale avtale, Oppfølgingsstatus oppfølgingsstatus) { + avtale.setEnhetOppfolging(oppfølgingsstatus.getOppfolgingsenhet()); + avtale.setKvalifiseringsgruppe(oppfølgingsstatus.getKvalifiseringsgruppe()); + avtale.setFormidlingsgruppe(oppfølgingsstatus.getFormidlingsgruppe()); + } + + public void slettemerk(Avtale avtale) { + super.sjekkTilgang(avtale); + List identer = slettemerkeProperties.getIdent(); + if (!identer.contains(this.getIdentifikator())) { + throw new IkkeAdminTilgangException(); + } + avtale.slettemerk(this.getIdentifikator()); + } + + public void endreMål(EndreMål endreMål, Avtale avtale) { + super.sjekkTilgang(avtale); + avtale.endreMål(endreMål, getIdentifikator()); + } + + public void endreInkluderingstilskudd(EndreInkluderingstilskudd endreInkluderingstilskudd, Avtale avtale) { + super.sjekkTilgang(avtale); + avtale.endreInkluderingstilskudd(endreInkluderingstilskudd, getIdentifikator()); + } + + public void forkortAvtale(Avtale avtale, LocalDate sluttDato, String grunn, String annetGrunn) { + super.sjekkTilgang(avtale); + avtale.forkortAvtale(sluttDato, grunn, annetGrunn, getIdentifikator()); + } + + public void forlengAvtale(LocalDate sluttDato, Avtale avtale) { + super.sjekkTilgang(avtale); + sjekkOgHentOppfølgingStatus(avtale, veilarbArenaClient); + avtale.forlengAvtale(sluttDato, getIdentifikator()); + } + + protected void oppdatereEnheterEtterForespørsel(Avtale avtale) { + final PdlRespons persondata = this.hentPersonDataForOpprettelseAvAvtale(avtale); + this.sjekkOgHentOppfølgingStatus(avtale, veilarbArenaClient); + super.hentGeoEnhetFraNorg2(avtale, persondata, norg2Client); + this.hentOppfolgingEnhetsnavnFraNorg2(avtale, norg2Client); + } + + public void endreStillingbeskrivelse(EndreStillingsbeskrivelse endreStillingsbeskrivelse, Avtale avtale) { + super.sjekkTilgang(avtale); + avtale.endreStillingsbeskrivelse(endreStillingsbeskrivelse, getIdentifikator()); + } + + public void endreOppfølgingOgTilrettelegging( + EndreOppfølgingOgTilrettelegging endreOppfølgingOgTilrettelegging, + Avtale avtale + ) { + super.sjekkTilgang(avtale); + avtale.endreOppfølgingOgTilrettelegging(endreOppfølgingOgTilrettelegging, getIdentifikator()); + } + + public void endreOmMentor(EndreOmMentor endreOmMentor, Avtale avtale) { + super.sjekkTilgang(avtale); + avtale.endreOmMentor(endreOmMentor, getIdentifikator()); + } + + public void endreKontaktinfo(EndreKontaktInformasjon endreKontaktInformasjon, Avtale avtale) { + super.sjekkTilgang(avtale); + avtale.endreKontaktInformasjon(endreKontaktInformasjon, getIdentifikator()); + } + + public void endreTilskuddsberegning(EndreTilskuddsberegning endreTilskuddsberegning, Avtale avtale) { + super.sjekkTilgang(avtale); + avtale.endreTilskuddsberegning(endreTilskuddsberegning, getIdentifikator()); + } + + public void sendTilbakeTilBeslutter(Avtale avtale) { + super.sjekkTilgang(avtale); + avtale.sendTilbakeTilBeslutter(); + } + + protected void oppdatereKostnadssted(Avtale avtale, Norg2Client norg2Client, String enhet) { + final Norg2OppfølgingResponse response = norg2Client.hentOppfølgingsEnhetsnavn(enhet); + + if (response == null) { + throw new FeilkodeException(Feilkode.ENHET_FINNES_IKKE); + } + NyttKostnadssted nyttKostnadssted = new NyttKostnadssted(enhet, response.getNavn()); + TreeSet tilskuddPerioder = avtale.finnTilskuddsperioderIkkeLukketForEndring(); + + if (tilskuddPerioder == null) { + throw new FeilkodeException(Feilkode.TILSKUDDSPERIODE_ER_IKKE_SATT); + } + avtale.oppdatereKostnadsstedForTilskuddsperioder(nyttKostnadssted); + } + + private LocalDate settStartDato(LocalDate startdato) { + return startdato != null ? startdato : LocalDate.now(); + } + + protected List hentAvtaleDeltakerAlleredeErRegistrertPaa( + Fnr deltakerFnr, + Tiltakstype tiltakstype, + UUID avtaleId, + LocalDate startDato, + LocalDate sluttDato, + AvtaleRepository avtaleRepository + ) { + if(avtaleId != null && startDato != null && sluttDato != null) { + return AlleredeRegistrertAvtale.filtrerAvtaleDeltakerAlleredeErRegistrertPaa( + avtaleRepository.finnAvtalerSomOverlapperForDeltakerVedGodkjenningAvAvtale( + deltakerFnr.asString(), + avtaleId.toString(), + Date.valueOf(settStartDato(startDato)), + Date.valueOf(sluttDato) + ), + tiltakstype + ); + } + return AlleredeRegistrertAvtale.filtrerAvtaleDeltakerAlleredeErRegistrertPaa( + avtaleRepository.finnAvtalerSomOverlapperForDeltakerVedOpprettelseAvAvtale( + deltakerFnr.asString(), + Date.valueOf(settStartDato(startDato)) + + ), + tiltakstype + ); + } + + @Override public UUID getAzureOid() { + return azureOid; + } + + @Override public NavIdent getNavIdent() { + return getIdentifikator(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AnnullertAvVeileder.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AnnullertAvVeileder.java new file mode 100644 index 000000000..726a69b3a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AnnullertAvVeileder.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +@Value +public class AnnullertAvVeileder { + Avtale avtale; + NavIdent utfortAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvbruttAvVeileder.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvbruttAvVeileder.java new file mode 100644 index 000000000..46e257565 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvbruttAvVeileder.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +@Value +public class AvbruttAvVeileder { + Avtale avtale; + NavIdent utfortAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleDeltMedAvtalepart.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleDeltMedAvtalepart.java new file mode 100644 index 000000000..2780e022a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleDeltMedAvtalepart.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; + +@Value +public class AvtaleDeltMedAvtalepart { + Avtale avtale; + Avtalerolle avtalepart; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleEndret.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleEndret.java new file mode 100644 index 000000000..b562c7b68 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleEndret.java @@ -0,0 +1,13 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; + +@Value +public class AvtaleEndret { + Avtale avtale; + Avtalerolle utfortAvRolle; + Identifikator utfortAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleForkortet.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleForkortet.java new file mode 100644 index 000000000..9f82fb86c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleForkortet.java @@ -0,0 +1,18 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleInnhold; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +import java.time.LocalDate; + +@Value +public class AvtaleForkortet { + Avtale avtale; + AvtaleInnhold avtaleInnhold; + LocalDate nySluttDato; + String grunn; + String annetGrunn; + NavIdent utførtAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleForlenget.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleForlenget.java new file mode 100644 index 000000000..87080880f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleForlenget.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +@Value +public class AvtaleForlenget { + Avtale avtale; + NavIdent utførtAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleGjenopprettet.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleGjenopprettet.java new file mode 100644 index 000000000..a022cbc45 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleGjenopprettet.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +@Value +public class AvtaleGjenopprettet { + Avtale avtale; + NavIdent utfortAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleGodkjent.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleGodkjent.java new file mode 100644 index 000000000..42906da43 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleGodkjent.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; + +public interface AvtaleGodkjent { + Avtale getAvtale(); +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleInng\303\245tt.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleInng\303\245tt.java" new file mode 100644 index 000000000..a9a6cdf71 --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleInng\303\245tt.java" @@ -0,0 +1,13 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +@Value +public class AvtaleInngått { + Avtale avtale; + Avtalerolle utførtAvRolle; + NavIdent utførtAv; +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleL\303\245stOpp.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleL\303\245stOpp.java" new file mode 100644 index 000000000..eaef725a0 --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleL\303\245stOpp.java" @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; + +@Value +public class AvtaleLåstOpp { + Avtale avtale; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleNyVeileder.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleNyVeileder.java new file mode 100644 index 000000000..1800be2fe --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleNyVeileder.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +@Value +public class AvtaleNyVeileder { + Avtale avtale; + NavIdent tidligereVeileder; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleOpprettetAvArbeidsgiver.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleOpprettetAvArbeidsgiver.java new file mode 100644 index 000000000..a8f524493 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleOpprettetAvArbeidsgiver.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; + +@Value +public class AvtaleOpprettetAvArbeidsgiver { + Avtale avtale; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleOpprettetAvArbeidsgiverErFordelt.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleOpprettetAvArbeidsgiverErFordelt.java new file mode 100644 index 000000000..effe64c14 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleOpprettetAvArbeidsgiverErFordelt.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; + +@Value +public class AvtaleOpprettetAvArbeidsgiverErFordelt { + Avtale avtale; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleOpprettetAvVeileder.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleOpprettetAvVeileder.java new file mode 100644 index 000000000..383321a4c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleOpprettetAvVeileder.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; + +@Value +public class AvtaleOpprettetAvVeileder { + Avtale avtale; + Identifikator utfortAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleSlettemerket.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleSlettemerket.java new file mode 100644 index 000000000..8a970c665 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/AvtaleSlettemerket.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; + +@Value +public class AvtaleSlettemerket { + Avtale avtale; + Identifikator utfortAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/FjernetEtterregistrering.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/FjernetEtterregistrering.java new file mode 100644 index 000000000..60297b3d3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/FjernetEtterregistrering.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; + +@Value +public class FjernetEtterregistrering { + Avtale avtale; + Identifikator utfortAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GamleVerdier.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GamleVerdier.java new file mode 100644 index 000000000..7b5a90364 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GamleVerdier.java @@ -0,0 +1,13 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class GamleVerdier { + private boolean godkjentAvDeltaker; + private boolean godkjentAvArbeidsgiver; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjenningerOpphevetAvArbeidsgiver.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjenningerOpphevetAvArbeidsgiver.java new file mode 100644 index 000000000..4555d8a61 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjenningerOpphevetAvArbeidsgiver.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; + +@Value +public class GodkjenningerOpphevetAvArbeidsgiver { + Avtale avtale; + GamleVerdier gamleVerdier; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjenningerOpphevetAvVeileder.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjenningerOpphevetAvVeileder.java new file mode 100644 index 000000000..b031ad90a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjenningerOpphevetAvVeileder.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; + +@Value +public class GodkjenningerOpphevetAvVeileder { + Avtale avtale; + GamleVerdier gamleVerdier; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentAvArbeidsgiver.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentAvArbeidsgiver.java new file mode 100644 index 000000000..9956a47c2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentAvArbeidsgiver.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; + +@Value +public class GodkjentAvArbeidsgiver implements AvtaleGodkjent { + Avtale avtale; + Identifikator utfortAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentAvDeltaker.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentAvDeltaker.java new file mode 100644 index 000000000..27f366484 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentAvDeltaker.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; + +@Value +public class GodkjentAvDeltaker implements AvtaleGodkjent { + Avtale avtale; + Identifikator utfortAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentAvVeileder.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentAvVeileder.java new file mode 100644 index 000000000..11637cba1 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentAvVeileder.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; + +@Value +public class GodkjentAvVeileder implements AvtaleGodkjent { + Avtale avtale; + Identifikator utfortAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentForEtterregistrering.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentForEtterregistrering.java new file mode 100644 index 000000000..5270e522e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentForEtterregistrering.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; + +@Value +public class GodkjentForEtterregistrering { + Avtale avtale; + Identifikator utfortAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentPaVegneAvArbeidsgiver.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentPaVegneAvArbeidsgiver.java new file mode 100644 index 000000000..4b5acda30 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentPaVegneAvArbeidsgiver.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; + +@Value +public class GodkjentPaVegneAvArbeidsgiver { + Avtale avtale; + Identifikator utfortAv; +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentPaVegneAvDeltaker.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentPaVegneAvDeltaker.java new file mode 100644 index 000000000..8f8c3bb09 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentPaVegneAvDeltaker.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; + +@Value +public class GodkjentPaVegneAvDeltaker { + Avtale avtale; + Identifikator utfortAv; +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentPaVegneAvDeltakerOgArbeidsgiver.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentPaVegneAvDeltakerOgArbeidsgiver.java new file mode 100644 index 000000000..cf534314e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/GodkjentPaVegneAvDeltakerOgArbeidsgiver.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; + +@Value +public class GodkjentPaVegneAvDeltakerOgArbeidsgiver { + Avtale avtale; + Identifikator utfortAv; +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/InkluderingstilskuddEndret.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/InkluderingstilskuddEndret.java new file mode 100644 index 000000000..c1fac4960 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/InkluderingstilskuddEndret.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +@Value +public class InkluderingstilskuddEndret { + Avtale avtale; + NavIdent utførtAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/KontaktinformasjonEndret.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/KontaktinformasjonEndret.java new file mode 100644 index 000000000..675bd96ac --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/KontaktinformasjonEndret.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +@Value +public class KontaktinformasjonEndret { + Avtale avtale; + NavIdent utførtAv; +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/M\303\245lEndret.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/M\303\245lEndret.java" new file mode 100644 index 000000000..472020e54 --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/M\303\245lEndret.java" @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +@Value +public class MålEndret { + Avtale avtale; + NavIdent utførtAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/OmMentorEndret.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/OmMentorEndret.java new file mode 100644 index 000000000..1aa86dff0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/OmMentorEndret.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +@Value +public class OmMentorEndret { + Avtale avtale; + NavIdent utførtAv; +} \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/Oppf\303\270lgingOgTilretteleggingEndret.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/Oppf\303\270lgingOgTilretteleggingEndret.java" new file mode 100644 index 000000000..347549498 --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/Oppf\303\270lgingOgTilretteleggingEndret.java" @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +@Value +public class OppfølgingOgTilretteleggingEndret { + Avtale avtale; + NavIdent utførtAv; +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/RefusjonFristForlenget.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/RefusjonFristForlenget.java new file mode 100644 index 000000000..9139ad2a6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/RefusjonFristForlenget.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; + +@Value +public class RefusjonFristForlenget { + Avtale avtale; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/RefusjonKlar.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/RefusjonKlar.java new file mode 100644 index 000000000..9ec3c955a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/RefusjonKlar.java @@ -0,0 +1,12 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; + +import java.time.LocalDate; + +@Value +public class RefusjonKlar { + Avtale avtale; + LocalDate fristForGodkjenning; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/RefusjonKlarRevarsel.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/RefusjonKlarRevarsel.java new file mode 100644 index 000000000..ac50d2b8a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/RefusjonKlarRevarsel.java @@ -0,0 +1,12 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; + +import java.time.LocalDate; + +@Value +public class RefusjonKlarRevarsel { + Avtale avtale; + LocalDate fristForGodkjenning; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/RefusjonKorrigert.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/RefusjonKorrigert.java new file mode 100644 index 000000000..9363b1359 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/RefusjonKorrigert.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; + +@Value +public class RefusjonKorrigert { + Avtale avtale; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/SignertAvMentor.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/SignertAvMentor.java new file mode 100644 index 000000000..98c7aa70e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/SignertAvMentor.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; + +@Value +public class SignertAvMentor implements AvtaleGodkjent { + Avtale avtale; + Identifikator utfortAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/StillingsbeskrivelseEndret.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/StillingsbeskrivelseEndret.java new file mode 100644 index 000000000..6763ac0b2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/StillingsbeskrivelseEndret.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +@Value +public class StillingsbeskrivelseEndret { + Avtale avtale; + NavIdent utførtAv; +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsberegningEndret.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsberegningEndret.java new file mode 100644 index 000000000..0d677eee2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsberegningEndret.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; + +@Value +public class TilskuddsberegningEndret { + Avtale avtale; + NavIdent utførtAv; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsperiodeAnnullert.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsperiodeAnnullert.java new file mode 100644 index 000000000..4acd24212 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsperiodeAnnullert.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.TilskuddPeriode; + +@Value +public class TilskuddsperiodeAnnullert { + Avtale avtale; + TilskuddPeriode tilskuddsperiode; +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsperiodeAvsl\303\245tt.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsperiodeAvsl\303\245tt.java" new file mode 100644 index 000000000..9b74ebfc5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsperiodeAvsl\303\245tt.java" @@ -0,0 +1,13 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; +import no.nav.tag.tiltaksgjennomforing.avtale.TilskuddPeriode; + +@Value +public class TilskuddsperiodeAvslått { + Avtale avtale; + Identifikator utfortAv; + TilskuddPeriode tilskuddsperiode; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsperiodeForkortet.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsperiodeForkortet.java new file mode 100644 index 000000000..084d16958 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsperiodeForkortet.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.TilskuddPeriode; + +@Value +public class TilskuddsperiodeForkortet { + Avtale avtale; + TilskuddPeriode tilskuddsperiode; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsperiodeGodkjent.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsperiodeGodkjent.java new file mode 100644 index 000000000..169e2bddc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/events/TilskuddsperiodeGodkjent.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; +import no.nav.tag.tiltaksgjennomforing.avtale.TilskuddPeriode; + +@Value +public class TilskuddsperiodeGodkjent { + Avtale avtale; + TilskuddPeriode tilskuddsperiode; + Identifikator utfortAv; + Integer resendingsnummer; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/ArbeidstreningStartOgSluttDatoStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/ArbeidstreningStartOgSluttDatoStrategy.java new file mode 100644 index 000000000..045d3200d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/ArbeidstreningStartOgSluttDatoStrategy.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.startOgSluttDatoStrategy; + +import no.nav.tag.tiltaksgjennomforing.exceptions.VarighetForLangArbeidstreningException; + +import java.time.LocalDate; + +public class ArbeidstreningStartOgSluttDatoStrategy implements StartOgSluttDatoStrategy { + @Override + public void sjekkStartOgSluttDato(LocalDate startDato, LocalDate sluttDato, boolean erGodkjentForEtterregistrering, boolean erAvtaleInngått) { + StartOgSluttDatoStrategy.super.sjekkStartOgSluttDato(startDato, sluttDato, erGodkjentForEtterregistrering, erAvtaleInngått); + if (startDato != null && sluttDato != null && startDato.plusMonths(18).minusDays(1).isBefore(sluttDato)) { + throw new VarighetForLangArbeidstreningException(); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/InkluderingstilskuddStartOgSluttDatoStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/InkluderingstilskuddStartOgSluttDatoStrategy.java new file mode 100644 index 000000000..9b4771102 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/InkluderingstilskuddStartOgSluttDatoStrategy.java @@ -0,0 +1,16 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.startOgSluttDatoStrategy; + +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; + +import java.time.LocalDate; + +public class InkluderingstilskuddStartOgSluttDatoStrategy implements StartOgSluttDatoStrategy { + @Override + public void sjekkStartOgSluttDato(LocalDate startDato, LocalDate sluttDato, boolean erGodkjentForEtterregistrering, boolean erAvtaleInngått) { + StartOgSluttDatoStrategy.super.sjekkStartOgSluttDato(startDato, sluttDato, erGodkjentForEtterregistrering, erAvtaleInngått); + if (startDato != null && sluttDato != null && startDato.plusMonths(12).isBefore(sluttDato)) { + throw new FeilkodeException(Feilkode.VARIGHET_FOR_LANG_INKLUDERINGSTILSKUDD); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/MentorStartOgSluttDatoStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/MentorStartOgSluttDatoStrategy.java new file mode 100644 index 000000000..fc8ce41cd --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/MentorStartOgSluttDatoStrategy.java @@ -0,0 +1,32 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.startOgSluttDatoStrategy; + + +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; + +import java.time.LocalDate; + +public class MentorStartOgSluttDatoStrategy implements StartOgSluttDatoStrategy { + private final Kvalifiseringsgruppe kvalifiseringsgruppe; + + public MentorStartOgSluttDatoStrategy(Kvalifiseringsgruppe kvalifiseringsgruppe) { + this.kvalifiseringsgruppe = kvalifiseringsgruppe; + } + + @Override + public void sjekkStartOgSluttDato(LocalDate startDato, LocalDate sluttDato, boolean erGodkjentForEtterregistrering, boolean erAvtaleInngått) { + StartOgSluttDatoStrategy.super.sjekkStartOgSluttDato(startDato, sluttDato, erGodkjentForEtterregistrering, erAvtaleInngått); + + if (startDato != null && sluttDato != null) { + if ((kvalifiseringsgruppe == Kvalifiseringsgruppe.SPESIELT_TILPASSET_INNSATS || kvalifiseringsgruppe == Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS) + && startDato.plusMonths(36).minusDays(1).isBefore(sluttDato)) { + throw new FeilkodeException(Feilkode.VARIGHET_FOR_LANG_MENTOR_36_MND); + } + } + + if (kvalifiseringsgruppe != Kvalifiseringsgruppe.SPESIELT_TILPASSET_INNSATS && kvalifiseringsgruppe != Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS && startDato != null && sluttDato != null && startDato.plusMonths(6).minusDays(1).isBefore(sluttDato)) { + throw new FeilkodeException(Feilkode.VARIGHET_FOR_LANG_MENTOR_6_MND); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/MidlertidigLonnstilskuddStartOgSluttDatoStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/MidlertidigLonnstilskuddStartOgSluttDatoStrategy.java new file mode 100644 index 000000000..4198a3de3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/MidlertidigLonnstilskuddStartOgSluttDatoStrategy.java @@ -0,0 +1,38 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.startOgSluttDatoStrategy; + +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; + +import java.time.LocalDate; + +public class MidlertidigLonnstilskuddStartOgSluttDatoStrategy implements StartOgSluttDatoStrategy { + private static final int TJUEFIRE_MND_MAKS_LENGDE = 24; + private static final int TOLV_MND_MAKS_LENGDE = 12; + private final Kvalifiseringsgruppe kvalifiseringsgruppe; + + MidlertidigLonnstilskuddStartOgSluttDatoStrategy(Kvalifiseringsgruppe kvalifiseringsgruppe) { + this.kvalifiseringsgruppe = kvalifiseringsgruppe; + } + + @Override + public void sjekkStartOgSluttDato(LocalDate startDato, LocalDate sluttDato, boolean erGodkjentForEtterregistrering, boolean erAvtaleInngått) { + StartOgSluttDatoStrategy.super.sjekkStartOgSluttDato(startDato, sluttDato, erGodkjentForEtterregistrering, erAvtaleInngått); + + if (startDato != null && sluttDato != null) { + if ((kvalifiseringsgruppe == Kvalifiseringsgruppe.SPESIELT_TILPASSET_INNSATS || kvalifiseringsgruppe == Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS) + && startDato.plusMonths(TJUEFIRE_MND_MAKS_LENGDE).minusDays(1).isBefore(sluttDato)) { + throw new FeilkodeException(Feilkode.VARIGHET_FOR_LANG_MIDLERTIDIG_LONNSTILSKUDD_24_MND); + } + + if (kvalifiseringsgruppe == Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS && startDato.plusMonths(TOLV_MND_MAKS_LENGDE).minusDays(1).isBefore(sluttDato)) { + throw new FeilkodeException(Feilkode.VARIGHET_FOR_LANG_MIDLERTIDIG_LONNSTILSKUDD_12_MND); + } + + // Ikke funnet kvalifiseringsgruppe, default 12 mnd + if (kvalifiseringsgruppe == null && startDato.plusMonths(TOLV_MND_MAKS_LENGDE).minusDays(1).isBefore(sluttDato)) { + throw new FeilkodeException(Feilkode.VARIGHET_FOR_LANG_MIDLERTIDIG_LONNSTILSKUDD_12_MND); + } + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/SommerjobbStartOgSluttDatoStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/SommerjobbStartOgSluttDatoStrategy.java new file mode 100644 index 000000000..28cb17a81 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/SommerjobbStartOgSluttDatoStrategy.java @@ -0,0 +1,32 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.startOgSluttDatoStrategy; + +import java.time.LocalDate; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; + +public class SommerjobbStartOgSluttDatoStrategy implements StartOgSluttDatoStrategy { + @Override + public void sjekkStartOgSluttDato(LocalDate startDato, LocalDate sluttDato, boolean erGodkjentForEtterregistrering, boolean erAvtaleInngått) { + StartOgSluttDatoStrategy.super.sjekkStartOgSluttDato(startDato, sluttDato, erGodkjentForEtterregistrering, erAvtaleInngått); + if (startDato != null) { + if (startDato.isBefore(LocalDate.of(startDato.getYear(), 6, 1)) ) { + throw new FeilkodeException(Feilkode.SOMMERJOBB_FOR_TIDLIG); + } + if (startDato.isAfter(LocalDate.of(startDato.getYear(), 8, 31))) { + throw new FeilkodeException(Feilkode.SOMMERJOBB_FOR_SENT); + } + } + if (startDato != null && sluttDato != null) { + if (startDato.plusWeeks(4).minusDays(1).isBefore(sluttDato)) { + throw new FeilkodeException(Feilkode.SOMMERJOBB_FOR_LANG_VARIGHET); + }else{ + if (sluttDato.isBefore(LocalDate.of(sluttDato.getYear(), 6, 1)) ) { + throw new FeilkodeException(Feilkode.SOMMERJOBB_FOR_TIDLIG); + } + if (sluttDato.isAfter(LocalDate.of(sluttDato.getYear(), 9, 27))) { + throw new FeilkodeException(Feilkode.SOMMERJOBB_FOR_SENT); + } + } + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/StartOgSluttDatoStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/StartOgSluttDatoStrategy.java new file mode 100644 index 000000000..0351fcb19 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/StartOgSluttDatoStrategy.java @@ -0,0 +1,21 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.startOgSluttDatoStrategy; + +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; +import no.nav.tag.tiltaksgjennomforing.utils.Now; + +import java.time.LocalDate; + +public interface StartOgSluttDatoStrategy { + default void sjekkStartOgSluttDato(LocalDate startDato, LocalDate sluttDato, boolean erGodkjentForEtterregistrering, boolean erAvtaleInngått) { + if (startDato != null && sluttDato != null && startDato.isAfter(sluttDato)) { + throw new FeilkodeException(Feilkode.START_ETTER_SLUTT); + } + if (startDato != null && sluttDato != null && !erGodkjentForEtterregistrering && startDato.plusDays(7).isBefore(Now.localDate()) && !erAvtaleInngått){ + throw new FeilkodeException(Feilkode.FORTIDLIG_STARTDATO); + } + if (startDato != null && sluttDato != null && sluttDato.isAfter(LocalDate.of(2089, 12, 31))) { + throw new FeilkodeException(Feilkode.SLUTTDATO_GRENSE_NÅDD); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/StartOgSluttDatoStrategyFactory.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/StartOgSluttDatoStrategyFactory.java new file mode 100644 index 000000000..1ca54efae --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/StartOgSluttDatoStrategyFactory.java @@ -0,0 +1,27 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.startOgSluttDatoStrategy; + +import lombok.experimental.UtilityClass; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; + +@UtilityClass +public class StartOgSluttDatoStrategyFactory { + public static StartOgSluttDatoStrategy create(Tiltakstype tiltakstype, Kvalifiseringsgruppe kvalifiseringsgruppe) { + switch (tiltakstype) { + case ARBEIDSTRENING: + return new ArbeidstreningStartOgSluttDatoStrategy(); + case MIDLERTIDIG_LONNSTILSKUDD: + return new MidlertidigLonnstilskuddStartOgSluttDatoStrategy(kvalifiseringsgruppe); + case VARIG_LONNSTILSKUDD: + return new VarigLonnstilskuddStartOgSluttDatoStrategy(); + case MENTOR: + return new MentorStartOgSluttDatoStrategy(kvalifiseringsgruppe); + case INKLUDERINGSTILSKUDD: + return new InkluderingstilskuddStartOgSluttDatoStrategy(); + case SOMMERJOBB: + return new SommerjobbStartOgSluttDatoStrategy(); + } + return new StartOgSluttDatoStrategy() { + }; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/VarigLonnstilskuddStartOgSluttDatoStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/VarigLonnstilskuddStartOgSluttDatoStrategy.java new file mode 100644 index 000000000..0a6b5ac83 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/VarigLonnstilskuddStartOgSluttDatoStrategy.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.startOgSluttDatoStrategy; + +import java.time.LocalDate; + +public class VarigLonnstilskuddStartOgSluttDatoStrategy implements StartOgSluttDatoStrategy { + @Override + public void sjekkStartOgSluttDato(LocalDate startDato, LocalDate sluttDato,boolean erGodkjentForEtterregistrering, boolean erAvtaleInngått) { + StartOgSluttDatoStrategy.super.sjekkStartOgSluttDato(startDato, sluttDato, erGodkjentForEtterregistrering, erAvtaleInngått); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseController.java new file mode 100644 index 000000000..7c0af1893 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseController.java @@ -0,0 +1,60 @@ +package no.nav.tag.tiltaksgjennomforing.datadeling; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.security.token.support.core.api.ProtectedWithClaims; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.UtviklerTilgangProperties; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.exceptions.RessursFinnesIkkeException; +import org.springframework.http.HttpStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpClientErrorException; + +import java.util.UUID; + +@RestController +@RequestMapping("/utvikler-admin/avtale-hendelse") +@RequiredArgsConstructor +@ProtectedWithClaims(issuer = "aad") +@Slf4j +public class AvtaleHendelseController { + + private final UtviklerTilgangProperties utviklerTilgangProperties; + private final TokenUtils tokenUtils; + private final AvtaleHendelseService avtaleHendelseService; + private final AvtaleRepository avtaleRepository; + private void sjekkTilgang() { + if (!tokenUtils.harAdGruppe(utviklerTilgangProperties.getGruppeTilgang())) { + throw new HttpClientErrorException(HttpStatus.FORBIDDEN); + } + } + + @PostMapping("send-melding-en-avtale/{avtaleId}") + public void sendMeldingForEnAvtale(@PathVariable("avtaleId") UUID avtaleId) { + log.info("Sender hendelsemelding for en avtale {}", avtaleId); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(RessursFinnesIkkeException::new); + avtaleHendelseService.sendAvtaleHendelseMeldingPåEnAvtale(avtale); + } + + @PostMapping("dry-send-melding-alle-avtaler") + public void drySendMeldingAlleAvtaler() { + log.info("DRY - Sender alle avtaler som hendelsemeldinger på topic"); + sjekkTilgang(); + avtaleHendelseService.sendAvtaleHendelseMeldingPåAlleAvtalerDRYRun(); + } + + + @PostMapping("send-melding-alle-avtaler") + @Transactional + public void sendMeldingAlleAvtaler() { + log.info("Sender alle avtaler som hendelsemeldinger på topic"); + sjekkTilgang(); + avtaleHendelseService.sendAvtaleHendelseMeldingPåAlleAvtaler(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseLytter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseLytter.java new file mode 100644 index 000000000..e901c65b8 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseLytter.java @@ -0,0 +1,151 @@ +package no.nav.tag.tiltaksgjennomforing.datadeling; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.*; +import no.nav.tag.tiltaksgjennomforing.avtale.events.*; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +@Slf4j +public class AvtaleHendelseLytter { + + private final AvtaleMeldingEntitetRepository avtaleMeldingEntitetRepository; + private final ObjectMapper objectMapper; + + @EventListener + public void avtaleOpprettetAvArbeidsgiver(AvtaleOpprettetAvArbeidsgiver event) { + Avtale avtale = event.getAvtale(); + lagHendelse(avtale, HendelseType.OPPRETTET_AV_ARBEIDSGIVER, avtale.getBedriftNr(), Avtalerolle.ARBEIDSGIVER); + } + + @EventListener + public void avtaleOpprettetAvVeileder(AvtaleOpprettetAvVeileder event) { + Avtale avtale = event.getAvtale(); + lagHendelse(avtale, HendelseType.OPPRETTET, event.getUtfortAv(), Avtalerolle.VEILEDER); + } + + @EventListener + public void avtaleEndret(AvtaleEndret event) { + Avtale avtale = event.getAvtale(); + lagHendelse(avtale, HendelseType.ENDRET, event.getUtfortAv(), event.getUtfortAvRolle()); + } + + @EventListener + public void avtaleInngått(AvtaleInngått event) { + Avtale avtale = event.getAvtale(); + lagHendelse(avtale, HendelseType.AVTALE_INNGÅTT, event.getUtførtAv(), event.getUtførtAvRolle()); + } + + @EventListener + public void avtaleForlenget(AvtaleForlenget event) { + lagHendelse(event.getAvtale(), HendelseType.AVTALE_FORLENGET, event.getUtførtAv(), Avtalerolle.VEILEDER); + } + + @EventListener + public void avtaleForkortet(AvtaleForkortet event) { + lagHendelse(event.getAvtale(), HendelseType.AVTALE_FORKORTET, event.getUtførtAv(), Avtalerolle.VEILEDER); + } + + @EventListener + public void avtaleAnnullert(AnnullertAvVeileder event) { + lagHendelse(event.getAvtale(), HendelseType.ANNULLERT, event.getUtfortAv(), Avtalerolle.VEILEDER); + } + + @EventListener + public void tilskuddsberegningEndret(TilskuddsberegningEndret event) { + lagHendelse(event.getAvtale(), HendelseType.TILSKUDDSBEREGNING_ENDRET, event.getUtførtAv(), Avtalerolle.VEILEDER); + } + + @EventListener + public void stillingsbeskrivelseEndret(StillingsbeskrivelseEndret event) { + lagHendelse(event.getAvtale(), HendelseType.STILLINGSBESKRIVELSE_ENDRET, event.getUtførtAv(), Avtalerolle.VEILEDER); + } + + @EventListener + public void kontaktinformasjonEndret(KontaktinformasjonEndret event) { + lagHendelse(event.getAvtale(), HendelseType.KONTAKTINFORMASJON_ENDRET, event.getUtførtAv(), Avtalerolle.VEILEDER); + } + + @EventListener + public void oppfølgingOgTilretteleggingEndret(OppfølgingOgTilretteleggingEndret event) { + lagHendelse(event.getAvtale(), HendelseType.OPPFØLGING_OG_TILRETTELEGGING_ENDRET, event.getUtførtAv(), Avtalerolle.VEILEDER); + } + + @EventListener + public void målEndret(MålEndret event) { + lagHendelse(event.getAvtale(), HendelseType.MÅL_ENDRET, event.getUtførtAv(), Avtalerolle.VEILEDER); + } + + @EventListener + public void inkluderingstilskuddEndret(InkluderingstilskuddEndret event) { + lagHendelse(event.getAvtale(), HendelseType.INKLUDERINGSTILSKUDD_ENDRET, event.getUtførtAv(), Avtalerolle.VEILEDER); + } + + @EventListener + public void omMentorEndret(OmMentorEndret event) { + lagHendelse(event.getAvtale(), HendelseType.OM_MENTOR_ENDRET, event.getUtførtAv(), Avtalerolle.VEILEDER); + } + + @EventListener + public void nyVeilederPåAvtale(AvtaleNyVeileder event) { + lagHendelse(event.getAvtale(), HendelseType.NY_VEILEDER, event.getAvtale().getVeilederNavIdent(), Avtalerolle.VEILEDER); + } + + @EventListener + public void godkjentAvArbeidsgiver(GodkjentAvArbeidsgiver event) { + lagHendelse(event.getAvtale(), HendelseType.GODKJENT_AV_ARBEIDSGIVER, event.getUtfortAv(), Avtalerolle.ARBEIDSGIVER); + } + @EventListener + public void godkjentAvVeileder(GodkjentAvVeileder event) { + lagHendelse(event.getAvtale(), HendelseType.GODKJENT_AV_VEILEDER, event.getUtfortAv(), Avtalerolle.VEILEDER); + } + @EventListener + public void godkjentAvDeltaker(GodkjentAvDeltaker event) { + lagHendelse(event.getAvtale(), HendelseType.GODKJENT_AV_DELTAKER, event.getUtfortAv(), Avtalerolle.DELTAKER); + } + @EventListener + public void godkjentPåVegneAvDeltaker(GodkjentPaVegneAvDeltaker event) { + lagHendelse(event.getAvtale(), HendelseType.GODKJENT_PAA_VEGNE_AV, event.getUtfortAv(), Avtalerolle.VEILEDER); + } + @EventListener + public void godkjentPåVegneAvArbeidsgiver(GodkjentPaVegneAvArbeidsgiver event) { + lagHendelse(event.getAvtale(), HendelseType.GODKJENT_PAA_VEGNE_AV_ARBEIDSGIVER, event.getUtfortAv(), Avtalerolle.VEILEDER); + } + @EventListener + public void godkjentPåVegneAvDeltakerOgArbeidsgiver(GodkjentPaVegneAvDeltakerOgArbeidsgiver event) { + lagHendelse(event.getAvtale(), HendelseType.GODKJENT_PAA_VEGNE_AV_DELTAKER_OG_ARBEIDSGIVER, event.getUtfortAv(), Avtalerolle.VEILEDER); + } + @EventListener + public void signertAvMentor(SignertAvMentor event) { + lagHendelse(event.getAvtale(), HendelseType.SIGNERT_AV_MENTOR, event.getUtfortAv(), Avtalerolle.MENTOR); + } + + private void lagHendelse(Avtale avtale, HendelseType hendelseType, Identifikator utførtAv, Avtalerolle utførtAvRolle) { + LocalDateTime tidspunkt = Now.localDateTime(); + UUID meldingId = UUID.randomUUID(); + + AvtaleHendelseUtførtAvRolle rolle = AvtaleHendelseUtførtAvRolle.SYSTEM; + if(utførtAvRolle != null) { + rolle = AvtaleHendelseUtførtAvRolle.valueOf(utførtAvRolle.name()); + } + var melding = AvtaleMelding.create(avtale, avtale.getGjeldendeInnhold(), utførtAv, rolle, hendelseType); + try { + String meldingSomString = objectMapper.writeValueAsString(melding); + AvtaleMeldingEntitet entitet = new AvtaleMeldingEntitet(meldingId, avtale.getId(), tidspunkt, hendelseType, avtale.statusSomEnum(), meldingSomString); + avtaleMeldingEntitetRepository.save(entitet); + } catch (JsonProcessingException e) { + log.error("Feil ved parsing av AvtaleHendelseMelding til json for avtale med id: {}", avtale.getId()); + throw new RuntimeException(e); + } + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseService.java new file mode 100644 index 000000000..861005474 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseService.java @@ -0,0 +1,104 @@ +package no.nav.tag.tiltaksgjennomforing.datadeling; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +@RequiredArgsConstructor +@Service +public class AvtaleHendelseService { + + private final AvtaleRepository avtaleRepository; + private final AvtaleMeldingEntitetRepository avtaleMeldingEntitetRepository; + + private final ObjectMapper objectMapper; + + public void sendAvtaleHendelseMeldingPåEnAvtale(Avtale avtale) { + lagMelding(avtale); + log.info("Send melding om avtale {}", avtale.getId()); + } + + @Async + public void sendAvtaleHendelseMeldingPåAlleAvtaler() { + AtomicInteger antallSendt = new AtomicInteger(); + + log.info("Henter ALLE avtaler for å lage patchehendelsemeldinger"); + List alleAvtaler = avtaleRepository.findAll(); // /o\ \o/ + log.info("Alle avtaler er hentet, totalt {} antall avtaler, looper og sender hendelsemeldinger", alleAvtaler.size()); + + alleAvtaler.forEach(avtale -> { + if(skalSendes(avtale)) { + lagMelding(avtale); + antallSendt.getAndIncrement(); + } + if(antallSendt.get() % 100 == 0) { + log.info("Gått igjennom {} antall avtaler", antallSendt.get()); + } + }); + log.info("Sendt totalt {} antall hendelsemeldinger", antallSendt.get()); + } + + @Async + public void sendAvtaleHendelseMeldingPåAlleAvtalerDRYRun() { + AtomicInteger antallSendt = new AtomicInteger(); + + log.info("DRY RUN - Henter ALLE avtaler for å lage patchehendelsemeldinger"); + List alleAvtaler = avtaleRepository.findAll(); // /o\ \o/ + log.info("DRY RUN - Alle avtaler er hentet, det ble hele {} avtaler. looper og sender hendelsemeldinger", alleAvtaler.size()); + + alleAvtaler.forEach(avtale -> { + if(skalSendes(avtale)) { + lagMeldingDRYRun(avtale); + antallSendt.getAndIncrement(); + } + if(antallSendt.get() % 100 == 0) { + log.info("Gått igjennom {} antall avtaler", antallSendt.get()); + } + }); + log.info("DRY RUN - Sendt totalt {} antall hendelsemeldinger", antallSendt.get()); + } + + // Skal alt sendes egentlig? + private boolean skalSendes(Avtale avtale) { + return true; + } + + private void lagMelding(Avtale avtale) { + var melding = AvtaleMelding.create(avtale, avtale.getGjeldendeInnhold(), new Identifikator("tiltaksgjennomforing-api"), AvtaleHendelseUtførtAvRolle.SYSTEM, HendelseType.STATUSENDRING); + UUID meldingId = UUID.randomUUID(); + LocalDateTime tidspunkt = Now.localDateTime(); + try { + String meldingSomString = objectMapper.writeValueAsString(melding); + AvtaleMeldingEntitet entitet = new AvtaleMeldingEntitet(meldingId, avtale.getId(), tidspunkt, HendelseType.STATUSENDRING, avtale.statusSomEnum(), meldingSomString); + avtaleMeldingEntitetRepository.save(entitet); + } catch (JsonProcessingException e) { + log.error("Feil ved parsing av AvtaleHendelseMelding til json for avtale med id: {}", avtale.getId()); + } + } + + private void lagMeldingDRYRun(Avtale avtale) { + var melding = AvtaleMelding.create(avtale, avtale.getGjeldendeInnhold(), new Identifikator("system"), AvtaleHendelseUtførtAvRolle.SYSTEM, HendelseType.STATUSENDRING); + try { + String meldingSomString = objectMapper.writeValueAsString(melding); + if(meldingSomString == null ) { + log.info("Melding ble ikke generert"); + } + } catch (JsonProcessingException e) { + log.error("Feil ved parsing av AvtaleHendelseMelding til json for avtale med id: {}", avtale.getId()); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseStatusendringJobb.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseStatusendringJobb.java new file mode 100644 index 000000000..a7e3a75dd --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseStatusendringJobb.java @@ -0,0 +1,67 @@ +package no.nav.tag.tiltaksgjennomforing.datadeling; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import no.nav.tag.tiltaksgjennomforing.leader.LeaderPodCheck; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@Profile({Miljø.DEV_FSS, Miljø.PROD_FSS, Miljø.LOCAL}) +@Component +@RequiredArgsConstructor +@EnableScheduling +@Slf4j +public class AvtaleHendelseStatusendringJobb { + private final AvtaleMeldingEntitetRepository avtaleMeldingEntitetRepository; + private final AvtaleRepository avtaleRepository; + private final LeaderPodCheck leaderPodCheck; + + private final ObjectMapper objectMapper; + + @Scheduled(fixedDelayString = "${tiltaksgjennomforing.avtale-hendelse-melding.fixed-delay}", timeUnit = TimeUnit.MINUTES) + public void sjekkOmStatusendring() { + if (!leaderPodCheck.isLeaderPod()) { + log.info("Pod er ikke leader, så kjører ikke jobb for å finne avtaler med statusendring"); + return; + } + + int antallNyeMeldinger = 0; + for (AvtaleMeldingEntitet avtaleMeldingEntitet : avtaleMeldingEntitetRepository.findNyesteAvtaleHendelseMeldingForAvtaleSomKanEndreStatus()) { + UUID avtaleId = avtaleMeldingEntitet.getAvtaleId(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(); + + if (avtale.statusSomEnum() != avtaleMeldingEntitet.getAvtaleStatus()) { + LocalDateTime tidspunkt = Now.localDateTime(); + AvtaleMelding avtaleMelding = AvtaleMelding.create(avtale, avtale.getGjeldendeInnhold(), new Identifikator("tiltaksgjennomforing-api"), AvtaleHendelseUtførtAvRolle.SYSTEM, HendelseType.STATUSENDRING); + try { + String meldingSomString = objectMapper.writeValueAsString(avtaleMelding); + AvtaleMeldingEntitet entitet = new AvtaleMeldingEntitet(UUID.randomUUID(), avtaleId, tidspunkt, HendelseType.STATUSENDRING, avtale.statusSomEnum(), meldingSomString); + avtaleMeldingEntitetRepository.save(entitet); + log.info("Avtale med id {} har byttet status til {}, siste melding har status {}, så sender melding med den nye statusen på topic {}", + avtale.getId(), avtale.statusSomEnum(), avtaleMeldingEntitet.getAvtaleStatus(), Topics.AVTALE_HENDELSE); + antallNyeMeldinger++; + } catch (JsonProcessingException e) { + log.error("Feil ved parsing av AvtaleHendelseMelding i statusendringjobb til json for hendelse med avtaleId {}", avtaleMelding.getAvtaleId()); + throw new RuntimeException(e); + } + } + } + + log.info("Jobb for å finne avtaler med statusendring har kjørt og sendte {} nye meldinger på topic {}", antallNyeMeldinger, Topics.AVTALE_HENDELSE); + } +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseUtf\303\270rtAvRolle.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseUtf\303\270rtAvRolle.java" new file mode 100644 index 000000000..0a70e2f1e --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseUtf\303\270rtAvRolle.java" @@ -0,0 +1,5 @@ +package no.nav.tag.tiltaksgjennomforing.datadeling; + +public enum AvtaleHendelseUtførtAvRolle { + VEILEDER, ARBEIDSGIVER, SYSTEM, DELTAKER, MENTOR, BESLUTTER +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMelding.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMelding.java new file mode 100644 index 000000000..914a9cff5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMelding.java @@ -0,0 +1,222 @@ +package no.nav.tag.tiltaksgjennomforing.datadeling; + +import lombok.Data; +import no.nav.tag.tiltaksgjennomforing.avtale.*; +import no.nav.tag.tiltaksgjennomforing.enhet.Formidlingsgruppe; +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +@Data +public class AvtaleMelding { + HendelseType hendelseType; + Status avtaleStatus; + + Identifikator deltakerFnr; + Identifikator mentorFnr; + Identifikator bedriftNr; + Identifikator veilederNavIdent; + Tiltakstype tiltakstype; + LocalDateTime opprettetTidspunkt; + UUID avtaleId; + Integer avtaleNr; + Instant sistEndret; + Instant annullertTidspunkt; + String annullertGrunn; + boolean slettemerket; + boolean opprettetAvArbeidsgiver; + String enhetGeografisk; + String enhetsnavnGeografisk; + String enhetOppfolging; + String enhetsnavnOppfolging; + boolean godkjentForEtterregistrering; + Kvalifiseringsgruppe kvalifiseringsgruppe; + Formidlingsgruppe formidlingsgruppe; + SortedSet tilskuddPeriode = new TreeSet<>(); + boolean feilregistrert; + + // Innhold + Integer versjon; + String deltakerFornavn; + String deltakerEtternavn; + String deltakerTlf; + String bedriftNavn; + String arbeidsgiverFornavn; + String arbeidsgiverEtternavn; + String arbeidsgiverTlf; + String veilederFornavn; + String veilederEtternavn; + String veilederTlf; + String oppfolging; + String tilrettelegging; + LocalDate startDato; + LocalDate sluttDato; + Integer stillingprosent; + String journalpostId; + String arbeidsoppgaver; + String stillingstittel; + Integer stillingStyrk08; + Integer stillingKonseptId; + Integer antallDagerPerUke; + RefusjonKontaktperson refusjonKontaktperson; + // Mentor + String mentorFornavn; + String mentorEtternavn; + String mentorOppgaver; + Double mentorAntallTimer; + Integer mentorTimelonn; + String mentorTlf; + + // Lønnstilskudd + String arbeidsgiverKontonummer; + Integer lonnstilskuddProsent; + Integer manedslonn; + BigDecimal feriepengesats; + BigDecimal arbeidsgiveravgift; + Boolean harFamilietilknytning; + String familietilknytningForklaring; + Integer feriepengerBelop; + Double otpSats; + Integer otpBelop; + Integer arbeidsgiveravgiftBelop; + Integer sumLonnsutgifter; + Integer sumLonnstilskudd; + Integer manedslonn100pst; + Integer sumLønnstilskuddRedusert; + LocalDate datoForRedusertProsent; + Stillingstype stillingstype; + + List maal = new ArrayList<>(); + + // Inkluderingstilskudd + List inkluderingstilskuddsutgift = new ArrayList<>(); + String inkluderingstilskuddBegrunnelse; + Integer inkluderingstilskuddTotalBeløp; + + // Godkjenning + LocalDateTime godkjentAvDeltaker; + LocalDateTime godkjentTaushetserklæringAvMentor; + LocalDateTime godkjentAvArbeidsgiver; + LocalDateTime godkjentAvVeileder; + LocalDateTime godkjentAvBeslutter; + LocalDateTime avtaleInngått; + LocalDateTime ikrafttredelsestidspunkt; + Identifikator godkjentAvNavIdent; + Identifikator godkjentAvBeslutterNavIdent; + + // Kostnadssted + String enhetKostnadssted; + String enhetsnavnKostnadssted; + GodkjentPaVegneGrunn godkjentPaVegneGrunn; + boolean godkjentPaVegneAv; + GodkjentPaVegneAvArbeidsgiverGrunn godkjentPaVegneAvArbeidsgiverGrunn; + boolean godkjentPaVegneAvArbeidsgiver; + AvtaleInnholdType innholdType; + Identifikator utførtAv; + AvtaleHendelseUtførtAvRolle utførtAvRolle; + + public static AvtaleMelding create(Avtale avtale, AvtaleInnhold avtaleInnhold, Identifikator utførtAv, AvtaleHendelseUtførtAvRolle utførtAvAvtaleRolle, HendelseType hendelseType) { + + AvtaleMelding avtaleMelding = new AvtaleMelding(); + avtaleMelding.setHendelseType(hendelseType); + avtaleMelding.setAvtaleStatus(avtale.statusSomEnum()); + avtaleMelding.setDeltakerFnr(avtale.getDeltakerFnr()); + avtaleMelding.setMentorFnr(avtale.getMentorFnr()); + avtaleMelding.setBedriftNr(avtale.getBedriftNr()); + avtaleMelding.setVeilederNavIdent(avtale.getVeilederNavIdent()); + avtaleMelding.setTiltakstype(avtale.getTiltakstype()); + avtaleMelding.setOpprettetTidspunkt(avtale.getOpprettetTidspunkt()); + avtaleMelding.setAvtaleId(avtale.getId()); + avtaleMelding.setAvtaleNr(avtale.getAvtaleNr()); + avtaleMelding.setSistEndret(avtale.getSistEndret()); + avtaleMelding.setAnnullertTidspunkt(avtale.getAnnullertTidspunkt()); + avtaleMelding.setAnnullertGrunn(avtale.getAnnullertGrunn()); + avtaleMelding.setSlettemerket(avtale.isSlettemerket()); + avtaleMelding.setOpprettetAvArbeidsgiver(avtale.isOpprettetAvArbeidsgiver()); + avtaleMelding.setEnhetGeografisk(avtale.getEnhetGeografisk()); + avtaleMelding.setEnhetsnavnGeografisk(avtale.getEnhetsnavnGeografisk()); + avtaleMelding.setEnhetOppfolging(avtale.getEnhetOppfolging()); + avtaleMelding.setEnhetsnavnOppfolging(avtale.getEnhetsnavnOppfolging()); + avtaleMelding.setGodkjentForEtterregistrering(avtale.isGodkjentForEtterregistrering()); + avtaleMelding.setKvalifiseringsgruppe(avtale.getKvalifiseringsgruppe()); + avtaleMelding.setFormidlingsgruppe(avtale.getFormidlingsgruppe()); + avtaleMelding.setFeilregistrert(avtale.isFeilregistrert()); + avtaleMelding.setVersjon(avtaleInnhold.getVersjon()); + avtaleMelding.setDeltakerFornavn(avtaleInnhold.getDeltakerFornavn()); + avtaleMelding.setDeltakerEtternavn(avtaleInnhold.getDeltakerEtternavn()); + avtaleMelding.setDeltakerTlf(avtaleInnhold.getDeltakerTlf()); + avtaleMelding.setBedriftNavn(avtaleInnhold.getBedriftNavn()); + avtaleMelding.setArbeidsgiverFornavn(avtaleInnhold.getArbeidsgiverFornavn()); + avtaleMelding.setArbeidsgiverEtternavn(avtaleInnhold.getArbeidsgiverEtternavn()); + avtaleMelding.setArbeidsgiverTlf(avtaleInnhold.getArbeidsgiverTlf()); + avtaleMelding.setVeilederFornavn(avtaleInnhold.getVeilederFornavn()); + avtaleMelding.setVeilederEtternavn(avtaleInnhold.getVeilederEtternavn()); + avtaleMelding.setVeilederTlf(avtaleInnhold.getVeilederTlf()); + avtaleMelding.setOppfolging(avtaleInnhold.getOppfolging()); + avtaleMelding.setTilrettelegging(avtaleInnhold.getTilrettelegging()); + avtaleMelding.setStartDato(avtaleInnhold.getStartDato()); + avtaleMelding.setSluttDato(avtaleInnhold.getSluttDato()); + avtaleMelding.setStillingprosent(avtaleInnhold.getStillingprosent()); + avtaleMelding.setJournalpostId(avtaleInnhold.getJournalpostId()); + avtaleMelding.setArbeidsoppgaver(avtaleInnhold.getArbeidsoppgaver()); + avtaleMelding.setStillingstittel(avtaleInnhold.getStillingstittel()); + avtaleMelding.setStillingStyrk08(avtaleInnhold.getStillingStyrk08()); + avtaleMelding.setStillingKonseptId(avtaleInnhold.getStillingKonseptId()); + avtaleMelding.setAntallDagerPerUke(avtaleInnhold.getAntallDagerPerUke()); + avtaleMelding.setRefusjonKontaktperson(avtaleInnhold.getRefusjonKontaktperson()); + avtaleMelding.setMentorFornavn(avtaleInnhold.getMentorFornavn()); + avtaleMelding.setMentorEtternavn(avtaleInnhold.getMentorEtternavn()); + avtaleMelding.setMentorOppgaver(avtaleInnhold.getMentorOppgaver()); + avtaleMelding.setMentorAntallTimer(avtaleInnhold.getMentorAntallTimer()); + avtaleMelding.setMentorTimelonn(avtaleInnhold.getMentorTimelonn()); + avtaleMelding.setMentorTlf(avtaleInnhold.getMentorTlf()); + avtaleMelding.setArbeidsgiverKontonummer(avtaleInnhold.getArbeidsgiverKontonummer()); + avtaleMelding.setLonnstilskuddProsent(avtaleInnhold.getLonnstilskuddProsent()); + avtaleMelding.setManedslonn(avtaleInnhold.getManedslonn()); + avtaleMelding.setFeriepengesats(avtaleInnhold.getFeriepengesats()); + avtaleMelding.setArbeidsgiveravgift(avtaleInnhold.getArbeidsgiveravgift()); + avtaleMelding.setHarFamilietilknytning(avtaleInnhold.getHarFamilietilknytning()); + avtaleMelding.setFamilietilknytningForklaring(avtaleInnhold.getFamilietilknytningForklaring()); + avtaleMelding.setFeriepengerBelop(avtaleInnhold.getFeriepengerBelop()); + avtaleMelding.setOtpSats(avtaleInnhold.getOtpSats()); + avtaleMelding.setOtpBelop(avtaleInnhold.getOtpBelop()); + avtaleMelding.setArbeidsgiveravgiftBelop(avtaleInnhold.getArbeidsgiveravgiftBelop()); + avtaleMelding.setSumLonnsutgifter(avtaleInnhold.getSumLonnsutgifter()); + avtaleMelding.setSumLonnstilskudd(avtaleInnhold.getSumLonnstilskudd()); + avtaleMelding.setManedslonn100pst(avtaleInnhold.getManedslonn100pst()); + avtaleMelding.setSumLønnstilskuddRedusert(avtaleInnhold.getSumLønnstilskuddRedusert()); + avtaleMelding.setDatoForRedusertProsent(avtaleInnhold.getDatoForRedusertProsent()); + avtaleMelding.setStillingstype(avtaleInnhold.getStillingstype()); + avtaleMelding.setInkluderingstilskuddBegrunnelse(avtaleInnhold.getInkluderingstilskuddBegrunnelse()); + avtaleMelding.setInkluderingstilskuddTotalBeløp(avtaleInnhold.inkluderingstilskuddTotalBeløp()); + avtaleMelding.setGodkjentAvDeltaker(avtaleInnhold.getGodkjentAvDeltaker()); + avtaleMelding.setGodkjentTaushetserklæringAvMentor(avtaleInnhold.getGodkjentTaushetserklæringAvMentor()); + avtaleMelding.setGodkjentAvArbeidsgiver(avtaleInnhold.getGodkjentAvArbeidsgiver()); + avtaleMelding.setGodkjentAvVeileder(avtaleInnhold.getGodkjentAvVeileder()); + avtaleMelding.setGodkjentAvBeslutter(avtaleInnhold.getGodkjentAvBeslutter()); + avtaleMelding.setAvtaleInngått(avtaleInnhold.getAvtaleInngått()); + avtaleMelding.setIkrafttredelsestidspunkt(avtaleInnhold.getIkrafttredelsestidspunkt()); + avtaleMelding.setGodkjentAvNavIdent(avtaleInnhold.getGodkjentAvNavIdent()); + avtaleMelding.setGodkjentAvBeslutterNavIdent(avtaleInnhold.getGodkjentAvBeslutterNavIdent()); + avtaleMelding.setEnhetKostnadssted(avtaleInnhold.getEnhetKostnadssted()); + avtaleMelding.setEnhetsnavnKostnadssted(avtaleInnhold.getEnhetsnavnKostnadssted()); + avtaleMelding.setGodkjentPaVegneGrunn(avtaleInnhold.getGodkjentPaVegneGrunn()); + avtaleMelding.setGodkjentPaVegneAv(avtaleInnhold.isGodkjentPaVegneAv()); + avtaleMelding.setGodkjentPaVegneAvArbeidsgiverGrunn(avtaleInnhold.getGodkjentPaVegneAvArbeidsgiverGrunn()); + avtaleMelding.setGodkjentPaVegneAvArbeidsgiver(avtaleInnhold.isGodkjentPaVegneAvArbeidsgiver()); + avtaleMelding.setInnholdType(avtaleInnhold.getInnholdType()); + avtaleMelding.setUtførtAv(utførtAv); + avtaleMelding.setUtførtAvRolle(utførtAvAvtaleRolle); + + //Lister + avtaleMelding.setTilskuddPeriode(Collections.emptySortedSet()); + avtaleMelding.setMaal(avtaleInnhold.getMaal()); + avtaleMelding.setInkluderingstilskuddsutgift(avtaleInnhold.getInkluderingstilskuddsutgift()); + + return avtaleMelding; + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMeldingEntitet.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMeldingEntitet.java new file mode 100644 index 000000000..692915105 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMeldingEntitet.java @@ -0,0 +1,43 @@ +package no.nav.tag.tiltaksgjennomforing.datadeling; + +import lombok.Data; +import lombok.NoArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.avtale.Status; +import org.springframework.data.domain.AbstractAggregateRoot; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "avtale_melding") +public class AvtaleMeldingEntitet extends AbstractAggregateRoot { + + @Id + private UUID meldingId; + private UUID avtaleId; + @Enumerated(EnumType.STRING) + private HendelseType hendelseType; + + @Enumerated(EnumType.STRING) + private Status avtaleStatus; + private LocalDateTime tidspunkt; + private String json; + private boolean sendt; + private boolean sendtCompacted; + + public AvtaleMeldingEntitet(UUID meldingId, UUID avtaleId, LocalDateTime tidspunkt, HendelseType hendelseType, Status avtaleStatus, String meldingAsJson) { + this.meldingId = meldingId; + this.avtaleId = avtaleId; + this.hendelseType = hendelseType; + this.tidspunkt = tidspunkt; + this.json = meldingAsJson; + this.avtaleStatus = avtaleStatus; + + registerEvent(new AvtaleMeldingOpprettet(this)); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMeldingEntitetRepository.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMeldingEntitetRepository.java new file mode 100644 index 000000000..4efec81e4 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMeldingEntitetRepository.java @@ -0,0 +1,16 @@ +package no.nav.tag.tiltaksgjennomforing.datadeling; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.UUID; + +public interface AvtaleMeldingEntitetRepository extends JpaRepository { + + List findAllByAvtaleId(UUID avtaleId); + @Query(nativeQuery = true, value = + "select * from avtale_melding where (avtale_id, tidspunkt) in (select avtale_id, max(tidspunkt) from avtale_melding group by avtale_id) and avtale_status in ('KLAR_FOR_OPPSTART', 'GJENNOMFØRES');") + List findNyesteAvtaleHendelseMeldingForAvtaleSomKanEndreStatus(); + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMeldingKafkaProdusent.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMeldingKafkaProdusent.java new file mode 100644 index 000000000..07d2b10e6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMeldingKafkaProdusent.java @@ -0,0 +1,60 @@ +package no.nav.tag.tiltaksgjennomforing.datadeling; + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; +import org.springframework.util.concurrent.ListenableFutureCallback; + +@Component +@Slf4j +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +public class AvtaleMeldingKafkaProdusent { + + private final KafkaTemplate aivenKafkaTemplate; + private final AvtaleMeldingEntitetRepository repository; + + public AvtaleMeldingKafkaProdusent(@Autowired @Qualifier("aivenKafkaTemplate") KafkaTemplate aivenKafkaTemplate, AvtaleMeldingEntitetRepository repository) { + this.aivenKafkaTemplate = aivenKafkaTemplate; + this.repository = repository; + } + + @TransactionalEventListener + public void avtaleMeldingOpprettet(AvtaleMeldingOpprettet event) { + String meldingId = event.getEntitet().getAvtaleId().toString(); + + aivenKafkaTemplate.send(Topics.AVTALE_HENDELSE, meldingId, event.getEntitet().getJson()).addCallback(new ListenableFutureCallback<>() { + @Override + public void onSuccess(SendResult result) { + log.info("AvtaleHendelse melding med avtaleId {} sendt til Kafka topic {}", meldingId, Topics.AVTALE_HENDELSE); + AvtaleMeldingEntitet entitet = event.getEntitet(); + entitet.setSendt(true); + repository.save(entitet); + } + @Override + public void onFailure(Throwable ex) { + log.error("AvtaleHendelse med avtaleId {} kunne ikke sendes til Kafka topic {}", meldingId, Topics.AVTALE_HENDELSE); + } + }); + + aivenKafkaTemplate.send(Topics.AVTALE_HENDELSE_COMPACT, meldingId, event.getEntitet().getJson()).addCallback(new ListenableFutureCallback<>() { + @Override + public void onSuccess(SendResult result) { + log.info("AvtaleHendelse melding med avtaleId {} sendt til Kafka topic {}", meldingId, Topics.AVTALE_HENDELSE_COMPACT); + AvtaleMeldingEntitet entitet = event.getEntitet(); + entitet.setSendtCompacted(true); + repository.save(entitet); + } + @Override + public void onFailure(Throwable ex) { + log.error("AvtaleHendelse med avtaleId {} kunne ikke sendes til Kafka topic {}", meldingId, Topics.AVTALE_HENDELSE_COMPACT); + } + }); + + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMeldingOpprettet.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMeldingOpprettet.java new file mode 100644 index 000000000..bca8fd21d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleMeldingOpprettet.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.datadeling; + +import lombok.Value; + +@Value +public class AvtaleMeldingOpprettet { + AvtaleMeldingEntitet entitet; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/InternalAvtaleHendelseController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/InternalAvtaleHendelseController.java new file mode 100644 index 000000000..f6288f0ee --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datadeling/InternalAvtaleHendelseController.java @@ -0,0 +1,83 @@ +package no.nav.tag.tiltaksgjennomforing.datadeling; + + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.victools.jsonschema.generator.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.security.token.support.core.api.ProtectedWithClaims; +import no.nav.security.token.support.core.api.Unprotected; +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.exceptions.RessursFinnesIkkeException; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.util.*; + +@Profile(value = {Miljø.DEV_FSS}) +@RestController +@RequestMapping("/avtale-hendelse") +@RequiredArgsConstructor +@ProtectedWithClaims(issuer = "aad") +@Slf4j +public class InternalAvtaleHendelseController { + + private final AvtaleMeldingEntitetRepository avtaleMeldingEntitetRepository; + private final AvtaleRepository avtaleRepository; + + private final TokenUtils tokenUtils; + + private void sjekkTilgang() { + if (!tokenUtils.harAdRolle("access_as_application")) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN); + } + } + + @PostMapping("/fnr") + public List hentSisteHendelseForFnr(@RequestBody AvtaleMeldingForFnr meldingForFnr) { + sjekkTilgang(); + List hendelser = new ArrayList<>(); + if(Fnr.erGyldigFnr(meldingForFnr.fnr)) { + List alleAvtalerForDeltaker = avtaleRepository.findAllByDeltakerFnr(new Fnr(meldingForFnr.fnr)); + alleAvtalerForDeltaker.forEach(avtale -> { + List avtaleMeldingEntiteter = avtaleMeldingEntitetRepository.findAllByAvtaleId(avtale.getId()); + AvtaleMeldingEntitet avtaleMeldingEntitet = avtaleMeldingEntiteter.stream().max(Comparator.comparing(melding -> melding.getTidspunkt())).orElseGet(null); + if(avtaleMeldingEntitet != null) { + hendelser.add(avtaleMeldingEntitet.getJson()); + } + }); + } + + return hendelser; + } + + @GetMapping("/{avtaleId}") + public String hentSisteHendelse(@PathVariable("avtaleId") UUID avtaleId) { + List avtaleMeldingEntiteter = avtaleMeldingEntitetRepository.findAllByAvtaleId(avtaleId); + AvtaleMeldingEntitet avtaleMeldingEntitet = avtaleMeldingEntiteter.stream().max(Comparator.comparing(melding -> melding.getTidspunkt())).orElseThrow(RessursFinnesIkkeException::new); + + return avtaleMeldingEntitet.getJson(); + } + + @GetMapping("/skjema") + @Unprotected + public String hentJsonSkjema() { + SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) + .without(Option.DEFINITIONS_FOR_ALL_OBJECTS); + SchemaGeneratorConfig config = configBuilder.build(); + SchemaGenerator generator = new SchemaGenerator(config); + JsonNode jsonSchema = generator.generateSchema(AvtaleMelding.class); + + return jsonSchema.toPrettyString(); + } + + + private record AvtaleMeldingForFnr(String fnr) { } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/AvroTiltakHendelseFabrikk.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/AvroTiltakHendelseFabrikk.java new file mode 100644 index 000000000..e48ecdb34 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/AvroTiltakHendelseFabrikk.java @@ -0,0 +1,81 @@ +package no.nav.tag.tiltaksgjennomforing.datavarehus; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.UUID; +import lombok.experimental.UtilityClass; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; + +@UtilityClass +public class AvroTiltakHendelseFabrikk { + public static AvroTiltakHendelse konstruer(Avtale avtale, LocalDateTime tidspunkt, UUID meldingId, DvhHendelseType hendelseType, String utførtAv) { + AvroTiltakHendelse hendelse = new AvroTiltakHendelse(); + hendelse.setMeldingId(meldingId.toString()); + hendelse.setTidspunkt(toInstant(tidspunkt)); + hendelse.setAvtaleId(avtale.getId().toString()); + hendelse.setAvtaleInnholdId(avtale.getGjeldendeInnhold().getId().toString()); + hendelse.setTiltakstype(TiltakType.valueOf(avtale.getTiltakstype().name())); + hendelse.setTiltakskodeArena(avtale.getTiltakstype().getTiltakskodeArena() != null ? TiltakKodeArena.valueOf(avtale.getTiltakstype().getTiltakskodeArena()) : null); + hendelse.setHendelseType(hendelseType.name()); + hendelse.setTiltakStatus(avtale.statusSomEnum().name()); + hendelse.setDeltakerFnr(avtale.getDeltakerFnr().asString()); + hendelse.setBedriftNr(avtale.getBedriftNr().asString()); + hendelse.setVeilederNavIdent(avtale.getVeilederNavIdent().asString()); + hendelse.setHarFamilietilknytning(avtale.getGjeldendeInnhold().getHarFamilietilknytning()); + hendelse.setStartDato(avtale.getGjeldendeInnhold().getStartDato()); + hendelse.setSluttDato(avtale.getGjeldendeInnhold().getSluttDato()); + hendelse.setStillingprosent(avtale.getGjeldendeInnhold().getStillingprosent()); + hendelse.setAntallDagerPerUke(avtale.getGjeldendeInnhold().getAntallDagerPerUke()); + hendelse.setStillingstittel(avtale.getGjeldendeInnhold().getStillingstittel()); + hendelse.setStillingstype(avtale.getGjeldendeInnhold().getStillingstype() != null ? StillingType.valueOf(avtale.getGjeldendeInnhold().getStillingstype().name()) : null); + hendelse.setStillingStyrk08(avtale.getGjeldendeInnhold().getStillingStyrk08()); + hendelse.setStillingKonseptId(avtale.getGjeldendeInnhold().getStillingKonseptId()); + hendelse.setLonnstilskuddProsent(avtale.getGjeldendeInnhold().getLonnstilskuddProsent()); + hendelse.setManedslonn(avtale.getGjeldendeInnhold().getManedslonn()); + hendelse.setFeriepengesats(avtale.getGjeldendeInnhold().getFeriepengesats() != null ? avtale.getGjeldendeInnhold().getFeriepengesats().floatValue() : null); + hendelse.setFeriepengerBelop(avtale.getGjeldendeInnhold().getFeriepengerBelop()); + hendelse.setArbeidsgiveravgift(avtale.getGjeldendeInnhold().getArbeidsgiveravgift() != null ? avtale.getGjeldendeInnhold().getArbeidsgiveravgift().floatValue() : null); + hendelse.setArbeidsgiveravgiftBelop(avtale.getGjeldendeInnhold().getFeriepengerBelop()); + hendelse.setOtpSats(avtale.getGjeldendeInnhold().getOtpSats() != null ? avtale.getGjeldendeInnhold().getOtpSats().floatValue() : null); + hendelse.setOtpBelop(avtale.getGjeldendeInnhold().getOtpBelop()); + hendelse.setSumLonnsutgifter(avtale.getGjeldendeInnhold().getSumLonnsutgifter()); + hendelse.setSumLonnstilskudd(avtale.getGjeldendeInnhold().getSumLonnstilskudd()); + hendelse.setSumLonnstilskuddRedusert(avtale.getGjeldendeInnhold().getSumLønnstilskuddRedusert()); + hendelse.setDatoForRedusertProsent(avtale.getGjeldendeInnhold().getDatoForRedusertProsent()); + hendelse.setGodkjentPaVegneAv(avtale.getGjeldendeInnhold().isGodkjentPaVegneAv()); + hendelse.setIkkeBankId(avtale.getGjeldendeInnhold().getGodkjentPaVegneGrunn() != null && avtale.getGjeldendeInnhold().getGodkjentPaVegneGrunn().isIkkeBankId()); + hendelse.setReservert(avtale.getGjeldendeInnhold().getGodkjentPaVegneGrunn() != null && avtale.getGjeldendeInnhold().getGodkjentPaVegneGrunn().isReservert()); + hendelse.setDigitalKompetanse(avtale.getGjeldendeInnhold().getGodkjentPaVegneGrunn() != null && avtale.getGjeldendeInnhold().getGodkjentPaVegneGrunn().isDigitalKompetanse()); + hendelse.setArenaMigreringDeltaker(avtale.getGjeldendeInnhold().getGodkjentPaVegneGrunn() != null && avtale.getGjeldendeInnhold().getGodkjentPaVegneGrunn().isArenaMigreringDeltaker()); + hendelse.setGodkjentAvDeltaker(toInstant(avtale.getGjeldendeInnhold().getGodkjentAvDeltaker())); + hendelse.setGodkjentAvArbeidsgiver(toInstant(avtale.getGjeldendeInnhold().getGodkjentAvArbeidsgiver())); + hendelse.setGodkjentAvArbeidsgiver(toInstant(avtale.getGjeldendeInnhold().getGodkjentAvArbeidsgiver())); + hendelse.setGodkjentAvVeileder(toInstant(avtale.getGjeldendeInnhold().getGodkjentAvVeileder())); + hendelse.setGodkjentAvBeslutter(toInstant(avtale.getGjeldendeInnhold().getGodkjentAvBeslutter())); + hendelse.setAvtaleInngaatt(toInstant(avtale.getGjeldendeInnhold().getAvtaleInngått())); + hendelse.setUtfortAv(utførtAv); + hendelse.setEnhetOppfolging(avtale.getEnhetOppfolging()); + hendelse.setEnhetGeografisk(avtale.getEnhetGeografisk()); + hendelse.setOpprettetAvArbeidsgiver(avtale.isOpprettetAvArbeidsgiver()); + hendelse.setAnnullertTidspunkt(avtale.getAnnullertTidspunkt()); + hendelse.setAnnullertGrunn(avtale.getAnnullertGrunn()); + hendelse.setMaster(erMaster(avtale)); + return hendelse; + } + + private Boolean erMaster(Avtale avtale) { + if(avtale.getTiltakstype() == Tiltakstype.SOMMERJOBB || avtale.getTiltakstype() == Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD || avtale.getTiltakstype() == Tiltakstype.VARIG_LONNSTILSKUDD) { + return Boolean.TRUE; + } + return Boolean.FALSE; + } + + private static Instant toInstant(LocalDateTime tidspunkt) { + if (tidspunkt == null) { + return null; + } + return tidspunkt.atZone(ZoneId.systemDefault()).toInstant(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhAvtalePatchService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhAvtalePatchService.java new file mode 100644 index 000000000..67f072176 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhAvtalePatchService.java @@ -0,0 +1,63 @@ +package no.nav.tag.tiltaksgjennomforing.datavarehus; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +@Service +@Slf4j +@RequiredArgsConstructor +public class DvhAvtalePatchService { + + private final AvtaleRepository avtaleRepository; + private final DvhMeldingEntitetRepository dvhRepository; + + @Async + public void lagDvhPatchMeldingForAlleAvtaler() { + AtomicInteger antallPatchet = new AtomicInteger(); + List alleAvtaler = avtaleRepository.findAllByGjeldendeInnhold_AvtaleInngåttNotNull(); + + alleAvtaler.forEach(avtale -> { + if(skalPatches(avtale)) { + lagDvhPatchMelding(avtale); + antallPatchet.getAndIncrement(); + if(antallPatchet.get() % 100 == 0) { + log.info("Migrert {} antall avtaler", antallPatchet.get()); + } + } + log.info("Avtale {} skal ikke patches i DVH", avtale.getId()); + }); + log.info("Migrert {} antall avtaler", antallPatchet.get()); + } + + @Transactional + void lagDvhPatchMelding(Avtale avtale) { + LocalDateTime tidspunkt = Now.localDateTime(); + UUID meldingId = UUID.randomUUID(); + var melding = AvroTiltakHendelseFabrikk.konstruer(avtale, tidspunkt, meldingId, DvhHendelseType.PATCHING, "system"); + DvhMeldingEntitet entitet = new DvhMeldingEntitet(meldingId, avtale.getId(), tidspunkt, avtale.statusSomEnum(), melding); + dvhRepository.save(entitet); + } + + private boolean skalPatches(Avtale avtale) { + if(avtale.erAvtaleInngått()) { + if(!avtale.erGodkjentAvVeileder()) { + log.warn("Avtale {} er inngått men ikke godkjent av veileder", avtale.getId()); + return false; + } + return true; + } else { + return false; + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhAvtalehendelseLytter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhAvtalehendelseLytter.java new file mode 100644 index 000000000..846901d0f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhAvtalehendelseLytter.java @@ -0,0 +1,85 @@ +package no.nav.tag.tiltaksgjennomforing.datavarehus; + +import lombok.RequiredArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; +import no.nav.tag.tiltaksgjennomforing.avtale.events.*; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class DvhAvtalehendelseLytter { + private final DvhMeldingEntitetRepository repository; + + @EventListener + public void avtaleInngått(AvtaleInngått event) { + Avtale avtale = event.getAvtale(); + lagHendelse(avtale, DvhHendelseType.INNGÅTT, event.getUtførtAv()); + } + + @EventListener + public void avtaleForlenget(AvtaleForlenget event) { + lagHendelse(event.getAvtale(), DvhHendelseType.FORLENGET, event.getUtførtAv()); + } + + @EventListener + public void avtaleForkortet(AvtaleForkortet event) { + lagHendelse(event.getAvtale(), DvhHendelseType.FORKORTET, event.getUtførtAv()); + } + + @EventListener + public void avtaleAnnullert(AnnullertAvVeileder event) { + lagHendelse(event.getAvtale(), DvhHendelseType.ANNULLERT, event.getUtfortAv()); + } + + @EventListener + public void tilskuddsberegningEndret(TilskuddsberegningEndret event) { + lagHendelse(event.getAvtale(), DvhHendelseType.ENDRET, event.getUtførtAv()); + } + + @EventListener + public void stillingsbeskrivelseEndret(StillingsbeskrivelseEndret event) { + lagHendelse(event.getAvtale(), DvhHendelseType.ENDRET, event.getUtførtAv()); + } + + @EventListener + public void kontaktinformasjonEndret(KontaktinformasjonEndret event) { + lagHendelse(event.getAvtale(), DvhHendelseType.ENDRET, event.getUtførtAv()); + } + + @EventListener + public void oppfølgingOgTilretteleggingEndret(OppfølgingOgTilretteleggingEndret event) { + lagHendelse(event.getAvtale(), DvhHendelseType.ENDRET, event.getUtførtAv()); + } + + @EventListener + public void målEndret(MålEndret event) { + lagHendelse(event.getAvtale(), DvhHendelseType.ENDRET, event.getUtførtAv()); + } + + @EventListener + public void inkluderingstilskuddEndret(InkluderingstilskuddEndret event) { + lagHendelse(event.getAvtale(), DvhHendelseType.ENDRET, event.getUtførtAv()); + } + + @EventListener + public void omMentorEndret(OmMentorEndret event) { + lagHendelse(event.getAvtale(), DvhHendelseType.ENDRET, event.getUtførtAv()); + } + + private void lagHendelse(Avtale avtale, DvhHendelseType endret, NavIdent utførtAv) { + if(avtale.erAvtaleInngått()) { + LocalDateTime tidspunkt = Now.localDateTime(); + UUID meldingId = UUID.randomUUID(); + DvhHendelseType hendelseType = endret; + var melding = AvroTiltakHendelseFabrikk.konstruer(avtale, tidspunkt, meldingId, hendelseType, utførtAv.asString()); + DvhMeldingEntitet entitet = new DvhMeldingEntitet(meldingId, avtale.getId(), tidspunkt, avtale.statusSomEnum(), melding); + repository.save(entitet); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhHendelseType.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhHendelseType.java new file mode 100644 index 000000000..614156550 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhHendelseType.java @@ -0,0 +1,5 @@ +package no.nav.tag.tiltaksgjennomforing.datavarehus; + +public enum DvhHendelseType { + MIGRERING, INNGÅTT, STATUSENDRING, ANNULLERT, FORKORTET, FORLENGET, ENDRET, PATCHING +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingEntitet.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingEntitet.java new file mode 100644 index 000000000..e9dbee95d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingEntitet.java @@ -0,0 +1,34 @@ +package no.nav.tag.tiltaksgjennomforing.datavarehus; + +import lombok.Data; +import lombok.NoArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.avtale.Status; +import org.springframework.data.domain.AbstractAggregateRoot; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "dvh_melding") +public class DvhMeldingEntitet extends AbstractAggregateRoot { + @Id + private UUID meldingId; + private UUID avtaleId; + private LocalDateTime tidspunkt; + @Enumerated(EnumType.STRING) + private Status tiltakStatus; + private String json; + private boolean sendt; + + public DvhMeldingEntitet(UUID meldingId, UUID avtaleId, LocalDateTime tidspunkt, Status tiltakStatus, AvroTiltakHendelse avroTiltakHendelse) { + this.meldingId = meldingId; + this.avtaleId = avtaleId; + this.tidspunkt = tidspunkt; + this.tiltakStatus = tiltakStatus; + this.json = avroTiltakHendelse.toString(); + registerEvent(new DvhMeldingOpprettet(this, avroTiltakHendelse)); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingEntitetRepository.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingEntitetRepository.java new file mode 100644 index 000000000..ba377240b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingEntitetRepository.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.datavarehus; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.UUID; + + +public interface DvhMeldingEntitetRepository extends JpaRepository { + @Query(nativeQuery = true, value = "select * from dvh_melding where (avtale_id, tidspunkt) in (select avtale_id, max(tidspunkt) from dvh_melding group by avtale_id) and tiltak_status in ('KLAR_FOR_OPPSTART', 'GJENNOMFØRES');") + List findNyesteDvhMeldingForAvtaleSomKanEndreStatus(); + + boolean existsByAvtaleId(UUID avtaleId); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingKafkaConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingKafkaConfiguration.java new file mode 100644 index 000000000..4a6e0139c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingKafkaConfiguration.java @@ -0,0 +1,70 @@ +package no.nav.tag.tiltaksgjennomforing.datavarehus; + +import io.confluent.kafka.serializers.KafkaAvroSerializer; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.config.SslConfigs; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; + +import java.util.HashMap; +import java.util.Map; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Configuration +@Slf4j +@EnableKafka +public class DvhMeldingKafkaConfiguration { + + @Value("${no.nav.gcp.kafka.aiven.bootstrap-servers}") + private String gcpBootstrapServers; + @Value("${no.nav.gcp.kafka.aiven.security-protocol}") + private String securityProtocol; + @Value("${no.nav.gcp.kafka.aiven.truststore-path}") + private String sslTruststoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.truststore-password}") + private String sslTruststorePasswordEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-path}") + private String sslKeystoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-password}") + private String sslKeystorePasswordEnvKey; + @Value("${no.nav.gcp.kafka.aiven.schema-registry-url}") + private String schemaRegistryUrl; + @Value("${no.nav.gcp.kafka.aiven.schema-registry-credentials-source}") + private String schemaRegistryCredentialsSource; + @Value("${no.nav.gcp.kafka.aiven.schema-registry-user-info}") + private String schemaRegistryUserInfo; + + private Map producerConfigs() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, gcpBootstrapServers); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, KafkaAvroSerializer.class); + props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol); + + props.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, ""); + props.put(SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG, "jks"); + props.put(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG, "PKCS12"); + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, sslTruststoreLocationEnvKey); + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, sslTruststorePasswordEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, sslKeystoreLocationEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, sslKeystorePasswordEnvKey); + + props.put("schema.registry.url", schemaRegistryUrl); + props.put("basic.auth.credentials.source", schemaRegistryCredentialsSource); + props.put("basic.auth.user.info", schemaRegistryUserInfo); + return props; + } + + @Bean + public KafkaTemplate dvhMeldingKafkaTemplate() { + return new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(producerConfigs())); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingKafkaProdusent.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingKafkaProdusent.java new file mode 100644 index 000000000..a8ab65efc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingKafkaProdusent.java @@ -0,0 +1,45 @@ +package no.nav.tag.tiltaksgjennomforing.datavarehus; + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; +import org.springframework.util.concurrent.ListenableFutureCallback; + +@Component +@Slf4j +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +public class DvhMeldingKafkaProdusent { + private final KafkaTemplate dvhMeldingKafkaTemplate; + private final DvhMeldingEntitetRepository repository; + + public DvhMeldingKafkaProdusent(@Autowired @Qualifier("dvhMeldingKafkaTemplate") KafkaTemplate dvhMeldingKafkaTemplate, DvhMeldingEntitetRepository repository) { + this.dvhMeldingKafkaTemplate = dvhMeldingKafkaTemplate; + this.repository = repository; + } + + @TransactionalEventListener + public void dvhMeldingOpprettet(DvhMeldingOpprettet event) { + String meldingId = event.getAvroTiltakHendelse().getMeldingId(); + String topic = Topics.DVH_MELDING; + dvhMeldingKafkaTemplate.send(topic, meldingId, event.getAvroTiltakHendelse()).addCallback(new ListenableFutureCallback<>() { + @Override + public void onSuccess(SendResult result) { + log.info("DvhMelding med id {} sendt til Kafka topic {}", meldingId, topic); + DvhMeldingEntitet entitet = event.getEntitet(); + entitet.setSendt(true); + repository.save(entitet); + } + + @Override + public void onFailure(Throwable ex) { + log.error("DvhMelding med id {} kunne ikke sendes til Kafka topic {}", meldingId, topic); + } + }); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingOpprettet.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingOpprettet.java new file mode 100644 index 000000000..d0fcac56f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingOpprettet.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.datavarehus; + +import lombok.Value; + +@Value +public class DvhMeldingOpprettet { + DvhMeldingEntitet entitet; + AvroTiltakHendelse avroTiltakHendelse; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingProperties.java new file mode 100644 index 000000000..6d1344943 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhMeldingProperties.java @@ -0,0 +1,17 @@ +package no.nav.tag.tiltaksgjennomforing.datavarehus; + +import lombok.Data; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.EnumSet; +import java.util.UUID; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.dvh-melding") +public class DvhMeldingProperties { + private UUID gruppeTilgang; + private EnumSet tiltakstyper = EnumSet.noneOf(Tiltakstype.class); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhStatusendringJobb.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhStatusendringJobb.java new file mode 100644 index 000000000..90694a5ce --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/DvhStatusendringJobb.java @@ -0,0 +1,51 @@ +package no.nav.tag.tiltaksgjennomforing.datavarehus; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.leader.LeaderPodCheck; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Profile("!local") +@Component +@RequiredArgsConstructor +@EnableScheduling +@Slf4j +public class DvhStatusendringJobb { + private final DvhMeldingEntitetRepository dvhMeldingRepository; + private final AvtaleRepository avtaleRepository; + private final LeaderPodCheck leaderPodCheck; + + @Scheduled(fixedDelayString = "${tiltaksgjennomforing.dvh-melding.fixed-delay}") + public void sjekkOmStatusendring() { + + if (!leaderPodCheck.isLeaderPod()) { + log.info("Pod er ikke leader, så kjører ikke jobb for å finne avtaler med statusendring"); + return; + } + + int antallNyeMeldinger = 0; + for (DvhMeldingEntitet dvhMeldingEntitet : dvhMeldingRepository.findNyesteDvhMeldingForAvtaleSomKanEndreStatus()) { + UUID avtaleId = dvhMeldingEntitet.getAvtaleId(); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(); + + if (avtale.statusSomEnum() != dvhMeldingEntitet.getTiltakStatus()) { + LocalDateTime tidspunkt = Now.localDateTime(); + AvroTiltakHendelse avroTiltakHendelse = AvroTiltakHendelseFabrikk.konstruer(avtale, tidspunkt, UUID.randomUUID(), DvhHendelseType.STATUSENDRING, "system"); + dvhMeldingRepository.save(new DvhMeldingEntitet(UUID.randomUUID(), avtaleId, tidspunkt, avtale.statusSomEnum(), avroTiltakHendelse)); + log.info("Avtale med id {} har byttet status til {}, siste melding har status {}, så sender melding med den nye statusen til datavarehus", avtale.getId(), avtale.statusSomEnum(), dvhMeldingEntitet.getTiltakStatus()); + antallNyeMeldinger++; + } + } + + log.info("Jobb for å finne avtaler med statusendring har kjørt og sendte {} nye meldinger til datavarehus", antallNyeMeldinger); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/InternalDvhMeldingProdusentController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/InternalDvhMeldingProdusentController.java new file mode 100644 index 000000000..5e2b32ebe --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/datavarehus/InternalDvhMeldingProdusentController.java @@ -0,0 +1,61 @@ +package no.nav.tag.tiltaksgjennomforing.datavarehus; + +import lombok.RequiredArgsConstructor; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; +import no.nav.security.token.support.core.api.ProtectedWithClaims; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpClientErrorException; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/utvikler-admin/dvh-melding") +@RequiredArgsConstructor +@ProtectedWithClaims(issuer = "aad") +@Slf4j +public class InternalDvhMeldingProdusentController { + private final AvtaleRepository avtaleRepository; + private final DvhMeldingEntitetRepository dvhMeldingRepository; + private final TokenUtils tokenUtils; + private final DvhMeldingProperties dvhMeldingProperties; + private final DvhAvtalePatchService dvhAvtalePatchService; + + @PostMapping("/patch") + public void patcheAvtale(@RequestBody PatchRequest request) { + log.info("Patcher avtaler til dvh"); + sjekkTilgang(); + avtaleRepository.findAllById(request.avtaleIder()).forEach(avtale -> { + UUID meldingId = UUID.randomUUID(); + String utførtAv = tokenUtils.hentBrukerOgIssuer().map(TokenUtils.BrukerOgIssuer::getBrukerIdent).orElse("patch"); + AvroTiltakHendelse avroTiltakHendelse = AvroTiltakHendelseFabrikk.konstruer(avtale, Now.localDateTime(), meldingId, DvhHendelseType.PATCHING, utførtAv); + dvhMeldingRepository.save(new DvhMeldingEntitet(meldingId, avtale.getId(), Now.localDateTime(), avtale.statusSomEnum(), avroTiltakHendelse)); + log.info("Patchet avtale {}, sendt melding med id {} til datavarehus", avtale.getId(), meldingId); + }); + } + + @PostMapping("patchalleavtaler") + public void patchAlleAvtaler() { + log.info("Patcher alle avtaler til dvh"); + sjekkTilgang(); + dvhAvtalePatchService.lagDvhPatchMeldingForAlleAvtaler(); + } + + private void sjekkTilgang() { + if (!tokenUtils.harAdGruppe(dvhMeldingProperties.getGruppeTilgang())) { + throw new HttpClientErrorException(HttpStatus.FORBIDDEN); + } + } + + private record PatchRequest(List avtaleIder) { + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/dokgen/DokgenProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/dokgen/DokgenProperties.java new file mode 100644 index 000000000..79b6a2b8c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/dokgen/DokgenProperties.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.dokgen; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.net.URI; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.dokgen") +public class DokgenProperties { + private URI uri; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/dokgen/DokgenService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/dokgen/DokgenService.java new file mode 100644 index 000000000..588490c84 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/dokgen/DokgenService.java @@ -0,0 +1,85 @@ +package no.nav.tag.tiltaksgjennomforing.dokgen; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.micrometer.core.instrument.MeterRegistry; +import java.math.BigDecimal; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.journalfoering.AvtaleTilJournalfoeringMapper; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DokgenService { + private static final BigDecimal HUNDRE = new BigDecimal("100"); + + private final DokgenProperties dokgenProperties; + private final MeterRegistry meterRegistry; + + public byte[] avtalePdf(Avtale avtale, Avtalerolle avtalerolle) { + var avtaleTilJournalfoering = AvtaleTilJournalfoeringMapper.tilJournalfoering(avtale.getGjeldendeInnhold(), avtalerolle); + gangOppSatserMed100(avtaleTilJournalfoering); + fjernGodkjentPåVegneAv(avtaleTilJournalfoering); + try { + byte[] bytes = restOperations().postForObject(dokgenProperties.getUri(), avtaleTilJournalfoering, byte[].class); + meterRegistry.counter("tiltaksgjennomforing.pdf.ok").increment(); + return bytes; + } catch (RestClientException e) { + log.error("Feil ved kall til dokgen for henting av PDF", e); + meterRegistry.counter("tiltaksgjennomforing.pdf.feil").increment(); + throw e; + } + } + + // TODO: Det bør heller ganges med 100 fra starten av i AvtaleTilJournalfoering. + // Slik som det er nå så gjøres det ganging med 100 både her og i tiltaksgjennomforing-prosess. + // Endring på dette krever en synkronisert fiks både her og i tiltaksgjennomforing-prosess. + private void gangOppSatserMed100(no.nav.tag.tiltaksgjennomforing.journalfoering.AvtaleTilJournalfoering avtaleTilJournalfoering) { + if (avtaleTilJournalfoering.getArbeidsgiveravgift() != null) { + avtaleTilJournalfoering.setArbeidsgiveravgift(avtaleTilJournalfoering.getArbeidsgiveravgift().multiply(HUNDRE)); + } + if (avtaleTilJournalfoering.getFeriepengesats() != null) { + avtaleTilJournalfoering.setFeriepengesats(avtaleTilJournalfoering.getFeriepengesats().multiply(HUNDRE)); + } + if (avtaleTilJournalfoering.getOtpSats() != null) { + avtaleTilJournalfoering.setOtpSats(avtaleTilJournalfoering.getOtpSats() * 100); + } + } + + private void fjernGodkjentPåVegneAv(no.nav.tag.tiltaksgjennomforing.journalfoering.AvtaleTilJournalfoering avtaleTilJournalfoering) { + avtaleTilJournalfoering.setGodkjentPaVegneAv(false); + avtaleTilJournalfoering.setGodkjentPaVegneGrunn(null); + } + + // Lager ny instans av RestOperations i stedet for å wire inn RestTemplate fordi det var vanskelig å få den til å bruke en ObjectMapper som hadde datoer på format 'yyyy-MM-dd' i stedet for et array + private RestOperations restOperations() { + RestTemplate rest = new RestTemplate(); + //this is crucial! + rest.getMessageConverters().add(0, mappingJacksonHttpMessageConverter()); + return rest; + } + + private MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() { + var converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper()); + return converter; + } + + private ObjectMapper objectMapper() { + var mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + return mapper; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Formidlingsgruppe.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Formidlingsgruppe.java new file mode 100644 index 000000000..304fa2bcf --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Formidlingsgruppe.java @@ -0,0 +1,28 @@ +package no.nav.tag.tiltaksgjennomforing.enhet; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum Formidlingsgruppe { + ARBEIDSSOKER("ARBS"), // Person er tilgjengelig for alt søk etter arbeidskraft, ordinær og vikar + IKKE_ARBEIDSSOKER("IARBS"), // Person er ikke tilgjengelig for søk etter arbeidskraft + INAKTIVERT_JOBBSKIFTER("IJOBS"), // Jobbskifter som er inaktivert fra nav.no + IKKE_SERVICEBEHOV("ISERV"), // Inaktivering, person mottar ikke bistand fra NAV + FRA_NAV_NO("JOBBS"), // Personen er ikke tilgjengelig for søk + PRE_ARBEIDSSOKER("PARBS"), // Personen fra nav.no som ønsker å bli arbeidssøker, men som enda ikke er verifisert + PRE_REAKTIVERT_ARBEIDSSOKER("RARBS"); //Person som er reaktivert fra nav.no + private final String formidlingskode; + + Formidlingsgruppe(String formidlingskode) { this.formidlingskode = formidlingskode; } + + @JsonValue + public String getKode() { + return formidlingskode; + } + + public static boolean ugyldigFormidlingsgruppe(Formidlingsgruppe formidlingsgruppe) { + return switch (formidlingsgruppe) { + case IKKE_ARBEIDSSOKER, INAKTIVERT_JOBBSKIFTER, IKKE_SERVICEBEHOV -> true; + case ARBEIDSSOKER, FRA_NAV_NO, PRE_ARBEIDSSOKER, PRE_REAKTIVERT_ARBEIDSSOKER -> false; + }; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Kvalifiseringsgruppe.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Kvalifiseringsgruppe.java new file mode 100644 index 000000000..ff844cb0d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Kvalifiseringsgruppe.java @@ -0,0 +1,63 @@ +package no.nav.tag.tiltaksgjennomforing.enhet; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public enum Kvalifiseringsgruppe { + SPESIELT_TILPASSET_INNSATS("BATT"), // Personen har nedsatt arbeidsevne og har et identifisert behov for kvalifisering og/eller tilrettelegging. Aktivitetsplan skal utformes. + SITUASJONSBESTEMT_INNSATS("BFORM"), // Personen har moderat bistandsbehov + VARIG_TILPASSET_INNSATS("VARIG"), // Personen har varig nedsatt arbeidsevne + BEHOV_FOR_ARBEIDSEVNEVURDERING("BKART"), // Personen har behov for arbeidsevnevurdering + STANDARD_INNSATS("IKVAL"), // Personen har behov for ordinær bistand + IKKE_VURDERT("IVURD"), // Ikke vurdert + RETTIGHETER_ETTER_FTRL_KAP11("KAP11"), // Rettigheter etter Ftrl. Kapittel 11 + HELSERELATERT_ARBEIDSRETTET_OPPFOLGING_I_NAV("OPPFI"), // Helserelatert arbeidsrettet oppfølging i NAV + SYKMELDT_OPPFOLGING_PA_ARBEIDSPLASSEN("VURDI"), // Sykmeldt, oppfølging på arbeidsplassen + SYKMELDT_UTEN_ARBEIDSGIVER("VURDU"); // Sykmeldt uten arbeidsgiver + + + private final String kvalifiseringskode; + + Kvalifiseringsgruppe(String kvalifiseringskode) { + this.kvalifiseringskode = kvalifiseringskode; + } + + @JsonValue + public String getKvalifiseringskode() { + return kvalifiseringskode; + } + + public static boolean ugyldigKvalifiseringsgruppe(Kvalifiseringsgruppe kvalifiseringsgruppe) { + return switch (kvalifiseringsgruppe) { + case STANDARD_INNSATS, BEHOV_FOR_ARBEIDSEVNEVURDERING, IKKE_VURDERT -> true; + case RETTIGHETER_ETTER_FTRL_KAP11, HELSERELATERT_ARBEIDSRETTET_OPPFOLGING_I_NAV, + SYKMELDT_OPPFOLGING_PA_ARBEIDSPLASSEN, SYKMELDT_UTEN_ARBEIDSGIVER, + SPESIELT_TILPASSET_INNSATS, SITUASJONSBESTEMT_INNSATS, VARIG_TILPASSET_INNSATS -> false; + }; + } + + public static boolean kvalifisererTilMidlertidiglonnstilskuddOgSommerjobbOgMentor(Kvalifiseringsgruppe kvalifiseringsgruppe) { + return switch (kvalifiseringsgruppe) { + case SPESIELT_TILPASSET_INNSATS, SITUASJONSBESTEMT_INNSATS, VARIG_TILPASSET_INNSATS -> true; + default -> false; + }; + } + + public static boolean kvalifisererTilVariglonnstilskudd(Kvalifiseringsgruppe kvalifiseringsgruppe) { + return kvalifiseringsgruppe == VARIG_TILPASSET_INNSATS; + } + + public Integer finnLonntilskuddProsentsatsUtifraKvalifiseringsgruppe(Integer prosentsatsLiten, Integer prosentsatsStor) { + switch (this) { + case SPESIELT_TILPASSET_INNSATS, VARIG_TILPASSET_INNSATS: + return prosentsatsStor; + case SITUASJONSBESTEMT_INNSATS: + return prosentsatsLiten; + default: { + log.warn("feilet med setting av kvalifiseringsgruppe. Kvalifiseringsgruppe: {}", this); + return null; + } + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2Client.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2Client.java new file mode 100644 index 000000000..5b9659a4b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2Client.java @@ -0,0 +1,59 @@ +package no.nav.tag.tiltaksgjennomforing.enhet; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.cache.EhCacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.Objects; + +@Slf4j +@Component +@AllArgsConstructor +public class Norg2Client { + + private final Norg2GeografiskProperties norg2GeografiskProperties; + private final Norg2OppfølgingProperties norg2OppfølgingProperties; + private final RestTemplate restTemplate; + + @Cacheable(EhCacheConfig.NORGNAVN_CACHE) + public Norg2OppfølgingResponse hentOppfølgingsEnhetsnavnFraCacheNorg2(String enhet) { + return this.hentOppfølgingsEnhetsnavn(enhet); + } + + @Cacheable(EhCacheConfig.NORG_GEO_ENHET) + public Norg2GeoResponse hentGeoEnhetFraCacheEllerNorg2(String geoTilknytning) { + return this.hentGeografiskEnhet(geoTilknytning); + } + + + public Norg2GeoResponse hentGeografiskEnhet(String geoOmråde) { + Norg2GeoResponse norg2GeoResponse; + try { + norg2GeoResponse = restTemplate.getForObject(norg2GeografiskProperties.getUrl() + geoOmråde, Norg2GeoResponse.class); + if (norg2GeoResponse.getEnhetNr() == null) { + log.warn("Fant ikke enhet med geoOmråde {}", geoOmråde); + } + } catch (Exception e) { + log.error("Feil v/oppslag på geoOmråde {}", geoOmråde); + throw e; + } + return norg2GeoResponse; + } + + public Norg2OppfølgingResponse hentOppfølgingsEnhetsnavn(String enhet) { + Norg2OppfølgingResponse norg2OppfølgingResponse = null; + try { + norg2OppfølgingResponse = restTemplate.getForObject(norg2OppfølgingProperties.getUrl() + enhet, Norg2OppfølgingResponse.class); + if (Objects.requireNonNull(norg2OppfølgingResponse).getNavn() == null) { + log.warn("Fant ingen navn til enhet: {}", enhet); + } + }catch (Exception e) { + log.error("Feil v/oppslag på enhet {}", enhet); + } + return norg2OppfølgingResponse; + } +} + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2GeoResponse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2GeoResponse.java new file mode 100644 index 000000000..8d8281359 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2GeoResponse.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.enhet; + +import lombok.Value; + +@Value +public class Norg2GeoResponse { + String navn; + String enhetNr; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2GeografiskProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2GeografiskProperties.java new file mode 100644 index 000000000..f96026a04 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2GeografiskProperties.java @@ -0,0 +1,12 @@ +package no.nav.tag.tiltaksgjennomforing.enhet; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "tiltaksgjennomforing.norg2.geografisk") +public class Norg2GeografiskProperties { + private String url; +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2Oppf\303\270lgingProperties.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2Oppf\303\270lgingProperties.java" new file mode 100644 index 000000000..619145951 --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2Oppf\303\270lgingProperties.java" @@ -0,0 +1,13 @@ +package no.nav.tag.tiltaksgjennomforing.enhet; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "tiltaksgjennomforing.norg2.enhet") +public class Norg2OppfølgingProperties { + private String url; +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2Oppf\303\270lgingResponse.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2Oppf\303\270lgingResponse.java" new file mode 100644 index 000000000..eff8db600 --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Norg2Oppf\303\270lgingResponse.java" @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.enhet; + +import lombok.Value; + +@Value +public class Norg2OppfølgingResponse { + Integer enhetId; + String enhetNr; + String navn; +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Oppf\303\270lgingsstatus.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Oppf\303\270lgingsstatus.java" new file mode 100644 index 000000000..659d0500e --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/Oppf\303\270lgingsstatus.java" @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.enhet; + +import lombok.Value; + +@Value +public class Oppfølgingsstatus { + Formidlingsgruppe formidlingsgruppe; + Kvalifiseringsgruppe kvalifiseringsgruppe; + String oppfolgingsenhet; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/VeilarbArenaClient.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/VeilarbArenaClient.java new file mode 100644 index 000000000..5681d62b0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/VeilarbArenaClient.java @@ -0,0 +1,120 @@ +package no.nav.tag.tiltaksgjennomforing.enhet; + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.cache.EhCacheConfig; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientResponseException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +@Slf4j +@Service +public class VeilarbArenaClient { + + private final RestTemplate restTemplate; + private final VeilarbArenaProperties veilarbArenaProperties; + + public VeilarbArenaClient( + @Qualifier("veilarbarenaRestTemplate") RestTemplate restTemplate, + VeilarbArenaProperties veilarbArenaProperties + ) { + this.restTemplate = restTemplate; + this.veilarbArenaProperties = veilarbArenaProperties; + } + + private boolean erMidlerTidiglonnstilskuddEllerSommerjobbEllerMentor(Tiltakstype tiltakstype) { + return (tiltakstype == Tiltakstype.SOMMERJOBB || + tiltakstype == Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD) || + tiltakstype == Tiltakstype.MENTOR; + } + + private boolean erVariglonnstilskudd(Tiltakstype tiltakstype) { + return tiltakstype.equals(Tiltakstype.VARIG_LONNSTILSKUDD); + } + + public Oppfølgingsstatus sjekkOgHentOppfølgingStatus(Avtale avtale) { + Oppfølgingsstatus oppfølgingStatus = hentOppfølgingStatus(avtale.getDeltakerFnr().asString()); + if (avtale.getTiltakstype() != Tiltakstype.SOMMERJOBB) { + sjekkStatus(avtale, oppfølgingStatus); + } + return oppfølgingStatus; + } + + public void sjekkOppfølingStatus(Avtale avtale) { + Oppfølgingsstatus oppfølgingStatus = hentOppfølgingStatus(avtale.getDeltakerFnr().asString()); + sjekkStatus(avtale, oppfølgingStatus); + } + + private void sjekkStatus(Avtale avtale, Oppfølgingsstatus oppfølgingStatus) { + if ( + oppfølgingStatus == null || + oppfølgingStatus.getFormidlingsgruppe() == null || + oppfølgingStatus.getKvalifiseringsgruppe() == null + ) { + throw new FeilkodeException(Feilkode.HENTING_AV_INNSATS_BEHOV_FEILET); + } + + if (Kvalifiseringsgruppe.ugyldigKvalifiseringsgruppe(oppfølgingStatus.getKvalifiseringsgruppe())) { + throw new FeilkodeException(Feilkode.KVALIFISERINGSGRUPPE_IKKE_RETTIGHET); + } + + if (erMidlerTidiglonnstilskuddEllerSommerjobbEllerMentor(avtale.getTiltakstype()) && + !Kvalifiseringsgruppe.kvalifisererTilMidlertidiglonnstilskuddOgSommerjobbOgMentor(oppfølgingStatus.getKvalifiseringsgruppe())) { + throw new FeilkodeException(Feilkode.KVALIFISERINGSGRUPPE_MIDLERTIDIG_LONNTILSKUDD_OG_SOMMERJOBB_FEIL); + } + + if (erVariglonnstilskudd(avtale.getTiltakstype()) && + !Kvalifiseringsgruppe.kvalifisererTilVariglonnstilskudd(oppfølgingStatus.getKvalifiseringsgruppe())) { + throw new FeilkodeException(Feilkode.KVALIFISERINGSGRUPPE_VARIG_LONNTILSKUDD_FEIL); + } + } + + @Cacheable(EhCacheConfig.ARENA_CACHCE) + public Oppfølgingsstatus HentOppfølgingsenhetFraCacheEllerArena(String fnr) { + return this.hentOppfølgingStatus(fnr); + } + + public String hentOppfølgingsEnhet(String fnr) { + Oppfølgingsstatus oppfølgingsstatus = hentOppfølgingStatus(fnr); + if (oppfølgingsstatus != null) { + return oppfølgingsstatus.getOppfolgingsenhet(); + } + return null; + } + + public Oppfølgingsstatus hentOppfølgingStatus(String fnr) { + String uri = UriComponentsBuilder.fromHttpUrl(veilarbArenaProperties.getUrl().toString()) + .queryParam("fnr", fnr).toUriString(); + try { + ResponseEntity respons = restTemplate.exchange( + uri, + HttpMethod.GET, + httpHeadere(), + Oppfølgingsstatus.class + ); + return respons.getBody(); + } catch (RestClientResponseException exception) { + if (exception.getRawStatusCode() == HttpStatus.NOT_FOUND.value() && + !exception.getResponseBodyAsString().isEmpty()) { + log.warn("Kandidat ikke registrert i veilarbarena"); + return null; + } + log.error("Kunne ikke hente Oppfølgingsstatus fra veilarbarena: status=" + + exception.getRawStatusCode(), exception); + return null; + } + } + + private HttpEntity httpHeadere() { + HttpHeaders headers = new HttpHeaders(); + headers.set("Nav-Consumer-Id", veilarbArenaProperties.getNavConsumerId()); + return new HttpEntity<>(headers); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/VeilarbArenaProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/VeilarbArenaProperties.java new file mode 100644 index 000000000..486fe6a87 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/enhet/VeilarbArenaProperties.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.enhet; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.net.URI; + +@Data +@Configuration +@ConfigurationProperties(prefix = "tiltaksgjennomforing.veilarbarena") +public class VeilarbArenaProperties { + private URI url; + private String navConsumerId; +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/AltM\303\245V\303\246reFyltUtException.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/AltM\303\245V\303\246reFyltUtException.java" new file mode 100644 index 000000000..1c27c0b86 --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/AltM\303\245V\303\246reFyltUtException.java" @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class AltMåVæreFyltUtException extends FeilkodeException { + + public AltMåVæreFyltUtException() { + super(Feilkode.ALT_MA_VAERE_FYLT_UT); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/AltinnFeilException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/AltinnFeilException.java new file mode 100644 index 000000000..273de2ddc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/AltinnFeilException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class AltinnFeilException extends FeilkodeException { + public AltinnFeilException() { + super(Feilkode.ALTINN_FEIL); + } +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/ArbeidsgiverSkalGodkjenneF\303\270rVeilederException.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/ArbeidsgiverSkalGodkjenneF\303\270rVeilederException.java" new file mode 100644 index 000000000..dae3c296e --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/ArbeidsgiverSkalGodkjenneF\303\270rVeilederException.java" @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class ArbeidsgiverSkalGodkjenneFørVeilederException extends FeilkodeException { + public ArbeidsgiverSkalGodkjenneFørVeilederException() { + super(Feilkode.ARBEIDSGIVER_SKAL_GODKJENNE_FOER_VEILEDER); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/AvtaleErIkkeFordeltException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/AvtaleErIkkeFordeltException.java new file mode 100644 index 000000000..e74c4cf1e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/AvtaleErIkkeFordeltException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class AvtaleErIkkeFordeltException extends FeilkodeException { + public AvtaleErIkkeFordeltException() { + super(Feilkode.IKKE_FORDELT); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/DeltakerHarGodkjentException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/DeltakerHarGodkjentException.java new file mode 100644 index 000000000..116312dbc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/DeltakerHarGodkjentException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class DeltakerHarGodkjentException extends FeilkodeException { + public DeltakerHarGodkjentException() { + super(Feilkode.DELTAKER_HAR_GODKJENT); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/ErAlleredeVeilederException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/ErAlleredeVeilederException.java new file mode 100644 index 000000000..a8e6a0551 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/ErAlleredeVeilederException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class ErAlleredeVeilederException extends FeilkodeException { + public ErAlleredeVeilederException() { + super(Feilkode.ER_ALLEREDE_VEILEDER); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/FeilLonnstilskuddsprosentException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/FeilLonnstilskuddsprosentException.java new file mode 100644 index 000000000..14f01efed --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/FeilLonnstilskuddsprosentException.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class FeilLonnstilskuddsprosentException extends FeilkodeException { + + public FeilLonnstilskuddsprosentException() { + super(Feilkode.LONNSTILSKUDD_PROSENT_ER_UGYLDIG); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/Feilkode.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/Feilkode.java new file mode 100644 index 000000000..876d758ec --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/Feilkode.java @@ -0,0 +1,120 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public enum Feilkode { + ALT_MA_VAERE_FYLT_UT, + ARBEIDSGIVER_SKAL_GODKJENNE_FOER_VEILEDER, + DELTAKER_SKAL_GODKJENNE_FOER_VEILEDER, + DELTAKER_HAR_GODKJENT, + ARBEIDSGIVER_HAR_GODKJENT, + ER_ALLEREDE_VEILEDER, + GODKJENT_PAA_VEGNE_GRUNN_MAA_VELGES, + GRUNN_TIL_AVBRYTELSE, + IKKE_VALGT_PART, + KAN_IKKE_ENDRE, + KAN_IKKE_LAASES_OPP, + KAN_IKKE_OPPHEVE, + KAN_IKKE_OPPHEVE_GODKJENNINGER_VED_INNGAATT_AVTALE, + SAMTIDIGE_ENDRINGER, + START_ETTER_SLUTT, + VARIGHET_DATO_TILBAKE_I_TID, + UGYLDIG_TLF, + IKKE_FORDELT, + VARIGHET_FOR_LANG_MIDLERTIDIG_LONNSTILSKUDD_12_MND, + VARIGHET_FOR_LANG_MIDLERTIDIG_LONNSTILSKUDD_24_MND, + VARIGHET_FOR_LANG_ARBEIDSTRENING, + VARIGHET_FOR_LANG_INKLUDERINGSTILSKUDD, + VEILEDER_SKAL_GODKJENNE_SIST, + ALTINN_FEIL, + NAV_ENHET_IKKE_FUNNET, + GOSYS_FEIL, + ENHET_ER_JURIDISK, + ENHET_ER_ORGLEDD, + ENHET_FINNES_IKKE, + IKKE_TILGANG_TIL_DELTAKER, + IKKE_TILGANG_TIL_AVTALE, + KAN_IKKE_GODKJENNE_AVTALE_KODE6, + KAN_IKKE_OPPRETTE_AVTALE_KODE6, + TILSKUDDSPERIODE_ER_IKKE_SATT, + TILSKUDDSPERIODE_ER_ALLEREDE_BEHANDLET, + TILSKUDDSPERIODE_KAN_KUN_BEHANDLES_VED_INNGAATT_AVTALE, + TILSKUDDSPERIODE_BEHANDLE_FOR_TIDLIG, + TILSKUDDSPERIODE_AVSLAGSFORKLARING_PAAKREVD, + TILSKUDDSPERIODE_INGEN_AVSLAGSAARSAKER, + TILSKUDDSPERIODE_ENHET_FIRE_SIFFER, + TILSKUDDSPERIODE_IKKE_GODKJENNE_EGNE, + UGYLDIG_SORTERINGSKOLONNE, + LONNSTILSKUDD_PROSENT_ER_UGYLDIG, + KONTOREGISTER_FEIL, + IKKE_ADMIN_TILGANG, + KONTOREGISTER_FEIL_BEDRIFT_IKKE_FUNNET, + KAN_IKKE_FORLENGE_FEIL_SLUTTDATO, + KAN_IKKE_FORLENGE_IKKE_GODKJENT_AVTALE, + KAN_IKKE_ENDRE_OKONOMI_IKKE_GODKJENT_AVTALE, + KAN_IKKE_ENDRE_OKONOMI_UGYLDIG_INPUT, + KAN_IKKE_LASTE_NED_PDF, + SOMMERJOBB_FOR_TIDLIG, + SOMMERJOBB_FOR_SENT, + SOMMERJOBB_FOR_LANG_VARIGHET, + SOMMERJOBB_IKKE_GAMMEL_NOK, + SOMMERJOBB_FOR_GAMMEL, + SOMMERJOBB_FOR_GAMMEL_FRA_OPPSTARTDATO, + DELTAKER_67_AAR, + FEIL_OTP_SATS, + KAN_IKKE_AVBRYTES_ALLEREDE_AVBRUTT, + KAN_IKKE_ANNULLERES_ALLEREDE_ANNULLERT, + KAN_IKKE_AVBRYTES_MÅ_FORKORTES, + KAN_IKKE_FORKORTE_ETTER_SLUTTDATO, + KAN_IKKE_FORKORTE_IKKE_GODKJENT_AVTALE, + KAN_IKKE_FORKORTE_FOR_STARTDATO, + KAN_IKKE_FORKORTE_GRUNN_MANGLER, + KAN_IKKE_ENDRE_KONTAKTINFO_GRUNN_MANGLER, + KAN_IKKE_ENDRE_KONTAKTINFO_GRUNN_IKKE_GODKJENT_AVTALE, + KAN_IKKE_ENDRE_STILLINGSBESKRIVELSE_GRUNN_MANGLER, + KAN_IKKE_ENDRE_STILLINGSBESKRIVELSE_GRUNN_IKKE_GODKJENT_AVTALE, + KAN_IKKE_ENDRE_OPPFØLGING_OG_TILRETTELEGGING_GRUNN_MANGLER, + KAN_IKKE_ENDRE_OPPFØLGING_OG_TILRETTELEGGING_GRUNN_IKKE_GODKJENT_AVTALE, + KAN_IKKE_ENDRE_MAAL_IKKE_INNGAATT_AVTALE, + KAN_IKKE_ENDRE_MAAL_TOM_LISTE, + KAN_IKKE_ENDRE_MAAL_IKKE_BESKRIVELSE_ELLER_KATEGORI, + KAN_IKKE_ENDRE_INKLUDERINGSTILSKUDD_IKKE_INNGAATT_AVTALE, + KAN_IKKE_ENDRE_INKLUDERINGSTILSKUDD_IKKE_BELOP_ELLER_TYPE, + KAN_IKKE_ENDRE_INKLUDERINGSTILSKUDD_TOM_LISTE, + KAN_IKKE_ENDRE_FEIL_TILTAKSTYPE, + MANGLER_AD_GRUPPE_BESLUTTER, + GODKJENN_PAA_VEGNE_AV_FEIL_TILTAKSTYPE, + KAN_IKKE_ENDRE_ANNULLERT_AVTALE, + KAN_IKKE_OPPDATERE_KOSTNADSSTED_INGAATT_AVTALE, + ARBEIDSGIVER_NOTIFKASJON_KALLET_FEILET, + KVALIFISERINGSGRUPPE_MIDLERTIDIG_LONNTILSKUDD_OG_SOMMERJOBB_FEIL, + KVALIFISERINGSGRUPPE_VARIG_LONNTILSKUDD_FEIL, + KVALIFISERINGSGRUPPE_IKKE_RETTIGHET, + FORMIDLINGSGRUPPE_IKKE_RETTIGHET, + HENTING_AV_INNSATS_BEHOV_FEILET, + KAN_IKKE_REBEREGNE, + KAN_IKKE_MERKES_FOR_ETTERREGISTRERING_AVTALE_GODKJENT, + FINNER_IKKE_AVTALE_PÅ_AVTALENUMMER, + FORTIDLIG_STARTDATO, + KAN_IKKE_GODKJENNE_DELTAKER_HAR_ALLEREDE_GODKJENT, + KAN_IKKE_GODKJENNE_MENTOR_HAR_ALLEREDE_GODKJENT, + KAN_IKKE_GODKJENNE_ARBEIDSGIVER_HAR_ALLEREDE_GODKJENT, + KAN_IKKE_GODKJENNE_VEILEDER_HAR_ALLEREDE_GODKJENT, + MANGLER_BEREGNING, + MANGLER_VEILEDER_PÅ_AVTALE, + AVTALE_INNEHOLDER_UTBETALT_TILSKUDDSPERIODE, + UGYLDIG_KOMBINASJON_AV_ISSUER_OG_ROLLE, + INKLUDERINGSTILSKUDD_SUM_FOR_HØY, + MENTOR_MÅ_SIGNERE_TAUSHETSERKLÆRING, + DELTAGER_OG_MENTOR_KAN_IKKE_HA_SAMME_FØDSELSNUMMER, + KAN_IKKE_ENDRE_OM_MENTOR_IKKE_INNGAATT_AVTALE, + VARIGHET_FOR_LANG_MENTOR_36_MND, + VARIGHET_FOR_LANG_MENTOR_6_MND, + KAN_IKKE_ENDRE_OM_MENTOR_UGYLDIG_INPUT, + DELTAKER_72_AAR, + AVTALE_INNEHOLDER_TILSKUDDSPERIODE_MED_GODKJENT_REFUSJON, + KAN_IKKE_LAGE_NYE_TILSKUDDSPRIODER_INNGAATT_AVTALE, + SLUTTDATO_GRENSE_NÅDD, + VARIG_LONNSTILSKUDD_TILSKUDDSPERIODE_MIDLERTIDIG_AVSKURDD, + FORLENG_MIDLERTIDIG_IKKE_TILGJENGELIG, + KAN_IKKE_ENDRE_ARENA_MIGRERINGSDATO_INNGAATT_AVTALE, + KAN_IKKE_FORKORTE_FOR_UTBETALT_TILSKUDDSPERIODE +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/FeilkodeException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/FeilkodeException.java new file mode 100644 index 000000000..a494e43c2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/FeilkodeException.java @@ -0,0 +1,18 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class FeilkodeException extends RuntimeException { + private final Feilkode feilkode; + + public FeilkodeException(Feilkode feilkode) { + this.feilkode = feilkode; + } + + public Feilkode getFeilkode() { + return feilkode; + } + + @Override + public String getMessage() { + return feilkode.name(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/FeilkodeExceptionHandler.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/FeilkodeExceptionHandler.java new file mode 100644 index 000000000..5b6454963 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/FeilkodeExceptionHandler.java @@ -0,0 +1,23 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@Slf4j +@ControllerAdvice +public class FeilkodeExceptionHandler extends ResponseEntityExceptionHandler { + private static final String FEILKODE = "feilkode"; + + @ExceptionHandler({ FeilkodeException.class }) + public ResponseEntity feilkodeException(FeilkodeException e) { + log.info("Feilkode inntruffet: {}", e.getFeilkode()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .header(FEILKODE, e.getFeilkode().name()) + .build(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/GosysFeilException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/GosysFeilException.java new file mode 100644 index 000000000..f441030d8 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/GosysFeilException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class GosysFeilException extends FeilkodeException { + public GosysFeilException() { + super(Feilkode.GOSYS_FEIL); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/IkkeAdminTilgangException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/IkkeAdminTilgangException.java new file mode 100644 index 000000000..5fe9ad978 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/IkkeAdminTilgangException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class IkkeAdminTilgangException extends FeilkodeException{ + public IkkeAdminTilgangException() { + super(Feilkode.IKKE_ADMIN_TILGANG); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/IkkeTilgangTilDeltakerException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/IkkeTilgangTilDeltakerException.java new file mode 100644 index 000000000..ea9e3e0a1 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/IkkeTilgangTilDeltakerException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class IkkeTilgangTilDeltakerException extends FeilkodeException { + public IkkeTilgangTilDeltakerException() { + super(Feilkode.IKKE_TILGANG_TIL_DELTAKER); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/IkkeValgtPartException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/IkkeValgtPartException.java new file mode 100644 index 000000000..71a02005e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/IkkeValgtPartException.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class IkkeValgtPartException extends FeilkodeException { + + + public IkkeValgtPartException() { + super(Feilkode.IKKE_VALGT_PART); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KanIkkeEndreException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KanIkkeEndreException.java new file mode 100644 index 000000000..70526d427 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KanIkkeEndreException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class KanIkkeEndreException extends FeilkodeException { + public KanIkkeEndreException() { + super(Feilkode.KAN_IKKE_ENDRE); + } +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KanIkkeGodkjenneAvtaleP\303\245Kode6Exception.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KanIkkeGodkjenneAvtaleP\303\245Kode6Exception.java" new file mode 100644 index 000000000..ef0d42ef7 --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KanIkkeGodkjenneAvtaleP\303\245Kode6Exception.java" @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class KanIkkeGodkjenneAvtalePåKode6Exception extends FeilkodeException { + public KanIkkeGodkjenneAvtalePåKode6Exception() { + super(Feilkode.IKKE_TILGANG_TIL_DELTAKER); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KanIkkeOppheveException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KanIkkeOppheveException.java new file mode 100644 index 000000000..8e4245332 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KanIkkeOppheveException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class KanIkkeOppheveException extends FeilkodeException { + public KanIkkeOppheveException() { + super(Feilkode.KAN_IKKE_OPPHEVE); + } +} diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KanIkkeOppretteAvtaleP\303\245Kode6Exception.java" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KanIkkeOppretteAvtaleP\303\245Kode6Exception.java" new file mode 100644 index 000000000..b0c9284d5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KanIkkeOppretteAvtaleP\303\245Kode6Exception.java" @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class KanIkkeOppretteAvtalePåKode6Exception extends FeilkodeException { + public KanIkkeOppretteAvtalePåKode6Exception() { + super(Feilkode.IKKE_TILGANG_TIL_DELTAKER); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KontoregisterFantIkkeBedriftFeilException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KontoregisterFantIkkeBedriftFeilException.java new file mode 100644 index 000000000..2df3a1847 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KontoregisterFantIkkeBedriftFeilException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class KontoregisterFantIkkeBedriftFeilException extends FeilkodeException { + public KontoregisterFantIkkeBedriftFeilException() { + super(Feilkode.KONTOREGISTER_FEIL_BEDRIFT_IKKE_FUNNET); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KontoregisterFeilException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KontoregisterFeilException.java new file mode 100644 index 000000000..799886854 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/KontoregisterFeilException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class KontoregisterFeilException extends FeilkodeException { + public KontoregisterFeilException() { + super(Feilkode.KONTOREGISTER_FEIL); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/NavEnhetIkkeFunnetException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/NavEnhetIkkeFunnetException.java new file mode 100644 index 000000000..e1dca9b05 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/NavEnhetIkkeFunnetException.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class NavEnhetIkkeFunnetException extends FeilkodeException { + + public NavEnhetIkkeFunnetException() { + super(Feilkode.NAV_ENHET_IKKE_FUNNET); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/RessursFinnesIkkeException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/RessursFinnesIkkeException.java new file mode 100644 index 000000000..9718fd1ec --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/RessursFinnesIkkeException.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class RessursFinnesIkkeException extends RuntimeException { +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/SamtidigeEndringerException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/SamtidigeEndringerException.java new file mode 100644 index 000000000..6bd66885b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/SamtidigeEndringerException.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class SamtidigeEndringerException extends FeilkodeException { + + public SamtidigeEndringerException() { + super(Feilkode.SAMTIDIGE_ENDRINGER); + } + + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/StartDatoErEtterSluttDatoException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/StartDatoErEtterSluttDatoException.java new file mode 100644 index 000000000..ffd048805 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/StartDatoErEtterSluttDatoException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class StartDatoErEtterSluttDatoException extends FeilkodeException { + public StartDatoErEtterSluttDatoException() { + super(Feilkode.START_ETTER_SLUTT); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/TilgangskontrollException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/TilgangskontrollException.java new file mode 100644 index 000000000..0f8435eed --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/TilgangskontrollException.java @@ -0,0 +1,17 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.FORBIDDEN) +@Deprecated(since = "Bør ikke bruke denne exception lenger fordi det er vanskelig å se i browser hvilken exception som kastes. Det er kun feilkode 403 som vises, og message vises ikke noe sted. Bruk i stedet FeilkodeException.") +public class TilgangskontrollException extends RuntimeException { + + public TilgangskontrollException(String message) { + super(message); + } + + public TilgangskontrollException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/TiltaksgjennomforingException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/TiltaksgjennomforingException.java new file mode 100644 index 000000000..9db803b7f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/TiltaksgjennomforingException.java @@ -0,0 +1,12 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class TiltaksgjennomforingException extends RuntimeException { + + public TiltaksgjennomforingException(String message) { + super(message); + } + + public TiltaksgjennomforingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/VarighetDatoErTilbakeITidException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/VarighetDatoErTilbakeITidException.java new file mode 100644 index 000000000..2b27f6348 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/VarighetDatoErTilbakeITidException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class VarighetDatoErTilbakeITidException extends FeilkodeException { + public VarighetDatoErTilbakeITidException() { + super(Feilkode.VARIGHET_DATO_TILBAKE_I_TID); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/VarighetForLangArbeidstreningException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/VarighetForLangArbeidstreningException.java new file mode 100644 index 000000000..ff2bd9841 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/VarighetForLangArbeidstreningException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class VarighetForLangArbeidstreningException extends FeilkodeException { + public VarighetForLangArbeidstreningException() { + super(Feilkode.VARIGHET_FOR_LANG_ARBEIDSTRENING); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/VeilederSkalGodkjenneSistException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/VeilederSkalGodkjenneSistException.java new file mode 100644 index 000000000..8bdd03059 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/exceptions/VeilederSkalGodkjenneSistException.java @@ -0,0 +1,7 @@ +package no.nav.tag.tiltaksgjennomforing.exceptions; + +public class VeilederSkalGodkjenneSistException extends FeilkodeException { + public VeilederSkalGodkjenneSistException() { + super(Feilkode.VEILEDER_SKAL_GODKJENNE_SIST); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/ByEnvironmentStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/ByEnvironmentStrategy.java new file mode 100644 index 000000000..2d455246d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/ByEnvironmentStrategy.java @@ -0,0 +1,39 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles; + + +import io.getunleash.strategy.Strategy; +import no.nav.tag.tiltaksgjennomforing.Miljø; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Optional; + +import static java.util.Arrays.asList; + +@Component +public class ByEnvironmentStrategy implements Strategy { + + private final String environment; + + public ByEnvironmentStrategy(@Value("${MILJO:}") String clusterName) { + this.environment = clusterName.isEmpty() ? Miljø.LOCAL : clusterName; + } + + @Override + public String getName() { + return "byEnvironment"; + } + + @Override + public boolean isEnabled(Map parameters) { + return Optional.ofNullable(parameters) + .map(map -> map.get("miljø")) + .map(env -> asList(env.split(",")).contains(environment)) + .orElse(false); + } + + String getEnvironment() { + return environment; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/ByOrgnummerStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/ByOrgnummerStrategy.java new file mode 100644 index 000000000..011e23141 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/ByOrgnummerStrategy.java @@ -0,0 +1,57 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles; + +import io.getunleash.UnleashContext; +import io.getunleash.strategy.Strategy; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.model.AltinnReportee; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring.AltinnTilgangsstyringService; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +@Data +@Slf4j +@Component +public class ByOrgnummerStrategy implements Strategy { + + static final String UNLEASH_PARAMETER_ORGNUMRE = "orgnumre"; + private final AltinnTilgangsstyringService altinnTilgangsstyringService; + + @Override + public String getName() { + return "byOrgnummer"; + } + + @Override + public boolean isEnabled(Map map) { + return false; + } + + @Override + public boolean isEnabled(Map parameters, UnleashContext unleashContext) { + return unleashContext.getUserId() + .flatMap(currentUserId -> Optional.ofNullable(parameters.get(UNLEASH_PARAMETER_ORGNUMRE)) + .map(enheterOrg -> Set.of(enheterOrg.split(",\\s?"))) + .map(enabledeOrg -> !Collections.disjoint(enabledeOrg, brukersOrganisasjoner(currentUserId)))) + .orElse(false); + } + + private List brukersOrganisasjoner(String currentUserId){ + if (NavIdent.erNavIdent(currentUserId)) { + return List.of(); + } + try { + //TODO: Fungerer pt. ikke. bruker kun dummy hentArbeidsgivrtoken. + Set altinnOrganisasjoner = altinnTilgangsstyringService.hentAltinnOrganisasjoner(new Fnr(currentUserId), () -> ""); + return altinnOrganisasjoner.stream().map(org -> org.getOrganizationNumber()).collect(Collectors.toList()); + }catch (Exception e){ + log.error("Feil ved oppslag på brukers organisasjoner i Altinn: {}", e.getMessage()); + return List.of(); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FakeFakeUnleash.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FakeFakeUnleash.java new file mode 100644 index 000000000..7a0873fdc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FakeFakeUnleash.java @@ -0,0 +1,123 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles; + +import io.getunleash.MoreOperations; +import io.getunleash.Unleash; +import io.getunleash.UnleashContext; +import io.getunleash.Variant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiPredicate; + +public final class FakeFakeUnleash implements Unleash { + private boolean enableAll = false; + private boolean disableAll = false; + private Map features = new HashMap<>(); + private Map variants = new HashMap<>(); + + @Override + public boolean isEnabled(String toggleName) { + return isEnabled(toggleName, false); + } + + @Override + public boolean isEnabled(String toggleName, boolean defaultSetting) { + if (features.containsKey(toggleName)) { + return features.get(toggleName); + } else if (enableAll) { + return true; + } else if (disableAll) { + return false; + } else { + return defaultSetting; + } + } + + @Override + public boolean isEnabled(String toggleName, UnleashContext unleashContext, BiPredicate biPredicate) { + if (features.containsKey(toggleName)) { + return features.get(toggleName); + } else if (enableAll) { + return true; + } else if (disableAll) { + return false; + } else { + return biPredicate.test(toggleName,unleashContext); + } + } + + @Override + public Variant getVariant(String toggleName, UnleashContext context) { + return getVariant(toggleName, Variant.DISABLED_VARIANT); + } + + @Override + public Variant getVariant(String toggleName, UnleashContext unleashContext, Variant defaultValue) { + return getVariant(toggleName, defaultValue); + } + + @Override + public Variant getVariant(String toggleName) { + return getVariant(toggleName, Variant.DISABLED_VARIANT); + } + + @Override + public Variant getVariant(String toggleName, Variant defaultValue) { + if(isEnabled(toggleName) && variants.containsKey(toggleName)) { + return variants.get(toggleName); + } else { + return defaultValue; + } + } + + @Override + public List getFeatureToggleNames() { + return new ArrayList<>(features.keySet()); + } + + @Override + public MoreOperations more() { + return null; + } + + public void enableAll() { + disableAll = false; + enableAll = true; + features.clear(); + } + + public void disableAll() { + disableAll = true; + enableAll = false; + features.clear(); + } + + public void resetAll() { + disableAll = false; + enableAll = false; + features.clear(); + } + + public void enable(String... features) { + for(String name: features) { + this.features.put(name, true); + } + } + + public void disable(String... features) { + for(String name: features) { + this.features.put(name, false); + } + } + + public void reset(String... features) { + for(String name: features) { + this.features.remove(name); + } + } + + public void setVariant(String t1, Variant a) { + variants.put(t1, a); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FeatureToggleConfig.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FeatureToggleConfig.java new file mode 100644 index 000000000..e0e7a2a55 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FeatureToggleConfig.java @@ -0,0 +1,59 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles; + + +import io.getunleash.DefaultUnleash; +import io.getunleash.Unleash; +import io.getunleash.util.UnleashConfig; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.ByEnhetStrategy; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.context.annotation.RequestScope; + +import javax.servlet.http.HttpServletRequest; + + +@Configuration +public class FeatureToggleConfig { + + private static final String APP_NAME = "tiltaksgjennomforing-api"; + + @Bean + @ConditionalOnProperty("tiltaksgjennomforing.unleash.enabled") + public Unleash initializeUnleash( + @Value("${tiltaksgjennomforing.unleash.api-uri}") String unleashUrl, + @Value("${tiltaksgjennomforing.unleash.api-token}") String apiKey, + ByEnvironmentStrategy byEnvironmentStrategy, + ByEnhetStrategy byEnhetStrategy, + ByOrgnummerStrategy byOrgnummerStrategy) { + UnleashConfig config = UnleashConfig.builder() + .appName(APP_NAME) + .instanceId(APP_NAME + "-" + byEnvironmentStrategy.getEnvironment()) + .unleashAPI(unleashUrl) + .apiKey(apiKey) + .build(); + + return new DefaultUnleash( + config, + byEnvironmentStrategy, + byEnhetStrategy, + byOrgnummerStrategy + ); + } + + @Bean + @ConditionalOnProperty("tiltaksgjennomforing.unleash.mock") + @RequestScope + public Unleash unleashMock(@Autowired HttpServletRequest request) { + FakeFakeUnleash fakeUnleash = new FakeFakeUnleash(); + boolean allEnabled = "enabled".equals(request.getHeader("features")); + if (allEnabled) { + fakeUnleash.enableAll(); + } else { + fakeUnleash.disableAll(); + } + return fakeUnleash; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FeatureToggleController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FeatureToggleController.java new file mode 100644 index 000000000..7485c2513 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FeatureToggleController.java @@ -0,0 +1,34 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles; + +import java.util.List; +import java.util.Map; +import io.getunleash.Variant; +import no.nav.security.token.support.core.api.Unprotected; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Unprotected +@RequestMapping("/feature") +public class FeatureToggleController { + private final FeatureToggleService featureToggleService; + + @Autowired + public FeatureToggleController(FeatureToggleService featureToggleService) { + this.featureToggleService = featureToggleService; + } + + @GetMapping + public Map feature(@RequestParam("feature") List features) { + return featureToggleService.hentFeatureToggles(features); + } + + @GetMapping("/variant") + public Map variant(@RequestParam("feature") List features) { + return featureToggleService.hentVarianter(features); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FeatureToggleService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FeatureToggleService.java new file mode 100644 index 000000000..e9fcde6d3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FeatureToggleService.java @@ -0,0 +1,53 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles; + +import io.getunleash.Unleash; +import io.getunleash.UnleashContext; +import io.getunleash.Variant; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class FeatureToggleService { + + private final Unleash unleash; + private final TokenUtils tokenUtils; + + @Autowired + public FeatureToggleService(Unleash unleash, TokenUtils tokenUtils) { + this.unleash = unleash; + this.tokenUtils = tokenUtils; + } + + public Map hentFeatureToggles(List features) { + + return features.stream().collect(Collectors.toMap( + feature -> feature, + feature -> isEnabled(feature) + )); + } + + public Map hentVarianter(List features) { + + return features.stream().collect(Collectors.toMap( + feature -> feature, + feature -> unleash.getVariant(feature, contextMedInnloggetBruker()) + )); + } + + + public Boolean isEnabled(String feature) { + return unleash.isEnabled(feature, contextMedInnloggetBruker()); + } + + private UnleashContext contextMedInnloggetBruker() { + UnleashContext.Builder builder = UnleashContext.builder(); + tokenUtils.hentBrukerOgIssuer().map(a -> builder.userId(a.getBrukerIdent())); + return builder.build(); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysEnhet.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysEnhet.java new file mode 100644 index 000000000..19b0681a6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysEnhet.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@JsonIgnoreProperties(ignoreUnknown = true) +@Data +public class AxsysEnhet { + private String enhetId; + private String navn; +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysProperties.java new file mode 100644 index 000000000..a68d026b7 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysProperties.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.net.URI; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.axsys") +public class AxsysProperties { + private URI uri; + private String navConsumerId; +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysRespons.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysRespons.java new file mode 100644 index 000000000..453bef9a1 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysRespons.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet; + +import lombok.Data; + +import java.util.List; +import java.util.stream.Collectors; + +@Data +public class AxsysRespons { + private List enheter; + + List tilEnheter() { + return enheter.stream().map(enhet -> new NavEnhet(enhet.getEnhetId(), enhet.getNavn())).collect(Collectors.toList()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysService.java new file mode 100644 index 000000000..4b978faca --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysService.java @@ -0,0 +1,57 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet; + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.CorrelationIdSupplier; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.cache.EhCacheConfig; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.util.List; + +@Component +@Slf4j +public class AxsysService { + private final AxsysProperties axsysProperties; + private final RestTemplate restTemplate; + + public AxsysService(AxsysProperties axsysProperties, RestTemplate restTemplate) { + this.axsysProperties = axsysProperties; + this.restTemplate = restTemplate; + } + + @Cacheable(EhCacheConfig.AXSYS_CACHE) + public List hentEnheterNavAnsattHarTilgangTil(NavIdent ident) { + URI uri = UriComponentsBuilder.fromUri(axsysProperties.getUri()) + .pathSegment(ident.asString()) + .queryParam("inkluderAlleEnheter", "false") + .build() + .toUri(); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Nav-Call-Id", CorrelationIdSupplier.get()); + headers.set("Nav-Consumer-Id", axsysProperties.getNavConsumerId()); + + try { + AxsysRespons respons = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<>(headers), AxsysRespons.class).getBody(); + return respons.tilEnheter(); + } catch (RestClientException exception) { + log.warn("Feil ved henting av enheter for ident " + ident, exception); + throw exception; + } + } + + @CacheEvict(cacheNames= EhCacheConfig.AXSYS_CACHE, allEntries=true) + public void cacheEvict() { + log.info("Tømmer axsys cache for data"); + } + +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/ByEnhetStrategy.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/ByEnhetStrategy.java new file mode 100644 index 000000000..794623049 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/ByEnhetStrategy.java @@ -0,0 +1,48 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet; + +import io.getunleash.UnleashContext; +import io.getunleash.strategy.Strategy; +import lombok.RequiredArgsConstructor; + +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; +import org.springframework.stereotype.Component; + +import java.util.*; + +import static java.util.stream.Collectors.toList; + +@Component +@RequiredArgsConstructor +public class ByEnhetStrategy implements Strategy { + + static final String PARAM = "valgtEnhet"; + private final AxsysService axsysService; + + @Override + public String getName() { + return "byEnhet"; + } + + @Override + public boolean isEnabled(Map parameters) { + return false; + } + + @Override + public boolean isEnabled(Map parameters, UnleashContext unleashContext) { + return unleashContext.getUserId() + .flatMap(currentUserId -> Optional.ofNullable(parameters.get(PARAM)) + .map(enheterString -> Set.of(enheterString.split(",\\s?"))) + .map(enabledeEnheter -> !Collections.disjoint(enabledeEnheter, brukersEnheter(currentUserId)))) + .orElse(false); + } + + private List brukersEnheter(String currentUserId) { + if (!NavIdent.erNavIdent(currentUserId)) { + return List.of(); + } + return axsysService.hentEnheterNavAnsattHarTilgangTil(new NavIdent(currentUserId)).stream() + .map(enhet -> enhet.getVerdi()).collect(toList()); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/NavEnhet.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/NavEnhet.java new file mode 100644 index 000000000..0d8276c27 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/NavEnhet.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet; + +import lombok.Value; + +@Value +public class NavEnhet { + String verdi; + String navn; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/AuditLoggingFilter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/AuditLoggingFilter.java new file mode 100644 index 000000000..cde10ed62 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/AuditLoggingFilter.java @@ -0,0 +1,98 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur; + +import com.jayway.jsonpath.JsonPath; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.auditing.AuditEntry; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.auditing.AuditLogger; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.auditing.EventType; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.jetbrains.annotations.NotNull; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.Objects; + +import static no.nav.tag.tiltaksgjennomforing.ApiBeskriver.API_BESKRIVELSE_ATTRIBUTT; +import static no.nav.tag.tiltaksgjennomforing.infrastruktur.CorrelationIdSupplier.MDC_CORRELATION_ID_KEY; + +/** + * Dette filteret fanger opp alle responser fra APIet. + * Dersom en person (arbeidsgiver, saksbehandler) har gjort et oppslag + * og får returnert en JSON som inneholder "deltakerFnr" så vil dette + * resultere i en audit-hendelse. + */ +@Slf4j +@Component +class AuditLoggingFilter extends OncePerRequestFilter { + private final TokenUtils tokenUtils; + private final AuditLogger auditLogger; + private static final String classname = AuditLoggingFilter.class.getName(); + + public AuditLoggingFilter(TokenUtils tokenUtils, AuditLogger auditLogger) { + this.tokenUtils = tokenUtils; + this.auditLogger = auditLogger; + } + + @Override + protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + var wrapper = new ContentCachingResponseWrapper(response); + filterChain.doFilter(request, wrapper); + String correlationId; + if (request.getAttribute(MDC_CORRELATION_ID_KEY) != null) + correlationId = (String) request.getAttribute(MDC_CORRELATION_ID_KEY); + else correlationId = null; + + if (correlationId == null) { + log.error("{}: feilet pga manglende correlationId.", classname); + } + if (response.getContentType() != null && response.getContentType().startsWith("application/json") && correlationId != null) { + try { + List fnrListe = JsonPath.read(wrapper.getContentInputStream(), "$..deltakerFnr"); + var utførtTid = Now.instant(); + String brukerId = tokenUtils.hentBrukerOgIssuer().map(TokenUtils.BrukerOgIssuer::getBrukerIdent).orElse(null); + var uri = URI.create(request.getRequestURI()); + // Logger kun oppslag dersom en innlogget bruker utførte oppslaget + if (brukerId != null) { + fnrListe.stream().filter(Objects::nonNull).distinct().forEach(deltakerFnr -> { + var apiBeskrivelse = (String) request.getAttribute(API_BESKRIVELSE_ATTRIBUTT); + if (apiBeskrivelse == null) { + log.warn("Manglende @ApiBeskrivelse for api-endepunkt {}", uri); + } + // Ikke logg at en bruker slår opp sin egen informasjon + if (!brukerId.equals(deltakerFnr)) { + var entry = new AuditEntry( + "tiltaksgjennomforing-api", + brukerId, + deltakerFnr, + EventType.READ, + true, + utførtTid, + apiBeskrivelse != null ? apiBeskrivelse + : "Oppslag i løsning for arbeidsmarkedstiltak", + uri, + HttpMethod.valueOf(request.getMethod()), + correlationId + ); + auditLogger.logg(entry); + } + }); + } + } catch (IOException ex) { + log.warn("{}: Klarte ikke dekode responsen. Var det ikke gyldig JSON?", classname); + } catch (Exception ex) { + log.error("{}: Logging feilet", classname, ex); + } + } + wrapper.copyBodyToResponse(); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/CorrelationIdFilter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/CorrelationIdFilter.java new file mode 100644 index 000000000..1c01b2690 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/CorrelationIdFilter.java @@ -0,0 +1,37 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; + +import static no.nav.tag.tiltaksgjennomforing.infrastruktur.CorrelationIdSupplier.MDC_CORRELATION_ID_KEY; + +@Data +@EqualsAndHashCode(callSuper = false) +@Component +public class CorrelationIdFilter extends OncePerRequestFilter { + private static final String HEADER_NAME = "X-Correlation-Id"; + + @Override + protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws ServletException, IOException { + try { + Optional.ofNullable(request.getHeader(HEADER_NAME)) + .filter(StringUtils::isNotBlank) + .ifPresentOrElse(CorrelationIdSupplier::set, CorrelationIdSupplier::generateToken); + request.setAttribute(MDC_CORRELATION_ID_KEY, CorrelationIdSupplier.get()); + response.addHeader(HEADER_NAME, CorrelationIdSupplier.get()); + chain.doFilter(request, response); + } finally { + CorrelationIdSupplier.remove(); + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/CorrelationIdSupplier.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/CorrelationIdSupplier.java new file mode 100644 index 000000000..8b608665c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/CorrelationIdSupplier.java @@ -0,0 +1,29 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur; + +import lombok.experimental.UtilityClass; +import org.slf4j.MDC; +import org.springframework.util.Assert; + +import java.util.UUID; + +@UtilityClass +public class CorrelationIdSupplier { + public static final String MDC_CORRELATION_ID_KEY = "correlationId"; + + public static void generateToken() { + MDC.put(MDC_CORRELATION_ID_KEY, UUID.randomUUID().toString()); + } + + public static void set(String token) { + Assert.hasLength(token, "Token kan ikke være blank"); + MDC.put(MDC_CORRELATION_ID_KEY, token); + } + + public static String get() { + return MDC.get(MDC_CORRELATION_ID_KEY); + } + + public static void remove() { + MDC.remove(MDC_CORRELATION_ID_KEY); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/HealthCheckController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/HealthCheckController.java new file mode 100644 index 000000000..07a9e52bb --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/HealthCheckController.java @@ -0,0 +1,22 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur; + +import no.nav.security.token.support.core.api.Unprotected; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@Unprotected +public class HealthCheckController { + private final JdbcTemplate jdbcTemplate; + + public HealthCheckController(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @GetMapping(value = "/internal/healthcheck") + public String healthcheck() { + return jdbcTemplate.queryForObject("select 'ok'", String.class); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/OpenAPIConfig.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/OpenAPIConfig.java new file mode 100644 index 000000000..9ae1e110a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/OpenAPIConfig.java @@ -0,0 +1,56 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur; + +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenAPIConfig { + + private OpenAPI getInfo(OpenAPI openAPI) { + return openAPI + .info(new Info().title("Tiltaksgjennomføring API") + .license(new License() + .name("MIT License") + .url("https://github.com/navikt/tiltaksgjennomforing-api/blob/master/LICENSE.md")) + ).externalDocs( + new ExternalDocumentation() + .description("Avtaleløsning for arbeidstiltak.") + .url("https://github.com/navikt/tiltaksgjennomforing-api")); + } + + @Bean + public OpenAPI TiltakOpenAPI() { + final String securitySchemeName = "bearerAuth"; + OpenAPI openAPI = new OpenAPI() + .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) + .components( + new Components() + .addSecuritySchemes( + securitySchemeName, new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + return getInfo(openAPI); + } + + @Bean + public GroupedOpenApi publicApi() { + return GroupedOpenApi.builder() + .group("tiltaksgjennomforing-api") + .pathsToMatch("/**") + .addOperationCustomizer((operation, handlerMethod) -> { + operation.addSecurityItem(new SecurityRequirement().addList("bearerAuth")); + return operation; + }) + .addOpenApiCustomiser(this::getInfo).build(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/TimedConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/TimedConfiguration.java new file mode 100644 index 000000000..2664b7b93 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/TimedConfiguration.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur; + +import io.micrometer.core.aop.TimedAspect; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TimedConfiguration { + @Bean + public TimedAspect timedAspect(MeterRegistry registry) { + return new TimedAspect(registry); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditConsoleLogger.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditConsoleLogger.java new file mode 100644 index 000000000..05aabbfdd --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditConsoleLogger.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.auditing; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@ConditionalOnProperty(value = "tiltaksgjennomforing.kafka.enabled", havingValue = "false", matchIfMissing = true) +public class AuditConsoleLogger implements AuditLogger { + @Override + public void logg(AuditEntry event) { + log.info("Audit-event: {}", event); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditEntry.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditEntry.java new file mode 100644 index 000000000..63f33b9d9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditEntry.java @@ -0,0 +1,20 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.auditing; + +import org.springframework.http.HttpMethod; + +import java.net.URI; +import java.time.Instant; + +public record AuditEntry( + String appNavn, + String utførtAv, + String oppslagPå, + EventType eventType, + boolean forespørselTillatt, + Instant oppslagUtførtTid, + String beskrivelse, + URI requestUrl, + HttpMethod requestMethod, + String correlationId +) { +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditKafkaConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditKafkaConfiguration.java new file mode 100644 index 000000000..2744d7b8b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditKafkaConfiguration.java @@ -0,0 +1,60 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.auditing; + +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.config.SslConfigs; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; + +import java.util.HashMap; +import java.util.Map; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Configuration +@Slf4j +@EnableKafka +public class AuditKafkaConfiguration { + + @Value("${no.nav.gcp.kafka.aiven.bootstrap-servers}") + private String gcpBootstrapServers; + @Value("${no.nav.gcp.kafka.aiven.security-protocol}") + private String securityProtocol; + @Value("${no.nav.gcp.kafka.aiven.truststore-path}") + private String sslTruststoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.truststore-password}") + private String sslTruststorePasswordEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-path}") + private String sslKeystoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-password}") + private String sslKeystorePasswordEnvKey; + + private Map producerConfigs() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, gcpBootstrapServers); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol); + + props.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, ""); + props.put(SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG, "jks"); + props.put(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG, "PKCS12"); + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, sslTruststoreLocationEnvKey); + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, sslTruststorePasswordEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, sslKeystoreLocationEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, sslKeystorePasswordEnvKey); + + return props; + } + + @Bean + public KafkaTemplate auditEntryTemplate() { + return new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(producerConfigs())); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditKafkaLogger.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditKafkaLogger.java new file mode 100644 index 000000000..08f73c0da --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditKafkaLogger.java @@ -0,0 +1,50 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.auditing; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; +import org.springframework.stereotype.Component; +import org.springframework.util.concurrent.ListenableFutureCallback; + +@Slf4j +@Component +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +public class AuditKafkaLogger implements AuditLogger { + private final KafkaTemplate auditKafkaTemplate; + private final ObjectMapper mapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + + public AuditKafkaLogger(@Qualifier("auditEntryTemplate") KafkaTemplate kafkaTemplate) { + this.auditKafkaTemplate = kafkaTemplate; + } + + @Override + public void logg(AuditEntry event) { + try { + auditKafkaTemplate.send(Topics.AUDIT_HENDELSE, mapper.writeValueAsString(event)) + .addCallback(new ListenableFutureCallback<>() { + @Override + public void onFailure(@NotNull Throwable ex) { + log.error("Audit-hendelse kunne ikke sendes til Kafka topic {}", Topics.AUDIT_HENDELSE, ex); + } + + @Override + public void onSuccess(SendResult result) { + + } + } + ); + } catch (JsonProcessingException ex) { + log.error("Audit-hendelse kunne ikke serialiseres til Kafkamelding", ex); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditLogger.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditLogger.java new file mode 100644 index 000000000..43e75ea82 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/AuditLogger.java @@ -0,0 +1,5 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.auditing; + +public interface AuditLogger { + void logg(AuditEntry event); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/EventType.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/EventType.java new file mode 100644 index 000000000..364f188b0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/auditing/EventType.java @@ -0,0 +1,5 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.auditing; + +public enum EventType { + CREATE, READ, UPDATE, DELETE +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/CacheDto.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/CacheDto.java new file mode 100644 index 000000000..b28db2e26 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/CacheDto.java @@ -0,0 +1,26 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.cache; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Data +@ConfigurationProperties(prefix = "caches") +public class CacheDto { + + private List ehcaches; + + @Data + public static class Cache { + + private String name; + + private int expiryInMinutes; + + private int maximumSize; + } +} + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/EhCacheConfig.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/EhCacheConfig.java new file mode 100644 index 000000000..a75fb4830 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/EhCacheConfig.java @@ -0,0 +1,59 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.cache; + + +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.config.CacheConfiguration; +import net.sf.ehcache.config.PersistenceConfiguration; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.ehcache.EhCacheCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; + +import static net.sf.ehcache.config.PersistenceConfiguration.Strategy.NONE; +import static net.sf.ehcache.store.MemoryStoreEvictionPolicy.LRU; + +@Configuration +@EnableCaching +public class EhCacheConfig extends CachingConfigurerSupport { + + public final static String ABAC_CACHE = "abac_cache"; + + public static final String AXSYS_CACHE = "axsys_cache"; + + public static final String PDL_CACHE = "pdl_cache"; + + public static final String NORGNAVN_CACHE = "norgnavn_cache"; + + public static final String NORG_GEO_ENHET = "norggeoenhet_cache"; + + public static final String ARENA_CACHCE = "arena_cache"; + + @Bean + public CacheManager ehCacheManager(CacheDto cacheDto) { + net.sf.ehcache.config.Configuration config = new net.sf.ehcache.config.Configuration(); + cacheDto.getEhcaches().forEach(cache -> config.addCache( + setupCache( + cache.getName(), + cache.getMaximumSize(), + Duration.ofMinutes(cache.getExpiryInMinutes()) + ) + )); + return CacheManager.newInstance(config); + } + + @Bean + public EhCacheCacheManager cacheManager(CacheDto cacheDto) { + return new EhCacheCacheManager(ehCacheManager(cacheDto)); + } + + private static CacheConfiguration setupCache(String name, int maxEntriesLocalHeap, Duration duration) { + return new CacheConfiguration(name, maxEntriesLocalHeap) + .memoryStoreEvictionPolicy(LRU) + .timeToIdleSeconds(duration.getSeconds()) + .timeToLiveSeconds(duration.getSeconds()) + .persistence(new PersistenceConfiguration().strategy(NONE)); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/EhCacheListener.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/EhCacheListener.java new file mode 100644 index 000000000..38b6a6f07 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/EhCacheListener.java @@ -0,0 +1,17 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.cache; + + +import lombok.extern.slf4j.Slf4j; +import org.ehcache.event.CacheEvent; +import org.ehcache.event.CacheEventListener; + +@Slf4j +public class EhCacheListener implements CacheEventListener { + + @Override + public void onEvent(CacheEvent cacheEvent) { + log.info("Key: {} | EventType: {} | Old value: {} | New value: {}", + cacheEvent.getKey(), cacheEvent.getType(), cacheEvent.getOldValue(), + cacheEvent.getNewValue()); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/EventAndKeyHashEventLogger.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/EventAndKeyHashEventLogger.java new file mode 100644 index 000000000..49fecdb68 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/EventAndKeyHashEventLogger.java @@ -0,0 +1,19 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.cache; + +import org.ehcache.event.CacheEvent; +import org.ehcache.event.CacheEventListener; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class EventAndKeyHashEventLogger implements CacheEventListener { + + @Override + public void onEvent( + CacheEvent cacheEvent) { + log.debug("Cacheevent: {}, key-hash: {}", cacheEvent.getType(), cacheEvent.getKey().hashCode()); + } + +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/EventTypeEventLogger.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/EventTypeEventLogger.java new file mode 100644 index 000000000..9f0f5a711 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/cache/EventTypeEventLogger.java @@ -0,0 +1,19 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.cache; + +import org.ehcache.event.CacheEvent; +import org.ehcache.event.CacheEventListener; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class EventTypeEventLogger implements CacheEventListener { + + @Override + public void onEvent( + CacheEvent cacheEvent) { + log.debug("Cacheevent: {}", cacheEvent.getType()); + } + +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/database/DatabaseProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/database/DatabaseProperties.java new file mode 100644 index 000000000..39e0cc4db --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/database/DatabaseProperties.java @@ -0,0 +1,17 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.database; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.database") +public class DatabaseProperties { + private String databaseNavn; + private String databaseUrl; + private String vaultSti; + private Integer maximumPoolSize; + private Integer minimumIdle; + private Integer maxLifetime; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/database/JpaConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/database/JpaConfiguration.java new file mode 100644 index 000000000..ff63d7af5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/database/JpaConfiguration.java @@ -0,0 +1,25 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.database; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.persistence.EntityManagerFactory; + +@Configuration +@EnableTransactionManagement +@RequiredArgsConstructor +public class JpaConfiguration { + private final EntityManagerFactory entityManagerFactory; + + @Bean + public PlatformTransactionManager transactionManager() { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + return transactionManager; + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/database/VaultDatabaseConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/database/VaultDatabaseConfiguration.java new file mode 100644 index 000000000..3f3d49ff2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/database/VaultDatabaseConfiguration.java @@ -0,0 +1,66 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.database; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.vault.jdbc.hikaricp.HikariCPVaultUtil; +import no.nav.vault.jdbc.hikaricp.VaultError; +import org.flywaydb.core.Flyway; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import javax.sql.DataSource; + +@Configuration +@Profile({ Miljø.DEV_FSS, Miljø.PROD_FSS }) +public class VaultDatabaseConfiguration { + private final DatabaseProperties config; + + @Autowired + public VaultDatabaseConfiguration(DatabaseProperties config) { + this.config = config; + } + + @Bean + public DataSource userDataSource() { + return dataSource("user"); + } + + @Bean + public FlywayMigrationStrategy flywayMigrationStrategy() { + return flyway -> { + Flyway.configure() + .dataSource(dataSource("admin")) + .initSql(String.format("SET ROLE \"%s\"", dbRole("admin"))) + .load() + .migrate(); + }; + } + + private HikariDataSource dataSource(String user) { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(config.getDatabaseUrl()); + if (config.getMaximumPoolSize() != null) { + hikariConfig.setMaximumPoolSize(config.getMaximumPoolSize()); + } + if (config.getMinimumIdle() != null) { + hikariConfig.setMinimumIdle(config.getMinimumIdle()); + } + if (config.getMaxLifetime() != null) { + hikariConfig.setMaxLifetime(config.getMaxLifetime()); + } + try { + return HikariCPVaultUtil.createHikariDataSourceWithVaultIntegration(hikariConfig, config.getVaultSti(), dbRole(user)); + } catch (VaultError vaultError) { + throw new BeanCreationException("Feil ved henting av credentials fra Vault: " + user, vaultError); + } + } + + private String dbRole(String role) { + return config.getDatabaseNavn() + "-" + role; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/kafka/AivenKafkaConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/kafka/AivenKafkaConfiguration.java new file mode 100644 index 000000000..043b123aa --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/kafka/AivenKafkaConfiguration.java @@ -0,0 +1,60 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka; + +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.config.SslConfigs; +import org.apache.kafka.common.security.auth.SecurityProtocol; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; + +import java.util.HashMap; +import java.util.Map; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Configuration +@Slf4j +@EnableKafka +public class AivenKafkaConfiguration { + + private final String javaKeystore = "jks"; + private final String pkcs12 = "PKCS12"; + @Value("${no.nav.gcp.kafka.aiven.bootstrap-servers}") + private String gcpBootstrapServers; + @Value("${no.nav.gcp.kafka.aiven.truststore-path}") + private String sslTruststoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.truststore-password}") + private String sslTruststorePasswordEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-path}") + private String sslKeystoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-password}") + private String sslKeystorePasswordEnvKey; + + private Map producerConfigs() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, gcpBootstrapServers); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, SecurityProtocol.SSL.name); + props.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, ""); + props.put(SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG, javaKeystore); + props.put(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG, pkcs12); + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, sslTruststoreLocationEnvKey); + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, sslTruststorePasswordEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, sslKeystoreLocationEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, sslKeystorePasswordEnvKey); + return props; + } + + @Bean + public KafkaTemplate aivenKafkaTemplate() { + return new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(producerConfigs())); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/kafka/Topics.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/kafka/Topics.java new file mode 100644 index 000000000..d5b0422da --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/kafka/Topics.java @@ -0,0 +1,19 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka; + +public class Topics { + //Tilskuddsperioder + public static final String TILSKUDDSPERIODE_GODKJENT = "arbeidsgiver.tiltak-tilskuddsperiode-godkjent"; + public static final String TILSKUDDSPERIODE_ANNULLERT = "arbeidsgiver.tiltak-tilskuddsperiode-annullert"; + public static final String TILSKUDDSPERIODE_FORKORTET = "arbeidsgiver.tiltak-tilskuddsperiode-forkortet"; + public static final String REFUSJON_ENDRET_STATUS = "arbeidsgiver.tiltak-refusjon-endret-status"; + //Varsel + public static final String TILTAK_SMS = "arbeidsgiver.tiltak-sms"; + public static final String TILTAK_VARSEL = "arbeidsgiver.tiltak-varsel"; + //Statistikk + public static final String DVH_MELDING = "arbeidsgiver.tiltak-dvh-melding"; + + public static final String AVTALE_HENDELSE = "arbeidsgiver.tiltak-avtale-hendelse"; + public static final String AVTALE_HENDELSE_COMPACT = "arbeidsgiver.tiltak-avtale-hendelse-compact"; + + public static final String AUDIT_HENDELSE = "arbeidsgiver.tiltak-audit-hendelse"; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/sts/STSClient.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/sts/STSClient.java new file mode 100644 index 000000000..421d21f99 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/sts/STSClient.java @@ -0,0 +1,48 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.sts; + +import java.net.URI; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +@Slf4j +@Component +public class STSClient { + + private final RestTemplate stsBasicAuthRestTemplate; + private final URI stsUri; + public STSClient(StsProperties stsProperties) { + this.stsBasicAuthRestTemplate = new RestTemplateBuilder() + .basicAuthentication(stsProperties.getUsername(), stsProperties.getPassword()) + .build(); + this.stsUri = stsProperties.getRestUri(); + } + public STSToken hentSTSToken() { + String uriString = UriComponentsBuilder.fromUri(stsUri) + .queryParam("grant_type", "client_credentials") + .queryParam("scope", "openid") + .toUriString(); + + return stsBasicAuthRestTemplate.exchange( + uriString, + HttpMethod.GET, + getRequestEntity(), + STSToken.class + ).getBody(); + + } + + private HttpEntity getRequestEntity() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + return new HttpEntity<>(headers); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/sts/STSToken.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/sts/STSToken.java new file mode 100644 index 000000000..639e903f5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/sts/STSToken.java @@ -0,0 +1,21 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.sts; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class STSToken { + + @JsonProperty("access_token") + String accessToken; + + @JsonProperty("token_type") + String tokenType; + + @JsonProperty("expires_in") + int expiresIn; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/sts/StsProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/sts/StsProperties.java new file mode 100644 index 000000000..b11582835 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/sts/StsProperties.java @@ -0,0 +1,16 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.sts; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.net.URI; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.sts") +public class StsProperties { + private URI restUri; + private String username; + private String password; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/AvtaleTilJournalfoering.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/AvtaleTilJournalfoering.java new file mode 100644 index 000000000..11f911793 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/AvtaleTilJournalfoering.java @@ -0,0 +1,93 @@ +package no.nav.tag.tiltaksgjennomforing.journalfoering; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.avtale.*; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AvtaleTilJournalfoering { + private Tiltakstype tiltakstype; + private UUID avtaleId; + private UUID avtaleVersjonId; + private LocalDate opprettet; + + private String deltakerFnr; + private String mentorFnr; + private String bedriftNr; + private String veilederNavIdent; + + private String deltakerFornavn; + private String deltakerEtternavn; + private String deltakerTlf; + private String bedriftNavn; + private String arbeidsgiverFornavn; + private String arbeidsgiverEtternavn; + private String arbeidsgiverTlf; + private String veilederFornavn; + private String veilederEtternavn; + private String veilederTlf; + private Integer versjon; + + private String oppfolging; + private String tilrettelegging; + + private LocalDate startDato; + private LocalDate sluttDato; + private Integer stillingprosent; + private String stillingstittel; + private Stillingstype stillingstype; + private String arbeidsoppgaver; + private Integer antallDagerPerUke; + private String enhetOppfolging; + + private RefusjonKontaktperson refusjonKontaktperson; + + // Lønnstilskudd + private String arbeidsgiverKontonummer; + private Integer lonnstilskuddProsent; + private Integer manedslonn; + private BigDecimal feriepengesats; + private BigDecimal arbeidsgiveravgift; + private Boolean harFamilietilknytning; + private String familietilknytningForklaring; + private Integer feriepengerBelop; + private Double otpSats; + private Integer otpBelop; + private Integer arbeidsgiveravgiftBelop; + private Integer sumLonnsutgifter; + private Integer sumLonnstilskudd; + private Integer manedslonn100pst; + + // Inkluderingstilskudd + private List inkluderingstilskuddsutgift = new ArrayList<>(); + private String inkluderingstilskuddBegrunnelse; + + // mentor + private String mentorFornavn; + private String mentorEtternavn; + private String mentorOppgaver; + private Double mentorAntallTimer; + private Integer mentorTimelonn; + + private List maal = new ArrayList<>(); + private List oppgaver = new ArrayList<>(); + + private GodkjentPaVegneGrunnTilJournalfoering godkjentPaVegneGrunn; + + private LocalDate godkjentAvDeltaker; + private LocalDate godkjentAvArbeidsgiver; + private LocalDate godkjentAvVeileder; + private LocalDate godkjentTaushetserklæringAvMentor; + private List tilskuddsPerioder; + private boolean godkjentPaVegneAv; + private Avtalerolle avtalerolle; + +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/AvtaleTilJournalfoeringMapper.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/AvtaleTilJournalfoeringMapper.java new file mode 100644 index 000000000..146fd4595 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/AvtaleTilJournalfoeringMapper.java @@ -0,0 +1,123 @@ +package no.nav.tag.tiltaksgjennomforing.journalfoering; + +import java.util.ArrayList; +import java.util.List; + +import no.nav.tag.tiltaksgjennomforing.avtale.*; + +public class AvtaleTilJournalfoeringMapper { + + public static AvtaleTilJournalfoering tilJournalfoering(AvtaleInnhold avtaleInnhold, Avtalerolle avtalerolle) { + Avtale avtale = avtaleInnhold.getAvtale(); + + AvtaleTilJournalfoering avtaleTilJournalfoering = new AvtaleTilJournalfoering(); + avtaleTilJournalfoering.setTiltakstype(avtale.getTiltakstype()); + avtaleTilJournalfoering.setArbeidsgiverKontonummer(avtale.getGjeldendeInnhold().getArbeidsgiverKontonummer()); + avtaleTilJournalfoering.setStillingstittel(avtale.getGjeldendeInnhold().getStillingstittel()); + avtaleTilJournalfoering.setArbeidsoppgaver(avtale.getGjeldendeInnhold().getArbeidsoppgaver()); + avtaleTilJournalfoering.setLonnstilskuddProsent(avtale.getGjeldendeInnhold().getLonnstilskuddProsent()); + avtaleTilJournalfoering.setManedslonn(avtale.getGjeldendeInnhold().getManedslonn()); + avtaleTilJournalfoering.setFeriepengesats(avtale.getGjeldendeInnhold().getFeriepengesats()); + avtaleTilJournalfoering.setArbeidsgiveravgift(avtale.getGjeldendeInnhold().getArbeidsgiveravgift()); + avtaleTilJournalfoering.setMentorFornavn(avtale.getGjeldendeInnhold().getMentorFornavn()); + avtaleTilJournalfoering.setMentorEtternavn(avtale.getGjeldendeInnhold().getMentorEtternavn()); + avtaleTilJournalfoering.setMentorOppgaver(avtale.getGjeldendeInnhold().getMentorOppgaver()); + avtaleTilJournalfoering.setMentorAntallTimer(avtale.getGjeldendeInnhold().getMentorAntallTimer()); + avtaleTilJournalfoering.setMentorTimelonn(avtale.getGjeldendeInnhold().getMentorTimelonn()); + avtaleTilJournalfoering.setGodkjentAvArbeidsgiver(avtaleInnhold.getGodkjentAvArbeidsgiver().toLocalDate()); + avtaleTilJournalfoering.setGodkjentAvVeileder(avtaleInnhold.getGodkjentAvVeileder().toLocalDate()); + avtaleTilJournalfoering.setGodkjentAvDeltaker(avtaleInnhold.getGodkjentAvDeltaker().toLocalDate()); + avtaleTilJournalfoering.setOpprettet(avtale.getOpprettetTidspunkt().toLocalDate()); + avtaleTilJournalfoering.setAvtaleId(avtale.getId()); + avtaleTilJournalfoering.setAvtaleVersjonId(avtaleInnhold.getId()); + avtaleTilJournalfoering.setDeltakerFnr(identifikatorAsString(avtale.getDeltakerFnr())); + avtaleTilJournalfoering.setMentorFnr(identifikatorAsString(avtale.getMentorFnr())); + avtaleTilJournalfoering.setBedriftNr(identifikatorAsString(avtale.getBedriftNr())); + avtaleTilJournalfoering.setVeilederNavIdent(identifikatorAsString(avtale.getVeilederNavIdent())); + avtaleTilJournalfoering.setEnhetOppfolging(avtale.getEnhetOppfolging()); + avtaleTilJournalfoering.setDeltakerFornavn(avtaleInnhold.getDeltakerFornavn()); + avtaleTilJournalfoering.setDeltakerEtternavn(avtaleInnhold.getDeltakerEtternavn()); + avtaleTilJournalfoering.setDeltakerTlf(avtaleInnhold.getDeltakerTlf()); + avtaleTilJournalfoering.setBedriftNavn(avtaleInnhold.getBedriftNavn()); + avtaleTilJournalfoering.setArbeidsgiverFornavn(avtaleInnhold.getArbeidsgiverFornavn()); + avtaleTilJournalfoering.setArbeidsgiverEtternavn(avtaleInnhold.getArbeidsgiverEtternavn()); + avtaleTilJournalfoering.setArbeidsgiverTlf(avtaleInnhold.getArbeidsgiverTlf()); + avtaleTilJournalfoering.setVeilederFornavn(avtaleInnhold.getVeilederFornavn()); + avtaleTilJournalfoering.setVeilederEtternavn(avtaleInnhold.getVeilederEtternavn()); + avtaleTilJournalfoering.setVeilederTlf(avtaleInnhold.getVeilederTlf()); + avtaleTilJournalfoering.setOppfolging(avtaleInnhold.getOppfolging()); + avtaleTilJournalfoering.setTilrettelegging(avtaleInnhold.getTilrettelegging()); + avtaleTilJournalfoering.setStartDato(avtaleInnhold.getStartDato()); + avtaleTilJournalfoering.setSluttDato(avtaleInnhold.getSluttDato()); + avtaleTilJournalfoering.setStillingprosent(avtaleInnhold.getStillingprosent()); + avtaleTilJournalfoering.setAntallDagerPerUke(avtaleInnhold.getAntallDagerPerUke()); + avtaleTilJournalfoering.setMaal(maalListToMaalTilJournalfoeringList(avtaleInnhold.getMaal())); + avtaleTilJournalfoering.setGodkjentPaVegneGrunn(godkjentPaVegneGrunn(avtaleInnhold.getGodkjentPaVegneGrunn())); + avtaleTilJournalfoering.setGodkjentPaVegneAv(avtaleInnhold.isGodkjentPaVegneAv()); + avtaleTilJournalfoering.setVersjon(avtaleInnhold.getVersjon()); + avtaleTilJournalfoering.setHarFamilietilknytning(avtaleInnhold.getHarFamilietilknytning()); + avtaleTilJournalfoering.setFamilietilknytningForklaring((avtaleInnhold.getFamilietilknytningForklaring())); + avtaleTilJournalfoering.setFeriepengerBelop(avtaleInnhold.getFeriepengerBelop()); + avtaleTilJournalfoering.setOtpSats(avtaleInnhold.getOtpSats()); + avtaleTilJournalfoering.setOtpBelop(avtaleInnhold.getOtpBelop()); + avtaleTilJournalfoering.setArbeidsgiveravgiftBelop(avtaleInnhold.getArbeidsgiveravgiftBelop()); + avtaleTilJournalfoering.setSumLonnsutgifter(avtaleInnhold.getSumLonnsutgifter()); + avtaleTilJournalfoering.setSumLonnstilskudd(avtaleInnhold.getSumLonnstilskudd()); + avtaleTilJournalfoering.setStillingstype(avtaleInnhold.getStillingstype()); + avtaleTilJournalfoering.setManedslonn100pst(avtaleInnhold.getManedslonn100pst()); + avtaleTilJournalfoering.setRefusjonKontaktperson(avtaleInnhold.getRefusjonKontaktperson()); + avtaleTilJournalfoering.setInkluderingstilskuddsutgift(avtaleInnhold.getInkluderingstilskuddsutgift()); + avtaleTilJournalfoering.setInkluderingstilskuddBegrunnelse(avtaleInnhold.getInkluderingstilskuddBegrunnelse()); + + if(avtalerolle != null) { + avtaleTilJournalfoering.setAvtalerolle(avtalerolle) ; + } + if(avtaleInnhold.getGodkjentTaushetserklæringAvMentor() != null){ + avtaleTilJournalfoering.setGodkjentTaushetserklæringAvMentor(avtaleInnhold.getGodkjentTaushetserklæringAvMentor().toLocalDate()); + } + + return avtaleTilJournalfoering; + } + + private static String identifikatorAsString(Identifikator id) { + return id != null ? id.asString() : ""; + } + + private static GodkjentPaVegneGrunnTilJournalfoering godkjentPaVegneGrunn(GodkjentPaVegneGrunn grunn) { + if (grunn == null) { + return null; + } + + return new GodkjentPaVegneGrunnTilJournalfoering( + grunn.isIkkeBankId(), + grunn.isDigitalKompetanse(), + grunn.isReservert(), + grunn.isArenaMigreringDeltaker() + ); + } + + + private static MaalTilJournalfoering maalToMaalTilJournalfoering(Maal maal) { + if (maal == null) { + return null; + } + + MaalTilJournalfoering maalTilJournalfoering = new MaalTilJournalfoering(); + + maalTilJournalfoering.setKategori(maal.getKategori().getVerdi()); + maalTilJournalfoering.setBeskrivelse(maal.getBeskrivelse()); + + return maalTilJournalfoering; + } + + private static List maalListToMaalTilJournalfoeringList(List list) { + if (list == null) { + return null; + } + List list1 = new ArrayList<>(list.size()); + for (Maal maal : list) { + list1.add(maalToMaalTilJournalfoering(maal)); + } + return list1; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/GodkjentPaVegneGrunnTilJournalfoering.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/GodkjentPaVegneGrunnTilJournalfoering.java new file mode 100644 index 000000000..b6194113d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/GodkjentPaVegneGrunnTilJournalfoering.java @@ -0,0 +1,16 @@ +package no.nav.tag.tiltaksgjennomforing.journalfoering; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GodkjentPaVegneGrunnTilJournalfoering { + + private boolean ikkeBankId; + private boolean digitalKompetanse; + private boolean reservert; + private boolean arenaMigreringDeltaker; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/InternalAvtaleController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/InternalAvtaleController.java new file mode 100644 index 000000000..d9100c997 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/InternalAvtaleController.java @@ -0,0 +1,59 @@ +package no.nav.tag.tiltaksgjennomforing.journalfoering; + +import io.micrometer.core.annotation.Timed; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.security.token.support.core.api.ProtectedWithClaims; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggingService; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleInnhold; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleInnholdRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.TilskuddPeriode; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.UUID; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/internal/avtaler") +@Timed +@RequiredArgsConstructor +@ProtectedWithClaims(issuer = "system") +@Slf4j +public class InternalAvtaleController { + + private final AvtaleInnholdRepository avtaleInnholdRepository; + private final InnloggingService innloggingService; + + @GetMapping + public List hentIkkeJournalfoerteAvtaler() { + try { + innloggingService.validerSystembruker(); + List avtaleVersjoner = avtaleInnholdRepository.finnAvtaleVersjonerTilJournalfoering(); + List avtalerTilJournalfoering = avtaleVersjoner.stream().map(avtaleInnhold -> { + SortedSet tilskuddPeriode = avtaleInnhold.getAvtale().getTilskuddPeriode(); + AvtaleTilJournalfoering avtaleTilJournalfoering = AvtaleTilJournalfoeringMapper.tilJournalfoering(avtaleInnhold, null); + avtaleTilJournalfoering.setTilskuddsPerioder(tilskuddPeriode.stream().toList()); + return avtaleTilJournalfoering; + }).collect(Collectors.toList()); + return avtalerTilJournalfoering; + } catch (Exception e) { + log.error("Feil ved henting av ikke-journalførte avtaler", e); + throw e; + } + } + + @PutMapping + @Transactional + public ResponseEntity journalfoerAvtaler(@RequestBody Map avtaleVersjonerTilJournalfoert) { + innloggingService.validerSystembruker(); + Iterable avtaleVersjoner = avtaleInnholdRepository.findAllById(avtaleVersjonerTilJournalfoert.keySet()); + avtaleVersjoner.forEach(avtaleVersjon -> avtaleVersjon.setJournalpostId(avtaleVersjonerTilJournalfoert.get(avtaleVersjon.getId()))); + avtaleInnholdRepository.saveAll(avtaleVersjoner); + return ResponseEntity.ok().build(); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/MaalTilJournalfoering.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/MaalTilJournalfoering.java new file mode 100644 index 000000000..619a2416d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/MaalTilJournalfoering.java @@ -0,0 +1,13 @@ +package no.nav.tag.tiltaksgjennomforing.journalfoering; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MaalTilJournalfoering { + private String kategori; + private String beskrivelse; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/OppgaveTilJournalFoering.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/OppgaveTilJournalFoering.java new file mode 100644 index 000000000..93db5596c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/journalfoering/OppgaveTilJournalFoering.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.journalfoering; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OppgaveTilJournalFoering { + private String tittel; + private String beskrivelse; + private String opplaering; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/kodeverk/KodeverkController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/kodeverk/KodeverkController.java new file mode 100644 index 000000000..9856e65a6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/kodeverk/KodeverkController.java @@ -0,0 +1,54 @@ +package no.nav.tag.tiltaksgjennomforing.kodeverk; + +import io.micrometer.core.annotation.Timed; +import no.nav.security.token.support.core.api.Unprotected; +import no.nav.tag.tiltaksgjennomforing.avtale.Status; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@Unprotected +@RequestMapping("/kodeverk") +@Timed +public class KodeverkController { + + @GetMapping + public Map> get() { + Map> map = new HashMap<>(); + map.put("tiltakstyper", tiltakstyper()); + map.put("statuser", statuser()); + return map; + } + + @GetMapping("/statuser") + public List statuser() { + return Arrays.stream(Status.values()).map(s -> { + HashMap statusMap = new HashMap<>(); + statusMap.put("navn", s.name()); + statusMap.put("beskrivelse", s.getBeskrivelse()); + return statusMap; + }).collect(Collectors.toList()); + } + + @GetMapping("/tiltakstyper") + public List tiltakstyper() { + return Arrays.stream(Tiltakstype.values()).map(t -> { + HashMap tiltakMap = new HashMap<>(); + tiltakMap.put("navn", t.name()); + tiltakMap.put("beskrivelse", t.getBeskrivelse()); + tiltakMap.put("behandlingstema", t.getBehandlingstema()); + if (t.getTiltakskodeArena() != null) { + tiltakMap.put("tiltakskodeArena", t.getTiltakskodeArena()); + } + return tiltakMap; + }).collect(Collectors.toList()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/leader/LeaderPodCheck.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/leader/LeaderPodCheck.java new file mode 100644 index 000000000..ac9a08842 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/leader/LeaderPodCheck.java @@ -0,0 +1,65 @@ +package no.nav.tag.tiltaksgjennomforing.leader; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@Component +@Slf4j +public class LeaderPodCheck { + + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + private final String path; + private final boolean enabled; + + public LeaderPodCheck(RestTemplate restTemplate, ObjectMapper objectMapper, @Value("${ELECTOR_PATH}") String electorPath) { + this.restTemplate = restTemplate; + this.objectMapper = objectMapper; + this.enabled = isNotBlank(electorPath); + this.path = "http://" + electorPath; + } + + public boolean isLeaderPod() { + if (!enabled) { + return true; + } + String hostname; + String leader; + try { + leader = getJSONFromUrl(path).name; + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + log.error("Feil v/henting av host for leader-election", e); + return false; + } catch (Exception e) { + log.error("Feil v/oppslag i leader-elector", e); + throw new RuntimeException(e); + } + return hostname.equals(leader); + } + + private Elector getJSONFromUrl(String electorPath) throws JsonProcessingException { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(List.of(MediaType.ALL)); + var entity = new HttpEntity<>(headers); + ResponseEntity responseEntity = restTemplate.exchange(electorPath, HttpMethod.GET, entity, String.class); + return objectMapper.readValue((String) responseEntity.getBody(), Elector.class); + } + + @Data + private static class Elector { + String name; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/metrikker/MetrikkRegistrering.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/metrikker/MetrikkRegistrering.java new file mode 100644 index 000000000..ea6d3c874 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/metrikker/MetrikkRegistrering.java @@ -0,0 +1,199 @@ +package no.nav.tag.tiltaksgjennomforing.metrikker; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleDeltMedAvtalepart; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleEndret; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleInngått; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleLåstOpp; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleNyVeileder; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleOpprettetAvArbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleOpprettetAvVeileder; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleSlettemerket; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjenningerOpphevetAvArbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjenningerOpphevetAvVeileder; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjentAvArbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjentAvDeltaker; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjentAvVeileder; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjentPaVegneAvArbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjentPaVegneAvDeltaker; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjentPaVegneAvDeltakerOgArbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.avtale.events.SignertAvMentor; +import no.nav.tag.tiltaksgjennomforing.avtale.events.TilskuddsperiodeAvslått; +import no.nav.tag.tiltaksgjennomforing.avtale.events.TilskuddsperiodeGodkjent; +import no.nav.tag.tiltaksgjennomforing.varsel.events.SmsSendt; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RequiredArgsConstructor +public class MetrikkRegistrering { + private final MeterRegistry meterRegistry; + + @EventListener + public void smsSendt(SmsSendt event) { + Counter.builder("tiltaksgjennomforing.smsvarsel.sendt").register(meterRegistry).increment(); + } + + @EventListener + public void avtaleOpprettet(AvtaleOpprettetAvVeileder event) { + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtale opprettet av veileder, avtaleId={} ident={}, tiltakstype={}", event.getAvtale().getId(), event.getUtfortAv(), tiltakstype); + counter("avtale.opprettet", Avtalerolle.VEILEDER, tiltakstype).increment(); + } + + @EventListener + public void avtaleOpprettetAvArbeidsgiver(AvtaleOpprettetAvArbeidsgiver event) { + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtale opprettet av arbeidsgiver, avtaleId={}, tiltakstype={}", event.getAvtale().getId(), tiltakstype); + counter("avtale.opprettet", Avtalerolle.ARBEIDSGIVER, tiltakstype).increment(); + } + + @EventListener + public void avtaleEndret(AvtaleEndret event) { + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtale endret, avtaleId={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), event.getUtfortAvRolle(), tiltakstype); + counter("avtale.endret", event.getUtfortAvRolle(), tiltakstype).increment(); + } + + @EventListener + public void godkjenningerOpphevet(GodkjenningerOpphevetAvVeileder event) { + Avtalerolle rolle = Avtalerolle.VEILEDER; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtalens godkjenninger opphevet, avtaleId={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), rolle, tiltakstype); + counter("avtale.godkjenning.opphevet", rolle, tiltakstype).increment(); + } + + @EventListener + public void godkjenningerOpphevet(GodkjenningerOpphevetAvArbeidsgiver event) { + Avtalerolle rolle = Avtalerolle.ARBEIDSGIVER; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtalens godkjenninger opphevet, avtaleId={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), rolle, tiltakstype); + counter("avtale.godkjenning.opphevet", rolle, tiltakstype).increment(); + } + + @EventListener + public void godkjentAvDeltaker(GodkjentAvDeltaker event) { + Avtalerolle rolle = Avtalerolle.DELTAKER; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtale godkjent, avtaleId={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), rolle, tiltakstype); + counter("avtale.godkjenning.godkjent", rolle, tiltakstype).increment(); + } + + @EventListener + public void godkjentAvMentor(SignertAvMentor event) { + Avtalerolle rolle = Avtalerolle.MENTOR; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Mentor har signert taushetserklæring, avtaleId={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), rolle, tiltakstype); + counter("avtale.godkjenning.godkjent", rolle, tiltakstype).increment(); + } + + @EventListener + public void godkjentAvArbeidsgiver(GodkjentAvArbeidsgiver event) { + Avtalerolle rolle = Avtalerolle.ARBEIDSGIVER; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtale godkjent, avtaleId={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), rolle, tiltakstype); + counter("avtale.godkjenning.godkjent", rolle, tiltakstype).increment(); + } + + @EventListener + public void godkjentAvVeileder(GodkjentAvVeileder event) { + Avtalerolle rolle = Avtalerolle.VEILEDER; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtale godkjent, avtaleId={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), rolle, tiltakstype); + counter("avtale.godkjenning.godkjent", rolle, tiltakstype).increment(); + } + + @EventListener + public void avtaleInngått(AvtaleInngått event) { + Avtalerolle rolle = event.getUtførtAvRolle(); + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtale inngått, avtaleId={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), rolle, tiltakstype); + counter("avtale.godkjenning.inngaatt", rolle, tiltakstype).increment(); + } + + @EventListener + public void tilskuddsperiodeGodkjent(TilskuddsperiodeGodkjent event) { + Avtalerolle rolle = Avtalerolle.BESLUTTER; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Tilskuddsperiode godkjent, avtaleId={}, tilskuddsperiodeId={}, løpenummer={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), event.getTilskuddsperiode().getId(), event.getTilskuddsperiode().getLøpenummer(), rolle, tiltakstype); + counter("avtale.tilskuddsperiode.godkjent", rolle, tiltakstype).increment(); + } + + @EventListener + public void tilskuddsperiodeAvslått(TilskuddsperiodeAvslått event) { + Avtalerolle rolle = Avtalerolle.BESLUTTER; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Tilskuddsperiode avslått, avtaleId={}, tilskuddsperiodeId={}, løpenummer={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), event.getTilskuddsperiode().getId(), event.getTilskuddsperiode().getLøpenummer(), rolle, tiltakstype); + counter("avtale.tilskuddsperiode.avslaatt", rolle, tiltakstype).increment(); + } + + @EventListener + public void godkjentPaVegneAv(GodkjentPaVegneAvDeltaker event) { + Avtalerolle rolle = Avtalerolle.VEILEDER; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtale godkjent på vegne av deltaker, avtaleId={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), rolle, tiltakstype); + counter("avtale.godkjenning.godkjentPaVegneAv", rolle, tiltakstype).increment(); + } + + @EventListener + public void godkjentPaVegneAvArbeidsgiver(GodkjentPaVegneAvArbeidsgiver event) { + Avtalerolle rolle = Avtalerolle.VEILEDER; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtale godkjent på vegne av arbeidsgiver, avtaleId={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), rolle, tiltakstype); + counter("avtale.godkjenning.godkjentPaVegneAvArbeidsgiver", rolle, tiltakstype).increment(); + } + + @EventListener + public void godkjentPaVegneAvDeltakerOgArbeidsgiver(GodkjentPaVegneAvDeltakerOgArbeidsgiver event) { + Avtalerolle rolle = Avtalerolle.VEILEDER; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtale godkjent på vegne av deltaker og arbeidsgiver, avtaleId={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), rolle, tiltakstype); + counter("avtale.godkjenning.godkjentPaVegneAvDeltakerOgArbeidsgiver", rolle, tiltakstype).increment(); + } + + @EventListener + public void avtaleLåstOpp(AvtaleLåstOpp event) { + Avtalerolle rolle = Avtalerolle.VEILEDER; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtale låst opp, avtaleId={}, avtalepart={}, tiltakstype={}", event.getAvtale().getId(), rolle, tiltakstype); + counter("avtale.laastOpp", rolle, tiltakstype).increment(); + } + + @EventListener + public void avtaleDeltMedAvtalepart(AvtaleDeltMedAvtalepart event) { + Avtalerolle rolle = Avtalerolle.VEILEDER; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + log.info("Avtale delt med {}, avtaleId={}, avtalepart={}, tiltakstype={}", event.getAvtalepart(), event.getAvtale().getId(), rolle, tiltakstype); + counter("avtale.deltMedAvtalepart", rolle, tiltakstype).increment(); + } + + @EventListener + public void avtaleNyVeileder(AvtaleNyVeileder event) { + Avtalerolle rolle = Avtalerolle.VEILEDER; + Tiltakstype tiltakstype = event.getAvtale().getTiltakstype(); + if (event.getTidligereVeileder() == null) { + log.info("Avtale tildelt veileder: avtaleId={}, veileder={}", event.getAvtale().getId(), event.getAvtale().getVeilederNavIdent().asString()); + } else { + log.info("Avtale byttet veileder: avtaleId={}, tidligere veileder={}, ny veileder={}", event.getAvtale().getId(), event.getTidligereVeileder().asString(), event.getAvtale().getVeilederNavIdent().asString()); + } + counter("avtale.endretVEileder", rolle, tiltakstype).increment(); + } + @EventListener + public void avtaleSlettemerket(AvtaleSlettemerket event) { + log.info("Avtale slettemerket, utfortAv={}, avtaleId={}", event.getUtfortAv().asString(), event.getAvtale().getId()); + } + + private Counter counter(String navn, Avtalerolle avtalerolle, Tiltakstype tiltakstype) { + var builder = Counter.builder("tiltaksgjennomforing." + navn) + .tag("tiltak", tiltakstype.name()) + .tag("avtalepart", avtalerolle.name()); + return builder.register(meterRegistry); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterConfiguration.java new file mode 100644 index 000000000..e8023f72c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterConfiguration.java @@ -0,0 +1,49 @@ +package no.nav.tag.tiltaksgjennomforing.okonomi; + +import no.nav.security.token.support.client.core.ClientProperties; +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse; +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenService; +import no.nav.security.token.support.client.spring.ClientConfigurationProperties; +import no.nav.security.token.support.client.spring.oauth2.EnableOAuth2Client; +import no.nav.tag.tiltaksgjennomforing.utils.ConditionalOnPropertyNotEmpty; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestTemplate; + +import java.util.Optional; + +@EnableOAuth2Client(cacheEnabled = true) +@Configuration +@ConditionalOnPropertyNotEmpty("tiltaksgjennomforing.kontoregister.azureConfig") +class KontoregisterConfiguration { + + /* + * Create one RestTemplate per OAuth2 client entry to separate between different scopes per API + */ + @Bean("azure") + RestTemplate downstreamResourceRestTemplate(RestTemplateBuilder restTemplateBuilder, + ClientConfigurationProperties clientConfigurationProperties, + OAuth2AccessTokenService oAuth2AccessTokenService) { + + ClientProperties clientProperties = + Optional.ofNullable(clientConfigurationProperties.getRegistration().get("kontoregister")) + .orElseThrow(() -> new RuntimeException("could not find oauth2 client config for kontoregister")); + return restTemplateBuilder + .additionalInterceptors(bearerTokenInterceptor(clientProperties, oAuth2AccessTokenService)) + .build(); + } + + + private ClientHttpRequestInterceptor bearerTokenInterceptor(ClientProperties clientProperties, + OAuth2AccessTokenService oAuth2AccessTokenService) { + return (request, body, execution) -> { + OAuth2AccessTokenResponse response = + oAuth2AccessTokenService.getAccessToken(clientProperties); + request.getHeaders().setBearerAuth(response.getAccessToken()); + return execution.execute(request, body); + }; + } + +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterProperties.java new file mode 100644 index 000000000..f69a715f7 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterProperties.java @@ -0,0 +1,13 @@ +package no.nav.tag.tiltaksgjennomforing.okonomi; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.kontoregister") +public class KontoregisterProperties { + private String uri; + private String consumerId; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterResponse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterResponse.java new file mode 100644 index 000000000..30f60e695 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterResponse.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.okonomi; + +import lombok.Value; + +@Value +public class KontoregisterResponse { + private final String mottaker; + private final String kontonr; + private final String feilmelding; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterService.java new file mode 100644 index 000000000..f066a4e38 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterService.java @@ -0,0 +1,5 @@ +package no.nav.tag.tiltaksgjennomforing.okonomi; + +public interface KontoregisterService { + String hentKontonummer(String bedriftNr); +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterServiceFake.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterServiceFake.java new file mode 100644 index 000000000..95103b64d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterServiceFake.java @@ -0,0 +1,16 @@ +package no.nav.tag.tiltaksgjennomforing.okonomi; + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.utils.ConditionalOnPropertyNotEmpty; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@ConditionalOnPropertyNotEmpty("tiltaksgjennomforing.kontoregister.fake") +public class KontoregisterServiceFake implements KontoregisterService { + + public String hentKontonummer(String bedriftNr) { + return "10000008162"; + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterServiceImpl.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterServiceImpl.java new file mode 100644 index 000000000..3fea96c72 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterServiceImpl.java @@ -0,0 +1,61 @@ +package no.nav.tag.tiltaksgjennomforing.okonomi; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import java.net.URI; +import java.net.URISyntaxException; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.exceptions.KontoregisterFantIkkeBedriftFeilException; +import no.nav.tag.tiltaksgjennomforing.exceptions.KontoregisterFeilException; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.CorrelationIdSupplier; +import no.nav.tag.tiltaksgjennomforing.utils.ConditionalOnPropertyNotEmpty; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +@Component +@Slf4j +@ConditionalOnPropertyNotEmpty("tiltaksgjennomforing.kontoregister.realClient") +public class KontoregisterServiceImpl implements KontoregisterService{ + + private final KontoregisterProperties kontoregisterProperties; + private final RestTemplate restTemplate; + + public KontoregisterServiceImpl(KontoregisterProperties kontoregisterProperties, @Qualifier("azure") RestTemplate restTemplate) { + this.kontoregisterProperties = kontoregisterProperties; + this.restTemplate = restTemplate; + } + + public String hentKontonummer(String bedriftNr) { + try { + ResponseEntity response = restTemplate.exchange(new URI(String.format("%s/%s", kontoregisterProperties.getUri(),bedriftNr)),HttpMethod.GET,lagRequest(), KontoregisterResponse.class); + return response.getBody().getKontonr(); + + } catch (RestClientException | URISyntaxException exception) { + if(exception instanceof HttpClientErrorException){ + HttpClientErrorException hcee = (HttpClientErrorException)exception; + if(hcee.getStatusCode() == NOT_FOUND) { + throw new KontoregisterFantIkkeBedriftFeilException(); + } + + } + log.error(String.format("Feil fra kontoregister med request-url: %s/%s", kontoregisterProperties.getUri(),bedriftNr), exception); + throw new KontoregisterFeilException(); + } + } + + private HttpEntity lagRequest(){ + HttpHeaders headers = new HttpHeaders(); + headers.set("Nav-consumer-Id", kontoregisterProperties.getConsumerId()); + headers.set("Nav-Call-Id", CorrelationIdSupplier.get()); + headers.setContentType(MediaType.APPLICATION_JSON); + return new HttpEntity(headers); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/ArbeidsgiverOrganisasjon.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/ArbeidsgiverOrganisasjon.java new file mode 100644 index 000000000..9fc628bb2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/ArbeidsgiverOrganisasjon.java @@ -0,0 +1,17 @@ +package no.nav.tag.tiltaksgjennomforing.orgenhet; + +import lombok.Value; +import lombok.experimental.FieldNameConstants; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; + +import java.util.ArrayList; +import java.util.List; + +@Value +@FieldNameConstants +public class ArbeidsgiverOrganisasjon { + private final BedriftNr bedriftNr; + private final String bedriftNavn; + private final List tilgangstyper = new ArrayList<>(); +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EnhetErJuridiskException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EnhetErJuridiskException.java new file mode 100644 index 000000000..94a817ad3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EnhetErJuridiskException.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.orgenhet; + +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; + +public class EnhetErJuridiskException extends FeilkodeException { + public EnhetErJuridiskException() { + super(Feilkode.ENHET_ER_JURIDISK); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EnhetErOrganisasjonsleddException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EnhetErOrganisasjonsleddException.java new file mode 100644 index 000000000..ef50d532d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EnhetErOrganisasjonsleddException.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.orgenhet; + +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; + +public class EnhetErOrganisasjonsleddException extends FeilkodeException { + public EnhetErOrganisasjonsleddException() { + super(Feilkode.ENHET_ER_ORGLEDD); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EnhetFinnesIkkeException.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EnhetFinnesIkkeException.java new file mode 100644 index 000000000..56330c663 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EnhetFinnesIkkeException.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.orgenhet; + +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; + +public class EnhetFinnesIkkeException extends FeilkodeException { + public EnhetFinnesIkkeException() { + super(Feilkode.ENHET_FINNES_IKKE); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregEnhet.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregEnhet.java new file mode 100644 index 000000000..86b43d4fb --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregEnhet.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.orgenhet; + +import lombok.Data; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; + +@Data +public class EregEnhet { + private String organisasjonsnummer; + private EregNavn navn; + private String type; + + public Organisasjon konverterTilDomeneObjekt() { + return new Organisasjon(new BedriftNr(organisasjonsnummer), navn.getSammensattnavn()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregNavn.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregNavn.java new file mode 100644 index 000000000..4dd5e39fc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregNavn.java @@ -0,0 +1,12 @@ +package no.nav.tag.tiltaksgjennomforing.orgenhet; + +import lombok.Data; + +@Data +public class EregNavn { + private String navnelinje1; + private String navnelinje2; + private String navnelinje3; + private String navnelinje4; + private String sammensattnavn; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregProperties.java new file mode 100644 index 000000000..63f783354 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregProperties.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.orgenhet; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.net.URI; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.ereg") +public class EregProperties { + private URI uri; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregService.java new file mode 100644 index 000000000..f070aea1a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregService.java @@ -0,0 +1,35 @@ +package no.nav.tag.tiltaksgjennomforing.orgenhet; + +import lombok.RequiredArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; + +@Service +@RequiredArgsConstructor +public class EregService { + private final EregProperties eregProperties; + private final RestTemplate restTemplate = new RestTemplate(); + + public Organisasjon hentVirksomhet(BedriftNr bedriftNr) { + URI uri = UriComponentsBuilder.fromUri(eregProperties.getUri()) + .pathSegment(bedriftNr.asString()) + .build() + .toUri(); + try { + EregEnhet eregEnhet = restTemplate.getForObject(uri, EregEnhet.class); + if ("JuridiskEnhet".equals(eregEnhet.getType())) { + throw new EnhetErJuridiskException(); + } else if ("Organisasjonsledd".equals(eregEnhet.getType())) { + throw new EnhetErOrganisasjonsleddException(); + } + return eregEnhet.konverterTilDomeneObjekt(); + } catch (RestClientException e) { + throw new EnhetFinnesIkkeException(); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/Organisasjon.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/Organisasjon.java new file mode 100644 index 000000000..a673c9e3a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/Organisasjon.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.orgenhet; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; + +@Value +public class Organisasjon { + private final BedriftNr bedriftNr; + private final String bedriftNavn; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/OrganisasjonController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/OrganisasjonController.java new file mode 100644 index 000000000..962555d53 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/orgenhet/OrganisasjonController.java @@ -0,0 +1,24 @@ +package no.nav.tag.tiltaksgjennomforing.orgenhet; + +import io.micrometer.core.annotation.Timed; +import lombok.RequiredArgsConstructor; +import no.nav.security.token.support.core.api.Protected; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Protected +@RestController +@RequestMapping("/organisasjoner") +@Timed +@RequiredArgsConstructor +public class OrganisasjonController { + private final EregService eregService; + + @GetMapping + public Organisasjon hentVirksomhet(@RequestParam("bedriftNr") BedriftNr bedriftNr) { + return eregService.hentVirksomhet(bedriftNr); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Adressebeskyttelse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Adressebeskyttelse.java new file mode 100644 index 000000000..0084f9bb7 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Adressebeskyttelse.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import lombok.Value; + +@Value +public class Adressebeskyttelse { + public static final Adressebeskyttelse INGEN_BESKYTTELSE = new Adressebeskyttelse(""); + private final String gradering; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Data.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Data.java new file mode 100644 index 000000000..686afcf4f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Data.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import lombok.Value; + +@Value +public class Data { + private final HentPerson hentPerson; + private final HentIdenter hentIdenter; + private final HentGeografiskTilknytning hentGeografiskTilknytning; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/HentGeografiskTilknytning.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/HentGeografiskTilknytning.java new file mode 100644 index 000000000..abab565fc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/HentGeografiskTilknytning.java @@ -0,0 +1,18 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import lombok.Value; + +@Value +public class HentGeografiskTilknytning { + private final String gtKommune; + private final String gtBydel; + private final String gtLand; + private final String regel; + + String getGeoTilknytning(){ + if(gtBydel == null){ + return gtKommune; + } + return gtBydel; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/HentIdenter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/HentIdenter.java new file mode 100644 index 000000000..fbaed36e2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/HentIdenter.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import lombok.Value; + +@Value +public class HentIdenter { + private final Identer[] identer; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/HentPerson.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/HentPerson.java new file mode 100644 index 000000000..71c5c7b31 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/HentPerson.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import lombok.Value; + +@Value +public class HentPerson { + private final Adressebeskyttelse[] adressebeskyttelse; + private final Navn[] navn; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Identer.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Identer.java new file mode 100644 index 000000000..cf730f354 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Identer.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import lombok.Value; + +@Value +public class Identer { + private final String ident; + private final String gruppe; + private final boolean historisk; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Navn.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Navn.java new file mode 100644 index 000000000..42ebf98eb --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Navn.java @@ -0,0 +1,11 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import lombok.Value; + +@Value +public class Navn { + public static final Navn TOMT_NAVN = new Navn("", "", ""); + private final String fornavn; + private final String mellomnavn; + private final String etternavn; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/NavnFormaterer.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/NavnFormaterer.java new file mode 100644 index 000000000..29ae8117b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/NavnFormaterer.java @@ -0,0 +1,28 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import org.apache.commons.text.WordUtils; +import org.springframework.util.StringUtils; + +public class NavnFormaterer { + private final Navn navn; + + public NavnFormaterer(Navn navn) { + this.navn = navn; + } + + public String getEtternavn() { + return storeForbokstaver(navn.getEtternavn()); + } + + public String getFornavn() { + String fornavnOgMellomnavn = navn.getFornavn(); + if (StringUtils.hasLength(navn.getMellomnavn())) { + fornavnOgMellomnavn += " " + navn.getMellomnavn(); + } + return storeForbokstaver(fornavnOgMellomnavn); + } + + private static String storeForbokstaver(String navn) { + return WordUtils.capitalizeFully(navn, '-', ' '); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/PdlRequest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/PdlRequest.java new file mode 100644 index 000000000..dcbb87f3a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/PdlRequest.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import lombok.Value; + +@Value +public class PdlRequest { + private final String query; + private final Variables variables; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/PdlRespons.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/PdlRespons.java new file mode 100644 index 000000000..3b42ada77 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/PdlRespons.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import lombok.Value; + +@Value +public class PdlRespons { + private final Data data; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/PersondataProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/PersondataProperties.java new file mode 100644 index 000000000..f6f90623c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/PersondataProperties.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.net.URI; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.persondata") +public class PersondataProperties { + private URI uri; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/PersondataService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/PersondataService.java new file mode 100644 index 000000000..dfab4a6a1 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/PersondataService.java @@ -0,0 +1,136 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.cache.EhCacheConfig; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.sts.STSClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.StreamUtils; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PersondataService { + private final RestTemplate restTemplate; + private final STSClient stsClient; + private final PersondataProperties persondataProperties; + + @Value("classpath:pdl/hentPerson.adressebeskyttelse.graphql") + private Resource adressebeskyttelseQueryResource; + + @Value("classpath:pdl/hentPersondata.graphql") + private Resource persondataQueryResource; + + @Value("classpath:pdl/hentIdenter.graphql") + private Resource identerQueryResource; + + @SneakyThrows + private static String resourceAsString(Resource adressebeskyttelseQuery) { + String filinnhold = StreamUtils.copyToString(adressebeskyttelseQuery.getInputStream(), StandardCharsets.UTF_8); + return filinnhold.replaceAll("\\s+", " "); + } + + protected Adressebeskyttelse hentAdressebeskyttelse(Fnr fnr) { + PdlRequest pdlRequest = new PdlRequest(resourceAsString(adressebeskyttelseQueryResource), new Variables(fnr.asString())); + return hentAdressebeskyttelseFraPdlRespons(utførKallTilPdl(pdlRequest)); + } + + private HttpEntity createRequestEntity(PdlRequest pdlRequest) { + String stsToken = stsClient.hentSTSToken().getAccessToken(); + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(stsToken); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Tema", "GEN"); + headers.set("Nav-Consumer-Token", "Bearer " + stsToken); + return new HttpEntity(pdlRequest, headers); + } + + private static Adressebeskyttelse hentAdressebeskyttelseFraPdlRespons(PdlRespons pdlRespons) { + try { + return pdlRespons.getData().getHentPerson().getAdressebeskyttelse()[0]; + } catch (NullPointerException | ArrayIndexOutOfBoundsException e) { + return Adressebeskyttelse.INGEN_BESKYTTELSE; + } + } + + public static Navn hentNavnFraPdlRespons(PdlRespons pdlRespons) { + try { + return pdlRespons.getData().getHentPerson().getNavn()[0]; + } catch (NullPointerException | ArrayIndexOutOfBoundsException e) { + return Navn.TOMT_NAVN; + } + } + + private static String hentAktørIdFraPdlRespons(PdlRespons pdlRespons) { + try { + return pdlRespons.getData().getHentIdenter().getIdenter()[0].getIdent(); + } catch (NullPointerException | ArrayIndexOutOfBoundsException e) { + return ""; + } + } + + public static Optional hentGeoLokasjonFraPdlRespons(PdlRespons pdlRespons) { + try { + return Optional.of(pdlRespons.getData().getHentGeografiskTilknytning().getGeoTilknytning()); + } catch (NullPointerException | ArrayIndexOutOfBoundsException e) { + return Optional.empty(); + } + } + + private PdlRespons utførKallTilPdl(PdlRequest pdlRequest) { + try { + return restTemplate.postForObject(persondataProperties.getUri(), createRequestEntity(pdlRequest), PdlRespons.class); + } catch (RestClientException exception) { + log.error("Feil fra PDL med request-url: " + persondataProperties.getUri(), exception); + throw exception; + } + } + + public String hentAktørId(Fnr fnr) { + PdlRequest pdlRequest = new PdlRequest(resourceAsString(identerQueryResource), new Variables(fnr.asString())); + return hentAktørIdFraPdlRespons(utførKallTilPdl(pdlRequest)); + } + + public boolean erKode6Eller7(Fnr fnr) { + String gradering = hentAdressebeskyttelse(fnr).getGradering(); + return "FORTROLIG".equals(gradering) || "STRENGT_FORTROLIG".equals(gradering) || "STRENGT_FORTROLIG_UTLAND".equals(gradering); + } + + public boolean erKode6(PdlRespons pdlRespons) { + try { + String gradering = hentAdressebeskyttelseFraPdlRespons(pdlRespons).getGradering(); + return "STRENGT_FORTROLIG".equals(gradering) || "STRENGT_FORTROLIG_UTLAND".equals(gradering); + } catch (NullPointerException e) { + return false; + } + } + + public boolean erKode6(Fnr fnr) { + String gradering = hentAdressebeskyttelse(fnr).getGradering(); + return "STRENGT_FORTROLIG".equals(gradering) || "STRENGT_FORTROLIG_UTLAND".equals(gradering); + } + + @Cacheable(EhCacheConfig.PDL_CACHE) + public PdlRespons hentPersondataFraPdl(Fnr fnr) { + PdlRequest pdlRequest = new PdlRequest(resourceAsString(persondataQueryResource), new Variables(fnr.asString())); + return utførKallTilPdl(pdlRequest); + } + + public PdlRespons hentPersondata(Fnr fnr) { + PdlRequest pdlRequest = new PdlRequest(resourceAsString(persondataQueryResource), new Variables(fnr.asString())); + return utførKallTilPdl(pdlRequest); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Variables.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Variables.java new file mode 100644 index 000000000..cc62f9d32 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/persondata/Variables.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import lombok.Value; + +@Value +public class Variables { + private final String ident; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/sporingslogg/LagSporingsloggFraAvtaleHendelser.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/sporingslogg/LagSporingsloggFraAvtaleHendelser.java new file mode 100644 index 000000000..9f80bb02b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/sporingslogg/LagSporingsloggFraAvtaleHendelser.java @@ -0,0 +1,169 @@ +package no.nav.tag.tiltaksgjennomforing.sporingslogg; + +import lombok.RequiredArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvbruttAvVeileder; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleDeltMedAvtalepart; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleEndret; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleGjenopprettet; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleInngått; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleLåstOpp; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleNyVeileder; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleOpprettetAvArbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleOpprettetAvArbeidsgiverErFordelt; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleOpprettetAvVeileder; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjenningerOpphevetAvArbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjenningerOpphevetAvVeileder; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjentAvArbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjentAvDeltaker; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjentAvVeileder; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjentPaVegneAvArbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjentPaVegneAvDeltaker; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjentPaVegneAvDeltakerOgArbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.avtale.events.RefusjonFristForlenget; +import no.nav.tag.tiltaksgjennomforing.avtale.events.RefusjonKlar; +import no.nav.tag.tiltaksgjennomforing.avtale.events.RefusjonKlarRevarsel; +import no.nav.tag.tiltaksgjennomforing.avtale.events.RefusjonKorrigert; +import no.nav.tag.tiltaksgjennomforing.avtale.events.SignertAvMentor; +import no.nav.tag.tiltaksgjennomforing.avtale.events.TilskuddsperiodeAvslått; +import no.nav.tag.tiltaksgjennomforing.avtale.events.TilskuddsperiodeGodkjent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class LagSporingsloggFraAvtaleHendelser { + private final SporingsloggRepository sporingsloggRepository; + + @EventListener + public void avtaleOpprettet(AvtaleOpprettetAvVeileder event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.OPPRETTET)); + } + + @EventListener + public void avtaleKlarForRefusjon(RefusjonKlar event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.REFUSJON_KLAR)); + } + + @EventListener + public void avtaleKlarForRefusjonRevarsel(RefusjonKlarRevarsel event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.REFUSJON_KLAR_REVARSEL)); + } + + @EventListener + public void refusjonFristForlengetVarsel(RefusjonFristForlenget event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.REFUSJON_FRIST_FORLENGET)); + } + + @EventListener + public void refusjonKorrigert(RefusjonKorrigert event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.REFUSJON_KORRIGERT)); + } + + @EventListener + public void avtaleOpprettetAvArbeidsgiver(AvtaleOpprettetAvArbeidsgiver event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.OPPRETTET_AV_ARBEIDSGIVER)); + } + + @EventListener + public void avtaleDeltMedAvtalepart(AvtaleDeltMedAvtalepart event) { + if (event.getAvtalepart() == Avtalerolle.ARBEIDSGIVER) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.DELT_MED_ARBEIDSGIVER)); + } else if (event.getAvtalepart() == Avtalerolle.DELTAKER) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.DELT_MED_DELTAKER)); + } else if (event.getAvtalepart() == Avtalerolle.MENTOR) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.DELT_MED_MENTOR)); + } + } + + @EventListener + public void tilskuddsperiodeAvslått(TilskuddsperiodeAvslått event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.TILSKUDDSPERIODE_AVSLATT)); + } + + @EventListener + public void tilskuddsperiodeGodkjent(TilskuddsperiodeGodkjent event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.TILSKUDDSPERIODE_GODKJENT)); + } + + @EventListener + public void avtaleEndret(AvtaleEndret event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.ENDRET)); + } + + @EventListener + public void godkjenningerOpphevetAvArbeidsgiver(GodkjenningerOpphevetAvArbeidsgiver event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.GODKJENNINGER_OPPHEVET_AV_ARBEIDSGIVER)); + } + + @EventListener + public void godkjenningerOpphevetAvVeileder(GodkjenningerOpphevetAvVeileder event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.GODKJENNINGER_OPPHEVET_AV_VEILEDER)); + } + + @EventListener + public void godkjentAvDeltaker(GodkjentAvDeltaker event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.GODKJENT_AV_DELTAKER)); + } + + @EventListener + public void godkjentAvDeltaker(SignertAvMentor event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.GODKJENT_AV_DELTAKER)); + } + + @EventListener + public void godkjentAvArbeidsgiver(GodkjentAvArbeidsgiver event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.GODKJENT_AV_ARBEIDSGIVER)); + } + + @EventListener + public void godkjentAvVeileder(GodkjentAvVeileder event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.GODKJENT_AV_VEILEDER)); + } + + @EventListener + public void avtaleInngått(AvtaleInngått event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.AVTALE_INNGÅTT)); + } + + @EventListener + public void godkjentPaVegneAv(GodkjentPaVegneAvDeltaker event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.GODKJENT_PAA_VEGNE_AV)); + } + + @EventListener + public void godkjentPaVegneAvArbeidsgiver(GodkjentPaVegneAvArbeidsgiver event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.GODKJENT_PAA_VEGNE_AV_ARBEIDSGIVER)); + } + + @EventListener + public void godkjentPaVegneAvDeltakerOgArbeidsgiver(GodkjentPaVegneAvDeltakerOgArbeidsgiver event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.GODKJENT_PAA_VEGNE_AV_DELTAKER_OG_ARBEIDSGIVER)); + } + + @EventListener + public void nyVeileder(AvtaleNyVeileder event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.NY_VEILEDER)); + } + + @EventListener + public void fordelt(AvtaleOpprettetAvArbeidsgiverErFordelt event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.AVTALE_FORDELT)); + } + + @EventListener + public void avbrutt(AvbruttAvVeileder event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.AVBRUTT)); + } + + @EventListener + public void låstOpp(AvtaleLåstOpp event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.LÅST_OPP)); + } + + @EventListener + public void gjenopprettet(AvtaleGjenopprettet event) { + sporingsloggRepository.save(Sporingslogg.nyHendelse(event.getAvtale(), HendelseType.GJENOPPRETTET)); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/sporingslogg/Sporingslogg.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/sporingslogg/Sporingslogg.java new file mode 100644 index 000000000..ea97ad84d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/sporingslogg/Sporingslogg.java @@ -0,0 +1,35 @@ +package no.nav.tag.tiltaksgjennomforing.sporingslogg; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.utils.Now; + +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = false) +@Entity +public class Sporingslogg { + @Id + private UUID id; + private LocalDateTime tidspunkt; + private UUID avtaleId; + @Enumerated(EnumType.STRING) + private HendelseType hendelseType; + + public static Sporingslogg nyHendelse(Avtale avtale, HendelseType hendelseType) { + Sporingslogg sporingslogg = new Sporingslogg(); + sporingslogg.id = UUID.randomUUID(); + sporingslogg.tidspunkt = Now.localDateTime(); + sporingslogg.avtaleId = avtale.getId(); + sporingslogg.hendelseType = hendelseType; + return sporingslogg; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/sporingslogg/SporingsloggRepository.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/sporingslogg/SporingsloggRepository.java new file mode 100644 index 000000000..f6715fac8 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/sporingslogg/SporingsloggRepository.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.sporingslogg; + +import org.springframework.data.repository.CrudRepository; + +import java.util.UUID; + +public interface SporingsloggRepository extends CrudRepository { +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/ArenaMigeringConfig.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/ArenaMigeringConfig.java new file mode 100644 index 000000000..61f560393 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/ArenaMigeringConfig.java @@ -0,0 +1,24 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@EnableAsync +@Configuration +public class ArenaMigeringConfig { + + @Bean + public Executor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(2); + executor.setQueueCapacity(500); + executor.setThreadNamePrefix("ArenaMigrering-"); + executor.initialize(); + return executor; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/RefusjonEndretStatusKafkaConsumer.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/RefusjonEndretStatusKafkaConsumer.java new file mode 100644 index 000000000..569e39392 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/RefusjonEndretStatusKafkaConsumer.java @@ -0,0 +1,37 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.TilskuddPeriode; +import no.nav.tag.tiltaksgjennomforing.avtale.TilskuddPeriodeRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.TilskuddPeriodeStatus; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Component +@Slf4j +public class RefusjonEndretStatusKafkaConsumer { + private final TilskuddPeriodeRepository tilskuddPeriodeRepository; + + + public RefusjonEndretStatusKafkaConsumer(TilskuddPeriodeRepository tilskuddPeriodeRepository) { + this.tilskuddPeriodeRepository = tilskuddPeriodeRepository; + } + + @KafkaListener(topics = Topics.REFUSJON_ENDRET_STATUS, containerFactory = "refusjonEndretStatusContainerFactory") + public void refusjonEndretStatus(RefusjonEndretStatusMelding melding) { + TilskuddPeriode tilskuddPeriode = tilskuddPeriodeRepository.findById(UUID.fromString(melding.getTilskuddsperiodeId())).orElseThrow(); + if(tilskuddPeriode.getStatus() == TilskuddPeriodeStatus.UBEHANDLET) { + log.error("En tilskuddsperiode {} som er ubehandlet har fått statusendring fra refusjon-api", melding.getTilskuddsperiodeId()); + } + tilskuddPeriode.setRefusjonStatus(melding.getStatus()); + log.info("Setter refusjonstatus til {} på tilskuddsperiode {}", melding.getStatus(), melding.getTilskuddsperiodeId()); + + tilskuddPeriodeRepository.save(tilskuddPeriode); + + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/RefusjonEndretStatusKafkaConsumerConfig.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/RefusjonEndretStatusKafkaConsumerConfig.java new file mode 100644 index 000000000..cc420fe21 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/RefusjonEndretStatusKafkaConsumerConfig.java @@ -0,0 +1,62 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.config.SslConfigs; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.support.serializer.JsonDeserializer; + +import java.util.HashMap; +import java.util.Map; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Configuration +@EnableKafka +public class RefusjonEndretStatusKafkaConsumerConfig { + + @Value("${no.nav.gcp.kafka.aiven.bootstrap-servers}") + private String gcpBootstrapServers; + @Value("${no.nav.gcp.kafka.aiven.truststore-path}") + private String sslTruststoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.truststore-password}") + private String sslTruststorePasswordEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-path}") + private String sslKeystoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-password}") + private String sslKeystorePasswordEnvKey; + @Value("${no.nav.gcp.kafka.aiven.security-protocol}") + private String securityProtocol; + + public ConsumerFactory refusjonConsumerFactory() { + Map props = new HashMap<>(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, gcpBootstrapServers); + props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol); + props.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, ""); + props.put(SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG, "jks"); + props.put(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG, "PKCS12"); + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, sslTruststoreLocationEnvKey); + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, sslTruststorePasswordEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, sslKeystoreLocationEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, sslKeystorePasswordEnvKey); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "tiltaksgjennomforing-api"); + props.put(JsonDeserializer.TRUSTED_PACKAGES, "*"); + return new DefaultKafkaConsumerFactory<>(props, + new StringDeserializer(), + new JsonDeserializer<>(RefusjonEndretStatusMelding.class, false)); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory refusjonEndretStatusContainerFactory() { + var factory = new ConcurrentKafkaListenerContainerFactory(); + factory.setConsumerFactory(refusjonConsumerFactory()); + return factory; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/RefusjonEndretStatusMelding.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/RefusjonEndretStatusMelding.java new file mode 100644 index 000000000..2a38eb422 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/RefusjonEndretStatusMelding.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.RefusjonStatus; + +@JsonIgnoreProperties(ignoreUnknown = true) +@Value +public class RefusjonEndretStatusMelding { + String refusjonId; + String bedriftNr; + String avtaleId; + RefusjonStatus status; + String tilskuddsperiodeId; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeAdminController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeAdminController.java new file mode 100644 index 000000000..2de0531a6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeAdminController.java @@ -0,0 +1,67 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + +import lombok.RequiredArgsConstructor; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; +import no.nav.security.token.support.core.api.ProtectedWithClaims; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.UtviklerTilgangProperties; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.TilskuddPeriode; +import no.nav.tag.tiltaksgjennomforing.avtale.TilskuddPeriodeRepository; +import no.nav.tag.tiltaksgjennomforing.exceptions.RessursFinnesIkkeException; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpClientErrorException; + +import java.util.UUID; + +@RestController +@RequestMapping("/utvikler-admin/tilskuddsperioder") +@RequiredArgsConstructor +@ProtectedWithClaims(issuer = "aad") +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Slf4j +public class TilskuddsperiodeAdminController { + private final TilskuddsperiodeKafkaProducer tilskuddsperiodeKafkaProducer; + private final AvtaleRepository avtaleRepository; + private final TilskuddPeriodeRepository tilskuddPeriodeRepository; + private final UtviklerTilgangProperties utviklerTilgangProperties; + private final TokenUtils tokenUtils; + + private void sjekkTilgang() { + if (!tokenUtils.harAdGruppe(utviklerTilgangProperties.getGruppeTilgang())) { + throw new HttpClientErrorException(HttpStatus.FORBIDDEN); + } + } + + // Generer en kafkamelding og send den. Oppdaterer ikke statuser eller lignende på perioden + @PostMapping("/send-tilskuddsperiode-godkjent-melding/{tilskuddsperiodeId}") + public void sendTilskuddsperiodeGodkjentMelding(@PathVariable("tilskuddsperiodeId") UUID id) { + sjekkTilgang(); + log.info("Lager og sender tilskuddsperiode godkjent-melding for tilskuddsperiode: {}", id); + TilskuddPeriode tilskuddPeriode = tilskuddPeriodeRepository.findById(id).orElseThrow(RessursFinnesIkkeException::new); + Avtale avtale = tilskuddPeriode.getAvtale(); + TilskuddsperiodeGodkjentMelding melding = TilskuddsperiodeGodkjentMelding.create(avtale, tilskuddPeriode, null); + tilskuddsperiodeKafkaProducer.publiserTilskuddsperiodeGodkjentMelding(melding); + + } + + // Generer en kafkamelding og send den. Oppdaterer ikke statuser eller lignende på perioden + @PostMapping("/send-tilskuddsperiode-annullert-melding/{tilskuddsperiodeId}") + public void sendTilskuddsperiodeAnnullertMelding(@PathVariable("tilskuddsperiodeId") UUID id) { + sjekkTilgang(); + log.info("Lager og sender tilskuddsperiode annullert-melding for tilskuddsperiode: {}", id); + TilskuddPeriode tilskuddPeriode = tilskuddPeriodeRepository.findById(id).orElseThrow(RessursFinnesIkkeException::new); + TilskuddsperiodeAnnullertMelding melding = new TilskuddsperiodeAnnullertMelding(tilskuddPeriode.getId(), TilskuddsperiodeAnnullertÅrsak.AVTALE_ANNULLERT); + tilskuddsperiodeKafkaProducer.publiserTilskuddsperiodeAnnullertMelding(melding); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeAnnullertMelding.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeAnnullertMelding.java new file mode 100644 index 000000000..4de2833c6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeAnnullertMelding.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + +import lombok.Value; + +import java.util.UUID; + +@Value +public class TilskuddsperiodeAnnullertMelding { + UUID tilskuddsperiodeId; + TilskuddsperiodeAnnullertÅrsak årsak; +} + +enum TilskuddsperiodeAnnullertÅrsak { + AVTALE_ANNULLERT, REFUSJON_FRIST_UTGÅTT, REFUSJON_IKKE_SØKT, +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeFakeKafkaProducer.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeFakeKafkaProducer.java new file mode 100644 index 000000000..f3b37efd0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeFakeKafkaProducer.java @@ -0,0 +1,58 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.events.TilskuddsperiodeAnnullert; +import no.nav.tag.tiltaksgjennomforing.avtale.events.TilskuddsperiodeForkortet; +import no.nav.tag.tiltaksgjennomforing.avtale.events.TilskuddsperiodeGodkjent; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.fake") +@Component +@Slf4j +public class TilskuddsperiodeFakeKafkaProducer { + private final RestTemplate restTemplate; + private final String url; + + public TilskuddsperiodeFakeKafkaProducer(RestTemplate restTemplate, @Value("${tiltaksgjennomforing.kafka.fake-url}") String url) { + this.restTemplate = restTemplate; + this.url = url; + } + + @TransactionalEventListener + public void tilskuddsperiodeGodkjent(TilskuddsperiodeGodkjent event) { + TilskuddsperiodeGodkjentMelding melding = TilskuddsperiodeGodkjentMelding.create(event.getAvtale(), event.getTilskuddsperiode(), event.getResendingsnummer()); + try { + restTemplate.exchange(url + "/tilskuddsperiode-godkjent", HttpMethod.POST, new HttpEntity<>(melding), Void.class); + } catch (RestClientException e) { + log.warn("Feil ved kall til tiltak-refusjon-api", e); + } + } + + @TransactionalEventListener + public void tilskuddsperiodeAnnullert(TilskuddsperiodeAnnullert event) { + TilskuddsperiodeAnnullertMelding melding = new TilskuddsperiodeAnnullertMelding(event.getTilskuddsperiode().getId(), TilskuddsperiodeAnnullertÅrsak.AVTALE_ANNULLERT); + try { + restTemplate.exchange(url + "/tilskuddsperiode-annullert", HttpMethod.POST, new HttpEntity<>(melding), Void.class); + } catch (RestClientException e) { + log.warn("Feil ved kall til tiltak-refusjon-api", e); + } + } + + @TransactionalEventListener + public void tilskuddsperiodeForkortet(TilskuddsperiodeForkortet event) { + TilskuddsperiodeForkortetMelding melding = new TilskuddsperiodeForkortetMelding(event.getTilskuddsperiode().getId(), event.getTilskuddsperiode().getBeløp(), event.getTilskuddsperiode().getSluttDato()); + try { + restTemplate.exchange(url + "/tilskuddsperiode-forkortet", HttpMethod.POST, new HttpEntity<>(melding), Void.class); + } catch (RestClientException e) { + log.warn("Feil ved kall til tiltak-refusjon-api", e); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeForkortetMelding.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeForkortetMelding.java new file mode 100644 index 000000000..3e22d2d2a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeForkortetMelding.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Value; + +import java.time.LocalDate; +import java.util.UUID; + +@Value +public class TilskuddsperiodeForkortetMelding { + UUID tilskuddsperiodeId; + Integer tilskuddsbeløp; + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate tilskuddTom; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeGodkjentMelding.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeGodkjentMelding.java new file mode 100644 index 000000000..dc30cf65a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeGodkjentMelding.java @@ -0,0 +1,85 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; +import no.nav.tag.tiltaksgjennomforing.avtale.TilskuddPeriode; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Value +public class TilskuddsperiodeGodkjentMelding { + + UUID avtaleId; + UUID tilskuddsperiodeId; + UUID avtaleInnholdId; + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate avtaleFom; + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate avtaleTom; + Tiltakstype tiltakstype; + String deltakerFornavn; + String deltakerEtternavn; + Identifikator deltakerFnr; + String arbeidsgiverFornavn; + String arbeidsgiverEtternavn; + String arbeidsgiverTlf; + NavIdent veilederNavIdent; + String bedriftNavn; + BedriftNr bedriftNr; + Integer tilskuddsbeløp; + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate tilskuddFom; + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate tilskuddTom; + Double feriepengerSats; + Double otpSats; + Double arbeidsgiveravgiftSats; + Integer lønnstilskuddsprosent; + Integer avtaleNr; + Integer løpenummer; + Integer resendingsnummer; + String enhet; + NavIdent beslutterNavIdent; + LocalDateTime godkjentTidspunkt; + + public static TilskuddsperiodeGodkjentMelding create(Avtale avtale, TilskuddPeriode tilskuddsperiode, Integer resendingsnummer) { + return new TilskuddsperiodeGodkjentMelding( + avtale.getId(), + tilskuddsperiode.getId(), + avtale.getGjeldendeInnhold().getId(), + avtale.getGjeldendeInnhold().getStartDato(), + avtale.getGjeldendeInnhold().getSluttDato(), + avtale.getTiltakstype(), + avtale.getGjeldendeInnhold().getDeltakerFornavn(), + avtale.getGjeldendeInnhold().getDeltakerEtternavn(), + avtale.getDeltakerFnr(), + avtale.getGjeldendeInnhold().getArbeidsgiverFornavn(), + avtale.getGjeldendeInnhold().getArbeidsgiverEtternavn(), + avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), + avtale.getVeilederNavIdent(), + avtale.getGjeldendeInnhold().getBedriftNavn(), + avtale.getBedriftNr(), + tilskuddsperiode.getBeløp(), + tilskuddsperiode.getStartDato(), + tilskuddsperiode.getSluttDato(), + avtale.getGjeldendeInnhold().getFeriepengesats().doubleValue(), + avtale.getGjeldendeInnhold().getOtpSats(), + avtale.getGjeldendeInnhold().getArbeidsgiveravgift().doubleValue(), + tilskuddsperiode.getLonnstilskuddProsent(), + avtale.getAvtaleNr(), + tilskuddsperiode.getLøpenummer(), + resendingsnummer, + tilskuddsperiode.getEnhet(), + tilskuddsperiode.getGodkjentAvNavIdent(), + tilskuddsperiode.getGodkjentTidspunkt() + ); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeKafkaProducer.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeKafkaProducer.java new file mode 100644 index 000000000..3e7e33a21 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeKafkaProducer.java @@ -0,0 +1,88 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.events.TilskuddsperiodeAnnullert; +import no.nav.tag.tiltaksgjennomforing.avtale.events.TilskuddsperiodeForkortet; +import no.nav.tag.tiltaksgjennomforing.avtale.events.TilskuddsperiodeGodkjent; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; +import org.springframework.util.concurrent.ListenableFutureCallback; + +import java.util.UUID; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Component +@Slf4j +public class TilskuddsperiodeKafkaProducer { + + private final KafkaTemplate aivenKafkaTemplate; + private final ObjectMapper objectMapper; + + public TilskuddsperiodeKafkaProducer(@Qualifier("aivenKafkaTemplate") KafkaTemplate aivenKafkaTemplate, ObjectMapper objectMapper) { + this.aivenKafkaTemplate = aivenKafkaTemplate; + this.objectMapper = objectMapper; + } + + @TransactionalEventListener + public void tilskuddsperiodeGodkjent(TilskuddsperiodeGodkjent event) { + TilskuddsperiodeGodkjentMelding melding = TilskuddsperiodeGodkjentMelding.create(event.getAvtale(), event.getTilskuddsperiode(), event.getResendingsnummer()); + publiserTilskuddsperiodeGodkjentMelding(melding); + } + + @TransactionalEventListener + public void tilskuddsperiodeAnnullert(TilskuddsperiodeAnnullert event) { + UUID tilskuddsperiodeId = event.getTilskuddsperiode().getId(); + TilskuddsperiodeAnnullertMelding melding = new TilskuddsperiodeAnnullertMelding(tilskuddsperiodeId, TilskuddsperiodeAnnullertÅrsak.AVTALE_ANNULLERT); + publiserTilskuddsperiodeAnnullertMelding(melding); + } + + @TransactionalEventListener + public void tilskuddsperiodeForkortet(TilskuddsperiodeForkortet event) { + UUID tilskuddsperiodeId = event.getTilskuddsperiode().getId(); + TilskuddsperiodeForkortetMelding melding = new TilskuddsperiodeForkortetMelding(tilskuddsperiodeId, event.getTilskuddsperiode().getBeløp(), event.getTilskuddsperiode().getSluttDato()); + publiserTilskuddsperiodeForkortetMelding(melding); + } + + public void publiserTilskuddsperiodeGodkjentMelding(TilskuddsperiodeGodkjentMelding melding) { + publiserMelding(Topics.TILSKUDDSPERIODE_GODKJENT, melding.getTilskuddsperiodeId().toString(), melding); + } + + public void publiserTilskuddsperiodeAnnullertMelding(TilskuddsperiodeAnnullertMelding melding) { + publiserMelding(Topics.TILSKUDDSPERIODE_ANNULLERT, melding.getTilskuddsperiodeId().toString(), melding); + } + + public void publiserTilskuddsperiodeForkortetMelding(TilskuddsperiodeForkortetMelding melding) { + publiserMelding(Topics.TILSKUDDSPERIODE_FORKORTET, melding.getTilskuddsperiodeId().toString(), melding); + } + + private void publiserMelding(String topic, String meldingId, Object melding) { + String meldingSomString; + try { + meldingSomString = objectMapper.writeValueAsString(melding); + } catch (JsonProcessingException e) { + log.error("Kunne ikke lage JSON for melding med id {} til topic {}", meldingId, topic); + return; + } + + aivenKafkaTemplate.send(topic, meldingId, meldingSomString) + .addCallback(new ListenableFutureCallback<>() { + @Override + public void onSuccess(SendResult result) { + log.info("Melding med id {} sendt til Kafka topic {}", meldingId, topic); + } + + @Override + public void onFailure(Throwable ex) { + log.warn("Melding med id {} kunne ikke sendes til Kafka topic {}", meldingId, topic); + } + }); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeUtbetaltStatus.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeUtbetaltStatus.java new file mode 100644 index 000000000..f49ee7134 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeUtbetaltStatus.java @@ -0,0 +1,13 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum TilskuddsperiodeUtbetaltStatus { + UTBETALT("RECONCILED"), + FEILET("VOIDED"); + + private final String status; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/ConditionalOnPropertyNotEmpty.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/ConditionalOnPropertyNotEmpty.java new file mode 100644 index 000000000..5be01db82 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/ConditionalOnPropertyNotEmpty.java @@ -0,0 +1,30 @@ +package no.nav.tag.tiltaksgjennomforing.utils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.core.type.AnnotatedTypeMetadata; + +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Conditional(ConditionalOnPropertyNotEmpty.OnPropertyNotEmptyCondition.class) +// Kopiert fra https://stackoverflow.com/questions/46118782/spel-conditionalonproperty-string-property-empty-or-nulll/49783092#49783092 +public @interface ConditionalOnPropertyNotEmpty { + String value(); + + class OnPropertyNotEmptyCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + Map attrs = metadata.getAnnotationAttributes(ConditionalOnPropertyNotEmpty.class.getName()); + String propertyName = (String) attrs.get("value"); + String val = context.getEnvironment().getProperty(propertyName); + return val != null && !val.trim().isEmpty(); + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/DatoUtils.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/DatoUtils.java new file mode 100644 index 000000000..5018db687 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/DatoUtils.java @@ -0,0 +1,12 @@ +package no.nav.tag.tiltaksgjennomforing.utils; + +import lombok.experimental.UtilityClass; + +import java.time.LocalDate; + +@UtilityClass +public class DatoUtils { + public static LocalDate sisteDatoIMnd(LocalDate dato) { + return LocalDate.of(dato.getYear(), dato.getMonth(), dato.lengthOfMonth()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/MultiValueMap.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/MultiValueMap.java new file mode 100644 index 000000000..3d9b9ba71 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/MultiValueMap.java @@ -0,0 +1,44 @@ +package no.nav.tag.tiltaksgjennomforing.utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Enkel implementasjon av en multivaluemap som gjør det enkelt å legge + * til flere verdier på en og samme nøkkel. + *

    + * Erstatter en google commons-implementasjon. + */ +public class MultiValueMap { + + private final HashMap> multivalMap; + + public MultiValueMap() { + this.multivalMap = new HashMap<>(); + } + + public void put(K key, V val) { + if (multivalMap.containsKey(key)) { + var multival = multivalMap.get(key); + multival.add(val); + } else { + var newList = new ArrayList(); + newList.add(val); + multivalMap.put(key, newList); + } + } + + public Collection get(K key) { + return multivalMap.get(key); + } + + public Map> toMap() { + return new HashMap<>(multivalMap); + } + + public static MultiValueMap empty() { + return new MultiValueMap<>(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/Now.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/Now.java new file mode 100644 index 000000000..9540cedc9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/Now.java @@ -0,0 +1,33 @@ +package no.nav.tag.tiltaksgjennomforing.utils; + +import java.time.*; + +// Inspirasjon fra https://medium.com/agorapulse-stories/how-to-solve-now-problem-in-your-java-tests-7c7f4a6d703c +public class Now { + private static ThreadLocal clock = ThreadLocal.withInitial(Clock::systemDefaultZone); + + public static void fixedDate(LocalDate localDate) { + Instant instant = ZonedDateTime.of(localDate.atStartOfDay(), ZoneId.systemDefault()).toInstant(); + clock.set(Clock.fixed(instant, ZoneId.systemDefault())); + } + + public static void resetClock() { + Now.clock.set(Clock.systemDefaultZone()); + } + + public static Instant instant() { + return Instant.now(clock.get()); + } + + public static LocalDate localDate() { + return LocalDate.now(clock.get()); + } + + public static LocalDateTime localDateTime() { + return LocalDateTime.now(clock.get()); + } + + public static YearMonth yearMonth() { + return YearMonth.now(clock.get()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/Periode.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/Periode.java new file mode 100644 index 000000000..0c62f84b7 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/Periode.java @@ -0,0 +1,19 @@ +package no.nav.tag.tiltaksgjennomforing.utils; + +import lombok.Value; + +import java.time.LocalDate; + +@Value +public class Periode { + private final LocalDate start; + private final LocalDate slutt; + + public Periode(LocalDate start, LocalDate slutt) { + if (start.isAfter(slutt)) { + throw new IllegalArgumentException("Startdato må være før eller lik som sluttdato"); + } + this.start = start; + this.slutt = slutt; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/TelefonnummerValidator.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/TelefonnummerValidator.java new file mode 100644 index 000000000..2a9e03cdc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/TelefonnummerValidator.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.utils; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class +TelefonnummerValidator { + public static boolean erGyldigMobilnummer(String tlf) { + if (tlf == null) { + return false; + } + return tlf.matches("[4|9][0-9]{7}"); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/Utils.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/Utils.java new file mode 100644 index 000000000..0f8c3c7e2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/utils/Utils.java @@ -0,0 +1,54 @@ +package no.nav.tag.tiltaksgjennomforing.utils; + +import lombok.experimental.UtilityClass; +import no.nav.tag.tiltaksgjennomforing.exceptions.TiltaksgjennomforingException; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.util.Collection; + +@UtilityClass +public class Utils { + public static T sjekkAtIkkeNull(T in, String feilmelding) { + if (in == null) { + throw new TiltaksgjennomforingException(feilmelding); + } + return in; + } + + public static boolean erIkkeTomme(Object... objekter) { + for (Object objekt : objekter) { + if (objekt instanceof String && ((String) objekt).isEmpty()) { + return false; + } + if (objekt instanceof Collection && ((Collection) objekt).isEmpty()) { + return false; + } + if (objekt == null) { + return false; + } + } + return true; + } + + public static boolean erNoenTomme(Object... objekter) { + return !erIkkeTomme(objekter); + } + + public static boolean erTom(Object objekt) { + return !erIkkeTomme(objekt); + } + + public static URI lagUri(String path) { + return UriComponentsBuilder + .fromPath(path) + .build() + .toUri(); + } + + public static void sjekkAtTekstIkkeOverskrider1000Tegn(String tekst, String feilmelding) { + if ((tekst != null) && tekst.length() > 1000) { + throw new TiltaksgjennomforingException(feilmelding); + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/LagSmsFraAvtaleHendelse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/LagSmsFraAvtaleHendelse.java new file mode 100644 index 000000000..e066f2955 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/LagSmsFraAvtaleHendelse.java @@ -0,0 +1,141 @@ +package no.nav.tag.tiltaksgjennomforing.varsel; + +import lombok.RequiredArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.avtale.*; +import no.nav.tag.tiltaksgjennomforing.avtale.events.*; +import no.nav.tag.tiltaksgjennomforing.varsel.kafka.SmsProducer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +public class LagSmsFraAvtaleHendelse { + private final SmsRepository smsRepository; + private final SmsProducer smsProducer; + + private static final BedriftNr NAV_ORGNR = new BedriftNr("889640782"); + + @EventListener + public void avtaleDeltMedAvtalepart(AvtaleDeltMedAvtalepart event) { + switch(event.getAvtalepart()){ + case ARBEIDSGIVER -> lagreOgSendKafkaMelding(smsTilArbeidsgiver(event.getAvtale(), HendelseType.DELT_MED_ARBEIDSGIVER)); + case DELTAKER -> lagreOgSendKafkaMelding(smsTilDeltaker(event.getAvtale(), HendelseType.DELT_MED_DELTAKER)); + case MENTOR -> lagreOgSendKafkaMelding(smsTilMentor(event.getAvtale(), HendelseType.DELT_MED_MENTOR)); + } + } + @EventListener + public void avtaleGodkjentAvDeltaker(GodkjentAvDeltaker event) { + var sms = smsTilVeileder(event.getAvtale(), HendelseType.GODKJENT_AV_DELTAKER); + lagreOgSendKafkaMelding(sms); + } + @EventListener + public void avtaleGodkjentAvArbeidsgiver(GodkjentAvArbeidsgiver event) { + var sms = smsTilVeileder(event.getAvtale(), HendelseType.GODKJENT_AV_ARBEIDSGIVER); + lagreOgSendKafkaMelding(sms); + } + @EventListener + public void avtaleInngått(AvtaleInngått event) { + var smsTilDeltaker = smsTilDeltaker(event.getAvtale(), HendelseType.AVTALE_INNGÅTT); + var smsTilArbeidsgiver = smsTilArbeidsgiver(event.getAvtale(), HendelseType.AVTALE_INNGÅTT); + + if(event.getAvtale().getTiltakstype() == Tiltakstype.MENTOR) { + var smsTilMentor = smsTilMentor(event.getAvtale(), HendelseType.AVTALE_INNGÅTT); + lagreOgSendKafkaMelding(smsTilMentor); + } + + lagreOgSendKafkaMelding(smsTilDeltaker); + lagreOgSendKafkaMelding(smsTilArbeidsgiver); + + } + @EventListener + public void godkjenningerOpphevetAvArbeidsgiver(GodkjenningerOpphevetAvArbeidsgiver event) { + if (event.getGamleVerdier().isGodkjentAvDeltaker()) { + var smsTilDeltaker = smsTilDeltaker(event.getAvtale(), HendelseType.GODKJENNINGER_OPPHEVET_AV_ARBEIDSGIVER); + lagreOgSendKafkaMelding(smsTilDeltaker); + } + var smsTilVeileder = smsTilVeileder(event.getAvtale(), HendelseType.OPPRETTET_AV_ARBEIDSGIVER); + lagreOgSendKafkaMelding(smsTilVeileder); + } + @EventListener + public void godkjenningerOpphevetAvVeileder(GodkjenningerOpphevetAvVeileder event) { + if (event.getGamleVerdier().isGodkjentAvDeltaker()) { + var smsTilDeltaker = smsTilDeltaker(event.getAvtale(), HendelseType.GODKJENNINGER_OPPHEVET_AV_VEILEDER); + lagreOgSendKafkaMelding(smsTilDeltaker); + } + if (event.getGamleVerdier().isGodkjentAvArbeidsgiver()) { + var smsTilArbeidsgiver = smsTilArbeidsgiver(event.getAvtale(), HendelseType.GODKJENNINGER_OPPHEVET_AV_VEILEDER); + lagreOgSendKafkaMelding(smsTilArbeidsgiver); + } + } + @EventListener + public void refusjonKlar(RefusjonKlar event) { + if(event.getAvtale().getTiltakstype() == Tiltakstype.SOMMERJOBB || event.getAvtale().getTiltakstype() == Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD || event.getAvtale().getTiltakstype() == Tiltakstype.VARIG_LONNSTILSKUDD || event.getAvtale().getTiltakstype() == Tiltakstype.MENTOR){ + String tiltakNavn = event.getAvtale().getTiltakstype().getBeskrivelse().toLowerCase(); + String smsTekst = String.format("Dere kan nå søke om refusjon for tilskudd til %s for avtale med nr: %s. Frist for å søke er %s. Søk om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", tiltakNavn, event.getAvtale().getAvtaleNr(), event.getFristForGodkjenning()); + refusjonVarslingMedKontaktperson(event.getAvtale(), smsTekst, HendelseType.REFUSJON_KLAR); + } + } + + @EventListener + public void refusjonKlarRevarsel(RefusjonKlarRevarsel event) { + if(event.getAvtale().getTiltakstype() == Tiltakstype.SOMMERJOBB || event.getAvtale().getTiltakstype() == Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD || event.getAvtale().getTiltakstype() == Tiltakstype.VARIG_LONNSTILSKUDD || event.getAvtale().getTiltakstype() == Tiltakstype.MENTOR) { + String tiltakNavn = event.getAvtale().getTiltakstype().getBeskrivelse().toLowerCase(); + String smsTekst = String.format("Fristen nærmer seg for å søke om refusjon for tilskudd til %s for avtale med nr: %s. Frist for å søke er %s. Søk om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.",tiltakNavn, event.getAvtale().getAvtaleNr(), event.getFristForGodkjenning()); + refusjonVarslingMedKontaktperson(event.getAvtale(), smsTekst, HendelseType.REFUSJON_KLAR_REVARSEL); + } + } + + @EventListener + public void refusjonFristForlenget(RefusjonFristForlenget event) { + if(event.getAvtale().getTiltakstype() == Tiltakstype.SOMMERJOBB || event.getAvtale().getTiltakstype() == Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD || event.getAvtale().getTiltakstype() == Tiltakstype.VARIG_LONNSTILSKUDD || event.getAvtale().getTiltakstype() == Tiltakstype.MENTOR) { + String smsTekst = String.format("Fristen for å godkjenne refusjon for avtale med nr: %s har blitt forlenget. Du kan sjekke fristen og søke om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", event.getAvtale().getAvtaleNr()); + refusjonVarslingMedKontaktperson(event.getAvtale(), smsTekst, HendelseType.REFUSJON_FRIST_FORLENGET); + } + } + + @EventListener + public void refusjonKorrigert(RefusjonKorrigert event) { + if(event.getAvtale().getTiltakstype() == Tiltakstype.SOMMERJOBB || event.getAvtale().getTiltakstype() == Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD || event.getAvtale().getTiltakstype() == Tiltakstype.VARIG_LONNSTILSKUDD || event.getAvtale().getTiltakstype() == Tiltakstype.MENTOR) { + String smsTekst = String.format("Tidligere innsendt refusjon på avtale med nr %d er korrigert. Se detaljer her: https://tiltak-refusjon.nav.no. Hilsen NAV.", event.getAvtale().getAvtaleNr()); + refusjonVarslingMedKontaktperson(event.getAvtale(), smsTekst, HendelseType.REFUSJON_KORRIGERT); + } + } + + private void refusjonVarslingMedKontaktperson(Avtale avtale, String smsTekst, HendelseType hendelseType) { + if (avtale.getGjeldendeInnhold().getRefusjonKontaktperson() != null && avtale.getGjeldendeInnhold().getRefusjonKontaktperson().getRefusjonKontaktpersonTlf() != null) { + if (avtale.getGjeldendeInnhold().getRefusjonKontaktperson().getØnskerVarslingOmRefusjon() != null && avtale.getGjeldendeInnhold().getRefusjonKontaktperson().getØnskerVarslingOmRefusjon()) { + Sms smsTilArbeidsgiver = Sms.nyttVarsel(avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), avtale.getBedriftNr(), smsTekst, hendelseType, avtale.getId()); + lagreOgSendKafkaMelding(smsTilArbeidsgiver); + } + Sms smsTilKontaktpersonForRefusjon = Sms.nyttVarsel(avtale.getGjeldendeInnhold().getRefusjonKontaktperson().getRefusjonKontaktpersonTlf(), avtale.getBedriftNr(), smsTekst, hendelseType, avtale.getId()); + lagreOgSendKafkaMelding(smsTilKontaktpersonForRefusjon); + } else { + Sms sms = Sms.nyttVarsel(avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), avtale.getBedriftNr(), smsTekst, hendelseType, avtale.getId()); + lagreOgSendKafkaMelding(sms); + } + } + + + private void lagreOgSendKafkaMelding(Sms sms) { + smsRepository.save(sms); + smsProducer.sendSmsVarselMeldingTilKafka(sms); + } + + private static Sms smsTilDeltaker(Avtale avtale, HendelseType hendelse) { + return Sms.nyttVarsel(avtale.getGjeldendeInnhold().getDeltakerTlf(), avtale.getDeltakerFnr(), "Du har mottatt et nytt varsel på https://arbeidsgiver.nav.no/tiltaksgjennomforing", hendelse, avtale.getId()); + } + + private static Sms smsTilMentor(Avtale avtale, HendelseType hendelse) { + return Sms.nyttVarsel(avtale.getGjeldendeInnhold().getMentorTlf(), avtale.getMentorFnr(), "Du har mottatt et nytt varsel på https://arbeidsgiver.nav.no/tiltaksgjennomforing", hendelse, avtale.getId()); + } + + private static Sms smsTilArbeidsgiver(Avtale avtale, HendelseType hendelse) { + return Sms.nyttVarsel(avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), avtale.getBedriftNr(), "Du har mottatt et nytt varsel på https://arbeidsgiver.nav.no/tiltaksgjennomforing", hendelse, avtale.getId()); + } + + private static Sms smsTilVeileder(Avtale avtale, HendelseType hendelse) { + return Sms.nyttVarsel(avtale.getGjeldendeInnhold().getVeilederTlf(), NAV_ORGNR, "Du har mottatt et nytt varsel på https://tiltaksgjennomforing.intern.nav.no/tiltaksgjennomforing", hendelse, avtale.getId()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/LagVarselFraAvtaleHendelser.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/LagVarselFraAvtaleHendelser.java new file mode 100644 index 000000000..a973d84f1 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/LagVarselFraAvtaleHendelser.java @@ -0,0 +1,222 @@ +package no.nav.tag.tiltaksgjennomforing.varsel; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.avtale.events.*; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class LagVarselFraAvtaleHendelser { + private final VarselRepository varselRepository; + + @EventListener + public void avtaleOpprettet(AvtaleOpprettetAvVeileder event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtfortAv(), HendelseType.OPPRETTET); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void avtaleOpprettetAvArbeidsgiver(AvtaleOpprettetAvArbeidsgiver event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.ARBEIDSGIVER, event.getAvtale().getBedriftNr(), HendelseType.OPPRETTET_AV_ARBEIDSGIVER); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void avtaleDeltMedAvtalepart(AvtaleDeltMedAvtalepart event) { + if (event.getAvtalepart() == Avtalerolle.ARBEIDSGIVER) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, null, HendelseType.DELT_MED_ARBEIDSGIVER); + varselRepository.saveAll(List.of(factory.veileder(), factory.arbeidsgiver())); + } else if (event.getAvtalepart() == Avtalerolle.DELTAKER) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, null, HendelseType.DELT_MED_DELTAKER); + varselRepository.saveAll(List.of(factory.veileder(), factory.deltaker())); + } else if (event.getAvtalepart() == Avtalerolle.MENTOR) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, null, HendelseType.DELT_MED_MENTOR); + varselRepository.saveAll(List.of(factory.veileder(), factory.mentor())); + } + } + + //TODO: Hent IDENTEN til beslutter her og ikke veileder + @EventListener + public void tilskuddsperiodeAvslått(TilskuddsperiodeAvslått event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.BESLUTTER, event.getUtfortAv(), HendelseType.TILSKUDDSPERIODE_AVSLATT); + varselRepository.saveAll(List.of(factory.veileder())); + } + + //TODO: Hent IDENTEN til beslutter her og ikke veileder + @EventListener + public void tilskuddsperiodeGodkjent(TilskuddsperiodeGodkjent event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.BESLUTTER, event.getUtfortAv(), HendelseType.TILSKUDDSPERIODE_GODKJENT); + varselRepository.saveAll(List.of(factory.veileder())); + } + + @EventListener + public void avtaleEndret(AvtaleEndret event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), event.getUtfortAvRolle(), event.getUtfortAv(), HendelseType.ENDRET); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void godkjenningerOpphevetAvArbeidsgiver(GodkjenningerOpphevetAvArbeidsgiver event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.ARBEIDSGIVER, event.getAvtale().getBedriftNr(), HendelseType.GODKJENNINGER_OPPHEVET_AV_ARBEIDSGIVER); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void godkjenningerOpphevetAvVeileder(GodkjenningerOpphevetAvVeileder event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, null, HendelseType.GODKJENNINGER_OPPHEVET_AV_VEILEDER); + varselRepository.saveAll(factory.alleParter()); + } + @EventListener + public void godkjentAvDeltaker(GodkjentAvDeltaker event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.DELTAKER, event.getUtfortAv(), HendelseType.GODKJENT_AV_DELTAKER); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void signertAvMentor(SignertAvMentor event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.MENTOR, event.getUtfortAv(), HendelseType.SIGNERT_AV_MENTOR); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void godkjentAvArbeidsgiver(GodkjentAvArbeidsgiver event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.ARBEIDSGIVER, event.getUtfortAv(), HendelseType.GODKJENT_AV_ARBEIDSGIVER); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void godkjentAvVeileder(GodkjentAvVeileder event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtfortAv(), HendelseType.GODKJENT_AV_VEILEDER); + varselRepository.save(factory.veileder()); + } + + @EventListener + public void godkjentPaVegneAv(GodkjentPaVegneAvDeltaker event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtfortAv(), HendelseType.GODKJENT_PAA_VEGNE_AV); + varselRepository.save(factory.veileder()); + } + + @EventListener + public void godkjentPaVegneAvArbeidsgiver(GodkjentPaVegneAvArbeidsgiver event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtfortAv(), HendelseType.GODKJENT_PAA_VEGNE_AV_ARBEIDSGIVER); + varselRepository.save(factory.veileder()); + } + + @EventListener + public void godkjentPaVegneAvDeltakerOgArbeidsgiver(GodkjentPaVegneAvDeltakerOgArbeidsgiver event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtfortAv(), HendelseType.GODKJENT_PAA_VEGNE_AV_DELTAKER_OG_ARBEIDSGIVER); + varselRepository.save(factory.veileder()); + } + + @EventListener + public void godkjentForEtterregistrering(GodkjentForEtterregistrering event) { + VarselFactory factory = new VarselFactory (event.getAvtale(), Avtalerolle.BESLUTTER, event.getUtfortAv(), HendelseType.GODKJENT_FOR_ETTERREGISTRERING); + varselRepository.save(factory.veileder()); + } + + @EventListener + public void fjernetEtterregistrering(FjernetEtterregistrering event) { + VarselFactory factory = new VarselFactory (event.getAvtale(), Avtalerolle.BESLUTTER, event.getUtfortAv(), HendelseType.FJERNET_ETTERREGISTRERING); + varselRepository.save(factory.veileder()); + } + + @EventListener + public void avtaleInngått(AvtaleInngått event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), event.getUtførtAvRolle(), event.getUtførtAv(), HendelseType.AVTALE_INNGÅTT); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void nyVeileder(AvtaleNyVeileder event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getAvtale().getVeilederNavIdent(), HendelseType.NY_VEILEDER); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void fordelt(AvtaleOpprettetAvArbeidsgiverErFordelt event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getAvtale().getVeilederNavIdent(), HendelseType.AVTALE_FORDELT); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void avbrutt(AvbruttAvVeileder event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtfortAv(), HendelseType.AVBRUTT); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void annullert(AnnullertAvVeileder event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtfortAv(), HendelseType.ANNULLERT); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void låstOpp(AvtaleLåstOpp event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, null, HendelseType.LÅST_OPP); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void gjenopprettet(AvtaleGjenopprettet event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtfortAv(), HendelseType.GJENOPPRETTET); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void forkortAvtale(AvtaleForkortet event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtførtAv(), HendelseType.AVTALE_FORKORTET); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void forlengAvtale(AvtaleForlenget event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtførtAv(), HendelseType.AVTALE_FORLENGET); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void målEndret(MålEndret event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtførtAv(), HendelseType.MÅL_ENDRET); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void inkluderingstilskuddEndret(InkluderingstilskuddEndret event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtførtAv(), HendelseType.INKLUDERINGSTILSKUDD_ENDRET); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void omMentorEndret(OmMentorEndret event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtførtAv(), HendelseType.OM_MENTOR_ENDRET); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void endreTilskuddsberegning(TilskuddsberegningEndret event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtførtAv(), HendelseType.TILSKUDDSBEREGNING_ENDRET); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void endreKontaktInformasjon(KontaktinformasjonEndret event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtførtAv(), HendelseType.KONTAKTINFORMASJON_ENDRET); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void endreStillingbeskrivelse(StillingsbeskrivelseEndret event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtførtAv(), HendelseType.STILLINGSBESKRIVELSE_ENDRET); + varselRepository.saveAll(factory.alleParter()); + } + + @EventListener + public void endreOppfølgingOgTilretteleggingInformasjon(OppfølgingOgTilretteleggingEndret event) { + VarselFactory factory = new VarselFactory(event.getAvtale(), Avtalerolle.VEILEDER, event.getUtførtAv(), HendelseType.OPPFØLGING_OG_TILRETTELEGGING_ENDRET); + varselRepository.saveAll(factory.alleParter()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/Sms.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/Sms.java new file mode 100644 index 000000000..ab81d7d4b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/Sms.java @@ -0,0 +1,55 @@ +package no.nav.tag.tiltaksgjennomforing.varsel; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; +import no.nav.tag.tiltaksgjennomforing.avtale.IdentifikatorConverter; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import no.nav.tag.tiltaksgjennomforing.varsel.events.SmsSendt; +import org.springframework.data.domain.AbstractAggregateRoot; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.UUID; + +@Slf4j +@Data +@EqualsAndHashCode(callSuper = false) +@AllArgsConstructor +@RequiredArgsConstructor +@Entity +public class Sms extends AbstractAggregateRoot { + @Id + private UUID smsVarselId; + private String telefonnummer; + @Convert(converter = IdentifikatorConverter.class) + private Identifikator identifikator; + private String meldingstekst; + private UUID avtaleId; + private LocalDateTime tidspunkt; + @Enumerated(EnumType.STRING) + private HendelseType hendelseType; + private String avsenderApplikasjon; + + public static Sms nyttVarsel(String telefonnummer, + Identifikator identifikator, + String meldingstekst, + HendelseType hendelseType, + UUID avtaleId) { + Sms sms = new Sms(); + sms.smsVarselId = UUID.randomUUID(); + sms.telefonnummer = telefonnummer; + sms.identifikator = identifikator; + sms.meldingstekst = meldingstekst; + sms.hendelseType = hendelseType; + sms.tidspunkt = Now.localDateTime(); + sms.avtaleId = avtaleId; + sms.avsenderApplikasjon = "tiltaksgjennomforing-api"; + sms.registerEvent(new SmsSendt(sms)); + return sms; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/SmsRepository.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/SmsRepository.java new file mode 100644 index 000000000..0ed9f3abd --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/SmsRepository.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.varsel; + +import org.springframework.data.repository.CrudRepository; + +import java.util.UUID; + +public interface SmsRepository extends CrudRepository { +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/Varsel.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/Varsel.java new file mode 100644 index 000000000..2e82af544 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/Varsel.java @@ -0,0 +1,84 @@ +package no.nav.tag.tiltaksgjennomforing.varsel; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.avtale.*; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.springframework.data.domain.AbstractAggregateRoot; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; +import java.util.stream.Collectors; + +@Data +@EqualsAndHashCode(callSuper = false) +@AllArgsConstructor +@RequiredArgsConstructor +@Entity +public class Varsel extends AbstractAggregateRoot { + @Id + private UUID id; + private boolean lest; + @Convert(converter = IdentifikatorConverter.class) + private Identifikator identifikator; + @Convert(converter = IdentifikatorConverter.class) + private Identifikator utførtAvIdentifikator; + private String tekst; + @Enumerated(EnumType.STRING) + private HendelseType hendelseType; + private boolean bjelle; + private UUID avtaleId; + private LocalDateTime tidspunkt; + @Enumerated(EnumType.STRING) + private Avtalerolle mottaker; + @Enumerated(EnumType.STRING) + private Avtalerolle utførtAv; + + private static String tilskuddsperiodeAvslåttTekst(Avtale avtale, String hendelseTypeTekst) { + TilskuddPeriode gjeldendePeriode = avtale.gjeldendeTilskuddsperiode(); + String avslagÅrsaker = gjeldendePeriode.getAvslagsårsaker().stream() + .map(type -> type.getTekst().toLowerCase()).collect(Collectors.joining(", ")); + return hendelseTypeTekst + .concat(gjeldendePeriode.getAvslåttAvNavIdent().asString()) + .concat(". Årsak til retur: ") + .concat(avslagÅrsaker) + .concat(". Forklaring: ") + .concat(gjeldendePeriode.getAvslagsforklaring()); + } + + private static String lagVarselTekst(Avtale avtale, HendelseType hendelseType) { + switch (hendelseType) { + case TILSKUDDSPERIODE_AVSLATT: + return tilskuddsperiodeAvslåttTekst(avtale, hendelseType.getTekst()); + case AVTALE_FORKORTET: + return "Avtale forkortet til " + avtale.getGjeldendeInnhold().getSluttDato().format(DateTimeFormatter.ofPattern("dd.MM.YYYY")); + case AVTALE_FORLENGET: + return "Avtale forlenget til " + avtale.getGjeldendeInnhold().getSluttDato().format(DateTimeFormatter.ofPattern("dd.MM.YYYY")); + default: + return hendelseType.getTekst(); + } + } + + public static Varsel nyttVarsel(Identifikator identifikator, boolean bjelle, Avtale avtale, Avtalerolle mottaker, Avtalerolle utførtAv, Identifikator utførtAvIdentifikator, HendelseType hendelseType, UUID avtaleId) { + Varsel varsel = new Varsel(); + varsel.id = UUID.randomUUID(); + varsel.tidspunkt = Now.localDateTime(); + varsel.identifikator = identifikator; + varsel.utførtAvIdentifikator = utførtAvIdentifikator; + varsel.tekst = lagVarselTekst(avtale, hendelseType); + varsel.hendelseType = hendelseType; + varsel.avtaleId = avtaleId; + varsel.bjelle = bjelle; + varsel.mottaker = mottaker; + varsel.utførtAv = utførtAv; + return varsel; + } + + public void settTilLest() { + lest = true; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/VarselController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/VarselController.java new file mode 100644 index 000000000..78c36dd2b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/VarselController.java @@ -0,0 +1,67 @@ +package no.nav.tag.tiltaksgjennomforing.varsel; + +import io.micrometer.core.annotation.Timed; +import lombok.RequiredArgsConstructor; +import no.nav.security.token.support.core.api.Protected; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggingService; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalepart; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@Protected +@RestController +@RequestMapping("/varsler") +@Timed +@RequiredArgsConstructor +public class VarselController { + private final InnloggingService innloggingService; + private final VarselRepository varselRepository; + private final AvtaleRepository avtaleRepository; + + @GetMapping("/oversikt") + public List hentVarslerMedBjelleForOversikt( + @CookieValue("innlogget-part") Avtalerolle innloggetPart) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + return varselRepository.findAllByLestIsFalseAndBjelleIsTrueAndIdentifikatorIn(avtalepart.identifikatorer()); + } + + @GetMapping("/avtale-modal") + public List hentVarslerMedBjelleForAvtale( + @RequestParam(value = "avtaleId") UUID avtaleId, @CookieValue("innlogget-part") Avtalerolle innloggetPart) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + return varselRepository.findAllByLestIsFalseAndBjelleIsTrueAndAvtaleIdAndIdentifikatorIn(avtaleId, avtalepart.identifikatorer()); + } + + @GetMapping("/avtale-logg") + public List hentAlleVarslerForAvtale( + @RequestParam(value = "avtaleId") UUID avtaleId, @CookieValue("innlogget-part") Avtalerolle innloggetPart) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + Avtale avtale = avtaleRepository.findById(avtaleId).orElseThrow(); + avtalepart.sjekkTilgang(avtale); + return varselRepository.findAllByAvtaleIdAndMottaker(avtaleId, innloggetPart); + } + + @PostMapping("{varselId}/sett-til-lest") + @Transactional + public ResponseEntity settTilLest(@PathVariable("varselId") UUID varselId, @CookieValue("innlogget-part") Avtalerolle innloggetPart) { + Avtalepart avtalepart = innloggingService.hentAvtalepart(innloggetPart); + Varsel varsel = varselRepository.findByIdAndIdentifikatorIn(varselId, avtalepart.identifikatorer()); + varsel.settTilLest(); + varselRepository.save(varsel); + return ResponseEntity.ok().build(); + } + + @PostMapping("/sett-alle-til-lest") + @Transactional + public ResponseEntity settFlereVarslerTilLest(@RequestBody List varselIder, @CookieValue("innlogget-part") Avtalerolle innloggetPart) { + varselIder.forEach(varselId -> settTilLest(varselId, innloggetPart)); + return ResponseEntity.ok().build(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/VarselFactory.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/VarselFactory.java new file mode 100644 index 000000000..bad6d0896 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/VarselFactory.java @@ -0,0 +1,46 @@ +package no.nav.tag.tiltaksgjennomforing.varsel; + +import java.util.List; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; + +public class VarselFactory { + private final Avtale avtale; + private final Avtalerolle utførtAv; + private final Identifikator utførtAvIdentifikator; + private final HendelseType hendelseType; + + public VarselFactory(Avtale avtale, Avtalerolle utførtAv, Identifikator utførtAvIdentifikator, HendelseType hendelseType) { + this.avtale = avtale; + this.hendelseType = hendelseType; + this.utførtAv = utførtAv; + this.utførtAvIdentifikator = utførtAvIdentifikator; + } + + public Varsel deltaker() { + return Varsel.nyttVarsel(avtale.getDeltakerFnr(), utførtAv != Avtalerolle.DELTAKER, avtale, Avtalerolle.DELTAKER, utførtAv, utførtAvIdentifikator, hendelseType, avtale.getId()); + } + + public Varsel arbeidsgiver() { + return Varsel.nyttVarsel(avtale.getBedriftNr(), utførtAv != Avtalerolle.ARBEIDSGIVER, avtale, Avtalerolle.ARBEIDSGIVER, utførtAv, utførtAvIdentifikator, hendelseType, avtale.getId()); + } + + + //TODO: Hent IDENTEN til beslutter her og ikke bare veileder + public Varsel veileder() { + return Varsel.nyttVarsel(avtale.getVeilederNavIdent(), utførtAv != Avtalerolle.VEILEDER, avtale, Avtalerolle.VEILEDER, utførtAv, utførtAvIdentifikator, hendelseType, avtale.getId()); + } + + public Varsel mentor() { + return Varsel.nyttVarsel(avtale.getMentorFnr(), utførtAv != Avtalerolle.MENTOR, avtale, Avtalerolle.MENTOR, utførtAv, utførtAvIdentifikator, hendelseType, avtale.getId()); + } + + public List alleParter() { + return switch (avtale.getTiltakstype()){ + case MENTOR -> List.of(deltaker(), arbeidsgiver(), veileder(), mentor()); + default -> List.of(deltaker(), arbeidsgiver(), veileder()); + }; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/VarselRepository.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/VarselRepository.java new file mode 100644 index 000000000..e25636e3a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/VarselRepository.java @@ -0,0 +1,35 @@ +package no.nav.tag.tiltaksgjennomforing.varsel; + +import io.micrometer.core.annotation.Timed; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +public interface VarselRepository extends JpaRepository { + @Timed(percentiles = { 0.5d, 0.75d, 0.9d, 0.99d, 0.999d }) + @Override + List findAll(); + + @Timed(percentiles = { 0.5d, 0.75d, 0.9d, 0.99d, 0.999d }) + Varsel findByIdAndIdentifikatorIn(UUID varselId, Collection identifikator); + + @Timed(percentiles = { 0.5d, 0.75d, 0.9d, 0.99d, 0.999d }) + List findAllByAvtaleIdAndIdentifikator(UUID avtaleId, Identifikator identifikator); + + @Timed(percentiles = { 0.5d, 0.75d, 0.9d, 0.99d, 0.999d }) + List findAllByAvtaleIdAndMottaker(UUID avtaleId, Avtalerolle mottaker); + + @Timed(percentiles = { 0.5d, 0.75d, 0.9d, 0.99d, 0.999d }) + List findAllByLestIsFalseAndBjelleIsTrueAndIdentifikatorIn(Collection identifikator); + + @Timed(percentiles = { 0.5d, 0.75d, 0.9d, 0.99d, 0.999d }) + List findAllByLestIsFalseAndBjelleIsTrueAndAvtaleIdAndIdentifikatorIn(UUID avtaleId, Collection identifikator); + + default List saveAll(Varsel... varsler) { + return saveAll(List.of(varsler)); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/events/SmsSendt.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/events/SmsSendt.java new file mode 100644 index 000000000..f1237241d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/events/SmsSendt.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.events; + +import lombok.Value; +import no.nav.tag.tiltaksgjennomforing.varsel.Sms; + +@Value +public class SmsSendt { + Sms sms; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselConsumer.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselConsumer.java new file mode 100644 index 000000000..7dbbd6756 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselConsumer.java @@ -0,0 +1,43 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.kafka; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Component +@RequiredArgsConstructor +@Slf4j +public class RefusjonVarselConsumer { + private final AvtaleRepository avtaleRepository; + + @KafkaListener(topics = Topics.TILTAK_VARSEL, containerFactory = "varselContainerFactory") + public void consume(RefusjonVarselMelding refusjonVarselMelding) { + Avtale avtale = avtaleRepository.findById(refusjonVarselMelding.getAvtaleId()).orElseThrow(RuntimeException::new); + VarselType varselType = refusjonVarselMelding.getVarselType(); + + try { + switch (varselType) { + case KLAR -> avtale.refusjonKlar(refusjonVarselMelding.getFristForGodkjenning()); + case REVARSEL -> avtale.refusjonRevarsel(refusjonVarselMelding.getFristForGodkjenning()); + case FRIST_FORLENGET -> avtale.refusjonFristForlenget(); + case KORRIGERT -> avtale.refusjonKorrigert(); + } + avtaleRepository.save(avtale); + } catch (FeilkodeException e) { + if (e.getFeilkode() == Feilkode.KAN_IKKE_ENDRE_ANNULLERT_AVTALE) { + log.warn("Avtale med id {} har ugyldig status, varsler derfor ikke om: {}", refusjonVarselMelding.getAvtaleId(), varselType); + } else { + throw e; + } + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselConsumerConfig.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselConsumerConfig.java new file mode 100644 index 000000000..af555179e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselConsumerConfig.java @@ -0,0 +1,62 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.kafka; + +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.config.SslConfigs; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.support.serializer.JsonDeserializer; + +import java.util.HashMap; +import java.util.Map; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Configuration +@EnableKafka +public class RefusjonVarselConsumerConfig { + + @Value("${no.nav.gcp.kafka.aiven.bootstrap-servers}") + private String gcpBootstrapServers; + @Value("${no.nav.gcp.kafka.aiven.truststore-path}") + private String sslTruststoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.truststore-password}") + private String sslTruststorePasswordEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-path}") + private String sslKeystoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-password}") + private String sslKeystorePasswordEnvKey; + @Value("${no.nav.gcp.kafka.aiven.security-protocol}") + private String securityProtocol; + + public ConsumerFactory varselConsumerFactory() { + Map props = new HashMap<>(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, gcpBootstrapServers); + props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol); + props.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, ""); + props.put(SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG, "jks"); + props.put(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG, "PKCS12"); + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, sslTruststoreLocationEnvKey); + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, sslTruststorePasswordEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, sslKeystoreLocationEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, sslKeystorePasswordEnvKey); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "tiltaksgjennomforing-api"); + props.put(JsonDeserializer.TRUSTED_PACKAGES, "*"); + return new DefaultKafkaConsumerFactory<>(props, + new StringDeserializer(), + new JsonDeserializer<>(RefusjonVarselMelding.class, false)); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory varselContainerFactory() { + var factory = new ConcurrentKafkaListenerContainerFactory(); + factory.setConsumerFactory(varselConsumerFactory()); + return factory; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselMelding.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselMelding.java new file mode 100644 index 000000000..b5bcee47d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselMelding.java @@ -0,0 +1,16 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.kafka; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.time.LocalDate; +import java.util.UUID; + +@Data +@AllArgsConstructor +public class RefusjonVarselMelding { + UUID avtaleId; + UUID tilskuddsperiodeId; + VarselType varselType; + LocalDate fristForGodkjenning; +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/SmsAivenKafkaConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/SmsAivenKafkaConfiguration.java new file mode 100644 index 000000000..4016cedc2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/SmsAivenKafkaConfiguration.java @@ -0,0 +1,73 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.kafka; + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.varsel.Sms; +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.config.SslConfigs; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.serializer.JsonSerializer; + +import java.util.HashMap; +import java.util.Map; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Configuration +@Slf4j +@EnableKafka +public class SmsAivenKafkaConfiguration { + + @Value("${no.nav.gcp.kafka.aiven.bootstrap-servers}") + private String gcpBootstrapServers; + @Value("${no.nav.gcp.kafka.aiven.security-protocol}") + private String securityProtocol; + @Value("${no.nav.gcp.kafka.aiven.truststore-path}") + private String sslTruststoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.truststore-password}") + private String sslTruststorePasswordEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-path}") + private String sslKeystoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-password}") + private String sslKeystorePasswordEnvKey; + @Value("${no.nav.gcp.kafka.aiven.schema-registry-url}") + private String schemaRegistryUrl; + @Value("${no.nav.gcp.kafka.aiven.schema-registry-credentials-source}") + private String schemaRegistryCredentialsSource; + @Value("${no.nav.gcp.kafka.aiven.schema-registry-user-info}") + private String schemaRegistryUserInfo; + + private Map producerConfigs() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, gcpBootstrapServers); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol); + + props.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, ""); + props.put(SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG, "jks"); + props.put(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG, "PKCS12"); + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, sslTruststoreLocationEnvKey); + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, sslTruststorePasswordEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, sslKeystoreLocationEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, sslKeystorePasswordEnvKey); + + props.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, false); + + props.put("schema.registry.url", schemaRegistryUrl); + props.put("basic.auth.credentials.source", schemaRegistryCredentialsSource); + props.put("basic.auth.user.info", schemaRegistryUserInfo); + return props; + } + + @Bean + public KafkaTemplate aivenTiltaksgjennomforingVarsel() { + return new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(producerConfigs())); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/SmsProducer.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/SmsProducer.java new file mode 100644 index 000000000..c18f2f73c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/SmsProducer.java @@ -0,0 +1,36 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.kafka; + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import no.nav.tag.tiltaksgjennomforing.varsel.Sms; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; +import org.springframework.stereotype.Component; +import org.springframework.util.concurrent.ListenableFutureCallback; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Component +@Slf4j +public class SmsProducer { + private final KafkaTemplate kafkaTemplate; + + public SmsProducer(@Qualifier("aivenTiltaksgjennomforingVarsel") KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + public void sendSmsVarselMeldingTilKafka(Sms sms) { + kafkaTemplate.send(Topics.TILTAK_SMS, sms.getSmsVarselId().toString(), sms).addCallback(new ListenableFutureCallback<>() { + @Override + public void onFailure(Throwable ex) { + log.warn("Sms med id={} kunne ikke sendes til Kafka topic", sms.getSmsVarselId()); + } + + @Override + public void onSuccess(SendResult result) { + log.info("Sms med id={} sendt på Kafka topic {}", sms.getSmsVarselId(), result.getProducerRecord().topic()); + } + }); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/VarselType.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/VarselType.java new file mode 100644 index 000000000..4345deef3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/VarselType.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.kafka; + +public enum VarselType { + KLAR, + REVARSEL, + FRIST_FORLENGET, + KORRIGERT +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/AltinnNotifikasjonsProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/AltinnNotifikasjonsProperties.java new file mode 100644 index 000000000..d259f1bd8 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/AltinnNotifikasjonsProperties.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +import lombok.Value; + +@Value +public class AltinnNotifikasjonsProperties { + Integer serviceCode; + Integer serviceEdition; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/ArbeidsgiverNotifikasjon.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/ArbeidsgiverNotifikasjon.java new file mode 100644 index 000000000..8feae63e6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/ArbeidsgiverNotifikasjon.java @@ -0,0 +1,66 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNrConverter; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.utils.Now; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = false) +@AllArgsConstructor +@RequiredArgsConstructor +@Entity +public class ArbeidsgiverNotifikasjon { + + @Id + private UUID id; + private LocalDateTime tidspunkt; + private UUID avtaleId; + @Enumerated(EnumType.STRING) + private HendelseType hendelseType; + @Convert(converter = BedriftNrConverter.class) + private BedriftNr virksomhetsnummer; + private String lenke; + private Integer serviceCode; + private Integer serviceEdition; + private boolean varselSendtVellykket; + @Enumerated(EnumType.STRING) + private NotifikasjonOperasjonType operasjonType; + private String statusResponse; + private boolean notifikasjonAktiv; + private String notifikasjonReferanseId; + + public static ArbeidsgiverNotifikasjon nyHendelse( + Avtale avtale, + HendelseType hendelseType, + NotifikasjonService notifikasjonService, + NotifikasjonParser notifikasjonParser) { + + final AltinnNotifikasjonsProperties notifikasjonerProperties = + notifikasjonParser.getNotifikasjonerProperties(avtale); + + final String lenke = notifikasjonService.getAvtaleLenke(avtale); + + ArbeidsgiverNotifikasjon notifikasjon = new ArbeidsgiverNotifikasjon(); + notifikasjon.id = UUID.randomUUID(); + notifikasjon.tidspunkt = Now.localDateTime(); + notifikasjon.avtaleId = avtale.getId(); + notifikasjon.hendelseType = hendelseType; + notifikasjon.virksomhetsnummer = avtale.getBedriftNr(); + notifikasjon.lenke = lenke; + notifikasjon.serviceCode = notifikasjonerProperties.getServiceCode(); + notifikasjon.serviceEdition = notifikasjonerProperties.getServiceEdition(); + notifikasjon.varselSendtVellykket = false; + + return notifikasjon; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/ArbeidsgiverNotifikasjonRepository.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/ArbeidsgiverNotifikasjonRepository.java new file mode 100644 index 000000000..6b66bb3d1 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/ArbeidsgiverNotifikasjonRepository.java @@ -0,0 +1,37 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +import io.micrometer.core.annotation.Timed; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.UUID; + +public interface ArbeidsgiverNotifikasjonRepository extends JpaRepository { + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + List findArbeidsgiverNotifikasjonByAvtaleIdAndVarselSendtVellykketAndNotifikasjonAktiv( + UUID avtaleId, + boolean varselSendtVellykket, + boolean notifikasjonAktiv); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + List findAllByAvtaleId(UUID avtaleId); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + @Query("FROM ArbeidsgiverNotifikasjon " + + "where avtaleId in (?1) and (statusResponse = 'NyBeskjedVellykket' or statusResponse = 'NyOppgaveVellykket')") + List findAllByAvtaleIdForDeleting(UUID avtaleId); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + List findArbeidsgiverNotifikasjonByAvtaleIdAndHendelseTypeAndStatusResponse( + UUID avtaleId, + HendelseType hendelsetype, + String statusResponse); + + @Timed(percentiles = {0.5d, 0.75d, 0.9d, 0.99d, 0.999d}) + ArbeidsgiverNotifikasjon findArbeidsgiverNotifikasjonsByAvtaleIdAndNotifikasjonReferanseIdAndOperasjonType( + UUID avtaleId, String notifikasjonReferanseId, NotifikasjonOperasjonType operasjonType); + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonEvent.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonEvent.java new file mode 100644 index 000000000..839cc37e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonEvent.java @@ -0,0 +1,9 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +import lombok.Data; + +@Data +public class NotifikasjonEvent { + ArbeidsgiverNotifikasjon notifikasjon; + boolean notifikasjonFerdigBehandlet; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonHandler.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonHandler.java new file mode 100644 index 000000000..d50c735c3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonHandler.java @@ -0,0 +1,136 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.FeilVedSendingResponse.FeilVedSendingResponse; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.FellesResponse; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.MutationStatus; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Component +@RequiredArgsConstructor +public class NotifikasjonHandler { + private final ObjectMapper objectMapper; + private final ArbeidsgiverNotifikasjonRepository arbeidsgiverNotifikasjonRepository; + + + public T readResponse(String json, Class contentClass) { + try { + return objectMapper.readValue(json, contentClass); + } catch (IOException exception) { + log.error("objectmapper feilet med lesing av data: ", exception); + } + return null; + } + + public FellesResponse konverterResponse(Object data) { + try { + if (data != null) { + return objectMapper.convertValue(data, FellesResponse.class); + } + } catch (Exception e) { + log.error("feilet med convertering av data til FellesMutationResponse klasse: ", e); + } + return null; + } + + public void sjekkOgSettStatusResponse( + ArbeidsgiverNotifikasjon notifikasjon, + FellesResponse response, + MutationStatus vellykketStatus) { + if (response != null) { + if (response.get__typename().equals(vellykketStatus.getStatus())) { + notifikasjon.setVarselSendtVellykket(true); + if (response.get__typename().equals(MutationStatus.NY_OPPGAVE_VELLYKKET.getStatus())) { + notifikasjon.setNotifikasjonAktiv(true); + } + } + notifikasjon.setStatusResponse(response.get__typename()); + arbeidsgiverNotifikasjonRepository.save(notifikasjon); + } + } + + public void saveNotifikasjon(ArbeidsgiverNotifikasjon notifikasjon) { + arbeidsgiverNotifikasjonRepository.save(notifikasjon); + } + + public List finnNotifikasjonerTilSletting(UUID id) { + return arbeidsgiverNotifikasjonRepository.findAllByAvtaleIdForDeleting(id); + } + + protected void oppdaterNotifikasjon( + ArbeidsgiverNotifikasjon notifikasjon, + ArbeidsgiverNotifikasjon notifikasjonReferanse, + FellesResponse response, + MutationStatus onsketStatus + ) { + final String typename = response.get__typename(); + if (typename.equals(onsketStatus.getStatus())) { + notifikasjonReferanse.setNotifikasjonAktiv(false); + notifikasjon.setVarselSendtVellykket(true); + } + notifikasjon.setStatusResponse(typename); + this.saveNotifikasjon(notifikasjon); + this.saveNotifikasjon(notifikasjonReferanse); + } + + public void logErrorOgSettFeilmelding(String response, ArbeidsgiverNotifikasjon notifikasjon) { + log.error("Feilet med henting av data response. Response: {}", response); + final FeilVedSendingResponse feilmelding = readResponse(response, FeilVedSendingResponse.class); + if (feilmelding.getErrors() != null && feilmelding.getErrors().length > 0) { + final String message = feilmelding.getErrors()[0].getMessage(); + if (message != null) { + notifikasjon.setStatusResponse(message); + this.saveNotifikasjon(notifikasjon); + } + } + } + + protected NotifikasjonEvent finnEllerOpprettNotifikasjonForHendelse( + Avtale avtale, + UUID notifikasjonReferanseId, + HendelseType hendelseTypeForNyNotifikasjon, + NotifikasjonService service, + NotifikasjonParser parser, + MutationStatus onsketStatus, + NotifikasjonOperasjonType operasjonType) { + + NotifikasjonEvent event = new NotifikasjonEvent(); + try { + ArbeidsgiverNotifikasjon notifikasjon = arbeidsgiverNotifikasjonRepository. + findArbeidsgiverNotifikasjonsByAvtaleIdAndNotifikasjonReferanseIdAndOperasjonType(avtale.getId(), notifikasjonReferanseId.toString(), operasjonType); + if (notifikasjon != null) { + event.setNotifikasjon(notifikasjon); + event.setNotifikasjonFerdigBehandlet(notifikasjon.getStatusResponse() != null && notifikasjon.getStatusResponse().equals(onsketStatus.getStatus())); + return event; + } + } catch (Exception e) { + log.warn("Feilet med henting av arbeidsgiverNotifikasjon med avtaleId {} og unik NotifikasjonReferanseId {} og OperasjonType {}", avtale.getId(), notifikasjonReferanseId.toString(), operasjonType); + event.setNotifikasjon(null); + event.setNotifikasjonFerdigBehandlet(true); + return event; + } + + ArbeidsgiverNotifikasjon utfoertNotifikasjon = ArbeidsgiverNotifikasjon.nyHendelse(avtale, + hendelseTypeForNyNotifikasjon, service, parser); + utfoertNotifikasjon.setNotifikasjonReferanseId(notifikasjonReferanseId.toString()); + utfoertNotifikasjon.setOperasjonType(operasjonType); + + event.setNotifikasjon(utfoertNotifikasjon); + event.setNotifikasjonFerdigBehandlet(false); + return event; + } + + protected List finnUtfoertNotifikasjon(UUID id, HendelseType hendelsetype, String statusResponse) { + return arbeidsgiverNotifikasjonRepository + .findArbeidsgiverNotifikasjonByAvtaleIdAndHendelseTypeAndStatusResponse(id, hendelsetype, statusResponse); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonHendelseLytter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonHendelseLytter.java new file mode 100644 index 000000000..b16da16f2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonHendelseLytter.java @@ -0,0 +1,169 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +import lombok.RequiredArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.avtale.events.*; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.MutationStatus; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@ConditionalOnProperty("tiltaksgjennomforing.notifikasjoner.enabled") +public class NotifikasjonHendelseLytter { + + private final ArbeidsgiverNotifikasjonRepository arbeidsgiverNotifikasjonRepository; + private final NotifikasjonService notifikasjonService; + private final NotifikasjonParser parser; + + private void opprettOgSendNyBeskjed(Avtale avtale, HendelseType hendelseType, NotifikasjonTekst tekst) { + final ArbeidsgiverNotifikasjon notifikasjon = ArbeidsgiverNotifikasjon.nyHendelse(avtale, + hendelseType, notifikasjonService, parser); + arbeidsgiverNotifikasjonRepository.save(notifikasjon); + notifikasjonService.opprettNyBeskjed(notifikasjon, + NotifikasjonMerkelapp.getMerkelapp(avtale.getTiltakstype().getBeskrivelse()), + tekst); + } + + @EventListener + public void avtaleOpprettet(AvtaleOpprettetAvVeileder event) { + final ArbeidsgiverNotifikasjon notifikasjon = ArbeidsgiverNotifikasjon.nyHendelse(event.getAvtale(), + HendelseType.OPPRETTET, notifikasjonService, parser); + arbeidsgiverNotifikasjonRepository.save(notifikasjon); + notifikasjonService.opprettOppgave(notifikasjon, + NotifikasjonMerkelapp.getMerkelapp(event.getAvtale().getTiltakstype().getBeskrivelse()), + NotifikasjonTekst.TILTAK_AVTALE_OPPRETTET); + } + + @EventListener + public void godkjenningerOpphevetAvVeileder(GodkjenningerOpphevetAvVeileder event) { + final ArbeidsgiverNotifikasjon notifikasjon = ArbeidsgiverNotifikasjon.nyHendelse(event.getAvtale(), + HendelseType.GODKJENNINGER_OPPHEVET_AV_VEILEDER, notifikasjonService, parser); + arbeidsgiverNotifikasjonRepository.save(notifikasjon); + notifikasjonService.opprettOppgave(notifikasjon, + NotifikasjonMerkelapp.getMerkelapp(event.getAvtale().getTiltakstype().getBeskrivelse()), + NotifikasjonTekst.TILTAK_GODKJENNINGER_OPPHEVET_AV_VEILEDER); + } + + @EventListener + public void avtaleKlarForRefusjon(RefusjonKlar event) { + opprettOgSendNyBeskjed(event.getAvtale(), HendelseType.REFUSJON_KLAR, + NotifikasjonTekst.TILTAK_AVTALE_KLAR_REFUSJON); + } + + @EventListener + public void godkjentAvArbeidsgiver(GodkjentAvArbeidsgiver event) { + notifikasjonService.oppgaveUtfoert( + event.getAvtale(), HendelseType.OPPRETTET, + MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + notifikasjonService.oppgaveUtfoert(event.getAvtale(), + HendelseType.GODKJENNINGER_OPPHEVET_AV_VEILEDER, MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + } + + @EventListener + public void godkjentAvVeileder(GodkjentAvVeileder event) { + notifikasjonService.oppgaveUtfoert(event.getAvtale(), + HendelseType.OPPRETTET, MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + notifikasjonService.oppgaveUtfoert(event.getAvtale(), + HendelseType.GODKJENNINGER_OPPHEVET_AV_VEILEDER, MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + } + + @EventListener + public void godkjentPaVegneAv(GodkjentPaVegneAvDeltaker event) { + notifikasjonService.oppgaveUtfoert(event.getAvtale(), + HendelseType.OPPRETTET, MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + notifikasjonService.oppgaveUtfoert(event.getAvtale(), + HendelseType.GODKJENNINGER_OPPHEVET_AV_VEILEDER, MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + } + + @EventListener + public void godkjentPaVegneAvArbeidsgiver(GodkjentPaVegneAvArbeidsgiver event) { + notifikasjonService.oppgaveUtfoert(event.getAvtale(), + HendelseType.OPPRETTET, MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + notifikasjonService.oppgaveUtfoert(event.getAvtale(), + HendelseType.GODKJENNINGER_OPPHEVET_AV_VEILEDER, MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + } + + @EventListener + public void godkjentPaVegneAvDeltakerOgArbeidsgiver(GodkjentPaVegneAvDeltakerOgArbeidsgiver event) { + notifikasjonService.oppgaveUtfoert(event.getAvtale(), + HendelseType.OPPRETTET, MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + notifikasjonService.oppgaveUtfoert(event.getAvtale(), + HendelseType.GODKJENNINGER_OPPHEVET_AV_VEILEDER, MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + } + + @EventListener + public void avtaleInngått(AvtaleInngått event) { + notifikasjonService.oppgaveUtfoert(event.getAvtale(), + HendelseType.OPPRETTET, MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + notifikasjonService.oppgaveUtfoert(event.getAvtale(), + HendelseType.GODKJENNINGER_OPPHEVET_AV_VEILEDER, MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + opprettOgSendNyBeskjed(event.getAvtale(), HendelseType.AVTALE_INNGÅTT, NotifikasjonTekst.TILTAK_AVTALE_INNGATT); + } + + @EventListener + public void sletteNotifikasjon(AvtaleSlettemerket event) { + notifikasjonService.softDeleteNotifikasjoner(event.getAvtale()); + } + + @EventListener + public void avtaleAnnullert(AnnullertAvVeileder event) { + notifikasjonService.softDeleteNotifikasjoner(event.getAvtale()); + } + + @EventListener + public void målEndret(MålEndret event) { + opprettOgSendNyBeskjed(event.getAvtale(), HendelseType.MÅL_ENDRET, + NotifikasjonTekst.TILTAK_MÅL_ENDRET); + } + + @EventListener + public void inkluderingstilskuddEndret(InkluderingstilskuddEndret event) { + opprettOgSendNyBeskjed(event.getAvtale(), HendelseType.INKLUDERINGSTILSKUDD_ENDRET, + NotifikasjonTekst.TILTAK_INKLUDERINGSTILSKUDD_ENDRET); + } + + @EventListener + public void omMentorEndret(OmMentorEndret event) { + opprettOgSendNyBeskjed(event.getAvtale(), HendelseType.OM_MENTOR_ENDRET, + NotifikasjonTekst.TILTAK_OM_MENTOR_ENDRET); + } + + @EventListener + public void endreStillingbeskrivelse(StillingsbeskrivelseEndret event) { + opprettOgSendNyBeskjed(event.getAvtale(), HendelseType.STILLINGSBESKRIVELSE_ENDRET, + NotifikasjonTekst.TILTAK_STILLINGSBESKRIVELSE_ENDRET); + } + + @EventListener + public void endreOppfølgingOgTilretteleggingInformasjon(OppfølgingOgTilretteleggingEndret event) { + opprettOgSendNyBeskjed(event.getAvtale(), HendelseType.OPPFØLGING_OG_TILRETTELEGGING_ENDRET, + NotifikasjonTekst.TILTAK_OPPFØLGING_OG_TILRETTELEGGING_ENDRET); + } + + @EventListener + public void endreKontaktInformasjon(KontaktinformasjonEndret event) { + opprettOgSendNyBeskjed(event.getAvtale(), HendelseType.KONTAKTINFORMASJON_ENDRET, + NotifikasjonTekst.TILTAK_KONTAKTINFORMASJON_ENDRET); + } + + @EventListener + public void endreTilskuddsberegning(TilskuddsberegningEndret event) { + opprettOgSendNyBeskjed(event.getAvtale(), HendelseType.TILSKUDDSBEREGNING_ENDRET, + NotifikasjonTekst.TILTAK_TILSKUDDSBEREGNING_ENDRET); + } + + @EventListener + public void forkortAvtale(AvtaleForkortet event) { + opprettOgSendNyBeskjed(event.getAvtale(), HendelseType.AVTALE_FORKORTET, + NotifikasjonTekst.TILTAK_AVTALE_FORKORTET); + } + + @EventListener + public void forlengAvtale(AvtaleForlenget event) { + opprettOgSendNyBeskjed(event.getAvtale(), HendelseType.AVTALE_FORLENGET, + NotifikasjonTekst.TILTAK_AVTALE_FORLENGET); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonMerkelapp.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonMerkelapp.java new file mode 100644 index 000000000..6596812f9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonMerkelapp.java @@ -0,0 +1,36 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +public enum NotifikasjonMerkelapp { + LONNTILSKUDD("Lønnstilskudd"), + MENTOR("Mentor"), + SOMMERJOBB("Sommerjobb"), + INKLUDERINGSTILSKUDD("Inkluderingstilskudd"), + ARBEIDSTRENING("Arbeidstrening"); + + private final String merkelapp; + + NotifikasjonMerkelapp(String merkelapp) { + this.merkelapp = merkelapp; + } + + public String getValue() { + return merkelapp; + } + + public static NotifikasjonMerkelapp getMerkelapp(String merkelapp) { + switch (merkelapp) { + case "Midlertidig lønnstilskudd": + case "Varig lønnstilskudd": + return NotifikasjonMerkelapp.LONNTILSKUDD; + case "Mentor": + return NotifikasjonMerkelapp.MENTOR; + case "Sommerjobb": + return NotifikasjonMerkelapp.SOMMERJOBB; + case "Inkluderingstilskudd": + return NotifikasjonMerkelapp.INKLUDERINGSTILSKUDD; + case "Arbeidstrening": + default: + return NotifikasjonMerkelapp.ARBEIDSTRENING; + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonOperasjonType.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonOperasjonType.java new file mode 100644 index 000000000..020415b5a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonOperasjonType.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +public enum NotifikasjonOperasjonType { + SEND_BESKJED("SEND_BESKJED"), + SEND_OPPGAVE("SEND_OPPGAVE"), + SETT_OPPGAVE_UTFOERT("SETT_OPPGAVE_UTFOERT"), + SOFTDELETE_NOTIFIKASJON("SOFTDELETE_NOTIFIKASJON"); + + private final String type; + + NotifikasjonOperasjonType(String type) { this.type = type; } + + public String getType() { return type; } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonParser.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonParser.java new file mode 100644 index 000000000..5df925786 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonParser.java @@ -0,0 +1,84 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +import lombok.Data; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring.AltinnTilgangsstyringProperties; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.exceptions.TiltaksgjennomforingException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; +import org.springframework.util.StreamUtils; + +import java.nio.charset.StandardCharsets; + +@Component +@Data +@Slf4j +public class NotifikasjonParser { + private final String nyOppgave; + private final String nyBeskjed; + private final String oppgaveUtfoertByEksternId; + private final String mineNotifikasjoner; + private final String softDeleteNotifikasjonByEksternId; + private final String hardDeleteNotifikasjon; + + private final AltinnTilgangsstyringProperties altinnTilgangsstyringProperties; + + public NotifikasjonParser( + @Value("classpath:varsler/opprettNyOppgave.graphql") Resource nyOppgave, + @Value("classpath:varsler/opprettNyBeskjed.graphql") Resource nyBeskjed, + @Value("classpath:varsler/oppgaveUtfoertByEksternId.graphql") Resource oppgaveUtfoertByEksternId, + @Value("classpath:varsler/mineNotifikasjoner.graphql") Resource mineNotifikasjoner, + @Value("classpath:varsler/softDeleteNotifikasjonByEksternId.graphql") Resource softDeleteNotifikasjonByEksternId, + @Value("classpath:varsler/hardDeleteNotifikasjon.graphql") Resource hardDeleteNotifikasjon, + AltinnTilgangsstyringProperties altinnTilgangsstyringProperties + ) { + this.nyOppgave = resourceAsString(nyOppgave); + this.nyBeskjed = resourceAsString(nyBeskjed); + this.oppgaveUtfoertByEksternId = resourceAsString(oppgaveUtfoertByEksternId); + this.mineNotifikasjoner = resourceAsString(mineNotifikasjoner); + this.softDeleteNotifikasjonByEksternId = resourceAsString(softDeleteNotifikasjonByEksternId); + this.hardDeleteNotifikasjon = resourceAsString(hardDeleteNotifikasjon); + this.altinnTilgangsstyringProperties = altinnTilgangsstyringProperties; + } + + @SneakyThrows + private static String resourceAsString(Resource adressebeskyttelseQuery) { + String filinnhold = StreamUtils.copyToString(adressebeskyttelseQuery.getInputStream(), StandardCharsets.UTF_8); + return filinnhold.replaceAll("\\s+", " "); + } + + public AltinnNotifikasjonsProperties getNotifikasjonerProperties(Avtale avtale) { + switch (avtale.getTiltakstype()) { + case MIDLERTIDIG_LONNSTILSKUDD: + return new AltinnNotifikasjonsProperties( + altinnTilgangsstyringProperties.getLtsMidlertidigServiceCode(), + altinnTilgangsstyringProperties.getLtsMidlertidigServiceEdition()); + case ARBEIDSTRENING: + return new AltinnNotifikasjonsProperties( + altinnTilgangsstyringProperties.getArbtreningServiceCode(), + altinnTilgangsstyringProperties.getArbtreningServiceEdition()); + case SOMMERJOBB: + return new AltinnNotifikasjonsProperties( + altinnTilgangsstyringProperties.getSommerjobbServiceCode(), + altinnTilgangsstyringProperties.getSommerjobbServiceEdition()); + case MENTOR: + return new AltinnNotifikasjonsProperties( + altinnTilgangsstyringProperties.getMentorServiceCode(), + altinnTilgangsstyringProperties.getMentorServiceEdition()); + case INKLUDERINGSTILSKUDD: + return new AltinnNotifikasjonsProperties( + altinnTilgangsstyringProperties.getInkluderingstilskuddServiceCode(), + altinnTilgangsstyringProperties.getInkluderingstilskuddServiceEdition()); + case VARIG_LONNSTILSKUDD: + return new AltinnNotifikasjonsProperties( + altinnTilgangsstyringProperties.getLtsVarigServiceCode(), + altinnTilgangsstyringProperties.getLtsVarigServiceEdition()); + default: + log.error("Kan ikke sette opp notifikasjon for ukjent tiltaktstype"); + throw new TiltaksgjennomforingException("Kan ikke sette opp notifikasjon for ukjent tiltaktstype"); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonService.java new file mode 100644 index 000000000..3169017cf --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonService.java @@ -0,0 +1,202 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.request.ArbeidsgiverMutationRequest; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.request.Variables; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.MutationStatus; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.nyBeskjed.NyBeskjedResponse; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.nyOppgave.NyOppgaveResponse; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.oppgaveUtfoertByEksternId.OppgaveUtfoertResponse; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.softDeleteNotifikasjonByEksternId.Data; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.softDeleteNotifikasjonByEksternId.SoftDeleteNotifikasjonResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + + +@Slf4j +@Component +@ConditionalOnProperty("tiltaksgjennomforing.notifikasjoner.enabled") +public class NotifikasjonService { + private final RestTemplate restTemplate; + private final NotifikasjonHandler handler; + private final NotifikasjonerProperties notifikasjonerProperties; + private final NotifikasjonParser notifikasjonParser; + + public NotifikasjonService( + @Qualifier("notifikasjonerRestTemplate") RestTemplate restTemplate, + @Autowired NotifikasjonParser notifikasjonParser, + NotifikasjonerProperties properties, + @Autowired NotifikasjonHandler handler + ) { + this.restTemplate = restTemplate; + this.notifikasjonerProperties = properties; + this.notifikasjonParser = notifikasjonParser; + this.handler = handler; + } + + private HttpEntity createRequestEntity(ArbeidsgiverMutationRequest arbeidsgiverMutationRequest) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + return new HttpEntity(arbeidsgiverMutationRequest, headers); + } + + public String opprettNotifikasjon(ArbeidsgiverMutationRequest arbeidsgiverMutationRequest) { + try { + return restTemplate.postForObject( + notifikasjonerProperties.getUri(), + createRequestEntity(arbeidsgiverMutationRequest), + String.class); + } catch (RestClientException exception) { + log.error("Feil med sending av notifikasjon: ", exception); + throw exception; + } + } + + public String getAvtaleLenke(Avtale avtale) { + return notifikasjonerProperties.getLenke().concat(avtale.getId().toString()) + .concat("/?bedrift=").concat(avtale.getBedriftNr().asString()); + } + + private String opprettNyMutasjon(ArbeidsgiverNotifikasjon notifikasjon, String mutation, String merkelapp, String tekst) { + Variables variables = new Variables(); + variables.setEksternId(notifikasjon.getId()); + variables.setVirksomhetsnummer(notifikasjon.getVirksomhetsnummer().asString()); + variables.setLenke(notifikasjon.getLenke()); + variables.setServiceCode(notifikasjon.getServiceCode().toString()); + variables.setMerkelapp(merkelapp); + variables.setTekst(tekst); + variables.setServiceEdition(notifikasjon.getServiceEdition().toString()); + variables.setGrupperingsId(notifikasjon.getAvtaleId()); + ArbeidsgiverMutationRequest request = new ArbeidsgiverMutationRequest( + mutation, + variables); + return opprettNotifikasjon(request); + } + + public void opprettNyBeskjed( + ArbeidsgiverNotifikasjon notifikasjon, + NotifikasjonMerkelapp merkelapp, + NotifikasjonTekst tekst) { + notifikasjon.setOperasjonType(NotifikasjonOperasjonType.SEND_BESKJED); + final String response = opprettNyMutasjon( + notifikasjon, + notifikasjonParser.getNyBeskjed(), + merkelapp.getValue(), + tekst.getTekst()); + final NyBeskjedResponse beskjed = handler.readResponse(response, NyBeskjedResponse.class); + if (beskjed.getData() != null) { + handler.sjekkOgSettStatusResponse( + notifikasjon, + handler.konverterResponse(beskjed.getData().getNyBeskjed()), + MutationStatus.NY_BESKJED_VELLYKKET); + } else { + handler.logErrorOgSettFeilmelding(response, notifikasjon); + } + } + + public void opprettOppgave( + ArbeidsgiverNotifikasjon notifikasjon, + NotifikasjonMerkelapp merkelapp, + NotifikasjonTekst tekst) { + notifikasjon.setOperasjonType(NotifikasjonOperasjonType.SEND_OPPGAVE); + final String response = opprettNyMutasjon( + notifikasjon, + notifikasjonParser.getNyOppgave(), + merkelapp.getValue(), + tekst.getTekst()); + final NyOppgaveResponse oppgave = handler.readResponse(response, NyOppgaveResponse.class); + if (oppgave.getData() != null) { + handler.sjekkOgSettStatusResponse( + notifikasjon, + handler.konverterResponse(oppgave.getData().getNyOppgave()), + MutationStatus.NY_OPPGAVE_VELLYKKET); + } else { + handler.logErrorOgSettFeilmelding(response, notifikasjon); + } + } + + public void oppgaveUtfoert( + Avtale avtale, + HendelseType hendelseTypeSomSkalMerkesUtfoert, + MutationStatus status, + HendelseType hendelseTypeForNyNotifikasjon) { + + final List notifikasjonList = + handler.finnUtfoertNotifikasjon(avtale.getId(), hendelseTypeSomSkalMerkesUtfoert, status.getStatus()); + + if (!notifikasjonList.isEmpty()) { + notifikasjonList.forEach(n -> { + final NotifikasjonEvent event = handler.finnEllerOpprettNotifikasjonForHendelse( + avtale, n.getId(), hendelseTypeForNyNotifikasjon, this, notifikasjonParser, + MutationStatus.OPPGAVE_UTFOERT_VELLYKKET, NotifikasjonOperasjonType.SETT_OPPGAVE_UTFOERT); + + if (!event.notifikasjonFerdigBehandlet) { + + Variables variables = new Variables(); + variables.setEksternId(n.getId()); + variables.setMerkelapp(NotifikasjonMerkelapp.getMerkelapp(avtale.getTiltakstype().getBeskrivelse()).getValue()); + + final String response = opprettNotifikasjon(new ArbeidsgiverMutationRequest( + notifikasjonParser.getOppgaveUtfoertByEksternId(), + variables + )); + + final OppgaveUtfoertResponse oppgaveUtfoert = handler.readResponse(response, OppgaveUtfoertResponse.class); + if (oppgaveUtfoert.getData() != null) { + handler.oppdaterNotifikasjon(event.getNotifikasjon(), + n, + handler.konverterResponse(oppgaveUtfoert.getData().getOppgaveUtfoertByEksternId()), + MutationStatus.OPPGAVE_UTFOERT_VELLYKKET); + } else { + handler.logErrorOgSettFeilmelding(response, event.getNotifikasjon()); + } + } + }); + } + } + + public void softDeleteNotifikasjoner(Avtale avtale) { + final List notifikasjonlist = + handler.finnNotifikasjonerTilSletting(avtale.getId()); + + if (!notifikasjonlist.isEmpty()) { + notifikasjonlist.forEach(n -> { + final NotifikasjonEvent event = handler.finnEllerOpprettNotifikasjonForHendelse( + avtale, n.getId(), HendelseType.ANNULLERT, this, notifikasjonParser, + MutationStatus.SOFT_DELETE_NOTIFIKASJON_VELLYKKET, NotifikasjonOperasjonType.SOFTDELETE_NOTIFIKASJON); + + if (!event.notifikasjonFerdigBehandlet) { + + Variables variables = new Variables(); + variables.setEksternId(n.getId()); + variables.setMerkelapp(NotifikasjonMerkelapp.getMerkelapp(avtale.getTiltakstype().getBeskrivelse()).getValue()); + + final String response = opprettNotifikasjon(new ArbeidsgiverMutationRequest( + notifikasjonParser.getSoftDeleteNotifikasjonByEksternId(), + variables)); + final Data data = handler.readResponse(response, SoftDeleteNotifikasjonResponse.class).getData(); + + if (data != null) { + handler.oppdaterNotifikasjon(event.getNotifikasjon(), + n, + handler.konverterResponse(data.getSoftDeleteNotifikasjonByEksternId()), + MutationStatus.SOFT_DELETE_NOTIFIKASJON_VELLYKKET); + } else { + handler.logErrorOgSettFeilmelding(response, event.getNotifikasjon()); + } + } + }); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonTekst.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonTekst.java new file mode 100644 index 000000000..9708e5ee7 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonTekst.java @@ -0,0 +1,27 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +public enum NotifikasjonTekst { + TILTAK_AVTALE_OPPRETTET("Ny avtale om arbeidstiltak opprettet. Åpne avtale og fyll ut innholdet."), + TILTAK_AVTALE_INNGATT("Avtale om arbeidstiltak godkjent."), + TILTAK_AVTALE_KLAR_REFUSJON("Du kan nå søke om refusjon."), + TILTAK_STILLINGSBESKRIVELSE_ENDRET("Stillingsbeskrivelse i avtale endret av veileder."), + TILTAK_MÅL_ENDRET("Mål i avtale endret av veileder."), + TILTAK_INKLUDERINGSTILSKUDD_ENDRET("Inkluderingstilskudd i avtalen endret av veileder."), + TILTAK_OM_MENTOR_ENDRET("Om mentor i avtale endret av veileder."), + TILTAK_OPPFØLGING_OG_TILRETTELEGGING_ENDRET("Oppfølging og tilrettelegging i avtale endret av veileder."), + TILTAK_AVTALE_FORKORTET("Avtale forkortet."), + TILTAK_AVTALE_FORLENGET("Avtale forlenget av veileder."), + TILTAK_TILSKUDDSBEREGNING_ENDRET("Tilskuddsberegning i avtale endret av veileder."), + TILTAK_GODKJENNINGER_OPPHEVET_AV_VEILEDER("Avtalen må godkjennes på nytt."), + TILTAK_KONTAKTINFORMASJON_ENDRET("Kontaktinformasjon i avtale endret av veileder."); + + private final String tekst; + + NotifikasjonTekst(String tekst) { + this.tekst = tekst; + } + + public String getTekst() { + return tekst; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonerProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonerProperties.java new file mode 100644 index 000000000..5e1f78943 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonerProperties.java @@ -0,0 +1,15 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.net.URI; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.notifikasjoner") +public class NotifikasjonerProperties { + private URI uri; + private String lenke; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/request/ArbeidsgiverMutationRequest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/request/ArbeidsgiverMutationRequest.java new file mode 100644 index 000000000..e6b68dca8 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/request/ArbeidsgiverMutationRequest.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.request; + + +import lombok.Value; + +@Value +public class ArbeidsgiverMutationRequest { + String query; + Variables variables; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/request/Variables.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/request/Variables.java new file mode 100644 index 000000000..a7a4dbb9a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/request/Variables.java @@ -0,0 +1,17 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.request; + +import lombok.Data; +import java.util.UUID; + +@Data +public class Variables { + UUID eksternId; + String virksomhetsnummer; + String lenke; + String serviceCode; + String serviceEdition; + String merkelapp; + String tekst; + UUID id; + UUID grupperingsId; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/FeilVedSendingResponse/Errors.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/FeilVedSendingResponse/Errors.java new file mode 100644 index 000000000..a0cef1883 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/FeilVedSendingResponse/Errors.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.FeilVedSendingResponse; + +import lombok.Value; + +@Value +public class Errors { + String message; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/FeilVedSendingResponse/FeilVedSendingResponse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/FeilVedSendingResponse/FeilVedSendingResponse.java new file mode 100644 index 000000000..5c0d75e0a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/FeilVedSendingResponse/FeilVedSendingResponse.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.FeilVedSendingResponse; + +import lombok.Value; + +@Value +public class FeilVedSendingResponse { + Errors[] errors; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/FellesResponse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/FellesResponse.java new file mode 100644 index 000000000..802575ff2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/FellesResponse.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response; + +import lombok.Value; + +@Value +public class FellesResponse { + String __typename; + String id; + String feilmelding; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/MutationStatus.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/MutationStatus.java new file mode 100644 index 000000000..3505452f8 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/MutationStatus.java @@ -0,0 +1,24 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response; + +public enum MutationStatus { + NY_OPPGAVE_VELLYKKET("NyOppgaveVellykket"), + NY_BESKJED_VELLYKKET("NyBeskjedVellykket"), + OPPGAVE_UTFOERT_VELLYKKET("OppgaveUtfoertVellykket"), + SOFT_DELETE_NOTIFIKASJON_VELLYKKET("SoftDeleteNotifikasjonVellykket"), + HARD_DELETE_NOTIFIKASJON_VELLYKKET("HardDeleteNotifikasjonVellykket"), + NOTIFIKASJON_FINNES_IKKE("NotifikasjonFinnesIkke"), + DUPLIKAT_ID_OG_MERKELAPP("DuplikatEksternIdOgMerkelapp"), + UGYLDIG_MOTTAKER("UgyldigMottaker"), + UKJENT_PRODUSENT("UkjentProdusent"), + UGYLDIG_MERKELAPP("UgyldigMerkelapp"); + + private final String status; + + MutationStatus(String status) { + this.status = status; + } + + public String getStatus() { + return status; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyBeskjed/Data.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyBeskjed/Data.java new file mode 100644 index 000000000..e893286f2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyBeskjed/Data.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.nyBeskjed; + +import lombok.Value; + +@Value +public class Data { + NyBeskjed nyBeskjed; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyBeskjed/NyBeskjed.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyBeskjed/NyBeskjed.java new file mode 100644 index 000000000..73958045f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyBeskjed/NyBeskjed.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.nyBeskjed; + +import lombok.Value; + +@Value +public class NyBeskjed { + String __typename; + String id; + String feilmelding; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyBeskjed/NyBeskjedResponse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyBeskjed/NyBeskjedResponse.java new file mode 100644 index 000000000..2a762f60a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyBeskjed/NyBeskjedResponse.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.nyBeskjed; + +import lombok.Value; + +@Value +public class NyBeskjedResponse { + Data data; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyOppgave/Data.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyOppgave/Data.java new file mode 100644 index 000000000..59cd829a5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyOppgave/Data.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.nyOppgave; + +import lombok.Value; + +@Value +public class Data { + NyOppgave nyOppgave; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyOppgave/NyOppgave.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyOppgave/NyOppgave.java new file mode 100644 index 000000000..289601092 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyOppgave/NyOppgave.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.nyOppgave; + +import lombok.Value; + +@Value +public class NyOppgave { + String __typename; + String id; + String feilmelding; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyOppgave/NyOppgaveResponse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyOppgave/NyOppgaveResponse.java new file mode 100644 index 000000000..431410f02 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/nyOppgave/NyOppgaveResponse.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.nyOppgave; + +import lombok.Value; + +@Value +public class NyOppgaveResponse { + Data data; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/oppgaveUtfoertByEksternId/Data.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/oppgaveUtfoertByEksternId/Data.java new file mode 100644 index 000000000..a448da1aa --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/oppgaveUtfoertByEksternId/Data.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.oppgaveUtfoertByEksternId; + +import lombok.Value; + +@Value +public class Data { + OppgaveUtfoertByEksternId oppgaveUtfoertByEksternId; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/oppgaveUtfoertByEksternId/OppgaveUtfoertByEksternId.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/oppgaveUtfoertByEksternId/OppgaveUtfoertByEksternId.java new file mode 100644 index 000000000..33a9d1d7c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/oppgaveUtfoertByEksternId/OppgaveUtfoertByEksternId.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.oppgaveUtfoertByEksternId; + +import lombok.Value; + +@Value +public class OppgaveUtfoertByEksternId { + String __typename; + String id; + String feilmelding; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/oppgaveUtfoertByEksternId/OppgaveUtfoertResponse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/oppgaveUtfoertByEksternId/OppgaveUtfoertResponse.java new file mode 100644 index 000000000..f1047c88f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/oppgaveUtfoertByEksternId/OppgaveUtfoertResponse.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.oppgaveUtfoertByEksternId; + +import lombok.Value; + +@Value +public class OppgaveUtfoertResponse { + Data data; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/softDeleteNotifikasjonByEksternId/Data.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/softDeleteNotifikasjonByEksternId/Data.java new file mode 100644 index 000000000..26158f4ba --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/softDeleteNotifikasjonByEksternId/Data.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.softDeleteNotifikasjonByEksternId; + +import lombok.Value; + +@Value +public class Data { + SoftDeleteNotifikasjonByEksternId softDeleteNotifikasjonByEksternId; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/softDeleteNotifikasjonByEksternId/SoftDeleteNotifikasjonByEksternId.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/softDeleteNotifikasjonByEksternId/SoftDeleteNotifikasjonByEksternId.java new file mode 100644 index 000000000..693d773c0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/softDeleteNotifikasjonByEksternId/SoftDeleteNotifikasjonByEksternId.java @@ -0,0 +1,10 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.softDeleteNotifikasjonByEksternId; + +import lombok.Value; + +@Value +public class SoftDeleteNotifikasjonByEksternId { + String __typename; + String id; + String feilmelding; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/softDeleteNotifikasjonByEksternId/SoftDeleteNotifikasjonResponse.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/softDeleteNotifikasjonByEksternId/SoftDeleteNotifikasjonResponse.java new file mode 100644 index 000000000..e0d6e2aea --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/response/softDeleteNotifikasjonByEksternId/SoftDeleteNotifikasjonResponse.java @@ -0,0 +1,8 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.softDeleteNotifikasjonByEksternId; + +import lombok.Value; + +@Value +public class SoftDeleteNotifikasjonResponse { + Data data; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/LagGosysVarselLytter.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/LagGosysVarselLytter.java new file mode 100644 index 000000000..d5c40c1f0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/LagGosysVarselLytter.java @@ -0,0 +1,25 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.oppgave; + +import lombok.RequiredArgsConstructor; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.events.AvtaleOpprettetAvArbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class LagGosysVarselLytter { + private final OppgaveVarselService oppgaveVarselService; + private final PersondataService persondataService; + + private void varsleGosys (Avtale avtale) { + final String aktørid = persondataService.hentAktørId(avtale.getDeltakerFnr()); + oppgaveVarselService.opprettOppgave(aktørid, avtale.getTiltakstype(), avtale.getId()); + } + + @TransactionalEventListener + public void opprettGosysVarsel(AvtaleOpprettetAvArbeidsgiver event) { + varsleGosys(event.getAvtale()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/OppgaveProperties.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/OppgaveProperties.java new file mode 100644 index 000000000..eee14d8b0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/OppgaveProperties.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.oppgave; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.net.URI; + +@Data +@Component +@ConfigurationProperties(prefix = "tiltaksgjennomforing.oppgave") +public class OppgaveProperties { + private URI oppgaveUri; +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/OppgaveRequest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/OppgaveRequest.java new file mode 100644 index 000000000..a4ddbe4a4 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/OppgaveRequest.java @@ -0,0 +1,37 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.oppgave; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Data; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import no.nav.tag.tiltaksgjennomforing.utils.Now; + +import java.time.LocalDate; + +@Data +@AllArgsConstructor +public class OppgaveRequest { + + private final static String BESKRIVELSE = "Avtale er opprettet av arbeidsgiver på tiltak %s. Se avtalen under filteret 'Ufordelte' i https://tiltaksgjennomforing.intern.nav.no/tiltaksgjennomforing"; + private final static String TEMA = "TIL"; + private final static String HOY_PRI = "NORM"; + private final static String OPPG_TYPE = "VURD_HENV"; + private final static String BEHANDLINGSTYPE = "ae0034"; + + private final String beskrivelse; + private final String tema = TEMA; + private final String prioritet = HOY_PRI; + private final String oppgavetype = OPPG_TYPE; + private final String behandlingstype = BEHANDLINGSTYPE; + private final String behandlingstema; + + @JsonFormat(pattern = "yyyy-MM-dd") + private final LocalDate aktivDato = Now.localDate(); + private final String aktoerId; + + public OppgaveRequest(String aktørId, Tiltakstype tiltakstype) { + this.aktoerId = aktørId; + this.behandlingstema = tiltakstype.getBehandlingstema(); + this.beskrivelse = String.format(BESKRIVELSE, tiltakstype.getBeskrivelse()); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/OppgaveVarselService.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/OppgaveVarselService.java new file mode 100644 index 000000000..261fef824 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/OppgaveVarselService.java @@ -0,0 +1,61 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.oppgave; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import no.nav.tag.tiltaksgjennomforing.exceptions.GosysFeilException; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.sts.STSClient; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.util.Arrays; +import java.util.UUID; + +@Slf4j +@Service +public class OppgaveVarselService { + + private static final String CORR_ID = "X-Correlation-ID"; + private final URI uri; + private final RestTemplate restTemplate; + private final STSClient stsClient; + + public OppgaveVarselService(OppgaveProperties props, RestTemplate restTemplate, STSClient stsClient) { + uri = UriComponentsBuilder.fromUri(props.getOppgaveUri()).build().toUri(); + this.restTemplate = restTemplate; + this.stsClient = stsClient; + } + + public void opprettOppgave(String aktørId, Tiltakstype tiltakstype, UUID avtaleId) { + OppgaveRequest oppgaveRequest = new OppgaveRequest(aktørId, tiltakstype); + OppgaveResponse oppgaveResponse; + + try { + oppgaveResponse = restTemplate.postForObject(uri, entityMedStsToken(oppgaveRequest, avtaleId), OppgaveResponse.class); + } catch (Exception e2) { + log.error("Kall til Oppgave feilet, avtaleId={} : {}", avtaleId, e2.getMessage()); + throw new GosysFeilException(); + } + log.info("Opprettet oppgave for tiltak {}. OppgaveId={}, avtaleId={}", tiltakstype.getBeskrivelse(), oppgaveResponse.getId(), avtaleId); + } + + private HttpEntity entityMedStsToken(final OppgaveRequest oppgaveRequest, UUID correlationId) { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(stsClient.hentSTSToken().getAccessToken()); + headers.set(CORR_ID, correlationId.toString()); + HttpEntity entity = new HttpEntity<>(oppgaveRequest, headers); + return entity; + } + + @Data + static class OppgaveResponse{ + private String id; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/avro/tiltaksgjennomforing.avsc b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/avro/tiltaksgjennomforing.avsc new file mode 100644 index 000000000..3133c6152 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/avro/tiltaksgjennomforing.avsc @@ -0,0 +1,353 @@ +{ + "namespace": "no.nav.tag.tiltaksgjennomforing.datavarehus", + "type": "record", + "name": "AvroTiltakHendelse", + "fields": [ + { + "name": "meldingId", + "type": "string" + }, + { + "name": "tidspunkt", + "type": { + "type": "long", + "logicalType": "timestamp-millis" + } + }, + { + "name": "hendelseType", + "type": "string" + }, + { + "name": "utfortAv", + "type": "string" + }, + { + "name": "avtaleId", + "type": "string" + }, + { + "name": "avtaleInnholdId", + "type": "string" + }, + { + "name": "journalpostId", + "type": [ + "null", + "string" + ] + }, + { + "name": "tiltakstype", + "type": { + "type": "enum", + "name": "TiltakType", + "symbols": [ + "ARBEIDSTRENING", + "MIDLERTIDIG_LONNSTILSKUDD", + "VARIG_LONNSTILSKUDD", + "SOMMERJOBB", + "MENTOR", + "INKLUDERINGSTILSKUDD" + ] + } + }, + { + "name": "tiltakskodeArena", + "type": [ + "null", + { + "type": "enum", + "name": "TiltakKodeArena", + "symbols": [ + "ARBTREN", + "MIDLONTIL", + "VARLONTIL", + "MENTOR", + "INKLUTILS" + ] + } + ] + }, + { + "name": "tiltakStatus", + "type": "string", + "doc": "Gyldige verdier: KLAR_FOR_OPPSTART, GJENNOMFØRES, AVSLUTTET, ANNULLERT" + }, + { + "name": "deltakerFnr", + "type": "string" + }, + { + "name": "bedriftNr", + "type": "string" + }, + { + "name": "harFamilietilknytning", + "type": [ + "null", + "boolean" + ] + }, + { + "name": "veilederNavIdent", + "type": "string" + }, + { + "name": "startDato", + "type": { + "type": "int", + "logicalType": "date" + } + }, + { + "name": "sluttDato", + "type": { + "type": "int", + "logicalType": "date" + } + }, + { + "name": "stillingprosent", + "type": [ + "null", + "int" + ] + }, + { + "name": "antallDagerPerUke", + "type": [ + "null", + "int" + ] + }, + { + "name": "maal", + "type": [ + "null", + "string" + ] + }, + { + "name": "stillingstype", + "type": [ + "null", + { + "type": "enum", + "name": "StillingType", + "symbols": [ + "FAST", + "MIDLERTIDIG" + ] + } + ] + }, + { + "name": "stillingstittel", + "type": [ + "null", + "string" + ] + }, + { + "name": "stillingStyrk08", + "type": [ + "null", + "int" + ] + }, + { + "name": "stillingKonseptId", + "type": [ + "null", + "int" + ] + }, + { + "name": "lonnstilskuddProsent", + "type": [ + "null", + "int" + ] + }, + { + "name": "manedslonn", + "type": [ + "null", + "int" + ] + }, + { + "name": "feriepengesats", + "type": [ + "null", + "float" + ] + }, + { + "name": "feriepengerBelop", + "type": [ + "null", + "int" + ] + }, + { + "name": "arbeidsgiveravgift", + "type": [ + "null", + "float" + ] + }, + { + "name": "arbeidsgiveravgiftBelop", + "type": [ + "null", + "int" + ] + }, + { + "name": "otpSats", + "type": [ + "null", + "float" + ] + }, + { + "name": "otpBelop", + "type": [ + "null", + "int" + ] + }, + { + "name": "sumLonnsutgifter", + "type": [ + "null", + "int" + ] + }, + { + "name": "sumLonnstilskudd", + "type": [ + "null", + "int" + ] + }, + { + "name": "sumLonnstilskuddRedusert", + "type": [ + "null", + "int" + ] + }, + { + "name": "datoForRedusertProsent", + "type": [ + "null", + { + "type": "int", + "logicalType": "date" + } + ] + }, + { + "name": "godkjentPaVegneAv", + "type": "boolean" + }, + { + "name": "ikkeBankId", + "type": "boolean" + }, + { + "name": "reservert", + "type": "boolean" + }, + { + "name": "digitalKompetanse", + "type": "boolean" + }, + { + "name": "arenaMigreringDeltaker", + "type": "boolean", + "default" : false + }, + { + "name": "godkjentAvDeltaker", + "type": { + "type": "long", + "logicalType": "timestamp-millis" + } + }, + { + "name": "godkjentAvArbeidsgiver", + "type": { + "type": "long", + "logicalType": "timestamp-millis" + } + }, + { + "name": "godkjentAvVeileder", + "type": { + "type": "long", + "logicalType": "timestamp-millis" + } + }, + { + "name": "godkjentAvBeslutter", + "type": [ + "null", + { + "type": "long", + "logicalType": "timestamp-millis" + } + ] + }, + { + "name": "avtaleInngaatt", + "type": { + "type": "long", + "logicalType": "timestamp-millis" + } + }, + { + "name": "enhetOppfolging", + "type": [ + "null", + "string" + ] + }, + { + "name": "enhetGeografisk", + "type": [ + "null", + "string" + ] + }, + { + "name": "opprettetAvArbeidsgiver", + "type": "boolean" + }, + { + "name": "annullertTidspunkt", + "type": [ + "null", + { + "type": "long", + "logicalType": "timestamp-millis" + } + ] + }, + { + "name": "annullertGrunn", + "type": [ + "null", + "string" + ] + }, + { + "name": "master", + "type": "boolean", + "default" : false + } + ] +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/banner.txt b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/banner.txt new file mode 100644 index 000000000..9af1793aa --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/banner.txt @@ -0,0 +1,6 @@ + __ .__.__ __ __ __ _____ .__ +_/ |_|__| |_/ |______ | | __ ______ ____ |__| ____ ____ ____ ____ ______/ ____\___________|__| ____ ____ +\ __\ | |\ __\__ \ | |/ / / ___// ___\ | |/ __ \ / \ / \ / _ \ / \ __\/ _ \_ __ \ |/ \ / ___\ + | | | | |_| | / __ \| < \___ \/ /_/ > | \ ___/| | \ | ( <_> ) Y Y \ | ( <_> ) | \/ | | \/ /_/ > + |__| |__|____/__| (____ /__|_ \/____ >___ /\__| |\___ >___| /___| /\____/|__|_| /__| \____/|__| |__|___| /\___ / + \/ \/ \/_____/\______| \/ \/ \/ \/ \//_____/ \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/config/application-dev-fss.yaml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/config/application-dev-fss.yaml new file mode 100644 index 000000000..0d1dac5d2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/config/application-dev-fss.yaml @@ -0,0 +1,147 @@ +spring: + datasource: + driverClassName: org.postgresql.Driver + kafka: + bootstrap-servers: b27apvl00045.preprod.local:8443, b27apvl00046.preprod.local:8443, b27apvl00047.preprod.local:8443 + properties: + security.protocol: SASL_SSL + sasl: + mechanism: PLAIN + jaas.config: org.apache.kafka.common.security.plain.PlainLoginModule required username="${tiltaksgjennomforing.serviceuser.username}" password="${tiltaksgjennomforing.serviceuser.password}"; + ssl.truststore: + location: ${javax.net.ssl.trustStore} + password: ${javax.net.ssl.trustStorePassword} + sql: + init: + platform: postgres + +no.nav.security.jwt: + issuer: + aad: + discoveryurl: ${AZURE_APP_WELL_KNOWN_URL} + accepted_audience: ${AZURE_APP_CLIENT_ID} + tokenx: + discoveryurl: ${TOKEN_X_WELL_KNOWN_URL} + accepted_audience: ${TOKEN_X_CLIENT_ID} + system: + discoveryurl: http://security-token-service.default.svc.nais.local/rest/v1/sts/.well-known/openid-configuration + accepted_audience: ${tiltaksgjennomforing.audience.system} + client: + registration: + notifikasjoner: + token-endpoint-url: https://login.microsoftonline.com/${AZURE_APP_TENANT_ID}/oauth2/v2.0/token + grant-type: client_credentials + scope: api://dev-gcp.fager.notifikasjon-produsent-api/.default + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + kontoregister: + token-endpoint-url: https://login.microsoftonline.com/${AZURE_APP_TENANT_ID}/oauth2/v2.0/token + grant-type: client_credentials + scope: api://dev-fss.okonomi.sokos-kontoregister-q2/.default + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + veilarbarena: + token-endpoint-url: https://login.microsoftonline.com/${AZURE_APP_TENANT_ID}/oauth2/v2.0/token + grant-type: client_credentials + scope: api://dev-fss.pto.veilarbarena/.default + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + poao-tilgang: + token-endpoint-url: https://login.microsoftonline.com/${AZURE_APP_TENANT_ID}/oauth2/v2.0/token + grant-type: client_credentials + scope: api://dev-fss.poao.poao-tilgang/.default + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + tokenx-altinn: + token-endpoint-url: ${TOKEN_X_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:token-exchange + authentication: + client-id: ${TOKEN_X_CLIENT_ID} + client-jwk: /var/run/secrets/nais.io/jwker/TOKEN_X_PRIVATE_JWK + client-auth-method: private_key_jwt + token-exchange: + audience: dev-gcp:arbeidsgiver:altinn-rettigheter-proxy + +tiltaksgjennomforing: + kontoregister: + uri: https://sokos-kontoregister-q2.dev.adeo.no/kontoregister/api/v1/hent-kontonummer-for-organisasjon/ + consumerId: ${tiltaksgjennomforing.serviceuser.username} + azureConfig: true + realClient: true + database: + database-navn: tiltaksgjennomforing-preprod + database-url: ${spring.datasource.url} + vault-sti: postgresql/preprod-fss + minimum-idle: 1 + maximum-pool-size: 8 + max-lifetime: 300000 + altinn-tilgangsstyring: + uri: https://api-gw-q1.adeo.no + proxyUri: https://altinn-rettigheter-proxy.dev.intern.nav.no/altinn-rettigheter-proxy + beOmRettighetBaseUrl: https://min-side-arbeidsgiver.dev.nav.no/min-side-arbeidsgiver/?fragment=be-om-tilgang + ltsMidlertidigServiceCode: 5516 + ltsMidlertidigServiceEdition: 1 + ltsVarigServiceCode: 5516 + ltsVarigServiceEdition: 2 + sommerjobbServiceCode: 5516 + sommerjobbServiceEdition: 3 + mentorServiceCode: 5516 + mentorServiceEdition: 4 + inkluderingstilskuddServiceCode: 5516 + inkluderingstilskuddServiceEdition: 5 + arbtreningServiceCode: 5332 + arbtreningServiceEdition: 1 + altinn-varsel: + uri: https://pep-gw-q1.oera-q.local:9443/ekstern/altinn/notificationagencyexternalbasic/v1 + ereg: + uri: https://ereg-services-q1.dev.intern.nav.no/api/v2/organisasjon + sts: + username: ${tiltaksgjennomforing.serviceuser.username} + password: ${tiltaksgjennomforing.serviceuser.password} + ws-uri: https://sts-q1.preprod.local/SecurityTokenServiceProvider/ + rest-uri: http://security-token-service.default.svc.nais.local/rest/v1/sts/token + axsys: + uri: https://axsys.nais.preprod.local/api/v1/tilgang + nav-consumer-id: ${tiltaksgjennomforing.serviceuser.username} + abac: + uri: https://abac-veilarb-q1.dev.intern.nav.no/application/asm-pdp/authorize + nav-consumer-id: ${tiltaksgjennomforing.serviceuser.username} + oppgave: + oppgave-uri: https://oppgave.nais.preprod.local/api/v1/oppgaver + unleash: + enabled: true + api-uri: ${UNLEASH_SERVER_API_URL}/api + api-token: ${UNLEASH_SERVER_API_TOKEN} + consumer: + system: + id: ${tiltaksgjennomforing.consumer.system} + veilarbarena: + url: https://veilarbarena.dev.intern.nav.no/veilarbarena/api/arena/status + nav-consumer-id: ${tiltaksgjennomforing.serviceuser.username} + poao-tilgang: + url: http://poao-tilgang.poao.svc.nais.local + norg2: + geografisk: + url: https://app-q1.adeo.no/norg2/api/v1/enhet/navkontor/ + enhet: + url: https://app-q1.adeo.no/norg2/api/v1/enhet/ + notifikasjoner: + uri: https://ag-notifikasjon-produsent-api.intern.dev.nav.no/api/graphql + lenke: https://tiltaksgjennomforing.dev.nav.no/tiltaksgjennomforing/avtale/ + enabled: true + dvh-melding: + gruppe-tilgang: ${beslutter.ad.gruppe} + utvikler-tilgang: + gruppe-tilgang: ${beslutter.ad.gruppe} + +http: + proxy: + parametername: nothing diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/config/application-dev-gcp-labs.yaml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/config/application-dev-gcp-labs.yaml new file mode 100644 index 000000000..3430a3a9e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/config/application-dev-gcp-labs.yaml @@ -0,0 +1,90 @@ +spring: + datasource: + url: jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;MODE=PostgreSQL + username: sa + password: sa + driverClassName: org.h2.Driver + h2: + console: + enabled: true + path: /h2-console + main: + banner-mode: "console" + sql: + init: + platform: postgres + +no.nav.security.jwt: + issuer: + aad: + discoveryurl: http://tiltak-fakelogin/metadata?issuer=aad + accepted_audience: fake-aad + tokenx: + discoveryurl: http://tiltak-fakelogin/metadata?issuer=tokenx + accepted_audience: fake-tokenx + +tiltaksgjennomforing: + kontoregister: + fake: true + beslutter-ad-gruppe: + id: 1a1d2745-952f-4a0f-839f-9530145b1d4a + kafka: + enabled: false + fake: true + fake-url: http://tiltak-refusjon-api-labs/fake-kafka + altinn-tilgangsstyring: + uri: http://tiltaksgjennomforing-wiremock/altinn-tilgangsstyring + proxyUri: http://heh:9090/ + beOmRettighetBaseUrl: https://arbeidsgiver-q.nav.no/min-side-arbeidsgiver/?fragment=be-om-tilgang + ltsMidlertidigServiceCode: 5516 + ltsMidlertidigServiceEdition: 1 + ltsVarigServiceCode: 5516 + ltsVarigServiceEdition: 2 + sommerjobbServiceCode: 5516 + sommerjobbServiceEdition: 3 + mentorServiceCode: 5516 + mentorServiceEdition: 4 + inkluderingstilskuddServiceCode: 5516 + inkluderingstilskuddServiceEdition: 5 + arbtreningServiceCode: 5332 + arbtreningServiceEdition: 1 + altinnApiKey: foo + apiGwApiKey: foo + ereg: + uri: http://tiltaksgjennomforing-wiremock/ereg + sts: + username: foo + password: bar + rest-uri: http://tiltaksgjennomforing-wiremock/sts/sts/token + axsys: + uri: http://tiltaksgjennomforing-wiremock/axsys + abac: + uri: http://tiltaksgjennomforing-wiremock/abac + nav-consumer-id: tiltaksgjennomforing-api + oppgave: + oppgave-uri: http://tiltaksgjennomforing-wiremock/api/v1/oppgaver + veilarbarena: + url: http://tiltaksgjennomforing-wiremock/veilarbarena/api/arena/status + nav-consumer-id: tiltaksgjennomforing-api + norg2: + geografisk: + url: http://tiltaksgjennomforing-wiremock/norg2/api/v1/enhet/navkontor/ + enhet: + url: http://tiltaksgjennomforing-wiremock/norg2/api/v1/enhet/ + consumer: + system: + id: testsystem + persondata: + uri: http://tiltaksgjennomforing-wiremock/persondata + unleash: + mock: true + dokgen: + uri: http://tiltak-dokgen/template/tiltak-avtale/create-pdf + notifikasjoner: + uri: https://notifikasjon-fake-produsent-api.labs.nais.io/api/graphql + lenke: https://arbeidsgiver.labs.nais.io/tiltaksgjennomforing/avtale/ + enabled: false + tilskuddsperioder: + tiltakstyper: SOMMERJOBB, MIDLERTIDIG_LONNSTILSKUDD, VARIG_LONNSTILSKUDD + +ELECTOR_PATH: null \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/config/application-prod-fss.yaml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/config/application-prod-fss.yaml new file mode 100644 index 000000000..da18d8d0f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/config/application-prod-fss.yaml @@ -0,0 +1,145 @@ +spring: + datasource: + platform: postgres + driverClassName: org.postgresql.Driver + kafka: + bootstrap-servers: a01apvl00145.adeo.no:8443, a01apvl00146.adeo.no:8443, a01apvl00147.adeo.no:8443, a01apvl00148.adeo.no:8443, a01apvl00149.adeo.no:8443, a01apvl00150.adeo.no:8443 + properties: + security.protocol: SASL_SSL + sasl: + mechanism: PLAIN + jaas.config: org.apache.kafka.common.security.plain.PlainLoginModule required username="${tiltaksgjennomforing.serviceuser.username}" password="${tiltaksgjennomforing.serviceuser.password}"; + ssl.truststore: + location: ${javax.net.ssl.trustStore} + password: ${javax.net.ssl.trustStorePassword} + +no.nav.security.jwt: + issuer: + tokenx: + discoveryurl: ${TOKEN_X_WELL_KNOWN_URL} + accepted_audience: ${TOKEN_X_CLIENT_ID} + aad: + discoveryurl: ${AZURE_APP_WELL_KNOWN_URL} + accepted_audience: ${AZURE_APP_CLIENT_ID} + system: + discoveryurl: http://security-token-service.default.svc.nais.local/rest/v1/sts/.well-known/openid-configuration + accepted_audience: ${tiltaksgjennomforing.audience.system} + client: + registration: + notifikasjoner: + token-endpoint-url: https://login.microsoftonline.com/${AZURE_APP_TENANT_ID}/oauth2/v2.0/token + grant-type: client_credentials + scope: api://prod-gcp.fager.notifikasjon-produsent-api/.default + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + kontoregister: + token-endpoint-url: https://login.microsoftonline.com/${AZURE_APP_TENANT_ID}/oauth2/v2.0/token + grant-type: client_credentials + scope: api://prod-fss.okonomi.sokos-kontoregister/.default + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + veilarbarena: + token-endpoint-url: https://login.microsoftonline.com/${AZURE_APP_TENANT_ID}/oauth2/v2.0/token + grant-type: client_credentials + scope: api://prod-fss.pto.veilarbarena/.default + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + poao-tilgang: + token-endpoint-url: https://login.microsoftonline.com/${AZURE_APP_TENANT_ID}/oauth2/v2.0/token + grant-type: client_credentials + scope: api://prod-fss.poao.poao-tilgang/.default + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + tokenx-altinn: + token-endpoint-url: ${TOKEN_X_TOKEN_ENDPOINT} + grant-type: urn:ietf:params:oauth:grant-type:token-exchange + authentication: + client-id: ${TOKEN_X_CLIENT_ID} + client-jwk: /var/run/secrets/nais.io/jwker/TOKEN_X_PRIVATE_JWK + client-auth-method: private_key_jwt + token-exchange: + audience: prod-gcp:arbeidsgiver:altinn-rettigheter-proxy + + +tiltaksgjennomforing: + kontoregister: + uri: https://sokos-kontoregister.nais.adeo.no/kontoregister/api/v1/hent-kontonummer-for-organisasjon/ + consumerId: ${tiltaksgjennomforing.serviceuser.username} + azureConfig: true + realClient: true + database: + database-navn: tiltaksgjennomforing-prod + database-url: ${spring.datasource.url} + vault-sti: postgresql/prod-fss + minimum-idle: 1 + maximum-pool-size: 8 + max-lifetime: 300000 + altinn-tilgangsstyring: + uri: https://api-gw.adeo.no/ + proxyUri: https://altinn-rettigheter-proxy.intern.nav.no/altinn-rettigheter-proxy/ + beOmRettighetBaseUrl: https://arbeidsgiver.nav.no/min-side-arbeidsgiver/?fragment=be-om-tilgang + sommerjobbServiceCode: 5516 + sommerjobbServiceEdition: 3 + ltsMidlertidigServiceCode: 5516 + ltsMidlertidigServiceEdition: 1 + ltsVarigServiceCode: 5516 + ltsVarigServiceEdition: 2 + arbtreningServiceCode: 5332 + arbtreningServiceEdition: 2 + mentorServiceCode: 5516 + mentorServiceEdition: 4 + inkluderingstilskuddServiceCode: 5516 + inkluderingstilskuddServiceEdition: 5 + altinn-varsel: + uri: https://pep-gw.oera.no:9443/ekstern/altinn/notificationagencyexternalbasic/v1 + ereg: + uri: https://ereg-services.intern.nav.no/api/v2/organisasjon + sts: + username: ${tiltaksgjennomforing.serviceuser.username} + password: ${tiltaksgjennomforing.serviceuser.password} + ws-uri: https://sts.adeo.no/SecurityTokenServiceProvider/ + rest-uri: http://security-token-service.default.svc.nais.local/rest/v1/sts/token + axsys: + uri: https://axsys.nais.adeo.no/api/v1/tilgang + nav-consumer-id: ${tiltaksgjennomforing.serviceuser.username} + abac: + uri: https://abac-veilarb.intern.nav.no/application/asm-pdp/authorize + nav-consumer-id: ${tiltaksgjennomforing.serviceuser.username} + oppgave: + oppgave-uri: https://oppgave.nais.adeo.no/api/v1/oppgaver + unleash: + enabled: true + api-uri: ${UNLEASH_SERVER_API_URL}/api + api-token: ${UNLEASH_SERVER_API_TOKEN} + consumer: + system: + id: ${tiltaksgjennomforing.consumer.system} + veilarbarena: + url: https://veilarbarena.intern.nav.no/veilarbarena/api/arena/status + nav-consumer-id: ${tiltaksgjennomforing.serviceuser.username} + poao-tilgang: + url: http://poao-tilgang.poao.svc.nais.local + norg2: + geografisk: + url: https://norg2.nais.adeo.no/norg2/api/v1/enhet/navkontor/ + enhet: + url: https://norg2.nais.adeo.no/norg2/api/v1/enhet/ + notifikasjoner: + uri: https://ag-notifikasjon-produsent-api.intern.nav.no/api/graphql + lenke: https://arbeidsgiver.nav.no/tiltaksgjennomforing/avtale/ + enabled: true + dvh-melding: + gruppe-tilgang: fb516b74-0f2e-4b62-bad8-d70b82c3ae0b + utvikler-tilgang: + gruppe-tilgang: fb516b74-0f2e-4b62-bad8-d70b82c3ae0b +http: + proxy: + parametername: nothing \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/config/application.yaml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/config/application.yaml new file mode 100644 index 000000000..21e548afd --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/config/application.yaml @@ -0,0 +1,112 @@ +server.servlet.context-path: /tiltaksgjennomforing-api + +management.endpoints.web: + exposure.include: info, health, metrics, prometheus + base-path: /internal/actuator + +spring: + jpa: + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQL9Dialect + application: + name: tiltaksgjennomforing-api + main: + banner-mode: "off" + jmx: + enabled: false + kafka: + consumer: + auto-offset-reset: earliest + enable-auto-commit: false + properties: + spring: + json: + trusted: + packages: "*" + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + group-id: ${spring.application.name} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + +#TODO: Feil pakke: Flytt Kafka avien verdier her under tiltaksgjennomforing.kafka +no.nav.gcp.kafka.aiven: + bootstrap-servers: ${KAFKA_BROKERS} + truststore-path: ${KAFKA_TRUSTSTORE_PATH} + truststore-password: ${KAFKA_CREDSTORE_PASSWORD} + keystore-path: ${KAFKA_KEYSTORE_PATH} + keystore-password: ${KAFKA_CREDSTORE_PASSWORD} + schema-registry-url: ${KAFKA_SCHEMA_REGISTRY} + schema-registry-credentials-source: USER_INFO + schema-registry-user-info: "${KAFKA_SCHEMA_REGISTRY_USER}:${KAFKA_SCHEMA_REGISTRY_PASSWORD}" + security-protocol: SSL + +tiltaksgjennomforing: + beslutter-ad-gruppe: + id: ${beslutter.ad.gruppe} + kafka: + enabled: true + persondata: + uri: http://pdl-api.pdl.svc.nais.local/graphql + dokgen: + uri: http://tiltak-dokgen.arbeidsgiver.svc.nais.local/template/tiltak-avtale/create-pdf + notifikasjoner: + uri: https://ag-notifikasjon-produsent-api.dev.intern.nav.no/api/graphql + lenke: https://arbeidsgiver.nav.no/tiltaksgjennomforing/avtale/ + dvh-melding: + fixed-delay: 600000 + avtale-hendelse-melding: + fixed-delay: 120 + tilskuddsperioder: + tiltakstyper: SOMMERJOBB,MIDLERTIDIG_LONNSTILSKUDD,VARIG_LONNSTILSKUDD + salesforcekontorer: + enheter: ${tiltaksgjennomforing.salesforce.enheter} + +no.nav.security.jwt: + client: + registration: + aad-graph: + token-endpoint-url: https://login.microsoftonline.com/${AZURE_APP_TENANT_ID}/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: https://graph.microsoft.com/.default + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + aad: + token-endpoint-url: https://login.microsoftonline.com/${AZURE_APP_TENANT_ID}/oauth2/v2.0/token + grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + aad-anonym: + token-endpoint-url: https://login.microsoftonline.com/${AZURE_APP_TENANT_ID}/oauth2/v2.0/token + grant-type: client_credentials + authentication: + client-id: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + client-auth-method: client_secret_basic + +caches: + ehcaches: + - name: pdl_cache + expiryInMinutes: 60 + maximumSize: 1000 + - name: norgnavn_cache + expiryInMinutes: 60 + maximumSize: 1000 + - name: norggeoenhet_cache + expiryInMinutes: 60 + maximumSize: 1000 + - name: arena_cache + expiryInMinutes: 60 + maximumSize: 1000 + - name: abac_cache + expiryInMinutes: 30 + maximumSize: 1000 + - name: axsys_cache + expiryInMinutes: 60 + maximumSize: 1000 diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V10__ny_tabell_bjellevarsel.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V10__ny_tabell_bjellevarsel.sql new file mode 100644 index 000000000..ac5e5ac89 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V10__ny_tabell_bjellevarsel.sql @@ -0,0 +1,10 @@ +create table bjelle_varsel +( + id uuid primary key, + varslbar_hendelse uuid, + lest boolean, + identifikator varchar(11), + varslingstekst varchar, + avtale_id uuid, + tidspunkt timestamp without time zone not null default now() +); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V11__legg_til_avbrutt_status.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V11__legg_til_avbrutt_status.sql new file mode 100644 index 000000000..bc42e8f48 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V11__legg_til_avbrutt_status.sql @@ -0,0 +1 @@ +alter table avtale add column avbrutt boolean default false; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V12__sluttdato.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V12__sluttdato.sql new file mode 100644 index 000000000..367d49f0e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V12__sluttdato.sql @@ -0,0 +1,2 @@ +alter table avtale add column slutt_dato date; +update avtale set slutt_dato = start_dato + interval '7' day * arbeidstrening_lengde; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V13__stillingprosent_endre_kolonnenavn.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V13__stillingprosent_endre_kolonnenavn.sql new file mode 100644 index 000000000..06960a3c3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V13__stillingprosent_endre_kolonnenavn.sql @@ -0,0 +1 @@ +alter table avtale rename column arbeidstrening_stillingprosent to stillingprosent; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V14__tabeller_arbeidstrening_lonnstilskudd.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V14__tabeller_arbeidstrening_lonnstilskudd.sql new file mode 100644 index 000000000..862acba4e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V14__tabeller_arbeidstrening_lonnstilskudd.sql @@ -0,0 +1,8 @@ +alter table avtale add column tiltakstype varchar not null default 'ARBEIDSTRENING'; +alter table avtale add column arbeidsgiver_kontonummer varchar(11); +alter table avtale add column stillingtype varchar; +alter table avtale add column stillingbeskrivelse varchar; +alter table avtale add column lonnstilskudd_prosent integer; +alter table avtale add column manedslonn integer; +alter table avtale add column feriepengesats decimal; +alter table avtale add column arbeidsgiveravgift decimal; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V15__godkjent_pa_vegne_av_inn_i_avtaletabell.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V15__godkjent_pa_vegne_av_inn_i_avtaletabell.sql new file mode 100644 index 000000000..9e095badc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V15__godkjent_pa_vegne_av_inn_i_avtaletabell.sql @@ -0,0 +1,7 @@ +alter table avtale add column ikke_bank_id boolean; +alter table avtale add column reservert boolean; +alter table avtale add column digital_kompetanse boolean; + +update avtale a set ikke_bank_id=(select g.ikke_bank_id from godkjent_pa_vegne_grunn g where g.avtale=a.id); +update avtale a set reservert=(select g.reservert from godkjent_pa_vegne_grunn g where g.avtale=a.id); +update avtale a set digital_kompetanse=(select g.digital_kompetanse from godkjent_pa_vegne_grunn g where g.avtale=a.id); diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V16__legg_til_sist_endret_slett_versjon.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V16__legg_til_sist_endret_slett_versjon.sql new file mode 100644 index 000000000..eaec2cbeb --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V16__legg_til_sist_endret_slett_versjon.sql @@ -0,0 +1,2 @@ +alter table avtale add column sist_endret timestamp default now(); +alter table avtale drop column versjon; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V17__sist_endret_defaultverdi.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V17__sist_endret_defaultverdi.sql new file mode 100644 index 000000000..8271395b9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V17__sist_endret_defaultverdi.sql @@ -0,0 +1 @@ +update avtale set sist_endret=opprettet_tidspunkt; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V18__avtaleinnhold.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V18__avtaleinnhold.sql new file mode 100644 index 000000000..cf80b433a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V18__avtaleinnhold.sql @@ -0,0 +1,71 @@ +create table avtale_innhold as +select id, + id as avtale, + deltaker_fornavn, + deltaker_etternavn, + deltaker_tlf, + bedrift_navn, + arbeidsgiver_fornavn, + arbeidsgiver_etternavn, + arbeidsgiver_tlf, + veileder_fornavn, + veileder_etternavn, + veileder_tlf, + oppfolging, + tilrettelegging, + start_dato, + slutt_dato, + stillingprosent, + journalpost_id, + godkjent_av_deltaker, + godkjent_av_arbeidsgiver, + godkjent_av_veileder, + godkjent_pa_vegne_av, + ikke_bank_id, + reservert, + digital_kompetanse, + arbeidsgiver_kontonummer, + stillingtype, + stillingbeskrivelse, + lonnstilskudd_prosent, + manedslonn, + feriepengesats, + arbeidsgiveravgift +from avtale; + +alter table avtale_innhold add column versjon integer; +update avtale_innhold set versjon = 1; + +alter table avtale drop column deltaker_fornavn; +alter table avtale drop column deltaker_etternavn; +alter table avtale drop column deltaker_tlf; +alter table avtale drop column bedrift_navn; +alter table avtale drop column arbeidsgiver_fornavn; +alter table avtale drop column arbeidsgiver_etternavn; +alter table avtale drop column arbeidsgiver_tlf; +alter table avtale drop column veileder_fornavn; +alter table avtale drop column veileder_etternavn; +alter table avtale drop column veileder_tlf; +alter table avtale drop column oppfolging; +alter table avtale drop column tilrettelegging; +alter table avtale drop column start_dato; +alter table avtale drop column slutt_dato; +alter table avtale drop column stillingprosent; +alter table avtale drop column journalpost_id; +alter table avtale drop column godkjent_av_deltaker; +alter table avtale drop column godkjent_av_arbeidsgiver; +alter table avtale drop column godkjent_av_veileder; +alter table avtale drop column godkjent_pa_vegne_av; +alter table avtale drop column ikke_bank_id; +alter table avtale drop column reservert; +alter table avtale drop column digital_kompetanse; +alter table avtale drop column arbeidsgiver_kontonummer; +alter table avtale drop column stillingtype; +alter table avtale drop column stillingbeskrivelse; +alter table avtale drop column lonnstilskudd_prosent; +alter table avtale drop column manedslonn; +alter table avtale drop column feriepengesats; +alter table avtale drop column arbeidsgiveravgift; + +alter table maal rename column avtale to avtale_innhold; +alter table oppgave rename column avtale to avtale_innhold; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V19__maalkategori_som_enums.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V19__maalkategori_som_enums.sql new file mode 100644 index 000000000..11c7e8cfe --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V19__maalkategori_som_enums.sql @@ -0,0 +1,6 @@ +update maal set kategori = 'FÅ_JOBB_I_BEDRIFTEN' where kategori = 'Få jobb i bedriften'; +update maal set kategori = 'ARBEIDSERFARING' where kategori = 'Arbeidserfaring'; +update maal set kategori = 'UTPRØVING' where kategori = 'Utprøving'; +update maal set kategori = 'SPRÅKOPPLÆRING' where kategori = 'Språkopplæring'; +update maal set kategori = 'OPPNÅ_FAGBREV_KOMPETANSEBEVIS' where kategori = 'Oppnå fagbrev/kompetansebevis'; +update maal set kategori = 'ANNET' where kategori = 'Annet'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V1__init.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V1__init.sql new file mode 100644 index 000000000..e262c1786 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V1__init.sql @@ -0,0 +1,51 @@ +CREATE TABLE avtale ( + id uuid primary key, + opprettet_tidspunkt timestamp without time zone not null default now(), + versjon integer not null default 1, + + deltaker_fornavn varchar(255), + deltaker_etternavn varchar(255), + deltaker_fnr varchar(11), + + bedrift_navn varchar(255), + bedrift_nr varchar(255), + + arbeidsgiver_fnr varchar(11), + arbeidsgiver_fornavn varchar(255), + arbeidsgiver_etternavn varchar(255), + arbeidsgiver_tlf varchar(255), + + veileder_nav_ident varchar(7), + veileder_fornavn varchar(255), + veileder_etternavn varchar(255), + veileder_tlf varchar(255), + + oppfolging varchar(255), + tilrettelegging varchar(255), + + start_dato date, + arbeidstrening_lengde integer, + arbeidstrening_stillingprosent integer, + + godkjent_av_deltaker boolean, + godkjent_av_arbeidsgiver boolean, + godkjent_av_veileder boolean +); + +CREATE TABLE maal ( + id uuid primary key, + opprettet_tidspunkt timestamp without time zone not null default now(), + kategori varchar(255), + beskrivelse varchar(255), + avtale uuid +); + + +CREATE TABLE oppgave ( + id uuid primary key, + opprettet_tidspunkt timestamp without time zone not null default now(), + tittel varchar(255), + beskrivelse varchar(255), + opplaering varchar(255), + avtale uuid +); diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V20__endre_fra_stillingbeskrivelse_til_arbeidsoppgaver.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V20__endre_fra_stillingbeskrivelse_til_arbeidsoppgaver.sql new file mode 100644 index 000000000..c5b918cee --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V20__endre_fra_stillingbeskrivelse_til_arbeidsoppgaver.sql @@ -0,0 +1 @@ +alter table avtale_innhold rename column stillingbeskrivelse to arbeidsoppgaver; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V21__mentor_nye_kolonner.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V21__mentor_nye_kolonner.sql new file mode 100644 index 000000000..3566a5d56 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V21__mentor_nye_kolonner.sql @@ -0,0 +1,5 @@ +alter table avtale_innhold add column mentor_fornavn varchar; +alter table avtale_innhold add column mentor_etternavn varchar; +alter table avtale_innhold add column mentor_oppgaver varchar; +alter table avtale_innhold add column mentor_antall_timer integer; +alter table avtale_innhold add column mentor_timelonn integer; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V22__endre_stillingstype_til_stillingstittel.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V22__endre_stillingstype_til_stillingstittel.sql new file mode 100644 index 000000000..3633c9b0d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V22__endre_stillingstype_til_stillingstittel.sql @@ -0,0 +1 @@ +alter table avtale_innhold rename column stillingtype to stillingstittel; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V23__avbryt_avtale_nye_kolonner.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V23__avbryt_avtale_nye_kolonner.sql new file mode 100644 index 000000000..b421cc0a5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V23__avbryt_avtale_nye_kolonner.sql @@ -0,0 +1,2 @@ +alter table avtale add column avbrutt_dato date; +alter table avtale add column avbrutt_grunn varchar; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V24__familietilknytning_nye_kolonner.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V24__familietilknytning_nye_kolonner.sql new file mode 100644 index 000000000..db1d491e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V24__familietilknytning_nye_kolonner.sql @@ -0,0 +1,2 @@ +alter table avtale_innhold add column har_familietilknytning boolean; +alter table avtale_innhold add column familietilknytning_forklaring varchar; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V25__hendelselogg.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V25__hendelselogg.sql new file mode 100644 index 000000000..697e2d927 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V25__hendelselogg.sql @@ -0,0 +1,7 @@ +create table hendelselogg( + id uuid primary key, + avtale_id uuid references avtale(id), + tidspunkt timestamp without time zone not null, + utført_av varchar, + hendelse varchar +); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V26__bjellvarsel_hendelsetype.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V26__bjellvarsel_hendelsetype.sql new file mode 100644 index 000000000..9aed62559 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V26__bjellvarsel_hendelsetype.sql @@ -0,0 +1 @@ +alter table bjelle_varsel add column varslbar_hendelse_type varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V27__opprettet_av_arbeidsgiver.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V27__opprettet_av_arbeidsgiver.sql new file mode 100644 index 000000000..1af75f502 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V27__opprettet_av_arbeidsgiver.sql @@ -0,0 +1,2 @@ +alter table avtale add column opprettet_av_arbeidsgiver boolean not null default false; +alter table avtale add column utkast_akseptert boolean not null default false; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V28__lonntilskudd_beregning.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V28__lonntilskudd_beregning.sql new file mode 100644 index 000000000..006643716 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V28__lonntilskudd_beregning.sql @@ -0,0 +1,6 @@ +alter table avtale_innhold add column feriepenger_belop integer; +alter table avtale_innhold add column otp_belop integer; +alter table avtale_innhold add column arbeidsgiveravgift_belop integer; +alter table avtale_innhold add column sum_lonnsutgifter integer; +alter table avtale_innhold add column sum_lonnstilskudd integer; + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V29__fjernet_utkast_akseptert.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V29__fjernet_utkast_akseptert.sql new file mode 100644 index 000000000..7fc16ffce --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V29__fjernet_utkast_akseptert.sql @@ -0,0 +1 @@ +alter table avtale drop column utkast_akseptert; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V2__expandInputFields.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V2__expandInputFields.sql new file mode 100644 index 000000000..046fb5df9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V2__expandInputFields.sql @@ -0,0 +1,5 @@ +ALTER TABLE avtale ALTER COLUMN oppfolging TYPE varchar(1000); +ALTER TABLE avtale ALTER COLUMN tilrettelegging TYPE varchar(1000); +ALTER TABLE maal ALTER COLUMN beskrivelse TYPE varchar(1000); +ALTER TABLE oppgave ALTER COLUMN beskrivelse TYPE varchar(1000); +ALTER TABLE oppgave ALTER COLUMN opplaering TYPE varchar(1000); diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V30__stillingstype.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V30__stillingstype.sql new file mode 100644 index 000000000..e5fdd3c31 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V30__stillingstype.sql @@ -0,0 +1 @@ +alter table avtale_innhold add column stillingstype varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V31__styrk08_konsept_id.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V31__styrk08_konsept_id.sql new file mode 100644 index 000000000..c7277215e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V31__styrk08_konsept_id.sql @@ -0,0 +1,2 @@ +alter table avtale_innhold add column stilling_styrk08 numeric; +alter table avtale_innhold add column stilling_konsept_id numeric; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V32__lonn_full_stilling.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V32__lonn_full_stilling.sql new file mode 100644 index 000000000..555d5f782 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V32__lonn_full_stilling.sql @@ -0,0 +1 @@ +alter table avtale_innhold add column manedslonn100pst integer; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V33__ny_tabell_tilskuddsperioder.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V33__ny_tabell_tilskuddsperioder.sql new file mode 100644 index 000000000..e5f4ea819 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V33__ny_tabell_tilskuddsperioder.sql @@ -0,0 +1,8 @@ +create table tilskudd_periode +( + id uuid primary key, + avtale_innhold uuid, + beløp integer, + start_dato date, + slutt_dato date +); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V34__otp_sats_avtaleinnhold.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V34__otp_sats_avtaleinnhold.sql new file mode 100644 index 000000000..f4c3a17e2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V34__otp_sats_avtaleinnhold.sql @@ -0,0 +1,6 @@ +alter table avtale_innhold add column otp_sats decimal; + +update avtale_innhold set otp_sats = 0.02 +where avtale +in (select id from avtale a where a.tiltakstype = 'MIDLERTIDIG_LONNSTILSKUDD' OR a.tiltakstype = 'VARIG_LONNSTILSKUDD' ) + and otp_sats is null; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V35__godkjent_av_nav_ident_avtaleinnhold.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V35__godkjent_av_nav_ident_avtaleinnhold.sql new file mode 100644 index 000000000..a5de0ab65 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V35__godkjent_av_nav_ident_avtaleinnhold.sql @@ -0,0 +1,2 @@ +alter table avtale_innhold + add column godkjent_av_nav_ident varchar; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V36__beslutter_nav_ident_tilskudd_periode.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V36__beslutter_nav_ident_tilskudd_periode.sql new file mode 100644 index 000000000..bd5d2c17a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V36__beslutter_nav_ident_tilskudd_periode.sql @@ -0,0 +1,4 @@ +alter table tilskudd_periode + add column godkjent_av_nav_ident varchar; +alter table tilskudd_periode + add column godkjent_tidspunkt timestamp without time zone; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V37__oppfolgingsenhet_og_geo_enhet.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V37__oppfolgingsenhet_og_geo_enhet.sql new file mode 100644 index 000000000..34e9d4eb9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V37__oppfolgingsenhet_og_geo_enhet.sql @@ -0,0 +1,4 @@ +alter table avtale + add column enhet_oppfolging varchar(4); +alter table avtale + add column enhet_geografisk varchar(4); diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V38__tilskuddsperiode_status.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V38__tilskuddsperiode_status.sql new file mode 100644 index 000000000..7880d055d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V38__tilskuddsperiode_status.sql @@ -0,0 +1,2 @@ +alter table tilskudd_periode + add column status varchar; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V39__migrere_tilskuddsperiode_status_type.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V39__migrere_tilskuddsperiode_status_type.sql new file mode 100644 index 000000000..1c061cade --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V39__migrere_tilskuddsperiode_status_type.sql @@ -0,0 +1,2 @@ +update tilskudd_periode + SET STATUS = 'UBEHANDLET'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V3__bedriftnr_null_til_tom_streng.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V3__bedriftnr_null_til_tom_streng.sql new file mode 100644 index 000000000..5580a9983 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V3__bedriftnr_null_til_tom_streng.sql @@ -0,0 +1 @@ +update avtale set bedrift_nr = '' where bedrift_nr is null; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V40__tilskuddsperiode_avslag.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V40__tilskuddsperiode_avslag.sql new file mode 100644 index 000000000..e705bceba --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V40__tilskuddsperiode_avslag.sql @@ -0,0 +1,10 @@ +alter table tilskudd_periode add column avslagsforklaring varchar; +alter table tilskudd_periode add column avslått_av_nav_ident varchar; +alter table tilskudd_periode add column avslått_tidspunkt timestamp without time zone; + +create table tilskudd_periode_avslagsårsaker +( + tilskudd_periode_id uuid references tilskudd_periode(id), + avslagsårsaker varchar, + primary key (tilskudd_periode_id, avslagsårsaker) +); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V41__lonnstilskudd_prosent_tilskudd.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V41__lonnstilskudd_prosent_tilskudd.sql new file mode 100644 index 000000000..e15658021 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V41__lonnstilskudd_prosent_tilskudd.sql @@ -0,0 +1 @@ +alter table tilskudd_periode add column lonnstilskudd_prosent integer default 0; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V42__lonnstilskudd_reduksjonsdato_og_sum.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V42__lonnstilskudd_reduksjonsdato_og_sum.sql new file mode 100644 index 000000000..f40523f67 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V42__lonnstilskudd_reduksjonsdato_og_sum.sql @@ -0,0 +1,2 @@ +alter table avtale_innhold add column sum_lønnstilskudd_redusert integer; +alter table avtale_innhold add column dato_for_redusert_prosent date; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V43__string_enum.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V43__string_enum.sql new file mode 100644 index 000000000..f6a45046a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V43__string_enum.sql @@ -0,0 +1,30 @@ +update varslbar_hendelse +set varslbar_hendelse_type = (case varslbar_hendelse_type + when '0' then 'OPPRETTET' + when '1' then 'GODKJENT_AV_ARBEIDSGIVER' + when '2' then 'GODKJENT_AV_VEILEDER' + when '3' then 'GODKJENT_AV_DELTAKER' + when '4' then 'GODKJENT_PAA_VEGNE_AV' + when '5' then 'GODKJENNINGER_OPPHEVET_AV_ARBEIDSGIVER' + when '6' then 'GODKJENNINGER_OPPHEVET_AV_VEILEDER' + when '7' then 'SMS_VARSLING_FEILET' + when '8' then 'ENDRET' + when '9' then 'DELT_MED_DELTAKER' + when '10' then 'DELT_MED_ARBEIDSGIVER' + when '11' then 'AVBRUTT' + when '12' then 'LÅST_OPP' + when '13' then 'GJENOPPRETTET' + when '14' then 'OPPRETTET_AV_ARBEIDSGIVER' + when '15' then 'NY_VEILEDER' + when '16' then 'AVTALE_FORDELT' + when '17' then 'TILSKUDDSPERIODE_AVSLATT' + when '18' then 'TILSKUDDSPERIODE_GODKJENT' + + else varslbar_hendelse_type end); + +update sms_varsel +set status = (case status + when '0' then 'USENDT' + when '1' then 'SENDT' + when '2' then 'FEIL' + else status end); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V44__ny_tabell_varsel.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V44__ny_tabell_varsel.sql new file mode 100644 index 000000000..2230d0066 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V44__ny_tabell_varsel.sql @@ -0,0 +1,13 @@ +create table varsel +( + id uuid primary key, + lest boolean, + identifikator varchar(11), + tekst varchar, + avtale_id uuid references avtale (id), + hendelse_type varchar, + tidspunkt timestamp without time zone not null default now(), + bjelle boolean, + utført_av varchar, + mottaker varchar +); diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V45__slettemerking.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V45__slettemerking.sql new file mode 100644 index 000000000..3c9155b7a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V45__slettemerking.sql @@ -0,0 +1 @@ +alter table avtale add column slettemerket boolean default false; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V46__tilskuddsperiode_references_avtale.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V46__tilskuddsperiode_references_avtale.sql new file mode 100644 index 000000000..8dd159215 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V46__tilskuddsperiode_references_avtale.sql @@ -0,0 +1,5 @@ +delete from tilskudd_periode_avslagsårsaker; +delete from tilskudd_periode; + +alter table tilskudd_periode drop column avtale_innhold; +alter table tilskudd_periode add column avtale_id uuid references avtale(id); \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V47__tilskuddsperiode_l\303\270penummer.sql" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V47__tilskuddsperiode_l\303\270penummer.sql" new file mode 100644 index 000000000..a24ee68a8 --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V47__tilskuddsperiode_l\303\270penummer.sql" @@ -0,0 +1,4 @@ +delete from tilskudd_periode_avslagsårsaker; +delete from tilskudd_periode; + +alter table tilskudd_periode add column løpenummer integer; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V48__avtalenr.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V48__avtalenr.sql new file mode 100644 index 000000000..2fe3adde8 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V48__avtalenr.sql @@ -0,0 +1 @@ +alter table avtale add column avtale_nr serial unique; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V49__annullert.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V49__annullert.sql new file mode 100644 index 000000000..e17bec425 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V49__annullert.sql @@ -0,0 +1,2 @@ +alter table avtale add column annullert_tidspunkt timestamp; +alter table avtale add column annullert_grunn varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V4__ny_kolonne_journalpost_id.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V4__ny_kolonne_journalpost_id.sql new file mode 100644 index 000000000..31278c38c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V4__ny_kolonne_journalpost_id.sql @@ -0,0 +1 @@ +alter table avtale add column journalpost_id varchar(9); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V50__avtale_forkortet.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V50__avtale_forkortet.sql new file mode 100644 index 000000000..fd20a2cc0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V50__avtale_forkortet.sql @@ -0,0 +1,9 @@ +create table avtale_forkortet( + id uuid primary key, + avtale_id uuid references avtale(id), + avtale_innhold_id uuid, + tidspunkt timestamp, + ny_slutt_dato date, + grunn varchar, + annet_grunn varchar +); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V51__feilregistrert.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V51__feilregistrert.sql new file mode 100644 index 000000000..0225c4369 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V51__feilregistrert.sql @@ -0,0 +1 @@ +alter table avtale add column feilregistrert boolean default false; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V52__antall_dager_per_uke.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V52__antall_dager_per_uke.sql new file mode 100644 index 000000000..f3f8edf4f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V52__antall_dager_per_uke.sql @@ -0,0 +1 @@ +alter table avtale_innhold add column antall_dager_per_uke integer; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V53__avtale_forkortet_utfort_av.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V53__avtale_forkortet_utfort_av.sql new file mode 100644 index 000000000..ef53db4c7 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V53__avtale_forkortet_utfort_av.sql @@ -0,0 +1 @@ +alter table avtale_forkortet add column utført_av varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V54__avtale_innhold_gjeldende_fra.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V54__avtale_innhold_gjeldende_fra.sql new file mode 100644 index 000000000..96d6abc14 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V54__avtale_innhold_gjeldende_fra.sql @@ -0,0 +1,2 @@ +alter table avtale_innhold add column ikrafttredelsestidspunkt timestamp without time zone; +update avtale_innhold set ikrafttredelsestidspunkt=godkjent_av_veileder; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V55__dvh_melding.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V55__dvh_melding.sql new file mode 100644 index 000000000..e8d37d833 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V55__dvh_melding.sql @@ -0,0 +1,8 @@ +create table dvh_melding +( + melding_id uuid primary key, + avtale_id uuid references avtale (id), + tidspunkt timestamp, + tiltak_status varchar, + json varchar +); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V56__tilskuddsperiode_aktiv.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V56__tilskuddsperiode_aktiv.sql new file mode 100644 index 000000000..684b1dda2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V56__tilskuddsperiode_aktiv.sql @@ -0,0 +1 @@ +alter table tilskudd_periode add column aktiv boolean not null default true; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V57__godkjent_av_beslutter.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V57__godkjent_av_beslutter.sql new file mode 100644 index 000000000..3621dfce9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V57__godkjent_av_beslutter.sql @@ -0,0 +1,4 @@ +alter table avtale_innhold add column avtale_inngått timestamp without time zone; +update avtale_innhold set avtale_inngått=godkjent_av_veileder; +alter table avtale_innhold add column godkjent_av_beslutter timestamp without time zone; +alter table avtale_innhold add column godkjent_av_beslutter_nav_ident varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V58__dvh_melding_sendingsbekreftelse.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V58__dvh_melding_sendingsbekreftelse.sql new file mode 100644 index 000000000..7a8e1b1b6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V58__dvh_melding_sendingsbekreftelse.sql @@ -0,0 +1 @@ +alter table dvh_melding add column sendt boolean not null default false; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V59__innholdtype.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V59__innholdtype.sql new file mode 100644 index 000000000..e1a0e5061 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V59__innholdtype.sql @@ -0,0 +1,3 @@ +alter table avtale_innhold add column innhold_type varchar; +update avtale_innhold set innhold_type = 'INNGÅ' where versjon = 1; +update avtale_innhold set innhold_type = 'LÅSE_OPP' where versjon > 1; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V5__legg_til_dato_godkjenning.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V5__legg_til_dato_godkjenning.sql new file mode 100644 index 000000000..69d1f650b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V5__legg_til_dato_godkjenning.sql @@ -0,0 +1,7 @@ +alter table avtale add dato_godkjent_deltaker timestamp without time zone; +alter table avtale add dato_godkjent_arbeidsgiver timestamp without time zone; +alter table avtale add dato_godkjent_veileder timestamp without time zone; + +update avtale set dato_godkjent_deltaker = '2019-01-01T00:00:00.000' where godkjent_av_deltaker is true; +update avtale set dato_godkjent_arbeidsgiver = '2019-01-01T00:00:00.000' where godkjent_av_arbeidsgiver is true; +update avtale set dato_godkjent_veileder = '2019-01-01T00:00:00.000' where godkjent_av_veileder is true; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V60__tilskuddsperiode_enhet.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V60__tilskuddsperiode_enhet.sql new file mode 100644 index 000000000..5a51d7012 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V60__tilskuddsperiode_enhet.sql @@ -0,0 +1 @@ +alter table tilskudd_periode add column enhet varchar(4); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V61__godkjent_pa_vegne_av_ag.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V61__godkjent_pa_vegne_av_ag.sql new file mode 100644 index 000000000..be9411876 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V61__godkjent_pa_vegne_av_ag.sql @@ -0,0 +1,5 @@ +alter table avtale_innhold add column godkjent_pa_vegne_av_arbeidsgiver boolean; + +alter table avtale_innhold add column klarer_ikke_gi_fa_tilgang boolean; +alter table avtale_innhold add column vet_ikke_hvem_som_kan_gi_tilgang boolean; +alter table avtale_innhold add column far_ikke_tilgang_personvern boolean; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V62__legg_til_geoNavnEnhet_og_oppfolgingNavnEnhet.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V62__legg_til_geoNavnEnhet_og_oppfolgingNavnEnhet.sql new file mode 100644 index 000000000..ff5f7c9e9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V62__legg_til_geoNavnEnhet_og_oppfolgingNavnEnhet.sql @@ -0,0 +1,2 @@ +alter table avtale add column enhetsnavn_geografisk varchar; +alter table avtale add column enhetsnavn_oppfolging varchar; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V63__legg_til_tilskuddsperiode_enhetsnavn.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V63__legg_til_tilskuddsperiode_enhetsnavn.sql new file mode 100644 index 000000000..fca212211 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V63__legg_til_tilskuddsperiode_enhetsnavn.sql @@ -0,0 +1,3 @@ +alter table tilskudd_periode add column enhetsnavn varchar; +alter table avtale_innhold add column enhet_kostnadssted varchar; +alter table avtale_innhold add column enhetsnavn_kostnadssted varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V64__opprette_notifikasjon_tabell.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V64__opprette_notifikasjon_tabell.sql new file mode 100644 index 000000000..ef1f462e6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V64__opprette_notifikasjon_tabell.sql @@ -0,0 +1,14 @@ +create table arbeidsgiver_notifikasjon +( + id uuid primary key, + tidspunkt timestamp without time zone not null default now(), + avtale_id uuid references avtale(id), + hendelse_type varchar, + virksomhetsnummer varchar, + lenke varchar, + service_code integer, + service_edition integer, + varsel_sendt_vellykket boolean, + status_response varchar, + notifikasjon_aktiv boolean +); diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V65__legge_til_oppgave_referanse_notifikasjon.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V65__legge_til_oppgave_referanse_notifikasjon.sql new file mode 100644 index 000000000..2e36e0397 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V65__legge_til_oppgave_referanse_notifikasjon.sql @@ -0,0 +1 @@ +alter table arbeidsgiver_notifikasjon add column notifikasjon_referanse_id varchar; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V66__legge_til_operasjon_type_notifikasjon.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V66__legge_til_operasjon_type_notifikasjon.sql new file mode 100644 index 000000000..6230b809e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V66__legge_til_operasjon_type_notifikasjon.sql @@ -0,0 +1 @@ +alter table arbeidsgiver_notifikasjon add column operasjon_type varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V67__legge_til_kvalifiseringsgruppe_avtale.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V67__legge_til_kvalifiseringsgruppe_avtale.sql new file mode 100644 index 000000000..eee4699cc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V67__legge_til_kvalifiseringsgruppe_avtale.sql @@ -0,0 +1,2 @@ +alter table avtale add column kvalifiseringsgruppe varchar; +alter table avtale add column formidlingsgruppe varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V68__legge_til_er_godkjent_for_etterregistrering_avtale.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V68__legge_til_er_godkjent_for_etterregistrering_avtale.sql new file mode 100644 index 000000000..4dc083498 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V68__legge_til_er_godkjent_for_etterregistrering_avtale.sql @@ -0,0 +1 @@ +alter table avtale add column godkjent_for_etterregistrering boolean; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V69__etterregistrering_default_false.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V69__etterregistrering_default_false.sql new file mode 100644 index 000000000..92ab20890 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V69__etterregistrering_default_false.sql @@ -0,0 +1,2 @@ +update avtale set godkjent_for_etterregistrering = false; +alter table avtale alter column godkjent_for_etterregistrering set default false; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V6__godkjenninger_og_pa_vegne_av.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V6__godkjenninger_og_pa_vegne_av.sql new file mode 100644 index 000000000..b0c985712 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V6__godkjenninger_og_pa_vegne_av.sql @@ -0,0 +1,17 @@ +ALTER TABLE avtale RENAME COLUMN godkjent_av_deltaker TO gammel_godkjent_av_deltaker; +ALTER TABLE avtale RENAME COLUMN godkjent_av_arbeidsgiver TO gammel_godkjent_av_arbeidsgiver; +ALTER TABLE avtale RENAME COLUMN godkjent_av_veileder TO gammel_godkjent_av_veileder; + + +ALTER TABLE avtale RENAME COLUMN dato_godkjent_deltaker TO godkjent_av_deltaker; +ALTER TABLE avtale RENAME COLUMN dato_godkjent_arbeidsgiver TO godkjent_av_arbeidsgiver; +ALTER TABLE avtale RENAME COLUMN dato_godkjent_veileder TO godkjent_av_veileder; + +alter table avtale add godkjent_pa_vegne_av boolean default false; + +create table godkjent_pa_vegne_grunn ( + avtale uuid primary key references avtale(id), + ikke_bank_id boolean default false, + reservert boolean default false, + digital_kompetanse boolean default false +); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V70__avtale_innhold_primary_key.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V70__avtale_innhold_primary_key.sql new file mode 100644 index 000000000..eda57a96c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V70__avtale_innhold_primary_key.sql @@ -0,0 +1,2 @@ +alter table avtale_innhold alter column id set not null; +alter table avtale_innhold add primary key (id); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V71__avtale_innhold_index.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V71__avtale_innhold_index.sql new file mode 100644 index 000000000..1145c85b9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V71__avtale_innhold_index.sql @@ -0,0 +1,2 @@ +alter table avtale_innhold add foreign key (avtale) references avtale (id); +create index on avtale_innhold(avtale); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V72__gjeldende_innhold.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V72__gjeldende_innhold.sql new file mode 100644 index 000000000..5df3f7c3a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V72__gjeldende_innhold.sql @@ -0,0 +1,2 @@ +alter table avtale add column gjeldende_innhold_id uuid references avtale_innhold(id); +update avtale set gjeldende_innhold_id=(select id from avtale_innhold ai where avtale.id=ai.avtale and versjon=(select max(versjon) from avtale_innhold ai2 where ai2.avtale=avtale.id)); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V73__legge_til_refusjon_kontaktperson_avtale_innhold.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V73__legge_til_refusjon_kontaktperson_avtale_innhold.sql new file mode 100644 index 000000000..0b152b851 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V73__legge_til_refusjon_kontaktperson_avtale_innhold.sql @@ -0,0 +1,4 @@ +alter table avtale_innhold add column refusjon_kontaktperson_fornavn varchar; +alter table avtale_innhold add column refusjon_kontaktperson_etternavn varchar; +alter table avtale_innhold add column refusjon_kontaktperson_tlf varchar; +alter table avtale_innhold add column ønsker_informasjon_om_refusjon boolean; \ No newline at end of file diff --git "a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V74__rename_til_\303\270nsker_varsling_avtale_innhold.sql" "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V74__rename_til_\303\270nsker_varsling_avtale_innhold.sql" new file mode 100644 index 000000000..6ee4e2dd5 --- /dev/null +++ "b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V74__rename_til_\303\270nsker_varsling_avtale_innhold.sql" @@ -0,0 +1 @@ +alter table avtale_innhold rename column ønsker_informasjon_om_refusjon to ønsker_varsling_om_refusjon; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V75__ny_sms_tabell.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V75__ny_sms_tabell.sql new file mode 100644 index 000000000..65afb4feb --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V75__ny_sms_tabell.sql @@ -0,0 +1,10 @@ +create table sms ( + sms_varsel_id uuid primary key, + telefonnummer varchar, + identifikator varchar, + meldingstekst varchar, + avtale_id uuid references avtale(id), + tidspunkt timestamp without time zone, + hendelse_type varchar, + avsender_applikasjon varchar +); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V76__varslbar_hendelse_til_sporingslogg.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V76__varslbar_hendelse_til_sporingslogg.sql new file mode 100644 index 000000000..34bed5c7c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V76__varslbar_hendelse_til_sporingslogg.sql @@ -0,0 +1,2 @@ +alter table varslbar_hendelse rename to sporingslogg; +alter table sporingslogg rename column varslbar_hendelse_type to hendelse_type; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V77__ubrukte_tabeller_endre_navn.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V77__ubrukte_tabeller_endre_navn.sql new file mode 100644 index 000000000..db931c6da --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V77__ubrukte_tabeller_endre_navn.sql @@ -0,0 +1,3 @@ +alter table bjelle_varsel rename to ikke_i_bruk_bjelle_varsel; +alter table sms_varsel rename to ikke_i_bruk_sms_varsel; +alter table hendelselogg rename to ikke_i_bruk_hendelselogg; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V78__inkluderingstilskudd.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V78__inkluderingstilskudd.sql new file mode 100644 index 000000000..603581936 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V78__inkluderingstilskudd.sql @@ -0,0 +1,10 @@ +CREATE TABLE inkluderingstilskuddsutgift +( + id uuid primary key, + avtale_innhold_id uuid references avtale_innhold(id), + beløp integer, + type varchar, + tidspunkt_lagt_til timestamp without time zone not null default now() +); + +alter table avtale_innhold add column inkluderingstilskudd_begrunnelse varchar; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V79__avtale_mentor_fnr_og_tlf.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V79__avtale_mentor_fnr_og_tlf.sql new file mode 100644 index 000000000..49602a0ad --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V79__avtale_mentor_fnr_og_tlf.sql @@ -0,0 +1,3 @@ +alter table avtale add column mentor_fnr varchar(11) default null; +alter table avtale_innhold add column godkjent_Taushetserklæring_Av_Mentor timestamp without time zone default null; +alter table avtale_innhold add column mentor_tlf varchar(255) default null; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V7__oppdatere_godkjent_av_dato.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V7__oppdatere_godkjent_av_dato.sql new file mode 100644 index 000000000..bbf017c2f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V7__oppdatere_godkjent_av_dato.sql @@ -0,0 +1,3 @@ +update avtale set godkjent_av_deltaker = '2019-01-01T00:00:00.000' where gammel_godkjent_av_deltaker is true; +update avtale set godkjent_av_arbeidsgiver = '2019-01-01T00:00:00.000' where gammel_godkjent_av_arbeidsgiver is true; +update avtale set godkjent_av_veileder = '2019-01-01T00:00:00.000' where gammel_godkjent_av_veileder is true; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V80__refusjonstatus_tilskuddsperiode.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V80__refusjonstatus_tilskuddsperiode.sql new file mode 100644 index 000000000..5cc376a0b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V80__refusjonstatus_tilskuddsperiode.sql @@ -0,0 +1,11 @@ +alter table tilskudd_periode + add column refusjon_status varchar; + +-- Flytt tidligere UTBETALT status fra status til refusjon_status +UPDATE tilskudd_periode +set REFUSJON_STATUS = 'UTBETALT' +where STATUS = 'UTBETALT'; + +UPDATE tilskudd_periode +set STATUS = 'GODKJENT' +where STATUS = 'UTBETALT'; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V8__ny_kolonne_deltaker_tlf.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V8__ny_kolonne_deltaker_tlf.sql new file mode 100644 index 000000000..928171197 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V8__ny_kolonne_deltaker_tlf.sql @@ -0,0 +1 @@ +alter table avtale add column deltaker_tlf varchar(255); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V90__avtale_melding.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V90__avtale_melding.sql new file mode 100644 index 000000000..62583c9f5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V90__avtale_melding.sql @@ -0,0 +1,10 @@ +create table avtale_melding +( + melding_id uuid primary key, + avtale_id uuid references avtale (id), + avtale_status varchar, + tidspunkt timestamp, + hendelse_type varchar, + json varchar, + sendt boolean not null default false +); diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V91__mentor_anntall_timer_decimal.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V91__mentor_anntall_timer_decimal.sql new file mode 100644 index 000000000..78a087544 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V91__mentor_anntall_timer_decimal.sql @@ -0,0 +1 @@ +alter table avtale_innhold alter column mentor_antall_timer type decimal(3,1); diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V92__avtale_melding_compacted.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V92__avtale_melding_compacted.sql new file mode 100644 index 000000000..a37dd9584 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V92__avtale_melding_compacted.sql @@ -0,0 +1 @@ +alter table avtale_melding add column sendt_compacted boolean not null default false diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V93__arena_migrering_vegne_av.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V93__arena_migrering_vegne_av.sql new file mode 100644 index 000000000..eeda24a66 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V93__arena_migrering_vegne_av.sql @@ -0,0 +1,2 @@ +alter table avtale_innhold add column arena_migrering_deltaker boolean default false; +alter table avtale_innhold add column arena_migrering_arbeidsgiver boolean default false; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V94__arena_migrering_vegne_av_fiks.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V94__arena_migrering_vegne_av_fiks.sql new file mode 100644 index 000000000..059eaa140 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V94__arena_migrering_vegne_av_fiks.sql @@ -0,0 +1,4 @@ +alter table avtale_innhold drop column arena_migrering_deltaker; +alter table avtale_innhold drop column arena_migrering_arbeidsgiver; +alter table avtale_innhold add column arena_migrering_deltaker boolean; +alter table avtale_innhold add column arena_migrering_arbeidsgiver boolean; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V95__arena_migrering_vegne_av_fiks_setter_false.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V95__arena_migrering_vegne_av_fiks_setter_false.sql new file mode 100644 index 000000000..704873e60 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V95__arena_migrering_vegne_av_fiks_setter_false.sql @@ -0,0 +1,2 @@ +update avtale_innhold set arena_migrering_deltaker = false where godkjent_pa_vegne_av = true; +update avtale_innhold set arena_migrering_arbeidsgiver = false where godkjent_pa_vegne_av_arbeidsgiver = true; diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V96__arenaryddeavtaler_tabell.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V96__arenaryddeavtaler_tabell.sql new file mode 100644 index 000000000..05cfd0a87 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V96__arenaryddeavtaler_tabell.sql @@ -0,0 +1,5 @@ +create table arena_rydde_avtale +( + id uuid primary key, + avtale uuid references avtale (id) +); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V97__varsel_utfort_av_identifikator.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V97__varsel_utfort_av_identifikator.sql new file mode 100644 index 000000000..eca225190 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V97__varsel_utfort_av_identifikator.sql @@ -0,0 +1 @@ +alter table varsel add column utført_av_identifikator varchar(11) default null; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V98__arenaryddeavtaler_migreringsdato.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V98__arenaryddeavtaler_migreringsdato.sql new file mode 100644 index 000000000..6d1cd43c9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V98__arenaryddeavtaler_migreringsdato.sql @@ -0,0 +1 @@ +alter table arena_rydde_avtale add column migreringsdato date; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V99__filtersok_tabell.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V99__filtersok_tabell.sql new file mode 100644 index 000000000..ad2ccf1d5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V99__filtersok_tabell.sql @@ -0,0 +1,7 @@ +create table filter_sok +( + sok_id varchar primary key, + sist_sokt_tidspunkt timestamp without time zone not null, + query_parametre text not null, + antall_ganger_sokt integer not null +); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V9__nye_tabeller_varsling.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V9__nye_tabeller_varsling.sql new file mode 100644 index 000000000..d2252e109 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/db/migration/V9__nye_tabeller_varsling.sql @@ -0,0 +1,17 @@ +create table varslbar_hendelse +( + id uuid primary key, + tidspunkt timestamp without time zone not null default now(), + avtale_id uuid, + varslbar_hendelse_type varchar +); + +create table sms_varsel +( + id uuid primary key, + varslbar_hendelse uuid, + status varchar, + telefonnummer varchar(255), + identifikator varchar(11), + meldingstekst varchar +); \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/logback-spring.xml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..2c93624c9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/logback-spring.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/pdl/hentIdenter.graphql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/pdl/hentIdenter.graphql new file mode 100644 index 000000000..9029472bb --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/pdl/hentIdenter.graphql @@ -0,0 +1,9 @@ +query($ident: ID!) { + hentIdenter(ident: $ident, grupper: [AKTORID]) { + identer { + ident + gruppe + historisk + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/pdl/hentPerson.adressebeskyttelse.graphql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/pdl/hentPerson.adressebeskyttelse.graphql new file mode 100644 index 000000000..3c8209442 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/pdl/hentPerson.adressebeskyttelse.graphql @@ -0,0 +1,7 @@ +query($ident: ID!) { + hentPerson(ident: $ident) { + adressebeskyttelse { + gradering + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/pdl/hentPersondata.graphql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/pdl/hentPersondata.graphql new file mode 100644 index 000000000..b1ab950d6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/pdl/hentPersondata.graphql @@ -0,0 +1,19 @@ +query($ident: ID!) { + hentPerson(ident: $ident) { + navn { + fornavn + mellomnavn + etternavn + } + adressebeskyttelse { + gradering + } + } + hentGeografiskTilknytning(ident: $ident){ + gtType + gtKommune + gtBydel + gtLand + regel + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/scripts/varsel_migrering.sql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/scripts/varsel_migrering.sql new file mode 100644 index 000000000..6cd11d90b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/scripts/varsel_migrering.sql @@ -0,0 +1,215 @@ +-- OPPRETTET ARBEIDSGIVER +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale er opprettet', avtale_id, hendelse, tidspunkt, true, 'ARBEIDSGIVER', 'DELTAKER' +FROM hendelselogg WHERE hendelse = 'OPPRETTET_AV_ARBEIDSGIVER'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale er opprettet', avtale_id, hendelse, tidspunkt, false, 'ARBEIDSGIVER', 'ARBEIDSGIVER' +FROM hendelselogg WHERE hendelse = 'OPPRETTET_AV_ARBEIDSGIVER'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale er opprettet', avtale_id, hendelse, tidspunkt, true, 'ARBEIDSGIVER', 'VEILEDER' +FROM hendelselogg WHERE hendelse = 'OPPRETTET_AV_ARBEIDSGIVER'; +----------------------------------------------------------------------- + +-- OPPRETTET VEILEDER +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale er opprettet', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'VEILEDER', 'DELTAKER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'OPPRETTET' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale er opprettet', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'VEILEDER', 'ARBEIDSGIVER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'OPPRETTET' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale er opprettet', avtale_id, varslbar_hendelse_type, tidspunkt, false, 'VEILEDER', 'VEILEDER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'OPPRETTET' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); +------------------------------------------ + +-- GODKJENT_AV_ARBEIDSGIVER +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale er godkjent av arbeidsgiver', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'ARBEIDSGIVER', 'VEILEDER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENT_AV_ARBEIDSGIVER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale er godkjent av arbeidsgiver', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'ARBEIDSGIVER', 'DELTAKER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENT_AV_ARBEIDSGIVER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale er godkjent av arbeidsgiver', avtale_id, varslbar_hendelse_type, tidspunkt, false, 'ARBEIDSGIVER', 'ARBEIDSGIVER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENT_AV_ARBEIDSGIVER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); +----------------------------------------- + +-- GODKJENT_AV_VEILEDER +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale er godkjent av veileder', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'VEILEDER', 'ARBEIDSGIVER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENT_AV_VEILEDER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale er godkjent av veileder', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'VEILEDER', 'DELTAKER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENT_AV_VEILEDER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale er godkjent av veileder', avtale_id, varslbar_hendelse_type, tidspunkt, false, 'VEILEDER', 'VEILEDER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENT_AV_VEILEDER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); +------------------------------------------- + +-- GODKJENT_AV_DELTAKER +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale er godkjent av deltaker', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'DELTAKER', 'ARBEIDSGIVER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENT_AV_DELTAKER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale er godkjent av deltaker', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'DELTAKER', 'VEILEDER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENT_AV_DELTAKER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale er godkjent av deltaker', avtale_id, varslbar_hendelse_type, tidspunkt, false, 'DELTAKER', 'DELTAKER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENT_AV_DELTAKER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); +--------------------------------------------- + +-- GODKJENT_PAA_VEGNE_AV +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Veileder godkjente avtalen på vegne av seg selv og deltaker', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'VEILEDER', 'ARBEIDSGIVER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENT_PAA_VEGNE_AV' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Veileder godkjente avtalen på vegne av seg selv og deltaker', avtale_id, varslbar_hendelse_type, tidspunkt, false, 'VEILEDER', 'VEILEDER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENT_PAA_VEGNE_AV' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Veileder godkjente avtalen på vegne av seg selv og deltaker', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'VEILEDER', 'DELTAKER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENT_PAA_VEGNE_AV' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); +------------------------------------------- + +-- GODKJENNINGER_OPPHEVET_AV_ARBEIDSGIVER +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtalens godkjenninger er opphevet av arbeidsgiver', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'ARBEIDSGIVER', 'DELTAKER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENNINGER_OPPHEVET_AV_ARBEIDSGIVER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtalens godkjenninger er opphevet av arbeidsgiver', avtale_id, varslbar_hendelse_type, tidspunkt, false, 'ARBEIDSGIVER', 'VEILEDER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENNINGER_OPPHEVET_AV_ARBEIDSGIVER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtalens godkjenninger er opphevet av arbeidsgiver', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'ARBEIDSGIVER', 'ARBEIDSGIVER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENNINGER_OPPHEVET_AV_ARBEIDSGIVER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); +--------------------------------------------- + +-- GODKJENNINGER_OPPHEVET_AV_VEILEDER +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtalens godkjenninger er opphevet av veileder', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'VEILEDER', 'ARBEIDSGIVER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENNINGER_OPPHEVET_AV_VEILEDER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtalens godkjenninger er opphevet av veileder', avtale_id, varslbar_hendelse_type, tidspunkt, false, 'VEILEDER', 'VEILEDER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENNINGER_OPPHEVET_AV_VEILEDER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtalens godkjenninger er opphevet av veileder', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'VEILEDER', 'DELTAKER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'GODKJENNINGER_OPPHEVET_AV_VEILEDER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); +--------------------------------------------- + +-- DELT_MED_DELTAKER +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale delt med deltaker', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'VEILEDER', 'DELTAKER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'DELT_MED_DELTAKER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale delt med deltaker', avtale_id, varslbar_hendelse_type, tidspunkt, false, 'VEILEDER', 'VEILEDER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'DELT_MED_DELTAKER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); +-------------------------------------------- + +-- DELT_MED_ARBEIDSGIVER +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale delt med arbeidsgiver', avtale_id, varslbar_hendelse_type, tidspunkt, true, 'VEILEDER', 'ARBEIDSGIVER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'DELT_MED_ARBEIDSGIVER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = varslbar_hendelse.avtale_id), 'Avtale delt med arbeidsgiver', avtale_id, varslbar_hendelse_type, tidspunkt, false, 'VEILEDER', 'VEILEDER' +FROM varslbar_hendelse WHERE varslbar_hendelse_type = 'DELT_MED_ARBEIDSGIVER' and exists (select 1 from avtale where avtale.id = varslbar_hendelse.avtale_id); +------------------------------------------- + +-- AVBRUTT +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale avbrutt av veileder', avtale_id, hendelse, tidspunkt, false, 'VEILEDER', 'VEILEDER' +FROM hendelselogg WHERE hendelse = 'AVBRUTT'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale avbrutt av veileder', avtale_id, hendelse, tidspunkt, true, 'VEILEDER', 'ARBEIDSGIVER' +FROM hendelselogg WHERE hendelse = 'AVBRUTT'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale avbrutt av veileder', avtale_id, hendelse, tidspunkt, true, 'VEILEDER', 'DELTAKER' +FROM hendelselogg WHERE hendelse = 'AVBRUTT'; +------------------------------------------ + +-- LÅST_OPP +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale låst opp av veileder', avtale_id, hendelse, tidspunkt, false, 'VEILEDER', 'VEILEDER' +FROM hendelselogg WHERE hendelse = 'LÅST_OPP'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale låst opp av veileder', avtale_id, hendelse, tidspunkt, true, 'VEILEDER', 'ARBEIDSGIVER' +FROM hendelselogg WHERE hendelse = 'LÅST_OPP'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale låst opp av veileder', avtale_id, hendelse, tidspunkt, true, 'VEILEDER', 'DELTAKER' +FROM hendelselogg WHERE hendelse = 'LÅST_OPP'; +------------------------------------------ + +-- GJENOPPRETTET +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale gjenopprettet', avtale_id, hendelse, tidspunkt, false, 'VEILEDER', 'VEILEDER' +FROM hendelselogg WHERE hendelse = 'GJENOPPRETTET'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale gjenopprettet', avtale_id, hendelse, tidspunkt, true, 'VEILEDER', 'ARBEIDSGIVER' +FROM hendelselogg WHERE hendelse = 'GJENOPPRETTET'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale gjenopprettet', avtale_id, hendelse, tidspunkt, true, 'VEILEDER', 'DELTAKER' +FROM hendelselogg WHERE hendelse = 'GJENOPPRETTET'; +-------------------------------------------- + +-- NY_VEILEDER +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale tildelt ny veileder', avtale_id, hendelse, tidspunkt, false, 'VEILEDER', 'VEILEDER' +FROM hendelselogg WHERE hendelse = 'NY_VEILEDER'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale tildelt ny veileder', avtale_id, hendelse, tidspunkt, true, 'VEILEDER', 'ARBEIDSGIVER' +FROM hendelselogg WHERE hendelse = 'NY_VEILEDER'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale tildelt ny veileder', avtale_id, hendelse, tidspunkt, true, 'VEILEDER', 'DELTAKER' +FROM hendelselogg WHERE hendelse = 'NY_VEILEDER'; +-------------------------------------------- + +-- AVTALE_FORDELT +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale tildelt veileder', avtale_id, hendelse, tidspunkt, false, 'VEILEDER', 'VEILEDER' +FROM hendelselogg WHERE hendelse = 'AVTALE_FORDELT'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale tildelt veileder', avtale_id, hendelse, tidspunkt, true, 'VEILEDER', 'ARBEIDSGIVER' +FROM hendelselogg WHERE hendelse = 'AVTALE_FORDELT'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale tildelt veileder', avtale_id, hendelse, tidspunkt, true, 'VEILEDER', 'DELTAKER' +FROM hendelselogg WHERE hendelse = 'AVTALE_FORDELT'; + +-------------------------------------------- + +-- ENDRET +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select veileder_nav_ident from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale endret', avtale_id, hendelse, tidspunkt, false, utført_av, 'VEILEDER' +FROM hendelselogg WHERE hendelse = 'ENDRET'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select bedrift_nr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale endret', avtale_id, hendelse, tidspunkt, false, utført_av, 'ARBEIDSGIVER' +FROM hendelselogg WHERE hendelse = 'ENDRET'; + +INSERT INTO varsel (id, lest, identifikator, tekst, avtale_id, hendelse_type, tidspunkt, bjelle, utført_av, mottaker) +SELECT uuid_in(md5(random()::text || clock_timestamp()::text)::cstring), true, (select deltaker_fnr from avtale where avtale.id = hendelselogg.avtale_id), 'Avtale endret', avtale_id, hendelse, tidspunkt, false, utført_av, 'DELTAKER' +FROM hendelselogg WHERE hendelse = 'ENDRET'; \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/hardDeleteNotifikasjon.graphql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/hardDeleteNotifikasjon.graphql new file mode 100644 index 000000000..89bb2aa80 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/hardDeleteNotifikasjon.graphql @@ -0,0 +1,11 @@ +mutation SletteNotifikasjonHelt($id: ID!) { + hardDeleteNotifikasjon(id: $id){ + __typename + ... on HardDeleteNotifikasjonVellykket { + id + } + ... on NotifikasjonFinnesIkke { + feilmelding + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/mineNotifikasjoner.graphql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/mineNotifikasjoner.graphql new file mode 100644 index 000000000..b29c8a6b0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/mineNotifikasjoner.graphql @@ -0,0 +1,25 @@ +query { + mineNotifikasjoner(merkelapp: String!) { + ... on NotifikasjonConnection { + edges { + cursor + node { + __typename + ... on Beskjed { + metadata { + id + } + } + ... on Oppgave { + metadata { + id + } + } + } + } + pageInfo { + hasNextPage + } + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/oppgaveUtfoert.graphql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/oppgaveUtfoert.graphql new file mode 100644 index 000000000..702e575ab --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/oppgaveUtfoert.graphql @@ -0,0 +1,14 @@ +mutation OppgaveUtfoert($id: ID!) { + oppgaveUtfoert(id: $id){ + __typename + ... on OppgaveUtfoertVellykket { + id + } + ... on Error { + feilmelding + } + ... on NotifikasjonFinnesIkke { + feilmelding + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/oppgaveUtfoertByEksternId.graphql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/oppgaveUtfoertByEksternId.graphql new file mode 100644 index 000000000..81fac2daa --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/oppgaveUtfoertByEksternId.graphql @@ -0,0 +1,14 @@ +mutation OppgaveUtfoertByEksternId($eksternId: ID! $merkelapp: String!) { + oppgaveUtfoertByEksternId(eksternId: $eksternId merkelapp: $merkelapp){ + __typename + ... on OppgaveUtfoertVellykket { + id + } + ... on Error { + feilmelding + } + ... on NotifikasjonFinnesIkke { + feilmelding + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/opprettNyBeskjed.graphql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/opprettNyBeskjed.graphql new file mode 100644 index 000000000..330a8bfdc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/opprettNyBeskjed.graphql @@ -0,0 +1,39 @@ +mutation OpprettNyBeskjed( + $eksternId: String! + $virksomhetsnummer: String! + $lenke: String! + $serviceCode: String! + $serviceEdition: String! + $merkelapp: String! + $tekst: String! + $grupperingsId: String! +) { + nyBeskjed(nyBeskjed: { + metadata: { + eksternId: $eksternId + grupperingsid: $grupperingsId + virksomhetsnummer: $virksomhetsnummer + } + mottakere: [ + { + altinn: { + serviceCode: $serviceCode + serviceEdition: $serviceEdition + } + } + ] + notifikasjon: { + merkelapp: $merkelapp + tekst: $tekst + lenke: $lenke + } + }) { + __typename + ... on NyBeskjedVellykket { + id + } + ... on Error { + feilmelding + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/opprettNyOppgave.graphql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/opprettNyOppgave.graphql new file mode 100644 index 000000000..d2e558f10 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/opprettNyOppgave.graphql @@ -0,0 +1,39 @@ +mutation OpprettNyOppgave( + $eksternId: String! + $virksomhetsnummer: String! + $serviceCode: String! + $serviceEdition: String! + $merkelapp: String! + $lenke: String! + $tekst: String! + $grupperingsId: String! +) { + nyOppgave(nyOppgave: { + metadata: { + eksternId: $eksternId + grupperingsid: $grupperingsId + virksomhetsnummer: $virksomhetsnummer + } + mottakere: [ + { + altinn: { + serviceCode: $serviceCode + serviceEdition: $serviceEdition + } + } + ] + notifikasjon: { + merkelapp: $merkelapp + tekst: $tekst + lenke: $lenke + } + }) { + __typename + ... on NyOppgaveVellykket { + id + } + ... on Error { + feilmelding + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/softDeleteNotifikasjon.graphql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/softDeleteNotifikasjon.graphql new file mode 100644 index 000000000..9e848070d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/softDeleteNotifikasjon.graphql @@ -0,0 +1,11 @@ +mutation SlettingAvNotifikasjoner($id: ID!) { + softDeleteNotifikasjon(id: $id) { + __typename + ... on SoftDeleteNotifikasjonVellykket { + id + } + ... on NotifikasjonFinnesIkke { + feilmelding + } + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/softDeleteNotifikasjonByEksternId.graphql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/softDeleteNotifikasjonByEksternId.graphql new file mode 100644 index 000000000..942d581b5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/softDeleteNotifikasjonByEksternId.graphql @@ -0,0 +1,11 @@ +mutation SlettingAvNotifikasjonerByEksternId($eksternId: ID! $merkelapp: String!) { + softDeleteNotifikasjonByEksternId(eksternId: $eksternId merkelapp: $merkelapp) { + __typename + ... on SoftDeleteNotifikasjonVellykket { + id + } + ... on NotifikasjonFinnesIkke { + feilmelding + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/whoami.graphql b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/whoami.graphql new file mode 100644 index 000000000..08ad06fcd --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/main/resources/varsler/whoami.graphql @@ -0,0 +1,3 @@ +query whoami { + whoami +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/security/jwt/test/support/JwkGenerator.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/security/jwt/test/support/JwkGenerator.java new file mode 100644 index 000000000..73f59a2c9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/security/jwt/test/support/JwkGenerator.java @@ -0,0 +1,66 @@ +package no.nav.security.jwt.test.support; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.IOUtils; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.text.ParseException; + +public class JwkGenerator { + + private static final String DEFAULT_KEYID = "localhost-signer"; + public static final String DEFAULT_JWKSET_FILE = "/jwkset.json"; + + public JwkGenerator() { + } + + public static RSAKey getDefaultRSAKey() { + return (RSAKey) getJWKSet().getKeyByKeyId(DEFAULT_KEYID); + } + + public static RSAKey getRSAKey(String keyID) { + return (RSAKey) getJWKSet().getKeyByKeyId(keyID); + } + + public static JWKSet getJWKSet() { + try { + return JWKSet.parse(IOUtils.readInputStreamToString(JwkGenerator.class.getResourceAsStream(DEFAULT_JWKSET_FILE), Charset.forName("UTF-8"))); + } catch (IOException | ParseException io) { + throw new RuntimeException(io); + } + } + + public static JWKSet getJWKSetFromFile(File file) { + try { + JWKSet set = JWKSet.load(file); + return set; + } catch (IOException | ParseException e) { + throw new RuntimeException(e); + } + } + + protected static KeyPair generateKeyPair() { + try { + KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); + gen.initialize(1024); //just for testing so 1024 is ok + return gen.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + protected static RSAKey createJWK(String keyID, KeyPair keyPair) { + RSAKey jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()) + .privateKey((RSAPrivateKey) keyPair.getPrivate()) + .keyID(keyID) + .build(); + return jwk; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/security/jwt/test/support/JwtTokenGenerator.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/security/jwt/test/support/JwtTokenGenerator.java new file mode 100644 index 000000000..7ef50282b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/security/jwt/test/support/JwtTokenGenerator.java @@ -0,0 +1,87 @@ +package no.nav.security.jwt.test.support; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSHeader.Builder; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class JwtTokenGenerator { + + public static final String ACR_LEVEL_4 = "Level4"; + public static final long EXPIRY = 60 * 60 * 3600; + + private JwtTokenGenerator() { + } + + public static String signedJWTAsString(String subject, String issuer, String audience) { + return createSignedJWT(subject, issuer, audience).serialize(); + } + + public static SignedJWT createSignedJWT(String subject, String issuer, String audience) { + return createSignedJWT(subject, EXPIRY, new HashMap<>(), issuer, audience, ACR_LEVEL_4, null); + } + + public static SignedJWT createSignedJWT(String subject, long expiryInMinutes, Map claims, String issuer, String audience, + String acrLevel, List groups) { + JWTClaimsSet claimsSet = buildClaimSet(subject, issuer, audience, acrLevel, TimeUnit.MINUTES.toMillis(expiryInMinutes), claims, groups); + return createSignedJWT(JwkGenerator.getDefaultRSAKey(), claimsSet); + } + + public static SignedJWT createSignedJWT(JWTClaimsSet claimsSet) { + return createSignedJWT(JwkGenerator.getDefaultRSAKey(), claimsSet); + } + + public static JWTClaimsSet buildClaimSet( + String subject, + String issuer, + String audience, + String authLevel, + long expiry, Map additionalClaims, + List groups + ) { + Date now = new Date(); + JWTClaimsSet.Builder claimSetBuilder = new JWTClaimsSet.Builder() + .subject(subject) + .issuer(issuer) + .audience(audience) + .jwtID(UUID.randomUUID().toString()) + .claim("acr", authLevel) + .claim("ver", "1.0") + .claim("nonce", "myNonce") + .claim("auth_time", now) + .claim("groups", groups) + .notBeforeTime(now) + .issueTime(now) + .expirationTime(new Date(now.getTime() + expiry)); + additionalClaims.keySet().forEach(key -> claimSetBuilder.claim(key, additionalClaims.get(key))); + return claimSetBuilder.build(); + } + + public static SignedJWT createSignedJWT(RSAKey rsaJwk, JWTClaimsSet claimsSet) { + try { + JWSHeader.Builder header = new Builder(JWSAlgorithm.RS256) + .keyID(rsaJwk.getKeyID()) + .type(JOSEObjectType.JWT); + + SignedJWT signedJWT = new SignedJWT(header.build(), claimsSet); + JWSSigner signer = new RSASSASigner(rsaJwk.toPrivateKey()); + signedJWT.sign(signer); + + return signedJWT; + } catch (JOSEException e) { + throw new RuntimeException(e); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/security/jwt/test/support/TokenGeneratorController.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/security/jwt/test/support/TokenGeneratorController.java new file mode 100644 index 000000000..b9c8623ac --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/security/jwt/test/support/TokenGeneratorController.java @@ -0,0 +1,188 @@ +package no.nav.security.jwt.test.support; + +import static no.nav.security.jwt.test.support.JwtTokenGenerator.ACR_LEVEL_4; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.util.IOUtils; +import com.nimbusds.jwt.SignedJWT; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import no.nav.security.token.support.core.api.Unprotected; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.BeslutterAdGruppeProperties; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/local") +public class TokenGeneratorController { + + public static final String ISSO_IDTOKEN = "isso-idtoken"; + public static final String SELVBETJENING_IDTOKEN = "selvbetjening-idtoken"; + private BeslutterAdGruppeProperties beslutterAdGruppeProperties; + + public TokenGeneratorController(BeslutterAdGruppeProperties beslutterAdGruppeProperties) { + this.beslutterAdGruppeProperties = beslutterAdGruppeProperties; + } + + private static void bakeCookie( + String subject, + String cookieName, + String redirect, + String expiry, + HttpServletResponse response, + Map claims, + String issuer, + String audience, + String acrLevel, + List groups + ) throws IOException { + long expiryTime = expiry != null ? Long.parseLong(expiry) : JwtTokenGenerator.EXPIRY; + SignedJWT token = JwtTokenGenerator.createSignedJWT(subject, expiryTime, claims, issuer, audience, acrLevel, groups); + Cookie cookie = new Cookie(cookieName, token.serialize()); + cookie.setPath("/"); + response.addCookie(cookie); + if (redirect != null) { + response.sendRedirect(redirect); + } + } + + @Unprotected + @GetMapping + public TokenEndpoint[] endpoints(HttpServletRequest request) { + String base = request.getRequestURL().toString(); + return new TokenEndpoint[]{new TokenEndpoint("Get JWT as serialized string", base + "/jwt", "subject"), + new TokenEndpoint("Get JWT as SignedJWT object with claims", base + "/claims", "subject"), + new TokenEndpoint("Add JWT as a cookie, (optional) redirect to secured uri", base + "/cookie", "subject", "redirect", "cookiename"), + new TokenEndpoint("Get JWKS used to sign token", base + "/jwks"), + new TokenEndpoint("Get JWKS used to sign token as JWKSet object", base + "/jwkset"), + new TokenEndpoint("Get token issuer metadata (ref oidc .well-known)", base + "/metadata")}; + } + + @Unprotected + @GetMapping("/jwt") + public String issueToken(@RequestParam(value = "subject", defaultValue = "00000000000") String subject) { + return JwtTokenGenerator.createSignedJWT(subject, "iss-localhost", "aud-localhost").serialize(); + } + + @Unprotected + @GetMapping("/system-jwt") + public String issueSystemToken(@RequestParam(value = "subject", defaultValue = "00000000000") String subject) { + return JwtTokenGenerator.createSignedJWT(subject, "system", "aud-system").serialize(); + } + + + @Unprotected + @GetMapping("/claims") + public SignedJWT jwtClaims(@RequestParam(value = "subject", defaultValue = "00000000000") String subject) { + return JwtTokenGenerator.createSignedJWT(subject, "iss-localhost", "aud-localhost"); + } + + @Unprotected + @GetMapping("/selvbetjening-login") + public void addSelvbetjeningCookie(@RequestHeader(value = "selvbetjening-id", defaultValue = "00000000000") String subject, + @RequestParam(value = "cookiename", defaultValue = SELVBETJENING_IDTOKEN) String cookieName, + @RequestParam(value = "acr-level", defaultValue = ACR_LEVEL_4) String acrLevel, + @RequestParam(value = "redirect", required = false) String redirect, + @RequestParam(value = "expiry", required = false) String expiry, + HttpServletResponse response) throws IOException { + bakeCookie(subject, cookieName, redirect, expiry, response, new HashMap<>(), "selvbetjening", "aud-selvbetjening", acrLevel, + List.of(beslutterAdGruppeProperties.getId().toString())); + } + + @Unprotected + @GetMapping("/isso-login") + public void addNavCookie(@RequestParam(value = "subject", defaultValue = "00000000000") String subject, + @RequestHeader(value = "isso-id", defaultValue = "Z123456") String navIdent, + @RequestParam(value = "cookiename", defaultValue = ISSO_IDTOKEN) String cookieName, + @RequestParam(value = "redirect", required = false) String redirect, + @RequestParam(value = "expiry", required = false) String expiry, + HttpServletResponse response + ) throws IOException { + bakeCookie(subject, cookieName, redirect, expiry, response, Collections.singletonMap("NAVident", navIdent), "isso", "aud-isso", null, + Collections.singletonList(beslutterAdGruppeProperties.getId().toString())); + } + + @Unprotected + @GetMapping("/logout") + public void removeCookies(@RequestParam(value = "redirect", required = false) String redirect, + HttpServletResponse response) throws IOException { + Cookie selvbetjeningCookie = new Cookie(SELVBETJENING_IDTOKEN, null); + selvbetjeningCookie.setMaxAge(0); + selvbetjeningCookie.setPath("/"); + response.addCookie(selvbetjeningCookie); + Cookie issoCookie = new Cookie(ISSO_IDTOKEN, null); + issoCookie.setMaxAge(0); + issoCookie.setPath("/"); + response.addCookie(issoCookie); + response.sendRedirect(redirect); + } + + @Unprotected + @GetMapping("/jwks") + public String jwks() throws IOException { + return IOUtils.readInputStreamToString(getClass().getResourceAsStream(JwkGenerator.DEFAULT_JWKSET_FILE), + Charset.defaultCharset()); + } + + @Unprotected + @GetMapping("/jwkset") + public JWKSet jwkSet() { + return JwkGenerator.getJWKSet(); + } + + @Unprotected + @GetMapping("/metadata-isso") + public String metadataIsso() throws IOException { + return IOUtils.readInputStreamToString(getClass().getResourceAsStream("/metadata-isso.json"), + Charset.defaultCharset()); + } + + @Unprotected + @GetMapping("/metadata-selvbetjening") + public String metadataSelvbetjening() throws IOException { + return IOUtils.readInputStreamToString(getClass().getResourceAsStream("/metadata-selvbetjening.json"), + Charset.defaultCharset()); + } + + @Unprotected + @GetMapping("/metadata-system") + public String metadataSystem() throws IOException { + return IOUtils.readInputStreamToString(getClass().getResourceAsStream("/metadata-system.json"), + Charset.defaultCharset()); + } + + static class TokenEndpoint { + String desc; + String uri; + String[] params; + + public TokenEndpoint(String desc, String uri, String... params) { + this.desc = desc; + this.uri = uri; + this.params = params; + + } + + public String getDesc() { + return desc; + } + + public String getUri() { + return uri; + } + + public String[] getParams() { + return params; + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/AssertFeilkode.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/AssertFeilkode.java new file mode 100644 index 000000000..10572bdf0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/AssertFeilkode.java @@ -0,0 +1,20 @@ +package no.nav.tag.tiltaksgjennomforing; + +import lombok.experimental.UtilityClass; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.failBecauseExceptionWasNotThrown; + +@UtilityClass +public class AssertFeilkode { + public static void assertFeilkode(Feilkode feilkode, Runnable shouldRaiseThrowable) { + try { + shouldRaiseThrowable.run(); + failBecauseExceptionWasNotThrown(FeilkodeException.class); + } catch (Exception e) { + assertThat(e).isInstanceOf(FeilkodeException.class).extracting(throwable -> ((FeilkodeException) throwable).getFeilkode()).isEqualTo(feilkode); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/DockerComposeTiltaksgjennomforingApplication.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/DockerComposeTiltaksgjennomforingApplication.java new file mode 100644 index 000000000..70dce2c46 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/DockerComposeTiltaksgjennomforingApplication.java @@ -0,0 +1,12 @@ +package no.nav.tag.tiltaksgjennomforing; + +import org.springframework.boot.builder.SpringApplicationBuilder; + +public class DockerComposeTiltaksgjennomforingApplication extends TiltaksgjennomforingApplication { + public static void main(String[] args) { + new SpringApplicationBuilder(TiltaksgjennomforingApplication.class) + .profiles("dockercompose", "testdata", "wiremock") + .build() + .run(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/IntegrasjonerMockServer.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/IntegrasjonerMockServer.java new file mode 100644 index 000000000..1965b8da9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/IntegrasjonerMockServer.java @@ -0,0 +1,31 @@ +package no.nav.tag.tiltaksgjennomforing; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile("wiremock") +@Slf4j +@Component +public class IntegrasjonerMockServer implements DisposableBean { + private final WireMockServer server; + + public IntegrasjonerMockServer() { + log.info("Starter mockserver for eksterne integrasjoner."); + server = new WireMockServer(WireMockConfiguration.options().usingFilesUnderClasspath(".").port(8090)); + server.start(); + } + + public WireMockServer getServer() { + return server; + } + + @Override + public void destroy() { + log.info("Stopper mockserver."); + server.stop(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LastInnMasseTestData.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LastInnMasseTestData.java new file mode 100644 index 000000000..408895f9a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LastInnMasseTestData.java @@ -0,0 +1,31 @@ +package no.nav.tag.tiltaksgjennomforing; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import org.apache.commons.text.RandomStringGenerator; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +@Profile("masse-testdata") +public class LastInnMasseTestData implements ApplicationListener { + private final AvtaleRepository avtaleRepository; + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + log.info("Laster inn masse testdata"); + + for (int i = 0; i < 555; i++) { + Avtale avtale = TestData.enLonnstilskuddAvtaleGodkjentAvVeilederTilbakeITid(); + avtale.getGjeldendeInnhold().setDeltakerFornavn(new RandomStringGenerator.Builder().withinRange('a', 'z').build().generate(5)); + avtaleRepository.save(avtale); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LastInnTestData.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LastInnTestData.java new file mode 100644 index 000000000..b4ce0baf8 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LastInnTestData.java @@ -0,0 +1,63 @@ +package no.nav.tag.tiltaksgjennomforing; + +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +@Profile("testdata") +public class LastInnTestData implements ApplicationListener { + private final AvtaleRepository avtaleRepository; + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + if (avtaleRepository.count() > 0) return; + log.info("Laster testdata"); + + avtaleRepository.save(TestData.enLonnstilskuddAvtaleGodkjentAvVeilederUtenTilskuddsperioder()); + avtaleRepository.save(TestData.enArbeidstreningAvtale()); + avtaleRepository.save(TestData.enMentorAvtaleSignert()); + avtaleRepository.save(TestData.enMentorAvtaleUsignert()); + avtaleRepository.save(TestData.enInkluderingstilskuddAvtale()); + avtaleRepository.save(TestData.enInkluderingstilskuddAvtaleUtfyltOgGodkjentAvArbeidsgiver()); + avtaleRepository.save(TestData.enAvtaleMedAltUtfylt()); + avtaleRepository.save(TestData.enAvtaleMedAltUtfyltGodkjentAvVeileder()); + avtaleRepository.save(TestData.enAvtaleMedFlereVersjoner()); + avtaleRepository.save(TestData.enAvtaleKlarForOppstart()); + Avtale lilly = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + lilly.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtaleRepository.save(lilly); + avtaleRepository.save(TestData.enLonnstilskuddAvtaleGodkjentAvVeileder()); + + avtaleRepository.save(TestData.enLonnstilskuddAvtaleGodkjentAvVeilederTilbakeITid()); + Now.fixedDate(LocalDate.of(2021, 6, 1)); + avtaleRepository.save(TestData.enSommerjobbAvtaleGodkjentAvVeileder()); + avtaleRepository.save(TestData.enSommerjobbAvtaleGodkjentAvBeslutter()); + avtaleRepository.save(TestData.enSommerjobbAvtaleGodkjentAvArbeidsgiver()); + Now.resetClock(); + avtaleRepository.save(TestData.enMidlertidigLonnstilskuddAvtaleMedSpesieltTilpassetInnsatsGodkjentAvVeileder()); + avtaleRepository.save(TestData.enMentorAvtaleMedMedAltUtfylt()); + avtaleRepository.save(TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordelt()); + avtaleRepository.save(TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedGeografiskEnhet()); + avtaleRepository.save(TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedGeografiskEnhet()); + avtaleRepository.save(TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedOppfølgningsEnhet()); + avtaleRepository.save(TestData.enAvtaleOpprettetAvArbeidsgiver(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD)); + avtaleRepository.save(TestData.enAvtaleOpprettetAvArbeidsgiver(Tiltakstype.VARIG_LONNSTILSKUDD)); + avtaleRepository.save(TestData.enVarigLonnstilskuddAvtaleMedBehandletIArenaPerioder()); + + + + + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LokalConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LokalConfiguration.java new file mode 100644 index 000000000..5ac8c7a0d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LokalConfiguration.java @@ -0,0 +1,20 @@ +package no.nav.tag.tiltaksgjennomforing; + +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.NotifikasjonService; +import org.mockito.Mockito; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class LokalConfiguration { + @Bean("azure") + RestTemplate restTemplate(){ + return new RestTemplateBuilder().build(); + } + + @Bean + NotifikasjonService notifikasjon() { return Mockito.mock(NotifikasjonService.class);} + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LokalSecurityAzureConfig.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LokalSecurityAzureConfig.java new file mode 100644 index 000000000..46538ef28 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LokalSecurityAzureConfig.java @@ -0,0 +1,24 @@ +package no.nav.tag.tiltaksgjennomforing; + +import lombok.AllArgsConstructor; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +@AllArgsConstructor +public class LokalSecurityAzureConfig { + + private final RestTemplateBuilder restTemplateBuilder; + + @Bean("notifikasjonerRestTemplate") + public RestTemplate anonymProxyRestTemplate(){ + return restTemplateBuilder.build(); + } + + @Bean("veilarbarenaRestTemplate") + public RestTemplate anonymProxyRestTemplateVeilabArena(){ + return restTemplateBuilder.build(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LokalTiltaksgjennomforingApplication.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LokalTiltaksgjennomforingApplication.java new file mode 100644 index 000000000..cb15e99a7 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/LokalTiltaksgjennomforingApplication.java @@ -0,0 +1,13 @@ +package no.nav.tag.tiltaksgjennomforing; + +import org.springframework.boot.builder.SpringApplicationBuilder; + + +public class LokalTiltaksgjennomforingApplication extends TiltaksgjennomforingApplication { + public static void main(String[] args) { + new SpringApplicationBuilder(TiltaksgjennomforingApplication.class) + .profiles(Miljø.LOCAL, "wiremock", "testdata") + .build() + .run(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/TestDataTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/TestDataTest.java new file mode 100644 index 000000000..d144c39a5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/TestDataTest.java @@ -0,0 +1,20 @@ +package no.nav.tag.tiltaksgjennomforing; + +import static org.assertj.core.api.Assertions.assertThat; +import no.nav.tag.tiltaksgjennomforing.avtale.EndreAvtale; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import org.junit.jupiter.api.Test; + +class TestDataTest { + + @Test + void endring_på_alle_TestData_endre_felter_så_ingen_er_Null_felter() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + TestData.endreMaalInfo(endreAvtale); + TestData.endreMentorInfo(endreAvtale); + TestData.endreInkluderingstilskuddInfo(endreAvtale); + assertThat(endreAvtale.getMaal()).isNotEmpty(); + assertThat(endreAvtale.getInkluderingstilskuddsutgift()).isNotEmpty(); + assertThat(endreAvtale).hasNoNullFieldsOrProperties(); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/AltinnTilgangsstyringServiceTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/AltinnTilgangsstyringServiceTest.java new file mode 100644 index 000000000..39090d97e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/AltinnTilgangsstyringServiceTest.java @@ -0,0 +1,113 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.model.AltinnReportee; +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring.AltinnTilgangsstyringProperties; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring.AltinnTilgangsstyringService; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import no.nav.tag.tiltaksgjennomforing.exceptions.AltinnFeilException; +import no.nav.tag.tiltaksgjennomforing.exceptions.TiltaksgjennomforingException; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.FeatureToggleService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles({ Miljø.LOCAL, "wiremock" }) +@DirtiesContext +public class AltinnTilgangsstyringServiceTest { + @Autowired + private AltinnTilgangsstyringService altinnTilgangsstyringService; + + @MockBean + private TokenUtils tokenUtils; + + @MockBean + private FeatureToggleService featureToggleService; + + @BeforeEach + public void setUp() { + // when(tokenUtils.hentSelvbetjeningToken()).thenReturn("token"); + when(featureToggleService.isEnabled(anyString())).thenReturn(false); + } + + @Test + public void hentOrganisasjoner__gyldig_fnr_en_bedrift_på_hvert_tiltak() { + Fnr fnr = new Fnr("10000000000"); + Map> tilganger = altinnTilgangsstyringService.hentTilganger(fnr, () -> ""); + Set organisasjoner = altinnTilgangsstyringService.hentAltinnOrganisasjoner(fnr, () -> ""); + + // Alt som finnes i tilganger-mappet skal også finnes i organisasjoner-settet + assertThat(organisasjoner).extracting(org -> new BedriftNr(org.getOrganizationNumber())).containsAll(tilganger.keySet()); + + // Sjekk at uvesentilg tilgang er med i organisasjoner + assertThat(organisasjoner).extracting(AltinnReportee::getOrganizationNumber).contains("980712306", "910825555"); + + + // Parents skal ikke være i tilgang-map + assertThat(tilganger).doesNotContainKeys(new BedriftNr("910825550"), new BedriftNr("910825555")); + + // Virksomheter skal være i tilgang-map + + assertThat(tilganger.get(new BedriftNr("999999999"))).containsOnly(Tiltakstype.ARBEIDSTRENING, Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, Tiltakstype.VARIG_LONNSTILSKUDD, Tiltakstype.SOMMERJOBB, Tiltakstype.MENTOR, Tiltakstype.INKLUDERINGSTILSKUDD); + + assertThat(tilganger.get(new BedriftNr("910712314"))).containsOnly(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD); + assertThat(tilganger.get(new BedriftNr("910712306"))).containsOnly(Tiltakstype.VARIG_LONNSTILSKUDD); + + // Ingen tilganger på ingen tiltak + assertThat(tilganger).doesNotContainKeys(new BedriftNr("980712306"), new BedriftNr("980825560")); + + } + + @Test + public void hentOrganisasjoner__tilgang_bare_for_arbeidstrening() { + Fnr fnr = new Fnr("20000000000"); + Map> tilganger = altinnTilgangsstyringService.hentTilganger(fnr, () -> ""); + Set organisasjoner = altinnTilgangsstyringService.hentAltinnOrganisasjoner(fnr, () -> ""); + + // Alt som finnes i tilganger-mappet skal også finnes i organisasjoner-settet + assertThat(organisasjoner).extracting(org -> new BedriftNr(org.getOrganizationNumber())).containsAll(tilganger.keySet()); + + // Parents skal ikke være i tilgang-map + assertThat(tilganger).doesNotContainKey(new BedriftNr("910825555")); + + // Virksomheter skal være i tilgang-map + assertThat(tilganger.get(new BedriftNr("999999999"))).containsOnly(Tiltakstype.ARBEIDSTRENING, Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, Tiltakstype.VARIG_LONNSTILSKUDD, Tiltakstype.SOMMERJOBB, Tiltakstype.MENTOR, Tiltakstype.INKLUDERINGSTILSKUDD); // TODO: Tilgangsstyring skal skille på midlertidig lønnstilskudd og sommerjobb + + } + + @Test + public void hentOrganisasjoner__ingen_tilgang() { + Fnr fnr = new Fnr("09000000000"); + Map> tilganger = altinnTilgangsstyringService.hentTilganger(fnr, () -> ""); + Set organisasjoner = altinnTilgangsstyringService.hentAltinnOrganisasjoner(fnr, () -> ""); + + assertThat(organisasjoner).isEmpty(); + assertThat(tilganger).isEmpty(); + } + + @Test + public void hentTilganger__midlertidig_feil_gir_feilkode() { + assertThatThrownBy(() -> altinnTilgangsstyringService.hentTilganger(new Fnr("31000000000"), () -> "")).isExactlyInstanceOf(AltinnFeilException.class); + } + + @Test + public void manglende_serviceCode_skal_kaste_feil() { + AltinnTilgangsstyringProperties altinnTilgangsstyringProperties = new AltinnTilgangsstyringProperties(); + assertThatThrownBy(() -> new AltinnTilgangsstyringService(altinnTilgangsstyringProperties, tokenUtils, "tiltaksgjennomforing-api")).isExactlyInstanceOf(TiltaksgjennomforingException.class); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/ArbeidsgiverTokenStrategyFactoryMock.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/ArbeidsgiverTokenStrategyFactoryMock.java new file mode 100644 index 000000000..0c2e8c687 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/ArbeidsgiverTokenStrategyFactoryMock.java @@ -0,0 +1,16 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring.ArbeidsgiverTokenStrategyFactory; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring.HentArbeidsgiverToken; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +@Component +@Primary +public class ArbeidsgiverTokenStrategyFactoryMock implements ArbeidsgiverTokenStrategyFactory { + + @Override + public HentArbeidsgiverToken create(TokenUtils.Issuer issuer) { + return () -> ""; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetArbeidsgiverTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetArbeidsgiverTest.java new file mode 100644 index 000000000..de8c2f8c2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetArbeidsgiverTest.java @@ -0,0 +1,46 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.util.*; + +import no.nav.tag.tiltaksgjennomforing.avtale.Arbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class InnloggetArbeidsgiverTest { + + @Mock + public AvtaleRepository avtaleRepository; + + Avtale avtale = TestData.enArbeidstreningAvtale(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + + @BeforeEach + public void setUp(){ + avtale.setAnnullertGrunn("Hemmelig"); + } + + @Test + public void hentAvtale_uten_annullertGrunn() { + when(avtaleRepository.findById(avtale.getId())).thenReturn(Optional.of(avtale)); + Avtale hentetAvtale = arbeidsgiver.hentAvtale(avtaleRepository, avtale.getId()); + assertThat(hentetAvtale.getAnnullertGrunn()).isNull(); + } + + @Test + public void hentAvtalerForMinsideArbeidsgiver_uten_annullertGrunn() { + when(avtaleRepository.findAllByBedriftNr(eq(avtale.getBedriftNr()))).thenReturn(Arrays.asList(avtale)); + List hentetAvtaler = arbeidsgiver.hentAvtalerForMinsideArbeidsgiver(avtaleRepository, avtale.getBedriftNr()); + assertThat(hentetAvtaler.get(0).getAnnullertGrunn()).isNull(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetBrukerTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetBrukerTest.java new file mode 100644 index 000000000..e1e5f9909 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggetBrukerTest.java @@ -0,0 +1,262 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.TilgangskontrollService; +import no.nav.tag.tiltaksgjennomforing.avtale.*; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2Client; +import no.nav.tag.tiltaksgjennomforing.enhet.VeilarbArenaClient; +import no.nav.tag.tiltaksgjennomforing.okonomi.KontoregisterService; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class InnloggetBrukerTest { + + private Fnr deltaker; + private NavIdent navIdent; + private Avtale avtale; + private BedriftNr bedriftNr; + private TilgangskontrollService tilgangskontrollService; + private KontoregisterService kontoregisterService; + private PersondataService persondataService; + private Norg2Client norg2Client; + private VeilarbArenaClient veilarbArenaClient; + private AvtaleRepository avtaleRepository; + + @BeforeEach + public void setup() { + deltaker = new Fnr("00000000000"); + navIdent = new NavIdent("X100000"); + bedriftNr = new BedriftNr("12345678901"); + avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(deltaker, bedriftNr, Tiltakstype.ARBEIDSTRENING), navIdent); + tilgangskontrollService = mock(TilgangskontrollService.class); + persondataService = mock(PersondataService.class); + kontoregisterService = mock(KontoregisterService.class); + veilarbArenaClient = mock(VeilarbArenaClient.class); + avtaleRepository = mock(AvtaleRepository.class); + } + + @Test + public void harTilgang__deltaker_skal_ha_tilgang_til_avtale() { + assertThat(new Deltaker(deltaker).harTilgang(avtale)).isTrue(); + } + + @Test + public void harTilgang__veileder_skal_ha_lesetilgang_til_avtale_hvis_toggle_er_på_og_tilgangskontroll_er_ok() { + Veileder veileder = new Veileder( + navIdent, + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + when(tilgangskontrollService.harSkrivetilgangTilKandidat( + veileder, + avtale.getDeltakerFnr()) + ).thenReturn(true); + + assertThat(veileder.harTilgang(avtale)).isTrue(); + verify(tilgangskontrollService).harSkrivetilgangTilKandidat(veileder, avtale.getDeltakerFnr()); + } + + @Test + public void harTilgang__veileder_skal_ikke_ha_lesetilgang_til_avtale_hvis_toggle_er_på_og_tilgangskontroll_feiler() { + Veileder veileder = new Veileder( + navIdent, + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + when(tilgangskontrollService.harSkrivetilgangTilKandidat( + veileder, + avtale.getDeltakerFnr() + )).thenReturn(false); + + assertThat(veileder.harTilgang(avtale)).isFalse(); + } + + @Test + public void harTilgang__veileder_skal_ha_skrivetilgang_til_avtale_hvis_toggle_er_på_og_tilgangskontroll_er_ok() { + Veileder veileder = new Veileder( + navIdent, + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, veilarbArenaClient + ); + when(tilgangskontrollService.harSkrivetilgangTilKandidat( + veileder, + avtale.getDeltakerFnr()) + ).thenReturn(true); + + assertThat(veileder.harTilgang(avtale)).isTrue(); + verify(tilgangskontrollService).harSkrivetilgangTilKandidat(veileder, avtale.getDeltakerFnr()); + } + + @Test + public void harTilgang__veileder_skal_ikke_ha_skrivetilgang_til_avtale_hvis_toggle_er_på_og_tilgangskontroll_feiler() { + Veileder veileder = new Veileder( + navIdent, + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + when(tilgangskontrollService.harSkrivetilgangTilKandidat( + veileder, + avtale.getDeltakerFnr()) + ).thenReturn(false); + + assertThat(veileder.harTilgang(avtale)).isFalse(); + } + + @Test + public void harTilgang__arbeidsgiver_skal_ikke_ha_tilgang_til_avtale() { + assertThat( + new Arbeidsgiver(TestData.etFodselsnummer(), + Set.of(), + Map.of(), + null, + null + ).harTilgang(avtale) + ).isFalse(); + } + + @Test + public void harTilgang__ikkepart_veileder_skal_ikke_ha_lesetilgang_hvis_toggle_er_av() { + assertThat( + new Veileder( + new NavIdent("X123456"), + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ).harTilgang(avtale) + ).isFalse(); + } + + @Test + public void harTilgang__ikkepart_veileder_skal_ikke_ha_skrivetilgang_hvis_toggle_er_av() { + assertThat( + new Veileder( + new NavIdent("X123456"), + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ).harTilgang(avtale) + ).isFalse(); + } + + @Test + public void harTilgang__ikkepart_selvbetjeningsbruker_skal_ikke_ha_tilgang() { + assertThat( + new Arbeidsgiver( + new Fnr("00000000001"), + Set.of(), + Map.of(), + null, + null).harTilgang(avtale) + ).isFalse(); + } + + @Test + public void harTilgang__arbeidsgiver_skal_kunne_representere_bedrift_uten_Fnr() { + Map> tilganger = Map.of(this.bedriftNr, Set.of(Tiltakstype.values())); + Arbeidsgiver Arbeidsgiver = new Arbeidsgiver( + new Fnr("00000000009"), + Set.of(), + tilganger, + null, + null + ); + assertThat(Arbeidsgiver.harTilgang(avtale)).isTrue(); + } + + @Test + public void harTilgang__arbeidsgiver_skal_ikke_ha_tilgang_til_avbrutt_avtale_eldre_enn_12_uker() { + Map> tilganger = Map.of(this.bedriftNr, Set.of(Tiltakstype.values())); + Arbeidsgiver arbeidsgiver = new Arbeidsgiver( + new Fnr("00000000009"), + Set.of(), + tilganger, + null, + null + ); + avtale.setAvbrutt(true); + avtale.setSistEndret(Now.instant().minus(84, ChronoUnit.DAYS).minusMillis(100)); + assertThat(arbeidsgiver.harTilgang(avtale)).isFalse(); + } + + @Test + public void harTilgang__arbeidsgiver_skal_ikke_ha_tilgang_til_avsluttet_avtale_eldre_enn_12_uker() { + Avtale avtale = TestData.enAvtaleMedAltUtfyltGodkjentAvVeileder(); + avtale.getGjeldendeInnhold().setSluttDato(Now.localDate().minusDays(85)); + Map> tilganger = Map.of(avtale.getBedriftNr(), Set.of(Tiltakstype.values())); + Arbeidsgiver Arbeidsgiver = new Arbeidsgiver( + new Fnr("00000000009"), + Set.of(), + tilganger, + null, + null + ); + assertThat(Arbeidsgiver.harTilgang(avtale)).isFalse(); + } + + @Test + public void harTilgang__arbeidsgiver_skal_ha_tilgang_til_avsluttet_avtale_eldre_enn_12_uker_når_ikke_godkjent_av_veileder() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setSluttDato(Now.localDate().minusDays(85)); + Map> tilganger = Map.of(avtale.getBedriftNr(), Set.of(Tiltakstype.values())); + Arbeidsgiver Arbeidsgiver = new Arbeidsgiver( + new Fnr("00000000009"), + Set.of(), + tilganger, + null, + null + ); + assertThat(Arbeidsgiver.harTilgang(avtale)).isTrue(); + } + + @Test + public void harTilgang__arbeidsgiver_med_arbeidsgivertilgang_skal_ikke_ha_lonnstilskuddtilgang() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + Map> tilganger = Map.of(avtale.getBedriftNr(), Set.of(Tiltakstype.ARBEIDSTRENING)); + Arbeidsgiver arbeidsgiver = new Arbeidsgiver( + new Fnr("00000000009"), + Set.of(), + tilganger, + null, + null + ); + assertThat(arbeidsgiver.harTilgang(avtale)).isFalse(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggingServiceTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggingServiceTest.java new file mode 100644 index 000000000..4bced6b30 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/InnloggingServiceTest.java @@ -0,0 +1,169 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import static no.nav.tag.tiltaksgjennomforing.AssertFeilkode.assertFeilkode; +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.ENHET_OPPFØLGING; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils.Issuer; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring.AltinnTilgangsstyringService; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring.ArbeidsgiverTokenStrategyFactoryImpl; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.TilgangskontrollException; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.AxsysService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class InnloggingServiceTest { + + @InjectMocks + private InnloggingService innloggingService; + + @Mock + private TokenUtils tokenUtils; + + @Mock + private AltinnTilgangsstyringService altinnTilgangsstyringService; + + @Mock + private AxsysService axsysService; + + @Mock + private SystembrukerProperties systembrukerProperties; + + @Mock + private BeslutterAdGruppeProperties beslutterAdGruppeProperties; + + @Mock + private ArbeidsgiverTokenStrategyFactoryImpl arbeidsgiverTokenStrategyFactory; + + + @Test + public void hentInnloggetBruker__er_selvbetjeningbruker() { + InnloggetDeltaker selvbetjeningBruker = TestData.enInnloggetDeltaker(); + værInnloggetDeltaker(selvbetjeningBruker); + assertThat(innloggingService.hentInnloggetBruker(Avtalerolle.DELTAKER)).isEqualTo(selvbetjeningBruker); + } + + @Test + public void hentInnloggetBruker__er_selvbetjeningbruker_mentor() { + InnloggetMentor selvbetjeningMentor = TestData.enInnloggetMentor(); + værInnloggetMentor(selvbetjeningMentor); + assertThat(innloggingService.hentInnloggetBruker(Avtalerolle.MENTOR)).isEqualTo(selvbetjeningMentor); + } + + @Test + public void hentInnloggetBruker__selvbetjeningbruker_type_arbeidsgiver_skal_hente_organisasjoner() { + InnloggetArbeidsgiver selvbetjeningBruker = new InnloggetArbeidsgiver(new Fnr("11111111111"), Set.of(), Map.of()); + when(altinnTilgangsstyringService.hentTilganger(eq((Fnr) selvbetjeningBruker.getIdentifikator()), any())).thenReturn(Map.of()); + when(altinnTilgangsstyringService.hentAltinnOrganisasjoner(eq((Fnr) selvbetjeningBruker.getIdentifikator()), any())).thenReturn(Set.of()); + værInnloggetArbeidsgiver(selvbetjeningBruker); + + when(arbeidsgiverTokenStrategyFactory.create(Issuer.ISSUER_TOKENX)).thenReturn(() -> ""); + + assertThat(innloggingService.hentInnloggetBruker(Optional.of(Avtalerolle.ARBEIDSGIVER).get())).isEqualTo(selvbetjeningBruker); + verify(altinnTilgangsstyringService).hentTilganger(eq((Fnr) selvbetjeningBruker.getIdentifikator()), any()); + verify(altinnTilgangsstyringService).hentAltinnOrganisasjoner(eq((Fnr) selvbetjeningBruker.getIdentifikator()), any()); + } + + @Test + public void hentInnloggetBruker__er_nav_ansatt_og_har_enhet() { + InnloggetVeileder navAnsatt = TestData.enInnloggetVeileder(); + when(axsysService.hentEnheterNavAnsattHarTilgangTil(any())).thenReturn(List.of(ENHET_OPPFØLGING)); + værInnloggetVeileder(navAnsatt); + + assertThat(innloggingService.hentInnloggetBruker(Avtalerolle.VEILEDER)).isEqualTo(navAnsatt); + } + + @Test + public void hentInnloggetBruker__er_nav_ansatt_og_beslutter() { + InnloggetBeslutter navAnsatt = TestData.enInnloggetBeslutter(); + when(axsysService.hentEnheterNavAnsattHarTilgangTil(any())).thenReturn(List.of(ENHET_OPPFØLGING)); + værInnloggetBeslutter(navAnsatt); + assertThat(innloggingService.hentInnloggetBruker(Avtalerolle.BESLUTTER)).isEqualTo(navAnsatt); + } + + @Test + public void hentInnloggetNavAnsatt__er_selvbetjeningbruker() { + værInnloggetArbeidsgiver(TestData.enInnloggetArbeidsgiver()); + assertFeilkode(Feilkode.UGYLDIG_KOMBINASJON_AV_ISSUER_OG_ROLLE, innloggingService::hentInnloggetVeileder); + } + + @Test + public void hentInnloggetBruker__er_uinnlogget() { + when(tokenUtils.hentBrukerOgIssuer()).thenReturn(Optional.empty()); + assertThatThrownBy(innloggingService::hentInnloggetVeileder).isExactlyInstanceOf(TilgangskontrollException.class); + } + + @Test + public void avviser_selvbetjeningBruker_som_systemBruker() { + værInnloggetArbeidsgiver(TestData.enInnloggetArbeidsgiver()); + assertThatThrownBy(innloggingService::validerSystembruker).isExactlyInstanceOf(TilgangskontrollException.class); + } + + @Test + public void avviser_navAnsatt_som_systemBruker() { + værInnloggetVeileder(TestData.enInnloggetVeileder()); + assertThatThrownBy(innloggingService::validerSystembruker).isExactlyInstanceOf(TilgangskontrollException.class); + } + + @Test + public void avviser_ukjent_systemBruker() { + værInnloggetSystem("ukjent"); + assertThatThrownBy(innloggingService::validerSystembruker).isExactlyInstanceOf(TilgangskontrollException.class); + } + + @Test + public void godtar_forventet_systemBruker() { + værInnloggetSystem("forventet"); + innloggingService.validerSystembruker(); + } + + private void værInnloggetSystem(String systemId) { + when(tokenUtils.hentBrukerOgIssuer()).thenReturn(Optional.of(new TokenUtils.BrukerOgIssuer(Issuer.ISSUER_SYSTEM, systemId))); + when(systembrukerProperties.getId()).thenReturn("forventet"); + } + + private void værInnloggetDeltaker(InnloggetDeltaker bruker) { + when(tokenUtils.hentBrukerOgIssuer()) + .thenReturn(Optional.of(new TokenUtils.BrukerOgIssuer(Issuer.ISSUER_TOKENX, bruker.getIdentifikator().asString()))); + } + + private void værInnloggetMentor(InnloggetMentor mentor) { + when(tokenUtils.hentBrukerOgIssuer()) + .thenReturn(Optional.of(new TokenUtils.BrukerOgIssuer(Issuer.ISSUER_TOKENX, mentor.getIdentifikator().asString()))); + } + + private void værInnloggetArbeidsgiver(InnloggetArbeidsgiver bruker) { + when(tokenUtils.hentBrukerOgIssuer()) + .thenReturn(Optional.of(new TokenUtils.BrukerOgIssuer(Issuer.ISSUER_TOKENX, bruker.getIdentifikator().asString()))); + } + + private void værInnloggetVeileder(InnloggetVeileder navAnsatt) { + when(tokenUtils.hentBrukerOgIssuer()) + .thenReturn(Optional.of(new TokenUtils.BrukerOgIssuer(Issuer.ISSUER_AAD, navAnsatt.getIdentifikator().asString()))); + } + + private void værInnloggetBeslutter(InnloggetBeslutter navAnsatt) { + when(tokenUtils.harAdGruppe(any())).thenReturn(true); + when(tokenUtils.hentBrukerOgIssuer()) + .thenReturn(Optional.of(new TokenUtils.BrukerOgIssuer(Issuer.ISSUER_AAD, navAnsatt.getIdentifikator().asString()))); + } + + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/TokenUtilsTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/TokenUtilsTest.java new file mode 100644 index 000000000..c4da3e094 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/TokenUtilsTest.java @@ -0,0 +1,187 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon; + +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTClaimsSet.Builder; +import no.nav.security.jwt.test.support.JwkGenerator; +import no.nav.security.jwt.test.support.JwtTokenGenerator; +import no.nav.security.token.support.core.context.TokenValidationContext; +import no.nav.security.token.support.core.context.TokenValidationContextHolder; +import no.nav.security.token.support.core.jwt.JwtToken; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils.BrukerOgIssuer; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils.Issuer; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.*; + +import static no.nav.security.jwt.test.support.JwtTokenGenerator.ACR_LEVEL_4; +import static no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils.Issuer.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class TokenUtilsTest { + + @InjectMocks + private TokenUtils tokenUtils; + + @Mock + private TokenValidationContextHolder contextHolder; + + @Test + public void hentInnloggetBruker__er_selvbetjeningbruker() { + InnloggetArbeidsgiver selvbetjeningBruker = TestData.enInnloggetArbeidsgiver(); + vaerInnloggetSelvbetjening(selvbetjeningBruker); + assertThat(tokenUtils.hentBrukerOgIssuer().get()) + .isEqualTo(new BrukerOgIssuer(ISSUER_TOKENX, selvbetjeningBruker.getIdentifikator().asString())); + } + + @Test + public void hentBeslutter_fra_nav_ansatt_innloggetBruker_med_riktig_beslutter_gruppe() { + InnloggetBeslutter navBeslutter = TestData.enInnloggetBeslutter(); + vaerInnloggetNavAnsatt(navBeslutter); + assertThat(tokenUtils.hentBrukerOgIssuer().get()).isEqualTo(new BrukerOgIssuer(ISSUER_AAD, navBeslutter.getIdentifikator().asString())); + UUID beslutterAdGruppe = UUID.fromString("928636f4-fd0d-4149-978e-a6fb68bb19de"); + assertThat(tokenUtils.harAdGruppe(beslutterAdGruppe)).isTrue(); + } + + @Test + public void hentBeslutter_fra_nav_ansatt_innloggetBruker_returnerer_false_naar_ad_gruppe_ikke_finnes_for_palogget_bruker() { + InnloggetBeslutter navBeslutter = TestData.enInnloggetBeslutter(); + vaerInnloggetNavAnsatt(navBeslutter); + assertThat(tokenUtils.hentBrukerOgIssuer().get()).isEqualTo(new BrukerOgIssuer(ISSUER_AAD, navBeslutter.getIdentifikator().asString())); + assertThat(tokenUtils.harAdGruppe(UUID.randomUUID())).isFalse(); + } + + @Test + public void hentInnloggetBruker__er_selvbetjeningbruker_må_være_nivå_4() { + InnloggetArbeidsgiver selvbetjeningBruker = TestData.enInnloggetArbeidsgiver(); + vaerInnloggetSelvbetjening(selvbetjeningBruker); + assertThat(tokenUtils.hentBrukerOgIssuer().get()) + .isEqualTo(new BrukerOgIssuer(ISSUER_TOKENX, selvbetjeningBruker.getIdentifikator().asString())); + vaerInnloggetSelvbetjeningNiva3(selvbetjeningBruker); + assertThat(tokenUtils.hentBrukerOgIssuer().isEmpty()).isTrue(); + } + + @Test + public void hentInnloggetBruker__er_nav_ansatt() { + InnloggetVeileder navAnsatt = TestData.enInnloggetVeileder(); + vaerInnloggetNavAnsatt(navAnsatt); + assertThat(tokenUtils.hentBrukerOgIssuer().get()).isEqualTo(new BrukerOgIssuer(ISSUER_AAD, navAnsatt.getIdentifikator().asString())); + } + + @Test + public void hentInnloggetBruker__er_system() { + vaerInnloggetSystem("systemId"); + assertThat(tokenUtils.hentBrukerOgIssuer().get()).isEqualTo(new BrukerOgIssuer(Issuer.ISSUER_SYSTEM, "systemId")); + } + + @Test + public void hentInnloggetBruker__er_aad_clientcredentials() { + vaerInnloggetAadClientCredentials(); + assertThat(tokenUtils.harAdRolle("access_as_application")).isTrue(); + + InnloggetVeileder navAnsatt = TestData.enInnloggetVeileder(); + vaerInnloggetNavAnsatt(navAnsatt); + assertThat(tokenUtils.harAdRolle("access_as_application")).isFalse(); + } + + @Test + public void hentInnloggetBruker__er_uinnlogget() { + vaerUinnlogget(); + assertThat(tokenUtils.hentBrukerOgIssuer().isEmpty()).isTrue(); + } + + private void vaerUinnlogget() { + JWTClaimsSet claimsSet = new Builder().build(); + Map tokenMap = new HashMap<>(); + String tokenAsString = JwtTokenGenerator.createSignedJWT(JwkGenerator.getDefaultRSAKey(), claimsSet).serialize(); + JwtToken token = new JwtToken(tokenAsString); + tokenMap.put(token.getIssuer(), token); + TokenValidationContext context = new TokenValidationContext(tokenMap); + when(contextHolder.getTokenValidationContext()).thenReturn(context); + } + + private void vaerInnloggetAadClientCredentials() { + lagM2MValidationContext(ISSUER_AAD, List.of("access_as_application")); + } + private void vaerInnloggetSystem(String systemId) { + lagTokenValidationContext(ISSUER_SYSTEM, systemId, null, null, null); + } + + private void vaerInnloggetSelvbetjening(InnloggetArbeidsgiver bruker) { + lagTokenValidationContext(ISSUER_TOKENX, "not_a_fnr", null, ACR_LEVEL_4, null, bruker.getIdentifikator().asString()); + } + + private void vaerInnloggetSelvbetjeningNiva3(InnloggetArbeidsgiver bruker) { + lagTokenValidationContext(ISSUER_TOKENX, "not_a_fnr", null, "Level3", null, bruker.getIdentifikator().asString()); + } + + private void vaerInnloggetNavAnsatt(InnloggetVeileder innloggetBruker) { + lagTokenValidationContext(ISSUER_AAD, "blablabla", innloggetBruker.getIdentifikator().asString(), null, null); + } + + private void vaerInnloggetNavAnsatt(InnloggetBeslutter innloggetBruker) { + lagTokenValidationContext(ISSUER_AAD, "blablabla", innloggetBruker.getIdentifikator().asString(), null, + Arrays.asList("928636f4-fd0d-4149-978e-a6fb68bb19de", "158234a2-fd1d-4445-578e-a6fb68bb11das")); + } + + private void lagTokenValidationContext(Issuer issuer, String subject, String navIdent, String acrLevel, List groups) { + lagTokenValidationContext(issuer, subject, navIdent, acrLevel, groups, null); + } + + private void lagTokenValidationContext(Issuer issuer, String subject, String navIdent, String acrLevel, List groups, String pid) { + Date now = new Date(); + JWTClaimsSet claimsSet = new Builder() + .subject(subject) + .claim("NAVident", navIdent) + .issuer(issuer.issuerName) + .audience("aud-aad") + .jwtID(UUID.randomUUID().toString()) + .claim("pid", pid) + .claim("groups", groups) + .claim("acr",acrLevel) + .claim("ver", "1.0") + .claim("auth_time", now) + .claim("nonce", "myNonce") + .notBeforeTime(now) + .issueTime(now) + .expirationTime(new Date(now.getTime() + 1000000)).build(); + + String tokenAsString = JwtTokenGenerator.createSignedJWT(JwkGenerator.getDefaultRSAKey(), claimsSet).serialize(); + Map tokenMap = new HashMap<>(); + JwtToken token = new JwtToken(tokenAsString); + tokenMap.put(token.getIssuer(), token); + TokenValidationContext context = new TokenValidationContext(tokenMap); + + when(contextHolder.getTokenValidationContext()).thenReturn(context); + } + + private void lagM2MValidationContext(Issuer issuer, List roles) { + Date now = new Date(); + JWTClaimsSet claimsSet = new Builder() + .subject("machine") + .issuer(issuer.issuerName) + .audience("aud-aad") + .jwtID(UUID.randomUUID().toString()) + .claim("roles", roles) + .claim("ver", "1.0") + .claim("auth_time", now) + .claim("nonce", "myNonce") + .notBeforeTime(now) + .issueTime(now) + .expirationTime(new Date(now.getTime() + 1000000)).build(); + + String tokenAsString = JwtTokenGenerator.createSignedJWT(JwkGenerator.getDefaultRSAKey(), claimsSet).serialize(); + Map tokenMap = new HashMap<>(); + JwtToken token = new JwtToken(tokenAsString); + tokenMap.put(token.getIssuer(), token); + TokenValidationContext context = new TokenValidationContext(tokenMap); + + when(contextHolder.getTokenValidationContext()).thenReturn(context); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacAdapterTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacAdapterTest.java new file mode 100644 index 000000000..a5a4e2ef3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/abac/adapter/AbacAdapterTest.java @@ -0,0 +1,127 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.adapter; + +import com.github.tomakehurst.wiremock.WireMockServer; +import no.nav.tag.tiltaksgjennomforing.IntegrasjonerMockServer; +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.cache.EhCacheConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cache.ehcache.EhCacheCacheManager; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest +@ActiveProfiles({ Miljø.LOCAL, "wiremock" }) +@DirtiesContext +public class AbacAdapterTest { + private final AbacAdapter abacAdapter; + private final EhCacheCacheManager cacheManager; + private final WireMockServer mockServer; + + public AbacAdapterTest( + @Autowired AbacAdapter abacAdapter, + @Autowired IntegrasjonerMockServer mockServerService, + @Autowired EhCacheCacheManager ehCacheCacheManager) { + this.abacAdapter = abacAdapter; + this.cacheManager = ehCacheCacheManager; + this.mockServer = mockServerService.getServer(); + } + + @BeforeEach + public void setup() { + cacheManager.getCache(EhCacheConfig.ABAC_CACHE).clear(); + } + + @Test + public void skal_teste_at_Abac_ikke_gi_lese_tilgang_på_På_Gitt_Bruker_Og_Veileder() { + NavIdent veilederIdent = new NavIdent("F142226"); + Fnr deltakerFnr = new Fnr("01118023456"); + + boolean utfall = abacAdapter.harSkriveTilgang(veilederIdent.asString(), deltakerFnr.asString()); + + assertFalse(utfall); + } + + @Test + public void skal_teste_abac_feiler_gir_false() { + NavIdent veilederIdent = new NavIdent("F142226"); + Fnr deltakerFnr = new Fnr("11111111111"); + + boolean utfall = abacAdapter.harSkriveTilgang(veilederIdent.asString(), deltakerFnr.asString()); + assertFalse(utfall); + } + + @Test + public void skal_teste_at_Abac_gi_lese_tilgang_på_Gitt_Bruker_Og_Veileder() { + NavIdent veilederIdent = new NavIdent("F142226"); + Fnr deltakerFnr = new Fnr("07098142678"); + + boolean utfall = abacAdapter.harSkriveTilgang(veilederIdent.asString(), deltakerFnr.asString()); + assertTrue(utfall); + } + + @Test + public void skal_teste_at_Abac_ikke_gir_tilgang_til_feil_person_fra_cache() { + NavIdent veilederIdent = new NavIdent("F142226"); + Fnr deltakerFnr = new Fnr("01118023456"); + + boolean harIkkeTilgang = abacAdapter.harSkriveTilgang(veilederIdent.asString(), deltakerFnr.asString()); + + assertFalse(harIkkeTilgang); + + NavIdent veilederSkalHaTilgang = new NavIdent("X142226"); + boolean harTilgang = abacAdapter.harSkriveTilgang(veilederSkalHaTilgang.asString(), deltakerFnr.asString()); + + assertTrue(harTilgang); + + } + + @Test + public void bekreft_antall_ganger_endepunkter_blir_kalt_ved_abac() { + NavIdent veilederIdent = new NavIdent("F142226"); + + Fnr første_deltakerFnr = new Fnr("07098142678"); + Fnr andre_deltakerFnr = new Fnr("01118023456"); + mockServer.resetAll(); + + boolean tilgang_navId_F142226_og_fnr_07098142678 = abacAdapter.harSkriveTilgang(veilederIdent.asString(), første_deltakerFnr.asString()); + boolean tilgang_navId_F142226_og_fnr_07098142678_response2 = abacAdapter.harSkriveTilgang(veilederIdent.asString(), første_deltakerFnr.asString()); + + mockServer.verify(exactly(1), postRequestedFor(urlEqualTo("/abac"))); + mockServer.resetAll(); + + boolean tilgang_navId_F142226_og_fnr_11111111111 = abacAdapter.harSkriveTilgang(veilederIdent.asString(), andre_deltakerFnr.asString()); + boolean tilgang_navId_F142226_og_fnr_11111111111_response2 = abacAdapter.harSkriveTilgang(veilederIdent.asString(), andre_deltakerFnr.asString()); + + mockServer.verify(exactly(1), postRequestedFor(urlEqualTo("/abac"))); + + assertTrue(tilgang_navId_F142226_og_fnr_07098142678); + assertTrue(tilgang_navId_F142226_og_fnr_07098142678_response2); + + assertFalse(tilgang_navId_F142226_og_fnr_11111111111); + assertFalse(tilgang_navId_F142226_og_fnr_11111111111_response2); + } + + @Test + public void ikke_cache_ved_feil_fra_abac() { + NavIdent veilederIdent = new NavIdent("F142226"); + Fnr deltakerFnr = new Fnr("11111111111"); + mockServer.resetAll(); + + boolean tilgang_navId_F142226_og_fnr_07098142678 = abacAdapter.harSkriveTilgang(veilederIdent.asString(), deltakerFnr.asString()); + boolean tilgang_navId_F142226_og_fnr_07098142678_response2 = abacAdapter.harSkriveTilgang(veilederIdent.asString(), deltakerFnr.asString()); + + mockServer.verify(exactly(2), postRequestedFor(urlEqualTo("/abac"))); + + assertFalse(tilgang_navId_F142226_og_fnr_07098142678); + assertFalse(tilgang_navId_F142226_og_fnr_07098142678_response2); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/veilarbabac/ClearCacheInterceptorTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/veilarbabac/ClearCacheInterceptorTest.java new file mode 100644 index 000000000..af5104e40 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/autorisasjon/veilarbabac/ClearCacheInterceptorTest.java @@ -0,0 +1,46 @@ +package no.nav.tag.tiltaksgjennomforing.autorisasjon.veilarbabac; + +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.adapter.AbacAdapter; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.adapter.ClearCacheInterceptor; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.AxsysService; +import org.junit.jupiter.api.Test; + +import javax.servlet.http.HttpServletRequest; + +import static org.mockito.Mockito.*; + +public class ClearCacheInterceptorTest { + + private HttpServletRequest request = mock(HttpServletRequest.class); + private AbacAdapter abacAdapter = mock(AbacAdapter.class); + private AxsysService axsysService = mock(AxsysService.class); + private ClearCacheInterceptor clearCacheInterceptor = new ClearCacheInterceptor(abacAdapter, axsysService); + + @Test + public void skal_evicte_cache_hvis_header_er_true() throws Exception { + when(request.getHeader(ClearCacheInterceptor.CLEAR_CACHE_HEADER)).thenReturn("true"); + clearCacheInterceptor.preHandle(request, null, null); + verify(abacAdapter).cacheEvict(); + verify(axsysService).cacheEvict(); + } + + @Test + public void skal_ikke_evicte_cache_hvis_header_er_false() throws Exception { + when(request.getHeader(ClearCacheInterceptor.CLEAR_CACHE_HEADER)).thenReturn("false"); + clearCacheInterceptor.preHandle(request, null, null); + verifyNoMoreInteractions(abacAdapter, axsysService); + } + + @Test + public void skal_ikke_evicte_cache_hvis_header_er_tilfeldig_streng() throws Exception { + when(request.getHeader(ClearCacheInterceptor.CLEAR_CACHE_HEADER)).thenReturn("ajsdfbgjd"); + clearCacheInterceptor.preHandle(request, null, null); + verifyNoMoreInteractions(abacAdapter, axsysService); + } + + @Test + public void skal_ikke_evicte_cache_hvis_header_er_udefinert() throws Exception { + clearCacheInterceptor.preHandle(request, null, null); + verifyNoMoreInteractions(abacAdapter, axsysService); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/ArbeidsgiverTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/ArbeidsgiverTest.java new file mode 100644 index 000000000..ad191cb18 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/ArbeidsgiverTest.java @@ -0,0 +1,107 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.Set; +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.model.AltinnReportee; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2Client; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2GeoResponse; +import no.nav.tag.tiltaksgjennomforing.enhet.VeilarbArenaClient; +import no.nav.tag.tiltaksgjennomforing.exceptions.KanIkkeOppheveException; +import no.nav.tag.tiltaksgjennomforing.exceptions.VarighetDatoErTilbakeITidException; +import no.nav.tag.tiltaksgjennomforing.persondata.PdlRespons; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.Test; + + +public class ArbeidsgiverTest { + + @Test + public void opphevGodkjenninger__kan_oppheve_ved_deltakergodkjenning() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + arbeidsgiver.opphevGodkjenninger(avtale); + assertThat(avtale.erGodkjentAvDeltaker()).isFalse(); + } + + @Test + public void opphevGodkjenninger__kan_ikke_oppheve_veiledergodkjenning() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + assertThatThrownBy(() -> arbeidsgiver.opphevGodkjenninger(avtale)).isInstanceOf(KanIkkeOppheveException.class); + } + + @Test + public void oprettAvtale__setter_startverdier_på_avtale() { + OpprettAvtale opprettAvtale = new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.ARBEIDSTRENING); + + PersondataService persondataService = mock(PersondataService.class); + Norg2Client norg2Client = mock(Norg2Client.class); + VeilarbArenaClient veilarbArenaClient = mock(VeilarbArenaClient.class); + + final PdlRespons pdlRespons = TestData.enPdlrespons(false); + final Norg2GeoResponse navEnhet = new Norg2GeoResponse("Nav Grorud", "0411"); + + when(persondataService.hentPersondata(TestData.etFodselsnummer())).thenReturn(pdlRespons); + when(norg2Client.hentGeografiskEnhet(pdlRespons.getData().getHentGeografiskTilknytning().getGtBydel())).thenReturn(navEnhet); + + Arbeidsgiver arbeidsgiver = new Arbeidsgiver( + TestData.etFodselsnummer(), + Set.of( + new AltinnReportee( + "", + "", + null, + TestData.etBedriftNr().asString(), + null, + null + ) + ), + Map.of(TestData.etBedriftNr(), Set.of(Tiltakstype.ARBEIDSTRENING)), + persondataService, + norg2Client); + + Avtale avtale = arbeidsgiver.opprettAvtale(opprettAvtale); + assertThat(avtale.isOpprettetAvArbeidsgiver()).isTrue(); + assertThat(avtale.getGjeldendeInnhold().getDeltakerFornavn()).isNotNull(); + assertThat(avtale.getGjeldendeInnhold().getDeltakerEtternavn()).isNotNull(); + assertThat(avtale.getEnhetGeografisk()).isEqualTo(navEnhet.getEnhetNr()); + } + + @Test + public void endreAvtale_validererFraDato() { + Avtale avtale = TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordelt(); + Arbeidsgiver arbeidsgiver = new Arbeidsgiver( + null, + null, + null, + null, + null + ); + assertThatThrownBy( + () -> arbeidsgiver.avvisDatoerTilbakeITid(avtale, Now.localDate().minusDays(1), null) + ).isInstanceOf(VarighetDatoErTilbakeITidException.class); + } + + @Test + public void endreAvtale_validererTilDato() { + Avtale avtale = TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordelt(); + Arbeidsgiver arbeidsgiver = new Arbeidsgiver( + null, + null, + null, + null, + null + ); + assertThatThrownBy( + () -> arbeidsgiver.avvisDatoerTilbakeITid(avtale, Now.localDate(), Now.localDate().minusDays(1)) + ).isInstanceOf(VarighetDatoErTilbakeITidException.class); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/ArbeidstreningStrategyTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/ArbeidstreningStrategyTest.java new file mode 100644 index 000000000..ad29805d5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/ArbeidstreningStrategyTest.java @@ -0,0 +1,53 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Objects; + +import static no.nav.tag.tiltaksgjennomforing.avtale.AvtaleInnhold.Fields.*; +import static org.assertj.core.api.Assertions.assertThat; + +class ArbeidstreningStrategyTest { + + private AvtaleInnhold avtaleInnhold; + private AvtaleInnholdStrategy strategy; + + @BeforeEach + private void setUp() { + avtaleInnhold = new AvtaleInnhold(); + strategy = AvtaleInnholdStrategyFactory.create(avtaleInnhold, Tiltakstype.ARBEIDSTRENING); + } + + @Test + void test_at_felter_relevante_i_arbeidstrening_kan_endres() { + strategy.endre(TestData.endringPåAlleArbeidstreningFelter()); + + // Test for collections + assertThat(avtaleInnhold.getMaal()).isNotEmpty(); + + // Test for ikke-collections + assertThat(avtaleInnhold).extracting( + deltakerFornavn, + deltakerEtternavn, + deltakerTlf, + bedriftNavn, + arbeidsgiverFornavn, + arbeidsgiverEtternavn, + arbeidsgiverTlf, + veilederFornavn, + veilederEtternavn, + veilederTlf, + stillingstittel, + arbeidsoppgaver, + stillingprosent, + antallDagerPerUke, + startDato, + sluttDato, + tilrettelegging, + oppfolging, + stillingKonseptId, + stillingStyrk08 + ).filteredOn(Objects::isNull).isEmpty(); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleApiTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleApiTest.java new file mode 100644 index 000000000..b626d56fd --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleApiTest.java @@ -0,0 +1,128 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import com.fasterxml.jackson.databind.ObjectMapper; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.SlettemerkeProperties; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.TilgangskontrollService; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2Client; +import no.nav.tag.tiltaksgjennomforing.enhet.VeilarbArenaClient; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.AxsysService; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.auditing.AuditConsoleLogger; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import javax.servlet.http.Cookie; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static java.lang.String.format; +import static no.nav.tag.tiltaksgjennomforing.avtale.AvtaleApiTest.AvtaleApiTestUtil.lagTokenForNavIdent; +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.enArbeidstreningAvtale; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ActiveProfiles("local") +@SpringBootTest +@AutoConfigureMockMvc +@DirtiesContext +public class AvtaleApiTest { + + public AvtaleApiTest(@Autowired MockMvc mockMvc) { + this.mockMvc = mockMvc; + } + + MockMvc mockMvc; + ObjectMapper mapper = new ObjectMapper(); + + @MockBean + AxsysService axsysService; + @Mock + VeilarbArenaClient veilarbArenaClient; + @Mock + Norg2Client norg2Client; + @MockBean + private AvtaleRepository avtaleRepository; + @MockBean + private TilgangskontrollService tilgangskontrollService; + @Mock + private PersondataService persondataService; + @SpyBean + private AuditConsoleLogger auditConsoleLogger; + + @Test + public void hentSkalReturnereRiktigAvtale() throws Exception { + Avtale avtale = enArbeidstreningAvtale(); + var navIdent = TestData.enNavIdent(); + Veileder veileder = new Veileder( + navIdent, + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), any(Fnr.class))).thenReturn(true); + when(axsysService.hentEnheterNavAnsattHarTilgangTil(any())).thenReturn(List.of()); + when(avtaleRepository.findById(avtale.getId())).thenReturn(Optional.of(avtale)); + var res = hentAvtaleForVeileder(navIdent, avtale.getId()); + assertEquals(200, res.getStatus()); + assertEquals(avtale.getId().toString(), mapper.readTree(res.getContentAsByteArray()).get("id").asText()); + + verify(auditConsoleLogger, times(1)).logg(any()); + } + + private MockHttpServletResponse hentAvtaleForVeileder(NavIdent navIdent, UUID avtaleId) throws Exception { + var headers = new HttpHeaders(); + headers.put("Authorization", List.of("Bearer " + lagTokenForNavIdent(navIdent))); + return mockMvc.perform(MockMvcRequestBuilders.get(URI.create("/avtaler/" + avtaleId)) + .contentType(MediaType.APPLICATION_JSON) + .headers(headers) + .cookie(new Cookie("innlogget-part", Avtalerolle.VEILEDER.toString())) + .accept(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + } + + static class AvtaleApiTestUtil { + private static String tokenRequest(String url) { + try { + return HttpClient.newHttpClient().send( + HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .build(), HttpResponse.BodyHandlers.ofString() + ).body(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private String lagTokenForFnr(String fnr) { + return tokenRequest(format("https://tiltak-fakelogin.ekstern.dev.nav.no/token?pid=%s&aud=fake-tokenx&iss=tokenx&acr=Level4", fnr)); + } + + static String lagTokenForNavIdent(NavIdent navIdent) { + return tokenRequest(format("https://tiltak-fakelogin.ekstern.dev.nav.no/token?NAVident=%s&aud=fake-aad&iss=aad&acr=Level4", navIdent.asString())); + } + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleArenaMigreringTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleArenaMigreringTest.java new file mode 100644 index 000000000..b28341cc6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleArenaMigreringTest.java @@ -0,0 +1,41 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AvtaleArenaMigreringTest { + + @Test + public void lonnstilskudd_tilskuddsperioder_skal_ha_status_ubehandlet_hvis_ikke_ryddeavtale() { + Now.fixedDate(LocalDate.of(2023, 02, 15)); + Avtale avtale = TestData.enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(LocalDate.of(2022, 05, 01), LocalDate.of(2023, 04,30)); + assertThat(avtale.getTilskuddPeriode()).isNotEmpty(); + + avtale.getTilskuddPeriode().forEach(tilskuddPeriode -> { + assertThat(tilskuddPeriode.getStatus()).isEqualTo(TilskuddPeriodeStatus.UBEHANDLET); + }); + Now.resetClock(); + } + + @Test + public void lonnstilskudd_skal_generere_tilskuddsperioder_med_behandlet_status_om_ryddeavtale() { + Now.fixedDate(LocalDate.of(2023, 02, 15)); + Avtale avtale = TestData.enMidlertidigLønnstilskuddsRyddeAvtaleMedStartOgSluttGodkjentAvAlleParter(LocalDate.of(2022, 05, 01), LocalDate.of(2023, 04,30)); + assertThat(avtale.getTilskuddPeriode()).isNotEmpty(); + + avtale.getTilskuddPeriode().forEach(tilskuddPeriode -> { + + }); + + TilskuddPeriode sisteBehandletIArena = avtale.getTilskuddPeriode().stream().filter(tilskuddPeriode -> tilskuddPeriode.getStartDato().isAfter(LocalDate.of(2011, 12, 31))).findFirst().get(); + assertThat(sisteBehandletIArena.getStatus()).isEqualTo(TilskuddPeriodeStatus.BEHANDLET_I_ARENA); + TilskuddPeriode førsteUbehandlet = avtale.getTilskuddPeriode().stream().filter(tilskuddPeriode -> tilskuddPeriode.getStartDato().isAfter(LocalDate.of(2023, 01, 01))).findFirst().get(); + assertThat(førsteUbehandlet.getStatus()).isEqualTo(TilskuddPeriodeStatus.UBEHANDLET); + + Now.resetClock(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleControllerTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleControllerTest.java new file mode 100644 index 000000000..0a1c20ad6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleControllerTest.java @@ -0,0 +1,614 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggingService; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.SlettemerkeProperties; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.TilgangskontrollService; +import no.nav.tag.tiltaksgjennomforing.enhet.Formidlingsgruppe; +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2Client; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2GeoResponse; +import no.nav.tag.tiltaksgjennomforing.enhet.Oppfølgingsstatus; +import no.nav.tag.tiltaksgjennomforing.enhet.VeilarbArenaClient; +import no.nav.tag.tiltaksgjennomforing.exceptions.IkkeTilgangTilDeltakerException; +import no.nav.tag.tiltaksgjennomforing.exceptions.KanIkkeOppretteAvtalePåKode6Exception; +import no.nav.tag.tiltaksgjennomforing.exceptions.KontoregisterFeilException; +import no.nav.tag.tiltaksgjennomforing.exceptions.RessursFinnesIkkeException; +import no.nav.tag.tiltaksgjennomforing.exceptions.TilgangskontrollException; +import no.nav.tag.tiltaksgjennomforing.exceptions.TiltaksgjennomforingException; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.NavEnhet; +import no.nav.tag.tiltaksgjennomforing.okonomi.KontoregisterService; +import no.nav.tag.tiltaksgjennomforing.orgenhet.EregService; +import no.nav.tag.tiltaksgjennomforing.orgenhet.Organisasjon; +import no.nav.tag.tiltaksgjennomforing.persondata.PdlRespons; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.enArbeidstreningAvtale; +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.enNavIdent; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +@SuppressWarnings("rawtypes") +@ExtendWith(MockitoExtension.class) +public class AvtaleControllerTest { + + @Mock + VeilarbArenaClient veilarbArenaClient; + @Mock + Norg2Client norg2Client; + @Spy + TilskuddsperiodeConfig tilskuddsperiodeConfig = new TilskuddsperiodeConfig(); + @InjectMocks + private AvtaleController avtaleController; + @Mock + private AvtaleRepository avtaleRepository; + @Mock + private TilgangskontrollService tilgangskontrollService; + @Mock + private InnloggingService innloggingService; + @Mock + private EregService eregService; + @Mock + private PersondataService persondataService; + @Mock + private KontoregisterService kontoregisterService; + + private Pageable pageable = PageRequest.of(0, 100); + + private static List lagListeMedAvtaler(Avtale avtale, int antall) { + List avtaler = new ArrayList<>(); + for (int i = 0; i <= antall; i++) { + avtaler.add(avtale); + } + return avtaler; + } + + private static OpprettAvtale lagOpprettAvtale() { + Fnr deltakerFnr = new Fnr("00000000000"); + BedriftNr bedriftNr = new BedriftNr("12345678"); + return new OpprettAvtale(deltakerFnr, bedriftNr, Tiltakstype.ARBEIDSTRENING); + } + + @Test + public void hentSkalKasteResourceNotFoundExceptionHvisAvtaleIkkeFins() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + Veileder veileder = TestData.enVeileder(avtale); + værInnloggetSom(veileder); + when(avtaleRepository.findById(avtale.getId())).thenReturn(Optional.empty()); + assertThatThrownBy( + () -> avtaleController.hent(avtale.getId(), Avtalerolle.VEILEDER) + ).isExactlyInstanceOf(RessursFinnesIkkeException.class); + } + + @Test + public void hentSkalKastTilgangskontrollExceptionHvisInnloggetNavAnsattIkkeHarTilgang() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + værInnloggetSom( + new Veileder( + new NavIdent("Z333333"), + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ) + ); + when(avtaleRepository.findById(avtale.getId())).thenReturn(Optional.of(avtale)); + assertThatThrownBy( + () -> avtaleController.hent(avtale.getId(), Avtalerolle.VEILEDER) + ).isExactlyInstanceOf(TilgangskontrollException.class); + } + + @Disabled("må skrives om") + @Test + public void hentAvtalerOpprettetAvVeileder_skal_returnere_tom_liste_dersom_veileder_ikke_har_tilgang() { + NavIdent veilederNavIdent = new NavIdent("Z222222"); + Avtale avtaleForVeilederSomSøkesEtter = Avtale.veilederOppretterAvtale(lagOpprettAvtale(), veilederNavIdent); + NavIdent identTilInnloggetVeileder = new NavIdent("Z333333"); + Veileder veileder = new Veileder( + identTilInnloggetVeileder, + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + værInnloggetSom(veileder); + Avtale exampleAvtale = Avtale.builder() + .veilederNavIdent(new NavIdent("Z222222")) + .build(); + when( + avtaleRepository.findAll(eq(Example.of(exampleAvtale)), eq(pageable)) + ).thenReturn(new PageImpl(List.of(avtaleForVeilederSomSøkesEtter)));; + when(tilgangskontrollService.harSkrivetilgangTilKandidat( + eq(veileder), + any(Fnr.class) + )).thenReturn(false); + AvtalePredicate avtalePredicate = new AvtalePredicate(); + + Map avtalerPageResponse = veileder.hentAlleAvtalerMedLesetilgang( + avtaleRepository, + avtalePredicate.setVeilederNavIdent(veilederNavIdent), + Avtale.Fields.sistEndret, + pageable + ); + + List avtaler = (List)avtalerPageResponse.get("avtaler"); + assertThat(avtaler).doesNotContain(avtaleForVeilederSomSøkesEtter); + } + + public void hentAvtaleOpprettetAvInnloggetVeileder_fordelt_oppfolgingsEnhet_og_geoEnhet() { + NavIdent navIdent = new NavIdent("Z123456"); + String navEnhet = "0904"; + Veileder veileder = new Veileder( + navIdent, + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + værInnloggetSom(veileder); + Avtale nyAvtaleMedGeografiskEnhet = TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedOppfølgningsEnhetOgGeografiskEnhet(); + Avtale nyAvtaleMedOppfølgningsEnhet = TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedOppfølgningsEnhet(); + + when( + avtaleRepository.findAllByEnhetGeografiskOrEnhetOppfolging(eq(navEnhet), eq(navEnhet), eq(pageable)) + ).thenReturn(new PageImpl(List.of(nyAvtaleMedGeografiskEnhet, nyAvtaleMedOppfølgningsEnhet))); + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), any(Fnr.class))).thenReturn(true); + + Map avtalerPageResponse = veileder.hentAlleAvtalerMedLesetilgang( + avtaleRepository, + new AvtalePredicate().setNavEnhet(navEnhet), + Avtale.Fields.sistEndret, + pageable + ); + + List avtaler = (List)avtalerPageResponse.get("avtaler"); + assertThat(avtaler).isNotNull(); + } + + @Disabled("må skrives om") + @Test + public void hentAvtaleOpprettetAvInnloggetVeileder_pa_avtaleNr() { + NavIdent navIdent = new NavIdent("Z123456"); + Veileder veileder = new Veileder( + navIdent, + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + værInnloggetSom(veileder); + + Avtale enArbeidstreningsAvtale = TestData.enArbeidstreningAvtale(); + enArbeidstreningsAvtale.setAvtaleNr(TestData.ET_AVTALENR); + + Avtale exampleAvtale = Avtale.builder() + .avtaleNr(TestData.ET_AVTALENR) + .build(); + when( + avtaleRepository.findAll(eq(Example.of(exampleAvtale)), eq(pageable)) + ).thenReturn(new PageImpl(List.of(enArbeidstreningsAvtale))); + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), any(Fnr.class))).thenReturn(true); + + Map avtalerPageResponse = veileder.hentAlleAvtalerMedLesetilgang( + avtaleRepository, + new AvtalePredicate().setAvtaleNr(TestData.ET_AVTALENR), + Avtale.Fields.sistEndret, + pageable + ); + + List avtaler = (List)avtalerPageResponse.get("avtaler"); + assertThat(avtaler).isNotNull(); + assertThat(avtaler.stream().filter(avtaleMinimalListevisning-> avtaleMinimalListevisning.getTiltakstype() == Tiltakstype.ARBEIDSTRENING).toList()).isNotNull(); + } + + @Test + public void mentorGodkjennTaushetserklæring_når_innlogget_er_ikke_Mentor(){ + Avtale enMentorAvtale = TestData.enMentorAvtaleUsignert(); + NavIdent navIdent = new NavIdent("Z123456"); + Veileder veileder = new Veileder( + navIdent, + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + værInnloggetSom(veileder); + + assertThatThrownBy(() -> { + avtaleController.mentorGodkjennTaushetserklæring(enMentorAvtale.getId(), Instant.now(),Avtalerolle.DELTAKER); + }).isExactlyInstanceOf(TiltaksgjennomforingException.class); + } + + @Test + public void mentorGodkjennTaushetserklæring_når_innlogget_er__Mentor(){ + Avtale enMentorAvtale = TestData.enMentorAvtaleUsignert(); + Mentor mentor = new Mentor(enMentorAvtale.getMentorFnr()); + værInnloggetSom(mentor); + + when(avtaleRepository.findById(enMentorAvtale.getId())).thenReturn(Optional.of(enMentorAvtale)); + + avtaleController.mentorGodkjennTaushetserklæring(enMentorAvtale.getId(), Instant.now(),Avtalerolle.DELTAKER); + } + + @Test + public void hentSkalKastTilgangskontrollExceptionHvisInnloggetSelvbetjeningBrukerIkkeHarTilgang() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + værInnloggetSom( + new Arbeidsgiver( + new Fnr("55555566666"), + Set.of(), + Map.of(), + null, + null + ) + ); + when(avtaleRepository.findById(avtale.getId())).thenReturn(Optional.of(avtale)); + assertThatThrownBy( + () -> avtaleController.hent(avtale.getId(), Avtalerolle.ARBEIDSGIVER) + ).isExactlyInstanceOf(TilgangskontrollException.class); + } + + @Test + public void opprettAvtaleSkalReturnereCreatedOgOpprettetLokasjon() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + Fnr fnr = avtale.getDeltakerFnr(); + + final NavIdent navIdent = new NavIdent("Z123456"); + final NavEnhet navEnhet = TestData.ENHET_OPPFØLGING; + final PdlRespons pdlRespons = TestData.enPdlrespons(false); + final OpprettAvtale opprettAvtale = new OpprettAvtale( + avtale.getDeltakerFnr(), + avtale.getBedriftNr(), + Tiltakstype.ARBEIDSTRENING + ); + var veileder = new Veileder( + navIdent, + tilgangskontrollService, + persondataService, + norg2Client, + Set.of(navEnhet), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + værInnloggetSom(veileder); + when(avtaleRepository.save(any(Avtale.class))).thenReturn(avtale); + when( + eregService.hentVirksomhet(avtale.getBedriftNr())).thenReturn( + new Organisasjon( + avtale.getBedriftNr(), + avtale.getGjeldendeInnhold().getBedriftNavn() + ) + ); + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), any())).thenReturn(true); + when(persondataService.hentPersondata(fnr)).thenReturn(pdlRespons); + when(norg2Client.hentGeografiskEnhet(pdlRespons.getData().getHentGeografiskTilknytning().getGtBydel())) + .thenReturn( + new Norg2GeoResponse( + TestData.ENHET_GEOGRAFISK.getNavn(), + TestData.ENHET_GEOGRAFISK.getVerdi() + ) + ); + when(veilarbArenaClient.sjekkOgHentOppfølgingStatus(any())) + .thenReturn( + new Oppfølgingsstatus( + Formidlingsgruppe.ARBEIDSSOKER, + Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS, + "0906" + ) + ); + + ResponseEntity svar = avtaleController.opprettAvtaleSomVeileder(opprettAvtale, null); + assertThat(svar.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(svar.getHeaders().getLocation().getPath()).isEqualTo("/avtaler/" + avtale.getId()); + } + + @Test + public void endreAvtaleSkalReturnereNotFoundHvisDenIkkeFins() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + værInnloggetSom(TestData.enVeileder(avtale)); + when(avtaleRepository.findById(avtale.getId())).thenReturn(Optional.empty()); + assertThatThrownBy( + () -> avtaleController.endreAvtale( + avtale.getId(), + avtale.getSistEndret(), + TestData.ingenEndring(), + Avtalerolle.VEILEDER + ) + ).isExactlyInstanceOf(RessursFinnesIkkeException.class); + } + + @Test + public void endreAvtaleSkalReturnereOkHvisInnloggetPersonErVeileder() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + Veileder veileder = new Veileder( + enNavIdent(), + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + værInnloggetSom(veileder); + when(tilgangskontrollService.harSkrivetilgangTilKandidat( + any(Veileder.class), + any(Fnr.class) + )).thenReturn(true); + when(avtaleRepository.findById(avtale.getId())).thenReturn(Optional.of(avtale)); + when(avtaleRepository.save(avtale)).thenReturn(avtale); + ResponseEntity svar = avtaleController.endreAvtale( + avtale.getId(), + avtale.getSistEndret(), + TestData.ingenEndring(), + Avtalerolle.VEILEDER + ); + assertThat(svar.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void endreAvtaleSkalReturnereForbiddenHvisInnloggetPersonIkkeHarTilgang() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + værInnloggetSom(TestData.enArbeidsgiver()); + when(avtaleRepository.findById(avtale.getId())).thenReturn(Optional.of(avtale)); + assertThatThrownBy( + () -> avtaleController.endreAvtale( + avtale.getId(), + avtale.getSistEndret(), + TestData.ingenEndring(), + Avtalerolle.ARBEIDSGIVER + ) + ).isInstanceOf(TilgangskontrollException.class); + } + + @Test + public void hentAlleAvtalerInnloggetBrukerHarTilgangTilSkalIkkeReturnereAvtalerManIkkeHarTilgangTil() { + Avtale avtaleMedTilgang = TestData.enArbeidstreningAvtale(); + Avtale avtaleUtenTilgang = Avtale.veilederOppretterAvtale( + new OpprettAvtale(new Fnr("01039513753"), new BedriftNr("111222333"), Tiltakstype.ARBEIDSTRENING), + new NavIdent("X643564") + ); + Deltaker deltaker = TestData.enDeltaker(avtaleMedTilgang); + værInnloggetSom(deltaker); + List avtalerBrukerHarTilgangTil = lagListeMedAvtaler(avtaleMedTilgang, 5); + List alleAvtaler = new ArrayList<>(); + alleAvtaler.addAll(avtalerBrukerHarTilgangTil); + alleAvtaler.addAll(lagListeMedAvtaler(avtaleUtenTilgang, 4)); + when(avtaleRepository.findAllByDeltakerFnr(eq(deltaker.getIdentifikator()), eq(pageable))).thenReturn(new PageImpl(alleAvtaler)); + + Map avtalerPageResponse = deltaker.hentAlleAvtalerMedLesetilgang( + avtaleRepository, + new AvtalePredicate(), + Avtale.Fields.sistEndret, + pageable + ); + + List avtaler = (List)avtalerPageResponse.get("avtaler"); + assertThat(avtaler) + .hasSize(avtalerBrukerHarTilgangTil.size()); + } + + @Test + public void opprettAvtaleSomVeileder__skal_feile_hvis_veileder_ikke_har_tilgang_til_bruker() { + PersondataService persondataServiceIMetode = mock(PersondataService.class); + Veileder enNavAnsatt = new Veileder( + new NavIdent("T000000"), + tilgangskontrollService, + persondataServiceIMetode, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + værInnloggetSom(enNavAnsatt); + Fnr deltakerFnr = new Fnr("11111100000"); + when( + tilgangskontrollService.harSkrivetilgangTilKandidat(enNavAnsatt, deltakerFnr) + ).thenReturn(false); + assertThatThrownBy( + () -> avtaleController.opprettAvtaleSomVeileder( + new OpprettAvtale(deltakerFnr, new BedriftNr("111222333"), Tiltakstype.ARBEIDSTRENING), + null + ) + ).isInstanceOf(IkkeTilgangTilDeltakerException.class); + } + + @Test + public void opprettAvtaleSomVeileder__skal_feile_hvis_kode6() { + PersondataService persondataServiceIMetode = mock(PersondataService.class); + Veileder enNavAnsatt = new Veileder( + new NavIdent("T000000"), + tilgangskontrollService, + persondataServiceIMetode, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + værInnloggetSom(enNavAnsatt); + Fnr deltakerFnr = new Fnr("11111100000"); + when( + tilgangskontrollService.harSkrivetilgangTilKandidat(enNavAnsatt, deltakerFnr) + ).thenReturn(true); + PdlRespons pdlRespons = TestData.enPdlrespons(true); + when(persondataServiceIMetode.hentPersondata(deltakerFnr)).thenReturn(pdlRespons); + when(persondataServiceIMetode.erKode6(pdlRespons)).thenCallRealMethod(); + assertThatThrownBy( + () -> avtaleController.opprettAvtaleSomVeileder( + new OpprettAvtale(deltakerFnr, new BedriftNr("111222333"), Tiltakstype.ARBEIDSTRENING), + null + ) + ).isInstanceOf(KanIkkeOppretteAvtalePåKode6Exception.class); + } + + @Test + public void opprettAvtaleSomArbeidsgiver__skal_feile_hvis_ag_ikke_har_tilgang_til_bedrift() { + Arbeidsgiver arbeidsgiver = new Arbeidsgiver( + TestData.etFodselsnummer(), + Set.of(), + Map.of(), + null, + null + ); + værInnloggetSom(arbeidsgiver); + assertThatThrownBy( + () -> avtaleController.opprettAvtaleSomArbeidsgiver( + new OpprettAvtale(new Fnr("99887765432"), new BedriftNr("111222333"), + Tiltakstype.ARBEIDSTRENING) + ) + ).isInstanceOf(TilgangskontrollException.class); + } + + private void værInnloggetSom(Avtalepart avtalepart) { + lenient().when(innloggingService.hentAvtalepart(any())).thenReturn(avtalepart); + if (avtalepart instanceof Veileder) { + lenient().when(innloggingService.hentVeileder()).thenReturn((Veileder) avtalepart); + } + if (avtalepart instanceof Arbeidsgiver) { + lenient().when(innloggingService.hentArbeidsgiver()).thenReturn((Arbeidsgiver) avtalepart); + } + } + + @Test + public void viser_ikke_avbruttGrunn_til_arbeidsgiver() { + Avtale avtale = enArbeidstreningAvtale(); + avtale.setAvbruttGrunn("Hemmelig"); + var arbeidsgiver = TestData.enArbeidsgiver(avtale); + værInnloggetSom(arbeidsgiver); + when(avtaleRepository.findById(avtale.getId())).thenReturn(Optional.of(avtale)); + Avtale hentetAvtale = avtaleController.hent(avtale.getId(), Avtalerolle.VEILEDER); + assertThat(hentetAvtale.getAvbruttGrunn()).isNull(); + } + + @Test + public void viser_ikke_navenheter_til_arbeidsgiver() { + Avtale avtale = enArbeidstreningAvtale(); + var arbeidsgiver = TestData.enArbeidsgiver(avtale); + værInnloggetSom(arbeidsgiver); + when(avtaleRepository.findById(avtale.getId())).thenReturn(Optional.of(avtale)); + Avtale hentetAvtale = avtaleController.hent(avtale.getId(), Avtalerolle.VEILEDER); + assertThat(hentetAvtale.getEnhetGeografisk()).isNull(); + assertThat(hentetAvtale.getEnhetOppfolging()).isNull(); + } + + + @Test + public void hentBedriftKontonummer_skal_returnere_nytt_bedriftKontonummer() { + NavIdent veilederNavIdent = new NavIdent("Z222222"); + Avtale avtale = Avtale.veilederOppretterAvtale(lagOpprettAvtale(), veilederNavIdent); + NavIdent identTilInnloggetVeileder = new NavIdent("Z333333"); + Veileder veileder = new Veileder( + identTilInnloggetVeileder, + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + værInnloggetSom(veileder); + when(kontoregisterService.hentKontonummer(anyString())).thenReturn("990983666"); + when( + tilgangskontrollService.harSkrivetilgangTilKandidat( + eq(veileder), + any(Fnr.class) + ) + ).thenReturn(true); + when(avtaleRepository.findById(avtale.getId())).thenReturn(Optional.of(avtale)); + String kontonummer = avtaleController.hentBedriftKontonummer(avtale.getId(), Avtalerolle.VEILEDER); + assertThat(kontonummer).isEqualTo("990983666"); + } + + @Test + public void hentBedriftKontonummer_skal_kaste_en_feil_når_innlogget_part_ikke_har_tilgang_til_Avtale() throws TilgangskontrollException { + NavIdent veilederNavIdent = new NavIdent("Z222222"); + Avtale avtale = Avtale.veilederOppretterAvtale(lagOpprettAvtale(), veilederNavIdent); + NavIdent identTilInnloggetVeileder = new NavIdent("Z333333"); + Veileder veileder = new Veileder( + identTilInnloggetVeileder, + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + værInnloggetSom(veileder); + when(tilgangskontrollService.harSkrivetilgangTilKandidat( + eq(veileder), + any(Fnr.class) + )).thenReturn(false); + when(avtaleRepository.findById(avtale.getId())).thenReturn(Optional.of(avtale)); + assertThatThrownBy( + () -> avtaleController.hentBedriftKontonummer(avtale.getId(), Avtalerolle.VEILEDER) + ).isInstanceOf(TilgangskontrollException.class); + } + + @Test + public void hentBedriftKontonummer_skal_kaste_en_feil_når_kontonummer_rest_service_svarer_med_feil_response_status_og_kaster_en_exception() { + NavIdent veilederNavIdent = new NavIdent("Z222222"); + Avtale avtale = Avtale.veilederOppretterAvtale(lagOpprettAvtale(), veilederNavIdent); + NavIdent identTilInnloggetVeileder = new NavIdent("Z333333"); + Veileder veileder = new Veileder( + identTilInnloggetVeileder, + tilgangskontrollService, + persondataService, + norg2Client, + Collections.emptySet(), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + værInnloggetSom(veileder); + when(kontoregisterService.hentKontonummer(anyString())).thenThrow(KontoregisterFeilException.class); + when(tilgangskontrollService.harSkrivetilgangTilKandidat( + eq(veileder), + any(Fnr.class) + )).thenReturn(true); + when(avtaleRepository.findById(avtale.getId())).thenReturn(Optional.of(avtale)); + assertThatThrownBy( + () -> avtaleController.hentBedriftKontonummer(avtale.getId(), Avtalerolle.VEILEDER) + ).isInstanceOf(KontoregisterFeilException.class); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtalePredicateTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtalePredicateTest.java new file mode 100644 index 000000000..c1b22185e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtalePredicateTest.java @@ -0,0 +1,142 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class AvtalePredicateTest { + + private static Tiltakstype annenTiltakstypeEnnPåAvtale(Avtale avtale) { + for (Tiltakstype t : Tiltakstype.values()) { + if (!t.equals(avtale.getTiltakstype())) { + return t; + } + } + return null; + } + + private static Status annenStatusEnnPåAvtale(Avtale avtale) { + for (Status s : Status.values()) { + if (!s.equals(avtale.getTiltakstype())) { + return s; + } + } + return null; + } + + @Test + void veileder_nav_ident_oppgitt() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + AvtalePredicate query = new AvtalePredicate(); + query.setVeilederNavIdent(avtale.getVeilederNavIdent()); + assertThat(query.test(avtale)).isTrue(); + } + + @Test + void deltaker_oppgitt() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + AvtalePredicate query = new AvtalePredicate(); + query.setDeltakerFnr(avtale.getDeltakerFnr()); + assertThat(query.test(avtale)).isTrue(); + } + + @Test + void enhet_oppfølgning_oppgitt() { + Avtale avtale = TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedOppfølgningsEnhet(); + AvtalePredicate query = new AvtalePredicate(); + query.setNavEnhet(TestData.ENHET_OPPFØLGING.getVerdi()); + assertThat(query.test(avtale)).isTrue(); + } + + @Test + void enhet_geografisk_oppgitt() { + Avtale avtale = TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedGeografiskEnhet(); + AvtalePredicate query = new AvtalePredicate(); + query.setNavEnhet(TestData.ENHET_GEOGRAFISK.getVerdi()); + assertThat(query.test(avtale)).isTrue(); + } + + @Test + void bedrift_oppgitt() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + AvtalePredicate query = new AvtalePredicate(); + query.setBedriftNr(avtale.getBedriftNr()); + assertThat(query.test(avtale)).isTrue(); + } + + @Test + void tiltakstype_oppgitt() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + AvtalePredicate query = new AvtalePredicate(); + query.setTiltakstype(avtale.getTiltakstype()); + assertThat(query.test(avtale)).isTrue(); + } + + @Test + void tiltakstype_annen_type() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + AvtalePredicate query = new AvtalePredicate(); + query.setTiltakstype(annenTiltakstypeEnnPåAvtale(avtale)); + assertThat(query.test(avtale)).isFalse(); + } + + @Test + void status_oppgitt() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + AvtalePredicate query = new AvtalePredicate(); + query.setStatus(avtale.statusSomEnum()); + assertThat(query.test(avtale)).isTrue(); + } + + @Test + void status_annen_type() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + AvtalePredicate query = new AvtalePredicate(); + query.setStatus(annenStatusEnnPåAvtale(avtale)); + assertThat(query.test(avtale)).isFalse(); + } + + @Test + void kombinasjon_med_feil() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + AvtalePredicate query = new AvtalePredicate(); + query.setVeilederNavIdent(avtale.getVeilederNavIdent()); + query.setBedriftNr(avtale.getBedriftNr()); + query.setDeltakerFnr(new Fnr("66666666666")); + assertThat(query.test(avtale)).isFalse(); + } + + @Test + void ingenting_oppgitt() { + AvtalePredicate query = new AvtalePredicate(); + assertThat(query.test(TestData.enArbeidstreningAvtale())).isTrue(); + } + + @Test + void avtaleNr_oppgitt() { + Avtale avtale = TestData.enArbeidstreningsAvtaleMedGittAvtaleNr(); + AvtalePredicate query = new AvtalePredicate(); + query.setAvtaleNr(TestData.ET_AVTALENR); + assertThat(query.test(avtale)).isTrue(); + } + + @Test + void ignorerer_ugyldig_felt_i_prediacate() { + FilterSok filterSok = new FilterSok(); + filterSok.setQueryParametre(""" + {"avtaleNr": 1, "avtaleTulleNummer": 1337} + """); + AvtalePredicate avtalePredicate = filterSok.getAvtalePredicate(); + assertThat(avtalePredicate.getAvtaleNr()).isEqualTo(1); + } + + @Test + void ignorerer_ugyldig_json_i_prediacate() { + FilterSok filterSok = new FilterSok(); + filterSok.setQueryParametre(""" + { + """); + AvtalePredicate avtalePredicate = filterSok.getAvtalePredicate(); + assertThat(avtalePredicate).isEqualTo(new AvtalePredicate()); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleRepositoryTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleRepositoryTest.java new file mode 100644 index 000000000..6a3dba8f3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleRepositoryTest.java @@ -0,0 +1,312 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.events.GodkjenningerOpphevetAvVeileder; +import no.nav.tag.tiltaksgjennomforing.datadeling.AvtaleMeldingEntitetRepository; +import no.nav.tag.tiltaksgjennomforing.datavarehus.DvhMeldingEntitetRepository; +import no.nav.tag.tiltaksgjennomforing.metrikker.MetrikkRegistrering; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import no.nav.tag.tiltaksgjennomforing.varsel.SmsRepository; +import no.nav.tag.tiltaksgjennomforing.varsel.VarselRepository; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.ArbeidsgiverNotifikasjonRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@SpringBootTest +@ActiveProfiles({Miljø.LOCAL, "wiremock"}) +@DirtiesContext +public class AvtaleRepositoryTest { + + @Autowired + private AvtaleRepository avtaleRepository; + + @Autowired + private VarselRepository varselRepository; + + @Autowired + private SmsRepository smsRepository; + + @Autowired + private AvtaleInnholdRepository avtaleInnholdRepository; + + @Autowired + private DvhMeldingEntitetRepository dvhMeldingEntitetRepository; + @Autowired + private AvtaleMeldingEntitetRepository avtaleMeldingEntitetRepository; + + @Autowired + private ArbeidsgiverNotifikasjonRepository arbeidsgiverNotifikasjonRepository; + + @MockBean + private MetrikkRegistrering metrikkRegistrering; + + + @BeforeEach + public void setup() { + varselRepository.deleteAll(); + smsRepository.deleteAll(); + avtaleInnholdRepository.deleteAll(); + arbeidsgiverNotifikasjonRepository.deleteAll(); + dvhMeldingEntitetRepository.deleteAll(); + avtaleMeldingEntitetRepository.deleteAll(); + avtaleRepository.deleteAll(); + } + + @Test + public void nyAvtaleSkalKunneLagreOgReturneresAvRepository() { + Avtale lagretAvtale = avtaleRepository.save(TestData.enArbeidstreningAvtale()); + + Optional avtaleOptional = avtaleRepository.findById(lagretAvtale.getId()); + assertThat(avtaleOptional).isPresent(); + } + + @Test + public void skalKunneLagreMaalFlereGanger() { + // Lage avtale + Avtale lagretAvtale = avtaleRepository.save(TestData.enArbeidstreningAvtale()); + + // Lagre maal skal fungere + EndreAvtale endreAvtale = new EndreAvtale(); + Maal maal = TestData.etMaal(); + endreAvtale.setMaal(List.of(maal)); + lagretAvtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtaleRepository.save(lagretAvtale); + + // Lage ny avtale + Avtale lagretAvtale2 = avtaleRepository.save(TestData.enArbeidstreningAvtale()); + + // Lagre maal skal enda fungere + EndreAvtale endreAvtale2 = new EndreAvtale(); + Maal maal2 = TestData.etMaal(); + endreAvtale2.setMaal(List.of(maal2)); + lagretAvtale2.endreAvtale(Now.instant(), endreAvtale2, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtaleRepository.save(lagretAvtale2); + } + + @Test + public void skalKunneLagreOppgaverFlereGanger() { + // Lage avtale + Avtale lagretAvtale = avtaleRepository.save(TestData.enArbeidstreningAvtale()); + + // Lagre maal skal fungere + EndreAvtale endreAvtale = new EndreAvtale(); + lagretAvtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtaleRepository.save(lagretAvtale); + + // Lage ny avtale + Avtale lagretAvtale2 = avtaleRepository.save(TestData.enArbeidstreningAvtale()); + + // Lagre maal skal enda fungere + EndreAvtale endreAvtale2 = new EndreAvtale(); + lagretAvtale2.endreAvtale(Now.instant(), endreAvtale2, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtaleRepository.save(lagretAvtale2); + } + + @Test + public void skalKunneLagreTilskuddsPeriode() { + // Lage avtale + Avtale lagretAvtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + lagretAvtale.getGjeldendeInnhold().setSumLonnstilskudd(20000); + lagretAvtale = avtaleRepository.save(lagretAvtale); + + // Lagre tilskuddsperiode skal fungere + EndreAvtale endreAvtale = new EndreAvtale(); + endreAvtale.setStartDato(lagretAvtale.getGjeldendeInnhold().getStartDato()); + endreAvtale.setSluttDato(lagretAvtale.getGjeldendeInnhold().getSluttDato()); + endreAvtale.setManedslonn(20000); + endreAvtale.setStillingprosent(100); + endreAvtale.setOtpSats(0.02); + endreAvtale.setFeriepengesats(BigDecimal.valueOf(0.12)); + endreAvtale.setArbeidsgiveravgift(BigDecimal.valueOf(0.141)); + endreAvtale.setLonnstilskuddProsent(40); + + lagretAvtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + Avtale nyLagretAvtale = avtaleRepository.save(lagretAvtale); + + var perioder = nyLagretAvtale.getTilskuddPeriode(); + assertThat(perioder).isNotEmpty(); + assertThat(lagretAvtale.getId()).isEqualTo(perioder.first().getAvtale().getId()); + } + + @Test + public void avtale_godkjent_pa_vegne_av_skal_lagres_med_pa_vegne_av_grunn() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + GodkjentPaVegneGrunn godkjentPaVegneGrunn = TestData.enGodkjentPaVegneGrunn(); + godkjentPaVegneGrunn.setIkkeBankId(true); + Veileder veileder = TestData.enVeileder(avtale); + + veileder.godkjennForVeilederOgDeltaker(godkjentPaVegneGrunn, avtale); + Avtale lagretAvtale = avtaleRepository.save(avtale); + + assertThat(lagretAvtale.getGjeldendeInnhold().getGodkjentPaVegneGrunn().isIkkeBankId()).isEqualTo(godkjentPaVegneGrunn.isIkkeBankId()); + } + + @Test + public void lagre_pa_vegne_skal_publisere_domainevent() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + Veileder veileder = TestData.enVeileder(avtale); + GodkjentPaVegneGrunn godkjentPaVegneGrunn = TestData.enGodkjentPaVegneGrunn(); + veileder.godkjennForVeilederOgDeltaker(godkjentPaVegneGrunn, avtale); + + avtaleRepository.save(avtale); + verify(metrikkRegistrering).godkjentPaVegneAv(any()); + } + + @Test + public void opprettAvtale__skal_publisere_domainevent() { + Avtale nyAvtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(new Fnr("10101033333"), new BedriftNr("101033333"), Tiltakstype.ARBEIDSTRENING), new NavIdent("Q000111")); + avtaleRepository.save(nyAvtale); + verify(metrikkRegistrering).avtaleOpprettet(any()); + } + + @Test + public void endreAvtale__skal_publisere_domainevent() { + Avtale avtale = avtaleRepository.save(TestData.enArbeidstreningAvtale()); + verify(metrikkRegistrering, never()).avtaleEndret(any()); + avtale.endreAvtale(Now.instant(), TestData.ingenEndring(), Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtaleRepository.save(avtale); + verify(metrikkRegistrering).avtaleEndret(any()); + } + + @Test + public void godkjennForArbeidsgiver__skal_publisere_domainevent() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + TestData.enArbeidsgiver(avtale).godkjennAvtale(avtale.getSistEndret(), avtale); + avtaleRepository.save(avtale); + verify(metrikkRegistrering).godkjentAvArbeidsgiver(any()); + } + + @Test + public void godkjennForDeltaker__skal_publisere_domainevent() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + TestData.enDeltaker(avtale).godkjennAvtale(avtale.getSistEndret(), avtale); + avtaleRepository.save(avtale); + verify(metrikkRegistrering).godkjentAvDeltaker(any()); + } + + @Test + public void godkjennForVeileder__skal_publisere_domainevent() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + TestData.enVeileder(avtale).godkjennAvtale(avtale.getSistEndret(), avtale); + avtaleRepository.save(avtale); + verify(metrikkRegistrering).godkjentAvVeileder(any()); + } + + @Test + public void opphevGodkjenning__skal_publisere_domainevent() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + TestData.enArbeidsgiver(avtale).godkjennForAvtalepart(avtale); + TestData.enVeileder(avtale).opphevGodkjenninger(avtale); + avtaleRepository.save(avtale); + verify(metrikkRegistrering).godkjenningerOpphevet(any(GodkjenningerOpphevetAvVeileder.class)); + } + + @Test + public void finnGodkjenteAvtalerMedTilskuddsperiodestatusOgNavEnheter__skal_ikke_kunne_hente_avtale_med_tiltakstype_arbeidstrening_3() { + Avtale avtale = enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(Now.localDate().plusDays(1), Now.localDate().plusMonths(3).plusDays(1)); + Avtale avtale2 = enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(Now.localDate().plusDays(5), Now.localDate().plusMonths(3).plusDays(5)); + Avtale avtale3 = enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(Now.localDate().plusDays(10), Now.localDate().plusMonths(3).plusDays(10)); + Avtale avtale4 = enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(Now.localDate().plusDays(15), Now.localDate().plusMonths(3).plusDays(15)); + avtale.getGjeldendeInnhold().setDeltakerFornavn("Arne"); + avtale2.getGjeldendeInnhold().setDeltakerFornavn("Bjarne"); + avtale3.getGjeldendeInnhold().setDeltakerFornavn("Carl"); + + avtaleRepository.save(avtale); + avtaleRepository.save(avtale2); + avtaleRepository.save(avtale3); + avtaleRepository.save(avtale4); + + Set navEnheter = Set.of(ENHET_OPPFØLGING.getVerdi()); + Set tiltakstype = Set.of(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, Tiltakstype.VARIG_LONNSTILSKUDD); + Sort by = Sort.by(Sort.Order.asc("startDato")); + Pageable pageable = PageRequest.of(0, 10, by); + long plussDato = ChronoUnit.DAYS.between(LocalDate.now(), LocalDate.now().plusMonths(3)); + LocalDate decisiondate = LocalDate.now().plusDays(plussDato); + + Page beslutterOversikt = avtaleRepository.finnGodkjenteAvtalerMedTilskuddsperiodestatusOgNavEnheter( + TilskuddPeriodeStatus.UBEHANDLET, + decisiondate, + tiltakstype, + navEnheter, + null, + null, + pageable + ); + + List beslutterOversiktList = BeslutterOversikt.getBeslutterOversikt(beslutterOversikt); + assertThat(beslutterOversiktList.size()).isEqualTo(4); + } + + @Test + public void findAllByEnhet__skal_kunne_hente_avtale_med_enhet() { + Pageable pageable = PageRequest.of(0, 100); + Avtale lagretAvtale = TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedOppfølgningsEnhet(); + avtaleRepository.save(lagretAvtale); + + Page avtaleMedRiktigEnhet = avtaleRepository + .findAllByVeilederNavIdentIsNullAndEnhetGeografiskOrVeilederNavIdentIsNullAndEnhetOppfolging(ENHET_GEOGRAFISK.getVerdi(), ENHET_OPPFØLGING.getVerdi(), pageable); + + assertThat(avtaleMedRiktigEnhet.getContent()).isNotEmpty(); + } + + @Test + public void findAllByEnhet__skal_ikke_kunne_hente_avtale_med_feil_enhet() { + Pageable pageable = PageRequest.of(0, 100); + Avtale lagretAvtale = TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedOppfølgningsEnhet(); + avtaleRepository.save(lagretAvtale); + + Page avtaleMedRiktigEnhet = avtaleRepository + .findAllByVeilederNavIdentIsNullAndEnhetGeografiskOrVeilederNavIdentIsNullAndEnhetOppfolging(ENHET_GEOGRAFISK.getVerdi(), ENHET_GEOGRAFISK.getVerdi(), pageable); + + assertThat(avtaleMedRiktigEnhet.getContent()).isEmpty(); + } + + @Test + public void findAllByEnhet__skal_kunne_hente_avtale_med_både_geografisk_og_oppfølgningsenhet() { + Pageable pageable = PageRequest.of(0, 100); + Avtale lagretAvtale = TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedOppfølgningsEnhetOgGeografiskEnhet(); + avtaleRepository.save(lagretAvtale); + + Page avtaleMedRiktigEnhet = avtaleRepository + .findAllByVeilederNavIdentIsNullAndEnhetGeografiskOrVeilederNavIdentIsNullAndEnhetOppfolging(ENHET_OPPFØLGING.getVerdi(), ENHET_OPPFØLGING.getVerdi(), pageable); + + assertThat(avtaleMedRiktigEnhet.getContent()).isNotEmpty(); + } + + @Test + public void findAllByEnhet__skal_kunne_hente_avtale_med_både_oppfølgning_og_geografiskenhet() { + Pageable pageable = PageRequest.of(0, 100); + Avtale lagretAvtale = TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedOppfølgningsEnhetOgGeografiskEnhet(); + avtaleRepository.save(lagretAvtale); + + Page avtaleMedRiktigEnhet = avtaleRepository.findAllByVeilederNavIdentIsNullAndEnhetGeografiskOrVeilederNavIdentIsNullAndEnhetOppfolging(ENHET_GEOGRAFISK.getVerdi(), ENHET_GEOGRAFISK.getVerdi(), pageable); + + assertThat(avtaleMedRiktigEnhet.getContent()).isNotEmpty(); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleSortererTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleSortererTest.java new file mode 100644 index 000000000..bc89a2d84 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleSortererTest.java @@ -0,0 +1,23 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class AvtaleSortererTest { + @Test + void sorterer_liste() { + Avtale avtale1 = TestData.enArbeidstreningAvtale(); + Avtale avtale2 = TestData.enArbeidstreningAvtale(); + Avtale avtale3 = TestData.enArbeidstreningAvtale(); + avtale1.getGjeldendeInnhold().setDeltakerFornavn("B"); + avtale2.getGjeldendeInnhold().setDeltakerFornavn("A"); + avtale3.getGjeldendeInnhold().setDeltakerFornavn(null); + List usortertListe = List.of(avtale3, avtale1, avtale2); + List sortertListe = usortertListe.stream().sorted(AvtaleSorterer.comparatorForAvtale(AvtaleInnhold.Fields.deltakerFornavn)).collect(Collectors.toList()); + + assertThat(sortertListe).containsExactly(avtale2, avtale1, avtale3); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleTest.java new file mode 100644 index 000000000..bb7b40d11 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtaleTest.java @@ -0,0 +1,1265 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.autorisasjon.SlettemerkeProperties; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.TilgangskontrollService; +import no.nav.tag.tiltaksgjennomforing.avtale.RefusjonKontaktperson.Fields; +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2Client; +import no.nav.tag.tiltaksgjennomforing.enhet.VeilarbArenaClient; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilLonnstilskuddsprosentException; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.SamtidigeEndringerException; +import no.nav.tag.tiltaksgjennomforing.exceptions.TiltaksgjennomforingException; +import no.nav.tag.tiltaksgjennomforing.exceptions.VarighetForLangArbeidstreningException; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.NavEnhet; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import static no.nav.tag.tiltaksgjennomforing.AssertFeilkode.assertFeilkode; +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.avtalerMedTilskuddsperioder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AvtaleTest { + + @Test + public void nyAvtaleFactorySkalReturnereRiktigeStandardverdier() { + Fnr deltakerFnr = new Fnr("23078637692"); + + NavIdent veilederNavIdent = new NavIdent("X123456"); + BedriftNr bedriftNr = new BedriftNr("000111222"); + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(deltakerFnr, bedriftNr, Tiltakstype.ARBEIDSTRENING), veilederNavIdent); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(avtale.getOpprettetTidspunkt()).isNotNull(); + softly.assertThat(avtale.getDeltakerFnr()).isEqualTo(deltakerFnr); + softly.assertThat(avtale.getGjeldendeInnhold().getDeltakerTlf()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getMaal()).isEmpty(); + softly.assertThat(avtale.getGjeldendeInnhold().getDeltakerFornavn()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getDeltakerEtternavn()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getBedriftNavn()).isNull(); + softly.assertThat(avtale.getBedriftNr()).isEqualTo(bedriftNr); + softly.assertThat(avtale.getGjeldendeInnhold().getArbeidsgiverFornavn()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getArbeidsgiverEtternavn()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getArbeidsgiverTlf()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getVeilederFornavn()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getVeilederEtternavn()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getVeilederTlf()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getOppfolging()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getTilrettelegging()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getStartDato()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getSluttDato()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getStillingprosent()).isNull(); + softly.assertThat(avtale.getGjeldendeInnhold().getStillingstittel()).isNull(); + softly.assertThat(avtale.erGodkjentAvDeltaker()).isFalse(); + softly.assertThat(avtale.erGodkjentAvArbeidsgiver()).isFalse(); + softly.assertThat(avtale.erGodkjentAvVeileder()).isFalse(); + }); + } + + @Test + public void nyAvtaleSkalFeileHvisManglerDeltaker() { + assertThatThrownBy(() -> Avtale.veilederOppretterAvtale(new OpprettAvtale(null, new BedriftNr("111222333"), Tiltakstype.ARBEIDSTRENING), new NavIdent("X123456"))).isInstanceOf(TiltaksgjennomforingException.class); + } + + @Test + public void nyAvtaleSkalFeileHvisManglerArbeidsgiver() { + assertThatThrownBy(() -> Avtale.veilederOppretterAvtale(new OpprettAvtale(new Fnr("23078637692"), null, Tiltakstype.ARBEIDSTRENING), new NavIdent("X123456"))).isInstanceOf(TiltaksgjennomforingException.class); + } + + @Test + public void nyAvtaleSkalFeileHvisManglerVeileder() { + assertThatThrownBy(() -> Avtale.veilederOppretterAvtale(new OpprettAvtale(new Fnr("23078637692"), new BedriftNr("000111222"), Tiltakstype.ARBEIDSTRENING), null)).isInstanceOf(TiltaksgjennomforingException.class); + } + + @Test + public void nyAvtaleSkalFeileHvisDeltakerErForUng() { + assertFeilkode(Feilkode.SOMMERJOBB_IKKE_GAMMEL_NOK, () -> Avtale.veilederOppretterAvtale(new OpprettAvtale(new Fnr("24010970772"), new BedriftNr("000111222"), Tiltakstype.ARBEIDSTRENING), null)); + } + + @Test + public void nyAvtaleSkalFeileHvisDeltakerErForGammelForSommerjobb() { + assertFeilkode(Feilkode.SOMMERJOBB_FOR_GAMMEL, () -> Avtale.veilederOppretterAvtale(new OpprettAvtale(new Fnr("08098114468"), new BedriftNr("000111222"), Tiltakstype.SOMMERJOBB), null)); + } + + @Test + public void sjekkVersjon__ugyldig_versjon() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + assertThatThrownBy(() -> avtale.sjekkSistEndret(Instant.MIN)).isInstanceOf(SamtidigeEndringerException.class); + } + + @Test + public void sjekkVersjon__null() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + assertThatThrownBy(() -> avtale.sjekkSistEndret(null)).isInstanceOf(SamtidigeEndringerException.class); + } + + @Test + public void sjekkVersjon__gyldig_versjon() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + avtale.sjekkSistEndret(Now.instant()); + } + + @Test + public void endreAvtaleSkalOppdatereRiktigeFelt() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + EndreAvtale endreAvtale = TestData.endringPåAlleArbeidstreningFelter(); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(avtale.getGjeldendeInnhold().getDeltakerFornavn()).isEqualTo(endreAvtale.getDeltakerFornavn()); + softly.assertThat(avtale.getGjeldendeInnhold().getDeltakerEtternavn()).isEqualTo(endreAvtale.getDeltakerEtternavn()); + softly.assertThat(avtale.getGjeldendeInnhold().getDeltakerTlf()).isEqualTo(endreAvtale.getDeltakerTlf()); + softly.assertThat(avtale.getGjeldendeInnhold().getBedriftNavn()).isEqualTo(endreAvtale.getBedriftNavn()); + softly.assertThat(avtale.getGjeldendeInnhold().getArbeidsgiverFornavn()).isEqualTo(endreAvtale.getArbeidsgiverFornavn()); + softly.assertThat(avtale.getGjeldendeInnhold().getArbeidsgiverEtternavn()).isEqualTo(endreAvtale.getArbeidsgiverEtternavn()); + softly.assertThat(avtale.getGjeldendeInnhold().getArbeidsgiverTlf()).isEqualTo(endreAvtale.getArbeidsgiverTlf()); + softly.assertThat(avtale.getGjeldendeInnhold().getVeilederFornavn()).isEqualTo(endreAvtale.getVeilederFornavn()); + softly.assertThat(avtale.getGjeldendeInnhold().getVeilederEtternavn()).isEqualTo(endreAvtale.getVeilederEtternavn()); + softly.assertThat(avtale.getGjeldendeInnhold().getVeilederTlf()).isEqualTo(endreAvtale.getVeilederTlf()); + softly.assertThat(avtale.getGjeldendeInnhold().getOppfolging()).isEqualTo(endreAvtale.getOppfolging()); + softly.assertThat(avtale.getGjeldendeInnhold().getTilrettelegging()).isEqualTo(endreAvtale.getTilrettelegging()); + softly.assertThat(avtale.getGjeldendeInnhold().getStartDato()).isEqualTo(endreAvtale.getStartDato()); + softly.assertThat(avtale.getGjeldendeInnhold().getSluttDato()).isEqualTo(endreAvtale.getSluttDato()); + softly.assertThat(avtale.getGjeldendeInnhold().getStillingprosent()).isEqualTo(endreAvtale.getStillingprosent()); + softly.assertThat(avtale.getGjeldendeInnhold().getMaal()).isEqualTo(endreAvtale.getMaal()); + softly.assertThat(avtale.getGjeldendeInnhold().getStillingstittel()).isEqualTo(endreAvtale.getStillingstittel()); + }); + } + + @Test + public void endreAvtale__for_langt_maal_skal_feile() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + Maal etMaal = TestData.etMaal(); + etMaal.setBeskrivelse("Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn.Dette er en string pa 1024 tegn."); + EndreAvtale endreAvtale = new EndreAvtale(); + endreAvtale.setMaal(List.of(etMaal)); + assertThatThrownBy(() -> avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder)).isInstanceOf(TiltaksgjennomforingException.class); + } + + @Test + public void endreAvtale__startdato_satt_men_ikke_sluttdato() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + EndreAvtale endreAvtale = new EndreAvtale(); + LocalDate startDato = Now.localDate(); + endreAvtale.setStartDato(startDato); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + assertThat(avtale.getGjeldendeInnhold().getStartDato()).isEqualTo(startDato); + } + + @Test + public void endreAvtale__sluttdato_satt_men_ikke_startdato() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + EndreAvtale endreAvtale = new EndreAvtale(); + LocalDate sluttDato = Now.localDate(); + endreAvtale.setSluttDato(sluttDato); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + assertThat(avtale.getGjeldendeInnhold().getSluttDato()).isEqualTo(sluttDato); + } + + @Test + public void endreAvtale__startdato_og_sluttdato_satt_18mnd() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + EndreAvtale endreAvtale = new EndreAvtale(); + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.plusMonths(18).minusDays(1); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + assertThat(avtale.getGjeldendeInnhold().getStartDato()).isEqualTo(startDato); + assertThat(avtale.getGjeldendeInnhold().getSluttDato()).isEqualTo(sluttDato); + } + + @Test + public void endreAvtale__startdato_og_sluttdato_satt_over_18mnd() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + EndreAvtale endreAvtale = new EndreAvtale(); + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.plusMonths(18); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + assertThatThrownBy(() -> avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder)).isInstanceOf(VarighetForLangArbeidstreningException.class); + } + + @Test + public void endreAvtale__startdato_er_etter_sluttdato() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + EndreAvtale endreAvtale = new EndreAvtale(); + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.minusDays(1); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + assertFeilkode(Feilkode.START_ETTER_SLUTT, () -> avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder)); + } + + @Test + public void endreAvtaleSkalIkkeEndreGodkjenninger() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + boolean deltakerGodkjenningFoerEndring = avtale.erGodkjentAvDeltaker(); + boolean arbeidsgiverGodkjenningFoerEndring = avtale.erGodkjentAvArbeidsgiver(); + boolean veilederGodkjenningFoerEndring = avtale.erGodkjentAvVeileder(); + + avtale.endreAvtale(Now.instant(), TestData.endringPåAlleArbeidstreningFelter(), Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(deltakerGodkjenningFoerEndring).isEqualTo(avtale.erGodkjentAvDeltaker()); + softly.assertThat(arbeidsgiverGodkjenningFoerEndring).isEqualTo(avtale.erGodkjentAvArbeidsgiver()); + softly.assertThat(veilederGodkjenningFoerEndring).isEqualTo(avtale.erGodkjentAvVeileder()); + }); + } + + @Test + public void endreAvtaleSkalInkrementereVersjon() throws InterruptedException { + Avtale avtale = TestData.enArbeidstreningAvtale(); + Instant førstEndret = avtale.getSistEndret(); + Thread.sleep(10); + avtale.endreAvtale(førstEndret, TestData.ingenEndring(), Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + assertThat(avtale.getSistEndret()).isAfter(førstEndret); + } + + @Test + public void kanIkkeGodkjennesNårNoeMangler__arbeidstrening() { + Set arbeidstreningsfelter = Set.of( + AvtaleInnhold.Fields.deltakerFornavn, + AvtaleInnhold.Fields.deltakerEtternavn, + AvtaleInnhold.Fields.deltakerTlf, + AvtaleInnhold.Fields.bedriftNavn, + AvtaleInnhold.Fields.arbeidsgiverFornavn, + AvtaleInnhold.Fields.arbeidsgiverEtternavn, + AvtaleInnhold.Fields.arbeidsgiverTlf, + AvtaleInnhold.Fields.veilederFornavn, + AvtaleInnhold.Fields.veilederEtternavn, + AvtaleInnhold.Fields.veilederTlf, + AvtaleInnhold.Fields.stillingstittel, + AvtaleInnhold.Fields.arbeidsoppgaver, + AvtaleInnhold.Fields.stillingprosent, + AvtaleInnhold.Fields.antallDagerPerUke, + AvtaleInnhold.Fields.maal, + AvtaleInnhold.Fields.startDato, + AvtaleInnhold.Fields.sluttDato, + AvtaleInnhold.Fields.tilrettelegging, + AvtaleInnhold.Fields.oppfolging + ); + + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.ARBEIDSTRENING), TestData.enNavIdent()); + + testAtAlleFelterMangler(avtale, arbeidstreningsfelter); + testAtHvertEnkeltFeltMangler(avtale, arbeidstreningsfelter, avtale.getTiltakstype()); + } + + @Test + public void kanIkkeGodkjennesNårNoeMangler__midlertidig_lønnstilskudd() { + Set lønnstilskuddfelter = Set.of( + AvtaleInnhold.Fields.deltakerFornavn, + AvtaleInnhold.Fields.deltakerEtternavn, + AvtaleInnhold.Fields.deltakerTlf, + AvtaleInnhold.Fields.bedriftNavn, + AvtaleInnhold.Fields.arbeidsgiverFornavn, + AvtaleInnhold.Fields.arbeidsgiverEtternavn, + AvtaleInnhold.Fields.arbeidsgiverTlf, + AvtaleInnhold.Fields.veilederFornavn, + AvtaleInnhold.Fields.veilederEtternavn, + AvtaleInnhold.Fields.veilederTlf, + AvtaleInnhold.Fields.stillingstittel, + AvtaleInnhold.Fields.arbeidsoppgaver, + AvtaleInnhold.Fields.stillingprosent, + AvtaleInnhold.Fields.antallDagerPerUke, + AvtaleInnhold.Fields.stillingstype, + AvtaleInnhold.Fields.startDato, + AvtaleInnhold.Fields.sluttDato, + AvtaleInnhold.Fields.arbeidsgiverKontonummer, + AvtaleInnhold.Fields.manedslonn, + AvtaleInnhold.Fields.feriepengesats, + AvtaleInnhold.Fields.otpSats, + AvtaleInnhold.Fields.arbeidsgiveravgift, + AvtaleInnhold.Fields.tilrettelegging, + AvtaleInnhold.Fields.oppfolging, + AvtaleInnhold.Fields.harFamilietilknytning + ); + + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), TestData.enNavIdent()); + + testAtAlleFelterMangler(avtale, lønnstilskuddfelter); + testAtHvertEnkeltFeltMangler(avtale, lønnstilskuddfelter, avtale.getTiltakstype()); + } + + @Test + public void kanIkkeGodkjennesNårNoeMangler__varig_lønnstilskudd() { + Set lønnstilskuddfelter = Set.of( + AvtaleInnhold.Fields.deltakerFornavn, + AvtaleInnhold.Fields.deltakerEtternavn, + AvtaleInnhold.Fields.deltakerTlf, + AvtaleInnhold.Fields.bedriftNavn, + AvtaleInnhold.Fields.arbeidsgiverFornavn, + AvtaleInnhold.Fields.arbeidsgiverEtternavn, + AvtaleInnhold.Fields.arbeidsgiverTlf, + AvtaleInnhold.Fields.veilederFornavn, + AvtaleInnhold.Fields.veilederEtternavn, + AvtaleInnhold.Fields.veilederTlf, + AvtaleInnhold.Fields.stillingstittel, + AvtaleInnhold.Fields.arbeidsoppgaver, + AvtaleInnhold.Fields.stillingprosent, + AvtaleInnhold.Fields.antallDagerPerUke, + AvtaleInnhold.Fields.stillingstype, + AvtaleInnhold.Fields.startDato, + AvtaleInnhold.Fields.sluttDato, + AvtaleInnhold.Fields.arbeidsgiverKontonummer, + AvtaleInnhold.Fields.manedslonn, + AvtaleInnhold.Fields.feriepengesats, + AvtaleInnhold.Fields.otpSats, + AvtaleInnhold.Fields.arbeidsgiveravgift, + AvtaleInnhold.Fields.tilrettelegging, + AvtaleInnhold.Fields.oppfolging, + AvtaleInnhold.Fields.harFamilietilknytning, + AvtaleInnhold.Fields.lonnstilskuddProsent, + Fields.refusjonKontaktpersonFornavn + ); + + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.VARIG_LONNSTILSKUDD), TestData.enNavIdent()); + avtale.getGjeldendeInnhold().setRefusjonKontaktperson(new RefusjonKontaktperson(null, "Duck", "12345678", true)); + testAtAlleFelterMangler(avtale, lønnstilskuddfelter); + testAtHvertEnkeltFeltMangler(avtale, lønnstilskuddfelter, avtale.getTiltakstype()); + } + + @Test + public void kanIkkeGodkjennesNårNoeMangler__mentor() { + Set mentorfelter = Set.of( + AvtaleInnhold.Fields.deltakerFornavn, + AvtaleInnhold.Fields.deltakerEtternavn, + AvtaleInnhold.Fields.deltakerTlf, + AvtaleInnhold.Fields.bedriftNavn, + AvtaleInnhold.Fields.arbeidsgiverFornavn, + AvtaleInnhold.Fields.arbeidsgiverEtternavn, + AvtaleInnhold.Fields.arbeidsgiverTlf, + AvtaleInnhold.Fields.veilederFornavn, + AvtaleInnhold.Fields.veilederEtternavn, + AvtaleInnhold.Fields.veilederTlf, + AvtaleInnhold.Fields.startDato, + AvtaleInnhold.Fields.sluttDato, + AvtaleInnhold.Fields.mentorFornavn, + AvtaleInnhold.Fields.mentorEtternavn, + AvtaleInnhold.Fields.mentorTimelonn, + AvtaleInnhold.Fields.mentorOppgaver, + AvtaleInnhold.Fields.mentorAntallTimer, + AvtaleInnhold.Fields.tilrettelegging, + AvtaleInnhold.Fields.oppfolging, + AvtaleInnhold.Fields.mentorTlf, + AvtaleInnhold.Fields.harFamilietilknytning + ); + + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MENTOR), TestData.enNavIdent()); + + testAtAlleFelterMangler(avtale, mentorfelter); + testAtHvertEnkeltFeltMangler(avtale, mentorfelter, avtale.getTiltakstype()); + } + + private static void testAtHvertEnkeltFeltMangler(Avtale avtale, Set felterSomKrevesForTiltakstype, Tiltakstype tiltakstype) { + for (String felt : felterSomKrevesForTiltakstype) { + EndreAvtale endreAvtale = endringPåAltUtenom(felt, tiltakstype); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + assertThat(avtale.felterSomIkkeErFyltUt()).containsOnly(felt); + assertFeilkode(Feilkode.ALT_MA_VAERE_FYLT_UT, () -> avtale.godkjennForArbeidsgiver(TestData.enIdentifikator())); + } + } + + private static void testAtAlleFelterMangler(Avtale avtale, Set arbeidstreningsfelter) { + assertThat(avtale.felterSomIkkeErFyltUt()).containsExactlyInAnyOrderElementsOf(arbeidstreningsfelter); + } + + private static EndreAvtale endringPåAltUtenom(String felt, Tiltakstype tiltakstype) { + EndreAvtale endreAvtale = + switch(tiltakstype){ + case ARBEIDSTRENING -> TestData.endringPåAlleArbeidstreningFelter(); + case MENTOR -> TestData.endringPåAlleMentorFelter(); + case INKLUDERINGSTILSKUDD -> TestData.endringPåAlleInkluderingstilskuddFelter(); + case MIDLERTIDIG_LONNSTILSKUDD, SOMMERJOBB, VARIG_LONNSTILSKUDD -> TestData.endringPåAlleLønnstilskuddFelter(); + }; + Object field = ReflectionTestUtils.getField(endreAvtale, felt); + if (field instanceof Collection) { + ((Collection) field).clear(); + } else { + ReflectionTestUtils.setField(endreAvtale, felt, null); + } + return endreAvtale; + } + + @Test + public void kanGodkjennesNaarAltErUtfylt() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.sjekkOmAltErKlarTilGodkjenning(); + } + + @Test + public void kan_ikke_godkjennes_når_alt_er_utfylt_men_beregning_mangler() { + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), TestData.enNavIdent()); + avtale.setEnhetOppfolging("0000"); + avtale.setEnhetsnavnOppfolging("0000"); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS); + avtale.endreAvtale(avtale.getSistEndret(), TestData.endringPåAlleLønnstilskuddFelter(), Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + // Later som at det har skjedd noe mystisk med prosenten, kan skyldes feil ved innhenting fra Arena + avtale.getGjeldendeInnhold().setLonnstilskuddProsent(null); + assertFeilkode(Feilkode.MANGLER_BEREGNING, () -> avtale.sjekkOmAltErKlarTilGodkjenning()); + + // Later som at det har skjedd noe mystisk med sum, kan skyldes feil ved innhenting fra Arena + avtale.getGjeldendeInnhold().setLonnstilskuddProsent(67); + avtale.getGjeldendeInnhold().setSumLonnstilskudd(null); + assertFeilkode(Feilkode.MANGLER_BEREGNING, () -> avtale.sjekkOmAltErKlarTilGodkjenning()); + } + + @Test + public void status__ny_avtale() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + assertThat(avtale.status()).isEqualTo(Status.PÅBEGYNT.getBeskrivelse()); + } + + @Test + public void status__null_startdato() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + avtale.getGjeldendeInnhold().setStartDato(null); + avtale.getGjeldendeInnhold().setSluttDato(null); + assertThat(avtale.status()).isEqualTo(Status.PÅBEGYNT.getBeskrivelse()); + } + + @Test + public void status__noe_fylt_ut() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + avtale.getGjeldendeInnhold().setStartDato(Now.localDate().plusDays(5)); + avtale.getGjeldendeInnhold().setSluttDato(avtale.getGjeldendeInnhold().getStartDato().plusMonths(3)); + avtale.getGjeldendeInnhold().setBedriftNavn("testbedriftsnavn"); + assertThat(avtale.status()).isEqualTo(Status.PÅBEGYNT.getBeskrivelse()); + } + + @Test + public void status__avsluttet_i_gaar() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setStartDato(Now.localDate().minusWeeks(4).minusDays(1)); + avtale.getGjeldendeInnhold().setSluttDato(avtale.getGjeldendeInnhold().getStartDato().plusWeeks(4)); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + assertThat(avtale.status()).isEqualTo(Status.AVSLUTTET.getBeskrivelse()); + } + + @Test + public void status__avslutter_i_dag() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setStartDato(Now.localDate().minusWeeks(4)); + avtale.getGjeldendeInnhold().setSluttDato(avtale.getGjeldendeInnhold().getStartDato().plusWeeks(4)); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + assertThat(avtale.status()).isEqualTo(Status.GJENNOMFØRES.getBeskrivelse()); + } + + @Test + public void status__startet_i_dag() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setStartDato(Now.localDate()); + avtale.getGjeldendeInnhold().setSluttDato(avtale.getGjeldendeInnhold().getStartDato().plusWeeks(4)); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + assertThat(avtale.status()).isEqualTo(Status.GJENNOMFØRES.getBeskrivelse()); + } + + @Test + public void status__starter_i_morgen() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setStartDato(Now.localDate().plusDays(1)); + avtale.getGjeldendeInnhold().setSluttDato(avtale.getGjeldendeInnhold().getStartDato().plusWeeks(4)); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + assertThat(avtale.status()).isEqualTo(Status.KLAR_FOR_OPPSTART.getBeskrivelse()); + } + + @Test + public void status__klar_for_godkjenning() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + assertThat(avtale.status()).isEqualTo(Status.MANGLER_GODKJENNING.getBeskrivelse()); + } + + @Test + public void status__veileder_har_godkjent() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setStartDato(Now.localDate().plusDays(1)); + avtale.getGjeldendeInnhold().setSluttDato(Now.localDate().plusDays(1).plusMonths(1).minusDays(1)); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + assertThat(avtale.status()).isEqualTo(Status.KLAR_FOR_OPPSTART.getBeskrivelse()); + } + + @Test + public void status__naar_deltaker_tlf_mangler() { + // Deltaker tlf ble innført etter at avtaler er opprettet. Det kan derfor være + // avtaler som er inngått som mangler tlf. + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setStartDato(Now.localDate().minusDays(1)); + avtale.getGjeldendeInnhold().setSluttDato(Now.localDate().minusDays(1).plusMonths(1)); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + avtale.getGjeldendeInnhold().setDeltakerTlf(null); + assertThat(avtale.status()).isEqualTo(Status.GJENNOMFØRES.getBeskrivelse()); + } + + @Test + public void status__annullert() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + avtale.annuller(TestData.enVeileder(avtale), "grunnen"); + assertThat(avtale.status()).isEqualTo(Status.ANNULLERT.getBeskrivelse()); + assertThat(avtale.getAnnullertTidspunkt()).isNotNull(); + assertThat(avtale.getAnnullertGrunn()).isEqualTo("grunnen"); + } + + @Test + public void avbryt_ufordelt_avtale_skal_bli_fordelt() { + Avtale avtale = TestData.enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordelt(); + Veileder veileder = TestData.enVeileder(new NavIdent("Z123456")); + avtale.annuller(veileder, "grunnen"); + + assertThat(avtale.status()).isEqualTo(Status.ANNULLERT.getBeskrivelse()); + assertThat(avtale.erUfordelt()).isFalse(); + assertThat(avtale.getVeilederNavIdent()).isEqualTo(veileder.getIdentifikator()); + } + + @Test + public void tom_avtale_kan_avbrytes() { + Avtale tomAvtale = TestData.enArbeidstreningAvtale(); + assertThat(tomAvtale.kanAvbrytes()).isTrue(); + } + + @Test + public void fylt_avtale_kan_avbrytes() { + Avtale fyltAvtale = TestData.enAvtaleMedAltUtfylt(); + assertThat(fyltAvtale.kanAvbrytes()).isTrue(); + } + + @Test + public void godkjent_av_veileder_avtale_kan_avbrytes() { + Avtale godkjentAvtale = TestData.enAvtaleMedAltUtfyltGodkjentAvVeileder(); + assertThat(godkjentAvtale.kanAvbrytes()).isTrue(); + } + + @Test + public void allerede_avbrutt_avtale_kan_ikke_avbrytes() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.setAvbrutt(true); + assertThat(avtale.kanAvbrytes()).isFalse(); + } + + @Test + public void sistEndretNå__kalles_ved_endreAvtale() throws InterruptedException { + Instant førEndringen = Now.instant(); + Avtale avtale = TestData.enArbeidstreningAvtale(); + Thread.sleep(10); + avtale.endreAvtale(Instant.MAX, TestData.ingenEndring(), Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + assertThat(avtale.getSistEndret()).isAfter(førEndringen); + } + + @Test + public void sistEndretNå__kalles_ved_godkjennForDeltaker() throws InterruptedException { + Instant førEndringen = Now.instant(); + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Thread.sleep(10); + avtale.godkjennForDeltaker(TestData.enIdentifikator()); + assertThat(avtale.getSistEndret()).isAfter(førEndringen); + } + + @Test + public void sistEndretNå__kalles_ved_godkjennForArbeidsgiver() throws InterruptedException { + Instant førEndringen = Now.instant(); + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Thread.sleep(10); + avtale.godkjennForArbeidsgiver(TestData.enIdentifikator()); + assertThat(avtale.getSistEndret()).isAfter(førEndringen); + } + + @Test + public void sistEndretNå__kalles_ved_godkjennForVeileder() throws InterruptedException { + Instant førEndringen = Now.instant(); + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Thread.sleep(10); + avtale.godkjennForArbeidsgiver(TestData.enIdentifikator()); + assertThat(avtale.getSistEndret()).isAfter(førEndringen); + } + + @Test + public void sistEndretNå__kalles_ved_godkjennForVeilederOgDeltaker() throws InterruptedException { + Instant førEndringen = Now.instant(); + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + Thread.sleep(10); + avtale.godkjennForVeilederOgDeltaker(TestData.enNavIdent(), TestData.enGodkjentPaVegneGrunn()); + assertThat(avtale.getSistEndret()).isAfter(førEndringen); + } + + @Test + public void sistEndretNå__kalles_ved_opphevGodkjenningerSomArbeidsgiver() throws InterruptedException { + Instant førEndringen = Now.instant(); + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Thread.sleep(10); + avtale.opphevGodkjenningerSomArbeidsgiver(); + assertThat(avtale.getSistEndret()).isAfter(førEndringen); + } + + @Test + public void sistEndretNå__kalles_ved_opphevGodkjenningerSomVeileder() throws InterruptedException { + Instant førEndringen = Now.instant(); + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Thread.sleep(10); + avtale.opphevGodkjenningerSomVeileder(); + assertThat(avtale.getSistEndret()).isAfter(førEndringen); + } + + @Test + public void sistEndretNå__kalles_ved_annuller() throws InterruptedException { + Instant førEndringen = Now.instant(); + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Thread.sleep(10); + avtale.annuller(TestData.enVeileder(avtale), "grunn"); + assertThat(avtale.getSistEndret()).isAfter(førEndringen); + } + + @Test + public void sistEndretNå__kalles_ved_endreOppfølgingOgTilrettelegging() throws InterruptedException { + Instant førEndringen = Now.instant(); + Avtale avtale = TestData.enAvtaleMedAltUtfyltGodkjentAvVeileder(); + Thread.sleep(10); + avtale.endreOppfølgingOgTilrettelegging(new EndreOppfølgingOgTilrettelegging("Oppfølging", "Tilrettelegging"), TestData.enNavIdent()); + assertThat(avtale.getSistEndret()).isAfter(førEndringen); + } + + @Test + public void sistEndretNå__kalles_ved_forlengAvtale() { + Avtale avtale = TestData.enAvtaleMedAltUtfyltGodkjentAvVeileder(); + Instant sistEndret = avtale.getSistEndret(); + avtale.forlengAvtale(avtale.getGjeldendeInnhold().getSluttDato().plusDays(1), TestData.enNavIdent()); + assertThat(avtale.getSistEndret()).isAfter(sistEndret); + } + + @Test + public void sistEndretNå__kalles_ved_forkortAvtale() { + Avtale avtale = TestData.enAvtaleMedAltUtfyltGodkjentAvVeileder(); + Instant sistEndret = avtale.getSistEndret(); + avtale.forkortAvtale(avtale.getGjeldendeInnhold().getSluttDato().minusDays(1), "lala", null, TestData.enNavIdent()); + assertThat(avtale.getSistEndret()).isAfter(sistEndret); + } + + @Test + public void avtaleklarForOppstart() { + Avtale avtale = TestData.enAvtaleKlarForOppstart(); + assertThat(avtale.status()).isEqualTo(Status.KLAR_FOR_OPPSTART.getBeskrivelse()); + } + + @Test + public void avtale_opprettet_av_arbedsgiver_skal_være_ufordelt() { + Avtale avtale = Avtale.arbeidsgiverOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD)); + assertThat(avtale.isOpprettetAvArbeidsgiver()).isTrue(); + assertThat(avtale.erUfordelt()).isTrue(); + } + + @Test + public void avtale_kan_være_ufordelt_selv_om_alt_er_utfylt() { + Avtale avtale = Avtale.arbeidsgiverOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD)); + avtale.endreAvtale(Now.instant(), TestData.endringPåAlleLønnstilskuddFelter(), Avtalerolle.ARBEIDSGIVER, avtalerMedTilskuddsperioder); + assertThat(avtale.erUfordelt()).isTrue(); + } + + @Test + public void avtale_skal_ikke_kunne_godkjennes_uten_navident() { + Avtale avtale = Avtale.arbeidsgiverOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD)); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS); + avtale.endreAvtale(Now.instant(), TestData.endringPåAlleLønnstilskuddFelter(), Avtalerolle.ARBEIDSGIVER, avtalerMedTilskuddsperioder); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + assertFeilkode(Feilkode.MANGLER_VEILEDER_PÅ_AVTALE, () -> arbeidsgiver.godkjennAvtale(Instant.now(), avtale)); + } + + @Test + public void ufordelt_avtale_ikke_klar_for_godkjenning() { + Avtale avtale = Avtale.arbeidsgiverOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.ARBEIDSTRENING)); + avtale.endreAvtale(Now.instant(), TestData.endringPåAlleArbeidstreningFelter(), Avtalerolle.ARBEIDSGIVER, avtalerMedTilskuddsperioder); + assertFeilkode(Feilkode.MANGLER_VEILEDER_PÅ_AVTALE, () -> avtale.sjekkOmAltErKlarTilGodkjenning()); + } + + @Test + public void ufordelt_avtale_må_tildeles_veileder_før_den_kan_godkjennes() { + Avtale avtale = Avtale.arbeidsgiverOppretterAvtale( + new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD)); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS); + avtale.endreAvtale(Now.instant(), TestData.endringPåAlleLønnstilskuddFelter(), Avtalerolle.ARBEIDSGIVER, avtalerMedTilskuddsperioder); + assertFeilkode(Feilkode.MANGLER_VEILEDER_PÅ_AVTALE, () -> avtale.sjekkOmAltErKlarTilGodkjenning()); + Veileder veileder = TestData.enVeileder(new NavIdent("Z123456")); + veileder.overtaAvtale(avtale); + avtale.godkjennForArbeidsgiver(TestData.enIdentifikator()); + assertThat(avtale.erGodkjentAvArbeidsgiver()).isTrue(); + } + + + @Test + public void ufordelt_midlertidig_lts_avtale_endrer_avtale_med_lavere_lønnstilskuddprosent_enn_mellom_kun_40_60_prosent() { + Avtale avtale = Avtale + .veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), + new NavIdent("Z123456")); + avtale.setKvalifiseringsgruppe(null); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setLonnstilskuddProsent(20); + assertThatThrownBy(() -> avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder)) + .isInstanceOf(FeilLonnstilskuddsprosentException.class); + } + + @Test + public void ufordelt_midlertidig_lts_avtale_endrer_avtale_med_høyere_enn_maks_lønnstilskuddprosent_enn_60_prosent() { + Avtale avtale = Avtale + .veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), + new NavIdent("Z123456")); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + avtale.setKvalifiseringsgruppe(null); + endreAvtale.setLonnstilskuddProsent(67); + assertThatThrownBy(() -> avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder)) + .isInstanceOf(FeilLonnstilskuddsprosentException.class); + } + + + @Test + public void ufordelt_varig_lts_avtale_endrer_avtale_med_lavere_lønnstilskuddprosent_enn_0_prosent() { + Avtale avtale = Avtale + .veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.VARIG_LONNSTILSKUDD), + new NavIdent("Z123456")); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setLonnstilskuddProsent(-1); + assertThatThrownBy(() -> avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder)) + .isInstanceOf(FeilLonnstilskuddsprosentException.class); + } + + @Test + public void ufordelt_varig_lts_avtale_endrer_avtale_med_høyere_enn_maks_lønnstilskuddprosent_enn_75_prosent() { + Avtale avtale = Avtale + .veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.VARIG_LONNSTILSKUDD), + new NavIdent("Z123456")); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setLonnstilskuddProsent(100); + assertThatThrownBy(() -> avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder)) + .isInstanceOf(FeilLonnstilskuddsprosentException.class); + } + + @Test + public void endre_tilskuddsberegning_setter_riktige_felter() { + Avtale avtale = TestData.enLonnstilskuddAvtaleGodkjentAvVeileder(); + double otpSats = 0.048; + BigDecimal feriepengesats = new BigDecimal("0.166"); + BigDecimal arbeidsgiveravgift = BigDecimal.ZERO; + int manedslonn = 44444; + + assertThat(avtale.getGjeldendeInnhold().getVersjon()).isEqualTo(1); + avtale.endreTilskuddsberegning(EndreTilskuddsberegning.builder().otpSats(otpSats).feriepengesats(feriepengesats).arbeidsgiveravgift(arbeidsgiveravgift).manedslonn(manedslonn).build(), TestData.enNavIdent()); + + assertThat(avtale.getGjeldendeInnhold().getVersjon()).isEqualTo(2); + assertThat(avtale.erGodkjentAvVeileder()).isTrue(); + assertThat(avtale.getGjeldendeInnhold().getSumLonnstilskudd()).isPositive(); + assertThat(avtale.getGjeldendeInnhold().getOtpSats()).isEqualTo(otpSats); + assertThat(avtale.getGjeldendeInnhold().getFeriepengesats()).isEqualTo(feriepengesats); + assertThat(avtale.getGjeldendeInnhold().getArbeidsgiveravgift()).isEqualTo(arbeidsgiveravgift); + assertThat(avtale.getGjeldendeInnhold().getManedslonn()).isEqualTo(manedslonn); + } + + @Test + public void endre_stillingsbeskrivelse_setter_riktige_felter() { + Avtale avtale = TestData.enLonnstilskuddAvtaleGodkjentAvVeileder(); + String stillingstittel = "Kokk"; + String arbeidsoppgaver = "Lage mat"; + Integer stillingStyrk08 = 1234; + Integer stillingKonseptId = 9999; + Integer stillingprosent = 90; + Integer antallDagerPerUke = 4; + var endreStillingsbeskrivelse = new EndreStillingsbeskrivelse(stillingstittel, arbeidsoppgaver, stillingStyrk08, stillingKonseptId, stillingprosent, antallDagerPerUke).toBuilder().build(); + + assertThat(avtale.getGjeldendeInnhold().getVersjon()).isEqualTo(1); + avtale.endreStillingsbeskrivelse(endreStillingsbeskrivelse, new NavIdent("Z123456")); + + assertThat(avtale.getGjeldendeInnhold().getVersjon()).isEqualTo(2); + assertThat(avtale.getGjeldendeInnhold().getStillingstittel()).isEqualTo(stillingstittel); + assertThat(avtale.getGjeldendeInnhold().getArbeidsoppgaver()).isEqualTo(arbeidsoppgaver); + assertThat(avtale.getGjeldendeInnhold().getStillingStyrk08()).isEqualTo(stillingStyrk08); + assertThat(avtale.getGjeldendeInnhold().getStillingKonseptId()).isEqualTo(stillingKonseptId); + assertThat(avtale.getGjeldendeInnhold().getStillingprosent()).isEqualTo(stillingprosent); + assertThat(avtale.getGjeldendeInnhold().getAntallDagerPerUke()).isEqualTo(antallDagerPerUke); + } + + @Test + public void endre_tilskuddsberegning_kun_inngått_avtale() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + assertFeilkode(Feilkode.KAN_IKKE_ENDRE_OKONOMI_IKKE_GODKJENT_AVTALE, () -> avtale.endreTilskuddsberegning(TestData.enEndreTilskuddsberegning(), TestData.enNavIdent())); + } + + @Test + public void endre_tilskuddsberegning_ugyldig_input() { + Avtale avtale = TestData.enLonnstilskuddAvtaleGodkjentAvVeileder(); + assertFeilkode(Feilkode.KAN_IKKE_ENDRE_OKONOMI_UGYLDIG_INPUT, () -> avtale.endreTilskuddsberegning(TestData.enEndreTilskuddsberegning().toBuilder().manedslonn(null).build(), TestData.enNavIdent())); + } + + @Test + public void forleng_setter_riktige_felter() { + Avtale avtale = TestData.enArbeidstreningAvtaleGodkjentAvVeileder(); + LocalDate nySluttDato = avtale.getGjeldendeInnhold().getSluttDato().plusMonths(1); + + assertThat(avtale.getGjeldendeInnhold().getVersjon()).isEqualTo(1); + avtale.forlengAvtale(nySluttDato, TestData.enNavIdent()); + + assertThat(avtale.getGjeldendeInnhold().getVersjon()).isEqualTo(2); + assertThat(avtale.erGodkjentAvVeileder()).isTrue(); + assertThat(avtale.getGjeldendeInnhold().getSluttDato()).isEqualTo(nySluttDato); + } + + @Test + public void forkort_setter_riktige_felter() { + Avtale avtale = TestData.enArbeidstreningAvtaleGodkjentAvVeileder(); + LocalDate nySluttDato = avtale.getGjeldendeInnhold().getSluttDato().minusDays(1); + + assertThat(avtale.getGjeldendeInnhold().getVersjon()).isEqualTo(1); + avtale.forkortAvtale(nySluttDato, "grunn", "", TestData.enNavIdent()); + + assertThat(avtale.getGjeldendeInnhold().getVersjon()).isEqualTo(2); + assertThat(avtale.erGodkjentAvVeileder()).isTrue(); + assertThat(avtale.getGjeldendeInnhold().getSluttDato()).isEqualTo(nySluttDato); + } + + @Test + public void forleng_kun_ved_inngått_avtale() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + assertFeilkode(Feilkode.KAN_IKKE_FORLENGE_IKKE_GODKJENT_AVTALE, () -> avtale.forlengAvtale(avtale.getGjeldendeInnhold().getStartDato().plusMonths(1).minusDays(1), TestData.enNavIdent())); + } + + @Test + public void forleng_kun_fremover() { + Avtale avtale = TestData.enLonnstilskuddAvtaleGodkjentAvVeileder(); + assertFeilkode(Feilkode.KAN_IKKE_FORLENGE_FEIL_SLUTTDATO, () -> avtale.forlengAvtale(avtale.getGjeldendeInnhold().getStartDato().minusDays(1), TestData.enNavIdent())); + } + + @Test + public void forleng_over_4_uker_sommerjobb() { + Now.fixedDate(LocalDate.of(2021, 6, 1)); + Avtale avtale = TestData.enSommerjobbAvtaleGodkjentAvVeileder(); + assertFeilkode(Feilkode.SOMMERJOBB_FOR_LANG_VARIGHET, () -> avtale.forlengAvtale(avtale.getGjeldendeInnhold().getStartDato().plusWeeks(4), TestData.enNavIdent())); + Now.resetClock(); + } + + @Test + public void forleng_24_mnd_midl_lts() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedSpesieltTilpassetInnsatsGodkjentAvVeileder(); + avtale.forlengAvtale(avtale.getGjeldendeInnhold().getStartDato().plusMonths(24).minusDays(1), TestData.enNavIdent()); + } + + @Test + public void forleng_over_24_mnd_midl_lts() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedSpesieltTilpassetInnsatsGodkjentAvVeileder(); + assertFeilkode(Feilkode.VARIGHET_FOR_LANG_MIDLERTIDIG_LONNSTILSKUDD_24_MND, () -> avtale.forlengAvtale(avtale.getGjeldendeInnhold().getStartDato().plusMonths(24), TestData.enNavIdent())); + } + + @Test + public void forlengAvale_med_startdato_tilbake_i_tid() { + Now.fixedDate(LocalDate.of(2021, 11, 25)); + LocalDate startDato = LocalDate.of(2021, 11, 30); + LocalDate sluttDato = LocalDate.of(2022, 11, 25); + Avtale avtale = TestData.enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(startDato, sluttDato); + Now.resetClock(); + LocalDate nySluttDato = avtale.getGjeldendeInnhold().getSluttDato().plusMonths(1); + avtale.forlengAvtale(nySluttDato, TestData.enNavIdent()); + assertThat(avtale.getGjeldendeInnhold().getSluttDato()).isEqualTo(nySluttDato); + } + + @Test + public void forlengAvtale_forleng_etter_reduksjon_edge_case_alle_perioder_godkjent() { + Now.fixedDate(LocalDate.of(2023, 03, 15)); + LocalDate startDato = LocalDate.of(2022, 03, 01); + LocalDate sluttDato = LocalDate.of(2023, 02, 28); + Avtale avtale = TestData.enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(startDato, sluttDato); + // Alle perioder er godkjent + avtale.getTilskuddPeriode().forEach(t -> { + t.godkjenn(TestData.enNavIdent2(), "1234"); + }); + LocalDate nySluttDato = sluttDato.plusMonths(6); + avtale.forlengAvtale(nySluttDato, TestData.enNavIdent()); + assertThat(avtale.getGjeldendeInnhold().getSluttDato()).isEqualTo(nySluttDato); + Now.resetClock(); + } + + @Test + public void forlengAvtale_forleng_etter_reduksjon_edge_case_alle_perioder_ubehandlet() { + Now.fixedDate(LocalDate.of(2023, 03, 15)); + LocalDate startDato = LocalDate.of(2022, 03, 01); + LocalDate sluttDato = LocalDate.of(2023, 02, 28); + Avtale avtale = TestData.enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(startDato, sluttDato); + LocalDate nySluttDato = sluttDato.plusMonths(6); + avtale.forlengAvtale(nySluttDato, TestData.enNavIdent()); + assertThat(avtale.getGjeldendeInnhold().getSluttDato()).isEqualTo(nySluttDato); + Now.resetClock(); + } + + + @Test + public void sommerjobb_må_være_godkjent_av_beslutter() { + Now.fixedDate(LocalDate.of(2021, 6, 1)); + Avtale avtale = TestData.enSommerjobbAvtaleGodkjentAvVeileder(); + assertThat(avtale.statusSomEnum()).isEqualTo(Status.MANGLER_GODKJENNING); + assertThat(avtale.getGjeldendeInnhold().getAvtaleInngått()).isNull(); + avtale.godkjennTilskuddsperiode(new NavIdent("B999999"), TestData.ENHET_OPPFØLGING.getVerdi()); + assertThat(avtale.statusSomEnum()).isEqualTo(Status.GJENNOMFØRES); + assertThat(avtale.getGjeldendeInnhold().getAvtaleInngått()).isNotNull(); + Now.resetClock(); + } + + @Test + public void ikke_lonnstilskudd_skal_inngås_ved_veileders_godkjenning() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + Deltaker deltaker = TestData.enDeltaker(avtale); + + // Gi veileder tilgang til deltaker + TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + + TilskuddsperiodeConfig tilskuddsperiodeConfig = new TilskuddsperiodeConfig(); + + Veileder veileder = new Veileder( + avtale.getVeilederNavIdent(), + tilgangskontrollService, + mock(PersondataService.class), + mock(Norg2Client.class), + Set.of(new NavEnhet("4802", "Trysil")), + mock(SlettemerkeProperties.class), + false, + mock(VeilarbArenaClient.class) + ); + when(tilgangskontrollService.harSkrivetilgangTilKandidat( + eq(veileder), + any(Fnr.class) + )).thenReturn(true); + veileder.endreAvtale( + Instant.now(), + TestData.endringPåAlleArbeidstreningFelter(), + avtale, + tilskuddsperiodeConfig.getTiltakstyper() + ); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + deltaker.godkjennAvtale(Instant.now(), avtale); + + veileder.godkjennAvtale(Instant.now(), avtale); + assertThat(avtale.erAvtaleInngått()).isTrue(); + } + + @Test + public void lonnstilskudd_må_være_godkjent_av_beslutter() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + assertThat(avtale.statusSomEnum()).isEqualTo(Status.MANGLER_GODKJENNING); + Deltaker deltaker = TestData.enDeltaker(avtale); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + + // Gi veileder tilgang til deltaker + TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + + Veileder veileder = new Veileder( + avtale.getVeilederNavIdent(), + tilgangskontrollService, + mock(PersondataService.class), + mock(Norg2Client.class), + Set.of(new NavEnhet("4802", "Trysil")), + mock(SlettemerkeProperties.class), + false, + mock(VeilarbArenaClient.class) + ); + + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), any(Fnr.class))).thenReturn(true); + + deltaker.godkjennAvtale(Instant.now(), avtale); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + veileder.godkjennAvtale(Instant.now(), avtale); + + assertThat(avtale.getGjeldendeInnhold().getAvtaleInngått()).isNull(); + avtale.godkjennTilskuddsperiode(new NavIdent("B999999"), TestData.ENHET_OPPFØLGING.getVerdi()); + assertThat(avtale.getGjeldendeInnhold().getAvtaleInngått()).isNotNull(); + } + + @Test + public void lonnstilskudd_skal_generere_tilskuddsperioder() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddsjobbAvtale(); + avtale.getGjeldendeInnhold().setLonnstilskuddProsent(60); + assertThat(avtale.getTilskuddPeriode()).isEmpty(); + + Veileder veileder = TestData.enVeileder(avtale); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + veileder.endreAvtale(Instant.now(), endreAvtale, avtale, EnumSet.of(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD)); + assertThat(avtale.getTilskuddPeriode()).isNotEmpty(); + } + + @Test + public void godkjenn_tilskuddsperiode_feil_enhet() { + Now.fixedDate(LocalDate.of(2021, 6, 1)); + Avtale avtale = TestData.enSommerjobbAvtaleGodkjentAvVeileder(); + NavIdent beslutter = new NavIdent("B999999"); + assertFeilkode(Feilkode.TILSKUDDSPERIODE_ENHET_FIRE_SIFFER, () -> avtale.godkjennTilskuddsperiode(beslutter, " 4444")); + assertFeilkode(Feilkode.TILSKUDDSPERIODE_ENHET_FIRE_SIFFER, () -> avtale.godkjennTilskuddsperiode(beslutter, "444")); + assertFeilkode(Feilkode.TILSKUDDSPERIODE_ENHET_FIRE_SIFFER, () -> avtale.godkjennTilskuddsperiode(beslutter, "44455")); + assertFeilkode(Feilkode.TILSKUDDSPERIODE_ENHET_FIRE_SIFFER, () -> avtale.godkjennTilskuddsperiode(beslutter, "")); + assertFeilkode(Feilkode.TILSKUDDSPERIODE_ENHET_FIRE_SIFFER, () -> avtale.godkjennTilskuddsperiode(beslutter, null)); + avtale.godkjennTilskuddsperiode(beslutter, "4444"); + Now.resetClock(); + } + + @Test + public void godkjenn_tilskuddsperiode_samme_veileder_og_beslutter() { + Now.fixedDate(LocalDate.of(2021, 6, 1)); + Avtale avtale = TestData.enSommerjobbAvtaleGodkjentAvVeileder(); + + // Kan ikke godkjenne når avtalen er tildelt seg selv + assertFeilkode(Feilkode.TILSKUDDSPERIODE_IKKE_GODKJENNE_EGNE, () -> avtale.godkjennTilskuddsperiode(avtale.getGjeldendeInnhold().getGodkjentAvNavIdent(), "4444")); + + // Kan heller ikke godkjenne når avtalen er tildelt en annen + avtale.overtaAvtale(new NavIdent("P887766")); + assertFeilkode(Feilkode.TILSKUDDSPERIODE_IKKE_GODKJENNE_EGNE, () -> avtale.godkjennTilskuddsperiode(avtale.getGjeldendeInnhold().getGodkjentAvNavIdent(), "4444")); + Now.resetClock(); + } + + @Test + public void forleng_og_forkort_skal_redusere_prosent() { + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), TestData.enNavIdent()); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(Now.localDate()); + endreAvtale.setSluttDato(Now.localDate().plusMonths(12).minusDays(1)); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtale.godkjennForDeltaker(TestData.etFodselsnummer()); + avtale.godkjennForArbeidsgiver(TestData.etFodselsnummer()); + avtale.godkjennForVeileder(TestData.enNavIdent()); + + avtale.forlengAvtale(Now.localDate().plusMonths(12), TestData.enNavIdent()); + assertThat(avtale.getGjeldendeInnhold().getDatoForRedusertProsent()).isEqualTo(Now.localDate().plusMonths(12)); + assertThat(avtale.getGjeldendeInnhold().getSumLønnstilskuddRedusert()).isNotNull(); + + avtale.forkortAvtale(Now.localDate().plusMonths(12).minusDays(1), "grunn", "", TestData.enNavIdent()); + assertThat(avtale.getGjeldendeInnhold().getDatoForRedusertProsent()).isNull(); + assertThat(avtale.getGjeldendeInnhold().getSumLønnstilskuddRedusert()).isNull(); + } + + @Test + public void godkjenn_avtale_skal_ikke_gå_hvis_over_72() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + Fnr fnr = new Fnr("07075014443"); + avtale.setDeltakerFnr(fnr); + Deltaker deltaker = TestData.enDeltaker(avtale); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + Veileder veileder = TestData.enVeileder(avtale); + + deltaker.godkjennAvtale(Instant.now(), avtale); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + + assertFeilkode(Feilkode.DELTAKER_72_AAR, () -> veileder.godkjennAvtale(Instant.now(), avtale)); + } + + // Man skal ikke kunne forkorte en avtale sånn at man får en sluttdato som er før en godkjent/utbetalt tilskuddsperide (refusjon) + @Test + public void kan_ikke_forkorte_forbi_utbetalt_tilskuddsperiode() { + Now.fixedDate(LocalDate.of(2023,1,1)); + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + Deltaker deltaker = TestData.enDeltaker(avtale); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + // Gi veileder tilgang til deltaker + TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + + Veileder veileder = new Veileder( + avtale.getVeilederNavIdent(), + tilgangskontrollService, + mock(PersondataService.class), + mock(Norg2Client.class), + Set.of(new NavEnhet("4802", "Trysil")), + mock(SlettemerkeProperties.class), + false, + mock(VeilarbArenaClient.class) + ); + + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), any(Fnr.class))).thenReturn(true); + deltaker.godkjennAvtale(Instant.now(), avtale); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + veileder.godkjennAvtale(Instant.now(), avtale); + + avtale.godkjennTilskuddsperiode(new NavIdent("B999999"), TestData.ENHET_OPPFØLGING.getVerdi()); + avtale.godkjennTilskuddsperiode(new NavIdent("B999999"), TestData.ENHET_OPPFØLGING.getVerdi()); + avtale.tilskuddsperiode(0).setRefusjonStatus(RefusjonStatus.UTBETALT); + avtale.tilskuddsperiode(1).setRefusjonStatus(RefusjonStatus.UTBETALT); + + avtale.forkortAvtale(LocalDate.of(2023,2, 28), "Grunn", "Grunn2", veileder.getNavIdent()); + assertThat(avtale.getGjeldendeInnhold().getSluttDato()).isEqualTo(LocalDate.of(2023,2,28)); + assertFeilkode(Feilkode.KAN_IKKE_FORKORTE_FOR_UTBETALT_TILSKUDDSPERIODE, () -> avtale.forkortAvtale(LocalDate.of(2023,2,27), "Grunn", "Grunn2", veileder.getNavIdent())); + + assertThat(avtale.getGjeldendeTilskuddsperiodestatus()).isEqualTo(TilskuddPeriodeStatus.GODKJENT); + Now.resetClock(); + } + + // Man skal ikke kunne forkorte en avtale sånn at man får en sluttdato som er før en godkjent/utbetalt tilskuddsperide (refusjon) + @Test + public void kan_ikke_forkorte_forbi_utbetalt_tilskuddsperiode_med_split_mitt_i_en_måned() { + Now.fixedDate(LocalDate.of(2023,1,15)); + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + Deltaker deltaker = TestData.enDeltaker(avtale); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + // Gi veileder tilgang til deltaker + TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + + Veileder veileder = new Veileder( + avtale.getVeilederNavIdent(), + tilgangskontrollService, + mock(PersondataService.class), + mock(Norg2Client.class), + Set.of(new NavEnhet("4802", "Trysil")), + mock(SlettemerkeProperties.class), + false, + mock(VeilarbArenaClient.class) + ); + + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), any(Fnr.class))).thenReturn(true); + deltaker.godkjennAvtale(Instant.now(), avtale); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + veileder.godkjennAvtale(Instant.now(), avtale); + + avtale.tilskuddsperiode(0).setRefusjonStatus(RefusjonStatus.UTBETALT); + avtale.tilskuddsperiode(1).setRefusjonStatus(RefusjonStatus.UTBETALT); + avtale.tilskuddsperiode(2).setRefusjonStatus(RefusjonStatus.UTBETALT); + avtale.tilskuddsperiode(3).setRefusjonStatus(RefusjonStatus.UTBETALT); + avtale.tilskuddsperiode(4).setRefusjonStatus(RefusjonStatus.UTBETALT); + avtale.tilskuddsperiode(5).setRefusjonStatus(RefusjonStatus.UTBETALT); + avtale.tilskuddsperiode(6).setRefusjonStatus(RefusjonStatus.UTBETALT); + avtale.godkjennTilskuddsperiode(new NavIdent("B999999"), TestData.ENHET_OPPFØLGING.getVerdi()); + avtale.getTilskuddPeriode().forEach(tilskuddPeriode -> { + System.out.print(tilskuddPeriode.getRefusjonStatus() + " "); + System.out.println(tilskuddPeriode.getStartDato()); + }); + avtale.forkortAvtale(LocalDate.of(2023,7, 14), "Grunn", "Grunn2", veileder.getNavIdent()); + assertThat(avtale.getGjeldendeInnhold().getSluttDato()).isEqualTo(LocalDate.of(2023,7,14)); + assertFeilkode(Feilkode.KAN_IKKE_FORKORTE_FOR_UTBETALT_TILSKUDDSPERIODE, () -> avtale.forkortAvtale(LocalDate.of(2023,7,13), "Grunn", "Grunn2", veileder.getNavIdent())); + Now.resetClock(); + } + + //40% + @Test + public void beregning_av_lønnstilskudd_ut_ifra_kvalifiseringsgruppe_SITUASJONSBESTEMT_INNSATS_og_MIDLERTIDIG_LONNSTILSKUDD() { + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), TestData.enNavIdent()); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS); + avtale.setGodkjentForEtterregistrering(true); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + avtale.endreAvtale(Instant.now(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + assertThat(avtale.getGjeldendeInnhold().getLonnstilskuddProsent()).isEqualTo(40); + } + + //60% + @Test + public void beregning_av_lønnstilskudd_ut_ifra_kvalifiseringsgruppe_SPESIELT_TILPASSET_INNSATS_og_MIDLERTIDIG_LONNSTILSKUDD() { + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), TestData.enNavIdent()); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SPESIELT_TILPASSET_INNSATS); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + avtale.endreAvtale(Instant.now(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + assertThat(avtale.getGjeldendeInnhold().getLonnstilskuddProsent()).isEqualTo(60); + } + + //50% + @Test + @Disabled("Utleding av lønnstilskuddprosent er skrudd av på sommerjobb inntil videre for å tilltate etterregistrering") + public void beregning_av_lønnstilskudd_ut_ifra_kvalifiseringsgruppe_SITUASJONSBESTEMT_INNSATS_og_SOMMERJOBB() { + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.SOMMERJOBB), TestData.enNavIdent()); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(LocalDate.of(2021, 6, 1)); + endreAvtale.setSluttDato(LocalDate.of(2021, 6, 1).plusWeeks(4).minusDays(1)); + avtale.endreAvtale(Instant.now(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + assertThat(avtale.getGjeldendeInnhold().getLonnstilskuddProsent()).isEqualTo(50); + } + + //75% + @Test + @Disabled("Utleding av lønnstilskuddprosent er skrudd av på sommerjobb inntil videre for å tilltate etterregistrering") + public void beregning_av_lønnstilskudd_ut_ifra_kvalifiseringsgruppe_SPESIELT_TILPASSET_INNSATS_og_SOMMERJOBB() { + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.SOMMERJOBB), TestData.enNavIdent()); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SPESIELT_TILPASSET_INNSATS); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(LocalDate.of(2021, 6, 1)); + endreAvtale.setSluttDato(LocalDate.of(2021, 6, 1).plusWeeks(4).minusDays(1)); + avtale.endreAvtale(Instant.now(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + assertThat(avtale.getGjeldendeInnhold().getLonnstilskuddProsent()).isEqualTo(75); + } + + @Test + public void godkjent_for_etterregistrering_starter_som_false() { + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), TestData.enNavIdent()); + assertThat(avtale.isGodkjentForEtterregistrering()).isFalse(); + } + + @Test + public void avtale_setter_godkjent_for_etterregistrering() { + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), TestData.enNavIdent()); + avtale.togglegodkjennEtterregistrering(TestData.enNavIdent()); + assertThat(avtale.isGodkjentForEtterregistrering()).isTrue(); + } + + @Test + public void avtale_kan_etterregistreres() { + Now.fixedDate(LocalDate.of(2021, 12, 20)); + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), TestData.enNavIdent()); + avtale.togglegodkjennEtterregistrering(TestData.enNavIdent()); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(LocalDate.of(2021, 12, 12)); + endreAvtale.setSluttDato(LocalDate.of(2021, 12, 1).plusYears(1)); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + assertThat(avtale.getGjeldendeInnhold().getStartDato()).isEqualTo(LocalDate.of(2021, 12, 12)); + Now.resetClock(); + + } + + @Test + public void avtale_FORTIDLIG_STARTDATO() { + Now.fixedDate(LocalDate.of(2021, 12, 20)); + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), TestData.enNavIdent()); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(LocalDate.of(2021, 12, 12)); + endreAvtale.setSluttDato(LocalDate.of(2021, 12, 1).plusYears(1)); + + assertFeilkode(Feilkode.FORTIDLIG_STARTDATO, () -> avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder)); + Now.resetClock(); + } + + @Test + public void avtale_feilkode_KAN_IKKE_MERKES_FOR_ETTERREGISTREING_AVTALE_INNGATT() { + Avtale avtale = TestData.enAvtaleMedAltUtfyltGodkjentAvVeileder(); + assertFeilkode(Feilkode.KAN_IKKE_MERKES_FOR_ETTERREGISTRERING_AVTALE_GODKJENT, () -> avtale.togglegodkjennEtterregistrering(TestData.enNavIdent())); + } + + @Test + public void avtale_skal_kunne_etterregistrere_med_arbeidsgiver_godkjenning() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.godkjennForArbeidsgiver(TestData.enIdentifikator()); + avtale.togglegodkjennEtterregistrering(TestData.enNavIdent()); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtalepartTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtalepartTest.java new file mode 100644 index 000000000..3e12cc90a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/AvtalepartTest.java @@ -0,0 +1,157 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import static no.nav.tag.tiltaksgjennomforing.AssertFeilkode.assertFeilkode; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.EnumSet; + +import no.nav.tag.tiltaksgjennomforing.exceptions.ArbeidsgiverSkalGodkjenneFørVeilederException; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.KanIkkeEndreException; +import no.nav.tag.tiltaksgjennomforing.exceptions.SamtidigeEndringerException; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.Test; + +public class AvtalepartTest { + + @Test + public void endreAvtale__skal_feile_for_deltaker() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Deltaker deltaker = TestData.enDeltaker(avtale); + assertThatThrownBy(() -> deltaker.endreAvtale(avtale.getSistEndret(), TestData.ingenEndring(), avtale, EnumSet.of(avtale.getTiltakstype()))).isInstanceOf(KanIkkeEndreException.class); + } + + @Test + public void godkjennForVeilederOgDeltaker__skal_feile_hvis_ag_ikke_har_godkjent() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Veileder veileder = TestData.enVeileder(avtale); + GodkjentPaVegneGrunn godkjentPaVegneGrunn = TestData.enGodkjentPaVegneGrunn(); + assertThatThrownBy(() -> veileder.godkjennForVeilederOgDeltaker(godkjentPaVegneGrunn, avtale)).isInstanceOf(ArbeidsgiverSkalGodkjenneFørVeilederException.class); + } + + @Test + public void godkjennForVeileder__skal_feile_hvis_mentor_ikke_har_signert() { + Avtale avtale = TestData.enMentorAvtaleUsignert(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + arbeidsgiver.godkjennAvtale(avtale.getSistEndret().plusMillis(60000), avtale); + Deltaker deltaker = TestData.enDeltaker(avtale); + deltaker.godkjennAvtale(avtale.getSistEndret().plusMillis(60000), avtale); + Veileder veileder = TestData.enVeileder(avtale); + assertFeilkode(Feilkode.MENTOR_MÅ_SIGNERE_TAUSHETSERKLÆRING,() -> veileder.godkjennAvtale(avtale.getSistEndret().plusMillis(60000), avtale)); + } + + @Test + public void godkjennForVeilederOgDeltaker__skal_feile_hvis_mentor_ikke_har_signert() { + Avtale avtale = TestData.enMentorAvtaleUsignert(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + arbeidsgiver.godkjennAvtale(avtale.getSistEndret().plusMillis(60000), avtale); + Veileder veileder = TestData.enVeileder(avtale); + assertFeilkode(Feilkode.MENTOR_MÅ_SIGNERE_TAUSHETSERKLÆRING,() -> veileder.godkjennForVeilederOgDeltaker(new GodkjentPaVegneGrunn(), avtale)); + } + + @Test + public void godkjennForVeilederOgDeltaker__skal_fungere_for_veileder() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + arbeidsgiver.godkjennForAvtalepart(avtale); + Veileder veileder = TestData.enVeileder(avtale); + GodkjentPaVegneGrunn godkjentPaVegneGrunn = TestData.enGodkjentPaVegneGrunn(); + veileder.godkjennForVeilederOgDeltaker(godkjentPaVegneGrunn, avtale); + assertThat(avtale.erGodkjentAvDeltaker()).isTrue(); + assertThat(avtale.erGodkjentAvVeileder()).isTrue(); + assertThat(avtale.getGjeldendeInnhold().isGodkjentPaVegneAv()).isTrue(); + } + + @Test + public void endreAvtale__skal_fungere_for_arbeidsgiver() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + arbeidsgiver.endreAvtale(Now.instant(), TestData.ingenEndring(), avtale, EnumSet.of(avtale.getTiltakstype())); + } + + @Test + public void endreAvtale__skal_fungere_for_veileder() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Veileder veileder = TestData.enVeileder(avtale); + veileder.endreAvtale(Now.instant(), TestData.ingenEndring(), avtale, EnumSet.of(avtale.getTiltakstype())); + } + + @Test + public void godkjennForAvtalepart__skal_ikke_fungere_hvis_versjon_er_feil() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Deltaker deltaker = TestData.enDeltaker(avtale); + assertThatThrownBy(() -> deltaker.godkjennAvtale(avtale.getSistEndret().minusMillis(1), avtale)).isInstanceOf(SamtidigeEndringerException.class); + } + + @Test + public void godkjennForAvtalepart__skal_fungere_for_deltaker() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Deltaker deltaker = TestData.enDeltaker(avtale); + deltaker.godkjennAvtale(avtale.getSistEndret(), avtale); + assertThat(avtale.erGodkjentAvDeltaker()).isTrue(); + assertThat(avtale.erGodkjentAvArbeidsgiver()).isFalse(); + assertThat(avtale.erGodkjentAvVeileder()).isFalse(); + } + + @Test + public void godkjennForAvtalepart__skal_fungere_for_arbeidsgiver() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + arbeidsgiver.godkjennAvtale(avtale.getSistEndret(), avtale); + assertThat(avtale.erGodkjentAvArbeidsgiver()).isTrue(); + assertThat(avtale.erGodkjentAvVeileder()).isFalse(); + assertThat(avtale.erGodkjentAvDeltaker()).isFalse(); + } + + @Test + public void godkjennForAvtalepart__skal_fungere_for_veileder() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + Veileder veileder = TestData.enVeileder(avtale); + veileder.godkjennAvtale(avtale.getSistEndret(), avtale); + assertThat(avtale.erGodkjentAvArbeidsgiver()).isTrue(); + } + + @Test + public void opphevGodkjenninger__veileder_skal_kunne_trekke_tilbake_egen_godkjenning() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + Veileder veileder = TestData.enVeileder(avtale); + veileder.opphevGodkjenninger(avtale); + assertThat(avtale.erGodkjentAvVeileder()).isFalse(); + } + + @Test + public void opphevGodkjenninger__feiler_hvis_alle_har_allerede_godkjent_og_avtale_er_inngått() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + arbeidsgiver.godkjennForAvtalepart(avtale); + Veileder veileder = TestData.enVeileder(avtale); + GodkjentPaVegneGrunn godkjentPaVegneGrunn = TestData.enGodkjentPaVegneGrunn(); + veileder.godkjennForVeilederOgDeltaker(godkjentPaVegneGrunn, avtale); + assertThat(avtale.erGodkjentAvDeltaker()).isTrue(); + assertThat(avtale.erGodkjentAvVeileder()).isTrue(); + assertThat(avtale.erGodkjentAvArbeidsgiver()).isTrue(); + assertThat(avtale.getGjeldendeInnhold().isGodkjentPaVegneAv()).isTrue(); + assertFeilkode(Feilkode.KAN_IKKE_OPPHEVE_GODKJENNINGER_VED_INNGAATT_AVTALE, () -> veileder.opphevGodkjenninger(avtale)); + } + + @Test + public void opphevGodkjenninger__feiler_hvis_ingen_har_godkjent() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Veileder veileder = TestData.enVeileder(avtale); + assertFeilkode(Feilkode.KAN_IKKE_OPPHEVE, () -> veileder.opphevGodkjenninger(avtale)); + } + + @Test + public void opphevGodkjenninger__kan_ikke_utfores_flere_ganger_etter_hverandre() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + arbeidsgiver.godkjennForAvtalepart(avtale); + + arbeidsgiver.opphevGodkjenninger(avtale); + assertFeilkode(Feilkode.KAN_IKKE_OPPHEVE, () -> arbeidsgiver.opphevGodkjenninger(avtale)); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/BeregningLonnstilskuddTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/BeregningLonnstilskuddTest.java new file mode 100644 index 000000000..082bc6aca --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/BeregningLonnstilskuddTest.java @@ -0,0 +1,161 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import static no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD; +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; + +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BeregningLonnstilskuddTest { + + private AvtaleInnhold avtaleInnhold; + private AvtaleInnholdStrategy strategy; + + @BeforeEach + public void setUp() { + avtaleInnhold = new AvtaleInnhold(); + strategy = AvtaleInnholdStrategyFactory.create(avtaleInnhold, MIDLERTIDIG_LONNSTILSKUDD); + } + + @Test + public void test_regn_ut_sumLonntilskudd_til_0_nar_lonnstilskudd_prosent_er_null() { + // GIVEN + EndreAvtale endreAvtale = new EndreAvtale(); + endreAvtale.setManedslonn(20000); + endreAvtale.setFeriepengesats(new BigDecimal(0.102)); + endreAvtale.setArbeidsgiveravgift(new BigDecimal(0.064)); + + // WHEN + strategy.endre(endreAvtale); + + // THEN + assertThat(avtaleInnhold.getSumLonnstilskudd()).isNull(); + } + + @Test + public void test_regn_ut_sumLonntilskudd_nå_rotp_ikke_er_satt() { + // GIVEN + EndreAvtale endreAvtale = new EndreAvtale(); + endreAvtale.setManedslonn(20000); + endreAvtale.setLonnstilskuddProsent(60); + endreAvtale.setOtpSats(0.02); + endreAvtale.setFeriepengesats(new BigDecimal(0.12)); + endreAvtale.setArbeidsgiveravgift(new BigDecimal(0.141)); + + // WHEN + strategy.endreAvtaleInnholdMedKvalifiseringsgruppe(endreAvtale, Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + + // THEN + assertThat(avtaleInnhold.getSumLonnstilskudd()).isEqualTo(15642); + } + + @Test + public void test_regn_ut_sumLonntilskudd() { + // GIVEN + EndreAvtale endreAvtale = new EndreAvtale(); + endreAvtale.setManedslonn(20000); + endreAvtale.setOtpSats(0.02); + endreAvtale.setLonnstilskuddProsent(60); + endreAvtale.setFeriepengesats(new BigDecimal(0.12)); + endreAvtale.setArbeidsgiveravgift(new BigDecimal(0.141)); + + // WHEN + strategy.endreAvtaleInnholdMedKvalifiseringsgruppe(endreAvtale, Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + + // THEN + assertThat(avtaleInnhold.getSumLonnstilskudd()).isEqualTo(15642); + } + + @Test + public void test_regn_ut_arbeidsgiveravgiftbelop() { + // GIVEN + EndreAvtale endreAvtale = new EndreAvtale(); + endreAvtale.setManedslonn(20000); + endreAvtale.setOtpSats(0.02); + endreAvtale.setFeriepengesats(new BigDecimal(0.12)); + endreAvtale.setArbeidsgiveravgift(new BigDecimal(0.141)); + + // WHEN + strategy.endre(endreAvtale); + + // THEN + assertThat(avtaleInnhold.getArbeidsgiveravgiftBelop()).isEqualTo(3222); + } + + @Test + public void test_regn_ut_oblig_tjenstepensjon() { + // GIVEN + EndreAvtale endreAvtale = new EndreAvtale(); + endreAvtale.setManedslonn(20000); + endreAvtale.setOtpSats(0.02); + endreAvtale.setFeriepengesats(new BigDecimal(0.12)); + + // WHEN + strategy.endre(endreAvtale); + + // THEN + assertThat(avtaleInnhold.getOtpBelop()).isEqualTo(448); + } + + @Test + public void test_regn_ut_oblig_tjenstepensjon_til_null_om_Feriepengersats_er_null() { + // GIVEN + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setFeriepengesats(null); + + // WHEN + strategy.endre(endreAvtale); + + // THEN + assertThat(avtaleInnhold.getOtpBelop()).isNull(); + assertThat(avtaleInnhold.getFeriepengerBelop()).isNull(); + } + + @Test + public void test_regn_ut_oblig_tjenstepensjon_til_null_om_manedslonn_er_null() { + // GIVEN + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setManedslonn(null); + + // WHEN + strategy.endre(endreAvtale); + + // THEN + assertThat(avtaleInnhold.getOtpBelop()).isNull(); + assertThat(avtaleInnhold.getFeriepengerBelop()).isNull(); + } + + @Test + public void test_regn_ut_feriepenger_belop() { + // GIVEN + EndreAvtale endreAvtale = new EndreAvtale(); + endreAvtale.setManedslonn(20000); + endreAvtale.setFeriepengesats(new BigDecimal(0.12)); + + // WHEN + strategy.endre(endreAvtale); + + // THEN + assertThat(avtaleInnhold.getFeriepengerBelop()).isEqualTo(2400); + } + + @Test + public void test_regn_ut_lonn_ved_100_prosent() { + // GIVEN + EndreAvtale endreAvtale = new EndreAvtale(); + endreAvtale.setStillingprosent(50); + endreAvtale.setManedslonn(10000); + endreAvtale.setOtpSats(0.02); + endreAvtale.setFeriepengesats(new BigDecimal(0.125)); + endreAvtale.setArbeidsgiveravgift(new BigDecimal(0.0)); + // WHEN + strategy.endre(endreAvtale); + + // THEN + assertThat(avtaleInnhold.getManedslonn100pst()).isEqualTo(22950); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/BeslutterTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/BeslutterTest.java new file mode 100644 index 000000000..b677149ad --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/BeslutterTest.java @@ -0,0 +1,118 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import static no.nav.tag.tiltaksgjennomforing.AssertFeilkode.assertFeilkode; +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.avtalerMedTilskuddsperioder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.*; + +import no.nav.tag.tiltaksgjennomforing.autorisasjon.SlettemerkeProperties; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.TilgangskontrollService; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2Client; +import no.nav.tag.tiltaksgjennomforing.enhet.VeilarbArenaClient; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.AxsysService; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.NavEnhet; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; + +import org.junit.jupiter.api.Test; + +class BeslutterTest { + + private TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + private AvtaleRepository avtaleRepository = mock(AvtaleRepository.class); + private AxsysService axsysService = mock(AxsysService.class); + private Norg2Client norg2Client = mock(Norg2Client.class); + + /*@Test + public void hentAlleAvtalerMedMuligTilgang__hent_ingen_GODKJENTE_når_avtaler_har_gjeldende_tilskuddsperiodestatus_ubehandlet() { + + // GITT + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + TilskuddPeriode tilskuddPeriode = new TilskuddPeriode(); + tilskuddPeriode.setStatus(TilskuddPeriodeStatus.GODKJENT); + tilskuddPeriode.setBeløp(1200); + tilskuddPeriode.setAvtale(avtale); + avtale.setTilskuddPeriode(new TreeSet<>(List.of(tilskuddPeriode))); + Beslutter beslutter = new Beslutter(new NavIdent("J987654"), tilgangskontrollService, axsysService, norg2Client); + Integer plussDato = ((int) ChronoUnit.DAYS.between(LocalDate.now(), LocalDate.now().plusMonths(3))); + AvtalePredicate avtalePredicate = new AvtalePredicate(); + avtalePredicate.setTilskuddPeriodeStatus(TilskuddPeriodeStatus.UBEHANDLET); + Pageable pageable = PageRequest.of(0, 100); + + Page avtalerMedTilskuddsperioder = avtaleRepository + .finnGodkjenteAvtalerMedTilskuddsperiodestatusOgNavEnheterUbehandletMinimal(TilskuddPeriodeStatus.UBEHANDLET.name(), Set.of(TestData.ENHET_OPPFØLGING.getVerdi(), plussDato, Set.of(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD.name(), "", pageable); + + + // NÅR + when(axsysService.hentEnheterNavAnsattHarTilgangTil(beslutter.getIdentifikator())).thenReturn(List.of(TestData.ENHET_OPPFØLGING)); + when(avtaleRepository + .finnGodkjenteAvtalerMedTilskuddsperiodestatusOgNavEnheterUbehandletMinimal(TilskuddPeriodeStatus.UBEHANDLET.name(), Set.of(TestData.ENHET_OPPFØLGING.getVerdi()), plussDato, Set.of(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD.name()), "", pageable)) + .thenReturn(new PageImpl(List.of(avtale))); + List avtaler = beslutter.hentAlleAvtalerMedMuligTilgang(avtaleRepository, avtalePredicate); + + assertThat(avtaler).isEmpty(); + }*/ + + @Test + public void toggle_godkjent_for_etterregistrering() { + + //GITT + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), TestData.enNavIdent()); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(LocalDate.of(2021, 12, 12)); + endreAvtale.setSluttDato(LocalDate.of(2021, 12, 1).plusYears(1)); + Beslutter beslutter = TestData.enBeslutter(avtale); + + // NÅR + beslutter.setOmAvtalenKanEtterregistreres(avtale); + assertThat(avtale.isGodkjentForEtterregistrering()).isTrue(); + + beslutter.setOmAvtalenKanEtterregistreres(avtale); + assertThat(avtale.isGodkjentForEtterregistrering()).isFalse(); + } + + @Test + public void kan_ikke_godkjenne_periode_på_enhet_som_ikke_finnes() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + + // Gi veileder tilgang til deltaker + TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + + Veileder veileder = new Veileder( + avtale.getVeilederNavIdent(), + tilgangskontrollService, + mock(PersondataService.class), + mock(Norg2Client.class), + Set.of(new NavEnhet("4802", "Trysil")), + mock(SlettemerkeProperties.class), + false, + mock(VeilarbArenaClient.class)); + + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), any(Fnr.class))) + .thenReturn(true); + + avtale.endreAvtale( + Instant.now(), + TestData.endringPåAlleLønnstilskuddFelter(), + Avtalerolle.VEILEDER, + avtalerMedTilskuddsperioder + ); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + veileder.godkjennForVeilederOgDeltaker(TestData.enGodkjentPaVegneGrunn(), avtale); + assertThat(avtale.erAvtaleInngått()).isFalse(); + Beslutter beslutter = TestData.enBeslutter(avtale); + assertFeilkode( + Feilkode.ENHET_FINNES_IKKE, + () -> beslutter.godkjennTilskuddsperiode(avtale, "9999") + ); + assertThat(avtale.erAvtaleInngått()).isFalse(); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/DeltakerAlleredePaTiltakTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/DeltakerAlleredePaTiltakTest.java new file mode 100644 index 000000000..5d6e91a00 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/DeltakerAlleredePaTiltakTest.java @@ -0,0 +1,270 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.datadeling.AvtaleMeldingEntitetRepository; +import no.nav.tag.tiltaksgjennomforing.datavarehus.DvhMeldingEntitetRepository; +import no.nav.tag.tiltaksgjennomforing.varsel.SmsRepository; +import no.nav.tag.tiltaksgjennomforing.varsel.VarselRepository; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.ArbeidsgiverNotifikasjonRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import java.sql.Date; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@SpringBootTest +@ActiveProfiles({Miljø.LOCAL}) +@DirtiesContext +public class DeltakerAlleredePaTiltakTest { + + @Autowired + private AvtaleRepository avtaleRepository; + + @Autowired + VarselRepository varselRepository; + + @Autowired + SmsRepository smsRepository; + + @Autowired + AvtaleInnholdRepository avtaleInnholdRepository; + + @Autowired + ArbeidsgiverNotifikasjonRepository arbeidsgiverNotifikasjonRepository; + + @Autowired + DvhMeldingEntitetRepository dvhMeldingEntitetRepository; + @Autowired + AvtaleMeldingEntitetRepository avtaleMeldingEntitetRepository; + + @BeforeEach + public void init() { + slettInnholdDatabase(); + } + + private void slettInnholdDatabase() { + varselRepository.deleteAll(); + smsRepository.deleteAll(); + avtaleInnholdRepository.deleteAll(); + arbeidsgiverNotifikasjonRepository.deleteAll(); + dvhMeldingEntitetRepository.deleteAll(); + avtaleMeldingEntitetRepository.deleteAll(); + avtaleRepository.deleteAll(); + } + + private void initAvtalerTilDBTest() { + settAvtaleInformasjon( + TestData.enArbeidstreningAvtale(), + new Fnr("00000000000"), + LocalDate.now(), + LocalDate.now().plusMonths(2), + LocalDateTime.now() + ); + settAvtaleInformasjon( + TestData.enMentorAvtaleUsignert(), + new Fnr("00000000000"), + LocalDate.now(), + LocalDate.now().plusMonths(2), + LocalDateTime.now() + ); + settAvtaleInformasjon( + TestData.enInkluderingstilskuddAvtale(), + new Fnr("00000000000"), + LocalDate.now(), + LocalDate.now().plusMonths(2), + LocalDateTime.now() + ); + } + + private void settAvtaleInformasjon(Avtale avtale, Fnr deltakerFnr, LocalDate startDato, LocalDate sluttDato, LocalDateTime godkjentAvVeileder) { + avtale.setDeltakerFnr(deltakerFnr); + avtale.getGjeldendeInnhold().setStartDato(startDato); + avtale.getGjeldendeInnhold().setSluttDato(sluttDato); + if(godkjentAvVeileder != null) { + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(godkjentAvVeileder); + } + avtaleRepository.save(avtale); + } + + @Test + public void skal_returnere_avtaler_deltaker_allerede_er_registrert_paa() { + initAvtalerTilDBTest(); + List avtaleAlleredeRegistrertPaDeltaker = avtaleRepository.finnAvtalerSomOverlapperForDeltakerVedGodkjenningAvAvtale( + "00000000000", + UUID.randomUUID().toString(), + Date.valueOf(LocalDate.now()), + Date.valueOf(LocalDate.now().plusMonths(1)) + ); + Assertions.assertEquals(3, avtaleAlleredeRegistrertPaDeltaker.size()); + } + + @Test + public void avtalePaDeltakerUtenNoenAvtaleIdOgSluttdato() { + initAvtalerTilDBTest(); + List avtalePaDeltakerUtenNoenAvtaleIdOgSluttdato = avtaleRepository.finnAvtalerSomOverlapperForDeltakerVedOpprettelseAvAvtale( + "00000000000", + Date.valueOf(LocalDate.now()) + ); + Assertions.assertEquals(3, avtalePaDeltakerUtenNoenAvtaleIdOgSluttdato.size()); + } + + @Test + public void avtalePaDeltakerMedKunOverlappendeStartdato() { + initAvtalerTilDBTest(); + List avtalePaDeltakerMedKunOverlappendeStartdato = avtaleRepository.finnAvtalerSomOverlapperForDeltakerVedGodkjenningAvAvtale( + "00000000000", + UUID.randomUUID().toString(), + Date.valueOf(LocalDate.now()), + Date.valueOf(LocalDate.now().plusMonths(3)) + ); + Assertions.assertEquals(3, avtalePaDeltakerMedKunOverlappendeStartdato.size()); + } + + @Test + public void avtalePaDeltakerMedKunOverlappendeSluttdato() { + initAvtalerTilDBTest(); + List avtalePaDeltakerMedKunOverlappendeSluttdato = avtaleRepository.finnAvtalerSomOverlapperForDeltakerVedGodkjenningAvAvtale( + "00000000000", + UUID.randomUUID().toString(), + Date.valueOf(LocalDate.now().minusMonths(1)), + Date.valueOf(LocalDate.now().plusMonths(1)) + ); + Assertions.assertEquals(3, avtalePaDeltakerMedKunOverlappendeSluttdato.size()); + } + + @Test + public void sjekkAtRegistreringAvArbeidstreningHvorDetAlleredeFinnesEnITidsrommetReturnererEttTreff() { + initAvtalerTilDBTest(); + Veileder veileder_z123456 = TestData.enVeileder(new NavIdent("Z123456")); + + List treffPaAvtalerSomErUlovligMatch = veileder_z123456.hentAvtaleDeltakerAlleredeErRegistrertPaa( + new Fnr("00000000000"), + Tiltakstype.ARBEIDSTRENING, + null, + LocalDate.now(), + LocalDate.now().plusMonths(1), + avtaleRepository + ); + Assertions.assertEquals(1, treffPaAvtalerSomErUlovligMatch.size()); + } + + @Test + public void sjekkAtDetIkkeFaarNoeTreffPaaArbeidstreningSomErUtenforStartOgSluttDato() { + Veileder veileder_z123456 = TestData.enVeileder(new NavIdent("Z123456")); + settAvtaleInformasjon( + TestData.enArbeidstreningAvtale(), + new Fnr("00000000000"), + LocalDate.now().plusMonths(1).plusDays(1), + LocalDate.now().plusMonths(3), + LocalDateTime.now() + ); + settAvtaleInformasjon( + TestData.enMentorAvtaleUsignert(), + new Fnr("00000000000"), + LocalDate.now(), + LocalDate.now().plusMonths(2), + LocalDateTime.now() + ); + settAvtaleInformasjon( + TestData.enInkluderingstilskuddAvtale(), + new Fnr("00000000000"), + LocalDate.now(), + LocalDate.now().plusMonths(2), + LocalDateTime.now() + ); + List treffPaAvtalerSomErUlovligMatch = veileder_z123456.hentAvtaleDeltakerAlleredeErRegistrertPaa( + new Fnr("00000000000"), + Tiltakstype.ARBEIDSTRENING, + null, + LocalDate.now(), + LocalDate.now().plusMonths(1), + avtaleRepository + ); + Assertions.assertEquals(0, treffPaAvtalerSomErUlovligMatch.size()); + } + + @Test + public void sjekkAtDetReturneresEnTreffPaaMentorTilskuddNarDetAlleredeFinnesAvtaleSammeTidsrom() { + Veileder veileder_z123456 = TestData.enVeileder(new NavIdent("Z123456")); + settAvtaleInformasjon( + TestData.enArbeidstreningAvtale(), + new Fnr("00000000000"), + LocalDate.now().plusMonths(1).plusDays(1), + LocalDate.now().plusMonths(3), + LocalDateTime.now() + ); + settAvtaleInformasjon( + TestData.enMentorAvtaleUsignert(), + new Fnr("00000000000"), + LocalDate.now(), + LocalDate.now().plusMonths(2), + LocalDateTime.now() + ); + settAvtaleInformasjon( + TestData.enInkluderingstilskuddAvtale(), + new Fnr("00000000000"), + LocalDate.now(), + LocalDate.now().plusMonths(2), + LocalDateTime.now() + ); + List treffPaAvtalerSomErUlovligMatch = veileder_z123456.hentAvtaleDeltakerAlleredeErRegistrertPaa( + new Fnr("00000000000"), + Tiltakstype.MENTOR, + null, + LocalDate.now(), + LocalDate.now().plusMonths(1), + avtaleRepository + ); + Assertions.assertEquals(1, treffPaAvtalerSomErUlovligMatch.size()); + } + + @Test + public void sjekkAtDetReturneresAvtalerSomIkkeErFerdigUtfylt() { + Veileder veileder_z123456 = TestData.enVeileder(new NavIdent("Z123456")); + settAvtaleInformasjon( + TestData.enArbeidstreningAvtale(), + new Fnr("00000000000"), + LocalDate.now().plusMonths(1).plusDays(1), + LocalDate.now().plusMonths(3), + LocalDateTime.now() + ); + settAvtaleInformasjon( + TestData.enArbeidstreningAvtale(), + new Fnr("00000000000"), + null, + null, + null + ); + settAvtaleInformasjon( + TestData.enMentorAvtaleUsignert(), + new Fnr("00000000000"), + LocalDate.now(), + LocalDate.now().plusMonths(2), + LocalDateTime.now() + ); + settAvtaleInformasjon( + TestData.enInkluderingstilskuddAvtale(), + new Fnr("00000000000"), + LocalDate.now(), + LocalDate.now().plusMonths(2), + LocalDateTime.now() + ); + List treffPaAvtalerSomErUlovligMatch = veileder_z123456.hentAvtaleDeltakerAlleredeErRegistrertPaa( + new Fnr("00000000000"), + Tiltakstype.ARBEIDSTRENING, + null, + LocalDate.now(), + LocalDate.now().plusMonths(1), + avtaleRepository + ); + Assertions.assertEquals(1, treffPaAvtalerSomErUlovligMatch.size()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/DeltakerTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/DeltakerTest.java new file mode 100644 index 000000000..477b80063 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/DeltakerTest.java @@ -0,0 +1,89 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.KanIkkeOppheveException; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static no.nav.tag.tiltaksgjennomforing.AssertFeilkode.assertFeilkode; +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.avtalerMedTilskuddsperioder; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DeltakerTest { + private AvtaleRepository avtaleRepository = mock(AvtaleRepository.class); + @Test + public void opphevGodkjenninger__kan_aldri_oppheve_godkjenninger() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Deltaker deltaker = TestData.enDeltaker(avtale); + assertThatThrownBy(() -> deltaker.opphevGodkjenninger(avtale)).isInstanceOf(KanIkkeOppheveException.class); + } + + @Test + public void mentor_Avtaler__skjul_mentor_fnr_for_deltaker() { + Pageable pageable = PageRequest.of(0, 100); + Avtale avtale = TestData.enMentorAvtaleSignert(); + Deltaker deltaker = TestData.enDeltaker(avtale); + AvtalePredicate avtalePredicate = new AvtalePredicate(); + when(avtaleRepository.findAllByDeltakerFnr(any(), eq(pageable))).thenReturn(new PageImpl(List.of(avtale))); + Page avtaler = deltaker.hentAlleAvtalerMedMuligTilgang(avtaleRepository, avtalePredicate, pageable); + assertThat(avtaler.getContent().get(0).getMentorFnr()).isNull(); + assertThat(avtaler.getContent().get(0).getGjeldendeInnhold().getMentorTimelonn()).isNull(); + + } + + @Test + public void mentor_en_Avtale__skjul_mentor_fnr_for_deltaker() { + Avtale avtale = TestData.enMentorAvtaleSignert(); + Deltaker deltaker = TestData.enDeltaker(avtale); + AvtalePredicate avtalePredicate = new AvtalePredicate(); + when(avtaleRepository.findById(any())).thenReturn(Optional.of(avtale)); + + Avtale avtaler = deltaker.hentAvtale(avtaleRepository,avtale.getId()); + assertThat(avtaler.getMentorFnr()).isNull(); + assertThat(avtaler.getGjeldendeInnhold().getMentorTimelonn()).isNull(); + + } + + @Test + public void deltaker_alder_ikke_eldre_enn_72() { + Now.fixedDate(LocalDate.of(2021, 01, 20)); + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(new Fnr("30015521534"), TestData.etBedriftNr(), Tiltakstype.VARIG_LONNSTILSKUDD), TestData.enNavIdent()); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(LocalDate.of(2021, 6, 1)); + endreAvtale.setSluttDato(LocalDate.of(2027, 1, 30)); + avtale.endreAvtale(Instant.now(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtale.godkjennForArbeidsgiver(TestData.enIdentifikator()); + assertFeilkode(Feilkode.DELTAKER_72_AAR, () -> avtale.godkjennForVeilederOgDeltaker(TestData.enNavIdent(), TestData.enGodkjentPaVegneGrunn())); + + Now.resetClock(); + } + + @Test + public void deltaker_alder_ikke_eldre_enn_67() { + Now.fixedDate(LocalDate.of(2021, 01, 20)); + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(new Fnr("30015521534"), TestData.etBedriftNr(), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), TestData.enNavIdent()); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + avtale.getGjeldendeInnhold().setLonnstilskuddProsent(60); + endreAvtale.setStartDato(LocalDate.of(2021, 6, 1)); + endreAvtale.setSluttDato(LocalDate.of(2022, 1, 30)); + avtale.endreAvtale(Instant.now(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtale.godkjennForArbeidsgiver(TestData.enIdentifikator()); + assertFeilkode(Feilkode.DELTAKER_67_AAR, () -> avtale.godkjennForVeilederOgDeltaker(TestData.enNavIdent(), TestData.enGodkjentPaVegneGrunn())); + + Now.resetClock(); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/FnrConverterTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/FnrConverterTest.java new file mode 100644 index 000000000..5c1dc009b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/FnrConverterTest.java @@ -0,0 +1,14 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class FnrConverterTest { + + @Test + public void skalReturnereNullOmVerdiErNull(){ + assertNull(new FnrConverter().convertToDatabaseColumn(null)); + } + +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/FnrTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/FnrTest.java new file mode 100644 index 000000000..434b10cbf --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/FnrTest.java @@ -0,0 +1,131 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDate; +import no.nav.tag.tiltaksgjennomforing.exceptions.TiltaksgjennomforingException; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.Test; + +public class FnrTest { + + @Test + public void fnrKanVæreNull(){ + assertThat(new Fnr(null)).isEqualTo(new Fnr(null)); + } + @Test + public void fnrSkalIkkeVaereTomt() { + assertThatThrownBy(() -> new Fnr("")).isExactlyInstanceOf(TiltaksgjennomforingException.class); + } + + @Test + public void fnrSkalIkkeHaMindreEnn11Siffer() { + assertThatThrownBy(() -> new Fnr("123")).isExactlyInstanceOf(TiltaksgjennomforingException.class); + } + + @Test + public void fnrSkalIkkeHaMerEnn11Siffer() { + assertThatThrownBy(() -> new Fnr("1234567890123")).isExactlyInstanceOf(TiltaksgjennomforingException.class); + } + + @Test + public void fnrSkalIkkeInneholdeBokstaver() { + assertThatThrownBy(() -> new Fnr("1234567890a")).isExactlyInstanceOf(TiltaksgjennomforingException.class); + } + + @Test + public void fnrSkalIkkeInneholdeAndreTingEnnTall() { + assertThatThrownBy(() -> new Fnr("12345678900 ")).isExactlyInstanceOf(TiltaksgjennomforingException.class); + } + + @Test + public void fnrSkalInneholde11Tall() { + String gyldigFnr = "01234567890"; + assertThat(new Fnr(gyldigFnr).asString()).isEqualTo(gyldigFnr); + } + + @Test + public void testFnr1() { + Fnr fnrOver16 = new Fnr("29110976648"); + assertThat(fnrOver16.erUnder16år()).isTrue(); + assertThat(fnrOver16.erOver30år()).isFalse(); + } + + @Test + public void testFnr2() { + Fnr fnr = new Fnr("19109613897"); + assertThat(fnr.erUnder16år()).isFalse(); + assertThat(fnr.erOver30år()).isFalse(); + } + + @Test + public void testFnr3() { + Fnr fnr = new Fnr("25128626630"); + assertThat(fnr.erOver30år()).isTrue(); + assertThat(fnr.erUnder16år()).isFalse(); + } + + @Test + public void testFnr4() { + Now.fixedDate(LocalDate.of(2021, 12, 20)); + Fnr fnr = new Fnr("23029149054"); + assertThat(fnr.erOver30årFørsteJanuar()).isFalse(); + assertThat(fnr.erUnder16år()).isFalse(); + Now.resetClock(); + } + + @Test + public void testFnr5() { + Now.fixedDate(LocalDate.of(2021, 12, 20)); + final Fnr fnr = new Fnr("23029149054"); + LocalDate startDato = LocalDate.of(2022, 1, 5); + assertThat(fnr.erOver30årFørsteJanuar()).isFalse(); + assertThat(fnr.erOver30årFraOppstartDato(startDato)).isTrue(); + Now.resetClock(); + } + + @Test + public void testDnr1() { + Fnr fnr = new Fnr("49120799125"); + assertThat(fnr.erUnder16år()).isTrue(); + assertThat(fnr.erOver30år()).isFalse(); + } + + @Test + public void testDnr2() { + Fnr fnr = new Fnr("64090099076"); + assertThat(fnr.erUnder16år()).isFalse(); + assertThat(fnr.erOver30år()).isFalse(); + } + + @Test + void testOver67År() { + Fnr fnr = new Fnr("30015521534"); + + Now.fixedDate(LocalDate.of(2022, 1, 29)); + assertThat(fnr.erOver67ÅrFraSluttDato(Now.localDate())).isFalse(); + + Now.fixedDate(LocalDate.of(2022, 1, 30)); + assertThat(fnr.erOver67ÅrFraSluttDato(Now.localDate())).isTrue(); + + Now.resetClock(); + } + + @Test + void testAtAldersjekkKanGjøresPåSyntetiskFnr() { + Fnr fnr = new Fnr("07459742977"); + assertThat(fnr.erUnder16år()).isFalse(); + assertThat(fnr.erOver30år()).isFalse(); + } + + @Test + void testAtAldersjekkKanGjøresPåSyntetiskFnrFraSkatteEtaten() { + Fnr fnr = new Fnr("21899797180"); + assertThat(fnr.erUnder16år()).isFalse(); + assertThat(fnr.erOver30år()).isFalse(); + } +} + + + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/GjeldendeTilskuddsperiodeTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/GjeldendeTilskuddsperiodeTest.java new file mode 100644 index 000000000..a8bedc86c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/GjeldendeTilskuddsperiodeTest.java @@ -0,0 +1,126 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.EnumSet; + +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter; +import static org.assertj.core.api.Assertions.assertThat; + +public class GjeldendeTilskuddsperiodeTest { + + @Test + public void godkjenner_og_neste_kan_behandles() { + Now.fixedDate(LocalDate.of(2021, 4, 20)); + LocalDate avtaleStart = Now.localDate().minusMonths(3).plusDays(13); + LocalDate avtaleSlutt = Now.localDate().plusMonths(6); + + Avtale avtale = TestData.enLonnstilskuddAvtaleMedAltUtfyltMedGodkjentForEtterregistrering(avtaleStart, avtaleSlutt); + + assertThat(avtale.tilskuddsperiode(0)).isEqualTo(avtale.gjeldendeTilskuddsperiode()); + + avtale.godkjennTilskuddsperiode(TestData.enNavIdent2(), "0000"); + assertThat(avtale.tilskuddsperiode(1)).isEqualTo(avtale.gjeldendeTilskuddsperiode()); + + avtale.godkjennTilskuddsperiode(TestData.enNavIdent2(), "0000"); + assertThat(avtale.tilskuddsperiode(2)).isEqualTo(avtale.gjeldendeTilskuddsperiode()); + + avtale.godkjennTilskuddsperiode(TestData.enNavIdent2(), "0000"); + assertThat(avtale.tilskuddsperiode(3)).isEqualTo(avtale.gjeldendeTilskuddsperiode()); + + avtale.godkjennTilskuddsperiode(TestData.enNavIdent2(), "0000"); + assertThat(avtale.tilskuddsperiode(4)).isEqualTo(avtale.gjeldendeTilskuddsperiode()); + + + avtale.godkjennTilskuddsperiode(TestData.enNavIdent2(), "0000"); + assertThat(avtale.tilskuddsperiode(5)).isEqualTo(avtale.gjeldendeTilskuddsperiode()); + + // Tilskuddsperiode index 5 har startdato som er mer enn 3 mnd frem i tid og vil ikke bli godkjent, slik den vil fortsatt være gjeldende. + avtale.godkjennTilskuddsperiode(TestData.enNavIdent2(), "0000"); + assertThat(avtale.tilskuddsperiode(5)).isEqualTo(avtale.gjeldendeTilskuddsperiode()); + + Now.resetClock(); + } + + // 2 + @Test + public void en_periode() { + LocalDate avtaleStart = Now.localDate(); + LocalDate avtaleSlutt = Now.localDate(); + Avtale avtale = enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(avtaleStart, avtaleSlutt); + assertThat(avtale.tilskuddsperiode(0)).isEqualTo(avtale.gjeldendeTilskuddsperiode()); + } + + // 5 + @Test + public void frem_i_tid() { + LocalDate avtaleStart = Now.localDate().plusDays(15); + LocalDate avtaleSlutt = Now.localDate().plusMonths(8); + Avtale avtale = enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(avtaleStart, avtaleSlutt); + assertThat(avtale.tilskuddsperiode(0)).isEqualTo(avtale.gjeldendeTilskuddsperiode()); + } + + // 6 + @Test + public void første_avslått__neste_kan_behandles() { + Now.fixedDate(LocalDate.of(2021, 4, 20)); + LocalDate avtaleStart = Now.localDate().minusMonths(6); + LocalDate avtaleSlutt = Now.localDate().plusMonths(8); + + Avtale avtale = TestData.enLonnstilskuddAvtaleMedAltUtfyltMedGodkjentForEtterregistrering(avtaleStart, avtaleSlutt); + + avtale.avslåTilskuddsperiode(TestData.enNavIdent2(), EnumSet.of(Avslagsårsak.ANNET), "Forklaring"); + assertThat(avtale.tilskuddsperiode(0)).isEqualTo(avtale.gjeldendeTilskuddsperiode()); + Now.resetClock(); + } + + // 7 + @Test + public void første_avslått__neste_kan_ikke_behandles() { + LocalDate avtaleStart = Now.localDate(); + LocalDate avtaleSlutt = Now.localDate().plusMonths(8); + Avtale avtale = enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(avtaleStart, avtaleSlutt); + avtale.avslåTilskuddsperiode(TestData.enNavIdent2(), EnumSet.of(Avslagsårsak.ANNET), "Forklaring"); + assertThat(avtale.tilskuddsperiode(0)).isEqualTo(avtale.gjeldendeTilskuddsperiode()); + } + + // 8 + @Disabled("Vil ikke virke lenger pga. tilskuddsperioder er 1mnd. nå istedenfor 3.") + @Test + public void godkjenner_og_neste_kan_ikke_behandles() { + Now.fixedDate(LocalDate.of(2021, 11, 30)); + LocalDate avtaleStart = Now.localDate(); + LocalDate avtaleSlutt = Now.localDate().plusMonths(6); + Avtale avtale = enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(avtaleStart, avtaleSlutt); + assertThat(avtale.gjeldendeTilskuddsperiode().getStartDato()).isEqualTo(avtaleStart); + avtale.godkjennTilskuddsperiode(TestData.enNavIdent2(), avtale.getEnhetGeografisk()); + assertThat(avtale.gjeldendeTilskuddsperiode().getStartDato()).isEqualTo(avtaleStart); + assertThat(avtale.gjeldendeTilskuddsperiode().getStatus()).isEqualTo(TilskuddPeriodeStatus.GODKJENT); + Now.resetClock(); + } + + @Test + public void godkjenner_3_første_tilskuddsperioder_neste_kan_ikke_behandles() { + Now.fixedDate(LocalDate.of(2021, 11, 30)); + LocalDate avtaleStart = Now.localDate(); + LocalDate avtaleSlutt = Now.localDate().plusMonths(6); + Avtale avtale = enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(avtaleStart, avtaleSlutt); + + assertThat(avtale.gjeldendeTilskuddsperiode().getStartDato()).isEqualTo(avtaleStart); + avtale.godkjennTilskuddsperiode(TestData.enNavIdent2(), avtale.getEnhetGeografisk()); + assertThat(avtale.gjeldendeTilskuddsperiode()).isEqualTo(avtale.tilskuddsperiode(1)); + avtale.godkjennTilskuddsperiode(TestData.enNavIdent2(), avtale.getEnhetGeografisk()); + assertThat(avtale.gjeldendeTilskuddsperiode()).isEqualTo(avtale.tilskuddsperiode(2)); + avtale.godkjennTilskuddsperiode(TestData.enNavIdent2(), avtale.getEnhetGeografisk()); + assertThat(avtale.gjeldendeTilskuddsperiode()).isEqualTo(avtale.tilskuddsperiode(3)); + avtale.godkjennTilskuddsperiode(TestData.enNavIdent2(), avtale.getEnhetGeografisk()); + assertThat(avtale.gjeldendeTilskuddsperiode()).isEqualTo(avtale.tilskuddsperiode(3)); + assertThat(avtale.gjeldendeTilskuddsperiode().getStatus()).isEqualTo(TilskuddPeriodeStatus.GODKJENT); + + + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/InkluderingstilskuddTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/InkluderingstilskuddTest.java new file mode 100644 index 000000000..bcecf47ae --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/InkluderingstilskuddTest.java @@ -0,0 +1,26 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.avtalerMedTilskuddsperioder; + +public class InkluderingstilskuddTest { + + @Test + public void endreInkluderingstilskudd_verifisere_enkel_endring() { + Avtale avtale = TestData.enInkluderingstilskuddAvtale(); + avtale.endreAvtale(avtale.getSistEndret(), TestData.endringPåAlleInkluderingstilskuddFelter(), Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + } + + @Test + public void endreInkluderingstilskudd_verifisere_endring_etter_godkjenning() { + Avtale avtale = TestData.enInkluderingstilskuddAvtale(); + avtale.godkjennForArbeidsgiver(TestData.enArbeidsgiver().getIdentifikator()); + avtale.godkjennForVeilederOgDeltaker(TestData.enNavIdent(), TestData.enGodkjentPaVegneGrunn()); + List eksisterendeUtgifter = avtale.getGjeldendeInnhold().getInkluderingstilskuddsutgift(); + EndreInkluderingstilskudd endreInkluderingstilskudd = TestData.endringMedNyeInkluderingstilskudd(eksisterendeUtgifter); + avtale.endreInkluderingstilskudd(endreInkluderingstilskudd, TestData.enNavIdent()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/LonnstilskuddStartOgSluttDatoStrategyTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/LonnstilskuddStartOgSluttDatoStrategyTest.java new file mode 100644 index 000000000..e4d8a701e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/LonnstilskuddStartOgSluttDatoStrategyTest.java @@ -0,0 +1,98 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static no.nav.tag.tiltaksgjennomforing.AssertFeilkode.assertFeilkode; +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.avtalerMedTilskuddsperioder; +import static no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD; +import static no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype.VARIG_LONNSTILSKUDD; + +class LonnstilskuddStartOgSluttDatoStrategyTest { + + private static Avtale enMidlertidig() { + return Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), MIDLERTIDIG_LONNSTILSKUDD), TestData.enNavIdent()); + } + + private static Avtale enVarig() { + return Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), VARIG_LONNSTILSKUDD), TestData.enNavIdent()); + } + + private void endreAvtale(Avtale avtale, EndreAvtale endreAvtale) { + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + } + + @Test + public void endreMidlertidigLønnstilskudd__startdato_og_sluttdato_satt_24mnd() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.plusMonths(24).minusDays(1); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + Avtale avtale = enMidlertidig(); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + endreAvtale(avtale, endreAvtale); + } + + @Test + public void endreMidlertidigLønnstilskudd__startdato_og_sluttdato_satt_over_24mnd__SPESIELT_TILPASSET_INNSATS() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.plusMonths(24).plusDays(1); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + Avtale avtale = enMidlertidig(); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SPESIELT_TILPASSET_INNSATS); + assertFeilkode(Feilkode.VARIGHET_FOR_LANG_MIDLERTIDIG_LONNSTILSKUDD_24_MND, () -> endreAvtale(avtale, endreAvtale)); + } + + @Test + public void endreMidlertidigLønnstilskudd__startdato_og_sluttdato_satt_over_24mnd__VARIG_TILPASSET_INNSATS() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.plusMonths(24).plusDays(1); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + Avtale avtale = enMidlertidig(); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + assertFeilkode(Feilkode.VARIGHET_FOR_LANG_MIDLERTIDIG_LONNSTILSKUDD_24_MND, () -> endreAvtale(avtale, endreAvtale)); + } + + @Test + public void endreMidlertidigLønnstilskudd__startdato_og_sluttdato_satt_over_24mnd__SITUASJONSBESTEMT_INNSATS() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.plusMonths(12).plusDays(1); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + Avtale avtale = enMidlertidig(); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS); + assertFeilkode(Feilkode.VARIGHET_FOR_LANG_MIDLERTIDIG_LONNSTILSKUDD_12_MND, () -> endreAvtale(avtale, endreAvtale)); + } + + @Test + public void endreMidlertidigLønnstilskudd__startdato_og_sluttdato_satt_over_12mnd__ikke_satt() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.plusMonths(12).plusDays(1); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + Avtale avtale = enMidlertidig(); + avtale.setKvalifiseringsgruppe(null); + assertFeilkode(Feilkode.VARIGHET_FOR_LANG_MIDLERTIDIG_LONNSTILSKUDD_12_MND, () -> endreAvtale(avtale, endreAvtale)); + } + + @Test + public void endreVarigLønnstilskudd__startdato_og_sluttdato_satt_over_24mnd() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.plusMonths(24).plusDays(1); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + endreAvtale(enVarig(), endreAvtale); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/MentorStartOgSluttDatoStrategyTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/MentorStartOgSluttDatoStrategyTest.java new file mode 100644 index 000000000..42c252c35 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/MentorStartOgSluttDatoStrategyTest.java @@ -0,0 +1,92 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static no.nav.tag.tiltaksgjennomforing.AssertFeilkode.assertFeilkode; +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.avtalerMedTilskuddsperioder; +import static no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype.MENTOR; + +public class MentorStartOgSluttDatoStrategyTest { + + private Avtale avtale; + + @BeforeEach + public void setUp() { + avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), TestData.etBedriftNr(), MENTOR), TestData.enNavIdent()); + } + + @Test + public void endreMentortilskudd__startdato_er_etter_sluttdato() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.minusDays(1); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + assertFeilkode(Feilkode.START_ETTER_SLUTT, () -> endreAvtale(endreAvtale)); + } + + private void endreAvtale(EndreAvtale endreAvtale) { + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + } + + @Test + public void endreMentortilskudd__startdato_og_sluttdato_satt_6mnd_hvis_ikke_spesiellt_tilpasset() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.plusMonths(6).minusDays(1); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + endreAvtale(endreAvtale); + } + + @Test + public void endreMentortilskudd__startdato_og_sluttdato_satt_over_6mnd_hvis_ikke_spesiellt_tilpasset() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.plusMonths(6); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + assertFeilkode(Feilkode.VARIGHET_FOR_LANG_MENTOR_6_MND, () -> endreAvtale(endreAvtale)); + } + + @Test + public void endreMentortilskudd__startdato_og_sluttdato_satt_over_6mnd_hvis_ikke_spesiellt() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.plusMonths(6); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS); + assertFeilkode(Feilkode.VARIGHET_FOR_LANG_MENTOR_6_MND, () -> endreAvtale(endreAvtale)); + } + + @Test + public void endreMentortilskudd__startdato_og_sluttdato_satt_36mnd_spesiellt_tilpasset() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.plusMonths(36).minusDays(1); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + endreAvtale(endreAvtale); + } + + @Test + public void endreMentortilskudd__startdato_og_sluttdato_satt_over_36mnd_spesiellt_tilpasset() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + + LocalDate startDato = Now.localDate(); + LocalDate sluttDato = startDato.plusMonths(36); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + assertFeilkode(Feilkode.VARIGHET_FOR_LANG_MENTOR_36_MND, () -> endreAvtale(endreAvtale)); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/MentorTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/MentorTest.java new file mode 100644 index 000000000..fe0ba0316 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/MentorTest.java @@ -0,0 +1,148 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import static no.nav.tag.tiltaksgjennomforing.AssertFeilkode.assertFeilkode; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +public class MentorTest { + + private AvtaleRepository avtaleRepository = mock(AvtaleRepository.class); + + private Pageable pageable = PageRequest.of(0, 100); + + @Test + public void hentAlleAvtalerMedMuligTilgang__mentor_en_avtale() { + + // GITT + Avtale avtaleUsignert = TestData.enMentorAvtaleUsignert(); + Avtale avtaleSignert = TestData.enMentorAvtaleSignert(); + Mentor mentor = TestData.enMentor(avtaleSignert); + AvtalePredicate avtalePredicate = new AvtalePredicate(); + + // NÅR + when(avtaleRepository.findAllByMentorFnr(any(), eq(pageable))).thenReturn(new PageImpl(List.of(avtaleUsignert, avtaleSignert))); + Page avtaler = mentor.hentAlleAvtalerMedMuligTilgang(avtaleRepository, avtalePredicate, pageable); + + assertThat(avtaler.getTotalElements()).isEqualTo(2); + assertThat(avtaler.getContent().get(0)).isEqualTo(avtaleUsignert); + assertThat(avtaler.getContent().get(1)).isEqualTo(avtaleSignert); + assertThat(avtaler.getContent().get(0).getDeltakerFnr()).isNull(); + assertThat(avtaler.getContent().get(1).getDeltakerFnr()).isNull(); + } + + @Test + public void deltakerFNR_skal_være_null_selv_om_mentor_har_signert(){ + // GITT + Avtale avtaleSignert = TestData.enMentorAvtaleSignert(); + Mentor mentor = TestData.enMentor(avtaleSignert); + // NÅR + when(avtaleRepository.findById(any())).thenReturn(Optional.of(avtaleSignert)); + Avtale avtale = mentor.hentAvtale(avtaleRepository, avtaleSignert.getId()); + + assertThat(avtale).isEqualTo(avtaleSignert); + assertThat(avtale.getDeltakerFnr()).isNull(); + } + + @Test + public void om_mentor_har_tilgang_til_en_annen_mentors_avtale() { + + // GITT + Avtale avtale = TestData.enMentorAvtaleUsignert(); + Mentor mentor = new Mentor(new Fnr("77665521872")); + // NÅR + boolean hartilgang = mentor.harTilgangTilAvtale(avtale); + assertFalse(hartilgang); + } + + @Test + public void om_mentor_har_tilgang_til_en_annen_mentors_avtale_TestDataTest() { + + // GITT + Avtale avtale = TestData.enMentorAvtaleUsignert(); + Mentor mentor = new Mentor(new Fnr("77665521872")); + // NÅR + boolean hartilgang = mentor.harTilgangTilAvtale(avtale); + assertFalse(hartilgang); + } + + @Test + public void hentAlleAvtalerMedMuligTilgang__mentor_en_ikke_signert_avtale_skal_returnere_avtale_med_kun_bedrift_navn() { + + // GITT + Avtale avtale = TestData.enMentorAvtaleUsignert(); + Mentor mentor = TestData.enMentor(avtale); + AvtalePredicate avtalePredicate = new AvtalePredicate(); + // NÅR + when(avtaleRepository.findAllByMentorFnr(any(), eq(pageable))).thenReturn(new PageImpl(List.of(avtale))); + Page avtaler = mentor.hentAlleAvtalerMedMuligTilgang(avtaleRepository, avtalePredicate, pageable); + + assertThat(avtaler).isNotEmpty(); + assertThat(avtaler.getContent().get(0).getDeltakerFnr()).isNull(); + assertThat(avtaler.getContent().get(0).getVeilederNavIdent()).isNull(); + assertThat(avtaler.getContent().get(0).getGjeldendeInnhold().getDeltakerFornavn()).isNull(); + assertThat(avtaler.getContent().get(0).getGjeldendeInnhold().getDeltakerEtternavn()).isNull(); + assertThat(avtaler.getContent().get(0).getGjeldendeInnhold().getVeilederTlf()).isNull(); + assertThat(avtaler.getContent().get(0).getGjeldendeInnhold().getArbeidsgiverKontonummer()).isNull(); + assertThat(avtaler.getContent().get(0).getBedriftNr()).isNotNull(); + } + + @Test + public void endreOmMentor__må_være_en_mentor_avtale() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + Veileder veileder = TestData.enVeileder(avtale); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + veileder.godkjennForVeilederOgDeltaker(TestData.enGodkjentPaVegneGrunn(), avtale); + EndreOmMentor endreOmMentor = new EndreOmMentor("Per", "Persen", "12345678", "litt mentorering", 5.0, 500); + assertFeilkode(Feilkode.KAN_IKKE_ENDRE_FEIL_TILTAKSTYPE, () -> veileder.endreOmMentor(endreOmMentor, avtale)); + } + + @Test + public void endreOmMentor__setter_riktige_felter() { + Avtale avtale = TestData.enMentorAvtaleSignert(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + Veileder veileder = TestData.enVeileder(avtale); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + veileder.godkjennForVeilederOgDeltaker(TestData.enGodkjentPaVegneGrunn(),avtale); + //veileder.godkjennAvtale(Instant.now(), avtale); + assertThat(avtale.getGjeldendeInnhold().getInnholdType()).isEqualTo(AvtaleInnholdType.INNGÅ); + EndreOmMentor endreOmMentor = new EndreOmMentor("Per", "Persen", "12345678", "litt mentorering", 5.0, 500); + veileder.endreOmMentor(endreOmMentor, avtale); + assertThat(avtale.getGjeldendeInnhold().getMentorFornavn()).isEqualTo("Per"); + assertThat(avtale.getGjeldendeInnhold().getMentorEtternavn()).isEqualTo("Persen"); + assertThat(avtale.getGjeldendeInnhold().getMentorTlf()).isEqualTo("12345678"); + assertThat(avtale.getGjeldendeInnhold().getMentorOppgaver()).isEqualTo("litt mentorering"); + assertThat(avtale.getGjeldendeInnhold().getMentorAntallTimer()).isEqualTo(5); + assertThat(avtale.getGjeldendeInnhold().getMentorTimelonn()).isEqualTo(500); + assertThat(avtale.getGjeldendeInnhold().getInnholdType()).isEqualTo(AvtaleInnholdType.ENDRE_OM_MENTOR); + } + + @Test + public void endreOmMentor__avtale_må_være_inngått() { + Avtale avtale = TestData.enMentorAvtaleSignert(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + Veileder veileder = TestData.enVeileder(avtale); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + assertThat(avtale.erAvtaleInngått()).isFalse(); + assertFeilkode( + Feilkode.KAN_IKKE_ENDRE_OM_MENTOR_IKKE_INNGAATT_AVTALE, + () -> veileder.endreOmMentor(new EndreOmMentor("Per", "Persen", "12345678", "litt mentorering", 5.0, 500), avtale) + ); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/MidlertidigLonnstilskuddStrategyTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/MidlertidigLonnstilskuddStrategyTest.java new file mode 100644 index 000000000..b81c12818 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/MidlertidigLonnstilskuddStrategyTest.java @@ -0,0 +1,77 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.AssertFeilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD; +import static no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype.VARIG_LONNSTILSKUDD; +import static org.assertj.core.api.Assertions.assertThat; + +class MidlertidigLonnstilskuddStrategyTest { + + private AvtaleInnhold avtaleInnhold; + private AvtaleInnholdStrategy strategy; + + @BeforeEach + public void setUp() { + avtaleInnhold = new AvtaleInnhold(); + strategy = AvtaleInnholdStrategyFactory.create(avtaleInnhold, MIDLERTIDIG_LONNSTILSKUDD); + } + + @Test + void test_at_feil_når_familietilknytning_ikke_er_fylt_ut() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setHarFamilietilknytning(true); + endreAvtale.setFamilietilknytningForklaring(null); + strategy.endre(endreAvtale); + + assertThat(strategy.alleFelterSomMåFyllesUt()).containsKey(AvtaleInnhold.Fields.familietilknytningForklaring); + } + + @Test + void test_at_ikke_feil_når_ikke_forklaring_og_nei_på_familietilknytning() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setHarFamilietilknytning(false); + endreAvtale.setFamilietilknytningForklaring(null); + strategy.endre(endreAvtale); + + assertThat(strategy.alleFelterSomMåFyllesUt()).doesNotContainKey(AvtaleInnhold.Fields.familietilknytningForklaring); + } + + @Test + void test_at_ikke_feil_når_alt_fylt_ut_og_har_familietilknytning() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setHarFamilietilknytning(true); + endreAvtale.setFamilietilknytningForklaring("En god forklaring"); + strategy.endre(endreAvtale); + } + + @Test + public void sjekk_riktig_otp_sats() { + strategy = AvtaleInnholdStrategyFactory.create(avtaleInnhold, VARIG_LONNSTILSKUDD); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setOtpSats(0.301); + AssertFeilkode.assertFeilkode(Feilkode.FEIL_OTP_SATS, () -> strategy.endre(endreAvtale)); + endreAvtale.setOtpSats(-0.001); + AssertFeilkode.assertFeilkode(Feilkode.FEIL_OTP_SATS, () -> strategy.endre(endreAvtale)); + + endreAvtale.setOtpSats(0.0); + strategy.endre(endreAvtale); + + endreAvtale.setOtpSats(null); + strategy.endre(endreAvtale); + + endreAvtale.setOtpSats(0.3); + strategy.endre(endreAvtale); + } + + @Test + void lonnstilskuddsprosent_skal_ikke_fylles_ut() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setLonnstilskuddProsent(null); + strategy.endre(endreAvtale); + assertThat(strategy.alleFelterSomMåFyllesUt()).doesNotContainKey(AvtaleInnhold.Fields.lonnstilskuddProsent); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/NavIdentTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/NavIdentTest.java new file mode 100644 index 000000000..0641739b6 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/NavIdentTest.java @@ -0,0 +1,21 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class NavIdentTest { + @ParameterizedTest + @CsvSource(value = { + "xxxxxx,false", + "X12345,false", + "00000000000,false", + "912345,false", + "X123456,true", + "x123456,true", + }) + void test_erNavIdent(String input, boolean expected) { + assertThat(NavIdent.erNavIdent(input)).isEqualTo(expected); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/RegnUtTilskuddsperioderForAvtaleTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/RegnUtTilskuddsperioderForAvtaleTest.java new file mode 100644 index 000000000..1e94c2cfe --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/RegnUtTilskuddsperioderForAvtaleTest.java @@ -0,0 +1,606 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; + +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.avtalerMedTilskuddsperioder; +import static no.nav.tag.tiltaksgjennomforing.utils.DatoUtils.sisteDatoIMnd; +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class RegnUtTilskuddsperioderForAvtaleTest { + + @Test + public void en_tilskuddsperiode() { + Now.fixedDate(LocalDate.of(2021, 1, 1)); + LocalDate fra = LocalDate.of(2021, 1, 1); + LocalDate til = LocalDate.of(2021, 3, 31); + + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(fra); + endreAvtale.setSluttDato(til); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + + assertThat(avtale.getTilskuddPeriode().size()).isEqualTo(3); + assertThat(avtale.getTilskuddPeriode().first().getBeløp()).isEqualTo(avtale.getGjeldendeInnhold().getSumLonnstilskudd()); + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void splitt_ved_nyttår() { + Now.fixedDate(LocalDate.of(2020, 12, 1)); + LocalDate fra = LocalDate.of(2020, 12, 1); + LocalDate til = LocalDate.of(2021, 1, 31); + + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(fra); + endreAvtale.setSluttDato(til); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + var tilskuddPerioder = avtale.getTilskuddPeriode(); + + assertThat(tilskuddPerioder.size()).isEqualTo(2); + Iterator iterator = tilskuddPerioder.iterator(); + TilskuddPeriode første = iterator.next(); + TilskuddPeriode andre = iterator.next(); + assertThat(første.getBeløp()).isEqualTo(andre.getBeløp()); + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void reduksjon_etter_6_mnd__30_prosent_lonnstilskudd() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setLonnstilskuddProsent(40); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + TilskuddPeriode tilskuddpeirode6mndEtterStart = finnTilskuddsperiodeForDato(avtale.getGjeldendeInnhold().getStartDato().plusMonths(6), avtale); + TilskuddPeriode tilskuddperiodeDagenFør6Mnd = finnTilskuddsperiodeForDato(avtale.getGjeldendeInnhold().getStartDato().plusMonths(6).minusDays(1), avtale); + + assertThat(tilskuddpeirode6mndEtterStart.getLonnstilskuddProsent()).isEqualTo(30); + assertThat(tilskuddperiodeDagenFør6Mnd.getLonnstilskuddProsent()).isEqualTo(40); + + harRiktigeEgenskaper(avtale); + } + + private TilskuddPeriode finnTilskuddsperiodeForDato(LocalDate dato, Avtale avtale) { + for (TilskuddPeriode tilskuddsperiode : avtale.getTilskuddPeriode()) { + if (tilskuddsperiode.getStartDato().isBefore(dato.plusDays(1)) && tilskuddsperiode.getSluttDato().isAfter(dato.minusDays(1))) { + return tilskuddsperiode; + } + } + return null; + } + + @Test + public void finnTilskuddsperiodeForDato() { + Now.fixedDate(LocalDate.of(2021, 1, 1)); + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(LocalDate.of(2021, 1, 1)); + endreAvtale.setSluttDato(LocalDate.of(2021, 10, 1)); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + TilskuddPeriode tilskuddPeriode1 = finnTilskuddsperiodeForDato(LocalDate.of(2021, 1, 1), avtale); + TilskuddPeriode tilskuddPeriode2 = finnTilskuddsperiodeForDato(LocalDate.of(2021, 2, 1), avtale); + + assertThat(tilskuddPeriode1).isEqualTo(avtale.tilskuddsperiode(0)); + assertThat(tilskuddPeriode2).isEqualTo(avtale.tilskuddsperiode(1)); + Now.resetClock(); + } + + @Test + public void sjekkAtEnhetsnrOgEnhetsnavnBlirSattPaEndreAvtale() { + final String ENHETS_NR = "1001"; + final String ENHETS_NAVN = "NAV Ullensaker"; + + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(avtale.getGjeldendeInnhold().getStartDato()); + endreAvtale.setSluttDato(avtale.getGjeldendeInnhold().getSluttDato()); + + avtale.oppdatereKostnadsstedForTilskuddsperioder(new NyttKostnadssted(ENHETS_NR, ENHETS_NAVN)); + assertThat(avtale.tilskuddsperiode(0).getEnhet()).isEqualTo(ENHETS_NR); + assertThat(avtale.tilskuddsperiode(0).getEnhetsnavn()).isEqualTo(ENHETS_NAVN); + assertThat(avtale.tilskuddsperiode(1).getEnhet()).isEqualTo(ENHETS_NR); + assertThat(avtale.tilskuddsperiode(2).getEnhetsnavn()).isEqualTo(ENHETS_NAVN); + + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + assertThat(avtale.tilskuddsperiode(0).getEnhet()).isEqualTo(ENHETS_NR); + assertThat(avtale.tilskuddsperiode(0).getEnhetsnavn()).isEqualTo(ENHETS_NAVN); + assertThat(avtale.tilskuddsperiode(1).getEnhet()).isEqualTo(ENHETS_NR); + assertThat(avtale.tilskuddsperiode(2).getEnhetsnavn()).isEqualTo(ENHETS_NAVN); + } + + @Test + public void reduksjon_etter_12_mnd_60_prosent_lonnstilskudd() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SPESIELT_TILPASSET_INNSATS); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setLonnstilskuddProsent(60); + endreAvtale.setSluttDato(endreAvtale.getStartDato().plusMonths(13)); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + TilskuddPeriode sisteTilskuddsperiode = avtale.tilskuddsperiode(avtale.getTilskuddPeriode().size() - 1); + assertThat(sisteTilskuddsperiode.getLonnstilskuddProsent()).isEqualTo(50); + harRiktigeEgenskaper(avtale); + } + + @Test + public void splitt_etter_reduksjon_30_prosnt_lonnstilskudd() { + Now.fixedDate(LocalDate.of(2020, 6, 28)); + LocalDate startDato = LocalDate.of(2020, 6, 30); + LocalDate sluttDato = LocalDate.of(2021, 1, 2); + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setLonnstilskuddProsent(40); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + assertThat(avtale.getTilskuddPeriode().size()).isEqualTo(9); + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void sjett_at_reduksjon_skjer_tidlig_12_mnd_ved_68_prosent_eller_høyre(){ + Now.fixedDate(LocalDate.of(2021, 1, 1)); + LocalDate startDato = LocalDate.of(2021, 1, 1); + LocalDate sluttDato = LocalDate.of(2022, 1, 10); + Avtale avtale = TestData.enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.VARIG_LONNSTILSKUDD); + + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + endreAvtale.setLonnstilskuddProsent(68); + + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + assertThat(avtale.getGjeldendeInnhold().getDatoForRedusertProsent()).isEqualTo(LocalDate.of(2022, 1, 1)); + assertThat(avtale.tilskuddsperiode(0).getLonnstilskuddProsent()).isEqualTo(68); + assertThat(avtale.tilskuddsperiode(avtale.getTilskuddPeriode().size() - 1).getLonnstilskuddProsent()).isEqualTo(67); + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void sjett_at_reduksjon_skjer_etter_12_mnd_ved_høyre_enn_68_prosent(){ + Now.fixedDate(LocalDate.of(2023, 1, 2)); + LocalDate startDato = LocalDate.of(2022, 1, 1); + LocalDate sluttDato = LocalDate.of(2023, 12, 12); + Avtale avtale = TestData.enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.VARIG_LONNSTILSKUDD); + avtale.setGodkjentForEtterregistrering(true); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + endreAvtale.setLonnstilskuddProsent(70); + + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + assertThat(avtale.getGjeldendeInnhold().getDatoForRedusertProsent()).isEqualTo(LocalDate.of(2023, 1, 1)); + assertThat(avtale.tilskuddsperiode(0).getLonnstilskuddProsent()).isEqualTo(70); + assertThat(avtale.tilskuddsperiode(avtale.getTilskuddPeriode().size() - 1).getLonnstilskuddProsent()).isEqualTo(67); + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void sjett_at_reduksjon_ikke_skjer_etter_12_mnd_under_68_prosent(){ + Now.fixedDate(LocalDate.of(2021, 1, 1)); + LocalDate startDato = LocalDate.of(2021, 2, 1); + LocalDate sluttDato = LocalDate.of(2023, 3, 10); + Avtale avtale = TestData.enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.VARIG_LONNSTILSKUDD); + + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + endreAvtale.setLonnstilskuddProsent(40); + + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + assertThat(avtale.getGjeldendeInnhold().getDatoForRedusertProsent()).isNull(); + assertThat(avtale.getGjeldendeInnhold().getLonnstilskuddProsent()).isEqualTo(40); + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void sjett_at_reduksjon_ikke_skjer_før_12_mnd_over_68_prosent_eller_høyre(){ + Now.fixedDate(LocalDate.of(2021, 1, 1)); + LocalDate startDato = LocalDate.of(2021, 2, 1); + LocalDate sluttDato = LocalDate.of(2021, 3, 1); + Avtale avtale = TestData.enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.VARIG_LONNSTILSKUDD); + + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + endreAvtale.setLonnstilskuddProsent(69); + + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + assertThat(avtale.getGjeldendeInnhold().getDatoForRedusertProsent()).isNull(); + assertThat(avtale.getGjeldendeInnhold().getLonnstilskuddProsent()).isEqualTo(69); + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void sjett_at_reduksjon_ikke_skjer_før_12_mnd_under_68_prosent(){ + Now.fixedDate(LocalDate.of(2021, 1, 1)); + LocalDate startDato = LocalDate.of(2021, 2, 1); + LocalDate sluttDato = LocalDate.of(2021, 6, 1); + Avtale avtale = TestData.enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.VARIG_LONNSTILSKUDD); + + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + endreAvtale.setLonnstilskuddProsent(35); + + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + assertThat(avtale.getGjeldendeInnhold().getDatoForRedusertProsent()).isNull(); + assertThat(avtale.getGjeldendeInnhold().getLonnstilskuddProsent()).isEqualTo(35); + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void sjekk_at_reduksjon_skjer_etter_6_mnd_ved_40_prosent() { + Now.fixedDate(LocalDate.of(2021, 1, 1)); + LocalDate startDato = LocalDate.of(2021, 1, 1); + LocalDate sluttDato = LocalDate.of(2021, 7, 1); + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + endreAvtale.setLonnstilskuddProsent(40); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + assertThat(avtale.getGjeldendeInnhold().getDatoForRedusertProsent()).isEqualTo(LocalDate.of(2021, 7, 1)); + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void sjekk_at_reduksjon_skjer_etter_12_mnd_ved_60_prosent() { + Now.fixedDate(LocalDate.of(2021,1,1)); + LocalDate startDato = LocalDate.of(2021, 1, 1); + LocalDate sluttDato = LocalDate.of(2022, 12, 31); + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SPESIELT_TILPASSET_INNSATS); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + endreAvtale.setLonnstilskuddProsent(60); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + assertThat(avtale.getGjeldendeInnhold().getDatoForRedusertProsent()).isEqualTo(LocalDate.of(2022, 1, 1)); + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void sjekk_at_ingen_redusering_under_1_år_60_prosent() { + Now.fixedDate(LocalDate.of(2021, 1, 1)); + LocalDate startDato = LocalDate.of(2021, 1, 1); + LocalDate sluttDato = LocalDate.of(2021, 12, 31); + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(startDato); + endreAvtale.setSluttDato(sluttDato); + endreAvtale.setLonnstilskuddProsent(60); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + assertThat(avtale.getGjeldendeInnhold().getDatoForRedusertProsent()).isNull(); + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void sjekk_at_varig_lonnstilskudd_ikke_reduserses() { + Now.fixedDate(LocalDate.of(2021, 1, 1)); + Avtale avtale = TestData.enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.VARIG_LONNSTILSKUDD); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setStartDato(LocalDate.of(2021, 1, 1)); + endreAvtale.setSluttDato(LocalDate.of(2031, 1, 1)); + endreAvtale.setLonnstilskuddProsent(60); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + assertThat(avtale.tilskuddsperiode(avtale.getTilskuddPeriode().size() - 1).getLonnstilskuddProsent()).isEqualTo(60); + assertThat(avtale.getGjeldendeInnhold().getDatoForRedusertProsent()).isNull(); + Now.resetClock(); + } + + @Test + public void sjekk_at_avtalen_ikke_annulleres_om_den_har_en_utbetalt_tilskuddsperiode() { + Avtale avtale = TestData.enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.VARIG_LONNSTILSKUDD); + avtale.tilskuddsperiode(0).setRefusjonStatus(RefusjonStatus.UTBETALT); + assertThrows(FeilkodeException.class, () -> avtale.annuller(TestData.enVeileder(avtale), "")); + } + + @Test + public void sjekk_at_avtalen_ikke_annulleres_om_den_har_en_godkjent_refusjon_på_tilskuddsperiode() { + Avtale avtale = TestData.enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.VARIG_LONNSTILSKUDD); + avtale.tilskuddsperiode(0).setRefusjonStatus(RefusjonStatus.SENDT_KRAV); + assertThrows(FeilkodeException.class, () -> avtale.annuller(TestData.enVeileder(avtale), "")); + } + + @Test + public void sjekk_at_utbetalte_perioder_beholdes_ved_endring() { + Avtale avtale = TestData.enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.VARIG_LONNSTILSKUDD); + avtale.tilskuddsperiode(0).setRefusjonStatus(RefusjonStatus.UTBETALT); + UUID idPåUtbetaltTilskuddsperiode = avtale.tilskuddsperiode(0).getId(); + Integer beløpPåUtbetaltTilskuddsperiode = avtale.tilskuddsperiode(0).getBeløp(); + + assertThat(avtale.tilskuddsperiode(0).getRefusjonStatus()).isEqualTo(RefusjonStatus.UTBETALT); + assertThat(avtale.tilskuddsperiode(0).getId()).isEqualTo(idPåUtbetaltTilskuddsperiode); + assertThat(avtale.tilskuddsperiode(0).getBeløp()).isEqualTo(beløpPåUtbetaltTilskuddsperiode); + harRiktigeEgenskaper(avtale); + } + + @Test + public void sjekk_at_nye_perioder_ved_forlengelse_starter_etter_utbetalte_perioder() { + Now.fixedDate(LocalDate.of(2021, 1, 1)); + Avtale avtale = TestData.enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(LocalDate.of(2021, 1, 1), LocalDate.of(2021, 4, 1)); + + avtale.tilskuddsperiode(0).setRefusjonStatus(RefusjonStatus.UTBETALT); + avtale.tilskuddsperiode(1).setRefusjonStatus(RefusjonStatus.UTBETALT); + + avtale.forlengAvtale(avtale.getGjeldendeInnhold().getSluttDato().plusMonths(3), TestData.enNavIdent()); + + assertThat(avtale.tilskuddsperiode(0).getRefusjonStatus()).isEqualTo(RefusjonStatus.UTBETALT); + assertThat(avtale.tilskuddsperiode(1).getRefusjonStatus()).isEqualTo(RefusjonStatus.UTBETALT); + + assertThat(avtale.tilskuddsperiode(1).getStartDato()).isEqualTo(avtale.tilskuddsperiode(0).getSluttDato().plusDays(1)); + assertThat(avtale.tilskuddsperiode(2).getStatus()).isEqualTo(TilskuddPeriodeStatus.UBEHANDLET); + + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void sjekk_at_godkjent_perioder_beholdes_ved_endring_som_påvirker_økonomi() { + Avtale avtale = TestData.enLonnstilskuddAvtaleGodkjentAvVeileder(); + avtale.tilskuddsperiode(0).setStatus(TilskuddPeriodeStatus.GODKJENT); + Integer beløpFørEndring = avtale.tilskuddsperiode(0).getBeløp(); + + EndreTilskuddsberegning endreTilskuddsberegning = EndreTilskuddsberegning.builder() + .manedslonn(99999) + .arbeidsgiveravgift(avtale.getGjeldendeInnhold().getArbeidsgiveravgift()) + .feriepengesats(avtale.getGjeldendeInnhold().getFeriepengesats()) + .otpSats(avtale.getGjeldendeInnhold().getOtpSats()) + .build(); + avtale.endreTilskuddsberegning(endreTilskuddsberegning, TestData.enNavIdent()); + + assertThat(avtale.tilskuddsperiode(0).getStatus()).isEqualTo(TilskuddPeriodeStatus.GODKJENT); + assertThat(avtale.tilskuddsperiode(0).getBeløp()).isEqualTo(beløpFørEndring); + harRiktigeEgenskaper(avtale); + } + + @Test + public void sjekk_at_godkjent_periode_annulleres() { + Avtale avtale = TestData.enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.VARIG_LONNSTILSKUDD); + avtale.tilskuddsperiode(0).setStatus(TilskuddPeriodeStatus.GODKJENT); + UUID idPåGodkjentTilskuddsperiode = avtale.tilskuddsperiode(0).getId(); + + avtale.annuller(TestData.enVeileder(avtale), ""); + + assertThat(avtale.tilskuddsperiode(0).getStatus()).isEqualTo(TilskuddPeriodeStatus.ANNULLERT); + assertThat(avtale.tilskuddsperiode(0).getId()).isEqualTo(idPåGodkjentTilskuddsperiode); + } + + @Test + public void sjekk_at_ubehandlet_periode_slettes() { + Avtale avtale = TestData.enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.VARIG_LONNSTILSKUDD); + avtale.tilskuddsperiode(0).setStatus(TilskuddPeriodeStatus.UBEHANDLET); + + avtale.annuller(TestData.enVeileder(avtale), ""); + + assertThat(avtale.getTilskuddPeriode()).isEmpty(); + } + + @Test + public void sjekk_at_godkjent_periode_annulleres_ved_forlengelse() { + Now.fixedDate(LocalDate.of(2021, 1, 1)); + LocalDate avtaleFørsteDag = LocalDate.of(2021, 1, 1); + Avtale avtale = TestData.enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(avtaleFørsteDag, avtaleFørsteDag); + + avtale.tilskuddsperiode(0).setStatus(TilskuddPeriodeStatus.GODKJENT); + UUID idPåGodkjentTilskuddsperiode = avtale.tilskuddsperiode(0).getId(); + + avtale.forlengAvtale(avtaleFørsteDag.plusDays(1), TestData.enNavIdent()); + + assertThat(avtale.tilskuddsperiode(0).getStatus()).isEqualTo(TilskuddPeriodeStatus.ANNULLERT); + assertThat(avtale.tilskuddsperiode(0).getId()).isEqualTo(idPåGodkjentTilskuddsperiode); + assertThat(avtale.tilskuddsperiode(0).getStartDato()).isEqualTo(avtaleFørsteDag); + assertThat(avtale.tilskuddsperiode(0).getSluttDato()).isEqualTo(avtaleFørsteDag); + + assertThat(avtale.tilskuddsperiode(1).getStatus()).isEqualTo(TilskuddPeriodeStatus.UBEHANDLET); + assertThat(avtale.tilskuddsperiode(1).getStartDato()).isEqualTo(avtaleFørsteDag); + assertThat(avtale.tilskuddsperiode(1).getSluttDato()).isEqualTo(avtaleFørsteDag.plusDays(1)); + + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void sjekk_at_godkjent_periode_ikke_annulleres_ved_forlengelse_om_den_dekker_hele_måneden() { + Now.fixedDate(LocalDate.of(2023, 1, 1)); + LocalDate avtaleFørsteDag = LocalDate.of(2023, 1, 1); + Avtale avtale = TestData.enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(avtaleFørsteDag, sisteDatoIMnd(avtaleFørsteDag)); + + avtale.tilskuddsperiode(0).setStatus(TilskuddPeriodeStatus.GODKJENT); + UUID idPåGodkjentTilskuddsperiode = avtale.tilskuddsperiode(0).getId(); + + avtale.forlengAvtale(sisteDatoIMnd(avtaleFørsteDag).plusDays(2), TestData.enNavIdent()); + + assertThat(avtale.tilskuddsperiode(0).getStatus()).isEqualTo(TilskuddPeriodeStatus.GODKJENT); + assertThat(avtale.tilskuddsperiode(0).getId()).isEqualTo(idPåGodkjentTilskuddsperiode); + assertThat(avtale.tilskuddsperiode(0).getStartDato()).isEqualTo(avtaleFørsteDag); + assertThat(avtale.tilskuddsperiode(0).getSluttDato()).isEqualTo(LocalDate.of(2023, 1, 31)); + + assertThat(avtale.tilskuddsperiode(1).getStatus()).isEqualTo(TilskuddPeriodeStatus.UBEHANDLET); + assertThat(avtale.tilskuddsperiode(1).getStartDato()).isEqualTo(LocalDate.of(2023, 2, 1)); + assertThat(avtale.tilskuddsperiode(1).getSluttDato()).isEqualTo(LocalDate.of(2023, 2, 2)); + + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void sjekk_at_godkjent_periode_ikke_annulleres_ved_økonomiendring_i_et_hull() { + Now.fixedDate(LocalDate.of(2020, 6, 28)); + LocalDate avtaleStart = LocalDate.of(2021, 1, 1); + LocalDate avtaleSlutt = LocalDate.of(2021, 8, 1); + Avtale avtale = TestData.enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(avtaleStart, avtaleSlutt); + + + avtale.tilskuddsperiode(0).setStatus(TilskuddPeriodeStatus.GODKJENT); + avtale.tilskuddsperiode(0).setRefusjonStatus(RefusjonStatus.UTBETALT); + avtale.tilskuddsperiode(1).setStatus(TilskuddPeriodeStatus.GODKJENT); + avtale.tilskuddsperiode(2).setRefusjonStatus(RefusjonStatus.UTBETALT); + avtale.tilskuddsperiode(2).setStatus(TilskuddPeriodeStatus.GODKJENT); + + EndreTilskuddsberegning endreTilskuddsberegning = EndreTilskuddsberegning.builder() + .manedslonn(77777) + .arbeidsgiveravgift(avtale.getGjeldendeInnhold().getArbeidsgiveravgift()) + .feriepengesats(avtale.getGjeldendeInnhold().getFeriepengesats()) + .otpSats(avtale.getGjeldendeInnhold().getOtpSats()) + .build(); + avtale.endreTilskuddsberegning(endreTilskuddsberegning, TestData.enNavIdent()); + + avtale.tilskuddsperiode(0).setStatus(TilskuddPeriodeStatus.GODKJENT); + avtale.tilskuddsperiode(0).setRefusjonStatus(RefusjonStatus.UTBETALT); + avtale.tilskuddsperiode(1).setStatus(TilskuddPeriodeStatus.GODKJENT); + avtale.tilskuddsperiode(2).setRefusjonStatus(RefusjonStatus.UTBETALT); + avtale.tilskuddsperiode(2).setStatus(TilskuddPeriodeStatus.GODKJENT); + + harRiktigeEgenskaper(avtale); + Now.resetClock(); + } + + @Test + public void genererMaks1MndTilskuddsperiode() { + Now.fixedDate(LocalDate.of(2021, 1, 01)); + LocalDate avtaleStart = LocalDate.of(2021, 1, 1); + LocalDate avtaleSlutt = LocalDate.of(2021, 6, 2); + Avtale avtale = TestData.enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(avtaleStart, avtaleSlutt); + + avtale.getTilskuddPeriode().forEach(tilskuddPeriode -> { + LocalDate fra = tilskuddPeriode.getStartDato(); + LocalDate til = tilskuddPeriode.getSluttDato(); + // fra og til dato er i samme måned + assertThat(fra.getMonth()).isEqualTo(til.getMonth()); + // Det er maks 1 måneds lengde på tilskuddsperiodene + int dagerITilskuddsperiode = fra.until(til).getDays() + 1; + int dagerIMåneden = fra.lengthOfMonth(); + assertThat(dagerITilskuddsperiode).isLessThanOrEqualTo(dagerIMåneden); + }); + Now.resetClock(); + } + + @Test + public void splittVedMånedsskifte() { + Now.fixedDate(LocalDate.of(2021, 1, 1)); + LocalDate avtaleStart = LocalDate.of(2021, 1, 20); + LocalDate avtaleSlutt = LocalDate.of(2022, 3, 2); + Avtale avtale = TestData.enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(avtaleStart, avtaleSlutt); + + avtale.getTilskuddPeriode().forEach(tilskuddPeriode -> { + // start- og sluttdato er alltid i samme måned + assertThat(tilskuddPeriode.getStartDato().getMonth()).isEqualTo(tilskuddPeriode.getSluttDato().getMonth()); + }); + + + Now.resetClock(); + } + + /* ------------ Metoder som kun brukes innad i denne test-klassen ------------ */ + private void harRiktigeEgenskaper(Avtale avtale) { + harOverlappendeDatoer(avtale.getTilskuddPeriode()); + harAlleDageneIAvtalenperioden(avtale.getTilskuddPeriode(), avtale.getGjeldendeInnhold().getStartDato(), avtale.getGjeldendeInnhold().getSluttDato()); + harRiktigeLøpenumre(avtale.getTilskuddPeriode()); + } + + private void harAlleDageneIAvtalenperioden(Collection tilskuddPerioder, LocalDate avtaleStart, LocalDate avtaleSlutt) { + long antallDager = avtaleStart.until(avtaleSlutt.plusDays(1), ChronoUnit.DAYS); + Set dateSet = new HashSet<>(); + List tilskuddsperioderUtenAnnullerte = tilskuddPerioder.stream().filter(tilskuddPeriode -> tilskuddPeriode.getStatus() != TilskuddPeriodeStatus.ANNULLERT).collect(Collectors.toList()); + for (TilskuddPeriode periode : tilskuddsperioderUtenAnnullerte) { + Set localDates = periode.getStartDato().datesUntil(periode.getSluttDato().plusDays(1)).collect(Collectors.toSet()); + dateSet.addAll(localDates); + } + if (dateSet.size() != antallDager) { + fail("Ulikt antall dager i avtalen og tilskuddsperiodene"); + } + + } + + private void harOverlappendeDatoer(Collection tilskuddPerioder) { + Set dateSet = new HashSet<>(); + List tilskuddsperioderUtenAnnullerte = tilskuddPerioder.stream().filter(tilskuddPeriode -> tilskuddPeriode.getStatus() != TilskuddPeriodeStatus.ANNULLERT).collect(Collectors.toList()); + for (TilskuddPeriode periode : tilskuddsperioderUtenAnnullerte) { + Set localDates = periode.getStartDato().datesUntil(periode.getSluttDato().plusDays(1)).collect(Collectors.toSet()); + + if (!Collections.disjoint(dateSet, localDates)) { + fail("Det finnes overlappende datoer"); + } else { + dateSet.addAll(localDates); + } + } + } + + private void harRiktigeLøpenumre(Collection tilskuddPerioder) { + int løpenummer = 1; + for (TilskuddPeriode tilskuddPeriode : tilskuddPerioder) { + assertThat(tilskuddPeriode.getLøpenummer()).isEqualTo(løpenummer++); + } + } + + + /* ------------ Tester av metoder som kun brukes innad i denne test-klassen ------------ */ + + @Test + public void sjekk_at_ikkeoverlappende_periode_ikke_har_overlappende_datoer() { + TilskuddPeriode tilskuddPeriode1 = new TilskuddPeriode(1000, LocalDate.of(2021, 1, 1), LocalDate.of(2021, 3, 31), 60); + TilskuddPeriode tilskuddPeriode2 = new TilskuddPeriode(1000, LocalDate.of(2021, 4, 1), LocalDate.of(2021, 6, 1), 60); + harOverlappendeDatoer(new TreeSet<>(Set.of(tilskuddPeriode1, tilskuddPeriode2))); + } + + @Test + public void sjekk_at_har_overlappende_datoer() { + TilskuddPeriode tilskuddPeriode1 = new TilskuddPeriode(1000, LocalDate.of(2021, 1, 1), LocalDate.of(2021, 3, 31), 60); + TilskuddPeriode tilskuddPeriode2 = new TilskuddPeriode(1000, LocalDate.of(2021, 3, 31), LocalDate.of(2021, 6, 1), 60); + assertThatThrownBy(() -> harOverlappendeDatoer(List.of(tilskuddPeriode1, tilskuddPeriode2))).isInstanceOf(AssertionError.class); + } + + @Test + public void sjekk_at_alle_dagene_i_avtalen_er_i_tilskuddsperiodene() { + TilskuddPeriode tilskuddPeriode1 = new TilskuddPeriode(1000, LocalDate.of(2021, 1, 1), LocalDate.of(2021, 3, 31), 60); + TilskuddPeriode tilskuddPeriode2 = new TilskuddPeriode(1000, LocalDate.of(2021, 4, 1), LocalDate.of(2021, 6, 1), 60); + harAlleDageneIAvtalenperioden(List.of(tilskuddPeriode1, tilskuddPeriode2), LocalDate.of(2021, 1, 1), LocalDate.of(2021, 6, 1)); + } + + @Test + public void sjekk_at_det_feiler_nar_ikke_alle_dagene_i_avtalen_er_i_tilskuddsperiodene() { + TilskuddPeriode tilskuddPeriode1 = new TilskuddPeriode(1000, LocalDate.of(2021, 1, 1), LocalDate.of(2021, 3, 31), 60); + TilskuddPeriode tilskuddPeriode2 = new TilskuddPeriode(1000, LocalDate.of(2021, 4, 1), LocalDate.of(2021, 5, 25), 60); + assertThatThrownBy(() -> harAlleDageneIAvtalenperioden(List.of(tilskuddPeriode1, tilskuddPeriode2), LocalDate.of(2021, 1, 1), LocalDate.of(2021, 5, 26))).isInstanceOf(AssertionError.class); + } + +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/TestData.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/TestData.java new file mode 100644 index 000000000..54b61fa2a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/TestData.java @@ -0,0 +1,886 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.model.AltinnReportee; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetArbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetBeslutter; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetDeltaker; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetMentor; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggetVeileder; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.SlettemerkeProperties; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.TilgangskontrollService; +import no.nav.tag.tiltaksgjennomforing.enhet.Formidlingsgruppe; +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2Client; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2GeoResponse; +import no.nav.tag.tiltaksgjennomforing.enhet.Norg2OppfølgingResponse; +import no.nav.tag.tiltaksgjennomforing.enhet.Oppfølgingsstatus; +import no.nav.tag.tiltaksgjennomforing.enhet.VeilarbArenaClient; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.NavEnhet; +import no.nav.tag.tiltaksgjennomforing.persondata.Adressebeskyttelse; +import no.nav.tag.tiltaksgjennomforing.persondata.Data; +import no.nav.tag.tiltaksgjennomforing.persondata.HentGeografiskTilknytning; +import no.nav.tag.tiltaksgjennomforing.persondata.HentPerson; +import no.nav.tag.tiltaksgjennomforing.persondata.Navn; +import no.nav.tag.tiltaksgjennomforing.persondata.PdlRespons; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; +import no.nav.tag.tiltaksgjennomforing.sporingslogg.Sporingslogg; +import no.nav.tag.tiltaksgjennomforing.utils.Now; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.mockito.Mockito.*; + +public class TestData { + + public static NavEnhet ENHET_OPPFØLGING = new NavEnhet("0906", "Oslo gamlebyen"); + public static NavEnhet ENHET_GEOGRAFISK = new NavEnhet("0904", "Vinstra"); + public static Integer ET_AVTALENR = 10; + + public static EnumSet avtalerMedTilskuddsperioder = EnumSet.of(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD, Tiltakstype.VARIG_LONNSTILSKUDD, Tiltakstype.SOMMERJOBB); + + public static Avtale enArbeidstreningAvtale() { + NavIdent veilderNavIdent = new NavIdent("Z123456"); + return Avtale.veilederOppretterAvtale(lagOpprettAvtale(Tiltakstype.ARBEIDSTRENING), veilderNavIdent); + } + + public static Avtale enInkluderingstilskuddAvtale() { + NavIdent veilderNavIdent = new NavIdent("Z123456"); + Avtale avtale = Avtale.veilederOppretterAvtale(lagOpprettAvtale(Tiltakstype.INKLUDERINGSTILSKUDD), veilderNavIdent); + avtale.endreAvtale(avtale.getSistEndret(), endringPåAlleInkluderingstilskuddFelter(), Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + return avtale; + } + + public static Avtale enInkluderingstilskuddAvtaleUtfyltOgGodkjentAvArbeidsgiver() { + NavIdent veilderNavIdent = new NavIdent("Z123456"); + Avtale avtale = Avtale.veilederOppretterAvtale(lagOpprettAvtale(Tiltakstype.INKLUDERINGSTILSKUDD), veilderNavIdent); + avtale.endreAvtale(avtale.getSistEndret(), endringPåAlleInkluderingstilskuddFelter(), Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + return avtale; + } + + public static Avtale enSommerjobbAvtale() { + NavIdent veilderNavIdent = new NavIdent("Z123456"); + return Avtale.veilederOppretterAvtale(lagOpprettAvtale(Tiltakstype.SOMMERJOBB), veilderNavIdent); + } + + public static Avtale enMidlertidigLonnstilskuddsjobbAvtale() { + NavIdent veilderNavIdent = new NavIdent("Z123456"); + return Avtale.veilederOppretterAvtale(lagOpprettAvtale(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), veilderNavIdent); + } + + public static Avtale enVarigLonnstilskuddsjobbAvtale() { + NavIdent veilderNavIdent = new NavIdent("Z123456"); + return Avtale.veilederOppretterAvtale(lagOpprettAvtale(Tiltakstype.VARIG_LONNSTILSKUDD), veilderNavIdent); + } + + public static Avtale enMentorAvtale() { + NavIdent veilederNavIdent = new NavIdent("Z123456"); + return Avtale.veilederOppretterAvtale(lagOpprettMentorAvtale(Tiltakstype.MENTOR), veilederNavIdent); + } + + public static Avtale enMentorAvtaleUsignert() { + Avtale avtale = enMentorAvtale(); + EndreAvtale endreAvtale = endringPåAlleMentorFelter(); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + return avtale; + } + + public static Avtale enMentorAvtaleSignert() { + Avtale avtale = enMentorAvtale(); + EndreAvtale endreAvtale = endringPåAlleMentorFelter(); + avtale.godkjennForMentor(avtale.getMentorFnr()); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + return avtale; + } + + public static Avtale enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordelt() { + return Avtale.arbeidsgiverOppretterAvtale(lagOpprettAvtale(Tiltakstype.ARBEIDSTRENING)); + } + + public static Avtale enAvtaleOpprettetAvArbeidsgiver(Tiltakstype tiltakstype) { + return Avtale.arbeidsgiverOppretterAvtale(lagOpprettAvtale(tiltakstype)); + } + + public static Avtale setOppfølgingPåAvtale(Avtale avtale) { + avtale.setEnhetOppfolging(ENHET_OPPFØLGING.getVerdi()); + avtale.setEnhetsnavnOppfolging(ENHET_OPPFØLGING.getNavn()); + return avtale; + } + + public static Avtale setGeografiskPåAvtale(Avtale avtale) { + avtale.setEnhetGeografisk(ENHET_GEOGRAFISK.getVerdi()); + avtale.setEnhetsnavnGeografisk(ENHET_GEOGRAFISK.getNavn()); + return avtale; + } + + public static Avtale setOppfølgingOgGeografiskPåAvtale(Avtale avtale) { + setOppfølgingPåAvtale(avtale); + setGeografiskPåAvtale(avtale); + return avtale; + } + + public static Avtale enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedOppfølgningsEnhet() { + Avtale avtale = enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordelt(); + setOppfølgingPåAvtale(avtale); + return avtale; + } + + public static Avtale enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedOppfølgningsEnhetOgGeografiskEnhet() { + Avtale avtale = enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordelt(); + setOppfølgingOgGeografiskPåAvtale(avtale); + return avtale; + } + + public static Avtale enArbeidstreningsAvtaleMedGittAvtaleNr() { + Avtale avtale = enArbeidstreningAvtale(); + avtale.setAvtaleNr(ET_AVTALENR); + return avtale; + } + + public static Avtale enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordeltMedGeografiskEnhet() { + Avtale avtale = enArbeidstreningAvtaleOpprettetAvArbeidsgiverOgErUfordelt(); + setGeografiskPåAvtale(avtale); + return avtale; + } + + public static Avtale enAvtaleMedAltUtfylt() { + NavIdent veilderNavIdent = new NavIdent("Z123456"); + Avtale avtale = Avtale.veilederOppretterAvtale(lagOpprettAvtale(Tiltakstype.ARBEIDSTRENING), veilderNavIdent); + avtale.endreAvtale(avtale.getSistEndret(), endringPåAlleArbeidstreningFelter(), Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + return avtale; + } + + public static Avtale enAvtaleMedAltUtfyltGodkjentAvVeileder() { + Avtale avtale = enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentTaushetserklæringAvMentor(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvNavIdent(TestData.enNavIdent()); + avtale.getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + avtale.getGjeldendeInnhold().setJournalpostId("1"); + avtale.getGjeldendeInnhold().setRefusjonKontaktperson(new RefusjonKontaktperson("Donald", "Duck", "55555123", true)); + return avtale; + } + + public static Avtale enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(LocalDate startDato, LocalDate sluttDato) { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + setOppfølgingOgGeografiskPåAvtale(avtale); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + EndreAvtale endring = TestData.endringPåAlleLønnstilskuddFelter(); + endring.setStartDato(startDato); + endring.setSluttDato(sluttDato); + avtale.setGodkjentForEtterregistrering(true); + avtale.endreAvtale(Now.instant(), endring, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtale.godkjennForArbeidsgiver(TestData.enIdentifikator()); + avtale.godkjennForDeltaker(TestData.enIdentifikator()); + avtale.godkjennForVeileder(TestData.enNavIdent()); + return avtale; + } + + public static Avtale enMidlertidigLønnstilskuddsRyddeAvtaleMedStartOgSluttGodkjentAvAlleParter(LocalDate startDato, LocalDate sluttDato) { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + avtale.setArenaRyddeAvtale(new ArenaRyddeAvtale()); + setOppfølgingOgGeografiskPåAvtale(avtale); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + EndreAvtale endring = TestData.endringPåAlleLønnstilskuddFelter(); + endring.setStartDato(startDato); + endring.setSluttDato(sluttDato); + avtale.setGodkjentForEtterregistrering(true); + avtale.endreAvtale(Now.instant(), endring, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtale.godkjennForArbeidsgiver(TestData.enIdentifikator()); + avtale.godkjennForDeltaker(TestData.enIdentifikator()); + avtale.godkjennForVeileder(TestData.enNavIdent()); + return avtale; + } + + public static Avtale enMidlertidigLonnstilskuddAvtaleMedAltUtfylt() { + return enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD); + } + + public static Avtale enMidlertidigLonnstilskuddAvtaleMedAltUtfyltUtenTilskuddsperioder() { + return enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD); + } + + public static Avtale enVarigLonnstilskuddAvtaleMedBehandletIArenaPerioder() { + Avtale avtale = enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype.VARIG_LONNSTILSKUDD); + avtale.getGjeldendeInnhold().setStartDato(LocalDate.now().minusYears(1)); + avtale.getGjeldendeInnhold().setSluttDato(LocalDate.now().plusYears(1)); + avtale.nyeTilskuddsperioderEtterMigreringFraArena(LocalDate.of(2023, 2, 1), false); + + // Godkjenning + Arbeidsgiver arbeidsgiver = enArbeidsgiver(avtale); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + Veileder veileder = enVeileder(avtale); + veileder.godkjennForVeilederOgDeltaker(enGodkjentPaVegneGrunn(), avtale); + + return avtale; + } + + public static Avtale enLonnstilskuddAvtaleMedAltUtfylt(Tiltakstype tiltakstype) { + NavIdent veilderNavIdent = new NavIdent("Z123456"); + Avtale avtale = Avtale.veilederOppretterAvtale(lagOpprettAvtale(tiltakstype), veilderNavIdent); + setOppfølgingPåAvtale(avtale); + if (tiltakstype == Tiltakstype.VARIG_LONNSTILSKUDD) { + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + } else { + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS); + } + avtale.endreAvtale(avtale.getSistEndret(), endringPåAlleLønnstilskuddFelter(), Avtalerolle.VEILEDER, EnumSet.of(avtale.getTiltakstype())); + avtale.setTiltakstype(tiltakstype); + avtale.getGjeldendeInnhold().setDeltakerFornavn("Lilly"); + avtale.getGjeldendeInnhold().setDeltakerEtternavn("Lønning"); + avtale.getGjeldendeInnhold().setArbeidsgiverKontonummer("22222222222"); + avtale.getGjeldendeInnhold().setManedslonn(20000); + avtale.getGjeldendeInnhold().setFeriepengesats(BigDecimal.valueOf(0.12)); + avtale.getGjeldendeInnhold().setArbeidsgiveravgift(BigDecimal.valueOf(0.141)); + avtale.getGjeldendeInnhold().setVersjon(1); + avtale.getGjeldendeInnhold().setJournalpostId(null); + avtale.getGjeldendeInnhold().setMaal(List.of()); + return avtale; + } + + public static Avtale enMidlertidigLonnstilskuddAvtaleMedSpesieltTilpassetInnsatsOgAltUtfylt(Tiltakstype tiltakstype) { + NavIdent veilderNavIdent = new NavIdent("Z123456"); + Avtale avtale = Avtale.veilederOppretterAvtale(lagOpprettAvtale(tiltakstype), veilderNavIdent); + setOppfølgingPåAvtale(avtale); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SPESIELT_TILPASSET_INNSATS); + avtale.endreAvtale(avtale.getSistEndret(), endringPåAlleLønnstilskuddFelter(), Avtalerolle.VEILEDER, EnumSet.of(avtale.getTiltakstype())); + avtale.setTiltakstype(tiltakstype); + avtale.getGjeldendeInnhold().setDeltakerFornavn("Lilly"); + avtale.getGjeldendeInnhold().setDeltakerEtternavn("Lønning"); + avtale.getGjeldendeInnhold().setArbeidsgiverKontonummer("22222222222"); + avtale.getGjeldendeInnhold().setManedslonn(20000); + avtale.getGjeldendeInnhold().setFeriepengesats(BigDecimal.valueOf(0.12)); + avtale.getGjeldendeInnhold().setArbeidsgiveravgift(BigDecimal.valueOf(0.141)); + avtale.getGjeldendeInnhold().setVersjon(1); + avtale.getGjeldendeInnhold().setJournalpostId(null); + avtale.getGjeldendeInnhold().setMaal(List.of()); + return avtale; + } + + + public static Avtale enLonnstilskuddAvtaleGodkjentAvVeileder() { + Avtale avtale = enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvNavIdent(TestData.enNavIdent()); + avtale.getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + avtale.getGjeldendeInnhold().setJournalpostId("1"); + return avtale; + } + + public static Avtale enMidlertidigLonnstilskuddAvtaleMedSpesieltTilpassetInnsatsGodkjentAvVeileder() { + Avtale avtale = enMidlertidigLonnstilskuddAvtaleMedSpesieltTilpassetInnsatsOgAltUtfylt(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvNavIdent(TestData.enNavIdent()); + avtale.getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + avtale.getGjeldendeInnhold().setJournalpostId("1"); + return avtale; + } + + public static Avtale enLonnstilskuddAvtaleGodkjentAvVeilederUtenTilskuddsperioder() { + Avtale avtale = enMidlertidigLonnstilskuddAvtaleMedAltUtfyltUtenTilskuddsperioder(); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvNavIdent(TestData.enNavIdent()); + avtale.getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + avtale.getGjeldendeInnhold().setJournalpostId("1"); + return avtale; + } + + public static Avtale enLonnstilskuddAvtaleGodkjentAvVeilederTilbakeITid() { + Avtale avtale = enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setDeltakerFornavn("Geir"); + avtale.getGjeldendeInnhold().setDeltakerEtternavn("Geirsen"); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvNavIdent(TestData.enNavIdent()); + avtale.getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + avtale.getGjeldendeInnhold().setStartDato(Now.localDate().minusMonths(3)); + avtale.getGjeldendeInnhold().setSluttDato(Now.localDate().plusMonths(1)); + avtale.getGjeldendeInnhold().setJournalpostId("1"); + return avtale; + } + + public static Avtale enSommerjobbAvtaleGodkjentAvVeileder() { + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), new BedriftNr("999999999"), Tiltakstype.SOMMERJOBB), new NavIdent("Z123456")); + setOppfølgingPåAvtale(avtale); + EndreAvtale endreAvtale = endringPåAlleLønnstilskuddFelter(); + endreAvtale.setLonnstilskuddProsent(50); + endreAvtale.setFeriepengesats(new BigDecimal("0.12")); + endreAvtale.setArbeidsgiveravgift(new BigDecimal("0.141")); + endreAvtale.setStartDato(LocalDate.of(2021, 6, 1)); + endreAvtale.setSluttDato(LocalDate.of(2021, 6, 1).plusWeeks(4).minusDays(1)); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + NavIdent veileder = TestData.enNavIdent(); + avtale.getGjeldendeInnhold().setGodkjentAvNavIdent(veileder); + avtale.setVeilederNavIdent(veileder); + return avtale; + } + + public static Avtale enSommerjobbAvtaleGodkjentAvBeslutter() { + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), new BedriftNr("999999999"), Tiltakstype.SOMMERJOBB), new NavIdent("Z123456")); + setOppfølgingPåAvtale(avtale); + avtale.setAvtaleNr(1); + EndreAvtale endreAvtale = endringPåAlleLønnstilskuddFelter(); + endreAvtale.setLonnstilskuddProsent(50); + endreAvtale.setFeriepengesats(new BigDecimal("0.12")); + endreAvtale.setArbeidsgiveravgift(new BigDecimal("0.141")); + endreAvtale.setStartDato(LocalDate.of(2021, 6, 1)); + endreAvtale.setSluttDato(LocalDate.of(2021, 6, 1).plusWeeks(4).minusDays(1)); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvBeslutter(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + avtale.getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvNavIdent(TestData.enNavIdent()); + avtale.getGjeldendeInnhold().setRefusjonKontaktperson(null); + return avtale; + } + + public static Avtale enSommerjobbAvtaleGodkjentAvArbeidsgiver() { + Avtale avtale = Avtale.veilederOppretterAvtale(new OpprettAvtale(TestData.etFodselsnummer(), new BedriftNr("999999999"), Tiltakstype.SOMMERJOBB), new NavIdent("Z123456")); + setOppfølgingPåAvtale(avtale); + EndreAvtale endreAvtale = endringPåAlleLønnstilskuddFelter(); + endreAvtale.setLonnstilskuddProsent(50); + endreAvtale.setFeriepengesats(new BigDecimal("0.12")); + endreAvtale.setArbeidsgiveravgift(new BigDecimal("0.141")); + endreAvtale.setStartDato(LocalDate.of(2021, 6, 1)); + endreAvtale.setSluttDato(LocalDate.of(2021, 6, 1).plusWeeks(4).minusDays(1)); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + return avtale; + } + + public static Avtale enMentorAvtaleMedMedAltUtfylt() { + Avtale avtale = enAvtaleMedAltUtfylt(); + avtale.setTiltakstype(Tiltakstype.MENTOR); + avtale.setMentorFnr(new Fnr("00000000000")); + EndreAvtale endreAvtale = new EndreAvtale(); + endreMentorInfo(endreAvtale); + avtale.endreAvtale(Now.instant(), endreAvtale, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtale.getGjeldendeInnhold().setVersjon(1); + avtale.getGjeldendeInnhold().setJournalpostId(null); + return avtale; + } + + public static Avtale enLonnstilskuddAvtaleMedAltUtfyltMedGodkjentForEtterregistrering(LocalDate avtaleStart, LocalDate avtaleSlutt) { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + EndreAvtale endring = TestData.endringPåAlleLønnstilskuddFelter(); + endring.setStartDato(avtaleStart); + endring.setSluttDato(avtaleSlutt); + avtale.setGodkjentForEtterregistrering(true); + avtale.endreAvtale(Now.instant(), endring, Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtale.godkjennForArbeidsgiver(TestData.enIdentifikator()); + avtale.godkjennForDeltaker(TestData.enIdentifikator()); + avtale.godkjennForVeileder(TestData.enNavIdent()); + return avtale; + } + + private static OpprettAvtale lagOpprettAvtale(Tiltakstype tiltakstype) { + Fnr deltakerFnr = new Fnr("00000000000"); + BedriftNr bedriftNr = new BedriftNr("999999999"); + return new OpprettAvtale(deltakerFnr, bedriftNr, tiltakstype); + } + + private static OpprettMentorAvtale lagOpprettMentorAvtale(Tiltakstype tiltakstype) { + Fnr deltakerFnr = new Fnr("00000000000"); + BedriftNr bedriftNr = new BedriftNr("999999999"); + Fnr mentorFnr = new Fnr("23090170716"); + return new OpprettMentorAvtale(deltakerFnr, mentorFnr, bedriftNr, tiltakstype, Avtalerolle.VEILEDER); + } + + public static EndreAvtale ingenEndring() { + return new EndreAvtale(); + } + + public static EndreInkluderingstilskudd endringMedNyeInkluderingstilskudd(List eksisterendeUtgifter) { + EndreInkluderingstilskudd endreInkluderingstilskudd = new EndreInkluderingstilskudd(); + endreInkluderingstilskudd.getInkluderingstilskuddsutgift().addAll(eksisterendeUtgifter); + endreInkluderingstilskudd.getInkluderingstilskuddsutgift().add(TestData.enInkluderingstilskuddsutgiftUtenId(13337, InkluderingstilskuddsutgiftType.PROGRAMVARE)); + return endreInkluderingstilskudd; + } + + public static EndreAvtale endreInkluderingstilskuddInfo(EndreAvtale endreAvtale) { + endreAvtale.getInkluderingstilskuddsutgift().add(TestData.enInkluderingstilskuddsutgift(13337, InkluderingstilskuddsutgiftType.PROGRAMVARE)); + endreAvtale.getInkluderingstilskuddsutgift().add(TestData.enInkluderingstilskuddsutgift(25697, InkluderingstilskuddsutgiftType.OPPLÆRING)); + endreAvtale.getInkluderingstilskuddsutgift().add(TestData.enInkluderingstilskuddsutgift(7195, InkluderingstilskuddsutgiftType.UTSTYR)); + endreAvtale.setInkluderingstilskuddBegrunnelse("Behov for tilskudd til dyre programvarelisenser og opplæring i disse."); + return endreAvtale; + } + + public static EndreAvtale endreKontaktInfo(EndreAvtale endreAvtale) { + endreAvtale.setDeltakerFornavn("Dagny"); + endreAvtale.setDeltakerEtternavn("Deltaker"); + endreAvtale.setDeltakerTlf("40000000"); + endreAvtale.setBedriftNavn("Pers butikk"); + endreAvtale.setArbeidsgiverFornavn("Per"); + endreAvtale.setArbeidsgiverEtternavn("Kremmer"); + endreAvtale.setArbeidsgiverTlf("99999999"); + endreAvtale.setVeilederFornavn("Vera"); + endreAvtale.setVeilederEtternavn("Veileder"); + endreAvtale.setVeilederTlf("44444444"); + endreAvtale.setHarFamilietilknytning(true); + endreAvtale.setFamilietilknytningForklaring("En middels god forklaring"); + return endreAvtale; + } + + public static EndreAvtale endreMaalInfo(EndreAvtale endreAvtale) { + endreAvtale.getMaal().add(TestData.etMaal()); + return endreAvtale; + } + + public static EndreAvtale endreMentorInfo(EndreAvtale endreAvtale) { + endreAvtale.setMentorFornavn("Mentor"); + endreAvtale.setMentorEtternavn("Mentorsen"); + endreAvtale.setMentorOppgaver("Mentoroppgaver"); + endreAvtale.setMentorAntallTimer(10.0); + endreAvtale.setMentorTlf("44444444"); + endreAvtale.setMentorTimelonn(1000); + return endreAvtale; + } + + public static EndreAvtale endringPåAlleArbeidstreningFelter() { + EndreAvtale endreAvtale = new EndreAvtale(); + endreKontaktInfo(endreAvtale); + endreAvtale.setStillingstittel("Butikkbetjent"); + endreAvtale.setStillingStyrk08(5223); + endreAvtale.setStillingKonseptId(112968); + endreAvtale.setArbeidsoppgaver("Butikkarbeid"); + endreAvtale.setStillingprosent(50); + endreAvtale.setAntallDagerPerUke(5); + endreAvtale.getMaal().add(TestData.etMaal()); + endreAvtale.setStartDato(Now.localDate()); + endreAvtale.setSluttDato(endreAvtale.getStartDato().plusMonths(12).minusDays(1)); + endreAvtale.setTilrettelegging("Ingen"); + endreAvtale.setOppfolging("Telefon hver uke"); + return endreAvtale; + } + + public static EndreAvtale endringPåAlleInkluderingstilskuddFelter() { + EndreAvtale endreAvtale = new EndreAvtale(); + endreKontaktInfo(endreAvtale); + endreAvtale.setOppfolging("Telefon hver uke"); + endreAvtale.setTilrettelegging("Ingen"); + endreAvtale.setStartDato(Now.localDate()); + endreAvtale.setSluttDato(endreAvtale.getStartDato().plusWeeks(2)); + endreInkluderingstilskuddInfo(endreAvtale); + return endreAvtale; + } + + public static EndreAvtale endringPåAlleLønnstilskuddFelter() { + EndreAvtale endreAvtale = new EndreAvtale(); + endreKontaktInfo(endreAvtale); + endreAvtale.setOppfolging("Telefon hver uke"); + endreAvtale.setTilrettelegging("Ingen"); + endreAvtale.setStartDato(Now.localDate()); + endreAvtale.setSluttDato(endreAvtale.getStartDato().plusMonths(12).minusDays(1)); + endreAvtale.setStillingprosent(50); + endreAvtale.setArbeidsoppgaver("Butikkarbeid"); + endreAvtale.setArbeidsgiverKontonummer("000111222"); + endreAvtale.setStillingstittel("Butikkbetjent"); + endreAvtale.setStillingStyrk08(5223); + endreAvtale.setStillingKonseptId(112968); + endreAvtale.setLonnstilskuddProsent(60); + endreAvtale.setManedslonn(10000); + endreAvtale.setFeriepengesats(BigDecimal.ONE); + endreAvtale.setArbeidsgiveravgift(BigDecimal.ONE); + endreAvtale.setOtpSats(0.02); + endreAvtale.setStillingstype(Stillingstype.FAST); + endreAvtale.setAntallDagerPerUke(5); + endreAvtale.setRefusjonKontaktperson(new RefusjonKontaktperson("Ola", "Olsen", "12345678", true)); + return endreAvtale; + } + + public static EndreAvtale endringPåAlleMentorFelter() { + EndreAvtale endreAvtale = new EndreAvtale(); + endreKontaktInfo(endreAvtale); + endreAvtale.setOppfolging("Telefon hver uke"); + endreAvtale.setTilrettelegging("Ingen"); + endreAvtale.setStartDato(Now.localDate()); + endreAvtale.setSluttDato(endreAvtale.getStartDato().plusMonths(6).minusDays(1)); + endreMentorInfo(endreAvtale); + return endreAvtale; + } + + public static Deltaker enDeltaker(Avtale avtale) { + return new Deltaker(avtale.getDeltakerFnr()); + } + + public static InnloggetDeltaker enInnloggetDeltaker() { + return new InnloggetDeltaker(new Fnr("99999999999")); + } + + public static InnloggetMentor enInnloggetMentor() { + return new InnloggetMentor(new Fnr("99999999999")); + } + + public static InnloggetArbeidsgiver enInnloggetArbeidsgiver() { + return new InnloggetArbeidsgiver(new Fnr("99999999999"), Collections.emptySet(), Map.of()); + } + + public static InnloggetVeileder enInnloggetVeileder() { + return new InnloggetVeileder(new NavIdent("F888888"), Set.of(ENHET_OPPFØLGING), false); + } + + public static InnloggetBeslutter enInnloggetBeslutter() { + return new InnloggetBeslutter(new NavIdent("F888888"), Set.of(ENHET_OPPFØLGING)); + } + + public static Arbeidsgiver enArbeidsgiver() { + return new Arbeidsgiver(new Fnr("01234567890"), Set.of(), Map.of(), null, null); + } + + public static Mentor enMentor(Avtale avtale) { + return new Mentor(avtale.getMentorFnr()); + } + + public static Arbeidsgiver enArbeidsgiver(Avtale avtale) { + return new Arbeidsgiver( + TestData.etFodselsnummer(), + Set.of(new AltinnReportee("Bedriftnavn", "", null, avtale.getBedriftNr().asString(), "", "")) + , Map.of(avtale.getBedriftNr(), + List.of(Tiltakstype.values())), + null, + null); + } + + public static Fnr etFodselsnummer() { + return new Fnr("00000000000"); + } + + public static void setGeoNavEnhet(Avtale avtale, NavEnhet geoNavEnhet) { + if (avtale.getEnhetGeografisk() == null) { + avtale.setEnhetGeografisk(geoNavEnhet.getVerdi()); + avtale.setEnhetsnavnGeografisk(geoNavEnhet.getNavn()); + } + } + + public static void setOppfolgingNavEnhet(Avtale avtale, NavEnhet oppfolgingNavEnhet) { + if (avtale.getEnhetOppfolging() == null) { + avtale.setEnhetOppfolging(oppfolgingNavEnhet.getVerdi()); + avtale.setEnhetsnavnOppfolging(oppfolgingNavEnhet.getNavn()); + } + } + + public static void setupVeilederMock( + Avtale avtale, + Avtalepart veileder, + PdlRespons pdlRespons, + TilgangskontrollService tilgangskontrollService, + VeilarbArenaClient veilarbArenaClient, + PersondataService persondataService, + Norg2Client norg2Client + ) { + lenient().when(tilgangskontrollService.harSkrivetilgangTilKandidat( + eq((Veileder) veileder), + eq(avtale.getDeltakerFnr()) + )).thenReturn(true); + + lenient().when(veilarbArenaClient.sjekkOgHentOppfølgingStatus(any())) + .thenReturn( + new Oppfølgingsstatus( + Formidlingsgruppe.ARBEIDSSOKER, + Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS, + avtale.getEnhetOppfolging() + ) + ); + when(persondataService.hentPersondata(avtale.getDeltakerFnr())).thenReturn(pdlRespons); + when(persondataService.erKode6(pdlRespons)).thenCallRealMethod(); + + when(norg2Client.hentGeografiskEnhet(pdlRespons.getData().getHentGeografiskTilknytning().getGtBydel())) + .thenReturn(new Norg2GeoResponse( + avtale.getEnhetsnavnOppfolging(), + avtale.getEnhetOppfolging() + )); + when(veilarbArenaClient.hentOppfølgingsEnhet(avtale.getDeltakerFnr().asString())) + .thenReturn(avtale.getEnhetOppfolging()); + + when(norg2Client.hentGeografiskEnhet(pdlRespons.getData().getHentGeografiskTilknytning().getGtBydel())) + .thenReturn(new Norg2GeoResponse( + avtale.getEnhetsnavnOppfolging(), + avtale.getEnhetOppfolging() + )); + } + + public static Veileder enVeilederMedMocketEndepunkt(Avtale avtale) { + final NavEnhet geoNavEnhet = TestData.ENHET_GEOGRAFISK; + final NavEnhet oppfolgingNavEnhet = TestData.ENHET_OPPFØLGING; + + TestData.setGeoNavEnhet(avtale, geoNavEnhet); + TestData.setOppfolgingNavEnhet(avtale, oppfolgingNavEnhet); + + final TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + final PersondataService persondataService = mock(PersondataService.class); + final Norg2Client norg2Client = mock(Norg2Client.class); + final PdlRespons pdlRespons = TestData.enPdlrespons(false); + final VeilarbArenaClient veilarbArenaClient = mock(VeilarbArenaClient.class); + var veileder = new Veileder( + avtale.getVeilederNavIdent(), + tilgangskontrollService, + persondataService, + norg2Client, + Set.of(new NavEnhet(avtale.getEnhetOppfolging(), avtale.getEnhetsnavnOppfolging())), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + + setupVeilederMock( + avtale, + veileder, + pdlRespons, + tilgangskontrollService, + veilarbArenaClient, + persondataService, + norg2Client + ); + + return spy( + veileder + ); + } + + public static Veileder enVeileder(Avtale avtale) { + TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + VeilarbArenaClient veilarbArenaClient = mock(VeilarbArenaClient.class); + + var veileder = new Veileder( + avtale.getVeilederNavIdent(), + tilgangskontrollService, + mock(PersondataService.class), + mock(Norg2Client.class), + Set.of(new NavEnhet("4802", "Oslo gamlebyen")), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + + lenient().when( + tilgangskontrollService.harSkrivetilgangTilKandidat( + eq(veileder), + eq(avtale.getDeltakerFnr()) + ) + ).thenReturn(true); + + lenient().when(veilarbArenaClient.sjekkOgHentOppfølgingStatus(any())) + .thenReturn( + new Oppfølgingsstatus( + Formidlingsgruppe.ARBEIDSSOKER, + Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS, + "0906" + ) + ); + + return veileder; + } + + public static Beslutter enBeslutter(Avtale avtale) { + TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + Norg2Client norg2Client = mock(Norg2Client.class); + NavIdent navIdent = new NavIdent("B999999"); + var beslutter = new Beslutter(navIdent, UUID.randomUUID(), Set.of(), tilgangskontrollService, norg2Client); + when(tilgangskontrollService.harSkrivetilgangTilKandidat(beslutter, avtale.getDeltakerFnr())).thenReturn(true); + when(norg2Client.hentOppfølgingsEnhetsnavn(eq("0000"))).thenReturn(new Norg2OppfølgingResponse(0, "0000", "Oslo")); + when(norg2Client.hentOppfølgingsEnhetsnavn(eq("0906"))).thenReturn(new Norg2OppfølgingResponse(906, "0906", "Oslo")); + return beslutter; + } + + public static Maal etMaal() { + Maal maal = new Maal(); + maal.setKategori(MaalKategori.FÅ_JOBB_I_BEDRIFTEN); + maal.setBeskrivelse("Lære butikkarbeid"); + return maal; + } + + public static Inkluderingstilskuddsutgift enInkluderingstilskuddsutgift(Integer beløp, InkluderingstilskuddsutgiftType type) { + Inkluderingstilskuddsutgift i = new Inkluderingstilskuddsutgift(); + i.setId(UUID.randomUUID()); + i.setBeløp(beløp); + i.setType(type); + return i; + } + + public static Inkluderingstilskuddsutgift enInkluderingstilskuddsutgiftUtenId(Integer beløp, InkluderingstilskuddsutgiftType type) { + Inkluderingstilskuddsutgift i = new Inkluderingstilskuddsutgift(); + i.setBeløp(beløp); + i.setType(type); + return i; + } + + public static GodkjentPaVegneGrunn enGodkjentPaVegneGrunn() { + GodkjentPaVegneGrunn paVegneGrunn = new GodkjentPaVegneGrunn(); + paVegneGrunn.setIkkeBankId(true); + return paVegneGrunn; + } + + public static GodkjentPaVegneAvDeltakerOgArbeidsgiverGrunn enGodkjentPaVegneAvDeltakerOgArbeidsgiverGrunn() { + GodkjentPaVegneAvArbeidsgiverGrunn arbeidsgiverGrunn = new GodkjentPaVegneAvArbeidsgiverGrunn(); + arbeidsgiverGrunn.setArenaMigreringArbeidsgiver(true); + GodkjentPaVegneGrunn paVegneGrunn = new GodkjentPaVegneGrunn(); + paVegneGrunn.setIkkeBankId(true); + GodkjentPaVegneAvDeltakerOgArbeidsgiverGrunn paVegneAvDeltakerOgArbeidsgiverGrunn = new GodkjentPaVegneAvDeltakerOgArbeidsgiverGrunn(arbeidsgiverGrunn, paVegneGrunn); + return paVegneAvDeltakerOgArbeidsgiverGrunn; + } + + public static InnloggetArbeidsgiver innloggetArbeidsgiver(Avtalepart avtalepartMedFnr, BedriftNr bedriftNr) { + Map> tilganger = Map.of(bedriftNr, Set.of(Tiltakstype.values())); + AltinnReportee altinnOrganisasjon = new AltinnReportee("Bedriften AS", "Business", bedriftNr.asString(), "BEDR", "Active", null); + return new InnloggetArbeidsgiver(avtalepartMedFnr.getIdentifikator(), Set.of(altinnOrganisasjon), tilganger); + } + + public static InnloggetDeltaker innloggetDeltaker(Avtalepart avtalepartMedFnr) { + return new InnloggetDeltaker(avtalepartMedFnr.getIdentifikator()); + } + + public static Identifikator enIdentifikator() { + return new Identifikator("test-id"); + } + + public static Sporingslogg enHendelse(Avtale avtale) { + return Sporingslogg.nyHendelse(avtale, HendelseType.OPPRETTET); + } + + public static Avtale enAvtaleMedFlereVersjoner() { + Avtale avtale = TestData.enAvtaleMedAltUtfyltGodkjentAvVeileder(); + avtale.endreKontaktInformasjon(new EndreKontaktInformasjon("Atle", + "Jørgensen", + avtale.getGjeldendeInnhold().getDeltakerTlf(), + avtale.getGjeldendeInnhold().getVeilederFornavn(), + avtale.getGjeldendeInnhold().getVeilederEtternavn(), + avtale.getGjeldendeInnhold().getVeilederTlf(), + avtale.getGjeldendeInnhold().getArbeidsgiverFornavn(), + avtale.getGjeldendeInnhold().getArbeidsgiverEtternavn(), + avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), + new RefusjonKontaktperson("Atle", "Jørgensen", "12345678", true)), + TestData.enNavIdent()); + return avtale; + } + + public static Avtale enAvtaleKlarForOppstart() { + Avtale avtale = enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setStartDato(Now.localDate().plusDays(7)); + avtale.getGjeldendeInnhold().setSluttDato(avtale.getGjeldendeInnhold().getStartDato().plusMonths(1)); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvNavIdent(TestData.enNavIdent()); + avtale.getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + avtale.getGjeldendeInnhold().setJournalpostId("1"); + return avtale; + } + + public static BedriftNr etBedriftNr() { + return new BedriftNr("777777777"); + } + + public static NavIdent enNavIdent() { + return new NavIdent("Q987654"); + } + + public static NavIdent enNavIdent2() { + return new NavIdent("B987654"); + } + + public static Veileder enVeileder(NavIdent navIdent) { + return enVeileder(navIdent, mock(VeilarbArenaClient.class)); + } + + public static Veileder enVeileder(NavIdent navIdent, VeilarbArenaClient veilarbArenaClient) { + TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + var veileder = new Veileder( + navIdent, + tilgangskontrollService, + mock(PersondataService.class), + mock(Norg2Client.class), + Set.of(ENHET_OPPFØLGING), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), any())).thenReturn(true); + return veileder; + } + + public static Veileder enVeileder(Avtale avtale, PersondataService persondataService) { + TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + var veileder = new Veileder( + avtale.getVeilederNavIdent(), + tilgangskontrollService, + persondataService, + mock(Norg2Client.class), + Set.of(ENHET_OPPFØLGING), + new SlettemerkeProperties(), + false, + mock(VeilarbArenaClient.class) + ); + when(tilgangskontrollService.harSkrivetilgangTilKandidat( + veileder, + avtale.getDeltakerFnr()) + ).thenReturn(true); + return veileder; + } + + public static PdlRespons enPdlrespons(boolean harKode6) { + Adressebeskyttelse[] adressebeskyttelser = new Adressebeskyttelse[1]; + if (harKode6) { + adressebeskyttelser[0] = new Adressebeskyttelse("STRENGT_FORTROLIG"); + } + + HentPerson hentPerson = new HentPerson(adressebeskyttelser, new Navn[] { new Navn("Donald", null, "Duck") }); + return new PdlRespons(new Data(hentPerson, null, new HentGeografiskTilknytning(null, "030101", null, null))); + } + + public static TilskuddPeriode enTilskuddPeriode() { + return TestData.enLonnstilskuddAvtaleGodkjentAvVeileder().getTilskuddPeriode().first(); + } + + public static EndreTilskuddsberegning enEndreTilskuddsberegning() { + double otpSats = 0.048; + BigDecimal feriepengesats = new BigDecimal("0.166"); + BigDecimal arbeidsgiveravgift = BigDecimal.ZERO; + int manedslonn = 44444; + return EndreTilskuddsberegning.builder().otpSats(otpSats).feriepengesats(feriepengesats).arbeidsgiveravgift(arbeidsgiveravgift).manedslonn(manedslonn).build(); + } + + public static Avtale enArbeidstreningAvtaleGodkjentAvVeileder() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + avtale.endreAvtale(avtale.getSistEndret(), endringPåAlleLønnstilskuddFelter(), Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(Now.localDateTime()); + avtale.getGjeldendeInnhold().setIkrafttredelsestidspunkt(Now.localDateTime()); + avtale.getGjeldendeInnhold().setJournalpostId("1"); + return avtale; + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddPeriodeTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddPeriodeTest.java new file mode 100644 index 000000000..1553d5a15 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/TilskuddPeriodeTest.java @@ -0,0 +1,91 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import static no.nav.tag.tiltaksgjennomforing.AssertFeilkode.assertFeilkode; +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.util.EnumSet; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +class TilskuddPeriodeTest { + @Test + void behandle_flere_ganger__etter_godkjenning() { + TilskuddPeriode tilskuddPeriode = TestData.enTilskuddPeriode(); + tilskuddPeriode.godkjenn(TestData.enNavIdent(), TestData.ENHET_GEOGRAFISK.getVerdi()); + assertFeilkode(Feilkode.TILSKUDDSPERIODE_ER_ALLEREDE_BEHANDLET, () -> tilskuddPeriode.godkjenn(TestData.enNavIdent(), TestData.ENHET_GEOGRAFISK.getVerdi())); + assertFeilkode(Feilkode.TILSKUDDSPERIODE_ER_ALLEREDE_BEHANDLET, () -> tilskuddPeriode.avslå(TestData.enNavIdent(), EnumSet.of(Avslagsårsak.FEIL_I_FAKTA), "Faktafeil")); + } + + @Test + void behandle_flere_ganger__etter_avslag() { + TilskuddPeriode tilskuddPeriode = TestData.enTilskuddPeriode(); + tilskuddPeriode.avslå(TestData.enNavIdent(), EnumSet.of(Avslagsårsak.FEIL_I_FAKTA), "Faktafeil"); + assertFeilkode(Feilkode.TILSKUDDSPERIODE_ER_ALLEREDE_BEHANDLET, () -> tilskuddPeriode.godkjenn(TestData.enNavIdent(), TestData.ENHET_GEOGRAFISK.getVerdi())); + assertFeilkode(Feilkode.TILSKUDDSPERIODE_ER_ALLEREDE_BEHANDLET, () -> tilskuddPeriode.avslå(TestData.enNavIdent(), EnumSet.of(Avslagsårsak.FEIL_I_FAKTA), "Faktafeil")); + } + + @Test + void godkjenn_setter_riktige_felter() { + TilskuddPeriode tilskuddPeriode = TestData.enTilskuddPeriode(); + NavIdent beslutter = TestData.enNavIdent(); + tilskuddPeriode.godkjenn(beslutter, TestData.ENHET_GEOGRAFISK.getVerdi()); + assertThat(tilskuddPeriode.getGodkjentAvNavIdent()).isEqualTo(beslutter); + assertThat(tilskuddPeriode.getGodkjentTidspunkt()).isNotNull(); + assertThat(tilskuddPeriode.getStatus()).isEqualTo(TilskuddPeriodeStatus.GODKJENT); + } + + @Test + void avslå_setter_riktige_felter() { + TilskuddPeriode tilskuddPeriode = TestData.enTilskuddPeriode(); + NavIdent beslutter = TestData.enNavIdent(); + tilskuddPeriode.avslå(beslutter, EnumSet.of(Avslagsårsak.FEIL_I_FAKTA, Avslagsårsak.ANNET), "Feil i fakta"); + assertThat(tilskuddPeriode.getAvslåttAvNavIdent()).isEqualTo(beslutter); + assertThat(tilskuddPeriode.getAvslåttTidspunkt()).isNotNull(); + assertThat(tilskuddPeriode.getStatus()).isEqualTo(TilskuddPeriodeStatus.AVSLÅTT); + assertThat(tilskuddPeriode.getAvslagsårsaker()).contains(Avslagsårsak.FEIL_I_FAKTA, Avslagsårsak.ANNET); + } + + @Test + void avslå__uten_årsaker() { + TilskuddPeriode tilskuddPeriode = TestData.enTilskuddPeriode(); + NavIdent beslutter = TestData.enNavIdent(); + assertFeilkode(Feilkode.TILSKUDDSPERIODE_INGEN_AVSLAGSAARSAKER, () -> tilskuddPeriode.avslå(beslutter, EnumSet.noneOf(Avslagsårsak.class), "Feil i fakta")); + } + + @Test + void avslå__uten_forklaring() { + TilskuddPeriode tilskuddPeriode = TestData.enTilskuddPeriode(); + NavIdent beslutter = TestData.enNavIdent(); + assertFeilkode(Feilkode.TILSKUDDSPERIODE_AVSLAGSFORKLARING_PAAKREVD, () -> tilskuddPeriode.avslå(beslutter, EnumSet.of(Avslagsårsak.FEIL_I_REGELFORSTÅELSE), " ")); + } + + @Test + void sjekker_utbetalt_status() { + TilskuddPeriode tilskuddPeriode = TestData.enTilskuddPeriode(); + tilskuddPeriode.setRefusjonStatus(RefusjonStatus.UTBETALT); + assertThat(tilskuddPeriode.erUtbetalt()).isTrue(); + } + + @Test + @Disabled("Tester kun midlertidig sperre for å ikke kunne godkjenne tilskudd for neste år.") + void godkjenn__skal_ikke_kunne_godkjenne_neste_års_tilskuddsperiode() { + //TODO: Dette er en test av en midlertidig sperre. + Now.fixedDate(LocalDate.of(2022, 10, 15)); + TilskuddPeriode tilskuddPeriode = TestData.enTilskuddPeriode(); + tilskuddPeriode.setStartDato(LocalDate.of(2023, 1, 1)); + tilskuddPeriode.setSluttDato(LocalDate.of(2023, 1, 31)); + + assertFeilkode(Feilkode.TILSKUDDSPERIODE_BEHANDLE_FOR_TIDLIG, () -> tilskuddPeriode.godkjenn(TestData.enNavIdent(), TestData.ENHET_GEOGRAFISK.getVerdi())); + + Now.fixedDate(LocalDate.of(2022, 12, 15)); + assertFeilkode(Feilkode.TILSKUDDSPERIODE_BEHANDLE_FOR_TIDLIG, () -> tilskuddPeriode.godkjenn(TestData.enNavIdent(), TestData.ENHET_GEOGRAFISK.getVerdi())); + + Now.fixedDate(LocalDate.of(2023, 1, 1)); + tilskuddPeriode.godkjenn(TestData.enNavIdent(), TestData.ENHET_GEOGRAFISK.getVerdi()); + + Now.resetClock(); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/VarigLonnstilskuddStrategyTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/VarigLonnstilskuddStrategyTest.java new file mode 100644 index 000000000..4b618faad --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/VarigLonnstilskuddStrategyTest.java @@ -0,0 +1,86 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import static no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype.VARIG_LONNSTILSKUDD; +import static org.assertj.core.api.Assertions.assertThat; + +import no.nav.tag.tiltaksgjennomforing.AssertFeilkode; +import no.nav.tag.tiltaksgjennomforing.avtale.RefusjonKontaktperson.Fields; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class VarigLonnstilskuddStrategyTest { + + private AvtaleInnhold avtaleInnhold; + private AvtaleInnholdStrategy strategy; + + @BeforeEach + public void setUp() { + avtaleInnhold = new AvtaleInnhold(); + strategy = AvtaleInnholdStrategyFactory.create(avtaleInnhold, VARIG_LONNSTILSKUDD); + } + + @Test + void test_at_feil_når_familietilknytning_ikke_er_fylt_ut() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setHarFamilietilknytning(true); + endreAvtale.setFamilietilknytningForklaring(null); + strategy.endre(endreAvtale); + + assertThat(strategy.alleFelterSomMåFyllesUt()).containsKey(AvtaleInnhold.Fields.familietilknytningForklaring); + } + + @Test + void test_at_ikke_feil_når_ikke_forklaring_og_nei_på_familietilknytning() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setHarFamilietilknytning(false); + endreAvtale.setFamilietilknytningForklaring(null); + strategy.endre(endreAvtale); + + assertThat(strategy.alleFelterSomMåFyllesUt()).doesNotContainKey(AvtaleInnhold.Fields.familietilknytningForklaring); + } + + @Test + void test_at_ikke_feil_når_alt_fylt_ut_og_har_familietilknytning() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setHarFamilietilknytning(true); + endreAvtale.setFamilietilknytningForklaring("En god forklaring"); + strategy.endre(endreAvtale); + } + + @Test + public void sjekk_riktig_otp_sats() { + strategy = AvtaleInnholdStrategyFactory.create(avtaleInnhold, VARIG_LONNSTILSKUDD); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setOtpSats(0.301); + AssertFeilkode.assertFeilkode(Feilkode.FEIL_OTP_SATS, () -> strategy.endre(endreAvtale)); + endreAvtale.setOtpSats(-0.001); + AssertFeilkode.assertFeilkode(Feilkode.FEIL_OTP_SATS, () -> strategy.endre(endreAvtale)); + + endreAvtale.setOtpSats(0.0); + strategy.endre(endreAvtale); + + endreAvtale.setOtpSats(null); + strategy.endre(endreAvtale); + + endreAvtale.setOtpSats(0.3); + strategy.endre(endreAvtale); + } + + @Test + public void sjekk_riktig_refusjon_kontakt_person_må_fylles_ut() { + strategy = AvtaleInnholdStrategyFactory.create(avtaleInnhold, VARIG_LONNSTILSKUDD); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setRefusjonKontaktperson(new RefusjonKontaktperson(null, "Duck","12345678", true)); + strategy.endre(endreAvtale); + assertThat(strategy.alleFelterSomMåFyllesUt()).extractingByKey(Fields.refusjonKontaktpersonFornavn).isNull(); + } + + @Test + void lonnstilskuddsprosent_må_fylles_ut() { + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setLonnstilskuddProsent(null); + strategy.endre(endreAvtale); + assertThat(strategy.alleFelterSomMåFyllesUt()).extractingByKey(AvtaleInnhold.Fields.lonnstilskuddProsent).isNull(); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/VeilederTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/VeilederTest.java new file mode 100644 index 000000000..c9a27b793 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/VeilederTest.java @@ -0,0 +1,463 @@ +package no.nav.tag.tiltaksgjennomforing.avtale; + +import no.nav.tag.tiltaksgjennomforing.autorisasjon.SlettemerkeProperties; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.TilgangskontrollService; +import no.nav.tag.tiltaksgjennomforing.enhet.*; +import no.nav.tag.tiltaksgjennomforing.exceptions.*; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.NavEnhet; +import no.nav.tag.tiltaksgjennomforing.persondata.PdlRespons; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import static no.nav.tag.tiltaksgjennomforing.AssertFeilkode.assertFeilkode; +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.avtalerMedTilskuddsperioder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +public class VeilederTest { + + @Test + public void godkjennAvtale__kan_ikke_godkjenne_foerst() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Veileder veileder = TestData.enVeileder(avtale); + assertThatThrownBy(() -> veileder.godkjennAvtale(avtale.getSistEndret(), avtale)) + .isExactlyInstanceOf(VeilederSkalGodkjenneSistException.class); + } + + @Test + public void godkjennAvtale__kan_godkjenne_sist() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + Veileder veileder = TestData.enVeileder(avtale); + veileder.godkjennAvtale(avtale.getSistEndret(), avtale); + assertThat(avtale.erGodkjentAvVeileder()).isTrue(); + } + + @Test + public void godkjennAvtale__kan_ikke_godkjenne_kode6() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + PersondataService persondataService = mock(PersondataService.class); + when(persondataService.erKode6(avtale.getDeltakerFnr())).thenReturn(true); + Veileder veileder = TestData.enVeileder(avtale, persondataService); + assertThatThrownBy(() -> veileder.godkjennAvtale(avtale.getSistEndret(), avtale)) + .isExactlyInstanceOf(KanIkkeGodkjenneAvtalePåKode6Exception.class); + } + + @Test + public void godkjennForVeilederOgDeltaker__kan_ikke_godkjenne_kode6() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + PersondataService persondataService = mock(PersondataService.class); + when(persondataService.erKode6(avtale.getDeltakerFnr())).thenReturn(true); + Veileder veileder = TestData.enVeileder(avtale, persondataService); + assertThatThrownBy(() -> veileder.godkjennForVeilederOgDeltaker(TestData.enGodkjentPaVegneGrunn(), avtale)) + .isExactlyInstanceOf(KanIkkeGodkjenneAvtalePåKode6Exception.class); + } + + @Test + public void opphevGodkjenninger__kan_ikke_oppheve_godkjenninger_når_avtale_er_inngått() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + avtale.getGjeldendeInnhold().setAvtaleInngått(LocalDateTime.now()); + Veileder veileder = TestData.enVeileder(avtale); + assertFeilkode( + Feilkode.KAN_IKKE_OPPHEVE_GODKJENNINGER_VED_INNGAATT_AVTALE, + () -> veileder.opphevGodkjenninger(avtale) + ); + } + + @Test + public void opphevGodkjenninger__kan_oppheve_godkjenninger_hvis_alle_parter_har_godkjent_men_ikke_inngått() { + Now.fixedDate(LocalDate.of(2021, 6, 1)); + Avtale avtale = TestData.enSommerjobbAvtaleGodkjentAvVeileder(); + Veileder veileder = TestData.enVeileder(avtale); + assertThat( + avtale.godkjentAvArbeidsgiver() != null && + avtale.godkjentAvDeltaker() != null && + avtale.godkjentAvVeileder() != null + ).isTrue(); + assertThat(avtale.erAvtaleInngått()).isFalse(); + + veileder.opphevGodkjenninger(avtale); + assertThat( + avtale.godkjentAvArbeidsgiver() == null && + avtale.godkjentAvDeltaker() == null && + avtale.godkjentAvVeileder() == null + ).isTrue(); + Now.resetClock(); + } + + @Test + public void opphevGodkjenninger__kan_ikke_oppheve_arbeidstrening_hvis_alle_parter_har_godkjent() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + Veileder veileder = TestData.enVeileder(avtale); + avtale.endreAvtale( + Instant.now(), + TestData.endringPåAlleArbeidstreningFelter(), + Avtalerolle.VEILEDER, + avtalerMedTilskuddsperioder + ); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + veileder.godkjennForVeilederOgDeltaker(TestData.enGodkjentPaVegneGrunn(), avtale); + + assertFeilkode( + Feilkode.KAN_IKKE_OPPHEVE_GODKJENNINGER_VED_INNGAATT_AVTALE, + () -> veileder.opphevGodkjenninger(avtale) + ); + } + + @Test + public void opphevgodkjenninger__kan_ikke_oppheve_hvis_første_tilskuddsperiode_er_godkjent() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + + // Gi veileder tilgang til deltaker + TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + Veileder veileder = new Veileder( + avtale.getVeilederNavIdent(), + tilgangskontrollService, + mock(PersondataService.class), + mock(Norg2Client.class), + Set.of(new NavEnhet("4802", "Trysil")), + mock(SlettemerkeProperties.class), + + false, + mock(VeilarbArenaClient.class)); + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), any(Fnr.class))) + .thenReturn(true); + + avtale.endreAvtale( + Instant.now(), + TestData.endringPåAlleLønnstilskuddFelter(), + Avtalerolle.VEILEDER, + avtalerMedTilskuddsperioder + ); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + veileder.godkjennForVeilederOgDeltaker(TestData.enGodkjentPaVegneGrunn(), avtale); + + assertThat(avtale.erAvtaleInngått()).isFalse(); + + veileder.opphevGodkjenninger(avtale); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + veileder.godkjennForVeilederOgDeltaker(TestData.enGodkjentPaVegneGrunn(), avtale); + + Beslutter beslutter = TestData.enBeslutter(avtale); + beslutter.godkjennTilskuddsperiode(avtale, "0000"); + + assertThat(avtale.erAvtaleInngått()).isTrue(); + assertFeilkode( + Feilkode.KAN_IKKE_OPPHEVE_GODKJENNINGER_VED_INNGAATT_AVTALE, + () -> veileder.opphevGodkjenninger(avtale) + ); + } + + + @Test + public void annullerAvtale__kan_annuller_avtale_etter_veiledergodkjenning() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + Veileder veileder = TestData.enVeileder(avtale); + veileder.annullerAvtale(avtale.getSistEndret(), "enGrunn", avtale); + assertThat(avtale.getAnnullertTidspunkt()).isNotNull(); + assertThat(avtale.getAnnullertGrunn()).isEqualTo("enGrunn"); + } + + @Test + public void annullerAvtale__kan_annullere_avtale_foer_veiledergodkjenning() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setGodkjentAvDeltaker(Now.localDateTime()); + avtale.getGjeldendeInnhold().setGodkjentAvArbeidsgiver(Now.localDateTime()); + Veileder veileder = TestData.enVeileder(avtale); + veileder.annullerAvtale(avtale.getSistEndret(), "enGrunn", avtale); + assertThat(avtale.getAnnullertTidspunkt()).isNotNull(); + assertThat(avtale.getAnnullertGrunn()).isEqualTo("enGrunn"); + } + + @Test + public void overtarAvtale() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + NavIdent gammelVeileder = avtale.getVeilederNavIdent(); + Veileder nyVeileder = TestData.enVeileder(new NavIdent("J987654")); + + nyVeileder.overtaAvtale(avtale); + assertThat(gammelVeileder).isNotEqualTo(nyVeileder.getIdentifikator()); + assertThat(avtale.getVeilederNavIdent()).isEqualTo(nyVeileder.getIdentifikator()); + } + + @Test + public void overta_avtale_hvor_veileder_allerede_er_satt_og_skal_bare_overskrive_oppfølgningsstatus_når_avtalen_endres() throws InterruptedException { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + + VeilarbArenaClient veilarbArenaClient = Mockito.spy(new VeilarbArenaClient(null, null)); + Veileder nyVeileder = TestData.enVeileder(new NavIdent("J987654"),veilarbArenaClient); + + Oppfølgingsstatus nyOppfølgingsstatusSomSkalIkkeSettes = new Oppfølgingsstatus( + Formidlingsgruppe.ARBEIDSSOKER, + Kvalifiseringsgruppe.SPESIELT_TILPASSET_INNSATS, + "0906" + ); + Mockito.doReturn(nyOppfølgingsstatusSomSkalIkkeSettes).when(veilarbArenaClient).hentOppfølgingStatus(Mockito.anyString()); + + nyVeileder.hentOppfølgingFraArena(avtale,veilarbArenaClient ); + + assertThat(avtale.getKvalifiseringsgruppe()).isEqualTo(avtale.getKvalifiseringsgruppe()); + + //SKal kunne endre oppfølgningsstatus på endre avtale + Oppfølgingsstatus nyOppfølgingsstatusSomSkalSettes = new Oppfølgingsstatus( + Formidlingsgruppe.ARBEIDSSOKER, + Kvalifiseringsgruppe.SPESIELT_TILPASSET_INNSATS, + "0906" + ); + Mockito.doReturn(nyOppfølgingsstatusSomSkalSettes).when(veilarbArenaClient).hentOppfølgingStatus(Mockito.anyString()); + nyVeileder.oppdatereOppfølgingStatusVedEndreAvtale(avtale); + assertThat(avtale.getKvalifiseringsgruppe()).isEqualTo(nyOppfølgingsstatusSomSkalSettes.getKvalifiseringsgruppe()); + } + + @Test + public void overtarAvtale_uten_tilskuddsprosent__verifiser_blir_satt_og_beregnet() { + Avtale avtale = Avtale.arbeidsgiverOppretterAvtale( + new OpprettAvtale( + TestData.etFodselsnummer(), + TestData.etBedriftNr(), + Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD + ) + ); + EndreAvtale endreAvtale = TestData.endringPåAlleLønnstilskuddFelter(); + endreAvtale.setLonnstilskuddProsent(null); + avtale.getGjeldendeInnhold().setSumLonnstilskudd(null); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + arbeidsgiver.endreAvtale(Now.instant(), endreAvtale, avtale, EnumSet.of(avtale.getTiltakstype())); + Veileder nyVeileder = TestData.enVeileder(new NavIdent("J987654")); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS); + avtale.setFormidlingsgruppe(Formidlingsgruppe.ARBEIDSSOKER); + avtale.getGjeldendeInnhold().setLonnstilskuddProsent(avtale.getKvalifiseringsgruppe() + .finnLonntilskuddProsentsatsUtifraKvalifiseringsgruppe(40, 60)); + assertThat(avtale.getGjeldendeInnhold().getSumLonnstilskudd()).isNull(); + + nyVeileder.overtaAvtale(avtale); + + assertThat(avtale.getGjeldendeInnhold().getSumLonnstilskudd()).isNotNull(); + } + + @Test + public void overtarAvtale__feil_hvis_samme_ident() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Veileder veileder = TestData.enVeileder(avtale); + assertThatThrownBy(() -> veileder.overtaAvtale(avtale)).isExactlyInstanceOf(ErAlleredeVeilederException.class); + } + + @Test + public void overtaAvtale__skal_genere_tilskuddsperioder_hvis_ufordelt() { + Avtale avtale = TestData.enAvtaleOpprettetAvArbeidsgiver(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + arbeidsgiver.endreAvtale( + Instant.now(), + TestData.endringPåAlleLønnstilskuddFelter(), + avtale, + EnumSet.of(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD) + ); + + assertThat(avtale.getTilskuddPeriode()).isEmpty(); + + Veileder veileder = TestData.enVeileder(new NavIdent("Z123456")); + + //Tilsvarende operasjon som gjøres fra endepunketet overta avtalecontrolleren + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS); + avtale.getGjeldendeInnhold().setLonnstilskuddProsent(60); + veileder.overtaAvtale(avtale); + + assertThat(avtale.getTilskuddPeriode()).isNotEmpty(); + + + + + } + + @Test + public void oprettAvtale__setter_startverdier_på_avtale() { + final Fnr fnr = TestData.etFodselsnummer(); + final NavIdent navIdent = new NavIdent("Q987654"); + final NavEnhet navEnhet = TestData.ENHET_GEOGRAFISK; + OpprettAvtale opprettAvtale = new OpprettAvtale( + TestData.etFodselsnummer(), + TestData.etBedriftNr(), + Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD + ); + + final TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + final PersondataService persondataService = mock(PersondataService.class); + final Norg2Client norg2Client = mock(Norg2Client.class); + final PdlRespons pdlRespons = TestData.enPdlrespons(false); + final VeilarbArenaClient veilarbArenaClient = mock(VeilarbArenaClient.class); + + Veileder veileder = new Veileder( + navIdent, + tilgangskontrollService, + persondataService, + norg2Client, + Set.of(navEnhet), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), any())).thenReturn(true); + when(persondataService.hentPersondata(fnr)).thenReturn(pdlRespons); + when(persondataService.erKode6(pdlRespons)).thenCallRealMethod(); + when(norg2Client.hentGeografiskEnhet(pdlRespons.getData().getHentGeografiskTilknytning().getGtBydel())) + .thenReturn(new Norg2GeoResponse( + TestData.ENHET_GEOGRAFISK.getNavn(), + TestData.ENHET_GEOGRAFISK.getVerdi() + )); + when(veilarbArenaClient.hentOppfølgingsEnhet(fnr.asString())).thenReturn(navEnhet.getVerdi()); + when(norg2Client.hentGeografiskEnhet(pdlRespons.getData().getHentGeografiskTilknytning().getGtBydel())) + .thenReturn(new Norg2GeoResponse( + TestData.ENHET_GEOGRAFISK.getNavn(), + TestData.ENHET_GEOGRAFISK.getVerdi() + )); + + Avtale avtale = veileder.opprettAvtale(opprettAvtale); + + assertThat(avtale.getVeilederNavIdent()).isEqualTo(TestData.enNavIdent()); + assertThat(avtale.getGjeldendeInnhold().getDeltakerFornavn()).isEqualTo("Donald"); + assertThat(avtale.getGjeldendeInnhold().getDeltakerEtternavn()).isEqualTo("Duck"); + assertThat(avtale.getEnhetGeografisk()).isEqualTo(TestData.ENHET_GEOGRAFISK.getVerdi()); + } + + @Test + public void opprettAvtale__skal_ikke_slettemerkes() { + final Fnr fnr = TestData.etFodselsnummer(); + final NavIdent navIdent = new NavIdent("Z123456"); + final PdlRespons pdlRespons = TestData.enPdlrespons(false); + final NavEnhet navEnhet = TestData.ENHET_OPPFØLGING; + + OpprettAvtale opprettAvtale = new OpprettAvtale( + TestData.etFodselsnummer(), + TestData.etBedriftNr(), + Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD + ); + + final VeilarbArenaClient veilarbArenaClient = mock(VeilarbArenaClient.class); + final Norg2Client norg2Client = mock(Norg2Client.class); + final PersondataService persondataService = mock(PersondataService.class); + final TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + + Veileder veileder = new Veileder( + navIdent, + tilgangskontrollService, + persondataService, + norg2Client, + Set.of(navEnhet), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), any())).thenReturn(true); + when(persondataService.hentPersondata(fnr)).thenReturn(pdlRespons); + when(veilarbArenaClient.hentOppfølgingsEnhet(fnr.asString())).thenReturn(navEnhet.getVerdi()); + when(norg2Client.hentGeografiskEnhet(pdlRespons.getData().getHentGeografiskTilknytning().getGtBydel())) + .thenReturn( + new Norg2GeoResponse(TestData.ENHET_GEOGRAFISK.getNavn(), + TestData.ENHET_GEOGRAFISK.getVerdi()) + ); + + + + Avtale avtale = veileder.opprettAvtale(opprettAvtale); + assertThat(avtale.isSlettemerket()).isFalse(); + } + + @Test + public void slettemerke__avtale_med_tilgang() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + NavIdent navIdent = new NavIdent("Z123456"); + + TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + SlettemerkeProperties slettemerkeProperties = new SlettemerkeProperties(); + slettemerkeProperties.setIdent(List.of(navIdent)); + Veileder veileder = new Veileder( + navIdent, + tilgangskontrollService, + mock(PersondataService.class), + mock(Norg2Client.class), + Set.of(new NavEnhet("4802", "Trysil")), + slettemerkeProperties, + false, + mock(VeilarbArenaClient.class) + ); + + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), eq(avtale.getDeltakerFnr()))) + .thenReturn(true); + + veileder.slettemerk(avtale); + assertThat(avtale.isSlettemerket()).isTrue(); + } + + @Test + public void slettemerke__avtale_uten_tilgang() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + + NavIdent navIdent = new NavIdent("X123456"); + + TilgangskontrollService tilgangskontrollService = mock(TilgangskontrollService.class); + + SlettemerkeProperties slettemerkeProperties = new SlettemerkeProperties(); + slettemerkeProperties.setIdent(List.of(new NavIdent("Z123456"))); + Veileder veileder = new Veileder( + navIdent, + tilgangskontrollService, + mock(PersondataService.class), + mock(Norg2Client.class), + Set.of(new NavEnhet("4802", "Trysil")), + slettemerkeProperties, + false, + mock(VeilarbArenaClient.class) + ); + when(tilgangskontrollService.harSkrivetilgangTilKandidat(eq(veileder), eq(avtale.getDeltakerFnr()))).thenReturn(true); + assertThatThrownBy(() -> veileder.slettemerk(avtale)).isExactlyInstanceOf(IkkeAdminTilgangException.class); + } + + @Test + public void slettemerket_ikke_tilgang_til_avtale() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + avtale.setSlettemerket(true); + Veileder veileder = TestData.enVeileder(avtale); + assertThat(veileder.harTilgang(avtale)).isFalse(); + } + + @Test + public void opprettelse_av_tiltak_med_forskjellige_kvalifiseringskoder(){ + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Oppfølgingsstatus oppfølgingsstatus = new Oppfølgingsstatus( + Formidlingsgruppe.ARBEIDSSOKER, + Kvalifiseringsgruppe.IKKE_VURDERT, + "0906" + ); + VeilarbArenaClient veilarbArenaClient = Mockito.spy(new VeilarbArenaClient(null, null)); + Mockito.doReturn(oppfølgingsstatus).when(veilarbArenaClient).hentOppfølgingStatus(Mockito.anyString()); + + assertThatThrownBy(() -> veilarbArenaClient.sjekkOppfølingStatus(avtale)) + .isExactlyInstanceOf(FeilkodeException.class) + .hasMessage(Feilkode.KVALIFISERINGSGRUPPE_IKKE_RETTIGHET.name()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/SommerjobbStartOgSluttDatoStrategyTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/SommerjobbStartOgSluttDatoStrategyTest.java new file mode 100644 index 000000000..cb4d47e32 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/avtale/startOgSluttDatoStrategy/SommerjobbStartOgSluttDatoStrategyTest.java @@ -0,0 +1,97 @@ +package no.nav.tag.tiltaksgjennomforing.avtale.startOgSluttDatoStrategy; + +import static no.nav.tag.tiltaksgjennomforing.AssertFeilkode.assertFeilkode; + +import java.time.LocalDate; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import org.junit.jupiter.api.Test; + +public class SommerjobbStartOgSluttDatoStrategyTest { + + @Test + public void sjekkStartOgSluttDatoEtterregistreringFeilDatoForSommerjobb(){ + LocalDate avtaleStart = LocalDate.of(LocalDate.now().minusYears(2).getYear(), 5,2); + LocalDate avtaleSlutt = LocalDate.of(LocalDate.now().minusYears(2).getYear(),7,28); + + + boolean erAvtaleInngått = true; + boolean erGodkjentForEtterregistrering = true; + SommerjobbStartOgSluttDatoStrategy sommerjobbStartOgSluttDatoStrategy = new SommerjobbStartOgSluttDatoStrategy(); + assertFeilkode(Feilkode.SOMMERJOBB_FOR_TIDLIG, () -> sommerjobbStartOgSluttDatoStrategy.sjekkStartOgSluttDato(avtaleStart, avtaleSlutt ,erGodkjentForEtterregistrering, erAvtaleInngått)); + } + + @Test + public void sjekkStartOgSluttDatoTilbakeITidUtenEtterregistreringInnenForFireUkerSommerjobbPeriode(){ + LocalDate avtaleStart = LocalDate.of(LocalDate.now().minusYears(2).getYear(), 9,1); + LocalDate avtaleSlutt = LocalDate.of(LocalDate.now().minusYears(2).getYear(),9,28); + + boolean erAvtaleInngått = true; + boolean erGodkjentForEtterregistrering = true; + SommerjobbStartOgSluttDatoStrategy sommerjobbStartOgSluttDatoStrategy = new SommerjobbStartOgSluttDatoStrategy(); + assertFeilkode(Feilkode.SOMMERJOBB_FOR_SENT, () -> sommerjobbStartOgSluttDatoStrategy.sjekkStartOgSluttDato(avtaleStart, avtaleSlutt ,erGodkjentForEtterregistrering, erAvtaleInngått)); + + } + + @Test + public void sjekkStartOgSluttDatoTilbakeITidUtenEtterregistreringIKKEInnenForFireUkerSommerjobbPeriode(){ + LocalDate avtaleStart = LocalDate.of(LocalDate.now().minusYears(2).getYear(), 8,31); + LocalDate avtaleSlutt = LocalDate.of(LocalDate.now().minusYears(2).getYear(),10,1); + + + boolean erAvtaleInngått = true; + boolean erGodkjentForEtterregistrering = true; + SommerjobbStartOgSluttDatoStrategy sommerjobbStartOgSluttDatoStrategy = new SommerjobbStartOgSluttDatoStrategy(); + assertFeilkode(Feilkode.SOMMERJOBB_FOR_LANG_VARIGHET, () -> sommerjobbStartOgSluttDatoStrategy.sjekkStartOgSluttDato(avtaleStart, avtaleSlutt ,erGodkjentForEtterregistrering, erAvtaleInngått)); + + } + + @Test + public void sjekkStartOgSluttDato(){ + LocalDate avtaleStart = LocalDate.of(LocalDate.now().getYear(), 6,1); + LocalDate avtaleSlutt = LocalDate.of(LocalDate.now().getYear(),6,20); + + + boolean erAvtaleInngått = true; + boolean erGodkjentForEtterregistrering = true; + SommerjobbStartOgSluttDatoStrategy sommerjobbStartOgSluttDatoStrategy = new SommerjobbStartOgSluttDatoStrategy(); + sommerjobbStartOgSluttDatoStrategy.sjekkStartOgSluttDato(avtaleStart, avtaleSlutt, erGodkjentForEtterregistrering, erAvtaleInngått ); + } + + @Test + public void avtaleSluttDatoErMerEnnFireUkerSent() { + LocalDate avtaleStart = LocalDate.of(LocalDate.now().getYear(),8,31); + LocalDate avtaleSlutt = LocalDate.of(LocalDate.now().getYear(),9,29); + boolean erAvtaleInngått = true; + boolean erGodkjentForEtterregistrering = true; + SommerjobbStartOgSluttDatoStrategy sommerjobbStartOgSluttDatoStrategy = new SommerjobbStartOgSluttDatoStrategy(); + assertFeilkode(Feilkode.SOMMERJOBB_FOR_LANG_VARIGHET, () -> sommerjobbStartOgSluttDatoStrategy.sjekkStartOgSluttDato(avtaleStart, avtaleSlutt ,erGodkjentForEtterregistrering, erAvtaleInngått)); + } + + @Test + public void avtale_periode_kan_ikke_være_over_4_uker() { + LocalDate avtaleStart = LocalDate.of(LocalDate.now().plusYears(1).getYear(),6,1); + LocalDate avtaleSlutt = LocalDate.of(LocalDate.now().plusYears(1).getYear(),6,29); + SommerjobbStartOgSluttDatoStrategy sommerjobbStartOgSluttDatoStrategy = new SommerjobbStartOgSluttDatoStrategy(); + assertFeilkode(Feilkode.SOMMERJOBB_FOR_LANG_VARIGHET, () -> sommerjobbStartOgSluttDatoStrategy.sjekkStartOgSluttDato(avtaleStart, avtaleSlutt, false, false)); + } + + @Test + public void avtale_periode_akkurat_4_uker() { + LocalDate avtaleStart = LocalDate.of(LocalDate.now().plusYears(1).getYear(),6,1); + LocalDate avtaleSlutt = LocalDate.of(LocalDate.now().plusYears(1).getYear(),6,28); + SommerjobbStartOgSluttDatoStrategy sommerjobbStartOgSluttDatoStrategy = new SommerjobbStartOgSluttDatoStrategy(); + sommerjobbStartOgSluttDatoStrategy.sjekkStartOgSluttDato(avtaleStart, avtaleSlutt, false, false); + } + + @Test + public void avtaleStartDatoErFørFørstJuni(){ + LocalDate avtaleStart = LocalDate.of(LocalDate.now().getYear(),5,31); + LocalDate avtaleSlutt = LocalDate.of(LocalDate.now().getYear(),7,14); + boolean erAvtaleInngått = true; + boolean erGodkjentForEtterregistrering = true; + SommerjobbStartOgSluttDatoStrategy sommerjobbStartOgSluttDatoStrategy = new SommerjobbStartOgSluttDatoStrategy(); + assertFeilkode(Feilkode.SOMMERJOBB_FOR_TIDLIG, () -> sommerjobbStartOgSluttDatoStrategy.sjekkStartOgSluttDato(avtaleStart, avtaleSlutt ,erGodkjentForEtterregistrering, erAvtaleInngått)); + + } +} + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseSchemaTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseSchemaTest.java new file mode 100644 index 000000000..e07747315 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/datadeling/AvtaleHendelseSchemaTest.java @@ -0,0 +1,19 @@ +package no.nav.tag.tiltaksgjennomforing.datadeling; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.victools.jsonschema.generator.*; +import org.junit.jupiter.api.Test; + +public class AvtaleHendelseSchemaTest { + + @Test + public void generer_json_schema() { + SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) + .without(Option.DEFINITIONS_FOR_ALL_OBJECTS); + SchemaGeneratorConfig config = configBuilder.build(); + SchemaGenerator generator = new SchemaGenerator(config); + JsonNode jsonSchema = generator.generateSchema(AvtaleMelding.class); + + System.out.println(jsonSchema.toPrettyString()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/domene/autorisasjon/InnloggetBrukerTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/domene/autorisasjon/InnloggetBrukerTest.java new file mode 100644 index 000000000..e69de29bb diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/enhet/VeilarbArenaClientTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/enhet/VeilarbArenaClientTest.java new file mode 100644 index 000000000..077018cab --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/enhet/VeilarbArenaClientTest.java @@ -0,0 +1,99 @@ +package no.nav.tag.tiltaksgjennomforing.enhet; + +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.avtalerMedTilskuddsperioder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.Instant; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.EndreAvtale; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import no.nav.tag.tiltaksgjennomforing.exceptions.Feilkode; +import no.nav.tag.tiltaksgjennomforing.exceptions.FeilkodeException; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles({Miljø.LOCAL, "wiremock"}) +@DirtiesContext +class VeilarbArenaClientTest { + + @Autowired + private VeilarbArenaClient veilarbArenaClient; + + @Test + public void hent_oppfølingsEnhet_fra_arena() { + String oppfølgingsEnhet = veilarbArenaClient.hentOppfølgingsEnhet("22095923112"); + assertThat(oppfølgingsEnhet).isEqualTo("0906"); + } + + @Test + public void finner_ikke_oppfølingsEnhet_for_fnr() { + String oppfølgingsEnhet = veilarbArenaClient.hentOppfølgingsEnhet("33333333333"); + assertThat(oppfølgingsEnhet).isNull(); + String oppfølgingsEnhet2 = veilarbArenaClient.hentOppfølgingsEnhet("11111111111"); + assertThat(oppfølgingsEnhet2).isNotEmpty(); + } + + @Test + public void sjekkAt_kvalifiseringsgruppe_som_faller_utenfor_kaster_exception() { + String fnr_har_kvalifiseringsgruppe_med_kode_IVURD = "02104317386"; + final Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + avtale.setDeltakerFnr(new Fnr(fnr_har_kvalifiseringsgruppe_med_kode_IVURD)); + avtale.setTiltakstype(Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD); + + assertThatThrownBy(() -> veilarbArenaClient.sjekkOgHentOppfølgingStatus(avtale)) + .isExactlyInstanceOf(FeilkodeException.class) + .hasMessage(Feilkode.KVALIFISERINGSGRUPPE_IKKE_RETTIGHET.name()); + } + + @Test + public void hent_og_sjekk_oppfølging_status() { + String fnr_har_riktig_kvalifisering_og_formidlingskode = "00000000000"; + final Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + avtale.setDeltakerFnr(new Fnr(fnr_har_riktig_kvalifisering_og_formidlingskode)); + + Oppfølgingsstatus oppfølgingsstatus = veilarbArenaClient.sjekkOgHentOppfølgingStatus(avtale); + assertThat(oppfølgingsstatus.getFormidlingsgruppe().getKode()).isEqualTo(("ARBS")); + assertThat(oppfølgingsstatus.getKvalifiseringsgruppe().getKvalifiseringskode()).isEqualTo(("VARIG")); + assertThat(oppfølgingsstatus.getOppfolgingsenhet()).isEqualTo(("0906")); + } + + @Test + public void sjekk_at_lonnstilskuddsprosent_blir_satt_paa_midlertidiglonnstilskudd_ved_AvtaleInnhold_constructor() { + final Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setLonnstilskuddProsent(null); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + + Oppfølgingsstatus oppfølgingsstatus = veilarbArenaClient.sjekkOgHentOppfølgingStatus(avtale); + avtale.setEnhetOppfolging(oppfølgingsstatus.getOppfolgingsenhet()); + avtale.setKvalifiseringsgruppe(oppfølgingsstatus.getKvalifiseringsgruppe()); + avtale.setFormidlingsgruppe(oppfølgingsstatus.getFormidlingsgruppe()); + avtale.endreAvtale(Instant.now(),new EndreAvtale(), Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + assertThat(avtale.getGjeldendeInnhold().getLonnstilskuddProsent()).isNotNull(); + assertThat(avtale.getGjeldendeInnhold().getLonnstilskuddProsent()).isEqualTo(60); + + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS); + avtale.endreAvtale(Instant.now(),new EndreAvtale(), Avtalerolle.VEILEDER, avtalerMedTilskuddsperioder); + + assertThat(avtale.getGjeldendeInnhold().getLonnstilskuddProsent()).isEqualTo(40); + } + + @Test + public void hent_oppfølging_status() { + Oppfølgingsstatus oppfølgingStatus = veilarbArenaClient.hentOppfølgingStatus("01056210306"); + + assertThat(oppfølgingStatus.getFormidlingsgruppe().getKode()).isEqualTo(("ARBS")); + assertThat(oppfølgingStatus.getKvalifiseringsgruppe().getKvalifiseringskode()).isEqualTo(("VARIG")); + assertThat(oppfølgingStatus.getOppfolgingsenhet()).isEqualTo(("0906")); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/ByEnvironmentStrategyTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/ByEnvironmentStrategyTest.java new file mode 100644 index 000000000..16f6afb1f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/ByEnvironmentStrategyTest.java @@ -0,0 +1,36 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static no.nav.tag.tiltaksgjennomforing.Miljø.*; +import static org.assertj.core.api.Assertions.assertThat; + +public class ByEnvironmentStrategyTest { + @Test + public void featureIsEnabledWhenEnvironmentInList() { + assertThat(new ByEnvironmentStrategy(LOCAL).isEnabled(Map.of("miljø", "local,dev-fss"))).isEqualTo(true); + } + + @Test + public void featureIsEnabledWhenLocalEnvironmentInList() { + assertThat(new ByEnvironmentStrategy(LOCAL).isEnabled(Map.of("miljø", "local"))).isEqualTo(true); + } + + @Test + public void featureIsDisabledWhenEnvironmentNotInList() { + assertThat(new ByEnvironmentStrategy(PROD_FSS).isEnabled(Map.of("miljø", "local"))).isEqualTo(false); + } + + @Test + public void skalReturnereFalseHvisParametreErNull() { + assertThat(new ByEnvironmentStrategy(DEV_FSS).isEnabled(null)).isEqualTo(false); + } + + @Test + public void skalReturnereFalseHvisMiljøIkkeErSatt() { + assertThat(new ByEnvironmentStrategy(DEV_FSS).isEnabled(new HashMap<>())).isEqualTo(false); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/ByOrgnummerStrategyTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/ByOrgnummerStrategyTest.java new file mode 100644 index 000000000..114ba911e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/ByOrgnummerStrategyTest.java @@ -0,0 +1,76 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles; + +import io.getunleash.UnleashContext; +import no.nav.arbeidsgiver.altinnrettigheter.proxy.klient.model.AltinnReportee; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.altinntilgangsstyring.AltinnTilgangsstyringService; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class ByOrgnummerStrategyTest { + + private UnleashContext unleashContext = UnleashContext.builder().userId("12345678901").build(); + + @Mock + AltinnTilgangsstyringService altinnTilgangsstyringService; + + @Test + public void skal_være_enablet_hvis_bruker_tilhører_organisasjon() { + Fnr fnr = new Fnr("12345678901"); + Set orgSet = new HashSet<>(); + orgSet.add(new AltinnReportee("", "AS", null, "999999999", "", "")); + + when(altinnTilgangsstyringService.hentAltinnOrganisasjoner(eq(fnr), any())).thenReturn(orgSet); + assertThat(new ByOrgnummerStrategy(altinnTilgangsstyringService).isEnabled(Map.of(ByOrgnummerStrategy.UNLEASH_PARAMETER_ORGNUMRE, "999999999"), unleashContext)).isTrue(); + } + + @Test + public void skal_være_disablet_hvis_bruker_ikke_tilhører_organisasjon() { + Fnr fnr = new Fnr("12345678901"); + Set orgSet = new HashSet<>(); + orgSet.add(new AltinnReportee("", "AS", null, "999999998", "", "")); + + when(altinnTilgangsstyringService.hentAltinnOrganisasjoner(fnr, () -> "")).thenReturn(orgSet); + assertThat(new ByOrgnummerStrategy(altinnTilgangsstyringService).isEnabled(Map.of(ByOrgnummerStrategy.UNLEASH_PARAMETER_ORGNUMRE, "999999999"), unleashContext)).isFalse(); + } + + @Test + public void navIdent_skal_returnere_false() { + UnleashContext unleashContext = UnleashContext.builder().userId("J154200").build(); + Set orgSet = new HashSet<>(); + orgSet.add(new AltinnReportee("", "AS", null, "999999998", "", "")); + assertThat(new ByOrgnummerStrategy(altinnTilgangsstyringService).isEnabled(Map.of(ByOrgnummerStrategy.UNLEASH_PARAMETER_ORGNUMRE, "999999999"), unleashContext)).isFalse(); + verify(altinnTilgangsstyringService, never()).hentAltinnOrganisasjoner(any(), any()); + } + + @Test + public void byOrgnummmer_strategy_håndterer_flere_orgnummer() { + Fnr fnr = new Fnr("12345678901"); + Set orgSet = new HashSet<>(); + orgSet.add(new AltinnReportee("", "AS", null, "999999999", "", "")); + + when(altinnTilgangsstyringService.hentAltinnOrganisasjoner(eq(fnr), any())).thenReturn(orgSet); + assertThat(new ByOrgnummerStrategy(altinnTilgangsstyringService).isEnabled(Map.of(ByOrgnummerStrategy.UNLEASH_PARAMETER_ORGNUMRE, "910825526,999999999"), unleashContext)).isTrue(); + } + + @Test + public void skal_være_disablet_hvis_feil_ved_oppslag_i_altinn() { + Fnr fnr = new Fnr("12345678901"); + Set orgSet = new HashSet<>(); + orgSet.add(new AltinnReportee("", "AS", null, "999999998", "", "")); + + when(altinnTilgangsstyringService.hentAltinnOrganisasjoner(fnr, () -> "")).thenThrow(RuntimeException.class); + assertThat(new ByOrgnummerStrategy(altinnTilgangsstyringService).isEnabled(Map.of(ByOrgnummerStrategy.UNLEASH_PARAMETER_ORGNUMRE, "999999999"), unleashContext)).isFalse(); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FeatureToggleServiceTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FeatureToggleServiceTest.java new file mode 100644 index 000000000..9d6c8c265 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/FeatureToggleServiceTest.java @@ -0,0 +1,63 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles; + +import io.getunleash.Unleash; +import io.getunleash.UnleashContext; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.TokenUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class FeatureToggleServiceTest { + + @Mock private Unleash unleash; + @Mock private TokenUtils innloggingService; + + @InjectMocks + private FeatureToggleService featureToggleService; + + @Test + public void hentFeatureToggles__skal_returnere_true_hvis_feature_er_på() { + when(unleash.isEnabled(eq("feature_som_er_på"), any(UnleashContext.class))).thenReturn(true); + Map toggles = featureToggleService.hentFeatureToggles(Arrays.asList("feature_som_er_på")); + assertThat(toggles.get("feature_som_er_på")).isTrue(); + } + + @Test + public void hentFeatureToggles__skal_returnere_false_hvis_feature_er_av() { + when(unleash.isEnabled(eq("feature_som_er_av"), any(UnleashContext.class))).thenReturn(false); + Map toggles = featureToggleService.hentFeatureToggles(Arrays.asList("feature_som_er_av")); + assertThat(toggles.get("feature_som_er_av")).isFalse(); + } + + @Test + public void hentFeatureToggles__skal_returnere_false_hvis_feature_ikke_finnes() { + Map toggles = featureToggleService.hentFeatureToggles(Arrays.asList("feature_som_ikke_finnes")); + assertThat(toggles.get("feature_som_ikke_finnes")).isFalse(); + } + + @Test + public void hentFeatureToggles__skal_kunne_returnere_flere_toggles() { + List features = Arrays.asList("feature1", "feature2", "feature3"); + when(unleash.isEnabled(eq("feature1"), any(UnleashContext.class))).thenReturn(true); + when(unleash.isEnabled(eq("feature2"), any(UnleashContext.class))).thenReturn(false); + + Map toggles = featureToggleService.hentFeatureToggles(features); + + assertThat(toggles.get("feature1")).isTrue(); + assertThat(toggles.get("feature2")).isFalse(); + assertThat(toggles.get("feature3")).isFalse(); + assertThat(toggles.size()).isEqualTo(3); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysServiceTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysServiceTest.java new file mode 100644 index 000000000..be2fdafd0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/AxsysServiceTest.java @@ -0,0 +1,42 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles({ Miljø.LOCAL, "wiremock" }) +@DirtiesContext +public class AxsysServiceTest { + @Autowired + private AxsysService axsysService; + + @Test + public void hentEnheter__returnerer_riktige_enheter() { + List enheter = axsysService.hentEnheterNavAnsattHarTilgangTil(new NavIdent("X123456")); + assertThat(enheter).containsOnly(new NavEnhet("0906", "NAV Storebyen"), new NavEnhet("0904", "NAV Lillebyen")); + } + + @Test + public void hentEnheter__ugyldig_ident_skal_ikke_ha_enheter() { + List enheter = axsysService.hentEnheterNavAnsattHarTilgangTil(new NavIdent("X999999")); + assertThat(enheter).isEmpty(); + } + + @Test + public void enheter__inneholder_hentetEnheter() { + List enheter = axsysService.hentEnheterNavAnsattHarTilgangTil(new NavIdent("X123456")); + List pilotEnheter = Collections.singletonList(new NavEnhet("0906", "NAV Storebyen")); + assertThat(pilotEnheter).containsAnyElementsOf(enheter); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/ByEnhetStrategyTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/ByEnhetStrategyTest.java new file mode 100644 index 000000000..72315494a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/featuretoggles/enhet/ByEnhetStrategyTest.java @@ -0,0 +1,72 @@ +package no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet; + +import io.getunleash.UnleashContext; +import org.junit.jupiter.api.Test; +import org.springframework.web.client.RestClientException; + +import java.util.Map; + +import static com.google.common.collect.Lists.newArrayList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.ByEnhetStrategy.PARAM; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +public class ByEnhetStrategyTest { + + private AxsysService axsysService = mock(AxsysService.class); + private UnleashContext unleashContext = UnleashContext.builder().userId("X123456").build(); + + @Test + public void skal_være_disablet_hvis_innlogget_med_fødselsnummer() { + assertThat(new ByEnhetStrategy(axsysService).isEnabled(Map.of(PARAM, "1234"), UnleashContext.builder().userId("00000000000").build())).isEqualTo(false); + } + + @Test + public void skal_være_disablet_hvis_det_toggle_evalueres_uten_kontekst() { + assertThat(new ByEnhetStrategy(axsysService).isEnabled(Map.of(PARAM, "1234"))).isEqualTo(false); + } + + @Test + public void skal_være_disablet_hvis_det_ikke_finnes_bruker_i_konteksten() { + assertThat(new ByEnhetStrategy(axsysService).isEnabled(Map.of(PARAM, "1234"), UnleashContext.builder().build())).isFalse(); + } + + @Test + public void skal_være_disablet_hvis_det_ikke_finnes_definerte_enheter() { + assertThat(new ByEnhetStrategy(axsysService).isEnabled(emptyMap(), unleashContext)).isFalse(); + assertThat(new ByEnhetStrategy(axsysService).isEnabled(singletonMap(PARAM, null), unleashContext)).isFalse(); //Map.of() tåler ikke null + assertThat(new ByEnhetStrategy(axsysService).isEnabled(Map.of(PARAM, ""), unleashContext)).isFalse(); + } + + @Test + public void skal_være_disablet_hvis_bruker_ikke_har_definerte_enheter() { + assertThat(new ByEnhetStrategy(axsysService).isEnabled(Map.of(PARAM, "1234"), unleashContext)).isFalse(); + } + + @Test + public void skal_være_disablet_hvis_bruker_har_definerte_enheter_men_ingen_er_i_listen() { + when(axsysService.hentEnheterNavAnsattHarTilgangTil(any())).thenReturn(newArrayList(new NavEnhet("1111", "Bergen"), new NavEnhet("2222", "Stavanger"))); + assertThat(new ByEnhetStrategy(axsysService).isEnabled(Map.of(PARAM, "1234"), unleashContext)).isFalse(); + } + + @Test + public void skal_være_enablet_hvis_bruker_har_definert_enhet() { + when(axsysService.hentEnheterNavAnsattHarTilgangTil(any())).thenReturn(newArrayList(new NavEnhet("1234", "Lillehammer"))); + assertThat(new ByEnhetStrategy(axsysService).isEnabled(Map.of(PARAM, "1234"), unleashContext)).isTrue(); + } + + @Test + public void skal_være_enablet_hvis_en_av_brukers_enheter_er_i_listen() { + when(axsysService.hentEnheterNavAnsattHarTilgangTil(any())).thenReturn(newArrayList(new NavEnhet("1111", "Bergen"), new NavEnhet("1234", "Lillehammer"))); + assertThat(new ByEnhetStrategy(axsysService).isEnabled(Map.of(PARAM, "1234,5678"), unleashContext)).isTrue(); + } + + @Test + public void skal_kaste_exception_hvis_axsys_kaster_exception() { + when(axsysService.hentEnheterNavAnsattHarTilgangTil(any())).thenThrow(new RestClientException("mock exception")); + assertThatThrownBy(() -> new ByEnhetStrategy(axsysService).isEnabled(Map.of(PARAM, "1234"), unleashContext)).isInstanceOf(RestClientException.class); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/CorrelationIdSupplierTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/CorrelationIdSupplierTest.java new file mode 100644 index 000000000..64d8cb7e9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/CorrelationIdSupplierTest.java @@ -0,0 +1,50 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class CorrelationIdSupplierTest { + @Test + public void set__egendefinert_token_kan_returneres_flere_ganger() { + String egendefinertToken = "foo"; + CorrelationIdSupplier.set(egendefinertToken); + String token1 = CorrelationIdSupplier.get(); + String token2 = CorrelationIdSupplier.get(); + assertThat(token1).isEqualTo(egendefinertToken); + assertThat(token2).isEqualTo(egendefinertToken); + } + + @Test + public void set__blank_fungerer_ikke() { + assertThatThrownBy(() -> CorrelationIdSupplier.set("")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void generate__token_kan_returneres_flere_ganger() { + CorrelationIdSupplier.generateToken(); + String token1 = CorrelationIdSupplier.get(); + String token2 = CorrelationIdSupplier.get(); + assertThat(token1).isNotNull().isEqualTo(token2); + } + + @Test + public void generate__ny_genereres() { + CorrelationIdSupplier.generateToken(); + String token1 = CorrelationIdSupplier.get(); + CorrelationIdSupplier.generateToken(); + String token2 = CorrelationIdSupplier.get(); + assertThat(token1).isNotEqualTo(token2); + } + + @Test + public void remove__fjerner_token() { + CorrelationIdSupplier.generateToken(); + String token1 = CorrelationIdSupplier.get(); + CorrelationIdSupplier.remove(); + String token2 = CorrelationIdSupplier.get(); + assertThat(token1).isNotNull(); + assertThat(token2).isNull(); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/caching/CachingConfigMockTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/caching/CachingConfigMockTest.java new file mode 100644 index 000000000..ed68c7e7b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/caching/CachingConfigMockTest.java @@ -0,0 +1,294 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.caching; + + +import no.nav.tag.tiltaksgjennomforing.autorisasjon.SlettemerkeProperties; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.TilgangskontrollService; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.adapter.AbacAdapter; +import no.nav.tag.tiltaksgjennomforing.avtale.*; +import no.nav.tag.tiltaksgjennomforing.enhet.*; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.NavEnhet; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.cache.EhCacheConfig; +import no.nav.tag.tiltaksgjennomforing.persondata.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.util.AopTestUtils; + +import java.time.Instant; +import java.util.Optional; +import java.util.Set; + +import static org.mockito.Mockito.*; + +@ActiveProfiles("cache-test") +@ContextConfiguration +@ExtendWith(SpringExtension.class) +public class CachingConfigMockTest { + + private TilgangskontrollService mockTilgangskontrollService; + private PersondataService mockPersondataService; + private Norg2Client mockNorg2Client; + private VeilarbArenaClient mockVeilarbArenaClient; + + @Autowired + private TilgangskontrollService tilgangskontrollService; + @Autowired + private PersondataService persondataService; + @Autowired + private Norg2Client norg2Client; + @Autowired + private VeilarbArenaClient veilarbArenaClient; + + private Avtale avtale = TestData.enMidlertidigLonnstilskuddsjobbAvtale(); + private final PdlRespons FØRSTE_PDL_RESPONSE = TestData.enPdlrespons(false); + private PdlRespons ANDRE_PDL_RESPONSE = new PdlRespons( + new Data( + new HentPerson(null, new Navn[]{new Navn("ola", "", "Norman")}), + null, + new HentGeografiskTilknytning("", "030202", "", "3") + ) + ); + private final Norg2OppfølgingResponse FØRSTE_NORG2_OPPFØLGNING_RESPONSE = new Norg2OppfølgingResponse( + 1, + "1000", + "NAV Agder" + ); + private final Norg2OppfølgingResponse ANDRE_NORG2_OPPFØLGNING_RESPONSE = new Norg2OppfølgingResponse( + 2, + "1001", + "NAV Agder2" + ); + private final Norg2GeoResponse FØRSTE_NORG2_GEO_RESPONSE = new Norg2GeoResponse( + "NAV St. Hanshaugen", + "0313" + ); + private final Norg2GeoResponse ANDRE_NORG2_GEO_RESPONSE = new Norg2GeoResponse( + "NAV St. Hanshaugen2", + "0314" + ); + private final Oppfølgingsstatus FØRSTE_OPPFØLGNING_ENHET_ARENA = new Oppfølgingsstatus( + Formidlingsgruppe.ARBEIDSSOKER, + Kvalifiseringsgruppe.SITUASJONSBESTEMT_INNSATS, + "0906" + ); + private final Oppfølgingsstatus ANDRE_OPPFØLGNING_ENHET_ARENA = new Oppfølgingsstatus( + Formidlingsgruppe.ARBEIDSSOKER, + Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS, + "0904" + ); + + + @EnableCaching + @Configuration + @Profile("cache-test") + public static class CachingTestConfig { + + @Bean + public TilgangskontrollService tilgangskontrollServiceMockImplementation() { + return mock(TilgangskontrollService.class); + } + + @Bean + public PersondataService persondataServiceMockImplementation() { + return mock(PersondataService.class); + } + + @Bean + public Norg2Client norg2ClientMockImplementation() { + return mock(Norg2Client.class); + } + + @Bean + public VeilarbArenaClient veilarbArenaClientMockImplementation() { + return mock(VeilarbArenaClient.class); + } + + @Bean + public AbacAdapter abacAdapterMockImplementation() { return mock(AbacAdapter.class); } + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager( + EhCacheConfig.ARENA_CACHCE, + EhCacheConfig.PDL_CACHE, + EhCacheConfig.NORGNAVN_CACHE, + EhCacheConfig.NORG_GEO_ENHET, + EhCacheConfig.AXSYS_CACHE, + EhCacheConfig.ABAC_CACHE + ); + } + } + + @BeforeEach + public void setUp() { + final NavEnhet oppfolgingNavEnhet = TestData.ENHET_OPPFØLGING; + + TestData.setGeoNavEnhet(avtale, oppfolgingNavEnhet); + TestData.setOppfolgingNavEnhet(avtale, oppfolgingNavEnhet); + + mockTilgangskontrollService = AopTestUtils.getTargetObject(tilgangskontrollService); + mockPersondataService = AopTestUtils.getTargetObject(persondataService); + mockNorg2Client = AopTestUtils.getTargetObject(norg2Client); + mockVeilarbArenaClient = AopTestUtils.getTargetObject(veilarbArenaClient); + + lenient().when(mockTilgangskontrollService.harSkrivetilgangTilKandidat( + any(), + eq(avtale.getDeltakerFnr()) + )).thenReturn(true, true, true); + + when(mockPersondataService.hentPersondataFraPdl(avtale.getDeltakerFnr())).thenReturn(FØRSTE_PDL_RESPONSE, ANDRE_PDL_RESPONSE); + when(mockNorg2Client.hentOppfølgingsEnhetsnavnFraCacheNorg2(any())).thenReturn( + FØRSTE_NORG2_OPPFØLGNING_RESPONSE, + ANDRE_NORG2_OPPFØLGNING_RESPONSE + ); + when(mockNorg2Client.hentGeoEnhetFraCacheEllerNorg2(any())).thenReturn(FØRSTE_NORG2_GEO_RESPONSE, ANDRE_NORG2_GEO_RESPONSE); + when(mockVeilarbArenaClient.HentOppfølgingsenhetFraCacheEllerArena(avtale.getDeltakerFnr().asString())).thenReturn( + FØRSTE_OPPFØLGNING_ENHET_ARENA, + ANDRE_OPPFØLGNING_ENHET_ARENA + ); + } + + @Test + public void bekreft_antall_ganger_Cacheable_endepunkter_blir_kalt_ved_norg2Client_oppfølgingsEnhetsnavn() { + Norg2OppfølgingResponse norg2OppfølgingResponse = norg2Client.hentOppfølgingsEnhetsnavnFraCacheNorg2( + avtale.getEnhetOppfolging() + ); + Norg2OppfølgingResponse norg2OppfølgingResponse2 = norg2Client.hentOppfølgingsEnhetsnavnFraCacheNorg2( + avtale.getEnhetOppfolging() + ); + + Assertions.assertEquals(FØRSTE_NORG2_OPPFØLGNING_RESPONSE.getEnhetId(), norg2OppfølgingResponse.getEnhetId()); + Assertions.assertEquals(FØRSTE_NORG2_OPPFØLGNING_RESPONSE.getEnhetNr(), norg2OppfølgingResponse.getEnhetNr()); + Assertions.assertEquals(FØRSTE_NORG2_OPPFØLGNING_RESPONSE.getNavn(), norg2OppfølgingResponse.getNavn()); + + Assertions.assertEquals(FØRSTE_NORG2_OPPFØLGNING_RESPONSE.getEnhetId(), norg2OppfølgingResponse2.getEnhetId()); + Assertions.assertEquals(FØRSTE_NORG2_OPPFØLGNING_RESPONSE.getEnhetNr(), norg2OppfølgingResponse2.getEnhetNr()); + Assertions.assertEquals(FØRSTE_NORG2_OPPFØLGNING_RESPONSE.getNavn(), norg2OppfølgingResponse2.getNavn()); + + /** Blir kalt 2 ganger. Andre iterasjon så treffer vi cache response istedenfor endepunkt */ + verify(mockNorg2Client, times(1)).hentOppfølgingsEnhetsnavnFraCacheNorg2(avtale.getEnhetOppfolging()); + } + + @Test + public void bekreft_antall_ganger_Cacheable_endepunkter_blir_kalt_ved_norg2Client_geoEnhet() { + Optional optionalGeoEnhet = PersondataService.hentGeoLokasjonFraPdlRespons(FØRSTE_PDL_RESPONSE); + String geoEnhet = optionalGeoEnhet.get(); + + Norg2GeoResponse norg2GeoResponse = norg2Client.hentGeoEnhetFraCacheEllerNorg2(geoEnhet); + Norg2GeoResponse norg2GeoResponse2 = norg2Client.hentGeoEnhetFraCacheEllerNorg2(geoEnhet); + + Assertions.assertEquals(FØRSTE_NORG2_GEO_RESPONSE.getNavn(), norg2GeoResponse.getNavn()); + Assertions.assertEquals(FØRSTE_NORG2_GEO_RESPONSE.getEnhetNr(), norg2GeoResponse.getEnhetNr()); + + Assertions.assertEquals(FØRSTE_NORG2_GEO_RESPONSE.getNavn(), norg2GeoResponse2.getNavn()); + Assertions.assertEquals(FØRSTE_NORG2_GEO_RESPONSE.getEnhetNr(), norg2GeoResponse2.getEnhetNr()); + + /** Blir kalt 2 ganger. Andre iterasjon så treffer vi cache response istedenfor endepunkt */ + verify(mockNorg2Client, times(1)).hentGeoEnhetFraCacheEllerNorg2(geoEnhet); + } + + @Test + public void bekreft_antall_ganger_Cacheable_endepunkter_blir_kalt_ved_arena() { + Oppfølgingsstatus arenaResponse = veilarbArenaClient.HentOppfølgingsenhetFraCacheEllerArena(avtale.getDeltakerFnr().asString()); + Oppfølgingsstatus arenaResponse2 = veilarbArenaClient.HentOppfølgingsenhetFraCacheEllerArena(avtale.getDeltakerFnr().asString()); + + Assertions.assertEquals(FØRSTE_OPPFØLGNING_ENHET_ARENA, arenaResponse); + Assertions.assertEquals(FØRSTE_OPPFØLGNING_ENHET_ARENA, arenaResponse2); + + /** Blir kalt 2 ganger. Andre iterasjon så treffer vi cache response istedenfor endepunkt */ + verify(mockVeilarbArenaClient, times(1)).HentOppfølgingsenhetFraCacheEllerArena(avtale.getDeltakerFnr().asString()); + } + + @Test + public void bekreft_antall_ganger_Cacheable_endepunkter_blir_kalt_ved_pdl() { + PdlRespons pdlRespons = persondataService.hentPersondataFraPdl(avtale.getDeltakerFnr()); + PdlRespons pdlRespons2 = persondataService.hentPersondataFraPdl(avtale.getDeltakerFnr()); + + Assertions.assertEquals( + persondataService.hentGeoLokasjonFraPdlRespons(FØRSTE_PDL_RESPONSE).get(), + persondataService.hentGeoLokasjonFraPdlRespons(pdlRespons).get() + ); + Assertions.assertEquals( + persondataService.hentGeoLokasjonFraPdlRespons(FØRSTE_PDL_RESPONSE).get(), + persondataService.hentGeoLokasjonFraPdlRespons(pdlRespons2).get() + ); + + + Assertions.assertEquals( + FØRSTE_PDL_RESPONSE.getData().getHentGeografiskTilknytning().getRegel(), + pdlRespons.getData().getHentGeografiskTilknytning().getRegel() + ); + Assertions.assertEquals( + FØRSTE_PDL_RESPONSE.getData().getHentGeografiskTilknytning().getRegel(), + pdlRespons2.getData().getHentGeografiskTilknytning().getRegel() + ); + + Assertions.assertEquals( + persondataService.hentNavnFraPdlRespons(FØRSTE_PDL_RESPONSE).getFornavn(), + persondataService.hentNavnFraPdlRespons(pdlRespons).getFornavn() + ); + Assertions.assertEquals( + persondataService.hentNavnFraPdlRespons(FØRSTE_PDL_RESPONSE).getFornavn(), + persondataService.hentNavnFraPdlRespons(pdlRespons2).getFornavn() + ); + + Assertions.assertEquals( + persondataService.hentNavnFraPdlRespons(FØRSTE_PDL_RESPONSE).getEtternavn(), + persondataService.hentNavnFraPdlRespons(pdlRespons).getEtternavn() + ); + Assertions.assertEquals( + persondataService.hentNavnFraPdlRespons(FØRSTE_PDL_RESPONSE).getEtternavn(), + persondataService.hentNavnFraPdlRespons(pdlRespons2).getEtternavn() + ); + + /** Blir kalt 2 ganger. Andre iterasjon så treffer vi cache response istedenfor endepunkt */ + verify(mockPersondataService, times(1)).hentPersondataFraPdl(avtale.getDeltakerFnr()); + } + + @Test + public void bekreft_antall_ganger_Cacheable_endepunkter_blir_kalt_ved_endreAvtale() { + Optional optionalGeoEnhet = PersondataService.hentGeoLokasjonFraPdlRespons(FØRSTE_PDL_RESPONSE); + String geoEnhet = optionalGeoEnhet.get(); + + Veileder veileder = new Veileder( + avtale.getVeilederNavIdent(), + tilgangskontrollService, + persondataService, + norg2Client, + Set.of(new NavEnhet(avtale.getEnhetOppfolging(), avtale.getEnhetsnavnOppfolging())), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + + veileder.endreAvtale( + Instant.now(), + TestData.endringPåAlleLønnstilskuddFelter(), + avtale, + TestData.avtalerMedTilskuddsperioder + ); + veileder.endreAvtale( + Instant.now(), + TestData.endringPåAlleLønnstilskuddFelter(), + avtale, + TestData.avtalerMedTilskuddsperioder + ); + + /** Blir kalt 2 ganger. Andre iterasjon så treffer vi cache response istedenfor endepunkt */ + verify(mockNorg2Client, times(1)).hentOppfølgingsEnhetsnavnFraCacheNorg2(avtale.getEnhetOppfolging()); + verify(mockNorg2Client, times(1)).hentGeoEnhetFraCacheEllerNorg2(geoEnhet); + verify(mockPersondataService, times(1)).hentPersondataFraPdl(avtale.getDeltakerFnr()); + verify(mockVeilarbArenaClient, times(1)).HentOppfølgingsenhetFraCacheEllerArena(avtale.getDeltakerFnr().asString()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/caching/CachingConfigTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/caching/CachingConfigTest.java new file mode 100644 index 000000000..feff0d0de --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/caching/CachingConfigTest.java @@ -0,0 +1,217 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.caching; + + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.SlettemerkeProperties; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.abac.TilgangskontrollService; +import no.nav.tag.tiltaksgjennomforing.avtale.*; +import no.nav.tag.tiltaksgjennomforing.enhet.*; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.enhet.NavEnhet; +import no.nav.tag.tiltaksgjennomforing.persondata.PdlRespons; +import no.nav.tag.tiltaksgjennomforing.persondata.PersondataService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cache.CacheManager; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.Instant; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import static no.nav.tag.tiltaksgjennomforing.infrastruktur.cache.EhCacheConfig.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; + + +@Slf4j +@SpringBootTest +@ActiveProfiles({ Miljø.LOCAL, "wiremock" }) +@ExtendWith(SpringExtension.class) +@DirtiesContext +public class CachingConfigTest { + + private final CacheManager cacheManager; + private final VeilarbArenaClient veilarbArenaClient; + private final Norg2Client norg2Client; + private final PersondataService persondataService; + + public CachingConfigTest( + @Autowired CacheManager cacheManager, + @Autowired VeilarbArenaClient veilarbArenaClient, + @Autowired Norg2Client norg2Client, + @Autowired PersondataService persondataService + ){ + this.cacheManager = cacheManager; + this.veilarbArenaClient = veilarbArenaClient; + this.norg2Client = norg2Client; + this.persondataService = persondataService; + } + + private T getCacheValue(String cacheName, K cacheKey, Class clazz) { + return (T) Objects.requireNonNull(Objects.requireNonNull(cacheManager.getCache(cacheName)).get(cacheKey)).get(); + } + + @Test + public void sjekk_at_caching_fanger_opp_data_fra_arena_cache() { + final NavEnhet oppfolgingNavEnhet = TestData.ENHET_OPPFØLGING; + final String ETT_FNR_NR = "00000000000"; + final String ETT_FNR_NR2 = "11111111111"; + final String ETT_FNR_NR3 = "22127748067"; + + Avtale avtale = TestData.enMidlertidigLonnstilskuddsjobbAvtale(); + avtale.setDeltakerFnr(new Fnr(ETT_FNR_NR)); + TestData.setGeoNavEnhet(avtale, oppfolgingNavEnhet); + TestData.setOppfolgingNavEnhet(avtale, oppfolgingNavEnhet); + + veilarbArenaClient.HentOppfølgingsenhetFraCacheEllerArena(avtale.getDeltakerFnr().asString()); + veilarbArenaClient.HentOppfølgingsenhetFraCacheEllerArena(ETT_FNR_NR2); + veilarbArenaClient.HentOppfølgingsenhetFraCacheEllerArena(ETT_FNR_NR2); + veilarbArenaClient.HentOppfølgingsenhetFraCacheEllerArena(ETT_FNR_NR3); + veilarbArenaClient.HentOppfølgingsenhetFraCacheEllerArena(ETT_FNR_NR2); + + Assertions.assertEquals("0906", getCacheValue(ARENA_CACHCE, ETT_FNR_NR, Oppfølgingsstatus.class).getOppfolgingsenhet()); + Assertions.assertEquals("0904", getCacheValue(ARENA_CACHCE, ETT_FNR_NR2, Oppfølgingsstatus.class).getOppfolgingsenhet()); + Assertions.assertEquals("0906", getCacheValue(ARENA_CACHCE, ETT_FNR_NR3, Oppfølgingsstatus.class).getOppfolgingsenhet()); + } + + @Test + public void sjekk_at_caching_fanger_opp_data_fra_norgnavn_cache() { + final NavEnhet oppfolgingNavEnhet = TestData.ENHET_OPPFØLGING; + Avtale avtale = TestData.enMidlertidigLonnstilskuddsjobbAvtale(); + + TestData.setOppfolgingNavEnhet(avtale, oppfolgingNavEnhet); + + Norg2OppfølgingResponse norg2OppfølgingResponse = norg2Client.hentOppfølgingsEnhetsnavnFraCacheNorg2( + avtale.getEnhetOppfolging() + ); + Norg2OppfølgingResponse norgnavnCacheForEnhet = getCacheValue( + NORGNAVN_CACHE, + avtale.getEnhetOppfolging(), + Norg2OppfølgingResponse.class + ); + + Assertions.assertEquals("NAV Agder", norgnavnCacheForEnhet.getNavn()); + Assertions.assertEquals("1000", norgnavnCacheForEnhet.getEnhetNr()); + Assertions.assertEquals(norg2OppfølgingResponse.getNavn(), norgnavnCacheForEnhet.getNavn()); + Assertions.assertEquals(norg2OppfølgingResponse.getEnhetNr(), norgnavnCacheForEnhet.getEnhetNr()); + } + + @Test + public void sjekk_at_caching_fanger_opp_data_fra_norggeoenhet_cache() { + PdlRespons pdlRespons = TestData.enPdlrespons(false); + Optional optionalGeoEnhet = PersondataService.hentGeoLokasjonFraPdlRespons(pdlRespons); + String geoEnhet = optionalGeoEnhet.get(); + + Norg2GeoResponse norg2GeoResponse = norg2Client.hentGeoEnhetFraCacheEllerNorg2(geoEnhet); + Norg2GeoResponse norggeoenhetCacheForGeoEnhet = getCacheValue(NORG_GEO_ENHET, geoEnhet, Norg2GeoResponse.class); + + Assertions.assertEquals("NAV St. Hanshaugen", norggeoenhetCacheForGeoEnhet.getNavn()); + Assertions.assertEquals("0313", norggeoenhetCacheForGeoEnhet.getEnhetNr()); + Assertions.assertEquals(norg2GeoResponse.getNavn(), norggeoenhetCacheForGeoEnhet.getNavn()); + Assertions.assertEquals(norg2GeoResponse.getEnhetNr(), norggeoenhetCacheForGeoEnhet.getEnhetNr()); + } + + @Test + public void sjekk_at_caching_fanger_opp_data_fra_pdl_cache() { + Fnr brukerFnr = new Fnr("00000000000"); + PdlRespons pdlRespons = persondataService.hentPersondataFraPdl(brukerFnr); + + PdlRespons pdlCache = getCacheValue(PDL_CACHE, brukerFnr, PdlRespons.class); + + Assertions.assertEquals("030104", persondataService.hentGeoLokasjonFraPdlRespons(pdlCache).get()); + Assertions.assertEquals("3", pdlCache.getData().getHentGeografiskTilknytning().getRegel()); + Assertions.assertEquals("Donald", persondataService.hentNavnFraPdlRespons(pdlCache).getFornavn()); + Assertions.assertEquals("Duck", persondataService.hentNavnFraPdlRespons(pdlCache).getEtternavn()); + Assertions.assertEquals( + persondataService.hentGeoLokasjonFraPdlRespons(pdlRespons).get(), + persondataService.hentGeoLokasjonFraPdlRespons(pdlCache).get() + ); + Assertions.assertEquals( + pdlRespons.getData().getHentGeografiskTilknytning().getRegel(), + pdlCache.getData().getHentGeografiskTilknytning().getRegel() + ); + Assertions.assertEquals( + persondataService.hentNavnFraPdlRespons(pdlRespons).getFornavn(), + persondataService.hentNavnFraPdlRespons(pdlCache).getFornavn() + ); + Assertions.assertEquals( + persondataService.hentNavnFraPdlRespons(pdlRespons).getEtternavn(), + persondataService.hentNavnFraPdlRespons(pdlCache).getEtternavn() + ); + } + + @Test + public void vertifisere_at_caching_fungerer_for_endreAvtale_av_veileder() { + final NavEnhet oppfolgingNavEnhet = TestData.ENHET_OPPFØLGING; + final String GEO_LOKASJON_FRA_PDL_MAPPING = "030104"; + Avtale avtale = TestData.enMidlertidigLonnstilskuddsjobbAvtale(); + TestData.setGeoNavEnhet(avtale, oppfolgingNavEnhet); + TestData.setOppfolgingNavEnhet(avtale, oppfolgingNavEnhet); + + final TilgangskontrollService mockTilgangskontrollService = mock(TilgangskontrollService.class); + + Veileder veileder = new Veileder( + avtale.getVeilederNavIdent(), + mockTilgangskontrollService, + persondataService, + norg2Client, + Set.of(new NavEnhet(avtale.getEnhetOppfolging(), avtale.getEnhetsnavnOppfolging())), + new SlettemerkeProperties(), + false, + veilarbArenaClient + ); + + lenient().when(mockTilgangskontrollService.harSkrivetilgangTilKandidat( + eq(veileder), + eq(avtale.getDeltakerFnr()) + )).thenReturn(true, true, true); + + veileder.endreAvtale( + Instant.now(), + TestData.endringPåAlleLønnstilskuddFelter(), + avtale, + TestData.avtalerMedTilskuddsperioder + ); + + veileder.endreAvtale( + Instant.now(), + TestData.endringPåAlleLønnstilskuddFelter(), + avtale, + TestData.avtalerMedTilskuddsperioder + ); + + Norg2OppfølgingResponse norgnavnCacheForEnhet = getCacheValue( + NORGNAVN_CACHE, + avtale.getEnhetOppfolging(), + Norg2OppfølgingResponse.class + ); + Norg2GeoResponse norggeoenhetCacheForGeoEnhet = getCacheValue( + NORG_GEO_ENHET, + GEO_LOKASJON_FRA_PDL_MAPPING, + Norg2GeoResponse.class + ); + PdlRespons pdlCache = getCacheValue(PDL_CACHE, avtale.getDeltakerFnr(), PdlRespons.class); + Oppfølgingsstatus arenaCache = getCacheValue(ARENA_CACHCE, avtale.getDeltakerFnr().asString(), Oppfølgingsstatus.class); + + Assertions.assertEquals("NAV St. Hanshaugen", norggeoenhetCacheForGeoEnhet.getNavn()); + Assertions.assertEquals("0313", norggeoenhetCacheForGeoEnhet.getEnhetNr()); + + Assertions.assertEquals("NAV Agder", norgnavnCacheForEnhet.getNavn()); + Assertions.assertEquals("1000", norgnavnCacheForEnhet.getEnhetNr()); + + Assertions.assertEquals("030104", persondataService.hentGeoLokasjonFraPdlRespons(pdlCache).get()); + Assertions.assertEquals("3", pdlCache.getData().getHentGeografiskTilknytning().getRegel()); + Assertions.assertEquals("Donald", persondataService.hentNavnFraPdlRespons(pdlCache).getFornavn()); + Assertions.assertEquals("Duck", persondataService.hentNavnFraPdlRespons(pdlCache).getEtternavn()); + + Assertions.assertEquals("0906", arenaCache.getOppfolgingsenhet()); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/kafka/AivenKafkaConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/kafka/AivenKafkaConfiguration.java new file mode 100644 index 000000000..56487da5c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/infrastruktur/kafka/AivenKafkaConfiguration.java @@ -0,0 +1,39 @@ +package no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka; + +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; + +import java.util.HashMap; +import java.util.Map; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@TestConfiguration +@Slf4j +@EnableKafka +public class AivenKafkaConfiguration { + + @Value("${no.nav.gcp.kafka.aiven.bootstrap-servers}") + private String bootstrapServers; + + private Map producerConfigs() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + return props; + } + + @Bean + public KafkaTemplate aivenKafkaTemplate() { + KafkaTemplate kafkaTemplate = new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(producerConfigs())); + return kafkaTemplate; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/journalfoering/AvtaleTilJournalfoeringMapperTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/journalfoering/AvtaleTilJournalfoeringMapperTest.java new file mode 100644 index 000000000..b96c96a66 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/journalfoering/AvtaleTilJournalfoeringMapperTest.java @@ -0,0 +1,161 @@ +package no.nav.tag.tiltaksgjennomforing.journalfoering; + +import static no.nav.tag.tiltaksgjennomforing.journalfoering.AvtaleTilJournalfoeringMapper.tilJournalfoering; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Arrays; +import java.util.UUID; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleInnhold; +import no.nav.tag.tiltaksgjennomforing.avtale.GodkjentPaVegneGrunn; +import no.nav.tag.tiltaksgjennomforing.avtale.Maal; +import no.nav.tag.tiltaksgjennomforing.avtale.MaalKategori; +import no.nav.tag.tiltaksgjennomforing.avtale.Stillingstype; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class AvtaleTilJournalfoeringMapperTest { + + private Avtale avtale; + private AvtaleInnhold avtaleInnhold; + private AvtaleTilJournalfoering tilJournalfoering; + private GodkjentPaVegneGrunn grunn; + + @BeforeEach + public void setUp() { + avtale = TestData.enAvtaleMedAltUtfyltGodkjentAvVeileder(); + avtaleInnhold = avtale.getGjeldendeInnhold(); + grunn = new GodkjentPaVegneGrunn(); + } + + @AfterEach + public void tearDown() { + avtale = null; + tilJournalfoering = null; + } + + @Test + public void mapper() { + final UUID avtaleId = UUID.randomUUID(); + avtale.setId(avtaleId); + avtale.getGjeldendeInnhold().setGodkjentPaVegneAv(true); + avtale.setOpprettetTidspunkt(Now.localDateTime()); + avtale.getGjeldendeInnhold().setStillingstype(Stillingstype.FAST); + + tilJournalfoering = tilJournalfoering(avtaleInnhold, null); + + assertEquals(avtaleId.toString(), tilJournalfoering.getAvtaleId().toString()); + assertEquals(avtaleInnhold.getId().toString(), tilJournalfoering.getAvtaleVersjonId().toString()); + assertEquals(avtale.getDeltakerFnr().asString(), tilJournalfoering.getDeltakerFnr()); + assertEquals(avtale.getBedriftNr().asString(), tilJournalfoering.getBedriftNr()); + assertEquals(avtale.getVeilederNavIdent().asString(), tilJournalfoering.getVeilederNavIdent()); + assertEquals(avtale.getOpprettetTidspunkt().toLocalDate(), tilJournalfoering.getOpprettet()); + assertEquals(avtale.getGjeldendeInnhold().getDeltakerFornavn(), tilJournalfoering.getDeltakerFornavn()); + assertEquals(avtale.getGjeldendeInnhold().getDeltakerEtternavn(), tilJournalfoering.getDeltakerEtternavn()); + assertEquals(avtale.getGjeldendeInnhold().getDeltakerTlf(), tilJournalfoering.getDeltakerTlf()); + assertEquals(avtale.getGjeldendeInnhold().getBedriftNavn(), tilJournalfoering.getBedriftNavn()); + assertEquals(avtale.getGjeldendeInnhold().getArbeidsgiverFornavn(), tilJournalfoering.getArbeidsgiverFornavn()); + assertEquals(avtale.getGjeldendeInnhold().getArbeidsgiverEtternavn(), tilJournalfoering.getArbeidsgiverEtternavn()); + assertEquals(avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), tilJournalfoering.getArbeidsgiverTlf()); + assertEquals(avtale.getGjeldendeInnhold().getVeilederFornavn(), tilJournalfoering.getVeilederFornavn()); + assertEquals(avtale.getGjeldendeInnhold().getVeilederEtternavn(), tilJournalfoering.getVeilederEtternavn()); + assertEquals(avtale.getGjeldendeInnhold().getVeilederTlf(), tilJournalfoering.getVeilederTlf()); + assertEquals(avtale.getGjeldendeInnhold().getOppfolging(), tilJournalfoering.getOppfolging()); + assertEquals(avtale.getGjeldendeInnhold().getTilrettelegging(), tilJournalfoering.getTilrettelegging()); + assertEquals(avtale.getGjeldendeInnhold().getStartDato(), tilJournalfoering.getStartDato()); + assertEquals(avtale.getGjeldendeInnhold().getSluttDato(), tilJournalfoering.getSluttDato()); + assertEquals(avtale.getGjeldendeInnhold().getStillingprosent(), tilJournalfoering.getStillingprosent()); + assertEquals(avtale.getGjeldendeInnhold().getAntallDagerPerUke(), tilJournalfoering.getAntallDagerPerUke()); + assertEquals(avtale.getGjeldendeInnhold().getGodkjentAvDeltaker().toLocalDate(), tilJournalfoering.getGodkjentAvDeltaker()); + assertEquals(avtale.getGjeldendeInnhold().getGodkjentAvArbeidsgiver().toLocalDate(), tilJournalfoering.getGodkjentAvArbeidsgiver()); + assertEquals(avtale.getGjeldendeInnhold().getGodkjentAvVeileder().toLocalDate(), tilJournalfoering.getGodkjentAvVeileder()); + assertEquals(avtale.getGjeldendeInnhold().isGodkjentPaVegneAv(), tilJournalfoering.isGodkjentPaVegneAv()); + assertEquals(avtale.getGjeldendeInnhold().getVersjon(), tilJournalfoering.getVersjon()); + assertEquals(avtale.getTiltakstype(), tilJournalfoering.getTiltakstype()); + assertEquals(avtale.getGjeldendeInnhold().getArbeidsgiverKontonummer(), tilJournalfoering.getArbeidsgiverKontonummer()); + assertEquals(avtale.getGjeldendeInnhold().getStillingstittel(), tilJournalfoering.getStillingstittel()); + assertEquals(avtale.getGjeldendeInnhold().getArbeidsoppgaver(), tilJournalfoering.getArbeidsoppgaver()); + assertEquals(avtale.getGjeldendeInnhold().getLonnstilskuddProsent(), tilJournalfoering.getLonnstilskuddProsent()); + assertEquals(avtale.getGjeldendeInnhold().getManedslonn(), tilJournalfoering.getManedslonn()); + assertEquals(avtale.getGjeldendeInnhold().getFeriepengesats(), tilJournalfoering.getFeriepengesats()); + assertEquals(avtale.getGjeldendeInnhold().getArbeidsgiveravgift(), tilJournalfoering.getArbeidsgiveravgift()); + assertEquals(avtale.getGjeldendeInnhold().getManedslonn100pst(), tilJournalfoering.getManedslonn100pst()); + assertNotNull(avtaleInnhold.getStillingstype()); + assertEquals(avtaleInnhold.getStillingstype(), tilJournalfoering.getStillingstype()); + } + + @Test + public void paaVegneGrunnErIkkeBankId() { + grunn.setIkkeBankId(true); + avtale.getGjeldendeInnhold().setGodkjentPaVegneGrunn(grunn); + tilJournalfoering = tilJournalfoering(avtaleInnhold, null); + assertTrue(tilJournalfoering.getGodkjentPaVegneGrunn().isIkkeBankId()); + assertFalse(tilJournalfoering.getGodkjentPaVegneGrunn().isDigitalKompetanse()); + assertFalse(tilJournalfoering.getGodkjentPaVegneGrunn().isReservert()); + } + + @Test + public void paaVegneGrunnErDigitalKompetanse() { + grunn.setDigitalKompetanse(true); + avtale.getGjeldendeInnhold().setGodkjentPaVegneGrunn(grunn); + tilJournalfoering = tilJournalfoering(avtaleInnhold, null); + assertFalse(tilJournalfoering.getGodkjentPaVegneGrunn().isIkkeBankId()); + assertTrue(tilJournalfoering.getGodkjentPaVegneGrunn().isDigitalKompetanse()); + assertFalse(tilJournalfoering.getGodkjentPaVegneGrunn().isReservert()); + } + + @Test + public void paaVegneGrunnErReservert() { + grunn.setReservert(true); + avtale.getGjeldendeInnhold().setGodkjentPaVegneGrunn(grunn); + tilJournalfoering = tilJournalfoering(avtaleInnhold, null); + assertFalse(tilJournalfoering.getGodkjentPaVegneGrunn().isIkkeBankId()); + assertFalse(tilJournalfoering.getGodkjentPaVegneGrunn().isDigitalKompetanse()); + assertTrue(tilJournalfoering.getGodkjentPaVegneGrunn().isReservert()); + + avtale.getGjeldendeInnhold().setGodkjentPaVegneGrunn(null); + tilJournalfoering = tilJournalfoering(avtaleInnhold, null); + assertNull(tilJournalfoering.getGodkjentPaVegneGrunn()); + } + + @Test + public void ingenPaaVegneGrunn() { + avtale.getGjeldendeInnhold().setGodkjentPaVegneGrunn(null); + tilJournalfoering = tilJournalfoering(avtaleInnhold, null); + assertNull(tilJournalfoering.getGodkjentPaVegneGrunn()); + } + + @Test + public void mapperMaal() { + Maal maal = new Maal(); + maal.setKategori(MaalKategori.FÅ_JOBB_I_BEDRIFTEN); + maal.setBeskrivelse("Beskrivelse"); + + Maal maal2 = new Maal(); + maal2.setKategori(MaalKategori.UTPRØVING); + maal2.setBeskrivelse("Beskrivelse-2"); + + avtaleInnhold.setMaal(Arrays.asList(maal, maal2)); + + tilJournalfoering = tilJournalfoering(avtaleInnhold, null); + + tilJournalfoering.getMaal().forEach(maalet -> { + if (maalet.getKategori().equals(MaalKategori.FÅ_JOBB_I_BEDRIFTEN.getVerdi())) { + assertEquals("Beskrivelse", maalet.getBeskrivelse()); + } else if (maalet.getKategori().equals(MaalKategori.UTPRØVING.getVerdi())) { + assertEquals("Beskrivelse-2", maalet.getBeskrivelse()); + } else { + fail("Mål; " + maalet); + } + }); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/journalfoering/InternalAvtaleControllerTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/journalfoering/InternalAvtaleControllerTest.java new file mode 100644 index 000000000..fcd6c7dc0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/journalfoering/InternalAvtaleControllerTest.java @@ -0,0 +1,119 @@ +package no.nav.tag.tiltaksgjennomforing.journalfoering; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.anyIterable; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.*; +import java.util.stream.Collectors; +import no.nav.tag.tiltaksgjennomforing.autorisasjon.InnloggingService; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleInnhold; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleInnholdRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import no.nav.tag.tiltaksgjennomforing.exceptions.TilgangskontrollException; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class InternalAvtaleControllerTest { + + private static final UUID AVTALE_ID_1 = UUID.randomUUID(); + private static final UUID AVTALE_ID_2 = UUID.randomUUID(); + private static final UUID AVTALE_ID_3 = UUID.randomUUID(); + private List avtaleInnholdList = treAvtalerSomSkalJournalføres().stream().map(avtale -> avtale.getGjeldendeInnhold()).collect(Collectors.toList()); + + private List avtaleInnholdListMedTilskuddsPerioder = enAvtaleMedTilskudsPerioderSomSkalJournalføres().stream().map(avtale -> avtale.getGjeldendeInnhold()).collect(Collectors.toList()); + + @InjectMocks + private InternalAvtaleController internalAvtaleController; + + @Mock + private InnloggingService innloggingService; + + @Mock + private AvtaleRepository avtaleRepository; + + @Mock + private AvtaleInnholdRepository avtaleInnholdRepository; + + @Test + public void henterAvtalerMedTilskuddsperioderTilJournalfoering() { + doNothing().when(innloggingService).validerSystembruker(); + when(avtaleInnholdRepository.finnAvtaleVersjonerTilJournalfoering()).thenReturn(avtaleInnholdListMedTilskuddsPerioder); + List avtalerTilJournalfoering = internalAvtaleController.hentIkkeJournalfoerteAvtaler(); + assertThat(avtalerTilJournalfoering).hasSize(1); + assertThat(avtalerTilJournalfoering.get(0).getTilskuddsPerioder()).hasSize(1); + avtalerTilJournalfoering.forEach(avtaleTilJournalfoering -> assertNotNull(avtaleTilJournalfoering.getAvtaleId())); + } + + @Test + public void henterAvtalerTilJournalfoering() { + doNothing().when(innloggingService).validerSystembruker(); + when(avtaleInnholdRepository.finnAvtaleVersjonerTilJournalfoering()).thenReturn(avtaleInnholdList); + List avtalerTilJournalfoering = internalAvtaleController.hentIkkeJournalfoerteAvtaler(); + assertThat(avtalerTilJournalfoering).hasSize(3); + avtalerTilJournalfoering.forEach(avtaleTilJournalfoering -> assertNotNull(avtaleTilJournalfoering.getAvtaleId())); + } + + @Test + public void henterIkkeAvtalerTilJournalfoering() { + doThrow(TilgangskontrollException.class).when(innloggingService).validerSystembruker(); + + assertThatThrownBy(internalAvtaleController::hentIkkeJournalfoerteAvtaler).isInstanceOf(TilgangskontrollException.class); + verify(avtaleRepository, never()).findAll(); + } + + @Test + public void journalfoererAvtaler() { + List godkjenteAvtaleVersjoner = treAvtalerSomSkalJournalføres().stream().map(avtale -> avtale.getGjeldendeInnhold()).collect(Collectors.toList()); + Map map = new HashMap<>(); + godkjenteAvtaleVersjoner.forEach(avtaleInnhold -> map.put(avtaleInnhold.getId(), "1")); + + doNothing().when(innloggingService).validerSystembruker(); + when(avtaleInnholdRepository.findAllById(map.keySet())).thenReturn(godkjenteAvtaleVersjoner); + internalAvtaleController.journalfoerAvtaler(map); + verify(avtaleInnholdRepository, atLeastOnce()).saveAll(anyIterable()); + } + + @Test + public void journalfoererIkkeAvtaler() { + doThrow(TilgangskontrollException.class).when(innloggingService).validerSystembruker(); + assertThatThrownBy(() -> internalAvtaleController.hentIkkeJournalfoerteAvtaler()).isInstanceOf(TilgangskontrollException.class); + verify(avtaleRepository, never()).findAllById(anyIterable()); + verify(avtaleRepository, never()).saveAll(anyIterable()); + } + + private static List treAvtalerSomSkalJournalføres() { + Avtale avtale = TestData.enAvtaleMedAltUtfyltGodkjentAvVeileder(); + avtale.setId(AVTALE_ID_1); + Avtale avtale2 = TestData.enAvtaleMedFlereVersjoner(); + avtale2.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale2.setId(AVTALE_ID_2); + Avtale avtale3 = TestData.enAvtaleMedFlereVersjoner(); + avtale3.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale3.setId(AVTALE_ID_3); + + return Arrays.asList(avtale, avtale2, avtale3); + } + + private static List enAvtaleMedTilskudsPerioderSomSkalJournalføres() { + Avtale avtale4 = TestData.enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(Now.localDate(), Now.localDate()); + avtale4.getGjeldendeInnhold().setGodkjentAvVeileder(Now.localDateTime()); + avtale4.setId(AVTALE_ID_3); + return Arrays.asList(avtale4); + } +} + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterServiceImplTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterServiceImplTest.java new file mode 100644 index 000000000..b074c8fa9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/okonomi/KontoregisterServiceImplTest.java @@ -0,0 +1,39 @@ +package no.nav.tag.tiltaksgjennomforing.okonomi; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.exceptions.KontoregisterFantIkkeBedriftFeilException; +import no.nav.tag.tiltaksgjennomforing.exceptions.KontoregisterFeilException; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles({ Miljø.LOCAL, "wiremock"}) +@DirtiesContext +public class KontoregisterServiceImplTest { + @Autowired + private KontoregisterService KontoregisterService; + + @Test + public void hentKontonummer__skal_returnere_verdi_fra_kall() { + String kontonummerTilbake = KontoregisterService.hentKontonummer("990983666"); + assertThat(kontonummerTilbake).isEqualTo("10000008162"); + } + + @Test + public void hentKontonummer__skal_returnere_fant_ikke_bedrift_feilmelding() { + assertThatThrownBy(() -> KontoregisterService.hentKontonummer("111234567")) + .isInstanceOf(KontoregisterFantIkkeBedriftFeilException.class); + } + + @Test + public void hentKontonummer__skal_returnere_feilmelding() { + assertThatThrownBy(() -> KontoregisterService.hentKontonummer("777333333")) + .isInstanceOf(KontoregisterFeilException.class); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregServiceTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregServiceTest.java new file mode 100644 index 000000000..876318994 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/orgenhet/EregServiceTest.java @@ -0,0 +1,32 @@ +package no.nav.tag.tiltaksgjennomforing.orgenhet; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@ActiveProfiles({ Miljø.LOCAL, "wiremock"}) +@DirtiesContext +public class EregServiceTest { + @Autowired + private EregService eregService; + + @Test + public void hentBedriftNavn__returnerer_navn_og_bedriftnr() { + Organisasjon organisasjon = eregService.hentVirksomhet(new BedriftNr("999999999")); + assertThat(organisasjon.getBedriftNr()).isEqualTo(new BedriftNr("999999999")); + assertThat(organisasjon.getBedriftNavn()).isEqualTo("Saltrød og Høneby"); + } + + @Test + public void hentBedriftNavn__kaster_exception_ved_404() { + assertThatThrownBy(() -> eregService.hentVirksomhet(new BedriftNr("799999999"))).isExactlyInstanceOf(EnhetFinnesIkkeException.class); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/persondata/NavnFormatererTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/persondata/NavnFormatererTest.java new file mode 100644 index 000000000..3f7008b04 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/persondata/NavnFormatererTest.java @@ -0,0 +1,28 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NavnFormatererTest { + @Test + void bare_fornavn_og_etternavn() { + NavnFormaterer navnFormaterer = new NavnFormaterer(new Navn("FOO", null, "BAR")); + assertThat(navnFormaterer.getFornavn()).isEqualTo("Foo"); + assertThat(navnFormaterer.getEtternavn()).isEqualTo("Bar"); + } + + @Test + void fornavn_mellomnavn_og_etternavn() { + NavnFormaterer navnFormaterer = new NavnFormaterer(new Navn("FOO", "BAR", "BAZ")); + assertThat(navnFormaterer.getFornavn()).isEqualTo("Foo Bar"); + assertThat(navnFormaterer.getEtternavn()).isEqualTo("Baz"); + } + + @Test + void navn_med_bindestrek() { + NavnFormaterer navnFormaterer = new NavnFormaterer(new Navn("FOO-BAR", "BARNEY BOO", "BAZZ-Y BAG")); + assertThat(navnFormaterer.getFornavn()).isEqualTo("Foo-Bar Barney Boo"); + assertThat(navnFormaterer.getEtternavn()).isEqualTo("Bazz-Y Bag"); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/persondata/PersondataServiceTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/persondata/PersondataServiceTest.java new file mode 100644 index 000000000..67ba61c4a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/persondata/PersondataServiceTest.java @@ -0,0 +1,131 @@ +package no.nav.tag.tiltaksgjennomforing.persondata; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles({ Miljø.LOCAL, "wiremock" }) +@DirtiesContext +public class PersondataServiceTest { + private static final Fnr STRENGT_FORTROLIG_PERSON = new Fnr("16053900422"); + private static final Fnr STRENGT_FORTROLIG_UTLAND_PERSON = new Fnr("28033114267"); + private static final Fnr FORTROLIG_PERSON = new Fnr("26067114433"); + private static final Fnr UGRADERT_PERSON = new Fnr("00000000000"); + private static final Fnr UGRADERT_PERSON_TOM_RESPONSE = new Fnr("27030960020"); + private static final Fnr USPESIFISERT_GRADERT_PERSON = new Fnr("18076641842"); + private static final Fnr PERSON_FINNES_IKKE = new Fnr("24080687881"); + private static final Fnr PERSON_FOR_RESPONS_UTEN_DATA = new Fnr("23097010706"); + private static final Fnr DONALD_DUCK = new Fnr("00000000000"); + @Autowired + private PersondataService persondataService; + + @Test + public void hentGradering__returnerer_strengt_fortrolig_person() { + Adressebeskyttelse adressebeskyttelse = persondataService.hentAdressebeskyttelse(STRENGT_FORTROLIG_PERSON); + assertThat(adressebeskyttelse.getGradering()).isEqualTo("STRENGT_FORTROLIG"); + } + + @Test + public void hentGradering__returnerer_strengt_fortrolig_utland_person() { + Adressebeskyttelse adressebeskyttelse = persondataService.hentAdressebeskyttelse(STRENGT_FORTROLIG_UTLAND_PERSON); + assertThat(adressebeskyttelse.getGradering()).isEqualTo("STRENGT_FORTROLIG_UTLAND"); + } + + @Test + public void hentGradering__returnerer_fortrolig_person() { + Adressebeskyttelse adressebeskyttelse = persondataService.hentAdressebeskyttelse(FORTROLIG_PERSON); + assertThat(adressebeskyttelse.getGradering()).isEqualTo("FORTROLIG"); + } + + @Test + public void hentGradering__returnerer_ugradert_person() { + Adressebeskyttelse adressebeskyttelse = persondataService.hentAdressebeskyttelse(UGRADERT_PERSON); + assertThat(adressebeskyttelse.getGradering()).isEqualTo("UGRADERT"); + } + + @Test + public void hentGradering__returnerer_tom_gradering() { + Adressebeskyttelse adressebeskyttelse = persondataService.hentAdressebeskyttelse(USPESIFISERT_GRADERT_PERSON); + assertThat(adressebeskyttelse).isEqualTo(Adressebeskyttelse.INGEN_BESKYTTELSE); + } + + @Test + public void hentGradering__person_finnes_ikke_er_ok() { + Adressebeskyttelse adressebeskyttelse = persondataService.hentAdressebeskyttelse(PERSON_FINNES_IKKE); + assertThat(adressebeskyttelse).isEqualTo(Adressebeskyttelse.INGEN_BESKYTTELSE); + } + + @Test + public void hentGradering__returnerer_ugradert_tom_gradering() { + Adressebeskyttelse adressebeskyttelse = persondataService.hentAdressebeskyttelse(UGRADERT_PERSON_TOM_RESPONSE); + assertThat(adressebeskyttelse).isEqualTo(Adressebeskyttelse.INGEN_BESKYTTELSE); + } + + @Test + public void hentGradering__person_får_respons_uten_data() { + Adressebeskyttelse adressebeskyttelse = persondataService.hentAdressebeskyttelse(PERSON_FOR_RESPONS_UTEN_DATA); + assertThat(adressebeskyttelse).isEqualTo(Adressebeskyttelse.INGEN_BESKYTTELSE); + } + + @Test + public void hentNavn__tomt_navn_hvis_person_ikke_finens() { + PdlRespons pdlRespons = persondataService.hentPersondata(PERSON_FINNES_IKKE); + assertThat(PersondataService.hentNavnFraPdlRespons(pdlRespons)).isEqualTo(Navn.TOMT_NAVN); + } + + @Test + public void hentNavn__navn_hvis_person_finnes() { + PdlRespons pdlRespons = persondataService.hentPersondata(DONALD_DUCK); + Navn navn = PersondataService.hentNavnFraPdlRespons(pdlRespons); + assertThat(navn).isEqualTo(new Navn("Donald", null, "Duck")); + } + + @Test + public void erKode6Eller7__strengt_fortrolig() { + assertThat(persondataService.erKode6(STRENGT_FORTROLIG_PERSON)).isTrue(); + } + + @Test + public void erKode6Eller7__strengt_fortrolig_utland() { + assertThat(persondataService.erKode6(STRENGT_FORTROLIG_UTLAND_PERSON)).isTrue(); + } + + @Test + public void erKode6Eller7__fortrolig() { + assertThat(persondataService.erKode6(FORTROLIG_PERSON)).isFalse(); + } + + @Test + public void erKode6Eller7__ugradert() { + assertThat(persondataService.erKode6(UGRADERT_PERSON)).isFalse(); + } + + @Test + public void erKode6Eller7__ugradertTom() { + assertThat(persondataService.erKode6(UGRADERT_PERSON_TOM_RESPONSE)).isFalse(); + } + + @Test + public void erKode6Eller7__uspesifisert_gradering() { + assertThat(persondataService.erKode6(USPESIFISERT_GRADERT_PERSON)).isFalse(); + } + + @Test + public void erKode6Eller7_person_finnes_ikke_er_ok() { + assertThat(persondataService.erKode6(PERSON_FINNES_IKKE)).isFalse(); + } + + @Test + public void henterGeoTilhørighet() { + PdlRespons pdlRespons = persondataService.hentPersondata(DONALD_DUCK); + assertThat(PersondataService.hentGeoLokasjonFraPdlRespons(pdlRespons).get()).isEqualTo("030104"); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/RefusjonEndretStatusTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/RefusjonEndretStatusTest.java new file mode 100644 index 000000000..a2fc61118 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/RefusjonEndretStatusTest.java @@ -0,0 +1,35 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + +import com.fasterxml.jackson.core.JsonProcessingException; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.RefusjonStatus; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import no.nav.tag.tiltaksgjennomforing.avtale.TilskuddPeriode; +import no.nav.tag.tiltaksgjennomforing.avtale.TilskuddPeriodeRepository; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class RefusjonEndretStatusTest { + + @Test + public void skal_kunne_finne_riktig_tilskuddsperiode_og_lagre_status_uten_å_kaste_en_feil() throws JsonProcessingException { + // GITT + TilskuddPeriodeRepository tilskuddPeriodeRepository = mock(TilskuddPeriodeRepository.class); + Avtale avtale = TestData.enMidlertidigLonnstilskuddAvtaleMedAltUtfylt(); + TilskuddPeriode tilskuddPeriode = TestData.enTilskuddPeriode(); + when(tilskuddPeriodeRepository.findById(any())).thenReturn(Optional.of(tilskuddPeriode)); + + // NÅR + RefusjonEndretStatusKafkaConsumer consumer = new RefusjonEndretStatusKafkaConsumer(tilskuddPeriodeRepository); + + consumer.refusjonEndretStatus(new RefusjonEndretStatusMelding("1234", "1234", "1234", RefusjonStatus.UTBETALT, tilskuddPeriode.getId().toString())); + + // SÅ + verify(tilskuddPeriodeRepository).save(any()); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeAnnullertKafkaProducerTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeAnnullertKafkaProducerTest.java new file mode 100644 index 000000000..024af1e35 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeAnnullertKafkaProducerTest.java @@ -0,0 +1,68 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.FeatureToggleService; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.test.EmbeddedKafkaBroker; +import org.springframework.kafka.test.context.EmbeddedKafka; +import org.springframework.kafka.test.utils.KafkaTestUtils; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@SpringBootTest(properties = { "tiltaksgjennomforing.kafka.enabled=true" }) +@DirtiesContext +@ActiveProfiles({ Miljø.LOCAL }) +@EmbeddedKafka(partitions = 1, topics = { Topics.TILSKUDDSPERIODE_ANNULLERT }) +class TilskuddsperiodeAnnullertKafkaProducerTest { + + @Autowired + private TilskuddsperiodeKafkaProducer tilskuddsperiodeKafkaProducer; + + @Autowired + private EmbeddedKafkaBroker embeddedKafka; + + @MockBean + private FeatureToggleService featureToggleService; + + @Test + public void skal_kunne_sende_tilskuddperiode_annullert_på_kafka_topic() throws JSONException { + when(featureToggleService.isEnabled(anyString())).thenReturn(true); + + var consumerProps = KafkaTestUtils.consumerProps("testGroup", "false", embeddedKafka); + consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + ConsumerFactory consumerFactory = new DefaultKafkaConsumerFactory<>(consumerProps); + Consumer consumer = consumerFactory.createConsumer(); + embeddedKafka.consumeFromAllEmbeddedTopics(consumer); + + // GITT + final UUID tilskuddPeriodeId = UUID.randomUUID(); + var tilskuddMelding = new TilskuddsperiodeAnnullertMelding(tilskuddPeriodeId, TilskuddsperiodeAnnullertÅrsak.AVTALE_ANNULLERT); + + //NÅR + tilskuddsperiodeKafkaProducer.publiserTilskuddsperiodeAnnullertMelding(tilskuddMelding); + + //SÅ + ConsumerRecord record = KafkaTestUtils.getSingleRecord(consumer, Topics.TILSKUDDSPERIODE_ANNULLERT); + JSONObject jsonRefusjonRecord = new JSONObject(record.value()); + assertThat(jsonRefusjonRecord.get("tilskuddsperiodeId")).isEqualTo(tilskuddPeriodeId.toString()); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeGodkjentKafkaProducerTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeGodkjentKafkaProducerTest.java new file mode 100644 index 000000000..9023fe8b0 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeGodkjentKafkaProducerTest.java @@ -0,0 +1,123 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.FeatureToggleService; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.test.EmbeddedKafkaBroker; +import org.springframework.kafka.test.context.EmbeddedKafka; +import org.springframework.kafka.test.utils.KafkaTestUtils; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.when; + +@SpringBootTest(properties = { "tiltaksgjennomforing.kafka.enabled=true" }) +@DirtiesContext +@ActiveProfiles({ Miljø.LOCAL }) +@EmbeddedKafka(partitions = 1, topics = { Topics.TILSKUDDSPERIODE_GODKJENT }) +class TilskuddsperiodeGodkjentKafkaProducerTest { + + @Autowired + private TilskuddsperiodeKafkaProducer tilskuddsperiodeKafkaProducer; + + @Autowired + private EmbeddedKafkaBroker embeddedKafka; + + @MockBean + private FeatureToggleService featureToggleService; + + @Test + public void skal_kunne_sende_tilskuddperiode_godkjent_på_kafka_topic() throws JSONException { + when(featureToggleService.isEnabled(anyString())).thenReturn(true); + + Map consumerProps = KafkaTestUtils.consumerProps("testGroup", "false", embeddedKafka); + consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + var consumerFactory = new DefaultKafkaConsumerFactory(consumerProps); + var consumer = consumerFactory.createConsumer(); + embeddedKafka.consumeFromAllEmbeddedTopics(consumer); + + // GITT + final UUID avtaleId = UUID.randomUUID(); + final UUID tilskuddPeriodeId = UUID.randomUUID(); + final UUID avtaleInnholdId = UUID.randomUUID(); + final LocalDate avtaleFom = LocalDate.of(2023, 1, 1); + final LocalDate avtaleTom = LocalDate.of(2023, 5, 1); + final Tiltakstype tiltakstype = Tiltakstype.VARIG_LONNSTILSKUDD; + final String deltakerFornavn = "Donald"; + final String deltakerEtternavn = "Duck"; + final Identifikator deltakerFnr = new Fnr("12345678901"); + final String arbeidsgiverFornavn = "Arne"; + final String arbeidsgiverEtternavn = "Arbeidsgiver"; + final String arbeidsgiverTlf = "41111111"; + final NavIdent veilederNavIdent = new NavIdent("X123456"); + final String bedriftNavn = "Donald Delivery"; + final BedriftNr bedriftnummer = new BedriftNr("99999999"); + final Integer tilskuddBeløp = 12000; + final LocalDate tilskuddFraDato = Now.localDate().minusDays(15); + final LocalDate tilskuddTilDato = Now.localDate().plusMonths(2); + final Integer avtaleNr = 234234234; + final Integer løpenummer = 3; + final NavIdent beslutterNavIdent = new NavIdent("X234567"); + + final TilskuddsperiodeGodkjentMelding tilskuddMelding = new TilskuddsperiodeGodkjentMelding(avtaleId, + tilskuddPeriodeId, avtaleInnholdId, avtaleFom, avtaleTom, tiltakstype, deltakerFornavn, deltakerEtternavn, + deltakerFnr, arbeidsgiverFornavn, arbeidsgiverEtternavn, arbeidsgiverTlf, veilederNavIdent, bedriftNavn, bedriftnummer, tilskuddBeløp, tilskuddFraDato, tilskuddTilDato, 10.6, 0.02, 14.1, 60, avtaleNr, løpenummer, 0, + "4808", beslutterNavIdent, LocalDateTime.now()); + + //NÅR + tilskuddsperiodeKafkaProducer.publiserTilskuddsperiodeGodkjentMelding(tilskuddMelding); + + //SÅ + ConsumerRecord record = KafkaTestUtils.getSingleRecord(consumer, Topics.TILSKUDDSPERIODE_GODKJENT); + JSONObject jsonRefusjonRecord = new JSONObject(record.value()); + assertThat(jsonRefusjonRecord.get("avtaleId")).isNotNull(); + assertThat(jsonRefusjonRecord.get("tilskuddsperiodeId")).isNotNull(); + assertThat(jsonRefusjonRecord.get("avtaleInnholdId")).isNotNull(); + assertThat(jsonRefusjonRecord.get("tiltakstype")).isNotNull(); + assertThat(jsonRefusjonRecord.get("deltakerFornavn")).isNotNull(); + assertThat(jsonRefusjonRecord.get("deltakerEtternavn")).isNotNull(); + assertThat(jsonRefusjonRecord.get("deltakerFnr")).isNotNull(); + assertThat(jsonRefusjonRecord.get("arbeidsgiverFornavn")).isNotNull(); + assertThat(jsonRefusjonRecord.get("arbeidsgiverEtternavn")).isNotNull(); + assertThat(jsonRefusjonRecord.get("arbeidsgiverTlf")).isNotNull(); + assertThat(jsonRefusjonRecord.get("veilederNavIdent")).isNotNull(); + assertThat(jsonRefusjonRecord.get("bedriftNavn")).isNotNull(); + assertThat(jsonRefusjonRecord.get("bedriftNr")).isNotNull(); + assertThat(jsonRefusjonRecord.get("tilskuddsbeløp")).isNotNull(); + assertThat(jsonRefusjonRecord.get("tilskuddFom")).isNotNull().isOfAnyClassIn(String.class); + assertThat(jsonRefusjonRecord.get("tilskuddTom")).isNotNull().isOfAnyClassIn(String.class); + assertThat(jsonRefusjonRecord.get("feriepengerSats")).isNotNull(); + assertThat(jsonRefusjonRecord.get("otpSats")).isNotNull(); + assertThat(jsonRefusjonRecord.get("arbeidsgiveravgiftSats")).isNotNull(); + assertThat(jsonRefusjonRecord.get("lønnstilskuddsprosent")).isNotNull(); + assertThat(jsonRefusjonRecord.get("avtaleNr")).isNotNull(); + assertThat(jsonRefusjonRecord.get("løpenummer")).isNotNull(); + assertThat(jsonRefusjonRecord.get("beslutterNavIdent")).isNotNull(); + assertThat(jsonRefusjonRecord.get("godkjentTidspunkt")).isNotNull(); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeResendingTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeResendingTest.java new file mode 100644 index 000000000..eaf94b7f2 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/tilskuddsperiode/TilskuddsperiodeResendingTest.java @@ -0,0 +1,89 @@ +package no.nav.tag.tiltaksgjennomforing.tilskuddsperiode; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import no.nav.tag.tiltaksgjennomforing.featuretoggles.FeatureToggleService; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.test.EmbeddedKafkaBroker; +import org.springframework.kafka.test.context.EmbeddedKafka; +import org.springframework.kafka.test.utils.KafkaTestUtils; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDate; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +@SpringBootTest(properties = { "tiltaksgjennomforing.kafka.enabled=true" }) +@DirtiesContext +@ActiveProfiles({ Miljø.LOCAL }) +@EmbeddedKafka(partitions = 1, topics = { Topics.TILSKUDDSPERIODE_GODKJENT }) +public class TilskuddsperiodeResendingTest { + + @Autowired + private EmbeddedKafkaBroker embeddedKafka; + @Autowired + private AvtaleRepository avtaleRepository; + + @MockBean + private FeatureToggleService featureToggleService; + + @Test + public void sjekk_at_godkjent_med_samme_løpenummer_får_resendings_nummer() throws JSONException { + when(featureToggleService.isEnabled(anyString())).thenReturn(true); + + Map consumerProps = KafkaTestUtils.consumerProps("testGroup", "false", embeddedKafka); + consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + var consumerFactory = new DefaultKafkaConsumerFactory(consumerProps); + var consumer = consumerFactory.createConsumer(); + embeddedKafka.consumeFromAllEmbeddedTopics(consumer); + + Now.fixedDate(LocalDate.of(2023, 03, 1)); + LocalDate avtaleStart = LocalDate.of(2022, 10, 20); + LocalDate avtaleSlutt = LocalDate.of(2024, 3, 2); + Avtale avtale = TestData.enMidlertidigLønnstilskuddsAvtaleMedStartOgSluttGodkjentAvAlleParter(avtaleStart, avtaleSlutt); + // Godkjenner første gang. Denne skal ikke ha noen resendingsnummer + avtale.godkjennTilskuddsperiode(TestData.enNavIdent2(), "4321"); + avtale.nyeTilskuddsperioderEtterMigreringFraArena(LocalDate.of(2022, 10, 20), false); + // Nå er perioden annullert en gang, godkjenner igjen. Da den nå har den samme løpenr må den få resendingsnummer = 1 + avtale.godkjennTilskuddsperiode(TestData.enNavIdent2(), "1234"); + avtale.getTilskuddPeriode().forEach(periode -> System.out.println(periode.getStartDato() + " " + periode.getLøpenummer() + " " + periode.getStatus())); + avtaleRepository.save(avtale); + + //SÅ + ConsumerRecords records = KafkaTestUtils.getRecords(consumer); + records.records("Topics.TILSKUDDSPERIODE_GODKJENT").forEach(record -> { + try { + JSONObject jsonRefusjonRecord = new JSONObject(record.value()); + String enhet = (String)jsonRefusjonRecord.get("enhet"); + if("4321".equals(enhet)) { + assertThat(jsonRefusjonRecord.get("resendingsnummer")).isNull(); + } + if("1234".equals(enhet)) { + assertThat((int)jsonRefusjonRecord.get("resendingsnummer")).isEqualTo(1); + } + assertThat(jsonRefusjonRecord.get("avtaleId")).isNotNull(); + } catch (JSONException e) { + throw new RuntimeException(e); + } + }); + Now.resetClock(); + + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/utils/TelefonnummerValidatorTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/utils/TelefonnummerValidatorTest.java new file mode 100644 index 000000000..627aa7173 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/utils/TelefonnummerValidatorTest.java @@ -0,0 +1,23 @@ +package no.nav.tag.tiltaksgjennomforing.utils; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static no.nav.tag.tiltaksgjennomforing.utils.TelefonnummerValidator.erGyldigMobilnummer; +import static org.assertj.core.api.Assertions.assertThat; + +public class TelefonnummerValidatorTest { + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"9", "4", "22222222", "433333333333", "9x999999", "92222222 "}) + void erGyldigMobilnummer__false(String tlf) { + assertThat(erGyldigMobilnummer(tlf)).isFalse(); + } + + @ParameterizedTest + @ValueSource(strings = {"92222222", "44444444"}) + void erGyldigMobilnummer__true(String tlf) { + assertThat(erGyldigMobilnummer(tlf)).isTrue(); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/utils/UtilsTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/utils/UtilsTest.java new file mode 100644 index 000000000..e6f82d0e9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/utils/UtilsTest.java @@ -0,0 +1,22 @@ +package no.nav.tag.tiltaksgjennomforing.utils; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UtilsTest { + @Test + public void erIkkeTomme__med_null() { + assertThat(Utils.erIkkeTomme(1, "k", null)).isFalse(); + } + + @Test + public void erIkkeTomme__med_tom_streng() { + assertThat(Utils.erIkkeTomme(1, "k", "")).isFalse(); + } + + @Test + public void erIkkeTomme__uten_null() { + assertThat(Utils.erIkkeTomme(1, "k", new Object())).isTrue(); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/LagSmsFraAvtaleHendelseTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/LagSmsFraAvtaleHendelseTest.java new file mode 100644 index 000000000..1b1714eff --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/LagSmsFraAvtaleHendelseTest.java @@ -0,0 +1,418 @@ +package no.nav.tag.tiltaksgjennomforing.varsel; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.UUID; +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.*; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import no.nav.tag.tiltaksgjennomforing.varsel.kafka.SmsProducer; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.kafka.test.context.EmbeddedKafka; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(properties = { "tiltaksgjennomforing.kafka.enabled=true" }) +@DirtiesContext +@ActiveProfiles(Miljø.LOCAL) +@EmbeddedKafka(partitions = 1, topics = {Topics.TILTAK_SMS }) +class LagSmsFraAvtaleHendelseTest { + @Autowired + SmsRepository smsRepository; + @Autowired + AvtaleRepository avtaleRepository; + @SpyBean + SmsProducer smsProducer; + + private static final String SELVBETJENINGSONE_VARSELTEKST = "Du har mottatt et nytt varsel på https://arbeidsgiver.nav.no/tiltaksgjennomforing"; + private static final String FAGSYSTEMSONE_VARSELTEKST = "Du har mottatt et nytt varsel på https://tiltaksgjennomforing.intern.nav.no/tiltaksgjennomforing"; + + @Test + void avtaleDeltMedAvtalepart__skal_opprette_sms_til_riktig_mottaker() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + avtale.getGjeldendeInnhold().setDeltakerTlf("42234567"); + avtale.getGjeldendeInnhold().setMentorTlf("42234200"); + avtale.delMedAvtalepart(Avtalerolle.ARBEIDSGIVER); + avtale = avtaleRepository.save(avtale); + assertSmsOpprettetOgSendt(HendelseType.DELT_MED_ARBEIDSGIVER, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), SELVBETJENINGSONE_VARSELTEKST); + avtale.delMedAvtalepart(Avtalerolle.DELTAKER); + avtale = avtaleRepository.save(avtale); + assertSmsOpprettetOgSendt(HendelseType.DELT_MED_DELTAKER, avtale.getId(), avtale.getGjeldendeInnhold().getDeltakerTlf(), SELVBETJENINGSONE_VARSELTEKST); + avtale.delMedAvtalepart(Avtalerolle.MENTOR); + avtale = avtaleRepository.save(avtale); + assertSmsOpprettetOgSendt(HendelseType.DELT_MED_MENTOR, avtale.getId(), avtale.getGjeldendeInnhold().getMentorTlf(), SELVBETJENINGSONE_VARSELTEKST); + } + + @Test + void avtaleGodkjent() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + Deltaker deltaker = TestData.enDeltaker(avtale); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + avtale = avtaleRepository.save(avtale); + assertSmsOpprettetOgSendt(HendelseType.GODKJENT_AV_ARBEIDSGIVER, avtale.getId(), avtale.getGjeldendeInnhold().getVeilederTlf(), FAGSYSTEMSONE_VARSELTEKST); + deltaker.godkjennAvtale(Instant.now(), avtale); + avtale = avtaleRepository.save(avtale); + assertSmsOpprettetOgSendt(HendelseType.GODKJENT_AV_DELTAKER, avtale.getId(), avtale.getGjeldendeInnhold().getVeilederTlf(), FAGSYSTEMSONE_VARSELTEKST); + } + + @Test + void avtaleInngått() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + Veileder veileder = TestData.enVeileder(avtale); + GodkjentPaVegneGrunn godkjentPaVegneGrunn = new GodkjentPaVegneGrunn(); + godkjentPaVegneGrunn.setIkkeBankId(true); + veileder.godkjennForVeilederOgDeltaker(godkjentPaVegneGrunn, avtale); + avtaleRepository.save(avtale); + + assertSmsOpprettetOgSendt(HendelseType.AVTALE_INNGÅTT, avtale.getId(), avtale.getGjeldendeInnhold().getDeltakerTlf(), SELVBETJENINGSONE_VARSELTEKST); + assertSmsOpprettetOgSendt(HendelseType.AVTALE_INNGÅTT, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), SELVBETJENINGSONE_VARSELTEKST); + } + + @Test + void avtaleInngåttMentor() { + Avtale avtale = TestData.enMentorAvtaleUsignert(); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + Mentor mentor = TestData.enMentor(avtale); + mentor.godkjennAvtale(Instant.now(), avtale); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + Veileder veileder = TestData.enVeileder(avtale); + + GodkjentPaVegneGrunn godkjentPaVegneGrunn = new GodkjentPaVegneGrunn(); + godkjentPaVegneGrunn.setIkkeBankId(true); + veileder.godkjennForVeilederOgDeltaker(godkjentPaVegneGrunn, avtale); + + avtaleRepository.save(avtale); + + assertSmsOpprettetOgSendt(HendelseType.AVTALE_INNGÅTT, avtale.getId(), avtale.getGjeldendeInnhold().getDeltakerTlf(), SELVBETJENINGSONE_VARSELTEKST); + assertSmsOpprettetOgSendt(HendelseType.AVTALE_INNGÅTT, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), SELVBETJENINGSONE_VARSELTEKST); + assertSmsOpprettetOgSendt(HendelseType.AVTALE_INNGÅTT, avtale.getId(), avtale.getGjeldendeInnhold().getMentorTlf(), SELVBETJENINGSONE_VARSELTEKST); + } + + @Test + void godkjenningerOpphevet() { + Avtale avtale = TestData.enAvtaleMedAltUtfylt(); + Veileder veileder = TestData.enVeileder(avtale); + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + Deltaker deltaker = TestData.enDeltaker(avtale); + deltaker.godkjennAvtale(Instant.now(), avtale); + //Arbeidsgiver opphever deltaker + arbeidsgiver.opphevGodkjenninger(avtale); + avtale = avtaleRepository.save(avtale); + assertSmsOpprettetOgSendt(HendelseType.GODKJENNINGER_OPPHEVET_AV_ARBEIDSGIVER, avtale.getId(), avtale.getGjeldendeInnhold().getDeltakerTlf(), SELVBETJENINGSONE_VARSELTEKST); + + deltaker.godkjennAvtale(Instant.now(), avtale); + arbeidsgiver.godkjennAvtale(Instant.now(), avtale); + //Veileder opphever arbeidsgiver og deltaker + veileder.opphevGodkjenninger(avtale); + avtale = avtaleRepository.save(avtale); + assertSmsOpprettetOgSendt(HendelseType.GODKJENNINGER_OPPHEVET_AV_VEILEDER, avtale.getId(), avtale.getGjeldendeInnhold().getDeltakerTlf(), SELVBETJENINGSONE_VARSELTEKST); + assertSmsOpprettetOgSendt(HendelseType.GODKJENNINGER_OPPHEVET_AV_VEILEDER, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), SELVBETJENINGSONE_VARSELTEKST); + } + + @Test + void refusjon_somerjobb_klar() { + Avtale avtale = TestData.enSommerjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + LocalDate fristForGodkjenning = LocalDate.of(2022,04,05); + + // I et reelt scenario kan ikke refusjonKlar bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonKlar(fristForGodkjenning); + avtaleRepository.save(avtale); + + String meldingstekst = String.format("Dere kan nå søke om refusjon for tilskudd til sommerjobb for avtale med nr: %s. Frist for å søke er %s. Søk om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr(), fristForGodkjenning); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KLAR, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_midlertidig_lonnstilskudd_klar() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddsjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + LocalDate fristForGodkjenning = LocalDate.of(2022,04,05); + // I et reelt scenario kan ikke refusjonKlar bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonKlar(fristForGodkjenning); + avtaleRepository.save(avtale); + + String meldingstekst = String.format("Dere kan nå søke om refusjon for tilskudd til midlertidig lønnstilskudd for avtale med nr: %s. Frist for å søke er %s. Søk om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr(), fristForGodkjenning); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KLAR, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_varig_lonnstilskudd_Klar() { + Avtale avtale = TestData.enVarigLonnstilskuddsjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + LocalDate fristForGodkjenning = LocalDate.of(2022,04,05); + // I et reelt scenario kan ikke refusjonKlar bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonKlar(fristForGodkjenning); + avtaleRepository.save(avtale); + + String meldingstekst = String.format("Dere kan nå søke om refusjon for tilskudd til varig lønnstilskudd for avtale med nr: %s. Frist for å søke er %s. Søk om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr(), fristForGodkjenning); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KLAR, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_mentor_klar() { + Avtale avtale = TestData.enMentorAvtaleUsignert(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + LocalDate fristForGodkjenning = LocalDate.of(2022, 04, 05); + // I et reelt scenario kan ikke refusjonKlar bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonKlar(fristForGodkjenning); + avtaleRepository.save(avtale); + + String meldingstekst = String.format("Dere kan nå søke om refusjon for tilskudd til mentor for avtale med nr: %s. Frist for å søke %s . Søk om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr(), fristForGodkjenning); + assertSmsIkkeOpprettetEllerSendt(HendelseType.REFUSJON_KLAR, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_arbeidstrening_klar() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + LocalDate fristForGodkjenning = LocalDate.of(2022,04,05); + // I et reelt scenario kan ikke refusjonKlar bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonKlar(fristForGodkjenning); + avtaleRepository.save(avtale); + + String meldingstekst = String.format("Dere kan nå søke om refusjon for tilskudd til arbeidstrening for avtale med nr: %s. Frist for å søke %s. Søk om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr(), fristForGodkjenning); + assertSmsIkkeOpprettetEllerSendt(HendelseType.REFUSJON_KLAR, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + + } + + @Test + void refusjon_sommerjobb_klar_revarsel() { + Avtale avtale = TestData.enSommerjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + LocalDate fristForGodkjenning = LocalDate.of(2022,04,05); + // I et reelt scenario kan ikke refusjonRevarsel bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonRevarsel(fristForGodkjenning); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Fristen nærmer seg for å søke om refusjon for tilskudd til sommerjobb for avtale med nr: %s. Frist for å søke er %s. Søk om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr(), fristForGodkjenning); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KLAR_REVARSEL, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_midlertidig_lonnstilskudd_klar_revarsel() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddsjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + LocalDate fristForGodkjenning = LocalDate.of(2022,04,05); + // I et reelt scenario kan ikke refusjonRevarsel bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonRevarsel(fristForGodkjenning); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Fristen nærmer seg for å søke om refusjon for tilskudd til midlertidig lønnstilskudd for avtale med nr: %s. Frist for å søke er %s. Søk om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr(), fristForGodkjenning); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KLAR_REVARSEL, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_varig_lonnstilskudd_klar_revarsel() { + Avtale avtale = TestData.enVarigLonnstilskuddsjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + LocalDate fristForGodkjenning = LocalDate.of(2022,04,05); + // I et reelt scenario kan ikke refusjonRevarsel bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonRevarsel(fristForGodkjenning); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Fristen nærmer seg for å søke om refusjon for tilskudd til varig lønnstilskudd for avtale med nr: %s. Frist for å søke er %s. Søk om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr(), fristForGodkjenning); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KLAR_REVARSEL, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_mentor_klar_revarsel() { + Avtale avtale = TestData.enMentorAvtaleUsignert(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + LocalDate fristForGodkjenning = LocalDate.of(2022,04,05); + // I et reelt scenario kan ikke refusjonRevarsel bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonRevarsel(fristForGodkjenning); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Fristen nærmer seg for å søke om refusjon for tilskudd til mentor for avtale med nr: %s. Frist for å søke er %s. Søk om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr(), fristForGodkjenning); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KLAR_REVARSEL, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_arbeidstrening_klar_revarsel() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + LocalDate fristForGodkjenning = LocalDate.of(2022,04,05); + // I et reelt scenario kan ikke refusjonRevarsel bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonRevarsel(fristForGodkjenning); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Fristen nærmer seg for å søke om refusjon for tilskudd til arbeidstrening for avtale med nr: %s. Frist for å søke er %s. Søk om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr(), fristForGodkjenning); + assertSmsIkkeOpprettetEllerSendt(HendelseType.REFUSJON_KLAR_REVARSEL, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_sommerjobb_frist_forlenget() { + Avtale avtale = TestData.enSommerjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + // I et reelt scenario kan ikke refusjonFristForlenget bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonFristForlenget(); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Fristen for å godkjenne refusjon for avtale med nr: %s har blitt forlenget. Du kan sjekke fristen og søke om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr()); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_FRIST_FORLENGET, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_midlertidig_lonnstilskudd_frist_forlenget() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddsjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + // I et reelt scenario kan ikke refusjonFristForlenget bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonFristForlenget(); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Fristen for å godkjenne refusjon for avtale med nr: %s har blitt forlenget. Du kan sjekke fristen og søke om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr()); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_FRIST_FORLENGET, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_varig_lonnstilskudd_frist_forlenget() { + Avtale avtale = TestData.enVarigLonnstilskuddsjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + // I et reelt scenario kan ikke refusjonFristForlenget bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonFristForlenget(); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Fristen for å godkjenne refusjon for avtale med nr: %s har blitt forlenget. Du kan sjekke fristen og søke om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr()); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_FRIST_FORLENGET, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + + @Test + void refusjon_mentor_frist_forlenget() { + Avtale avtale = TestData.enMentorAvtaleUsignert(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + // I et reelt scenario kan ikke refusjonFristForlenget bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonFristForlenget(); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Fristen for å godkjenne refusjon for avtale med nr: %s har blitt forlenget. Du kan sjekke fristen og søke om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr()); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_FRIST_FORLENGET, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_arbeidstrening_frist_forlenget() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + // I et reelt scenario kan ikke refusjonFristForlenget bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonFristForlenget(); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Fristen for å godkjenne refusjon for avtale med nr: %s har blitt forlenget. Du kan sjekke fristen og søke om refusjon her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr()); + assertSmsIkkeOpprettetEllerSendt(HendelseType.REFUSJON_FRIST_FORLENGET, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_sommerjobb_korrigert() { + Avtale avtale = TestData.enSommerjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + // I et reelt scenario kan ikke refusjonKorrigert bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonKorrigert(); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Tidligere innsendt refusjon på avtale med nr %d er korrigert. Se detaljer her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr()); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KORRIGERT, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_midlertidig_lonnstilskudd_korrigert() { + Avtale avtale = TestData.enMidlertidigLonnstilskuddsjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + // I et reelt scenario kan ikke refusjonKorrigert bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonKorrigert(); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Tidligere innsendt refusjon på avtale med nr %d er korrigert. Se detaljer her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr()); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KORRIGERT, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_varig_lonnstilskudd_korrigert() { + Avtale avtale = TestData.enVarigLonnstilskuddsjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + // I et reelt scenario kan ikke refusjonKorrigert bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonKorrigert(); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Tidligere innsendt refusjon på avtale med nr %d er korrigert. Se detaljer her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr()); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KORRIGERT, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_mentor_korrigert() { + Avtale avtale = TestData.enMentorAvtaleUsignert(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + // I et reelt scenario kan ikke refusjonKorrigert bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonKorrigert(); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Tidligere innsendt refusjon på avtale med nr %d er korrigert. Se detaljer her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr()); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KORRIGERT, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + @Test + void refusjon_arbeidstrening_korrigert() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + // I et reelt scenario kan ikke refusjonKorrigert bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonKorrigert(); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Tidligere innsendt refusjon på avtale med nr %d er korrigert. Se detaljer her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr()); + assertSmsIkkeOpprettetEllerSendt(HendelseType.REFUSJON_KORRIGERT, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + } + + + @Test + void refusjonKorrigertKontaktperson__begge_skal_få_sms() { + Avtale avtale = TestData.enSommerjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + RefusjonKontaktperson refusjonKontaktperson = new RefusjonKontaktperson("Per", "Persen", "49876543", true); + avtale.getGjeldendeInnhold().setRefusjonKontaktperson(refusjonKontaktperson); + // I et reelt scenario kan ikke refusjonKorrigert bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonKorrigert(); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Tidligere innsendt refusjon på avtale med nr %d er korrigert. Se detaljer her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr()); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KORRIGERT, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KORRIGERT, avtale.getId(), avtale.getGjeldendeInnhold().getRefusjonKontaktperson().getRefusjonKontaktpersonTlf(), meldingstekst); + } + + @Test + void refusjonKorrigertKontaktperson__bare_kontaktperson_skal_få_sms() { + Avtale avtale = TestData.enSommerjobbAvtale(); + avtale.getGjeldendeInnhold().setArbeidsgiverTlf("41234567"); + RefusjonKontaktperson refusjonKontaktperson = new RefusjonKontaktperson("Per", "Persen", "49876543", false); + avtale.getGjeldendeInnhold().setRefusjonKontaktperson(refusjonKontaktperson); + // I et reelt scenario kan ikke refusjonKorrigert bli kalt uten at avtalen er godkjent av alle parter+beslutter ++ + avtale.refusjonKorrigert(); + avtaleRepository.save(avtale); + String meldingstekst = String.format("Tidligere innsendt refusjon på avtale med nr %d er korrigert. Se detaljer her: https://tiltak-refusjon.nav.no. Hilsen NAV.", avtale.getAvtaleNr()); + assertSmsIkkeOpprettetEllerSendt(HendelseType.REFUSJON_KORRIGERT, avtale.getId(), avtale.getGjeldendeInnhold().getArbeidsgiverTlf(), meldingstekst); + assertSmsOpprettetOgSendt(HendelseType.REFUSJON_KORRIGERT, avtale.getId(), avtale.getGjeldendeInnhold().getRefusjonKontaktperson().getRefusjonKontaktpersonTlf(), meldingstekst); + } + + private void assertSmsOpprettetOgSendt(HendelseType hendelseType, UUID avtaleId, String telefonnummer, String meldingstekst) { + assertThat(smsRepository.findAll()) + .filteredOn(sms -> sms.getHendelseType() == hendelseType + && sms.getAvtaleId().equals(avtaleId) + && sms.getTelefonnummer().equals(telefonnummer) + && sms.getMeldingstekst().equals(meldingstekst)) + .hasSize(1); + verify(smsProducer).sendSmsVarselMeldingTilKafka(argThat((Sms sms) -> + sms.getAvtaleId().equals(avtaleId) + && sms.getHendelseType().equals(hendelseType) + && sms.getTelefonnummer().equals(telefonnummer) + && sms.getMeldingstekst().equals(meldingstekst))); + } + + private void assertSmsIkkeOpprettetEllerSendt(HendelseType hendelseType, UUID avtaleId, String telefonnummer, String meldingstekst) { + assertThat(smsRepository.findAll()) + .filteredOn(sms -> sms.getHendelseType() == hendelseType + && sms.getAvtaleId().equals(avtaleId) + && sms.getTelefonnummer().equals(telefonnummer) + && sms.getMeldingstekst().equals(meldingstekst)) + .hasSize(0); + verify(smsProducer, never()).sendSmsVarselMeldingTilKafka(argThat((Sms sms) -> + sms.getAvtaleId().equals(avtaleId) + && sms.getHendelseType().equals(hendelseType) + && sms.getTelefonnummer().equals(telefonnummer) + && sms.getMeldingstekst().equals(meldingstekst))); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/LagVarselFraAvtaleHendelserTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/LagVarselFraAvtaleHendelserTest.java new file mode 100644 index 000000000..1052d8e1b --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/LagVarselFraAvtaleHendelserTest.java @@ -0,0 +1,272 @@ +package no.nav.tag.tiltaksgjennomforing.varsel; + +import static no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle.ARBEIDSGIVER; +import static no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle.BESLUTTER; +import static no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle.DELTAKER; +import static no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle.MENTOR; +import static no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle.VEILEDER; +import static no.nav.tag.tiltaksgjennomforing.avtale.HendelseType.*; +import static no.nav.tag.tiltaksgjennomforing.avtale.TestData.avtalerMedTilskuddsperioder; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.EnumSet; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.Arbeidsgiver; +import no.nav.tag.tiltaksgjennomforing.avtale.Avslagsårsak; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleInnholdRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.BedriftNr; +import no.nav.tag.tiltaksgjennomforing.avtale.Beslutter; +import no.nav.tag.tiltaksgjennomforing.avtale.Deltaker; +import no.nav.tag.tiltaksgjennomforing.avtale.EndreStillingsbeskrivelse; +import no.nav.tag.tiltaksgjennomforing.avtale.Fnr; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.avtale.NavIdent; +import no.nav.tag.tiltaksgjennomforing.avtale.OpprettAvtale; +import no.nav.tag.tiltaksgjennomforing.avtale.OpprettMentorAvtale; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import no.nav.tag.tiltaksgjennomforing.avtale.Veileder; +import no.nav.tag.tiltaksgjennomforing.datadeling.AvtaleMeldingEntitetRepository; +import no.nav.tag.tiltaksgjennomforing.datavarehus.DvhMeldingEntitetRepository; +import no.nav.tag.tiltaksgjennomforing.enhet.Kvalifiseringsgruppe; +import no.nav.tag.tiltaksgjennomforing.enhet.VeilarbArenaClient; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.ArbeidsgiverNotifikasjonRepository; +import no.nav.tag.tiltaksgjennomforing.varsel.oppgave.LagGosysVarselLytter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles(Miljø.LOCAL) +@DirtiesContext +class LagVarselFraAvtaleHendelserTest { + @Autowired + AvtaleRepository avtaleRepository; + @Autowired + AvtaleInnholdRepository avtaleInnholdRepository; + @Autowired + VarselRepository varselRepository; + @Autowired + ArbeidsgiverNotifikasjonRepository arbeidsgiverNotifikasjonRepository; + @Autowired + DvhMeldingEntitetRepository dvhMeldingEntitetRepository; + @Autowired + AvtaleMeldingEntitetRepository avtaleMeldingEntitetRepository; + @MockBean + LagGosysVarselLytter lagGosysVarselLytter; + @Autowired + VeilarbArenaClient veilarbArenaClient; + @Autowired + SmsRepository smsRepository; + + @BeforeEach + void setUp() { + smsRepository.deleteAll(); + varselRepository.deleteAll(); + arbeidsgiverNotifikasjonRepository.deleteAll(); + avtaleInnholdRepository.deleteAll(); + dvhMeldingEntitetRepository.deleteAll(); + avtaleMeldingEntitetRepository.deleteAll(); + avtaleRepository.deleteAll(); + } + + @Test + void test_alt() { + Avtale avtale = avtaleRepository.save(Avtale.veilederOppretterAvtale(new OpprettAvtale(new Fnr("00000000000"), new BedriftNr("999999999"), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD), TestData.enNavIdent())); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + + assertHendelse(OPPRETTET, VEILEDER, VEILEDER, false); + assertHendelse(OPPRETTET, VEILEDER, ARBEIDSGIVER, true); + assertHendelse(OPPRETTET, VEILEDER, DELTAKER, true); + + avtale.endreAvtale(Now.instant(), TestData.endringPåAlleLønnstilskuddFelter(), ARBEIDSGIVER, avtalerMedTilskuddsperioder); + avtale = avtaleRepository.save(avtale); + assertHendelse(ENDRET, ARBEIDSGIVER, VEILEDER, true); + assertHendelse(ENDRET, ARBEIDSGIVER, ARBEIDSGIVER, false); + assertHendelse(ENDRET, ARBEIDSGIVER, DELTAKER, true); + + avtale.togglegodkjennEtterregistrering(TestData.enNavIdent()); + avtale = avtaleRepository.save(avtale); + assertHendelse(GODKJENT_FOR_ETTERREGISTRERING, BESLUTTER, VEILEDER, true); + + avtale.togglegodkjennEtterregistrering(TestData.enNavIdent()); + avtale = avtaleRepository.save(avtale); + assertHendelse(FJERNET_ETTERREGISTRERING, BESLUTTER, VEILEDER, true); + + avtale.delMedAvtalepart(DELTAKER); + avtale = avtaleRepository.save(avtale); + assertHendelse(DELT_MED_DELTAKER, VEILEDER, VEILEDER, false); + assertHendelse(DELT_MED_DELTAKER, VEILEDER, DELTAKER, true); + assertIngenHendelse(DELT_MED_DELTAKER, ARBEIDSGIVER); + + avtale.delMedAvtalepart(ARBEIDSGIVER); + avtale = avtaleRepository.save(avtale); + assertHendelse(DELT_MED_ARBEIDSGIVER, VEILEDER, VEILEDER, false); + assertHendelse(DELT_MED_ARBEIDSGIVER, VEILEDER, ARBEIDSGIVER, true); + assertIngenHendelse(DELT_MED_ARBEIDSGIVER, DELTAKER); + + Deltaker deltaker = TestData.enDeltaker(avtale); + deltaker.godkjennAvtale(Now.instant(), avtale); + avtale = avtaleRepository.save(avtale); + assertHendelse(GODKJENT_AV_DELTAKER, DELTAKER, VEILEDER, true); + assertHendelse(GODKJENT_AV_DELTAKER, DELTAKER, ARBEIDSGIVER, true); + assertHendelse(GODKJENT_AV_DELTAKER, DELTAKER, DELTAKER, false); + + Veileder veileder = TestData.enVeileder(avtale); + veileder.opphevGodkjenninger(avtale); + avtale = avtaleRepository.save(avtale); + assertHendelse(GODKJENNINGER_OPPHEVET_AV_VEILEDER, VEILEDER, VEILEDER, false); + assertHendelse(GODKJENNINGER_OPPHEVET_AV_VEILEDER, VEILEDER, ARBEIDSGIVER, true); + assertHendelse(GODKJENNINGER_OPPHEVET_AV_VEILEDER, VEILEDER, DELTAKER, true); + + Arbeidsgiver arbeidsgiver = TestData.enArbeidsgiver(avtale); + arbeidsgiver.godkjennAvtale(Now.instant(), avtale); + avtale = avtaleRepository.save(avtale); + assertHendelse(GODKJENT_AV_ARBEIDSGIVER, ARBEIDSGIVER, VEILEDER, true); + assertHendelse(GODKJENT_AV_ARBEIDSGIVER, ARBEIDSGIVER, ARBEIDSGIVER, false); + assertHendelse(GODKJENT_AV_ARBEIDSGIVER, ARBEIDSGIVER, DELTAKER, true); + + arbeidsgiver.opphevGodkjenninger(avtale); + avtale = avtaleRepository.save(avtale); + assertHendelse(GODKJENNINGER_OPPHEVET_AV_ARBEIDSGIVER, ARBEIDSGIVER, VEILEDER, true); + assertHendelse(GODKJENNINGER_OPPHEVET_AV_ARBEIDSGIVER, ARBEIDSGIVER, ARBEIDSGIVER, false); + assertHendelse(GODKJENNINGER_OPPHEVET_AV_ARBEIDSGIVER, ARBEIDSGIVER, DELTAKER, true); + + arbeidsgiver.godkjennAvtale(Now.instant(), avtale); + veileder.godkjennForVeilederOgDeltaker(TestData.enGodkjentPaVegneGrunn(), avtale); + avtale = avtaleRepository.save(avtale); + assertHendelse(GODKJENT_PAA_VEGNE_AV, VEILEDER, VEILEDER, false); + assertIngenHendelse(GODKJENT_PAA_VEGNE_AV, ARBEIDSGIVER); + assertIngenHendelse(GODKJENT_PAA_VEGNE_AV, DELTAKER); + + Beslutter beslutter = TestData.enBeslutter(avtale); + beslutter.avslåTilskuddsperiode(avtale, EnumSet.of(Avslagsårsak.FEIL_I_REGELFORSTÅELSE), "Forklaring"); + avtale = avtaleRepository.save(avtale); + assertHendelse(TILSKUDDSPERIODE_AVSLATT, BESLUTTER, VEILEDER, true); + assertIngenHendelse(TILSKUDDSPERIODE_AVSLATT, ARBEIDSGIVER); + assertIngenHendelse(TILSKUDDSPERIODE_AVSLATT, DELTAKER); + + veileder.sendTilbakeTilBeslutter(avtale); + beslutter.godkjennTilskuddsperiode(avtale, TestData.ENHET_OPPFØLGING.getVerdi()); + avtale = avtaleRepository.save(avtale); + assertHendelse(TILSKUDDSPERIODE_GODKJENT, BESLUTTER, VEILEDER, true); + assertIngenHendelse(TILSKUDDSPERIODE_GODKJENT, ARBEIDSGIVER); + assertIngenHendelse(TILSKUDDSPERIODE_GODKJENT, DELTAKER); + + veileder.endreStillingbeskrivelse(EndreStillingsbeskrivelse.builder().stillingstittel("Tittel").arbeidsoppgaver("Oppgaver").stillingprosent(100).stillingKonseptId(1).stillingStyrk08(1).antallDagerPerUke(5).build(), avtale); + avtale = avtaleRepository.save(avtale); + assertHendelse(STILLINGSBESKRIVELSE_ENDRET, VEILEDER, VEILEDER, false); + assertHendelse(STILLINGSBESKRIVELSE_ENDRET, VEILEDER, ARBEIDSGIVER, true); + assertHendelse(STILLINGSBESKRIVELSE_ENDRET, VEILEDER, DELTAKER, true); + + Veileder nyVeileder = TestData.enVeileder(new NavIdent("I000000")); + nyVeileder.overtaAvtale(avtale); + avtale = avtaleRepository.save(avtale); + assertHendelse(NY_VEILEDER, VEILEDER, VEILEDER, false); + assertHendelse(NY_VEILEDER, VEILEDER, ARBEIDSGIVER, true); + assertHendelse(NY_VEILEDER, VEILEDER, DELTAKER, true); + } + + @Test + void test_for_arbeidsgiver_oppretter() { + Avtale avtale = avtaleRepository.save(Avtale.arbeidsgiverOppretterAvtale(new OpprettAvtale(new Fnr("00000000000"), new BedriftNr("999999999"), Tiltakstype.MIDLERTIDIG_LONNSTILSKUDD))); + + assertHendelse(OPPRETTET_AV_ARBEIDSGIVER, ARBEIDSGIVER, VEILEDER, true); + assertHendelse(OPPRETTET_AV_ARBEIDSGIVER, ARBEIDSGIVER, ARBEIDSGIVER, false); + assertHendelse(OPPRETTET_AV_ARBEIDSGIVER, ARBEIDSGIVER, DELTAKER, true); + + Veileder veileder = TestData.enVeileder(TestData.enNavIdent()); + veileder.overtaAvtale(avtale); + avtale = avtaleRepository.save(avtale); + assertHendelse(AVTALE_FORDELT, VEILEDER, VEILEDER, false); + assertHendelse(AVTALE_FORDELT, VEILEDER, ARBEIDSGIVER, true); + assertHendelse(AVTALE_FORDELT, VEILEDER, DELTAKER, true); + } + + @Test + void test_for_arbeidsgiver_oppretter_mentor_avtale() { + Avtale avtale = avtaleRepository.save(Avtale.arbeidsgiverOppretterAvtale(new OpprettMentorAvtale(new Fnr("00000000000"),new Fnr("00000000000"), new BedriftNr("999999999"), Tiltakstype.MENTOR, ARBEIDSGIVER))); + + assertHendelse(OPPRETTET_AV_ARBEIDSGIVER, ARBEIDSGIVER, VEILEDER, true); + assertHendelse(OPPRETTET_AV_ARBEIDSGIVER, ARBEIDSGIVER, ARBEIDSGIVER, false); + assertHendelse(OPPRETTET_AV_ARBEIDSGIVER, ARBEIDSGIVER, DELTAKER, true); + + Veileder veileder = TestData.enVeileder(TestData.enNavIdent()); + veileder.overtaAvtale(avtale); + avtale = avtaleRepository.save(avtale); + assertHendelse(AVTALE_FORDELT, VEILEDER, VEILEDER, false); + assertHendelse(AVTALE_FORDELT, VEILEDER, ARBEIDSGIVER, true); + assertHendelse(AVTALE_FORDELT, VEILEDER, DELTAKER, true); + assertHendelse(AVTALE_FORDELT, VEILEDER, MENTOR, true); + } + + @Test + void test_for_delt_med_mentor() { + Avtale avtale = avtaleRepository.save(Avtale.veilederOppretterAvtale(new OpprettMentorAvtale(new Fnr("00000000000") , new Fnr("00000000000"), new BedriftNr("999999999"), Tiltakstype.MENTOR, VEILEDER), TestData.enNavIdent())); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + avtale.endreAvtale(Now.instant(), TestData.endringPåAlleMentorFelter(), VEILEDER, avtalerMedTilskuddsperioder); + avtale = avtaleRepository.save(avtale); + + avtale.delMedAvtalepart(DELTAKER); + avtale = avtaleRepository.save(avtale); + assertHendelse(DELT_MED_DELTAKER, VEILEDER, VEILEDER, false); + assertHendelse(DELT_MED_DELTAKER, VEILEDER, DELTAKER, true); + assertIngenHendelse(DELT_MED_DELTAKER, ARBEIDSGIVER); + + avtale.delMedAvtalepart(MENTOR); + avtale = avtaleRepository.save(avtale); + assertHendelse(DELT_MED_MENTOR, VEILEDER, VEILEDER, false); + assertHendelse(DELT_MED_MENTOR, VEILEDER, MENTOR, true); + assertIngenHendelse(DELT_MED_MENTOR, ARBEIDSGIVER); + + } + + @Test + void forleng_avtale() { + Avtale avtale = TestData.enLonnstilskuddAvtaleGodkjentAvVeileder(); + avtale.setKvalifiseringsgruppe(Kvalifiseringsgruppe.VARIG_TILPASSET_INNSATS); + avtale = avtaleRepository.save(avtale); + Veileder veileder = TestData.enVeileder(avtale); + + veileder.forlengAvtale(avtale.getGjeldendeInnhold().getSluttDato().plusMonths(1), avtale); + avtaleRepository.save(avtale); + + assertHendelse(AVTALE_FORLENGET, VEILEDER, VEILEDER, false); + assertHendelse(AVTALE_FORLENGET, VEILEDER, ARBEIDSGIVER, true); + assertHendelse(AVTALE_FORLENGET, VEILEDER, DELTAKER, true); + } + + @Test + void endre_tilskuddsberegning() { + Avtale avtale = avtaleRepository.save(TestData.enLonnstilskuddAvtaleGodkjentAvVeileder()); + Veileder veileder = TestData.enVeileder(avtale); + + veileder.endreTilskuddsberegning(TestData.enEndreTilskuddsberegning(), avtale); + avtaleRepository.save(avtale); + + assertHendelse(TILSKUDDSBEREGNING_ENDRET, VEILEDER, VEILEDER, false); + assertHendelse(TILSKUDDSBEREGNING_ENDRET, VEILEDER, ARBEIDSGIVER, true); + assertHendelse(TILSKUDDSBEREGNING_ENDRET, VEILEDER, DELTAKER, true); + } + + private void assertHendelse(HendelseType hendelseType, Avtalerolle utførtAv, Avtalerolle mottaker, boolean bjelle) { + assertThat(varselRepository.findAll()) + .filteredOn(varsel -> varsel.getMottaker() == mottaker && varsel.getUtførtAv() == utførtAv && varsel.getHendelseType() == hendelseType && varsel.isBjelle() == bjelle) + .hasSize(1); + } + + private void assertIngenHendelse(HendelseType hendelseType, Avtalerolle mottaker) { + assertThat(varselRepository.findAll()) + .filteredOn(varsel -> varsel.getMottaker() == mottaker && varsel.getHendelseType() == hendelseType) + .isEmpty(); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/SporingsloggRepositoryTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/SporingsloggRepositoryTest.java new file mode 100644 index 000000000..280a70120 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/SporingsloggRepositoryTest.java @@ -0,0 +1,36 @@ +package no.nav.tag.tiltaksgjennomforing.varsel; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import no.nav.tag.tiltaksgjennomforing.sporingslogg.Sporingslogg; +import no.nav.tag.tiltaksgjennomforing.sporingslogg.SporingsloggRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles(Miljø.LOCAL) +@DirtiesContext +public class SporingsloggRepositoryTest { + @Autowired + private SporingsloggRepository sporingsloggRepository; + @Autowired + private AvtaleRepository avtaleRepository; + + @Test + public void save__lagrer_alle_felter() { + Avtale avtale = TestData.enArbeidstreningAvtale(); + avtaleRepository.save(avtale); + Sporingslogg sporingslogg = TestData.enHendelse(avtale); + Sporingslogg lagretSporingslogg = sporingsloggRepository.save(sporingslogg); + assertThat(lagretSporingslogg.getId()).isNotNull(); + assertThat(lagretSporingslogg.getTidspunkt()).isNotNull(); + assertThat(lagretSporingslogg).isEqualToIgnoringNullFields(sporingslogg); + } +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/VarselFactoryTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/VarselFactoryTest.java new file mode 100644 index 000000000..a41752dfc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/VarselFactoryTest.java @@ -0,0 +1,24 @@ +package no.nav.tag.tiltaksgjennomforing.varsel; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import no.nav.tag.tiltaksgjennomforing.avtale.Avtalerolle; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import org.junit.jupiter.api.Test; + +class VarselFactoryTest { + + @Test + public void skal_returnere_4_parter_Mentor_Deltaker_Arbeidsgiver_Veileder_Ventor_I_VarselListe(){ + VarselFactory factory = new VarselFactory(TestData.enMentorAvtaleUsignert(), Avtalerolle.MENTOR, TestData.enNavIdent() , HendelseType.OPPRETTET); + assertEquals(4,factory.alleParter().toArray().length); + } + + @Test + public void skal_returnere_3_parter_Deltaker_Arbeidsgiver_Veileder_Ventor_I_VarselListe(){ + VarselFactory factory = new VarselFactory(TestData.enArbeidstreningAvtale(), Avtalerolle.ARBEIDSGIVER, TestData.enNavIdent(), HendelseType.OPPRETTET); + assertEquals(3,factory.alleParter().toArray().length); + } + +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselConsumerTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselConsumerTest.java new file mode 100644 index 000000000..d0a65a30a --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselConsumerTest.java @@ -0,0 +1,81 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.kafka; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import no.nav.tag.tiltaksgjennomforing.utils.Now; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.support.serializer.JsonDeserializer; +import org.springframework.kafka.test.EmbeddedKafkaBroker; +import org.springframework.kafka.test.context.EmbeddedKafka; +import org.springframework.kafka.test.utils.KafkaTestUtils; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.time.LocalDate; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + + +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { "tiltaksgjennomforing.kafka.enabled=true" }) +@DirtiesContext +@ActiveProfiles({ Miljø.LOCAL }) +@EnableKafka +@EmbeddedKafka(partitions = 1, topics = { Topics.TILTAK_VARSEL }) + +class RefusjonVarselConsumerTest { + + @Autowired + RefusjonVarselTestProducer refusjonVarselTestProducer; + + @Autowired + private AvtaleRepository avtaleRepository; + + @Autowired + EmbeddedKafkaBroker embeddedKafkaBroker; + + @Test + public void skal_sende_sms_når_det_leses_varsel_kafkamelding() throws InterruptedException { + Now.fixedDate(LocalDate.of(2021, 6, 1)); + Avtale avtale = TestData.enSommerjobbAvtaleGodkjentAvBeslutter(); + avtale = avtaleRepository.save(avtale); + LocalDate fristForGodkjenning = avtale.tilskuddsperiode(0).getSluttDato().plusMonths(2); + var varselMelding = new RefusjonVarselMelding( + avtale.getId(), + avtale.tilskuddsperiode(0).getId(), + VarselType.KLAR, + fristForGodkjenning + ); + refusjonVarselTestProducer.publiserMelding("testId-KLAR", varselMelding); + Thread.sleep(1000L); + + + Map consumerProps = KafkaTestUtils.consumerProps("testGroup", "true", this.embeddedKafkaBroker); + consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + ConsumerFactory consumerFactory = new DefaultKafkaConsumerFactory<>( + consumerProps, + new StringDeserializer(), + new JsonDeserializer<>(RefusjonVarselMelding.class) + ); + Consumer consumer = consumerFactory.createConsumer(); + this.embeddedKafkaBroker.consumeFromAnEmbeddedTopic(consumer, Topics.TILTAK_VARSEL); + ConsumerRecords replies = KafkaTestUtils.getRecords(consumer); + assertThat(replies.count()).isGreaterThanOrEqualTo(1); + Now.resetClock(); + } + +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselTestProducer.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselTestProducer.java new file mode 100644 index 000000000..1c3f73817 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselTestProducer.java @@ -0,0 +1,38 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.kafka; + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; +import org.springframework.stereotype.Component; +import org.springframework.util.concurrent.ListenableFutureCallback; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Component +@Slf4j +public class RefusjonVarselTestProducer { + KafkaTemplate kafkaTemplate; + + RefusjonVarselTestProducer(@Qualifier("refusjonVarselTestMeldingKafkaTemplate") KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + public void publiserMelding(String meldingId, RefusjonVarselMelding refusjonVarselMelding) { + kafkaTemplate.send(Topics.TILTAK_VARSEL, meldingId, refusjonVarselMelding) + .addCallback(new ListenableFutureCallback<>() { + @Override + public void onSuccess(SendResult result) { + log.info("Melding med id {} sendt til Kafka topic {}", meldingId, Topics.TILTAK_VARSEL); + } + + @Override + public void onFailure(Throwable ex) { + log.error("Melding med id {} kunne ikke sendes til Kafka topic {}", meldingId, Topics.TILTAK_VARSEL); + log.error("Feilmelding: ", ex); + } + }); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselTestProducerConfig.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselTestProducerConfig.java new file mode 100644 index 000000000..2a5a39029 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/RefusjonVarselTestProducerConfig.java @@ -0,0 +1,62 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.kafka; + +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.config.SslConfigs; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.support.serializer.JsonSerializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; + +import java.util.HashMap; +import java.util.Map; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Configuration +@EnableKafka +public class RefusjonVarselTestProducerConfig { + + @Value("${no.nav.gcp.kafka.aiven.bootstrap-servers}") + private String bootstrapServers; + @Value("${no.nav.gcp.kafka.aiven.security-protocol}") + private String securityProtocol; + @Value("${no.nav.gcp.kafka.aiven.truststore-path}") + private String sslTruststoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.truststore-password}") + private String sslTruststorePasswordEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-path}") + private String sslKeystoreLocationEnvKey; + @Value("${no.nav.gcp.kafka.aiven.keystore-password}") + private String sslKeystorePasswordEnvKey; + + private Map producerKafkaConfig() { + final String javaKeystore = "jks"; + final String pkcs12 = "PKCS12"; + Map props = new HashMap<>(); + + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol); + props.put(CommonClientConfigs.GROUP_ID_CONFIG, "tiltaksgjennomforing-api"); + props.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, ""); + props.put(SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG, javaKeystore); + props.put(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG, pkcs12); + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, sslTruststoreLocationEnvKey); + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, sslTruststorePasswordEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, sslKeystoreLocationEnvKey); + props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, sslKeystorePasswordEnvKey); + return props; + } + + @Bean + public KafkaTemplate refusjonVarselTestMeldingKafkaTemplate() { + return new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(producerKafkaConfig())); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/SmsAivenKafkaConfiguration.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/SmsAivenKafkaConfiguration.java new file mode 100644 index 000000000..0754b638e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/SmsAivenKafkaConfiguration.java @@ -0,0 +1,41 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.kafka; + +import lombok.extern.slf4j.Slf4j; +import no.nav.tag.tiltaksgjennomforing.varsel.Sms; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.serializer.JsonSerializer; + +import java.util.HashMap; +import java.util.Map; + +@ConditionalOnProperty("tiltaksgjennomforing.kafka.enabled") +@Configuration +@Slf4j +@EnableKafka +public class SmsAivenKafkaConfiguration { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapAddress; + + @Bean + public KafkaTemplate aivenTiltaksgjennomforingVarsel() { + return new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(getProducerConfigs())); + } + + private Map getProducerConfigs() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + props.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, false); + return props; + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/SmsVarselProducerTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/SmsVarselProducerTest.java new file mode 100644 index 000000000..e78a4ef74 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/kafka/SmsVarselProducerTest.java @@ -0,0 +1,65 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.kafka; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.avtale.Identifikator; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.kafka.Topics; +import no.nav.tag.tiltaksgjennomforing.varsel.Sms; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.test.EmbeddedKafkaBroker; +import org.springframework.kafka.test.context.EmbeddedKafka; +import org.springframework.kafka.test.utils.KafkaTestUtils; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@DirtiesContext +@SpringBootTest(properties = { "tiltaksgjennomforing.kafka.enabled=true" }) +@ActiveProfiles({ Miljø.LOCAL }) +@EmbeddedKafka(partitions = 1, controlledShutdown = false, topics = { Topics.TILTAK_SMS }) +public class SmsVarselProducerTest { + @Autowired + private EmbeddedKafkaBroker embeddedKafka; + @Autowired + private SmsProducer producer; + + private Consumer consumer; + + @BeforeEach + public void setUp() { + Map consumerProps = KafkaTestUtils.consumerProps("testGroup", "false", embeddedKafka); + consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + + ConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); + consumer = cf.createConsumer(); + embeddedKafka.consumeFromAnEmbeddedTopic(consumer, Topics.TILTAK_SMS); + } + + @Test + public void smsVarselOpprettet__skal_sendes_på_kafka_topic_med_riktige_felter() throws JSONException { + producer.sendSmsVarselMeldingTilKafka(Sms.nyttVarsel("tlf", new Identifikator("id"), "melding", HendelseType.AVTALE_INNGÅTT, UUID.randomUUID())); + + ConsumerRecord record = KafkaTestUtils.getSingleRecord(consumer, Topics.TILTAK_SMS); + JSONObject json = new JSONObject(record.value()); + assertThat(json.getString("smsVarselId")).isNotNull(); + assertThat(json.getString("identifikator")).isEqualTo("id"); + assertThat(json.getString("meldingstekst")).isEqualTo("melding"); + assertThat(json.getString("telefonnummer")).isEqualTo("tlf"); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonHandlerTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonHandlerTest.java new file mode 100644 index 000000000..d19ecc2f1 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonHandlerTest.java @@ -0,0 +1,113 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.FellesResponse; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.MutationStatus; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.nyBeskjed.NyBeskjedResponse; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.nyOppgave.NyOppgaveResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; + +@SpringBootTest +@ActiveProfiles({Miljø.LOCAL}) +@DirtiesContext +public class NotifikasjonHandlerTest { + + @Autowired + private NotifikasjonHandler notifikasjonHandler; + + @Autowired + private NotifikasjonService notifikasjonService; + + @Autowired + NotifikasjonParser parser; + + @MockBean + private ArbeidsgiverNotifikasjonRepository arbeidsgiverNotifikasjonRepository; + + String response; + + @BeforeEach + public void init() { + response = """ + { + "data": { + "nyBeskjed": { + "__typename": "NyBeskjedVellykket", + "id": "d69f8c4f-8d34-47b0-9539-d3c2e54115da" + } + } + }"""; + } + + @Test + public void sjekkOgSettStatusResponseTest() { + ArbeidsgiverNotifikasjon arbeidsgiverNotifikasjon = new ArbeidsgiverNotifikasjon(); + FellesResponse response = new FellesResponse("" + + MutationStatus.NY_OPPGAVE_VELLYKKET.getStatus(), + "231a0f8c-237c-4357-8101-6a356a9ace86", + "nihil ut eum alias saepe nesciunt minima"); + MutationStatus mutationStatus = MutationStatus.NY_OPPGAVE_VELLYKKET; + + notifikasjonHandler.sjekkOgSettStatusResponse(arbeidsgiverNotifikasjon, response, mutationStatus); + + Mockito.verify(arbeidsgiverNotifikasjonRepository).save(any()); + } + + @Test + public void readResponseTest() { + final NyBeskjedResponse nyBeskjedResponse = notifikasjonHandler.readResponse(response, NyBeskjedResponse.class); + final NyOppgaveResponse feiletObjectMapping = + notifikasjonHandler.readResponse(response, NyOppgaveResponse.class); + + assertThat(nyBeskjedResponse.getData().getNyBeskjed()).isNotNull(); + assertThat(feiletObjectMapping.getData().getNyOppgave()).isNull(); + } + + @Test + public void readResponseNarAPIsenderError() { + final String response = "{ \"errors\" :[ " + + "{ \"message\": \"Field 'eksternId' of variable 'eksternId' has coerced Null value for NonNull type 'ID!'\"," + + " \"locations\":[ { \"line\":1, \"column\":36 } ], \"extensions\":{ \"classification\": \"ValidationError\" } } ] }"; + + ArbeidsgiverNotifikasjon notifikasjon = + ArbeidsgiverNotifikasjon.nyHendelse(TestData.enArbeidstreningAvtale(), + HendelseType.GODKJENT_AV_VEILEDER, + notifikasjonService, parser); + + final NyBeskjedResponse parsetBeskjedResponse = notifikasjonHandler.readResponse(response, NyBeskjedResponse.class); + + assertThat(parsetBeskjedResponse.getData()).isNull(); + + notifikasjonHandler.logErrorOgSettFeilmelding(response, notifikasjon); + + assertThat(notifikasjon.getStatusResponse()) + .isEqualTo("Field 'eksternId' of variable 'eksternId' has coerced Null value for NonNull type 'ID!'"); + + + } + + @Test + public void convertResponseTest() { + final NyBeskjedResponse nyBeskjedResponse = notifikasjonHandler.readResponse(response, NyBeskjedResponse.class); + FellesResponse fellesResponse = + notifikasjonHandler.konverterResponse(nyBeskjedResponse.getData().getNyBeskjed()); + FellesResponse fellesResponseFeilet = + notifikasjonHandler.konverterResponse(nyBeskjedResponse.getData()); + + assertThat(fellesResponse.getId()).isNotNull(); + assertThat(fellesResponseFeilet.getId()).isNull(); + + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonServiceTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonServiceTest.java new file mode 100644 index 000000000..05862ea36 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/notifikasjon/NotifikasjonServiceTest.java @@ -0,0 +1,238 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon; + +import no.nav.tag.tiltaksgjennomforing.Miljø; +import no.nav.tag.tiltaksgjennomforing.avtale.Avtale; +import no.nav.tag.tiltaksgjennomforing.avtale.AvtaleRepository; +import no.nav.tag.tiltaksgjennomforing.avtale.HendelseType; +import no.nav.tag.tiltaksgjennomforing.avtale.TestData; +import no.nav.tag.tiltaksgjennomforing.varsel.notifikasjon.response.MutationStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = {"tiltaksgjennomforing.notifikasjoner.enabled=true"}) +@ActiveProfiles({Miljø.LOCAL, "wiremock"}) +@DirtiesContext +public class NotifikasjonServiceTest { + + @Autowired + NotifikasjonService notifikasjonService; + + @Autowired + NotifikasjonParser parser; + + @Autowired + ArbeidsgiverNotifikasjonRepository arbeidsgiverNotifikasjonRepository; + + @Autowired + AvtaleRepository avtaleRepository; + + Avtale avtale; + ArbeidsgiverNotifikasjon notifikasjon; + + @BeforeEach + public void init() { + avtale = TestData.enArbeidstreningAvtale(); + avtaleRepository.save(avtale); + notifikasjon = ArbeidsgiverNotifikasjon.nyHendelse( + avtale, + HendelseType.OPPRETTET, + notifikasjonService, + parser); + } + + private List finnAntallNotifikasjonerMedGittMutasjonStatus( + List notifikasjonList, MutationStatus onsketStatus) { + return notifikasjonList.stream(). + filter(n -> n.getStatusResponse().equals(onsketStatus.getStatus())).collect(Collectors.toList()); + } + + @Test + public void opprettNyBeskjedTest() { + arbeidsgiverNotifikasjonRepository.deleteAll(); + notifikasjonService.opprettNyBeskjed( + notifikasjon, + NotifikasjonMerkelapp.getMerkelapp(avtale.getTiltakstype().getBeskrivelse()), + NotifikasjonTekst.TILTAK_AVTALE_OPPRETTET); + + assertThat(arbeidsgiverNotifikasjonRepository.findAllByAvtaleId(avtale.getId())).isNotEmpty(); + } + + @Test + public void opprettNyOppgaveTest() { + arbeidsgiverNotifikasjonRepository.deleteAll(); + notifikasjonService.opprettOppgave( + notifikasjon, + NotifikasjonMerkelapp.getMerkelapp(avtale.getTiltakstype().getBeskrivelse()), + NotifikasjonTekst.TILTAK_AVTALE_OPPRETTET); + + assertThat(arbeidsgiverNotifikasjonRepository. + findArbeidsgiverNotifikasjonByAvtaleIdAndVarselSendtVellykketAndNotifikasjonAktiv( + avtale.getId(), + true, + true)) + .isNotEmpty(); + } + + @Test + public void findArbeidsgiverNotifikasjonByIdAndHendelseTypeAndStatusResponseTest() { + arbeidsgiverNotifikasjonRepository.deleteAll(); + notifikasjonService.opprettOppgave( + notifikasjon, + NotifikasjonMerkelapp.getMerkelapp(avtale.getTiltakstype().getBeskrivelse()), + NotifikasjonTekst.TILTAK_AVTALE_OPPRETTET); + + List notifikasjonList = + arbeidsgiverNotifikasjonRepository. + findArbeidsgiverNotifikasjonByAvtaleIdAndHendelseTypeAndStatusResponse( + avtale.getId(), + this.notifikasjon.getHendelseType(), + MutationStatus.NY_OPPGAVE_VELLYKKET.getStatus()); + ArbeidsgiverNotifikasjon notifikasjon = notifikasjonList.get(0); + + assertThat(notifikasjon).isNotNull(); + assertThat(notifikasjon.getAvtaleId()).isEqualTo(avtale.getId()); + assertThat(notifikasjon.getHendelseType()).isEqualTo(HendelseType.OPPRETTET); + assertThat(notifikasjon.getStatusResponse()).isEqualTo(MutationStatus.NY_OPPGAVE_VELLYKKET.getStatus()); + } + + @Test + public void settOppgaveUtfoertTest() { + arbeidsgiverNotifikasjonRepository.deleteAll(); + notifikasjonService.opprettOppgave( + notifikasjon, + NotifikasjonMerkelapp.getMerkelapp(avtale.getTiltakstype().getBeskrivelse()), + NotifikasjonTekst.TILTAK_AVTALE_OPPRETTET); + + List notifikasjonList = + arbeidsgiverNotifikasjonRepository. + findArbeidsgiverNotifikasjonByAvtaleIdAndVarselSendtVellykketAndNotifikasjonAktiv( + avtale.getId(), + true, + true); + ArbeidsgiverNotifikasjon notifikasjon = notifikasjonList.get(0); + + assertThat(notifikasjonList.get(0)).isNotNull(); + assertThat(notifikasjon.getAvtaleId()).isEqualTo(avtale.getId()); + assertThat(notifikasjon.getStatusResponse()).isEqualTo(MutationStatus.NY_OPPGAVE_VELLYKKET.getStatus()); + assertThat(notifikasjon.isNotifikasjonAktiv()).isTrue(); + + notifikasjonService.oppgaveUtfoert( + avtale, + HendelseType.OPPRETTET, + MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + + List oppdatertNotifikasjonList = + arbeidsgiverNotifikasjonRepository.findAllByAvtaleId(avtale.getId()); + + assertThat(oppdatertNotifikasjonList.get(0)).isNotNull(); + assertThat(oppdatertNotifikasjonList.get(0).getAvtaleId()).isEqualTo(avtale.getId()); + assertThat(oppdatertNotifikasjonList.get(0).isNotifikasjonAktiv()).isFalse(); + assertThat(oppdatertNotifikasjonList.get(1)).isNotNull(); + assertThat(oppdatertNotifikasjonList.get(1).getAvtaleId()).isEqualTo(avtale.getId()); + assertThat(oppdatertNotifikasjonList.get(1).getStatusResponse()) + .isEqualTo(MutationStatus.OPPGAVE_UTFOERT_VELLYKKET.getStatus()); + } + + @Test + public void oppGaveUtfoertSkalKunlagresEnGangPrHendelse() { + arbeidsgiverNotifikasjonRepository.deleteAll(); + notifikasjonService.opprettOppgave( + notifikasjon, + NotifikasjonMerkelapp.getMerkelapp(avtale.getTiltakstype().getBeskrivelse()), + NotifikasjonTekst.TILTAK_AVTALE_OPPRETTET); + + notifikasjonService.oppgaveUtfoert( + avtale, + HendelseType.OPPRETTET, + MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + + notifikasjonService.oppgaveUtfoert( + avtale, + HendelseType.OPPRETTET, + MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + + List oppdatertNotifikasjonList = + arbeidsgiverNotifikasjonRepository.findAllByAvtaleId(avtale.getId()); + + assertThat(oppdatertNotifikasjonList.size()).isEqualTo(2); + } + + @Test + public void softDeleteNotifikasjonTest() { + arbeidsgiverNotifikasjonRepository.deleteAll(); + notifikasjonService.opprettOppgave( + notifikasjon, + NotifikasjonMerkelapp.getMerkelapp(avtale.getTiltakstype().getBeskrivelse()), + NotifikasjonTekst.TILTAK_AVTALE_OPPRETTET); + + List notifikasjonList = + arbeidsgiverNotifikasjonRepository.findAllByAvtaleId(avtale.getId()); + ArbeidsgiverNotifikasjon notifikasjon = notifikasjonList.get(0); + + assertThat(notifikasjonList.get(0)).isNotNull(); + assertThat(notifikasjon.getStatusResponse()).isEqualTo(MutationStatus.NY_OPPGAVE_VELLYKKET.getStatus()); + assertThat(notifikasjon.isNotifikasjonAktiv()).isTrue(); + + notifikasjonService.softDeleteNotifikasjoner(avtale); + + List oppdatertNotifikasjonList = + arbeidsgiverNotifikasjonRepository.findAllByAvtaleId(avtale.getId()); + + assertThat(oppdatertNotifikasjonList.get(0)).isNotNull(); + assertThat(oppdatertNotifikasjonList.get(0).getAvtaleId()).isEqualTo(avtale.getId()); + assertThat(oppdatertNotifikasjonList.get(0).isNotifikasjonAktiv()).isFalse(); + assertThat(oppdatertNotifikasjonList.get(1).getStatusResponse()) + .isEqualTo(MutationStatus.SOFT_DELETE_NOTIFIKASJON_VELLYKKET.getStatus()); + } + + @Test + public void softDeleteSkalIkkeOverskriveOppgaveUtfoertReferanseId() { + arbeidsgiverNotifikasjonRepository.deleteAll(); + + final ArbeidsgiverNotifikasjon not_avtaleOpprettet = + ArbeidsgiverNotifikasjon.nyHendelse(avtale, HendelseType.OPPRETTET, notifikasjonService, parser); + + final ArbeidsgiverNotifikasjon not_avtaleInngattBeskjed = + ArbeidsgiverNotifikasjon.nyHendelse(avtale, HendelseType.AVTALE_INNGÅTT, notifikasjonService, parser); + + notifikasjonService.opprettOppgave(not_avtaleOpprettet, + NotifikasjonMerkelapp.getMerkelapp(avtale.getTiltakstype().getBeskrivelse()), + NotifikasjonTekst.TILTAK_AVTALE_OPPRETTET); + + notifikasjonService.oppgaveUtfoert(avtale, + HendelseType.OPPRETTET, MutationStatus.NY_OPPGAVE_VELLYKKET, HendelseType.AVTALE_INNGÅTT); + + notifikasjonService.opprettNyBeskjed(not_avtaleInngattBeskjed, + NotifikasjonMerkelapp.getMerkelapp(avtale.getTiltakstype().getBeskrivelse()), + NotifikasjonTekst.TILTAK_AVTALE_INNGATT); + + final List allByAvtaleId = + arbeidsgiverNotifikasjonRepository.findAllByAvtaleId(avtale.getId()); + + assertThat(allByAvtaleId.size()).isEqualTo(3); + + notifikasjonService.softDeleteNotifikasjoner(avtale); + + final List allByAvtaleIdAfterSoftDelete = + arbeidsgiverNotifikasjonRepository.findAll(); + + assertThat(finnAntallNotifikasjonerMedGittMutasjonStatus(allByAvtaleIdAfterSoftDelete, MutationStatus.NY_OPPGAVE_VELLYKKET).size()). + isEqualTo(1); + assertThat(finnAntallNotifikasjonerMedGittMutasjonStatus(allByAvtaleIdAfterSoftDelete, MutationStatus.OPPGAVE_UTFOERT_VELLYKKET).size()). + isEqualTo(1); + assertThat(finnAntallNotifikasjonerMedGittMutasjonStatus(allByAvtaleIdAfterSoftDelete, MutationStatus.NY_BESKJED_VELLYKKET).size()). + isEqualTo(1); + assertThat(finnAntallNotifikasjonerMedGittMutasjonStatus(allByAvtaleIdAfterSoftDelete, MutationStatus.SOFT_DELETE_NOTIFIKASJON_VELLYKKET).size()). + isEqualTo(2); + } + +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/OppgaveVarselServiceTest.java b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/OppgaveVarselServiceTest.java new file mode 100644 index 000000000..660712926 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/java/no/nav/tag/tiltaksgjennomforing/varsel/oppgave/OppgaveVarselServiceTest.java @@ -0,0 +1,85 @@ +package no.nav.tag.tiltaksgjennomforing.varsel.oppgave; + +import no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype; +import no.nav.tag.tiltaksgjennomforing.exceptions.GosysFeilException; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.sts.STSClient; +import no.nav.tag.tiltaksgjennomforing.infrastruktur.sts.STSToken; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpEntity; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.UUID; + +import static no.nav.tag.tiltaksgjennomforing.avtale.Tiltakstype.VARIG_LONNSTILSKUDD; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class OppgaveVarselServiceTest { + + private URI uri = URI.create("test"); + private OppgaveVarselService.OppgaveResponse oppgaveResponse = new OppgaveVarselService.OppgaveResponse(); + private OppgaveProperties oppgaveProperties = new OppgaveProperties(); + + @Captor + private ArgumentCaptor> requestCaptor; + + @Mock + private STSClient stsClient; + + @Mock + private RestTemplate restTemplate; + + private OppgaveVarselService oppgaveVarselService; + + @BeforeEach + public void setUp() { + oppgaveResponse.setId("oppgaveId"); + oppgaveProperties.setOppgaveUri(uri); + oppgaveVarselService = new OppgaveVarselService(oppgaveProperties, restTemplate, stsClient); + } + + @ParameterizedTest + @EnumSource(Tiltakstype.class) + public void oppretterOppgaveRequestForTiltak(Tiltakstype tiltakstype){ + when(stsClient.hentSTSToken()).thenReturn(new STSToken()); + when(restTemplate.postForObject(any(URI.class), any(), any(Class.class))).thenReturn(oppgaveResponse); + + oppgaveVarselService.opprettOppgave("aktørId", tiltakstype, UUID.randomUUID()); + verify(restTemplate).postForObject(eq(uri), requestCaptor.capture(), eq(OppgaveVarselService.OppgaveResponse.class)); + OppgaveRequest request = requestCaptor.getValue().getBody(); + + assertThat(request.getAktivDato()).isToday(); + assertThat(request.getAktoerId()).isEqualTo("aktørId"); + assertThat(request.getBehandlingstema()).isEqualTo(tiltakstype.getBehandlingstema()); + assertThat(request.getBeskrivelse()).contains(tiltakstype.getBeskrivelse()); + assertThat(request.getBeskrivelse()).contains("Avtale er opprettet av arbeidsgiver på tiltak "); + assertThat(request.getBehandlingstype()).isEqualTo("ae0034"); + assertThat(request.getOppgavetype()).isEqualTo("VURD_HENV"); + assertThat(request.getPrioritet()).isEqualTo("NORM"); + assertThat(request.getTema()).isEqualTo("TIL"); + } + + @Test + public void oppretterOppgaveRequestFeiler() { + when(stsClient.hentSTSToken()).thenReturn(new STSToken()); + when(restTemplate.postForObject(any(URI.class), any(), any(Class.class))).thenThrow(RuntimeException.class); + + assertThrows(GosysFeilException.class, () -> { + oppgaveVarselService.opprettOppgave("aktørId", VARIG_LONNSTILSKUDD, UUID.randomUUID()); + }); + } +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/admin-kall.http b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/admin-kall.http new file mode 100644 index 000000000..340e6d4e9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/admin-kall.http @@ -0,0 +1,30 @@ +### Annuller en tilskuddsperiode +# Husk å bytte ut UUID med tilskuddsperiodens id, og SESSION_ID med sesjons-id'en du får fra cookies i frontend +POST https://tiltaksgjennomforing.intern.nav.no/tiltaksgjennomforing/api/utvikler-admin/annuller-tilskuddsperiode/ +Cookie: io.nais.wonderwall.session= + +### Lag tilskuddsperioder for en enkelt avtale +# Husk å bytte ut UUID med avtalens id, og SESSION_ID med sesjons-id'en du får fra cookies i frontend +POST https://tiltaksgjennomforing.intern.nav.no/tiltaksgjennomforing/api/utvikler-admin/lag-tilskuddsperioder-for-en-avtale//2023-02-01 +Cookie: io.nais.wonderwall.session= + +### Reberegn ubehandlet perioder +POST https://tiltaksgjennomforing.intern.nav.no/tiltaksgjennomforing/api/utvikler-admin/reberegn-ubehandlede-tilskuddsperioder/ +Cookie: io.nais.wonderwall.session= + + +### Annuller perioder med sluttdato før oppgitt dato på avtale og lag nye med status behandlet i arena +POST https://tiltaksgjennomforing.intern.nav.no/tiltaksgjennomforing/api/utvikler-admin/annuller-og-generer-behandlet-i-arena-perioder/58f5f3ff-fd7b-469b-9e7b-6c43b2888fba/2023-02-01 +Cookie: io.nais.wonderwall.session= + + +### Patch Dvh meldinger +POST https://tiltaksgjennomforing.intern.nav.no/tiltaksgjennomforing/api/utvikler-admin/dvh-melding/patch +Content-Type: application/json +Cookie: io.nais.wonderwall.session= + +{ + "avtaleIder": [ + "AVTALE-ID-HER" + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/config/application-dockercompose.yml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/config/application-dockercompose.yml new file mode 100644 index 000000000..c9ea00d50 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/config/application-dockercompose.yml @@ -0,0 +1,95 @@ +spring: + datasource: + url: "jdbc:postgresql://localhost:6432/sample?user=sample&password=sample" + kafka: + properties: + security.protocol: PLAINTEXT + ssl: + keystore: null + truststore: null + bootstrap-servers: localhost:9092 + +tiltaksgjennomforing: + kontoregister: + uri: http://localhost:8090/kontoregister/api/v1/hent-kontonr-org + consumerId: tiltak-refusjon-api + realClient: true + beslutter-ad-gruppe: + id: 1a1d2745-952f-4a0f-839f-9530145b1d4a + kafka: + enabled: true + + altinn-tilgangsstyring: + uri: http://localhost:8090/altinn-tilgangsstyring + proxyUri: http://heh:9090/ + beOmRettighetBaseUrl: https://arbeidsgiver-q.nav.no/min-side-arbeidsgiver/?fragment=be-om-tilgang + ltsMidlertidigServiceCode: 5516 + ltsMidlertidigServiceEdition: 1 + ltsVarigServiceCode: 5516 + ltsVarigServiceEdition: 2 + sommerjobbServiceCode: 5516 + sommerjobbServiceEdition: 3 + arbtreningServiceCode: 5332 + arbtreningServiceEdition: 1 + altinnApiKey: foo + apiGwApiKey: foo + ereg: + uri: http://localhost:8090/ereg + sts: + username: foo + password: bar + rest-uri: http://localhost:8090/sts/sts/token + axsys: + uri: http://localhost:8090/axsys + abac: + uri: http://localhost:8090/abac + nav-consumer-id: tiltaksgjennomforing-api + consumer: + system: + id: testsystem + persondata: + uri: http://localhost:8090/persondata + oppgave: + oppgave-uri: http://localhost:8090/api/v1/oppgaver + unleash: + mock: true + veilarbarena: + url: http://localhost:8090/veilarbarena/api/arena/status + nav-consumer-id: tiltaksgjennomforing-api + norg2: + geografisk: + url: http://localhost:8090/norg2/api/v1/enhet/navkontor/ + enhet: + url: http://localhost:8090/norg2/api/v1/enhet/ + dokgen: + uri: http://localhost:9000/template/tiltak-avtale/create-pdf + dvh-melding: + gruppe-tilgang: 1a1d2745-952f-4a0f-839f-9530145b1d4a + fixed-delay: 20000 + avtale-hendelse-melding: + fixed-delay: 120 + tilskuddsperioder: + tiltakstyper: SOMMERJOBB, MIDLERTIDIG_LONNSTILSKUDD, VARIG_LONNSTILSKUDD + +no.nav.gcp.kafka.aiven: + bootstrap-servers: localhost:9092 + truststore-path: null + truststore-password: null + keystore-path: null + keystore-password: null + schema-registry-url: http://localhost:1337 + schema-registry-credentials-source: null + schema-registry-user-info: null + security-protocol: PLAINTEXT + +no.nav.security.jwt: + issuer: + aad: + discoveryurl: https://tiltak-fakelogin.ekstern.dev.nav.no/metadata?issuer=aad + accepted_audience: fake-aad + tokenx: + discoveryurl: https://tiltak-fakelogin.ekstern.dev.nav.no/metadata?issuer=tokenx + accepted_audience: fake-tokenx + client: null + +ELECTOR_PATH: null \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/config/application-local.yaml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/config/application-local.yaml new file mode 100644 index 000000000..9e40c91d8 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/config/application-local.yaml @@ -0,0 +1,118 @@ +spring: + datasource: + url: jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;MODE=PostgreSQL + username: sa + password: sa + driverClassName: org.h2.Driver + jpa: + properties: + hibernate: + show_sql: false + use_sql_comments: false + format_sql: false + h2: + console: + enabled: true + path: /h2-console + main: + banner-mode: "console" + kafka: + bootstrap-servers: ${spring.embedded.kafka.brokers} + sql: + init: + platform: postgres +no.nav.gcp.kafka.aiven: + bootstrap-servers: ${spring.embedded.kafka.brokers} + truststore-path: null + truststore-password: null + keystore-path: null + keystore-password: null + schema-registry-url: null + schema-registry-credentials-source: null + schema-registry-user-info: null + security-protocol: PLAINTEXT + +no.nav.security.jwt: + issuer: + aad: + discoveryurl: https://tiltak-fakelogin.ekstern.dev.nav.no/metadata?issuer=aad + accepted_audience: fake-aad + tokenx: + discoveryurl: https://tiltak-fakelogin.ekstern.dev.nav.no/metadata?issuer=tokenx + accepted_audience: fake-tokenx + client: null + +tiltaksgjennomforing: + kontoregister: + uri: http://localhost:8090/kontoregister/api/v1/hent-kontonr-org + consumerId: tiltak-refusjon-api + realClient: true + beslutter-ad-gruppe: + id: 1a1d2745-952f-4a0f-839f-9530145b1d4a + kafka: + enabled: false + fake: true + fake-url: http://localhost:8081/fake-kafka + altinn-tilgangsstyring: + uri: http://localhost:8090/altinn-tilgangsstyring + proxyUri: http://heh:9090/ + beOmRettighetBaseUrl: https://min-side-arbeidsgiver.dev.nav.no/min-side-arbeidsgiver/?fragment=be-om-tilgang + ltsMidlertidigServiceCode: 5516 + ltsMidlertidigServiceEdition: 1 + ltsVarigServiceCode: 5516 + ltsVarigServiceEdition: 2 + sommerjobbServiceCode: 5516 + sommerjobbServiceEdition: 3 + mentorServiceCode: 5516 + mentorServiceEdition: 4 + inkluderingstilskuddServiceCode: 5516 + inkluderingstilskuddServiceEdition: 5 + arbtreningServiceCode: 5332 + arbtreningServiceEdition: 1 + altinnApiKey: foo + apiGwApiKey: foo + ereg: + uri: http://localhost:8090/ereg + sts: + username: foo + password: bar + rest-uri: http://localhost:8090/sts/sts/token + axsys: + uri: http://localhost:8090/axsys + abac: + uri: http://localhost:8090/abac + nav-consumer-id: tiltaksgjennomforing-api + consumer: + system: + id: testsystem + persondata: + uri: http://localhost:8090/persondata + oppgave: + oppgave-uri: http://localhost:8090/api/v1/oppgaver + unleash: + mock: true + veilarbarena: + url: http://localhost:8090/veilarbarena/api/arena/status + nav-consumer-id: tiltaksgjennomforing-api + norg2: + geografisk: + url: http://localhost:8090/norg2/api/v1/enhet/navkontor/ + enhet: + url: http://localhost:8090/norg2/api/v1/enhet/ + dokgen: + uri: http://localhost:5913/template/tiltak-avtale/create-pdf + notifikasjoner: + uri: http://localhost:8090/api/graphql + lenke: http://localhost:3000/tiltaksgjennomforing/avtale/ + enabled: false + dvh-melding: + gruppe-tilgang: 1a1d2745-952f-4a0f-839f-9530145b1d4a + fixed-delay: 20000 + tilskuddsperioder: + tiltakstyper: SOMMERJOBB, MIDLERTIDIG_LONNSTILSKUDD, VARIG_LONNSTILSKUDD + salesforcekontorer: + enheter: 0906 + utvikler-tilgang: + gruppe-tilgang: 1a1d2745-952f-4a0f-839f-9530145b1d4a + +ELECTOR_PATH: null diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/issuers.properties b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/issuers.properties new file mode 100644 index 000000000..54fa99315 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/issuers.properties @@ -0,0 +1,9 @@ +no.nav.security.oidc.issuer.number1.discoveryurl=http://metadata +no.nav.security.oidc.issuer.number1.accepted_audience=aud1 +no.nav.security.oidc.issuer.number1.cookie_name=idtoken +no.nav.security.oidc.issuer.number2.discoveryurl=http://metadata2 +no.nav.security.oidc.issuer.number2.accepted_audience=aud2 +no.nav.security.oidc.issuer.number3.discoveryurl=http://metadata3 +no.nav.security.oidc.issuer.number3.accepted_audience=aud3,aud4 +#no.nav.security.oidc.issuer.number2.cookie_name= + \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/jwkset.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/jwkset.json new file mode 100644 index 000000000..fe4a65951 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/jwkset.json @@ -0,0 +1,13 @@ +{ + "keys": [ + { + "kty": "RSA", + "d": "MRf73iiXUEhJFxDTtJ5rEHNQsAG8XFuXkz9vXXbMp1_OTo11bEx3SnHiwmO_mSAAeXWNJniLw07V1-nk551h5in_ueAPwXTOf8qddacvDEBZwcxeqfu_Kjh1R0ji8Xn1a037CpH2IO34Lyw2gmsGFdMZgDwa5Z0KJjPCU6W8tF6CA-2omAdNzrFaWtaPFpBC0NzYaaB111bKIXxngG97Cnu81deEEKmX-vL-O4tpvUUybuquxrlFvVlTeYlrQqv50_IKsKSYkg-iu1cbqIiWrRq9eTmA6EppmZbqHjKSM5JYFbPB_oZ9QeHKnp1_MTom-jKMEpw18qq-PzdX_skZWQ", + "e": "AQAB", + "use": "sig", + "kid": "localhost-signer", + "alg": "RS256", + "n": "lFTMP9TSUwLua0G8M7foqmdUS2us1-JOF8H_tClVG3IEQMRvMmHJoGSdldWDHsNwRG3Wevl_8fZoGocw9hPqj93j-vI4-ZkbxwhPyRqlS0FNIPD1Ln5R6AmHu7b-paRIz3lvqpyTRwnGBI9weE4u6WOpOQ8DjJMNPq4WcM42AgDJAvc6UuhcWW_MLIsjkKp_VYKxzthSuiRAxXi8Pz4ZhiTAEZI-UN61DYU9YEFNujg5XtIQsRwQn1Vj7BknGwkdf_iCGJgDlKUOz9hAojOMXTAwetUx6I5nngIM5vaXWJCmKn6SzcTYgHWWVrn8qaSazioaydLaYN9NuQ0MdIvsQw" + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/jwtkeystore.jks b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/jwtkeystore.jks new file mode 100644 index 000000000..ce90a81f7 Binary files /dev/null and b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/jwtkeystore.jks differ diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/logback-spring.xml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/logback-spring.xml new file mode 100644 index 000000000..451990c10 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/logback-spring.xml @@ -0,0 +1,13 @@ + + + + + ${CONSOLE_LOG_PATTERN} + utf8 + + + + + + + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/abac.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/abac.json new file mode 100644 index 000000000..e687feacc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/abac.json @@ -0,0 +1,73 @@ +{ + "mappings": [ + { + "priority": 1, + "request": { + "method": "POST", + "urlPattern": "/abac", + "bodyPatterns": [ + { + "matchesJsonPath": "$[?(@.Request.Resource[0].Attribute[2].Value == \"01118023456\")]", + "ignoreArrayOrder": true, + "ignoreExtraElements": true + }, + { + "matchesJsonPath": "$[?(@.Request.AccessSubject.Attribute[0].Value == \"F142226\")]", + "ignoreArrayOrder": true, + "ignoreExtraElements": true + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "Response": { + "Decision": "Deny" + } + } + } + },{ + "priority": 2, + "request": { + "method": "POST", + "urlPattern": "/abac", + "bodyPatterns": [ + { + "matchesJsonPath": "$[?(@.Request.Resource[0].Attribute[2].Value == \"11111111111\")]", + "ignoreArrayOrder": true, + "ignoreExtraElements": true + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + } + } + }, + { + "priority": 3, + "request": { + "method": "POST", + "urlPattern": "/abac" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "Response": { + "Decision": "Permit" + } + } + } + } + ] +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_10.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_10.json new file mode 100644 index 000000000..740d5b3db --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_10.json @@ -0,0 +1,272 @@ +{ + "mappings": [ + { + "priority": 1, + "request": { + "method": "GET", + "urlPath": "/altinn-tilgangsstyring/ekstern/altinn/api/serviceowner/reportees", + "queryParameters": { + "subject": { + "matches": "^1\\d{10}$" + }, + "serviceCode": { + "absent": true + }, + "serviceEdition": { + "absent": true + } + } + }, + "response": { + "status": 200, + "jsonBody": [ + { + "Name": "Bedriften Med Sommerjobb", + "Type": "Business", + "OrganizationNumber": "910712307", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825555" + }, + { + "Name": "Saltrød og Høneby", + "Type": "Business", + "OrganizationNumber": "999999999", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825550" + }, + { + "Name": "SALTRØD FORELDER", + "Type": "Enterprise", + "OrganizationNumber": "910825550", + "OrganizationForm": "AS", + "Status": "Active" + }, + { + "Name": "BIRTAVARRE OG VÆRLANDET", + "Type": "Enterprise", + "OrganizationNumber": "910825555", + "OrganizationForm": "AS", + "Status": "Active" + }, + { + "Name": "Bedriften Med Midlertidi Lts", + "Type": "Business", + "OrganizationNumber": "910712314", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825555" + }, + { + "Name": "Bedriften Med Varig Lts", + "Type": "Business", + "OrganizationNumber": "910712306", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825555" + }, + { + "Name": "IngenTiltak Hjørnet", + "Type": "Business", + "OrganizationNumber": "980712306", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825555" + } + ], + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 1, + "request": { + "method": "GET", + "urlPath": "/altinn-tilgangsstyring/ekstern/altinn/api/serviceowner/reportees", + "queryParameters": { + "subject": { + "matches": "^1\\d{10}$" + }, + "serviceCode": { + "equalTo": "5332" + }, + "serviceEdition": { + "equalTo": "1" + } + } + }, + "response": { + "status": 200, + "jsonBody": [ + { + "Name": "Saltrød og Høneby", + "Type": "Business", + "OrganizationNumber": "999999999", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825555" + }, + { + "Name": "BIRTAVARRE OG VÆRLANDET FORELDER", + "Type": "Enterprise", + "OrganizationNumber": "910825550", + "OrganizationForm": "AS", + "Status": "Active" + } + ], + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 1, + "request": { + "method": "GET", + "urlPath": "/altinn-tilgangsstyring/ekstern/altinn/api/serviceowner/reportees", + "queryParameters": { + "subject": { + "matches": "^1\\d{10}$" + }, + "serviceCode": { + "equalTo": "5516" + }, + "serviceEdition": { + "equalTo": "1" + } + } + }, + "response": { + "status": 200, + "jsonBody": [ + { + "Name": "Saltrød og Høneby", + "Type": "Business", + "OrganizationNumber": "999999999", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825555" + }, + { + "Name": "Bedriften Med Midlertidi Lts", + "Type": "Business", + "OrganizationNumber": "910712314", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825550" + }, + { + "Name": "BIRTAVARRE OG VÆRLANDET FORELDER", + "Type": "Enterprise", + "OrganizationNumber": "910825550", + "OrganizationForm": "AS", + "Status": "Active" + } + ], + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 1, + "request": { + "method": "GET", + "urlPath": "/altinn-tilgangsstyring/ekstern/altinn/api/serviceowner/reportees", + "queryParameters": { + "subject": { + "matches": "^1\\d{10}$" + }, + "serviceCode": { + "equalTo": "5516" + }, + "serviceEdition": { + "equalTo": "2" + } + } + }, + "response": { + "status": 200, + "jsonBody": [ + { + "Name": "Bedriften Med Varig Lts", + "Type": "Business", + "OrganizationNumber": "910712306", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825550" + }, + { + "Name": "BIRTAVARRE OG VÆRLANDET FORELDER", + "Type": "Enterprise", + "OrganizationNumber": "910825550", + "OrganizationForm": "AS", + "Status": "Active" + }, + { + "Name": "Saltrød og Høneby", + "Type": "Business", + "OrganizationNumber": "999999999", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825555" + } + ], + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 1, + "request": { + "method": "GET", + "urlPath": "/altinn-tilgangsstyring/ekstern/altinn/api/serviceowner/reportees", + "queryParameters": { + "subject": { + "matches": "^1\\d{10}$" + }, + "serviceCode": { + "equalTo": "5516" + }, + "serviceEdition": { + "matches": "3|4|5" + } + } + }, + "response": { + "status": 200, + "jsonBody": [ + { + "Name": "Bedriften Med Sommerjobb", + "Type": "Business", + "OrganizationNumber": "910712307", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825550" + }, + { + "Name": "BIRTAVARRE OG VÆRLANDET FORELDER", + "Type": "Enterprise", + "OrganizationNumber": "910825550", + "OrganizationForm": "AS", + "Status": "Active" + }, + { + "Name": "Saltrød og Høneby", + "Type": "Business", + "OrganizationNumber": "999999999", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825555" + } + ], + "headers": { + "Content-Type": "application/json" + } + } + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_20.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_20.json new file mode 100644 index 000000000..780587e4c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_20.json @@ -0,0 +1,86 @@ +{ + "mappings": [ + { + "priority": 1, + "request": { + "method": "GET", + "urlPath": "/altinn-tilgangsstyring/ekstern/altinn/api/serviceowner/reportees", + "queryParameters": { + "subject": { + "matches": "^2\\d{10}$" + }, + "serviceCode": { + "absent": true + }, + "serviceEdition": { + "absent": true + } + } + }, + "response": { + "status": 200, + "jsonBody": [ + { + "Name": "Saltrød og Høneby", + "Type": "Business", + "OrganizationNumber": "999999999", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825555" + }, + { + "Name": "BIRTAVARRE OG VÆRLANDET FORELDER", + "Type": "Enterprise", + "OrganizationNumber": "910825555", + "OrganizationForm": "AS", + "Status": "Active" + } + ], + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 1, + "request": { + "method": "GET", + "urlPath": "/altinn-tilgangsstyring/ekstern/altinn/api/serviceowner/reportees", + "queryParameters": { + "subject": { + "matches": "^2\\d{10}$" + }, + "serviceCode": { + "matches": "5332|5516" + }, + "serviceEdition": { + "matches": "1|2|3|4|5" + } + } + }, + "response": { + "status": 200, + "jsonBody": [ + { + "Name": "Saltrød og Høneby", + "Type": "Business", + "OrganizationNumber": "999999999", + "OrganizationForm": "BEDR", + "Status": "Active", + "ParentOrganizationNumber": "910825555" + }, + { + "Name": "BIRTAVARRE OG VÆRLANDET FORELDER", + "Type": "Enterprise", + "OrganizationNumber": "910825550", + "OrganizationForm": "AS", + "Status": "Active" + } + ], + "headers": { + "Content-Type": "application/json" + } + } + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_31.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_31.json new file mode 100644 index 000000000..2c47adf4e --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_31.json @@ -0,0 +1,29 @@ +{ + "mappings": [ + { + "priority": 1, + "request": { + "method": "GET", + "urlPath": "/altinn-tilgangsstyring/ekstern/altinn/api/serviceowner/reportees", + "queryParameters": { + "subject": { + "matches": "^31\\d{9}$" + }, + "serviceCode": { + "matches": "5332|^$" + }, + "serviceEdition": { + "matches": "1|^$" + } + } + }, + "response": { + "status": 500, + "headers": { + "Content-Type": "application/json" + }, + "statusMessage": "Internal Server Error: 500 UserID" + } + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_ikke_treff.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_ikke_treff.json new file mode 100644 index 000000000..b9068558d --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_ikke_treff.json @@ -0,0 +1,72 @@ +{ + "mappings": [ + { + "priority": 2, + "request": { + "method": "GET", + "urlPath": "/altinn-tilgangsstyring/ekstern/altinn/api/serviceowner/reportees", + "queryParameters" : { + "subject" : { + "matches" : "^\\d{11}$" + }, + "serviceCode": { + "matches": "5332|5516|^$" + }, + "serviceEdition": { + "matches": "1|2|3|4|5|^$" + } + } + }, + "response": { + "status": 200, + "body": "[]", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 2, + "request": { + "method": "GET", + "urlPath": "/altinn-tilgangsstyring/ekstern/altinn/api/serviceowner/reportees", + "queryParameters" : { + "subject" : { + "matches" : "^\\d{11}$" + }, + "serviceCode": { + "absent": true + }, + "serviceEdition": { + "absent": true + } + } + }, + "response": { + "status": 200, + "body": "[]", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "GET", + "urlPath": "/altinn-tilgangsstyring/ekstern/altinn/api/serviceowner/reportees", + "queryParameters" : { + "subject" : { + "doesNotMatch" : "^\\d{11}$" + } + } + }, + "response": { + "status": 400, + "headers": { + "Content-Type": "application/json" + }, + "statusMessage": "Invalid social security number" + } + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_varsel.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_varsel.json new file mode 100644 index 000000000..dca3446c3 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/altinn_varsel.json @@ -0,0 +1,17 @@ +{ + "mappings": [ + { + "request": { + "method": "POST", + "urlPath": "/altinn-varsel" + }, + "response": { + "status": 200, + "body": "", + "headers": { + "Content-Type": "application/soap+xml" + } + } + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/axsys.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/axsys.json new file mode 100644 index 000000000..0e7cb56c1 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/axsys.json @@ -0,0 +1,42 @@ +{ + "mappings": [ + { + "priority": 1, + "request": { + "method": "GET", + "urlPathPattern": "/axsys/[X,Z]123456", + "queryParameters": { + "inkluderAlleEnheter": { + "equalTo": "false" + } + } + }, + "response": { + "status": 200, + "body": "{\"enheter\":[{\"enhetId\":\"0906\",\"fagomrader\":[\"ABC\",\"DEF\",\"GHI\",\"JKL\",\"MNO\",\"PQR\",\"STU\",\"WXY\"],\"navn\":\"NAV Storebyen\"},{\"enhetId\":\"0904\",\"fagomrader\":[\"ABC\",\"DEF\",\"GHI\",\"JKL\",\"MNO\",\"PQR\",\"STU\",\"WXY\"],\"navn\":\"NAV Lillebyen\"}]}", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 2, + "request": { + "method": "GET", + "urlPathPattern": "/axsys/[A-Z]{1}[0-9]{6}", + "queryParameters": { + "inkluderAlleEnheter": { + "equalTo": "false" + } + } + }, + "response": { + "status": 200, + "body": "{\"enheter\": []}", + "headers": { + "Content-Type": "application/json" + } + } + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/ereg.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/ereg.json new file mode 100644 index 000000000..ad18964d9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/ereg.json @@ -0,0 +1,255 @@ +{ + "mappings": [ + { + "request": { + "method": "GET", + "urlPattern": "/ereg/9[0-9]{8}" + }, + "response": { + "status": 200, + "body": "{\"organisasjonsnummer\":\"999999999\",\"type\":\"Virksomhet\",\"navn\":{\"sammensattnavn\":\"Saltrød og Høneby\",\"navnelinje1\":\"Saltrød og\",\"navnelinje4\":\"Høneby\",\"bruksperiode\":{\"fom\":\"2019-01-14T11:39:39.512\"},\"gyldighetsperiode\":{\"fom\":\"2018-10-28\"}},\"organisasjonDetaljer\":{\"registreringsdato\":\"2018-10-28T00:00:00\",\"enhetstyper\":[{\"enhetstype\":\"BEDR\",\"bruksperiode\":{\"fom\":\"2019-01-14T11:39:39.511\"},\"gyldighetsperiode\":{\"fom\":\"2019-01-14\"}}],\"navn\":[{\"navnelinje1\":\"Saltrød og Høneby\",\"bruksperiode\":{\"fom\":\"2019-01-14T11:39:39.512\"},\"gyldighetsperiode\":{\"fom\":\"2018-10-28\"}}],\"forretningsadresser\":[{\"adresselinje1\":\"Nedre Tyholms vei13, 4836 Arendal\",\"adresselinje3\":\"Nedre Tyholms vei 13, 4836 Arendal\",\"postnummer\":\"4836\",\"landkode\":\"NO\",\"kommunenummer\":\"0906\",\"bruksperiode\":{\"fom\":\"2019-01-14T11:39:39.512\"},\"gyldighetsperiode\":{\"fom\":\"2018-10-28\"}}],\"postadresser\":[{\"adresselinje1\":\"Postboks 502 Lund,4605 KRISTIANSAN\",\"adresselinje2\":\"DS\",\"adresselinje3\":\"Postboks 502 Lund,4605 KRISTIANSA\",\"postnummer\":\"4605\",\"landkode\":\"NO\",\"kommunenummer\":\"1001\",\"bruksperiode\":{\"fom\":\"2019-01-14T11:39:39.512\"},\"gyldighetsperiode\":{\"fom\":\"2018-10-28\"}}],\"sistEndret\":\"2018-10-28\"},\"virksomhetDetaljer\":{\"enhetstype\":\"BEDR\"}}", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "GET", + "urlPattern": "/ereg/8[0-9]{8}" + }, + "response": { + "status": 200, + "jsonBody": { + "organisasjonsnummer": "885807542", + "type": "JuridiskEnhet", + "navn": { + "sammensattnavn": "ADDPERSONA AS", + "navnelinje1": "ADDPERSONA AS", + "bruksperiode": { + "fom": "2015-02-23T08:04:53.2" + }, + "gyldighetsperiode": { + "fom": "2003-06-28" + } + }, + "organisasjonDetaljer": { + "registreringsdato": "2003-06-28T00:00:00", + "stiftelsesdato": "2003-06-16", + "enhetstyper": [ + { + "enhetstype": "AS", + "bruksperiode": { + "fom": "2014-05-21T18:49:43.852" + }, + "gyldighetsperiode": { + "fom": "2003-06-28" + } + } + ], + "navn": [ + { + "sammensattnavn": "ADDPERSONA AS", + "navnelinje1": "ADDPERSONA AS", + "bruksperiode": { + "fom": "2015-02-23T08:04:53.2" + }, + "gyldighetsperiode": { + "fom": "2003-06-28" + } + } + ], + "naeringer": [ + { + "naeringskode": "78.200", + "hjelpeenhet": false, + "bruksperiode": { + "fom": "2014-05-22T00:42:12.169" + }, + "gyldighetsperiode": { + "fom": "2003-07-01" + } + } + ], + "forretningsadresser": [ + { + "type": "Forretningsadresse", + "adresselinje1": "Karl Johans gate 8", + "postnummer": "0154", + "landkode": "NO", + "kommunenummer": "0301", + "bruksperiode": { + "fom": "2018-04-10T04:00:47.12" + }, + "gyldighetsperiode": { + "fom": "2018-04-09" + } + } + ], + "epostadresser": [ + { + "adresse": "post@addpersona.no", + "bruksperiode": { + "fom": "2014-05-21T18:49:43.85" + }, + "gyldighetsperiode": { + "fom": "2014-03-18" + } + } + ], + "internettadresser": [ + { + "adresse": "www.addpersona.no", + "bruksperiode": { + "fom": "2014-05-21T18:49:43.85" + }, + "gyldighetsperiode": { + "fom": "2014-03-18" + } + } + ], + "telefonnummer": [ + { + "nummer": "22 41 02 07", + "telefontype": "TFON", + "bruksperiode": { + "fom": "2014-05-21T18:49:43.852" + }, + "gyldighetsperiode": { + "fom": "2014-03-18" + } + } + ], + "mobiltelefonnummer": [ + { + "nummer": "415 77 755", + "telefontype": "MTLF", + "bruksperiode": { + "fom": "2014-05-21T18:49:43.851" + }, + "gyldighetsperiode": { + "fom": "2014-03-18" + } + } + ], + "formaal": [ + { + "formaal": "Hele eller deler av rekrutteringsprosessen, utleie av personell til\nmidlertidige og faste stillinger, HR-rådgivning, karriereveiledning,\nkonsulentvirksomhet og hva som herved står i forbindelse.", + "bruksperiode": { + "fom": "2021-12-09T04:00:55.958" + }, + "gyldighetsperiode": { + "fom": "2021-12-08" + } + } + ], + "registrertMVA": [ + { + "registrertIMVA": true, + "bruksperiode": { + "fom": "2014-05-21T18:49:43.852" + }, + "gyldighetsperiode": { + "fom": "2014-03-18" + } + } + ], + "ansatte": [ + { + "antall": 26, + "bruksperiode": { + "fom": "2022-12-16T00:59:52.877" + }, + "gyldighetsperiode": { + "fom": "2022-12-14" + } + } + ], + "navSpesifikkInformasjon": { + "erIA": false, + "bruksperiode": { + "fom": "1900-01-01T00:00:00" + }, + "gyldighetsperiode": { + "fom": "1900-01-01" + } + }, + "maalform": "NB", + "sistEndret": "2022-12-14" + }, + "juridiskEnhetDetaljer": { + "enhetstype": "AS", + "sektorkode": "2100", + "kapitalopplysninger": [ + { + "kapital": 100000, + "kapitalInnbetalt": 100000, + "valuta": "NOK", + "fritekst": "null", + "bruksperiode": { + "fom": "2014-05-21T18:11:43.927" + }, + "gyldighetsperiode": { + "fom": "2014-03-18" + } + } + ], + "foretaksregisterRegistreringer": [ + { + "registrert": true, + "bruksperiode": { + "fom": "2014-05-21T18:49:43.851" + }, + "gyldighetsperiode": { + "fom": "2014-03-18" + } + } + ] + }, + "fusjoner": [ + { + "juridiskEnhet": { + "organisasjonsnummer": "989023438", + "type": "JuridiskEnhet", + "navn": { + "sammensattnavn": "RABBE & HUSBY HOLDING AS", + "navnelinje1": "RABBE & HUSBY HOLDING AS", + "bruksperiode": { + "fom": "2015-02-23T08:04:53.2" + }, + "gyldighetsperiode": { + "fom": "2006-04-11" + } + }, + "organisasjonDetaljer": { + "navn": [ + { + "sammensattnavn": "RABBE & HUSBY HOLDING AS", + "navnelinje1": "RABBE & HUSBY HOLDING AS", + "bruksperiode": { + "fom": "2015-02-23T08:04:53.2" + }, + "gyldighetsperiode": { + "fom": "2006-04-11" + } + } + ] + } + }, + "virkningsdato": "2017-12-11", + "bruksperiode": { + "fom": "2017-12-12T04:01:20.917" + }, + "gyldighetsperiode": { + "fom": "2017-07-01" + } + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/kontoregister.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/kontoregister.json new file mode 100644 index 000000000..3ff474a05 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/kontoregister.json @@ -0,0 +1,76 @@ +{ + "mappings": [ + { + "priority": 1, + "request": { + "method": "GET", + "urlPath": "/kontoregister/api/v1/hent-kontonr-org/990983666" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json", + "Accept":"application/json" + }, + "jsonBody": + { + "mottaker": "889640782", + "kontonr": "10000008162" + } + } + },{ + "priority": 1, + "request": { + "method": "GET", + "urlPath": "/kontoregister/api/v1/hent-kontonr-org/999999999" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json", + "Accept":"application/json" + }, + "jsonBody": + { + "mottaker": "889640782", + "kontonr": "10000008162" + } + } + }, + { + "priority": 1, + "request": { + "method": "GET", + "urlPath": "/kontoregister/api/v1/hent-kontonr-org/111234567" + }, + "response": { + "status": 404, + "headers": { + "Content-Type": "application/json", + "Accept":"application/json" + }, + "jsonBody": + { + "feilmelding": "Organisasjonsnummer ikke funnet i kontoregister" + } + } + },{ + "priority": 1, + "request": { + "method": "GET", + "urlPath": "/kontoregister/api/v1/hent-kontonr-org/777333333" + }, + "response": { + "status": 500, + "headers": { + "Content-Type": "application/json", + "Accept":"application/json" + }, + "jsonBody": + { + "feilmelding": "Organisasjonsnummer ikke funnet i kontoregister" + } + } + } + ] +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/leaderelector.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/leaderelector.json new file mode 100644 index 000000000..7f2ba161c --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/leaderelector.json @@ -0,0 +1,17 @@ +{ + "mappings": [ + { + "request": { + "method": "GET", + "urlPathPattern": "/leaderelector" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "text/plain;charset=utf-8" + }, + "body": "{\"name\":\"tiltaksgjennomforing-api\"}" + } + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/minsideArbeidsgiverNotifikasjon.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/minsideArbeidsgiverNotifikasjon.json new file mode 100644 index 000000000..1e0115503 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/minsideArbeidsgiverNotifikasjon.json @@ -0,0 +1,108 @@ +{ + "mappings": [ + { + "request": { + "method": "POST", + "urlPath": "/api/graphql", + "headers": { + "Content-Type": { + "equalTo": "application/json" + } + }, + "bodyPatterns": [ + { + "matchesJsonPath": { + "expression": "$..query", + "contains": "mutation OpprettNyBeskjed" + } + } + ] + }, + "response": { + "status": 200, + "body": "{ \"data\": { \"nyBeskjed\": { \"__typename\": \"NyBeskjedVellykket\", \"id\": \"af2322fc-2977-4b1f-8dab-616c7e6cb013\" } }}", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "POST", + "urlPath": "/api/graphql", + "headers": { + "Content-Type": { + "equalTo": "application/json" + } + }, + "bodyPatterns": [ + { + "matchesJsonPath": { + "expression": "$..query", + "contains": "mutation OpprettNyOppgave" + } + } + ] + }, + "response": { + "status": 200, + "body": "{ \"data\": { \"nyOppgave\": { \"__typename\": \"NyOppgaveVellykket\", \"id\": \"9d3efc70-a86f-44e1-a038-9f2a3b478697\" } }}", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "POST", + "urlPath": "/api/graphql", + "headers": { + "Content-Type": { + "equalTo": "application/json" + } + }, + "bodyPatterns": [ + { + "matchesJsonPath": { + "expression": "$..query", + "contains": "mutation SlettingAvNotifikasjonerByEksternId" + } + } + ] + }, + "response": { + "status": 200, + "body": "{ \"data\": { \"softDeleteNotifikasjonByEksternId\": { \"__typename\": \"SoftDeleteNotifikasjonVellykket\", \"id\": \"8d5cb4b6-b1ef-4dbd-97df-8c5550c2dc83\" } } }", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "POST", + "urlPath": "/api/graphql", + "headers": { + "Content-Type": { + "equalTo": "application/json" + } + }, + "bodyPatterns": [ + { + "matchesJsonPath": { + "expression": "$..query", + "contains": "mutation OppgaveUtfoertByEksternId" + } + } + ] + }, + "response": { + "status": 200, + "body": "{ \"data\": { \"oppgaveUtfoertByEksternId\": { \"__typename\": \"OppgaveUtfoertVellykket\", \"id\": \"7c9761ab-0672-4ac8-b624-edbbee435483\" } } }", + "headers": { + "Content-Type": "application/json" + } + } + } + ] +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/norg2.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/norg2.json new file mode 100644 index 000000000..659d1a280 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/norg2.json @@ -0,0 +1,66 @@ +{ + "mappings": [ + { + "request": { + "method": "GET", + "urlPathPattern": "/norg2/api/v1/enhet/navkontor/\\d{6}" + }, + "response": { + "status": 200, + "jsonBody": { + "enhetId": 100000047, + "navn": "NAV St. Hanshaugen", + "enhetNr": "0313", + "antallRessurser": 199, + "status": "Aktiv", + "orgNivaa": "EN", + "type": "LOKAL", + "organisasjonsnummer": "993585653", + "underEtableringDato": "1970-01-01", + "aktiveringsdato": "1970-01-01", + "underAvviklingDato": null, + "nedleggelsesdato": null, + "oppgavebehandler": true, + "versjon": 35, + "sosialeTjenester": "--> Kontoret har tilpasset åpningstid pga Korona-viruset: 10 - 14. NAV St. Hanshaugen tilbyr ...", + "kanalstrategi": null, + "orgNrTilKommunaltNavKontor": "971179686" + }, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "GET", + "urlPathPattern": "/norg2/api/v1/enhet/\\d{4}" + }, + "response": { + "status": 200, + "jsonBody": { + "enhetId": 100000193, + "navn": "NAV Agder", + "enhetNr": "1000", + "antallRessurser": 80, + "status": "Aktiv", + "orgNivaa": "FYLKE", + "type": "FYLKE", + "organisasjonsnummer": null, + "underEtableringDato": "1970-01-01", + "aktiveringsdato": "1970-01-01", + "underAvviklingDato": null, + "nedleggelsesdato": null, + "oppgavebehandler": true, + "versjon": 8, + "sosialeTjenester": "Besøksadresse: Holthes vei 4\nPostadresse: Postboks 1853 Stoa, 4858 ARENDAL\nFaksnummer: 38530101\nEnhets-ID: 1000\n \nTelefontid: 08.00-15.30\nBesøkstid: 08.00-15.30\nVarelevering: 08.00-15.30 (ytterdør er låst, men nummeret til resepsjonist er skiltet)\n\nHenvendelser til fylkesadministrasjonen settes internt til vårt sentralbordnummer som er 76 123.\nOpplys gjerne om nytt felles sentralbordnummer for fylkesadministrasjonen som er 22 82 20 00.\n\nVirkemidler/Tiltak, Saksbehandling av Tiltakspenger og tilleggstønader TA: enhetnr 1087\nNAV Spesialtjenester Agder (tidligere ARK): enhetsnr 1096\nArbeidslivssentret: enhetsnr 1091", + "kanalstrategi": null, + "orgNrTilKommunaltNavKontor": null + }, + "headers": { + "Content-Type": "application/json" + } + } + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/oppgave.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/oppgave.json new file mode 100644 index 000000000..47505460f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/oppgave.json @@ -0,0 +1,31 @@ +{ + "mappings": [ + { + "request": { + "method": "POST", + "urlPattern": "/api/v1/oppgaver", + "headers": { + "X-Correlation-ID": { + "matches": ".*" + } + }, + "bodyPatterns": [ + { + "equalToJson": "{\n \"aktoerId\": \"2135315926224\",\n \"tema\": \"TIL\",\n \"prioritet\": \"NORM\",\n \"oppgavetype\": \"VURD_HENV\",\n \"behandlingstype\":\"ae0034\"\n}", + "ignoreArrayOrder": true, + "ignoreExtraElements": true + } + ] + }, + "response": { + "status": 200, + "body": "{\n \"id\": 315992387,\n \"tildeltEnhetsnr\": \"0219\",\n \"aktoerId\": \"2796812837591\",\n \"tema\": \"TIL\",\n \"oppgavetype\": \"VURD_HENV\",\n \"behandlingstype\": \"ae0034\",\n \"versjon\": 1,\n \"opprettetAv\": \"srvtiltaksgjennomf\",\n \"prioritet\": \"HOY\",\n \"status\": \"OPPRETTET\",\n \"metadata\": {},\n \"aktivDato\": \"2020-10-07\",\n \"opprettetTidspunkt\": \"2020-10-07T11:16:15.551+02:00\"\n}", + "headers": { + "Content-Type": "application/json" + } + } + } + ] +} + + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/pdl.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/pdl.json new file mode 100644 index 000000000..2cfbed58f --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/pdl.json @@ -0,0 +1,248 @@ +{ + "mappings": [ + { + "request": { + "method": "POST", + "urlPath": "/persondata" + }, + "response": { + "status": 200, + "body": "{\"errors\": [{\"message\": \"Fant ikke person\", \"locations\": [{\"line\": 1, \"column\": 8}], \"path\": [\"hentPerson\"], \"extensions\": {\"code\": \"not_found\", \"classification\": \"ExecutionAborted\"}}], \"data\": {\"hentPerson\": null}}", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "POST", + "urlPath": "/persondata", + "headers": { + "Content-Type": { + "equalTo": "application/json", + "caseInsensitive": true + } + }, + "bodyPatterns": [ + { + "equalToJson": { + "query": "query($ident: ID!) { hentPerson(ident: $ident) { adressebeskyttelse { gradering } } }", + "variables": { + "ident": "16053900422" + } + } + } + ] + }, + "response": { + "status": 200, + "body": "{\"data\": {\"hentPerson\": {\"adressebeskyttelse\": [{\"gradering\": \"STRENGT_FORTROLIG\"}]}}}", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "POST", + "urlPath": "/persondata", + "headers": { + "Content-Type": { + "equalTo": "application/json", + "caseInsensitive": true + } + }, + "bodyPatterns": [ + { + "equalToJson": { + "query": "query($ident: ID!) { hentPerson(ident: $ident) { adressebeskyttelse { gradering } } }", + "variables": { + "ident": "28033114267" + } + } + } + ] + }, + "response": { + "status": 200, + "body": "{\"data\": {\"hentPerson\": {\"adressebeskyttelse\": [{\"gradering\": \"STRENGT_FORTROLIG_UTLAND\"}]}}}", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "POST", + "urlPath": "/persondata", + "headers": { + "Content-Type": { + "equalTo": "application/json", + "caseInsensitive": true + } + }, + "bodyPatterns": [ + { + "equalToJson": { + "query": "query($ident: ID!) { hentPerson(ident: $ident) { adressebeskyttelse { gradering } } }", + "variables": { + "ident": "00000000000" + } + } + } + ] + }, + "response": { + "status": 200, + "body": "{\"data\": {\"hentPerson\": {\"adressebeskyttelse\": [{\"gradering\": \"UGRADERT\"}]}}}", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "POST", + "urlPath": "/persondata", + "headers": { + "Content-Type": { + "equalTo": "application/json", + "caseInsensitive": true + } + }, + "bodyPatterns": [ + { + "equalToJson": { + "query": "query($ident: ID!) { hentPerson(ident: $ident) { adressebeskyttelse { gradering } } }", + "variables": { + "ident": "26067114433" + } + } + } + ] + }, + "response": { + "status": 200, + "body": "{\"data\": {\"hentPerson\": {\"adressebeskyttelse\": [{\"gradering\": \"FORTROLIG\"}]}}}", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "POST", + "urlPath": "/persondata", + "headers": { + "Content-Type": { + "equalTo": "application/json", + "caseInsensitive": true + } + }, + "bodyPatterns": [ + { + "equalToJson": { + "query": "query($ident: ID!) { hentPerson(ident: $ident) { adressebeskyttelse { gradering } } }", + "variables": { + "ident": "18076641842" + } + } + } + ] + }, + "response": { + "status": 200, + "body": "{\"data\": {\"hentPerson\": {\"adressebeskyttelse\": [{\"gradering\": \"\"}]}}}", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "POST", + "urlPath": "/persondata", + "headers": { + "Content-Type": { + "equalTo": "application/json", + "caseInsensitive": true + } + }, + "bodyPatterns": [ + { + "equalToJson": { + "query": "query($ident: ID!) { hentPerson(ident: $ident) { adressebeskyttelse { gradering } } }", + "variables": { + "ident": "23097010706" + } + } + } + ] + }, + "response": { + "status": 200, + "body": "{\"errors\": [{\"message\": \"Fant ikke person\", \"locations\": [{\"line\": 1, \"column\": 8}], \"path\": [\"hentPerson\"], \"extensions\": {\"code\": \"not_found\", \"classification\": \"ExecutionAborted\"}}]}", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "POST", + "urlPath": "/persondata", + "headers": { + "Content-Type": { + "equalTo": "application/json" + } + }, + "bodyPatterns": [ + { + "equalToJson": { + "query": "query($ident: ID!) { hentPerson(ident: $ident) { navn { fornavn mellomnavn etternavn } adressebeskyttelse { gradering } } hentGeografiskTilknytning(ident: $ident){ gtType gtKommune gtBydel gtLand regel } }", + "variables": { + "ident": "00000000000" + } + } + } + ] + }, + "response": { + "status": 200, + "body": "{\"data\":{\"hentPerson\":{\"navn\":[{\"fornavn\":\"Donald\",\"etternavn\":\"Duck\"}]},\"hentGeografiskTilknytning\":{\"gtType\":\"BYDEL\",\"gtKommune\":null,\"gtBydel\":\"030104\",\"gtLand\":null,\"regel\":\"3\"}}}", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "POST", + "urlPath": "/persondata", + "headers": { + "Content-Type": { + "equalTo": "application/json", + "caseInsensitive": true + } + }, + "bodyPatterns": [ + { + "equalToJson": { + "query": "query($ident: ID!) { hentIdenter(ident: $ident, grupper: [AKTORID]) { identer { ident gruppe historisk } } }", + "variables": { + "ident": "00000000000" + } + } + } + ] + }, + "response": { + "status": 200, + "body": "{ \"data\": { \"hentIdenter\": { \"identer\": [{ \"ident\": \"2135315926224\", \"gruppe\": \"AKTORID\", \"historisk\": false }]}}}", + "headers": { + "Content-Type": "application/json" + } + } + } + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/sts.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/sts.json new file mode 100644 index 000000000..e5eaa60e9 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/sts.json @@ -0,0 +1,17 @@ +{ + "mappings": [ + { + "request": { + "method": "GET", + "urlPattern": "/sts/sts/token.*" + }, + "response": { + "status": 200, + "body": "{\"access_token\":\"fdg\",\"token_type\":\"asfsdg\",\"expires_in\":325}", + "headers": { + "Content-Type": "application/json" + } + } + } + ] +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/veilarbarena.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/veilarbarena.json new file mode 100644 index 000000000..f6a3e3a85 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/mappings/veilarbarena.json @@ -0,0 +1,162 @@ +{ + "mappings": [ + { + "priority": 1, + "request": { + "method": "GET", + "urlPattern": "^.*\\/veilarbarena\\/api\\/arena\\/status\\?fnr=31129118213" + }, + "response": { + "status": 200, + "jsonBody": { + "rettighetsgruppe": "IYT", + "formidlingsgruppe": "ARBS", + "kvalifiseringsgruppe": "BFORM", + "oppfolgingsenhet": "0906", + "iservFraDato": null + }, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 1, + "request": { + "method": "GET", + "urlPattern": "^.*\\/veilarbarena\\/api\\/arena\\/status\\?fnr=02104317386" + }, + "response": { + "status": 200, + "jsonBody": { + "rettighetsgruppe": "IYT", + "formidlingsgruppe": "ARBS", + "kvalifiseringsgruppe": "IVURD", + "oppfolgingsenhet": "0906", + "iservFraDato": null + }, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 1, + "request": { + "method": "GET", + "urlPattern": "^.*\\/veilarbarena\\/api\\/arena\\/status\\?fnr=09019621658" + }, + "response": { + "status": 200, + "jsonBody": { + "rettighetsgruppe": "IYT", + "formidlingsgruppe": "IJOBS", + "kvalifiseringsgruppe": "IVURD", + "oppfolgingsenhet": "0906", + "iservFraDato": null + }, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 1, + "request": { + "method": "GET", + "urlPattern": "^.*\\/veilarbarena\\/api\\/arena\\/status\\?fnr=11111111111" + }, + "response": { + "status": 200, + "jsonBody": { + "rettighetsgruppe": "IYT", + "formidlingsgruppe": "IJOBS", + "kvalifiseringsgruppe": "BFORM", + "oppfolgingsenhet": "0904", + "iservFraDato": null + }, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 1, + "request": { + "method": "GET", + "urlPattern": "/veilarbarena/api/arena/status?fnr=22222222222" + }, + "response": { + "status": 404, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 1, + "request": { + "method": "GET", + "urlPattern": "/veilarbarena/api/arena/status?fnr=33333333333" + }, + "response": { + "status": 404, + "jsonBody": { + "timestamp": "2021-01-04T13:06:25.211+00:00", + "status": 404, + "error": "Not Found", + "message": "", + "path": "/veilarbarena/api/arena/status?fnr=33333333333" + }, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 1, + "request": { + "method": "GET", + "urlPath": "/veilarbarena/api/arena/status", + "queryParameters": { + "fnr": { + "equalTo": "06109845173" + } + } + }, + "response": { + "status": 404, + "jsonBody": { + "timestamp": "2021-01-04T13:06:25.211+00:00", + "status": 404, + "error": "Not Found", + "message": "", + "path": "/veilarbarena/api/arena/status?fnr=06109845173" + }, + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "priority": 2, + "request": { + "method": "GET", + "urlPattern": "^.*\\/veilarbarena\\/api\\/arena\\/status\\?fnr=((0[1-9]|[1-2][0-9]|31(?!(?:0[2469]|11))|30(?!02))(0[1-9]|1[0-2])\\d{7}$|[0-0]{11})$" + }, + "response": { + "status": 200, + "jsonBody": { + "rettighetsgruppe": "IYT", + "formidlingsgruppe": "ARBS", + "kvalifiseringsgruppe": "VARIG", + "oppfolgingsenhet": "0906", + "iservFraDato": null + }, + "headers": { + "Content-Type": "application/json" + } + } + } + ] +} diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/metadata-isso.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/metadata-isso.json new file mode 100644 index 000000000..1c7779218 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/metadata-isso.json @@ -0,0 +1,34 @@ +{ + "issuer": "isso", + "authorization_endpoint": "na", + "token_endpoint": "na", + "end_session_endpoint": "na", + "jwks_uri": "http://jwks", + "response_modes_supported": [ + "query", + "fragment", + "form_post" + ], + "response_types_supported": [ + "code", + "code id_token", + "code token", + "code id_token token", + "id_token", + "id_token token", + "token", + "token id_token" + ], + "scopes_supported": [ + "openid" + ], + "subject_types_supported": [ + "pairwise" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_post" + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/metadata-selvbetjening.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/metadata-selvbetjening.json new file mode 100644 index 000000000..f0f89fd25 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/metadata-selvbetjening.json @@ -0,0 +1,34 @@ +{ + "issuer": "selvbetjening", + "authorization_endpoint": "na", + "token_endpoint": "na", + "end_session_endpoint": "na", + "jwks_uri": "http://jwks", + "response_modes_supported": [ + "query", + "fragment", + "form_post" + ], + "response_types_supported": [ + "code", + "code id_token", + "code token", + "code id_token token", + "id_token", + "id_token token", + "token", + "token id_token" + ], + "scopes_supported": [ + "openid" + ], + "subject_types_supported": [ + "pairwise" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_post" + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/metadata-system.json b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/metadata-system.json new file mode 100644 index 000000000..4762dae11 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/metadata-system.json @@ -0,0 +1,34 @@ +{ + "issuer": "system", + "authorization_endpoint": "na", + "token_endpoint": "na", + "end_session_endpoint": "na", + "jwks_uri": "http://jwks", + "response_modes_supported": [ + "query", + "fragment", + "form_post" + ], + "response_types_supported": [ + "code", + "code id_token", + "code token", + "code id_token token", + "id_token", + "id_token token", + "token", + "token id_token" + ], + "scopes_supported": [ + "openid" + ], + "subject_types_supported": [ + "pairwise" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_post" + ] +} \ No newline at end of file diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/policies/requestSamlPolicy.xml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/policies/requestSamlPolicy.xml new file mode 100644 index 000000000..eb36375e5 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/policies/requestSamlPolicy.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0 + + + http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0 + + http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer + + + + + + + + + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/policies/requestSamlPolicyNoTransportBinding.xml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/policies/requestSamlPolicyNoTransportBinding.xml new file mode 100644 index 000000000..a72aa4bcc --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/policies/requestSamlPolicyNoTransportBinding.xml @@ -0,0 +1,22 @@ + + + + + + + + + + http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0 + + http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0 + http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer + + + + + + + + + diff --git a/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/policies/untPolicy.xml b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/policies/untPolicy.xml new file mode 100644 index 000000000..a9c629370 --- /dev/null +++ b/jdk_17_maven/cs/rest/tiltaksgjennomforing-api/src/test/resources/policies/untPolicy.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jdk_17_maven/em/embedded/pom.xml b/jdk_17_maven/em/embedded/pom.xml index b3da070b1..9925b31bb 100644 --- a/jdk_17_maven/em/embedded/pom.xml +++ b/jdk_17_maven/em/embedded/pom.xml @@ -14,6 +14,7 @@ web grpc + rest - \ No newline at end of file + diff --git a/jdk_17_maven/em/embedded/rest/familie-ba-sak/pom.xml b/jdk_17_maven/em/embedded/rest/familie-ba-sak/pom.xml new file mode 100644 index 000000000..b7761eaf6 --- /dev/null +++ b/jdk_17_maven/em/embedded/rest/familie-ba-sak/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + org.evomaster + evomaster-benchmark-jdk17-em-embedded-rest + 3.0.1-SNAPSHOT + + + evomaster-benchmark-jdk17-em-embedded-rest-familie-ba-sak + jar + + + + + + org.springframework.boot + spring-boot-dependencies + 3.1.3 + pom + import + + + + + + + no.nav.familie.ba.sak + familie-ba-sak + 1-SNAPSHOT + + + org.testcontainers + testcontainers + compile + + + org.springframework + spring-context + + + no.nav.security + mock-oauth2-server + 2.1.4 + + + com.fasterxml.jackson.module + jackson-module-kotlin + 2.17.0 + + + + + + diff --git a/jdk_17_maven/em/embedded/rest/familie-ba-sak/src/main/java/em/embedded/familie/ba/sak/EmbeddedEvoMasterController.java b/jdk_17_maven/em/embedded/rest/familie-ba-sak/src/main/java/em/embedded/familie/ba/sak/EmbeddedEvoMasterController.java new file mode 100644 index 000000000..4d7afa40e --- /dev/null +++ b/jdk_17_maven/em/embedded/rest/familie-ba-sak/src/main/java/em/embedded/familie/ba/sak/EmbeddedEvoMasterController.java @@ -0,0 +1,307 @@ +package em.embedded.familie.ba.sak; + +import com.nimbusds.jose.JOSEObjectType; +import no.nav.security.mock.oauth2.MockOAuth2Server; +import no.nav.security.mock.oauth2.OAuth2Config; +import no.nav.security.mock.oauth2.token.RequestMapping; +import no.nav.security.mock.oauth2.token.RequestMappingTokenCallback; +import org.evomaster.client.java.controller.EmbeddedSutController; +import org.evomaster.client.java.controller.InstrumentedSutStarter; +import org.evomaster.client.java.controller.api.dto.SutInfoDto; +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto; +import org.evomaster.client.java.controller.api.dto.auth.HttpVerb; +import org.evomaster.client.java.controller.api.dto.auth.LoginEndpointDto; +import org.evomaster.client.java.controller.api.dto.auth.TokenHandlingDto; +import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType; +import org.evomaster.client.java.controller.problem.ProblemInfo; +import org.evomaster.client.java.controller.problem.RestProblem; +import org.evomaster.client.java.sql.DbSpecification; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.testcontainers.containers.GenericContainer; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.*; + + +public class EmbeddedEvoMasterController extends EmbeddedSutController { + + private static final String POSTGRES_VERSION = "13.13"; + + private static final String POSTGRES_PASSWORD = "password"; + + private static final int POSTGRES_PORT = 5432; + + private static final GenericContainer postgresContainer = new GenericContainer("postgres:" + POSTGRES_VERSION) + .withEnv("POSTGRES_PASSWORD", POSTGRES_PASSWORD) + .withEnv("POSTGRES_HOST_AUTH_METHOD", "trust") //to allow all connections without a password + .withEnv("POSTGRES_DB", "familiebasak") + .withTmpFs(Collections.singletonMap("/var/lib/postgresql/data", "rw")) + .withExposedPorts(POSTGRES_PORT); + + private ConfigurableApplicationContext ctx; + + private MockOAuth2Server oAuth2Server; + + private final String ISSUER_ID = "azuread"; + + private final String DEFAULT_AUDIENCE = "some-audience"; + + private final String PROSESSERING_ROLLE = "928636f4-fd0d-4149-978e-a6fb68bb19de"; + + private final String TOKEN_PARAM = "name"; + + private static final String A0 = "TaskRunner"; + private static final String A1 = "Veileder"; + private static final String A2 = "Saksbehandler"; + private static final String A3 = "Beslutter"; + private static final String A4 = "Forvalter"; + private static final String A5 = "Kode6"; + private static final String A6 = "Kode7"; + private static final String A7 = "System"; + + private static final String veileder = "93a26831-9866-4410-927b-74ff51a9107c"; + private static final String saksbehandler = "d21e00a4-969d-4b28-8782-dc818abfae65"; + private static final String beslutter = "9449c153-5a1e-44a7-84c6-7cc7a8867233"; + private static final String forvalter = "c62e908a-cf20-4ad0-b7b3-3ff6ca4bf38b"; + private static final String kode6 = "5ef775f2-61f8-4283-bf3d-8d03f428aa14"; + private static final String kode7 = "ea930b6b-9397-44d9-b9e6-f4cf527a632a"; + + private Connection sqlConnection; + private List dbSpecification; + + public EmbeddedEvoMasterController() { + this(40100); + } + + public EmbeddedEvoMasterController(int port) { + setControllerPort(port); + } + + + public static void main(String[] args) { + int port = 40100; + if (args.length > 0) { + port = Integer.parseInt(args[0]); + } + + EmbeddedEvoMasterController controller = new EmbeddedEvoMasterController(port); + InstrumentedSutStarter starter = new InstrumentedSutStarter(controller); + + starter.start(); + } + + @Override + public boolean isSutRunning() { + return ctx!=null && ctx.isRunning(); + } + + @Override + public String getPackagePrefixesToCover() { + return "no.nav.familie.ba.sak."; + } + + + @Override + public List getInfoForAuthentication() { + + String url = oAuth2Server.baseUrl() + ISSUER_ID + "/token"; + + return Arrays.asList( + getAuthenticationDto(A0,url), + getAuthenticationDto(A1,url), + getAuthenticationDto(A2,url), + getAuthenticationDto(A3,url), + getAuthenticationDto(A4,url), + getAuthenticationDto(A5,url), + getAuthenticationDto(A6,url), + getAuthenticationDto(A7,url) + ); + } + + private RequestMapping getRequestMapping(String label, List groups, String id, String name) { + Map claims = new HashMap<>(); + claims.put("groups",groups); + claims.put("name",name); + claims.put("NAVident", id); + claims.put("sub","subject"); + claims.put("aud","some-audience"); + claims.put("tid",ISSUER_ID); + claims.put("azp",id); + + RequestMapping rm = new RequestMapping(TOKEN_PARAM,label,claims,JOSEObjectType.JWT.getType()); + + return rm; + } + + private OAuth2Config getOAuth2Config(){ + + List mappings = Arrays.asList( getRequestMapping(A0, Arrays.asList(PROSESSERING_ROLLE),"Z0042", "Task Runner"), + getRequestMapping(A1, Arrays.asList(veileder),"Z0000", "Mock McMockface"), + getRequestMapping(A2, Arrays.asList(saksbehandler),"Z0001", "Foo Bar"), + getRequestMapping(A3, Arrays.asList(beslutter),"Z0002", "John Smith"), + getRequestMapping(A4, Arrays.asList(forvalter),"Z0003", "Mario Rossi"), + getRequestMapping(A5, Arrays.asList(kode6),"Z0004", "Kode Six"), + getRequestMapping(A6, Arrays.asList(kode7),"Z0005", "Kode Seven"), + getRequestMapping(A7, Arrays.asList(),"VL", "The System") + ); + + RequestMappingTokenCallback callback = new RequestMappingTokenCallback( + ISSUER_ID, + mappings, + 360000 + ); + + Set callbacks = Set.of( + callback + ); + + OAuth2Config config = new OAuth2Config( + true, + null, + null, + false, + new no.nav.security.mock.oauth2.token.OAuth2TokenProvider(), + callbacks + ); + + return config; + } + + private AuthenticationDto getAuthenticationDto(String label, String oauth2Url){ + + AuthenticationDto dto = new AuthenticationDto(label); + LoginEndpointDto x = new LoginEndpointDto(); + dto.loginEndpointAuth = x; + + x.externalEndpointURL = oauth2Url; + x.payloadRaw = TOKEN_PARAM+"="+label+"&grant_type=client_credentials&code=foo&client_id=foo&client_secret=secret"; + x.verb = HttpVerb.POST; + x.contentType = "application/x-www-form-urlencoded"; + x.expectCookies = false; + + TokenHandlingDto token = new TokenHandlingDto(); + token.headerPrefix = "Bearer "; + token.httpHeaderName = "Authorization"; + token.extractFromField = "/access_token"; + x.token = token; + + return dto; + } + + + @Override + public ProblemInfo getProblemInfo() { + return new RestProblem( + "http://localhost:" + getSutPort() + "/v3/api-docs", + null + ); + } + + @Override + public SutInfoDto.OutputFormat getPreferredOutputFormat() { + return SutInfoDto.OutputFormat.JAVA_JUNIT_5; + } + + @Override + public String startSut() { + postgresContainer.start(); + + oAuth2Server = new MockOAuth2Server(getOAuth2Config()); + oAuth2Server.start(8081); //ephemeral gives issues in generated tests + + String wellKnownUrl = oAuth2Server.wellKnownUrl(ISSUER_ID).toString(); + + + String postgresURL = "jdbc:postgresql://" + postgresContainer.getHost() + ":" + postgresContainer.getMappedPort(POSTGRES_PORT) + "/familiebasak"; + + //TODO should go through all the environment variables in application properties + System.setProperty("AZUREAD_TOKEN_ENDPOINT_URL","http://fake-azure-token-endpoint.no:8080"); + System.setProperty("AZURE_OPENID_CONFIG_TOKEN_ENDPOINT","bar"); + System.setProperty("AZURE_APP_CLIENT_ID","bar"); + System.setProperty("NAIS_APP_NAME","bar"); + System.setProperty("UNLEASH_SERVER_API_URL","http://fake-unleash-server-api.no:8080"); + System.setProperty("UNLEASH_SERVER_API_TOKEN","bar"); + System.setProperty("BA_SAK_CLIENT_ID", DEFAULT_AUDIENCE); + + ctx = SpringApplication.run(no.nav.familie.ba.sak.FamilieBaSakApplication.class, new String[]{ + "--server.port=0", + "--spring.profiles.active=dev", + "--management.server.port=-1", + "--server.ssl.enabled=false", + "--spring.datasource.url=" + postgresURL, + "--spring.datasource.username=postgres", + "--spring.datasource.password=" + POSTGRES_PASSWORD, + "--sentry.logging.enabled=false", + "--sentry.environment=local", + //TODO check when dealing with Kafka + "--funksjonsbrytere.kafka.producer.enabled=false", + "--funksjonsbrytere.enabled=false", + "--logging.level.root=OFF", + "--logging.config=classpath:logback-spring.xml", + "--logging.level.org.springframework=INFO", + "--no.nav.security.jwt.issuer.azuread.discoveryurl="+wellKnownUrl, + "--prosessering.rolle=" + PROSESSERING_ROLLE, + "--FAMILIE_EF_SAK_API_URL=http://fake-familie-ef-sak/api", + "--FAMILIE_KLAGE_URL=http://fake-familie-klage", + "--FAMILIE_BREV_API_URL=http://fake-familie-brev", + "--FAMILIE_BA_INFOTRYGD_FEED_API_URL=http://fake-familie-ba-infotrygd-feed/api", + "--FAMILIE_BA_INFOTRYGD_API_URL=http://fake-familie-ba-infotrygd", + "--FAMILIE_TILBAKE_API_URL=http://fake-familie-tilbake/api", + "--PDL_URL=http://fake-pdl-api.default", + "--FAMILIE_INTEGRASJONER_API_URL=http://fake-familie-integrasjoner/api", + "--FAMILIE_OPPDRAG_API_URL=http://fake-familie-oppdrag/api", + "--SANITY_FAMILIE_API_URL=http://fake-xsrv1mh6.apicdn.sanity.io/v2021-06-07/data/query/ba-brev", + "--ECB_API_URL=http://fake-data-api.ecb.europa.eu/service/data/EXR/", + "--rolle.veileder=" + veileder, + "--rolle.saksbehandler=" + saksbehandler, + "--rolle.beslutter=" + beslutter, + "--rolle.forvalter=" + forvalter, + "--rolle.kode6=" + kode6, + "--rolle.kode7=" + kode7 + }); + + if (sqlConnection != null) { + try { + sqlConnection.close(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + try { + sqlConnection = DriverManager.getConnection(postgresURL, "postgres", POSTGRES_PASSWORD); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + dbSpecification = Arrays.asList(new DbSpecification(DatabaseType.POSTGRES, sqlConnection)); + + return "http://localhost:" + getSutPort(); + } + + protected int getSutPort() { + // return ctx.getEnvironment().getProperty("server.port", Integer.class); + return (Integer) ((Map) ctx.getEnvironment() + .getPropertySources().get("server.ports").getSource()) + .get("local.server.port"); + } + + @Override + public void stopSut() { + postgresContainer.stop(); + if(oAuth2Server!=null) oAuth2Server.shutdown(); + if(ctx!=null)ctx.stop(); + } + + @Override + public void resetStateOfSUT() { + } + + @Override + public List getDbSpecifications() { + return dbSpecification; + } +} diff --git a/jdk_17_maven/em/embedded/rest/familie-tilbake/pom.xml b/jdk_17_maven/em/embedded/rest/familie-tilbake/pom.xml new file mode 100644 index 000000000..775372f4d --- /dev/null +++ b/jdk_17_maven/em/embedded/rest/familie-tilbake/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + org.evomaster + evomaster-benchmark-jdk17-em-embedded-rest + 3.0.1-SNAPSHOT + + + evomaster-benchmark-jdk17-em-embedded-rest-familie-tilbake + jar + + + + + org.springframework.boot + spring-boot-dependencies + 3.2.0 + pom + import + + + + + + + no.nav + familie-tilbake + 1.0-SNAPSHOT + + + org.testcontainers + testcontainers + 1.19.1 + compile + + + org.springframework + spring-context + + + + diff --git a/jdk_17_maven/em/embedded/rest/familie-tilbake/src/main/java/em/embedded/familie/tilbake/EmbeddedEvoMasterController.java b/jdk_17_maven/em/embedded/rest/familie-tilbake/src/main/java/em/embedded/familie/tilbake/EmbeddedEvoMasterController.java new file mode 100644 index 000000000..d1cf34069 --- /dev/null +++ b/jdk_17_maven/em/embedded/rest/familie-tilbake/src/main/java/em/embedded/familie/tilbake/EmbeddedEvoMasterController.java @@ -0,0 +1,162 @@ +package em.embedded.familie.tilbake; + +import no.nav.familie.tilbake.Launcher; +import org.evomaster.client.java.controller.EmbeddedSutController; +import org.evomaster.client.java.controller.InstrumentedSutStarter; +import org.evomaster.client.java.controller.api.dto.SutInfoDto; +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto; +import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType; +import org.evomaster.client.java.controller.problem.ProblemInfo; +import org.evomaster.client.java.controller.problem.RestProblem; +import org.evomaster.client.java.sql.DbCleaner; +import org.evomaster.client.java.sql.DbSpecification; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.jdbc.core.JdbcTemplate; +import org.testcontainers.containers.GenericContainer; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class EmbeddedEvoMasterController extends EmbeddedSutController { + + private static final String POSTGRES_VERSION = "13.13"; + + private static final String POSTGRES_PASSWORD = "password"; + + private static final int POSTGRES_PORT = 5432; + + private static final GenericContainer postgresContainer = new GenericContainer("postgres:" + POSTGRES_VERSION) + .withEnv("POSTGRES_PASSWORD", POSTGRES_PASSWORD) + .withEnv("POSTGRES_HOST_AUTH_METHOD", "trust") //to allow all connections without a password + .withEnv("POSTGRES_DB", "familietilbake") + .withTmpFs(Collections.singletonMap("/var/lib/postgresql/data", "rw")) + .withExposedPorts(POSTGRES_PORT); + + private ConfigurableApplicationContext ctx; + + private Connection sqlConnection; + private List dbSpecification; + + public EmbeddedEvoMasterController() { + this(40100); + } + + public EmbeddedEvoMasterController(int port) { + setControllerPort(port); + } + + public static void main(String[] args) { + int port = 40100; + if (args.length > 0) { + port = Integer.parseInt(args[0]); + } + + EmbeddedEvoMasterController controller = new EmbeddedEvoMasterController(port); + InstrumentedSutStarter starter = new InstrumentedSutStarter(controller); + + starter.start(); + } + + @Override + public boolean isSutRunning() { + return ctx!=null && ctx.isRunning(); + } + + @Override + public String getPackagePrefixesToCover() { + return "no.nav.familie.tilbake."; + } + + @Override + public List getInfoForAuthentication() { + return null; + } + + @Override + public ProblemInfo getProblemInfo() { + return new RestProblem( + "http://localhost:" + getSutPort() + "/v3/api-docs", + null + ); + } + + @Override + public SutInfoDto.OutputFormat getPreferredOutputFormat() { + return SutInfoDto.OutputFormat.JAVA_JUNIT_5; + } + + @Override + public String startSut() { + postgresContainer.start(); + + String postgresURL = "jdbc:postgresql://" + postgresContainer.getHost() + ":" + postgresContainer.getMappedPort(POSTGRES_PORT) + "/familietilbake"; + + System.setProperty("AZURE_APP_WELL_KNOWN_URL", "http://azure:8080/"); + System.setProperty("AZURE_OPENID_CONFIG_TOKEN_ENDPOINT", "http://foo:8080/"); + System.setProperty("UNLEASH_SERVER_API_URL", "http://bar:8080/"); + System.setProperty("UNLEASH_SERVER_API_TOKEN", "71c722758740d43341c295ffdc237bd3"); + System.setProperty("NAIS_APP_NAME", "familietilbake"); + System.setProperty("NAIS_CLUSTER_NAME", "dev-gcp"); + System.setProperty("KAFKA_TRUSTSTORE_PATH", "dev-gcp"); + + ctx = SpringApplication.run(Launcher.class, new String[]{ + "--server.port=0", + "--spring.profiles.active=dev", + "--management.server.port=-1", + "--server.ssl.enabled=false", + "--spring.datasource.url=" + postgresURL, + "--spring.datasource.username=postgres", + "--spring.datasource.password=" + POSTGRES_PASSWORD, + "--sentry.logging.enabled=false", + "--sentry.environment=local", + "--logging.level.root=OFF", + "--logging.config=classpath:logback-spring.xml", + "--logging.level.org.springframework=INFO" + }); + + if (sqlConnection != null) { + try { + sqlConnection.close(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + try { + sqlConnection = DriverManager.getConnection(postgresURL, "postgres", POSTGRES_PASSWORD); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + dbSpecification = Arrays.asList(new DbSpecification(DatabaseType.POSTGRES, sqlConnection)); + + return "http://localhost:" + getSutPort(); + } + + protected int getSutPort() { + return (Integer) ((Map) ctx.getEnvironment() + .getPropertySources().get("server.ports").getSource()) + .get("local.server.port"); + } + + @Override + public void stopSut() { + postgresContainer.stop(); + if(ctx!=null) ctx.stop(); + } + + @Override + public void resetStateOfSUT() { + } + + @Override + public List getDbSpecifications() { + return dbSpecification; + } +} diff --git a/jdk_17_maven/em/embedded/rest/pom.xml b/jdk_17_maven/em/embedded/rest/pom.xml new file mode 100644 index 000000000..1197685bd --- /dev/null +++ b/jdk_17_maven/em/embedded/rest/pom.xml @@ -0,0 +1,22 @@ + + 4.0.0 + + + org.evomaster + evomaster-benchmark-jdk17-em-embedded + 3.0.1-SNAPSHOT + + + evomaster-benchmark-jdk17-em-embedded-rest + pom + + + familie-ba-sak + + + + + + + diff --git a/jdk_17_maven/em/embedded/rest/signal-server/pom.xml b/jdk_17_maven/em/embedded/rest/signal-server/pom.xml new file mode 100644 index 000000000..1f397667d --- /dev/null +++ b/jdk_17_maven/em/embedded/rest/signal-server/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + org.evomaster + evomaster-benchmark-jdk17-em-embedded-rest + 3.0.1-SNAPSHOT + + + evomaster-benchmark-jdk17-em-embedded-rest-signal-server + + + 17 + 17 + UTF-8 + + + + + org.whispersystems.textsecure + service + 10.3.0 + compile + + + org.testcontainers + testcontainers + 1.19.1 + compile + + + + redis.clients + jedis + 4.3.1 + + + + diff --git a/jdk_17_maven/em/embedded/rest/signal-server/src/main/java/em/embedded/textsecuregcm/EmbeddedEvoMasterController.java b/jdk_17_maven/em/embedded/rest/signal-server/src/main/java/em/embedded/textsecuregcm/EmbeddedEvoMasterController.java new file mode 100644 index 000000000..7a0eef4f3 --- /dev/null +++ b/jdk_17_maven/em/embedded/rest/signal-server/src/main/java/em/embedded/textsecuregcm/EmbeddedEvoMasterController.java @@ -0,0 +1,174 @@ +package em.embedded.textsecuregcm; + +import org.evomaster.client.java.controller.EmbeddedSutController; +import org.evomaster.client.java.controller.InstrumentedSutStarter; +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto; +import org.evomaster.client.java.controller.api.dto.SutInfoDto; +import org.evomaster.client.java.controller.problem.ProblemInfo; +import org.evomaster.client.java.controller.problem.RestProblem; +import org.evomaster.client.java.sql.DbSpecification; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; +import org.whispersystems.textsecuregcm.WhisperServerService; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +import java.util.List; + +public class EmbeddedEvoMasterController extends EmbeddedSutController { + + private static final int DYNAMODB_PORT = 8000; + + private static final String DYNAMODB_VERSION = "1.25.0"; + + private static final GenericContainer dynamoDBContainer = new GenericContainer("amazon/dynamodb-local:" + DYNAMODB_VERSION) + .withExposedPorts(DYNAMODB_PORT); + + private static final int REDIS_PORT = 6379; + + private static final String REDIS_VERSION = "7.2.3"; + + private static final DockerImageName REDIS_IMAGE = DockerImageName.parse("redis:" + REDIS_VERSION); + + private static JedisPool jedisPool; + + private static final GenericContainer redisContainer = new GenericContainer(REDIS_IMAGE) + .withExposedPorts(REDIS_PORT) + .withEnv("ALLOW_EMPTY_PASSWORD", "yes") + .withEnv("REDIS_NODES", "redis-cluster-01") + .withCopyFileToContainer(MountableFile.forHostPath("src/main/resources/redis.conf"), "/usr/local/etc/redis/redis.conf") + .withCommand("redis-server /usr/local/etc/redis/redis.conf"); + + public static void main(String[] args) { + + int port = 40100; + if (args.length > 0) { + port = Integer.parseInt(args[0]); + } + + EmbeddedEvoMasterController controller = new EmbeddedEvoMasterController(port); + InstrumentedSutStarter starter = new InstrumentedSutStarter(controller); + + starter.start(); + } + + public EmbeddedEvoMasterController() { + this(40100); + } + + public EmbeddedEvoMasterController(int port) { + setControllerPort(port); + } + + private WhisperServerService application; + + @Override + public boolean isSutRunning() { + if (application == null) { + return false; + } + + return application.getJettyServer().isRunning(); + } + + @Override + public String getPackagePrefixesToCover() { + return "org.whispersystems.textsecuregcm."; + } + + @Override + public List getInfoForAuthentication() { + return null; + } + + @Override + public ProblemInfo getProblemInfo() { + return new RestProblem( + "http://localhost:" + application.getJettyPort() + "/assets/swagger.json", + null + ); + } + + @Override + public SutInfoDto.OutputFormat getPreferredOutputFormat() { + return SutInfoDto.OutputFormat.JAVA_JUNIT_5; + } + + @Override + public String startSut() { + System.setProperty("aws.region", "us-west-2"); + + redisContainer.start(); + jedisPool = new JedisPool(redisContainer.getHost(), redisContainer.getMappedPort(REDIS_PORT)); + + dynamoDBContainer.start(); + + application = new WhisperServerService(); + + String redisURL = "redis://" + redisContainer.getHost() + ":" + redisContainer.getMappedPort(REDIS_PORT) + "/"; + + //Dirty hack for DW... + System.setProperty("dw.server.applicationConnectors[0].port", "0"); + + System.setProperty("dw.cacheCluster.configurationUri", redisURL); + System.setProperty("dw.clientPresenceCluster.configurationUri", redisURL); + System.setProperty("dw.pubsub.uri", redisURL); + System.setProperty("dw.pushSchedulerCluster.configurationUri", redisURL); + System.setProperty("dw.rateLimitersCluster.configurationUri", redisURL); + System.setProperty("dw.messageCache.cluster.configurationUri", redisURL); + System.setProperty("dw.metricsCluster.configurationUri", redisURL); + + System.setProperty("secrets.bundle.filename", "src/main/resources/secrets.yml"); + + try { + application.run("server", "src/main/resources/em-sample.yml"); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + + try { + Thread.sleep(3_000); + } catch (InterruptedException e) { + + } + + while(!application.getJettyServer().isStarted()) { + try { + Thread.sleep(3_000); + } catch (InterruptedException e) { + + } + } + + return "http://localhost:" + application.getJettyPort(); + } + + @Override + public void stopSut() { + if (application != null) { + try { + application.getJettyServer().stop(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + redisContainer.stop(); + dynamoDBContainer.stop(); + } + + @Override + public void resetStateOfSUT() { + try (Jedis jedis = jedisPool.getResource()) { + jedis.flushAll(); + } + } + + @Override + public List getDbSpecifications() { + return null; + } + +} diff --git a/jdk_17_maven/em/embedded/rest/signal-server/src/main/resources/em-sample.yml b/jdk_17_maven/em/embedded/rest/signal-server/src/main/resources/em-sample.yml new file mode 100644 index 000000000..2855c5494 --- /dev/null +++ b/jdk_17_maven/em/embedded/rest/signal-server/src/main/resources/em-sample.yml @@ -0,0 +1,409 @@ +# Example, relatively minimal, configuration that passes validation (see `io.dropwizard.cli.CheckCommand`) +# +# `unset` values will need to be set to work properly. +# Most other values are technically valid for a local/demonstration environment, but are probably not production-ready. +server: + rootPath: '/*' + applicationConnectors: + - type: http + port: ${PORT:-8080} + +logging: + level: INFO + appenders: + - type: console + threshold: ALL + timeZone: UTC + target: stdout + - type: logstashtcpsocket + destination: example.com:10516 + apiKey: secret://datadog.apiKey + environment: staging + +metrics: + reporters: + - type: signal-datadog + frequency: 10 seconds + tags: + - "env:staging" + - "service:chat" + udpTransport: + statsdHost: localhost + port: 8125 + excludesAttributes: + - m1_rate + - m5_rate + - m15_rate + - mean_rate + - stddev + useRegexFilters: true + excludes: + - ^.+\.total$ + - ^.+\.request\.filtering$ + - ^.+\.response\.filtering$ + - ^executor\..+$ + - ^lettuce\..+$ + reportOnStop: true + +adminEventLoggingConfiguration: + credentials: | + { + "key": "value" + } + projectId: some-project-id + logName: some-log-name + +grpcPort: 8080 + +stripe: + apiKey: secret://stripe.apiKey + idempotencyKeyGenerator: secret://stripe.idempotencyKeyGenerator + boostDescription: > + Example + supportedCurrenciesByPaymentMethod: + CARD: + - usd + - eur + SEPA_DEBIT: + - eur + + +braintree: + merchantId: unset + publicKey: unset + privateKey: secret://braintree.privateKey + environment: unset + graphqlUrl: unset + merchantAccounts: + # ISO 4217 currency code and its corresponding sub-merchant account + 'xts': unset + supportedCurrenciesByPaymentMethod: + PAYPAL: + - usd + +dynamoDbClientConfiguration: + region: us-west-2 # AWS Region + +dynamoDbTables: + accounts: + tableName: Example_Accounts + phoneNumberTableName: Example_Accounts_PhoneNumbers + phoneNumberIdentifierTableName: Example_Accounts_PhoneNumberIdentifiers + usernamesTableName: Example_Accounts_Usernames + scanPageSize: 100 + clientReleases: + tableName: Example_ClientReleases + deletedAccounts: + tableName: Example_DeletedAccounts + deletedAccountsLock: + tableName: Example_DeletedAccountsLock + issuedReceipts: + tableName: Example_IssuedReceipts + expiration: P30D # Duration of time until rows expire + generator: abcdefg12345678= # random base64-encoded binary sequence + ecKeys: + tableName: Example_Keys + ecSignedPreKeys: + tableName: Example_EC_Signed_Pre_Keys + pqKeys: + tableName: Example_PQ_Keys + pqLastResortKeys: + tableName: Example_PQ_Last_Resort_Keys + messages: + tableName: Example_Messages + expiration: P30D # Duration of time until rows expire + phoneNumberIdentifiers: + tableName: Example_PhoneNumberIdentifiers + profiles: + tableName: Example_Profiles + pushChallenge: + tableName: Example_PushChallenge + redeemedReceipts: + tableName: Example_RedeemedReceipts + expiration: P30D # Duration of time until rows expire + registrationRecovery: + tableName: Example_RegistrationRecovery + expiration: P300D # Duration of time until rows expire + remoteConfig: + tableName: Example_RemoteConfig + reportMessage: + tableName: Example_ReportMessage + subscriptions: + tableName: Example_Subscriptions + verificationSessions: + tableName: Example_VerificationSessions + +cacheCluster: # Redis server configuration for cache cluster + configurationUri: redis://redis.example.com:6379/ + +clientPresenceCluster: # Redis server configuration for client presence cluster + configurationUri: redis://redis.example.com:6379/ + +pubsub: # Redis server configuration for pubsub cluster + uri: redis://redis.example.com:6379/ + +pushSchedulerCluster: # Redis server configuration for push scheduler cluster + configurationUri: redis://redis.example.com:6379/ + +rateLimitersCluster: # Redis server configuration for rate limiters cluster + configurationUri: redis://redis.example.com:6379/ + +directoryV2: + client: # Configuration for interfacing with Contact Discovery Service v2 cluster + userAuthenticationTokenSharedSecret: secret://directoryV2.client.userAuthenticationTokenSharedSecret + userIdTokenSharedSecret: secret://directoryV2.client.userIdTokenSharedSecret + +svr2: + uri: svr2.example.com + userAuthenticationTokenSharedSecret: secret://svr2.userAuthenticationTokenSharedSecret + userIdTokenSharedSecret: secret://svr2.userIdTokenSharedSecret + svrCaCertificates: + - | + -----BEGIN CERTIFICATE----- + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + AAAAAAAAAAAAAAAAAAAA + -----END CERTIFICATE----- + + +messageCache: # Redis server configuration for message store cache + persistDelayMinutes: 1 + cluster: + configurationUri: redis://redis.example.com:6379/ + +metricsCluster: + configurationUri: redis://redis.example.com:6379/ + +awsAttachments: # AWS S3 configuration + accessKey: secret://awsAttachments.accessKey + accessSecret: secret://awsAttachments.accessSecret + bucket: aws-attachments + region: us-west-2 + +gcpAttachments: # GCP Storage configuration + domain: example.com + email: user@example.cocm + maxSizeInBytes: 1024 + pathPrefix: + rsaSigningKey: secret://gcpAttachments.rsaSigningKey + +tus: + uploadUri: https://example.org/upload + userAuthenticationTokenSharedSecret: secret://tus.userAuthenticationTokenSharedSecret + +accountDatabaseCrawler: + chunkSize: 10 # accounts per run + +apn: # Apple Push Notifications configuration + sandbox: true + bundleId: com.example.textsecuregcm + keyId: secret://apn.keyId + teamId: secret://apn.teamId + signingKey: secret://apn.signingKey + +fcm: # FCM configuration + credentials: secret://fcm.credentials + +cdn: + accessKey: secret://cdn.accessKey + accessSecret: secret://cdn.accessSecret + bucket: cdn # S3 Bucket name + region: us-west-2 # AWS region + +dogstatsd: + environment: dev + +unidentifiedDelivery: + certificate: secret://unidentifiedDelivery.certificate + privateKey: secret://unidentifiedDelivery.privateKey + expiresDays: 7 + +recaptcha: + projectPath: projects/example + credentialConfigurationJson: "{ }" # service account configuration for backend authentication + +hCaptcha: + apiKey: secret://hCaptcha.apiKey + +shortCode: + baseUrl: https://example.com/shortcodes/ + +storageService: + uri: storage.example.com + userAuthenticationTokenSharedSecret: secret://storageService.userAuthenticationTokenSharedSecret + storageCaCertificates: + - | + -----BEGIN CERTIFICATE----- + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + AAAAAAAAAAAAAAAAAAAA + -----END CERTIFICATE----- + +zkConfig: + serverPublic: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + serverSecret: secret://zkConfig.serverSecret + +genericZkConfig: + serverSecret: secret://genericZkConfig.serverSecret + +appConfig: + application: example + environment: example + configuration: example + +remoteConfig: + authorizedUsers: + - # 1st authorized user + - # 2nd authorized user + - # ... + - # Nth authorized user + requiredHostedDomain: example.com + audiences: + - # 1st audience + - # 2nd audience + - # ... + - # Nth audience + globalConfig: # keys and values that are given to clients on GET /v1/config + EXAMPLE_KEY: VALUE + +paymentsService: + userAuthenticationTokenSharedSecret: secret://paymentsService.userAuthenticationTokenSharedSecret + fixerApiKey: secret://paymentsService.fixerApiKey + coinMarketCapApiKey: secret://paymentsService.coinMarketCapApiKey + coinMarketCapCurrencyIds: + MOB: 7878 + paymentCurrencies: + # list of symbols for supported currencies + - MOB + +artService: + userAuthenticationTokenSharedSecret: secret://artService.userAuthenticationTokenSharedSecret + userAuthenticationTokenUserIdSecret: secret://artService.userAuthenticationTokenUserIdSecret + +badges: + badges: + - id: TEST + category: other + sprites: # exactly 6 + - sprite-1.png + - sprite-2.png + - sprite-3.png + - sprite-4.png + - sprite-5.png + - sprite-6.png + svg: example.svg + svgs: + - light: example-light.svg + dark: example-dark.svg + badgeIdsEnabledForAll: + - TEST + receiptLevels: + '1': TEST + +subscription: # configuration for Stripe subscriptions + badgeGracePeriod: P15D + levels: + 500: + badge: EXAMPLE + prices: + # list of ISO 4217 currency codes and amounts for the given badge level + xts: + amount: '10' + processorIds: + STRIPE: price_example # stripe Price ID + BRAINTREE: plan_example # braintree Plan ID + +oneTimeDonations: + sepaMaxTransactionSizeEuros: '10000' + boost: + level: 1 + expiration: P90D + badge: EXAMPLE + gift: + level: 10 + expiration: P90D + badge: EXAMPLE + currencies: + # ISO 4217 currency codes and amounts in those currencies + xts: + minimum: '0.5' + gift: '2' + boosts: + - '1' + - '2' + - '4' + - '8' + - '20' + - '40' + +registrationService: + host: registration.example.com + port: 443 + credentialConfigurationJson: | + { + "example": "example" + } + identityTokenAudience: https://registration.example.com + registrationCaCertificate: | # Registration service TLS certificate trust root + -----BEGIN CERTIFICATE----- + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + AAAAAAAAAAAAAAAAAAAA + -----END CERTIFICATE----- + +turn: + secret: secret://turn.secret + +commandStopListener: + path: /example/path + +linkDevice: + secret: secret://linkDevice.secret diff --git a/jdk_17_maven/em/embedded/rest/signal-server/src/main/resources/redis.conf b/jdk_17_maven/em/embedded/rest/signal-server/src/main/resources/redis.conf new file mode 100644 index 000000000..9e07f28c8 --- /dev/null +++ b/jdk_17_maven/em/embedded/rest/signal-server/src/main/resources/redis.conf @@ -0,0 +1,5 @@ +port 6379 +cluster-enabled yes +cluster-config-file nodes.conf +cluster-node-timeout 5000 +appendonly yes diff --git a/jdk_17_maven/em/embedded/rest/signal-server/src/main/resources/secrets.yml b/jdk_17_maven/em/embedded/rest/signal-server/src/main/resources/secrets.yml new file mode 100644 index 000000000..123704c88 --- /dev/null +++ b/jdk_17_maven/em/embedded/rest/signal-server/src/main/resources/secrets.yml @@ -0,0 +1,88 @@ +datadog.apiKey: unset + +stripe.apiKey: unset +stripe.idempotencyKeyGenerator: abcdefg12345678= # base64 for creating request idempotency hash + +braintree.privateKey: unset + +directoryV2.client.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth tokens for Signal users +directoryV2.client.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth identity tokens for Signal users + +svr2.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth tokens for Signal users +svr2.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth identity tokens for Signal users + +tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= + +awsAttachments.accessKey: test +awsAttachments.accessSecret: test + +gcpAttachments.rsaSigningKey: | + -----BEGIN PRIVATE KEY----- + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + AAAAAAAA + -----END PRIVATE KEY----- + +apn.teamId: team-id +apn.keyId: key-id +apn.signingKey: | + -----BEGIN PRIVATE KEY----- + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz + AAAAAAAA + -----END PRIVATE KEY----- + +fcm.credentials: | + { "json": true } + +cdn.accessKey: test # AWS Access Key ID +cdn.accessSecret: test # AWS Access Secret + +unidentifiedDelivery.certificate: ABCD1234 +unidentifiedDelivery.privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA + +hCaptcha.apiKey: unset + +storageService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + +zkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA== + +genericZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA== + +paymentsService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # base64-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users +paymentsService.fixerApiKey: unset +paymentsService.coinMarketCapApiKey: unset + +artService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # base64-encoded 32-byte secret not shared with any external service, but used in ArtController +artService.userAuthenticationTokenUserIdSecret: AAAAAAAAAAA= # base64-encoded secret to obscure user phone numbers from Sticker Creator + +currentReportingKey.secret: AAAAAAAAAAA= +currentReportingKey.salt: AAAAAAAAAAA= + +turn.secret: AAAAAAAAAAA= + +linkDevice.secret: AAAAAAAAAAA= diff --git a/jdk_17_maven/em/embedded/rest/tiltaksgjennomforing-api/pom.xml b/jdk_17_maven/em/embedded/rest/tiltaksgjennomforing-api/pom.xml new file mode 100644 index 000000000..ec9bbf763 --- /dev/null +++ b/jdk_17_maven/em/embedded/rest/tiltaksgjennomforing-api/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + org.evomaster + evomaster-benchmark-jdk17-em-embedded-rest + 3.0.1-SNAPSHOT + + + em-embedded-rest-tiltaksgjennomforing-api + jar + + + + + org.springframework.boot + spring-boot-dependencies + 2.7.14 + pom + import + + + + + + + no.nav.tag + tiltaksgjennomforing-api + 1.0.0-SNAPSHOT + + + org.testcontainers + testcontainers + 1.19.1 + compile + + + org.springframework + spring-context + + + + + diff --git a/jdk_17_maven/em/embedded/rest/tiltaksgjennomforing-api/src/main/java/em/embedded/rest/tiltaksgjennomforing/api/EmbeddedEvoMasterController.java b/jdk_17_maven/em/embedded/rest/tiltaksgjennomforing-api/src/main/java/em/embedded/rest/tiltaksgjennomforing/api/EmbeddedEvoMasterController.java new file mode 100644 index 000000000..3c7468e08 --- /dev/null +++ b/jdk_17_maven/em/embedded/rest/tiltaksgjennomforing-api/src/main/java/em/embedded/rest/tiltaksgjennomforing/api/EmbeddedEvoMasterController.java @@ -0,0 +1,174 @@ +package em.embedded.rest.tiltaksgjennomforing.api; + +import no.nav.tag.tiltaksgjennomforing.TiltaksgjennomforingApplication; +import org.evomaster.client.java.controller.EmbeddedSutController; +import org.evomaster.client.java.controller.InstrumentedSutStarter; +import org.evomaster.client.java.controller.api.dto.SutInfoDto; +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto; +import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType; +import org.evomaster.client.java.controller.problem.ProblemInfo; +import org.evomaster.client.java.controller.problem.RestProblem; +import org.evomaster.client.java.sql.DbCleaner; +import org.evomaster.client.java.sql.DbSpecification; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.jdbc.core.JdbcTemplate; +import org.testcontainers.containers.GenericContainer; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class EmbeddedEvoMasterController extends EmbeddedSutController { + + private static final String POSTGRES_VERSION = "13.13"; + + private static final String POSTGRES_PASSWORD = "password"; + + private static final int POSTGRES_PORT = 5432; + + private static final GenericContainer postgresContainer = new GenericContainer("postgres:" + POSTGRES_VERSION) + .withEnv("POSTGRES_PASSWORD", POSTGRES_PASSWORD) + .withEnv("POSTGRES_HOST_AUTH_METHOD", "trust") //to allow all connections without a password + .withEnv("POSTGRES_DB", "tiltaksgjennomforing") + .withTmpFs(Collections.singletonMap("/var/lib/postgresql/data", "rw")) + .withExposedPorts(POSTGRES_PORT); + + private ConfigurableApplicationContext ctx; + + private Connection sqlConnection; + private List dbSpecification; + + public EmbeddedEvoMasterController() { + this(40100); + } + + public EmbeddedEvoMasterController(int port) { + setControllerPort(port); + } + + public static void main(String[] args) { + int port = 40100; + if (args.length > 0) { + port = Integer.parseInt(args[0]); + } + + EmbeddedEvoMasterController controller = new EmbeddedEvoMasterController(port); + InstrumentedSutStarter starter = new InstrumentedSutStarter(controller); + + starter.start(); + } + + @Override + public boolean isSutRunning() { + return ctx!=null && ctx.isRunning(); + } + + @Override + public String getPackagePrefixesToCover() { + return "no.nav.tag.tiltaksgjennomforing."; + } + + @Override + public List getInfoForAuthentication() { + return null; + } + + @Override + public ProblemInfo getProblemInfo() { + return new RestProblem( + "http://localhost:" + getSutPort() + "/v3/api-docs", + null + ); + } + + @Override + public SutInfoDto.OutputFormat getPreferredOutputFormat() { + return SutInfoDto.OutputFormat.JAVA_JUNIT_5; + } + + @Override + public String startSut() { + postgresContainer.start(); + + String postgresURL = "jdbc:postgresql://" + postgresContainer.getHost() + ":" + postgresContainer.getMappedPort(POSTGRES_PORT) + "/tiltaksgjennomforing"; + + + //TODO should go through all the environment variables in application properties + System.setProperty("KAFKA_BROKERS","KAFKA_BROKERS"); + System.setProperty("KAFKA_TRUSTSTORE_PATH","KAFKA_TRUSTSTORE_PATH"); + System.setProperty("KAFKA_CREDSTORE_PASSWORD","KAFKA_CREDSTORE_PASSWORD"); + System.setProperty("KAFKA_KEYSTORE_PATH","KAFKA_KEYSTORE_PATH"); + System.setProperty("KAFKA_CREDSTORE_PASSWORD","KAFKA_CREDSTORE_PASSWORD"); + System.setProperty("KAFKA_SCHEMA_REGISTRY","KAFKA_SCHEMA_REGISTRY"); + System.setProperty("KAFKA_SCHEMA_REGISTRY_USER","KAFKA_SCHEMA_REGISTRY_USER"); + System.setProperty("KAFKA_SCHEMA_REGISTRY_PASSWORD","KAFKA_SCHEMA_REGISTRY_PASSWORD"); + System.setProperty("AZURE_APP_WELL_KNOWN_URL","http://not-existing-fake-url-FOO.com"); + System.setProperty("TOKEN_X_WELL_KNOWN_URL","http://not-existing-fake-url-BAR.com"); + System.setProperty("AZURE_APP_TENANT_ID","AZURE_APP_TENANT_ID"); + System.setProperty("AZURE_APP_CLIENT_ID","AZURE_APP_CLIENT_ID"); + System.setProperty("AZURE_APP_CLIENT_SECRET","AZURE_APP_CLIENT_SECRET"); + System.setProperty("beslutter.ad.gruppe","99ea78dc-db77-44d0-b193-c5dc22f01e1d"); + + ctx = SpringApplication.run(TiltaksgjennomforingApplication.class, new String[]{ + "--server.port=0", + "--spring.profiles.active=dev-fss", + "--management.server.port=-1", + "--server.ssl.enabled=false", + "--spring.datasource.url=" + postgresURL, + "--spring.datasource.username=postgres", + "--spring.datasource.password=" + POSTGRES_PASSWORD, + "--sentry.logging.enabled=false", + "--sentry.environment=local", + "--logging.level.root=OFF", + "--logging.config=classpath:logback-spring.xml", + "--logging.level.org.springframework=INFO", + }); + + + if (sqlConnection != null) { + try { + sqlConnection.close(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + try { + sqlConnection = DriverManager.getConnection(postgresURL, "postgres", POSTGRES_PASSWORD); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + + dbSpecification = Arrays.asList(new DbSpecification(DatabaseType.POSTGRES, sqlConnection)); + + return "http://localhost:" + getSutPort(); + } + + protected int getSutPort() { + return (Integer) ((Map) ctx.getEnvironment() + .getPropertySources().get("server.ports").getSource()) + .get("local.server.port"); + } + + @Override + public void stopSut() { + postgresContainer.stop(); + if(ctx != null) ctx.stop(); + } + + @Override + public void resetStateOfSUT() { + + } + + @Override + public List getDbSpecifications() { + return dbSpecification; + } +} diff --git a/jdk_17_maven/em/external/pom.xml b/jdk_17_maven/em/external/pom.xml index 73226e26b..fd2b36444 100644 --- a/jdk_17_maven/em/external/pom.xml +++ b/jdk_17_maven/em/external/pom.xml @@ -14,6 +14,17 @@ web grpc + rest + + + + + org.postgresql + postgresql + 42.1.4 + + + \ No newline at end of file diff --git a/jdk_17_maven/em/external/rest/familie-ba-sak/pom.xml b/jdk_17_maven/em/external/rest/familie-ba-sak/pom.xml new file mode 100644 index 000000000..8b27a9dc8 --- /dev/null +++ b/jdk_17_maven/em/external/rest/familie-ba-sak/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + org.evomaster + evomaster-benchmark-jdk17-em-external-rest + 3.0.1-SNAPSHOT + + + evomaster-benchmark-jdk17-em-external-rest-familie-ba-sak + jar + + + + org.testcontainers + testcontainers + compile + + + org.postgresql + postgresql + + + no.nav.security + mock-oauth2-server + 2.1.4 + + + com.fasterxml.jackson.module + jackson-module-kotlin + 2.17.0 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + familie-ba-sak-evomaster-runner + + + + em.external.familie.ba.sak.ExternalEvoMasterController + + org.evomaster.client.java.instrumentation.InstrumentingAgent + + org.evomaster.client.java.instrumentation.InstrumentingAgent + + true + true + + + + + + + + + + + + diff --git a/jdk_17_maven/em/external/rest/familie-ba-sak/src/main/java/em/external/familie/ba/sak/ExternalEvoMasterController.java b/jdk_17_maven/em/external/rest/familie-ba-sak/src/main/java/em/external/familie/ba/sak/ExternalEvoMasterController.java new file mode 100644 index 000000000..f0de7512d --- /dev/null +++ b/jdk_17_maven/em/external/rest/familie-ba-sak/src/main/java/em/external/familie/ba/sak/ExternalEvoMasterController.java @@ -0,0 +1,388 @@ +package em.external.familie.ba.sak; + +import com.nimbusds.jose.JOSEObjectType; +import no.nav.security.mock.oauth2.MockOAuth2Server; +import no.nav.security.mock.oauth2.OAuth2Config; +import no.nav.security.mock.oauth2.token.RequestMapping; +import no.nav.security.mock.oauth2.token.RequestMappingTokenCallback; +import org.evomaster.client.java.controller.ExternalSutController; +import org.evomaster.client.java.controller.InstrumentedSutStarter; +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto; +import org.evomaster.client.java.controller.api.dto.SutInfoDto; +import org.evomaster.client.java.controller.api.dto.auth.HttpVerb; +import org.evomaster.client.java.controller.api.dto.auth.LoginEndpointDto; +import org.evomaster.client.java.controller.api.dto.auth.TokenHandlingDto; +import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType; +import org.evomaster.client.java.controller.problem.RestProblem; +import org.evomaster.client.java.sql.DbCleaner; +import org.evomaster.client.java.sql.SqlScriptRunner; +import org.evomaster.client.java.sql.SqlScriptRunnerCached; +import org.evomaster.client.java.sql.DbSpecification; +import org.evomaster.client.java.controller.problem.ProblemInfo; +import org.testcontainers.containers.GenericContainer; + + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.*; + +public class ExternalEvoMasterController extends ExternalSutController { + + + public static void main(String[] args) { + + int controllerPort = 40100; + if (args.length > 0) { + controllerPort = Integer.parseInt(args[0]); + } + int sutPort = 12345; + if (args.length > 1) { + sutPort = Integer.parseInt(args[1]); + } + String jarLocation = "cs/rest/familie-ba-sak/target"; + if (args.length > 2) { + jarLocation = args[2]; + } + if(! jarLocation.endsWith(".jar")) { + jarLocation += "/familie-ba-sak-sut.jar"; + } + + int timeoutSeconds = 120; + if(args.length > 3){ + timeoutSeconds = Integer.parseInt(args[3]); + } + String command = "java"; + if(args.length > 4){ + command = args[4]; + } + + + ExternalEvoMasterController controller = + new ExternalEvoMasterController(controllerPort, jarLocation, + sutPort, timeoutSeconds, command); + InstrumentedSutStarter starter = new InstrumentedSutStarter(controller); + + starter.start(); + } + + private final int timeoutSeconds; + private final int sutPort; + private String jarLocation; + private Connection sqlConnection; + + private List dbSpecification; + + + private static final String POSTGRES_VERSION = "13.13"; + + private static final String POSTGRES_PASSWORD = "password"; + + private static final int POSTGRES_PORT = 5432; + + private static final GenericContainer postgres = new GenericContainer("postgres:" + POSTGRES_VERSION) + .withEnv("POSTGRES_PASSWORD", POSTGRES_PASSWORD) + .withEnv("POSTGRES_HOST_AUTH_METHOD", "trust") //to allow all connections without a password + .withEnv("POSTGRES_DB", "familiebasak") + .withTmpFs(Collections.singletonMap("/var/lib/postgresql/data", "rw")) + .withExposedPorts(POSTGRES_PORT); + + private MockOAuth2Server oAuth2Server; + + private int oAuth2Port; + + private final String ISSUER_ID = "azuread"; + + private final String DEFAULT_AUDIENCE = "some-audience"; + + private final String PROSESSERING_ROLLE = "928636f4-fd0d-4149-978e-a6fb68bb19de"; + + private final String TOKEN_PARAM = "name"; + + private static final String A0 = "TaskRunner"; + private static final String A1 = "Veileder"; + private static final String A2 = "Saksbehandler"; + private static final String A3 = "Beslutter"; + private static final String A4 = "Forvalter"; + private static final String A5 = "Kode6"; + private static final String A6 = "Kode7"; + private static final String A7 = "System"; + + private static final String veileder = "93a26831-9866-4410-927b-74ff51a9107c"; + private static final String saksbehandler = "d21e00a4-969d-4b28-8782-dc818abfae65"; + private static final String beslutter = "9449c153-5a1e-44a7-84c6-7cc7a8867233"; + private static final String forvalter = "c62e908a-cf20-4ad0-b7b3-3ff6ca4bf38b"; + private static final String kode6 = "5ef775f2-61f8-4283-bf3d-8d03f428aa14"; + private static final String kode7 = "ea930b6b-9397-44d9-b9e6-f4cf527a632a"; + + + + public ExternalEvoMasterController(){ + this(40100, "../core/target", 12345, 120, "java"); + } + + public ExternalEvoMasterController(String jarLocation) { + this(); + this.jarLocation = jarLocation; + } + + public ExternalEvoMasterController( + int controllerPort, String jarLocation, int sutPort, int timeoutSeconds, String command + ) { + + if(jarLocation==null || jarLocation.isEmpty()){ + throw new IllegalArgumentException("Missing jar location"); + } + + + this.sutPort = sutPort; + this.oAuth2Port = sutPort + 1; + this.jarLocation = jarLocation; + this.timeoutSeconds = timeoutSeconds; + setControllerPort(controllerPort); + setJavaCommand(command); + } + + + @Override + public String[] getInputParameters() { + + String wellKnownUrl = oAuth2Server.wellKnownUrl(ISSUER_ID).toString(); + + return new String[]{ + "--server.port=" + sutPort, + "--spring.profiles.active=dev", + "--management.server.port=-1", + "--server.ssl.enabled=false", + "--spring.datasource.url=" + dbUrl(), + "--spring.datasource.username=postgres", + "--spring.datasource.password=" + POSTGRES_PASSWORD, + "--sentry.logging.enabled=false", + "--sentry.environment=local", + //TODO check when dealing with Kafka + "--funksjonsbrytere.kafka.producer.enabled=false", + "--funksjonsbrytere.enabled=false", + "--logging.level.root=OFF", + "--logging.config=classpath:logback-spring.xml", + "--logging.level.org.springframework=INFO", + "--no.nav.security.jwt.issuer.azuread.discoveryurl="+wellKnownUrl, + "--prosessering.rolle=" + PROSESSERING_ROLLE, + "--FAMILIE_EF_SAK_API_URL=http://fake-familie-ef-sak/api", + "--FAMILIE_KLAGE_URL=http://fake-familie-klage", + "--FAMILIE_BREV_API_URL=http://fake-familie-brev", + "--FAMILIE_BA_INFOTRYGD_FEED_API_URL=http://fake-familie-ba-infotrygd-feed/api", + "--FAMILIE_BA_INFOTRYGD_API_URL=http://fake-familie-ba-infotrygd", + "--FAMILIE_TILBAKE_API_URL=http://fake-familie-tilbake/api", + "--PDL_URL=http://fake-pdl-api.default", + "--FAMILIE_INTEGRASJONER_API_URL=http://fake-familie-integrasjoner/api", + "--FAMILIE_OPPDRAG_API_URL=http://fake-familie-oppdrag/api", + "--SANITY_FAMILIE_API_URL=http://fake-xsrv1mh6.apicdn.sanity.io/v2021-06-07/data/query/ba-brev", + "--ECB_API_URL=http://fake-data-api.ecb.europa.eu/service/data/EXR/", + "--rolle.veileder=" + veileder, + "--rolle.saksbehandler=" + saksbehandler, + "--rolle.beslutter=" + beslutter, + "--rolle.forvalter=" + forvalter, + "--rolle.kode6=" + kode6, + "--rolle.kode7=" + kode7 + }; + } + + public String[] getJVMParameters() { + + return new String[]{ + "-DAZUREAD_TOKEN_ENDPOINT_URL=http://fake-azure-token-endpoint.no:8080", + "-DAZURE_OPENID_CONFIG_TOKEN_ENDPOINT=bar", + "-DAZURE_APP_CLIENT_ID=bar", + "-DNAIS_APP_NAME=bar", + "-DUNLEASH_SERVER_API_URL=http://fake-unleash-server-api.no:8080", + "-DUNLEASH_SERVER_API_TOKEN=bar", + "-DBA_SAK_CLIENT_ID="+DEFAULT_AUDIENCE + }; + } + + private String dbUrl() { + + String host = postgres.getContainerIpAddress(); + int port = postgres.getMappedPort(5432); + + + return "jdbc:postgresql://"+host+":"+port+"/familiebasak"; + } + + @Override + public String getBaseURL() { + return "http://localhost:" + sutPort; + } + + @Override + public String getPathToExecutableJar() { + return jarLocation; + } + + @Override + public String getLogMessageOfInitializedServer() { + return "Jetty started on port"; + } + + @Override + public long getMaxAwaitForInitializationInSeconds() { + return timeoutSeconds; + } + + @Override + public void preStart() { + postgres.start(); + oAuth2Server = new MockOAuth2Server(getOAuth2Config()); + oAuth2Server.start(oAuth2Port); + } + + @Override + public void postStart() { + closeDataBaseConnection(); + + try { + sqlConnection = DriverManager.getConnection(dbUrl(), "postgres", POSTGRES_PASSWORD); + dbSpecification = Arrays.asList(new DbSpecification(DatabaseType.POSTGRES,sqlConnection)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void resetStateOfSUT() { + } + + @Override + public void preStop() { + closeDataBaseConnection(); + } + + @Override + public void postStop() { + postgres.stop(); + if(oAuth2Server!=null) oAuth2Server.shutdown(); + } + + private void closeDataBaseConnection() { + if (sqlConnection != null) { + try { + sqlConnection.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + sqlConnection = null; + } + } + + @Override + public String getPackagePrefixesToCover() { + return "no.nav.familie.ba.sak."; + } + + @Override + public ProblemInfo getProblemInfo() { + return new RestProblem( + "http://localhost:" + sutPort + "/v3/api-docs", + null + ); + } + + @Override + public SutInfoDto.OutputFormat getPreferredOutputFormat() { + return SutInfoDto.OutputFormat.JAVA_JUNIT_5; + } + + + @Override + public List getInfoForAuthentication() { + + String url = oAuth2Server.baseUrl() + ISSUER_ID + "/token"; + + return Arrays.asList( + getAuthenticationDto(A0,url), + getAuthenticationDto(A1,url), + getAuthenticationDto(A2,url), + getAuthenticationDto(A3,url), + getAuthenticationDto(A4,url), + getAuthenticationDto(A5,url), + getAuthenticationDto(A6,url), + getAuthenticationDto(A7,url) + ); + } + + private RequestMapping getRequestMapping(String label, List groups, String id, String name) { + Map claims = new HashMap<>(); + claims.put("groups",groups); + claims.put("name",name); + claims.put("NAVident", id); + claims.put("sub","subject"); + claims.put("aud","some-audience"); + claims.put("tid",ISSUER_ID); + claims.put("azp",id); + + RequestMapping rm = new RequestMapping(TOKEN_PARAM,label,claims, JOSEObjectType.JWT.getType()); + + return rm; + } + + private OAuth2Config getOAuth2Config(){ + + List mappings = Arrays.asList( getRequestMapping(A0, Arrays.asList(PROSESSERING_ROLLE),"Z0042", "Task Runner"), + getRequestMapping(A1, Arrays.asList(veileder),"Z0000", "Mock McMockface"), + getRequestMapping(A2, Arrays.asList(saksbehandler),"Z0001", "Foo Bar"), + getRequestMapping(A3, Arrays.asList(beslutter),"Z0002", "John Smith"), + getRequestMapping(A4, Arrays.asList(forvalter),"Z0003", "Mario Rossi"), + getRequestMapping(A5, Arrays.asList(kode6),"Z0004", "Kode Six"), + getRequestMapping(A6, Arrays.asList(kode7),"Z0005", "Kode Seven"), + getRequestMapping(A7, Arrays.asList(),"VL", "The System") + ); + + RequestMappingTokenCallback callback = new RequestMappingTokenCallback( + ISSUER_ID, + mappings, + 360000 + ); + + Set callbacks = Set.of( + callback + ); + + OAuth2Config config = new OAuth2Config( + true, + null, + null, + false, + new no.nav.security.mock.oauth2.token.OAuth2TokenProvider(), + callbacks + ); + + return config; + } + + private AuthenticationDto getAuthenticationDto(String label, String oauth2Url){ + + AuthenticationDto dto = new AuthenticationDto(label); + LoginEndpointDto x = new LoginEndpointDto(); + dto.loginEndpointAuth = x; + + x.externalEndpointURL = oauth2Url; + x.payloadRaw = TOKEN_PARAM+"="+label+"&grant_type=client_credentials&code=foo&client_id=foo&client_secret=secret"; + x.verb = HttpVerb.POST; + x.contentType = "application/x-www-form-urlencoded"; + x.expectCookies = false; + + TokenHandlingDto token = new TokenHandlingDto(); + token.headerPrefix = "Bearer "; + token.httpHeaderName = "Authorization"; + token.extractFromField = "/access_token"; + x.token = token; + + return dto; + } + + + @Override + public List getDbSpecifications() { + return dbSpecification; + } +} diff --git a/jdk_17_maven/em/external/rest/pom.xml b/jdk_17_maven/em/external/rest/pom.xml new file mode 100644 index 000000000..625d03276 --- /dev/null +++ b/jdk_17_maven/em/external/rest/pom.xml @@ -0,0 +1,19 @@ + + 4.0.0 + + + org.evomaster + evomaster-benchmark-jdk17-em-external + 3.0.1-SNAPSHOT + + + evomaster-benchmark-jdk17-em-external-rest + pom + + + + familie-ba-sak + + + \ No newline at end of file diff --git a/jdk_17_maven/em/pom.xml b/jdk_17_maven/em/pom.xml index 3190d9d72..529dfc5da 100644 --- a/jdk_17_maven/em/pom.xml +++ b/jdk_17_maven/em/pom.xml @@ -47,6 +47,16 @@ + + + + org.testcontainers + testcontainers + 1.19.1 + test + + + diff --git a/openapi-swagger/familie-ba-sak.json b/openapi-swagger/familie-ba-sak.json new file mode 100644 index 000000000..16f007ca9 --- /dev/null +++ b/openapi-swagger/familie-ba-sak.json @@ -0,0 +1,14634 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost:22245", + "description": "Generated server url" + } + ], + "security": [ + { + "oauth2": [ + "read", + "write" + ] + } + ], + "paths": { + "/api/vilkaarsvurdering/{behandlingId}/{vilkaarId}": { + "put": { + "tags": [ + "vilk-år-controller" + ], + "operationId": "endreVilkår", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "vilkaarId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestPersonResultat" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + }, + "delete": { + "tags": [ + "vilk-år-controller" + ], + "operationId": "slettVilkårsperiode", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "vilkaarId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/vilkaarsvurdering/{behandlingId}/annenvurdering/{annenVurderingId}": { + "put": { + "tags": [ + "vilk-år-controller" + ], + "operationId": "endreAnnenVurdering", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "annenVurderingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestAnnenVurdering" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/vedtaksperioder/standardbegrunnelser/{vedtaksperiodeId}": { + "put": { + "tags": [ + "vedtaksperiode-med-begrunnelser-controller" + ], + "operationId": "oppdaterVedtaksperiodeStandardbegrunnelser", + "parameters": [ + { + "name": "vedtaksperiodeId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestPutVedtaksperiodeMedStandardbegrunnelser" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursListRestUtvidetVedtaksperiodeMedBegrunnelser" + } + } + } + } + } + } + }, + "/api/vedtaksperioder/fritekster/{vedtaksperiodeId}": { + "put": { + "tags": [ + "vedtaksperiode-med-begrunnelser-controller" + ], + "operationId": "oppdaterVedtaksperiodeMedFritekster", + "parameters": [ + { + "name": "vedtaksperiodeId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestPutVedtaksperiodeMedFritekster" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursListRestUtvidetVedtaksperiodeMedBegrunnelser" + } + } + } + } + } + } + }, + "/api/vedtaksperioder/endringstidspunkt": { + "put": { + "tags": [ + "vedtaksperiode-med-begrunnelser-controller" + ], + "operationId": "genererVedtaksperioderTilOgMedFørsteEndringstidspunkt", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestGenererVedtaksperioderForOverstyrtEndringstidspunkt" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/task/rekjor": { + "put": { + "tags": [ + "task-controller" + ], + "operationId": "rekjørTask", + "parameters": [ + { + "name": "taskId", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/task/rekjorAlle": { + "put": { + "tags": [ + "task-controller" + ], + "operationId": "rekjørTasks", + "parameters": [ + { + "name": "status", + "in": "header", + "required": true, + "schema": { + "type": "string", + "enum": [ + "FERDIG", + "FEILET", + "PLUKKET", + "BEHANDLER", + "KLAR_TIL_PLUKK", + "UBEHANDLET", + "AVVIKSHÅNDTERT", + "MANUELL_OPPFØLGING" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/task/kommenter": { + "put": { + "tags": [ + "task-controller" + ], + "operationId": "kommenterTask", + "parameters": [ + { + "name": "taskId", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KommentarDTO" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/task/avvikshaandter": { + "put": { + "tags": [ + "task-controller" + ], + "operationId": "avvikshåndterTask", + "parameters": [ + { + "name": "taskId", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AvvikshåndterDTO" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/sett-på-vent/{behandlingId}": { + "put": { + "tags": [ + "sett-p-å-vent-controller" + ], + "operationId": "oppdaterSettBehandlingPåVent", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestSettPåVent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + }, + "post": { + "tags": [ + "sett-p-å-vent-controller" + ], + "operationId": "settBehandlingPåVent", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestSettPåVent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/sett-på-vent/{behandlingId}/fortsettbehandling": { + "put": { + "tags": [ + "sett-p-å-vent-controller" + ], + "operationId": "gjenopptaBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/satsendring/{fagsakId}/kjor-satsendring-synkront": { + "put": { + "tags": [ + "satsendring-controller" + ], + "operationId": "utførSatsendringSynkrontPåFagsak", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursUnit" + } + } + } + } + } + } + }, + "/api/refusjon-eøs/behandlinger/{behandlingId}/perioder/{id}": { + "put": { + "tags": [ + "refusjon-e-øs-controller" + ], + "operationId": "oppdaterRefusjonEøsPeriode", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestRefusjonEøs" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + }, + "delete": { + "tags": [ + "refusjon-e-øs-controller" + ], + "operationId": "fjernRefusjonEøsPeriode", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/preprod/{behandlingId}/fyll-ut-vilkarsvurdering": { + "put": { + "tags": [ + "preprod-controller" + ], + "operationId": "settFomPåTommeVilkårTilFødselsdato", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/kompetanse/{behandlingId}": { + "put": { + "tags": [ + "kompetanse-controller" + ], + "operationId": "oppdaterKompetanse", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestKompetanse" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/feilutbetalt-valuta/behandling/{behandlingId}/periode/{id}": { + "put": { + "tags": [ + "feilutbetalt-valuta-controller" + ], + "operationId": "oppdaterFeilutbetaltValutaPeriode", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestFeilutbetaltValuta" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + }, + "delete": { + "tags": [ + "feilutbetalt-valuta-controller" + ], + "operationId": "fjernFeilutbetaltValutaPeriode", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/endretutbetalingandel/{behandlingId}/{endretUtbetalingAndelId}": { + "put": { + "tags": [ + "endret-utbetaling-andel-controller" + ], + "operationId": "oppdaterEndretUtbetalingAndelOgOppdaterTilkjentYtelse", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "endretUtbetalingAndelId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestEndretUtbetalingAndel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + }, + "delete": { + "tags": [ + "endret-utbetaling-andel-controller" + ], + "operationId": "fjernEndretUtbetalingAndelOgOppdaterTilkjentYtelse", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "endretUtbetalingAndelId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/differanseberegning/valutakurs/{behandlingId}": { + "put": { + "tags": [ + "valutakurs-controller" + ], + "operationId": "oppdaterValutakurs", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestValutakurs" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/differanseberegning/utenlandskperidebeløp/{behandlingId}": { + "put": { + "tags": [ + "utenlandsk-periodebel-øp-controller" + ], + "operationId": "oppdaterUtenlandskPeriodebeløp", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestUtenlandskPeriodebeløp" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/behandlinger": { + "put": { + "tags": [ + "behandling-controller" + ], + "operationId": "opprettEllerOppdaterBehandlingFraHendelse", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NyBehandlingHendelse" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + }, + "post": { + "tags": [ + "behandling-controller" + ], + "operationId": "opprettBehandling", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NyBehandling" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/steg/henlegg": { + "put": { + "tags": [ + "behandling-steg-controller" + ], + "operationId": "henleggBehandlingOgSendBrev", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestHenleggBehandlingInfo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/behandlingstema": { + "put": { + "tags": [ + "behandling-controller" + ], + "operationId": "endreBehandlingstema", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestEndreBehandlingstema" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/arbeidsfordeling/{behandlingId}": { + "put": { + "tags": [ + "arbeidsfordeling-controller" + ], + "operationId": "endreBehandlendeEnhet", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestEndreBehandlendeEnhet" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/testverktoy/vedtak-om-overgangsstønad": { + "post": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "mottaHendelseOmVedtakOmOvergangsstønad", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonIdent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/internal/vedtak-om-overgangsstønad": { + "post": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "mottaHendelseOmVedtakOmOvergangsstønad_1", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonIdent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/testverktoy/foedselshendelse": { + "post": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "mottaFødselshendelse", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NyBehandlingHendelse" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/internal/foedselshendelse": { + "post": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "mottaFødselshendelse_1", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NyBehandlingHendelse" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/vilkaarsvurdering/{behandlingId}": { + "post": { + "tags": [ + "vilk-år-controller" + ], + "operationId": "nyttVilkår", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestNyttVilkår" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/tilgang": { + "post": { + "tags": [ + "tilgang-controller" + ], + "operationId": "hentTilgangOgDiskresjonskode", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TilgangRequestDTO" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursTilgangDTO" + } + } + } + } + } + } + }, + "/api/tilbakekreving/{behandlingId}/forhandsvis-varselbrev": { + "post": { + "tags": [ + "tilbakekreving-controller" + ], + "operationId": "hentForhåndsvisningVarselbrev", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForhåndsvisTilbakekrevingsvarselbrevRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursByte[]" + } + } + } + } + } + } + }, + "/api/stonadsstatistikk/vedtakV2": { + "post": { + "tags": [ + "st-ønadsstatistikk-controller" + ], + "operationId": "hentVedtakDvhV2", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VedtakDVHV2" + } + } + } + } + } + } + } + }, + "/api/stonadsstatistikk/send-til-dvh": { + "post": { + "tags": [ + "st-ønadsstatistikk-controller" + ], + "operationId": "sendTilStønadsstatistikk", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/stonadsstatistikk/send-til-dvh-manuell": { + "post": { + "tags": [ + "st-ønadsstatistikk-controller" + ], + "operationId": "sendTilStønadsstatistikkManuell", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/stonadsstatistikk/ettersend-manuell-migrering/{dryRun}": { + "post": { + "tags": [ + "st-ønadsstatistikk-controller" + ], + "operationId": "ettersendManuellMigrereringer", + "parameters": [ + { + "name": "dryRun", + "in": "path", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/småbarnstilleggkorrigering/behandling/{behandlingId}": { + "post": { + "tags": [ + "sm-åbarnstillegg-controller" + ], + "operationId": "leggTilSmåBarnstilleggPåBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SmåbarnstilleggKorrigeringRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + }, + "delete": { + "tags": [ + "sm-åbarnstillegg-controller" + ], + "operationId": "fjernSmåbarnstilleggFraMåned", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SmåbarnstilleggKorrigeringRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/skatt/perioder": { + "post": { + "tags": [ + "skatteetaten-controller" + ], + "operationId": "hentPerioderMedUtvidetBarnetrygd", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SkatteetatenPerioderRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json;charset=UTF-8": { + "schema": { + "$ref": "#/components/schemas/RessursSkatteetatenPerioderResponse" + } + } + } + } + } + } + }, + "/api/skatt/perioder/test": { + "post": { + "tags": [ + "skatteetaten-controller" + ], + "operationId": "hentPerioderMedUtvidetBarnetrygdForMidlertidigTest", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SkatteetatenPerioderRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json;charset=UTF-8": { + "schema": { + "$ref": "#/components/schemas/RessursSkatteetatenPerioderResponse" + } + } + } + } + } + } + }, + "/api/satsendring/saker-uten-sats": { + "post": { + "tags": [ + "satsendring-controller" + ], + "operationId": "finnSakerUtenSisteSats", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PairStringString" + } + } + } + } + } + } + }, + "/api/satsendring/kjorsatsendring": { + "post": { + "tags": [ + "satsendring-controller" + ], + "operationId": "utførSatsendringITaskPåFagsaker", + "requestBody": { + "content": { + "application/json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/satsendring/kjorsatsendringForListeMedIdenter": { + "post": { + "tags": [ + "satsendring-controller" + ], + "operationId": "utførSatsendringPåListeIdenter", + "requestBody": { + "content": { + "application/json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/satsendring/henleggBehandlingerMedLangFristSenereEnn/{valideringsdato}": { + "post": { + "tags": [ + "satsendring-controller" + ], + "operationId": "henleggBehandlingerMedLangLiggetid", + "parameters": [ + { + "name": "valideringsdato", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/samhandler/navn": { + "post": { + "tags": [ + "samhandler-controller" + ], + "operationId": "søkSamhandlerinfoFraNavn", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SøkSamhandlerInfoRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursListSamhandlerInfo" + } + } + } + } + } + } + }, + "/api/saksstatistikk/registrer-sendt-fra-statistikk": { + "post": { + "tags": [ + "saksstatistikk-controller" + ], + "description": "Oppdaterer saksstatistikk mellomlagring om at en melding har blitt sendt. Setter sendtTidspunkt slik at melding ikke blir sendt på nytt.", + "operationId": "registrerSendtFraStatistikk", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaksstatistikkSendtRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SaksstatistikkMellomlagring" + } + } + } + } + } + } + }, + "/api/refusjon-eøs/behandlinger/{behandlingId}": { + "get": { + "tags": [ + "refusjon-e-øs-controller" + ], + "operationId": "hentRefusjonEøsPerioder", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursListRestRefusjonEøs" + } + } + } + } + } + }, + "post": { + "tags": [ + "refusjon-e-øs-controller" + ], + "operationId": "leggTilRefusjonEøsPeriode", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestRefusjonEøs" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/person/registrer-manuell-dodsfall/{behandlingId}": { + "post": { + "tags": [ + "person-controller" + ], + "operationId": "registrerManuellDødsfallPåPerson", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestManuellDødsfall" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/overgangsstonad": { + "post": { + "tags": [ + "vedtak-om-overgangsst-ønad-controller" + ], + "operationId": "håndterVedtakOmOvergangsstønad", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Personident" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/oppgave/{oppgaveId}/tilbakestill": { + "post": { + "tags": [ + "oppgave-controller" + ], + "operationId": "tilbakestillFordelingPåOppgave", + "parameters": [ + { + "name": "oppgaveId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursOppgave" + } + } + } + } + } + } + }, + "/api/oppgave/{oppgaveId}/fordel": { + "post": { + "tags": [ + "oppgave-controller" + ], + "operationId": "fordelOppgave", + "parameters": [ + { + "name": "oppgaveId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "saksbehandler", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/oppgave/{oppgaveId}/ferdigstillOgKnyttjournalpost": { + "post": { + "tags": [ + "oppgave-controller" + ], + "operationId": "ferdigstillOppgaveOgKnyttJournalpostTilBehandling", + "parameters": [ + { + "name": "oppgaveId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestFerdigstillOppgaveKnyttJournalpost" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/oppgave/hent-oppgaver": { + "post": { + "tags": [ + "oppgave-controller" + ], + "operationId": "hentOppgaver", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestFinnOppgaveRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursFinnOppgaveResponseDto" + } + } + } + } + } + } + }, + "/api/oppgave/hent-frister-for-apne-utvidet-barnetrygd-behandlinger": { + "post": { + "tags": [ + "oppgave-controller" + ], + "operationId": "hentFristerForÅpneUtvidetBarnetrygdBehandlinger", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/oppgave/fjern-behandles-av-applikasjon": { + "post": { + "tags": [ + "oppgave-controller" + ], + "operationId": "fjernBehandlesAvApplikasjonFor", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/korrigertvedtak/behandling/{behandlingId}": { + "post": { + "tags": [ + "korrigert-vedtak-controller" + ], + "operationId": "opprettKorrigertVedtakPåBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KorrigerVedtakRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + }, + "patch": { + "tags": [ + "korrigert-vedtak-controller" + ], + "operationId": "settKorrigertVedtakTilInaktivPåBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/korrigertetterbetaling/behandling/{behandlingId}": { + "get": { + "tags": [ + "korrigert-etterbetaling-controller" + ], + "operationId": "hentAlleKorrigerteEtterbetalingPåBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursListRestKorrigertEtterbetaling" + } + } + } + } + } + }, + "post": { + "tags": [ + "korrigert-etterbetaling-controller" + ], + "operationId": "opprettKorrigertEtterbetalingPåBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KorrigertEtterbetalingRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + }, + "patch": { + "tags": [ + "korrigert-etterbetaling-controller" + ], + "operationId": "settKorrigertEtterbetalingTilInaktivPåBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/konsistensavstemming/run": { + "post": { + "tags": [ + "konsistensavstemming-controller" + ], + "operationId": "kjørKonsistensavstemming", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartKonsistensavstemming" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/konsistensavstemming/dryrun": { + "post": { + "tags": [ + "konsistensavstemming-controller" + ], + "operationId": "kjørKonsistensavstemmingUtenSendingTilØkonomi", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/klage/fagsaker/{fagsakId}/opprett-revurdering-klage/": { + "post": { + "tags": [ + "ekstern-klage-controller" + ], + "operationId": "opprettRevurderingKlage", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursOpprettRevurderingResponse" + } + } + } + } + } + } + }, + "/api/journalpost/{journalpostId}/journalfør/{oppgaveId}": { + "post": { + "tags": [ + "journalf-øring-controller" + ], + "operationId": "journalførV2", + "parameters": [ + { + "name": "journalpostId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "oppgaveId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "journalfoerendeEnhet", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestJournalføring" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/journalpost/for-bruker": { + "post": { + "tags": [ + "journalf-øring-controller" + ], + "operationId": "hentJournalposterForBruker", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonIdent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursListJournalpost" + } + } + } + } + } + } + }, + "/api/infotrygd/hent-infotrygdstonader-for-soker": { + "post": { + "tags": [ + "infotrygd-controller" + ], + "operationId": "hentInfotrygdstønaderForSøker", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Personident" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestInfotrygdstønader" + } + } + } + } + } + } + }, + "/api/infotrygd/hent-infotrygdsaker-for-soker": { + "post": { + "tags": [ + "infotrygd-controller" + ], + "operationId": "hentInfotrygdsakerForSøker", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Personident" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestInfotrygdsaker" + } + } + } + } + } + } + }, + "/api/infotrygd/har-lopende-sak": { + "post": { + "tags": [ + "infotrygd-controller" + ], + "operationId": "harLøpendeSak", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Personident" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestLøpendeSak" + } + } + } + } + } + } + }, + "/api/ident": { + "post": { + "tags": [ + "ident-controller" + ], + "operationId": "håndterPdlHendelse", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonIdent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/forvalter/start-manuell-restart-av-smaabarnstillegg-jobb/skalOppretteOppgaver/{skalOppretteOppgaver}": { + "post": { + "tags": [ + "forvalter-controller" + ], + "operationId": "triggManuellStartAvSmåbarnstillegg", + "parameters": [ + { + "name": "skalOppretteOppgaver", + "in": "path", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/forvalter/sjekkOmTilkjentYtelseForBehandlingHarUkorrektOpphørsdato": { + "post": { + "tags": [ + "forvalter-controller" + ], + "operationId": "sjekkOmTilkjentYtelseForBehandlingHarUkorrektOpphørsdato", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BehandlingerMedFeilIUtbetalingsoppdrag" + } + } + } + } + } + } + }, + "/api/forvalter/sendKorrigertUtbetalingsoppdragForBehandlinger": { + "post": { + "tags": [ + "forvalter-controller" + ], + "operationId": "sendKorrigertUtbetalingsoppdragForBehandlinger", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SendUtbetalingsoppdragPåNyttResponse" + } + } + } + } + } + } + }, + "/api/forvalter/sendKorrigertUtbetalingsoppdragForBehandling/{behandlingId}/{versjon}": { + "post": { + "tags": [ + "forvalter-controller" + ], + "operationId": "sendKorrigertUtbetalingsoppdragForBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "versjon", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SendUtbetalingsoppdragPåNyttResponse" + } + } + } + } + } + } + }, + "/api/forvalter/populer-stonad-fom-tom/{behandlingId}": { + "post": { + "tags": [ + "forvalter-controller" + ], + "operationId": "populerStønadFomTomForBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "boolean" + } + } + } + } + } + } + }, + "/api/forvalter/populer-stonad-fom-tom-alle/{limit}": { + "post": { + "tags": [ + "forvalter-controller" + ], + "operationId": "populerStønadFomTom", + "parameters": [ + { + "name": "limit", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/forvalter/oppdaterLøpendeStatusPåFagsaker": { + "post": { + "tags": [ + "forvalter-controller" + ], + "operationId": "oppdaterLøpendeStatusPåFagsaker", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/forvalter/lag-og-send-utbetalingsoppdrag-til-økonomi": { + "post": { + "tags": [ + "forvalter-controller" + ], + "operationId": "lagOgSendUtbetalingsoppdragTilØkonomi", + "requestBody": { + "content": { + "application/json": { + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/forvalter/kjor-satsendring-uten-validering": { + "post": { + "tags": [ + "forvalter-controller" + ], + "operationId": "kjørSatsendringFor", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/forvalter/identifiser-utbetalinger-over-100-prosent": { + "post": { + "tags": [ + "forvalter-controller" + ], + "operationId": "identifiserUtbetalingerOver100Prosent", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PairStringString" + } + } + } + } + } + } + }, + "/api/forvalter/finnBehandlingerMedPotensieltFeilUtbetalingsoppdrag": { + "post": { + "tags": [ + "forvalter-controller" + ], + "operationId": "identifiserBehandlingerSomKanKrevePatching", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BehandlingerMedFeilIUtbetalingsoppdrag" + } + } + } + } + } + } + }, + "/api/forvalter/ferdigstill-oppgaver": { + "post": { + "tags": [ + "forvalter-controller" + ], + "operationId": "ferdigstillListeMedOppgaver", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/feilutbetalt-valuta/behandling/{behandlingId}": { + "get": { + "tags": [ + "feilutbetalt-valuta-controller" + ], + "operationId": "hentFeilutbetaltValutaPerioder", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursListRestFeilutbetaltValuta" + } + } + } + } + } + }, + "post": { + "tags": [ + "feilutbetalt-valuta-controller" + ], + "operationId": "leggTilFeilutbetaltValutaPeriode", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestFeilutbetaltValuta" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/feature": { + "post": { + "tags": [ + "feature-toggle-controller" + ], + "operationId": "hentToggles", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursMapStringBoolean" + } + } + } + } + } + } + }, + "/api/fagsaker": { + "post": { + "tags": [ + "fagsak-controller" + ], + "operationId": "hentEllerOpprettFagsak", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FagsakRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestMinimalFagsak" + } + } + } + } + } + } + }, + "/api/fagsaker/{fagsakId}/opprett-klagebehandling": { + "post": { + "tags": [ + "klage-controller" + ], + "operationId": "opprettKlage", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OpprettKlageDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursLong" + } + } + } + } + } + } + }, + "/api/fagsaker/sok": { + "post": { + "tags": [ + "fagsak-controller" + ], + "operationId": "søkFagsak", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestSøkParam" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursListRestFagsakDeltager" + } + } + } + } + } + } + }, + "/api/fagsaker/sok/fagsaker-hvor-person-mottar-lopende-ytelse": { + "post": { + "tags": [ + "fagsak-controller" + ], + "operationId": "søkFagsakerHvorPersonMottarLøpendeYtelse", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestSøkFagsakRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursListRestFagsakIdOgTilknyttetAktørId" + } + } + } + } + } + } + }, + "/api/fagsaker/sok/fagsaker-hvor-person-er-deltaker": { + "post": { + "tags": [ + "fagsak-controller" + ], + "operationId": "søkFagsakerHvorPersonErSøkerEllerMottarOrdinærBarnetrygd", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestSøkFagsakRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursListRestFagsakIdOgTilknyttetAktørId" + } + } + } + } + } + } + }, + "/api/fagsaker/sok/fagsakdeltagere": { + "post": { + "tags": [ + "fagsak-controller" + ], + "operationId": "oppgiFagsakdeltagere", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestSøkParam" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursListRestFagsakDeltager" + } + } + } + } + } + } + }, + "/api/fagsaker/hent-fagsaker-paa-person": { + "post": { + "tags": [ + "fagsak-controller" + ], + "operationId": "hentMinimalFagsakerForPerson", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestHentFagsakerForPerson" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursListRestMinimalFagsak" + } + } + } + } + } + } + }, + "/api/fagsaker/hent-fagsak-paa-person": { + "post": { + "tags": [ + "fagsak-controller" + ], + "operationId": "hentMinimalFagsakForPerson", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestHentFagsakForPerson" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestMinimalFagsak" + } + } + } + } + } + } + }, + "/api/endretutbetalingandel/{behandlingId}": { + "post": { + "tags": [ + "endret-utbetaling-andel-controller" + ], + "operationId": "lagreEndretUtbetalingAndelOgOppdaterTilkjentYtelse", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/endretutbetalingandel/{behandlingId}/tilbakestill": { + "post": { + "tags": [ + "endret-utbetaling-andel-controller" + ], + "operationId": "tilbakestillBehandlingTilBehandlingsresultat", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/ekstern/pensjon/hent-barnetrygd": { + "post": { + "tags": [ + "pensjon-controller" + ], + "description": "Tjeneste for Pensjon for å hente barnetrygd og relaterte saker for en gitt person.", + "operationId": "hentBarnetrygd", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BarnetrygdTilPensjonRequest" + } + } + }, + "required": true + }, + "responses": { + "500": { + "description": "Uventet feil", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Ressurs" + } + } + } + } + }, + "400": { + "description": "Ugyldig input. fraDato maks tilbake 2 år" + }, + "200": { + "description": "Liste over fagsaker og relaterte fagsaker(hvis barna finnes i flere fagsaker) fra ba-sak \n \n fagsakId: unik id for fagsaken\n fagsakEiersIdent: Fnr for eier av fagsaken\n barnetrygdPerioder: Liste over perioder med barnetrygd\n \n ", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BarnetrygdTilPensjon" + } + } + } + } + } + } + } + }, + "/api/dokument/vedtaksbrev/{vedtakId}": { + "get": { + "tags": [ + "dokument-controller" + ], + "operationId": "hentVedtaksbrev", + "parameters": [ + { + "name": "vedtakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursByte[]" + } + } + } + } + } + }, + "post": { + "tags": [ + "dokument-controller" + ], + "operationId": "genererVedtaksbrev", + "parameters": [ + { + "name": "vedtakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursByte[]" + } + } + } + } + } + } + }, + "/api/dokument/send-brev/{behandlingId}": { + "post": { + "tags": [ + "dokument-controller" + ], + "operationId": "sendBrev", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManueltBrevRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/dokument/forhaandsvis-brev/{behandlingId}": { + "post": { + "tags": [ + "dokument-controller" + ], + "operationId": "hentForhåndsvisning", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManueltBrevRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursByte[]" + } + } + } + } + } + } + }, + "/api/dokument/fagsak/{fagsakId}/send-brev": { + "post": { + "tags": [ + "dokument-controller" + ], + "operationId": "sendBrevPåFagsak", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManueltBrevRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestMinimalFagsak" + } + } + } + } + } + } + }, + "/api/dokument/fagsak/{fagsakId}/forhaandsvis-brev": { + "post": { + "tags": [ + "dokument-controller" + ], + "operationId": "hentForhåndsvisningPåFagsak", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManueltBrevRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursByte[]" + } + } + } + } + } + } + }, + "/api/brevmottaker/{behandlingId}": { + "get": { + "tags": [ + "brevmottaker-controller" + ], + "operationId": "hentBrevmottakere", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursListRestBrevmottaker" + } + } + } + } + } + }, + "post": { + "tags": [ + "brevmottaker-controller" + ], + "operationId": "leggTilBrevmottaker", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestBrevmottaker" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/bisys/hent-utvidet-barnetrygd": { + "post": { + "tags": [ + "bisys-controller" + ], + "description": "Tjeneste for BISYS for å hente utvidet barnetrygd og småbarnstillegg for en gitt person.", + "operationId": "hentUtvidetBarnetrygd", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BisysUtvidetBarnetrygdRequest" + } + } + }, + "required": true + }, + "responses": { + "500": { + "description": "Uventet feil" + }, + "400": { + "description": "Ugyldig input. fraDato maks tilbake 5 år" + }, + "200": { + "description": "Liste over perioder som brukeren har hatt innvilget ytelse. Returnerer både fra Infotrygd og BA-SAK \n \n stønadstype: Hva slags type stønad. UTVIDET eller SMÅBARNSTILLEGG\n fomMåned: Første måned i perioden\n tomMåned: Den siste måneden i perioden. Hvis null, så er stønaden løpende\n beløp: utbetalingsbeløp\n manueltBeregnet: Beløpet er manuelt beregnet og kan inneholde andre stønader som barnetrygd\n \n ", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UtvidetBarnetrygdPeriode" + } + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/steg/vilkårsvurdering": { + "post": { + "tags": [ + "behandling-steg-controller" + ], + "operationId": "validerVilkårsvurdering", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/steg/tilbakekreving": { + "post": { + "tags": [ + "behandling-steg-controller" + ], + "operationId": "lagreTilbakekrevingOgGåVidereTilNesteSteg", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestTilbakekreving" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/steg/send-til-beslutter": { + "post": { + "tags": [ + "behandling-steg-controller" + ], + "operationId": "sendTilBeslutter", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "behandlendeEnhet", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/steg/registrer-søknad": { + "post": { + "tags": [ + "behandling-steg-controller" + ], + "operationId": "registrereSøknadOgHentPersongrunnlag", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestRegistrerSøknad" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/steg/registrer-institusjon-og-verge": { + "post": { + "tags": [ + "behandling-steg-controller" + ], + "operationId": "registerInstitusjonOgVerge", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestRegistrerInstitusjonOgVerge" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/steg/iverksett-vedtak": { + "post": { + "tags": [ + "behandling-steg-controller" + ], + "operationId": "iverksettVedtak", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestBeslutningPåVedtak" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/steg/behandlingsresultat": { + "post": { + "tags": [ + "behandling-steg-controller" + ], + "operationId": "utledBehandlingsresultat", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/legg-til-barn": { + "post": { + "tags": [ + "grunnlag-controller" + ], + "operationId": "leggTilBarnIPersonopplysningsgrunnlag", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeggTilBarnDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/internal/test-satsendring/{fagsakId}": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "utførSatsendringPåFagsak", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/testverktoy/test-satsendring/{fagsakId}": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "utførSatsendringPåFagsak_1", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/internal/ta-behandlinger-etter-ventefrist-av-vent": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "taBehandlingerEtterVentefristAvVent", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/testverktoy/ta-behandlinger-etter-ventefrist-av-vent": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "taBehandlingerEtterVentefristAvVent_1", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/testverktoy/redirect/behandling/{behandlingId}": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "redirectTilBarnetrygd", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/internal/redirect/behandling/{behandlingId}": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "redirectTilBarnetrygd_1", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/testverktoy/kjor-intern-konsistensavstemming/{maksAntallTasker}": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "kjørInternKonsistensavstemming", + "parameters": [ + { + "name": "maksAntallTasker", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/internal/kjor-intern-konsistensavstemming/{maksAntallTasker}": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "kjørInternKonsistensavstemming_1", + "parameters": [ + { + "name": "maksAntallTasker", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/testverktoy/hent-simulering-pa-behandling/{behandlingId}": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "hentSimuleringPåBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ØkonomiSimuleringMottaker" + } + } + } + } + } + } + } + }, + "/internal/hent-simulering-pa-behandling/{behandlingId}": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "hentSimuleringPåBehandling_1", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ØkonomiSimuleringMottaker" + } + } + } + } + } + } + } + }, + "/testverktoy/behandling/{behandlingId}/vedtaksperiodertest": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "hentVedtaksperioderTestPåBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/internal/behandling/{behandlingId}/vedtaksperiodertest": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "hentVedtaksperioderTestPåBehandling_1", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/testverktoy/behandling/{behandlingId}/begrunnelsetest": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "hentBegrunnelsetestPåBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/internal/behandling/{behandlingId}/begrunnelsetest": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "hentBegrunnelsetestPåBehandling_1", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/testverktoy/autobrev": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "kjørSchedulerForAutobrev", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/internal/autobrev": { + "get": { + "tags": [ + "test-verkt-øy-controller" + ], + "operationId": "kjørSchedulerForAutobrev_1", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/vilkaarsvurdering/vilkaarsbegrunnelser": { + "get": { + "tags": [ + "vilk-år-controller" + ], + "operationId": "hentTeksterForVilkårsbegrunnelser", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursMapVedtakBegrunnelseTypeListRestVedtakBegrunnelseTilknyttetVilkår" + } + } + } + } + } + } + }, + "/api/vedtaksperioder/brevbegrunnelser/{vedtaksperiodeId}": { + "get": { + "tags": [ + "vedtaksperiode-med-begrunnelser-controller" + ], + "operationId": "genererBrevBegrunnelserForPeriode", + "parameters": [ + { + "name": "vedtaksperiodeId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursSetString" + } + } + } + } + } + } + }, + "/api/vedtaksperioder/behandling/{behandlingId}/hent-vedtaksperioder": { + "get": { + "tags": [ + "vedtaksperiode-med-begrunnelser-controller" + ], + "operationId": "hentRestUtvidetVedtaksperiodeMedBegrunnelser", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursListRestUtvidetVedtaksperiodeMedBegrunnelser" + } + } + } + } + } + } + }, + "/api/tidslinjer/{behandlingId}": { + "get": { + "tags": [ + "tidslinje-controller" + ], + "operationId": "hentTidslinjer", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestTidslinjer" + } + } + } + } + } + } + }, + "/api/task/{id}": { + "get": { + "tags": [ + "task-controller" + ], + "operationId": "taskMedId", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursTaskDto" + } + } + } + } + } + } + }, + "/api/task/v2": { + "get": { + "tags": [ + "task-controller" + ], + "operationId": "task2", + "parameters": [ + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "FERDIG", + "FEILET", + "PLUKKET", + "BEHANDLER", + "KLAR_TIL_PLUKK", + "UBEHANDLET", + "AVVIKSHÅNDTERT", + "MANUELL_OPPFØLGING" + ] + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursPaginableResponseTaskDto" + } + } + } + } + } + } + }, + "/api/task/logg/{id}": { + "get": { + "tags": [ + "task-controller" + ], + "operationId": "tasklogg", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursListTaskloggDto" + } + } + } + } + } + } + }, + "/api/task/ferdigNaaFeiletFoer": { + "get": { + "tags": [ + "task-controller" + ], + "operationId": "tasksSomErFerdigNåMenFeiletFør", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursPaginableResponseTaskDto" + } + } + } + } + } + } + }, + "/api/task/callId/{callId}": { + "get": { + "tags": [ + "task-controller" + ], + "operationId": "tasksForCallId", + "parameters": [ + { + "name": "callId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursPaginableResponseTaskDto" + } + } + } + } + } + } + }, + "/api/task/antall-til-oppfolging": { + "get": { + "tags": [ + "task-controller" + ], + "operationId": "antallTilOppfølging", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursLong" + } + } + } + } + } + } + }, + "/api/skatt/personer": { + "get": { + "tags": [ + "skatteetaten-controller" + ], + "operationId": "finnPersonerMedUtvidetBarnetrygd", + "parameters": [ + { + "name": "aar", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json;charset=UTF-8": { + "schema": { + "$ref": "#/components/schemas/RessursSkatteetatenPersonerResponse" + } + } + } + } + } + } + }, + "/api/skatt/personer/test": { + "get": { + "tags": [ + "skatteetaten-controller" + ], + "operationId": "finnPersonerMedUtvidetBarnetrygdTest", + "parameters": [ + { + "name": "aar", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json;charset=UTF-8": { + "schema": { + "$ref": "#/components/schemas/RessursSkatteetatenPersonerResponse" + } + } + } + } + } + } + }, + "/api/satsendring/{fagsakId}/kan-kjore-satsendring": { + "get": { + "tags": [ + "satsendring-controller" + ], + "operationId": "kanKjøreSatsendringPåFagsak", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursBoolean" + } + } + } + } + } + } + }, + "/api/satsendring/kjorsatsendring/{fagsakId}": { + "get": { + "tags": [ + "satsendring-controller" + ], + "operationId": "utførSatsendringITaskPåFagsak", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/samhandler/orgnr/{orgnr}": { + "get": { + "tags": [ + "samhandler-controller" + ], + "operationId": "hentSamhandlerDataForOrganisasjon", + "parameters": [ + { + "name": "orgnr", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursSamhandlerInfo" + } + } + } + } + } + } + }, + "/api/saksstatistikk/sak/{fagsakId}": { + "get": { + "tags": [ + "saksstatistikk-controller" + ], + "operationId": "hentSakDvh", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SakDVH" + } + } + } + } + } + } + }, + "/api/saksstatistikk/behandling/{behandlingId}": { + "get": { + "tags": [ + "saksstatistikk-controller" + ], + "operationId": "hentBehandlingDvh", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BehandlingDVH" + } + } + } + } + } + } + }, + "/api/person": { + "get": { + "tags": [ + "person-controller" + ], + "operationId": "hentPerson", + "parameters": [ + { + "name": "personIdent", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "personIdentBody", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/PersonIdent" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestPersonInfo" + } + } + } + } + } + } + }, + "/api/person/oppdater-registeropplysninger/{behandlingId}": { + "get": { + "tags": [ + "person-controller" + ], + "operationId": "hentOgOppdaterRegisteropplysninger", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/person/enkel": { + "get": { + "tags": [ + "person-controller" + ], + "operationId": "hentPersonEnkel", + "parameters": [ + { + "name": "personIdent", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "personIdentBody", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/PersonIdent" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestPersonInfo" + } + } + } + } + } + } + }, + "/api/person/adresse": { + "get": { + "tags": [ + "person-controller" + ], + "operationId": "hentPersonAdresse", + "parameters": [ + { + "name": "personIdent", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestPersonInfo" + } + } + } + } + } + } + }, + "/api/oppgave/{oppgaveId}": { + "get": { + "tags": [ + "oppgave-controller" + ], + "operationId": "hentDataForManuellJournalføring", + "parameters": [ + { + "name": "oppgaveId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursDataForManuellJournalføring" + } + } + } + } + } + } + }, + "/api/oppgave/{oppgaveId}/ferdigstill": { + "get": { + "tags": [ + "oppgave-controller" + ], + "operationId": "ferdigstillOppgave", + "parameters": [ + { + "name": "oppgaveId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/logg/{behandlingId}": { + "get": { + "tags": [ + "logg-controller" + ], + "operationId": "hentLoggForBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursListLogg" + } + } + } + } + } + } + }, + "/api/klage/fagsaker/{fagsakId}/vedtak": { + "get": { + "tags": [ + "ekstern-klage-controller" + ], + "operationId": "hentVedtak", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursListFagsystemVedtak" + } + } + } + } + } + } + }, + "/api/klage/fagsaker/{fagsakId}/kan-opprette-revurdering-klage": { + "get": { + "tags": [ + "ekstern-klage-controller" + ], + "operationId": "kanOppretteRevurderingKlage", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursKanOppretteRevurderingResponse" + } + } + } + } + } + } + }, + "/api/journalpost/{journalpostId}/hent": { + "get": { + "tags": [ + "journalf-øring-controller" + ], + "operationId": "hentJournalpost", + "parameters": [ + { + "name": "journalpostId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursJournalpost" + } + } + } + } + } + } + }, + "/api/journalpost/{journalpostId}/hent/{dokumentInfoId}": { + "get": { + "tags": [ + "journalf-øring-controller" + ], + "operationId": "hentDokument", + "parameters": [ + { + "name": "journalpostId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "dokumentInfoId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursByte[]" + } + } + } + } + } + } + }, + "/api/journalpost/{journalpostId}/dokument/{dokumentInfoId}": { + "get": { + "tags": [ + "journalf-øring-controller" + ], + "operationId": "hentDokumentBytearray", + "parameters": [ + { + "name": "journalpostId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "dokumentInfoId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + }, + "/api/internstatistikk": { + "get": { + "tags": [ + "intern-statistikk-controller" + ], + "operationId": "hentAntallFagsakerOpprettet", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursInternStatistikkResponse" + } + } + } + } + } + } + }, + "/api/internstatistikk/antallSoknader": { + "get": { + "tags": [ + "intern-statistikk-controller" + ], + "operationId": "hentSøknadsstatistikkForPeriode", + "parameters": [ + { + "name": "fom", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "tom", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursSøknadsstatistikkForPeriode" + } + } + } + } + } + } + }, + "/api/forvalter/finnÅpneFagsakerMedFlereMigreringsbehandlinger": { + "get": { + "tags": [ + "forvalter-controller" + ], + "operationId": "finnÅpneFagsakerMedFlereMigreringsbehandlinger", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PairLongString" + } + } + } + } + } + } + } + }, + "/api/forvalter/finnÅpneFagsakerMedFlereMigreringsbehandlingerOgLøpendeSakIInfotrygd": { + "get": { + "tags": [ + "forvalter-controller" + ], + "operationId": "finnÅpneFagsakerMedFlereMigreringsbehandlingerOgLøpendeSakIInfotrygd", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PairLongString" + } + } + } + } + } + } + } + }, + "/api/forvalter/finnFagsakerSomSkalAvsluttes": { + "get": { + "tags": [ + "forvalter-controller" + ], + "operationId": "finnFagsakerSomSkalAvsluttes", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + } + } + } + } + }, + "/api/fagsaker/{fagsakId}": { + "get": { + "tags": [ + "fagsak-controller" + ], + "operationId": "hentRestFagsak", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestFagsak" + } + } + } + } + } + } + }, + "/api/fagsaker/{fagsakId}/opprett-tilbakekreving": { + "get": { + "tags": [ + "fagsak-controller" + ], + "operationId": "opprettTilbakekrevingsbehandling", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursString" + } + } + } + } + } + } + }, + "/api/fagsaker/{fagsakId}/hent-klagebehandlinger": { + "get": { + "tags": [ + "klage-controller" + ], + "operationId": "hentKlagebehandlinger", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursListKlagebehandlingDto" + } + } + } + } + } + } + }, + "/api/fagsaker/{fagsakId}/har-apen-tilbakekreving": { + "get": { + "tags": [ + "fagsak-controller" + ], + "operationId": "harÅpenTilbakekreving", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursBoolean" + } + } + } + } + } + } + }, + "/api/fagsaker/minimal/{fagsakId}": { + "get": { + "tags": [ + "fagsak-controller" + ], + "operationId": "hentMinimalFagsak", + "parameters": [ + { + "name": "fagsakId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestMinimalFagsak" + } + } + } + } + } + } + }, + "/api/ekstern/pensjon/bestill-personer-med-barnetrygd/{år}": { + "get": { + "tags": [ + "pensjon-controller" + ], + "description": "Tjeneste for Pensjon for å bestille identer med barnetrygd for et gitt år på kafka.", + "operationId": "bestillPersonerMedBarnetrygdForGittÅrPåKafka", + "parameters": [ + { + "name": "år", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "400": { + "description": "Ugyldig input. År må være av type string - 4 tegn" + }, + "500": { + "description": "Uventet feil", + "content": { + "text/plain": {} + } + }, + "202": { + "description": "\n RequestId som blir med kafka-meldingene\n ", + "content": { + "text/plain": {} + } + } + } + } + }, + "/api/behandlinger/{behandlingId}": { + "get": { + "tags": [ + "utvidet-behandling-controller" + ], + "operationId": "hentUtvidetBehandling", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/steg/behandlingsresultat/valider": { + "get": { + "tags": [ + "behandling-steg-controller" + ], + "operationId": "validerBehandlingsresultat", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursBoolean" + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/simulering": { + "get": { + "tags": [ + "simulering-controller" + ], + "operationId": "hentSimulering", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestSimulering" + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/personer-med-ugyldig-etterbetalingsperiode": { + "get": { + "tags": [ + "behandling-controller" + ], + "operationId": "hentPersonerMedUgyldigEtterbetalingsperiode", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursListString" + } + } + } + } + } + } + }, + "/api/behandlinger/{behandlingId}/endringstidspunkt": { + "get": { + "tags": [ + "endringstidspunkt-controller" + ], + "operationId": "hentEndringstidspunkt", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursLocalDate" + } + } + } + } + } + } + }, + "/api/vilkaarsvurdering/{behandlingId}/vilkaar": { + "delete": { + "tags": [ + "vilk-år-controller" + ], + "operationId": "slettVilkår", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestSlettVilkår" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/kompetanse/{behandlingId}/{kompetanseId}": { + "delete": { + "tags": [ + "kompetanse-controller" + ], + "operationId": "slettKompetanse", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "kompetanseId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/differanseberegning/valutakurs/{behandlingId}/{valutakursId}": { + "delete": { + "tags": [ + "valutakurs-controller" + ], + "operationId": "slettValutakurs", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "valutakursId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/differanseberegning/utenlandskperidebeløp/{behandlingId}/{utenlandskPeriodebeløpId}": { + "delete": { + "tags": [ + "utenlandsk-periodebel-øp-controller" + ], + "operationId": "slettUtenlandskPeriodebeløp", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "utenlandskPeriodebeløpId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + }, + "/api/brevmottaker/{behandlingId}/{mottakerId}": { + "delete": { + "tags": [ + "brevmottaker-controller" + ], + "operationId": "fjernBrevmottaker", + "parameters": [ + { + "name": "behandlingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "mottakerId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RessursRestUtvidetBehandling" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "IVedtakBegrunnelse": { + "required": [ + "kanDelesOpp", + "sanityApiNavn", + "vedtakBegrunnelseType" + ], + "type": "object", + "properties": { + "vedtakBegrunnelseType": { + "type": "string", + "enum": [ + "INNVILGET", + "EØS_INNVILGET", + "INSTITUSJON_INNVILGET", + "REDUKSJON", + "INSTITUSJON_REDUKSJON", + "EØS_REDUKSJON", + "AVSLAG", + "EØS_AVSLAG", + "INSTITUSJON_AVSLAG", + "OPPHØR", + "EØS_OPPHØR", + "INSTITUSJON_OPPHØR", + "FORTSATT_INNVILGET", + "EØS_FORTSATT_INNVILGET", + "INSTITUSJON_FORTSATT_INNVILGET", + "ENDRET_UTBETALING", + "ETTER_ENDRET_UTBETALING" + ] + }, + "sanityApiNavn": { + "type": "string" + }, + "kanDelesOpp": { + "type": "boolean" + } + } + }, + "RestAnnenVurdering": { + "required": [ + "id", + "resultat", + "type" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "resultat": { + "type": "string", + "enum": [ + "OPPFYLT", + "IKKE_OPPFYLT", + "IKKE_VURDERT" + ] + }, + "type": { + "type": "string", + "enum": [ + "OPPLYSNINGSPLIKT" + ] + }, + "begrunnelse": { + "type": "string" + } + } + }, + "RestPersonResultat": { + "required": [ + "andreVurderinger", + "personIdent", + "vilkårResultater" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + }, + "vilkårResultater": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestVilkårResultat" + } + }, + "andreVurderinger": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestAnnenVurdering" + } + } + } + }, + "RestVilkårResultat": { + "required": [ + "begrunnelse", + "behandlingId", + "endretAv", + "endretTidspunkt", + "erAutomatiskVurdert", + "erVurdert", + "id", + "resultat", + "utdypendeVilkårsvurderinger", + "vilkårType" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "vilkårType": { + "type": "string", + "enum": [ + "UNDER_18_ÅR", + "BOR_MED_SØKER", + "GIFT_PARTNERSKAP", + "BOSATT_I_RIKET", + "LOVLIG_OPPHOLD", + "UTVIDET_BARNETRYGD" + ] + }, + "resultat": { + "type": "string", + "enum": [ + "OPPFYLT", + "IKKE_OPPFYLT", + "IKKE_VURDERT" + ] + }, + "periodeFom": { + "type": "string", + "format": "date" + }, + "periodeTom": { + "type": "string", + "format": "date" + }, + "begrunnelse": { + "type": "string" + }, + "endretAv": { + "type": "string" + }, + "endretTidspunkt": { + "type": "string", + "format": "date-time" + }, + "behandlingId": { + "type": "integer", + "format": "int64" + }, + "erVurdert": { + "type": "boolean" + }, + "erAutomatiskVurdert": { + "type": "boolean" + }, + "erEksplisittAvslagPåSøknad": { + "type": "boolean" + }, + "avslagBegrunnelser": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IVedtakBegrunnelse" + } + }, + "vurderesEtter": { + "type": "string", + "enum": [ + "NASJONALE_REGLER", + "EØS_FORORDNINGEN" + ] + }, + "utdypendeVilkårsvurderinger": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "VURDERING_ANNET_GRUNNLAG", + "VURDERT_MEDLEMSKAP", + "DELT_BOSTED", + "DELT_BOSTED_SKAL_IKKE_DELES", + "OMFATTET_AV_NORSK_LOVGIVNING", + "OMFATTET_AV_NORSK_LOVGIVNING_UTLAND", + "ANNEN_FORELDER_OMFATTET_AV_NORSK_LOVGIVNING", + "BARN_BOR_I_NORGE", + "BARN_BOR_I_EØS", + "BARN_BOR_I_STORBRITANNIA", + "BARN_BOR_I_NORGE_MED_SØKER", + "BARN_BOR_I_EØS_MED_SØKER", + "BARN_BOR_I_EØS_MED_ANNEN_FORELDER", + "BARN_BOR_I_STORBRITANNIA_MED_SØKER", + "BARN_BOR_I_STORBRITANNIA_MED_ANNEN_FORELDER", + "BARN_BOR_ALENE_I_ANNET_EØS_LAND" + ] + } + }, + "resultatBegrunnelse": { + "type": "string", + "enum": [ + "IKKE_AKTUELT" + ] + } + } + }, + "BarnMedOpplysninger": { + "required": [ + "erFolkeregistrert", + "ident", + "inkludertISøknaden", + "manueltRegistrert", + "navn" + ], + "type": "object", + "properties": { + "ident": { + "type": "string" + }, + "navn": { + "type": "string" + }, + "fødselsdato": { + "type": "string", + "format": "date" + }, + "inkludertISøknaden": { + "type": "boolean" + }, + "manueltRegistrert": { + "type": "boolean" + }, + "erFolkeregistrert": { + "type": "boolean" + } + } + }, + "RessursRestUtvidetBehandling": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/RestUtvidetBehandling" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestArbeidsfordelingPåBehandling": { + "required": [ + "behandlendeEnhetId", + "behandlendeEnhetNavn", + "manueltOverstyrt" + ], + "type": "object", + "properties": { + "behandlendeEnhetId": { + "type": "string" + }, + "behandlendeEnhetNavn": { + "type": "string" + }, + "manueltOverstyrt": { + "type": "boolean" + } + } + }, + "RestBehandlingStegTilstand": { + "required": [ + "behandlingSteg", + "behandlingStegStatus" + ], + "type": "object", + "properties": { + "behandlingSteg": { + "type": "string", + "enum": [ + "HENLEGG_BEHANDLING", + "REGISTRERE_INSTITUSJON_OG_VERGE", + "REGISTRERE_PERSONGRUNNLAG", + "REGISTRERE_SØKNAD", + "FILTRERING_FØDSELSHENDELSER", + "VILKÅRSVURDERING", + "BEHANDLINGSRESULTAT", + "VURDER_TILBAKEKREVING", + "SEND_TIL_BESLUTTER", + "BESLUTTE_VEDTAK", + "IVERKSETT_MOT_OPPDRAG", + "VENTE_PÅ_STATUS_FRA_ØKONOMI", + "IVERKSETT_MOT_FAMILIE_TILBAKE", + "JOURNALFØR_VEDTAKSBREV", + "DISTRIBUER_VEDTAKSBREV", + "FERDIGSTILLE_BEHANDLING", + "BEHANDLING_AVSLUTTET" + ] + }, + "behandlingStegStatus": { + "type": "string", + "enum": [ + "IKKE_UTFØRT", + "UTFØRT" + ] + } + } + }, + "RestBrevmottaker": { + "required": [ + "adresselinje1", + "landkode", + "navn", + "postnummer", + "poststed", + "type" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string", + "enum": [ + "BRUKER_MED_UTENLANDSK_ADRESSE", + "FULLMEKTIG", + "VERGE", + "DØDSBO" + ] + }, + "navn": { + "type": "string" + }, + "adresselinje1": { + "type": "string" + }, + "adresselinje2": { + "type": "string" + }, + "postnummer": { + "type": "string" + }, + "poststed": { + "type": "string" + }, + "landkode": { + "type": "string" + } + } + }, + "RestEndretUtbetalingAndel": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "personIdent": { + "type": "string" + }, + "prosent": { + "type": "number" + }, + "fom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "tom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "getårsak": { + "type": "string", + "enum": [ + "DELT_BOSTED", + "ETTERBETALING_3ÅR", + "ENDRE_MOTTAKER", + "ALLEREDE_UTBETALT" + ] + }, + "avtaletidspunktDeltBosted": { + "type": "string", + "format": "date" + }, + "søknadstidspunkt": { + "type": "string", + "format": "date" + }, + "begrunnelse": { + "type": "string" + }, + "erTilknyttetAndeler": { + "type": "boolean" + } + } + }, + "RestFeilutbetaltValuta": { + "required": [ + "feilutbetaltBeløp", + "fom", + "tom" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "fom": { + "type": "string", + "format": "date" + }, + "tom": { + "type": "string", + "format": "date" + }, + "feilutbetaltBeløp": { + "type": "integer", + "format": "int32" + }, + "erPerMåned": { + "type": "boolean" + } + } + }, + "RestFødselshendelsefiltreringResultat": { + "required": [ + "begrunnelse", + "filtreringsregel", + "resultat" + ], + "type": "object", + "properties": { + "filtreringsregel": { + "type": "string", + "enum": [ + "MOR_GYLDIG_FNR", + "BARN_GYLDIG_FNR", + "MOR_LEVER", + "BARN_LEVER", + "MER_ENN_5_MND_SIDEN_FORRIGE_BARN", + "MOR_ER_OVER_18_ÅR", + "MOR_HAR_IKKE_VERGE", + "MOR_MOTTAR_IKKE_LØPENDE_UTVIDET", + "MOR_HAR_IKKE_LØPENDE_EØS_BARNETRYGD", + "FAGSAK_IKKE_MIGRERT_UT_AV_INFOTRYGD_ETTER_BARN_FØDT", + "LØPER_IKKE_BARNETRYGD_FOR_BARNET", + "MOR_HAR_IKKE_OPPFYLT_UTVIDET_VILKÅR_VED_FØDSELSDATO", + "MOR_HAR_IKKE_OPPHØRT_BARNETRYGD" + ] + }, + "resultat": { + "type": "string", + "enum": [ + "OPPFYLT", + "IKKE_OPPFYLT", + "IKKE_VURDERT" + ] + }, + "begrunnelse": { + "type": "string" + } + } + }, + "RestKompetanse": { + "required": [ + "barnIdenter", + "id", + "status" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "fom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "tom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "barnIdenter": { + "type": "array", + "items": { + "type": "string" + } + }, + "søkersAktivitet": { + "type": "string", + "enum": [ + "ARBEIDER", + "SELVSTENDIG_NÆRINGSDRIVENDE", + "UTSENDT_ARBEIDSTAKER_FRA_NORGE", + "MOTTAR_UFØRETRYGD", + "ARBEIDER_PÅ_NORSKREGISTRERT_SKIP", + "ARBEIDER_PÅ_NORSK_SOKKEL", + "ARBEIDER_FOR_ET_NORSK_FLYSELSKAP", + "ARBEIDER_VED_UTENLANDSK_UTENRIKSSTASJON", + "MOTTAR_UTBETALING_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET", + "MOTTAR_UFØRETRYGD_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET", + "MOTTAR_PENSJON_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET", + "MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN", + "MOTTAR_PENSJON", + "INAKTIV", + "I_ARBEID", + "FORSIKRET_I_BOSTEDSLAND", + "IKKE_AKTUELT", + "UTSENDT_ARBEIDSTAKER" + ] + }, + "søkersAktivitetsland": { + "type": "string" + }, + "annenForeldersAktivitet": { + "type": "string", + "enum": [ + "ARBEIDER", + "SELVSTENDIG_NÆRINGSDRIVENDE", + "UTSENDT_ARBEIDSTAKER_FRA_NORGE", + "MOTTAR_UFØRETRYGD", + "ARBEIDER_PÅ_NORSKREGISTRERT_SKIP", + "ARBEIDER_PÅ_NORSK_SOKKEL", + "ARBEIDER_FOR_ET_NORSK_FLYSELSKAP", + "ARBEIDER_VED_UTENLANDSK_UTENRIKSSTASJON", + "MOTTAR_UTBETALING_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET", + "MOTTAR_UFØRETRYGD_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET", + "MOTTAR_PENSJON_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET", + "MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN", + "MOTTAR_PENSJON", + "INAKTIV", + "I_ARBEID", + "FORSIKRET_I_BOSTEDSLAND", + "IKKE_AKTUELT", + "UTSENDT_ARBEIDSTAKER" + ] + }, + "annenForeldersAktivitetsland": { + "type": "string" + }, + "barnetsBostedsland": { + "type": "string" + }, + "resultat": { + "type": "string", + "enum": [ + "NORGE_ER_PRIMÆRLAND", + "NORGE_ER_SEKUNDÆRLAND", + "TO_PRIMÆRLAND" + ] + }, + "status": { + "type": "string", + "enum": [ + "IKKE_UTFYLT", + "UFULLSTENDIG", + "OK" + ] + }, + "erAnnenForelderOmfattetAvNorskLovgivning": { + "type": "boolean" + } + } + }, + "RestKorrigertEtterbetaling": { + "required": [ + "aktiv", + "beløp", + "getårsak", + "id", + "opprettetTidspunkt" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "getårsak": { + "type": "string", + "enum": [ + "FEIL_TIDLIGERE_UTBETALT_BELØP", + "REFUSJON_FRA_UDI", + "REFUSJON_FRA_ANDRE_MYNDIGHETER", + "MOTREGNING" + ] + }, + "begrunnelse": { + "type": "string" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "beløp": { + "type": "integer", + "format": "int32" + }, + "aktiv": { + "type": "boolean" + } + } + }, + "RestKorrigertVedtak": { + "required": [ + "aktiv", + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "vedtaksdato": { + "type": "string", + "format": "date" + }, + "begrunnelse": { + "type": "string" + }, + "aktiv": { + "type": "boolean" + } + } + }, + "RestPerson": { + "required": [ + "kjønn", + "målform", + "navn", + "personIdent", + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "SØKER", + "ANNENPART", + "BARN" + ] + }, + "fødselsdato": { + "type": "string", + "format": "date" + }, + "personIdent": { + "type": "string" + }, + "navn": { + "type": "string" + }, + "kjønn": { + "type": "string", + "enum": [ + "MANN", + "KVINNE", + "UKJENT" + ] + }, + "registerhistorikk": { + "$ref": "#/components/schemas/RestRegisterhistorikk" + }, + "målform": { + "type": "string", + "enum": [ + "NB", + "NN" + ] + }, + "dødsfallDato": { + "type": "string", + "format": "date" + } + } + }, + "RestPersonMedAndeler": { + "required": [ + "beløp", + "stønadFom", + "stønadTom", + "ytelsePerioder" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + }, + "beløp": { + "type": "integer", + "format": "int32" + }, + "stønadFom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "stønadTom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "ytelsePerioder": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestYtelsePeriode" + } + } + } + }, + "RestRefusjonEøs": { + "required": [ + "fom", + "land", + "refusjonAvklart", + "refusjonsbeløp", + "tom" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "fom": { + "type": "string", + "format": "date" + }, + "tom": { + "type": "string", + "format": "date" + }, + "refusjonsbeløp": { + "type": "integer", + "format": "int32" + }, + "land": { + "type": "string" + }, + "refusjonAvklart": { + "type": "boolean" + } + } + }, + "RestRegisterhistorikk": { + "required": [ + "hentetTidspunkt" + ], + "type": "object", + "properties": { + "hentetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "sivilstand": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestRegisteropplysning" + } + }, + "oppholdstillatelse": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestRegisteropplysning" + } + }, + "statsborgerskap": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestRegisteropplysning" + } + }, + "bostedsadresse": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestRegisteropplysning" + } + }, + "dødsboadresse": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestRegisteropplysning" + } + } + } + }, + "RestRegisteropplysning": { + "required": [ + "verdi" + ], + "type": "object", + "properties": { + "fom": { + "type": "string", + "format": "date" + }, + "tom": { + "type": "string", + "format": "date" + }, + "verdi": { + "type": "string" + } + } + }, + "RestSettPåVent": { + "required": [ + "frist", + "getårsak" + ], + "type": "object", + "properties": { + "frist": { + "type": "string", + "format": "date" + }, + "getårsak": { + "type": "string", + "enum": [ + "AVVENTER_DOKUMENTASJON" + ] + } + } + }, + "RestTilbakekreving": { + "required": [ + "begrunnelse", + "valg" + ], + "type": "object", + "properties": { + "valg": { + "type": "string", + "enum": [ + "OPPRETT_TILBAKEKREVING_MED_VARSEL", + "OPPRETT_TILBAKEKREVING_UTEN_VARSEL", + "IGNORER_TILBAKEKREVING" + ] + }, + "varsel": { + "type": "string" + }, + "begrunnelse": { + "type": "string" + }, + "tilbakekrevingsbehandlingId": { + "type": "string" + } + } + }, + "RestTotrinnskontroll": { + "required": [ + "godkjent", + "opprettetTidspunkt", + "saksbehandler" + ], + "type": "object", + "properties": { + "saksbehandler": { + "type": "string" + }, + "beslutter": { + "type": "string" + }, + "godkjent": { + "type": "boolean" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + } + } + }, + "RestUtenlandskPeriodebeløp": { + "required": [ + "barnIdenter", + "id", + "status" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "fom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "tom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "barnIdenter": { + "type": "array", + "items": { + "type": "string" + } + }, + "beløp": { + "minimum": 0.0, + "exclusiveMinimum": false, + "type": "number" + }, + "valutakode": { + "type": "string" + }, + "intervall": { + "type": "string", + "enum": [ + "ÅRLIG", + "KVARTALSVIS", + "MÅNEDLIG", + "UKENTLIG" + ] + }, + "kalkulertMånedligBeløp": { + "type": "number" + }, + "status": { + "type": "string", + "enum": [ + "IKKE_UTFYLT", + "UFULLSTENDIG", + "OK" + ] + } + } + }, + "RestUtvidetBehandling": { + "required": [ + "arbeidsfordelingPåBehandling", + "behandlingId", + "brevmottakere", + "endretAv", + "endretUtbetalingAndeler", + "feilutbetaltValuta", + "fødselshendelsefiltreringResultater", + "getårsak", + "kategori", + "kompetanser", + "opprettetTidspunkt", + "personResultater", + "personer", + "personerMedAndelerTilkjentYtelse", + "refusjonEøs", + "resultat", + "skalBehandlesAutomatisk", + "status", + "steg", + "stegTilstand", + "type", + "underkategori", + "utbetalingsperioder", + "utenlandskePeriodebeløp", + "valutakurser" + ], + "type": "object", + "properties": { + "behandlingId": { + "type": "integer", + "format": "int64" + }, + "steg": { + "type": "string", + "enum": [ + "HENLEGG_BEHANDLING", + "REGISTRERE_INSTITUSJON_OG_VERGE", + "REGISTRERE_PERSONGRUNNLAG", + "REGISTRERE_SØKNAD", + "FILTRERING_FØDSELSHENDELSER", + "VILKÅRSVURDERING", + "BEHANDLINGSRESULTAT", + "VURDER_TILBAKEKREVING", + "SEND_TIL_BESLUTTER", + "BESLUTTE_VEDTAK", + "IVERKSETT_MOT_OPPDRAG", + "VENTE_PÅ_STATUS_FRA_ØKONOMI", + "IVERKSETT_MOT_FAMILIE_TILBAKE", + "JOURNALFØR_VEDTAKSBREV", + "DISTRIBUER_VEDTAKSBREV", + "FERDIGSTILLE_BEHANDLING", + "BEHANDLING_AVSLUTTET" + ] + }, + "stegTilstand": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestBehandlingStegTilstand" + } + }, + "status": { + "type": "string", + "enum": [ + "UTREDES", + "SATT_PÅ_VENT", + "SATT_PÅ_MASKINELL_VENT", + "FATTER_VEDTAK", + "IVERKSETTER_VEDTAK", + "AVSLUTTET" + ] + }, + "resultat": { + "type": "string", + "enum": [ + "INNVILGET", + "INNVILGET_OG_OPPHØRT", + "INNVILGET_OG_ENDRET", + "INNVILGET_ENDRET_OG_OPPHØRT", + "ENDRET_OG_FORTSATT_INNVILGET", + "DELVIS_INNVILGET", + "DELVIS_INNVILGET_OG_OPPHØRT", + "DELVIS_INNVILGET_OG_ENDRET", + "DELVIS_INNVILGET_ENDRET_OG_OPPHØRT", + "AVSLÅTT", + "AVSLÅTT_OG_OPPHØRT", + "AVSLÅTT_OG_ENDRET", + "AVSLÅTT_ENDRET_OG_OPPHØRT", + "ENDRET_UTBETALING", + "ENDRET_UTEN_UTBETALING", + "ENDRET_OG_OPPHØRT", + "OPPHØRT", + "FORTSATT_OPPHØRT", + "FORTSATT_INNVILGET", + "HENLAGT_FEILAKTIG_OPPRETTET", + "HENLAGT_SØKNAD_TRUKKET", + "HENLAGT_AUTOMATISK_FØDSELSHENDELSE", + "HENLAGT_TEKNISK_VEDLIKEHOLD", + "IKKE_VURDERT" + ] + }, + "skalBehandlesAutomatisk": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "FØRSTEGANGSBEHANDLING", + "REVURDERING", + "MIGRERING_FRA_INFOTRYGD", + "MIGRERING_FRA_INFOTRYGD_OPPHØRT", + "TEKNISK_OPPHØR", + "TEKNISK_ENDRING" + ] + }, + "kategori": { + "type": "string", + "enum": [ + "EØS", + "NASJONAL" + ] + }, + "underkategori": { + "type": "string", + "enum": [ + "ORDINÆR", + "UTVIDET" + ] + }, + "getårsak": { + "type": "string", + "enum": [ + "SØKNAD", + "FØDSELSHENDELSE", + "ÅRLIG_KONTROLL", + "DØDSFALL_BRUKER", + "NYE_OPPLYSNINGER", + "KLAGE", + "TEKNISK_OPPHØR", + "TEKNISK_ENDRING", + "KORREKSJON_VEDTAKSBREV", + "OMREGNING_6ÅR", + "OMREGNING_18ÅR", + "OMREGNING_SMÅBARNSTILLEGG", + "SATSENDRING", + "SMÅBARNSTILLEGG", + "MIGRERING", + "ENDRE_MIGRERINGSDATO", + "HELMANUELL_MIGRERING" + ] + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "endretAv": { + "type": "string" + }, + "arbeidsfordelingPåBehandling": { + "$ref": "#/components/schemas/RestArbeidsfordelingPåBehandling" + }, + "søknadsgrunnlag": { + "$ref": "#/components/schemas/SøknadDTO" + }, + "personer": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestPerson" + } + }, + "personResultater": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestPersonResultat" + } + }, + "fødselshendelsefiltreringResultater": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestFødselshendelsefiltreringResultat" + } + }, + "utbetalingsperioder": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Utbetalingsperiode" + } + }, + "personerMedAndelerTilkjentYtelse": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestPersonMedAndeler" + } + }, + "endretUtbetalingAndeler": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestEndretUtbetalingAndel" + } + }, + "kompetanser": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestKompetanse" + } + }, + "tilbakekreving": { + "$ref": "#/components/schemas/RestTilbakekreving" + }, + "vedtak": { + "$ref": "#/components/schemas/RestVedtak" + }, + "totrinnskontroll": { + "$ref": "#/components/schemas/RestTotrinnskontroll" + }, + "aktivSettPåVent": { + "$ref": "#/components/schemas/RestSettPåVent" + }, + "migreringsdato": { + "type": "string", + "format": "date" + }, + "valutakurser": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestValutakurs" + } + }, + "utenlandskePeriodebeløp": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestUtenlandskPeriodebeløp" + } + }, + "verge": { + "$ref": "#/components/schemas/VergeInfo" + }, + "korrigertEtterbetaling": { + "$ref": "#/components/schemas/RestKorrigertEtterbetaling" + }, + "korrigertVedtak": { + "$ref": "#/components/schemas/RestKorrigertVedtak" + }, + "feilutbetaltValuta": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestFeilutbetaltValuta" + } + }, + "brevmottakere": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestBrevmottaker" + } + }, + "refusjonEøs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestRefusjonEøs" + } + } + } + }, + "RestValutakurs": { + "required": [ + "barnIdenter", + "id", + "status" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "fom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "tom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "barnIdenter": { + "type": "array", + "items": { + "type": "string" + } + }, + "valutakursdato": { + "type": "string", + "format": "date" + }, + "valutakode": { + "type": "string" + }, + "kurs": { + "type": "number" + }, + "status": { + "type": "string", + "enum": [ + "IKKE_UTFYLT", + "UFULLSTENDIG", + "OK" + ] + } + } + }, + "RestVedtak": { + "required": [ + "aktiv", + "id" + ], + "type": "object", + "properties": { + "aktiv": { + "type": "boolean" + }, + "vedtaksdato": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "integer", + "format": "int64" + } + } + }, + "RestYtelsePeriode": { + "required": [ + "beløp", + "skalUtbetales", + "stønadFom", + "stønadTom", + "ytelseType" + ], + "type": "object", + "properties": { + "beløp": { + "type": "integer", + "format": "int32" + }, + "stønadFom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "stønadTom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "ytelseType": { + "type": "string", + "enum": [ + "ORDINÆR_BARNETRYGD", + "UTVIDET_BARNETRYGD", + "SMÅBARNSTILLEGG" + ] + }, + "skalUtbetales": { + "type": "boolean" + } + } + }, + "SøkerMedOpplysninger": { + "required": [ + "ident", + "målform" + ], + "type": "object", + "properties": { + "ident": { + "type": "string" + }, + "målform": { + "type": "string", + "enum": [ + "NB", + "NN" + ] + } + } + }, + "SøknadDTO": { + "required": [ + "barnaMedOpplysninger", + "endringAvOpplysningerBegrunnelse", + "søkerMedOpplysninger", + "underkategori" + ], + "type": "object", + "properties": { + "underkategori": { + "type": "string", + "enum": [ + "ORDINÆR", + "UTVIDET" + ] + }, + "søkerMedOpplysninger": { + "$ref": "#/components/schemas/SøkerMedOpplysninger" + }, + "barnaMedOpplysninger": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BarnMedOpplysninger" + } + }, + "endringAvOpplysningerBegrunnelse": { + "type": "string" + } + } + }, + "Utbetalingsperiode": { + "required": [ + "antallBarn", + "periodeFom", + "periodeTom", + "utbetalingsperiodeDetaljer", + "utbetaltPerMnd", + "vedtaksperiodetype", + "ytelseTyper" + ], + "type": "object", + "properties": { + "periodeFom": { + "type": "string", + "format": "date" + }, + "periodeTom": { + "type": "string", + "format": "date" + }, + "vedtaksperiodetype": { + "type": "string", + "enum": [ + "UTBETALING", + "UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING", + "OPPHØR", + "AVSLAG", + "FORTSATT_INNVILGET", + "ENDRET_UTBETALING" + ] + }, + "utbetalingsperiodeDetaljer": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UtbetalingsperiodeDetalj" + } + }, + "ytelseTyper": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ORDINÆR_BARNETRYGD", + "UTVIDET_BARNETRYGD", + "SMÅBARNSTILLEGG" + ] + } + }, + "antallBarn": { + "type": "integer", + "format": "int32" + }, + "utbetaltPerMnd": { + "type": "integer", + "format": "int32" + } + } + }, + "UtbetalingsperiodeDetalj": { + "required": [ + "erPåvirketAvEndring", + "person", + "prosent", + "utbetaltPerMnd", + "ytelseType" + ], + "type": "object", + "properties": { + "person": { + "$ref": "#/components/schemas/RestPerson" + }, + "ytelseType": { + "type": "string", + "enum": [ + "ORDINÆR_BARNETRYGD", + "UTVIDET_BARNETRYGD", + "SMÅBARNSTILLEGG" + ] + }, + "utbetaltPerMnd": { + "type": "integer", + "format": "int32" + }, + "erPåvirketAvEndring": { + "type": "boolean" + }, + "endringsårsak": { + "type": "string", + "enum": [ + "DELT_BOSTED", + "ETTERBETALING_3ÅR", + "ENDRE_MOTTAKER", + "ALLEREDE_UTBETALT" + ] + }, + "prosent": { + "type": "number" + } + } + }, + "VergeInfo": { + "required": [ + "ident" + ], + "type": "object", + "properties": { + "ident": { + "type": "string" + } + } + }, + "RestPutVedtaksperiodeMedStandardbegrunnelser": { + "required": [ + "standardbegrunnelser" + ], + "type": "object", + "properties": { + "standardbegrunnelser": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "RessursListRestUtvidetVedtaksperiodeMedBegrunnelser": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestUtvidetVedtaksperiodeMedBegrunnelser" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestUtvidetVedtaksperiodeMedBegrunnelser": { + "required": [ + "begrunnelser", + "fritekster", + "gyldigeBegrunnelser", + "id", + "type", + "utbetalingsperiodeDetaljer" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "fom": { + "type": "string", + "format": "date" + }, + "tom": { + "type": "string", + "format": "date" + }, + "type": { + "type": "string", + "enum": [ + "UTBETALING", + "UTBETALING_MED_REDUKSJON_FRA_SIST_IVERKSATTE_BEHANDLING", + "OPPHØR", + "AVSLAG", + "FORTSATT_INNVILGET", + "ENDRET_UTBETALING" + ] + }, + "begrunnelser": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestVedtaksbegrunnelse" + } + }, + "fritekster": { + "type": "array", + "items": { + "type": "string" + } + }, + "gyldigeBegrunnelser": { + "type": "array", + "items": { + "type": "string" + } + }, + "utbetalingsperiodeDetaljer": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UtbetalingsperiodeDetalj" + } + } + } + }, + "RestVedtaksbegrunnelse": { + "required": [ + "standardbegrunnelse", + "vedtakBegrunnelseSpesifikasjon", + "vedtakBegrunnelseType" + ], + "type": "object", + "properties": { + "standardbegrunnelse": { + "type": "string" + }, + "vedtakBegrunnelseSpesifikasjon": { + "type": "string" + }, + "vedtakBegrunnelseType": { + "type": "string", + "enum": [ + "INNVILGET", + "EØS_INNVILGET", + "INSTITUSJON_INNVILGET", + "REDUKSJON", + "INSTITUSJON_REDUKSJON", + "EØS_REDUKSJON", + "AVSLAG", + "EØS_AVSLAG", + "INSTITUSJON_AVSLAG", + "OPPHØR", + "EØS_OPPHØR", + "INSTITUSJON_OPPHØR", + "FORTSATT_INNVILGET", + "EØS_FORTSATT_INNVILGET", + "INSTITUSJON_FORTSATT_INNVILGET", + "ENDRET_UTBETALING", + "ETTER_ENDRET_UTBETALING" + ] + } + } + }, + "RestPutVedtaksperiodeMedFritekster": { + "required": [ + "fritekster" + ], + "type": "object", + "properties": { + "fritekster": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "RestGenererVedtaksperioderForOverstyrtEndringstidspunkt": { + "required": [ + "behandlingId", + "overstyrtEndringstidspunkt" + ], + "type": "object", + "properties": { + "behandlingId": { + "type": "integer", + "format": "int64" + }, + "overstyrtEndringstidspunkt": { + "type": "string", + "format": "date" + } + } + }, + "RessursString": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + } + } + }, + "KommentarDTO": { + "required": [ + "kommentar", + "settTilManuellOppfølging" + ], + "type": "object", + "properties": { + "settTilManuellOppfølging": { + "type": "boolean" + }, + "kommentar": { + "type": "string" + } + } + }, + "AvvikshåndterDTO": { + "required": [ + "avvikstype", + "getårsak" + ], + "type": "object", + "properties": { + "avvikstype": { + "type": "string", + "enum": [ + "ANNET", + "DUPLIKAT" + ] + }, + "getårsak": { + "type": "string" + } + } + }, + "RessursUnit": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Unit" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "Unit": { + "type": "object" + }, + "NyBehandlingHendelse": { + "required": [ + "barnasIdenter", + "morsIdent" + ], + "type": "object", + "properties": { + "morsIdent": { + "type": "string" + }, + "barnasIdenter": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "RestHenleggBehandlingInfo": { + "required": [ + "begrunnelse", + "getårsak" + ], + "type": "object", + "properties": { + "getårsak": { + "type": "string", + "enum": [ + "SØKNAD_TRUKKET", + "FEILAKTIG_OPPRETTET", + "FØDSELSHENDELSE_UGYLDIG_UTFALL", + "TEKNISK_VEDLIKEHOLD" + ] + }, + "begrunnelse": { + "type": "string" + } + } + }, + "RestEndreBehandlingstema": { + "required": [ + "behandlingKategori", + "behandlingUnderkategori" + ], + "type": "object", + "properties": { + "behandlingUnderkategori": { + "type": "string", + "enum": [ + "UTVIDET", + "ORDINÆR" + ] + }, + "behandlingKategori": { + "type": "string", + "enum": [ + "EØS", + "NASJONAL" + ] + } + } + }, + "RestEndreBehandlendeEnhet": { + "required": [ + "begrunnelse", + "enhetId" + ], + "type": "object", + "properties": { + "enhetId": { + "type": "string" + }, + "begrunnelse": { + "type": "string" + } + } + }, + "PersonIdent": { + "required": [ + "ident" + ], + "type": "object", + "properties": { + "ident": { + "pattern": "(^$|.{11})", + "type": "string" + } + } + }, + "RestNyttVilkår": { + "required": [ + "personIdent", + "vilkårType" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + }, + "vilkårType": { + "type": "string", + "enum": [ + "UNDER_18_ÅR", + "BOR_MED_SØKER", + "GIFT_PARTNERSKAP", + "BOSATT_I_RIKET", + "LOVLIG_OPPHOLD", + "UTVIDET_BARNETRYGD" + ] + } + } + }, + "TilgangRequestDTO": { + "required": [ + "brukerIdent" + ], + "type": "object", + "properties": { + "brukerIdent": { + "type": "string" + } + } + }, + "RessursTilgangDTO": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/TilgangDTO" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "TilgangDTO": { + "required": [ + "adressebeskyttelsegradering", + "saksbehandlerHarTilgang" + ], + "type": "object", + "properties": { + "saksbehandlerHarTilgang": { + "type": "boolean" + }, + "adressebeskyttelsegradering": { + "type": "string", + "enum": [ + "STRENGT_FORTROLIG_UTLAND", + "FORTROLIG", + "STRENGT_FORTROLIG", + "UGRADERT" + ] + } + } + }, + "ForhåndsvisTilbakekrevingsvarselbrevRequest": { + "required": [ + "fritekst" + ], + "type": "object", + "properties": { + "fritekst": { + "type": "string" + } + } + }, + "RessursByte[]": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "string", + "format": "byte" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "Kompetanse": { + "required": [ + "barnsIdenter", + "fom" + ], + "type": "object", + "properties": { + "barnsIdenter": { + "type": "array", + "items": { + "type": "string" + } + }, + "fom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "tom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "sokersaktivitet": { + "type": "string", + "enum": [ + "ARBEIDER", + "SELVSTENDIG_NÆRINGSDRIVENDE", + "UTSENDT_ARBEIDSTAKER_FRA_NORGE", + "MOTTAR_UFØRETRYGD", + "ARBEIDER_PÅ_NORSKREGISTRERT_SKIP", + "ARBEIDER_PÅ_NORSK_SOKKEL", + "ARBEIDER_FOR_ET_NORSK_FLYSELSKAP", + "ARBEIDER_VED_UTENLANDSK_UTENRIKSSTASJON", + "MOTTAR_UTBETALING_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET", + "MOTTAR_UFØRETRYGD_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET", + "MOTTAR_PENSJON_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET", + "MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN", + "MOTTAR_PENSJON", + "INAKTIV", + "I_ARBEID", + "FORSIKRET_I_BOSTEDSLAND", + "IKKE_AKTUELT", + "UTSENDT_ARBEIDSTAKER" + ] + }, + "sokersAktivitetsland": { + "type": "string" + }, + "annenForeldersAktivitet": { + "type": "string", + "enum": [ + "ARBEIDER", + "SELVSTENDIG_NÆRINGSDRIVENDE", + "UTSENDT_ARBEIDSTAKER_FRA_NORGE", + "MOTTAR_UFØRETRYGD", + "ARBEIDER_PÅ_NORSKREGISTRERT_SKIP", + "ARBEIDER_PÅ_NORSK_SOKKEL", + "ARBEIDER_FOR_ET_NORSK_FLYSELSKAP", + "ARBEIDER_VED_UTENLANDSK_UTENRIKSSTASJON", + "MOTTAR_UTBETALING_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET", + "MOTTAR_UFØRETRYGD_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET", + "MOTTAR_PENSJON_FRA_NAV_UNDER_OPPHOLD_I_UTLANDET", + "MOTTAR_UTBETALING_SOM_ERSTATTER_LØNN", + "MOTTAR_PENSJON", + "INAKTIV", + "I_ARBEID", + "FORSIKRET_I_BOSTEDSLAND", + "IKKE_AKTUELT", + "UTSENDT_ARBEIDSTAKER" + ] + }, + "annenForeldersAktivitetsland": { + "type": "string" + }, + "barnetsBostedsland": { + "type": "string" + }, + "resultat": { + "type": "string", + "enum": [ + "NORGE_ER_PRIMÆRLAND", + "NORGE_ER_SEKUNDÆRLAND", + "TO_PRIMÆRLAND" + ] + }, + "annenForelderOmfattetAvNorskLovgivning": { + "type": "boolean" + } + } + }, + "PersonDVHV2": { + "required": [ + "bostedsland", + "delingsprosentYtelse", + "personIdent", + "rolle", + "statsborgerskap" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + }, + "rolle": { + "type": "string" + }, + "statsborgerskap": { + "type": "array", + "items": { + "type": "string" + } + }, + "bostedsland": { + "type": "string" + }, + "delingsprosentYtelse": { + "type": "integer", + "format": "int32" + } + } + }, + "UtbetalingsDetaljDVHV2": { + "required": [ + "delytelseId", + "klassekode", + "person", + "utbetaltPrMnd" + ], + "type": "object", + "properties": { + "person": { + "$ref": "#/components/schemas/PersonDVHV2" + }, + "klassekode": { + "type": "string" + }, + "utbetaltPrMnd": { + "type": "integer", + "format": "int32" + }, + "delytelseId": { + "type": "string" + }, + "ytelseType": { + "type": "string", + "enum": [ + "ORDINÆR_BARNETRYGD", + "UTVIDET_BARNETRYGD", + "SMÅBARNSTILLEGG", + "MANUELL_VURDERING" + ] + } + } + }, + "UtbetalingsperiodeDVHV2": { + "required": [ + "hjemmel", + "stønadFom", + "stønadTom", + "utbetalingsDetaljer", + "utbetaltPerMnd" + ], + "type": "object", + "properties": { + "hjemmel": { + "type": "string" + }, + "utbetaltPerMnd": { + "type": "integer", + "format": "int32" + }, + "stønadFom": { + "type": "string", + "format": "date" + }, + "stønadTom": { + "type": "string", + "format": "date" + }, + "utbetalingsDetaljer": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UtbetalingsDetaljDVHV2" + } + } + } + }, + "VedtakDVHV2": { + "required": [ + "behandlingTypeV2", + "behandlingsId", + "behandlingÅrsakV2", + "ensligForsørger", + "fagsakId", + "funksjonellId", + "kategoriV2", + "personV2", + "tidspunktVedtak", + "utbetalingsperioderV2" + ], + "type": "object", + "properties": { + "fagsakId": { + "type": "string" + }, + "behandlingsId": { + "type": "string" + }, + "tidspunktVedtak": { + "type": "string", + "format": "date-time" + }, + "personV2": { + "$ref": "#/components/schemas/PersonDVHV2" + }, + "ensligForsørger": { + "type": "boolean" + }, + "kategoriV2": { + "type": "string", + "enum": [ + "EØS", + "NASJONAL" + ] + }, + "underkategoriV2": { + "type": "string", + "enum": [ + "UTVIDET", + "ORDINÆR", + "INSTITUSJON" + ] + }, + "behandlingTypeV2": { + "type": "string", + "enum": [ + "FØRSTEGANGSBEHANDLING", + "REVURDERING", + "MIGRERING_FRA_INFOTRYGD", + "MIGRERING_FRA_INFOTRYGD_OPPHØRT", + "TEKNISK_ENDRING" + ] + }, + "utbetalingsperioderV2": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UtbetalingsperiodeDVHV2" + } + }, + "kompetanseperioder": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Kompetanse" + } + }, + "funksjonellId": { + "type": "string" + }, + "behandlingÅrsakV2": { + "type": "string", + "enum": [ + "SØKNAD", + "FØDSELSHENDELSE", + "ÅRLIG_KONTROLL", + "DØDSFALL_BRUKER", + "NYE_OPPLYSNINGER", + "KLAGE", + "TEKNISK_ENDRING", + "KORREKSJON_VEDTAKSBREV", + "OMREGNING_6ÅR", + "OMREGNING_18ÅR", + "OMREGNING_SMÅBARNSTILLEGG", + "SMÅBARNSTILLEGG", + "MIGRERING", + "SATSENDRING", + "ENDRE_MIGRERINGSDATO", + "HELMANUELL_MIGRERING" + ] + }, + "fagsakType": { + "type": "string", + "enum": [ + "NORMAL", + "BARN_ENSLIG_MINDREÅRIG", + "INSTITUSJON" + ] + } + } + }, + "SmåbarnstilleggKorrigeringRequest": { + "required": [ + "getårMåned" + ], + "type": "object", + "properties": { + "getårMåned": { + "type": "string", + "example": "2020-12" + } + } + }, + "SkatteetatenPerioderRequest": { + "required": [ + "aar", + "identer" + ], + "type": "object", + "properties": { + "aar": { + "type": "string" + }, + "identer": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "RessursSkatteetatenPerioderResponse": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SkatteetatenPerioderResponse" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "SkatteetatenPeriode": { + "required": [ + "delingsprosent", + "fraMaaned" + ], + "type": "object", + "properties": { + "fraMaaned": { + "type": "string" + }, + "delingsprosent": { + "type": "string", + "enum": [ + "0", + "50", + "usikker" + ] + }, + "tomMaaned": { + "type": "string" + } + } + }, + "SkatteetatenPerioder": { + "required": [ + "ident", + "perioder", + "sisteVedtakPaaIdent" + ], + "type": "object", + "properties": { + "ident": { + "type": "string" + }, + "sisteVedtakPaaIdent": { + "type": "string", + "format": "date-time" + }, + "perioder": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SkatteetatenPeriode" + } + } + } + }, + "SkatteetatenPerioderResponse": { + "required": [ + "brukere" + ], + "type": "object", + "properties": { + "brukere": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SkatteetatenPerioder" + } + } + } + }, + "PairStringString": { + "required": [ + "first", + "second" + ], + "type": "object", + "properties": { + "first": { + "type": "string" + }, + "second": { + "type": "string" + } + } + }, + "SøkSamhandlerInfoRequest": { + "type": "object", + "properties": { + "navn": { + "type": "string" + }, + "postnummer": { + "type": "string" + }, + "område": { + "type": "string" + } + } + }, + "RessursListSamhandlerInfo": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SamhandlerInfo" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "SamhandlerAdresse": { + "required": [ + "adresseType", + "adresselinjer", + "postNr", + "postSted" + ], + "type": "object", + "properties": { + "adresselinjer": { + "type": "array", + "items": { + "type": "string" + } + }, + "postNr": { + "type": "string" + }, + "postSted": { + "type": "string" + }, + "adresseType": { + "type": "string" + } + } + }, + "SamhandlerInfo": { + "required": [ + "adresser", + "navn", + "tssEksternId" + ], + "type": "object", + "properties": { + "tssEksternId": { + "type": "string" + }, + "navn": { + "type": "string" + }, + "adresser": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SamhandlerAdresse" + } + }, + "orgNummer": { + "type": "string" + } + } + }, + "SaksstatistikkSendtRequest": { + "required": [ + "json", + "offset", + "sendtTidspunkt", + "type" + ], + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string", + "enum": [ + "SAK", + "BEHANDLING" + ] + }, + "json": { + "type": "string" + }, + "sendtTidspunkt": { + "type": "string", + "format": "date-time" + } + } + }, + "SaksstatistikkMellomlagring": { + "required": [ + "funksjonellId", + "id", + "json", + "kontraktVersjon", + "opprettetTidspunkt", + "type" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "offsetVerdiOnPrem": { + "type": "integer", + "format": "int64" + }, + "offsetVerdi": { + "type": "integer", + "format": "int64" + }, + "funksjonellId": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "SAK", + "BEHANDLING" + ] + }, + "kontraktVersjon": { + "type": "string" + }, + "json": { + "type": "string" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "konvertertTidspunkt": { + "type": "string", + "format": "date-time" + }, + "sendtTidspunkt": { + "type": "string", + "format": "date-time" + }, + "typeId": { + "type": "integer", + "format": "int64" + } + } + }, + "RestManuellDødsfall": { + "required": [ + "begrunnelse", + "dødsfallDato", + "personIdent" + ], + "type": "object", + "properties": { + "dødsfallDato": { + "type": "string", + "format": "date" + }, + "personIdent": { + "type": "string" + }, + "begrunnelse": { + "type": "string" + } + } + }, + "Personident": { + "required": [ + "ident" + ], + "type": "object", + "properties": { + "ident": { + "type": "string" + } + } + }, + "Oppgave": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "identer": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OppgaveIdentV2" + } + }, + "tildeltEnhetsnr": { + "type": "string" + }, + "endretAvEnhetsnr": { + "type": "string" + }, + "opprettetAvEnhetsnr": { + "type": "string" + }, + "journalpostId": { + "type": "string" + }, + "journalpostkilde": { + "type": "string" + }, + "behandlesAvApplikasjon": { + "type": "string" + }, + "saksreferanse": { + "type": "string" + }, + "bnr": { + "type": "string" + }, + "samhandlernr": { + "type": "string" + }, + "aktoerId": { + "pattern": "[0-9]{13}", + "type": "string" + }, + "orgnr": { + "type": "string" + }, + "tilordnetRessurs": { + "type": "string" + }, + "beskrivelse": { + "type": "string" + }, + "temagruppe": { + "type": "string" + }, + "tema": { + "type": "string", + "enum": [ + "BAR", + "ENF", + "KON", + "OPP" + ] + }, + "behandlingstema": { + "type": "string" + }, + "oppgavetype": { + "type": "string" + }, + "behandlingstype": { + "type": "string" + }, + "versjon": { + "type": "integer", + "format": "int32" + }, + "mappeId": { + "type": "integer", + "format": "int64" + }, + "fristFerdigstillelse": { + "type": "string" + }, + "aktivDato": { + "type": "string" + }, + "opprettetTidspunkt": { + "type": "string" + }, + "opprettetAv": { + "type": "string" + }, + "endretAv": { + "type": "string" + }, + "ferdigstiltTidspunkt": { + "type": "string" + }, + "endretTidspunkt": { + "type": "string" + }, + "prioritet": { + "type": "string", + "enum": [ + "HOY", + "NORM", + "LAV" + ] + }, + "status": { + "type": "string", + "enum": [ + "OPPRETTET", + "AAPNET", + "UNDER_BEHANDLING", + "FERDIGSTILT", + "FEILREGISTRERT" + ] + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "writeOnly": true + } + } + }, + "OppgaveIdentV2": { + "type": "object", + "properties": { + "ident": { + "type": "string" + }, + "gruppe": { + "type": "string", + "enum": [ + "AKTOERID", + "FOLKEREGISTERIDENT", + "NPID", + "ORGNR", + "SAMHANDLERNR" + ] + } + } + }, + "RessursOppgave": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Oppgave" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "NavnOgIdent": { + "required": [ + "id", + "navn" + ], + "type": "object", + "properties": { + "navn": { + "type": "string" + }, + "id": { + "type": "string" + } + } + }, + "RestFerdigstillOppgaveKnyttJournalpost": { + "required": [ + "bruker", + "journalpostId", + "navIdent", + "nyBehandlingstype", + "nyBehandlingsårsak", + "opprettOgKnyttTilNyBehandling", + "tilknyttedeBehandlingIder" + ], + "type": "object", + "properties": { + "journalpostId": { + "type": "string" + }, + "tilknyttedeBehandlingIder": { + "type": "array", + "items": { + "type": "string" + } + }, + "opprettOgKnyttTilNyBehandling": { + "type": "boolean" + }, + "navIdent": { + "type": "string" + }, + "bruker": { + "$ref": "#/components/schemas/NavnOgIdent" + }, + "nyBehandlingstype": { + "type": "string", + "enum": [ + "FØRSTEGANGSBEHANDLING", + "REVURDERING", + "MIGRERING_FRA_INFOTRYGD", + "MIGRERING_FRA_INFOTRYGD_OPPHØRT", + "TEKNISK_OPPHØR", + "TEKNISK_ENDRING" + ] + }, + "nyBehandlingsårsak": { + "type": "string", + "enum": [ + "SØKNAD", + "FØDSELSHENDELSE", + "ÅRLIG_KONTROLL", + "DØDSFALL_BRUKER", + "NYE_OPPLYSNINGER", + "KLAGE", + "TEKNISK_OPPHØR", + "TEKNISK_ENDRING", + "KORREKSJON_VEDTAKSBREV", + "OMREGNING_6ÅR", + "OMREGNING_18ÅR", + "OMREGNING_SMÅBARNSTILLEGG", + "SATSENDRING", + "SMÅBARNSTILLEGG", + "MIGRERING", + "ENDRE_MIGRERINGSDATO", + "HELMANUELL_MIGRERING" + ] + }, + "kategori": { + "type": "string", + "enum": [ + "EØS", + "NASJONAL" + ] + }, + "underkategori": { + "type": "string", + "enum": [ + "UTVIDET", + "ORDINÆR" + ] + }, + "datoMottatt": { + "type": "string", + "format": "date-time" + } + } + }, + "RestFinnOppgaveRequest": { + "type": "object", + "properties": { + "behandlingstema": { + "type": "string" + }, + "behandlingstype": { + "type": "string" + }, + "oppgavetype": { + "type": "string" + }, + "enhet": { + "type": "string" + }, + "saksbehandler": { + "type": "string" + }, + "journalpostId": { + "type": "string" + }, + "tilordnetRessurs": { + "type": "string" + }, + "tildeltRessurs": { + "type": "boolean" + }, + "opprettetFomTidspunkt": { + "type": "string", + "format": "date-time" + }, + "opprettetTomTidspunkt": { + "type": "string", + "format": "date-time" + }, + "fristFomDato": { + "type": "string", + "format": "date" + }, + "fristTomDato": { + "type": "string", + "format": "date" + }, + "aktivFomDato": { + "type": "string", + "format": "date" + }, + "aktivTomDato": { + "type": "string", + "format": "date" + }, + "limit": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + } + } + }, + "FinnOppgaveResponseDto": { + "required": [ + "antallTreffTotalt", + "oppgaver" + ], + "type": "object", + "properties": { + "antallTreffTotalt": { + "type": "integer", + "format": "int64" + }, + "oppgaver": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Oppgave" + } + } + } + }, + "RessursFinnOppgaveResponseDto": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/FinnOppgaveResponseDto" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "KorrigerVedtakRequest": { + "required": [ + "vedtaksdato" + ], + "type": "object", + "properties": { + "vedtaksdato": { + "type": "string", + "format": "date" + }, + "begrunnelse": { + "type": "string" + } + } + }, + "KorrigertEtterbetalingRequest": { + "required": [ + "beløp", + "getårsak" + ], + "type": "object", + "properties": { + "getårsak": { + "type": "string", + "enum": [ + "FEIL_TIDLIGERE_UTBETALT_BELØP", + "REFUSJON_FRA_UDI", + "REFUSJON_FRA_ANDRE_MYNDIGHETER", + "MOTREGNING" + ] + }, + "begrunnelse": { + "type": "string" + }, + "beløp": { + "type": "integer", + "format": "int32" + } + } + }, + "StartKonsistensavstemming": { + "required": [ + "triggerTid" + ], + "type": "object", + "properties": { + "triggerTid": { + "type": "string", + "format": "date-time" + } + } + }, + "IkkeOpprettet": { + "required": [ + "getårsak" + ], + "type": "object", + "properties": { + "getårsak": { + "type": "string", + "enum": [ + "ÅPEN_BEHANDLING", + "INGEN_BEHANDLING", + "FEIL" + ] + }, + "detaljer": { + "type": "string" + } + } + }, + "OpprettRevurderingResponse": { + "required": [ + "opprettetBehandling" + ], + "type": "object", + "properties": { + "opprettetBehandling": { + "type": "boolean" + }, + "opprettet": { + "$ref": "#/components/schemas/Opprettet" + }, + "ikkeOpprettet": { + "$ref": "#/components/schemas/IkkeOpprettet" + } + } + }, + "Opprettet": { + "required": [ + "eksternBehandlingId" + ], + "type": "object", + "properties": { + "eksternBehandlingId": { + "type": "string" + } + } + }, + "RessursOpprettRevurderingResponse": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/OpprettRevurderingResponse" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "InstitusjonInfo": { + "type": "object", + "properties": { + "orgNummer": { + "type": "string" + }, + "tssEksternId": { + "type": "string" + }, + "navn": { + "type": "string" + } + } + }, + "LogiskVedlegg": { + "required": [ + "logiskVedleggId", + "tittel" + ], + "type": "object", + "properties": { + "logiskVedleggId": { + "type": "string" + }, + "tittel": { + "type": "string" + } + } + }, + "RestJournalføring": { + "required": [ + "avsender", + "bruker", + "dokumenter", + "fagsakType", + "knyttTilFagsak", + "navIdent", + "nyBehandlingstype", + "nyBehandlingsårsak", + "opprettOgKnyttTilNyBehandling", + "tilknyttedeBehandlingIder" + ], + "type": "object", + "properties": { + "avsender": { + "$ref": "#/components/schemas/NavnOgIdent" + }, + "bruker": { + "$ref": "#/components/schemas/NavnOgIdent" + }, + "datoMottatt": { + "type": "string", + "format": "date-time" + }, + "journalpostTittel": { + "type": "string" + }, + "kategori": { + "type": "string", + "enum": [ + "EØS", + "NASJONAL" + ] + }, + "underkategori": { + "type": "string", + "enum": [ + "UTVIDET", + "ORDINÆR" + ] + }, + "knyttTilFagsak": { + "type": "boolean" + }, + "opprettOgKnyttTilNyBehandling": { + "type": "boolean" + }, + "tilknyttedeBehandlingIder": { + "type": "array", + "items": { + "type": "string" + } + }, + "dokumenter": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestJournalpostDokument" + } + }, + "navIdent": { + "type": "string" + }, + "nyBehandlingstype": { + "type": "string", + "enum": [ + "FØRSTEGANGSBEHANDLING", + "REVURDERING", + "MIGRERING_FRA_INFOTRYGD", + "MIGRERING_FRA_INFOTRYGD_OPPHØRT", + "TEKNISK_OPPHØR", + "TEKNISK_ENDRING" + ] + }, + "nyBehandlingsårsak": { + "type": "string", + "enum": [ + "SØKNAD", + "FØDSELSHENDELSE", + "ÅRLIG_KONTROLL", + "DØDSFALL_BRUKER", + "NYE_OPPLYSNINGER", + "KLAGE", + "TEKNISK_OPPHØR", + "TEKNISK_ENDRING", + "KORREKSJON_VEDTAKSBREV", + "OMREGNING_6ÅR", + "OMREGNING_18ÅR", + "OMREGNING_SMÅBARNSTILLEGG", + "SATSENDRING", + "SMÅBARNSTILLEGG", + "MIGRERING", + "ENDRE_MIGRERINGSDATO", + "HELMANUELL_MIGRERING" + ] + }, + "fagsakType": { + "type": "string", + "enum": [ + "NORMAL", + "BARN_ENSLIG_MINDREÅRIG", + "INSTITUSJON" + ] + }, + "institusjon": { + "$ref": "#/components/schemas/InstitusjonInfo" + } + } + }, + "RestJournalpostDokument": { + "required": [ + "dokumentInfoId" + ], + "type": "object", + "properties": { + "dokumentTittel": { + "type": "string" + }, + "dokumentInfoId": { + "type": "string" + }, + "brevkode": { + "type": "string" + }, + "logiskeVedlegg": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogiskVedlegg" + } + }, + "eksisterendeLogiskeVedlegg": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogiskVedlegg" + } + } + } + }, + "AvsenderMottaker": { + "required": [ + "erLikBruker" + ], + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "FNR", + "HPRNR", + "NULL", + "ORGNR", + "UKJENT", + "UTL_ORG" + ] + }, + "navn": { + "type": "string" + }, + "land": { + "type": "string" + }, + "erLikBruker": { + "type": "boolean" + } + } + }, + "Bruker": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "AKTOERID", + "FNR", + "ORGNR" + ] + } + } + }, + "DigitalpostSendt": { + "required": [ + "adresse" + ], + "type": "object", + "properties": { + "adresse": { + "type": "string" + } + } + }, + "DokumentInfo": { + "required": [ + "dokumentInfoId" + ], + "type": "object", + "properties": { + "dokumentInfoId": { + "type": "string" + }, + "tittel": { + "type": "string" + }, + "brevkode": { + "type": "string" + }, + "dokumentstatus": { + "type": "string", + "enum": [ + "FERDIGSTILT", + "AVBRUTT", + "UNDER_REDIGERING", + "KASSERT" + ] + }, + "dokumentvarianter": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Dokumentvariant" + } + }, + "logiskeVedlegg": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogiskVedlegg" + } + } + } + }, + "Dokumentvariant": { + "required": [ + "saksbehandlerHarTilgang", + "variantformat" + ], + "type": "object", + "properties": { + "variantformat": { + "type": "string", + "enum": [ + "ORIGINAL", + "ARKIV", + "FULLVERSJON", + "PRODUKSJON", + "PRODUKSJON_DLF", + "SLADDET" + ] + }, + "filnavn": { + "type": "string" + }, + "saksbehandlerHarTilgang": { + "type": "boolean" + } + } + }, + "FysiskpostSendt": { + "required": [ + "adressetekstKonvolutt" + ], + "type": "object", + "properties": { + "adressetekstKonvolutt": { + "type": "string" + } + } + }, + "Journalpost": { + "required": [ + "journalpostId", + "journalposttype", + "journalstatus" + ], + "type": "object", + "properties": { + "journalpostId": { + "type": "string" + }, + "journalposttype": { + "type": "string", + "enum": [ + "I", + "U", + "N" + ] + }, + "journalstatus": { + "type": "string", + "enum": [ + "MOTTATT", + "JOURNALFOERT", + "FERDIGSTILT", + "EKSPEDERT", + "UNDER_ARBEID", + "FEILREGISTRERT", + "UTGAAR", + "AVBRUTT", + "UKJENT_BRUKER", + "RESERVERT", + "OPPLASTING_DOKUMENT", + "UKJENT" + ] + }, + "tema": { + "type": "string" + }, + "behandlingstema": { + "type": "string" + }, + "tittel": { + "type": "string" + }, + "sak": { + "$ref": "#/components/schemas/Sak" + }, + "bruker": { + "$ref": "#/components/schemas/Bruker" + }, + "avsenderMottaker": { + "$ref": "#/components/schemas/AvsenderMottaker" + }, + "journalforendeEnhet": { + "type": "string" + }, + "kanal": { + "type": "string" + }, + "dokumenter": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DokumentInfo" + } + }, + "relevanteDatoer": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RelevantDato" + } + }, + "eksternReferanseId": { + "type": "string" + }, + "utsendingsinfo": { + "$ref": "#/components/schemas/Utsendingsinfo" + }, + "datoMottatt": { + "type": "string", + "format": "date-time" + } + } + }, + "RelevantDato": { + "required": [ + "dato", + "datotype" + ], + "type": "object", + "properties": { + "dato": { + "type": "string", + "format": "date-time" + }, + "datotype": { + "type": "string" + } + } + }, + "RessursListJournalpost": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Journalpost" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "Sak": { + "type": "object", + "properties": { + "arkivsaksnummer": { + "type": "string" + }, + "arkivsaksystem": { + "type": "string" + }, + "fagsakId": { + "type": "string" + }, + "sakstype": { + "type": "string" + }, + "fagsaksystem": { + "type": "string" + } + } + }, + "Utsendingsinfo": { + "required": [ + "utsendingsmåter", + "varselSendt" + ], + "type": "object", + "properties": { + "varselSendt": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VarselSendt" + } + }, + "fysiskpostSendt": { + "$ref": "#/components/schemas/FysiskpostSendt" + }, + "digitalpostSendt": { + "$ref": "#/components/schemas/DigitalpostSendt" + }, + "utsendingsmåter": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "FYSISK_POST", + "DIGITAL_POST" + ] + } + } + } + }, + "VarselSendt": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "SMS", + "EPOST" + ] + }, + "varslingstidspunkt": { + "type": "string", + "format": "date-time" + } + } + }, + "Barn": { + "type": "object", + "properties": { + "barnFnr": { + "type": "string" + }, + "barnetrygdTom": { + "type": "string" + }, + "stønadstype": { + "type": "string" + } + } + }, + "Delytelse": { + "required": [ + "beløp", + "fom", + "typeDelytelse", + "typeUtbetaling" + ], + "type": "object", + "properties": { + "fom": { + "type": "string", + "format": "date" + }, + "tom": { + "type": "string", + "format": "date" + }, + "beløp": { + "type": "number", + "format": "double" + }, + "typeDelytelse": { + "type": "string" + }, + "oppgjørsordning": { + "type": "string" + }, + "typeUtbetaling": { + "type": "string" + } + } + }, + "RessursRestInfotrygdstønader": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/RestInfotrygdstønader" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestInfotrygdstønader": { + "required": [ + "harTilgang", + "stønader" + ], + "type": "object", + "properties": { + "stønader": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Stønad" + } + }, + "adressebeskyttelsegradering": { + "type": "string", + "enum": [ + "STRENGT_FORTROLIG_UTLAND", + "FORTROLIG", + "STRENGT_FORTROLIG", + "UGRADERT" + ] + }, + "harTilgang": { + "type": "boolean" + } + } + }, + "Stønad": { + "required": [ + "barn", + "delytelse" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "status": { + "type": "string" + }, + "tekstkode": { + "type": "string" + }, + "iverksattFom": { + "type": "string" + }, + "virkningFom": { + "type": "string" + }, + "barn": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Barn" + } + }, + "opphørtIver": { + "type": "string" + }, + "opphørtFom": { + "type": "string" + }, + "opphørsgrunn": { + "type": "string" + }, + "delytelse": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Delytelse" + } + }, + "antallBarn": { + "type": "integer", + "format": "int32" + }, + "mottakerNummer": { + "type": "integer", + "format": "int64" + } + } + }, + "RessursRestInfotrygdsaker": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/RestInfotrygdsaker" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestInfotrygdsaker": { + "required": [ + "harTilgang", + "saker" + ], + "type": "object", + "properties": { + "saker": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Sak" + } + }, + "adressebeskyttelsegradering": { + "type": "string", + "enum": [ + "STRENGT_FORTROLIG_UTLAND", + "FORTROLIG", + "STRENGT_FORTROLIG", + "UGRADERT" + ] + }, + "harTilgang": { + "type": "boolean" + } + } + }, + "RessursRestLøpendeSak": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/RestLøpendeSak" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestLøpendeSak": { + "required": [ + "harLøpendeSak" + ], + "type": "object", + "properties": { + "harLøpendeSak": { + "type": "boolean" + } + } + }, + "BehandlingerMedFeilIUtbetalingsoppdrag": { + "required": [ + "behandlinger", + "validerteUtbetalingsoppdrag" + ], + "type": "object", + "properties": { + "behandlinger": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "validerteUtbetalingsoppdrag": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidertUtbetalingsoppdrag" + } + } + } + }, + "Utbetalingsoppdrag": { + "required": [ + "aktoer", + "avstemmingTidspunkt", + "fagSystem", + "gomregning", + "kodeEndring", + "saksbehandlerId", + "saksnummer", + "utbetalingsperiode" + ], + "type": "object", + "properties": { + "kodeEndring": { + "type": "string", + "enum": [ + "NY", + "ENDR", + "UEND" + ] + }, + "fagSystem": { + "type": "string" + }, + "saksnummer": { + "type": "string" + }, + "aktoer": { + "type": "string" + }, + "saksbehandlerId": { + "type": "string" + }, + "avstemmingTidspunkt": { + "type": "string", + "format": "date-time" + }, + "utbetalingsperiode": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Utbetalingsperiode" + } + }, + "gOmregning": { + "type": "boolean", + "writeOnly": true + }, + "gomregning": { + "type": "boolean" + } + } + }, + "ValidertUtbetalingsoppdrag": { + "required": [ + "behandlingId", + "harKorrekteOpphørsdatoer" + ], + "type": "object", + "properties": { + "harKorrekteOpphørsdatoer": { + "type": "boolean" + }, + "behandlingId": { + "type": "integer", + "format": "int64" + }, + "utbetalingsperioderMedFeilOpphørsdato": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Utbetalingsperiode" + } + }, + "korrigerteUtbetalingsperioder": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Utbetalingsperiode" + } + }, + "gammeltUtbetalingsoppdrag": { + "$ref": "#/components/schemas/Utbetalingsoppdrag" + }, + "nyttUtbetalingsoppdrag": { + "$ref": "#/components/schemas/Utbetalingsoppdrag" + } + } + }, + "PairLongString": { + "required": [ + "first", + "second" + ], + "type": "object", + "properties": { + "first": { + "type": "integer", + "format": "int64" + }, + "second": { + "type": "string" + } + } + }, + "SendUtbetalingsoppdragPåNyttResponse": { + "required": [ + "harFeil", + "iverksattOk" + ], + "type": "object", + "properties": { + "iverksattOk": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "harFeil": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/PairLongString" + } + } + } + }, + "RessursMapStringBoolean": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "FagsakRequest": { + "required": [ + "personIdent" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + }, + "fagsakType": { + "type": "string", + "enum": [ + "NORMAL", + "BARN_ENSLIG_MINDREÅRIG", + "INSTITUSJON" + ] + }, + "institusjon": { + "$ref": "#/components/schemas/InstitusjonInfo" + } + } + }, + "RessursRestMinimalFagsak": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/RestMinimalFagsak" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestMinimalFagsak": { + "required": [ + "behandlinger", + "fagsakType", + "gjeldendeUtbetalingsperioder", + "id", + "opprettetTidspunkt", + "status", + "søkerFødselsnummer", + "tilbakekrevingsbehandlinger", + "underBehandling" + ], + "type": "object", + "properties": { + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "søkerFødselsnummer": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "OPPRETTET", + "LØPENDE", + "AVSLUTTET" + ] + }, + "løpendeKategori": { + "type": "string", + "enum": [ + "EØS", + "NASJONAL" + ] + }, + "løpendeUnderkategori": { + "type": "string", + "enum": [ + "UTVIDET", + "ORDINÆR" + ] + }, + "underBehandling": { + "type": "boolean" + }, + "gjeldendeUtbetalingsperioder": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Utbetalingsperiode" + } + }, + "behandlinger": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestVisningBehandling" + } + }, + "tilbakekrevingsbehandlinger": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestTilbakekrevingsbehandling" + } + }, + "migreringsdato": { + "type": "string", + "format": "date" + }, + "fagsakType": { + "type": "string", + "enum": [ + "NORMAL", + "BARN_ENSLIG_MINDREÅRIG", + "INSTITUSJON" + ] + }, + "institusjon": { + "$ref": "#/components/schemas/InstitusjonInfo" + } + } + }, + "RestTilbakekrevingsbehandling": { + "required": [ + "aktiv", + "behandlingId", + "opprettetTidspunkt", + "status", + "type" + ], + "type": "object", + "properties": { + "behandlingId": { + "type": "string", + "format": "uuid" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "aktiv": { + "type": "boolean" + }, + "getårsak": { + "type": "string", + "enum": [ + "REVURDERING_KLAGE_NFP", + "REVURDERING_KLAGE_KA", + "REVURDERING_OPPLYSNINGER_OM_VILKÅR", + "REVURDERING_OPPLYSNINGER_OM_FORELDELSE", + "REVURDERING_FEILUTBETALT_BELØP_HELT_ELLER_DELVIS_BORTFALT" + ] + }, + "type": { + "type": "string", + "enum": [ + "TILBAKEKREVING", + "REVURDERING_TILBAKEKREVING" + ] + }, + "status": { + "type": "string", + "enum": [ + "AVSLUTTET", + "FATTER_VEDTAK", + "IVERKSETTER_VEDTAK", + "OPPRETTET", + "UTREDES" + ] + }, + "resultat": { + "type": "string", + "enum": [ + "IKKE_FASTSATT", + "INGEN_TILBAKEBETALING", + "DELVIS_TILBAKEBETALING", + "FULL_TILBAKEBETALING", + "HENLAGT" + ] + }, + "vedtaksdato": { + "type": "string", + "format": "date-time" + } + } + }, + "RestVisningBehandling": { + "required": [ + "aktiv", + "aktivertTidspunkt", + "behandlingId", + "kategori", + "opprettetTidspunkt", + "resultat", + "status", + "type", + "underkategori" + ], + "type": "object", + "properties": { + "behandlingId": { + "type": "integer", + "format": "int64" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "aktivertTidspunkt": { + "type": "string", + "format": "date-time" + }, + "kategori": { + "type": "string", + "enum": [ + "EØS", + "NASJONAL" + ] + }, + "underkategori": { + "type": "string", + "enum": [ + "ORDINÆR", + "UTVIDET" + ] + }, + "aktiv": { + "type": "boolean" + }, + "getårsak": { + "type": "string", + "enum": [ + "SØKNAD", + "FØDSELSHENDELSE", + "ÅRLIG_KONTROLL", + "DØDSFALL_BRUKER", + "NYE_OPPLYSNINGER", + "KLAGE", + "TEKNISK_OPPHØR", + "TEKNISK_ENDRING", + "KORREKSJON_VEDTAKSBREV", + "OMREGNING_6ÅR", + "OMREGNING_18ÅR", + "OMREGNING_SMÅBARNSTILLEGG", + "SATSENDRING", + "SMÅBARNSTILLEGG", + "MIGRERING", + "ENDRE_MIGRERINGSDATO", + "HELMANUELL_MIGRERING" + ] + }, + "type": { + "type": "string", + "enum": [ + "FØRSTEGANGSBEHANDLING", + "REVURDERING", + "MIGRERING_FRA_INFOTRYGD", + "MIGRERING_FRA_INFOTRYGD_OPPHØRT", + "TEKNISK_OPPHØR", + "TEKNISK_ENDRING" + ] + }, + "status": { + "type": "string", + "enum": [ + "UTREDES", + "SATT_PÅ_VENT", + "SATT_PÅ_MASKINELL_VENT", + "FATTER_VEDTAK", + "IVERKSETTER_VEDTAK", + "AVSLUTTET" + ] + }, + "resultat": { + "type": "string", + "enum": [ + "INNVILGET", + "INNVILGET_OG_OPPHØRT", + "INNVILGET_OG_ENDRET", + "INNVILGET_ENDRET_OG_OPPHØRT", + "ENDRET_OG_FORTSATT_INNVILGET", + "DELVIS_INNVILGET", + "DELVIS_INNVILGET_OG_OPPHØRT", + "DELVIS_INNVILGET_OG_ENDRET", + "DELVIS_INNVILGET_ENDRET_OG_OPPHØRT", + "AVSLÅTT", + "AVSLÅTT_OG_OPPHØRT", + "AVSLÅTT_OG_ENDRET", + "AVSLÅTT_ENDRET_OG_OPPHØRT", + "ENDRET_UTBETALING", + "ENDRET_UTEN_UTBETALING", + "ENDRET_OG_OPPHØRT", + "OPPHØRT", + "FORTSATT_OPPHØRT", + "FORTSATT_INNVILGET", + "HENLAGT_FEILAKTIG_OPPRETTET", + "HENLAGT_SØKNAD_TRUKKET", + "HENLAGT_AUTOMATISK_FØDSELSHENDELSE", + "HENLAGT_TEKNISK_VEDLIKEHOLD", + "IKKE_VURDERT" + ] + }, + "vedtaksdato": { + "type": "string", + "format": "date-time" + } + } + }, + "OpprettKlageDto": { + "required": [ + "kravMottattDato" + ], + "type": "object", + "properties": { + "kravMottattDato": { + "type": "string", + "format": "date" + } + } + }, + "RessursLong": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "integer", + "format": "int64" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestSøkParam": { + "required": [ + "barnasIdenter", + "personIdent" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + }, + "barnasIdenter": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "RessursListRestFagsakDeltager": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestFagsakDeltager" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestFagsakDeltager": { + "required": [ + "harTilgang", + "ident", + "rolle" + ], + "type": "object", + "properties": { + "navn": { + "type": "string" + }, + "ident": { + "type": "string" + }, + "rolle": { + "type": "string", + "enum": [ + "BARN", + "FORELDER", + "UKJENT" + ] + }, + "fagsakType": { + "type": "string", + "enum": [ + "NORMAL", + "BARN_ENSLIG_MINDREÅRIG", + "INSTITUSJON" + ] + }, + "kjønn": { + "type": "string", + "enum": [ + "MANN", + "KVINNE", + "UKJENT" + ] + }, + "fagsakId": { + "type": "integer", + "format": "int64" + }, + "fagsakStatus": { + "type": "string", + "enum": [ + "OPPRETTET", + "LØPENDE", + "AVSLUTTET" + ] + }, + "adressebeskyttelseGradering": { + "type": "string", + "enum": [ + "STRENGT_FORTROLIG_UTLAND", + "FORTROLIG", + "STRENGT_FORTROLIG", + "UGRADERT" + ] + }, + "harTilgang": { + "type": "boolean" + } + } + }, + "RestSøkFagsakRequest": { + "required": [ + "personIdent" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + } + } + }, + "RessursListRestFagsakIdOgTilknyttetAktørId": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestFagsakIdOgTilknyttetAktørId" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestFagsakIdOgTilknyttetAktørId": { + "required": [ + "aktørId", + "fagsakId" + ], + "type": "object", + "properties": { + "aktørId": { + "type": "string" + }, + "fagsakId": { + "type": "integer", + "format": "int64" + } + } + }, + "RestHentFagsakerForPerson": { + "required": [ + "fagsakTyper", + "personIdent" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + }, + "fagsakTyper": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "NORMAL", + "BARN_ENSLIG_MINDREÅRIG", + "INSTITUSJON" + ] + } + } + } + }, + "RessursListRestMinimalFagsak": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestMinimalFagsak" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestHentFagsakForPerson": { + "required": [ + "fagsakType", + "personIdent" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + }, + "fagsakType": { + "type": "string", + "enum": [ + "NORMAL", + "BARN_ENSLIG_MINDREÅRIG", + "INSTITUSJON" + ] + } + } + }, + "BarnetrygdTilPensjonRequest": { + "required": [ + "fraDato", + "ident" + ], + "type": "object", + "properties": { + "ident": { + "type": "string" + }, + "fraDato": { + "type": "string", + "example": "2020-12-01" + } + } + }, + "Ressurs": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "object" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "BarnetrygdPeriode": { + "required": [ + "delingsprosentYtelse", + "personIdent", + "stønadFom", + "stønadTom", + "utbetaltPerMnd" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + }, + "delingsprosentYtelse": { + "type": "integer", + "format": "int32" + }, + "ytelseTypeEkstern": { + "type": "string", + "enum": [ + "ORDINÆR_BARNETRYGD", + "UTVIDET_BARNETRYGD", + "SMÅBARNSTILLEGG" + ] + }, + "utbetaltPerMnd": { + "type": "integer", + "format": "int32" + }, + "stønadFom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + }, + "stønadTom": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "month": { + "type": "string", + "enum": [ + "JANUARY", + "FEBRUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER" + ] + }, + "monthValue": { + "type": "integer", + "format": "int32" + }, + "leapYear": { + "type": "boolean" + } + } + } + } + }, + "BarnetrygdTilPensjon": { + "required": [ + "barnetrygdPerioder", + "fagsakEiersIdent", + "fagsakId" + ], + "type": "object", + "properties": { + "fagsakId": { + "type": "string" + }, + "fagsakEiersIdent": { + "type": "string" + }, + "barnetrygdPerioder": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BarnetrygdPeriode" + } + } + } + }, + "Enhet": { + "required": [ + "enhetId", + "enhetNavn" + ], + "type": "object", + "properties": { + "enhetId": { + "type": "string" + }, + "enhetNavn": { + "type": "string" + } + } + }, + "ManueltBrevRequest": { + "required": [ + "barnIBrev", + "brevmal", + "mottakerIdent", + "mottakerMålform", + "mottakerNavn", + "mottakerlandSed", + "multiselectVerdier" + ], + "type": "object", + "properties": { + "brevmal": { + "type": "string", + "enum": [ + "INFORMASJONSBREV_DELT_BOSTED", + "INNHENTE_OPPLYSNINGER", + "INNHENTE_OPPLYSNINGER_ETTER_SØKNAD_I_SED", + "INNHENTE_OPPLYSNINGER_OG_INFORMASJON_OM_AT_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_HAR_SØKT", + "INNHENTE_OPPLYSNINGER_INSTITUSJON", + "HENLEGGE_TRUKKET_SØKNAD", + "VARSEL_OM_REVURDERING", + "VARSEL_OM_REVURDERING_INSTITUSJON", + "VARSEL_OM_REVURDERING_DELT_BOSTED_PARAGRAF_14", + "VARSEL_OM_REVURDERING_SAMBOER", + "VARSEL_OM_VEDTAK_ETTER_SØKNAD_I_SED", + "VARSEL_OM_REVURDERING_FRA_NASJONAL_TIL_EØS", + "VARSEL_ANNEN_FORELDER_MED_SELVSTENDIG_RETT_SØKT", + "VARSEL_OM_ÅRLIG_REVURDERING_EØS", + "VARSEL_OM_ÅRLIG_REVURDERING_EØS_MED_INNHENTING_AV_OPPLYSNINGER", + "SVARTIDSBREV", + "SVARTIDSBREV_INSTITUSJON", + "FORLENGET_SVARTIDSBREV", + "FORLENGET_SVARTIDSBREV_INSTITUSJON", + "INFORMASJONSBREV_FØDSEL_MINDREÅRIG", + "INFORMASJONSBREV_FØDSEL_VERGEMÅL", + "INFORMASJONSBREV_KAN_SØKE", + "INFORMASJONSBREV_KAN_SØKE_EØS", + "INFORMASJONSBREV_FØDSEL_GENERELL", + "INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_FÅTT_EN_SØKNAD_FRA_ANNEN_FORELDER", + "INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_HAR_GJORT_VEDTAK_TIL_ANNEN_FORELDER", + "INFORMASJONSBREV_TIL_FORELDER_OMFATTET_NORSK_LOVGIVNING_VARSEL_OM_ÅRLIG_KONTROLL", + "INFORMASJONSBREV_TIL_FORELDER_MED_SELVSTENDIG_RETT_VI_HAR_FÅTT_F016_KAN_SØKE_OM_BARNETRYGD", + "VEDTAK_FØRSTEGANGSVEDTAK", + "VEDTAK_ENDRING", + "VEDTAK_OPPHØRT", + "VEDTAK_OPPHØR_MED_ENDRING", + "VEDTAK_AVSLAG", + "VEDTAK_FORTSATT_INNVILGET", + "VEDTAK_KORREKSJON_VEDTAKSBREV", + "VEDTAK_OPPHØR_DØDSFALL", + "VEDTAK_FØRSTEGANGSVEDTAK_INSTITUSJON", + "VEDTAK_ENDRING_INSTITUSJON", + "VEDTAK_OPPHØRT_INSTITUSJON", + "VEDTAK_OPPHØR_MED_ENDRING_INSTITUSJON", + "VEDTAK_AVSLAG_INSTITUSJON", + "VEDTAK_FORTSATT_INNVILGET_INSTITUSJON", + "AUTOVEDTAK_BARN_6_OG_18_ÅR_OG_SMÅBARNSTILLEGG", + "AUTOVEDTAK_NYFØDT_FØRSTE_BARN", + "AUTOVEDTAK_NYFØDT_BARN_FRA_FØR" + ] + }, + "multiselectVerdier": { + "type": "array", + "items": { + "type": "string" + } + }, + "mottakerIdent": { + "type": "string" + }, + "barnIBrev": { + "type": "array", + "items": { + "type": "string" + } + }, + "datoAvtale": { + "type": "string" + }, + "mottakerMålform": { + "type": "string", + "enum": [ + "NB", + "NN" + ] + }, + "mottakerNavn": { + "type": "string" + }, + "enhet": { + "$ref": "#/components/schemas/Enhet" + }, + "antallUkerSvarfrist": { + "type": "integer", + "format": "int32" + }, + "barnasFødselsdager": { + "type": "array", + "items": { + "type": "string", + "format": "date" + } + }, + "behandlingKategori": { + "type": "string", + "enum": [ + "EØS", + "NASJONAL" + ] + }, + "vedrørende": { + "$ref": "#/components/schemas/Person" + }, + "mottakerlandSed": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Person": { + "required": [ + "fødselsnummer", + "navn" + ], + "type": "object", + "properties": { + "fødselsnummer": { + "type": "string" + }, + "navn": { + "type": "string" + } + } + }, + "BisysUtvidetBarnetrygdRequest": { + "required": [ + "fraDato", + "personIdent" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + }, + "fraDato": { + "type": "string", + "example": "2020-12-01" + } + } + }, + "UtvidetBarnetrygdPeriode": { + "required": [ + "beløp", + "fomMåned", + "manueltBeregnet", + "stønadstype" + ], + "type": "object", + "properties": { + "stønadstype": { + "type": "string", + "enum": [ + "UTVIDET", + "SMÅBARNSTILLEGG" + ] + }, + "fomMåned": { + "type": "string", + "example": "2020-12" + }, + "tomMåned": { + "type": "string", + "example": "2020-12" + }, + "beløp": { + "type": "number", + "format": "double" + }, + "manueltBeregnet": { + "type": "boolean" + }, + "deltBosted": { + "type": "boolean" + } + } + }, + "NyBehandling": { + "required": [ + "barnasIdenter", + "behandlingType", + "behandlingÅrsak", + "fagsakId", + "skalBehandlesAutomatisk", + "søkersIdent" + ], + "type": "object", + "properties": { + "kategori": { + "type": "string", + "enum": [ + "EØS", + "NASJONAL" + ] + }, + "underkategori": { + "type": "string", + "enum": [ + "UTVIDET", + "ORDINÆR" + ] + }, + "søkersIdent": { + "type": "string" + }, + "behandlingType": { + "type": "string", + "enum": [ + "FØRSTEGANGSBEHANDLING", + "REVURDERING", + "MIGRERING_FRA_INFOTRYGD", + "MIGRERING_FRA_INFOTRYGD_OPPHØRT", + "TEKNISK_OPPHØR", + "TEKNISK_ENDRING" + ] + }, + "behandlingÅrsak": { + "type": "string", + "enum": [ + "SØKNAD", + "FØDSELSHENDELSE", + "ÅRLIG_KONTROLL", + "DØDSFALL_BRUKER", + "NYE_OPPLYSNINGER", + "KLAGE", + "TEKNISK_OPPHØR", + "TEKNISK_ENDRING", + "KORREKSJON_VEDTAKSBREV", + "OMREGNING_6ÅR", + "OMREGNING_18ÅR", + "OMREGNING_SMÅBARNSTILLEGG", + "SATSENDRING", + "SMÅBARNSTILLEGG", + "MIGRERING", + "ENDRE_MIGRERINGSDATO", + "HELMANUELL_MIGRERING" + ] + }, + "skalBehandlesAutomatisk": { + "type": "boolean" + }, + "navIdent": { + "type": "string" + }, + "barnasIdenter": { + "type": "array", + "items": { + "type": "string" + } + }, + "nyMigreringsdato": { + "type": "string", + "format": "date" + }, + "søknadMottattDato": { + "type": "string", + "format": "date" + }, + "søknadsinfo": { + "$ref": "#/components/schemas/Søknadsinfo" + }, + "fagsakId": { + "type": "integer", + "format": "int64" + } + } + }, + "Søknadsinfo": { + "required": [ + "brevkode", + "erDigital", + "journalpostId" + ], + "type": "object", + "properties": { + "journalpostId": { + "type": "string" + }, + "brevkode": { + "type": "string" + }, + "erDigital": { + "type": "boolean" + } + } + }, + "RestRegistrerSøknad": { + "required": [ + "bekreftEndringerViaFrontend", + "søknad" + ], + "type": "object", + "properties": { + "søknad": { + "$ref": "#/components/schemas/SøknadDTO" + }, + "bekreftEndringerViaFrontend": { + "type": "boolean" + } + } + }, + "RestRegistrerInstitusjonOgVerge": { + "type": "object", + "properties": { + "vergeInfo": { + "$ref": "#/components/schemas/VergeInfo" + }, + "institusjonInfo": { + "$ref": "#/components/schemas/InstitusjonInfo" + } + } + }, + "RestBeslutningPåVedtak": { + "required": [ + "beslutning", + "kontrollerteSider" + ], + "type": "object", + "properties": { + "beslutning": { + "type": "string", + "enum": [ + "GODKJENT", + "UNDERKJENT" + ] + }, + "begrunnelse": { + "type": "string" + }, + "kontrollerteSider": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "LeggTilBarnDto": { + "required": [ + "barnIdent" + ], + "type": "object", + "properties": { + "barnIdent": { + "type": "string" + } + } + }, + "Aktør": { + "required": [ + "aktørId", + "endretAv", + "endretTidspunkt", + "opprettetAv", + "opprettetTidspunkt", + "personidenter", + "versjon" + ], + "type": "object", + "properties": { + "aktørId": { + "pattern": "^\\d{13}$", + "type": "string" + }, + "personidenter": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/Personident" + } + }, + "opprettetAv": { + "type": "string" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "endretAv": { + "type": "string" + }, + "endretTidspunkt": { + "type": "string", + "format": "date-time" + }, + "versjon": { + "type": "integer", + "format": "int64" + } + } + }, + "Behandling": { + "required": [ + "aktiv", + "aktivertTidspunkt", + "behandlingStegTilstand", + "endretAv", + "endretTidspunkt", + "fagsak", + "id", + "kategori", + "opprettetAv", + "opprettetTidspunkt", + "opprettetÅrsak", + "resultat", + "skalBehandlesAutomatisk", + "status", + "steg", + "type", + "underkategori", + "versjon" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "fagsak": { + "$ref": "#/components/schemas/Fagsak" + }, + "behandlingStegTilstand": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/BehandlingStegTilstand" + } + }, + "resultat": { + "type": "string", + "enum": [ + "INNVILGET", + "INNVILGET_OG_OPPHØRT", + "INNVILGET_OG_ENDRET", + "INNVILGET_ENDRET_OG_OPPHØRT", + "ENDRET_OG_FORTSATT_INNVILGET", + "DELVIS_INNVILGET", + "DELVIS_INNVILGET_OG_OPPHØRT", + "DELVIS_INNVILGET_OG_ENDRET", + "DELVIS_INNVILGET_ENDRET_OG_OPPHØRT", + "AVSLÅTT", + "AVSLÅTT_OG_OPPHØRT", + "AVSLÅTT_OG_ENDRET", + "AVSLÅTT_ENDRET_OG_OPPHØRT", + "ENDRET_UTBETALING", + "ENDRET_UTEN_UTBETALING", + "ENDRET_OG_OPPHØRT", + "OPPHØRT", + "FORTSATT_OPPHØRT", + "FORTSATT_INNVILGET", + "HENLAGT_FEILAKTIG_OPPRETTET", + "HENLAGT_SØKNAD_TRUKKET", + "HENLAGT_AUTOMATISK_FØDSELSHENDELSE", + "HENLAGT_TEKNISK_VEDLIKEHOLD", + "IKKE_VURDERT" + ] + }, + "type": { + "type": "string", + "enum": [ + "FØRSTEGANGSBEHANDLING", + "REVURDERING", + "MIGRERING_FRA_INFOTRYGD", + "MIGRERING_FRA_INFOTRYGD_OPPHØRT", + "TEKNISK_OPPHØR", + "TEKNISK_ENDRING" + ] + }, + "opprettetÅrsak": { + "type": "string", + "enum": [ + "SØKNAD", + "FØDSELSHENDELSE", + "ÅRLIG_KONTROLL", + "DØDSFALL_BRUKER", + "NYE_OPPLYSNINGER", + "KLAGE", + "TEKNISK_OPPHØR", + "TEKNISK_ENDRING", + "KORREKSJON_VEDTAKSBREV", + "OMREGNING_6ÅR", + "OMREGNING_18ÅR", + "OMREGNING_SMÅBARNSTILLEGG", + "SATSENDRING", + "SMÅBARNSTILLEGG", + "MIGRERING", + "ENDRE_MIGRERINGSDATO", + "HELMANUELL_MIGRERING" + ] + }, + "skalBehandlesAutomatisk": { + "type": "boolean" + }, + "kategori": { + "type": "string", + "enum": [ + "EØS", + "NASJONAL" + ] + }, + "underkategori": { + "type": "string", + "enum": [ + "UTVIDET", + "ORDINÆR" + ] + }, + "aktiv": { + "type": "boolean" + }, + "status": { + "type": "string", + "enum": [ + "UTREDES", + "SATT_PÅ_VENT", + "SATT_PÅ_MASKINELL_VENT", + "FATTER_VEDTAK", + "IVERKSETTER_VEDTAK", + "AVSLUTTET" + ] + }, + "overstyrtEndringstidspunkt": { + "type": "string", + "format": "date" + }, + "verge": { + "$ref": "#/components/schemas/Verge" + }, + "aktivertTidspunkt": { + "type": "string", + "format": "date-time" + }, + "opprettetAv": { + "type": "string" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "endretAv": { + "type": "string" + }, + "endretTidspunkt": { + "type": "string", + "format": "date-time" + }, + "versjon": { + "type": "integer", + "format": "int64" + }, + "steg": { + "type": "string", + "enum": [ + "HENLEGG_BEHANDLING", + "REGISTRERE_INSTITUSJON_OG_VERGE", + "REGISTRERE_PERSONGRUNNLAG", + "REGISTRERE_SØKNAD", + "FILTRERING_FØDSELSHENDELSER", + "VILKÅRSVURDERING", + "BEHANDLINGSRESULTAT", + "VURDER_TILBAKEKREVING", + "SEND_TIL_BESLUTTER", + "BESLUTTE_VEDTAK", + "IVERKSETT_MOT_OPPDRAG", + "VENTE_PÅ_STATUS_FRA_ØKONOMI", + "IVERKSETT_MOT_FAMILIE_TILBAKE", + "JOURNALFØR_VEDTAKSBREV", + "DISTRIBUER_VEDTAKSBREV", + "FERDIGSTILLE_BEHANDLING", + "BEHANDLING_AVSLUTTET" + ] + } + } + }, + "BehandlingStegTilstand": { + "required": [ + "behandlingSteg", + "behandlingStegStatus", + "endretAv", + "endretTidspunkt", + "id", + "opprettetAv", + "opprettetTidspunkt", + "versjon" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "behandlingSteg": { + "type": "string", + "enum": [ + "HENLEGG_BEHANDLING", + "REGISTRERE_INSTITUSJON_OG_VERGE", + "REGISTRERE_PERSONGRUNNLAG", + "REGISTRERE_SØKNAD", + "FILTRERING_FØDSELSHENDELSER", + "VILKÅRSVURDERING", + "BEHANDLINGSRESULTAT", + "VURDER_TILBAKEKREVING", + "SEND_TIL_BESLUTTER", + "BESLUTTE_VEDTAK", + "IVERKSETT_MOT_OPPDRAG", + "VENTE_PÅ_STATUS_FRA_ØKONOMI", + "IVERKSETT_MOT_FAMILIE_TILBAKE", + "JOURNALFØR_VEDTAKSBREV", + "DISTRIBUER_VEDTAKSBREV", + "FERDIGSTILLE_BEHANDLING", + "BEHANDLING_AVSLUTTET" + ] + }, + "behandlingStegStatus": { + "type": "string", + "enum": [ + "IKKE_UTFØRT", + "UTFØRT" + ] + }, + "opprettetAv": { + "type": "string" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "endretAv": { + "type": "string" + }, + "endretTidspunkt": { + "type": "string", + "format": "date-time" + }, + "versjon": { + "type": "integer", + "format": "int64" + } + } + }, + "Fagsak": { + "required": [ + "aktør", + "arkivert", + "endretAv", + "endretTidspunkt", + "id", + "opprettetAv", + "opprettetTidspunkt", + "status", + "type", + "versjon" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "aktør": { + "$ref": "#/components/schemas/Aktør" + }, + "institusjon": { + "$ref": "#/components/schemas/Institusjon" + }, + "status": { + "type": "string", + "enum": [ + "OPPRETTET", + "LØPENDE", + "AVSLUTTET" + ] + }, + "type": { + "type": "string", + "enum": [ + "NORMAL", + "BARN_ENSLIG_MINDREÅRIG", + "INSTITUSJON" + ] + }, + "arkivert": { + "type": "boolean" + }, + "opprettetAv": { + "type": "string" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "endretAv": { + "type": "string" + }, + "endretTidspunkt": { + "type": "string", + "format": "date-time" + }, + "versjon": { + "type": "integer", + "format": "int64" + } + } + }, + "Institusjon": { + "required": [ + "endretAv", + "endretTidspunkt", + "id", + "opprettetAv", + "opprettetTidspunkt", + "orgNummer", + "versjon" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "orgNummer": { + "type": "string" + }, + "tssEksternId": { + "type": "string" + }, + "opprettetAv": { + "type": "string" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "endretAv": { + "type": "string" + }, + "endretTidspunkt": { + "type": "string", + "format": "date-time" + }, + "versjon": { + "type": "integer", + "format": "int64" + } + } + }, + "Verge": { + "required": [ + "behandling", + "endretAv", + "endretTidspunkt", + "id", + "ident", + "opprettetAv", + "opprettetTidspunkt", + "versjon" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "ident": { + "type": "string" + }, + "behandling": { + "$ref": "#/components/schemas/Behandling" + }, + "opprettetAv": { + "type": "string" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "endretAv": { + "type": "string" + }, + "endretTidspunkt": { + "type": "string", + "format": "date-time" + }, + "versjon": { + "type": "integer", + "format": "int64" + } + } + }, + "ØkonomiSimuleringMottaker": { + "required": [ + "behandling", + "endretAv", + "endretTidspunkt", + "getøkonomiSimuleringPostering", + "id", + "mottakerType", + "opprettetAv", + "opprettetTidspunkt", + "versjon" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "mottakerNummer": { + "type": "string" + }, + "mottakerType": { + "type": "string", + "enum": [ + "BRUKER", + "ARBG_ORG", + "ARBG_PRIV" + ] + }, + "behandling": { + "$ref": "#/components/schemas/Behandling" + }, + "getøkonomiSimuleringPostering": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ØkonomiSimuleringPostering" + } + }, + "opprettetAv": { + "type": "string" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "endretAv": { + "type": "string" + }, + "endretTidspunkt": { + "type": "string", + "format": "date-time" + }, + "versjon": { + "type": "integer", + "format": "int64" + } + } + }, + "ØkonomiSimuleringPostering": { + "required": [ + "beløp", + "betalingType", + "endretAv", + "endretTidspunkt", + "erManuellPostering", + "fagOmrådeKode", + "fom", + "forfallsdato", + "getøkonomiSimuleringMottaker", + "id", + "opprettetAv", + "opprettetTidspunkt", + "posteringType", + "tom", + "utenInntrekk", + "versjon" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "getøkonomiSimuleringMottaker": { + "type": "integer", + "format": "int64" + }, + "fagOmrådeKode": { + "type": "string", + "enum": [ + "BARNETRYGD", + "BARNETRYGD_MANUELT", + "BARNETRYGD_INFOTRYGD", + "BARNETRYGD_INFOTRYGD_MANUELT", + "ENSLIG_FORSØRGER_OVERGANGSSTØNAD", + "ENSLIG_FORSØRGER_OVERGANGSSTØNAD_INFOTRYGD", + "ENSLIG_FORSØRGER_OVERGANGSSTØNAD_MANUELL_POSTERING", + "ENSLIG_FORSØRGER_SKOLEPENGER", + "ENSLIG_FORSØRGER_SKOLEPENGER_INFOTRYGD", + "ENSLIG_FORSØRGER_BARNETILSYN", + "ENSLIG_FORSØRGER_BARNETILSYN_INFOTRYGD", + "TILBAKEKREVING_EF_MANUELL_POSTERING", + "KONTANTSTØTTE", + "KONTANTSTØTTE_INFOTRYGD" + ] + }, + "fom": { + "type": "string", + "format": "date" + }, + "tom": { + "type": "string", + "format": "date" + }, + "betalingType": { + "type": "string", + "enum": [ + "DEBIT", + "KREDIT" + ] + }, + "beløp": { + "type": "number" + }, + "posteringType": { + "type": "string", + "enum": [ + "YTELSE", + "FEILUTBETALING", + "FORSKUDSSKATT", + "JUSTERING", + "TREKK", + "MOTP" + ] + }, + "forfallsdato": { + "type": "string", + "format": "date" + }, + "utenInntrekk": { + "type": "boolean" + }, + "opprettetAv": { + "type": "string" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "endretAv": { + "type": "string" + }, + "endretTidspunkt": { + "type": "string", + "format": "date-time" + }, + "versjon": { + "type": "integer", + "format": "int64" + }, + "erManuellPostering": { + "type": "boolean" + } + } + }, + "RessursMapVedtakBegrunnelseTypeListRestVedtakBegrunnelseTilknyttetVilkår": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestVedtakBegrunnelseTilknyttetVilkår" + } + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestVedtakBegrunnelseTilknyttetVilkår": { + "required": [ + "id", + "navn" + ], + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "navn": { + "type": "string" + }, + "vilkår": { + "type": "string", + "enum": [ + "UNDER_18_ÅR", + "BOR_MED_SØKER", + "GIFT_PARTNERSKAP", + "BOSATT_I_RIKET", + "LOVLIG_OPPHOLD", + "UTVIDET_BARNETRYGD" + ] + } + } + }, + "RessursSetString": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RessursRestTidslinjer": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/RestTidslinjer" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestTidslinjePeriodeRegelverk": { + "required": [ + "fraOgMed", + "innhold" + ], + "type": "object", + "properties": { + "fraOgMed": { + "type": "string", + "format": "date" + }, + "tilOgMed": { + "type": "string", + "format": "date" + }, + "innhold": { + "type": "string", + "enum": [ + "NASJONALE_REGLER", + "EØS_FORORDNINGEN" + ] + } + } + }, + "RestTidslinjePeriodeResultat": { + "required": [ + "fraOgMed", + "innhold" + ], + "type": "object", + "properties": { + "fraOgMed": { + "type": "string", + "format": "date" + }, + "tilOgMed": { + "type": "string", + "format": "date" + }, + "innhold": { + "type": "string", + "enum": [ + "OPPFYLT", + "IKKE_OPPFYLT", + "IKKE_VURDERT" + ] + } + } + }, + "RestTidslinjePeriodeVilkårRegelverkResultat": { + "required": [ + "fraOgMed", + "innhold" + ], + "type": "object", + "properties": { + "fraOgMed": { + "type": "string", + "format": "date" + }, + "tilOgMed": { + "type": "string", + "format": "date" + }, + "innhold": { + "$ref": "#/components/schemas/VilkårRegelverkResultat" + } + } + }, + "RestTidslinjer": { + "required": [ + "barnasTidslinjer", + "søkersTidslinjer" + ], + "type": "object", + "properties": { + "barnasTidslinjer": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/RestTidslinjerForBarn" + } + }, + "søkersTidslinjer": { + "$ref": "#/components/schemas/RestTidslinjerForSøker" + } + } + }, + "RestTidslinjerForBarn": { + "required": [ + "oppfyllerEgneVilkårIKombinasjonMedSøkerTidslinje", + "regelverkTidslinje", + "vilkårTidslinjer" + ], + "type": "object", + "properties": { + "vilkårTidslinjer": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestTidslinjePeriodeVilkårRegelverkResultat" + } + } + }, + "oppfyllerEgneVilkårIKombinasjonMedSøkerTidslinje": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestTidslinjePeriodeResultat" + } + }, + "regelverkTidslinje": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestTidslinjePeriodeRegelverk" + } + } + } + }, + "RestTidslinjerForSøker": { + "required": [ + "oppfyllerEgneVilkårTidslinje", + "vilkårTidslinjer" + ], + "type": "object", + "properties": { + "vilkårTidslinjer": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestTidslinjePeriodeVilkårRegelverkResultat" + } + } + }, + "oppfyllerEgneVilkårTidslinje": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestTidslinjePeriodeResultat" + } + } + } + }, + "VilkårRegelverkResultat": { + "required": [ + "regelverkResultat", + "utdypendeVilkårsvurderinger", + "vilkår" + ], + "type": "object", + "properties": { + "vilkår": { + "type": "string", + "enum": [ + "UNDER_18_ÅR", + "BOR_MED_SØKER", + "GIFT_PARTNERSKAP", + "BOSATT_I_RIKET", + "LOVLIG_OPPHOLD", + "UTVIDET_BARNETRYGD" + ] + }, + "regelverkResultat": { + "type": "string", + "enum": [ + "OPPFYLT_EØS_FORORDNINGEN", + "OPPFYLT_NASJONALE_REGLER", + "OPPFYLT_REGELVERK_IKKE_SATT", + "OPPFYLT_BLANDET_REGELVERK", + "IKKE_OPPFYLT", + "IKKE_FULLT_VURDERT" + ] + }, + "utdypendeVilkårsvurderinger": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "VURDERING_ANNET_GRUNNLAG", + "VURDERT_MEDLEMSKAP", + "DELT_BOSTED", + "DELT_BOSTED_SKAL_IKKE_DELES", + "OMFATTET_AV_NORSK_LOVGIVNING", + "OMFATTET_AV_NORSK_LOVGIVNING_UTLAND", + "ANNEN_FORELDER_OMFATTET_AV_NORSK_LOVGIVNING", + "BARN_BOR_I_NORGE", + "BARN_BOR_I_EØS", + "BARN_BOR_I_STORBRITANNIA", + "BARN_BOR_I_NORGE_MED_SØKER", + "BARN_BOR_I_EØS_MED_SØKER", + "BARN_BOR_I_EØS_MED_ANNEN_FORELDER", + "BARN_BOR_I_STORBRITANNIA_MED_SØKER", + "BARN_BOR_I_STORBRITANNIA_MED_ANNEN_FORELDER", + "BARN_BOR_ALENE_I_ANNET_EØS_LAND" + ] + } + }, + "resultat": { + "type": "string", + "enum": [ + "OPPFYLT", + "IKKE_OPPFYLT", + "IKKE_VURDERT" + ] + }, + "regelverk": { + "type": "string", + "enum": [ + "NASJONALE_REGLER", + "EØS_FORORDNINGEN" + ] + } + } + }, + "RessursTaskDto": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/TaskDto" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + } + } + }, + "TaskDto": { + "required": [ + "antallLogger", + "callId", + "id", + "metadata", + "opprettetTidspunkt", + "payload", + "status", + "taskStepType" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "status": { + "type": "string", + "enum": [ + "FERDIG", + "FEILET", + "PLUKKET", + "BEHANDLER", + "KLAR_TIL_PLUKK", + "UBEHANDLET", + "AVVIKSHÅNDTERT", + "MANUELL_OPPFØLGING" + ] + }, + "avvikstype": { + "type": "string", + "enum": [ + "ANNET", + "DUPLIKAT" + ] + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "triggerTid": { + "type": "string", + "format": "date-time" + }, + "taskStepType": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "payload": { + "type": "string" + }, + "antallLogger": { + "type": "integer", + "format": "int32" + }, + "sistKjørt": { + "type": "string", + "format": "date-time" + }, + "kommentar": { + "type": "string" + }, + "callId": { + "type": "string" + } + } + }, + "PaginableResponseTaskDto": { + "required": [ + "tasks" + ], + "type": "object", + "properties": { + "tasks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskDto" + } + } + } + }, + "RessursPaginableResponseTaskDto": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/PaginableResponseTaskDto" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + } + } + }, + "RessursListTaskloggDto": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskloggDto" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + } + } + }, + "TaskloggDto": { + "required": [ + "id", + "node", + "opprettetTidspunkt", + "type" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "endretAv": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "FERDIG", + "FEILET", + "PLUKKET", + "BEHANDLER", + "KLAR_TIL_PLUKK", + "UBEHANDLET", + "AVVIKSHÅNDTERT", + "MANUELL_OPPFØLGING", + "KOMMENTAR" + ] + }, + "node": { + "type": "string" + }, + "melding": { + "type": "string" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + } + } + }, + "RessursSkatteetatenPersonerResponse": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SkatteetatenPersonerResponse" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "SkatteetatenPerson": { + "required": [ + "ident", + "sisteVedtakPaaIdent" + ], + "type": "object", + "properties": { + "ident": { + "type": "string" + }, + "sisteVedtakPaaIdent": { + "type": "string", + "format": "date-time" + } + } + }, + "SkatteetatenPersonerResponse": { + "required": [ + "brukere" + ], + "type": "object", + "properties": { + "brukere": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SkatteetatenPerson" + } + } + } + }, + "RessursBoolean": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "boolean" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RessursSamhandlerInfo": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SamhandlerInfo" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "AktørDVH": { + "required": [ + "aktorId", + "rolle" + ], + "type": "object", + "properties": { + "aktorId": { + "type": "integer", + "format": "int64" + }, + "rolle": { + "type": "string" + }, + "rolleBeskrivelse": { + "type": "string" + } + } + }, + "SakDVH": { + "required": [ + "aktorId", + "avsender", + "bostedsland", + "funksjonellId", + "funksjonellTid", + "opprettetDato", + "sakId", + "sakStatus", + "tekniskTid", + "versjon", + "ytelseType" + ], + "type": "object", + "properties": { + "funksjonellTid": { + "type": "string", + "format": "date-time" + }, + "tekniskTid": { + "type": "string", + "format": "date-time" + }, + "opprettetDato": { + "type": "string", + "format": "date" + }, + "funksjonellId": { + "type": "string" + }, + "sakId": { + "type": "string" + }, + "aktorId": { + "type": "integer", + "format": "int64" + }, + "bostedsland": { + "type": "string" + }, + "aktorer": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AktørDVH" + } + }, + "sakStatus": { + "type": "string" + }, + "avsender": { + "type": "string" + }, + "versjon": { + "type": "string" + }, + "ytelseType": { + "type": "string" + } + } + }, + "BehandlingDVH": { + "required": [ + "ansvarligEnhetKode", + "ansvarligEnhetType", + "automatiskBehandlet", + "avsender", + "behandlendeEnhetKode", + "behandlendeEnhetType", + "behandlingAarsak", + "behandlingId", + "behandlingKategori", + "behandlingStatus", + "behandlingType", + "funksjonellId", + "funksjonellTid", + "mottattDato", + "registrertDato", + "resultatBegrunnelser", + "sakId", + "tekniskTid", + "totrinnsbehandling", + "utenlandstilsnitt", + "versjon" + ], + "type": "object", + "properties": { + "funksjonellTid": { + "type": "string", + "format": "date-time" + }, + "tekniskTid": { + "type": "string", + "format": "date-time" + }, + "mottattDato": { + "type": "string", + "format": "date-time" + }, + "registrertDato": { + "type": "string", + "format": "date-time" + }, + "vedtaksDato": { + "type": "string", + "format": "date" + }, + "funksjonellId": { + "type": "string" + }, + "behandlingId": { + "type": "string" + }, + "relatertBehandlingId": { + "type": "string" + }, + "sakId": { + "type": "string" + }, + "vedtakId": { + "type": "string" + }, + "behandlingType": { + "type": "string" + }, + "behandlingStatus": { + "type": "string" + }, + "behandlingKategori": { + "type": "string" + }, + "behandlingUnderkategori": { + "type": "string" + }, + "behandlingAarsak": { + "type": "string" + }, + "automatiskBehandlet": { + "type": "boolean" + }, + "resultat": { + "type": "string" + }, + "utenlandstilsnitt": { + "type": "string" + }, + "behandlingTypeBeskrivelse": { + "type": "string" + }, + "behandlingStatusBeskrivelse": { + "type": "string" + }, + "resultatBeskrivelse": { + "type": "string" + }, + "resultatBegrunnelser": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResultatBegrunnelseDVH" + } + }, + "utenlandstilsnittBeskrivelse": { + "type": "string" + }, + "beslutter": { + "type": "string" + }, + "saksbehandler": { + "type": "string" + }, + "behandlingOpprettetAv": { + "type": "string" + }, + "behandlingOpprettetType": { + "type": "string" + }, + "behandlingOpprettetTypeBeskrivelse": { + "type": "string" + }, + "ansvarligEnhetKode": { + "type": "string" + }, + "ansvarligEnhetType": { + "type": "string" + }, + "behandlendeEnhetKode": { + "type": "string" + }, + "behandlendeEnhetType": { + "type": "string" + }, + "totrinnsbehandling": { + "type": "boolean" + }, + "avsender": { + "type": "string" + }, + "versjon": { + "type": "string" + }, + "førsteInnvilgedeVilkårsdato": { + "type": "string", + "format": "date" + }, + "settPaaVent": { + "$ref": "#/components/schemas/SettPåVent" + } + } + }, + "ResultatBegrunnelseDVH": { + "required": [ + "type", + "vedtakBegrunnelse" + ], + "type": "object", + "properties": { + "fom": { + "type": "string", + "format": "date" + }, + "tom": { + "type": "string", + "format": "date" + }, + "type": { + "type": "string" + }, + "vedtakBegrunnelse": { + "type": "string" + } + } + }, + "SettPåVent": { + "required": [ + "aarsak", + "frist", + "tidSattPaaVent" + ], + "type": "object", + "properties": { + "frist": { + "type": "string", + "format": "date-time" + }, + "tidSattPaaVent": { + "type": "string", + "format": "date-time" + }, + "aarsak": { + "type": "string" + } + } + }, + "RessursListRestRefusjonEøs": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestRefusjonEøs" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RessursRestPersonInfo": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/RestPersonInfo" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestBostedsadresse": { + "required": [ + "postnummer" + ], + "type": "object", + "properties": { + "adresse": { + "type": "string" + }, + "postnummer": { + "type": "string" + } + } + }, + "RestForelderBarnRelasjon": { + "required": [ + "navn", + "personIdent", + "relasjonRolle" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + }, + "relasjonRolle": { + "type": "string", + "enum": [ + "BARN", + "FAR", + "MEDMOR", + "MOR", + "DOEDFOEDT_BARN" + ] + }, + "navn": { + "type": "string" + }, + "fødselsdato": { + "type": "string", + "format": "date" + }, + "adressebeskyttelseGradering": { + "type": "string", + "enum": [ + "STRENGT_FORTROLIG_UTLAND", + "FORTROLIG", + "STRENGT_FORTROLIG", + "UGRADERT" + ] + } + } + }, + "RestForelderBarnRelasjonnMaskert": { + "required": [ + "adressebeskyttelseGradering", + "relasjonRolle" + ], + "type": "object", + "properties": { + "relasjonRolle": { + "type": "string", + "enum": [ + "BARN", + "FAR", + "MEDMOR", + "MOR", + "DOEDFOEDT_BARN" + ] + }, + "adressebeskyttelseGradering": { + "type": "string", + "enum": [ + "STRENGT_FORTROLIG_UTLAND", + "FORTROLIG", + "STRENGT_FORTROLIG", + "UGRADERT" + ] + } + } + }, + "RestPersonInfo": { + "required": [ + "forelderBarnRelasjon", + "forelderBarnRelasjonMaskert", + "harTilgang", + "kommunenummer", + "personIdent" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + }, + "fødselsdato": { + "type": "string", + "format": "date" + }, + "navn": { + "type": "string" + }, + "kjønn": { + "type": "string", + "enum": [ + "MANN", + "KVINNE", + "UKJENT" + ] + }, + "adressebeskyttelseGradering": { + "type": "string", + "enum": [ + "STRENGT_FORTROLIG_UTLAND", + "FORTROLIG", + "STRENGT_FORTROLIG", + "UGRADERT" + ] + }, + "harTilgang": { + "type": "boolean" + }, + "forelderBarnRelasjon": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestForelderBarnRelasjon" + } + }, + "forelderBarnRelasjonMaskert": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestForelderBarnRelasjonnMaskert" + } + }, + "kommunenummer": { + "type": "string" + }, + "dødsfallDato": { + "type": "string" + }, + "bostedsadresse": { + "$ref": "#/components/schemas/RestBostedsadresse" + } + } + }, + "DataForManuellJournalføring": { + "required": [ + "oppgave" + ], + "type": "object", + "properties": { + "oppgave": { + "$ref": "#/components/schemas/Oppgave" + }, + "person": { + "$ref": "#/components/schemas/RestPersonInfo" + }, + "journalpost": { + "$ref": "#/components/schemas/Journalpost" + }, + "minimalFagsak": { + "$ref": "#/components/schemas/RestMinimalFagsak" + } + } + }, + "RessursDataForManuellJournalføring": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/DataForManuellJournalføring" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "Logg": { + "required": [ + "behandlingId", + "id", + "opprettetAv", + "opprettetTidspunkt", + "rolle", + "tekst", + "tittel", + "type" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "opprettetAv": { + "type": "string" + }, + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "behandlingId": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string", + "enum": [ + "AUTOVEDTAK_TIL_MANUELL_BEHANDLING", + "VERGE_REGISTRERT", + "INSTITUSJON_REGISTRERT", + "FØDSELSHENDELSE", + "LIVSHENDELSE", + "BEHANDLENDE_ENHET_ENDRET", + "BEHANDLING_OPPRETTET", + "BEHANDLINGSTYPE_ENDRET", + "BARN_LAGT_TIL", + "DOKUMENT_MOTTATT", + "SØKNAD_REGISTRERT", + "VILKÅRSVURDERING", + "SEND_TIL_BESLUTTER", + "SEND_TIL_SYSTEM", + "GODKJENNE_VEDTAK", + "MIGRERING_BEKREFTET", + "DISTRIBUERE_BREV", + "BREV_IKKE_DISTRIBUERT", + "BREV_IKKE_DISTRIBUERT_UKJENT_DØDSBO", + "FERDIGSTILLE_BEHANDLING", + "HENLEGG_BEHANDLING", + "BEHANDLIG_SATT_PÅ_VENT", + "BEHANDLIG_GJENOPPTATT", + "BEHANDLING_SATT_PÅ_MASKINELL_VENT", + "BEHANDLING_TATT_AV_MASKINELL_VENT", + "VENTENDE_BEHANDLING_ENDRET", + "KORRIGERT_ETTERBETALING", + "MANUELT_SMÅBARNSTILLEGG_JUSTERING", + "KORRIGERT_VEDTAK", + "FEILUTBETALT_VALUTA_LAGT_TIL", + "FEILUTBETALT_VALUTA_FJERNET", + "BREVMOTTAKER_LAGT_TIL_ELLER_FJERNET", + "REFUSJON_EØS_LAGT_TIL", + "REFUSJON_EØS_FJERNET", + "MANUELL_DØDSFALL_DATO_REGISTRERT" + ] + }, + "tittel": { + "type": "string" + }, + "rolle": { + "type": "string", + "enum": [ + "SYSTEM", + "BESLUTTER", + "SAKSBEHANDLER", + "VEILEDER", + "UKJENT" + ] + }, + "tekst": { + "type": "string" + } + } + }, + "RessursListLogg": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Logg" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RessursListRestKorrigertEtterbetaling": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestKorrigertEtterbetaling" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "FagsystemVedtak": { + "required": [ + "behandlingstype", + "eksternBehandlingId", + "fagsystemType", + "resultat", + "vedtakstidspunkt" + ], + "type": "object", + "properties": { + "eksternBehandlingId": { + "type": "string" + }, + "behandlingstype": { + "type": "string" + }, + "resultat": { + "type": "string" + }, + "vedtakstidspunkt": { + "type": "string", + "format": "date-time" + }, + "fagsystemType": { + "type": "string", + "enum": [ + "ORDNIÆR", + "TILBAKEKREVING", + "SANKSJON_1_MND", + "UTESTENGELSE" + ] + }, + "regelverk": { + "type": "string", + "enum": [ + "NASJONAL", + "EØS" + ] + } + } + }, + "RessursListFagsystemVedtak": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FagsystemVedtak" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "KanOppretteRevurderingResponse": { + "required": [ + "kanOpprettes" + ], + "type": "object", + "properties": { + "kanOpprettes": { + "type": "boolean" + }, + "getårsak": { + "type": "string", + "enum": [ + "ÅPEN_BEHANDLING", + "INGEN_BEHANDLING" + ] + } + } + }, + "RessursKanOppretteRevurderingResponse": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/KanOppretteRevurderingResponse" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RessursJournalpost": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Journalpost" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "InternStatistikkResponse": { + "required": [ + "antallBehandlingerIkkeFerdigstilt", + "antallBehandlingerPerÅrsak", + "antallFagsakerLøpende", + "antallFagsakerTotalt" + ], + "type": "object", + "properties": { + "antallFagsakerTotalt": { + "type": "integer", + "format": "int64" + }, + "antallFagsakerLøpende": { + "type": "integer", + "format": "int64" + }, + "antallBehandlingerIkkeFerdigstilt": { + "type": "integer", + "format": "int64" + }, + "antallBehandlingerPerÅrsak": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + } + } + }, + "RessursInternStatistikkResponse": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/InternStatistikkResponse" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "AntallSøknader": { + "required": [ + "digitaleSøknader", + "digitaliseringsgrad", + "papirsøknader", + "totalt" + ], + "type": "object", + "properties": { + "totalt": { + "type": "integer", + "format": "int32" + }, + "papirsøknader": { + "type": "integer", + "format": "int32" + }, + "digitaleSøknader": { + "type": "integer", + "format": "int32" + }, + "digitaliseringsgrad": { + "type": "number", + "format": "float" + } + } + }, + "RessursSøknadsstatistikkForPeriode": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SøknadsstatistikkForPeriode" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "SøknadsstatistikkForPeriode": { + "required": [ + "fom", + "ordinærBarnetrygd", + "tom", + "utvidetBarnetrygd" + ], + "type": "object", + "properties": { + "fom": { + "type": "string", + "format": "date" + }, + "tom": { + "type": "string", + "format": "date" + }, + "ordinærBarnetrygd": { + "$ref": "#/components/schemas/AntallSøknader" + }, + "utvidetBarnetrygd": { + "$ref": "#/components/schemas/AntallSøknader" + } + } + }, + "RessursListRestFeilutbetaltValuta": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestFeilutbetaltValuta" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RessursRestFagsak": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/RestFagsak" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestFagsak": { + "required": [ + "behandlinger", + "fagsakType", + "gjeldendeUtbetalingsperioder", + "id", + "opprettetTidspunkt", + "status", + "søkerFødselsnummer", + "tilbakekrevingsbehandlinger", + "underBehandling" + ], + "type": "object", + "properties": { + "opprettetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "søkerFødselsnummer": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "OPPRETTET", + "LØPENDE", + "AVSLUTTET" + ] + }, + "underBehandling": { + "type": "boolean" + }, + "løpendeKategori": { + "type": "string", + "enum": [ + "EØS", + "NASJONAL" + ] + }, + "løpendeUnderkategori": { + "type": "string", + "enum": [ + "UTVIDET", + "ORDINÆR" + ] + }, + "gjeldendeUtbetalingsperioder": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Utbetalingsperiode" + } + }, + "behandlinger": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestUtvidetBehandling" + } + }, + "tilbakekrevingsbehandlinger": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestTilbakekrevingsbehandling" + } + }, + "fagsakType": { + "type": "string", + "enum": [ + "NORMAL", + "BARN_ENSLIG_MINDREÅRIG", + "INSTITUSJON" + ] + }, + "institusjon": { + "$ref": "#/components/schemas/InstitusjonInfo" + } + } + }, + "KlagebehandlingDto": { + "required": [ + "fagsakId", + "id", + "klageinstansResultat", + "mottattDato", + "opprettet", + "status" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "fagsakId": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string", + "enum": [ + "OPPRETTET", + "UTREDES", + "VENTER", + "FERDIGSTILT" + ] + }, + "opprettet": { + "type": "string", + "format": "date-time" + }, + "mottattDato": { + "type": "string", + "format": "date" + }, + "resultat": { + "type": "string", + "enum": [ + "MEDHOLD", + "IKKE_MEDHOLD", + "IKKE_MEDHOLD_FORMKRAV_AVVIST", + "IKKE_SATT", + "HENLAGT" + ] + }, + "getårsak": { + "type": "string", + "enum": [ + "FEIL_I_LOVANDVENDELSE", + "FEIL_REGELVERKSFORSTÅELSE", + "FEIL_ELLER_ENDRET_FAKTA", + "FEIL_PROSESSUELL", + "ANNET" + ] + }, + "vedtaksdato": { + "type": "string", + "format": "date-time" + }, + "klageinstansResultat": { + "type": "array", + "items": { + "$ref": "#/components/schemas/KlageinstansResultatDto" + } + }, + "henlagtÅrsak": { + "type": "string", + "enum": [ + "TRUKKET_TILBAKE", + "FEILREGISTRERT" + ] + } + } + }, + "KlageinstansResultatDto": { + "required": [ + "journalpostReferanser", + "mottattEllerAvsluttetTidspunkt", + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "KLAGEBEHANDLING_AVSLUTTET", + "ANKEBEHANDLING_OPPRETTET", + "ANKEBEHANDLING_AVSLUTTET", + "ANKE_I_TRYGDERETTENBEHANDLING_OPPRETTET", + "BEHANDLING_FEILREGISTRERT" + ] + }, + "utfall": { + "type": "string", + "enum": [ + "TRUKKET", + "RETUR", + "OPPHEVET", + "MEDHOLD", + "DELVIS_MEDHOLD", + "STADFESTELSE", + "UGUNST", + "AVVIST", + "INNSTILLING_STADFESTELSE", + "INNSTILLING_AVVIST" + ] + }, + "mottattEllerAvsluttetTidspunkt": { + "type": "string", + "format": "date-time" + }, + "journalpostReferanser": { + "type": "array", + "items": { + "type": "string" + } + }, + "getårsakFeilregistrert": { + "type": "string" + } + } + }, + "RessursListKlagebehandlingDto": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/KlagebehandlingDto" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RessursListRestBrevmottaker": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RestBrevmottaker" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RessursRestSimulering": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/RestSimulering" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestSimulering": { + "required": [ + "etterbetaling", + "feilutbetaling", + "perioder" + ], + "type": "object", + "properties": { + "perioder": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SimuleringsPeriode" + } + }, + "fomDatoNestePeriode": { + "type": "string", + "format": "date" + }, + "etterbetaling": { + "type": "number" + }, + "feilutbetaling": { + "type": "number" + }, + "fom": { + "type": "string", + "format": "date" + }, + "tomDatoNestePeriode": { + "type": "string", + "format": "date" + }, + "forfallsdatoNestePeriode": { + "type": "string", + "format": "date" + }, + "tidSimuleringHentet": { + "type": "string", + "format": "date" + }, + "tomSisteUtbetaling": { + "type": "string", + "format": "date" + } + } + }, + "SimuleringsPeriode": { + "required": [ + "etterbetaling", + "feilutbetaling", + "fom", + "forfallsdato", + "manuellPostering", + "nyttBeløp", + "resultat", + "tidligereUtbetalt", + "tom" + ], + "type": "object", + "properties": { + "fom": { + "type": "string", + "format": "date" + }, + "tom": { + "type": "string", + "format": "date" + }, + "forfallsdato": { + "type": "string", + "format": "date" + }, + "nyttBeløp": { + "type": "number" + }, + "tidligereUtbetalt": { + "type": "number" + }, + "manuellPostering": { + "type": "number" + }, + "resultat": { + "type": "number" + }, + "feilutbetaling": { + "type": "number" + }, + "etterbetaling": { + "type": "number" + } + } + }, + "RessursListString": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RessursLocalDate": { + "required": [ + "melding", + "status" + ], + "type": "object", + "properties": { + "data": { + "type": "string", + "format": "date" + }, + "status": { + "type": "string", + "enum": [ + "SUKSESS", + "FEILET", + "IKKE_HENTET", + "IKKE_TILGANG", + "FUNKSJONELL_FEIL" + ] + }, + "melding": { + "type": "string" + }, + "frontendFeilmelding": { + "type": "string" + }, + "stacktrace": { + "type": "string" + } + } + }, + "RestSlettVilkår": { + "required": [ + "personIdent", + "vilkårType" + ], + "type": "object", + "properties": { + "personIdent": { + "type": "string" + }, + "vilkårType": { + "type": "string", + "enum": [ + "UNDER_18_ÅR", + "BOR_MED_SØKER", + "GIFT_PARTNERSKAP", + "BOSATT_I_RIKET", + "LOVLIG_OPPHOLD", + "UTVIDET_BARNETRYGD" + ] + } + } + } + }, + "securitySchemes": { + "oauth2": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/authorize", + "tokenUrl": "https://login.microsoftonline.com/navq.onmicrosoft.com/oauth2/v2.0/token", + "scopes": { + "api://bar/.default": "read,write" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/openapi-swagger/pay-publicapi.json b/openapi-swagger/pay-publicapi.json new file mode 100644 index 000000000..fd8830b6a --- /dev/null +++ b/openapi-swagger/pay-publicapi.json @@ -0,0 +1,2774 @@ +{ + "openapi" : "3.0.1", + "info" : { + "description" : "The GOV.UK Pay REST API. Read [our documentation](https://docs.payments.service.gov.uk/) for more details.", + "title" : "GOV.UK Pay API", + "version" : "1.0.3" + }, + "servers" : [ { + "url" : "https://publicapi.payments.service.gov.uk" + } ], + "tags" : [ { + "name" : "Agreements" + }, { + "name" : "Card payments" + }, { + "name" : "Refunding card payments" + } ], + "paths" : { + "/v1/agreements" : { + "get" : { + "description" : "You can use this endpoint to search for recurring payments agreements. The agreements are sorted by date, with the most recently-created agreements appearing first.", + "operationId" : "Search agreements", + "parameters" : [ { + "description" : "Returns agreements with a `reference` that exactly matches the value you sent. This parameter is not case sensitive. A `reference` was associated with the agreement when that agreement was created.", + "example" : "CT-22-23-0001", + "in" : "query", + "name" : "reference", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns agreements in a matching `status`. `status` reflects where an agreement is in its lifecycle. You can [read more about the meanings of the different agreement status values](https://docs.payments.service.gov.uk/recurring_payments/#understanding-agreement-status).", + "in" : "query", + "name" : "status", + "schema" : { + "type" : "string", + "enum" : [ "created", "active", "cancelled", "inactive" ] + } + }, { + "description" : "Returns a specific page of results. Defaults to `1`. You can [read about search pagination](https://docs.payments.service.gov.uk/api_reference/#pagination)", + "example" : 1, + "in" : "query", + "name" : "page", + "schema" : { + "type" : "string" + } + }, { + "description" : "The number of agreements returned per results page. Defaults to `500`. Maximum value is `500`. You can [read about search pagination](https://docs.payments.service.gov.uk/api_reference/#pagination)", + "example" : 50, + "in" : "query", + "name" : "display_size", + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/AgreementSearchResults" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Your request failed. Check the `code` and `description` in the response to find out why your request failed." + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Search agreements for recurring payments", + "tags" : [ "Agreements" ] + }, + "post" : { + "description" : "You can use this endpoint to create a new agreement.", + "operationId" : "Create an agreement", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CreateAgreementRequest" + } + } + }, + "description" : "requestPayload", + "required" : true + }, + "responses" : { + "201" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Agreement" + } + } + }, + "description" : "Created" + }, + "400" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Bad request" + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Your request failed. Check the `code` and `description` in the response to find out why your request failed." + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Create an agreement for recurring payments", + "tags" : [ "Agreements" ] + } + }, + "/v1/agreements/{agreementId}" : { + "get" : { + "description" : "You can use this endpoint to get information about a single recurring payments agreement.", + "operationId" : "Get an agreement", + "parameters" : [ { + "description" : "Returns the agreement with the matching `agreement_id`. GOV.UK Pay generated an `agreement_id` when you created the agreement.", + "example" : "cgc1ocvh0pt9fqs0ma67r42l58", + "in" : "path", + "name" : "agreementId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Agreement" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Get information about a single agreement for recurring payments", + "tags" : [ "Agreements" ] + } + }, + "/v1/agreements/{agreementId}/cancel" : { + "post" : { + "description" : "You can use this endpoint to cancel a recurring payments agreement in the `active` status.", + "operationId" : "Cancel an agreement", + "parameters" : [ { + "description" : "The `agreement_id` of the agreement you are cancelling", + "example" : "cgc1ocvh0pt9fqs0ma67r42l58", + "in" : "path", + "name" : "agreementId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "No Content" + }, + "400" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Cancellation of agreement failed" + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Cancel an agreement for recurring payments", + "tags" : [ "Agreements" ] + } + }, + "/v1/auth" : { + "post" : { + "description" : "You can use this endpoint to [authorise payments](https://docs.payments.service.gov.uk/moto_payments/moto_send_card_details_api/) you have created with `authorisation_mode` set to `moto_api`.", + "operationId" : "Authorise a MOTO payment", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/AuthorisationRequest" + } + } + }, + "required" : true + }, + "responses" : { + "204" : { + "description" : "Your authorisation request was successful." + }, + "400" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Your request is invalid. Check the `code` and `description` in the response to find out why your request failed." + }, + "402" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "The `card_number` you sent is not a valid card number or you chose not to accept this card type. Check the `code` and `description` fields in the response to find out why your request failed." + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "A value you sent is invalid or missing. Check the `code` and `description` in the response to find out why your request failed." + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "There is something wrong with GOV.UK Pay. If there are no issues on our status page (https://payments.statuspage.io), you can contact us with your error code and we'll investigate." + } + }, + "summary" : "Send card details to authorise a MOTO payment", + "tags" : [ "Authorise card payments" ] + } + }, + "/v1/disputes" : { + "get" : { + "description" : "You can use this endpoint to search disputes. A dispute is when [a paying user challenges a completed payment through their bank](https://docs.payments.service.gov.uk/disputes/).", + "operationId" : "Search disputes", + "parameters" : [ { + "description" : "Returns disputes raised on or after the `from_date`. Date and time must be coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", + "example" : "2015-08-13T12:35:00Z", + "in" : "query", + "name" : "from_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns disputes raised before the `to_date`. Date and time must be coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", + "example" : "2015-08-13T12:35:00Z", + "in" : "query", + "name" : "to_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns disputes settled on or after the `from_settled_date`. Date must be in ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. Disputes are settled when your payment service provider takes the disputed amount from a payout to your bank account.", + "in" : "query", + "name" : "from_settled_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns disputes settled before the `to_settled_date`. Date must be in ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. Disputes are settled when your payment service provider takes the disputed amount from a payout to your bank account.", + "in" : "query", + "name" : "to_settled_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns disputes with a matching `status`. `status` reflects what stage of the dispute process a dispute is at. You can [read more about the meanings of the different status values](https://docs.payments.service.gov.uk/disputes/#dispute-status)", + "example" : "won", + "in" : "query", + "name" : "status", + "schema" : { + "type" : "string", + "enum" : [ "needs_response", "under_review", "lost", "won" ] + } + }, { + "description" : "Returns a specific page of results. Defaults to `1`.", + "in" : "query", + "name" : "page", + "schema" : { + "type" : "string" + } + }, { + "description" : "The number of disputes returned per results page. Defaults to `500`. Maximum value is `500`.", + "in" : "query", + "name" : "display_size", + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/DisputesSearchResults" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Invalid parameters: from_date, to_date, from_settled_date, to_settled_date, status, display_size. See Public API documentation for the correct data formats" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Search disputes", + "tags" : [ "Disputes" ] + } + }, + "/v1/payments" : { + "get" : { + "description" : "You can use this endpoint to [search for payments you’ve previously created](https://docs.payments.service.gov.uk/reporting/#search-payments/). Payments are sorted by date, with the most recently-created payment appearing first.", + "operationId" : "Search payments", + "parameters" : [ { + "description" : "Returns payments with `reference` values exactly matching your specified value.", + "in" : "query", + "name" : "reference", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments with matching `email` values. You can send full or partial email addresses. `email` is the paying user’s email address.", + "in" : "query", + "name" : "email", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments in a matching `state`. `state` reflects where a payment is in the [payment status lifecycle](https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle).", + "example" : "success", + "in" : "query", + "name" : "state", + "schema" : { + "type" : "string", + "enum" : [ "created", "started", "submitted", "success", "failed", "cancelled", "error" ] + } + }, { + "description" : "Returns payments paid with a particular card brand.", + "in" : "query", + "name" : "card_brand", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments created on or after the `from_date`. Date and time must be coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", + "example" : "2015-08-13T12:35:00Z", + "in" : "query", + "name" : "from_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments created before the `to_date`. Date and time must be coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", + "example" : "2015-08-13T12:35:00Z", + "in" : "query", + "name" : "to_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns a [specific page of results](https://docs.payments.service.gov.uk/api_reference/#pagination). Defaults to `1`.", + "in" : "query", + "name" : "page", + "schema" : { + "type" : "string" + } + }, { + "description" : "The number of payments returned [per results page](https://docs.payments.service.gov.uk/api_reference/#pagination). Defaults to `500`. Maximum value is `500`.", + "in" : "query", + "name" : "display_size", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments paid with cards under this cardholder name.", + "in" : "query", + "name" : "cardholder_name", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments paid by cards beginning with the `first_digits_card_number` value. `first_digits_card_number` value must be 6 digits.", + "in" : "query", + "name" : "first_digits_card_number", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments paid by cards ending with the `last_digits_card_number` value. `last_digits_card_number` value must be 4 digits.", + "in" : "query", + "name" : "last_digits_card_number", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments settled on or after the `from_settled_date` value. You can only search by settled date if your payment service provider is Stripe. Date must be in ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. Payments are settled when your payment service provider sends funds to your bank account.", + "in" : "query", + "name" : "from_settled_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments settled before the `to_settled_date` value. You can only search by settled date if your payment service provider is Stripe. Date must be in ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. Payments are settled when your payment service provider sends funds to your bank account.", + "in" : "query", + "name" : "to_settled_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns payments that were authorised using the agreement with this `agreement_id`. Must be an exact match.", + "example" : "abcefghjklmnopqr1234567890", + "in" : "query", + "name" : "agreement_id", + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PaymentSearchResults" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Invalid parameters: from_date, to_date, status, display_size. See Public API documentation for the correct data formats" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Search payments", + "tags" : [ "Card payments" ] + }, + "post" : { + "description" : "You can use this endpoint to [create a new payment](https://docs.payments.service.gov.uk/making_payments/).", + "operationId" : "Create a payment", + "parameters" : [ { + "in" : "header", + "name" : "Idempotency-Key", + "schema" : { + "type" : "string", + "pattern" : "^$|^[a-zA-Z0-9-]+$" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CreateCardPaymentRequest" + } + } + }, + "description" : "requestPayload", + "required" : true + }, + "responses" : { + "201" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CreatePaymentResult" + } + } + }, + "description" : "Created" + }, + "400" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Bad request" + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Your request failed. Check the `code` and `description` in the response to find out why your request failed." + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Create a payment", + "tags" : [ "Card payments" ] + } + }, + "/v1/payments/{paymentId}" : { + "get" : { + "description" : "You can use this endpoint to [get details about a single payment you’ve previously created](https://docs.payments.service.gov.uk/reporting/#get-information-about-a-single-payment).", + "operationId" : "Get a payment", + "parameters" : [ { + "description" : "Returns the payment with the matching `payment_id`.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PaymentWithAllLinks" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Get information about a single payment", + "tags" : [ "Card payments" ] + } + }, + "/v1/payments/{paymentId}/cancel" : { + "post" : { + "description" : "You can use this endpoint [to cancel an unfinished payment](https://docs.payments.service.gov.uk/making_payments/#cancel-a-payment-that-s-in-progress).", + "operationId" : "Cancel a payment", + "parameters" : [ { + "description" : "The `payment_id` of the payment you’re cancelling.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "No Content" + }, + "400" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Cancellation of payment failed" + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "409" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Conflict" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Cancel payment", + "tags" : [ "Card payments" ] + } + }, + "/v1/payments/{paymentId}/capture" : { + "post" : { + "description" : "You can use this endpoint to [take (‘capture’) a delayed payment from the paying user’s bank account](https://docs.payments.service.gov.uk/delayed_capture/).", + "operationId" : "Capture a payment", + "parameters" : [ { + "description" : "The `payment_id` of the payment you’re capturing.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "204" : { + "description" : "No Content" + }, + "400" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Capture of payment failed" + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "409" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Conflict" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Take a delayed payment", + "tags" : [ "Card payments" ] + } + }, + "/v1/payments/{paymentId}/events" : { + "get" : { + "description" : "You can use this endpoint to [get a list of a payment’s events](https://docs.payments.service.gov.uk/reporting/#get-a-payment-s-events). A payment event is when a payment’s `state` changes, such as when the payment is created, or when the paying user submits their details.", + "operationId" : "Get events for a payment", + "parameters" : [ { + "description" : "Payment identifier", + "example" : "hu20sqlact5260q2nanm0q8u93", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PaymentEvents" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Get a payment's events", + "tags" : [ "Card payments" ] + } + }, + "/v1/payments/{paymentId}/refunds" : { + "get" : { + "description" : "You can use this endpoint to [get a list of refunds for a payment](https://docs.payments.service.gov.uk/refunding_payments/#get-all-refunds-for-a-single-payment).", + "operationId" : "Get all refunds for a payment", + "parameters" : [ { + "description" : "The unique `payment_id` of the payment you want a list of refunds for.", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RefundForSearchResult" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Get information about a payment’s refunds", + "tags" : [ "Refunding card payments" ] + }, + "post" : { + "description" : "You can use this endpoint to [fully or partially refund a payment](https://docs.payments.service.gov.uk/refunding_payments).", + "operationId" : "Submit a refund for a payment", + "parameters" : [ { + "description" : "The unique `payment_id` of the payment you want to refund.", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PaymentRefundRequest" + } + } + }, + "description" : "requestPayload", + "required" : true + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Refund" + } + } + }, + "description" : "successful operation" + }, + "202" : { + "description" : "ACCEPTED" + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "412" : { + "description" : "Refund amount available mismatch" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Refund a payment", + "tags" : [ "Refunding card payments" ] + } + }, + "/v1/payments/{paymentId}/refunds/{refundId}" : { + "get" : { + "description" : "You can use this endpoint to [get details about an individual refund](https://docs.payments.service.gov.uk/refunding_payments/#checking-the-status-of-a-refund).", + "operationId" : "Get a payment refund", + "parameters" : [ { + "description" : "The unique `payment_id` of the payment you want to view a refund of.", + "in" : "path", + "name" : "paymentId", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "description" : "The unique `refund_id` of the refund you want to view. If one payment has multiple refunds, each refund has a different `refund_id`.", + "in" : "path", + "name" : "refundId", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Refund" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "404" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Not found" + }, + "429" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ErrorResponse" + } + } + }, + "description" : "Too many requests" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Check the status of a refund", + "tags" : [ "Refunding card payments" ] + } + }, + "/v1/refunds" : { + "get" : { + "description" : "You can use this endpoint to [search refunds you’ve previously created](https://docs.payments.service.gov.uk/refunding_payments/#searching-refunds). The refunds are sorted by date, with the most recently created refunds appearing first.", + "operationId" : "Search refunds", + "parameters" : [ { + "description" : "Returns refunds created on or after the `from_date`. Date and time must use Coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", + "example" : "2015-08-13T12:35:00Z", + "in" : "query", + "name" : "from_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns refunds created before the `to_date`. Date and time must use Coordinated Universal Time (UTC) and ISO 8601 format to second-level accuracy - `YYYY-MM-DDThh:mm:ssZ`.", + "example" : "2015-08-13T12:35:00Z", + "in" : "query", + "name" : "to_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns refunds settled on or after the `from_settled_date` value. You can only use `from_settled_date` if your payment service provider is Stripe. Date must use ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. Refunds are settled when Stripe takes the refund from your account balance.", + "example" : "2022-08-13", + "in" : "query", + "name" : "from_settled_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns refunds settled before the `to_settled_date` value. You can only use `to_settled_date` if your payment service provider is Stripe. Date must use ISO 8601 format to date-level accuracy - `YYYY-MM-DD`. Refunds are settled when Stripe takes the refund from your account balance.", + "example" : "2022-08-13", + "in" : "query", + "name" : "to_settled_date", + "schema" : { + "type" : "string" + } + }, { + "description" : "Returns a [specific page of results](https://docs.payments.service.gov.uk/api_reference/#pagination). Defaults to `1`.", + "in" : "query", + "name" : "page", + "schema" : { + "type" : "string" + } + }, { + "description" : "The number of refunds returned [per results page](https://docs.payments.service.gov.uk/api_reference/#pagination). Defaults to `500`. Maximum value is `500`.", + "in" : "query", + "name" : "display_size", + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RefundSearchResults" + } + } + }, + "description" : "OK - your request was successful." + }, + "401" : { + "description" : "Your API key is missing or invalid. Read more about [authenticating GOV.UK Pay API requests](https://docs.payments.service.gov.uk/api_reference/#authentication)" + }, + "422" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Invalid parameters. See Public API documentation for the correct data formats" + }, + "500" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RequestError" + } + } + }, + "description" : "Downstream system error" + } + }, + "security" : [ { + "BearerAuth" : [ ] + } ], + "summary" : "Search refunds", + "tags" : [ "Refunding card payments" ] + } + } + }, + "components" : { + "schemas" : { + "Address" : { + "type" : "object", + "description" : "A structure representing the billing address of a card", + "properties" : { + "city" : { + "type" : "string", + "description" : "The paying user's city.", + "example" : "address city", + "maxLength" : 255, + "minLength" : 0 + }, + "country" : { + "type" : "string", + "description" : "The paying user’s country, displayed as a 2-character ISO-3166-1-alpha-2 code.", + "example" : "GB" + }, + "line1" : { + "type" : "string", + "description" : "The first line of the paying user’s address.", + "example" : "address line 1", + "maxLength" : 255, + "minLength" : 0 + }, + "line2" : { + "type" : "string", + "description" : "The second line of the paying user’s address.", + "example" : "address line 2", + "maxLength" : 255, + "minLength" : 0 + }, + "postcode" : { + "type" : "string", + "description" : "The paying user's postcode.", + "example" : "AB1 2CD", + "maxLength" : 25, + "minLength" : 0 + } + } + }, + "Agreement" : { + "type" : "object", + "description" : "Contains information about a user's agreement for recurring payments. An agreement represents an understanding between you and your paying user that you'll use their card to make ongoing payments for a service.", + "properties" : { + "agreement_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this agreement when you created it.", + "example" : "cgc1ocvh0pt9fqs0ma67r42l58" + }, + "cancelled_date" : { + "type" : "string", + "description" : "The date and time this agreement was cancelled. This value uses Coordinated Universal Time (UTC) and ISO 8601 format – `YYYY-MM-DDThh:mm:ss.sssZ`.", + "example" : "2022-07-08T14:33:00.000Z" + }, + "created_date" : { + "type" : "string", + "description" : "The date and time you created this agreement. This value uses Coordinated Universal Time (UTC) and ISO 8601 format – `YYYY-MM-DDThh:mm:ss.sssZ`.", + "example" : "2022-07-08T14:33:00.000Z" + }, + "description" : { + "type" : "string", + "description" : "The description you sent when creating this agreement.", + "example" : "Dorset Council 2022/23 council tax subscription." + }, + "payment_instrument" : { + "$ref" : "#/components/schemas/PaymentInstrument" + }, + "reference" : { + "type" : "string", + "description" : "The reference you sent when creating this agreement.", + "example" : "CT-22-23-0001" + }, + "status" : { + "type" : "string", + "description" : "The status of this agreement. You can [read more about the meanings of each agreement status.](https://docs.payments.service.gov.uk/recurring_payments/#understanding-agreement-status)", + "enum" : [ "created", "active", "cancelled", "inactive" ] + }, + "user_identifier" : { + "type" : "string", + "description" : "The identifier you sent when creating this agreement. `user_identifier` helps you identify users in your records.", + "example" : "user-3fb81107-76b7-4910" + } + } + }, + "AgreementSearchResults" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/SearchNavigationLinks" + }, + "count" : { + "type" : "integer", + "format" : "int32", + "description" : "Number of agreements on the current page of search results.", + "example" : 20 + }, + "page" : { + "type" : "integer", + "format" : "int32", + "description" : "The [page of agreements you’re viewing](https://docs.payments.service.gov.uk/api_reference/#pagination). To view other pages, make this request again using the `page` parameter.", + "example" : 1 + }, + "results" : { + "type" : "array", + "description" : "Contains agreements matching your search criteria.", + "items" : { + "$ref" : "#/components/schemas/Agreement" + } + }, + "total" : { + "type" : "integer", + "format" : "int32", + "description" : "Total number of agreements matching your search criteria.", + "example" : 100 + } + } + }, + "AuthorisationRequest" : { + "type" : "object", + "description" : "Contains the user's payment information. This information will be sent to the payment service provider to authorise the payment.", + "properties" : { + "card_number" : { + "type" : "string", + "description" : "The full card number from the paying user's card.", + "example" : "4242424242424242", + "maxLength" : 19, + "minLength" : 12 + }, + "cardholder_name" : { + "type" : "string", + "description" : "The name on the paying user's card.", + "example" : "J. Citizen", + "maxLength" : 255, + "minLength" : 0 + }, + "cvc" : { + "type" : "string", + "description" : "The card verification code (CVC) or card verification value (CVV) on the paying user's card.", + "example" : "123", + "maxLength" : 4, + "minLength" : 3 + }, + "expiry_date" : { + "type" : "string", + "description" : "The expiry date of the paying user's card. This value must be in `MM/YY` format.", + "example" : "09/22", + "maxLength" : 5, + "minLength" : 5 + }, + "one_time_token" : { + "type" : "string", + "description" : "This single use token authorises your request and matches it to a payment. GOV.UK Pay generated the `one_time_token` when the payment was created.", + "example" : "12345-edsfr-6789-gtyu" + } + }, + "required" : [ "card_number", "cardholder_name", "cvc", "expiry_date", "one_time_token" ] + }, + "AuthorisationSummary" : { + "type" : "object", + "description" : "Object containing information about the authentication of the payment.", + "properties" : { + "three_d_secure" : { + "$ref" : "#/components/schemas/ThreeDSecure" + } + } + }, + "CardDetails" : { + "type" : "object", + "description" : "A structure representing the payment card", + "properties" : { + "billing_address" : { + "$ref" : "#/components/schemas/Address" + }, + "card_brand" : { + "type" : "string", + "description" : "The brand of card the user paid with.", + "example" : "Visa", + "readOnly" : true + }, + "card_type" : { + "type" : "string", + "description" : "The type of card the user paid with.`null` means your user paid with Google Pay or we did not recognise which type of card they paid with.", + "enum" : [ "debit", "credit", "null" ], + "example" : "debit", + "readOnly" : true + }, + "cardholder_name" : { + "type" : "string", + "example" : "Mr. Card holder" + }, + "expiry_date" : { + "type" : "string", + "description" : "The expiry date of the card the user paid with in `MM/YY` format.", + "example" : "04/24", + "readOnly" : true + }, + "first_digits_card_number" : { + "type" : "string", + "example" : "123456", + "readOnly" : true + }, + "last_digits_card_number" : { + "type" : "string", + "example" : "1234", + "readOnly" : true + }, + "wallet_type" : { + "type" : "string", + "description" : "The digital wallet type that the user paid with", + "enum" : [ "Apple Pay", "Google Pay" ], + "example" : "Apple Pay" + } + } + }, + "CardDetailsFromResponse" : { + "type" : "object", + "properties" : { + "billing_address" : { + "$ref" : "#/components/schemas/Address" + }, + "card_brand" : { + "type" : "string" + }, + "card_type" : { + "type" : "string" + }, + "cardholder_name" : { + "type" : "string" + }, + "expiry_date" : { + "type" : "string" + }, + "first_digits_card_number" : { + "type" : "string" + }, + "last_digits_card_number" : { + "type" : "string" + } + } + }, + "CreateAgreementRequest" : { + "type" : "object", + "description" : "The Agreement Request Payload", + "properties" : { + "description" : { + "type" : "string", + "description" : "A human-readable description of the purpose of the agreement for recurring payments. We’ll show the description to your user when they make their first payment to activate this agreement. Limited to 255 characters.", + "example" : "Dorset Council 2022/23 council tax subscription.", + "maxLength" : 255, + "minLength" : 1 + }, + "reference" : { + "type" : "string", + "description" : "Associate a reference with this agreement to help you identify it. Limited to 255 characters.", + "example" : "CT-22-23-0001", + "maxLength" : 255, + "minLength" : 1 + }, + "user_identifier" : { + "type" : "string", + "description" : "Associate an identifier with the user who will enter into this agreement with your service.user_identifier is not unique – multiple agreements can have identical user_identifier values.You should not include personal data in user_identifier.", + "example" : "user-3fb81107-76b7-4910", + "maxLength" : 255, + "minLength" : 1 + } + } + }, + "CreateCardPaymentRequest" : { + "type" : "object", + "description" : "The create payment request body", + "properties" : { + "agreement_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with a recurring payments agreement. Including `agreement_id` in your request tells the API to take this payment using the card details that are associated with this agreement. `agreement_id` must match an active agreement ID. You must set `authorisation_mode` to `agreement` for the API to accept `agreement_id`.", + "example" : "abcefghjklmnopqr1234567890", + "maxLength" : 26, + "minLength" : 26 + }, + "amount" : { + "type" : "integer", + "format" : "int32", + "description" : "Sets the amount the user will pay, in pence.", + "example" : 12000, + "maximum" : 10000000, + "minimum" : 0 + }, + "authorisation_mode" : { + "type" : "string", + "description" : "Sets how you intend to authorise the payment. Defaults to `web`. Payments created with `web` mode follow the [standard GOV.UK Pay payment journey](https://docs.payments.service.gov.uk/payment_flow/). Paying users visit the `next_url` in the response to complete their payment. Payments created with `agreement` mode are authorised with an agreement for recurring payments. If you create an `agreement` payment, you must also send an active `agreement_id`. You must not send `return_url`, `email`, or `prefilled_cardholder_details` or your request will fail. Payments created with `moto_api` mode return an `auth_url_post` object and a `one_time_token`. You can use `auth_url_post` and `one_time_token` to send the paying user’s card details through the API and complete the payment. If you create a `moto_api` payment, do not send a `return_url` in your request.", + "enum" : [ "web", "agreement", "moto_api" ] + }, + "delayed_capture" : { + "type" : "boolean", + "description" : "You can use this parameter to [delay taking a payment from the paying user’s bank account](https://docs.payments.service.gov.uk/delayed_capture/#delay-taking-a-payment). For example, you might want to do your own anti-fraud checks on payments, or check that users are eligible for your service. Defaults to `false`.", + "example" : false + }, + "description" : { + "type" : "string", + "description" : "A human-readable description of the payment you’re creating. Paying users see this description on the payment pages. Service staff see the description in the GOV.UK Pay admin tool", + "example" : "New passport application", + "maxLength" : 255, + "minLength" : 0 + }, + "email" : { + "type" : "string", + "description" : "email", + "example" : "Joe.Bogs@example.org" + }, + "language" : { + "type" : "string", + "description" : "[Sets the language of the user’s payment page](https://docs.payments.service.gov.uk/optional_features/welsh_language) with an ISO-6391 Alpha-2 code of a supported language.", + "enum" : [ "en", "cy" ], + "example" : "en" + }, + "metadata" : { + "$ref" : "#/components/schemas/ExternalMetadata" + }, + "moto" : { + "type" : "boolean", + "description" : "You can use this parameter to [designate a payment as a Mail Order / Telephone Order (MOTO) payment](https://docs.payments.service.gov.uk/moto_payments).", + "example" : false + }, + "prefilled_cardholder_details" : { + "$ref" : "#/components/schemas/PrefilledCardholderDetails" + }, + "reference" : { + "type" : "string", + "description" : "Associate a reference with this payment. `reference` is not unique - multiple payments can have identical `reference` values.", + "example" : "12345", + "maxLength" : 255, + "minLength" : 0 + }, + "return_url" : { + "type" : "string", + "description" : "The URL [the paying user is directed to after their payment journey on GOV.UK Pay ends](https://docs.payments.service.gov.uk/making_payments/#choose-the-return-url-and-match-your-users-to-payments).", + "example" : "https://service-name.gov.uk/transactions/12345", + "maxLength" : 2000, + "minLength" : 0 + }, + "set_up_agreement" : { + "type" : "string", + "description" : "Use this parameter to set up an existing agreement for recurring payments. The `set_up_agreement` value you send must be a valid `agreement_id`.", + "example" : "abcefghjklmnopqr1234567890", + "maxLength" : 26, + "minLength" : 26 + } + }, + "required" : [ "amount", "description", "reference", "return_url" ] + }, + "CreatePaymentResult" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/PaymentLinks" + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The amount, in pence, the user has paid or will pay. `amount` will match the value you sent in the request body.", + "example" : 1200 + }, + "card_details" : { + "$ref" : "#/components/schemas/CardDetailsFromResponse" + }, + "created_date" : { + "type" : "string", + "description" : "The date you created the payment.", + "example" : "2016-01-21T17:15:00.000Z" + }, + "delayed_capture" : { + "type" : "boolean", + "description" : "`delayed_capture` is `true` if you’re controlling [when GOV.UK Pay takes (‘captures’) the payment from the paying user’s bank account](https://docs.payments.service.gov.uk/delayed_capture).", + "example" : false, + "readOnly" : true + }, + "description" : { + "type" : "string", + "description" : "The description you sent in the request body when creating this payment.", + "example" : "New passport application" + }, + "email" : { + "type" : "string", + "description" : "The paying user’s email address. The paying user’s email field will be prefilled with this value when they make their payment. `email` does not appear if you did not include it in the request body.", + "example" : "citizen@example.org" + }, + "language" : { + "type" : "string", + "description" : "The language of the user’s payment page.", + "enum" : [ "en", "cy" ], + "example" : "en" + }, + "metadata" : { + "$ref" : "#/components/schemas/ExternalMetadata" + }, + "moto" : { + "type" : "boolean", + "description" : "Indicates if this payment is a [Mail Order / Telephone Order (MOTO) payment](https://docs.payments.service.gov.uk/moto_payments).", + "example" : false + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93" + }, + "payment_provider" : { + "type" : "string", + "example" : "worldpay" + }, + "provider_id" : { + "type" : "string", + "description" : "The reference number your payment service provider associated with the payment.", + "example" : "null" + }, + "reference" : { + "type" : "string", + "description" : "The reference number you associated with this payment.", + "example" : "12345" + }, + "refund_summary" : { + "$ref" : "#/components/schemas/RefundSummary" + }, + "return_url" : { + "type" : "string", + "description" : "The URL you direct the paying user to after their payment journey on GOV.UK Pay ends.", + "example" : "https://service-name.gov.uk/transactions/12345" + }, + "settlement_summary" : { + "$ref" : "#/components/schemas/PaymentSettlementSummary" + }, + "state" : { + "$ref" : "#/components/schemas/PaymentState" + } + } + }, + "DisputeDetailForSearch" : { + "type" : "object", + "description" : "Contains disputes matching your search criteria.", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/DisputeLinksForSearch" + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The disputed amount in pence.", + "example" : 1200, + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "description" : "The date and time the user's bank told GOV.UK Pay about this dispute.", + "example" : "2022-07-28T16:43:00.000Z", + "readOnly" : true + }, + "dispute_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this dispute when the paying user disputed the payment.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "evidence_due_date" : { + "type" : "string", + "description" : "The deadline for submitting your supporting evidence. This value uses Coordinated Universal Time (UTC) and ISO 8601 format", + "example" : "2022-07-28T16:43:00.000Z", + "readOnly" : true + }, + "fee" : { + "type" : "integer", + "format" : "int64", + "description" : "The payment service provider’s dispute fee, in pence.", + "example" : 1200, + "readOnly" : true + }, + "net_amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The amount, in pence, your payment service provider will take for a lost dispute. 'net_amount' is deducted from your payout after you lose the dispute. For example, a 'net_amount' of '-1500' means your PSP will take £15.00 from your next payout into your bank account. 'net_amount' is always a negative value. 'net_amount' only appears if you lose the dispute.", + "example" : -2400, + "readOnly" : true + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "reason" : { + "type" : "string", + "description" : "The reason the paying user gave for disputing this payment. Possible values are: 'credit_not_processed', 'duplicate', 'fraudulent', 'general', 'product_not_received', 'product_unacceptable', 'unrecognised', 'subscription_cancelled', >'other'", + "example" : "fraudulent", + "readOnly" : true + }, + "settlement_summary" : { + "$ref" : "#/components/schemas/SettlementSummary" + }, + "status" : { + "type" : "string", + "description" : "The current status of the dispute. Possible values are: 'needs_response', 'won', 'lost', 'under_review'", + "example" : "under_review", + "readOnly" : true + } + } + }, + "DisputeLinksForSearch" : { + "type" : "object", + "description" : "links for search dispute resource", + "properties" : { + "payment" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "DisputesSearchResults" : { + "type" : "object", + "properties" : { + "count" : { + "type" : "integer", + "format" : "int32", + "description" : "Number of disputes on the current page of search results.", + "example" : 20 + }, + "links" : { + "$ref" : "#/components/schemas/SearchNavigationLinks" + }, + "page" : { + "type" : "integer", + "format" : "int32", + "description" : "The page of results you’re viewing. To view other pages, make this request again using the 'page' parameter.", + "example" : 1 + }, + "results" : { + "type" : "array", + "description" : "Contains disputes matching your search criteria.", + "items" : { + "$ref" : "#/components/schemas/DisputeDetailForSearch" + } + }, + "total" : { + "type" : "integer", + "format" : "int32", + "description" : "Number of total disputes matching your search criteria.", + "example" : 100 + } + } + }, + "EmbeddedRefunds" : { + "type" : "object", + "properties" : { + "refunds" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/Refund" + } + } + } + }, + "ErrorResponse" : { + "type" : "object", + "description" : "An error response", + "properties" : { + "code" : { + "type" : "string", + "description" : "A GOV.UK Pay API error code. You can [find out more about this code in our documentation](https://docs.payments.service.gov.uk/api_reference/#gov-uk-pay-api-error-codes).", + "example" : "P0900" + }, + "description" : { + "type" : "string", + "description" : "Additional details about the error", + "example" : "Too many requests" + } + } + }, + "ExternalMetadata" : { + "type" : "object", + "example" : "{\"property1\": \"value1\", \"property2\": \"value2\"}\"", + "properties" : { + "metadata" : { + "type" : "object", + "additionalProperties" : { + "type" : "object" + } + } + } + }, + "Link" : { + "type" : "object", + "description" : "A link related to a payment", + "properties" : { + "href" : { + "type" : "string", + "description" : "A URL that lets you perform additional actions to this payment when combined with the associated `method`.", + "example" : "https://an.example.link/from/payment/platform", + "readOnly" : true + }, + "method" : { + "type" : "string", + "description" : "An API method that lets you perform additional actions to this paymentwhen combined with the associated `href`.", + "example" : "GET", + "readOnly" : true + } + } + }, + "PaymentDetailForSearch" : { + "type" : "object", + "description" : "Contains payments matching your search criteria.", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/PaymentLinksForSearch" + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The description assigned to the payment when it was created.", + "example" : 1200 + }, + "authorisation_mode" : { + "type" : "string", + "description" : "How the payment will be authorised. Payments created in `web` mode require the paying user to visit the `next_url` to complete the payment.", + "enum" : [ "web", "moto_api", "external" ] + }, + "authorisation_summary" : { + "$ref" : "#/components/schemas/AuthorisationSummary" + }, + "card_brand" : { + "type" : "string", + "deprecated" : true, + "description" : "This attribute is deprecated. Please use `card_details.card_brand` instead.", + "example" : "Visa", + "readOnly" : true + }, + "card_details" : { + "$ref" : "#/components/schemas/CardDetails" + }, + "corporate_card_surcharge" : { + "type" : "integer", + "format" : "int64", + "description" : "The [corporate card surcharge](https://docs.payments.service.gov.uk/corporate_card_surcharges/#add-corporate-card-fees) amount in pence.", + "example" : 250, + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "example" : "2016-01-21T17:15:00.000Z", + "readOnly" : true + }, + "delayed_capture" : { + "type" : "boolean", + "description" : "`delayed_capture` is `true` if you’re [controlling how long it takes GOV.UK Pay to take (‘capture’) a payment](https://docs.payments.service.gov.uk/delayed_capture).", + "example" : false, + "readOnly" : true + }, + "description" : { + "type" : "string", + "description" : "The description assigned to the payment when it was created.", + "example" : "Your Service Description" + }, + "email" : { + "type" : "string", + "example" : "The paying user’s email address." + }, + "fee" : { + "type" : "integer", + "format" : "int64", + "description" : "The [payment service provider’s (PSP) transaction fee](https://docs.payments.service.gov.uk/reporting/#psp-fees), in pence. `fee` only appears when we have taken (‘captured’) the payment from the user or if their payment fails after they submitted their card details. `fee` will not appear if your PSP is Worldpay or you are using an API key from a test service.", + "example" : 5, + "readOnly" : true + }, + "language" : { + "type" : "string", + "description" : "The ISO-6391 Alpha-2 code of the [language of the user's payment page](https://docs.payments.service.gov.uk/optional_features/welsh_language).", + "enum" : [ "en", "cy" ], + "example" : "en" + }, + "metadata" : { + "$ref" : "#/components/schemas/ExternalMetadata" + }, + "moto" : { + "type" : "boolean", + "description" : "Indicates if this payment is a [Mail Order / Telephone Order (MOTO) payment](https://docs.payments.service.gov.uk/moto_payments).", + "example" : false + }, + "net_amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The amount, in pence, that will be paid into your bank account after your payment service provider takes the `fee`.", + "example" : 1195, + "readOnly" : true + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "payment_provider" : { + "type" : "string", + "description" : "The payment service provider that processed this payment.", + "example" : "worldpay", + "readOnly" : true + }, + "provider_id" : { + "type" : "string", + "description" : "The unique ID your payment service provider generated for this payment. This is not the same as the `payment_id`.", + "example" : "reference-from-payment-gateway", + "readOnly" : true + }, + "reference" : { + "type" : "string", + "description" : "The reference associated with the payment when it was created. `reference` is not unique - multiple payments can have the same `reference` value.", + "example" : "your-reference" + }, + "refund_summary" : { + "$ref" : "#/components/schemas/RefundSummary" + }, + "return_url" : { + "type" : "string", + "description" : "The URL you direct the paying user to after their payment journey on GOV.UK Pay ends.", + "example" : "http://your.service.domain/your-reference", + "readOnly" : true + }, + "settlement_summary" : { + "$ref" : "#/components/schemas/PaymentSettlementSummary" + }, + "state" : { + "$ref" : "#/components/schemas/PaymentState" + }, + "total_amount" : { + "type" : "integer", + "format" : "int64", + "description" : "Amount your user paid in pence, including corporate card fees. `total_amount` only appears if you [added a corporate card surcharge to the payment](https://docs.payments.service.gov.uk/corporate_card_surcharges/#add-corporate-card-fees).", + "example" : 1450, + "readOnly" : true + } + } + }, + "PaymentEvent" : { + "type" : "object", + "description" : "A List of Payment Events information", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/PaymentEventLink" + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "state" : { + "$ref" : "#/components/schemas/PaymentState" + }, + "updated" : { + "type" : "string", + "description" : "When this payment’s state changed. This value uses Coordinated Universal Time (UTC) and ISO-8601 format - `YYYY-MM-DDThh:mm:ss.SSSZ`.", + "example" : "2017-01-10T16:44:48.646Z", + "readOnly" : true + } + } + }, + "PaymentEventLink" : { + "type" : "object", + "description" : "Resource link for a payment of a payment event", + "properties" : { + "payment_url" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "PaymentEvents" : { + "type" : "object", + "description" : "A List of Payment Events information", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/PaymentLinksForEvents" + }, + "events" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PaymentEvent" + } + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + } + } + }, + "PaymentInstrument" : { + "type" : "object", + "properties" : { + "CardDetails" : { + "$ref" : "#/components/schemas/CardDetailsFromResponse" + }, + "created_date" : { + "type" : "string", + "description" : "The date and time you created this payment instrument. This value uses Coordinated Universal Time (UTC) and ISO 8601 format – `YYYY-MM-DDThh:mm:ss.sssZ`.", + "example" : "2022-07-08T14:33:00.000Z" + }, + "type" : { + "type" : "string", + "description" : "The type of payment instrument.", + "enum" : [ "card" ] + } + } + }, + "PaymentLinks" : { + "type" : "object", + "description" : "links for payment", + "properties" : { + "auth_url_post" : { + "$ref" : "#/components/schemas/PostLink" + }, + "cancel" : { + "$ref" : "#/components/schemas/PostLink" + }, + "capture" : { + "$ref" : "#/components/schemas/PostLink" + }, + "events" : { + "$ref" : "#/components/schemas/Link" + }, + "next_url" : { + "$ref" : "#/components/schemas/Link" + }, + "next_url_post" : { + "$ref" : "#/components/schemas/PostLink" + }, + "refunds" : { + "$ref" : "#/components/schemas/Link" + }, + "self" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "PaymentLinksForEvents" : { + "type" : "object", + "description" : "links for events resource", + "properties" : { + "self" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "PaymentLinksForSearch" : { + "type" : "object", + "description" : "links for search payment resource", + "properties" : { + "cancel" : { + "$ref" : "#/components/schemas/PostLink" + }, + "capture" : { + "$ref" : "#/components/schemas/PostLink" + }, + "events" : { + "$ref" : "#/components/schemas/Link" + }, + "refunds" : { + "$ref" : "#/components/schemas/Link" + }, + "self" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "PaymentRefundRequest" : { + "type" : "object", + "description" : "The Payment Refund Request Payload", + "properties" : { + "amount" : { + "type" : "integer", + "format" : "int32", + "description" : "The amount you want to [refund to your user](https://docs.payments.service.gov.uk/refunding_payments/) in pence.", + "example" : 150000, + "maximum" : 10000000, + "minimum" : 1 + }, + "refund_amount_available" : { + "type" : "integer", + "format" : "int32", + "description" : "Amount in pence. Total amount still available before issuing the refund", + "example" : 200000, + "maximum" : 10000000, + "minimum" : 1 + } + }, + "required" : [ "amount" ] + }, + "PaymentSearchResults" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/SearchNavigationLinks" + }, + "count" : { + "type" : "integer", + "format" : "int32", + "description" : "Number of payments on the current page of search results.", + "example" : 20 + }, + "page" : { + "type" : "integer", + "format" : "int32", + "description" : "The [page of results you’re viewing](https://docs.payments.service.gov.uk/api_reference/#pagination). To view other pages, make this request again using the `page` parameter.", + "example" : 1 + }, + "results" : { + "type" : "array", + "description" : "Contains payments matching your search criteria.", + "items" : { + "$ref" : "#/components/schemas/PaymentDetailForSearch" + } + }, + "total" : { + "type" : "integer", + "format" : "int32", + "description" : "Total number of payments matching your search criteria.", + "example" : 100 + } + } + }, + "PaymentSettlementSummary" : { + "type" : "object", + "description" : "A structure representing information about a settlement", + "properties" : { + "capture_submit_time" : { + "type" : "string", + "description" : "The date and time GOV.UK Pay asked your payment service provider to take the payment from your user’s account. This value uses Coordinated Universal Time (UTC) and ISO 8601 format - `YYYY-MM-DDThh:mm:ss.SSSZ`", + "example" : "2016-01-21T17:15:00.000Z", + "readOnly" : true + }, + "captured_date" : { + "type" : "string", + "description" : "The date your payment service provider took the payment from your user. This value uses ISO 8601 format - `YYYY-MM-DD`", + "example" : "2016-01-21", + "readOnly" : true + }, + "settled_date" : { + "type" : "string", + "description" : "The date that the transaction was paid into the service's account.", + "example" : "2016-01-21", + "readOnly" : true + } + } + }, + "PaymentState" : { + "type" : "object", + "description" : "A structure representing the current state of the payment in its lifecycle.", + "properties" : { + "can_retry" : { + "type" : "boolean", + "description" : "If `can_retry` is `true`, you can use this agreement to try to take another recurring payment. If `can_retry` is `false`, you cannot take another recurring payment with this agreement. `can_retry` only appears on failed payments that were attempted using an agreement for recurring payments.", + "nullable" : true, + "readOnly" : true + }, + "code" : { + "type" : "string", + "description" : "An [API error code](https://docs.payments.service.gov.uk/api_reference/#gov-uk-pay-api-error-codes)that explains why the payment failed. `code` only appears if the payment failed.", + "example" : "P010", + "readOnly" : true + }, + "finished" : { + "type" : "boolean", + "description" : "Indicates whether a payment journey is finished.", + "readOnly" : true + }, + "message" : { + "type" : "string", + "description" : "A description of what went wrong with this payment. `message` only appears if the payment failed.", + "example" : "User cancelled the payment", + "readOnly" : true + }, + "status" : { + "type" : "string", + "description" : "Where the payment is in [the payment status lifecycle](https://docs.payments.service.gov.uk/api_reference/#payment-status-meanings).", + "example" : "created", + "readOnly" : true + } + } + }, + "PaymentWithAllLinks" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/PaymentLinks" + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The description assigned to the payment when it was created.", + "example" : 1200 + }, + "authorisation_mode" : { + "type" : "string", + "description" : "How the payment will be authorised. Payments created in `web` mode require the paying user to visit the `next_url` to complete the payment.", + "enum" : [ "web", "moto_api", "external" ] + }, + "authorisation_summary" : { + "$ref" : "#/components/schemas/AuthorisationSummary" + }, + "card_brand" : { + "type" : "string", + "deprecated" : true, + "description" : "This attribute is deprecated. Please use `card_details.card_brand` instead.", + "example" : "Visa", + "readOnly" : true + }, + "card_details" : { + "$ref" : "#/components/schemas/CardDetails" + }, + "corporate_card_surcharge" : { + "type" : "integer", + "format" : "int64", + "description" : "The [corporate card surcharge](https://docs.payments.service.gov.uk/corporate_card_surcharges/#add-corporate-card-fees) amount in pence.", + "example" : 250, + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "example" : "2016-01-21T17:15:00.000Z", + "readOnly" : true + }, + "delayed_capture" : { + "type" : "boolean", + "description" : "`delayed_capture` is `true` if you’re [controlling how long it takes GOV.UK Pay to take (‘capture’) a payment](https://docs.payments.service.gov.uk/delayed_capture).", + "example" : false, + "readOnly" : true + }, + "description" : { + "type" : "string", + "description" : "The description assigned to the payment when it was created.", + "example" : "Your Service Description" + }, + "email" : { + "type" : "string", + "example" : "The paying user’s email address." + }, + "fee" : { + "type" : "integer", + "format" : "int64", + "description" : "The [payment service provider’s (PSP) transaction fee](https://docs.payments.service.gov.uk/reporting/#psp-fees), in pence. `fee` only appears when we have taken (‘captured’) the payment from the user or if their payment fails after they submitted their card details. `fee` will not appear if your PSP is Worldpay or you are using an API key from a test service.", + "example" : 5, + "readOnly" : true + }, + "language" : { + "type" : "string", + "description" : "The ISO-6391 Alpha-2 code of the [language of the user's payment page](https://docs.payments.service.gov.uk/optional_features/welsh_language).", + "enum" : [ "en", "cy" ], + "example" : "en" + }, + "metadata" : { + "$ref" : "#/components/schemas/ExternalMetadata" + }, + "moto" : { + "type" : "boolean", + "description" : "Indicates if this payment is a [Mail Order / Telephone Order (MOTO) payment](https://docs.payments.service.gov.uk/moto_payments).", + "example" : false + }, + "net_amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The amount, in pence, that will be paid into your bank account after your payment service provider takes the `fee`.", + "example" : 1195, + "readOnly" : true + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93", + "readOnly" : true + }, + "payment_provider" : { + "type" : "string", + "description" : "The payment service provider that processed this payment.", + "example" : "worldpay", + "readOnly" : true + }, + "provider_id" : { + "type" : "string", + "description" : "The unique ID your payment service provider generated for this payment. This is not the same as the `payment_id`.", + "example" : "reference-from-payment-gateway", + "readOnly" : true + }, + "reference" : { + "type" : "string", + "description" : "The reference associated with the payment when it was created. `reference` is not unique - multiple payments can have the same `reference` value.", + "example" : "your-reference" + }, + "refund_summary" : { + "$ref" : "#/components/schemas/RefundSummary" + }, + "return_url" : { + "type" : "string", + "description" : "The URL you direct the paying user to after their payment journey on GOV.UK Pay ends.", + "example" : "http://your.service.domain/your-reference", + "readOnly" : true + }, + "settlement_summary" : { + "$ref" : "#/components/schemas/PaymentSettlementSummary" + }, + "state" : { + "$ref" : "#/components/schemas/PaymentState" + }, + "total_amount" : { + "type" : "integer", + "format" : "int64", + "description" : "Amount your user paid in pence, including corporate card fees. `total_amount` only appears if you [added a corporate card surcharge to the payment](https://docs.payments.service.gov.uk/corporate_card_surcharges/#add-corporate-card-fees).", + "example" : 1450, + "readOnly" : true + } + } + }, + "PostLink" : { + "type" : "object", + "description" : "A POST link related to a payment", + "properties" : { + "href" : { + "type" : "string", + "description" : "A URL that lets you perform additional actions to this payment when combined with the associated `method`.", + "example" : "https://an.example.link/from/payment/platform", + "readOnly" : true + }, + "method" : { + "type" : "string", + "example" : "POST", + "readOnly" : true + }, + "params" : { + "type" : "object", + "additionalProperties" : { + "type" : "object", + "example" : { + "description" : "This is a value for a parameter called description" + } + }, + "example" : { + "description" : "This is a value for a parameter called description" + } + }, + "type" : { + "type" : "string", + "example" : "application/x-www-form-urlencoded" + } + } + }, + "PrefilledCardholderDetails" : { + "type" : "object", + "description" : "prefilled_cardholder_details", + "properties" : { + "billing_address" : { + "$ref" : "#/components/schemas/Address" + }, + "cardholder_name" : { + "type" : "string", + "description" : "The cardholder name you prefilled when you created this payment.", + "example" : "J. Bogs", + "maxLength" : 255, + "minLength" : 0 + } + } + }, + "Refund" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/RefundLinksForSearch" + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The amount refunded to the user in pence.", + "example" : 120, + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "description" : "The date and time you created this refund. This value uses Coordinated Universal Time (UTC) and ISO 8601 format - `YYYY-MM-DDThh:mm:ss.SSSZ`.", + "example" : "2017-01-10T16:52:07.855Z", + "readOnly" : true + }, + "refund_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this refund when you created it.", + "example" : "act4c33g40j3edfmi8jknab84x", + "readOnly" : true + }, + "settlement_summary" : { + "$ref" : "#/components/schemas/RefundSettlementSummary" + }, + "status" : { + "type" : "string", + "description" : "The [status of the refund](https://docs.payments.service.gov.uk/refunding_payments/#checking-the-status-of-a-refund-status).", + "enum" : [ "submitted", "success", "error" ], + "example" : "success", + "readOnly" : true + } + } + }, + "RefundDetailForSearch" : { + "type" : "object", + "description" : "Contains the refunds matching your search criteria.", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/RefundLinksForSearch" + }, + "amount" : { + "type" : "integer", + "format" : "int64", + "description" : "The amount refunded to the user in pence.", + "example" : 120, + "readOnly" : true + }, + "created_date" : { + "type" : "string", + "description" : "The date and time you created this refund. This value uses Coordinated Universal Time (UTC) and ISO 8601 format - `YYYY-MM-DDThh:mm:ss.SSSZ`.", + "example" : "2017-01-10T16:52:07.855Z", + "readOnly" : true + }, + "refund_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay automatically associated with this refund when you created it.", + "example" : "act4c33g40j3edfmi8jknab84x", + "readOnly" : true + }, + "settlement_summary" : { + "$ref" : "#/components/schemas/RefundSettlementSummary" + }, + "status" : { + "type" : "string", + "description" : "The [status of the refund](https://docs.payments.service.gov.uk/refunding_payments/#checking-the-status-of-a-refund-status).", + "enum" : [ "submitted", "success", "error" ], + "example" : "success", + "readOnly" : true + } + } + }, + "RefundForSearchResult" : { + "type" : "object", + "properties" : { + "_embedded" : { + "$ref" : "#/components/schemas/EmbeddedRefunds" + }, + "_links" : { + "$ref" : "#/components/schemas/RefundLinksForSearch" + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93" + } + } + }, + "RefundLinksForSearch" : { + "type" : "object", + "description" : "links for search refunds resource", + "properties" : { + "payment" : { + "$ref" : "#/components/schemas/Link" + }, + "self" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "RefundSearchResults" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/SearchNavigationLinks" + }, + "count" : { + "type" : "integer", + "format" : "int32", + "description" : "Number of refunds on the current page of search results.", + "example" : 20 + }, + "page" : { + "type" : "integer", + "format" : "int32", + "description" : "The [page of results](payments.service.gov.uk/api_reference/#pagination) you’re viewing. To view other pages, make this request again using the `page` parameter.", + "example" : 1 + }, + "results" : { + "type" : "array", + "description" : "Contains the refunds matching your search criteria.", + "items" : { + "$ref" : "#/components/schemas/RefundDetailForSearch" + } + }, + "total" : { + "type" : "integer", + "format" : "int32", + "description" : "Number of refunds matching your search criteria.", + "example" : 100 + } + } + }, + "RefundSettlementSummary" : { + "type" : "object", + "description" : "A structure representing information about a settlement for refunds", + "properties" : { + "settled_date" : { + "type" : "string", + "description" : "The date Stripe took the refund from a payout to your bank account. `settled_date` only appears if Stripe has taken the refund. This value uses Coordinated Universal Time (UTC) and ISO 8601 format - `YYYY-MM-DD`.", + "example" : "2016-01-21", + "readOnly" : true + } + }, + "readOnly" : true + }, + "RefundSummary" : { + "type" : "object", + "description" : "A structure representing the refunds availability", + "properties" : { + "amount_available" : { + "type" : "integer", + "format" : "int64", + "description" : "How much you can refund to the user, in pence.", + "example" : 100, + "readOnly" : true + }, + "amount_submitted" : { + "type" : "integer", + "format" : "int64", + "description" : "How much you’ve already refunded to the user, in pence.", + "readOnly" : true + }, + "status" : { + "type" : "string", + "description" : "Whether you can [refund the payment](https://docs.payments.service.gov.uk/refunding_payments/#checking-the-status-of-a-refund-status).", + "example" : "available" + } + } + }, + "RefundsResponse" : { + "type" : "object", + "properties" : { + "_links" : { + "$ref" : "#/components/schemas/RefundLinksForSearch" + }, + "payment_id" : { + "type" : "string", + "description" : "The unique ID GOV.UK Pay associated with this payment when you created it.", + "example" : "hu20sqlact5260q2nanm0q8u93" + } + } + }, + "RequestError" : { + "type" : "object", + "description" : "A Request Error response", + "properties" : { + "code" : { + "type" : "string", + "description" : "An [API error code](https://docs.payments.service.gov.uk/api_reference/#gov-uk-pay-api-error-codes)that explains why the payment failed.

    `code` only appears if the payment failed.", + "example" : "P0102" + }, + "description" : { + "type" : "string", + "description" : "Additional details about the error.", + "example" : "Invalid attribute value: amount. Must be less than or equal to 10000000" + }, + "field" : { + "type" : "string", + "description" : "The parameter in your request that's causing the error.", + "example" : "amount" + }, + "header" : { + "type" : "string", + "description" : "The header in your request that's causing the error.", + "example" : "Idempotency-Key" + } + } + }, + "SearchNavigationLinks" : { + "type" : "object", + "description" : "Links to navigate through pages of your search.", + "properties" : { + "first_page" : { + "$ref" : "#/components/schemas/Link" + }, + "last_page" : { + "$ref" : "#/components/schemas/Link" + }, + "next_page" : { + "$ref" : "#/components/schemas/Link" + }, + "prev_page" : { + "$ref" : "#/components/schemas/Link" + }, + "self" : { + "$ref" : "#/components/schemas/Link" + } + } + }, + "SettlementSummary" : { + "type" : "object", + "description" : "Contains information about when a lost dispute was settled. A dispute is settled when your payment service provider takes it from a payout to your bank account. 'settlement_summary' only appears if you lost the dispute.", + "properties" : { + "settled_date" : { + "type" : "string", + "description" : "The date your payment service provider took the disputed payment and dispute fee from a payout to your bank account. This value appears in ISO 8601 format - `YYYY-MM-DD`. `settled_date` only appears if you lost the dispute.", + "example" : "2022-07-28", + "readOnly" : true + } + } + }, + "ThreeDSecure" : { + "type" : "object", + "description" : "Object containing information about the 3D Secure authentication of the payment.", + "properties" : { + "required" : { + "type" : "boolean", + "description" : "Indicates if this payment was authorised with 3D Secure authentication. `required` is `true` if the payment required 3D Secure authentication." + } + } + } + }, + "securitySchemes" : { + "BearerAuth" : { + "description" : "GOV.UK Pay authenticates API calls with [OAuth2 HTTP bearer tokens](http://tools.ietf.org/html/rfc6750). You need to use an `\"Authorization\"` HTTP header to provide your API key, with a `\"Bearer\"` prefix. For example: `Authorization: Bearer {YOUR_API_KEY_HERE}`", + "scheme" : "bearer", + "type" : "http" + } + } + } +} \ No newline at end of file diff --git a/scripts/dist.py b/scripts/dist.py index 3c4ad0e17..f4a53f39d 100755 --- a/scripts/dist.py +++ b/scripts/dist.py @@ -202,6 +202,10 @@ def build_jdk_11_maven(): copy(folder + "/cs/rest-gui/market/market-rest/target/market-sut.jar", DIST) copy(folder + "/em/external/rest/market/target/market-evomaster-runner.jar", DIST) + copy(folder + "/cs/rest/pay-publicapi/target/pay-publicapi-sut.jar", DIST) + copy(folder + "/em/external/rest/pay-publicapi/target/pay-publicapi-evomaster-runner.jar", DIST) + + ind1 = os.environ.get('SUT_LOCATION_IND1', '') if ind1 == '': print("\nWARN: SUT_LOCATION_IND1 env variable is not defined") @@ -218,10 +222,12 @@ def build_jdk_17_maven(): copy(folder + "/cs/web/spring-petclinic/target/spring-petclinic-sut.jar", DIST) copy(folder + "/em/external/web/spring-petclinic/target/spring-petclinic-evomaster-runner.jar", DIST) - copy(folder + "/cs/grpc/signal-registration/target/signal-registration-sut.jar", DIST) copy(folder + "/em/external/grpc/signal-registration/target/signal-registration-evomaster-runner.jar", DIST) + copy(folder + "/cs/rest/familie-ba-sak/target/familie-ba-sak-sut.jar", DIST) + copy(folder + "/em/external/rest/familie-ba-sak/target/familie-ba-sak-evomaster-runner.jar", DIST) + #################### def build_jdk_11_gradle(): diff --git a/statistics/data.csv b/statistics/data.csv index 12cab8099..5ac5daf0d 100644 --- a/statistics/data.csv +++ b/statistics/data.csv @@ -1,4 +1,6 @@ EMB,NAME,TYPE,LANGUAGE,RUNTIME,BUILD,FILES,LOCS,DATABASE,LICENSE,ENDPOINTS,URL +TRUE,familie-ba-sak,REST,Kotlin,JDK 17,Maven,1089,143556,PostgreSQL,MIT,183,https://github.com/navikt/familie-ba-sak +TRUE,pay-publicapi,REST,Java,JDK 11,Maven,377,34576,Redis,MIT,10,https://github.com/alphagov/pay-publicapi TRUE,session-service,REST,Java,JDK 8,Maven,15,1471,MongoDB,UNDEFINED,8,https://github.com/cBioPortal/session-service TRUE,bibliothek,REST,Java,JDK 17,Gradle,33,2176,MongoDB,MIT,8,https://github.com/PaperMC/bibliothek TRUE,reservations-api,REST,Java,JDK 11,Gradle,39,1853,MongoDB,UNDEFINED,7,https://github.com/cyrilgavala/reservations-api diff --git a/statistics/table_emb.md b/statistics/table_emb.md index 6f2f6355b..9dd64306a 100644 --- a/statistics/table_emb.md +++ b/statistics/table_emb.md @@ -10,6 +10,7 @@ |__grpc-ncs__|gRPC|638|9|6|Java|JDK 8|Maven|| |__languagetool__|REST|174781|1385|2|Java|JDK 8|Maven|| |__ocvn__|REST|45521|526|258|Java|JDK 8|Maven|H2, MongoDB| +|__pay-publicapi__|REST|34576|377|10|Java|JDK 11|Maven|Redis| |__genome-nexus__|REST|30004|405|23|Java|JDK 8|Maven|MongoDB| |__market__|REST|9861|124|13|Java|JDK 11|Maven|H2| |__scout-api__|REST|9736|93|49|Java|JDK 8|Maven|H2| @@ -24,6 +25,7 @@ |__session-service__|REST|1471|15|8|Java|JDK 8|Maven|MongoDB| |__rest-scs__|REST|862|13|11|Java|JDK 8|Maven|| |__rest-ncs__|REST|605|9|6|Java|JDK 8|Maven|| +|__familie-ba-sak__|REST|143556|1089|183|Kotlin|JDK 17|Maven|PostgreSQL| |__rest-news__|REST|857|11|7|Kotlin|JDK 8|Maven|H2| |__thrift-scs__|Thrift|772|14|11|Java|JDK 8|Maven|| |__thrift-ncs__|Thrift|585|9|6|Java|JDK 8|Maven||